From 76b25c2e26ef68442de627ca8e4890a51879a33e Mon Sep 17 00:00:00 2001 From: Ievgenii Gryshkun Date: Fri, 15 Mar 2019 12:04:47 +0200 Subject: [PATCH 01/19] Improve coverage for mutation addSimpleProductToCart or Cart contains a product after product got status Out of Stock --- .../Model/Cart/AddProductsToCart.php | 8 +++---- .../CatalogInventory/AddProductToCartTest.php | 23 +++++++++++++++++++ 2 files changed, 27 insertions(+), 4 deletions(-) diff --git a/app/code/Magento/QuoteGraphQl/Model/Cart/AddProductsToCart.php b/app/code/Magento/QuoteGraphQl/Model/Cart/AddProductsToCart.php index 005cf3a10ca8..02db4a4fc3bb 100644 --- a/app/code/Magento/QuoteGraphQl/Model/Cart/AddProductsToCart.php +++ b/app/code/Magento/QuoteGraphQl/Model/Cart/AddProductsToCart.php @@ -50,16 +50,16 @@ public function __construct( */ public function execute(Quote $cart, array $cartItems): void { - foreach ($cartItems as $cartItemData) { - $this->addProductToCart->execute($cart, $cartItemData); - } - if ($cart->getData('has_error')) { throw new GraphQlInputException( __('Shopping cart error: %message', ['message' => $this->getCartErrors($cart)]) ); } + foreach ($cartItems as $cartItemData) { + $this->addProductToCart->execute($cart, $cartItemData); + } + $this->cartRepository->save($cart); } diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/CatalogInventory/AddProductToCartTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/CatalogInventory/AddProductToCartTest.php index 17c2af8dc59d..889ef9683891 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/CatalogInventory/AddProductToCartTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/CatalogInventory/AddProductToCartTest.php @@ -58,6 +58,29 @@ public function testAddProductIfQuantityIsNotAvailable() self::fail('Should be "The requested qty is not available" error message.'); } + /** + * @magentoApiDataFixture Magento/Catalog/_files/products.php + * @magentoApiDataFixture Magento/Checkout/_files/active_quote.php + * @expectedException \Exception + * @expectedExceptionMessage Some of the products are out of stock. + */ + public function testUpdateProductOutOfStockInCart() + { + $sku = 'simple'; + $qty = 1; + $maskedQuoteId = $this->getMaskedQuoteId(); + + $query = $this->getAddSimpleProductQuery($maskedQuoteId, $sku, $qty); + $this->graphQlQuery($query); + + $product = Bootstrap::getObjectManager()->create(\Magento\Catalog\Model\Product::class); + $productId = $product->getIdBySku($sku); + $product->load($productId); + $product->setStockData(['is_in_stock' => 0]); + $product->save(); + + $this->graphQlQuery($query); + } /** * @magentoApiDataFixture Magento/Catalog/_files/products.php * @magentoApiDataFixture Magento/Checkout/_files/active_quote.php From 9bb2932bd9ad1f4d81a5060f3c64ebe45153392d Mon Sep 17 00:00:00 2001 From: Ievgenii Gryshkun Date: Tue, 26 Mar 2019 12:25:59 +0200 Subject: [PATCH 02/19] Improve coverage for mutation addSimpleProductToCart or Cart contains a product after product got status Out of Stock --- .../Magento/QuoteGraphQl/Model/Cart/AddProductsToCart.php | 8 ++++---- .../QuoteGraphQl/Model/Cart/AddSimpleProductToCart.php | 5 +++++ 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/app/code/Magento/QuoteGraphQl/Model/Cart/AddProductsToCart.php b/app/code/Magento/QuoteGraphQl/Model/Cart/AddProductsToCart.php index 02db4a4fc3bb..005cf3a10ca8 100644 --- a/app/code/Magento/QuoteGraphQl/Model/Cart/AddProductsToCart.php +++ b/app/code/Magento/QuoteGraphQl/Model/Cart/AddProductsToCart.php @@ -50,16 +50,16 @@ public function __construct( */ public function execute(Quote $cart, array $cartItems): void { + foreach ($cartItems as $cartItemData) { + $this->addProductToCart->execute($cart, $cartItemData); + } + if ($cart->getData('has_error')) { throw new GraphQlInputException( __('Shopping cart error: %message', ['message' => $this->getCartErrors($cart)]) ); } - foreach ($cartItems as $cartItemData) { - $this->addProductToCart->execute($cart, $cartItemData); - } - $this->cartRepository->save($cart); } diff --git a/app/code/Magento/QuoteGraphQl/Model/Cart/AddSimpleProductToCart.php b/app/code/Magento/QuoteGraphQl/Model/Cart/AddSimpleProductToCart.php index 1b32866ed883..ce1ef465a302 100644 --- a/app/code/Magento/QuoteGraphQl/Model/Cart/AddSimpleProductToCart.php +++ b/app/code/Magento/QuoteGraphQl/Model/Cart/AddSimpleProductToCart.php @@ -65,6 +65,11 @@ public function __construct( */ public function execute(Quote $cart, array $cartItemData): void { + if ($cart->getData('has_error')) { + throw new GraphQlInputException( + __('Shopping cart error: %message', ['message' => $this->getCartErrors($cart)]) + ); + } $sku = $this->extractSku($cartItemData); $qty = $this->extractQty($cartItemData); $customizableOptions = $this->extractCustomizableOptions($cartItemData); From 6df7a812797fbf0db98291208451f6e80adeb4d2 Mon Sep 17 00:00:00 2001 From: Ievgenii Gryshkun Date: Thu, 18 Jul 2019 08:30:43 +0300 Subject: [PATCH 03/19] GraphQl-439: Improve coverage for mutation addSimpleProductToCart or Cart contains a product after product got status Out of Stock --- .../Model/Cart/AddProductsToCart.php | 26 --- .../Model/Cart/AddSimpleProductToCart.php | 5 - .../Model/Resolver/GetCartMessages.php | 72 +++++++++ .../Magento/QuoteGraphQl/etc/schema.graphqls | 9 ++ .../GraphQl/Quote/GetCartMessagesTest.php | 153 ++++++++++++++++++ 5 files changed, 234 insertions(+), 31 deletions(-) create mode 100644 app/code/Magento/QuoteGraphQl/Model/Resolver/GetCartMessages.php create mode 100644 dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/GetCartMessagesTest.php diff --git a/app/code/Magento/QuoteGraphQl/Model/Cart/AddProductsToCart.php b/app/code/Magento/QuoteGraphQl/Model/Cart/AddProductsToCart.php index 005cf3a10ca8..f8d7844dd051 100644 --- a/app/code/Magento/QuoteGraphQl/Model/Cart/AddProductsToCart.php +++ b/app/code/Magento/QuoteGraphQl/Model/Cart/AddProductsToCart.php @@ -8,7 +8,6 @@ namespace Magento\QuoteGraphQl\Model\Cart; use Magento\Framework\GraphQl\Exception\GraphQlInputException; -use Magento\Framework\Message\AbstractMessage; use Magento\Quote\Api\CartRepositoryInterface; use Magento\Quote\Model\Quote; @@ -53,31 +52,6 @@ public function execute(Quote $cart, array $cartItems): void foreach ($cartItems as $cartItemData) { $this->addProductToCart->execute($cart, $cartItemData); } - - if ($cart->getData('has_error')) { - throw new GraphQlInputException( - __('Shopping cart error: %message', ['message' => $this->getCartErrors($cart)]) - ); - } - $this->cartRepository->save($cart); } - - /** - * Collecting cart errors - * - * @param Quote $cart - * @return string - */ - private function getCartErrors(Quote $cart): string - { - $errorMessages = []; - - /** @var AbstractMessage $error */ - foreach ($cart->getErrors() as $error) { - $errorMessages[] = $error->getText(); - } - - return implode(PHP_EOL, $errorMessages); - } } diff --git a/app/code/Magento/QuoteGraphQl/Model/Cart/AddSimpleProductToCart.php b/app/code/Magento/QuoteGraphQl/Model/Cart/AddSimpleProductToCart.php index ce1ef465a302..1b32866ed883 100644 --- a/app/code/Magento/QuoteGraphQl/Model/Cart/AddSimpleProductToCart.php +++ b/app/code/Magento/QuoteGraphQl/Model/Cart/AddSimpleProductToCart.php @@ -65,11 +65,6 @@ public function __construct( */ public function execute(Quote $cart, array $cartItemData): void { - if ($cart->getData('has_error')) { - throw new GraphQlInputException( - __('Shopping cart error: %message', ['message' => $this->getCartErrors($cart)]) - ); - } $sku = $this->extractSku($cartItemData); $qty = $this->extractQty($cartItemData); $customizableOptions = $this->extractCustomizableOptions($cartItemData); diff --git a/app/code/Magento/QuoteGraphQl/Model/Resolver/GetCartMessages.php b/app/code/Magento/QuoteGraphQl/Model/Resolver/GetCartMessages.php new file mode 100644 index 000000000000..fd1beec0de46 --- /dev/null +++ b/app/code/Magento/QuoteGraphQl/Model/Resolver/GetCartMessages.php @@ -0,0 +1,72 @@ +getCartForUser = $getCartForUser; + } + + /** + * @inheritdoc + */ + public function resolve(Field $field, $context, ResolveInfo $info, array $value = null, array $args = null) + { + $cartId = $args['input']['cart_id']; + if (isset($cartId) === false || empty($cartId)) { + throw new GraphQlInputException(__('Required parameter "cart_id" is missing.')); + } + $currentUserId = $context->getUserId(); + $cart = $this->getCartForUser->execute($cartId, $currentUserId); + if (empty($cart->getData('has_error'))) { + throw new GraphQlNoSuchEntityException(__('Requested cart hasn\'t errors.')); + } + return ['messages' => $this->getCartErrors($cart)]; + } + + /** + * Collecting cart errors + * + * @param Quote $cart + * @return array + */ + private function getCartErrors(Quote $cart): array + { + $errorMessages = []; + + /** @var AbstractMessage $error */ + foreach ($cart->getErrors() as $error) { + $errorMessages[] = $error->getText(); + } + + return $errorMessages; + } +} diff --git a/app/code/Magento/QuoteGraphQl/etc/schema.graphqls b/app/code/Magento/QuoteGraphQl/etc/schema.graphqls index 79cea3855f6f..e4aa600fcc52 100644 --- a/app/code/Magento/QuoteGraphQl/etc/schema.graphqls +++ b/app/code/Magento/QuoteGraphQl/etc/schema.graphqls @@ -17,6 +17,7 @@ type Mutation { setBillingAddressOnCart(input: SetBillingAddressOnCartInput): SetBillingAddressOnCartOutput @resolver(class: "\\Magento\\QuoteGraphQl\\Model\\Resolver\\SetBillingAddressOnCart") setShippingMethodsOnCart(input: SetShippingMethodsOnCartInput): SetShippingMethodsOnCartOutput @resolver(class: "\\Magento\\QuoteGraphQl\\Model\\Resolver\\SetShippingMethodsOnCart") setPaymentMethodOnCart(input: SetPaymentMethodOnCartInput): SetPaymentMethodOnCartOutput @resolver(class: "Magento\\QuoteGraphQl\\Model\\Resolver\\SetPaymentMethodOnCart") + getCartMessages(input: GetCartMessagesInput): GetCartMessagesOutput @resolver(class: "\\Magento\\QuoteGraphQl\\Model\\Resolver\\GetCartMessages") @doc(description:"Return all shopping cart messages for a guest or logged in user") } input AddSimpleProductsToCartInput { @@ -124,6 +125,10 @@ input PaymentMethodInput { purchase_order_number: String @doc(description:"Purchase order number") } +input GetCartMessagesInput { + cart_id: String! +} + type SetPaymentMethodOnCartOutput { cart: Cart! } @@ -144,6 +149,10 @@ type ApplyCouponToCartOutput { cart: Cart! } +type GetCartMessagesOutput { + messages: [String]! +} + type Cart { items: [CartItemInterface] @resolver(class: "\\Magento\\QuoteGraphQl\\Model\\Resolver\\CartItems") applied_coupon: AppliedCoupon @resolver(class: "\\Magento\\QuoteGraphQl\\Model\\Resolver\\AppliedCoupon") diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/GetCartMessagesTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/GetCartMessagesTest.php new file mode 100644 index 000000000000..7d12d308f84a --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/GetCartMessagesTest.php @@ -0,0 +1,153 @@ +quoteResource = $objectManager->get(QuoteResource::class); + $this->quoteFactory = $objectManager->get(QuoteFactory::class); + $this->quoteIdToMaskedId = $objectManager->get(QuoteIdToMaskedQuoteIdInterface::class); + } + + /** + * @magentoApiDataFixture Magento/Catalog/_files/products.php + * @magentoApiDataFixture Magento/Checkout/_files/active_quote.php + */ + public function testGetCartMessages() + { + $sku = 'simple'; + $qty = 1; + $maskedQuoteId = $this->getMaskedQuoteId(); + + $queryAddProduct = $this->getAddSimpleProductQuery($maskedQuoteId, $sku, $qty); + $this->graphQlQuery($queryAddProduct); + + $product = Bootstrap::getObjectManager()->create(\Magento\Catalog\Model\Product::class); + $productId = $product->getIdBySku($sku); + $product->load($productId); + $product->setStockData(['is_in_stock' => 0]); + $product->save(); + + $queryGetMessages = $this->getCartMessagesQuery($maskedQuoteId); + $response = $this->graphQlQuery($queryGetMessages); + self::assertEquals('Some of the products are out of stock.', $response['getCartMessages']['messages'][0]); + } + /** + * @expectedException \Exception + * @expectedExceptionMessage Required parameter "cart_id" is missing. + */ + public function testRequiredParamMissing() + { + $maskedQuoteId = ''; + + $query = $this->getCartMessagesQuery($maskedQuoteId); + $this->graphQlQuery($query); + } + + /** + * @expectedException \Exception + * @expectedExceptionMessage Requested cart hasn't errors. + */ + public function testCartWithoutErrors() + { + $maskedQuoteId = $this->getMaskedQuoteId(); + + $query = $this->getCartMessagesQuery($maskedQuoteId); + $this->graphQlQuery($query); + } + + /** + * @return string + */ + public function getMaskedQuoteId() : string + { + $quote = $this->quoteFactory->create(); + $this->quoteResource->load($quote, 'test_order_1', 'reserved_order_id'); + + return $this->quoteIdToMaskedId->execute((int)$quote->getId()); + } + + /** + * @param string $maskedQuoteId + * @return string + */ + public function getCartMessagesQuery(string $maskedQuoteId): string + { + return << Date: Thu, 18 Jul 2019 10:34:30 +0300 Subject: [PATCH 04/19] GraphQl-439: Improve coverage for mutation addSimpleProductToCart or Cart contains a product after product got status Out of Stock #475 --- .../testsuite/Magento/GraphQl/Quote/GetCartMessagesTest.php | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/GetCartMessagesTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/GetCartMessagesTest.php index 7d12d308f84a..03d294b53b10 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/GetCartMessagesTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/GetCartMessagesTest.php @@ -13,6 +13,11 @@ use Magento\Quote\Model\QuoteIdToMaskedQuoteIdInterface; use Magento\Quote\Model\ResourceModel\Quote as QuoteResource; +/** + * Class GetCartMessagesTestTest + * + * @package Magento\GraphQl\Quote + */ class GetCartMessagesTestTest extends GraphQlAbstract { /** @@ -90,6 +95,7 @@ public function testCartWithoutErrors() /** * @return string + * @throws \Magento\Framework\Exception\NoSuchEntityException */ public function getMaskedQuoteId() : string { From f9a57840e5acf2b20adfbc784ab31821abfbfc82 Mon Sep 17 00:00:00 2001 From: Ievgenii Gryshkun Date: Mon, 29 Jul 2019 09:13:15 +0300 Subject: [PATCH 05/19] Improve coverage for mutation addSimpleProductToCart or Cart contains a product after product got status Out of Stock --- .../Magento/GraphQl/CatalogInventory/AddProductToCartTest.php | 4 ++-- .../testsuite/Magento/GraphQl/Quote/GetCartMessagesTest.php | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/CatalogInventory/AddProductToCartTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/CatalogInventory/AddProductToCartTest.php index 6ac7235a0e8a..91982d298c9d 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/CatalogInventory/AddProductToCartTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/CatalogInventory/AddProductToCartTest.php @@ -56,9 +56,9 @@ public function testUpdateProductOutOfStockInCart() { $sku = 'simple'; $qty = 1; - $maskedQuoteId = $this->getMaskedQuoteId(); + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_order_1'); - $query = $this->getAddSimpleProductQuery($maskedQuoteId, $sku, $qty); + $query = $this->getQuery($maskedQuoteId, $sku, $qty); $this->graphQlQuery($query); $product = Bootstrap::getObjectManager()->create(\Magento\Catalog\Model\Product::class); diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/GetCartMessagesTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/GetCartMessagesTest.php index 03d294b53b10..1c3dc2400bf3 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/GetCartMessagesTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/GetCartMessagesTest.php @@ -14,11 +14,11 @@ use Magento\Quote\Model\ResourceModel\Quote as QuoteResource; /** - * Class GetCartMessagesTestTest + * Class GetCartMessagesTest * * @package Magento\GraphQl\Quote */ -class GetCartMessagesTestTest extends GraphQlAbstract +class GetCartMessagesTest extends GraphQlAbstract { /** * @var QuoteResource From 20f0d553d075e95f1a89d62b2acb43330458b888 Mon Sep 17 00:00:00 2001 From: Ievgenii Gryshkun <44254165+XxXgeoXxX@users.noreply.github.com> Date: Wed, 31 Jul 2019 13:07:17 +0300 Subject: [PATCH 06/19] Delete mypatch.patch --- mypatch.patch | 762710 ----------------------------------------------- 1 file changed, 762710 deletions(-) delete mode 100644 mypatch.patch diff --git a/mypatch.patch b/mypatch.patch deleted file mode 100644 index 79342b902e4d..000000000000 --- a/mypatch.patch +++ /dev/null @@ -1,762710 +0,0 @@ -diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md -index dae954a0970..89aea729985 100644 ---- a/.github/CONTRIBUTING.md -+++ b/.github/CONTRIBUTING.md -@@ -1,36 +1,48 @@ - # Contributing to Magento 2 code - - Contributions to the Magento 2 codebase are done using the fork & pull model. --This contribution model has contributors maintaining their own copy of the forked codebase (which can easily be synced with the main copy). The forked repository is then used to submit a request to the base repository to “pull” a set of changes. For more information on pull requests please refer to [GitHub Help](https://help.github.com/articles/about-pull-requests/). -+This contribution model has contributors maintaining their own fork of the Magento 2 repository. -+The forked repository is then used to submit a request to the base repository to “pull” a set of changes. -+For more information on pull requests please refer to [GitHub Help](https://help.github.com/articles/about-pull-requests/). - - Contributions can take the form of new components or features, changes to existing features, tests, documentation (such as developer guides, user guides, examples, or specifications), bug fixes or optimizations. - --The Magento 2 development team will review all issues and contributions submitted by the community of developers in the first in, first out order. During the review we might require clarifications from the contributor. If there is no response from the contributor within two weeks, the pull request will be closed. -+The Magento 2 development team or community maintainers will review all issues and contributions submitted by the community of developers in the first in, first out order. -+During the review we might require clarifications from the contributor. -+If there is no response from the contributor within two weeks, the pull request will be closed. - -+For more detailed information on contribution please read our [beginners guide](https://github.com/magento/magento2/wiki/Getting-Started). - - ## Contribution requirements - --1. Contributions must adhere to the [Magento coding standards](https://devdocs.magento.com/guides/v2.2/coding-standards/bk-coding-standards.html). -+1. Contributions must adhere to the [Magento coding standards](https://devdocs.magento.com/guides/v2.3/coding-standards/bk-coding-standards.html). - 2. Pull requests (PRs) must be accompanied by a meaningful description of their purpose. Comprehensive descriptions increase the chances of a pull request being merged quickly and without additional clarification requests. --3. Commits must be accompanied by meaningful commit messages. Please see the [Magento Pull Request Template](https://github.com/magento/magento2/blob/2.2-develop/.github/PULL_REQUEST_TEMPLATE.md) for more information. -+3. Commits must be accompanied by meaningful commit messages. Please see the [Magento Pull Request Template](https://github.com/magento/magento2/blob/2.3-develop/.github/PULL_REQUEST_TEMPLATE.md) for more information. - 4. PRs which include bug fixes must be accompanied with a step-by-step description of how to reproduce the bug. - 3. PRs which include new logic or new features must be submitted along with: - * Unit/integration test coverage --* Proposed [documentation](http://devdocs.magento.com) updates. Documentation contributions can be submitted via the [devdocs GitHub](https://github.com/magento/devdocs). -+* Proposed [documentation](https://devdocs.magento.com) updates. Documentation contributions can be submitted via the [devdocs GitHub](https://github.com/magento/devdocs). - 4. For larger features or changes, please [open an issue](https://github.com/magento/magento2/issues) to discuss the proposed changes prior to development. This may prevent duplicate or unnecessary effort and allow other contributors to provide input. - 5. All automated tests must pass (all builds on [Travis CI](https://travis-ci.org/magento/magento2) must be green). - - ## Contribution process - --If you are a new GitHub user, we recommend that you create your own [free github account](https://github.com/signup/free). This will allow you to collaborate with the Magento 2 development team, fork the Magento 2 project and send pull requests. -+If you are a new GitHub user, we recommend that you create your own [free github account](https://github.com/signup/free). -+This will allow you to collaborate with the Magento 2 development team, fork the Magento 2 project and send pull requests. - - 1. Search current [listed issues](https://github.com/magento/magento2/issues) (open or closed) for similar proposals of intended contribution before starting work on a new contribution. - 2. Review the [Contributor License Agreement](https://magento.com/legaldocuments/mca) if this is your first time contributing. - 3. Create and test your work. --4. Fork the Magento 2 repository according to the [Fork A Repository instructions](http://devdocs.magento.com/guides/v2.2/contributor-guide/contributing.html#fork) and when you are ready to send us a pull request – follow the [Create A Pull Request instructions](http://devdocs.magento.com/guides/v2.2/contributor-guide/contributing.html#pull_request). -+4. Fork the Magento 2 repository according to the [Fork A Repository instructions](https://devdocs.magento.com/guides/v2.3/contributor-guide/contributing.html#fork) and when you are ready to send us a pull request – follow the [Create A Pull Request instructions](https://devdocs.magento.com/guides/v2.3/contributor-guide/contributing.html#pull_request). - 5. Once your contribution is received the Magento 2 development team will review the contribution and collaborate with you as needed. - - ## Code of Conduct - - Please note that this project is released with a Contributor Code of Conduct. We expect you to agree to its terms when participating in this project. - The full text is available in the repository [Wiki](https://github.com/magento/magento2/wiki/Magento-Code-of-Conduct). -+ -+## Connecting with Community! -+ -+If you have any questions, join us in [#beginners](https://magentocommeng.slack.com/messages/CH8BGFX9D) Slack chat. If you are not on our slack, [click here](http://tinyurl.com/engcom-slack) to join. -+ -+Need to find a project? Check out the [Slack Channels](https://github.com/magento/magento2/wiki/Slack-Channels) (with listed project info) and the [Magento Community Portal](https://opensource.magento.com/). -diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md -index 12ad4e452b1..2b1720ccaab 100644 ---- a/.github/ISSUE_TEMPLATE.md -+++ b/.github/ISSUE_TEMPLATE.md -@@ -5,11 +5,12 @@ - - Information on your environment, - - Steps to reproduce, - - Expected and actual results, -+ Fields marked with (*) are required. Please don't remove the template. - - Please also have a look at our guidelines article before adding a new issue https://github.com/magento/magento2/wiki/Issue-reporting-guidelines - --> - --### Preconditions -+### Preconditions (*) - - 1. [Screenshots, logs or description] - --### Actual result -+### Actual result (*) - - 1. [Screenshots, logs or description] -diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md -new file mode 100644 -index 00000000000..33a6ef02ace ---- /dev/null -+++ b/.github/ISSUE_TEMPLATE/bug_report.md -@@ -0,0 +1,34 @@ -+--- -+name: Bug report -+about: Technical issue with the Magento 2 core components -+ -+--- -+ -+ -+ -+### Preconditions (*) -+ -+1. -+2. -+ -+### Steps to reproduce (*) -+ -+1. -+2. -+ -+### Expected result (*) -+ -+1. [Screenshots, logs or description] -+2. -+ -+### Actual result (*) -+ -+1. [Screenshots, logs or description] -+2. -diff --git a/.github/ISSUE_TEMPLATE/developer-experience-issue.md b/.github/ISSUE_TEMPLATE/developer-experience-issue.md -new file mode 100644 -index 00000000000..423d4818fb3 ---- /dev/null -+++ b/.github/ISSUE_TEMPLATE/developer-experience-issue.md -@@ -0,0 +1,19 @@ -+--- -+name: Developer experience issue -+about: Issues related to customization, extensibility, modularity -+ -+--- -+ -+ -+ -+### Summary (*) -+ -+ -+### Examples (*) -+ -+ -+### Proposed solution -+ -diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md -new file mode 100644 -index 00000000000..f64185773ca ---- /dev/null -+++ b/.github/ISSUE_TEMPLATE/feature_request.md -@@ -0,0 +1,21 @@ -+--- -+name: Feature request -+about: Please consider reporting directly to https://github.com/magento/community-features -+ -+--- -+ -+ -+ -+### Description (*) -+ -+ -+### Expected behavior (*) -+ -+ -+### Benefits -+ -+ -+### Additional information -+ -diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md -index 5b0b9d74e45..11da06ee704 100644 ---- a/.github/PULL_REQUEST_TEMPLATE.md -+++ b/.github/PULL_REQUEST_TEMPLATE.md -@@ -3,12 +3,13 @@ - To help us process this pull request we recommend that you add the following information: - - Summary of the pull request, - - Issue(s) related to the changes made, -- - Manual testing scenarios, -+ - Manual testing scenarios -+ Fields marked with (*) are required. Please don't remove the template. - --> - - - --### Description -+### Description (*) - - 1. magento/magento2#: Issue title --2. ... - --### Manual testing scenarios -+### Manual testing scenarios (*) - -+ -+### Contribution checklist (*) - - [ ] Pull request has a meaningful description of its purpose - - [ ] All commits are accompanied by meaningful commit messages - - [ ] All new or changed code is covered with unit/integration tests (if applicable) -- - [ ] All automated tests passed successfully (all builds on Travis CI are green) -+ - [ ] All automated tests passed successfully (all builds are green) -diff --git a/.gitignore b/.gitignore -index 8831ad0a17c..8ec1104f255 100644 ---- a/.gitignore -+++ b/.gitignore -@@ -33,7 +33,6 @@ atlassian* - /.php_cs - /.php_cs.cache - /grunt-config.json --/dev/tools/grunt/configs/local-themes.js - /pub/media/*.* - !/pub/media/.htaccess - /pub/media/attribute/* -@@ -49,6 +48,8 @@ atlassian* - /pub/media/import/* - !/pub/media/import/.htaccess - /pub/media/logo/* -+/pub/media/custom_options/* -+!/pub/media/custom_options/.htaccess - /pub/media/theme/* - /pub/media/theme_customization/* - !/pub/media/theme_customization/.htaccess -diff --git a/.htaccess b/.htaccess -index d22b5a1395c..71a5cf708db 100644 ---- a/.htaccess -+++ b/.htaccess -@@ -27,6 +27,11 @@ - #AddType x-mapp-php5 .php - #AddHandler x-mapp-php5 .php - -+############################################ -+## enable usage of methods arguments in backtrace -+ -+ SetEnv MAGE_DEBUG_SHOW_ARGS 1 -+ - ############################################ - ## default index file - -@@ -364,6 +369,15 @@ - Require all denied - - -+ -+ -+ order allow,deny -+ deny from all -+ -+ = 2.4> -+ Require all denied -+ -+ - - # For 404s and 403s that aren't handled by the application, show plain 404 response - ErrorDocument 404 /pub/errors/404.php -diff --git a/.htaccess.sample b/.htaccess.sample -index c9ddff2cca4..c9e83a53cc8 100644 ---- a/.htaccess.sample -+++ b/.htaccess.sample -@@ -27,6 +27,11 @@ - #AddType x-mapp-php5 .php - #AddHandler x-mapp-php5 .php - -+############################################ -+## enable usage of methods arguments in backtrace -+ -+ SetEnv MAGE_DEBUG_SHOW_ARGS 1 -+ - ############################################ - ## default index file - -@@ -341,6 +346,15 @@ - Require all denied - - -+ -+ -+ order allow,deny -+ deny from all -+ -+ = 2.4> -+ Require all denied -+ -+ - - # For 404s and 403s that aren't handled by the application, show plain 404 response - ErrorDocument 404 /pub/errors/404.php -diff --git a/.travis.yml b/.travis.yml -deleted file mode 100644 -index cc730ca5a2c..00000000000 ---- a/.travis.yml -+++ /dev/null -@@ -1,65 +0,0 @@ --sudo: required --dist: trusty --group: edge --addons: -- apt: -- packages: -- - mysql-server-5.6 -- - mysql-client-core-5.6 -- - mysql-client-5.6 -- - postfix -- firefox: "46.0" -- hosts: -- - magento2.travis --services: -- - rabbitmq -- - elasticsearch --language: php --php: -- - 7.1 -- - 7.2 --env: -- global: -- - COMPOSER_BIN_DIR=~/bin -- - INTEGRATION_SETS=3 -- - NODE_JS_VERSION=8 -- - MAGENTO_HOST_NAME="magento2.travis" -- matrix: -- - TEST_SUITE=unit -- - TEST_SUITE=static -- - TEST_SUITE=js GRUNT_COMMAND=spec -- - TEST_SUITE=js GRUNT_COMMAND=static -- - TEST_SUITE=integration INTEGRATION_INDEX=1 -- - TEST_SUITE=integration INTEGRATION_INDEX=2 -- - TEST_SUITE=integration INTEGRATION_INDEX=3 -- - TEST_SUITE=functional --matrix: -- exclude: -- - php: 7.1 -- env: TEST_SUITE=static -- - php: 7.1 -- env: TEST_SUITE=js GRUNT_COMMAND=spec -- - php: 7.1 -- env: TEST_SUITE=js GRUNT_COMMAND=static -- - php: 7.1 -- env: TEST_SUITE=functional --cache: -- apt: true -- directories: -- - $HOME/.composer/cache -- - $HOME/.nvm -- - $HOME/node_modules -- - $HOME/yarn.lock --before_install: -- - curl -O https://download.elastic.co/elasticsearch/release/org/elasticsearch/distribution/deb/elasticsearch/2.3.0/elasticsearch-2.3.0.deb && sudo dpkg -i --force-confnew elasticsearch-2.3.0.deb && sudo service elasticsearch restart -- - ./dev/travis/before_install.sh --install: composer install --no-interaction --before_script: ./dev/travis/before_script.sh --script: -- # Set arguments for variants of phpunit based tests; '|| true' prevents failing script when leading test fails -- - test $TEST_SUITE = "functional" && TEST_FILTER='dev/tests/functional/testsuites/Magento/Mtf/TestSuite/InjectableTests.php' || true -- -- # The scripts for grunt/phpunit type tests -- - if [ $TEST_SUITE == "functional" ]; then dev/tests/functional/vendor/phpunit/phpunit/phpunit -c dev/tests/$TEST_SUITE $TEST_FILTER; fi -- - if [ $TEST_SUITE != "functional" ] && [ $TEST_SUITE != "js" ]; then phpunit -c dev/tests/$TEST_SUITE $TEST_FILTER; fi -- - if [ $TEST_SUITE == "js" ]; then grunt $GRUNT_COMMAND; fi -diff --git a/.travis.yml.sample b/.travis.yml.sample -new file mode 100644 -index 00000000000..76885ebab28 ---- /dev/null -+++ b/.travis.yml.sample -@@ -0,0 +1,68 @@ -+sudo: required -+dist: trusty -+group: edge -+addons: -+ apt: -+ packages: -+ - mysql-server-5.6 -+ - mysql-client-core-5.6 -+ - mysql-client-5.6 -+ - postfix -+ firefox: "46.0" -+ hosts: -+ - magento2.travis -+services: -+ - rabbitmq -+ - elasticsearch -+language: php -+php: -+ - 7.1 -+ - 7.2 -+env: -+ global: -+ - COMPOSER_BIN_DIR=~/bin -+ - INTEGRATION_SETS=3 -+ - NODE_JS_VERSION=8 -+ - MAGENTO_HOST_NAME="magento2.travis" -+ matrix: -+ - TEST_SUITE=unit -+ - TEST_SUITE=static -+ - TEST_SUITE=js GRUNT_COMMAND=spec -+ - TEST_SUITE=js GRUNT_COMMAND=static -+ - TEST_SUITE=integration INTEGRATION_INDEX=1 -+ - TEST_SUITE=integration INTEGRATION_INDEX=2 -+ - TEST_SUITE=integration INTEGRATION_INDEX=3 -+ - TEST_SUITE=functional -+ - TEST_SUITE=graphql-api-functional -+matrix: -+ exclude: -+ - php: 7.1 -+ env: TEST_SUITE=static -+ - php: 7.1 -+ env: TEST_SUITE=js GRUNT_COMMAND=spec -+ - php: 7.1 -+ env: TEST_SUITE=js GRUNT_COMMAND=static -+ - php: 7.1 -+ env: TEST_SUITE=functional -+ - php: 7.1 -+ env: TEST_SUITE=graphql-api-functional -+cache: -+ apt: true -+ directories: -+ - $HOME/.composer/cache -+ - $HOME/.nvm -+ - $HOME/node_modules -+ - $HOME/yarn.lock -+before_install: -+ - ./dev/travis/before_install.sh -+install: composer install --no-interaction -+before_script: ./dev/travis/before_script.sh -+script: -+ # Set arguments for variants of phpunit based tests; '|| true' prevents failing script when leading test fails -+ - test $TEST_SUITE = "functional" && TEST_FILTER='dev/tests/functional/testsuites/Magento/Mtf/TestSuite/InjectableTests.php' || true -+ -+ # The scripts for grunt/phpunit type tests -+ - if [ $TEST_SUITE == "functional" ]; then dev/tests/functional/vendor/phpunit/phpunit/phpunit -c dev/tests/$TEST_SUITE $TEST_FILTER; fi -+ - if [ $TEST_SUITE != "functional" ] && [ $TEST_SUITE != "js" ] && [ $TEST_SUITE != "graphql-api-functional" ]; then phpunit -c dev/tests/$TEST_SUITE $TEST_FILTER; fi -+ - if [ $TEST_SUITE == "js" ]; then grunt $GRUNT_COMMAND; fi -+ - if [ $TEST_SUITE == "graphql-api-functional" ]; then phpunit -c dev/tests/api-functional; fi -diff --git a/CHANGELOG.md b/CHANGELOG.md -index 078b93bd719..0999bee6ea4 100644 ---- a/CHANGELOG.md -+++ b/CHANGELOG.md -@@ -1,6 +1,10 @@ -+2.3.0 -+============= -+To get detailed information about changes in Magento 2.3.0, see the [Release Notes](https://devdocs.magento.com/guides/v2.3/release-notes/bk-release-notes.html) -+ - 2.1.0 - ============= --To get detailed information about changes in Magento 2.1.0, please visit [Magento Community Edition (CE) Release Notes](http://devdocs.magento.com/guides/v2.1/release-notes/ReleaseNotes2.1.0CE.html "Magento Community Edition (CE) Release Notes") -+To get detailed information about changes in Magento 2.1.0, please visit [Magento Community Edition (CE) Release Notes](https://devdocs.magento.com/guides/v2.1/release-notes/ReleaseNotes2.1.0CE.html "Magento Community Edition (CE) Release Notes") - - 2.0.0 - ============= -@@ -153,7 +157,7 @@ To get detailed information about changes in Magento 2.1.0, please visit [Magent - * Updated styles - * Sample Data: - * Improved sample data installation UX -- * Updated sample data with Product Heros, color swatches, MAP and rule based product relations -+ * Updated sample data with Product Heroes, color swatches, MAP and rule based product relations - * Improved sample data upgrade flow - * Added the ability to log errors and set the error flag during sample data installation - * Various improvements: -@@ -485,7 +489,7 @@ Tests: - * Fixed an issue where found records in global search in Backend could not be selected via keyboard - * Fixed an issue where Category menu items went out of screen when page side was reached - * Fixed an issue where subcategories in menu were shown instantly when user moved mouse quickly -- * Fixed an issue where popup header was our of window range while creating group product -+ * Fixed an issue where popup header was out of window range while creating group product - * Fixed an issue where region field was absent in customer address form on backend for "United Kingdom" country - * Fixed an ability to edit the Order from Admin panel - * Fixed an issue where email could not be retrieved from \Magento\Quote\Api\Data\AddressInterface after adding an address on OnePageCheckout -@@ -622,7 +626,7 @@ Tests: - * Fixed an issue where filters were not shown on product reviews report grid - * Fixed an issue where second customer address was not deleted from customer account - * Fixed an issue where custom options pop-up was still displayed after submit -- * Fixed an issue where Second Product was not added to Shopping Cart from Wishlist at first atempt -+ * Fixed an issue where Second Product was not added to Shopping Cart from Wishlist at first attempt - * Fixed an issue where customer invalid email message was not displayed - * Fixed an issue where All Access Tokens for Customer without Tokens could not be revoked - * Fixed an issue where it was impossible to add Product to Shopping Cart from shared Wishlist -@@ -781,7 +785,7 @@ Tests: - * Refactored controller actions in the Product area - * Moved commands cache.php, indexer.php, log.php, test.php, compiler.php, singletenant\_compiler.php, generator.php, pack.php, deploy.php and file\_assembler.php to the new bin/magento CLI framework - * Data Migration Tool -- * The Data Migraiton Tool is published in the separate [repository](https://github.com/magento/data-migration-tool-ce "Data Migration Tool repository") -+ * The Data Migration Tool is published in the separate [repository](https://github.com/magento/data-migration-tool-ce "Data Migration Tool repository") - * Fixed bugs - * Fixed an issue where error appeared during placing order with virtual product - * Fixed an issue where billing and shipping sections didn't contain address information on order print -@@ -1021,7 +1025,7 @@ Tests: - * Improved backend menu keyboard accessibility - * Accessibility improvements: WAI-ARIA in a product item on a category page and related products - * Checkout flow code can work with a separate DB storage -- * Unit tests moved to module directories -+ * Unit tests moved to module directories - * Addressed naming inconsistencies in REST routes - * Added Advanced Developer workflow for frontend developers - * Setup -@@ -2280,7 +2284,7 @@ Tests: - * Fixed an issue where no results were found for Coupons reports - * Fixed an issue with incremental Qty setting - * Fixed an issue with allowing importing of negative weight values -- * Fixed an issue with Inventory - Only X left Treshold being not dependent on Qty for Item's Status to Become Out of Stock -+ * Fixed an issue with Inventory - Only X left Threshold being not dependent on Qty for Item's Status to Become Out of Stock - * Fixed an issue where the "Catalog Search Index index was rebuilt." message was displayed when reindexing the Catalog Search index - * Search module: - * Integrated the Search library to the advanced search functionality -@@ -2702,7 +2706,7 @@ Tests: - * Ability to support extensible service data objects - * No Code Duplication in Root Templates - * Fixed bugs: -- * Persistance session application. Loggin out the customer -+ * Persistence session application. Logging out the customer - * Placing the order with two terms and conditions - * Saving of custom option by service catalogProductCustomOptionsWriteServiceV1 - * Placing the order on frontend if enter in the street address line 1 and 2 255 symbols -@@ -2961,7 +2965,7 @@ Tests: - * Fixed an issue with incorrect items label for the cases when there are more than one item in the category - * Fixed an issue when configurable product was out of stock in Google Shopping while being in stock in the Magento backend - * Fixed an issue when swipe gesture in menu widget was not supported on mobile -- * Fixed an issue when it was impossible to enter alpha-numeric zip code on the stage of estimating shipping and tax rates -+ * Fixed an issue when it was impossible to enter alphanumeric zip code on the stage of estimating shipping and tax rates - * Fixed an issue when custom price was not applied when editing an order - * Fixed an issue when items were not returned to stock after unsuccessful order was placed - * Fixed an issue when error message appeared "Cannot save the credit memo” while creating credit memo -@@ -4132,7 +4136,7 @@ Tests: - * Moved Multishipping functionality to newly created module Multishipping - * Extracted Product duplication behavior from Product model to Product\Copier model - * Replaced event "catalog_model_product_duplicate" with composite Product\Copier model -- * Replaced event "catalog_product_prepare_save" with controller product initialization helper that can be customozed via plugins -+ * Replaced event "catalog_product_prepare_save" with controller product initialization helper that can be customized via plugins - * Consolidated Authorize.Net functionality in single module Authorizenet - * Eliminated dependency of Sales module on Shipping and Usa modules - * Eliminated dependency of Shipping module on Customer module -@@ -4331,7 +4335,7 @@ Tests: - * Fixed order placing with virtual product using Express Checkout - * Fixed the error during order placement with Recurring profile payment - * Fixed wrong redirect after customer registration during multishipping checkout -- * Fixed inability to crate shipping labels -+ * Fixed inability to create shipping labels - * Fixed inability to switch language, if the default language is English - * Fixed an issue with incorrect XML appearing in cache after some actions on the frontend - * Fixed product export -diff --git a/README.md b/README.md -index 6b2ac458eb4..73154c18d89 100644 ---- a/README.md -+++ b/README.md -@@ -1,19 +1,17 @@ --[![Build Status](https://travis-ci.org/magento/magento2.svg?branch=2.3-develop)](https://travis-ci.org/magento/magento2) - [![Open Source Helpers](https://www.codetriage.com/magento/magento2/badges/users.svg)](https://www.codetriage.com/magento/magento2) - [![Gitter](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/magento/magento2?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge) --[![Crowdin](https://d322cqt584bo4o.cloudfront.net/magento-2/localized.png)](https://crowdin.com/project/magento-2) -+[![Crowdin](https://d322cqt584bo4o.cloudfront.net/magento-2/localized.svg)](https://crowdin.com/project/magento-2) -

Welcome

- Welcome to Magento 2 installation! We're glad you chose to install Magento 2, a cutting-edge, feature-rich eCommerce solution that gets results. - --## Magento system requirements --[Magento system requirements](http://devdocs.magento.com/guides/v2.3/install-gde/system-requirements2.html) -+## Magento System Requirements -+[Magento System Requirements](https://devdocs.magento.com/guides/v2.3/install-gde/system-requirements.html). - - ## Install Magento --To install Magento, see either: - --* [Installation guide](http://devdocs.magento.com/guides/v2.3/install-gde/bk-install-guide.html) -+* [Installation Guide](https://devdocs.magento.com/guides/v2.3/install-gde/bk-install-guide.html). - --

Contributing to the Magento 2 code base

-+

Contributing to the Magento 2 Code Base

- Contributions can take the form of new components or features, changes to existing features, tests, documentation (such as developer guides, user guides, examples, or specifications), bug fixes, optimizations, or just good suggestions. - - To learn about how to make a contribution, click [here][1]. -@@ -22,10 +20,10 @@ To learn about issues, click [here][2]. To open an issue, click [here][3]. - - To suggest documentation improvements, click [here][4]. - --[1]: --[2]: -+[1]: -+[2]: - [3]: --[4]: -+[4]: - -

Community Maintainers

- The members of this team have been recognized for their outstanding commitment to maintaining and improving Magento. Magento has granted them permission to accept, merge, and reject pull requests, as well as review issues, and thanks these Community Maintainers for their valuable contributions. -@@ -40,35 +38,33 @@ Magento is thankful for any contribution that can improve our code base, documen - - - --

Labels applied by the Magento team

-+### Labels Applied by the Magento Team -+We apply labels to public Pull Requests and Issues to help other participants retrieve additional information about current progress, component assignments, Magento release lines, and much more. -+Please review the [Code Contributions guide](https://devdocs.magento.com/guides/v2.3/contributor-guide/contributing.html#labels) for detailed information on labels used in Magento 2 repositories. - --| Label | Description | --| ------------- |-------------| --| ![DOC](http://devdocs.magento.com/common/images/github_DOC.png) | Affects Documentation domain. | --| ![PROD](http://devdocs.magento.com/common/images/github_PROD.png) | Affects the Product team (mostly feature requests or business logic change). | --| ![TECH](http://devdocs.magento.com/common/images/github_TECH.png) | Affects Architect Group (mostly to make decisions around technology changes). | --| ![accept](http://devdocs.magento.com/common/images/github_accept.png) | The pull request has been accepted and will be merged into mainline code. | --| ![reject](http://devdocs.magento.com/common/images/github_reject.png) | The pull request has been rejected and will not be merged into mainline code. Possible reasons can include but are not limited to: issue has already been fixed in another code contribution, or there is an issue with the code contribution. | --| ![bug report](http://devdocs.magento.com/common/images/github_bug.png) | The Magento Team has confirmed that this issue contains the minimum required information to reproduce. | --| ![acknowledged](http://devdocs.magento.com/common/images/gitHub_acknowledged.png) | The Magento Team has validated the issue and an internal ticket has been created. | --| ![in progress](http://devdocs.magento.com/common/images/github_inProgress.png) | The internal ticket is currently in progress, fix is scheduled to be delivered. | --| ![needs update](http://devdocs.magento.com/common/images/github_needsUpdate.png) | The Magento Team needs additional information from the reporter to properly prioritize and process the issue or pull request. | -+## Reporting Security Issues - --To learn more about issue gate labels click [here](https://github.com/magento/magento2/wiki/Magento-Issue-Gates) -+To report security vulnerabilities or learn more about reporting security issues in Magento software or web sites visit the [Magento Bug Bounty Program](https://hackerone.com/magento) on hackerone. Please create a hackerone account [there](https://hackerone.com/magento) to submit and follow-up your issue. - --

Reporting security issues

-+Stay up-to-date on the latest security news and patches for Magento by signing up for [Security Alert Notifications](https://magento.com/security/sign-up). - --To report security vulnerabilities in Magento software or web sites, please create a Bugcrowd researcher account there to submit and follow-up your issue. Learn more about reporting security issues here. -+## License - --Stay up-to-date on the latest security news and patches for Magento by signing up for Security Alert Notifications. -+Each Magento source file included in this distribution is licensed under OSL 3.0 or the Magento Enterprise Edition (MEE) license. - --

License

-+[Open Software License (OSL 3.0)](https://opensource.org/licenses/osl-3.0.php). -+Please see [LICENSE.txt](https://github.com/magento/magento2/blob/2.3-develop/LICENSE.txt) for the full text of the OSL 3.0 license or contact license@magentocommerce.com for a copy. - --Each Magento source file included in this distribution is licensed under OSL 3.0 or the Magento Enterprise Edition (MEE) license -+Subject to Licensee's payment of fees and compliance with the terms and conditions of the MEE License, the MEE License supersedes the OSL 3.0 license for each source file. -+Please see LICENSE_EE.txt for the full text of the MEE License or visit https://magento.com/legal/terms/enterprise. - --http://opensource.org/licenses/osl-3.0.php Open Software License (OSL 3.0) --Please see LICENSE.txt for the full text of the OSL 3.0 license or contact license@magentocommerce.com for a copy. -+## Community Engineering Slack - --Subject to Licensee's payment of fees and compliance with the terms and conditions of the MEE License, the MEE License supersedes the OSL 3.0 license for each source file. --Please see LICENSE_EE.txt for the full text of the MEE License or visit http://magento.com/legal/terms/enterprise. -+To connect with Magento and the Community, join us on the [Magento Community Engineering Slack](https://magentocommeng.slack.com). If you are interested in joining Slack, or a specific channel, send us request at [engcom@adobe.com](mailto:engcom@adobe.com) or [self signup](https://tinyurl.com/engcom-slack). -+ -+ -+We have channels for each project. These channels are recommended for new members: - -+- [general](https://magentocommeng.slack.com/messages/C4YS78WE6): Open chat for introductions and Magento 2 questions -+- [github](https://magentocommeng.slack.com/messages/C7KB93M32): Support for GitHub issues, pull requests, and processes -+- [public-backlog](https://magentocommeng.slack.com/messages/CCV3J3RV5): Discussions of the Magento 2 backlog -diff --git a/SECURITY.md b/SECURITY.md -new file mode 100644 -index 00000000000..2b06199e5f9 ---- /dev/null -+++ b/SECURITY.md -@@ -0,0 +1,10 @@ -+# Reporting Security Issues -+ -+Magento values the contributions of the security research community, and we look forward to working with you to minimize risk to Magento merchants. -+ -+## Where should I report security issues? -+ -+We strongly encourage you to report all security issues privately via our [bug bounty program](https://hackerone.com/magento). Please provide us with relevant technical details and repro steps to expedite our investigation. If you prefer not to use HackerOne, email us directly at `psirt@adobe.com` with details and repro steps. -+ -+## Learning More About Security -+To learn more about securing a Magento store, please visit the [Security Center](https://magento.com/security). -diff --git a/app/autoload.php b/app/autoload.php -index 54087d02554..d6407083dc0 100644 ---- a/app/autoload.php -+++ b/app/autoload.php -@@ -28,6 +28,9 @@ $vendorAutoload = BP . "/{$vendorDir}/autoload.php"; - /* 'composer install' validation */ - if (file_exists($vendorAutoload)) { - $composerAutoloader = include $vendorAutoload; -+} else if (file_exists("{$vendorDir}/autoload.php")) { -+ $vendorAutoload = "{$vendorDir}/autoload.php"; -+ $composerAutoloader = include $vendorAutoload; - } else { - throw new \Exception( - 'Vendor autoload is not found. Please run \'composer install\' under application root directory.' -diff --git a/app/bootstrap.php b/app/bootstrap.php -index 70b632537a7..4974acdf0fc 100644 ---- a/app/bootstrap.php -+++ b/app/bootstrap.php -@@ -8,18 +8,21 @@ - * Environment initialization - */ - error_reporting(E_ALL); -+if (in_array('phar', \stream_get_wrappers())) { -+ stream_wrapper_unregister('phar'); -+} - #ini_set('display_errors', 1); - - /* PHP version validation */ --if (!defined('PHP_VERSION_ID') || !(PHP_VERSION_ID === 70002 || PHP_VERSION_ID === 70004 || PHP_VERSION_ID >= 70006)) { -+if (!defined('PHP_VERSION_ID') || PHP_VERSION_ID < 70103) { - if (PHP_SAPI == 'cli') { -- echo 'Magento supports 7.0.2, 7.0.4, and 7.0.6 or later. ' . -- 'Please read http://devdocs.magento.com/guides/v2.2/install-gde/system-requirements.html'; -+ echo 'Magento supports PHP 7.1.3 or later. ' . -+ 'Please read https://devdocs.magento.com/guides/v2.3/install-gde/system-requirements-tech.html'; - } else { - echo << --

Magento supports PHP 7.0.2, 7.0.4, and 7.0.6 or later. Please read -- -+

Magento supports PHP 7.1.3 or later. Please read -+ - Magento System Requirements. - - HTML; -@@ -31,8 +34,6 @@ require_once __DIR__ . '/autoload.php'; - // Sets default autoload mappings, may be overridden in Bootstrap::create - \Magento\Framework\App\Bootstrap::populateAutoloader(BP, []); - --require_once BP . '/app/functions.php'; -- - /* Custom umask value may be provided in optional mage_umask file in root */ - $umaskFile = BP . '/magento_umask'; - $mask = file_exists($umaskFile) ? octdec(file_get_contents($umaskFile)) : 002; -diff --git a/app/code/Magento/AdminNotification/Block/Grid/Renderer/Actions.php b/app/code/Magento/AdminNotification/Block/Grid/Renderer/Actions.php -index 6f0e42bdcbe..82f70d92e49 100644 ---- a/app/code/Magento/AdminNotification/Block/Grid/Renderer/Actions.php -+++ b/app/code/Magento/AdminNotification/Block/Grid/Renderer/Actions.php -@@ -8,6 +8,11 @@ - - namespace Magento\AdminNotification\Block\Grid\Renderer; - -+/** -+ * Renderer class for action in the admin notifications grid -+ * -+ * @package Magento\AdminNotification\Block\Grid\Renderer -+ */ - class Actions extends \Magento\Backend\Block\Widget\Grid\Column\Renderer\AbstractRenderer - { - /** -@@ -37,7 +42,9 @@ class Actions extends \Magento\Backend\Block\Widget\Grid\Column\Renderer\Abstrac - */ - public function render(\Magento\Framework\DataObject $row) - { -- $readDetailsHtml = $row->getUrl() ? '' . -+ $readDetailsHtml = $row->getUrl() ? '' . - __('Read Details') . '' : ''; - - $markAsReadHtml = !$row->getIsRead() ? ' -+ -+ -+ -+ -+ System -+ Notifications -+ magento-backend-system -+ -+ -+ Notifications -+ Notifications -+ magento-adminnotification-system-adminnotification -+ -+ -diff --git a/app/code/Magento/AdminNotification/Test/Mftf/Test/AdminSystemNotificationNavigateMenuTest.xml b/app/code/Magento/AdminNotification/Test/Mftf/Test/AdminSystemNotificationNavigateMenuTest.xml -new file mode 100644 -index 00000000000..75dceb40286 ---- /dev/null -+++ b/app/code/Magento/AdminNotification/Test/Mftf/Test/AdminSystemNotificationNavigateMenuTest.xml -@@ -0,0 +1,36 @@ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ <description value="Admin should be able to navigate to System > Notifications"/> -+ <severity value="CRITICAL"/> -+ <testCaseId value="MC-14125"/> -+ <group value="menu"/> -+ <group value="mtf_migrated"/> -+ </annotations> -+ <before> -+ <actionGroup ref="LoginAsAdmin" stepKey="LoginAsAdmin"/> -+ </before> -+ <after> -+ <actionGroup ref="logout" stepKey="logout"/> -+ </after> -+ <actionGroup ref="AdminNavigateMenuActionGroup" stepKey="navigateToSystemNotificationPage"> -+ <argument name="menuUiId" value="{{AdminMenuSystem.dataUiId}}"/> -+ <argument name="submenuUiId" value="{{AdminMenuSystemOtherSettingsNotifications.dataUiId}}"/> -+ </actionGroup> -+ <actionGroup ref="AdminAssertPageTitleActionGroup" stepKey="seePageTitle"> -+ <argument name="title" value="{{AdminMenuSystemOtherSettingsNotifications.pageTitle}}"/> -+ </actionGroup> -+ </test> -+</tests> -diff --git a/app/code/Magento/AdminNotification/composer.json b/app/code/Magento/AdminNotification/composer.json -index e5cf487908c..fe0727474fa 100644 ---- a/app/code/Magento/AdminNotification/composer.json -+++ b/app/code/Magento/AdminNotification/composer.json -@@ -11,7 +11,8 @@ - "magento/module-backend": "*", - "magento/module-media-storage": "*", - "magento/module-store": "*", -- "magento/module-ui": "*" -+ "magento/module-ui": "*", -+ "magento/module-config": "*" - }, - "type": "magento2-module", - "license": [ -diff --git a/app/code/Magento/AdminNotification/etc/adminhtml/menu.xml b/app/code/Magento/AdminNotification/etc/adminhtml/menu.xml -index fbed5c0960b..04d700b9f90 100644 ---- a/app/code/Magento/AdminNotification/etc/adminhtml/menu.xml -+++ b/app/code/Magento/AdminNotification/etc/adminhtml/menu.xml -@@ -7,6 +7,6 @@ - --> - <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Backend:etc/menu.xsd"> - <menu> -- <add id="Magento_AdminNotification::system_adminnotification" title="Notifications" translate="title" module="Magento_AdminNotification" sortOrder="10" parent="Magento_Backend::system_other_settings" action="adminhtml/notification" resource="Magento_AdminNotification::adminnotification"/> -+ <add id="Magento_AdminNotification::system_adminnotification" title="Notifications" translate="title" module="Magento_AdminNotification" sortOrder="10" parent="Magento_Backend::system_other_settings" action="adminhtml/notification" resource="Magento_AdminNotification::adminnotification"/> - </menu> - </config> -diff --git a/app/code/Magento/AdminNotification/etc/db_schema.xml b/app/code/Magento/AdminNotification/etc/db_schema.xml -index 35e6045b607..29d928ced20 100644 ---- a/app/code/Magento/AdminNotification/etc/db_schema.xml -+++ b/app/code/Magento/AdminNotification/etc/db_schema.xml -@@ -21,16 +21,16 @@ - default="0" comment="Flag if notification read"/> - <column xsi:type="smallint" name="is_remove" padding="5" unsigned="true" nullable="false" identity="false" - default="0" comment="Flag if notification might be removed"/> -- <constraint xsi:type="primary" name="PRIMARY"> -+ <constraint xsi:type="primary" referenceId="PRIMARY"> - <column name="notification_id"/> - </constraint> -- <index name="ADMINNOTIFICATION_INBOX_SEVERITY" indexType="btree"> -+ <index referenceId="ADMINNOTIFICATION_INBOX_SEVERITY" indexType="btree"> - <column name="severity"/> - </index> -- <index name="ADMINNOTIFICATION_INBOX_IS_READ" indexType="btree"> -+ <index referenceId="ADMINNOTIFICATION_INBOX_IS_READ" indexType="btree"> - <column name="is_read"/> - </index> -- <index name="ADMINNOTIFICATION_INBOX_IS_REMOVE" indexType="btree"> -+ <index referenceId="ADMINNOTIFICATION_INBOX_IS_REMOVE" indexType="btree"> - <column name="is_remove"/> - </index> - </table> -@@ -40,7 +40,7 @@ - default="0" comment="Problem type"/> - <column xsi:type="timestamp" name="created_at" on_update="false" nullable="false" default="CURRENT_TIMESTAMP" - comment="Create date"/> -- <constraint xsi:type="primary" name="PRIMARY"> -+ <constraint xsi:type="primary" referenceId="PRIMARY"> - <column name="identity"/> - </constraint> - </table> -diff --git a/app/code/Magento/AdminNotification/view/adminhtml/layout/adminhtml_notification_block.xml b/app/code/Magento/AdminNotification/view/adminhtml/layout/adminhtml_notification_block.xml -index c68313211c2..06fd380cb2a 100644 ---- a/app/code/Magento/AdminNotification/view/adminhtml/layout/adminhtml_notification_block.xml -+++ b/app/code/Magento/AdminNotification/view/adminhtml/layout/adminhtml_notification_block.xml -@@ -11,7 +11,7 @@ - <block class="Magento\Backend\Block\Widget\Grid" name="adminhtml.notification.container.grid" as="grid"> - <arguments> - <argument name="id" xsi:type="string">notificationGrid</argument> -- <argument name="dataSource" xsi:type="object">Magento\AdminNotification\Model\ResourceModel\Grid\Collection</argument> -+ <argument name="dataSource" xsi:type="object" shared="false">Magento\AdminNotification\Model\ResourceModel\Grid\Collection</argument> - <argument name="default_dir" xsi:type="string">DESC</argument> - <argument name="default_sort" xsi:type="string">date_added</argument> - <argument name="save_parameters_in_session" xsi:type="string">1</argument> -diff --git a/app/code/Magento/AdminNotification/view/adminhtml/layout/default.xml b/app/code/Magento/AdminNotification/view/adminhtml/layout/default.xml -index aa8ba23d0ee..eed6b53f343 100644 ---- a/app/code/Magento/AdminNotification/view/adminhtml/layout/default.xml -+++ b/app/code/Magento/AdminNotification/view/adminhtml/layout/default.xml -@@ -8,7 +8,7 @@ - <page xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:View/Layout/etc/page_configuration.xsd"> - <body> - <referenceContainer name="notifications"> -- <uiComponent name="notification_area"/> -+ <uiComponent name="notification_area" aclResource="Magento_AdminNotification::show_list"/> - <block class="Magento\AdminNotification\Block\System\Messages\UnreadMessagePopup" - name="unread_system_messages" - as="unread_system_messages" -diff --git a/app/code/Magento/AdminNotification/view/adminhtml/templates/notification/window.phtml b/app/code/Magento/AdminNotification/view/adminhtml/templates/notification/window.phtml -index 3f79e803ccc..b4f19bda36c 100644 ---- a/app/code/Magento/AdminNotification/view/adminhtml/templates/notification/window.phtml -+++ b/app/code/Magento/AdminNotification/view/adminhtml/templates/notification/window.phtml -@@ -4,10 +4,6 @@ - * See COPYING.txt for license details. - */ - --// @codingStandardsIgnoreFile -- --?> --<?php - /** - * @see \Magento\AdminNotification\Block\Window - */ -@@ -19,11 +15,13 @@ - "autoOpen": true, - "buttons": false, - "modalClass": "modal-system-messages", -- "title": "<?= /* @escapeNotVerified */ $block->getHeaderText() ?>" -+ "title": "<?= $block->escapeHtmlAttr($block->getHeaderText()) ?>" - } - }'> - <li class="message message-warning warning"> -- <?= /* @escapeNotVerified */ $block->getNoticeMessageText() ?><br/> -- <a href="<?= /* @escapeNotVerified */ $block->getNoticeMessageUrl() ?>"><?= /* @escapeNotVerified */ $block->getReadDetailsText() ?></a> -+ <?= $block->escapeHtml($block->getNoticeMessageText()) ?><br/> -+ <a href="<?= $block->escapeUrl($block->getNoticeMessageUrl()) ?>"> -+ <?= $block->escapeHtml($block->getReadDetailsText()) ?> -+ </a> - </li> - </ul> -diff --git a/app/code/Magento/AdminNotification/view/adminhtml/templates/system/messages.phtml b/app/code/Magento/AdminNotification/view/adminhtml/templates/system/messages.phtml -index 01d6fdcb295..22512b9055f 100644 ---- a/app/code/Magento/AdminNotification/view/adminhtml/templates/system/messages.phtml -+++ b/app/code/Magento/AdminNotification/view/adminhtml/templates/system/messages.phtml -@@ -4,41 +4,41 @@ - * See COPYING.txt for license details. - */ - --// @codingStandardsIgnoreFile -- -+/** @var $block \Magento\AdminNotification\Block\System\Messages */ - ?> --<?php /** @var $block \Magento\AdminNotification\Block\System\Messages */ ?> - - <?php $lastCritical = $block->getLastCritical();?> --<div id="system_messages" class="message-system<?php if ($lastCritical): ?> message-system-unread<?php endif; ?>"> -+<div id="system_messages" -+ class="message-system<?php if ($lastCritical) : ?> -+ message-system-unread<?php endif; ?>"> - <div class="message-system-inner"> -- <?php if ($lastCritical): ?> -+ <?php if ($lastCritical) : ?> - <ul class="message-system-list"> - <li class="message message-warning error"> -- <?= /* @escapeNotVerified */ $lastCritical->getText() ?> -+ <?= $block->escapeHtml($lastCritical->getText()) ?> - </li> - </ul> - <?php endif; ?> - <div class="message-system-short"> - <span class="message-system-short-label"> -- <?= /* @escapeNotVerified */ __('System Messages:') ?> -+ <?= $block->escapeHtml(__('System Messages:')) ?> - </span> - -- <?php if ($block->getCriticalCount()): ?> -+ <?php if ($block->getCriticalCount()) : ?> - <div class="message message-warning error"> - <a class="message-link" href="#" title="<?= $block->escapeHtml(__('Critical System Messages')) ?>"> -- <?= /* @escapeNotVerified */ $block->getCriticalCount() ?> -+ <?= (int) $block->getCriticalCount() ?> - </a> - </div> -- <?php endif;?> -+ <?php endif; ?> - -- <?php if ($block->getMajorCount()): ?> -+ <?php if ($block->getMajorCount()) : ?> - <div class="message message-warning warning"> - <a class="message-link" href="#" title="<?= $block->escapeHtml(__('Major System Messages')) ?>"> -- <?= /* @escapeNotVerified */ $block->getMajorCount() ?> -+ <?= (int) $block->getMajorCount() ?> - </a> - </div> -- <?php endif;?> -+ <?php endif; ?> - </div> - <div id="message-system-all" title="<?= $block->escapeHtml(__('System messages')) ?>" data-mage-init='<?= $block->escapeHtml($block->getSystemMessageDialogJson()) ?>'></div> - </div> -diff --git a/app/code/Magento/AdminNotification/view/adminhtml/templates/system/messages/popup.phtml b/app/code/Magento/AdminNotification/view/adminhtml/templates/system/messages/popup.phtml -index 0448daaf176..494e6086562 100644 ---- a/app/code/Magento/AdminNotification/view/adminhtml/templates/system/messages/popup.phtml -+++ b/app/code/Magento/AdminNotification/view/adminhtml/templates/system/messages/popup.phtml -@@ -4,16 +4,15 @@ - * See COPYING.txt for license details. - */ - --// @codingStandardsIgnoreFile -- -+/** @var $block \Magento\AdminNotification\Block\System\Messages\UnreadMessagePopup */ - ?> --<?php /** @var $block \Magento\AdminNotification\Block\System\Messages\UnreadMessagePopup */ ?> - --<div style="display:none" id="system_messages_list" data-role="system_messages_list" title="<?= $block->escapeHtml($block->getPopupTitle()) ?>"> -+<div style="display:none" id="system_messages_list" data-role="system_messages_list" -+ title="<?= $block->escapeHtmlAttr($block->getPopupTitle()) ?>"> - <ul class="message-system-list messages"> -- <?php foreach ($block->getUnreadMessages() as $message): ?> -- <li class="message message-warning <?= /* @escapeNotVerified */ $block->getItemClass($message) ?>"> -- <?= /* @escapeNotVerified */ $message->getText() ?> -+ <?php foreach ($block->getUnreadMessages() as $message) : ?> -+ <li class="message message-warning <?= $block->escapeHtmlAttr($block->getItemClass($message)) ?>"> -+ <?= $block->escapeHtml($message->getText()) ?> - </li> - <?php endforeach;?> - </ul> -@@ -27,4 +26,4 @@ - } - } - } --</script> -\ No newline at end of file -+</script> -diff --git a/app/code/Magento/AdminNotification/view/adminhtml/templates/toolbar_entry.phtml b/app/code/Magento/AdminNotification/view/adminhtml/templates/toolbar_entry.phtml -index 777448cdd82..38398727e0f 100644 ---- a/app/code/Magento/AdminNotification/view/adminhtml/templates/toolbar_entry.phtml -+++ b/app/code/Magento/AdminNotification/view/adminhtml/templates/toolbar_entry.phtml -@@ -4,81 +4,78 @@ - * See COPYING.txt for license details. - */ - --// @codingStandardsIgnoreFile -+/** @var $this \Magento\AdminNotification\Block\ToolbarEntry */ - --?> --<?php /** @var $this \Magento\AdminNotification\Block\ToolbarEntry */ ?> --<?php - $notificationCount = $block->getUnreadNotificationCount(); - $notificationCounterMax = $block->getNotificationCounterMax(); - ?> - <div - data-mage-init='{"toolbarEntry": {}}' - class="notifications-wrapper admin__action-dropdown-wrap" -- data-notification-count="<?= /* @escapeNotVerified */ $notificationCount ?>"> -+ data-notification-count="<?= (int)$notificationCount ?>"> - <?php if ($notificationCount > 0) : ?> - <a -- href="<?= /* @escapeNotVerified */ $block->getUrl('adminhtml/notification/index') ?>" -+ href="<?= $block->escapeUrl($block->getUrl('adminhtml/notification/index')) ?>" - class="notifications-action admin__action-dropdown" - data-mage-init='{"dropdown":{}}' -- title="<?= /* @escapeNotVerified */ __('Notifications') ?>" -+ title="<?= $block->escapeHtmlAttr(__('Notifications')) ?>" - data-toggle="dropdown"> - <span class="notifications-counter"> -- <?= /* @escapeNotVerified */ ($notificationCount > $notificationCounterMax) ? $notificationCounterMax . '+' : $notificationCount ?> -+ <?= /* @noEscape */ ($notificationCount > $notificationCounterMax) ? (int)$notificationCounterMax . '+' : (int)$notificationCount ?> - </span> - </a> - <ul - class="admin__action-dropdown-menu" -- data-mark-as-read-url="<?= /* @escapeNotVerified */ $block->getUrl('adminhtml/notification/ajaxMarkAsRead') ?>"> -+ data-mark-as-read-url="<?= $block->escapeUrl($block->getUrl('adminhtml/notification/ajaxMarkAsRead')) ?>"> - <?php foreach ($block->getLatestUnreadNotifications() as $notification) : ?> -- <?php /** @var $notification \Magento\AdminNotification\Model\Inbox*/ ?> -- <li class="notifications-entry<?php if ($notification->getSeverity() == 1): ?> notifications-critical<?php endif; ?>" -- data-notification-id="<?= /* @escapeNotVerified */ $notification->getId() ?>" -- data-notification-severity="<?php if ($notification->getSeverity() == 1): ?>1<?php endif; ?>"> -- <?php -- $notificationDescription = $block->escapeHtml($notification->getDescription()); -- $notificationDescriptionLength = $block->getNotificationDescriptionLength(); -- ?> -- <strong class="notifications-entry-title"> -- <?= $block->escapeHtml($notification->getTitle()) ?> -- </strong> -- <?php if (strlen($notificationDescription) > $notificationDescriptionLength) : ?> -- <p class="notifications-entry-description _cutted"> -- <span class="notifications-entry-description-start"> -- <?= /* @escapeNotVerified */ substr($notificationDescription, 0, $notificationDescriptionLength) ?> -- </span> -- <span class="notifications-entry-description-end"> -- <?= /* @escapeNotVerified */ substr($notificationDescription, $notificationDescriptionLength) ?> -- </span> -- </p> -- <?php else : ?> -- <p class="notifications-entry-description"> -- <?= /* @escapeNotVerified */ $notificationDescription ?> -- </p> -- <?php endif; ?> -- <time class="notifications-entry-time"> -- <?= /* @escapeNotVerified */ $block->formatNotificationDate($notification->getDateAdded()) ?> -- </time> -- <button -- type="button" -- class="notifications-close" -- title="<?= /* @escapeNotVerified */ __('Close') ?>" -- ></button> -- </li> -+ <?php /** @var $notification \Magento\AdminNotification\Model\Inbox */ ?> -+ <li class="notifications-entry<?php if ($notification->getSeverity() == 1) : ?> notifications-critical<?php endif; ?>" -+ data-notification-id="<?= $block->escapeHtmlAttr($notification->getId()) ?>" -+ data-notification-severity="<?php if ($notification->getSeverity() == 1) : ?>1<?php endif; ?>"> -+ <?php -+ $notificationDescription = $notification->getDescription(); -+ $notificationDescriptionLength = $block->getNotificationDescriptionLength(); -+ ?> -+ <strong class="notifications-entry-title"> -+ <?= $block->escapeHtml($notification->getTitle()) ?> -+ </strong> -+ <?php if (strlen($notificationDescription) > $notificationDescriptionLength) : ?> -+ <p class="notifications-entry-description _cutted"> -+ <span class="notifications-entry-description-start"> -+ <?= $block->escapeHtml(substr($notificationDescription, 0, $notificationDescriptionLength)) ?> -+ </span> -+ <span class="notifications-entry-description-end"> -+ <?= $block->escapeHtml(substr($notificationDescription, $notificationDescriptionLength)) ?> -+ </span> -+ </p> -+ <?php else : ?> -+ <p class="notifications-entry-description"> -+ <?= $block->escapeHtml($notificationDescription) ?> -+ </p> -+ <?php endif; ?> -+ <time class="notifications-entry-time"> -+ <?= $block->escapeHtml($block->formatNotificationDate($notification->getDateAdded())) ?> -+ </time> -+ <button -+ type="button" -+ class="notifications-close" -+ title="<?= $block->escapeHtmlAttr(__('Close')) ?>" -+ ></button> -+ </li> - <?php endforeach; ?> - <li class="notifications-entry notifications-entry-last"> - <a -- href="<?= /* @escapeNotVerified */ $block->getUrl('adminhtml/notification/index') ?>" -+ href="<?= $block->escapeUrl($block->getUrl('adminhtml/notification/index')) ?>" - class="action-tertiary action-more"> -- <?= /* @escapeNotVerified */ __('See All (') ?><span class="notifications-counter"><?= /* @escapeNotVerified */ $notificationCount ?></span><?= /* @escapeNotVerified */ __(' unread)') ?> -+ <?= $block->escapeHtml(__('See All (')) ?><span class="notifications-counter"><?= (int)$notificationCount ?></span><?= $block->escapeHtml(__(' unread)')) ?> - </a> - </li> - </ul> - <?php else : ?> - <a - class="notifications-action admin__action-dropdown" -- href="<?= /* @escapeNotVerified */ $block->getUrl('adminhtml/notification/index') ?>" -- title="<?= /* @escapeNotVerified */ __('Notifications') ?>"> -+ href="<?= $block->escapeUrl($block->getUrl('adminhtml/notification/index')) ?>" -+ title="<?= $block->escapeHtmlAttr(__('Notifications')) ?>"> - </a> - <?php endif; ?> - </div> -diff --git a/app/code/Magento/AdvancedPricingImportExport/Controller/Adminhtml/Export/GetFilter.php b/app/code/Magento/AdvancedPricingImportExport/Controller/Adminhtml/Export/GetFilter.php -index 02413a1899c..d78266ab753 100644 ---- a/app/code/Magento/AdvancedPricingImportExport/Controller/Adminhtml/Export/GetFilter.php -+++ b/app/code/Magento/AdvancedPricingImportExport/Controller/Adminhtml/Export/GetFilter.php -@@ -5,12 +5,14 @@ - */ - namespace Magento\AdvancedPricingImportExport\Controller\Adminhtml\Export; - -+use Magento\Framework\App\Action\HttpGetActionInterface; -+use Magento\Framework\App\Action\HttpPostActionInterface as HttpPostActionInterface; - use Magento\ImportExport\Controller\Adminhtml\Export as ExportController; - use Magento\Framework\Controller\ResultFactory; - use Magento\AdvancedPricingImportExport\Model\Export\AdvancedPricing as ExportAdvancedPricing; - use Magento\Catalog\Model\Product as CatalogProduct; - --class GetFilter extends ExportController -+class GetFilter extends ExportController implements HttpGetActionInterface, HttpPostActionInterface - { - /** - * Get grid-filter of entity attributes action. -@@ -37,10 +39,10 @@ class GetFilter extends ExportController - ); - return $resultLayout; - } catch (\Exception $e) { -- $this->messageManager->addError($e->getMessage()); -+ $this->messageManager->addErrorMessage($e->getMessage()); - } - } else { -- $this->messageManager->addError(__('Please correct the data sent.')); -+ $this->messageManager->addErrorMessage(__('Please correct the data sent.')); - } - /** @var \Magento\Backend\Model\View\Result\Redirect $resultRedirect */ - $resultRedirect = $this->resultFactory->create(ResultFactory::TYPE_REDIRECT); -diff --git a/app/code/Magento/AdvancedPricingImportExport/Model/Import/AdvancedPricing.php b/app/code/Magento/AdvancedPricingImportExport/Model/Import/AdvancedPricing.php -index 2e17e734b1e..591e648547d 100644 ---- a/app/code/Magento/AdvancedPricingImportExport/Model/Import/AdvancedPricing.php -+++ b/app/code/Magento/AdvancedPricingImportExport/Model/Import/AdvancedPricing.php -@@ -185,6 +185,7 @@ class AdvancedPricing extends \Magento\ImportExport\Model\Import\Entity\Abstract - * @param AdvancedPricing\Validator\Website $websiteValidator - * @param AdvancedPricing\Validator\TierPrice $tierPriceValidator - * @SuppressWarnings(PHPMD.UnusedFormalParameter) -+ * @throws \Exception - */ - public function __construct( - \Magento\Framework\Json\Helper\Data $jsonHelper, -@@ -255,6 +256,7 @@ class AdvancedPricing extends \Magento\ImportExport\Model\Import\Entity\Abstract - * @param array $rowData - * @param int $rowNum - * @return bool -+ * @throws \Zend_Validate_Exception - */ - public function validateRow(array $rowData, $rowNum) - { -@@ -308,6 +310,7 @@ class AdvancedPricing extends \Magento\ImportExport\Model\Import\Entity\Abstract - * Save advanced pricing - * - * @return $this -+ * @throws \Exception - */ - public function saveAdvancedPricing() - { -@@ -319,6 +322,7 @@ class AdvancedPricing extends \Magento\ImportExport\Model\Import\Entity\Abstract - * Deletes Advanced price data from raw data. - * - * @return $this -+ * @throws \Exception - */ - public function deleteAdvancedPricing() - { -@@ -347,6 +351,7 @@ class AdvancedPricing extends \Magento\ImportExport\Model\Import\Entity\Abstract - * Replace advanced pricing - * - * @return $this -+ * @throws \Exception - */ - public function replaceAdvancedPricing() - { -@@ -360,6 +365,7 @@ class AdvancedPricing extends \Magento\ImportExport\Model\Import\Entity\Abstract - * @return $this - * @SuppressWarnings(PHPMD.CyclomaticComplexity) - * @SuppressWarnings(PHPMD.NPathComplexity) -+ * @throws \Exception - */ - protected function saveAndReplaceAdvancedPrices() - { -@@ -368,8 +374,8 @@ class AdvancedPricing extends \Magento\ImportExport\Model\Import\Entity\Abstract - $this->_cachedSkuToDelete = null; - } - $listSku = []; -+ $tierPrices = []; - while ($bunch = $this->_dataSourceModel->getNextBunch()) { -- $tierPrices = []; - foreach ($bunch as $rowNum => $rowData) { - if (!$this->validateRow($rowData, $rowNum)) { - $this->addRowError(ValidatorInterface::ERROR_SKU_IS_EMPTY, $rowNum); -@@ -397,23 +403,28 @@ class AdvancedPricing extends \Magento\ImportExport\Model\Import\Entity\Abstract - ]; - } - } -- if (\Magento\ImportExport\Model\Import::BEHAVIOR_REPLACE == $behavior) { -- if ($listSku) { -- $this->processCountNewPrices($tierPrices); -- if ($this->deleteProductTierPrices(array_unique($listSku), self::TABLE_TIER_PRICE)) { -- $this->saveProductPrices($tierPrices, self::TABLE_TIER_PRICE); -- $this->setUpdatedAt($listSku); -- } -- } -- } elseif (\Magento\ImportExport\Model\Import::BEHAVIOR_APPEND == $behavior) { -+ -+ if (\Magento\ImportExport\Model\Import::BEHAVIOR_APPEND == $behavior) { - $this->processCountExistingPrices($tierPrices, self::TABLE_TIER_PRICE) - ->processCountNewPrices($tierPrices); -+ - $this->saveProductPrices($tierPrices, self::TABLE_TIER_PRICE); - if ($listSku) { - $this->setUpdatedAt($listSku); - } - } - } -+ -+ if (\Magento\ImportExport\Model\Import::BEHAVIOR_REPLACE == $behavior) { -+ if ($listSku) { -+ $this->processCountNewPrices($tierPrices); -+ if ($this->deleteProductTierPrices(array_unique($listSku), self::TABLE_TIER_PRICE)) { -+ $this->saveProductPrices($tierPrices, self::TABLE_TIER_PRICE); -+ $this->setUpdatedAt($listSku); -+ } -+ } -+ } -+ - return $this; - } - -@@ -423,6 +434,7 @@ class AdvancedPricing extends \Magento\ImportExport\Model\Import\Entity\Abstract - * @param array $priceData - * @param string $table - * @return $this -+ * @throws \Exception - */ - protected function saveProductPrices(array $priceData, $table) - { -@@ -454,6 +466,7 @@ class AdvancedPricing extends \Magento\ImportExport\Model\Import\Entity\Abstract - * @param array $listSku - * @param string $table - * @return boolean -+ * @throws \Exception - */ - protected function deleteProductTierPrices(array $listSku, $table) - { -@@ -531,6 +544,7 @@ class AdvancedPricing extends \Magento\ImportExport\Model\Import\Entity\Abstract - * Retrieve product skus - * - * @return array -+ * @throws \Exception - */ - protected function retrieveOldSkus() - { -@@ -551,6 +565,7 @@ class AdvancedPricing extends \Magento\ImportExport\Model\Import\Entity\Abstract - * @param array $prices - * @param string $table - * @return $this -+ * @throws \Exception - */ - protected function processCountExistingPrices($prices, $table) - { -@@ -562,11 +577,14 @@ class AdvancedPricing extends \Magento\ImportExport\Model\Import\Entity\Abstract - - $tableName = $this->_resourceFactory->create()->getTable($table); - $productEntityLinkField = $this->getProductEntityLinkField(); -- $existingPrices = $this->_connection->fetchAssoc( -+ $existingPrices = $this->_connection->fetchAll( - $this->_connection->select()->from( - $tableName, -- ['value_id', $productEntityLinkField, 'all_groups', 'customer_group_id'] -- )->where($productEntityLinkField . ' IN (?)', $existProductIds) -+ [$productEntityLinkField, 'all_groups', 'customer_group_id', 'qty'] -+ )->where( -+ $productEntityLinkField . ' IN (?)', -+ $existProductIds -+ ) - ); - foreach ($existingPrices as $existingPrice) { - foreach ($prices as $sku => $skuPrices) { -@@ -591,8 +609,10 @@ class AdvancedPricing extends \Magento\ImportExport\Model\Import\Entity\Abstract - foreach ($prices as $price) { - if ($existingPrice['all_groups'] == $price['all_groups'] - && $existingPrice['customer_group_id'] == $price['customer_group_id'] -+ && (int) $existingPrice['qty'] === (int) $price['qty'] - ) { - $this->countItemsUpdated++; -+ continue; - } - } - } -diff --git a/app/code/Magento/AdvancedPricingImportExport/Test/Unit/Model/Import/AdvancedPricing/Validator/WebsiteTest.php b/app/code/Magento/AdvancedPricingImportExport/Test/Unit/Model/Import/AdvancedPricing/Validator/WebsiteTest.php -index b46e286e750..d78c4f5e61a 100644 ---- a/app/code/Magento/AdvancedPricingImportExport/Test/Unit/Model/Import/AdvancedPricing/Validator/WebsiteTest.php -+++ b/app/code/Magento/AdvancedPricingImportExport/Test/Unit/Model/Import/AdvancedPricing/Validator/WebsiteTest.php -@@ -103,13 +103,13 @@ class WebsiteTest extends \PHPUnit\Framework\TestCase - $this->webSiteModel->expects($this->once())->method('getBaseCurrency')->willReturn($currency); - - $expectedResult = AdvancedPricing::VALUE_ALL_WEBSITES . ' [' . $currencyCode . ']'; -- $this->websiteString = $this->getMockBuilder( -+ $websiteString = $this->getMockBuilder( - \Magento\AdvancedPricingImportExport\Model\Import\AdvancedPricing\Validator\Website::class - ) - ->setMethods(['_clearMessages', '_addMessages']) - ->setConstructorArgs([$this->storeResolver, $this->webSiteModel]) - ->getMock(); -- $result = $this->websiteString->getAllWebsitesValue(); -+ $result = $websiteString->getAllWebsitesValue(); - - $this->assertEquals($expectedResult, $result); - } -diff --git a/app/code/Magento/AdvancedPricingImportExport/Test/Unit/Model/Import/AdvancedPricingTest.php b/app/code/Magento/AdvancedPricingImportExport/Test/Unit/Model/Import/AdvancedPricingTest.php -index 340e81746f0..2aa59c1cfb7 100644 ---- a/app/code/Magento/AdvancedPricingImportExport/Test/Unit/Model/Import/AdvancedPricingTest.php -+++ b/app/code/Magento/AdvancedPricingImportExport/Test/Unit/Model/Import/AdvancedPricingTest.php -@@ -921,7 +921,7 @@ class AdvancedPricingTest extends \Magento\ImportExport\Test\Unit\Model\Import\A - ); - $dbSelectMock = $this->createMock(\Magento\Framework\DB\Select::class); - $this->connection->expects($this->once()) -- ->method('fetchAssoc') -+ ->method('fetchAll') - ->willReturn($existingPrices); - $this->connection->expects($this->once()) - ->method('select') -@@ -930,7 +930,7 @@ class AdvancedPricingTest extends \Magento\ImportExport\Test\Unit\Model\Import\A - ->method('from') - ->with( - self::TABLE_NAME, -- ['value_id', self::LINK_FIELD, 'all_groups', 'customer_group_id'] -+ [self::LINK_FIELD, 'all_groups', 'customer_group_id', 'qty'] - )->willReturnSelf(); - $this->advancedPricing->expects($this->once()) - ->method('retrieveOldSkus') -diff --git a/app/code/Magento/AdvancedSearch/Model/Recommendations/DataProvider.php b/app/code/Magento/AdvancedSearch/Model/Recommendations/DataProvider.php -index 546983bb5e5..c0c224766eb 100644 ---- a/app/code/Magento/AdvancedSearch/Model/Recommendations/DataProvider.php -+++ b/app/code/Magento/AdvancedSearch/Model/Recommendations/DataProvider.php -@@ -10,6 +10,9 @@ use Magento\Store\Model\ScopeInterface; - use Magento\Search\Model\QueryInterface; - use Magento\AdvancedSearch\Model\SuggestedQueriesInterface; - -+/** -+ * Class DataProvider -+ */ - class DataProvider implements SuggestedQueriesInterface - { - /** -@@ -51,6 +54,8 @@ class DataProvider implements SuggestedQueriesInterface - private $recommendationsFactory; - - /** -+ * DataProvider constructor. -+ * - * @param ScopeConfigInterface $scopeConfig - * @param \Magento\Catalog\Model\Layer\Resolver $layerResolver - * @param \Magento\AdvancedSearch\Model\ResourceModel\RecommendationsFactory $recommendationsFactory -@@ -69,18 +74,20 @@ class DataProvider implements SuggestedQueriesInterface - } - - /** -+ * Is Results Count Enabled -+ * - * @return bool - */ - public function isResultsCountEnabled() - { -- return (bool)$this->scopeConfig->getValue( -+ return $this->scopeConfig->isSetFlag( - self::CONFIG_RESULTS_COUNT_ENABLED, - ScopeInterface::SCOPE_STORE - ); - } - - /** -- * {@inheritdoc} -+ * @inheritdoc - */ - public function getItems(QueryInterface $query) - { -@@ -102,6 +109,8 @@ class DataProvider implements SuggestedQueriesInterface - } - - /** -+ * Return Search Recommendations -+ * - * @param QueryInterface $query - * @return array - */ -@@ -126,17 +135,21 @@ class DataProvider implements SuggestedQueriesInterface - } - - /** -+ * Is Search Recommendations Enabled -+ * - * @return bool - */ - private function isSearchRecommendationsEnabled() - { -- return (bool)$this->scopeConfig->getValue( -+ return $this->scopeConfig->isSetFlag( - self::CONFIG_IS_ENABLED, - ScopeInterface::SCOPE_STORE - ); - } - - /** -+ * Return Search Recommendations Count -+ * - * @return int - */ - private function getSearchRecommendationsCount() -diff --git a/app/code/Magento/AdvancedSearch/Model/ResourceModel/Index.php b/app/code/Magento/AdvancedSearch/Model/ResourceModel/Index.php -index 7751a3b7509..b20872da2f8 100644 ---- a/app/code/Magento/AdvancedSearch/Model/ResourceModel/Index.php -+++ b/app/code/Magento/AdvancedSearch/Model/ResourceModel/Index.php -@@ -16,6 +16,7 @@ use Magento\Framework\Search\Request\Dimension; - use Magento\Catalog\Model\Indexer\Category\Product\AbstractAction; - use Magento\Framework\Search\Request\IndexScopeResolverInterface as TableResolver; - use Magento\Catalog\Model\Indexer\Product\Price\DimensionCollectionFactory; -+use Magento\Store\Model\Indexer\WebsiteDimensionProvider; - - /** - * @api -@@ -47,12 +48,17 @@ class Index extends AbstractDb - */ - private $dimensionCollectionFactory; - -+ /** -+ * @var int|null -+ */ -+ private $websiteId; -+ - /** - * Index constructor. - * @param Context $context - * @param StoreManagerInterface $storeManager - * @param MetadataPool $metadataPool -- * @param null $connectionName -+ * @param string|null $connectionName - * @param TableResolver|null $tableResolver - * @param DimensionCollectionFactory|null $dimensionCollectionFactory - */ -@@ -94,12 +100,17 @@ class Index extends AbstractDb - $catalogProductIndexPriceSelect = []; - - foreach ($this->dimensionCollectionFactory->create() as $dimensions) { -- $catalogProductIndexPriceSelect[] = $connection->select()->from( -- $this->tableResolver->resolve('catalog_product_index_price', $dimensions), -- ['entity_id', 'customer_group_id', 'website_id', 'min_price'] -- ); -- if ($productIds) { -- current($catalogProductIndexPriceSelect)->where('entity_id IN (?)', $productIds); -+ if (!isset($dimensions[WebsiteDimensionProvider::DIMENSION_NAME]) || -+ $this->websiteId === null || -+ $dimensions[WebsiteDimensionProvider::DIMENSION_NAME]->getValue() === $this->websiteId) { -+ $select = $connection->select()->from( -+ $this->tableResolver->resolve('catalog_product_index_price', $dimensions), -+ ['entity_id', 'customer_group_id', 'website_id', 'min_price'] -+ ); -+ if ($productIds) { -+ $select->where('entity_id IN (?)', $productIds); -+ } -+ $catalogProductIndexPriceSelect[] = $select; - } - } - -@@ -123,9 +134,12 @@ class Index extends AbstractDb - */ - public function getPriceIndexData($productIds, $storeId) - { -+ $websiteId = $this->storeManager->getStore($storeId)->getWebsiteId(); -+ -+ $this->websiteId = $websiteId; - $priceProductsIndexData = $this->_getCatalogProductPriceData($productIds); -+ $this->websiteId = null; - -- $websiteId = $this->storeManager->getStore($storeId)->getWebsiteId(); - if (!isset($priceProductsIndexData[$websiteId])) { - return []; - } -diff --git a/app/code/Magento/AdvancedSearch/etc/adminhtml/system.xml b/app/code/Magento/AdvancedSearch/etc/adminhtml/system.xml -index fa7774f5cec..2c4f7fca183 100644 ---- a/app/code/Magento/AdvancedSearch/etc/adminhtml/system.xml -+++ b/app/code/Magento/AdvancedSearch/etc/adminhtml/system.xml -@@ -48,18 +48,18 @@ - </depends> - </field> - <!--<group id="suggestions">--> -- <field id="search_suggestion_enabled" translate="label comment" type="select" sortOrder="70" showInDefault="1" showInWebsite="1" showInStore="1"> -+ <field id="search_suggestion_enabled" translate="label comment" type="select" sortOrder="90" showInDefault="1" showInWebsite="1" showInStore="1"> - <label>Enable Search Suggestions</label> - <comment>When you enable this option your site may slow down.</comment> - <source_model>Magento\Config\Model\Config\Source\Yesno</source_model> - </field> -- <field id="search_suggestion_count" translate="label" type="text" sortOrder="71" showInDefault="1" showInWebsite="1" showInStore="1"> -+ <field id="search_suggestion_count" translate="label" type="text" sortOrder="91" showInDefault="1" showInWebsite="1" showInStore="1"> - <label>Search Suggestions Count</label> - <depends> - <field id="search_suggestion_enabled">1</field> - </depends> - </field> -- <field id="search_suggestion_count_results_enabled" translate="label" type="select" sortOrder="72" showInDefault="1" showInWebsite="1" showInStore="1"> -+ <field id="search_suggestion_count_results_enabled" translate="label" type="select" sortOrder="92" showInDefault="1" showInWebsite="1" showInStore="1"> - <label>Show Results Count for Each Suggestion</label> - <source_model>Magento\Config\Model\Config\Source\Yesno</source_model> - <comment>When you enable this option your site may slow down.</comment> -diff --git a/app/code/Magento/AdvancedSearch/etc/db_schema.xml b/app/code/Magento/AdvancedSearch/etc/db_schema.xml -index 9fae4041109..2dd8c68e2d5 100644 ---- a/app/code/Magento/AdvancedSearch/etc/db_schema.xml -+++ b/app/code/Magento/AdvancedSearch/etc/db_schema.xml -@@ -14,13 +14,13 @@ - default="0" comment="Query Id"/> - <column xsi:type="int" name="relation_id" padding="10" unsigned="true" nullable="false" identity="false" - default="0" comment="Relation Id"/> -- <constraint xsi:type="primary" name="PRIMARY"> -+ <constraint xsi:type="primary" referenceId="PRIMARY"> - <column name="id"/> - </constraint> -- <constraint xsi:type="foreign" name="CATALOGSEARCH_RECOMMENDATIONS_QUERY_ID_SEARCH_QUERY_QUERY_ID" -+ <constraint xsi:type="foreign" referenceId="CATALOGSEARCH_RECOMMENDATIONS_QUERY_ID_SEARCH_QUERY_QUERY_ID" - table="catalogsearch_recommendations" column="query_id" referenceTable="search_query" - referenceColumn="query_id" onDelete="CASCADE"/> -- <constraint xsi:type="foreign" name="CATALOGSEARCH_RECOMMENDATIONS_RELATION_ID_SEARCH_QUERY_QUERY_ID" -+ <constraint xsi:type="foreign" referenceId="CATALOGSEARCH_RECOMMENDATIONS_RELATION_ID_SEARCH_QUERY_QUERY_ID" - table="catalogsearch_recommendations" column="relation_id" referenceTable="search_query" - referenceColumn="query_id" onDelete="CASCADE"/> - </table> -diff --git a/app/code/Magento/AdvancedSearch/view/adminhtml/layout/catalog_search_block.xml b/app/code/Magento/AdvancedSearch/view/adminhtml/layout/catalog_search_block.xml -index b6ef596281e..f3544863348 100644 ---- a/app/code/Magento/AdvancedSearch/view/adminhtml/layout/catalog_search_block.xml -+++ b/app/code/Magento/AdvancedSearch/view/adminhtml/layout/catalog_search_block.xml -@@ -11,7 +11,7 @@ - <block class="Magento\AdvancedSearch\Block\Adminhtml\Search\Grid" name="search.edit.grid" as="grid"> - <arguments> - <argument name="id" xsi:type="string">catalog_search_grid</argument> -- <argument name="dataSource" xsi:type="object">Magento\AdvancedSearch\Model\ResourceModel\Search\Grid\Collection</argument> -+ <argument name="dataSource" xsi:type="object" shared="false">Magento\AdvancedSearch\Model\ResourceModel\Search\Grid\Collection</argument> - <argument name="default_sort" xsi:type="string">name</argument> - <argument name="default_dir" xsi:type="string">ASC</argument> - <argument name="save_parameters_in_session" xsi:type="string">1</argument> -diff --git a/app/code/Magento/AdvancedSearch/view/adminhtml/templates/system/config/testconnection.phtml b/app/code/Magento/AdvancedSearch/view/adminhtml/templates/system/config/testconnection.phtml -index ae202cbfaf4..a583b2d51e4 100644 ---- a/app/code/Magento/AdvancedSearch/view/adminhtml/templates/system/config/testconnection.phtml -+++ b/app/code/Magento/AdvancedSearch/view/adminhtml/templates/system/config/testconnection.phtml -@@ -3,13 +3,12 @@ - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ --// @codingStandardsIgnoreFile - ?> - <button class="scalable" type="button" id="<?= $block->getHtmlId() ?>" data-mage-init='{"testConnection":{ -- "url": "<?= /* @escapeNotVerified */ $block->getAjaxUrl() ?>", -+ "url": "<?= $block->escapeUrl($block->getAjaxUrl()) ?>", - "elementId": "<?= $block->getHtmlId() ?>", -- "successText": "<?= /* @escapeNotVerified */ __('Successful! Test again?') ?>", -- "failedText": "<?= /* @escapeNotVerified */ __('Connection failed! Test again?') ?>", -- "fieldMapping": "<?= /* @escapeNotVerified */ $block->getFieldMapping() ?>"}, "validation": {}}'> -- <span><span><span id="<?= $block->getHtmlId() ?>_result"><?= $block->escapeHtml($block->getButtonLabel()) ?></span></span></span> -+ "successText": "<?= $block->escapeHtmlAttr(__('Successful! Test again?')) ?>", -+ "failedText": "<?= $block->escapeHtmlAttr(__('Connection failed! Test again?')) ?>", -+ "fieldMapping": "<?= /* @noEscape */ $block->getFieldMapping() ?>"}, "validation": {}}'> -+ <span id="<?= $block->getHtmlId() ?>_result"><?= $block->escapeHtml($block->getButtonLabel()) ?></span> - </button> -diff --git a/app/code/Magento/AdvancedSearch/view/adminhtml/web/js/testconnection.js b/app/code/Magento/AdvancedSearch/view/adminhtml/web/js/testconnection.js -index e28f1b4d07d..ba08b8ecd29 100644 ---- a/app/code/Magento/AdvancedSearch/view/adminhtml/web/js/testconnection.js -+++ b/app/code/Magento/AdvancedSearch/view/adminhtml/web/js/testconnection.js -@@ -40,7 +40,8 @@ define([ - element = $('#' + this.options.elementId), - self = this, - params = {}, -- msg = ''; -+ msg = '', -+ fieldToCheck = this.options.fieldToCheck || 'success'; - - element.removeClass('success').addClass('fail'); - $.each($.parseJSON(this.options.fieldMapping), function (key, el) { -@@ -49,9 +50,10 @@ define([ - $.ajax({ - url: this.options.url, - showLoader: true, -- data: params -+ data: params, -+ headers: this.options.headers || {} - }).done(function (response) { -- if (response.success) { -+ if (response[fieldToCheck]) { - element.removeClass('fail').addClass('success'); - result = self.options.successText; - } else { -diff --git a/app/code/Magento/AdvancedSearch/view/frontend/templates/search_data.phtml b/app/code/Magento/AdvancedSearch/view/frontend/templates/search_data.phtml -index 6e660555053..1fa23cf3d62 100644 ---- a/app/code/Magento/AdvancedSearch/view/frontend/templates/search_data.phtml -+++ b/app/code/Magento/AdvancedSearch/view/frontend/templates/search_data.phtml -@@ -3,7 +3,7 @@ - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ --// @codingStandardsIgnoreFile -+ - /** - * @var \Magento\AdvancedSearch\Block\SearchData $block - */ -@@ -11,15 +11,15 @@ - <?php - /** @var \Magento\Search\Model\QueryResult[] $data */ - $data = $block->getItems(); --if (count($data)):?> -+if (count($data)) : ?> - <dl class="block"> -- <dt class="title"><?= /* @escapeNotVerified */ __($block->getTitle()) ?></dt> -+ <dt class="title"><?= $block->escapeHtml(__($block->getTitle())) ?></dt> - <?php foreach ($data as $additionalInfo) : ?> - <dd class="item"> -- <a href="<?= /* @escapeNotVerified */ $block->getLink($additionalInfo->getQueryText()) ?>" -+ <a href="<?= $block->escapeUrl($block->getLink($additionalInfo->getQueryText())) ?>" - ><?= $block->escapeHtml($additionalInfo->getQueryText()) ?></a> -- <?php if ($block->isShowResultsCount()): ?> -- <span class="count"><?= /* @escapeNotVerified */ $additionalInfo->getResultsCount() ?></span> -+ <?php if ($block->isShowResultsCount()) : ?> -+ <span class="count"><?= /* @noEscape */ (int)$additionalInfo->getResultsCount() ?></span> - <?php endif; ?> - </dd> - <?php endforeach; ?> -diff --git a/dev/tests/acceptance/tests/functional/Magento/FunctionalTest/Framework/LICENSE.txt b/app/code/Magento/AmqpStore/LICENSE.txt -similarity index 100% -rename from dev/tests/acceptance/tests/functional/Magento/FunctionalTest/Framework/LICENSE.txt -rename to app/code/Magento/AmqpStore/LICENSE.txt -diff --git a/dev/tests/acceptance/tests/functional/Magento/FunctionalTest/Framework/LICENSE_AFL.txt b/app/code/Magento/AmqpStore/LICENSE_AFL.txt -similarity index 100% -rename from dev/tests/acceptance/tests/functional/Magento/FunctionalTest/Framework/LICENSE_AFL.txt -rename to app/code/Magento/AmqpStore/LICENSE_AFL.txt -diff --git a/app/code/Magento/AmqpStore/Plugin/AsynchronousOperations/MassConsumerEnvelopeCallback.php b/app/code/Magento/AmqpStore/Plugin/AsynchronousOperations/MassConsumerEnvelopeCallback.php -new file mode 100644 -index 00000000000..e05004cea78 ---- /dev/null -+++ b/app/code/Magento/AmqpStore/Plugin/AsynchronousOperations/MassConsumerEnvelopeCallback.php -@@ -0,0 +1,101 @@ -+<?php -+/** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+ -+declare(strict_types=1); -+ -+namespace Magento\AmqpStore\Plugin\AsynchronousOperations; -+ -+use Magento\Framework\Exception\NoSuchEntityException; -+use Magento\Store\Model\StoreManagerInterface; -+use Magento\Framework\MessageQueue\EnvelopeFactory; -+use PhpAmqpLib\Wire\AMQPTable; -+use Magento\Framework\MessageQueue\EnvelopeInterface; -+use Magento\AsynchronousOperations\Model\MassConsumerEnvelopeCallback as SubjectMassConsumerEnvelopeCallback; -+use Psr\Log\LoggerInterface; -+ -+/** -+ * Plugin to get 'store_id' from the new custom header 'store_id' in amqp -+ * 'application_headers' properties and setCurrentStore by value 'store_id'. -+ */ -+class MassConsumerEnvelopeCallback -+{ -+ /** -+ * @var StoreManagerInterface -+ */ -+ private $storeManager; -+ -+ /** -+ * @var EnvelopeFactory -+ */ -+ private $envelopeFactory; -+ -+ /** -+ * @var LoggerInterface -+ */ -+ private $logger; -+ -+ /** -+ * @param EnvelopeFactory $envelopeFactory -+ * @param StoreManagerInterface $storeManager -+ * @param LoggerInterface $logger -+ */ -+ public function __construct( -+ EnvelopeFactory $envelopeFactory, -+ StoreManagerInterface $storeManager, -+ LoggerInterface $logger -+ ) { -+ $this->storeManager = $storeManager; -+ $this->envelopeFactory = $envelopeFactory; -+ $this->logger = $logger; -+ } -+ -+ /** -+ * Check if amqpProperties['application_headers'] have 'store_id' and use it to setCurrentStore -+ * Restore original store value in consumer process after execution. -+ * Reject queue messages because of wrong store_id. -+ * -+ * @param SubjectMassConsumerEnvelopeCallback $subject -+ * @param callable $proceed -+ * @param EnvelopeInterface $message -+ * @return void -+ * @SuppressWarnings(PHPMD.UnusedFormalParameter) -+ */ -+ public function aroundExecute( -+ SubjectMassConsumerEnvelopeCallback $subject, -+ callable $proceed, -+ EnvelopeInterface $message -+ ) { -+ $amqpProperties = $message->getProperties(); -+ if (isset($amqpProperties['application_headers'])) { -+ $headers = $amqpProperties['application_headers']; -+ if ($headers instanceof AMQPTable) { -+ $headers = $headers->getNativeData(); -+ } -+ if (isset($headers['store_id'])) { -+ $storeId = $headers['store_id']; -+ try { -+ $currentStoreId = $this->storeManager->getStore()->getId(); -+ } catch (NoSuchEntityException $e) { -+ $this->logger->error( -+ sprintf( -+ "Can't set currentStoreId during processing queue. Message rejected. Error %s.", -+ $e->getMessage() -+ ) -+ ); -+ $subject->getQueue()->reject($message, false, $e->getMessage()); -+ return; -+ } -+ if (isset($storeId) && $storeId !== $currentStoreId) { -+ $this->storeManager->setCurrentStore($storeId); -+ } -+ } -+ } -+ $proceed($message); -+ if (isset($storeId, $currentStoreId) && $storeId !== $currentStoreId) { -+ $this->storeManager->setCurrentStore($currentStoreId);//restore original store value -+ } -+ } -+} -diff --git a/app/code/Magento/AmqpStore/Plugin/Framework/Amqp/Bulk/Exchange.php b/app/code/Magento/AmqpStore/Plugin/Framework/Amqp/Bulk/Exchange.php -new file mode 100644 -index 00000000000..c5db1f5300c ---- /dev/null -+++ b/app/code/Magento/AmqpStore/Plugin/Framework/Amqp/Bulk/Exchange.php -@@ -0,0 +1,117 @@ -+<?php -+/** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+ -+declare(strict_types=1); -+ -+namespace Magento\AmqpStore\Plugin\Framework\Amqp\Bulk; -+ -+use Magento\Framework\Exception\NoSuchEntityException; -+use Magento\Store\Model\StoreManagerInterface; -+use Magento\Framework\MessageQueue\EnvelopeFactory; -+use PhpAmqpLib\Exception\AMQPInvalidArgumentException; -+use PhpAmqpLib\Wire\AMQPTable; -+use Magento\Framework\Amqp\Bulk\Exchange as SubjectExchange; -+use Magento\Framework\MessageQueue\EnvelopeInterface; -+use Psr\Log\LoggerInterface; -+ -+/** -+ * Plugin to set 'store_id' to the new custom header 'store_id' in amqp -+ * 'application_headers'. -+ */ -+class Exchange -+{ -+ /** -+ * @var StoreManagerInterface -+ */ -+ private $storeManager; -+ -+ /** -+ * @var EnvelopeFactory -+ */ -+ private $envelopeFactory; -+ -+ /** -+ * @var LoggerInterface -+ */ -+ private $logger; -+ -+ /** -+ * Exchange constructor. -+ * @param EnvelopeFactory $envelopeFactory -+ * @param StoreManagerInterface $storeManager -+ * @param LoggerInterface $logger -+ */ -+ public function __construct( -+ EnvelopeFactory $envelopeFactory, -+ StoreManagerInterface $storeManager, -+ LoggerInterface $logger -+ ) { -+ $this->storeManager = $storeManager; -+ $this->envelopeFactory = $envelopeFactory; -+ $this->logger = $logger; -+ } -+ -+ /** -+ * Set current store_id in amqpProperties['application_headers'] -+ * so consumer may check store_id and execute operation in correct store scope. -+ * Prevent publishing inconsistent messages because of store_id not defined or wrong. -+ * -+ * @param SubjectExchange $subject -+ * @param string $topic -+ * @param EnvelopeInterface[] $envelopes -+ * @return array -+ * @throws AMQPInvalidArgumentException -+ * @throws \LogicException -+ * @SuppressWarnings(PHPMD.UnusedFormalParameter) -+ */ -+ public function beforeEnqueue(SubjectExchange $subject, $topic, array $envelopes) -+ { -+ try { -+ $storeId = $this->storeManager->getStore()->getId(); -+ } catch (NoSuchEntityException $e) { -+ $errorMessage = sprintf( -+ "Can't get current storeId and inject to amqp message. Error %s.", -+ $e->getMessage() -+ ); -+ $this->logger->error($errorMessage); -+ throw new \LogicException($errorMessage); -+ } -+ -+ $updatedEnvelopes = []; -+ foreach ($envelopes as $envelope) { -+ $properties = $envelope->getProperties(); -+ if (!isset($properties)) { -+ $properties = []; -+ } -+ if (isset($properties['application_headers'])) { -+ $headers = $properties['application_headers']; -+ if ($headers instanceof AMQPTable) { -+ try { -+ $headers->set('store_id', $storeId); -+ // phpcs:ignore Magento2.Exceptions.ThrowCatch -+ } catch (AMQPInvalidArgumentException $ea) { -+ $errorMessage = sprintf("Can't set storeId to amqp message. Error %s.", $ea->getMessage()); -+ $this->logger->error($errorMessage); -+ throw new AMQPInvalidArgumentException($errorMessage); -+ } -+ $properties['application_headers'] = $headers; -+ } -+ } else { -+ $properties['application_headers'] = new AMQPTable(['store_id' => $storeId]); -+ } -+ $updatedEnvelopes[] = $this->envelopeFactory->create( -+ [ -+ 'body' => $envelope->getBody(), -+ 'properties' => $properties -+ ] -+ ); -+ } -+ if (!empty($updatedEnvelopes)) { -+ $envelopes = $updatedEnvelopes; -+ } -+ return [$topic, $envelopes]; -+ } -+} -diff --git a/app/code/Magento/AmqpStore/README.md b/app/code/Magento/AmqpStore/README.md -new file mode 100644 -index 00000000000..0f84c8ff327 ---- /dev/null -+++ b/app/code/Magento/AmqpStore/README.md -@@ -0,0 +1,3 @@ -+# Amqp Store -+ -+**AmqpStore** provides ability to specify store before publish messages with Amqp. -diff --git a/app/code/Magento/AmqpStore/composer.json b/app/code/Magento/AmqpStore/composer.json -new file mode 100644 -index 00000000000..bd11527bbb3 ---- /dev/null -+++ b/app/code/Magento/AmqpStore/composer.json -@@ -0,0 +1,30 @@ -+{ -+ "name": "magento/module-amqp-store", -+ "description": "N/A", -+ "config": { -+ "sort-packages": true -+ }, -+ "require": { -+ "magento/framework": "*", -+ "magento/framework-amqp": "*", -+ "magento/module-store": "*", -+ "php": "~7.1.3||~7.2.0" -+ }, -+ "suggest": { -+ "magento/module-asynchronous-operations": "*", -+ "magento/framework-message-queue": "*" -+ }, -+ "type": "magento2-module", -+ "license": [ -+ "OSL-3.0", -+ "AFL-3.0" -+ ], -+ "autoload": { -+ "files": [ -+ "registration.php" -+ ], -+ "psr-4": { -+ "Magento\\AmqpStore\\": "" -+ } -+ } -+} -diff --git a/app/code/Magento/AmqpStore/etc/di.xml b/app/code/Magento/AmqpStore/etc/di.xml -new file mode 100644 -index 00000000000..3bbbebd2495 ---- /dev/null -+++ b/app/code/Magento/AmqpStore/etc/di.xml -@@ -0,0 +1,15 @@ -+<?xml version="1.0"?> -+<!-- -+/** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd"> -+ <type name="Magento\Framework\Amqp\Bulk\Exchange"> -+ <plugin name="amqpStoreIdFieldForAmqpBulkExchange" type="Magento\AmqpStore\Plugin\Framework\Amqp\Bulk\Exchange"/> -+ </type> -+ <type name="Magento\AsynchronousOperations\Model\MassConsumerEnvelopeCallback"> -+ <plugin name="amqpStoreIdFieldForAsynchronousOperationsMassConsumerEnvelopeCallback" type="Magento\AmqpStore\Plugin\AsynchronousOperations\MassConsumerEnvelopeCallback"/> -+ </type> -+</config> -diff --git a/app/code/Magento/AmqpStore/etc/module.xml b/app/code/Magento/AmqpStore/etc/module.xml -new file mode 100644 -index 00000000000..085b97b5e2f ---- /dev/null -+++ b/app/code/Magento/AmqpStore/etc/module.xml -@@ -0,0 +1,14 @@ -+<?xml version="1.0"?> -+<!-- -+/** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:Module/etc/module.xsd"> -+ <module name="Magento_AmqpStore"> -+ <sequence> -+ <module name="Magento_Store"/> -+ </sequence> -+ </module> -+</config> -diff --git a/app/code/Magento/AmqpStore/registration.php b/app/code/Magento/AmqpStore/registration.php -new file mode 100644 -index 00000000000..4922879bfbf ---- /dev/null -+++ b/app/code/Magento/AmqpStore/registration.php -@@ -0,0 +1,9 @@ -+<?php -+/** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+ -+use \Magento\Framework\Component\ComponentRegistrar; -+ -+ComponentRegistrar::register(ComponentRegistrar::MODULE, 'Magento_AmqpStore', __DIR__); -diff --git a/app/code/Magento/Analytics/Controller/Adminhtml/BIEssentials/SignUp.php b/app/code/Magento/Analytics/Controller/Adminhtml/BIEssentials/SignUp.php -index ff9126a83d5..87666cb880e 100644 ---- a/app/code/Magento/Analytics/Controller/Adminhtml/BIEssentials/SignUp.php -+++ b/app/code/Magento/Analytics/Controller/Adminhtml/BIEssentials/SignUp.php -@@ -5,6 +5,7 @@ - */ - namespace Magento\Analytics\Controller\Adminhtml\BIEssentials; - -+use Magento\Framework\App\Action\HttpGetActionInterface as HttpGetActionInterface; - use Magento\Backend\App\Action; - use Magento\Backend\App\Action\Context; - use Magento\Framework\App\Config\ScopeConfigInterface; -@@ -12,7 +13,7 @@ use Magento\Framework\App\Config\ScopeConfigInterface; - /** - * Provides link to BI Essentials signup - */ --class SignUp extends Action -+class SignUp extends Action implements HttpGetActionInterface - { - /** - * Path to config value with URL to BI Essentials sign-up page. -diff --git a/app/code/Magento/Analytics/Controller/Adminhtml/Reports/Show.php b/app/code/Magento/Analytics/Controller/Adminhtml/Reports/Show.php -index cec09377770..9068654fa94 100644 ---- a/app/code/Magento/Analytics/Controller/Adminhtml/Reports/Show.php -+++ b/app/code/Magento/Analytics/Controller/Adminhtml/Reports/Show.php -@@ -5,6 +5,7 @@ - */ - namespace Magento\Analytics\Controller\Adminhtml\Reports; - -+use Magento\Framework\App\Action\HttpGetActionInterface as HttpGetActionInterface; - use Magento\Analytics\Model\Exception\State\SubscriptionUpdateException; - use Magento\Analytics\Model\ReportUrlProvider; - use Magento\Backend\App\Action; -@@ -16,7 +17,7 @@ use Magento\Framework\Exception\LocalizedException; - /** - * Provide redirect to resource with reports. - */ --class Show extends Action -+class Show extends Action implements HttpGetActionInterface - { - /** - * @var ReportUrlProvider -diff --git a/app/code/Magento/Analytics/Model/Cryptographer.php b/app/code/Magento/Analytics/Model/Cryptographer.php -index 665d564814b..efddcb501aa 100644 ---- a/app/code/Magento/Analytics/Model/Cryptographer.php -+++ b/app/code/Magento/Analytics/Model/Cryptographer.php -@@ -129,7 +129,12 @@ class Cryptographer - */ - private function validateCipherMethod($cipherMethod) - { -- $methods = openssl_get_cipher_methods(); -+ $methods = array_map( -+ 'strtolower', -+ openssl_get_cipher_methods() -+ ); -+ $cipherMethod = strtolower($cipherMethod); -+ - return (false !== array_search($cipherMethod, $methods)); - } - } -diff --git a/app/code/Magento/Analytics/Model/ReportXml/ModuleIterator.php b/app/code/Magento/Analytics/Model/ReportXml/ModuleIterator.php -index fecbf2033c1..4d623441974 100644 ---- a/app/code/Magento/Analytics/Model/ReportXml/ModuleIterator.php -+++ b/app/code/Magento/Analytics/Model/ReportXml/ModuleIterator.php -@@ -5,7 +5,7 @@ - */ - namespace Magento\Analytics\Model\ReportXml; - --use Magento\Framework\Module\Manager as ModuleManager; -+use \Magento\Framework\Module\ModuleManagerInterface as ModuleManager; - - /** - * Iterator for ReportXml modules -diff --git a/app/code/Magento/Analytics/README.md b/app/code/Magento/Analytics/README.md -index 7ec64abcd9b..f600f1d406e 100644 ---- a/app/code/Magento/Analytics/README.md -+++ b/app/code/Magento/Analytics/README.md -@@ -1,6 +1,6 @@ - # Magento_Analytics Module - --The Magento_Analytics module integrates your Magento instance with the [Magento Business Intelligence (MBI)](https://magento.com/products/business-intelligence) to use [Advanced Reporting](http://devdocs.magento.com/guides/v2.2/advanced-reporting/modules.html) functionality. -+The Magento_Analytics module integrates your Magento instance with the [Magento Business Intelligence (MBI)](https://magento.com/products/business-intelligence) to use [Advanced Reporting](https://devdocs.magento.com/guides/v2.2/advanced-reporting/modules.html) functionality. - - The module implements the following functionality: - -@@ -16,8 +16,8 @@ The module implements the following functionality: - - ## Structure - --Beyond the [usual module file structure](http://devdocs.magento.com/guides/v2.2/architecture/archi_perspectives/components/modules/mod_intro.html) the module contains a directory `ReportXml`. --[Report XML](http://devdocs.magento.com/guides/v2.2/advanced-reporting/report-xml.html) is a markup language used to build reports for Advanced Reporting. -+Beyond the [usual module file structure](https://devdocs.magento.com/guides/v2.2/architecture/archi_perspectives/components/modules/mod_intro.html) the module contains a directory `ReportXml`. -+[Report XML](https://devdocs.magento.com/guides/v2.2/advanced-reporting/report-xml.html) is a markup language used to build reports for Advanced Reporting. - The language declares SQL queries using XML declaration. - - ## Subscription Process -diff --git a/app/code/Magento/Analytics/ReportXml/DB/ColumnsResolver.php b/app/code/Magento/Analytics/ReportXml/DB/ColumnsResolver.php -index 14b80c6814b..3af168886a4 100644 ---- a/app/code/Magento/Analytics/ReportXml/DB/ColumnsResolver.php -+++ b/app/code/Magento/Analytics/ReportXml/DB/ColumnsResolver.php -@@ -76,7 +76,7 @@ class ColumnsResolver - $columnName = $this->nameResolver->getName($attributeData); - if (isset($attributeData['function'])) { - $prefix = ''; -- if (isset($attributeData['distinct']) && $attributeData['distinct'] == true) { -+ if (!empty($attributeData['distinct'])) { - $prefix = ' DISTINCT '; - } - $expression = new ColumnValueExpression( -diff --git a/app/code/Magento/Analytics/ReportXml/DB/SelectBuilder.php b/app/code/Magento/Analytics/ReportXml/DB/SelectBuilder.php -index 81ee9b15781..b4b7adebf74 100644 ---- a/app/code/Magento/Analytics/ReportXml/DB/SelectBuilder.php -+++ b/app/code/Magento/Analytics/ReportXml/DB/SelectBuilder.php -@@ -11,6 +11,7 @@ use Magento\Framework\DB\Select; - - /** - * Responsible for Select object creation, works as a builder. Returns Select as result; -+ * - * Used in SQL assemblers. - */ - class SelectBuilder -@@ -85,11 +86,13 @@ class SelectBuilder - * Set joins conditions - * - * @param array $joins -- * @return void -+ * @return $this - */ - public function setJoins($joins) - { - $this->joins = $joins; -+ -+ return $this; - } - - /** -@@ -106,11 +109,13 @@ class SelectBuilder - * Set connection name - * - * @param string $connectionName -- * @return void -+ * @return $this - */ - public function setConnectionName($connectionName) - { - $this->connectionName = $connectionName; -+ -+ return $this; - } - - /** -@@ -127,11 +132,13 @@ class SelectBuilder - * Set columns - * - * @param array $columns -- * @return void -+ * @return $this - */ - public function setColumns($columns) - { - $this->columns = $columns; -+ -+ return $this; - } - - /** -@@ -148,11 +155,13 @@ class SelectBuilder - * Set filters - * - * @param array $filters -- * @return void -+ * @return $this - */ - public function setFilters($filters) - { - $this->filters = $filters; -+ -+ return $this; - } - - /** -@@ -169,11 +178,13 @@ class SelectBuilder - * Set from condition - * - * @param array $from -- * @return void -+ * @return $this - */ - public function setFrom($from) - { - $this->from = $from; -+ -+ return $this; - } - - /** -@@ -236,11 +247,13 @@ class SelectBuilder - * Set group - * - * @param array $group -- * @return void -+ * @return $this - */ - public function setGroup($group) - { - $this->group = $group; -+ -+ return $this; - } - - /** -@@ -257,11 +270,13 @@ class SelectBuilder - * Set parameters - * - * @param array $params -- * @return void -+ * @return $this - */ - public function setParams($params) - { - $this->params = $params; -+ -+ return $this; - } - - /** -@@ -278,10 +293,12 @@ class SelectBuilder - * Set having condition - * - * @param array $having -- * @return void -+ * @return $this - */ - public function setHaving($having) - { - $this->having = $having; -+ -+ return $this; - } - } -diff --git a/app/code/Magento/Analytics/ReportXml/ReportProvider.php b/app/code/Magento/Analytics/ReportXml/ReportProvider.php -index 60e722930c2..8966d018dc6 100644 ---- a/app/code/Magento/Analytics/ReportXml/ReportProvider.php -+++ b/app/code/Magento/Analytics/ReportXml/ReportProvider.php -@@ -55,7 +55,7 @@ class ReportProvider - private function getIteratorName(Query $query) - { - $config = $query->getConfig(); -- return isset($config['iterator']) ? $config['iterator'] : null; -+ return $config['iterator'] ?? null; - } - - /** -diff --git a/app/code/Magento/Analytics/Test/Mftf/Data/AdminMenuData.xml b/app/code/Magento/Analytics/Test/Mftf/Data/AdminMenuData.xml -new file mode 100644 -index 00000000000..01b86101def ---- /dev/null -+++ b/app/code/Magento/Analytics/Test/Mftf/Data/AdminMenuData.xml -@@ -0,0 +1,21 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<entities xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataProfileSchema.xsd"> -+ <entity name="AdminMenuReports"> -+ <data key="pageTitle">Reports</data> -+ <data key="title">Reports</data> -+ <data key="dataUiId">magento-reports-report</data> -+ </entity> -+ <entity name="AdminMenuReportsBusinessIntelligenceAdvancedReporting"> -+ <data key="pageTitle">AdvancedReporting</data> -+ <data key="title">AdvancedReporting</data> -+ <data key="dataUiId">magento-analytics-advanced-reporting</data> -+ </entity> -+</entities> -diff --git a/app/code/Magento/Analytics/Test/Mftf/Data/UserData.xml b/app/code/Magento/Analytics/Test/Mftf/Data/UserData.xml -index 8324ad5ba99..83f27def4b4 100644 ---- a/app/code/Magento/Analytics/Test/Mftf/Data/UserData.xml -+++ b/app/code/Magento/Analytics/Test/Mftf/Data/UserData.xml -@@ -7,7 +7,7 @@ - --> - - <entities xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -- xsi:noNamespaceSchemaLocation="../../../../../../../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="adminNoReport" type="user"> - <data key="username" unique="suffix">noreport</data> - <data key="firstname">No</data> -@@ -18,7 +18,7 @@ - <data key="interface_local">en_US</data> - <data key="is_active">true</data> - <data key="current_password">123123q</data> --</entity> -+ </entity> - <entity name="restrictedWebUser" type="user"> - <data key="username" unique="suffix">restrictedWebUser</data> - <data key="firstname">restricted</data> -diff --git a/app/code/Magento/Analytics/Test/Mftf/Data/UserRoleData.xml b/app/code/Magento/Analytics/Test/Mftf/Data/UserRoleData.xml -index 71d8bdcd599..099cc71321b 100644 ---- a/app/code/Magento/Analytics/Test/Mftf/Data/UserRoleData.xml -+++ b/app/code/Magento/Analytics/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:DataGenerator/etc/dataProfileSchema.xsd"> - <entity name="adminNoReportRole" type="user_role"> - <data key="rolename" unique="suffix">noreport</data> - <data key="current_password">123123q</data> -diff --git a/app/code/Magento/Analytics/Test/Mftf/Metadata/user-meta.xml b/app/code/Magento/Analytics/Test/Mftf/Metadata/user-meta.xml -deleted file mode 100644 -index 06186d2d104..00000000000 ---- a/app/code/Magento/Analytics/Test/Mftf/Metadata/user-meta.xml -+++ /dev/null -@@ -1,23 +0,0 @@ --<?xml version="1.0" encoding="UTF-8"?> --<!-- -- /** -- * Copyright © Magento, Inc. All rights reserved. -- * See COPYING.txt for license details. -- */ ----> --<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="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> -- <field key="username">string</field> -- <field key="firstname">string</field> -- <field key="lastname">string</field> -- <field key="email">string</field> -- <field key="password">string</field> -- <field key="password_confirmation">string</field> -- <field key="interface_locale">string</field> -- <field key="is_active">boolean</field> -- <field key="current_password">string</field> -- </operation> --</operations> -diff --git a/app/code/Magento/Analytics/Test/Mftf/Metadata/user_role-meta.xml b/app/code/Magento/Analytics/Test/Mftf/Metadata/user_role-meta.xml -deleted file mode 100644 -index f5246880792..00000000000 ---- a/app/code/Magento/Analytics/Test/Mftf/Metadata/user_role-meta.xml -+++ /dev/null -@@ -1,19 +0,0 @@ --<?xml version="1.0" encoding="UTF-8"?> --<!-- -- /** -- * Copyright © Magento, Inc. All rights reserved. -- * See COPYING.txt for license details. -- */ ----> --<operations xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -- xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/DataGenerator/etc/dataOperation.xsd"> -- <operation name="CreateUserRole" dataType="user_role" type="create" -- auth="adminFormKey" url="/admin/user_role/saverole/" method="POST" successRegex="/messages-message-success/" returnRegex="" > -- <contentType>application/x-www-form-urlencoded</contentType> -- <field key="rolename">string</field> -- <field key="current_password">string</field> -- <array key="resource"> -- <value>string</value> -- </array> -- </operation> --</operations> -diff --git a/app/code/Magento/Analytics/Test/Mftf/Page/AdminConfigPage.xml b/app/code/Magento/Analytics/Test/Mftf/Page/AdminConfigPage.xml -new file mode 100644 -index 00000000000..c4ced12e67e ---- /dev/null -+++ b/app/code/Magento/Analytics/Test/Mftf/Page/AdminConfigPage.xml -@@ -0,0 +1,12 @@ -+<?xml version="1.0" encoding="utf-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+<pages xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/PageObject.xsd"> -+ <page name="AdminConfigGeneralAnalyticsPage" url="admin/system_config/edit/section/analytics/" area="admin" module="Magento_Config"> -+ <section name="AdminConfigAdvancedReportingSection"/> -+ </page> -+</pages> -diff --git a/app/code/Magento/Analytics/Test/Mftf/Section/AdminAdvancedReportingSection.xml b/app/code/Magento/Analytics/Test/Mftf/Section/AdminAdvancedReportingSection.xml -new file mode 100644 -index 00000000000..7b6851e5bdf ---- /dev/null -+++ b/app/code/Magento/Analytics/Test/Mftf/Section/AdminAdvancedReportingSection.xml -@@ -0,0 +1,12 @@ -+<?xml version="1.0" encoding="utf-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> -+ <section name="AdminAdvancedReportingSection"> -+ <element name="goToAdvancedReporting" type="text" selector="//div[@class='dashboard-advanced-reports-actions']/a[@title='Go to Advanced Reporting']" timeout="30"/> -+ </section> -+</sections> -\ No newline at end of file -diff --git a/app/code/Magento/Analytics/Test/Mftf/Section/AdminConfigAdvancedReportingSection.xml b/app/code/Magento/Analytics/Test/Mftf/Section/AdminConfigAdvancedReportingSection.xml -new file mode 100644 -index 00000000000..36e4779a48d ---- /dev/null -+++ b/app/code/Magento/Analytics/Test/Mftf/Section/AdminConfigAdvancedReportingSection.xml -@@ -0,0 +1,21 @@ -+<?xml version="1.0" encoding="utf-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> -+ <section name="AdminConfigAdvancedReportingSection"> -+ <element name="advancedReportingMenuItem" type="text" selector="//*[@class='admin__page-nav-link item-nav']//span[text()='Advanced Reporting']"/> -+ <element name="advancedReportingService" type="select" selector="#analytics_general_enabled"/> -+ <element name="advancedReportingServiceLabel" type="text" selector="#row_analytics_general_enabled>td.label>label>span"/> -+ <element name="advancedReportingServiceStatus" type="text" selector="#row_analytics_general_enabled>td.value>p>span"/> -+ <element name="advancedReportingIndustry" type="select" selector="#analytics_general_vertical"/> -+ <element name="advancedReportingIndustryLabel" type="text" selector=".config-vertical-label>label>span"/> -+ <element name="advancedReportingHour" type="select" selector="#row_analytics_general_collection_time>td:nth-child(2)>select:nth-child(2)"/> -+ <element name="advancedReportingMinute" type="select" selector="#row_analytics_general_collection_time>td:nth-child(2)>select:nth-child(4)"/> -+ <element name="advancedReportingSeconds" type="select" selector="#row_analytics_general_collection_time>td:nth-child(2)>select:nth-child(6)"/> -+ <element name="advancedReportingBlankIndustryError" type="text" selector=".message-error>div"/> -+ </section> -+</sections> -diff --git a/app/code/Magento/Analytics/Test/Mftf/Test/AdminAdvancedReportingButtonTest.xml b/app/code/Magento/Analytics/Test/Mftf/Test/AdminAdvancedReportingButtonTest.xml -new file mode 100644 -index 00000000000..e660a2eb8d4 ---- /dev/null -+++ b/app/code/Magento/Analytics/Test/Mftf/Test/AdminAdvancedReportingButtonTest.xml -@@ -0,0 +1,38 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> -+ <test name="AdminAdvancedReportingButtonTest"> -+ <annotations> -+ <stories value="AdvancedReporting"/> -+ <title value="AdvancedReportingButtonTest"/> -+ <description value="Test log in to AdvancedReporting and tests AdvancedReportingButtonTest"/> -+ <testCaseId value="MC-14800"/> -+ <skip> -+ <issueId value="MC-14800" /> -+ </skip> -+ <severity value="CRITICAL"/> -+ <group value="analytics"/> -+ <group value="mtf_migrated"/> -+ </annotations> -+ -+ <before> -+ <actionGroup ref = "LoginAsAdmin" stepKey="loginAsAdmin"/> -+ </before> -+ <after> -+ <actionGroup ref="logout" stepKey="logout"/> -+ </after> -+ -+ <!--Navigate through Advanced Reporting button on dashboard to Sign Up page--> -+ <amOnPage url="{{AdminDashboardPage.url}}" stepKey="amOnDashboardPage"/> -+ <waitForPageLoad stepKey="waitForDashboardPageLoad"/> -+ <click selector="{{AdminAdvancedReportingSection.goToAdvancedReporting}}" stepKey="clickGoToAdvancedReporting"/> -+ <switchToNextTab stepKey="switchToNewTab"/> -+ <seeInCurrentUrl url="advancedreporting.rjmetrics.com/report" stepKey="seeAssertAdvancedReportingPageUrl"/> -+ </test> -+</tests> -\ No newline at end of file -diff --git a/app/code/Magento/Analytics/Test/Mftf/Test/AdminAdvancedReportingNavigateMenuTest.xml b/app/code/Magento/Analytics/Test/Mftf/Test/AdminAdvancedReportingNavigateMenuTest.xml -new file mode 100644 -index 00000000000..67d67152856 ---- /dev/null -+++ b/app/code/Magento/Analytics/Test/Mftf/Test/AdminAdvancedReportingNavigateMenuTest.xml -@@ -0,0 +1,35 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> -+ <test name="AdminAdvancedReportingNavigateMenuTest"> -+ <annotations> -+ <features value="Analytics"/> -+ <stories value="Menu Navigation"/> -+ <title value="Admin advanced reporting navigate menu test"/> -+ <description value="Admin should be able to navigate through advanced reporting admin menu to BI reports page"/> -+ <severity value="CRITICAL"/> -+ <testCaseId value="MC-14152"/> -+ <group value="menu"/> -+ <group value="mtf_migrated"/> -+ </annotations> -+ <before> -+ <actionGroup ref="LoginAsAdmin" stepKey="LoginAsAdmin"/> -+ </before> -+ <after> -+ <actionGroup ref="logout" stepKey="logout"/> -+ </after> -+ <actionGroup ref="AdminNavigateMenuActionGroup" stepKey="navigateAdvancedReportingPage"> -+ <argument name="menuUiId" value="{{AdminMenuReports.dataUiId}}"/> -+ <argument name="submenuUiId" value="{{AdminMenuReportsBusinessIntelligenceAdvancedReporting.dataUiId}}"/> -+ </actionGroup> -+ <switchToNextTab stepKey="switchToNewTab"/> -+ <seeInCurrentUrl url="advancedreporting.rjmetrics.com/report" stepKey="seeAssertAdvancedReportingPageUrl"/> -+ </test> -+</tests> -diff --git a/app/code/Magento/Analytics/Test/Mftf/Test/AdminConfigurationBlankIndustryTest.xml b/app/code/Magento/Analytics/Test/Mftf/Test/AdminConfigurationBlankIndustryTest.xml -index fefb7874ef7..914cb59b64e 100644 ---- a/app/code/Magento/Analytics/Test/Mftf/Test/AdminConfigurationBlankIndustryTest.xml -+++ b/app/code/Magento/Analytics/Test/Mftf/Test/AdminConfigurationBlankIndustryTest.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="AdminConfigurationBlankIndustryTest"> - <annotations> - <features value="Analytics"/> -@@ -18,15 +18,14 @@ - <group value="analytics"/> - </annotations> - <after> -- <amOnPage stepKey="amOnLogoutPage" url="admin/admin/auth/logout/"/> -+ <actionGroup ref="logout" stepKey="logoutOfAdmin"/> - </after> - <actionGroup ref="LoginActionGroup" stepKey="loginAsAdmin"/> -- <amOnPage stepKey="amOnAdminConfig" url="{{AdminConfigPage.url}}"/> -- <waitForPageLoad stepKey="waitForAdminConfig"/> -- <click stepKey="clickAdvancedReportingConfigMenu" selector="{{AdminConfigSection.advancedReportingMenuItem}}"/> -- <see stepKey="seeAdvancedReportingIndustryLabel" selector="{{AdminConfigSection.advancedReportingIndustryLabel}}" userInput="Industry"/> -- <selectOption stepKey="selectAdvancedReportingIndustry" selector="{{AdminConfigSection.advancedReportingIndustry}}" userInput="--Please Select--"/> -- <click stepKey="clickSaveConfigButton" selector="{{AdminConfigSection.saveButton}}"/> -- <see stepKey="seeBlankIndustryErrorMessage" selector="{{AdminConfigSection.advancedReportingBlankIndustryError}}" userInput="Please select an industry."/> -+ <amOnPage url="{{AdminConfigGeneralAnalyticsPage.url}}" stepKey="amOnAdminConfig"/> -+ <selectOption selector="{{AdminConfigAdvancedReportingSection.advancedReportingService}}" userInput="Enable" stepKey="selectAdvancedReportingServiceEnabled"/> -+ <see selector="{{AdminConfigAdvancedReportingSection.advancedReportingIndustryLabel}}" userInput="Industry" stepKey="seeAdvancedReportingIndustryLabel"/> -+ <selectOption selector="{{AdminConfigAdvancedReportingSection.advancedReportingIndustry}}" userInput="--Please Select--" stepKey="selectAdvancedReportingIndustry"/> -+ <click selector="{{AdminMainActionsSection.save}}" stepKey="clickSaveConfigButton"/> -+ <see selector="{{AdminConfigAdvancedReportingSection.advancedReportingBlankIndustryError}}" userInput="Please select an industry." stepKey="seeBlankIndustryErrorMessage"/> - </test> - </tests> -diff --git a/app/code/Magento/Analytics/Test/Mftf/Test/AdminConfigurationEnableDisableAnalyticsTest.xml b/app/code/Magento/Analytics/Test/Mftf/Test/AdminConfigurationEnableDisableAnalyticsTest.xml -index 15c9727cc8c..1c1a3b27b06 100644 ---- a/app/code/Magento/Analytics/Test/Mftf/Test/AdminConfigurationEnableDisableAnalyticsTest.xml -+++ b/app/code/Magento/Analytics/Test/Mftf/Test/AdminConfigurationEnableDisableAnalyticsTest.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"> -+<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> - <test name="AdminConfigurationEnableDisableAnalyticsTest"> - <annotations> - <features value="Analytics"/> -@@ -16,32 +16,26 @@ - <severity value="MAJOR"/> - <testCaseId value="MAGETWO-66465"/> - <group value="analytics"/> -- <skip> -- <issueId value="MAGETWO-90659"/> -- </skip> - </annotations> - <after> -- <amOnPage stepKey="amOnLogoutPage" url="admin/admin/auth/logout/"/> -+ <actionGroup ref="logout" stepKey="logoutOfAdmin"/> - </after> - <actionGroup ref="LoginActionGroup" stepKey="loginAsAdmin"/> -- <!--Goto admin stores configure page --> -- <amOnPage stepKey="amOnAdminConfig" url="{{AdminConfigPage.url}}"/> -- <!--Enable Advanced Reporting--> -- <click stepKey="clickAdvancedReportingConfigMenu" selector="{{AdminConfigSection.advancedReportingMenuItem}}"/> -- <see stepKey="seeAdvancedReportingServiceLabelEnabled" selector="{{AdminConfigSection.advancedReportingServiceLabel}}" userInput="Advanced Reporting Service"/> -- <selectOption stepKey="selectAdvancedReportingServiceEnabled" selector="{{AdminConfigSection.advancedReportingService}}" userInput="Enable"/> -- <see stepKey="seeAdvancedReportingIndustryLabel" selector="{{AdminConfigSection.advancedReportingIndustryLabel}}" userInput="Industry"/> -- <selectOption stepKey="selectAdvancedReportingIndustry" selector="{{AdminConfigSection.advancedReportingIndustry}}" userInput="Apps and Games"/> -- <click stepKey="clickSaveConfigButtonEnabled" selector="{{AdminConfigSection.saveButton}}"/> -- <see stepKey="seeSaveConfigurationMessageEnabled" selector="{{AdminConfigSection.advancedReportingSuccessMessage}}" userInput="You saved the configuration."/> -- <see stepKey="seeAdvancedReportingServiceEnabled" selector="{{AdminConfigSection.advancedReportingService}}" userInput="Enable"/> -- <see stepKey="seeAdvancedReportingServiceStatusEnabled" selector="{{AdminConfigSection.advancedReportingServiceStatus}}" userInput="Subscription status: Pending"/> -+ <amOnPage url="{{AdminConfigGeneralAnalyticsPage.url}}" stepKey="amOnAdminConfig"/> -+ <see selector="{{AdminConfigAdvancedReportingSection.advancedReportingServiceLabel}}" userInput="Advanced Reporting Service" stepKey="seeAdvancedReportingServiceLabelEnabled"/> -+ <selectOption selector="{{AdminConfigAdvancedReportingSection.advancedReportingService}}" userInput="Enable" stepKey="selectAdvancedReportingServiceEnabled"/> -+ <see selector="{{AdminConfigAdvancedReportingSection.advancedReportingIndustryLabel}}" userInput="Industry" stepKey="seeAdvancedReportingIndustryLabel"/> -+ <selectOption selector="{{AdminConfigAdvancedReportingSection.advancedReportingIndustry}}" userInput="Apps and Games" stepKey="selectAdvancedReportingIndustry"/> -+ <click selector="{{AdminMainActionsSection.save}}" stepKey="clickSaveConfigButton1"/> -+ <see selector="{{AdminMessagesSection.success}}" userInput="You saved the configuration." stepKey="seeSuccess"/> -+ <see selector="{{AdminConfigAdvancedReportingSection.advancedReportingService}}" userInput="Enable" stepKey="seeAdvancedReportingServiceEnabled"/> -+ <see selector="{{AdminConfigAdvancedReportingSection.advancedReportingServiceStatus}}" userInput="Subscription status: Pending" stepKey="seeAdvancedReportingServiceStatusEnabled"/> - <!--Disable Advanced Reporting--> -- <see stepKey="seeAdvancedReportingServiceLabelDisabled" selector="{{AdminConfigSection.advancedReportingServiceLabel}}" userInput="Advanced Reporting Service"/> -- <selectOption stepKey="selectAdvancedReportingServiceDisabled" selector="{{AdminConfigSection.advancedReportingService}}" userInput="Disable"/> -- <click stepKey="clickSaveConfigButtonDisabled" selector="{{AdminConfigSection.saveButton}}"/> -- <see stepKey="seeSaveConfigurationMessageDisabled" selector="{{AdminConfigSection.advancedReportingSuccessMessage}}" userInput="You saved the configuration."/> -- <see stepKey="seeAdvancedReportingServiceDisabled" selector="{{AdminConfigSection.advancedReportingService}}" userInput="Disable"/> -- <see stepKey="seeAdvancedReportingServiceStatusDisabled" selector="{{AdminConfigSection.advancedReportingServiceStatus}}" userInput="Subscription status: Disabled"/> -+ <see selector="{{AdminConfigAdvancedReportingSection.advancedReportingServiceLabel}}" userInput="Advanced Reporting Service" stepKey="seeAdvancedReportingServiceLabelDisabled"/> -+ <selectOption selector="{{AdminConfigAdvancedReportingSection.advancedReportingService}}" userInput="Disable" stepKey="selectAdvancedReportingServiceDisabled"/> -+ <click selector="{{AdminMainActionsSection.save}}" stepKey="clickSaveConfigButton2"/> -+ <see selector="{{AdminMessagesSection.success}}" userInput="You saved the configuration." stepKey="seeSuccess2"/> -+ <see selector="{{AdminConfigAdvancedReportingSection.advancedReportingService}}" userInput="Disable" stepKey="seeAdvancedReportingServiceDisabled"/> -+ <see selector="{{AdminConfigAdvancedReportingSection.advancedReportingServiceStatus}}" userInput="Subscription status: Disabled" stepKey="seeAdvancedReportingServiceStatusDisabled"/> - </test> - </tests> -diff --git a/app/code/Magento/Analytics/Test/Mftf/Test/AdminConfigurationIndustryTest.xml b/app/code/Magento/Analytics/Test/Mftf/Test/AdminConfigurationIndustryTest.xml -index d4f30737bae..bb682c44680 100644 ---- a/app/code/Magento/Analytics/Test/Mftf/Test/AdminConfigurationIndustryTest.xml -+++ b/app/code/Magento/Analytics/Test/Mftf/Test/AdminConfigurationIndustryTest.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="AdminConfigurationIndustryTest"> - <annotations> - <features value="Analytics"/> -@@ -17,18 +17,14 @@ - <severity value="MAJOR"/> - <testCaseId value="MAGETWO-63898"/> - <group value="analytics"/> -- <skip> -- <issueId value="MAGETWO-90659"/> -- </skip> - </annotations> - - <actionGroup ref="LoginActionGroup" stepKey="loginAsAdmin"/> -- -- <amOnPage stepKey="amOnAdminConfig" url="{{AdminConfigPage.url}}"/> -- <click stepKey="clickAdvancedReportingConfigMenu" selector="{{AdminConfigSection.advancedReportingMenuItem}}"/> -- <see stepKey="seeAdvancedReportingIndustryLabel" selector="{{AdminConfigSection.advancedReportingIndustryLabel}}" userInput="Industry"/> -- <selectOption stepKey="selectAdvancedReportingIndustry" selector="{{AdminConfigSection.advancedReportingIndustry}}" userInput="Apps and Games"/> -- <click stepKey="clickSaveConfigButton" selector="{{AdminConfigSection.saveButton}}"/> -- <see stepKey="seeIndustrySuccessMessage" selector="{{AdminConfigSection.advancedReportingSuccessMessage}}" userInput="You saved the configuration."/> -+ <amOnPage url="{{AdminConfigGeneralAnalyticsPage.url}}" stepKey="amOnAdminConfig"/> -+ <selectOption selector="{{AdminConfigAdvancedReportingSection.advancedReportingService}}" userInput="Enable" stepKey="selectAdvancedReportingServiceEnabled"/> -+ <see selector="{{AdminConfigAdvancedReportingSection.advancedReportingIndustryLabel}}" userInput="Industry" stepKey="seeAdvancedReportingIndustryLabel"/> -+ <selectOption selector="{{AdminConfigAdvancedReportingSection.advancedReportingIndustry}}" userInput="Apps and Games" stepKey="selectAdvancedReportingIndustry"/> -+ <click selector="{{AdminMainActionsSection.save}}" stepKey="clickSaveConfigButton"/> -+ <see selector="{{AdminMessagesSection.success}}" userInput="You saved the configuration." stepKey="seeSuccess"/> - </test> - </tests> -diff --git a/app/code/Magento/Analytics/Test/Mftf/Test/AdminConfigurationPermissionTest.xml b/app/code/Magento/Analytics/Test/Mftf/Test/AdminConfigurationPermissionTest.xml -index b3ccd3afd1b..58e809ec45c 100644 ---- a/app/code/Magento/Analytics/Test/Mftf/Test/AdminConfigurationPermissionTest.xml -+++ b/app/code/Magento/Analytics/Test/Mftf/Test/AdminConfigurationPermissionTest.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="AdminConfigurationPermissionTest"> - <annotations> - <features value="Analytics"/> -@@ -17,49 +17,49 @@ - <severity value="CRITICAL"/> - <testCaseId value="MAGETWO-82648"/> - <group value="analytics"/> -- <skip> -- <issueId value="MAGETWO-90659"/> -- </skip> - </annotations> - <before> -- <createData stepKey="noReportUserRole" entity="adminNoReportRole"/> -- <createData stepKey="noReportUser" entity="adminNoReport"/> -+ <createData entity="adminNoReportRole" stepKey="noReportUserRole"/> -+ <createData entity="adminNoReport" stepKey="noReportUser"/> -+ <actionGroup ref="LoginActionGroup" stepKey="loginAsAdmin"/> - </before> - <after> -- <amOnPage stepKey="amOnLogoutPage" url="admin/admin/auth/logout/"/> -+ <actionGroup ref="logout" stepKey="logoutOfAdmin"/> - </after> -- <actionGroup ref="LoginActionGroup" stepKey="loginAsAdmin"/> -+ <amOnPage url="{{AdminUsersPage.url}}" stepKey="amOnAdminUsersPage"/> -+ <fillField selector="{{AdminUserGridSection.usernameFilterTextField}}" userInput="$$noReportUser.username$$" stepKey="fillUsernameSearch"/> -+ <click selector="{{AdminUserGridSection.searchButton}}" stepKey="clickSearchButton"/> -+ <waitForPageLoad time="10" stepKey="wait1"/> -+ <see selector="{{AdminUserGridSection.usernameInFirstRow}}" userInput="$$noReportUser.username$$" stepKey="seeFoundUsername"/> -+ <click selector="{{AdminUserGridSection.searchResultFirstRow}}" stepKey="clickFoundUsername"/> -+ <waitForPageLoad time="30" stepKey="wait2"/> -+ <seeInField selector="{{AdminEditUserSection.usernameTextField}}" userInput="$$noReportUser.username$$" stepKey="seeUsernameInField"/> -+ <fillField selector="{{AdminEditUserSection.currentPasswordField}}" userInput="{{_ENV.MAGENTO_ADMIN_PASSWORD}}" stepKey="fillCurrentPassword"/> -+ <click selector="{{AdminEditUserSection.userRoleTab}}" stepKey="clickUserRoleTab"/> - -- <amOnPage stepKey="amOnAdminUsersPage" url="{{AdminUsersPage.url}}"/> -- <fillField stepKey="fillUsernameSearch" selector="{{AdminUserGridSection.usernameFilterTextField}}" userInput="$$noReportUser.username$$"/> -- <click stepKey="clickSearchButton" selector="{{AdminUserGridSection.searchButton}}"/> -- <waitForPageLoad stepKey="wait1" time="10"/> -- <see stepKey="seeFoundUsername" selector="{{AdminUserGridSection.usernameInFirstRow}}" userInput="$$noReportUser.username$$"/> -- <click stepKey="clickFoundUsername" selector="{{AdminUserGridSection.searchResultFirstRow}}"/> -- <waitForPageLoad stepKey="wait2" time="30"/> -- <seeInField stepKey="seeUsernameInField" selector="{{AdminEditUserSection.usernameTextField}}" userInput="$$noReportUser.username$$"/> -- <fillField stepKey="fillCurrentPassword" selector="{{AdminEditUserSection.currentPasswordField}}" userInput="{{_ENV.MAGENTO_ADMIN_PASSWORD}}"/> -- <click stepKey="clickUserRoleTab" selector="{{AdminEditUserSection.userRoleTab}}"/> -+ <fillField selector="{{AdminEditUserSection.roleNameFilterTextField}}" userInput="$$noReportUserRole.rolename$$" stepKey="fillRoleNameSearch"/> -+ <click selector="{{AdminEditUserSection.searchButton}}" stepKey="clickSearchButtonUserRole"/> -+ <waitForPageLoad time="10" stepKey="wait3"/> -+ <see selector="{{AdminEditUserSection.roleNameInFirstRow}}" userInput="$$noReportUserRole.rolename$$" stepKey="seeFoundRoleName"/> -+ <click selector="{{AdminEditUserSection.searchResultFirstRow}}" stepKey="clickFoundRoleName"/> -+ <click selector="{{AdminEditUserSection.saveButton}}" stepKey="clickSaveButton"/> -+ <waitForPageLoad time="10" stepKey="wait4"/> -+ <see selector="{{AdminMessagesSection.success}}" userInput="You saved the user." stepKey="seeSuccess"/> - -- <fillField stepKey="fillRoleNameSearch" selector="{{AdminEditUserSection.roleNameFilterTextField}}" userInput="$$noReportUserRole.rolename$$"/> -- <click stepKey="clickSearchButtonUserRole" selector="{{AdminEditUserSection.searchButton}}"/> -- <waitForPageLoad stepKey="wait3" time="10"/> -- <see stepKey="seeFoundRoleName" selector="{{AdminEditUserSection.roleNameInFirstRow}}" userInput="$$noReportUserRole.rolename$$"/> -- <click stepKey="clickFoundRoleName" selector="{{AdminEditUserSection.searchResultFirstRow}}"/> -- <click stepKey="clickSaveButton" selector="{{AdminEditUserSection.saveButton}}"/> -- <waitForPageLoad stepKey="wait4" time="10"/> -- <see stepKey="saveUserSuccessMessage" selector="{{AdminUserGridSection.successMessage}}" userInput="You saved the user."/> -+ <amOnPage url="{{AdminConfigPage.url}}" stepKey="amOnAdminConfig"/> -+ <conditionalClick selector="{{AdminConfigSection.generalTab}}" dependentSelector="{{AdminConfigSection.generalTabOpened}}" visible="false" stepKey="openGeneralTabIfClosed"/> -+ <scrollTo selector="{{AdminConfigAdvancedReportingSection.advancedReportingMenuItem}}" stepKey="scrollToMenuItem"/> -+ <!--<see stepKey="seeAdvancedReportingConfigMenuItem" selector="{{AdminConfigAdvancedReportingSection.advancedReportingMenuItem}}" userInput="Advanced Reporting"/>--> -+ <seeElementInDOM selector="{{AdminConfigAdvancedReportingSection.advancedReportingMenuItem}}" stepKey="seeAdvancedReportingConfigMenuItem"/> -+ <actionGroup ref="logout" stepKey="logoutOfAdmin2"/> - -- <amOnPage stepKey="amOnAdminConfig" url="{{AdminConfigPage.url}}"/> -- <see stepKey="seeAdvancedReportingConfigMenuItem" selector="{{AdminConfigSection.advancedReportingMenuItem}}" userInput="Advanced Reporting"/> -- <amOnPage stepKey="amOnLogoutPage2" url="admin/admin/auth/logout/"/> -- -- <amOnPage stepKey="amOnAdminLoginPage" url="{{AdminLoginPage.url}}"/> -- <fillField stepKey="fillUsernameNoReport" selector="{{AdminLoginFormSection.username}}" userInput="$$noReportUser.username$$"/> -- <fillField stepKey="fillPasswordNoReport" selector="{{AdminLoginFormSection.password}}" userInput="$$noReportUser.password$$"/> -- <click stepKey="clickOnSignIn2" selector="{{AdminLoginFormSection.signIn}}"/> -- <waitForPageLoad stepKey="wait5" time="10"/> -- <amOnPage stepKey="amOnAdminConfig2" url="{{AdminConfigPage.url}}"/> -- <dontSee stepKey="dontSeeAdvancedReportingConfigMenuItem" selector="{{AdminConfigSection.advancedReportingMenuItem}}" userInput="Advanced Reporting"/> -+ <amOnPage url="{{AdminLoginPage.url}}" stepKey="amOnAdminLoginPage"/> -+ <fillField selector="{{AdminLoginFormSection.username}}" userInput="$$noReportUser.username$$" stepKey="fillUsernameNoReport"/> -+ <fillField selector="{{AdminLoginFormSection.password}}" userInput="$$noReportUser.password$$" stepKey="fillPasswordNoReport"/> -+ <click selector="{{AdminLoginFormSection.signIn}}" stepKey="clickOnSignIn2"/> -+ <waitForPageLoad time="10" stepKey="wait5"/> -+ <amOnPage url="{{AdminConfigPage.url}}" stepKey="amOnAdminConfig2"/> -+ <conditionalClick selector="{{AdminConfigSection.generalTab}}" dependentSelector="{{AdminConfigSection.generalTabOpened}}" visible="false" stepKey="openGeneralTabIfClosed2"/> -+ <dontSeeElementInDOM selector="{{AdminConfigAdvancedReportingSection.advancedReportingMenuItem}}" stepKey="dontSeeAdvancedReportingConfigMenuItem"/> - </test> - </tests> -diff --git a/app/code/Magento/Analytics/Test/Mftf/Test/AdminConfigurationTimeToSendDataTest.xml b/app/code/Magento/Analytics/Test/Mftf/Test/AdminConfigurationTimeToSendDataTest.xml -index fc1ff7d18b5..58e62500b82 100644 ---- a/app/code/Magento/Analytics/Test/Mftf/Test/AdminConfigurationTimeToSendDataTest.xml -+++ b/app/code/Magento/Analytics/Test/Mftf/Test/AdminConfigurationTimeToSendDataTest.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="AdminConfigurationTimeToSendDataTest"> - <annotations> - <features value="Analytics"/> -@@ -19,17 +19,16 @@ - <group value="analytics"/> - </annotations> - <after> -- <amOnPage stepKey="amOnLogoutPage" url="admin/admin/auth/logout/"/> -+ <actionGroup ref="logout" stepKey="logoutOfAdmin"/> - </after> - <actionGroup ref="LoginActionGroup" stepKey="loginAsAdmin"/> -- <amOnPage stepKey="amOnAdminConfig" url="{{AdminConfigPage.url}}"/> -- <waitForPageLoad stepKey="waitForAdminConfig"/> -- <click stepKey="clickAdvancedReportingConfigMenu" selector="{{AdminConfigSection.advancedReportingMenuItem}}"/> -- <selectOption stepKey="selectAdvancedReportingIndustry" selector="{{AdminConfigSection.advancedReportingIndustry}}" userInput="Apps and Games"/> -- <selectOption stepKey="selectAdvancedReportingHour" selector="{{AdminConfigSection.advancedReportingHour}}" userInput="11"/> -- <selectOption stepKey="selectAdvancedReportingMinute" selector="{{AdminConfigSection.advancedReportingMinute}}" userInput="11"/> -- <selectOption stepKey="selectAdvancedReportingSeconds" selector="{{AdminConfigSection.advancedReportingSeconds}}" userInput="00"/> -- <click stepKey="clickSaveConfigButton" selector="{{AdminConfigSection.saveButton}}"/> -- <see stepKey="seeBlankIndustryErrorMessage" selector="{{AdminConfigSection.advancedReportingSuccessMessage}}" userInput="You saved the configuration."/> -+ <amOnPage url="{{AdminConfigGeneralAnalyticsPage.url}}" stepKey="amOnAdminConfig"/> -+ <selectOption selector="{{AdminConfigAdvancedReportingSection.advancedReportingService}}" userInput="Enable" stepKey="selectAdvancedReportingServiceEnabled"/> -+ <selectOption selector="{{AdminConfigAdvancedReportingSection.advancedReportingIndustry}}" userInput="Apps and Games" stepKey="selectAdvancedReportingIndustry"/> -+ <selectOption selector="{{AdminConfigAdvancedReportingSection.advancedReportingHour}}" userInput="11" stepKey="selectAdvancedReportingHour"/> -+ <selectOption selector="{{AdminConfigAdvancedReportingSection.advancedReportingMinute}}" userInput="11" stepKey="selectAdvancedReportingMinute"/> -+ <selectOption selector="{{AdminConfigAdvancedReportingSection.advancedReportingSeconds}}" userInput="00" stepKey="selectAdvancedReportingSeconds"/> -+ <click selector="{{AdminMainActionsSection.save}}" stepKey="clickSaveConfigButton"/> -+ <see selector="{{AdminMessagesSection.success}}" userInput="You saved the configuration." stepKey="seeSuccess"/> - </test> - </tests> -diff --git a/app/code/Magento/Analytics/Test/Unit/Block/Adminhtml/System/Config/AdditionalCommentTest.php b/app/code/Magento/Analytics/Test/Unit/Block/Adminhtml/System/Config/AdditionalCommentTest.php -index 407e323aeaa..9428f8954c6 100644 ---- a/app/code/Magento/Analytics/Test/Unit/Block/Adminhtml/System/Config/AdditionalCommentTest.php -+++ b/app/code/Magento/Analytics/Test/Unit/Block/Adminhtml/System/Config/AdditionalCommentTest.php -@@ -39,6 +39,15 @@ class AdditionalCommentTest extends \PHPUnit\Framework\TestCase - ->setMethods(['getComment', 'getLabel']) - ->disableOriginalConstructor() - ->getMock(); -+ -+ $objectManager = new ObjectManager($this); -+ $escaper = $objectManager->getObject(\Magento\Framework\Escaper::class); -+ $reflection = new \ReflectionClass($this->abstractElementMock); -+ $reflection_property = $reflection->getProperty('_escaper'); -+ $reflection_property->setAccessible(true); -+ $reflection_property->setValue($this->abstractElementMock, $escaper); -+ -+ $this->abstractElementMock->setEscaper($escaper); - $this->contextMock = $this->getMockBuilder(Context::class) - ->disableOriginalConstructor() - ->getMock(); -diff --git a/app/code/Magento/Analytics/Test/Unit/Block/Adminhtml/System/Config/CollectionTimeLabelTest.php b/app/code/Magento/Analytics/Test/Unit/Block/Adminhtml/System/Config/CollectionTimeLabelTest.php -index d567d658823..08ee3c35693 100644 ---- a/app/code/Magento/Analytics/Test/Unit/Block/Adminhtml/System/Config/CollectionTimeLabelTest.php -+++ b/app/code/Magento/Analytics/Test/Unit/Block/Adminhtml/System/Config/CollectionTimeLabelTest.php -@@ -46,6 +46,14 @@ class CollectionTimeLabelTest extends \PHPUnit\Framework\TestCase - ->setMethods(['getComment']) - ->disableOriginalConstructor() - ->getMock(); -+ -+ $objectManager = new ObjectManager($this); -+ $escaper = $objectManager->getObject(\Magento\Framework\Escaper::class); -+ $reflection = new \ReflectionClass($this->abstractElementMock); -+ $reflection_property = $reflection->getProperty('_escaper'); -+ $reflection_property->setAccessible(true); -+ $reflection_property->setValue($this->abstractElementMock, $escaper); -+ - $this->contextMock = $this->getMockBuilder(Context::class) - ->setMethods(['getLocaleDate']) - ->disableOriginalConstructor() -diff --git a/app/code/Magento/Analytics/Test/Unit/Block/Adminhtml/System/Config/SubscriptionStatusLabelTest.php b/app/code/Magento/Analytics/Test/Unit/Block/Adminhtml/System/Config/SubscriptionStatusLabelTest.php -index 78ff581f3de..b43225be957 100644 ---- a/app/code/Magento/Analytics/Test/Unit/Block/Adminhtml/System/Config/SubscriptionStatusLabelTest.php -+++ b/app/code/Magento/Analytics/Test/Unit/Block/Adminhtml/System/Config/SubscriptionStatusLabelTest.php -@@ -51,6 +51,14 @@ class SubscriptionStatusLabelTest extends \PHPUnit\Framework\TestCase - ->setMethods(['getComment']) - ->disableOriginalConstructor() - ->getMock(); -+ -+ $objectManager = new ObjectManager($this); -+ $escaper = $objectManager->getObject(\Magento\Framework\Escaper::class); -+ $reflection = new \ReflectionClass($this->abstractElementMock); -+ $reflection_property = $reflection->getProperty('_escaper'); -+ $reflection_property->setAccessible(true); -+ $reflection_property->setValue($this->abstractElementMock, $escaper); -+ - $this->formMock = $this->getMockBuilder(Form::class) - ->disableOriginalConstructor() - ->getMock(); -diff --git a/app/code/Magento/Analytics/Test/Unit/Block/Adminhtml/System/Config/VerticalTest.php b/app/code/Magento/Analytics/Test/Unit/Block/Adminhtml/System/Config/VerticalTest.php -index 6a0cecc7810..0b5e86a5233 100644 ---- a/app/code/Magento/Analytics/Test/Unit/Block/Adminhtml/System/Config/VerticalTest.php -+++ b/app/code/Magento/Analytics/Test/Unit/Block/Adminhtml/System/Config/VerticalTest.php -@@ -39,6 +39,14 @@ class VerticalTest extends \PHPUnit\Framework\TestCase - ->setMethods(['getComment', 'getLabel', 'getHint']) - ->disableOriginalConstructor() - ->getMock(); -+ -+ $objectManager = new ObjectManager($this); -+ $escaper = $objectManager->getObject(\Magento\Framework\Escaper::class); -+ $reflection = new \ReflectionClass($this->abstractElementMock); -+ $reflection_property = $reflection->getProperty('_escaper'); -+ $reflection_property->setAccessible(true); -+ $reflection_property->setValue($this->abstractElementMock, $escaper); -+ - $this->contextMock = $this->getMockBuilder(Context::class) - ->disableOriginalConstructor() - ->getMock(); -diff --git a/app/code/Magento/Analytics/Test/Unit/Model/ExportDataHandlerTest.php b/app/code/Magento/Analytics/Test/Unit/Model/ExportDataHandlerTest.php -index 47747027ed7..cf00556cfe5 100644 ---- a/app/code/Magento/Analytics/Test/Unit/Model/ExportDataHandlerTest.php -+++ b/app/code/Magento/Analytics/Test/Unit/Model/ExportDataHandlerTest.php -@@ -191,7 +191,7 @@ class ExportDataHandlerTest extends \PHPUnit\Framework\TestCase - ->with( - $archiveSource, - $archiveAbsolutePath, -- $isArchiveSourceDirectory ? true : false -+ $isArchiveSourceDirectory - ); - - $fileContent = 'Some text'; -@@ -222,7 +222,7 @@ class ExportDataHandlerTest extends \PHPUnit\Framework\TestCase - { - return [ - 'Data source for archive is directory' => [true], -- 'Data source for archive doesn\'t directory' => [false], -+ 'Data source for archive isn\'t directory' => [false], - ]; - } - -diff --git a/app/code/Magento/Analytics/Test/Unit/Model/ReportXml/ModuleIteratorTest.php b/app/code/Magento/Analytics/Test/Unit/Model/ReportXml/ModuleIteratorTest.php -index f314d77f32b..b08d41ac829 100644 ---- a/app/code/Magento/Analytics/Test/Unit/Model/ReportXml/ModuleIteratorTest.php -+++ b/app/code/Magento/Analytics/Test/Unit/Model/ReportXml/ModuleIteratorTest.php -@@ -7,9 +7,12 @@ - namespace Magento\Analytics\Test\Unit\Model\ReportXml; - - use Magento\Analytics\Model\ReportXml\ModuleIterator; --use Magento\Framework\Module\Manager as ModuleManager; -+use \Magento\Framework\Module\ModuleManagerInterface as ModuleManager; - use Magento\Framework\TestFramework\Unit\Helper\ObjectManager as ObjectManagerHelper; - -+/** -+ * Module iterator test. -+ */ - class ModuleIteratorTest extends \PHPUnit\Framework\TestCase - { - /** -diff --git a/app/code/Magento/Analytics/Test/Unit/ReportXml/DB/SelectBuilderTest.php b/app/code/Magento/Analytics/Test/Unit/ReportXml/DB/SelectBuilderTest.php -index 8be2f0ee968..a4362d583df 100644 ---- a/app/code/Magento/Analytics/Test/Unit/ReportXml/DB/SelectBuilderTest.php -+++ b/app/code/Magento/Analytics/Test/Unit/ReportXml/DB/SelectBuilderTest.php -@@ -64,12 +64,12 @@ class SelectBuilderTest extends \PHPUnit\Framework\TestCase - ['link-type' => 'right', 'table' => 'attribute', 'condition' => 'neq'], - ]; - $groups = ['id', 'name']; -- $this->selectBuilder->setConnectionName($connectionName); -- $this->selectBuilder->setFrom($from); -- $this->selectBuilder->setColumns($columns); -- $this->selectBuilder->setFilters([$filter]); -- $this->selectBuilder->setJoins($joins); -- $this->selectBuilder->setGroup($groups); -+ $this->selectBuilder->setConnectionName($connectionName) -+ ->setFrom($from) -+ ->setColumns($columns) -+ ->setFilters([$filter]) -+ ->setJoins($joins) -+ ->setGroup($groups); - $this->resourceConnectionMock->expects($this->once()) - ->method('getConnection') - ->with($connectionName) -diff --git a/app/code/Magento/Analytics/etc/adminhtml/system.xml b/app/code/Magento/Analytics/etc/adminhtml/system.xml -index 4e21648d00c..c7da840b7e6 100644 ---- a/app/code/Magento/Analytics/etc/adminhtml/system.xml -+++ b/app/code/Magento/Analytics/etc/adminhtml/system.xml -@@ -36,6 +36,9 @@ - <source_model>Magento\Analytics\Model\Config\Source\Vertical</source_model> - <backend_model>Magento\Analytics\Model\Config\Backend\Vertical</backend_model> - <frontend_model>Magento\Analytics\Block\Adminhtml\System\Config\Vertical</frontend_model> -+ <depends> -+ <field id="analytics/general/enabled">1</field> -+ </depends> - </field> - <field id="additional_comment" translate="label comment" type="label" sortOrder="40" showInDefault="1" showInWebsite="0" showInStore="0"> - <label><![CDATA[<strong>Get more insights from Magento Business Intelligence</strong>]]></label> -diff --git a/app/code/Magento/Analytics/view/adminhtml/templates/dashboard/section.phtml b/app/code/Magento/Analytics/view/adminhtml/templates/dashboard/section.phtml -index a22c603b2a8..b056ffce1fa 100644 ---- a/app/code/Magento/Analytics/view/adminhtml/templates/dashboard/section.phtml -+++ b/app/code/Magento/Analytics/view/adminhtml/templates/dashboard/section.phtml -@@ -3,7 +3,6 @@ - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ --// @codingStandardsIgnoreFile - ?> - - <section class="dashboard-advanced-reports" data-index="dashboard-advanced-reports"> -diff --git a/app/code/Magento/AsynchronousOperations/Api/Data/BulkSummaryInterface.php b/app/code/Magento/AsynchronousOperations/Api/Data/BulkSummaryInterface.php -index f5dd5bb13ea..a433ec0953a 100644 ---- a/app/code/Magento/AsynchronousOperations/Api/Data/BulkSummaryInterface.php -+++ b/app/code/Magento/AsynchronousOperations/Api/Data/BulkSummaryInterface.php -@@ -13,6 +13,8 @@ namespace Magento\AsynchronousOperations\Api\Data; - */ - interface BulkSummaryInterface extends \Magento\Framework\Bulk\BulkSummaryInterface - { -+ const USER_TYPE = 'user_type'; -+ - /** - * Retrieve existing extension attributes object. - * -@@ -31,4 +33,19 @@ interface BulkSummaryInterface extends \Magento\Framework\Bulk\BulkSummaryInterf - public function setExtensionAttributes( - \Magento\AsynchronousOperations\Api\Data\BulkSummaryExtensionInterface $extensionAttributes - ); -+ -+ /** -+ * Get user type -+ * -+ * @return int -+ */ -+ public function getUserType(); -+ -+ /** -+ * Set user type -+ * -+ * @param int $userType -+ * @return $this -+ */ -+ public function setUserType($userType); - } -diff --git a/app/code/Magento/AsynchronousOperations/Controller/Adminhtml/Bulk/Details.php b/app/code/Magento/AsynchronousOperations/Controller/Adminhtml/Bulk/Details.php -index 9e9dbd3dd67..a450187dd09 100644 ---- a/app/code/Magento/AsynchronousOperations/Controller/Adminhtml/Bulk/Details.php -+++ b/app/code/Magento/AsynchronousOperations/Controller/Adminhtml/Bulk/Details.php -@@ -6,9 +6,9 @@ - namespace Magento\AsynchronousOperations\Controller\Adminhtml\Bulk; - - /** -- * Class View Opertion Details Controller -+ * Class View Operation Details Controller - */ --class Details extends \Magento\Backend\App\Action -+class Details extends \Magento\Backend\App\Action implements \Magento\Framework\App\Action\HttpGetActionInterface - { - /** - * @var \Magento\Framework\View\Result\PageFactory -diff --git a/app/code/Magento/AsynchronousOperations/Model/BulkManagement.php b/app/code/Magento/AsynchronousOperations/Model/BulkManagement.php -index 4f086ce8ac2..faf01921e57 100644 ---- a/app/code/Magento/AsynchronousOperations/Model/BulkManagement.php -+++ b/app/code/Magento/AsynchronousOperations/Model/BulkManagement.php -@@ -5,6 +5,7 @@ - */ - namespace Magento\AsynchronousOperations\Model; - -+use Magento\Framework\App\ObjectManager; - use Magento\Framework\App\ResourceConnection; - use Magento\AsynchronousOperations\Api\Data\BulkSummaryInterface; - use Magento\AsynchronousOperations\Api\Data\BulkSummaryInterfaceFactory; -@@ -13,6 +14,7 @@ use Magento\Framework\MessageQueue\BulkPublisherInterface; - use Magento\Framework\EntityManager\EntityManager; - use Magento\Framework\EntityManager\MetadataPool; - use Magento\AsynchronousOperations\Model\ResourceModel\Operation\CollectionFactory; -+use Magento\Authorization\Model\UserContextInterface; - - /** - * Class BulkManagement -@@ -51,6 +53,11 @@ class BulkManagement implements \Magento\Framework\Bulk\BulkManagementInterface - */ - private $resourceConnection; - -+ /** -+ * @var \Magento\Authorization\Model\UserContextInterface -+ */ -+ private $userContext; -+ - /** - * @var \Psr\Log\LoggerInterface - */ -@@ -65,6 +72,7 @@ class BulkManagement implements \Magento\Framework\Bulk\BulkManagementInterface - * @param MetadataPool $metadataPool - * @param ResourceConnection $resourceConnection - * @param \Psr\Log\LoggerInterface $logger -+ * @param UserContextInterface $userContext - */ - public function __construct( - EntityManager $entityManager, -@@ -73,7 +81,8 @@ class BulkManagement implements \Magento\Framework\Bulk\BulkManagementInterface - BulkPublisherInterface $publisher, - MetadataPool $metadataPool, - ResourceConnection $resourceConnection, -- \Psr\Log\LoggerInterface $logger -+ \Psr\Log\LoggerInterface $logger, -+ UserContextInterface $userContext = null - ) { - $this->entityManager = $entityManager; - $this->bulkSummaryFactory= $bulkSummaryFactory; -@@ -82,6 +91,7 @@ class BulkManagement implements \Magento\Framework\Bulk\BulkManagementInterface - $this->resourceConnection = $resourceConnection; - $this->publisher = $publisher; - $this->logger = $logger; -+ $this->userContext = $userContext ?: ObjectManager::getInstance()->get(UserContextInterface::class); - } - - /** -@@ -93,6 +103,10 @@ class BulkManagement implements \Magento\Framework\Bulk\BulkManagementInterface - $connection = $this->resourceConnection->getConnectionByName($metadata->getEntityConnectionName()); - // save bulk summary and related operations - $connection->beginTransaction(); -+ $userType = $this->userContext->getUserType(); -+ if ($userType === null) { -+ $userType = UserContextInterface::USER_TYPE_ADMIN; -+ } - try { - /** @var \Magento\AsynchronousOperations\Api\Data\BulkSummaryInterface $bulkSummary */ - $bulkSummary = $this->bulkSummaryFactory->create(); -@@ -100,6 +114,7 @@ class BulkManagement implements \Magento\Framework\Bulk\BulkManagementInterface - $bulkSummary->setBulkId($bulkUuid); - $bulkSummary->setDescription($description); - $bulkSummary->setUserId($userId); -+ $bulkSummary->setUserType($userType); - $bulkSummary->setOperationCount((int)$bulkSummary->getOperationCount() + count($operations)); - - $this->entityManager->save($bulkSummary); -diff --git a/app/code/Magento/AsynchronousOperations/Model/BulkSummary.php b/app/code/Magento/AsynchronousOperations/Model/BulkSummary.php -index e99233d0769..1d834ad10b2 100644 ---- a/app/code/Magento/AsynchronousOperations/Model/BulkSummary.php -+++ b/app/code/Magento/AsynchronousOperations/Model/BulkSummary.php -@@ -78,6 +78,22 @@ class BulkSummary extends DataObject implements BulkSummaryInterface, \Magento\F - return $this->setData(self::USER_ID, $userId); - } - -+ /** -+ * @inheritDoc -+ */ -+ public function getUserType() -+ { -+ return $this->getData(self::USER_TYPE); -+ } -+ -+ /** -+ * @inheritDoc -+ */ -+ public function setUserType($userType) -+ { -+ return $this->setData(self::USER_TYPE, $userType); -+ } -+ - /** - * @inheritDoc - */ -diff --git a/app/code/Magento/AsynchronousOperations/Model/MassConsumer.php b/app/code/Magento/AsynchronousOperations/Model/MassConsumer.php -index 28bc8141a8e..e3ba8b06819 100644 ---- a/app/code/Magento/AsynchronousOperations/Model/MassConsumer.php -+++ b/app/code/Magento/AsynchronousOperations/Model/MassConsumer.php -@@ -8,17 +8,11 @@ declare(strict_types=1); - - namespace Magento\AsynchronousOperations\Model; - --use Magento\Framework\App\ResourceConnection; --use Psr\Log\LoggerInterface; --use Magento\Framework\MessageQueue\MessageLockException; --use Magento\Framework\MessageQueue\ConnectionLostException; --use Magento\Framework\Exception\NotFoundException; --use Magento\Framework\MessageQueue\CallbackInvoker; -+use Magento\Framework\Registry; -+use Magento\Framework\MessageQueue\CallbackInvokerInterface; - use Magento\Framework\MessageQueue\ConsumerConfigurationInterface; - use Magento\Framework\MessageQueue\EnvelopeInterface; - use Magento\Framework\MessageQueue\QueueInterface; --use Magento\Framework\MessageQueue\LockInterface; --use Magento\Framework\MessageQueue\MessageController; - use Magento\Framework\MessageQueue\ConsumerInterface; - - /** -@@ -29,68 +23,53 @@ use Magento\Framework\MessageQueue\ConsumerInterface; - class MassConsumer implements ConsumerInterface - { - /** -- * @var \Magento\Framework\MessageQueue\CallbackInvoker -+ * @var CallbackInvokerInterface - */ - private $invoker; - -- /** -- * @var \Magento\Framework\App\ResourceConnection -- */ -- private $resource; -- - /** - * @var \Magento\Framework\MessageQueue\ConsumerConfigurationInterface - */ - private $configuration; - - /** -- * @var \Magento\Framework\MessageQueue\MessageController -+ * @var Registry - */ -- private $messageController; -+ private $registry; - - /** -- * @var LoggerInterface -+ * @var MassConsumerEnvelopeCallbackFactory - */ -- private $logger; -- -- /** -- * @var OperationProcessor -- */ -- private $operationProcessor; -+ private $massConsumerEnvelopeCallback; - - /** - * Initialize dependencies. - * -- * @param CallbackInvoker $invoker -- * @param ResourceConnection $resource -- * @param MessageController $messageController -+ * @param CallbackInvokerInterface $invoker - * @param ConsumerConfigurationInterface $configuration -- * @param OperationProcessorFactory $operationProcessorFactory -- * @param LoggerInterface $logger -+ * @param MassConsumerEnvelopeCallbackFactory $massConsumerEnvelopeCallback -+ * @param Registry $registry - */ - public function __construct( -- CallbackInvoker $invoker, -- ResourceConnection $resource, -- MessageController $messageController, -+ CallbackInvokerInterface $invoker, - ConsumerConfigurationInterface $configuration, -- OperationProcessorFactory $operationProcessorFactory, -- LoggerInterface $logger -+ MassConsumerEnvelopeCallbackFactory $massConsumerEnvelopeCallback, -+ Registry $registry = null - ) { - $this->invoker = $invoker; -- $this->resource = $resource; -- $this->messageController = $messageController; - $this->configuration = $configuration; -- $this->operationProcessor = $operationProcessorFactory->create([ -- 'configuration' => $configuration -- ]); -- $this->logger = $logger; -+ $this->massConsumerEnvelopeCallback = $massConsumerEnvelopeCallback; -+ $this->registry = $registry ?? \Magento\Framework\App\ObjectManager::getInstance() -+ ->get(Registry::class); - } - - /** -- * {@inheritdoc} -+ * @inheritdoc - */ - public function process($maxNumberOfMessages = null) - { -+ $this->registry->register('isSecureArea', true, true); -+ - $queue = $this->configuration->getQueue(); - - if (!isset($maxNumberOfMessages)) { -@@ -98,6 +77,8 @@ class MassConsumer implements ConsumerInterface - } else { - $this->invoker->invoke($queue, $maxNumberOfMessages, $this->getTransactionCallback($queue)); - } -+ -+ $this->registry->unregister('isSecureArea'); - } - - /** -@@ -108,38 +89,14 @@ class MassConsumer implements ConsumerInterface - */ - private function getTransactionCallback(QueueInterface $queue) - { -- return function (EnvelopeInterface $message) use ($queue) { -- /** @var LockInterface $lock */ -- $lock = null; -- try { -- $topicName = $message->getProperties()['topic_name']; -- $lock = $this->messageController->lock($message, $this->configuration->getConsumerName()); -- -- $allowedTopics = $this->configuration->getTopicNames(); -- if (in_array($topicName, $allowedTopics)) { -- $this->operationProcessor->process($message->getBody()); -- } else { -- $queue->reject($message); -- return; -- } -- $queue->acknowledge($message); -- } catch (MessageLockException $exception) { -- $queue->acknowledge($message); -- } catch (ConnectionLostException $e) { -- if ($lock) { -- $this->resource->getConnection() -- ->delete($this->resource->getTableName('queue_lock'), ['id = ?' => $lock->getId()]); -- } -- } catch (NotFoundException $e) { -- $queue->acknowledge($message); -- $this->logger->warning($e->getMessage()); -- } catch (\Exception $e) { -- $queue->reject($message, false, $e->getMessage()); -- if ($lock) { -- $this->resource->getConnection() -- ->delete($this->resource->getTableName('queue_lock'), ['id = ?' => $lock->getId()]); -- } -- } -+ $callbackInstance = $this->massConsumerEnvelopeCallback->create( -+ [ -+ 'configuration' => $this->configuration, -+ 'queue' => $queue, -+ ] -+ ); -+ return function (EnvelopeInterface $message) use ($callbackInstance) { -+ $callbackInstance->execute($message); - }; - } - } -diff --git a/app/code/Magento/AsynchronousOperations/Model/MassConsumerEnvelopeCallback.php b/app/code/Magento/AsynchronousOperations/Model/MassConsumerEnvelopeCallback.php -new file mode 100644 -index 00000000000..42437292e6f ---- /dev/null -+++ b/app/code/Magento/AsynchronousOperations/Model/MassConsumerEnvelopeCallback.php -@@ -0,0 +1,137 @@ -+<?php -+/** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+ -+declare(strict_types=1); -+ -+namespace Magento\AsynchronousOperations\Model; -+ -+use Magento\Framework\App\ResourceConnection; -+use Psr\Log\LoggerInterface; -+use Magento\Framework\MessageQueue\MessageLockException; -+use Magento\Framework\MessageQueue\ConnectionLostException; -+use Magento\Framework\Exception\NotFoundException; -+use Magento\Framework\MessageQueue\ConsumerConfigurationInterface; -+use Magento\Framework\MessageQueue\EnvelopeInterface; -+use Magento\Framework\MessageQueue\QueueInterface; -+use Magento\Framework\MessageQueue\LockInterface; -+use Magento\Framework\MessageQueue\MessageController; -+ -+/** -+ * Class used as public callback function by async consumer. -+ * @SuppressWarnings(PHPMD.CouplingBetweenObjects) -+ */ -+class MassConsumerEnvelopeCallback -+{ -+ /** -+ * @var QueueInterface -+ */ -+ private $queue; -+ -+ /** -+ * @var ResourceConnection -+ */ -+ private $resource; -+ -+ /** -+ * @var ConsumerConfigurationInterface -+ */ -+ private $configuration; -+ -+ /** -+ * @var MessageController -+ */ -+ private $messageController; -+ -+ /** -+ * @var LoggerInterface -+ */ -+ private $logger; -+ -+ /** -+ * @var OperationProcessor -+ */ -+ private $operationProcessor; -+ -+ /** -+ * @param ResourceConnection $resource -+ * @param MessageController $messageController -+ * @param ConsumerConfigurationInterface $configuration -+ * @param OperationProcessorFactory $operationProcessorFactory -+ * @param LoggerInterface $logger -+ * @param QueueInterface $queue -+ */ -+ public function __construct( -+ ResourceConnection $resource, -+ MessageController $messageController, -+ ConsumerConfigurationInterface $configuration, -+ OperationProcessorFactory $operationProcessorFactory, -+ LoggerInterface $logger, -+ QueueInterface $queue -+ ) { -+ $this->resource = $resource; -+ $this->messageController = $messageController; -+ $this->configuration = $configuration; -+ $this->operationProcessor = $operationProcessorFactory->create( -+ [ -+ 'configuration' => $configuration -+ ] -+ ); -+ $this->logger = $logger; -+ $this->queue = $queue; -+ } -+ -+ /** -+ * Get transaction callback. This handles the case of async. -+ * -+ * @param EnvelopeInterface $message -+ * @return void -+ */ -+ public function execute(EnvelopeInterface $message) -+ { -+ $queue = $this->queue; -+ /** @var LockInterface $lock */ -+ $lock = null; -+ try { -+ $topicName = $message->getProperties()['topic_name']; -+ $lock = $this->messageController->lock($message, $this->configuration->getConsumerName()); -+ -+ $allowedTopics = $this->configuration->getTopicNames(); -+ if (in_array($topicName, $allowedTopics)) { -+ $this->operationProcessor->process($message->getBody()); -+ } else { -+ $queue->reject($message); -+ return; -+ } -+ $queue->acknowledge($message); -+ } catch (MessageLockException $exception) { -+ $queue->acknowledge($message); -+ } catch (ConnectionLostException $e) { -+ if ($lock) { -+ $this->resource->getConnection() -+ ->delete($this->resource->getTableName('queue_lock'), ['id = ?' => $lock->getId()]); -+ } -+ } catch (NotFoundException $e) { -+ $queue->acknowledge($message); -+ $this->logger->warning($e->getMessage()); -+ } catch (\Exception $e) { -+ $queue->reject($message, false, $e->getMessage()); -+ if ($lock) { -+ $this->resource->getConnection() -+ ->delete($this->resource->getTableName('queue_lock'), ['id = ?' => $lock->getId()]); -+ } -+ } -+ } -+ -+ /** -+ * Get message queue. -+ * -+ * @return QueueInterface -+ */ -+ public function getQueue() -+ { -+ return $this->queue; -+ } -+} -diff --git a/app/code/Magento/AsynchronousOperations/Model/MassSchedule.php b/app/code/Magento/AsynchronousOperations/Model/MassSchedule.php -index 2d516e82f40..89d468159c6 100644 ---- a/app/code/Magento/AsynchronousOperations/Model/MassSchedule.php -+++ b/app/code/Magento/AsynchronousOperations/Model/MassSchedule.php -@@ -8,6 +8,7 @@ declare(strict_types=1); - - namespace Magento\AsynchronousOperations\Model; - -+use Magento\Framework\App\ObjectManager; - use Magento\Framework\DataObject\IdentityGeneratorInterface; - use Magento\Framework\Exception\LocalizedException; - use Magento\AsynchronousOperations\Api\Data\ItemStatusInterfaceFactory; -@@ -18,9 +19,13 @@ use Magento\Framework\Bulk\BulkManagementInterface; - use Magento\Framework\Exception\BulkException; - use Psr\Log\LoggerInterface; - use Magento\AsynchronousOperations\Model\ResourceModel\Operation\OperationRepository; -+use Magento\Authorization\Model\UserContextInterface; -+use Magento\Framework\Encryption\Encryptor; - - /** - * Class MassSchedule used for adding multiple entities as Operations to Bulk Management with the status tracking -+ * -+ * @SuppressWarnings(PHPMD.CouplingBetweenObjects) Suppressed without refactoring to not introduce BiC - */ - class MassSchedule - { -@@ -54,6 +59,16 @@ class MassSchedule - */ - private $operationRepository; - -+ /** -+ * @var \Magento\Authorization\Model\UserContextInterface -+ */ -+ private $userContext; -+ -+ /** -+ * @var Encryptor -+ */ -+ private $encryptor; -+ - /** - * Initialize dependencies. - * -@@ -63,6 +78,8 @@ class MassSchedule - * @param BulkManagementInterface $bulkManagement - * @param LoggerInterface $logger - * @param OperationRepository $operationRepository -+ * @param UserContextInterface $userContext -+ * @param Encryptor|null $encryptor - */ - public function __construct( - IdentityGeneratorInterface $identityService, -@@ -70,7 +87,9 @@ class MassSchedule - AsyncResponseInterfaceFactory $asyncResponseFactory, - BulkManagementInterface $bulkManagement, - LoggerInterface $logger, -- OperationRepository $operationRepository -+ OperationRepository $operationRepository, -+ UserContextInterface $userContext = null, -+ Encryptor $encryptor = null - ) { - $this->identityService = $identityService; - $this->itemStatusInterfaceFactory = $itemStatusInterfaceFactory; -@@ -78,15 +97,17 @@ class MassSchedule - $this->bulkManagement = $bulkManagement; - $this->logger = $logger; - $this->operationRepository = $operationRepository; -+ $this->userContext = $userContext ?: ObjectManager::getInstance()->get(UserContextInterface::class); -+ $this->encryptor = $encryptor ?: ObjectManager::getInstance()->get(Encryptor::class); - } - - /** - * Schedule new bulk operation based on the list of entities - * -- * @param $topicName -- * @param $entitiesArray -- * @param null $groupId -- * @param null $userId -+ * @param string $topicName -+ * @param array $entitiesArray -+ * @param string $groupId -+ * @param string $userId - * @return AsyncResponseInterface - * @throws BulkException - * @throws LocalizedException -@@ -95,6 +116,10 @@ class MassSchedule - { - $bulkDescription = __('Topic %1', $topicName); - -+ if ($userId == null) { -+ $userId = $this->userContext->getUserId(); -+ } -+ - if ($groupId == null) { - $groupId = $this->identityService->generateId(); - -@@ -114,9 +139,13 @@ class MassSchedule - $requestItem = $this->itemStatusInterfaceFactory->create(); - - try { -- $operations[] = $this->operationRepository->createByTopic($topicName, $entityParams, $groupId); -+ $operation = $this->operationRepository->createByTopic($topicName, $entityParams, $groupId); -+ $operations[] = $operation; - $requestItem->setId($key); - $requestItem->setStatus(ItemStatusInterface::STATUS_ACCEPTED); -+ $requestItem->setDataHash( -+ $this->encryptor->hash($operation->getSerializedData(), Encryptor::HASH_VERSION_SHA256) -+ ); - $requestItems[] = $requestItem; - } catch (\Exception $exception) { - $this->logger->error($exception); -diff --git a/app/code/Magento/AsynchronousOperations/Test/Unit/Model/BulkManagementTest.php b/app/code/Magento/AsynchronousOperations/Test/Unit/Model/BulkManagementTest.php -index 3a45c34df17..e5951fb129e 100644 ---- a/app/code/Magento/AsynchronousOperations/Test/Unit/Model/BulkManagementTest.php -+++ b/app/code/Magento/AsynchronousOperations/Test/Unit/Model/BulkManagementTest.php -@@ -108,6 +108,7 @@ class BulkManagementTest extends \PHPUnit\Framework\TestCase - $bulkUuid = 'bulk-001'; - $description = 'Bulk summary description...'; - $userId = 1; -+ $userType = \Magento\Authorization\Model\UserContextInterface::USER_TYPE_ADMIN; - $connectionName = 'default'; - $topicNames = ['topic.name.0', 'topic.name.1']; - $operation = $this->getMockBuilder(\Magento\AsynchronousOperations\Api\Data\OperationInterface::class) -@@ -131,6 +132,7 @@ class BulkManagementTest extends \PHPUnit\Framework\TestCase - $bulkSummary->expects($this->once())->method('setBulkId')->with($bulkUuid)->willReturnSelf(); - $bulkSummary->expects($this->once())->method('setDescription')->with($description)->willReturnSelf(); - $bulkSummary->expects($this->once())->method('setUserId')->with($userId)->willReturnSelf(); -+ $bulkSummary->expects($this->once())->method('setUserType')->with($userType)->willReturnSelf(); - $bulkSummary->expects($this->once())->method('getOperationCount')->willReturn(1); - $bulkSummary->expects($this->once())->method('setOperationCount')->with(3)->willReturnSelf(); - $this->entityManager->expects($this->once())->method('save')->with($bulkSummary)->willReturn($bulkSummary); -diff --git a/app/code/Magento/AsynchronousOperations/composer.json b/app/code/Magento/AsynchronousOperations/composer.json -index 7d5a097eeea..18927b5f4ec 100644 ---- a/app/code/Magento/AsynchronousOperations/composer.json -+++ b/app/code/Magento/AsynchronousOperations/composer.json -@@ -10,7 +10,6 @@ - "magento/module-authorization": "*", - "magento/module-backend": "*", - "magento/module-ui": "*", -- "magento/module-user": "*", - "php": "~7.1.3||~7.2.0" - }, - "suggest": { -diff --git a/app/code/Magento/AsynchronousOperations/etc/db_schema.xml b/app/code/Magento/AsynchronousOperations/etc/db_schema.xml -index 1b99ce9a280..5cd55408838 100644 ---- a/app/code/Magento/AsynchronousOperations/etc/db_schema.xml -+++ b/app/code/Magento/AsynchronousOperations/etc/db_schema.xml -@@ -14,20 +14,22 @@ - <column xsi:type="varbinary" name="uuid" nullable="true" length="39" - comment="Bulk UUID (can be exposed to reference bulk entity)"/> - <column xsi:type="int" name="user_id" padding="10" unsigned="true" nullable="true" identity="false" -- comment="ID of the user that performed an action"/> -+ comment="ID of the WebAPI user that performed an action"/> -+ <column xsi:type="int" name="user_type" nullable="true" comment="Which type of user"/> - <column xsi:type="varchar" name="description" nullable="true" length="255" comment="Bulk Description"/> - <column xsi:type="int" name="operation_count" padding="10" unsigned="true" nullable="false" identity="false" - comment="Total number of operations scheduled within this bulk"/> - <column xsi:type="timestamp" name="start_time" on_update="false" nullable="false" default="CURRENT_TIMESTAMP" - comment="Bulk start time"/> -- <constraint xsi:type="primary" name="PRIMARY"> -+ <constraint xsi:type="primary" referenceId="PRIMARY"> - <column name="id"/> - </constraint> -- <constraint xsi:type="foreign" name="MAGENTO_BULK_USER_ID_ADMIN_USER_USER_ID" table="magento_bulk" -- column="user_id" referenceTable="admin_user" referenceColumn="user_id" onDelete="CASCADE"/> -- <constraint xsi:type="unique" name="MAGENTO_BULK_UUID"> -+ <constraint xsi:type="unique" referenceId="MAGENTO_BULK_UUID"> - <column name="uuid"/> - </constraint> -+ <index referenceId="MAGENTO_BULK_USER_ID_ADMIN_USER_USER_ID" indexType="btree"> -+ <column name="user_id"/> -+ </index> - </table> - <table name="magento_operation" resource="default" engine="innodb" comment="Operation entity"> - <column xsi:type="int" name="id" padding="10" unsigned="true" nullable="false" identity="true" -@@ -45,12 +47,12 @@ - comment="Code of the error that appeared during operation execution (used to aggregate related failed operations)"/> - <column xsi:type="varchar" name="result_message" nullable="true" length="255" - comment="Operation result message"/> -- <constraint xsi:type="primary" name="PRIMARY"> -+ <constraint xsi:type="primary" referenceId="PRIMARY"> - <column name="id"/> - </constraint> -- <constraint xsi:type="foreign" name="MAGENTO_OPERATION_BULK_UUID_MAGENTO_BULK_UUID" table="magento_operation" -+ <constraint xsi:type="foreign" referenceId="MAGENTO_OPERATION_BULK_UUID_MAGENTO_BULK_UUID" table="magento_operation" - column="bulk_uuid" referenceTable="magento_bulk" referenceColumn="uuid" onDelete="CASCADE"/> -- <index name="MAGENTO_OPERATION_BULK_UUID_ERROR_CODE" indexType="btree"> -+ <index referenceId="MAGENTO_OPERATION_BULK_UUID_ERROR_CODE" indexType="btree"> - <column name="bulk_uuid"/> - <column name="error_code"/> - </index> -@@ -60,13 +62,13 @@ - <column xsi:type="int" name="id" padding="10" unsigned="true" nullable="false" identity="true" - comment="Internal ID"/> - <column xsi:type="varbinary" name="bulk_uuid" nullable="true" length="39" comment="Related Bulk UUID"/> -- <constraint xsi:type="primary" name="PRIMARY"> -+ <constraint xsi:type="primary" referenceId="PRIMARY"> - <column name="id"/> - </constraint> -- <constraint xsi:type="foreign" name="MAGENTO_ACKNOWLEDGED_BULK_BULK_UUID_MAGENTO_BULK_UUID" -+ <constraint xsi:type="foreign" referenceId="MAGENTO_ACKNOWLEDGED_BULK_BULK_UUID_MAGENTO_BULK_UUID" - table="magento_acknowledged_bulk" column="bulk_uuid" referenceTable="magento_bulk" - referenceColumn="uuid" onDelete="CASCADE"/> -- <constraint xsi:type="unique" name="MAGENTO_ACKNOWLEDGED_BULK_BULK_UUID"> -+ <constraint xsi:type="unique" referenceId="MAGENTO_ACKNOWLEDGED_BULK_BULK_UUID"> - <column name="bulk_uuid"/> - </constraint> - </table> -diff --git a/app/code/Magento/AsynchronousOperations/etc/db_schema_whitelist.json b/app/code/Magento/AsynchronousOperations/etc/db_schema_whitelist.json -index d62f16ffca8..9b6c0709e19 100644 ---- a/app/code/Magento/AsynchronousOperations/etc/db_schema_whitelist.json -+++ b/app/code/Magento/AsynchronousOperations/etc/db_schema_whitelist.json -@@ -4,14 +4,19 @@ - "id": true, - "uuid": true, - "user_id": true, -+ "user_type": true, - "description": true, - "operation_count": true, - "start_time": true - }, -+ "index": { -+ "MAGENTO_BULK_USER_ID_ADMIN_USER_USER_ID": true, -+ "MAGENTO_BULK_USER_ID": true -+ }, - "constraint": { - "PRIMARY": true, -- "MAGENTO_BULK_USER_ID_ADMIN_USER_USER_ID": true, -- "MAGENTO_BULK_UUID": true -+ "MAGENTO_BULK_UUID": true, -+ "MAGENTO_BULK_USER_ID_ADMIN_USER_USER_ID": true - } - }, - "magento_operation": { -diff --git a/app/code/Magento/Authorization/Model/Role.php b/app/code/Magento/Authorization/Model/Role.php -index 2546df86d09..dcc46ee77ee 100644 ---- a/app/code/Magento/Authorization/Model/Role.php -+++ b/app/code/Magento/Authorization/Model/Role.php -@@ -51,19 +51,29 @@ class Role extends \Magento\Framework\Model\AbstractModel - } - - /** -- * {@inheritdoc} -+ * @inheritDoc -+ * -+ * @SuppressWarnings(PHPMD.SerializationAware) -+ * @deprecated Do not use PHP serialization. - */ - public function __sleep() - { -+ trigger_error('Using PHP serialization is deprecated', E_USER_DEPRECATED); -+ - $properties = parent::__sleep(); - return array_diff($properties, ['_resource', '_resourceCollection']); - } - - /** -- * {@inheritdoc} -+ * @inheritDoc -+ * -+ * @SuppressWarnings(PHPMD.SerializationAware) -+ * @deprecated Do not use PHP serialization. - */ - public function __wakeup() - { -+ trigger_error('Using PHP serialization is deprecated', E_USER_DEPRECATED); -+ - parent::__wakeup(); - $objectManager = \Magento\Framework\App\ObjectManager::getInstance(); - $this->_resource = $objectManager->get(\Magento\Authorization\Model\ResourceModel\Role::class); -diff --git a/app/code/Magento/Authorization/etc/db_schema.xml b/app/code/Magento/Authorization/etc/db_schema.xml -index 45c02128bfc..a38828eb6ef 100644 ---- a/app/code/Magento/Authorization/etc/db_schema.xml -+++ b/app/code/Magento/Authorization/etc/db_schema.xml -@@ -21,14 +21,14 @@ - comment="User ID"/> - <column xsi:type="varchar" name="user_type" nullable="true" length="16" comment="User Type"/> - <column xsi:type="varchar" name="role_name" nullable="true" length="50" comment="Role Name"/> -- <constraint xsi:type="primary" name="PRIMARY"> -+ <constraint xsi:type="primary" referenceId="PRIMARY"> - <column name="role_id"/> - </constraint> -- <index name="AUTHORIZATION_ROLE_PARENT_ID_SORT_ORDER" indexType="btree"> -+ <index referenceId="AUTHORIZATION_ROLE_PARENT_ID_SORT_ORDER" indexType="btree"> - <column name="parent_id"/> - <column name="sort_order"/> - </index> -- <index name="AUTHORIZATION_ROLE_TREE_LEVEL" indexType="btree"> -+ <index referenceId="AUTHORIZATION_ROLE_TREE_LEVEL" indexType="btree"> - <column name="tree_level"/> - </index> - </table> -@@ -40,17 +40,17 @@ - <column xsi:type="varchar" name="resource_id" nullable="true" length="255" comment="Resource ID"/> - <column xsi:type="varchar" name="privileges" nullable="true" length="20" comment="Privileges"/> - <column xsi:type="varchar" name="permission" nullable="true" length="10" comment="Permission"/> -- <constraint xsi:type="primary" name="PRIMARY"> -+ <constraint xsi:type="primary" referenceId="PRIMARY"> - <column name="rule_id"/> - </constraint> -- <constraint xsi:type="foreign" name="AUTHORIZATION_RULE_ROLE_ID_AUTHORIZATION_ROLE_ROLE_ID" -+ <constraint xsi:type="foreign" referenceId="AUTHORIZATION_RULE_ROLE_ID_AUTHORIZATION_ROLE_ROLE_ID" - table="authorization_rule" column="role_id" referenceTable="authorization_role" - referenceColumn="role_id" onDelete="CASCADE"/> -- <index name="AUTHORIZATION_RULE_RESOURCE_ID_ROLE_ID" indexType="btree"> -+ <index referenceId="AUTHORIZATION_RULE_RESOURCE_ID_ROLE_ID" indexType="btree"> - <column name="resource_id"/> - <column name="role_id"/> - </index> -- <index name="AUTHORIZATION_RULE_ROLE_ID_RESOURCE_ID" indexType="btree"> -+ <index referenceId="AUTHORIZATION_RULE_ROLE_ID_RESOURCE_ID" indexType="btree"> - <column name="role_id"/> - <column name="resource_id"/> - </index> -diff --git a/app/code/Magento/Authorizenet/Block/Adminhtml/Order/View/Info/FraudDetails.php b/app/code/Magento/Authorizenet/Block/Adminhtml/Order/View/Info/FraudDetails.php -index a7a670d64d7..c693ebe95d5 100644 ---- a/app/code/Magento/Authorizenet/Block/Adminhtml/Order/View/Info/FraudDetails.php -+++ b/app/code/Magento/Authorizenet/Block/Adminhtml/Order/View/Info/FraudDetails.php -@@ -3,13 +3,18 @@ - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -+declare(strict_types=1); -+ - namespace Magento\Authorizenet\Block\Adminhtml\Order\View\Info; - - use Magento\Authorizenet\Model\Directpost; - - /** -+ * Fraud information block for Authorize.net payment method -+ * - * @api - * @since 100.0.2 -+ * @deprecated 2.3.1 Authorize.net is removing all support for this payment method - */ - class FraudDetails extends \Magento\Backend\Block\Template - { -@@ -33,6 +38,8 @@ class FraudDetails extends \Magento\Backend\Block\Template - } - - /** -+ * Return payment method model -+ * - * @return \Magento\Sales\Model\Order\Payment - */ - public function getPayment() -@@ -42,6 +49,8 @@ class FraudDetails extends \Magento\Backend\Block\Template - } - - /** -+ * Produce and return the block's HTML output -+ * - * @return string - */ - protected function _toHtml() -diff --git a/app/code/Magento/Authorizenet/Block/Adminhtml/Order/View/Info/PaymentDetails.php b/app/code/Magento/Authorizenet/Block/Adminhtml/Order/View/Info/PaymentDetails.php -new file mode 100644 -index 00000000000..23034270640 ---- /dev/null -+++ b/app/code/Magento/Authorizenet/Block/Adminhtml/Order/View/Info/PaymentDetails.php -@@ -0,0 +1,29 @@ -+<?php -+/** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+declare(strict_types=1); -+ -+namespace Magento\Authorizenet\Block\Adminhtml\Order\View\Info; -+ -+use Magento\Framework\Phrase; -+use Magento\Payment\Block\ConfigurableInfo; -+ -+/** -+ * Payment information block for Authorize.net payment method -+ * @deprecated 2.3.1 Authorize.net is removing all support for this payment method -+ */ -+class PaymentDetails extends ConfigurableInfo -+{ -+ /** -+ * Returns localized label for payment info block -+ * -+ * @param string $field -+ * @return string | Phrase -+ */ -+ protected function getLabel($field) -+ { -+ return __($field); -+ } -+} -diff --git a/app/code/Magento/Authorizenet/Block/Transparent/Iframe.php b/app/code/Magento/Authorizenet/Block/Transparent/Iframe.php -index 296d22d6f61..65161413cb1 100644 ---- a/app/code/Magento/Authorizenet/Block/Transparent/Iframe.php -+++ b/app/code/Magento/Authorizenet/Block/Transparent/Iframe.php -@@ -3,13 +3,17 @@ - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -+declare(strict_types=1); -+ - namespace Magento\Authorizenet\Block\Transparent; - - use Magento\Payment\Block\Transparent\Iframe as TransparentIframe; - - /** -+ * Transparent Iframe block for Authorize.net payments - * @api - * @since 100.0.2 -+ * @deprecated 2.3.1 Authorize.net is removing all support for this payment method - */ - class Iframe extends TransparentIframe - { -diff --git a/app/code/Magento/Authorizenet/Controller/Adminhtml/Authorizenet/Directpost/Payment/AddConfigured.php b/app/code/Magento/Authorizenet/Controller/Adminhtml/Authorizenet/Directpost/Payment/AddConfigured.php -index 46d395b978e..f71314613fc 100644 ---- a/app/code/Magento/Authorizenet/Controller/Adminhtml/Authorizenet/Directpost/Payment/AddConfigured.php -+++ b/app/code/Magento/Authorizenet/Controller/Adminhtml/Authorizenet/Directpost/Payment/AddConfigured.php -@@ -4,8 +4,17 @@ - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -+declare(strict_types=1); -+ - namespace Magento\Authorizenet\Controller\Adminhtml\Authorizenet\Directpost\Payment; - --class AddConfigured extends \Magento\Sales\Controller\Adminhtml\Order\Create\AddConfigured -+use Magento\Framework\App\Action\HttpPutActionInterface; -+use Magento\Sales\Controller\Adminhtml\Order\Create\AddConfigured as BaseAddConfigured; -+ -+/** -+ * Class AddConfigured -+ * @deprecated 2.3.1 Authorize.net is removing all support for this payment method -+ */ -+class AddConfigured extends BaseAddConfigured implements HttpPutActionInterface - { - } -diff --git a/app/code/Magento/Authorizenet/Controller/Adminhtml/Authorizenet/Directpost/Payment/Cancel.php b/app/code/Magento/Authorizenet/Controller/Adminhtml/Authorizenet/Directpost/Payment/Cancel.php -index 3432e14d77b..3ebea4704db 100644 ---- a/app/code/Magento/Authorizenet/Controller/Adminhtml/Authorizenet/Directpost/Payment/Cancel.php -+++ b/app/code/Magento/Authorizenet/Controller/Adminhtml/Authorizenet/Directpost/Payment/Cancel.php -@@ -4,8 +4,17 @@ - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -+declare(strict_types=1); -+ - namespace Magento\Authorizenet\Controller\Adminhtml\Authorizenet\Directpost\Payment; - --class Cancel extends \Magento\Sales\Controller\Adminhtml\Order\Create\Cancel -+use Magento\Framework\App\Action\HttpPostActionInterface; -+use Magento\Sales\Controller\Adminhtml\Order\Create\Cancel as BaseCancel; -+ -+/** -+ * Class Cancel -+ * @deprecated 2.3.1 Authorize.net is removing all support for this payment method -+ */ -+class Cancel extends BaseCancel implements HttpPostActionInterface - { - } -diff --git a/app/code/Magento/Authorizenet/Controller/Adminhtml/Authorizenet/Directpost/Payment/ConfigureProductToAdd.php b/app/code/Magento/Authorizenet/Controller/Adminhtml/Authorizenet/Directpost/Payment/ConfigureProductToAdd.php -index 9fa3c7dd19b..19eb4571a85 100644 ---- a/app/code/Magento/Authorizenet/Controller/Adminhtml/Authorizenet/Directpost/Payment/ConfigureProductToAdd.php -+++ b/app/code/Magento/Authorizenet/Controller/Adminhtml/Authorizenet/Directpost/Payment/ConfigureProductToAdd.php -@@ -4,8 +4,17 @@ - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -+declare(strict_types=1); -+ - namespace Magento\Authorizenet\Controller\Adminhtml\Authorizenet\Directpost\Payment; - --class ConfigureProductToAdd extends \Magento\Sales\Controller\Adminhtml\Order\Create\ConfigureProductToAdd -+use Magento\Framework\App\Action\HttpPutActionInterface; -+use Magento\Sales\Controller\Adminhtml\Order\Create\ConfigureProductToAdd as BaseConfigureProductToAdd; -+ -+/** -+ * Class ConfigureProductToAdd -+ * @deprecated 2.3.1 Authorize.net is removing all support for this payment method -+ */ -+class ConfigureProductToAdd extends BaseConfigureProductToAdd implements HttpPutActionInterface - { - } -diff --git a/app/code/Magento/Authorizenet/Controller/Adminhtml/Authorizenet/Directpost/Payment/ConfigureQuoteItems.php b/app/code/Magento/Authorizenet/Controller/Adminhtml/Authorizenet/Directpost/Payment/ConfigureQuoteItems.php -index c1ea98aea23..d314149059c 100644 ---- a/app/code/Magento/Authorizenet/Controller/Adminhtml/Authorizenet/Directpost/Payment/ConfigureQuoteItems.php -+++ b/app/code/Magento/Authorizenet/Controller/Adminhtml/Authorizenet/Directpost/Payment/ConfigureQuoteItems.php -@@ -4,8 +4,17 @@ - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -+declare(strict_types=1); -+ - namespace Magento\Authorizenet\Controller\Adminhtml\Authorizenet\Directpost\Payment; - --class ConfigureQuoteItems extends \Magento\Sales\Controller\Adminhtml\Order\Create\ConfigureQuoteItems -+use Magento\Framework\App\Action\HttpPutActionInterface; -+use Magento\Sales\Controller\Adminhtml\Order\Create\ConfigureQuoteItems as BaseConfigureQuoteItems; -+ -+/** -+ * Class ConfigureQuoteItems -+ * @deprecated 2.3 Authorize.net is removing all support for this payment method -+ */ -+class ConfigureQuoteItems extends BaseConfigureQuoteItems implements HttpPutActionInterface - { - } -diff --git a/app/code/Magento/Authorizenet/Controller/Adminhtml/Authorizenet/Directpost/Payment/Index.php b/app/code/Magento/Authorizenet/Controller/Adminhtml/Authorizenet/Directpost/Payment/Index.php -index b206f89ab8b..33ac620499e 100644 ---- a/app/code/Magento/Authorizenet/Controller/Adminhtml/Authorizenet/Directpost/Payment/Index.php -+++ b/app/code/Magento/Authorizenet/Controller/Adminhtml/Authorizenet/Directpost/Payment/Index.php -@@ -4,8 +4,14 @@ - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -+declare(strict_types=1); -+ - namespace Magento\Authorizenet\Controller\Adminhtml\Authorizenet\Directpost\Payment; - -+/** -+ * Class Index -+ * @deprecated 2.3.1 Authorize.net is removing all support for this payment method -+ */ - class Index extends \Magento\Sales\Controller\Adminhtml\Order\Create\Index - { - } -diff --git a/app/code/Magento/Authorizenet/Controller/Adminhtml/Authorizenet/Directpost/Payment/LoadBlock.php b/app/code/Magento/Authorizenet/Controller/Adminhtml/Authorizenet/Directpost/Payment/LoadBlock.php -index 43e456e7669..577840c0a9b 100644 ---- a/app/code/Magento/Authorizenet/Controller/Adminhtml/Authorizenet/Directpost/Payment/LoadBlock.php -+++ b/app/code/Magento/Authorizenet/Controller/Adminhtml/Authorizenet/Directpost/Payment/LoadBlock.php -@@ -4,8 +4,14 @@ - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -+declare(strict_types=1); -+ - namespace Magento\Authorizenet\Controller\Adminhtml\Authorizenet\Directpost\Payment; - -+/** -+ * Class LoadBlock -+ * @deprecated 2.3.1 Authorize.net is removing all support for this payment method -+ */ - class LoadBlock extends \Magento\Sales\Controller\Adminhtml\Order\Create\LoadBlock - { - } -diff --git a/app/code/Magento/Authorizenet/Controller/Adminhtml/Authorizenet/Directpost/Payment/Place.php b/app/code/Magento/Authorizenet/Controller/Adminhtml/Authorizenet/Directpost/Payment/Place.php -index b393015ce12..fc4cce07bd0 100644 ---- a/app/code/Magento/Authorizenet/Controller/Adminhtml/Authorizenet/Directpost/Payment/Place.php -+++ b/app/code/Magento/Authorizenet/Controller/Adminhtml/Authorizenet/Directpost/Payment/Place.php -@@ -3,21 +3,26 @@ - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -+declare(strict_types=1); -+ - namespace Magento\Authorizenet\Controller\Adminhtml\Authorizenet\Directpost\Payment; - --use Magento\Framework\Escaper; --use Magento\Catalog\Helper\Product; -+use Magento\Authorizenet\Helper\Backend\Data as DataHelper; - use Magento\Backend\App\Action\Context; --use Magento\Framework\View\Result\PageFactory; - use Magento\Backend\Model\View\Result\ForwardFactory; --use Magento\Authorizenet\Helper\Backend\Data as DataHelper; -+use Magento\Catalog\Helper\Product; -+use Magento\Framework\Escaper; -+use Magento\Framework\View\Result\PageFactory; -+use Magento\Framework\App\Action\HttpPostActionInterface; -+use Magento\Sales\Controller\Adminhtml\Order\Create; - - /** - * Class Place - * - * @SuppressWarnings(PHPMD.CouplingBetweenObjects) -+ * @deprecated 2.3.1 Authorize.net is removing all support for this payment method - */ --class Place extends \Magento\Sales\Controller\Adminhtml\Order\Create -+class Place extends Create implements HttpPostActionInterface - { - /** - * @var DataHelper -diff --git a/app/code/Magento/Authorizenet/Controller/Adminhtml/Authorizenet/Directpost/Payment/ProcessData.php b/app/code/Magento/Authorizenet/Controller/Adminhtml/Authorizenet/Directpost/Payment/ProcessData.php -index 35720249be3..3d0d572bd62 100644 ---- a/app/code/Magento/Authorizenet/Controller/Adminhtml/Authorizenet/Directpost/Payment/ProcessData.php -+++ b/app/code/Magento/Authorizenet/Controller/Adminhtml/Authorizenet/Directpost/Payment/ProcessData.php -@@ -4,8 +4,17 @@ - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -+declare(strict_types=1); -+ - namespace Magento\Authorizenet\Controller\Adminhtml\Authorizenet\Directpost\Payment; - --class ProcessData extends \Magento\Sales\Controller\Adminhtml\Order\Create\ProcessData -+use Magento\Sales\Controller\Adminhtml\Order\Create\ProcessData as BaseProcessData; -+use Magento\Framework\App\Action\HttpPostActionInterface; -+ -+/** -+ * Class ProcessData -+ * @deprecated 2.3.1 Authorize.net is removing all support for this payment method -+ */ -+class ProcessData extends BaseProcessData implements HttpPostActionInterface - { - } -diff --git a/app/code/Magento/Authorizenet/Controller/Adminhtml/Authorizenet/Directpost/Payment/Redirect.php b/app/code/Magento/Authorizenet/Controller/Adminhtml/Authorizenet/Directpost/Payment/Redirect.php -index bd9de956dc6..333751f9365 100644 ---- a/app/code/Magento/Authorizenet/Controller/Adminhtml/Authorizenet/Directpost/Payment/Redirect.php -+++ b/app/code/Magento/Authorizenet/Controller/Adminhtml/Authorizenet/Directpost/Payment/Redirect.php -@@ -3,6 +3,8 @@ - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -+declare(strict_types=1); -+ - namespace Magento\Authorizenet\Controller\Adminhtml\Authorizenet\Directpost\Payment; - - use Magento\Backend\App\Action; -@@ -10,11 +12,16 @@ use Magento\Backend\Model\View\Result\ForwardFactory; - use Magento\Framework\View\Result\LayoutFactory; - use Magento\Framework\View\Result\PageFactory; - use Magento\Payment\Block\Transparent\Iframe; -+use Magento\Framework\App\Action\HttpGetActionInterface; -+use Magento\Framework\App\Action\HttpPostActionInterface; -+use Magento\Sales\Controller\Adminhtml\Order\Create; - - /** -+ * Class Redirect - * @SuppressWarnings(PHPMD.CouplingBetweenObjects) -+ * @deprecated 2.3.1 Authorize.net is removing all support for this payment method - */ --class Redirect extends \Magento\Sales\Controller\Adminhtml\Order\Create -+class Redirect extends Create implements HttpGetActionInterface, HttpPostActionInterface - { - /** - * Core registry -diff --git a/app/code/Magento/Authorizenet/Controller/Adminhtml/Authorizenet/Directpost/Payment/Reorder.php b/app/code/Magento/Authorizenet/Controller/Adminhtml/Authorizenet/Directpost/Payment/Reorder.php -index 80b9f54524f..06a6403915f 100644 ---- a/app/code/Magento/Authorizenet/Controller/Adminhtml/Authorizenet/Directpost/Payment/Reorder.php -+++ b/app/code/Magento/Authorizenet/Controller/Adminhtml/Authorizenet/Directpost/Payment/Reorder.php -@@ -4,8 +4,17 @@ - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -+declare(strict_types=1); -+ - namespace Magento\Authorizenet\Controller\Adminhtml\Authorizenet\Directpost\Payment; - --class Reorder extends \Magento\Sales\Controller\Adminhtml\Order\Create\Reorder -+use Magento\Framework\App\Action\HttpPostActionInterface; -+use Magento\Sales\Controller\Adminhtml\Order\Create\Reorder as BaseReorder; -+ -+/** -+ * Class Reorder -+ * @deprecated 2.3.1 Authorize.net is removing all support for this payment method -+ */ -+class Reorder extends BaseReorder implements HttpPostActionInterface - { - } -diff --git a/app/code/Magento/Authorizenet/Controller/Adminhtml/Authorizenet/Directpost/Payment/ReturnQuote.php b/app/code/Magento/Authorizenet/Controller/Adminhtml/Authorizenet/Directpost/Payment/ReturnQuote.php -index 82a5ee08f7c..c42e7ecbeef 100644 ---- a/app/code/Magento/Authorizenet/Controller/Adminhtml/Authorizenet/Directpost/Payment/ReturnQuote.php -+++ b/app/code/Magento/Authorizenet/Controller/Adminhtml/Authorizenet/Directpost/Payment/ReturnQuote.php -@@ -4,9 +4,19 @@ - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -+declare(strict_types=1); -+ - namespace Magento\Authorizenet\Controller\Adminhtml\Authorizenet\Directpost\Payment; - --class ReturnQuote extends \Magento\Sales\Controller\Adminhtml\Order\Create -+use Magento\Framework\App\Action\HttpGetActionInterface; -+use Magento\Framework\App\Action\HttpPostActionInterface; -+use Magento\Sales\Controller\Adminhtml\Order\Create; -+ -+/** -+ * Class ReturnQuote -+ * @deprecated 2.3.1 Authorize.net is removing all support for this payment method -+ */ -+class ReturnQuote extends Create implements HttpPostActionInterface, HttpGetActionInterface - { - /** - * Return quote -diff --git a/app/code/Magento/Authorizenet/Controller/Adminhtml/Authorizenet/Directpost/Payment/Save.php b/app/code/Magento/Authorizenet/Controller/Adminhtml/Authorizenet/Directpost/Payment/Save.php -index 7519f3415c4..cc93ce5daed 100644 ---- a/app/code/Magento/Authorizenet/Controller/Adminhtml/Authorizenet/Directpost/Payment/Save.php -+++ b/app/code/Magento/Authorizenet/Controller/Adminhtml/Authorizenet/Directpost/Payment/Save.php -@@ -4,8 +4,14 @@ - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -+declare(strict_types=1); -+ - namespace Magento\Authorizenet\Controller\Adminhtml\Authorizenet\Directpost\Payment; - -+/** -+ * Class Save -+ * @deprecated 2.3.1 Authorize.net is removing all support for this payment method -+ */ - class Save extends \Magento\Sales\Controller\Adminhtml\Order\Create\Save - { - } -diff --git a/app/code/Magento/Authorizenet/Controller/Adminhtml/Authorizenet/Directpost/Payment/ShowUpdateResult.php b/app/code/Magento/Authorizenet/Controller/Adminhtml/Authorizenet/Directpost/Payment/ShowUpdateResult.php -index b55da878b2e..af80bde1083 100644 ---- a/app/code/Magento/Authorizenet/Controller/Adminhtml/Authorizenet/Directpost/Payment/ShowUpdateResult.php -+++ b/app/code/Magento/Authorizenet/Controller/Adminhtml/Authorizenet/Directpost/Payment/ShowUpdateResult.php -@@ -4,8 +4,14 @@ - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -+declare(strict_types=1); -+ - namespace Magento\Authorizenet\Controller\Adminhtml\Authorizenet\Directpost\Payment; - -+/** -+ * Class ShowUpdateResult -+ * @deprecated 2.3.1 Authorize.net is removing all support for this payment method -+ */ - class ShowUpdateResult extends \Magento\Sales\Controller\Adminhtml\Order\Create\ShowUpdateResult - { - } -diff --git a/app/code/Magento/Authorizenet/Controller/Adminhtml/Authorizenet/Directpost/Payment/Start.php b/app/code/Magento/Authorizenet/Controller/Adminhtml/Authorizenet/Directpost/Payment/Start.php -index f0ac5f80c11..689b30d63be 100644 ---- a/app/code/Magento/Authorizenet/Controller/Adminhtml/Authorizenet/Directpost/Payment/Start.php -+++ b/app/code/Magento/Authorizenet/Controller/Adminhtml/Authorizenet/Directpost/Payment/Start.php -@@ -4,8 +4,17 @@ - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -+declare(strict_types=1); -+ - namespace Magento\Authorizenet\Controller\Adminhtml\Authorizenet\Directpost\Payment; - --abstract class Start extends \Magento\Sales\Controller\Adminhtml\Order\Create -+use Magento\Framework\App\Action\HttpPostActionInterface; -+use Magento\Sales\Controller\Adminhtml\Order\Create; -+ -+/** -+ * Class Start -+ * @deprecated 2.3.1 Authorize.net is removing all support for this payment method -+ */ -+abstract class Start extends Create implements HttpPostActionInterface - { - } -diff --git a/app/code/Magento/Authorizenet/Controller/Directpost/Payment.php b/app/code/Magento/Authorizenet/Controller/Directpost/Payment.php -index fc9e7807cd9..cfaa5f1cfcd 100644 ---- a/app/code/Magento/Authorizenet/Controller/Directpost/Payment.php -+++ b/app/code/Magento/Authorizenet/Controller/Directpost/Payment.php -@@ -3,16 +3,22 @@ - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -+declare(strict_types=1); -+ - namespace Magento\Authorizenet\Controller\Directpost; - -+use Magento\Framework\App\Action\HttpGetActionInterface; -+use Magento\Framework\App\Action\HttpPostActionInterface; - use Magento\Payment\Block\Transparent\Iframe; -+use Magento\Framework\App\Action\Action; - - /** - * DirectPost Payment Controller - * - * @SuppressWarnings(PHPMD.CouplingBetweenObjects) -+ * @deprecated 2.3.1 Authorize.net is removing all support for this payment method - */ --abstract class Payment extends \Magento\Framework\App\Action\Action -+abstract class Payment extends Action implements HttpGetActionInterface, HttpPostActionInterface - { - /** - * Core registry -@@ -44,6 +50,8 @@ abstract class Payment extends \Magento\Framework\App\Action\Action - } - - /** -+ * Get checkout model -+ * - * @return \Magento\Checkout\Model\Session - */ - protected function _getCheckout() -@@ -63,6 +71,7 @@ abstract class Payment extends \Magento\Framework\App\Action\Action - - /** - * Response action. -+ * - * Action for Authorize.net SIM Relay Request. - * - * @param string $area -diff --git a/app/code/Magento/Authorizenet/Controller/Directpost/Payment/BackendResponse.php b/app/code/Magento/Authorizenet/Controller/Directpost/Payment/BackendResponse.php -index 3ad9f470909..e0610a92feb 100644 ---- a/app/code/Magento/Authorizenet/Controller/Directpost/Payment/BackendResponse.php -+++ b/app/code/Magento/Authorizenet/Controller/Directpost/Payment/BackendResponse.php -@@ -4,18 +4,32 @@ - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -+declare(strict_types=1); -+ - namespace Magento\Authorizenet\Controller\Directpost\Payment; - - use Magento\Authorizenet\Helper\DataFactory; - use Magento\Authorizenet\Model\Directpost; - use Magento\Authorizenet\Model\DirectpostFactory; - use Magento\Framework\App\Action\Context; -+use Magento\Framework\App\CsrfAwareActionInterface; -+use Magento\Framework\App\Request\InvalidRequestException; -+use Magento\Framework\App\RequestInterface; - use Magento\Framework\Controller\ResultFactory; - use Magento\Framework\Exception\LocalizedException; - use Magento\Framework\Registry; -+use Magento\Framework\App\Action\HttpGetActionInterface; -+use Magento\Framework\App\Action\HttpPostActionInterface; - use Psr\Log\LoggerInterface; - --class BackendResponse extends \Magento\Authorizenet\Controller\Directpost\Payment -+/** -+ * Class BackendResponse -+ * @deprecated 2.3.1 Authorize.net is removing all support for this payment method -+ */ -+class BackendResponse extends \Magento\Authorizenet\Controller\Directpost\Payment implements -+ CsrfAwareActionInterface, -+ HttpGetActionInterface, -+ HttpPostActionInterface - { - /** - * @var LoggerInterface -@@ -48,8 +62,26 @@ class BackendResponse extends \Magento\Authorizenet\Controller\Directpost\Paymen - $this->logger = $logger ?: $this->_objectManager->get(LoggerInterface::class); - } - -+ /** -+ * @inheritDoc -+ */ -+ public function createCsrfValidationException( -+ RequestInterface $request -+ ): ?InvalidRequestException { -+ return null; -+ } -+ -+ /** -+ * @inheritDoc -+ */ -+ public function validateForCsrf(RequestInterface $request): ?bool -+ { -+ return true; -+ } -+ - /** - * Response action. -+ * - * Action for Authorize.net SIM Relay Request. - * - * @return \Magento\Framework\Controller\ResultInterface -diff --git a/app/code/Magento/Authorizenet/Controller/Directpost/Payment/Place.php b/app/code/Magento/Authorizenet/Controller/Directpost/Payment/Place.php -index 92957481b92..7d672a75f5b 100644 ---- a/app/code/Magento/Authorizenet/Controller/Directpost/Payment/Place.php -+++ b/app/code/Magento/Authorizenet/Controller/Directpost/Payment/Place.php -@@ -3,9 +3,11 @@ - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -+declare(strict_types=1); - - namespace Magento\Authorizenet\Controller\Directpost\Payment; - -+use Magento\Framework\App\Action\HttpPostActionInterface as HttpPostActionInterface; - use Magento\Authorizenet\Controller\Directpost\Payment; - use Magento\Authorizenet\Helper\DataFactory; - use Magento\Checkout\Model\Type\Onepage; -@@ -24,8 +26,9 @@ use Psr\Log\LoggerInterface; - * Class Place - * - * @SuppressWarnings(PHPMD.CouplingBetweenObjects) -+ * @deprecated 2.3.1 Authorize.net is removing all support for this payment method - */ --class Place extends Payment -+class Place extends Payment implements HttpPostActionInterface - { - /** - * @var \Magento\Quote\Api\CartManagementInterface -diff --git a/app/code/Magento/Authorizenet/Controller/Directpost/Payment/Redirect.php b/app/code/Magento/Authorizenet/Controller/Directpost/Payment/Redirect.php -index 028b90bf7da..8c9510243f6 100644 ---- a/app/code/Magento/Authorizenet/Controller/Directpost/Payment/Redirect.php -+++ b/app/code/Magento/Authorizenet/Controller/Directpost/Payment/Redirect.php -@@ -4,15 +4,20 @@ - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -+declare(strict_types=1); -+ - namespace Magento\Authorizenet\Controller\Directpost\Payment; - --use Magento\Framework\App\ObjectManager; -+use Magento\Authorizenet\Controller\Directpost\Payment; -+use Magento\Framework\App\Action\HttpGetActionInterface; -+use Magento\Framework\App\Action\HttpPostActionInterface; - use Magento\Payment\Block\Transparent\Iframe; - - /** - * Class Redirect -+ * @deprecated 2.3.1 Authorize.net is removing all support for this payment method - */ --class Redirect extends \Magento\Authorizenet\Controller\Directpost\Payment -+class Redirect extends Payment implements HttpGetActionInterface, HttpPostActionInterface - { - /** - * Retrieve params and put javascript into iframe -diff --git a/app/code/Magento/Authorizenet/Controller/Directpost/Payment/Response.php b/app/code/Magento/Authorizenet/Controller/Directpost/Payment/Response.php -index d88e77d6c4e..17fc3cb72e4 100644 ---- a/app/code/Magento/Authorizenet/Controller/Directpost/Payment/Response.php -+++ b/app/code/Magento/Authorizenet/Controller/Directpost/Payment/Response.php -@@ -4,12 +4,43 @@ - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -+declare(strict_types=1); -+ - namespace Magento\Authorizenet\Controller\Directpost\Payment; - --class Response extends \Magento\Authorizenet\Controller\Directpost\Payment -+use Magento\Framework\App\CsrfAwareActionInterface; -+use Magento\Framework\App\Request\InvalidRequestException; -+use Magento\Framework\App\RequestInterface; -+use Magento\Authorizenet\Controller\Directpost\Payment; -+use Magento\Framework\App\Action\HttpGetActionInterface; -+use Magento\Framework\App\Action\HttpPostActionInterface; -+ -+/** -+ * Class Response -+ * @deprecated 2.3.1 Authorize.net is removing all support for this payment method -+ */ -+class Response extends Payment implements CsrfAwareActionInterface, HttpGetActionInterface, HttpPostActionInterface - { -+ /** -+ * @inheritDoc -+ */ -+ public function createCsrfValidationException( -+ RequestInterface $request -+ ): ?InvalidRequestException { -+ return null; -+ } -+ -+ /** -+ * @inheritDoc -+ */ -+ public function validateForCsrf(RequestInterface $request): ?bool -+ { -+ return true; -+ } -+ - /** - * Response action. -+ * - * Action for Authorize.net SIM Relay Request. - * - * @return \Magento\Framework\Controller\ResultInterface -diff --git a/app/code/Magento/Authorizenet/Controller/Directpost/Payment/ReturnQuote.php b/app/code/Magento/Authorizenet/Controller/Directpost/Payment/ReturnQuote.php -index 3030a75055b..c974632f584 100644 ---- a/app/code/Magento/Authorizenet/Controller/Directpost/Payment/ReturnQuote.php -+++ b/app/code/Magento/Authorizenet/Controller/Directpost/Payment/ReturnQuote.php -@@ -4,9 +4,19 @@ - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -+declare(strict_types=1); -+ - namespace Magento\Authorizenet\Controller\Directpost\Payment; - --class ReturnQuote extends \Magento\Authorizenet\Controller\Directpost\Payment -+use Magento\Framework\App\Action\HttpGetActionInterface; -+use Magento\Framework\App\Action\HttpPostActionInterface; -+use Magento\Authorizenet\Controller\Directpost\Payment; -+ -+/** -+ * Class ReturnQuote -+ * @deprecated 2.3.1 Authorize.net is removing all support for this payment method -+ */ -+class ReturnQuote extends Payment implements HttpPostActionInterface, HttpGetActionInterface - { - /** - * Return customer quote by ajax -diff --git a/app/code/Magento/Authorizenet/Helper/Backend/Data.php b/app/code/Magento/Authorizenet/Helper/Backend/Data.php -index 24bdb238732..d291125ccae 100644 ---- a/app/code/Magento/Authorizenet/Helper/Backend/Data.php -+++ b/app/code/Magento/Authorizenet/Helper/Backend/Data.php -@@ -3,6 +3,8 @@ - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -+declare(strict_types=1); -+ - namespace Magento\Authorizenet\Helper\Backend; - - use Magento\Authorizenet\Helper\Data as FrontendDataHelper; -@@ -16,6 +18,7 @@ use Magento\Backend\Model\UrlInterface; - * - * @api - * @since 100.0.2 -+ * @deprecated 2.3.1 Authorize.net is removing all support for this payment method - */ - class Data extends FrontendDataHelper - { -diff --git a/app/code/Magento/Authorizenet/Helper/Data.php b/app/code/Magento/Authorizenet/Helper/Data.php -index 8bcc1f3f3f0..e240cd692a1 100644 ---- a/app/code/Magento/Authorizenet/Helper/Data.php -+++ b/app/code/Magento/Authorizenet/Helper/Data.php -@@ -3,6 +3,8 @@ - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -+declare(strict_types=1); -+ - namespace Magento\Authorizenet\Helper; - - use Magento\Framework\App\Helper\AbstractHelper; -@@ -17,6 +19,7 @@ use Magento\Authorizenet\Model\Authorizenet; - * - * @api - * @since 100.0.2 -+ * @deprecated 2.3.1 Authorize.net is removing all support for this payment method - */ - class Data extends AbstractHelper - { -@@ -153,6 +156,7 @@ class Data extends AbstractHelper - - /** - * Update all child and parent order's edit increment numbers. -+ * - * Needed for Admin area. - * - * @param \Magento\Sales\Model\Order $order -@@ -255,6 +259,7 @@ class Data extends AbstractHelper - - /** - * Format price with currency sign -+ * - * @param \Magento\Payment\Model\InfoInterface $payment - * @param float $amount - * @return string -diff --git a/app/code/Magento/Authorizenet/Helper/DataFactory.php b/app/code/Magento/Authorizenet/Helper/DataFactory.php -index f3ccf16e7d3..71f16ab4af6 100644 ---- a/app/code/Magento/Authorizenet/Helper/DataFactory.php -+++ b/app/code/Magento/Authorizenet/Helper/DataFactory.php -@@ -3,6 +3,8 @@ - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -+declare(strict_types=1); -+ - namespace Magento\Authorizenet\Helper; - - use Magento\Framework\Exception\LocalizedException; -@@ -10,6 +12,7 @@ use Magento\Framework\ObjectManagerInterface; - - /** - * Class DataFactory -+ * @deprecated 2.3.1 Authorize.net is removing all support for this payment method - */ - class DataFactory - { -diff --git a/app/code/Magento/Authorizenet/Model/Authorizenet.php b/app/code/Magento/Authorizenet/Model/Authorizenet.php -index ae9ac833a43..9370b649a23 100644 ---- a/app/code/Magento/Authorizenet/Model/Authorizenet.php -+++ b/app/code/Magento/Authorizenet/Model/Authorizenet.php -@@ -3,15 +3,20 @@ - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -+declare(strict_types=1); -+ - namespace Magento\Authorizenet\Model; - - use Magento\Authorizenet\Model\TransactionService; - use Magento\Framework\HTTP\ZendClientFactory; - - /** -+ * Model for Authorize.net payment method -+ * - * @SuppressWarnings(PHPMD.TooManyFields) - * @SuppressWarnings(PHPMD.ExcessiveClassComplexity) - * @SuppressWarnings(PHPMD.CouplingBetweenObjects) -+ * @deprecated 2.3.1 Authorize.net is removing all support for this payment method - */ - abstract class Authorizenet extends \Magento\Payment\Model\Method\Cc - { -diff --git a/app/code/Magento/Authorizenet/Model/Debug.php b/app/code/Magento/Authorizenet/Model/Debug.php -index 255c2e3aba4..93d508cc744 100644 ---- a/app/code/Magento/Authorizenet/Model/Debug.php -+++ b/app/code/Magento/Authorizenet/Model/Debug.php -@@ -3,9 +3,13 @@ - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -+declare(strict_types=1); -+ - namespace Magento\Authorizenet\Model; - - /** -+ * Authorize.net debug payment method model -+ * - * @method string getRequestBody() - * @method \Magento\Authorizenet\Model\Debug setRequestBody(string $value) - * @method string getResponseBody() -@@ -18,10 +22,13 @@ namespace Magento\Authorizenet\Model; - * @method \Magento\Authorizenet\Model\Debug setRequestDump(string $value) - * @method string getResultDump() - * @method \Magento\Authorizenet\Model\Debug setResultDump(string $value) -+ * @deprecated 2.3.1 Authorize.net is removing all support for this payment method - */ - class Debug extends \Magento\Framework\Model\AbstractModel - { - /** -+ * Construct debug class -+ * - * @return void - */ - protected function _construct() -diff --git a/app/code/Magento/Authorizenet/Model/Directpost.php b/app/code/Magento/Authorizenet/Model/Directpost.php -index d5c11ab54cd..946ec8ba01a 100644 ---- a/app/code/Magento/Authorizenet/Model/Directpost.php -+++ b/app/code/Magento/Authorizenet/Model/Directpost.php -@@ -3,6 +3,8 @@ - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -+declare(strict_types=1); -+ - namespace Magento\Authorizenet\Model; - - use Magento\Framework\App\ObjectManager; -@@ -14,6 +16,7 @@ use Magento\Payment\Model\Method\TransparentInterface; - * @SuppressWarnings(PHPMD.TooManyFields) - * @SuppressWarnings(PHPMD.CouplingBetweenObjects) - * @SuppressWarnings(PHPMD.ExcessiveClassComplexity) -+ * @deprecated 2.3.1 Authorize.net is removing all support for this payment method - */ - class Directpost extends \Magento\Authorizenet\Model\Authorizenet implements TransparentInterface, ConfigInterface - { -@@ -27,7 +30,7 @@ class Directpost extends \Magento\Authorizenet\Model\Authorizenet implements Tra - /** - * @var string - */ -- protected $_infoBlockType = \Magento\Payment\Block\Info::class; -+ protected $_infoBlockType = \Magento\Authorizenet\Block\Adminhtml\Order\View\Info\PaymentDetails::class; - - /** - * Payment Method feature -@@ -371,8 +374,7 @@ class Directpost extends \Magento\Authorizenet\Model\Authorizenet implements Tra - } - - /** -- * Refund the amount -- * Need to decode last 4 digits for request. -+ * Refund the amount need to decode last 4 digits for request. - * - * @param \Magento\Framework\DataObject|\Magento\Payment\Model\InfoInterface $payment - * @param float $amount -@@ -544,15 +546,16 @@ class Directpost extends \Magento\Authorizenet\Model\Authorizenet implements Tra - public function validateResponse() - { - $response = $this->getResponse(); -- //md5 check -- if (!$this->getConfigData('trans_md5') -- || !$this->getConfigData('login') -- || !$response->isValidHash($this->getConfigData('trans_md5'), $this->getConfigData('login')) -+ $hashConfigKey = !empty($response->getData('x_SHA2_Hash')) ? 'signature_key' : 'trans_md5'; -+ -+ //hash check -+ if (!$response->isValidHash($this->getConfigData($hashConfigKey), $this->getConfigData('login')) - ) { - throw new \Magento\Framework\Exception\LocalizedException( - __('The transaction was declined because the response hash validation failed.') - ); - } -+ - return true; - } - -@@ -626,6 +629,14 @@ class Directpost extends \Magento\Authorizenet\Model\Authorizenet implements Tra - $payment->setIsTransactionPending(true) - ->setIsFraudDetected(true); - } -+ -+ $additionalInformationKeys = explode(',', $this->getValue('paymentInfoKeys')); -+ foreach ($additionalInformationKeys as $paymentInfoKey) { -+ $paymentInfoValue = $response->getDataByKey($paymentInfoKey); -+ if ($paymentInfoValue !== null) { -+ $payment->setAdditionalInformation($paymentInfoKey, $paymentInfoValue); -+ } -+ } - } - - /** -@@ -644,7 +655,7 @@ class Directpost extends \Magento\Authorizenet\Model\Authorizenet implements Tra - case self::RESPONSE_CODE_ERROR: - $errorMessage = $this->dataHelper->wrapGatewayError($this->getResponse()->getXResponseReasonText()); - $order = $this->getOrderFromResponse(); -- $this->paymentFailures->handle((int)$order->getQuoteId(), $errorMessage); -+ $this->paymentFailures->handle((int)$order->getQuoteId(), (string)$errorMessage); - throw new \Magento\Framework\Exception\LocalizedException($errorMessage); - default: - throw new \Magento\Framework\Exception\LocalizedException( -@@ -682,6 +693,7 @@ class Directpost extends \Magento\Authorizenet\Model\Authorizenet implements Tra - - /** - * Operate with order using information from Authorize.net. -+ * - * Authorize order or authorize and capture it. - * - * @param \Magento\Sales\Model\Order $order -@@ -699,6 +711,7 @@ class Directpost extends \Magento\Authorizenet\Model\Authorizenet implements Tra - //decline the order (in case of wrong response code) but don't return money to customer. - $message = $e->getMessage(); - $this->declineOrder($order, $message, false); -+ - throw $e; - } - -@@ -769,7 +782,7 @@ class Directpost extends \Magento\Authorizenet\Model\Authorizenet implements Tra - } - - /** -- * Add status comment -+ * Add status comment to history - * - * @param \Magento\Sales\Model\Order\Payment $payment - * @return $this -@@ -824,6 +837,7 @@ class Directpost extends \Magento\Authorizenet\Model\Authorizenet implements Tra - ->void($response); - } - $order->registerCancellation($message)->save(); -+ $this->_eventManager->dispatch('order_cancel_after', ['order' => $order ]); - } catch (\Exception $e) { - //quiet decline - $this->getPsrLogger()->critical($e); -@@ -858,7 +872,7 @@ class Directpost extends \Magento\Authorizenet\Model\Authorizenet implements Tra - * Getter for specified value according to set payment method code - * - * @param mixed $key -- * @param null $storeId -+ * @param mixed $storeId - * @return mixed - */ - public function getValue($key, $storeId = null) -@@ -918,10 +932,12 @@ class Directpost extends \Magento\Authorizenet\Model\Authorizenet implements Tra - $payment->setIsTransactionDenied(true); - } - $this->addStatusCommentOnUpdate($payment, $response, $transactionId); -- return []; -+ return $response->getData(); - } - - /** -+ * Add status comment on update -+ * - * @param \Magento\Sales\Model\Order\Payment $payment - * @param \Magento\Framework\DataObject $response - * @param string $transactionId -@@ -996,8 +1012,9 @@ class Directpost extends \Magento\Authorizenet\Model\Authorizenet implements Tra - } - - /** -- * @return \Psr\Log\LoggerInterface -+ * Get psr logger. - * -+ * @return \Psr\Log\LoggerInterface - * @deprecated 100.1.0 - */ - private function getPsrLogger() -@@ -1038,7 +1055,9 @@ class Directpost extends \Magento\Authorizenet\Model\Authorizenet implements Tra - } - - /** -- * Checks if filter action is Report Only. Transactions that trigger this filter are processed as normal, -+ * Checks if filter action is Report Only. -+ * -+ * Transactions that trigger this filter are processed as normal, - * but are also reported in the Merchant Interface as triggering this filter. - * - * @param string $fdsFilterAction -diff --git a/app/code/Magento/Authorizenet/Model/Directpost/Request.php b/app/code/Magento/Authorizenet/Model/Directpost/Request.php -index d9a403e5c99..10be4cd5feb 100644 ---- a/app/code/Magento/Authorizenet/Model/Directpost/Request.php -+++ b/app/code/Magento/Authorizenet/Model/Directpost/Request.php -@@ -3,13 +3,17 @@ - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -+declare(strict_types=1); - - namespace Magento\Authorizenet\Model\Directpost; - - use Magento\Authorizenet\Model\Request as AuthorizenetRequest; -+use Magento\Framework\App\ObjectManager; -+use Magento\Framework\Intl\DateTimeFactory; - - /** - * Authorize.net request model for DirectPost model -+ * @deprecated 2.3.1 Authorize.net is removing all support for this payment method - */ - class Request extends AuthorizenetRequest - { -@@ -18,9 +22,35 @@ class Request extends AuthorizenetRequest - */ - protected $_transKey = null; - -+ /** -+ * Hexadecimal signature key. -+ * -+ * @var string -+ */ -+ private $signatureKey = ''; -+ -+ /** -+ * @var DateTimeFactory -+ */ -+ private $dateTimeFactory; -+ -+ /** -+ * @param array $data -+ * @param DateTimeFactory $dateTimeFactory -+ */ -+ public function __construct( -+ array $data = [], -+ DateTimeFactory $dateTimeFactory = null -+ ) { -+ $this->dateTimeFactory = $dateTimeFactory ?? ObjectManager::getInstance() -+ ->get(DateTimeFactory::class); -+ parent::__construct($data); -+ } -+ - /** - * Return merchant transaction key. -- * Needed to generate sign. -+ * -+ * Needed to generate MD5 sign. - * - * @return string - */ -@@ -31,7 +61,8 @@ class Request extends AuthorizenetRequest - - /** - * Set merchant transaction key. -- * Needed to generate sign. -+ * -+ * Needed to generate MD5 sign. - * - * @param string $transKey - * @return $this -@@ -43,7 +74,7 @@ class Request extends AuthorizenetRequest - } - - /** -- * Generates the fingerprint for request. -+ * Generates the MD5 fingerprint for request. - * - * @param string $merchantApiLoginId - * @param string $merchantTransactionKey -@@ -63,7 +94,7 @@ class Request extends AuthorizenetRequest - ) { - return hash_hmac( - "md5", -- $merchantApiLoginId . "^" . $fpSequence . "^" . $fpTimestamp . "^" . $amount . "^" . $currencyCode, -+ $merchantApiLoginId . '^' . $fpSequence . '^' . $fpTimestamp . '^' . $amount . '^' . $currencyCode, - $merchantTransactionKey - ); - } -@@ -78,7 +109,7 @@ class Request extends AuthorizenetRequest - { - $this->setXVersion('3.1')->setXDelimData('FALSE')->setXRelayResponse('TRUE'); - -- $this->setXTestRequest($paymentMethod->getConfigData('test') ? 'TRUE' : 'FALSE'); -+ $this->setSignatureKey($paymentMethod->getConfigData('signature_key')); - - $this->setXLogin($paymentMethod->getConfigData('login')) - ->setXMethod(\Magento\Authorizenet\Model\Authorizenet::REQUEST_METHOD_CC) -@@ -112,73 +143,138 @@ class Request extends AuthorizenetRequest - sprintf('%.2F', $order->getBaseShippingAmount()) - ); - -- //need to use strval() because NULL values IE6-8 decodes as "null" in JSON in JavaScript, -+ //need to use (string) because NULL values IE6-8 decodes as "null" in JSON in JavaScript, - //but we need "" for null values. - $billing = $order->getBillingAddress(); - if (!empty($billing)) { -- $this->setXFirstName(strval($billing->getFirstname())) -- ->setXLastName(strval($billing->getLastname())) -- ->setXCompany(strval($billing->getCompany())) -- ->setXAddress(strval($billing->getStreetLine(1))) -- ->setXCity(strval($billing->getCity())) -- ->setXState(strval($billing->getRegion())) -- ->setXZip(strval($billing->getPostcode())) -- ->setXCountry(strval($billing->getCountryId())) -- ->setXPhone(strval($billing->getTelephone())) -- ->setXFax(strval($billing->getFax())) -- ->setXCustId(strval($billing->getCustomerId())) -- ->setXCustomerIp(strval($order->getRemoteIp())) -- ->setXCustomerTaxId(strval($billing->getTaxId())) -- ->setXEmail(strval($order->getCustomerEmail())) -- ->setXEmailCustomer(strval($paymentMethod->getConfigData('email_customer'))) -- ->setXMerchantEmail(strval($paymentMethod->getConfigData('merchant_email'))); -+ $this->setXFirstName((string)$billing->getFirstname()) -+ ->setXLastName((string)$billing->getLastname()) -+ ->setXCompany((string)$billing->getCompany()) -+ ->setXAddress((string)$billing->getStreetLine(1)) -+ ->setXCity((string)$billing->getCity()) -+ ->setXState((string)$billing->getRegion()) -+ ->setXZip((string)$billing->getPostcode()) -+ ->setXCountry((string)$billing->getCountryId()) -+ ->setXPhone((string)$billing->getTelephone()) -+ ->setXFax((string)$billing->getFax()) -+ ->setXCustId((string)$billing->getCustomerId()) -+ ->setXCustomerIp((string)$order->getRemoteIp()) -+ ->setXCustomerTaxId((string)$billing->getTaxId()) -+ ->setXEmail((string)$order->getCustomerEmail()) -+ ->setXEmailCustomer((string)$paymentMethod->getConfigData('email_customer')) -+ ->setXMerchantEmail((string)$paymentMethod->getConfigData('merchant_email')); - } - - $shipping = $order->getShippingAddress(); - if (!empty($shipping)) { - $this->setXShipToFirstName( -- strval($shipping->getFirstname()) -+ (string)$shipping->getFirstname() - )->setXShipToLastName( -- strval($shipping->getLastname()) -+ (string)$shipping->getLastname() - )->setXShipToCompany( -- strval($shipping->getCompany()) -+ (string)$shipping->getCompany() - )->setXShipToAddress( -- strval($shipping->getStreetLine(1)) -+ (string)$shipping->getStreetLine(1) - )->setXShipToCity( -- strval($shipping->getCity()) -+ (string)$shipping->getCity() - )->setXShipToState( -- strval($shipping->getRegion()) -+ (string)$shipping->getRegion() - )->setXShipToZip( -- strval($shipping->getPostcode()) -+ (string)$shipping->getPostcode() - )->setXShipToCountry( -- strval($shipping->getCountryId()) -+ (string)$shipping->getCountryId() - ); - } - -- $this->setXPoNum(strval($payment->getPoNumber())); -+ $this->setXPoNum((string)$payment->getPoNumber()); - - return $this; - } - - /** - * Set sign hash into the request object. -- * All needed fields should be placed in the object fist. -+ * -+ * All needed fields should be placed in the object first. - * - * @return $this - */ - public function signRequestData() - { -- $fpTimestamp = time(); -- $hash = $this->generateRequestSign( -- $this->getXLogin(), -- $this->_getTransactionKey(), -- $this->getXAmount(), -- $this->getXCurrencyCode(), -- $this->getXFpSequence(), -- $fpTimestamp -- ); -+ $fpDate = $this->dateTimeFactory->create('now', new \DateTimeZone('UTC')); -+ $fpTimestamp = $fpDate->getTimestamp(); -+ -+ if (!empty($this->getSignatureKey())) { -+ $hash = $this->generateSha2RequestSign( -+ (string)$this->getXLogin(), -+ (string)$this->getSignatureKey(), -+ (string)$this->getXAmount(), -+ (string)$this->getXCurrencyCode(), -+ (string)$this->getXFpSequence(), -+ $fpTimestamp -+ ); -+ } else { -+ $hash = $this->generateRequestSign( -+ $this->getXLogin(), -+ $this->_getTransactionKey(), -+ $this->getXAmount(), -+ $this->getXCurrencyCode(), -+ $this->getXFpSequence(), -+ $fpTimestamp -+ ); -+ } -+ - $this->setXFpTimestamp($fpTimestamp); - $this->setXFpHash($hash); -+ - return $this; - } -+ -+ /** -+ * Generates the SHA2 fingerprint for request. -+ * -+ * @param string $merchantApiLoginId -+ * @param string $merchantSignatureKey -+ * @param string $amount -+ * @param string $currencyCode -+ * @param string $fpSequence An invoice number or random number. -+ * @param int $fpTimestamp -+ * @return string The fingerprint. -+ */ -+ private function generateSha2RequestSign( -+ string $merchantApiLoginId, -+ string $merchantSignatureKey, -+ string $amount, -+ string $currencyCode, -+ string $fpSequence, -+ int $fpTimestamp -+ ): string { -+ $message = $merchantApiLoginId . '^' . $fpSequence . '^' . $fpTimestamp . '^' . $amount . '^' . $currencyCode; -+ -+ return strtoupper(hash_hmac('sha512', $message, pack('H*', $merchantSignatureKey))); -+ } -+ -+ /** -+ * Return merchant hexadecimal signature key. -+ * -+ * Needed to generate SHA2 sign. -+ * -+ * @return string -+ */ -+ private function getSignatureKey(): string -+ { -+ return $this->signatureKey; -+ } -+ -+ /** -+ * Set merchant hexadecimal signature key. -+ * -+ * Needed to generate SHA2 sign. -+ * -+ * @param string $signatureKey -+ * @return void -+ */ -+ private function setSignatureKey(string $signatureKey) -+ { -+ $this->signatureKey = $signatureKey; -+ } - } -diff --git a/app/code/Magento/Authorizenet/Model/Directpost/Request/Factory.php b/app/code/Magento/Authorizenet/Model/Directpost/Request/Factory.php -index 2cdd02d7f84..6036935f57b 100644 ---- a/app/code/Magento/Authorizenet/Model/Directpost/Request/Factory.php -+++ b/app/code/Magento/Authorizenet/Model/Directpost/Request/Factory.php -@@ -3,12 +3,15 @@ - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -+declare(strict_types=1); -+ - namespace Magento\Authorizenet\Model\Directpost\Request; - - use Magento\Authorizenet\Model\Request\Factory as AuthorizenetRequestFactory; - - /** - * Factory class for @see \Magento\Authorizenet\Model\Directpost\Request -+ * @deprecated 2.3.1 Authorize.net is removing all support for this payment method - */ - class Factory extends AuthorizenetRequestFactory - { -diff --git a/app/code/Magento/Authorizenet/Model/Directpost/Response.php b/app/code/Magento/Authorizenet/Model/Directpost/Response.php -index dc62c1e990d..b5604a78cb9 100644 ---- a/app/code/Magento/Authorizenet/Model/Directpost/Response.php -+++ b/app/code/Magento/Authorizenet/Model/Directpost/Response.php -@@ -3,6 +3,8 @@ - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -+declare(strict_types=1); -+ - namespace Magento\Authorizenet\Model\Directpost; - - use Magento\Authorizenet\Model\Response as AuthorizenetResponse; -@@ -10,6 +12,7 @@ use Magento\Framework\Encryption\Helper\Security; - - /** - * Authorize.net response model for DirectPost model -+ * @deprecated 2.3.1 Authorize.net is removing all support for this payment method - */ - class Response extends AuthorizenetResponse - { -@@ -24,25 +27,31 @@ class Response extends AuthorizenetResponse - */ - public function generateHash($merchantMd5, $merchantApiLogin, $amount, $transactionId) - { -- if (!$amount) { -- $amount = '0.00'; -- } -- - return strtoupper(md5($merchantMd5 . $merchantApiLogin . $transactionId . $amount)); - } - - /** - * Return if is valid order id. - * -- * @param string $merchantMd5 -+ * @param string $storedHash - * @param string $merchantApiLogin - * @return bool - */ -- public function isValidHash($merchantMd5, $merchantApiLogin) -+ public function isValidHash($storedHash, $merchantApiLogin) - { -- $hash = $this->generateHash($merchantMd5, $merchantApiLogin, $this->getXAmount(), $this->getXTransId()); -+ if (empty($this->getData('x_amount'))) { -+ $this->setData('x_amount', '0.00'); -+ } - -- return Security::compareStrings($hash, $this->getData('x_MD5_Hash')); -+ if (!empty($this->getData('x_SHA2_Hash'))) { -+ $hash = $this->generateSha2Hash($storedHash); -+ return Security::compareStrings($hash, $this->getData('x_SHA2_Hash')); -+ } elseif (!empty($this->getData('x_MD5_Hash'))) { -+ $hash = $this->generateHash($storedHash, $merchantApiLogin, $this->getXAmount(), $this->getXTransId()); -+ return Security::compareStrings($hash, $this->getData('x_MD5_Hash')); -+ } -+ -+ return false; - } - - /** -@@ -54,4 +63,54 @@ class Response extends AuthorizenetResponse - { - return $this->getXResponseCode() == \Magento\Authorizenet\Model\Directpost::RESPONSE_CODE_APPROVED; - } -+ -+ /** -+ * Generates an SHA2 hash to compare against AuthNet's. -+ * -+ * @param string $signatureKey -+ * @return string -+ * @see https://support.authorize.net/s/article/MD5-Hash-End-of-Life-Signature-Key-Replacement -+ */ -+ private function generateSha2Hash(string $signatureKey): string -+ { -+ $hashFields = [ -+ 'x_trans_id', -+ 'x_test_request', -+ 'x_response_code', -+ 'x_auth_code', -+ 'x_cvv2_resp_code', -+ 'x_cavv_response', -+ 'x_avs_code', -+ 'x_method', -+ 'x_account_number', -+ 'x_amount', -+ 'x_company', -+ 'x_first_name', -+ 'x_last_name', -+ 'x_address', -+ 'x_city', -+ 'x_state', -+ 'x_zip', -+ 'x_country', -+ 'x_phone', -+ 'x_fax', -+ 'x_email', -+ 'x_ship_to_company', -+ 'x_ship_to_first_name', -+ 'x_ship_to_last_name', -+ 'x_ship_to_address', -+ 'x_ship_to_city', -+ 'x_ship_to_state', -+ 'x_ship_to_zip', -+ 'x_ship_to_country', -+ 'x_invoice_num', -+ ]; -+ -+ $message = '^'; -+ foreach ($hashFields as $field) { -+ $message .= ($this->getData($field) ?? '') . '^'; -+ } -+ -+ return strtoupper(hash_hmac('sha512', $message, pack('H*', $signatureKey))); -+ } - } -diff --git a/app/code/Magento/Authorizenet/Model/Directpost/Response/Factory.php b/app/code/Magento/Authorizenet/Model/Directpost/Response/Factory.php -index c2a24ef386a..4fda5ac62b4 100644 ---- a/app/code/Magento/Authorizenet/Model/Directpost/Response/Factory.php -+++ b/app/code/Magento/Authorizenet/Model/Directpost/Response/Factory.php -@@ -3,12 +3,15 @@ - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -+declare(strict_types=1); -+ - namespace Magento\Authorizenet\Model\Directpost\Response; - - use Magento\Authorizenet\Model\Response\Factory as AuthorizenetResponseFactory; - - /** - * Factory class for @see \Magento\Authorizenet\Model\Directpost\Response -+ * @deprecated 2.3.1 Authorize.net is removing all support for this payment method - */ - class Factory extends AuthorizenetResponseFactory - { -diff --git a/app/code/Magento/Authorizenet/Model/Directpost/Session.php b/app/code/Magento/Authorizenet/Model/Directpost/Session.php -index 7ddedac1613..26c5ff0cb7e 100644 ---- a/app/code/Magento/Authorizenet/Model/Directpost/Session.php -+++ b/app/code/Magento/Authorizenet/Model/Directpost/Session.php -@@ -3,12 +3,17 @@ - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -+declare(strict_types=1); -+ - namespace Magento\Authorizenet\Model\Directpost; - - use Magento\Framework\Session\SessionManager; - - /** - * Authorize.net DirectPost session model -+ * -+ * @SuppressWarnings(PHPMD.CookieAndSessionMisuse) -+ * @deprecated 2.3.1 Authorize.net is removing all support for this payment method - */ - class Session extends SessionManager - { -diff --git a/app/code/Magento/Authorizenet/Model/Request.php b/app/code/Magento/Authorizenet/Model/Request.php -index dc52f84baec..552439fc8bb 100644 ---- a/app/code/Magento/Authorizenet/Model/Request.php -+++ b/app/code/Magento/Authorizenet/Model/Request.php -@@ -3,12 +3,15 @@ - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -+declare(strict_types=1); -+ - namespace Magento\Authorizenet\Model; - - use Magento\Framework\DataObject; - - /** - * Request object -+ * @deprecated 2.3.1 Authorize.net is removing all support for this payment method - */ - class Request extends DataObject - { -diff --git a/app/code/Magento/Authorizenet/Model/Request/Factory.php b/app/code/Magento/Authorizenet/Model/Request/Factory.php -index e60bbd0c88e..a7a636280e2 100644 ---- a/app/code/Magento/Authorizenet/Model/Request/Factory.php -+++ b/app/code/Magento/Authorizenet/Model/Request/Factory.php -@@ -3,10 +3,13 @@ - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -+declare(strict_types=1); -+ - namespace Magento\Authorizenet\Model\Request; - - /** - * Factory class for @see \Magento\Authorizenet\Model\Request -+ * @deprecated 2.3.1 Authorize.net is removing all support for this payment method - */ - class Factory - { -diff --git a/app/code/Magento/Authorizenet/Model/ResourceModel/Debug.php b/app/code/Magento/Authorizenet/Model/ResourceModel/Debug.php -index ee6ec6783bb..2c21d0e2e28 100644 ---- a/app/code/Magento/Authorizenet/Model/ResourceModel/Debug.php -+++ b/app/code/Magento/Authorizenet/Model/ResourceModel/Debug.php -@@ -3,10 +3,13 @@ - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -+declare(strict_types=1); -+ - namespace Magento\Authorizenet\Model\ResourceModel; - - /** - * Resource Authorize.net debug model -+ * @deprecated 2.3.1 Authorize.net is removing all support for this payment method - */ - class Debug extends \Magento\Framework\Model\ResourceModel\Db\AbstractDb - { -diff --git a/app/code/Magento/Authorizenet/Model/ResourceModel/Debug/Collection.php b/app/code/Magento/Authorizenet/Model/ResourceModel/Debug/Collection.php -index 095ac5a91dd..b84ee1e72a2 100644 ---- a/app/code/Magento/Authorizenet/Model/ResourceModel/Debug/Collection.php -+++ b/app/code/Magento/Authorizenet/Model/ResourceModel/Debug/Collection.php -@@ -3,10 +3,13 @@ - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -+declare(strict_types=1); -+ - namespace Magento\Authorizenet\Model\ResourceModel\Debug; - - /** - * Resource Authorize.net debug collection model -+ * @deprecated 2.3.1 Authorize.net is removing all support for this payment method - */ - class Collection extends \Magento\Framework\Model\ResourceModel\Db\Collection\AbstractCollection - { -diff --git a/app/code/Magento/Authorizenet/Model/Response.php b/app/code/Magento/Authorizenet/Model/Response.php -index 52b43c251dc..c552663a153 100644 ---- a/app/code/Magento/Authorizenet/Model/Response.php -+++ b/app/code/Magento/Authorizenet/Model/Response.php -@@ -3,12 +3,15 @@ - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -+declare(strict_types=1); -+ - namespace Magento\Authorizenet\Model; - - use Magento\Framework\DataObject; - - /** - * Response object -+ * @deprecated 2.3.1 Authorize.net is removing all support for this payment method - */ - class Response extends DataObject - { -diff --git a/app/code/Magento/Authorizenet/Model/Response/Factory.php b/app/code/Magento/Authorizenet/Model/Response/Factory.php -index 74bf8953471..45780955660 100644 ---- a/app/code/Magento/Authorizenet/Model/Response/Factory.php -+++ b/app/code/Magento/Authorizenet/Model/Response/Factory.php -@@ -3,10 +3,13 @@ - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -+declare(strict_types=1); -+ - namespace Magento\Authorizenet\Model\Response; - - /** - * Factory class for @see \Magento\Authorizenet\Model\Response -+ * @deprecated 2.3.1 Authorize.net is removing all support for this payment method - */ - class Factory - { -diff --git a/app/code/Magento/Authorizenet/Model/Source/Cctype.php b/app/code/Magento/Authorizenet/Model/Source/Cctype.php -index 0a5fe8ab9b3..ffb35847224 100644 ---- a/app/code/Magento/Authorizenet/Model/Source/Cctype.php -+++ b/app/code/Magento/Authorizenet/Model/Source/Cctype.php -@@ -3,16 +3,21 @@ - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -+declare(strict_types=1); -+ - namespace Magento\Authorizenet\Model\Source; - - use Magento\Payment\Model\Source\Cctype as PaymentCctype; - - /** - * Authorize.net Payment CC Types Source Model -+ * @deprecated 2.3.1 Authorize.net is removing all support for this payment method - */ - class Cctype extends PaymentCctype - { - /** -+ * Return all supported credit card types -+ * - * @return string[] - */ - public function getAllowedTypes() -diff --git a/app/code/Magento/Authorizenet/Model/Source/PaymentAction.php b/app/code/Magento/Authorizenet/Model/Source/PaymentAction.php -index 9943e1001da..c6e57557f65 100644 ---- a/app/code/Magento/Authorizenet/Model/Source/PaymentAction.php -+++ b/app/code/Magento/Authorizenet/Model/Source/PaymentAction.php -@@ -3,18 +3,20 @@ - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -+declare(strict_types=1); -+ - namespace Magento\Authorizenet\Model\Source; - - use Magento\Framework\Option\ArrayInterface; - - /** -- * - * Authorize.net Payment Action Dropdown source -+ * @deprecated 2.3.1 Authorize.net is removing all support for this payment method - */ - class PaymentAction implements ArrayInterface - { - /** -- * {@inheritdoc} -+ * @inheritdoc - */ - public function toOptionArray() - { -diff --git a/app/code/Magento/Authorizenet/Model/TransactionService.php b/app/code/Magento/Authorizenet/Model/TransactionService.php -index 693a5b890fa..af0b02e94cf 100644 ---- a/app/code/Magento/Authorizenet/Model/TransactionService.php -+++ b/app/code/Magento/Authorizenet/Model/TransactionService.php -@@ -3,6 +3,7 @@ - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -+declare(strict_types=1); - - namespace Magento\Authorizenet\Model; - -@@ -15,7 +16,7 @@ use Magento\Payment\Model\Method\Logger; - - /** - * Class TransactionService -- * @package Magento\Authorizenet\Model -+ * @deprecated 2.3.1 Authorize.net is removing all support for this payment method - */ - class TransactionService - { -@@ -74,6 +75,7 @@ class TransactionService - - /** - * Get transaction information -+ * - * @param \Magento\Authorizenet\Model\Authorizenet $context - * @param string $transactionId - * @return \Magento\Framework\Simplexml\Element -@@ -142,6 +144,7 @@ class TransactionService - - /** - * Create request body to get transaction details -+ * - * @param string $login - * @param string $transactionKey - * @param string $transactionId -diff --git a/app/code/Magento/Authorizenet/Observer/AddFieldsToResponseObserver.php b/app/code/Magento/Authorizenet/Observer/AddFieldsToResponseObserver.php -index 03846dddfde..bdd10437927 100644 ---- a/app/code/Magento/Authorizenet/Observer/AddFieldsToResponseObserver.php -+++ b/app/code/Magento/Authorizenet/Observer/AddFieldsToResponseObserver.php -@@ -3,11 +3,19 @@ - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -+declare(strict_types=1); -+ - namespace Magento\Authorizenet\Observer; - - use Magento\Framework\Event\ObserverInterface; - use Magento\Sales\Model\Order; - -+/** -+ * Class AddFieldsToResponseObserver -+ * -+ * @SuppressWarnings(PHPMD.CookieAndSessionMisuse) -+ * @deprecated 2.3.1 Authorize.net is removing all support for this payment method -+ */ - class AddFieldsToResponseObserver implements ObserverInterface - { - /** -diff --git a/app/code/Magento/Authorizenet/Observer/SaveOrderAfterSubmitObserver.php b/app/code/Magento/Authorizenet/Observer/SaveOrderAfterSubmitObserver.php -index 8426d004c20..45f0adfa96f 100644 ---- a/app/code/Magento/Authorizenet/Observer/SaveOrderAfterSubmitObserver.php -+++ b/app/code/Magento/Authorizenet/Observer/SaveOrderAfterSubmitObserver.php -@@ -3,11 +3,17 @@ - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -+declare(strict_types=1); -+ - namespace Magento\Authorizenet\Observer; - - use Magento\Framework\Event\ObserverInterface; - use Magento\Sales\Model\Order; - -+/** -+ * Class SaveOrderAfterSubmitObserver -+ * @deprecated 2.3.1 Authorize.net is removing all support for this payment method -+ */ - class SaveOrderAfterSubmitObserver implements ObserverInterface - { - /** -diff --git a/app/code/Magento/Authorizenet/Observer/UpdateAllEditIncrementsObserver.php b/app/code/Magento/Authorizenet/Observer/UpdateAllEditIncrementsObserver.php -index 3e62fe2278d..d6cc51eb63c 100644 ---- a/app/code/Magento/Authorizenet/Observer/UpdateAllEditIncrementsObserver.php -+++ b/app/code/Magento/Authorizenet/Observer/UpdateAllEditIncrementsObserver.php -@@ -3,11 +3,17 @@ - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -+declare(strict_types=1); -+ - namespace Magento\Authorizenet\Observer; - - use Magento\Framework\Event\ObserverInterface; - use Magento\Sales\Model\Order; - -+/** -+ * Class UpdateAllEditIncrementsObserver -+ * @deprecated 2.3.1 Authorize.net is removing all support for this payment method -+ */ - class UpdateAllEditIncrementsObserver implements ObserverInterface - { - /** -diff --git a/app/code/Magento/Authorizenet/Test/Unit/Model/Directpost/RequestTest.php b/app/code/Magento/Authorizenet/Test/Unit/Model/Directpost/RequestTest.php -new file mode 100644 -index 00000000000..94d8f3a0d27 ---- /dev/null -+++ b/app/code/Magento/Authorizenet/Test/Unit/Model/Directpost/RequestTest.php -@@ -0,0 +1,80 @@ -+<?php -+/** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+namespace Magento\Authorizenet\Test\Unit\Model\Directpost; -+ -+use Magento\Authorizenet\Model\Directpost\Request; -+use Magento\Framework\Intl\DateTimeFactory; -+use PHPUnit_Framework_MockObject_MockObject as MockObject; -+ -+class RequestTest extends \PHPUnit\Framework\TestCase -+{ -+ /** -+ * @var DateTimeFactory|MockObject -+ */ -+ private $dateTimeFactory; -+ -+ /** -+ * @var Request -+ */ -+ private $requestModel; -+ -+ protected function setUp() -+ { -+ $this->dateTimeFactory = $this->getMockBuilder(DateTimeFactory::class) -+ ->disableOriginalConstructor() -+ ->getMock(); -+ $dateTime = new \DateTime('2016-07-05 00:00:00', new \DateTimeZone('UTC')); -+ $this->dateTimeFactory->method('create') -+ ->willReturn($dateTime); -+ -+ $this->requestModel = new Request([], $this->dateTimeFactory); -+ } -+ -+ /** -+ * @param string $signatureKey -+ * @param string $expectedHash -+ * @dataProvider signRequestDataProvider -+ */ -+ public function testSignRequestData(string $signatureKey, string $expectedHash) -+ { -+ /** @var \Magento\Authorizenet\Model\Directpost $paymentMethod */ -+ $paymentMethod = $this->createMock(\Magento\Authorizenet\Model\Directpost::class); -+ $paymentMethod->method('getConfigData') -+ ->willReturnMap( -+ [ -+ ['test', null, true], -+ ['login', null, 'login'], -+ ['trans_key', null, 'trans_key'], -+ ['signature_key', null, $signatureKey], -+ ] -+ ); -+ -+ $this->requestModel->setConstantData($paymentMethod); -+ $this->requestModel->signRequestData(); -+ $signHash = $this->requestModel->getXFpHash(); -+ -+ $this->assertEquals($expectedHash, $signHash); -+ } -+ -+ /** -+ * @return array -+ */ -+ public function signRequestDataProvider() -+ { -+ return [ -+ [ -+ 'signatureKey' => '3EAFCE5697C1B4B9748385C1FCD29D86F3B9B41C7EED85A3A01DFF65' . -+ '70C8C29373C2A153355C3313CDF4AF723C0036DBF244A0821713A910024EE85547CEF37F', -+ 'expectedHash' => '719ED94DF5CF3510CB5531E8115462C8F12CBCC8E917BD809E8D40B4FF06' . -+ '1E14953554403DD9813CCCE0F31B184EB4DEF558E9C0747505A0C25420372DB00BE1' -+ ], -+ [ -+ 'signatureKey' => '', -+ 'expectedHash' => '3656211f2c41d1e4c083606f326c0460' -+ ], -+ ]; -+ } -+} -diff --git a/app/code/Magento/Authorizenet/Test/Unit/Model/Directpost/ResponseTest.php b/app/code/Magento/Authorizenet/Test/Unit/Model/Directpost/ResponseTest.php -index 15c7eecb09a..ff4aa8b5ee3 100644 ---- a/app/code/Magento/Authorizenet/Test/Unit/Model/Directpost/ResponseTest.php -+++ b/app/code/Magento/Authorizenet/Test/Unit/Model/Directpost/ResponseTest.php -@@ -13,53 +13,16 @@ class ResponseTest extends \PHPUnit\Framework\TestCase - /** - * @var \Magento\Authorizenet\Model\Directpost\Response - */ -- protected $responseModel; -+ private $responseModel; - - protected function setUp() - { - $objectManager = new ObjectManager($this); -- $this->responseModel = $objectManager->getObject(\Magento\Authorizenet\Model\Directpost\Response::class); -- } -- -- /** -- * @param string $merchantMd5 -- * @param string $merchantApiLogin -- * @param float|null $amount -- * @param float|string $amountTestFunc -- * @param string $transactionId -- * @dataProvider generateHashDataProvider -- */ -- public function testGenerateHash($merchantMd5, $merchantApiLogin, $amount, $amountTestFunc, $transactionId) -- { -- $this->assertEquals( -- $this->generateHash($merchantMd5, $merchantApiLogin, $amountTestFunc, $transactionId), -- $this->responseModel->generateHash($merchantMd5, $merchantApiLogin, $amount, $transactionId) -+ $this->responseModel = $objectManager->getObject( -+ \Magento\Authorizenet\Model\Directpost\Response::class - ); - } - -- /** -- * @return array -- */ -- public function generateHashDataProvider() -- { -- return [ -- [ -- 'merchantMd5' => 'FCD7F001E9274FDEFB14BFF91C799306', -- 'merchantApiLogin' => 'Magento', -- 'amount' => null, -- 'amountTestFunc' => '0.00', -- 'transactionId' => '1' -- ], -- [ -- 'merchantMd5' => '8AEF4E508261A287C3E2F544720FCA3A', -- 'merchantApiLogin' => 'Magento2', -- 'amount' => 100.50, -- 'amountTestFunc' => 100.50, -- 'transactionId' => '2' -- ] -- ]; -- } -- - /** - * @param $merchantMd5 - * @param $merchantApiLogin -@@ -73,7 +36,8 @@ class ResponseTest extends \PHPUnit\Framework\TestCase - } - - /** -- * @param string $merchantMd5 -+ * @param string $storedHash -+ * @param string $hashKey - * @param string $merchantApiLogin - * @param float|null $amount - * @param string $transactionId -@@ -81,12 +45,21 @@ class ResponseTest extends \PHPUnit\Framework\TestCase - * @param bool $expectedValue - * @dataProvider isValidHashDataProvider - */ -- public function testIsValidHash($merchantMd5, $merchantApiLogin, $amount, $transactionId, $hash, $expectedValue) -- { -+ public function testIsValidHash( -+ string $storedHash, -+ string $hashKey, -+ string $merchantApiLogin, -+ $amount, -+ string $transactionId, -+ string $hash, -+ bool $expectedValue -+ ) { - $this->responseModel->setXAmount($amount); - $this->responseModel->setXTransId($transactionId); -- $this->responseModel->setData('x_MD5_Hash', $hash); -- $this->assertEquals($expectedValue, $this->responseModel->isValidHash($merchantMd5, $merchantApiLogin)); -+ $this->responseModel->setData($hashKey, $hash); -+ $result = $this->responseModel->isValidHash($storedHash, $merchantApiLogin); -+ -+ $this->assertEquals($expectedValue, $result); - } - - /** -@@ -94,9 +67,14 @@ class ResponseTest extends \PHPUnit\Framework\TestCase - */ - public function isValidHashDataProvider() - { -+ $signatureKey = '3EAFCE5697C1B4B9748385C1FCD29D86F3B9B41C7EED85A3A01DFF6570C8C' . -+ '29373C2A153355C3313CDF4AF723C0036DBF244A0821713A910024EE85547CEF37F'; -+ $expectedSha2Hash = '368D48E0CD1274BF41C059138DA69985594021A4AD5B4C5526AE88C8F' . -+ '7C5769B13C5E1E4358900F3E51076FB69D14B0A797904C22E8A11A52AA49CDE5FBB703C'; - return [ - [ - 'merchantMd5' => 'FCD7F001E9274FDEFB14BFF91C799306', -+ 'hashKey' => 'x_MD5_Hash', - 'merchantApiLogin' => 'Magento', - 'amount' => null, - 'transactionId' => '1', -@@ -105,11 +83,21 @@ class ResponseTest extends \PHPUnit\Framework\TestCase - ], - [ - 'merchantMd5' => '8AEF4E508261A287C3E2F544720FCA3A', -+ 'hashKey' => 'x_MD5_Hash', - 'merchantApiLogin' => 'Magento2', - 'amount' => 100.50, - 'transactionId' => '2', - 'hash' => '1F24A4EC9A169B2B2A072A5F168E16DC', - 'expectedValue' => false -+ ], -+ [ -+ 'signatureKey' => $signatureKey, -+ 'hashKey' => 'x_SHA2_Hash', -+ 'merchantApiLogin' => 'Magento2', -+ 'amount' => 100.50, -+ 'transactionId' => '2', -+ 'hash' => $expectedSha2Hash, -+ 'expectedValue' => true - ] - ]; - } -diff --git a/app/code/Magento/Authorizenet/etc/adminhtml/system.xml b/app/code/Magento/Authorizenet/etc/adminhtml/system.xml -index 1319fa102d0..fc86c0d2dc6 100644 ---- a/app/code/Magento/Authorizenet/etc/adminhtml/system.xml -+++ b/app/code/Magento/Authorizenet/etc/adminhtml/system.xml -@@ -9,7 +9,7 @@ - <system> - <section id="payment"> - <group id="authorizenet_directpost" translate="label" type="text" sortOrder="34" showInDefault="1" showInWebsite="1" showInStore="1"> -- <label>Authorize.net Direct Post</label> -+ <label>Authorize.Net Direct Post (Deprecated)</label> - <field id="active" translate="label" type="select" sortOrder="10" showInDefault="1" showInWebsite="1" showInStore="0" canRestore="1"> - <label>Enabled</label> - <source_model>Magento\Config\Model\Config\Source\Yesno</source_model> -@@ -29,6 +29,10 @@ - <label>Transaction Key</label> - <backend_model>Magento\Config\Model\Config\Backend\Encrypted</backend_model> - </field> -+ <field id="signature_key" translate="label" type="obscure" sortOrder="55" showInDefault="1" showInWebsite="1" showInStore="0"> -+ <label>Signature Key</label> -+ <backend_model>Magento\Config\Model\Config\Backend\Encrypted</backend_model> -+ </field> - <field id="trans_md5" translate="label" type="obscure" sortOrder="60" showInDefault="1" showInWebsite="1" showInStore="0"> - <label>Merchant MD5</label> - <backend_model>Magento\Config\Model\Config\Backend\Encrypted</backend_model> -diff --git a/app/code/Magento/Authorizenet/etc/config.xml b/app/code/Magento/Authorizenet/etc/config.xml -index eacf77cda1e..60356460f55 100644 ---- a/app/code/Magento/Authorizenet/etc/config.xml -+++ b/app/code/Magento/Authorizenet/etc/config.xml -@@ -19,9 +19,10 @@ - <order_status>processing</order_status> - <payment_action>authorize</payment_action> - <test>1</test> -- <title>Credit Card Direct Post (Authorize.net) -+ Credit Card Direct Post (Authorize.Net) - - -+ - 0 - USD - 1 -@@ -32,6 +33,8 @@ - https://secure.authorize.net/gateway/transact.dll - https://apitest.authorize.net/xml/v1/request.api - https://api2.authorize.net/xml/v1/request.api -+ x_card_type,x_account_number,x_avs_code,x_auth_code,x_response_reason_text,x_cvv2_resp_code -+ authorizenet - - - -diff --git a/app/code/Magento/Authorizenet/etc/di.xml b/app/code/Magento/Authorizenet/etc/di.xml -index 4beb2456be1..69d24019f2f 100644 ---- a/app/code/Magento/Authorizenet/etc/di.xml -+++ b/app/code/Magento/Authorizenet/etc/di.xml -@@ -35,4 +35,9 @@ - - - -+ -+ -+ Magento\Authorizenet\Model\Directpost -+ -+ - -diff --git a/app/code/Magento/Authorizenet/etc/payment.xml b/app/code/Magento/Authorizenet/etc/payment.xml -new file mode 100644 -index 00000000000..1d2cac374d8 ---- /dev/null -+++ b/app/code/Magento/Authorizenet/etc/payment.xml -@@ -0,0 +1,15 @@ -+ -+ -+ -+ -+ -+ -+ -+ -+ -diff --git a/app/code/Magento/Authorizenet/i18n/en_US.csv b/app/code/Magento/Authorizenet/i18n/en_US.csv -index bb59afffff2..d724bd960d3 100644 ---- a/app/code/Magento/Authorizenet/i18n/en_US.csv -+++ b/app/code/Magento/Authorizenet/i18n/en_US.csv -@@ -45,7 +45,7 @@ void,void - "Fraud Filters","Fraud Filters" - "Place Order","Place Order" - "Sorry, but something went wrong. Please contact the seller.","Sorry, but something went wrong. Please contact the seller." --"Authorize.net Direct Post","Authorize.net Direct Post" -+"Authorize.Net Direct Post (Deprecated)","Authorize.Net Direct Post (Deprecated)" - Enabled,Enabled - "Payment Action","Payment Action" - Title,Title -@@ -67,3 +67,9 @@ Debug,Debug - "Minimum Order Total","Minimum Order Total" - "Maximum Order Total","Maximum Order Total" - "Sort Order","Sort Order" -+"x_card_type","Credit Card Type" -+"x_account_number", "Credit Card Number" -+"x_avs_code","AVS Response Code" -+"x_auth_code","Processor Authentication Code" -+"x_response_reason_text","Processor Response Text" -+"x_cvv2_resp_code","CVV2 Response Code" -diff --git a/app/code/Magento/Authorizenet/view/adminhtml/templates/directpost/iframe.phtml b/app/code/Magento/Authorizenet/view/adminhtml/templates/directpost/iframe.phtml -index 2ef93aca480..30887139894 100644 ---- a/app/code/Magento/Authorizenet/view/adminhtml/templates/directpost/iframe.phtml -+++ b/app/code/Magento/Authorizenet/view/adminhtml/templates/directpost/iframe.phtml -@@ -4,8 +4,6 @@ - * See COPYING.txt for license details. - */ - --// @codingStandardsIgnoreFile -- - /** - * @var $block \Magento\Authorizenet\Block\Transparent\Iframe - */ -@@ -15,15 +13,15 @@ $helper = $block->getHelper('adminhtml'); - - - -\ No newline at end of file -diff --git a/app/code/Magento/AuthorizenetAcceptjs/view/adminhtml/web/js/authorizenet.js b/app/code/Magento/AuthorizenetAcceptjs/view/adminhtml/web/js/authorizenet.js -new file mode 100644 -index 00000000000..0eb865d7666 ---- /dev/null -+++ b/app/code/Magento/AuthorizenetAcceptjs/view/adminhtml/web/js/authorizenet.js -@@ -0,0 +1,196 @@ -+/** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+define([ -+ 'jquery', -+ 'uiComponent', -+ 'Magento_Ui/js/modal/alert', -+ 'Magento_AuthorizenetAcceptjs/js/view/payment/acceptjs-client' -+], function ($, Class, alert, AcceptjsClient) { -+ 'use strict'; -+ -+ return Class.extend({ -+ defaults: { -+ acceptjsClient: null, -+ $selector: null, -+ selector: 'edit_form', -+ container: 'payment_form_authorizenet_acceptjs', -+ active: false, -+ imports: { -+ onActiveChange: 'active' -+ } -+ }, -+ -+ /** -+ * @{inheritdoc} -+ */ -+ initConfig: function (config) { -+ this._super(); -+ -+ this.acceptjsClient = AcceptjsClient({ -+ environment: config.environment -+ }); -+ -+ return this; -+ }, -+ -+ /** -+ * @{inheritdoc} -+ */ -+ initObservable: function () { -+ this.$selector = $('#' + this.selector); -+ this._super() -+ .observe('active'); -+ -+ // re-init payment method events -+ this.$selector.off('changePaymentMethod.' + this.code) -+ .on('changePaymentMethod.' + this.code, this.changePaymentMethod.bind(this)); -+ -+ return this; -+ }, -+ -+ /** -+ * Enable/disable current payment method -+ * -+ * @param {Object} event -+ * @param {String} method -+ * @returns {Object} -+ */ -+ changePaymentMethod: function (event, method) { -+ this.active(method === this.code); -+ -+ return this; -+ }, -+ -+ /** -+ * Triggered when payment changed -+ * -+ * @param {Boolean} isActive -+ */ -+ onActiveChange: function (isActive) { -+ if (!isActive) { -+ -+ return; -+ } -+ -+ this.disableEventListeners(); -+ -+ window.order.addExcludedPaymentMethod(this.code); -+ -+ this.enableEventListeners(); -+ }, -+ -+ /** -+ * Sets the payment details on the form -+ * -+ * @param {Object} tokens -+ */ -+ setPaymentDetails: function (tokens) { -+ var $ccNumber = $(this.getSelector('cc_number')), -+ ccLast4 = $ccNumber.val().replace(/[^\d]/g, '').substr(-4); -+ -+ $(this.getSelector('opaque_data_descriptor')).val(tokens.opaqueDataDescriptor); -+ $(this.getSelector('opaque_data_value')).val(tokens.opaqueDataValue); -+ $(this.getSelector('cc_last_4')).val(ccLast4); -+ $ccNumber.val(''); -+ $(this.getSelector('cc_exp_month')).val(''); -+ $(this.getSelector('cc_exp_year')).val(''); -+ -+ if (this.useCvv) { -+ $(this.getSelector('cc_cid')).val(''); -+ } -+ }, -+ -+ /** -+ * Trigger order submit -+ */ -+ submitOrder: function () { -+ var authData = {}, -+ cardData = {}, -+ secureData = {}; -+ -+ this.$selector.validate().form(); -+ this.$selector.trigger('afterValidate.beforeSubmit'); -+ -+ authData.clientKey = this.clientKey; -+ authData.apiLoginID = this.apiLoginID; -+ -+ cardData.cardNumber = $(this.getSelector('cc_number')).val(); -+ cardData.month = $(this.getSelector('cc_exp_month')).val(); -+ cardData.year = $(this.getSelector('cc_exp_year')).val(); -+ -+ if (this.useCvv) { -+ cardData.cardCode = $(this.getSelector('cc_cid')).val(); -+ } -+ -+ secureData.authData = authData; -+ secureData.cardData = cardData; -+ -+ this.disableEventListeners(); -+ -+ this.acceptjsClient.createTokens(secureData) -+ .always(function () { -+ $('body').trigger('processStop'); -+ this.enableEventListeners(); -+ }.bind(this)) -+ .done(function (tokens) { -+ this.setPaymentDetails(tokens); -+ this.placeOrder(); -+ }.bind(this)) -+ .fail(function (messages) { -+ this.tokens = null; -+ -+ if (messages.length > 0) { -+ this._showError(messages[0]); -+ } -+ }.bind(this)); -+ -+ return false; -+ }, -+ -+ /** -+ * Place order -+ */ -+ placeOrder: function () { -+ this.$selector.trigger('realOrder'); -+ }, -+ -+ /** -+ * Get jQuery selector -+ * -+ * @param {String} field -+ * @returns {String} -+ */ -+ getSelector: function (field) { -+ return '#' + this.code + '_' + field; -+ }, -+ -+ /** -+ * Show alert message -+ * -+ * @param {String} message -+ */ -+ _showError: function (message) { -+ alert({ -+ content: message -+ }); -+ }, -+ -+ /** -+ * Enable form event listeners -+ */ -+ enableEventListeners: function () { -+ this.$selector.on('submitOrder.authorizenetacceptjs', this.submitOrder.bind(this)); -+ }, -+ -+ /** -+ * Disable form event listeners -+ */ -+ disableEventListeners: function () { -+ this.$selector.off('submitOrder'); -+ this.$selector.off('submit'); -+ } -+ -+ }); -+}); -diff --git a/app/code/Magento/AuthorizenetAcceptjs/view/adminhtml/web/js/payment-form.js b/app/code/Magento/AuthorizenetAcceptjs/view/adminhtml/web/js/payment-form.js -new file mode 100644 -index 00000000000..68c2f22f6ed ---- /dev/null -+++ b/app/code/Magento/AuthorizenetAcceptjs/view/adminhtml/web/js/payment-form.js -@@ -0,0 +1,18 @@ -+/** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+define([ -+ 'Magento_AuthorizenetAcceptjs/js/authorizenet', -+ 'jquery' -+], function (AuthorizenetAcceptjs, $) { -+ 'use strict'; -+ -+ return function (data, element) { -+ var $form = $(element), -+ config = data.config; -+ -+ config.active = $form.length > 0 && !$form.is(':hidden'); -+ new AuthorizenetAcceptjs(config); -+ }; -+}); -diff --git a/app/code/Magento/AuthorizenetAcceptjs/view/base/requirejs-config.js b/app/code/Magento/AuthorizenetAcceptjs/view/base/requirejs-config.js -new file mode 100644 -index 00000000000..83ddd1094ea ---- /dev/null -+++ b/app/code/Magento/AuthorizenetAcceptjs/view/base/requirejs-config.js -@@ -0,0 +1,19 @@ -+/** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+ -+var config = { -+ shim: { -+ acceptjs: { -+ exports: 'Accept' -+ }, -+ acceptjssandbox: { -+ exports: 'Accept' -+ } -+ }, -+ paths: { -+ acceptjssandbox: 'https://jstest.authorize.net/v1/Accept', -+ acceptjs: 'https://js.authorize.net/v1/Accept' -+ } -+}; -diff --git a/app/code/Magento/AuthorizenetAcceptjs/view/base/web/js/view/payment/acceptjs-client.js b/app/code/Magento/AuthorizenetAcceptjs/view/base/web/js/view/payment/acceptjs-client.js -new file mode 100644 -index 00000000000..935465f5298 ---- /dev/null -+++ b/app/code/Magento/AuthorizenetAcceptjs/view/base/web/js/view/payment/acceptjs-client.js -@@ -0,0 +1,73 @@ -+/** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+ -+define([ -+ 'jquery', -+ 'uiClass', -+ 'Magento_AuthorizenetAcceptjs/js/view/payment/acceptjs-factory', -+ 'Magento_AuthorizenetAcceptjs/js/view/payment/validator-handler' -+], function ($, Class, acceptjsFactory, validatorHandler) { -+ 'use strict'; -+ -+ return Class.extend({ -+ defaults: { -+ environment: 'production' -+ }, -+ -+ /** -+ * @{inheritdoc} -+ */ -+ initialize: function () { -+ validatorHandler.initialize(); -+ -+ this._super(); -+ -+ return this; -+ }, -+ -+ /** -+ * Creates the token pair with the provided data -+ * -+ * @param {Object} data -+ * @return {jQuery.Deferred} -+ */ -+ createTokens: function (data) { -+ var deferred = $.Deferred(); -+ -+ if (this.acceptjsClient) { -+ this._createTokens(deferred, data); -+ } else { -+ acceptjsFactory(this.environment) -+ .done(function (client) { -+ this.acceptjsClient = client; -+ this._createTokens(deferred, data); -+ }.bind(this)); -+ } -+ -+ return deferred.promise(); -+ }, -+ -+ /** -+ * Creates a token from the payment information in the form -+ * -+ * @param {jQuery.Deferred} deferred -+ * @param {Object} data -+ */ -+ _createTokens: function (deferred, data) { -+ this.acceptjsClient.dispatchData(data, function (response) { -+ validatorHandler.validate(response, function (valid, messages) { -+ if (valid) { -+ deferred.resolve({ -+ opaqueDataDescriptor: response.opaqueData.dataDescriptor, -+ opaqueDataValue: response.opaqueData.dataValue -+ }); -+ } else { -+ deferred.reject(messages); -+ } -+ }); -+ }); -+ } -+ }); -+}); -diff --git a/app/code/Magento/AuthorizenetAcceptjs/view/base/web/js/view/payment/acceptjs-factory.js b/app/code/Magento/AuthorizenetAcceptjs/view/base/web/js/view/payment/acceptjs-factory.js -new file mode 100644 -index 00000000000..e98a204e36c ---- /dev/null -+++ b/app/code/Magento/AuthorizenetAcceptjs/view/base/web/js/view/payment/acceptjs-factory.js -@@ -0,0 +1,38 @@ -+/** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+ -+define([ -+ 'jquery' -+], function ($) { -+ 'use strict'; -+ -+ return function (environment) { -+ var deferred = $.Deferred(), -+ dependency = 'acceptjs'; -+ -+ if (environment === 'sandbox') { -+ dependency = 'acceptjssandbox'; -+ } -+ -+ require([dependency], function (accept) { -+ var $body = $('body'); -+ -+ /* -+ * Acceptjs doesn't safely load dependent files which leads to a race condition when trying to use -+ * the sdk right away. -+ * @see https://community.developer.authorize.net/t5/Integration-and-Testing/ -+ * Dynamically-loading-Accept-js-E-WC-03-Accept-js-is-not-loaded/td-p/63283 -+ */ -+ $body.on('handshake.acceptjs', function () { -+ deferred.resolve(accept); -+ $body.off('handshake.acceptjs'); -+ }); -+ }, -+ deferred.reject -+ ); -+ -+ return deferred.promise(); -+ }; -+}); -diff --git a/app/code/Magento/AuthorizenetAcceptjs/view/base/web/js/view/payment/response-validator.js b/app/code/Magento/AuthorizenetAcceptjs/view/base/web/js/view/payment/response-validator.js -new file mode 100644 -index 00000000000..3c44ca2f9e4 ---- /dev/null -+++ b/app/code/Magento/AuthorizenetAcceptjs/view/base/web/js/view/payment/response-validator.js -@@ -0,0 +1,38 @@ -+/** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+ -+define([ -+ 'jquery', -+ 'mage/translate' -+], function ($, $t) { -+ 'use strict'; -+ -+ return { -+ /** -+ * Validate Authorizenet-Acceptjs response -+ * -+ * @param {Object} context -+ * @returns {jQuery.Deferred} -+ */ -+ validate: function (context) { -+ var state = $.Deferred(), -+ messages = []; -+ -+ if (context.messages.resultCode === 'Ok') { -+ state.resolve(); -+ } else { -+ if (context.messages.message.length > 0) { -+ $.each(context.messages.message, function (index, element) { -+ messages.push($t(element.text)); -+ }); -+ } -+ state.reject(messages); -+ } -+ -+ return state.promise(); -+ } -+ }; -+}); -+ -diff --git a/app/code/Magento/AuthorizenetAcceptjs/view/base/web/js/view/payment/validator-handler.js b/app/code/Magento/AuthorizenetAcceptjs/view/base/web/js/view/payment/validator-handler.js -new file mode 100644 -index 00000000000..109f159c9a7 ---- /dev/null -+++ b/app/code/Magento/AuthorizenetAcceptjs/view/base/web/js/view/payment/validator-handler.js -@@ -0,0 +1,59 @@ -+/** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+ -+define([ -+ 'jquery', -+ 'Magento_AuthorizenetAcceptjs/js/view/payment/response-validator' -+], function ($, responseValidator) { -+ 'use strict'; -+ -+ return { -+ validators: [], -+ -+ /** -+ * Init list of validators -+ */ -+ initialize: function () { -+ this.add(responseValidator); -+ }, -+ -+ /** -+ * Add new validator -+ * @param {Object} validator -+ */ -+ add: function (validator) { -+ this.validators.push(validator); -+ }, -+ -+ /** -+ * Run pull of validators -+ * @param {Object} context -+ * @param {Function} callback -+ */ -+ validate: function (context, callback) { -+ var self = this, -+ deferred; -+ -+ // no available validators -+ if (!self.validators.length) { -+ callback(true); -+ -+ return; -+ } -+ -+ // get list of deferred validators -+ deferred = $.map(self.validators, function (current) { -+ return current.validate(context); -+ }); -+ -+ $.when.apply($, deferred) -+ .done(function () { -+ callback(true); -+ }).fail(function (error) { -+ callback(false, error); -+ }); -+ } -+ }; -+}); -diff --git a/app/code/Magento/AuthorizenetAcceptjs/view/frontend/layout/checkout_index_index.xml b/app/code/Magento/AuthorizenetAcceptjs/view/frontend/layout/checkout_index_index.xml -new file mode 100644 -index 00000000000..f31b06c9be9 ---- /dev/null -+++ b/app/code/Magento/AuthorizenetAcceptjs/view/frontend/layout/checkout_index_index.xml -@@ -0,0 +1,49 @@ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ uiComponent -+ -+ -+ -+ -+ -+ -+ -+ Magento_AuthorizenetAcceptjs/js/view/payment/authorizenet -+ -+ -+ true -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -\ No newline at end of file -diff --git a/app/code/Magento/AuthorizenetAcceptjs/view/frontend/web/js/view/payment/authorizenet.js b/app/code/Magento/AuthorizenetAcceptjs/view/frontend/web/js/view/payment/authorizenet.js -new file mode 100644 -index 00000000000..a05fe739a44 ---- /dev/null -+++ b/app/code/Magento/AuthorizenetAcceptjs/view/frontend/web/js/view/payment/authorizenet.js -@@ -0,0 +1,20 @@ -+/** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+ -+define([ -+ 'uiComponent', -+ 'Magento_Checkout/js/model/payment/renderer-list' -+], -+function (Component, rendererList) { -+ 'use strict'; -+ -+ rendererList.push({ -+ type: 'authorizenet_acceptjs', -+ component: 'Magento_AuthorizenetAcceptjs/js/view/payment/method-renderer/authorizenet-accept' -+ }); -+ -+ /** Add view logic here if needed */ -+ return Component.extend({}); -+}); -diff --git a/app/code/Magento/AuthorizenetAcceptjs/view/frontend/web/js/view/payment/method-renderer/authorizenet-accept.js b/app/code/Magento/AuthorizenetAcceptjs/view/frontend/web/js/view/payment/method-renderer/authorizenet-accept.js -new file mode 100644 -index 00000000000..983318c4cda ---- /dev/null -+++ b/app/code/Magento/AuthorizenetAcceptjs/view/frontend/web/js/view/payment/method-renderer/authorizenet-accept.js -@@ -0,0 +1,146 @@ -+/** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+ -+define([ -+ 'jquery', -+ 'Magento_Payment/js/view/payment/cc-form', -+ 'Magento_AuthorizenetAcceptjs/js/view/payment/acceptjs-client', -+ 'Magento_Checkout/js/model/full-screen-loader', -+ 'Magento_Ui/js/model/messageList', -+ 'Magento_Payment/js/model/credit-card-validation/validator' -+], function ($, Component, AcceptjsClient, fullScreenLoader, globalMessageList) { -+ 'use strict'; -+ -+ return Component.extend({ -+ defaults: { -+ active: false, -+ template: 'Magento_AuthorizenetAcceptjs/payment/authorizenet-acceptjs', -+ tokens: null, -+ ccForm: 'Magento_Payment/payment/cc-form', -+ acceptjsClient: null -+ }, -+ -+ /** -+ * Set list of observable attributes -+ * -+ * @returns {exports.initObservable} -+ */ -+ initObservable: function () { -+ this._super() -+ .observe(['active']); -+ -+ return this; -+ }, -+ -+ /** -+ * @returns {String} -+ */ -+ getCode: function () { -+ return 'authorizenet_acceptjs'; -+ }, -+ -+ /** -+ * Initialize form elements for validation -+ */ -+ initFormElement: function (element) { -+ this.formElement = element; -+ this.acceptjsClient = AcceptjsClient({ -+ environment: window.checkoutConfig.payment[this.getCode()].environment -+ }); -+ $(this.formElement).validation(); -+ }, -+ -+ /** -+ * @returns {Object} -+ */ -+ getData: function () { -+ return { -+ method: this.getCode(), -+ 'additional_data': { -+ opaqueDataDescriptor: this.tokens ? this.tokens.opaqueDataDescriptor : null, -+ opaqueDataValue: this.tokens ? this.tokens.opaqueDataValue : null, -+ ccLast4: this.creditCardNumber().substr(-4) -+ } -+ }; -+ }, -+ -+ /** -+ * Check if payment is active -+ * -+ * @returns {Boolean} -+ */ -+ isActive: function () { -+ var active = this.getCode() === this.isChecked(); -+ -+ this.active(active); -+ -+ return active; -+ }, -+ -+ /** -+ * Prepare data to place order -+ */ -+ beforePlaceOrder: function () { -+ var authData = {}, -+ cardData = {}, -+ secureData = {}; -+ -+ if (!$(this.formElement).valid()) { -+ return; -+ } -+ -+ authData.clientKey = window.checkoutConfig.payment[this.getCode()].clientKey; -+ authData.apiLoginID = window.checkoutConfig.payment[this.getCode()].apiLoginID; -+ -+ cardData.cardNumber = this.creditCardNumber(); -+ cardData.month = this.creditCardExpMonth(); -+ cardData.year = this.creditCardExpYear(); -+ -+ if (this.hasVerification()) { -+ cardData.cardCode = this.creditCardVerificationNumber(); -+ } -+ -+ secureData.authData = authData; -+ secureData.cardData = cardData; -+ -+ fullScreenLoader.startLoader(); -+ -+ this.acceptjsClient.createTokens(secureData) -+ .always(function () { -+ fullScreenLoader.stopLoader(); -+ }) -+ .done(function (tokens) { -+ this.tokens = tokens; -+ this.placeOrder(); -+ }.bind(this)) -+ .fail(function (messages) { -+ this.tokens = null; -+ this._showErrors(messages); -+ }.bind(this)); -+ }, -+ -+ /** -+ * Should the cvv field be used -+ * -+ * @return {Boolean} -+ */ -+ hasVerification: function () { -+ return window.checkoutConfig.payment[this.getCode()].useCvv; -+ }, -+ -+ /** -+ * Show error messages -+ * -+ * @param {String[]} errorMessages -+ */ -+ _showErrors: function (errorMessages) { -+ $.each(errorMessages, function (index, message) { -+ globalMessageList.addErrorMessage({ -+ message: message -+ }); -+ }); -+ } -+ }); -+}); -diff --git a/app/code/Magento/AuthorizenetAcceptjs/view/frontend/web/template/payment/authorizenet-acceptjs.html b/app/code/Magento/AuthorizenetAcceptjs/view/frontend/web/template/payment/authorizenet-acceptjs.html -new file mode 100644 -index 00000000000..6db52a2b102 ---- /dev/null -+++ b/app/code/Magento/AuthorizenetAcceptjs/view/frontend/web/template/payment/authorizenet-acceptjs.html -@@ -0,0 +1,45 @@ -+ -+

-diff --git a/app/code/Magento/AuthorizenetCardinal/Gateway/Request/Authorize3DSecureBuilder.php b/app/code/Magento/AuthorizenetCardinal/Gateway/Request/Authorize3DSecureBuilder.php -new file mode 100644 -index 00000000000..7e3d63d6f18 ---- /dev/null -+++ b/app/code/Magento/AuthorizenetCardinal/Gateway/Request/Authorize3DSecureBuilder.php -@@ -0,0 +1,82 @@ -+subjectReader = $subjectReader; -+ $this->config = $config; -+ $this->jwtParser = $jwtParser; -+ } -+ -+ /** -+ * @inheritdoc -+ */ -+ public function build(array $buildSubject): array -+ { -+ if ($this->config->isActive() === false) { -+ return []; -+ } -+ -+ $paymentDO = $this->subjectReader->readPayment($buildSubject); -+ $payment = $paymentDO->getPayment(); -+ $data = []; -+ -+ if ($payment instanceof Payment) { -+ $cardinalJwt = (string)$payment->getAdditionalInformation('cardinalJWT'); -+ $jwtPayload = $this->jwtParser->execute($cardinalJwt); -+ $eciFlag = $jwtPayload['Payload']['Payment']['ExtendedData']['ECIFlag'] ?? ''; -+ $cavv = $jwtPayload['Payload']['Payment']['ExtendedData']['CAVV'] ?? ''; -+ $data = [ -+ 'transactionRequest' => [ -+ 'cardholderAuthentication' => [ -+ 'authenticationIndicator' => $eciFlag, -+ 'cardholderAuthenticationValue' => $cavv -+ ], -+ ] -+ ]; -+ } -+ -+ return $data; -+ } -+} -diff --git a/app/code/Magento/AuthorizenetCardinal/LICENSE.txt b/app/code/Magento/AuthorizenetCardinal/LICENSE.txt -new file mode 100644 -index 00000000000..49525fd99da ---- /dev/null -+++ b/app/code/Magento/AuthorizenetCardinal/LICENSE.txt -@@ -0,0 +1,48 @@ -+ -+Open Software License ("OSL") v. 3.0 -+ -+This Open Software License (the "License") applies to any original work of authorship (the "Original Work") whose owner (the "Licensor") has placed the following licensing notice adjacent to the copyright notice for the Original Work: -+ -+Licensed under the Open Software License version 3.0 -+ -+ 1. Grant of Copyright License. Licensor grants You a worldwide, royalty-free, non-exclusive, sublicensable license, for the duration of the copyright, to do the following: -+ -+ 1. to reproduce the Original Work in copies, either alone or as part of a collective work; -+ -+ 2. to translate, adapt, alter, transform, modify, or arrange the Original Work, thereby creating derivative works ("Derivative Works") based upon the Original Work; -+ -+ 3. to distribute or communicate copies of the Original Work and Derivative Works to the public, with the proviso that copies of Original Work or Derivative Works that You distribute or communicate shall be licensed under this Open Software License; -+ -+ 4. to perform the Original Work publicly; and -+ -+ 5. to display the Original Work publicly. -+ -+ 2. Grant of Patent License. Licensor grants You a worldwide, royalty-free, non-exclusive, sublicensable license, under patent claims owned or controlled by the Licensor that are embodied in the Original Work as furnished by the Licensor, for the duration of the patents, to make, use, sell, offer for sale, have made, and import the Original Work and Derivative Works. -+ -+ 3. Grant of Source Code License. The term "Source Code" means the preferred form of the Original Work for making modifications to it and all available documentation describing how to modify the Original Work. Licensor agrees to provide a machine-readable copy of the Source Code of the Original Work along with each copy of the Original Work that Licensor distributes. Licensor reserves the right to satisfy this obligation by placing a machine-readable copy of the Source Code in an information repository reasonably calculated to permit inexpensive and convenient access by You for as long as Licensor continues to distribute the Original Work. -+ -+ 4. Exclusions From License Grant. Neither the names of Licensor, nor the names of any contributors to the Original Work, nor any of their trademarks or service marks, may be used to endorse or promote products derived from this Original Work without express prior permission of the Licensor. Except as expressly stated herein, nothing in this License grants any license to Licensor's trademarks, copyrights, patents, trade secrets or any other intellectual property. No patent license is granted to make, use, sell, offer for sale, have made, or import embodiments of any patent claims other than the licensed claims defined in Section 2. No license is granted to the trademarks of Licensor even if such marks are included in the Original Work. Nothing in this License shall be interpreted to prohibit Licensor from licensing under terms different from this License any Original Work that Licensor otherwise would have a right to license. -+ -+ 5. External Deployment. The term "External Deployment" means the use, distribution, or communication of the Original Work or Derivative Works in any way such that the Original Work or Derivative Works may be used by anyone other than You, whether those works are distributed or communicated to those persons or made available as an application intended for use over a network. As an express condition for the grants of license hereunder, You must treat any External Deployment by You of the Original Work or a Derivative Work as a distribution under section 1(c). -+ -+ 6. Attribution Rights. You must retain, in the Source Code of any Derivative Works that You create, all copyright, patent, or trademark notices from the Source Code of the Original Work, as well as any notices of licensing and any descriptive text identified therein as an "Attribution Notice." You must cause the Source Code for any Derivative Works that You create to carry a prominent Attribution Notice reasonably calculated to inform recipients that You have modified the Original Work. -+ -+ 7. Warranty of Provenance and Disclaimer of Warranty. Licensor warrants that the copyright in and to the Original Work and the patent rights granted herein by Licensor are owned by the Licensor or are sublicensed to You under the terms of this License with the permission of the contributor(s) of those copyrights and patent rights. Except as expressly stated in the immediately preceding sentence, the Original Work is provided under this License on an "AS IS" BASIS and WITHOUT WARRANTY, either express or implied, including, without limitation, the warranties of non-infringement, merchantability or fitness for a particular purpose. THE ENTIRE RISK AS TO THE QUALITY OF THE ORIGINAL WORK IS WITH YOU. This DISCLAIMER OF WARRANTY constitutes an essential part of this License. No license to the Original Work is granted by this License except under this disclaimer. -+ -+ 8. Limitation of Liability. Under no circumstances and under no legal theory, whether in tort (including negligence), contract, or otherwise, shall the Licensor be liable to anyone for any indirect, special, incidental, or consequential damages of any character arising as a result of this License or the use of the Original Work including, without limitation, damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses. This limitation of liability shall not apply to the extent applicable law prohibits such limitation. -+ -+ 9. Acceptance and Termination. If, at any time, You expressly assented to this License, that assent indicates your clear and irrevocable acceptance of this License and all of its terms and conditions. If You distribute or communicate copies of the Original Work or a Derivative Work, You must make a reasonable effort under the circumstances to obtain the express assent of recipients to the terms of this License. This License conditions your rights to undertake the activities listed in Section 1, including your right to create Derivative Works based upon the Original Work, and doing so without honoring these terms and conditions is prohibited by copyright law and international treaty. Nothing in this License is intended to affect copyright exceptions and limitations (including 'fair use' or 'fair dealing'). This License shall terminate immediately and You may no longer exercise any of the rights granted to You by this License upon your failure to honor the conditions in Section 1(c). -+ -+ 10. Termination for Patent Action. This License shall terminate automatically and You may no longer exercise any of the rights granted to You by this License as of the date You commence an action, including a cross-claim or counterclaim, against Licensor or any licensee alleging that the Original Work infringes a patent. This termination provision shall not apply for an action alleging patent infringement by combinations of the Original Work with other software or hardware. -+ -+ 11. Jurisdiction, Venue and Governing Law. Any action or suit relating to this License may be brought only in the courts of a jurisdiction wherein the Licensor resides or in which Licensor conducts its primary business, and under the laws of that jurisdiction excluding its conflict-of-law provisions. The application of the United Nations Convention on Contracts for the International Sale of Goods is expressly excluded. Any use of the Original Work outside the scope of this License or after its termination shall be subject to the requirements and penalties of copyright or patent law in the appropriate jurisdiction. This section shall survive the termination of this License. -+ -+ 12. Attorneys' Fees. In any action to enforce the terms of this License or seeking damages relating thereto, the prevailing party shall be entitled to recover its costs and expenses, including, without limitation, reasonable attorneys' fees and costs incurred in connection with such action, including any appeal of such action. This section shall survive the termination of this License. -+ -+ 13. Miscellaneous. If any provision of this License is held to be unenforceable, such provision shall be reformed only to the extent necessary to make it enforceable. -+ -+ 14. Definition of "You" in This License. "You" throughout this License, whether in upper or lower case, means an individual or a legal entity exercising rights under, and complying with all of the terms of, this License. For legal entities, "You" includes any entity that controls, is controlled by, or is under common control with you. For purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. -+ -+ 15. Right to Use. You may use the Original Work in all ways not otherwise restricted or conditioned by this License or by law, and Licensor promises not to interfere with or be responsible for such uses by You. -+ -+ 16. Modification of This License. This License is Copyright (C) 2005 Lawrence Rosen. Permission is granted to copy, distribute, or communicate this License without modification. Nothing in this License permits You to modify this License as applied to the Original Work or to Derivative Works. However, You may modify the text of this License and copy, distribute or communicate your modified version (the "Modified License") and apply it to other original works of authorship subject to the following conditions: (i) You may not indicate in any way that your Modified License is the "Open Software License" or "OSL" and you may not use those names in the name of your Modified License; (ii) You must replace the notice specified in the first paragraph above with the notice "Licensed under " or with a notice of your own that is not confusingly similar to the notice in this License; and (iii) You may not claim that your original works are open source software unless your Modified License has been approved by Open Source Initiative (OSI) and You comply with its license review and certification process. -\ No newline at end of file -diff --git a/app/code/Magento/AuthorizenetCardinal/LICENSE_AFL.txt b/app/code/Magento/AuthorizenetCardinal/LICENSE_AFL.txt -new file mode 100644 -index 00000000000..f39d641b18a ---- /dev/null -+++ b/app/code/Magento/AuthorizenetCardinal/LICENSE_AFL.txt -@@ -0,0 +1,48 @@ -+ -+Academic Free License ("AFL") v. 3.0 -+ -+This Academic Free License (the "License") applies to any original work of authorship (the "Original Work") whose owner (the "Licensor") has placed the following licensing notice adjacent to the copyright notice for the Original Work: -+ -+Licensed under the Academic Free License version 3.0 -+ -+ 1. Grant of Copyright License. Licensor grants You a worldwide, royalty-free, non-exclusive, sublicensable license, for the duration of the copyright, to do the following: -+ -+ 1. to reproduce the Original Work in copies, either alone or as part of a collective work; -+ -+ 2. to translate, adapt, alter, transform, modify, or arrange the Original Work, thereby creating derivative works ("Derivative Works") based upon the Original Work; -+ -+ 3. to distribute or communicate copies of the Original Work and Derivative Works to the public, under any license of your choice that does not contradict the terms and conditions, including Licensor's reserved rights and remedies, in this Academic Free License; -+ -+ 4. to perform the Original Work publicly; and -+ -+ 5. to display the Original Work publicly. -+ -+ 2. Grant of Patent License. Licensor grants You a worldwide, royalty-free, non-exclusive, sublicensable license, under patent claims owned or controlled by the Licensor that are embodied in the Original Work as furnished by the Licensor, for the duration of the patents, to make, use, sell, offer for sale, have made, and import the Original Work and Derivative Works. -+ -+ 3. Grant of Source Code License. The term "Source Code" means the preferred form of the Original Work for making modifications to it and all available documentation describing how to modify the Original Work. Licensor agrees to provide a machine-readable copy of the Source Code of the Original Work along with each copy of the Original Work that Licensor distributes. Licensor reserves the right to satisfy this obligation by placing a machine-readable copy of the Source Code in an information repository reasonably calculated to permit inexpensive and convenient access by You for as long as Licensor continues to distribute the Original Work. -+ -+ 4. Exclusions From License Grant. Neither the names of Licensor, nor the names of any contributors to the Original Work, nor any of their trademarks or service marks, may be used to endorse or promote products derived from this Original Work without express prior permission of the Licensor. Except as expressly stated herein, nothing in this License grants any license to Licensor's trademarks, copyrights, patents, trade secrets or any other intellectual property. No patent license is granted to make, use, sell, offer for sale, have made, or import embodiments of any patent claims other than the licensed claims defined in Section 2. No license is granted to the trademarks of Licensor even if such marks are included in the Original Work. Nothing in this License shall be interpreted to prohibit Licensor from licensing under terms different from this License any Original Work that Licensor otherwise would have a right to license. -+ -+ 5. External Deployment. The term "External Deployment" means the use, distribution, or communication of the Original Work or Derivative Works in any way such that the Original Work or Derivative Works may be used by anyone other than You, whether those works are distributed or communicated to those persons or made available as an application intended for use over a network. As an express condition for the grants of license hereunder, You must treat any External Deployment by You of the Original Work or a Derivative Work as a distribution under section 1(c). -+ -+ 6. Attribution Rights. You must retain, in the Source Code of any Derivative Works that You create, all copyright, patent, or trademark notices from the Source Code of the Original Work, as well as any notices of licensing and any descriptive text identified therein as an "Attribution Notice." You must cause the Source Code for any Derivative Works that You create to carry a prominent Attribution Notice reasonably calculated to inform recipients that You have modified the Original Work. -+ -+ 7. Warranty of Provenance and Disclaimer of Warranty. Licensor warrants that the copyright in and to the Original Work and the patent rights granted herein by Licensor are owned by the Licensor or are sublicensed to You under the terms of this License with the permission of the contributor(s) of those copyrights and patent rights. Except as expressly stated in the immediately preceding sentence, the Original Work is provided under this License on an "AS IS" BASIS and WITHOUT WARRANTY, either express or implied, including, without limitation, the warranties of non-infringement, merchantability or fitness for a particular purpose. THE ENTIRE RISK AS TO THE QUALITY OF THE ORIGINAL WORK IS WITH YOU. This DISCLAIMER OF WARRANTY constitutes an essential part of this License. No license to the Original Work is granted by this License except under this disclaimer. -+ -+ 8. Limitation of Liability. Under no circumstances and under no legal theory, whether in tort (including negligence), contract, or otherwise, shall the Licensor be liable to anyone for any indirect, special, incidental, or consequential damages of any character arising as a result of this License or the use of the Original Work including, without limitation, damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses. This limitation of liability shall not apply to the extent applicable law prohibits such limitation. -+ -+ 9. Acceptance and Termination. If, at any time, You expressly assented to this License, that assent indicates your clear and irrevocable acceptance of this License and all of its terms and conditions. If You distribute or communicate copies of the Original Work or a Derivative Work, You must make a reasonable effort under the circumstances to obtain the express assent of recipients to the terms of this License. This License conditions your rights to undertake the activities listed in Section 1, including your right to create Derivative Works based upon the Original Work, and doing so without honoring these terms and conditions is prohibited by copyright law and international treaty. Nothing in this License is intended to affect copyright exceptions and limitations (including "fair use" or "fair dealing"). This License shall terminate immediately and You may no longer exercise any of the rights granted to You by this License upon your failure to honor the conditions in Section 1(c). -+ -+ 10. Termination for Patent Action. This License shall terminate automatically and You may no longer exercise any of the rights granted to You by this License as of the date You commence an action, including a cross-claim or counterclaim, against Licensor or any licensee alleging that the Original Work infringes a patent. This termination provision shall not apply for an action alleging patent infringement by combinations of the Original Work with other software or hardware. -+ -+ 11. Jurisdiction, Venue and Governing Law. Any action or suit relating to this License may be brought only in the courts of a jurisdiction wherein the Licensor resides or in which Licensor conducts its primary business, and under the laws of that jurisdiction excluding its conflict-of-law provisions. The application of the United Nations Convention on Contracts for the International Sale of Goods is expressly excluded. Any use of the Original Work outside the scope of this License or after its termination shall be subject to the requirements and penalties of copyright or patent law in the appropriate jurisdiction. This section shall survive the termination of this License. -+ -+ 12. Attorneys' Fees. In any action to enforce the terms of this License or seeking damages relating thereto, the prevailing party shall be entitled to recover its costs and expenses, including, without limitation, reasonable attorneys' fees and costs incurred in connection with such action, including any appeal of such action. This section shall survive the termination of this License. -+ -+ 13. Miscellaneous. If any provision of this License is held to be unenforceable, such provision shall be reformed only to the extent necessary to make it enforceable. -+ -+ 14. Definition of "You" in This License. "You" throughout this License, whether in upper or lower case, means an individual or a legal entity exercising rights under, and complying with all of the terms of, this License. For legal entities, "You" includes any entity that controls, is controlled by, or is under common control with you. For purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. -+ -+ 15. Right to Use. You may use the Original Work in all ways not otherwise restricted or conditioned by this License or by law, and Licensor promises not to interfere with or be responsible for such uses by You. -+ -+ 16. Modification of This License. This License is Copyright © 2005 Lawrence Rosen. Permission is granted to copy, distribute, or communicate this License without modification. Nothing in this License permits You to modify this License as applied to the Original Work or to Derivative Works. However, You may modify the text of this License and copy, distribute or communicate your modified version (the "Modified License") and apply it to other original works of authorship subject to the following conditions: (i) You may not indicate in any way that your Modified License is the "Academic Free License" or "AFL" and you may not use those names in the name of your Modified License; (ii) You must replace the notice specified in the first paragraph above with the notice "Licensed under " or with a notice of your own that is not confusingly similar to the notice in this License; and (iii) You may not claim that your original works are open source software unless your Modified License has been approved by Open Source Initiative (OSI) and You comply with its license review and certification process. -diff --git a/app/code/Magento/AuthorizenetCardinal/Model/Checkout/ConfigProvider.php b/app/code/Magento/AuthorizenetCardinal/Model/Checkout/ConfigProvider.php -new file mode 100644 -index 00000000000..d0cde9c643e ---- /dev/null -+++ b/app/code/Magento/AuthorizenetCardinal/Model/Checkout/ConfigProvider.php -@@ -0,0 +1,45 @@ -+config = $config; -+ } -+ -+ /** -+ * @inheritdoc -+ */ -+ public function getConfig(): array -+ { -+ $config['cardinal'] = [ -+ 'isActiveFor' => [ -+ 'authorizenet' => $this->config->isActive() -+ ] -+ ]; -+ -+ return $config; -+ } -+} -diff --git a/app/code/Magento/AuthorizenetCardinal/Model/Config.php b/app/code/Magento/AuthorizenetCardinal/Model/Config.php -new file mode 100644 -index 00000000000..e70a6a2e39c ---- /dev/null -+++ b/app/code/Magento/AuthorizenetCardinal/Model/Config.php -@@ -0,0 +1,49 @@ -+scopeConfig = $scopeConfig; -+ } -+ -+ /** -+ * If this config option set to false no AuthorizenetCardinal integration should be available -+ * -+ * @param int|null $storeId -+ * @return bool -+ */ -+ public function isActive(?int $storeId = null): bool -+ { -+ $enabled = $this->scopeConfig->isSetFlag( -+ 'three_d_secure/cardinal/enabled_authorizenet', -+ ScopeInterface::SCOPE_STORE, -+ $storeId -+ ); -+ -+ return $enabled; -+ } -+} -diff --git a/app/code/Magento/AuthorizenetCardinal/Observer/DataAssignObserver.php b/app/code/Magento/AuthorizenetCardinal/Observer/DataAssignObserver.php -new file mode 100644 -index 00000000000..cb2cdf64ae3 ---- /dev/null -+++ b/app/code/Magento/AuthorizenetCardinal/Observer/DataAssignObserver.php -@@ -0,0 +1,63 @@ -+config = $config; -+ } -+ -+ /** -+ * @inheritdoc -+ */ -+ public function execute(Observer $observer) -+ { -+ if ($this->config->isActive() === false) { -+ return; -+ } -+ -+ $data = $this->readDataArgument($observer); -+ $additionalData = $data->getData(PaymentInterface::KEY_ADDITIONAL_DATA); -+ if (!is_array($additionalData)) { -+ return; -+ } -+ -+ $paymentInfo = $this->readPaymentModelArgument($observer); -+ if (isset($additionalData[self::JWT_KEY])) { -+ $paymentInfo->setAdditionalInformation( -+ self::JWT_KEY, -+ $additionalData[self::JWT_KEY] -+ ); -+ } -+ } -+} -diff --git a/app/code/Magento/AuthorizenetCardinal/README.md b/app/code/Magento/AuthorizenetCardinal/README.md -new file mode 100644 -index 00000000000..2324f680baf ---- /dev/null -+++ b/app/code/Magento/AuthorizenetCardinal/README.md -@@ -0,0 +1 @@ -+The AuthorizenetCardinal module provides a possibility to enable 3-D Secure 2.0 support for AuthorizenetAcceptjs payment integration. -\ No newline at end of file -diff --git a/app/code/Magento/AuthorizenetCardinal/Test/Unit/Observer/DataAssignObserverTest.php b/app/code/Magento/AuthorizenetCardinal/Test/Unit/Observer/DataAssignObserverTest.php -new file mode 100644 -index 00000000000..9f560507e34 ---- /dev/null -+++ b/app/code/Magento/AuthorizenetCardinal/Test/Unit/Observer/DataAssignObserverTest.php -@@ -0,0 +1,100 @@ -+ 'foo' -+ ]; -+ -+ $config = $this->createMock(Config::class); -+ $config->method('isActive') -+ ->willReturn(true); -+ $observerContainer = $this->createMock(Observer::class); -+ $event = $this->createMock(Event::class); -+ $paymentInfoModel = $this->createMock(InfoInterface::class); -+ $dataObject = new DataObject([PaymentInterface::KEY_ADDITIONAL_DATA => $additionalInfo]); -+ $observerContainer->method('getEvent') -+ ->willReturn($event); -+ $event->method('getDataByKey') -+ ->willReturnMap( -+ [ -+ [AbstractDataAssignObserver::MODEL_CODE, $paymentInfoModel], -+ [AbstractDataAssignObserver::DATA_CODE, $dataObject] -+ ] -+ ); -+ $paymentInfoModel->expects($this->once()) -+ ->method('setAdditionalInformation') -+ ->with('cardinalJWT', 'foo'); -+ -+ $observer = new DataAssignObserver($config); -+ $observer->execute($observerContainer); -+ } -+ -+ /** -+ * Tests case when Cardinal JWT is absent. -+ */ -+ public function testDoesntSetDataWhenEmpty() -+ { -+ $config = $this->createMock(Config::class); -+ $config->method('isActive') -+ ->willReturn(true); -+ $observerContainer = $this->createMock(Observer::class); -+ $event = $this->createMock(Event::class); -+ $paymentInfoModel = $this->createMock(InfoInterface::class); -+ $observerContainer->method('getEvent') -+ ->willReturn($event); -+ $event->method('getDataByKey') -+ ->willReturnMap( -+ [ -+ [AbstractDataAssignObserver::MODEL_CODE, $paymentInfoModel], -+ [AbstractDataAssignObserver::DATA_CODE, new DataObject()] -+ ] -+ ); -+ $paymentInfoModel->expects($this->never()) -+ ->method('setAdditionalInformation'); -+ -+ $observer = new DataAssignObserver($config); -+ $observer->execute($observerContainer); -+ } -+ -+ /** -+ * Tests case when CardinalCommerce is disabled. -+ */ -+ public function testDoesntSetDataWhenDisabled() -+ { -+ $config = $this->createMock(Config::class); -+ $config->method('isActive') -+ ->willReturn(false); -+ $observerContainer = $this->createMock(Observer::class); -+ $observerContainer->expects($this->never()) -+ ->method('getEvent'); -+ $observer = new DataAssignObserver($config); -+ $observer->execute($observerContainer); -+ } -+} -diff --git a/app/code/Magento/AuthorizenetCardinal/composer.json b/app/code/Magento/AuthorizenetCardinal/composer.json -new file mode 100644 -index 00000000000..2d3ceee2093 ---- /dev/null -+++ b/app/code/Magento/AuthorizenetCardinal/composer.json -@@ -0,0 +1,31 @@ -+{ -+ "name": "magento/module-authorizenet-cardinal", -+ "description": "Provides a possibility to enable 3-D Secure 2.0 support for Authorize.Net Acceptjs.", -+ "config": { -+ "sort-packages": true -+ }, -+ "require": { -+ "php": "~7.1.3||~7.2.0", -+ "magento/module-authorizenet-acceptjs": "*", -+ "magento/framework": "*", -+ "magento/module-cardinal-commerce": "*", -+ "magento/module-payment": "*", -+ "magento/module-sales": "*", -+ "magento/module-quote": "*", -+ "magento/module-checkout": "*", -+ "magento/module-store": "*" -+ }, -+ "type": "magento2-module", -+ "license": [ -+ "OSL-3.0", -+ "AFL-3.0" -+ ], -+ "autoload": { -+ "files": [ -+ "registration.php" -+ ], -+ "psr-4": { -+ "Magento\\AuthorizenetCardinal\\": "" -+ } -+ } -+} -diff --git a/app/code/Magento/AuthorizenetCardinal/etc/adminhtml/system.xml b/app/code/Magento/AuthorizenetCardinal/etc/adminhtml/system.xml -new file mode 100644 -index 00000000000..2be287a5e87 ---- /dev/null -+++ b/app/code/Magento/AuthorizenetCardinal/etc/adminhtml/system.xml -@@ -0,0 +1,22 @@ -+ -+ -+ -+ -+
-+ -+ -+ -+ -+ Magento\Config\Model\Config\Source\Yesno -+ three_d_secure/cardinal/enabled_authorizenet -+ -+ -+ -+
-+
-+
-diff --git a/app/code/Magento/AuthorizenetCardinal/etc/config.xml b/app/code/Magento/AuthorizenetCardinal/etc/config.xml -new file mode 100644 -index 00000000000..d94bcdc4790 ---- /dev/null -+++ b/app/code/Magento/AuthorizenetCardinal/etc/config.xml -@@ -0,0 +1,16 @@ -+ -+ -+ -+ -+ -+ -+ 0 -+ -+ -+ -+ -diff --git a/app/code/Magento/AuthorizenetCardinal/etc/di.xml b/app/code/Magento/AuthorizenetCardinal/etc/di.xml -new file mode 100644 -index 00000000000..45541a3cf49 ---- /dev/null -+++ b/app/code/Magento/AuthorizenetCardinal/etc/di.xml -@@ -0,0 +1,16 @@ -+ -+ -+ -+ -+ -+ -+ Magento\AuthorizenetCardinal\Gateway\Request\Authorize3DSecureBuilder -+ -+ -+ -+ -diff --git a/app/code/Magento/AuthorizenetCardinal/etc/events.xml b/app/code/Magento/AuthorizenetCardinal/etc/events.xml -new file mode 100644 -index 00000000000..5b0afbe6846 ---- /dev/null -+++ b/app/code/Magento/AuthorizenetCardinal/etc/events.xml -@@ -0,0 +1,13 @@ -+ -+ -+ -+ -+ -+ -+ -+ -diff --git a/app/code/Magento/AuthorizenetCardinal/etc/frontend/di.xml b/app/code/Magento/AuthorizenetCardinal/etc/frontend/di.xml -new file mode 100644 -index 00000000000..13c7a223e82 ---- /dev/null -+++ b/app/code/Magento/AuthorizenetCardinal/etc/frontend/di.xml -@@ -0,0 +1,18 @@ -+ -+ -+ -+ -+ -+ -+ -+ Magento\AuthorizenetCardinal\Model\Checkout\ConfigProvider -+ -+ -+ -+ -+ -diff --git a/app/code/Magento/AuthorizenetCardinal/etc/module.xml b/app/code/Magento/AuthorizenetCardinal/etc/module.xml -new file mode 100644 -index 00000000000..fdf8151311f ---- /dev/null -+++ b/app/code/Magento/AuthorizenetCardinal/etc/module.xml -@@ -0,0 +1,15 @@ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -diff --git a/app/code/Magento/AuthorizenetCardinal/registration.php b/app/code/Magento/AuthorizenetCardinal/registration.php -new file mode 100644 -index 00000000000..0153e9eaa4d ---- /dev/null -+++ b/app/code/Magento/AuthorizenetCardinal/registration.php -@@ -0,0 +1,9 @@ -+arrayManager = $arrayManager; -+ } -+ -+ /** -+ * Return additional data -+ * -+ * @param array $data -+ * @return array -+ */ -+ public function getData(array $data): array -+ { -+ $additionalData = $this->arrayManager->get(static::PATH_ADDITIONAL_DATA, $data) ?? []; -+ foreach ($additionalData as $key => $value) { -+ $additionalData[$this->snakeCaseToCamelCase($key)] = $value; -+ unset($additionalData[$key]); -+ } -+ return $additionalData; -+ } -+ -+ /** -+ * Converts an input string from snake_case to camelCase. -+ * -+ * @param string $input -+ * @return string -+ */ -+ private function snakeCaseToCamelCase($input) -+ { -+ return lcfirst(str_replace('_', '', ucwords($input, '_'))); -+ } -+} -diff --git a/app/code/Magento/AuthorizenetGraphQl/README.md b/app/code/Magento/AuthorizenetGraphQl/README.md -new file mode 100644 -index 00000000000..8b920e56934 ---- /dev/null -+++ b/app/code/Magento/AuthorizenetGraphQl/README.md -@@ -0,0 +1,3 @@ -+# AuthorizenetGraphQl -+ -+ **AuthorizenetGraphQl** defines the data types needed to pass payment information data from the client to Magento. -diff --git a/app/code/Magento/AuthorizenetGraphQl/composer.json b/app/code/Magento/AuthorizenetGraphQl/composer.json -new file mode 100644 -index 00000000000..6adf11ff72b ---- /dev/null -+++ b/app/code/Magento/AuthorizenetGraphQl/composer.json -@@ -0,0 +1,25 @@ -+{ -+ "name": "magento/module-authorizenet-graph-ql", -+ "description": "N/A", -+ "type": "magento2-module", -+ "require": { -+ "php": "~7.1.3||~7.2.0", -+ "magento/framework": "*", -+ "magento/module-quote-graph-ql": "*" -+ }, -+ "suggest": { -+ "magento/module-graph-ql": "*" -+ }, -+ "license": [ -+ "OSL-3.0", -+ "AFL-3.0" -+ ], -+ "autoload": { -+ "files": [ -+ "registration.php" -+ ], -+ "psr-4": { -+ "Magento\\AuthorizenetGraphQl\\": "" -+ } -+ } -+} -diff --git a/app/code/Magento/AuthorizenetGraphQl/etc/graphql/di.xml b/app/code/Magento/AuthorizenetGraphQl/etc/graphql/di.xml -new file mode 100644 -index 00000000000..e8ea45091c0 ---- /dev/null -+++ b/app/code/Magento/AuthorizenetGraphQl/etc/graphql/di.xml -@@ -0,0 +1,16 @@ -+ -+ -+ -+ -+ -+ -+ Magento\AuthorizenetGraphQl\Model\AuthorizenetDataProvider -+ -+ -+ -+ -diff --git a/app/code/Magento/AuthorizenetGraphQl/etc/module.xml b/app/code/Magento/AuthorizenetGraphQl/etc/module.xml -new file mode 100644 -index 00000000000..85a780a8819 ---- /dev/null -+++ b/app/code/Magento/AuthorizenetGraphQl/etc/module.xml -@@ -0,0 +1,10 @@ -+ -+ -+ -+ -+ -diff --git a/app/code/Magento/AuthorizenetGraphQl/etc/schema.graphqls b/app/code/Magento/AuthorizenetGraphQl/etc/schema.graphqls -new file mode 100644 -index 00000000000..1d724bbde3c ---- /dev/null -+++ b/app/code/Magento/AuthorizenetGraphQl/etc/schema.graphqls -@@ -0,0 +1,12 @@ -+# Copyright © Magento, Inc. All rights reserved. -+# See COPYING.txt for license details. -+ -+input PaymentMethodAdditionalDataInput { -+ authorizenet_acceptjs: AuthorizenetInput @doc(description: "Defines the required attributes for Authorize.Net payments") -+} -+ -+input AuthorizenetInput { -+ opaque_data_descriptor: String! @doc(description: "Authorize.Net's description of the transaction request") -+ opaque_data_value: String! @doc(description: "The nonce returned by Authorize.Net") -+ cc_last_4: Int! @doc(description: "The last four digits of the credit or debit card") -+} -\ No newline at end of file -diff --git a/app/code/Magento/AuthorizenetGraphQl/registration.php b/app/code/Magento/AuthorizenetGraphQl/registration.php -new file mode 100644 -index 00000000000..2e50f9fe92a ---- /dev/null -+++ b/app/code/Magento/AuthorizenetGraphQl/registration.php -@@ -0,0 +1,10 @@ -+_actionFlag->set('', \Magento\Framework\App\ActionInterface::FLAG_NO_DISPATCH, true); - $this->_response->setRedirect($this->_url->getCurrentUrl()); -- $this->messageManager->addError(__('Invalid Form Key. Please refresh the page.')); -+ $this->messageManager->addErrorMessage(__('Invalid Form Key. Please refresh the page.')); - $isRedirectNeeded = true; - } - } -@@ -205,7 +205,7 @@ class Authentication - $this->_auth->login($username, $password); - } catch (AuthenticationException $e) { - if (!$request->getParam('messageSent')) { -- $this->messageManager->addError($e->getMessage()); -+ $this->messageManager->addErrorMessage($e->getMessage()); - $request->setParam('messageSent', true); - $outputValue = false; - } -diff --git a/app/code/Magento/Backend/App/DefaultPath.php b/app/code/Magento/Backend/App/DefaultPath.php -index df8b7183897..b790a2edc3f 100644 ---- a/app/code/Magento/Backend/App/DefaultPath.php -+++ b/app/code/Magento/Backend/App/DefaultPath.php -@@ -42,6 +42,6 @@ class DefaultPath implements \Magento\Framework\App\DefaultPathInterface - */ - public function getPart($code) - { -- return isset($this->_parts[$code]) ? $this->_parts[$code] : null; -+ return $this->_parts[$code] ?? null; - } - } -diff --git a/app/code/Magento/Backend/App/Request/BackendValidator.php b/app/code/Magento/Backend/App/Request/BackendValidator.php -index 878f9cb4dc4..7ec6c83f29b 100644 ---- a/app/code/Magento/Backend/App/Request/BackendValidator.php -+++ b/app/code/Magento/Backend/App/Request/BackendValidator.php -@@ -77,6 +77,8 @@ class BackendValidator implements ValidatorInterface - } - - /** -+ * Validate request -+ * - * @param RequestInterface $request - * @param ActionInterface $action - * -@@ -115,6 +117,8 @@ class BackendValidator implements ValidatorInterface - } - - /** -+ * Create exception -+ * - * @param RequestInterface $request - * @param ActionInterface $action - * -@@ -142,8 +146,9 @@ class BackendValidator implements ValidatorInterface - $exception = new InvalidRequestException($response); - } else { - //For regular requests. -+ $startPageUrl = $this->backendUrl->getStartupPageUrl(); - $response = $this->redirectFactory->create() -- ->setUrl($this->backendUrl->getStartupPageUrl()); -+ ->setUrl($this->backendUrl->getUrl($startPageUrl)); - $exception = new InvalidRequestException( - $response, - [ -@@ -166,7 +171,7 @@ class BackendValidator implements ValidatorInterface - ActionInterface $action - ): void { - if ($action instanceof AbstractAction) { -- //Abstract Action has build-in validation. -+ //Abstract Action has built-in validation. - if (!$action->_processUrlKeys()) { - throw new InvalidRequestException($action->getResponse()); - } -diff --git a/app/code/Magento/Backend/Block/Dashboard/Bar.php b/app/code/Magento/Backend/Block/Dashboard/Bar.php -index 7ccb2d51ccd..57f13c740f7 100644 ---- a/app/code/Magento/Backend/Block/Dashboard/Bar.php -+++ b/app/code/Magento/Backend/Block/Dashboard/Bar.php -@@ -5,6 +5,8 @@ - */ - namespace Magento\Backend\Block\Dashboard; - -+use Magento\Store\Model\Store; -+ - /** - * Adminhtml dashboard bar block - * -@@ -23,6 +25,8 @@ class Bar extends \Magento\Backend\Block\Dashboard\AbstractDashboard - protected $_currentCurrencyCode = null; - - /** -+ * Get totals -+ * - * @return array - */ - public function getTotals() -@@ -31,6 +35,8 @@ class Bar extends \Magento\Backend\Block\Dashboard\AbstractDashboard - } - - /** -+ * Add total -+ * - * @param string $label - * @param float $value - * @param bool $isQuantity -@@ -73,6 +79,7 @@ class Bar extends \Magento\Backend\Block\Dashboard\AbstractDashboard - * Retrieve currency model if not set then return currency model for current store - * - * @return \Magento\Directory\Model\Currency -+ * @SuppressWarnings(PHPMD.RequestAwareBlockMethod) - */ - public function getCurrency() - { -@@ -90,7 +97,8 @@ class Bar extends \Magento\Backend\Block\Dashboard\AbstractDashboard - $this->getRequest()->getParam('group') - )->getWebsite()->getBaseCurrency(); - } else { -- $this->_currentCurrencyCode = $this->_storeManager->getStore()->getBaseCurrency(); -+ $this->_currentCurrencyCode = $this->_storeManager->getStore(Store::DEFAULT_STORE_ID) -+ ->getBaseCurrency(); - } - } - -diff --git a/app/code/Magento/Backend/Block/Dashboard/Graph.php b/app/code/Magento/Backend/Block/Dashboard/Graph.php -index 8e238ccab44..b76421e4e6f 100644 ---- a/app/code/Magento/Backend/Block/Dashboard/Graph.php -+++ b/app/code/Magento/Backend/Block/Dashboard/Graph.php -@@ -3,6 +3,8 @@ - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -+declare(strict_types=1); -+ - namespace Magento\Backend\Block\Dashboard; - - /** -@@ -15,7 +17,7 @@ class Graph extends \Magento\Backend\Block\Dashboard\AbstractDashboard - /** - * Api URL - */ -- const API_URL = 'http://chart.apis.google.com/chart'; -+ const API_URL = 'https://image-charts.com/chart'; - - /** - * All series -@@ -76,6 +78,7 @@ class Graph extends \Magento\Backend\Block\Dashboard\AbstractDashboard - /** - * Google chart api data encoding - * -+ * @deprecated since the Google Image Charts API not accessible from March 14, 2019 - * @var string - */ - protected $_encoding = 'e'; -@@ -111,8 +114,8 @@ class Graph extends \Magento\Backend\Block\Dashboard\AbstractDashboard - \Magento\Backend\Helper\Dashboard\Data $dashboardData, - array $data = [] - ) { -- $this->_dashboardData = $dashboardData; - parent::__construct($context, $collectionFactory, $data); -+ $this->_dashboardData = $dashboardData; - } - - /** -@@ -128,7 +131,7 @@ class Graph extends \Magento\Backend\Block\Dashboard\AbstractDashboard - /** - * Set data rows - * -- * @param array $rows -+ * @param string $rows - * @return void - */ - public function setDataRows($rows) -@@ -152,15 +155,14 @@ class Graph extends \Magento\Backend\Block\Dashboard\AbstractDashboard - * Get series - * - * @param string $seriesId -- * @return array|false -+ * @return array|bool - */ - public function getSeries($seriesId) - { - if (isset($this->_allSeries[$seriesId])) { - return $this->_allSeries[$seriesId]; -- } else { -- return false; - } -+ return false; - } - - /** -@@ -187,11 +189,12 @@ class Graph extends \Magento\Backend\Block\Dashboard\AbstractDashboard - { - $params = [ - 'cht' => 'lc', -- 'chf' => 'bg,s,ffffff', -- 'chco' => 'ef672f', - 'chls' => '7', -- 'chxs' => '0,676056,15,0,l,676056|1,676056,15,0,l,676056', -- 'chm' => 'h,f2ebde,0,0:1:.1,1,-1', -+ 'chf' => 'bg,s,f4f4f4|c,lg,90,ffffff,0.1,ededed,0', -+ 'chm' => 'B,f4d4b2,0,0,0', -+ 'chco' => 'db4814', -+ 'chxs' => '0,0,11|1,0,11', -+ 'chma' => '15,15,15,15' - ]; - - $this->_allSeries = $this->getRowsData($this->_dataRows); -@@ -279,20 +282,11 @@ class Graph extends \Magento\Backend\Block\Dashboard\AbstractDashboard - $this->_axisLabels['x'] = $dates; - $this->_allSeries = $datas; - -- //Google encoding values -- if ($this->_encoding == "s") { -- // simple encoding -- $params['chd'] = "s:"; -- $dataDelimiter = ""; -- $dataSetdelimiter = ","; -- $dataMissing = "_"; -- } else { -- // extended encoding -- $params['chd'] = "e:"; -- $dataDelimiter = ""; -- $dataSetdelimiter = ","; -- $dataMissing = "__"; -- } -+ // Image-Charts Awesome data format values -+ $params['chd'] = "a:"; -+ $dataDelimiter = ","; -+ $dataSetdelimiter = "|"; -+ $dataMissing = "_"; - - // process each string in the array, and find the max length - $localmaxvalue = [0]; -@@ -306,7 +300,6 @@ class Graph extends \Magento\Backend\Block\Dashboard\AbstractDashboard - $minvalue = min($localminvalue); - - // default values -- $yrange = 0; - $yLabels = []; - $miny = 0; - $maxy = 0; -@@ -314,14 +307,13 @@ class Graph extends \Magento\Backend\Block\Dashboard\AbstractDashboard - - if ($minvalue >= 0 && $maxvalue >= 0) { - if ($maxvalue > 10) { -- $p = pow(10, $this->_getPow($maxvalue)); -+ $p = pow(10, $this->_getPow((int) $maxvalue)); - $maxy = ceil($maxvalue / $p) * $p; - $yLabels = range($miny, $maxy, $p); - } else { - $maxy = ceil($maxvalue + 1); - $yLabels = range($miny, $maxy, 1); - } -- $yrange = $maxy; - $yorigin = 0; - } - -@@ -329,44 +321,14 @@ class Graph extends \Magento\Backend\Block\Dashboard\AbstractDashboard - - foreach ($this->getAllSeries() as $index => $serie) { - $thisdataarray = $serie; -- if ($this->_encoding == "s") { -- // SIMPLE ENCODING -- for ($j = 0; $j < sizeof($thisdataarray); $j++) { -- $currentvalue = $thisdataarray[$j]; -- if (is_numeric($currentvalue)) { -- $ylocation = round( -- (strlen($this->_simpleEncoding) - 1) * ($yorigin + $currentvalue) / $yrange -- ); -- $chartdata[] = substr($this->_simpleEncoding, $ylocation, 1) . $dataDelimiter; -- } else { -- $chartdata[] = $dataMissing . $dataDelimiter; -- } -- } -- } else { -- // EXTENDED ENCODING -- for ($j = 0; $j < sizeof($thisdataarray); $j++) { -- $currentvalue = $thisdataarray[$j]; -- if (is_numeric($currentvalue)) { -- if ($yrange) { -- $ylocation = 4095 * ($yorigin + $currentvalue) / $yrange; -- } else { -- $ylocation = 0; -- } -- $firstchar = floor($ylocation / 64); -- $secondchar = $ylocation % 64; -- $mappedchar = substr( -- $this->_extendedEncoding, -- $firstchar, -- 1 -- ) . substr( -- $this->_extendedEncoding, -- $secondchar, -- 1 -- ); -- $chartdata[] = $mappedchar . $dataDelimiter; -- } else { -- $chartdata[] = $dataMissing . $dataDelimiter; -- } -+ $count = count($thisdataarray); -+ for ($j = 0; $j < $count; $j++) { -+ $currentvalue = $thisdataarray[$j]; -+ if (is_numeric($currentvalue)) { -+ $ylocation = $yorigin + $currentvalue; -+ $chartdata[] = $ylocation . $dataDelimiter; -+ } else { -+ $chartdata[] = $dataMissing . $dataDelimiter; - } - } - $chartdata[] = $dataSetdelimiter; -@@ -381,45 +343,13 @@ class Graph extends \Magento\Backend\Block\Dashboard\AbstractDashboard - - $valueBuffer = []; - -- if (sizeof($this->_axisLabels) > 0) { -+ if (count($this->_axisLabels) > 0) { - $params['chxt'] = implode(',', array_keys($this->_axisLabels)); - $indexid = 0; - foreach ($this->_axisLabels as $idx => $labels) { - if ($idx == 'x') { -- /** -- * Format date -- */ -- foreach ($this->_axisLabels[$idx] as $_index => $_label) { -- if ($_label != '') { -- $period = new \DateTime($_label, new \DateTimeZone($timezoneLocal)); -- switch ($this->getDataHelper()->getParam('period')) { -- case '24h': -- $this->_axisLabels[$idx][$_index] = $this->_localeDate->formatDateTime( -- $period->setTime($period->format('H'), 0, 0), -- \IntlDateFormatter::NONE, -- \IntlDateFormatter::SHORT -- ); -- break; -- case '7d': -- case '1m': -- $this->_axisLabels[$idx][$_index] = $this->_localeDate->formatDateTime( -- $period, -- \IntlDateFormatter::SHORT, -- \IntlDateFormatter::NONE -- ); -- break; -- case '1y': -- case '2y': -- $this->_axisLabels[$idx][$_index] = date('m/Y', strtotime($_label)); -- break; -- } -- } else { -- $this->_axisLabels[$idx][$_index] = ''; -- } -- } -- -+ $this->formatAxisLabelDate((string) $idx, (string) $timezoneLocal); - $tmpstring = implode('|', $this->_axisLabels[$idx]); -- - $valueBuffer[] = $indexid . ":|" . $tmpstring; - } elseif ($idx == 'y') { - $valueBuffer[] = $indexid . ":|" . implode('|', $yLabels); -@@ -438,12 +368,51 @@ class Graph extends \Magento\Backend\Block\Dashboard\AbstractDashboard - foreach ($params as $name => $value) { - $p[] = $name . '=' . urlencode($value); - } -- return self::API_URL . '?' . implode('&', $p); -- } else { -- $gaData = urlencode(base64_encode(json_encode($params))); -- $gaHash = $this->_dashboardData->getChartDataHash($gaData); -- $params = ['ga' => $gaData, 'h' => $gaHash]; -- return $this->getUrl('adminhtml/*/tunnel', ['_query' => $params]); -+ return (string) self::API_URL . '?' . implode('&', $p); -+ } -+ $gaData = urlencode(base64_encode(json_encode($params))); -+ $gaHash = $this->_dashboardData->getChartDataHash($gaData); -+ $params = ['ga' => $gaData, 'h' => $gaHash]; -+ return $this->getUrl('adminhtml/*/tunnel', ['_query' => $params]); -+ } -+ -+ /** -+ * Format dates for axis labels -+ * -+ * @param string $idx -+ * @param string $timezoneLocal -+ * -+ * @return void -+ */ -+ private function formatAxisLabelDate($idx, $timezoneLocal) -+ { -+ foreach ($this->_axisLabels[$idx] as $_index => $_label) { -+ if ($_label != '') { -+ $period = new \DateTime($_label, new \DateTimeZone($timezoneLocal)); -+ switch ($this->getDataHelper()->getParam('period')) { -+ case '24h': -+ $this->_axisLabels[$idx][$_index] = $this->_localeDate->formatDateTime( -+ $period->setTime((int) $period->format('H'), 0, 0), -+ \IntlDateFormatter::NONE, -+ \IntlDateFormatter::SHORT -+ ); -+ break; -+ case '7d': -+ case '1m': -+ $this->_axisLabels[$idx][$_index] = $this->_localeDate->formatDateTime( -+ $period, -+ \IntlDateFormatter::SHORT, -+ \IntlDateFormatter::NONE -+ ); -+ break; -+ case '1y': -+ case '2y': -+ $this->_axisLabels[$idx][$_index] = date('m/Y', strtotime($_label)); -+ break; -+ } -+ } else { -+ $this->_axisLabels[$idx][$_index] = ''; -+ } - } - } - -@@ -540,6 +509,8 @@ class Graph extends \Magento\Backend\Block\Dashboard\AbstractDashboard - } - - /** -+ * Sets data helper -+ * - * @param \Magento\Backend\Helper\Dashboard\AbstractDashboard $dataHelper - * @return void - */ -diff --git a/app/code/Magento/Backend/Block/Dashboard/Orders/Grid.php b/app/code/Magento/Backend/Block/Dashboard/Orders/Grid.php -index 50279786c0a..bca7f13b0ce 100644 ---- a/app/code/Magento/Backend/Block/Dashboard/Orders/Grid.php -+++ b/app/code/Magento/Backend/Block/Dashboard/Orders/Grid.php -@@ -19,21 +19,21 @@ class Grid extends \Magento\Backend\Block\Dashboard\Grid - protected $_collectionFactory; - - /** -- * @var \Magento\Framework\Module\Manager -+ * @var \Magento\Framework\Module\ModuleManagerInterface - */ - protected $_moduleManager; - - /** - * @param \Magento\Backend\Block\Template\Context $context - * @param \Magento\Backend\Helper\Data $backendHelper -- * @param \Magento\Framework\Module\Manager $moduleManager -+ * @param \Magento\Framework\Module\ModuleManagerInterface $moduleManager - * @param \Magento\Reports\Model\ResourceModel\Order\CollectionFactory $collectionFactory - * @param array $data - */ - public function __construct( - \Magento\Backend\Block\Template\Context $context, - \Magento\Backend\Helper\Data $backendHelper, -- \Magento\Framework\Module\Manager $moduleManager, -+ \Magento\Framework\Module\ModuleManagerInterface $moduleManager, - \Magento\Reports\Model\ResourceModel\Order\CollectionFactory $collectionFactory, - array $data = [] - ) { -@@ -43,6 +43,8 @@ class Grid extends \Magento\Backend\Block\Dashboard\Grid - } - - /** -+ * Construct. -+ * - * @return void - */ - protected function _construct() -@@ -52,6 +54,8 @@ class Grid extends \Magento\Backend\Block\Dashboard\Grid - } - - /** -+ * Prepare collection. -+ * - * @return $this - */ - protected function _prepareCollection() -@@ -110,6 +114,8 @@ class Grid extends \Magento\Backend\Block\Dashboard\Grid - } - - /** -+ * Prepare columns. -+ * - * @return $this - */ - protected function _prepareColumns() -@@ -129,7 +135,9 @@ class Grid extends \Magento\Backend\Block\Dashboard\Grid - ] - ); - -- $baseCurrencyCode = $this->_storeManager->getStore((int)$this->getParam('store'))->getBaseCurrencyCode(); -+ $baseCurrencyCode = $this->_storeManager->getStore( -+ (int)$this->getParam('store') -+ )->getBaseCurrencyCode(); - - $this->addColumn( - 'total', -@@ -149,7 +157,7 @@ class Grid extends \Magento\Backend\Block\Dashboard\Grid - } - - /** -- * {@inheritdoc} -+ * @inheritdoc - */ - public function getRowUrl($row) - { -diff --git a/app/code/Magento/Backend/Block/Dashboard/Sales.php b/app/code/Magento/Backend/Block/Dashboard/Sales.php -index 6d7a4d6458a..3455ff087a7 100644 ---- a/app/code/Magento/Backend/Block/Dashboard/Sales.php -+++ b/app/code/Magento/Backend/Block/Dashboard/Sales.php -@@ -18,20 +18,20 @@ class Sales extends \Magento\Backend\Block\Dashboard\Bar - protected $_template = 'Magento_Backend::dashboard/salebar.phtml'; - - /** -- * @var \Magento\Framework\Module\Manager -+ * @var \Magento\Framework\Module\ModuleManagerInterface - */ - protected $_moduleManager; - - /** - * @param \Magento\Backend\Block\Template\Context $context - * @param \Magento\Reports\Model\ResourceModel\Order\CollectionFactory $collectionFactory -- * @param \Magento\Framework\Module\Manager $moduleManager -+ * @param \Magento\Framework\Module\ModuleManagerInterface $moduleManager - * @param array $data - */ - public function __construct( - \Magento\Backend\Block\Template\Context $context, - \Magento\Reports\Model\ResourceModel\Order\CollectionFactory $collectionFactory, -- \Magento\Framework\Module\Manager $moduleManager, -+ \Magento\Framework\Module\ModuleManagerInterface $moduleManager, - array $data = [] - ) { - $this->_moduleManager = $moduleManager; -@@ -39,6 +39,8 @@ class Sales extends \Magento\Backend\Block\Dashboard\Bar - } - - /** -+ * Prepare layout. -+ * - * @return $this|void - */ - protected function _prepareLayout() -diff --git a/app/code/Magento/Backend/Block/Dashboard/Tab/Products/Ordered.php b/app/code/Magento/Backend/Block/Dashboard/Tab/Products/Ordered.php -index cac10ae3720..7dc897a62a3 100644 ---- a/app/code/Magento/Backend/Block/Dashboard/Tab/Products/Ordered.php -+++ b/app/code/Magento/Backend/Block/Dashboard/Tab/Products/Ordered.php -@@ -19,21 +19,21 @@ class Ordered extends \Magento\Backend\Block\Dashboard\Grid - protected $_collectionFactory; - - /** -- * @var \Magento\Framework\Module\Manager -+ * @var \Magento\Framework\Module\ModuleManagerInterface - */ - protected $_moduleManager; - - /** - * @param \Magento\Backend\Block\Template\Context $context - * @param \Magento\Backend\Helper\Data $backendHelper -- * @param \Magento\Framework\Module\Manager $moduleManager -+ * @param \Magento\Framework\Module\ModuleManagerInterface $moduleManager - * @param \Magento\Sales\Model\ResourceModel\Report\Bestsellers\CollectionFactory $collectionFactory - * @param array $data - */ - public function __construct( - \Magento\Backend\Block\Template\Context $context, - \Magento\Backend\Helper\Data $backendHelper, -- \Magento\Framework\Module\Manager $moduleManager, -+ \Magento\Framework\Module\ModuleManagerInterface $moduleManager, - \Magento\Sales\Model\ResourceModel\Report\Bestsellers\CollectionFactory $collectionFactory, - array $data = [] - ) { -@@ -43,6 +43,8 @@ class Ordered extends \Magento\Backend\Block\Dashboard\Grid - } - - /** -+ * Construct. -+ * - * @return void - */ - protected function _construct() -@@ -52,7 +54,7 @@ class Ordered extends \Magento\Backend\Block\Dashboard\Grid - } - - /** -- * {@inheritdoc} -+ * @inheritdoc - */ - protected function _prepareCollection() - { -@@ -81,7 +83,7 @@ class Ordered extends \Magento\Backend\Block\Dashboard\Grid - } - - /** -- * {@inheritdoc} -+ * @inheritdoc - */ - protected function _prepareColumns() - { -diff --git a/app/code/Magento/Backend/Block/Dashboard/Totals.php b/app/code/Magento/Backend/Block/Dashboard/Totals.php -index 4dcda367758..e57a6249af4 100644 ---- a/app/code/Magento/Backend/Block/Dashboard/Totals.php -+++ b/app/code/Magento/Backend/Block/Dashboard/Totals.php -@@ -11,6 +11,9 @@ - */ - namespace Magento\Backend\Block\Dashboard; - -+/** -+ * Totals block. -+ */ - class Totals extends \Magento\Backend\Block\Dashboard\Bar - { - /** -@@ -19,20 +22,20 @@ class Totals extends \Magento\Backend\Block\Dashboard\Bar - protected $_template = 'Magento_Backend::dashboard/totalbar.phtml'; - - /** -- * @var \Magento\Framework\Module\Manager -+ * @var \Magento\Framework\Module\ModuleManagerInterface - */ - protected $_moduleManager; - - /** - * @param \Magento\Backend\Block\Template\Context $context - * @param \Magento\Reports\Model\ResourceModel\Order\CollectionFactory $collectionFactory -- * @param \Magento\Framework\Module\Manager $moduleManager -+ * @param \Magento\Framework\Module\ModuleManagerInterface $moduleManager - * @param array $data - */ - public function __construct( - \Magento\Backend\Block\Template\Context $context, - \Magento\Reports\Model\ResourceModel\Order\CollectionFactory $collectionFactory, -- \Magento\Framework\Module\Manager $moduleManager, -+ \Magento\Framework\Module\ModuleManagerInterface $moduleManager, - array $data = [] - ) { - $this->_moduleManager = $moduleManager; -@@ -40,6 +43,7 @@ class Totals extends \Magento\Backend\Block\Dashboard\Bar - } - - /** -+ * @inheritDoc - * @return $this|void - */ - protected function _prepareLayout() -diff --git a/app/code/Magento/Backend/Block/DataProviders/ImageUploadConfig.php b/app/code/Magento/Backend/Block/DataProviders/ImageUploadConfig.php -new file mode 100644 -index 00000000000..9c17b0e5538 ---- /dev/null -+++ b/app/code/Magento/Backend/Block/DataProviders/ImageUploadConfig.php -@@ -0,0 +1,40 @@ -+imageUploadConfig = $imageUploadConfig; -+ } -+ -+ /** -+ * Get image resize configuration -+ * -+ * @return int -+ */ -+ public function getIsResizeEnabled(): int -+ { -+ return (int)$this->imageUploadConfig->isResizeEnabled(); -+ } -+} -diff --git a/app/code/Magento/Backend/Block/Media/Uploader.php b/app/code/Magento/Backend/Block/Media/Uploader.php -index 5bad74d8a8b..84fa487281a 100644 ---- a/app/code/Magento/Backend/Block/Media/Uploader.php -+++ b/app/code/Magento/Backend/Block/Media/Uploader.php -@@ -3,10 +3,14 @@ - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -+declare(strict_types=1); -+ - namespace Magento\Backend\Block\Media; - - use Magento\Framework\App\ObjectManager; - use Magento\Framework\Serialize\Serializer\Json; -+use Magento\Framework\Image\Adapter\UploadConfigInterface; -+use Magento\Backend\Model\Image\UploadResizeConfigInterface; - - /** - * Adminhtml media library uploader -@@ -35,24 +39,46 @@ class Uploader extends \Magento\Backend\Block\Widget - */ - private $jsonEncoder; - -+ /** -+ * @var UploadResizeConfigInterface -+ */ -+ private $imageUploadConfig; -+ -+ /** -+ * @var UploadConfigInterface -+ * @deprecated -+ * @see \Magento\Backend\Model\Image\UploadResizeConfigInterface -+ */ -+ private $imageConfig; -+ - /** - * @param \Magento\Backend\Block\Template\Context $context - * @param \Magento\Framework\File\Size $fileSize - * @param array $data - * @param Json $jsonEncoder -+ * @param UploadConfigInterface $imageConfig -+ * @param UploadResizeConfigInterface $imageUploadConfig - */ - public function __construct( - \Magento\Backend\Block\Template\Context $context, - \Magento\Framework\File\Size $fileSize, - array $data = [], -- Json $jsonEncoder = null -+ Json $jsonEncoder = null, -+ UploadConfigInterface $imageConfig = null, -+ UploadResizeConfigInterface $imageUploadConfig = null - ) { - $this->_fileSizeService = $fileSize; - $this->jsonEncoder = $jsonEncoder ?: ObjectManager::getInstance()->get(Json::class); -+ $this->imageConfig = $imageConfig -+ ?: ObjectManager::getInstance()->get(UploadConfigInterface::class); -+ $this->imageUploadConfig = $imageUploadConfig -+ ?: ObjectManager::getInstance()->get(UploadResizeConfigInterface::class); - parent::__construct($context, $data); - } - - /** -+ * Initialize block. -+ * - * @return void - */ - protected function _construct() -@@ -90,6 +116,26 @@ class Uploader extends \Magento\Backend\Block\Widget - return $this->_fileSizeService; - } - -+ /** -+ * Get Image Upload Maximum Width Config. -+ * -+ * @return int -+ */ -+ public function getImageUploadMaxWidth() -+ { -+ return $this->imageUploadConfig->getMaxWidth(); -+ } -+ -+ /** -+ * Get Image Upload Maximum Height Config. -+ * -+ * @return int -+ */ -+ public function getImageUploadMaxHeight() -+ { -+ return $this->imageUploadConfig->getMaxHeight(); -+ } -+ - /** - * Prepares layout and set element renderer - * -diff --git a/app/code/Magento/Backend/Block/Menu.php b/app/code/Magento/Backend/Block/Menu.php -index 7d86497288a..1e2561e2efe 100644 ---- a/app/code/Magento/Backend/Block/Menu.php -+++ b/app/code/Magento/Backend/Block/Menu.php -@@ -9,12 +9,12 @@ namespace Magento\Backend\Block; - /** - * Backend menu block - * -- * @api -- * @method \Magento\Backend\Block\Menu setAdditionalCacheKeyInfo(array $cacheKeyInfo) -+ * @method $this setAdditionalCacheKeyInfo(array $cacheKeyInfo) - * @method array getAdditionalCacheKeyInfo() -- * @SuppressWarnings(PHPMD.CouplingBetweenObjects) - * @api - * @since 100.0.2 -+ * -+ * @SuppressWarnings(PHPMD.CouplingBetweenObjects) - */ - class Menu extends \Magento\Backend\Block\Template - { -@@ -75,7 +75,12 @@ class Menu extends \Magento\Backend\Block\Template - private $anchorRenderer; - - /** -- * @param Template\Context $context -+ * @var \Magento\Framework\App\Route\ConfigInterface -+ */ -+ private $routeConfig; -+ -+ /** -+ * @param \Magento\Backend\Block\Template\Context $context - * @param \Magento\Backend\Model\UrlInterface $url - * @param \Magento\Backend\Model\Menu\Filter\IteratorFactory $iteratorFactory - * @param \Magento\Backend\Model\Auth\Session $authSession -@@ -84,6 +89,9 @@ class Menu extends \Magento\Backend\Block\Template - * @param array $data - * @param MenuItemChecker|null $menuItemChecker - * @param AnchorRenderer|null $anchorRenderer -+ * @param \Magento\Framework\App\Route\ConfigInterface|null $routeConfig -+ * -+ * @SuppressWarnings(PHPMD.ExcessiveParameterList) - */ - public function __construct( - \Magento\Backend\Block\Template\Context $context, -@@ -94,7 +102,8 @@ class Menu extends \Magento\Backend\Block\Template - \Magento\Framework\Locale\ResolverInterface $localeResolver, - array $data = [], - MenuItemChecker $menuItemChecker = null, -- AnchorRenderer $anchorRenderer = null -+ AnchorRenderer $anchorRenderer = null, -+ \Magento\Framework\App\Route\ConfigInterface $routeConfig = null - ) { - $this->_url = $url; - $this->_iteratorFactory = $iteratorFactory; -@@ -103,6 +112,9 @@ class Menu extends \Magento\Backend\Block\Template - $this->_localeResolver = $localeResolver; - $this->menuItemChecker = $menuItemChecker; - $this->anchorRenderer = $anchorRenderer; -+ $this->routeConfig = $routeConfig ?: -+ \Magento\Framework\App\ObjectManager::getInstance() -+ ->get(\Magento\Framework\App\Route\ConfigInterface::class); - parent::__construct($context, $data); - } - -@@ -130,6 +142,7 @@ class Menu extends \Magento\Backend\Block\Template - - /** - * Render menu item mouse events -+ * - * @param \Magento\Backend\Model\Menu\Item $menuItem - * @return string - */ -@@ -203,8 +216,9 @@ class Menu extends \Magento\Backend\Block\Template - */ - protected function _callbackSecretKey($match) - { -+ $routeId = $this->routeConfig->getRouteByFrontName($match[1]); - return \Magento\Backend\Model\UrlInterface::SECRET_KEY_PARAM_NAME . '/' . $this->_url->getSecretKey( -- $match[1], -+ $routeId ?: $match[1], - $match[2], - $match[3] - ); -@@ -342,7 +356,7 @@ class Menu extends \Magento\Backend\Block\Template - * @param \Magento\Backend\Model\Menu\Item $menuItem - * @param int $level - * @param int $limit -- * @param $id int -+ * @param int|null $id - * @return string HTML code - */ - protected function _addSubMenu($menuItem, $level, $limit, $id = null) -diff --git a/app/code/Magento/Backend/Block/Page/Footer.php b/app/code/Magento/Backend/Block/Page/Footer.php -index 3d1570e5ddf..e0c173a4cbf 100644 ---- a/app/code/Magento/Backend/Block/Page/Footer.php -+++ b/app/code/Magento/Backend/Block/Page/Footer.php -@@ -40,7 +40,7 @@ class Footer extends \Magento\Backend\Block\Template - } - - /** -- * @return void -+ * @inheritdoc - */ - protected function _construct() - { -@@ -57,4 +57,12 @@ class Footer extends \Magento\Backend\Block\Template - { - return $this->productMetadata->getVersion(); - } -+ -+ /** -+ * @inheritdoc -+ */ -+ protected function getCacheLifetime() -+ { -+ return 3600 * 24 * 10; -+ } - } -diff --git a/app/code/Magento/Backend/Block/Store/Switcher.php b/app/code/Magento/Backend/Block/Store/Switcher.php -index 1468df2b0b4..9c35cfb5df8 100644 ---- a/app/code/Magento/Backend/Block/Store/Switcher.php -+++ b/app/code/Magento/Backend/Block/Store/Switcher.php -@@ -17,7 +17,7 @@ class Switcher extends \Magento\Backend\Block\Template - /** - * URL for store switcher hint - */ -- const HINT_URL = 'http://docs.magento.com/m2/ce/user_guide/configuration/scope.html'; -+ const HINT_URL = 'https://docs.magento.com/m2/ce/user_guide/configuration/scope.html'; - - /** - * Name of website variable -@@ -86,6 +86,8 @@ class Switcher extends \Magento\Backend\Block\Template - protected $_storeFactory; - - /** -+ * Switcher constructor. -+ * - * @param \Magento\Backend\Block\Template\Context $context - * @param \Magento\Store\Model\WebsiteFactory $websiteFactory - * @param \Magento\Store\Model\GroupFactory $storeGroupFactory -@@ -106,7 +108,7 @@ class Switcher extends \Magento\Backend\Block\Template - } - - /** -- * @return void -+ * @inheritdoc - */ - protected function _construct() - { -@@ -130,6 +132,8 @@ class Switcher extends \Magento\Backend\Block\Template - } - - /** -+ * Get website collection. -+ * - * @return \Magento\Store\Model\ResourceModel\Website\Collection - */ - public function getWebsiteCollection() -@@ -169,6 +173,8 @@ class Switcher extends \Magento\Backend\Block\Template - } - - /** -+ * Set website variable name. -+ * - * @param string $varName - * @return $this - */ -@@ -179,6 +185,8 @@ class Switcher extends \Magento\Backend\Block\Template - } - - /** -+ * Get website variable name. -+ * - * @return string - */ - public function getWebsiteVarName() -@@ -191,6 +199,8 @@ class Switcher extends \Magento\Backend\Block\Template - } - - /** -+ * Check if current website selected. -+ * - * @param \Magento\Store\Model\Website $website - * @return bool - */ -@@ -200,6 +210,8 @@ class Switcher extends \Magento\Backend\Block\Template - } - - /** -+ * Return website Id. -+ * - * @return int|null - */ - public function getWebsiteId() -@@ -211,6 +223,8 @@ class Switcher extends \Magento\Backend\Block\Template - } - - /** -+ * Return group collection provided website. -+ * - * @param int|\Magento\Store\Model\Website $website - * @return \Magento\Store\Model\ResourceModel\Group\Collection - */ -@@ -247,6 +261,8 @@ class Switcher extends \Magento\Backend\Block\Template - } - - /** -+ * Sets store group variable name. -+ * - * @param string $varName - * @return $this - */ -@@ -257,6 +273,8 @@ class Switcher extends \Magento\Backend\Block\Template - } - - /** -+ * Return store group variable name. -+ * - * @return string - */ - public function getStoreGroupVarName() -@@ -269,6 +287,8 @@ class Switcher extends \Magento\Backend\Block\Template - } - - /** -+ * Is provided group selected. -+ * - * @param \Magento\Store\Model\Group $group - * @return bool - */ -@@ -278,6 +298,8 @@ class Switcher extends \Magento\Backend\Block\Template - } - - /** -+ * Return store group Id. -+ * - * @return int|null - */ - public function getStoreGroupId() -@@ -289,6 +311,8 @@ class Switcher extends \Magento\Backend\Block\Template - } - - /** -+ * Return store collection. -+ * - * @param \Magento\Store\Model\Group|int $group - * @return \Magento\Store\Model\ResourceModel\Store\Collection - */ -@@ -328,6 +352,8 @@ class Switcher extends \Magento\Backend\Block\Template - } - - /** -+ * Return store Id. -+ * - * @return int|null - */ - public function getStoreId() -@@ -339,6 +365,8 @@ class Switcher extends \Magento\Backend\Block\Template - } - - /** -+ * Check is provided store selected. -+ * - * @param \Magento\Store\Model\Store $store - * @return bool - */ -@@ -358,6 +386,8 @@ class Switcher extends \Magento\Backend\Block\Template - } - - /** -+ * Sets store variable name. -+ * - * @param string $varName - * @return $this - */ -@@ -368,6 +398,8 @@ class Switcher extends \Magento\Backend\Block\Template - } - - /** -+ * Return store variable name. -+ * - * @return mixed|string - */ - public function getStoreVarName() -@@ -380,6 +412,8 @@ class Switcher extends \Magento\Backend\Block\Template - } - - /** -+ * Return switch url. -+ * - * @return string - */ - public function getSwitchUrl() -@@ -399,6 +433,8 @@ class Switcher extends \Magento\Backend\Block\Template - } - - /** -+ * Checks if scope selected. -+ * - * @return bool - */ - public function hasScopeSelected() -@@ -472,6 +508,8 @@ class Switcher extends \Magento\Backend\Block\Template - } - - /** -+ * Sets store ids. -+ * - * @param array $storeIds - * @return $this - */ -@@ -482,6 +520,8 @@ class Switcher extends \Magento\Backend\Block\Template - } - - /** -+ * Return store ids. -+ * - * @return array - */ - public function getStoreIds() -@@ -490,6 +530,8 @@ class Switcher extends \Magento\Backend\Block\Template - } - - /** -+ * Check if system is run in the single store mode. -+ * - * @return bool - */ - public function isShow() -@@ -498,6 +540,8 @@ class Switcher extends \Magento\Backend\Block\Template - } - - /** -+ * Render block. -+ * - * @return string - */ - protected function _toHtml() -diff --git a/app/code/Magento/Backend/Block/System/Design/Edit/Tab/General.php b/app/code/Magento/Backend/Block/System/Design/Edit/Tab/General.php -index 5a09e1f17f6..6004d08b4b7 100644 ---- a/app/code/Magento/Backend/Block/System/Design/Edit/Tab/General.php -+++ b/app/code/Magento/Backend/Block/System/Design/Edit/Tab/General.php -@@ -6,6 +6,9 @@ - - namespace Magento\Backend\Block\System\Design\Edit\Tab; - -+/** -+ * General system tab block. -+ */ - class General extends \Magento\Backend\Block\Widget\Form\Generic - { - /** -@@ -90,7 +93,7 @@ class General extends \Magento\Backend\Block\Widget\Form\Generic - ] - ); - -- $dateFormat = $this->_localeDate->getDateFormat(\IntlDateFormatter::SHORT); -+ $dateFormat = $this->_localeDate->getDateFormatWithLongYear(); - $fieldset->addField( - 'date_from', - 'date', -diff --git a/app/code/Magento/Backend/Block/System/Store/Delete/Form.php b/app/code/Magento/Backend/Block/System/Store/Delete/Form.php -index e479e8f560d..47a156c16ce 100644 ---- a/app/code/Magento/Backend/Block/System/Store/Delete/Form.php -+++ b/app/code/Magento/Backend/Block/System/Store/Delete/Form.php -@@ -5,6 +5,9 @@ - */ - namespace Magento\Backend\Block\System\Store\Delete; - -+use Magento\Backup\Helper\Data as BackupHelper; -+use Magento\Framework\App\ObjectManager; -+ - /** - * Adminhtml cms block edit form - * -@@ -12,6 +15,25 @@ namespace Magento\Backend\Block\System\Store\Delete; - */ - class Form extends \Magento\Backend\Block\Widget\Form\Generic - { -+ /** -+ * @var BackupHelper -+ */ -+ private $backup; -+ -+ /** -+ * @inheritDoc -+ */ -+ public function __construct( -+ \Magento\Backend\Block\Template\Context $context, -+ \Magento\Framework\Registry $registry, -+ \Magento\Framework\Data\FormFactory $formFactory, -+ array $data = [], -+ ?BackupHelper $backup = null -+ ) { -+ parent::__construct($context, $registry, $formFactory, $data); -+ $this->backup = $backup ?? ObjectManager::getInstance()->get(BackupHelper::class); -+ } -+ - /** - * Init form - * -@@ -25,7 +47,7 @@ class Form extends \Magento\Backend\Block\Widget\Form\Generic - } - - /** -- * {@inheritdoc} -+ * @inheritDoc - */ - protected function _prepareForm() - { -@@ -45,6 +67,12 @@ class Form extends \Magento\Backend\Block\Widget\Form\Generic - - $fieldset->addField('item_id', 'hidden', ['name' => 'item_id', 'value' => $dataObject->getId()]); - -+ $backupOptions = ['0' => __('No')]; -+ $backupSelected = '0'; -+ if ($this->backup->isEnabled()) { -+ $backupOptions['1'] = __('Yes'); -+ $backupSelected = '1'; -+ } - $fieldset->addField( - 'create_backup', - 'select', -@@ -52,8 +80,8 @@ class Form extends \Magento\Backend\Block\Widget\Form\Generic - 'label' => __('Create DB Backup'), - 'title' => __('Create DB Backup'), - 'name' => 'create_backup', -- 'options' => ['1' => __('Yes'), '0' => __('No')], -- 'value' => '1' -+ 'options' => $backupOptions, -+ 'value' => $backupSelected - ] - ); - -diff --git a/app/code/Magento/Backend/Block/Template/Context.php b/app/code/Magento/Backend/Block/Template/Context.php -index 6efc8d86802..27c777c6d40 100644 ---- a/app/code/Magento/Backend/Block/Template/Context.php -+++ b/app/code/Magento/Backend/Block/Template/Context.php -@@ -17,7 +17,9 @@ namespace Magento\Backend\Block\Template; - * the classes they were introduced for. - * - * @api -+ * @SuppressWarnings(PHPMD.CookieAndSessionMisuse) - * @SuppressWarnings(PHPMD.CouplingBetweenObjects) -+ * @SuppressWarnings(PHPMD.CookieAndSessionMisuse) - * @since 100.0.2 - */ - class Context extends \Magento\Framework\View\Element\Template\Context -@@ -173,6 +175,8 @@ class Context extends \Magento\Framework\View\Element\Template\Context - } - - /** -+ * Get backend session instance. -+ * - * @return \Magento\Backend\Model\Session - */ - public function getBackendSession() -@@ -181,6 +185,8 @@ class Context extends \Magento\Framework\View\Element\Template\Context - } - - /** -+ * Get math random instance. -+ * - * @return \Magento\Framework\Math\Random - */ - public function getMathRandom() -@@ -189,6 +195,8 @@ class Context extends \Magento\Framework\View\Element\Template\Context - } - - /** -+ * Get form key instance. -+ * - * @return \Magento\Framework\Data\Form\FormKey - */ - public function getFormKey() -@@ -197,7 +205,9 @@ class Context extends \Magento\Framework\View\Element\Template\Context - } - - /** -- * @return \Magento\Framework\Data\Form\FormKey -+ * Get name builder instance. -+ * -+ * @return \Magento\Framework\Code\NameBuilder - */ - public function getNameBuilder() - { -diff --git a/app/code/Magento/Backend/Block/Widget/Button/ButtonList.php b/app/code/Magento/Backend/Block/Widget/Button/ButtonList.php -index 94af9a1d757..5a792ddb391 100644 ---- a/app/code/Magento/Backend/Block/Widget/Button/ButtonList.php -+++ b/app/code/Magento/Backend/Block/Widget/Button/ButtonList.php -@@ -7,6 +7,8 @@ - namespace Magento\Backend\Block\Widget\Button; - - /** -+ * Button list widget -+ * - * @api - * @since 100.0.2 - */ -@@ -127,12 +129,6 @@ class ButtonList - */ - public function sortButtons(Item $itemA, Item $itemB) - { -- $sortOrderA = intval($itemA->getSortOrder()); -- $sortOrderB = intval($itemB->getSortOrder()); -- -- if ($sortOrderA == $sortOrderB) { -- return 0; -- } -- return ($sortOrderA < $sortOrderB) ? -1 : 1; -+ return (int)$itemA->getSortOrder() <=> (int)$itemB->getSortOrder(); - } - } -diff --git a/app/code/Magento/Backend/Block/Widget/Form.php b/app/code/Magento/Backend/Block/Widget/Form.php -index 30221618edb..38d5d90a22d 100644 ---- a/app/code/Magento/Backend/Block/Widget/Form.php -+++ b/app/code/Magento/Backend/Block/Widget/Form.php -@@ -3,8 +3,11 @@ - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -+ - namespace Magento\Backend\Block\Widget; - -+use Magento\Framework\App\ObjectManager; -+ - /** - * Backend form widget - * -@@ -27,13 +30,23 @@ class Form extends \Magento\Backend\Block\Widget - */ - protected $_template = 'Magento_Backend::widget/form.phtml'; - -+ /** @var Form\Element\ElementCreator */ -+ private $creator; -+ - /** -+ * Constructs form -+ * - * @param \Magento\Backend\Block\Template\Context $context - * @param array $data -+ * @param Form\Element\ElementCreator|null $creator - */ -- public function __construct(\Magento\Backend\Block\Template\Context $context, array $data = []) -- { -+ public function __construct( -+ \Magento\Backend\Block\Template\Context $context, -+ array $data = [], -+ Form\Element\ElementCreator $creator = null -+ ) { - parent::__construct($context, $data); -+ $this->creator = $creator ?: ObjectManager::getInstance()->get(Form\Element\ElementCreator::class); - } - - /** -@@ -46,7 +59,6 @@ class Form extends \Magento\Backend\Block\Widget - parent::_construct(); - - $this->setDestElementId('edit_form'); -- $this->setShowGlobalIcon(false); - } - - /** -@@ -148,6 +160,7 @@ class Form extends \Magento\Backend\Block\Widget - - /** - * Initialize form fields values -+ * - * Method will be called after prepareForm and can be used for field values initialization - * - * @return $this -@@ -173,32 +186,11 @@ class Form extends \Magento\Backend\Block\Widget - if (!$this->_isAttributeVisible($attribute)) { - continue; - } -- if (($inputType = $attribute->getFrontend()->getInputType()) && !in_array( -- $attribute->getAttributeCode(), -- $exclude -- ) && ('media_image' != $inputType || $attribute->getAttributeCode() == 'image') -+ if (($inputType = $attribute->getFrontend()->getInputType()) -+ && !in_array($attribute->getAttributeCode(), $exclude) -+ && ('media_image' !== $inputType || $attribute->getAttributeCode() == 'image') - ) { -- $fieldType = $inputType; -- $rendererClass = $attribute->getFrontend()->getInputRendererClass(); -- if (!empty($rendererClass)) { -- $fieldType = $inputType . '_' . $attribute->getAttributeCode(); -- $fieldset->addType($fieldType, $rendererClass); -- } -- -- $element = $fieldset->addField( -- $attribute->getAttributeCode(), -- $fieldType, -- [ -- 'name' => $attribute->getAttributeCode(), -- 'label' => $attribute->getFrontend()->getLocalizedLabel(), -- 'class' => $attribute->getFrontend()->getClass(), -- 'required' => $attribute->getIsRequired(), -- 'note' => $attribute->getNote() -- ] -- )->setEntityAttribute( -- $attribute -- ); -- -+ $element = $this->creator->create($fieldset, $attribute); - $element->setAfterElementHtml($this->_getAdditionalElementHtml($element)); - - $this->_applyTypeSpecificConfig($inputType, $element, $attribute); -diff --git a/app/code/Magento/Backend/Block/Widget/Form/Container.php b/app/code/Magento/Backend/Block/Widget/Form/Container.php -index 8b7babc1bb9..febaae38616 100644 ---- a/app/code/Magento/Backend/Block/Widget/Form/Container.php -+++ b/app/code/Magento/Backend/Block/Widget/Form/Container.php -@@ -56,6 +56,8 @@ class Container extends \Magento\Backend\Block\Widget\Container - protected $_template = 'Magento_Backend::widget/form/container.phtml'; - - /** -+ * Initialize form. -+ * - * @return void - */ - protected function _construct() -@@ -83,7 +85,7 @@ class Container extends \Magento\Backend\Block\Widget\Container - -1 - ); - -- $objId = $this->getRequest()->getParam($this->_objectId); -+ $objId = (int)$this->getRequest()->getParam($this->_objectId); - - if (!empty($objId)) { - $this->addButton( -@@ -93,7 +95,7 @@ class Container extends \Magento\Backend\Block\Widget\Container - 'class' => 'delete', - 'onclick' => 'deleteConfirm(\'' . __( - 'Are you sure you want to do this?' -- ) . '\', \'' . $this->getDeleteUrl() . '\')' -+ ) . '\', \'' . $this->getDeleteUrl() . '\', {data: {}})' - ] - ); - } -@@ -151,11 +153,13 @@ class Container extends \Magento\Backend\Block\Widget\Container - } - - /** -+ * Get URL for delete button. -+ * - * @return string - */ - public function getDeleteUrl() - { -- return $this->getUrl('*/*/delete', [$this->_objectId => $this->getRequest()->getParam($this->_objectId)]); -+ return $this->getUrl('*/*/delete', [$this->_objectId => (int)$this->getRequest()->getParam($this->_objectId)]); - } - - /** -@@ -183,6 +187,8 @@ class Container extends \Magento\Backend\Block\Widget\Container - } - - /** -+ * Get form HTML. -+ * - * @return string - */ - public function getFormHtml() -@@ -192,6 +198,8 @@ class Container extends \Magento\Backend\Block\Widget\Container - } - - /** -+ * Get form init scripts. -+ * - * @return string - */ - public function getFormInitScripts() -@@ -203,6 +211,8 @@ class Container extends \Magento\Backend\Block\Widget\Container - } - - /** -+ * Get form scripts. -+ * - * @return string - */ - public function getFormScripts() -@@ -214,6 +224,8 @@ class Container extends \Magento\Backend\Block\Widget\Container - } - - /** -+ * Get header width. -+ * - * @return string - */ - public function getHeaderWidth() -@@ -222,6 +234,8 @@ class Container extends \Magento\Backend\Block\Widget\Container - } - - /** -+ * Get header css class. -+ * - * @return string - */ - public function getHeaderCssClass() -@@ -230,6 +244,8 @@ class Container extends \Magento\Backend\Block\Widget\Container - } - - /** -+ * Get header HTML. -+ * - * @return string - */ - public function getHeaderHtml() -diff --git a/app/code/Magento/Backend/Block/Widget/Form/Element/Dependence.php b/app/code/Magento/Backend/Block/Widget/Form/Element/Dependence.php -index eff49c3b75a..d599d5fbad5 100644 ---- a/app/code/Magento/Backend/Block/Widget/Form/Element/Dependence.php -+++ b/app/code/Magento/Backend/Block/Widget/Form/Element/Dependence.php -@@ -4,14 +4,13 @@ - * See COPYING.txt for license details. - */ - -+namespace Magento\Backend\Block\Widget\Form\Element; -+ - /** - * Form element dependencies mapper - * Assumes that one element may depend on other element values. - * Will toggle as "enabled" only if all elements it depends from toggle as true. -- */ --namespace Magento\Backend\Block\Widget\Form\Element; -- --/** -+ * - * @api - * @since 100.0.2 - */ -@@ -117,6 +116,7 @@ class Dependence extends \Magento\Backend\Block\AbstractBlock - - /** - * HTML output getter -+ * - * @return string - */ - protected function _toHtml() -@@ -139,7 +139,8 @@ require(['mage/adminhtml/form'], function(){ - } - - /** -- * Field dependences JSON map generator -+ * Field dependencies JSON map generator -+ * - * @return string - */ - protected function _getDependsJson() -diff --git a/app/code/Magento/Backend/Block/Widget/Form/Element/ElementCreator.php b/app/code/Magento/Backend/Block/Widget/Form/Element/ElementCreator.php -new file mode 100644 -index 00000000000..b9cdd259796 ---- /dev/null -+++ b/app/code/Magento/Backend/Block/Widget/Form/Element/ElementCreator.php -@@ -0,0 +1,136 @@ -+modifiers = $modifiers; -+ } -+ -+ /** -+ * Creates element -+ * -+ * @param Fieldset $fieldset -+ * @param Attribute $attribute -+ * -+ * @return AbstractElement -+ */ -+ public function create(Fieldset $fieldset, Attribute $attribute): AbstractElement -+ { -+ $config = $this->getElementConfig($attribute); -+ -+ if (!empty($config['rendererClass'])) { -+ $fieldType = $config['inputType'] . '_' . $attribute->getAttributeCode(); -+ $fieldset->addType($fieldType, $config['rendererClass']); -+ } -+ -+ return $fieldset -+ ->addField($config['attribute_code'], $config['inputType'], $config) -+ ->setEntityAttribute($attribute); -+ } -+ -+ /** -+ * Returns element config -+ * -+ * @param Attribute $attribute -+ * @return array -+ */ -+ private function getElementConfig(Attribute $attribute): array -+ { -+ $defaultConfig = $this->createDefaultConfig($attribute); -+ $config = $this->modifyConfig($defaultConfig); -+ -+ $config['label'] = __($config['label']); -+ -+ return $config; -+ } -+ -+ /** -+ * Returns default config -+ * -+ * @param Attribute $attribute -+ * @return array -+ */ -+ private function createDefaultConfig(Attribute $attribute): array -+ { -+ return [ -+ 'inputType' => $attribute->getFrontend()->getInputType(), -+ 'rendererClass' => $attribute->getFrontend()->getInputRendererClass(), -+ 'attribute_code' => $attribute->getAttributeCode(), -+ 'name' => $attribute->getAttributeCode(), -+ 'label' => $attribute->getFrontend()->getLabel(), -+ 'class' => $attribute->getFrontend()->getClass(), -+ 'required' => $attribute->getIsRequired(), -+ 'note' => $attribute->getNote(), -+ ]; -+ } -+ -+ /** -+ * Modify config -+ * -+ * @param array $config -+ * @return array -+ */ -+ private function modifyConfig(array $config): array -+ { -+ if ($this->isModified($config['attribute_code'])) { -+ return $this->applyModifier($config); -+ } -+ return $config; -+ } -+ -+ /** -+ * Returns bool if attribute need to modify -+ * -+ * @param string $attribute_code -+ * @return bool -+ */ -+ private function isModified($attribute_code): bool -+ { -+ return isset($this->modifiers[$attribute_code]); -+ } -+ -+ /** -+ * Apply modifier to config -+ * -+ * @param array $config -+ * @return array -+ */ -+ private function applyModifier(array $config): array -+ { -+ $modifiedConfig = $this->modifiers[$config['attribute_code']]; -+ foreach (array_keys($config) as $key) { -+ if (isset($modifiedConfig[$key])) { -+ $config[$key] = $modifiedConfig[$key]; -+ } -+ } -+ return $config; -+ } -+} -diff --git a/app/code/Magento/Backend/Block/Widget/Grid.php b/app/code/Magento/Backend/Block/Widget/Grid.php -index 72ab5a265d8..66298d23389 100644 ---- a/app/code/Magento/Backend/Block/Widget/Grid.php -+++ b/app/code/Magento/Backend/Block/Widget/Grid.php -@@ -12,7 +12,7 @@ namespace Magento\Backend\Block\Widget; - * @api - * @deprecated 100.2.0 in favour of UI component implementation - * @method string getRowClickCallback() getRowClickCallback() -- * @method \Magento\Backend\Block\Widget\Grid setRowClickCallback() setRowClickCallback(string $value) -+ * @method \Magento\Backend\Block\Widget\Grid setRowClickCallback(string $value) - * @SuppressWarnings(PHPMD.TooManyFields) - * @since 100.0.2 - */ -@@ -150,7 +150,10 @@ class Grid extends \Magento\Backend\Block\Widget - } - - /** -+ * Internal constructor, that is called from real constructor -+ * - * @return void -+ * - * @SuppressWarnings(PHPMD.NPathComplexity) - */ - protected function _construct() -@@ -709,6 +712,7 @@ class Grid extends \Magento\Backend\Block\Widget - - /** - * Grid url getter -+ * - * Version of getGridUrl() but with parameters - * - * @param array $params url parameters -diff --git a/app/code/Magento/Backend/Block/Widget/Grid/Column/Filter/Theme.php b/app/code/Magento/Backend/Block/Widget/Grid/Column/Filter/Theme.php -index d49ad294114..a0907726ccc 100644 ---- a/app/code/Magento/Backend/Block/Widget/Grid/Column/Filter/Theme.php -+++ b/app/code/Magento/Backend/Block/Widget/Grid/Column/Filter/Theme.php -@@ -9,6 +9,9 @@ - */ - namespace Magento\Backend\Block\Widget\Grid\Column\Filter; - -+/** -+ * Theme grid filter -+ */ - class Theme extends \Magento\Backend\Block\Widget\Grid\Column\Filter\AbstractFilter - { - /** -@@ -54,7 +57,8 @@ class Theme extends \Magento\Backend\Block\Widget\Grid\Column\Filter\AbstractFil - } - - /** -- * Retrieve options setted in column. -+ * Retrieve options set in column. -+ * - * Or load if options was not set. - * - * @return array -diff --git a/app/code/Magento/Backend/Block/Widget/Grid/Column/Multistore.php b/app/code/Magento/Backend/Block/Widget/Grid/Column/Multistore.php -index c45a222d1eb..424fee98d62 100644 ---- a/app/code/Magento/Backend/Block/Widget/Grid/Column/Multistore.php -+++ b/app/code/Magento/Backend/Block/Widget/Grid/Column/Multistore.php -@@ -3,6 +3,8 @@ - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -+declare(strict_types=1); -+ - namespace Magento\Backend\Block\Widget\Grid\Column; - - /** -@@ -14,15 +16,6 @@ namespace Magento\Backend\Block\Widget\Grid\Column; - */ - class Multistore extends \Magento\Backend\Block\Widget\Grid\Column - { -- /** -- * @param \Magento\Backend\Block\Template\Context $context -- * @param array $data -- */ -- public function __construct(\Magento\Backend\Block\Template\Context $context, array $data = []) -- { -- parent::__construct($context, $data); -- } -- - /** - * Get header css class name - * -diff --git a/app/code/Magento/Backend/Block/Widget/Grid/Column/Renderer/AbstractRenderer.php b/app/code/Magento/Backend/Block/Widget/Grid/Column/Renderer/AbstractRenderer.php -index b8a2e283b29..623a75015eb 100644 ---- a/app/code/Magento/Backend/Block/Widget/Grid/Column/Renderer/AbstractRenderer.php -+++ b/app/code/Magento/Backend/Block/Widget/Grid/Column/Renderer/AbstractRenderer.php -@@ -28,6 +28,8 @@ abstract class AbstractRenderer extends \Magento\Backend\Block\AbstractBlock imp - protected $_column; - - /** -+ * Set column for renderer. -+ * - * @param Column $column - * @return $this - */ -@@ -38,6 +40,8 @@ abstract class AbstractRenderer extends \Magento\Backend\Block\AbstractBlock imp - } - - /** -+ * Returns row associated with the renderer. -+ * - * @return Column - */ - public function getColumn() -@@ -48,7 +52,7 @@ abstract class AbstractRenderer extends \Magento\Backend\Block\AbstractBlock imp - /** - * Renders grid column - * -- * @param Object $row -+ * @param DataObject $row - * @return string - */ - public function render(DataObject $row) -@@ -66,7 +70,7 @@ abstract class AbstractRenderer extends \Magento\Backend\Block\AbstractBlock imp - /** - * Render column for export - * -- * @param Object $row -+ * @param DataObject $row - * @return string - */ - public function renderExport(DataObject $row) -@@ -75,7 +79,9 @@ abstract class AbstractRenderer extends \Magento\Backend\Block\AbstractBlock imp - } - - /** -- * @param Object $row -+ * Returns value of the row. -+ * -+ * @param DataObject $row - * @return mixed - */ - protected function _getValue(DataObject $row) -@@ -92,7 +98,9 @@ abstract class AbstractRenderer extends \Magento\Backend\Block\AbstractBlock imp - } - - /** -- * @param Object $row -+ * Get pre-rendered input element. -+ * -+ * @param DataObject $row - * @return string - */ - public function _getInputValueElement(DataObject $row) -@@ -108,7 +116,9 @@ abstract class AbstractRenderer extends \Magento\Backend\Block\AbstractBlock imp - } - - /** -- * @param Object $row -+ * Get input value by row. -+ * -+ * @param DataObject $row - * @return mixed - */ - protected function _getInputValue(DataObject $row) -@@ -117,6 +127,8 @@ abstract class AbstractRenderer extends \Magento\Backend\Block\AbstractBlock imp - } - - /** -+ * Renders header of the column, -+ * - * @return string - */ - public function renderHeader() -@@ -148,6 +160,8 @@ abstract class AbstractRenderer extends \Magento\Backend\Block\AbstractBlock imp - } - - /** -+ * Render HTML properties. -+ * - * @return string - */ - public function renderProperty() -@@ -172,6 +186,8 @@ abstract class AbstractRenderer extends \Magento\Backend\Block\AbstractBlock imp - } - - /** -+ * Returns HTML for CSS. -+ * - * @return string - */ - public function renderCss() -diff --git a/app/code/Magento/Backend/Block/Widget/Grid/Column/Renderer/Currency.php b/app/code/Magento/Backend/Block/Widget/Grid/Column/Renderer/Currency.php -index b3f467ce37c..03566bce3fc 100644 ---- a/app/code/Magento/Backend/Block/Widget/Grid/Column/Renderer/Currency.php -+++ b/app/code/Magento/Backend/Block/Widget/Grid/Column/Renderer/Currency.php -@@ -82,7 +82,7 @@ class Currency extends \Magento\Backend\Block\Widget\Grid\Column\Renderer\Abstra - { - if ($data = (string)$this->_getValue($row)) { - $currency_code = $this->_getCurrencyCode($row); -- $data = floatval($data) * $this->_getRate($row); -+ $data = (float)$data * $this->_getRate($row); - $sign = (bool)(int)$this->getColumn()->getShowNumberSign() && $data > 0 ? '+' : ''; - $data = sprintf("%f", $data); - $data = $this->_localeCurrency->getCurrency($currency_code)->toCurrency($data); -@@ -118,10 +118,10 @@ class Currency extends \Magento\Backend\Block\Widget\Grid\Column\Renderer\Abstra - protected function _getRate($row) - { - if ($rate = $this->getColumn()->getRate()) { -- return floatval($rate); -+ return (float)$rate; - } - if ($rate = $row->getData($this->getColumn()->getRateField())) { -- return floatval($rate); -+ return (float)$rate; - } - return $this->_defaultBaseCurrency->getRate($this->_getCurrencyCode($row)); - } -diff --git a/app/code/Magento/Backend/Block/Widget/Grid/Column/Renderer/Price.php b/app/code/Magento/Backend/Block/Widget/Grid/Column/Renderer/Price.php -index e4300c63485..9da23af83f0 100644 ---- a/app/code/Magento/Backend/Block/Widget/Grid/Column/Renderer/Price.php -+++ b/app/code/Magento/Backend/Block/Widget/Grid/Column/Renderer/Price.php -@@ -60,7 +60,7 @@ class Price extends \Magento\Backend\Block\Widget\Grid\Column\Renderer\AbstractR - return $data; - } - -- $data = floatval($data) * $this->_getRate($row); -+ $data = (float)$data * $this->_getRate($row); - $data = sprintf("%f", $data); - $data = $this->_localeCurrency->getCurrency($currencyCode)->toCurrency($data); - return $data; -@@ -94,10 +94,10 @@ class Price extends \Magento\Backend\Block\Widget\Grid\Column\Renderer\AbstractR - protected function _getRate($row) - { - if ($rate = $this->getColumn()->getRate()) { -- return floatval($rate); -+ return (float)$rate; - } - if ($rate = $row->getData($this->getColumn()->getRateField())) { -- return floatval($rate); -+ return (float)$rate; - } - return 1; - } -diff --git a/app/code/Magento/Backend/Block/Widget/Grid/Massaction/AbstractMassaction.php b/app/code/Magento/Backend/Block/Widget/Grid/Massaction/AbstractMassaction.php -index 185b1116b8f..891b2a3ada7 100644 ---- a/app/code/Magento/Backend/Block/Widget/Grid/Massaction/AbstractMassaction.php -+++ b/app/code/Magento/Backend/Block/Widget/Grid/Massaction/AbstractMassaction.php -@@ -7,6 +7,7 @@ - namespace Magento\Backend\Block\Widget\Grid\Massaction; - - use Magento\Backend\Block\Widget\Grid\Massaction\VisibilityCheckerInterface as VisibilityChecker; -+use Magento\Framework\Data\Collection\AbstractDb; - use Magento\Framework\DataObject; - - /** -@@ -52,7 +53,7 @@ abstract class AbstractMassaction extends \Magento\Backend\Block\Widget - } - - /** -- * @return void -+ * @inheritdoc - */ - protected function _construct() - { -@@ -217,6 +218,7 @@ abstract class AbstractMassaction extends \Magento\Backend\Block\Widget - * Retrieve JSON string of selected checkboxes - * - * @return string -+ * @SuppressWarnings(PHPMD.RequestAwareBlockMethod) - */ - public function getSelectedJson() - { -@@ -231,6 +233,7 @@ abstract class AbstractMassaction extends \Magento\Backend\Block\Widget - * Retrieve array of selected checkboxes - * - * @return string[] -+ * @SuppressWarnings(PHPMD.RequestAwareBlockMethod) - */ - public function getSelected() - { -@@ -252,6 +255,8 @@ abstract class AbstractMassaction extends \Magento\Backend\Block\Widget - } - - /** -+ * Get mass action javascript code. -+ * - * @return string - */ - public function getJavaScript() -@@ -268,6 +273,8 @@ abstract class AbstractMassaction extends \Magento\Backend\Block\Widget - } - - /** -+ * Get grid ids in JSON format. -+ * - * @return string - */ - public function getGridIdsJson() -@@ -275,23 +282,28 @@ abstract class AbstractMassaction extends \Magento\Backend\Block\Widget - if (!$this->getUseSelectAll()) { - return ''; - } -- /** @var \Magento\Framework\Data\Collection $allIdsCollection */ -- $allIdsCollection = clone $this->getParentBlock()->getCollection(); - -- if ($this->getMassactionIdField()) { -- $massActionIdField = $this->getMassactionIdField(); -+ /** @var \Magento\Framework\Model\ResourceModel\Db\Collection\AbstractCollection $collection */ -+ $collection = clone $this->getParentBlock()->getCollection(); -+ -+ if ($collection instanceof AbstractDb) { -+ $idsSelect = clone $collection->getSelect(); -+ $idsSelect->reset(\Magento\Framework\DB\Select::ORDER); -+ $idsSelect->reset(\Magento\Framework\DB\Select::LIMIT_COUNT); -+ $idsSelect->reset(\Magento\Framework\DB\Select::LIMIT_OFFSET); -+ $idsSelect->reset(\Magento\Framework\DB\Select::COLUMNS); -+ $idsSelect->columns($this->getMassactionIdField(), 'main_table'); -+ $idList = $collection->getConnection()->fetchCol($idsSelect); - } else { -- $massActionIdField = $this->getParentBlock()->getMassactionIdField(); -+ $idList = $collection->setPageSize(0)->getColumnValues($this->getMassactionIdField()); - } - -- $gridIds = $allIdsCollection->setPageSize(0)->getColumnValues($massActionIdField); -- if (!empty($gridIds)) { -- return join(",", $gridIds); -- } -- return ''; -+ return implode(',', $idList); - } - - /** -+ * Get Html id. -+ * - * @return string - */ - public function getHtmlId() -diff --git a/app/code/Magento/Backend/Block/Widget/Tabs.php b/app/code/Magento/Backend/Block/Widget/Tabs.php -index 333904e398c..c7c1f93e8ca 100644 ---- a/app/code/Magento/Backend/Block/Widget/Tabs.php -+++ b/app/code/Magento/Backend/Block/Widget/Tabs.php -@@ -8,6 +8,8 @@ namespace Magento\Backend\Block\Widget; - use Magento\Backend\Block\Widget\Tab\TabInterface; - - /** -+ * Tabs widget -+ * - * @api - * @SuppressWarnings(PHPMD.NumberOfChildren) - * @since 100.0.2 -@@ -178,6 +180,8 @@ class Tabs extends \Magento\Backend\Block\Widget - } - - /** -+ * Get active tab id -+ * - * @return string - */ - public function getActiveTabId() -@@ -187,6 +191,7 @@ class Tabs extends \Magento\Backend\Block\Widget - - /** - * Set Active Tab -+ * - * Tab has to be not hidden and can show - * - * @param string $tabId -@@ -231,7 +236,7 @@ class Tabs extends \Magento\Backend\Block\Widget - } - - /** -- * {@inheritdoc} -+ * @inheritdoc - */ - protected function _beforeToHtml() - { -@@ -282,6 +287,8 @@ class Tabs extends \Magento\Backend\Block\Widget - } - - /** -+ * Apply tabs order -+ * - * @param array $orderByPosition - * @param array $orderByIdentity - * -@@ -294,7 +301,7 @@ class Tabs extends \Magento\Backend\Block\Widget - /** - * Rearrange the positions by using the after tag for each tab. - * -- * @var integer $position -+ * @var int $position - * @var TabInterface $tab - */ - foreach ($orderByPosition as $position => $tab) { -@@ -338,6 +345,8 @@ class Tabs extends \Magento\Backend\Block\Widget - } - - /** -+ * Get js object name -+ * - * @return string - */ - public function getJsObjectName() -@@ -346,6 +355,8 @@ class Tabs extends \Magento\Backend\Block\Widget - } - - /** -+ * Get tabs ids -+ * - * @return string[] - */ - public function getTabsIds() -@@ -358,6 +369,8 @@ class Tabs extends \Magento\Backend\Block\Widget - } - - /** -+ * Get tab id -+ * - * @param \Magento\Framework\DataObject|TabInterface $tab - * @param bool $withPrefix - * @return string -@@ -371,6 +384,8 @@ class Tabs extends \Magento\Backend\Block\Widget - } - - /** -+ * CVan show tab -+ * - * @param \Magento\Framework\DataObject|TabInterface $tab - * @return bool - */ -@@ -383,6 +398,8 @@ class Tabs extends \Magento\Backend\Block\Widget - } - - /** -+ * Get tab is hidden -+ * - * @param \Magento\Framework\DataObject|TabInterface $tab - * @return bool - * @SuppressWarnings(PHPMD.BooleanGetMethodName) -@@ -396,6 +413,8 @@ class Tabs extends \Magento\Backend\Block\Widget - } - - /** -+ * Get tab url -+ * - * @param \Magento\Framework\DataObject|TabInterface $tab - * @return string - */ -@@ -414,6 +433,8 @@ class Tabs extends \Magento\Backend\Block\Widget - } - - /** -+ * Get tab title -+ * - * @param \Magento\Framework\DataObject|TabInterface $tab - * @return string - */ -@@ -426,6 +447,8 @@ class Tabs extends \Magento\Backend\Block\Widget - } - - /** -+ * Get tab class -+ * - * @param \Magento\Framework\DataObject|TabInterface $tab - * @return string - */ -@@ -441,6 +464,8 @@ class Tabs extends \Magento\Backend\Block\Widget - } - - /** -+ * Get tab label -+ * - * @param \Magento\Framework\DataObject|TabInterface $tab - * @return string - */ -@@ -453,6 +478,8 @@ class Tabs extends \Magento\Backend\Block\Widget - } - - /** -+ * Get tab content -+ * - * @param \Magento\Framework\DataObject|TabInterface $tab - * @return string - */ -@@ -468,7 +495,8 @@ class Tabs extends \Magento\Backend\Block\Widget - } - - /** -- * Mark tabs as dependant of each other -+ * Mark tabs as dependent of each other -+ * - * Arbitrary number of tabs can be specified, but at least two - * - * @param string $tabOneId -diff --git a/app/code/Magento/Backend/Console/Command/AbstractCacheCommand.php b/app/code/Magento/Backend/Console/Command/AbstractCacheCommand.php -index 70b01046f6a..11da740c466 100644 ---- a/app/code/Magento/Backend/Console/Command/AbstractCacheCommand.php -+++ b/app/code/Magento/Backend/Console/Command/AbstractCacheCommand.php -@@ -11,13 +11,15 @@ use Symfony\Component\Console\Command\Command; - use Symfony\Component\Console\Input\InputOption; - - /** -+ * Abstract cache command -+ * - * @api - * @since 100.0.2 - */ - abstract class AbstractCacheCommand extends Command - { - /** -- * Input option bootsrap -+ * Input option bootstrap - */ - const INPUT_KEY_BOOTSTRAP = 'bootstrap'; - -@@ -40,7 +42,7 @@ abstract class AbstractCacheCommand extends Command - } - - /** -- * {@inheritdoc} -+ * @inheritdoc - */ - protected function configure() - { -diff --git a/app/code/Magento/Backend/Controller/Adminhtml/Auth/DeniedJson.php b/app/code/Magento/Backend/Controller/Adminhtml/Auth/DeniedJson.php -index ad454609776..23731e29f0d 100644 ---- a/app/code/Magento/Backend/Controller/Adminhtml/Auth/DeniedJson.php -+++ b/app/code/Magento/Backend/Controller/Adminhtml/Auth/DeniedJson.php -@@ -6,7 +6,9 @@ - */ - namespace Magento\Backend\Controller\Adminhtml\Auth; - --class DeniedJson extends \Magento\Backend\Controller\Adminhtml\Auth -+use Magento\Framework\App\Action\HttpGetActionInterface as HttpGetActionInterface; -+ -+class DeniedJson extends \Magento\Backend\Controller\Adminhtml\Auth implements HttpGetActionInterface - { - /** - * @var \Magento\Framework\Controller\Result\JsonFactory -diff --git a/app/code/Magento/Backend/Controller/Adminhtml/Auth/Login.php b/app/code/Magento/Backend/Controller/Adminhtml/Auth/Login.php -index e1ea57f6303..1de77c810f3 100644 ---- a/app/code/Magento/Backend/Controller/Adminhtml/Auth/Login.php -+++ b/app/code/Magento/Backend/Controller/Adminhtml/Auth/Login.php -@@ -6,11 +6,14 @@ - */ - namespace Magento\Backend\Controller\Adminhtml\Auth; - -+use Magento\Framework\App\Action\HttpGetActionInterface as HttpGet; -+use Magento\Framework\App\Action\HttpPostActionInterface as HttpPost; -+ - /** - * @api - * @since 100.0.2 - */ --class Login extends \Magento\Backend\Controller\Adminhtml\Auth -+class Login extends \Magento\Backend\Controller\Adminhtml\Auth implements HttpGet, HttpPost - { - /** - * @var \Magento\Framework\View\Result\PageFactory -diff --git a/app/code/Magento/Backend/Controller/Adminhtml/Auth/Logout.php b/app/code/Magento/Backend/Controller/Adminhtml/Auth/Logout.php -index 41e32c92928..d7ad080395e 100644 ---- a/app/code/Magento/Backend/Controller/Adminhtml/Auth/Logout.php -+++ b/app/code/Magento/Backend/Controller/Adminhtml/Auth/Logout.php -@@ -6,7 +6,10 @@ - */ - namespace Magento\Backend\Controller\Adminhtml\Auth; - --class Logout extends \Magento\Backend\Controller\Adminhtml\Auth -+use Magento\Framework\App\Action\HttpGetActionInterface as HttpGet; -+use Magento\Framework\App\Action\HttpPostActionInterface as HttpPost; -+ -+class Logout extends \Magento\Backend\Controller\Adminhtml\Auth implements HttpGet, HttpPost - { - /** - * Administrator logout action -@@ -16,7 +19,7 @@ class Logout extends \Magento\Backend\Controller\Adminhtml\Auth - public function execute() - { - $this->_auth->logout(); -- $this->messageManager->addSuccess(__('You have logged out.')); -+ $this->messageManager->addSuccessMessage(__('You have logged out.')); - - /** @var \Magento\Backend\Model\View\Result\Redirect $resultRedirect */ - $resultRedirect = $this->resultRedirectFactory->create(); -diff --git a/app/code/Magento/Backend/Controller/Adminhtml/Cache/CleanImages.php b/app/code/Magento/Backend/Controller/Adminhtml/Cache/CleanImages.php -index 7a926b1c09c..79bc19256d2 100644 ---- a/app/code/Magento/Backend/Controller/Adminhtml/Cache/CleanImages.php -+++ b/app/code/Magento/Backend/Controller/Adminhtml/Cache/CleanImages.php -@@ -6,10 +6,11 @@ - */ - namespace Magento\Backend\Controller\Adminhtml\Cache; - -+use Magento\Framework\App\Action\HttpGetActionInterface as HttpGetActionInterface; - use Magento\Framework\Exception\LocalizedException; - use Magento\Framework\Controller\ResultFactory; - --class CleanImages extends \Magento\Backend\Controller\Adminhtml\Cache -+class CleanImages extends \Magento\Backend\Controller\Adminhtml\Cache implements HttpGetActionInterface - { - /** - * Authorization level of a basic admin session -@@ -28,11 +29,11 @@ class CleanImages extends \Magento\Backend\Controller\Adminhtml\Cache - try { - $this->_objectManager->create(\Magento\Catalog\Model\Product\Image::class)->clearCache(); - $this->_eventManager->dispatch('clean_catalog_images_cache_after'); -- $this->messageManager->addSuccess(__('The image cache was cleaned.')); -+ $this->messageManager->addSuccessMessage(__('The image cache was cleaned.')); - } catch (LocalizedException $e) { -- $this->messageManager->addError($e->getMessage()); -+ $this->messageManager->addErrorMessage($e->getMessage()); - } catch (\Exception $e) { -- $this->messageManager->addException($e, __('An error occurred while clearing the image cache.')); -+ $this->messageManager->addExceptionMessage($e, __('An error occurred while clearing the image cache.')); - } - - /** @var \Magento\Backend\Model\View\Result\Redirect $resultRedirect */ -diff --git a/app/code/Magento/Backend/Controller/Adminhtml/Cache/CleanMedia.php b/app/code/Magento/Backend/Controller/Adminhtml/Cache/CleanMedia.php -index 72f23ab65cf..36aca1afcc4 100644 ---- a/app/code/Magento/Backend/Controller/Adminhtml/Cache/CleanMedia.php -+++ b/app/code/Magento/Backend/Controller/Adminhtml/Cache/CleanMedia.php -@@ -6,10 +6,11 @@ - */ - namespace Magento\Backend\Controller\Adminhtml\Cache; - -+use Magento\Framework\App\Action\HttpGetActionInterface as HttpGetActionInterface; - use Magento\Framework\Exception\LocalizedException; - use Magento\Framework\Controller\ResultFactory; - --class CleanMedia extends \Magento\Backend\Controller\Adminhtml\Cache -+class CleanMedia extends \Magento\Backend\Controller\Adminhtml\Cache implements HttpGetActionInterface - { - /** - * Authorization level of a basic admin session -@@ -28,11 +29,12 @@ class CleanMedia extends \Magento\Backend\Controller\Adminhtml\Cache - try { - $this->_objectManager->get(\Magento\Framework\View\Asset\MergeService::class)->cleanMergedJsCss(); - $this->_eventManager->dispatch('clean_media_cache_after'); -- $this->messageManager->addSuccess(__('The JavaScript/CSS cache has been cleaned.')); -+ $this->messageManager->addSuccessMessage(__('The JavaScript/CSS cache has been cleaned.')); - } catch (LocalizedException $e) { -- $this->messageManager->addError($e->getMessage()); -+ $this->messageManager->addErrorMessage($e->getMessage()); - } catch (\Exception $e) { -- $this->messageManager->addException($e, __('An error occurred while clearing the JavaScript/CSS cache.')); -+ $this->messageManager -+ ->addExceptionMessage($e, __('An error occurred while clearing the JavaScript/CSS cache.')); - } - - /** @var \Magento\Backend\Model\View\Result\Redirect $resultRedirect */ -diff --git a/app/code/Magento/Backend/Controller/Adminhtml/Cache/CleanStaticFiles.php b/app/code/Magento/Backend/Controller/Adminhtml/Cache/CleanStaticFiles.php -index 27ae2fc31e1..a3a26c5cf62 100644 ---- a/app/code/Magento/Backend/Controller/Adminhtml/Cache/CleanStaticFiles.php -+++ b/app/code/Magento/Backend/Controller/Adminhtml/Cache/CleanStaticFiles.php -@@ -6,9 +6,10 @@ - */ - namespace Magento\Backend\Controller\Adminhtml\Cache; - -+use Magento\Framework\App\Action\HttpGetActionInterface as HttpGetActionInterface; - use Magento\Framework\Controller\ResultFactory; - --class CleanStaticFiles extends \Magento\Backend\Controller\Adminhtml\Cache -+class CleanStaticFiles extends \Magento\Backend\Controller\Adminhtml\Cache implements HttpGetActionInterface - { - /** - * Authorization level of a basic admin session -@@ -26,7 +27,7 @@ class CleanStaticFiles extends \Magento\Backend\Controller\Adminhtml\Cache - { - $this->_objectManager->get(\Magento\Framework\App\State\CleanupFiles::class)->clearMaterializedViewFiles(); - $this->_eventManager->dispatch('clean_static_files_cache_after'); -- $this->messageManager->addSuccess(__('The static files cache has been cleaned.')); -+ $this->messageManager->addSuccessMessage(__('The static files cache has been cleaned.')); - - /** @var \Magento\Backend\Model\View\Result\Redirect $resultRedirect */ - $resultRedirect = $this->resultFactory->create(ResultFactory::TYPE_REDIRECT); -diff --git a/app/code/Magento/Backend/Controller/Adminhtml/Cache/FlushAll.php b/app/code/Magento/Backend/Controller/Adminhtml/Cache/FlushAll.php -index ca89ea58fa6..daf424d14c5 100644 ---- a/app/code/Magento/Backend/Controller/Adminhtml/Cache/FlushAll.php -+++ b/app/code/Magento/Backend/Controller/Adminhtml/Cache/FlushAll.php -@@ -6,7 +6,9 @@ - */ - namespace Magento\Backend\Controller\Adminhtml\Cache; - --class FlushAll extends \Magento\Backend\Controller\Adminhtml\Cache -+use Magento\Framework\App\Action\HttpGetActionInterface as HttpGetActionInterface; -+ -+class FlushAll extends \Magento\Backend\Controller\Adminhtml\Cache implements HttpGetActionInterface - { - /** - * Authorization level of a basic admin session -@@ -27,7 +29,7 @@ class FlushAll extends \Magento\Backend\Controller\Adminhtml\Cache - foreach ($this->_cacheFrontendPool as $cacheFrontend) { - $cacheFrontend->getBackend()->clean(); - } -- $this->messageManager->addSuccess(__("You flushed the cache storage.")); -+ $this->messageManager->addSuccessMessage(__("You flushed the cache storage.")); - /** @var \Magento\Backend\Model\View\Result\Redirect $resultRedirect */ - $resultRedirect = $this->resultRedirectFactory->create(); - return $resultRedirect->setPath('adminhtml/*'); -diff --git a/app/code/Magento/Backend/Controller/Adminhtml/Cache/FlushSystem.php b/app/code/Magento/Backend/Controller/Adminhtml/Cache/FlushSystem.php -index f0fed159e0f..f3474bf4387 100644 ---- a/app/code/Magento/Backend/Controller/Adminhtml/Cache/FlushSystem.php -+++ b/app/code/Magento/Backend/Controller/Adminhtml/Cache/FlushSystem.php -@@ -6,7 +6,9 @@ - */ - namespace Magento\Backend\Controller\Adminhtml\Cache; - --class FlushSystem extends \Magento\Backend\Controller\Adminhtml\Cache -+use Magento\Framework\App\Action\HttpGetActionInterface as HttpGetActionInterface; -+ -+class FlushSystem extends \Magento\Backend\Controller\Adminhtml\Cache implements HttpGetActionInterface - { - /** - * Authorization level of a basic admin session -@@ -27,7 +29,7 @@ class FlushSystem extends \Magento\Backend\Controller\Adminhtml\Cache - $cacheFrontend->clean(); - } - $this->_eventManager->dispatch('adminhtml_cache_flush_system'); -- $this->messageManager->addSuccess(__("The Magento cache storage has been flushed.")); -+ $this->messageManager->addSuccessMessage(__("The Magento cache storage has been flushed.")); - /** @var \Magento\Backend\Model\View\Result\Redirect $resultRedirect */ - $resultRedirect = $this->resultRedirectFactory->create(); - return $resultRedirect->setPath('adminhtml/*'); -diff --git a/app/code/Magento/Backend/Controller/Adminhtml/Cache/Index.php b/app/code/Magento/Backend/Controller/Adminhtml/Cache/Index.php -index 05bd309ca62..f1e908bb842 100644 ---- a/app/code/Magento/Backend/Controller/Adminhtml/Cache/Index.php -+++ b/app/code/Magento/Backend/Controller/Adminhtml/Cache/Index.php -@@ -6,7 +6,9 @@ - */ - namespace Magento\Backend\Controller\Adminhtml\Cache; - --class Index extends \Magento\Backend\Controller\Adminhtml\Cache -+use Magento\Framework\App\Action\HttpGetActionInterface as HttpGetActionInterface; -+ -+class Index extends \Magento\Backend\Controller\Adminhtml\Cache implements HttpGetActionInterface - { - /** - * Display cache management grid -diff --git a/app/code/Magento/Backend/Controller/Adminhtml/Cache/MassDisable.php b/app/code/Magento/Backend/Controller/Adminhtml/Cache/MassDisable.php -index 2bfa937b06b..03b88ca1d3f 100644 ---- a/app/code/Magento/Backend/Controller/Adminhtml/Cache/MassDisable.php -+++ b/app/code/Magento/Backend/Controller/Adminhtml/Cache/MassDisable.php -@@ -67,12 +67,12 @@ class MassDisable extends \Magento\Backend\Controller\Adminhtml\Cache - } - if ($updatedTypes > 0) { - $this->_cacheState->persist(); -- $this->messageManager->addSuccess(__("%1 cache type(s) disabled.", $updatedTypes)); -+ $this->messageManager->addSuccessMessage(__("%1 cache type(s) disabled.", $updatedTypes)); - } - } catch (LocalizedException $e) { -- $this->messageManager->addError($e->getMessage()); -+ $this->messageManager->addErrorMessage($e->getMessage()); - } catch (\Exception $e) { -- $this->messageManager->addException($e, __('An error occurred while disabling cache.')); -+ $this->messageManager->addExceptionMessage($e, __('An error occurred while disabling cache.')); - } - } - -diff --git a/app/code/Magento/Backend/Controller/Adminhtml/Cache/MassEnable.php b/app/code/Magento/Backend/Controller/Adminhtml/Cache/MassEnable.php -index 113e0f2d896..1b98a00d4bf 100644 ---- a/app/code/Magento/Backend/Controller/Adminhtml/Cache/MassEnable.php -+++ b/app/code/Magento/Backend/Controller/Adminhtml/Cache/MassEnable.php -@@ -66,12 +66,12 @@ class MassEnable extends \Magento\Backend\Controller\Adminhtml\Cache - } - if ($updatedTypes > 0) { - $this->_cacheState->persist(); -- $this->messageManager->addSuccess(__("%1 cache type(s) enabled.", $updatedTypes)); -+ $this->messageManager->addSuccessMessage(__("%1 cache type(s) enabled.", $updatedTypes)); - } - } catch (LocalizedException $e) { -- $this->messageManager->addError($e->getMessage()); -+ $this->messageManager->addErrorMessage($e->getMessage()); - } catch (\Exception $e) { -- $this->messageManager->addException($e, __('An error occurred while enabling cache.')); -+ $this->messageManager->addExceptionMessage($e, __('An error occurred while enabling cache.')); - } - } - -diff --git a/app/code/Magento/Backend/Controller/Adminhtml/Cache/MassRefresh.php b/app/code/Magento/Backend/Controller/Adminhtml/Cache/MassRefresh.php -index 3843b030afb..bde211debcf 100644 ---- a/app/code/Magento/Backend/Controller/Adminhtml/Cache/MassRefresh.php -+++ b/app/code/Magento/Backend/Controller/Adminhtml/Cache/MassRefresh.php -@@ -37,12 +37,12 @@ class MassRefresh extends \Magento\Backend\Controller\Adminhtml\Cache - $updatedTypes++; - } - if ($updatedTypes > 0) { -- $this->messageManager->addSuccess(__("%1 cache type(s) refreshed.", $updatedTypes)); -+ $this->messageManager->addSuccessMessage(__("%1 cache type(s) refreshed.", $updatedTypes)); - } - } catch (LocalizedException $e) { -- $this->messageManager->addError($e->getMessage()); -+ $this->messageManager->addErrorMessage($e->getMessage()); - } catch (\Exception $e) { -- $this->messageManager->addException($e, __('An error occurred while refreshing cache.')); -+ $this->messageManager->addExceptionMessage($e, __('An error occurred while refreshing cache.')); - } - - /** @var \Magento\Backend\Model\View\Result\Redirect $resultRedirect */ -diff --git a/app/code/Magento/Backend/Controller/Adminhtml/Dashboard/Index.php b/app/code/Magento/Backend/Controller/Adminhtml/Dashboard/Index.php -index d8c52f6c50b..decca6837fa 100644 ---- a/app/code/Magento/Backend/Controller/Adminhtml/Dashboard/Index.php -+++ b/app/code/Magento/Backend/Controller/Adminhtml/Dashboard/Index.php -@@ -6,7 +6,11 @@ - */ - namespace Magento\Backend\Controller\Adminhtml\Dashboard; - --class Index extends \Magento\Backend\Controller\Adminhtml\Dashboard -+use Magento\Backend\Controller\Adminhtml\Dashboard as DashboardAction; -+use Magento\Framework\App\Action\HttpGetActionInterface; -+use Magento\Framework\App\Action\HttpPostActionInterface; -+ -+class Index extends DashboardAction implements HttpGetActionInterface, HttpPostActionInterface - { - /** - * @var \Magento\Framework\View\Result\PageFactory -diff --git a/app/code/Magento/Backend/Controller/Adminhtml/Dashboard/ProductsViewed.php b/app/code/Magento/Backend/Controller/Adminhtml/Dashboard/ProductsViewed.php -index 3907f4a4f71..a42a44814cb 100644 ---- a/app/code/Magento/Backend/Controller/Adminhtml/Dashboard/ProductsViewed.php -+++ b/app/code/Magento/Backend/Controller/Adminhtml/Dashboard/ProductsViewed.php -@@ -6,12 +6,17 @@ - */ - namespace Magento\Backend\Controller\Adminhtml\Dashboard; - --class ProductsViewed extends AjaxBlock -+use Magento\Framework\App\Action\HttpPostActionInterface; -+ -+/** -+ * Get most viewed products controller. -+ */ -+class ProductsViewed extends AjaxBlock implements HttpPostActionInterface - { - /** - * Gets most viewed products list - * -- * @return \Magento\Backend\Model\View\Result\Page -+ * @return \Magento\Framework\Controller\Result\Raw - */ - public function execute() - { -diff --git a/app/code/Magento/Backend/Controller/Adminhtml/Dashboard/RefreshStatistics.php b/app/code/Magento/Backend/Controller/Adminhtml/Dashboard/RefreshStatistics.php -index f831fa67f4b..c709859adb1 100644 ---- a/app/code/Magento/Backend/Controller/Adminhtml/Dashboard/RefreshStatistics.php -+++ b/app/code/Magento/Backend/Controller/Adminhtml/Dashboard/RefreshStatistics.php -@@ -6,7 +6,13 @@ - - namespace Magento\Backend\Controller\Adminhtml\Dashboard; - --class RefreshStatistics extends \Magento\Reports\Controller\Adminhtml\Report\Statistics -+use Magento\Framework\App\Action\HttpPostActionInterface; -+use Magento\Reports\Controller\Adminhtml\Report\Statistics; -+ -+/** -+ * Refresh Dashboard statistics action. -+ */ -+class RefreshStatistics extends Statistics implements HttpPostActionInterface - { - /** - * @param \Magento\Backend\App\Action\Context $context -@@ -25,6 +31,8 @@ class RefreshStatistics extends \Magento\Reports\Controller\Adminhtml\Report\Sta - } - - /** -+ * Refresh statistics. -+ * - * @return \Magento\Backend\Model\View\Result\Redirect - */ - public function execute() -@@ -34,9 +42,9 @@ class RefreshStatistics extends \Magento\Reports\Controller\Adminhtml\Report\Sta - foreach ($collectionsNames as $collectionName) { - $this->_objectManager->create($collectionName)->aggregate(); - } -- $this->messageManager->addSuccess(__('We updated lifetime statistic.')); -+ $this->messageManager->addSuccessMessage(__('We updated lifetime statistic.')); - } catch (\Exception $e) { -- $this->messageManager->addError(__('We can\'t refresh lifetime statistics.')); -+ $this->messageManager->addErrorMessage(__('We can\'t refresh lifetime statistics.')); - $this->logger->critical($e); - } - -diff --git a/app/code/Magento/Backend/Controller/Adminhtml/Index/GlobalSearch.php b/app/code/Magento/Backend/Controller/Adminhtml/Index/GlobalSearch.php -index 9ca4021d083..37f3064aeaa 100644 ---- a/app/code/Magento/Backend/Controller/Adminhtml/Index/GlobalSearch.php -+++ b/app/code/Magento/Backend/Controller/Adminhtml/Index/GlobalSearch.php -@@ -6,11 +6,15 @@ - */ - namespace Magento\Backend\Controller\Adminhtml\Index; - -+use Magento\Backend\Controller\Adminhtml\Index as IndexAction; -+use Magento\Framework\App\Action\HttpGetActionInterface; -+use Magento\Framework\App\Action\HttpPostActionInterface as HttpPostActionInterface; -+ - /** - * @api - * @since 100.0.2 - */ --class GlobalSearch extends \Magento\Backend\Controller\Adminhtml\Index -+class GlobalSearch extends IndexAction implements HttpGetActionInterface, HttpPostActionInterface - { - /** - * @var \Magento\Framework\Controller\Result\JsonFactory -diff --git a/app/code/Magento/Backend/Controller/Adminhtml/Index/Index.php b/app/code/Magento/Backend/Controller/Adminhtml/Index/Index.php -index a5c71fb2dbc..afb1b457327 100644 ---- a/app/code/Magento/Backend/Controller/Adminhtml/Index/Index.php -+++ b/app/code/Magento/Backend/Controller/Adminhtml/Index/Index.php -@@ -6,7 +6,10 @@ - */ - namespace Magento\Backend\Controller\Adminhtml\Index; - --class Index extends \Magento\Backend\Controller\Adminhtml\Index -+use Magento\Framework\App\Action\HttpGetActionInterface as HttpGet; -+use Magento\Framework\App\Action\HttpPostActionInterface as HttpPost; -+ -+class Index extends \Magento\Backend\Controller\Adminhtml\Index implements HttpGet, HttpPost - { - /** - * Admin area entry point -diff --git a/app/code/Magento/Backend/Controller/Adminhtml/Noroute/Index.php b/app/code/Magento/Backend/Controller/Adminhtml/Noroute/Index.php -index ce59d2fd48e..e84987d8e1d 100644 ---- a/app/code/Magento/Backend/Controller/Adminhtml/Noroute/Index.php -+++ b/app/code/Magento/Backend/Controller/Adminhtml/Noroute/Index.php -@@ -6,6 +6,9 @@ - */ - namespace Magento\Backend\Controller\Adminhtml\Noroute; - -+/** -+ * @SuppressWarnings(PHPMD.AllPurposeAction) -+ */ - class Index extends \Magento\Backend\App\Action - { - /** -diff --git a/app/code/Magento/Backend/Controller/Adminhtml/System/Account/Index.php b/app/code/Magento/Backend/Controller/Adminhtml/System/Account/Index.php -index 54771bfdc1a..648f1be86f5 100644 ---- a/app/code/Magento/Backend/Controller/Adminhtml/System/Account/Index.php -+++ b/app/code/Magento/Backend/Controller/Adminhtml/System/Account/Index.php -@@ -6,7 +6,9 @@ - */ - namespace Magento\Backend\Controller\Adminhtml\System\Account; - --class Index extends \Magento\Backend\Controller\Adminhtml\System\Account -+use Magento\Framework\App\Action\HttpGetActionInterface as HttpGetActionInterface; -+ -+class Index extends \Magento\Backend\Controller\Adminhtml\System\Account implements HttpGetActionInterface - { - /** - * @var \Magento\Framework\View\Result\PageFactory -diff --git a/app/code/Magento/Backend/Controller/Adminhtml/System/Account/Save.php b/app/code/Magento/Backend/Controller/Adminhtml/System/Account/Save.php -index 1b10c151a9d..d95b0541c2c 100644 ---- a/app/code/Magento/Backend/Controller/Adminhtml/System/Account/Save.php -+++ b/app/code/Magento/Backend/Controller/Adminhtml/System/Account/Save.php -@@ -76,12 +76,12 @@ class Save extends \Magento\Backend\Controller\Adminhtml\System\Account - $errors = $user->validate(); - if ($errors !== true && !empty($errors)) { - foreach ($errors as $error) { -- $this->messageManager->addError($error); -+ $this->messageManager->addErrorMessage($error); - } - } else { - $user->save(); - $user->sendNotificationEmailsIfRequired(); -- $this->messageManager->addSuccess(__('You saved the account.')); -+ $this->messageManager->addSuccessMessage(__('You saved the account.')); - } - } catch (UserLockedException $e) { - $this->_auth->logout(); -@@ -91,12 +91,12 @@ class Save extends \Magento\Backend\Controller\Adminhtml\System\Account - } catch (ValidatorException $e) { - $this->messageManager->addMessages($e->getMessages()); - if ($e->getMessage()) { -- $this->messageManager->addError($e->getMessage()); -+ $this->messageManager->addErrorMessage($e->getMessage()); - } - } catch (LocalizedException $e) { -- $this->messageManager->addError($e->getMessage()); -+ $this->messageManager->addErrorMessage($e->getMessage()); - } catch (\Exception $e) { -- $this->messageManager->addError(__('An error occurred while saving account.')); -+ $this->messageManager->addErrorMessage(__('An error occurred while saving account.')); - } - - /** @var \Magento\Backend\Model\View\Result\Redirect $resultRedirect */ -diff --git a/app/code/Magento/Backend/Controller/Adminhtml/System/Design/Delete.php b/app/code/Magento/Backend/Controller/Adminhtml/System/Design/Delete.php -index 76402169f26..21f28188cf8 100644 ---- a/app/code/Magento/Backend/Controller/Adminhtml/System/Design/Delete.php -+++ b/app/code/Magento/Backend/Controller/Adminhtml/System/Design/Delete.php -@@ -19,11 +19,11 @@ class Delete extends \Magento\Backend\Controller\Adminhtml\System\Design - - try { - $design->delete(); -- $this->messageManager->addSuccess(__('You deleted the design change.')); -+ $this->messageManager->addSuccessMessage(__('You deleted the design change.')); - } catch (\Magento\Framework\Exception\LocalizedException $e) { -- $this->messageManager->addError($e->getMessage()); -+ $this->messageManager->addErrorMessage($e->getMessage()); - } catch (\Exception $e) { -- $this->messageManager->addException($e, __("You can't delete the design change.")); -+ $this->messageManager->addExceptionMessage($e, __("You can't delete the design change.")); - } - } - /** @var \Magento\Backend\Model\View\Result\Redirect $resultRedirect */ -diff --git a/app/code/Magento/Backend/Controller/Adminhtml/System/Design/Index.php b/app/code/Magento/Backend/Controller/Adminhtml/System/Design/Index.php -index 30b26f22941..c6a05b5a71d 100644 ---- a/app/code/Magento/Backend/Controller/Adminhtml/System/Design/Index.php -+++ b/app/code/Magento/Backend/Controller/Adminhtml/System/Design/Index.php -@@ -6,7 +6,9 @@ - */ - namespace Magento\Backend\Controller\Adminhtml\System\Design; - --class Index extends \Magento\Backend\Controller\Adminhtml\System\Design -+use Magento\Framework\App\Action\HttpGetActionInterface as HttpGetActionInterface; -+ -+class Index extends \Magento\Backend\Controller\Adminhtml\System\Design implements HttpGetActionInterface - { - /** - * @return \Magento\Backend\Model\View\Result\Page -diff --git a/app/code/Magento/Backend/Controller/Adminhtml/System/Design/Save.php b/app/code/Magento/Backend/Controller/Adminhtml/System/Design/Save.php -index 1f478604ced..25cfb61d658 100644 ---- a/app/code/Magento/Backend/Controller/Adminhtml/System/Design/Save.php -+++ b/app/code/Magento/Backend/Controller/Adminhtml/System/Design/Save.php -@@ -6,7 +6,12 @@ - */ - namespace Magento\Backend\Controller\Adminhtml\System\Design; - --class Save extends \Magento\Backend\Controller\Adminhtml\System\Design -+use Magento\Framework\App\Action\HttpPostActionInterface; -+ -+/** -+ * Save design action. -+ */ -+class Save extends \Magento\Backend\Controller\Adminhtml\System\Design implements HttpPostActionInterface - { - /** - * Filtering posted data. Converting localized data if needed -@@ -26,6 +31,8 @@ class Save extends \Magento\Backend\Controller\Adminhtml\System\Design - } - - /** -+ * Save design action. -+ * - * @return \Magento\Backend\Model\View\Result\Redirect - */ - public function execute() -@@ -50,14 +57,14 @@ class Save extends \Magento\Backend\Controller\Adminhtml\System\Design - try { - $design->save(); - $this->_eventManager->dispatch('theme_save_after'); -- $this->messageManager->addSuccess(__('You saved the design change.')); -+ $this->messageManager->addSuccessMessage(__('You saved the design change.')); - } catch (\Exception $e) { -- $this->messageManager->addError($e->getMessage()); -+ $this->messageManager->addErrorMessage($e->getMessage()); - $this->_objectManager->get(\Magento\Backend\Model\Session::class)->setDesignData($data); -- return $resultRedirect->setPath('adminhtml/*/', ['id' => $design->getId()]); -+ return $resultRedirect->setPath('*/*/edit', ['id' => $design->getId()]); - } - } - -- return $resultRedirect->setPath('adminhtml/*/'); -+ return $resultRedirect->setPath('*/*/'); - } - } -diff --git a/app/code/Magento/Backend/Controller/Adminhtml/System/Store.php b/app/code/Magento/Backend/Controller/Adminhtml/System/Store.php -index 4fbae6abb42..a9be14b77b2 100644 ---- a/app/code/Magento/Backend/Controller/Adminhtml/System/Store.php -+++ b/app/code/Magento/Backend/Controller/Adminhtml/System/Store.php -@@ -14,6 +14,7 @@ use Magento\Framework\Filesystem; - * Store controller - * - * @author Magento Core Team -+ * @SuppressWarnings(PHPMD.AllPurposeAction) - */ - abstract class Store extends Action - { -@@ -86,6 +87,8 @@ abstract class Store extends Action - * Backup database - * - * @return bool -+ * -+ * @deprecated Backup module is to be removed. - */ - protected function _backupDatabase() - { -@@ -103,12 +106,12 @@ abstract class Store extends Action - ->setType('db') - ->setPath($filesystem->getDirectoryRead(DirectoryList::VAR_DIR)->getAbsolutePath('backups')); - $backupDb->createBackup($backup); -- $this->messageManager->addSuccess(__('The database was backed up.')); -+ $this->messageManager->addSuccessMessage(__('The database was backed up.')); - } catch (\Magento\Framework\Exception\LocalizedException $e) { -- $this->messageManager->addError($e->getMessage()); -+ $this->messageManager->addErrorMessage($e->getMessage()); - return false; - } catch (\Exception $e) { -- $this->messageManager->addException( -+ $this->messageManager->addExceptionMessage( - $e, - __('We can\'t create a backup right now. Please try again later.') - ); -@@ -125,7 +128,7 @@ abstract class Store extends Action - */ - protected function _addDeletionNotice($typeTitle) - { -- $this->messageManager->addNotice( -+ $this->messageManager->addNoticeMessage( - __( - 'Deleting a %1 will not delete the information associated with the %1 (e.g. categories, products, etc.)' - . ', but the %1 will not be able to be restored. It is suggested that you create a database backup ' -diff --git a/app/code/Magento/Backend/Controller/Adminhtml/System/Store/DeleteGroup.php b/app/code/Magento/Backend/Controller/Adminhtml/System/Store/DeleteGroup.php -index 925ae4c69ee..4e323be709a 100644 ---- a/app/code/Magento/Backend/Controller/Adminhtml/System/Store/DeleteGroup.php -+++ b/app/code/Magento/Backend/Controller/Adminhtml/System/Store/DeleteGroup.php -@@ -15,13 +15,13 @@ class DeleteGroup extends \Magento\Backend\Controller\Adminhtml\System\Store - { - $itemId = $this->getRequest()->getParam('item_id', null); - if (!($model = $this->_objectManager->create(\Magento\Store\Model\Group::class)->load($itemId))) { -- $this->messageManager->addError(__('Something went wrong. Please try again.')); -+ $this->messageManager->addErrorMessage(__('Something went wrong. Please try again.')); - /** @var \Magento\Backend\Model\View\Result\Redirect $redirectResult */ - $redirectResult = $this->resultRedirectFactory->create(); - return $redirectResult->setPath('adminhtml/*/'); - } - if (!$model->isCanDelete()) { -- $this->messageManager->addError(__('This store cannot be deleted.')); -+ $this->messageManager->addErrorMessage(__('This store cannot be deleted.')); - /** @var \Magento\Backend\Model\View\Result\Redirect $redirectResult */ - $redirectResult = $this->resultRedirectFactory->create(); - return $redirectResult->setPath('adminhtml/*/editGroup', ['group_id' => $itemId]); -diff --git a/app/code/Magento/Backend/Controller/Adminhtml/System/Store/DeleteGroupPost.php b/app/code/Magento/Backend/Controller/Adminhtml/System/Store/DeleteGroupPost.php -index b6fbd88c766..49c327060da 100644 ---- a/app/code/Magento/Backend/Controller/Adminhtml/System/Store/DeleteGroupPost.php -+++ b/app/code/Magento/Backend/Controller/Adminhtml/System/Store/DeleteGroupPost.php -@@ -1,16 +1,20 @@ - resultFactory->create(ResultFactory::TYPE_REDIRECT); - - if (!($model = $this->_objectManager->create(\Magento\Store\Model\Group::class)->load($itemId))) { -- $this->messageManager->addError(__('Something went wrong. Please try again.')); -+ $this->messageManager->addErrorMessage(__('Something went wrong. Please try again.')); - return $redirectResult->setPath('adminhtml/*/'); - } - if (!$model->isCanDelete()) { -- $this->messageManager->addError(__('This store cannot be deleted.')); -+ $this->messageManager->addErrorMessage(__('This store cannot be deleted.')); - return $redirectResult->setPath('adminhtml/*/editGroup', ['group_id' => $model->getId()]); - } - -@@ -35,12 +39,12 @@ class DeleteGroupPost extends \Magento\Backend\Controller\Adminhtml\System\Store - - try { - $model->delete(); -- $this->messageManager->addSuccess(__('You deleted the store.')); -+ $this->messageManager->addSuccessMessage(__('You deleted the store.')); - return $redirectResult->setPath('adminhtml/*/'); - } catch (\Magento\Framework\Exception\LocalizedException $e) { -- $this->messageManager->addError($e->getMessage()); -+ $this->messageManager->addErrorMessage($e->getMessage()); - } catch (\Exception $e) { -- $this->messageManager->addException($e, __('Unable to delete the store. Please try again later.')); -+ $this->messageManager->addExceptionMessage($e, __('Unable to delete the store. Please try again later.')); - } - return $redirectResult->setPath('adminhtml/*/editGroup', ['group_id' => $itemId]); - } -diff --git a/app/code/Magento/Backend/Controller/Adminhtml/System/Store/DeleteStore.php b/app/code/Magento/Backend/Controller/Adminhtml/System/Store/DeleteStore.php -index b31de6cacc5..c340b1ec53a 100644 ---- a/app/code/Magento/Backend/Controller/Adminhtml/System/Store/DeleteStore.php -+++ b/app/code/Magento/Backend/Controller/Adminhtml/System/Store/DeleteStore.php -@@ -15,13 +15,13 @@ class DeleteStore extends \Magento\Backend\Controller\Adminhtml\System\Store - { - $itemId = $this->getRequest()->getParam('item_id', null); - if (!($model = $this->_objectManager->create(\Magento\Store\Model\Store::class)->load($itemId))) { -- $this->messageManager->addError(__('Something went wrong. Please try again.')); -+ $this->messageManager->addErrorMessage(__('Something went wrong. Please try again.')); - /** @var \Magento\Backend\Model\View\Result\Redirect $redirectResult */ - $redirectResult = $this->resultRedirectFactory->create(); - return $redirectResult->setPath('adminhtml/*/'); - } - if (!$model->isCanDelete()) { -- $this->messageManager->addError(__('This store view cannot be deleted.')); -+ $this->messageManager->addErrorMessage(__('This store view cannot be deleted.')); - /** @var \Magento\Backend\Model\View\Result\Redirect $redirectResult */ - $redirectResult = $this->resultRedirectFactory->create(); - return $redirectResult->setPath('adminhtml/*/editStore', ['store_id' => $itemId]); -diff --git a/app/code/Magento/Backend/Controller/Adminhtml/System/Store/DeleteStorePost.php b/app/code/Magento/Backend/Controller/Adminhtml/System/Store/DeleteStorePost.php -index 13b104c5ec4..7999012b435 100644 ---- a/app/code/Magento/Backend/Controller/Adminhtml/System/Store/DeleteStorePost.php -+++ b/app/code/Magento/Backend/Controller/Adminhtml/System/Store/DeleteStorePost.php -@@ -1,14 +1,17 @@ - resultFactory->create(ResultFactory::TYPE_REDIRECT); - if (!($model = $this->_objectManager->create(\Magento\Store\Model\Store::class)->load($itemId))) { -- $this->messageManager->addError(__('Something went wrong. Please try again.')); -+ $this->messageManager->addErrorMessage(__('Something went wrong. Please try again.')); - return $redirectResult->setPath('adminhtml/*/'); - } - if (!$model->isCanDelete()) { -- $this->messageManager->addError(__('This store view cannot be deleted.')); -+ $this->messageManager->addErrorMessage(__('This store view cannot be deleted.')); - return $redirectResult->setPath('adminhtml/*/editStore', ['store_id' => $model->getId()]); - } - -@@ -37,12 +40,13 @@ class DeleteStorePost extends \Magento\Backend\Controller\Adminhtml\System\Store - try { - $model->delete(); - -- $this->messageManager->addSuccess(__('You deleted the store view.')); -+ $this->messageManager->addSuccessMessage(__('You deleted the store view.')); - return $redirectResult->setPath('adminhtml/*/'); - } catch (\Magento\Framework\Exception\LocalizedException $e) { -- $this->messageManager->addError($e->getMessage()); -+ $this->messageManager->addErrorMessage($e->getMessage()); - } catch (\Exception $e) { -- $this->messageManager->addException($e, __('Unable to delete the store view. Please try again later.')); -+ $this->messageManager -+ ->addExceptionMessage($e, __('Unable to delete the store view. Please try again later.')); - } - return $redirectResult->setPath('adminhtml/*/editStore', ['store_id' => $itemId]); - } -diff --git a/app/code/Magento/Backend/Controller/Adminhtml/System/Store/DeleteWebsite.php b/app/code/Magento/Backend/Controller/Adminhtml/System/Store/DeleteWebsite.php -index 1f2ec4b2ba4..9e8664b8ecd 100644 ---- a/app/code/Magento/Backend/Controller/Adminhtml/System/Store/DeleteWebsite.php -+++ b/app/code/Magento/Backend/Controller/Adminhtml/System/Store/DeleteWebsite.php -@@ -6,7 +6,9 @@ - */ - namespace Magento\Backend\Controller\Adminhtml\System\Store; - --class DeleteWebsite extends \Magento\Backend\Controller\Adminhtml\System\Store -+use Magento\Framework\App\Action\HttpGetActionInterface as HttpGetActionInterface; -+ -+class DeleteWebsite extends \Magento\Backend\Controller\Adminhtml\System\Store implements HttpGetActionInterface - { - /** - * @return \Magento\Framework\Controller\ResultInterface -@@ -15,13 +17,13 @@ class DeleteWebsite extends \Magento\Backend\Controller\Adminhtml\System\Store - { - $itemId = $this->getRequest()->getParam('item_id', null); - if (!($model = $this->_objectManager->create(\Magento\Store\Model\Website::class)->load($itemId))) { -- $this->messageManager->addError(__('Something went wrong. Please try again.')); -+ $this->messageManager->addErrorMessage(__('Something went wrong. Please try again.')); - /** @var \Magento\Backend\Model\View\Result\Redirect $redirectResult */ - $redirectResult = $this->resultRedirectFactory->create(); - return $redirectResult->setPath('adminhtml/*/'); - } - if (!$model->isCanDelete()) { -- $this->messageManager->addError(__('This website cannot be deleted.')); -+ $this->messageManager->addErrorMessage(__('This website cannot be deleted.')); - /** @var \Magento\Backend\Model\View\Result\Redirect $redirectResult */ - $redirectResult = $this->resultRedirectFactory->create(); - return $redirectResult->setPath('adminhtml/*/editWebsite', ['website_id' => $itemId]); -diff --git a/app/code/Magento/Backend/Controller/Adminhtml/System/Store/DeleteWebsitePost.php b/app/code/Magento/Backend/Controller/Adminhtml/System/Store/DeleteWebsitePost.php -index c2d24b8c41a..3fee1a25c9f 100644 ---- a/app/code/Magento/Backend/Controller/Adminhtml/System/Store/DeleteWebsitePost.php -+++ b/app/code/Magento/Backend/Controller/Adminhtml/System/Store/DeleteWebsitePost.php -@@ -6,11 +6,16 @@ - */ - namespace Magento\Backend\Controller\Adminhtml\System\Store; - -+use Magento\Framework\App\Action\HttpPostActionInterface as HttpPostActionInterface; - use Magento\Framework\Controller\ResultFactory; - --class DeleteWebsitePost extends \Magento\Backend\Controller\Adminhtml\System\Store -+/** -+ * Delete website. -+ */ -+class DeleteWebsitePost extends \Magento\Backend\Controller\Adminhtml\System\Store implements HttpPostActionInterface - { - /** -+ * @inheritDoc - * @return \Magento\Backend\Model\View\Result\Redirect - */ - public function execute() -@@ -23,11 +28,11 @@ class DeleteWebsitePost extends \Magento\Backend\Controller\Adminhtml\System\Sto - $redirectResult = $this->resultFactory->create(ResultFactory::TYPE_REDIRECT); - - if (!$model) { -- $this->messageManager->addError(__('Something went wrong. Please try again.')); -+ $this->messageManager->addErrorMessage(__('Something went wrong. Please try again.')); - return $redirectResult->setPath('adminhtml/*/'); - } - if (!$model->isCanDelete()) { -- $this->messageManager->addError(__('This website cannot be deleted.')); -+ $this->messageManager->addErrorMessage(__('This website cannot be deleted.')); - return $redirectResult->setPath('adminhtml/*/editWebsite', ['website_id' => $model->getId()]); - } - -@@ -37,12 +42,12 @@ class DeleteWebsitePost extends \Magento\Backend\Controller\Adminhtml\System\Sto - - try { - $model->delete(); -- $this->messageManager->addSuccess(__('You deleted the website.')); -+ $this->messageManager->addSuccessMessage(__('You deleted the website.')); - return $redirectResult->setPath('adminhtml/*/'); - } catch (\Magento\Framework\Exception\LocalizedException $e) { -- $this->messageManager->addError($e->getMessage()); -+ $this->messageManager->addErrorMessage($e->getMessage()); - } catch (\Exception $e) { -- $this->messageManager->addException($e, __('Unable to delete the website. Please try again later.')); -+ $this->messageManager->addExceptionMessage($e, __('Unable to delete the website. Please try again later.')); - } - return $redirectResult->setPath('*/*/editWebsite', ['website_id' => $itemId]); - } -diff --git a/app/code/Magento/Backend/Controller/Adminhtml/System/Store/EditStore.php b/app/code/Magento/Backend/Controller/Adminhtml/System/Store/EditStore.php -index cbc068a4808..e5cd43b521f 100644 ---- a/app/code/Magento/Backend/Controller/Adminhtml/System/Store/EditStore.php -+++ b/app/code/Magento/Backend/Controller/Adminhtml/System/Store/EditStore.php -@@ -6,7 +6,9 @@ - */ - namespace Magento\Backend\Controller\Adminhtml\System\Store; - --class EditStore extends \Magento\Backend\Controller\Adminhtml\System\Store -+use Magento\Framework\App\Action\HttpGetActionInterface as HttpGetActionInterface; -+ -+class EditStore extends \Magento\Backend\Controller\Adminhtml\System\Store implements HttpGetActionInterface - { - /** - * @return \Magento\Framework\Controller\ResultInterface -@@ -57,7 +59,7 @@ class EditStore extends \Magento\Backend\Controller\Adminhtml\System\Store - if ($model->getId() || $this->_coreRegistry->registry('store_action') == 'add') { - $this->_coreRegistry->register('store_data', $model); - if ($this->_coreRegistry->registry('store_action') == 'edit' && $codeBase && !$model->isReadOnly()) { -- $this->messageManager->addNotice($codeBase); -+ $this->messageManager->addNoticeMessage($codeBase); - } - $resultPage = $this->createPage(); - if ($this->_coreRegistry->registry('store_action') == 'add') { -@@ -71,7 +73,7 @@ class EditStore extends \Magento\Backend\Controller\Adminhtml\System\Store - )); - return $resultPage; - } else { -- $this->messageManager->addError($notExists); -+ $this->messageManager->addErrorMessage($notExists); - /** @var \Magento\Backend\Model\View\Result\Redirect $resultRedirect */ - $resultRedirect = $this->resultRedirectFactory->create(); - return $resultRedirect->setPath('adminhtml/*/'); -diff --git a/app/code/Magento/Backend/Controller/Adminhtml/System/Store/EditWebsite.php b/app/code/Magento/Backend/Controller/Adminhtml/System/Store/EditWebsite.php -index bfdf7cdbeb8..74ed7951e62 100644 ---- a/app/code/Magento/Backend/Controller/Adminhtml/System/Store/EditWebsite.php -+++ b/app/code/Magento/Backend/Controller/Adminhtml/System/Store/EditWebsite.php -@@ -6,7 +6,9 @@ - */ - namespace Magento\Backend\Controller\Adminhtml\System\Store; - --class EditWebsite extends \Magento\Backend\Controller\Adminhtml\System\Store -+use Magento\Framework\App\Action\HttpGetActionInterface as HttpGetActionInterface; -+ -+class EditWebsite extends \Magento\Backend\Controller\Adminhtml\System\Store implements HttpGetActionInterface - { - /** - * @return \Magento\Backend\Model\View\Result\Forward -diff --git a/app/code/Magento/Backend/Controller/Adminhtml/System/Store/Index.php b/app/code/Magento/Backend/Controller/Adminhtml/System/Store/Index.php -index b104704f41b..54da065c4af 100644 ---- a/app/code/Magento/Backend/Controller/Adminhtml/System/Store/Index.php -+++ b/app/code/Magento/Backend/Controller/Adminhtml/System/Store/Index.php -@@ -6,12 +6,13 @@ - */ - namespace Magento\Backend\Controller\Adminhtml\System\Store; - -+use Magento\Framework\App\Action\HttpGetActionInterface as HttpGetActionInterface; - use Magento\Framework\Controller\ResultFactory; - - /** - * Class Index returns Stores page - */ --class Index extends \Magento\Backend\Controller\Adminhtml\System\Store -+class Index extends \Magento\Backend\Controller\Adminhtml\System\Store implements HttpGetActionInterface - { - /** - * Returns Stores page -diff --git a/app/code/Magento/Backend/Controller/Adminhtml/System/Store/Save.php b/app/code/Magento/Backend/Controller/Adminhtml/System/Store/Save.php -index 8ca783f887e..b67f1f23f16 100644 ---- a/app/code/Magento/Backend/Controller/Adminhtml/System/Store/Save.php -+++ b/app/code/Magento/Backend/Controller/Adminhtml/System/Store/Save.php -@@ -6,12 +6,14 @@ - */ - namespace Magento\Backend\Controller\Adminhtml\System\Store; - -+use Magento\Framework\App\Action\HttpPostActionInterface as HttpPostActionInterface; -+ - /** - * Class Save - * - * Save controller for system entities such as: Store, StoreGroup, Website - */ --class Save extends \Magento\Backend\Controller\Adminhtml\System\Store -+class Save extends \Magento\Backend\Controller\Adminhtml\System\Store implements HttpPostActionInterface - { - /** - * Process Website model save -@@ -32,7 +34,7 @@ class Save extends \Magento\Backend\Controller\Adminhtml\System\Store - } - - $websiteModel->save(); -- $this->messageManager->addSuccess(__('You saved the website.')); -+ $this->messageManager->addSuccessMessage(__('You saved the website.')); - - return $postData; - } -@@ -68,7 +70,7 @@ class Save extends \Magento\Backend\Controller\Adminhtml\System\Store - ); - } - $storeModel->save(); -- $this->messageManager->addSuccess(__('You saved the store view.')); -+ $this->messageManager->addSuccessMessage(__('You saved the store view.')); - - return $postData; - } -@@ -98,7 +100,7 @@ class Save extends \Magento\Backend\Controller\Adminhtml\System\Store - ); - } - $groupModel->save(); -- $this->messageManager->addSuccess(__('You saved the store.')); -+ $this->messageManager->addSuccessMessage(__('You saved the store.')); - - return $postData; - } -@@ -134,10 +136,10 @@ class Save extends \Magento\Backend\Controller\Adminhtml\System\Store - $redirectResult->setPath('adminhtml/*/'); - return $redirectResult; - } catch (\Magento\Framework\Exception\LocalizedException $e) { -- $this->messageManager->addError($e->getMessage()); -+ $this->messageManager->addErrorMessage($e->getMessage()); - $this->_getSession()->setPostData($postData); - } catch (\Exception $e) { -- $this->messageManager->addException( -+ $this->messageManager->addExceptionMessage( - $e, - __('Something went wrong while saving. Please review the error log.') - ); -diff --git a/app/code/Magento/Backend/Model/AdminPathConfig.php b/app/code/Magento/Backend/Model/AdminPathConfig.php -index e7338adca4a..0e77835a513 100644 ---- a/app/code/Magento/Backend/Model/AdminPathConfig.php -+++ b/app/code/Magento/Backend/Model/AdminPathConfig.php -@@ -48,10 +48,7 @@ class AdminPathConfig implements PathConfigInterface - } - - /** -- * {@inheritdoc} -- * -- * @param \Magento\Framework\App\RequestInterface $request -- * @return string -+ * @inheritdoc - */ - public function getCurrentSecureUrl(\Magento\Framework\App\RequestInterface $request) - { -@@ -59,28 +56,29 @@ class AdminPathConfig implements PathConfigInterface - } - - /** -- * {@inheritdoc} -- * -- * @param string $path -- * @return bool -+ * @inheritdoc - */ - public function shouldBeSecure($path) - { -- return parse_url( -- (string)$this->coreConfig->getValue(Store::XML_PATH_UNSECURE_BASE_URL, 'default'), -- PHP_URL_SCHEME -- ) === 'https' -- || $this->backendConfig->isSetFlag(Store::XML_PATH_SECURE_IN_ADMINHTML) -- && parse_url( -- (string)$this->coreConfig->getValue(Store::XML_PATH_SECURE_BASE_URL, 'default'), -- PHP_URL_SCHEME -- ) === 'https'; -+ $baseUrl = (string)$this->coreConfig->getValue(Store::XML_PATH_UNSECURE_BASE_URL, 'default'); -+ if (parse_url($baseUrl, PHP_URL_SCHEME) === 'https') { -+ return true; -+ } -+ -+ if ($this->backendConfig->isSetFlag(Store::XML_PATH_SECURE_IN_ADMINHTML)) { -+ if ($this->backendConfig->isSetFlag('admin/url/use_custom')) { -+ $adminBaseUrl = (string)$this->coreConfig->getValue('admin/url/custom', 'default'); -+ } else { -+ $adminBaseUrl = (string)$this->coreConfig->getValue(Store::XML_PATH_SECURE_BASE_URL, 'default'); -+ } -+ return parse_url($adminBaseUrl, PHP_URL_SCHEME) === 'https'; -+ } -+ -+ return false; - } - - /** -- * {@inheritdoc} -- * -- * @return string -+ * @inheritdoc - */ - public function getDefaultPath() - { -diff --git a/app/code/Magento/Backend/Model/Auth/Session.php b/app/code/Magento/Backend/Model/Auth/Session.php -index 593b4219d45..61db71c1803 100644 ---- a/app/code/Magento/Backend/Model/Auth/Session.php -+++ b/app/code/Magento/Backend/Model/Auth/Session.php -@@ -5,21 +5,25 @@ - */ - namespace Magento\Backend\Model\Auth; - -+use Magento\Framework\Acl; -+use Magento\Framework\AclFactory; -+use Magento\Framework\App\ObjectManager; - use Magento\Framework\Stdlib\Cookie\CookieMetadataFactory; - use Magento\Framework\Stdlib\CookieManagerInterface; -+use Magento\Backend\Spi\SessionUserHydratorInterface; -+use Magento\Backend\Spi\SessionAclHydratorInterface; -+use Magento\User\Model\User; -+use Magento\User\Model\UserFactory; - - /** - * Backend Auth session model - * - * @api -- * @method \Magento\User\Model\User|null getUser() -- * @method \Magento\Backend\Model\Auth\Session setUser(\Magento\User\Model\User $value) -- * @method \Magento\Framework\Acl|null getAcl() -- * @method \Magento\Backend\Model\Auth\Session setAcl(\Magento\Framework\Acl $value) - * @method int getUpdatedAt() - * @method \Magento\Backend\Model\Auth\Session setUpdatedAt(int $value) - * - * @SuppressWarnings(PHPMD.CouplingBetweenObjects) -+ * @SuppressWarnings(PHPMD.CookieAndSessionMisuse) - * @todo implement solution that keeps is_first_visit flag in session during redirects - * @api - * @since 100.0.2 -@@ -55,6 +59,36 @@ class Session extends \Magento\Framework\Session\SessionManager implements \Mage - */ - protected $_config; - -+ /** -+ * @var SessionUserHydratorInterface -+ */ -+ private $userHydrator; -+ -+ /** -+ * @var SessionAclHydratorInterface -+ */ -+ private $aclHydrator; -+ -+ /** -+ * @var UserFactory -+ */ -+ private $userFactory; -+ -+ /** -+ * @var AclFactory -+ */ -+ private $aclFactory; -+ -+ /** -+ * @var User|null -+ */ -+ private $user; -+ -+ /** -+ * @var Acl|null -+ */ -+ private $acl; -+ - /** - * @param \Magento\Framework\App\Request\Http $request - * @param \Magento\Framework\Session\SidResolverInterface $sidResolver -@@ -69,6 +103,10 @@ class Session extends \Magento\Framework\Session\SessionManager implements \Mage - * @param \Magento\Backend\Model\UrlInterface $backendUrl - * @param \Magento\Backend\App\ConfigInterface $config - * @throws \Magento\Framework\Exception\SessionException -+ * @param SessionUserHydratorInterface|null $userHydrator -+ * @param SessionAclHydratorInterface|null $aclHydrator -+ * @param UserFactory|null $userFactory -+ * @param AclFactory|null $aclFactory - * @SuppressWarnings(PHPMD.ExcessiveParameterList) - */ - public function __construct( -@@ -83,11 +121,19 @@ class Session extends \Magento\Framework\Session\SessionManager implements \Mage - \Magento\Framework\App\State $appState, - \Magento\Framework\Acl\Builder $aclBuilder, - \Magento\Backend\Model\UrlInterface $backendUrl, -- \Magento\Backend\App\ConfigInterface $config -+ \Magento\Backend\App\ConfigInterface $config, -+ ?SessionUserHydratorInterface $userHydrator = null, -+ ?SessionAclHydratorInterface $aclHydrator = null, -+ ?UserFactory $userFactory = null, -+ ?AclFactory $aclFactory = null - ) { - $this->_config = $config; - $this->_aclBuilder = $aclBuilder; - $this->_backendUrl = $backendUrl; -+ $this->userHydrator = $userHydrator ?? ObjectManager::getInstance()->get(SessionUserHydratorInterface::class); -+ $this->aclHydrator = $aclHydrator ?? ObjectManager::getInstance()->get(SessionAclHydratorInterface::class); -+ $this->userFactory = $userFactory ?? ObjectManager::getInstance()->get(UserFactory::class); -+ $this->aclFactory = $aclFactory ?? ObjectManager::getInstance()->get(AclFactory::class); - parent::__construct( - $request, - $sidResolver, -@@ -230,6 +276,16 @@ class Session extends \Magento\Framework\Session\SessionManager implements \Mage - return $this; - } - -+ /** -+ * @inheritDoc -+ */ -+ public function destroy(array $options = null) -+ { -+ $this->user = null; -+ $this->acl = null; -+ parent::destroy($options); -+ } -+ - /** - * Process of configuring of current auth storage when logout was performed - * -@@ -253,4 +309,136 @@ class Session extends \Magento\Framework\Session\SessionManager implements \Mage - { - return true; - } -+ -+ /** -+ * Logged-in user. -+ * -+ * @return User|null -+ */ -+ public function getUser() -+ { -+ if (!$this->user) { -+ $userData = $this->getUserData(); -+ if ($userData) { -+ /** @var User $user */ -+ $user = $this->userFactory->create(); -+ $this->userHydrator->hydrate($user, $userData); -+ $this->user = $user; -+ } -+ } -+ -+ return $this->user; -+ } -+ -+ /** -+ * Set logged-in user instance. -+ * -+ * @param User|null $user -+ * @return Session -+ */ -+ public function setUser($user) -+ { -+ $this->setUserData(null); -+ if ($user) { -+ $this->setUserData($this->userHydrator->extract($user)); -+ } -+ $this->user = $user; -+ -+ return $this; -+ } -+ -+ /** -+ * Is user logged in? -+ * -+ * @return bool -+ */ -+ public function hasUser() -+ { -+ return $this->user || $this->hasUserData(); -+ } -+ -+ /** -+ * Remove logged-in user. -+ * -+ * @return Session -+ */ -+ public function unsUser() -+ { -+ $this->user = null; -+ return $this->unsUserData(); -+ } -+ -+ /** -+ * Logged-in user's ACL data. -+ * -+ * @return Acl|null -+ */ -+ public function getAcl() -+ { -+ if (!$this->acl) { -+ $aclData = $this->getUserAclData(); -+ if ($aclData) { -+ /** @var Acl $acl */ -+ $acl = $this->aclFactory->create(); -+ $this->aclHydrator->hydrate($acl, $aclData); -+ $this->acl = $acl; -+ } -+ } -+ -+ return $this->acl; -+ } -+ -+ /** -+ * Set logged-in user's ACL data instance. -+ * -+ * @param Acl|null $acl -+ * @return Session -+ */ -+ public function setAcl($acl) -+ { -+ $this->setUserAclData(null); -+ if ($acl) { -+ $this->setUserAclData($this->aclHydrator->extract($acl)); -+ } -+ $this->acl = $acl; -+ -+ return $this; -+ } -+ -+ /** -+ * Whether ACL data is present. -+ * -+ * @return bool -+ */ -+ public function hasAcl() -+ { -+ return $this->acl || $this->hasUserAclData(); -+ } -+ -+ /** -+ * Remove ACL data. -+ * -+ * @return Session -+ */ -+ public function unsAcl() -+ { -+ $this->acl = null; -+ return $this->unsUserAclData(); -+ } -+ -+ /** -+ * @inheritDoc -+ */ -+ public function writeClose() -+ { -+ //Updating data in session in case these objects has been changed. -+ if ($this->user) { -+ $this->setUser($this->user); -+ } -+ if ($this->acl) { -+ $this->setAcl($this->acl); -+ } -+ -+ parent::writeClose(); -+ } - } -diff --git a/app/code/Magento/Backend/Model/Auth/SessionAclHydrator.php b/app/code/Magento/Backend/Model/Auth/SessionAclHydrator.php -new file mode 100644 -index 00000000000..34e01be6966 ---- /dev/null -+++ b/app/code/Magento/Backend/Model/Auth/SessionAclHydrator.php -@@ -0,0 +1,36 @@ -+ $acl->_rules, 'resources' => $acl->_resources, 'roles' => $acl->_roleRegistry]; -+ } -+ -+ /** -+ * @inheritDoc -+ */ -+ public function hydrate(Acl $target, array $data): void -+ { -+ $target->_rules = $data['rules']; -+ $target->_resources = $data['resources']; -+ $target->_roleRegistry = $data['roles']; -+ } -+} -diff --git a/app/code/Magento/Backend/Model/Auth/SessionUserHydrator.php b/app/code/Magento/Backend/Model/Auth/SessionUserHydrator.php -new file mode 100644 -index 00000000000..6dee8b7b302 ---- /dev/null -+++ b/app/code/Magento/Backend/Model/Auth/SessionUserHydrator.php -@@ -0,0 +1,54 @@ -+roleFactory = $roleFactory; -+ } -+ -+ /** -+ * @inheritDoc -+ */ -+ public function extract(User $user): array -+ { -+ return ['data' => $user->getData(), 'role_data' => $user->getRole()->getData()]; -+ } -+ -+ /** -+ * @inheritDoc -+ */ -+ public function hydrate(User $target, array $data): void -+ { -+ $target->setData($data['data']); -+ /** @var Role $role */ -+ $role = $this->roleFactory->create(); -+ $role->setData($data['role_data']); -+ $target->setData('extracted_role', $role); -+ $target->getRole(); -+ } -+} -diff --git a/app/code/Magento/Backend/Model/Config/SessionLifetime/BackendModel.php b/app/code/Magento/Backend/Model/Config/SessionLifetime/BackendModel.php -index c106afb90a0..f6d08883d7a 100644 ---- a/app/code/Magento/Backend/Model/Config/SessionLifetime/BackendModel.php -+++ b/app/code/Magento/Backend/Model/Config/SessionLifetime/BackendModel.php -@@ -16,14 +16,17 @@ use Magento\Framework\Exception\LocalizedException; - */ - class BackendModel extends Value - { -- /** Maximum dmin session lifetime; 1 year*/ -+ /** Maximum admin session lifetime; 1 year*/ - const MAX_LIFETIME = 31536000; - - /** Minimum admin session lifetime */ - const MIN_LIFETIME = 60; - - /** -+ * Processing object before save data -+ * - * @since 100.1.0 -+ * @throws LocalizedException - */ - public function beforeSave() - { -diff --git a/app/code/Magento/Backend/Model/Image/UploadResizeConfig.php b/app/code/Magento/Backend/Model/Image/UploadResizeConfig.php -new file mode 100644 -index 00000000000..8155aa5e2fe ---- /dev/null -+++ b/app/code/Magento/Backend/Model/Image/UploadResizeConfig.php -@@ -0,0 +1,72 @@ -+config = $config; -+ } -+ -+ /** -+ * Get maximal width value for resized image -+ * -+ * @return int -+ */ -+ public function getMaxWidth(): int -+ { -+ return (int)$this->config->getValue(self::XML_PATH_MAX_WIDTH_IMAGE); -+ } -+ -+ /** -+ * Get maximal height value for resized image -+ * -+ * @return int -+ */ -+ public function getMaxHeight(): int -+ { -+ return (int)$this->config->getValue(self::XML_PATH_MAX_HEIGHT_IMAGE); -+ } -+ -+ /** -+ * Get config value for frontend resize -+ * -+ * @return bool -+ */ -+ public function isResizeEnabled(): bool -+ { -+ return (bool)$this->config->getValue(self::XML_PATH_ENABLE_RESIZE); -+ } -+} -diff --git a/app/code/Magento/Backend/Model/Image/UploadResizeConfigInterface.php b/app/code/Magento/Backend/Model/Image/UploadResizeConfigInterface.php -new file mode 100644 -index 00000000000..50582dfafbc ---- /dev/null -+++ b/app/code/Magento/Backend/Model/Image/UploadResizeConfigInterface.php -@@ -0,0 +1,37 @@ -+getChildren()->add($item, null, $index); - } else { -- $index = intval($index); -+ $index = (int) $index; - if (!isset($this[$index])) { - $this->offsetSet($index, $item); - $this->_logger->info( -diff --git a/app/code/Magento/Backend/Model/Menu/Builder.php b/app/code/Magento/Backend/Model/Menu/Builder.php -index ae572deab53..1c6e900bc5c 100644 ---- a/app/code/Magento/Backend/Model/Menu/Builder.php -+++ b/app/code/Magento/Backend/Model/Menu/Builder.php -@@ -102,6 +102,6 @@ class Builder - */ - protected function _getParam($params, $paramName, $defaultValue = null) - { -- return isset($params[$paramName]) ? $params[$paramName] : $defaultValue; -+ return $params[$paramName] ?? $defaultValue; - } - } -diff --git a/app/code/Magento/Backend/Model/Menu/Item.php b/app/code/Magento/Backend/Model/Menu/Item.php -index 67c6216cbbc..d535e9c84df 100644 ---- a/app/code/Magento/Backend/Model/Menu/Item.php -+++ b/app/code/Magento/Backend/Model/Menu/Item.php -@@ -145,7 +145,7 @@ class Item - protected $_moduleList; - - /** -- * @var \Magento\Framework\Module\Manager -+ * @var \Magento\Framework\Module\ModuleManagerInterface - */ - private $_moduleManager; - -@@ -163,7 +163,7 @@ class Item - * @param \Magento\Backend\Model\MenuFactory $menuFactory - * @param \Magento\Backend\Model\UrlInterface $urlModel - * @param \Magento\Framework\Module\ModuleListInterface $moduleList -- * @param \Magento\Framework\Module\Manager $moduleManager -+ * @param \Magento\Framework\Module\ModuleManagerInterface $moduleManager - * @param array $data - */ - public function __construct( -@@ -173,7 +173,7 @@ class Item - \Magento\Backend\Model\MenuFactory $menuFactory, - \Magento\Backend\Model\UrlInterface $urlModel, - \Magento\Framework\Module\ModuleListInterface $moduleList, -- \Magento\Framework\Module\Manager $moduleManager, -+ \Magento\Framework\Module\ModuleManagerInterface $moduleManager, - array $data = [] - ) { - $this->_validator = $validator; -diff --git a/app/code/Magento/Backend/Model/Search/Customer.php b/app/code/Magento/Backend/Model/Search/Customer.php -index 35a7359ce99..e76a1b77ab2 100644 ---- a/app/code/Magento/Backend/Model/Search/Customer.php -+++ b/app/code/Magento/Backend/Model/Search/Customer.php -@@ -89,7 +89,7 @@ class Customer extends \Magento\Framework\DataObject - - $this->searchCriteriaBuilder->setCurrentPage($this->getStart()); - $this->searchCriteriaBuilder->setPageSize($this->getLimit()); -- $searchFields = ['firstname', 'lastname', 'company']; -+ $searchFields = ['firstname', 'lastname', 'billing_company']; - $filters = []; - foreach ($searchFields as $field) { - $filters[] = $this->filterBuilder -diff --git a/app/code/Magento/Backend/Model/Session/Quote.php b/app/code/Magento/Backend/Model/Session/Quote.php -index 11edaa26f44..ed031287456 100644 ---- a/app/code/Magento/Backend/Model/Session/Quote.php -+++ b/app/code/Magento/Backend/Model/Session/Quote.php -@@ -24,6 +24,7 @@ use Magento\Customer\Api\GroupManagementInterface; - * @method Quote setOrderId($orderId) - * @method int getOrderId() - * @SuppressWarnings(PHPMD.CouplingBetweenObjects) -+ * @SuppressWarnings(PHPMD.CookieAndSessionMisuse) - * @since 100.0.2 - */ - class Quote extends \Magento\Framework\Session\SessionManager -@@ -149,7 +150,8 @@ class Quote extends \Magento\Framework\Session\SessionManager - $this->_quote = $this->quoteFactory->create(); - if ($this->getStoreId()) { - if (!$this->getQuoteId()) { -- $this->_quote->setCustomerGroupId($this->groupManagement->getDefaultGroup()->getId()); -+ $customerGroupId = $this->groupManagement->getDefaultGroup($this->getStoreId())->getId(); -+ $this->_quote->setCustomerGroupId($customerGroupId); - $this->_quote->setIsActive(false); - $this->_quote->setStoreId($this->getStoreId()); - -diff --git a/app/code/Magento/Backend/Model/Url.php b/app/code/Magento/Backend/Model/Url.php -index f199fd0fe7b..ba0c3d1cfac 100644 ---- a/app/code/Magento/Backend/Model/Url.php -+++ b/app/code/Magento/Backend/Model/Url.php -@@ -13,6 +13,7 @@ use Magento\Framework\App\ObjectManager; - * Class \Magento\Backend\Model\UrlInterface - * - * @SuppressWarnings(PHPMD.CouplingBetweenObjects) -+ * @SuppressWarnings(PHPMD.CookieAndSessionMisuse) - * @api - * @since 100.0.2 - */ -@@ -366,6 +367,19 @@ class Url extends \Magento\Framework\Url implements \Magento\Backend\Model\UrlIn - return $this->_menu; - } - -+ /** -+ * Set scope entity -+ * -+ * @param mixed $scopeId -+ * @return \Magento\Framework\UrlInterface -+ */ -+ public function setScope($scopeId) -+ { -+ parent::setScope($scopeId); -+ $this->_scope = $this->_scopeResolver->getScope($scopeId); -+ return $this; -+ } -+ - /** - * Set custom auth session - * -@@ -402,13 +416,13 @@ class Url extends \Magento\Framework\Url implements \Magento\Backend\Model\UrlIn - } - - /** -- * Retrieve action path. -- * Add backend area front name as a prefix to action path -+ * Retrieve action path, add backend area front name as a prefix to action path - * - * @return string - */ - protected function _getActionPath() - { -+ - $path = parent::_getActionPath(); - if ($path) { - if ($this->getAreaFrontName()) { -@@ -448,8 +462,7 @@ class Url extends \Magento\Framework\Url implements \Magento\Backend\Model\UrlIn - } - - /** -- * Get config data by path -- * Use only global config values for backend -+ * Get config data by path, use only global config values for backend - * - * @param string $path - * @return null|string -diff --git a/app/code/Magento/Backend/Spi/SessionAclHydratorInterface.php b/app/code/Magento/Backend/Spi/SessionAclHydratorInterface.php -new file mode 100644 -index 00000000000..7227cc92fcc ---- /dev/null -+++ b/app/code/Magento/Backend/Spi/SessionAclHydratorInterface.php -@@ -0,0 +1,34 @@ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -diff --git a/app/code/Magento/Backend/Test/Mftf/ActionGroup/AdminNavigateMenuActionGroup.xml b/app/code/Magento/Backend/Test/Mftf/ActionGroup/AdminNavigateMenuActionGroup.xml -new file mode 100644 -index 00000000000..8e0f5a06761 ---- /dev/null -+++ b/app/code/Magento/Backend/Test/Mftf/ActionGroup/AdminNavigateMenuActionGroup.xml -@@ -0,0 +1,19 @@ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -diff --git a/app/code/Magento/Backend/Test/Mftf/ActionGroup/AssertAdminDashboardPageIsVisibleActionGroup.xml b/app/code/Magento/Backend/Test/Mftf/ActionGroup/AssertAdminDashboardPageIsVisibleActionGroup.xml -new file mode 100644 -index 00000000000..1c86a736ac2 ---- /dev/null -+++ b/app/code/Magento/Backend/Test/Mftf/ActionGroup/AssertAdminDashboardPageIsVisibleActionGroup.xml -@@ -0,0 +1,15 @@ -+ -+ -+ -+ -+ -+ -+ -+ -+ -diff --git a/app/code/Magento/Backend/Test/Mftf/ActionGroup/AssertAdminPageIsNot404ActionGroup.xml b/app/code/Magento/Backend/Test/Mftf/ActionGroup/AssertAdminPageIsNot404ActionGroup.xml -new file mode 100644 -index 00000000000..eaf7c7cd8a6 ---- /dev/null -+++ b/app/code/Magento/Backend/Test/Mftf/ActionGroup/AssertAdminPageIsNot404ActionGroup.xml -@@ -0,0 +1,14 @@ -+ -+ -+ -+ -+ -+ -+ -+ -diff --git a/app/code/Magento/Backend/Test/Mftf/ActionGroup/AssertAdminSuccessLoginActionGroup.xml b/app/code/Magento/Backend/Test/Mftf/ActionGroup/AssertAdminSuccessLoginActionGroup.xml -new file mode 100644 -index 00000000000..844f58c789a ---- /dev/null -+++ b/app/code/Magento/Backend/Test/Mftf/ActionGroup/AssertAdminSuccessLoginActionGroup.xml -@@ -0,0 +1,14 @@ -+ -+ -+ -+ -+ -+ -+ -+ -diff --git a/app/code/Magento/Backend/Test/Mftf/ActionGroup/AssertMessageInAdminPanelActionGroup.xml b/app/code/Magento/Backend/Test/Mftf/ActionGroup/AssertMessageInAdminPanelActionGroup.xml -new file mode 100644 -index 00000000000..23823ea085a ---- /dev/null -+++ b/app/code/Magento/Backend/Test/Mftf/ActionGroup/AssertMessageInAdminPanelActionGroup.xml -@@ -0,0 +1,20 @@ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -diff --git a/app/code/Magento/Backend/Test/Mftf/ActionGroup/AssertMessageOnAdminLoginActionGroup.xml b/app/code/Magento/Backend/Test/Mftf/ActionGroup/AssertMessageOnAdminLoginActionGroup.xml -new file mode 100644 -index 00000000000..607fba3736c ---- /dev/null -+++ b/app/code/Magento/Backend/Test/Mftf/ActionGroup/AssertMessageOnAdminLoginActionGroup.xml -@@ -0,0 +1,20 @@ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -diff --git a/app/code/Magento/Backend/Test/Mftf/ActionGroup/AssertOrderGraphImageOnDashboardActionGroup.xml b/app/code/Magento/Backend/Test/Mftf/ActionGroup/AssertOrderGraphImageOnDashboardActionGroup.xml -new file mode 100644 -index 00000000000..3e3b0bc6a8a ---- /dev/null -+++ b/app/code/Magento/Backend/Test/Mftf/ActionGroup/AssertOrderGraphImageOnDashboardActionGroup.xml -@@ -0,0 +1,15 @@ -+ -+ -+ -+ -+ -+ -+ -+ -+ -diff --git a/app/code/Magento/Backend/Test/Mftf/ActionGroup/LoginActionGroup.xml b/app/code/Magento/Backend/Test/Mftf/ActionGroup/LoginActionGroup.xml -index bcff329d79d..9ba4430bafe 100644 ---- a/app/code/Magento/Backend/Test/Mftf/ActionGroup/LoginActionGroup.xml -+++ b/app/code/Magento/Backend/Test/Mftf/ActionGroup/LoginActionGroup.xml -@@ -7,7 +7,7 @@ - --> - - -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> - - - -diff --git a/app/code/Magento/Backend/Test/Mftf/ActionGroup/LoginAdminWithCredentialsActionGroup.xml b/app/code/Magento/Backend/Test/Mftf/ActionGroup/LoginAdminWithCredentialsActionGroup.xml -new file mode 100644 -index 00000000000..6aaa612b249 ---- /dev/null -+++ b/app/code/Magento/Backend/Test/Mftf/ActionGroup/LoginAdminWithCredentialsActionGroup.xml -@@ -0,0 +1,22 @@ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -diff --git a/app/code/Magento/Backend/Test/Mftf/ActionGroup/LoginAsAdminActionGroup.xml b/app/code/Magento/Backend/Test/Mftf/ActionGroup/LoginAsAdminActionGroup.xml -index 8a24ab2a2f1..b2fbadcbe38 100644 ---- a/app/code/Magento/Backend/Test/Mftf/ActionGroup/LoginAsAdminActionGroup.xml -+++ b/app/code/Magento/Backend/Test/Mftf/ActionGroup/LoginAsAdminActionGroup.xml -@@ -7,16 +7,15 @@ - --> - - -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> - - -- -+ - - -- -- -- -+ -+ - - - -- -\ No newline at end of file -+ -diff --git a/app/code/Magento/Backend/Test/Mftf/ActionGroup/LogoutActionGroup.xml b/app/code/Magento/Backend/Test/Mftf/ActionGroup/LogoutActionGroup.xml -index cdaf231e9dd..a4d922086df 100644 ---- a/app/code/Magento/Backend/Test/Mftf/ActionGroup/LogoutActionGroup.xml -+++ b/app/code/Magento/Backend/Test/Mftf/ActionGroup/LogoutActionGroup.xml -@@ -7,7 +7,7 @@ - --> - - -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> - - - -diff --git a/app/code/Magento/Backend/Test/Mftf/ActionGroup/SecondaryGridActionGroup.xml b/app/code/Magento/Backend/Test/Mftf/ActionGroup/SecondaryGridActionGroup.xml -index 9fe5f54f1db..6f27b03e4df 100644 ---- a/app/code/Magento/Backend/Test/Mftf/ActionGroup/SecondaryGridActionGroup.xml -+++ b/app/code/Magento/Backend/Test/Mftf/ActionGroup/SecondaryGridActionGroup.xml -@@ -7,7 +7,7 @@ - --> - - -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> - - - -diff --git a/app/code/Magento/Backend/Test/Mftf/ActionGroup/SetAdminAccountActionGroup.xml b/app/code/Magento/Backend/Test/Mftf/ActionGroup/SetAdminAccountActionGroup.xml -new file mode 100644 -index 00000000000..a0492ac9b95 ---- /dev/null -+++ b/app/code/Magento/Backend/Test/Mftf/ActionGroup/SetAdminAccountActionGroup.xml -@@ -0,0 +1,25 @@ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -diff --git a/app/code/Magento/Backend/Test/Mftf/ActionGroup/SetWebsiteCountryOptionsToDefaultActionGroup.xml b/app/code/Magento/Backend/Test/Mftf/ActionGroup/SetWebsiteCountryOptionsToDefaultActionGroup.xml -new file mode 100644 -index 00000000000..4519648eb1d ---- /dev/null -+++ b/app/code/Magento/Backend/Test/Mftf/ActionGroup/SetWebsiteCountryOptionsToDefaultActionGroup.xml -@@ -0,0 +1,20 @@ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -diff --git a/app/code/Magento/Backend/Test/Mftf/ActionGroup/SortByIdDescendingActionGroup.xml b/app/code/Magento/Backend/Test/Mftf/ActionGroup/SortByIdDescendingActionGroup.xml -index b7b63c5d9a6..fd353964bae 100644 ---- a/app/code/Magento/Backend/Test/Mftf/ActionGroup/SortByIdDescendingActionGroup.xml -+++ b/app/code/Magento/Backend/Test/Mftf/ActionGroup/SortByIdDescendingActionGroup.xml -@@ -7,7 +7,7 @@ - --> - - -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> - - - -diff --git a/app/code/Magento/Backend/Test/Mftf/Data/AdminMenuData.xml b/app/code/Magento/Backend/Test/Mftf/Data/AdminMenuData.xml -new file mode 100644 -index 00000000000..4fe600d194e ---- /dev/null -+++ b/app/code/Magento/Backend/Test/Mftf/Data/AdminMenuData.xml -@@ -0,0 +1,46 @@ -+ -+ -+ -+ -+ -+ Content -+ Content -+ magento-backend-content -+ -+ -+ Store Design Schedule -+ Schedule -+ magento-backend-system-design-schedule -+ -+ -+ Dashboard -+ Dashboard -+ magento-backend-dashboard -+ -+ -+ Stores -+ Stores -+ magento-backend-stores -+ -+ -+ Stores -+ All Stores -+ magento-backend-system-store -+ -+ -+ Configuration -+ Configuration -+ magento-config-system-config -+ -+ -+ Cache Management -+ Cache Management -+ magento-backend-system-cache -+ -+ -diff --git a/app/code/Magento/Backend/Test/Mftf/Data/BackenedData.xml b/app/code/Magento/Backend/Test/Mftf/Data/BackenedData.xml -index 286685315a7..016e936977c 100644 ---- a/app/code/Magento/Backend/Test/Mftf/Data/BackenedData.xml -+++ b/app/code/Magento/Backend/Test/Mftf/Data/BackenedData.xml -@@ -7,7 +7,7 @@ - --> - - -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataProfileSchema.xsd"> - - data - -diff --git a/app/code/Magento/Backend/Test/Mftf/Data/CookieConfigData.xml b/app/code/Magento/Backend/Test/Mftf/Data/CookieConfigData.xml -new file mode 100644 -index 00000000000..52a6c27a37e ---- /dev/null -+++ b/app/code/Magento/Backend/Test/Mftf/Data/CookieConfigData.xml -@@ -0,0 +1,23 @@ -+ -+ -+ -+ -+ -+ web/cookie/cookie_domain -+ website -+ base -+ testDomain.com -+ -+ -+ web/cookie/cookie_domain -+ website -+ base -+ '' -+ -+ -diff --git a/app/code/Magento/Backend/Test/Mftf/Page/AdminConfigurationStoresPage.xml b/app/code/Magento/Backend/Test/Mftf/Page/AdminConfigurationStoresPage.xml -index a53938d5346..8afc2c5bbb3 100644 ---- a/app/code/Magento/Backend/Test/Mftf/Page/AdminConfigurationStoresPage.xml -+++ b/app/code/Magento/Backend/Test/Mftf/Page/AdminConfigurationStoresPage.xml -@@ -7,11 +7,14 @@ - --> - - -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/PageObject.xsd"> - -
- - -
- -+ -+
-+ - -diff --git a/app/code/Magento/Backend/Test/Mftf/Page/AdminDashboardPage.xml b/app/code/Magento/Backend/Test/Mftf/Page/AdminDashboardPage.xml -new file mode 100644 -index 00000000000..0e95d5c139a ---- /dev/null -+++ b/app/code/Magento/Backend/Test/Mftf/Page/AdminDashboardPage.xml -@@ -0,0 +1,15 @@ -+ -+ -+ -+ -+ -+
-+
-+ -+ -diff --git a/app/code/Magento/Backend/Test/Mftf/Page/AdminForgotPasswordPage.xml b/app/code/Magento/Backend/Test/Mftf/Page/AdminForgotPasswordPage.xml -new file mode 100644 -index 00000000000..84af56d102d ---- /dev/null -+++ b/app/code/Magento/Backend/Test/Mftf/Page/AdminForgotPasswordPage.xml -@@ -0,0 +1,14 @@ -+ -+ -+ -+ -+ -+
-+ -+ -diff --git a/app/code/Magento/Backend/Test/Mftf/Page/AdminLoginPage.xml b/app/code/Magento/Backend/Test/Mftf/Page/AdminLoginPage.xml -index ca0797f7ded..78226d79273 100644 ---- a/app/code/Magento/Backend/Test/Mftf/Page/AdminLoginPage.xml -+++ b/app/code/Magento/Backend/Test/Mftf/Page/AdminLoginPage.xml -@@ -7,8 +7,9 @@ - --> - - -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/PageObject.xsd"> - -+
-
- - -diff --git a/app/code/Magento/Backend/Test/Mftf/Page/AdminLogoutPage.xml b/app/code/Magento/Backend/Test/Mftf/Page/AdminLogoutPage.xml -index 75ef114ec64..713199771e8 100644 ---- a/app/code/Magento/Backend/Test/Mftf/Page/AdminLogoutPage.xml -+++ b/app/code/Magento/Backend/Test/Mftf/Page/AdminLogoutPage.xml -@@ -7,6 +7,6 @@ - --> - - -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/PageObject.xsd"> - - -diff --git a/app/code/Magento/Backend/Test/Mftf/Page/AdminSystemAccountPage.xml b/app/code/Magento/Backend/Test/Mftf/Page/AdminSystemAccountPage.xml -new file mode 100644 -index 00000000000..2f04c2c11d2 ---- /dev/null -+++ b/app/code/Magento/Backend/Test/Mftf/Page/AdminSystemAccountPage.xml -@@ -0,0 +1,14 @@ -+ -+ -+ -+ -+ -+
-+ -+ -diff --git a/app/code/Magento/Backend/Test/Mftf/Section/AdminConfirmationModalSection.xml b/app/code/Magento/Backend/Test/Mftf/Section/AdminConfirmationModalSection.xml -index dc512e66528..2ec25da4619 100644 ---- a/app/code/Magento/Backend/Test/Mftf/Section/AdminConfirmationModalSection.xml -+++ b/app/code/Magento/Backend/Test/Mftf/Section/AdminConfirmationModalSection.xml -@@ -7,7 +7,7 @@ - --> - - -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> -
- - -diff --git a/app/code/Magento/Backend/Test/Mftf/Section/AdminDashboardSection.xml b/app/code/Magento/Backend/Test/Mftf/Section/AdminDashboardSection.xml -new file mode 100644 -index 00000000000..61fe7ffa48e ---- /dev/null -+++ b/app/code/Magento/Backend/Test/Mftf/Section/AdminDashboardSection.xml -@@ -0,0 +1,21 @@ -+ -+ -+ -+ -+
-+ -+ -+ -+ -+ -+ -+ -+ -+
-+
-diff --git a/app/code/Magento/Backend/Test/Mftf/Section/AdminForgotPasswordFormSection.xml b/app/code/Magento/Backend/Test/Mftf/Section/AdminForgotPasswordFormSection.xml -new file mode 100644 -index 00000000000..efaca221233 ---- /dev/null -+++ b/app/code/Magento/Backend/Test/Mftf/Section/AdminForgotPasswordFormSection.xml -@@ -0,0 +1,15 @@ -+ -+ -+ -+ -+
-+ -+ -+
-+
-diff --git a/app/code/Magento/Backend/Test/Mftf/Section/AdminGridTableSection.xml b/app/code/Magento/Backend/Test/Mftf/Section/AdminGridTableSection.xml -index 3e8f8a8f2e4..cc92e530cf3 100644 ---- a/app/code/Magento/Backend/Test/Mftf/Section/AdminGridTableSection.xml -+++ b/app/code/Magento/Backend/Test/Mftf/Section/AdminGridTableSection.xml -@@ -7,7 +7,7 @@ - --> - - -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> -
- -
-diff --git a/app/code/Magento/Backend/Test/Mftf/Section/AdminHeaderSection.xml b/app/code/Magento/Backend/Test/Mftf/Section/AdminHeaderSection.xml -index 92b06878ab8..186bb183d68 100644 ---- a/app/code/Magento/Backend/Test/Mftf/Section/AdminHeaderSection.xml -+++ b/app/code/Magento/Backend/Test/Mftf/Section/AdminHeaderSection.xml -@@ -7,8 +7,13 @@ - --> - - -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> -
- -+ -+ -+ -+ -+ -
-
-diff --git a/app/code/Magento/Backend/Test/Mftf/Section/AdminLoginFormSection.xml b/app/code/Magento/Backend/Test/Mftf/Section/AdminLoginFormSection.xml -index b65a969e334..bd65dea89ab 100644 ---- a/app/code/Magento/Backend/Test/Mftf/Section/AdminLoginFormSection.xml -+++ b/app/code/Magento/Backend/Test/Mftf/Section/AdminLoginFormSection.xml -@@ -7,10 +7,11 @@ - --> - - -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> -
- - - -+ -
-
-diff --git a/app/code/Magento/Backend/Test/Mftf/Section/AdminLoginMessagesSection.xml b/app/code/Magento/Backend/Test/Mftf/Section/AdminLoginMessagesSection.xml -new file mode 100644 -index 00000000000..f6ada50ada3 ---- /dev/null -+++ b/app/code/Magento/Backend/Test/Mftf/Section/AdminLoginMessagesSection.xml -@@ -0,0 +1,14 @@ -+ -+ -+ -+ -+
-+ -+
-+
-diff --git a/app/code/Magento/Backend/Test/Mftf/Section/AdminMainActionsSection.xml b/app/code/Magento/Backend/Test/Mftf/Section/AdminMainActionsSection.xml -index f8d259cc8e4..4867b5ba5ae 100644 ---- a/app/code/Magento/Backend/Test/Mftf/Section/AdminMainActionsSection.xml -+++ b/app/code/Magento/Backend/Test/Mftf/Section/AdminMainActionsSection.xml -@@ -7,9 +7,11 @@ - --> - - -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> -
-- -- -+ -+ -+ -+ -
-
-diff --git a/app/code/Magento/Backend/Test/Mftf/Section/AdminMenuSection.xml b/app/code/Magento/Backend/Test/Mftf/Section/AdminMenuSection.xml -new file mode 100644 -index 00000000000..8498ad8c52e ---- /dev/null -+++ b/app/code/Magento/Backend/Test/Mftf/Section/AdminMenuSection.xml -@@ -0,0 +1,28 @@ -+ -+ -+ -+ -+
-+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+
-+
-diff --git a/app/code/Magento/Backend/Test/Mftf/Section/AdminMessagesSection.xml b/app/code/Magento/Backend/Test/Mftf/Section/AdminMessagesSection.xml -index ff5e02397cb..be3ef92acf0 100644 ---- a/app/code/Magento/Backend/Test/Mftf/Section/AdminMessagesSection.xml -+++ b/app/code/Magento/Backend/Test/Mftf/Section/AdminMessagesSection.xml -@@ -7,10 +7,12 @@ - --> - - -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> -
- - - -+ -+ -
-
-diff --git a/app/code/Magento/Backend/Test/Mftf/Section/AdminSecondaryGridSection.xml b/app/code/Magento/Backend/Test/Mftf/Section/AdminSecondaryGridSection.xml -index ea84ce7ea0c..9051eb747a7 100644 ---- a/app/code/Magento/Backend/Test/Mftf/Section/AdminSecondaryGridSection.xml -+++ b/app/code/Magento/Backend/Test/Mftf/Section/AdminSecondaryGridSection.xml -@@ -7,7 +7,7 @@ - --> - - -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> -
- - -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 00000000000..2f799721a8c ---- /dev/null -+++ b/app/code/Magento/Backend/Test/Mftf/Section/AdminSlideOutDialogSection.xml -@@ -0,0 +1,16 @@ -+ -+ -+ -+
-+ -+ -+ -+ -+
-+
-diff --git a/app/code/Magento/Backend/Test/Mftf/Section/AdminSystemAccountSection.xml b/app/code/Magento/Backend/Test/Mftf/Section/AdminSystemAccountSection.xml -new file mode 100644 -index 00000000000..a78b381b6e7 ---- /dev/null -+++ b/app/code/Magento/Backend/Test/Mftf/Section/AdminSystemAccountSection.xml -@@ -0,0 +1,15 @@ -+ -+ -+ -+ -+
-+ -+ -+
-+
-diff --git a/app/code/Magento/Backend/Test/Mftf/Section/CountryOptionsSection.xml b/app/code/Magento/Backend/Test/Mftf/Section/CountryOptionsSection.xml -new file mode 100644 -index 00000000000..2e2e5aec35e ---- /dev/null -+++ b/app/code/Magento/Backend/Test/Mftf/Section/CountryOptionsSection.xml -@@ -0,0 +1,18 @@ -+ -+ -+ -+ -+
-+ -+ -+ -+ -+ -+
-+
-diff --git a/app/code/Magento/Backend/Test/Mftf/Section/LocaleOptionsSection.xml b/app/code/Magento/Backend/Test/Mftf/Section/LocaleOptionsSection.xml -new file mode 100644 -index 00000000000..a460aaebf10 ---- /dev/null -+++ b/app/code/Magento/Backend/Test/Mftf/Section/LocaleOptionsSection.xml -@@ -0,0 +1,16 @@ -+ -+ -+ -+ -+
-+ -+ -+ -+
-+
-diff --git a/app/code/Magento/Backend/Test/Mftf/Test/AdminAttributeTextSwatchesCanBeFiledTest.xml b/app/code/Magento/Backend/Test/Mftf/Test/AdminAttributeTextSwatchesCanBeFiledTest.xml -new file mode 100644 -index 00000000000..2c061e54f55 ---- /dev/null -+++ b/app/code/Magento/Backend/Test/Mftf/Test/AdminAttributeTextSwatchesCanBeFiledTest.xml -@@ -0,0 +1,116 @@ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ <description value="Check that attribute text swatches can be filed"/> -+ <severity value="MAJOR"/> -+ <testCaseId value="MAGETWO-96710"/> -+ <useCaseId value="MAGETWO-96409"/> -+ <group value="backend"/> -+ <group value="ui"/> -+ </annotations> -+ <before> -+ -+ <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> -+ -+ </before> -+ <after> -+ <!-- Delete all 10 store views --> -+ <actionGroup ref="AdminDeleteStoreViewActionGroup" stepKey="deleteStoreView1"> -+ <argument name="customStore" value="customStore"/> -+ </actionGroup> -+ <actionGroup ref="AdminDeleteStoreViewActionGroup" stepKey="deleteStoreView2"> -+ <argument name="customStore" value="NewStoreViewData"/> -+ </actionGroup> -+ <actionGroup ref="AdminDeleteStoreViewActionGroup" stepKey="deleteStoreView3"> -+ <argument name="customStore" value="storeViewData"/> -+ </actionGroup> -+ <actionGroup ref="AdminDeleteStoreViewActionGroup" stepKey="deleteStoreView4"> -+ <argument name="customStore" value="storeViewData1"/> -+ </actionGroup> -+ <actionGroup ref="AdminDeleteStoreViewActionGroup" stepKey="deleteStoreView5"> -+ <argument name="customStore" value="storeViewData2"/> -+ </actionGroup> -+ <actionGroup ref="AdminDeleteStoreViewActionGroup" stepKey="deleteStoreView6"> -+ <argument name="customStore" value="storeViewData3"/> -+ </actionGroup> -+ <actionGroup ref="AdminDeleteStoreViewActionGroup" stepKey="deleteStoreView7"> -+ <argument name="customStore" value="storeViewData4"/> -+ </actionGroup> -+ <actionGroup ref="AdminDeleteStoreViewActionGroup" stepKey="deleteStoreView8"> -+ <argument name="customStore" value="storeViewData5"/> -+ </actionGroup> -+ <actionGroup ref="AdminDeleteStoreViewActionGroup" stepKey="deleteStoreView9"> -+ <argument name="customStore" value="storeViewData6"/> -+ </actionGroup> -+ <actionGroup ref="AdminDeleteStoreViewActionGroup" stepKey="deleteStoreView10"> -+ <argument name="customStore" value="storeViewData7"/> -+ </actionGroup> -+ -+ <actionGroup ref="logout" stepKey="logout"/> -+ </after> -+ -+ <!-- Create 10 store views --> -+ <actionGroup ref="AdminCreateStoreViewActionGroup" stepKey="createStoreView1"> -+ <argument name="customStore" value="customStore"/> -+ </actionGroup> -+ <actionGroup ref="AdminCreateStoreViewActionGroup" stepKey="createStoreView2"> -+ <argument name="customStore" value="NewStoreViewData"/> -+ </actionGroup> -+ <actionGroup ref="AdminCreateStoreViewActionGroup" stepKey="createStoreView3"> -+ <argument name="customStore" value="storeViewData"/> -+ </actionGroup> -+ <actionGroup ref="AdminCreateStoreViewActionGroup" stepKey="createStoreView4"> -+ <argument name="customStore" value="storeViewData1"/> -+ </actionGroup> -+ <actionGroup ref="AdminCreateStoreViewActionGroup" stepKey="createStoreView5"> -+ <argument name="customStore" value="storeViewData2"/> -+ </actionGroup> -+ <actionGroup ref="AdminCreateStoreViewActionGroup" stepKey="createStoreView6"> -+ <argument name="customStore" value="storeViewData3"/> -+ </actionGroup> -+ <actionGroup ref="AdminCreateStoreViewActionGroup" stepKey="createStoreView7"> -+ <argument name="customStore" value="storeViewData4"/> -+ </actionGroup> -+ <actionGroup ref="AdminCreateStoreViewActionGroup" stepKey="createStoreView8"> -+ <argument name="customStore" value="storeViewData5"/> -+ </actionGroup> -+ <actionGroup ref="AdminCreateStoreViewActionGroup" stepKey="createStoreView9"> -+ <argument name="customStore" value="storeViewData6"/> -+ </actionGroup> -+ <actionGroup ref="AdminCreateStoreViewActionGroup" stepKey="createStoreView10"> -+ <argument name="customStore" value="storeViewData7"/> -+ </actionGroup> -+ -+ <!--Navigate to Product attribute page--> -+ <amOnPage url="{{ProductAttributePage.url}}" stepKey="navigateToNewProductAttributePage"/> -+ <waitForPageLoad stepKey="waitForPageLoad"/> -+ <fillField userInput="test_label" selector="{{AttributePropertiesSection.DefaultLabel}}" stepKey="fillDefaultLabel"/> -+ <selectOption selector="{{AttributePropertiesSection.InputType}}" userInput="Text Swatch" stepKey="selectInputType"/> -+ <click selector="{{AttributePropertiesSection.addSwatch}}" stepKey="clickAddSwatch"/> -+ <waitForAjaxLoad stepKey="waitForAjaxLoad"/> -+ -+ <!-- Fill Swatch and Description fields for Admin --> -+ <fillField selector="{{AttributeManageSwatchSection.swatchField('Admin')}}" userInput="test" stepKey="fillSwatchForAdmin"/> -+ <fillField selector="{{AttributeManageSwatchSection.descriptionField('Admin')}}" userInput="test" stepKey="fillDescriptionForAdmin"/> -+ -+ <!-- Grab value Swatch and Description fields for Admin --> -+ <grabValueFrom selector="{{AttributeManageSwatchSection.swatchField('Admin')}}" stepKey="grabSwatchForAdmin"/> -+ <grabValueFrom selector="{{AttributeManageSwatchSection.descriptionField('Admin')}}" stepKey="grabDescriptionForAdmin"/> -+ -+ <!-- Check that Swatch and Description fields for Admin are not empty--> -+ <assertNotEmpty actual="$grabSwatchForAdmin" stepKey="checkSwatchFieldForAdmin"/> -+ <assertNotEmpty actual="$grabDescriptionForAdmin" stepKey="checkDescriptionFieldForAdmin"/> -+ </test> -+</tests> -diff --git a/app/code/Magento/Backend/Test/Mftf/Test/AdminContentScheduleNavigateMenuTest.xml b/app/code/Magento/Backend/Test/Mftf/Test/AdminContentScheduleNavigateMenuTest.xml -new file mode 100644 -index 00000000000..bead59653ee ---- /dev/null -+++ b/app/code/Magento/Backend/Test/Mftf/Test/AdminContentScheduleNavigateMenuTest.xml -@@ -0,0 +1,36 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> -+ <test name="AdminContentScheduleNavigateMenuTest"> -+ <annotations> -+ <features value="Backend"/> -+ <stories value="Menu Navigation"/> -+ <title value="Admin content schedule navigate menu test"/> -+ <description value="Admin should be able to navigate to Content > Schedule"/> -+ <severity value="CRITICAL"/> -+ <testCaseId value="MC-14117"/> -+ <group value="menu"/> -+ <group value="mtf_migrated"/> -+ </annotations> -+ <before> -+ <actionGroup ref="LoginAsAdmin" stepKey="LoginAsAdmin"/> -+ </before> -+ <after> -+ <actionGroup ref="logout" stepKey="logout"/> -+ </after> -+ <actionGroup ref="AdminNavigateMenuActionGroup" stepKey="navigateToContentSchedulePage"> -+ <argument name="menuUiId" value="{{AdminMenuContent.dataUiId}}"/> -+ <argument name="submenuUiId" value="{{AdminMenuContentDesignSchedule.dataUiId}}"/> -+ </actionGroup> -+ <actionGroup ref="AdminAssertPageTitleActionGroup" stepKey="seePageTitle"> -+ <argument name="title" value="{{AdminMenuContentDesignSchedule.pageTitle}}"/> -+ </actionGroup> -+ </test> -+</tests> -diff --git a/app/code/Magento/Backend/Test/Mftf/Test/AdminDashboardNavigateMenuTest.xml b/app/code/Magento/Backend/Test/Mftf/Test/AdminDashboardNavigateMenuTest.xml -new file mode 100644 -index 00000000000..33561d7c3b0 ---- /dev/null -+++ b/app/code/Magento/Backend/Test/Mftf/Test/AdminDashboardNavigateMenuTest.xml -@@ -0,0 +1,33 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> -+ <test name="AdminDashboardNavigateMenuTest"> -+ <annotations> -+ <features value="Backend"/> -+ <stories value="Menu Navigation"/> -+ <title value="Admin dashboard navigate menu test"/> -+ <description value="Admin should be able to navigate to Dashboard"/> -+ <severity value="CRITICAL"/> -+ <testCaseId value="MC-14116"/> -+ <group value="menu"/> -+ <group value="mtf_migrated"/> -+ </annotations> -+ <before> -+ <actionGroup ref="LoginAsAdmin" stepKey="LoginAsAdmin"/> -+ </before> -+ <after> -+ <actionGroup ref="logout" stepKey="logout"/> -+ </after> -+ <click selector="{{AdminMenuSection.menuItem(AdminMenuDashboard.dataUiId)}}" stepKey="clickOnMenuItem"/> -+ <actionGroup ref="AdminAssertPageTitleActionGroup" stepKey="seePageTitle"> -+ <argument name="title" value="{{AdminMenuDashboard.pageTitle}}"/> -+ </actionGroup> -+ </test> -+</tests> -diff --git a/app/code/Magento/Backend/Test/Mftf/Test/AdminDashboardWithChartsChart.xml b/app/code/Magento/Backend/Test/Mftf/Test/AdminDashboardWithChartsChart.xml -new file mode 100644 -index 00000000000..f48c7752efc ---- /dev/null -+++ b/app/code/Magento/Backend/Test/Mftf/Test/AdminDashboardWithChartsChart.xml -@@ -0,0 +1,125 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> -+ <test name="AdminDashboardWithChartsTest"> -+ <annotations> -+ <features value="Backend"/> -+ <stories value="Google Charts on Magento dashboard"/> -+ <title value="Admin should see Google chart on Magento dashboard"/> -+ <description value="Google chart on Magento dashboard page is displaying properly"/> -+ <severity value="MAJOR"/> -+ <testCaseId value="MAGETWO-98934"/> -+ <useCaseId value="MAGETWO-98584"/> -+ <group value="backend"/> -+ </annotations> -+ <before> -+ <magentoCLI command="config:set admin/dashboard/enable_charts 1" stepKey="setEnableCharts"/> -+ <createData entity="SimpleProduct2" stepKey="createProduct"> -+ <field key="price">150</field> -+ </createData> -+ <createData entity="Simple_US_Customer" stepKey="createCustomer"> -+ <field key="firstname">John1</field> -+ <field key="lastname">Doe1</field> -+ </createData> -+ </before> -+ <after> -+ <!-- Reset admin order filter --> -+ <comment userInput="Reset admin order filter" stepKey="resetAdminOrderFilter"/> -+ <actionGroup ref="AdminOrdersGridClearFiltersActionGroup" stepKey="clearOrderFilters"/> -+ <waitForLoadingMaskToDisappear stepKey="waitForLoadingOrderGrid"/> -+ <magentoCLI command="config:set admin/dashboard/enable_charts 0" stepKey="setDisableChartsAsDefault"/> -+ <deleteData createDataKey="createProduct" stepKey="deleteProduct"/> -+ <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> -+ <actionGroup ref="logout" stepKey="logout"/> -+ </after> -+ <!-- Login as admin --> -+ <comment userInput="Login as admin" stepKey="adminLogin"/> -+ <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> -+ <!-- Grab quantity value --> -+ <comment userInput="Grab quantity value from dashboard" stepKey="grabQuantityFromDashboard"/> -+ <grabTextFrom selector="{{AdminDashboardSection.dashboardTotals('Quantity')}}" stepKey="grabStartQuantity"/> -+ <!-- Login as customer --> -+ <comment userInput="Login as customer" stepKey="loginAsCustomer"/> -+ <actionGroup ref="LoginToStorefrontActionGroup" stepKey="customerLogin"> -+ <argument name="Customer" value="$$createCustomer$$"/> -+ </actionGroup> -+ <!-- Add Product to Shopping Cart--> -+ <comment userInput="Add product to the shopping cart" stepKey="addProductToCart"/> -+ <amOnPage url="{{StorefrontProductPage.url($$createProduct.custom_attributes[url_key]$$)}}" stepKey="navigateToSimpleProductPage"/> -+ <waitForPageLoad stepKey="waitForProductPageLoad"/> -+ <actionGroup ref="addToCartFromStorefrontProductPage" stepKey="addToCartFromStorefrontProductPage"> -+ <argument name="productName" value="$$createProduct.name$$"/> -+ </actionGroup> -+ <!--Go to Checkout--> -+ <comment userInput="Go to checkout" stepKey="goToCheckout"/> -+ <actionGroup ref="GoToCheckoutFromMinicartActionGroup" stepKey="goToCheckoutFromMinicart"/> -+ <waitForLoadingMaskToDisappear stepKey="waitForLoadingCheckoutPageWithShippingMethod"/> -+ <click selector="{{CheckoutShippingMethodsSection.firstShippingMethod}}" stepKey="selectFirstShippingMethod"/> -+ <waitForLoadingMaskToDisappear stepKey="waitForLoadingMask1"/> -+ <waitForElement selector="{{CheckoutShippingMethodsSection.next}}" time="30" stepKey="waitForNextButton"/> -+ <click selector="{{CheckoutShippingMethodsSection.next}}" stepKey="clickNext"/> -+ <!-- Checkout select Check/Money Order payment --> -+ <comment userInput="Select Check/Money payment" stepKey="checkoutSelectCheckMoneyPayment"/> -+ <actionGroup ref="CheckoutSelectCheckMoneyOrderPaymentActionGroup" stepKey="selectCheckMoneyPayment"/> -+ <!-- Place Order --> -+ <comment userInput="Place order" stepKey="placeOrder"/> -+ <click selector="{{CheckoutPaymentSection.placeOrder}}" stepKey="clickPlaceOrder"/> -+ <see selector="{{CheckoutSuccessMainSection.successTitle}}" userInput="Thank you for your purchase!" stepKey="seeSuccessTitle"/> -+ <see selector="{{CheckoutSuccessMainSection.orderNumberText}}" userInput="Your order number is: " stepKey="seeOrderNumber"/> -+ <grabTextFrom selector="{{CheckoutSuccessMainSection.orderNumber22}}" stepKey="grabOrderNumber"/> -+ <!-- Search for Order in the order grid --> -+ <comment userInput="Search for Order in the order grid" stepKey="searchOrderInGrid"/> -+ <actionGroup ref="filterOrderGridById" stepKey="filterOrderGridById"> -+ <argument name="orderId" value="$grabOrderNumber"/> -+ </actionGroup> -+ <waitForLoadingMaskToDisappear stepKey="waitForSearchingOrder"/> -+ <!-- Create invoice --> -+ <comment userInput="Create invoice" stepKey="createInvoice"/> -+ <click selector="{{AdminOrdersGridSection.firstRow}}" stepKey="clickOrderRow"/> -+ <waitForPageLoad stepKey="waitForOrderPageToLoad"/> -+ <click selector="{{AdminOrderDetailsMainActionsSection.invoice}}" stepKey="clickInvoiceButton"/> -+ <waitForPageLoad stepKey="waitForInvoicePageToLoad"/> -+ <see selector="{{AdminHeaderSection.pageTitle}}" userInput="New Invoice" stepKey="seeNewInvoiceInPageTitle" after="clickInvoiceButton"/> -+ <see selector="{{AdminInvoiceTotalSection.total('Subtotal')}}" userInput="$150.00" stepKey="seeCorrectGrandTotal"/> -+ <click selector="{{AdminInvoiceMainActionsSection.submitInvoice}}" stepKey="clickSubmitInvoice"/> -+ <see selector="{{AdminOrderDetailsMessagesSection.successMessage}}" userInput="The invoice has been created." stepKey="seeSuccessInvoiceMessage"/> -+ <!--Create Shipment for the order--> -+ <comment userInput="Create Shipment for the order" stepKey="createShipmentForOrder"/> -+ <amOnPage url="{{AdminOrdersPage.url}}" stepKey="onOrdersPage2"/> -+ <waitForPageLoad time="30" stepKey="waitForOrderListPageLoading"/> -+ <click selector="{{AdminOrdersGridSection.firstRow}}" stepKey="openOrderPageForShip"/> -+ <waitForPageLoad stepKey="waitForOrderDetailsPage"/> -+ <click selector="{{AdminOrderDetailsMainActionsSection.ship}}" stepKey="clickShipAction"/> -+ <waitForPageLoad stepKey="waitForShipmentPagePage"/> -+ <seeInCurrentUrl url="{{AdminShipmentNewPage.url}}" stepKey="seeOrderShipmentUrl"/> -+ <!--Submit Shipment--> -+ <comment userInput="Submit Shipment" stepKey="submitShipment"/> -+ <click selector="{{AdminShipmentMainActionsSection.submitShipment}}" stepKey="clickSubmitShipment"/> -+ <waitForPageLoad stepKey="waitForShipmentSubmit"/> -+ <see selector="{{AdminOrderDetailsMessagesSection.successMessage}}" userInput="The shipment has been created." stepKey="seeShipmentCreateSuccess"/> -+ <!-- Go to dashboard page --> -+ <comment userInput="Go to dashboard page" stepKey="goToDashboardPage"/> -+ <amOnPage url="{{AdminDashboardPage.url}}" stepKey="amOnDashboardPage"/> -+ <waitForPageLoad stepKey="waitForDashboardPageLoad4"/> -+ <!-- Grab quantity value --> -+ <comment userInput="Grab quantity value from dashboard at the end" stepKey="grabQuantityFromDashboardAtTheEnd"/> -+ <grabTextFrom selector="{{AdminDashboardSection.dashboardTotals('Quantity')}}" stepKey="grabEndQuantity"/> -+ <!-- Assert that page is not broken --> -+ <comment userInput="Assert that dashboard page is not broken" stepKey="assertDashboardPageIsNotBroken"/> -+ <seeElement selector="{{AdminDashboardSection.dashboardDiagramOrderContentTab}}" stepKey="seeOrderContentTab"/> -+ <seeElement selector="{{AdminDashboardSection.dashboardDiagramContent}}" stepKey="seeDiagramContent"/> -+ <click selector="{{AdminDashboardSection.dashboardDiagramAmounts}}" stepKey="clickDashboardAmount"/> -+ <waitForLoadingMaskToDisappear stepKey="waitForDashboardAmountLoading"/> -+ <seeElement selector="{{AdminDashboardSection.dashboardDiagramAmountsContentTab}}" stepKey="seeDiagramAmountContent"/> -+ <seeElement selector="{{AdminDashboardSection.dashboardDiagramTotals}}" stepKey="seeAmountTotals"/> -+ <dontSeeJsError stepKey="dontSeeJsError"/> -+ <assertGreaterThan expected="$grabStartQuantity" actual="$grabEndQuantity" stepKey="checkQuantityWasChanged"/> -+ </test> -+</tests> -diff --git a/app/code/Magento/Backend/Test/Mftf/Test/AdminLoginAfterChangeCookieDomainTest.xml b/app/code/Magento/Backend/Test/Mftf/Test/AdminLoginAfterChangeCookieDomainTest.xml -new file mode 100644 -index 00000000000..93d411c8827 ---- /dev/null -+++ b/app/code/Magento/Backend/Test/Mftf/Test/AdminLoginAfterChangeCookieDomainTest.xml -@@ -0,0 +1,34 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> -+ <test name="AdminLoginAfterChangeCookieDomainTest"> -+ <annotations> -+ <features value="Backend"/> -+ <stories value="Login on the Admin Backend"/> -+ <title value="Admin user can login after changing cookie domain on main website scope without changing cookie domain on default scope"/> -+ <description value="Admin user can login after changing cookie domain on main website scope without changing cookie domain on default scope"/> -+ <severity value="CRITICAL"/> -+ <testCaseId value="MC-17847"/> -+ <useCaseId value="MC-17275"/> -+ <group value="backend"/> -+ </annotations> -+ <before> -+ <magentoCLI command="config:set {{ChangedCookieDomainForMainWebsiteConfigData.path}} --scope={{ChangedCookieDomainForMainWebsiteConfigData.scope}} --scope-code={{ChangedCookieDomainForMainWebsiteConfigData.scope_code}} {{ChangedCookieDomainForMainWebsiteConfigData.value}}" stepKey="changeDomainForMainWebsiteBeforeTestRun"/> -+ <magentoCLI command="cache:flush config" stepKey="flushCacheBeforeTestRun"/> -+ </before> -+ <after> -+ <magentoCLI command="config:set {{EmptyCookieDomainForMainWebsiteConfigData.path}} --scope={{EmptyCookieDomainForMainWebsiteConfigData.scope}} --scope-code={{EmptyCookieDomainForMainWebsiteConfigData.scope_code}} {{EmptyCookieDomainForMainWebsiteConfigData.value}}" stepKey="changeDomainForMainWebsiteAfterTestComplete"/> -+ <magentoCLI command="cache:flush config" stepKey="flushCacheAfterTestComplete"/> -+ </after> -+ <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> -+ <actionGroup ref="AssertAdminDashboardPageIsVisibleActionGroup" stepKey="seeDashboardPage"/> -+ <actionGroup ref="logout" stepKey="logoutFromAdmin"/> -+ </test> -+</tests> -diff --git a/app/code/Magento/Backend/Test/Mftf/Test/AdminLoginAfterJSMinificationTest.xml b/app/code/Magento/Backend/Test/Mftf/Test/AdminLoginAfterJSMinificationTest.xml -new file mode 100644 -index 00000000000..38749dfd792 ---- /dev/null -+++ b/app/code/Magento/Backend/Test/Mftf/Test/AdminLoginAfterJSMinificationTest.xml -@@ -0,0 +1,37 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> -+ <test name="AdminLoginAfterJSMinificationTest"> -+ <annotations> -+ <features value="Backend"/> -+ <stories value="Admin Panel JS minification"/> -+ <title value="Admin panel should be accessible with JS minification enabled"/> -+ <description value="Admin panel should be accessible with JS minification enabled"/> -+ <testCaseId value="MC-14104"/> -+ <severity value="MAJOR"/> -+ <group value="backend"/> -+ <group value="mtf_migrated"/> -+ <skip> -+ <issueId value="MC-17140"/> -+ </skip> -+ </annotations> -+ <before> -+ <magentoCLI command="config:set {{MinifyJavaScriptFilesEnableConfigData.path}} {{MinifyJavaScriptFilesEnableConfigData.value}}" stepKey="enableJsMinification"/> -+ <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> -+ </before> -+ <after> -+ <magentoCLI command="config:set {{MinifyJavaScriptFilesDisableConfigData.path}} {{MinifyJavaScriptFilesDisableConfigData.value}}" stepKey="disableJsMinification"/> -+ <actionGroup ref="logout" stepKey="logout"/> -+ </after> -+ -+ <actionGroup ref="AssertAdminSuccessLoginActionGroup" stepKey="loggedInSuccessfully"/> -+ <actionGroup ref="AssertAdminPageIsNot404ActionGroup" stepKey="dontSee404Page"/> -+ </test> -+</tests> -diff --git a/app/code/Magento/Backend/Test/Mftf/Test/AdminLoginTest.xml b/app/code/Magento/Backend/Test/Mftf/Test/AdminLoginTest.xml -index 99d0f665473..ce33f01c601 100644 ---- a/app/code/Magento/Backend/Test/Mftf/Test/AdminLoginTest.xml -+++ b/app/code/Magento/Backend/Test/Mftf/Test/AdminLoginTest.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="AdminLoginTest"> - <annotations> - <features value="Backend"/> -@@ -20,11 +20,8 @@ - <group value="login"/> - </annotations> - -- <amOnPage url="{{AdminLoginPage.url}}" stepKey="amOnAdminLoginPage"/> -- <fillField selector="{{AdminLoginFormSection.username}}" userInput="{{_ENV.MAGENTO_ADMIN_USERNAME}}" stepKey="fillUsername"/> -- <fillField selector="{{AdminLoginFormSection.password}}" userInput="{{_ENV.MAGENTO_ADMIN_PASSWORD}}" stepKey="fillPassword"/> -- <click selector="{{AdminLoginFormSection.signIn}}" stepKey="clickOnSignIn"/> -- <closeAdminNotification stepKey="closeAdminNotification"/> -+ <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> - <seeInCurrentUrl url="{{AdminLoginPage.url}}" stepKey="seeAdminLoginUrl"/> -+ <actionGroup ref="logout" stepKey="logoutFromAdmin"/> - </test> --</tests> -+</tests> -\ No newline at end of file -diff --git a/app/code/Magento/Backend/Test/Mftf/Test/AdminMenuNavigationWithSecretKeysTest.xml b/app/code/Magento/Backend/Test/Mftf/Test/AdminMenuNavigationWithSecretKeysTest.xml -new file mode 100644 -index 00000000000..c9a3b8089cc ---- /dev/null -+++ b/app/code/Magento/Backend/Test/Mftf/Test/AdminMenuNavigationWithSecretKeysTest.xml -@@ -0,0 +1,50 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> -+ <test name="AdminMenuNavigationWithSecretKeysTest"> -+ <annotations> -+ <features value="Backend"/> -+ <stories value="Menu Navigation"/> -+ <title value="Admin should be able to navigate between menu options with secret url keys enabled"/> -+ <description value="Admin should be able to navigate between menu options with secret url keys enabled"/> -+ <severity value="CRITICAL"/> -+ <testCaseId value="MAGETWO-95349"/> -+ <group value="menu"/> -+ </annotations> -+ <before> -+ <magentoCLI command="config:set admin/security/use_form_key 1" stepKey="enableUrlSecretKeys"/> -+ <magentoCLI command="cache:clean config full_page" stepKey="cleanInvalidatedCaches1"/> -+ <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> -+ </before> -+ <after> -+ <magentoCLI command="config:set admin/security/use_form_key 0" stepKey="disableUrlSecretKeys"/> -+ <magentoCLI command="cache:clean config full_page" stepKey="cleanInvalidatedCaches2"/> -+ <actionGroup ref="logout" stepKey="logout"/> -+ </after> -+ -+ <click selector="{{AdminMenuSection.stores}}" stepKey="clickStoresMenuOption1"/> -+ <waitForLoadingMaskToDisappear stepKey="waitForStoresMenu1" /> -+ <click selector="{{AdminMenuSection.configuration}}" stepKey="clickStoresConfigurationMenuOption1"/> -+ <waitForPageLoad stepKey="waitForConfigurationPageLoad1"/> -+ <seeCurrentUrlMatches regex="~\/admin\/system_config\/~" stepKey="seeCurrentUrlMatchesConfigPath1"/> -+ -+ <click selector="{{AdminMenuSection.catalog}}" stepKey="clickCatalogMenuOption"/> -+ <waitForLoadingMaskToDisappear stepKey="waitForCatalogMenu1" /> -+ <click selector="{{AdminMenuSection.catalogProducts}}" stepKey="clickCatalogProductsMenuOption"/> -+ <waitForPageLoad stepKey="waitForProductsPageLoad"/> -+ <seeCurrentUrlMatches regex="~\/catalog\/product\/~" stepKey="seeCurrentUrlMatchesProductsPath"/> -+ -+ <click selector="{{AdminMenuSection.stores}}" stepKey="clickStoresMenuOption2"/> -+ <waitForLoadingMaskToDisappear stepKey="waitForStoresMenu2" /> -+ <click selector="{{AdminMenuSection.configuration}}" stepKey="clickStoresConfigurationMenuOption2"/> -+ <waitForPageLoad stepKey="waitForConfigurationPageLoad2"/> -+ <seeCurrentUrlMatches regex="~\/admin\/system_config\/~" stepKey="seeCurrentUrlMatchesConfigPath2"/> -+ </test> -+</tests> -diff --git a/app/code/Magento/Backend/Test/Mftf/Test/AdminPrivacyPolicyTest.xml b/app/code/Magento/Backend/Test/Mftf/Test/AdminPrivacyPolicyTest.xml -new file mode 100644 -index 00000000000..4f215d20a7a ---- /dev/null -+++ b/app/code/Magento/Backend/Test/Mftf/Test/AdminPrivacyPolicyTest.xml -@@ -0,0 +1,93 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> -+ <test name="AdminPrivacyPolicyTest"> -+ <annotations> -+ <features value="Backend"/> -+ <stories value="Checks to see if privacy policy url is in the admin page and every sub page"/> -+ <title value="There should be a privacy policy url in the admin page and every sub page"/> -+ <description value="There should be a privacy policy url in the admin page and every sub page"/> -+ <severity value="CRITICAL"/> -+ <testCaseId value="MC-17787"/> -+ <group value="backend"/> -+ <group value="login"/> -+ </annotations> -+ -+ <!-- Logging in Magento admin and checking for Privacy policy footer in dashboard --> -+ <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> -+ <closeAdminNotification stepKey="closeAdminNotification"/> -+ <seeLink userInput="Privacy Policy" url="https://magento.com/sites/default/files/REVISED-MAGENTO-PRIVACY-POLICY.pdf" stepKey="seePrivacyPolicyLinkDashboard"/> -+ -+ <!-- Checking for Privacy policy footer in salesOrderPage --> -+ <actionGroup ref="AdminNavigateMenuActionGroup" stepKey="navigateToSalesOrder"> -+ <argument name="menuUiId" value="magento-sales-sales"/> -+ <argument name="submenuUiId" value="magento-sales-sales-order"/> -+ </actionGroup> -+ <seeLink userInput="Privacy Policy" url="https://magento.com/sites/default/files/REVISED-MAGENTO-PRIVACY-POLICY.pdf" stepKey="seePrivacyPolicyLinkSalesOrder"/> -+ -+ <!-- Checking for Privacy policy footer in catalogProductsPage --> -+ <actionGroup ref="AdminNavigateMenuActionGroup" stepKey="navigateToCatalogProducts"> -+ <argument name="menuUiId" value="magento-catalog-catalog"/> -+ <argument name="submenuUiId" value="magento-catalog-catalog-products"/> -+ </actionGroup> -+ <seeLink userInput="Privacy Policy" url="https://magento.com/sites/default/files/REVISED-MAGENTO-PRIVACY-POLICY.pdf" stepKey="seePrivacyPolicyLinkCatalogProducts"/> -+ -+ <!-- Checking for Privacy policy footer in customersAllCustomersPage --> -+ <actionGroup ref="AdminNavigateMenuActionGroup" stepKey="navigateToCustomersAllCustomers"> -+ <argument name="menuUiId" value="magento-customer-customer"/> -+ <argument name="submenuUiId" value="magento-customer-customer-manage"/> -+ </actionGroup> -+ <seeLink userInput="Privacy Policy" url="https://magento.com/sites/default/files/REVISED-MAGENTO-PRIVACY-POLICY.pdf" stepKey="seePrivacyPolicyLinkCustomersAllCustomers"/> -+ -+ <!-- Checking for Privacy policy footer in marketingCatalogPriceRulePage --> -+ <actionGroup ref="AdminNavigateMenuActionGroup" stepKey="navigateToMarketingCatalogPriceRule"> -+ <argument name="menuUiId" value="magento-backend-marketing"/> -+ <argument name="submenuUiId" value="magento-catalogrule-promo-catalog"/> -+ </actionGroup> -+ <seeLink userInput="Privacy Policy" url="https://magento.com/sites/default/files/REVISED-MAGENTO-PRIVACY-POLICY.pdf" stepKey="seePrivacyPolicyLinkMarketingCatalogPriceRule"/> -+ -+ <!-- Checking for Privacy policy footer in contentBlocksPage --> -+ <actionGroup ref="AdminNavigateMenuActionGroup" stepKey="navigateToContentBlocks"> -+ <argument name="menuUiId" value="magento-backend-content"/> -+ <argument name="submenuUiId" value="magento-cms-cms-block"/> -+ </actionGroup> -+ <seeLink userInput="Privacy Policy" url="https://magento.com/sites/default/files/REVISED-MAGENTO-PRIVACY-POLICY.pdf" stepKey="seePrivacyPolicyLinkContentBlocks"/> -+ -+ <!-- Checking for Privacy policy footer in reportSearcbTermsPage --> -+ <actionGroup ref="AdminNavigateMenuActionGroup" stepKey="navigateToReportsSearchTerms"> -+ <argument name="menuUiId" value="magento-reports-report"/> -+ <argument name="submenuUiId" value="magento-search-report-search-term"/> -+ </actionGroup> -+ <seeLink userInput="Privacy Policy" url="https://magento.com/sites/default/files/REVISED-MAGENTO-PRIVACY-POLICY.pdf" stepKey="seePrivacyPolicyLinkReportsSearchTerms"/> -+ -+ <!-- Checking for Privacy policy footer in storesAllStoresPage --> -+ <actionGroup ref="AdminNavigateMenuActionGroup" stepKey="navigateToStoresAllStores"> -+ <argument name="menuUiId" value="magento-backend-stores"/> -+ <argument name="submenuUiId" value="magento-backend-system-store"/> -+ </actionGroup> -+ <seeLink userInput="Privacy Policy" url="https://magento.com/sites/default/files/REVISED-MAGENTO-PRIVACY-POLICY.pdf" stepKey="seePrivacyPolicyLinkStoresAllStores"/> -+ -+ <!-- Checking for Privacy policy footer in systemImportPage --> -+ <actionGroup ref="AdminNavigateMenuActionGroup" stepKey="navigateToSystemImport"> -+ <argument name="menuUiId" value="magento-backend-system"/> -+ <argument name="submenuUiId" value="magento-importexport-system-convert-import"/> -+ </actionGroup> -+ <seeLink userInput="Privacy Policy" url="https://magento.com/sites/default/files/REVISED-MAGENTO-PRIVACY-POLICY.pdf" stepKey="seePrivacyPolicyLinkSystemImport"/> -+ -+ <!-- Checking for Privacy policy footer in findPartnersAndExtensionsPage --> -+ <actionGroup ref="AdminNavigateMenuActionGroup" stepKey="navigateToFindPartnersAndExtensions"> -+ <argument name="menuUiId" value="magento-marketplace-partners"/> -+ <argument name="submenuUiId" value="magento-marketplace-partners"/> -+ </actionGroup> -+ <seeLink userInput="Privacy Policy" url="https://magento.com/sites/default/files/REVISED-MAGENTO-PRIVACY-POLICY.pdf" stepKey="seePrivacyPolicyLinkFindPartnersAndExtensions"/> -+ </test> -+</tests> -+ -+ -diff --git a/app/code/Magento/Backend/Test/Mftf/Test/AdminStoresAllStoresNavigateMenuTest.xml b/app/code/Magento/Backend/Test/Mftf/Test/AdminStoresAllStoresNavigateMenuTest.xml -new file mode 100644 -index 00000000000..7758b387e39 ---- /dev/null -+++ b/app/code/Magento/Backend/Test/Mftf/Test/AdminStoresAllStoresNavigateMenuTest.xml -@@ -0,0 +1,36 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> -+ <test name="AdminStoresAllStoresNavigateMenuTest"> -+ <annotations> -+ <features value="Backend"/> -+ <stories value="Menu Navigation"/> -+ <title value="Admin stores all stores navigate menu test"/> -+ <description value="Admin should be able to navigate to Stores > All Stores"/> -+ <severity value="CRITICAL"/> -+ <testCaseId value="MC-14118"/> -+ <group value="menu"/> -+ <group value="mtf_migrated"/> -+ </annotations> -+ <before> -+ <actionGroup ref="LoginAsAdmin" stepKey="LoginAsAdmin"/> -+ </before> -+ <after> -+ <actionGroup ref="logout" stepKey="logout"/> -+ </after> -+ <actionGroup ref="AdminNavigateMenuActionGroup" stepKey="navigateToStoresAllStoresPage"> -+ <argument name="menuUiId" value="{{AdminMenuStores.dataUiId}}"/> -+ <argument name="submenuUiId" value="{{AdminMenuStoresSettingsAllStores.dataUiId}}"/> -+ </actionGroup> -+ <actionGroup ref="AdminAssertPageTitleActionGroup" stepKey="seePageTitle"> -+ <argument name="title" value="{{AdminMenuStoresSettingsAllStores.pageTitle}}"/> -+ </actionGroup> -+ </test> -+</tests> -diff --git a/app/code/Magento/Backend/Test/Mftf/Test/AdminStoresConfigurationNavigateMenuTest.xml b/app/code/Magento/Backend/Test/Mftf/Test/AdminStoresConfigurationNavigateMenuTest.xml -new file mode 100644 -index 00000000000..a54269b186b ---- /dev/null -+++ b/app/code/Magento/Backend/Test/Mftf/Test/AdminStoresConfigurationNavigateMenuTest.xml -@@ -0,0 +1,36 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> -+ <test name="AdminStoresConfigurationNavigateMenuTest"> -+ <annotations> -+ <features value="Backend"/> -+ <stories value="Menu Navigation"/> -+ <title value="Admin stores configuration navigate menu test"/> -+ <description value="Admin should be able to navigate to Stores > Configuration"/> -+ <severity value="CRITICAL"/> -+ <testCaseId value="MC-14119"/> -+ <group value="menu"/> -+ <group value="mtf_migrated"/> -+ </annotations> -+ <before> -+ <actionGroup ref="LoginAsAdmin" stepKey="LoginAsAdmin"/> -+ </before> -+ <after> -+ <actionGroup ref="logout" stepKey="logout"/> -+ </after> -+ <actionGroup ref="AdminNavigateMenuActionGroup" stepKey="navigateToStoresConfigurationPage"> -+ <argument name="menuUiId" value="{{AdminMenuStores.dataUiId}}"/> -+ <argument name="submenuUiId" value="{{AdminMenuStoresSettingsConfiguration.dataUiId}}"/> -+ </actionGroup> -+ <actionGroup ref="AdminAssertPageTitleActionGroup" stepKey="seePageTitle"> -+ <argument name="title" value="{{AdminMenuStoresSettingsConfiguration.pageTitle}}"/> -+ </actionGroup> -+ </test> -+</tests> -diff --git a/app/code/Magento/Backend/Test/Mftf/Test/AdminSystemCacheManagementNavigateMenuTest.xml b/app/code/Magento/Backend/Test/Mftf/Test/AdminSystemCacheManagementNavigateMenuTest.xml -new file mode 100644 -index 00000000000..516631c1bd1 ---- /dev/null -+++ b/app/code/Magento/Backend/Test/Mftf/Test/AdminSystemCacheManagementNavigateMenuTest.xml -@@ -0,0 +1,36 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> -+ <test name="AdminSystemCacheManagementNavigateMenuTest"> -+ <annotations> -+ <features value="Backend"/> -+ <stories value="Menu Navigation"/> -+ <title value="Admin system cache management navigate menu test"/> -+ <description value="Admin should be able to navigate to System > Cache Management"/> -+ <severity value="CRITICAL"/> -+ <testCaseId value="MC-14120"/> -+ <group value="menu"/> -+ <group value="mtf_migrated"/> -+ </annotations> -+ <before> -+ <actionGroup ref="LoginAsAdmin" stepKey="LoginAsAdmin"/> -+ </before> -+ <after> -+ <actionGroup ref="logout" stepKey="logout"/> -+ </after> -+ <actionGroup ref="AdminNavigateMenuActionGroup" stepKey="navigateToSystemCacheManagementPage"> -+ <argument name="menuUiId" value="{{AdminMenuSystem.dataUiId}}"/> -+ <argument name="submenuUiId" value="{{AdminMenuSystemToolsCacheManagement.dataUiId}}"/> -+ </actionGroup> -+ <actionGroup ref="AdminAssertPageTitleActionGroup" stepKey="seePageTitle"> -+ <argument name="title" value="{{AdminMenuSystemToolsCacheManagement.pageTitle}}"/> -+ </actionGroup> -+ </test> -+</tests> -diff --git a/app/code/Magento/Backend/Test/Mftf/Test/AdminUserLoginWithStoreCodeInUrlTest.xml b/app/code/Magento/Backend/Test/Mftf/Test/AdminUserLoginWithStoreCodeInUrlTest.xml -new file mode 100644 -index 00000000000..df5ca6d0378 ---- /dev/null -+++ b/app/code/Magento/Backend/Test/Mftf/Test/AdminUserLoginWithStoreCodeInUrlTest.xml -@@ -0,0 +1,32 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> -+ <test name="AdminUserLoginWithStoreCodeInUrlTest"> -+ <annotations> -+ <features value="Backend"/> -+ <stories value="Admin Panel URL with Store Code"/> -+ <title value="Admin panel should be accessible with Add Store Code to URL setting enabled"/> -+ <description value="Admin panel should be accessible with Add Store Code to URL setting enabled"/> -+ <testCaseId value="MC-14279"/> -+ <severity value="CRITICAL"/> -+ <group value="backend"/> -+ <group value="mtf_migrated"/> -+ </annotations> -+ <before> -+ <magentoCLI command="config:set {{StorefrontEnableAddStoreCodeToUrls.path}} {{StorefrontEnableAddStoreCodeToUrls.value}}" stepKey="addStoreCodeToUrlEnable"/> -+ </before> -+ <after> -+ <magentoCLI command="config:set {{StorefrontDisableAddStoreCodeToUrls.path}} {{StorefrontDisableAddStoreCodeToUrls.value}}" stepKey="addStoreCodeToUrlDisable"/> -+ </after> -+ -+ <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> -+ <actionGroup ref="AssertAdminDashboardPageIsVisibleActionGroup" stepKey="seeDashboardPage"/> -+ </test> -+</tests> -diff --git a/app/code/Magento/Backend/Test/Unit/Block/Widget/Grid/Column/Filter/TextTest.php b/app/code/Magento/Backend/Test/Unit/Block/Widget/Grid/Column/Filter/TextTest.php -index 1bf23649e2e..5aa8cf7a0c5 100644 ---- a/app/code/Magento/Backend/Test/Unit/Block/Widget/Grid/Column/Filter/TextTest.php -+++ b/app/code/Magento/Backend/Test/Unit/Block/Widget/Grid/Column/Filter/TextTest.php -@@ -8,6 +8,9 @@ namespace Magento\Backend\Test\Unit\Block\Widget\Grid\Column\Filter; - - use Magento\Framework\TestFramework\Unit\Helper\ObjectManager as ObjectManagerHelper; - -+/** -+ * Unit test for \Magento\Backend\Block\Widget\Grid\Column\Filter\Text -+ */ - class TextTest extends \PHPUnit\Framework\TestCase - { - /** @var \Magento\Backend\Block\Widget\Grid\Column\Filter\Text*/ -@@ -31,7 +34,10 @@ class TextTest extends \PHPUnit\Framework\TestCase - ->setMethods(['getEscaper']) - ->disableOriginalConstructor() - ->getMock(); -- $this->escaper = $this->createPartialMock(\Magento\Framework\Escaper::class, ['escapeHtml']); -+ $this->escaper = $this->createPartialMock( -+ \Magento\Framework\Escaper::class, -+ ['escapeHtml', 'escapeHtmlAttr'] -+ ); - $this->helper = $this->createMock(\Magento\Framework\DB\Helper::class); - - $this->context->expects($this->once())->method('getEscaper')->willReturn($this->escaper); -@@ -60,6 +66,13 @@ class TextTest extends \PHPUnit\Framework\TestCase - $this->block->setColumn($column); - - $this->escaper->expects($this->any())->method('escapeHtml')->willReturn('escapedHtml'); -+ $this->escaper->expects($this->once()) -+ ->method('escapeHtmlAttr') -+ ->willReturnCallback( -+ function ($string) { -+ return $string; -+ } -+ ); - $column->expects($this->any())->method('getId')->willReturn('id'); - $column->expects($this->once())->method('getHtmlId')->willReturn('htmlId'); - -diff --git a/app/code/Magento/Backend/Test/Unit/Block/Widget/Grid/MassactionTest.php b/app/code/Magento/Backend/Test/Unit/Block/Widget/Grid/MassactionTest.php -index e8143b5f6b4..e62b73f3924 100644 ---- a/app/code/Magento/Backend/Test/Unit/Block/Widget/Grid/MassactionTest.php -+++ b/app/code/Magento/Backend/Test/Unit/Block/Widget/Grid/MassactionTest.php -@@ -269,62 +269,6 @@ class MassactionTest extends \PHPUnit\Framework\TestCase - $this->assertEmpty($this->_block->getGridIdsJson()); - } - -- /** -- * @param array $items -- * @param string $result -- * -- * @dataProvider dataProviderGetGridIdsJsonWithUseSelectAll -- */ -- public function testGetGridIdsJsonWithUseSelectAll(array $items, $result) -- { -- $this->_block->setUseSelectAll(true); -- -- if ($this->_block->getMassactionIdField()) { -- $massActionIdField = $this->_block->getMassactionIdField(); -- } else { -- $massActionIdField = $this->_block->getParentBlock()->getMassactionIdField(); -- } -- -- $collectionMock = $this->getMockBuilder(\Magento\Framework\Data\Collection::class) -- ->disableOriginalConstructor() -- ->getMock(); -- -- $this->_gridMock->expects($this->once()) -- ->method('getCollection') -- ->willReturn($collectionMock); -- $collectionMock->expects($this->once()) -- ->method('setPageSize') -- ->with(0) -- ->willReturnSelf(); -- $collectionMock->expects($this->once()) -- ->method('getColumnValues') -- ->with($massActionIdField) -- ->willReturn($items); -- -- $this->assertEquals($result, $this->_block->getGridIdsJson()); -- } -- -- /** -- * @return array -- */ -- public function dataProviderGetGridIdsJsonWithUseSelectAll() -- { -- return [ -- [ -- [], -- '', -- ], -- [ -- [1], -- '1', -- ], -- [ -- [1, 2, 3], -- '1,2,3', -- ], -- ]; -- } -- - /** - * @param string $itemId - * @param array|\Magento\Framework\DataObject $item -diff --git a/app/code/Magento/Backend/Test/Unit/Controller/Adminhtml/Cache/CleanMediaTest.php b/app/code/Magento/Backend/Test/Unit/Controller/Adminhtml/Cache/CleanMediaTest.php -index b1911da0242..ac0f4a2f467 100644 ---- a/app/code/Magento/Backend/Test/Unit/Controller/Adminhtml/Cache/CleanMediaTest.php -+++ b/app/code/Magento/Backend/Test/Unit/Controller/Adminhtml/Cache/CleanMediaTest.php -@@ -38,7 +38,7 @@ class CleanMediaTest extends \PHPUnit\Framework\TestCase - $messageManagerParams = $helper->getConstructArguments(\Magento\Framework\Message\Manager::class); - $messageManagerParams['exceptionMessageFactory'] = $exceptionMessageFactory; - $messageManager = $this->getMockBuilder(\Magento\Framework\Message\Manager::class) -- ->setMethods(['addSuccess']) -+ ->setMethods(['addSuccessMessage']) - ->setConstructorArgs($messageManagerParams) - ->getMock(); - -@@ -86,7 +86,7 @@ class CleanMediaTest extends \PHPUnit\Framework\TestCase - $mergeService->expects($this->once())->method('cleanMergedJsCss'); - - $messageManager->expects($this->once()) -- ->method('addSuccess') -+ ->method('addSuccessMessage') - ->with('The JavaScript/CSS cache has been cleaned.'); - - $valueMap = [ -diff --git a/app/code/Magento/Backend/Test/Unit/Controller/Adminhtml/Cache/CleanStaticFilesTest.php b/app/code/Magento/Backend/Test/Unit/Controller/Adminhtml/Cache/CleanStaticFilesTest.php -index 40d9ca1aa89..fc457cd9681 100644 ---- a/app/code/Magento/Backend/Test/Unit/Controller/Adminhtml/Cache/CleanStaticFilesTest.php -+++ b/app/code/Magento/Backend/Test/Unit/Controller/Adminhtml/Cache/CleanStaticFilesTest.php -@@ -76,7 +76,7 @@ class CleanStaticFilesTest extends \PHPUnit\Framework\TestCase - ->with('clean_static_files_cache_after'); - - $this->messageManagerMock->expects($this->once()) -- ->method('addSuccess') -+ ->method('addSuccessMessage') - ->with('The static files cache has been cleaned.'); - - $resultRedirect = $this->getMockBuilder(\Magento\Backend\Model\View\Result\Redirect::class) -diff --git a/app/code/Magento/Backend/Test/Unit/Controller/Adminhtml/Cache/MassDisableTest.php b/app/code/Magento/Backend/Test/Unit/Controller/Adminhtml/Cache/MassDisableTest.php -index 197b46acc61..a8b248c611e 100644 ---- a/app/code/Magento/Backend/Test/Unit/Controller/Adminhtml/Cache/MassDisableTest.php -+++ b/app/code/Magento/Backend/Test/Unit/Controller/Adminhtml/Cache/MassDisableTest.php -@@ -156,7 +156,7 @@ class MassDisableTest extends \PHPUnit\Framework\TestCase - ->willReturn(['someCache']); - - $this->messageManagerMock->expects($this->once()) -- ->method('addError') -+ ->method('addErrorMessage') - ->with('These cache type(s) don\'t exist: someCache') - ->willReturnSelf(); - -@@ -176,7 +176,7 @@ class MassDisableTest extends \PHPUnit\Framework\TestCase - ->willThrowException($exception); - - $this->messageManagerMock->expects($this->once()) -- ->method('addException') -+ ->method('addExceptionMessage') - ->with($exception, 'An error occurred while disabling cache.') - ->willReturnSelf(); - -@@ -216,7 +216,7 @@ class MassDisableTest extends \PHPUnit\Framework\TestCase - ->method('persist'); - - $this->messageManagerMock->expects($this->once()) -- ->method('addSuccess') -+ ->method('addSuccessMessage') - ->with('1 cache type(s) disabled.') - ->willReturnSelf(); - -diff --git a/app/code/Magento/Backend/Test/Unit/Controller/Adminhtml/Cache/MassEnableTest.php b/app/code/Magento/Backend/Test/Unit/Controller/Adminhtml/Cache/MassEnableTest.php -index 9b364019315..6eac44a564f 100644 ---- a/app/code/Magento/Backend/Test/Unit/Controller/Adminhtml/Cache/MassEnableTest.php -+++ b/app/code/Magento/Backend/Test/Unit/Controller/Adminhtml/Cache/MassEnableTest.php -@@ -156,7 +156,7 @@ class MassEnableTest extends \PHPUnit\Framework\TestCase - ->willReturn(['someCache']); - - $this->messageManagerMock->expects($this->once()) -- ->method('addError') -+ ->method('addErrorMessage') - ->with('These cache type(s) don\'t exist: someCache') - ->willReturnSelf(); - -@@ -176,7 +176,7 @@ class MassEnableTest extends \PHPUnit\Framework\TestCase - ->willThrowException($exception); - - $this->messageManagerMock->expects($this->once()) -- ->method('addException') -+ ->method('addExceptionMessage') - ->with($exception, 'An error occurred while enabling cache.') - ->willReturnSelf(); - -@@ -216,7 +216,7 @@ class MassEnableTest extends \PHPUnit\Framework\TestCase - ->method('persist'); - - $this->messageManagerMock->expects($this->once()) -- ->method('addSuccess') -+ ->method('addSuccessMessage') - ->with('1 cache type(s) enabled.') - ->willReturnSelf(); - -diff --git a/app/code/Magento/Backend/Test/Unit/Controller/Adminhtml/Dashboard/RefreshStatisticsTest.php b/app/code/Magento/Backend/Test/Unit/Controller/Adminhtml/Dashboard/RefreshStatisticsTest.php -index e8dcc00345f..a985681919f 100644 ---- a/app/code/Magento/Backend/Test/Unit/Controller/Adminhtml/Dashboard/RefreshStatisticsTest.php -+++ b/app/code/Magento/Backend/Test/Unit/Controller/Adminhtml/Dashboard/RefreshStatisticsTest.php -@@ -107,7 +107,7 @@ class RefreshStatisticsTest extends \PHPUnit\Framework\TestCase - $this->resultRedirectFactory->expects($this->any())->method('create')->willReturn($this->resultRedirect); - - $this->messageManager->expects($this->once()) -- ->method('addSuccess') -+ ->method('addSuccessMessage') - ->with(__('We updated lifetime statistic.')); - - $this->objectManager->expects($this->any()) -diff --git a/app/code/Magento/Backend/Test/Unit/Controller/Adminhtml/System/Account/SaveTest.php b/app/code/Magento/Backend/Test/Unit/Controller/Adminhtml/System/Account/SaveTest.php -index 844a821df1c..a8490d6ba2e 100644 ---- a/app/code/Magento/Backend/Test/Unit/Controller/Adminhtml/System/Account/SaveTest.php -+++ b/app/code/Magento/Backend/Test/Unit/Controller/Adminhtml/System/Account/SaveTest.php -@@ -71,7 +71,7 @@ class SaveTest extends \PHPUnit\Framework\TestCase - ->getMock(); - $this->_messagesMock = $this->getMockBuilder(\Magento\Framework\Message\Manager::class) - ->disableOriginalConstructor() -- ->setMethods(['addSuccess']) -+ ->setMethods(['addSuccessMessage']) - ->getMockForAbstractClass(); - - $this->_authSessionMock = $this->getMockBuilder(\Magento\Backend\Model\Auth\Session::class) -@@ -221,7 +221,7 @@ class SaveTest extends \PHPUnit\Framework\TestCase - - $this->_requestMock->setParams($requestParams); - -- $this->_messagesMock->expects($this->once())->method('addSuccess')->with($this->equalTo($testedMessage)); -+ $this->_messagesMock->expects($this->once())->method('addSuccessMessage')->with($this->equalTo($testedMessage)); - - $this->_controller->execute(); - } -diff --git a/app/code/Magento/Backend/Test/Unit/Model/AdminPathConfigTest.php b/app/code/Magento/Backend/Test/Unit/Model/AdminPathConfigTest.php -index 4911dc1e996..b373459b786 100644 ---- a/app/code/Magento/Backend/Test/Unit/Model/AdminPathConfigTest.php -+++ b/app/code/Magento/Backend/Test/Unit/Model/AdminPathConfigTest.php -@@ -76,17 +76,35 @@ class AdminPathConfigTest extends \PHPUnit\Framework\TestCase - * @param $unsecureBaseUrl - * @param $useSecureInAdmin - * @param $secureBaseUrl -+ * @param $useCustomUrl -+ * @param $customUrl - * @param $expected - * @dataProvider shouldBeSecureDataProvider - */ -- public function testShouldBeSecure($unsecureBaseUrl, $useSecureInAdmin, $secureBaseUrl, $expected) -- { -- $coreConfigValueMap = [ -+ public function testShouldBeSecure( -+ $unsecureBaseUrl, -+ $useSecureInAdmin, -+ $secureBaseUrl, -+ $useCustomUrl, -+ $customUrl, -+ $expected -+ ) { -+ $coreConfigValueMap = $this->returnValueMap([ - [\Magento\Store\Model\Store::XML_PATH_UNSECURE_BASE_URL, 'default', null, $unsecureBaseUrl], - [\Magento\Store\Model\Store::XML_PATH_SECURE_BASE_URL, 'default', null, $secureBaseUrl], -- ]; -- $this->coreConfig->expects($this->any())->method('getValue')->will($this->returnValueMap($coreConfigValueMap)); -- $this->backendConfig->expects($this->any())->method('isSetFlag')->willReturn($useSecureInAdmin); -+ ['admin/url/custom', 'default', null, $customUrl], -+ ]); -+ $backendConfigFlagsMap = $this->returnValueMap([ -+ [\Magento\Store\Model\Store::XML_PATH_SECURE_IN_ADMINHTML, $useSecureInAdmin], -+ ['admin/url/use_custom', $useCustomUrl], -+ ]); -+ $this->coreConfig->expects($this->atLeast(1))->method('getValue') -+ ->will($coreConfigValueMap); -+ $this->coreConfig->expects($this->atMost(2))->method('getValue') -+ ->will($coreConfigValueMap); -+ -+ $this->backendConfig->expects($this->atMost(2))->method('isSetFlag') -+ ->will($backendConfigFlagsMap); - $this->assertEquals($expected, $this->adminPathConfig->shouldBeSecure('')); - } - -@@ -96,13 +114,13 @@ class AdminPathConfigTest extends \PHPUnit\Framework\TestCase - public function shouldBeSecureDataProvider() - { - return [ -- ['http://localhost/', false, 'default', false], -- ['http://localhost/', true, 'default', false], -- ['https://localhost/', false, 'default', true], -- ['https://localhost/', true, 'default', true], -- ['http://localhost/', false, 'https://localhost/', false], -- ['http://localhost/', true, 'https://localhost/', true], -- ['https://localhost/', true, 'https://localhost/', true], -+ ['http://localhost/', false, 'default', false, '', false], -+ ['http://localhost/', true, 'default', false, '', false], -+ ['https://localhost/', false, 'default', false, '', true], -+ ['https://localhost/', true, 'default', false, '', true], -+ ['http://localhost/', false, 'https://localhost/', false, '', false], -+ ['http://localhost/', true, 'https://localhost/', false, '', true], -+ ['https://localhost/', true, 'https://localhost/', false, '', true], - ]; - } - -diff --git a/app/code/Magento/Backend/Test/Unit/Model/Auth/SessionTest.php b/app/code/Magento/Backend/Test/Unit/Model/Auth/SessionTest.php -deleted file mode 100644 -index f1a4bc355b0..00000000000 ---- a/app/code/Magento/Backend/Test/Unit/Model/Auth/SessionTest.php -+++ /dev/null -@@ -1,273 +0,0 @@ --<?php --/** -- * Copyright © Magento, Inc. All rights reserved. -- * See COPYING.txt for license details. -- */ --namespace Magento\Backend\Test\Unit\Model\Auth; -- --use Magento\Backend\Model\Auth\Session; --use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; -- --/** -- * Class SessionTest tests Magento\Backend\Model\Auth\Session -- * -- * @SuppressWarnings(PHPMD.CouplingBetweenObjects) -- */ --class SessionTest extends \PHPUnit\Framework\TestCase --{ -- /** -- * @var \Magento\Backend\App\Config | \PHPUnit_Framework_MockObject_MockObject -- */ -- protected $config; -- -- /** -- * @var \Magento\Framework\Session\Config | \PHPUnit_Framework_MockObject_MockObject -- */ -- protected $sessionConfig; -- -- /** -- * @var \Magento\Framework\Stdlib\CookieManagerInterface | \PHPUnit_Framework_MockObject_MockObject -- */ -- protected $cookieManager; -- -- /** -- * @var \Magento\Framework\Stdlib\Cookie\CookieMetadataFactory | \PHPUnit_Framework_MockObject_MockObject -- */ -- protected $cookieMetadataFactory; -- -- /** -- * @var \Magento\Framework\Session\Storage | \PHPUnit_Framework_MockObject_MockObject -- */ -- protected $storage; -- -- /** -- * @var \Magento\Framework\Acl\Builder | \PHPUnit_Framework_MockObject_MockObject -- */ -- protected $aclBuilder; -- -- /** -- * @var Session -- */ -- protected $session; -- -- protected function setUp() -- { -- $this->cookieMetadataFactory = $this->createPartialMock( -- \Magento\Framework\Stdlib\Cookie\CookieMetadataFactory::class, -- ['createPublicCookieMetadata'] -- ); -- -- $this->config = $this->createPartialMock(\Magento\Backend\App\Config::class, ['getValue']); -- $this->cookieManager = $this->createPartialMock( -- \Magento\Framework\Stdlib\Cookie\PhpCookieManager::class, -- ['getCookie', 'setPublicCookie'] -- ); -- $this->storage = $this->createPartialMock( -- \Magento\Framework\Session\Storage::class, -- ['getUser', 'getAcl', 'setAcl'] -- ); -- $this->sessionConfig = $this->createPartialMock( -- \Magento\Framework\Session\Config::class, -- ['getCookiePath', 'getCookieDomain', 'getCookieSecure', 'getCookieHttpOnly'] -- ); -- $this->aclBuilder = $this->getMockBuilder(\Magento\Framework\Acl\Builder::class) -- ->disableOriginalConstructor() -- ->getMock(); -- $objectManager = new ObjectManager($this); -- $this->session = $objectManager->getObject( -- \Magento\Backend\Model\Auth\Session::class, -- [ -- 'config' => $this->config, -- 'sessionConfig' => $this->sessionConfig, -- 'cookieManager' => $this->cookieManager, -- 'cookieMetadataFactory' => $this->cookieMetadataFactory, -- 'storage' => $this->storage, -- 'aclBuilder' => $this->aclBuilder -- ] -- ); -- } -- -- protected function tearDown() -- { -- $this->config = null; -- $this->sessionConfig = null; -- $this->session = null; -- } -- -- /** -- * @dataProvider refreshAclDataProvider -- * @param $isUserPassedViaParams -- */ -- public function testRefreshAcl($isUserPassedViaParams) -- { -- $aclMock = $this->getMockBuilder(\Magento\Framework\Acl::class)->disableOriginalConstructor()->getMock(); -- $this->aclBuilder->expects($this->any())->method('getAcl')->willReturn($aclMock); -- $userMock = $this->getMockBuilder(\Magento\User\Model\User::class) -- ->setMethods(['getReloadAclFlag', 'setReloadAclFlag', 'unsetData', 'save']) -- ->disableOriginalConstructor() -- ->getMock(); -- $userMock->expects($this->any())->method('getReloadAclFlag')->willReturn(true); -- $userMock->expects($this->once())->method('setReloadAclFlag')->with('0')->willReturnSelf(); -- $userMock->expects($this->once())->method('save'); -- $this->storage->expects($this->once())->method('setAcl')->with($aclMock); -- $this->storage->expects($this->any())->method('getAcl')->willReturn($aclMock); -- if ($isUserPassedViaParams) { -- $this->session->refreshAcl($userMock); -- } else { -- $this->storage->expects($this->once())->method('getUser')->willReturn($userMock); -- $this->session->refreshAcl(); -- } -- $this->assertSame($aclMock, $this->session->getAcl()); -- } -- -- /** -- * @return array -- */ -- public function refreshAclDataProvider() -- { -- return [ -- 'User set via params' => [true], -- 'User set to session object' => [false] -- ]; -- } -- -- public function testIsLoggedInPositive() -- { -- $user = $this->createPartialMock(\Magento\User\Model\User::class, ['getId', '__wakeup']); -- $user->expects($this->once()) -- ->method('getId') -- ->will($this->returnValue(1)); -- -- $this->storage->expects($this->any()) -- ->method('getUser') -- ->will($this->returnValue($user)); -- -- $this->assertTrue($this->session->isLoggedIn()); -- } -- -- public function testProlong() -- { -- $name = session_name(); -- $cookie = 'cookie'; -- $lifetime = 900; -- $path = '/'; -- $domain = 'magento2'; -- $secure = true; -- $httpOnly = true; -- -- $this->config->expects($this->once()) -- ->method('getValue') -- ->with(\Magento\Backend\Model\Auth\Session::XML_PATH_SESSION_LIFETIME) -- ->willReturn($lifetime); -- $cookieMetadata = $this->createMock(\Magento\Framework\Stdlib\Cookie\PublicCookieMetadata::class); -- $cookieMetadata->expects($this->once()) -- ->method('setDuration') -- ->with($lifetime) -- ->will($this->returnSelf()); -- $cookieMetadata->expects($this->once()) -- ->method('setPath') -- ->with($path) -- ->will($this->returnSelf()); -- $cookieMetadata->expects($this->once()) -- ->method('setDomain') -- ->with($domain) -- ->will($this->returnSelf()); -- $cookieMetadata->expects($this->once()) -- ->method('setSecure') -- ->with($secure) -- ->will($this->returnSelf()); -- $cookieMetadata->expects($this->once()) -- ->method('setHttpOnly') -- ->with($httpOnly) -- ->will($this->returnSelf()); -- -- $this->cookieMetadataFactory->expects($this->once()) -- ->method('createPublicCookieMetadata') -- ->will($this->returnValue($cookieMetadata)); -- -- $this->cookieManager->expects($this->once()) -- ->method('getCookie') -- ->with($name) -- ->will($this->returnValue($cookie)); -- $this->cookieManager->expects($this->once()) -- ->method('setPublicCookie') -- ->with($name, $cookie, $cookieMetadata); -- -- $this->sessionConfig->expects($this->once()) -- ->method('getCookiePath') -- ->will($this->returnValue($path)); -- $this->sessionConfig->expects($this->once()) -- ->method('getCookieDomain') -- ->will($this->returnValue($domain)); -- $this->sessionConfig->expects($this->once()) -- ->method('getCookieSecure') -- ->will($this->returnValue($secure)); -- $this->sessionConfig->expects($this->once()) -- ->method('getCookieHttpOnly') -- ->will($this->returnValue($httpOnly)); -- -- $this->session->prolong(); -- -- $this->assertLessThanOrEqual(time(), $this->session->getUpdatedAt()); -- } -- -- /** -- * @dataProvider isAllowedDataProvider -- * @param bool $isUserDefined -- * @param bool $isAclDefined -- * @param bool $isAllowed -- * @param true $expectedResult -- */ -- public function testIsAllowed($isUserDefined, $isAclDefined, $isAllowed, $expectedResult) -- { -- $userAclRole = 'userAclRole'; -- if ($isAclDefined) { -- $aclMock = $this->getMockBuilder(\Magento\Framework\Acl::class)->disableOriginalConstructor()->getMock(); -- $this->storage->expects($this->any())->method('getAcl')->willReturn($aclMock); -- } -- if ($isUserDefined) { -- $userMock = $this->getMockBuilder(\Magento\User\Model\User::class)->disableOriginalConstructor()->getMock(); -- $this->storage->expects($this->once())->method('getUser')->willReturn($userMock); -- } -- if ($isAclDefined && $isUserDefined) { -- $userMock->expects($this->any())->method('getAclRole')->willReturn($userAclRole); -- $aclMock->expects($this->once())->method('isAllowed')->with($userAclRole)->willReturn($isAllowed); -- } -- -- $this->assertEquals($expectedResult, $this->session->isAllowed('resource')); -- } -- -- /** -- * @return array -- */ -- public function isAllowedDataProvider() -- { -- return [ -- "Negative: User not defined" => [false, true, true, false], -- "Negative: Acl not defined" => [true, false, true, false], -- "Negative: Permission denied" => [true, true, false, false], -- "Positive: Permission granted" => [true, true, false, false], -- ]; -- } -- -- /** -- * @dataProvider firstPageAfterLoginDataProvider -- * @param bool $isFirstPageAfterLogin -- */ -- public function testFirstPageAfterLogin($isFirstPageAfterLogin) -- { -- $this->session->setIsFirstPageAfterLogin($isFirstPageAfterLogin); -- $this->assertEquals($isFirstPageAfterLogin, $this->session->isFirstPageAfterLogin()); -- } -- -- /** -- * @return array -- */ -- public function firstPageAfterLoginDataProvider() -- { -- return [ -- 'First page after login' => [true], -- 'Not first page after login' => [false], -- ]; -- } --} -diff --git a/app/code/Magento/Backend/Test/Unit/Model/Authorization/RoleLocatorTest.php b/app/code/Magento/Backend/Test/Unit/Model/Authorization/RoleLocatorTest.php -deleted file mode 100644 -index 5b3910e9445..00000000000 ---- a/app/code/Magento/Backend/Test/Unit/Model/Authorization/RoleLocatorTest.php -+++ /dev/null -@@ -1,36 +0,0 @@ --<?php --/** -- * Copyright © Magento, Inc. All rights reserved. -- * See COPYING.txt for license details. -- */ --namespace Magento\Backend\Test\Unit\Model\Authorization; -- --class RoleLocatorTest extends \PHPUnit\Framework\TestCase --{ -- /** -- * @var \Magento\Backend\Model\Authorization\RoleLocator -- */ -- protected $_model; -- -- /** -- * @var \PHPUnit_Framework_MockObject_MockObject -- */ -- protected $_sessionMock = []; -- -- protected function setUp() -- { -- $this->_sessionMock = $this->createPartialMock( -- \Magento\Backend\Model\Auth\Session::class, -- ['getUser', 'getAclRole', 'hasUser'] -- ); -- $this->_model = new \Magento\Backend\Model\Authorization\RoleLocator($this->_sessionMock); -- } -- -- public function testGetAclRoleIdReturnsCurrentUserAclRoleId() -- { -- $this->_sessionMock->expects($this->once())->method('hasUser')->will($this->returnValue(true)); -- $this->_sessionMock->expects($this->once())->method('getUser')->will($this->returnSelf()); -- $this->_sessionMock->expects($this->once())->method('getAclRole')->will($this->returnValue('some_role')); -- $this->assertEquals('some_role', $this->_model->getAclRoleId()); -- } --} -diff --git a/app/code/Magento/Backend/Test/Unit/Model/Locale/ManagerTest.php b/app/code/Magento/Backend/Test/Unit/Model/Locale/ManagerTest.php -deleted file mode 100644 -index 77eb7cdb34d..00000000000 ---- a/app/code/Magento/Backend/Test/Unit/Model/Locale/ManagerTest.php -+++ /dev/null -@@ -1,127 +0,0 @@ --<?php --/** -- * Copyright © Magento, Inc. All rights reserved. -- * See COPYING.txt for license details. -- */ --namespace Magento\Backend\Test\Unit\Model\Locale; -- --use Magento\Framework\Locale\Resolver; -- --class ManagerTest extends \PHPUnit\Framework\TestCase --{ -- /** -- * @var \Magento\Backend\Model\Locale\Manager -- */ -- protected $_model; -- -- /** -- * @var \PHPUnit_Framework_MockObject_MockObject|\Magento\Framework\TranslateInterface -- */ -- protected $_translator; -- -- /** -- * @var \Magento\Backend\Model\Session -- */ -- protected $_session; -- -- /** -- * @var \PHPUnit_Framework_MockObject_MockObject|\Magento\Backend\Model\Auth\Session -- */ -- protected $_authSession; -- -- /** -- * @var \PHPUnit_Framework_MockObject_MockObject|\Magento\Backend\App\ConfigInterface -- */ -- protected $_backendConfig; -- -- protected function setUp() -- { -- $this->_session = $this->createMock(\Magento\Backend\Model\Session::class); -- -- $this->_authSession = $this->createPartialMock(\Magento\Backend\Model\Auth\Session::class, ['getUser']); -- -- $this->_backendConfig = $this->getMockForAbstractClass( -- \Magento\Backend\App\ConfigInterface::class, -- [], -- '', -- false -- ); -- -- $userMock = new \Magento\Framework\DataObject(); -- -- $this->_authSession->expects($this->any())->method('getUser')->will($this->returnValue($userMock)); -- -- $this->_translator = $this->getMockBuilder(\Magento\Framework\TranslateInterface::class) -- ->setMethods(['init', 'setLocale']) -- ->getMockForAbstractClass(); -- -- $this->_translator->expects($this->any())->method('setLocale')->will($this->returnValue($this->_translator)); -- -- $this->_translator->expects($this->any())->method('init')->will($this->returnValue(false)); -- -- $this->_model = new \Magento\Backend\Model\Locale\Manager( -- $this->_session, -- $this->_authSession, -- $this->_translator, -- $this->_backendConfig -- ); -- } -- -- /** -- * @return array -- */ -- public function switchBackendInterfaceLocaleDataProvider() -- { -- return ['case1' => ['locale' => 'de_DE'], 'case2' => ['locale' => 'en_US']]; -- } -- -- /** -- * @param string $locale -- * @dataProvider switchBackendInterfaceLocaleDataProvider -- * @covers \Magento\Backend\Model\Locale\Manager::switchBackendInterfaceLocale -- */ -- public function testSwitchBackendInterfaceLocale($locale) -- { -- $this->_model->switchBackendInterfaceLocale($locale); -- -- $userInterfaceLocale = $this->_authSession->getUser()->getInterfaceLocale(); -- $this->assertEquals($userInterfaceLocale, $locale); -- -- $sessionLocale = $this->_session->getSessionLocale(); -- $this->assertEquals($sessionLocale, null); -- } -- -- /** -- * @covers \Magento\Backend\Model\Locale\Manager::getUserInterfaceLocale -- */ -- public function testGetUserInterfaceLocaleDefault() -- { -- $locale = $this->_model->getUserInterfaceLocale(); -- -- $this->assertEquals($locale, Resolver::DEFAULT_LOCALE); -- } -- -- /** -- * @covers \Magento\Backend\Model\Locale\Manager::getUserInterfaceLocale -- */ -- public function testGetUserInterfaceLocale() -- { -- $this->_model->switchBackendInterfaceLocale('de_DE'); -- $locale = $this->_model->getUserInterfaceLocale(); -- -- $this->assertEquals($locale, 'de_DE'); -- } -- -- /** -- * @covers \Magento\Backend\Model\Locale\Manager::getUserInterfaceLocale -- */ -- public function testGetUserInterfaceGeneralLocale() -- { -- $this->_backendConfig->expects($this->any()) -- ->method('getValue') -- ->with('general/locale/code') -- ->willReturn('test_locale'); -- $locale = $this->_model->getUserInterfaceLocale(); -- $this->assertEquals($locale, 'test_locale'); -- } --} -diff --git a/app/code/Magento/Backend/Test/Unit/Model/Menu/ConfigTest.php b/app/code/Magento/Backend/Test/Unit/Model/Menu/ConfigTest.php -index 260a38a481b..2b5f644e359 100644 ---- a/app/code/Magento/Backend/Test/Unit/Model/Menu/ConfigTest.php -+++ b/app/code/Magento/Backend/Test/Unit/Model/Menu/ConfigTest.php -@@ -168,6 +168,6 @@ class ConfigTest extends \PHPUnit\Framework\TestCase - } catch (\Exception $e) { - return; - } -- $this->fail("Generic \Exception was not throwed"); -+ $this->fail("Generic \Exception was not thrown"); - } - } -diff --git a/app/code/Magento/Backend/Test/Unit/Model/Session/QuoteTest.php b/app/code/Magento/Backend/Test/Unit/Model/Session/QuoteTest.php -index 869d4ba3f45..d159225089a 100644 ---- a/app/code/Magento/Backend/Test/Unit/Model/Session/QuoteTest.php -+++ b/app/code/Magento/Backend/Test/Unit/Model/Session/QuoteTest.php -@@ -267,7 +267,10 @@ class QuoteTest extends \PHPUnit\Framework\TestCase - $cartInterfaceMock->expects($this->atLeastOnce())->method('getId')->willReturn($quoteId); - $defaultGroup = $this->getMockBuilder(\Magento\Customer\Api\Data\GroupInterface::class)->getMock(); - $defaultGroup->expects($this->any())->method('getId')->will($this->returnValue($customerGroupId)); -- $this->groupManagementMock->expects($this->any())->method('getDefaultGroup')->willReturn($defaultGroup); -+ $this->groupManagementMock -+ ->method('getDefaultGroup') -+ ->with($storeId) -+ ->willReturn($defaultGroup); - - $dataCustomerMock = $this->getMockBuilder(\Magento\Customer\Api\Data\CustomerInterface::class) - ->disableOriginalConstructor() -diff --git a/app/code/Magento/Backend/Test/Unit/Service/V1/ModuleServiceTest.php b/app/code/Magento/Backend/Test/Unit/Service/V1/ModuleServiceTest.php -new file mode 100644 -index 00000000000..c7ff1d95617 ---- /dev/null -+++ b/app/code/Magento/Backend/Test/Unit/Service/V1/ModuleServiceTest.php -@@ -0,0 +1,71 @@ -+<?php -+/** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+declare(strict_types=1); -+ -+namespace Magento\Backend\Test\Unit\Service\V1; -+ -+use Magento\Backend\Service\V1\ModuleService; -+use Magento\Framework\Module\ModuleListInterface; -+use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; -+ -+/** -+ * Module List Service Test -+ * -+ * Covers \Magento\Sales\Model\ValidatorResultMerger -+ */ -+class ModuleServiceTest extends \PHPUnit\Framework\TestCase -+{ -+ /** -+ * Testable Object -+ * -+ * @var ModuleService -+ */ -+ private $moduleService; -+ -+ /** -+ * @var ModuleListInterface|\PHPUnit_Framework_MockObject_MockObject -+ */ -+ private $moduleListMock; -+ -+ /** -+ * Object Manager -+ * -+ * @var ObjectManager -+ */ -+ private $objectManager; -+ -+ /** -+ * Set Up -+ * -+ * @return void -+ */ -+ protected function setUp() -+ { -+ $this->moduleListMock = $this->createMock(ModuleListInterface::class); -+ $this->objectManager = new ObjectManager($this); -+ $this->moduleService = $this->objectManager->getObject( -+ ModuleService::class, -+ [ -+ 'moduleList' => $this->moduleListMock, -+ ] -+ ); -+ } -+ -+ /** -+ * Test getModules method -+ * -+ * @return void -+ */ -+ public function testGetModules() -+ { -+ $moduleNames = ['Magento_Backend', 'Magento_Catalog', 'Magento_Customer']; -+ $this->moduleListMock->expects($this->once())->method('getNames')->willReturn($moduleNames); -+ -+ $expected = $moduleNames; -+ $actual = $this->moduleService->getModules(); -+ $this->assertEquals($expected, $actual); -+ } -+} -diff --git a/app/code/Magento/Backend/composer.json b/app/code/Magento/Backend/composer.json -index f9408768136..e54bd136b34 100644 ---- a/app/code/Magento/Backend/composer.json -+++ b/app/code/Magento/Backend/composer.json -@@ -22,6 +22,7 @@ - "magento/module-store": "*", - "magento/module-translation": "*", - "magento/module-ui": "*", -+ "magento/module-authorization": "*", - "magento/module-user": "*" - }, - "suggest": { -diff --git a/app/code/Magento/Backend/etc/adminhtml/di.xml b/app/code/Magento/Backend/etc/adminhtml/di.xml -index d8e9674d2b4..5f566396ab5 100644 ---- a/app/code/Magento/Backend/etc/adminhtml/di.xml -+++ b/app/code/Magento/Backend/etc/adminhtml/di.xml -@@ -14,8 +14,6 @@ - <preference for="Magento\Framework\App\DefaultPathInterface" type="Magento\Backend\App\DefaultPath" /> - <preference for="Magento\Backend\App\ConfigInterface" type="Magento\Backend\App\Config" /> - <preference for="Magento\Framework\App\Response\Http\FileFactory" type="Magento\Backend\App\Response\Http\FileFactory" /> -- <preference for="Magento\Framework\App\Request\ValidatorInterface" -- type="Magento\Backend\App\Request\BackendValidator" /> - <type name="Magento\Framework\Stdlib\DateTime\Timezone"> - <arguments> - <argument name="scopeType" xsi:type="const">Magento\Framework\App\Config\ScopeConfigInterface::SCOPE_TYPE_DEFAULT</argument> -@@ -88,6 +86,7 @@ - <arguments> - <argument name="lifetimePath" xsi:type="const">Magento\Backend\Model\Auth\Session::XML_PATH_SESSION_LIFETIME</argument> - <argument name="sessionName" xsi:type="const">Magento\Backend\Model\Session\AdminConfig::SESSION_NAME_ADMIN</argument> -+ <argument name="scopeType" xsi:type="const">Magento\Framework\App\Config\ScopeConfigInterface::SCOPE_TYPE_DEFAULT</argument> - </arguments> - </type> - <type name="Magento\Framework\View\Result\PageFactory"> -@@ -169,4 +168,6 @@ - <argument name="defaultClass" xsi:type="string">Magento\Backend\Block\Template</argument> - </arguments> - </type> -+ <preference for="CsrfRequestValidator" type="Magento\Backend\App\Request\BackendValidator" /> -+ <preference for="Magento\Backend\Model\Image\UploadResizeConfigInterface" type="Magento\Backend\Model\Image\UploadResizeConfig" /> - </config> -diff --git a/app/code/Magento/Backend/etc/adminhtml/system.xml b/app/code/Magento/Backend/etc/adminhtml/system.xml -index be1b836d648..65744e56d94 100644 ---- a/app/code/Magento/Backend/etc/adminhtml/system.xml -+++ b/app/code/Magento/Backend/etc/adminhtml/system.xml -@@ -112,7 +112,7 @@ - <group id="debug" translate="label" type="text" sortOrder="20" showInDefault="1" showInWebsite="1" showInStore="1"> - <label>Debug</label> - <field id="template_hints_storefront" translate="label" type="select" sortOrder="20" showInDefault="1" showInWebsite="1" showInStore="1"> -- <label>Enabled Template Path Hints for Storefront</label> -+ <label>Enable Template Path Hints for Storefront</label> - <source_model>Magento\Config\Model\Config\Source\Yesno</source_model> - </field> - <field id="template_hints_storefront_show_with_parameter" translate="label" type="select" sortOrder="20" showInDefault="1" showInWebsite="1" showInStore="1"> -@@ -129,10 +129,10 @@ - <field id="*/*/template_hints_storefront">1</field> - <field id="*/*/template_hints_storefront_show_with_parameter">1</field> - </depends> -- <comment>Add the following paramater to the URL to show template hints ?templatehints=[parameter_value]</comment> -+ <comment>Add the following parameter to the URL to show template hints ?templatehints=[parameter_value]</comment> - </field> - <field id="template_hints_admin" translate="label" type="select" sortOrder="20" showInDefault="1" showInWebsite="0" showInStore="0"> -- <label>Enabled Template Path Hints for Admin</label> -+ <label>Enable Template Path Hints for Admin</label> - <source_model>Magento\Config\Model\Config\Source\Yesno</source_model> - </field> - <field id="template_hints_blocks" translate="label" type="select" sortOrder="21" showInDefault="1" showInWebsite="1" showInStore="1"> -@@ -169,27 +169,31 @@ - </group> - <group id="js" translate="label" type="text" sortOrder="100" showInDefault="1" showInWebsite="1" showInStore="1"> - <label>JavaScript Settings</label> -- <field id="merge_files" translate="label" type="select" sortOrder="10" showInDefault="1" showInWebsite="1" showInStore="1"> -+ <field id="merge_files" translate="label" type="select" sortOrder="10" showInDefault="1" showInWebsite="1" showInStore="1" canRestore="1"> - <label>Merge JavaScript Files</label> - <source_model>Magento\Config\Model\Config\Source\Yesno</source_model> - </field> -- <field id="enable_js_bundling" translate="label" type="select" sortOrder="10" showInDefault="1" showInWebsite="1" showInStore="1"> -+ <field id="enable_js_bundling" translate="label" type="select" sortOrder="10" showInDefault="1" showInWebsite="1" showInStore="1" canRestore="1"> - <label>Enable JavaScript Bundling</label> - <source_model>Magento\Config\Model\Config\Source\Yesno</source_model> - </field> -- <field id="minify_files" translate="label comment" type="select" sortOrder="20" showInDefault="1" showInWebsite="1" showInStore="1"> -+ <field id="minify_files" translate="label comment" type="select" sortOrder="20" showInDefault="1" showInWebsite="1" showInStore="1" canRestore="1"> - <label>Minify JavaScript Files</label> - <source_model>Magento\Config\Model\Config\Source\Yesno</source_model> - <comment>Minification is not applied in developer mode.</comment> - </field> -+ <field id="move_inline_to_bottom" translate="label" type="select" sortOrder="25" showInDefault="1" showInWebsite="1" showInStore="1" canRestore="1"> -+ <label>Move JS code to the bottom of the page</label> -+ <source_model>Magento\Config\Model\Config\Source\Yesno</source_model> -+ </field> - </group> - <group id="css" translate="label" type="text" sortOrder="110" showInDefault="1" showInWebsite="1" showInStore="1"> - <label>CSS Settings</label> -- <field id="merge_css_files" translate="label" type="select" sortOrder="10" showInDefault="1" showInWebsite="1" showInStore="1"> -+ <field id="merge_css_files" translate="label" type="select" sortOrder="10" showInDefault="1" showInWebsite="1" showInStore="1" canRestore="1"> - <label>Merge CSS Files</label> - <source_model>Magento\Config\Model\Config\Source\Yesno</source_model> - </field> -- <field id="minify_files" translate="label comment" type="select" sortOrder="20" showInDefault="1" showInWebsite="1" showInStore="1"> -+ <field id="minify_files" translate="label comment" type="select" sortOrder="20" showInDefault="1" showInWebsite="1" showInStore="1" canRestore="1"> - <label>Minify CSS Files</label> - <source_model>Magento\Config\Model\Config\Source\Yesno</source_model> - <comment>Minification is not applied in developer mode.</comment> -@@ -197,7 +201,7 @@ - </group> - <group id="image" translate="label" type="text" sortOrder="120" showInDefault="1" showInWebsite="0" showInStore="0"> - <label>Image Processing Settings</label> -- <field id="default_adapter" translate="label" type="select" sortOrder="10" showInDefault="1" showInWebsite="0" showInStore="0" canRestore="1"> -+ <field id="default_adapter" translate="label comment" type="select" sortOrder="10" showInDefault="1" showInWebsite="0" showInStore="0" canRestore="1"> - <label>Image Adapter</label> - <source_model>Magento\Config\Model\Config\Source\Image\Adapter</source_model> - <backend_model>Magento\Config\Model\Config\Backend\Image\Adapter</backend_model> -@@ -234,6 +238,7 @@ - <field id="destinations" translate="label" type="multiselect" sortOrder="40" showInDefault="1" showInWebsite="1" showInStore="1"> - <label>Top destinations</label> - <source_model>Magento\Directory\Model\Config\Source\Country</source_model> -+ <can_be_empty>1</can_be_empty> - </field> - </group> - <group id="locale" translate="label" type="text" sortOrder="8" showInDefault="1" showInWebsite="1" showInStore="1"> -@@ -314,19 +319,19 @@ - <label>Disable Email Communications</label> - <source_model>Magento\Config\Model\Config\Source\Yesno</source_model> - </field> -- <field id="host" translate="label" type="text" sortOrder="20" showInDefault="1" showInWebsite="1" showInStore="1" canRestore="1"> -+ <field id="host" translate="label comment" type="text" sortOrder="20" showInDefault="1" showInWebsite="1" showInStore="1" canRestore="1"> - <label>Host</label> - <comment>For Windows server only.</comment> - </field> -- <field id="port" translate="label" type="text" sortOrder="30" showInDefault="1" showInWebsite="1" showInStore="1" canRestore="1"> -+ <field id="port" translate="label comment" type="text" sortOrder="30" showInDefault="1" showInWebsite="1" showInStore="1" canRestore="1"> - <label>Port (25)</label> - <comment>For Windows server only.</comment> - </field> -- <field id="set_return_path" translate="label" type="select" sortOrder="70" showInDefault="1" showInWebsite="0" showInStore="0"> -+ <field id="set_return_path" translate="label" type="select" sortOrder="70" showInDefault="1" showInWebsite="1" showInStore="1"> - <label>Set Return-Path</label> - <source_model>Magento\Config\Model\Config\Source\Yesnocustom</source_model> - </field> -- <field id="return_path_email" translate="label" type="text" sortOrder="80" showInDefault="1" showInWebsite="0" showInStore="0"> -+ <field id="return_path_email" translate="label" type="text" sortOrder="80" showInDefault="1" showInWebsite="1" showInStore="1"> - <label>Return-Path Email</label> - <validate>validate-email</validate> - <backend_model>Magento\Config\Model\Config\Backend\Email\Address</backend_model> -@@ -335,6 +340,30 @@ - </depends> - </field> - </group> -+ <group id="upload_configuration" translate="label" type="text" sortOrder="1000" showInDefault="1" showInWebsite="1" showInStore="1"> -+ <label>Images Upload Configuration</label> -+ <field id="enable_resize" translate="label" type="select" sortOrder="200" showInDefault="1" showInWebsite="0" showInStore="0" canRestore="1"> -+ <label>Enable Frontend Resize</label> -+ <source_model>Magento\Config\Model\Config\Source\Yesno</source_model> -+ <comment>Resize performed via javascript before file upload.</comment> -+ </field> -+ <field id="max_width" translate="label comment" type="text" sortOrder="300" showInDefault="1" showInWebsite="0" showInStore="0" canRestore="1"> -+ <label>Maximum Width</label> -+ <validate>validate-greater-than-zero validate-number required-entry</validate> -+ <comment>Maximum allowed width for uploaded image.</comment> -+ <depends> -+ <field id="enable_resize">1</field> -+ </depends> -+ </field> -+ <field id="max_height" translate="label comment" type="text" sortOrder="400" showInDefault="1" showInWebsite="0" showInStore="0" canRestore="1"> -+ <label>Maximum Height</label> -+ <validate>validate-greater-than-zero validate-number required-entry</validate> -+ <comment>Maximum allowed height for uploaded image.</comment> -+ <depends> -+ <field id="enable_resize">1</field> -+ </depends> -+ </field> -+ </group> - </section> - <section id="admin" translate="label" type="text" sortOrder="20" showInDefault="1" showInWebsite="0" showInStore="0"> - <label>Admin</label> -@@ -435,7 +464,7 @@ - <![CDATA[<strong style="color:red">Warning!</strong> When using Store Code in URLs, in some cases system may not work properly if URLs without Store Codes are specified in the third-party services (e.g. PayPal etc.).]]> - </comment> - </field> -- <field id="redirect_to_base" translate="label" type="select" sortOrder="20" showInDefault="1" showInWebsite="1" showInStore="1" canRestore="1"> -+ <field id="redirect_to_base" translate="label comment" type="select" sortOrder="20" showInDefault="1" showInWebsite="1" showInStore="1" canRestore="1"> - <label>Auto-redirect to Base URL</label> - <source_model>Magento\Config\Model\Config\Source\Web\Redirect</source_model> - <comment>I.e. redirect from http://example.com/store/ to http://www.example.com/store/</comment> -@@ -448,7 +477,7 @@ - <source_model>Magento\Config\Model\Config\Source\Yesno</source_model> - </field> - </group> -- <group id="unsecure" translate="label" type="text" sortOrder="10" showInDefault="1" showInWebsite="1" showInStore="1"> -+ <group id="unsecure" translate="label comment" type="text" sortOrder="10" showInDefault="1" showInWebsite="1" showInStore="1"> - <label>Base URLs</label> - <comment>Any of the fields allow fully qualified URLs that end with '/' (slash) e.g. http://example.com/magento/</comment> - <field id="base_url" translate="label comment" type="text" sortOrder="10" showInDefault="1" showInWebsite="1" showInStore="1"> -@@ -456,7 +485,7 @@ - <backend_model>Magento\Config\Model\Config\Backend\Baseurl</backend_model> - <comment>Specify URL or {{base_url}} placeholder.</comment> - </field> -- <field id="base_link_url" translate="label" type="text" sortOrder="20" showInDefault="1" showInWebsite="1" showInStore="1" canRestore="1"> -+ <field id="base_link_url" translate="label comment" type="text" sortOrder="20" showInDefault="1" showInWebsite="1" showInStore="1" canRestore="1"> - <label>Base Link URL</label> - <backend_model>Magento\Config\Model\Config\Backend\Baseurl</backend_model> - <comment>May start with {{unsecure_base_url}} placeholder.</comment> -@@ -466,13 +495,13 @@ - <backend_model>Magento\Config\Model\Config\Backend\Baseurl</backend_model> - <comment>May be empty or start with {{unsecure_base_url}} placeholder.</comment> - </field> -- <field id="base_media_url" translate="label" type="text" sortOrder="40" showInDefault="1" showInWebsite="1" showInStore="1"> -+ <field id="base_media_url" translate="label comment" type="text" sortOrder="40" showInDefault="1" showInWebsite="1" showInStore="1"> - <label>Base URL for User Media Files</label> - <backend_model>Magento\Config\Model\Config\Backend\Baseurl</backend_model> - <comment>May be empty or start with {{unsecure_base_url}} placeholder.</comment> - </field> - </group> -- <group id="secure" translate="label" type="text" sortOrder="20" showInDefault="1" showInWebsite="1" showInStore="1"> -+ <group id="secure" translate="label comment" type="text" sortOrder="20" showInDefault="1" showInWebsite="1" showInStore="1"> - <label>Base URLs (Secure)</label> - <comment>Any of the fields allow fully qualified URLs that end with '/' (slash) e.g. https://example.com/magento/</comment> - <field id="base_url" translate="label comment" type="text" sortOrder="10" showInDefault="1" showInWebsite="1" showInStore="1"> -@@ -480,7 +509,7 @@ - <backend_model>Magento\Config\Model\Config\Backend\Baseurl</backend_model> - <comment>Specify URL or {{base_url}}, or {{unsecure_base_url}} placeholder.</comment> - </field> -- <field id="base_link_url" translate="label" type="text" sortOrder="20" showInDefault="1" showInWebsite="1" showInStore="1" canRestore="1"> -+ <field id="base_link_url" translate="label comment" type="text" sortOrder="20" showInDefault="1" showInWebsite="1" showInStore="1" canRestore="1"> - <label>Secure Base Link URL</label> - <backend_model>Magento\Config\Model\Config\Backend\Baseurl</backend_model> - <comment>May start with {{secure_base_url}} or {{unsecure_base_url}} placeholder.</comment> -@@ -490,24 +519,24 @@ - <backend_model>Magento\Config\Model\Config\Backend\Baseurl</backend_model> - <comment>May be empty or start with {{secure_base_url}}, or {{unsecure_base_url}} placeholder.</comment> - </field> -- <field id="base_media_url" translate="label" type="text" sortOrder="40" showInDefault="1" showInWebsite="1" showInStore="1"> -+ <field id="base_media_url" translate="label comment" type="text" sortOrder="40" showInDefault="1" showInWebsite="1" showInStore="1"> - <label>Secure Base URL for User Media Files</label> - <backend_model>Magento\Config\Model\Config\Backend\Baseurl</backend_model> - <comment>May be empty or start with {{secure_base_url}}, or {{unsecure_base_url}} placeholder.</comment> - </field> -- <field id="use_in_frontend" translate="label" type="select" sortOrder="50" showInDefault="1" showInWebsite="1" showInStore="1" canRestore="1"> -+ <field id="use_in_frontend" translate="label comment" type="select" sortOrder="50" showInDefault="1" showInWebsite="1" showInStore="1" canRestore="1"> - <label>Use Secure URLs on Storefront</label> - <source_model>Magento\Config\Model\Config\Source\Yesno</source_model> - <backend_model>Magento\Config\Model\Config\Backend\Secure</backend_model> - <comment>Enter https protocol to use Secure URLs on Storefront.</comment> - </field> -- <field id="use_in_adminhtml" translate="label" type="select" sortOrder="60" showInDefault="1" showInWebsite="0" showInStore="0" canRestore="1"> -+ <field id="use_in_adminhtml" translate="label comment" type="select" sortOrder="60" showInDefault="1" showInWebsite="0" showInStore="0" canRestore="1"> - <label>Use Secure URLs in Admin</label> - <source_model>Magento\Config\Model\Config\Source\Yesno</source_model> - <backend_model>Magento\Config\Model\Config\Backend\Secure</backend_model> - <comment>Enter https protocol to use Secure URLs in Admin.</comment> - </field> -- <field id="enable_hsts" translate="label" type="select" sortOrder="70" showInDefault="1" showInWebsite="1" showInStore="1"> -+ <field id="enable_hsts" translate="label comment" type="select" sortOrder="70" showInDefault="1" showInWebsite="1" showInStore="1"> - <label>Enable HTTP Strict Transport Security (HSTS)</label> - <source_model>Magento\Config\Model\Config\Source\Yesno</source_model> - <backend_model>Magento\Config\Model\Config\Backend\Secure</backend_model> -@@ -517,7 +546,7 @@ - <field id="use_in_adminhtml">1</field> - </depends> - </field> -- <field id="enable_upgrade_insecure" translate="label" type="select" sortOrder="80" showInDefault="1" showInWebsite="1" showInStore="1"> -+ <field id="enable_upgrade_insecure" translate="label comment" type="select" sortOrder="80" showInDefault="1" showInWebsite="1" showInStore="1"> - <label>Upgrade Insecure Requests</label> - <source_model>Magento\Config\Model\Config\Source\Yesno</source_model> - <backend_model>Magento\Config\Model\Config\Backend\Secure</backend_model> -diff --git a/app/code/Magento/Backend/etc/config.xml b/app/code/Magento/Backend/etc/config.xml -index b7aaf8bf20d..8283fa18dd3 100644 ---- a/app/code/Magento/Backend/etc/config.xml -+++ b/app/code/Magento/Backend/etc/config.xml -@@ -28,6 +28,11 @@ - <dashboard> - <enable_charts>1</enable_charts> - </dashboard> -+ <upload_configuration> -+ <enable_resize>1</enable_resize> -+ <max_width>1920</max_width> -+ <max_height>1200</max_height> -+ </upload_configuration> - </system> - <general> - <validator_data> -diff --git a/app/code/Magento/Backend/etc/di.xml b/app/code/Magento/Backend/etc/di.xml -index c526703da99..41db85b9323 100644 ---- a/app/code/Magento/Backend/etc/di.xml -+++ b/app/code/Magento/Backend/etc/di.xml -@@ -198,4 +198,8 @@ - <argument name="anchorRenderer" xsi:type="object">Magento\Backend\Block\AnchorRenderer</argument> - </arguments> - </type> -+ <preference for="Magento\Backend\Spi\SessionUserHydratorInterface" -+ type="Magento\Backend\Model\Auth\SessionUserHydrator" /> -+ <preference for="Magento\Backend\Spi\SessionAclHydratorInterface" -+ type="Magento\Backend\Model\Auth\SessionAclHydrator" /> - </config> -diff --git a/app/code/Magento/Backend/etc/module.xml b/app/code/Magento/Backend/etc/module.xml -index 6d1691a0e56..03976396f6f 100644 ---- a/app/code/Magento/Backend/etc/module.xml -+++ b/app/code/Magento/Backend/etc/module.xml -@@ -6,9 +6,10 @@ - */ - --> - <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:Module/etc/module.xsd"> -- <module name="Magento_Backend" > -+ <module name="Magento_Backend"> - <sequence> - <module name="Magento_Directory"/> -+ <module name="Magento_Theme"/> - </sequence> - </module> - </config> -diff --git a/app/code/Magento/Backend/view/adminhtml/layout/adminhtml_cache_block.xml b/app/code/Magento/Backend/view/adminhtml/layout/adminhtml_cache_block.xml -index 50d210f7102..6d2ecd8d36a 100644 ---- a/app/code/Magento/Backend/view/adminhtml/layout/adminhtml_cache_block.xml -+++ b/app/code/Magento/Backend/view/adminhtml/layout/adminhtml_cache_block.xml -@@ -11,7 +11,7 @@ - <block class="Magento\Backend\Block\Widget\Grid" name="adminhtml.cache.grid" as="grid"> - <arguments> - <argument name="id" xsi:type="string">cache_grid</argument> -- <argument name="dataSource" xsi:type="object">Magento\Backend\Model\Cache\ResourceModel\Grid\Collection</argument> -+ <argument name="dataSource" xsi:type="object" shared="false">Magento\Backend\Model\Cache\ResourceModel\Grid\Collection</argument> - <argument name="pager_visibility" xsi:type="string">0</argument> - </arguments> - <block class="Magento\Backend\Block\Widget\Grid\Massaction" name="adminhtml.cache.massaction" as="grid.massaction"> -diff --git a/app/code/Magento/Backend/view/adminhtml/layout/adminhtml_system_design_grid_block.xml b/app/code/Magento/Backend/view/adminhtml/layout/adminhtml_system_design_grid_block.xml -index b96614f4bd8..41bfe6e78a2 100644 ---- a/app/code/Magento/Backend/view/adminhtml/layout/adminhtml_system_design_grid_block.xml -+++ b/app/code/Magento/Backend/view/adminhtml/layout/adminhtml_system_design_grid_block.xml -@@ -11,7 +11,7 @@ - <block class="Magento\Backend\Block\Widget\Grid" name="adminhtml.system.design.grid" as="grid"> - <arguments> - <argument name="id" xsi:type="string">designGrid</argument> -- <argument name="dataSource" xsi:type="object">Magento\Theme\Model\ResourceModel\Design\Collection</argument> -+ <argument name="dataSource" xsi:type="object" shared="false">Magento\Theme\Model\ResourceModel\Design\Collection</argument> - <argument name="use_ajax" xsi:type="string">1</argument> - <argument name="save_parameters_in_session" xsi:type="string">1</argument> - <argument name="grid_url" xsi:type="url" path="*/*/grid"> -diff --git a/app/code/Magento/Backend/view/adminhtml/layout/adminhtml_system_store_grid_block.xml b/app/code/Magento/Backend/view/adminhtml/layout/adminhtml_system_store_grid_block.xml -index 126de5eb408..d0c0d8fcbf6 100644 ---- a/app/code/Magento/Backend/view/adminhtml/layout/adminhtml_system_store_grid_block.xml -+++ b/app/code/Magento/Backend/view/adminhtml/layout/adminhtml_system_store_grid_block.xml -@@ -12,7 +12,7 @@ - <arguments> - <argument name="id" xsi:type="string">storeGrid</argument> - <argument name="save_parameters_in_session" xsi:type="string">1</argument> -- <argument name="dataSource" xsi:type="object">Magento\Store\Model\ResourceModel\Website\Grid\Collection</argument> -+ <argument name="dataSource" xsi:type="object" shared="false">Magento\Store\Model\ResourceModel\Website\Grid\Collection</argument> - </arguments> - <block class="Magento\Backend\Block\Widget\Grid\ColumnSet" name="adminhtml.system.store.grid.columnSet" as="grid.columnSet"> - <arguments> -diff --git a/app/code/Magento/Backend/view/adminhtml/layout/default.xml b/app/code/Magento/Backend/view/adminhtml/layout/default.xml -index e80932e5039..a7faab0bc46 100644 ---- a/app/code/Magento/Backend/view/adminhtml/layout/default.xml -+++ b/app/code/Magento/Backend/view/adminhtml/layout/default.xml -@@ -60,11 +60,17 @@ - </container> - <container name="legal.system" htmlTag="div" htmlClass="footer-legal-system col-m-6"> - <block class="Magento\Backend\Block\Page\Footer" name="version" as="version" /> -+ <block class="Magento\Framework\View\Element\Template" name="privacyPolicy" as="privacyPolicy" template="Magento_Backend::page/privacyPolicy.phtml"> -+ <arguments> -+ <argument name="privacypolicy_url" xsi:type="string">https://magento.com/sites/default/files/REVISED-MAGENTO-PRIVACY-POLICY.pdf</argument> -+ </arguments> -+ </block> - <block class="Magento\Framework\View\Element\Template" name="report" as="report" template="Magento_Backend::page/report.phtml"> - <arguments> - <argument name="bugreport_url" xsi:type="string">https://github.com/magento/magento2/issues</argument> - </arguments> - </block> -+ - </container> - </container> - </referenceContainer> -diff --git a/app/code/Magento/Backend/view/adminhtml/templates/admin/access_denied.phtml b/app/code/Magento/Backend/view/adminhtml/templates/admin/access_denied.phtml -index 843328fbf17..be309423c48 100644 ---- a/app/code/Magento/Backend/view/adminhtml/templates/admin/access_denied.phtml -+++ b/app/code/Magento/Backend/view/adminhtml/templates/admin/access_denied.phtml -@@ -3,14 +3,13 @@ - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -- --// @codingStandardsIgnoreFile -- - ?> - <?php - /** - * @see \Magento\Backend\Block\Denied - */ -+ -+// phpcs:disable Magento2.Security.Superglobal - ?> - <hr class="access-denied-hr"/> - <div class="access-denied-page"> -@@ -21,10 +20,10 @@ - <li><span><?= $block->escapeHtml(__('Contact a system administrator or store owner to gain permissions.')) ?></span></li> - <li> - <span><?= $block->escapeHtml(__('Return to ')) ?> -- <?php if(isset($_SERVER['HTTP_REFERER'])): ?> -+ <?php if (isset($_SERVER['HTTP_REFERER'])) : ?> - <a href="<?= $block->escapeUrl(__($_SERVER['HTTP_REFERER'])) ?>"> - <?= $block->escapeHtml(__('previous page')) ?></a><?= $block->escapeHtml(__('.')) ?> -- <?php else: ?> -+ <?php else : ?> - <a href="<?= $block->escapeHtmlAttr(__('javascript:history.back()')) ?>"> - <?= $block->escapeHtml(__('previous page')) ?></a><?= $block->escapeHtml(__('.')) ?> - <?php endif ?> -diff --git a/app/code/Magento/Backend/view/adminhtml/templates/admin/formkey.phtml b/app/code/Magento/Backend/view/adminhtml/templates/admin/formkey.phtml -index 9629db9fa45..edc14190e4e 100644 ---- a/app/code/Magento/Backend/view/adminhtml/templates/admin/formkey.phtml -+++ b/app/code/Magento/Backend/view/adminhtml/templates/admin/formkey.phtml -@@ -4,4 +4,4 @@ - * See COPYING.txt for license details. - */ - ?> --<div><input name="form_key" type="hidden" value="<?= /* @escapeNotVerified */ $block->getFormKey() ?>" /></div> -+<div><input name="form_key" type="hidden" value="<?= $block->escapeHtmlAttr($block->getFormKey()) ?>" /></div> -diff --git a/app/code/Magento/Backend/view/adminhtml/templates/admin/login.phtml b/app/code/Magento/Backend/view/adminhtml/templates/admin/login.phtml -index 805e9783f3f..1d05450f44c 100644 ---- a/app/code/Magento/Backend/view/adminhtml/templates/admin/login.phtml -+++ b/app/code/Magento/Backend/view/adminhtml/templates/admin/login.phtml -@@ -4,19 +4,20 @@ - * See COPYING.txt for license details. - */ - --// @codingStandardsIgnoreFile -- -+/** -+ * @var \Magento\Framework\View\Element\AbstractBlock $block -+ */ - ?> - - <form method="post" action="" id="login-form" data-mage-init='{"form": {}, "validation": {}}' autocomplete="off"> - <fieldset class="admin__fieldset"> - <legend class="admin__legend"> -- <span><?= /* @escapeNotVerified */ __('Welcome, please sign in') ?></span> -+ <span><?= $block->escapeHtml(__('Welcome, please sign in')) ?></span> - </legend><br/> -- <input name="form_key" type="hidden" value="<?= /* @escapeNotVerified */ $block->getFormKey() ?>" /> -+ <input name="form_key" type="hidden" value="<?= $block->escapeHtmlAttr($block->getFormKey()) ?>" /> - <div class="admin__field _required field-username"> - <label for="username" class="admin__field-label"> -- <span><?= /* @escapeNotVerified */ __('Username') ?></span> -+ <span><?= $block->escapeHtml(__('Username')) ?></span> - </label> - <div class="admin__field-control"> - <input id="username" -@@ -26,14 +27,14 @@ - autofocus - value="" - data-validate="{required:true}" -- placeholder="<?= /* @escapeNotVerified */ __('user name') ?>" -+ placeholder="<?= $block->escapeHtmlAttr(__('user name')) ?>" - autocomplete="off" - /> - </div> - </div> - <div class="admin__field _required field-password"> - <label for="login" class="admin__field-label"> -- <span><?= /* @escapeNotVerified */ __('Password') ?></span> -+ <span><?= $block->escapeHtml(__('Password')) ?></span> - </label> - <div class="admin__field-control"> - <input id="login" -@@ -42,8 +43,8 @@ - name="login[password]" - data-validate="{required:true}" - value="" -- placeholder="<?= /* @escapeNotVerified */ __('password') ?>" -- autocomplete="new-password" -+ placeholder="<?= $block->escapeHtmlAttr(__('password')) ?>" -+ autocomplete="off" - /> - </div> - </div> -diff --git a/app/code/Magento/Backend/view/adminhtml/templates/admin/login_buttons.phtml b/app/code/Magento/Backend/view/adminhtml/templates/admin/login_buttons.phtml -index 2459bc54e0c..18ee8a86517 100644 ---- a/app/code/Magento/Backend/view/adminhtml/templates/admin/login_buttons.phtml -+++ b/app/code/Magento/Backend/view/adminhtml/templates/admin/login_buttons.phtml -@@ -8,6 +8,6 @@ - <button - <?php $block->getUiId(); ?> - class="action-login action-primary"> -- <span><?= /* @escapeNotVerified */ __('Sign in') ?></span> -+ <span><?= $block->escapeHtml(__('Sign in')) ?></span> - </button> - </div> -diff --git a/app/code/Magento/Backend/view/adminhtml/templates/admin/overlay_popup.phtml b/app/code/Magento/Backend/view/adminhtml/templates/admin/overlay_popup.phtml -index 93509cc62f7..ac81861c993 100644 ---- a/app/code/Magento/Backend/view/adminhtml/templates/admin/overlay_popup.phtml -+++ b/app/code/Magento/Backend/view/adminhtml/templates/admin/overlay_popup.phtml -@@ -3,15 +3,12 @@ - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -- --// @codingStandardsIgnoreFile -- - ?> - <div class="wrapper-popup"> - <div class="middle" id="anchor-content"> - <div id="page:main-container"> -- <?php if ($block->getChildHtml('left')): ?> -- <div class="columns <?= /* @escapeNotVerified */ $block->getContainerCssClass() ?>" id="page:container"> -+ <?php if ($block->getChildHtml('left')) : ?> -+ <div class="columns <?= $block->escapeHtmlAttr($block->getContainerCssClass()) ?>" id="page:container"> - <div id="page:left" class="side-col"> - <?= $block->getChildHtml('left') ?> - </div> -@@ -24,13 +21,13 @@ - </div> - </div> - </div> -- <?php else: ?> -+ <?php else : ?> - <div id="messages" data-container-for="messages"><?= $block->getLayout()->getMessagesBlock()->getGroupedHtml() ?></div> - <?= $block->getChildHtml('content') ?> -- <?php endif; ?> -+ <?php endif; ?> - </div> - </div> -- <?php if ($block->getChildHtml('footer')): ?> -+ <?php if ($block->getChildHtml('footer')) : ?> - <div class="footer"> - <?= $block->getChildHtml('footer') ?> - </div> -diff --git a/app/code/Magento/Backend/view/adminhtml/templates/admin/page.phtml b/app/code/Magento/Backend/view/adminhtml/templates/admin/page.phtml -index ebb8e26c93f..d8cab67ecb7 100644 ---- a/app/code/Magento/Backend/view/adminhtml/templates/admin/page.phtml -+++ b/app/code/Magento/Backend/view/adminhtml/templates/admin/page.phtml -@@ -3,19 +3,16 @@ - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -- --// @codingStandardsIgnoreFile -- - ?> - <?php /** @var $block \Magento\Backend\Block\Page */ ?> - <!doctype html> --<html lang="<?= /* @escapeNotVerified */ $block->getLang() ?>" class="no-js"> -+<html lang="<?= $block->escapeHtmlAttr($block->getLang()) ?>" class="no-js"> - - <head> - <?= $block->getChildHtml('head') ?> - </head> - --<body id="html-body"<?= $block->getBodyClass() ? ' class="' . $block->getBodyClass() . '"' : '' ?> data-container="body" data-mage-init='{"loaderAjax":{},"loader":{}}'> -+<body id="html-body" class="<?= $block->escapeHtmlAttr($block->getBodyClass()) ?>" data-container="body" data-mage-init='{"loaderAjax":{},"loader":{}}'> - <div class="page-wrapper"> - <?= $block->getChildHtml('notification_window') ?> - <?= $block->getChildHtml('global_notices') ?> -@@ -31,8 +28,8 @@ - <?= $block->getLayout()->getMessagesBlock()->getGroupedHtml() ?> - </div> - <?= $block->getChildHtml('page_main_actions') ?> -- <?php if ($block->getChildHtml('left')): ?> -- <div id="page:main-container" class="<?= /* @escapeNotVerified */ $block->getContainerCssClass() ?> col-2-left-layout"> -+ <?php if ($block->getChildHtml('left')) : ?> -+ <div id="page:main-container" class="<?= $block->escapeHtmlAttr($block->getContainerCssClass()) ?> col-2-left-layout"> - <div class="main-col" id="content"> - <?= $block->getChildHtml('content') ?> - </div> -@@ -41,7 +38,7 @@ - <?= $block->getChildHtml('left') ?> - </div> - </div> -- <?php else: ?> -+ <?php else : ?> - <div id="page:main-container" class="col-1-layout"> - <?= $block->getChildHtml('content') ?> - </div> -diff --git a/app/code/Magento/Backend/view/adminhtml/templates/dashboard/graph.phtml b/app/code/Magento/Backend/view/adminhtml/templates/dashboard/graph.phtml -index ae123511bd4..12b388c2107 100644 ---- a/app/code/Magento/Backend/view/adminhtml/templates/dashboard/graph.phtml -+++ b/app/code/Magento/Backend/view/adminhtml/templates/dashboard/graph.phtml -@@ -3,33 +3,33 @@ - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -- --// @codingStandardsIgnoreFile -- - ?> - <div class="dashboard-diagram"> - <div class="dashboard-diagram-switcher"> - <label for="order_<?= $block->getHtmlId() ?>_period" -- class="label"><?= /* @escapeNotVerified */ __('Select Range:') ?></label> -+ class="label"><?= $block->escapeHtml(__('Select Range:')) ?></label> - <select name="period" id="order_<?= $block->getHtmlId() ?>_period" - onchange="changeDiagramsPeriod(this);" class="admin__control-select"> -- <?php foreach ($this->helper('Magento\Backend\Helper\Dashboard\Data')->getDatePeriods() as $value => $label): ?> -- <?php if (in_array($value, ['custom'])) { -+ <?php //phpcs:disable ?> -+ <?php foreach ($this->helper(\Magento\Backend\Helper\Dashboard\Data::class)->getDatePeriods() as $value => $label) : ?> -+ <?php -+ //phpcs:enable -+ if (in_array($value, ['custom'])) { - continue; - } ?> -- <option value="<?= /* @escapeNotVerified */ $value ?>" -- <?php if ($block->getRequest()->getParam('period') == $value): ?> selected="selected"<?php endif; ?> -- ><?= /* @escapeNotVerified */ $label ?></option> -+ <option value="<?= /* @noEscape */ $value ?>" -+ <?php if ($block->getRequest()->getParam('period') == $value) : ?> selected="selected"<?php endif; ?> -+ ><?= $block->escapeHtml($label) ?></option> - <?php endforeach; ?> - </select> - </div> -- <?php if ($block->getCount()): ?> -+ <?php if ($block->getCount()) : ?> - <div class="dashboard-diagram-image"> -- <img src="<?= /* @escapeNotVerified */ $block->getChartUrl(false) ?>" class="dashboard-diagram-chart" alt="Chart" title="Chart" /> -+ <img src="<?= $block->escapeUrl($block->getChartUrl(false)) ?>" class="dashboard-diagram-chart" alt="Chart" title="Chart" /> - </div> -- <?php else: ?> -+ <?php else : ?> - <div class="dashboard-diagram-nodata"> -- <span><?= /* @escapeNotVerified */ __('No Data Found') ?></span> -+ <span><?= $block->escapeHtml(__('No Data Found')) ?></span> - </div> - <?php endif; ?> - </div> -diff --git a/app/code/Magento/Backend/view/adminhtml/templates/dashboard/graph/disabled.phtml b/app/code/Magento/Backend/view/adminhtml/templates/dashboard/graph/disabled.phtml -index 7dddc151218..f8e584ce5b9 100644 ---- a/app/code/Magento/Backend/view/adminhtml/templates/dashboard/graph/disabled.phtml -+++ b/app/code/Magento/Backend/view/adminhtml/templates/dashboard/graph/disabled.phtml -@@ -3,9 +3,7 @@ - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -- --// @codingStandardsIgnoreFile - ?> - <div class="dashboard-diagram-disabled"> -- <?= /* @escapeNotVerified */ __('Chart is disabled. To enable the chart, click <a href="%1">here</a>.', $block->getConfigUrl()) ?> -+ <?= /* @noEscape */ __('Chart is disabled. To enable the chart, click <a href="%1">here</a>.', $block->escapeUrl($block->getConfigUrl())) ?> - </div> -diff --git a/app/code/Magento/Backend/view/adminhtml/templates/dashboard/grid.phtml b/app/code/Magento/Backend/view/adminhtml/templates/dashboard/grid.phtml -index 1041aef59ce..7c05335642b 100644 ---- a/app/code/Magento/Backend/view/adminhtml/templates/dashboard/grid.phtml -+++ b/app/code/Magento/Backend/view/adminhtml/templates/dashboard/grid.phtml -@@ -3,90 +3,87 @@ - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -- --// @codingStandardsIgnoreFile -- - ?> - <?php - --$numColumns = sizeof($block->getColumns()); -+$numColumns = count($block->getColumns()); - ?> --<?php if ($block->getCollection()): ?> -+<?php if ($block->getCollection()) : ?> - <div class="dashboard-item-content"> -- <?php if ($block->getCollection()->getSize()>0): ?> -- <table class="admin__table-primary dashboard-data" id="<?= /* @escapeNotVerified */ $block->getId() ?>_table"> -+ <?php if ($block->getCollection()->getSize() > 0) : ?> -+ <table class="admin__table-primary dashboard-data" id="<?= $block->escapeHtmlAttr($block->getId()) ?>_table"> - <?php - /* This part is commented to remove all <col> tags from the code. */ - /* foreach ($block->getColumns() as $_column): ?> - <col <?= $_column->getHtmlProperty() ?> /> - <?php endforeach; */ ?> -- <?php if ($block->getHeadersVisibility() || $block->getFilterVisibility()): ?> -+ <?php if ($block->getHeadersVisibility() || $block->getFilterVisibility()) : ?> - <thead> -- <?php if ($block->getHeadersVisibility()): ?> -+ <?php if ($block->getHeadersVisibility()) : ?> - <tr> -- <?php foreach ($block->getColumns() as $_column): ?> -+ <?php foreach ($block->getColumns() as $_column) : ?> - <?= $_column->getHeaderHtml() ?> - <?php endforeach; ?> - </tr> - <?php endif; ?> - </thead> - <?php endif; ?> -- <?php if (!$block->getIsCollapsed()): ?> -+ <?php if (!$block->getIsCollapsed()) : ?> - <tbody> -- <?php foreach ($block->getCollection() as $_index => $_item): ?> -- <tr title="<?= /* @escapeNotVerified */ $block->getRowUrl($_item) ?>"> -- <?php $i = 0; foreach ($block->getColumns() as $_column): ?> -- <td class="<?= /* @escapeNotVerified */ $_column->getCssProperty() ?> <?= ++$i == $numColumns ? 'last' : '' ?>"><?= (($_html = $_column->getRowField($_item)) != '' ? $_html : ' ') ?></td> -+ <?php foreach ($block->getCollection() as $_index => $_item) : ?> -+ <tr title="<?= $block->escapeHtmlAttr($block->getRowUrl($_item)) ?>"> -+ <?php $i = 0; foreach ($block->getColumns() as $_column) : ?> -+ <td class="<?= $block->escapeHtmlAttr($_column->getCssProperty()) ?> <?= /* @noEscape */ ++$i == $numColumns ? 'last' : '' ?>"><?= /* @noEscape */ (($_html = $_column->getRowField($_item)) != '' ? $_html : ' ') ?></td> - <?php endforeach; ?> - </tr> - <?php endforeach; ?> - </tbody> - <?php endif; ?> - </table> -- <?php else: ?> -- <div class="<?= /* @escapeNotVerified */ $block->getEmptyTextClass() ?>"><?= /* @escapeNotVerified */ $block->getEmptyText() ?></div> -+ <?php else : ?> -+ <div class="<?= $block->escapeHtmlAttr($block->getEmptyTextClass()) ?>"><?= $block->escapeHtml($block->getEmptyText()) ?></div> - <?php endif; ?> - </div> --<?php if ($block->canDisplayContainer()): ?> -+ <?php if ($block->canDisplayContainer()) : ?> - <script> - var deps = []; - --<?php if ($block->getDependencyJsObject()): ?> -+ <?php if ($block->getDependencyJsObject()) : ?> - deps.push('uiRegistry'); --<?php endif; ?> -+ <?php endif; ?> - --<?php if (strpos($block->getRowClickCallback(), 'order.') !== false): ?> -+ <?php if (strpos($block->getRowClickCallback(), 'order.') !== false) : ?> - deps.push('Magento_Sales/order/create/form'); --<?php endif; ?> -+ <?php endif; ?> - - deps.push('mage/adminhtml/grid'); - - require(deps, function(<?= ($block->getDependencyJsObject() ? 'registry' : '') ?>){ -- <?php //TODO: getJsObjectName and getRowClickCallback has unexpected behavior. Should be removed ?> -+ <?php //TODO: getJsObjectName and getRowClickCallback has unexpected behavior. Should be removed ?> - -- <?php if ($block->getDependencyJsObject()): ?> -- registry.get('<?= /* @escapeNotVerified */ $block->getDependencyJsObject() ?>', function (<?= /* @escapeNotVerified */ $block->getDependencyJsObject() ?>) { -- <?php endif; ?> -+ <?php if ($block->getDependencyJsObject()) : ?> -+ registry.get('<?= $block->escapeJs($block->getDependencyJsObject()) ?>', function (<?= $block->escapeJs($block->getDependencyJsObject()) ?>) { -+ <?php endif; ?> - -- <?= /* @escapeNotVerified */ $block->getJsObjectName() ?> = new varienGrid('<?= /* @escapeNotVerified */ $block->getId() ?>', '<?= /* @escapeNotVerified */ $block->getGridUrl() ?>', '<?= /* @escapeNotVerified */ $block->getVarNamePage() ?>', '<?= /* @escapeNotVerified */ $block->getVarNameSort() ?>', '<?= /* @escapeNotVerified */ $block->getVarNameDir() ?>', '<?= /* @escapeNotVerified */ $block->getVarNameFilter() ?>'); -- <?= /* @escapeNotVerified */ $block->getJsObjectName() ?>.useAjax = '<?= /* @escapeNotVerified */ $block->getUseAjax() ?>'; -- <?php if ($block->getRowClickCallback()): ?> -- <?= /* @escapeNotVerified */ $block->getJsObjectName() ?>.rowClickCallback = <?= /* @escapeNotVerified */ $block->getRowClickCallback() ?>; -- <?php endif; ?> -- <?php if ($block->getCheckboxCheckCallback()): ?> -- <?= /* @escapeNotVerified */ $block->getJsObjectName() ?>.checkboxCheckCallback = <?= /* @escapeNotVerified */ $block->getCheckboxCheckCallback() ?>; -- <?php endif; ?> -- <?php if ($block->getRowInitCallback()): ?> -- <?= /* @escapeNotVerified */ $block->getJsObjectName() ?>.initRowCallback = <?= /* @escapeNotVerified */ $block->getRowInitCallback() ?>; -- <?= /* @escapeNotVerified */ $block->getJsObjectName() ?>.rows.each(function(row){<?= /* @escapeNotVerified */ $block->getRowInitCallback() ?>(<?= /* @escapeNotVerified */ $block->getJsObjectName() ?>, row)}); -- <?php endif; ?> -- <?php if ($block->getMassactionBlock()->isAvailable()): ?> -- <?= /* @escapeNotVerified */ $block->getMassactionBlock()->getJavaScript() ?> -- <?php endif ?> -+ <?= $block->escapeJs($block->getJsObjectName()) ?> = new varienGrid('<?= $block->escapeJs($block->getId()) ?>', '<?= $block->escapeJs($block->getGridUrl()) ?>', '<?= $block->escapeJs($block->getVarNamePage()) ?>', '<?= $block->escapeJs($block->getVarNameSort()) ?>', '<?= $block->escapeJs($block->getVarNameDir()) ?>', '<?= $block->escapeJs($block->getVarNameFilter()) ?>'); -+ <?= $block->escapeJs($block->getJsObjectName()) ?>.useAjax = '<?= $block->escapeJs($block->getUseAjax()) ?>'; -+ <?php if ($block->getRowClickCallback()) : ?> -+ <?= $block->escapeJs($block->getJsObjectName()) ?>.rowClickCallback = <?= /* @noEscape */ $block->getRowClickCallback() ?>; -+ <?php endif; ?> -+ <?php if ($block->getCheckboxCheckCallback()) : ?> -+ <?= $block->escapeJs($block->getJsObjectName()) ?>.checkboxCheckCallback = <?= /* @noEscape */ $block->getCheckboxCheckCallback() ?>; -+ <?php endif; ?> -+ <?php if ($block->getRowInitCallback()) : ?> -+ <?= $block->escapeJs($block->getJsObjectName()) ?>.initRowCallback = <?= /* @noEscape */ $block->getRowInitCallback() ?>; -+ <?= $block->escapeJs($block->getJsObjectName()) ?>.rows.each(function(row){<?= /* @noEscape */ $block->getRowInitCallback() ?>(<?= $block->escapeJs($block->getJsObjectName()) ?>, row)}); -+ <?php endif; ?> -+ <?php if ($block->getMassactionBlock()->isAvailable()) : ?> -+ <?= /* @noEscape */ $block->getMassactionBlock()->getJavaScript() ?> -+ <?php endif ?> - -- <?php if ($block->getDependencyJsObject()): ?> -+ <?php if ($block->getDependencyJsObject()) : ?> - }); -- <?php endif; ?> -+ <?php endif; ?> - - }); - </script> -diff --git a/app/code/Magento/Backend/view/adminhtml/templates/dashboard/index.phtml b/app/code/Magento/Backend/view/adminhtml/templates/dashboard/index.phtml -index 865e0fac383..6152c8fe1cf 100644 ---- a/app/code/Magento/Backend/view/adminhtml/templates/dashboard/index.phtml -+++ b/app/code/Magento/Backend/view/adminhtml/templates/dashboard/index.phtml -@@ -3,9 +3,6 @@ - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -- --// @codingStandardsIgnoreFile -- - ?> - - <?php if (is_array($block->getChildBlock('diagrams')->getTabsIds())) : ?> -@@ -17,13 +14,13 @@ require([ - - window.changeDiagramsPeriod = function(periodObj) { - periodParam = periodObj.value ? 'period/' + periodObj.value + '/' : ''; --<?php foreach ($block->getChildBlock('diagrams')->getTabsIds() as $tabId): ?> -- ajaxBlockParam = 'block/tab_<?= /* @escapeNotVerified */ $tabId ?>/'; -- ajaxBlockUrl = '<?= $block->getUrl('adminhtml/*/ajaxBlock', ['_current' => true, 'block' => '', 'period' => '']) ?>' + ajaxBlockParam + periodParam; -+ <?php foreach ($block->getChildBlock('diagrams')->getTabsIds() as $tabId) : ?> -+ ajaxBlockParam = 'block/tab_<?= $block->escapeJs($tabId) ?>/'; -+ ajaxBlockUrl = '<?= $block->escapeJs($block->getUrl('adminhtml/*/ajaxBlock', ['_current' => true, 'block' => '', 'period' => ''])) ?>' + ajaxBlockParam + periodParam; - new Ajax.Request(ajaxBlockUrl, { - parameters: {isAjax: 'true', form_key: FORM_KEY}, - onSuccess: function(transport) { -- tabContentElementId = '<?= /* @escapeNotVerified */ $block->getChildBlock('diagrams')->getId() ?>_<?= /* @escapeNotVerified */ $tabId ?>_content'; -+ tabContentElementId = '<?= $block->escapeJs($block->getChildBlock('diagrams')->getId()) ?>_<?= $block->escapeJs($tabId) ?>_content'; - try { - if (transport.responseText.isJSON()) { - var response = transport.responseText.evalJSON() -@@ -44,8 +41,8 @@ window.changeDiagramsPeriod = function(periodObj) { - } - } - }); --<?php endforeach; ?> -- ajaxBlockUrl = '<?= $block->getUrl('adminhtml/*/ajaxBlock', ['_current' => true, 'block' => 'totals', 'period' => '']) ?>' + periodParam; -+ <?php endforeach; ?> -+ ajaxBlockUrl = '<?= $block->escapeJs($block->getUrl('adminhtml/*/ajaxBlock', ['_current' => true, 'block' => 'totals', 'period' => ''])) ?>' + periodParam; - new Ajax.Request(ajaxBlockUrl, { - parameters: {isAjax: 'true', form_key: FORM_KEY}, - onSuccess: function(transport) { -@@ -93,15 +90,15 @@ window.changeDiagramsPeriod = function(periodObj) { - <div class="dashboard-secondary col-m-4 col-m-pull-8"> - <?= $block->getChildHtml('sales') ?> - <div class="dashboard-item"> -- <div class="dashboard-item-title"><?= /* @escapeNotVerified */ __('Last Orders') ?></div> -+ <div class="dashboard-item-title"><?= $block->escapeHtml(__('Last Orders')) ?></div> - <?= $block->getChildHtml('lastOrders') ?> - </div> - <div class="dashboard-item"> -- <div class="dashboard-item-title"><?= /* @escapeNotVerified */ __('Last Search Terms') ?></div> -+ <div class="dashboard-item-title"><?= $block->escapeHtml(__('Last Search Terms')) ?></div> - <?= $block->getChildHtml('lastSearches') ?> - </div> - <div class="dashboard-item"> -- <div class="dashboard-item-title"><?= /* @escapeNotVerified */ __('Top Search Terms') ?></div> -+ <div class="dashboard-item-title"><?= $block->escapeHtml(__('Top Search Terms')) ?></div> - <?= $block->getChildHtml('topSearches') ?> - </div> - </div> -diff --git a/app/code/Magento/Backend/view/adminhtml/templates/dashboard/salebar.phtml b/app/code/Magento/Backend/view/adminhtml/templates/dashboard/salebar.phtml -index 450a2c89b50..139a7cad418 100644 ---- a/app/code/Magento/Backend/view/adminhtml/templates/dashboard/salebar.phtml -+++ b/app/code/Magento/Backend/view/adminhtml/templates/dashboard/salebar.phtml -@@ -3,18 +3,15 @@ - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -- --// @codingStandardsIgnoreFile -- - ?> --<?php if (sizeof($block->getTotals()) > 0): ?> -- <?php foreach ($block->getTotals() as $_total): ?> -+<?php if (count($block->getTotals()) > 0) : ?> -+ <?php foreach ($block->getTotals() as $_total) : ?> - <div class="dashboard-item dashboard-item-primary"> -- <div class="dashboard-item-title"><?= /* @escapeNotVerified */ $_total['label'] ?></div> -+ <div class="dashboard-item-title"><?= $block->escapeHtml($_total['label']) ?></div> - <div class="dashboard-item-content"> - <strong class="dashboard-sales-value"> -- <?= /* @escapeNotVerified */ $_total['value'] ?> -- <span class="dashboard-sales-decimals"><?= /* @escapeNotVerified */ $_total['decimals'] ?></span> -+ <?= /* @noEscape */ $_total['value'] ?> -+ <span class="dashboard-sales-decimals"><?= /* @noEscape */ $_total['decimals'] ?></span> - </strong> - </div> - </div> -diff --git a/app/code/Magento/Backend/view/adminhtml/templates/dashboard/searches.phtml b/app/code/Magento/Backend/view/adminhtml/templates/dashboard/searches.phtml -index f6e837fd54e..7a7a71f07fa 100644 ---- a/app/code/Magento/Backend/view/adminhtml/templates/dashboard/searches.phtml -+++ b/app/code/Magento/Backend/view/adminhtml/templates/dashboard/searches.phtml -@@ -3,16 +3,13 @@ - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -- --// @codingStandardsIgnoreFile -- - ?> --<?php if (count($block->getCollection()->getItems()) > 0): ?> -+<?php if (count($block->getCollection()->getItems()) > 0) : ?> - <div class="searches-results"> -- <?php foreach ($block->getCollection()->getItems() as $item): ?> -- <span><?= /* @escapeNotVerified */ $item->getQueryText() ?></span><br /> -+ <?php foreach ($block->getCollection()->getItems() as $item) : ?> -+ <span><?= $block->escapeHtml($item->getQueryText()) ?></span><br /> - <?php endforeach; ?> - </div> --<?php else: ?> -- <div class="empty-text"><?= /* @escapeNotVerified */ __('There are no search keywords.') ?></div> -+<?php else : ?> -+ <div class="empty-text"><?= $block->escapeHtml(__('There are no search keywords.')) ?></div> - <?php endif; ?> -diff --git a/app/code/Magento/Backend/view/adminhtml/templates/dashboard/store/switcher.phtml b/app/code/Magento/Backend/view/adminhtml/templates/dashboard/store/switcher.phtml -index bf9ae27f17b..87e5399ddda 100644 ---- a/app/code/Magento/Backend/view/adminhtml/templates/dashboard/store/switcher.phtml -+++ b/app/code/Magento/Backend/view/adminhtml/templates/dashboard/store/switcher.phtml -@@ -3,31 +3,28 @@ - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -- --// @codingStandardsIgnoreFile -- - ?> --<p class="switcher"><label for="store_switcher"><?= /* @escapeNotVerified */ __('View Statistics For:') ?></label> -+<p class="switcher"><label for="store_switcher"><?= $block->escapeHtml(__('View Statistics For:')) ?></label> - <?= $block->getHintHtml() ?> - <select name="store_switcher" id="store_switcher" class="left-col-block" onchange="return switchStore(this);"> -- <option value=""><?= /* @escapeNotVerified */ __('All Websites') ?></option> -- <?php foreach ($block->getWebsiteCollection() as $_website): ?> -+ <option value=""><?= $block->escapeHtml(__('All Websites')) ?></option> -+ <?php foreach ($block->getWebsiteCollection() as $_website) : ?> - <?php $showWebsite = false; ?> -- <?php foreach ($block->getGroupCollection($_website) as $_group): ?> -+ <?php foreach ($block->getGroupCollection($_website) as $_group) : ?> - <?php $showGroup = false; ?> -- <?php foreach ($block->getStoreCollection($_group) as $_store): ?> -- <?php if ($showWebsite == false): ?> -+ <?php foreach ($block->getStoreCollection($_group) as $_store) : ?> -+ <?php if ($showWebsite == false) : ?> - <?php $showWebsite = true; ?> -- <option website="true" value="<?= /* @escapeNotVerified */ $_website->getId() ?>"<?php if ($block->getRequest()->getParam('website') == $_website->getId()): ?> selected="selected"<?php endif; ?>><?= /* @escapeNotVerified */ $_website->getName() ?></option> -+ <option website="true" value="<?= $block->escapeHtmlAttr($_website->getId()) ?>"<?php if ($block->getRequest()->getParam('website') == $_website->getId()) : ?> selected="selected"<?php endif; ?>><?= $block->escapeHtml($_website->getName()) ?></option> - <?php endif; ?> -- <?php if ($showGroup == false): ?> -+ <?php if ($showGroup == false) : ?> - <?php $showGroup = true; ?> -- <!--optgroup label="   <?= /* @escapeNotVerified */ $_group->getName() ?>"--> -- <option group="true" value="<?= /* @escapeNotVerified */ $_group->getId() ?>"<?php if ($block->getRequest()->getParam('group') == $_group->getId()): ?> selected="selected"<?php endif; ?>>   <?= /* @escapeNotVerified */ $_group->getName() ?></option> -+ <!--optgroup label="   <?= $block->escapeHtmlAttr($_group->getName()) ?>"--> -+ <option group="true" value="<?= $block->escapeHtmlAttr($_group->getId()) ?>"<?php if ($block->getRequest()->getParam('group') == $_group->getId()) : ?> selected="selected"<?php endif; ?>>   <?= $block->escapeHtml($_group->getName()) ?></option> - <?php endif; ?> -- <option value="<?= /* @escapeNotVerified */ $_store->getId() ?>"<?php if ($block->getStoreId() == $_store->getId()): ?> selected="selected"<?php endif; ?>>      <?= /* @escapeNotVerified */ $_store->getName() ?></option> -+ <option value="<?= $block->escapeHtmlAttr($_store->getId()) ?>"<?php if ($block->getStoreId() == $_store->getId()) : ?> selected="selected"<?php endif; ?>>      <?= $block->escapeHtml($_store->getName()) ?></option> - <?php endforeach; ?> -- <?php if ($showGroup): ?> -+ <?php if ($showGroup) : ?> - <!--</optgroup>--> - <?php endif; ?> - <?php endforeach; ?> -@@ -57,7 +54,7 @@ - var select = $('order_amounts_period'); - } - var periodParam = select.value ? 'period/' + select.value + '/' : ''; -- setLocation('<?= /* @escapeNotVerified */ $block->getSwitchUrl() ?>' + storeParam + periodParam); -+ setLocation('<?= $block->escapeJs($block->getSwitchUrl()) ?>' + storeParam + periodParam); - } - }); - </script> -diff --git a/app/code/Magento/Backend/view/adminhtml/templates/dashboard/totalbar.phtml b/app/code/Magento/Backend/view/adminhtml/templates/dashboard/totalbar.phtml -index 8605304b1f5..918eea75fab 100644 ---- a/app/code/Magento/Backend/view/adminhtml/templates/dashboard/totalbar.phtml -+++ b/app/code/Magento/Backend/view/adminhtml/templates/dashboard/totalbar.phtml -@@ -3,19 +3,16 @@ - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -- --// @codingStandardsIgnoreFile -- - ?> --<?php if (sizeof($block->getTotals()) > 0): ?> -+<?php if (count($block->getTotals()) > 0) : ?> - <div class="dashboard-totals" id="dashboard_diagram_totals"> - <ul class="dashboard-totals-list"> -- <?php foreach ($block->getTotals() as $_total): ?> -+ <?php foreach ($block->getTotals() as $_total) : ?> - <li class="dashboard-totals-item"> -- <span class="dashboard-totals-label"><?= /* @escapeNotVerified */ $_total['label'] ?></span> -+ <span class="dashboard-totals-label"><?= $block->escapeHtml($_total['label']) ?></span> - <strong class="dashboard-totals-value"> -- <?= /* @escapeNotVerified */ $_total['value'] ?> -- <span class="dashboard-totals-decimals"><?= /* @escapeNotVerified */ $_total['decimals'] ?></span> -+ <?= /* @noEscape */ $_total['value'] ?> -+ <span class="dashboard-totals-decimals"><?= /* @noEscape */ $_total['decimals'] ?></span> - </strong> - </li> - <?php endforeach; ?> -diff --git a/app/code/Magento/Backend/view/adminhtml/templates/media/uploader.phtml b/app/code/Magento/Backend/view/adminhtml/templates/media/uploader.phtml -index 1e14dd83763..83482b1720c 100644 ---- a/app/code/Magento/Backend/view/adminhtml/templates/media/uploader.phtml -+++ b/app/code/Magento/Backend/view/adminhtml/templates/media/uploader.phtml -@@ -4,24 +4,23 @@ - * See COPYING.txt for license details. - */ - --// @codingStandardsIgnoreFile -- - /** @var $block \Magento\Backend\Block\Media\Uploader */ - ?> - - <div id="<?= $block->getHtmlId() ?>" class="uploader" - data-mage-init='{ - "Magento_Backend/js/media-uploader" : { -- "maxFileSize": <?= /* @escapeNotVerified */ $block->getFileSizeService()->getMaxFileSize() ?>, -- "maxWidth":<?= /* @escapeNotVerified */ \Magento\Framework\File\Uploader::MAX_IMAGE_WIDTH ?> , -- "maxHeight": <?= /* @escapeNotVerified */ \Magento\Framework\File\Uploader::MAX_IMAGE_HEIGHT ?> -+ "maxFileSize": <?= /* @noEscape */ $block->getFileSizeService()->getMaxFileSize() ?>, -+ "maxWidth": <?= /* @noEscape */ $block->getImageUploadMaxWidth() ?>, -+ "maxHeight": <?= /* @noEscape */ $block->getImageUploadMaxHeight() ?>, -+ "isResizeEnabled": <?= /* @noEscape */ $block->getImageUploadConfigData()->getIsResizeEnabled() ?> - } - }' - > - <div class="fileinput-button form-buttons button"> -- <span><?= /* @escapeNotVerified */ __('Browse Files...') ?></span> -- <input id="fileupload" type="file" name="<?= /* @escapeNotVerified */ $block->getConfig()->getFileField() ?>" -- data-url="<?= /* @escapeNotVerified */ $block->getConfig()->getUrl() ?>" multiple="multiple" /> -+ <span><?= $block->escapeHtml(__('Browse Files...')) ?></span> -+ <input id="fileupload" type="file" name="<?= $block->escapeHtmlAttr($block->getConfig()->getFileField()) ?>" -+ data-url="<?= $block->escapeHtmlAttr($block->getConfig()->getUrl()) ?>" multiple="multiple" /> - </div> - <div class="clear"></div> - <script id="<?= $block->getHtmlId() ?>-template" type="text/x-magento-template" data-template="uploader"> -diff --git a/app/code/Magento/Backend/view/adminhtml/templates/menu.phtml b/app/code/Magento/Backend/view/adminhtml/templates/menu.phtml -index c448bd61b07..815cf9c8e4c 100644 ---- a/app/code/Magento/Backend/view/adminhtml/templates/menu.phtml -+++ b/app/code/Magento/Backend/view/adminhtml/templates/menu.phtml -@@ -3,12 +3,9 @@ - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -- --// @codingStandardsIgnoreFile -- - ?> - - <nav data-mage-init='{"globalNavigation": {}}' class="admin__menu"> -- <?= /* @escapeNotVerified */ $block->renderNavigation($block->getMenuModel(), 0, 12) ?> -+ <?= /* @noEscape */ $block->renderNavigation($block->getMenuModel(), 0, 12) ?> - </nav> - -diff --git a/app/code/Magento/Backend/view/adminhtml/templates/page/copyright.phtml b/app/code/Magento/Backend/view/adminhtml/templates/page/copyright.phtml -index 55bdbb460ba..e3a5c84ea45 100644 ---- a/app/code/Magento/Backend/view/adminhtml/templates/page/copyright.phtml -+++ b/app/code/Magento/Backend/view/adminhtml/templates/page/copyright.phtml -@@ -3,9 +3,6 @@ - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -- --// @codingStandardsIgnoreFile -- - ?> --<a class="link-copyright" href="http://magento.com" target="_blank" title="<?= /* @escapeNotVerified */ __('Magento') ?>"></a> --<?= /* @escapeNotVerified */ __('Copyright © %1 Magento Commerce Inc. All rights reserved.', date('Y')) ?> -+<a class="link-copyright" href="http://magento.com" target="_blank" title="<?= $block->escapeHtmlAttr(__('Magento')) ?>"></a> -+<?= $block->escapeHtml(__('Copyright © %1 Magento Commerce Inc. All rights reserved.', date('Y'))) ?> -diff --git a/app/code/Magento/Backend/view/adminhtml/templates/page/footer.phtml b/app/code/Magento/Backend/view/adminhtml/templates/page/footer.phtml -index 78e2e6db15e..3f21dcda9a5 100644 ---- a/app/code/Magento/Backend/view/adminhtml/templates/page/footer.phtml -+++ b/app/code/Magento/Backend/view/adminhtml/templates/page/footer.phtml -@@ -3,11 +3,8 @@ - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -- --// @codingStandardsIgnoreFile -- - ?> - <p class="magento-version"> -- <strong><?= /* @escapeNotVerified */ __('Magento') ?></strong> -- <?= /* @escapeNotVerified */ __('ver. %1', $block->getMagentoVersion()) ?> -+ <strong><?= $block->escapeHtml(__('Magento')) ?></strong> -+ <?= $block->escapeHtml(__('ver. %1', $block->getMagentoVersion())) ?> - </p> -diff --git a/app/code/Magento/Backend/view/adminhtml/templates/page/header.phtml b/app/code/Magento/Backend/view/adminhtml/templates/page/header.phtml -index f952001f5e2..89f14466400 100644 ---- a/app/code/Magento/Backend/view/adminhtml/templates/page/header.phtml -+++ b/app/code/Magento/Backend/view/adminhtml/templates/page/header.phtml -@@ -4,26 +4,23 @@ - * See COPYING.txt for license details. - */ - --// @codingStandardsIgnoreFile -- - /** @var $block \Magento\Backend\Block\Page\Header */ -+$part = $block->getShowPart(); - ?> --<?php switch ($block->getShowPart()): -- case 'logo': ?> -+<?php if ($part === 'logo') : ?> - <?php $edition = $block->hasEdition() ? 'data-edition="' . $block->escapeHtml($block->getEdition()) . '"' : ''; ?> - <?php $logoSrc = ($block->hasLogoImageSrc()) ? $block->escapeHtml($block->getLogoImageSrc()) : 'images/magento-logo.svg' ?> - <a -- href="<?= /* @escapeNotVerified */ $block->getHomeLink() ?>" -- <?= /* @escapeNotVerified */ $edition ?> -+ href="<?= $block->escapeUrl($block->getHomeLink()) ?>" -+ <?= /* @noEscape */ $edition ?> - class="logo"> -- <img class="logo-img" src="<?= /* @escapeNotVerified */ $block->getViewFileUrl($logoSrc) ?>" -+ <img class="logo-img" src="<?= /* @noEscape */ $block->getViewFileUrl($logoSrc) ?>" - alt="<?= $block->escapeHtml(__('Magento Admin Panel')) ?>" title="<?= $block->escapeHtml(__('Magento Admin Panel')) ?>"/> - </a> -- <?php break; ?> -- <?php case 'user': ?> -+<?php elseif ($part === 'user') : ?> - <div class="admin-user admin__action-dropdown-wrap"> - <a -- href="<?= /* @escapeNotVerified */ $block->getUrl('adminhtml/system_account/index') ?>" -+ href="<?= /* @noEscape */ $block->getUrl('adminhtml/system_account/index') ?>" - class="admin__action-dropdown" - title="<?= $block->escapeHtml(__('My Account')) ?>" - data-mage-init='{"dropdown":{}}' -@@ -33,36 +30,35 @@ - </span> - </a> - <ul class="admin__action-dropdown-menu"> -- <?php if ($block->getAuthorization()->isAllowed('Magento_Backend::myaccount')): ?> -+ <?php if ($block->getAuthorization()->isAllowed('Magento_Backend::myaccount')) : ?> - <li> - <a -- href="<?= /* @escapeNotVerified */ $block->getUrl('adminhtml/system_account/index') ?>" -- <?= /* @escapeNotVerified */ $block->getUiId('user', 'account', 'settings') ?> -+ href="<?= /* @noEscape */ $block->getUrl('adminhtml/system_account/index') ?>" -+ <?= /* @noEscape */ $block->getUiId('user', 'account', 'settings') ?> - title="<?= $block->escapeHtml(__('Account Setting')) ?>"> -- <?= /* @escapeNotVerified */ __('Account Setting') ?> (<span class="admin-user-name"><?= $block->escapeHtml($block->getUser()->getUserName()) ?></span>) -+ <?= $block->escapeHtml(__('Account Setting')) ?> (<span class="admin-user-name"><?= $block->escapeHtml($block->getUser()->getUserName()) ?></span>) - </a> - </li> - <?php endif; ?> - <li> - <a -- href="<?= /* @escapeNotVerified */ $block->getBaseUrl() ?>" -+ href="<?= /* @noEscape */ $block->getBaseUrl() ?>" - title="<?= $block->escapeHtml(__('Customer View')) ?>" - target="_blank" class="store-front"> -- <?= /* @escapeNotVerified */ __('Customer View') ?> -+ <?= $block->escapeHtml(__('Customer View')) ?> - </a> - </li> - <li> - <a -- href="<?= /* @escapeNotVerified */ $block->getLogoutLink() ?>" -+ href="<?= /* @noEscape */ $block->getLogoutLink() ?>" - class="account-signout" - title="<?= $block->escapeHtml(__('Sign Out')) ?>"> -- <?= /* @escapeNotVerified */ __('Sign Out') ?> -+ <?= $block->escapeHtml(__('Sign Out')) ?> - </a> - </li> - </ul> - </div> -- <?php break; ?> -- <?php case 'other': ?> -- <?= $block->getChildHtml() ?> -- <?php break; ?> --<?php endswitch; ?> -+ -+<?php elseif ($part === 'other') : ?> -+ <?= $block->getChildHtml() ?> -+<?php endif; ?> -diff --git a/app/code/Magento/Backend/view/adminhtml/templates/page/js/calendar.phtml b/app/code/Magento/Backend/view/adminhtml/templates/page/js/calendar.phtml -index e47bf5830f9..94df9ef9eb8 100644 ---- a/app/code/Magento/Backend/view/adminhtml/templates/page/js/calendar.phtml -+++ b/app/code/Magento/Backend/view/adminhtml/templates/page/js/calendar.phtml -@@ -3,7 +3,6 @@ - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ --// no notice of license for now - ?> - - <?php -@@ -22,20 +21,20 @@ require([ - - $.extend(true, $, { - calendarConfig: { -- dayNames: <?= /* @escapeNotVerified */ $days['wide'] ?>, -- dayNamesMin: <?= /* @escapeNotVerified */ $days['abbreviated'] ?>, -- monthNames: <?= /* @escapeNotVerified */ $months['wide'] ?>, -- monthNamesShort: <?= /* @escapeNotVerified */ $months['abbreviated'] ?>, -- infoTitle: "<?= /* @escapeNotVerified */ __('About the calendar') ?>", -- firstDay: <?= /* @escapeNotVerified */ $firstDay ?>, -- closeText: "<?= /* @escapeNotVerified */ __('Close') ?>", -- currentText: "<?= /* @escapeNotVerified */ __('Go Today') ?>", -- prevText: "<?= /* @escapeNotVerified */ __('Previous') ?>", -- nextText: "<?= /* @escapeNotVerified */ __('Next') ?>", -- weekHeader: "<?= /* @escapeNotVerified */ __('WK') ?>", -- timeText: "<?= /* @escapeNotVerified */ __('Time') ?>", -- hourText: "<?= /* @escapeNotVerified */ __('Hour') ?>", -- minuteText: "<?= /* @escapeNotVerified */ __('Minute') ?>", -+ dayNames: <?= /* @noEscape */ $days['wide'] ?>, -+ dayNamesMin: <?= /* @noEscape */ $days['abbreviated'] ?>, -+ monthNames: <?= /* @noEscape */ $months['wide'] ?>, -+ monthNamesShort: <?= /* @noEscape */ $months['abbreviated'] ?>, -+ infoTitle: "<?= $block->escapeJs(__('About the calendar')) ?>", -+ firstDay: <?= /* @noEscape */ $firstDay ?>, -+ closeText: "<?= $block->escapeJs(__('Close')) ?>", -+ currentText: "<?= $block->escapeJs(__('Go Today')) ?>", -+ prevText: "<?= $block->escapeJs(__('Previous')) ?>", -+ nextText: "<?= $block->escapeJs(__('Next')) ?>", -+ weekHeader: "<?= $block->escapeJs(__('WK')) ?>", -+ timeText: "<?= $block->escapeJs(__('Time')) ?>", -+ hourText: "<?= $block->escapeJs(__('Hour')) ?>", -+ minuteText: "<?= $block->escapeJs(__('Minute')) ?>", - dateFormat: $.datepicker.RFC_2822, - showOn: "button", - showAnim: "", -@@ -52,11 +51,11 @@ require([ - showMinute: false, - serverTimezoneSeconds: <?= (int) $block->getStoreTimestamp() ?>, - serverTimezoneOffset: <?= (int) $block->getTimezoneOffsetSeconds() ?>, -- yearRange: '<?= /* @escapeNotVerified */ $block->getYearRange() ?>' -+ yearRange: '<?= $block->escapeJs($block->getYearRange()) ?>' - } - }); - --enUS = <?= /* @escapeNotVerified */ $enUS ?>; // en_US locale reference -+enUS = <?= /* @noEscape */ $enUS ?>; // en_US locale reference - - }); - </script> -diff --git a/app/code/Magento/Backend/view/adminhtml/templates/page/js/components.phtml b/app/code/Magento/Backend/view/adminhtml/templates/page/js/components.phtml -index c6c7bcc901e..5277a1df2f3 100644 ---- a/app/code/Magento/Backend/view/adminhtml/templates/page/js/components.phtml -+++ b/app/code/Magento/Backend/view/adminhtml/templates/page/js/components.phtml -@@ -3,9 +3,6 @@ - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -- --// @codingStandardsIgnoreFile -- - ?> - - <?php -diff --git a/app/code/Magento/Backend/view/adminhtml/templates/page/js/require_js.phtml b/app/code/Magento/Backend/view/adminhtml/templates/page/js/require_js.phtml -index 9cb2cec0919..68453d9ff8f 100644 ---- a/app/code/Magento/Backend/view/adminhtml/templates/page/js/require_js.phtml -+++ b/app/code/Magento/Backend/view/adminhtml/templates/page/js/require_js.phtml -@@ -5,9 +5,9 @@ - */ - ?> - <script> -- var BASE_URL = '<?= /* @escapeNotVerified */ $block->getUrl('*') ?>'; -- var FORM_KEY = '<?= /* @escapeNotVerified */ $block->getFormKey() ?>'; -+ var BASE_URL = '<?= /* @noEscape */ $block->getUrl('*') ?>'; -+ var FORM_KEY = '<?= /* @noEscape */ $block->getFormKey() ?>'; - var require = { -- "baseUrl": "<?= /* @escapeNotVerified */ $block->getViewFileUrl('/') ?>" -+ "baseUrl": "<?= /* @noEscape */ $block->getViewFileUrl('/') ?>" - }; - </script> -diff --git a/app/code/Magento/Backend/view/adminhtml/templates/page/notices.phtml b/app/code/Magento/Backend/view/adminhtml/templates/page/notices.phtml -index 5418ad58b95..93df0aec94e 100644 ---- a/app/code/Magento/Backend/view/adminhtml/templates/page/notices.phtml -+++ b/app/code/Magento/Backend/view/adminhtml/templates/page/notices.phtml -@@ -3,29 +3,26 @@ - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -- --// @codingStandardsIgnoreFile -- - ?> - <?php - /** - * @see \Magento\Backend\Block\Page\Notices - */ - ?> --<?php if ($block->displayNoscriptNotice()): ?> -+<?php if ($block->displayNoscriptNotice()) : ?> - <noscript> - <div class="messages"> - <div class="message message-warning message-noscript"> -- <strong><?= /* @escapeNotVerified */ __('JavaScript may be disabled in your browser.') ?></strong> -- <?= /* @escapeNotVerified */ __('To use this website you must first enable JavaScript in your browser.') ?> -+ <strong><?= $block->escapeHtml(__('JavaScript may be disabled in your browser.')) ?></strong> -+ <?= $block->escapeHtml(__('To use this website you must first enable JavaScript in your browser.')) ?> - </div> - </div> - </noscript> - <?php endif; ?> --<?php if ($block->displayDemoNotice()): ?> -+<?php if ($block->displayDemoNotice()) : ?> - <div class="messages"> - <div class="message message-warning message-demo-mode"> -- <?= /* @escapeNotVerified */ __('This is only a demo store. You can browse and place orders, but nothing will be processed.') ?> -+ <?= $block->escapeHtml(__('This is only a demo store. You can browse and place orders, but nothing will be processed.')) ?> - </div> - </div> - <?php endif; ?> -diff --git a/app/code/Magento/Backend/view/adminhtml/templates/page/privacyPolicy.phtml b/app/code/Magento/Backend/view/adminhtml/templates/page/privacyPolicy.phtml -new file mode 100644 -index 00000000000..8c876762783 ---- /dev/null -+++ b/app/code/Magento/Backend/view/adminhtml/templates/page/privacyPolicy.phtml -@@ -0,0 +1,11 @@ -+<?php -+/** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+ -+?> -+ -+<a class="link-report" href="<?= $block->escapeUrl($block->getPrivacypolicyUrl()) ?>" id="footer_privacy" target="_blank"> -+ <?= $block->escapeHtml(__('Privacy Policy')) ?> -+</a> | -\ No newline at end of file -diff --git a/app/code/Magento/Backend/view/adminhtml/templates/page/report.phtml b/app/code/Magento/Backend/view/adminhtml/templates/page/report.phtml -index 4ef6d378cc4..2965983e121 100644 ---- a/app/code/Magento/Backend/view/adminhtml/templates/page/report.phtml -+++ b/app/code/Magento/Backend/view/adminhtml/templates/page/report.phtml -@@ -3,12 +3,9 @@ - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -- --// @codingStandardsIgnoreFile -- - ?> --<?php if ($block->getBugreportUrl()): ?> -- <a class="link-report" href="<?= /* @escapeNotVerified */ $block->getBugreportUrl() ?>" id="footer_bug_tracking" target="_blank"> -- <?= /* @escapeNotVerified */ __('Report an Issue') ?> -+<?php if ($block->getBugreportUrl()) : ?> -+ <a class="link-report" href="<?= $block->escapeUrl($block->getBugreportUrl()) ?>" id="footer_bug_tracking" target="_blank"> -+ <?= $block->escapeHtml(__('Report an Issue')) ?> - </a> - <?php endif; ?> -diff --git a/app/code/Magento/Backend/view/adminhtml/templates/pageactions.phtml b/app/code/Magento/Backend/view/adminhtml/templates/pageactions.phtml -index 69d545f12d0..56a8161b57e 100644 ---- a/app/code/Magento/Backend/view/adminhtml/templates/pageactions.phtml -+++ b/app/code/Magento/Backend/view/adminhtml/templates/pageactions.phtml -@@ -3,12 +3,9 @@ - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -- --// @codingStandardsIgnoreFile -- - ?> --<?php if ($block->getChildHtml()):?> -- <div data-mage-init='{"floatingHeader": {}}' class="page-actions" <?= /* @escapeNotVerified */ $block->getUiId('content-header') ?>> -+<?php if ($block->getChildHtml()) :?> -+ <div data-mage-init='{"floatingHeader": {}}' class="page-actions floating-header" <?= /* @noEscape */ $block->getUiId('content-header') ?>> - <?= $block->getChildHtml() ?> - </div> - <?php endif; ?> -diff --git a/app/code/Magento/Backend/view/adminhtml/templates/store/switcher.phtml b/app/code/Magento/Backend/view/adminhtml/templates/store/switcher.phtml -index bb968c57610..da18bc18375 100644 ---- a/app/code/Magento/Backend/view/adminhtml/templates/store/switcher.phtml -+++ b/app/code/Magento/Backend/view/adminhtml/templates/store/switcher.phtml -@@ -4,27 +4,25 @@ - * See COPYING.txt for license details. - */ - --// @codingStandardsIgnoreFile -- - /* @var $block \Magento\Backend\Block\Store\Switcher */ - ?> --<?php if ($websites = $block->getWebsites()): ?> -+<?php if ($websites = $block->getWebsites()) : ?> - - <div class="store-switcher store-view"> -- <span class="store-switcher-label"><?= /* @escapeNotVerified */ __('Store View:') ?></span> -+ <span class="store-switcher-label"><?= $block->escapeHtml(__('Store View:')) ?></span> - <div class="actions dropdown closable"> - <input type="hidden" name="store_switcher" id="store_switcher" -- data-role="store-view-id" data-param="<?= /* @escapeNotVerified */ $block->getStoreVarName() ?>" -+ data-role="store-view-id" data-param="<?= $block->escapeHtmlAttr($block->getStoreVarName()) ?>" - value="<?= $block->escapeHtml($block->getStoreId()) ?>" -- onchange="switchScope(this);"<?= /* @escapeNotVerified */ $block->getUiId() ?> /> -+ onchange="switchScope(this);"<?= /* @noEscape */ $block->getUiId() ?> /> - <input type="hidden" name="store_group_switcher" id="store_group_switcher" -- data-role="store-group-id" data-param="<?= /* @escapeNotVerified */ $block->getStoreGroupVarName() ?>" -+ data-role="store-group-id" data-param="<?= $block->escapeHtmlAttr($block->getStoreGroupVarName()) ?>" - value="<?= $block->escapeHtml($block->getStoreGroupId()) ?>" -- onchange="switchScope(this);"<?= /* @escapeNotVerified */ $block->getUiId() ?> /> -+ onchange="switchScope(this);"<?= /* @noEscape */ $block->getUiId() ?> /> - <input type="hidden" name="website_switcher" id="website_switcher" -- data-role="website-id" data-param="<?= /* @escapeNotVerified */ $block->getWebsiteVarName() ?>" -+ data-role="website-id" data-param="<?= $block->escapeHtmlAttr($block->getWebsiteVarName()) ?>" - value="<?= $block->escapeHtml($block->getWebsiteId()) ?>" -- onchange="switchScope(this);"<?= /* @escapeNotVerified */ $block->getUiId() ?> /> -+ onchange="switchScope(this);"<?= /* @noEscape */ $block->getUiId() ?> /> - <button - type="button" - class="admin__action-dropdown" -@@ -32,96 +30,64 @@ - data-toggle="dropdown" - aria-haspopup="true" - id="store-change-button"> -- <?= /* @escapeNotVerified */ $block->getCurrentSelectionName() ?> -+ <?= $block->escapeHtml($block->getCurrentSelectionName()) ?> - </button> - <ul class="dropdown-menu" data-role="stores-list"> -- <?php if ($block->hasDefaultOption()): ?> -- <li class="store-switcher-all <?php if ( ! ($block->getDefaultSelectionName() != $block->getCurrentSelectionName())) { -- echo "disabled"; -- } ?> <?php if ( ! $block->hasScopeSelected()) { -- ?> current<?php -- } ?>"> -- <?php if ($block->getDefaultSelectionName() != $block->getCurrentSelectionName()) { -- ?> -+ <?php if ($block->hasDefaultOption()) : ?> -+ <li class="store-switcher-all <?php if (!($block->getDefaultSelectionName() != $block->getCurrentSelectionName())) : ?>disabled<?php endif; ?> <?php if (!$block->hasScopeSelected()) : ?>current<?php endif; ?>"> -+ <?php if ($block->getDefaultSelectionName() != $block->getCurrentSelectionName()) : ?> - <a data-role="store-view-id" data-value="" href="#"> -- <?= /* @escapeNotVerified */ $block->getDefaultSelectionName() ?> -+ <?= $block->escapeHtml($block->getDefaultSelectionName()) ?> - </a> -- <?php -- } else { -- ?> -- <span><?= /* @escapeNotVerified */ $block->getDefaultSelectionName() ?></span> -- <?php -- } ?> -+ <?php else : ?> -+ <span><?= $block->escapeHtml($block->getDefaultSelectionName()) ?></span> -+ <?php endif; ?> - </li> - <?php endif; ?> -- <?php foreach ($websites as $website): ?> -+ <?php foreach ($websites as $website) : ?> - <?php $showWebsite = false; ?> -- <?php foreach ($website->getGroups() as $group): ?> -+ <?php foreach ($website->getGroups() as $group) : ?> - <?php $showGroup = false; ?> -- <?php foreach ($block->getStores($group) as $store): ?> -- <?php if ($showWebsite == false): ?> -+ <?php foreach ($block->getStores($group) as $store) : ?> -+ <?php if ($showWebsite == false) : ?> - <?php $showWebsite = true; ?> -- <li class="store-switcher-website <?php if ( ! ($block->isWebsiteSwitchEnabled() && ! $block->isWebsiteSelected($website))) { -- echo "disabled"; -- } ?> <?php if ($block->isWebsiteSelected($website)) { -- ?> current<?php -- } ?>"> -- <?php if ($block->isWebsiteSwitchEnabled() && ! $block->isWebsiteSelected($website)) { -- ?> -+ <li class="store-switcher-website <?php if (!($block->isWebsiteSwitchEnabled() && ! $block->isWebsiteSelected($website))) : ?>disabled<?php endif; ?> <?php if ($block->isWebsiteSelected($website)) : ?>current<?php endif; ?>"> -+ <?php if ($block->isWebsiteSwitchEnabled() && ! $block->isWebsiteSelected($website)) : ?> - <a data-role="website-id" data-value="<?= $block->escapeHtml($website->getId()) ?>" href="#"> - <?= $block->escapeHtml($website->getName()) ?> - </a> -- <?php -- } else { -- ?> -+ <?php else : ?> - <span><?= $block->escapeHtml($website->getName()) ?></span> -- <?php -- } ?> -+ <?php endif; ?> - </li> - <?php endif; ?> -- <?php if ($showGroup == false): ?> -+ <?php if ($showGroup == false) : ?> - <?php $showGroup = true; ?> -- <li class="store-switcher-store <?php if ( ! ($block->isStoreGroupSwitchEnabled() && ! $block->isStoreGroupSelected($group))) { -- echo "disabled"; -- } ?> <?php if ($block->isStoreGroupSelected($group)) { -- ?> current<?php -- } ?>"> -- <?php if ($block->isStoreGroupSwitchEnabled() && ! $block->isStoreGroupSelected($group)) { -- ?> -+ <li class="store-switcher-store <?php if (!($block->isStoreGroupSwitchEnabled() && ! $block->isStoreGroupSelected($group))) : ?>disabled<?php endif; ?> <?php if ($block->isStoreGroupSelected($group)) : ?>current<?php endif; ?>"> -+ <?php if ($block->isStoreGroupSwitchEnabled() && ! $block->isStoreGroupSelected($group)) : ?> - <a data-role="store-group-id" data-value="<?= $block->escapeHtml($group->getId()) ?>" href="#"> - <?= $block->escapeHtml($group->getName()) ?> - </a> -- <?php -- } else { -- ?> -+ <?php else : ?> - <span><?= $block->escapeHtml($group->getName()) ?></span> -- <?php -- } ?> -+ <?php endif; ?> - </li> - <?php endif; ?> -- <li class="store-switcher-store-view <?php if ( ! ($block->isStoreSwitchEnabled() && ! $block->isStoreSelected($store))) { -- echo "disabled"; -- } ?> <?php if ($block->isStoreSelected($store)) { -- ?> current<?php -- } ?>"> -- <?php if ($block->isStoreSwitchEnabled() && ! $block->isStoreSelected($store)) { -- ?> -+ <li class="store-switcher-store-view <?php if (!($block->isStoreSwitchEnabled() && !$block->isStoreSelected($store))) : ?>disabled<?php endif; ?> <?php if ($block->isStoreSelected($store)) :?>current<?php endif; ?>"> -+ <?php if ($block->isStoreSwitchEnabled() && ! $block->isStoreSelected($store)) : ?> - <a data-role="store-view-id" data-value="<?= $block->escapeHtml($store->getId()) ?>" href="#"> - <?= $block->escapeHtml($store->getName()) ?> - </a> -- <?php -- } else { -- ?> -+ <?php else : ?> - <span><?= $block->escapeHtml($store->getName()) ?></span> -- <?php -- } ?> -+ <?php endif; ?> - </li> - <?php endforeach; ?> - <?php endforeach; ?> - <?php endforeach; ?> -- <?php if ($block->getShowManageStoresLink() && $block->getAuthorization()->isAllowed('Magento_Backend::store')): ?> -+ <?php if ($block->getShowManageStoresLink() && $block->getAuthorization()->isAllowed('Magento_Backend::store')) : ?> - <li class="dropdown-toolbar"> -- <a href="<?= /* @escapeNotVerified */ $block->getUrl('*/system_store') ?>"><?= /* @escapeNotVerified */ __('Stores Configuration') ?></a> -+ <a href="<?= /* @noEscape */ $block->getUrl('*/system_store') ?>"><?= $block->escapeHtml(__('Stores Configuration')) ?></a> - </li> - <?php endif; ?> - </ul> -@@ -173,10 +139,10 @@ require([ - scopeSwitcherHandler(switcherParams); - } else { - -- <?php if ($block->getUseConfirm()): ?> -+ <?php if ($block->getUseConfirm()) : ?> - - confirm({ -- content: "<?= /* @escapeNotVerified */ __('Please confirm scope switching. All data that hasn\'t been saved will be lost.') ?>", -+ content: "<?= $block->escapeJs(__('Please confirm scope switching. All data that hasn\'t been saved will be lost.')) ?>", - actions: { - confirm: function() { - reload(); -@@ -187,16 +153,16 @@ require([ - } - }); - -- <?php else: ?> -+ <?php else : ?> - reload(); - <?php endif; ?> - } - - function reload() { -- <?php if (!$block->isUsingIframe()): ?> -- var url = '<?= /* @escapeNotVerified */ $block->getSwitchUrl() ?>' + scopeParams; -+ <?php if (!$block->isUsingIframe()) : ?> -+ var url = '<?= $block->escapeJs($block->getSwitchUrl()) ?>' + scopeParams; - setLocation(url); -- <?php else: ?> -+ <?php else : ?> - jQuery('#preview_selected_store').val(scopeId); - jQuery('#preview_form').submit(); - -diff --git a/app/code/Magento/Backend/view/adminhtml/templates/store/switcher/form/renderer/fieldset.phtml b/app/code/Magento/Backend/view/adminhtml/templates/store/switcher/form/renderer/fieldset.phtml -index 49a2c681285..382fb6e81c5 100644 ---- a/app/code/Magento/Backend/view/adminhtml/templates/store/switcher/form/renderer/fieldset.phtml -+++ b/app/code/Magento/Backend/view/adminhtml/templates/store/switcher/form/renderer/fieldset.phtml -@@ -3,43 +3,40 @@ - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -- --// @codingStandardsIgnoreFile -- - ?> - <?php $_element = $block->getElement() ?> --<?php if ($_element->getFieldsetContainerId()): ?> -- <div id="<?= /* @escapeNotVerified */ $_element->getFieldsetContainerId() ?>">789 -+<?php if ($_element->getFieldsetContainerId()) : ?> -+ <div id="<?= $block->escapeHtmlAttr($_element->getFieldsetContainerId()) ?>">789 - <?php endif; ?> - --<?php if (!$_element->getNoContainer()): ?> -- <fieldset class="admin__fieldset fieldset <?= /* @escapeNotVerified */ $_element->getClass() ?>" id="<?= $_element->getHtmlId() ?>"> -+<?php if (!$_element->getNoContainer()) : ?> -+ <fieldset class="admin__fieldset fieldset <?= $block->escapeHtmlAttr($_element->getClass()) ?>" id="<?= $_element->getHtmlId() ?>"> - <?php endif; ?> - -- <?php if ($_element->getLegend()): ?> -+ <?php if ($_element->getLegend()) : ?> - <legend class="admin__legend legend"> -- <span><?= /* @escapeNotVerified */ $_element->getLegend() ?></span> -+ <span><?= $block->escapeHtml($_element->getLegend()) ?></span> - <?= $block->getHintHtml() ?> - </legend><br/> -- <?= /* @escapeNotVerified */ $_element->getHeaderBar() ?> -- <?php else: ?> -+ <?= /* @noEscape */ $_element->getHeaderBar() ?> -+ <?php else : ?> - <?= $block->getHintHtml() ?> - <?php endif; ?> - <div class="admin__fieldset tree-store-scope"> -- <?php if ($_element->getComment()): ?> -+ <?php if ($_element->getComment()) : ?> - <p class="comment"><?= $block->escapeHtml($_element->getComment()) ?></p> - <?php endif; ?> -- <?php if ($_element->hasHtmlContent()): ?> -- <?= $_element->getHtmlContent() ?> -- <?php else: ?> -+ <?php if ($_element->hasHtmlContent()) : ?> -+ <?= $_element->getHtmlContent() ?> -+ <?php else : ?> - <?= $_element->getChildrenHtml() ?> - <?php endif; ?> - </div> - <?= $_element->getSubFieldsetHtml() ?> - --<?php if (!$_element->getNoContainer()): ?> -+<?php if (!$_element->getNoContainer()) : ?> - </fieldset> - <?php endif; ?> --<?php if ($_element->getFieldsetContainerId()): ?> -+<?php if ($_element->getFieldsetContainerId()) : ?> - </div> - <?php endif; ?> -diff --git a/app/code/Magento/Backend/view/adminhtml/templates/store/switcher/form/renderer/fieldset/element.phtml b/app/code/Magento/Backend/view/adminhtml/templates/store/switcher/form/renderer/fieldset/element.phtml -index e25c9d22ca4..959a27279e5 100644 ---- a/app/code/Magento/Backend/view/adminhtml/templates/store/switcher/form/renderer/fieldset/element.phtml -+++ b/app/code/Magento/Backend/view/adminhtml/templates/store/switcher/form/renderer/fieldset/element.phtml -@@ -3,9 +3,6 @@ - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -- --// @codingStandardsIgnoreFile -- - ?> - <?php - /* @var $block \Magento\Backend\Block\Widget\Form\Renderer\Fieldset\Element */ -@@ -24,21 +21,21 @@ $fieldAttributes = $fieldId . ' class="' . $fieldClass . '" ' - . $block->getUiId('form-field', $element->getId()); - ?> - --<?php if (!$element->getNoDisplay()): ?> -- <?php if ($element->getType() == 'hidden'): ?> -+<?php if (!$element->getNoDisplay()) : ?> -+ <?php if ($element->getType() == 'hidden') : ?> - <?= $element->getElementHtml() ?> -- <?php else: ?> -- <div<?= /* @escapeNotVerified */ $fieldAttributes ?>> -- <?php if ($elementBeforeLabel): ?> -+ <?php else : ?> -+ <div<?= /* @noEscape */ $fieldAttributes ?>> -+ <?php if ($elementBeforeLabel) : ?> - <?= $element->getElementHtml() ?> - <?= $element->getLabelHtml('', $element->getScopeLabel()) ?> -- <?= /* @escapeNotVerified */ $note ?> -- <?php else: ?> -+ <?= /* @noEscape */ $note ?> -+ <?php else : ?> - <?= $element->getLabelHtml('', $element->getScopeLabel()) ?> - <div class="admin__field-control control"> -- <?= /* @escapeNotVerified */ ($addOn) ? '<div class="addon">' . $element->getElementHtml() . '</div>' : $element->getElementHtml() ?> -+ <?= /* @noEscape */ ($addOn) ? '<div class="addon">' . $element->getElementHtml() . '</div>' : $element->getElementHtml() ?> - <?= $block->getHintHtml() ?> -- <?= /* @escapeNotVerified */ $note ?> -+ <?= /* @noEscape */ $note ?> - </div> - <?php endif; ?> - </div> -diff --git a/app/code/Magento/Backend/view/adminhtml/templates/system/autocomplete.phtml b/app/code/Magento/Backend/view/adminhtml/templates/system/autocomplete.phtml -index 22d93241f43..7ac867970e8 100644 ---- a/app/code/Magento/Backend/view/adminhtml/templates/system/autocomplete.phtml -+++ b/app/code/Magento/Backend/view/adminhtml/templates/system/autocomplete.phtml -@@ -3,15 +3,12 @@ - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -- --// @codingStandardsIgnoreFile -- - ?> - <ul class="dropdown-menu"> -- <?php foreach ($items as $item): ?> -- <li id="<?= /* @escapeNotVerified */ $item['id'] ?>" class="item"> -- <a href="<?= /* @escapeNotVerified */ $item['url'] ?>" class="title"><?= $block->escapeHtml($item['name']) ?></a> -- <div class="type"><?= /* @escapeNotVerified */ $item['type'] ?></div> -+ <?php foreach ($items as $item) : ?> -+ <li id="<?= $block->escapeHtmlAttr($item['id']) ?>" class="item"> -+ <a href="<?= $block->escapeUrl($item['url']) ?>" class="title"><?= $block->escapeHtml($item['name']) ?></a> -+ <div class="type"><?= $block->escapeHtml($item['type']) ?></div> - <?= $block->escapeHtml($item['description']) ?> - </li> - <?php endforeach ?> -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 8e30afdf51f..c392ebf3883 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 -@@ -4,17 +4,15 @@ - * See COPYING.txt for license details. - */ - --// @codingStandardsIgnoreFile -- - /** @var \Magento\Backend\Block\Cache\Permissions|null $permissions */ - $permissions = $block->getData('permissions'); - ?> --<?php if ($permissions && $permissions->hasAccessToAdditionalActions()): ?> -+<?php if ($permissions && $permissions->hasAccessToAdditionalActions()) : ?> - <div class="additional-cache-management"> -- <?php if ($permissions->hasAccessToFlushCatalogImages()): ?> -- <h2> -- <span><?= $block->escapeHtml(__('Additional Cache Management')); ?></span> -- </h2> -+ <h2> -+ <span><?= $block->escapeHtml(__('Additional Cache Management')); ?></span> -+ </h2> -+ <?php if ($permissions->hasAccessToFlushCatalogImages()) : ?> - <p> - <button onclick="setLocation('<?= $block->escapeJs($block->getCleanImagesUrl()); ?>')" type="button"> - <?= $block->escapeHtml(__('Flush Catalog Images Cache')); ?> -@@ -22,7 +20,7 @@ $permissions = $block->getData('permissions'); - <span><?= $block->escapeHtml(__('Pregenerated product images files')); ?></span> - </p> - <?php endif; ?> -- <?php if ($permissions->hasAccessToFlushJsCss()): ?> -+ <?php if ($permissions->hasAccessToFlushJsCss()) : ?> - <p> - <button onclick="setLocation('<?= $block->escapeJs($block->getCleanMediaUrl()); ?>')" type="button"> - <?= $block->escapeHtml(__('Flush JavaScript/CSS Cache')); ?> -@@ -30,7 +28,7 @@ $permissions = $block->getData('permissions'); - <span><?= $block->escapeHtml(__('Themes JavaScript and CSS files combined to one file')) ?></span> - </p> - <?php endif; ?> -- <?php if (!$block->isInProductionMode() && $permissions->hasAccessToFlushStaticFiles()): ?> -+ <?php if (!$block->isInProductionMode() && $permissions->hasAccessToFlushStaticFiles()) : ?> - <p> - <button onclick="setLocation('<?= $block->escapeJs($block->getCleanStaticFilesUrl()); ?>')" type="button"> - <?= $block->escapeHtml(__('Flush Static Files Cache')); ?> -diff --git a/app/code/Magento/Backend/view/adminhtml/templates/system/cache/edit.phtml b/app/code/Magento/Backend/view/adminhtml/templates/system/cache/edit.phtml -index 8e52f245f74..d1c51f0755a 100644 ---- a/app/code/Magento/Backend/view/adminhtml/templates/system/cache/edit.phtml -+++ b/app/code/Magento/Backend/view/adminhtml/templates/system/cache/edit.phtml -@@ -3,9 +3,6 @@ - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -- --// @codingStandardsIgnoreFile -- - ?> - <?php - /** -@@ -17,7 +14,7 @@ - */ - ?> - <div data-mage-init='{"floatingHeader": {}}' class="page-actions"><?= $block->getSaveButtonHtml() ?></div> --<form action="<?= /* @escapeNotVerified */ $block->getSaveUrl() ?>" method="post" id="config-edit-form" enctype="multipart/form-data"> -+<form action="<?= $block->escapeUrl($block->getSaveUrl()) ?>" method="post" id="config-edit-form" enctype="multipart/form-data"> - <?= $block->getBlockHtml('formkey') ?> - - <script> -@@ -34,26 +31,30 @@ - <?= $block->getChildHtml('form') ?> - <div class="entry-edit"> - <div class="entry-edit-head"> -- <h4 class="icon-head head-edit-form fieldset-legend"><?= /* @escapeNotVerified */ __('Catalog') ?></h4> -+ <h4 class="icon-head head-edit-form fieldset-legend"><?= $block->escapeHtml(__('Catalog')) ?></h4> - </div> - <fieldset id="catalog"> - <table class="form-list"> - <tbody> -- <?php foreach ($block->getCatalogData() as $_item): ?> -- <?php /* disable reindex buttons. functionality moved to index management*/?> -- <?php if ($_item['buttons'][0]['name'] != 'clear_images_cache') { -- continue; --}?> -+ <?php foreach ($block->getCatalogData() as $_item) : ?> -+ <?php /* disable reindex buttons. functionality moved to index management*/?> -+ <?php -+ if ($_item['buttons'][0]['name'] != 'clear_images_cache') { -+ continue; -+ } -+ ?> - <tr> -- <td class="label"><label><?= /* @escapeNotVerified */ $_item['label'] ?></label></td> -+ <td class="label"><label><?= $block->escapeHtml($_item['label']) ?></label></td> - <td class="value"> -- <?php foreach ($_item['buttons'] as $_button): ?> -+ <?php foreach ($_item['buttons'] as $_button) : ?> - <?php $clickAction = "setCacheAction('catalog_action',this)"; ?> -- <?php if (isset($_button['warning']) && $_button['warning']): ?> -+ <?php if (isset($_button['warning']) && $_button['warning']) : ?> -+ <?php //phpcs:disable ?> - <?php $clickAction = "if (confirm('" . addslashes($_button['warning']) . "')) {{$clickAction}}"; ?> -+ <?php //phpcs:enable ?> - <?php endif; ?> -- <button <?php if (!isset($_button['disabled']) || !$_button['disabled']):?>onclick="<?= /* @escapeNotVerified */ $clickAction ?>"<?php endif; ?> id="<?= /* @escapeNotVerified */ $_button['name'] ?>" type="button" class="scalable <?php if (isset($_button['disabled']) && $_button['disabled']):?>disabled<?php endif; ?>" style=""><span><span><span><?= /* @escapeNotVerified */ $_button['action'] ?></span></span></span></button> -- <?php if (isset($_button['comment'])): ?> <br /> <small><?= /* @escapeNotVerified */ $_button['comment'] ?></small> <?php endif; ?> -+ <button <?php if (!isset($_button['disabled']) || !$_button['disabled']) :?>onclick="<?= /* @noEscape */ $clickAction ?>"<?php endif; ?> id="<?= $block->escapeHtmlAttr($_button['name']) ?>" type="button" class="scalable <?php if (isset($_button['disabled']) && $_button['disabled']) :?>disabled<?php endif; ?>" style=""><span><span><span><?= $block->escapeHtml($_button['action']) ?></span></span></span></button> -+ <?php if (isset($_button['comment'])) : ?> <br /> <small><?= $block->escapeHtml($_button['comment']) ?></small> <?php endif; ?> - <?php endforeach; ?> - </td> - <td><small> </small></td> -@@ -66,16 +67,16 @@ - - <div class="entry-edit"> - <div class="entry-edit-head"> -- <h4 class="icon-head head-edit-form fieldset-legend"><?= /* @escapeNotVerified */ __('JavaScript/CSS') ?></h4> -+ <h4 class="icon-head head-edit-form fieldset-legend"><?= $block->escapeHtml(__('JavaScript/CSS')) ?></h4> - </div> - - <fieldset id="jscss"> - <table class="form-list"> - <tbody> - <tr> -- <td class="label"><label><?= /* @escapeNotVerified */ __('JavaScript/CSS Cache') ?></label></td> -+ <td class="label"><label><?= $block->escapeHtml(__('JavaScript/CSS Cache')) ?></label></td> - <td class="value"> -- <button onclick="setCacheAction('jscss_action', this)" id='jscss_action' type="button" class="scalable"><span><span><span><?= /* @escapeNotVerified */ __('Clear') ?></span></span></span></button> -+ <button onclick="setCacheAction('jscss_action', this)" id='jscss_action' type="button" class="scalable"><span><span><span><?= $block->escapeHtml(__('Clear')) ?></span></span></span></button> - </td> - </tr> - </tbody> -diff --git a/app/code/Magento/Backend/view/adminhtml/templates/system/design/edit.phtml b/app/code/Magento/Backend/view/adminhtml/templates/system/design/edit.phtml -index 6b2f932cac7..c9cd765de35 100644 ---- a/app/code/Magento/Backend/view/adminhtml/templates/system/design/edit.phtml -+++ b/app/code/Magento/Backend/view/adminhtml/templates/system/design/edit.phtml -@@ -4,7 +4,7 @@ - * See COPYING.txt for license details. - */ - ?> --<form action="<?= /* @escapeNotVerified */ $block->getSaveUrl() ?>" method="post" id="design-edit-form"> -+<form action="<?= $block->escapeUrl($block->getSaveUrl()) ?>" method="post" id="design-edit-form"> - <?= $block->getBlockHtml('formkey') ?> - </form> - <script> -diff --git a/app/code/Magento/Backend/view/adminhtml/templates/system/design/index.phtml b/app/code/Magento/Backend/view/adminhtml/templates/system/design/index.phtml -index 902c6932f0a..c0928f4723b 100644 ---- a/app/code/Magento/Backend/view/adminhtml/templates/system/design/index.phtml -+++ b/app/code/Magento/Backend/view/adminhtml/templates/system/design/index.phtml -@@ -3,8 +3,5 @@ - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -- --// @codingStandardsIgnoreFile -- - ?> - <?= $block->getChildHtml('grid') ?> -diff --git a/app/code/Magento/Backend/view/adminhtml/templates/system/search.phtml b/app/code/Magento/Backend/view/adminhtml/templates/system/search.phtml -index b50183ced29..6e94770c6e4 100644 ---- a/app/code/Magento/Backend/view/adminhtml/templates/system/search.phtml -+++ b/app/code/Magento/Backend/view/adminhtml/templates/system/search.phtml -@@ -4,8 +4,6 @@ - * See COPYING.txt for license details. - */ - --// @codingStandardsIgnoreFile -- - /** @var $block \Magento\Backend\Block\GlobalSearch */ - ?> - <div class="search-global" data-mage-init='{"globalSearch": {}}'> -@@ -17,32 +15,34 @@ - class="search-global-input" - id="search-global" - name="query" -- data-mage-init='<?= /* @noEscape */ $this->helper('Magento\Framework\Json\Helper\Data')->jsonEncode($block->getWidgetInitOptions()) ?>'> -+ <?php //phpcs:disable ?> -+ data-mage-init='<?= /* @noEscape */ $this->helper(\Magento\Framework\Json\Helper\Data::class)->jsonEncode($block->getWidgetInitOptions()) ?>'> -+ <?php //phpcs:enable ?> - <button - type="submit" - class="search-global-action" -- title="<?= /* @escapeNotVerified */ __('Search') ?>" -+ title="<?= $block->escapeHtmlAttr(__('Search')) ?>" - ></button> - </div> - </form> - <script data-template="search-suggest" type="text/x-magento-template"> - <ul class="search-global-menu"> - <li class="item"> -- <a id="searchPreviewProducts" href="<?= /* @escapeNotVerified */ $block->getUrl('catalog/product/index/') ?>?search=<%- data.term%>" class="title">"<%- data.term%>" in Products</a> -+ <a id="searchPreviewProducts" href="<?= $block->escapeUrl($block->getUrl('catalog/product/index/')) ?>?search=<%- data.term%>" class="title">"<%- data.term%>" in Products</a> - </li> - <li class="item"> -- <a id="searchPreviewOrders" href="<?= /* @escapeNotVerified */ $block->getUrl('sales/order/index/') ?>?search=<%- data.term%>" class="title">"<%- data.term%>" in Orders</a> -+ <a id="searchPreviewOrders" href="<?= $block->escapeUrl($block->getUrl('sales/order/index/')) ?>?search=<%- data.term%>" class="title">"<%- data.term%>" in Orders</a> - </li> - <li class="item"> -- <a id="searchPreviewCustomers" href="<?= /* @escapeNotVerified */ $block->getUrl('customer/index/index/') ?>?search=<%- data.term%>" class="title">"<%- data.term%>" in Customers</a> -+ <a id="searchPreviewCustomers" href="<?= $block->escapeUrl($block->getUrl('customer/index/index/')) ?>?search=<%- data.term%>" class="title">"<%- data.term%>" in Customers</a> - </li> - <li class="item"> -- <a id="searchPreviewPages" href="<?= /* @escapeNotVerified */ $block->getUrl('cms/page/index/') ?>?search=<%- data.term%>" class="title">"<%- data.term%>" in Pages</a> -+ <a id="searchPreviewPages" href="<?= $block->escapeUrl($block->getUrl('cms/page/index/')) ?>?search=<%- data.term%>" class="title">"<%- data.term%>" in Pages</a> - </li> - <% if (data.items.length) { %> - <% _.each(data.items, function(value){ %> - <li class="item" -- <%- data.optionData(value) %> -+ <%= data.optionData(value) %> - > - <a href="<%- value.url %>" class="title"><%- value.name %></a> - <span class="type"><%- value.type %></span> -@@ -52,7 +52,7 @@ - <% } else { %> - <li> - <span class="mage-suggest-no-records"> -- <?= /* @escapeNotVerified */ __('No records found.') ?> -+ <?= $block->escapeHtml(__('No records found.')) ?> - </span> - </li> - <% } %> -diff --git a/app/code/Magento/Backend/view/adminhtml/templates/widget/accordion.phtml b/app/code/Magento/Backend/view/adminhtml/templates/widget/accordion.phtml -index 60cd51b496a..fecf5365544 100644 ---- a/app/code/Magento/Backend/view/adminhtml/templates/widget/accordion.phtml -+++ b/app/code/Magento/Backend/view/adminhtml/templates/widget/accordion.phtml -@@ -3,9 +3,6 @@ - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -- --// @codingStandardsIgnoreFile -- - ?> - <?php - /** -@@ -13,9 +10,9 @@ - */ - $items = $block->getItems(); - ?> --<?php if (!empty($items)): ?> -+<?php if (!empty($items)) : ?> - <dl id="tab_content_<?= $block->getHtmlId() ?>" name="tab_content_<?= $block->getHtmlId() ?>" class="accordion"> -- <?php foreach ($items as $_item): ?> -+ <?php foreach ($items as $_item) : ?> - <?= $block->getChildHtml($_item->getId()) ?> - <?php endforeach ?> - </dl> -@@ -23,7 +20,7 @@ $items = $block->getItems(); - require([ - 'mage/adminhtml/accordion' - ], function(){ -- tab_content_<?= $block->getHtmlId() ?>AccordionJs = new varienAccordion('tab_content_<?= $block->getHtmlId() ?>', '<?= /* @escapeNotVerified */ $block->getShowOnlyOne() ?>'); -- }); -+ tab_content_<?= $block->getHtmlId() ?>AccordionJs = new varienAccordion('tab_content_<?= $block->getHtmlId() ?>', '<?= $block->escapeJs($block->getShowOnlyOne()) ?>'); -+ }); - </script> - <?php endif; ?> -diff --git a/app/code/Magento/Backend/view/adminhtml/templates/widget/breadcrumbs.phtml b/app/code/Magento/Backend/view/adminhtml/templates/widget/breadcrumbs.phtml -index 74cd4eb7f2c..fb7cc63ebc1 100644 ---- a/app/code/Magento/Backend/view/adminhtml/templates/widget/breadcrumbs.phtml -+++ b/app/code/Magento/Backend/view/adminhtml/templates/widget/breadcrumbs.phtml -@@ -3,25 +3,22 @@ - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -- --// @codingStandardsIgnoreFile -- - ?> --<?php if (!empty($links)): ?> -+<?php if (!empty($links)) : ?> - <ul class="breadcrumbs"> - <?php $_size = count($links); ?> -- <?php foreach ($links as $_index => $_link): ?> -+ <?php foreach ($links as $_index => $_link) : ?> - <li> -- <?php if (empty($_link['url'])): ?> -- <?php if ($_index != $_size-1): ?> -+ <?php if (empty($_link['url'])) : ?> -+ <?php if ($_index != $_size-1) : ?> - <span><?= $block->escapeHtml($_link['label']) ?></span> -- <?php else: ?> -+ <?php else : ?> - <strong><?= $block->escapeHtml($_link['label']) ?></strong> - <?php endif; ?> -- <?php else: ?> -- <a href="<?= /* @escapeNotVerified */ $_link['url'] ?>" title="<?= $block->escapeHtml($_link['title']) ?>"><?= $block->escapeHtml($_link['label']) ?></a> -+ <?php else : ?> -+ <a href="<?= $block->escapeUrl($_link['url']) ?>" title="<?= $block->escapeHtml($_link['title']) ?>"><?= $block->escapeHtml($_link['label']) ?></a> - <?php endif; ?> -- <?php if ($_index != $_size-1): ?> -+ <?php if ($_index != $_size-1) : ?> - » - <?php endif; ?> - </li> -diff --git a/app/code/Magento/Backend/view/adminhtml/templates/widget/button.phtml b/app/code/Magento/Backend/view/adminhtml/templates/widget/button.phtml -index d3376745afe..b743b9bee10 100644 ---- a/app/code/Magento/Backend/view/adminhtml/templates/widget/button.phtml -+++ b/app/code/Magento/Backend/view/adminhtml/templates/widget/button.phtml -@@ -3,9 +3,6 @@ - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -- --// @codingStandardsIgnoreFile -- - ?> - <?php - /** -@@ -13,7 +10,7 @@ - */ - ?> - <?= $block->getBeforeHtml() ?> --<button <?= /* @escapeNotVerified */ $block->getAttributesHtml(), $block->getUiId() ?>> -- <span><?= /* @escapeNotVerified */ $block->getLabel() ?></span> -+<button <?= /* @noEscape */ $block->getAttributesHtml(), $block->getUiId() ?>> -+ <span><?= $block->escapeHtml($block->getLabel()) ?></span> - </button> - <?= $block->getAfterHtml() ?> -diff --git a/app/code/Magento/Backend/view/adminhtml/templates/widget/button/split.phtml b/app/code/Magento/Backend/view/adminhtml/templates/widget/button/split.phtml -index a115777624e..0123de098a9 100644 ---- a/app/code/Magento/Backend/view/adminhtml/templates/widget/button/split.phtml -+++ b/app/code/Magento/Backend/view/adminhtml/templates/widget/button/split.phtml -@@ -3,9 +3,6 @@ - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -- --// @codingStandardsIgnoreFile -- - ?> - <?php - /** @var $block \Magento\Backend\Block\Widget\Button\SplitButton */ -@@ -15,20 +12,20 @@ - <button <?= $block->getButtonAttributesHtml() ?>> - <span><?= $block->escapeHtml($block->getLabel()) ?></span> - </button> -- <?php if ($block->hasSplit()): ?> -+ <?php if ($block->hasSplit()) : ?> - <button <?= $block->getToggleAttributesHtml() ?>> - <span>Select</span> - </button> - -- <?php if (!$block->getDisabled()): ?> -- <ul class="dropdown-menu" <?= /* @escapeNotVerified */ $block->getUiId("dropdown-menu") ?>> -- <?php foreach ($block->getOptions() as $key => $option): ?> -+ <?php if (!$block->getDisabled()) : ?> -+ <ul class="dropdown-menu" <?= /* @noEscape */ $block->getUiId("dropdown-menu") ?>> -+ <?php foreach ($block->getOptions() as $key => $option) : ?> - <li> - <span <?= $block->getOptionAttributesHtml($key, $option) ?>> - <?= $block->escapeHtml($option['label']) ?> - </span> -- <?php if (isset($option['hint'])): ?> -- <div class="tooltip" <?= /* @escapeNotVerified */ $block->getUiId('item', $key, 'tooltip') ?>> -+ <?php if (isset($option['hint'])) : ?> -+ <div class="tooltip" <?= /* @noEscape */ $block->getUiId('item', $key, 'tooltip') ?>> - <a href="<?= $block->escapeHtml($option['hint']['href']) ?>" class="help"> - <?= $block->escapeHtml($option['hint']['label']) ?> - </a> -diff --git a/app/code/Magento/Backend/view/adminhtml/templates/widget/form.phtml b/app/code/Magento/Backend/view/adminhtml/templates/widget/form.phtml -index dc7d02795a0..62f8d25ce1b 100644 ---- a/app/code/Magento/Backend/view/adminhtml/templates/widget/form.phtml -+++ b/app/code/Magento/Backend/view/adminhtml/templates/widget/form.phtml -@@ -4,8 +4,6 @@ - * See COPYING.txt for license details. - */ - --// @codingStandardsIgnoreFile -- - /** @var $block \Magento\Backend\Block\Widget\Form */ - ?> - <?php /* @todo replace .form-inline with better class name */?> -diff --git a/app/code/Magento/Backend/view/adminhtml/templates/widget/form/container.phtml b/app/code/Magento/Backend/view/adminhtml/templates/widget/form/container.phtml -index bcbda8fc761..aa289dbf1eb 100644 ---- a/app/code/Magento/Backend/view/adminhtml/templates/widget/form/container.phtml -+++ b/app/code/Magento/Backend/view/adminhtml/templates/widget/form/container.phtml -@@ -4,15 +4,13 @@ - * See COPYING.txt for license details. - */ - --// @codingStandardsIgnoreFile -- - /** @var $block \Magento\Backend\Block\Widget\Form\Container */ -- ?> --<?= /* @escapeNotVerified */ $block->getFormInitScripts() ?> --<?php if ($block->getButtonsHtml('header')): ?> -- <div class="page-form-actions" <?= /* @escapeNotVerified */ $block->getUiId('content-header') ?>><?= $block->getButtonsHtml('header') ?></div> -+?> -+<?= /* @noEscape */ $block->getFormInitScripts() ?> -+<?php if ($block->getButtonsHtml('header')) : ?> -+ <div class="page-form-actions" <?= /* @noEscape */ $block->getUiId('content-header') ?>><?= $block->getButtonsHtml('header') ?></div> - <?php endif; ?> --<?php if ($block->getButtonsHtml('toolbar')): ?> -+<?php if ($block->getButtonsHtml('toolbar')) : ?> - <div class="page-main-actions"> - <div class="page-actions"> - <div class="page-actions-buttons"> -@@ -22,7 +20,7 @@ - </div> - <?php endif; ?> - <?= $block->getFormHtml() ?> --<?php if ($block->hasFooterButtons()): ?> -+<?php if ($block->hasFooterButtons()) : ?> - <div class="content-footer"> - <p class="form-buttons"><?= $block->getButtonsHtml('footer') ?></p> - </div> -@@ -36,7 +34,7 @@ require([ - - $('#edit_form').form() - .validation({ -- validationUrl: '<?= /* @escapeNotVerified */ $block->getValidationUrl() ?>', -+ validationUrl: '<?= $block->escapeJs($block->getValidationUrl()) ?>', - highlight: function(element) { - var detailsElement = $(element).closest('details'); - if (detailsElement.length && detailsElement.is('.details')) { -@@ -51,4 +49,4 @@ require([ - - }); - </script> --<?= /* @escapeNotVerified */ $block->getFormScripts() ?> -+<?= /* @noEscape */ $block->getFormScripts() ?> -diff --git a/app/code/Magento/Backend/view/adminhtml/templates/widget/form/element.phtml b/app/code/Magento/Backend/view/adminhtml/templates/widget/form/element.phtml -index 720bc1e5825..ec53f7e5c74 100644 ---- a/app/code/Magento/Backend/view/adminhtml/templates/widget/form/element.phtml -+++ b/app/code/Magento/Backend/view/adminhtml/templates/widget/form/element.phtml -@@ -4,59 +4,46 @@ - * See COPYING.txt for license details. - */ - --// @codingStandardsIgnoreFile -- -+$type = $element->getType(); - ?> --<?php switch ($element->getType()) { -- case 'fieldset': ?> -- -+<?php if ($type === 'fieldset') : ?> - <fieldset> -- <legend><?= /* @escapeNotVerified */ $element->getLegend() ?></legend><br /> -- <?php foreach ($element->getElements() as $_element): ?> -- <?= /* @escapeNotVerified */ $formBlock->drawElement($_element) ?> -+ <legend><?= $block->escapeHtml($element->getLegend()) ?></legend><br /> -+ <?php foreach ($element->getElements() as $_element) : ?> -+ <?= /* @noEscape */ $formBlock->drawElement($_element) ?> - <?php endforeach; ?> - </fieldset> -- <?php break; -- case 'column': ?> -- <?php break; -- case 'hidden': ?> -- <input type="<?= /* @escapeNotVerified */ $element->getType() ?>" name="<?= /* @escapeNotVerified */ $element->getName() ?>" id="<?= $element->getHtmlId() ?>" value="<?= /* @escapeNotVerified */ $element->getValue() ?>"> -- <?php break; -- case 'select': ?> -+<?php elseif ($type === 'hidden') : ?> -+ <input type="<?= $block->escapeHtmlAttr($element->getType()) ?>" name="<?= $block->escapeHtmlAttr($element->getName()) ?>" id="<?= $element->getHtmlId() ?>" value="<?= $block->escapeHtmlAttr($element->getValue()) ?>"> -+ <?php elseif ($type === 'select') : ?> - <span class="form_row"> -- <?php if ($element->getLabel()): ?><label for="<?= $element->getHtmlId() ?>"><?= /* @escapeNotVerified */ $element->getLabel() ?>:</label><?php endif; ?> -- <select name="<?= /* @escapeNotVerified */ $element->getName() ?>" id="<?= $element->getHtmlId() ?>" class="select<?= /* @escapeNotVerified */ $element->getClass() ?>" title="<?= /* @escapeNotVerified */ $element->getTitle() ?>"> -- <?php foreach ($element->getValues() as $_value): ?> -- <option <?= /* @escapeNotVerified */ $_value->serialize() ?><?php if ($_value->getValue() == $element->getValue()): ?> selected="selected"<?php endif; ?>><?= /* @escapeNotVerified */ $_value->getLabel() ?></option> -+ <?php if ($element->getLabel()) : ?><label for="<?= $element->getHtmlId() ?>"><?= $block->escapeHtml($element->getLabel()) ?>:</label><?php endif; ?> -+ <select name="<?= $block->escapeHtmlAttr($element->getName()) ?>" id="<?= $element->getHtmlId() ?>" class="select<?= $block->escapeHtmlAttr($element->getClass()) ?>" title="<?= $block->escapeHtmlAttr($element->getTitle()) ?>"> -+ <?php foreach ($element->getValues() as $_value) : ?> -+ <option <?= /* @noEscape */ $_value->serialize() ?><?php if ($_value->getValue() == $element->getValue()) : ?> selected="selected"<?php endif; ?>><?= $block->escapeHtml($_value->getLabel()) ?></option> - <?php endforeach; ?> - </select> - </span> -- <?php break; -- case 'text': -- case 'button': -- case 'password': ?> -+<?php elseif ($type === 'text' || $type === 'button' || $type === 'password') : ?> - <span class="form_row"> -- <?php if ($element->getLabel()): ?><label for="<?= $element->getHtmlId() ?>" <?= /* @escapeNotVerified */ $block->getUiId('label') ?>><?= /* @escapeNotVerified */ $element->getLabel() ?>:</label><?php endif; ?> -- <input type="<?= /* @escapeNotVerified */ $element->getType() ?>" name="<?= /* @escapeNotVerified */ $element->getName() ?>" id="<?= $element->getHtmlId() ?>" value="<?= /* @escapeNotVerified */ $element->getValue() ?>" class="input-text <?= /* @escapeNotVerified */ $element->getClass() ?>" title="<?= /* @escapeNotVerified */ $element->getTitle() ?>" <?= ($element->getOnClick() ? 'onClick="' . $element->getOnClick() . '"' : '') ?>/> -+ <?php if ($element->getLabel()) : ?><label for="<?= $element->getHtmlId() ?>" <?= /* @noEscape */ $block->getUiId('label') ?>><?= $block->escapeHtml($element->getLabel()) ?>:</label><?php endif; ?> -+ <input type="<?= $block->escapeHtmlAttr($element->getType()) ?>" name="<?= $block->escapeHtmlAttr($element->getName()) ?>" id="<?= /* @noEscape */ $element->getHtmlId() ?>" value="<?= $block->escapeHtmlAttr($element->getValue()) ?>" class="input-text <?= $block->escapeHtmlAttr($element->getClass()) ?>" title="<?= $block->escapeHtmlAttr($element->getTitle()) ?>" <?= /* @noEscape */ ($element->getOnClick() ? 'onClick="' . $element->getOnClick() . '"' : '') ?>/> - </span> -- <?php break; -- case 'radio': ?> -+<?php elseif ($type === 'radio') : ?> - <span class="form_row"> -- <?php if ($element->getLabel()): ?><label for="<?= $element->getHtmlId() ?>"><?= /* @escapeNotVerified */ $element->getLabel() ?>:</label><?php endif; ?> -- <input type="<?= /* @escapeNotVerified */ $element->getType() ?>" name="<?= /* @escapeNotVerified */ $element->getName() ?>" id="<?= $element->getHtmlId() ?>" value="<?= /* @escapeNotVerified */ $element->getValue() ?>" class="input-text <?= /* @escapeNotVerified */ $element->getClass() ?>" title="<?= /* @escapeNotVerified */ $element->getTitle() ?>"/> -+ <?php if ($element->getLabel()) : ?><label for="<?= $element->getHtmlId() ?>"><?= $block->escapeHtml($element->getLabel()) ?>:</label><?php endif; ?> -+ <input type="<?= $block->escapeHtmlAttr($element->getType()) ?>" name="<?= $block->escapeHtmlAttr($element->getName()) ?>" id="<?= $element->getHtmlId() ?>" value="<?= $block->escapeHtmlAttr($element->getValue()) ?>" class="input-text <?= $block->escapeHtmlAttr($element->getClass()) ?>" title="<?= $block->escapeHtmlAttr($element->getTitle()) ?>"/> - </span> -- <?php break; -- case 'radios': ?> -+<?php elseif ($type === 'radios') : ?> - <span class="form_row"> -- <label for="<?= $element->getHtmlId() ?>"><?= /* @escapeNotVerified */ $element->getLabel() ?>:</label> -- <?php foreach ($element->getRadios() as $_radio): ?> -- <input type="radio" name="<?= /* @escapeNotVerified */ $_radio->getName() ?>" id="<?= $_radio->getHtmlId() ?>" value="<?= /* @escapeNotVerified */ $_radio->getValue() ?>" class="input-radio <?= /* @escapeNotVerified */ $_radio->getClass() ?>" title="<?= /* @escapeNotVerified */ $_radio->getTitle() ?>" <?= ($_radio->getValue() == $element->getChecked()) ? 'checked="true"' : '' ?> > <?= /* @escapeNotVerified */ $_radio->getLabel() ?> -+ <label for="<?= $element->getHtmlId() ?>"><?= $block->escapeHtml($element->getLabel()) ?>:</label> -+ <?php foreach ($element->getRadios() as $_radio) : ?> -+ <input type="radio" name="<?= $block->escapeHtmlAttr($_radio->getName()) ?>" id="<?= $_radio->getHtmlId() ?>" value="<?= $block->escapeHtmlAttr($_radio->getValue()) ?>" class="input-radio <?= $block->escapeHtmlAttr($_radio->getClass()) ?>" title="<?= $block->escapeHtmlAttr($_radio->getTitle()) ?>" <?= ($_radio->getValue() == $element->getChecked()) ? 'checked="true"' : '' ?> > <?= $block->escapeHtml($_radio->getLabel()) ?> - <?php endforeach; ?> - </span> -- <?php break; -- case 'wysiwyg': ?> -+<?php elseif ($type === 'wysiwyg') : ?> - <span class="form_row"> -- <label for="<?= $element->getHtmlId() ?>"><?= /* @escapeNotVerified */ $element->getLabel() ?>:</label> -+ <label for="<?= $element->getHtmlId() ?>"><?= $block->escapeHtml($element->getLabel()) ?>:</label> - <script> - require([ - "wysiwygAdapter" -@@ -65,7 +52,7 @@ - tinyMCE.init({ - mode : "exact", - theme : "advanced", -- elements : "<?= /* @escapeNotVerified */ $element->getName() ?>", -+ elements : "<?= $block->escapeJs($element->getName()) ?>", - plugins : "inlinepopups,style,layer,table,save,advhr,advimage,advlink,emotions,iespell,insertdatetime,preview,zoom,media,searchreplace,print,contextmenu,paste,directionality,fullscreen,noneditable,visualchars,nonbreaking,xhtmlxtras", - theme_advanced_buttons1 : "newdocument,|,bold,italic,underline,strikethrough,|,justifyleft,justifycenter,justifyright,justifyfull,|,styleselect,formatselect,fontselect,fontsizeselect", - theme_advanced_buttons2 : "cut,copy,paste,pastetext,pasteword,|,search,replace,|,bullist,numlist,|,outdent,indent,|,undo,redo,|,link,unlink,anchor,image,cleanup,help,code,|,insertdate,inserttime,preview,|,forecolor,backcolor", -@@ -84,24 +71,16 @@ - }); - }); - </script> -- <textarea name="<?= /* @escapeNotVerified */ $element->getName() ?>" title="<?= /* @escapeNotVerified */ $element->getTitle() ?>" id="<?= $element->getHtmlId() ?>" class="textarea <?= /* @escapeNotVerified */ $element->getClass() ?>" cols="80" rows="20"><?= /* @escapeNotVerified */ $element->getValue() ?></textarea> -+ <textarea name="<?= $block->escapeHtmlAttr($element->getName()) ?>" title="<?= $block->escapeHtmlAttr($element->getTitle()) ?>" id="<?= $element->getHtmlId() ?>" class="textarea <?= $block->escapeHtmlAttr($element->getClass()) ?>" cols="80" rows="20"><?= $block->escapeHtml($element->getValue()) ?></textarea> - </span> -- <?php break; -- case 'textarea': ?> -+<?php elseif ($type === 'textarea') : ?> - <span class="form_row"> -- <label for="<?= $element->getHtmlId() ?>"><?= /* @escapeNotVerified */ $element->getLabel() ?>:</label> -- <textarea name="<?= /* @escapeNotVerified */ $element->getName() ?>" title="<?= /* @escapeNotVerified */ $element->getTitle() ?>" id="<?= $element->getHtmlId() ?>" class="textarea <?= /* @escapeNotVerified */ $element->getClass() ?>" cols="15" rows="2"><?= /* @escapeNotVerified */ $element->getValue() ?></textarea> -+ <label for="<?= $element->getHtmlId() ?>"><?= $block->escapeHtml($element->getLabel()) ?>:</label> -+ <textarea name="<?= $block->escapeHtmlAttr($element->getName()) ?>" title="<?= $block->escapeHtmlAttr($element->getTitle()) ?>" id="<?= $element->getHtmlId() ?>" class="textarea <?= $block->escapeHtmlAttr($element->getClass()) ?>" cols="15" rows="2"><?= $block->escapeHtml($element->getValue()) ?></textarea> - </span> -- <?php break; -- case 'editor': ?> -- <?php break; -- case 'file': ?> -- <?php break; -- case 'checkbox': ?> -- <?php break; --} ?> --<?php if ($element->getScript()): ?> -+<?php endif; ?> -+<?php if ($element->getScript()) : ?> - <script> -- <?= /* @escapeNotVerified */ $element->getScript() ?> -+ <?= /* @noEscape */ $element->getScript() ?> - </script> - <?php endif; ?> -diff --git a/app/code/Magento/Backend/view/adminhtml/templates/widget/form/element/gallery.phtml b/app/code/Magento/Backend/view/adminhtml/templates/widget/form/element/gallery.phtml -index e11c0efc123..5c07b35e72a 100644 ---- a/app/code/Magento/Backend/view/adminhtml/templates/widget/form/element/gallery.phtml -+++ b/app/code/Magento/Backend/view/adminhtml/templates/widget/form/element/gallery.phtml -@@ -3,20 +3,17 @@ - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -- --// @codingStandardsIgnoreFile -- - ?> - <tr> - <td colspan="2"> --<label for="gallery"><?= /* @escapeNotVerified */ __('Images') ?></label> -+<label for="gallery"><?= $block->escapeHtml(__('Images')) ?></label> - <table id="gallery" class="gallery" border="0" cellspacing="3" cellpadding="0"> - <thead id="gallery_thead" class="gallery"> - <tr class="gallery"> -- <td class="gallery" valign="middle" align="center"><?= /* @escapeNotVerified */ __('Big Image') ?></td> -- <td class="gallery" valign="middle" align="center"><?= /* @escapeNotVerified */ __('Thumbnail') ?></td> -- <td class="gallery" valign="middle" align="center"><?= /* @escapeNotVerified */ __('Sort Order') ?></td> -- <td class="gallery" valign="middle" align="center"><?= /* @escapeNotVerified */ __('Delete') ?></td> -+ <td class="gallery" valign="middle" align="center"><?= $block->escapeHtml(__('Big Image')) ?></td> -+ <td class="gallery" valign="middle" align="center"><?= $block->escapeHtml(__('Thumbnail')) ?></td> -+ <td class="gallery" valign="middle" align="center"><?= $block->escapeHtml(__('Sort Order')) ?></td> -+ <td class="gallery" valign="middle" align="center"><?= $block->escapeHtml(__('Delete')) ?></td> - </tr> - </thead> - -@@ -28,22 +25,22 @@ - - <tbody class="gallery"> - --<?php $i = 0; if (!is_null($block->getValues())): ?> -- <?php foreach ($block->getValues() as $image): $i++; ?> -- <tr id="<?= $block->getElement()->getHtmlId() ?>_tr_<?= /* @escapeNotVerified */ $image->getValueId() ?>" class="gallery"> -- <?php foreach ($block->getValues()->getAttributeBackend()->getImageTypes() as $type): ?> -+<?php $i = 0; if ($block->getValues() !== null) : ?> -+ <?php foreach ($block->getValues() as $image) : $i++; ?> -+ <tr id="<?= $block->getElement()->getHtmlId() ?>_tr_<?= $block->escapeHtmlAttr($image->getValueId()) ?>" class="gallery"> -+ <?php foreach ($block->getValues()->getAttributeBackend()->getImageTypes() as $type) : ?> - <td class="gallery" align="center" style="vertical-align:bottom;"> -- <a href="<?= /* @escapeNotVerified */ $image->setType($type)->getSourceUrl() ?>" target="_blank" onclick="imagePreview('<?= $block->getElement()->getHtmlId() ?>_image_<?= /* @escapeNotVerified */ $type ?>_<?= /* @escapeNotVerified */ $image->getValueId() ?>');return false;"> -- <img id="<?= $block->getElement()->getHtmlId() ?>_image_<?= /* @escapeNotVerified */ $type ?>_<?= /* @escapeNotVerified */ $image->getValueId() ?>" src="<?= /* @escapeNotVerified */ $image->setType($type)->getSourceUrl() ?>?<?= /* @escapeNotVerified */ time() ?>" alt="<?= /* @escapeNotVerified */ $image->getValue() ?>" title="<?= /* @escapeNotVerified */ $image->getValue() ?>" height="25" class="small-image-preview v-middle"/></a><br/> -- <input type="file" name="<?= /* @escapeNotVerified */ $block->getElement()->getName() ?>_<?= /* @escapeNotVerified */ $type ?>[<?= /* @escapeNotVerified */ $image->getValueId() ?>]" size="1"></td> -+ <a href="<?= $block->escapeUrl($image->setType($type)->getSourceUrl()) ?>" target="_blank" onclick="imagePreview('<?= $block->getElement()->getHtmlId() ?>_image_<?= $block->escapeHtmlAttr($block->escapeJs($type)) ?>_<?= $block->escapeHtmlAttr($block->escapeJs($image->getValueId())) ?>');return false;"> -+ <img id="<?= $block->getElement()->getHtmlId() ?>_image_<?= $block->escapeHtmlAttr($type) ?>_<?= $block->escapeHtmlAttr($image->getValueId()) ?>" src="<?= $block->escapeUrl($image->setType($type)->getSourceUrl()) ?>?<?= /* @noEscape */ time() ?>" alt="<?= $block->escapeHtmlAttr($image->getValue()) ?>" title="<?= $block->escapeHtmlAttr($image->getValue()) ?>" height="25" class="small-image-preview v-middle"/></a><br/> -+ <input type="file" name="<?= $block->escapeHtmlAttr($block->getElement()->getName()) ?>_<?= $block->escapeHtmlAttr($type) ?>[<?= $block->escapeHtmlAttr($image->getValueId()) ?>]" size="1"></td> - <?php endforeach; ?> -- <td class="gallery" align="center" style="vertical-align:bottom;"><input type="input" name="<?= /* @escapeNotVerified */ $block->getElement()->getParentName() ?>[position][<?= /* @escapeNotVerified */ $image->getValueId() ?>]" value="<?= /* @escapeNotVerified */ $image->getPosition() ?>" id="<?= $block->getElement()->getHtmlId() ?>_position_<?= /* @escapeNotVerified */ $image->getValueId() ?>" size="3"/></td> -- <td class="gallery" align="center" style="vertical-align:bottom;"><?= $block->getDeleteButtonHtml($image->getValueId()) ?><input type="hidden" name="<?= /* @escapeNotVerified */ $block->getElement()->getParentName() ?>[delete][<?= /* @escapeNotVerified */ $image->getValueId() ?>]" id="<?= $block->getElement()->getHtmlId() ?>_delete_<?= /* @escapeNotVerified */ $image->getValueId() ?>"/></td> -+ <td class="gallery" align="center" style="vertical-align:bottom;"><input type="input" name="<?= $block->escapeHtmlAttr($block->getElement()->getParentName()) ?>[position][<?= $block->escapeHtmlAttr($image->getValueId()) ?>]" value="<?= $block->escapeHtmlAttr($image->getPosition()) ?>" id="<?= $block->getElement()->getHtmlId() ?>_position_<?= $block->escapeHtmlAttr($image->getValueId()) ?>" size="3"/></td> -+ <td class="gallery" align="center" style="vertical-align:bottom;"><?= $block->getDeleteButtonHtml($image->getValueId()) ?><input type="hidden" name="<?= $block->escapeHtmlAttr($block->getElement()->getParentName()) ?>[delete][<?= $block->escapeHtmlAttr($image->getValueId()) ?>]" id="<?= $block->getElement()->getHtmlId() ?>_delete_<?= $block->escapeHtmlAttr($image->getValueId()) ?>"/></td> - </tr> - <?php endforeach; ?> - <?php endif; ?> - --<?php if ($i == 0): ?> -+<?php if ($i == 0) : ?> - <script> - document.getElementById("gallery_thead").style.visibility="hidden"; - </script> -@@ -56,7 +53,7 @@ require([ - 'prototype' - ], function () { - id = 0; --num_of_images = <?= /* @escapeNotVerified */ $i ?>; -+num_of_images = <?= /* @noEscape */ $i ?>; - - window.addNewImage = function() - { -@@ -65,17 +62,19 @@ window.addNewImage = function() - - id--; - num_of_images++; -- new_file_input = '<input type="file" name="<?= /* @escapeNotVerified */ $block->getElement()->getName() ?>_%j%[%id%]" size="1">'; -+ new_file_input = '<input type="file" name="<?= $block->escapeHtmlAttr($block->getElement()->getName()) ?>_%j%[%id%]" size="1">'; - - // Sort order input - var new_row_input = document.createElement( 'input' ); - new_row_input.type = 'text'; -- new_row_input.name = '<?= /* @escapeNotVerified */ $block->getElement()->getParentName() ?>[position]['+id+']'; -+ new_row_input.name = '<?= $block->escapeJs($block->getElement()->getParentName()) ?>[position]['+id+']'; - new_row_input.size = '3'; - new_row_input.value = '0'; - - // Delete button -- new_row_button = <?= /* @escapeNotVerified */ $this->helper('Magento\Framework\Json\Helper\Data')->jsonEncode($block->getDeleteButtonHtml("this")) ?>; -+ <?php //phpcs:disable ?> -+ new_row_button = <?= /* @noEscape */ $this->helper(\Magento\Framework\Json\Helper\Data::class)->jsonEncode($block->getDeleteButtonHtml("this")) ?>; -+ <?php // phpcs:enable ?> - - table = document.getElementById( "gallery" ); - -@@ -114,8 +113,8 @@ window.deleteImage = function(image) - document.getElementById("gallery_thead").style.visibility="hidden"; - } - if (image>0) { -- document.getElementById('<?= /* @escapeNotVerified */ $block->getElement()->getName() ?>_delete_'+image).value=image; -- document.getElementById('<?= /* @escapeNotVerified */ $block->getElement()->getName() ?>_tr_'+image).style.display='none'; -+ document.getElementById('<?= $block->escapeJs($block->getElement()->getName()) ?>_delete_'+image).value=image; -+ document.getElementById('<?= $block->escapeJs($block->getElement()->getName()) ?>_tr_'+image).style.display='none'; - } else { - image.parentNode.parentNode.parentNode.removeChild( image.parentNode.parentNode ); - } -diff --git a/app/code/Magento/Backend/view/adminhtml/templates/widget/form/renderer/element.phtml b/app/code/Magento/Backend/view/adminhtml/templates/widget/form/renderer/element.phtml -index ae0bcb826a1..e74e5b1e0fe 100644 ---- a/app/code/Magento/Backend/view/adminhtml/templates/widget/form/renderer/element.phtml -+++ b/app/code/Magento/Backend/view/adminhtml/templates/widget/form/renderer/element.phtml -@@ -3,16 +3,13 @@ - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -- --// @codingStandardsIgnoreFile -- - ?> - <?php $_element = $block->getElement() ?> --<?php if ($_element->getNoSpan() !== true): ?> -+<?php if ($_element->getNoSpan() !== true) : ?> - <span class="field-row"> - <?php endif; ?> - <?= $_element->getLabelHtml() ?> - <?= $_element->getElementHtml() ?> --<?php if ($_element->getNoSpan() !== true): ?> -+<?php if ($_element->getNoSpan() !== true) : ?> - </span> - <?php endif; ?> -diff --git a/app/code/Magento/Backend/view/adminhtml/templates/widget/form/renderer/fieldset.phtml b/app/code/Magento/Backend/view/adminhtml/templates/widget/form/renderer/fieldset.phtml -index aaf1cb5ff55..7eb6e95fa85 100644 ---- a/app/code/Magento/Backend/view/adminhtml/templates/widget/form/renderer/fieldset.phtml -+++ b/app/code/Magento/Backend/view/adminhtml/templates/widget/form/renderer/fieldset.phtml -@@ -3,9 +3,6 @@ - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -- --// @codingStandardsIgnoreFile -- - ?> - <?php - /** @var $element \Magento\Framework\Data\Form\Element\Fieldset */ -@@ -32,40 +29,43 @@ if ($isField) { - - <?php - /** --* @todo investigate situations, when the following is needed: --* echo $element->getHeaderBar(); --* echo $element->getSubFieldsetHtml(); --*/ ?> -+ * @todo investigate situations, when the following is needed: -+ * echo $element->getHeaderBar(); -+ * echo $element->getSubFieldsetHtml(); -+ */ ?> - --<?php if ($isWrapped): ?> -+<?php if ($isWrapped) : ?> - <div class="fieldset-wrapper <?= ($isCollapsable) ? 'admin__collapsible-block-wrapper ' : '' ?>" -- id="<?= /* @escapeNotVerified */ $containerId ? $containerId : $id . '-wrapper' ?>" -- data-role="<?= /* @escapeNotVerified */ $id ?>-wrapper"> -+ id="<?= $block->escapeHtmlAttr($containerId ? $containerId : $id . '-wrapper') ?>" -+ data-role="<?= $block->escapeHtmlAttr($id) ?>-wrapper"> - <div class="fieldset-wrapper-title admin__fieldset-wrapper-title"> -- <strong <?php /* @escapeNotVerified */ echo($isCollapsable) ? -+ <strong <?= /* @noEscape */ $isCollapsable ? - 'class="admin__collapsible-title" data-toggle="collapse" data-target="#' . $id . '-content"' : - 'class="title"'; ?>> -- <span><?= /* @escapeNotVerified */ $element->getLegend() ?></span> -+ <span><?= $block->escapeHtml($element->getLegend()) ?></span> - </strong> -- <?= /* @escapeNotVerified */ $titleActions ?> -+ <?= /* @noEscape */ $titleActions ?> - </div> - <div class="fieldset-wrapper-content admin__fieldset-wrapper-content<?= ($isCollapsable) ? ' collapse' : '' ?>" -- id="<?= /* @escapeNotVerified */ $id ?>-content" -- data-role="<?= /* @escapeNotVerified */ $id ?>-content"> -+ id="<?= $block->escapeHtmlAttr($id) ?>-content" -+ data-role="<?= $block->escapeHtmlAttr($id) ?>-content"> - <?php endif; ?> - -- <?php if (!$element->getNoContainer()): ?> -- <fieldset class="<?= /* @escapeNotVerified */ $cssClass ?>" id="<?= /* @escapeNotVerified */ $id ?>"> -- <?php if ($element->getLegend() && !$isWrapped): ?> -- <legend class="<?= /* @escapeNotVerified */ $isField ? 'label admin__field-label' : 'admin__legend legend' ?>"> -- <span><?= /* @escapeNotVerified */ $element->getLegend() ?></span> -+ <?php if (!$element->getNoContainer()) : ?> -+ <fieldset class="<?= $block->escapeHtmlAttr($cssClass) ?>" id="<?= $block->escapeHtmlAttr($id) ?>"> -+ <?php if (strlen($element->getBeforeElementHtml())) : ?> -+ <?= $element->getBeforeElementHtml() ?> -+ <?php endif ?> -+ <?php if ($element->getLegend() && !$isWrapped) : ?> -+ <legend class="<?= /* @noEscape */ $isField ? 'label admin__field-label' : 'admin__legend legend' ?>"> -+ <span><?= $block->escapeHtml($element->getLegend()) ?></span> - </legend><br /> - <?php endif; ?> - <?php endif; ?> - - - <div class="messages"> -- <?php if ($element->getComment() && !$isField): ?> -+ <?php if ($element->getComment() && !$isField) : ?> - <div class="message message-notice"><?= $block->escapeHtml($element->getComment()) ?></div> - <?php endif; ?> - </div> -@@ -73,34 +73,34 @@ if ($isField) { - - <?= ($isField) ? '<div class="control admin__field-control">' : '' ?> - -- <?php if ($element->hasHtmlContent() && !$isField): ?> -+ <?php if ($element->hasHtmlContent() && !$isField) : ?> - <?= $element->getHtmlContent() ?> -- <?php else: ?> -+ <?php else : ?> - -- <?php if ($isField && $count > 1):?> -- <div class="fields-group-<?= /* @escapeNotVerified */ $count ?>"> -+ <?php if ($isField && $count > 1) : ?> -+ <div class="fields-group-<?= /* @noEscape */ $count ?>"> - <?php endif; ?> - - <?= $element->getBasicChildrenHtml() ?> - - <?= ($isField && $count > 1) ? '</div>' : '' ?> - -- <?php if ($element->getComment() && $isField): ?> -+ <?php if ($element->getComment() && $isField) : ?> - <div class="note"><?= $block->escapeHtml($element->getComment()) ?></div> - <?php endif; ?> - -- <?php if ($element->hasAdvanced() && !$isField): ?> -+ <?php if ($element->hasAdvanced() && !$isField) : ?> - <?= (!$element->getNoContainer() && $advancedAfter) ? '</fieldset>' : '' ?> -- <details data-mage-init='{"details": {}}' class="details admin__collapsible-block-wrapper" id="details<?= /* @escapeNotVerified */ $id ?>"> -- <summary class="details-summary admin__collapsible-title" id="details-summary<?= /* @escapeNotVerified */ $id ?>"> -- <span><?= /* @escapeNotVerified */ $advancedLabel ?></span> -+ <details data-mage-init='{"details": {}}' class="details admin__collapsible-block-wrapper" id="details<?= /* @noEscape */ $id ?>"> -+ <summary class="details-summary admin__collapsible-title" id="details-summary<?= /* @noEscape */ $id ?>"> -+ <span><?= $block->escapeHtml($advancedLabel) ?></span> - </summary> -- <div class="details-content admin__fieldset" id="details-content<?= /* @escapeNotVerified */ $id ?>"> -+ <div class="details-content admin__fieldset" id="details-content<?= /* @noEscape */ $id ?>"> - <?= $element->getAdvancedChildrenHtml() ?> - </div> - </details> -- <?php elseif ($element->hasAdvanced() && $isField): ?> -- <div class="nested" id="nested<?= /* @escapeNotVerified */ $id ?>"> -+ <?php elseif ($element->hasAdvanced() && $isField) : ?> -+ <div class="nested" id="nested<?= /* @noEscape */ $id ?>"> - <?= $element->getAdvancedChildrenHtml() ?> - </div> - <?php endif; ?> -@@ -110,11 +110,11 @@ if ($isField) { - <?php endif; ?> - - -- <?php if (!$element->getNoContainer() && !$advancedAfter): ?> -+ <?php if (!$element->getNoContainer() && !$advancedAfter) : ?> - </fieldset> - <?php endif; ?> - --<?php if ($isWrapped): ?> -+<?php if ($isWrapped) : ?> - </div> - </div> - <?php endif; ?> -diff --git a/app/code/Magento/Backend/view/adminhtml/templates/widget/form/renderer/fieldset/element.phtml b/app/code/Magento/Backend/view/adminhtml/templates/widget/form/renderer/fieldset/element.phtml -index 3608ed7662e..bec6fe84fb2 100644 ---- a/app/code/Magento/Backend/view/adminhtml/templates/widget/form/renderer/fieldset/element.phtml -+++ b/app/code/Magento/Backend/view/adminhtml/templates/widget/form/renderer/fieldset/element.phtml -@@ -3,9 +3,6 @@ - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -- --// @codingStandardsIgnoreFile -- - ?> - <?php - /* @var $block \Magento\Backend\Block\Widget\Form\Renderer\Fieldset\Element */ -@@ -26,20 +23,20 @@ $fieldAttributes = $fieldId . ' class="' . $fieldClass . '" ' - . ($element->getFieldExtraAttributes() ? ' ' . $element->getFieldExtraAttributes() : ''); - ?> - --<?php if (!$element->getNoDisplay()): ?> -- <?php if ($element->getType() == 'hidden'): ?> -+<?php if (!$element->getNoDisplay()) : ?> -+ <?php if ($element->getType() == 'hidden') : ?> - <?= $element->getElementHtml() ?> -- <?php else: ?> -- <div<?= /* @escapeNotVerified */ $fieldAttributes ?>> -- <?php if ($elementBeforeLabel): ?> -+ <?php else : ?> -+ <div<?= /* @noEscape */ $fieldAttributes ?>> -+ <?php if ($elementBeforeLabel) : ?> - <?= $element->getElementHtml() ?> - <?= $element->getLabelHtml('', $element->getScopeLabel()) ?> -- <?= /* @escapeNotVerified */ $note ?> -- <?php else: ?> -+ <?= /* @noEscape */ $note ?> -+ <?php else : ?> - <?= $element->getLabelHtml('', $element->getScopeLabel()) ?> - <div class="admin__field-control control"> -- <?= /* @escapeNotVerified */ ($addOn) ? '<div class="admin__field">' . $element->getElementHtml() . '</div>' : $element->getElementHtml() ?> -- <?= /* @escapeNotVerified */ $note ?> -+ <?= /* @noEscape */ ($addOn) ? '<div class="admin__field">' . $element->getElementHtml() . '</div>' : $element->getElementHtml() ?> -+ <?= /* @noEscape */ $note ?> - </div> - <?php endif; ?> - </div> -diff --git a/app/code/Magento/Backend/view/adminhtml/templates/widget/grid.phtml b/app/code/Magento/Backend/view/adminhtml/templates/widget/grid.phtml -index c665c1095a5..63cdae13490 100644 ---- a/app/code/Magento/Backend/view/adminhtml/templates/widget/grid.phtml -+++ b/app/code/Magento/Backend/view/adminhtml/templates/widget/grid.phtml -@@ -3,9 +3,6 @@ - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -- --// @codingStandardsIgnoreFile -- - ?> - <?php - /** -@@ -19,101 +16,101 @@ - * - */ - /* @var $block \Magento\Backend\Block\Widget\Grid */ --$numColumns = !is_null($block->getColumns()) ? sizeof($block->getColumns()) : 0; -+$numColumns = $block->getColumns() !== null ? count($block->getColumns()) : 0; - ?> --<?php if ($block->getCollection()): ?> -+<?php if ($block->getCollection()) : ?> - --<?php if ($block->canDisplayContainer()): ?> -+ <?php if ($block->canDisplayContainer()) : ?> - <div id="<?= $block->escapeHtml($block->getId()) ?>" data-grid-id="<?= $block->escapeHtml($block->getId()) ?>"> --<?php else: ?> --<?= $block->getLayout()->getMessagesBlock()->getGroupedHtml() ?> --<?php endif; ?> -+ <?php else : ?> -+ <?= $block->getLayout()->getMessagesBlock()->getGroupedHtml() ?> -+ <?php endif; ?> - - <div class="admin__data-grid-header admin__data-grid-toolbar"> - <?php $massActionAvailable = $block->getChildBlock('grid.massaction') && $block->getChildBlock('grid.massaction')->isAvailable() ?> -- <?php if ($block->getPagerVisibility() || $block->getExportTypes() || $block->getChildBlock('grid.columnSet')->getFilterVisibility() || $massActionAvailable): ?> -+ <?php if ($block->getPagerVisibility() || $block->getExportTypes() || $block->getChildBlock('grid.columnSet')->getFilterVisibility() || $massActionAvailable) : ?> - <div class="admin__data-grid-header-row"> -- <?php if ($massActionAvailable): ?> -+ <?php if ($massActionAvailable) : ?> - <?= $block->getMainButtonsHtml() ? '<div class="admin__filter-actions">' . $block->getMainButtonsHtml() . '</div>' : '' ?> - <?php endif; ?> - -- <?php if ($block->getChildBlock('grid.export')): ?> -+ <?php if ($block->getChildBlock('grid.export')) : ?> - <?= $block->getChildHtml('grid.export') ?> - <?php endif; ?> - </div> - <?php endif; ?> -- <div class="<?php if($massActionAvailable) { echo '_massaction ';} ?>admin__data-grid-header-row"> -- <?php if ($massActionAvailable): ?> -+ <div class="<?php if ($massActionAvailable) { echo '_massaction ';} ?>admin__data-grid-header-row"> -+ <?php if ($massActionAvailable) : ?> - <?= $block->getChildHtml('grid.massaction') ?> -- <?php else: ?> -+ <?php else : ?> - <?= $block->getMainButtonsHtml() ? '<div class="admin__filter-actions">' . $block->getMainButtonsHtml() . '</div>' : '' ?> - <?php endif; ?> - <?php $countRecords = $block->getCollection()->getSize(); ?> - <div class="admin__control-support-text"> -- <span id="<?= $block->escapeHtml($block->getHtmlId()) ?>-total-count" <?= /* @escapeNotVerified */ $block->getUiId('total-count') ?>> -- <?= /* @escapeNotVerified */ $countRecords ?> -+ <span id="<?= $block->escapeHtml($block->getHtmlId()) ?>-total-count" <?= /* @noEscape */ $block->getUiId('total-count') ?>> -+ <?= /* @noEscape */ $countRecords ?> - </span> -- <?= /* @escapeNotVerified */ __('records found') ?> -+ <?= $block->escapeHtml(__('records found')) ?> - <span id="<?= $block->escapeHtml($block->getHtmlId()) ?>_massaction-count" -- class="mass-select-info _empty"><strong data-role="counter">0</strong> <span><?= /* @escapeNotVerified */ __('selected') ?></span></span> -+ class="mass-select-info _empty"><strong data-role="counter">0</strong> <span><?= $block->escapeHtml(__('selected')) ?></span></span> - </div> -- <?php if ($block->getPagerVisibility()): ?> -+ <?php if ($block->getPagerVisibility()) : ?> - <div class="admin__data-grid-pager-wrap"> -- <select name="<?= /* @escapeNotVerified */ $block->getVarNameLimit() ?>" -+ <select name="<?= $block->escapeHtmlAttr($block->getVarNameLimit()) ?>" - id="<?= $block->escapeHtml($block->getHtmlId()) ?>_page-limit" -- onchange="<?= /* @escapeNotVerified */ $block->getJsObjectName() ?>.loadByElement(this)" <?= /* @escapeNotVerified */ $block->getUiId('per-page') ?> -+ onchange="<?= /* @noEscape */ $block->getJsObjectName() ?>.loadByElement(this)" <?= /* @noEscape */ $block->getUiId('per-page') ?> - class="admin__control-select"> -- <option value="20"<?php if ($block->getCollection()->getPageSize() == 20): ?> -+ <option value="20"<?php if ($block->getCollection()->getPageSize() == 20) : ?> - selected="selected"<?php endif; ?>>20 - </option> -- <option value="30"<?php if ($block->getCollection()->getPageSize() == 30): ?> -+ <option value="30"<?php if ($block->getCollection()->getPageSize() == 30) : ?> - selected="selected"<?php endif; ?>>30 - </option> -- <option value="50"<?php if ($block->getCollection()->getPageSize() == 50): ?> -+ <option value="50"<?php if ($block->getCollection()->getPageSize() == 50) : ?> - selected="selected"<?php endif; ?>>50 - </option> -- <option value="100"<?php if ($block->getCollection()->getPageSize() == 100): ?> -+ <option value="100"<?php if ($block->getCollection()->getPageSize() == 100) : ?> - selected="selected"<?php endif; ?>>100 - </option> -- <option value="200"<?php if ($block->getCollection()->getPageSize() == 200): ?> -+ <option value="200"<?php if ($block->getCollection()->getPageSize() == 200) : ?> - selected="selected"<?php endif; ?>>200 - </option> - </select> - <label for="<?= $block->escapeHtml($block->getHtmlId()) ?>_page-limit" -- class="admin__control-support-text"><?= /* @escapeNotVerified */ __('per page') ?></label> -+ class="admin__control-support-text"><?= $block->escapeHtml(__('per page')) ?></label> - <div class="admin__data-grid-pager"> - <?php $_curPage = $block->getCollection()->getCurPage() ?> - <?php $_lastPage = $block->getCollection()->getLastPageNumber() ?> - -- <?php if ($_curPage > 1): ?> -+ <?php if ($_curPage > 1) : ?> - <button class="action-previous" - type="button" -- onclick="<?= /* @escapeNotVerified */ $block->getJsObjectName() ?>.setPage('<?= /* @escapeNotVerified */ ($_curPage - 1) ?>');return false;"> -- <span><?= /* @escapeNotVerified */ __('Previous page') ?></span> -+ onclick="<?= /* @noEscape */ $block->getJsObjectName() ?>.setPage('<?= /* @noEscape */ ($_curPage - 1) ?>');return false;"> -+ <span><?= $block->escapeHtml(__('Previous page')) ?></span> - </button> -- <?php else: ?> -- <button type="button" class="action-previous disabled"><span><?= /* @escapeNotVerified */ __('Previous page') ?></span></button> -+ <?php else : ?> -+ <button type="button" class="action-previous disabled"><span><?= $block->escapeHtml(__('Previous page')) ?></span></button> - <?php endif; ?> - - <input type="text" - id="<?= $block->escapeHtml($block->getHtmlId()) ?>_page-current" -- name="<?= /* @escapeNotVerified */ $block->getVarNamePage() ?>" -- value="<?= /* @escapeNotVerified */ $_curPage ?>" -+ name="<?= $block->escapeHtmlAttr($block->getVarNamePage()) ?>" -+ value="<?= $block->escapeHtmlAttr($_curPage) ?>" - class="admin__control-text" -- onkeypress="<?= /* @escapeNotVerified */ $block->getJsObjectName() ?>.inputPage(event, '<?= /* @escapeNotVerified */ $_lastPage ?>')" <?= /* @escapeNotVerified */ $block->getUiId('current-page') ?> /> -+ onkeypress="<?= /* @noEscape */ $block->getJsObjectName() ?>.inputPage(event, '<?= /* @noEscape */ $_lastPage ?>')" <?= /* @noEscape */ $block->getUiId('current-page') ?> /> - - <label class="admin__control-support-text" for="<?= $block->escapeHtml($block->getHtmlId()) - ?>_page-current"> -- <?= /* @escapeNotVerified */ __('of %1', '<span>' . $block->getCollection()->getLastPageNumber() . '</span>') ?> -+ <?= /* @noEscape */ __('of %1', '<span>' . $block->getCollection()->getLastPageNumber() . '</span>') ?> - </label> -- <?php if ($_curPage < $_lastPage): ?> -- <button title="<?= /* @escapeNotVerified */ __('Next page') ?>" -+ <?php if ($_curPage < $_lastPage) : ?> -+ <button type="button" title="<?= $block->escapeHtmlAttr(__('Next page')) ?>" - class="action-next" -- onclick="<?= /* @escapeNotVerified */ $block->getJsObjectName() ?>.setPage('<?= /* @escapeNotVerified */ ($_curPage + 1) ?>');return false;"> -- <span><?= /* @escapeNotVerified */ __('Next page') ?></span> -+ onclick="<?= /* @noEscape */ $block->getJsObjectName() ?>.setPage('<?= /* @noEscape */ ($_curPage + 1) ?>');return false;"> -+ <span><?= $block->escapeHtml(__('Next page')) ?></span> - </button> -- <?php else: ?> -- <button type="button" class="action-next disabled"><span><?= /* @escapeNotVerified */ __('Next page') ?></span></button> -+ <?php else : ?> -+ <button type="button" class="action-next disabled"><span><?= $block->escapeHtml(__('Next page')) ?></span></button> - <?php endif; ?> - </div> - </div> -@@ -121,77 +118,77 @@ $numColumns = !is_null($block->getColumns()) ? sizeof($block->getColumns()) : 0; - </div> - </div> - <div class="admin__data-grid-wrap admin__data-grid-wrap-static"> -- <?php if ($block->getGridCssClass()): ?> -- <table class="<?= /* @escapeNotVerified */ $block->getGridCssClass() ?> data-grid" id="<?= $block->escapeHtml($block->getId()) ?>_table"> -+ <?php if ($block->getGridCssClass()) : ?> -+ <table class="<?= $block->escapeHtmlAttr($block->getGridCssClass()) ?> data-grid" id="<?= $block->escapeHtml($block->getId()) ?>_table"> - <!-- Rendering column set --> - <?= $block->getChildHtml('grid.columnSet') ?> - </table> -- <?php else: ?> -+ <?php else : ?> - - <table class="data-grid" id="<?= $block->escapeHtml($block->getId()) ?>_table"> - <!-- Rendering column set --> - <?= $block->getChildHtml('grid.columnSet') ?> - </table> - -- <?php if ($block->getChildBlock('grid.bottom.links')): ?> -+ <?php if ($block->getChildBlock('grid.bottom.links')) : ?> - <?= $block->getChildHtml('grid.bottom.links') ?> - <?php endif; ?> - - <?php endif ?> - </div> --<?php if ($block->canDisplayContainer()): ?> -+ <?php if ($block->canDisplayContainer()) : ?> - </div> - <script> - var deps = []; - -- <?php if ($block->getDependencyJsObject()): ?> -+ <?php if ($block->getDependencyJsObject()) : ?> - deps.push('uiRegistry'); -- <?php endif; ?> -+ <?php endif; ?> - -- <?php if (strpos($block->getRowClickCallback(), 'order.') !== false): ?> -+ <?php if (strpos($block->getRowClickCallback(), 'order.') !== false) : ?> - deps.push('Magento_Sales/order/create/form'); - deps.push('jquery'); -- <?php endif; ?> -+ <?php endif; ?> - - deps.push('mage/adminhtml/grid'); - - require(deps, function(<?= ($block->getDependencyJsObject() ? 'registry' : '') ?>){ - <?php //TODO: getJsObjectName and getRowClickCallback has unexpected behavior. Should be removed ?> - -- <?php if ($block->getDependencyJsObject()): ?> -- registry.get('<?= /* @escapeNotVerified */ $block->getDependencyJsObject() ?>', function (<?= /* @escapeNotVerified */ $block->getDependencyJsObject() ?>) { -+ <?php if ($block->getDependencyJsObject()) : ?> -+ registry.get('<?= $block->escapeJs($block->getDependencyJsObject()) ?>', function (<?= $block->escapeJs($block->getDependencyJsObject()) ?>) { - <?php endif; ?> - -- <?= /* @escapeNotVerified */ $block->getJsObjectName() ?> = new varienGrid('<?= $block->escapeHtml($block->getId()) ?>', '<?= /* @escapeNotVerified */ $block->getGridUrl() ?>', '<?= /* @escapeNotVerified */ $block->getVarNamePage() ?>', '<?= /* @escapeNotVerified */ $block->getVarNameSort() ?>', '<?= /* @escapeNotVerified */ $block->getVarNameDir() ?>', '<?= /* @escapeNotVerified */ $block->getVarNameFilter() ?>'); -- <?= /* @escapeNotVerified */ $block->getJsObjectName() ?>.useAjax = <?= /* @escapeNotVerified */ $block->getUseAjax() ? 'true' : 'false' ?>; -- <?php if ($block->getRowClickCallback()): ?> -- <?= /* @escapeNotVerified */ $block->getJsObjectName() ?>.rowClickCallback = <?= /* @escapeNotVerified */ $block->getRowClickCallback() ?>; -+ <?= $block->escapeJs($block->getJsObjectName()) ?> = new varienGrid('<?= $block->escapeHtml($block->getId()) ?>', '<?= $block->escapeJs($block->getGridUrl()) ?>', '<?= $block->escapeJs($block->getVarNamePage()) ?>', '<?= $block->escapeJs($block->getVarNameSort()) ?>', '<?= $block->escapeJs($block->getVarNameDir()) ?>', '<?= $block->escapeJs($block->getVarNameFilter()) ?>'); -+ <?= $block->escapeJs($block->getJsObjectName()) ?>.useAjax = <?= /* @noEscape */ $block->getUseAjax() ? 'true' : 'false' ?>; -+ <?php if ($block->getRowClickCallback()) : ?> -+ <?= $block->escapeJs($block->getJsObjectName()) ?>.rowClickCallback = <?= /* @noEscape */ $block->getRowClickCallback() ?>; - <?php endif; ?> -- <?php if ($block->getCheckboxCheckCallback()): ?> -- <?= /* @escapeNotVerified */ $block->getJsObjectName() ?>.checkboxCheckCallback = <?= /* @escapeNotVerified */ $block->getCheckboxCheckCallback() ?>; -+ <?php if ($block->getCheckboxCheckCallback()) : ?> -+ <?= $block->escapeJs($block->getJsObjectName()) ?>.checkboxCheckCallback = <?= /* @noEscape */ $block->getCheckboxCheckCallback() ?>; - <?php endif; ?> -- <?php if ($block->getSortableUpdateCallback()): ?> -- <?= /* @escapeNotVerified */ $block->getJsObjectName() ?>.sortableUpdateCallback = <?= /* @escapeNotVerified */ $block->getSortableUpdateCallback() ?>; -+ <?php if ($block->getSortableUpdateCallback()) : ?> -+ <?= $block->escapeJs($block->getJsObjectName()) ?>.sortableUpdateCallback = <?= /* @noEscape */ $block->getSortableUpdateCallback() ?>; - <?php endif; ?> -- <?= /* @escapeNotVerified */ $block->getJsObjectName() ?>.bindSortable(); -- <?php if ($block->getRowInitCallback()): ?> -- <?= /* @escapeNotVerified */ $block->getJsObjectName() ?>.initRowCallback = <?= /* @escapeNotVerified */ $block->getRowInitCallback() ?>; -- <?= /* @escapeNotVerified */ $block->getJsObjectName() ?>.initGridRows(); -+ <?= $block->escapeJs($block->getJsObjectName()) ?>.bindSortable(); -+ <?php if ($block->getRowInitCallback()) : ?> -+ <?= $block->escapeJs($block->getJsObjectName()) ?>.initRowCallback = <?= /* @noEscape */ $block->getRowInitCallback() ?>; -+ <?= $block->escapeJs($block->getJsObjectName()) ?>.initGridRows(); - <?php endif; ?> -- <?php if ($block->getChildBlock('grid.massaction') && $block->getChildBlock('grid.massaction')->isAvailable()): ?> -- <?= /* @escapeNotVerified */ $block->getChildBlock('grid.massaction')->getJavaScript() ?> -+ <?php if ($block->getChildBlock('grid.massaction') && $block->getChildBlock('grid.massaction')->isAvailable()) : ?> -+ <?= /* @noEscape */ $block->getChildBlock('grid.massaction')->getJavaScript() ?> - <?php endif ?> -- <?= /* @escapeNotVerified */ $block->getAdditionalJavaScript() ?> -+ <?= /* @noEscape */ $block->getAdditionalJavaScript() ?> - -- <?php if ($block->getDependencyJsObject()): ?> -+ <?php if ($block->getDependencyJsObject()) : ?> - }); - <?php endif; ?> - }); - </script> - <?php endif; ?> - --<?php if ($block->getChildBlock('grid.js')): ?> -- <?= $block->getChildHtml('grid.js') ?> --<?php endif; ?> -+ <?php if ($block->getChildBlock('grid.js')) : ?> -+ <?= $block->getChildHtml('grid.js') ?> -+ <?php endif; ?> - - <?php endif ?> -diff --git a/app/code/Magento/Backend/view/adminhtml/templates/widget/grid/column_set.phtml b/app/code/Magento/Backend/view/adminhtml/templates/widget/grid/column_set.phtml -index 5ff9cfbd96f..0906694520c 100644 ---- a/app/code/Magento/Backend/view/adminhtml/templates/widget/grid/column_set.phtml -+++ b/app/code/Magento/Backend/view/adminhtml/templates/widget/grid/column_set.phtml -@@ -3,42 +3,39 @@ - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -- --// @codingStandardsIgnoreFile -- - ?> - <?php - /** - * Template for \Magento\Backend\Block\Widget\Grid\ColumnSet - * @var $block \Magento\Backend\Block\Widget\Grid\ColumnSet - */ --$numColumns = sizeof($block->getColumns()); -+$numColumns = count($block->getColumns()); - ?> --<?php if ($block->getCollection()): ?> -+<?php if ($block->getCollection()) : ?> - <?php - /* This part is commented to remove all <col> tags from the code. */ - /* foreach ($block->getColumns() as $_column): ?> - <col <?= $_column->getHtmlProperty() ?> /> - <?php endforeach; */ ?> -- <?php if ($block->isHeaderVisible()): ?> -+ <?php if ($block->isHeaderVisible()) : ?> - <thead> -- <?php if ($block->isHeaderVisible() || $block->getFilterVisibility()): ?> -+ <?php if ($block->isHeaderVisible() || $block->getFilterVisibility()) : ?> - <tr> -- <?php foreach ($block->getColumns() as $_column): ?> -- <?php /* @var $_column \Magento\Backend\Block\Widget\Grid\Column */ ?> -- <?php if ($_column->getHeaderHtml() == ' '):?> -- <th class="data-grid-th" data-column="<?= /* @escapeNotVerified */ $_column->getId() ?>" -+ <?php foreach ($block->getColumns() as $_column) : ?> -+ <?php /* @var $_column \Magento\Backend\Block\Widget\Grid\Column */ ?> -+ <?php if ($_column->getHeaderHtml() == ' ') :?> -+ <th class="data-grid-th" data-column="<?= $block->escapeHtmlAttr($_column->getId()) ?>" - <?= $_column->getHeaderHtmlProperty() ?>> </th> -- <?php else: ?> -+ <?php else : ?> - <?= $_column->getHeaderHtml() ?> - <?php endif; ?> - <?php endforeach; ?> - </tr> - <?php endif; ?> -- <?php if ($block->isFilterVisible()): ?> -+ <?php if ($block->isFilterVisible()) : ?> - <tr class="data-grid-filters" data-role="filter-form"> -- <?php $i = 0; foreach ($block->getColumns() as $_column): ?> -- <td data-column="<?= /* @escapeNotVerified */ $_column->getId() ?>" <?= $_column->getHeaderHtmlProperty() ?>> -+ <?php $i = 0; foreach ($block->getColumns() as $_column) : ?> -+ <td data-column="<?= $block->escapeHtmlAttr($_column->getId()) ?>" <?= $_column->getHeaderHtmlProperty() ?>> - <?= $_column->getFilterHtml() ?> - </td> - <?php endforeach; ?> -@@ -49,75 +46,76 @@ $numColumns = sizeof($block->getColumns()); - - <tbody> - -- <?php if ($block->getCollection()->getSize() > 0 && !$block->getIsCollapsed()): ?> -- <?php foreach ($block->getCollection() as $_index => $_item): ?> -- <?php if ($block->hasMultipleRows($_item)) :?> -- <?php $block->updateItemByFirstMultiRow($_item); ?> -- <tr title="<?= /* @escapeNotVerified */ $block->getRowUrl($_item) ?>" data-role="row" -- <?php if ($_class = $block->getRowClass($_item)):?> class="<?= /* @escapeNotVerified */ $_class ?>"<?php endif;?> -- ><?php $i = 0; foreach ($block->getColumns() as $_column): -- if ($block->shouldRenderCell($_item, $_column)): -+ <?php if ($block->getCollection()->getSize() > 0 && !$block->getIsCollapsed()) : ?> -+ <?php foreach ($block->getCollection() as $_index => $_item) : ?> -+ <?php if ($block->hasMultipleRows($_item)) :?> -+ <?php $block->updateItemByFirstMultiRow($_item); ?> -+ <tr title="<?= $block->escapeHtmlAttr($block->getRowUrl($_item)) ?>" data-role="row" -+ <?php if ($_class = $block->getRowClass($_item)) :?> class="<?= $block->escapeHtmlAttr($_class) ?>"<?php endif;?> -+ ><?php $i = 0; foreach ($block->getColumns() as $_column) : -+ if ($block->shouldRenderCell($_item, $_column)) : - $_rowspan = $block->getRowspan($_item, $_column); -- ?><td data-column="<?= /* @escapeNotVerified */ $_column->getId() ?>" -- <?= ($_rowspan ? 'rowspan="' . $_rowspan . '" ' : '') ?> -- class="<?= /* @escapeNotVerified */ $_column->getCssProperty() ?> <?= /* @escapeNotVerified */ $_column->getId() == 'massaction' ? 'data-grid-checkbox-cell': '' ?> <?= ++$i == $numColumns ? 'last' : '' ?>" -+ ?><td data-column="<?= $block->escapeHtmlAttr($_column->getId()) ?>" -+ <?= /* @noEscape */ ($_rowspan ? 'rowspan="' . $_rowspan . '" ' : '') ?> -+ class="<?= $block->escapeHtmlAttr($_column->getCssProperty()) ?> <?= /* @noEscape */ $_column->getId() == 'massaction' ? 'data-grid-checkbox-cell': '' ?> <?= ++$i == $numColumns ? 'last' : '' ?>" - > -- <?= (($_html = $_column->getRowField($_item)) != '' ? $_html : ' ') ?> -+ <?= /* @noEscape */ (($_html = $_column->getRowField($_item)) != '' ? $_html : ' ') ?> - </td><?php -- if ($block->shouldRenderEmptyCell($_item, $_column)):?> -- <td colspan="<?= /* @escapeNotVerified */ $block->getEmptyCellColspan($_item) ?>" class="last"> -- <?= /* @escapeNotVerified */ $block->getEmptyCellLabel() ?> -+ if ($block->shouldRenderEmptyCell($_item, $_column)) :?> -+ <td colspan="<?= $block->escapeHtmlAttr($block->getEmptyCellColspan($_item)) ?>" class="last"> -+ <?= $block->escapeHtml($block->getEmptyCellLabel()) ?> - </td><?php - endif; - endif; - endforeach; -- ?></tr> -- <?php $_isFirstRow = true; ?> -- <?php foreach ($block->getMultipleRows($_item) as $_i):?> -- <?php if ($_isFirstRow) : ?> -- <?php $_isFirstRow = false; continue; ?> -- <?php endif; ?> -+?></tr> -+ <?php $_isFirstRow = true; ?> -+ <?php foreach ($block->getMultipleRows($_item) as $_i) : ?> -+ <?php -+ if ($_isFirstRow) { -+ $_isFirstRow = false; -+ continue; -+ } -+ ?> - <tr data-role="row"> -- <?php $i = 0; foreach ($block->getMultipleRowColumns($_i) as $_column): -- ?><td data-column="<?= /* @escapeNotVerified */ $_column->getId() ?>" -- class="<?= /* @escapeNotVerified */ $_column->getCssProperty() ?> <?= /* @escapeNotVerified */ $_column->getId() == 'massaction' ? 'data-grid-checkbox-cell': '' ?> <?= ++$i == $numColumns-1 ? 'last' : '' ?>" -+ <?php $i = 0; foreach ($block->getMultipleRowColumns($_i) as $_column) : -+ ?><td data-column="<?= $block->escapeHtmlAttr($_column->getId()) ?>" -+ class="<?= $block->escapeHtmlAttr($_column->getCssProperty()) ?> <?= /* @noEscape */ $_column->getId() == 'massaction' ? 'data-grid-checkbox-cell': '' ?> <?= ++$i == $numColumns-1 ? 'last' : '' ?>" - > -- <?= (($_html = $_column->getRowField($_i)) != '' ? $_html : ' ') ?> -+ <?= /* @noEscape */ (($_html = $_column->getRowField($_i)) != '' ? $_html : ' ') ?> - </td><?php - endforeach; ?> - </tr> - <?php endforeach;?> - -- <?php if ($block->shouldRenderSubTotal($_item)): ?> -+ <?php if ($block->shouldRenderSubTotal($_item)) : ?> - <tr class="subtotals"> -- <?php $i = 0; foreach ($block->getMultipleRowColumns() as $_column): ?> -- <td data-column="<?= /* @escapeNotVerified */ $_column->getId() ?>" -- class="<?= /* @escapeNotVerified */ $_column->getCssProperty() ?> <?= /* @escapeNotVerified */ $_column->getId() == 'massaction' ? 'data-grid-checkbox-cell': '' ?> <?= ++$i == $numColumns ? 'last' : '' ?>" -+ <?php $i = 0; foreach ($block->getMultipleRowColumns() as $_column) : ?> -+ <td data-column="<?= $block->escapeHtmlAttr($_column->getId()) ?>" -+ class="<?= $block->escapeHtmlAttr($_column->getCssProperty()) ?> <?= /* @noEscape */ $_column->getId() == 'massaction' ? 'data-grid-checkbox-cell': '' ?> <?= ++$i == $numColumns ? 'last' : '' ?>" - > -- <?php /* @escapeNotVerified */ echo $_column->hasSubtotalsLabel() ? $_column->getSubtotalsLabel() -- : $_column->getRowField($block->getSubTotals($_item)); -- ?> -+ <?= /* @noEscape */ $_column->hasSubtotalsLabel() ? $block->escapeHtml($_column->getSubtotalsLabel()) : $_column->getRowField($block->getSubTotals($_item)) ?> - </td> - <?php endforeach; ?> - </tr> - <?php endif; ?> -- <?php else: ?> -- <tr data-role="row" title="<?= /* @escapeNotVerified */ $block->getRowUrl($_item) ?>"<?php if ($_class = $block->getRowClass($_item)):?> -- class="<?= /* @escapeNotVerified */ $_class ?>"<?php endif;?> -+ <?php else : ?> -+ <tr data-role="row" title="<?= $block->escapeHtmlAttr($block->getRowUrl($_item)) ?>"<?php if ($_class = $block->getRowClass($_item)) : ?> -+ class="<?= $block->escapeHtmlAttr($_class) ?>"<?php endif;?> - > -- <?php $i = 0; foreach ($block->getColumns() as $_column): ?> -- <?php if ($block->shouldRenderCell($_item, $_column)):?> -- <td data-column="<?= /* @escapeNotVerified */ $_column->getId() ?>" -- class="<?= /* @escapeNotVerified */ $_column->getCssProperty() ?> <?= /* @escapeNotVerified */ $_column->getId() == 'massaction' ? 'data-grid-checkbox-cell': '' ?> <?= ++$i == $numColumns ? 'last' : '' ?>" -+ <?php $i = 0; foreach ($block->getColumns() as $_column) : ?> -+ <?php if ($block->shouldRenderCell($_item, $_column)) : ?> -+ <td data-column="<?= $block->escapeHtmlAttr($_column->getId()) ?>" -+ class="<?= $block->escapeHtmlAttr($_column->getCssProperty()) ?> <?= /* @noEscape */ $_column->getId() == 'massaction' ? 'data-grid-checkbox-cell': '' ?> <?= ++$i == $numColumns ? 'last' : '' ?>" - > -- <?= (($_html = $_column->getRowField($_item)) != '' ? $_html : ' ') ?> -+ <?= /* @noEscape */ (($_html = $_column->getRowField($_item)) != '' ? $_html : ' ') ?> - </td> -- <?php if ($block->shouldRenderEmptyCell($_item, $_column)):?> -- <td data-column="<?= /* @escapeNotVerified */ $_column->getId() ?>" -- colspan="<?= /* @escapeNotVerified */ $block->getEmptyCellColspan($_item) ?>" -- class="col-no-records <?= /* @escapeNotVerified */ $block->getEmptyTextClass() ?> last" -+ <?php if ($block->shouldRenderEmptyCell($_item, $_column)) : ?> -+ <td data-column="<?= $block->escapeHtmlAttr($_column->getId()) ?>" -+ colspan="<?= $block->escapeHtmlAttr($block->getEmptyCellColspan($_item)) ?>" -+ class="col-no-records <?= $block->escapeHtmlAttr($block->getEmptyTextClass()) ?> last" - > -- <?= /* @escapeNotVerified */ $block->getEmptyCellLabel() ?> -+ <?= $block->escapeHtml($block->getEmptyCellLabel()) ?> - </td> - <?php endif;?> - <?php endif;?> -@@ -125,23 +123,22 @@ $numColumns = sizeof($block->getColumns()); - </tr> - <?php endif; ?> - <?php endforeach; ?> -- <?php elseif ($block->getEmptyText()): ?> -+ <?php elseif ($block->getEmptyText()) : ?> - <tr class="data-grid-tr-no-data" data-role="row"> -- <td class="<?= /* @escapeNotVerified */ $block->getEmptyTextClass() ?>" -- colspan="<?= /* @escapeNotVerified */ $numColumns ?>"><?= /* @escapeNotVerified */ $block->getEmptyText() ?></td> -+ <td class="<?= $block->escapeHtmlAttr($block->getEmptyTextClass()) ?>" -+ colspan="<?= $block->escapeHtmlAttr($numColumns) ?>"><?= $block->escapeHtml($block->getEmptyText()) ?></td> - </tr> - <?php endif; ?> - </tbody> - -- <?php if ($block->shouldRenderTotal()): ?> -+ <?php if ($block->shouldRenderTotal()) : ?> - <tfoot> - <tr class="totals" data-role="row"> -- <?php foreach ($block->getColumns() as $_column): ?> -- <th data-column="<?= /* @escapeNotVerified */ $_column->getId() ?>" -- class="<?= /* @escapeNotVerified */ $_column->getCssProperty() ?>" -+ <?php foreach ($block->getColumns() as $_column) : ?> -+ <th data-column="<?= $block->escapeHtmlAttr($_column->getId()) ?>" -+ class="<?= $block->escapeHtmlAttr($_column->getCssProperty()) ?>" - > -- <?php /* @escapeNotVerified */ echo($_column->hasTotalsLabel()) ? $_column->getTotalsLabel() -- : $_column->getRowField($block->getTotals()) ?> -+ <?= /* @noEscape */ ($_column->hasTotalsLabel()) ? $block->escapeHtml($_column->getTotalsLabel()) : $_column->getRowField($block->getTotals()) ?> - </th> - <?php endforeach; ?> - </tr> -diff --git a/app/code/Magento/Backend/view/adminhtml/templates/widget/grid/container.phtml b/app/code/Magento/Backend/view/adminhtml/templates/widget/grid/container.phtml -index 8a40853a405..6a8ec2a934c 100644 ---- a/app/code/Magento/Backend/view/adminhtml/templates/widget/grid/container.phtml -+++ b/app/code/Magento/Backend/view/adminhtml/templates/widget/grid/container.phtml -@@ -3,11 +3,8 @@ - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -- --// @codingStandardsIgnoreFile -- - ?> --<?php if ($block->getButtonsHtml()): ?> -+<?php if ($block->getButtonsHtml()) : ?> - <div data-mage-init='{"floatingHeader": {}}' class="page-actions"><?= $block->getButtonsHtml() ?></div> - <?php endif; ?> - <?= $block->getGridHtml() ?> -diff --git a/app/code/Magento/Backend/view/adminhtml/templates/widget/grid/container/empty.phtml b/app/code/Magento/Backend/view/adminhtml/templates/widget/grid/container/empty.phtml -index 700e9749f73..e62c6ca3b42 100644 ---- a/app/code/Magento/Backend/view/adminhtml/templates/widget/grid/container/empty.phtml -+++ b/app/code/Magento/Backend/view/adminhtml/templates/widget/grid/container/empty.phtml -@@ -3,8 +3,5 @@ - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -- --// @codingStandardsIgnoreFile -- - ?> - <?= $block->getGridHtml() ?> -diff --git a/app/code/Magento/Backend/view/adminhtml/templates/widget/grid/export.phtml b/app/code/Magento/Backend/view/adminhtml/templates/widget/grid/export.phtml -index 35be61dad7f..e70b93d0afc 100644 ---- a/app/code/Magento/Backend/view/adminhtml/templates/widget/grid/export.phtml -+++ b/app/code/Magento/Backend/view/adminhtml/templates/widget/grid/export.phtml -@@ -3,17 +3,14 @@ - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -- --// @codingStandardsIgnoreFile -- - ?> - <div class="admin__data-grid-export"> -- <label for="<?= /* @escapeNotVerified */ $block->getId() ?>_export" class="admin__control-support-text"> -- <?= /* @escapeNotVerified */ __('Export to:') ?> -+ <label for="<?= $block->escapeHtmlAttr($block->getId()) ?>_export" class="admin__control-support-text"> -+ <?= $block->escapeHtml(__('Export to:')) ?> - </label> -- <select name="<?= /* @escapeNotVerified */ $block->getId() ?>_export" id="<?= /* @escapeNotVerified */ $block->getId() ?>_export" class="admin__control-select"> -- <?php foreach ($block->getExportTypes() as $_type): ?> -- <option value="<?= /* @escapeNotVerified */ $_type->getUrl() ?>"><?= /* @escapeNotVerified */ $_type->getLabel() ?></option> -+ <select name="<?= $block->escapeHtmlAttr($block->getId()) ?>_export" id="<?= $block->escapeHtmlAttr($block->getId()) ?>_export" class="admin__control-select"> -+ <?php foreach ($block->getExportTypes() as $_type) : ?> -+ <option value="<?= $block->escapeHtmlAttr($_type->getUrl()) ?>"><?= $block->escapeHtml($_type->getLabel()) ?></option> - <?php endforeach; ?> - </select> - <?= $block->getExportButtonHtml() ?> -diff --git a/app/code/Magento/Backend/view/adminhtml/templates/widget/grid/extended.phtml b/app/code/Magento/Backend/view/adminhtml/templates/widget/grid/extended.phtml -index f97db4ad993..0bb453f25d7 100644 ---- a/app/code/Magento/Backend/view/adminhtml/templates/widget/grid/extended.phtml -+++ b/app/code/Magento/Backend/view/adminhtml/templates/widget/grid/extended.phtml -@@ -3,9 +3,6 @@ - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -- --// @codingStandardsIgnoreFile -- - ?> - <?php - /** -@@ -17,35 +14,35 @@ - * getPagerVisibility() - * getVarNamePage() - */ --$numColumns = sizeof($block->getColumns()); -+$numColumns = count($block->getColumns()); - - /** - * @var \Magento\Backend\Block\Widget\Grid\Extended $block - */ - ?> --<?php if ($block->getCollection()): ?> -- <?php if ($block->canDisplayContainer()): ?> -+<?php if ($block->getCollection()) : ?> -+ <?php if ($block->canDisplayContainer()) : ?> - - <div id="<?= $block->escapeHtml($block->getId()) ?>" data-grid-id="<?= $block->escapeHtml($block->getId()) ?>"> -- <?php else: ?> -+ <?php else : ?> - <?= $block->getLayout()->getMessagesBlock()->getGroupedHtml() ?> - <?php endif; ?> - <?php $massActionAvailable = $block->getMassactionBlock() && $block->getMassactionBlock()->isAvailable() ?> -- <?php if ($block->getPagerVisibility() || $block->getExportTypes() || $block->getFilterVisibility() || $massActionAvailable): ?> -+ <?php if ($block->getPagerVisibility() || $block->getExportTypes() || $block->getFilterVisibility() || $massActionAvailable) : ?> - <div class="admin__data-grid-header admin__data-grid-toolbar"> - <div class="admin__data-grid-header-row"> -- <?php if ($massActionAvailable): ?> -+ <?php if ($massActionAvailable) : ?> - <?= $block->getMainButtonsHtml() ? '<div class="admin__filter-actions">' . $block->getMainButtonsHtml() . '</div>' : '' ?> - <?php endif; ?> -- <?php if ($block->getExportTypes()): ?> -+ <?php if ($block->getExportTypes()) : ?> - <div class="admin__data-grid-export"> - <label - class="admin__control-support-text" -- for="<?= $block->escapeHtml($block->getId()) ?>_export"><?= /* @escapeNotVerified */ __('Export to:') ?></label> -+ for="<?= $block->escapeHtml($block->getId()) ?>_export"><?= $block->escapeHtml(__('Export to:')) ?></label> - <select name="<?= $block->escapeHtml($block->getId()) ?>_export" id="<?= $block->escapeHtml($block->getId()) ?>_export" - class="admin__control-select"> -- <?php foreach ($block->getExportTypes() as $_type): ?> -- <option value="<?= /* @escapeNotVerified */ $_type->getUrl() ?>"><?= /* @escapeNotVerified */ $_type->getLabel() ?></option> -+ <?php foreach ($block->getExportTypes() as $_type) : ?> -+ <option value="<?= $block->escapeHtmlAttr($_type->getUrl()) ?>"><?= $block->escapeHtml($_type->getLabel()) ?></option> - <?php endforeach; ?> - </select> - <?= $block->getExportButtonHtml() ?> -@@ -54,76 +51,76 @@ $numColumns = sizeof($block->getColumns()); - </div> - - <div class="admin__data-grid-header-row <?= $massActionAvailable ? '_massaction' : '' ?>"> -- <?php if ($massActionAvailable): ?> -+ <?php if ($massActionAvailable) : ?> - <?= $block->getMassactionBlockHtml() ?> -- <?php else: ?> -+ <?php else : ?> - <?= $block->getMainButtonsHtml() ? '<div class="admin__filter-actions">' . $block->getMainButtonsHtml() . '</div>' : '' ?> - <?php endif; ?> - <?php $countRecords = $block->getCollection()->getSize(); ?> - <div class="admin__control-support-text"> -- <span id="<?= $block->escapeHtml($block->getHtmlId()) ?>-total-count" <?= /* @escapeNotVerified */ $block->getUiId('total-count') ?>> -- <?= /* @escapeNotVerified */ $countRecords ?> -+ <span id="<?= $block->escapeHtml($block->getHtmlId()) ?>-total-count" <?= /* @noEscape */ $block->getUiId('total-count') ?>> -+ <?= /* @noEscape */ $countRecords ?> - </span> -- <?= /* @escapeNotVerified */ __('records found') ?> -+ <?= $block->escapeHtml(__('records found')) ?> - <span id="<?= $block->escapeHtml($block->getHtmlId()) ?>_massaction-count" -- class="mass-select-info _empty"><strong data-role="counter">0</strong> <span><?= /* @escapeNotVerified */ __('selected') ?></span></span> -+ class="mass-select-info _empty"><strong data-role="counter">0</strong> <span><?= $block->escapeHtml(__('selected')) ?></span></span> - </div> - -- <?php if ($block->getPagerVisibility()): ?> -+ <?php if ($block->getPagerVisibility()) : ?> - <div class="admin__data-grid-pager-wrap"> -- <select name="<?= /* @escapeNotVerified */ $block->getVarNameLimit() ?>" -+ <select name="<?= $block->escapeHtmlAttr($block->getVarNameLimit()) ?>" - id="<?= $block->escapeHtml($block->getHtmlId()) ?>_page-limit" -- onchange="<?= /* @escapeNotVerified */ $block->getJsObjectName() ?>.loadByElement(this)" -+ onchange="<?= /* @noEscape */ $block->getJsObjectName() ?>.loadByElement(this)" - class="admin__control-select"> -- <option value="20"<?php if ($block->getCollection()->getPageSize() == 20): ?> -+ <option value="20"<?php if ($block->getCollection()->getPageSize() == 20) : ?> - selected="selected"<?php endif; ?>>20 - </option> -- <option value="30"<?php if ($block->getCollection()->getPageSize() == 30): ?> -+ <option value="30"<?php if ($block->getCollection()->getPageSize() == 30) : ?> - selected="selected"<?php endif; ?>>30 - </option> -- <option value="50"<?php if ($block->getCollection()->getPageSize() == 50): ?> -+ <option value="50"<?php if ($block->getCollection()->getPageSize() == 50) : ?> - selected="selected"<?php endif; ?>>50 - </option> -- <option value="100"<?php if ($block->getCollection()->getPageSize() == 100): ?> -+ <option value="100"<?php if ($block->getCollection()->getPageSize() == 100) : ?> - selected="selected"<?php endif; ?>>100 - </option> -- <option value="200"<?php if ($block->getCollection()->getPageSize() == 200): ?> -+ <option value="200"<?php if ($block->getCollection()->getPageSize() == 200) : ?> - selected="selected"<?php endif; ?>>200 - </option> - </select> - <label for="<?= $block->escapeHtml($block->getHtmlId()) ?><?= $block->escapeHtml($block->getHtmlId()) ?>_page-limit" -- class="admin__control-support-text"><?= /* @escapeNotVerified */ __('per page') ?></label> -+ class="admin__control-support-text"><?= $block->escapeHtml(__('per page')) ?></label> - - <div class="admin__data-grid-pager"> - <?php $_curPage = $block->getCollection()->getCurPage() ?> - <?php $_lastPage = $block->getCollection()->getLastPageNumber() ?> -- <?php if ($_curPage > 1): ?> -+ <?php if ($_curPage > 1) : ?> - <button class="action-previous" - type="button" -- onclick="<?= /* @escapeNotVerified */ $block->getJsObjectName() ?>.setPage('<?= /* @escapeNotVerified */ ($_curPage - 1) ?>');return false;"> -- <span><?= /* @escapeNotVerified */ __('Previous page') ?></span> -+ onclick="<?= /* @noEscape */ $block->getJsObjectName() ?>.setPage('<?= /* @noEscape */ ($_curPage - 1) ?>');return false;"> -+ <span><?= $block->escapeHtml(__('Previous page')) ?></span> - </button> -- <?php else: ?> -- <button type="button" class="action-previous disabled"><span><?= /* @escapeNotVerified */ __('Previous page') ?></span></button> -+ <?php else : ?> -+ <button type="button" class="action-previous disabled"><span><?= $block->escapeHtml(__('Previous page')) ?></span></button> - <?php endif; ?> - <input type="text" - id="<?= $block->escapeHtml($block->getHtmlId()) ?>_page-current" -- name="<?= /* @escapeNotVerified */ $block->getVarNamePage() ?>" -- value="<?= /* @escapeNotVerified */ $_curPage ?>" -+ name="<?= $block->escapeHtmlAttr($block->getVarNamePage()) ?>" -+ value="<?= $block->escapeHtmlAttr($_curPage) ?>" - class="admin__control-text" -- onkeypress="<?= /* @escapeNotVerified */ $block->getJsObjectName() ?>.inputPage(event, '<?= /* @escapeNotVerified */ $_lastPage ?>')" <?= /* @escapeNotVerified */ $block->getUiId('current-page') ?> /> -+ onkeypress="<?= /* @noEscape */ $block->getJsObjectName() ?>.inputPage(event, '<?= /* @noEscape */ $_lastPage ?>')" <?= /* @noEscape */ $block->getUiId('current-page') ?> /> - <label class="admin__control-support-text" for="<?= $block->escapeHtml($block->getHtmlId()) ?>_page-current"> -- <?= /* @escapeNotVerified */ __('of %1', '<span>' . $block->getCollection()->getLastPageNumber() . '</span>') ?> -+ <?= /* @noEscape */ __('of %1', '<span>' . $block->getCollection()->getLastPageNumber() . '</span>') ?> - </label> -- <?php if ($_curPage < $_lastPage): ?> -+ <?php if ($_curPage < $_lastPage) : ?> - <button type="button" -- title="<?= /* @escapeNotVerified */ __('Next page') ?>" -+ title="<?= $block->escapeHtmlAttr(__('Next page')) ?>" - class="action-next" -- onclick="<?= /* @escapeNotVerified */ $block->getJsObjectName() ?>.setPage('<?= /* @escapeNotVerified */ ($_curPage + 1) ?>');return false;"> -- <span><?= /* @escapeNotVerified */ __('Next page') ?></span> -+ onclick="<?= /* @noEscape */ $block->getJsObjectName() ?>.setPage('<?= /* @noEscape */ ($_curPage + 1) ?>');return false;"> -+ <span><?= $block->escapeHtml(__('Next page')) ?></span> - </button> -- <?php else: ?> -- <button type="button" class="action-next disabled"><span><?= /* @escapeNotVerified */ __('Next page') ?></span></button> -+ <?php else : ?> -+ <button type="button" class="action-next disabled"><span><?= $block->escapeHtml(__('Next page')) ?></span></button> - <?php endif; ?> - </div> - </div> -@@ -140,25 +137,25 @@ $numColumns = sizeof($block->getColumns()); - <col <?= $_column->getHtmlProperty() ?> /> - <?php endforeach; */ - ?> -- <?php if ($block->getHeadersVisibility() || $block->getFilterVisibility()): ?> -+ <?php if ($block->getHeadersVisibility() || $block->getFilterVisibility()) : ?> - <thead> -- <?php if ($block->getHeadersVisibility()): ?> -+ <?php if ($block->getHeadersVisibility()) : ?> - <tr> -- <?php foreach ($block->getColumns() as $_column): ?> -- <?php if ($_column->getHeaderHtml() == ' '):?> -- <th class="data-grid-th" data-column="<?= /* @escapeNotVerified */ $_column->getId() ?>" -+ <?php foreach ($block->getColumns() as $_column) : ?> -+ <?php if ($_column->getHeaderHtml() == ' ') : ?> -+ <th class="data-grid-th" data-column="<?= $block->escapeHtmlAttr($_column->getId()) ?>" - <?= $_column->getHeaderHtmlProperty() ?>> </th> -- <?php else: ?> -+ <?php else : ?> - <?= $_column->getHeaderHtml() ?> - <?php endif; ?> - <?php endforeach; ?> - </tr> - <?php endif; ?> -- <?php if ($block->getFilterVisibility()): ?> -+ <?php if ($block->getFilterVisibility()) : ?> - <tr class="data-grid-filters" data-role="filter-form"> - <?php $i = 0; -- foreach ($block->getColumns() as $_column): ?> -- <td data-column="<?= /* @escapeNotVerified */ $_column->getId() ?>" <?= $_column->getHeaderHtmlProperty() ?>> -+ foreach ($block->getColumns() as $_column) : ?> -+ <td data-column="<?= $block->escapeHtmlAttr($_column->getId()) ?>" <?= $_column->getHeaderHtmlProperty() ?>> - <?= $_column->getFilterHtml() ?> - </td> - <?php endforeach; ?> -@@ -166,12 +163,12 @@ $numColumns = sizeof($block->getColumns()); - <?php endif ?> - </thead> - <?php endif; ?> -- <?php if ($block->getCountTotals()): ?> -+ <?php if ($block->getCountTotals()) : ?> - <tfoot> - <tr class="totals"> -- <?php foreach ($block->getColumns() as $_column): ?> -- <th class="<?= /* @escapeNotVerified */ $_column->getCssProperty() ?>"> -- <?= /* @escapeNotVerified */ ($_column->hasTotalsLabel()) ? $_column->getTotalsLabel() : $_column->getRowField($_column->getGrid()->getTotals()) ?> -+ <?php foreach ($block->getColumns() as $_column) : ?> -+ <th class="<?= $block->escapeHtmlAttr($_column->getCssProperty()) ?>"> -+ <?= /* @noEscape */ ($_column->hasTotalsLabel()) ? $block->escapeHtml($_column->getTotalsLabel()) : $_column->getRowField($_column->getGrid()->getTotals()) ?> - </th> - <?php endforeach; ?> - </tr> -@@ -179,116 +176,114 @@ $numColumns = sizeof($block->getColumns()); - <?php endif; ?> - - <tbody> -- <?php if (($block->getCollection()->getSize() > 0) && (!$block->getIsCollapsed())): ?> -- <?php foreach ($block->getCollection() as $_index => $_item): ?> -- <tr title="<?= /* @escapeNotVerified */ $block->getRowUrl($_item) ?>"<?php if ($_class = $block->getRowClass($_item)): ?> -- class="<?= /* @escapeNotVerified */ $_class ?>"<?php endif; ?> ><?php -+ <?php if (($block->getCollection()->getSize() > 0) && (!$block->getIsCollapsed())) : ?> -+ <?php foreach ($block->getCollection() as $_index => $_item) : ?> -+ <tr title="<?= $block->escapeHtmlAttr($block->getRowUrl($_item)) ?>"<?php if ($_class = $block->getRowClass($_item)) : ?> -+ class="<?= $block->escapeHtmlAttr($_class) ?>"<?php endif; ?> ><?php - $i = 0; -- foreach ($block->getColumns() as $_column): -- if ($block->shouldRenderCell($_item, $_column)): -+ foreach ($block->getColumns() as $_column) : -+ if ($block->shouldRenderCell($_item, $_column)) : - $_rowspan = $block->getRowspan($_item, $_column); - ?> -- <td <?= ($_rowspan ? 'rowspan="' . $_rowspan . '" ' : '') ?> -- class="<?= /* @escapeNotVerified */ $_column->getCssProperty() ?> -- <?= /* @escapeNotVerified */ $_column->getId() == 'massaction' ? 'data-grid-checkbox-cell': '' ?>"> -- <?= (($_html = $_column->getRowField($_item)) != '' ? $_html : ' ') ?> -+ <td <?= /* @noEscape */ ($_rowspan ? 'rowspan="' . $_rowspan . '" ' : '') ?> -+ class="<?= $block->escapeHtmlAttr($_column->getCssProperty()) ?> -+ <?= /* @noEscape */ $_column->getId() == 'massaction' ? 'data-grid-checkbox-cell': '' ?>"> -+ <?= /* @noEscape */ (($_html = $_column->getRowField($_item)) != '' ? $_html : ' ') ?> - </td><?php -- if ($block->shouldRenderEmptyCell($_item, $_column)): -+ if ($block->shouldRenderEmptyCell($_item, $_column)) : - ?> -- <td colspan="<?= /* @escapeNotVerified */ $block->getEmptyCellColspan($_item) ?>" -- class="last"><?= /* @escapeNotVerified */ $block->getEmptyCellLabel() ?></td><?php -+ <td colspan="<?= $block->escapeHtmlAttr($block->getEmptyCellColspan($_item)) ?>" -+ class="last"><?= $block->escapeHtml($block->getEmptyCellLabel()) ?></td><?php - endif; - endif; - endforeach; ?> - </tr> -- <?php if ($_multipleRows = $block->getMultipleRows($_item)): ?> -- <?php foreach ($_multipleRows as $_i): ?> -+ <?php if ($_multipleRows = $block->getMultipleRows($_item)) : ?> -+ <?php foreach ($_multipleRows as $_i) : ?> - <tr> - <?php $i = 0; -- foreach ($block->getMultipleRowColumns($_i) as $_column): ?> -- <td class="<?= /* @escapeNotVerified */ $_column->getCssProperty() ?> -- <?= /* @escapeNotVerified */ $_column->getId() == 'massaction' ? 'data-grid-checkbox-cell': '' ?>"> -- <?= (($_html = $_column->getRowField($_i)) != '' ? $_html : ' ') ?> -+ foreach ($block->getMultipleRowColumns($_i) as $_column) : ?> -+ <td class="<?= $block->escapeHtmlAttr($_column->getCssProperty()) ?> -+ <?= /* @noEscape */ $_column->getId() == 'massaction' ? 'data-grid-checkbox-cell': '' ?>"> -+ <?= /* @noEscape */ (($_html = $_column->getRowField($_i)) != '' ? $_html : ' ') ?> - </td> - <?php endforeach; ?> - </tr> - <?php endforeach; ?> - <?php endif; ?> - -- <?php if ($block->shouldRenderSubTotal($_item)): ?> -+ <?php if ($block->shouldRenderSubTotal($_item)) : ?> - <tr class="subtotals"> - <?php $i = 0; -- foreach ($block->getSubTotalColumns() as $_column): ?> -- <td class="<?= /* @escapeNotVerified */ $_column->getCssProperty() ?> -- <?= /* @escapeNotVerified */ $_column->getId() == 'massaction' ? 'data-grid-checkbox-cell': '' ?>"> -- <?php /* @escapeNotVerified */ echo($_column->hasSubtotalsLabel() ? $_column->getSubtotalsLabel() : -- $_column->getRowField($block->getSubTotalItem($_item)) -- ); -- ?> -+ foreach ($block->getSubTotalColumns() as $_column) : ?> -+ <td class="<?= $block->escapeHtmlAttr($_column->getCssProperty()) ?> -+ <?= /* @noEscape */ $_column->getId() == 'massaction' ? 'data-grid-checkbox-cell': '' ?>"> -+ <?= /* @noEscape */ $_column->hasSubtotalsLabel() ? $block->escapeHtml($_column->getSubtotalsLabel()) : $_column->getRowField($block->getSubTotalItem($_item)) ?> - </td> - <?php endforeach; ?> - </tr> - <?php endif; ?> - <?php endforeach; ?> -- <?php elseif ($block->getEmptyText()): ?> -+ <?php elseif ($block->getEmptyText()) : ?> - <tr class="data-grid-tr-no-data"> -- <td class="<?= /* @escapeNotVerified */ $block->getEmptyTextClass() ?>" -- colspan="<?= /* @escapeNotVerified */ $numColumns ?>"><?= /* @escapeNotVerified */ $block->getEmptyText() ?></td> -+ <td class="<?= $block->escapeHtmlAttr($block->getEmptyTextClass()) ?>" -+ colspan="<?= $block->escapeHtmlAttr($numColumns) ?>"><?= $block->escapeHtml($block->getEmptyText()) ?></td> - </tr> - <?php endif; ?> - </tbody> - </table> - - </div> -- <?php if ($block->canDisplayContainer()): ?> -+ <?php if ($block->canDisplayContainer()) : ?> - </div> - <script> - var deps = []; - -- <?php if ($block->getDependencyJsObject()): ?> -+ <?php if ($block->getDependencyJsObject()) : ?> - deps.push('uiRegistry'); -- <?php endif; ?> -+ <?php endif; ?> - -- <?php if (strpos($block->getRowClickCallback(), 'order.') !== false): ?> -+ <?php if (strpos($block->getRowClickCallback(), 'order.') !== false) : ?> - deps.push('Magento_Sales/order/create/form') -- <?php endif; ?> -+ <?php endif; ?> - - deps.push('mage/adminhtml/grid'); - -- <?php if (is_array($block->getRequireJsDependencies())): ?> -- <?php foreach ($block->getRequireJsDependencies() as $dependency): ?> -- deps.push('<?= /* @escapeNotVerified */ $dependency ?>'); -- <?php endforeach; ?> -- <?php endif; ?> -+ <?php if (is_array($block->getRequireJsDependencies())) : ?> -+ <?php foreach ($block->getRequireJsDependencies() as $dependency) : ?> -+ deps.push('<?= $block->escapeJs($dependency) ?>'); -+ <?php endforeach; ?> -+ <?php endif; ?> - - require(deps, function(<?= ($block->getDependencyJsObject() ? 'registry' : '') ?>){ - <?php //TODO: getJsObjectName and getRowClickCallback has unexpected behavior. Should be removed ?> - - //<![CDATA[ -- <?php if ($block->getDependencyJsObject()): ?> -- registry.get('<?= /* @escapeNotVerified */ $block->getDependencyJsObject() ?>', function (<?= /* @escapeNotVerified */ $block->getDependencyJsObject() ?>) { -- <?php endif; ?> -- -- <?= /* @escapeNotVerified */ $block->getJsObjectName() ?> = new varienGrid(<?= /* @noEscape */ $this->helper('Magento\Framework\Json\Helper\Data')->jsonEncode($block->getId()) ?>, '<?= /* @escapeNotVerified */ $block->getGridUrl() ?>', '<?= /* @escapeNotVerified */ $block->getVarNamePage() ?>', '<?= /* @escapeNotVerified */ $block->getVarNameSort() ?>', '<?= /* @escapeNotVerified */ $block->getVarNameDir() ?>', '<?= /* @escapeNotVerified */ $block->getVarNameFilter() ?>'); -- <?= /* @escapeNotVerified */ $block->getJsObjectName() ?>.useAjax = '<?= /* @escapeNotVerified */ $block->getUseAjax() ?>'; -- <?php if ($block->getRowClickCallback()): ?> -- <?= /* @escapeNotVerified */ $block->getJsObjectName() ?>.rowClickCallback = <?= /* @escapeNotVerified */ $block->getRowClickCallback() ?>; -- <?php endif; ?> -- <?php if ($block->getCheckboxCheckCallback()): ?> -- <?= /* @escapeNotVerified */ $block->getJsObjectName() ?>.checkboxCheckCallback = <?= /* @escapeNotVerified */ $block->getCheckboxCheckCallback() ?>; -- <?php endif; ?> -- <?php if ($block->getRowInitCallback()): ?> -- <?= /* @escapeNotVerified */ $block->getJsObjectName() ?>.initRowCallback = <?= /* @escapeNotVerified */ $block->getRowInitCallback() ?>; -- <?= /* @escapeNotVerified */ $block->getJsObjectName() ?>.initGridRows(); -- <?php endif; ?> -- <?php if ($block->getMassactionBlock() && $block->getMassactionBlock()->isAvailable()): ?> -- <?= /* @escapeNotVerified */ $block->getMassactionBlock()->getJavaScript() ?> -- <?php endif ?> -- <?= /* @escapeNotVerified */ $block->getAdditionalJavaScript() ?> -+ <?php if ($block->getDependencyJsObject()) : ?> -+ registry.get('<?= $block->escapeJs($block->getDependencyJsObject()) ?>', function (<?= $block->escapeJs($block->getDependencyJsObject()) ?>) { -+ <?php endif; ?> -+ <?php // phpcs:disable ?> -+ <?= $block->escapeJs($block->getJsObjectName()) ?> = new varienGrid(<?= /* @noEscape */ $this->helper(\Magento\Framework\Json\Helper\Data::class)->jsonEncode($block->getId()) ?>, '<?= $block->escapeJs($block->getGridUrl()) ?>', '<?= $block->escapeJs($block->getVarNamePage()) ?>', '<?= $block->escapeJs($block->getVarNameSort()) ?>', '<?= $block->escapeJs($block->getVarNameDir()) ?>', '<?= $block->escapeJs($block->getVarNameFilter()) ?>'); -+ <?php //phpcs:enable ?> -+ <?= $block->escapeJs($block->getJsObjectName()) ?>.useAjax = '<?= $block->escapeJs($block->getUseAjax()) ?>'; -+ <?php if ($block->getRowClickCallback()) : ?> -+ <?= $block->escapeJs($block->getJsObjectName()) ?>.rowClickCallback = <?= /* @noEscape */ $block->getRowClickCallback() ?>; -+ <?php endif; ?> -+ <?php if ($block->getCheckboxCheckCallback()) : ?> -+ <?= $block->escapeJs($block->getJsObjectName()) ?>.checkboxCheckCallback = <?= /* @noEscape */ $block->getCheckboxCheckCallback() ?>; -+ <?php endif; ?> -+ <?php if ($block->getRowInitCallback()) : ?> -+ <?= $block->escapeJs($block->getJsObjectName()) ?>.initRowCallback = <?= /* @noEscape */ $block->getRowInitCallback() ?>; -+ <?= $block->escapeJs($block->getJsObjectName()) ?>.initGridRows(); -+ <?php endif; ?> -+ <?php if ($block->getMassactionBlock() && $block->getMassactionBlock()->isAvailable()) : ?> -+ <?= /* @noEscape */ $block->getMassactionBlock()->getJavaScript() ?> -+ <?php endif ?> -+ <?= /* @noEscape */ $block->getAdditionalJavaScript() ?> - -- <?php if ($block->getDependencyJsObject()): ?> -+ <?php if ($block->getDependencyJsObject()) : ?> - }); -- <?php endif; ?> -+ <?php endif; ?> - //]]> - - }); -diff --git a/app/code/Magento/Backend/view/adminhtml/templates/widget/grid/massaction.phtml b/app/code/Magento/Backend/view/adminhtml/templates/widget/grid/massaction.phtml -index ea995d9a80b..9a21cd4ef71 100644 ---- a/app/code/Magento/Backend/view/adminhtml/templates/widget/grid/massaction.phtml -+++ b/app/code/Magento/Backend/view/adminhtml/templates/widget/grid/massaction.phtml -@@ -3,14 +3,11 @@ - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -- --// @codingStandardsIgnoreFile -- - ?> --<?= /* @escapeNotVerified */ $block->getSomething() ?> -+<?= /* @noEscape */ $block->getSomething() ?> - <div id="<?= $block->getHtmlId() ?>" class="admin__grid-massaction"> - -- <?php if ($block->getHideFormElement() !== true):?> -+ <?php if ($block->getHideFormElement() !== true) : ?> - <form action="" id="<?= $block->getHtmlId() ?>-form" method="post"> - <?php endif ?> - <div class="admin__grid-massaction-form"> -@@ -18,23 +15,23 @@ - <select - id="<?= $block->getHtmlId() ?>-select" - class="required-entry local-validation admin__control-select" -- <?= /* @escapeNotVerified */ $block->getUiId('select') ?>> -- <option class="admin__control-select-placeholder" value="" selected><?= /* @escapeNotVerified */ __('Actions') ?></option> -- <?php foreach ($block->getItems() as $_item):?> -- <option value="<?= /* @escapeNotVerified */ $_item->getId() ?>"<?= ($_item->getSelected() ? ' selected="selected"' : '') ?>><?= /* @escapeNotVerified */ $_item->getLabel() ?></option> -+ <?= /* @noEscape */ $block->getUiId('select') ?>> -+ <option class="admin__control-select-placeholder" value="" selected><?= $block->escapeHtml(__('Actions')) ?></option> -+ <?php foreach ($block->getItems() as $_item) : ?> -+ <option value="<?= $block->escapeHtmlAttr($_item->getId()) ?>"<?= ($_item->getSelected() ? ' selected="selected"' : '') ?>><?= $block->escapeHtml($_item->getLabel()) ?></option> - <?php endforeach; ?> - </select> - <span class="outer-span" id="<?= $block->getHtmlId() ?>-form-hiddens"></span> - <span class="outer-span" id="<?= $block->getHtmlId() ?>-form-additional"></span> - <?= $block->getApplyButtonHtml() ?> - </div> -- <?php if ($block->getHideFormElement() !== true):?> -+ <?php if ($block->getHideFormElement() !== true) :?> - </form> - <?php endif ?> - <div class="no-display"> -- <?php foreach ($block->getItems() as $_item): ?> -- <div id="<?= $block->getHtmlId() ?>-item-<?= /* @escapeNotVerified */ $_item->getId() ?>-block"> -- <?php if ('' != $_item->getBlockName()):?> -+ <?php foreach ($block->getItems() as $_item) : ?> -+ <div id="<?= $block->getHtmlId() ?>-item-<?= /* @noEscape */ $_item->getId() ?>-block"> -+ <?php if ('' != $_item->getBlockName()) :?> - <?= $block->getChildHtml($_item->getBlockName()) ?> - <?php endif;?> - </div> -@@ -47,21 +44,21 @@ - class="action-select-multiselect _disabled" - disabled="disabled" - data-menu="grid-mass-select"> -- <optgroup label="<?= /* @escapeNotVerified */ __('Mass Actions') ?>"> -+ <optgroup label="<?= $block->escapeHtmlAttr(__('Mass Actions')) ?>"> - <option disabled selected></option> -- <?php if ($block->getUseSelectAll()):?> -+ <?php if ($block->getUseSelectAll()) :?> - <option value="selectAll"> -- <?= /* @escapeNotVerified */ __('Select All') ?> -+ <?= $block->escapeHtml(__('Select All')) ?> - </option> - <option value="unselectAll"> -- <?= /* @escapeNotVerified */ __('Unselect All') ?> -+ <?= $block->escapeHtml(__('Unselect All')) ?> - </option> - <?php endif; ?> - <option value="selectVisible"> -- <?= /* @escapeNotVerified */ __('Select Visible') ?> -+ <?= $block->escapeHtml(__('Select Visible')) ?> - </option> - <option value="unselectVisible"> -- <?= /* @escapeNotVerified */ __('Unselect Visible') ?> -+ <?= $block->escapeHtml(__('Unselect Visible')) ?> - </option> - </optgroup> - </select> -@@ -78,25 +75,25 @@ - var massAction = $('option:selected', this).val(); - this.blur(); - switch (massAction) { -- <?php if ($block->getUseSelectAll()):?> -+ <?php if ($block->getUseSelectAll()) : ?> - case 'selectAll': -- return <?= /* @escapeNotVerified */ $block->getJsObjectName() ?>.selectAll(); -+ return <?= $block->escapeJs($block->getJsObjectName()) ?>.selectAll(); - break; - case 'unselectAll': -- return <?= /* @escapeNotVerified */ $block->getJsObjectName() ?>.unselectAll(); -+ return <?= $block->escapeJs($block->getJsObjectName()) ?>.unselectAll(); - break; - <?php endif; ?> - case 'selectVisible': -- return <?= /* @escapeNotVerified */ $block->getJsObjectName() ?>.selectVisible(); -+ return <?= $block->escapeJs($block->getJsObjectName()) ?>.selectVisible(); - break; - case 'unselectVisible': -- return <?= /* @escapeNotVerified */ $block->getJsObjectName() ?>.unselectVisible(); -+ return <?= $block->escapeJs($block->getJsObjectName()) ?>.unselectVisible(); - break; - } - }); - }); -- <?php if (!$block->getParentBlock()->canDisplayContainer()): ?> -- <?= /* @escapeNotVerified */ $block->getJsObjectName() ?>.setGridIds('<?= /* @escapeNotVerified */ $block->getGridIdsJson() ?>'); -+ <?php if (!$block->getParentBlock()->canDisplayContainer()) : ?> -+ <?= $block->escapeJs($block->getJsObjectName()) ?>.setGridIds('<?= $block->escapeJs($block->getGridIdsJson()) ?>'); - <?php endif; ?> - </script> - </div> -diff --git a/app/code/Magento/Backend/view/adminhtml/templates/widget/grid/massaction_extended.phtml b/app/code/Magento/Backend/view/adminhtml/templates/widget/grid/massaction_extended.phtml -index f969fa61097..c0f30fc282f 100644 ---- a/app/code/Magento/Backend/view/adminhtml/templates/widget/grid/massaction_extended.phtml -+++ b/app/code/Magento/Backend/view/adminhtml/templates/widget/grid/massaction_extended.phtml -@@ -3,13 +3,10 @@ - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -- --// @codingStandardsIgnoreFile -- - ?> - <div id="<?= $block->getHtmlId() ?>" class="admin__grid-massaction"> - -- <?php if ($block->getHideFormElement() !== true):?> -+ <?php if ($block->getHideFormElement() !== true) : ?> - <form action="" id="<?= $block->getHtmlId() ?>-form" method="post"> - <?php endif ?> - <div class="admin__grid-massaction-form"> -@@ -17,21 +14,21 @@ - <select - id="<?= $block->getHtmlId() ?>-select" - class="required-entry local-validation admin__control-select"> -- <option class="admin__control-select-placeholder" value="" selected><?= /* @escapeNotVerified */ __('Actions') ?></option> -- <?php foreach ($block->getItems() as $_item): ?> -- <option value="<?= /* @escapeNotVerified */ $_item->getId() ?>"<?= ($_item->getSelected() ? ' selected="selected"' : '') ?>><?= /* @escapeNotVerified */ $_item->getLabel() ?></option> -+ <option class="admin__control-select-placeholder" value="" selected><?= $block->escapeHtml(__('Actions')) ?></option> -+ <?php foreach ($block->getItems() as $_item) : ?> -+ <option value="<?= $block->escapeHtmlAttr($_item->getId()) ?>"<?= ($_item->getSelected() ? ' selected="selected"' : '') ?>><?= $block->escapeHtml($_item->getLabel()) ?></option> - <?php endforeach; ?> - </select> - <span class="outer-span" id="<?= $block->getHtmlId() ?>-form-hiddens"></span> - <span class="outer-span" id="<?= $block->getHtmlId() ?>-form-additional"></span> - <?= $block->getApplyButtonHtml() ?> - </div> -- <?php if ($block->getHideFormElement() !== true):?> -+ <?php if ($block->getHideFormElement() !== true) : ?> - </form> - <?php endif ?> - <div class="no-display"> -- <?php foreach ($block->getItems() as $_item): ?> -- <div id="<?= $block->getHtmlId() ?>-item-<?= /* @escapeNotVerified */ $_item->getId() ?>-block"> -+ <?php foreach ($block->getItems() as $_item) : ?> -+ <div id="<?= $block->getHtmlId() ?>-item-<?= /* @noEscape */ $_item->getId() ?>-block"> - <?= $_item->getAdditionalActionBlockHtml() ?> - </div> - <?php endforeach; ?> -@@ -41,21 +38,21 @@ - id="<?= $block->getHtmlId() ?>-mass-select" - class="action-select-multiselect" - data-menu="grid-mass-select"> -- <optgroup label="<?= /* @escapeNotVerified */ __('Mass Actions') ?>"> -+ <optgroup label="<?= $block->escapeHtml(__('Mass Actions')) ?>"> - <option disabled selected></option> -- <?php if ($block->getUseSelectAll()):?> -+ <?php if ($block->getUseSelectAll()) : ?> - <option value="selectAll"> -- <?= /* @escapeNotVerified */ __('Select All') ?> -+ <?= $block->escapeHtml(__('Select All')) ?> - </option> - <option value="unselectAll"> -- <?= /* @escapeNotVerified */ __('Unselect All') ?> -+ <?= $block->escapeHtml(__('Unselect All')) ?> - </option> - <?php endif; ?> - <option value="selectVisible"> -- <?= /* @escapeNotVerified */ __('Select Visible') ?> -+ <?= $block->escapeHtml(__('Select Visible')) ?> - </option> - <option value="unselectVisible"> -- <?= /* @escapeNotVerified */ __('Unselect Visible') ?> -+ <?= $block->escapeHtml(__('Unselect Visible')) ?> - </option> - </optgroup> - </select> -@@ -67,27 +64,27 @@ - $('#<?= $block->getHtmlId() ?>-mass-select').change(function () { - var massAction = $('option:selected', this).val(); - switch (massAction) { -- <?php if ($block->getUseSelectAll()):?> -+ <?php if ($block->getUseSelectAll()) : ?> - case 'selectAll': -- return <?= /* @escapeNotVerified */ $block->getJsObjectName() ?>.selectAll(); -+ return <?= $block->escapeJs($block->getJsObjectName()) ?>.selectAll(); - break; - case 'unselectAll': -- return <?= /* @escapeNotVerified */ $block->getJsObjectName() ?>.unselectAll(); -+ return <?= $block->escapeJs($block->getJsObjectName()) ?>.unselectAll(); - break; - <?php endif; ?> - case 'selectVisible': -- return <?= /* @escapeNotVerified */ $block->getJsObjectName() ?>.selectVisible(); -+ return <?= $block->escapeJs($block->getJsObjectName()) ?>.selectVisible(); - break; - case 'unselectVisible': -- return <?= /* @escapeNotVerified */ $block->getJsObjectName() ?>.unselectVisible(); -+ return <?= $block->escapeJs($block->getJsObjectName()) ?>.unselectVisible(); - break; - } - this.blur(); - }); - }); - -- <?php if (!$block->getParentBlock()->canDisplayContainer()): ?> -- <?= /* @escapeNotVerified */ $block->getJsObjectName() ?>.setGridIds('<?= /* @escapeNotVerified */ $block->getGridIdsJson() ?>'); -+ <?php if (!$block->getParentBlock()->canDisplayContainer()) : ?> -+ <?= $block->escapeJs($block->getJsObjectName()) ?>.setGridIds('<?= /* @noEscape */ $block->getGridIdsJson() ?>'); - <?php endif; ?> - </script> - </div> -diff --git a/app/code/Magento/Backend/view/adminhtml/templates/widget/grid/serializer.phtml b/app/code/Magento/Backend/view/adminhtml/templates/widget/grid/serializer.phtml -index 70e2a879879..2208a009295 100644 ---- a/app/code/Magento/Backend/view/adminhtml/templates/widget/grid/serializer.phtml -+++ b/app/code/Magento/Backend/view/adminhtml/templates/widget/grid/serializer.phtml -@@ -3,18 +3,18 @@ - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -- --// @codingStandardsIgnoreFile -- - ?> - <?php - /** - * @var $block \Magento\Backend\Block\Widget\Grid\Serializer - */ - ?> --<?php $_id = 'id_' . md5(microtime()) ?> -+<?php -+// phpcs:ignore -+$_id = 'id_' . md5(microtime()); -+?> - <?php $formId = $block->getFormId()?> --<?php if (!empty($formId)) :?> -+<?php if (!empty($formId)) : ?> - <script> - require([ - 'prototype', -@@ -23,11 +23,11 @@ - Event.observe(window, "load", function(){ - var serializeInput = document.createElement('input'); - serializeInput.type = 'hidden'; -- serializeInput.name = '<?= /* @escapeNotVerified */ $block->getInputElementName() ?>'; -- serializeInput.id = '<?= /* @escapeNotVerified */ $_id ?>'; -+ serializeInput.name = '<?= $block->escapeJs($block->getInputElementName()) ?>'; -+ serializeInput.id = '<?= /* @noEscape */ $_id ?>'; - try { -- document.getElementById('<?= /* @escapeNotVerified */ $formId ?>').appendChild(serializeInput); -- new serializerController('<?= /* @escapeNotVerified */ $_id ?>', <?= /* @escapeNotVerified */ $block->getDataAsJSON() ?>, <?= /* @escapeNotVerified */ $block->getColumnInputNames(true) ?>, <?= /* @escapeNotVerified */ $block->getGridBlock()->getJsObjectName() ?>, '<?= /* @escapeNotVerified */ $block->getReloadParamName() ?>'); -+ document.getElementById('<?= $block->escapeJs($formId) ?>').appendChild(serializeInput); -+ new serializerController('<?= /* @noEscape */ $_id ?>', <?= /* @noEscape */ $block->getDataAsJSON() ?>, <?= /* @noEscape */ $block->getColumnInputNames(true) ?>, <?= $block->escapeJs($block->getGridBlock()->getJsObjectName()) ?>, '<?= $block->escapeJs($block->getReloadParamName()) ?>'); - } catch(e) { - //Error add serializer - } -@@ -35,12 +35,12 @@ - }); - </script> - <?php else :?> --<input type="hidden" name="<?= /* @escapeNotVerified */ $block->getInputElementName() ?>" value="" id="<?= /* @escapeNotVerified */ $_id ?>" /> -+<input type="hidden" name="<?= $block->escapeHtmlAttr($block->getInputElementName()) ?>" value="" id="<?= /* @noEscape */ $_id ?>" /> - <script> - require([ - 'mage/adminhtml/grid' - ], function(){ -- new serializerController('<?= /* @escapeNotVerified */ $_id ?>', <?= /* @escapeNotVerified */ $block->getDataAsJSON() ?>, <?= /* @escapeNotVerified */ $block->getColumnInputNames(true) ?>, <?= /* @escapeNotVerified */ $block->getGridBlock()->getJsObjectName() ?>, '<?= /* @escapeNotVerified */ $block->getReloadParamName() ?>'); -+ new serializerController('<?= /* @noEscape */ $_id ?>', <?= /* @noEscape */ $block->getDataAsJSON() ?>, <?= /* @noEscape */ $block->getColumnInputNames(true) ?>, <?= $block->escapeJs($block->getGridBlock()->getJsObjectName()) ?>, '<?= $block->escapeJs($block->getReloadParamName()) ?>'); - }); - </script> - <?php endif;?> -diff --git a/app/code/Magento/Backend/view/adminhtml/templates/widget/tabs.phtml b/app/code/Magento/Backend/view/adminhtml/templates/widget/tabs.phtml -index 287028f9a11..5246aac088a 100644 ---- a/app/code/Magento/Backend/view/adminhtml/templates/widget/tabs.phtml -+++ b/app/code/Magento/Backend/view/adminhtml/templates/widget/tabs.phtml -@@ -4,46 +4,47 @@ - * See COPYING.txt for license details. - */ - --// @codingStandardsIgnoreFile -- - /** @var $block \Magento\Backend\Block\Widget\Tabs */ - ?> --<?php if (!empty($tabs)): ?> -+<?php if (!empty($tabs)) : ?> - --<div class="admin__page-nav" data-role="container" id="<?= /* @escapeNotVerified */ $block->getId() ?>"> -- <?php if ($block->getTitle()): ?> -- <div class="admin__page-nav-title" data-role="title" <?= /* @escapeNotVerified */ $block->getUiId('title') ?>> -- <strong><?= /* @escapeNotVerified */ $block->getTitle() ?></strong> -+<div class="admin__page-nav" data-role="container" id="<?= $block->escapeHtmlAttr($block->getId()) ?>"> -+ <?php if ($block->getTitle()) : ?> -+ <div class="admin__page-nav-title" data-role="title" <?= /* @noEscape */ $block->getUiId('title') ?>> -+ <strong><?= $block->escapeHtml($block->getTitle()) ?></strong> - <span data-role="title-messages" class="admin__page-nav-title-messages"></span> - </div> - <?php endif ?> -- <ul <?= /* @escapeNotVerified */ $block->getUiId('tab', $block->getId()) ?> class="<?= /* @escapeNotVerified */ $block->getIsHoriz() ? 'tabs-horiz' : 'tabs admin__page-nav-items' ?>"> -- <?php foreach ($tabs as $_tab): ?> -- -- <?php if (!$block->canShowTab($_tab)): continue; endif; ?> -+ <ul <?= /* @noEscape */ $block->getUiId('tab', $block->getId()) ?> class="<?= /* @noEscape */ $block->getIsHoriz() ? 'tabs-horiz' : 'tabs admin__page-nav-items' ?>"> -+ <?php foreach ($tabs as $_tab) : ?> -+ <?php -+ if (!$block->canShowTab($_tab)) : -+ continue; -+ endif; -+ ?> - <?php $_tabClass = 'tab-item-link ' . $block->getTabClass($_tab) . ' ' . (preg_match('/\s?ajax\s?/', $_tab->getClass()) ? 'notloaded' : '') ?> - <?php $_tabType = (!preg_match('/\s?ajax\s?/', $_tabClass) && $block->getTabUrl($_tab) != '#') ? 'link' : '' ?> - <?php $_tabHref = $block->getTabUrl($_tab) == '#' ? '#' . $block->getTabId($_tab) . '_content' : $block->getTabUrl($_tab) ?> - -- <li class="admin__page-nav-item" <?php if ($block->getTabIsHidden($_tab)): ?> style="display:none"<?php endif; ?><?= /* @escapeNotVerified */ $block->getUiId('tab', 'item', $_tab->getId()) ?>> -- <a href="<?= /* @escapeNotVerified */ $_tabHref ?>" id="<?= /* @escapeNotVerified */ $block->getTabId($_tab) ?>" name="<?= /* @escapeNotVerified */ $block->getTabId($_tab, false) ?>" title="<?= /* @escapeNotVerified */ $block->getTabTitle($_tab) ?>" -- class="admin__page-nav-link <?= /* @escapeNotVerified */ $_tabClass ?>" -- data-tab-type="<?= /* @escapeNotVerified */ $_tabType ?>" -- <?= /* @escapeNotVerified */ $block->getUiId('tab', 'link', $_tab->getId()) ?>> -+ <li class="admin__page-nav-item" <?php if ($block->getTabIsHidden($_tab)) : ?> style="display:none"<?php endif; ?><?= /* @noEscape */ $block->getUiId('tab', 'item', $_tab->getId()) ?>> -+ <a href="<?= $block->escapeUrl($_tabHref) ?>" id="<?= $block->escapeHtmlAttr($block->getTabId($_tab)) ?>" name="<?= $block->escapeHtmlAttr($block->getTabId($_tab, false)) ?>" title="<?= $block->escapeHtmlAttr($block->getTabTitle($_tab)) ?>" -+ class="admin__page-nav-link <?= $block->escapeHtmlAttr($_tabClass) ?>" -+ data-tab-type="<?= $block->escapeHtmlAttr($_tabType) ?>" -+ <?= /* @noEscape */ $block->getUiId('tab', 'link', $_tab->getId()) ?>> - -- <span><?= /* @escapeNotVerified */ $block->getTabLabel($_tab) ?></span> -+ <span><?= $block->escapeHtml($block->getTabLabel($_tab)) ?></span> - - <span class="admin__page-nav-item-messages" data-role="item-messages"> - <span class="admin__page-nav-item-message _changed"> - <span class="admin__page-nav-item-message-icon"></span> - <span class="admin__page-nav-item-message-tooltip"> -- <?= /* @escapeNotVerified */ __('Changes have been made to this section that have not been saved.') ?> -+ <?= $block->escapeHtml(__('Changes have been made to this section that have not been saved.')) ?> - </span> - </span> - <span class="admin__page-nav-item-message _error"> - <span class="admin__page-nav-item-message-icon"></span> - <span class="admin__page-nav-item-message-tooltip"> -- <?= /* @escapeNotVerified */ __('This tab contains invalid data. Please resolve this before saving.') ?> -+ <?= $block->escapeHtml(__('This tab contains invalid data. Please resolve this before saving.')) ?> - </span> - </span> - <span class="admin__page-nav-item-message-loader"> -@@ -54,7 +55,7 @@ - </span> - </span> - </a> -- <div id="<?= /* @escapeNotVerified */ $block->getTabId($_tab) ?>_content" style="display:none;"<?= /* @escapeNotVerified */ $block->getUiId('tab', 'content', $_tab->getId()) ?>><?= /* @escapeNotVerified */ $block->getTabContent($_tab) ?></div> -+ <div id="<?= $block->escapeHtmlAttr($block->getTabId($_tab)) ?>_content" style="display:none;"<?= /* @noEscape */ $block->getUiId('tab', 'content', $_tab->getId()) ?>><?= /* @noEscape */ $block->getTabContent($_tab) ?></div> - </li> - <?php endforeach; ?> - </ul> -@@ -63,11 +64,11 @@ - <script> - require(['jquery',"mage/backend/tabs"], function($){ - $(function() { -- $('#<?= /* @escapeNotVerified */ $block->getId() ?>').tabs({ -- active: '<?= /* @escapeNotVerified */ $block->getActiveTabId() ?>', -- destination: '#<?= /* @escapeNotVerified */ $block->getDestElementId() ?>', -- shadowTabs: <?= /* @escapeNotVerified */ $block->getAllShadowTabs() ?>, -- tabsBlockPrefix: '<?= /* @escapeNotVerified */ $block->getId() ?>_', -+ $('#<?= /* @noEscape */ $block->getId() ?>').tabs({ -+ active: '<?= /* @noEscape */ $block->getActiveTabId() ?>', -+ destination: '#<?= /* @noEscape */ $block->getDestElementId() ?>', -+ shadowTabs: <?= /* @noEscape */ $block->getAllShadowTabs() ?>, -+ tabsBlockPrefix: '<?= /* @noEscape */ $block->getId() ?>_', - tabIdArgument: 'active_tab' - }); - }); -diff --git a/app/code/Magento/Backend/view/adminhtml/templates/widget/tabshoriz.phtml b/app/code/Magento/Backend/view/adminhtml/templates/widget/tabshoriz.phtml -index 062528e7422..747dc577d23 100644 ---- a/app/code/Magento/Backend/view/adminhtml/templates/widget/tabshoriz.phtml -+++ b/app/code/Magento/Backend/view/adminhtml/templates/widget/tabshoriz.phtml -@@ -3,41 +3,38 @@ - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -- --// @codingStandardsIgnoreFile -- - ?> --<!-- <?php if ($block->getTitle()): ?> -- <h3><?= /* @escapeNotVerified */ $block->getTitle() ?></h3> -+<!-- <?php if ($block->getTitle()) : ?> -+ <h3><?= $block->escapeHtml($block->getTitle()) ?></h3> - <?php endif ?> --> --<?php if (!empty($tabs)): ?> --<div id="<?= /* @escapeNotVerified */ $block->getId() ?>"> -+<?php if (!empty($tabs)) : ?> -+<div id="<?= $block->escapeHtmlAttr($block->getId()) ?>"> - <ul class="tabs-horiz"> --<?php foreach ($tabs as $_tab): ?> -- <?php $_tabClass = 'tab-item-link ' . $block->getTabClass($_tab) . ' ' . (preg_match('/\s?ajax\s?/', $_tab->getClass()) ? 'notloaded' : '') ?> -- <?php $_tabType = (!preg_match('/\s?ajax\s?/', $_tabClass) && $block->getTabUrl($_tab) != '#') ? 'link' : '' ?> -- <?php $_tabHref = $block->getTabUrl($_tab) == '#' ? '#' . $block->getTabId($_tab) . '_content' : $block->getTabUrl($_tab) ?> -+ <?php foreach ($tabs as $_tab) : ?> -+ <?php $_tabClass = 'tab-item-link ' . $block->getTabClass($_tab) . ' ' . (preg_match('/\s?ajax\s?/', $_tab->getClass()) ? 'notloaded' : '') ?> -+ <?php $_tabType = (!preg_match('/\s?ajax\s?/', $_tabClass) && $block->getTabUrl($_tab) != '#') ? 'link' : '' ?> -+ <?php $_tabHref = $block->getTabUrl($_tab) == '#' ? '#' . $block->getTabId($_tab) . '_content' : $block->getTabUrl($_tab) ?> - <li> -- <a href="<?= /* @escapeNotVerified */ $_tabHref ?>" id="<?= /* @escapeNotVerified */ $block->getTabId($_tab) ?>" title="<?= /* @escapeNotVerified */ $block->getTabTitle($_tab) ?>" class="<?php $_tabClass ?>" data-tab-type="<?php $_tabType ?>"> -+ <a href="<?= $block->escapeHtmlAttr($_tabHref) ?>" id="<?= $block->escapeHtmlAttr($block->getTabId($_tab)) ?>" title="<?= $block->escapeHtmlAttr($block->getTabTitle($_tab)) ?>" class="<?= $block->escapeHtmlAttr($_tabClass) ?>" data-tab-type="<?= $block->escapeHtmlAttr($_tabType) ?>"> - <span> -- <span class="changed" title="<?= /* @escapeNotVerified */ __('The information in this tab has been changed.') ?>"></span> -- <span class="error" title="<?= /* @escapeNotVerified */ __('This tab contains invalid data. Please resolve this before saving.') ?>"></span> -- <span class="loader" title="<?= /* @escapeNotVerified */ __('Loading...') ?>"></span> -- <?= /* @escapeNotVerified */ $block->getTabLabel($_tab) ?> -+ <span class="changed" title="<?= $block->escapeHtmlAttr(__('The information in this tab has been changed.')) ?>"></span> -+ <span class="error" title="<?= $block->escapeHtmlAttr(__('This tab contains invalid data. Please resolve this before saving.')) ?>"></span> -+ <span class="loader" title="<?= $block->escapeHtmlAttr(__('Loading...')) ?>"></span> -+ <?= $block->escapeHtml($block->getTabLabel($_tab)) ?> - </span> - </a> -- <div id="<?= /* @escapeNotVerified */ $block->getTabId($_tab) ?>_content" style="display:none"><?= /* @escapeNotVerified */ $block->getTabContent($_tab) ?></div> -+ <div id="<?= $block->escapeHtmlAttr($block->getTabId($_tab)) ?>_content" style="display:none"><?= /* @noEscape */ $block->getTabContent($_tab) ?></div> - </li> --<?php endforeach; ?> -+ <?php endforeach; ?> - </ul> - </div> - <script> - require(["jquery","mage/backend/tabs"], function($){ - $(function() { -- $('#<?= /* @escapeNotVerified */ $block->getId() ?>').tabs({ -- active: '<?= /* @escapeNotVerified */ $block->getActiveTabId() ?>', -- destination: '#<?= /* @escapeNotVerified */ $block->getDestElementId() ?>', -- shadowTabs: <?= /* @escapeNotVerified */ $block->getAllShadowTabs() ?> -+ $('#<?= /* @noEscape */ $block->getId() ?>').tabs({ -+ active: '<?= /* @noEscape */ $block->getActiveTabId() ?>', -+ destination: '#<?= /* @noEscape */ $block->getDestElementId() ?>', -+ shadowTabs: <?= /* @noEscape */ $block->getAllShadowTabs() ?> - }); - }); - }); -diff --git a/app/code/Magento/Backend/view/adminhtml/templates/widget/tabsleft.phtml b/app/code/Magento/Backend/view/adminhtml/templates/widget/tabsleft.phtml -index 6af34ca502f..4f69191e797 100644 ---- a/app/code/Magento/Backend/view/adminhtml/templates/widget/tabsleft.phtml -+++ b/app/code/Magento/Backend/view/adminhtml/templates/widget/tabsleft.phtml -@@ -3,21 +3,18 @@ - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -- --// @codingStandardsIgnoreFile -- - ?> --<dl id="dl-<?= /* @escapeNotVerified */ $id ?>" class="accordion"> --<?php foreach ($sections as $sectionId => $section): ?> -- <dt id="dt-<?= /* @escapeNotVerified */ $id ?>-<?= /* @escapeNotVerified */ $sectionId ?>"> -- <strong><?= /* @escapeNotVerified */ $section['title'] ?></strong> -+<dl id="dl-<?= /* @noEscape */ $id ?>" class="accordion"> -+<?php foreach ($sections as $sectionId => $section) : ?> -+ <dt id="dt-<?= /* @noEscape */ $id ?>-<?= /* @noEscape */ $sectionId ?>"> -+ <strong><?= $block->escapeHtml($section['title']) ?></strong> - </dt> -- <dd id="dd-<?= /* @escapeNotVerified */ $id ?>-<?= /* @escapeNotVerified */ $section['id'] ?>" class="section-menu <?= !empty($section['active']) ? 'open' : '' ?>"> -+ <dd id="dd-<?= /* @noEscape */ $id ?>-<?= /* @noEscape */ $section['id'] ?>" class="section-menu <?= !empty($section['active']) ? 'open' : '' ?>"> - <ul> -- <?php foreach ($section['children'] as $menuId => $menuItem): ?> -- <li id="li-<?= /* @escapeNotVerified */ $id ?>-<?= /* @escapeNotVerified */ $sectionId ?>-<?= /* @escapeNotVerified */ $menuId ?>"> -- <a href="#" title="<?= /* @escapeNotVerified */ $menuItem['title'] ?>"> -- <span><?= /* @escapeNotVerified */ $menuItem['label'] ?></span> -+ <?php foreach ($section['children'] as $menuId => $menuItem) : ?> -+ <li id="li-<?= /* @noEscape */ $id ?>-<?= /* @noEscape */ $sectionId ?>-<?= /* @noEscape */ $menuId ?>"> -+ <a href="#" title="<?= $block->escapeHtmlAttr($menuItem['title']) ?>"> -+ <span><?= $block->escapeHtml($menuItem['label']) ?></span> - </a> - </li> - <?php endforeach ?> -diff --git a/app/code/Magento/Backend/view/adminhtml/templates/widget/view/container.phtml b/app/code/Magento/Backend/view/adminhtml/templates/widget/view/container.phtml -index f29e8fd8624..a0d56911ae2 100644 ---- a/app/code/Magento/Backend/view/adminhtml/templates/widget/view/container.phtml -+++ b/app/code/Magento/Backend/view/adminhtml/templates/widget/view/container.phtml -@@ -3,9 +3,6 @@ - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -- --// @codingStandardsIgnoreFile -- - ?> - <div data-mage-init='{"floatingHeader": {}}' class="page-actions"><?= $block->getButtonsHtml() ?></div> - <?= $block->getViewHtml() ?> -diff --git a/app/code/Magento/Backend/view/adminhtml/ui_component/design_config_listing.xml b/app/code/Magento/Backend/view/adminhtml/ui_component/design_config_listing.xml -index 93309c9a22e..b0abec3aa9b 100644 ---- a/app/code/Magento/Backend/view/adminhtml/ui_component/design_config_listing.xml -+++ b/app/code/Magento/Backend/view/adminhtml/ui_component/design_config_listing.xml -@@ -6,6 +6,7 @@ - */ - --> - <listing xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Ui:etc/ui_configuration.xsd"> -+ <listingToolbar name="listing_top" /> - <columns name="design_config_columns"> - <column name="theme_theme_id" component="Magento_Ui/js/grid/columns/select" sortOrder="40"> - <settings> -diff --git a/app/code/Magento/Backend/view/adminhtml/web/js/media-uploader.js b/app/code/Magento/Backend/view/adminhtml/web/js/media-uploader.js -index f9f43cebc59..119e7a35747 100644 ---- a/app/code/Magento/Backend/view/adminhtml/web/js/media-uploader.js -+++ b/app/code/Magento/Backend/view/adminhtml/web/js/media-uploader.js -@@ -33,9 +33,20 @@ define([ - * @private - */ - _create: function () { -- var -- self = this, -- progressTmpl = mageTemplate('[data-template="uploader"]'); -+ var self = this, -+ progressTmpl = mageTemplate('[data-template="uploader"]'), -+ isResizeEnabled = this.options.isResizeEnabled, -+ resizeConfiguration = { -+ action: 'resize', -+ maxWidth: this.options.maxWidth, -+ maxHeight: this.options.maxHeight -+ }; -+ -+ if (!isResizeEnabled) { -+ resizeConfiguration = { -+ action: 'resize' -+ }; -+ } - - this.element.find('input[type=file]').fileupload({ - dataType: 'json', -@@ -52,8 +63,7 @@ define([ - * @param {Object} data - */ - add: function (e, data) { -- var -- fileSize, -+ var fileSize, - tmpl; - - $.each(data.files, function (index, file) { -@@ -124,11 +134,9 @@ define([ - process: [{ - action: 'load', - fileTypes: /^image\/(gif|jpeg|png)$/ -- }, { -- action: 'resize', -- maxWidth: this.options.maxWidth, -- maxHeight: this.options.maxHeight -- }, { -+ }, -+ resizeConfiguration, -+ { - action: 'save' - }] - }); -diff --git a/app/code/Magento/Backend/view/adminhtml/web/js/validate-store.js b/app/code/Magento/Backend/view/adminhtml/web/js/validate-store.js -index 0a692a9b868..c2a0d4dab1f 100644 ---- a/app/code/Magento/Backend/view/adminhtml/web/js/validate-store.js -+++ b/app/code/Magento/Backend/view/adminhtml/web/js/validate-store.js -@@ -67,6 +67,7 @@ define([ - * 'Confirm' action handler. - */ - confirm: function () { -+ $('body').trigger('processStart'); - dataPost().postData(requestData); - } - } -diff --git a/app/code/Magento/Backend/view/adminhtml/web/template/dynamic-rows/grid.html b/app/code/Magento/Backend/view/adminhtml/web/template/dynamic-rows/grid.html -index fe30ca7e83f..0033f4c071e 100644 ---- a/app/code/Magento/Backend/view/adminhtml/web/template/dynamic-rows/grid.html -+++ b/app/code/Magento/Backend/view/adminhtml/web/template/dynamic-rows/grid.html -@@ -69,7 +69,7 @@ - - <!-- ko foreach: { data: $record().elems(), as: 'elem'} --> - <td if="elem.template" -- visible="elem.visible" -+ visible="elem.visible() && elem.formElement !== 'hidden'" - disable="elem.disabled" - css="$parent.setClasses(elem)" - template="elem.template" -diff --git a/app/code/Magento/Backup/Controller/Adminhtml/Index.php b/app/code/Magento/Backup/Controller/Adminhtml/Index.php -index dcafbc7370d..b62963947d7 100644 ---- a/app/code/Magento/Backup/Controller/Adminhtml/Index.php -+++ b/app/code/Magento/Backup/Controller/Adminhtml/Index.php -@@ -5,21 +5,26 @@ - */ - namespace Magento\Backup\Controller\Adminhtml; - -+use Magento\Backend\App\Action; -+use Magento\Backup\Helper\Data as Helper; -+use Magento\Framework\App\ObjectManager; -+ - /** - * Backup admin controller - * - * @author Magento Core Team <core@magentocommerce.com> - * @api - * @since 100.0.2 -+ * @SuppressWarnings(PHPMD.AllPurposeAction) - */ --abstract class Index extends \Magento\Backend\App\Action -+abstract class Index extends Action - { - /** - * Authorization level of a basic admin session - * - * @see _isAllowed() - */ -- const ADMIN_RESOURCE = 'Magento_Backend::backup'; -+ const ADMIN_RESOURCE = 'Magento_Backup::backup'; - - /** - * Core registry -@@ -48,6 +53,11 @@ abstract class Index extends \Magento\Backend\App\Action - */ - protected $maintenanceMode; - -+ /** -+ * @var Helper -+ */ -+ private $helper; -+ - /** - * @param \Magento\Backend\App\Action\Context $context - * @param \Magento\Framework\Registry $coreRegistry -@@ -55,6 +65,7 @@ abstract class Index extends \Magento\Backend\App\Action - * @param \Magento\Framework\App\Response\Http\FileFactory $fileFactory - * @param \Magento\Backup\Model\BackupFactory $backupModelFactory - * @param \Magento\Framework\App\MaintenanceMode $maintenanceMode -+ * @param Helper|null $helper - */ - public function __construct( - \Magento\Backend\App\Action\Context $context, -@@ -62,13 +73,27 @@ abstract class Index extends \Magento\Backend\App\Action - \Magento\Framework\Backup\Factory $backupFactory, - \Magento\Framework\App\Response\Http\FileFactory $fileFactory, - \Magento\Backup\Model\BackupFactory $backupModelFactory, -- \Magento\Framework\App\MaintenanceMode $maintenanceMode -+ \Magento\Framework\App\MaintenanceMode $maintenanceMode, -+ ?Helper $helper = null - ) { - $this->_coreRegistry = $coreRegistry; - $this->_backupFactory = $backupFactory; - $this->_fileFactory = $fileFactory; - $this->_backupModelFactory = $backupModelFactory; - $this->maintenanceMode = $maintenanceMode; -+ $this->helper = $helper ?? ObjectManager::getInstance()->get(Helper::class); - parent::__construct($context); - } -+ -+ /** -+ * @inheritDoc -+ */ -+ public function dispatch(\Magento\Framework\App\RequestInterface $request) -+ { -+ if (!$this->helper->isEnabled()) { -+ return $this->_redirect('*/*/disabled'); -+ } -+ -+ return parent::dispatch($request); -+ } - } -diff --git a/app/code/Magento/Backup/Controller/Adminhtml/Index/Create.php b/app/code/Magento/Backup/Controller/Adminhtml/Index/Create.php -index 27770182a6d..ee5a56e8148 100644 ---- a/app/code/Magento/Backup/Controller/Adminhtml/Index/Create.php -+++ b/app/code/Magento/Backup/Controller/Adminhtml/Index/Create.php -@@ -1,15 +1,18 @@ - <?php - /** -- * - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ - namespace Magento\Backup\Controller\Adminhtml\Index; - -+use Magento\Framework\App\Action\HttpPostActionInterface; - use Magento\Framework\App\Filesystem\DirectoryList; - use Magento\Framework\Filesystem; - --class Create extends \Magento\Backup\Controller\Adminhtml\Index -+/** -+ * Create backup controller -+ */ -+class Create extends \Magento\Backup\Controller\Adminhtml\Index implements HttpPostActionInterface - { - /** - * Create backup action. -@@ -55,7 +58,9 @@ class Create extends \Magento\Backup\Controller\Adminhtml\Index - $this->_coreRegistry->register('backup_manager', $backupManager); - - if ($this->getRequest()->getParam('maintenance_mode')) { -- if (!$this->maintenanceMode->set(true)) { -+ $this->maintenanceMode->set(true); -+ -+ if (!$this->maintenanceMode->isOn()) { - $response->setError( - __( - 'You need more permissions to activate maintenance mode right now.' -@@ -82,7 +87,7 @@ class Create extends \Magento\Backup\Controller\Adminhtml\Index - - $backupManager->create(); - -- $this->messageManager->addSuccess($successMessage); -+ $this->messageManager->addSuccessMessage($successMessage); - - $response->setRedirectUrl($this->getUrl('*/*/index')); - } catch (\Magento\Framework\Backup\Exception\NotEnoughFreeSpace $e) { -diff --git a/app/code/Magento/Backup/Controller/Adminhtml/Index/Disabled.php b/app/code/Magento/Backup/Controller/Adminhtml/Index/Disabled.php -new file mode 100644 -index 00000000000..f6fe430ae08 ---- /dev/null -+++ b/app/code/Magento/Backup/Controller/Adminhtml/Index/Disabled.php -@@ -0,0 +1,48 @@ -+<?php -+/** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+ -+declare(strict_types=1); -+ -+namespace Magento\Backup\Controller\Adminhtml\Index; -+ -+use Magento\Backend\App\Action; -+use Magento\Backend\App\Action\Context; -+use Magento\Framework\App\Action\HttpGetActionInterface; -+use Magento\Framework\View\Result\PageFactory; -+ -+/** -+ * Inform that backup is disabled. -+ */ -+class Disabled extends Action implements HttpGetActionInterface -+{ -+ /** -+ * @see _isAllowed() -+ */ -+ const ADMIN_RESOURCE = 'Magento_Backend::backup'; -+ -+ /** -+ * @var PageFactory -+ */ -+ private $pageFactory; -+ -+ /** -+ * @param Context $context -+ * @param PageFactory $pageFactory -+ */ -+ public function __construct(Context $context, PageFactory $pageFactory) -+ { -+ parent::__construct($context); -+ $this->pageFactory = $pageFactory; -+ } -+ -+ /** -+ * @inheritDoc -+ */ -+ public function execute() -+ { -+ return $this->pageFactory->create(); -+ } -+} -diff --git a/app/code/Magento/Backup/Controller/Adminhtml/Index/Index.php b/app/code/Magento/Backup/Controller/Adminhtml/Index/Index.php -index 3bbda65cb4c..271e3713034 100644 ---- a/app/code/Magento/Backup/Controller/Adminhtml/Index/Index.php -+++ b/app/code/Magento/Backup/Controller/Adminhtml/Index/Index.php -@@ -6,7 +6,9 @@ - */ - namespace Magento\Backup\Controller\Adminhtml\Index; - --class Index extends \Magento\Backup\Controller\Adminhtml\Index -+use Magento\Framework\App\Action\HttpGetActionInterface as HttpGetActionInterface; -+ -+class Index extends \Magento\Backup\Controller\Adminhtml\Index implements HttpGetActionInterface - { - /** - * Backup list action -diff --git a/app/code/Magento/Backup/Controller/Adminhtml/Index/MassDelete.php b/app/code/Magento/Backup/Controller/Adminhtml/Index/MassDelete.php -index 04292d27590..90657fc2490 100644 ---- a/app/code/Magento/Backup/Controller/Adminhtml/Index/MassDelete.php -+++ b/app/code/Magento/Backup/Controller/Adminhtml/Index/MassDelete.php -@@ -49,13 +49,13 @@ class MassDelete extends \Magento\Backup\Controller\Adminhtml\Index - - $resultData->setIsSuccess(true); - if ($allBackupsDeleted) { -- $this->messageManager->addSuccess(__('You deleted the selected backup(s).')); -+ $this->messageManager->addSuccessMessage(__('You deleted the selected backup(s).')); - } else { - throw new \Exception($deleteFailMessage); - } - } catch (\Exception $e) { - $resultData->setIsSuccess(false); -- $this->messageManager->addError($deleteFailMessage); -+ $this->messageManager->addErrorMessage($deleteFailMessage); - } - - return $this->_redirect('backup/*/index'); -diff --git a/app/code/Magento/Backup/Controller/Adminhtml/Index/Rollback.php b/app/code/Magento/Backup/Controller/Adminhtml/Index/Rollback.php -index 0451f6ed09b..7f450e7e313 100644 ---- a/app/code/Magento/Backup/Controller/Adminhtml/Index/Rollback.php -+++ b/app/code/Magento/Backup/Controller/Adminhtml/Index/Rollback.php -@@ -6,13 +6,16 @@ - */ - namespace Magento\Backup\Controller\Adminhtml\Index; - -+use Magento\Framework\App\Action\HttpPostActionInterface; - use Magento\Framework\App\Filesystem\DirectoryList; - use Magento\Framework\Filesystem; - - /** -+ * Backup rollback controller. -+ * - * @SuppressWarnings(PHPMD.CouplingBetweenObjects) - */ --class Rollback extends \Magento\Backup\Controller\Adminhtml\Index -+class Rollback extends \Magento\Backup\Controller\Adminhtml\Index implements HttpPostActionInterface - { - /** - * Rollback Action -@@ -82,7 +85,9 @@ class Rollback extends \Magento\Backup\Controller\Adminhtml\Index - } - - if ($this->getRequest()->getParam('maintenance_mode')) { -- if (!$this->maintenanceMode->set(true)) { -+ $this->maintenanceMode->set(true); -+ -+ if (!$this->maintenanceMode->isOn()) { - $response->setError( - __( - 'You need more permissions to activate maintenance mode right now.' -@@ -122,6 +127,7 @@ class Rollback extends \Magento\Backup\Controller\Adminhtml\Index - $adminSession->destroy(); - - $response->setRedirectUrl($this->getUrl('*')); -+ // phpcs:disable Magento2.Exceptions.ThrowCatch - } catch (\Magento\Framework\Backup\Exception\CantLoadSnapshot $e) { - $errorMsg = __('We can\'t find the backup file.'); - } catch (\Magento\Framework\Backup\Exception\FtpConnectionFailed $e) { -diff --git a/app/code/Magento/Backup/Cron/SystemBackup.php b/app/code/Magento/Backup/Cron/SystemBackup.php -index 750262ab1c1..9502377a39d 100644 ---- a/app/code/Magento/Backup/Cron/SystemBackup.php -+++ b/app/code/Magento/Backup/Cron/SystemBackup.php -@@ -8,6 +8,9 @@ namespace Magento\Backup\Cron; - use Magento\Framework\App\Filesystem\DirectoryList; - use Magento\Store\Model\ScopeInterface; - -+/** -+ * Performs scheduled backup. -+ */ - class SystemBackup - { - const XML_PATH_BACKUP_ENABLED = 'system/backup/enabled'; -@@ -101,6 +104,10 @@ class SystemBackup - */ - public function execute() - { -+ if (!$this->_backupData->isEnabled()) { -+ return $this; -+ } -+ - if (!$this->_scopeConfig->isSetFlag(self::XML_PATH_BACKUP_ENABLED, ScopeInterface::SCOPE_STORE)) { - return $this; - } -diff --git a/app/code/Magento/Backup/Helper/Data.php b/app/code/Magento/Backup/Helper/Data.php -index 3d60bf9d9c9..c6df6a73668 100644 ---- a/app/code/Magento/Backup/Helper/Data.php -+++ b/app/code/Magento/Backup/Helper/Data.php -@@ -3,6 +3,9 @@ - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -+ -+declare(strict_types=1); -+ - namespace Magento\Backup\Helper; - - use Magento\Framework\App\Filesystem\DirectoryList; -@@ -110,7 +113,7 @@ class Data extends \Magento\Framework\App\Helper\AbstractHelper - public function getExtensionByType($type) - { - $extensions = $this->getExtensions(); -- return isset($extensions[$type]) ? $extensions[$type] : ''; -+ return $extensions[$type] ?? ''; - } - - /** -@@ -285,4 +288,14 @@ class Data extends \Magento\Framework\App\Helper\AbstractHelper - - return $result; - } -+ -+ /** -+ * Is backup functionality enabled. -+ * -+ * @return bool -+ */ -+ public function isEnabled(): bool -+ { -+ return $this->scopeConfig->isSetFlag('system/backup/functionality_enabled'); -+ } - } -diff --git a/app/code/Magento/Backup/Model/Backup.php b/app/code/Magento/Backup/Model/Backup.php -index 3768f2bf8c8..c3507ecf5b4 100644 ---- a/app/code/Magento/Backup/Model/Backup.php -+++ b/app/code/Magento/Backup/Model/Backup.php -@@ -14,6 +14,7 @@ use Magento\Framework\Filesystem\DriverPool; - * @method string getPath() - * @method string getName() - * @method string getTime() -+ * @SuppressWarnings(PHPMD.CookieAndSessionMisuse) - * @SuppressWarnings(PHPMD.CouplingBetweenObjects) - * @api - * @since 100.0.2 -@@ -80,6 +81,7 @@ class Backup extends \Magento\Framework\DataObject implements \Magento\Framework - * @param \Magento\Framework\Encryption\EncryptorInterface $encryptor - * @param \Magento\Framework\Filesystem $filesystem - * @param array $data -+ * @throws \Magento\Framework\Exception\FileSystemException - */ - public function __construct( - \Magento\Backup\Helper\Data $helper, -@@ -242,7 +244,7 @@ class Backup extends \Magento\Framework\DataObject implements \Magento\Framework - /** - * Return content of backup file - * -- * @return string -+ * @return array - * @throws \Magento\Framework\Exception\LocalizedException - */ - public function &getFile() -@@ -275,8 +277,9 @@ class Backup extends \Magento\Framework\DataObject implements \Magento\Framework - * - * @param bool $write - * @return $this -- * @throws \Magento\Framework\Exception\InputException - * @throws \Magento\Framework\Backup\Exception\NotEnoughPermissions -+ * @throws \Magento\Framework\Exception\FileSystemException -+ * @throws \Magento\Framework\Exception\InputException - */ - public function open($write = false) - { -@@ -330,6 +333,7 @@ class Backup extends \Magento\Framework\DataObject implements \Magento\Framework - * - * @param int $length - * @return string -+ * @throws \Magento\Framework\Exception\InputException - */ - public function read($length) - { -@@ -340,6 +344,7 @@ class Backup extends \Magento\Framework\DataObject implements \Magento\Framework - * Check end of file. - * - * @return bool -+ * @throws \Magento\Framework\Exception\InputException - */ - public function eof() - { -@@ -370,6 +375,7 @@ class Backup extends \Magento\Framework\DataObject implements \Magento\Framework - * Close open backup file - * - * @return $this -+ * @throws \Magento\Framework\Exception\InputException - */ - public function close() - { -@@ -383,6 +389,8 @@ class Backup extends \Magento\Framework\DataObject implements \Magento\Framework - * Print output - * - * @return string -+ * @return \Magento\Framework\Filesystem\Directory\ReadInterface|string|void -+ * @throws \Magento\Framework\Exception\FileSystemException - */ - public function output() - { -@@ -398,6 +406,8 @@ class Backup extends \Magento\Framework\DataObject implements \Magento\Framework - } - - /** -+ * Get Size -+ * - * @return int|mixed - */ - public function getSize() -@@ -419,6 +429,7 @@ class Backup extends \Magento\Framework\DataObject implements \Magento\Framework - * - * @param string $password - * @return bool -+ * @throws \Exception - */ - public function validateUserPassword($password) - { -diff --git a/app/code/Magento/Backup/Model/BackupFactory.php b/app/code/Magento/Backup/Model/BackupFactory.php -index 28b0a7baf43..f88b410a237 100644 ---- a/app/code/Magento/Backup/Model/BackupFactory.php -+++ b/app/code/Magento/Backup/Model/BackupFactory.php -@@ -39,8 +39,8 @@ class BackupFactory - */ - public function create($timestamp, $type) - { -- $fsCollection = $this->_objectManager->get(\Magento\Backup\Model\Fs\Collection::class); -- $backupInstance = $this->_objectManager->get(\Magento\Backup\Model\Backup::class); -+ $fsCollection = $this->_objectManager->create(\Magento\Backup\Model\Fs\Collection::class); -+ $backupInstance = $this->_objectManager->create(\Magento\Backup\Model\Backup::class); - - foreach ($fsCollection as $backup) { - if ($backup->getTime() === (int) $timestamp && $backup->getType() === $type) { -diff --git a/app/code/Magento/Backup/Model/Config/Backend/Cron.php b/app/code/Magento/Backup/Model/Config/Backend/Cron.php -index 4855ef11295..2f0e4069f04 100644 ---- a/app/code/Magento/Backup/Model/Config/Backend/Cron.php -+++ b/app/code/Magento/Backup/Model/Config/Backend/Cron.php -@@ -76,8 +76,8 @@ class Cron extends \Magento\Framework\App\Config\Value - - if ($enabled) { - $cronExprArray = [ -- intval($time[1]), # Minute -- intval($time[0]), # Hour -+ (int) $time[1], # Minute -+ (int) $time[0], # Hour - $frequency == $frequencyMonthly ? '1' : '*', # Day of the Month - '*', # Month of the Year - $frequency == $frequencyWeekly ? '1' : '*', # Day of the Week -diff --git a/app/code/Magento/Backup/Model/Db.php b/app/code/Magento/Backup/Model/Db.php -index 8fbd5da1c98..084b35448a8 100644 ---- a/app/code/Magento/Backup/Model/Db.php -+++ b/app/code/Magento/Backup/Model/Db.php -@@ -5,11 +5,18 @@ - */ - namespace Magento\Backup\Model; - -+use Magento\Backup\Helper\Data as Helper; -+use Magento\Backup\Model\ResourceModel\Table\GetListTables; -+use Magento\Backup\Model\ResourceModel\View\CreateViewsBackup; -+use Magento\Framework\App\ObjectManager; -+use Magento\Framework\Exception\RuntimeException; -+ - /** - * Database backup model - * - * @api - * @since 100.0.2 -+ * @deprecated Backup module is to be removed. - */ - class Db implements \Magento\Framework\Backup\Db\BackupDbInterface - { -@@ -34,15 +41,40 @@ class Db implements \Magento\Framework\Backup\Db\BackupDbInterface - protected $_resource = null; - - /** -- * @param \Magento\Backup\Model\ResourceModel\Db $resourceDb -+ * @var Helper -+ */ -+ private $helper; -+ -+ /** -+ * @var GetListTables -+ */ -+ private $getListTables; -+ -+ /** -+ * @var CreateViewsBackup -+ */ -+ private $getViewsBackup; -+ -+ /** -+ * Db constructor. -+ * @param ResourceModel\Db $resourceDb - * @param \Magento\Framework\App\ResourceConnection $resource -+ * @param Helper|null $helper -+ * @param GetListTables|null $getListTables -+ * @param CreateViewsBackup|null $getViewsBackup - */ - public function __construct( -- \Magento\Backup\Model\ResourceModel\Db $resourceDb, -- \Magento\Framework\App\ResourceConnection $resource -+ ResourceModel\Db $resourceDb, -+ \Magento\Framework\App\ResourceConnection $resource, -+ ?Helper $helper = null, -+ ?GetListTables $getListTables = null, -+ ?CreateViewsBackup $getViewsBackup = null - ) { - $this->_resourceDb = $resourceDb; - $this->_resource = $resource; -+ $this->helper = $helper ?? ObjectManager::getInstance()->get(Helper::class); -+ $this->getListTables = $getListTables ?? ObjectManager::getInstance()->get(GetListTables::class); -+ $this->getViewsBackup = $getViewsBackup ?? ObjectManager::getInstance()->get(CreateViewsBackup::class); - } - - /** -@@ -63,6 +95,8 @@ class Db implements \Magento\Framework\Backup\Db\BackupDbInterface - } - - /** -+ * Tables list. -+ * - * @return array - */ - public function getTables() -@@ -71,6 +105,8 @@ class Db implements \Magento\Framework\Backup\Db\BackupDbInterface - } - - /** -+ * Command to recreate given table. -+ * - * @param string $tableName - * @param bool $addDropIfExists - * @return string -@@ -81,6 +117,8 @@ class Db implements \Magento\Framework\Backup\Db\BackupDbInterface - } - - /** -+ * Generate table's data dump. -+ * - * @param string $tableName - * @return string - */ -@@ -90,6 +128,8 @@ class Db implements \Magento\Framework\Backup\Db\BackupDbInterface - } - - /** -+ * Header for dumps. -+ * - * @return string - */ - public function getHeader() -@@ -98,6 +138,8 @@ class Db implements \Magento\Framework\Backup\Db\BackupDbInterface - } - - /** -+ * Footer for dumps. -+ * - * @return string - */ - public function getFooter() -@@ -106,6 +148,8 @@ class Db implements \Magento\Framework\Backup\Db\BackupDbInterface - } - - /** -+ * Get backup SQL. -+ * - * @return string - */ - public function renderSql() -@@ -124,18 +168,19 @@ class Db implements \Magento\Framework\Backup\Db\BackupDbInterface - } - - /** -- * Create backup and stream write to adapter -- * -- * @param \Magento\Framework\Backup\Db\BackupInterface $backup -- * @return $this -+ * @inheritDoc - */ - public function createBackup(\Magento\Framework\Backup\Db\BackupInterface $backup) - { -+ if (!$this->helper->isEnabled()) { -+ throw new RuntimeException(__('Backup functionality is disabled')); -+ } -+ - $backup->open(true); - - $this->getResource()->beginTransaction(); - -- $tables = $this->getResource()->getTables(); -+ $tables = $this->getListTables->execute(); - - $backup->write($this->getResource()->getHeader()); - -@@ -172,6 +217,8 @@ class Db implements \Magento\Framework\Backup\Db\BackupDbInterface - $backup->write($this->getResource()->getTableDataAfterSql($table)); - } - } -+ $this->getViewsBackup->execute($backup); -+ - $backup->write($this->getResource()->getTableForeignKeysSql()); - $backup->write($this->getResource()->getTableTriggersSql()); - $backup->write($this->getResource()->getFooter()); -@@ -179,8 +226,6 @@ class Db implements \Magento\Framework\Backup\Db\BackupDbInterface - $this->getResource()->commitTransaction(); - - $backup->close(); -- -- return $this; - } - - /** -diff --git a/app/code/Magento/Backup/Model/Fs/Collection.php b/app/code/Magento/Backup/Model/Fs/Collection.php -index 2d08ac04528..b17c17f7074 100644 ---- a/app/code/Magento/Backup/Model/Fs/Collection.php -+++ b/app/code/Magento/Backup/Model/Fs/Collection.php -@@ -115,7 +115,8 @@ class Collection extends \Magento\Framework\Data\Collection\Filesystem - if (isset($row['display_name']) && $row['display_name'] == '') { - $row['display_name'] = 'WebSetupWizard'; - } -- $row['id'] = $row['time'] . '_' . $row['type'] . (isset($row['display_name']) ? $row['display_name'] : ''); -+ $row['id'] = $row['time'] . '_' . $row['type'] -+ . (isset($row['display_name']) ? '_' . $row['display_name'] : ''); - return $row; - } - } -diff --git a/app/code/Magento/Backup/Model/ResourceModel/Table/GetListTables.php b/app/code/Magento/Backup/Model/ResourceModel/Table/GetListTables.php -new file mode 100644 -index 00000000000..73c4221feba ---- /dev/null -+++ b/app/code/Magento/Backup/Model/ResourceModel/Table/GetListTables.php -@@ -0,0 +1,44 @@ -+<?php -+/** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+declare(strict_types=1); -+ -+namespace Magento\Backup\Model\ResourceModel\Table; -+ -+use Magento\Framework\App\ResourceConnection; -+ -+/** -+ * Provides full list of tables in the database. This list excludes views, to allow different backup process. -+ */ -+class GetListTables -+{ -+ private const TABLE_TYPE = 'BASE TABLE'; -+ -+ /** -+ * @var ResourceConnection -+ */ -+ private $resource; -+ -+ /** -+ * @param ResourceConnection $resource -+ */ -+ public function __construct(ResourceConnection $resource) -+ { -+ $this->resource = $resource; -+ } -+ -+ /** -+ * Get list of database tables excluding views. -+ * -+ * @return array -+ */ -+ public function execute(): array -+ { -+ return $this->resource->getConnection('backup')->fetchCol( -+ "SHOW FULL TABLES WHERE `Table_type` = ?", -+ self::TABLE_TYPE -+ ); -+ } -+} -diff --git a/app/code/Magento/Backup/Model/ResourceModel/View/CreateViewsBackup.php b/app/code/Magento/Backup/Model/ResourceModel/View/CreateViewsBackup.php -new file mode 100644 -index 00000000000..51b49dcb9e4 ---- /dev/null -+++ b/app/code/Magento/Backup/Model/ResourceModel/View/CreateViewsBackup.php -@@ -0,0 +1,116 @@ -+<?php -+/** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+declare(strict_types=1); -+ -+namespace Magento\Backup\Model\ResourceModel\View; -+ -+use Magento\Framework\App\ResourceConnection; -+use Magento\Framework\Backup\Db\BackupInterface; -+use Magento\Framework\DB\Adapter\AdapterInterface; -+ -+/** -+ * Creates backup of Views in the database. -+ */ -+class CreateViewsBackup -+{ -+ /** -+ * @var GetListViews -+ */ -+ private $getListViews; -+ -+ /** -+ * @var ResourceConnection -+ */ -+ private $resourceConnection; -+ -+ /** -+ * @var AdapterInterface -+ */ -+ private $connection; -+ -+ /** -+ * @param GetListViews $getListViews -+ * @param ResourceConnection $resourceConnection -+ */ -+ public function __construct( -+ GetListViews $getListViews, -+ ResourceConnection $resourceConnection -+ ) { -+ $this->getListViews = $getListViews; -+ $this->resourceConnection = $resourceConnection; -+ } -+ -+ /** -+ * Write backup data to backup file. -+ * -+ * @param BackupInterface $backup -+ */ -+ public function execute(BackupInterface $backup): void -+ { -+ $views = $this->getListViews->execute(); -+ -+ foreach ($views as $view) { -+ $backup->write($this->getViewHeader($view)); -+ $backup->write($this->getDropViewSql($view)); -+ $backup->write($this->getCreateView($view)); -+ } -+ } -+ -+ /** -+ * Retrieve Database connection for Backup. -+ * -+ * @return AdapterInterface -+ */ -+ private function getConnection(): AdapterInterface -+ { -+ if (!$this->connection) { -+ $this->connection = $this->resourceConnection->getConnection('backup'); -+ } -+ -+ return $this->connection; -+ } -+ -+ /** -+ * Get CREATE VIEW query for the specific view. -+ * -+ * @param string $viewName -+ * @return string -+ */ -+ private function getCreateView(string $viewName): string -+ { -+ $quotedViewName = $this->getConnection()->quoteIdentifier($viewName); -+ $query = 'SHOW CREATE VIEW ' . $quotedViewName; -+ $row = $this->getConnection()->fetchRow($query); -+ $regExp = '/\sDEFINER\=\`([^`]*)\`\@\`([^`]*)\`/'; -+ $sql = preg_replace($regExp, '', $row['Create View']); -+ -+ return $sql . ';' . "\n"; -+ } -+ -+ /** -+ * Prepare a header for View being dumped. -+ * -+ * @param string $viewName -+ * @return string -+ */ -+ public function getViewHeader(string $viewName): string -+ { -+ $quotedViewName = $this->getConnection()->quoteIdentifier($viewName); -+ return "\n--\n" . "-- Structure for view {$quotedViewName}\n" . "--\n\n"; -+ } -+ -+ /** -+ * Make sure that View being created is deleted if already exists. -+ * -+ * @param string $viewName -+ * @return string -+ */ -+ public function getDropViewSql(string $viewName): string -+ { -+ $quotedViewName = $this->getConnection()->quoteIdentifier($viewName); -+ return sprintf('DROP VIEW IF EXISTS %s;\n', $quotedViewName); -+ } -+} -diff --git a/app/code/Magento/Backup/Model/ResourceModel/View/GetListViews.php b/app/code/Magento/Backup/Model/ResourceModel/View/GetListViews.php -new file mode 100644 -index 00000000000..c76ea284218 ---- /dev/null -+++ b/app/code/Magento/Backup/Model/ResourceModel/View/GetListViews.php -@@ -0,0 +1,44 @@ -+<?php -+/** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+declare(strict_types=1); -+ -+namespace Magento\Backup\Model\ResourceModel\View; -+ -+use Magento\Framework\App\ResourceConnection; -+ -+/** -+ * Get list of database views. -+ */ -+class GetListViews -+{ -+ private const TABLE_TYPE = 'VIEW'; -+ -+ /** -+ * @var ResourceConnection -+ */ -+ private $resource; -+ -+ /** -+ * @param ResourceConnection $resource -+ */ -+ public function __construct(ResourceConnection $resource) -+ { -+ $this->resource = $resource; -+ } -+ -+ /** -+ * Get list of database views. -+ * -+ * @return array -+ */ -+ public function execute(): array -+ { -+ return $this->resource->getConnection('backup')->fetchCol( -+ "SHOW FULL TABLES WHERE `Table_type` = ?", -+ self::TABLE_TYPE -+ ); -+ } -+} -diff --git a/app/code/Magento/Backup/Test/Mftf/ActionGroup/CreateBackupActionGroup.xml b/app/code/Magento/Backup/Test/Mftf/ActionGroup/CreateBackupActionGroup.xml -new file mode 100644 -index 00000000000..89381112e6c ---- /dev/null -+++ b/app/code/Magento/Backup/Test/Mftf/ActionGroup/CreateBackupActionGroup.xml -@@ -0,0 +1,54 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> -+ -+ <actionGroup name="createSystemBackup"> -+ <arguments> -+ <argument name="backup" defaultValue="SystemBackup"/> -+ </arguments> -+ <click selector="{{AdminMainActionsSection.systemBackup}}" stepKey="clickCreateBackupButton"/> -+ <waitForElementVisible selector="{{AdminCreateBackupFormSection.backupNameField}}" stepKey="waitForForm"/> -+ <fillField selector="{{AdminCreateBackupFormSection.backupNameField}}" userInput="{{backup.name}}" stepKey="fillBackupName"/> -+ <click selector="{{AdminCreateBackupFormSection.ok}}" stepKey="clickOk"/> -+ <waitForElementNotVisible selector=".loading-mask" time="300" stepKey="waitForBackupProcess"/> -+ <see selector="{{AdminMessagesSection.success}}" userInput="You created the system backup." stepKey="seeSuccessMessage"/> -+ <see selector="{{AdminGridTableSection.backupNameColumn}}" userInput="{{backup.name}}" stepKey="seeBackupInGrid"/> -+ <see selector="{{AdminGridTableSection.backupTypeByName(backup.name)}}" userInput="{{backup.type}}" stepKey="seeBackupType"/> -+ </actionGroup> -+ -+ <actionGroup name="createMediaBackup"> -+ <arguments> -+ <argument name="backup" defaultValue="MediaBackup"/> -+ </arguments> -+ <click selector="{{AdminMainActionsSection.mediaBackup}}" stepKey="clickCreateBackupButton"/> -+ <waitForElementVisible selector="{{AdminCreateBackupFormSection.backupNameField}}" stepKey="waitForForm"/> -+ <fillField selector="{{AdminCreateBackupFormSection.backupNameField}}" userInput="{{backup.name}}" stepKey="fillBackupName"/> -+ <click selector="{{AdminCreateBackupFormSection.ok}}" stepKey="clickOk"/> -+ <waitForPageLoad time="120" stepKey="waitForBackupProcess"/> -+ <see selector="{{AdminMessagesSection.success}}" userInput="You created the database and media backup." stepKey="seeSuccessMessage"/> -+ <see selector="{{AdminGridTableSection.backupNameColumn}}" userInput="{{backup.name}}" stepKey="seeBackupInGrid"/> -+ <see selector="{{AdminGridTableSection.backupTypeByName(backup.name)}}" userInput="{{backup.type}}" stepKey="seeBackupType"/> -+ </actionGroup> -+ -+ <actionGroup name="createDatabaseBackup"> -+ <arguments> -+ <argument name="backup" defaultValue="DatabaseBackup"/> -+ </arguments> -+ <click selector="{{AdminMainActionsSection.databaseBackup}}" stepKey="clickCreateBackupButton"/> -+ <waitForElementVisible selector="{{AdminCreateBackupFormSection.backupNameField}}" stepKey="waitForForm"/> -+ <fillField selector="{{AdminCreateBackupFormSection.backupNameField}}" userInput="{{backup.name}}" stepKey="fillBackupName"/> -+ <click selector="{{AdminCreateBackupFormSection.ok}}" stepKey="clickOk"/> -+ <waitForPageLoad time="120" stepKey="waitForBackupProcess"/> -+ <see selector="{{AdminMessagesSection.success}}" userInput="You created the database backup." stepKey="seeSuccessMessage"/> -+ <see selector="{{AdminGridTableSection.backupNameColumn}}" userInput="{{backup.name}}" stepKey="seeBackupInGrid"/> -+ <see selector="{{AdminGridTableSection.backupTypeByName(backup.name)}}" userInput="{{backup.type}}" stepKey="seeBackupType"/> -+ </actionGroup> -+ -+</actionGroups> -diff --git a/app/code/Magento/Backup/Test/Mftf/ActionGroup/DeleteBackupActionGroup.xml b/app/code/Magento/Backup/Test/Mftf/ActionGroup/DeleteBackupActionGroup.xml -new file mode 100644 -index 00000000000..ebc4ac1fb05 ---- /dev/null -+++ b/app/code/Magento/Backup/Test/Mftf/ActionGroup/DeleteBackupActionGroup.xml -@@ -0,0 +1,26 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> -+ -+ <actionGroup name="deleteBackup"> -+ <arguments> -+ <argument name="backup"/> -+ </arguments> -+ <see selector="{{AdminGridTableSection.backupNameColumn}}" userInput="{{backup.name}}" stepKey="seeBackupInGrid"/> -+ <click selector="{{AdminGridTableSection.backupRowCheckbox(backup.name)}}" stepKey="selectBackupRow"/> -+ <selectOption selector="{{AdminGridActionSection.actionSelect}}" userInput="Delete" stepKey="selectDeleteAction"/> -+ <click selector="{{AdminGridActionSection.submitButton}}" stepKey="clickSubmit"/> -+ <waitForPageLoad stepKey="waitForConfirmWindowToAppear"/> -+ <see selector="{{AdminConfirmationModalSection.message}}" userInput="Are you sure you want to delete the selected backup(s)?" stepKey="seeConfirmationModal"/> -+ <waitForPageLoad stepKey="waitForSubmitAction"/> -+ <click selector="{{AdminConfirmationModalSection.ok}}" stepKey="clickOkConfirmDelete"/> -+ <dontSee selector="{{AdminGridTableSection.backupNameColumn}}" userInput="{{backup.name}}" stepKey="dontSeeBackupInGrid"/> -+ </actionGroup> -+</actionGroups> -diff --git a/app/code/Magento/Backup/Test/Mftf/Data/BackupData.xml b/app/code/Magento/Backup/Test/Mftf/Data/BackupData.xml -new file mode 100644 -index 00000000000..ad218cdd575 ---- /dev/null -+++ b/app/code/Magento/Backup/Test/Mftf/Data/BackupData.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="SystemBackup" type="backup"> -+ <data key="name" unique="suffix">systemBackup</data> -+ <data key="type">System</data> -+ </entity> -+ <entity name="MediaBackup" type="backup"> -+ <data key="name" unique="suffix">mediaBackup</data> -+ <data key="type">Database and Media</data> -+ </entity> -+ <entity name="DatabaseBackup" type="backup"> -+ <data key="name" unique="suffix">databaseBackup</data> -+ <data key="type">Database</data> -+ </entity> -+ <entity name="WebSetupWizardBackup" type="backup"> -+ <data key="name">WebSetupWizard</data> -+ <data key="type">Database</data> -+ </entity> -+</entities> -\ No newline at end of file -diff --git a/app/code/Magento/Backup/Test/Mftf/Page/BackupIndexPage.xml b/app/code/Magento/Backup/Test/Mftf/Page/BackupIndexPage.xml -new file mode 100644 -index 00000000000..aad29e6e6d5 ---- /dev/null -+++ b/app/code/Magento/Backup/Test/Mftf/Page/BackupIndexPage.xml -@@ -0,0 +1,16 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<pages xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/PageObject.xsd"> -+ <page name="BackupIndexPage" url="/backup/index/" area="admin" module="Magento_Backup"> -+ <section name="AdminMainActionsSection"/> -+ <section name="AdminGridTableSection"/> -+ <section name="AdminCreateBackupFormSection"/> -+ </page> -+</pages> -diff --git a/app/code/Magento/Backup/Test/Mftf/Section/AdminCreateBackupFormSection.xml b/app/code/Magento/Backup/Test/Mftf/Section/AdminCreateBackupFormSection.xml -new file mode 100644 -index 00000000000..af88146bcfb ---- /dev/null -+++ b/app/code/Magento/Backup/Test/Mftf/Section/AdminCreateBackupFormSection.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="AdminCreateBackupFormSection"> -+ <element name="backupNameField" type="input" selector="input[name='backup_name']"/> -+ <element name="maintenanceModeCheckbox" type="checkbox" selector="input[name='maintenance_mode']"/> -+ <element name="excludeMediaCheckbox" type="checkbox" selector="input[name='exclude_media']"/> -+ <element name="ok" type="button" selector=".modal-header button.primary"/> -+ <element name="cancel" type="button" selector=".modal-header button.cancel"/> -+ </section> -+</sections> -diff --git a/app/code/Magento/Backup/Test/Mftf/Section/AdminGridActionSection.xml b/app/code/Magento/Backup/Test/Mftf/Section/AdminGridActionSection.xml -new file mode 100644 -index 00000000000..cca6b428a28 ---- /dev/null -+++ b/app/code/Magento/Backup/Test/Mftf/Section/AdminGridActionSection.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="AdminGridActionSection"> -+ <element name="actionSelect" type="select" selector="#backupsGrid_massaction-select"/> -+ <element name="submitButton" type="button" selector="#backupsGrid_massaction button[title='Submit']"/> -+ </section> -+</sections> -diff --git a/app/code/Magento/Backup/Test/Mftf/Section/AdminGridTableSection.xml b/app/code/Magento/Backup/Test/Mftf/Section/AdminGridTableSection.xml -new file mode 100644 -index 00000000000..72fb5168493 ---- /dev/null -+++ b/app/code/Magento/Backup/Test/Mftf/Section/AdminGridTableSection.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="AdminGridTableSection"> -+ <element name="backupNameColumn" type="text" selector="table.data-grid td[data-column='display_name']"/> -+ <element name="backupTypeByName" type="text" selector="//table//td[contains(., '{{name}}')]/parent::tr/td[@data-column='type']" parameterized="true"/> -+ <element name="backupRowCheckbox" type="checkbox" selector="//table//td[contains(., '{{name}}')]/parent::tr/td[@data-column='massaction']//input" parameterized="true"/> -+ </section> -+</sections> -diff --git a/app/code/Magento/Backup/Test/Mftf/Section/AdminMainActionsSection.xml b/app/code/Magento/Backup/Test/Mftf/Section/AdminMainActionsSection.xml -new file mode 100644 -index 00000000000..11ba79f32b5 ---- /dev/null -+++ b/app/code/Magento/Backup/Test/Mftf/Section/AdminMainActionsSection.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="AdminMainActionsSection"> -+ <element name="systemBackup" type="button" selector="button[data-ui-id*='createsnapshotbutton']"/> -+ <element name="mediaBackup" type="button" selector="button[data-ui-id*='createmediabackupbutton']"/> -+ <element name="databaseBackup" type="button" selector="button.database-backup[data-ui-id*='createbutton']"/> -+ </section> -+</sections> -diff --git a/app/code/Magento/Backup/Test/Mftf/Test/AdminCreateAndDeleteBackupsTest.xml b/app/code/Magento/Backup/Test/Mftf/Test/AdminCreateAndDeleteBackupsTest.xml -new file mode 100644 -index 00000000000..26f8817c0a1 ---- /dev/null -+++ b/app/code/Magento/Backup/Test/Mftf/Test/AdminCreateAndDeleteBackupsTest.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="AdminCreateAndDeleteBackupsTest"> -+ <annotations> -+ <features value="Backup"/> -+ <stories value="Create and delete backups"/> -+ <title value="Create and delete backups"/> -+ <description value="An admin user can create a backup of each type and delete each backup."/> -+ <severity value="CRITICAL"/> -+ <testCaseId value="MAGETWO-94176"/> -+ <group value="backup"/> -+ <skip> -+ <issueId value="MC-5807"/> -+ </skip> -+ </annotations> -+ -+ <!--Login to admin area--> -+ <actionGroup ref="LoginActionGroup" stepKey="loginAsAdmin"/> -+ -+ <!--Go to backup index page--> -+ <amOnPage url="{{BackupIndexPage.url}}" stepKey="goToBackupPage"/> -+ <waitForPageLoad stepKey="waitForBackupPage"/> -+ -+ <!--Create system backup--> -+ <actionGroup ref="createSystemBackup" stepKey="createSystemBackup"/> -+ -+ <!--Create database/media backup--> -+ <actionGroup ref="createMediaBackup" stepKey="createMediaBackup"/> -+ -+ <!--Create database backup--> -+ <actionGroup ref="createDatabaseBackup" stepKey="createDatabaseBackup"/> -+ -+ <!--Delete system backup--> -+ <actionGroup ref="deleteBackup" stepKey="deleteSystemBackup"> -+ <argument name="backup" value="SystemBackup"/> -+ </actionGroup> -+ -+ <!--Delete database/media backup--> -+ <actionGroup ref="deleteBackup" stepKey="deleteMediaBackup"> -+ <argument name="backup" value="MediaBackup"/> -+ </actionGroup> -+ -+ <!--Delete database backup--> -+ <actionGroup ref="deleteBackup" stepKey="deleteDatabaseBackup"> -+ <argument name="backup" value="DatabaseBackup"/> -+ </actionGroup> -+ -+ </test> -+</tests> -diff --git a/app/code/Magento/Backup/Test/Unit/Cron/SystemBackupTest.php b/app/code/Magento/Backup/Test/Unit/Cron/SystemBackupTest.php -index b7dfb30c0a1..56a7ef42a0b 100644 ---- a/app/code/Magento/Backup/Test/Unit/Cron/SystemBackupTest.php -+++ b/app/code/Magento/Backup/Test/Unit/Cron/SystemBackupTest.php -@@ -3,134 +3,44 @@ - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -+ -+declare(strict_types=1); -+ - namespace Magento\Backup\Test\Unit\Cron; - -+use Magento\Backup\Cron\SystemBackup; -+use PHPUnit\Framework\TestCase; -+use Magento\Backup\Helper\Data as Helper; - use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; - --class SystemBackupTest extends \PHPUnit\Framework\TestCase -+class SystemBackupTest extends TestCase - { - /** -- * @var \Magento\Framework\TestFramework\Unit\Helper\ObjectManager -- */ -- private $objectManager; -- -- /** -- * @var \Magento\Backup\Cron\SystemBackup -- */ -- private $systemBackup; -- -- /** -- * @var \Magento\Framework\App\Config\ScopeConfigInterface|\PHPUnit_Framework_MockObject_MockObject -- */ -- private $scopeConfigMock; -- -- /** -- * @var \Magento\Backup\Helper\Data|\PHPUnit_Framework_MockObject_MockObject -- */ -- private $backupDataMock; -- -- /** -- * @var \Magento\Framework\Registry|\PHPUnit_Framework_MockObject_MockObject -- */ -- private $registryMock; -- -- /** -- * @var \Psr\Log\LoggerInterface|\PHPUnit_Framework_MockObject_MockObject -+ * @var Helper|\PHPUnit_Framework_MockObject_MockObject - */ -- private $loggerMock; -+ private $helperMock; - - /** -- * Filesystem facade -- * -- * @var \Magento\Framework\Filesystem|\PHPUnit_Framework_MockObject_MockObject -+ * @var SystemBackup - */ -- private $filesystemMock; -+ private $cron; - - /** -- * @var \Magento\Framework\Backup\Factory|\PHPUnit_Framework_MockObject_MockObject -+ * @inheritDoc - */ -- private $backupFactoryMock; -- -- /** -- * @var \Magento\Framework\App\MaintenanceMode|\PHPUnit_Framework_MockObject_MockObject -- */ -- private $maintenanceModeMock; -- -- /** -- * @var \Magento\Framework\Backup\Db|\PHPUnit_Framework_MockObject_MockObject -- */ -- private $backupDbMock; -- -- /** -- * @var \Magento\Framework\ObjectManagerInterface|\PHPUnit_Framework_MockObject_MockObject -- */ -- private $objectManagerMock; -- - protected function setUp() - { -- $this->objectManagerMock = $this->getMockBuilder(\Magento\Framework\ObjectManagerInterface::class) -- ->getMock(); -- $this->scopeConfigMock = $this->getMockBuilder(\Magento\Framework\App\Config\ScopeConfigInterface::class) -- ->getMock(); -- $this->backupDataMock = $this->getMockBuilder(\Magento\Backup\Helper\Data::class) -- ->disableOriginalConstructor() -- ->getMock(); -- $this->registryMock = $this->getMockBuilder(\Magento\Framework\Registry::class) -- ->disableOriginalConstructor() -- ->getMock(); -- $this->loggerMock = $this->getMockBuilder(\Psr\Log\LoggerInterface::class) -- ->getMock(); -- $this->filesystemMock = $this->getMockBuilder(\Magento\Framework\Filesystem::class) -- ->disableOriginalConstructor() -- ->getMock(); -- $this->backupFactoryMock = $this->getMockBuilder(\Magento\Framework\Backup\Factory::class) -- ->disableOriginalConstructor() -- ->getMock(); -- $this->maintenanceModeMock = $this->getMockBuilder(\Magento\Framework\App\MaintenanceMode::class) -- ->disableOriginalConstructor() -- ->getMock(); -- -- $this->backupDbMock = $this->getMockBuilder(\Magento\Framework\Backup\Db::class) -- ->disableOriginalConstructor() -- ->getMock(); -- $this->backupDbMock->expects($this->any())->method('setBackupExtension')->willReturnSelf(); -- $this->backupDbMock->expects($this->any())->method('setTime')->willReturnSelf(); -- $this->backupDbMock->expects($this->any())->method('setBackupsDir')->willReturnSelf(); -- -- $this->objectManager = new ObjectManager($this); -- $this->systemBackup = $this->objectManager->getObject( -- \Magento\Backup\Cron\SystemBackup::class, -- [ -- 'backupData' => $this->backupDataMock, -- 'coreRegistry' => $this->registryMock, -- 'logger' => $this->loggerMock, -- 'scopeConfig' => $this->scopeConfigMock, -- 'filesystem' => $this->filesystemMock, -- 'backupFactory' => $this->backupFactoryMock, -- 'maintenanceMode' => $this->maintenanceModeMock, -- ] -- ); -+ $objectManager = new ObjectManager($this); -+ $this->helperMock = $this->getMockBuilder(Helper::class)->disableOriginalConstructor()->getMock(); -+ $this->cron = $objectManager->getObject(SystemBackup::class, ['backupData' => $this->helperMock]); - } - - /** -- * @expectedException \Exception -+ * Test that cron doesn't do anything if backups are disabled. - */ -- public function testExecuteThrowsException() -+ public function testDisabled() - { -- $type = 'db'; -- $this->scopeConfigMock->expects($this->any())->method('isSetFlag')->willReturn(true); -- -- $this->scopeConfigMock->expects($this->once())->method('getValue') -- ->with('system/backup/type', 'store') -- ->willReturn($type); -- -- $this->backupFactoryMock->expects($this->once())->method('create')->willReturn($this->backupDbMock); -- -- $this->backupDbMock->expects($this->once())->method('create')->willThrowException(new \Exception); -- -- $this->backupDataMock->expects($this->never())->method('getCreateSuccessMessageByType')->with($type); -- $this->loggerMock->expects($this->never())->method('info'); -- -- $this->systemBackup->execute(); -+ $this->helperMock->expects($this->any())->method('isEnabled')->willReturn(false); -+ $this->cron->execute(); - } - } -diff --git a/app/code/Magento/Backup/Test/Unit/Model/BackupFactoryTest.php b/app/code/Magento/Backup/Test/Unit/Model/BackupFactoryTest.php -index 629028bfd6f..abf5e63276a 100644 ---- a/app/code/Magento/Backup/Test/Unit/Model/BackupFactoryTest.php -+++ b/app/code/Magento/Backup/Test/Unit/Model/BackupFactoryTest.php -@@ -56,7 +56,7 @@ class BackupFactoryTest extends \PHPUnit\Framework\TestCase - $this->_objectManager->expects( - $this->at(0) - )->method( -- 'get' -+ 'create' - )->with( - \Magento\Backup\Model\Fs\Collection::class - )->will( -@@ -65,7 +65,7 @@ class BackupFactoryTest extends \PHPUnit\Framework\TestCase - $this->_objectManager->expects( - $this->at(1) - )->method( -- 'get' -+ 'create' - )->with( - \Magento\Backup\Model\Backup::class - )->will( -diff --git a/app/code/Magento/Backup/Test/Unit/Model/DbTest.php b/app/code/Magento/Backup/Test/Unit/Model/DbTest.php -deleted file mode 100644 -index 0cab5f0ad1e..00000000000 ---- a/app/code/Magento/Backup/Test/Unit/Model/DbTest.php -+++ /dev/null -@@ -1,243 +0,0 @@ --<?php --/** -- * Copyright © Magento, Inc. All rights reserved. -- * See COPYING.txt for license details. -- */ --declare(strict_types=1); -- --namespace Magento\Backup\Test\Unit\Model; -- --use Magento\Backup\Model\Db; --use Magento\Backup\Model\ResourceModel\Db as DbResource; --use Magento\Framework\App\ResourceConnection; --use Magento\Framework\Backup\Db\BackupInterface; --use Magento\Framework\DataObject; --use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; -- --class DbTest extends \PHPUnit\Framework\TestCase --{ -- /** -- * @var ObjectManager -- */ -- private $objectManager; -- -- /** -- * @var Db -- */ -- private $dbModel; -- -- /** -- * @var DbResource|\PHPUnit_Framework_MockObject_MockObject -- */ -- private $dbResourceMock; -- -- /** -- * @var ResourceConnection|\PHPUnit_Framework_MockObject_MockObject -- */ -- private $connectionResourceMock; -- -- protected function setUp() -- { -- $this->dbResourceMock = $this->getMockBuilder(DbResource::class) -- ->disableOriginalConstructor() -- ->getMock(); -- $this->connectionResourceMock = $this->getMockBuilder(ResourceConnection::class) -- ->disableOriginalConstructor() -- ->getMock(); -- -- $this->objectManager = new ObjectManager($this); -- $this->dbModel = $this->objectManager->getObject( -- Db::class, -- [ -- 'resourceDb' => $this->dbResourceMock, -- 'resource' => $this->connectionResourceMock -- ] -- ); -- } -- -- public function testGetResource() -- { -- self::assertEquals($this->dbResourceMock, $this->dbModel->getResource()); -- } -- -- public function testGetTables() -- { -- $tables = []; -- $this->dbResourceMock->expects($this->once()) -- ->method('getTables') -- ->willReturn($tables); -- -- self::assertEquals($tables, $this->dbModel->getTables()); -- } -- -- public function testGetTableCreateScript() -- { -- $tableName = 'some_table'; -- $script = 'script'; -- $this->dbResourceMock->expects($this->once()) -- ->method('getTableCreateScript') -- ->with($tableName, false) -- ->willReturn($script); -- -- self::assertEquals($script, $this->dbModel->getTableCreateScript($tableName, false)); -- } -- -- public function testGetTableDataDump() -- { -- $tableName = 'some_table'; -- $dump = 'dump'; -- $this->dbResourceMock->expects($this->once()) -- ->method('getTableDataDump') -- ->with($tableName) -- ->willReturn($dump); -- -- self::assertEquals($dump, $this->dbModel->getTableDataDump($tableName)); -- } -- -- public function testGetHeader() -- { -- $header = 'header'; -- $this->dbResourceMock->expects($this->once()) -- ->method('getHeader') -- ->willReturn($header); -- -- self::assertEquals($header, $this->dbModel->getHeader()); -- } -- -- public function testGetFooter() -- { -- $footer = 'footer'; -- $this->dbResourceMock->expects($this->once()) -- ->method('getFooter') -- ->willReturn($footer); -- -- self::assertEquals($footer, $this->dbModel->getFooter()); -- } -- -- public function testRenderSql() -- { -- $header = 'header'; -- $script = 'script'; -- $tableName = 'some_table'; -- $tables = [$tableName, $tableName]; -- $dump = 'dump'; -- $footer = 'footer'; -- -- $this->dbResourceMock->expects($this->once()) -- ->method('getTables') -- ->willReturn($tables); -- $this->dbResourceMock->expects($this->once()) -- ->method('getHeader') -- ->willReturn($header); -- $this->dbResourceMock->expects($this->exactly(2)) -- ->method('getTableCreateScript') -- ->with($tableName, true) -- ->willReturn($script); -- $this->dbResourceMock->expects($this->exactly(2)) -- ->method('getTableDataDump') -- ->with($tableName) -- ->willReturn($dump); -- $this->dbResourceMock->expects($this->once()) -- ->method('getFooter') -- ->willReturn($footer); -- -- self::assertEquals( -- $header . $script . $dump . $script . $dump . $footer, -- $this->dbModel->renderSql() -- ); -- } -- -- public function testCreateBackup() -- { -- /** @var BackupInterface|\PHPUnit_Framework_MockObject_MockObject $backupMock */ -- $backupMock = $this->getMockBuilder(BackupInterface::class)->getMock(); -- /** @var DataObject $tableStatus */ -- $tableStatus = new DataObject(); -- -- $tableName = 'some_table'; -- $tables = [$tableName]; -- $header = 'header'; -- $footer = 'footer'; -- $dropSql = 'drop_sql'; -- $createSql = 'create_sql'; -- $beforeSql = 'before_sql'; -- $afterSql = 'after_sql'; -- $dataSql = 'data_sql'; -- $foreignKeysSql = 'foreign_keys'; -- $triggersSql = 'triggers_sql'; -- $rowsCount = 2; -- $dataLength = 1; -- -- $this->dbResourceMock->expects($this->once()) -- ->method('beginTransaction'); -- $this->dbResourceMock->expects($this->once()) -- ->method('commitTransaction'); -- $this->dbResourceMock->expects($this->once()) -- ->method('getTables') -- ->willReturn($tables); -- $this->dbResourceMock->expects($this->once()) -- ->method('getTableDropSql') -- ->willReturn($dropSql); -- $this->dbResourceMock->expects($this->once()) -- ->method('getTableCreateSql') -- ->with($tableName, false) -- ->willReturn($createSql); -- $this->dbResourceMock->expects($this->once()) -- ->method('getTableDataBeforeSql') -- ->with($tableName) -- ->willReturn($beforeSql); -- $this->dbResourceMock->expects($this->once()) -- ->method('getTableDataAfterSql') -- ->with($tableName) -- ->willReturn($afterSql); -- $this->dbResourceMock->expects($this->once()) -- ->method('getTableDataSql') -- ->with($tableName, $rowsCount, 0) -- ->willReturn($dataSql); -- $this->dbResourceMock->expects($this->once()) -- ->method('getTableStatus') -- ->with($tableName) -- ->willReturn($tableStatus); -- $this->dbResourceMock->expects($this->once()) -- ->method('getTables') -- ->willReturn($createSql); -- $this->dbResourceMock->expects($this->once()) -- ->method('getHeader') -- ->willReturn($header); -- $this->dbResourceMock->expects($this->once()) -- ->method('getTableHeader') -- ->willReturn($header); -- $this->dbResourceMock->expects($this->once()) -- ->method('getFooter') -- ->willReturn($footer); -- $this->dbResourceMock->expects($this->once()) -- ->method('getTableForeignKeysSql') -- ->willReturn($foreignKeysSql); -- $this->dbResourceMock->expects($this->once()) -- ->method('getTableTriggersSql') -- ->willReturn($triggersSql); -- $backupMock->expects($this->once()) -- ->method('open'); -- $backupMock->expects($this->once()) -- ->method('close'); -- -- $tableStatus->setRows($rowsCount); -- $tableStatus->setDataLength($dataLength); -- -- $backupMock->expects($this->any()) -- ->method('write') -- ->withConsecutive( -- [$this->equalTo($header)], -- [$this->equalTo($header . $dropSql . "\n")], -- [$this->equalTo($createSql . "\n")], -- [$this->equalTo($beforeSql)], -- [$this->equalTo($dataSql)], -- [$this->equalTo($afterSql)], -- [$this->equalTo($foreignKeysSql)], -- [$this->equalTo($triggersSql)], -- [$this->equalTo($footer)] -- ); -- -- $this->dbModel->createBackup($backupMock); -- } --} -diff --git a/app/code/Magento/Backup/etc/adminhtml/system.xml b/app/code/Magento/Backup/etc/adminhtml/system.xml -index 4028452d044..aa6635b4dde 100644 ---- a/app/code/Magento/Backup/etc/adminhtml/system.xml -+++ b/app/code/Magento/Backup/etc/adminhtml/system.xml -@@ -9,15 +9,24 @@ - <system> - <section id="system"> - <group id="backup" translate="label" type="text" sortOrder="500" showInDefault="1" showInWebsite="0" showInStore="0"> -- <label>Scheduled Backup Settings</label> -+ <label>Backup Settings</label> -+ <field id="functionality_enabled" translate="label" type="select" sortOrder="5" showInDefault="1" showInWebsite="0" showInStore="0"> -+ <label>Enable Backup</label> -+ <comment>Disabled by default for security reasons</comment> -+ <source_model>Magento\Config\Model\Config\Source\Yesno</source_model> -+ </field> - <field id="enabled" translate="label" type="select" sortOrder="10" showInDefault="1" showInWebsite="0" showInStore="0"> - <label>Enable Scheduled Backup</label> - <source_model>Magento\Config\Model\Config\Source\Yesno</source_model> -+ <depends> -+ <field id="functionality_enabled">1</field> -+ </depends> - </field> - <field id="type" translate="label" type="select" sortOrder="20" showInDefault="1" showInWebsite="0" showInStore="0"> -- <label>Backup Type</label> -+ <label>Scheduled Backup Type</label> - <depends> - <field id="enabled">1</field> -+ <field id="functionality_enabled">1</field> - </depends> - <source_model>Magento\Backup\Model\Config\Source\Type</source_model> - </field> -@@ -25,12 +34,14 @@ - <label>Start Time</label> - <depends> - <field id="enabled">1</field> -+ <field id="functionality_enabled">1</field> - </depends> - </field> - <field id="frequency" translate="label" type="select" sortOrder="40" showInDefault="1" showInWebsite="0" showInStore="0"> - <label>Frequency</label> - <depends> - <field id="enabled">1</field> -+ <field id="functionality_enabled">1</field> - </depends> - <source_model>Magento\Cron\Model\Config\Source\Frequency</source_model> - <backend_model>Magento\Backup\Model\Config\Backend\Cron</backend_model> -@@ -40,6 +51,7 @@ - <comment>Please put your store into maintenance mode during backup.</comment> - <depends> - <field id="enabled">1</field> -+ <field id="functionality_enabled">1</field> - </depends> - <source_model>Magento\Config\Model\Config\Source\Yesno</source_model> - </field> -diff --git a/app/code/Magento/Backup/etc/config.xml b/app/code/Magento/Backup/etc/config.xml -new file mode 100644 -index 00000000000..fb0808983b9 ---- /dev/null -+++ b/app/code/Magento/Backup/etc/config.xml -@@ -0,0 +1,16 @@ -+<?xml version="1.0"?> -+<!-- -+/** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Store:etc/config.xsd"> -+ <default> -+ <system> -+ <backup> -+ <functionality_enabled>0</functionality_enabled> -+ </backup> -+ </system> -+ </default> -+</config> -diff --git a/app/code/Magento/Backup/view/adminhtml/layout/backup_index_block.xml b/app/code/Magento/Backup/view/adminhtml/layout/backup_index_block.xml -index e17618b97e2..e3e984d933f 100644 ---- a/app/code/Magento/Backup/view/adminhtml/layout/backup_index_block.xml -+++ b/app/code/Magento/Backup/view/adminhtml/layout/backup_index_block.xml -@@ -11,7 +11,7 @@ - <block class="Magento\Backend\Block\Widget\Grid" name="adminhtml.system.backup.grid" as="grid"> - <arguments> - <argument name="id" xsi:type="string">backupsGrid</argument> -- <argument name="dataSource" xsi:type="object">Magento\Backup\Model\Fs\Collection</argument> -+ <argument name="dataSource" xsi:type="object" shared="false">Magento\Backup\Model\Fs\Collection</argument> - <argument name="default_sort" xsi:type="string">time</argument> - <argument name="default_dir" xsi:type="string">desc</argument> - </arguments> -diff --git a/app/code/Magento/Backup/view/adminhtml/layout/backup_index_disabled.xml b/app/code/Magento/Backup/view/adminhtml/layout/backup_index_disabled.xml -new file mode 100644 -index 00000000000..3470f528e5c ---- /dev/null -+++ b/app/code/Magento/Backup/view/adminhtml/layout/backup_index_disabled.xml -@@ -0,0 +1,17 @@ -+<?xml version="1.0"?> -+<!-- -+/** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+<page xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:View/Layout/etc/page_configuration.xsd"> -+ <head> -+ <title>Backup functionality is disabled -+ -+ -+ -+ -+ -+ -+ -diff --git a/app/code/Magento/Backup/view/adminhtml/templates/backup/dialogs.phtml b/app/code/Magento/Backup/view/adminhtml/templates/backup/dialogs.phtml -index 304a60c60ca..81aa49efd11 100644 ---- a/app/code/Magento/Backup/view/adminhtml/templates/backup/dialogs.phtml -+++ b/app/code/Magento/Backup/view/adminhtml/templates/backup/dialogs.phtml -@@ -3,14 +3,11 @@ - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -- --// @codingStandardsIgnoreFile -- - ?> - - - -+ -diff --git a/app/code/Magento/Braintree/view/frontend/templates/multishipping/form_paypal.phtml b/app/code/Magento/Braintree/view/frontend/templates/multishipping/form_paypal.phtml -new file mode 100644 -index 00000000000..ea3eb2214c2 ---- /dev/null -+++ b/app/code/Magento/Braintree/view/frontend/templates/multishipping/form_paypal.phtml -@@ -0,0 +1,29 @@ -+ -+ -+ -diff --git a/app/code/Magento/Braintree/view/frontend/templates/paypal/button.phtml b/app/code/Magento/Braintree/view/frontend/templates/paypal/button.phtml -index c1ef461ecae..e0a9e46bd7c 100644 ---- a/app/code/Magento/Braintree/view/frontend/templates/paypal/button.phtml -+++ b/app/code/Magento/Braintree/view/frontend/templates/paypal/button.phtml -@@ -15,21 +15,18 @@ $config = [ - 'id' => $id, - 'clientToken' => $block->getClientToken(), - 'displayName' => $block->getMerchantName(), -- 'actionSuccess' => $block->getActionSuccess() -+ 'actionSuccess' => $block->getActionSuccess(), -+ 'environment' => $block->getEnvironment() - ] - ]; - - ?> -- - -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 b220e2c98d7..46db8a99073 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 @@ class Checkbox extends \Magento\Bundle\Block\Catalog\Product\View\Type\Bundle\Op - '\'; - '; - } -+ -+ /** -+ * @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 a4b8c6bde73..629f08dc751 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 @@ class Multi extends \Magento\Bundle\Block\Catalog\Product\View\Type\Bundle\Optio - '\'; - '; - } -+ -+ /** -+ * @inheritdoc -+ */ -+ public function getSelectionPrice($selection) -+ { -+ $price = parent::getSelectionPrice($selection); -+ $qty = $selection->getSelectionQty(); -+ -+ return $price * $qty; -+ } - } -diff --git a/app/code/Magento/Bundle/Block/Adminhtml/Catalog/Product/Edit/Tab/Attributes/Extend.php b/app/code/Magento/Bundle/Block/Adminhtml/Catalog/Product/Edit/Tab/Attributes/Extend.php -index 0e21e566d5e..91b015782fe 100644 ---- a/app/code/Magento/Bundle/Block/Adminhtml/Catalog/Product/Edit/Tab/Attributes/Extend.php -+++ b/app/code/Magento/Bundle/Block/Adminhtml/Catalog/Product/Edit/Tab/Attributes/Extend.php -@@ -4,13 +4,13 @@ - * See COPYING.txt for license details. - */ - -+namespace Magento\Bundle\Block\Adminhtml\Catalog\Product\Edit\Tab\Attributes; -+ - /** -- * Bundle Extended Attribures Block -+ * Bundle Extended Attributes Block. - * -- * @author Magento Core Team -+ * @author Magento Core Team - */ --namespace Magento\Bundle\Block\Adminhtml\Catalog\Product\Edit\Tab\Attributes; -- - class Extend extends \Magento\Catalog\Block\Adminhtml\Form\Renderer\Fieldset\Element - { - /** -@@ -75,7 +75,7 @@ class Extend extends \Magento\Catalog\Block\Adminhtml\Form\Renderer\Fieldset\Ele - } - - /** -- * Execute method getElementHtml from parrent class -+ * Execute method getElementHtml from parent class - * - * @return string - */ -@@ -85,6 +85,8 @@ class Extend extends \Magento\Catalog\Block\Adminhtml\Form\Renderer\Fieldset\Ele - } - - /** -+ * Get options. -+ * - * @return array - */ - public function getOptions() -@@ -106,6 +108,8 @@ class Extend extends \Magento\Catalog\Block\Adminhtml\Form\Renderer\Fieldset\Ele - } - - /** -+ * Is disabled field. -+ * - * @return bool - */ - public function isDisabledField() -@@ -118,6 +122,8 @@ class Extend extends \Magento\Catalog\Block\Adminhtml\Form\Renderer\Fieldset\Ele - } - - /** -+ * Get product. -+ * - * @return mixed - */ - public function getProduct() -@@ -129,6 +135,8 @@ class Extend extends \Magento\Catalog\Block\Adminhtml\Form\Renderer\Fieldset\Ele - } - - /** -+ * Get extended element. -+ * - * @param string $switchAttributeCode - * @return \Magento\Framework\Data\Form\Element\Select - * @throws \Magento\Framework\Exception\LocalizedException -diff --git a/app/code/Magento/Bundle/Block/Adminhtml/Sales/Order/Items/Renderer.php b/app/code/Magento/Bundle/Block/Adminhtml/Sales/Order/Items/Renderer.php -index 23fc2026ab1..82a0086ad67 100644 ---- a/app/code/Magento/Bundle/Block/Adminhtml/Sales/Order/Items/Renderer.php -+++ b/app/code/Magento/Bundle/Block/Adminhtml/Sales/Order/Items/Renderer.php -@@ -100,6 +100,8 @@ class Renderer extends \Magento\Sales\Block\Adminhtml\Items\Renderer\DefaultRend - } - - /** -+ * Check if item can be shipped separately -+ * - * @param mixed $item - * @return bool - * @SuppressWarnings(PHPMD.CyclomaticComplexity) -@@ -136,6 +138,8 @@ class Renderer extends \Magento\Sales\Block\Adminhtml\Items\Renderer\DefaultRend - } - - /** -+ * Check if child items calculated -+ * - * @param mixed $item - * @return bool - * @SuppressWarnings(PHPMD.CyclomaticComplexity) -@@ -174,6 +178,8 @@ class Renderer extends \Magento\Sales\Block\Adminhtml\Items\Renderer\DefaultRend - } - - /** -+ * Retrieve selection attributes values -+ * - * @param mixed $item - * @return mixed|null - */ -@@ -191,6 +197,8 @@ class Renderer extends \Magento\Sales\Block\Adminhtml\Items\Renderer\DefaultRend - } - - /** -+ * Retrieve order item options array -+ * - * @return array - */ - public function getOrderOptions() -@@ -212,6 +220,8 @@ class Renderer extends \Magento\Sales\Block\Adminhtml\Items\Renderer\DefaultRend - } - - /** -+ * Retrieve order item -+ * - * @return mixed - */ - public function getOrderItem() -@@ -223,6 +233,8 @@ class Renderer extends \Magento\Sales\Block\Adminhtml\Items\Renderer\DefaultRend - } - - /** -+ * Get html info for item -+ * - * @param mixed $item - * @return string - */ -@@ -245,6 +257,8 @@ class Renderer extends \Magento\Sales\Block\Adminhtml\Items\Renderer\DefaultRend - } - - /** -+ * Check if we can show price info for this item -+ * - * @param object $item - * @return bool - */ -diff --git a/app/code/Magento/Bundle/Block/Catalog/Product/View/Type/Bundle.php b/app/code/Magento/Bundle/Block/Catalog/Product/View/Type/Bundle.php -index 542f170da8c..fa488b073f5 100644 ---- a/app/code/Magento/Bundle/Block/Catalog/Product/View/Type/Bundle.php -+++ b/app/code/Magento/Bundle/Block/Catalog/Product/View/Type/Bundle.php -@@ -7,6 +7,7 @@ namespace Magento\Bundle\Block\Catalog\Product\View\Type; - - use Magento\Bundle\Model\Option; - use Magento\Catalog\Model\Product; -+use Magento\Framework\DataObject; - - /** - * Catalog bundle product info block -@@ -56,6 +57,11 @@ class Bundle extends \Magento\Catalog\Block\Product\View\AbstractView - */ - private $catalogRuleProcessor; - -+ /** -+ * @var array -+ */ -+ private $optionsPosition = []; -+ - /** - * @param \Magento\Catalog\Block\Product\Context $context - * @param \Magento\Framework\Stdlib\ArrayUtils $arrayUtils -@@ -86,6 +92,8 @@ class Bundle extends \Magento\Catalog\Block\Product\View\AbstractView - } - - /** -+ * Return catalog rule processor or creates processor if it does not exist -+ * - * @deprecated 100.2.0 - * @return \Magento\CatalogRule\Model\ResourceModel\Product\CollectionProcessor - */ -@@ -101,6 +109,7 @@ class Bundle extends \Magento\Catalog\Block\Product\View\AbstractView - - /** - * Returns the bundle product options -+ * - * Will return cached options data if the product options are already initialized - * In a case when $stripSelection parameter is true will reload stored bundle selections collection from DB - * -@@ -135,6 +144,8 @@ class Bundle extends \Magento\Catalog\Block\Product\View\AbstractView - } - - /** -+ * Return true if product has options -+ * - * @return bool - */ - public function hasOptions() -@@ -150,7 +161,6 @@ class Bundle extends \Magento\Catalog\Block\Product\View\AbstractView - * Returns JSON encoded config to be used in JS scripts - * - * @return string -- * - */ - public function getJsonConfig() - { -@@ -161,7 +171,7 @@ class Bundle extends \Magento\Catalog\Block\Product\View\AbstractView - - $defaultValues = []; - $preConfiguredFlag = $currentProduct->hasPreconfiguredValues(); -- /** @var \Magento\Framework\DataObject|null $preConfiguredValues */ -+ /** @var DataObject|null $preConfiguredValues */ - $preConfiguredValues = $preConfiguredFlag ? $currentProduct->getPreconfiguredValues() : null; - - $position = 0; -@@ -172,19 +182,25 @@ class Bundle extends \Magento\Catalog\Block\Product\View\AbstractView - } - $optionId = $optionItem->getId(); - $options[$optionId] = $this->getOptionItemData($optionItem, $currentProduct, $position); -+ $this->optionsPosition[$position] = $optionId; - - // Add attribute default value (if set) - if ($preConfiguredFlag) { - $configValue = $preConfiguredValues->getData('bundle_option/' . $optionId); - if ($configValue) { - $defaultValues[$optionId] = $configValue; -+ $configQty = $preConfiguredValues->getData('bundle_option_qty/' . $optionId); -+ if ($configQty) { -+ $options[$optionId]['selections'][$configValue]['qty'] = $configQty; -+ } - } -+ $options = $this->processOptions($optionId, $options, $preConfiguredValues); - } - $position++; - } - $config = $this->getConfigData($currentProduct, $options); - -- $configObj = new \Magento\Framework\DataObject( -+ $configObj = new DataObject( - [ - 'config' => $config, - ] -@@ -370,6 +386,7 @@ class Bundle extends \Magento\Catalog\Block\Product\View\AbstractView - $config = [ - 'options' => $options, - 'selected' => $this->selectedOptions, -+ 'positions' => $this->optionsPosition, - 'bundleId' => $product->getId(), - 'priceFormat' => $this->localeFormat->getPriceFormat(), - 'prices' => [ -@@ -388,4 +405,30 @@ class Bundle extends \Magento\Catalog\Block\Product\View\AbstractView - ]; - return $config; - } -+ -+ /** -+ * Set preconfigured quantities and selections to options. -+ * -+ * @param string $optionId -+ * @param array $options -+ * @param DataObject $preConfiguredValues -+ * @return array -+ */ -+ private function processOptions(string $optionId, array $options, DataObject $preConfiguredValues) -+ { -+ $preConfiguredQtys = $preConfiguredValues->getData("bundle_option_qty/${optionId}") ?? []; -+ $selections = $options[$optionId]['selections']; -+ array_walk($selections, function (&$selection, $selectionId) use ($preConfiguredQtys) { -+ if (is_array($preConfiguredQtys) && isset($preConfiguredQtys[$selectionId])) { -+ $selection['qty'] = $preConfiguredQtys[$selectionId]; -+ } else { -+ if ((int)$preConfiguredQtys > 0) { -+ $selection['qty'] = $preConfiguredQtys; -+ } -+ } -+ }); -+ $options[$optionId]['selections'] = $selections; -+ -+ return $options; -+ } - } -diff --git a/app/code/Magento/Bundle/Block/Catalog/Product/View/Type/Bundle/Option.php b/app/code/Magento/Bundle/Block/Catalog/Product/View/Type/Bundle/Option.php -index 5d326e7c01d..7c5a64ca023 100644 ---- a/app/code/Magento/Bundle/Block/Catalog/Product/View/Type/Bundle/Option.php -+++ b/app/code/Magento/Bundle/Block/Catalog/Product/View/Type/Bundle/Option.php -@@ -6,6 +6,8 @@ - - namespace Magento\Bundle\Block\Catalog\Product\View\Type\Bundle; - -+use Magento\Catalog\Model\Product; -+ - /** - * Bundle option renderer - * @api -@@ -167,7 +169,9 @@ class Option extends \Magento\Bundle\Block\Catalog\Product\Price - */ - protected function assignSelection(\Magento\Bundle\Model\Option $option, $selectionId) - { -- if ($selectionId && $option->getSelectionById($selectionId)) { -+ if (is_array($selectionId)) { -+ $this->_selectedOptions = $selectionId; -+ } elseif ($selectionId && $option->getSelectionById($selectionId)) { - $this->_selectedOptions = $selectionId; - } elseif (!$option->getRequired()) { - $this->_selectedOptions = 'None'; -@@ -177,7 +181,7 @@ class Option extends \Magento\Bundle\Block\Catalog\Product\Price - /** - * Define if selection is selected - * -- * @param \Magento\Catalog\Model\Product $selection -+ * @param Product $selection - * @return bool - */ - public function isSelected($selection) -@@ -217,7 +221,7 @@ class Option extends \Magento\Bundle\Block\Catalog\Product\Price - /** - * Get product model - * -- * @return \Magento\Catalog\Model\Product -+ * @return Product - */ - public function getProduct() - { -@@ -228,7 +232,9 @@ class Option extends \Magento\Bundle\Block\Catalog\Product\Price - } - - /** -- * @param \Magento\Catalog\Model\Product $selection -+ * Get bundle option price title. -+ * -+ * @param Product $selection - * @param bool $includeContainer - * @return string - */ -@@ -250,7 +256,7 @@ class Option extends \Magento\Bundle\Block\Catalog\Product\Price - /** - * Get price for selection product - * -- * @param \Magento\Catalog\Model\Product $selection -+ * @param Product $selection - * @return int|float - */ - public function getSelectionPrice($selection) -@@ -273,7 +279,7 @@ class Option extends \Magento\Bundle\Block\Catalog\Product\Price - /** - * Get title price for selection product - * -- * @param \Magento\Catalog\Model\Product $selection -+ * @param Product $selection - * @param bool $includeContainer - * @return string - */ -@@ -295,7 +301,7 @@ class Option extends \Magento\Bundle\Block\Catalog\Product\Price - */ - public function setValidationContainer($elementId, $containerId) - { -- return; -+ return ''; - } - - /** -@@ -314,7 +320,7 @@ class Option extends \Magento\Bundle\Block\Catalog\Product\Price - /** - * Format price string - * -- * @param \Magento\Catalog\Model\Product $selection -+ * @param Product $selection - * @param bool $includeContainer - * @return string - */ -diff --git a/app/code/Magento/Bundle/Block/Checkout/Cart/Item/Renderer.php b/app/code/Magento/Bundle/Block/Checkout/Cart/Item/Renderer.php -index c75ebc70060..c0a2d9d4303 100644 ---- a/app/code/Magento/Bundle/Block/Checkout/Cart/Item/Renderer.php -+++ b/app/code/Magento/Bundle/Block/Checkout/Cart/Item/Renderer.php -@@ -32,7 +32,7 @@ class Renderer extends \Magento\Checkout\Block\Cart\Item\Renderer - * @param \Magento\Framework\Url\Helper\Data $urlHelper - * @param \Magento\Framework\Message\ManagerInterface $messageManager - * @param PriceCurrencyInterface $priceCurrency -- * @param \Magento\Framework\Module\Manager $moduleManager -+ * @param \Magento\Framework\Module\ModuleManagerInterface $moduleManager - * @param InterpretationStrategyInterface $messageInterpretationStrategy - * @param Configuration $bundleProductConfiguration - * @param array $data -@@ -46,7 +46,7 @@ class Renderer extends \Magento\Checkout\Block\Cart\Item\Renderer - \Magento\Framework\Url\Helper\Data $urlHelper, - \Magento\Framework\Message\ManagerInterface $messageManager, - PriceCurrencyInterface $priceCurrency, -- \Magento\Framework\Module\Manager $moduleManager, -+ \Magento\Framework\Module\ModuleManagerInterface $moduleManager, - InterpretationStrategyInterface $messageInterpretationStrategy, - Configuration $bundleProductConfiguration, - array $data = [] -@@ -69,6 +69,7 @@ class Renderer extends \Magento\Checkout\Block\Cart\Item\Renderer - - /** - * Overloaded method for getting list of bundle options -+ * - * Caches result in quote item, because it can be used in cart 'recent view' and on same page in cart checkout - * - * @return array -@@ -88,7 +89,7 @@ class Renderer extends \Magento\Checkout\Block\Cart\Item\Renderer - $messages = []; - $quoteItem = $this->getItem(); - -- // Add basic messages occuring during this page load -+ // Add basic messages occurring during this page load - $baseMessages = $quoteItem->getMessage(false); - if ($baseMessages) { - foreach ($baseMessages as $message) { -diff --git a/app/code/Magento/Bundle/Block/DataProviders/OptionPriceRenderer.php b/app/code/Magento/Bundle/Block/DataProviders/OptionPriceRenderer.php -new file mode 100644 -index 00000000000..058b3a981b5 ---- /dev/null -+++ b/app/code/Magento/Bundle/Block/DataProviders/OptionPriceRenderer.php -@@ -0,0 +1,63 @@ -+layout = $layout; -+ } -+ -+ /** -+ * Format tier price string -+ * -+ * @param Product $selection -+ * @param array $arguments -+ * @return string -+ */ -+ public function renderTierPrice(Product $selection, array $arguments = []): string -+ { -+ if (!array_key_exists('zone', $arguments)) { -+ $arguments['zone'] = Render::ZONE_ITEM_OPTION; -+ } -+ -+ $priceHtml = ''; -+ -+ /** @var Render $priceRender */ -+ $priceRender = $this->layout->getBlock('product.price.render.default'); -+ if ($priceRender !== false) { -+ $priceHtml = $priceRender->render( -+ TierPrice::PRICE_CODE, -+ $selection, -+ $arguments -+ ); -+ } -+ -+ return $priceHtml; -+ } -+} -diff --git a/app/code/Magento/Bundle/Controller/Adminhtml/Bundle/Product/Edit/Crosssell.php b/app/code/Magento/Bundle/Controller/Adminhtml/Bundle/Product/Edit/Crosssell.php -index ba7b9b5fcb5..08fac66c4a4 100644 ---- a/app/code/Magento/Bundle/Controller/Adminhtml/Bundle/Product/Edit/Crosssell.php -+++ b/app/code/Magento/Bundle/Controller/Adminhtml/Bundle/Product/Edit/Crosssell.php -@@ -6,6 +6,16 @@ - */ - namespace Magento\Bundle\Controller\Adminhtml\Bundle\Product\Edit; - --class Crosssell extends \Magento\Catalog\Controller\Adminhtml\Product\Crosssell -+use Magento\Catalog\Controller\Adminhtml\Product\Crosssell as CatalogCrossel; -+use Magento\Framework\App\Action\HttpPostActionInterface; -+ -+/** -+ * Class Crosssell -+ * -+ * @package Magento\Bundle\Controller\Adminhtml\Bundle\Product\Edit -+ * @deprecated Not used since cross-sell products grid moved to UI components. -+ * @see Magento_Catalog::view/adminhtml/ui_component/crosssell_product_listing.xml -+ */ -+class Crosssell extends CatalogCrossel implements HttpPostActionInterface - { - } -diff --git a/app/code/Magento/Bundle/Controller/Adminhtml/Bundle/Product/Edit/CrosssellGrid.php b/app/code/Magento/Bundle/Controller/Adminhtml/Bundle/Product/Edit/CrosssellGrid.php -index 87550d768ca..b301d6ee2fe 100644 ---- a/app/code/Magento/Bundle/Controller/Adminhtml/Bundle/Product/Edit/CrosssellGrid.php -+++ b/app/code/Magento/Bundle/Controller/Adminhtml/Bundle/Product/Edit/CrosssellGrid.php -@@ -6,6 +6,16 @@ - */ - namespace Magento\Bundle\Controller\Adminhtml\Bundle\Product\Edit; - --class CrosssellGrid extends \Magento\Catalog\Controller\Adminhtml\Product\CrosssellGrid -+use Magento\Catalog\Controller\Adminhtml\Product\CrosssellGrid as CatalogCrosssellGrid; -+use Magento\Framework\App\Action\HttpPostActionInterface; -+ -+/** -+ * Class CrosssellGrid -+ * -+ * @package Magento\Bundle\Controller\Adminhtml\Bundle\Product\Edit -+ * @deprecated Not used since cross-sell products grid moved to UI components. -+ * @see Magento_Catalog::view/adminhtml/ui_component/crosssell_product_listing.xml -+ */ -+class CrosssellGrid extends CatalogCrosssellGrid implements HttpPostActionInterface - { - } -diff --git a/app/code/Magento/Bundle/Controller/Adminhtml/Bundle/Product/Edit/Related.php b/app/code/Magento/Bundle/Controller/Adminhtml/Bundle/Product/Edit/Related.php -index 58f464fba3a..7534cfddcaa 100644 ---- a/app/code/Magento/Bundle/Controller/Adminhtml/Bundle/Product/Edit/Related.php -+++ b/app/code/Magento/Bundle/Controller/Adminhtml/Bundle/Product/Edit/Related.php -@@ -6,6 +6,16 @@ - */ - namespace Magento\Bundle\Controller\Adminhtml\Bundle\Product\Edit; - --class Related extends \Magento\Catalog\Controller\Adminhtml\Product\Related -+use Magento\Catalog\Controller\Adminhtml\Product\Related as CatalogRelated; -+use Magento\Framework\App\Action\HttpPostActionInterface; -+ -+/** -+ * Class Related -+ * -+ * @package Magento\Bundle\Controller\Adminhtml\Bundle\Product\Edit -+ * @deprecated Not used since related products grid moved to UI components. -+ * @see Magento_Catalog::view/adminhtml/ui_component/related_product_listing.xml -+ */ -+class Related extends CatalogRelated implements HttpPostActionInterface - { - } -diff --git a/app/code/Magento/Bundle/Controller/Adminhtml/Bundle/Product/Edit/RelatedGrid.php b/app/code/Magento/Bundle/Controller/Adminhtml/Bundle/Product/Edit/RelatedGrid.php -index a27efe8bc2b..e0fc9226663 100644 ---- a/app/code/Magento/Bundle/Controller/Adminhtml/Bundle/Product/Edit/RelatedGrid.php -+++ b/app/code/Magento/Bundle/Controller/Adminhtml/Bundle/Product/Edit/RelatedGrid.php -@@ -6,6 +6,16 @@ - */ - namespace Magento\Bundle\Controller\Adminhtml\Bundle\Product\Edit; - --class RelatedGrid extends \Magento\Catalog\Controller\Adminhtml\Product\RelatedGrid -+use Magento\Catalog\Controller\Adminhtml\Product\RelatedGrid as CatalogRelatedGrid; -+use Magento\Framework\App\Action\HttpPostActionInterface; -+ -+/** -+ * Class RelatedGrid -+ * -+ * @package Magento\Bundle\Controller\Adminhtml\Bundle\Product\Edit -+ * @deprecated Not used since related products grid moved to UI components. -+ * @see Magento_Catalog::view/adminhtml/ui_component/related_product_listing.xml -+ */ -+class RelatedGrid extends CatalogRelatedGrid implements HttpPostActionInterface - { - } -diff --git a/app/code/Magento/Bundle/Controller/Adminhtml/Bundle/Product/Edit/Upsell.php b/app/code/Magento/Bundle/Controller/Adminhtml/Bundle/Product/Edit/Upsell.php -index d0a6343569e..239b13970e6 100644 ---- a/app/code/Magento/Bundle/Controller/Adminhtml/Bundle/Product/Edit/Upsell.php -+++ b/app/code/Magento/Bundle/Controller/Adminhtml/Bundle/Product/Edit/Upsell.php -@@ -6,6 +6,16 @@ - */ - namespace Magento\Bundle\Controller\Adminhtml\Bundle\Product\Edit; - --class Upsell extends \Magento\Catalog\Controller\Adminhtml\Product\Upsell -+use Magento\Catalog\Controller\Adminhtml\Product\Upsell as CatalogUpsell; -+use Magento\Framework\App\Action\HttpPostActionInterface; -+ -+/** -+ * Class Upsell -+ * -+ * @package Magento\Bundle\Controller\Adminhtml\Bundle\Product\Edit -+ * @deprecated Not used since upsell products grid moved to UI components. -+ * @see Magento_Catalog::view/adminhtml/ui_component/upsell_product_listing.xml -+ */ -+class Upsell extends CatalogUpsell implements HttpPostActionInterface - { - } -diff --git a/app/code/Magento/Bundle/Controller/Adminhtml/Bundle/Product/Edit/UpsellGrid.php b/app/code/Magento/Bundle/Controller/Adminhtml/Bundle/Product/Edit/UpsellGrid.php -index 74c87be5b71..ed3312d3b07 100644 ---- a/app/code/Magento/Bundle/Controller/Adminhtml/Bundle/Product/Edit/UpsellGrid.php -+++ b/app/code/Magento/Bundle/Controller/Adminhtml/Bundle/Product/Edit/UpsellGrid.php -@@ -6,6 +6,13 @@ - */ - namespace Magento\Bundle\Controller\Adminhtml\Bundle\Product\Edit; - -+/** -+ * Class UpsellGrid -+ * -+ * @package Magento\Bundle\Controller\Adminhtml\Bundle\Product\Edit -+ * @deprecated Not used since upsell products grid moved to UI components. -+ * @see Magento_Catalog::view/adminhtml/ui_component/upsell_product_listing.xml -+ */ - class UpsellGrid extends \Magento\Catalog\Controller\Adminhtml\Product\UpsellGrid - { - } -diff --git a/app/code/Magento/Bundle/Model/OptionRepository.php b/app/code/Magento/Bundle/Model/OptionRepository.php -index 59e658b08df..0b96ea8d5b7 100644 ---- a/app/code/Magento/Bundle/Model/OptionRepository.php -+++ b/app/code/Magento/Bundle/Model/OptionRepository.php -@@ -90,7 +90,7 @@ class OptionRepository implements \Magento\Bundle\Api\ProductOptionRepositoryInt - } - - /** -- * {@inheritdoc} -+ * @inheritdoc - */ - public function get($sku, $optionId) - { -@@ -106,23 +106,24 @@ class OptionRepository implements \Magento\Bundle\Api\ProductOptionRepositoryInt - - $productLinks = $this->linkManagement->getChildren($product->getSku(), $optionId); - -- /** @var \Magento\Bundle\Api\Data\OptionInterface $option */ -+ /** @var \Magento\Bundle\Api\Data\OptionInterface $optionDataObject */ - $optionDataObject = $this->optionFactory->create(); - $this->dataObjectHelper->populateWithArray( - $optionDataObject, - $option->getData(), - \Magento\Bundle\Api\Data\OptionInterface::class - ); -- $optionDataObject->setOptionId($option->getId()) -- ->setTitle($option->getTitle() === null ? $option->getDefaultTitle() : $option->getTitle()) -- ->setSku($product->getSku()) -- ->setProductLinks($productLinks); -+ -+ $optionDataObject->setOptionId($option->getId()); -+ $optionDataObject->setTitle($option->getTitle() === null ? $option->getDefaultTitle() : $option->getTitle()); -+ $optionDataObject->setSku($product->getSku()); -+ $optionDataObject->setProductLinks($productLinks); - - return $optionDataObject; - } - - /** -- * {@inheritdoc} -+ * @inheritdoc - */ - public function getList($sku) - { -@@ -131,6 +132,8 @@ class OptionRepository implements \Magento\Bundle\Api\ProductOptionRepositoryInt - } - - /** -+ * Return list of product options -+ * - * @param ProductInterface $product - * @return \Magento\Bundle\Api\Data\OptionInterface[] - */ -@@ -140,7 +143,7 @@ class OptionRepository implements \Magento\Bundle\Api\ProductOptionRepositoryInt - } - - /** -- * {@inheritdoc} -+ * @inheritdoc - */ - public function delete(\Magento\Bundle\Api\Data\OptionInterface $option) - { -@@ -156,20 +159,19 @@ class OptionRepository implements \Magento\Bundle\Api\ProductOptionRepositoryInt - } - - /** -- * {@inheritdoc} -+ * @inheritdoc - */ - public function deleteById($sku, $optionId) - { -- $product = $this->getProduct($sku); -- $optionCollection = $this->type->getOptionsCollection($product); -- $optionCollection->setIdFilter($optionId); -- $hasBeenDeleted = $this->delete($optionCollection->getFirstItem()); -+ /** @var \Magento\Bundle\Api\Data\OptionInterface $option */ -+ $option = $this->get($sku, $optionId); -+ $hasBeenDeleted = $this->delete($option); - - return $hasBeenDeleted; - } - - /** -- * {@inheritdoc} -+ * @inheritdoc - */ - public function save( - \Magento\Catalog\Api\Data\ProductInterface $product, -@@ -189,6 +191,9 @@ class OptionRepository implements \Magento\Bundle\Api\ProductOptionRepositoryInt - * @param \Magento\Catalog\Api\Data\ProductInterface $product - * @param \Magento\Bundle\Api\Data\OptionInterface $option - * @return $this -+ * @throws InputException -+ * @throws NoSuchEntityException -+ * @throws \Magento\Framework\Exception\CouldNotSaveException - */ - protected function updateOptionSelection( - \Magento\Catalog\Api\Data\ProductInterface $product, -@@ -228,9 +233,12 @@ class OptionRepository implements \Magento\Bundle\Api\ProductOptionRepositoryInt - } - - /** -+ * Retrieve product by SKU -+ * - * @param string $sku - * @return \Magento\Catalog\Api\Data\ProductInterface -- * @throws \Magento\Framework\Exception\InputException -+ * @throws InputException -+ * @throws NoSuchEntityException - */ - private function getProduct($sku) - { -diff --git a/app/code/Magento/Bundle/Model/Plugin/Frontend/Product.php b/app/code/Magento/Bundle/Model/Plugin/Frontend/Product.php -new file mode 100644 -index 00000000000..499f0cd2ca9 ---- /dev/null -+++ b/app/code/Magento/Bundle/Model/Plugin/Frontend/Product.php -@@ -0,0 +1,48 @@ -+type = $type; -+ } -+ -+ /** -+ * Add child identities to product identities -+ * -+ * @param CatalogProduct $product -+ * @param array $identities -+ * @return array -+ */ -+ public function afterGetIdentities(CatalogProduct $product, array $identities): array -+ { -+ foreach ($this->type->getChildrenIds($product->getEntityId()) as $childIds) { -+ foreach ($childIds as $childId) { -+ $identities[] = CatalogProduct::CACHE_TAG . '_' . $childId; -+ } -+ } -+ -+ return array_unique($identities); -+ } -+} -diff --git a/app/code/Magento/Bundle/Model/Product/Price.php b/app/code/Magento/Bundle/Model/Product/Price.php -index 00b6b2d7a3f..0e75ad2dc82 100644 ---- a/app/code/Magento/Bundle/Model/Product/Price.php -+++ b/app/code/Magento/Bundle/Model/Product/Price.php -@@ -11,8 +11,11 @@ use Magento\Framework\App\ObjectManager; - use Magento\Catalog\Api\Data\ProductTierPriceExtensionFactory; - - /** -+ * Bundle product type price model -+ * - * @api - * @SuppressWarnings(PHPMD.CouplingBetweenObjects) -+ * @SuppressWarnings(PHPMD.CookieAndSessionMisuse) - * @since 100.0.2 - */ - class Price extends \Magento\Catalog\Model\Product\Type\Price -@@ -180,9 +183,9 @@ class Price extends \Magento\Catalog\Model\Product\Type\Price - /** - * Get product final price - * -- * @param float $qty -- * @param \Magento\Catalog\Model\Product $product -- * @return float -+ * @param float $qty -+ * @param \Magento\Catalog\Model\Product $product -+ * @return float - */ - public function getFinalPrice($qty, $product) - { -@@ -207,9 +210,9 @@ class Price extends \Magento\Catalog\Model\Product\Type\Price - * Returns final price of a child product - * - * @param \Magento\Catalog\Model\Product $product -- * @param float $productQty -+ * @param float $productQty - * @param \Magento\Catalog\Model\Product $childProduct -- * @param float $childProductQty -+ * @param float $childProductQty - * @return float - */ - public function getChildFinalPrice($product, $productQty, $childProduct, $childProductQty) -@@ -220,10 +223,10 @@ class Price extends \Magento\Catalog\Model\Product\Type\Price - /** - * Retrieve Price considering tier price - * -- * @param \Magento\Catalog\Model\Product $product -- * @param string|null $which -- * @param bool|null $includeTax -- * @param bool $takeTierPrice -+ * @param \Magento\Catalog\Model\Product $product -+ * @param string|null $which -+ * @param bool|null $includeTax -+ * @param bool $takeTierPrice - * @return float|array - * @SuppressWarnings(PHPMD.CyclomaticComplexity) - * @SuppressWarnings(PHPMD.NPathComplexity) -@@ -255,67 +258,68 @@ class Price extends \Magento\Catalog\Model\Product\Type\Price - foreach ($options as $option) { - /* @var $option \Magento\Bundle\Model\Option */ - $selections = $option->getSelections(); -- if ($selections) { -- $selectionMinimalPrices = []; -- $selectionMaximalPrices = []; -- -- foreach ($option->getSelections() as $selection) { -- /* @var $selection \Magento\Bundle\Model\Selection */ -- if (!$selection->isSalable()) { -- /** -- * @todo CatalogInventory Show out of stock Products -- */ -- continue; -- } -- -- $qty = $selection->getSelectionQty(); -- -- $item = $product->getPriceType() == self::PRICE_TYPE_FIXED ? $product : $selection; -- -- $selectionMinimalPrices[] = $this->_catalogData->getTaxPrice( -- $item, -- $this->getSelectionFinalTotalPrice( -- $product, -- $selection, -- 1, -- $qty, -- true, -- $takeTierPrice -- ), -- $includeTax -- ); -- $selectionMaximalPrices[] = $this->_catalogData->getTaxPrice( -- $item, -- $this->getSelectionFinalTotalPrice( -- $product, -- $selection, -- 1, -- null, -- true, -- $takeTierPrice -- ), -- $includeTax -+ if (empty($selections)) { -+ continue; -+ } -+ $selectionMinimalPrices = []; -+ $selectionMaximalPrices = []; -+ -+ foreach ($option->getSelections() as $selection) { -+ /* @var $selection \Magento\Bundle\Model\Selection */ -+ if (!$selection->isSalable()) { -+ /** -+ * @todo CatalogInventory Show out of stock Products -+ */ -+ continue; -+ } -+ -+ $qty = $selection->getSelectionQty(); -+ -+ $item = $product->getPriceType() == self::PRICE_TYPE_FIXED ? $product : $selection; -+ -+ $selectionMinimalPrices[] = $this->_catalogData->getTaxPrice( -+ $item, -+ $this->getSelectionFinalTotalPrice( -+ $product, -+ $selection, -+ 1, -+ $qty, -+ true, -+ $takeTierPrice -+ ), -+ $includeTax -+ ); -+ $selectionMaximalPrices[] = $this->_catalogData->getTaxPrice( -+ $item, -+ $this->getSelectionFinalTotalPrice( -+ $product, -+ $selection, -+ 1, -+ null, -+ true, -+ $takeTierPrice -+ ), -+ $includeTax -+ ); -+ } -+ -+ if (count($selectionMinimalPrices)) { -+ $selMinPrice = min($selectionMinimalPrices); -+ if ($option->getRequired()) { -+ $minimalPrice += $selMinPrice; -+ $minPriceFounded = true; -+ } elseif (true !== $minPriceFounded) { -+ $selMinPrice += $minimalPrice; -+ $minPriceFounded = false === $minPriceFounded ? $selMinPrice : min( -+ $minPriceFounded, -+ $selMinPrice - ); - } - -- if (count($selectionMinimalPrices)) { -- $selMinPrice = min($selectionMinimalPrices); -- if ($option->getRequired()) { -- $minimalPrice += $selMinPrice; -- $minPriceFounded = true; -- } elseif (true !== $minPriceFounded) { -- $selMinPrice += $minimalPrice; -- $minPriceFounded = false === $minPriceFounded ? $selMinPrice : min( -- $minPriceFounded, -- $selMinPrice -- ); -- } -- -- if ($option->isMultiSelection()) { -- $maximalPrice += array_sum($selectionMaximalPrices); -- } else { -- $maximalPrice += max($selectionMaximalPrices); -- } -+ if ($option->isMultiSelection()) { -+ $maximalPrice += array_sum($selectionMaximalPrices); -+ } else { -+ $maximalPrice += max($selectionMaximalPrices); - } - } - } -@@ -338,23 +342,25 @@ class Price extends \Magento\Catalog\Model\Product\Type\Price - - $prices[] = $valuePrice; - } -- if (count($prices)) { -- if ($customOption->getIsRequire()) { -- $minimalPrice += $this->_catalogData->getTaxPrice($product, min($prices), $includeTax); -- } -- -- $multiTypes = [ -- \Magento\Catalog\Api\Data\ProductCustomOptionInterface::OPTION_TYPE_CHECKBOX, -- \Magento\Catalog\Api\Data\ProductCustomOptionInterface::OPTION_TYPE_MULTIPLE, -- ]; -- -- if (in_array($customOption->getType(), $multiTypes)) { -- $maximalValue = array_sum($prices); -- } else { -- $maximalValue = max($prices); -- } -- $maximalPrice += $this->_catalogData->getTaxPrice($product, $maximalValue, $includeTax); -+ if (empty($prices)) { -+ continue; -+ } -+ -+ if ($customOption->getIsRequire()) { -+ $minimalPrice += $this->_catalogData->getTaxPrice($product, min($prices), $includeTax); -+ } -+ -+ $multiTypes = [ -+ \Magento\Catalog\Api\Data\ProductCustomOptionInterface::OPTION_TYPE_CHECKBOX, -+ \Magento\Catalog\Api\Data\ProductCustomOptionInterface::OPTION_TYPE_MULTIPLE, -+ ]; -+ -+ if (in_array($customOption->getType(), $multiTypes)) { -+ $maximalValue = array_sum($prices); -+ } else { -+ $maximalValue = max($prices); - } -+ $maximalPrice += $this->_catalogData->getTaxPrice($product, $maximalValue, $includeTax); - } else { - $valuePrice = $customOption->getPrice(true); - -@@ -402,8 +408,8 @@ class Price extends \Magento\Catalog\Model\Product\Type\Price - * - * @param \Magento\Catalog\Model\Product $bundleProduct - * @param \Magento\Catalog\Model\Product $selectionProduct -- * @param float|null $selectionQty -- * @param null|bool $multiplyQty Whether to multiply selection's price by its quantity -+ * @param float|null $selectionQty -+ * @param null|bool $multiplyQty Whether to multiply selection's price by its quantity - * @return float - * - * @see \Magento\Bundle\Model\Product\Price::getSelectionFinalTotalPrice() -@@ -418,7 +424,7 @@ class Price extends \Magento\Catalog\Model\Product\Type\Price - * - * @param \Magento\Catalog\Model\Product $bundleProduct - * @param \Magento\Catalog\Model\Product $selectionProduct -- * @param float $qty -+ * @param float $qty - * @return float - */ - public function getSelectionPreFinalPrice($bundleProduct, $selectionProduct, $qty = null) -@@ -427,15 +433,14 @@ class Price extends \Magento\Catalog\Model\Product\Type\Price - } - - /** -- * Calculate final price of selection -- * with take into account tier price -+ * Calculate final price of selection with take into account tier price - * -- * @param \Magento\Catalog\Model\Product $bundleProduct -- * @param \Magento\Catalog\Model\Product $selectionProduct -- * @param float $bundleQty -- * @param float $selectionQty -- * @param bool $multiplyQty -- * @param bool $takeTierPrice -+ * @param \Magento\Catalog\Model\Product $bundleProduct -+ * @param \Magento\Catalog\Model\Product $selectionProduct -+ * @param float $bundleQty -+ * @param float $selectionQty -+ * @param bool $multiplyQty -+ * @param bool $takeTierPrice - * @return float - */ - public function getSelectionFinalTotalPrice( -@@ -454,7 +459,11 @@ class Price extends \Magento\Catalog\Model\Product\Type\Price - } - - if ($bundleProduct->getPriceType() == self::PRICE_TYPE_DYNAMIC) { -- $price = $selectionProduct->getFinalPrice($takeTierPrice ? $selectionQty : 1); -+ $totalQty = $bundleQty * $selectionQty; -+ if (!$takeTierPrice || $totalQty === 0) { -+ $totalQty = 1; -+ } -+ $price = $selectionProduct->getFinalPrice($totalQty); - } else { - if ($selectionProduct->getSelectionPriceType()) { - // percent -@@ -485,10 +494,10 @@ class Price extends \Magento\Catalog\Model\Product\Type\Price - /** - * Apply tier price for bundle - * -- * @param \Magento\Catalog\Model\Product $product -- * @param float $qty -- * @param float $finalPrice -- * @return float -+ * @param \Magento\Catalog\Model\Product $product -+ * @param float $qty -+ * @param float $finalPrice -+ * @return float - */ - protected function _applyTierPrice($product, $qty, $finalPrice) - { -@@ -509,9 +518,9 @@ class Price extends \Magento\Catalog\Model\Product\Type\Price - /** - * Get product tier price by qty - * -- * @param float $qty -- * @param \Magento\Catalog\Model\Product $product -- * @return float|array -+ * @param float $qty -+ * @param \Magento\Catalog\Model\Product $product -+ * @return float|array - * @SuppressWarnings(PHPMD.CyclomaticComplexity) - * @SuppressWarnings(PHPMD.NPathComplexity) - */ -@@ -605,11 +614,11 @@ class Price extends \Magento\Catalog\Model\Product\Type\Price - /** - * Calculate and apply special price - * -- * @param float $finalPrice -- * @param float $specialPrice -+ * @param float $finalPrice -+ * @param float $specialPrice - * @param string $specialPriceFrom - * @param string $specialPriceTo -- * @param mixed $store -+ * @param mixed $store - * @return float - */ - public function calculateSpecialPrice( -@@ -634,7 +643,7 @@ class Price extends \Magento\Catalog\Model\Product\Type\Price - * - * @param /Magento/Catalog/Model/Product $bundleProduct - * @param float|string $price -- * @param int $bundleQty -+ * @param int $bundleQty - * @return float - */ - public function getLowestPrice($bundleProduct, $price, $bundleQty = 1) -diff --git a/app/code/Magento/Bundle/Model/Product/SaveHandler.php b/app/code/Magento/Bundle/Model/Product/SaveHandler.php -index fc215aa6b8e..99e8188146b 100644 ---- a/app/code/Magento/Bundle/Model/Product/SaveHandler.php -+++ b/app/code/Magento/Bundle/Model/Product/SaveHandler.php -@@ -58,6 +58,8 @@ class SaveHandler implements ExtensionInterface - } - - /** -+ * Perform action on Bundle product relation/extension attribute -+ * - * @param object $entity - * @param array $arguments - * -@@ -83,7 +85,7 @@ class SaveHandler implements ExtensionInterface - : []; - - if (!$entity->getCopyFromView()) { -- $this->processRemovedOptions($entity->getSku(), $existingOptionsIds, $optionIds); -+ $this->processRemovedOptions($entity, $existingOptionsIds, $optionIds); - $newOptionsIds = array_diff($optionIds, $existingOptionsIds); - $this->saveOptions($entity, $bundleProductOptions, $newOptionsIds); - } else { -@@ -96,6 +98,8 @@ class SaveHandler implements ExtensionInterface - } - - /** -+ * Remove option product links -+ * - * @param string $entitySku - * @param \Magento\Bundle\Api\Data\OptionInterface $option - * @return void -@@ -154,16 +158,19 @@ class SaveHandler implements ExtensionInterface - /** - * Removes old options that no longer exists. - * -- * @param string $entitySku -+ * @param ProductInterface $entity - * @param array $existingOptionsIds - * @param array $optionIds - * @return void - */ -- private function processRemovedOptions(string $entitySku, array $existingOptionsIds, array $optionIds): void -+ private function processRemovedOptions(ProductInterface $entity, array $existingOptionsIds, array $optionIds): void - { -+ $metadata = $this->metadataPool->getMetadata(ProductInterface::class); -+ $parentId = $entity->getData($metadata->getLinkField()); - foreach (array_diff($existingOptionsIds, $optionIds) as $optionId) { -- $option = $this->optionRepository->get($entitySku, $optionId); -- $this->removeOptionLinks($entitySku, $option); -+ $option = $this->optionRepository->get($entity->getSku(), $optionId); -+ $option->setParentId($parentId); -+ $this->removeOptionLinks($entity->getSku(), $option); - $this->optionRepository->delete($option); - } - } -diff --git a/app/code/Magento/Bundle/Model/Product/Type.php b/app/code/Magento/Bundle/Model/Product/Type.php -index 17ecba545ef..2dc519dbf15 100644 ---- a/app/code/Magento/Bundle/Model/Product/Type.php -+++ b/app/code/Magento/Bundle/Model/Product/Type.php -@@ -6,13 +6,14 @@ - - namespace Magento\Bundle\Model\Product; - --use Magento\Framework\App\ObjectManager; -+use Magento\Bundle\Model\ResourceModel\Selection\Collection as Selections; -+use Magento\Bundle\Model\ResourceModel\Selection\Collection\FilterApplier as SelectionCollectionFilterApplier; - use Magento\Catalog\Api\ProductRepositoryInterface; -+use Magento\Framework\App\ObjectManager; -+use Magento\Framework\EntityManager\MetadataPool; - use Magento\Framework\Pricing\PriceCurrencyInterface; - use Magento\Framework\Serialize\Serializer\Json; --use Magento\Framework\EntityManager\MetadataPool; --use Magento\Bundle\Model\ResourceModel\Selection\Collection\FilterApplier as SelectionCollectionFilterApplier; --use Magento\Bundle\Model\ResourceModel\Selection\Collection as Selections; -+use Magento\Framework\Stdlib\ArrayUtils; - - /** - * Bundle Type Model -@@ -160,6 +161,11 @@ class Type extends \Magento\Catalog\Model\Product\Type\AbstractType - */ - private $selectionCollectionFilterApplier; - -+ /** -+ * @var ArrayUtils -+ */ -+ private $arrayUtility; -+ - /** - * @param \Magento\Catalog\Model\Product\Option $catalogProductOption - * @param \Magento\Eav\Model\Config $eavConfig -@@ -185,6 +191,7 @@ class Type extends \Magento\Catalog\Model\Product\Type\AbstractType - * @param \Magento\Framework\Serialize\Serializer\Json $serializer - * @param MetadataPool|null $metadataPool - * @param SelectionCollectionFilterApplier|null $selectionCollectionFilterApplier -+ * @param ArrayUtils|null $arrayUtility - * - * @SuppressWarnings(PHPMD.ExcessiveParameterList) - */ -@@ -212,7 +219,8 @@ class Type extends \Magento\Catalog\Model\Product\Type\AbstractType - \Magento\CatalogInventory\Api\StockStateInterface $stockState, - Json $serializer = null, - MetadataPool $metadataPool = null, -- SelectionCollectionFilterApplier $selectionCollectionFilterApplier = null -+ SelectionCollectionFilterApplier $selectionCollectionFilterApplier = null, -+ ArrayUtils $arrayUtility = null - ) { - $this->_catalogProduct = $catalogProduct; - $this->_catalogData = $catalogData; -@@ -232,6 +240,7 @@ class Type extends \Magento\Catalog\Model\Product\Type\AbstractType - - $this->selectionCollectionFilterApplier = $selectionCollectionFilterApplier - ?: ObjectManager::getInstance()->get(SelectionCollectionFilterApplier::class); -+ $this->arrayUtility= $arrayUtility ?: ObjectManager::getInstance()->get(ArrayUtils::class); - - parent::__construct( - $catalogProductOption, -@@ -308,8 +317,11 @@ class Type extends \Magento\Catalog\Model\Product\Type\AbstractType - $selectionIds = $this->serializer->unserialize($customOption->getValue()); - if (!empty($selectionIds)) { - $selections = $this->getSelectionsByIds($selectionIds, $product); -- foreach ($selections->getItems() as $selection) { -- $skuParts[] = $selection->getSku(); -+ foreach ($selectionIds as $selectionId) { -+ $entity = $selections->getItemByColumnValue('selection_id', $selectionId); -+ if (isset($entity) && $entity->getEntityId()) { -+ $skuParts[] = $entity->getSku(); -+ } - } - } - } -@@ -537,7 +549,7 @@ class Type extends \Magento\Catalog\Model\Product\Type\AbstractType - foreach ($options as $quoteItemOption) { - if ($quoteItemOption->getCode() == 'selection_qty_' . $selection->getSelectionId()) { - if ($optionUpdateFlag) { -- $quoteItemOption->setValue(intval($quoteItemOption->getValue())); -+ $quoteItemOption->setValue((int) $quoteItemOption->getValue()); - } else { - $quoteItemOption->setValue($value); - } -@@ -559,7 +571,7 @@ class Type extends \Magento\Catalog\Model\Product\Type\AbstractType - */ - public function prepareQuoteItemQty($qty, $product) - { -- return intval($qty); -+ return (int) $qty; - } - - /** -@@ -625,6 +637,7 @@ class Type extends \Magento\Catalog\Model\Product\Type\AbstractType - - /** - * Prepare product and its configuration to be added to some products list. -+ * - * Perform standard preparation process and then prepare of bundle selections options. - * - * @param \Magento\Framework\DataObject $buyRequest -@@ -669,7 +682,7 @@ class Type extends \Magento\Catalog\Model\Product\Type\AbstractType - $options - ); - -- $selectionIds = $this->multiToFlatArray($options); -+ $selectionIds = array_values($this->arrayUtility->flatten($options)); - // If product has not been configured yet then $selections array should be empty - if (!empty($selectionIds)) { - $selections = $this->getSelectionsByIds($selectionIds, $product); -@@ -733,9 +746,9 @@ class Type extends \Magento\Catalog\Model\Product\Type\AbstractType - * for selection (not for all bundle) - */ - $price = $product->getPriceModel() -- ->getSelectionFinalTotalPrice($product, $selection, 0, $qty); -+ ->getSelectionFinalTotalPrice($product, $selection, 0, 1); - $attributes = [ -- 'price' => $this->priceCurrency->convert($price), -+ 'price' => $price, - 'qty' => $qty, - 'option_label' => $selection->getOption() - ->getTitle(), -@@ -790,6 +803,8 @@ class Type extends \Magento\Catalog\Model\Product\Type\AbstractType - } - - /** -+ * Cast array values to int -+ * - * @param array $array - * @return int[]|int[][] - */ -@@ -808,24 +823,6 @@ class Type extends \Magento\Catalog\Model\Product\Type\AbstractType - return $array; - } - -- /** -- * @param array $array -- * @return int[] -- */ -- private function multiToFlatArray(array $array) -- { -- $flatArray = []; -- foreach ($array as $key => $value) { -- if (is_array($value)) { -- $flatArray = array_merge($flatArray, $this->multiToFlatArray($value)); -- } else { -- $flatArray[$key] = $value; -- } -- } -- -- return $flatArray; -- } -- - /** - * Retrieve message for specify option(s) - * -@@ -920,8 +917,7 @@ class Type extends \Magento\Catalog\Model\Product\Type\AbstractType - } - - /** -- * Prepare additional options/information for order item which will be -- * created from this product -+ * Prepare additional options/information for order item which will be created from this product - * - * @param \Magento\Catalog\Model\Product $product - * @return array -@@ -987,6 +983,7 @@ class Type extends \Magento\Catalog\Model\Product\Type\AbstractType - - /** - * Sort selections method for usort function -+ * - * Sort selections by option position, selection position and selection id - * - * @param \Magento\Catalog\Model\Product $firstItem -@@ -1009,10 +1006,8 @@ class Type extends \Magento\Catalog\Model\Product\Type\AbstractType - $secondItem->getPosition(), - $secondItem->getSelectionId(), - ]; -- if ($aPosition == $bPosition) { -- return 0; -- } -- return $aPosition < $bPosition ? -1 : 1; -+ -+ return $aPosition <=> $bPosition; - } - - /** -@@ -1050,6 +1045,7 @@ class Type extends \Magento\Catalog\Model\Product\Type\AbstractType - - /** - * Retrieve additional searchable data from type instance -+ * - * Using based on product id and store_id data - * - * @param \Magento\Catalog\Model\Product $product -@@ -1118,6 +1114,7 @@ class Type extends \Magento\Catalog\Model\Product\Type\AbstractType - - /** - * Retrieve products divided into groups required to purchase -+ * - * At least one product in each group has to be purchased - * - * @param \Magento\Catalog\Model\Product $product -@@ -1214,6 +1211,8 @@ class Type extends \Magento\Catalog\Model\Product\Type\AbstractType - } - - /** -+ * Returns selection qty -+ * - * @param \Magento\Framework\DataObject $selection - * @param int[] $qtys - * @param int $selectionOptionId -@@ -1232,6 +1231,8 @@ class Type extends \Magento\Catalog\Model\Product\Type\AbstractType - } - - /** -+ * Returns qty -+ * - * @param \Magento\Catalog\Model\Product $product - * @param \Magento\Framework\DataObject $selection - * @return float|int -@@ -1249,6 +1250,8 @@ class Type extends \Magento\Catalog\Model\Product\Type\AbstractType - } - - /** -+ * Validate required options -+ * - * @param \Magento\Catalog\Model\Product $product - * @param bool $isStrictProcessMode - * @param \Magento\Bundle\Model\ResourceModel\Option\Collection $optionsCollection -@@ -1270,6 +1273,8 @@ class Type extends \Magento\Catalog\Model\Product\Type\AbstractType - } - - /** -+ * Check if selection is salable -+ * - * @param \Magento\Bundle\Model\ResourceModel\Selection\Collection $selections - * @param bool $skipSaleableCheck - * @param \Magento\Bundle\Model\ResourceModel\Option\Collection $optionsCollection -@@ -1300,6 +1305,8 @@ class Type extends \Magento\Catalog\Model\Product\Type\AbstractType - } - - /** -+ * Validate result -+ * - * @param array $_result - * @return void - * @throws \Magento\Framework\Exception\LocalizedException -@@ -1318,6 +1325,8 @@ class Type extends \Magento\Catalog\Model\Product\Type\AbstractType - } - - /** -+ * Merge selections with options -+ * - * @param \Magento\Catalog\Model\Product\Option[] $options - * @param \Magento\Framework\DataObject[] $selections - * @return \Magento\Framework\DataObject[] -diff --git a/app/code/Magento/Bundle/Model/ResourceModel/Indexer/Price.php b/app/code/Magento/Bundle/Model/ResourceModel/Indexer/Price.php -index 0b6e97cfb92..b71853cde41 100644 ---- a/app/code/Magento/Bundle/Model/ResourceModel/Indexer/Price.php -+++ b/app/code/Magento/Bundle/Model/ResourceModel/Indexer/Price.php -@@ -6,20 +6,173 @@ - namespace Magento\Bundle\Model\ResourceModel\Indexer; - - use Magento\Catalog\Api\Data\ProductInterface; -+use Magento\Catalog\Model\ResourceModel\Product\Indexer\Price\BasePriceModifier; -+use Magento\Framework\Indexer\DimensionalIndexerInterface; -+use Magento\Framework\EntityManager\MetadataPool; -+use Magento\Catalog\Model\Indexer\Product\Price\TableMaintainer; -+use Magento\Catalog\Model\ResourceModel\Product\Indexer\Price\IndexTableStructureFactory; -+use Magento\Catalog\Model\ResourceModel\Product\Indexer\Price\IndexTableStructure; -+use Magento\Catalog\Model\ResourceModel\Product\Indexer\Price\Query\JoinAttributeProcessor; -+use Magento\Customer\Model\Indexer\CustomerGroupDimensionProvider; -+use Magento\Store\Model\Indexer\WebsiteDimensionProvider; -+use Magento\Catalog\Model\Product\Attribute\Source\Status; - - /** - * Bundle products Price indexer resource model - * -- * @author Magento Core Team -+ * @SuppressWarnings(PHPMD.CouplingBetweenObjects) - */ --class Price extends \Magento\Catalog\Model\ResourceModel\Product\Indexer\Price\DefaultPrice -+class Price implements DimensionalIndexerInterface - { -+ /** -+ * @var IndexTableStructureFactory -+ */ -+ private $indexTableStructureFactory; -+ -+ /** -+ * @var TableMaintainer -+ */ -+ private $tableMaintainer; -+ -+ /** -+ * @var MetadataPool -+ */ -+ private $metadataPool; -+ -+ /** -+ * @var \Magento\Framework\App\ResourceConnection -+ */ -+ private $resource; -+ -+ /** -+ * @var bool -+ */ -+ private $fullReindexAction; -+ -+ /** -+ * @var string -+ */ -+ private $connectionName; -+ -+ /** -+ * @var \Magento\Framework\DB\Adapter\AdapterInterface -+ */ -+ private $connection; -+ -+ /** -+ * Mapping between dimensions and field in database -+ * -+ * @var array -+ */ -+ private $dimensionToFieldMapper = [ -+ WebsiteDimensionProvider::DIMENSION_NAME => 'pw.website_id', -+ CustomerGroupDimensionProvider::DIMENSION_NAME => 'cg.customer_group_id', -+ ]; -+ -+ /** -+ * @var BasePriceModifier -+ */ -+ private $basePriceModifier; -+ -+ /** -+ * @var JoinAttributeProcessor -+ */ -+ private $joinAttributeProcessor; -+ -+ /** -+ * @var \Magento\Framework\Event\ManagerInterface -+ */ -+ private $eventManager; -+ -+ /** -+ * @var \Magento\Framework\Module\ModuleManagerInterface -+ */ -+ private $moduleManager; -+ -+ /** -+ * @param IndexTableStructureFactory $indexTableStructureFactory -+ * @param TableMaintainer $tableMaintainer -+ * @param MetadataPool $metadataPool -+ * @param \Magento\Framework\App\ResourceConnection $resource -+ * @param BasePriceModifier $basePriceModifier -+ * @param JoinAttributeProcessor $joinAttributeProcessor -+ * @param \Magento\Framework\Event\ManagerInterface $eventManager -+ * @param \Magento\Framework\Module\ModuleManagerInterface $moduleManager -+ * @param bool $fullReindexAction -+ * @param string $connectionName -+ * -+ * @SuppressWarnings(PHPMD.ExcessiveParameterList) -+ */ -+ public function __construct( -+ IndexTableStructureFactory $indexTableStructureFactory, -+ TableMaintainer $tableMaintainer, -+ MetadataPool $metadataPool, -+ \Magento\Framework\App\ResourceConnection $resource, -+ BasePriceModifier $basePriceModifier, -+ JoinAttributeProcessor $joinAttributeProcessor, -+ \Magento\Framework\Event\ManagerInterface $eventManager, -+ \Magento\Framework\Module\ModuleManagerInterface $moduleManager, -+ $fullReindexAction = false, -+ $connectionName = 'indexer' -+ ) { -+ $this->indexTableStructureFactory = $indexTableStructureFactory; -+ $this->tableMaintainer = $tableMaintainer; -+ $this->connectionName = $connectionName; -+ $this->metadataPool = $metadataPool; -+ $this->resource = $resource; -+ $this->fullReindexAction = $fullReindexAction; -+ $this->basePriceModifier = $basePriceModifier; -+ $this->joinAttributeProcessor = $joinAttributeProcessor; -+ $this->eventManager = $eventManager; -+ $this->moduleManager = $moduleManager; -+ } -+ - /** - * @inheritdoc -+ * @param array $dimensions -+ * @param \Traversable $entityIds -+ * @throws \Exception - */ -- protected function reindex($entityIds = null) -+ public function executeByDimensions(array $dimensions, \Traversable $entityIds) - { -- $this->_prepareBundlePrice($entityIds); -+ $this->tableMaintainer->createMainTmpTable($dimensions); -+ -+ $temporaryPriceTable = $this->indexTableStructureFactory->create( -+ [ -+ 'tableName' => $this->tableMaintainer->getMainTmpTable($dimensions), -+ 'entityField' => 'entity_id', -+ 'customerGroupField' => 'customer_group_id', -+ 'websiteField' => 'website_id', -+ 'taxClassField' => 'tax_class_id', -+ 'originalPriceField' => 'price', -+ 'finalPriceField' => 'final_price', -+ 'minPriceField' => 'min_price', -+ 'maxPriceField' => 'max_price', -+ 'tierPriceField' => 'tier_price', -+ ] -+ ); -+ -+ $entityIds = iterator_to_array($entityIds); -+ -+ $this->prepareTierPriceIndex($dimensions, $entityIds); -+ -+ $this->prepareBundlePriceTable(); -+ -+ $this->prepareBundlePriceByType( -+ \Magento\Bundle\Model\Product\Price::PRICE_TYPE_FIXED, -+ $dimensions, -+ $entityIds -+ ); -+ -+ $this->prepareBundlePriceByType( -+ \Magento\Bundle\Model\Product\Price::PRICE_TYPE_DYNAMIC, -+ $dimensions, -+ $entityIds -+ ); -+ -+ $this->calculateBundleOptionPrice($temporaryPriceTable, $dimensions); -+ -+ $this->basePriceModifier->modifyPrice($temporaryPriceTable, $entityIds); - } - - /** -@@ -27,9 +180,9 @@ class Price extends \Magento\Catalog\Model\ResourceModel\Product\Indexer\Price\D - * - * @return string - */ -- protected function _getBundlePriceTable() -+ private function getBundlePriceTable() - { -- return $this->tableStrategy->getTableName('catalog_product_index_price_bundle'); -+ return $this->getTable('catalog_product_index_price_bundle_tmp'); - } - - /** -@@ -37,9 +190,9 @@ class Price extends \Magento\Catalog\Model\ResourceModel\Product\Indexer\Price\D - * - * @return string - */ -- protected function _getBundleSelectionTable() -+ private function getBundleSelectionTable() - { -- return $this->tableStrategy->getTableName('catalog_product_index_price_bundle_sel'); -+ return $this->getTable('catalog_product_index_price_bundle_sel_tmp'); - } - - /** -@@ -47,9 +200,9 @@ class Price extends \Magento\Catalog\Model\ResourceModel\Product\Indexer\Price\D - * - * @return string - */ -- protected function _getBundleOptionTable() -+ private function getBundleOptionTable() - { -- return $this->tableStrategy->getTableName('catalog_product_index_price_bundle_opt'); -+ return $this->getTable('catalog_product_index_price_bundle_opt_tmp'); - } - - /** -@@ -57,9 +210,9 @@ class Price extends \Magento\Catalog\Model\ResourceModel\Product\Indexer\Price\D - * - * @return $this - */ -- protected function _prepareBundlePriceTable() -+ private function prepareBundlePriceTable() - { -- $this->getConnection()->delete($this->_getBundlePriceTable()); -+ $this->getConnection()->delete($this->getBundlePriceTable()); - return $this; - } - -@@ -68,9 +221,9 @@ class Price extends \Magento\Catalog\Model\ResourceModel\Product\Indexer\Price\D - * - * @return $this - */ -- protected function _prepareBundleSelectionTable() -+ private function prepareBundleSelectionTable() - { -- $this->getConnection()->delete($this->_getBundleSelectionTable()); -+ $this->getConnection()->delete($this->getBundleSelectionTable()); - return $this; - } - -@@ -79,9 +232,9 @@ class Price extends \Magento\Catalog\Model\ResourceModel\Product\Indexer\Price\D - * - * @return $this - */ -- protected function _prepareBundleOptionTable() -+ private function prepareBundleOptionTable() - { -- $this->getConnection()->delete($this->_getBundleOptionTable()); -+ $this->getConnection()->delete($this->getBundleOptionTable()); - return $this; - } - -@@ -89,51 +242,58 @@ class Price extends \Magento\Catalog\Model\ResourceModel\Product\Indexer\Price\D - * Prepare temporary price index data for bundle products by price type - * - * @param int $priceType -+ * @param array $dimensions - * @param int|array $entityIds the entity ids limitation -- * @return $this -+ * @return void -+ * @throws \Exception - * @SuppressWarnings(PHPMD.ExcessiveMethodLength) - */ -- protected function _prepareBundlePriceByType($priceType, $entityIds = null) -+ private function prepareBundlePriceByType($priceType, array $dimensions, $entityIds = null) - { - $connection = $this->getConnection(); -- $table = $this->_getBundlePriceTable(); -- - $select = $connection->select()->from( - ['e' => $this->getTable('catalog_product_entity')], - ['entity_id'] -- )->join( -+ )->joinInner( - ['cg' => $this->getTable('customer_group')], -- '', -+ array_key_exists(CustomerGroupDimensionProvider::DIMENSION_NAME, $dimensions) -+ ? sprintf( -+ '%s = %s', -+ $this->dimensionToFieldMapper[CustomerGroupDimensionProvider::DIMENSION_NAME], -+ $dimensions[CustomerGroupDimensionProvider::DIMENSION_NAME]->getValue() -+ ) : '', - ['customer_group_id'] -- ); -- $this->_addWebsiteJoinToSelect($select, true); -- $this->_addProductWebsiteJoinToSelect($select, 'cw.website_id', "e.entity_id"); -- $select->columns( -- 'website_id', -- 'cw' -- )->join( -- ['cwd' => $this->_getWebsiteDateTable()], -- 'cw.website_id = cwd.website_id', -+ )->joinInner( -+ ['pw' => $this->getTable('catalog_product_website')], -+ 'pw.product_id = e.entity_id', -+ ['pw.website_id'] -+ )->joinInner( -+ ['cwd' => $this->getTable('catalog_product_index_website')], -+ 'pw.website_id = cwd.website_id', - [] -- )->joinLeft( -- ['tp' => $this->_getTierPriceIndexTable()], -- 'tp.entity_id = e.entity_id AND tp.website_id = cw.website_id' . -+ ); -+ $select->joinLeft( -+ ['tp' => $this->getTable('catalog_product_index_tier_price')], -+ 'tp.entity_id = e.entity_id AND tp.website_id = pw.website_id' . - ' AND tp.customer_group_id = cg.customer_group_id', - [] - )->where( - 'e.type_id=?', -- $this->getTypeId() -+ \Magento\Bundle\Ui\DataProvider\Product\Listing\Collector\BundlePrice::PRODUCT_TYPE - ); - -- // add enable products limitation -- $statusCond = $connection->quoteInto( -- '=?', -- \Magento\Catalog\Model\Product\Attribute\Source\Status::STATUS_ENABLED -- ); -- $linkField = $this->getMetadataPool()->getMetadata(ProductInterface::class)->getLinkField(); -- $this->_addAttributeToSelect($select, 'status', "e.$linkField", 'cs.store_id', $statusCond, true); -+ foreach ($dimensions as $dimension) { -+ if (!isset($this->dimensionToFieldMapper[$dimension->getName()])) { -+ throw new \LogicException( -+ 'Provided dimension is not valid for Price indexer: ' . $dimension->getName() -+ ); -+ } -+ $select->where($this->dimensionToFieldMapper[$dimension->getName()] . ' = ?', $dimension->getValue()); -+ } -+ -+ $this->joinAttributeProcessor->process($select, 'status', Status::STATUS_ENABLED); - if ($this->moduleManager->isEnabled('Magento_Tax')) { -- $taxClassId = $this->_addAttributeToSelect($select, 'tax_class_id', "e.$linkField", 'cs.store_id'); -+ $taxClassId = $this->joinAttributeProcessor->process($select, 'tax_class_id'); - } else { - $taxClassId = new \Zend_Db_Expr('0'); - } -@@ -146,13 +306,12 @@ class Price extends \Magento\Catalog\Model\ResourceModel\Product\Indexer\Price\D - ); - } - -- $priceTypeCond = $connection->quoteInto('=?', $priceType); -- $this->_addAttributeToSelect($select, 'price_type', "e.$linkField", 'cs.store_id', $priceTypeCond); -+ $this->joinAttributeProcessor->process($select, 'price_type', $priceType); - -- $price = $this->_addAttributeToSelect($select, 'price', "e.$linkField", 'cs.store_id'); -- $specialPrice = $this->_addAttributeToSelect($select, 'special_price', "e.$linkField", 'cs.store_id'); -- $specialFrom = $this->_addAttributeToSelect($select, 'special_from_date', "e.$linkField", 'cs.store_id'); -- $specialTo = $this->_addAttributeToSelect($select, 'special_to_date', "e.$linkField", 'cs.store_id'); -+ $price = $this->joinAttributeProcessor->process($select, 'price'); -+ $specialPrice = $this->joinAttributeProcessor->process($select, 'special_price'); -+ $specialFrom = $this->joinAttributeProcessor->process($select, 'special_from_date'); -+ $specialTo = $this->joinAttributeProcessor->process($select, 'special_to_date'); - $currentDate = new \Zend_Db_Expr('cwd.website_date'); - - $specialFromDate = $connection->getDatePartSql($specialFrom); -@@ -174,11 +333,13 @@ class Price extends \Magento\Catalog\Model\ResourceModel\Product\Indexer\Price\D - 'ROUND((1 - ' . $tierExpr . ' / 100) * ' . $price . ', 4)', - 'NULL' - ); -- $finalPrice = $connection->getLeastSql([ -+ $finalPrice = $connection->getLeastSql( -+ [ - $price, - $connection->getIfNullSql($specialPriceExpr, $price), - $connection->getIfNullSql($tierPrice, $price), -- ]); -+ ] -+ ); - } else { - $finalPrice = new \Zend_Db_Expr('0'); - $tierPrice = $connection->getCheckSql($tierExpr . ' IS NOT NULL', '0', 'NULL'); -@@ -205,39 +366,41 @@ class Price extends \Magento\Catalog\Model\ResourceModel\Product\Indexer\Price\D - /** - * Add additional external limitation - */ -- $this->_eventManager->dispatch( -+ $this->eventManager->dispatch( - 'catalog_product_prepare_index_select', - [ - 'select' => $select, - 'entity_field' => new \Zend_Db_Expr('e.entity_id'), -- 'website_field' => new \Zend_Db_Expr('cw.website_id'), -- 'store_field' => new \Zend_Db_Expr('cs.store_id') -+ 'website_field' => new \Zend_Db_Expr('pw.website_id'), -+ 'store_field' => new \Zend_Db_Expr('cwd.default_store_id') - ] - ); - -- $query = $select->insertFromSelect($table); -+ $query = $select->insertFromSelect($this->getBundlePriceTable()); - $connection->query($query); -- -- return $this; - } - - /** - * Calculate fixed bundle product selections price - * -- * @return $this -+ * @param IndexTableStructure $priceTable -+ * @param array $dimensions -+ * -+ * @return void -+ * @throws \Exception - */ -- protected function _calculateBundleOptionPrice() -+ private function calculateBundleOptionPrice($priceTable, $dimensions) - { - $connection = $this->getConnection(); - -- $this->_prepareBundleSelectionTable(); -- $this->_calculateBundleSelectionPrice(\Magento\Bundle\Model\Product\Price::PRICE_TYPE_FIXED); -- $this->_calculateBundleSelectionPrice(\Magento\Bundle\Model\Product\Price::PRICE_TYPE_DYNAMIC); -+ $this->prepareBundleSelectionTable(); -+ $this->calculateBundleSelectionPrice($dimensions, \Magento\Bundle\Model\Product\Price::PRICE_TYPE_FIXED); -+ $this->calculateBundleSelectionPrice($dimensions, \Magento\Bundle\Model\Product\Price::PRICE_TYPE_DYNAMIC); - -- $this->_prepareBundleOptionTable(); -+ $this->prepareBundleOptionTable(); - - $select = $connection->select()->from( -- $this->_getBundleSelectionTable(), -+ $this->getBundleSelectionTable(), - ['entity_id', 'customer_group_id', 'website_id', 'option_id'] - )->group( - ['entity_id', 'customer_group_id', 'website_id', 'option_id'] -@@ -254,24 +417,24 @@ class Price extends \Magento\Catalog\Model\ResourceModel\Product\Indexer\Price\D - ] - ); - -- $query = $select->insertFromSelect($this->_getBundleOptionTable()); -+ $query = $select->insertFromSelect($this->getBundleOptionTable()); - $connection->query($query); - -- $this->_prepareDefaultFinalPriceTable(); -- $this->applyBundlePrice(); -- $this->applyBundleOptionPrice(); -- -- return $this; -+ $this->getConnection()->delete($priceTable->getTableName()); -+ $this->applyBundlePrice($priceTable); -+ $this->applyBundleOptionPrice($priceTable); - } - - /** - * Calculate bundle product selections price by product type - * -+ * @param array $dimensions - * @param int $priceType -- * @return $this -+ * @return void -+ * @throws \Exception - * @SuppressWarnings(PHPMD.ExcessiveMethodLength) - */ -- protected function _calculateBundleSelectionPrice($priceType) -+ private function calculateBundleSelectionPrice($dimensions, $priceType) - { - $connection = $this->getConnection(); - -@@ -312,10 +475,12 @@ class Price extends \Magento\Catalog\Model\ResourceModel\Product\Indexer\Price\D - 'NULL' - ); - -- $priceExpr = $connection->getLeastSql([ -+ $priceExpr = $connection->getLeastSql( -+ [ - $priceExpr, - $connection->getIfNullSql($tierExpr, $priceExpr), -- ]); -+ ] -+ ); - } else { - $price = 'idx.min_price * bs.selection_qty'; - $specialExpr = $connection->getCheckSql( -@@ -328,15 +493,18 @@ class Price extends \Magento\Catalog\Model\ResourceModel\Product\Indexer\Price\D - 'ROUND((1 - i.tier_percent / 100) * ' . $price . ', 4)', - 'NULL' - ); -- $priceExpr = $connection->getLeastSql([ -+ $priceExpr = $connection->getLeastSql( -+ [ - $specialExpr, - $connection->getIfNullSql($tierExpr, $price), -- ]); -+ ] -+ ); - } - -- $linkField = $this->getMetadataPool()->getMetadata(ProductInterface::class)->getLinkField(); -+ $metadata = $this->metadataPool->getMetadata(ProductInterface::class); -+ $linkField = $metadata->getLinkField(); - $select = $connection->select()->from( -- ['i' => $this->_getBundlePriceTable()], -+ ['i' => $this->getBundlePriceTable()], - ['entity_id', 'customer_group_id', 'website_id'] - )->join( - ['parent_product' => $this->getTable('catalog_product_entity')], -@@ -355,7 +523,7 @@ class Price extends \Magento\Catalog\Model\ResourceModel\Product\Indexer\Price\D - 'bs.selection_id = bsp.selection_id AND bsp.website_id = i.website_id', - [''] - )->join( -- ['idx' => $this->getIdxTable()], -+ ['idx' => $this->getMainTable($dimensions)], - 'bs.product_id = idx.entity_id AND i.customer_group_id = idx.customer_group_id' . - ' AND i.website_id = idx.website_id', - [] -@@ -375,49 +543,26 @@ class Price extends \Magento\Catalog\Model\ResourceModel\Product\Indexer\Price\D - ] - ); - -- $query = $select->insertFromSelect($this->_getBundleSelectionTable()); -+ $query = $select->insertFromSelect($this->getBundleSelectionTable()); - $connection->query($query); -- -- return $this; -- } -- -- /** -- * Prepare temporary index price for bundle products -- * -- * @param int|array $entityIds the entity ids limitation -- * @return $this -- */ -- protected function _prepareBundlePrice($entityIds = null) -- { -- if (!$this->hasEntity() && empty($entityIds)) { -- return $this; -- } -- $this->_prepareTierPriceIndex($entityIds); -- $this->_prepareBundlePriceTable(); -- $this->_prepareBundlePriceByType(\Magento\Bundle\Model\Product\Price::PRICE_TYPE_FIXED, $entityIds); -- $this->_prepareBundlePriceByType(\Magento\Bundle\Model\Product\Price::PRICE_TYPE_DYNAMIC, $entityIds); -- -- $this->_calculateBundleOptionPrice(); -- $this->_applyCustomOption(); -- -- $this->_movePriceDataToIndexTable(); -- -- return $this; - } - - /** - * Prepare percentage tier price for bundle products - * -- * @param int|array $entityIds -- * @return $this -+ * @param array $dimensions -+ * @param array $entityIds -+ * @return void -+ * @throws \Exception - */ -- protected function _prepareTierPriceIndex($entityIds = null) -+ private function prepareTierPriceIndex($dimensions, $entityIds) - { - $connection = $this->getConnection(); -- $linkField = $this->getMetadataPool()->getMetadata(ProductInterface::class)->getLinkField(); -+ $metadata = $this->metadataPool->getMetadata(ProductInterface::class); -+ $linkField = $metadata->getLinkField(); - // remove index by bundle products - $select = $connection->select()->from( -- ['i' => $this->_getTierPriceIndexTable()], -+ ['i' => $this->getTable('catalog_product_index_tier_price')], - null - )->join( - ['e' => $this->getTable('catalog_product_entity')], -@@ -425,7 +570,7 @@ class Price extends \Magento\Catalog\Model\ResourceModel\Product\Indexer\Price\D - [] - )->where( - 'e.type_id=?', -- $this->getTypeId() -+ \Magento\Bundle\Ui\DataProvider\Product\Listing\Collector\BundlePrice::PRODUCT_TYPE - ); - $query = $select->deleteFromSelect('i'); - $connection->query($query); -@@ -442,40 +587,47 @@ class Price extends \Magento\Catalog\Model\ResourceModel\Product\Indexer\Price\D - 'tp.all_groups = 1 OR (tp.all_groups = 0 AND tp.customer_group_id = cg.customer_group_id)', - ['customer_group_id'] - )->join( -- ['cw' => $this->getTable('store_website')], -- 'tp.website_id = 0 OR tp.website_id = cw.website_id', -+ ['pw' => $this->getTable('store_website')], -+ 'tp.website_id = 0 OR tp.website_id = pw.website_id', - ['website_id'] - )->where( -- 'cw.website_id != 0' -+ 'pw.website_id != 0' - )->where( - 'e.type_id=?', -- $this->getTypeId() -+ \Magento\Bundle\Ui\DataProvider\Product\Listing\Collector\BundlePrice::PRODUCT_TYPE - )->columns( - new \Zend_Db_Expr('MIN(tp.value)') - )->group( -- ['e.entity_id', 'cg.customer_group_id', 'cw.website_id'] -+ ['e.entity_id', 'cg.customer_group_id', 'pw.website_id'] - ); - - if (!empty($entityIds)) { - $select->where('e.entity_id IN(?)', $entityIds); - } -+ foreach ($dimensions as $dimension) { -+ if (!isset($this->dimensionToFieldMapper[$dimension->getName()])) { -+ throw new \LogicException( -+ 'Provided dimension is not valid for Price indexer: ' . $dimension->getName() -+ ); -+ } -+ $select->where($this->dimensionToFieldMapper[$dimension->getName()] . ' = ?', $dimension->getValue()); -+ } - -- $query = $select->insertFromSelect($this->_getTierPriceIndexTable()); -+ $query = $select->insertFromSelect($this->getTable('catalog_product_index_tier_price')); - $connection->query($query); -- -- return $this; - } - - /** - * Create bundle price. - * -- * @return void -+ * @param IndexTableStructure $priceTable -+ * @return void - */ -- private function applyBundlePrice(): void -+ private function applyBundlePrice($priceTable): void - { - $select = $this->getConnection()->select(); - $select->from( -- $this->_getBundlePriceTable(), -+ $this->getBundlePriceTable(), - [ - 'entity_id', - 'customer_group_id', -@@ -486,11 +638,10 @@ class Price extends \Magento\Catalog\Model\ResourceModel\Product\Indexer\Price\D - 'min_price', - 'max_price', - 'tier_price', -- 'base_tier', - ] - ); - -- $query = $select->insertFromSelect($this->_getDefaultFinalPriceTable()); -+ $query = $select->insertFromSelect($priceTable->getTableName()); - $this->getConnection()->query($query); - } - -@@ -498,13 +649,14 @@ class Price extends \Magento\Catalog\Model\ResourceModel\Product\Indexer\Price\D - * Make insert/update bundle option price. - * - * @return void -+ * @param IndexTableStructure $priceTable - */ -- private function applyBundleOptionPrice(): void -+ private function applyBundleOptionPrice($priceTable): void - { - $connection = $this->getConnection(); - - $subSelect = $connection->select()->from( -- $this->_getBundleOptionTable(), -+ $this->getBundleOptionTable(), - [ - 'entity_id', - 'customer_group_id', -@@ -534,7 +686,47 @@ class Price extends \Magento\Catalog\Model\ResourceModel\Product\Indexer\Price\D - ] - ); - -- $query = $select->crossUpdateFromSelect(['i' => $this->_getDefaultFinalPriceTable()]); -+ $query = $select->crossUpdateFromSelect(['i' => $priceTable->getTableName()]); - $connection->query($query); - } -+ -+ /** -+ * Get main table -+ * -+ * @param array $dimensions -+ * @return string -+ */ -+ private function getMainTable($dimensions) -+ { -+ if ($this->fullReindexAction) { -+ return $this->tableMaintainer->getMainReplicaTable($dimensions); -+ } -+ return $this->tableMaintainer->getMainTable($dimensions); -+ } -+ -+ /** -+ * Get connection -+ * -+ * @return \Magento\Framework\DB\Adapter\AdapterInterface -+ * @throws \DomainException -+ */ -+ private function getConnection(): \Magento\Framework\DB\Adapter\AdapterInterface -+ { -+ if ($this->connection === null) { -+ $this->connection = $this->resource->getConnection($this->connectionName); -+ } -+ -+ return $this->connection; -+ } -+ -+ /** -+ * Get table -+ * -+ * @param string $tableName -+ * @return string -+ */ -+ private function getTable($tableName) -+ { -+ return $this->resource->getTableName($tableName, $this->connectionName); -+ } - } -diff --git a/app/code/Magento/Bundle/Model/ResourceModel/Selection/Collection.php b/app/code/Magento/Bundle/Model/ResourceModel/Selection/Collection.php -index e9295b22674..21ba1f75ba9 100644 ---- a/app/code/Magento/Bundle/Model/ResourceModel/Selection/Collection.php -+++ b/app/code/Magento/Bundle/Model/ResourceModel/Selection/Collection.php -@@ -5,10 +5,8 @@ - */ - namespace Magento\Bundle\Model\ResourceModel\Selection; - --use Magento\Customer\Api\GroupManagementInterface; - use Magento\Framework\DataObject; - use Magento\Framework\DB\Select; --use Magento\Framework\EntityManager\MetadataPool; - use Magento\Catalog\Model\ResourceModel\Product\Collection\ProductLimitationFactory; - use Magento\Framework\App\ObjectManager; - -@@ -17,6 +15,7 @@ use Magento\Framework\App\ObjectManager; - * - * @api - * @SuppressWarnings(PHPMD.CouplingBetweenObjects) -+ * @SuppressWarnings(PHPMD.CookieAndSessionMisuse) - * @since 100.0.2 - */ - class Collection extends \Magento\Catalog\Model\ResourceModel\Product\Collection -@@ -45,6 +44,95 @@ class Collection extends \Magento\Catalog\Model\ResourceModel\Product\Collection - */ - private $websiteScopePriceJoined = false; - -+ /** -+ * @var \Magento\CatalogInventory\Model\ResourceModel\Stock\Item -+ */ -+ private $stockItem; -+ -+ /** -+ * Collection constructor. -+ * @param \Magento\Framework\Data\Collection\EntityFactory $entityFactory -+ * @param \Psr\Log\LoggerInterface $logger -+ * @param \Magento\Framework\Data\Collection\Db\FetchStrategyInterface $fetchStrategy -+ * @param \Magento\Framework\Event\ManagerInterface $eventManager -+ * @param \Magento\Eav\Model\Config $eavConfig -+ * @param \Magento\Framework\App\ResourceConnection $resource -+ * @param \Magento\Eav\Model\EntityFactory $eavEntityFactory -+ * @param \Magento\Catalog\Model\ResourceModel\Helper $resourceHelper -+ * @param \Magento\Framework\Validator\UniversalFactory $universalFactory -+ * @param \Magento\Store\Model\StoreManagerInterface $storeManager -+ * @param \Magento\Framework\Module\ModuleManagerInterface $moduleManager -+ * @param \Magento\Catalog\Model\Indexer\Product\Flat\State $catalogProductFlatState -+ * @param \Magento\Framework\App\Config\ScopeConfigInterface $scopeConfig -+ * @param \Magento\Catalog\Model\Product\OptionFactory $productOptionFactory -+ * @param \Magento\Catalog\Model\ResourceModel\Url $catalogUrl -+ * @param \Magento\Framework\Stdlib\DateTime\TimezoneInterface $localeDate -+ * @param \Magento\Customer\Model\Session $customerSession -+ * @param \Magento\Framework\Stdlib\DateTime $dateTime -+ * @param \Magento\Customer\Api\GroupManagementInterface $groupManagement -+ * @param \Magento\Framework\DB\Adapter\AdapterInterface|null $connection -+ * @param ProductLimitationFactory|null $productLimitationFactory -+ * @param \Magento\Framework\EntityManager\MetadataPool|null $metadataPool -+ * @param \Magento\Catalog\Model\Indexer\Category\Product\TableMaintainer|null $tableMaintainer -+ * @param \Magento\CatalogInventory\Model\ResourceModel\Stock\Item|null $stockItem -+ * @SuppressWarnings(PHPMD.ExcessiveParameterList) -+ */ -+ public function __construct( -+ \Magento\Framework\Data\Collection\EntityFactory $entityFactory, -+ \Psr\Log\LoggerInterface $logger, -+ \Magento\Framework\Data\Collection\Db\FetchStrategyInterface $fetchStrategy, -+ \Magento\Framework\Event\ManagerInterface $eventManager, -+ \Magento\Eav\Model\Config $eavConfig, -+ \Magento\Framework\App\ResourceConnection $resource, -+ \Magento\Eav\Model\EntityFactory $eavEntityFactory, -+ \Magento\Catalog\Model\ResourceModel\Helper $resourceHelper, -+ \Magento\Framework\Validator\UniversalFactory $universalFactory, -+ \Magento\Store\Model\StoreManagerInterface $storeManager, -+ \Magento\Framework\Module\ModuleManagerInterface $moduleManager, -+ \Magento\Catalog\Model\Indexer\Product\Flat\State $catalogProductFlatState, -+ \Magento\Framework\App\Config\ScopeConfigInterface $scopeConfig, -+ \Magento\Catalog\Model\Product\OptionFactory $productOptionFactory, -+ \Magento\Catalog\Model\ResourceModel\Url $catalogUrl, -+ \Magento\Framework\Stdlib\DateTime\TimezoneInterface $localeDate, -+ \Magento\Customer\Model\Session $customerSession, -+ \Magento\Framework\Stdlib\DateTime $dateTime, -+ \Magento\Customer\Api\GroupManagementInterface $groupManagement, -+ \Magento\Framework\DB\Adapter\AdapterInterface $connection = null, -+ ProductLimitationFactory $productLimitationFactory = null, -+ \Magento\Framework\EntityManager\MetadataPool $metadataPool = null, -+ \Magento\Catalog\Model\Indexer\Category\Product\TableMaintainer $tableMaintainer = null, -+ \Magento\CatalogInventory\Model\ResourceModel\Stock\Item $stockItem = null -+ ) { -+ parent::__construct( -+ $entityFactory, -+ $logger, -+ $fetchStrategy, -+ $eventManager, -+ $eavConfig, -+ $resource, -+ $eavEntityFactory, -+ $resourceHelper, -+ $universalFactory, -+ $storeManager, -+ $moduleManager, -+ $catalogProductFlatState, -+ $scopeConfig, -+ $productOptionFactory, -+ $catalogUrl, -+ $localeDate, -+ $customerSession, -+ $dateTime, -+ $groupManagement, -+ $connection, -+ $productLimitationFactory, -+ $metadataPool, -+ $tableMaintainer -+ ); -+ -+ $this->stockItem = $stockItem -+ ?? ObjectManager::getInstance()->get(\Magento\CatalogInventory\Model\ResourceModel\Stock\Item::class); -+ } -+ - /** - * Initialize collection - * -@@ -58,19 +146,14 @@ class Collection extends \Magento\Catalog\Model\ResourceModel\Product\Collection - } - - /** -- * Set store id for each collection item when collection was loaded -+ * Set store id for each collection item when collection was loaded. -+ * phpcs:disable Generic.CodeAnalysis.UselessOverridingMethod - * - * @return $this - */ - public function _afterLoad() - { -- parent::_afterLoad(); -- if ($this->getStoreId() && $this->_items) { -- foreach ($this->_items as $item) { -- $item->setStoreId($this->getStoreId()); -- } -- } -- return $this; -+ return parent::_afterLoad(); - } - - /** -@@ -170,28 +253,30 @@ class Collection extends \Magento\Catalog\Model\ResourceModel\Product\Collection - */ - public function addQuantityFilter() - { -- $stockItemTable = $this->getTable('cataloginventory_stock_item'); -- $stockStatusTable = $this->getTable('cataloginventory_stock_status'); -+ $manageStockExpr = $this->stockItem->getManageStockExpr('stock_item'); -+ $backordersExpr = $this->stockItem->getBackordersExpr('stock_item'); -+ $minQtyExpr = $this->getConnection()->getCheckSql( -+ 'selection.selection_can_change_qty', -+ $this->stockItem->getMinSaleQtyExpr('stock_item'), -+ 'selection.selection_qty' -+ ); -+ -+ $where = $manageStockExpr . ' = 0'; -+ $where .= ' OR (' -+ . 'stock_item.is_in_stock = ' . \Magento\CatalogInventory\Model\Stock::STOCK_IN_STOCK -+ . ' AND (' -+ . $backordersExpr . ' != ' . \Magento\CatalogInventory\Model\Stock::BACKORDERS_NO -+ . ' OR ' -+ . $minQtyExpr . ' <= stock_item.qty' -+ . ')' -+ . ')'; -+ - $this->getSelect() - ->joinInner( -- ['stock' => $stockStatusTable], -- 'selection.product_id = stock.product_id', -- [] -- )->joinInner( -- ['stock_item' => $stockItemTable], -+ ['stock_item' => $this->stockItem->getMainTable()], - 'selection.product_id = stock_item.product_id', - [] -- ) -- ->where( -- '(' -- . 'selection.selection_can_change_qty > 0' -- . ' or ' -- . 'selection.selection_qty <= stock.qty' -- . ' or ' -- .'stock_item.manage_stock = 0' -- . ')' -- ) -- ->where('stock.stock_status = 1'); -+ )->where($where); - - return $this; - } -@@ -267,7 +352,10 @@ class Collection extends \Magento\Catalog\Model\ResourceModel\Product\Collection - } - - /** -+ * Get Catalog Rule Processor. -+ * - * @return \Magento\CatalogRule\Model\ResourceModel\Product\CollectionProcessor -+ * - * @deprecated 100.2.0 - */ - private function getCatalogRuleProcessor() -diff --git a/app/code/Magento/Bundle/Plugin/UpdatePriceInQuoteItemOptions.php b/app/code/Magento/Bundle/Plugin/UpdatePriceInQuoteItemOptions.php -new file mode 100644 -index 00000000000..d5aafb8ad2b ---- /dev/null -+++ b/app/code/Magento/Bundle/Plugin/UpdatePriceInQuoteItemOptions.php -@@ -0,0 +1,55 @@ -+serializer = $serializer; -+ } -+ -+ /** -+ * Update price on quote item options level -+ * -+ * @param OrigQuoteItem $subject -+ * @param AbstractItem $result -+ * @return AbstractItem -+ * -+ * @SuppressWarnings(PHPMD.UnusedFormalParameter) -+ */ -+ public function afterCalcRowTotal(OrigQuoteItem $subject, AbstractItem $result) -+ { -+ $bundleAttributes = $result->getProduct()->getCustomOption('bundle_selection_attributes'); -+ if ($bundleAttributes !== null) { -+ $actualPrice = $result->getPrice(); -+ $parsedValue = $this->serializer->unserialize($bundleAttributes->getValue()); -+ if (is_array($parsedValue) && array_key_exists('price', $parsedValue)) { -+ $parsedValue['price'] = $actualPrice; -+ } -+ $bundleAttributes->setValue($this->serializer->serialize($parsedValue)); -+ } -+ -+ return $result; -+ } -+} -diff --git a/app/code/Magento/Bundle/Pricing/Adjustment/Calculator.php b/app/code/Magento/Bundle/Pricing/Adjustment/Calculator.php -index adb0777151b..04a6ee0bd45 100644 ---- a/app/code/Magento/Bundle/Pricing/Adjustment/Calculator.php -+++ b/app/code/Magento/Bundle/Pricing/Adjustment/Calculator.php -@@ -198,6 +198,8 @@ class Calculator implements BundleCalculatorInterface - } - - /** -+ * Get selection price list provider. -+ * - * @return SelectionPriceListProviderInterface - * @deprecated 100.2.0 - */ -@@ -281,7 +283,7 @@ class Calculator implements BundleCalculatorInterface - * @param float $basePriceValue - * @param Product $bundleProduct - * @param \Magento\Bundle\Pricing\Price\BundleSelectionPrice[] $selectionPriceList -- * @param null|bool|string|arrayy $exclude -+ * @param null|bool|string|array $exclude - * @return \Magento\Framework\Pricing\Amount\AmountInterface - */ - protected function calculateFixedBundleAmount($basePriceValue, $bundleProduct, $selectionPriceList, $exclude) -diff --git a/app/code/Magento/Bundle/Pricing/Price/BundleSelectionFactory.php b/app/code/Magento/Bundle/Pricing/Price/BundleSelectionFactory.php -index 927b8fbff8d..a28d721cc9a 100644 ---- a/app/code/Magento/Bundle/Pricing/Price/BundleSelectionFactory.php -+++ b/app/code/Magento/Bundle/Pricing/Price/BundleSelectionFactory.php -@@ -54,7 +54,7 @@ class BundleSelectionFactory - ) { - $arguments['bundleProduct'] = $bundleProduct; - $arguments['saleableItem'] = $selection; -- $arguments['quantity'] = $quantity ? floatval($quantity) : 1.; -+ $arguments['quantity'] = $quantity ? (float)$quantity : 1.; - - return $this->objectManager->create(self::SELECTION_CLASS_DEFAULT, $arguments); - } -diff --git a/app/code/Magento/Bundle/Setup/Patch/Data/UpdateBundleRelatedEntityTypes.php b/app/code/Magento/Bundle/Setup/Patch/Data/UpdateBundleRelatedEntityTypes.php -new file mode 100644 -index 00000000000..701def7fc13 ---- /dev/null -+++ b/app/code/Magento/Bundle/Setup/Patch/Data/UpdateBundleRelatedEntityTypes.php -@@ -0,0 +1,204 @@ -+moduleDataSetup = $moduleDataSetup; -+ $this->eavSetupFactory = $eavSetupFactory; -+ } -+ -+ /** -+ * @inheritdoc -+ */ -+ public function apply() -+ { -+ /** @var \Magento\Eav\Setup\EavSetup $eavSetup */ -+ $eavSetup = $this->eavSetupFactory->create(['setup' => $this->moduleDataSetup]); -+ -+ $attributeSetId = $eavSetup->getDefaultAttributeSetId(ProductAttributeInterface::ENTITY_TYPE_CODE); -+ $eavSetup->addAttributeGroup( -+ ProductAttributeInterface::ENTITY_TYPE_CODE, -+ $attributeSetId, -+ 'Bundle Items', -+ 16 -+ ); -+ $this->upgradePriceType($eavSetup); -+ $this->upgradeSkuType($eavSetup); -+ $this->upgradeWeightType($eavSetup); -+ $this->upgradeShipmentType($eavSetup); -+ } -+ -+ /** -+ * Upgrade Dynamic Price attribute -+ * -+ * @param EavSetup $eavSetup -+ * @return void -+ */ -+ private function upgradePriceType(EavSetup $eavSetup) -+ { -+ $eavSetup->updateAttribute( -+ ProductAttributeInterface::ENTITY_TYPE_CODE, -+ 'price_type', -+ 'frontend_input', -+ 'boolean', -+ 31 -+ ); -+ $eavSetup->updateAttribute( -+ ProductAttributeInterface::ENTITY_TYPE_CODE, -+ 'price_type', -+ 'frontend_label', -+ 'Dynamic Price' -+ ); -+ $eavSetup->updateAttribute(ProductAttributeInterface::ENTITY_TYPE_CODE, 'price_type', 'default_value', 0); -+ } -+ -+ /** -+ * Upgrade Dynamic Sku attribute -+ * -+ * @param EavSetup $eavSetup -+ * @return void -+ */ -+ private function upgradeSkuType(EavSetup $eavSetup) -+ { -+ $eavSetup->updateAttribute( -+ ProductAttributeInterface::ENTITY_TYPE_CODE, -+ 'sku_type', -+ 'frontend_input', -+ 'boolean', -+ 21 -+ ); -+ $eavSetup->updateAttribute( -+ ProductAttributeInterface::ENTITY_TYPE_CODE, -+ 'sku_type', -+ 'frontend_label', -+ 'Dynamic SKU' -+ ); -+ $eavSetup->updateAttribute(ProductAttributeInterface::ENTITY_TYPE_CODE, 'sku_type', 'default_value', 0); -+ $eavSetup->updateAttribute(ProductAttributeInterface::ENTITY_TYPE_CODE, 'sku_type', 'is_visible', 1); -+ } -+ -+ /** -+ * Upgrade Dynamic Weight attribute -+ * -+ * @param EavSetup $eavSetup -+ * @return void -+ */ -+ private function upgradeWeightType(EavSetup $eavSetup) -+ { -+ $eavSetup->updateAttribute( -+ ProductAttributeInterface::ENTITY_TYPE_CODE, -+ 'weight_type', -+ 'frontend_input', -+ 'boolean', -+ 71 -+ ); -+ $eavSetup->updateAttribute( -+ ProductAttributeInterface::ENTITY_TYPE_CODE, -+ 'weight_type', -+ 'frontend_label', -+ 'Dynamic Weight' -+ ); -+ $eavSetup->updateAttribute(ProductAttributeInterface::ENTITY_TYPE_CODE, 'weight_type', 'default_value', 0); -+ $eavSetup->updateAttribute(ProductAttributeInterface::ENTITY_TYPE_CODE, 'weight_type', 'is_visible', 1); -+ } -+ -+ /** -+ * Upgrade Ship Bundle Items attribute -+ * -+ * @param EavSetup $eavSetup -+ * @return void -+ */ -+ private function upgradeShipmentType(EavSetup $eavSetup) -+ { -+ $attributeSetId = $eavSetup->getDefaultAttributeSetId(ProductAttributeInterface::ENTITY_TYPE_CODE); -+ $eavSetup->addAttributeToGroup( -+ ProductAttributeInterface::ENTITY_TYPE_CODE, -+ $attributeSetId, -+ 'Bundle Items', -+ 'shipment_type', -+ 1 -+ ); -+ $eavSetup->updateAttribute( -+ ProductAttributeInterface::ENTITY_TYPE_CODE, -+ 'shipment_type', -+ 'frontend_input', -+ 'select' -+ ); -+ $eavSetup->updateAttribute( -+ ProductAttributeInterface::ENTITY_TYPE_CODE, -+ 'shipment_type', -+ 'frontend_label', -+ 'Ship Bundle Items' -+ ); -+ $eavSetup->updateAttribute( -+ ProductAttributeInterface::ENTITY_TYPE_CODE, -+ 'shipment_type', -+ 'source_model', -+ \Magento\Bundle\Model\Product\Attribute\Source\Shipment\Type::class -+ ); -+ $eavSetup->updateAttribute(ProductAttributeInterface::ENTITY_TYPE_CODE, 'shipment_type', 'default_value', 0); -+ $eavSetup->updateAttribute(ProductAttributeInterface::ENTITY_TYPE_CODE, 'shipment_type', 'is_visible', 1); -+ } -+ -+ /** -+ * @inheritdoc -+ */ -+ public static function getDependencies() -+ { -+ return [ -+ ApplyAttributesUpdate::class, -+ ]; -+ } -+ -+ /** -+ * @inheritdoc -+ */ -+ public static function getVersion() -+ { -+ return '2.0.2'; -+ } -+ -+ /** -+ * @inheritdoc -+ */ -+ public function getAliases() -+ { -+ return []; -+ } -+} -diff --git a/app/code/Magento/Bundle/Setup/Patch/Data/UpdateBundleRelatedEntityTytpes.php b/app/code/Magento/Bundle/Setup/Patch/Data/UpdateBundleRelatedEntityTytpes.php -deleted file mode 100644 -index 44647ea76a1..00000000000 ---- a/app/code/Magento/Bundle/Setup/Patch/Data/UpdateBundleRelatedEntityTytpes.php -+++ /dev/null -@@ -1,204 +0,0 @@ --moduleDataSetup = $moduleDataSetup; -- $this->eavSetupFactory = $eavSetupFactory; -- } -- -- /** -- * {@inheritdoc} -- */ -- public function apply() -- { -- /** @var \Magento\Eav\Setup\EavSetup $eavSetup */ -- $eavSetup = $this->eavSetupFactory->create(['setup' => $this->moduleDataSetup]); -- -- $attributeSetId = $eavSetup->getDefaultAttributeSetId(ProductAttributeInterface::ENTITY_TYPE_CODE); -- $eavSetup->addAttributeGroup( -- ProductAttributeInterface::ENTITY_TYPE_CODE, -- $attributeSetId, -- 'Bundle Items', -- 16 -- ); -- $this->upgradePriceType($eavSetup); -- $this->upgradeSkuType($eavSetup); -- $this->upgradeWeightType($eavSetup); -- $this->upgradeShipmentType($eavSetup); -- } -- -- /** -- * Upgrade Dynamic Price attribute -- * -- * @param EavSetup $eavSetup -- * @return void -- */ -- private function upgradePriceType(EavSetup $eavSetup) -- { -- $eavSetup->updateAttribute( -- ProductAttributeInterface::ENTITY_TYPE_CODE, -- 'price_type', -- 'frontend_input', -- 'boolean', -- 31 -- ); -- $eavSetup->updateAttribute( -- ProductAttributeInterface::ENTITY_TYPE_CODE, -- 'price_type', -- 'frontend_label', -- 'Dynamic Price' -- ); -- $eavSetup->updateAttribute(ProductAttributeInterface::ENTITY_TYPE_CODE, 'price_type', 'default_value', 0); -- } -- -- /** -- * Upgrade Dynamic Sku attribute -- * -- * @param EavSetup $eavSetup -- * @return void -- */ -- private function upgradeSkuType(EavSetup $eavSetup) -- { -- $eavSetup->updateAttribute( -- ProductAttributeInterface::ENTITY_TYPE_CODE, -- 'sku_type', -- 'frontend_input', -- 'boolean', -- 21 -- ); -- $eavSetup->updateAttribute( -- ProductAttributeInterface::ENTITY_TYPE_CODE, -- 'sku_type', -- 'frontend_label', -- 'Dynamic SKU' -- ); -- $eavSetup->updateAttribute(ProductAttributeInterface::ENTITY_TYPE_CODE, 'sku_type', 'default_value', 0); -- $eavSetup->updateAttribute(ProductAttributeInterface::ENTITY_TYPE_CODE, 'sku_type', 'is_visible', 1); -- } -- -- /** -- * Upgrade Dynamic Weight attribute -- * -- * @param EavSetup $eavSetup -- * @return void -- */ -- private function upgradeWeightType(EavSetup $eavSetup) -- { -- $eavSetup->updateAttribute( -- ProductAttributeInterface::ENTITY_TYPE_CODE, -- 'weight_type', -- 'frontend_input', -- 'boolean', -- 71 -- ); -- $eavSetup->updateAttribute( -- ProductAttributeInterface::ENTITY_TYPE_CODE, -- 'weight_type', -- 'frontend_label', -- 'Dynamic Weight' -- ); -- $eavSetup->updateAttribute(ProductAttributeInterface::ENTITY_TYPE_CODE, 'weight_type', 'default_value', 0); -- $eavSetup->updateAttribute(ProductAttributeInterface::ENTITY_TYPE_CODE, 'weight_type', 'is_visible', 1); -- } -- -- /** -- * Upgrade Ship Bundle Items attribute -- * -- * @param EavSetup $eavSetup -- * @return void -- */ -- private function upgradeShipmentType(EavSetup $eavSetup) -- { -- $attributeSetId = $eavSetup->getDefaultAttributeSetId(ProductAttributeInterface::ENTITY_TYPE_CODE); -- $eavSetup->addAttributeToGroup( -- ProductAttributeInterface::ENTITY_TYPE_CODE, -- $attributeSetId, -- 'Bundle Items', -- 'shipment_type', -- 1 -- ); -- $eavSetup->updateAttribute( -- ProductAttributeInterface::ENTITY_TYPE_CODE, -- 'shipment_type', -- 'frontend_input', -- 'select' -- ); -- $eavSetup->updateAttribute( -- ProductAttributeInterface::ENTITY_TYPE_CODE, -- 'shipment_type', -- 'frontend_label', -- 'Ship Bundle Items' -- ); -- $eavSetup->updateAttribute( -- ProductAttributeInterface::ENTITY_TYPE_CODE, -- 'shipment_type', -- 'source_model', -- \Magento\Bundle\Model\Product\Attribute\Source\Shipment\Type::class -- ); -- $eavSetup->updateAttribute(ProductAttributeInterface::ENTITY_TYPE_CODE, 'shipment_type', 'default_value', 0); -- $eavSetup->updateAttribute(ProductAttributeInterface::ENTITY_TYPE_CODE, 'shipment_type', 'is_visible', 1); -- } -- -- /** -- * {@inheritdoc} -- */ -- public static function getDependencies() -- { -- return [ -- ApplyAttributesUpdate::class, -- ]; -- } -- -- /** -- * {@inheritdoc} -- */ -- public static function getVersion() -- { -- return '2.0.2'; -- } -- -- /** -- * {@inheritdoc} -- */ -- public function getAliases() -- { -- return []; -- } --} -diff --git a/app/code/Magento/Bundle/Test/Mftf/ActionGroup/AdminBundleProductActionGroup.xml b/app/code/Magento/Bundle/Test/Mftf/ActionGroup/AdminBundleProductActionGroup.xml -index a5e62fca948..836826734f0 100644 ---- a/app/code/Magento/Bundle/Test/Mftf/ActionGroup/AdminBundleProductActionGroup.xml -+++ b/app/code/Magento/Bundle/Test/Mftf/ActionGroup/AdminBundleProductActionGroup.xml -@@ -7,7 +7,7 @@ - --> - - -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> - - - -diff --git a/app/code/Magento/Bundle/Test/Mftf/ActionGroup/AdminClearFiltersActionGroup.xml b/app/code/Magento/Bundle/Test/Mftf/ActionGroup/AdminClearFiltersActionGroup.xml -old mode 100644 -new mode 100755 -index f3e5eff3834..b3ac72d3f41 ---- a/app/code/Magento/Bundle/Test/Mftf/ActionGroup/AdminClearFiltersActionGroup.xml -+++ b/app/code/Magento/Bundle/Test/Mftf/ActionGroup/AdminClearFiltersActionGroup.xml -@@ -7,7 +7,7 @@ - --> - - -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> - - - -diff --git a/app/code/Magento/Bundle/Test/Mftf/ActionGroup/AdminCreateApiBundleProductActionGroup.xml b/app/code/Magento/Bundle/Test/Mftf/ActionGroup/AdminCreateApiBundleProductActionGroup.xml -new file mode 100644 -index 00000000000..4cd16320d3d ---- /dev/null -+++ b/app/code/Magento/Bundle/Test/Mftf/ActionGroup/AdminCreateApiBundleProductActionGroup.xml -@@ -0,0 +1,167 @@ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ 4.99 -+ -+ -+ 2.89 -+ -+ -+ 7.33 -+ -+ -+ 18.25 -+ -+ -+ -+ {{productName}} -+ -+ -+ -+ false -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ 4.99 -+ -+ -+ 2.89 -+ -+ -+ 7.33 -+ -+ -+ 18.25 -+ -+ -+ -+ {{productName}} -+ -+ -+ -+ false -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ 10 -+ -+ -+ 20 -+ -+ -+ -+ {{productName}} -+ -+ -+ -+ Drop-down Option -+ -+ -+ -+ Radio Buttons Option -+ -+ -+ -+ Checkbox Option -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -diff --git a/app/code/Magento/Bundle/Test/Mftf/ActionGroup/AdminOrderBundleProductActionGroup.xml b/app/code/Magento/Bundle/Test/Mftf/ActionGroup/AdminOrderBundleProductActionGroup.xml -new file mode 100644 -index 00000000000..d73d31c4498 ---- /dev/null -+++ b/app/code/Magento/Bundle/Test/Mftf/ActionGroup/AdminOrderBundleProductActionGroup.xml -@@ -0,0 +1,23 @@ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -diff --git a/app/code/Magento/Bundle/Test/Mftf/ActionGroup/BundleProductFilterActionGroup.xml b/app/code/Magento/Bundle/Test/Mftf/ActionGroup/BundleProductFilterActionGroup.xml -index 8ab7af1d031..177f9203ed1 100644 ---- a/app/code/Magento/Bundle/Test/Mftf/ActionGroup/BundleProductFilterActionGroup.xml -+++ b/app/code/Magento/Bundle/Test/Mftf/ActionGroup/BundleProductFilterActionGroup.xml -@@ -7,7 +7,7 @@ - --> - - -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> - - - -diff --git a/app/code/Magento/Bundle/Test/Mftf/ActionGroup/CreateBundleProductActionGroup.xml b/app/code/Magento/Bundle/Test/Mftf/ActionGroup/CreateBundleProductActionGroup.xml -index af8fc1459d9..d86d720ed7f 100644 ---- a/app/code/Magento/Bundle/Test/Mftf/ActionGroup/CreateBundleProductActionGroup.xml -+++ b/app/code/Magento/Bundle/Test/Mftf/ActionGroup/CreateBundleProductActionGroup.xml -@@ -7,7 +7,7 @@ - --> - - -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> - - - -@@ -15,11 +15,12 @@ - - - -- -- -+ -+ -+ - - -- -+ - - - -@@ -55,4 +56,74 @@ - - - -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ - -diff --git a/app/code/Magento/Bundle/Test/Mftf/ActionGroup/EnableDisableProductActionGroup.xml b/app/code/Magento/Bundle/Test/Mftf/ActionGroup/EnableDisableProductActionGroup.xml -index 2ae9748c773..20bde5f87bd 100644 ---- a/app/code/Magento/Bundle/Test/Mftf/ActionGroup/EnableDisableProductActionGroup.xml -+++ b/app/code/Magento/Bundle/Test/Mftf/ActionGroup/EnableDisableProductActionGroup.xml -@@ -7,14 +7,15 @@ - --> - - -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> - - - - - - -- -+ -+ - - - -diff --git a/app/code/Magento/Bundle/Test/Mftf/ActionGroup/SetBundleProductAttributesActionGroup.xml b/app/code/Magento/Bundle/Test/Mftf/ActionGroup/SetBundleProductAttributesActionGroup.xml -new file mode 100644 -index 00000000000..50af5993af5 ---- /dev/null -+++ b/app/code/Magento/Bundle/Test/Mftf/ActionGroup/SetBundleProductAttributesActionGroup.xml -@@ -0,0 +1,82 @@ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -diff --git a/app/code/Magento/Bundle/Test/Mftf/ActionGroup/StoreFrontAddProductToCartFromBundleActionGroup.xml b/app/code/Magento/Bundle/Test/Mftf/ActionGroup/StoreFrontAddProductToCartFromBundleActionGroup.xml -new file mode 100644 -index 00000000000..441303e8f1b ---- /dev/null -+++ b/app/code/Magento/Bundle/Test/Mftf/ActionGroup/StoreFrontAddProductToCartFromBundleActionGroup.xml -@@ -0,0 +1,24 @@ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -diff --git a/app/code/Magento/Bundle/Test/Mftf/ActionGroup/StorefrontProductCartActionGroup.xml b/app/code/Magento/Bundle/Test/Mftf/ActionGroup/StorefrontProductCartActionGroup.xml -index 48697d43ec8..1767db0a009 100644 ---- a/app/code/Magento/Bundle/Test/Mftf/ActionGroup/StorefrontProductCartActionGroup.xml -+++ b/app/code/Magento/Bundle/Test/Mftf/ActionGroup/StorefrontProductCartActionGroup.xml -@@ -7,7 +7,7 @@ - --> - - -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> - - - -@@ -24,4 +24,38 @@ - - - -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ - -diff --git a/app/code/Magento/Bundle/Test/Mftf/ActionGroup/StorefrontSelectBundleProductDropDownOptionActionGroup.xml b/app/code/Magento/Bundle/Test/Mftf/ActionGroup/StorefrontSelectBundleProductDropDownOptionActionGroup.xml -new file mode 100644 -index 00000000000..e6afdce6ab6 ---- /dev/null -+++ b/app/code/Magento/Bundle/Test/Mftf/ActionGroup/StorefrontSelectBundleProductDropDownOptionActionGroup.xml -@@ -0,0 +1,18 @@ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -diff --git a/app/code/Magento/Bundle/Test/Mftf/ActionGroup/StorefrontSelectCustomizeAndAddToTheCartButtonActionGroup.xml b/app/code/Magento/Bundle/Test/Mftf/ActionGroup/StorefrontSelectCustomizeAndAddToTheCartButtonActionGroup.xml -new file mode 100644 -index 00000000000..cf2ccfac470 ---- /dev/null -+++ b/app/code/Magento/Bundle/Test/Mftf/ActionGroup/StorefrontSelectCustomizeAndAddToTheCartButtonActionGroup.xml -@@ -0,0 +1,16 @@ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -diff --git a/app/code/Magento/Bundle/Test/Mftf/ActionGroup/VerifyProductTypeOrderActionGroup.xml b/app/code/Magento/Bundle/Test/Mftf/ActionGroup/VerifyProductTypeOrderActionGroup.xml -new file mode 100644 -index 00000000000..a00f1e36786 ---- /dev/null -+++ b/app/code/Magento/Bundle/Test/Mftf/ActionGroup/VerifyProductTypeOrderActionGroup.xml -@@ -0,0 +1,14 @@ -+ -+ -+ -+ -+ -+ -+ -+ -diff --git a/app/code/Magento/Bundle/Test/Mftf/Data/BundleLinkData.xml b/app/code/Magento/Bundle/Test/Mftf/Data/BundleLinkData.xml -index 7123a573bc2..60d11345731 100644 ---- a/app/code/Magento/Bundle/Test/Mftf/Data/BundleLinkData.xml -+++ b/app/code/Magento/Bundle/Test/Mftf/Data/BundleLinkData.xml -@@ -7,7 +7,7 @@ - --> - - -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataProfileSchema.xsd"> - - - -diff --git a/app/code/Magento/Bundle/Test/Mftf/Data/BundleOptionData.xml b/app/code/Magento/Bundle/Test/Mftf/Data/BundleOptionData.xml -index e10fe4e33c2..a53ae9be4b7 100644 ---- a/app/code/Magento/Bundle/Test/Mftf/Data/BundleOptionData.xml -+++ b/app/code/Magento/Bundle/Test/Mftf/Data/BundleOptionData.xml -@@ -7,7 +7,7 @@ - --> - - -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataProfileSchema.xsd"> - - bundle-option-dropdown - true -diff --git a/app/code/Magento/Bundle/Test/Mftf/Data/BundleProductsSummaryData.xml b/app/code/Magento/Bundle/Test/Mftf/Data/BundleProductsSummaryData.xml -new file mode 100644 -index 00000000000..5cd286c0c6a ---- /dev/null -+++ b/app/code/Magento/Bundle/Test/Mftf/Data/BundleProductsSummaryData.xml -@@ -0,0 +1,17 @@ -+ -+ -+ -+ -+ -+ 1,968.00 -+ 5.00 -+ 1,973.00 -+ Flat Rate - Fixed -+ -+ -diff --git a/app/code/Magento/Bundle/Test/Mftf/Data/CustomAttributeData.xml b/app/code/Magento/Bundle/Test/Mftf/Data/CustomAttributeData.xml -index 380b5b89590..256bfd77469 100644 ---- a/app/code/Magento/Bundle/Test/Mftf/Data/CustomAttributeData.xml -+++ b/app/code/Magento/Bundle/Test/Mftf/Data/CustomAttributeData.xml -@@ -6,7 +6,7 @@ - */ - --> - -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataProfileSchema.xsd"> - - price_type - 0 -@@ -23,4 +23,12 @@ - price_view - 0 - -+ -+ weight_type -+ 1 -+ -+ -+ sku_type -+ 1 -+ - -diff --git a/app/code/Magento/Bundle/Test/Mftf/Data/ProductData.xml b/app/code/Magento/Bundle/Test/Mftf/Data/ProductData.xml -index f48810e0534..5034c72b9ee 100644 ---- a/app/code/Magento/Bundle/Test/Mftf/Data/ProductData.xml -+++ b/app/code/Magento/Bundle/Test/Mftf/Data/ProductData.xml -@@ -7,7 +7,7 @@ - --> - - -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataProfileSchema.xsd"> - - BundleProduct - BundleProduct2 -@@ -23,6 +23,7 @@ - bundleproduct2 - 10 - 20 -+ 30 - 4 - bundle - 10 -@@ -30,6 +31,22 @@ - $10.00 - Default - -+ -+ FixedBundleProduct -+ fixed-bundle-product -+ bundle -+ 4 -+ 1.23 -+ 4 -+ 1 -+ fixed-bundle-product -+ CustomAttributeCategoryIds -+ EavStockItem -+ CustomAttributePriceView -+ CustomAttributeFixPrice -+ CustomAttributeFixWeight -+ CustomAttributeFixSku -+ - - Api Bundle Product - api-bundle-product -@@ -59,4 +76,35 @@ - CustomAttributeDynamicPrice - CustomAttributePriceViewRange - -+ -+ Api Fixed Bundle Product -+ api-fixed-bundle-product -+ bundle -+ 4 -+ 1.23 -+ 4 -+ 1 -+ api-fixed-bundle-product -+ EavStockItem -+ ApiProductDescription -+ ApiProductShortDescription -+ CustomAttributeFixPrice -+ CustomAttributePriceView -+ -+ -+ BundleProduct -+ bundle-product -+ bundle -+ 4 -+ 4 -+ 1 -+ bundle-product -+ EavStockItem -+ CustomAttributeCategoryIds -+ EavStockItem -+ ApiProductDescription -+ ApiProductShortDescription -+ CustomAttributeDynamicPrice -+ CustomAttributePriceViewRange -+ - -diff --git a/app/code/Magento/Bundle/Test/Mftf/Metadata/bundle_link-meta.xml b/app/code/Magento/Bundle/Test/Mftf/Metadata/bundle_link-meta.xml -index ca39253aa54..254f542316d 100644 ---- a/app/code/Magento/Bundle/Test/Mftf/Metadata/bundle_link-meta.xml -+++ b/app/code/Magento/Bundle/Test/Mftf/Metadata/bundle_link-meta.xml -@@ -7,7 +7,7 @@ - --> - - -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataOperation.xsd"> - - application/json - -diff --git a/app/code/Magento/Bundle/Test/Mftf/Metadata/bundle_option-meta.xml b/app/code/Magento/Bundle/Test/Mftf/Metadata/bundle_option-meta.xml -index c912ea5eac4..4e1dc7ac9cb 100644 ---- a/app/code/Magento/Bundle/Test/Mftf/Metadata/bundle_option-meta.xml -+++ b/app/code/Magento/Bundle/Test/Mftf/Metadata/bundle_option-meta.xml -@@ -7,7 +7,7 @@ - --> - - -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataOperation.xsd"> - - application/json - -diff --git a/app/code/Magento/Bundle/Test/Mftf/Metadata/bundle_options-meta.xml b/app/code/Magento/Bundle/Test/Mftf/Metadata/bundle_options-meta.xml -index 12cba3fc179..df931c74191 100644 ---- a/app/code/Magento/Bundle/Test/Mftf/Metadata/bundle_options-meta.xml -+++ b/app/code/Magento/Bundle/Test/Mftf/Metadata/bundle_options-meta.xml -@@ -7,7 +7,7 @@ - --> - - -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataOperation.xsd"> - - application/json - -diff --git a/app/code/Magento/Bundle/Test/Mftf/Page/AdminCatalogProductPage.xml b/app/code/Magento/Bundle/Test/Mftf/Page/AdminCatalogProductPage.xml -index cb97521499e..782c97aab1a 100644 ---- a/app/code/Magento/Bundle/Test/Mftf/Page/AdminCatalogProductPage.xml -+++ b/app/code/Magento/Bundle/Test/Mftf/Page/AdminCatalogProductPage.xml -@@ -7,7 +7,7 @@ - --> - - -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/PageObject.xsd"> - -
- -diff --git a/app/code/Magento/Bundle/Test/Mftf/Page/AdminProductCreatePage.xml b/app/code/Magento/Bundle/Test/Mftf/Page/AdminProductCreatePage.xml -index f0048e2fc95..562ded6c8e4 100644 ---- a/app/code/Magento/Bundle/Test/Mftf/Page/AdminProductCreatePage.xml -+++ b/app/code/Magento/Bundle/Test/Mftf/Page/AdminProductCreatePage.xml -@@ -7,7 +7,7 @@ - --> - - -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/PageObject.xsd"> - -
- -diff --git a/app/code/Magento/Bundle/Test/Mftf/Section/AdminOrderBundleProductSection.xml b/app/code/Magento/Bundle/Test/Mftf/Section/AdminOrderBundleProductSection.xml -new file mode 100644 -index 00000000000..915b11ebdbb ---- /dev/null -+++ b/app/code/Magento/Bundle/Test/Mftf/Section/AdminOrderBundleProductSection.xml -@@ -0,0 +1,14 @@ -+ -+ -+ -+ -+
-+ -+
-+
-diff --git a/app/code/Magento/Bundle/Test/Mftf/Section/AdminProductDropdownOrderSection.xml b/app/code/Magento/Bundle/Test/Mftf/Section/AdminProductDropdownOrderSection.xml -new file mode 100644 -index 00000000000..787f7ade8ff ---- /dev/null -+++ b/app/code/Magento/Bundle/Test/Mftf/Section/AdminProductDropdownOrderSection.xml -@@ -0,0 +1,14 @@ -+ -+ -+ -+ -+
-+ -+
-+
-diff --git a/app/code/Magento/Bundle/Test/Mftf/Section/AdminProductFormBundleSection.xml b/app/code/Magento/Bundle/Test/Mftf/Section/AdminProductFormBundleSection.xml -index 25f1d95dc86..cb6f6c1b56a 100644 ---- a/app/code/Magento/Bundle/Test/Mftf/Section/AdminProductFormBundleSection.xml -+++ b/app/code/Magento/Bundle/Test/Mftf/Section/AdminProductFormBundleSection.xml -@@ -7,7 +7,7 @@ - --> - - -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> -
- - -@@ -22,6 +22,8 @@ - - - -+ -+ - - - -@@ -49,7 +51,9 @@ - - - -- -+ -+ -+ - - - -@@ -61,12 +65,34 @@ - - - -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ - -- -- - - -- -+ - - -
-diff --git a/app/code/Magento/Bundle/Test/Mftf/Section/BundleStorefrontSection.xml b/app/code/Magento/Bundle/Test/Mftf/Section/BundleStorefrontSection.xml -index 1a8709bd84e..7a188fd58e1 100644 ---- a/app/code/Magento/Bundle/Test/Mftf/Section/BundleStorefrontSection.xml -+++ b/app/code/Magento/Bundle/Test/Mftf/Section/BundleStorefrontSection.xml -@@ -7,7 +7,7 @@ - --> - - -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> -
- - -diff --git a/app/code/Magento/Bundle/Test/Mftf/Section/StorefrontBundledSection.xml b/app/code/Magento/Bundle/Test/Mftf/Section/StorefrontBundledSection.xml -index f724f9bbfe1..c47cf6095c7 100644 ---- a/app/code/Magento/Bundle/Test/Mftf/Section/StorefrontBundledSection.xml -+++ b/app/code/Magento/Bundle/Test/Mftf/Section/StorefrontBundledSection.xml -@@ -7,8 +7,10 @@ - --> - - -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> -
-+ -+ - - - -@@ -24,10 +26,17 @@ - - - -+ -+ - - - -+ - -+ - -+ -+ -+ -
-
-diff --git a/app/code/Magento/Bundle/Test/Mftf/Section/StorefrontCategoryProductSection.xml b/app/code/Magento/Bundle/Test/Mftf/Section/StorefrontCategoryProductSection.xml -index c76f822a091..3d5dc61d88a 100644 ---- a/app/code/Magento/Bundle/Test/Mftf/Section/StorefrontCategoryProductSection.xml -+++ b/app/code/Magento/Bundle/Test/Mftf/Section/StorefrontCategoryProductSection.xml -@@ -7,7 +7,7 @@ - --> - - -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> -
- - -diff --git a/app/code/Magento/Bundle/Test/Mftf/Section/StorefrontProductActionSection.xml b/app/code/Magento/Bundle/Test/Mftf/Section/StorefrontProductActionSection.xml -index abc9bc6dab5..eb92fd37564 100644 ---- a/app/code/Magento/Bundle/Test/Mftf/Section/StorefrontProductActionSection.xml -+++ b/app/code/Magento/Bundle/Test/Mftf/Section/StorefrontProductActionSection.xml -@@ -6,10 +6,12 @@ - */ - --> - -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> -
-- -+ - -- -+ -+ -+ -
-
-diff --git a/app/code/Magento/Bundle/Test/Mftf/Section/StorefrontProductInfoMainSection.xml b/app/code/Magento/Bundle/Test/Mftf/Section/StorefrontProductInfoMainSection.xml -index 41c00b5eda1..fae1ec331b6 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/Test/Mftf/Test/AdminAddBundleItemsTest.xml b/app/code/Magento/Bundle/Test/Mftf/Test/AdminAddBundleItemsTest.xml -index d94e196ea5a..401d360a34c 100644 ---- a/app/code/Magento/Bundle/Test/Mftf/Test/AdminAddBundleItemsTest.xml -+++ b/app/code/Magento/Bundle/Test/Mftf/Test/AdminAddBundleItemsTest.xml -@@ -7,7 +7,7 @@ - --> - - -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> - - - -diff --git a/app/code/Magento/Bundle/Test/Mftf/Test/AdminAddDefaultImageBundleProductTest.xml b/app/code/Magento/Bundle/Test/Mftf/Test/AdminAddDefaultImageBundleProductTest.xml -index e1f90790b30..21e6be98b31 100644 ---- a/app/code/Magento/Bundle/Test/Mftf/Test/AdminAddDefaultImageBundleProductTest.xml -+++ b/app/code/Magento/Bundle/Test/Mftf/Test/AdminAddDefaultImageBundleProductTest.xml -@@ -7,7 +7,7 @@ - --> - - -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> - - - -diff --git a/app/code/Magento/Bundle/Test/Mftf/Test/AdminAddDefaultVideoBundleProductTest.xml b/app/code/Magento/Bundle/Test/Mftf/Test/AdminAddDefaultVideoBundleProductTest.xml -new file mode 100644 -index 00000000000..c49202f31ae ---- /dev/null -+++ b/app/code/Magento/Bundle/Test/Mftf/Test/AdminAddDefaultVideoBundleProductTest.xml -@@ -0,0 +1,66 @@ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ <description value="Admin should be able to add default video for a Bundle Product"/> -+ <severity value="MAJOR"/> -+ <testCaseId value="MC-110"/> -+ <group value="Bundle"/> -+ </annotations> -+ <before> -+ <createData entity="SimpleProduct2" stepKey="simpleProduct1"/> -+ <createData entity="SimpleProduct2" stepKey="simpleProduct2"/> -+ </before> -+ <after> -+ <deleteData createDataKey="simpleProduct1" stepKey="deleteSimpleProduct1"/> -+ <deleteData createDataKey="simpleProduct2" stepKey="deleteSimpleProduct2"/> -+ </after> -+ -+ <!-- Create a bundle product --> -+ <!-- Replacing steps in base AdminAddDefaultVideoSimpleProductTest --> -+ <actionGroup ref="goToCreateProductPage" stepKey="goToCreateProductPage"> -+ <argument name="product" value="BundleProduct"/> -+ </actionGroup> -+ <actionGroup ref="fillProductNameAndSkuInProductForm" stepKey="fillMainProductForm"> -+ <argument name="product" value="BundleProduct"/> -+ </actionGroup> -+ -+ <!-- Add two bundle items --> -+ <scrollTo selector="{{AdminProductFormBundleSection.bundleItemsToggle}}" x="0" y="-100" stepKey="scrollToSection" after="addProductVideo"/> -+ <conditionalClick selector="{{AdminProductFormBundleSection.bundleItemsToggle}}" dependentSelector="{{AdminProductFormBundleSection.bundleItemsToggle}}" visible="false" stepKey="openBundleSection" after="scrollToSection"/> -+ <click selector="{{AdminProductFormBundleSection.addOption}}" stepKey="clickAddOption" after="openBundleSection"/> -+ <waitForElementVisible selector="{{AdminProductFormBundleSection.bundleOptionXTitle('0')}}" stepKey="waitForBundleTitle" after="clickAddOption"/> -+ <fillField selector="{{AdminProductFormBundleSection.bundleOptionXTitle('0')}}" userInput="{{BundleProduct.optionTitle1}}" stepKey="fillBundleTitle" after="waitForBundleTitle"/> -+ <selectOption selector="{{AdminProductFormBundleSection.bundleOptionXInputType('0')}}" userInput="{{BundleProduct.optionInputType1}}" stepKey="selectOptionBundleTitle" after="fillBundleTitle"/> -+ <waitForElementVisible selector="{{AdminProductFormBundleSection.addProductsToOption}}" stepKey="waitForAddProducts" after="selectOptionBundleTitle"/> -+ <click selector="{{AdminProductFormBundleSection.addProductsToOption}}" stepKey="clickAddProducts" after="waitForAddProducts"/> -+ <waitForPageLoad stepKey="waitForPageLoad" after="clickAddProducts"/> -+ <actionGroup ref="filterProductGridBySku" stepKey="filterProductGridBySku1" after="waitForPageLoad"> -+ <argument name="product" value="$$simpleProduct1$$"/> -+ </actionGroup> -+ <checkOption selector="{{AdminAddProductsToOptionPanel.firstCheckbox}}" stepKey="checkOption1" after="filterProductGridBySku1"/> -+ <actionGroup ref="filterProductGridBySku" stepKey="filterProductGridBySku2" after="checkOption1"> -+ <argument name="product" value="$$simpleProduct2$$"/> -+ </actionGroup> -+ <checkOption selector="{{AdminAddProductsToOptionPanel.firstCheckbox}}" stepKey="checkOption2" after="filterProductGridBySku2"/> -+ <click selector="{{AdminAddProductsToOptionPanel.addSelectedProducts}}" stepKey="addProducts" after="checkOption2"/> -+ <fillField selector="{{AdminProductFormBundleSection.bundleOptionXProductYQuantity('0', '0')}}" userInput="{{BundleProduct.defaultQuantity}}" stepKey="fillQty1" after="addProducts"/> -+ <fillField selector="{{AdminProductFormBundleSection.bundleOptionXProductYQuantity('0', '1')}}" userInput="{{BundleProduct.defaultQuantity}}" stepKey="fillQty2" before="saveProductForm"/> -+ -+ <!-- Assert product in storefront product page --> -+ <actionGroup ref="AssertProductNameAndSkuInStorefrontProductPage" stepKey="AssertProductInStorefrontProductPage"> -+ <argument name="product" value="BundleProduct"/> -+ </actionGroup> -+ </test> -+</tests> -diff --git a/app/code/Magento/Bundle/Test/Mftf/Test/AdminAttributeSetSelectionTest.xml b/app/code/Magento/Bundle/Test/Mftf/Test/AdminAttributeSetSelectionTest.xml -index 795982eb4b9..1d2f21b7d15 100644 ---- a/app/code/Magento/Bundle/Test/Mftf/Test/AdminAttributeSetSelectionTest.xml -+++ b/app/code/Magento/Bundle/Test/Mftf/Test/AdminAttributeSetSelectionTest.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="AdminAttributeSetSelectionTest"> - <annotations> - <features value="Bundle"/> -diff --git a/app/code/Magento/Bundle/Test/Mftf/Test/AdminBasicBundleProductAttributesTest.xml b/app/code/Magento/Bundle/Test/Mftf/Test/AdminBasicBundleProductAttributesTest.xml -new file mode 100644 -index 00000000000..c6a07f7ed95 ---- /dev/null -+++ b/app/code/Magento/Bundle/Test/Mftf/Test/AdminBasicBundleProductAttributesTest.xml -@@ -0,0 +1,189 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> -+ <test name="AdminBasicBundleProductAttributesTest"> -+ <annotations> -+ <features value="Bundle"/> -+ <stories value="Create/Edit bundle product in Admin"/> -+ <title value="Admin should be able to set/edit all the basic product attributes when creating/editing a bundle product"/> -+ <description value="Admin should be able to set/edit all the basic product attributes when creating/editing a bundle product"/> -+ <severity value="CRITICAL"/> -+ <testCaseId value="MC-222"/> -+ <group value="Bundle"/> -+ </annotations> -+ <before> -+ <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> -+ </before> -+ <after> -+ <amOnPage url="{{AdminLogoutPage.url}}" stepKey="amOnLogoutPage"/> -+ </after> -+ <!--Create attribute set--> -+ <actionGroup ref="CreateDefaultAttributeSet" stepKey="createDefaultAttributeSet"> -+ <argument name="label" value="{{ProductAttributeFrontendLabel.label}}"/> -+ </actionGroup> -+ -+ <!--Go to product creation page--> -+ <amOnPage url="{{AdminProductCreatePage.url(BundleProduct.set, BundleProduct.type)}}" stepKey="goToBundleProductCreationPage"/> -+ <waitForPageLoad stepKey="waitForBundleProductCreationPage"/> -+ -+ <!--Enable/Disable Toggle--> -+ <checkOption selector="{{AdminProductFormBundleSection.enableDisableToggle}}" stepKey="clickOnEnableDisableToggle"/> -+ -+ <!--Fill out product attributes--> -+ <actionGroup ref="SetBundleProductAttributes" stepKey="fillOutAllAttributes"> -+ <!--primarily uses default values--> -+ <argument name="attributeSet" value="{{ProductAttributeFrontendLabel.label}}"/> -+ <argument name="bundleProductName" value="{{BundleProduct.name}}"/> -+ <argument name="bundleProductSku" value="{{BundleProduct.sku}}"/> -+ <argument name="visibilty" value="catalog"/> -+ </actionGroup> -+ -+ <!--Verify form was filled out correctly--> -+ -+ <!--Enable/Disable Toggle check--> -+ <dontSeeCheckboxIsChecked selector="{{AdminProductFormBundleSection.enableDisableToggle}}" stepKey="seeToggleIsOff"/> -+ -+ <!--Apply Attribute Set--> -+ <seeOptionIsSelected selector="{{AdminProductFormSection.attributeSet}}" userInput="{{ProductAttributeFrontendLabel.label}}" stepKey="seeAttributeSet"/> -+ -+ <!--Product name and SKU--> -+ <seeInField selector="{{AdminProductFormBundleSection.productName}}" userInput="{{BundleProduct.name}}" stepKey="seeProductName"/> -+ <seeInField selector="{{AdminProductFormBundleSection.productSku}}" userInput="{{BundleProduct.sku}}" stepKey="seeProductSku"/> -+ -+ <!--Dynamic SKU Toggle--> -+ <dontSeeCheckboxIsChecked selector="{{AdminProductFormBundleSection.dynamicSkuToggle}}" stepKey="seeDynamicSkuToggleOff"/> -+ -+ <!--Dynamic Price Toggle--> -+ <dontSeeCheckboxIsChecked selector="{{AdminProductFormBundleSection.dynamicPriceToggle}}" stepKey="seeDynamicPriceToggleOff"/> -+ -+ <!--Tax Class--> -+ <seeOptionIsSelected selector="{{AdminProductFormBundleSection.taxClassDropDown}}" userInput="Taxable Goods" stepKey="seeCorrectTaxClass"/> -+ -+ <!--Fill out price--> -+ <seeInField selector="{{AdminProductFormBundleSection.priceField}}" userInput="10" stepKey="seePrice"/> -+ -+ <!--Stock status--> -+ <seeOptionIsSelected selector="{{AdminProductFormBundleSection.stockStatusField}}" userInput="In Stock" stepKey="seeStockStatus"/> -+ -+ <!--Dynamic weight--> -+ <dontSeeCheckboxIsChecked selector="{{AdminProductFormBundleSection.dynamicWeightToggle}}" stepKey="seeDynamicWeightOff"/> -+ -+ <!--Weight--> -+ <seeInField selector="{{AdminProductFormBundleSection.weightField}}" userInput="10" stepKey="seeWeight"/> -+ -+ <!--Visibilty--> -+ <seeOptionIsSelected selector="{{AdminProductFormBundleSection.visibilityDropDown}}" userInput="Catalog" stepKey="seeVisibility"/> -+ -+ <!--Categories--> -+ <seeElement selector="{{AdminProductFormBundleSection.defaultCategory}}" stepKey="seeDefaultCategory"/> -+ -+ <!--New from - to--> -+ <seeInField selector="{{AdminProductFormBundleSection.fromDate}}" userInput="10/10/2018" stepKey="seeFirstDate"/> -+ <seeInField selector="{{AdminProductFormBundleSection.toDate}}" userInput="10/10/2018" stepKey="seeSecondDate"/> -+ -+ <!--Country of manufacture--> -+ <seeOptionIsSelected selector="{{AdminProductFormBundleSection.countryOfManufactureDropDown}}" userInput="Italy" stepKey="seeCountryOfManufacture"/> -+ -+ <!--Create second attribute set for edit--> -+ <actionGroup ref="CreateDefaultAttributeSet" stepKey="createSecondAttributeSet"> -+ <argument name="label" value="{{ProductAttributeFrontendLabelTwo.label}}"/> -+ </actionGroup> -+ -+ <!--Filter catalog--> -+ <amOnPage url="{{AdminCatalogProductPage.url}}" stepKey="goToCatalogProductPage"/> -+ <waitForPageLoad stepKey="WaitForPageToLoad"/> -+ <actionGroup ref="filterProductGridByName" stepKey="filterBundleProductOptionsDownToName"> -+ <argument name="product" value="BundleProduct"/> -+ </actionGroup> -+ <click selector="{{AdminProductFiltersSection.attributeSetOfFirstRow(ProductAttributeFrontendLabel.label)}}" stepKey="clickAttributeSet2"/> -+ <waitForPageLoad stepKey="waitForPageLoad2"/> -+ -+ <!--Edit fields--> -+ -+ <!--Enable/Disable Toggle--> -+ <checkOption selector="{{AdminProductFormBundleSection.enableDisableToggle}}" stepKey="clickOnEnableDisableToggleAgain"/> -+ -+ <!--Apply Attribute Set--> -+ <click selector="{{AdminProductFormSection.attributeSet}}" stepKey="startEditAttrSet"/> -+ <fillField selector="{{AdminProductFormSection.attributeSetFilter}}" userInput="{{ProductAttributeFrontendLabelTwo.label}}" stepKey="searchForAttrSet"/> -+ <click selector="{{AdminProductFormSection.attributeSetFilterResultByName(ProductAttributeFrontendLabelTwo.label)}}" stepKey="selectAttrSet"/> -+ -+ <!--Product name and SKU--> -+ <fillField selector="{{AdminProductFormBundleSection.productName}}" userInput="{{BundleProduct.name2}}" stepKey="fillProductName"/> -+ <fillField selector="{{AdminProductFormBundleSection.productSku}}" userInput="{{BundleProduct.sku2}}" stepKey="fillProductSku"/> -+ <click selector="{{AdminProductFormBundleSection.productName}}" stepKey="clickUnselectField"/> -+ -+ <!--Dynamic SKU Toggle--> -+ <checkOption selector="{{AdminProductFormBundleSection.dynamicSkuToggle}}" stepKey="clickOnToggle"/> -+ <click selector="{{AdminProductFormBundleSection.productName}}" stepKey="clickUnselectFieldAgain"/> -+ -+ <!--Fill out price--> -+ <fillField selector="{{AdminProductFormBundleSection.priceField}}" userInput="20" stepKey="fillOutPrice"/> -+ -+ <!--Stock status--> -+ <selectOption selector="{{AdminProductFormBundleSection.stockStatusField}}" userInput="Out of Stock" stepKey="stockStatus"/> -+ -+ <!--Dynamic weight--> -+ <checkOption selector="{{AdminProductFormBundleSection.dynamicWeightToggle}}" stepKey="dynamicWeight"/> -+ -+ <!--Visibilty--> -+ <selectOption selector="{{AdminProductFormBundleSection.visibilityDropDown}}" userInput="Not Visible Individually" stepKey="openVisibility"/> -+ -+ <!--New from - to--> -+ <fillField selector="{{AdminProductFormBundleSection.fromDate}}" userInput="10/20/2018" stepKey="fillInFirstDate"/> -+ <fillField selector="{{AdminProductFormBundleSection.toDate}}" userInput="10/20/2018" stepKey="fillInSecondDate"/> -+ -+ <!--Country of manufacture--> -+ <selectOption selector="{{AdminProductFormBundleSection.countryOfManufactureDropDown}}" userInput="France" stepKey="countryOfManufactureDropDown"/> -+ -+ <!--Save the product--> -+ <click selector="{{AdminProductFormActionSection.saveButton}}" stepKey="clickSaveButton"/> -+ <seeElement selector="{{AdminCategoryMessagesSection.SuccessMessage}}" stepKey="messageYouSavedTheProductIsShown"/> -+ -+ <!--Verify form was filled out correctly after edit--> -+ -+ <!--Enable/Disable Toggle--> -+ <seeElement selector="{{AdminProductFormBundleSection.enableDisableToggleOn}}" stepKey="seeToggleIsOn2"/> -+ -+ <!--Attribute Set--> -+ <seeOptionIsSelected selector="{{AdminProductFormSection.attributeSet}}" userInput="{{ProductAttributeFrontendLabelTwo.label}}" stepKey="seeAttributeSet2"/> -+ -+ <!--Product name and SKU--> -+ <seeInField selector="{{AdminProductFormBundleSection.productName}}" userInput="{{BundleProduct.name2}}" stepKey="seeProductName2"/> -+ <seeInField selector="{{AdminProductFormBundleSection.productSku}}" userInput="{{BundleProduct.sku2}}" stepKey="seeProductSku2"/> -+ -+ <!--Dynamic SKU Toggle--> -+ <seeElement selector="{{AdminProductFormBundleSection.dynamicSkuToggleOn}}" stepKey="seeDynamicSkuToggleOn2"/> -+ -+ <!--Tax Class--> -+ <seeOptionIsSelected selector="{{AdminProductFormBundleSection.taxClassDropDown}}" userInput="Taxable Goods" stepKey="seeCorrectTaxClass2"/> -+ -+ <!--Price--> -+ <seeInField selector="{{AdminProductFormBundleSection.priceField}}" userInput="20" stepKey="seePrice2"/> -+ -+ <!--Stock status--> -+ <seeOptionIsSelected selector="{{AdminProductFormBundleSection.stockStatusField}}" userInput="Out of Stock" stepKey="seeStockStatus2"/> -+ -+ <!--Dynamic weight--> -+ <seeElement selector="{{AdminProductFormBundleSection.dynamicWeightToggleOn}}" stepKey="seeDynamicWeightOn2"/> -+ -+ <!--Visibilty--> -+ <seeOptionIsSelected selector="{{AdminProductFormBundleSection.visibilityDropDown}}" userInput="Not Visible Individually" stepKey="seeVisibility2"/> -+ -+ <!--Categories--> -+ <seeElement selector="{{AdminProductFormBundleSection.categoriesDropDown}}" stepKey="seeDefaultCategory2"/> -+ -+ <!--New from - to--> -+ <seeInField selector="{{AdminProductFormBundleSection.fromDate}}" userInput="10/20/2018" stepKey="seeFirstDate2"/> -+ <seeInField selector="{{AdminProductFormBundleSection.toDate}}" userInput="10/20/2018" stepKey="seeSecondDate2"/> -+ -+ <!--Country of manufacture--> -+ <seeOptionIsSelected selector="{{AdminProductFormBundleSection.countryOfManufactureDropDown}}" userInput="France" stepKey="seeCountryOfManufacture2"/> -+ </test> -+</tests> -diff --git a/app/code/Magento/Bundle/Test/Mftf/Test/AdminBundleProductSetEditContentTest.xml b/app/code/Magento/Bundle/Test/Mftf/Test/AdminBundleProductSetEditContentTest.xml -new file mode 100644 -index 00000000000..65733a5bcc0 ---- /dev/null -+++ b/app/code/Magento/Bundle/Test/Mftf/Test/AdminBundleProductSetEditContentTest.xml -@@ -0,0 +1,40 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> -+ <test name="AdminBundleProductSetEditContentTest" extends="AdminSimpleProductSetEditContentTest"> -+ <annotations> -+ <features value="Bundle"/> -+ <stories value="Create/Edit bundle product in Admin"/> -+ <title value="Admin should be able to set/edit product Content when editing a bundle product"/> -+ <description value="Admin should be able to set/edit product Content when editing a bundle product"/> -+ <severity value="CRITICAL"/> -+ <testCaseId value="MC-3343"/> -+ <group value="Bundle"/> -+ <group value="WYSIWYGDisabled"/> -+ </annotations> -+ <after> -+ <!-- Delete bundle product --> -+ <actionGroup ref="deleteProductUsingProductGrid" stepKey="deleteProduct"> -+ <argument name="product" value="BundleProduct"/> -+ </actionGroup> -+ </after> -+ -+ <!-- Create product --> -+ <actionGroup ref="goToCreateProductPage" stepKey="goToCreateProduct"> -+ <argument name="product" value="BundleProduct"/> -+ </actionGroup> -+ <actionGroup ref="fillProductNameAndSkuInProductForm" stepKey="fillProductForm"> -+ <argument name="product" value="BundleProduct"/> -+ </actionGroup> -+ -+ <!--Checking content storefront--> -+ <amOnPage url="{{BundleProduct.sku}}.html" stepKey="goToStorefront"/> -+ </test> -+</tests> -diff --git a/app/code/Magento/Bundle/Test/Mftf/Test/AdminDeleteABundleProduct.xml b/app/code/Magento/Bundle/Test/Mftf/Test/AdminDeleteABundleProduct.xml -index bf62212babd..86db6f372b5 100644 ---- a/app/code/Magento/Bundle/Test/Mftf/Test/AdminDeleteABundleProduct.xml -+++ b/app/code/Magento/Bundle/Test/Mftf/Test/AdminDeleteABundleProduct.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="AdminDeleteABundleProduct"> - <annotations> - <features value="Bundle"/> -diff --git a/app/code/Magento/Bundle/Test/Mftf/Test/AdminDeleteBundleDynamicProductTest.xml b/app/code/Magento/Bundle/Test/Mftf/Test/AdminDeleteBundleDynamicProductTest.xml -new file mode 100644 -index 00000000000..a4e26256e97 ---- /dev/null -+++ b/app/code/Magento/Bundle/Test/Mftf/Test/AdminDeleteBundleDynamicProductTest.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="AdminDeleteBundleDynamicProductTest"> -+ <annotations> -+ <features value="Bundle"/> -+ <stories value="Delete products"/> -+ <title value="Delete Bundle Dynamic Product"/> -+ <description value="Admin should be able to delete a bundle dynamic product"/> -+ <severity value="CRITICAL"/> -+ <testCaseId value="MC-11016"/> -+ <group value="mtf_migrated"/> -+ <skip> -+ <issueId value="MC-16393"/> -+ </skip> -+ </annotations> -+ <before> -+ <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> -+ <createData entity="_defaultCategory" stepKey="createCategory"/> -+ <createData entity="ApiBundleProductPriceViewRange" stepKey="createDynamicBundleProduct"> -+ <requiredEntity createDataKey="createCategory"/> -+ </createData> -+ </before> -+ <after> -+ <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> -+ <actionGroup ref="logout" stepKey="logout"/> -+ </after> -+ <actionGroup ref="deleteProductUsingProductGrid" stepKey="deleteBundleProductFilteredBySkuAndName"> -+ <argument name="product" value="$$createDynamicBundleProduct$$"/> -+ </actionGroup> -+ <see selector="{{AdminCategoryMessagesSection.SuccessMessage}}" userInput="A total of 1 record(s) have been deleted." stepKey="deleteMessage"/> -+ <!-- Verify product on Product Page --> -+ <amOnPage url="{{StorefrontProductPage.url($$createDynamicBundleProduct.name$$)}}" stepKey="amOnBundleProductPage"/> -+ <see selector="{{StorefrontProductInfoMainSection.productName}}" userInput="Whoops, our bad..." stepKey="seeWhoops"/> -+ <!-- Search for the product by sku --> -+ <fillField selector="{{StorefrontQuickSearchSection.searchPhrase}}" userInput="$$createDynamicBundleProduct.sku$$" stepKey="fillSearchBarByProductSku"/> -+ <waitForPageLoad stepKey="waitForSearchButton"/> -+ <click selector="{{StorefrontQuickSearchSection.searchButton}}" stepKey="clickSearchButton"/> -+ <waitForPageLoad stepKey="waitForSearchResults"/> -+ <!-- Should not see any search results --> -+ <dontSee userInput="$$createDynamicBundleProduct.sku$$" selector="{{StorefrontCatalogSearchMainSection.searchResults}}" stepKey="dontSeeProduct"/> -+ <see selector="{{StorefrontCatalogSearchMainSection.message}}" userInput="Your search returned no results." stepKey="seeCantFindProductOneMessage"/> -+ <!-- Go to the category page that we created in the before block --> -+ <amOnPage url="{{StorefrontCategoryPage.url($$createCategory.name$$)}}" stepKey="onCategoryPage"/> -+ <!-- Should not see the product --> -+ <dontSee userInput="$$createDynamicBundleProduct.name$$" selector="{{StorefrontCategoryMainSection.productsList}}" stepKey="dontSeeProductInCategory"/> -+ <see selector="{{StorefrontCategoryMainSection.emptyProductMessage}}" userInput="We can't find products matching the selection." stepKey="seeEmptyProductMessage"/> -+ </test> -+</tests> -diff --git a/app/code/Magento/Bundle/Test/Mftf/Test/AdminDeleteBundleFixedProductTest.xml b/app/code/Magento/Bundle/Test/Mftf/Test/AdminDeleteBundleFixedProductTest.xml -new file mode 100644 -index 00000000000..2527dae7ead ---- /dev/null -+++ b/app/code/Magento/Bundle/Test/Mftf/Test/AdminDeleteBundleFixedProductTest.xml -@@ -0,0 +1,52 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> -+ <test name="AdminDeleteBundleFixedProductTest"> -+ <annotations> -+ <features value="Bundle"/> -+ <stories value="Delete products"/> -+ <title value="Delete Bundle Fixed Product"/> -+ <description value="Admin should be able to delete a bundle fixed product"/> -+ <severity value="CRITICAL"/> -+ <testCaseId value="MC-11017"/> -+ <group value="mtf_migrated"/> -+ </annotations> -+ <before> -+ <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> -+ <createData entity="_defaultCategory" stepKey="createCategory"/> -+ <createData entity="FixedBundleProduct" stepKey="createFixedBundleProduct"> -+ <requiredEntity createDataKey="createCategory"/> -+ </createData> -+ </before> -+ <after> -+ <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> -+ <actionGroup ref="logout" stepKey="logout"/> -+ </after> -+ <actionGroup ref="deleteProductUsingProductGrid" stepKey="deleteBundleProductFilteredBySkuAndName"> -+ <argument name="product" value="$$createFixedBundleProduct$$"/> -+ </actionGroup> -+ <see selector="{{AdminCategoryMessagesSection.SuccessMessage}}" userInput="A total of 1 record(s) have been deleted." stepKey="deleteMessage"/> -+ <!-- Verify product on Product Page --> -+ <amOnPage url="{{StorefrontProductPage.url($$createFixedBundleProduct.name$$)}}" stepKey="amOnBundleProductPage"/> -+ <see selector="{{StorefrontProductInfoMainSection.productName}}" userInput="Whoops, our bad..." stepKey="seeWhoops"/> -+ <!-- Search for the product by sku --> -+ <fillField selector="{{StorefrontQuickSearchSection.searchPhrase}}" userInput="$$createFixedBundleProduct.sku$$" stepKey="fillSearchBarByProductSku"/> -+ <waitForPageLoad stepKey="waitForSearchButton"/> -+ <click selector="{{StorefrontQuickSearchSection.searchButton}}" stepKey="clickSearchButton"/> -+ <waitForPageLoad stepKey="waitForSearchResults"/> -+ <!-- Should not see any search results --> -+ <dontSee userInput="$$createFixedBundleProduct.sku$$" selector="{{StorefrontCatalogSearchMainSection.searchResults}}" stepKey="dontSeeProduct"/> -+ <see selector="{{StorefrontCatalogSearchMainSection.message}}" userInput="Your search returned no results." stepKey="seeCantFindProductOneMessage"/> -+ <!-- Go to the category page that we created in the before block --> -+ <amOnPage url="{{StorefrontCategoryPage.url($$createCategory.name$$)}}" stepKey="onCategoryPage"/> -+ <!-- Should not see the product --> -+ <dontSee userInput="$$createFixedBundleProduct.name$$" selector="{{StorefrontCategoryMainSection.productsList}}" stepKey="dontSeeProductInCategory"/> -+ <see selector="{{StorefrontCategoryMainSection.emptyProductMessage}}" userInput="We can't find products matching the selection." stepKey="seeEmptyProductMessage"/> -+ </test> -+</tests> -diff --git a/app/code/Magento/Bundle/Test/Mftf/Test/AdminEditRelatedBundleProductTest.xml b/app/code/Magento/Bundle/Test/Mftf/Test/AdminEditRelatedBundleProductTest.xml -new file mode 100644 -index 00000000000..08faa9d2444 ---- /dev/null -+++ b/app/code/Magento/Bundle/Test/Mftf/Test/AdminEditRelatedBundleProductTest.xml -@@ -0,0 +1,78 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> -+ <test name="AdminEditRelatedBundleProductTest"> -+ <annotations> -+ <features value="Bundle"/> -+ <stories value="Create/Edit bundle product in Admin"/> -+ <title value="Admin should be able to set/edit Related Products information when editing a bundle product"/> -+ <description value="Admin should be able to set/edit Related Products information when editing a bundle product"/> -+ <severity value="CRITICAL"/> -+ <testCaseId value="MC-3342"/> -+ <group value="Bundle"/> -+ </annotations> -+ <before> -+ <!--Admin login--> -+ <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> -+ <createData entity="SimpleProduct2" stepKey="simpleProduct0"/> -+ <createData entity="SimpleProduct2" stepKey="simpleProduct1"/> -+ </before> -+ <after> -+ <!-- Delete the bundled product --> -+ <actionGroup stepKey="deleteBundle" ref="deleteProductUsingProductGrid"> -+ <argument name="product" value="BundleProduct"/> -+ </actionGroup> -+ <!--Logging out--> -+ <amOnPage url="{{AdminLogoutPage.url}}" stepKey="amOnLogoutPage"/> -+ <deleteData createDataKey="simpleProduct0" stepKey="deleteSimpleProduct0"/> -+ <deleteData createDataKey="simpleProduct1" stepKey="deleteSimpleProduct1"/> -+ </after> -+ -+ <!-- Create a bundle product --> -+ <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="visitAdminProductPageBundle"/> -+ <waitForPageLoad stepKey="waitForProductPageLoadBundle"/> -+ <actionGroup ref="goToCreateProductPage" stepKey="goToCreateBundleProduct"> -+ <argument name="product" value="BundleProduct"/> -+ </actionGroup> -+ -+ <actionGroup ref="fillProductNameAndSkuInProductForm" stepKey="fillBundleProductNameAndSku"> -+ <argument name="product" value="BundleProduct"/> -+ </actionGroup> -+ -+ <actionGroup ref="addRelatedProductBySku" stepKey="addRelatedProduct0"> -+ <argument name="sku" value="$$simpleProduct0.sku$$"/> -+ </actionGroup> -+ -+ <!--Save the product--> -+ <click selector="{{AdminProductFormActionSection.saveButton}}" stepKey="clickSaveButton"/> -+ <see selector="{{AdminCategoryMessagesSection.SuccessMessage}}" userInput="You saved the product." stepKey="messageYouSavedTheProductIsShown"/> -+ -+ <actionGroup ref="addRelatedProductBySku" stepKey="addRelatedProduct1"> -+ <argument name="sku" value="$$simpleProduct1.sku$$"/> -+ </actionGroup> -+ -+ <!--Remove previous related product--> -+ <click selector="{{AdminProductFormRelatedUpSellCrossSellSection.removeRelatedProduct($$simpleProduct0.sku$$)}}" stepKey="removeRelatedProduct"/> -+ -+ <!--Save the product--> -+ <click selector="{{AdminProductFormActionSection.saveButton}}" stepKey="clickSaveButtonAfterEdit"/> -+ <see selector="{{AdminCategoryMessagesSection.SuccessMessage}}" userInput="You saved the product." stepKey="messageYouSavedTheProductIsShownAgain"/> -+ -+ <!--See related product in admin--> -+ <scrollTo selector="{{AdminProductFormRelatedUpSellCrossSellSection.relatedDropdown}}" stepKey="scrollTo"/> -+ <conditionalClick selector="{{AdminProductFormRelatedUpSellCrossSellSection.relatedDropdown}}" dependentSelector="{{AdminProductFormRelatedUpSellCrossSellSection.relatedDependent}}" visible="false" stepKey="openDropDownIfClosedRelatedSee"/> -+ <see selector="{{AdminProductFormRelatedUpSellCrossSellSection.selectedRelatedProduct}}" userInput="$$simpleProduct1.sku$$" stepKey="seeRelatedProduct"/> -+ -+ <!--See related product in storefront--> -+ <amOnPage url="{{BundleProduct.sku}}.html" stepKey="goToStorefront"/> -+ <waitForPageLoad stepKey="waitForStorefront"/> -+ <see userInput="$$simpleProduct1.sku$$" stepKey="seeRelatedProductInStorefront"/> -+ </test> -+</tests> -diff --git a/app/code/Magento/Bundle/Test/Mftf/Test/AdminFilterProductListByBundleProduct.xml b/app/code/Magento/Bundle/Test/Mftf/Test/AdminFilterProductListByBundleProduct.xml -index 9faf9e69bc8..40a6e1b75c6 100644 ---- a/app/code/Magento/Bundle/Test/Mftf/Test/AdminFilterProductListByBundleProduct.xml -+++ b/app/code/Magento/Bundle/Test/Mftf/Test/AdminFilterProductListByBundleProduct.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="AdminFilterProductListByBundleProduct"> - <annotations> - <features value="Bundle"/> -diff --git a/app/code/Magento/Bundle/Test/Mftf/Test/AdminMassDeleteBundleProducts.xml b/app/code/Magento/Bundle/Test/Mftf/Test/AdminMassDeleteBundleProducts.xml -index 6cb86d80283..2f891fcc8f1 100644 ---- a/app/code/Magento/Bundle/Test/Mftf/Test/AdminMassDeleteBundleProducts.xml -+++ b/app/code/Magento/Bundle/Test/Mftf/Test/AdminMassDeleteBundleProducts.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="AdminMassDeleteBundleProductsTest"> - <annotations> - <features value="Bundle"/> -@@ -99,8 +99,8 @@ - <fillField selector="{{AdminProductFormBundleSection.productSku}}" userInput="{{BundleProduct.sku2}}" stepKey="fillProductSku2"/> - - <!--Trigger SEO drop down--> -- <conditionalClick selector="{{AdminProductFormBundleSection.seoDropdown}}" dependentSelector="{{AdminProductFormBundleSection.seoDependent}}" visible="false" stepKey="OpenDropDownIfClosed2"/> -- <waitForPageLoad stepKey="WaitForDropDownSEO"/> -+ <scrollTo selector="{{AdminProductFormBundleSection.seoDropdown}}" stepKey="moveToSEOSection"/> -+ <conditionalClick selector="{{AdminProductFormBundleSection.seoDropdown}}" dependentSelector="{{AdminProductFormBundleSection.urlKey}}" visible="false" stepKey="openDropDownIfClosed"/> - - <!--Fill URL input--> - <fillField userInput="{{BundleProduct.urlKey2}}" selector="{{AdminProductFormBundleSection.urlKey}}" stepKey="FillsinSEOlinkExtension2"/> -diff --git a/app/code/Magento/Bundle/Test/Mftf/Test/AdminProductBundleCreationTest.xml b/app/code/Magento/Bundle/Test/Mftf/Test/AdminProductBundleCreationTest.xml -index 643f13dfd61..1f46e1fc9f0 100644 ---- a/app/code/Magento/Bundle/Test/Mftf/Test/AdminProductBundleCreationTest.xml -+++ b/app/code/Magento/Bundle/Test/Mftf/Test/AdminProductBundleCreationTest.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="AdminProductBundleCreationTest"> - <annotations> - <features value="Bundle"/> -@@ -31,6 +31,8 @@ - <deleteData createDataKey="createPreReqCategory" stepKey="deletePreReqCategory"/> - <deleteData createDataKey="simpleProduct1" stepKey="deleteSimpleProduct1"/> - <deleteData createDataKey="simpleProduct2" stepKey="deleteSimpleProduct2"/> -+ <actionGroup ref="AdminOpenProductIndexPageActionGroup" stepKey="navigateToProductIndexPage"/> -+ <actionGroup ref="deleteProductsIfTheyExist" stepKey="deleteAllProducts"/> - <actionGroup ref="logout" stepKey="logout"/> - </after> - <!-- go to bundle product creation page--> -diff --git a/app/code/Magento/Bundle/Test/Mftf/Test/AdminRemoveDefaultImageBundleProductTest.xml b/app/code/Magento/Bundle/Test/Mftf/Test/AdminRemoveDefaultImageBundleProductTest.xml -index ccd729ac841..1438958b92b 100644 ---- a/app/code/Magento/Bundle/Test/Mftf/Test/AdminRemoveDefaultImageBundleProductTest.xml -+++ b/app/code/Magento/Bundle/Test/Mftf/Test/AdminRemoveDefaultImageBundleProductTest.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="AdminRemoveDefaultImageBundleProductTest"> - <annotations> - <features value="Bundle"/> -diff --git a/app/code/Magento/Bundle/Test/Mftf/Test/AdminRemoveDefaultVideoBundleProductTest.xml b/app/code/Magento/Bundle/Test/Mftf/Test/AdminRemoveDefaultVideoBundleProductTest.xml -new file mode 100644 -index 00000000000..d050c5443d1 ---- /dev/null -+++ b/app/code/Magento/Bundle/Test/Mftf/Test/AdminRemoveDefaultVideoBundleProductTest.xml -@@ -0,0 +1,66 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> -+ <test name="AdminRemoveDefaultVideoBundleProductTest" extends="AdminRemoveDefaultVideoSimpleProductTest"> -+ <annotations> -+ <features value="Bundle"/> -+ <stories value="Add/remove images and videos for all product types and category"/> -+ <title value="Admin should be able to remove default video from a Bundle Product"/> -+ <description value="Admin should be able to remove default video from a Bundle Product"/> -+ <severity value="MAJOR"/> -+ <testCaseId value="MC-205"/> -+ <group value="Bundle"/> -+ </annotations> -+ <before> -+ <createData entity="SimpleProduct2" stepKey="simpleProduct1"/> -+ <createData entity="SimpleProduct2" stepKey="simpleProduct2"/> -+ </before> -+ <after> -+ <deleteData createDataKey="simpleProduct1" stepKey="deleteSimpleProduct1"/> -+ <deleteData createDataKey="simpleProduct2" stepKey="deleteSimpleProduct2"/> -+ </after> -+ -+ <!-- Create a bundle product --> -+ <!-- Replacing steps in base AdminRemoveDefaultVideoSimpleProductTest --> -+ <actionGroup ref="goToCreateProductPage" stepKey="goToCreateProductPage"> -+ <argument name="product" value="BundleProduct"/> -+ </actionGroup> -+ <actionGroup ref="fillProductNameAndSkuInProductForm" stepKey="fillMainProductForm"> -+ <argument name="product" value="BundleProduct"/> -+ </actionGroup> -+ -+ <!-- Add two bundle items --> -+ <scrollTo selector="{{AdminProductFormBundleSection.bundleItemsToggle}}" x="0" y="-100" stepKey="scrollToSection" after="addProductVideo"/> -+ <conditionalClick selector="{{AdminProductFormBundleSection.bundleItemsToggle}}" dependentSelector="{{AdminProductFormBundleSection.bundleItemsToggle}}" visible="false" stepKey="openBundleSection" after="scrollToSection"/> -+ <click selector="{{AdminProductFormBundleSection.addOption}}" stepKey="clickAddOption" after="openBundleSection"/> -+ <waitForElementVisible selector="{{AdminProductFormBundleSection.bundleOptionXTitle('0')}}" stepKey="waitForBundleTitle" after="clickAddOption"/> -+ <fillField selector="{{AdminProductFormBundleSection.bundleOptionXTitle('0')}}" userInput="{{BundleProduct.optionTitle1}}" stepKey="fillBundleTitle" after="waitForBundleTitle"/> -+ <selectOption selector="{{AdminProductFormBundleSection.bundleOptionXInputType('0')}}" userInput="{{BundleProduct.optionInputType1}}" stepKey="selectOptionBundleTitle" after="fillBundleTitle"/> -+ <waitForElementVisible selector="{{AdminProductFormBundleSection.addProductsToOption}}" stepKey="waitForAddProducts" after="selectOptionBundleTitle"/> -+ <click selector="{{AdminProductFormBundleSection.addProductsToOption}}" stepKey="clickAddProducts" after="waitForAddProducts"/> -+ <waitForPageLoad stepKey="waitForPageLoad" after="clickAddProducts"/> -+ <actionGroup ref="filterProductGridBySku" stepKey="filterProductGridBySku1" after="waitForPageLoad"> -+ <argument name="product" value="$$simpleProduct1$$"/> -+ </actionGroup> -+ <checkOption selector="{{AdminAddProductsToOptionPanel.firstCheckbox}}" stepKey="checkOption1" after="filterProductGridBySku1"/> -+ <actionGroup ref="filterProductGridBySku" stepKey="filterProductGridBySku2" after="checkOption1"> -+ <argument name="product" value="$$simpleProduct2$$"/> -+ </actionGroup> -+ <checkOption selector="{{AdminAddProductsToOptionPanel.firstCheckbox}}" stepKey="checkOption2" after="filterProductGridBySku2"/> -+ <click selector="{{AdminAddProductsToOptionPanel.addSelectedProducts}}" stepKey="addProducts" after="checkOption2"/> -+ <fillField selector="{{AdminProductFormBundleSection.bundleOptionXProductYQuantity('0', '0')}}" userInput="{{BundleProduct.defaultQuantity}}" stepKey="fillQty1" after="addProducts"/> -+ <fillField selector="{{AdminProductFormBundleSection.bundleOptionXProductYQuantity('0', '1')}}" userInput="{{BundleProduct.defaultQuantity}}" stepKey="fillQty2" before="saveProductForm"/> -+ -+ <!-- Assert product in storefront product page --> -+ <actionGroup ref="AssertProductNameAndSkuInStorefrontProductPage" stepKey="AssertProductInStorefrontProductPage"> -+ <argument name="product" value="BundleProduct"/> -+ </actionGroup> -+ </test> -+</tests> -diff --git a/app/code/Magento/Bundle/Test/Mftf/Test/AdvanceCatalogSearchBundleProductTest.xml b/app/code/Magento/Bundle/Test/Mftf/Test/AdvanceCatalogSearchBundleProductTest.xml -new file mode 100644 -index 00000000000..52bce676008 ---- /dev/null -+++ b/app/code/Magento/Bundle/Test/Mftf/Test/AdvanceCatalogSearchBundleProductTest.xml -@@ -0,0 +1,195 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> -+ <test name="AdvanceCatalogSearchBundleByNameTest" extends="AdvanceCatalogSearchSimpleProductByNameTest"> -+ <annotations> -+ <features value="Bundle"/> -+ <stories value="Advanced Catalog Product Search for all product types"/> -+ <title value="Guest customer should be able to advance search Bundle product with product name"/> -+ <description value="Guest customer should be able to advance search Bundle product with product name"/> -+ <severity value="MAJOR"/> -+ <testCaseId value="MC-139"/> -+ <group value="Bundle"/> -+ </annotations> -+ <before> -+ <createData entity="ApiProductWithDescription" stepKey="simple1" before="simple2"/> -+ <createData entity="ApiProductWithDescription" stepKey="simple2" before="product"/> -+ <createData entity="ApiBundleProduct" stepKey="product"/> -+ <createData entity="DropDownBundleOption" stepKey="bundleOption"> -+ <requiredEntity createDataKey="product"/> -+ </createData> -+ <createData entity="ApiBundleLink" stepKey="createBundleLink1"> -+ <requiredEntity createDataKey="product"/> -+ <requiredEntity createDataKey="bundleOption"/> -+ <requiredEntity createDataKey="simple1"/> -+ </createData> -+ <createData entity="ApiBundleLink" stepKey="createBundleLink2"> -+ <requiredEntity createDataKey="product"/> -+ <requiredEntity createDataKey="bundleOption"/> -+ <requiredEntity createDataKey="simple2"/> -+ </createData> -+ <magentoCLI command="indexer:reindex" stepKey="reindex"/> -+ <magentoCLI command="cache:flush" stepKey="flushCache"/> -+ </before> -+ <after> -+ <deleteData createDataKey="simple1" stepKey="deleteSimple1" before="deleteSimple2"/> -+ <deleteData createDataKey="simple2" stepKey="deleteSimple2" before="delete"/> -+ </after> -+ </test> -+ <test name="AdvanceCatalogSearchBundleBySkuTest" extends="AdvanceCatalogSearchSimpleProductBySkuTest"> -+ <annotations> -+ <features value="Bundle"/> -+ <stories value="Advanced Catalog Product Search for all product types"/> -+ <title value="Guest customer should be able to advance search Bundle product with product sku"/> -+ <description value="Guest customer should be able to advance search Bundle product with product sku"/> -+ <severity value="MAJOR"/> -+ <testCaseId value="MC-143"/> -+ <group value="Bundle"/> -+ </annotations> -+ <before> -+ <createData entity="ApiProductWithDescription" stepKey="simple1" before="simple2"/> -+ <createData entity="ApiProductWithDescription" stepKey="simple2" before="product"/> -+ <createData entity="ApiBundleProduct" stepKey="product"/> -+ <createData entity="DropDownBundleOption" stepKey="bundleOption"> -+ <requiredEntity createDataKey="product"/> -+ </createData> -+ <createData entity="ApiBundleLink" stepKey="createBundleLink1"> -+ <requiredEntity createDataKey="product"/> -+ <requiredEntity createDataKey="bundleOption"/> -+ <requiredEntity createDataKey="simple1"/> -+ </createData> -+ <createData entity="ApiBundleLink" stepKey="createBundleLink2"> -+ <requiredEntity createDataKey="product"/> -+ <requiredEntity createDataKey="bundleOption"/> -+ <requiredEntity createDataKey="simple2"/> -+ </createData> -+ <magentoCLI command="indexer:reindex" stepKey="reindex"/> -+ <magentoCLI command="cache:flush" stepKey="flushCache"/> -+ </before> -+ <after> -+ <deleteData createDataKey="simple1" stepKey="deleteSimple1" before="deleteSimple2"/> -+ <deleteData createDataKey="simple2" stepKey="deleteSimple2" before="delete"/> -+ </after> -+ </test> -+ <test name="AdvanceCatalogSearchBundleByDescriptionTest" extends="AdvanceCatalogSearchSimpleProductByDescriptionTest"> -+ <annotations> -+ <features value="Bundle"/> -+ <stories value="Advanced Catalog Product Search for all product types"/> -+ <title value="Guest customer should be able to advance search Bundle product with product description"/> -+ <description value="Guest customer should be able to advance search Bundle product with product description"/> -+ <severity value="MAJOR"/> -+ <testCaseId value="MC-242"/> -+ <group value="Bundle"/> -+ </annotations> -+ <before> -+ <createData entity="ApiProductWithDescription" stepKey="simple1" before="simple2"/> -+ <createData entity="ApiProductWithDescription" stepKey="simple2" before="product"/> -+ <createData entity="ApiBundleProduct" stepKey="product"/> -+ <createData entity="DropDownBundleOption" stepKey="bundleOption"> -+ <requiredEntity createDataKey="product"/> -+ </createData> -+ <createData entity="ApiBundleLink" stepKey="createBundleLink1"> -+ <requiredEntity createDataKey="product"/> -+ <requiredEntity createDataKey="bundleOption"/> -+ <requiredEntity createDataKey="simple1"/> -+ </createData> -+ <createData entity="ApiBundleLink" stepKey="createBundleLink2"> -+ <requiredEntity createDataKey="product"/> -+ <requiredEntity createDataKey="bundleOption"/> -+ <requiredEntity createDataKey="simple2"/> -+ </createData> -+ <magentoCLI command="indexer:reindex" stepKey="reindex"/> -+ <magentoCLI command="cache:flush" stepKey="flushCache"/> -+ </before> -+ <after> -+ <deleteData createDataKey="simple1" stepKey="deleteSimple1" before="deleteSimple2"/> -+ <deleteData createDataKey="simple2" stepKey="deleteSimple2" before="delete"/> -+ </after> -+ </test> -+ <test name="AdvanceCatalogSearchBundleByShortDescriptionTest" extends="AdvanceCatalogSearchSimpleProductByShortDescriptionTest"> -+ <annotations> -+ <features value="Bundle"/> -+ <stories value="Advanced Catalog Product Search for all product types"/> -+ <title value="Guest customer should be able to advance search Bundle product with product short description"/> -+ <description value="Guest customer should be able to advance search Bundle product with product short description"/> -+ <severity value="MAJOR"/> -+ <testCaseId value="MC-250"/> -+ <group value="Bundle"/> -+ </annotations> -+ <before> -+ <createData entity="ApiProductWithDescription" stepKey="simple1" before="simple2"/> -+ <createData entity="ApiProductWithDescription" stepKey="simple2" before="product"/> -+ <createData entity="ApiBundleProduct" stepKey="product"/> -+ <createData entity="DropDownBundleOption" stepKey="bundleOption"> -+ <requiredEntity createDataKey="product"/> -+ </createData> -+ <createData entity="ApiBundleLink" stepKey="createBundleLink1"> -+ <requiredEntity createDataKey="product"/> -+ <requiredEntity createDataKey="bundleOption"/> -+ <requiredEntity createDataKey="simple1"/> -+ </createData> -+ <createData entity="ApiBundleLink" stepKey="createBundleLink2"> -+ <requiredEntity createDataKey="product"/> -+ <requiredEntity createDataKey="bundleOption"/> -+ <requiredEntity createDataKey="simple2"/> -+ </createData> -+ <magentoCLI command="indexer:reindex" stepKey="reindex"/> -+ <magentoCLI command="cache:flush" stepKey="flushCache"/> -+ </before> -+ <after> -+ <deleteData createDataKey="simple1" stepKey="deleteSimple1" before="deleteSimple2"/> -+ <deleteData createDataKey="simple2" stepKey="deleteSimple2" before="delete"/> -+ </after> -+ </test> -+ <test name="AdvanceCatalogSearchBundleByPriceTest" extends="AdvanceCatalogSearchSimpleProductByPriceTest"> -+ <annotations> -+ <features value="Bundle"/> -+ <stories value="Advanced Catalog Product Search for all product types"/> -+ <title value="Guest customer should be able to advance search Bundle product with product price"/> -+ <description value="Guest customer should be able to advance search Bundle product with product price"/> -+ <severity value="MAJOR"/> -+ <testCaseId value="MC-251"/> -+ <group value="Bundle"/> -+ </annotations> -+ <before> -+ <createData entity="ApiProductWithDescription" stepKey="simple1" before="simple2"/> -+ <createData entity="ApiProductWithDescription" stepKey="simple2" before="product"/> -+ <createData entity="ApiBundleProduct" stepKey="product"/> -+ <createData entity="DropDownBundleOption" stepKey="bundleOption"> -+ <requiredEntity createDataKey="product"/> -+ </createData> -+ <createData entity="ApiBundleLink" stepKey="createBundleLink1"> -+ <requiredEntity createDataKey="product"/> -+ <requiredEntity createDataKey="bundleOption"/> -+ <requiredEntity createDataKey="simple1"/> -+ </createData> -+ <createData entity="ApiBundleLink" stepKey="createBundleLink2"> -+ <requiredEntity createDataKey="product"/> -+ <requiredEntity createDataKey="bundleOption"/> -+ <requiredEntity createDataKey="simple2"/> -+ </createData> -+ <getData entity="GetProduct" stepKey="arg1"> -+ <requiredEntity createDataKey="product"/> -+ </getData> -+ <getData entity="GetProduct" stepKey="arg2"> -+ <requiredEntity createDataKey="simple1"/> -+ </getData> -+ <getData entity="GetProduct" stepKey="arg3"> -+ <requiredEntity createDataKey="simple2"/> -+ </getData> -+ <magentoCLI command="indexer:reindex" stepKey="reindex"/> -+ <magentoCLI command="cache:flush" stepKey="flushCache"/> -+ </before> -+ <after> -+ <deleteData createDataKey="simple1" stepKey="deleteSimple1" before="deleteSimple2"/> -+ <deleteData createDataKey="simple2" stepKey="deleteSimple2" before="delete"/> -+ </after> -+ </test> -+</tests> -diff --git a/app/code/Magento/Bundle/Test/Mftf/Test/BundleProductFixedPricingTest.xml b/app/code/Magento/Bundle/Test/Mftf/Test/BundleProductFixedPricingTest.xml -index a579460906d..c922b981aec 100644 ---- a/app/code/Magento/Bundle/Test/Mftf/Test/BundleProductFixedPricingTest.xml -+++ b/app/code/Magento/Bundle/Test/Mftf/Test/BundleProductFixedPricingTest.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="BundleProductFixedPricingTest"> - <annotations> - <features value="Bundle"/> -@@ -31,6 +31,10 @@ - <deleteData createDataKey="createPreReqCategory" stepKey="deletePreReqCategory"/> - <deleteData createDataKey="simpleProduct1" stepKey="deleteSimpleProduct1"/> - <deleteData createDataKey="simpleProduct2" stepKey="deleteSimpleProduct2"/> -+ <!-- Delete the bundled product we created in the test body --> -+ <actionGroup ref="deleteProductBySku" stepKey="deleteBundleProduct"> -+ <argument name="sku" value="{{BundleProduct.sku}}"/> -+ </actionGroup> - <actionGroup ref="logout" stepKey="logout"/> - </after> - <!--Go to bundle product creation page--> -diff --git a/app/code/Magento/Bundle/Test/Mftf/Test/CurrencyChangingBundleProductInCartTest.xml b/app/code/Magento/Bundle/Test/Mftf/Test/CurrencyChangingBundleProductInCartTest.xml -new file mode 100644 -index 00000000000..ded8bb3c833 ---- /dev/null -+++ b/app/code/Magento/Bundle/Test/Mftf/Test/CurrencyChangingBundleProductInCartTest.xml -@@ -0,0 +1,83 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> -+ <test name="CurrencyChangingBundleProductInCartTest"> -+ <annotations> -+ <features value="Bundle"/> -+ <stories value="Check that after changing currency price of cart is correct when the bundle product added to the cart"/> -+ <title value="User should be able change the currency and get right price in cart when the bundle product added to the cart"/> -+ <description value="User should be able change the currency and add one more product in cart and get right price in previous currency"/> -+ <severity value="MAJOR"/> -+ <testCaseId value="MAGETWO-94467"/> -+ <group value="Bundle"/> -+ </annotations> -+ <before> -+ <actionGroup ref="LoginAsAdmin" stepKey="login"/> -+ <createData entity="SimpleProduct2" stepKey="simpleProduct1"/> -+ <createData entity="SimpleProduct2" stepKey="simpleProduct2"/> -+ </before> -+ <after> -+ <!-- Delete the bundled product --> -+ <actionGroup stepKey="deleteBundle" ref="deleteProductUsingProductGrid"> -+ <argument name="product" value="BundleProduct"/> -+ </actionGroup> -+ <actionGroup ref="AdminClearFiltersActionGroup" stepKey="ClearFiltersAfter"/> -+ <waitForPageLoad stepKey="waitForClearFilter"/> -+ <!--Clear Configs--> -+ <amOnPage url="{{AdminLoginPage.url}}" stepKey="navigateToAdmin"/> -+ <waitForPageLoad stepKey="waitForAdminLoginPageLoad"/> -+ <amOnPage url="{{ConfigCurrencySetupPage.url}}" stepKey="navigateToConfigCurrencySetupPage"/> -+ <waitForPageLoad stepKey="waitForConfigCurrencySetupPageForUnselectEuroCurrency"/> -+ <unselectOption selector="{{CurrencySetupSection.allowCurrencies}}" userInput="Euro" stepKey="unselectEuro"/> -+ <scrollToTopOfPage stepKey="scrollToTopOfPage"/> -+ <click selector="{{CurrencySetupSection.currencyOptions}}" stepKey="closeOptions"/> -+ <waitForPageLoad stepKey="waitForCloseOptions"/> -+ <click stepKey="saveUnselectedConfigs" selector="{{AdminConfigSection.saveButton}}"/> -+ <amOnPage url="{{AdminLogoutPage.url}}" stepKey="logout"/> -+ <deleteData createDataKey="simpleProduct1" stepKey="deleteSimpleProduct1"/> -+ <deleteData createDataKey="simpleProduct2" stepKey="deleteSimpleProduct2"/> -+ </after> -+ <!--Go to bundle product creation page--> -+ <amOnPage url="{{AdminProductCreatePage.url(BundleProduct.set, BundleProduct.type)}}" stepKey="goToBundleProductCreationPage"/> -+ <waitForPageLoad stepKey="waitForBundleProductCreatePageToLoad"/> -+ <actionGroup ref="fillMainBundleProductForm" stepKey="fillMainFieldsForBundle"/> -+ <!-- Add Option, a "Radio Buttons" type option --> -+ <actionGroup ref="addBundleOptionWithTwoProducts" stepKey="addBundleOptionWithTwoProducts2"> -+ <argument name="x" value="0"/> -+ <argument name="n" value="1"/> -+ <argument name="prodOneSku" value="$$simpleProduct1.sku$$"/> -+ <argument name="prodTwoSku" value="$$simpleProduct2.sku$$"/> -+ <argument name="optionTitle" value="Option"/> -+ <argument name="inputType" value="radio"/> -+ </actionGroup> -+ <checkOption selector="{{AdminProductFormBundleSection.userDefinedQuantity('0', '0')}}" stepKey="userDefinedQuantitiyOptionProduct0"/> -+ <checkOption selector="{{AdminProductFormBundleSection.userDefinedQuantity('0', '1')}}" stepKey="userDefinedQuantitiyOptionProduct1"/> -+ <actionGroup ref="saveProductForm" stepKey="saveProduct"/> -+ <amOnPage url="{{ConfigCurrencySetupPage.url}}" stepKey="navigateToConfigCurrencySetupPage"/> -+ <waitForPageLoad stepKey="waitForConfigCurrencySetupPage"/> -+ <conditionalClick selector="{{CurrencySetupSection.currencyOptions}}" dependentSelector="{{CurrencySetupSection.allowCurrencies}}" visible="false" stepKey="openOptions"/> -+ <waitForPageLoad stepKey="waitForOptions"/> -+ <selectOption selector="{{CurrencySetupSection.allowCurrencies}}" parameterArray="['Euro', 'US Dollar']" stepKey="selectCurrencies"/> -+ <click stepKey="saveConfigs" selector="{{AdminConfigSection.saveButton}}"/> -+ <!-- Go to storefront BundleProduct --> -+ <amOnPage url="{{BundleProduct.sku}}.html" stepKey="goToStorefront"/> -+ <waitForPageLoad stepKey="waitForStorefront"/> -+ <actionGroup ref="StoreFrontAddProductToCartFromBundleWithCurrencyActionGroup" stepKey="addProduct1ToCartAndChangeCurrencyToEuro"> -+ <argument name="product" value="$$simpleProduct1$$"/> -+ <argument name="currency" value="EUR - Euro"/> -+ </actionGroup> -+ <actionGroup ref="StoreFrontAddProductToCartFromBundleWithCurrencyActionGroup" stepKey="addProduct2ToCartAndChangeCurrencyToUSD"> -+ <argument name="product" value="$$simpleProduct2$$"/> -+ <argument name="currency" value="USD - US Dollar"/> -+ </actionGroup> -+ <click stepKey="openMiniCart" selector="{{StorefrontMinicartSection.showCart}}"/> -+ <waitForPageLoad stepKey="waitForMiniCart"/> -+ <see stepKey="seeCartSubtotal" userInput="$12,300.00"/> -+ </test> -+</tests> -diff --git a/app/code/Magento/Bundle/Test/Mftf/Test/EnableDisableBundleProductStatusTest.xml b/app/code/Magento/Bundle/Test/Mftf/Test/EnableDisableBundleProductStatusTest.xml -index 7a7b4673eda..0cfd1f99a8c 100644 ---- a/app/code/Magento/Bundle/Test/Mftf/Test/EnableDisableBundleProductStatusTest.xml -+++ b/app/code/Magento/Bundle/Test/Mftf/Test/EnableDisableBundleProductStatusTest.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="EnableDisableBundleProductStatusTest"> - <annotations> - <features value="Bundle"/> -diff --git a/app/code/Magento/Bundle/Test/Mftf/Test/EndToEndB2CAdminTest.xml b/app/code/Magento/Bundle/Test/Mftf/Test/EndToEndB2CAdminTest.xml -index 9402d1d4801..9040d675be3 100644 ---- a/app/code/Magento/Bundle/Test/Mftf/Test/EndToEndB2CAdminTest.xml -+++ b/app/code/Magento/Bundle/Test/Mftf/Test/EndToEndB2CAdminTest.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="EndToEndB2CAdminTest"> - <!--Create Bundle Product--> - <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="visitAdminProductPageBundle" after="seeSimpleProductInGrid"/> -diff --git a/app/code/Magento/Bundle/Test/Mftf/Test/MassEnableDisableBundleProductsTest.xml b/app/code/Magento/Bundle/Test/Mftf/Test/MassEnableDisableBundleProductsTest.xml -index 89867341e96..ff192538637 100644 ---- a/app/code/Magento/Bundle/Test/Mftf/Test/MassEnableDisableBundleProductsTest.xml -+++ b/app/code/Magento/Bundle/Test/Mftf/Test/MassEnableDisableBundleProductsTest.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="MassEnableDisableBundleProductsTest"> - <annotations> - <features value="Bundle"/> -@@ -17,9 +17,6 @@ - <severity value="CRITICAL"/> - <testCaseId value="MC-217"/> - <group value="Bundle"/> -- <skip> -- <issueId value="MAGETWO-92898"/> -- </skip> - </annotations> - <before> - <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> -@@ -63,15 +60,7 @@ - <fillField selector="{{AdminProductFormBundleSection.bundleOptionXProductYQuantity('0', '0')}}" userInput="{{BundleProduct.defaultQuantity}}" stepKey="fillProductDefaultQty1"/> - <fillField selector="{{AdminProductFormBundleSection.bundleOptionXProductYQuantity('0', '1')}}" userInput="{{BundleProduct.defaultQuantity}}" stepKey="fillProductDefaultQty2"/> - -- <fillField selector="{{AdminProductFormBundleSection.productName}}" userInput="{{BundleProduct.name}}" stepKey="fillProductName"/> -- <fillField selector="{{AdminProductFormBundleSection.productSku}}" userInput="{{BundleProduct.sku}}" stepKey="fillProductSku"/> -- -- <!--Trigger SEO drop down--> -- <conditionalClick selector="{{AdminProductFormBundleSection.seoDropdown}}" dependentSelector="{{AdminProductFormBundleSection.seoDependent}}" visible="false" stepKey="OpenDropDownIfClosed"/> -- <waitForPageLoad stepKey="WaitForDropDownSEO"/> -- -- <!--Fill URL input--> -- <fillField userInput="{{BundleProduct.urlKey}}" selector="{{AdminProductFormBundleSection.urlKey}}" stepKey="FillsinSEOlinkExtension"/> -+ <actionGroup ref="AncillaryPrepBundleProduct" stepKey="createBundledProductForTwoSimpleProducts"/> - - <!--Save the product--> - <click selector="{{AdminProductFormActionSection.saveButton}}" stepKey="clickSaveButton"/> -@@ -107,7 +96,8 @@ - <fillField selector="{{AdminProductFormBundleSection.productSku}}" userInput="{{BundleProduct.sku2}}" stepKey="fillProductSku2"/> - - <!--Trigger SEO drop down--> -- <conditionalClick selector="{{AdminProductFormBundleSection.seoDropdown}}" dependentSelector="{{AdminProductFormBundleSection.seoDependent}}" visible="false" stepKey="OpenDropDownIfClosed2"/> -+ <scrollTo selector="{{AdminProductFormBundleSection.seoDropdown}}" stepKey="moveToSEOSection"/> -+ <conditionalClick selector="{{AdminProductFormBundleSection.seoDropdown}}" dependentSelector="{{AdminProductFormBundleSection.urlKey}}" visible="false" stepKey="openDropDownIfClosed"/> - <waitForPageLoad stepKey="WaitForDropDownSEO2"/> - - <!--Fill URL input--> -diff --git a/app/code/Magento/Bundle/Test/Mftf/Test/NewBundleProductSelectionTest.xml b/app/code/Magento/Bundle/Test/Mftf/Test/NewBundleProductSelectionTest.xml -index 8a0a1ceaf52..e0a6a9afd64 100644 ---- a/app/code/Magento/Bundle/Test/Mftf/Test/NewBundleProductSelectionTest.xml -+++ b/app/code/Magento/Bundle/Test/Mftf/Test/NewBundleProductSelectionTest.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="NewBundleProductSelectionTest"> - <annotations> - <features value="Bundle"/> -diff --git a/app/code/Magento/Bundle/Test/Mftf/Test/NewProductsListWidgetBundleProductTest.xml b/app/code/Magento/Bundle/Test/Mftf/Test/NewProductsListWidgetBundleProductTest.xml -new file mode 100644 -index 00000000000..8efe32a7d84 ---- /dev/null -+++ b/app/code/Magento/Bundle/Test/Mftf/Test/NewProductsListWidgetBundleProductTest.xml -@@ -0,0 +1,76 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+/** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> -+ <test name="NewProductsListWidgetBundleProductTest" extends="NewProductsListWidgetTest"> -+ <annotations> -+ <features value="Bundle"/> -+ <stories value="New products list widget"/> -+ <title value="Admin should be able to set Bundle Product as new so that it shows up in the Catalog New Products List Widget"/> -+ <description value="Admin should be able to set Bundle Product as new so that it shows up in the Catalog New Products List Widget"/> -+ <severity value="MAJOR"/> -+ <testCaseId value="MC-123"/> -+ <group value="Bundle"/> -+ <group value="WYSIWYGDisabled"/> -+ </annotations> -+ -+ <before> -+ <createData entity="SimpleProduct2" stepKey="simpleProduct1"/> -+ <createData entity="SimpleProduct2" stepKey="simpleProduct2"/> -+ </before> -+ -+ <after> -+ <deleteData createDataKey="simpleProduct1" stepKey="deleteSimpleProduct1"/> -+ <deleteData createDataKey="simpleProduct2" stepKey="deleteSimpleProduct2"/> -+ </after> -+ -+ <!-- A Cms page containing the New Products Widget gets created here via extends --> -+ -+ <!-- Create a product to appear in the widget, fill in basic info first --> -+ -+ <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="amOnProductList"/> -+ <waitForPageLoad stepKey="waitForProductList"/> -+ <click selector="{{AdminProductGridActionSection.addProductToggle}}" stepKey="clickAddProductToggle"/> -+ <click selector="{{AdminProductGridActionSection.addBundleProduct}}" stepKey="clickAddBundleProduct"/> -+ <fillField selector="{{AdminProductFormSection.productName}}" userInput="{{_defaultProduct.name}}" stepKey="fillProductName"/> -+ <fillField selector="{{AdminProductFormSection.productSku}}" userInput="{{_defaultProduct.sku}}" stepKey="fillProductSku"/> -+ <fillField selector="{{AdminProductFormSection.setProductAsNewFrom}}" userInput="01/1/2000" stepKey="fillProductNewFrom"/> -+ <fillField selector="{{AdminProductFormSection.setProductAsNewTo}}" userInput="01/1/2099" stepKey="fillProductNewTo"/> -+ -+ <!-- and then configure bundled items for this product --> -+ -+ <scrollTo selector="{{AdminProductFormBundleSection.addOption}}" stepKey="scrollToAddOptionButton"/> -+ <click selector="{{AdminProductFormBundleSection.addOption}}" stepKey="clickAddOption"/> -+ <waitForPageLoad stepKey="waitForOptions"/> -+ <fillField selector="{{AdminProductFormBundleSection.bundleOptionXTitle('0')}}" userInput="MFTF Test Bundle 1" stepKey="fillOptionTitle"/> -+ <selectOption selector="{{AdminProductFormBundleSection.bundleOptionXInputType('0')}}" userInput="checkbox" stepKey="selectInputType"/> -+ <click selector="{{AdminProductFormBundleSection.addProductsToOption}}" stepKey="clickAddProductsToOption"/> -+ <waitForPageLoad stepKey="waitForPageLoadAfterBundleProducts"/> -+ <actionGroup ref="filterProductGridBySku" stepKey="filterBundleProductOptions"> -+ <argument name="product" value="$$simpleProduct1$$"/> -+ </actionGroup> -+ <checkOption selector="{{AdminAddProductsToOptionPanel.firstCheckbox}}" stepKey="selectFirstGridRow"/> -+ <actionGroup ref="filterProductGridBySku" stepKey="filterBundleProductOptions2"> -+ <argument name="product" value="$$simpleProduct2$$"/> -+ </actionGroup> -+ <checkOption selector="{{AdminAddProductsToOptionPanel.firstCheckbox}}" stepKey="selectFirstGridRow2"/> -+ <click selector="{{AdminAddProductsToOptionPanel.addSelectedProducts}}" stepKey="clickAddSelectedBundleProducts"/> -+ <fillField selector="{{AdminProductFormBundleSection.bundleOptionXProductYQuantity('0', '0')}}" userInput="1" stepKey="fillProductDefaultQty1"/> -+ <fillField selector="{{AdminProductFormBundleSection.bundleOptionXProductYQuantity('0', '1')}}" userInput="1" stepKey="fillProductDefaultQty2"/> -+ <click selector="{{AdminProductFormActionSection.saveButton}}" stepKey="clickSaveProduct"/> -+ -+ <!-- If PageCache is enabled, Cache clearing happens here, via merge --> -+ -+ <!-- Check for product on the CMS page with the New Products widget --> -+ -+ <amOnPage url="{{_newDefaultCmsPage.identifier}}" stepKey="amOnCmsPage"/> -+ <waitForPageLoad stepKey="waitForCmsPage"/> -+ <see selector="{{StorefrontCategoryMainSection.ProductItemInfo}}" userInput="{{_defaultProduct.name}}" stepKey="seeProductName"/> -+ </test> -+</tests> -diff --git a/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontAddBundleOptionsToCartTest.xml b/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontAddBundleOptionsToCartTest.xml -new file mode 100644 -index 00000000000..779f1370c4d ---- /dev/null -+++ b/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontAddBundleOptionsToCartTest.xml -@@ -0,0 +1,143 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> -+ <test name="StorefrontAddBundleOptionsToCartTest"> -+ <annotations> -+ <features value="Bundle"/> -+ <stories value="MAGETWO-95813: Only two bundle options are added to the cart"/> -+ <title value="Checking adding of bundle options to the cart"/> -+ <description value="Verifying adding of bundle options to the cart"/> -+ <severity value="MAJOR"/> -+ <testCaseId value="MAGETWO-95933"/> -+ <group value="Bundle"/> -+ <skip> -+ <issueId value="MC-16684"/> -+ </skip> -+ </annotations> -+ <before> -+ <actionGroup ref="LoginAsAdmin" stepKey="login"/> -+ <createData entity="SimpleProduct2" stepKey="simpleProduct1"/> -+ <createData entity="SimpleProduct2" stepKey="simpleProduct2"/> -+ <createData entity="SimpleProduct2" stepKey="simpleProduct3"/> -+ <createData entity="SimpleProduct2" stepKey="simpleProduct4"/> -+ <createData entity="SimpleProduct2" stepKey="simpleProduct5"/> -+ <createData entity="SimpleProduct2" stepKey="simpleProduct6"/> -+ <createData entity="SimpleProduct2" stepKey="simpleProduct7"/> -+ <createData entity="SimpleProduct2" stepKey="simpleProduct8"/> -+ <createData entity="SimpleProduct2" stepKey="simpleProduct9"/> -+ <createData entity="SimpleProduct2" stepKey="simpleProduct10"/> -+ </before> -+ <after> -+ <deleteData createDataKey="simpleProduct1" stepKey="deleteSimpleProduct1"/> -+ <deleteData createDataKey="simpleProduct2" stepKey="deleteSimpleProduct2"/> -+ <deleteData createDataKey="simpleProduct3" stepKey="deleteSimpleProduct3"/> -+ <deleteData createDataKey="simpleProduct4" stepKey="deleteSimpleProduct4"/> -+ <deleteData createDataKey="simpleProduct5" stepKey="deleteSimpleProduct5"/> -+ <deleteData createDataKey="simpleProduct6" stepKey="deleteSimpleProduct6"/> -+ <deleteData createDataKey="simpleProduct7" stepKey="deleteSimpleProduct7"/> -+ <deleteData createDataKey="simpleProduct8" stepKey="deleteSimpleProduct8"/> -+ <deleteData createDataKey="simpleProduct9" stepKey="deleteSimpleProduct9"/> -+ <deleteData createDataKey="simpleProduct10" stepKey="deleteSimpleProduct10"/> -+ <!--delete created bundle product--> -+ <actionGroup stepKey="deleteProduct1" ref="deleteProductBySku"> -+ <argument name="sku" value="{{BundleProduct.sku}}"/> -+ </actionGroup> -+ <conditionalClick selector="{{AdminProductGridFilterSection.clearFilters}}" -+ dependentSelector="{{AdminProductGridFilterSection.clearFilters}}" visible="true" stepKey="clickClearFilters"/> -+ <actionGroup ref="logout" stepKey="logout"/> -+ </after> -+ -+ <!-- Start creating a bundle product --> -+ <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="goToProductList"/> -+ <waitForPageLoad stepKey="waitForProductList"/> -+ <actionGroup ref="goToCreateProductPage" stepKey="goToCreateProduct"> -+ <argument name="product" value="BundleProduct"/> -+ </actionGroup> -+ <actionGroup ref="fillProductNameAndSkuInProductForm" stepKey="fillNameAndSku"> -+ <argument name="product" value="BundleProduct"/> -+ </actionGroup> -+ -+ <!-- Add Option One, a "Checkbox" type option, with tree products --> -+ <actionGroup ref="addBundleOptionWithTreeProducts" stepKey="addBundleOptionWithTreeProducts"> -+ <argument name="x" value="0"/> -+ <argument name="n" value="1"/> -+ <argument name="prodOneSku" value="$$simpleProduct1.sku$$"/> -+ <argument name="prodTwoSku" value="$$simpleProduct2.sku$$"/> -+ <argument name="prodTreeSku" value="$$simpleProduct3.sku$$"/> -+ <argument name="optionTitle" value="Option One"/> -+ <argument name="inputType" value="checkbox"/> -+ </actionGroup> -+ -+ <!-- Add Option Two, a "Radio Buttons" type option, with one product --> -+ <actionGroup ref="addBundleOptionWithOneProduct" stepKey="addBundleOptionWithOneProduct"> -+ <argument name="x" value="1"/> -+ <argument name="n" value="2"/> -+ <argument name="prodOneSku" value="$$simpleProduct4.sku$$"/> -+ <argument name="prodTwoSku" value=""/> -+ <argument name="optionTitle" value="Option Two"/> -+ <argument name="inputType" value="radio"/> -+ </actionGroup> -+ -+ <!-- Add Option Tree, a "Checkbox" type option, with six products --> -+ <actionGroup ref="addBundleOptionWithSixProducts" stepKey="addBundleOptionWithSixProducts"> -+ <argument name="x" value="2"/> -+ <argument name="n" value="3"/> -+ <argument name="prodOneSku" value="$$simpleProduct5.sku$$"/> -+ <argument name="prodTwoSku" value="$$simpleProduct6.sku$$"/> -+ <argument name="prodTreeSku" value="$$simpleProduct7.sku$$"/> -+ <argument name="prodFourSku" value="$$simpleProduct8.sku$$"/> -+ <argument name="prodFiveSku" value="$$simpleProduct9.sku$$"/> -+ <argument name="prodSixSku" value="$$simpleProduct10.sku$$"/> -+ <argument name="optionTitle" value="Option Tree"/> -+ <argument name="inputType" value="checkbox"/> -+ </actionGroup> -+ -+ <!-- Save product--> -+ <actionGroup ref="saveProductForm" stepKey="saveProduct"/> -+ -+ <!--Go to Storefront and open Bundle Product page--> -+ <amOnPage url="{{BundleProduct.sku}}.html" stepKey="goToStorefront"/> -+ <waitForPageLoad stepKey="waitForStorefront"/> -+ -+ <!--Click "Customize and Add to Cart" button--> -+ <click selector="{{StorefrontBundledSection.addToCart}}" stepKey="clickCustomize"/> -+ -+ <!--Assert Bundle Product Price--> -+ <grabTextFrom selector="{{StorefrontBundledSection.bundleProductsPrice}}" stepKey="grabProductsPrice"/> -+ <assertEquals expected='$123.00' expectedType="string" actual="$grabProductsPrice" message="ExpectedPrice" stepKey="assertBundleProductPrice"/> -+ -+ <!--Chose all products from 1st & 3rd options --> -+ <click stepKey="selectProduct1" selector="{{StorefrontBundledSection.productCheckbox('1','1')}}"/> -+ <click stepKey="selectProduct2" selector="{{StorefrontBundledSection.productCheckbox('1','2')}}"/> -+ <click stepKey="selectProduct3" selector="{{StorefrontBundledSection.productCheckbox('1','3')}}"/> -+ <click stepKey="selectProduct5" selector="{{StorefrontBundledSection.productCheckbox('3','1')}}"/> -+ <click stepKey="selectProduct6" selector="{{StorefrontBundledSection.productCheckbox('3','2')}}"/> -+ <click stepKey="selectProduct7" selector="{{StorefrontBundledSection.productCheckbox('3','3')}}"/> -+ <click stepKey="selectProduct8" selector="{{StorefrontBundledSection.productCheckbox('3','4')}}"/> -+ <click stepKey="selectProduct9" selector="{{StorefrontBundledSection.productCheckbox('3','5')}}"/> -+ <click stepKey="selectProduct10" selector="{{StorefrontBundledSection.productCheckbox('3','6')}}"/> -+ -+ <!--Click "Add to Cart" button--> -+ <click selector="{{StorefrontBundleProductActionSection.addToCartButton}}" stepKey="clickAddBundleProductToCart"/> -+ <waitForPageLoad time="30" stepKey="waitForAddBundleProductPageLoad"/> -+ -+ <!--Click "mini cart" icon--> -+ <actionGroup ref="StorefrontOpenCartFromMinicartActionGroup" stepKey="openCart"/> -+ <waitForPageLoad stepKey="waitForDetailsOpen"/> -+ -+ <!--Check all products and Cart Subtotal --> -+ <actionGroup ref="StorefrontCheckCartActionGroup" stepKey="cartAssert" after="waitForDetailsOpen"> -+ <argument name="subtotal" value="1,968.00"/> -+ <argument name="shipping" value="5.00"/> -+ <argument name="shippingMethod" value="Flat Rate - Fixed"/> -+ <argument name="total" value="1,973.00"/> -+ </actionGroup> -+ </test> -+</tests> -diff --git a/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontAddBundleProductWithZeroPriceToShoppingCartTest.xml b/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontAddBundleProductWithZeroPriceToShoppingCartTest.xml -new file mode 100644 -index 00000000000..33181d6e920 ---- /dev/null -+++ b/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontAddBundleProductWithZeroPriceToShoppingCartTest.xml -@@ -0,0 +1,78 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> -+ <test name="StorefrontAddBundleProductWithZeroPriceToShoppingCartTest"> -+ <annotations> -+ <features value="Bundle"/> -+ <stories value="Add Bundle product with zero price to shopping cart"/> -+ <title value="Add Bundle product with zero price to shopping cart"/> -+ <description value="Add Bundle product with zero price to shopping cart"/> -+ <severity value="CRITICAL"/> -+ <testCaseId value="MAGETWO-95167"/> -+ <group value="bundle"/> -+ </annotations> -+ <before> -+ <!--Enable freeShipping--> -+ <createData entity="FreeShippinMethodConfig" stepKey="enableFreeShipping"/> -+ <!--Create category--> -+ <createData entity="SimpleSubCategory" stepKey="createSubCategory"/> -+ <!--Create simple with zero price product--> -+ <createData entity="ApiProductWithDescription" stepKey="apiSimple"> -+ <field key="price">0</field> -+ </createData> -+ <!--Create Bundle product--> -+ <createData entity="ApiBundleProductPriceViewRange" stepKey="apiBundleProduct"> -+ <requiredEntity createDataKey="createSubCategory"/> -+ </createData> -+ <!--Create Attribute--> -+ <createData entity="DropDownBundleOption" stepKey="bundleOption"> -+ <requiredEntity createDataKey="apiBundleProduct"/> -+ </createData> -+ <createData entity="ApiBundleLink" stepKey="createBundleLink"> -+ <requiredEntity createDataKey="apiBundleProduct"/> -+ <requiredEntity createDataKey="bundleOption"/> -+ <requiredEntity createDataKey="apiSimple"/> -+ </createData> -+ <magentoCLI command="indexer:reindex" stepKey="reindex"/> -+ </before> -+ <after> -+ <createData entity="FreeShippinMethodDefault" stepKey="disableFreeShipping"/> -+ <deleteData createDataKey="apiSimple" stepKey="deleteSimple"/> -+ <deleteData createDataKey="apiBundleProduct" stepKey="deleteBundleProduct"/> -+ <deleteData createDataKey="createSubCategory" stepKey="deleteCategory"/> -+ <actionGroup ref="AdminOrdersGridClearFiltersActionGroup" stepKey="clearFilters"/> -+ <actionGroup ref="logout" stepKey="logout"/> -+ </after> -+ <!--Open category page--> -+ <amOnPage url="{{StorefrontCategoryPage.url($$createSubCategory.custom_attributes[url_key]$$)}}" stepKey="amOnCategoryPage"/> -+ <!--Add bundle product to cart--> -+ <actionGroup ref="StorefrontAddBundleProductFromCategoryToCartActionGroup" stepKey="addBundleProductToCart"> -+ <argument name="productName" value="$$apiBundleProduct.name$$"/> -+ </actionGroup> -+ <actionGroup ref="GoToCheckoutFromMinicartActionGroup" stepKey="goToCheckoutFromMinicart"/> -+ -+ <!--Place order--> -+ <actionGroup ref="GuestCheckoutFillingShippingSectionActionGroup" stepKey="guestCheckoutFillingShipping"> -+ <argument name="shippingMethod" value="Free Shipping"/> -+ </actionGroup> -+ <actionGroup ref="ClickPlaceOrderActionGroup" stepKey="checkoutPlaceOrder"/> -+ <grabTextFrom selector="{{CheckoutSuccessMainSection.orderNumber}}" stepKey="grabOrderNumber"/> -+ -+ <!--Check subtotal in created order--> -+ <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> -+ <actionGroup ref="filterOrderGridById" stepKey="filterOrderById"> -+ <argument name="orderId" value="$grabOrderNumber"/> -+ </actionGroup> -+ <click selector="{{AdminOrdersGridSection.firstRow}}" stepKey="clickOrderRow"/> -+ <waitForPageLoad stepKey="waitForAdminOrderPageLoad"/> -+ <scrollTo selector="{{AdminOrderTotalSection.subTotal}}" stepKey="scrollToOrderTotalSection"/> -+ <see selector="{{AdminOrderTotalSection.subTotal}}" userInput="$0.00" stepKey="checkSubtotal"/> -+ </test> -+</tests> -diff --git a/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontAdminEditDataTest.xml b/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontAdminEditDataTest.xml -index c0d659f1665..40132ea9565 100644 ---- a/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontAdminEditDataTest.xml -+++ b/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontAdminEditDataTest.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="StorefrontAdminEditDataTest"> - <annotations> - <features value="Bundle"/> -diff --git a/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontBundleCartTest.xml b/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontBundleCartTest.xml -index 655081df610..695c3a8bf7d 100644 ---- a/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontBundleCartTest.xml -+++ b/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontBundleCartTest.xml -@@ -7,7 +7,7 @@ - --> - - <tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -- xsi:noNamespaceSchemaLocation="../../../../../../vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Test/etc/testSchema.xsd"> -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> - <test name="StorefrontBundleCartTest"> - <annotations> - <features value="Bundle"/> -diff --git a/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontBundleProductDetailsTest.xml b/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontBundleProductDetailsTest.xml -index a475ef16ed5..d7394b2dbf3 100644 ---- a/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontBundleProductDetailsTest.xml -+++ b/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontBundleProductDetailsTest.xml -@@ -7,7 +7,7 @@ - --> - - <tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -- xsi:noNamespaceSchemaLocation="../../../../../../vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Test/etc/testSchema.xsd"> -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> - <test name="StorefrontBundleProductDetailsTest"> - <annotations> - <features value="Bundle"/> -@@ -32,6 +32,8 @@ - <deleteData createDataKey="createPreReqCategory" stepKey="deletePreReqCategory"/> - <deleteData createDataKey="simpleProduct1" stepKey="deleteSimpleProduct1"/> - <deleteData createDataKey="simpleProduct2" stepKey="deleteSimpleProduct2"/> -+ <actionGroup ref="AdminOpenProductIndexPageActionGroup" stepKey="navigateToProductIndexPage"/> -+ <actionGroup ref="deleteProductsIfTheyExist" stepKey="deleteAllProducts"/> - <actionGroup ref="logout" stepKey="logout"/> - </after> - <!-- go to bundle product creation page--> -diff --git a/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontBundleProductShownInCategoryListAndGrid.xml b/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontBundleProductShownInCategoryListAndGrid.xml -index 577079965ca..9ad4b6828d6 100644 ---- a/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontBundleProductShownInCategoryListAndGrid.xml -+++ b/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontBundleProductShownInCategoryListAndGrid.xml -@@ -7,7 +7,7 @@ - --> - - <tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -- xsi:noNamespaceSchemaLocation="../../../../../../vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Test/etc/testSchema.xsd"> -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> - <test name="StorefrontBundleProductShownInCategoryListAndGrid"> - <annotations> - <features value="Bundle"/> -diff --git a/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontCheckBundleProductOptionTierPrices.xml b/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontCheckBundleProductOptionTierPrices.xml -new file mode 100644 -index 00000000000..532af1ea76d ---- /dev/null -+++ b/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontCheckBundleProductOptionTierPrices.xml -@@ -0,0 +1,106 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> -+ <test name="StorefrontCheckBundleProductOptionTierPrices"> -+ <annotations> -+ <features value="Bundle"/> -+ <stories value="View bundle products"/> -+ <title value="Check tier prices for bundle options"/> -+ <description value="Check tier prices for bundle options"/> -+ <testCaseId value="MAGETWO-98968"/> -+ <useCaseId value="MAGETWO-98603"/> -+ <severity value="AVERAGE"/> -+ <group value="catalog"/> -+ <group value="bundle"/> -+ </annotations> -+ <before> -+ <!-- Create Dynamic Bundle product --> -+ <actionGroup ref="AdminCreateApiDynamicBundleProductAllOptionTypesActionGroup" stepKey="createBundleProduct"/> -+ -+ <!-- Add tier prices to simple products --> -+ <!-- Simple product 1 --> -+ <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> -+ <amOnPage url="{{AdminProductEditPage.url($$simpleProduct1CreateBundleProduct.id$$)}}" stepKey="openAdminEditPageProduct1"/> -+ <actionGroup ref="ProductSetAdvancedPricing" stepKey="addTierPriceProduct1"> -+ <argument name="group" value="ALL GROUPS"/> -+ <argument name="quantity" value="5"/> -+ <argument name="price" value="Discount"/> -+ <argument name="amount" value="50"/> -+ </actionGroup> -+ <!-- Simple product 2 --> -+ <amOnPage url="{{AdminProductEditPage.url($$simpleProduct2CreateBundleProduct.id$$)}}" stepKey="openAdminEditPageProduct2"/> -+ <actionGroup ref="ProductSetAdvancedPricing" stepKey="addTierPriceProduct2"> -+ <argument name="group" value="ALL GROUPS"/> -+ <argument name="quantity" value="7"/> -+ <argument name="price" value="Discount"/> -+ <argument name="amount" value="25"/> -+ </actionGroup> -+ <actionGroup ref="logout" stepKey="logoutAsAdmin"/> -+ -+ <!-- Run reindex --> -+ <magentoCLI command="indexer:reindex" stepKey="reindex"/> -+ </before> -+ <after> -+ <deleteData createDataKey="createBundleProductCreateBundleProduct" stepKey="deleteDynamicBundleProduct"/> -+ <deleteData createDataKey="simpleProduct1CreateBundleProduct" stepKey="deleteSimpleProduct1"/> -+ <deleteData createDataKey="simpleProduct2CreateBundleProduct" stepKey="deleteSimpleProduct2"/> -+ </after> -+ -+ <!-- Go to storefront product page --> -+ <amOnPage url="{{StorefrontProductPage.url($$createBundleProductCreateBundleProduct.custom_attributes[url_key]$$)}}" stepKey="navigateToBundleProductPage"/> -+ <click selector="{{StorefrontBundledSection.addToCart}}" stepKey="clickCustomize"/> -+ -+ <!--"Drop-down" type option--> -+ <!-- Check Tier Prices for product 1 --> -+ <selectOption selector="{{StorefrontBundledSection.dropDownOptionOneProducts('Drop-down Option')}}" userInput="$$simpleProduct1CreateBundleProduct.sku$$ +$$$simpleProduct1CreateBundleProduct.price$$.00" stepKey="selectDropDownOptionProduct1"/> -+ <seeOptionIsSelected selector="{{StorefrontBundledSection.dropDownOptionOneProducts('Drop-down Option')}}" userInput="$$simpleProduct1CreateBundleProduct.sku$$ +$$$simpleProduct1CreateBundleProduct.price$$.00" stepKey="checkDropDownOptionProduct1"/> -+ <grabTextFrom selector="{{StorefrontBundledSection.dropDownOptionTierPrices('Drop-down Option')}}" stepKey="DropDownTierPriceTextProduct1"/> -+ <assertContains stepKey="assertDropDownTierPriceTextProduct1"> -+ <expectedResult type="string">Buy 5 for $5.00 each and save 50%</expectedResult> -+ <actualResult type="variable">DropDownTierPriceTextProduct1</actualResult> -+ </assertContains> -+ <!-- Check Tier Prices for product 2 --> -+ <selectOption selector="{{StorefrontBundledSection.dropDownOptionOneProducts('Drop-down Option')}}" userInput="$$simpleProduct2CreateBundleProduct.sku$$ +$$$simpleProduct2CreateBundleProduct.price$$.00" stepKey="selectDropDownOptionProduct2"/> -+ <seeOptionIsSelected selector="{{StorefrontBundledSection.dropDownOptionOneProducts('Drop-down Option')}}" userInput="$$simpleProduct2CreateBundleProduct.sku$$ +$$$simpleProduct2CreateBundleProduct.price$$.00" stepKey="checkDropDownOptionProduct2"/> -+ <grabTextFrom selector="{{StorefrontBundledSection.dropDownOptionTierPrices('Drop-down Option')}}" stepKey="dropDownTierPriceTextProduct2"/> -+ <assertContains stepKey="assertDropDownTierPriceTextProduct2"> -+ <expectedResult type="string">Buy 7 for $15.00 each and save 25%</expectedResult> -+ <actualResult type="variable">dropDownTierPriceTextProduct2</actualResult> -+ </assertContains> -+ -+ <!--"Radio Buttons" type option--> -+ <!-- Check Tier Prices for product 1 --> -+ <grabTextFrom selector="{{StorefrontBundledSection.radioButtonOptionLabel('Radio Buttons Option', '$$simpleProduct1CreateBundleProduct.sku$$')}}" stepKey="radioButtonsOptionTierPriceTextProduct1"/> -+ <assertContains stepKey="assertRadioButtonsOptionTierPriceTextProduct1"> -+ <expectedResult type="string">Buy 5 for $5.00 each and save 50%</expectedResult> -+ <actualResult type="variable">radioButtonsOptionTierPriceTextProduct1</actualResult> -+ </assertContains> -+ <!-- Check Tier Prices for product 2 --> -+ <grabTextFrom selector="{{StorefrontBundledSection.radioButtonOptionLabel('Radio Buttons Option', '$$simpleProduct2CreateBundleProduct.sku$$')}}" stepKey="radioButtonsOptionTierPriceTextProduct2"/> -+ <assertContains stepKey="assertRadioButtonsOptionTierPriceTextProduct2"> -+ <expectedResult type="string">Buy 7 for $15.00 each and save 25%</expectedResult> -+ <actualResult type="variable">radioButtonsOptionTierPriceTextProduct2</actualResult> -+ </assertContains> -+ -+ <!--"Checkbox" type option--> -+ <!-- Check Tier Prices for product 1 --> -+ <grabTextFrom selector="{{StorefrontBundledSection.checkboxOptionLabel('Checkbox Option', '$$simpleProduct1CreateBundleProduct.sku$$')}}" stepKey="checkBoxOptionTierPriceTextProduct1"/> -+ <assertContains stepKey="assertCheckBoxOptionTierPriceTextProduct1"> -+ <expectedResult type="string">Buy 5 for $5.00 each and save 50%</expectedResult> -+ <actualResult type="variable">checkBoxOptionTierPriceTextProduct1</actualResult> -+ </assertContains> -+ <!-- Check Tier Prices for product 2 --> -+ <grabTextFrom selector="{{StorefrontBundledSection.checkboxOptionLabel('Checkbox Option', '$$simpleProduct2CreateBundleProduct.sku$$')}}" stepKey="checkBoxOptionTierPriceTextProduct2"/> -+ <assertContains stepKey="assertCheckBoxOptionTierPriceTextProduct2"> -+ <expectedResult type="string">Buy 7 for $15.00 each and save 25%</expectedResult> -+ <actualResult type="variable">checkBoxOptionTierPriceTextProduct2</actualResult> -+ </assertContains> -+ </test> -+</tests> -diff --git a/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontCustomerSelectAndSetBundleOptionsTest.xml b/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontCustomerSelectAndSetBundleOptionsTest.xml -index 26e5e436ed5..5e6e8915414 100644 ---- a/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontCustomerSelectAndSetBundleOptionsTest.xml -+++ b/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontCustomerSelectAndSetBundleOptionsTest.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="StorefrontCustomerSelectAndSetBundleOptionsTest"> - <annotations> - <features value="Bundle"/> -diff --git a/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontEditBundleProductTest.xml b/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontEditBundleProductTest.xml -index a50a73c7f6b..58806126aee 100644 ---- a/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontEditBundleProductTest.xml -+++ b/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontEditBundleProductTest.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="StorefrontEditBundleProductTest"> - <annotations> - <features value="Bundle"/> -@@ -94,9 +94,8 @@ - <click stepKey="clickEdit" selector="{{CheckoutCartProductSection.nthEditButton('1')}}"/> - <waitForPageLoad stepKey="waitForStorefront2"/> - -- <!-- Choose both of the options on the storefront --> -- <click stepKey="selectFirstBundleOption2" selector="{{StorefrontBundledSection.nthBundledOption('1','1')}}"/> -- <click stepKey="selectSecondBundleOption2" selector="{{StorefrontBundledSection.nthBundledOption('1','2')}}"/> -+ <!-- Check second one option to choose both of the options on the storefront --> -+ <click selector="{{StorefrontBundledSection.nthBundledOption('1','2')}}" stepKey="selectSecondBundleOption2"/> - - <waitForPageLoad stepKey="waitForPriceUpdate3"/> - <see stepKey="seeDoublePrice" selector="{{StorefrontBundledSection.configuredPrice}}" userInput="2,460.00"/> -diff --git a/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontGoToDetailsPageWhenAddingToCartTest.xml b/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontGoToDetailsPageWhenAddingToCartTest.xml -index 6c476183a35..ccd6a58223b 100644 ---- a/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontGoToDetailsPageWhenAddingToCartTest.xml -+++ b/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontGoToDetailsPageWhenAddingToCartTest.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="StorefrontGoToDetailsPageWhenAddingToCart"> - <annotations> - <features value="Bundle"/> -diff --git a/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontVerifyDynamicBundleProductPricesForCombinationOfOptionsTest.xml b/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontVerifyDynamicBundleProductPricesForCombinationOfOptionsTest.xml -index 1b1a46d1c8b..31a5f9bab77 100644 ---- a/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontVerifyDynamicBundleProductPricesForCombinationOfOptionsTest.xml -+++ b/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontVerifyDynamicBundleProductPricesForCombinationOfOptionsTest.xml -@@ -7,10 +7,11 @@ - --> - - <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="StorefrontVerifyDynamicBundleProductPricesForCombinationOfOptionsTest"> - <annotations> - <features value="Bundle"/> -+ <stories value="View bundle products"/> - <title value="Verify dynamic bundle product prices for combination of options"/> - <description value="Verify prices for various configurations of Dynamic Bundle product"/> - <severity value="CRITICAL"/> -diff --git a/app/code/Magento/Bundle/Test/Unit/Block/Catalog/Product/View/Type/BundleTest.php b/app/code/Magento/Bundle/Test/Unit/Block/Catalog/Product/View/Type/BundleTest.php -index c252e5f9961..07d2e1b995c 100644 ---- a/app/code/Magento/Bundle/Test/Unit/Block/Catalog/Product/View/Type/BundleTest.php -+++ b/app/code/Magento/Bundle/Test/Unit/Block/Catalog/Product/View/Type/BundleTest.php -@@ -280,6 +280,7 @@ class BundleTest extends \PHPUnit\Framework\TestCase - $this->assertEquals(110, $jsonConfig['prices']['oldPrice']['amount']); - $this->assertEquals(100, $jsonConfig['prices']['basePrice']['amount']); - $this->assertEquals(100, $jsonConfig['prices']['finalPrice']['amount']); -+ $this->assertEquals([1], $jsonConfig['positions']); - } - - /** -diff --git a/app/code/Magento/Bundle/Test/Unit/Block/DataProviders/OptionPriceRendererTest.php b/app/code/Magento/Bundle/Test/Unit/Block/DataProviders/OptionPriceRendererTest.php -new file mode 100644 -index 00000000000..1af73bafc62 ---- /dev/null -+++ b/app/code/Magento/Bundle/Test/Unit/Block/DataProviders/OptionPriceRendererTest.php -@@ -0,0 +1,99 @@ -+<?php -+/** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+declare(strict_types=1); -+ -+namespace Magento\Bundle\Test\Unit\Block\DataProviders; -+ -+use Magento\Bundle\Block\DataProviders\OptionPriceRenderer; -+use Magento\Catalog\Model\Product; -+use Magento\Framework\Pricing\Render; -+use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; -+use Magento\Framework\View\Element\BlockInterface; -+use Magento\Framework\View\LayoutInterface; -+use PHPUnit\Framework\MockObject\MockObject; -+use PHPUnit\Framework\TestCase; -+ -+/** -+ * Class to test additional data for bundle options -+ */ -+class OptionPriceRendererTest extends TestCase -+{ -+ /** -+ * @var LayoutInterface|MockObject -+ */ -+ private $layoutMock; -+ -+ /** -+ * @var OptionPriceRenderer -+ */ -+ private $renderer; -+ -+ /** -+ * @inheritdoc -+ */ -+ protected function setUp() -+ { -+ $objectManager = new ObjectManager($this); -+ -+ $this->layoutMock = $this->createMock( -+ LayoutInterface::class -+ ); -+ -+ $this->renderer = $objectManager->getObject( -+ OptionPriceRenderer::class, -+ ['layout' => $this->layoutMock] -+ ); -+ } -+ -+ /** -+ * Test to render Tier price html -+ * -+ * @return void -+ */ -+ public function testRenderTierPrice(): void -+ { -+ $expectedHtml = 'tier price html'; -+ $expectedArguments = ['zone' => Render::ZONE_ITEM_OPTION]; -+ -+ $productMock = $this->createMock(Product::class); -+ -+ $priceRenderer = $this->createPartialMock(BlockInterface::class, ['toHtml', 'render']); -+ $priceRenderer->expects($this->once()) -+ ->method('render') -+ ->with('tier_price', $productMock, $expectedArguments) -+ ->willReturn($expectedHtml); -+ -+ $this->layoutMock->method('getBlock') -+ ->with('product.price.render.default') -+ ->willReturn($priceRenderer); -+ -+ $this->assertEquals( -+ $expectedHtml, -+ $this->renderer->renderTierPrice($productMock), -+ 'Render Tier price is wrong' -+ ); -+ } -+ -+ /** -+ * Test to render Tier price html when render block is not exists -+ * -+ * @return void -+ */ -+ public function testRenderTierPriceNotExist(): void -+ { -+ $productMock = $this->createMock(Product::class); -+ -+ $this->layoutMock->method('getBlock') -+ ->with('product.price.render.default') -+ ->willReturn(false); -+ -+ $this->assertEquals( -+ '', -+ $this->renderer->renderTierPrice($productMock), -+ 'Render Tier price is wrong' -+ ); -+ } -+} -diff --git a/app/code/Magento/Bundle/Test/Unit/Model/OptionRepositoryTest.php b/app/code/Magento/Bundle/Test/Unit/Model/OptionRepositoryTest.php -index b4a466b413a..2450f63c389 100644 ---- a/app/code/Magento/Bundle/Test/Unit/Model/OptionRepositoryTest.php -+++ b/app/code/Magento/Bundle/Test/Unit/Model/OptionRepositoryTest.php -@@ -8,6 +8,7 @@ - namespace Magento\Bundle\Test\Unit\Model; - - use Magento\Bundle\Model\OptionRepository; -+use Magento\Framework\Exception\NoSuchEntityException; - - /** - * @SuppressWarnings(PHPMD.CouplingBetweenObjects) -@@ -84,7 +85,7 @@ class OptionRepositoryTest extends \PHPUnit\Framework\TestCase - ->getMock(); - $this->optionResourceMock = $this->createPartialMock( - \Magento\Bundle\Model\ResourceModel\Option::class, -- ['delete', '__wakeup', 'save', 'removeOptionSelections'] -+ ['get', 'delete', '__wakeup', 'save', 'removeOptionSelections'] - ); - $this->storeManagerMock = $this->createMock(\Magento\Store\Model\StoreManagerInterface::class); - $this->linkManagementMock = $this->createMock(\Magento\Bundle\Api\ProductLinkManagementInterface::class); -@@ -227,32 +228,92 @@ class OptionRepositoryTest extends \PHPUnit\Framework\TestCase - $this->model->delete($optionMock); - } - -+ /** -+ * Test successful delete action for given $optionId -+ */ - public function testDeleteById() - { - $productSku = 'sku'; - $optionId = 100; -- $productMock = $this->createMock(\Magento\Catalog\Api\Data\ProductInterface::class); -+ -+ $optionMock = $this->createMock(\Magento\Bundle\Model\Option::class); -+ $optionMock->expects($this->exactly(2)) -+ ->method('getId') -+ ->willReturn($optionId); -+ -+ $optionMock->expects($this->once()) -+ ->method('getData') -+ ->willReturn([ -+ 'title' => 'Option title', -+ 'option_id' => $optionId -+ ]); -+ -+ $this->optionFactoryMock->expects($this->once()) -+ ->method('create') -+ ->willReturn($optionMock); -+ -+ $productMock = $this->createPartialMock( -+ \Magento\Catalog\Model\Product::class, -+ ['getTypeId', 'getTypeInstance', 'getStoreId', 'getPriceType', '__wakeup', 'getSku'] -+ ); - $productMock->expects($this->once()) - ->method('getTypeId') - ->willReturn(\Magento\Catalog\Model\Product\Type::TYPE_BUNDLE); -- $this->productRepositoryMock->expects($this->once()) -+ $productMock->expects($this->exactly(2))->method('getSku')->willReturn($productSku); -+ -+ $this->productRepositoryMock -+ ->expects($this->once()) - ->method('get') - ->with($productSku) - ->willReturn($productMock); - -+ $optCollectionMock = $this->createMock(\Magento\Bundle\Model\ResourceModel\Option\Collection::class); -+ $optCollectionMock->expects($this->once())->method('getItemById')->with($optionId)->willReturn($optionMock); -+ $this->typeMock->expects($this->once()) -+ ->method('getOptionsCollection') -+ ->with($productMock) -+ ->willReturn($optCollectionMock); -+ -+ $this->assertTrue($this->model->deleteById($productSku, $optionId)); -+ } -+ -+ /** -+ * Tests if NoSuchEntityException thrown when provided $optionId not found -+ */ -+ public function testDeleteByIdException() -+ { -+ $productSku = 'sku'; -+ $optionId = null; -+ - $optionMock = $this->createMock(\Magento\Bundle\Model\Option::class); -+ $optionMock->expects($this->exactly(1)) -+ ->method('getId') -+ ->willReturn($optionId); -+ -+ $productMock = $this->createPartialMock( -+ \Magento\Catalog\Model\Product::class, -+ ['getTypeId', 'getTypeInstance', 'getStoreId', 'getPriceType', '__wakeup', 'getSku'] -+ ); -+ $productMock->expects($this->once()) -+ ->method('getTypeId') -+ ->willReturn(\Magento\Catalog\Model\Product\Type::TYPE_BUNDLE); -+ -+ $this->productRepositoryMock -+ ->expects($this->once()) -+ ->method('get') -+ ->with($productSku) -+ ->willReturn($productMock); - - $optCollectionMock = $this->createMock(\Magento\Bundle\Model\ResourceModel\Option\Collection::class); -+ $optCollectionMock->expects($this->once())->method('getItemById')->with($optionId)->willReturn($optionMock); - $this->typeMock->expects($this->once()) - ->method('getOptionsCollection') - ->with($productMock) - ->willReturn($optCollectionMock); - -- $optCollectionMock->expects($this->once())->method('setIdFilter')->with($optionId)->willReturnSelf(); -- $optCollectionMock->expects($this->once())->method('getFirstItem')->willReturn($optionMock); -+ $this->expectException(NoSuchEntityException::class); - -- $this->optionResourceMock->expects($this->once())->method('delete')->with($optionMock)->willReturnSelf(); -- $this->assertTrue($this->model->deleteById($productSku, $optionId)); -+ $this->model->deleteById($productSku, $optionId); - } - - /** -diff --git a/app/code/Magento/Bundle/Test/Unit/Model/Plugin/Frontend/ProductTest.php b/app/code/Magento/Bundle/Test/Unit/Model/Plugin/Frontend/ProductTest.php -new file mode 100644 -index 00000000000..ee08618eab5 ---- /dev/null -+++ b/app/code/Magento/Bundle/Test/Unit/Model/Plugin/Frontend/ProductTest.php -@@ -0,0 +1,73 @@ -+<?php -+/** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+ -+namespace Magento\Bundle\Test\Unit\Model\Plugin\Frontend; -+ -+use Magento\Bundle\Model\Plugin\Frontend\Product as ProductPlugin; -+use Magento\Bundle\Model\Product\Type; -+use Magento\Catalog\Model\Product; -+use PHPUnit_Framework_MockObject_MockObject as MockObject; -+ -+class ProductTest extends \PHPUnit\Framework\TestCase -+{ -+ /** @var \Magento\Bundle\Model\Plugin\Product */ -+ private $plugin; -+ -+ /** @var MockObject|Type */ -+ private $type; -+ -+ /** @var MockObject|\Magento\Catalog\Model\Product */ -+ private $product; -+ -+ protected function setUp() -+ { -+ $this->product = $this->getMockBuilder(Product::class) -+ ->disableOriginalConstructor() -+ ->setMethods(['getEntityId']) -+ ->getMock(); -+ -+ $this->type = $this->getMockBuilder(Type::class) -+ ->disableOriginalConstructor() -+ ->setMethods(['getChildrenIds']) -+ ->getMock(); -+ -+ $this->plugin = new ProductPlugin($this->type); -+ } -+ -+ public function testAfterGetIdentities() -+ { -+ $baseIdentities = [ -+ 'SomeCacheId', -+ 'AnotherCacheId', -+ ]; -+ $id = 12345; -+ $childIds = [ -+ 1 => [1, 2, 5, 100500], -+ 12 => [7, 22, 45, 24612] -+ ]; -+ $expectedIdentities = [ -+ 'SomeCacheId', -+ 'AnotherCacheId', -+ Product::CACHE_TAG . '_' . 1, -+ Product::CACHE_TAG . '_' . 2, -+ Product::CACHE_TAG . '_' . 5, -+ Product::CACHE_TAG . '_' . 100500, -+ Product::CACHE_TAG . '_' . 7, -+ Product::CACHE_TAG . '_' . 22, -+ Product::CACHE_TAG . '_' . 45, -+ Product::CACHE_TAG . '_' . 24612, -+ ]; -+ $this->product->expects($this->once()) -+ ->method('getEntityId') -+ ->will($this->returnValue($id)); -+ $this->type->expects($this->once()) -+ ->method('getChildrenIds') -+ ->with($id) -+ ->will($this->returnValue($childIds)); -+ $identities = $this->plugin->afterGetIdentities($this->product, $baseIdentities); -+ $this->assertEquals($expectedIdentities, $identities); -+ } -+} -diff --git a/app/code/Magento/Bundle/Test/Unit/Model/Product/TypeTest.php b/app/code/Magento/Bundle/Test/Unit/Model/Product/TypeTest.php -index 4bbf5641c55..9d7629c6f0a 100644 ---- a/app/code/Magento/Bundle/Test/Unit/Model/Product/TypeTest.php -+++ b/app/code/Magento/Bundle/Test/Unit/Model/Product/TypeTest.php -@@ -15,6 +15,7 @@ use Magento\Framework\EntityManager\EntityMetadataInterface; - use Magento\Framework\EntityManager\MetadataPool; - use Magento\Framework\Exception\LocalizedException; - use Magento\Framework\Serialize\Serializer\Json; -+use Magento\Framework\Stdlib\ArrayUtils; - - /** - * Class TypeTest -@@ -87,6 +88,11 @@ class TypeTest extends \PHPUnit\Framework\TestCase - */ - private $serializer; - -+ /** -+ * @var ArrayUtils|\PHPUnit_Framework_MockObject_MockObject -+ */ -+ private $arrayUtility; -+ - /** - * @return void - */ -@@ -159,6 +165,11 @@ class TypeTest extends \PHPUnit\Framework\TestCase - ->disableOriginalConstructor() - ->getMock(); - -+ $this->arrayUtility = $this->getMockBuilder(ArrayUtils::class) -+ ->setMethods(['flatten']) -+ ->disableOriginalConstructor() -+ ->getMock(); -+ - $objectHelper = new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this); - $this->model = $objectHelper->getObject( - \Magento\Bundle\Model\Product\Type::class, -@@ -175,6 +186,7 @@ class TypeTest extends \PHPUnit\Framework\TestCase - 'priceCurrency' => $this->priceCurrency, - 'serializer' => $this->serializer, - 'metadataPool' => $this->metadataPool, -+ 'arrayUtility' => $this->arrayUtility - ] - ); - } -@@ -421,6 +433,8 @@ class TypeTest extends \PHPUnit\Framework\TestCase - return $resultValue; - } - ); -+ $bundleOptions = [3 => 5]; -+ - $product->expects($this->any()) - ->method('getId') - ->willReturn(333); -@@ -438,9 +452,7 @@ class TypeTest extends \PHPUnit\Framework\TestCase - ->with($selectionCollection, true, true); - $productType->expects($this->once()) - ->method('setStoreFilter'); -- $buyRequest->expects($this->once()) -- ->method('getBundleOption') -- ->willReturn([3 => 5]); -+ $buyRequest->expects($this->once())->method('getBundleOption')->willReturn($bundleOptions); - $selectionCollection->expects($this->any()) - ->method('getItems') - ->willReturn([$selection]); -@@ -491,6 +503,9 @@ class TypeTest extends \PHPUnit\Framework\TestCase - $option->expects($this->once()) - ->method('getTitle') - ->willReturn('Title for option'); -+ -+ $this->arrayUtility->expects($this->once())->method('flatten')->willReturn($bundleOptions); -+ - $buyRequest->expects($this->once()) - ->method('getBundleOptionQty') - ->willReturn([3 => 5]); -@@ -513,10 +528,6 @@ class TypeTest extends \PHPUnit\Framework\TestCase - ->method('getSelectionId') - ->willReturn(314); - -- $this->priceCurrency->expects($this->once()) -- ->method('convert') -- ->willReturn(3.14); -- - $result = $this->model->prepareForCartAdvanced($buyRequest, $product); - $this->assertEquals([$product, $productType], $result); - } -@@ -657,6 +668,8 @@ class TypeTest extends \PHPUnit\Framework\TestCase - return $resultValue; - } - ); -+ $bundleOptions = [3 => 5]; -+ - $product->expects($this->any()) - ->method('getId') - ->willReturn(333); -@@ -676,7 +689,10 @@ class TypeTest extends \PHPUnit\Framework\TestCase - ->method('setStoreFilter'); - $buyRequest->expects($this->once()) - ->method('getBundleOption') -- ->willReturn([3 => 5]); -+ ->willReturn($bundleOptions); -+ -+ $this->arrayUtility->expects($this->once())->method('flatten')->willReturn($bundleOptions); -+ - $selectionCollection->expects($this->any()) - ->method('getItems') - ->willReturn([$selection]); -@@ -737,10 +753,6 @@ class TypeTest extends \PHPUnit\Framework\TestCase - ->method('prepareForCart') - ->willReturn([]); - -- $this->priceCurrency->expects($this->once()) -- ->method('convert') -- ->willReturn(3.14); -- - $result = $this->model->prepareForCartAdvanced($buyRequest, $product); - $this->assertEquals('We can\'t add this item to your shopping cart right now.', $result); - } -@@ -898,9 +910,10 @@ class TypeTest extends \PHPUnit\Framework\TestCase - ->with($selectionCollection, true, true); - $productType->expects($this->once()) - ->method('setStoreFilter'); -- $buyRequest->expects($this->once()) -- ->method('getBundleOption') -- ->willReturn([3 => 5]); -+ -+ $bundleOptions = [3 => 5]; -+ $buyRequest->expects($this->once())->method('getBundleOption')->willReturn($bundleOptions); -+ - $selectionCollection->expects($this->any()) - ->method('getItems') - ->willReturn([$selection]); -@@ -951,6 +964,9 @@ class TypeTest extends \PHPUnit\Framework\TestCase - $option->expects($this->once()) - ->method('getTitle') - ->willReturn('Title for option'); -+ -+ $this->arrayUtility->expects($this->once())->method('flatten')->willReturn($bundleOptions); -+ - $buyRequest->expects($this->once()) - ->method('getBundleOptionQty') - ->willReturn([3 => 5]); -@@ -961,10 +977,6 @@ class TypeTest extends \PHPUnit\Framework\TestCase - ->method('prepareForCart') - ->willReturn('string'); - -- $this->priceCurrency->expects($this->once()) -- ->method('convert') -- ->willReturn(3.14); -- - $result = $this->model->prepareForCartAdvanced($buyRequest, $product); - $this->assertEquals('string', $result); - } -@@ -1065,13 +1077,15 @@ class TypeTest extends \PHPUnit\Framework\TestCase - ->willReturn(333); - $productType->expects($this->once()) - ->method('setStoreFilter'); -- $buyRequest->expects($this->once()) -- ->method('getBundleOption') -- ->willReturn([]); -+ -+ $bundleOptions = []; -+ $buyRequest->expects($this->once())->method('getBundleOption')->willReturn($bundleOptions); - $buyRequest->expects($this->once()) - ->method('getBundleOptionQty') - ->willReturn([3 => 5]); - -+ $this->arrayUtility->expects($this->once())->method('flatten')->willReturn($bundleOptions); -+ - $result = $this->model->prepareForCartAdvanced($buyRequest, $product, 'single'); - $this->assertEquals([$product], $result); - } -@@ -1177,9 +1191,12 @@ class TypeTest extends \PHPUnit\Framework\TestCase - ->with($selectionCollection, true, true); - $productType->expects($this->once()) - ->method('setStoreFilter'); -- $buyRequest->expects($this->once()) -- ->method('getBundleOption') -- ->willReturn([3 => 5]); -+ -+ $bundleOptions = [3 => 5]; -+ $buyRequest->expects($this->once())->method('getBundleOption')->willReturn($bundleOptions); -+ -+ $this->arrayUtility->expects($this->once())->method('flatten')->willReturn($bundleOptions); -+ - $selectionCollection->expects($this->at(0)) - ->method('getItems') - ->willReturn([$selection]); -@@ -1301,9 +1318,12 @@ class TypeTest extends \PHPUnit\Framework\TestCase - ->willReturn($option); - $productType->expects($this->once()) - ->method('setStoreFilter'); -- $buyRequest->expects($this->once()) -- ->method('getBundleOption') -- ->willReturn([3 => 5]); -+ -+ $bundleOptions = [3 => 5]; -+ $buyRequest->expects($this->once())->method('getBundleOption')->willReturn($bundleOptions); -+ -+ $this->arrayUtility->expects($this->once())->method('flatten')->willReturn($bundleOptions); -+ - $selectionCollection->expects($this->any()) - ->method('getItems') - ->willReturn([$selection]); -@@ -1595,7 +1615,7 @@ class TypeTest extends \PHPUnit\Framework\TestCase - ->disableOriginalConstructor() - ->getMock(); - $selectionItemMock = $this->getMockBuilder(\Magento\Framework\DataObject::class) -- ->setMethods(['getSku', '__wakeup']) -+ ->setMethods(['getSku', 'getEntityId', '__wakeup']) - ->disableOriginalConstructor() - ->getMock(); - -@@ -1623,9 +1643,12 @@ class TypeTest extends \PHPUnit\Framework\TestCase - ->will($this->returnValue($serializeIds)); - $selectionMock = $this->getSelectionsByIdsMock($selectionIds, $productMock, 5, 6); - $selectionMock->expects(($this->any())) -- ->method('getItems') -- ->will($this->returnValue([$selectionItemMock])); -- $selectionItemMock->expects($this->any()) -+ ->method('getItemByColumnValue') -+ ->will($this->returnValue($selectionItemMock)); -+ $selectionItemMock->expects($this->at(0)) -+ ->method('getEntityId') -+ ->will($this->returnValue(1)); -+ $selectionItemMock->expects($this->once()) - ->method('getSku') - ->will($this->returnValue($itemSku)); - -diff --git a/app/code/Magento/Bundle/Test/Unit/Model/ResourceModel/Selection/CollectionTest.php b/app/code/Magento/Bundle/Test/Unit/Model/ResourceModel/Selection/CollectionTest.php -deleted file mode 100644 -index e595f9a47f0..00000000000 ---- a/app/code/Magento/Bundle/Test/Unit/Model/ResourceModel/Selection/CollectionTest.php -+++ /dev/null -@@ -1,156 +0,0 @@ --<?php --/** -- * Copyright © Magento, Inc. All rights reserved. -- * See COPYING.txt for license details. -- */ --namespace Magento\Bundle\Test\Unit\Model\ResourceModel\Selection; -- --use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; --use Magento\Store\Model\StoreManagerInterface; --use Magento\Store\Api\Data\StoreInterface; --use Magento\Framework\Validator\UniversalFactory; --use Magento\Eav\Model\Entity\AbstractEntity; --use Magento\Framework\DB\Adapter\AdapterInterface; --use Magento\Catalog\Model\ResourceModel\Product\Collection\ProductLimitationFactory; --use Magento\Framework\DB\Select; -- --/** -- * Class CollectionTest. -- */ --class CollectionTest extends \PHPUnit\Framework\TestCase --{ -- /** -- * @var \PHPUnit_Framework_MockObject_MockObject -- */ -- private $storeManager; -- -- /** -- * @var \PHPUnit_Framework_MockObject_MockObject -- */ -- private $store; -- -- /** -- * @var \PHPUnit_Framework_MockObject_MockObject -- */ -- private $universalFactory; -- -- /** -- * @var \PHPUnit_Framework_MockObject_MockObject -- */ -- private $entity; -- -- /** -- * @var \PHPUnit_Framework_MockObject_MockObject -- */ -- private $adapter; -- -- /** -- * @var \PHPUnit_Framework_MockObject_MockObject -- */ -- private $select; -- -- /** -- * @var \Magento\Bundle\Model\ResourceModel\Selection\Collection -- */ -- private $model; -- -- protected function setUp() -- { -- $objectManager = new ObjectManager($this); -- $this->storeManager = $this->getMockBuilder(StoreManagerInterface::class) -- ->disableOriginalConstructor() -- ->getMock(); -- $this->store = $this->getMockBuilder(StoreInterface::class) -- ->disableOriginalConstructor() -- ->getMock(); -- $this->universalFactory = $this->getMockBuilder(UniversalFactory::class) -- ->disableOriginalConstructor() -- ->getMock(); -- $this->entity = $this->getMockBuilder(AbstractEntity::class) -- ->disableOriginalConstructor() -- ->getMock(); -- $this->adapter = $this->getMockBuilder(AdapterInterface::class) -- ->disableOriginalConstructor() -- ->getMock(); -- $this->select = $this->getMockBuilder(Select::class) -- ->disableOriginalConstructor() -- ->getMock(); -- $factory = $this->getMockBuilder(ProductLimitationFactory::class) -- ->disableOriginalConstructor() -- ->setMethods(['create']) -- ->getMock(); -- -- $this->storeManager->expects($this->any()) -- ->method('getStore') -- ->willReturn($this->store); -- $this->store->expects($this->any()) -- ->method('getId') -- ->willReturn(1); -- $this->universalFactory->expects($this->any()) -- ->method('create') -- ->willReturn($this->entity); -- $this->entity->expects($this->any()) -- ->method('getConnection') -- ->willReturn($this->adapter); -- $this->entity->expects($this->any()) -- ->method('getDefaultAttributes') -- ->willReturn([]); -- $this->adapter->expects($this->any()) -- ->method('select') -- ->willReturn($this->select); -- -- $this->model = $objectManager->getObject( -- \Magento\Bundle\Model\ResourceModel\Selection\Collection::class, -- [ -- 'storeManager' => $this->storeManager, -- 'universalFactory' => $this->universalFactory, -- 'productLimitationFactory' => $factory -- ] -- ); -- } -- -- public function testAddQuantityFilter() -- { -- $statusTableName = 'cataloginventory_stock_status'; -- $itemTableName = 'cataloginventory_stock_item'; -- $this->entity->expects($this->exactly(2)) -- ->method('getTable') -- ->willReturnMap([ -- ['cataloginventory_stock_item', $itemTableName], -- ['cataloginventory_stock_status', $statusTableName], -- ]); -- $this->select->expects($this->exactly(2)) -- ->method('joinInner') -- ->withConsecutive( -- [ -- ['stock' => $statusTableName], -- 'selection.product_id = stock.product_id', -- [], -- ], -- [ -- ['stock_item' => $itemTableName], -- 'selection.product_id = stock_item.product_id', -- [], -- ] -- )->willReturnSelf(); -- $this->select -- ->expects($this->exactly(2)) -- ->method('where') -- ->withConsecutive( -- [ -- '(' -- . 'selection.selection_can_change_qty > 0' -- . ' or ' -- . 'selection.selection_qty <= stock.qty' -- . ' or ' -- .'stock_item.manage_stock = 0' -- . ')', -- ], -- [ -- 'stock.stock_status = 1', -- ] -- )->willReturnSelf(); -- -- $this->assertEquals($this->model, $this->model->addQuantityFilter()); -- } --} -diff --git a/app/code/Magento/Bundle/Test/Unit/Pricing/Price/SpecialPriceTest.php b/app/code/Magento/Bundle/Test/Unit/Pricing/Price/SpecialPriceTest.php -index f38dfc5538c..3e60e057fe6 100644 ---- a/app/code/Magento/Bundle/Test/Unit/Pricing/Price/SpecialPriceTest.php -+++ b/app/code/Magento/Bundle/Test/Unit/Pricing/Price/SpecialPriceTest.php -@@ -6,6 +6,7 @@ - namespace Magento\Bundle\Test\Unit\Pricing\Price; - - use \Magento\Bundle\Pricing\Price\SpecialPrice; -+use Magento\Store\Api\Data\WebsiteInterface; - - class SpecialPriceTest extends \PHPUnit\Framework\TestCase - { -@@ -77,12 +78,6 @@ class SpecialPriceTest extends \PHPUnit\Framework\TestCase - ->method('getSpecialPrice') - ->will($this->returnValue($specialPrice)); - -- $store = $this->getMockBuilder(\Magento\Store\Model\Store::class) -- ->disableOriginalConstructor() -- ->getMock(); -- $this->saleable->expects($this->once()) -- ->method('getStore') -- ->will($this->returnValue($store)); - $this->saleable->expects($this->once()) - ->method('getSpecialFromDate') - ->will($this->returnValue($specialFromDate)); -@@ -92,7 +87,7 @@ class SpecialPriceTest extends \PHPUnit\Framework\TestCase - - $this->localeDate->expects($this->once()) - ->method('isScopeDateInInterval') -- ->with($store, $specialFromDate, $specialToDate) -+ ->with(WebsiteInterface::ADMIN_CODE, $specialFromDate, $specialToDate) - ->will($this->returnValue($isScopeDateInInterval)); - - $this->priceCurrencyMock->expects($this->never()) -diff --git a/app/code/Magento/Bundle/Ui/DataProvider/Product/Form/Modifier/BundlePanel.php b/app/code/Magento/Bundle/Ui/DataProvider/Product/Form/Modifier/BundlePanel.php -index 98fd96c52cc..ad6fc12712c 100644 ---- a/app/code/Magento/Bundle/Ui/DataProvider/Product/Form/Modifier/BundlePanel.php -+++ b/app/code/Magento/Bundle/Ui/DataProvider/Product/Form/Modifier/BundlePanel.php -@@ -14,6 +14,7 @@ use Magento\Framework\Stdlib\ArrayManager; - use Magento\Framework\UrlInterface; - use Magento\Ui\Component\Container; - use Magento\Ui\Component\Form; -+use Magento\Ui\Component\Form\Fieldset; - use Magento\Ui\Component\Modal; - - /** -@@ -69,13 +70,26 @@ class BundlePanel extends AbstractModifier - } - - /** -- * {@inheritdoc} -+ * @inheritdoc -+ * - * @SuppressWarnings(PHPMD.ExcessiveMethodLength) - */ - public function modifyMeta(array $meta) - { - $meta = $this->removeFixedTierPrice($meta); -- $path = $this->arrayManager->findPath(static::CODE_BUNDLE_DATA, $meta, null, 'children'); -+ -+ $groupCode = static::CODE_BUNDLE_DATA; -+ $path = $this->arrayManager->findPath($groupCode, $meta, null, 'children'); -+ if (empty($path)) { -+ $meta[$groupCode]['children'] = []; -+ $meta[$groupCode]['arguments']['data']['config'] = [ -+ 'componentType' => Fieldset::NAME, -+ 'label' => __('Bundle Items'), -+ 'collapsible' => true -+ ]; -+ -+ $path = $this->arrayManager->findPath($groupCode, $meta, null, 'children'); -+ } - - $meta = $this->arrayManager->merge( - $path, -@@ -220,7 +234,7 @@ class BundlePanel extends AbstractModifier - } - - /** -- * {@inheritdoc} -+ * @inheritdoc - */ - public function modifyData(array $data) - { -diff --git a/app/code/Magento/Bundle/etc/db_schema.xml b/app/code/Magento/Bundle/etc/db_schema.xml -index 8092f34c533..97e86e5c173 100644 ---- a/app/code/Magento/Bundle/etc/db_schema.xml -+++ b/app/code/Magento/Bundle/etc/db_schema.xml -@@ -18,13 +18,13 @@ - <column xsi:type="int" name="position" padding="10" unsigned="true" nullable="false" identity="false" - default="0" comment="Position"/> - <column xsi:type="varchar" name="type" nullable="true" length="255" comment="Type"/> -- <constraint xsi:type="primary" name="PRIMARY"> -+ <constraint xsi:type="primary" referenceId="PRIMARY"> - <column name="option_id"/> - </constraint> -- <constraint xsi:type="foreign" name="CAT_PRD_BNDL_OPT_PARENT_ID_CAT_PRD_ENTT_ENTT_ID" -+ <constraint xsi:type="foreign" referenceId="CAT_PRD_BNDL_OPT_PARENT_ID_CAT_PRD_ENTT_ENTT_ID" - table="catalog_product_bundle_option" column="parent_id" referenceTable="catalog_product_entity" - referenceColumn="entity_id" onDelete="CASCADE"/> -- <index name="CATALOG_PRODUCT_BUNDLE_OPTION_PARENT_ID" indexType="btree"> -+ <index referenceId="CATALOG_PRODUCT_BUNDLE_OPTION_PARENT_ID" indexType="btree"> - <column name="parent_id"/> - </index> - </table> -@@ -39,13 +39,13 @@ - <column xsi:type="varchar" name="title" nullable="true" length="255" comment="Title"/> - <column xsi:type="int" name="parent_product_id" padding="10" unsigned="true" nullable="false" identity="false" - comment="Parent Product Id"/> -- <constraint xsi:type="primary" name="PRIMARY"> -+ <constraint xsi:type="primary" referenceId="PRIMARY"> - <column name="value_id"/> - </constraint> -- <constraint xsi:type="foreign" name="CAT_PRD_BNDL_OPT_VAL_OPT_ID_CAT_PRD_BNDL_OPT_OPT_ID" -+ <constraint xsi:type="foreign" referenceId="CAT_PRD_BNDL_OPT_VAL_OPT_ID_CAT_PRD_BNDL_OPT_OPT_ID" - table="catalog_product_bundle_option_value" column="option_id" - referenceTable="catalog_product_bundle_option" referenceColumn="option_id" onDelete="CASCADE"/> -- <constraint xsi:type="unique" name="CAT_PRD_BNDL_OPT_VAL_OPT_ID_PARENT_PRD_ID_STORE_ID"> -+ <constraint xsi:type="unique" referenceId="CAT_PRD_BNDL_OPT_VAL_OPT_ID_PARENT_PRD_ID_STORE_ID"> - <column name="option_id"/> - <column name="parent_product_id"/> - <column name="store_id"/> -@@ -73,19 +73,19 @@ - comment="Selection Qty"/> - <column xsi:type="smallint" name="selection_can_change_qty" padding="6" unsigned="false" nullable="false" - identity="false" default="0" comment="Selection Can Change Qty"/> -- <constraint xsi:type="primary" name="PRIMARY"> -+ <constraint xsi:type="primary" referenceId="PRIMARY"> - <column name="selection_id"/> - </constraint> -- <constraint xsi:type="foreign" name="CAT_PRD_BNDL_SELECTION_OPT_ID_CAT_PRD_BNDL_OPT_OPT_ID" -+ <constraint xsi:type="foreign" referenceId="CAT_PRD_BNDL_SELECTION_OPT_ID_CAT_PRD_BNDL_OPT_OPT_ID" - table="catalog_product_bundle_selection" column="option_id" - referenceTable="catalog_product_bundle_option" referenceColumn="option_id" onDelete="CASCADE"/> -- <constraint xsi:type="foreign" name="CAT_PRD_BNDL_SELECTION_PRD_ID_CAT_PRD_ENTT_ENTT_ID" -+ <constraint xsi:type="foreign" referenceId="CAT_PRD_BNDL_SELECTION_PRD_ID_CAT_PRD_ENTT_ENTT_ID" - table="catalog_product_bundle_selection" column="product_id" referenceTable="catalog_product_entity" - referenceColumn="entity_id" onDelete="CASCADE"/> -- <index name="CATALOG_PRODUCT_BUNDLE_SELECTION_OPTION_ID" indexType="btree"> -+ <index referenceId="CATALOG_PRODUCT_BUNDLE_SELECTION_OPTION_ID" indexType="btree"> - <column name="option_id"/> - </index> -- <index name="CATALOG_PRODUCT_BUNDLE_SELECTION_PRODUCT_ID" indexType="btree"> -+ <index referenceId="CATALOG_PRODUCT_BUNDLE_SELECTION_PRODUCT_ID" indexType="btree"> - <column name="product_id"/> - </index> - </table> -@@ -97,72 +97,72 @@ - comment="Website Id"/> - <column xsi:type="smallint" name="selection_price_type" padding="5" unsigned="true" nullable="false" - identity="false" default="0" comment="Selection Price Type"/> -- <column xsi:type="decimal" name="selection_price_value" scale="4" precision="12" unsigned="false" -+ <column xsi:type="decimal" name="selection_price_value" scale="6" precision="20" unsigned="false" - nullable="false" default="0" comment="Selection Price Value"/> - <column xsi:type="int" name="parent_product_id" padding="10" unsigned="true" nullable="false" identity="false" - comment="Parent Product Id"/> -- <constraint xsi:type="primary" name="PK_CATALOG_PRODUCT_BUNDLE_SELECTION_PRICE"> -+ <constraint xsi:type="primary" referenceId="PK_CATALOG_PRODUCT_BUNDLE_SELECTION_PRICE"> - <column name="selection_id"/> - <column name="parent_product_id"/> - <column name="website_id"/> - </constraint> -- <constraint xsi:type="foreign" name="CAT_PRD_BNDL_SELECTION_PRICE_WS_ID_STORE_WS_WS_ID" -+ <constraint xsi:type="foreign" referenceId="CAT_PRD_BNDL_SELECTION_PRICE_WS_ID_STORE_WS_WS_ID" - table="catalog_product_bundle_selection_price" column="website_id" referenceTable="store_website" - referenceColumn="website_id" onDelete="CASCADE"/> -- <constraint xsi:type="foreign" name="FK_DCF37523AA05D770A70AA4ED7C2616E4" -+ <constraint xsi:type="foreign" referenceId="FK_DCF37523AA05D770A70AA4ED7C2616E4" - table="catalog_product_bundle_selection_price" column="selection_id" - referenceTable="catalog_product_bundle_selection" referenceColumn="selection_id" - onDelete="CASCADE"/> -- <index name="CATALOG_PRODUCT_BUNDLE_SELECTION_PRICE_WEBSITE_ID" indexType="btree"> -+ <index referenceId="CATALOG_PRODUCT_BUNDLE_SELECTION_PRICE_WEBSITE_ID" indexType="btree"> - <column name="website_id"/> - </index> - </table> - <table name="catalog_product_bundle_price_index" resource="default" engine="innodb" - comment="Catalog Product Bundle Price Index"> - <column xsi:type="int" name="entity_id" padding="10" unsigned="true" nullable="false" identity="false" -- comment="Entity Id"/> -+ comment="Entity ID"/> - <column xsi:type="smallint" name="website_id" padding="5" unsigned="true" nullable="false" identity="false" - comment="Website Id"/> - <column xsi:type="int" name="customer_group_id" padding="10" unsigned="true" nullable="false" identity="false" -- comment="Customer Group Id"/> -- <column xsi:type="decimal" name="min_price" scale="4" precision="12" unsigned="false" nullable="false" -+ comment="Customer Group ID"/> -+ <column xsi:type="decimal" name="min_price" scale="6" precision="20" unsigned="false" nullable="false" - comment="Min Price"/> -- <column xsi:type="decimal" name="max_price" scale="4" precision="12" unsigned="false" nullable="false" -+ <column xsi:type="decimal" name="max_price" scale="6" precision="20" unsigned="false" nullable="false" - comment="Max Price"/> -- <constraint xsi:type="primary" name="PRIMARY"> -+ <constraint xsi:type="primary" referenceId="PRIMARY"> - <column name="entity_id"/> - <column name="website_id"/> - <column name="customer_group_id"/> - </constraint> -- <constraint xsi:type="foreign" name="CAT_PRD_BNDL_PRICE_IDX_CSTR_GROUP_ID_CSTR_GROUP_CSTR_GROUP_ID" -+ <constraint xsi:type="foreign" referenceId="CAT_PRD_BNDL_PRICE_IDX_CSTR_GROUP_ID_CSTR_GROUP_CSTR_GROUP_ID" - table="catalog_product_bundle_price_index" column="customer_group_id" - referenceTable="customer_group" referenceColumn="customer_group_id" onDelete="CASCADE"/> -- <constraint xsi:type="foreign" name="CAT_PRD_BNDL_PRICE_IDX_ENTT_ID_CAT_PRD_ENTT_ENTT_ID" -+ <constraint xsi:type="foreign" referenceId="CAT_PRD_BNDL_PRICE_IDX_ENTT_ID_CAT_PRD_ENTT_ENTT_ID" - table="catalog_product_bundle_price_index" column="entity_id" - referenceTable="catalog_product_entity" referenceColumn="entity_id" onDelete="CASCADE"/> -- <constraint xsi:type="foreign" name="CAT_PRD_BNDL_PRICE_IDX_WS_ID_STORE_WS_WS_ID" -+ <constraint xsi:type="foreign" referenceId="CAT_PRD_BNDL_PRICE_IDX_WS_ID_STORE_WS_WS_ID" - table="catalog_product_bundle_price_index" column="website_id" referenceTable="store_website" - referenceColumn="website_id" onDelete="CASCADE"/> -- <index name="CATALOG_PRODUCT_BUNDLE_PRICE_INDEX_WEBSITE_ID" indexType="btree"> -+ <index referenceId="CATALOG_PRODUCT_BUNDLE_PRICE_INDEX_WEBSITE_ID" indexType="btree"> - <column name="website_id"/> - </index> -- <index name="CATALOG_PRODUCT_BUNDLE_PRICE_INDEX_CUSTOMER_GROUP_ID" indexType="btree"> -+ <index referenceId="CATALOG_PRODUCT_BUNDLE_PRICE_INDEX_CUSTOMER_GROUP_ID" indexType="btree"> - <column name="customer_group_id"/> - </index> - </table> - <table name="catalog_product_bundle_stock_index" resource="default" engine="innodb" - comment="Catalog Product Bundle Stock Index"> - <column xsi:type="int" name="entity_id" padding="10" unsigned="true" nullable="false" identity="false" -- comment="Entity Id"/> -+ comment="Entity ID"/> - <column xsi:type="smallint" name="website_id" padding="5" unsigned="true" nullable="false" identity="false" -- comment="Website Id"/> -+ comment="Website ID"/> - <column xsi:type="smallint" name="stock_id" padding="5" unsigned="true" nullable="false" identity="false" -- comment="Stock Id"/> -+ comment="Stock ID"/> - <column xsi:type="int" name="option_id" padding="10" unsigned="true" nullable="false" identity="false" - default="0" comment="Option Id"/> - <column xsi:type="smallint" name="stock_status" padding="6" unsigned="false" nullable="true" identity="false" - default="0" comment="Stock Status"/> -- <constraint xsi:type="primary" name="PRIMARY"> -+ <constraint xsi:type="primary" referenceId="PRIMARY"> - <column name="entity_id"/> - <column name="website_id"/> - <column name="stock_id"/> -@@ -172,32 +172,32 @@ - <table name="catalog_product_index_price_bundle_idx" resource="default" engine="innodb" - comment="Catalog Product Index Price Bundle Idx"> - <column xsi:type="int" name="entity_id" padding="10" unsigned="true" nullable="false" identity="false" -- comment="Entity Id"/> -+ comment="Entity ID"/> - <column xsi:type="int" name="customer_group_id" padding="11" unsigned="false" nullable="false" - identity="false"/> - <column xsi:type="smallint" name="website_id" padding="5" unsigned="true" nullable="false" identity="false" -- comment="Website Id"/> -+ comment="Website ID"/> - <column xsi:type="smallint" name="tax_class_id" padding="5" unsigned="true" nullable="true" identity="false" -- default="0" comment="Tax Class Id"/> -+ default="0" comment="Tax Class ID"/> - <column xsi:type="smallint" name="price_type" padding="5" unsigned="true" nullable="false" identity="false" - comment="Price Type"/> -- <column xsi:type="decimal" name="special_price" scale="4" precision="12" unsigned="false" nullable="true" -+ <column xsi:type="decimal" name="special_price" scale="6" precision="20" unsigned="false" nullable="true" - comment="Special Price"/> -- <column xsi:type="decimal" name="tier_percent" scale="4" precision="12" unsigned="false" nullable="true" -+ <column xsi:type="decimal" name="tier_percent" scale="6" precision="20" unsigned="false" nullable="true" - comment="Tier Percent"/> -- <column xsi:type="decimal" name="orig_price" scale="4" precision="12" unsigned="false" nullable="true" -+ <column xsi:type="decimal" name="orig_price" scale="6" precision="20" unsigned="false" nullable="true" - comment="Orig Price"/> -- <column xsi:type="decimal" name="price" scale="4" precision="12" unsigned="false" nullable="true" -+ <column xsi:type="decimal" name="price" scale="6" precision="20" unsigned="false" nullable="true" - comment="Price"/> -- <column xsi:type="decimal" name="min_price" scale="4" precision="12" unsigned="false" nullable="true" -+ <column xsi:type="decimal" name="min_price" scale="6" precision="20" unsigned="false" nullable="true" - comment="Min Price"/> -- <column xsi:type="decimal" name="max_price" scale="4" precision="12" unsigned="false" nullable="true" -+ <column xsi:type="decimal" name="max_price" scale="6" precision="20" unsigned="false" nullable="true" - comment="Max Price"/> -- <column xsi:type="decimal" name="tier_price" scale="4" precision="12" unsigned="false" nullable="true" -+ <column xsi:type="decimal" name="tier_price" scale="6" precision="20" unsigned="false" nullable="true" - comment="Tier Price"/> -- <column xsi:type="decimal" name="base_tier" scale="4" precision="12" unsigned="false" nullable="true" -+ <column xsi:type="decimal" name="base_tier" scale="6" precision="20" unsigned="false" nullable="true" - comment="Base Tier"/> -- <constraint xsi:type="primary" name="PRIMARY"> -+ <constraint xsi:type="primary" referenceId="PRIMARY"> - <column name="entity_id"/> - <column name="customer_group_id"/> - <column name="website_id"/> -@@ -206,32 +206,32 @@ - <table name="catalog_product_index_price_bundle_tmp" resource="default" engine="memory" - comment="Catalog Product Index Price Bundle Tmp"> - <column xsi:type="int" name="entity_id" padding="10" unsigned="true" nullable="false" identity="false" -- comment="Entity Id"/> -+ comment="Entity ID"/> - <column xsi:type="int" name="customer_group_id" padding="11" unsigned="false" nullable="false" - identity="false"/> - <column xsi:type="smallint" name="website_id" padding="5" unsigned="true" nullable="false" identity="false" -- comment="Website Id"/> -+ comment="Website ID"/> - <column xsi:type="smallint" name="tax_class_id" padding="5" unsigned="true" nullable="true" identity="false" -- default="0" comment="Tax Class Id"/> -+ default="0" comment="Tax Class ID"/> - <column xsi:type="smallint" name="price_type" padding="5" unsigned="true" nullable="false" identity="false" - comment="Price Type"/> -- <column xsi:type="decimal" name="special_price" scale="4" precision="12" unsigned="false" nullable="true" -+ <column xsi:type="decimal" name="special_price" scale="6" precision="20" unsigned="false" nullable="true" - comment="Special Price"/> -- <column xsi:type="decimal" name="tier_percent" scale="4" precision="12" unsigned="false" nullable="true" -+ <column xsi:type="decimal" name="tier_percent" scale="6" precision="20" unsigned="false" nullable="true" - comment="Tier Percent"/> -- <column xsi:type="decimal" name="orig_price" scale="4" precision="12" unsigned="false" nullable="true" -+ <column xsi:type="decimal" name="orig_price" scale="6" precision="20" unsigned="false" nullable="true" - comment="Orig Price"/> -- <column xsi:type="decimal" name="price" scale="4" precision="12" unsigned="false" nullable="true" -+ <column xsi:type="decimal" name="price" scale="6" precision="20" unsigned="false" nullable="true" - comment="Price"/> -- <column xsi:type="decimal" name="min_price" scale="4" precision="12" unsigned="false" nullable="true" -+ <column xsi:type="decimal" name="min_price" scale="6" precision="20" unsigned="false" nullable="true" - comment="Min Price"/> -- <column xsi:type="decimal" name="max_price" scale="4" precision="12" unsigned="false" nullable="true" -+ <column xsi:type="decimal" name="max_price" scale="6" precision="20" unsigned="false" nullable="true" - comment="Max Price"/> -- <column xsi:type="decimal" name="tier_price" scale="4" precision="12" unsigned="false" nullable="true" -+ <column xsi:type="decimal" name="tier_price" scale="6" precision="20" unsigned="false" nullable="true" - comment="Tier Price"/> -- <column xsi:type="decimal" name="base_tier" scale="4" precision="12" unsigned="false" nullable="true" -+ <column xsi:type="decimal" name="base_tier" scale="6" precision="20" unsigned="false" nullable="true" - comment="Base Tier"/> -- <constraint xsi:type="primary" name="PRIMARY"> -+ <constraint xsi:type="primary" referenceId="PRIMARY"> - <column name="entity_id"/> - <column name="customer_group_id"/> - <column name="website_id"/> -@@ -240,11 +240,11 @@ - <table name="catalog_product_index_price_bundle_sel_idx" resource="default" engine="innodb" - comment="Catalog Product Index Price Bundle Sel Idx"> - <column xsi:type="int" name="entity_id" padding="10" unsigned="true" nullable="false" identity="false" -- comment="Entity Id"/> -+ comment="Entity ID"/> - <column xsi:type="int" name="customer_group_id" padding="11" unsigned="false" nullable="false" - identity="false"/> - <column xsi:type="smallint" name="website_id" padding="5" unsigned="true" nullable="false" identity="false" -- comment="Website Id"/> -+ comment="Website ID"/> - <column xsi:type="int" name="option_id" padding="10" unsigned="true" nullable="false" identity="false" - default="0" comment="Option Id"/> - <column xsi:type="int" name="selection_id" padding="10" unsigned="true" nullable="false" identity="false" -@@ -253,11 +253,11 @@ - default="0" comment="Group Type"/> - <column xsi:type="smallint" name="is_required" padding="5" unsigned="true" nullable="true" identity="false" - default="0" comment="Is Required"/> -- <column xsi:type="decimal" name="price" scale="4" precision="12" unsigned="false" nullable="true" -+ <column xsi:type="decimal" name="price" scale="6" precision="20" unsigned="false" nullable="true" - comment="Price"/> -- <column xsi:type="decimal" name="tier_price" scale="4" precision="12" unsigned="false" nullable="true" -+ <column xsi:type="decimal" name="tier_price" scale="6" precision="20" unsigned="false" nullable="true" - comment="Tier Price"/> -- <constraint xsi:type="primary" name="PRIMARY"> -+ <constraint xsi:type="primary" referenceId="PRIMARY"> - <column name="entity_id"/> - <column name="customer_group_id"/> - <column name="website_id"/> -@@ -268,11 +268,11 @@ - <table name="catalog_product_index_price_bundle_sel_tmp" resource="default" engine="memory" - comment="Catalog Product Index Price Bundle Sel Tmp"> - <column xsi:type="int" name="entity_id" padding="10" unsigned="true" nullable="false" identity="false" -- comment="Entity Id"/> -+ comment="Entity ID"/> - <column xsi:type="int" name="customer_group_id" padding="11" unsigned="false" nullable="false" - identity="false"/> - <column xsi:type="smallint" name="website_id" padding="5" unsigned="true" nullable="false" identity="false" -- comment="Website Id"/> -+ comment="Website ID"/> - <column xsi:type="int" name="option_id" padding="10" unsigned="true" nullable="false" identity="false" - default="0" comment="Option Id"/> - <column xsi:type="int" name="selection_id" padding="10" unsigned="true" nullable="false" identity="false" -@@ -281,11 +281,11 @@ - default="0" comment="Group Type"/> - <column xsi:type="smallint" name="is_required" padding="5" unsigned="true" nullable="true" identity="false" - default="0" comment="Is Required"/> -- <column xsi:type="decimal" name="price" scale="4" precision="12" unsigned="false" nullable="true" -+ <column xsi:type="decimal" name="price" scale="6" precision="20" unsigned="false" nullable="true" - comment="Price"/> -- <column xsi:type="decimal" name="tier_price" scale="4" precision="12" unsigned="false" nullable="true" -+ <column xsi:type="decimal" name="tier_price" scale="6" precision="20" unsigned="false" nullable="true" - comment="Tier Price"/> -- <constraint xsi:type="primary" name="PRIMARY"> -+ <constraint xsi:type="primary" referenceId="PRIMARY"> - <column name="entity_id"/> - <column name="customer_group_id"/> - <column name="website_id"/> -@@ -296,24 +296,24 @@ - <table name="catalog_product_index_price_bundle_opt_idx" resource="default" engine="innodb" - comment="Catalog Product Index Price Bundle Opt Idx"> - <column xsi:type="int" name="entity_id" padding="10" unsigned="true" nullable="false" identity="false" -- comment="Entity Id"/> -+ comment="Entity ID"/> - <column xsi:type="int" name="customer_group_id" padding="11" unsigned="false" nullable="false" - identity="false"/> - <column xsi:type="smallint" name="website_id" padding="5" unsigned="true" nullable="false" identity="false" -- comment="Website Id"/> -+ comment="Website ID"/> - <column xsi:type="int" name="option_id" padding="10" unsigned="true" nullable="false" identity="false" - default="0" comment="Option Id"/> -- <column xsi:type="decimal" name="min_price" scale="4" precision="12" unsigned="false" nullable="true" -+ <column xsi:type="decimal" name="min_price" scale="6" precision="20" unsigned="false" nullable="true" - comment="Min Price"/> -- <column xsi:type="decimal" name="alt_price" scale="4" precision="12" unsigned="false" nullable="true" -+ <column xsi:type="decimal" name="alt_price" scale="6" precision="20" unsigned="false" nullable="true" - comment="Alt Price"/> -- <column xsi:type="decimal" name="max_price" scale="4" precision="12" unsigned="false" nullable="true" -+ <column xsi:type="decimal" name="max_price" scale="6" precision="20" unsigned="false" nullable="true" - comment="Max Price"/> -- <column xsi:type="decimal" name="tier_price" scale="4" precision="12" unsigned="false" nullable="true" -+ <column xsi:type="decimal" name="tier_price" scale="6" precision="20" unsigned="false" nullable="true" - comment="Tier Price"/> -- <column xsi:type="decimal" name="alt_tier_price" scale="4" precision="12" unsigned="false" nullable="true" -+ <column xsi:type="decimal" name="alt_tier_price" scale="6" precision="20" unsigned="false" nullable="true" - comment="Alt Tier Price"/> -- <constraint xsi:type="primary" name="PRIMARY"> -+ <constraint xsi:type="primary" referenceId="PRIMARY"> - <column name="entity_id"/> - <column name="customer_group_id"/> - <column name="website_id"/> -@@ -323,24 +323,24 @@ - <table name="catalog_product_index_price_bundle_opt_tmp" resource="default" engine="memory" - comment="Catalog Product Index Price Bundle Opt Tmp"> - <column xsi:type="int" name="entity_id" padding="10" unsigned="true" nullable="false" identity="false" -- comment="Entity Id"/> -+ comment="Entity ID"/> - <column xsi:type="int" name="customer_group_id" padding="11" unsigned="false" nullable="false" - identity="false"/> - <column xsi:type="smallint" name="website_id" padding="5" unsigned="true" nullable="false" identity="false" -- comment="Website Id"/> -+ comment="Website ID"/> - <column xsi:type="int" name="option_id" padding="10" unsigned="true" nullable="false" identity="false" - default="0" comment="Option Id"/> -- <column xsi:type="decimal" name="min_price" scale="4" precision="12" unsigned="false" nullable="true" -+ <column xsi:type="decimal" name="min_price" scale="6" precision="20" unsigned="false" nullable="true" - comment="Min Price"/> -- <column xsi:type="decimal" name="alt_price" scale="4" precision="12" unsigned="false" nullable="true" -+ <column xsi:type="decimal" name="alt_price" scale="6" precision="20" unsigned="false" nullable="true" - comment="Alt Price"/> -- <column xsi:type="decimal" name="max_price" scale="4" precision="12" unsigned="false" nullable="true" -+ <column xsi:type="decimal" name="max_price" scale="6" precision="20" unsigned="false" nullable="true" - comment="Max Price"/> -- <column xsi:type="decimal" name="tier_price" scale="4" precision="12" unsigned="false" nullable="true" -+ <column xsi:type="decimal" name="tier_price" scale="6" precision="20" unsigned="false" nullable="true" - comment="Tier Price"/> -- <column xsi:type="decimal" name="alt_tier_price" scale="4" precision="12" unsigned="false" nullable="true" -+ <column xsi:type="decimal" name="alt_tier_price" scale="6" precision="20" unsigned="false" nullable="true" - comment="Alt Tier Price"/> -- <constraint xsi:type="primary" name="PRIMARY"> -+ <constraint xsi:type="primary" referenceId="PRIMARY"> - <column name="entity_id"/> - <column name="customer_group_id"/> - <column name="website_id"/> -diff --git a/app/code/Magento/Bundle/etc/di.xml b/app/code/Magento/Bundle/etc/di.xml -index 733b089dccd..72155d922a2 100644 ---- a/app/code/Magento/Bundle/etc/di.xml -+++ b/app/code/Magento/Bundle/etc/di.xml -@@ -123,6 +123,9 @@ - </argument> - </arguments> - </type> -+ <type name="Magento\Quote\Model\Quote\Item"> -+ <plugin name="update_price_for_bundle_in_quote_item_option" type="Magento\Bundle\Plugin\UpdatePriceInQuoteItemOptions"/> -+ </type> - <type name="Magento\Quote\Model\Quote\Item\ToOrderItem"> - <plugin name="append_bundle_data_to_order" type="Magento\Bundle\Model\Plugin\QuoteItem"/> - </type> -@@ -140,6 +143,13 @@ - </argument> - </arguments> - </type> -+ <type name="Magento\Sales\Model\Order\ProductOption"> -+ <arguments> -+ <argument name="processorPool" xsi:type="array"> -+ <item name="bundle" xsi:type="object">Magento\Bundle\Model\ProductOptionProcessor</item> -+ </argument> -+ </arguments> -+ </type> - <type name="Magento\Bundle\Ui\DataProvider\Product\Listing\Collector\BundlePrice"> - <arguments> - <argument name="excludeAdjustments" xsi:type="array"> -diff --git a/app/code/Magento/Bundle/etc/frontend/di.xml b/app/code/Magento/Bundle/etc/frontend/di.xml -index 54f6d3b4b0f..fc820ff87a1 100644 ---- a/app/code/Magento/Bundle/etc/frontend/di.xml -+++ b/app/code/Magento/Bundle/etc/frontend/di.xml -@@ -13,4 +13,7 @@ - </argument> - </arguments> - </type> -+ <type name="Magento\Catalog\Model\Product"> -+ <plugin name="bundle" type="Magento\Bundle\Model\Plugin\Frontend\Product" sortOrder="100" /> -+ </type> - </config> -diff --git a/app/code/Magento/Bundle/view/adminhtml/templates/catalog/product/edit/tab/attributes/extend.phtml b/app/code/Magento/Bundle/view/adminhtml/templates/catalog/product/edit/tab/attributes/extend.phtml -index 224cd71538b..f028c7013df 100644 ---- a/app/code/Magento/Bundle/view/adminhtml/templates/catalog/product/edit/tab/attributes/extend.phtml -+++ b/app/code/Magento/Bundle/view/adminhtml/templates/catalog/product/edit/tab/attributes/extend.phtml -@@ -4,8 +4,6 @@ - * See COPYING.txt for license details. - */ - --// @codingStandardsIgnoreFile -- - /** @var $block \Magento\Bundle\Block\Adminhtml\Catalog\Product\Edit\Tab\Attributes\Extend */ - $elementHtml = $block->getParentElementHtml(); - -@@ -20,18 +18,18 @@ $isElementReadonly = $block->getElement() - ->getReadonly(); - ?> - --<?php if (!($attributeCode === 'price' && $block->getCanReadPrice() === false)) { ?> -- <div class="<?= /* @escapeNotVerified */ $attributeCode ?> "><?= /* @escapeNotVerified */ $elementHtml ?></div> --<?php } ?> -+<?php if (!($attributeCode === 'price' && $block->getCanReadPrice() === false)) : ?> -+ <div class="<?= $block->escapeHtmlAttr($attributeCode) ?> "><?= /* @noEscape */ $elementHtml ?></div> -+<?php endif; ?> - - <?= $block->getExtendedElement($switchAttributeCode)->toHtml() ?> - - <?php if (!$isElementReadonly && $block->getDisableChild()) { ?> - <script> - require(['prototype'], function () { -- function <?= /* @escapeNotVerified */ $switchAttributeCode ?>_change() { -- var $attribute = $('<?= /* @escapeNotVerified */ $attributeCode ?>'); -- if ($('<?= /* @escapeNotVerified */ $switchAttributeCode ?>').value == '<?= /* @escapeNotVerified */ $block::DYNAMIC ?>') { -+ function <?= /* @noEscape */ $switchAttributeCode ?>_change() { -+ var $attribute = $('<?= $block->escapeJs($attributeCode) ?>'); -+ if ($('<?= /* @noEscape */ $switchAttributeCode ?>').value == '<?= $block->escapeJs($block::DYNAMIC) ?>') { - if ($attribute) { - $attribute.disabled = true; - $attribute.value = ''; -@@ -43,13 +41,13 @@ $isElementReadonly = $block->getElement() - } else { - if ($attribute) { - <?php if ($attributeCode === 'price' && !$block->getCanEditPrice() && $block->getCanReadPrice() -- && $block->getProduct()->isObjectNew()) { ?> -- <?php $defaultProductPrice = $block->getDefaultProductPrice() ?: "''"; ?> -- $attribute.value = <?= /* @escapeNotVerified */ $defaultProductPrice ?>; -- <?php } else { ?> -+ && $block->getProduct()->isObjectNew()) : ?> -+ <?php $defaultProductPrice = $block->getDefaultProductPrice() ?: "''"; ?> -+ $attribute.value = <?= /* @noEscape */ (string)$defaultProductPrice ?>; -+ <?php else : ?> - $attribute.disabled = false; - $attribute.addClassName('required-entry'); -- <?php } ?> -+ <?php endif; ?> - } - if ($('dynamic-price-warning')) { - $('dynamic-price-warning').hide(); -@@ -58,11 +56,11 @@ $isElementReadonly = $block->getElement() - } - - <?php if (!($attributeCode === 'price' && !$block->getCanEditPrice() -- && !$block->getProduct()->isObjectNew())) { ?> -- $('<?= /* @escapeNotVerified */ $switchAttributeCode ?>').observe('change', <?= /* @escapeNotVerified */ $switchAttributeCode ?>_change); -- <?php } ?> -+ && !$block->getProduct()->isObjectNew())) : ?> -+ $('<?= /* @noEscape */ $switchAttributeCode ?>').observe('change', <?= /* @noEscape */ $switchAttributeCode ?>_change); -+ <?php endif; ?> - Event.observe(window, 'load', function(){ -- <?= /* @escapeNotVerified */ $switchAttributeCode ?>_change(); -+ <?= /* @noEscape */ $switchAttributeCode ?>_change(); - }); - }); - </script> -diff --git a/app/code/Magento/Bundle/view/adminhtml/templates/product/composite/fieldset/options/bundle.phtml b/app/code/Magento/Bundle/view/adminhtml/templates/product/composite/fieldset/options/bundle.phtml -index 87798a6ba62..53ad0a96324 100644 ---- a/app/code/Magento/Bundle/view/adminhtml/templates/product/composite/fieldset/options/bundle.phtml -+++ b/app/code/Magento/Bundle/view/adminhtml/templates/product/composite/fieldset/options/bundle.phtml -@@ -3,17 +3,16 @@ - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -- --// @codingStandardsIgnoreFile -- - ?> - - <?php /* @var $block \Magento\Bundle\Block\Adminhtml\Catalog\Product\Composite\Fieldset\Bundle */ ?> - <?php $options = $block->decorateArray($block->getOptions(true)); ?> --<?php if (count($options)): ?> -+<?php if (count($options)) : ?> - <fieldset id="catalog_product_composite_configure_fields_bundle" - class="fieldset admin__fieldset composite-bundle<?= $block->getIsLastFieldset() ? ' last-fieldset' : '' ?>"> -- <legend class="legend admin__legend"><span><?= /* @escapeNotVerified */ __('Bundle Items') ?></span></legend><br /> -+ <legend class="legend admin__legend"> -+ <span><?= $block->escapeHtml(__('Bundle Items')) ?></span> -+ </legend><br /> - <?php foreach ($options as $option) : ?> - <?php if ($option->getSelections()) : ?> - <?= $block->getOptionHtml($option) ?> -@@ -71,7 +70,7 @@ require([ - } - } - }; -- ProductConfigure.bundleControl = new BundleControl(<?= /* @escapeNotVerified */ $block->getJsonConfig() ?>); -+ ProductConfigure.bundleControl = new BundleControl(<?= /* @noEscape */ $block->getJsonConfig() ?>); - }); - </script> - -diff --git a/app/code/Magento/Bundle/view/adminhtml/templates/product/composite/fieldset/options/type/checkbox.phtml b/app/code/Magento/Bundle/view/adminhtml/templates/product/composite/fieldset/options/type/checkbox.phtml -index 44ed02f2758..08e89699b1f 100644 ---- a/app/code/Magento/Bundle/view/adminhtml/templates/product/composite/fieldset/options/type/checkbox.phtml -+++ b/app/code/Magento/Bundle/view/adminhtml/templates/product/composite/fieldset/options/type/checkbox.phtml -@@ -3,60 +3,58 @@ - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -- --// @codingStandardsIgnoreFile -- -+// phpcs:disable Magento2.Templates.ThisInTemplate.FoundThis - ?> - <?php /* @var $block \Magento\Bundle\Block\Adminhtml\Catalog\Product\Composite\Fieldset\Options\Type\Checkbox */ ?> - <?php $_option = $block->getOption(); ?> - <?php $_selections = $_option->getSelections(); ?> --<?php $_skipSaleableCheck = $this->helper('Magento\Catalog\Helper\Product')->getSkipSaleableCheck(); ?> -+<?php $_skipSaleableCheck = $this->helper(Magento\Catalog\Helper\Product::class)->getSkipSaleableCheck(); ?> - --<div class="field admin__field options<?php if ($_option->getRequired()) echo ' required _required' ?>"> -+<div class="field admin__field options<?php if ($_option->getRequired()) { echo ' required _required'; } ?>"> - <label class="label admin__field-label"> - <span><?= $block->escapeHtml($_option->getTitle()) ?></span> - </label> - - <div class="control admin__field-control"> -- <div class="nested <?php if ($_option->getDecoratedIsLast()):?> last<?php endif;?>"> -+ <div class="nested <?php if ($_option->getDecoratedIsLast()) :?> last<?php endif;?>"> - -- <?php if (count($_selections) == 1 && $_option->getRequired()): ?> -- <?= /* @escapeNotVerified */ $block->getSelectionQtyTitlePrice($_selections[0]) ?> -+ <?php if (count($_selections) == 1 && $_option->getRequired()) : ?> -+ <?= /* @noEscape */ $block->getSelectionQtyTitlePrice($_selections[0]) ?> - <input type="hidden" -- name="bundle_option[<?= /* @escapeNotVerified */ $_option->getId() ?>]" -- value="<?= /* @escapeNotVerified */ $_selections[0]->getSelectionId() ?>" -- price="<?= /* @escapeNotVerified */ $block->getSelectionPrice($_selections[0]) ?>" /> -- <?php else:?> -+ name="bundle_option[<?= $block->escapeHtmlAttr($_option->getId()) ?>]" -+ value="<?= $block->escapeHtmlAttr($_selections[0]->getSelectionId()) ?>" -+ price="<?= $block->escapeHtmlAttr($block->getSelectionPrice($_selections[0])) ?>" /> -+ <?php else :?> - -- <?php foreach ($_selections as $_selection): ?> -+ <?php foreach ($_selections as $_selection) : ?> - <div class="field choice admin__field admin__field-option"> - <input -- class="change-container-classname admin__control-checkbox checkbox bundle-option-<?= /* @escapeNotVerified */ $_option->getId() ?> <?php if ($_option->getRequired()) echo 'validate-one-required-by-name' ?>" -- id="bundle-option-<?= /* @escapeNotVerified */ $_option->getId() ?>-<?= /* @escapeNotVerified */ $_selection->getSelectionId() ?>" -+ class="change-container-classname admin__control-checkbox checkbox bundle-option-<?= $block->escapeHtmlAttr($_option->getId()) ?> <?php if ($_option->getRequired()) { echo 'validate-one-required-by-name'; } ?>" -+ id="bundle-option-<?= $block->escapeHtmlAttr($_option->getId()) ?>-<?= $block->escapeHtmlAttr($_selection->getSelectionId()) ?>" - type="checkbox" -- name="bundle_option[<?= /* @escapeNotVerified */ $_option->getId() ?>][<?= /* @escapeNotVerified */ $_selection->getId() ?>]" -- <?php if ($block->isSelected($_selection)):?> -+ name="bundle_option[<?= $block->escapeHtmlAttr($_option->getId()) ?>][<?= $block->escapeHtmlAttr($_selection->getId()) ?>]" -+ <?php if ($block->isSelected($_selection)) :?> - <?= ' checked="checked"' ?> - <?php endif;?> -- <?php if (!$_selection->isSaleable() && !$_skipSaleableCheck):?> -+ <?php if (!$_selection->isSaleable() && !$_skipSaleableCheck) :?> - <?= ' disabled="disabled"' ?> - <?php endif;?> -- value="<?= /* @escapeNotVerified */ $_selection->getSelectionId() ?>" -+ value="<?= $block->escapeHtmlAttr($_selection->getSelectionId()) ?>" - onclick="ProductConfigure.bundleControl.changeSelection(this)" -- price="<?= /* @escapeNotVerified */ $block->getSelectionPrice($_selection) ?>" /> -+ price="<?= $block->escapeHtmlAttr($block->getSelectionPrice($_selection)) ?>" /> - - <label class="admin__field-label" -- for="bundle-option-<?= /* @escapeNotVerified */ $_option->getId() ?>-<?= /* @escapeNotVerified */ $_selection->getSelectionId() ?>"> -- <span><?= /* @escapeNotVerified */ $block->getSelectionQtyTitlePrice($_selection) ?></span> -+ for="bundle-option-<?= $block->escapeHtmlAttr($_option->getId()) ?>-<?= $block->escapeHtmlAttr($_selection->getSelectionId()) ?>"> -+ <span><?= /* @noEscape */ $block->getSelectionQtyTitlePrice($_selection) ?></span> - </label> - -- <?php if ($_option->getRequired()): ?> -- <?= /* @escapeNotVerified */ $block->setValidationContainer('bundle-option-' . $_option->getId() . '-' . $_selection->getSelectionId(), 'bundle-option-' . $_option->getId() . '-container') ?> -+ <?php if ($_option->getRequired()) : ?> -+ <?= /* @noEscape */ $block->setValidationContainer('bundle-option-' . $_option->getId() . '-' . $_selection->getSelectionId(), 'bundle-option-' . $_option->getId() . '-container') ?> - <?php endif;?> - </div> - <?php endforeach; ?> - -- <div id="bundle-option-<?= /* @escapeNotVerified */ $_option->getId() ?>-container"></div> -+ <div id="bundle-option-<?= $block->escapeHtmlAttr($_option->getId()) ?>-container"></div> - <?php endif; ?> - </div> - </div> -diff --git a/app/code/Magento/Bundle/view/adminhtml/templates/product/composite/fieldset/options/type/multi.phtml b/app/code/Magento/Bundle/view/adminhtml/templates/product/composite/fieldset/options/type/multi.phtml -index 8c13dd6479d..f4c4e3e51ae 100644 ---- a/app/code/Magento/Bundle/view/adminhtml/templates/product/composite/fieldset/options/type/multi.phtml -+++ b/app/code/Magento/Bundle/view/adminhtml/templates/product/composite/fieldset/options/type/multi.phtml -@@ -3,32 +3,34 @@ - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -- --// @codingStandardsIgnoreFile -- -+// phpcs:disable Magento2.Templates.ThisInTemplate.FoundThis - ?> - <?php /* @var $block \Magento\Bundle\Block\Adminhtml\Catalog\Product\Composite\Fieldset\Options\Type\Multi */ ?> - <?php $_option = $block->getOption(); ?> - <?php $_selections = $_option->getSelections(); ?> --<?php $_skipSaleableCheck = $this->helper('Magento\Catalog\Helper\Product')->getSkipSaleableCheck(); ?> --<div class="field admin__field <?php if ($_option->getRequired()) echo ' required' ?><?php if ($_option->getDecoratedIsLast()):?> last<?php endif; ?>"> -+<?php $_skipSaleableCheck = $this->helper(Magento\Catalog\Helper\Product::class)->getSkipSaleableCheck(); ?> -+<div class="field admin__field <?php if ($_option->getRequired()) { echo ' required'; } ?><?php if ($_option->getDecoratedIsLast()) :?> last<?php endif; ?>"> - <label class="label admin__field-label"><span><?= $block->escapeHtml($_option->getTitle()) ?></span></label> - <div class="control admin__field-control"> -- <?php if (count($_selections) == 1 && $_option->getRequired()): ?> -- <?= /* @escapeNotVerified */ $block->getSelectionQtyTitlePrice($_selections[0]) ?> -- <input type="hidden" name="bundle_option[<?= /* @escapeNotVerified */ $_option->getId() ?>]" -- value="<?= /* @escapeNotVerified */ $_selections[0]->getSelectionId() ?>" -- price="<?= /* @escapeNotVerified */ $block->getSelectionPrice($_selections[0]) ?>" /> -- <?php else: ?> -- <select multiple="multiple" size="5" id="bundle-option-<?= /* @escapeNotVerified */ $_option->getId() ?>" -- name="bundle_option[<?= /* @escapeNotVerified */ $_option->getId() ?>][]" -- class="admin__control-multiselect bundle-option-<?= /* @escapeNotVerified */ $_option->getId() ?><?php if ($_option->getRequired()) echo ' required-entry' ?> multiselect change-container-classname" -+ <?php if (count($_selections) == 1 && $_option->getRequired()) : ?> -+ <?= /* @noEscape */ $block->getSelectionQtyTitlePrice($_selections[0]) ?> -+ <input type="hidden" name="bundle_option[<?= $block->escapeHtmlAttr($_option->getId()) ?>]" -+ value="<?= $block->escapeHtmlAttr($_selections[0]->getSelectionId()) ?>" -+ price="<?= $block->escapeHtmlAttr($block->getSelectionPrice($_selections[0])) ?>" /> -+ <?php else : ?> -+ <select multiple="multiple" size="5" id="bundle-option-<?= $block->escapeHtmlAttr($_option->getId()) ?>" -+ name="bundle_option[<?= $block->escapeHtmlAttr($_option->getId()) ?>][]" -+ class="admin__control-multiselect bundle-option-<?= $block->escapeHtmlAttr($_option->getId()) ?><?php if ($_option->getRequired()) { echo ' required-entry'; } ?> multiselect change-container-classname" - onchange="ProductConfigure.bundleControl.changeSelection(this)"> -- <?php if(!$_option->getRequired()): ?> -- <option value=""><?= /* @escapeNotVerified */ __('None') ?></option> -+ <?php if (!$_option->getRequired()) : ?> -+ <option value=""><?= $block->escapeHtml(__('None')) ?></option> - <?php endif; ?> -- <?php foreach ($_selections as $_selection): ?> -- <option value="<?= /* @escapeNotVerified */ $_selection->getSelectionId() ?>"<?php if ($block->isSelected($_selection)) echo ' selected="selected"' ?><?php if (!$_selection->isSaleable() && !$_skipSaleableCheck) echo ' disabled="disabled"' ?> price="<?= /* @escapeNotVerified */ $block->getSelectionPrice($_selection) ?>"><?= /* @escapeNotVerified */ $block->getSelectionQtyTitlePrice($_selection, false) ?></option> -+ <?php foreach ($_selections as $_selection) : ?> -+ <option value="<?= $block->escapeHtmlAttr($_selection->getSelectionId()) ?>" -+ <?php if ($block->isSelected($_selection)) { echo ' selected="selected"'; } ?> -+ <?php if (!$_selection->isSaleable() && !$_skipSaleableCheck) { echo ' disabled="disabled"'; } ?> -+ price="<?= $block->escapeHtmlAttr($block->getSelectionPrice($_selection)) ?>"> -+ <?= /* @noEscape */ $block->getSelectionQtyTitlePrice($_selection, false) ?></option> - <?php endforeach; ?> - </select> - <?php endif; ?> -diff --git a/app/code/Magento/Bundle/view/adminhtml/templates/product/composite/fieldset/options/type/radio.phtml b/app/code/Magento/Bundle/view/adminhtml/templates/product/composite/fieldset/options/type/radio.phtml -index f0912979a92..0c3835fb32a 100644 ---- a/app/code/Magento/Bundle/view/adminhtml/templates/product/composite/fieldset/options/type/radio.phtml -+++ b/app/code/Magento/Bundle/view/adminhtml/templates/product/composite/fieldset/options/type/radio.phtml -@@ -3,69 +3,73 @@ - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -- --// @codingStandardsIgnoreFile -- -+// phpcs:disable Magento2.Templates.ThisInTemplate.FoundThis - ?> - <?php /* @var $block \Magento\Bundle\Block\Adminhtml\Catalog\Product\Composite\Fieldset\Options\Type\Radio */ ?> - <?php $_option = $block->getOption(); ?> - <?php $_selections = $_option->getSelections(); ?> - <?php $_default = $_option->getDefaultSelection(); ?> --<?php $_skipSaleableCheck = $this->helper('Magento\Catalog\Helper\Product')->getSkipSaleableCheck(); ?> -+<?php $_skipSaleableCheck = $this->helper(Magento\Catalog\Helper\Product::class)->getSkipSaleableCheck(); ?> - <?php list($_defaultQty, $_canChangeQty) = $block->getDefaultValues(); ?> - --<div class="field admin__field options<?php if ($_option->getRequired()) echo ' required' ?>"> -+<div class="field admin__field options<?php if ($_option->getRequired()) { echo ' required'; } ?>"> - <label class="label admin__field-label"><span><?= $block->escapeHtml($_option->getTitle()) ?></span></label> - <div class="control admin__field-control"> -- <div class="nested<?php if ($_option->getDecoratedIsLast()):?> last<?php endif; ?>"> -- <?php if ($block->showSingle()): ?> -- <?= /* @escapeNotVerified */ $block->getSelectionTitlePrice($_selections[0]) ?> -+ <div class="nested<?php if ($_option->getDecoratedIsLast()) :?> last<?php endif; ?>"> -+ <?php if ($block->showSingle()) : ?> -+ <?= /* @noEscape */ $block->getSelectionTitlePrice($_selections[0]) ?> - <input type="hidden" -- name="bundle_option[<?= /* @escapeNotVerified */ $_option->getId() ?>]" -- value="<?= /* @escapeNotVerified */ $_selections[0]->getSelectionId() ?>" -- price="<?= /* @escapeNotVerified */ $block->getSelectionPrice($_selections[0]) ?>" /> -- <?php else:?> -- <?php if (!$_option->getRequired()): ?> -+ name="bundle_option[<?= $block->escapeHtmlAttr($_option->getId()) ?>]" -+ value="<?= $block->escapeHtmlAttr($_selections[0]->getSelectionId()) ?>" -+ price="<?= $block->escapeHtmlAttr($block->getSelectionPrice($_selections[0])) ?>" /> -+ <?php else :?> -+ <?php if (!$_option->getRequired()) : ?> - <div class="field choice admin__field admin__field-option"> - <input type="radio" - class="radio admin__control-radio" -- id="bundle-option-<?= /* @escapeNotVerified */ $_option->getId() ?>" -- name="bundle_option[<?= /* @escapeNotVerified */ $_option->getId() ?>]"<?= ($_default && $_default->isSalable()) ? '' : ' checked="checked" ' ?> -+ id="bundle-option-<?= $block->escapeHtmlAttr($_option->getId()) ?>" -+ name="bundle_option[<?= $block->escapeHtmlAttr($_option->getId()) ?>]"<?= ($_default && $_default->isSalable()) ? '' : ' checked="checked" ' ?> - value="" - onclick="ProductConfigure.bundleControl.changeSelection(this)" /> - <label class="admin__field-label" -- for="bundle-option-<?= /* @escapeNotVerified */ $_option->getId() ?>"><span><?= /* @escapeNotVerified */ __('None') ?></span></label> -+ for="bundle-option-<?= $block->escapeHtmlAttr($_option->getId()) ?>"><span><?= $block->escapeHtml(__('None')) ?></span></label> - </div> - <?php endif; ?> - -- <?php foreach ($_selections as $_selection): ?> -+ <?php foreach ($_selections as $_selection) : ?> - <div class="field choice admin__field admin__field-option"> - <input type="radio" - class="radio admin__control-radio <?= $_option->getRequired() ? ' validate-one-required-by-name' : '' ?> change-container-classname" -- id="bundle-option-<?= /* @escapeNotVerified */ $_option->getId() ?>-<?= /* @escapeNotVerified */ $_selection->getSelectionId() ?>" -- name="bundle_option[<?= /* @escapeNotVerified */ $_option->getId() ?>]" -- <?php if ($block->isSelected($_selection)) echo ' checked="checked"' ?><?php if (!$_selection->isSaleable() && !$_skipSaleableCheck) echo ' disabled="disabled"' ?> -- value="<?= /* @escapeNotVerified */ $_selection->getSelectionId() ?>" -+ id="bundle-option-<?= $block->escapeHtmlAttr($_option->getId()) ?>-<?= $block->escapeHtmlAttr($_selection->getSelectionId()) ?>" -+ name="bundle_option[<?= $block->escapeHtmlAttr($_option->getId()) ?>]" -+ <?php if ($block->isSelected($_selection)) { echo ' checked="checked"'; } ?> -+ <?php if (!$_selection->isSaleable() && !$_skipSaleableCheck) { echo ' disabled="disabled"'; } ?> -+ value="<?= $block->escapeHtmlAttr($_selection->getSelectionId()) ?>" - onclick="ProductConfigure.bundleControl.changeSelection(this)" -- price="<?= /* @escapeNotVerified */ $block->getSelectionPrice($_selection) ?>" -- qtyId="bundle-option-<?= /* @escapeNotVerified */ $_option->getId() ?>-qty-input" /> -+ price="<?= $block->escapeHtmlAttr($block->getSelectionPrice($_selection)) ?>" -+ qtyId="bundle-option-<?= $block->escapeHtmlAttr($_option->getId()) ?>-qty-input" /> - <label class="admin__field-label" -- for="bundle-option-<?= /* @escapeNotVerified */ $_option->getId() ?>-<?= /* @escapeNotVerified */ $_selection->getSelectionId() ?>"><span><?= /* @escapeNotVerified */ $block->getSelectionTitlePrice($_selection) ?></span></label> -- <?php if ($_option->getRequired()): ?> -- <?= /* @escapeNotVerified */ $block->setValidationContainer('bundle-option-'.$_option->getId().'-'.$_selection->getSelectionId(), 'bundle-option-'.$_option->getId().'-container') ?> -+ for="bundle-option-<?= $block->escapeHtmlAttr($_option->getId()) ?>-<?= $block->escapeHtmlAttr($_selection->getSelectionId()) ?>"> -+ <span><?= /* @noEscape */ $block->getSelectionTitlePrice($_selection) ?></span> -+ </label> -+ <?php if ($_option->getRequired()) : ?> -+ <?= /* @noEscape */ $block->setValidationContainer('bundle-option-'.$_option->getId().'-'.$_selection->getSelectionId(), 'bundle-option-'.$_option->getId().'-container') ?> - <?php endif; ?> - </div> - <?php endforeach; ?> -- <div id="bundle-option-<?= /* @escapeNotVerified */ $_option->getId() ?>-container"></div> -+ <div id="bundle-option-<?= $block->escapeHtmlAttr($_option->getId()) ?>-container"></div> - <?php endif; ?> - <div class="field admin__field qty"> - <label class="label admin__field-label" -- for="bundle-option-<?= /* @escapeNotVerified */ $_option->getId() ?>-qty-input"><span><?= /* @escapeNotVerified */ __('Quantity:') ?></span></label> -- <div class="control admin__field-control"><input <?php if (!$_canChangeQty) echo ' disabled="disabled"' ?> -- id="bundle-option-<?= /* @escapeNotVerified */ $_option->getId() ?>-qty-input" -- class="input-text admin__control-text qty<?php if (!$_canChangeQty) echo ' qty-disabled' ?>" -+ for="bundle-option-<?= $block->escapeHtmlAttr($_option->getId()) ?>-qty-input"> -+ <span><?= $block->escapeHtml(__('Quantity:')) ?></span> -+ </label> -+ <div class="control admin__field-control"><input <?php if (!$_canChangeQty) { echo ' disabled="disabled"'; } ?> -+ id="bundle-option-<?= $block->escapeHtmlAttr($_option->getId()) ?>-qty-input" -+ class="input-text admin__control-text qty<?php if (!$_canChangeQty) { echo ' qty-disabled'; } ?>" - type="text" -- name="bundle_option_qty[<?= /* @escapeNotVerified */ $_option->getId() ?>]" value="<?= /* @escapeNotVerified */ $_defaultQty ?>" /> -+ name="bundle_option_qty[<?= $block->escapeHtmlAttr($_option->getId()) ?>]" -+ value="<?= $block->escapeHtmlAttr($_defaultQty) ?>" /> - </div> - </div> - </div> -diff --git a/app/code/Magento/Bundle/view/adminhtml/templates/product/composite/fieldset/options/type/select.phtml b/app/code/Magento/Bundle/view/adminhtml/templates/product/composite/fieldset/options/type/select.phtml -index 32766f62163..fbb7f7fbb7b 100644 ---- a/app/code/Magento/Bundle/view/adminhtml/templates/product/composite/fieldset/options/type/select.phtml -+++ b/app/code/Magento/Bundle/view/adminhtml/templates/product/composite/fieldset/options/type/select.phtml -@@ -3,36 +3,39 @@ - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -- --// @codingStandardsIgnoreFile -- -+// phpcs:disable Magento2.Templates.ThisInTemplate.FoundThis - ?> - <?php /* @var $block \Magento\Bundle\Block\Adminhtml\Catalog\Product\Composite\Fieldset\Options\Type\Select */ ?> - <?php $_option = $block->getOption(); ?> - <?php $_selections = $_option->getSelections(); ?> - <?php $_default = $_option->getDefaultSelection(); ?> --<?php $_skipSaleableCheck = $this->helper('Magento\Catalog\Helper\Product')->getSkipSaleableCheck(); ?> -+<?php $_skipSaleableCheck = $this->helper(Magento\Catalog\Helper\Product::class)->getSkipSaleableCheck(); ?> - <?php list($_defaultQty, $_canChangeQty) = $block->getDefaultValues(); ?> - --<div class="field admin__field option<?php if ($_option->getDecoratedIsLast()):?> last<?php endif; ?><?php if ($_option->getRequired()) echo ' required _required' ?>"> -+<div class="field admin__field option<?php if ($_option->getDecoratedIsLast()) :?> last<?php endif; ?><?php if ($_option->getRequired()) { echo ' required _required'; } ?>"> - <label class="label admin__field-label"><span><?= $block->escapeHtml($_option->getTitle()) ?></span></label> - <div class="control admin__field-control"> -- <?php if ($block->showSingle()): ?> -- <?= /* @escapeNotVerified */ $block->getSelectionTitlePrice($_selections[0]) ?> -- <input type="hidden" name="bundle_option[<?= /* @escapeNotVerified */ $_option->getId() ?>]" -- value="<?= /* @escapeNotVerified */ $_selections[0]->getSelectionId() ?>" -- price="<?= /* @escapeNotVerified */ $block->getSelectionPrice($_selections[0]) ?>" /> -- <?php else:?> -- <select id="bundle-option-<?= /* @escapeNotVerified */ $_option->getId() ?>" -- name="bundle_option[<?= /* @escapeNotVerified */ $_option->getId() ?>]" -- class="bundle-option-<?= /* @escapeNotVerified */ $_option->getId() ?><?php if ($_option->getRequired()) echo ' required-entry' ?> select admin__control-select change-container-classname" -+ <?php if ($block->showSingle()) : ?> -+ <?= /* @noEscape */ $block->getSelectionTitlePrice($_selections[0]) ?> -+ <input type="hidden" -+ name="bundle_option[<?= $block->escapeHtmlAttr($_option->getId()) ?>]" -+ value="<?= $block->escapeHtmlAttr($_selections[0]->getSelectionId()) ?>" -+ price="<?= $block->escapeHtmlAttr($block->getSelectionPrice($_selections[0])) ?>" /> -+ <?php else :?> -+ <select id="bundle-option-<?= $block->escapeHtmlAttr($_option->getId()) ?>" -+ name="bundle_option[<?= $block->escapeHtmlAttr($_option->getId()) ?>]" -+ class="bundle-option-<?= $block->escapeHtmlAttr($_option->getId()) ?><?php if ($_option->getRequired()) { echo ' required-entry'; } ?> select admin__control-select change-container-classname" - onchange="ProductConfigure.bundleControl.changeSelection(this)"> -- <option value=""><?= /* @escapeNotVerified */ __('Choose a selection...') ?></option> -- <?php foreach ($_selections as $_selection): ?> -+ <option value=""><?= $block->escapeHtml(__('Choose a selection...')) ?></option> -+ <?php foreach ($_selections as $_selection) : ?> - <option -- value="<?= /* @escapeNotVerified */ $_selection->getSelectionId() ?>"<?php if ($block->isSelected($_selection)) echo ' selected="selected"' ?><?php if (!$_selection->isSaleable() && !$_skipSaleableCheck) echo ' disabled="disabled"' ?> -- price="<?= /* @escapeNotVerified */ $block->getSelectionPrice($_selection) ?>" -- qtyId="bundle-option-<?= /* @escapeNotVerified */ $_option->getId() ?>-qty-input"><?= /* @escapeNotVerified */ $block->getSelectionTitlePrice($_selection, false) ?></option> -+ value="<?= $block->escapeHtmlAttr($_selection->getSelectionId()) ?>" -+ <?php if ($block->isSelected($_selection)) { echo ' selected="selected"'; } ?> -+ <?php if (!$_selection->isSaleable() && !$_skipSaleableCheck) { echo ' disabled="disabled"'; } ?> -+ price="<?= $block->escapeHtmlAttr($block->getSelectionPrice($_selection)) ?>" -+ qtyId="bundle-option-<?= $block->escapeHtmlAttr($_option->getId()) ?>-qty-input"> -+ <?= /* @noEscape */ $block->getSelectionTitlePrice($_selection, false) ?> -+ </option> - <?php endforeach; ?> - </select> - <?php endif; ?> -@@ -40,12 +43,16 @@ - <div class="nested"> - <div class="field admin__field qty"> - <label class="label admin__field-label" -- for="bundle-option-<?= /* @escapeNotVerified */ $_option->getId() ?>-qty-input"><span><?= /* @escapeNotVerified */ __('Quantity:') ?></span></label> -+ for="bundle-option-<?= $block->escapeHtmlAttr($_option->getId()) ?>-qty-input"> -+ <span><?= $block->escapeHtml(__('Quantity:')) ?></span> -+ </label> - <div class="control admin__field-control"> -- <input <?php if (!$_canChangeQty) echo ' disabled="disabled"' ?> -- id="bundle-option-<?= /* @escapeNotVerified */ $_option->getId() ?>-qty-input" -- class="input-text admin__control-text qty<?php if (!$_canChangeQty) echo ' qty-disabled' ?>" type="text" -- name="bundle_option_qty[<?= /* @escapeNotVerified */ $_option->getId() ?>]" value="<?= /* @escapeNotVerified */ $_defaultQty ?>" /> -+ <input <?php if (!$_canChangeQty) { echo ' disabled="disabled"'; } ?> -+ id="bundle-option-<?= $block->escapeHtmlAttr($_option->getId()) ?>-qty-input" -+ class="input-text admin__control-text qty<?php if (!$_canChangeQty) { echo ' qty-disabled'; } ?>" -+ type="text" -+ name="bundle_option_qty[<?= $block->escapeHtmlAttr($_option->getId()) ?>]" -+ value="<?= $block->escapeHtmlAttr($_defaultQty) ?>" /> - </div> - </div> - </div> -diff --git a/app/code/Magento/Bundle/view/adminhtml/templates/product/edit/bundle.phtml b/app/code/Magento/Bundle/view/adminhtml/templates/product/edit/bundle.phtml -index 5b27412dd88..c8ab6cc5b98 100644 ---- a/app/code/Magento/Bundle/view/adminhtml/templates/product/edit/bundle.phtml -+++ b/app/code/Magento/Bundle/view/adminhtml/templates/product/edit/bundle.phtml -@@ -4,8 +4,6 @@ - * See COPYING.txt for license details. - */ - --// @codingStandardsIgnoreFile -- - /** @var $block \Magento\Bundle\Block\Adminhtml\Catalog\Product\Edit\Tab\Bundle */ - ?> - <script> -@@ -19,14 +17,20 @@ if(typeof Bundle=='undefined') { - <div id="bundle_product_container" class="entry-edit form-inline"> - <fieldset class="fieldset"> - <div class="field field-ship-bundle-items"> -- <label for="shipment_type" class="label"><?= /* @escapeNotVerified */ __('Ship Bundle Items') ?></label> -+ <label for="shipment_type" class="label"><?= $block->escapeHtml(__('Ship Bundle Items')) ?></label> - <div class="control"> -- <select <?php if ($block->isReadonly()): ?>disabled="disabled" <?php endif;?> -+ <select <?php if ($block->isReadonly()) : ?>disabled="disabled" <?php endif;?> - id="shipment_type" -- name="<?= /* @escapeNotVerified */ $block->getFieldSuffix() ?>[shipment_type]" -+ name="<?= $block->escapeHtmlAttr($block->getFieldSuffix()) ?>[shipment_type]" - class="select"> -- <option value="1"><?= /* @escapeNotVerified */ __('Separately') ?></option> -- <option value="0"<?php if ($block->getProduct()->getShipmentType() == 0): ?> selected="selected"<?php endif; ?>><?= /* @escapeNotVerified */ __('Together') ?></option> -+ <option value="1"><?= $block->escapeHtml(__('Separately')) ?></option> -+ <option value="0" -+ <?php if ($block->getProduct()->getShipmentType() == 0) : ?> -+ selected="selected" -+ <?php endif; ?> -+ > -+ <?= $block->escapeHtml(__('Together')) ?> -+ </option> - </select> - </div> - </div> -@@ -48,7 +52,7 @@ require(["prototype", "mage/adminhtml/form"], function(){ - // re-bind form elements onchange - varienWindowOnload(true); - -- <?php if ($block->isReadonly()):?> -+ <?php if ($block->isReadonly()) :?> - $('product_bundle_container').select('input', 'select', 'textarea', 'button').each(function(input){ - input.disabled = true; - if (input.tagName.toLowerCase() == 'button') { -diff --git a/app/code/Magento/Bundle/view/adminhtml/templates/product/edit/bundle/option.phtml b/app/code/Magento/Bundle/view/adminhtml/templates/product/edit/bundle/option.phtml -index 783d71beb16..4d68d363b74 100644 ---- a/app/code/Magento/Bundle/view/adminhtml/templates/product/edit/bundle/option.phtml -+++ b/app/code/Magento/Bundle/view/adminhtml/templates/product/edit/bundle/option.phtml -@@ -3,16 +3,14 @@ - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -- --// @codingStandardsIgnoreFile -- -+// phpcs:disable Magento2.Templates.ThisInTemplate.FoundThis - /** @var $block \Magento\Bundle\Block\Adminhtml\Catalog\Product\Edit\Tab\Bundle\Option */ - ?> - <script id="bundle-option-template" type="text/x-magento-template"> -- <div id="<?= /* @escapeNotVerified */ $block->getFieldId() ?>_<%- data.index %>" class="option-box"> -- <div class="fieldset-wrapper admin__collapsible-block-wrapper opened" id="<?= /* @escapeNotVerified */ $block->getFieldId() ?>_<%- data.index %>-wrapper"> -+ <div id="<?= $block->escapeHtmlAttr($block->getFieldId()) ?>_<%- data.index %>" class="option-box"> -+ <div class="fieldset-wrapper admin__collapsible-block-wrapper opened" id="<?= $block->escapeHtmlAttr($block->getFieldId()) ?>_<%- data.index %>-wrapper"> - <div class="fieldset-wrapper-title"> -- <strong class="admin__collapsible-title" data-toggle="collapse" data-target="#<?= /* @escapeNotVerified */ $block->getFieldId() ?>_<%- data.index %>-content"> -+ <strong class="admin__collapsible-title" data-toggle="collapse" data-target="#<?= $block->escapeHtmlAttr($block->getFieldId()) ?>_<%- data.index %>-content"> - <span><%- data.default_title %></span> - </strong> - <div class="actions"> -@@ -20,55 +18,56 @@ - </div> - <div data-role="draggable-handle" class="draggable-handle"></div> - </div> -- <div class="fieldset-wrapper-content in collapse" id="<?= /* @escapeNotVerified */ $block->getFieldId() ?>_<%- data.index %>-content"> -+ <div class="fieldset-wrapper-content in collapse" id="<?= $block->escapeHtmlAttr($block->getFieldId()) ?>_<%- data.index %>-content"> - <fieldset class="fieldset"> - <fieldset class="fieldset-alt"> - <div class="field field-option-title required"> -- <label class="label" for="id_<?= /* @escapeNotVerified */ $block->getFieldName() ?>_<%- data.index %>_title"> -- <?= /* @escapeNotVerified */ __('Option Title') ?> -+ <label class="label" for="id_<?= $block->escapeHtmlAttr($block->getFieldName()) ?>_<%- data.index %>_title"> -+ <?= $block->escapeHtml(__('Option Title')) ?> - </label> - <div class="control"> -- <?php if ($block->isDefaultStore()): ?> -+ <?php if ($block->isDefaultStore()) : ?> - <input class="input-text required-entry" - type="text" -- name="<?= /* @escapeNotVerified */ $block->getFieldName() ?>[<%- data.index %>][title]" -- id="id_<?= /* @escapeNotVerified */ $block->getFieldName() ?>_<%- data.index %>_title" -+ name="<?= $block->escapeHtmlAttr($block->getFieldName()) ?>[<%- data.index %>][title]" -+ id="id_<?= $block->escapeHtmlAttr($block->getFieldName()) ?>_<%- data.index %>_title" - value="<%- data.title %>" - data-original-value="<%- data.title %>" /> -- <?php else: ?> -+ <?php else : ?> - <input class="input-text required-entry" - type="text" -- name="<?= /* @escapeNotVerified */ $block->getFieldName() ?>[<%- data.index %>][default_title]" -- id="id_<?= /* @escapeNotVerified */ $block->getFieldName() ?>_<%- data.index %>_default_title" -+ name="<?= $block->escapeHtmlAttr($block->getFieldName()) ?>[<%- data.index %>][default_title]" -+ id="id_<?= $block->escapeHtmlAttr($block->getFieldName()) ?>_<%- data.index %>_default_title" - value="<%- data.default_title %>" - data-original-value="<%- data.default_title %>" /> - <?php endif; ?> - <input type="hidden" -- id="<?= /* @escapeNotVerified */ $block->getFieldId() ?>_id_<%- data.index %>" -- name="<?= /* @escapeNotVerified */ $block->getFieldName() ?>[<%- data.index %>][option_id]" -+ id="<?= $block->escapeHtmlAttr($block->getFieldId()) ?>_id_<%- data.index %>" -+ name="<?= $block->escapeHtmlAttr($block->getFieldName()) ?>[<%- data.index %>][option_id]" - value="<%- data.option_id %>" /> - <input type="hidden" -- name="<?= /* @escapeNotVerified */ $block->getFieldName() ?>[<%- data.index %>][delete]" -+ name="<?= $block->escapeHtmlAttr($block->getFieldName()) ?>[<%- data.index %>][delete]" - value="" - data-state="deleted" /> - </div> - </div> -- <?php if (!$block->isDefaultStore()): ?> -+ <?php if (!$block->isDefaultStore()) : ?> - <div class="field field-option-store-view required"> -- <label class="label" for="id_<?= /* @escapeNotVerified */ $block->getFieldName() ?>_<%- data.index %>_title_store"> -- <?= /* @escapeNotVerified */ __('Store View Title') ?> -+ <label class="label" for="id_<?= $block->escapeHtmlAttr($block->getFieldName()) ?>_<%- data.index %>_title_store"> -+ <?= $block->escapeHtml(__('Store View Title')) ?> - </label> - <div class="control"> -- <input class="input-text required-entry" type="text" -- name="<?= /* @escapeNotVerified */ $block->getFieldName() ?>[<%- data.index %>][title]" -- id="id_<?= /* @escapeNotVerified */ $block->getFieldName() ?>_<%- data.index %>_title_store" -+ <input class="input-text required-entry" -+ type="text" -+ name="<?= $block->escapeHtmlAttr($block->getFieldName()) ?>[<%- data.index %>][title]" -+ id="id_<?= $block->escapeHtmlAttr($block->getFieldName()) ?>_<%- data.index %>_title_store" - value="<%- data.title %>" /> - </div> - </div> - <?php endif; ?> - <div class="field field-option-input-type required"> -- <label class="label" for="<?= /* @escapeNotVerified */ $block->getFieldId() . '_<%- data.index %>_type' ?>"> -- <?= /* @escapeNotVerified */ __('Input Type') ?> -+ <label class="label" for="<?= $block->escapeHtmlAttr($block->getFieldId() . '_<%- data.index %>_type') ?>"> -+ <?= $block->escapeHtml(__('Input Type')) ?> - </label> - <div class="control"> - <?= $block->getTypeSelectHtml() ?> -@@ -81,19 +80,19 @@ - checked="checked" - id="field-option-req" /> - <label for="field-option-req"> -- <?= /* @escapeNotVerified */ __('Required') ?> -+ <?= $block->escapeHtml(__('Required')) ?> - </label> - <span style="display:none"><?= $block->getRequireSelectHtml() ?></span> - </div> - </div> - <div class="field field-option-position no-display"> - <label class="label" for="field-option-position"> -- <?= /* @escapeNotVerified */ __('Position') ?> -+ <?= $block->escapeHtml(__('Position')) ?> - </label> - <div class="control"> - <input class="input-text validate-zero-or-greater" - type="text" -- name="<?= /* @escapeNotVerified */ $block->getFieldName() ?>[<%- data.index %>][position]" -+ name="<?= $block->escapeHtmlAttr($block->getFieldName()) ?>[<%- data.index %>][position]" - value="<%- data.position %>" - id="field-option-position" /> - </div> -@@ -101,13 +100,13 @@ - </fieldset> - - <div class="no-products-message"> -- <?= /* @escapeNotVerified */ __('There are no products in this option.') ?> -+ <?= $block->escapeHtml(__('There are no products in this option.')) ?> - </div> - <?= $block->getAddSelectionButtonHtml() ?> - </fieldset> - </div> - </div> -- <div id="<?= /* @escapeNotVerified */ $block->getFieldId() ?>_search_<%- data.index %>" class="selection-search"></div> -+ <div id="<?= $block->escapeHtmlAttr($block->getFieldId()) ?>_search_<%- data.index %>" class="selection-search"></div> - </div> - </script> - -@@ -141,7 +140,7 @@ function changeInputType(oldObject, oType) { - - Bundle.Option = Class.create(); - Bundle.Option.prototype = { -- idLabel : '<?= /* @escapeNotVerified */ $block->getFieldId() ?>', -+ idLabel : '<?= $block->escapeJs($block->getFieldId()) ?>', - templateText : '', - itemsCount : 0, - initialize : function(template) { -@@ -150,7 +149,7 @@ Bundle.Option.prototype = { - - add : function(data) { - if (!data) { -- data = <?= /* @escapeNotVerified */ $this->helper('Magento\Framework\Json\Helper\Data')->jsonEncode(['default_title' => __('New Option')]) ?>; -+ data = <?= /* @noEscape */ $this->helper(Magento\Framework\Json\Helper\Data::class)->jsonEncode(['default_title' => __('New Option')]) ?>; - } else { - data.title = data.title.replace(/</g, "<"); - data.title = data.title.replace(/"/g, """); -@@ -280,17 +279,17 @@ Bundle.Option.prototype = { - var optionIndex = 0; - bOption = new Bundle.Option(optionTemplate); - <?php -- foreach ($block->getOptions() as $_option) { -- /** @var $_option \Magento\Bundle\Model\Option */ -- /* @escapeNotVerified */ echo 'optionIndex = bOption.add(', $_option->toJson(), ');', PHP_EOL; -- if ($_option->getSelections()) { -- foreach ($_option->getSelections() as $_selection) { -- /** @var $_selection \Magento\Catalog\Model\Product */ -- $_selection->setName($block->escapeHtml($_selection->getName())); -- /* @escapeNotVerified */ echo 'bSelection.addRow(optionIndex,', $_selection->toJson(), ');', PHP_EOL; -- } -+foreach ($block->getOptions() as $_option) { -+ /** @var $_option \Magento\Bundle\Model\Option */ -+ /* @noEscape */ echo 'optionIndex = bOption.add(', $_option->toJson(), ');', PHP_EOL; -+ if ($_option->getSelections()) { -+ foreach ($_option->getSelections() as $_selection) { -+ /** @var $_selection \Magento\Catalog\Model\Product */ -+ $_selection->setName($block->escapeHtml($_selection->getName())); -+ /* @noEscape */ echo 'bSelection.addRow(optionIndex,', $_selection->toJson(), ');', PHP_EOL; - } - } -+} - ?> - function togglePriceType() { - bOption['priceType' + ($('price_type').value == '1' ? 'Fixed' : 'Dynamic')](); -diff --git a/app/code/Magento/Bundle/view/adminhtml/templates/product/edit/bundle/option/selection.phtml b/app/code/Magento/Bundle/view/adminhtml/templates/product/edit/bundle/option/selection.phtml -index 91c245afe57..0f1167f3d3e 100644 ---- a/app/code/Magento/Bundle/view/adminhtml/templates/product/edit/bundle/option/selection.phtml -+++ b/app/code/Magento/Bundle/view/adminhtml/templates/product/edit/bundle/option/selection.phtml -@@ -4,8 +4,6 @@ - * See COPYING.txt for license details. - */ - --// @codingStandardsIgnoreFile -- - /** @var $block \Magento\Bundle\Block\Adminhtml\Catalog\Product\Edit\Tab\Bundle\Option\Selection */ - ?> - <script id="bundle-option-selection-box-template" type="text/x-magento-template"> -@@ -13,16 +11,16 @@ - <thead> - <tr class="headings"> - <th class="col-draggable"></th> -- <th class="col-default"><?= /* @escapeNotVerified */ __('Default') ?></th> -- <th class="col-name"><?= /* @escapeNotVerified */ __('Name') ?></th> -- <th class="col-sku"><?= /* @escapeNotVerified */ __('SKU') ?></th> -- <?php if ($block->getCanReadPrice() !== false): ?> -- <th class="col-price price-type-box"><?= /* @escapeNotVerified */ __('Price') ?></th> -- <th class="col-price price-type-box"><?= /* @escapeNotVerified */ __('Price Type') ?></th> -+ <th class="col-default"><?= $block->escapeHtml(__('Default')) ?></th> -+ <th class="col-name"><?= $block->escapeHtml(__('Name')) ?></th> -+ <th class="col-sku"><?= $block->escapeHtml(__('SKU')) ?></th> -+ <?php if ($block->getCanReadPrice() !== false) : ?> -+ <th class="col-price price-type-box"><?= $block->escapeHtml(__('Price')) ?></th> -+ <th class="col-price price-type-box"><?= $block->escapeHtml(__('Price Type')) ?></th> - <?php endif; ?> -- <th class="col-qty"><?= /* @escapeNotVerified */ __('Default Quantity') ?></th> -- <th class="col-uqty qty-box"><?= /* @escapeNotVerified */ __('User Defined') ?></th> -- <th class="col-order type-order" style="display:none"><?= /* @escapeNotVerified */ __('Position') ?></th> -+ <th class="col-qty"><?= $block->escapeHtml(__('Default Quantity')) ?></th> -+ <th class="col-uqty qty-box"><?= $block->escapeHtml(__('User Defined')) ?></th> -+ <th class="col-order type-order" style="display:none"><?= $block->escapeHtml(__('Position')) ?></th> - <th class="col-actions"></th> - </tr> - </thead> -@@ -33,31 +31,38 @@ - <script id="bundle-option-selection-row-template" type="text/x-magento-template"> - <td class="col-draggable"> - <span data-role="draggable-handle" class="draggable-handle"></span> -- <input type="hidden" id="<?= /* @escapeNotVerified */ $block->getFieldId() ?>_id<%- data.index %>" -- name="<?= /* @escapeNotVerified */ $block->getFieldName() ?>[<%- data.parentIndex %>][<%- data.index %>][selection_id]" -+ <input type="hidden" -+ id="<?= $block->escapeHtmlAttr($block->getFieldId()) ?>_id<%- data.index %>" -+ name="<?= $block->escapeHtmlAttr($block->getFieldName()) ?>[<%- data.parentIndex %>][<%- data.index %>][selection_id]" - value="<%- data.selection_id %>"/> -- <input type="hidden" name="<?= /* @escapeNotVerified */ $block->getFieldName() ?>[<%- data.parentIndex %>][<%- data.index %>][option_id]" -+ <input type="hidden" -+ name="<?= $block->escapeHtmlAttr($block->getFieldName()) ?>[<%- data.parentIndex %>][<%- data.index %>][option_id]" - value="<%- data.option_id %>"/> -- <input type="hidden" class="product" -- name="<?= /* @escapeNotVerified */ $block->getFieldName() ?>[<%- data.parentIndex %>][<%- data.index %>][product_id]" -+ <input type="hidden" -+ class="product" -+ name="<?= $block->escapeHtmlAttr($block->getFieldName()) ?>[<%- data.parentIndex %>][<%- data.index %>][product_id]" - value="<%- data.product_id %>"/> -- <input type="hidden" name="<?= /* @escapeNotVerified */ $block->getFieldName() ?>[<%- data.parentIndex %>][<%- data.index %>][delete]" -- value="" class="delete"/> -+ <input type="hidden" name="<?= $block->escapeHtmlAttr($block->getFieldName()) ?>[<%- data.parentIndex %>][<%- data.index %>][delete]" -+ value="" -+ class="delete"/> - </td> - <td class="col-default"> -- <input onclick="bSelection.checkGroup(event)" type="<%- data.option_type %>" class="default" -- name="<?= /* @escapeNotVerified */ $block->getFieldName() ?>[<%- data.parentIndex %>][<%- data.index %>][is_default]" -+ <input onclick="bSelection.checkGroup(event)" -+ type="<%- data.option_type %>" -+ class="default" -+ name="<?= $block->escapeHtmlAttr($block->getFieldName()) ?>[<%- data.parentIndex %>][<%- data.index %>][is_default]" - value="1" <%- data.checked %> /> - </td> - <td class="col-name"><%- data.name %></td> - <td class="col-sku"><%- data.sku %></td> --<?php if ($block->getCanReadPrice() !== false): ?> -+<?php if ($block->getCanReadPrice() !== false) : ?> - <td class="col-price price-type-box"> -- <input id="<?= /* @escapeNotVerified */ $block->getFieldId() ?>_<%- data.index %>_price_value" -- class="input-text required-entry validate-zero-or-greater" type="text" -- name="<?= /* @escapeNotVerified */ $block->getFieldName() ?>[<%- data.parentIndex %>][<%- data.index %>][selection_price_value]" -+ <input id="<?= $block->escapeHtmlAttr($block->getFieldId()) ?>_<%- data.index %>_price_value" -+ class="input-text required-entry validate-zero-or-greater" -+ type="text" -+ name="<?= $block->escapeHtmlAttr($block->getFieldName()) ?>[<%- data.parentIndex %>][<%- data.index %>][selection_price_value]" - value="<%- data.selection_price_value %>" -- <?php if ($block->getCanEditPrice() === false): ?> -+ <?php if ($block->getCanEditPrice() === false) : ?> - disabled="disabled" - <?php endif; ?>/> - </td> -@@ -65,19 +70,23 @@ - <?= $block->getPriceTypeSelectHtml() ?> - <div><?= $block->getCheckboxScopeHtml() ?></div> - </td> --<?php else: ?> -- <input type="hidden" id="<?= /* @escapeNotVerified */ $block->getFieldId() ?>_<%- data.index %>_price_value" -- name="<?= /* @escapeNotVerified */ $block->getFieldName() ?>[<%- data.parentIndex %>][<%- data.index %>][selection_price_value]" value="0" /> -- <input type="hidden" id="<?= /* @escapeNotVerified */ $block->getFieldId() ?>_<%- data.index %>_price_type" -- name="<?= /* @escapeNotVerified */ $block->getFieldName() ?>[<%- data.parentIndex %>][<%- data.index %>][selection_price_type]" value="0" /> -- <?php if ($block->isUsedWebsitePrice()): ?> -- <input type="hidden" id="<?= /* @escapeNotVerified */ $block->getFieldId() ?>_<%- data.index %>_price_scope" -- name="<?= /* @escapeNotVerified */ $block->getFieldName() ?>[<%- data.parentIndex %>][<%- data.index %>][default_price_scope]" value="1" /> -+<?php else : ?> -+ <input type="hidden" -+ id="<?= $block->escapeHtmlAttr($block->getFieldId()) ?>_<%- data.index %>_price_value" -+ name="<?= $block->escapeHtmlAttr($block->getFieldName()) ?>[<%- data.parentIndex %>][<%- data.index %>][selection_price_value]" value="0" /> -+ <input type="hidden" -+ id="<?= $block->escapeHtmlAttr($block->getFieldId()) ?>_<%- data.index %>_price_type" -+ name="<?= $block->escapeHtmlAttr($block->getFieldName()) ?>[<%- data.parentIndex %>][<%- data.index %>][selection_price_type]" value="0" /> -+ <?php if ($block->isUsedWebsitePrice()) : ?> -+ <input type="hidden" -+ id="<?= $block->escapeHtmlAttr($block->getFieldId()) ?>_<%- data.index %>_price_scope" -+ name="<?= $block->escapeHtmlAttr($block->getFieldName()) ?>[<%- data.parentIndex %>][<%- data.index %>][default_price_scope]" value="1" /> - <?php endif; ?> - <?php endif; ?> - <td class="col-qty"> -- <input class="input-text required-entry validate-greater-zero-based-on-option validate-zero-or-greater" type="text" -- name="<?= /* @escapeNotVerified */ $block->getFieldName() ?>[<%- data.parentIndex %>][<%- data.index %>][selection_qty]" -+ <input class="input-text required-entry validate-greater-zero-based-on-option validate-zero-or-greater" -+ type="text" -+ name="<?= $block->escapeHtmlAttr($block->getFieldName()) ?>[<%- data.parentIndex %>][<%- data.index %>][selection_qty]" - value="<%- data.selection_qty %>" /> - </td> - <td class="col-uqty qty-box"> -@@ -85,8 +94,9 @@ - <span style="display:none"><?= $block->getQtyTypeSelectHtml() ?></span> - </td> - <td class="col-order type-order" style="display:none"> -- <input class="input-text required-entry validate-zero-or-greater" type="text" -- name="<?= /* @escapeNotVerified */ $block->getFieldName() ?>[<%- data.parentIndex %>][<%- data.index %>][position]" -+ <input class="input-text required-entry validate-zero-or-greater" -+ type="text" -+ name="<?= $block->escapeHtmlAttr($block->getFieldName()) ?>[<%- data.parentIndex %>][<%- data.index %>][position]" - value="<%- data.position %>" /> - </td> - <td class="col-actions"> -@@ -106,7 +116,7 @@ var bundleTemplateBox = jQuery('#bundle-option-selection-box-template').html(), - - Bundle.Selection = Class.create(); - Bundle.Selection.prototype = { -- idLabel : '<?= /* @escapeNotVerified */ $block->getFieldId() ?>', -+ idLabel : '<?= $block->escapeJs($block->getFieldId()) ?>', - scopePrice : <?= (int)$block->isUsedWebsitePrice() ?>, - templateBox : '', - templateRow : '', -@@ -115,7 +125,7 @@ Bundle.Selection.prototype = { - gridSelection: new Hash(), - gridRemoval: new Hash(), - gridSelectedProductSkus: [], -- selectionSearchUrl: '<?= /* @escapeNotVerified */ $block->getSelectionSearchUrl() ?>', -+ selectionSearchUrl: '<?= $block->escapeUrl($block->getSelectionSearchUrl()) ?>', - - initialize : function() { - this.templateBox = '<div class="tier form-list" id="' + this.idLabel + '_box_<%- data.parentIndex %>">' + bundleTemplateBox + '</div>'; -diff --git a/app/code/Magento/Bundle/view/adminhtml/templates/sales/creditmemo/create/items/renderer.phtml b/app/code/Magento/Bundle/view/adminhtml/templates/sales/creditmemo/create/items/renderer.phtml -index 3aba02fadff..c480d9b126d 100644 ---- a/app/code/Magento/Bundle/view/adminhtml/templates/sales/creditmemo/create/items/renderer.phtml -+++ b/app/code/Magento/Bundle/view/adminhtml/templates/sales/creditmemo/create/items/renderer.phtml -@@ -3,9 +3,7 @@ - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -- --// @codingStandardsIgnoreFile -- -+// phpcs:disable Magento2.Templates.ThisInTemplate.FoundThis - ?> - <?php - /** -@@ -21,19 +19,19 @@ - - <?php $_prevOptionId = '' ?> - --<?php if ($block->getOrderOptions() || $_item->getDescription()): ?> -+<?php if ($block->getOrderOptions() || $_item->getDescription()) : ?> - <?php $_showlastRow = true ?> --<?php else: ?> -+<?php else : ?> - <?php $_showlastRow = false ?> - <?php endif; ?> - --<?php foreach ($items as $_item): ?> -+<?php foreach ($items as $_item) : ?> - <?php $block->setPriceDataObject($_item) ?> - <?php $attributes = $block->getSelectionAttributes($_item) ?> -- <?php if ($_item->getOrderItem()->getParentItem()): ?> -- <?php if ($_prevOptionId != $attributes['option_id']): ?> -+ <?php if ($_item->getOrderItem()->getParentItem()) : ?> -+ <?php if ($_prevOptionId != $attributes['option_id']) : ?> - <tr> -- <td class="col-product"><div class="option-label"><?= /* @escapeNotVerified */ $attributes['option_label'] ?></div></td> -+ <td class="col-product"><div class="option-label"><?= $block->escapeHtml($attributes['option_label']) ?></div></td> - <td> </td> - <td> </td> - <td> </td> -@@ -43,165 +41,164 @@ - <td> </td> - <td class="last"> </td> - </tr> -- <?php $_prevOptionId = $attributes['option_id'] ?> -+ <?php $_prevOptionId = $attributes['option_id'] ?> - <?php endif; ?> - <?php endif; ?> - <tr<?= (++$_index == $_count && !$_showlastRow) ? ' class="border"' : '' ?>> -- <?php if (!$_item->getOrderItem()->getParentItem()): ?> -+ <?php if (!$_item->getOrderItem()->getParentItem()) : ?> - <td class="col-product"> - <div class="product-title"><?= $block->escapeHtml($_item->getName()) ?></div> - <div class="product-sku-block"> -- <span><?= /* @escapeNotVerified */ __('SKU') ?>:</span> -- <?= implode('<br />', $this->helper('Magento\Catalog\Helper\Data')->splitSku($block->escapeHtml($_item->getSku()))) ?> -+ <span><?= $block->escapeHtml(__('SKU')) ?>:</span> -+ <?= /* @noEscape */ implode('<br />', $this->helper(Magento\Catalog\Helper\Data::class)->splitSku($_item->getSku())) ?> - </div> - </td> -- <?php else: ?> -+ <?php else : ?> - <td class="col-product"><div class="option-value"><?= $block->getValueHtml($_item) ?></div></td> - <?php endif; ?> - <td class="col-price"> -- <?php if ($block->canShowPriceInfo($_item)): ?> -+ <?php if ($block->canShowPriceInfo($_item)) : ?> - <?= $block->getColumnHtml($_item, 'price') ?> -- <?php else: ?> -+ <?php else : ?> -   - <?php endif; ?> - </td> - <td class="col-ordered-qty"> -- <?php if ($block->canShowPriceInfo($_item)): ?> -+ <?php if ($block->canShowPriceInfo($_item)) : ?> - <table class="qty-table"> - <tr> -- <th><?= /* @escapeNotVerified */ __('Ordered') ?></th> -- <td><?= /* @escapeNotVerified */ $_item->getOrderItem()->getQtyOrdered()*1 ?></td> -+ <th><?= $block->escapeHtml(__('Ordered')) ?></th> -+ <td><?= (float)$_item->getOrderItem()->getQtyOrdered() * 1 ?></td> - </tr> -- <?php if ((float) $_item->getOrderItem()->getQtyInvoiced()): ?> -+ <?php if ((float) $_item->getOrderItem()->getQtyInvoiced()) : ?> - <tr> -- <th><?= /* @escapeNotVerified */ __('Invoiced') ?></th> -- <td><?= /* @escapeNotVerified */ $_item->getOrderItem()->getQtyInvoiced()*1 ?></td> -+ <th><?= $block->escapeHtml(__('Invoiced')) ?></th> -+ <td><?= (float)$_item->getOrderItem()->getQtyInvoiced() * 1 ?></td> - </tr> - <?php endif; ?> -- <?php if ((float) $_item->getOrderItem()->getQtyShipped() && $block->isShipmentSeparately($_item)): ?> -+ <?php if ((float) $_item->getOrderItem()->getQtyShipped() && $block->isShipmentSeparately($_item)) : ?> - <tr> -- <th><?= /* @escapeNotVerified */ __('Shipped') ?></th> -- <td><?= /* @escapeNotVerified */ $_item->getOrderItem()->getQtyShipped()*1 ?></td> -+ <th><?= $block->escapeHtml(__('Shipped')) ?></th> -+ <td><?= (float)$_item->getOrderItem()->getQtyShipped() * 1 ?></td> - </tr> - <?php endif; ?> -- <?php if ((float) $_item->getOrderItem()->getQtyRefunded()): ?> -+ <?php if ((float) $_item->getOrderItem()->getQtyRefunded()) : ?> - <tr> -- <th><?= /* @escapeNotVerified */ __('Refunded') ?></th> -- <td><?= /* @escapeNotVerified */ $_item->getOrderItem()->getQtyRefunded()*1 ?></td> -+ <th><?= $block->escapeHtml(__('Refunded')) ?></th> -+ <td><?= (float)$_item->getOrderItem()->getQtyRefunded() * 1 ?></td> - </tr> - <?php endif; ?> -- <?php if ((float) $_item->getOrderItem()->getQtyCanceled()): ?> -+ <?php if ((float) $_item->getOrderItem()->getQtyCanceled()) : ?> - <tr> -- <th><?= /* @escapeNotVerified */ __('Canceled') ?></th> -- <td><?= /* @escapeNotVerified */ $_item->getOrderItem()->getQtyCanceled()*1 ?></td> -+ <th><?= $block->escapeHtml(__('Canceled')) ?></th> -+ <td><?= (float)$_item->getOrderItem()->getQtyCanceled() * 1 ?></td> - </tr> - <?php endif; ?> - </table> -- <?php elseif ($block->isShipmentSeparately($_item)): ?> -+ <?php elseif ($block->isShipmentSeparately($_item)) : ?> - <table class="qty-table"> - <tr> -- <th><?= /* @escapeNotVerified */ __('Ordered') ?></th> -- <td><?= /* @escapeNotVerified */ $_item->getOrderItem()->getQtyOrdered()*1 ?></td> -+ <th><?= $block->escapeHtml(__('Ordered')) ?></th> -+ <td><?= (float)$_item->getOrderItem()->getQtyOrdered() * 1 ?></td> - </tr> -- <?php if ((float) $_item->getOrderItem()->getQtyShipped()): ?> -+ <?php if ((float) $_item->getOrderItem()->getQtyShipped()) : ?> - <tr> -- <th><?= /* @escapeNotVerified */ __('Shipped') ?></th> -- <td><?= /* @escapeNotVerified */ $_item->getOrderItem()->getQtyShipped()*1 ?></td> -+ <th><?= $block->escapeHtml(__('Shipped')) ?></th> -+ <td><?= (float)$_item->getOrderItem()->getQtyShipped() * 1 ?></td> - </tr> - <?php endif; ?> - </table> -- <?php else: ?> -+ <?php else : ?> -   - <?php endif; ?> - </td> - <?php if ($block->canParentReturnToStock($_item)) : ?> - <td class="col-return-to-stock"> -- <?php if ($block->canShowPriceInfo($_item)): ?> -+ <?php if ($block->canShowPriceInfo($_item)) : ?> - <?php if ($block->canReturnItemToStock($_item)) : ?> - <input type="checkbox" - class="admin__control-checkbox" -- name="creditmemo[items][<?= /* @escapeNotVerified */ $_item->getOrderItemId() ?>][back_to_stock]" -- value="1"<?php if ($_item->getBackToStock()):?> checked="checked"<?php endif;?> /> -+ name="creditmemo[items][<?= $block->escapeHtmlAttr($_item->getOrderItemId()) ?>][back_to_stock]" -+ value="1"<?php if ($_item->getBackToStock()) :?> checked="checked"<?php endif;?> /> - <label class="admin__field-label"></label> - <?php endif; ?> -- <?php else: ?> -+ <?php else : ?> -   - <?php endif; ?> - </td> - <?php endif; ?> - <td class="col-refund col-qty"> -- <?php if ($block->canShowPriceInfo($_item)): ?> -+ <?php if ($block->canShowPriceInfo($_item)) : ?> - <?php if ($block->canEditQty()) : ?> - <input type="text" - class="input-text admin__control-text qty-input" -- name="creditmemo[items][<?= /* @escapeNotVerified */ $_item->getOrderItemId() ?>][qty]" -- value="<?= /* @escapeNotVerified */ $_item->getQty()*1 ?>" /> -- <?php else: ?> -- <?= /* @escapeNotVerified */ $_item->getQty()*1 ?> -+ name="creditmemo[items][<?= $block->escapeHtmlAttr($_item->getOrderItemId()) ?>][qty]" -+ value="<?= (float)$_item->getQty() * 1 ?>" /> -+ <?php else : ?> -+ <?= (float)$_item->getQty() * 1 ?> - <?php endif; ?> -- <?php else: ?> -+ <?php else : ?> -   - <?php endif; ?> - </td> - <td class="col-subtotal"> -- <?php if ($block->canShowPriceInfo($_item)): ?> -+ <?php if ($block->canShowPriceInfo($_item)) : ?> - <?= $block->getColumnHtml($_item, 'subtotal') ?> -- <?php else: ?> -+ <?php else : ?> -   - <?php endif; ?> - </td> - <td class="col-tax-amount"> -- <?php if ($block->canShowPriceInfo($_item)): ?> -- <?= /* @escapeNotVerified */ $block->displayPriceAttribute('tax_amount') ?> -- <?php else: ?> -+ <?php if ($block->canShowPriceInfo($_item)) : ?> -+ <?= /* @noEscape */ $block->displayPriceAttribute('tax_amount') ?> -+ <?php else : ?> -   - <?php endif; ?> - </td> - <td class="col-discont"> -- <?php if ($block->canShowPriceInfo($_item)): ?> -- <?= /* @escapeNotVerified */ $block->displayPriceAttribute('discount_amount') ?> -- <?php else: ?> -+ <?php if ($block->canShowPriceInfo($_item)) : ?> -+ <?= /* @noEscape */ $block->displayPriceAttribute('discount_amount') ?> -+ <?php else : ?> -   - <?php endif; ?> - </td> - <td class="col-total last"> -- <?php if ($block->canShowPriceInfo($_item)): ?> -+ <?php if ($block->canShowPriceInfo($_item)) : ?> - <?= $block->getColumnHtml($_item, 'total') ?> -- <?php else: ?> -+ <?php else : ?> -   - <?php endif; ?> - </td> - </tr> - <?php endforeach; ?> --<?php if ($_showlastRow): ?> -+<?php if ($_showlastRow) : ?> - <tr class="border"> - <td class="col-product"> -- <?php if ($block->getOrderOptions($_item->getOrderItem())): ?> -+ <?php if ($block->getOrderOptions($_item->getOrderItem())) : ?> - <dl class="item-options"> -- <?php foreach ($block->getOrderOptions($_item->getOrderItem()) as $option): ?> -- <dt><?= /* @escapeNotVerified */ $option['label'] ?></dt> -+ <?php foreach ($block->getOrderOptions($_item->getOrderItem()) as $option) : ?> -+ <dt><?= $block->escapeHtml($option['label']) ?></dt> - <dd> -- <?php if (isset($option['custom_view']) && $option['custom_view']): ?> -- <?= /* @escapeNotVerified */ $option['value'] ?> -- <?php else: ?> -- <?= $block->truncateString($option['value'], 55, '', $_remainder) ?> -- <?php if ($_remainder):?> -- ... <span id="<?= /* @escapeNotVerified */ $_id = 'id' . uniqid() ?>"><?= /* @escapeNotVerified */ $_remainder ?></span> -+ <?php if (isset($option['custom_view']) && $option['custom_view']) : ?> -+ <?= $block->escapeHtml($option['value']) ?> -+ <?php else : ?> -+ <?= $block->escapeHtml($block->truncateString($option['value'], 55, '', $_remainder)) ?> -+ <?php if ($_remainder) :?> -+ ... <span id="<?= $block->escapeHtmlAttr($_id = 'id' . uniqid()) ?>"><?= $block->escapeHtml($_remainder) ?></span> - <script> --require(['prototype'], function(){ -- -- $('<?= /* @escapeNotVerified */ $_id ?>').hide(); -- $('<?= /* @escapeNotVerified */ $_id ?>').up().observe('mouseover', function(){$('<?= /* @escapeNotVerified */ $_id ?>').show();}); -- $('<?= /* @escapeNotVerified */ $_id ?>').up().observe('mouseout', function(){$('<?= /* @escapeNotVerified */ $_id ?>').hide();}); -- --}); --</script> -+ require(['prototype'], function(){ -+ <?php $escapedId = $block->escapeJs($_id) ?> -+ $('<?= /* @noEscape */ $escapedId ?>').hide(); -+ $('<?= /* @noEscape */ $escapedId ?>').up().observe('mouseover', function(){$('<?= /* @noEscape */ $escapedId ?>').show();}); -+ $('<?= /* @noEscape */ $escapedId ?>').up().observe('mouseout', function(){$('<?= /* @noEscape */ $escapedId ?>').hide();}); -+ }); -+ </script> - <?php endif;?> - <?php endif;?> - </dd> - <?php endforeach; ?> - </dl> -- <?php else: ?> -+ <?php else : ?> -   - <?php endif; ?> - <?= $block->escapeHtml($_item->getDescription()) ?> -diff --git a/app/code/Magento/Bundle/view/adminhtml/templates/sales/creditmemo/view/items/renderer.phtml b/app/code/Magento/Bundle/view/adminhtml/templates/sales/creditmemo/view/items/renderer.phtml -index a9e71a79f89..0d54e1528df 100644 ---- a/app/code/Magento/Bundle/view/adminhtml/templates/sales/creditmemo/view/items/renderer.phtml -+++ b/app/code/Magento/Bundle/view/adminhtml/templates/sales/creditmemo/view/items/renderer.phtml -@@ -3,9 +3,7 @@ - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -- --// @codingStandardsIgnoreFile -- -+// phpcs:disable Magento2.Templates.ThisInTemplate.FoundThis - ?> - <?php - /** -@@ -21,19 +19,19 @@ - - <?php $_prevOptionId = '' ?> - --<?php if ($block->getOrderOptions() || $_item->getDescription()): ?> -+<?php if ($block->getOrderOptions() || $_item->getDescription()) : ?> - <?php $_showlastRow = true ?> --<?php else: ?> -+<?php else : ?> - <?php $_showlastRow = false ?> - <?php endif; ?> - --<?php foreach ($items as $_item): ?> -+<?php foreach ($items as $_item) : ?> - <?php $block->setPriceDataObject($_item) ?> - <?php $attributes = $block->getSelectionAttributes($_item) ?> -- <?php if ($_item->getOrderItem()->getParentItem()): ?> -- <?php if ($_prevOptionId != $attributes['option_id']): ?> -+ <?php if ($_item->getOrderItem()->getParentItem()) : ?> -+ <?php if ($_prevOptionId != $attributes['option_id']) : ?> - <tr> -- <td class="col-product"><div class="option-label"><?= /* @escapeNotVerified */ $attributes['option_label'] ?></div></td> -+ <td class="col-product"><div class="option-label"><?= $block->escapeHtml($attributes['option_label']) ?></div></td> - <td> </td> - <td> </td> - <td> </td> -@@ -41,88 +39,87 @@ - <td> </td> - <td class="last"> </td> - </tr> -- <?php $_prevOptionId = $attributes['option_id'] ?> -+ <?php $_prevOptionId = $attributes['option_id'] ?> - <?php endif; ?> - <?php endif; ?> - <tr<?= (++$_index == $_count && !$_showlastRow) ? ' class="border"' : '' ?>> -- <?php if (!$_item->getOrderItem()->getParentItem()): ?> -+ <?php if (!$_item->getOrderItem()->getParentItem()) : ?> - <td class="col-product"> - <div class="product-title"><?= $block->escapeHtml($_item->getName()) ?></div> - <div class="product-sku-block"> -- <span><?= /* @escapeNotVerified */ __('SKU') ?>:</span> -- <?= implode('<br />', $this->helper('Magento\Catalog\Helper\Data')->splitSku($block->escapeHtml($_item->getSku()))) ?> -+ <span><?= $block->escapeHtml(__('SKU')) ?>:</span> -+ <?= /* @noEscape */ implode('<br />', $this->helper(Magento\Catalog\Helper\Data::class)->splitSku($_item->getSku())) ?> - </div> - </td> -- <?php else: ?> -+ <?php else : ?> - <td class="col-product"><div class="option-value"><?= $block->getValueHtml($_item) ?></div></td> - <?php endif; ?> - <td class="col-price"> -- <?php if ($block->canShowPriceInfo($_item)): ?> -+ <?php if ($block->canShowPriceInfo($_item)) : ?> - <?= $block->getColumnHtml($_item, 'price') ?> -- <?php else: ?> -+ <?php else : ?> -   - <?php endif; ?> - </td> - <td class="col-qty"> -- <?php if ($block->canShowPriceInfo($_item)): ?> -- <?= /* @escapeNotVerified */ $_item->getQty()*1 ?> -- <?php else: ?> -+ <?php if ($block->canShowPriceInfo($_item)) : ?> -+ <?= (float)$_item->getQty() * 1 ?> -+ <?php else : ?> -   - <?php endif; ?> - </td> - <td class="col-subtotal"> -- <?php if ($block->canShowPriceInfo($_item)): ?> -+ <?php if ($block->canShowPriceInfo($_item)) : ?> - <?= $block->getColumnHtml($_item, 'subtotal') ?> -- <?php else: ?> -+ <?php else : ?> -   - <?php endif; ?> - </td> - <td class="col-tax"> -- <?php if ($block->canShowPriceInfo($_item)): ?> -- <?= /* @escapeNotVerified */ $block->displayPriceAttribute('tax_amount') ?> -- <?php else: ?> -+ <?php if ($block->canShowPriceInfo($_item)) : ?> -+ <?= /* @noEscape */ $block->displayPriceAttribute('tax_amount') ?> -+ <?php else : ?> -   - <?php endif; ?> - </td> - <td class="col-discount"> -- <?php if ($block->canShowPriceInfo($_item)): ?> -- <?= /* @escapeNotVerified */ $block->displayPriceAttribute('discount_amount') ?> -- <?php else: ?> -+ <?php if ($block->canShowPriceInfo($_item)) : ?> -+ <?= /* @noEscape */ $block->displayPriceAttribute('discount_amount') ?> -+ <?php else : ?> -   - <?php endif; ?> - </td> - <td class="col-total last"> -- <?php if ($block->canShowPriceInfo($_item)): ?> -+ <?php if ($block->canShowPriceInfo($_item)) : ?> - <?= $block->getColumnHtml($_item, 'total') ?> -- <?php else: ?> -+ <?php else : ?> -   - <?php endif; ?> - </td> - </tr> - <?php endforeach; ?> --<?php if ($_showlastRow): ?> -+<?php if ($_showlastRow) : ?> - <tr class="border"> - <td class="col-product"> -- <?php if ($block->getOrderOptions()): ?> -+ <?php if ($block->getOrderOptions()) : ?> - <dl class="item-options"> -- <?php foreach ($block->getOrderOptions() as $option): ?> -- <dt><?= /* @escapeNotVerified */ $option['label'] ?></dt> -+ <?php foreach ($block->getOrderOptions() as $option) : ?> -+ <dt><?= $block->escapeHtml($option['label']) ?></dt> - <dd> -- <?php if (isset($option['custom_view']) && $option['custom_view']): ?> -- <?= /* @escapeNotVerified */ $option['value'] ?> -- <?php else: ?> -- <?= $block->truncateString($option['value'], 55, '', $_remainder) ?> -- <?php if ($_remainder):?> -- ... <span id="<?= /* @escapeNotVerified */ $_id = 'id' . uniqid() ?>"><?= /* @escapeNotVerified */ $_remainder ?></span> -+ <?php if (isset($option['custom_view']) && $option['custom_view']) : ?> -+ <?= $block->escapeHtml($option['value']) ?> -+ <?php else : ?> -+ <?= $block->escapeHtml($block->truncateString($option['value'], 55, '', $_remainder)) ?> -+ <?php if ($_remainder) :?> -+ ... <span id="<?= $block->escapeHtmlAttr($_id = 'id' . uniqid()) ?>"><?= $block->escapeHtml($_remainder) ?></span> - <script> --require(['prototype'], function(){ -- -- $('<?= /* @escapeNotVerified */ $_id ?>').hide(); -- $('<?= /* @escapeNotVerified */ $_id ?>').up().observe('mouseover', function(){$('<?= /* @escapeNotVerified */ $_id ?>').show();}); -- $('<?= /* @escapeNotVerified */ $_id ?>').up().observe('mouseout', function(){$('<?= /* @escapeNotVerified */ $_id ?>').hide();}); -- --}); --</script> -+ require(['prototype'], function(){ -+ <?php $escapedId = $block->escapeJs($_id) ?> -+ $('<?= /* @noEscape */ $escapedId ?>').hide(); -+ $('<?= /* @noEscape */ $escapedId ?>').up().observe('mouseover', function(){$('<?= /* @noEscape */ $escapedId ?>').show();}); -+ $('<?= /* @noEscape */ $escapedId ?>').up().observe('mouseout', function(){$('<?= /* @noEscape */ $escapedId ?>').hide();}); -+ }); -+ </script> - <?php endif;?> - <?php endif;?> - </dd> -diff --git a/app/code/Magento/Bundle/view/adminhtml/templates/sales/invoice/create/items/renderer.phtml b/app/code/Magento/Bundle/view/adminhtml/templates/sales/invoice/create/items/renderer.phtml -index ff26d67bd83..a7d49b4b353 100644 ---- a/app/code/Magento/Bundle/view/adminhtml/templates/sales/invoice/create/items/renderer.phtml -+++ b/app/code/Magento/Bundle/view/adminhtml/templates/sales/invoice/create/items/renderer.phtml -@@ -3,9 +3,7 @@ - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -- --// @codingStandardsIgnoreFile -- -+// phpcs:disable Magento2.Templates.ThisInTemplate.FoundThis - ?> - <?php - /** -@@ -21,19 +19,28 @@ - - <?php $_prevOptionId = '' ?> - --<?php if ($block->getOrderOptions() || $_item->getDescription()): ?> -+<?php if ($block->getOrderOptions() || $_item->getDescription()) : ?> - <?php $_showlastRow = true ?> --<?php else: ?> -+<?php else : ?> - <?php $_showlastRow = false ?> - <?php endif; ?> - --<?php foreach ($items as $_item): ?> -+<?php foreach ($items as $_item) : ?> -+ <?php -+ $shipTogether = ($_item->getOrderItem()->getProductType() == \Magento\Catalog\Model\Product\Type::TYPE_BUNDLE) ? -+ !$_item->getOrderItem()->isShipSeparately() : !$_item->getOrderItem()->getParentItem()->isShipSeparately() -+ ?> - <?php $block->setPriceDataObject($_item) ?> -- <?php if ($_item->getOrderItem()->getParentItem()): ?> -+ <?php if ($_item->getOrderItem()->getParentItem()) : ?> -+ <?php -+ if ($shipTogether) { -+ continue; -+ } -+ ?> - <?php $attributes = $block->getSelectionAttributes($_item) ?> -- <?php if ($_prevOptionId != $attributes['option_id']): ?> -+ <?php if ($_prevOptionId != $attributes['option_id']) : ?> - <tr> -- <td class="col-product"><div class="option-label"><?= /* @escapeNotVerified */ $attributes['option_label'] ?></div></td> -+ <td class="col-product"><div class="option-label"><?= $block->escapeHtml($attributes['option_label']) ?></div></td> - <td> </td> - <td> </td> - <td> </td> -@@ -42,152 +49,152 @@ - <td> </td> - <td class="last"> </td> - </tr> -- <?php $_prevOptionId = $attributes['option_id'] ?> -+ <?php $_prevOptionId = $attributes['option_id'] ?> - <?php endif; ?> - <?php endif; ?> - <tr<?= (++$_index == $_count && !$_showlastRow) ? ' class="border"' : '' ?>> -- <?php if (!$_item->getOrderItem()->getParentItem()): ?> -+ <?php if (!$_item->getOrderItem()->getParentItem()) : ?> - <td class="col-product"> - <div class="product-title"><?= $block->escapeHtml($_item->getName()) ?></div> - <div class="product-sku-block"> -- <span><?= /* @escapeNotVerified */ __('SKU') ?>:</span> -- <?= implode('<br />', $this->helper('Magento\Catalog\Helper\Data')->splitSku($block->escapeHtml($_item->getSku()))) ?> -+ <span><?= $block->escapeHtml(__('SKU')) ?>:</span> -+ <?= /* @noEscape */ implode('<br />', $this->helper(Magento\Catalog\Helper\Data::class)->splitSku($_item->getSku())) ?> - </div> - </td> -- <?php else: ?> -+ <?php else : ?> - <td class="col-product"> - <div class="option-value"><?= $block->getValueHtml($_item) ?></div> - </td> - <?php endif; ?> - <td class="col-price"> -- <?php if ($block->canShowPriceInfo($_item)): ?> -+ <?php if ($block->canShowPriceInfo($_item) || $shipTogether) : ?> - <?= $block->getColumnHtml($_item, 'price') ?> -- <?php else: ?> -+ <?php else : ?> -   - <?php endif; ?> - </td> - <td class="col-qty"> -- <?php if ($block->canShowPriceInfo($_item)): ?> -+ <?php if ($block->canShowPriceInfo($_item) || $shipTogether) : ?> - <table class="qty-table"> - <tr> -- <th><?= /* @escapeNotVerified */ __('Ordered') ?></th> -- <td><span><?= /* @escapeNotVerified */ $_item->getOrderItem()->getQtyOrdered()*1 ?></span></td> -+ <th><?= $block->escapeHtml(__('Ordered')) ?></th> -+ <td><span><?= (float)$_item->getOrderItem()->getQtyOrdered() * 1 ?></span></td> - </tr> -- <?php if ((float) $_item->getOrderItem()->getQtyInvoiced()): ?> -+ <?php if ((float) $_item->getOrderItem()->getQtyInvoiced()) : ?> - <tr> -- <th><?= /* @escapeNotVerified */ __('Invoiced') ?></th> -- <td><?= /* @escapeNotVerified */ $_item->getOrderItem()->getQtyInvoiced()*1 ?></td> -+ <th><?= $block->escapeHtml(__('Invoiced')) ?></th> -+ <td><?= (float)$_item->getOrderItem()->getQtyInvoiced() * 1 ?></td> - </tr> - <?php endif; ?> -- <?php if ((float) $_item->getOrderItem()->getQtyShipped() && $block->isShipmentSeparately($_item)): ?> -+ <?php if ((float) $_item->getOrderItem()->getQtyShipped() && $block->isShipmentSeparately($_item)) : ?> - <tr> -- <th><?= /* @escapeNotVerified */ __('Shipped') ?></th> -- <td><?= /* @escapeNotVerified */ $_item->getOrderItem()->getQtyShipped()*1 ?></td> -+ <th><?= $block->escapeHtml(__('Shipped')) ?></th> -+ <td><?= (float)$_item->getOrderItem()->getQtyShipped() * 1 ?></td> - </tr> - <?php endif; ?> -- <?php if ((float) $_item->getOrderItem()->getQtyRefunded()): ?> -+ <?php if ((float) $_item->getOrderItem()->getQtyRefunded()) : ?> - <tr> -- <th><?= /* @escapeNotVerified */ __('Refunded') ?></th> -- <td><?= /* @escapeNotVerified */ $_item->getOrderItem()->getQtyRefunded()*1 ?></td> -+ <th><?= $block->escapeHtml(__('Refunded')) ?></th> -+ <td><?= (float)$_item->getOrderItem()->getQtyRefunded() * 1 ?></td> - </tr> - <?php endif; ?> -- <?php if ((float) $_item->getOrderItem()->getQtyCanceled()): ?> -+ <?php if ((float) $_item->getOrderItem()->getQtyCanceled()) : ?> - <tr> -- <th><?= /* @escapeNotVerified */ __('Canceled') ?></th> -- <td><?= /* @escapeNotVerified */ $_item->getOrderItem()->getQtyCanceled()*1 ?></td> -+ <th><?= $block->escapeHtml(__('Canceled')) ?></th> -+ <td><?= (float)$_item->getOrderItem()->getQtyCanceled() * 1 ?></td> - </tr> - <?php endif; ?> - </table> -- <?php elseif ($block->isShipmentSeparately($_item)): ?> -+ <?php elseif ($block->isShipmentSeparately($_item)) : ?> - <table class="qty-table"> - <tr> -- <th><?= /* @escapeNotVerified */ __('Ordered') ?></th> -- <td><?= /* @escapeNotVerified */ $_item->getOrderItem()->getQtyOrdered()*1 ?></td> -+ <th><?= $block->escapeHtml(__('Ordered')) ?></th> -+ <td><?= (float)$_item->getOrderItem()->getQtyOrdered() * 1 ?></td> - </tr> -- <?php if ((float) $_item->getOrderItem()->getQtyShipped()): ?> -+ <?php if ((float) $_item->getOrderItem()->getQtyShipped()) : ?> - <tr> -- <th><?= /* @escapeNotVerified */ __('Shipped') ?></th> -- <td><?= /* @escapeNotVerified */ $_item->getOrderItem()->getQtyShipped()*1 ?></td> -+ <th><?= $block->escapeHtml(__('Shipped')) ?></th> -+ <td><?= (float)$_item->getOrderItem()->getQtyShipped() * 1 ?></td> - </tr> - <?php endif; ?> - </table> -- <?php else: ?> -+ <?php else : ?> -   - <?php endif; ?> - </td> - <td class="col-qty-invoice"> -- <?php if ($block->canShowPriceInfo($_item)): ?> -+ <?php if ($block->canShowPriceInfo($_item) || $shipTogether) : ?> - <?php if ($block->canEditQty()) : ?> - <input type="text" - class="input-text admin__control-text qty-input" -- name="invoice[items][<?= /* @escapeNotVerified */ $_item->getOrderItemId() ?>]" -- value="<?= /* @escapeNotVerified */ $_item->getQty()*1 ?>" /> -+ name="invoice[items][<?= $block->escapeHtmlAttr($_item->getOrderItemId()) ?>]" -+ value="<?= (float)$_item->getQty() * 1 ?>" /> - <?php else : ?> -- <?= /* @escapeNotVerified */ $_item->getQty()*1 ?> -+ <?= (float)$_item->getQty() * 1 ?> - <?php endif; ?> -- <?php else: ?> -+ <?php else : ?> -   - <?php endif; ?> - </td> - <td class="col-subtotal"> -- <?php if ($block->canShowPriceInfo($_item)): ?> -+ <?php if ($block->canShowPriceInfo($_item)) : ?> - <?= $block->getColumnHtml($_item, 'subtotal') ?> -- <?php else: ?> -+ <?php else : ?> -   - <?php endif; ?> - </td> - <td class="col-tax"> -- <?php if ($block->canShowPriceInfo($_item)): ?> -- <?= /* @escapeNotVerified */ $block->displayPriceAttribute('tax_amount') ?> -- <?php else: ?> -+ <?php if ($block->canShowPriceInfo($_item)) : ?> -+ <?= /* @noEscape */ $block->displayPriceAttribute('tax_amount') ?> -+ <?php else : ?> -   - <?php endif; ?> - </td> - <td class="col-discount"> -- <?php if ($block->canShowPriceInfo($_item)): ?> -- <?= /* @escapeNotVerified */ $block->displayPriceAttribute('discount_amount') ?> -- <?php else: ?> -+ <?php if ($block->canShowPriceInfo($_item)) : ?> -+ <?= /* @noEscape */ $block->displayPriceAttribute('discount_amount') ?> -+ <?php else : ?> -   - <?php endif; ?> - </td> - <td class="col-total last"> -- <?php if ($block->canShowPriceInfo($_item)): ?> -+ <?php if ($block->canShowPriceInfo($_item)) : ?> - <?= $block->getColumnHtml($_item, 'total') ?> -- <?php else: ?> -+ <?php else : ?> -   - <?php endif; ?> - </td> - </tr> - <?php endforeach; ?> --<?php if ($_showlastRow): ?> -+<?php if ($_showlastRow) : ?> - <tr class="border"> - <td class="col-product"> -- <?php if ($block->getOrderOptions($_item->getOrderItem())): ?> -+ <?php if ($block->getOrderOptions($_item->getOrderItem())) : ?> - <dl class="item-options"> -- <?php foreach ($block->getOrderOptions($_item->getOrderItem()) as $option): ?> -- <dt><?= /* @escapeNotVerified */ $option['label'] ?></dt> -+ <?php foreach ($block->getOrderOptions($_item->getOrderItem()) as $option) : ?> -+ <dt><?= $block->escapeHtml($option['label']) ?></dt> - <dd> -- <?php if (isset($option['custom_view']) && $option['custom_view']): ?> -- <?= /* @escapeNotVerified */ $option['value'] ?> -- <?php else: ?> -- <?= $block->truncateString($option['value'], 55, '', $_remainder) ?> -- <?php if ($_remainder):?> -- ... <span id="<?= /* @escapeNotVerified */ $_id = 'id' . uniqid() ?>"><?= /* @escapeNotVerified */ $_remainder ?></span> -+ <?php if (isset($option['custom_view']) && $option['custom_view']) : ?> -+ <?= $block->escapeHtml($option['value']) ?> -+ <?php else : ?> -+ <?= $block->escapeHtml($block->truncateString($option['value'], 55, '', $_remainder)) ?> -+ <?php if ($_remainder) :?> -+ ... <span id="<?= $block->escapeHtmlAttr($_id = 'id' . uniqid()) ?>"><?= $block->escapeHtml($_remainder) ?></span> - <script> --require(['prototype'], function(){ -- -- $('<?= /* @escapeNotVerified */ $_id ?>').hide(); -- $('<?= /* @escapeNotVerified */ $_id ?>').up().observe('mouseover', function(){$('<?= /* @escapeNotVerified */ $_id ?>').show();}); -- $('<?= /* @escapeNotVerified */ $_id ?>').up().observe('mouseout', function(){$('<?= /* @escapeNotVerified */ $_id ?>').hide();}); -+ require(['prototype'], function(){ -+ <?php $escapedId = $block->escapeJs($_id) ?> -+ $('<?= /* @noEscape */ $escapedId ?>').hide(); -+ $('<?= /* @noEscape */ $escapedId ?>').up().observe('mouseover', function(){$('<?= /* @noEscape */ $escapedId?>').show();}); -+ $('<?= /* @noEscape */ $escapedId ?>').up().observe('mouseout', function(){$('<?= /* @noEscape */ $escapedId ?>').hide();}); - --}); --</script> -+ }); -+ </script> - <?php endif;?> - <?php endif;?> - </dd> - <?php endforeach; ?> - </dl> -- <?php else: ?> -+ <?php else : ?> -   - <?php endif; ?> - <?= $block->escapeHtml($_item->getDescription()) ?> -diff --git a/app/code/Magento/Bundle/view/adminhtml/templates/sales/invoice/view/items/renderer.phtml b/app/code/Magento/Bundle/view/adminhtml/templates/sales/invoice/view/items/renderer.phtml -index 5f344409b6a..e29bb5dbc94 100644 ---- a/app/code/Magento/Bundle/view/adminhtml/templates/sales/invoice/view/items/renderer.phtml -+++ b/app/code/Magento/Bundle/view/adminhtml/templates/sales/invoice/view/items/renderer.phtml -@@ -3,9 +3,7 @@ - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -- --// @codingStandardsIgnoreFile -- -+// phpcs:disable Magento2.Templates.ThisInTemplate.FoundThis - ?> - <?php - /** -@@ -21,19 +19,19 @@ - - <?php $_prevOptionId = '' ?> - --<?php if ($block->getOrderOptions() || $_item->getDescription()): ?> -+<?php if ($block->getOrderOptions() || $_item->getDescription()) : ?> - <?php $_showlastRow = true ?> --<?php else: ?> -+<?php else : ?> - <?php $_showlastRow = false ?> - <?php endif; ?> - --<?php foreach ($items as $_item): ?> -+<?php foreach ($items as $_item) : ?> - <?php $block->setPriceDataObject($_item) ?> -- <?php if ($_item->getOrderItem()->getParentItem()): ?> -+ <?php if ($_item->getOrderItem()->getParentItem()) : ?> - <?php $attributes = $block->getSelectionAttributes($_item) ?> -- <?php if ($_prevOptionId != $attributes['option_id']): ?> -+ <?php if ($_prevOptionId != $attributes['option_id']) : ?> - <tr> -- <td class="col-product"><div class="option-label"><?= /* @escapeNotVerified */ $attributes['option_label'] ?></div></td> -+ <td class="col-product"><div class="option-label"><?= $block->escapeHtml($attributes['option_label']) ?></div></td> - <td> </td> - <td> </td> - <td> </td> -@@ -41,89 +39,88 @@ - <td> </td> - <td class="last"> </td> - </tr> -- <?php $_prevOptionId = $attributes['option_id'] ?> -+ <?php $_prevOptionId = $attributes['option_id'] ?> - <?php endif; ?> - <?php endif; ?> - <tr<?= (++$_index == $_count && !$_showlastRow) ? ' class="border"' : '' ?>> -- <?php if (!$_item->getOrderItem()->getParentItem()): ?> -+ <?php if (!$_item->getOrderItem()->getParentItem()) : ?> - <td class="col-product"> - <div class="product-title"><?= $block->escapeHtml($_item->getName()) ?></div> - <div class="product-sku-block"> -- <span><?= /* @escapeNotVerified */ __('SKU') ?>:</span> -- <?= implode('<br />', $this->helper('Magento\Catalog\Helper\Data')->splitSku($block->escapeHtml($_item->getSku()))) ?> -+ <span><?= $block->escapeHtml(__('SKU')) ?>:</span> -+ <?= /* @noEscape */ implode('<br />', $this->helper(Magento\Catalog\Helper\Data::class)->splitSku($_item->getSku())) ?> - </div> -- <?php else: ?> -+ <?php else : ?> - <td class="col-product"> - <div class="option-value"><?= $block->getValueHtml($_item) ?></div> - </td> - <?php endif; ?> - <td class="col-price"> -- <?php if ($block->canShowPriceInfo($_item)): ?> -+ <?php if ($block->canShowPriceInfo($_item)) : ?> - <?= $block->getColumnHtml($_item, 'price') ?> -- <?php else: ?> -+ <?php else : ?> -   - <?php endif; ?> - </td> - <td class="col-qty"> -- <?php if ($block->canShowPriceInfo($_item)): ?> -- <?= /* @escapeNotVerified */ $_item->getQty()*1 ?> -- <?php else: ?> -+ <?php if ($block->canShowPriceInfo($_item)) : ?> -+ <?= (float)$_item->getQty() * 1 ?> -+ <?php else : ?> -   - <?php endif; ?> - </td> - <td class="col-subtotal"> -- <?php if ($block->canShowPriceInfo($_item)): ?> -+ <?php if ($block->canShowPriceInfo($_item)) : ?> - <?= $block->getColumnHtml($_item, 'subtotal') ?> -- <?php else: ?> -+ <?php else : ?> -   - <?php endif; ?> - </td> - <td class="col-tax"> -- <?php if ($block->canShowPriceInfo($_item)): ?> -- <?= /* @escapeNotVerified */ $block->displayPriceAttribute('tax_amount') ?> -- <?php else: ?> -+ <?php if ($block->canShowPriceInfo($_item)) : ?> -+ <?= /* @noEscape */ $block->displayPriceAttribute('tax_amount') ?> -+ <?php else : ?> -   - <?php endif; ?> - </td> - <td class="col-discount"> -- <?php if ($block->canShowPriceInfo($_item)): ?> -- <?= /* @escapeNotVerified */ $block->displayPriceAttribute('discount_amount') ?> -- <?php else: ?> -+ <?php if ($block->canShowPriceInfo($_item)) : ?> -+ <?= /* @noEscape */ $block->displayPriceAttribute('discount_amount') ?> -+ <?php else : ?> -   - <?php endif; ?> - </td> - <td class="col-total last"> -- <?php if ($block->canShowPriceInfo($_item)): ?> -+ <?php if ($block->canShowPriceInfo($_item)) : ?> - <?= $block->getColumnHtml($_item, 'total') ?> -- <?php else: ?> -+ <?php else : ?> -   - <?php endif; ?> - </td> - </tr> - <?php endforeach; ?> --<?php if ($_showlastRow): ?> -+<?php if ($_showlastRow) : ?> - <tr class="border"> - <td class="col-product"> -- <?php if ($block->getOrderOptions()): ?> -+ <?php if ($block->getOrderOptions()) : ?> - <dl class="item-options"> -- <?php foreach ($block->getOrderOptions() as $option): ?> -- <dt><?= /* @escapeNotVerified */ $option['label'] ?></dt> -+ <?php foreach ($block->getOrderOptions() as $option) : ?> -+ <dt><?= $block->escapeHtml($option['label']) ?></dt> - <dd> -- <?php if (isset($option['custom_view']) && $option['custom_view']): ?> -- <?= /* @escapeNotVerified */ $option['value'] ?> -- <?php else: ?> -- <?= $block->truncateString($option['value'], 55, '', $_remainder) ?> -- <?php if ($_remainder):?> -- ... <span id="<?= /* @escapeNotVerified */ $_id = 'id' . uniqid() ?>"><?= /* @escapeNotVerified */ $_remainder ?></span> -+ <?php if (isset($option['custom_view']) && $option['custom_view']) : ?> -+ <?= $block->escapeHtml($option['value']) ?> -+ <?php else : ?> -+ <?= $block->escapeHtml($block->truncateString($option['value'], 55, '', $_remainder)) ?> -+ <?php if ($_remainder) :?> -+ ... <span id="<?= $block->escapeHtmlAttr($_id = 'id' . uniqid()) ?>"><?= $block->escapeHtml($_remainder) ?></span> - <script> --require(['protoype'], function(){ -- -- $('<?= /* @escapeNotVerified */ $_id ?>').hide(); -- $('<?= /* @escapeNotVerified */ $_id ?>').up().observe('mouseover', function(){$('<?= /* @escapeNotVerified */ $_id ?>').show();}); -- $('<?= /* @escapeNotVerified */ $_id ?>').up().observe('mouseout', function(){$('<?= /* @escapeNotVerified */ $_id ?>').hide();}); -- --}); --</script> -+ require(['prototype'], function(){ -+ <?php $escapedId = $block->escapeJs($_id) ?> -+ $('<?= /* @noEscape */ $escapedId ?>').hide(); -+ $('<?= /* @noEscape */ $escapedId ?>').up().observe('mouseover', function(){$('<?= /* @noEscape */ $escapedId ?>').show();}); -+ $('<?= /* @noEscape */ $escapedId ?>').up().observe('mouseout', function(){$('<?= /* @noEscape */ $escapedId ?>').hide();}); -+ }); -+ </script> - <?php endif;?> - <?php endif;?> - </dd> -diff --git a/app/code/Magento/Bundle/view/adminhtml/templates/sales/order/view/items/renderer.phtml b/app/code/Magento/Bundle/view/adminhtml/templates/sales/order/view/items/renderer.phtml -index bb0857a80d6..233e57a0033 100644 ---- a/app/code/Magento/Bundle/view/adminhtml/templates/sales/order/view/items/renderer.phtml -+++ b/app/code/Magento/Bundle/view/adminhtml/templates/sales/order/view/items/renderer.phtml -@@ -3,9 +3,7 @@ - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -- --// @codingStandardsIgnoreFile -- -+// phpcs:disable Magento2.Templates.ThisInTemplate.FoundThis - ?> - <?php - /** -@@ -16,24 +14,24 @@ - - <?php $_item = $block->getItem() ?> - <?php $items = array_merge([$_item], $_item->getChildrenItems()); ?> --<?php $_count = count ($items) ?> -+<?php $_count = count($items) ?> - <?php $_index = 0 ?> - - <?php $_prevOptionId = '' ?> - --<?php if($block->getOrderOptions() || $_item->getDescription() || $block->canDisplayGiftmessage()): ?> -+<?php if ($block->getOrderOptions() || $_item->getDescription() || $block->canDisplayGiftmessage()) : ?> - <?php $_showlastRow = true ?> --<?php else: ?> -+<?php else : ?> - <?php $_showlastRow = false ?> - <?php endif; ?> - --<?php foreach ($items as $_item): ?> -+<?php foreach ($items as $_item) : ?> - <?php $block->setPriceDataObject($_item) ?> - <?php $attributes = $block->getSelectionAttributes($_item) ?> -- <?php if ($_item->getParentItem()): ?> -- <?php if ($_prevOptionId != $attributes['option_id']): ?> -+ <?php if ($_item->getParentItem()) : ?> -+ <?php if ($_prevOptionId != $attributes['option_id']) : ?> - <tr> -- <td class="col-product"><div class="option-label"><?= /* @escapeNotVerified */ $attributes['option_label'] ?></div></td> -+ <td class="col-product"><div class="option-label"><?= $block->escapeHtml($attributes['option_label']) ?></div></td> - <td> </td> - <td> </td> - <td> </td> -@@ -44,161 +42,160 @@ - <td> </td> - <td class="last"> </td> - </tr> -- <?php $_prevOptionId = $attributes['option_id'] ?> -+ <?php $_prevOptionId = $attributes['option_id'] ?> - <?php endif; ?> - <?php endif; ?> - <tr<?= (++$_index==$_count && !$_showlastRow)?' class="border"':'' ?>> -- <?php if (!$_item->getParentItem()): ?> -+ <?php if (!$_item->getParentItem()) : ?> - <td class="col-product"> -- <div class="product-title" id="order_item_<?= /* @escapeNotVerified */ $_item->getId() ?>_title"> -+ <div class="product-title" id="order_item_<?= $block->escapeHtmlAttr($_item->getId()) ?>_title"> - <?= $block->escapeHtml($_item->getName()) ?> - </div> - <div class="product-sku-block"> -- <span><?= /* @escapeNotVerified */ __('SKU') ?>:</span> -- <?= implode('<br />', $this->helper('Magento\Catalog\Helper\Data')->splitSku($block->escapeHtml($_item->getSku()))) ?> -+ <span><?= $block->escapeHtml(__('SKU')) ?>:</span> -+ <?= /* @noEscape */ implode('<br />', $this->helper(Magento\Catalog\Helper\Data::class)->splitSku($_item->getSku())) ?> - </div> - </td> -- <?php else: ?> -+ <?php else : ?> - <td class="col-product"> - <div class="option-value"><?= $block->getValueHtml($_item) ?></div> - </td> - <?php endif; ?> - <td class="col-status"> -- <?php if ($block->canShowPriceInfo($_item)): ?> -- <?= /* @escapeNotVerified */ $_item->getStatus() ?> -- <?php else: ?> -+ <?php if ($block->canShowPriceInfo($_item)) : ?> -+ <?= $block->escapeHtml($_item->getStatus()) ?> -+ <?php else : ?> -   - <?php endif; ?> - </td> - <td class="col-price-original"> -- <?php if ($block->canShowPriceInfo($_item)): ?> -- <?= /* @escapeNotVerified */ $block->displayPriceAttribute('original_price') ?> -- <?php else: ?> -+ <?php if ($block->canShowPriceInfo($_item)) : ?> -+ <?= /* @noEscape */ $block->displayPriceAttribute('original_price') ?> -+ <?php else : ?> -   - <?php endif; ?> - </td> - <td class="col-price"> -- <?php if ($block->canShowPriceInfo($_item)): ?> -+ <?php if ($block->canShowPriceInfo($_item)) : ?> - <?= $block->getColumnHtml($_item, 'price') ?> -- <?php else: ?> -+ <?php else : ?> -   - <?php endif; ?> - </td> - <td class="col-ordered-qty"> -- <?php if ($block->canShowPriceInfo($_item)): ?> -+ <?php if ($block->canShowPriceInfo($_item)) : ?> - <table class="qty-table"> - <tr> -- <th><?= /* @escapeNotVerified */ __('Ordered') ?></th> -- <td><?= /* @escapeNotVerified */ $_item->getQtyOrdered()*1 ?></td> -+ <th><?= $block->escapeHtml(__('Ordered')) ?></th> -+ <td><?= (float)$_item->getQtyOrdered() * 1 ?></td> - </tr> -- <?php if ((float) $_item->getQtyInvoiced()): ?> -+ <?php if ((float) $_item->getQtyInvoiced()) : ?> - <tr> -- <th><?= /* @escapeNotVerified */ __('Invoiced') ?></th> -- <td><?= /* @escapeNotVerified */ $_item->getQtyInvoiced()*1 ?></td> -+ <th><?= $block->escapeHtml(__('Invoiced')) ?></th> -+ <td><?= (float)$_item->getQtyInvoiced() * 1 ?></td> - </tr> - <?php endif; ?> -- <?php if ((float) $_item->getQtyShipped() && $block->isShipmentSeparately($_item)): ?> -+ <?php if ((float) $_item->getQtyShipped() && $block->isShipmentSeparately($_item)) : ?> - <tr> -- <th><?= /* @escapeNotVerified */ __('Shipped') ?></th> -- <td><?= /* @escapeNotVerified */ $_item->getQtyShipped()*1 ?></td> -+ <th><?= $block->escapeHtml(__('Shipped')) ?></th> -+ <td><?= (float)$_item->getQtyShipped() * 1 ?></td> - </tr> - <?php endif; ?> -- <?php if ((float) $_item->getQtyRefunded()): ?> -+ <?php if ((float) $_item->getQtyRefunded()) : ?> - <tr> -- <th><?= /* @escapeNotVerified */ __('Refunded') ?></th> -- <td><?= /* @escapeNotVerified */ $_item->getQtyRefunded()*1 ?></td> -+ <th><?= $block->escapeHtml(__('Refunded')) ?></th> -+ <td><?= (float)$_item->getQtyRefunded() * 1 ?></td> - </tr> - <?php endif; ?> -- <?php if ((float) $_item->getQtyCanceled()): ?> -+ <?php if ((float) $_item->getQtyCanceled()) : ?> - <tr> -- <th><?= /* @escapeNotVerified */ __('Canceled') ?></th> -- <td><?= /* @escapeNotVerified */ $_item->getQtyCanceled()*1 ?></td> -+ <th><?= $block->escapeHtml(__('Canceled')) ?></th> -+ <td><?= (float)$_item->getQtyCanceled() * 1 ?></td> - </tr> - <?php endif; ?> - </table> -- <?php elseif ($block->isShipmentSeparately($_item)): ?> -+ <?php elseif ($block->isShipmentSeparately($_item)) : ?> - <table class="qty-table"> - <tr> -- <th><?= /* @escapeNotVerified */ __('Ordered') ?></th> -- <td><?= /* @escapeNotVerified */ $_item->getQtyOrdered()*1 ?></td> -+ <th><?= $block->escapeHtml(__('Ordered')) ?></th> -+ <td><?= (float)$_item->getQtyOrdered() * 1 ?></td> - </tr> -- <?php if ((float) $_item->getQtyShipped()): ?> -+ <?php if ((float) $_item->getQtyShipped()) : ?> - <tr> -- <th><?= /* @escapeNotVerified */ __('Shipped') ?></th> -- <td><?= /* @escapeNotVerified */ $_item->getQtyShipped()*1 ?></td> -+ <th><?= $block->escapeHtml(__('Shipped')) ?></th> -+ <td><?= (float)$_item->getQtyShipped() * 1 ?></td> - </tr> - <?php endif; ?> - </table> -- <?php else: ?> -+ <?php else : ?> -   - <?php endif; ?> - </td> - <td class="col-subtotal"> -- <?php if ($block->canShowPriceInfo($_item)): ?> -+ <?php if ($block->canShowPriceInfo($_item)) : ?> - <?= $block->getColumnHtml($_item, 'subtotal') ?> -- <?php else: ?> -+ <?php else : ?> -   - <?php endif; ?> - </td> - <td class="col-tax-amount"> -- <?php if ($block->canShowPriceInfo($_item)): ?> -- <?= /* @escapeNotVerified */ $block->displayPriceAttribute('tax_amount') ?> -- <?php else: ?> -+ <?php if ($block->canShowPriceInfo($_item)) : ?> -+ <?= /* @noEscape */ $block->displayPriceAttribute('tax_amount') ?> -+ <?php else : ?> -   - <?php endif; ?> - </td> - <td class="col-tax-percent"> -- <?php if ($block->canShowPriceInfo($_item)): ?> -- <?= /* @escapeNotVerified */ $block->displayTaxPercent($_item) ?> -- <?php else: ?> -+ <?php if ($block->canShowPriceInfo($_item)) : ?> -+ <?= /* @noEscape */ $block->displayTaxPercent($_item) ?> -+ <?php else : ?> -   - <?php endif; ?> - </td> - <td class="col-discont"> -- <?php if ($block->canShowPriceInfo($_item)): ?> -- <?= /* @escapeNotVerified */ $block->displayPriceAttribute('discount_amount') ?> -- <?php else: ?> -+ <?php if ($block->canShowPriceInfo($_item)) : ?> -+ <?= /* @noEscape */ $block->displayPriceAttribute('discount_amount') ?> -+ <?php else : ?> -   - <?php endif; ?> - </td> - <td class="col-total last"> -- <?php if ($block->canShowPriceInfo($_item)): ?> -+ <?php if ($block->canShowPriceInfo($_item)) : ?> - <?= $block->getColumnHtml($_item, 'total') ?> -- <?php else: ?> -+ <?php else : ?> -   - <?php endif; ?> - </td> - </tr> - <?php endforeach; ?> --<?php if($_showlastRow): ?> -- <tr<?php if (!$block->canDisplayGiftmessage()) echo ' class="border"' ?>> -+<?php if ($_showlastRow) : ?> -+ <tr<?php if (!$block->canDisplayGiftmessage()) { echo ' class="border"'; } ?>> - <td class="col-product"> -- <?php if ($block->getOrderOptions()): ?> -+ <?php if ($block->getOrderOptions()) : ?> - <dl class="item-options"> -- <?php foreach ($block->getOrderOptions() as $option): ?> -- <dt><?= /* @escapeNotVerified */ $option['label'] ?>:</dt> -+ <?php foreach ($block->getOrderOptions() as $option) : ?> -+ <dt><?= $block->escapeHtml($option['label']) ?>:</dt> - <dd> -- <?php if (isset($option['custom_view']) && $option['custom_view']): ?> -- <?= /* @escapeNotVerified */ $option['value'] ?> -- <?php else: ?> -- <?= $block->truncateString($option['value'], 55, '', $_remainder) ?> -- <?php if ($_remainder):?> -- ... <span id="<?= /* @escapeNotVerified */ $_id = 'id' . uniqid() ?>"><?= /* @escapeNotVerified */ $_remainder ?></span> -+ <?php if (isset($option['custom_view']) && $option['custom_view']) : ?> -+ <?= $block->escapeHtml($option['value']) ?> -+ <?php else : ?> -+ <?= $block->escapeHtml($block->truncateString($option['value'], 55, '', $_remainder)) ?> -+ <?php if ($_remainder) :?> -+ ... <span id="<?= $block->escapeHtmlAttr($_id = 'id' . uniqid()) ?>"><?= $block->escapeHtml($_remainder) ?></span> - <script> --require(['prototype'], function(){ -- -- $('<?= /* @escapeNotVerified */ $_id ?>').hide(); -- $('<?= /* @escapeNotVerified */ $_id ?>').up().observe('mouseover', function(){$('<?= /* @escapeNotVerified */ $_id ?>').show();}); -- $('<?= /* @escapeNotVerified */ $_id ?>').up().observe('mouseout', function(){$('<?= /* @escapeNotVerified */ $_id ?>').hide();}); -- --}); --</script> -+ require(['prototype'], function(){ -+ <?php $escapedId = $block->escapeJs($_id) ?> -+ $('<?= /* @noEscape */ $escapedId ?>').hide(); -+ $('<?= /* @noEscape */ $escapedId ?>').up().observe('mouseover', function(){$('<?= /* @noEscape */ $escapedId ?>').show();}); -+ $('<?= /* @noEscape */ $escapedId ?>').up().observe('mouseout', function(){$('<?= /* @noEscape */ $escapedId ?>').hide();}); -+ }); -+ </script> - <?php endif;?> - <?php endif;?> - </dd> - <?php endforeach; ?> - </dl> -- <?php else: ?> -+ <?php else : ?> -   - <?php endif; ?> - <?= $block->escapeHtml($_item->getDescription()) ?> -diff --git a/app/code/Magento/Bundle/view/adminhtml/templates/sales/shipment/create/items/renderer.phtml b/app/code/Magento/Bundle/view/adminhtml/templates/sales/shipment/create/items/renderer.phtml -index 2ede8277bcf..2e52ed90662 100644 ---- a/app/code/Magento/Bundle/view/adminhtml/templates/sales/shipment/create/items/renderer.phtml -+++ b/app/code/Magento/Bundle/view/adminhtml/templates/sales/shipment/create/items/renderer.phtml -@@ -3,9 +3,7 @@ - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -- --// @codingStandardsIgnoreFile -- -+// phpcs:disable Magento2.Templates.ThisInTemplate.FoundThis - ?> - - <?php /** @var $block \Magento\Bundle\Block\Adminhtml\Sales\Order\Items\Renderer */ ?> -@@ -17,85 +15,84 @@ - - <?php $_prevOptionId = '' ?> - --<?php if ($block->getOrderOptions() || $_item->getDescription()): ?> -+<?php if ($block->getOrderOptions() || $_item->getDescription()) : ?> - <?php $_showlastRow = true ?> --<?php else: ?> -+<?php else : ?> - <?php $_showlastRow = false ?> - <?php endif; ?> - --<?php foreach ($items as $_item): ?> -+<?php foreach ($items as $_item) : ?> - <?php $block->setPriceDataObject($_item) ?> -- <?php if ($_item->getOrderItem()->getParentItem()): ?> -+ <?php if ($_item->getOrderItem()->getParentItem()) : ?> - <?php $attributes = $block->getSelectionAttributes($_item) ?> -- <?php if ($_prevOptionId != $attributes['option_id']): ?> -+ <?php if ($_prevOptionId != $attributes['option_id']) : ?> - <tr> -- <td class="col-product"><div class="option-label"><?= /* @escapeNotVerified */ $attributes['option_label'] ?></div></td> -+ <td class="col-product"><div class="option-label"><?= $block->escapeHtml($attributes['option_label']) ?></div></td> - <td class="col-product"> </td> - <td class="col-qty last"> </td> - </tr> -- <?php $_prevOptionId = $attributes['option_id'] ?> -+ <?php $_prevOptionId = $attributes['option_id'] ?> - <?php endif; ?> - <?php endif; ?> - <tr class="<?= (++$_index == $_count && !$_showlastRow) ? 'border' : '' ?>"> -- <?php if (!$_item->getOrderItem()->getParentItem()): ?> -+ <?php if (!$_item->getOrderItem()->getParentItem()) : ?> - <td class="col-product"> - <div class="product-title"><?= $block->escapeHtml($_item->getName()) ?></div> - <div class="product-sku-block"> -- <span><?= /* @escapeNotVerified */ __('SKU') ?>:</span> -- <?= implode('<br />', $this->helper('Magento\Catalog\Helper\Data')->splitSku($block->escapeHtml($_item->getSku()))) ?> -+ <span><?= $block->escapeHtml(__('SKU')) ?>:</span> -+ <?= /* @noEscape */ implode('<br />', $this->helper(Magento\Catalog\Helper\Data::class)->splitSku($_item->getSku())) ?> - </div> - </td> -- <?php else: ?> -+ <?php else : ?> - <td class="col-product"><div class="option-value"><?= $block->getValueHtml($_item) ?></div></td> - <?php endif; ?> - <td class="col-ordered-qty"> -- <?php if ($block->isShipmentSeparately($_item)): ?> -+ <?php if ($block->isShipmentSeparately($_item)) : ?> - <?= $block->getColumnHtml($_item, 'qty') ?> -- <?php else: ?> -+ <?php else : ?> -   - <?php endif; ?> - </td> - <td class="col-qty last"> -- <?php if ($block->isShipmentSeparately($_item)): ?> -+ <?php if ($block->isShipmentSeparately($_item)) : ?> - <input type="text" - class="input-text admin__control-text" -- name="shipment[items][<?= /* @escapeNotVerified */ $_item->getOrderItemId() ?>]" -- value="<?= /* @escapeNotVerified */ $_item->getQty()*1 ?>" /> -- <?php else: ?> -+ name="shipment[items][<?= $block->escapeHtmlAttr($_item->getOrderItemId()) ?>]" -+ value="<?= (float)$_item->getQty() * 1 ?>" /> -+ <?php else : ?> -   - <?php endif; ?> - </td> - </tr> - <?php endforeach; ?> --<?php if ($_showlastRow): ?> -+<?php if ($_showlastRow) : ?> - <tr class="border"> - <td class="col-product"> -- <?php if ($block->getOrderOptions($_item->getOrderItem())): ?> -+ <?php if ($block->getOrderOptions($_item->getOrderItem())) : ?> - <dl class="item-options"> -- <?php foreach ($block->getOrderOptions($_item->getOrderItem()) as $option): ?> -- <dt><?= /* @escapeNotVerified */ $option['label'] ?></dt> -+ <?php foreach ($block->getOrderOptions($_item->getOrderItem()) as $option) : ?> -+ <dt><?= $block->escapeHtml($option['label']) ?></dt> - <dd> -- <?php if (isset($option['custom_view']) && $option['custom_view']): ?> -- <?= /* @escapeNotVerified */ $option['value'] ?> -- <?php else: ?> -- <?= $block->truncateString($option['value'], 55, '', $_remainder) ?> -- <?php if ($_remainder):?> -- ... <span id="<?= /* @escapeNotVerified */ $_id = 'id' . uniqid() ?>"><?= /* @escapeNotVerified */ $_remainder ?></span> -+ <?php if (isset($option['custom_view']) && $option['custom_view']) : ?> -+ <?= $block->escapeHtml($option['value']) ?> -+ <?php else : ?> -+ <?= $block->escapeHtml($block->truncateString($option['value'], 55, '', $_remainder)) ?> -+ <?php if ($_remainder) :?> -+ ... <span id="<?= $block->escapeHtmlAttr($_id = 'id' . uniqid()) ?>"><?= $block->escapeHtml($_remainder) ?></span> - <script> --require(['prototype'], function(){ -- -- $('<?= /* @escapeNotVerified */ $_id ?>').hide(); -- $('<?= /* @escapeNotVerified */ $_id ?>').up().observe('mouseover', function(){$('<?= /* @escapeNotVerified */ $_id ?>').show();}); -- $('<?= /* @escapeNotVerified */ $_id ?>').up().observe('mouseout', function(){$('<?= /* @escapeNotVerified */ $_id ?>').hide();}); -- --}); --</script> -+ require(['prototype'], function(){ -+ <?php $escapedId = $block->escapeJs($_id) ?> -+ $('<?= /* @noEscape */ $escapedId ?>').hide(); -+ $('<?= /* @noEscape */ $escapedId ?>').up().observe('mouseover', function(){$('<?= /* @noEscape */ $escapedId ?>').show();}); -+ $('<?= /* @noEscape */ $escapedId ?>').up().observe('mouseout', function(){$('<?= /* @noEscape */ $escapedId ?>').hide();}); -+ }); -+ </script> - <?php endif;?> - <?php endif;?> - </dd> - <?php endforeach; ?> - </dl> -- <?php else: ?> -+ <?php else : ?> -   - <?php endif; ?> - <?= $block->escapeHtml($_item->getDescription()) ?> -diff --git a/app/code/Magento/Bundle/view/adminhtml/templates/sales/shipment/view/items/renderer.phtml b/app/code/Magento/Bundle/view/adminhtml/templates/sales/shipment/view/items/renderer.phtml -index 71eabd45cbb..521669700e1 100644 ---- a/app/code/Magento/Bundle/view/adminhtml/templates/sales/shipment/view/items/renderer.phtml -+++ b/app/code/Magento/Bundle/view/adminhtml/templates/sales/shipment/view/items/renderer.phtml -@@ -3,9 +3,7 @@ - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -- --// @codingStandardsIgnoreFile -- -+// phpcs:disable Magento2.Templates.ThisInTemplate.FoundThis - ?> - <?php /** @var $block \Magento\Bundle\Block\Adminhtml\Sales\Order\Items\Renderer */ ?> - -@@ -17,74 +15,73 @@ - - <?php $_prevOptionId = '' ?> - --<?php if ($block->getOrderOptions() || $_item->getDescription()): ?> -+<?php if ($block->getOrderOptions() || $_item->getDescription()) : ?> - <?php $_showlastRow = true ?> --<?php else: ?> -+<?php else : ?> - <?php $_showlastRow = false ?> - <?php endif; ?> - --<?php foreach ($items as $_item): ?> -+<?php foreach ($items as $_item) : ?> - <?php $block->setPriceDataObject($_item) ?> -- <?php if ($_item->getParentItem()): ?> -+ <?php if ($_item->getParentItem()) : ?> - <?php $attributes = $block->getSelectionAttributes($_item) ?> -- <?php if ($_prevOptionId != $attributes['option_id']): ?> -+ <?php if ($_prevOptionId != $attributes['option_id']) : ?> - <tr> -- <td class="col-product"><div class="option-label"><?= /* @escapeNotVerified */ $attributes['option_label'] ?></div></td> -+ <td class="col-product"><div class="option-label"><?= $block->escapeHtml($attributes['option_label']) ?></div></td> - <td class="col-qty last"> </td> - </tr> -- <?php $_prevOptionId = $attributes['option_id'] ?> -+ <?php $_prevOptionId = $attributes['option_id'] ?> - <?php endif; ?> - <?php endif; ?> - <tr<?= (++$_index == $_count && !$_showlastRow) ? ' class="border"' : '' ?>> -- <?php if (!$_item->getParentItem()): ?> -+ <?php if (!$_item->getParentItem()) : ?> - <td class="col-product"> - <div class="product-title"><?= $block->escapeHtml($_item->getName()) ?></div> - <div class="product-sku-block"> -- <span><?= /* @escapeNotVerified */ __('SKU') ?>:</span> -- <?= implode('<br />', $this->helper('Magento\Catalog\Helper\Data')->splitSku($block->escapeHtml($_item->getSku()))) ?> -+ <span><?= $block->escapeHtml(__('SKU')) ?>:</span> -+ <?= /* @noEscape */ implode('<br />', $this->helper(Magento\Catalog\Helper\Data::class)->splitSku($_item->getSku())) ?> - </div> - </td> -- <?php else: ?> -+ <?php else : ?> - <td class="col-product"><div class="option-value"><?= $block->getValueHtml($_item) ?></div></td> - <?php endif; ?> - <td class="col-qty last"> -- <?php if (($block->isShipmentSeparately() && $_item->getParentItem()) || (!$block->isShipmentSeparately() && !$_item->getParentItem())): ?> -- <?php if (isset($shipItems[$_item->getId()])): ?> -- <?= /* @escapeNotVerified */ $shipItems[$_item->getId()]->getQty()*1 ?> -- <?php elseif ($_item->getIsVirtual()): ?> -- <?= /* @escapeNotVerified */ __('N/A') ?> -- <?php else: ?> -+ <?php if (($block->isShipmentSeparately() && $_item->getParentItem()) || (!$block->isShipmentSeparately() && !$_item->getParentItem())) : ?> -+ <?php if (isset($shipItems[$_item->getId()])) : ?> -+ <?= (float)$shipItems[$_item->getId()]->getQty() * 1 ?> -+ <?php elseif ($_item->getIsVirtual()) : ?> -+ <?= $block->escapeHtml(__('N/A')) ?> -+ <?php else : ?> - 0 - <?php endif; ?> -- <?php else: ?> -+ <?php else : ?> -   - <?php endif; ?> - </td> - </tr> - <?php endforeach; ?> --<?php if ($_showlastRow): ?> -+<?php if ($_showlastRow) : ?> - <tr class="border"> - <td class="col-product"> -- <?php if ($block->getOrderOptions($_item->getOrderItem())): ?> -+ <?php if ($block->getOrderOptions($_item->getOrderItem())) : ?> - <dl class="item-options"> -- <?php foreach ($block->getOrderOptions($_item->getOrderItem()) as $option): ?> -- <dt><?= /* @escapeNotVerified */ $option['label'] ?></dt> -+ <?php foreach ($block->getOrderOptions($_item->getOrderItem()) as $option) : ?> -+ <dt><?= $block->escapeHtml($option['label']) ?></dt> - <dd> -- <?php if (isset($option['custom_view']) && $option['custom_view']): ?> -- <?= /* @escapeNotVerified */ $option['value'] ?> -- <?php else: ?> -- <?= $block->truncateString($option['value'], 55, '', $_remainder) ?> -- <?php if ($_remainder):?> -- ... <span id="<?= /* @escapeNotVerified */ $_id = 'id' . uniqid() ?>"><?= /* @escapeNotVerified */ $_remainder ?></span> -+ <?php if (isset($option['custom_view']) && $option['custom_view']) : ?> -+ <?= $block->escapeHtml($option['value']) ?> -+ <?php else : ?> -+ <?= $block->escapeHtml($block->truncateString($option['value'], 55, '', $_remainder)) ?> -+ <?php if ($_remainder) :?> -+ ... <span id="<?= $block->escapeHtmlAttr($_id = 'id' . uniqid()) ?>"><?= $block->escapeHtml($_remainder) ?></span> - <script> --require(['prototype'], function(){ -- -- $('<?= /* @escapeNotVerified */ $_id ?>').hide(); -- $('<?= /* @escapeNotVerified */ $_id ?>').up().observe('mouseover', function(){$('<?= /* @escapeNotVerified */ $_id ?>').show();}); -- $('<?= /* @escapeNotVerified */ $_id ?>').up().observe('mouseout', function(){$('<?= /* @escapeNotVerified */ $_id ?>').hide();}); -- --}); --</script> -+ require(['prototype'], function(){ -+ <?php $escapedId = $block->escapeJs($_id) ?> -+ $('<?= /* @noEscape */ $escapedId ?>').hide(); -+ $('<?= /* @noEscape */ $escapedId ?>').up().observe('mouseover', function(){$('<?= /* @noEscape */ $escapedId ?>').show();}); -+ $('<?= /* @noEscape */ $escapedId ?>').up().observe('mouseout', function(){$('<?= /* @noEscape */ $escapedId ?>').hide();}); -+ }); -+ </script> - <?php endif;?> - <?php endif;?> - </dd> -diff --git a/app/code/Magento/Bundle/view/base/templates/product/price/final_price.phtml b/app/code/Magento/Bundle/view/base/templates/product/price/final_price.phtml -index 5955d0337bb..26264cc2cc8 100644 ---- a/app/code/Magento/Bundle/view/base/templates/product/price/final_price.phtml -+++ b/app/code/Magento/Bundle/view/base/templates/product/price/final_price.phtml -@@ -3,9 +3,6 @@ - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -- --// @codingStandardsIgnoreFile -- - ?> - - <?php -@@ -21,76 +18,63 @@ $maximalPrice = $finalPriceModel->getMaximalPrice(); - $regularPriceModel = $block->getPriceType('regular_price'); - $maximalRegularPrice = $regularPriceModel->getMaximalPrice(); - $minimalRegularPrice = $regularPriceModel->getMinimalPrice(); -+$regularPriceAttributes = [ -+ 'display_label' => __('Regular Price'), -+ 'price_id' => $block->getPriceId('old-price-' . $idSuffix), -+ 'include_container' => true, -+ 'skip_adjustments' => true -+]; -+$renderMinimalRegularPrice = $block->renderAmount($minimalRegularPrice, $regularPriceAttributes); - ?> --<?php if ($block->getSaleableItem()->getPriceView()): ?> -+<?php if ($block->getSaleableItem()->getPriceView()) : ?> - <p class="minimal-price"> -- <?php /* @escapeNotVerified */ echo $block->renderAmount($minimalPrice, [ -+ <?= /* @noEscape */ $block->renderAmount($minimalPrice, [ - 'display_label' => __('As low as'), - 'price_id' => $block->getPriceId('from-'), - 'include_container' => true - ]); ?> -- <?php if ($minimalPrice < $minimalRegularPrice): ?> -+ <?php if ($minimalPrice < $minimalRegularPrice) : ?> - <span class="old-price"> -- <?php /* @escapeNotVerified */ echo $block->renderAmount($minimalRegularPrice, [ -- 'display_label' => __('Regular Price'), -- 'price_id' => $block->getPriceId('old-price-' . $idSuffix), -- 'include_container' => true, -- 'skip_adjustments' => true -- ]); ?> -+ <?= /* @noEscape */ $renderMinimalRegularPrice ?> - </span> - <?php endif ?> - </p> --<?php else: ?> -- <?php if ($block->showRangePrice()): ?> -+<?php else : ?> -+ <?php if ($block->showRangePrice()) : ?> - <p class="price-from"> -- <?php /* @escapeNotVerified */ echo $block->renderAmount($minimalPrice, [ -+ <?= /* @noEscape */ $block->renderAmount($minimalPrice, [ - 'display_label' => __('From'), - 'price_id' => $block->getPriceId('from-'), - 'price_type' => 'minPrice', - 'include_container' => true - ]); ?> -- <?php if ($minimalPrice < $minimalRegularPrice): ?> -+ <?php if ($minimalPrice < $minimalRegularPrice) : ?> - <span class="old-price"> -- <?php /* @escapeNotVerified */ echo $block->renderAmount($minimalRegularPrice, [ -- 'display_label' => __('Regular Price'), -- 'price_id' => $block->getPriceId('old-price-' . $idSuffix), -- 'include_container' => true, -- 'skip_adjustments' => true -- ]); ?> -+ <?= /* @noEscape */ $renderMinimalRegularPrice ?> - </span> - <?php endif ?> - </p> - <p class="price-to"> -- <?php /* @escapeNotVerified */ echo $block->renderAmount($maximalPrice, [ -+ <?= /* @noEscape */ $block->renderAmount($maximalPrice, [ - 'display_label' => __('To'), - 'price_id' => $block->getPriceId('to-'), - 'price_type' => 'maxPrice', - 'include_container' => true - ]); ?> -- <?php if ($maximalPrice < $maximalRegularPrice): ?> -+ <?php if ($maximalPrice < $maximalRegularPrice) : ?> - <span class="old-price"> -- <?php /* @escapeNotVerified */ echo $block->renderAmount($maximalRegularPrice, [ -- 'display_label' => __('Regular Price'), -- 'price_id' => $block->getPriceId('old-price-' . $idSuffix), -- 'include_container' => true, -- 'skip_adjustments' => true -- ]); ?> -+ <?= /* @noEscape */ $block->renderAmount($maximalRegularPrice, $regularPriceAttributes); ?> - </span> - <?php endif ?> - </p> -- <?php else: ?> -- <?php /* @escapeNotVerified */ echo $block->renderAmount($minimalPrice, [ -+ <?php else : ?> -+ <?= /* @noEscape */ $block->renderAmount($minimalPrice, [ - 'price_id' => $block->getPriceId('product-price-'), - 'include_container' => true - ]); ?> -- <?php if ($minimalPrice < $minimalRegularPrice): ?> -+ <?php if ($minimalPrice < $minimalRegularPrice) : ?> - <span class="old-price"> -- <?php /* @escapeNotVerified */ echo $block->renderAmount($minimalRegularPrice, [ -- 'display_label' => __('Regular Price'), -- 'price_id' => $block->getPriceId('old-price-' . $idSuffix), -- 'include_container' => true, -- 'skip_adjustments' => true -- ]); ?> -+ <?= /* @noEscape */ $renderMinimalRegularPrice ?> - </span> - <?php endif ?> - <?php endif ?> -diff --git a/app/code/Magento/Bundle/view/base/templates/product/price/selection/amount.phtml b/app/code/Magento/Bundle/view/base/templates/product/price/selection/amount.phtml -index 53d24dd7c2c..00bd9955632 100644 ---- a/app/code/Magento/Bundle/view/base/templates/product/price/selection/amount.phtml -+++ b/app/code/Magento/Bundle/view/base/templates/product/price/selection/amount.phtml -@@ -3,11 +3,8 @@ - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -- --// @codingStandardsIgnoreFile -- - ?> - - <?php /** @var \Magento\Framework\Pricing\Render\Amount $block */ ?> - --<?= /* @escapeNotVerified */ $block->formatCurrency($block->getDisplayValue(), (bool) $block->getIncludeContainer()) ?> -+<?= /* @noEscape */ $block->formatCurrency($block->getDisplayValue(), (bool) $block->getIncludeContainer()) ?> -diff --git a/app/code/Magento/Bundle/view/base/templates/product/price/tier_prices.phtml b/app/code/Magento/Bundle/view/base/templates/product/price/tier_prices.phtml -index 5f152c4bbef..f5f67588a1c 100644 ---- a/app/code/Magento/Bundle/view/base/templates/product/price/tier_prices.phtml -+++ b/app/code/Magento/Bundle/view/base/templates/product/price/tier_prices.phtml -@@ -3,9 +3,6 @@ - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -- --// @codingStandardsIgnoreFile -- - ?> - - <?php -@@ -16,10 +13,10 @@ $tierPriceModel = $block->getPrice(); - $tierPrices = $tierPriceModel->getTierPriceList(); - ?> - <?php if (count($tierPrices)) : ?> -- <ul class="<?= /* @escapeNotVerified */ ($block->hasListClass() ? $block->getListClass() : 'prices-tier items') ?>"> -+ <ul class="<?= $block->escapeHtmlAttr(($block->hasListClass() ? $block->getListClass() : 'prices-tier items')) ?>"> - <?php foreach ($tierPrices as $index => $price) : ?> - <li class="item"> -- <?php /* @escapeNotVerified */ echo __( -+ <?= /* @noEscape */ __( - 'Buy %1 with %2 discount each', - $price['price_qty'], - '<strong class="benefit">' . round($price['percentage_value']) . '%</strong>' -diff --git a/app/code/Magento/Bundle/view/base/web/js/price-bundle.js b/app/code/Magento/Bundle/view/base/web/js/price-bundle.js -index e56cc6f32d8..49ee253ad1e 100644 ---- a/app/code/Magento/Bundle/view/base/web/js/price-bundle.js -+++ b/app/code/Magento/Bundle/view/base/web/js/price-bundle.js -@@ -27,7 +27,8 @@ define([ - '<% } %>', - controlContainer: 'dd', // should be eliminated - priceFormat: {}, -- isFixedPrice: false -+ isFixedPrice: false, -+ optionTierPricesBlocksSelector: '#option-tier-prices-{1} [data-role="selection-tier-prices"]' - }; - - $.widget('mage.priceBundle', { -@@ -91,6 +92,8 @@ define([ - if (changes) { - priceBox.trigger('updatePrice', changes); - } -+ -+ this._displayTierPriceBlock(bundleOption); - this.updateProductSummary(); - }, - -@@ -207,6 +210,35 @@ define([ - return this; - }, - -+ /** -+ * Show or hide option tier prices block -+ * -+ * @param {Object} optionElement -+ * @private -+ */ -+ _displayTierPriceBlock: function (optionElement) { -+ var optionType = optionElement.prop('type'), -+ optionId, -+ optionValue, -+ optionTierPricesElements; -+ -+ if (optionType === 'select-one') { -+ optionId = utils.findOptionId(optionElement[0]); -+ optionValue = optionElement.val() || null; -+ optionTierPricesElements = $(this.options.optionTierPricesBlocksSelector.replace('{1}', optionId)); -+ -+ _.each(optionTierPricesElements, function (tierPriceElement) { -+ var selectionId = $(tierPriceElement).data('selection-id') + ''; -+ -+ if (selectionId === optionValue) { -+ $(tierPriceElement).show(); -+ } else { -+ $(tierPriceElement).hide(); -+ } -+ }); -+ } -+ }, -+ - /** - * Handler to update productSummary box - */ -@@ -374,8 +406,17 @@ define([ - function applyTierPrice(oneItemPrice, qty, optionConfig) { - var tiers = optionConfig.tierPrice, - magicKey = _.keys(oneItemPrice)[0], -+ tiersFirstKey = _.keys(optionConfig)[0], - lowest = false; - -+ if (!tiers) {//tiers is undefined when options has only one option -+ tiers = optionConfig[tiersFirstKey].tierPrice; -+ } -+ -+ tiers.sort(function (a, b) {//sorting based on "price_qty" -+ return a['price_qty'] - b['price_qty']; -+ }); -+ - _.each(tiers, function (tier, index) { - if (tier['price_qty'] > qty) { - return; -diff --git a/app/code/Magento/Bundle/view/frontend/layout/catalog_product_view_type_bundle.xml b/app/code/Magento/Bundle/view/frontend/layout/catalog_product_view_type_bundle.xml -index 5b8c050e5af..d12f2e8f6a9 100644 ---- a/app/code/Magento/Bundle/view/frontend/layout/catalog_product_view_type_bundle.xml -+++ b/app/code/Magento/Bundle/view/frontend/layout/catalog_product_view_type_bundle.xml -@@ -29,10 +29,22 @@ - <container name="product.info.bundle.options.top" as="product_info_bundle_options_top"> - <block class="Magento\Catalog\Block\Product\View" name="bundle.back.button" as="backButton" before="-" template="Magento_Bundle::catalog/product/view/backbutton.phtml"/> - </container> -- <block class="Magento\Bundle\Block\Catalog\Product\View\Type\Bundle\Option\Select" name="product.info.bundle.options.select" as="select"/> -+ <block class="Magento\Bundle\Block\Catalog\Product\View\Type\Bundle\Option\Select" name="product.info.bundle.options.select" as="select"> -+ <arguments> -+ <argument name="tier_price_renderer" xsi:type="object">\Magento\Bundle\Block\DataProviders\OptionPriceRenderer</argument> -+ </arguments> -+ </block> - <block class="Magento\Bundle\Block\Catalog\Product\View\Type\Bundle\Option\Multi" name="product.info.bundle.options.multi" as="multi"/> -- <block class="Magento\Bundle\Block\Catalog\Product\View\Type\Bundle\Option\Radio" name="product.info.bundle.options.radio" as="radio"/> -- <block class="Magento\Bundle\Block\Catalog\Product\View\Type\Bundle\Option\Checkbox" name="product.info.bundle.options.checkbox" as="checkbox"/> -+ <block class="Magento\Bundle\Block\Catalog\Product\View\Type\Bundle\Option\Radio" name="product.info.bundle.options.radio" as="radio"> -+ <arguments> -+ <argument name="tier_price_renderer" xsi:type="object">\Magento\Bundle\Block\DataProviders\OptionPriceRenderer</argument> -+ </arguments> -+ </block> -+ <block class="Magento\Bundle\Block\Catalog\Product\View\Type\Bundle\Option\Checkbox" name="product.info.bundle.options.checkbox" as="checkbox"> -+ <arguments> -+ <argument name="tier_price_renderer" xsi:type="object">\Magento\Bundle\Block\DataProviders\OptionPriceRenderer</argument> -+ </arguments> -+ </block> - </block> - </referenceBlock> - <referenceBlock name="product.info.form.options"> -diff --git a/app/code/Magento/Bundle/view/frontend/templates/catalog/product/view/backbutton.phtml b/app/code/Magento/Bundle/view/frontend/templates/catalog/product/view/backbutton.phtml -index 31a39c1cd16..ba58544c26a 100644 ---- a/app/code/Magento/Bundle/view/frontend/templates/catalog/product/view/backbutton.phtml -+++ b/app/code/Magento/Bundle/view/frontend/templates/catalog/product/view/backbutton.phtml -@@ -6,5 +6,5 @@ - ?> - <button type="button" - class="action back customization"> -- <span><?= /* @escapeNotVerified */ __('Go back to product details') ?></span> -+ <span><?= $block->escapeHtml(__('Go back to product details')) ?></span> - </button> -diff --git a/app/code/Magento/Bundle/view/frontend/templates/catalog/product/view/customize.phtml b/app/code/Magento/Bundle/view/frontend/templates/catalog/product/view/customize.phtml -index d7aea4237b1..480ffea5bc8 100644 ---- a/app/code/Magento/Bundle/view/frontend/templates/catalog/product/view/customize.phtml -+++ b/app/code/Magento/Bundle/view/frontend/templates/catalog/product/view/customize.phtml -@@ -3,18 +3,15 @@ - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -- --// @codingStandardsIgnoreFile -- - ?> - - <?php $_product = $block->getProduct() ?> --<?php if ($_product->isSaleable() && $block->hasOptions()):?> -+<?php if ($_product->isSaleable() && $block->hasOptions()) :?> - <div class="bundle-actions"> - <button id="bundle-slide" - class="action primary customize" - type="button"> -- <span><?= /* @escapeNotVerified */ __('Customize and Add to Cart') ?></span> -+ <span><?= $block->escapeHtml(__('Customize and Add to Cart')) ?></span> - </button> - </div> - <?php endif;?> -diff --git a/app/code/Magento/Bundle/view/frontend/templates/catalog/product/view/options/notice.phtml b/app/code/Magento/Bundle/view/frontend/templates/catalog/product/view/options/notice.phtml -index 2dbea0fd213..927b64352d5 100644 ---- a/app/code/Magento/Bundle/view/frontend/templates/catalog/product/view/options/notice.phtml -+++ b/app/code/Magento/Bundle/view/frontend/templates/catalog/product/view/options/notice.phtml -@@ -4,4 +4,4 @@ - * See COPYING.txt for license details. - */ - ?> --<p class="required"><?= /* @escapeNotVerified */ __('* Required Fields') ?></p> -+<p class="required"><?= $block->escapeHtml(__('* Required Fields')) ?></p> -diff --git a/app/code/Magento/Bundle/view/frontend/templates/catalog/product/view/summary.phtml b/app/code/Magento/Bundle/view/frontend/templates/catalog/product/view/summary.phtml -index bc4337fa7ae..9bf179e622f 100644 ---- a/app/code/Magento/Bundle/view/frontend/templates/catalog/product/view/summary.phtml -+++ b/app/code/Magento/Bundle/view/frontend/templates/catalog/product/view/summary.phtml -@@ -3,40 +3,37 @@ - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -- --// @codingStandardsIgnoreFile -- - ?> - - <?php - $_product = $block->getProduct(); - ?> --<?php if ($_product->isSaleable() && $block->hasOptions()): ?> -+<?php if ($_product->isSaleable() && $block->hasOptions()) : ?> - <div id="bundleSummary" - class="block-bundle-summary" - data-mage-init='{"sticky":{"container": ".product-add-form"}}'> - <div class="title"> -- <strong><?= /* @escapeNotVerified */ __('Your Customization') ?></strong> -+ <strong><?= $block->escapeHtml(__('Your Customization')) ?></strong> - </div> - <div class="content"> - <div class="bundle-info"> - <?= $block->getImage($_product, 'bundled_product_customization_page')->toHtml() ?> - <div class="product-details"> - <strong class="product name"><?= $block->escapeHtml($_product->getName()) ?></strong> -- <?php if ($_product->getIsSalable()): ?> -- <p class="available stock" title="<?= /* @escapeNotVerified */ __('Availability') ?>"> -- <span><?= /* @escapeNotVerified */ __('In stock') ?></span> -+ <?php if ($_product->getIsSalable()) : ?> -+ <p class="available stock" title="<?= $block->escapeHtmlAttr(__('Availability')) ?>"> -+ <span><?= $block->escapeHtml(__('In stock')) ?></span> - </p> -- <?php else: ?> -- <p class="unavailable stock" title="<?= /* @escapeNotVerified */ __('Availability') ?>"> -- <span><?= /* @escapeNotVerified */ __('Out of stock') ?></span> -+ <?php else : ?> -+ <p class="unavailable stock" title="<?= $block->escapeHtmlAttr(__('Availability')) ?>"> -+ <span><?= $block->escapeHtml(__('Out of stock')) ?></span> - </p> - <?php endif; ?> - <?= $block->getChildHtml('', true) ?> - </div> - </div> - <div class="bundle-summary"> -- <strong class="subtitle"><?= /* @escapeNotVerified */ __('Summary') ?></strong> -+ <strong class="subtitle"><?= $block->escapeHtml(__('Summary')) ?></strong> - <div id="bundle-summary" data-container="product-summary"> - <ul data-mage-init='{"productSummary": []}' class="bundle items"></ul> - <script data-template="bundle-summary" type="text/x-magento-template"> -@@ -46,7 +43,7 @@ - </li> - </script> - <script data-template="bundle-option" type="text/x-magento-template"> -- <div><?= /* @escapeNotVerified */ __('%1 x %2', '<%- data._quantity_ %>', '<%- data._label_ %>') ?></div> -+ <div><?= /* @noEscape */ __('%1 x %2', '<%- data._quantity_ %>', '<%- data._label_ %>') ?></div> - </script> - </div> - </div> -@@ -61,7 +58,7 @@ - "slideBackSelector": ".action.customization.back", - "bundleProductSelector": "#bundleProduct", - "bundleOptionsContainer": ".product-add-form" -- <?php if ($block->isStartCustomization()): ?> -+ <?php if ($block->isStartCustomization()) : ?> - ,"autostart": true - <?php endif;?> - } -diff --git a/app/code/Magento/Bundle/view/frontend/templates/catalog/product/view/type/bundle.phtml b/app/code/Magento/Bundle/view/frontend/templates/catalog/product/view/type/bundle.phtml -index ce9ef89a82b..ee29fc61d01 100644 ---- a/app/code/Magento/Bundle/view/frontend/templates/catalog/product/view/type/bundle.phtml -+++ b/app/code/Magento/Bundle/view/frontend/templates/catalog/product/view/type/bundle.phtml -@@ -4,19 +4,17 @@ - * See COPYING.txt for license details. - */ - --// @codingStandardsIgnoreFile -- - /* @var $block \Magento\Bundle\Block\Catalog\Product\View\Type\Bundle */ - ?> - <?php $_product = $block->getProduct() ?> --<?php if ($block->displayProductStockStatus()): ?> -- <?php if ($_product->isAvailable()): ?> -- <p class="stock available" title="<?= /* @escapeNotVerified */ __('Availability:') ?>"> -- <span><?= /* @escapeNotVerified */ __('In stock') ?></span> -+<?php if ($block->displayProductStockStatus()) : ?> -+ <?php if ($_product->isAvailable()) : ?> -+ <p class="stock available" title="<?= $block->escapeHtmlAttr(__('Availability:')) ?>"> -+ <span><?= $block->escapeHtml(__('In stock')) ?></span> - </p> -- <?php else: ?> -- <p class="stock unavailable" title="<?= /* @escapeNotVerified */ __('Availability:') ?>"> -- <span><?= /* @escapeNotVerified */ __('Out of stock') ?></span> -+ <?php else : ?> -+ <p class="stock unavailable" title="<?= $block->escapeHtmlAttr(__('Availability:')) ?>"> -+ <span><?= $block->escapeHtml(__('Out of stock')) ?></span> - </p> - <?php endif; ?> - <?php endif; ?> -diff --git a/app/code/Magento/Bundle/view/frontend/templates/catalog/product/view/type/bundle/option/checkbox.phtml b/app/code/Magento/Bundle/view/frontend/templates/catalog/product/view/type/bundle/option/checkbox.phtml -index bda649eb603..5b56598dc58 100644 ---- a/app/code/Magento/Bundle/view/frontend/templates/catalog/product/view/type/bundle/option/checkbox.phtml -+++ b/app/code/Magento/Bundle/view/frontend/templates/catalog/product/view/type/bundle/option/checkbox.phtml -@@ -3,9 +3,6 @@ - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -- --// @codingStandardsIgnoreFile -- - ?> - - <?php /* @var $block \Magento\Bundle\Block\Catalog\Product\View\Type\Bundle\Option\Checkbox */ ?> -@@ -17,31 +14,34 @@ - </label> - <div class="control"> - <div class="nested options-list"> -- <?php if ($block->showSingle()): ?> -- <?= /* @escapeNotVerified */ $block->getSelectionQtyTitlePrice($_selections[0]) ?> -+ <?php if ($block->showSingle()) : ?> -+ <?= /* @noEscape */ $block->getSelectionQtyTitlePrice($_selections[0]) ?> -+ <?= /* @noEscape */ $block->getTierPriceRenderer()->renderTierPrice($_selections[0]) ?> - <input type="hidden" -- class="bundle-option-<?= /* @escapeNotVerified */ $_option->getId() ?> product bundle option" -- name="bundle_option[<?= /* @escapeNotVerified */ $_option->getId() ?>]" -- value="<?= /* @escapeNotVerified */ $_selections[0]->getSelectionId() ?>"/> -- <?php else:?> -- <?php foreach($_selections as $_selection): ?> -+ class="bundle-option-<?= $block->escapeHtmlAttr($_option->getId()) ?> product bundle option" -+ name="bundle_option[<?= $block->escapeHtml($_option->getId()) ?>]" -+ value="<?= $block->escapeHtmlAttr($_selections[0]->getSelectionId()) ?>"/> -+ <?php else :?> -+ <?php foreach ($_selections as $_selection) : ?> - <div class="field choice"> -- <input class="bundle-option-<?= /* @escapeNotVerified */ $_option->getId() ?> checkbox product bundle option change-container-classname" -- id="bundle-option-<?= /* @escapeNotVerified */ $_option->getId() ?>-<?= /* @escapeNotVerified */ $_selection->getSelectionId() ?>" -+ <input class="bundle-option-<?= $block->escapeHtmlAttr($_option->getId()) ?> checkbox product bundle option change-container-classname" -+ id="bundle-option-<?= $block->escapeHtmlAttr($_option->getId()) ?>-<?= $block->escapeHtmlAttr($_selection->getSelectionId()) ?>" - type="checkbox" -- <?php if ($_option->getRequired()) /* @escapeNotVerified */ echo 'data-validate="{\'validate-one-required-by-name\':\'input[name^="bundle_option[' . $_option->getId() . ']"]:checked\'}"'?> -- name="bundle_option[<?= /* @escapeNotVerified */ $_option->getId() ?>][<?= /* @escapeNotVerified */ $_selection->getId() ?>]" -- data-selector="bundle_option[<?= /* @escapeNotVerified */ $_option->getId() ?>][<?= /* @escapeNotVerified */ $_selection->getId() ?>]" -- <?php if ($block->isSelected($_selection)) echo ' checked="checked"' ?> -- <?php if (!$_selection->isSaleable()) echo ' disabled="disabled"' ?> -- value="<?= /* @escapeNotVerified */ $_selection->getSelectionId() ?>"/> -+ <?php if ($_option->getRequired()) { echo 'data-validate="{\'validate-one-required-by-name\':\'input[name^="bundle_option[' . $block->escapeHtmlAttr($_option->getId()) . ']"]:checked\'}"'; } ?> -+ name="bundle_option[<?= $block->escapeHtmlAttr($_option->getId()) ?>][<?= $block->escapeHtmlAttr($_selection->getId()) ?>]" -+ data-selector="bundle_option[<?= $block->escapeHtmlAttr($_option->getId()) ?>][<?= $block->escapeHtmlAttr($_selection->getId()) ?>]" -+ <?php if ($block->isSelected($_selection)) { echo ' checked="checked"'; } ?> -+ <?php if (!$_selection->isSaleable()) { echo ' disabled="disabled"'; } ?> -+ value="<?= $block->escapeHtmlAttr($_selection->getSelectionId()) ?>"/> - <label class="label" -- for="bundle-option-<?= /* @escapeNotVerified */ $_option->getId() ?>-<?= /* @escapeNotVerified */ $_selection->getSelectionId() ?>"> -- <span><?= /* @escapeNotVerified */ $block->getSelectionQtyTitlePrice($_selection) ?></span> -+ for="bundle-option-<?= $block->escapeHtmlAttr($_option->getId()) ?>-<?= $block->escapeHtmlAttr($_selection->getSelectionId()) ?>"> -+ <span><?= /* @noEscape */ $block->getSelectionQtyTitlePrice($_selection) ?></span> -+ <br/> -+ <?= /* @noEscape */ $block->getTierPriceRenderer()->renderTierPrice($_selection) ?> - </label> - </div> - <?php endforeach; ?> -- <div id="bundle-option-<?= /* @escapeNotVerified */ $_option->getId() ?>-container"></div> -+ <div id="bundle-option-<?= $block->escapeHtmlAttr($_option->getId()) ?>-container"></div> - <?php endif; ?> - </div> - </div> -diff --git a/app/code/Magento/Bundle/view/frontend/templates/catalog/product/view/type/bundle/option/multi.phtml b/app/code/Magento/Bundle/view/frontend/templates/catalog/product/view/type/bundle/option/multi.phtml -index 718d43070a5..d6f9fdb74ef 100644 ---- a/app/code/Magento/Bundle/view/frontend/templates/catalog/product/view/type/bundle/option/multi.phtml -+++ b/app/code/Magento/Bundle/view/frontend/templates/catalog/product/view/type/bundle/option/multi.phtml -@@ -3,39 +3,36 @@ - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -- --// @codingStandardsIgnoreFile -- - ?> - <?php /* @var $block \Magento\Bundle\Block\Catalog\Product\View\Type\Bundle\Option\Multi */ ?> - <?php $_option = $block->getOption() ?> - <?php $_selections = $_option->getSelections() ?> - <div class="field option <?= ($_option->getRequired()) ? ' required': '' ?>"> -- <label class="label" for="bundle-option-<?= /* @escapeNotVerified */ $_option->getId() ?>"> -+ <label class="label" for="bundle-option-<?= $block->escapeHtmlAttr($_option->getId()) ?>"> - <span><?= $block->escapeHtml($_option->getTitle()) ?></span> - </label> - <div class="control"> -- <?php if ($block->showSingle()): ?> -- <?= /* @escapeNotVerified */ $block->getSelectionQtyTitlePrice($_selections[0]) ?> -+ <?php if ($block->showSingle()) : ?> -+ <?= /* @noEscape */ $block->getSelectionQtyTitlePrice($_selections[0]) ?> - <input type="hidden" -- name="bundle_option[<?= /* @escapeNotVerified */ $_option->getId() ?>]" -- value="<?= /* @escapeNotVerified */ $_selections[0]->getSelectionId() ?>"/> -- <?php else: ?> -+ name="bundle_option[<?= $block->escapeHtmlAttr($_option->getId()) ?>]" -+ value="<?= $block->escapeHtmlAttr($_selections[0]->getSelectionId()) ?>"/> -+ <?php else : ?> - <select multiple="multiple" - size="5" -- id="bundle-option-<?= /* @escapeNotVerified */ $_option->getId() ?>" -- name="bundle_option[<?= /* @escapeNotVerified */ $_option->getId() ?>][]" -- data-selector="bundle_option[<?= /* @escapeNotVerified */ $_option->getId() ?>][]" -- class="bundle-option-<?= /* @escapeNotVerified */ $_option->getId() ?> multiselect product bundle option change-container-classname" -- <?php if ($_option->getRequired()) echo 'data-validate={required:true}' ?>> -- <?php if(!$_option->getRequired()): ?> -- <option value=""><?= /* @escapeNotVerified */ __('None') ?></option> -+ id="bundle-option-<?= $block->escapeHtmlAttr($_option->getId()) ?>" -+ name="bundle_option[<?= $block->escapeHtmlAttr($_option->getId()) ?>][]" -+ data-selector="bundle_option[<?= $block->escapeHtmlAttr($_option->getId()) ?>][]" -+ class="bundle-option-<?= $block->escapeHtmlAttr($_option->getId()) ?> multiselect product bundle option change-container-classname" -+ <?php if ($_option->getRequired()) { echo 'data-validate={required:true}'; } ?>> -+ <?php if (!$_option->getRequired()) : ?> -+ <option value=""><?= $block->escapeHtml(__('None')) ?></option> - <?php endif; ?> -- <?php foreach ($_selections as $_selection): ?> -- <option value="<?= /* @escapeNotVerified */ $_selection->getSelectionId() ?>" -- <?php if ($block->isSelected($_selection)) echo ' selected="selected"' ?> -- <?php if (!$_selection->isSaleable()) echo ' disabled="disabled"' ?>> -- <?= /* @escapeNotVerified */ $block->getSelectionQtyTitlePrice($_selection, false) ?> -+ <?php foreach ($_selections as $_selection) : ?> -+ <option value="<?= $block->escapeHtmlAttr($_selection->getSelectionId()) ?>" -+ <?php if ($block->isSelected($_selection)) { echo ' selected="selected"'; } ?> -+ <?php if (!$_selection->isSaleable()) { echo ' disabled="disabled"'; } ?>> -+ <?= /* @noEscape */ $block->getSelectionQtyTitlePrice($_selection, false) ?> - </option> - <?php endforeach; ?> - </select> -diff --git a/app/code/Magento/Bundle/view/frontend/templates/catalog/product/view/type/bundle/option/radio.phtml b/app/code/Magento/Bundle/view/frontend/templates/catalog/product/view/type/bundle/option/radio.phtml -index 7ea89e86098..11ed226c176 100644 ---- a/app/code/Magento/Bundle/view/frontend/templates/catalog/product/view/type/bundle/option/radio.phtml -+++ b/app/code/Magento/Bundle/view/frontend/templates/catalog/product/view/type/bundle/option/radio.phtml -@@ -3,9 +3,6 @@ - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -- --// @codingStandardsIgnoreFile -- - ?> - <?php /* @var $block \Magento\Bundle\Block\Catalog\Product\View\Type\Bundle\Option\Radio */ ?> - <?php $_option = $block->getOption(); ?> -@@ -19,8 +16,9 @@ - </label> - <div class="control"> - <div class="nested options-list"> -- <?php if ($block->showSingle()): ?> -- <?= /* @escapeNotVerified */ $block->getSelectionTitlePrice($_selections[0]) ?> -+ <?php if ($block->showSingle()) : ?> -+ <?= /* @noEscape */ $block->getSelectionTitlePrice($_selections[0]) ?> -+ <?= /* @noEscape */ $block->getTierPriceRenderer()->renderTierPrice($_selections[0]) ?> - <input type="hidden" - class="bundle-option-<?= (int)$_option->getId() ?> product bundle option" - name="bundle_option[<?= (int)$_option->getId() ?>]" -@@ -28,52 +26,54 @@ - id="bundle-option-<?= (int)$_option->getId() ?>-<?= (int)$_selections[0]->getSelectionId() ?>" - checked="checked" - /> -- <?php else:?> -- <?php if (!$_option->getRequired()): ?> -+ <?php else :?> -+ <?php if (!$_option->getRequired()) : ?> - <div class="field choice"> - <input type="radio" - class="radio product bundle option" -- id="bundle-option-<?= /* @escapeNotVerified */ $_option->getId() ?>" -- name="bundle_option[<?= /* @escapeNotVerified */ $_option->getId() ?>]" -- data-selector="bundle_option[<?= /* @escapeNotVerified */ $_option->getId() ?>]" -+ id="bundle-option-<?= $block->escapeHtmlAttr($_option->getId()) ?>" -+ name="bundle_option[<?= $block->escapeHtmlAttr($_option->getId()) ?>]" -+ data-selector="bundle_option[<?= $block->escapeHtmlAttr($_option->getId()) ?>]" - <?= ($_default && $_default->isSalable())?'':' checked="checked" ' ?> - value=""/> -- <label class="label" for="bundle-option-<?= /* @escapeNotVerified */ $_option->getId() ?>"> -- <span><?= /* @escapeNotVerified */ __('None') ?></span> -+ <label class="label" for="bundle-option-<?= $block->escapeHtmlAttr($_option->getId()) ?>"> -+ <span><?= $block->escapeHtml(__('None')) ?></span> - </label> - </div> - <?php endif; ?> -- <?php foreach ($_selections as $_selection): ?> -+ <?php foreach ($_selections as $_selection) : ?> - <div class="field choice"> - <input type="radio" - class="radio product bundle option change-container-classname" -- id="bundle-option-<?= /* @escapeNotVerified */ $_option->getId() ?>-<?= /* @escapeNotVerified */ $_selection->getSelectionId() ?>" -- <?php if ($_option->getRequired()) echo 'data-validate="{\'validate-one-required-by-name\':true}"'?> -- name="bundle_option[<?= /* @escapeNotVerified */ $_option->getId() ?>]" -- data-selector="bundle_option[<?= /* @escapeNotVerified */ $_option->getId() ?>]" -- <?php if ($block->isSelected($_selection)) echo ' checked="checked"' ?> -- <?php if (!$_selection->isSaleable()) echo ' disabled="disabled"' ?> -- value="<?= /* @escapeNotVerified */ $_selection->getSelectionId() ?>"/> -+ id="bundle-option-<?= $block->escapeHtmlAttr($_option->getId()) ?>-<?= $block->escapeHtmlAttr($_selection->getSelectionId()) ?>" -+ <?php if ($_option->getRequired()) { echo 'data-validate="{\'validate-one-required-by-name\':true}"'; }?> -+ name="bundle_option[<?= $block->escapeHtmlAttr($_option->getId()) ?>]" -+ data-selector="bundle_option[<?= $block->escapeHtmlAttr($_option->getId()) ?>]" -+ <?php if ($block->isSelected($_selection)) { echo ' checked="checked"'; } ?> -+ <?php if (!$_selection->isSaleable()) { echo ' disabled="disabled"'; } ?> -+ value="<?= $block->escapeHtmlAttr($_selection->getSelectionId()) ?>"/> - <label class="label" -- for="bundle-option-<?= /* @escapeNotVerified */ $_option->getId() ?>-<?= /* @escapeNotVerified */ $_selection->getSelectionId() ?>"> -- <span><?= /* @escapeNotVerified */ $block->getSelectionTitlePrice($_selection) ?></span> -+ for="bundle-option-<?= $block->escapeHtmlAttr($_option->getId()) ?>-<?= $block->escapeHtmlAttr($_selection->getSelectionId()) ?>"> -+ <span><?= /* @noEscape */ $block->getSelectionTitlePrice($_selection) ?></span> -+ <br/> -+ <?= /* @noEscape */ $block->getTierPriceRenderer()->renderTierPrice($_selection) ?> - </label> - </div> - <?php endforeach; ?> -- <div id="bundle-option-<?= /* @escapeNotVerified */ $_option->getId() ?>-container"></div> -+ <div id="bundle-option-<?= $block->escapeHtmlAttr($_option->getId()) ?>-container"></div> - <?php endif; ?> - <div class="field qty qty-holder"> -- <label class="label" for="bundle-option-<?= /* @escapeNotVerified */ $_option->getId() ?>-qty-input"> -- <span><?= /* @escapeNotVerified */ __('Quantity') ?></span> -+ <label class="label" for="bundle-option-<?= $block->escapeHtmlAttr($_option->getId()) ?>-qty-input"> -+ <span><?= $block->escapeHtml(__('Quantity')) ?></span> - </label> - <div class="control"> -- <input <?php if (!$_canChangeQty) echo ' disabled="disabled"' ?> -- id="bundle-option-<?= /* @escapeNotVerified */ $_option->getId() ?>-qty-input" -- class="input-text qty<?php if (!$_canChangeQty) echo ' qty-disabled' ?>" -+ <input <?php if (!$_canChangeQty) { echo ' disabled="disabled"'; } ?> -+ id="bundle-option-<?= $block->escapeHtmlAttr($_option->getId()) ?>-qty-input" -+ class="input-text qty<?php if (!$_canChangeQty) { echo ' qty-disabled'; } ?>" - type="number" -- name="bundle_option_qty[<?= /* @escapeNotVerified */ $_option->getId() ?>]" -- data-selector="bundle_option_qty[<?= /* @escapeNotVerified */ $_option->getId() ?>]" -- value="<?= /* @escapeNotVerified */ $_defaultQty ?>"/> -+ name="bundle_option_qty[<?= $block->escapeHtmlAttr($_option->getId()) ?>]" -+ data-selector="bundle_option_qty[<?= $block->escapeHtmlAttr($_option->getId()) ?>]" -+ value="<?= $block->escapeHtmlAttr($_defaultQty) ?>"/> - </div> - </div> - </div> -diff --git a/app/code/Magento/Bundle/view/frontend/templates/catalog/product/view/type/bundle/option/select.phtml b/app/code/Magento/Bundle/view/frontend/templates/catalog/product/view/type/bundle/option/select.phtml -index 977daa2b2a4..1edf45fe8ca 100644 ---- a/app/code/Magento/Bundle/view/frontend/templates/catalog/product/view/type/bundle/option/select.phtml -+++ b/app/code/Magento/Bundle/view/frontend/templates/catalog/product/view/type/bundle/option/select.phtml -@@ -3,9 +3,6 @@ - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -- --// @codingStandardsIgnoreFile -- - ?> - - <?php /* @var $block \Magento\Bundle\Block\Catalog\Product\View\Type\Bundle\Option\Select */ ?> -@@ -14,45 +11,55 @@ - <?php $_default = $_option->getDefaultSelection(); ?> - <?php list($_defaultQty, $_canChangeQty) = $block->getDefaultValues(); ?> - <div class="field option <?= ($_option->getRequired()) ? ' required': '' ?>"> -- <label class="label" for="bundle-option-<?= /* @escapeNotVerified */ $_option->getId() ?>"> -+ <label class="label" for="bundle-option-<?= $block->escapeHtmlAttr($_option->getId()) ?>"> - <span><?= $block->escapeHtml($_option->getTitle()) ?></span> - </label> - <div class="control"> -- <?php if ($block->showSingle()): ?> -- <?= /* @escapeNotVerified */ $block->getSelectionTitlePrice($_selections[0]) ?> -+ <?php if ($block->showSingle()) : ?> -+ <?= /* @noEscape */ $block->getSelectionTitlePrice($_selections[0]) ?> -+ <?= /* @noEscape */ $block->getTierPriceRenderer()->renderTierPrice($_selections[0]) ?> - <input type="hidden" -- class="bundle-option-<?= /* @escapeNotVerified */ $_option->getId() ?> product bundle option" -- name="bundle_option[<?= /* @escapeNotVerified */ $_option->getId() ?>]" -- value="<?= /* @escapeNotVerified */ $_selections[0]->getSelectionId() ?>"/> -- <?php else:?> -- <select id="bundle-option-<?= /* @escapeNotVerified */ $_option->getId() ?>" -- name="bundle_option[<?= /* @escapeNotVerified */ $_option->getId() ?>]" -- data-selector="bundle_option[<?= /* @escapeNotVerified */ $_option->getId() ?>]" -- class="bundle-option-<?= /* @escapeNotVerified */ $_option->getId() ?> product bundle option bundle-option-select change-container-classname" -- <?php if ($_option->getRequired()) echo 'data-validate = {required:true}' ?>> -- <option value=""><?= /* @escapeNotVerified */ __('Choose a selection...') ?></option> -- <?php foreach ($_selections as $_selection): ?> -- <option value="<?= /* @escapeNotVerified */ $_selection->getSelectionId() ?>" -- <?php if ($block->isSelected($_selection)) echo ' selected="selected"' ?> -- <?php if (!$_selection->isSaleable()) echo ' disabled="disabled"' ?>> -- <?= /* @escapeNotVerified */ $block->getSelectionTitlePrice($_selection, false) ?> -+ class="bundle-option-<?= $block->escapeHtmlAttr($_option->getId()) ?> product bundle option" -+ name="bundle_option[<?= $block->escapeHtmlAttr($_option->getId()) ?>]" -+ value="<?= $block->escapeHtmlAttr($_selections[0]->getSelectionId()) ?>"/> -+ <?php else :?> -+ <select id="bundle-option-<?= $block->escapeHtmlAttr($_option->getId()) ?>" -+ name="bundle_option[<?= $block->escapeHtmlAttr($_option->getId()) ?>]" -+ data-selector="bundle_option[<?= $block->escapeHtmlAttr($_option->getId()) ?>]" -+ class="bundle-option-<?= $block->escapeHtmlAttr($_option->getId()) ?> product bundle option bundle-option-select change-container-classname" -+ <?php if ($_option->getRequired()) { echo 'data-validate = {required:true}'; } ?>> -+ <option value=""><?= $block->escapeHtml(__('Choose a selection...')) ?></option> -+ <?php foreach ($_selections as $_selection) : ?> -+ <option value="<?= $block->escapeHtmlAttr($_selection->getSelectionId()) ?>" -+ <?php if ($block->isSelected($_selection)) { echo ' selected="selected"'; } ?> -+ <?php if (!$_selection->isSaleable()) { echo ' disabled="disabled"'; } ?>> -+ <?= /* @noEscape */ $block->getSelectionTitlePrice($_selection, false) ?> - </option> - <?php endforeach; ?> - </select> -+ <div id="option-tier-prices-<?= $block->escapeHtmlAttr($_option->getId()) ?>" class="option-tier-prices"> -+ <?php foreach ($_selections as $_selection) : ?> -+ <div data-role="selection-tier-prices" -+ data-selection-id="<?= $block->escapeHtmlAttr($_selection->getSelectionId()) ?>" -+ class="selection-tier-prices"> -+ <?= /* @noEscape */ $block->getTierPriceRenderer()->renderTierPrice($_selection) ?> -+ </div> -+ <?php endforeach; ?> -+ </div> - <?php endif; ?> - <div class="nested"> - <div class="field qty qty-holder"> -- <label class="label" for="bundle-option-<?= /* @escapeNotVerified */ $_option->getId() ?>-qty-input"> -- <span><?= /* @escapeNotVerified */ __('Quantity') ?></span> -+ <label class="label" for="bundle-option-<?= $block->escapeHtmlAttr($_option->getId()) ?>-qty-input"> -+ <span><?= $block->escapeHtml(__('Quantity')) ?></span> - </label> - <div class="control"> -- <input <?php if (!$_canChangeQty) echo ' disabled="disabled"' ?> -- id="bundle-option-<?= /* @escapeNotVerified */ $_option->getId() ?>-qty-input" -- class="input-text qty<?php if (!$_canChangeQty) echo ' qty-disabled' ?>" -+ <input <?php if (!$_canChangeQty) { echo ' disabled="disabled"'; } ?> -+ id="bundle-option-<?= $block->escapeHtmlAttr($_option->getId()) ?>-qty-input" -+ class="input-text qty<?php if (!$_canChangeQty) { echo ' qty-disabled'; } ?>" - type="number" -- name="bundle_option_qty[<?= /* @escapeNotVerified */ $_option->getId() ?>]" -- data-selector="bundle_option_qty[<?= /* @escapeNotVerified */ $_option->getId() ?>]" -- value="<?= /* @escapeNotVerified */ $_defaultQty ?>"/> -+ name="bundle_option_qty[<?= $block->escapeHtmlAttr($_option->getId()) ?>]" -+ data-selector="bundle_option_qty[<?= $block->escapeHtmlAttr($_option->getId()) ?>]" -+ value="<?= $block->escapeHtmlAttr($_defaultQty) ?>"/> - </div> - </div> - </div> -diff --git a/app/code/Magento/Bundle/view/frontend/templates/catalog/product/view/type/bundle/options.phtml b/app/code/Magento/Bundle/view/frontend/templates/catalog/product/view/type/bundle/options.phtml -index 157e2d95972..cac96a8aca7 100644 ---- a/app/code/Magento/Bundle/view/frontend/templates/catalog/product/view/type/bundle/options.phtml -+++ b/app/code/Magento/Bundle/view/frontend/templates/catalog/product/view/type/bundle/options.phtml -@@ -3,24 +3,22 @@ - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -- --// @codingStandardsIgnoreFile -- -+// phpcs:disable Magento2.Templates.ThisInTemplate.FoundThis - ?> - <?php /** @var $block Magento\Bundle\Block\Catalog\Product\View\Type\Bundle */ ?> - <?php - $product = $block->getProduct(); --$helper = $this->helper('Magento\Catalog\Helper\Output'); -+$helper = $this->helper(Magento\Catalog\Helper\Output::class); - $stripSelection = $product->getConfigureMode() ? true : false; - $options = $block->decorateArray($block->getOptions($stripSelection)); - ?> --<?php if ($product->isSaleable()):?> -- <?php if (count($options)): ?> -+<?php if ($product->isSaleable()) :?> -+ <?php if (count($options)) : ?> - <script type="text/x-magento-init"> - { - "#product_addtocart_form": { - "priceBundle": { -- "optionConfig": <?= /* @escapeNotVerified */ $block->getJsonConfig() ?>, -+ "optionConfig": <?= /* @noEscape */ $block->getJsonConfig() ?>, - "controlContainer": ".field.option" - } - } -@@ -28,17 +26,20 @@ $options = $block->decorateArray($block->getOptions($stripSelection)); - </script> - <fieldset class="fieldset fieldset-bundle-options"> - <legend id="customizeTitle" class="legend title"> -- <span><?= /* @escapeNotVerified */ __('Customize %1', $helper->productAttribute($product, $product->getName(), 'name')) ?></span> -+ <span><?= /* @noEscape */ __('Customize %1', $helper->productAttribute($product, $product->getName(), 'name')) ?></span> - </legend><br /> - <?= $block->getChildHtml('product_info_bundle_options_top') ?> -- <?php foreach ($options as $option): ?> -- <?php if (!$option->getSelections()): ?> -- <?php continue; ?> -- <?php endif; ?> -- <?= $block->getOptionHtml($option) ?> -+ <?php foreach ($options as $option) : ?> -+ <?php -+ if (!$option->getSelections()) { -+ continue; -+ } else { -+ echo $block->getOptionHtml($option); -+ } -+ ?> - <?php endforeach; ?> - </fieldset> -- <?php else: ?> -- <p class="empty"><?= /* @escapeNotVerified */ __('No options of this product are available.') ?></p> -+ <?php else : ?> -+ <p class="empty"><?= $block->escapeHtml(__('No options of this product are available.')) ?></p> - <?php endif; ?> - <?php endif;?> -diff --git a/app/code/Magento/Bundle/view/frontend/templates/email/order/items/creditmemo/default.phtml b/app/code/Magento/Bundle/view/frontend/templates/email/order/items/creditmemo/default.phtml -index a87c2167e66..cbf7755ae99 100644 ---- a/app/code/Magento/Bundle/view/frontend/templates/email/order/items/creditmemo/default.phtml -+++ b/app/code/Magento/Bundle/view/frontend/templates/email/order/items/creditmemo/default.phtml -@@ -3,9 +3,7 @@ - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -- --// @codingStandardsIgnoreFile -- -+// phpcs:disable Magento2.Templates.ThisInTemplate.FoundThis - ?> - <?php /** @var $block \Magento\Bundle\Block\Sales\Order\Items\Renderer */ ?> - <?php $parentItem = $block->getItem() ?> -@@ -13,15 +11,15 @@ - - <?php $items = $block->getChildren($parentItem) ?> - --<?php if ($block->getItemOptions() || $parentItem->getDescription() || $this->helper('Magento\GiftMessage\Helper\Message')->isMessagesAllowed('order_item', $parentItem) && $parentItem->getGiftMessageId()): ?> -+<?php if ($block->getItemOptions() || $parentItem->getDescription() || $this->helper(Magento\GiftMessage\Helper\Message::class)->isMessagesAllowed('order_item', $parentItem) && $parentItem->getGiftMessageId()) : ?> - <?php $_showlastRow = true ?> --<?php else: ?> -+<?php else : ?> - <?php $_showlastRow = false ?> - <?php endif; ?> - - <?php $_prevOptionId = '' ?> - --<?php foreach ($items as $_item): ?> -+<?php foreach ($items as $_item) : ?> - - <?php - // As part of invoice item renderer logic, the order is set on each invoice item. -@@ -30,40 +28,40 @@ - $_item->setOrder($_order); - ?> - -- <?php if ($_item->getOrderItem()->getParentItem()): ?> -+ <?php if ($_item->getOrderItem()->getParentItem()) : ?> - <?php $attributes = $block->getSelectionAttributes($_item) ?> -- <?php if ($_prevOptionId != $attributes['option_id']): ?> -+ <?php if ($_prevOptionId != $attributes['option_id']) : ?> - <tr class="bundle-option-label"> - <td colspan="3"> -- <strong><?= /* @escapeNotVerified */ $attributes['option_label'] ?></strong> -+ <strong><?= $block->escapeHtml($attributes['option_label']) ?></strong> - </td> - </tr> - <?php $_prevOptionId = $attributes['option_id'] ?> - <?php endif; ?> - <?php endif; ?> -- <?php if (!$_item->getOrderItem()->getParentItem()): ?> -+ <?php if (!$_item->getOrderItem()->getParentItem()) : ?> - <tr class="bundle-item bundle-parent"> - <td class="item-info"> - <p class="product-name"><?= $block->escapeHtml($_item->getName()) ?></p> -- <p class="sku"><?= /* @escapeNotVerified */ __('SKU') ?>: <?= $block->escapeHtml($block->getSku($_item)) ?></p> -+ <p class="sku"><?= $block->escapeHtml(__('SKU')) ?>: <?= $block->escapeHtml($block->getSku($_item)) ?></p> - </td> -- <?php else: ?> -+ <?php else : ?> - <tr class="bundle-item bundle-option-value"> - <td class="item-info"> - <p><?= $block->getValueHtml($_item) ?></p> - </td> - <?php endif; ?> - <td class="item-qty"> -- <?php if ($block->canShowPriceInfo($_item)): ?> -- <?= /* @escapeNotVerified */ $_item->getQty() * 1 ?> -- <?php else: ?> -+ <?php if ($block->canShowPriceInfo($_item)) : ?> -+ <?= (float)$_item->getQty() * 1 ?> -+ <?php else : ?> -   - <?php endif; ?> - </td> - <td class="item-price"> -- <?php if ($block->canShowPriceInfo($_item)): ?> -- <?= /* @escapeNotVerified */ $block->getItemPrice($_item) ?> -- <?php else: ?> -+ <?php if ($block->canShowPriceInfo($_item)) : ?> -+ <?= /* @noEscape */ $block->getItemPrice($_item) ?> -+ <?php else : ?> -   - <?php endif; ?> - </td> -@@ -71,14 +69,14 @@ - - <?php endforeach; ?> - --<?php if ($_showlastRow): ?> -+<?php if ($_showlastRow) : ?> - <tr> - <td colspan="3" class="item-extra"> -- <?php if ($block->getItemOptions()): ?> -+ <?php if ($block->getItemOptions()) : ?> - <dl> -- <?php foreach ($block->getItemOptions() as $option): ?> -- <dt><strong><em><?= /* @escapeNotVerified */ $option['label'] ?></em></strong></dt> -- <dd><?= /* @escapeNotVerified */ $option['value'] ?></dd> -+ <?php foreach ($block->getItemOptions() as $option) : ?> -+ <dt><strong><em><?= $block->escapeHtml($option['label']) ?></em></strong></dt> -+ <dd><?= $block->escapeHtml($option['value']) ?></dd> - <?php endforeach; ?> - </dl> - <?php endif; ?> -diff --git a/app/code/Magento/Bundle/view/frontend/templates/email/order/items/invoice/default.phtml b/app/code/Magento/Bundle/view/frontend/templates/email/order/items/invoice/default.phtml -index ee79ca3b0a7..d24fa390fa0 100644 ---- a/app/code/Magento/Bundle/view/frontend/templates/email/order/items/invoice/default.phtml -+++ b/app/code/Magento/Bundle/view/frontend/templates/email/order/items/invoice/default.phtml -@@ -3,9 +3,7 @@ - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -- --// @codingStandardsIgnoreFile -- -+// phpcs:disable Magento2.Templates.ThisInTemplate.FoundThis - ?> - <?php /** @var $block \Magento\Bundle\Block\Sales\Order\Items\Renderer */ ?> - -@@ -14,15 +12,15 @@ - <?php $_index = 0 ?> - <?php $_order = $block->getItem()->getOrder(); ?> - --<?php if ($block->getItemOptions() || $parentItem->getDescription() || $this->helper('Magento\GiftMessage\Helper\Message')->isMessagesAllowed('order_item', $parentItem) && $parentItem->getGiftMessageId()): ?> -+<?php if ($block->getItemOptions() || $parentItem->getDescription() || $this->helper(Magento\GiftMessage\Helper\Message::class)->isMessagesAllowed('order_item', $parentItem) && $parentItem->getGiftMessageId()) : ?> - <?php $_showlastRow = true ?> --<?php else: ?> -+<?php else : ?> - <?php $_showlastRow = false ?> - <?php endif; ?> - - <?php $_prevOptionId = '' ?> - --<?php foreach ($items as $_item): ?> -+<?php foreach ($items as $_item) : ?> - - <?php - // As part of invoice item renderer logic, the order is set on each invoice item. -@@ -31,40 +29,40 @@ - $_item->setOrder($_order); - ?> - -- <?php if ($_item->getOrderItem()->getParentItem()): ?> -+ <?php if ($_item->getOrderItem()->getParentItem()) : ?> - <?php $attributes = $block->getSelectionAttributes($_item) ?> -- <?php if ($_prevOptionId != $attributes['option_id']): ?> -+ <?php if ($_prevOptionId != $attributes['option_id']) : ?> - <tr class="bundle-option-label"> - <td colspan="3"> -- <strong><em><?= /* @escapeNotVerified */ $attributes['option_label'] ?></em></strong> -+ <strong><em><?= $block->escapeHtml($attributes['option_label']) ?></em></strong> - </td> - </tr> - <?php $_prevOptionId = $attributes['option_id'] ?> - <?php endif; ?> - <?php endif; ?> -- <?php if (!$_item->getOrderItem()->getParentItem()): ?> -+ <?php if (!$_item->getOrderItem()->getParentItem()) : ?> - <tr class="bundle-item bundle-parent"> - <td class="item-info"> - <p class="product-name"><?= $block->escapeHtml($_item->getName()) ?></p> -- <p class="sku"><?= /* @escapeNotVerified */ __('SKU') ?>: <?= $block->escapeHtml($block->getSku($_item)) ?></p> -+ <p class="sku"><?= $block->escapeHtml(__('SKU')) ?>: <?= $block->escapeHtml($block->getSku($_item)) ?></p> - </td> -- <?php else: ?> -+ <?php else : ?> - <tr class="bundle-item bundle-option-value"> - <td class="item-info"> - <p><?= $block->getValueHtml($_item) ?></p> - </td> - <?php endif; ?> - <td class="item-qty"> -- <?php if ($block->canShowPriceInfo($_item)): ?> -- <?= /* @escapeNotVerified */ $_item->getQty() * 1 ?> -- <?php else: ?> -+ <?php if ($block->canShowPriceInfo($_item)) : ?> -+ <?= (float)$_item->getQty() * 1 ?> -+ <?php else : ?> -   - <?php endif; ?> - </td> - <td class="item-price"> -- <?php if ($block->canShowPriceInfo($_item)): ?> -- <?= /* @escapeNotVerified */ $block->getItemPrice($_item) ?> -- <?php else: ?> -+ <?php if ($block->canShowPriceInfo($_item)) : ?> -+ <?= /* @noEscape */ $block->getItemPrice($_item) ?> -+ <?php else : ?> -   - <?php endif; ?> - </td> -@@ -72,14 +70,14 @@ - - <?php endforeach; ?> - --<?php if ($_showlastRow): ?> -+<?php if ($_showlastRow) : ?> - <tr> - <td colspan="3" class="item-extra"> -- <?php if ($block->getItemOptions()): ?> -+ <?php if ($block->getItemOptions()) : ?> - <dl> -- <?php foreach ($block->getItemOptions() as $option): ?> -- <dt><strong><em><?= /* @escapeNotVerified */ $option['label'] ?></em></strong></dt> -- <dd><?= /* @escapeNotVerified */ $option['value'] ?></dd> -+ <?php foreach ($block->getItemOptions() as $option) : ?> -+ <dt><strong><em><?= $block->escapeHtml($option['label']) ?></em></strong></dt> -+ <dd><?= $block->escapeHtml($option['value']) ?></dd> - <?php endforeach; ?> - </dl> - <?php endif; ?> -diff --git a/app/code/Magento/Bundle/view/frontend/templates/email/order/items/order/default.phtml b/app/code/Magento/Bundle/view/frontend/templates/email/order/items/order/default.phtml -index c9c3103c448..94dc0917fcf 100644 ---- a/app/code/Magento/Bundle/view/frontend/templates/email/order/items/order/default.phtml -+++ b/app/code/Magento/Bundle/view/frontend/templates/email/order/items/order/default.phtml -@@ -3,9 +3,7 @@ - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -- --// @codingStandardsIgnoreFile -- -+// phpcs:disable Magento2.Templates.ThisInTemplate.FoundThis - ?> - <?php /** @var $block \Magento\Bundle\Block\Sales\Order\Items\Renderer */ ?> - <?php $_item = $block->getItem() ?> -@@ -14,41 +12,41 @@ - <?php $parentItem = $block->getItem() ?> - <?php $items = array_merge([$parentItem], $parentItem->getChildrenItems()); ?> - --<?php if ($block->getItemOptions() || $_item->getDescription() || $this->helper('Magento\GiftMessage\Helper\Message')->isMessagesAllowed('order_item', $_item) && $_item->getGiftMessageId()): ?> -+<?php if ($block->getItemOptions() || $_item->getDescription() || $this->helper(Magento\GiftMessage\Helper\Message::class)->isMessagesAllowed('order_item', $_item) && $_item->getGiftMessageId()) : ?> - <?php $_showlastRow = true ?> --<?php else: ?> -+<?php else : ?> - <?php $_showlastRow = false ?> - <?php endif; ?> - - <?php $_prevOptionId = '' ?> - --<?php foreach ($items as $_item): ?> -+<?php foreach ($items as $_item) : ?> - -- <?php if ($_item->getParentItem()): ?> -+ <?php if ($_item->getParentItem()) : ?> - <?php $attributes = $block->getSelectionAttributes($_item) ?> -- <?php if ($_prevOptionId != $attributes['option_id']): ?> -+ <?php if ($_prevOptionId != $attributes['option_id']) : ?> - <tr class="bundle-option-label"> - <td colspan="3"> -- <strong><em><?= /* @escapeNotVerified */ $attributes['option_label'] ?></em></strong> -+ <strong><em><?= $block->escapeHtml($attributes['option_label']) ?></em></strong> - </td> - </tr> - <?php $_prevOptionId = $attributes['option_id'] ?> - <?php endif; ?> - <?php endif; ?> -- <?php if (!$_item->getParentItem()): ?> -+ <?php if (!$_item->getParentItem()) : ?> - <tr class="bundle-item bundle-parent"> - <td class="item-info"> - <p class="product-name"><?= $block->escapeHtml($_item->getName()) ?></p> -- <p class="sku"><?= /* @escapeNotVerified */ __('SKU') ?>: <?= $block->escapeHtml($block->getSku($_item)) ?></p> -+ <p class="sku"><?= $block->escapeHtml(__('SKU')) ?>: <?= $block->escapeHtml($block->getSku($_item)) ?></p> - </td> - <td class="item-qty"> -- <?= /* @escapeNotVerified */ $_item->getQtyOrdered() * 1 ?> -+ <?= (float)$_item->getQtyOrdered() * 1 ?> - </td> - <td class="item-price"> -- <?= /* @escapeNotVerified */ $block->getItemPrice($_item) ?> -+ <?= /* @noEscape */ $block->getItemPrice($_item) ?> - </td> - </tr> -- <?php else: ?> -+ <?php else : ?> - <tr class="bundle-item bundle-option-value"> - <td class="item-info" colspan="3"> - <p><?= $block->getValueHtml($_item) ?></p> -@@ -58,25 +56,25 @@ - - <?php endforeach; ?> - --<?php if ($_showlastRow): ?> -+<?php if ($_showlastRow) : ?> - <tr> - <td colspan="3" class="item-extra"> -- <?php if ($block->getItemOptions()): ?> -+ <?php if ($block->getItemOptions()) : ?> - <dl> -- <?php foreach ($block->getItemOptions() as $option): ?> -- <dt><strong><em><?= /* @escapeNotVerified */ $option['label'] ?></em></strong></dt> -- <dd><?= /* @escapeNotVerified */ $option['value'] ?></dd> -+ <?php foreach ($block->getItemOptions() as $option) : ?> -+ <dt><strong><em><?= $block->escapeHtml($option['label']) ?></em></strong></dt> -+ <dd><?= $block->escapeHtml($option['value']) ?></dd> - <?php endforeach; ?> - </dl> - <?php endif; ?> -- <?php if ($_item->getGiftMessageId() && $_giftMessage = $this->helper('Magento\GiftMessage\Helper\Message')->getGiftMessage($_item->getGiftMessageId())): ?> -+ <?php if ($_item->getGiftMessageId() && $_giftMessage = $this->helper(Magento\GiftMessage\Helper\Message::class)->getGiftMessage($_item->getGiftMessageId())) : ?> - <table class="message-gift"> - <tr> - <td> -- <h3><?= /* @escapeNotVerified */ __('Gift Message') ?></h3> -- <strong><?= /* @escapeNotVerified */ __('From:') ?></strong> <?= $block->escapeHtml($_giftMessage->getSender()) ?> -- <br /><strong><?= /* @escapeNotVerified */ __('To:') ?></strong> <?= $block->escapeHtml($_giftMessage->getRecipient()) ?> -- <br /><strong><?= /* @escapeNotVerified */ __('Message:') ?></strong> -+ <h3><?= $block->escapeHtml(__('Gift Message')) ?></h3> -+ <strong><?= $block->escapeHtml(__('From:')) ?></strong> <?= $block->escapeHtml($_giftMessage->getSender()) ?> -+ <br /><strong><?= $block->escapeHtml(__('To:')) ?></strong> <?= $block->escapeHtml($_giftMessage->getRecipient()) ?> -+ <br /><strong><?= $block->escapeHtml(__('Message:')) ?></strong> - <br /><?= $block->escapeHtml($_giftMessage->getMessage()) ?> - </td> - </tr> -diff --git a/app/code/Magento/Bundle/view/frontend/templates/email/order/items/shipment/default.phtml b/app/code/Magento/Bundle/view/frontend/templates/email/order/items/shipment/default.phtml -index 61582087d1f..bf84ad33b92 100644 ---- a/app/code/Magento/Bundle/view/frontend/templates/email/order/items/shipment/default.phtml -+++ b/app/code/Magento/Bundle/view/frontend/templates/email/order/items/shipment/default.phtml -@@ -3,9 +3,7 @@ - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -- --// @codingStandardsIgnoreFile -- -+// phpcs:disable Magento2.Templates.ThisInTemplate.FoundThis - ?> - <?php /** @var $block \Magento\Bundle\Block\Sales\Order\Items\Renderer */ ?> - -@@ -14,49 +12,49 @@ - <?php $items = array_merge([$parentItem->getOrderItem()], $parentItem->getOrderItem()->getChildrenItems()) ?> - <?php $shipItems = $block->getChildren($parentItem) ?> - --<?php if ($block->getItemOptions() || $parentItem->getDescription() || $this->helper('Magento\GiftMessage\Helper\Message')->isMessagesAllowed('order_item', $parentItem) && $parentItem->getGiftMessageId()): ?> -+<?php if ($block->getItemOptions() || $parentItem->getDescription() || $this->helper(Magento\GiftMessage\Helper\Message::class)->isMessagesAllowed('order_item', $parentItem) && $parentItem->getGiftMessageId()) : ?> - <?php $_showlastRow = true ?> --<?php else: ?> -+<?php else : ?> - <?php $_showlastRow = false ?> - <?php endif; ?> - - <?php $_prevOptionId = '' ?> - --<?php foreach ($items as $_item): ?> -+<?php foreach ($items as $_item) : ?> - -- <?php if ($_item->getParentItem()): ?> -+ <?php if ($_item->getParentItem()) : ?> - <?php $attributes = $block->getSelectionAttributes($_item) ?> -- <?php if ($_prevOptionId != $attributes['option_id']): ?> -+ <?php if ($_prevOptionId != $attributes['option_id']) : ?> - <tr class="bundle-option-label"> - <td colspan="2"> -- <strong><em><?= /* @escapeNotVerified */ $attributes['option_label'] ?></em></strong> -+ <strong><em><?= $block->escapeHtml($attributes['option_label']) ?></em></strong> - </td> - </tr> - <?php $_prevOptionId = $attributes['option_id'] ?> - <?php endif; ?> - <?php endif; ?> -- <?php if (!$_item->getParentItem()): ?> -+ <?php if (!$_item->getParentItem()) : ?> - <tr class="bundle-item bundle-parent"> - <td class="item-info"> - <p class="product-name"><?= $block->escapeHtml($_item->getName()) ?></p> -- <p class="sku"><?= /* @escapeNotVerified */ __('SKU') ?>: <?= $block->escapeHtml($block->getSku($_item)) ?></p> -+ <p class="sku"><?= $block->escapeHtml(__('SKU')) ?>: <?= $block->escapeHtml($block->getSku($_item)) ?></p> - </td> -- <?php else: ?> -+ <?php else : ?> - <tr class="bundle-item bundle-option-value"> - <td class="item-info"> - <p><?= $block->getValueHtml($_item) ?></p> - </td> - <?php endif; ?> - <td class="item-qty"> -- <?php if (($block->isShipmentSeparately() && $_item->getParentItem()) || (!$block->isShipmentSeparately() && !$_item->getParentItem())): ?> -- <?php if (isset($shipItems[$_item->getId()])): ?> -- <?= /* @escapeNotVerified */ $shipItems[$_item->getId()]->getQty() * 1 ?> -- <?php elseif ($_item->getIsVirtual()): ?> -- <?= /* @escapeNotVerified */ __('N/A') ?> -- <?php else: ?> -+ <?php if (($block->isShipmentSeparately() && $_item->getParentItem()) || (!$block->isShipmentSeparately() && !$_item->getParentItem())) : ?> -+ <?php if (isset($shipItems[$_item->getId()])) : ?> -+ <?= (float)$shipItems[$_item->getId()]->getQty() * 1 ?> -+ <?php elseif ($_item->getIsVirtual()) : ?> -+ <?= $block->escapeHtml(__('N/A')) ?> -+ <?php else : ?> - 0 - <?php endif; ?> -- <?php else: ?> -+ <?php else : ?> -   - <?php endif; ?> - </td> -@@ -64,14 +62,14 @@ - - <?php endforeach; ?> - --<?php if ($_showlastRow): ?> -+<?php if ($_showlastRow) : ?> - <tr> - <td colspan="2" class="item-extra"> -- <?php if ($block->getItemOptions()): ?> -+ <?php if ($block->getItemOptions()) : ?> - <dl> -- <?php foreach ($block->getItemOptions() as $option): ?> -- <dt><strong><em><?= /* @escapeNotVerified */ $option['label'] ?></em></strong></dt> -- <dd><?= /* @escapeNotVerified */ $option['value'] ?></dd> -+ <?php foreach ($block->getItemOptions() as $option) : ?> -+ <dt><strong><em><?= $block->escapeHtml($option['label']) ?></em></strong></dt> -+ <dd><?= $block->escapeHtml($option['value']) ?></dd> - <?php endforeach; ?> - </dl> - <?php endif; ?> -diff --git a/app/code/Magento/Bundle/view/frontend/templates/js/components.phtml b/app/code/Magento/Bundle/view/frontend/templates/js/components.phtml -index bad5acc209b..1bd7e554a73 100644 ---- a/app/code/Magento/Bundle/view/frontend/templates/js/components.phtml -+++ b/app/code/Magento/Bundle/view/frontend/templates/js/components.phtml -@@ -3,8 +3,6 @@ - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -- --// @codingStandardsIgnoreFile -- - ?> - <?= $block->getChildHtml() ?> -+ -diff --git a/app/code/Magento/Bundle/view/frontend/templates/sales/order/creditmemo/items/renderer.phtml b/app/code/Magento/Bundle/view/frontend/templates/sales/order/creditmemo/items/renderer.phtml -index b9d075966c5..64e6594b4f7 100644 ---- a/app/code/Magento/Bundle/view/frontend/templates/sales/order/creditmemo/items/renderer.phtml -+++ b/app/code/Magento/Bundle/view/frontend/templates/sales/order/creditmemo/items/renderer.phtml -@@ -3,9 +3,7 @@ - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -- --// @codingStandardsIgnoreFile -- -+// phpcs:disable Magento2.Templates.ThisInTemplate.FoundThis - ?> - <?php /** @var $block \Magento\Bundle\Block\Sales\Order\Items\Renderer */ ?> - <?php $parentItem = $block->getItem() ?> -@@ -16,91 +14,95 @@ - - <?php $_prevOptionId = '' ?> - --<?php foreach ($items as $_item): ?> -+<?php foreach ($items as $_item) : ?> - -- <?php if ($block->getItemOptions() || $parentItem->getDescription() || $this->helper('Magento\GiftMessage\Helper\Message')->isMessagesAllowed('order_item', $parentItem) && $parentItem->getGiftMessageId()): ?> -+ <?php if ($block->getItemOptions() || $parentItem->getDescription() || $this->helper(Magento\GiftMessage\Helper\Message::class)->isMessagesAllowed('order_item', $parentItem) && $parentItem->getGiftMessageId()) : ?> - <?php $_showlastRow = true ?> -- <?php else: ?> -+ <?php else : ?> - <?php $_showlastRow = false ?> - <?php endif; ?> - -- <?php if ($_item->getOrderItem()->getParentItem()): ?> -+ <?php if ($_item->getOrderItem()->getParentItem()) : ?> - <?php $attributes = $block->getSelectionAttributes($_item) ?> -- <?php if ($_prevOptionId != $attributes['option_id']): ?> -+ <?php if ($_prevOptionId != $attributes['option_id']) : ?> - <tr class="options-label"> -- <td class="col label" colspan="7"><div class="option label"><?= /* @escapeNotVerified */ $attributes['option_label'] ?></div></td> -+ <td class="col label" colspan="7"><div class="option label"><?= $block->escapeHtml($attributes['option_label']) ?></div></td> - </tr> - <?php $_prevOptionId = $attributes['option_id'] ?> - <?php endif; ?> - <?php endif; ?> --<tr id="order-item-row-<?= /* @escapeNotVerified */ $_item->getId() ?>" class="<?php if ($_item->getOrderItem()->getParentItem()): ?>item-options-container<?php else: ?>item-parent<?php endif; ?>"<?php if ($_item->getParentItem()): ?> data-th="<?= /* @escapeNotVerified */ $attributes['option_label'] ?>"<?php endif; ?>> -- <?php if (!$_item->getOrderItem()->getParentItem()): ?> -- <td class="col name" data-th="<?= $block->escapeHtml(__('Product Name')) ?>"> -+<tr id="order-item-row-<?= $block->escapeHtmlAttr($_item->getId()) ?>" -+ class="<?php if ($_item->getOrderItem()->getParentItem()) : ?>item-options-container<?php else : ?>item-parent<?php endif; ?>" -+ <?php if ($_item->getParentItem()) : ?> -+ data-th="<?= $block->escapeHtmlAttr($attributes['option_label']) ?>" -+ <?php endif; ?>> -+ <?php if (!$_item->getOrderItem()->getParentItem()) : ?> -+ <td class="col name" data-th="<?= $block->escapeHtmlAttr(__('Product Name')) ?>"> - <strong class="product name product-item-name"><?= $block->escapeHtml($_item->getName()) ?></strong> - </td> -- <?php else: ?> -- <td class="col value" data-th="<?= $block->escapeHtml(__('Product Name')) ?>"><?= $block->getValueHtml($_item) ?></td> -+ <?php else : ?> -+ <td class="col value" data-th="<?= $block->escapeHtmlAttr(__('Product Name')) ?>"><?= $block->getValueHtml($_item) ?></td> - <?php endif; ?> -- <td class="col sku" data-th="<?= $block->escapeHtml(__('SKU')) ?>"><?= $block->escapeHtml($_item->getSku()) ?></td> -- <td class="col price" data-th="<?= $block->escapeHtml(__('Price')) ?>"> -- <?php if ($block->canShowPriceInfo($_item)): ?> -+ <td class="col sku" data-th="<?= $block->escapeHtmlAttr(__('SKU')) ?>"><?= $block->escapeHtml($_item->getSku()) ?></td> -+ <td class="col price" data-th="<?= $block->escapeHtmlAttr(__('Price')) ?>"> -+ <?php if ($block->canShowPriceInfo($_item)) : ?> - <?= $block->getItemPriceHtml($_item) ?> -- <?php else: ?> -+ <?php else : ?> -   - <?php endif; ?> - </td> -- <td class="col qty" data-th="<?= $block->escapeHtml(__('Quantity')) ?>"> -- <?php if ($block->canShowPriceInfo($_item)): ?> -- <?= /* @escapeNotVerified */ $_item->getQty()*1 ?> -- <?php else: ?> -+ <td class="col qty" data-th="<?= $block->escapeHtmlAttr(__('Quantity')) ?>"> -+ <?php if ($block->canShowPriceInfo($_item)) : ?> -+ <?= (float)$_item->getQty() * 1 ?> -+ <?php else : ?> -   - <?php endif; ?> - </td> -- <td class="col subtotal" data-th="<?= $block->escapeHtml(__('Subtotal')) ?>"> -- <?php if ($block->canShowPriceInfo($_item)): ?> -+ <td class="col subtotal" data-th="<?= $block->escapeHtmlAttr(__('Subtotal')) ?>"> -+ <?php if ($block->canShowPriceInfo($_item)) : ?> - <?= $block->getItemRowTotalHtml($_item) ?> -- <?php else: ?> -+ <?php else : ?> -   - <?php endif; ?> - </td> -- <td class="col discount" data-th="<?= $block->escapeHtml(__('Discount Amount')) ?>"> -- <?php if ($block->canShowPriceInfo($_item)): ?> -- <?= /* @escapeNotVerified */ $block->getOrder()->formatPrice(-$_item->getDiscountAmount()) ?> -- <?php else: ?> -+ <td class="col discount" data-th="<?= $block->escapeHtmlAttr(__('Discount Amount')) ?>"> -+ <?php if ($block->canShowPriceInfo($_item)) : ?> -+ <?= $block->escapeHtml($block->getOrder()->formatPrice(-$_item->getDiscountAmount()), ['span']) ?> -+ <?php else : ?> -   - <?php endif; ?> - </td> -- <td class="col rowtotal" data-th="<?= $block->escapeHtml(__('Row Total')) ?>"> -- <?php if ($block->canShowPriceInfo($_item)): ?> -+ <td class="col rowtotal" data-th="<?= $block->escapeHtmlAttr(__('Row Total')) ?>"> -+ <?php if ($block->canShowPriceInfo($_item)) : ?> - <?= $block->getItemRowTotalAfterDiscountHtml($_item) ?> -- <?php else: ?> -+ <?php else : ?> -   - <?php endif; ?> - </td> - </tr> - <?php endforeach; ?> - --<?php if ($_showlastRow && (($_options = $block->getItemOptions()) || $block->escapeHtml($_item->getDescription()))): ?> -+<?php if ($_showlastRow && (($_options = $block->getItemOptions()) || $block->escapeHtml($_item->getDescription()))) : ?> - <tr> - <td class="col options" colspan="7"> -- <?php if ($_options = $block->getItemOptions()): ?> -+ <?php if ($_options = $block->getItemOptions()) : ?> - <dl class="item-options"> - <?php foreach ($_options as $_option) : ?> - <dt><?= $block->escapeHtml($_option['label']) ?></dt> -- <?php if (!$block->getPrintStatus()): ?> -+ <?php if (!$block->getPrintStatus()) : ?> - <?php $_formatedOptionValue = $block->getFormatedOptionValue($_option) ?> -- <dd<?php if (isset($_formatedOptionValue['full_view'])): ?> class="tooltip wrapper"<?php endif; ?>> -- <?= /* @escapeNotVerified */ $_formatedOptionValue['value'] ?> -- <?php if (isset($_formatedOptionValue['full_view'])): ?> -+ <dd<?php if (isset($_formatedOptionValue['full_view'])) : ?> class="tooltip wrapper"<?php endif; ?>> -+ <?= /* @noEscape */ $_formatedOptionValue['value'] ?> -+ <?php if (isset($_formatedOptionValue['full_view'])) : ?> - <div class="tooltip content"> - <dl class="item options"> - <dt><?= $block->escapeHtml($_option['label']) ?></dt> -- <dd><?= /* @escapeNotVerified */ $_formatedOptionValue['full_view'] ?></dd> -+ <dd><?= /* @noEscape */ $_formatedOptionValue['full_view'] ?></dd> - </dl> - </div> - <?php endif; ?> - </dd> -- <?php else: ?> -+ <?php else : ?> - <dd><?= $block->escapeHtml((isset($_option['print_value']) ? $_option['print_value'] : $_option['value'])) ?></dd> - <?php endif; ?> - <?php endforeach; ?> -diff --git a/app/code/Magento/Bundle/view/frontend/templates/sales/order/invoice/items/renderer.phtml b/app/code/Magento/Bundle/view/frontend/templates/sales/order/invoice/items/renderer.phtml -index e18d75ce77b..b2025e53986 100644 ---- a/app/code/Magento/Bundle/view/frontend/templates/sales/order/invoice/items/renderer.phtml -+++ b/app/code/Magento/Bundle/view/frontend/templates/sales/order/invoice/items/renderer.phtml -@@ -3,9 +3,7 @@ - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -- --// @codingStandardsIgnoreFile -- -+// phpcs:disable Magento2.Templates.ThisInTemplate.FoundThis - ?> - <?php /** @var $block \Magento\Bundle\Block\Sales\Order\Items\Renderer */ ?> - <?php $parentItem = $block->getItem() ?> -@@ -15,77 +13,85 @@ - <?php $_index = 0 ?> - - <?php $_prevOptionId = '' ?> --<?php foreach ($items as $_item): ?> -+<?php foreach ($items as $_item) : ?> - -- <?php if ($block->getItemOptions() || $parentItem->getDescription() || $this->helper('Magento\GiftMessage\Helper\Message')->isMessagesAllowed('order_item', $parentItem) && $parentItem->getGiftMessageId()): ?> -+ <?php if ($block->getItemOptions() || $parentItem->getDescription() || $this->helper(Magento\GiftMessage\Helper\Message::class)->isMessagesAllowed('order_item', $parentItem) && $parentItem->getGiftMessageId()) : ?> - <?php $_showlastRow = true ?> -- <?php else: ?> -+ <?php else : ?> - <?php $_showlastRow = false ?> - <?php endif; ?> - -- <?php if ($_item->getOrderItem()->getParentItem()): ?> -+ <?php if ($_item->getOrderItem()->getParentItem()) : ?> - <?php $attributes = $block->getSelectionAttributes($_item) ?> -- <?php if ($_prevOptionId != $attributes['option_id']): ?> -+ <?php if ($_prevOptionId != $attributes['option_id']) : ?> - <tr class="options-label"> -- <td class="col label" colspan="5"><div class="option label"><?= /* @escapeNotVerified */ $attributes['option_label'] ?></div></td> -+ <td class="col label" colspan="5"> -+ <div class="option label"><?= $block->escapeHtml($attributes['option_label']) ?></div> -+ </td> - </tr> - <?php $_prevOptionId = $attributes['option_id'] ?> - <?php endif; ?> - <?php endif; ?> -- <tr id="order-item-row-<?= /* @escapeNotVerified */ $_item->getId() ?>" class="<?php if ($_item->getOrderItem()->getParentItem()): ?>item-options-container<?php else: ?>item-parent<?php endif; ?>"<?php if ($_item->getOrderItem()->getParentItem()): ?> data-th="<?= /* @escapeNotVerified */ $attributes['option_label'] ?>"<?php endif; ?>> -- <?php if (!$_item->getOrderItem()->getParentItem()): ?> -- <td class="col name" data-th="<?= $block->escapeHtml(__('Product Name')) ?>"> -+ <tr id="order-item-row-<?= $block->escapeHtmlAttr($_item->getId()) ?>" -+ class="<?php if ($_item->getOrderItem()->getParentItem()) : ?>item-options-container -+ <?php else : ?>item-parent -+ <?php endif; ?>" -+ <?php if ($_item->getOrderItem()->getParentItem()) : ?> -+ data-th="<?= $block->escapeHtmlAttr($attributes['option_label']) ?>" -+ <?php endif; ?>> -+ <?php if (!$_item->getOrderItem()->getParentItem()) : ?> -+ <td class="col name" data-th="<?= $block->escapeHtmlAttr(__('Product Name')) ?>"> - <strong class="product name product-item-name"><?= $block->escapeHtml($_item->getName()) ?></strong> - </td> -- <?php else: ?> -- <td class="col value" data-th="<?= $block->escapeHtml(__('Product Name')) ?>"><?= $block->getValueHtml($_item) ?></td> -+ <?php else : ?> -+ <td class="col value" data-th="<?= $block->escapeHtmlAttr(__('Product Name')) ?>"><?= $block->getValueHtml($_item) ?></td> - <?php endif; ?> -- <td class="col sku" data-th="<?= $block->escapeHtml(__('SKU')) ?>"><?= $block->escapeHtml($_item->getSku()) ?></td> -- <td class="col price" data-th="<?= $block->escapeHtml(__('Price')) ?>"> -- <?php if ($block->canShowPriceInfo($_item)): ?> -+ <td class="col sku" data-th="<?= $block->escapeHtmlAttr(__('SKU')) ?>"><?= $block->escapeHtml($_item->getSku()) ?></td> -+ <td class="col price" data-th="<?= $block->escapeHtmlAttr(__('Price')) ?>"> -+ <?php if ($block->canShowPriceInfo($_item)) : ?> - <?= $block->getItemPriceHtml($_item) ?> -- <?php else: ?> -+ <?php else : ?> -   - <?php endif; ?> - </td> -- <td class="col qty" data-th="<?= $block->escapeHtml(__('Qty Invoiced')) ?>"> -- <?php if ($block->canShowPriceInfo($_item)): ?> -- <?= /* @escapeNotVerified */ $_item->getQty()*1 ?> -- <?php else: ?> -+ <td class="col qty" data-th="<?= $block->escapeHtmlAttr(__('Qty Invoiced')) ?>"> -+ <?php if ($block->canShowPriceInfo($_item)) : ?> -+ <?= (float)$_item->getQty() * 1 ?> -+ <?php else : ?> -   - <?php endif; ?> - </td> -- <td class="col subtotal" data-th="<?= $block->escapeHtml(__('Subtotal')) ?>"> -- <?php if ($block->canShowPriceInfo($_item)): ?> -+ <td class="col subtotal" data-th="<?= $block->escapeHtmlAttr(__('Subtotal')) ?>"> -+ <?php if ($block->canShowPriceInfo($_item)) : ?> - <?= $block->getItemRowTotalHtml($_item) ?> -- <?php else: ?> -+ <?php else : ?> -   - <?php endif; ?> - </td> - </tr> - <?php endforeach; ?> - --<?php if ($_showlastRow && (($_options = $block->getItemOptions()) || $block->escapeHtml($_item->getDescription()))): ?> -+<?php if ($_showlastRow && (($_options = $block->getItemOptions()) || $block->escapeHtml($_item->getDescription()))) : ?> - <tr> - <td class="col options" colspan="5"> -- <?php if ($_options = $block->getItemOptions()): ?> -+ <?php if ($_options = $block->getItemOptions()) : ?> - <dl class="item-options"> - <?php foreach ($_options as $_option) : ?> - <dt><?= $block->escapeHtml($_option['label']) ?></dt> -- <?php if (!$block->getPrintStatus()): ?> -+ <?php if (!$block->getPrintStatus()) : ?> - <?php $_formatedOptionValue = $block->getFormatedOptionValue($_option) ?> -- <dd<?php if (isset($_formatedOptionValue['full_view'])): ?> class="tooltip wrapper"<?php endif; ?>> -- <?= /* @escapeNotVerified */ $_formatedOptionValue['value'] ?> -- <?php if (isset($_formatedOptionValue['full_view'])): ?> -+ <dd<?php if (isset($_formatedOptionValue['full_view'])) : ?> class="tooltip wrapper"<?php endif; ?>> -+ <?= /* @noEscape */ $_formatedOptionValue['value'] ?> -+ <?php if (isset($_formatedOptionValue['full_view'])) : ?> - <div class="tooltip content"> - <dl class="item options"> - <dt><?= $block->escapeHtml($_option['label']) ?></dt> -- <dd><?= /* @escapeNotVerified */ $_formatedOptionValue['full_view'] ?></dd> -+ <dd><?= /* @noEscape */ $_formatedOptionValue['full_view'] ?></dd> - </dl> - </div> - <?php endif; ?> - </dd> -- <?php else: ?> -+ <?php else : ?> - <dd><?= $block->escapeHtml((isset($_option['print_value']) ? $_option['print_value'] : $_option['value'])) ?></dd> - <?php endif; ?> - <?php endforeach; ?> -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 063d66edb9e..3507901dff8 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 -@@ -3,133 +3,150 @@ - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -- --// @codingStandardsIgnoreFile -- -+// phpcs:disable Magento2.Templates.ThisInTemplate.FoundThis - /** @var $block \Magento\Bundle\Block\Sales\Order\Items\Renderer */ -+$parentItem = $block->getItem(); -+$items = array_merge([$parentItem], $parentItem->getChildrenItems()); -+$index = 0; -+$prevOptionId = ''; - ?> --<?php $parentItem = $block->getItem() ?> --<?php $items = array_merge([$parentItem], $parentItem->getChildrenItems()); ?> --<?php $_index = 0 ?> - --<?php $_prevOptionId = '' ?> -+<?php foreach ($items as $item) : ?> - --<?php foreach ($items as $_item): ?> -- -- <?php if ($block->getItemOptions() || $parentItem->getDescription() || $this->helper('Magento\GiftMessage\Helper\Message')->isMessagesAllowed('order_item', $parentItem) && $parentItem->getGiftMessageId()): ?> -- <?php $_showlastRow = true ?> -- <?php else: ?> -- <?php $_showlastRow = false ?> -+ <?php if ($block->getItemOptions() -+ || $parentItem->getDescription() -+ || $this->helper(Magento\GiftMessage\Helper\Message::class)->isMessagesAllowed('order_item', $parentItem) -+ && $parentItem->getGiftMessageId()) : ?> -+ <?php $showLastRow = true; ?> -+ <?php else : ?> -+ <?php $showLastRow = false; ?> - <?php endif; ?> - -- <?php if ($_item->getParentItem()): ?> -- <?php $attributes = $block->getSelectionAttributes($_item) ?> -- <?php if ($_prevOptionId != $attributes['option_id']): ?> -+ <?php if ($item->getParentItem()) : ?> -+ <?php $attributes = $block->getSelectionAttributes($item) ?> -+ <?php if ($prevOptionId != $attributes['option_id']) : ?> - <tr class="options-label"> -- <td class="col label" colspan="5"><?= /* @escapeNotVerified */ $attributes['option_label'] ?></td> -+ <td class="col label" colspan="5"><?= $block->escapeHtml($attributes['option_label']); ?></td> - </tr> -- <?php $_prevOptionId = $attributes['option_id'] ?> -+ <?php $prevOptionId = $attributes['option_id'] ?> - <?php endif; ?> - <?php endif; ?> --<tr id="order-item-row-<?= /* @escapeNotVerified */ $_item->getId() ?>" class="<?php if ($_item->getParentItem()): ?>item-options-container<?php else: ?>item-parent<?php endif; ?>"<?php if ($_item->getParentItem()): ?> data-th="<?= /* @escapeNotVerified */ $attributes['option_label'] ?>"<?php endif; ?>> -- <?php if (!$_item->getParentItem()): ?> -- <td class="col name" data-th="<?= $block->escapeHtml(__('Product Name')) ?>"> -- <strong class="product name product-item-name"><?= $block->escapeHtml($_item->getName()) ?></strong> -+<tr id="order-item-row-<?= /* @noEscape */ $item->getId() ?>" -+ class="<?php if ($item->getParentItem()) : ?> -+ item-options-container -+ <?php else : ?> -+ item-parent -+ <?php endif; ?>" -+ <?php if ($item->getParentItem()) : ?> -+ data-th="<?= $block->escapeHtmlAttr($attributes['option_label']); ?>" -+ <?php endif; ?>> -+ <?php if (!$item->getParentItem()) : ?> -+ <td class="col name" data-th="<?= $block->escapeHtmlAttr(__('Product Name')); ?>"> -+ <strong class="product name product-item-name"><?= $block->escapeHtml($item->getName()); ?></strong> -+ </td> -+ <?php else : ?> -+ <td class="col value" data-th="<?= $block->escapeHtmlAttr(__('Product Name')); ?>"> -+ <?= $block->getValueHtml($item); ?> - </td> -- <?php else: ?> -- <td class="col value" data-th="<?= $block->escapeHtml(__('Product Name')) ?>"><?= $block->getValueHtml($_item) ?></td> - <?php endif; ?> -- <td class="col sku" data-th="<?= $block->escapeHtml(__('SKU')) ?>"><?= /* @escapeNotVerified */ $block->prepareSku($_item->getSku()) ?></td> -- <td class="col price" data-th="<?= $block->escapeHtml(__('Price')) ?>"> -- <?php if (!$_item->getParentItem()): ?> -- <?= $block->getItemPriceHtml() ?> -- <?php else: ?> -+ <td class="col sku" data-th="<?= $block->escapeHtmlAttr(__('SKU')); ?>"> -+ <?= /* @noEscape */ $block->prepareSku($item->getSku()); ?> -+ </td> -+ <td class="col price" data-th="<?= $block->escapeHtmlAttr(__('Price')); ?>"> -+ <?php if (!$item->getParentItem()) : ?> -+ <?= /* @noEscape */ $block->getItemPriceHtml(); ?> -+ <?php else : ?> -   - <?php endif; ?> - </td> -- <td class="col qty" data-th="<?= $block->escapeHtml(__('Quantity')) ?>"> -- <?php if ( -- ($_item->getParentItem() && $block->isChildCalculated()) || -- (!$_item->getParentItem() && !$block->isChildCalculated()) || ($_item->getQtyShipped() > 0 && $_item->getParentItem() && $block->isShipmentSeparately())):?> -+ <td class="col qty" data-th="<?= $block->escapeHtmlAttr(__('Quantity')); ?>"> -+ <?php if (($item->getParentItem() && $block->isChildCalculated()) || -+ (!$item->getParentItem() && !$block->isChildCalculated()) || -+ ($item->getQtyShipped() > 0 && $item->getParentItem() && $block->isShipmentSeparately())) : ?> - <ul class="items-qty"> - <?php endif; ?> -- <?php if (($_item->getParentItem() && $block->isChildCalculated()) || -- (!$_item->getParentItem() && !$block->isChildCalculated())): ?> -- <?php if ($_item->getQtyOrdered() > 0): ?> -+ <?php if (($item->getParentItem() && $block->isChildCalculated()) || -+ (!$item->getParentItem() && !$block->isChildCalculated())) : ?> -+ <?php if ($item->getQtyOrdered() > 0) : ?> - <li class="item"> -- <span class="title"><?= /* @escapeNotVerified */ __('Ordered') ?></span> -- <span class="content"><?= /* @escapeNotVerified */ $_item->getQtyOrdered()*1 ?></span> -+ <span class="title"><?= $block->escapeHtml(__('Ordered')); ?></span> -+ <span class="content"><?= /* @noEscape */ $item->getQtyOrdered() * 1; ?></span> - </li> - <?php endif; ?> -- <?php if ($_item->getQtyShipped() > 0 && !$block->isShipmentSeparately()): ?> -+ <?php if ($item->getQtyShipped() > 0 && !$block->isShipmentSeparately()) : ?> - <li class="item"> -- <span class="title"><?= /* @escapeNotVerified */ __('Shipped') ?></span> -- <span class="content"><?= /* @escapeNotVerified */ $_item->getQtyShipped()*1 ?></span> -+ <span class="title"><?= $block->escapeHtml(__('Shipped')); ?></span> -+ <span class="content"><?= /* @noEscape */ $item->getQtyShipped() * 1; ?></span> - </li> - <?php endif; ?> -- <?php if ($_item->getQtyCanceled() > 0): ?> -+ <?php if ($item->getQtyCanceled() > 0) : ?> - <li class="item"> -- <span class="title"><?= /* @escapeNotVerified */ __('Canceled') ?></span> -- <span class="content"><?= /* @escapeNotVerified */ $_item->getQtyCanceled()*1 ?></span> -+ <span class="title"><?= $block->escapeHtml(__('Canceled')); ?></span> -+ <span class="content"><?= /* @noEscape */ $item->getQtyCanceled() * 1; ?></span> - </li> - <?php endif; ?> -- <?php if ($_item->getQtyRefunded() > 0): ?> -+ <?php if ($item->getQtyRefunded() > 0) : ?> - <li class="item"> -- <span class="title"><?= /* @escapeNotVerified */ __('Refunded') ?></span> -- <span class="content"><?= /* @escapeNotVerified */ $_item->getQtyRefunded()*1 ?></span> -+ <span class="title"><?= $block->escapeHtml(__('Refunded')); ?></span> -+ <span class="content"><?= /* @noEscape */ $item->getQtyRefunded() * 1; ?></span> - </li> - <?php endif; ?> -- <?php elseif ($_item->getQtyShipped() > 0 && $_item->getParentItem() && $block->isShipmentSeparately()): ?> -+ <?php elseif ($item->getQtyShipped() > 0 && $item->getParentItem() && $block->isShipmentSeparately()) : ?> - <li class="item"> -- <span class="title"><?= /* @escapeNotVerified */ __('Shipped') ?></span> -- <span class="content"><?= /* @escapeNotVerified */ $_item->getQtyShipped()*1 ?></span> -+ <span class="title"><?= $block->escapeHtml(__('Shipped')); ?></span> -+ <span class="content"><?= /* @noEscape */ $item->getQtyShipped() * 1; ?></span> - </li> -- <?php else: ?> --   -+ <?php else : ?> -+ <span class="content"><?= /* @noEscape */ $parentItem->getQtyOrdered() * 1; ?></span> - <?php endif; ?> -- <?php if ( -- ($_item->getParentItem() && $block->isChildCalculated()) || -- (!$_item->getParentItem() && !$block->isChildCalculated()) || ($_item->getQtyShipped() > 0 && $_item->getParentItem() && $block->isShipmentSeparately())):?> -+ <?php if (($item->getParentItem() && $block->isChildCalculated()) || -+ (!$item->getParentItem() && !$block->isChildCalculated()) || -+ ($item->getQtyShipped() > 0 && $item->getParentItem() && $block->isShipmentSeparately())) :?> - </ul> - <?php endif; ?> - </td> -- <td class="col subtotal" data-th="<?= $block->escapeHtml(__('Subtotal')) ?>"> -- <?php if (!$_item->getParentItem()): ?> -- <?= $block->getItemRowTotalHtml() ?> -- <?php else: ?> -+ <td class="col subtotal" data-th="<?= $block->escapeHtmlAttr(__('Subtotal')) ?>"> -+ <?php if (!$item->getParentItem()) : ?> -+ <?= /* @noEscape */ $block->getItemRowTotalHtml(); ?> -+ <?php else : ?> -   - <?php endif; ?> - </td> - </tr> - <?php endforeach; ?> - --<?php if ($_showlastRow && (($_options = $block->getItemOptions()) || $block->escapeHtml($_item->getDescription()))): ?> -+<?php if ($showLastRow && (($options = $block->getItemOptions()) || $block->escapeHtml($item->getDescription()))) : ?> - <tr> - <td class="col options" colspan="5"> -- <?php if ($_options = $block->getItemOptions()): ?> -+ <?php if ($options = $block->getItemOptions()) : ?> - <dl class="item-options"> -- <?php foreach ($_options as $_option) : ?> -- <dt><?= $block->escapeHtml($_option['label']) ?></dt> -- <?php if (!$block->getPrintStatus()): ?> -- <?php $_formatedOptionValue = $block->getFormatedOptionValue($_option) ?> -- <dd<?php if (isset($_formatedOptionValue['full_view'])): ?> class="tooltip wrapper"<?php endif; ?>> -- <?= /* @escapeNotVerified */ $_formatedOptionValue['value'] ?> -- <?php if (isset($_formatedOptionValue['full_view'])): ?> -+ <?php foreach ($options as $option) : ?> -+ <dt><?= $block->escapeHtml($option['label']) ?></dt> -+ <?php if (!$block->getPrintStatus()) : ?> -+ <?php $formattedOptionValue = $block->getFormatedOptionValue($option) ?> -+ <dd<?php if (isset($formattedOptionValue['full_view'])) : ?> -+ class="tooltip wrapper" -+ <?php endif; ?>> -+ <?= /* @noEscape */ $formattedOptionValue['value'] ?> -+ <?php if (isset($formattedOptionValue['full_view'])) : ?> - <div class="tooltip content"> - <dl class="item options"> -- <dt><?= $block->escapeHtml($_option['label']) ?></dt> -- <dd><?= /* @escapeNotVerified */ $_formatedOptionValue['full_view'] ?></dd> -+ <dt><?= $block->escapeHtml($option['label']); ?></dt> -+ <dd><?= /* @noEscape */ $formattedOptionValue['full_view']; ?></dd> - </dl> - </div> - <?php endif; ?> - </dd> -- <?php else: ?> -- <dd><?= $block->escapeHtml((isset($_option['print_value']) ? $_option['print_value'] : $_option['value'])) ?></dd> -+ <?php else : ?> -+ <dd><?= $block->escapeHtml((isset($option['print_value']) ? -+ $option['print_value'] : -+ $option['value'])); ?> -+ </dd> - <?php endif; ?> - <?php endforeach; ?> - </dl> - <?php endif; ?> -- <?= $block->escapeHtml($_item->getDescription()) ?> -+ <?= $block->escapeHtml($item->getDescription()); ?> - </td> - </tr> - <?php endif; ?> -diff --git a/app/code/Magento/Bundle/view/frontend/templates/sales/order/shipment/items/renderer.phtml b/app/code/Magento/Bundle/view/frontend/templates/sales/order/shipment/items/renderer.phtml -index 0cd39156b25..c2ed3b161f6 100644 ---- a/app/code/Magento/Bundle/view/frontend/templates/sales/order/shipment/items/renderer.phtml -+++ b/app/code/Magento/Bundle/view/frontend/templates/sales/order/shipment/items/renderer.phtml -@@ -3,9 +3,7 @@ - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -- --// @codingStandardsIgnoreFile -- -+// phpcs:disable Magento2.Templates.ThisInTemplate.FoundThis - ?> - <?php /** @var $block \Magento\Bundle\Block\Sales\Order\Items\Renderer */ ?> - -@@ -16,69 +14,69 @@ - - <?php $_prevOptionId = '' ?> - --<?php foreach ($items as $_item): ?> -+<?php foreach ($items as $_item) : ?> - -- <?php if ($block->getItemOptions() || $parentItem->getDescription() || $this->helper('Magento\GiftMessage\Helper\Message')->isMessagesAllowed('order_item', $parentItem) && $parentItem->getGiftMessageId()): ?> -+ <?php if ($block->getItemOptions() || $parentItem->getDescription() || $this->helper(Magento\GiftMessage\Helper\Message::class)->isMessagesAllowed('order_item', $parentItem) && $parentItem->getGiftMessageId()) : ?> - <?php $_showlastRow = true ?> -- <?php else: ?> -+ <?php else : ?> - <?php $_showlastRow = false ?> - <?php endif; ?> - -- <?php if ($_item->getParentItem()): ?> -+ <?php if ($_item->getParentItem()) : ?> - <?php $attributes = $block->getSelectionAttributes($_item) ?> -- <?php if ($_prevOptionId != $attributes['option_id']): ?> -+ <?php if ($_prevOptionId != $attributes['option_id']) : ?> - <tr class="options-label"> -- <td colspan="3" class="col label"><div class="option label"><?= /* @escapeNotVerified */ $attributes['option_label'] ?></div></td> -+ <td colspan="3" class="col label"><div class="option label"><?= $block->escapeHtml($attributes['option_label']) ?></div></td> - </tr> - <?php $_prevOptionId = $attributes['option_id'] ?> - <?php endif; ?> - <?php endif; ?> -- <tr id="order-item-row-<?= /* @escapeNotVerified */ $_item->getId() ?>" class="<?php if ($_item->getParentItem()): ?>item-options-container<?php else: ?>item-parent<?php endif; ?>"<?php if ($_item->getParentItem()): ?> data-th="<?= /* @escapeNotVerified */ $attributes['option_label'] ?>"<?php endif; ?>> -- <?php if (!$_item->getParentItem()): ?> -- <td class="col name" data-th="<?= $block->escapeHtml(__('Product Name')) ?>"> -+ <tr id="order-item-row-<?= $block->escapeHtmlAttr($_item->getId()) ?>" class="<?php if ($_item->getParentItem()) : ?>item-options-container<?php else : ?>item-parent<?php endif; ?>"<?php if ($_item->getParentItem()) : ?> data-th="<?= $block->escapeHtmlAttr($attributes['option_label']) ?>"<?php endif; ?>> -+ <?php if (!$_item->getParentItem()) : ?> -+ <td class="col name" data-th="<?= $block->escapeHtmlAttr(__('Product Name')) ?>"> - <strong class="product name product-item-name"><?= $block->escapeHtml($_item->getName()) ?></strong> - </td> -- <?php else: ?> -- <td class="col value" data-th="<?= $block->escapeHtml(__('Product Name')) ?>"><?= $block->getValueHtml($_item) ?></td> -+ <?php else : ?> -+ <td class="col value" data-th="<?= $block->escapeHtmlAttr(__('Product Name')) ?>"><?= $block->getValueHtml($_item) ?></td> - <?php endif; ?> -- <td class="col sku" data-th="<?= $block->escapeHtml(__('SKU')) ?>"><?= $block->escapeHtml($_item->getSku()) ?></td> -- <td class="col qty" data-th="<?= $block->escapeHtml(__('Qty Shipped')) ?>"> -- <?php if (($block->isShipmentSeparately() && $_item->getParentItem()) || (!$block->isShipmentSeparately() && !$_item->getParentItem())): ?> -- <?php if (isset($shipItems[$_item->getId()])): ?> -- <?= /* @escapeNotVerified */ $shipItems[$_item->getId()]->getQty()*1 ?> -- <?php elseif ($_item->getIsVirtual()): ?> -- <?= /* @escapeNotVerified */ __('N/A') ?> -- <?php else: ?> -+ <td class="col sku" data-th="<?= $block->escapeHtmlAttr(__('SKU')) ?>"><?= $block->escapeHtml($_item->getSku()) ?></td> -+ <td class="col qty" data-th="<?= $block->escapeHtmlAttr(__('Qty Shipped')) ?>"> -+ <?php if (($block->isShipmentSeparately() && $_item->getParentItem()) || (!$block->isShipmentSeparately() && !$_item->getParentItem())) : ?> -+ <?php if (isset($shipItems[$_item->getId()])) : ?> -+ <?= (float)$shipItems[$_item->getId()]->getQty() * 1 ?> -+ <?php elseif ($_item->getIsVirtual()) : ?> -+ <?= $block->escapeHtml(__('N/A')) ?> -+ <?php else : ?> - 0 - <?php endif; ?> -- <?php else: ?> -+ <?php else : ?> -   - <?php endif; ?> - </td> - </tr> - <?php endforeach; ?> - --<?php if ($_showlastRow && (($_options = $block->getItemOptions()) || $block->escapeHtml($_item->getDescription()))): ?> -+<?php if ($_showlastRow && (($_options = $block->getItemOptions()) || $block->escapeHtml($_item->getDescription()))) : ?> - <tr> - <td class="col options" colspan="3"> -- <?php if ($_options = $block->getItemOptions()): ?> -+ <?php if ($_options = $block->getItemOptions()) : ?> - <dl class="item-options"> - <?php foreach ($_options as $_option) : ?> - <dt><?= $block->escapeHtml($_option['label']) ?></dt> -- <?php if (!$block->getPrintStatus()): ?> -+ <?php if (!$block->getPrintStatus()) : ?> - <?php $_formatedOptionValue = $block->getFormatedOptionValue($_option) ?> -- <dd<?php if (isset($_formatedOptionValue['full_view'])): ?> class="tooltip wrapper"<?php endif; ?>> -- <?= /* @escapeNotVerified */ $_formatedOptionValue['value'] ?> -- <?php if (isset($_formatedOptionValue['full_view'])): ?> -+ <dd<?php if (isset($_formatedOptionValue['full_view'])) : ?> class="tooltip wrapper"<?php endif; ?>> -+ <?= /* @noEscape */ $_formatedOptionValue['value'] ?> -+ <?php if (isset($_formatedOptionValue['full_view'])) : ?> - <div class="tooltip content"> - <dl class="item options"> - <dt><?= $block->escapeHtml($_option['label']) ?></dt> -- <dd><?= /* @escapeNotVerified */ $_formatedOptionValue['full_view'] ?></dd> -+ <dd><?= /* @noEscape */ $_formatedOptionValue['full_view'] ?></dd> - </dl> - </div> - <?php endif; ?> - </dd> -- <?php else: ?> -+ <?php else : ?> - <dd><?= $block->escapeHtml((isset($_option['print_value']) ? $_option['print_value'] : $_option['value'])) ?></dd> - <?php endif; ?> - <?php endforeach; ?> -diff --git a/app/code/Magento/Bundle/view/frontend/web/js/product-summary.js b/app/code/Magento/Bundle/view/frontend/web/js/product-summary.js -index d8d4cb1e99b..1e7fe6b6673 100644 ---- a/app/code/Magento/Bundle/view/frontend/web/js/product-summary.js -+++ b/app/code/Magento/Bundle/view/frontend/web/js/product-summary.js -@@ -56,8 +56,9 @@ define([ - - // Clear Summary box - this.element.html(''); -- -- $.each(this.cache.currentElement.selected, $.proxy(this._renderOption, this)); -+ this.cache.currentElement.positions.forEach(function (optionId) { -+ this._renderOption(optionId, this.cache.currentElement.selected[optionId]); -+ }, this); - this.element - .parents(this.options.bundleSummaryContainer) - .toggleClass('empty', !this.cache.currentElementCount); // Zero elements equal '.empty' container -diff --git a/app/code/Magento/BundleGraphQl/Model/BundleProductTypeResolver.php b/app/code/Magento/BundleGraphQl/Model/BundleProductTypeResolver.php -index b904d3f62a7..211d625fbc7 100644 ---- a/app/code/Magento/BundleGraphQl/Model/BundleProductTypeResolver.php -+++ b/app/code/Magento/BundleGraphQl/Model/BundleProductTypeResolver.php -@@ -8,19 +8,22 @@ declare(strict_types=1); - namespace Magento\BundleGraphQl\Model; - - use Magento\Framework\GraphQl\Query\Resolver\TypeResolverInterface; -+use Magento\Bundle\Model\Product\Type as Type; - - /** -- * {@inheritdoc} -+ * @inheritdoc - */ - class BundleProductTypeResolver implements TypeResolverInterface - { -+ const BUNDLE_PRODUCT = 'BundleProduct'; -+ - /** -- * {@inheritdoc} -+ * @inheritdoc - */ - public function resolveType(array $data) : string - { -- if (isset($data['type_id']) && $data['type_id'] == 'bundle') { -- return 'BundleProduct'; -+ if (isset($data['type_id']) && $data['type_id'] == Type::TYPE_CODE) { -+ return self::BUNDLE_PRODUCT; - } - return ''; - } -diff --git a/app/code/Magento/BundleGraphQl/Model/Resolver/BundleItemLinks.php b/app/code/Magento/BundleGraphQl/Model/Resolver/BundleItemLinks.php -index f90945d19f9..184f7177a99 100644 ---- a/app/code/Magento/BundleGraphQl/Model/Resolver/BundleItemLinks.php -+++ b/app/code/Magento/BundleGraphQl/Model/Resolver/BundleItemLinks.php -@@ -7,15 +7,15 @@ declare(strict_types=1); - - namespace Magento\BundleGraphQl\Model\Resolver; - -+use Magento\Framework\Exception\LocalizedException; - use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; - use Magento\BundleGraphQl\Model\Resolver\Links\Collection; - use Magento\Framework\GraphQl\Config\Element\Field; --use Magento\Framework\GraphQl\Query\Resolver\Value; - use Magento\Framework\GraphQl\Query\Resolver\ValueFactory; - use Magento\Framework\GraphQl\Query\ResolverInterface; - - /** -- * {@inheritdoc} -+ * @inheritdoc - */ - class BundleItemLinks implements ResolverInterface - { -@@ -42,16 +42,14 @@ class BundleItemLinks implements ResolverInterface - } - - /** -- * {@inheritDoc} -+ * @inheritdoc - */ -- public function resolve(Field $field, $context, ResolveInfo $info, array $value = null, array $args = null) : Value -+ public function resolve(Field $field, $context, ResolveInfo $info, array $value = null, array $args = null) - { - if (!isset($value['option_id']) || !isset($value['parent_id'])) { -- $result = function () { -- return null; -- }; -- return $this->valueFactory->create($result); -+ throw new LocalizedException(__('"option_id" and "parent_id" values should be specified')); - } -+ - $this->linkCollection->addIdFilters((int)$value['option_id'], (int)$value['parent_id']); - $result = function () use ($value) { - return $this->linkCollection->getLinksForOptionId((int)$value['option_id']); -diff --git a/app/code/Magento/BundleGraphQl/Model/Resolver/BundleItems.php b/app/code/Magento/BundleGraphQl/Model/Resolver/BundleItems.php -index 9474f825fe5..b67bd69ecf9 100644 ---- a/app/code/Magento/BundleGraphQl/Model/Resolver/BundleItems.php -+++ b/app/code/Magento/BundleGraphQl/Model/Resolver/BundleItems.php -@@ -13,12 +13,11 @@ use Magento\BundleGraphQl\Model\Resolver\Options\Collection; - use Magento\Catalog\Api\Data\ProductInterface; - use Magento\Framework\EntityManager\MetadataPool; - use Magento\Framework\GraphQl\Config\Element\Field; --use Magento\Framework\GraphQl\Query\Resolver\Value; - use Magento\Framework\GraphQl\Query\Resolver\ValueFactory; - use Magento\Framework\GraphQl\Query\ResolverInterface; - - /** -- * {@inheritdoc} -+ * @inheritdoc - */ - class BundleItems implements ResolverInterface - { -@@ -35,21 +34,21 @@ class BundleItems implements ResolverInterface - /** - * @var MetadataPool - */ -- private $metdataPool; -+ private $metadataPool; - - /** - * @param Collection $bundleOptionCollection - * @param ValueFactory $valueFactory -- * @param MetadataPool $metdataPool -+ * @param MetadataPool $metadataPool - */ - public function __construct( - Collection $bundleOptionCollection, - ValueFactory $valueFactory, -- MetadataPool $metdataPool -+ MetadataPool $metadataPool - ) { - $this->bundleOptionCollection = $bundleOptionCollection; - $this->valueFactory = $valueFactory; -- $this->metdataPool = $metdataPool; -+ $this->metadataPool = $metadataPool; - } - - /** -@@ -57,9 +56,9 @@ class BundleItems implements ResolverInterface - * - * {@inheritDoc} - */ -- public function resolve(Field $field, $context, ResolveInfo $info, array $value = null, array $args = null) : Value -+ public function resolve(Field $field, $context, ResolveInfo $info, array $value = null, array $args = null) - { -- $linkField = $this->metdataPool->getMetadata(ProductInterface::class)->getLinkField(); -+ $linkField = $this->metadataPool->getMetadata(ProductInterface::class)->getLinkField(); - if ($value['type_id'] !== Type::TYPE_CODE - || !isset($value[$linkField]) - || !isset($value[ProductInterface::SKU]) -diff --git a/app/code/Magento/BundleGraphQl/Model/Resolver/Options/Collection.php b/app/code/Magento/BundleGraphQl/Model/Resolver/Options/Collection.php -index 149155c8627..7608d6e9e4d 100644 ---- a/app/code/Magento/BundleGraphQl/Model/Resolver/Options/Collection.php -+++ b/app/code/Magento/BundleGraphQl/Model/Resolver/Options/Collection.php -@@ -61,6 +61,7 @@ class Collection - * Add parent id/sku pair to use for option filter at fetch time. - * - * @param int $parentId -+ * @param int $parentEntityId - * @param string $sku - */ - public function addParentFilterData(int $parentId, int $parentEntityId, string $sku) : void -diff --git a/app/code/Magento/BundleGraphQl/Model/Resolver/Options/Label.php b/app/code/Magento/BundleGraphQl/Model/Resolver/Options/Label.php -index a4757108ee5..de72b18982c 100644 ---- a/app/code/Magento/BundleGraphQl/Model/Resolver/Options/Label.php -+++ b/app/code/Magento/BundleGraphQl/Model/Resolver/Options/Label.php -@@ -7,10 +7,10 @@ declare(strict_types=1); - - namespace Magento\BundleGraphQl\Model\Resolver\Options; - -+use Magento\Framework\Exception\LocalizedException; - use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; --use Magento\CatalogGraphQl\Model\Resolver\Products\DataProvider\Deferred\Product; -+use Magento\CatalogGraphQl\Model\Resolver\Products\DataProvider\Deferred\Product as ProductDataProvider; - use Magento\Framework\GraphQl\Config\Element\Field; --use Magento\Framework\GraphQl\Query\Resolver\Value; - use Magento\Framework\GraphQl\Query\Resolver\ValueFactory; - use Magento\Framework\GraphQl\Query\ResolverInterface; - -@@ -19,29 +19,28 @@ use Magento\Framework\GraphQl\Query\ResolverInterface; - */ - class Label implements ResolverInterface - { -- - /** - * @var ValueFactory - */ - private $valueFactory; - - /** -- * @var Product -+ * @var ProductDataProvider - */ - private $product; - - /** - * @param ValueFactory $valueFactory -- * @param Product $product -+ * @param ProductDataProvider $product - */ -- public function __construct(ValueFactory $valueFactory, Product $product) -+ public function __construct(ValueFactory $valueFactory, ProductDataProvider $product) - { - $this->valueFactory = $valueFactory; - $this->product = $product; - } - - /** -- * @inheritDoc -+ * @inheritdoc - */ - public function resolve( - Field $field, -@@ -49,12 +48,9 @@ class Label implements ResolverInterface - ResolveInfo $info, - array $value = null, - array $args = null -- ): Value { -+ ) { - if (!isset($value['sku'])) { -- $result = function () { -- return null; -- }; -- return $this->valueFactory->create($result); -+ throw new LocalizedException(__('"sku" value should be specified')); - } - - $this->product->addProductSku($value['sku']); -diff --git a/app/code/Magento/BundleGraphQl/Model/Resolver/Product/Fields/DynamicPrice.php b/app/code/Magento/BundleGraphQl/Model/Resolver/Product/Fields/DynamicPrice.php -index e8dc3decc2a..978e1c455fc 100644 ---- a/app/code/Magento/BundleGraphQl/Model/Resolver/Product/Fields/DynamicPrice.php -+++ b/app/code/Magento/BundleGraphQl/Model/Resolver/Product/Fields/DynamicPrice.php -@@ -5,36 +5,20 @@ - */ - declare(strict_types=1); - -- - namespace Magento\BundleGraphQl\Model\Resolver\Product\Fields; - - use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; - use Magento\Bundle\Model\Product\Type as Bundle; - use Magento\Framework\GraphQl\Config\Element\Field; --use Magento\Framework\GraphQl\Query\Resolver\Value; --use Magento\Framework\GraphQl\Query\Resolver\ValueFactory; - use Magento\Framework\GraphQl\Query\ResolverInterface; - - /** -- * {@inheritdoc} -+ * @inheritdoc - */ - class DynamicPrice implements ResolverInterface - { - /** -- * @var ValueFactory -- */ -- private $valueFactory; -- -- /** -- * @param ValueFactory $valueFactory -- */ -- public function __construct(ValueFactory $valueFactory) -- { -- $this->valueFactory = $valueFactory; -- } -- -- /** -- * {@inheritdoc} -+ * @inheritdoc - */ - public function resolve( - Field $field, -@@ -42,16 +26,12 @@ class DynamicPrice implements ResolverInterface - ResolveInfo $info, - array $value = null, - array $args = null -- ): Value { -+ ) { - $result = null; - if ($value['type_id'] === Bundle::TYPE_CODE) { - $result = isset($value['price_type']) ? !$value['price_type'] : null; - } - -- return $this->valueFactory->create( -- function () use ($result) { -- return $result; -- } -- ); -+ return $result; - } - } -diff --git a/app/code/Magento/BundleGraphQl/Model/Resolver/Product/Fields/DynamicSku.php b/app/code/Magento/BundleGraphQl/Model/Resolver/Product/Fields/DynamicSku.php -index 37e1557d36d..73f84c278a6 100644 ---- a/app/code/Magento/BundleGraphQl/Model/Resolver/Product/Fields/DynamicSku.php -+++ b/app/code/Magento/BundleGraphQl/Model/Resolver/Product/Fields/DynamicSku.php -@@ -5,36 +5,20 @@ - */ - declare(strict_types=1); - -- - namespace Magento\BundleGraphQl\Model\Resolver\Product\Fields; - - use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; - use Magento\Bundle\Model\Product\Type as Bundle; - use Magento\Framework\GraphQl\Config\Element\Field; --use Magento\Framework\GraphQl\Query\Resolver\Value; --use Magento\Framework\GraphQl\Query\Resolver\ValueFactory; - use Magento\Framework\GraphQl\Query\ResolverInterface; - - /** -- * {@inheritdoc} -+ * @inheritdoc - */ - class DynamicSku implements ResolverInterface - { - /** -- * @var ValueFactory -- */ -- private $valueFactory; -- -- /** -- * @param ValueFactory $valueFactory -- */ -- public function __construct(ValueFactory $valueFactory) -- { -- $this->valueFactory = $valueFactory; -- } -- -- /** -- * {@inheritdoc} -+ * @inheritdoc - */ - public function resolve( - Field $field, -@@ -42,18 +26,12 @@ class DynamicSku implements ResolverInterface - ResolveInfo $info, - array $value = null, - array $args = null -- ): Value { -- $result = function () { -- return null; -- }; -+ ) { -+ $result = null; - if ($value['type_id'] === Bundle::TYPE_CODE) { - $result = isset($value['sku_type']) ? !$value['sku_type'] : null; - } - -- return $this->valueFactory->create( -- function () use ($result) { -- return $result; -- } -- ); -+ return $result; - } - } -diff --git a/app/code/Magento/BundleGraphQl/Model/Resolver/Product/Fields/DynamicWeight.php b/app/code/Magento/BundleGraphQl/Model/Resolver/Product/Fields/DynamicWeight.php -index 5f79bba449e..a4bb8ef64fc 100644 ---- a/app/code/Magento/BundleGraphQl/Model/Resolver/Product/Fields/DynamicWeight.php -+++ b/app/code/Magento/BundleGraphQl/Model/Resolver/Product/Fields/DynamicWeight.php -@@ -5,36 +5,20 @@ - */ - declare(strict_types=1); - -- - namespace Magento\BundleGraphQl\Model\Resolver\Product\Fields; - - use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; - use Magento\Bundle\Model\Product\Type as Bundle; - use Magento\Framework\GraphQl\Config\Element\Field; --use Magento\Framework\GraphQl\Query\Resolver\Value; --use Magento\Framework\GraphQl\Query\Resolver\ValueFactory; - use Magento\Framework\GraphQl\Query\ResolverInterface; - - /** -- * {@inheritdoc} -+ * @inheritdoc - */ - class DynamicWeight implements ResolverInterface - { - /** -- * @var ValueFactory -- */ -- private $valueFactory; -- -- /** -- * @param ValueFactory $valueFactory -- */ -- public function __construct(ValueFactory $valueFactory) -- { -- $this->valueFactory = $valueFactory; -- } -- -- /** -- * {@inheritdoc} -+ * @inheritdoc - */ - public function resolve( - Field $field, -@@ -42,18 +26,12 @@ class DynamicWeight implements ResolverInterface - ResolveInfo $info, - array $value = null, - array $args = null -- ): Value { -- $result = function () { -- return null; -- }; -+ ) { -+ $result = null; - if ($value['type_id'] === Bundle::TYPE_CODE) { - $result = isset($value['weight_type']) ? !$value['weight_type'] : null; - } - -- return $this->valueFactory->create( -- function () use ($result) { -- return $result; -- } -- ); -+ return $result; - } - } -diff --git a/app/code/Magento/BundleGraphQl/Model/Resolver/Product/Fields/PriceView.php b/app/code/Magento/BundleGraphQl/Model/Resolver/Product/Fields/PriceView.php -index ef8e93748c7..b7351b09d43 100644 ---- a/app/code/Magento/BundleGraphQl/Model/Resolver/Product/Fields/PriceView.php -+++ b/app/code/Magento/BundleGraphQl/Model/Resolver/Product/Fields/PriceView.php -@@ -5,19 +5,16 @@ - */ - declare(strict_types=1); - -- - namespace Magento\BundleGraphQl\Model\Resolver\Product\Fields; - - use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; - use Magento\Bundle\Model\Product\Type as Bundle; - use Magento\Framework\GraphQl\Config\Element\Field; - use Magento\Framework\GraphQl\Query\EnumLookup; --use Magento\Framework\GraphQl\Query\Resolver\Value; --use Magento\Framework\GraphQl\Query\Resolver\ValueFactory; - use Magento\Framework\GraphQl\Query\ResolverInterface; - - /** -- * {@inheritdoc} -+ * @inheritdoc - */ - class PriceView implements ResolverInterface - { -@@ -26,23 +23,16 @@ class PriceView implements ResolverInterface - */ - private $enumLookup; - -- /** -- * @var ValueFactory -- */ -- private $valueFactory; -- - /** - * @param EnumLookup $enumLookup -- * @param ValueFactory $valueFactory - */ -- public function __construct(EnumLookup $enumLookup, ValueFactory $valueFactory) -+ public function __construct(EnumLookup $enumLookup) - { - $this->enumLookup = $enumLookup; -- $this->valueFactory = $valueFactory; - } - - /** -- * {@inheritdoc} -+ * @inheritdoc - */ - public function resolve( - Field $field, -@@ -50,19 +40,13 @@ class PriceView implements ResolverInterface - ResolveInfo $info, - array $value = null, - array $args = null -- ): Value { -- $result = function () { -- return null; -- }; -+ ) { -+ $result = null; - if ($value['type_id'] === Bundle::TYPE_CODE) { - $result = isset($value['price_view']) - ? $this->enumLookup->getEnumValueFromField('PriceViewEnum', $value['price_view']) : null; - } - -- return $this->valueFactory->create( -- function () use ($result) { -- return $result; -- } -- ); -+ return $result; - } - } -diff --git a/app/code/Magento/BundleGraphQl/Model/Resolver/Product/Fields/ShipBundleItems.php b/app/code/Magento/BundleGraphQl/Model/Resolver/Product/Fields/ShipBundleItems.php -index e2bd12a84e2..6babf6520e1 100644 ---- a/app/code/Magento/BundleGraphQl/Model/Resolver/Product/Fields/ShipBundleItems.php -+++ b/app/code/Magento/BundleGraphQl/Model/Resolver/Product/Fields/ShipBundleItems.php -@@ -5,19 +5,16 @@ - */ - declare(strict_types=1); - -- - namespace Magento\BundleGraphQl\Model\Resolver\Product\Fields; - - use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; - use Magento\Bundle\Model\Product\Type as Bundle; - use Magento\Framework\GraphQl\Config\Element\Field; - use Magento\Framework\GraphQl\Query\EnumLookup; --use Magento\Framework\GraphQl\Query\Resolver\Value; --use Magento\Framework\GraphQl\Query\Resolver\ValueFactory; - use Magento\Framework\GraphQl\Query\ResolverInterface; - - /** -- * {@inheritdoc} -+ * @inheritdoc - */ - class ShipBundleItems implements ResolverInterface - { -@@ -26,23 +23,16 @@ class ShipBundleItems implements ResolverInterface - */ - private $enumLookup; - -- /** -- * @var ValueFactory -- */ -- private $valueFactory; -- - /** - * @param EnumLookup $enumLookup -- * @param ValueFactory $valueFactory - */ -- public function __construct(EnumLookup $enumLookup, ValueFactory $valueFactory) -+ public function __construct(EnumLookup $enumLookup) - { - $this->enumLookup = $enumLookup; -- $this->valueFactory = $valueFactory; - } - - /** -- * {@inheritdoc} -+ * @inheritdoc - */ - public function resolve( - Field $field, -@@ -50,19 +40,10 @@ class ShipBundleItems implements ResolverInterface - ResolveInfo $info, - array $value = null, - array $args = null -- ): Value { -- $result = function () { -- return null; -- }; -- if ($value['type_id'] === Bundle::TYPE_CODE) { -- $result = isset($value['shipment_type']) -- ? $this->enumLookup->getEnumValueFromField('ShipBundleItemsEnum', $value['shipment_type']) : null; -- } -+ ) { -+ $result = isset($value['shipment_type']) && $value['type_id'] === Bundle::TYPE_CODE -+ ? $this->enumLookup->getEnumValueFromField('ShipBundleItemsEnum', $value['shipment_type']) : null; - -- return $this->valueFactory->create( -- function () use ($result) { -- return $result; -- } -- ); -+ return $result; - } - } -diff --git a/app/code/Magento/BundleGraphQl/etc/schema.graphqls b/app/code/Magento/BundleGraphQl/etc/schema.graphqls -index edde6079dfb..26d2cbcd704 100644 ---- a/app/code/Magento/BundleGraphQl/etc/schema.graphqls -+++ b/app/code/Magento/BundleGraphQl/etc/schema.graphqls -@@ -1,35 +1,35 @@ - # Copyright © Magento, Inc. All rights reserved. - # See COPYING.txt for license details. - --type BundleItem @doc(description: "BundleItem defines an individual item in a bundle product") { -- option_id: Int @doc(description: "An ID assigned to each type of item in a bundle product") -- title: String @doc(description: "The display name of the item") -- required: Boolean @doc(description: "Indicates whether the item must be included in the bundle") -- type: String @doc(description: "The input type that the customer uses to select the item. Examples include radio button and checkbox") -- position: Int @doc(description: "he relative position of this item compared to the other bundle items") -- sku: String @doc(description: "The SKU of the bundle product") -- options: [BundleItemOption] @doc(description: "An array of additional options for this bundle item") @resolver(class: "Magento\\BundleGraphQl\\Model\\Resolver\\BundleItemLinks") -+type BundleItem @doc(description: "BundleItem defines an individual item in a bundle product.") { -+ option_id: Int @doc(description: "An ID assigned to each type of item in a bundle product.") -+ title: String @doc(description: "The display name of the item.") -+ required: Boolean @doc(description: "Indicates whether the item must be included in the bundle.") -+ type: String @doc(description: "The input type that the customer uses to select the item. Examples include radio button and checkbox.") -+ position: Int @doc(description: "he relative position of this item compared to the other bundle items.") -+ sku: String @doc(description: "The SKU of the bundle product.") -+ options: [BundleItemOption] @doc(description: "An array of additional options for this bundle item.") @resolver(class: "Magento\\BundleGraphQl\\Model\\Resolver\\BundleItemLinks") - } - --type BundleItemOption @doc(description: "BundleItemOption defines characteristics and options for a specific bundle item") { -- id: Int @doc(description: "The ID assigned to the bundled item option") -- label: String @doc(description: "The text that identifies the bundled item option") @resolver(class: "Magento\\BundleGraphQl\\Model\\Resolver\\Options\\Label") -- qty: Float @doc(description: "Indicates the quantity of this specific bundle item") -- position: Int @doc(description: "When a bundle item contains multiple options, the relative position of this option compared to the other options") -- is_default: Boolean @doc(description: "Indicates whether this option is the default option") -- price: Float @doc(description: "The price of the selected option") -- price_type: PriceTypeEnum @doc(description: "One of FIXED, PERCENT, or DYNAMIC") -- can_change_quantity: Boolean @doc(description: "Indicates whether the customer can change the number of items for this option") -- product: ProductInterface @doc(description: "Contains details about this product option") @resolver(class: "Magento\\CatalogGraphQl\\Model\\Resolver\\Product") -+type BundleItemOption @doc(description: "BundleItemOption defines characteristics and options for a specific bundle item.") { -+ id: Int @doc(description: "The ID assigned to the bundled item option.") -+ label: String @doc(description: "The text that identifies the bundled item option.") @resolver(class: "Magento\\BundleGraphQl\\Model\\Resolver\\Options\\Label") -+ qty: Float @doc(description: "Indicates the quantity of this specific bundle item.") -+ position: Int @doc(description: "When a bundle item contains multiple options, the relative position of this option compared to the other options.") -+ is_default: Boolean @doc(description: "Indicates whether this option is the default option.") -+ price: Float @doc(description: "The price of the selected option.") -+ price_type: PriceTypeEnum @doc(description: "One of FIXED, PERCENT, or DYNAMIC.") -+ can_change_quantity: Boolean @doc(description: "Indicates whether the customer can change the number of items for this option.") -+ product: ProductInterface @doc(description: "Contains details about this product option.") @resolver(class: "Magento\\CatalogGraphQl\\Model\\Resolver\\Product") - } - --type BundleProduct implements ProductInterface, PhysicalProductInterface, CustomizableProductInterface @doc(description: "BundleProduct defines basic features of a bundle product and contains multiple BundleItems") { -- price_view: PriceViewEnum @doc(description: "One of PRICE_RANGE or AS_LOW_AS") @resolver(class: "Magento\\BundleGraphQl\\Model\\Resolver\\Product\\Fields\\PriceView") -- dynamic_price: Boolean @doc(description: "Indicates whether the bundle product has a dynamic price") @resolver(class: "Magento\\BundleGraphQl\\Model\\Resolver\\Product\\Fields\\DynamicPrice") -- dynamic_sku: Boolean @doc(description: "Indicates whether the bundle product has a dynamic SK") @resolver(class: "Magento\\BundleGraphQl\\Model\\Resolver\\Product\\Fields\\DynamicSku") -- ship_bundle_items: ShipBundleItemsEnum @doc(description: "Indicates whether to ship bundle items together or individually") @resolver(class: "Magento\\BundleGraphQl\\Model\\Resolver\\Product\\Fields\\ShipBundleItems") -- dynamic_weight: Boolean @doc(description: "Indicates whether the bundle product has a dynamically calculated weight") @resolver(class: "Magento\\BundleGraphQl\\Model\\Resolver\\Product\\Fields\\DynamicWeight") -- items: [BundleItem] @doc(description: "An array containing information about individual bundle items") @resolver(class: "Magento\\BundleGraphQl\\Model\\Resolver\\BundleItems") -+type BundleProduct implements ProductInterface, PhysicalProductInterface, CustomizableProductInterface @doc(description: "BundleProduct defines basic features of a bundle product and contains multiple BundleItems.") { -+ price_view: PriceViewEnum @doc(description: "One of PRICE_RANGE or AS_LOW_AS.") @resolver(class: "Magento\\BundleGraphQl\\Model\\Resolver\\Product\\Fields\\PriceView") -+ dynamic_price: Boolean @doc(description: "Indicates whether the bundle product has a dynamic price.") @resolver(class: "Magento\\BundleGraphQl\\Model\\Resolver\\Product\\Fields\\DynamicPrice") -+ dynamic_sku: Boolean @doc(description: "Indicates whether the bundle product has a dynamic SK.") @resolver(class: "Magento\\BundleGraphQl\\Model\\Resolver\\Product\\Fields\\DynamicSku") -+ ship_bundle_items: ShipBundleItemsEnum @doc(description: "Indicates whether to ship bundle items together or individually.") @resolver(class: "Magento\\BundleGraphQl\\Model\\Resolver\\Product\\Fields\\ShipBundleItems") -+ dynamic_weight: Boolean @doc(description: "Indicates whether the bundle product has a dynamically calculated weight.") @resolver(class: "Magento\\BundleGraphQl\\Model\\Resolver\\Product\\Fields\\DynamicWeight") -+ items: [BundleItem] @doc(description: "An array containing information about individual bundle items.") @resolver(class: "Magento\\BundleGraphQl\\Model\\Resolver\\BundleItems") - } - - enum PriceViewEnum @doc(description: "This enumeration defines whether a bundle product's price is displayed as the lowest possible value or as a range.") { -diff --git a/app/code/Magento/BundleImportExport/Model/Export/RowCustomizer.php b/app/code/Magento/BundleImportExport/Model/Export/RowCustomizer.php -index e0c94097e4d..2cefc60a429 100644 ---- a/app/code/Magento/BundleImportExport/Model/Export/RowCustomizer.php -+++ b/app/code/Magento/BundleImportExport/Model/Export/RowCustomizer.php -@@ -322,7 +322,7 @@ class RowCustomizer implements RowCustomizerInterface - */ - protected function getTypeValue($type) - { -- return isset($this->typeMapping[$type]) ? $this->typeMapping[$type] : self::VALUE_DYNAMIC; -+ return $this->typeMapping[$type] ?? self::VALUE_DYNAMIC; - } - - /** -@@ -333,7 +333,7 @@ class RowCustomizer implements RowCustomizerInterface - */ - protected function getPriceViewValue($type) - { -- return isset($this->priceViewMapping[$type]) ? $this->priceViewMapping[$type] : self::VALUE_PRICE_RANGE; -+ return $this->priceViewMapping[$type] ?? self::VALUE_PRICE_RANGE; - } - - /** -@@ -344,7 +344,7 @@ class RowCustomizer implements RowCustomizerInterface - */ - protected function getPriceTypeValue($type) - { -- return isset($this->priceTypeMapping[$type]) ? $this->priceTypeMapping[$type] : null; -+ return $this->priceTypeMapping[$type] ?? null; - } - - /** -@@ -355,7 +355,7 @@ class RowCustomizer implements RowCustomizerInterface - */ - private function getShipmentTypeValue($type) - { -- return isset($this->shipmentTypeMapping[$type]) ? $this->shipmentTypeMapping[$type] : null; -+ return $this->shipmentTypeMapping[$type] ?? null; - } - - /** -diff --git a/app/code/Magento/BundleImportExport/Model/Import/Product/Type/Bundle.php b/app/code/Magento/BundleImportExport/Model/Import/Product/Type/Bundle.php -index 3ed7e144ddd..81a47d72602 100644 ---- a/app/code/Magento/BundleImportExport/Model/Import/Product/Type/Bundle.php -+++ b/app/code/Magento/BundleImportExport/Model/Import/Product/Type/Bundle.php -@@ -20,6 +20,7 @@ use Magento\Store\Model\StoreManagerInterface; - - /** - * Class Bundle -+ * - * @package Magento\BundleImportExport\Model\Import\Product\Type - * @SuppressWarnings(PHPMD.ExcessiveClassComplexity) - */ -@@ -349,6 +350,8 @@ class Bundle extends \Magento\CatalogImportExport\Model\Import\Product\Type\Abst - } - - /** -+ * Deprecated method for retrieving mapping between skus and products. -+ * - * @deprecated Misspelled method - * @see retrieveProductsByCachedSkus - */ -@@ -600,6 +603,7 @@ class Bundle extends \Magento\CatalogImportExport\Model\Import\Product\Type\Abst - - /** - * Populate array for insert option values -+ * - * @param array $optionIds - * @return array - */ -@@ -779,7 +783,7 @@ class Bundle extends \Magento\CatalogImportExport\Model\Import\Product\Type\Abst - */ - private function getStoreIdByCode(string $storeCode): int - { -- if (!isset($this->storeIdToCode[$storeCode])) { -+ if (!isset($this->storeCodeToId[$storeCode])) { - /** @var $store \Magento\Store\Model\Store */ - foreach ($this->storeManager->getStores() as $store) { - $this->storeCodeToId[$store->getCode()] = $store->getId(); -diff --git a/app/code/Magento/BundleImportExport/Test/Unit/Model/Import/Product/Type/BundleTest.php b/app/code/Magento/BundleImportExport/Test/Unit/Model/Import/Product/Type/BundleTest.php -index b0794f45646..a8650a4e6e9 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 @@ class BundleTest extends \Magento\ImportExport\Test\Unit\Model\Import\AbstractIm - 'price_type' => 'fixed', - 'shipment_type' => '1', - 'default_qty' => '1', -- 'is_defaul' => '1', -+ 'is_default' => '1', - 'position' => '1', - 'option_id' => '1'] - ] -@@ -264,7 +264,7 @@ class BundleTest extends \Magento\ImportExport\Test\Unit\Model\Import\AbstractIm - 'price_type' => 'percent', - 'shipment_type' => 0, - 'default_qty' => '2', -- 'is_defaul' => '1', -+ 'is_default' => '1', - 'position' => '6', - 'option_id' => '6'] - ] -@@ -324,7 +324,7 @@ class BundleTest extends \Magento\ImportExport\Test\Unit\Model\Import\AbstractIm - . '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 @@ class BundleTest extends \Magento\ImportExport\Test\Unit\Model\Import\AbstractIm - . '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 8acf170d43c..b2aa0d000e9 100644 ---- a/app/code/Magento/CacheInvalidate/Model/PurgeCache.php -+++ b/app/code/Magento/CacheInvalidate/Model/PurgeCache.php -@@ -7,6 +7,9 @@ namespace Magento\CacheInvalidate\Model; - - use Magento\Framework\Cache\InvalidateLogger; - -+/** -+ * Class PurgeCache -+ */ - class PurgeCache - { - const HEADER_X_MAGENTO_TAGS_PATTERN = 'X-Magento-Tags-Pattern'; -@@ -26,6 +29,18 @@ class PurgeCache - */ - private $logger; - -+ /** -+ * Batch size of the purge request. -+ * -+ * Based on default Varnish 4 http_req_hdr_len size minus a 512 bytes margin for method, -+ * header name, line feeds etc. -+ * -+ * @see https://varnish-cache.org/docs/4.1/reference/varnishd.html -+ * -+ * @var int -+ */ -+ private $requestSize = 7680; -+ - /** - * Constructor - * -@@ -44,18 +59,66 @@ class PurgeCache - } - - /** -- * Send curl purge request -- * to invalidate cache by tags pattern -+ * Send curl purge request to invalidate cache by tags pattern - * - * @param string $tagsPattern - * @return bool Return true if successful; otherwise return false - */ - public function sendPurgeRequest($tagsPattern) - { -+ $successful = true; - $socketAdapter = $this->socketAdapterFactory->create(); - $servers = $this->cacheServer->getUris(); -- $headers = [self::HEADER_X_MAGENTO_TAGS_PATTERN => $tagsPattern]; - $socketAdapter->setOptions(['timeout' => 10]); -+ -+ $formattedTagsChunks = $this->splitTags($tagsPattern); -+ foreach ($formattedTagsChunks as $formattedTagsChunk) { -+ if (!$this->sendPurgeRequestToServers($socketAdapter, $servers, $formattedTagsChunk)) { -+ $successful = false; -+ } -+ } -+ -+ return $successful; -+ } -+ -+ /** -+ * Split tags by batches -+ * -+ * @param string $tagsPattern -+ * @return \Generator -+ */ -+ private function splitTags($tagsPattern) -+ { -+ $tagsBatchSize = 0; -+ $formattedTagsChunk = []; -+ $formattedTags = explode('|', $tagsPattern); -+ foreach ($formattedTags as $formattedTag) { -+ if ($tagsBatchSize + strlen($formattedTag) > $this->requestSize - count($formattedTagsChunk) - 1) { -+ yield implode('|', $formattedTagsChunk); -+ $formattedTagsChunk = []; -+ $tagsBatchSize = 0; -+ } -+ -+ $tagsBatchSize += strlen($formattedTag); -+ $formattedTagsChunk[] = $formattedTag; -+ } -+ if (!empty($formattedTagsChunk)) { -+ yield implode('|', $formattedTagsChunk); -+ } -+ } -+ -+ /** -+ * Send curl purge request to servers to invalidate cache by tags pattern -+ * -+ * @param \Zend\Http\Client\Adapter\Socket $socketAdapter -+ * @param \Zend\Uri\Uri[] $servers -+ * @param string $formattedTagsChunk -+ * @return bool Return true if successful; otherwise return false -+ */ -+ private function sendPurgeRequestToServers($socketAdapter, $servers, $formattedTagsChunk) -+ { -+ $headers = [self::HEADER_X_MAGENTO_TAGS_PATTERN => $formattedTagsChunk]; -+ $unresponsiveServerError = []; - foreach ($servers as $server) { - $headers['Host'] = $server->getHost(); - try { -@@ -69,12 +132,31 @@ class PurgeCache - $socketAdapter->read(); - $socketAdapter->close(); - } catch (\Exception $e) { -- $this->logger->critical($e->getMessage(), compact('server', 'tagsPattern')); -+ $unresponsiveServerError[] = "Cache host: " . $server->getHost() . ":" . $server->getPort() . -+ "resulted in error message: " . $e->getMessage(); -+ } -+ } -+ -+ $errorCount = count($unresponsiveServerError); -+ -+ if ($errorCount > 0) { -+ $loggerMessage = implode(" ", $unresponsiveServerError); -+ -+ if ($errorCount == count($servers)) { -+ $this->logger->critical( -+ 'No cache server(s) could be purged ' . $loggerMessage, -+ compact('server', 'formattedTagsChunk') -+ ); - return false; - } -+ -+ $this->logger->warning( -+ 'Unresponsive cache server(s) hit' . $loggerMessage, -+ compact('server', 'formattedTagsChunk') -+ ); - } - -- $this->logger->execute(compact('servers', 'tagsPattern')); -+ $this->logger->execute(compact('servers', 'formattedTagsChunk')); - return true; - } - } -diff --git a/app/code/Magento/Captcha/Controller/Refresh/Index.php b/app/code/Magento/Captcha/Controller/Refresh/Index.php -index e89a80646ed..e401e03e955 100644 ---- a/app/code/Magento/Captcha/Controller/Refresh/Index.php -+++ b/app/code/Magento/Captcha/Controller/Refresh/Index.php -@@ -8,9 +8,10 @@ - */ - namespace Magento\Captcha\Controller\Refresh; - -+use Magento\Framework\App\Action\HttpPostActionInterface as HttpPostActionInterface; - use Magento\Framework\App\Action\Context; - --class Index extends \Magento\Framework\App\Action\Action -+class Index extends \Magento\Framework\App\Action\Action implements HttpPostActionInterface - { - /** - * @var \Magento\Captcha\Helper\Data -diff --git a/app/code/Magento/Captcha/CustomerData/Captcha.php b/app/code/Magento/Captcha/CustomerData/Captcha.php -new file mode 100644 -index 00000000000..a744daacdc6 ---- /dev/null -+++ b/app/code/Magento/Captcha/CustomerData/Captcha.php -@@ -0,0 +1,61 @@ -+<?php -+/** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+ -+declare(strict_types=1); -+ -+namespace Magento\Captcha\CustomerData; -+ -+use Magento\Customer\CustomerData\SectionSourceInterface; -+ -+/** -+ * Captcha section -+ */ -+class Captcha extends \Magento\Framework\DataObject implements SectionSourceInterface -+{ -+ /** -+ * @var array -+ */ -+ private $formIds; -+ -+ /** -+ * @var \Magento\Captcha\Helper\Data -+ */ -+ private $helper; -+ -+ /** -+ * @param \Magento\Captcha\Helper\Data $helper -+ * @param array $formIds -+ * @param array $data -+ * @codeCoverageIgnore -+ */ -+ public function __construct( -+ \Magento\Captcha\Helper\Data $helper, -+ array $formIds, -+ array $data = [] -+ ) { -+ parent::__construct($data); -+ $this->helper = $helper; -+ $this->formIds = $formIds; -+ } -+ -+ /** -+ * @inheritdoc -+ */ -+ public function getSectionData() :array -+ { -+ $data = []; -+ -+ foreach ($this->formIds as $formId) { -+ $captchaModel = $this->helper->getCaptcha($formId); -+ $data[$formId] = [ -+ 'isRequired' => $captchaModel->isRequired(), -+ 'timestamp' => time() -+ ]; -+ } -+ -+ return $data; -+ } -+} -diff --git a/app/code/Magento/Captcha/Model/Checkout/ConfigProvider.php b/app/code/Magento/Captcha/Model/Checkout/ConfigProvider.php -index ef5f5a8edce..34ee62044ff 100644 ---- a/app/code/Magento/Captcha/Model/Checkout/ConfigProvider.php -+++ b/app/code/Magento/Captcha/Model/Checkout/ConfigProvider.php -@@ -5,6 +5,9 @@ - */ - namespace Magento\Captcha\Model\Checkout; - -+/** -+ * Configuration provider for Captcha rendering. -+ */ - class ConfigProvider implements \Magento\Checkout\Model\ConfigProviderInterface - { - /** -@@ -38,7 +41,7 @@ class ConfigProvider implements \Magento\Checkout\Model\ConfigProviderInterface - } - - /** -- * {@inheritdoc} -+ * @inheritdoc - */ - public function getConfig() - { -@@ -49,7 +52,8 @@ class ConfigProvider implements \Magento\Checkout\Model\ConfigProviderInterface - 'imageHeight' => $this->getImageHeight($formId), - 'imageSrc' => $this->getImageSrc($formId), - 'refreshUrl' => $this->getRefreshUrl(), -- 'isRequired' => $this->isRequired($formId) -+ 'isRequired' => $this->isRequired($formId), -+ 'timestamp' => time() - ]; - } - return $config; -diff --git a/app/code/Magento/Captcha/Model/Customer/Plugin/AjaxLogin.php b/app/code/Magento/Captcha/Model/Customer/Plugin/AjaxLogin.php -index 91f3a785df3..84ac71046c3 100644 ---- a/app/code/Magento/Captcha/Model/Customer/Plugin/AjaxLogin.php -+++ b/app/code/Magento/Captcha/Model/Customer/Plugin/AjaxLogin.php -@@ -10,6 +10,9 @@ use Magento\Captcha\Helper\Data as CaptchaHelper; - use Magento\Framework\Session\SessionManagerInterface; - use Magento\Framework\Controller\Result\JsonFactory; - -+/** -+ * Around plugin for login action. -+ */ - class AjaxLogin - { - /** -@@ -61,6 +64,8 @@ class AjaxLogin - } - - /** -+ * Check captcha data on login action. -+ * - * @param \Magento\Customer\Controller\Ajax\Login $subject - * @param \Closure $proceed - * @return $this -@@ -94,18 +99,20 @@ class AjaxLogin - if ($formId === $loginFormId) { - $captchaModel = $this->helper->getCaptcha($formId); - if ($captchaModel->isRequired($username)) { -- $captchaModel->logAttempt($username); - if (!$captchaModel->isCorrect($captchaString)) { - $this->sessionManager->setUsername($username); -+ $captchaModel->logAttempt($username); - return $this->returnJsonError(__('Incorrect CAPTCHA')); - } - } -+ $captchaModel->logAttempt($username); - } - } - return $proceed(); - } - - /** -+ * Format JSON response. - * - * @param \Magento\Framework\Phrase $phrase - * @return \Magento\Framework\Controller\Result\Json -diff --git a/app/code/Magento/Captcha/Model/DefaultModel.php b/app/code/Magento/Captcha/Model/DefaultModel.php -index cf6690df5c8..483f9c3fb4d 100644 ---- a/app/code/Magento/Captcha/Model/DefaultModel.php -+++ b/app/code/Magento/Captcha/Model/DefaultModel.php -@@ -78,6 +78,11 @@ class DefaultModel extends \Zend\Captcha\Image implements \Magento\Captcha\Model - */ - protected $session; - -+ /** -+ * @var string -+ */ -+ private $words; -+ - /** - * @param \Magento\Framework\Session\SessionManagerInterface $session - * @param \Magento\Captcha\Helper\Data $captchaData -@@ -311,18 +316,18 @@ class DefaultModel extends \Zend\Captcha\Image implements \Magento\Captcha\Model - */ - public function isCorrect($word) - { -- $storedWord = $this->getWord(); -+ $storedWords = $this->getWords(); - $this->clearWord(); - -- if (!$word || !$storedWord) { -+ if (!$word || !$storedWords) { - return false; - } - - if (!$this->isCaseSensitive()) { -- $storedWord = strtolower($storedWord); -+ $storedWords = strtolower($storedWords); - $word = strtolower($word); - } -- return $word === $storedWord; -+ return in_array($word, explode(',', $storedWords)); - } - - /** -@@ -481,7 +486,7 @@ class DefaultModel extends \Zend\Captcha\Image implements \Magento\Captcha\Model - /** - * Get captcha word - * -- * @return string -+ * @return string|null - */ - public function getWord() - { -@@ -489,6 +494,17 @@ class DefaultModel extends \Zend\Captcha\Image implements \Magento\Captcha\Model - return time() < $sessionData['expires'] ? $sessionData['data'] : null; - } - -+ /** -+ * Get captcha words -+ * -+ * @return string|null -+ */ -+ private function getWords() -+ { -+ $sessionData = $this->session->getData($this->getFormIdKey(self::SESSION_WORD)); -+ return time() < $sessionData['expires'] ? $sessionData['words'] : null; -+ } -+ - /** - * Set captcha word - * -@@ -498,9 +514,10 @@ class DefaultModel extends \Zend\Captcha\Image implements \Magento\Captcha\Model - */ - protected function setWord($word) - { -+ $this->words = $this->words ? $this->words . ',' . $word : $word; - $this->session->setData( - $this->getFormIdKey(self::SESSION_WORD), -- ['data' => $word, 'expires' => time() + $this->getTimeout()] -+ ['data' => $word, 'words' => $this->words, 'expires' => time() + $this->getTimeout()] - ); - $this->word = $word; - return $this; -diff --git a/app/code/Magento/Captcha/Observer/CaptchaStringResolver.php b/app/code/Magento/Captcha/Observer/CaptchaStringResolver.php -index 9b97225e60d..39579616fa9 100644 ---- a/app/code/Magento/Captcha/Observer/CaptchaStringResolver.php -+++ b/app/code/Magento/Captcha/Observer/CaptchaStringResolver.php -@@ -18,6 +18,6 @@ class CaptchaStringResolver - { - $captchaParams = $request->getPost(\Magento\Captcha\Helper\Data::INPUT_NAME_FIELD_VALUE); - -- return isset($captchaParams[$formId]) ? $captchaParams[$formId] : ''; -+ return $captchaParams[$formId] ?? ''; - } - } -diff --git a/app/code/Magento/Captcha/Observer/CheckGuestCheckoutObserver.php b/app/code/Magento/Captcha/Observer/CheckGuestCheckoutObserver.php -deleted file mode 100644 -index 7ccaa76b6c7..00000000000 ---- a/app/code/Magento/Captcha/Observer/CheckGuestCheckoutObserver.php -+++ /dev/null -@@ -1,82 +0,0 @@ --<?php --/** -- * Copyright © Magento, Inc. All rights reserved. -- * See COPYING.txt for license details. -- */ --namespace Magento\Captcha\Observer; -- --use Magento\Framework\Event\ObserverInterface; -- --class CheckGuestCheckoutObserver implements ObserverInterface --{ -- /** -- * @var \Magento\Captcha\Helper\Data -- */ -- protected $_helper; -- -- /** -- * @var \Magento\Framework\App\ActionFlag -- */ -- protected $_actionFlag; -- -- /** -- * @var CaptchaStringResolver -- */ -- protected $captchaStringResolver; -- -- /** -- * @var \Magento\Checkout\Model\Type\Onepage -- */ -- protected $_typeOnepage; -- -- /** -- * @var \Magento\Framework\Json\Helper\Data -- */ -- protected $jsonHelper; -- -- /** -- * @param \Magento\Captcha\Helper\Data $helper -- * @param \Magento\Framework\App\ActionFlag $actionFlag -- * @param CaptchaStringResolver $captchaStringResolver -- * @param \Magento\Checkout\Model\Type\Onepage $typeOnepage -- * @param \Magento\Framework\Json\Helper\Data $jsonHelper -- */ -- public function __construct( -- \Magento\Captcha\Helper\Data $helper, -- \Magento\Framework\App\ActionFlag $actionFlag, -- CaptchaStringResolver $captchaStringResolver, -- \Magento\Checkout\Model\Type\Onepage $typeOnepage, -- \Magento\Framework\Json\Helper\Data $jsonHelper -- ) { -- $this->_helper = $helper; -- $this->_actionFlag = $actionFlag; -- $this->captchaStringResolver = $captchaStringResolver; -- $this->_typeOnepage = $typeOnepage; -- $this->jsonHelper = $jsonHelper; -- } -- -- /** -- * Check Captcha On Checkout as Guest Page -- * -- * @param \Magento\Framework\Event\Observer $observer -- * @return $this -- */ -- public function execute(\Magento\Framework\Event\Observer $observer) -- { -- $formId = 'guest_checkout'; -- $captchaModel = $this->_helper->getCaptcha($formId); -- $checkoutMethod = $this->_typeOnepage->getQuote()->getCheckoutMethod(); -- if ($checkoutMethod == \Magento\Checkout\Model\Type\Onepage::METHOD_GUEST -- && $captchaModel->isRequired() -- ) { -- $controller = $observer->getControllerAction(); -- if (!$captchaModel->isCorrect($this->captchaStringResolver->resolve($controller->getRequest(), $formId))) { -- $this->_actionFlag->set('', \Magento\Framework\App\Action\Action::FLAG_NO_DISPATCH, true); -- $result = ['error' => 1, 'message' => __('Incorrect CAPTCHA')]; -- $controller->getResponse()->representJson($this->jsonHelper->jsonEncode($result)); -- } -- } -- -- return $this; -- } --} -diff --git a/app/code/Magento/Captcha/Observer/CheckRegisterCheckoutObserver.php b/app/code/Magento/Captcha/Observer/CheckRegisterCheckoutObserver.php -deleted file mode 100644 -index 8e110a9f465..00000000000 ---- a/app/code/Magento/Captcha/Observer/CheckRegisterCheckoutObserver.php -+++ /dev/null -@@ -1,82 +0,0 @@ --<?php --/** -- * Copyright © Magento, Inc. All rights reserved. -- * See COPYING.txt for license details. -- */ --namespace Magento\Captcha\Observer; -- --use Magento\Framework\Event\ObserverInterface; -- --class CheckRegisterCheckoutObserver implements ObserverInterface --{ -- /** -- * @var \Magento\Captcha\Helper\Data -- */ -- protected $_helper; -- -- /** -- * @var \Magento\Framework\App\ActionFlag -- */ -- protected $_actionFlag; -- -- /** -- * @var CaptchaStringResolver -- */ -- protected $captchaStringResolver; -- -- /** -- * @var \Magento\Checkout\Model\Type\Onepage -- */ -- protected $_typeOnepage; -- -- /** -- * @var \Magento\Framework\Json\Helper\Data -- */ -- protected $jsonHelper; -- -- /** -- * @param \Magento\Captcha\Helper\Data $helper -- * @param \Magento\Framework\App\ActionFlag $actionFlag -- * @param CaptchaStringResolver $captchaStringResolver -- * @param \Magento\Checkout\Model\Type\Onepage $typeOnepage -- * @param \Magento\Framework\Json\Helper\Data $jsonHelper -- */ -- public function __construct( -- \Magento\Captcha\Helper\Data $helper, -- \Magento\Framework\App\ActionFlag $actionFlag, -- CaptchaStringResolver $captchaStringResolver, -- \Magento\Checkout\Model\Type\Onepage $typeOnepage, -- \Magento\Framework\Json\Helper\Data $jsonHelper -- ) { -- $this->_helper = $helper; -- $this->_actionFlag = $actionFlag; -- $this->captchaStringResolver = $captchaStringResolver; -- $this->_typeOnepage = $typeOnepage; -- $this->jsonHelper = $jsonHelper; -- } -- -- /** -- * Check Captcha On Checkout Register Page -- * -- * @param \Magento\Framework\Event\Observer $observer -- * @return $this -- */ -- public function execute(\Magento\Framework\Event\Observer $observer) -- { -- $formId = 'register_during_checkout'; -- $captchaModel = $this->_helper->getCaptcha($formId); -- $checkoutMethod = $this->_typeOnepage->getQuote()->getCheckoutMethod(); -- if ($checkoutMethod == \Magento\Checkout\Model\Type\Onepage::METHOD_REGISTER -- && $captchaModel->isRequired() -- ) { -- $controller = $observer->getControllerAction(); -- if (!$captchaModel->isCorrect($this->captchaStringResolver->resolve($controller->getRequest(), $formId))) { -- $this->_actionFlag->set('', \Magento\Framework\App\Action\Action::FLAG_NO_DISPATCH, true); -- $result = ['error' => 1, 'message' => __('Incorrect CAPTCHA')]; -- $controller->getResponse()->representJson($this->jsonHelper->jsonEncode($result)); -- } -- } -- -- return $this; -- } --} -diff --git a/app/code/Magento/Captcha/Observer/CheckUserLoginObserver.php b/app/code/Magento/Captcha/Observer/CheckUserLoginObserver.php -index bdc8dfa2189..dd4974c5d84 100644 ---- a/app/code/Magento/Captcha/Observer/CheckUserLoginObserver.php -+++ b/app/code/Magento/Captcha/Observer/CheckUserLoginObserver.php -@@ -3,6 +3,7 @@ - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -+ - namespace Magento\Captcha\Observer; - - use Magento\Customer\Model\AuthenticationInterface; -@@ -11,7 +12,10 @@ use Magento\Framework\Exception\NoSuchEntityException; - use Magento\Customer\Api\CustomerRepositoryInterface; - - /** -+ * Check captcha on user login page observer. -+ * - * @SuppressWarnings(PHPMD.CouplingBetweenObjects) -+ * @SuppressWarnings(PHPMD.CookieAndSessionMisuse) - */ - class CheckUserLoginObserver implements ObserverInterface - { -@@ -140,7 +144,7 @@ class CheckUserLoginObserver implements ObserverInterface - $customer = $this->getCustomerRepository()->get($login); - $this->getAuthentication()->processAuthenticationFailure($customer->getId()); - } catch (NoSuchEntityException $e) { -- //do nothing as customer existance is validated later in authenticate method -+ //do nothing as customer existence is validated later in authenticate method - } - $this->messageManager->addError(__('Incorrect CAPTCHA')); - $this->_actionFlag->set('', \Magento\Framework\App\Action\Action::FLAG_NO_DISPATCH, true); -diff --git a/app/code/Magento/Captcha/Test/Mftf/ActionGroup/AdminLoginWithCaptchaActionGroup.xml b/app/code/Magento/Captcha/Test/Mftf/ActionGroup/AdminLoginWithCaptchaActionGroup.xml -new file mode 100644 -index 00000000000..07329e26598 ---- /dev/null -+++ b/app/code/Magento/Captcha/Test/Mftf/ActionGroup/AdminLoginWithCaptchaActionGroup.xml -@@ -0,0 +1,17 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> -+ <actionGroup name="AdminLoginWithCaptchaActionGroup" extends="LoginAsAdmin"> -+ <arguments> -+ <argument name="captcha" type="string" /> -+ </arguments> -+ <fillField stepKey="fillCaptchaField" after="fillPassword" userInput="{{captcha}}" selector="{{AdminLoginFormSection.captchaField}}" /> -+ </actionGroup> -+</actionGroups> -diff --git a/app/code/Magento/Captcha/Test/Mftf/ActionGroup/AssertCaptchaNotVisibleOnCustomerLoginFormActionGroup.xml b/app/code/Magento/Captcha/Test/Mftf/ActionGroup/AssertCaptchaNotVisibleOnCustomerLoginFormActionGroup.xml -new file mode 100644 -index 00000000000..a371f177e35 ---- /dev/null -+++ b/app/code/Magento/Captcha/Test/Mftf/ActionGroup/AssertCaptchaNotVisibleOnCustomerLoginFormActionGroup.xml -@@ -0,0 +1,22 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> -+ <actionGroup name="AssertCaptchaNotVisibleOnCustomerLoginFormActionGroup"> -+ <waitForPageLoad stepKey="waitForPageLoaded" /> -+ <dontSee selector="{{StorefrontCustomerSignInFormSection.captchaField}}" stepKey="dontSeeCaptchaField"/> -+ <dontSee selector="{{StorefrontCustomerSignInFormSection.captchaImg}}" stepKey="dontSeeCaptchaImage"/> -+ <dontSee selector="{{StorefrontCustomerSignInFormSection.captchaReload}}" stepKey="dontSeeCaptchaReloadButton"/> -+ <reloadPage stepKey="refreshPage"/> -+ <waitForPageLoad stepKey="waitForPageReloaded" /> -+ <dontSee selector="{{StorefrontCustomerSignInFormSection.captchaField}}" stepKey="dontSeeCaptchaFieldAfterPageReload"/> -+ <dontSee selector="{{StorefrontCustomerSignInFormSection.captchaImg}}" stepKey="dontSeeCaptchaImageAfterPageReload"/> -+ <dontSee selector="{{StorefrontCustomerSignInFormSection.captchaReload}}" stepKey="dontSeeCaptchaReloadButtonAfterPageReload"/> -+ </actionGroup> -+</actionGroups> -diff --git a/app/code/Magento/Captcha/Test/Mftf/ActionGroup/AssertCaptchaVisibleOnAdminLoginFormActionGroup.xml b/app/code/Magento/Captcha/Test/Mftf/ActionGroup/AssertCaptchaVisibleOnAdminLoginFormActionGroup.xml -new file mode 100644 -index 00000000000..aa02588000d ---- /dev/null -+++ b/app/code/Magento/Captcha/Test/Mftf/ActionGroup/AssertCaptchaVisibleOnAdminLoginFormActionGroup.xml -@@ -0,0 +1,21 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> -+ <actionGroup name="AssertCaptchaVisibleOnAdminLoginFormActionGroup"> -+ <waitForElementVisible selector="{{AdminLoginFormSection.captchaField}}" stepKey="seeCaptchaField"/> -+ <waitForElementVisible selector="{{AdminLoginFormSection.captchaImg}}" stepKey="seeCaptchaImage"/> -+ <waitForElementVisible selector="{{AdminLoginFormSection.captchaReload}}" stepKey="seeCaptchaReloadButton"/> -+ <reloadPage stepKey="refreshPage"/> -+ <waitForPageLoad stepKey="waitForPageReloaded" /> -+ <waitForElementVisible selector="{{AdminLoginFormSection.captchaField}}" stepKey="seeCaptchaFieldAfterPageReload"/> -+ <waitForElementVisible selector="{{AdminLoginFormSection.captchaImg}}" stepKey="seeCaptchaImageAfterPageReload"/> -+ <waitForElementVisible selector="{{AdminLoginFormSection.captchaReload}}" stepKey="seeCaptchaReloadButtonAfterPageReload"/> -+ </actionGroup> -+</actionGroups> -diff --git a/app/code/Magento/Captcha/Test/Mftf/ActionGroup/AssertCaptchaVisibleOnContactUsFormActionGroup.xml b/app/code/Magento/Captcha/Test/Mftf/ActionGroup/AssertCaptchaVisibleOnContactUsFormActionGroup.xml -new file mode 100644 -index 00000000000..d800c65cabb ---- /dev/null -+++ b/app/code/Magento/Captcha/Test/Mftf/ActionGroup/AssertCaptchaVisibleOnContactUsFormActionGroup.xml -@@ -0,0 +1,21 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> -+ <actionGroup name="AssertCaptchaVisibleOnContactUsFormActionGroup"> -+ <waitForElementVisible selector="{{StorefrontContactUsFormSection.captchaField}}" stepKey="waitToSeeCaptchaField"/> -+ <waitForElementVisible selector="{{StorefrontContactUsFormSection.captchaImg}}" stepKey="waitToSeeCaptchaImage"/> -+ <waitForElementVisible selector="{{StorefrontContactUsFormSection.captchaReload}}" stepKey="waitToSeeCaptchaReloadButton"/> -+ <reloadPage stepKey="refreshPage"/> -+ <waitForPageLoad stepKey="waitForPageReloaded" /> -+ <waitForElementVisible selector="{{StorefrontContactUsFormSection.captchaField}}" stepKey="waitToSeeCaptchaFieldAfterPageReload"/> -+ <waitForElementVisible selector="{{StorefrontContactUsFormSection.captchaImg}}" stepKey="waitToSeeCaptchaImageAfterPageReload"/> -+ <waitForElementVisible selector="{{StorefrontContactUsFormSection.captchaReload}}" stepKey="waitToSeeCaptchaReloadButtonAfterPageReload"/> -+ </actionGroup> -+</actionGroups> -diff --git a/app/code/Magento/Captcha/Test/Mftf/ActionGroup/AssertCaptchaVisibleOnCustomerAccountCreatePageActionGroup.xml b/app/code/Magento/Captcha/Test/Mftf/ActionGroup/AssertCaptchaVisibleOnCustomerAccountCreatePageActionGroup.xml -new file mode 100644 -index 00000000000..6c09d1d4938 ---- /dev/null -+++ b/app/code/Magento/Captcha/Test/Mftf/ActionGroup/AssertCaptchaVisibleOnCustomerAccountCreatePageActionGroup.xml -@@ -0,0 +1,16 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> -+ <actionGroup name="AssertCaptchaVisibleOnCustomerAccountCreatePageActionGroup"> -+ <waitForElementVisible selector="{{StorefrontCustomerCreateFormSection.captchaField}}" stepKey="waitForCaptchaField"/> -+ <waitForElementVisible selector="{{StorefrontCustomerCreateFormSection.captchaImg}}" stepKey="waitForCaptchaImage"/> -+ <waitForElementVisible selector="{{StorefrontCustomerCreateFormSection.captchaReload}}" stepKey="waitForCaptchaReloadButton"/> -+ </actionGroup> -+</actionGroups> -diff --git a/app/code/Magento/Captcha/Test/Mftf/ActionGroup/AssertCaptchaVisibleOnCustomerAccountInfoActionGroup.xml b/app/code/Magento/Captcha/Test/Mftf/ActionGroup/AssertCaptchaVisibleOnCustomerAccountInfoActionGroup.xml -new file mode 100644 -index 00000000000..c68ffbfb5be ---- /dev/null -+++ b/app/code/Magento/Captcha/Test/Mftf/ActionGroup/AssertCaptchaVisibleOnCustomerAccountInfoActionGroup.xml -@@ -0,0 +1,23 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> -+ <actionGroup name="AssertCaptchaVisibleOnCustomerAccountInfoActionGroup"> -+ <checkOption selector="{{StorefrontCustomerAccountInformationSection.changeEmail}}" stepKey="clickChangeEmailCheckbox" /> -+ <waitForElementVisible selector="{{StorefrontCustomerAccountInformationSection.captchaField}}" stepKey="seeCaptchaField"/> -+ <waitForElementVisible selector="{{StorefrontCustomerAccountInformationSection.captchaImg}}" stepKey="seeCaptchaImage"/> -+ <waitForElementVisible selector="{{StorefrontCustomerAccountInformationSection.captchaReload}}" stepKey="seeCaptchaReloadButton"/> -+ <reloadPage stepKey="refreshPage"/> -+ <waitForPageLoad stepKey="waitForPageReloaded" /> -+ <checkOption selector="{{StorefrontCustomerAccountInformationSection.changeEmail}}" stepKey="clickChangeEmailCheckboxAfterPageReload" /> -+ <waitForElementVisible selector="{{StorefrontCustomerAccountInformationSection.captchaField}}" stepKey="seeCaptchaFieldAfterPageReload"/> -+ <waitForElementVisible selector="{{StorefrontCustomerAccountInformationSection.captchaImg}}" stepKey="seeCaptchaImageAfterPageReload"/> -+ <waitForElementVisible selector="{{StorefrontCustomerAccountInformationSection.captchaReload}}" stepKey="seeCaptchaReloadButtonAfterPageReload"/> -+ </actionGroup> -+</actionGroups> -diff --git a/app/code/Magento/Captcha/Test/Mftf/ActionGroup/AssertCaptchaVisibleOnCustomerLoginFormActionGroup.xml b/app/code/Magento/Captcha/Test/Mftf/ActionGroup/AssertCaptchaVisibleOnCustomerLoginFormActionGroup.xml -new file mode 100644 -index 00000000000..5616b099c02 ---- /dev/null -+++ b/app/code/Magento/Captcha/Test/Mftf/ActionGroup/AssertCaptchaVisibleOnCustomerLoginFormActionGroup.xml -@@ -0,0 +1,21 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> -+ <actionGroup name="AssertCaptchaVisibleOnCustomerLoginFormActionGroup"> -+ <waitForElementVisible selector="{{StorefrontCustomerSignInFormSection.captchaField}}" stepKey="waitToSeeCaptchaField"/> -+ <waitForElementVisible selector="{{StorefrontCustomerSignInFormSection.captchaImg}}" stepKey="waitToSeeCaptchaImage"/> -+ <waitForElementVisible selector="{{StorefrontCustomerSignInFormSection.captchaReload}}" stepKey="waitToSeeCaptchaReloadButton"/> -+ <reloadPage stepKey="refreshPage"/> -+ <waitForPageLoad stepKey="waitForPageReloaded" /> -+ <waitForElementVisible selector="{{StorefrontCustomerSignInFormSection.captchaField}}" stepKey="waitToSeeCaptchaFieldAfterPageReload"/> -+ <waitForElementVisible selector="{{StorefrontCustomerSignInFormSection.captchaImg}}" stepKey="waitToSeeCaptchaImageAfterPageReload"/> -+ <waitForElementVisible selector="{{StorefrontCustomerSignInFormSection.captchaReload}}" stepKey="waitToSeeCaptchaReloadButtonAfterPageReload"/> -+ </actionGroup> -+</actionGroups> -diff --git a/app/code/Magento/Captcha/Test/Mftf/ActionGroup/CaptchaFormsDisplayingActionGroup.xml b/app/code/Magento/Captcha/Test/Mftf/ActionGroup/CaptchaFormsDisplayingActionGroup.xml -new file mode 100644 -index 00000000000..f3b6eb1d9af ---- /dev/null -+++ b/app/code/Magento/Captcha/Test/Mftf/ActionGroup/CaptchaFormsDisplayingActionGroup.xml -@@ -0,0 +1,23 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> -+ <actionGroup name="CaptchaFormsDisplayingActionGroup"> -+ <click selector="{{CaptchaFormsDisplayingSection.store}}" stepKey="ClickToGoStores"/> -+ <waitForPageLoad stepKey="waitForStoresLoaded"/> -+ <click selector="{{CaptchaFormsDisplayingSection.config}}" stepKey="ClickToGoConfiguration"/> -+ <waitForPageLoad stepKey="waitForConfigurationsLoaded"/> -+ <scrollTo selector="{{CaptchaFormsDisplayingSection.customer}}" x="0" y="-80" stepKey="ScrollToCustomers"/> -+ <click selector="{{CaptchaFormsDisplayingSection.customer}}" stepKey="ClickToCustomers"/> -+ <waitForPageLoad stepKey="waitForCustomerConfigurationsLoaded"/> -+ <click selector="{{CaptchaFormsDisplayingSection.customerConfig}}" stepKey="ClickToGoCustomerConfiguration"/> -+ <scrollTo selector="{{CaptchaFormsDisplayingSection.captcha}}" stepKey="scrollToCaptcha"/> -+ <conditionalClick selector="{{CaptchaFormsDisplayingSection.captcha}}" dependentSelector="{{CaptchaFormsDisplayingSection.dependent}}" visible="false" stepKey="ClickToOpenCaptcha"/> -+ </actionGroup> -+</actionGroups> -diff --git a/app/code/Magento/Captcha/Test/Mftf/ActionGroup/StorefrontCustomerChangeEmailWithCaptchaActionGroup.xml b/app/code/Magento/Captcha/Test/Mftf/ActionGroup/StorefrontCustomerChangeEmailWithCaptchaActionGroup.xml -new file mode 100644 -index 00000000000..8aff3d5482f ---- /dev/null -+++ b/app/code/Magento/Captcha/Test/Mftf/ActionGroup/StorefrontCustomerChangeEmailWithCaptchaActionGroup.xml -@@ -0,0 +1,18 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> -+ <actionGroup name="StorefrontCustomerChangeEmailWithCaptchaActionGroup" extends="StorefrontCustomerChangeEmailActionGroup"> -+ <arguments> -+ <argument name="captcha" type="string" /> -+ </arguments> -+ -+ <fillField selector="{{StorefrontCustomerAccountInformationSection.captchaField}}" userInput="{{captcha}}" stepKey="fillCaptchaField" after="fillCurrentPassword" /> -+ </actionGroup> -+</actionGroups> -diff --git a/app/code/Magento/Captcha/Test/Mftf/ActionGroup/StorefrontFillContactUsFormWithCaptchaActionGroup.xml b/app/code/Magento/Captcha/Test/Mftf/ActionGroup/StorefrontFillContactUsFormWithCaptchaActionGroup.xml -new file mode 100644 -index 00000000000..3546fa2e57a ---- /dev/null -+++ b/app/code/Magento/Captcha/Test/Mftf/ActionGroup/StorefrontFillContactUsFormWithCaptchaActionGroup.xml -@@ -0,0 +1,17 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> -+ <actionGroup name="StorefrontFillContactUsFormWithCaptchaActionGroup" extends="StorefrontFillContactUsFormActionGroup"> -+ <arguments> -+ <argument name="captcha" type="string" /> -+ </arguments> -+ <fillField stepKey="fillCaptchaField" after="fillComment" userInput="{{captcha}}" selector="{{StorefrontContactUsFormSection.captchaField}}" /> -+ </actionGroup> -+</actionGroups> -diff --git a/app/code/Magento/Captcha/Test/Mftf/ActionGroup/StorefrontFillCustomerAccountCreationFormWithCaptchaActionGroup.xml b/app/code/Magento/Captcha/Test/Mftf/ActionGroup/StorefrontFillCustomerAccountCreationFormWithCaptchaActionGroup.xml -new file mode 100644 -index 00000000000..d67ebc1a007 ---- /dev/null -+++ b/app/code/Magento/Captcha/Test/Mftf/ActionGroup/StorefrontFillCustomerAccountCreationFormWithCaptchaActionGroup.xml -@@ -0,0 +1,17 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> -+ <actionGroup name="StorefrontFillCustomerAccountCreationFormWithCaptchaActionGroup" extends="StorefrontFillCustomerAccountCreationFormActionGroup"> -+ <arguments> -+ <argument name="captcha" type="string" /> -+ </arguments> -+ <fillField stepKey="fillCaptchaField" after="fillConfirmPassword" userInput="{{captcha}}" selector="{{StorefrontCustomerCreateFormSection.captchaField}}" /> -+ </actionGroup> -+</actionGroups> -diff --git a/app/code/Magento/Captcha/Test/Mftf/ActionGroup/StorefrontFillCustomerLoginFormWithCaptchaActionGroup.xml b/app/code/Magento/Captcha/Test/Mftf/ActionGroup/StorefrontFillCustomerLoginFormWithCaptchaActionGroup.xml -new file mode 100644 -index 00000000000..5ad727a8fe9 ---- /dev/null -+++ b/app/code/Magento/Captcha/Test/Mftf/ActionGroup/StorefrontFillCustomerLoginFormWithCaptchaActionGroup.xml -@@ -0,0 +1,17 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> -+ <actionGroup name="StorefrontFillCustomerLoginFormWithCaptchaActionGroup" extends="StorefrontFillCustomerLoginFormActionGroup"> -+ <arguments> -+ <argument name="captcha" type="string" /> -+ </arguments> -+ <fillField stepKey="fillCaptchaField" after="fillPassword" userInput="{{captcha}}" selector="{{StorefrontCustomerSignInFormSection.captchaField}}" /> -+ </actionGroup> -+</actionGroups> -diff --git a/app/code/Magento/Captcha/Test/Mftf/Data/CaptchaConfigData.xml b/app/code/Magento/Captcha/Test/Mftf/Data/CaptchaConfigData.xml -new file mode 100644 -index 00000000000..90f48c320f2 ---- /dev/null -+++ b/app/code/Magento/Captcha/Test/Mftf/Data/CaptchaConfigData.xml -@@ -0,0 +1,142 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<entities xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataProfileSchema.xsd"> -+ <entity name="StorefrontCustomerCaptchaEnableConfigData"> -+ <!-- Magento default value --> -+ <data key="path">customer/captcha/enable</data> -+ <data key="scope_id">0</data> -+ <data key="label">Yes</data> -+ <data key="value">1</data> -+ </entity> -+ <entity name="StorefrontCustomerCaptchaDisableConfigData"> -+ <data key="path">customer/captcha/enable</data> -+ <data key="scope_id">0</data> -+ <data key="label">No</data> -+ <data key="value">0</data> -+ </entity> -+ <entity name="StorefrontCaptchaOnCustomerCreateFormConfigData"> -+ <data key="path">customer/captcha/forms</data> -+ <data key="scope_id">0</data> -+ <data key="label">Create user</data> -+ <data key="value">user_create</data> -+ </entity> -+ <entity name="StorefrontCaptchaOnContactUsFormConfigData"> -+ <data key="path">customer/captcha/forms</data> -+ <data key="scope_id">0</data> -+ <data key="label">Contact Us</data> -+ <data key="value">contact_us</data> -+ </entity> -+ <entity name="StorefrontCaptchaOnCustomerLoginConfigData"> -+ <!-- Magento default value --> -+ <data key="path">customer/captcha/forms</data> -+ <data key="scope_id">0</data> -+ <data key="label">Login</data> -+ <data key="value">user_login</data> -+ </entity> -+ <entity name="StorefrontCaptchaOnCustomerChangePasswordConfigData"> -+ <data key="path">customer/captcha/forms</data> -+ <data key="scope_id">0</data> -+ <data key="label">Change password</data> -+ <data key="value">user_edit</data> -+ </entity> -+ <entity name="StorefrontCaptchaOnCustomerForgotPasswordConfigData"> -+ <!-- Magento default value --> -+ <data key="path">customer/captcha/forms</data> -+ <data key="scope_id">0</data> -+ <data key="label">Forgot password</data> -+ <data key="value">user_forgotpassword</data> -+ </entity> -+ <entity name="StorefrontCustomerCaptchaModeAlwaysConfigData"> -+ <data key="path">customer/captcha/mode</data> -+ <data key="scope_id">0</data> -+ <data key="label">Always</data> -+ <data key="value">always</data> -+ </entity> -+ <entity name="StorefrontCustomerCaptchaModeAfterFailConfigData"> -+ <!-- Magento default value --> -+ <data key="path">customer/captcha/mode</data> -+ <data key="scope_id">0</data> -+ <data key="label">After number of attempts to login</data> -+ <data key="value">after_fail</data> -+ </entity> -+ <entity name="StorefrontCustomerCaptchaLength3ConfigData"> -+ <data key="path">customer/captcha/length</data> -+ <data key="scope">admin</data> -+ <data key="scope_id">1</data> -+ <data key="label">3</data> -+ <data key="value">3</data> -+ </entity> -+ <entity name="StorefrontCustomerCaptchaSymbols1ConfigData"> -+ <data key="path">customer/captcha/symbols</data> -+ <data key="scope">admin</data> -+ <data key="scope_id">1</data> -+ <data key="label">1</data> -+ <data key="value">1</data> -+ </entity> -+ <entity name="StorefrontCustomerCaptchaDefaultLengthConfigData"> -+ <!-- Magento default value --> -+ <data key="path">customer/captcha/length</data> -+ <data key="scope">admin</data> -+ <data key="scope_id">1</data> -+ <data key="label">4-5</data> -+ <data key="value">4-5</data> -+ </entity> -+ <entity name="StorefrontCustomerCaptchaDefaultSymbolsConfigData"> -+ <!-- Magento default value --> -+ <data key="path">customer/captcha/symbols</data> -+ <data key="scope">admin</data> -+ <data key="scope_id">1</data> -+ <data key="label">ABCDEFGHJKMnpqrstuvwxyz23456789</data> -+ <data key="value">ABCDEFGHJKMnpqrstuvwxyz23456789</data> -+ </entity> -+ <entity name="AdminCaptchaEnableConfigData"> -+ <!-- Magento default value --> -+ <data key="path">admin/captcha/enable</data> -+ <data key="scope_id">0</data> -+ <data key="label">Yes</data> -+ <data key="value">1</data> -+ </entity> -+ <entity name="AdminCaptchaDisableConfigData"> -+ <data key="path">admin/captcha/enable</data> -+ <data key="scope_id">0</data> -+ <data key="label">No</data> -+ <data key="value">0</data> -+ </entity> -+ <entity name="AdminCaptchaLength3ConfigData"> -+ <data key="path">admin/captcha/length</data> -+ <data key="scope">admin</data> -+ <data key="scope_id">1</data> -+ <data key="label">3</data> -+ <data key="value">3</data> -+ </entity> -+ <entity name="AdminCaptchaSymbols1ConfigData"> -+ <data key="path">admin/captcha/symbols</data> -+ <data key="scope">admin</data> -+ <data key="scope_id">1</data> -+ <data key="label">1</data> -+ <data key="value">1</data> -+ </entity> -+ <entity name="AdminCaptchaDefaultLengthConfigData"> -+ <!-- Magento default value --> -+ <data key="path">admin/captcha/length</data> -+ <data key="scope">admin</data> -+ <data key="scope_id">1</data> -+ <data key="label">4-5</data> -+ <data key="value">4-5</data> -+ </entity> -+ <entity name="AdminCaptchaDefaultSymbolsConfigData"> -+ <!-- Magento default value --> -+ <data key="path">admin/captcha/symbols</data> -+ <data key="scope">admin</data> -+ <data key="scope_id">1</data> -+ <data key="label">ABCDEFGHJKMnpqrstuvwxyz23456789</data> -+ <data key="value">ABCDEFGHJKMnpqrstuvwxyz23456789</data> -+ </entity> -+</entities> -diff --git a/app/code/Magento/Captcha/Test/Mftf/Data/CaptchaData.xml b/app/code/Magento/Captcha/Test/Mftf/Data/CaptchaData.xml -new file mode 100644 -index 00000000000..d8fb206b811 ---- /dev/null -+++ b/app/code/Magento/Captcha/Test/Mftf/Data/CaptchaData.xml -@@ -0,0 +1,19 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<entities xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataProfileSchema.xsd"> -+ <entity name="WrongCaptcha"> -+ <data key="value" unique="suffix">WrongCAPTCHA</data> -+ </entity> -+ -+ <!-- This CAPTCHA will only work if "StorefrontCustomerCaptchaLength3ConfigData" and "StorefrontCustomerCaptchaSymbols1ConfigData" config is set. --> -+ <entity name="PreconfiguredCaptcha"> -+ <data key="value">111</data> -+ </entity> -+</entities> -diff --git a/app/code/Magento/Captcha/Test/Mftf/Data/CaptchaFormsDisplayingData.xml b/app/code/Magento/Captcha/Test/Mftf/Data/CaptchaFormsDisplayingData.xml -new file mode 100644 -index 00000000000..57a09219fe4 ---- /dev/null -+++ b/app/code/Magento/Captcha/Test/Mftf/Data/CaptchaFormsDisplayingData.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="CaptchaData"> -+ <data key="createUser">Create user</data> -+ <data key="login">Login</data> -+ <data key="passwd">Forgot password</data> -+ <data key="contactUs">Contact Us</data> -+ <data key="changePasswd">Change password</data> -+ <data key="register">Register during Checkout</data> -+ <data key="checkoutAsGuest">Check Out as Guest</data> -+ </entity> -+</entities> -diff --git a/app/code/Magento/Captcha/Test/Mftf/Section/AdminLoginFormSection.xml b/app/code/Magento/Captcha/Test/Mftf/Section/AdminLoginFormSection.xml -new file mode 100644 -index 00000000000..2bcc6fc542d ---- /dev/null -+++ b/app/code/Magento/Captcha/Test/Mftf/Section/AdminLoginFormSection.xml -@@ -0,0 +1,16 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> -+ <section name="AdminLoginFormSection"> -+ <element name="captchaField" type="input" selector="#login-form input[name='captcha[backend_login]']" /> -+ <element name="captchaImg" type="block" selector="#login-form img#backend_login"/> -+ <element name="captchaReload" type="block" selector="#login-form img#captcha-reload"/> -+ </section> -+</sections> -diff --git a/app/code/Magento/Captcha/Test/Mftf/Section/CaptchaFormsDisplayingSection.xml b/app/code/Magento/Captcha/Test/Mftf/Section/CaptchaFormsDisplayingSection.xml -new file mode 100644 -index 00000000000..4c974e6fced ---- /dev/null -+++ b/app/code/Magento/Captcha/Test/Mftf/Section/CaptchaFormsDisplayingSection.xml -@@ -0,0 +1,27 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="../../../../../../../vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Page/etc/SectionObject.xsd"> -+ <section name="CaptchaFormsDisplayingSection"> -+ <element name="store" type="button" selector="#menu-magento-backend-stores"/> -+ <element name="config" type="button" selector="//li[@data-ui-id='menu-magento-config-system-config']//span"/> -+ <element name="customer" type="button" selector="//div[@class='admin__page-nav-title title _collapsible']//strong[text()='Customers']"/> -+ <element name="customerConfig" type="text" selector="//span[text()='Customer Configuration']"/> -+ <element name="captcha" type="button" selector="#customer_captcha-head"/> -+ <element name="dependent" type="button" selector="//a[@id='customer_captcha-head' and @class='open']"/> -+ <element name="forms" type="multiselect" selector="#customer_captcha_forms"/> -+ <element name="createUser" type="multiselect" selector="//select[@id='customer_captcha_forms']/option[@value='user_create']"/> -+ <element name="forgotpassword" type="multiselect" selector="//select[@id='customer_captcha_forms']/option[@value='user_forgotpassword']"/> -+ <element name="userLogin" type="multiselect" selector="//select[@id='customer_captcha_forms']/option[@value='user_login']"/> -+ <element name="guestCheckout" type="multiselect" selector="//select[@id='customer_captcha_forms']/option[@value='guest_checkout']"/> -+ <element name="register" type="multiselect" selector="//select[@id='customer_captcha_forms']/option[@value='register_during_checkout']"/> -+ <element name="userEdit" type="multiselect" selector="//select[@id='customer_captcha_forms']/option[@value='user_edit']"/> -+ <element name="contactUs" type="multiselect" selector="//select[@id='customer_captcha_forms']/option[@value='contact_us']"/> -+ </section> -+</sections> -diff --git a/app/code/Magento/Captcha/Test/Mftf/Section/StorefrontContactUsCaptchaSection.xml b/app/code/Magento/Captcha/Test/Mftf/Section/StorefrontContactUsCaptchaSection.xml -new file mode 100644 -index 00000000000..f587812576f ---- /dev/null -+++ b/app/code/Magento/Captcha/Test/Mftf/Section/StorefrontContactUsCaptchaSection.xml -@@ -0,0 +1,16 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> -+ <section name="StorefrontContactUsCaptchaSection"> -+ <element name="captchaField" type="input" selector="#captcha_contact_us"/> -+ <element name="captchaImg" type="block" selector=".captcha-img"/> -+ <element name="captchaReload" type="block" selector=".captcha-reload"/> -+ </section> -+</sections> -diff --git a/app/code/Magento/Captcha/Test/Mftf/Section/StorefrontContactUsFormSection.xml b/app/code/Magento/Captcha/Test/Mftf/Section/StorefrontContactUsFormSection.xml -new file mode 100644 -index 00000000000..60cf961ba7e ---- /dev/null -+++ b/app/code/Magento/Captcha/Test/Mftf/Section/StorefrontContactUsFormSection.xml -@@ -0,0 +1,16 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> -+ <section name="StorefrontContactUsFormSection"> -+ <element name="captchaField" type="input" selector="#contact-form input[name='captcha[contact_us]']" /> -+ <element name="captchaImg" type="block" selector="#contact-form img.captcha-img"/> -+ <element name="captchaReload" type="block" selector="#contact-form button.captcha-reload"/> -+ </section> -+</sections> -diff --git a/app/code/Magento/Captcha/Test/Mftf/Section/StorefrontCustomerAccountInformationSection.xml b/app/code/Magento/Captcha/Test/Mftf/Section/StorefrontCustomerAccountInformationSection.xml -new file mode 100644 -index 00000000000..a273c8d4abd ---- /dev/null -+++ b/app/code/Magento/Captcha/Test/Mftf/Section/StorefrontCustomerAccountInformationSection.xml -@@ -0,0 +1,16 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> -+ <section name="StorefrontCustomerAccountInformationSection"> -+ <element name="captchaField" type="input" selector="#captcha_user_edit"/> -+ <element name="captchaImg" type="block" selector=".captcha-img"/> -+ <element name="captchaReload" type="block" selector=".captcha-reload"/> -+ </section> -+</sections> -diff --git a/app/code/Magento/Captcha/Test/Mftf/Section/StorefrontCustomerCreateFormSection.xml b/app/code/Magento/Captcha/Test/Mftf/Section/StorefrontCustomerCreateFormSection.xml -new file mode 100644 -index 00000000000..f48e6124cb2 ---- /dev/null -+++ b/app/code/Magento/Captcha/Test/Mftf/Section/StorefrontCustomerCreateFormSection.xml -@@ -0,0 +1,16 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> -+ <section name="StorefrontCustomerCreateFormSection"> -+ <element name="captchaField" type="input" selector="#captcha_user_create"/> -+ <element name="captchaImg" type="block" selector=".captcha-img"/> -+ <element name="captchaReload" type="block" selector=".captcha-reload"/> -+ </section> -+</sections> -diff --git a/app/code/Magento/Captcha/Test/Mftf/Section/StorefrontCustomerSignInFormSection.xml b/app/code/Magento/Captcha/Test/Mftf/Section/StorefrontCustomerSignInFormSection.xml -new file mode 100644 -index 00000000000..54aa36d1ca2 ---- /dev/null -+++ b/app/code/Magento/Captcha/Test/Mftf/Section/StorefrontCustomerSignInFormSection.xml -@@ -0,0 +1,16 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> -+ <section name="StorefrontCustomerSignInFormSection"> -+ <element name="captchaField" type="input" selector="#captcha_user_login"/> -+ <element name="captchaImg" type="block" selector=".captcha-img"/> -+ <element name="captchaReload" type="block" selector=".captcha-reload"/> -+ </section> -+</sections> -diff --git a/app/code/Magento/Captcha/Test/Mftf/Section/StorefrontCustomerSignInPopupFormSection.xml b/app/code/Magento/Captcha/Test/Mftf/Section/StorefrontCustomerSignInPopupFormSection.xml -new file mode 100644 -index 00000000000..7a0557c4a27 ---- /dev/null -+++ b/app/code/Magento/Captcha/Test/Mftf/Section/StorefrontCustomerSignInPopupFormSection.xml -@@ -0,0 +1,16 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> -+ <section name="StorefrontCustomerSignInPopupFormSection"> -+ <element name="captchaField" type="input" selector="#captcha_user_login"/> -+ <element name="captchaImg" type="block" selector=".captcha-img"/> -+ <element name="captchaReload" type="block" selector=".captcha-reload"/> -+ </section> -+</sections> -diff --git a/app/code/Magento/Captcha/Test/Mftf/Test/AdminLoginWithCaptchaTest.xml b/app/code/Magento/Captcha/Test/Mftf/Test/AdminLoginWithCaptchaTest.xml -new file mode 100644 -index 00000000000..e5ee55910df ---- /dev/null -+++ b/app/code/Magento/Captcha/Test/Mftf/Test/AdminLoginWithCaptchaTest.xml -@@ -0,0 +1,68 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> -+ <test name="AdminLoginWithCaptchaTest"> -+ <annotations> -+ <features value="Captcha"/> -+ <stories value="Admin login + Captcha"/> -+ <title value="Captcha on Admin login form"/> -+ <description value="Test creation for admin login with captcha."/> -+ <testCaseId value="MC-14012" /> -+ <severity value="MAJOR"/> -+ <group value="captcha"/> -+ <group value="mtf_migrated"/> -+ </annotations> -+ -+ <before> -+ <magentoCLI command="config:set {{AdminCaptchaLength3ConfigData.path}} {{AdminCaptchaLength3ConfigData.value}}" stepKey="setCaptchaLength" /> -+ <magentoCLI command="config:set {{AdminCaptchaSymbols1ConfigData.path}} {{AdminCaptchaSymbols1ConfigData.value}}" stepKey="setCaptchaSymbols" /> -+ <magentoCLI command="cache:clean config full_page" stepKey="cleanInvalidatedCaches"/> -+ </before> -+ <after> -+ <magentoCLI command="config:set {{AdminCaptchaDefaultLengthConfigData.path}} {{AdminCaptchaDefaultLengthConfigData.value}}" stepKey="setDefaultCaptchaLength" /> -+ <magentoCLI command="config:set {{AdminCaptchaDefaultSymbolsConfigData.path}} {{AdminCaptchaDefaultSymbolsConfigData.value}}" stepKey="setDefaultCaptchaSymbols" /> -+ <magentoCLI command="cache:clean config full_page" stepKey="cleanInvalidatedCaches"/> -+ </after> -+ -+ <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdminWithWrongCredentialsFirstAttempt"> -+ <argument name="adminUser" value="AdminUserWrongCredentials" /> -+ </actionGroup> -+ <actionGroup ref="AssertMessageOnAdminLoginActionGroup" stepKey="seeFirstLoginErrorMessage" /> -+ -+ <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdminWithWrongCredentialsSecondAttempt"> -+ <argument name="adminUser" value="AdminUserWrongCredentials" /> -+ </actionGroup> -+ <actionGroup ref="AssertMessageOnAdminLoginActionGroup" stepKey="seeSecondLoginErrorMessage" /> -+ -+ <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdminWithWrongCredentialsThirdAttempt"> -+ <argument name="adminUser" value="AdminUserWrongCredentials" /> -+ </actionGroup> -+ <actionGroup ref="AssertMessageOnAdminLoginActionGroup" stepKey="seeThirdLoginErrorMessage" /> -+ -+ <!-- Check captcha visibility on admin login page --> -+ <actionGroup ref="AssertCaptchaVisibleOnAdminLoginFormActionGroup" stepKey="assertCaptchaVisible" /> -+ -+ <!-- Submit form with incorrect captcha --> -+ <actionGroup ref="AdminLoginWithCaptchaActionGroup" stepKey="loginAsAdminWithIncorrectCaptcha"> -+ <argument name="adminUser" value="DefaultAdminUser" /> -+ <argument name="captcha" value="{{WrongCaptcha.value}}" /> -+ </actionGroup> -+ <actionGroup ref="AssertMessageOnAdminLoginActionGroup" stepKey="seeIncorrectCaptchaErrorMessage"> -+ <argument name="message" value="Incorrect CAPTCHA." /> -+ </actionGroup> -+ <actionGroup ref="AssertCaptchaVisibleOnAdminLoginFormActionGroup" stepKey="assertCaptchaVisibleAfterIncorrectCaptcha" /> -+ -+ <actionGroup ref="AdminLoginWithCaptchaActionGroup" stepKey="loginAsAdminWithCorrectCaptcha"> -+ <argument name="adminUser" value="DefaultAdminUser" /> -+ <argument name="captcha" value="{{PreconfiguredCaptcha.value}}" /> -+ </actionGroup> -+ <actionGroup ref="AssertAdminSuccessLoginActionGroup" stepKey="verifyAdminLoggedIn" /> -+ </test> -+</tests> -diff --git a/app/code/Magento/Captcha/Test/Mftf/Test/AdminResetUserPasswordFailedTest.xml b/app/code/Magento/Captcha/Test/Mftf/Test/AdminResetUserPasswordFailedTest.xml -new file mode 100644 -index 00000000000..8f9c5828e2f ---- /dev/null -+++ b/app/code/Magento/Captcha/Test/Mftf/Test/AdminResetUserPasswordFailedTest.xml -@@ -0,0 +1,19 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> -+ <test name="AdminResetUserPasswordFailedTest"> -+ <before> -+ <magentoCLI command="config:set {{AdminCaptchaDisableConfigData.path}} {{AdminCaptchaDisableConfigData.value}} " stepKey="disableAdminCaptcha"/> -+ </before> -+ <after> -+ <magentoCLI command="config:set {{AdminCaptchaEnableConfigData.path}} {{AdminCaptchaEnableConfigData.value}} " stepKey="enableAdminCaptcha"/> -+ </after> -+ </test> -+</tests> -diff --git a/app/code/Magento/Captcha/Test/Mftf/Test/CaptchaFormsDisplayingTest.xml b/app/code/Magento/Captcha/Test/Mftf/Test/CaptchaFormsDisplayingTest.xml -new file mode 100644 -index 00000000000..977ee78c0d2 ---- /dev/null -+++ b/app/code/Magento/Captcha/Test/Mftf/Test/CaptchaFormsDisplayingTest.xml -@@ -0,0 +1,117 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> -+ <test name="CaptchaFormsDisplayingTest"> -+ <annotations> -+ <features value="Captcha"/> -+ <stories value="MAGETWO-91552 - [github] CAPTCHA doesn't show when check out as guest"/> -+ <title value="Captcha forms displaying"/> -+ <description value="Captcha forms displaying"/> -+ <severity value="MAJOR"/> -+ <testCaseId value="MAGETWO-93941"/> -+ <group value="captcha"/> -+ </annotations> -+ -+ <!--Login as admin--> -+ <actionGroup ref="LoginAsAdmin" stepKey="LoginAsAdmin"/> -+ <!--Go to Captcha--> -+ <actionGroup ref="CaptchaFormsDisplayingActionGroup" stepKey="CaptchaFormsDisplayingActionGroup"/> -+ <waitForPageLoad stepKey="WaitForPageLoaded"/> -+ <!--Verify fields removed--> -+ <grabTextFrom selector="{{CaptchaFormsDisplayingSection.forms}}" stepKey="formItems"/> -+ <assertNotContains stepKey="checkoutAsGuest"> -+ <expectedResult type="string">{{CaptchaData.checkoutAsGuest}}</expectedResult> -+ <actualResult type="variable">$formItems</actualResult> -+ </assertNotContains> -+ <assertNotContains stepKey="register"> -+ <expectedResult type="string">{{CaptchaData.register}}</expectedResult> -+ <actualResult type="variable">$formItems</actualResult> -+ </assertNotContains> -+ <!--Verify fields existence--> -+ <grabTextFrom selector="{{CaptchaFormsDisplayingSection.createUser}}" stepKey="createUser"/> -+ <assertEquals stepKey="CreateUserFieldIsPresent"> -+ <expectedResult type="string">{{CaptchaData.createUser}}</expectedResult> -+ <actualResult type="variable">$createUser</actualResult> -+ </assertEquals> -+ <grabTextFrom selector="{{CaptchaFormsDisplayingSection.userLogin}}" stepKey="login"/> -+ <assertEquals stepKey="LoginFieldIsPresent"> -+ <expectedResult type="string">{{CaptchaData.login}}</expectedResult> -+ <actualResult type="variable">login</actualResult> -+ </assertEquals> -+ <grabTextFrom selector="{{CaptchaFormsDisplayingSection.forgotpassword}}" stepKey="forgotpassword"/> -+ <assertEquals stepKey="PasswordFieldIsPresent"> -+ <expectedResult type="string">{{CaptchaData.passwd}}</expectedResult> -+ <actualResult type="variable">$forgotpassword</actualResult> -+ </assertEquals> -+ <grabTextFrom selector="{{CaptchaFormsDisplayingSection.contactUs}}" stepKey="contactUs"/> -+ <assertEquals stepKey="contactUsFieldIsPresent"> -+ <expectedResult type="string">{{CaptchaData.contactUs}}</expectedResult> -+ <actualResult type="variable">$contactUs</actualResult> -+ </assertEquals> -+ <grabTextFrom selector="{{CaptchaFormsDisplayingSection.userEdit}}" stepKey="userEdit"/> -+ <assertEquals stepKey="userEditFieldIsPresent"> -+ <expectedResult type="string">{{CaptchaData.changePasswd}}</expectedResult> -+ <actualResult type="variable">$userEdit</actualResult> -+ </assertEquals> -+ -+ <!--Roll back configuration--> -+ <scrollToTopOfPage stepKey="ScrollToTop"/> -+ <click selector="{{CaptchaFormsDisplayingSection.captcha}}" stepKey="ClickToCloseCaptcha"/> -+ </test> -+ <test name="CaptchaWithDisabledGuestCheckout"> -+ <annotations> -+ <features value="Captcha"/> -+ <stories value="MC-5602 - CAPTCHA doesn't appear in login popup after refreshing page."/> -+ <title value="Captcha is displaying on login form with disabled guest checkout"/> -+ <description value="Captcha is displaying on login form with disabled guest checkout"/> -+ <severity value="MAJOR"/> -+ <testCaseId value="MAGETWO-96691"/> -+ <group value="captcha"/> -+ </annotations> -+ <before> -+ <magentoCLI command="config:set checkout/options/guest_checkout 0" stepKey="disableGuestCheckout"/> -+ <magentoCLI command="config:set customer/captcha/failed_attempts_login 1" stepKey="decreaseLoginAttempt"/> -+ <createData entity="ApiCategory" stepKey="createCategory"/> -+ <createData entity="ApiSimpleProduct" stepKey="createSimpleProduct"> -+ <requiredEntity createDataKey="createCategory"/> -+ </createData> -+ </before> -+ <after> -+ <magentoCLI command="config:set checkout/options/guest_checkout 1" stepKey="enableGuestCheckout"/> -+ <magentoCLI command="config:set customer/captcha/failed_attempts_login 3" stepKey="increaseLoginAttempt"/> -+ <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> -+ <deleteData createDataKey="createSimpleProduct" stepKey="deleteSimpleProduct1"/> -+ </after> -+ <amOnPage url="{{StorefrontProductPage.url($$createSimpleProduct.sku$$)}}" stepKey="openProductPage"/> -+ <waitForPageLoad stepKey="waitForPageLoad"/> -+ <click selector="{{StorefrontProductActionSection.addToCart}}" stepKey="addToCart" /> -+ <waitForPageLoad stepKey="waitForAddToCart"/> -+ <waitForElementVisible selector="{{StorefrontMessagesSection.success}}" stepKey="waitForSuccessMessage"/> -+ <waitForText userInput="You added $$createSimpleProduct.name$$ to your shopping cart." stepKey="waitForText"/> -+ <click selector="{{StorefrontMinicartSection.showCart}}" stepKey="clickCart"/> -+ <click selector="{{StorefrontMinicartSection.goToCheckout}}" stepKey="goToCheckout"/> -+ <waitForElementVisible selector="{{StorefrontCustomerSignInPopupFormSection.email}}" stepKey="waitEmailFieldVisible"/> -+ <fillField selector="{{StorefrontCustomerSignInPopupFormSection.email}}" userInput="{{Simple_US_Customer.email}}" stepKey="fillCustomerEmail"/> -+ <fillField selector="{{StorefrontCustomerSignInPopupFormSection.password}}" userInput="incorrectPassword" stepKey="fillIncorrectCustomerPassword"/> -+ <click selector="{{StorefrontCustomerSignInPopupFormSection.signIn}}" stepKey="clickSignIn"/> -+ <waitForElementVisible selector="{{StorefrontCustomerSignInPopupFormSection.errorMessage}}" stepKey="seeErrorMessage"/> -+ <waitForElementVisible selector="{{StorefrontCustomerSignInPopupFormSection.captchaField}}" stepKey="seeCaptchaField"/> -+ <waitForElementVisible selector="{{StorefrontCustomerSignInPopupFormSection.captchaImg}}" stepKey="seeCaptchaImage"/> -+ <waitForElementVisible selector="{{StorefrontCustomerSignInPopupFormSection.captchaReload}}" stepKey="seeCaptchaReloadButton"/> -+ <reloadPage stepKey="refreshPage"/> -+ <waitForPageLoad stepKey="waitForPageLoad2"/> -+ <click selector="{{StorefrontMinicartSection.showCart}}" stepKey="clickCart2"/> -+ <click selector="{{StorefrontMinicartSection.goToCheckout}}" stepKey="goToCheckout2"/> -+ <waitForElementVisible selector="{{StorefrontCustomerSignInPopupFormSection.email}}" stepKey="waitEmailFieldVisible2"/> -+ <waitForElementVisible selector="{{StorefrontCustomerSignInPopupFormSection.captchaField}}" stepKey="seeCaptchaField2"/> -+ <waitForElementVisible selector="{{StorefrontCustomerSignInPopupFormSection.captchaImg}}" stepKey="seeCaptchaImage2"/> -+ <waitForElementVisible selector="{{StorefrontCustomerSignInPopupFormSection.captchaReload}}" stepKey="seeCaptchaReloadButton2"/> -+ </test> -+</tests> -diff --git a/app/code/Magento/Captcha/Test/Mftf/Test/StorefrontCaptchaEditCustomerEmailTest.xml b/app/code/Magento/Captcha/Test/Mftf/Test/StorefrontCaptchaEditCustomerEmailTest.xml -new file mode 100644 -index 00000000000..54237087227 ---- /dev/null -+++ b/app/code/Magento/Captcha/Test/Mftf/Test/StorefrontCaptchaEditCustomerEmailTest.xml -@@ -0,0 +1,102 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> -+ <test name="StorefrontCaptchaEditCustomerEmailTest"> -+ <annotations> -+ <features value="Captcha"/> -+ <stories value="Customer Account Info Edit + Captcha"/> -+ <title value="Test for checking captcha on the customer account edit page."/> -+ <description value="Test for checking captcha on the customer account edit page and customer is locked."/> -+ <testCaseId value="MC-14013" /> -+ <severity value="MAJOR"/> -+ <group value="captcha"/> -+ <group value="mtf_migrated"/> -+ </annotations> -+ <before> -+ <!-- Setup CAPTCHA for testing --> -+ <magentoCLI command="config:set {{StorefrontCaptchaOnCustomerChangePasswordConfigData.path}} {{StorefrontCaptchaOnCustomerChangePasswordConfigData.value}}" stepKey="enableUserEditCaptcha"/> -+ <magentoCLI command="config:set {{StorefrontCustomerCaptchaLength3ConfigData.path}} {{StorefrontCustomerCaptchaLength3ConfigData.value}}" stepKey="setCaptchaLength" /> -+ <magentoCLI command="config:set {{StorefrontCustomerCaptchaSymbols1ConfigData.path}} {{StorefrontCustomerCaptchaSymbols1ConfigData.value}}" stepKey="setCaptchaSymbols" /> -+ <magentoCLI command="cache:clean config full_page" stepKey="cleanInvalidatedCaches"/> -+ -+ <createData entity="Simple_US_Customer" stepKey="customer"/> -+ <!-- Sign in as customer --> -+ <actionGroup ref="LoginToStorefrontActionGroup" stepKey="loginToStorefrontAccount"> -+ <argument name="Customer" value="$$customer$$"/> -+ </actionGroup> -+ </before> -+ <after> -+ <!-- Revert Captcha forms configurations --> -+ <magentoCLI command="config:set {{StorefrontCaptchaOnCustomerLoginConfigData.path}} {{StorefrontCaptchaOnCustomerLoginConfigData.value}},{{StorefrontCaptchaOnCustomerForgotPasswordConfigData.value}}" stepKey="enableCaptchaOnDefaultForms" /> -+ <magentoCLI command="config:set {{StorefrontCustomerCaptchaDefaultLengthConfigData.path}} {{StorefrontCustomerCaptchaDefaultLengthConfigData.value}}" stepKey="setDefaultCaptchaLength" /> -+ <magentoCLI command="config:set {{StorefrontCustomerCaptchaDefaultSymbolsConfigData.path}} {{StorefrontCustomerCaptchaDefaultSymbolsConfigData.value}}" stepKey="setDefaultCaptchaSymbols" /> -+ <magentoCLI command="cache:clean config full_page" stepKey="cleanInvalidatedCaches"/> -+ -+ <deleteData createDataKey="customer" stepKey="deleteCustomer"/> -+ </after> -+ -+ <!-- Open Customer edit page --> -+ <actionGroup ref="StorefrontOpenCustomerAccountInfoEditPageActionGroup" stepKey="goToCustomerEditPage" /> -+ -+ <!-- Update email with incorrect password 3 times. --> -+ <actionGroup ref="StorefrontCustomerChangeEmailActionGroup" stepKey="changeEmailFirstAttempt"> -+ <argument name="email" value="$$customer.email$$" /> -+ <argument name="password" value="{{Colorado_US_Customer.password}}" /> -+ </actionGroup> -+ -+ <actionGroup ref="AssertMessageCustomerChangeAccountInfoActionGroup" stepKey="assertAccountMessageFirstAttempt"> -+ <argument name="message" value="The password doesn't match this account. Verify the password and try again." /> -+ <argument name="messageType" value="error" /> -+ </actionGroup> -+ -+ <actionGroup ref="StorefrontCustomerChangeEmailActionGroup" stepKey="changeEmailSecondAttempt"> -+ <argument name="email" value="$$customer.email$$" /> -+ <argument name="password" value="{{Colorado_US_Customer.password}}" /> -+ </actionGroup> -+ -+ <actionGroup ref="AssertMessageCustomerChangeAccountInfoActionGroup" stepKey="assertAccountMessageSecondAttempt"> -+ <argument name="message" value="The password doesn't match this account. Verify the password and try again." /> -+ <argument name="messageType" value="error" /> -+ </actionGroup> -+ -+ <actionGroup ref="StorefrontCustomerChangeEmailActionGroup" stepKey="changeEmailThirdAttempt"> -+ <argument name="email" value="$$customer.email$$" /> -+ <argument name="password" value="{{Colorado_US_Customer.password}}" /> -+ </actionGroup> -+ -+ <actionGroup ref="AssertMessageCustomerChangeAccountInfoActionGroup" stepKey="assertAccountMessageThirdAttempt"> -+ <argument name="message" value="The password doesn't match this account. Verify the password and try again." /> -+ <argument name="messageType" value="error" /> -+ </actionGroup> -+ -+ <!-- Check captcha visibility after incorrect password submit form --> -+ <actionGroup ref="AssertCaptchaVisibleOnCustomerAccountInfoActionGroup" stepKey="assertCaptchaVisible" /> -+ -+ <!-- Try to submit form with incorrect captcha --> -+ <actionGroup ref="StorefrontCustomerChangeEmailWithCaptchaActionGroup" stepKey="changeEmailWithIncorrectCaptcha"> -+ <argument name="email" value="$$customer.email$$" /> -+ <argument name="password" value="{{Colorado_US_Customer.password}}" /> -+ <argument name="captcha" value="{{WrongCaptcha.value}}" /> -+ </actionGroup> -+ -+ <actionGroup ref="AssertMessageCustomerChangeAccountInfoActionGroup" stepKey="assertAccountMessageAfterIncorrectCaptcha"> -+ <argument name="message" value="Incorrect CAPTCHA" /> -+ <argument name="messageType" value="error" /> -+ </actionGroup> -+ -+ <!-- Update customer email correct password and CAPTCHA --> -+ <actionGroup ref="StorefrontCustomerChangeEmailWithCaptchaActionGroup" stepKey="changeEmailCorrectAttempt"> -+ <argument name="email" value="$$customer.email$$" /> -+ <argument name="password" value="$$customer.password$$" /> -+ <argument name="captcha" value="{{PreconfiguredCaptcha.value}}" /> -+ </actionGroup> -+ <actionGroup ref="AssertMessageCustomerChangeAccountInfoActionGroup" stepKey="assertAccountMessageCorrectAttempt" /> -+ </test> -+</tests> -diff --git a/app/code/Magento/Captcha/Test/Mftf/Test/StorefrontCaptchaOnContactUsTest.xml b/app/code/Magento/Captcha/Test/Mftf/Test/StorefrontCaptchaOnContactUsTest.xml -new file mode 100644 -index 00000000000..0c6a3f31c1d ---- /dev/null -+++ b/app/code/Magento/Captcha/Test/Mftf/Test/StorefrontCaptchaOnContactUsTest.xml -@@ -0,0 +1,64 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> -+ <test name="StorefrontCaptchaOnContactUsTest"> -+ <annotations> -+ <features value="Captcha"/> -+ <stories value="Submit Contact us form + Captcha"/> -+ <title value="Captcha on contact us form test"/> -+ <description value="Test creation for send comment using the contact us form with captcha."/> -+ <testCaseId value="MC-14103" /> -+ <severity value="MAJOR"/> -+ <group value="captcha"/> -+ <group value="mtf_migrated"/> -+ </annotations> -+ <before> -+ <magentoCLI command="config:set {{StorefrontCustomerCaptchaLength3ConfigData.path}} {{StorefrontCustomerCaptchaLength3ConfigData.value}}" stepKey="setCaptchaLength" /> -+ <magentoCLI command="config:set {{StorefrontCustomerCaptchaSymbols1ConfigData.path}} {{StorefrontCustomerCaptchaSymbols1ConfigData.value}}" stepKey="setCaptchaSymbols" /> -+ <magentoCLI command="config:set {{StorefrontCaptchaOnContactUsFormConfigData.path}} {{StorefrontCaptchaOnContactUsFormConfigData.value}}" stepKey="enableUserEditCaptcha"/> -+ <magentoCLI command="cache:clean config full_page" stepKey="cleanInvalidatedCaches"/> -+ </before> -+ <after> -+ <magentoCLI command="config:set {{StorefrontCustomerCaptchaDefaultLengthConfigData.path}} {{StorefrontCustomerCaptchaDefaultLengthConfigData.value}}" stepKey="setDefaultCaptchaLength" /> -+ <magentoCLI command="config:set {{StorefrontCustomerCaptchaDefaultSymbolsConfigData.path}} {{StorefrontCustomerCaptchaDefaultSymbolsConfigData.value}}" stepKey="setDefaultCaptchaSymbols" /> -+ <magentoCLI command="config:set {{StorefrontCaptchaOnCustomerLoginConfigData.path}} {{StorefrontCaptchaOnCustomerLoginConfigData.value}},{{StorefrontCaptchaOnCustomerForgotPasswordConfigData.value}}" stepKey="enableCaptchaOnDefaultForms" /> -+ <magentoCLI command="cache:clean config full_page" stepKey="cleanInvalidatedCaches"/> -+ </after> -+ -+ <!-- Open storefront contact us form --> -+ <actionGroup ref="StorefrontOpenContactUsPageActionGroup" stepKey="goToContactUsPage" /> -+ -+ <!-- Check Captcha items --> -+ <actionGroup ref="AssertCaptchaVisibleOnContactUsFormActionGroup" stepKey="seeCaptchaOnContactUsForm" /> -+ -+ <!-- Submit Contact Us form --> -+ <actionGroup ref="StorefrontFillContactUsFormWithCaptchaActionGroup" stepKey="fillContactUsFormWithWrongCaptcha"> -+ <argument name="customer" value="Simple_US_Customer" /> -+ <argument name="contactUsData" value="DefaultContactUsData" /> -+ <argument name="captcha" value="{{WrongCaptcha.value}}" /> -+ </actionGroup> -+ <actionGroup ref="StorefrontSubmitContactUsFormActionGroup" stepKey="submitContactUsFormWithWrongCaptcha" /> -+ -+ <!-- Check Captcha items after form reload --> -+ <actionGroup ref="AssertMessageContactUsFormActionGroup" stepKey="verifyErrorMessage"> -+ <argument name="message" value="Incorrect CAPTCHA" /> -+ <argument name="messageType" value="error" /> -+ </actionGroup> -+ <actionGroup ref="AssertCaptchaVisibleOnContactUsFormActionGroup" stepKey="seeCaptchaOnContactUsFormAfterWrongCaptcha" /> -+ -+ <actionGroup ref="StorefrontFillContactUsFormWithCaptchaActionGroup" stepKey="fillContactUsFormWithCorrectCaptcha"> -+ <argument name="customer" value="Simple_US_Customer" /> -+ <argument name="contactUsData" value="DefaultContactUsData" /> -+ <argument name="captcha" value="{{PreconfiguredCaptcha.value}}" /> -+ </actionGroup> -+ <actionGroup ref="StorefrontSubmitContactUsFormActionGroup" stepKey="submitContactUsFormWithCorrectCaptcha" /> -+ <actionGroup ref="AssertMessageContactUsFormActionGroup" stepKey="verifySuccessMessage" /> -+ </test> -+</tests> -diff --git a/app/code/Magento/Captcha/Test/Mftf/Test/StorefrontCaptchaOnCustomerLoginTest.xml b/app/code/Magento/Captcha/Test/Mftf/Test/StorefrontCaptchaOnCustomerLoginTest.xml -new file mode 100644 -index 00000000000..5a1be68d3f2 ---- /dev/null -+++ b/app/code/Magento/Captcha/Test/Mftf/Test/StorefrontCaptchaOnCustomerLoginTest.xml -@@ -0,0 +1,79 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> -+ <test name="StorefrontCaptchaOnCustomerLoginTest"> -+ <annotations> -+ <features value="Captcha"/> -+ <stories value="Login with Customer Account + Captcha"/> -+ <title value="Captcha customer login page test"/> -+ <description value="Check CAPTCHA on Storefront Login Page."/> -+ <severity value="MAJOR"/> -+ <testCaseId value="MC-14010" /> -+ <group value="captcha"/> -+ <group value="mtf_migrated"/> -+ </annotations> -+ <before> -+ <magentoCLI command="config:set {{StorefrontCustomerCaptchaLength3ConfigData.path}} {{StorefrontCustomerCaptchaLength3ConfigData.value}}" stepKey="setCaptchaLength" /> -+ <magentoCLI command="config:set {{StorefrontCustomerCaptchaSymbols1ConfigData.path}} {{StorefrontCustomerCaptchaSymbols1ConfigData.value}}" stepKey="setCaptchaSymbols" /> -+ <magentoCLI command="cache:clean config full_page" stepKey="cleanInvalidatedCaches"/> -+ <createData entity="Simple_US_Customer" stepKey="customer"/> -+ </before> -+ <after> -+ <magentoCLI command="config:set {{StorefrontCustomerCaptchaDefaultLengthConfigData.path}} {{StorefrontCustomerCaptchaDefaultLengthConfigData.value}}" stepKey="setDefaultCaptchaLength" /> -+ <magentoCLI command="config:set {{StorefrontCustomerCaptchaDefaultSymbolsConfigData.path}} {{StorefrontCustomerCaptchaDefaultSymbolsConfigData.value}}" stepKey="setDefaultCaptchaSymbols" /> -+ <magentoCLI command="cache:clean config full_page" stepKey="cleanInvalidatedCaches"/> -+ <deleteData createDataKey="customer" stepKey="deleteCustomer"/> -+ </after> -+ -+ <!-- Open storefront login form --> -+ <actionGroup ref="StorefrontOpenCustomerLoginPageActionGroup" stepKey="goToSignInPage" /> -+ -+ <!-- Login with wrong credentials 3 times --> -+ <actionGroup ref="StorefrontFillCustomerLoginFormActionGroup" stepKey="fillLoginFormFirstAttempt"> -+ <argument name="customer" value="Colorado_US_Customer" /> -+ </actionGroup> -+ <actionGroup ref="StorefrontClickSignOnCustomerLoginFormActionGroup" stepKey="clickSignInAccountButtonFirstAttempt" /> -+ <actionGroup ref="AssertMessageCustomerLoginActionGroup" stepKey="seeErrorMessageAfterFirstAttempt" /> -+ <actionGroup ref="AssertCaptchaNotVisibleOnCustomerLoginFormActionGroup" stepKey="dontSeeCaptchaAfterFirstAttempt" /> -+ -+ <actionGroup ref="StorefrontFillCustomerLoginFormActionGroup" stepKey="fillLoginFormSecondAttempt"> -+ <argument name="customer" value="Colorado_US_Customer" /> -+ </actionGroup> -+ <actionGroup ref="StorefrontClickSignOnCustomerLoginFormActionGroup" stepKey="clickSignInAccountButtonSecondAttempt" /> -+ <actionGroup ref="AssertMessageCustomerLoginActionGroup" stepKey="seeErrorMessageAfterSecondAttempt" /> -+ <actionGroup ref="AssertCaptchaNotVisibleOnCustomerLoginFormActionGroup" stepKey="dontSeeCaptchaAfterSecondAttempt" /> -+ -+ <actionGroup ref="StorefrontFillCustomerLoginFormActionGroup" stepKey="fillLoginFormThirdAttempt"> -+ <argument name="customer" value="Colorado_US_Customer" /> -+ </actionGroup> -+ <actionGroup ref="StorefrontClickSignOnCustomerLoginFormActionGroup" stepKey="clickSignInAccountButtonThirdAttempt" /> -+ <actionGroup ref="AssertMessageCustomerLoginActionGroup" stepKey="seeErrorMessageAfterThirdAttempt" /> -+ <actionGroup ref="AssertCaptchaVisibleOnCustomerLoginFormActionGroup" stepKey="seeCaptchaAfterThirdAttempt" /> -+ -+ <!-- Submit form with incorrect captcha --> -+ <actionGroup ref="StorefrontFillCustomerLoginFormWithCaptchaActionGroup" stepKey="fillLoginFormCorrectAccountIncorrectCaptcha"> -+ <argument name="customer" value="$$customer$$" /> -+ <argument name="captcha" value="{{WrongCaptcha.value}}" /> -+ </actionGroup> -+ <actionGroup ref="StorefrontClickSignOnCustomerLoginFormActionGroup" stepKey="clickSignInAccountButtonCorrectAccountIncorrectCaptcha" /> -+ <actionGroup ref="AssertMessageCustomerLoginActionGroup" stepKey="seeErrorMessageAfterIncorrectCaptcha"> -+ <argument name="message" value="Incorrect CAPTCHA" /> -+ </actionGroup> -+ -+ <actionGroup ref="StorefrontFillCustomerLoginFormWithCaptchaActionGroup" stepKey="fillLoginFormCorrectAccountCorrectCaptcha"> -+ <argument name="customer" value="$$customer$$" /> -+ <argument name="captcha" value="{{PreconfiguredCaptcha.value}}" /> -+ </actionGroup> -+ <actionGroup ref="StorefrontClickSignOnCustomerLoginFormActionGroup" stepKey="clickSignInAccountButtonCorrectAccountCorrectCaptcha" /> -+ <actionGroup ref="AssertCustomerWelcomeMessageActionGroup" stepKey="assertCustomerLoggedIn"> -+ <argument name="customerFullName" value="$$customer.firstname$$ $$customer.lastname$$" /> -+ </actionGroup> -+ </test> -+</tests> -diff --git a/app/code/Magento/Captcha/Test/Mftf/Test/StorefrontCaptchaRegisterNewCustomerTest.xml b/app/code/Magento/Captcha/Test/Mftf/Test/StorefrontCaptchaRegisterNewCustomerTest.xml -new file mode 100644 -index 00000000000..2c331f958e4 ---- /dev/null -+++ b/app/code/Magento/Captcha/Test/Mftf/Test/StorefrontCaptchaRegisterNewCustomerTest.xml -@@ -0,0 +1,68 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> -+ <test name="StorefrontCaptchaRegisterNewCustomerTest"> -+ <annotations> -+ <features value="Captcha"/> -+ <stories value="Create New Customer Account + Captcha"/> -+ <title value="Test creation for customer register with captcha on storefront."/> -+ <description value="Test creation for customer register with captcha on storefront."/> -+ <severity value="MAJOR"/> -+ <testCaseId value="MC-14805" /> -+ <group value="captcha"/> -+ <group value="mtf_migrated"/> -+ </annotations> -+ <before> -+ <!-- Enable captcha for customer. --> -+ <magentoCLI command="config:set {{StorefrontCaptchaOnCustomerCreateFormConfigData.path}} {{StorefrontCaptchaOnCustomerCreateFormConfigData.value}}" stepKey="enableUserRegistrationCaptcha" /> -+ <magentoCLI command="config:set {{StorefrontCustomerCaptchaModeAlwaysConfigData.path}} {{StorefrontCustomerCaptchaModeAlwaysConfigData.value}}" stepKey="alwaysEnableCaptcha" /> -+ <magentoCLI command="config:set {{StorefrontCustomerCaptchaLength3ConfigData.path}} {{StorefrontCustomerCaptchaLength3ConfigData.value}}" stepKey="setCaptchaLength" /> -+ <magentoCLI command="config:set {{StorefrontCustomerCaptchaSymbols1ConfigData.path}} {{StorefrontCustomerCaptchaSymbols1ConfigData.value}}" stepKey="setCaptchaSymbols" /> -+ <magentoCLI command="cache:clean config full_page" stepKey="cleanInvalidatedCaches"/> -+ </before> -+ <after> -+ <!-- Set default configuration. --> -+ <magentoCLI command="config:set {{StorefrontCaptchaOnCustomerLoginConfigData.path}} {{StorefrontCaptchaOnCustomerLoginConfigData.value}},{{StorefrontCaptchaOnCustomerForgotPasswordConfigData.value}}" stepKey="enableCaptchaOnDefaultForms" /> -+ <magentoCLI command="config:set {{StorefrontCustomerCaptchaModeAfterFailConfigData.path}} {{StorefrontCustomerCaptchaModeAfterFailConfigData.value}}" stepKey="defaultCaptchaMode" /> -+ <magentoCLI command="config:set {{StorefrontCustomerCaptchaDefaultLengthConfigData.path}} {{StorefrontCustomerCaptchaDefaultLengthConfigData.value}}" stepKey="setDefaultCaptchaLength" /> -+ <magentoCLI command="config:set {{StorefrontCustomerCaptchaDefaultSymbolsConfigData.path}} {{StorefrontCustomerCaptchaDefaultSymbolsConfigData.value}}" stepKey="setDefaultCaptchaSymbols" /> -+ <magentoCLI command="cache:clean config full_page" stepKey="cleanInvalidatedCaches"/> -+ </after> -+ -+ <!-- Open Customer registration page --> -+ <actionGroup ref="StorefrontOpenCustomerAccountCreatePageActionGroup" stepKey="goToCustomerAccountCreatePage" /> -+ -+ <!-- Check captcha visibility registration page load --> -+ <actionGroup ref="AssertCaptchaVisibleOnCustomerAccountCreatePageActionGroup" stepKey="verifyCaptchaVisible" /> -+ -+ <!-- Submit form with incorrect captcha --> -+ <actionGroup ref="StorefrontFillCustomerAccountCreationFormWithCaptchaActionGroup" stepKey="fillNewCustomerAccountFormWithIncorrectCaptcha"> -+ <argument name="customer" value="Simple_US_Customer" /> -+ <argument name="captcha" value="{{WrongCaptcha.value}}" /> -+ </actionGroup> -+ -+ <actionGroup ref="StorefrontClickCreateAnAccountCustomerAccountCreationFormActionGroup" stepKey="clickCreateAnAccountButton" /> -+ -+ <actionGroup ref="AssertMessageCustomerCreateAccountActionGroup" stepKey="assertMessage"> -+ <argument name="message" value="Incorrect CAPTCHA" /> -+ <argument name="messageType" value="error" /> -+ </actionGroup> -+ -+ <actionGroup ref="AssertCaptchaVisibleOnCustomerAccountCreatePageActionGroup" stepKey="verifyCaptchaVisibleAfterFail" /> -+ -+ <!-- Submit form with correct captcha --> -+ <actionGroup ref="StorefrontFillCustomerAccountCreationFormWithCaptchaActionGroup" stepKey="fillNewCustomerAccountFormWithCorrectCaptcha"> -+ <argument name="customer" value="Simple_US_Customer" /> -+ <argument name="captcha" value="{{PreconfiguredCaptcha.value}}" /> -+ </actionGroup> -+ <actionGroup ref="StorefrontClickCreateAnAccountCustomerAccountCreationFormActionGroup" stepKey="clickCreateAnAccountButtonAfterCorrectCaptcha" /> -+ <actionGroup ref="AssertMessageCustomerCreateAccountActionGroup" stepKey="assertSuccessMessage" /> -+ </test> -+</tests> -diff --git a/app/code/Magento/Captcha/Test/Mftf/Test/StorefrontResetCustomerPasswordFailedTest.xml b/app/code/Magento/Captcha/Test/Mftf/Test/StorefrontResetCustomerPasswordFailedTest.xml -new file mode 100644 -index 00000000000..36d7989b9ac ---- /dev/null -+++ b/app/code/Magento/Captcha/Test/Mftf/Test/StorefrontResetCustomerPasswordFailedTest.xml -@@ -0,0 +1,19 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> -+ <test name="StorefrontResetCustomerPasswordFailedTest"> -+ <before> -+ <magentoCLI command="config:set {{StorefrontCustomerCaptchaDisableConfigData.path}} {{StorefrontCustomerCaptchaDisableConfigData.value}}" stepKey="disableCaptcha"/> -+ </before> -+ <after> -+ <magentoCLI command="config:set {{StorefrontCustomerCaptchaEnableConfigData.path}} {{StorefrontCustomerCaptchaEnableConfigData.value}}" stepKey="enableCaptcha"/> -+ </after> -+ </test> -+</tests> -diff --git a/app/code/Magento/Captcha/Test/Unit/Model/Checkout/ConfigProviderTest.php b/app/code/Magento/Captcha/Test/Unit/Model/Checkout/ConfigProviderTest.php -index 655fcd6118e..8764dbd4cec 100644 ---- a/app/code/Magento/Captcha/Test/Unit/Model/Checkout/ConfigProviderTest.php -+++ b/app/code/Magento/Captcha/Test/Unit/Model/Checkout/ConfigProviderTest.php -@@ -77,6 +77,7 @@ class ConfigProviderTest extends \PHPUnit\Framework\TestCase - ->will($this->returnValue('https://magento.com/captcha')); - - $config = $this->model->getConfig(); -+ unset($config['captcha'][$this->formId]['timestamp']); - $this->assertEquals($config, $expectedConfig); - } - -diff --git a/app/code/Magento/Captcha/Test/Unit/Model/DefaultTest.php b/app/code/Magento/Captcha/Test/Unit/Model/DefaultTest.php -index 0500b29f787..eef75d2c01e 100644 ---- a/app/code/Magento/Captcha/Test/Unit/Model/DefaultTest.php -+++ b/app/code/Magento/Captcha/Test/Unit/Model/DefaultTest.php -@@ -24,7 +24,7 @@ class DefaultTest extends \PHPUnit\Framework\TestCase - 'enable' => '1', - 'font' => 'linlibertine', - 'mode' => 'after_fail', -- 'forms' => 'user_forgotpassword,user_create,guest_checkout,register_during_checkout', -+ 'forms' => 'user_forgotpassword,user_create', - 'failed_attempts_login' => '3', - 'failed_attempts_ip' => '1000', - 'timeout' => '7', -@@ -35,8 +35,6 @@ class DefaultTest extends \PHPUnit\Framework\TestCase - 'always_for' => [ - 'user_create', - 'user_forgotpassword', -- 'guest_checkout', -- 'register_during_checkout', - 'contact_us', - ], - ]; -@@ -185,7 +183,13 @@ class DefaultTest extends \PHPUnit\Framework\TestCase - { - self::$_defaultConfig['case_sensitive'] = '1'; - $this->assertFalse($this->_object->isCorrect('abcdef5')); -- $sessionData = ['user_create_word' => ['data' => 'AbCdEf5', 'expires' => time() + self::EXPIRE_FRAME]]; -+ $sessionData = [ -+ 'user_create_word' => [ -+ 'data' => 'AbCdEf5', -+ 'words' => 'AbCdEf5', -+ 'expires' => time() + self::EXPIRE_FRAME -+ ] -+ ]; - $this->_object->getSession()->setData($sessionData); - self::$_defaultConfig['case_sensitive'] = '0'; - $this->assertTrue($this->_object->isCorrect('abcdef5')); -@@ -226,7 +230,7 @@ class DefaultTest extends \PHPUnit\Framework\TestCase - { - $this->assertEquals($this->_object->getWord(), 'AbCdEf5'); - $this->_object->getSession()->setData( -- ['user_create_word' => ['data' => 'AbCdEf5', 'expires' => time() - 360]] -+ ['user_create_word' => ['data' => 'AbCdEf5', 'words' => 'AbCdEf5','expires' => time() - 360]] - ); - $this->assertNull($this->_object->getWord()); - } -@@ -249,7 +253,13 @@ class DefaultTest extends \PHPUnit\Framework\TestCase - ->getMock(); - $session->expects($this->any())->method('isLoggedIn')->will($this->returnValue(false)); - -- $session->setData(['user_create_word' => ['data' => 'AbCdEf5', 'expires' => time() + self::EXPIRE_FRAME]]); -+ $session->setData([ -+ 'user_create_word' => [ -+ 'data' => 'AbCdEf5', -+ 'words' => 'AbCdEf5', -+ 'expires' => time() + self::EXPIRE_FRAME -+ ] -+ ]); - return $session; - } - -@@ -362,8 +372,7 @@ class DefaultTest extends \PHPUnit\Framework\TestCase - return [ - [true, 'contact_us'], - [false, 'user_create'], -- [false, 'user_forgotpassword'], -- [false, 'guest_checkout'] -+ [false, 'user_forgotpassword'] - ]; - } - } -diff --git a/app/code/Magento/Captcha/Test/Unit/Observer/CheckGuestCheckoutObserverTest.php b/app/code/Magento/Captcha/Test/Unit/Observer/CheckGuestCheckoutObserverTest.php -deleted file mode 100644 -index d3f29fae8a5..00000000000 ---- a/app/code/Magento/Captcha/Test/Unit/Observer/CheckGuestCheckoutObserverTest.php -+++ /dev/null -@@ -1,211 +0,0 @@ --<?php --/** -- * Copyright © Magento, Inc. All rights reserved. -- * See COPYING.txt for license details. -- */ --declare(strict_types=1); -- --namespace Magento\Captcha\Test\Unit\Observer; -- --use Magento\Captcha\Model\DefaultModel as CaptchaModel; --use Magento\Captcha\Observer\CheckGuestCheckoutObserver; --use Magento\Captcha\Helper\Data as CaptchaDataHelper; --use Magento\Framework\App\Action\Action; --use Magento\Framework\App\ActionFlag; --use Magento\Captcha\Observer\CaptchaStringResolver; --use Magento\Checkout\Model\Type\Onepage; --use Magento\Framework\App\Request\Http; --use Magento\Framework\App\Response\Http as HttpResponse; --use Magento\Framework\Event\Observer; --use Magento\Framework\Json\Helper\Data as JsonHelper; --use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; --use Magento\Quote\Model\Quote; -- --/** -- * @SuppressWarnings(PHPMD.CouplingBetweenObjects) -- */ --class CheckGuestCheckoutObserverTest extends \PHPUnit\Framework\TestCase --{ -- const FORM_ID = 'guest_checkout'; -- -- /** -- * @var CheckGuestCheckoutObserver -- */ -- private $checkGuestCheckoutObserver; -- -- /** -- * @var ObjectManager -- */ -- private $objectManager; -- -- /** -- * @var Observer -- */ -- private $observer; -- -- /** -- * @var HttpResponse|\PHPUnit_Framework_MockObject_MockObject -- */ -- private $responseMock; -- -- /** -- * @var HttpResponse|\PHPUnit_Framework_MockObject_MockObject -- */ -- private $requestMock; -- -- /** -- * @var ActionFlag|\PHPUnit_Framework_MockObject_MockObject -- */ -- private $actionFlagMock; -- -- /** -- * @var CaptchaStringResolver|\PHPUnit_Framework_MockObject_MockObject -- */ -- private $captchaStringResolverMock; -- -- /** -- * @var JsonHelper|\PHPUnit_Framework_MockObject_MockObject -- */ -- private $jsonHelperMock; -- -- /** -- * @var CaptchaModel|\PHPUnit_Framework_MockObject_MockObject -- */ -- private $captchaModelMock; -- -- /** -- * @var Quote|\PHPUnit_Framework_MockObject_MockObject -- */ -- private $quoteModelMock; -- -- /** -- * @var Action|\PHPUnit_Framework_MockObject_MockObject -- */ -- private $controllerMock; -- -- protected function setUp() -- { -- $onepageModelTypeMock = $this->createMock(Onepage::class); -- $captchaHelperMock = $this->createMock(CaptchaDataHelper::class); -- $this->objectManager = new ObjectManager($this); -- $this->actionFlagMock = $this->createMock(ActionFlag::class); -- $this->captchaStringResolverMock = $this->createMock(CaptchaStringResolver::class); -- $this->captchaModelMock = $this->createMock(CaptchaModel::class); -- $this->quoteModelMock = $this->createMock(Quote::class); -- $this->controllerMock = $this->createMock(Action::class); -- $this->requestMock = $this->createMock(Http::class); -- $this->responseMock = $this->createMock(HttpResponse::class); -- $this->observer = new Observer(['controller_action' => $this->controllerMock]); -- $this->jsonHelperMock = $this->createMock(JsonHelper::class); -- -- $this->checkGuestCheckoutObserver = $this->objectManager->getObject( -- CheckGuestCheckoutObserver::class, -- [ -- 'helper' => $captchaHelperMock, -- 'actionFlag' => $this->actionFlagMock, -- 'captchaStringResolver' => $this->captchaStringResolverMock, -- 'typeOnepage' => $onepageModelTypeMock, -- 'jsonHelper' => $this->jsonHelperMock -- ] -- ); -- -- $captchaHelperMock->expects($this->once()) -- ->method('getCaptcha') -- ->with(self::FORM_ID) -- ->willReturn($this->captchaModelMock); -- $onepageModelTypeMock->expects($this->once()) -- ->method('getQuote') -- ->willReturn($this->quoteModelMock); -- } -- -- public function testCheckGuestCheckoutForRegister() -- { -- $this->quoteModelMock->expects($this->once()) -- ->method('getCheckoutMethod') -- ->willReturn(Onepage::METHOD_REGISTER); -- $this->captchaModelMock->expects($this->never()) -- ->method('isRequired'); -- -- $this->checkGuestCheckoutObserver->execute($this->observer); -- } -- -- public function testCheckGuestCheckoutWithNoCaptchaRequired() -- { -- $this->quoteModelMock->expects($this->once()) -- ->method('getCheckoutMethod') -- ->willReturn(Onepage::METHOD_GUEST); -- $this->captchaModelMock->expects($this->once()) -- ->method('isRequired') -- ->willReturn(false); -- $this->captchaModelMock->expects($this->never()) -- ->method('isCorrect'); -- -- $this->checkGuestCheckoutObserver->execute($this->observer); -- } -- -- public function testCheckGuestCheckoutWithIncorrectCaptcha() -- { -- $captchaValue = 'some_word'; -- $encodedJsonValue = '{}'; -- -- $this->quoteModelMock->expects($this->once()) -- ->method('getCheckoutMethod') -- ->willReturn(Onepage::METHOD_GUEST); -- $this->captchaModelMock->expects($this->once()) -- ->method('isRequired') -- ->willReturn(true); -- $this->controllerMock->expects($this->once()) -- ->method('getRequest') -- ->willReturn($this->requestMock); -- $this->controllerMock->expects($this->once()) -- ->method('getResponse') -- ->willReturn($this->responseMock); -- $this->controllerMock->expects($this->once()) -- ->method('getResponse') -- ->willReturn($this->responseMock); -- $this->captchaStringResolverMock->expects($this->once()) -- ->method('resolve') -- ->with($this->requestMock, self::FORM_ID) -- ->willReturn($captchaValue); -- $this->captchaModelMock->expects($this->once()) -- ->method('isCorrect') -- ->with($captchaValue) -- ->willReturn(false); -- $this->actionFlagMock->expects($this->once()) -- ->method('set') -- ->with('', Action::FLAG_NO_DISPATCH, true); -- $this->jsonHelperMock->expects($this->once()) -- ->method('jsonEncode') -- ->willReturn($encodedJsonValue); -- $this->responseMock->expects($this->once()) -- ->method('representJson') -- ->with($encodedJsonValue); -- -- $this->checkGuestCheckoutObserver->execute($this->observer); -- } -- -- public function testCheckGuestCheckoutWithCorrectCaptcha() -- { -- $this->quoteModelMock->expects($this->once()) -- ->method('getCheckoutMethod') -- ->willReturn(Onepage::METHOD_GUEST); -- $this->captchaModelMock->expects($this->once()) -- ->method('isRequired') -- ->willReturn(true); -- $this->controllerMock->expects($this->once()) -- ->method('getRequest') -- ->willReturn($this->requestMock); -- $this->captchaStringResolverMock->expects($this->once()) -- ->method('resolve') -- ->with($this->requestMock, self::FORM_ID) -- ->willReturn('some_word'); -- $this->captchaModelMock->expects($this->once()) -- ->method('isCorrect') -- ->with('some_word') -- ->willReturn(true); -- $this->actionFlagMock->expects($this->never()) -- ->method('set'); -- -- $this->checkGuestCheckoutObserver->execute($this->observer); -- } --} -diff --git a/app/code/Magento/Captcha/Test/Unit/Observer/CheckRegisterCheckoutObserverTest.php b/app/code/Magento/Captcha/Test/Unit/Observer/CheckRegisterCheckoutObserverTest.php -deleted file mode 100644 -index 89012ef6538..00000000000 ---- a/app/code/Magento/Captcha/Test/Unit/Observer/CheckRegisterCheckoutObserverTest.php -+++ /dev/null -@@ -1,211 +0,0 @@ --<?php --/** -- * Copyright © Magento, Inc. All rights reserved. -- * See COPYING.txt for license details. -- */ --declare(strict_types=1); -- --namespace Magento\Captcha\Test\Unit\Observer; -- --use Magento\Captcha\Model\DefaultModel as CaptchaModel; --use Magento\Captcha\Observer\CheckRegisterCheckoutObserver; --use Magento\Captcha\Helper\Data as CaptchaDataHelper; --use Magento\Framework\App\Action\Action; --use Magento\Framework\App\ActionFlag; --use Magento\Captcha\Observer\CaptchaStringResolver; --use Magento\Checkout\Model\Type\Onepage; --use Magento\Framework\App\Request\Http; --use Magento\Framework\App\Response\Http as HttpResponse; --use Magento\Framework\Event\Observer; --use Magento\Framework\Json\Helper\Data as JsonHelper; --use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; --use Magento\Quote\Model\Quote; -- --/** -- * @SuppressWarnings(PHPMD.CouplingBetweenObjects) -- */ --class CheckRegisterCheckoutObserverTest extends \PHPUnit\Framework\TestCase --{ -- const FORM_ID = 'register_during_checkout'; -- -- /** -- * @var CheckRegisterCheckoutObserver -- */ -- private $checkRegisterCheckoutObserver; -- -- /** -- * @var ObjectManager -- */ -- private $objectManager; -- -- /** -- * @var Observer -- */ -- private $observer; -- -- /** -- * @var HttpResponse|\PHPUnit_Framework_MockObject_MockObject -- */ -- private $responseMock; -- -- /** -- * @var HttpResponse|\PHPUnit_Framework_MockObject_MockObject -- */ -- private $requestMock; -- -- /** -- * @var ActionFlag|\PHPUnit_Framework_MockObject_MockObject -- */ -- private $actionFlagMock; -- -- /** -- * @var CaptchaStringResolver|\PHPUnit_Framework_MockObject_MockObject -- */ -- private $captchaStringResolverMock; -- -- /** -- * @var JsonHelper|\PHPUnit_Framework_MockObject_MockObject -- */ -- private $jsonHelperMock; -- -- /** -- * @var CaptchaModel|\PHPUnit_Framework_MockObject_MockObject -- */ -- private $captchaModelMock; -- -- /** -- * @var Quote|\PHPUnit_Framework_MockObject_MockObject -- */ -- private $quoteModelMock; -- -- /** -- * @var Action|\PHPUnit_Framework_MockObject_MockObject -- */ -- private $controllerMock; -- -- protected function setUp() -- { -- $onepageModelTypeMock = $this->createMock(Onepage::class); -- $captchaHelperMock = $this->createMock(CaptchaDataHelper::class); -- $this->objectManager = new ObjectManager($this); -- $this->actionFlagMock = $this->createMock(ActionFlag::class); -- $this->captchaStringResolverMock = $this->createMock(CaptchaStringResolver::class); -- $this->captchaModelMock = $this->createMock(CaptchaModel::class); -- $this->quoteModelMock = $this->createMock(Quote::class); -- $this->controllerMock = $this->createMock(Action::class); -- $this->requestMock = $this->createMock(Http::class); -- $this->responseMock = $this->createMock(HttpResponse::class); -- $this->observer = new Observer(['controller_action' => $this->controllerMock]); -- $this->jsonHelperMock = $this->createMock(JsonHelper::class); -- -- $this->checkRegisterCheckoutObserver = $this->objectManager->getObject( -- CheckRegisterCheckoutObserver::class, -- [ -- 'helper' => $captchaHelperMock, -- 'actionFlag' => $this->actionFlagMock, -- 'captchaStringResolver' => $this->captchaStringResolverMock, -- 'typeOnepage' => $onepageModelTypeMock, -- 'jsonHelper' => $this->jsonHelperMock -- ] -- ); -- -- $captchaHelperMock->expects($this->once()) -- ->method('getCaptcha') -- ->with(self::FORM_ID) -- ->willReturn($this->captchaModelMock); -- $onepageModelTypeMock->expects($this->once()) -- ->method('getQuote') -- ->willReturn($this->quoteModelMock); -- } -- -- public function testCheckRegisterCheckoutForGuest() -- { -- $this->quoteModelMock->expects($this->once()) -- ->method('getCheckoutMethod') -- ->willReturn(Onepage::METHOD_GUEST); -- $this->captchaModelMock->expects($this->never()) -- ->method('isRequired'); -- -- $this->checkRegisterCheckoutObserver->execute($this->observer); -- } -- -- public function testCheckRegisterCheckoutWithNoCaptchaRequired() -- { -- $this->quoteModelMock->expects($this->once()) -- ->method('getCheckoutMethod') -- ->willReturn(Onepage::METHOD_REGISTER); -- $this->captchaModelMock->expects($this->once()) -- ->method('isRequired') -- ->willReturn(false); -- $this->captchaModelMock->expects($this->never()) -- ->method('isCorrect'); -- -- $this->checkRegisterCheckoutObserver->execute($this->observer); -- } -- -- public function testCheckRegisterCheckoutWithIncorrectCaptcha() -- { -- $captchaValue = 'some_word'; -- $encodedJsonValue = '{}'; -- -- $this->quoteModelMock->expects($this->once()) -- ->method('getCheckoutMethod') -- ->willReturn(Onepage::METHOD_REGISTER); -- $this->captchaModelMock->expects($this->once()) -- ->method('isRequired') -- ->willReturn(true); -- $this->controllerMock->expects($this->once()) -- ->method('getRequest') -- ->willReturn($this->requestMock); -- $this->controllerMock->expects($this->once()) -- ->method('getResponse') -- ->willReturn($this->responseMock); -- $this->controllerMock->expects($this->once()) -- ->method('getResponse') -- ->willReturn($this->responseMock); -- $this->captchaStringResolverMock->expects($this->once()) -- ->method('resolve') -- ->with($this->requestMock, self::FORM_ID) -- ->willReturn($captchaValue); -- $this->captchaModelMock->expects($this->once()) -- ->method('isCorrect') -- ->with($captchaValue) -- ->willReturn(false); -- $this->actionFlagMock->expects($this->once()) -- ->method('set') -- ->with('', Action::FLAG_NO_DISPATCH, true); -- $this->jsonHelperMock->expects($this->once()) -- ->method('jsonEncode') -- ->willReturn($encodedJsonValue); -- $this->responseMock->expects($this->once()) -- ->method('representJson') -- ->with($encodedJsonValue); -- -- $this->checkRegisterCheckoutObserver->execute($this->observer); -- } -- -- public function testCheckRegisterCheckoutWithCorrectCaptcha() -- { -- $this->quoteModelMock->expects($this->once()) -- ->method('getCheckoutMethod') -- ->willReturn(Onepage::METHOD_REGISTER); -- $this->captchaModelMock->expects($this->once()) -- ->method('isRequired') -- ->willReturn(true); -- $this->controllerMock->expects($this->once()) -- ->method('getRequest') -- ->willReturn($this->requestMock); -- $this->captchaStringResolverMock->expects($this->once()) -- ->method('resolve') -- ->with($this->requestMock, self::FORM_ID) -- ->willReturn('some_word'); -- $this->captchaModelMock->expects($this->once()) -- ->method('isCorrect') -- ->with('some_word') -- ->willReturn(true); -- $this->actionFlagMock->expects($this->never()) -- ->method('set'); -- -- $this->checkRegisterCheckoutObserver->execute($this->observer); -- } --} -diff --git a/app/code/Magento/Captcha/Test/Unit/Observer/CheckUserLoginBackendObserverTest.php b/app/code/Magento/Captcha/Test/Unit/Observer/CheckUserLoginBackendObserverTest.php -new file mode 100644 -index 00000000000..415f022a736 ---- /dev/null -+++ b/app/code/Magento/Captcha/Test/Unit/Observer/CheckUserLoginBackendObserverTest.php -@@ -0,0 +1,137 @@ -+<?php -+/** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+declare(strict_types=1); -+ -+namespace Magento\Captcha\Test\Unit\Observer; -+ -+use Magento\Captcha\Helper\Data; -+use Magento\Captcha\Model\DefaultModel; -+use Magento\Captcha\Observer\CaptchaStringResolver; -+use Magento\Captcha\Observer\CheckUserLoginBackendObserver; -+use Magento\Framework\App\RequestInterface; -+use Magento\Framework\Event; -+use Magento\Framework\Event\Observer; -+use Magento\Framework\Message\ManagerInterface; -+use PHPUnit\Framework\TestCase; -+use PHPUnit_Framework_MockObject_MockObject as MockObject; -+ -+/** -+ * Class CheckUserLoginBackendObserverTest -+ */ -+class CheckUserLoginBackendObserverTest extends TestCase -+{ -+ /** -+ * @var CheckUserLoginBackendObserver -+ */ -+ private $observer; -+ -+ /** -+ * @var ManagerInterface|MockObject -+ */ -+ private $messageManagerMock; -+ -+ /** -+ * @var CaptchaStringResolver|MockObject -+ */ -+ private $captchaStringResolverMock; -+ -+ /** -+ * @var RequestInterface|MockObject -+ */ -+ private $requestMock; -+ -+ /** -+ * @var Data|MockObject -+ */ -+ private $helperMock; -+ -+ /** -+ * Set Up -+ * -+ * @return void -+ */ -+ protected function setUp() -+ { -+ $this->helperMock = $this->createMock(Data::class); -+ $this->messageManagerMock = $this->createMock(ManagerInterface::class); -+ $this->captchaStringResolverMock = $this->createMock(CaptchaStringResolver::class); -+ $this->requestMock = $this->createMock(RequestInterface::class); -+ -+ $this->observer = new CheckUserLoginBackendObserver( -+ $this->helperMock, -+ $this->captchaStringResolverMock, -+ $this->requestMock -+ ); -+ } -+ -+ /** -+ * Test check user login in backend with correct captcha -+ * -+ * @dataProvider requiredCaptchaDataProvider -+ * @param bool $isRequired -+ * @return void -+ */ -+ public function testCheckOnBackendLoginWithCorrectCaptcha(bool $isRequired): void -+ { -+ $formId = 'backend_login'; -+ $login = 'admin'; -+ $captchaValue = 'captcha-value'; -+ -+ /** @var Observer|MockObject $observerMock */ -+ $observerMock = $this->createPartialMock(Observer::class, ['getEvent']); -+ $eventMock = $this->createPartialMock(Event::class, ['getUsername']); -+ $captcha = $this->createMock(DefaultModel::class); -+ -+ $eventMock->method('getUsername')->willReturn('admin'); -+ $observerMock->method('getEvent')->willReturn($eventMock); -+ $captcha->method('isRequired')->with($login)->willReturn($isRequired); -+ $captcha->method('isCorrect')->with($captchaValue)->willReturn(true); -+ $this->helperMock->method('getCaptcha')->with($formId)->willReturn($captcha); -+ $this->captchaStringResolverMock->method('resolve')->with($this->requestMock, $formId) -+ ->willReturn($captchaValue); -+ -+ $this->observer->execute($observerMock); -+ } -+ -+ /** -+ * @return array -+ */ -+ public function requiredCaptchaDataProvider(): array -+ { -+ return [ -+ [true], -+ [false] -+ ]; -+ } -+ -+ /** -+ * Test check user login in backend with wrong captcha -+ * -+ * @return void -+ * @expectedException \Magento\Framework\Exception\Plugin\AuthenticationException -+ */ -+ public function testCheckOnBackendLoginWithWrongCaptcha(): void -+ { -+ $formId = 'backend_login'; -+ $login = 'admin'; -+ $captchaValue = 'captcha-value'; -+ -+ /** @var Observer|MockObject $observerMock */ -+ $observerMock = $this->createPartialMock(Observer::class, ['getEvent']); -+ $eventMock = $this->createPartialMock(Event::class, ['getUsername']); -+ $captcha = $this->createMock(DefaultModel::class); -+ -+ $eventMock->method('getUsername')->willReturn($login); -+ $observerMock->method('getEvent')->willReturn($eventMock); -+ $captcha->method('isRequired')->with($login)->willReturn(true); -+ $captcha->method('isCorrect')->with($captchaValue)->willReturn(false); -+ $this->helperMock->method('getCaptcha')->with($formId)->willReturn($captcha); -+ $this->captchaStringResolverMock->method('resolve')->with($this->requestMock, $formId) -+ ->willReturn($captchaValue); -+ -+ $this->observer->execute($observerMock); -+ } -+} -diff --git a/app/code/Magento/Captcha/etc/config.xml b/app/code/Magento/Captcha/etc/config.xml -index 71c474de90f..dd748dd05cc 100644 ---- a/app/code/Magento/Captcha/etc/config.xml -+++ b/app/code/Magento/Captcha/etc/config.xml -@@ -53,8 +53,6 @@ - <always_for> - <user_create>1</user_create> - <user_forgotpassword>1</user_forgotpassword> -- <guest_checkout>1</guest_checkout> -- <register_during_checkout>1</register_during_checkout> - <contact_us>1</contact_us> - </always_for> - </captcha> -@@ -77,12 +75,6 @@ - <user_forgotpassword> - <label>Forgot password</label> - </user_forgotpassword> -- <guest_checkout> -- <label>Check Out as Guest</label> -- </guest_checkout> -- <register_during_checkout> -- <label>Register during Checkout</label> -- </register_during_checkout> - <contact_us> - <label>Contact Us</label> - </contact_us> -diff --git a/app/code/Magento/Captcha/etc/db_schema.xml b/app/code/Magento/Captcha/etc/db_schema.xml -index fa9a14abb89..158e2f43b9f 100644 ---- a/app/code/Magento/Captcha/etc/db_schema.xml -+++ b/app/code/Magento/Captcha/etc/db_schema.xml -@@ -9,11 +9,11 @@ - xsi:noNamespaceSchemaLocation="urn:magento:framework:Setup/Declaration/Schema/etc/schema.xsd"> - <table name="captcha_log" resource="default" engine="innodb" comment="Count Login Attempts"> - <column xsi:type="varchar" name="type" nullable="false" length="32" comment="Type"/> -- <column xsi:type="varchar" name="value" nullable="false" length="32" comment="Value"/> -+ <column xsi:type="varchar" name="value" nullable="false" length="255" comment="Value"/> - <column xsi:type="int" name="count" padding="10" unsigned="true" nullable="false" identity="false" default="0" - comment="Count"/> - <column xsi:type="timestamp" name="updated_at" on_update="false" nullable="true" comment="Update Time"/> -- <constraint xsi:type="primary" name="PRIMARY"> -+ <constraint xsi:type="primary" referenceId="PRIMARY"> - <column name="type"/> - <column name="value"/> - </constraint> -diff --git a/app/code/Magento/Captcha/etc/di.xml b/app/code/Magento/Captcha/etc/di.xml -index 955896eb127..83c4e8aa1e2 100644 ---- a/app/code/Magento/Captcha/etc/di.xml -+++ b/app/code/Magento/Captcha/etc/di.xml -@@ -27,13 +27,12 @@ - </arguments> - </type> - <type name="Magento\Customer\Controller\Ajax\Login"> -- <plugin name="configurable_product" type="Magento\Captcha\Model\Customer\Plugin\AjaxLogin" sortOrder="50" /> -+ <plugin name="captcha_validation" type="Magento\Captcha\Model\Customer\Plugin\AjaxLogin" sortOrder="50" /> - </type> - <type name="Magento\Captcha\Model\Customer\Plugin\AjaxLogin"> - <arguments> - <argument name="formIds" xsi:type="array"> - <item name="user_login" xsi:type="string">user_login</item> -- <item name="guest_checkout" xsi:type="string">guest_checkout</item> - </argument> - </arguments> - </type> -diff --git a/app/code/Magento/Captcha/etc/events.xml b/app/code/Magento/Captcha/etc/events.xml -index e3ddd19de2d..970c0d07726 100644 ---- a/app/code/Magento/Captcha/etc/events.xml -+++ b/app/code/Magento/Captcha/etc/events.xml -@@ -18,10 +18,6 @@ - <event name="admin_user_authenticate_before"> - <observer name="captcha" instance="Magento\Captcha\Observer\CheckUserLoginBackendObserver" /> - </event> -- <event name="controller_action_predispatch_checkout_onepage_saveBilling"> -- <observer name="captcha_guest" instance="Magento\Captcha\Observer\CheckGuestCheckoutObserver" /> -- <observer name="captcha_register" instance="Magento\Captcha\Observer\CheckRegisterCheckoutObserver" /> -- </event> - <event name="customer_customer_authenticated"> - <observer name="captcha_reset_attempt" instance="Magento\Captcha\Observer\ResetAttemptForFrontendObserver" /> - </event> -diff --git a/app/code/Magento/Captcha/etc/frontend/di.xml b/app/code/Magento/Captcha/etc/frontend/di.xml -index 225e62c8e82..490f1eab851 100644 ---- a/app/code/Magento/Captcha/etc/frontend/di.xml -+++ b/app/code/Magento/Captcha/etc/frontend/di.xml -@@ -17,7 +17,20 @@ - <arguments> - <argument name="formIds" xsi:type="array"> - <item name="user_login" xsi:type="string">user_login</item> -- <item name="guest_checkout" xsi:type="string">guest_checkout</item> -+ </argument> -+ </arguments> -+ </type> -+ <type name="Magento\Captcha\CustomerData\Captcha"> -+ <arguments> -+ <argument name="formIds" xsi:type="array"> -+ <item name="user_login" xsi:type="string">user_login</item> -+ </argument> -+ </arguments> -+ </type> -+ <type name="Magento\Customer\CustomerData\SectionPoolInterface"> -+ <arguments> -+ <argument name="sectionSourceMap" xsi:type="array"> -+ <item name="captcha" xsi:type="string">Magento\Captcha\CustomerData\Captcha</item> - </argument> - </arguments> - </type> -diff --git a/app/code/Magento/Captcha/etc/frontend/sections.xml b/app/code/Magento/Captcha/etc/frontend/sections.xml -new file mode 100644 -index 00000000000..7f2070e10c8 ---- /dev/null -+++ b/app/code/Magento/Captcha/etc/frontend/sections.xml -@@ -0,0 +1,13 @@ -+<?xml version="1.0"?> -+<!-- -+/** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Customer:etc/sections.xsd"> -+ <action name="customer/ajax/login"> -+ <section name="captcha"/> -+ </action> -+</config> -diff --git a/app/code/Magento/Captcha/view/adminhtml/templates/default.phtml b/app/code/Magento/Captcha/view/adminhtml/templates/default.phtml -index 1be4bd19cd4..88e0d5edc2a 100644 ---- a/app/code/Magento/Captcha/view/adminhtml/templates/default.phtml -+++ b/app/code/Magento/Captcha/view/adminhtml/templates/default.phtml -@@ -4,8 +4,6 @@ - * See COPYING.txt for license details. - */ - --// @codingStandardsIgnoreFile -- - /** @var \Magento\Captcha\Block\Captcha\DefaultCaptcha $block */ - - /** @var \Magento\Captcha\Model\DefaultModel $captcha */ -diff --git a/app/code/Magento/Captcha/view/frontend/layout/checkout_index_index.xml b/app/code/Magento/Captcha/view/frontend/layout/checkout_index_index.xml -index 4ed56fd56cc..7180372f004 100644 ---- a/app/code/Magento/Captcha/view/frontend/layout/checkout_index_index.xml -+++ b/app/code/Magento/Captcha/view/frontend/layout/checkout_index_index.xml -@@ -36,29 +36,7 @@ - <item name="captcha" xsi:type="array"> - <item name="component" xsi:type="string">Magento_Captcha/js/view/checkout/loginCaptcha</item> - <item name="displayArea" xsi:type="string">additional-login-form-fields</item> -- <item name="formId" xsi:type="string">guest_checkout</item> -- <item name="configSource" xsi:type="string">checkoutConfig</item> -- </item> -- </item> -- </item> -- </item> -- </item> -- </item> -- </item> -- </item> -- </item> -- <item name="billing-step" xsi:type="array"> -- <item name="children" xsi:type="array"> -- <item name="payment" xsi:type="array"> -- <item name="children" xsi:type="array"> -- <item name="customer-email" xsi:type="array"> -- <item name="children" xsi:type="array"> -- <item name="additional-login-form-fields" xsi:type="array"> -- <item name="children" xsi:type="array"> -- <item name="captcha" xsi:type="array"> -- <item name="component" xsi:type="string">Magento_Captcha/js/view/checkout/loginCaptcha</item> -- <item name="displayArea" xsi:type="string">additional-login-form-fields</item> -- <item name="formId" xsi:type="string">guest_checkout</item> -+ <item name="formId" xsi:type="string">user_login</item> - <item name="configSource" xsi:type="string">checkoutConfig</item> - </item> - </item> -diff --git a/app/code/Magento/Captcha/view/frontend/requirejs-config.js b/app/code/Magento/Captcha/view/frontend/requirejs-config.js -index 3b322711f8b..42c80632d3e 100644 ---- a/app/code/Magento/Captcha/view/frontend/requirejs-config.js -+++ b/app/code/Magento/Captcha/view/frontend/requirejs-config.js -@@ -6,7 +6,8 @@ - var config = { - map: { - '*': { -- captcha: 'Magento_Captcha/captcha' -+ captcha: 'Magento_Captcha/js/captcha', -+ 'Magento_Captcha/captcha': 'Magento_Captcha/js/captcha' - } - } - }; -diff --git a/app/code/Magento/Captcha/view/frontend/templates/default.phtml b/app/code/Magento/Captcha/view/frontend/templates/default.phtml -index 6c9a5fe85f5..ead8c590eee 100644 ---- a/app/code/Magento/Captcha/view/frontend/templates/default.phtml -+++ b/app/code/Magento/Captcha/view/frontend/templates/default.phtml -@@ -4,8 +4,6 @@ - * See COPYING.txt for license details. - */ - --// @codingStandardsIgnoreFile -- - /** @var \Magento\Captcha\Block\Captcha\DefaultCaptcha $block */ - - /** @var \Magento\Captcha\Model\DefaultModel $captcha */ -diff --git a/app/code/Magento/Captcha/view/frontend/templates/js/components.phtml b/app/code/Magento/Captcha/view/frontend/templates/js/components.phtml -index bad5acc209b..5902a9f25cc 100644 ---- a/app/code/Magento/Captcha/view/frontend/templates/js/components.phtml -+++ b/app/code/Magento/Captcha/view/frontend/templates/js/components.phtml -@@ -3,8 +3,5 @@ - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -- --// @codingStandardsIgnoreFile -- - ?> - <?= $block->getChildHtml() ?> -diff --git a/app/code/Magento/Captcha/view/frontend/web/captcha.js b/app/code/Magento/Captcha/view/frontend/web/js/captcha.js -similarity index 100% -rename from app/code/Magento/Captcha/view/frontend/web/captcha.js -rename to app/code/Magento/Captcha/view/frontend/web/js/captcha.js -diff --git a/app/code/Magento/Captcha/view/frontend/web/js/model/captcha.js b/app/code/Magento/Captcha/view/frontend/web/js/model/captcha.js -index 3a235df73a9..e79cfb35ee0 100644 ---- a/app/code/Magento/Captcha/view/frontend/web/js/model/captcha.js -+++ b/app/code/Magento/Captcha/view/frontend/web/js/model/captcha.js -@@ -17,11 +17,12 @@ define([ - imageSource: ko.observable(captchaData.imageSrc), - visibility: ko.observable(false), - captchaValue: ko.observable(null), -- isRequired: captchaData.isRequired, -+ isRequired: ko.observable(captchaData.isRequired), - isCaseSensitive: captchaData.isCaseSensitive, - imageHeight: captchaData.imageHeight, - refreshUrl: captchaData.refreshUrl, - isLoading: ko.observable(false), -+ timestamp: null, - - /** - * @return {String} -@@ -41,7 +42,7 @@ define([ - * @return {Boolean} - */ - getIsVisible: function () { -- return this.visibility; -+ return this.visibility(); - }, - - /** -@@ -55,14 +56,14 @@ define([ - * @return {Boolean} - */ - getIsRequired: function () { -- return this.isRequired; -+ return this.isRequired(); - }, - - /** - * @param {Boolean} flag - */ - setIsRequired: function (flag) { -- this.isRequired = flag; -+ this.isRequired(flag); - }, - - /** -diff --git a/app/code/Magento/Captcha/view/frontend/web/js/view/checkout/defaultCaptcha.js b/app/code/Magento/Captcha/view/frontend/web/js/view/checkout/defaultCaptcha.js -index f80b2ab163f..d79c42a7115 100644 ---- a/app/code/Magento/Captcha/view/frontend/web/js/view/checkout/defaultCaptcha.js -+++ b/app/code/Magento/Captcha/view/frontend/web/js/view/checkout/defaultCaptcha.js -@@ -7,8 +7,10 @@ define([ - 'jquery', - 'uiComponent', - 'Magento_Captcha/js/model/captcha', -- 'Magento_Captcha/js/model/captchaList' --], function ($, Component, Captcha, captchaList) { -+ 'Magento_Captcha/js/model/captchaList', -+ 'Magento_Customer/js/customer-data', -+ 'underscore' -+], function ($, Component, Captcha, captchaList, customerData, _) { - 'use strict'; - - var captchaConfig; -@@ -34,12 +36,49 @@ define([ - if (window[this.configSource] && window[this.configSource].captcha) { - captchaConfig = window[this.configSource].captcha; - $.each(captchaConfig, function (formId, captchaData) { -+ var captcha; -+ - captchaData.formId = formId; -- captchaList.add(Captcha(captchaData)); -- }); -+ captcha = Captcha(captchaData); -+ this.checkCustomerData(formId, customerData.get('captcha')(), captcha); -+ this.subscribeCustomerData(formId, captcha); -+ captchaList.add(captcha); -+ }.bind(this)); - } - }, - -+ /** -+ * Check customer data for captcha configuration. -+ * -+ * @param {String} formId -+ * @param {Object} captchaData -+ * @param {Object} captcha -+ */ -+ checkCustomerData: function (formId, captchaData, captcha) { -+ if (!_.isEmpty(captchaData) && -+ !_.isEmpty(captchaData)[formId] && -+ captchaData[formId].timestamp > captcha.timestamp -+ ) { -+ if (!captcha.isRequired() && captchaData[formId].isRequired) { -+ captcha.refresh(); -+ } -+ captcha.isRequired(captchaData[formId].isRequired); -+ captcha.timestamp = captchaData[formId].timestamp; -+ } -+ }, -+ -+ /** -+ * Subscribe for customer data updates. -+ * -+ * @param {String} formId -+ * @param {Object} captcha -+ */ -+ subscribeCustomerData: function (formId, captcha) { -+ customerData.get('captcha').subscribe(function (captchaData) { -+ this.checkCustomerData(formId, captchaData, captcha); -+ }.bind(this)); -+ }, -+ - /** - * @return {Boolean} - */ -@@ -89,6 +128,15 @@ define([ - return this.currentCaptcha !== null ? this.currentCaptcha.getIsRequired() : false; - }, - -+ /** -+ * Set isRequired on current captcha model. -+ * -+ * @param {Boolean} flag -+ */ -+ setIsRequired: function (flag) { -+ this.currentCaptcha.setIsRequired(flag); -+ }, -+ - /** - * @return {Boolean} - */ -diff --git a/app/code/Magento/Captcha/view/frontend/web/js/view/checkout/loginCaptcha.js b/app/code/Magento/Captcha/view/frontend/web/js/view/checkout/loginCaptcha.js -index 7709febea60..49528f6ce85 100644 ---- a/app/code/Magento/Captcha/view/frontend/web/js/view/checkout/loginCaptcha.js -+++ b/app/code/Magento/Captcha/view/frontend/web/js/view/checkout/loginCaptcha.js -@@ -6,9 +6,10 @@ - define([ - 'Magento_Captcha/js/view/checkout/defaultCaptcha', - 'Magento_Captcha/js/model/captchaList', -- 'Magento_Customer/js/action/login' -+ 'Magento_Customer/js/action/login', -+ 'underscore' - ], --function (defaultCaptcha, captchaList, loginAction) { -+function (defaultCaptcha, captchaList, loginAction, _) { - 'use strict'; - - return defaultCaptcha.extend({ -@@ -26,9 +27,10 @@ function (defaultCaptcha, captchaList, loginAction) { - - loginAction.registerLoginCallback(function (loginData) { - if (loginData['captcha_form_id'] && -- loginData['captcha_form_id'] == self.formId //eslint-disable-line eqeqeq -+ loginData['captcha_form_id'] === self.formId && -+ self.isRequired() - ) { -- self.refresh(); -+ _.defer(self.refresh.bind(self)); - } - }); - } -diff --git a/app/code/Magento/Captcha/view/frontend/web/onepage.js b/app/code/Magento/Captcha/view/frontend/web/onepage.js -deleted file mode 100644 -index 7f5f11d2057..00000000000 ---- a/app/code/Magento/Captcha/view/frontend/web/onepage.js -+++ /dev/null -@@ -1,22 +0,0 @@ --/** -- * Copyright © Magento, Inc. All rights reserved. -- * See COPYING.txt for license details. -- */ -- --/** -- * @deprecated since version 2.2.0 -- */ --define(['jquery'], function ($) { -- 'use strict'; -- -- $(document).on('login', function () { -- var type; -- -- $('[data-captcha="guest_checkout"], [data-captcha="register_during_checkout"]').hide(); -- $('[role="guest_checkout"], [role="register_during_checkout"]').hide(); -- type = $('#login\\:guest').is(':checked') ? 'guest_checkout' : 'register_during_checkout'; -- $('[role="' + type + '"], [data-captcha="' + type + '"]').show(); -- }).on('billingSave', function () { -- $('.captcha-reload:visible').trigger('click'); -- }); --}); -diff --git a/app/code/Magento/Captcha/view/frontend/web/template/checkout/captcha.html b/app/code/Magento/Captcha/view/frontend/web/template/checkout/captcha.html -index 575b3ca6f73..3f48ec330c0 100644 ---- a/app/code/Magento/Captcha/view/frontend/web/template/checkout/captcha.html -+++ b/app/code/Magento/Captcha/view/frontend/web/template/checkout/captcha.html -@@ -4,12 +4,12 @@ - * See COPYING.txt for license details. - */ - --> -+<input name="captcha_form_id" type="hidden" data-bind="value: formId, attr: {'data-scope': dataScope}" /> - <!-- ko if: (isRequired() && getIsVisible())--> - <div class="field captcha required" data-bind="blockLoader: getIsLoading()"> - <label data-bind="attr: {for: 'captcha_' + formId}" class="label"><span data-bind="i18n: 'Please type the letters and numbers below'"></span></label> - <div class="control captcha"> - <input name="captcha_string" type="text" class="input-text required-entry" data-bind="value: captchaValue(), attr: {id: 'captcha_' + formId, 'data-scope': dataScope}" autocomplete="off"/> -- <input name="captcha_form_id" type="hidden" data-bind="value: formId, attr: {'data-scope': dataScope}" /> - <div class="nested"> - <div class="field captcha no-label"> - <div class="control captcha-image"> -diff --git a/app/code/Magento/CardinalCommerce/LICENSE.txt b/app/code/Magento/CardinalCommerce/LICENSE.txt -new file mode 100644 -index 00000000000..49525fd99da ---- /dev/null -+++ b/app/code/Magento/CardinalCommerce/LICENSE.txt -@@ -0,0 +1,48 @@ -+ -+Open Software License ("OSL") v. 3.0 -+ -+This Open Software License (the "License") applies to any original work of authorship (the "Original Work") whose owner (the "Licensor") has placed the following licensing notice adjacent to the copyright notice for the Original Work: -+ -+Licensed under the Open Software License version 3.0 -+ -+ 1. Grant of Copyright License. Licensor grants You a worldwide, royalty-free, non-exclusive, sublicensable license, for the duration of the copyright, to do the following: -+ -+ 1. to reproduce the Original Work in copies, either alone or as part of a collective work; -+ -+ 2. to translate, adapt, alter, transform, modify, or arrange the Original Work, thereby creating derivative works ("Derivative Works") based upon the Original Work; -+ -+ 3. to distribute or communicate copies of the Original Work and Derivative Works to the public, with the proviso that copies of Original Work or Derivative Works that You distribute or communicate shall be licensed under this Open Software License; -+ -+ 4. to perform the Original Work publicly; and -+ -+ 5. to display the Original Work publicly. -+ -+ 2. Grant of Patent License. Licensor grants You a worldwide, royalty-free, non-exclusive, sublicensable license, under patent claims owned or controlled by the Licensor that are embodied in the Original Work as furnished by the Licensor, for the duration of the patents, to make, use, sell, offer for sale, have made, and import the Original Work and Derivative Works. -+ -+ 3. Grant of Source Code License. The term "Source Code" means the preferred form of the Original Work for making modifications to it and all available documentation describing how to modify the Original Work. Licensor agrees to provide a machine-readable copy of the Source Code of the Original Work along with each copy of the Original Work that Licensor distributes. Licensor reserves the right to satisfy this obligation by placing a machine-readable copy of the Source Code in an information repository reasonably calculated to permit inexpensive and convenient access by You for as long as Licensor continues to distribute the Original Work. -+ -+ 4. Exclusions From License Grant. Neither the names of Licensor, nor the names of any contributors to the Original Work, nor any of their trademarks or service marks, may be used to endorse or promote products derived from this Original Work without express prior permission of the Licensor. Except as expressly stated herein, nothing in this License grants any license to Licensor's trademarks, copyrights, patents, trade secrets or any other intellectual property. No patent license is granted to make, use, sell, offer for sale, have made, or import embodiments of any patent claims other than the licensed claims defined in Section 2. No license is granted to the trademarks of Licensor even if such marks are included in the Original Work. Nothing in this License shall be interpreted to prohibit Licensor from licensing under terms different from this License any Original Work that Licensor otherwise would have a right to license. -+ -+ 5. External Deployment. The term "External Deployment" means the use, distribution, or communication of the Original Work or Derivative Works in any way such that the Original Work or Derivative Works may be used by anyone other than You, whether those works are distributed or communicated to those persons or made available as an application intended for use over a network. As an express condition for the grants of license hereunder, You must treat any External Deployment by You of the Original Work or a Derivative Work as a distribution under section 1(c). -+ -+ 6. Attribution Rights. You must retain, in the Source Code of any Derivative Works that You create, all copyright, patent, or trademark notices from the Source Code of the Original Work, as well as any notices of licensing and any descriptive text identified therein as an "Attribution Notice." You must cause the Source Code for any Derivative Works that You create to carry a prominent Attribution Notice reasonably calculated to inform recipients that You have modified the Original Work. -+ -+ 7. Warranty of Provenance and Disclaimer of Warranty. Licensor warrants that the copyright in and to the Original Work and the patent rights granted herein by Licensor are owned by the Licensor or are sublicensed to You under the terms of this License with the permission of the contributor(s) of those copyrights and patent rights. Except as expressly stated in the immediately preceding sentence, the Original Work is provided under this License on an "AS IS" BASIS and WITHOUT WARRANTY, either express or implied, including, without limitation, the warranties of non-infringement, merchantability or fitness for a particular purpose. THE ENTIRE RISK AS TO THE QUALITY OF THE ORIGINAL WORK IS WITH YOU. This DISCLAIMER OF WARRANTY constitutes an essential part of this License. No license to the Original Work is granted by this License except under this disclaimer. -+ -+ 8. Limitation of Liability. Under no circumstances and under no legal theory, whether in tort (including negligence), contract, or otherwise, shall the Licensor be liable to anyone for any indirect, special, incidental, or consequential damages of any character arising as a result of this License or the use of the Original Work including, without limitation, damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses. This limitation of liability shall not apply to the extent applicable law prohibits such limitation. -+ -+ 9. Acceptance and Termination. If, at any time, You expressly assented to this License, that assent indicates your clear and irrevocable acceptance of this License and all of its terms and conditions. If You distribute or communicate copies of the Original Work or a Derivative Work, You must make a reasonable effort under the circumstances to obtain the express assent of recipients to the terms of this License. This License conditions your rights to undertake the activities listed in Section 1, including your right to create Derivative Works based upon the Original Work, and doing so without honoring these terms and conditions is prohibited by copyright law and international treaty. Nothing in this License is intended to affect copyright exceptions and limitations (including 'fair use' or 'fair dealing'). This License shall terminate immediately and You may no longer exercise any of the rights granted to You by this License upon your failure to honor the conditions in Section 1(c). -+ -+ 10. Termination for Patent Action. This License shall terminate automatically and You may no longer exercise any of the rights granted to You by this License as of the date You commence an action, including a cross-claim or counterclaim, against Licensor or any licensee alleging that the Original Work infringes a patent. This termination provision shall not apply for an action alleging patent infringement by combinations of the Original Work with other software or hardware. -+ -+ 11. Jurisdiction, Venue and Governing Law. Any action or suit relating to this License may be brought only in the courts of a jurisdiction wherein the Licensor resides or in which Licensor conducts its primary business, and under the laws of that jurisdiction excluding its conflict-of-law provisions. The application of the United Nations Convention on Contracts for the International Sale of Goods is expressly excluded. Any use of the Original Work outside the scope of this License or after its termination shall be subject to the requirements and penalties of copyright or patent law in the appropriate jurisdiction. This section shall survive the termination of this License. -+ -+ 12. Attorneys' Fees. In any action to enforce the terms of this License or seeking damages relating thereto, the prevailing party shall be entitled to recover its costs and expenses, including, without limitation, reasonable attorneys' fees and costs incurred in connection with such action, including any appeal of such action. This section shall survive the termination of this License. -+ -+ 13. Miscellaneous. If any provision of this License is held to be unenforceable, such provision shall be reformed only to the extent necessary to make it enforceable. -+ -+ 14. Definition of "You" in This License. "You" throughout this License, whether in upper or lower case, means an individual or a legal entity exercising rights under, and complying with all of the terms of, this License. For legal entities, "You" includes any entity that controls, is controlled by, or is under common control with you. For purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. -+ -+ 15. Right to Use. You may use the Original Work in all ways not otherwise restricted or conditioned by this License or by law, and Licensor promises not to interfere with or be responsible for such uses by You. -+ -+ 16. Modification of This License. This License is Copyright (C) 2005 Lawrence Rosen. Permission is granted to copy, distribute, or communicate this License without modification. Nothing in this License permits You to modify this License as applied to the Original Work or to Derivative Works. However, You may modify the text of this License and copy, distribute or communicate your modified version (the "Modified License") and apply it to other original works of authorship subject to the following conditions: (i) You may not indicate in any way that your Modified License is the "Open Software License" or "OSL" and you may not use those names in the name of your Modified License; (ii) You must replace the notice specified in the first paragraph above with the notice "Licensed under <insert your license name here>" or with a notice of your own that is not confusingly similar to the notice in this License; and (iii) You may not claim that your original works are open source software unless your Modified License has been approved by Open Source Initiative (OSI) and You comply with its license review and certification process. -\ No newline at end of file -diff --git a/app/code/Magento/CardinalCommerce/LICENSE_AFL.txt b/app/code/Magento/CardinalCommerce/LICENSE_AFL.txt -new file mode 100644 -index 00000000000..f39d641b18a ---- /dev/null -+++ b/app/code/Magento/CardinalCommerce/LICENSE_AFL.txt -@@ -0,0 +1,48 @@ -+ -+Academic Free License ("AFL") v. 3.0 -+ -+This Academic Free License (the "License") applies to any original work of authorship (the "Original Work") whose owner (the "Licensor") has placed the following licensing notice adjacent to the copyright notice for the Original Work: -+ -+Licensed under the Academic Free License version 3.0 -+ -+ 1. Grant of Copyright License. Licensor grants You a worldwide, royalty-free, non-exclusive, sublicensable license, for the duration of the copyright, to do the following: -+ -+ 1. to reproduce the Original Work in copies, either alone or as part of a collective work; -+ -+ 2. to translate, adapt, alter, transform, modify, or arrange the Original Work, thereby creating derivative works ("Derivative Works") based upon the Original Work; -+ -+ 3. to distribute or communicate copies of the Original Work and Derivative Works to the public, under any license of your choice that does not contradict the terms and conditions, including Licensor's reserved rights and remedies, in this Academic Free License; -+ -+ 4. to perform the Original Work publicly; and -+ -+ 5. to display the Original Work publicly. -+ -+ 2. Grant of Patent License. Licensor grants You a worldwide, royalty-free, non-exclusive, sublicensable license, under patent claims owned or controlled by the Licensor that are embodied in the Original Work as furnished by the Licensor, for the duration of the patents, to make, use, sell, offer for sale, have made, and import the Original Work and Derivative Works. -+ -+ 3. Grant of Source Code License. The term "Source Code" means the preferred form of the Original Work for making modifications to it and all available documentation describing how to modify the Original Work. Licensor agrees to provide a machine-readable copy of the Source Code of the Original Work along with each copy of the Original Work that Licensor distributes. Licensor reserves the right to satisfy this obligation by placing a machine-readable copy of the Source Code in an information repository reasonably calculated to permit inexpensive and convenient access by You for as long as Licensor continues to distribute the Original Work. -+ -+ 4. Exclusions From License Grant. Neither the names of Licensor, nor the names of any contributors to the Original Work, nor any of their trademarks or service marks, may be used to endorse or promote products derived from this Original Work without express prior permission of the Licensor. Except as expressly stated herein, nothing in this License grants any license to Licensor's trademarks, copyrights, patents, trade secrets or any other intellectual property. No patent license is granted to make, use, sell, offer for sale, have made, or import embodiments of any patent claims other than the licensed claims defined in Section 2. No license is granted to the trademarks of Licensor even if such marks are included in the Original Work. Nothing in this License shall be interpreted to prohibit Licensor from licensing under terms different from this License any Original Work that Licensor otherwise would have a right to license. -+ -+ 5. External Deployment. The term "External Deployment" means the use, distribution, or communication of the Original Work or Derivative Works in any way such that the Original Work or Derivative Works may be used by anyone other than You, whether those works are distributed or communicated to those persons or made available as an application intended for use over a network. As an express condition for the grants of license hereunder, You must treat any External Deployment by You of the Original Work or a Derivative Work as a distribution under section 1(c). -+ -+ 6. Attribution Rights. You must retain, in the Source Code of any Derivative Works that You create, all copyright, patent, or trademark notices from the Source Code of the Original Work, as well as any notices of licensing and any descriptive text identified therein as an "Attribution Notice." You must cause the Source Code for any Derivative Works that You create to carry a prominent Attribution Notice reasonably calculated to inform recipients that You have modified the Original Work. -+ -+ 7. Warranty of Provenance and Disclaimer of Warranty. Licensor warrants that the copyright in and to the Original Work and the patent rights granted herein by Licensor are owned by the Licensor or are sublicensed to You under the terms of this License with the permission of the contributor(s) of those copyrights and patent rights. Except as expressly stated in the immediately preceding sentence, the Original Work is provided under this License on an "AS IS" BASIS and WITHOUT WARRANTY, either express or implied, including, without limitation, the warranties of non-infringement, merchantability or fitness for a particular purpose. THE ENTIRE RISK AS TO THE QUALITY OF THE ORIGINAL WORK IS WITH YOU. This DISCLAIMER OF WARRANTY constitutes an essential part of this License. No license to the Original Work is granted by this License except under this disclaimer. -+ -+ 8. Limitation of Liability. Under no circumstances and under no legal theory, whether in tort (including negligence), contract, or otherwise, shall the Licensor be liable to anyone for any indirect, special, incidental, or consequential damages of any character arising as a result of this License or the use of the Original Work including, without limitation, damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses. This limitation of liability shall not apply to the extent applicable law prohibits such limitation. -+ -+ 9. Acceptance and Termination. If, at any time, You expressly assented to this License, that assent indicates your clear and irrevocable acceptance of this License and all of its terms and conditions. If You distribute or communicate copies of the Original Work or a Derivative Work, You must make a reasonable effort under the circumstances to obtain the express assent of recipients to the terms of this License. This License conditions your rights to undertake the activities listed in Section 1, including your right to create Derivative Works based upon the Original Work, and doing so without honoring these terms and conditions is prohibited by copyright law and international treaty. Nothing in this License is intended to affect copyright exceptions and limitations (including "fair use" or "fair dealing"). This License shall terminate immediately and You may no longer exercise any of the rights granted to You by this License upon your failure to honor the conditions in Section 1(c). -+ -+ 10. Termination for Patent Action. This License shall terminate automatically and You may no longer exercise any of the rights granted to You by this License as of the date You commence an action, including a cross-claim or counterclaim, against Licensor or any licensee alleging that the Original Work infringes a patent. This termination provision shall not apply for an action alleging patent infringement by combinations of the Original Work with other software or hardware. -+ -+ 11. Jurisdiction, Venue and Governing Law. Any action or suit relating to this License may be brought only in the courts of a jurisdiction wherein the Licensor resides or in which Licensor conducts its primary business, and under the laws of that jurisdiction excluding its conflict-of-law provisions. The application of the United Nations Convention on Contracts for the International Sale of Goods is expressly excluded. Any use of the Original Work outside the scope of this License or after its termination shall be subject to the requirements and penalties of copyright or patent law in the appropriate jurisdiction. This section shall survive the termination of this License. -+ -+ 12. Attorneys' Fees. In any action to enforce the terms of this License or seeking damages relating thereto, the prevailing party shall be entitled to recover its costs and expenses, including, without limitation, reasonable attorneys' fees and costs incurred in connection with such action, including any appeal of such action. This section shall survive the termination of this License. -+ -+ 13. Miscellaneous. If any provision of this License is held to be unenforceable, such provision shall be reformed only to the extent necessary to make it enforceable. -+ -+ 14. Definition of "You" in This License. "You" throughout this License, whether in upper or lower case, means an individual or a legal entity exercising rights under, and complying with all of the terms of, this License. For legal entities, "You" includes any entity that controls, is controlled by, or is under common control with you. For purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. -+ -+ 15. Right to Use. You may use the Original Work in all ways not otherwise restricted or conditioned by this License or by law, and Licensor promises not to interfere with or be responsible for such uses by You. -+ -+ 16. Modification of This License. This License is Copyright © 2005 Lawrence Rosen. Permission is granted to copy, distribute, or communicate this License without modification. Nothing in this License permits You to modify this License as applied to the Original Work or to Derivative Works. However, You may modify the text of this License and copy, distribute or communicate your modified version (the "Modified License") and apply it to other original works of authorship subject to the following conditions: (i) You may not indicate in any way that your Modified License is the "Academic Free License" or "AFL" and you may not use those names in the name of your Modified License; (ii) You must replace the notice specified in the first paragraph above with the notice "Licensed under <insert your license name here>" or with a notice of your own that is not confusingly similar to the notice in this License; and (iii) You may not claim that your original works are open source software unless your Modified License has been approved by Open Source Initiative (OSI) and You comply with its license review and certification process. -diff --git a/app/code/Magento/CardinalCommerce/Model/Adminhtml/Source/Environment.php b/app/code/Magento/CardinalCommerce/Model/Adminhtml/Source/Environment.php -new file mode 100644 -index 00000000000..29e2939a0ae ---- /dev/null -+++ b/app/code/Magento/CardinalCommerce/Model/Adminhtml/Source/Environment.php -@@ -0,0 +1,36 @@ -+<?php -+/** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+declare(strict_types=1); -+ -+namespace Magento\CardinalCommerce\Model\Adminhtml\Source; -+ -+/** -+ * CardinalCommerce Environment Dropdown source -+ */ -+class Environment implements \Magento\Framework\Data\OptionSourceInterface -+{ -+ private const ENVIRONMENT_PRODUCTION = 'production'; -+ private const ENVIRONMENT_SANDBOX = 'sandbox'; -+ -+ /** -+ * Possible environment types -+ * -+ * @return array -+ */ -+ public function toOptionArray(): array -+ { -+ return [ -+ [ -+ 'value' => self::ENVIRONMENT_SANDBOX, -+ 'label' => 'Sandbox', -+ ], -+ [ -+ 'value' => self::ENVIRONMENT_PRODUCTION, -+ 'label' => 'Production' -+ ] -+ ]; -+ } -+} -diff --git a/app/code/Magento/CardinalCommerce/Model/Checkout/ConfigProvider.php b/app/code/Magento/CardinalCommerce/Model/Checkout/ConfigProvider.php -new file mode 100644 -index 00000000000..a6794eae900 ---- /dev/null -+++ b/app/code/Magento/CardinalCommerce/Model/Checkout/ConfigProvider.php -@@ -0,0 +1,53 @@ -+<?php -+/** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+declare(strict_types=1); -+ -+namespace Magento\CardinalCommerce\Model\Checkout; -+ -+use Magento\CardinalCommerce\Model\Config; -+use Magento\CardinalCommerce\Model\Request\TokenBuilder; -+use Magento\Checkout\Model\ConfigProviderInterface; -+ -+/** -+ * Configuration provider. -+ */ -+class ConfigProvider implements ConfigProviderInterface -+{ -+ /** -+ * @var TokenBuilder -+ */ -+ private $requestJwtBuilder; -+ -+ /** -+ * @var Config -+ */ -+ private $config; -+ -+ /** -+ * @param TokenBuilder $requestJwtBuilder -+ * @param Config $config -+ */ -+ public function __construct( -+ TokenBuilder $requestJwtBuilder, -+ Config $config -+ ) { -+ $this->requestJwtBuilder = $requestJwtBuilder; -+ $this->config = $config; -+ } -+ -+ /** -+ * @inheritdoc -+ */ -+ public function getConfig(): array -+ { -+ $config['cardinal'] = [ -+ 'environment' => $this->config->getEnvironment(), -+ 'requestJWT' => $this->requestJwtBuilder->build() -+ ]; -+ -+ return $config; -+ } -+} -diff --git a/app/code/Magento/CardinalCommerce/Model/Config.php b/app/code/Magento/CardinalCommerce/Model/Config.php -new file mode 100644 -index 00000000000..64c72dae8d5 ---- /dev/null -+++ b/app/code/Magento/CardinalCommerce/Model/Config.php -@@ -0,0 +1,120 @@ -+<?php -+/** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+declare(strict_types=1); -+ -+namespace Magento\CardinalCommerce\Model; -+ -+use Magento\Framework\App\Config\ScopeConfigInterface; -+use Magento\Store\Model\ScopeInterface; -+ -+/** -+ * CardinalCommerce integration configuration. -+ * -+ * Class is a proxy service for retrieving configuration settings. -+ */ -+class Config -+{ -+ /** -+ * @var ScopeConfigInterface -+ */ -+ private $scopeConfig; -+ -+ /** -+ * @param ScopeConfigInterface $scopeConfig -+ */ -+ public function __construct(ScopeConfigInterface $scopeConfig) -+ { -+ $this->scopeConfig = $scopeConfig; -+ } -+ -+ /** -+ * Returns CardinalCommerce API Key used for authentication. -+ * -+ * A shared secret value between the merchant and Cardinal. This value should never be exposed to the public. -+ * -+ * @param int|null $storeId -+ * @return string -+ */ -+ public function getApiKey(?int $storeId = null): string -+ { -+ $apiKey = $this->scopeConfig->getValue( -+ 'three_d_secure/cardinal/api_key', -+ ScopeInterface::SCOPE_STORE, -+ $storeId -+ ); -+ return $apiKey; -+ } -+ -+ /** -+ * Returns CardinalCommerce API Identifier. -+ * -+ * GUID used to identify the specific API Key. -+ * -+ * @param int|null $storeId -+ * @return string -+ */ -+ public function getApiIdentifier(?int $storeId = null): string -+ { -+ $apiIdentifier = $this->scopeConfig->getValue( -+ 'three_d_secure/cardinal/api_identifier', -+ ScopeInterface::SCOPE_STORE, -+ $storeId -+ ); -+ return $apiIdentifier; -+ } -+ -+ /** -+ * Returns CardinalCommerce Org Unit Id. -+ * -+ * GUID to identify the merchant organization within Cardinal systems. -+ * -+ * @param int|null $storeId -+ * @return string -+ */ -+ public function getOrgUnitId(?int $storeId = null): string -+ { -+ $orgUnitId = $this->scopeConfig->getValue( -+ 'three_d_secure/cardinal/org_unit_id', -+ ScopeInterface::SCOPE_STORE, -+ $storeId -+ ); -+ return $orgUnitId; -+ } -+ -+ /** -+ * Returns CardinalCommerce environment. -+ * -+ * Sandbox or production. -+ * -+ * @param int|null $storeId -+ * @return string -+ */ -+ public function getEnvironment(?int $storeId = null): string -+ { -+ $environment = $this->scopeConfig->getValue( -+ 'three_d_secure/cardinal/environment', -+ ScopeInterface::SCOPE_STORE, -+ $storeId -+ ); -+ return $environment; -+ } -+ -+ /** -+ * If is "true" extra information about interaction with CardinalCommerce API are written to payment.log file -+ * -+ * @param int|null $storeId -+ * @return bool -+ */ -+ public function isDebugModeEnabled(?int $storeId = null): bool -+ { -+ $debugModeEnabled = $this->scopeConfig->isSetFlag( -+ 'three_d_secure/cardinal/debug', -+ ScopeInterface::SCOPE_STORE, -+ $storeId -+ ); -+ return $debugModeEnabled; -+ } -+} -diff --git a/app/code/Magento/CardinalCommerce/Model/JwtManagement.php b/app/code/Magento/CardinalCommerce/Model/JwtManagement.php -new file mode 100644 -index 00000000000..953af1751fd ---- /dev/null -+++ b/app/code/Magento/CardinalCommerce/Model/JwtManagement.php -@@ -0,0 +1,141 @@ -+<?php -+/** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+declare(strict_types=1); -+ -+namespace Magento\CardinalCommerce\Model; -+ -+use Magento\Framework\Serialize\Serializer\Json; -+ -+/** -+ * JSON Web Token management. -+ */ -+class JwtManagement -+{ -+ /** -+ * The signing algorithm. Cardinal supported algorithm is 'HS256' -+ */ -+ private const SIGN_ALGORITHM = 'HS256'; -+ -+ /** -+ * @var Json -+ */ -+ private $json; -+ -+ /** -+ * @param Json $json -+ */ -+ public function __construct( -+ Json $json -+ ) { -+ $this->json = $json; -+ } -+ -+ /** -+ * Converts JWT string into array. -+ * -+ * @param string $jwt The JWT -+ * @param string $key The secret key -+ * -+ * @return array -+ * @throws \InvalidArgumentException -+ */ -+ public function decode(string $jwt, string $key): array -+ { -+ if (empty($jwt)) { -+ throw new \InvalidArgumentException('JWT is empty'); -+ } -+ -+ $parts = explode('.', $jwt); -+ if (count($parts) != 3) { -+ throw new \InvalidArgumentException('Wrong number of segments in JWT'); -+ } -+ -+ [$headB64, $payloadB64, $signatureB64] = $parts; -+ -+ $headerJson = $this->urlSafeB64Decode($headB64); -+ $header = $this->json->unserialize($headerJson); -+ -+ $payloadJson = $this->urlSafeB64Decode($payloadB64); -+ $payload = $this->json->unserialize($payloadJson); -+ -+ $signature = $this->urlSafeB64Decode($signatureB64); -+ if ($signature !== $this->sign($headB64 . '.' . $payloadB64, $key, $header['alg'])) { -+ throw new \InvalidArgumentException('JWT signature verification failed'); -+ } -+ -+ return $payload; -+ } -+ -+ /** -+ * Converts and signs array into a JWT string. -+ * -+ * @param array $payload -+ * @param string $key -+ * -+ * @return string -+ * @throws \InvalidArgumentException -+ */ -+ public function encode(array $payload, string $key): string -+ { -+ $header = ['typ' => 'JWT', 'alg' => self::SIGN_ALGORITHM]; -+ -+ $headerJson = $this->json->serialize($header); -+ $segments[] = $this->urlSafeB64Encode($headerJson); -+ -+ $payloadJson = $this->json->serialize($payload); -+ $segments[] = $this->urlSafeB64Encode($payloadJson); -+ -+ $signature = $this->sign(implode('.', $segments), $key, $header['alg']); -+ $segments[] = $this->urlSafeB64Encode($signature); -+ -+ return implode('.', $segments); -+ } -+ /** -+ * Sign a string with a given key and algorithm. -+ * -+ * @param string $msg The message to sign. -+ * @param string $key The secret key. -+ * @param string $algorithm The signing algorithm. -+ * -+ * @return string -+ * @throws \InvalidArgumentException -+ */ -+ private function sign(string $msg, string $key, string $algorithm): string -+ { -+ if ($algorithm !== self::SIGN_ALGORITHM) { -+ throw new \InvalidArgumentException('Algorithm ' . $algorithm . ' is not supported'); -+ } -+ -+ return hash_hmac('sha256', $msg, $key, true); -+ } -+ -+ /** -+ * Decode a string with URL-safe Base64. -+ * -+ * @param string $input A Base64 encoded string -+ * -+ * @return string -+ */ -+ private function urlSafeB64Decode(string $input): string -+ { -+ // phpcs:ignore Magento2.Functions.DiscouragedFunction -+ return base64_decode( -+ str_pad(strtr($input, '-_', '+/'), strlen($input) % 4, '=', STR_PAD_RIGHT) -+ ); -+ } -+ -+ /** -+ * Encode a string with URL-safe Base64. -+ * -+ * @param string $input The string you want encoded -+ * -+ * @return string -+ */ -+ private function urlSafeB64Encode(string $input): string -+ { -+ return str_replace('=', '', strtr(base64_encode($input), '+/', '-_')); -+ } -+} -diff --git a/app/code/Magento/CardinalCommerce/Model/Request/TokenBuilder.php b/app/code/Magento/CardinalCommerce/Model/Request/TokenBuilder.php -new file mode 100644 -index 00000000000..e045d00dc55 ---- /dev/null -+++ b/app/code/Magento/CardinalCommerce/Model/Request/TokenBuilder.php -@@ -0,0 +1,99 @@ -+<?php -+/** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+declare(strict_types=1); -+ -+namespace Magento\CardinalCommerce\Model\Request; -+ -+use Magento\CardinalCommerce\Model\JwtManagement; -+use Magento\CardinalCommerce\Model\Config; -+use Magento\Checkout\Model\Session; -+use Magento\Framework\DataObject\IdentityGeneratorInterface; -+use Magento\Framework\Intl\DateTimeFactory; -+ -+/** -+ * Cardinal request token builder. -+ * -+ * @SuppressWarnings(PHPMD.CookieAndSessionMisuse) -+ */ -+class TokenBuilder -+{ -+ /** -+ * @var JwtManagement -+ */ -+ private $jwtManagement; -+ -+ /** -+ * @var Session -+ */ -+ private $checkoutSession; -+ -+ /** -+ * @var Config -+ */ -+ private $config; -+ -+ /** -+ * @var IdentityGeneratorInterface -+ */ -+ private $identityGenerator; -+ -+ /** -+ * @var DateTimeFactory -+ */ -+ private $dateTimeFactory; -+ -+ /** -+ * @param JwtManagement $jwtManagement -+ * @param Session $checkoutSession -+ * @param Config $config -+ * @param IdentityGeneratorInterface $identityGenerator -+ * @param DateTimeFactory $dateTimeFactory -+ */ -+ public function __construct( -+ JwtManagement $jwtManagement, -+ Session $checkoutSession, -+ Config $config, -+ IdentityGeneratorInterface $identityGenerator, -+ DateTimeFactory $dateTimeFactory -+ ) { -+ $this->jwtManagement = $jwtManagement; -+ $this->checkoutSession = $checkoutSession; -+ $this->config = $config; -+ $this->identityGenerator = $identityGenerator; -+ $this->dateTimeFactory = $dateTimeFactory; -+ } -+ -+ /** -+ * Builds request JWT. -+ * -+ * @return string -+ */ -+ public function build() -+ { -+ $quote = $this->checkoutSession->getQuote(); -+ $currentDate = $this->dateTimeFactory->create('now', new \DateTimeZone('UTC')); -+ $orderDetails = [ -+ 'OrderDetails' => [ -+ 'OrderNumber' => $quote->getId(), -+ 'Amount' => $quote->getBaseGrandTotal() * 100, -+ 'CurrencyCode' => $quote->getBaseCurrencyCode() -+ ] -+ ]; -+ -+ $token = [ -+ 'jti' => $this->identityGenerator->generateId(), -+ 'iss' => $this->config->getApiIdentifier(), -+ 'iat' => $currentDate->getTimestamp(), -+ 'OrgUnitId' => $this->config->getOrgUnitId(), -+ 'Payload' => $orderDetails, -+ 'ObjectifyPayload' => true -+ ]; -+ -+ $jwt = $this->jwtManagement->encode($token, $this->config->getApiKey()); -+ -+ return $jwt; -+ } -+} -diff --git a/app/code/Magento/CardinalCommerce/Model/Response/JwtParser.php b/app/code/Magento/CardinalCommerce/Model/Response/JwtParser.php -new file mode 100644 -index 00000000000..1865605d50a ---- /dev/null -+++ b/app/code/Magento/CardinalCommerce/Model/Response/JwtParser.php -@@ -0,0 +1,120 @@ -+<?php -+/** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+declare(strict_types=1); -+ -+namespace Magento\CardinalCommerce\Model\Response; -+ -+use Magento\CardinalCommerce\Model\JwtManagement; -+use Magento\CardinalCommerce\Model\Config; -+use Magento\Framework\Exception\LocalizedException; -+use Psr\Log\LoggerInterface; -+use Magento\Payment\Model\Method\Logger as PaymentLogger; -+ -+/** -+ * Parse content of CardinalCommerce response JWT. -+ */ -+class JwtParser -+{ -+ /** -+ * @var JwtManagement -+ */ -+ private $jwtManagement; -+ -+ /** -+ * @var Config -+ */ -+ private $config; -+ -+ /** -+ * @var JwtPayloadValidatorInterface -+ */ -+ private $tokenValidator; -+ -+ /** -+ * @var LoggerInterface -+ */ -+ private $logger; -+ -+ /** -+ * @var PaymentLogger -+ */ -+ private $paymentLogger; -+ -+ /** -+ * @param JwtManagement $jwtManagement -+ * @param Config $config -+ * @param JwtPayloadValidatorInterface $tokenValidator -+ * @param PaymentLogger $paymentLogger -+ * @param LoggerInterface $logger -+ */ -+ public function __construct( -+ JwtManagement $jwtManagement, -+ Config $config, -+ JwtPayloadValidatorInterface $tokenValidator, -+ PaymentLogger $paymentLogger, -+ LoggerInterface $logger -+ ) { -+ $this->jwtManagement = $jwtManagement; -+ $this->config = $config; -+ $this->tokenValidator = $tokenValidator; -+ $this->paymentLogger = $paymentLogger; -+ $this->logger = $logger; -+ } -+ -+ /** -+ * Returns response JWT payload. -+ * -+ * @param string $jwt -+ * @return array -+ * @throws LocalizedException -+ */ -+ public function execute(string $jwt): array -+ { -+ $jwtPayload = ''; -+ try { -+ $this->debug(['Cardinal Response JWT:' => $jwt]); -+ $jwtPayload = $this->jwtManagement->decode($jwt, $this->config->getApiKey()); -+ $this->debug(['Cardinal Response JWT payload:' => $jwtPayload]); -+ if (!$this->tokenValidator->validate($jwtPayload)) { -+ $this->throwException(); -+ } -+ } catch (\InvalidArgumentException $e) { -+ $this->logger->critical($e, ['CardinalCommerce3DSecure']); -+ $this->throwException(); -+ } -+ -+ return $jwtPayload; -+ } -+ -+ /** -+ * Log JWT data. -+ * -+ * @param array $data -+ * @return void -+ */ -+ private function debug(array $data) -+ { -+ if ($this->config->isDebugModeEnabled()) { -+ $this->paymentLogger->debug($data, ['iss'], true); -+ } -+ } -+ -+ /** -+ * Throw general localized exception. -+ * -+ * @return void -+ * @throws LocalizedException -+ */ -+ private function throwException() -+ { -+ throw new LocalizedException( -+ __( -+ 'Authentication Failed. Your card issuer cannot authenticate this card. ' . -+ 'Please select another card or form of payment to complete your purchase.' -+ ) -+ ); -+ } -+} -diff --git a/app/code/Magento/CardinalCommerce/Model/Response/JwtPayloadValidator.php b/app/code/Magento/CardinalCommerce/Model/Response/JwtPayloadValidator.php -new file mode 100644 -index 00000000000..9720b90cad9 ---- /dev/null -+++ b/app/code/Magento/CardinalCommerce/Model/Response/JwtPayloadValidator.php -@@ -0,0 +1,132 @@ -+<?php -+/** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+declare(strict_types=1); -+ -+namespace Magento\CardinalCommerce\Model\Response; -+ -+use Magento\Framework\Intl\DateTimeFactory; -+ -+/** -+ * Validates payload of CardinalCommerce response JWT. -+ */ -+class JwtPayloadValidator implements JwtPayloadValidatorInterface -+{ -+ /** -+ * Resulting state of the transaction. -+ * -+ * SUCCESS - The transaction resulted in success for the payment type used. For example, -+ * with a CCA transaction this would indicate the user has successfully completed authentication. -+ * -+ * NOACTION - The transaction was successful but requires in no additional action. For example, -+ * with a CCA transaction this would indicate that the user is not currently enrolled in 3D Secure, -+ * but the API calls were successful. -+ * -+ * FAILURE - The transaction resulted in an error. For example, with a CCA transaction this would indicate -+ * that the user failed authentication or an error was encountered while processing the transaction. -+ * -+ * ERROR - A service level error was encountered. These are generally reserved for connectivity -+ * or API authentication issues. For example if your JWT was incorrectly signed, or Cardinal -+ * services are currently unreachable. -+ * -+ * @var array -+ */ -+ private $allowedActionCode = ['SUCCESS', 'NOACTION']; -+ -+ /** -+ * 3DS status of transaction from ECI Flag value. Liability shift applies. -+ * -+ * 05 - Successful 3D Authentication (Visa, AMEX, JCB) -+ * 02 - Successful 3D Authentication (MasterCard) -+ * 06 - Attempted Processing or User Not Enrolled (Visa, AMEX, JCB) -+ * 01 - Attempted Processing or User Not Enrolled (MasterCard) -+ * 07 - 3DS authentication is either failed or could not be attempted; -+ * possible reasons being both card and Issuing Bank are not secured by 3DS, -+ * technical errors, or improper configuration. (Visa, AMEX, JCB) -+ * 00 - 3DS authentication is either failed or could not be attempted; -+ * possible reasons being both card and Issuing Bank are not secured by 3DS, -+ * technical errors, or improper configuration. (MasterCard) -+ * -+ * @var array -+ */ -+ private $allowedECIFlag = ['05', '02', '06', '01']; -+ -+ /** -+ * @var DateTimeFactory -+ */ -+ private $dateTimeFactory; -+ -+ /** -+ * @param DateTimeFactory $dateTimeFactory -+ */ -+ public function __construct( -+ DateTimeFactory $dateTimeFactory -+ ) { -+ $this->dateTimeFactory = $dateTimeFactory; -+ } -+ /** -+ * @inheritdoc -+ */ -+ public function validate(array $jwtPayload): bool -+ { -+ $transactionState = $jwtPayload['Payload']['ActionCode'] ?? ''; -+ $errorNumber = $jwtPayload['Payload']['ErrorNumber'] ?? -1; -+ $eciFlag = $jwtPayload['Payload']['Payment']['ExtendedData']['ECIFlag'] ?? ''; -+ $expTimestamp = $jwtPayload['exp'] ?? 0; -+ -+ return $this->isValidErrorNumber((int)$errorNumber) -+ && $this->isValidTransactionState($transactionState) -+ && $this->isValidEciFlag($eciFlag) -+ && $this->isNotExpired((int)$expTimestamp); -+ } -+ -+ /** -+ * Checks application error number. -+ * -+ * A non-zero value represents the error encountered while attempting the process the message request. -+ * -+ * @param int $errorNumber -+ * @return bool -+ */ -+ private function isValidErrorNumber(int $errorNumber) -+ { -+ return $errorNumber === 0; -+ } -+ -+ /** -+ * Checks if value of transaction state identifier is in allowed list. -+ * -+ * @param string $transactionState -+ * @return bool -+ */ -+ private function isValidTransactionState(string $transactionState) -+ { -+ return in_array($transactionState, $this->allowedActionCode); -+ } -+ -+ /** -+ * Checks if value of ECI Flag identifier is in allowed list. -+ * -+ * @param string $eciFlag -+ * @return bool -+ */ -+ private function isValidEciFlag(string $eciFlag) -+ { -+ return in_array($eciFlag, $this->allowedECIFlag); -+ } -+ -+ /** -+ * Checks if token is not expired. -+ * -+ * @param int $expTimestamp -+ * @return bool -+ */ -+ private function isNotExpired(int $expTimestamp) -+ { -+ $currentDate = $this->dateTimeFactory->create('now', new \DateTimeZone('UTC')); -+ -+ return $currentDate->getTimestamp() < $expTimestamp; -+ } -+} -diff --git a/app/code/Magento/CardinalCommerce/Model/Response/JwtPayloadValidatorInterface.php b/app/code/Magento/CardinalCommerce/Model/Response/JwtPayloadValidatorInterface.php -new file mode 100644 -index 00000000000..774c0daee6c ---- /dev/null -+++ b/app/code/Magento/CardinalCommerce/Model/Response/JwtPayloadValidatorInterface.php -@@ -0,0 +1,21 @@ -+<?php -+/** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+ -+namespace Magento\CardinalCommerce\Model\Response; -+ -+/** -+ * Validates payload of CardinalCommerce response JWT. -+ */ -+interface JwtPayloadValidatorInterface -+{ -+ /** -+ * Validates token payload. -+ * -+ * @param array $jwtPayload -+ * @return bool -+ */ -+ public function validate(array $jwtPayload); -+} -diff --git a/app/code/Magento/CardinalCommerce/README.md b/app/code/Magento/CardinalCommerce/README.md -new file mode 100644 -index 00000000000..54db9114a2a ---- /dev/null -+++ b/app/code/Magento/CardinalCommerce/README.md -@@ -0,0 +1 @@ -+The CardinalCommerce module provides a possibility to enable 3-D Secure 2.0 support for payment methods. -\ No newline at end of file -diff --git a/app/code/Magento/CardinalCommerce/Test/Unit/Model/JwtManagementTest.php b/app/code/Magento/CardinalCommerce/Test/Unit/Model/JwtManagementTest.php -new file mode 100644 -index 00000000000..70eae201c15 ---- /dev/null -+++ b/app/code/Magento/CardinalCommerce/Test/Unit/Model/JwtManagementTest.php -@@ -0,0 +1,173 @@ -+<?php -+/** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+declare(strict_types=1); -+ -+namespace Magento\CardinalCommerce\Test\Unit\Model; -+ -+use Magento\CardinalCommerce\Model\JwtManagement; -+use Magento\Framework\Serialize\Serializer\Json; -+ -+/** -+ * Tests JWT encode and decode. -+ */ -+class JwtManagementTest extends \PHPUnit\Framework\TestCase -+{ -+ /** -+ * API key -+ */ -+ private const API_KEY = 'API key'; -+ -+ /** -+ * @var JwtManagement -+ */ -+ private $model; -+ -+ /** -+ * @inheritdoc -+ */ -+ protected function setUp() -+ { -+ $this->model = new JwtManagement(new Json()); -+ } -+ -+ /** -+ * Tests JWT encode. -+ */ -+ public function testEncode() -+ { -+ $jwt = $this->model->encode($this->getTokenPayload(), self::API_KEY); -+ -+ $this->assertEquals( -+ $this->getValidHS256Jwt(), -+ $jwt, -+ 'Generated JWT isn\'t equal to expected' -+ ); -+ } -+ -+ /** -+ * Tests JWT decode. -+ */ -+ public function testDecode() -+ { -+ $tokenPayload = $this->model->decode($this->getValidHS256Jwt(), self::API_KEY); -+ -+ $this->assertEquals( -+ $this->getTokenPayload(), -+ $tokenPayload, -+ 'JWT payload isn\'t equal to expected' -+ ); -+ } -+ -+ /** -+ * Tests JWT decode. -+ * -+ * @param string $jwt -+ * @param string $errorMessage -+ * @dataProvider decodeWithExceptionDataProvider -+ */ -+ public function testDecodeWithException(string $jwt, string $errorMessage) -+ { -+ $this->expectException(\InvalidArgumentException::class); -+ $this->expectExceptionMessage($errorMessage); -+ -+ $this->model->decode($jwt, self::API_KEY); -+ } -+ -+ /** -+ * @return array -+ */ -+ public function decodeWithExceptionDataProvider(): array -+ { -+ return [ -+ [ -+ 'jwt' => '', -+ 'errorMessage' => 'JWT is empty', -+ ], -+ [ -+ 'jwt' => 'dddd.dddd', -+ 'errorMessage' => 'Wrong number of segments in JWT', -+ ], -+ [ -+ 'jwt' => 'dddd.dddd.dddd', -+ 'errorMessage' => 'Unable to unserialize value. Error: Syntax error', -+ ], -+ [ -+ 'jwt' => $this->getHS512Jwt(), -+ 'errorMessage' => 'Algorithm HS512 is not supported', -+ ], -+ [ -+ 'jwt' => $this->getJwtWithInvalidSignature(), -+ 'errorMessage' => 'JWT signature verification failed', -+ ], -+ ]; -+ } -+ -+ /** -+ * Returns valid JWT, signed using HS256. -+ * -+ * @return string -+ */ -+ private function getValidHS256Jwt(): string -+ { -+ return 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJqdGkiOiJhNWE1OWJmYi1hYzA2LTRjNWYtYmU1Yy0zNTFiNjR' . -+ 'hZTYwOGUiLCJpc3MiOiI1NjU2MGEzNThiOTQ2ZTBjODQ1MjM2NWRzIiwiaWF0IjoiMTQ0ODk5Nzg2NSIsIk9yZ1Vua' . -+ 'XRJZCI6IjU2NTYwN2MxOGI5NDZlMDU4NDYzZHM4ciIsIlBheWxvYWQiOnsiT3JkZXJEZXRhaWxzIjp7Ik9yZGVyTnV' . -+ 'tYmVyIjoiMTI1IiwiQW1vdW50IjoiMTUwMCIsIkN1cnJlbmN5Q29kZSI6IlVTRCJ9fSwiT2JqZWN0aWZ5UGF5bG9hZ' . -+ 'CI6dHJ1ZX0.emv9N75JIvyk_gQHMNJYQ2UzmbM5ISBQs1Y222zO1jk'; -+ } -+ -+ /** -+ * Returns JWT, signed using not supported HS512. -+ * -+ * @return string -+ */ -+ private function getHS512Jwt(): string -+ { -+ return 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzUxMiJ9.eyJqdGkiOiJhNWE1OWJmYi1hYzA2LTRjNWYtYmU1Yy0zNTFiNjR' . -+ 'hZTYwOGUiLCJpc3MiOiI1NjU2MGEzNThiOTQ2ZTBjODQ1MjM2NWRzIiwiaWF0IjoiMTQ0ODk5Nzg2NSIsIk9yZ1V' . -+ 'uaXRJZCI6IjU2NTYwN2MxOGI5NDZlMDU4NDYzZHM4ciIsIlBheWxvYWQiOnsiT3JkZXJEZXRhaWxzIjp7Ik9yZGV' . -+ 'yTnVtYmVyIjoiMTI1IiwiQW1vdW50IjoiMTUwMCIsIkN1cnJlbmN5Q29kZSI6IlVTRCJ9fSwiT2JqZWN0aWZ5UGF' . -+ '5bG9hZCI6dHJ1ZX0.4fwdXfIgUe7bAiHP2bZsxSG-s-wJOyaCmCe9MOQhs-m6LLjRGarguA_0SqZA2EeUaytMO4R' . -+ 'G84ZEOfbYfS8c1A'; -+ } -+ -+ /** -+ * Returns JWT with invalid signature. -+ * -+ * @return string -+ */ -+ private function getJwtWithInvalidSignature(): string -+ { -+ return 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJqdGkiOiJhNWE1OWJmYi1hYzA2LTRjNWYtYmU1Yy0zNTFiNjR' . -+ 'hZTYwOGUiLCJpc3MiOiI1NjU2MGEzNThiOTQ2ZTBjODQ1MjM2NWRzIiwiaWF0IjoiMTQ0ODk5Nzg2NSIsIk9yZ1Vua' . -+ 'XRJZCI6IjU2NTYwN2MxOGI5NDZlMDU4NDYzZHM4ciIsIlBheWxvYWQiOnsiT3JkZXJEZXRhaWxzIjp7Ik9yZGVyTnV' . -+ 'tYmVyIjoiMTI1IiwiQW1vdW50IjoiMTUwMCIsIkN1cnJlbmN5Q29kZSI6IlVTRCJ9fSwiT2JqZWN0aWZ5UGF5bG9hZ' . -+ 'CI6dHJ1ZX0.InvalidSign'; -+ } -+ -+ /** -+ * Returns token decoded payload. -+ * -+ * @return array -+ */ -+ private function getTokenPayload(): array -+ { -+ return [ -+ 'jti' => 'a5a59bfb-ac06-4c5f-be5c-351b64ae608e', -+ 'iss' => '56560a358b946e0c8452365ds', -+ 'iat' => '1448997865', -+ 'OrgUnitId' => '565607c18b946e058463ds8r', -+ 'Payload' => [ -+ 'OrderDetails' => [ -+ 'OrderNumber' => '125', -+ 'Amount' => '1500', -+ 'CurrencyCode' => 'USD' -+ ] -+ ], -+ 'ObjectifyPayload' => true -+ ]; -+ } -+} -diff --git a/app/code/Magento/CardinalCommerce/Test/Unit/Model/Response/JwtPayloadValidatorTest.php b/app/code/Magento/CardinalCommerce/Test/Unit/Model/Response/JwtPayloadValidatorTest.php -new file mode 100644 -index 00000000000..cbaae9f777a ---- /dev/null -+++ b/app/code/Magento/CardinalCommerce/Test/Unit/Model/Response/JwtPayloadValidatorTest.php -@@ -0,0 +1,202 @@ -+<?php -+/** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+declare(strict_types=1); -+ -+namespace Magento\CardinalCommerce\Test\Unit\Model\Response; -+ -+use Magento\CardinalCommerce\Model\Response\JwtPayloadValidator; -+use Magento\Framework\Intl\DateTimeFactory; -+ -+/** -+ * Class JwtPayloadValidatorTest -+ */ -+class JwtPayloadValidatorTest extends \PHPUnit\Framework\TestCase -+{ -+ /** -+ * @var JwtPayloadValidator -+ */ -+ private $model; -+ -+ /** -+ * @inheritdoc -+ */ -+ protected function setUp() -+ { -+ $this->model = new JwtPayloadValidator(new DateTimeFactory()); -+ } -+ -+ /** -+ * Tests successful cases. -+ * -+ * @param array $token -+ * @dataProvider validateSuccessDataProvider -+ */ -+ public function testValidateSuccess(array $token) -+ { -+ $this->assertTrue( -+ $this->model->validate($token) -+ ); -+ } -+ -+ /** -+ * @case 1. All data are correct, the transaction was successful (Visa, AMEX) -+ * @case 2. All data are correct, the transaction was successful but requires in no additional action (Visa, AMEX) -+ * @case 3. All data are correct, the transaction was successful (MasterCard) -+ * @case 4. All data are correct, the transaction was successful but requires in no additional action (MasterCard) -+ * -+ * @return array -+ */ -+ public function validateSuccessDataProvider() -+ { -+ $expTimestamp = $this->getValidExpTimestamp(); -+ -+ return [ -+ 1 => $this->createToken('05', '0', 'SUCCESS', $expTimestamp), -+ 2 => $this->createToken('06', '0', 'NOACTION', $expTimestamp), -+ 3 => $this->createToken('02', '0', 'SUCCESS', $expTimestamp), -+ 4 => $this->createToken('01', '0', 'NOACTION', $expTimestamp), -+ ]; -+ } -+ -+ /** -+ * Case when 3DS authentication is either failed or could not be attempted. -+ * -+ * @param array $token -+ * @dataProvider validationEciFailsDataProvider -+ */ -+ public function testValidationEciFails(array $token) -+ { -+ $this->assertFalse( -+ $this->model->validate($token), -+ 'Negative ECIFlag value validation fails' -+ ); -+ } -+ -+ /** -+ * ECIFlag value when 3DS authentication is either failed or could not be attempted. -+ * -+ * @case 1. Visa, AMEX, JCB -+ * @case 2. MasterCard -+ * @return array -+ */ -+ public function validationEciFailsDataProvider(): array -+ { -+ $expTimestamp = $this->getValidExpTimestamp(); -+ return [ -+ 1 => $this->createToken('07', '0', 'NOACTION', $expTimestamp), -+ 2 => $this->createToken('00', '0', 'NOACTION', $expTimestamp), -+ ]; -+ } -+ -+ /** -+ * Case when resulting state of the transaction is negative. -+ * -+ * @param array $token -+ * @dataProvider validationActionCodeFailsDataProvider -+ */ -+ public function testValidationActionCodeFails(array $token) -+ { -+ $this->assertFalse( -+ $this->model->validate($token), -+ 'Negative ActionCode value validation fails' -+ ); -+ } -+ -+ /** -+ * ECIFlag value when 3DS authentication is either failed or could not be attempted. -+ * -+ * @return array -+ */ -+ public function validationActionCodeFailsDataProvider(): array -+ { -+ $expTimestamp = $this->getValidExpTimestamp(); -+ return [ -+ $this->createToken('05', '0', 'FAILURE', $expTimestamp), -+ $this->createToken('05', '0', 'ERROR', $expTimestamp), -+ ]; -+ } -+ -+ /** -+ * Case when ErrorNumber not equal 0. -+ */ -+ public function testValidationErrorNumberFails() -+ { -+ $notAllowedErrorNumber = '10'; -+ $expTimestamp = $this->getValidExpTimestamp(); -+ $token = $this->createToken('05', $notAllowedErrorNumber, 'SUCCESS', $expTimestamp); -+ $this->assertFalse( -+ $this->model->validate($token), -+ 'Negative ErrorNumber value validation fails' -+ ); -+ } -+ -+ /** -+ * Case when ErrorNumber not equal 0. -+ */ -+ public function testValidationExpirationFails() -+ { -+ $expTimestamp = $this->getOutdatedExpTimestamp(); -+ $token = $this->createToken('05', '0', 'SUCCESS', $expTimestamp); -+ $this->assertFalse( -+ $this->model->validate($token), -+ 'Expiration date validation fails' -+ ); -+ } -+ -+ /** -+ * Creates a token. -+ * -+ * @param string $eciFlag -+ * @param string $errorNumber -+ * @param string $actionCode -+ * @param int $expTimestamp -+ * -+ * @return array -+ */ -+ private function createToken(string $eciFlag, string $errorNumber, string $actionCode, int $expTimestamp): array -+ { -+ return [ -+ [ -+ 'Payload' => [ -+ 'Payment' => [ -+ 'ExtendedData' => [ -+ 'ECIFlag' => $eciFlag, -+ ], -+ ], -+ 'ActionCode' => $actionCode, -+ 'ErrorNumber' => $errorNumber -+ ], -+ 'exp' => $expTimestamp -+ ] -+ ]; -+ } -+ -+ /** -+ * Returns valid expiration timestamp. -+ * -+ * @return int -+ */ -+ private function getValidExpTimestamp() -+ { -+ $dateTimeFactory = new DateTimeFactory(); -+ $currentDate = $dateTimeFactory->create('now', new \DateTimeZone('UTC')); -+ -+ return $currentDate->getTimestamp() + 3600; -+ } -+ -+ /** -+ * Returns outdated expiration timestamp. -+ * -+ * @return int -+ */ -+ private function getOutdatedExpTimestamp() -+ { -+ $dateTimeFactory = new DateTimeFactory(); -+ $currentDate = $dateTimeFactory->create('now', new \DateTimeZone('UTC')); -+ -+ return $currentDate->getTimestamp() - 3600; -+ } -+} -diff --git a/app/code/Magento/CardinalCommerce/composer.json b/app/code/Magento/CardinalCommerce/composer.json -new file mode 100644 -index 00000000000..3e839228dc7 ---- /dev/null -+++ b/app/code/Magento/CardinalCommerce/composer.json -@@ -0,0 +1,27 @@ -+{ -+ "name": "magento/module-cardinal-commerce", -+ "description": "Provides a possibility to enable 3-D Secure 2.0 support for payment methods.", -+ "config": { -+ "sort-packages": true -+ }, -+ "require": { -+ "php": "~7.1.3||~7.2.0", -+ "magento/framework": "*", -+ "magento/module-checkout": "*", -+ "magento/module-payment": "*", -+ "magento/module-store": "*" -+ }, -+ "type": "magento2-module", -+ "license": [ -+ "OSL-3.0", -+ "AFL-3.0" -+ ], -+ "autoload": { -+ "files": [ -+ "registration.php" -+ ], -+ "psr-4": { -+ "Magento\\CardinalCommerce\\": "" -+ } -+ } -+} -diff --git a/app/code/Magento/CardinalCommerce/etc/adminhtml/system.xml b/app/code/Magento/CardinalCommerce/etc/adminhtml/system.xml -new file mode 100644 -index 00000000000..4fa40436d4a ---- /dev/null -+++ b/app/code/Magento/CardinalCommerce/etc/adminhtml/system.xml -@@ -0,0 +1,48 @@ -+<?xml version="1.0"?> -+<!-- -+/** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Config:etc/system_file.xsd"> -+ <system> -+ <section id="three_d_secure" translate="label" type="text" sortOrder="410" showInDefault="1" showInWebsite="1" showInStore="1"> -+ <label>3D Secure</label> -+ <tab>sales</tab> -+ <resource>Magento_Sales::three_d_secure</resource> -+ <group id="cardinal" type="text" sortOrder="13" showInDefault="1" showInWebsite="1" showInStore="0"> -+ <group id="config" translate="label comment" sortOrder="15" showInDefault="1" showInWebsite="1" showInStore="0"> -+ <label>Configuration</label> -+ <comment><![CDATA[For support contact <a href="mailto:support@cardinalcommerce.com">support@cardinalcommerce.com</a>.]]> -+ </comment> -+ <field id="environment" translate="label" type="select" sortOrder="20" showInDefault="1" showInWebsite="1" showInStore="0"> -+ <label>Environment</label> -+ <source_model>Magento\CardinalCommerce\Model\Adminhtml\Source\Environment</source_model> -+ <config_path>three_d_secure/cardinal/environment</config_path> -+ </field> -+ <field id="org_unit_id" translate="label" type="obscure" sortOrder="30" showInDefault="1" showInWebsite="1" showInStore="0"> -+ <label>Org Unit Id</label> -+ <config_path>three_d_secure/cardinal/org_unit_id</config_path> -+ <backend_model>Magento\Config\Model\Config\Backend\Encrypted</backend_model> -+ </field> -+ <field id="api_key" translate="label" type="obscure" sortOrder="40" showInDefault="1" showInWebsite="1" showInStore="0"> -+ <label>API Key</label> -+ <config_path>three_d_secure/cardinal/api_key</config_path> -+ <backend_model>Magento\Config\Model\Config\Backend\Encrypted</backend_model> -+ </field> -+ <field id="api_identifier" translate="label" type="obscure" sortOrder="50" showInDefault="1" showInWebsite="1" showInStore="0"> -+ <label>API Identifier</label> -+ <config_path>three_d_secure/cardinal/api_identifier</config_path> -+ <backend_model>Magento\Config\Model\Config\Backend\Encrypted</backend_model> -+ </field> -+ <field id="debug" translate="label" type="select" sortOrder="60" showInDefault="1" showInWebsite="1" showInStore="0"> -+ <label>Debug</label> -+ <source_model>Magento\Config\Model\Config\Source\Yesno</source_model> -+ <config_path>three_d_secure/cardinal/debug</config_path> -+ </field> -+ </group> -+ </group> -+ </section> -+ </system> -+</config> -diff --git a/app/code/Magento/CardinalCommerce/etc/config.xml b/app/code/Magento/CardinalCommerce/etc/config.xml -new file mode 100644 -index 00000000000..60b111a59cb ---- /dev/null -+++ b/app/code/Magento/CardinalCommerce/etc/config.xml -@@ -0,0 +1,20 @@ -+<?xml version="1.0"?> -+<!-- -+/** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Store:etc/config.xsd"> -+ <default> -+ <three_d_secure> -+ <cardinal> -+ <environment>production</environment> -+ <api_key backend_model="Magento\Config\Model\Config\Backend\Encrypted" /> -+ <org_unit_id backend_model="Magento\Config\Model\Config\Backend\Encrypted" /> -+ <api_identifier backend_model="Magento\Config\Model\Config\Backend\Encrypted" /> -+ <debug>0</debug> -+ </cardinal> -+ </three_d_secure> -+ </default> -+</config> -diff --git a/app/code/Magento/CardinalCommerce/etc/di.xml b/app/code/Magento/CardinalCommerce/etc/di.xml -new file mode 100644 -index 00000000000..ffd3c50ef50 ---- /dev/null -+++ b/app/code/Magento/CardinalCommerce/etc/di.xml -@@ -0,0 +1,10 @@ -+<?xml version="1.0"?> -+<!-- -+/** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd"> -+ <preference for="Magento\CardinalCommerce\Model\Response\JwtPayloadValidatorInterface" type="Magento\CardinalCommerce\Model\Response\JwtPayloadValidator" /> -+</config> -diff --git a/app/code/Magento/CardinalCommerce/etc/frontend/di.xml b/app/code/Magento/CardinalCommerce/etc/frontend/di.xml -new file mode 100644 -index 00000000000..e3913291aa6 ---- /dev/null -+++ b/app/code/Magento/CardinalCommerce/etc/frontend/di.xml -@@ -0,0 +1,18 @@ -+<?xml version="1.0"?> -+<!-- -+/** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd"> -+ <type name="Magento\Checkout\Model\CompositeConfigProvider"> -+ <arguments> -+ <argument name="configProviders" xsi:type="array"> -+ <item name="cardinal_config_provider" xsi:type="object"> -+ Magento\CardinalCommerce\Model\Checkout\ConfigProvider -+ </item> -+ </argument> -+ </arguments> -+ </type> -+</config> -diff --git a/app/code/Magento/CardinalCommerce/etc/module.xml b/app/code/Magento/CardinalCommerce/etc/module.xml -new file mode 100644 -index 00000000000..8605e81d3fd ---- /dev/null -+++ b/app/code/Magento/CardinalCommerce/etc/module.xml -@@ -0,0 +1,15 @@ -+<?xml version="1.0"?> -+<!-- -+/** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:Module/etc/module.xsd"> -+ <module name="Magento_CardinalCommerce" > -+ <sequence> -+ <module name="Magento_Checkout"/> -+ <module name="Magento_Payment"/> -+ </sequence> -+ </module> -+</config> -diff --git a/app/code/Magento/CardinalCommerce/registration.php b/app/code/Magento/CardinalCommerce/registration.php -new file mode 100644 -index 00000000000..26fb168fb0a ---- /dev/null -+++ b/app/code/Magento/CardinalCommerce/registration.php -@@ -0,0 +1,9 @@ -+<?php -+/** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+ -+use \Magento\Framework\Component\ComponentRegistrar; -+ -+ComponentRegistrar::register(ComponentRegistrar::MODULE, 'Magento_CardinalCommerce', __DIR__); -diff --git a/app/code/Magento/CardinalCommerce/view/frontend/requirejs-config.js b/app/code/Magento/CardinalCommerce/view/frontend/requirejs-config.js -new file mode 100644 -index 00000000000..0c5e3964d04 ---- /dev/null -+++ b/app/code/Magento/CardinalCommerce/view/frontend/requirejs-config.js -@@ -0,0 +1,20 @@ -+/** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+ -+var config = { -+ shim: { -+ cardinaljs: { -+ exports: 'Cardinal' -+ }, -+ cardinaljsSandbox: { -+ exports: 'Cardinal' -+ } -+ }, -+ paths: { -+ cardinaljsSandbox: 'https://includestest.ccdc02.com/cardinalcruise/v1/songbird', -+ cardinaljs: 'https://songbird.cardinalcommerce.com/edge/v1/songbird' -+ } -+}; -+ -diff --git a/app/code/Magento/CardinalCommerce/view/frontend/web/js/cardinal-client.js b/app/code/Magento/CardinalCommerce/view/frontend/web/js/cardinal-client.js -new file mode 100644 -index 00000000000..2ddb450d2f8 ---- /dev/null -+++ b/app/code/Magento/CardinalCommerce/view/frontend/web/js/cardinal-client.js -@@ -0,0 +1,131 @@ -+/** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+ -+define([ -+ 'jquery', -+ 'uiClass', -+ 'Magento_CardinalCommerce/js/cardinal-factory', -+ 'Magento_Checkout/js/model/quote', -+ 'mage/translate' -+], function ($, Class, cardinalFactory, quote, $t) { -+ 'use strict'; -+ -+ return { -+ /** -+ * Starts Cardinal Consumer Authentication -+ * -+ * @param {Object} cardData -+ * @return {jQuery.Deferred} -+ */ -+ startAuthentication: function (cardData) { -+ var deferred = $.Deferred(); -+ -+ if (this.cardinalClient) { -+ this._startAuthentication(deferred, cardData); -+ } else { -+ cardinalFactory(this.getEnvironment()) -+ .done(function (client) { -+ this.cardinalClient = client; -+ this._startAuthentication(deferred, cardData); -+ }.bind(this)); -+ } -+ -+ return deferred.promise(); -+ }, -+ -+ /** -+ * Cardinal Consumer Authentication -+ * -+ * @param {jQuery.Deferred} deferred -+ * @param {Object} cardData -+ */ -+ _startAuthentication: function (deferred, cardData) { -+ //this.cardinalClient.configure({ logging: { level: 'verbose' } }); -+ this.cardinalClient.on('payments.validated', function (data, jwt) { -+ if (data.ErrorNumber !== 0) { -+ deferred.reject(data.ErrorDescription); -+ } else if ($.inArray(data.ActionCode, ['FAILURE', 'ERROR']) !== -1) { -+ deferred.reject($t('Authentication Failed. Please try again with another form of payment.')); -+ } else { -+ deferred.resolve(jwt); -+ } -+ this.cardinalClient.off('payments.validated'); -+ }.bind(this)); -+ -+ this.cardinalClient.on('payments.setupComplete', function () { -+ this.cardinalClient.start('cca', this.getRequestOrderObject(cardData)); -+ this.cardinalClient.off('payments.setupComplete'); -+ }.bind(this)); -+ -+ this.cardinalClient.setup('init', { -+ jwt: this.getRequestJWT() -+ }); -+ }, -+ -+ /** -+ * Returns request order object. -+ * -+ * The request order object is structured object that is used to pass data -+ * to Cardinal that describes an order you'd like to process. -+ * -+ * If you pass a request object in both the JWT and the browser, -+ * Cardinal will merge the objects together where the browser overwrites -+ * the JWT object as it is considered the most recently captured data. -+ * -+ * @param {Object} cardData -+ * @returns {Object} -+ */ -+ getRequestOrderObject: function (cardData) { -+ var totalAmount = quote.totals()['base_grand_total'], -+ currencyCode = quote.totals()['base_currency_code'], -+ billingAddress = quote.billingAddress(), -+ requestObject; -+ -+ requestObject = { -+ OrderDetails: { -+ Amount: totalAmount * 100, -+ CurrencyCode: currencyCode -+ }, -+ Consumer: { -+ Account: { -+ AccountNumber: cardData.accountNumber, -+ ExpirationMonth: cardData.expMonth, -+ ExpirationYear: cardData.expYear, -+ CardCode: cardData.cardCode -+ }, -+ BillingAddress: { -+ FirstName: billingAddress.firstname, -+ LastName: billingAddress.lastname, -+ Address1: billingAddress.street[0], -+ Address2: billingAddress.street[1], -+ City: billingAddress.city, -+ State: billingAddress.region, -+ PostalCode: billingAddress.postcode, -+ CountryCode: billingAddress.countryId, -+ Phone1: billingAddress.telephone -+ } -+ } -+ }; -+ -+ return requestObject; -+ }, -+ -+ /** -+ * Returns request JWT -+ * @returns {String} -+ */ -+ getRequestJWT: function () { -+ return window.checkoutConfig.cardinal.requestJWT; -+ }, -+ -+ /** -+ * Returns type of environment -+ * @returns {String} -+ */ -+ getEnvironment: function () { -+ return window.checkoutConfig.cardinal.environment; -+ } -+ }; -+}); -diff --git a/app/code/Magento/CardinalCommerce/view/frontend/web/js/cardinal-factory.js b/app/code/Magento/CardinalCommerce/view/frontend/web/js/cardinal-factory.js -new file mode 100644 -index 00000000000..1da92ba2ff7 ---- /dev/null -+++ b/app/code/Magento/CardinalCommerce/view/frontend/web/js/cardinal-factory.js -@@ -0,0 +1,29 @@ -+/** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+ -+define([ -+ 'jquery' -+], function ($) { -+ 'use strict'; -+ -+ return function (environment) { -+ var deferred = $.Deferred(), -+ dependency = 'cardinaljs'; -+ -+ if (environment === 'sandbox') { -+ dependency = 'cardinaljsSandbox'; -+ } -+ -+ require( -+ [dependency], -+ function (Cardinal) { -+ deferred.resolve(Cardinal); -+ }, -+ deferred.reject -+ ); -+ -+ return deferred.promise(); -+ }; -+}); -diff --git a/app/code/Magento/Catalog/Api/CategoryLinkRepositoryInterface.php b/app/code/Magento/Catalog/Api/CategoryLinkRepositoryInterface.php -index 30ac06107ba..a65355c6909 100644 ---- a/app/code/Magento/Catalog/Api/CategoryLinkRepositoryInterface.php -+++ b/app/code/Magento/Catalog/Api/CategoryLinkRepositoryInterface.php -@@ -32,18 +32,20 @@ interface CategoryLinkRepositoryInterface - * - * @throws \Magento\Framework\Exception\CouldNotSaveException - * @throws \Magento\Framework\Exception\StateException -+ * @throws \Magento\Framework\Exception\InputException - */ - public function delete(\Magento\Catalog\Api\Data\CategoryProductLinkInterface $productLink); - - /** - * Remove the product assignment from the category by category id and sku - * -- * @param string $sku -+ * @param int $categoryId - * @param string $sku - * @return bool will returned True if products successfully deleted - * - * @throws \Magento\Framework\Exception\CouldNotSaveException - * @throws \Magento\Framework\Exception\StateException -+ * @throws \Magento\Framework\Exception\InputException - */ - public function deleteByIds($categoryId, $sku); - } -diff --git a/app/code/Magento/Catalog/Api/Data/CategoryInterface.php b/app/code/Magento/Catalog/Api/Data/CategoryInterface.php -index b9a23e9d08e..1940a0ac80c 100644 ---- a/app/code/Magento/Catalog/Api/Data/CategoryInterface.php -+++ b/app/code/Magento/Catalog/Api/Data/CategoryInterface.php -@@ -1,7 +1,5 @@ - <?php - /** -- * Category data interface -- * - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -@@ -9,6 +7,8 @@ - namespace Magento\Catalog\Api\Data; - - /** -+ * Category data interface. -+ * - * @api - * @since 100.0.2 - */ -@@ -46,11 +46,15 @@ interface CategoryInterface extends \Magento\Framework\Api\CustomAttributesDataI - /**#@-*/ - - /** -+ * Retrieve category id. -+ * - * @return int|null - */ - public function getId(); - - /** -+ * Set category id. -+ * - * @param int $id - * @return $this - */ -@@ -74,7 +78,7 @@ interface CategoryInterface extends \Magento\Framework\Api\CustomAttributesDataI - /** - * Get category name - * -- * @return string -+ * @return string|null - */ - public function getName(); - -@@ -132,60 +136,82 @@ interface CategoryInterface extends \Magento\Framework\Api\CustomAttributesDataI - public function setLevel($level); - - /** -+ * Retrieve children ids comma separated. -+ * - * @return string|null - */ - public function getChildren(); - - /** -+ * Retrieve category creation date and time. -+ * - * @return string|null - */ - public function getCreatedAt(); - - /** -+ * Set category creation date and time. -+ * - * @param string $createdAt - * @return $this - */ - public function setCreatedAt($createdAt); - - /** -+ * Retrieve category last update date and time. -+ * - * @return string|null - */ - public function getUpdatedAt(); - - /** -+ * Set category last update date and time. -+ * - * @param string $updatedAt - * @return $this - */ - public function setUpdatedAt($updatedAt); - - /** -+ * Retrieve category full path. -+ * - * @return string|null - */ - public function getPath(); - - /** -+ * Set category full path. -+ * - * @param string $path - * @return $this - */ - public function setPath($path); - - /** -+ * Retrieve available sort by for category. -+ * - * @return string[]|null - */ - public function getAvailableSortBy(); - - /** -+ * Set available sort by for category. -+ * - * @param string[]|string $availableSortBy - * @return $this - */ - public function setAvailableSortBy($availableSortBy); - - /** -+ * Get category is included in menu. -+ * - * @return bool|null - */ - public function getIncludeInMenu(); - - /** -+ * Set category is included in menu. -+ * - * @param bool $includeInMenu - * @return $this - */ -diff --git a/app/code/Magento/Catalog/Api/Data/ProductRender/ImageInterface.php b/app/code/Magento/Catalog/Api/Data/ProductRender/ImageInterface.php -index 4cdb2631ede..45b070d2706 100644 ---- a/app/code/Magento/Catalog/Api/Data/ProductRender/ImageInterface.php -+++ b/app/code/Magento/Catalog/Api/Data/ProductRender/ImageInterface.php -@@ -92,6 +92,7 @@ interface ImageInterface extends ExtensibleDataInterface - - /** - * Retrieve image label -+ * - * Image label is short description of this image - * - * @return string -@@ -111,7 +112,7 @@ interface ImageInterface extends ExtensibleDataInterface - /** - * Retrieve resize width - * -- * This width is image dimension, which represents the width, that can be used for perfomance improvements -+ * This width is image dimension, which represents the width, that can be used for performance improvements - * - * @return float - * @since 101.1.0 -@@ -128,6 +129,8 @@ interface ImageInterface extends ExtensibleDataInterface - public function setResizedWidth($width); - - /** -+ * Set resized height -+ * - * @param string $height - * @return void - * @since 101.1.0 -diff --git a/app/code/Magento/Catalog/Api/Data/ProductRender/PriceInfoInterface.php b/app/code/Magento/Catalog/Api/Data/ProductRender/PriceInfoInterface.php -index 2ef4d068317..9768b3c08c8 100644 ---- a/app/code/Magento/Catalog/Api/Data/ProductRender/PriceInfoInterface.php -+++ b/app/code/Magento/Catalog/Api/Data/ProductRender/PriceInfoInterface.php -@@ -8,6 +8,7 @@ namespace Magento\Catalog\Api\Data\ProductRender; - - /** - * Price interface. -+ * - * @api - * @since 101.1.0 - */ -@@ -23,6 +24,7 @@ interface PriceInfoInterface extends \Magento\Framework\Api\ExtensibleDataInterf - - /** - * Set the final price: usually it calculated as minimal price of the product -+ * - * Can be different depends on type of product - * - * @param float $finalPrice -@@ -33,6 +35,7 @@ interface PriceInfoInterface extends \Magento\Framework\Api\ExtensibleDataInterf - - /** - * Retrieve max price of a product -+ * - * E.g. for product with custom options is price with the most expensive custom option - * - * @return float -@@ -51,6 +54,7 @@ interface PriceInfoInterface extends \Magento\Framework\Api\ExtensibleDataInterf - - /** - * Set max regular price -+ * - * Max regular price is the same, as maximum price, except of excluding calculating special price and catalog rules - * in it - * -@@ -105,6 +109,8 @@ interface PriceInfoInterface extends \Magento\Framework\Api\ExtensibleDataInterf - public function getSpecialPrice(); - - /** -+ * Retrieve minimal price -+ * - * @return float - * @since 101.1.0 - */ -@@ -129,6 +135,7 @@ interface PriceInfoInterface extends \Magento\Framework\Api\ExtensibleDataInterf - - /** - * Regular price - is price of product without discounts and special price with taxes and fixed product tax -+ * - * Usually this price is corresponding to price in admin panel of product - * - * @param float $regularPrice -@@ -148,7 +155,7 @@ interface PriceInfoInterface extends \Magento\Framework\Api\ExtensibleDataInterf - /** - * Set dto with formatted prices - * -- * @param string[] $formattedPriceInfo -+ * @param FormattedPriceInfoInterface $formattedPriceInfo - * @return void - * @since 101.1.0 - */ -diff --git a/app/code/Magento/Catalog/Api/Data/ProductRenderInterface.php b/app/code/Magento/Catalog/Api/Data/ProductRenderInterface.php -index 910168d8854..166a1aba76b 100644 ---- a/app/code/Magento/Catalog/Api/Data/ProductRenderInterface.php -+++ b/app/code/Magento/Catalog/Api/Data/ProductRenderInterface.php -@@ -30,7 +30,7 @@ interface ProductRenderInterface extends ExtensibleDataInterface - /** - * Set information needed for render "Add To Cart" button on front - * -- * @param \Magento\Catalog\Api\Data\ProductRender\ButtonInterface $addToCartData -+ * @param ButtonInterface $cartAddToCartButton - * @return void - * @since 101.1.0 - */ -@@ -47,7 +47,7 @@ interface ProductRenderInterface extends ExtensibleDataInterface - /** - * Set information needed for render "Add To Compare" button on front - * -- * @param ButtonInterface $compareUrlData -+ * @param ButtonInterface $compareButton - * @return string - * @since 101.1.0 - */ -@@ -55,6 +55,7 @@ interface ProductRenderInterface extends ExtensibleDataInterface - - /** - * Provide information needed for render prices and adjustments for different product types on front -+ * - * Prices are represented in raw format and in current currency - * - * @return \Magento\Catalog\Api\Data\ProductRender\PriceInfoInterface -@@ -73,6 +74,7 @@ interface ProductRenderInterface extends ExtensibleDataInterface - - /** - * Provide enough information, that needed to render image on front -+ * - * Images can be separated by image codes - * - * @return \Magento\Catalog\Api\Data\ProductRender\ImageInterface[] -@@ -167,6 +169,7 @@ interface ProductRenderInterface extends ExtensibleDataInterface - - /** - * Set information about product saleability (Stock, other conditions) -+ * - * Is used to provide information to frontend JS renders - * You can add plugin, in order to hide product on product page or product list on front - * -@@ -178,6 +181,7 @@ interface ProductRenderInterface extends ExtensibleDataInterface - - /** - * Provide information about current store id or requested store id -+ * - * Product should be assigned to provided store id - * This setting affect store scope attributes - * -@@ -197,6 +201,7 @@ interface ProductRenderInterface extends ExtensibleDataInterface - - /** - * Provide current or desired currency code to product -+ * - * This setting affect formatted prices* - * - * @return string -diff --git a/app/code/Magento/Catalog/Api/ProductRenderListInterface.php b/app/code/Magento/Catalog/Api/ProductRenderListInterface.php -index f79efa4c814..954acd35a07 100644 ---- a/app/code/Magento/Catalog/Api/ProductRenderListInterface.php -+++ b/app/code/Magento/Catalog/Api/ProductRenderListInterface.php -@@ -4,18 +4,22 @@ - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -+ - namespace Magento\Catalog\Api; - - /** -- * Interface which provides product renders information for products -+ * Interface which provides product renders information for products. -+ * - * @api - * @since 101.1.0 - */ - interface ProductRenderListInterface - { - /** -- * Collect and retrieve the list of product render info -- * This info contains raw prices and formated prices, product name, stock status, store_id, etc -+ * Collect and retrieve the list of product render info. -+ * -+ * This info contains raw prices and formatted prices, product name, stock status, store_id, etc. -+ * - * @see \Magento\Catalog\Api\Data\ProductRenderInfoDtoInterface - * - * @param \Magento\Framework\Api\SearchCriteriaInterface $searchCriteria -diff --git a/app/code/Magento/Catalog/Block/Adminhtml/Category/AbstractCategory.php b/app/code/Magento/Catalog/Block/Adminhtml/Category/AbstractCategory.php -index 33167987462..ffb648cdf43 100644 ---- a/app/code/Magento/Catalog/Block/Adminhtml/Category/AbstractCategory.php -+++ b/app/code/Magento/Catalog/Block/Adminhtml/Category/AbstractCategory.php -@@ -67,6 +67,8 @@ class AbstractCategory extends \Magento\Backend\Block\Template - } - - /** -+ * Get category id -+ * - * @return int|string|null - */ - public function getCategoryId() -@@ -78,6 +80,8 @@ class AbstractCategory extends \Magento\Backend\Block\Template - } - - /** -+ * Get category name -+ * - * @return string - */ - public function getCategoryName() -@@ -86,6 +90,8 @@ class AbstractCategory extends \Magento\Backend\Block\Template - } - - /** -+ * Get category path -+ * - * @return mixed - */ - public function getCategoryPath() -@@ -97,6 +103,8 @@ class AbstractCategory extends \Magento\Backend\Block\Template - } - - /** -+ * Check store root category -+ * - * @return bool - */ - public function hasStoreRootCategory() -@@ -109,6 +117,8 @@ class AbstractCategory extends \Magento\Backend\Block\Template - } - - /** -+ * Get store from request -+ * - * @return Store - */ - public function getStore() -@@ -118,6 +128,8 @@ class AbstractCategory extends \Magento\Backend\Block\Template - } - - /** -+ * Get root category for tree -+ * - * @param mixed|null $parentNodeCategory - * @param int $recursionLevel - * @return Node|array|null -@@ -149,10 +161,11 @@ class AbstractCategory extends \Magento\Backend\Block\Template - - $root = $tree->getNodeById($rootId); - -- if ($root && $rootId != \Magento\Catalog\Model\Category::TREE_ROOT_ID) { -+ if ($root) { - $root->setIsVisible(true); -- } elseif ($root && $root->getId() == \Magento\Catalog\Model\Category::TREE_ROOT_ID) { -- $root->setName(__('Root')); -+ if ($root->getId() == \Magento\Catalog\Model\Category::TREE_ROOT_ID) { -+ $root->setName(__('Root')); -+ } - } - - $this->_coreRegistry->register('root', $root); -@@ -162,6 +175,8 @@ class AbstractCategory extends \Magento\Backend\Block\Template - } - - /** -+ * Get Default Store Id -+ * - * @return int - */ - protected function _getDefaultStoreId() -@@ -170,6 +185,8 @@ class AbstractCategory extends \Magento\Backend\Block\Template - } - - /** -+ * Get category collection -+ * - * @return \Magento\Framework\Model\ResourceModel\Db\Collection\AbstractCollection - */ - public function getCategoryCollection() -@@ -227,6 +244,8 @@ class AbstractCategory extends \Magento\Backend\Block\Template - } - - /** -+ * Get category node for tree -+ * - * @param mixed $parentNodeCategory - * @param int $recursionLevel - * @return Node -@@ -249,6 +268,8 @@ class AbstractCategory extends \Magento\Backend\Block\Template - } - - /** -+ * Get category save url -+ * - * @param array $args - * @return string - */ -@@ -260,6 +281,8 @@ class AbstractCategory extends \Magento\Backend\Block\Template - } - - /** -+ * Get category edit url -+ * - * @return string - */ - public function getEditUrl() -diff --git a/app/code/Magento/Catalog/Block/Adminhtml/Category/Edit/DeleteButton.php b/app/code/Magento/Catalog/Block/Adminhtml/Category/Edit/DeleteButton.php -index 20411a4c4d7..2eef1188e39 100644 ---- a/app/code/Magento/Catalog/Block/Adminhtml/Category/Edit/DeleteButton.php -+++ b/app/code/Magento/Catalog/Block/Adminhtml/Category/Edit/DeleteButton.php -@@ -27,7 +27,8 @@ class DeleteButton extends AbstractCategory implements ButtonProviderInterface - return [ - 'id' => 'delete', - 'label' => __('Delete'), -- 'on_click' => "categoryDelete('" . $this->getDeleteUrl() . "')", -+ 'on_click' => "deleteConfirm('" .__('Are you sure you want to delete this category?') ."', '" -+ . $this->getDeleteUrl() . "', {data: {}})", - 'class' => 'delete', - 'sort_order' => 10 - ]; -diff --git a/app/code/Magento/Catalog/Block/Adminhtml/Category/Helper/Pricestep.php b/app/code/Magento/Catalog/Block/Adminhtml/Category/Helper/Pricestep.php -index b77a5e2e952..3266922d116 100644 ---- a/app/code/Magento/Catalog/Block/Adminhtml/Category/Helper/Pricestep.php -+++ b/app/code/Magento/Catalog/Block/Adminhtml/Category/Helper/Pricestep.php -@@ -11,6 +11,9 @@ - */ - namespace Magento\Catalog\Block\Adminhtml\Category\Helper; - -+/** -+ * Pricestep Helper -+ */ - class Pricestep extends \Magento\Framework\Data\Form\Element\Text - { - /** -@@ -40,7 +43,7 @@ class Pricestep extends \Magento\Framework\Data\Form\Element\Text - $disabled = true; - } - -- parent::addClass('validate-number validate-number-range number-range-0.01-1000000000'); -+ parent::addClass('validate-number validate-number-range number-range-0.01-9999999999999999'); - $html = parent::getElementHtml(); - $htmlId = 'use_config_' . $this->getHtmlId(); - $html .= '<br/><input id="' . $htmlId . '" name="use_config[]" value="' . $this->getId() . '"'; -diff --git a/app/code/Magento/Catalog/Block/Adminhtml/Category/Tree.php b/app/code/Magento/Catalog/Block/Adminhtml/Category/Tree.php -index a67f55235b6..9a4a9fa7680 100644 ---- a/app/code/Magento/Catalog/Block/Adminhtml/Category/Tree.php -+++ b/app/code/Magento/Catalog/Block/Adminhtml/Category/Tree.php -@@ -71,7 +71,7 @@ class Tree extends \Magento\Catalog\Block\Adminhtml\Category\AbstractCategory - } - - /** -- * @return void -+ * @inheritdoc - */ - protected function _construct() - { -@@ -80,7 +80,7 @@ class Tree extends \Magento\Catalog\Block\Adminhtml\Category\AbstractCategory - } - - /** -- * @return $this -+ * @inheritdoc - */ - protected function _prepareLayout() - { -@@ -182,6 +182,8 @@ class Tree extends \Magento\Catalog\Block\Adminhtml\Category\AbstractCategory - } - - /** -+ * Get add root button html -+ * - * @return string - */ - public function getAddRootButtonHtml() -@@ -190,6 +192,8 @@ class Tree extends \Magento\Catalog\Block\Adminhtml\Category\AbstractCategory - } - - /** -+ * Get add sub button html -+ * - * @return string - */ - public function getAddSubButtonHtml() -@@ -198,6 +202,8 @@ class Tree extends \Magento\Catalog\Block\Adminhtml\Category\AbstractCategory - } - - /** -+ * Get expand button html -+ * - * @return string - */ - public function getExpandButtonHtml() -@@ -206,6 +212,8 @@ class Tree extends \Magento\Catalog\Block\Adminhtml\Category\AbstractCategory - } - - /** -+ * Get collapse button html -+ * - * @return string - */ - public function getCollapseButtonHtml() -@@ -214,6 +222,8 @@ class Tree extends \Magento\Catalog\Block\Adminhtml\Category\AbstractCategory - } - - /** -+ * Get store switcher -+ * - * @return string - */ - public function getStoreSwitcherHtml() -@@ -222,6 +232,8 @@ class Tree extends \Magento\Catalog\Block\Adminhtml\Category\AbstractCategory - } - - /** -+ * Get loader tree url -+ * - * @param bool|null $expanded - * @return string - */ -@@ -235,14 +247,18 @@ class Tree extends \Magento\Catalog\Block\Adminhtml\Category\AbstractCategory - } - - /** -+ * Get nodes url -+ * - * @return string - */ - public function getNodesUrl() - { -- return $this->getUrl('catalog/category/jsonTree'); -+ return $this->getUrl('catalog/category/tree'); - } - - /** -+ * Get switcher tree url -+ * - * @return string - */ - public function getSwitchTreeUrl() -@@ -254,6 +270,8 @@ class Tree extends \Magento\Catalog\Block\Adminhtml\Category\AbstractCategory - } - - /** -+ * Get is was expanded -+ * - * @return bool - * @SuppressWarnings(PHPMD.BooleanGetMethodName) - */ -@@ -263,6 +281,8 @@ class Tree extends \Magento\Catalog\Block\Adminhtml\Category\AbstractCategory - } - - /** -+ * Get move url -+ * - * @return string - */ - public function getMoveUrl() -@@ -271,6 +291,8 @@ class Tree extends \Magento\Catalog\Block\Adminhtml\Category\AbstractCategory - } - - /** -+ * Get tree -+ * - * @param mixed|null $parenNodeCategory - * @return array - */ -@@ -282,6 +304,8 @@ class Tree extends \Magento\Catalog\Block\Adminhtml\Category\AbstractCategory - } - - /** -+ * Get tree json -+ * - * @param mixed|null $parenNodeCategory - * @return string - */ -@@ -367,7 +391,7 @@ class Tree extends \Magento\Catalog\Block\Adminhtml\Category\AbstractCategory - } - } - -- if ($isParent || $node->getLevel() < 2) { -+ if ($isParent || $node->getLevel() < 1) { - $item['expanded'] = true; - } - -@@ -390,6 +414,8 @@ class Tree extends \Magento\Catalog\Block\Adminhtml\Category\AbstractCategory - } - - /** -+ * Is category movable -+ * - * @param Node|array $node - * @return bool - */ -@@ -403,6 +429,8 @@ class Tree extends \Magento\Catalog\Block\Adminhtml\Category\AbstractCategory - } - - /** -+ * Is parent selected category -+ * - * @param Node|array $node - * @return bool - */ -diff --git a/app/code/Magento/Catalog/Block/Adminhtml/Form/Renderer/Fieldset/Element.php b/app/code/Magento/Catalog/Block/Adminhtml/Form/Renderer/Fieldset/Element.php -index ad6df27b893..8f1d1dcf7ee 100644 ---- a/app/code/Magento/Catalog/Block/Adminhtml/Form/Renderer/Fieldset/Element.php -+++ b/app/code/Magento/Catalog/Block/Adminhtml/Form/Renderer/Fieldset/Element.php -@@ -4,13 +4,13 @@ - * See COPYING.txt for license details. - */ - -+namespace Magento\Catalog\Block\Adminhtml\Form\Renderer\Fieldset; -+ - /** - * Catalog fieldset element renderer - * - * @author Magento Core Team <core@magentocommerce.com> - */ --namespace Magento\Catalog\Block\Adminhtml\Form\Renderer\Fieldset; -- - class Element extends \Magento\Backend\Block\Widget\Form\Renderer\Fieldset\Element - { - /** -@@ -29,7 +29,7 @@ class Element extends \Magento\Backend\Block\Widget\Form\Renderer\Fieldset\Eleme - } - - /** -- * Retireve associated with element attribute object -+ * Retrieve associated with element attribute object - * - * @return \Magento\Catalog\Model\ResourceModel\Eav\Attribute - */ -diff --git a/app/code/Magento/Catalog/Block/Adminhtml/Helper/Form/Wysiwyg.php b/app/code/Magento/Catalog/Block/Adminhtml/Helper/Form/Wysiwyg.php -index 62c27a42141..a829c058d89 100644 ---- a/app/code/Magento/Catalog/Block/Adminhtml/Helper/Form/Wysiwyg.php -+++ b/app/code/Magento/Catalog/Block/Adminhtml/Helper/Form/Wysiwyg.php -@@ -11,6 +11,9 @@ - */ - namespace Magento\Catalog\Block\Adminhtml\Helper\Form; - -+/** -+ * Wysiwyg helper. -+ */ - class Wysiwyg extends \Magento\Framework\Data\Form\Element\Textarea - { - /** -@@ -23,7 +26,7 @@ class Wysiwyg extends \Magento\Framework\Data\Form\Element\Textarea - /** - * Catalog data - * -- * @var \Magento\Framework\Module\Manager -+ * @var \Magento\Framework\Module\ModuleManagerInterface - */ - protected $_moduleManager = null; - -@@ -43,7 +46,7 @@ class Wysiwyg extends \Magento\Framework\Data\Form\Element\Textarea - * @param \Magento\Framework\Escaper $escaper - * @param \Magento\Cms\Model\Wysiwyg\Config $wysiwygConfig - * @param \Magento\Framework\View\LayoutInterface $layout -- * @param \Magento\Framework\Module\Manager $moduleManager -+ * @param \Magento\Framework\Module\ModuleManagerInterface $moduleManager - * @param \Magento\Backend\Helper\Data $backendData - * @param array $data - */ -@@ -53,7 +56,7 @@ class Wysiwyg extends \Magento\Framework\Data\Form\Element\Textarea - \Magento\Framework\Escaper $escaper, - \Magento\Cms\Model\Wysiwyg\Config $wysiwygConfig, - \Magento\Framework\View\LayoutInterface $layout, -- \Magento\Framework\Module\Manager $moduleManager, -+ \Magento\Framework\Module\ModuleManagerInterface $moduleManager, - \Magento\Backend\Helper\Data $backendData, - array $data = [] - ) { -diff --git a/app/code/Magento/Catalog/Block/Adminhtml/Product/Attribute/Edit/Tab/Advanced.php b/app/code/Magento/Catalog/Block/Adminhtml/Product/Attribute/Edit/Tab/Advanced.php -index dd09e40ac5b..1b675696866 100644 ---- a/app/code/Magento/Catalog/Block/Adminhtml/Product/Attribute/Edit/Tab/Advanced.php -+++ b/app/code/Magento/Catalog/Block/Adminhtml/Product/Attribute/Edit/Tab/Advanced.php -@@ -4,11 +4,6 @@ - * See COPYING.txt for license details. - */ - --/** -- * Product attribute add/edit form main tab -- * -- * @author Magento Core Team <core@magentocommerce.com> -- */ - namespace Magento\Catalog\Block\Adminhtml\Product\Attribute\Edit\Tab; - - use Magento\Backend\Block\Widget\Form\Generic; -@@ -18,6 +13,8 @@ use Magento\Eav\Helper\Data; - use Magento\Framework\App\ObjectManager; - - /** -+ * Product attribute add/edit form main tab -+ * - * @api - * @since 100.0.2 - */ -@@ -73,6 +70,7 @@ class Advanced extends Generic - * Adding product form elements for editing attribute - * - * @return $this -+ * @throws \Magento\Framework\Exception\LocalizedException - * @SuppressWarnings(PHPMD) - */ - protected function _prepareForm() -@@ -255,7 +253,7 @@ class Advanced extends Generic - } - - /** -- * Initialize form fileds values -+ * Initialize form fields values - * - * @return $this - */ -diff --git a/app/code/Magento/Catalog/Block/Adminhtml/Product/Attribute/Set/Main.php b/app/code/Magento/Catalog/Block/Adminhtml/Product/Attribute/Set/Main.php -index 1b188de4071..3b9036c1fbb 100644 ---- a/app/code/Magento/Catalog/Block/Adminhtml/Product/Attribute/Set/Main.php -+++ b/app/code/Magento/Catalog/Block/Adminhtml/Product/Attribute/Set/Main.php -@@ -6,13 +6,13 @@ - namespace Magento\Catalog\Block\Adminhtml\Product\Attribute\Set; - - /** -- * Adminhtml Catalog Attribute Set Main Block -- * - * @author Magento Core Team <core@magentocommerce.com> - */ - use Magento\Catalog\Model\Entity\Product\Attribute\Group\AttributeMapperInterface; - - /** -+ * Adminhtml Catalog Attribute Set Main Block. -+ * - * @api - * @SuppressWarnings(PHPMD.CouplingBetweenObjects) - * @since 100.0.2 -@@ -140,7 +140,7 @@ class Main extends \Magento\Backend\Block\Template - ) . '\', \'' . $this->getUrl( - 'catalog/*/delete', - ['id' => $setId] -- ) . '\')', -+ ) . '\',{data: {}})', - 'class' => 'delete' - ] - ); -diff --git a/app/code/Magento/Catalog/Block/Adminhtml/Product/Attribute/Set/Main/Formgroup.php b/app/code/Magento/Catalog/Block/Adminhtml/Product/Attribute/Set/Main/Formgroup.php -index ee92fd7c19b..26ffc6e0df3 100644 ---- a/app/code/Magento/Catalog/Block/Adminhtml/Product/Attribute/Set/Main/Formgroup.php -+++ b/app/code/Magento/Catalog/Block/Adminhtml/Product/Attribute/Set/Main/Formgroup.php -@@ -11,6 +11,9 @@ namespace Magento\Catalog\Block\Adminhtml\Product\Attribute\Set\Main; - - use Magento\Backend\Block\Widget\Form; - -+/** -+ * Form group for attribute set -+ */ - class Formgroup extends \Magento\Backend\Block\Widget\Form\Generic - { - /** -@@ -37,6 +40,8 @@ class Formgroup extends \Magento\Backend\Block\Widget\Form\Generic - } - - /** -+ * Prepare form elements -+ * - * @return void - */ - protected function _prepareForm() -@@ -77,13 +82,15 @@ class Formgroup extends \Magento\Backend\Block\Widget\Form\Generic - } - - /** -+ * Returns set id -+ * - * @return int - */ - protected function _getSetId() - { -- return intval( -+ return (int)( - $this->getRequest()->getParam('id') -- ) > 0 ? intval( -+ ) > 0 ? (int)( - $this->getRequest()->getParam('id') - ) : $this->_typeFactory->create()->load( - $this->_coreRegistry->registry('entityType') -diff --git a/app/code/Magento/Catalog/Block/Adminhtml/Product/Edit.php b/app/code/Magento/Catalog/Block/Adminhtml/Product/Edit.php -index 285caa974fd..e6afc41ebeb 100644 ---- a/app/code/Magento/Catalog/Block/Adminhtml/Product/Edit.php -+++ b/app/code/Magento/Catalog/Block/Adminhtml/Product/Edit.php -@@ -12,8 +12,18 @@ - */ - namespace Magento\Catalog\Block\Adminhtml\Product; - -+use Magento\Framework\Escaper; -+ -+/** -+ * Class Edit -+ */ - class Edit extends \Magento\Backend\Block\Widget - { -+ /** -+ * @var Escaper -+ */ -+ private $escaper; -+ - /** - * @var string - */ -@@ -47,6 +57,7 @@ class Edit extends \Magento\Backend\Block\Widget - * @param \Magento\Eav\Model\Entity\Attribute\SetFactory $attributeSetFactory - * @param \Magento\Framework\Registry $registry - * @param \Magento\Catalog\Helper\Product $productHelper -+ * @param Escaper $escaper - * @param array $data - */ - public function __construct( -@@ -55,16 +66,20 @@ class Edit extends \Magento\Backend\Block\Widget - \Magento\Eav\Model\Entity\Attribute\SetFactory $attributeSetFactory, - \Magento\Framework\Registry $registry, - \Magento\Catalog\Helper\Product $productHelper, -+ Escaper $escaper, - array $data = [] - ) { - $this->_productHelper = $productHelper; - $this->_attributeSetFactory = $attributeSetFactory; - $this->_coreRegistry = $registry; - $this->jsonEncoder = $jsonEncoder; -+ $this->escaper = $escaper; - parent::__construct($context, $data); - } - - /** -+ * Edit Product constructor -+ * - * @return void - */ - protected function _construct() -@@ -144,6 +159,8 @@ class Edit extends \Magento\Backend\Block\Widget - } - - /** -+ * Retrieve back button html -+ * - * @return string - */ - public function getBackButtonHtml() -@@ -152,6 +169,8 @@ class Edit extends \Magento\Backend\Block\Widget - } - - /** -+ * Retrieve cancel button html -+ * - * @return string - */ - public function getCancelButtonHtml() -@@ -160,6 +179,8 @@ class Edit extends \Magento\Backend\Block\Widget - } - - /** -+ * Retrieve save button html -+ * - * @return string - */ - public function getSaveButtonHtml() -@@ -168,6 +189,8 @@ class Edit extends \Magento\Backend\Block\Widget - } - - /** -+ * Retrieve save and edit button html -+ * - * @return string - */ - public function getSaveAndEditButtonHtml() -@@ -176,6 +199,8 @@ class Edit extends \Magento\Backend\Block\Widget - } - - /** -+ * Retrieve delete button html -+ * - * @return string - */ - public function getDeleteButtonHtml() -@@ -194,6 +219,8 @@ class Edit extends \Magento\Backend\Block\Widget - } - - /** -+ * Retrieve validation url -+ * - * @return string - */ - public function getValidationUrl() -@@ -202,6 +229,8 @@ class Edit extends \Magento\Backend\Block\Widget - } - - /** -+ * Retrieve save url -+ * - * @return string - */ - public function getSaveUrl() -@@ -210,6 +239,8 @@ class Edit extends \Magento\Backend\Block\Widget - } - - /** -+ * Retrieve save and continue url -+ * - * @return string - */ - public function getSaveAndContinueUrl() -@@ -221,6 +252,8 @@ class Edit extends \Magento\Backend\Block\Widget - } - - /** -+ * Retrieve product id -+ * - * @return mixed - */ - public function getProductId() -@@ -229,6 +262,8 @@ class Edit extends \Magento\Backend\Block\Widget - } - - /** -+ * Retrieve product set id -+ * - * @return mixed - */ - public function getProductSetId() -@@ -241,6 +276,8 @@ class Edit extends \Magento\Backend\Block\Widget - } - - /** -+ * Retrieve duplicate url -+ * - * @return string - */ - public function getDuplicateUrl() -@@ -249,6 +286,8 @@ class Edit extends \Magento\Backend\Block\Widget - } - - /** -+ * Retrieve product header -+ * - * @deprecated 101.1.0 - * @return string - */ -@@ -263,6 +302,8 @@ class Edit extends \Magento\Backend\Block\Widget - } - - /** -+ * Get product attribute set name -+ * - * @return string - */ - public function getAttributeSetName() -@@ -275,11 +316,14 @@ class Edit extends \Magento\Backend\Block\Widget - } - - /** -+ * Retrieve id of selected tab -+ * - * @return string - */ - public function getSelectedTabId() - { -- return addslashes(htmlspecialchars($this->getRequest()->getParam('tab'))); -+ // phpcs:ignore Magento2.Functions.DiscouragedFunction -+ return addslashes($this->escaper->escapeHtml($this->getRequest()->getParam('tab'))); - } - - /** -diff --git a/app/code/Magento/Catalog/Block/Adminhtml/Product/Edit/Action/Attribute/Tab/Attributes.php b/app/code/Magento/Catalog/Block/Adminhtml/Product/Edit/Action/Attribute/Tab/Attributes.php -index 4310f61c571..2df0ff0b6cd 100644 ---- a/app/code/Magento/Catalog/Block/Adminhtml/Product/Edit/Action/Attribute/Tab/Attributes.php -+++ b/app/code/Magento/Catalog/Block/Adminhtml/Product/Edit/Action/Attribute/Tab/Attributes.php -@@ -9,13 +9,18 @@ - * - * @author Magento Core Team <core@magentocommerce.com> - */ -+declare(strict_types=1); -+ - namespace Magento\Catalog\Block\Adminhtml\Product\Edit\Action\Attribute\Tab; - - use Magento\Framework\Data\Form\Element\AbstractElement; - - /** -+ * Attributes tab block -+ * - * @api - * @SuppressWarnings(PHPMD.DepthOfInheritance) -+ * @SuppressWarnings(PHPMD.CouplingBetweenObjects) - * @since 100.0.2 - */ - class Attributes extends \Magento\Catalog\Block\Adminhtml\Form implements -@@ -31,6 +36,9 @@ class Attributes extends \Magento\Catalog\Block\Adminhtml\Form implements - */ - protected $_attributeAction; - -+ /** @var array */ -+ private $excludeFields; -+ - /** - * @param \Magento\Backend\Block\Template\Context $context - * @param \Magento\Framework\Registry $registry -@@ -38,6 +46,7 @@ class Attributes extends \Magento\Catalog\Block\Adminhtml\Form implements - * @param \Magento\Catalog\Model\ProductFactory $productFactory - * @param \Magento\Catalog\Helper\Product\Edit\Action\Attribute $attributeAction - * @param array $data -+ * @param array|null $excludeFields - */ - public function __construct( - \Magento\Backend\Block\Template\Context $context, -@@ -45,37 +54,25 @@ class Attributes extends \Magento\Catalog\Block\Adminhtml\Form implements - \Magento\Framework\Data\FormFactory $formFactory, - \Magento\Catalog\Model\ProductFactory $productFactory, - \Magento\Catalog\Helper\Product\Edit\Action\Attribute $attributeAction, -- array $data = [] -+ array $data = [], -+ array $excludeFields = null - ) { - $this->_attributeAction = $attributeAction; - $this->_productFactory = $productFactory; -- parent::__construct($context, $registry, $formFactory, $data); -- } -+ $this->excludeFields = $excludeFields ?: []; - -- /** -- * @return void -- */ -- protected function _construct() -- { -- parent::_construct(); -- $this->setShowGlobalIcon(true); -+ parent::__construct($context, $registry, $formFactory, $data); - } - - /** -+ * Prepares form -+ * - * @return void -+ * @throws \Magento\Framework\Exception\LocalizedException - */ -- protected function _prepareForm() -+ protected function _prepareForm(): void - { -- $this->setFormExcludedFieldList( -- [ -- 'category_ids', -- 'gallery', -- 'image', -- 'media_gallery', -- 'quantity_and_stock_status', -- 'tier_price', -- ] -- ); -+ $this->setFormExcludedFieldList($this->getExcludedFields()); - $this->_eventManager->dispatch( - 'adminhtml_catalog_product_form_prepare_excluded_field_list', - ['object' => $this] -@@ -149,12 +146,14 @@ HTML; - weightHandle.hideWeightSwitcher(); - });</script> - HTML; -- // @codingStandardsIgnoreEnd -+ // @codingStandardsIgnoreEnd - } - return $html; - } - - /** -+ * Returns tab label -+ * - * @return \Magento\Framework\Phrase - */ - public function getTabLabel() -@@ -163,6 +162,8 @@ HTML; - } - - /** -+ * Return Tab title -+ * - * @return \Magento\Framework\Phrase - */ - public function getTabTitle() -@@ -171,6 +172,8 @@ HTML; - } - - /** -+ * Can show tab in tabs -+ * - * @return bool - */ - public function canShowTab() -@@ -179,10 +182,22 @@ HTML; - } - - /** -+ * Tab not hidden -+ * - * @return bool - */ - public function isHidden() - { - return false; - } -+ -+ /** -+ * Returns excluded fields -+ * -+ * @return array -+ */ -+ private function getExcludedFields(): array -+ { -+ return $this->excludeFields; -+ } - } -diff --git a/app/code/Magento/Catalog/Block/Adminhtml/Product/Edit/Action/Attribute/Tab/Inventory.php b/app/code/Magento/Catalog/Block/Adminhtml/Product/Edit/Action/Attribute/Tab/Inventory.php -index 750bf6f8a02..964872b6e51 100644 ---- a/app/code/Magento/Catalog/Block/Adminhtml/Product/Edit/Action/Attribute/Tab/Inventory.php -+++ b/app/code/Magento/Catalog/Block/Adminhtml/Product/Edit/Action/Attribute/Tab/Inventory.php -@@ -70,11 +70,11 @@ class Inventory extends \Magento\Backend\Block\Widget implements \Magento\Backen - * Retrieve current store id - * - * @return int -+ * @SuppressWarnings(PHPMD.RequestAwareBlockMethod) - */ - public function getStoreId() - { -- $storeId = $this->getRequest()->getParam('store'); -- return intval($storeId); -+ return (int)$this->getRequest()->getParam('store'); - } - - /** -@@ -99,6 +99,8 @@ class Inventory extends \Magento\Backend\Block\Widget implements \Magento\Backen - } - - /** -+ * Return Tab title. -+ * - * @return \Magento\Framework\Phrase - */ - public function getTabTitle() -@@ -107,7 +109,7 @@ class Inventory extends \Magento\Backend\Block\Widget implements \Magento\Backen - } - - /** -- * @return bool -+ * @inheritdoc - */ - public function canShowTab() - { -@@ -115,7 +117,7 @@ class Inventory extends \Magento\Backend\Block\Widget implements \Magento\Backen - } - - /** -- * @return bool -+ * @inheritdoc - */ - public function isHidden() - { -@@ -123,6 +125,8 @@ class Inventory extends \Magento\Backend\Block\Widget implements \Magento\Backen - } - - /** -+ * Get availability status. -+ * - * @param string $fieldName - * @return bool - * @SuppressWarnings(PHPMD.UnusedFormalParameter) -diff --git a/app/code/Magento/Catalog/Block/Adminhtml/Product/Edit/AttributeSet.php b/app/code/Magento/Catalog/Block/Adminhtml/Product/Edit/AttributeSet.php -index 3467c7aac28..d95ee7f8f2c 100644 ---- a/app/code/Magento/Catalog/Block/Adminhtml/Product/Edit/AttributeSet.php -+++ b/app/code/Magento/Catalog/Block/Adminhtml/Product/Edit/AttributeSet.php -@@ -11,6 +11,9 @@ - */ - namespace Magento\Catalog\Block\Adminhtml\Product\Edit; - -+/** -+ * Admin AttributeSet block -+ */ - class AttributeSet extends \Magento\Backend\Block\Widget\Form - { - /** -@@ -42,12 +45,14 @@ class AttributeSet extends \Magento\Backend\Block\Widget\Form - public function getSelectorOptions() - { - return [ -- 'source' => $this->getUrl('catalog/product/suggestAttributeSets'), -+ 'source' => $this->escapeUrl($this->getUrl('catalog/product/suggestAttributeSets')), - 'className' => 'category-select', - 'showRecent' => true, - 'storageKey' => 'product-template-key', - 'minLength' => 0, -- 'currentlySelected' => $this->_coreRegistry->registry('product')->getAttributeSetId() -+ 'currentlySelected' => $this->escapeHtml( -+ $this->_coreRegistry->registry('product')->getAttributeSetId() -+ ) - ]; - } - } -diff --git a/app/code/Magento/Catalog/Block/Adminhtml/Product/Edit/Tab/Alerts/Price.php b/app/code/Magento/Catalog/Block/Adminhtml/Product/Edit/Tab/Alerts/Price.php -index 3b4a5f9eabf..e754ab97005 100644 ---- a/app/code/Magento/Catalog/Block/Adminhtml/Product/Edit/Tab/Alerts/Price.php -+++ b/app/code/Magento/Catalog/Block/Adminhtml/Product/Edit/Tab/Alerts/Price.php -@@ -19,7 +19,7 @@ class Price extends Extended - /** - * Catalog data - * -- * @var \Magento\Framework\Module\Manager -+ * @var \Magento\Framework\Module\ModuleManagerInterface - */ - protected $moduleManager; - -@@ -32,14 +32,14 @@ class Price extends Extended - * @param \Magento\Backend\Block\Template\Context $context - * @param \Magento\Backend\Helper\Data $backendHelper - * @param \Magento\ProductAlert\Model\PriceFactory $priceFactory -- * @param \Magento\Framework\Module\Manager $moduleManager -+ * @param \Magento\Framework\Module\ModuleManagerInterface $moduleManager - * @param array $data - */ - public function __construct( - \Magento\Backend\Block\Template\Context $context, - \Magento\Backend\Helper\Data $backendHelper, - \Magento\ProductAlert\Model\PriceFactory $priceFactory, -- \Magento\Framework\Module\Manager $moduleManager, -+ \Magento\Framework\Module\ModuleManagerInterface $moduleManager, - array $data = [] - ) { - $this->_priceFactory = $priceFactory; -@@ -48,6 +48,8 @@ class Price extends Extended - } - - /** -+ * Construct. -+ * - * @return void - */ - protected function _construct() -@@ -63,6 +65,8 @@ class Price extends Extended - } - - /** -+ * @inheritDoc -+ * - * @return Grid - */ - protected function _prepareCollection() -@@ -80,6 +84,8 @@ class Price extends Extended - } - - /** -+ * @inheritDoc -+ * - * @return $this - */ - protected function _prepareColumns() -@@ -116,6 +122,8 @@ class Price extends Extended - } - - /** -+ * @inheritDoc -+ * - * @return string - */ - public function getGridUrl() -diff --git a/app/code/Magento/Catalog/Block/Adminhtml/Product/Edit/Tab/Alerts/Stock.php b/app/code/Magento/Catalog/Block/Adminhtml/Product/Edit/Tab/Alerts/Stock.php -index d572690143c..2c6647fd57b 100644 ---- a/app/code/Magento/Catalog/Block/Adminhtml/Product/Edit/Tab/Alerts/Stock.php -+++ b/app/code/Magento/Catalog/Block/Adminhtml/Product/Edit/Tab/Alerts/Stock.php -@@ -19,7 +19,7 @@ class Stock extends Extended - /** - * Catalog data - * -- * @var \Magento\Framework\Module\Manager -+ * @var \Magento\Framework\Module\ModuleManagerInterface - */ - protected $moduleManager; - -@@ -32,14 +32,14 @@ class Stock extends Extended - * @param \Magento\Backend\Block\Template\Context $context - * @param \Magento\Backend\Helper\Data $backendHelper - * @param \Magento\ProductAlert\Model\StockFactory $stockFactory -- * @param \Magento\Framework\Module\Manager $moduleManager -+ * @param \Magento\Framework\Module\ModuleManagerInterface $moduleManager - * @param array $data - */ - public function __construct( - \Magento\Backend\Block\Template\Context $context, - \Magento\Backend\Helper\Data $backendHelper, - \Magento\ProductAlert\Model\StockFactory $stockFactory, -- \Magento\Framework\Module\Manager $moduleManager, -+ \Magento\Framework\Module\ModuleManagerInterface $moduleManager, - array $data = [] - ) { - $this->_stockFactory = $stockFactory; -@@ -48,6 +48,8 @@ class Stock extends Extended - } - - /** -+ * Construct. -+ * - * @return void - */ - protected function _construct() -@@ -63,6 +65,8 @@ class Stock extends Extended - } - - /** -+ * @inheritDoc -+ * - * @return Grid - */ - protected function _prepareCollection() -@@ -80,6 +84,8 @@ class Stock extends Extended - } - - /** -+ * @inheritDoc -+ * - * @return $this - */ - protected function _prepareColumns() -@@ -103,6 +109,8 @@ class Stock extends Extended - } - - /** -+ * Get grid url. -+ * - * @return string - */ - public function getGridUrl() -diff --git a/app/code/Magento/Catalog/Block/Adminhtml/Product/Edit/Tab/Attributes/Search.php b/app/code/Magento/Catalog/Block/Adminhtml/Product/Edit/Tab/Attributes/Search.php -index e1b97f996c7..42463354926 100644 ---- a/app/code/Magento/Catalog/Block/Adminhtml/Product/Edit/Tab/Attributes/Search.php -+++ b/app/code/Magento/Catalog/Block/Adminhtml/Product/Edit/Tab/Attributes/Search.php -@@ -11,6 +11,9 @@ - */ - namespace Magento\Catalog\Block\Adminhtml\Product\Edit\Tab\Attributes; - -+/** -+ * Admin product attribute search block -+ */ - class Search extends \Magento\Backend\Block\Widget - { - /** -@@ -62,13 +65,15 @@ class Search extends \Magento\Backend\Block\Widget - } - - /** -+ * Get selector options -+ * - * @return array - */ - public function getSelectorOptions() - { - $templateId = $this->_coreRegistry->registry('product')->getAttributeSetId(); - return [ -- 'source' => $this->getUrl('catalog/product/suggestAttributes'), -+ 'source' => $this->escapeUrl($this->getUrl('catalog/product/suggestAttributes')), - 'minLength' => 0, - 'ajaxOptions' => ['data' => ['template_id' => $templateId]], - 'template' => '[data-template-for="product-attribute-search-' . $this->getGroupId() . '"]', -@@ -110,6 +115,8 @@ class Search extends \Magento\Backend\Block\Widget - } - - /** -+ * Get add attribute url -+ * - * @return string - */ - public function getAddAttributeUrl() -diff --git a/app/code/Magento/Catalog/Block/Adminhtml/Product/Edit/Tab/Crosssell.php b/app/code/Magento/Catalog/Block/Adminhtml/Product/Edit/Tab/Crosssell.php -index e4f700e5790..e5ce59c550a 100644 ---- a/app/code/Magento/Catalog/Block/Adminhtml/Product/Edit/Tab/Crosssell.php -+++ b/app/code/Magento/Catalog/Block/Adminhtml/Product/Edit/Tab/Crosssell.php -@@ -10,9 +10,13 @@ use Magento\Backend\Block\Widget\Grid\Extended; - use Magento\Catalog\Model\Product; - - /** -+ * Crossel product edit tab -+ * - * @api - * @SuppressWarnings(PHPMD.CouplingBetweenObjects) - * @since 100.0.2 -+ * @deprecated Not used since cross-sell products grid moved to UI components. -+ * @see \Magento\Catalog\Ui\DataProvider\Product\Related\CrossSellDataProvider - */ - class Crosssell extends Extended - { -diff --git a/app/code/Magento/Catalog/Block/Adminhtml/Product/Edit/Tab/Inventory.php b/app/code/Magento/Catalog/Block/Adminhtml/Product/Edit/Tab/Inventory.php -index 20e12889cae..9278b84362e 100644 ---- a/app/code/Magento/Catalog/Block/Adminhtml/Product/Edit/Tab/Inventory.php -+++ b/app/code/Magento/Catalog/Block/Adminhtml/Product/Edit/Tab/Inventory.php -@@ -18,7 +18,7 @@ class Inventory extends \Magento\Backend\Block\Widget - protected $_template = 'Magento_Catalog::catalog/product/tab/inventory.phtml'; - - /** -- * @var \Magento\Framework\Module\Manager -+ * @var \Magento\Framework\Module\ModuleManagerInterface - */ - protected $moduleManager; - -@@ -53,7 +53,7 @@ class Inventory extends \Magento\Backend\Block\Widget - * @param \Magento\Backend\Block\Template\Context $context - * @param \Magento\CatalogInventory\Model\Source\Backorders $backorders - * @param \Magento\CatalogInventory\Model\Source\Stock $stock -- * @param \Magento\Framework\Module\Manager $moduleManager -+ * @param \Magento\Framework\Module\ModuleManagerInterface $moduleManager - * @param \Magento\Framework\Registry $coreRegistry - * @param \Magento\CatalogInventory\Api\StockRegistryInterface $stockRegistry - * @param \Magento\CatalogInventory\Api\StockConfigurationInterface $stockConfiguration -@@ -63,7 +63,7 @@ class Inventory extends \Magento\Backend\Block\Widget - \Magento\Backend\Block\Template\Context $context, - \Magento\CatalogInventory\Model\Source\Backorders $backorders, - \Magento\CatalogInventory\Model\Source\Stock $stock, -- \Magento\Framework\Module\Manager $moduleManager, -+ \Magento\Framework\Module\ModuleManagerInterface $moduleManager, - \Magento\Framework\Registry $coreRegistry, - \Magento\CatalogInventory\Api\StockRegistryInterface $stockRegistry, - \Magento\CatalogInventory\Api\StockConfigurationInterface $stockConfiguration, -@@ -79,6 +79,8 @@ class Inventory extends \Magento\Backend\Block\Widget - } - - /** -+ * Get backorder option. -+ * - * @return array - */ - public function getBackordersOption() -@@ -128,6 +130,8 @@ class Inventory extends \Magento\Backend\Block\Widget - } - - /** -+ * Get field value. -+ * - * @param string $field - * @return string|null - */ -@@ -145,6 +149,8 @@ class Inventory extends \Magento\Backend\Block\Widget - } - - /** -+ * Get config field value. -+ * - * @param string $field - * @return string|null - */ -@@ -163,6 +169,8 @@ class Inventory extends \Magento\Backend\Block\Widget - } - - /** -+ * Get default config value. -+ * - * @param string $field - * @return string|null - */ -@@ -182,6 +190,8 @@ class Inventory extends \Magento\Backend\Block\Widget - } - - /** -+ * Is new. -+ * - * @return bool - */ - public function isNew() -@@ -193,6 +203,8 @@ class Inventory extends \Magento\Backend\Block\Widget - } - - /** -+ * Get field suffix. -+ * - * @return string - */ - public function getFieldSuffix() -@@ -221,6 +233,8 @@ class Inventory extends \Magento\Backend\Block\Widget - } - - /** -+ * Is single store mode enabled. -+ * - * @return bool - */ - public function isSingleStoreMode() -diff --git a/app/code/Magento/Catalog/Block/Adminhtml/Product/Edit/Tab/Price/Group/AbstractGroup.php b/app/code/Magento/Catalog/Block/Adminhtml/Product/Edit/Tab/Price/Group/AbstractGroup.php -index 5ffd3d1dda3..42990116e93 100644 ---- a/app/code/Magento/Catalog/Block/Adminhtml/Product/Edit/Tab/Price/Group/AbstractGroup.php -+++ b/app/code/Magento/Catalog/Block/Adminhtml/Product/Edit/Tab/Price/Group/AbstractGroup.php -@@ -41,7 +41,7 @@ abstract class AbstractGroup extends Widget implements RendererInterface - /** - * Catalog data - * -- * @var \Magento\Framework\Module\Manager -+ * @var \Magento\Framework\Module\ModuleManagerInterface - */ - protected $moduleManager; - -@@ -81,7 +81,7 @@ abstract class AbstractGroup extends Widget implements RendererInterface - * @param \Magento\Backend\Block\Template\Context $context - * @param GroupRepositoryInterface $groupRepository - * @param \Magento\Directory\Helper\Data $directoryHelper -- * @param \Magento\Framework\Module\Manager $moduleManager -+ * @param \Magento\Framework\Module\ModuleManagerInterface $moduleManager - * @param \Magento\Framework\Registry $registry - * @param GroupManagementInterface $groupManagement - * @param \Magento\Framework\Api\SearchCriteriaBuilder $searchCriteriaBuilder -@@ -92,7 +92,7 @@ abstract class AbstractGroup extends Widget implements RendererInterface - \Magento\Backend\Block\Template\Context $context, - GroupRepositoryInterface $groupRepository, - \Magento\Directory\Helper\Data $directoryHelper, -- \Magento\Framework\Module\Manager $moduleManager, -+ \Magento\Framework\Module\ModuleManagerInterface $moduleManager, - \Magento\Framework\Registry $registry, - GroupManagementInterface $groupManagement, - \Magento\Framework\Api\SearchCriteriaBuilder $searchCriteriaBuilder, -diff --git a/app/code/Magento/Catalog/Block/Adminhtml/Product/Edit/Tab/Related.php b/app/code/Magento/Catalog/Block/Adminhtml/Product/Edit/Tab/Related.php -index 0c59a7402da..23b927598e8 100644 ---- a/app/code/Magento/Catalog/Block/Adminhtml/Product/Edit/Tab/Related.php -+++ b/app/code/Magento/Catalog/Block/Adminhtml/Product/Edit/Tab/Related.php -@@ -9,8 +9,12 @@ use Magento\Backend\Block\Widget\Grid\Column; - use Magento\Backend\Block\Widget\Grid\Extended; - - /** -+ * Related product edit tab -+ * - * @api - * @since 100.0.2 -+ * @deprecated Not used since related products grid moved to UI components. -+ * @see \Magento\Catalog\Ui\DataProvider\Product\Related\RelatedDataProvider - */ - class Related extends Extended - { -@@ -318,7 +322,7 @@ class Related extends Extended - } - - /** -- * Rerieve grid URL -+ * Retrieve grid URL - * - * @return string - */ -diff --git a/app/code/Magento/Catalog/Block/Adminhtml/Product/Edit/Tab/Upsell.php b/app/code/Magento/Catalog/Block/Adminhtml/Product/Edit/Tab/Upsell.php -index 323b1785bc9..41ad72ca39e 100644 ---- a/app/code/Magento/Catalog/Block/Adminhtml/Product/Edit/Tab/Upsell.php -+++ b/app/code/Magento/Catalog/Block/Adminhtml/Product/Edit/Tab/Upsell.php -@@ -6,8 +6,12 @@ - namespace Magento\Catalog\Block\Adminhtml\Product\Edit\Tab; - - /** -+ * Upsell product edit tab -+ * - * @api - * @since 100.0.2 -+ * @deprecated Not used since upsell products grid moved to UI components. -+ * @see \Magento\Catalog\Ui\DataProvider\Product\Related\CrossSellDataProvider - */ - class Upsell extends \Magento\Backend\Block\Widget\Grid\Extended - { -diff --git a/app/code/Magento/Catalog/Block/Adminhtml/Product/Edit/Tabs.php b/app/code/Magento/Catalog/Block/Adminhtml/Product/Edit/Tabs.php -index 04c3a208b97..51c326763b0 100644 ---- a/app/code/Magento/Catalog/Block/Adminhtml/Product/Edit/Tabs.php -+++ b/app/code/Magento/Catalog/Block/Adminhtml/Product/Edit/Tabs.php -@@ -8,13 +8,13 @@ namespace Magento\Catalog\Block\Adminhtml\Product\Edit; - - use Magento\Backend\Block\Template\Context; - use Magento\Backend\Block\Widget\Accordion; --use Magento\Backend\Block\Widget\Tabs as WigetTabs; -+use Magento\Backend\Block\Widget\Tabs as WidgetTabs; - use Magento\Backend\Model\Auth\Session; - use Magento\Catalog\Helper\Catalog; - use Magento\Catalog\Helper\Data; - use Magento\Eav\Model\ResourceModel\Entity\Attribute\Group\CollectionFactory; - use Magento\Framework\Json\EncoderInterface; --use Magento\Framework\Module\Manager; -+use Magento\Framework\Module\ModuleManagerInterface; - use Magento\Framework\Registry; - use Magento\Framework\Translate\InlineInterface; - -@@ -22,7 +22,7 @@ use Magento\Framework\Translate\InlineInterface; - * Admin product edit tabs - * @SuppressWarnings(PHPMD.CouplingBetweenObjects) - */ --class Tabs extends WigetTabs -+class Tabs extends WidgetTabs - { - const BASIC_TAB_GROUP_CODE = 'basic'; - -@@ -65,7 +65,7 @@ class Tabs extends WigetTabs - protected $_collectionFactory; - - /** -- * @var Manager -+ * @var ModuleManagerInterface - */ - protected $_moduleManager; - -@@ -78,7 +78,7 @@ class Tabs extends WigetTabs - * @param Context $context - * @param EncoderInterface $jsonEncoder - * @param Session $authSession -- * @param Manager $moduleManager -+ * @param ModuleManagerInterface $moduleManager - * @param CollectionFactory $collectionFactory - * @param Catalog $helperCatalog - * @param Data $catalogData -@@ -91,7 +91,7 @@ class Tabs extends WigetTabs - Context $context, - EncoderInterface $jsonEncoder, - Session $authSession, -- Manager $moduleManager, -+ ModuleManagerInterface $moduleManager, - CollectionFactory $collectionFactory, - Catalog $helperCatalog, - Data $catalogData, -@@ -109,7 +109,7 @@ class Tabs extends WigetTabs - } - - /** -- * @return void -+ * @inheritdoc - */ - protected function _construct() - { -@@ -119,6 +119,8 @@ class Tabs extends WigetTabs - } - - /** -+ * Get group collection. -+ * - * @param int $attributeSetId - * @return \Magento\Eav\Model\ResourceModel\Entity\Attribute\Group\Collection - */ -@@ -131,10 +133,11 @@ class Tabs extends WigetTabs - } - - /** -- * @return $this -+ * @inheritdoc - * @SuppressWarnings(PHPMD.CyclomaticComplexity) -- * @SuppressWarnings(PHPMD.NPathComplexity) - * @SuppressWarnings(PHPMD.ExcessiveMethodLength) -+ * @SuppressWarnings(PHPMD.NPathComplexity) -+ * @SuppressWarnings(PHPMD.RequestAwareBlockMethod) - */ - protected function _prepareLayout() - { -@@ -315,6 +318,8 @@ class Tabs extends WigetTabs - } - - /** -+ * Set attribute tab block. -+ * - * @param string $attributeTabBlock - * @return $this - */ -@@ -337,6 +342,8 @@ class Tabs extends WigetTabs - } - - /** -+ * Get accordion. -+ * - * @param string $parentTab - * @return string - */ -diff --git a/app/code/Magento/Catalog/Block/Adminhtml/Product/Frontend/Product/Watermark.php b/app/code/Magento/Catalog/Block/Adminhtml/Product/Frontend/Product/Watermark.php -index 1f74969c3d1..7f80aece60e 100644 ---- a/app/code/Magento/Catalog/Block/Adminhtml/Product/Frontend/Product/Watermark.php -+++ b/app/code/Magento/Catalog/Block/Adminhtml/Product/Frontend/Product/Watermark.php -@@ -4,15 +4,15 @@ - * See COPYING.txt for license details. - */ - -+namespace Magento\Catalog\Block\Adminhtml\Product\Frontend\Product; -+ -+use Magento\Framework\Data\Form\Element\AbstractElement; -+ - /** - * Fieldset config form element renderer - * - * @author Magento Core Team <core@magentocommerce.com> - */ --namespace Magento\Catalog\Block\Adminhtml\Product\Frontend\Product; -- --use Magento\Framework\Data\Form\Element\AbstractElement; -- - class Watermark extends \Magento\Backend\Block\AbstractBlock implements - \Magento\Framework\Data\Form\Element\Renderer\RendererInterface - { -@@ -60,6 +60,8 @@ class Watermark extends \Magento\Backend\Block\AbstractBlock implements - } - - /** -+ * Render form element as HTML -+ * - * @param AbstractElement $element - * @return string - */ -@@ -124,13 +126,14 @@ class Watermark extends \Magento\Backend\Block\AbstractBlock implements - } - - /** -+ * Get header html for render -+ * - * @param AbstractElement $element - * @return string - * @SuppressWarnings(PHPMD.UnusedLocalVariable) - */ - protected function _getHeaderHtml($element) - { -- $id = $element->getHtmlId(); - $default = !$this->getRequest()->getParam('website') && !$this->getRequest()->getParam('store'); - - $html = '<h4 class="icon-head head-edit-form">' . $element->getLegend() . '</h4>'; -@@ -148,6 +151,8 @@ class Watermark extends \Magento\Backend\Block\AbstractBlock implements - } - - /** -+ * Get footer html for render -+ * - * @param AbstractElement $element - * @return string - * @SuppressWarnings(PHPMD.UnusedFormalParameter) -diff --git a/app/code/Magento/Catalog/Block/Adminhtml/Product/Grid.php b/app/code/Magento/Catalog/Block/Adminhtml/Product/Grid.php -index 8f3a0793cc4..7e43f2fc064 100644 ---- a/app/code/Magento/Catalog/Block/Adminhtml/Product/Grid.php -+++ b/app/code/Magento/Catalog/Block/Adminhtml/Product/Grid.php -@@ -8,13 +8,15 @@ namespace Magento\Catalog\Block\Adminhtml\Product; - use Magento\Store\Model\Store; - - /** -+ * Catalog product grid -+ * - * @api - * @since 100.0.2 - */ - class Grid extends \Magento\Backend\Block\Widget\Grid\Extended - { - /** -- * @var \Magento\Framework\Module\Manager -+ * @var \Magento\Framework\Module\ModuleManagerInterface - */ - protected $moduleManager; - -@@ -57,7 +59,7 @@ class Grid extends \Magento\Backend\Block\Widget\Grid\Extended - * @param \Magento\Catalog\Model\Product\Type $type - * @param \Magento\Catalog\Model\Product\Attribute\Source\Status $status - * @param \Magento\Catalog\Model\Product\Visibility $visibility -- * @param \Magento\Framework\Module\Manager $moduleManager -+ * @param \Magento\Framework\Module\ModuleManagerInterface $moduleManager - * @param array $data - * - * @SuppressWarnings(PHPMD.ExcessiveParameterList) -@@ -71,7 +73,7 @@ class Grid extends \Magento\Backend\Block\Widget\Grid\Extended - \Magento\Catalog\Model\Product\Type $type, - \Magento\Catalog\Model\Product\Attribute\Source\Status $status, - \Magento\Catalog\Model\Product\Visibility $visibility, -- \Magento\Framework\Module\Manager $moduleManager, -+ \Magento\Framework\Module\ModuleManagerInterface $moduleManager, - array $data = [] - ) { - $this->_websiteFactory = $websiteFactory; -@@ -85,7 +87,7 @@ class Grid extends \Magento\Backend\Block\Widget\Grid\Extended - } - - /** -- * @return void -+ * @inheritDoc - */ - protected function _construct() - { -@@ -99,7 +101,10 @@ class Grid extends \Magento\Backend\Block\Widget\Grid\Extended - } - - /** -+ * Get store. -+ * - * @return Store -+ * @throws \Magento\Framework\Exception\NoSuchEntityException - */ - protected function _getStore() - { -@@ -108,7 +113,7 @@ class Grid extends \Magento\Backend\Block\Widget\Grid\Extended - } - - /** -- * @return $this -+ * @inheritDoc - */ - protected function _prepareCollection() - { -@@ -136,7 +141,6 @@ class Grid extends \Magento\Backend\Block\Widget\Grid\Extended - ); - } - if ($store->getId()) { -- //$collection->setStoreId($store->getId()); - $collection->addStoreFilter($store); - $collection->joinAttribute( - 'name', -@@ -187,8 +191,7 @@ class Grid extends \Magento\Backend\Block\Widget\Grid\Extended - } - - /** -- * @param \Magento\Backend\Block\Widget\Grid\Column $column -- * @return $this -+ * @inheritDoc - */ - protected function _addColumnFilterToCollection($column) - { -@@ -208,8 +211,9 @@ class Grid extends \Magento\Backend\Block\Widget\Grid\Extended - } - - /** -- * @return $this -+ * @inheritDoc - * @SuppressWarnings(PHPMD.ExcessiveMethodLength) -+ * @throws \Exception - */ - protected function _prepareColumns() - { -@@ -364,16 +368,11 @@ class Grid extends \Magento\Backend\Block\Widget\Grid\Extended - ] - ); - -- $block = $this->getLayout()->getBlock('grid.bottom.links'); -- if ($block) { -- $this->setChild('grid.bottom.links', $block); -- } -- - return parent::_prepareColumns(); - } - - /** -- * @return $this -+ * @inheritDoc - */ - protected function _prepareMassaction() - { -@@ -425,7 +424,7 @@ class Grid extends \Magento\Backend\Block\Widget\Grid\Extended - } - - /** -- * @return string -+ * @inheritDoc - */ - public function getGridUrl() - { -@@ -433,8 +432,7 @@ class Grid extends \Magento\Backend\Block\Widget\Grid\Extended - } - - /** -- * @param \Magento\Catalog\Model\Product|\Magento\Framework\DataObject $row -- * @return string -+ * @inheritDoc - */ - public function getRowUrl($row) - { -diff --git a/app/code/Magento/Catalog/Block/Adminhtml/Product/Helper/Form/Gallery.php b/app/code/Magento/Catalog/Block/Adminhtml/Product/Helper/Form/Gallery.php -index 0557a215383..36740f5853d 100644 ---- a/app/code/Magento/Catalog/Block/Adminhtml/Product/Helper/Form/Gallery.php -+++ b/app/code/Magento/Catalog/Block/Adminhtml/Product/Helper/Form/Gallery.php -@@ -11,11 +11,16 @@ - */ - namespace Magento\Catalog\Block\Adminhtml\Product\Helper\Form; - -+use Magento\Framework\App\ObjectManager; -+use Magento\Framework\App\Request\DataPersistorInterface; - use Magento\Framework\Registry; - use Magento\Catalog\Model\Product; - use Magento\Eav\Model\Entity\Attribute; - use Magento\Catalog\Api\Data\ProductInterface; - -+/** -+ * Adminhtml gallery block -+ */ - class Gallery extends \Magento\Framework\View\Element\AbstractBlock - { - /** -@@ -66,27 +71,37 @@ class Gallery extends \Magento\Framework\View\Element\AbstractBlock - */ - protected $registry; - -+ /** -+ * @var DataPersistorInterface -+ */ -+ private $dataPersistor; -+ - /** - * @param \Magento\Framework\View\Element\Context $context - * @param \Magento\Store\Model\StoreManagerInterface $storeManager - * @param Registry $registry - * @param \Magento\Framework\Data\Form $form - * @param array $data -+ * @param DataPersistorInterface|null $dataPersistor - */ - public function __construct( - \Magento\Framework\View\Element\Context $context, - \Magento\Store\Model\StoreManagerInterface $storeManager, - Registry $registry, - \Magento\Framework\Data\Form $form, -- $data = [] -+ $data = [], -+ DataPersistorInterface $dataPersistor = null - ) { - $this->storeManager = $storeManager; - $this->registry = $registry; - $this->form = $form; -+ $this->dataPersistor = $dataPersistor ?: ObjectManager::getInstance()->get(DataPersistorInterface::class); - parent::__construct($context, $data); - } - - /** -+ * Returns element html. -+ * - * @return string - */ - public function getElementHtml() -@@ -102,7 +117,24 @@ class Gallery extends \Magento\Framework\View\Element\AbstractBlock - */ - public function getImages() - { -- return $this->getDataObject()->getData('media_gallery') ?: null; -+ $images = $this->getDataObject()->getData('media_gallery') ?: null; -+ if ($images === null) { -+ $images = ((array)$this->dataPersistor->get('catalog_product'))['product']['media_gallery'] ?? null; -+ } -+ -+ return $images; -+ } -+ -+ /** -+ * Get value for given type. -+ * -+ * @param string $type -+ * @return string|null -+ */ -+ public function getImageValue(string $type) -+ { -+ $product = (array)$this->dataPersistor->get('catalog_product'); -+ return $product['product'][$type] ?? null; - } - - /** -@@ -117,11 +149,13 @@ class Gallery extends \Magento\Framework\View\Element\AbstractBlock - $content->setId($this->getHtmlId() . '_content')->setElement($this); - $content->setFormName($this->formName); - $galleryJs = $content->getJsObjectName(); -- $content->getUploader()->getConfig()->setMegiaGallery($galleryJs); -+ $content->getUploader()->getConfig()->setMediaGallery($galleryJs); - return $content->toHtml(); - } - - /** -+ * Returns html id -+ * - * @return string - */ - protected function getHtmlId() -@@ -130,6 +164,8 @@ class Gallery extends \Magento\Framework\View\Element\AbstractBlock - } - - /** -+ * Returns name -+ * - * @return string - */ - public function getName() -@@ -138,6 +174,8 @@ class Gallery extends \Magento\Framework\View\Element\AbstractBlock - } - - /** -+ * Returns suffix for field name -+ * - * @return string - */ - public function getFieldNameSuffix() -@@ -146,6 +184,8 @@ class Gallery extends \Magento\Framework\View\Element\AbstractBlock - } - - /** -+ * Returns data scope html id -+ * - * @return string - */ - public function getDataScopeHtmlId() -@@ -230,7 +270,6 @@ class Gallery extends \Magento\Framework\View\Element\AbstractBlock - /** - * Retrieve attribute field name - * -- * - * @param Attribute $attribute - * @return string - */ -@@ -244,6 +283,8 @@ class Gallery extends \Magento\Framework\View\Element\AbstractBlock - } - - /** -+ * Returns html content of the block -+ * - * @return string - */ - public function toHtml() -diff --git a/app/code/Magento/Catalog/Block/Adminhtml/Product/Helper/Form/Gallery/Content.php b/app/code/Magento/Catalog/Block/Adminhtml/Product/Helper/Form/Gallery/Content.php -index 8d51f78ff5b..f5d0ec7da61 100644 ---- a/app/code/Magento/Catalog/Block/Adminhtml/Product/Helper/Form/Gallery/Content.php -+++ b/app/code/Magento/Catalog/Block/Adminhtml/Product/Helper/Form/Gallery/Content.php -@@ -13,11 +13,17 @@ - */ - namespace Magento\Catalog\Block\Adminhtml\Product\Helper\Form\Gallery; - -+use Magento\Framework\App\ObjectManager; - use Magento\Backend\Block\Media\Uploader; - use Magento\Framework\View\Element\AbstractBlock; - use Magento\Framework\App\Filesystem\DirectoryList; - use Magento\Framework\Exception\FileSystemException; -+use Magento\Backend\Block\DataProviders\ImageUploadConfig as ImageUploadConfigDataProvider; -+use Magento\MediaStorage\Helper\File\Storage\Database; - -+/** -+ * Block for gallery content. -+ */ - class Content extends \Magento\Backend\Block\Widget - { - /** -@@ -40,29 +46,53 @@ class Content extends \Magento\Backend\Block\Widget - */ - private $imageHelper; - -+ /** -+ * @var ImageUploadConfigDataProvider -+ */ -+ private $imageUploadConfigDataProvider; -+ -+ /** -+ * @var Database -+ */ -+ private $fileStorageDatabase; -+ - /** - * @param \Magento\Backend\Block\Template\Context $context - * @param \Magento\Framework\Json\EncoderInterface $jsonEncoder - * @param \Magento\Catalog\Model\Product\Media\Config $mediaConfig - * @param array $data -+ * @param ImageUploadConfigDataProvider $imageUploadConfigDataProvider -+ * @param Database $fileStorageDatabase - */ - public function __construct( - \Magento\Backend\Block\Template\Context $context, - \Magento\Framework\Json\EncoderInterface $jsonEncoder, - \Magento\Catalog\Model\Product\Media\Config $mediaConfig, -- array $data = [] -+ array $data = [], -+ ImageUploadConfigDataProvider $imageUploadConfigDataProvider = null, -+ Database $fileStorageDatabase = null - ) { - $this->_jsonEncoder = $jsonEncoder; - $this->_mediaConfig = $mediaConfig; - parent::__construct($context, $data); -+ $this->imageUploadConfigDataProvider = $imageUploadConfigDataProvider -+ ?: ObjectManager::getInstance()->get(ImageUploadConfigDataProvider::class); -+ $this->fileStorageDatabase = $fileStorageDatabase -+ ?: ObjectManager::getInstance()->get(Database::class); - } - - /** -+ * Prepare layout. -+ * - * @return AbstractBlock - */ - protected function _prepareLayout() - { -- $this->addChild('uploader', \Magento\Backend\Block\Media\Uploader::class); -+ $this->addChild( -+ 'uploader', -+ \Magento\Backend\Block\Media\Uploader::class, -+ ['image_upload_config_data' => $this->imageUploadConfigDataProvider] -+ ); - - $this->getUploader()->getConfig()->setUrl( - $this->_urlBuilder->addSessionParam()->getUrl('catalog/product_gallery/upload') -@@ -103,6 +133,8 @@ class Content extends \Magento\Backend\Block\Widget - } - - /** -+ * Returns js object name -+ * - * @return string - */ - public function getJsObjectName() -@@ -111,6 +143,8 @@ class Content extends \Magento\Backend\Block\Widget - } - - /** -+ * Returns buttons for add image action. -+ * - * @return string - */ - public function getAddImagesButton() -@@ -124,6 +158,8 @@ class Content extends \Magento\Backend\Block\Widget - } - - /** -+ * Returns image json -+ * - * @return string - */ - public function getImagesJson() -@@ -138,6 +174,13 @@ class Content extends \Magento\Backend\Block\Widget - $images = $this->sortImagesByPosition($value['images']); - foreach ($images as &$image) { - $image['url'] = $this->_mediaConfig->getMediaUrl($image['file']); -+ if ($this->fileStorageDatabase->checkDbUsage() && -+ !$mediaDir->isFile($this->_mediaConfig->getMediaPath($image['file'])) -+ ) { -+ $this->fileStorageDatabase->saveFileToFilesystem( -+ $this->_mediaConfig->getMediaPath($image['file']) -+ ); -+ } - try { - $fileHandler = $mediaDir->stat($this->_mediaConfig->getMediaPath($image['file'])); - $image['size'] = $fileHandler['size']; -@@ -161,14 +204,19 @@ class Content extends \Magento\Backend\Block\Widget - private function sortImagesByPosition($images) - { - if (is_array($images)) { -- usort($images, function ($imageA, $imageB) { -- return ($imageA['position'] < $imageB['position']) ? -1 : 1; -- }); -+ usort( -+ $images, -+ function ($imageA, $imageB) { -+ return ($imageA['position'] < $imageB['position']) ? -1 : 1; -+ } -+ ); - } - return $images; - } - - /** -+ * Returns image values json -+ * - * @return string - */ - public function getImagesValuesJson() -@@ -193,9 +241,11 @@ class Content extends \Magento\Backend\Block\Widget - $imageTypes = []; - foreach ($this->getMediaAttributes() as $attribute) { - /* @var $attribute \Magento\Eav\Model\Entity\Attribute */ -+ $value = $this->getElement()->getDataObject()->getData($attribute->getAttributeCode()) -+ ?: $this->getElement()->getImageValue($attribute->getAttributeCode()); - $imageTypes[$attribute->getAttributeCode()] = [ - 'code' => $attribute->getAttributeCode(), -- 'value' => $this->getElement()->getDataObject()->getData($attribute->getAttributeCode()), -+ 'value' => $value, - 'label' => $attribute->getFrontend()->getLabel(), - 'scope' => __($this->getElement()->getScopeLabel($attribute)), - 'name' => $this->getElement()->getAttributeFieldName($attribute), -@@ -241,6 +291,8 @@ class Content extends \Magento\Backend\Block\Widget - } - - /** -+ * Returns image helper object. -+ * - * @return \Magento\Catalog\Helper\Image - * @deprecated 101.0.3 - */ -diff --git a/app/code/Magento/Catalog/Block/Adminhtml/Rss/NotifyStock.php b/app/code/Magento/Catalog/Block/Adminhtml/Rss/NotifyStock.php -index ac1fd8c692e..c296a5aa0db 100644 ---- a/app/code/Magento/Catalog/Block/Adminhtml/Rss/NotifyStock.php -+++ b/app/code/Magento/Catalog/Block/Adminhtml/Rss/NotifyStock.php -@@ -9,6 +9,7 @@ use Magento\Framework\App\Rss\DataProviderInterface; - - /** - * Class NotifyStock -+ * - * @package Magento\Catalog\Block\Adminhtml\Rss - */ - class NotifyStock extends \Magento\Backend\Block\AbstractBlock implements DataProviderInterface -@@ -41,7 +42,7 @@ class NotifyStock extends \Magento\Backend\Block\AbstractBlock implements DataPr - } - - /** -- * @return void -+ * @inheritdoc - */ - protected function _construct() - { -@@ -50,12 +51,12 @@ class NotifyStock extends \Magento\Backend\Block\AbstractBlock implements DataPr - } - - /** -- * {@inheritdoc} -+ * @inheritdoc - */ - public function getRssData() - { -- $newUrl = $this->rssUrlBuilder->getUrl(['_secure' => true, '_nosecret' => true, 'type' => 'notifystock']); -- $title = __('Low Stock Products'); -+ $newUrl = $this->rssUrlBuilder->getUrl(['_secure' => true, '_nosecret' => true, 'type' => 'notifystock']); -+ $title = __('Low Stock Products')->render(); - $data = ['title' => $title, 'description' => $title, 'link' => $newUrl, 'charset' => 'UTF-8']; - - foreach ($this->rssModel->getProductsCollection() as $item) { -@@ -65,7 +66,7 @@ class NotifyStock extends \Magento\Backend\Block\AbstractBlock implements DataPr - ['id' => $item->getId(), '_secure' => true, '_nosecret' => true] - ); - $qty = 1 * $item->getQty(); -- $description = __('%1 has reached a quantity of %2.', $item->getName(), $qty); -+ $description = __('%1 has reached a quantity of %2.', $item->getName(), $qty)->render(); - $data['entries'][] = ['title' => $item->getName(), 'link' => $url, 'description' => $description]; - } - -@@ -73,7 +74,7 @@ class NotifyStock extends \Magento\Backend\Block\AbstractBlock implements DataPr - } - - /** -- * {@inheritdoc} -+ * @inheritdoc - */ - public function getCacheLifetime() - { -@@ -81,7 +82,7 @@ class NotifyStock extends \Magento\Backend\Block\AbstractBlock implements DataPr - } - - /** -- * {@inheritdoc} -+ * @inheritdoc - */ - public function isAllowed() - { -@@ -89,7 +90,7 @@ class NotifyStock extends \Magento\Backend\Block\AbstractBlock implements DataPr - } - - /** -- * {@inheritdoc} -+ * @inheritdoc - */ - public function getFeeds() - { -@@ -97,7 +98,7 @@ class NotifyStock extends \Magento\Backend\Block\AbstractBlock implements DataPr - } - - /** -- * {@inheritdoc} -+ * @inheritdoc - */ - public function isAuthRequired() - { -diff --git a/app/code/Magento/Catalog/Block/Product/AbstractProduct.php b/app/code/Magento/Catalog/Block/Product/AbstractProduct.php -index 4102c82a0a3..c8da0f70f73 100644 ---- a/app/code/Magento/Catalog/Block/Product/AbstractProduct.php -+++ b/app/code/Magento/Catalog/Block/Product/AbstractProduct.php -@@ -125,6 +125,7 @@ class AbstractProduct extends \Magento\Framework\View\Element\Template - - /** - * Retrieve url for add product to cart -+ * - * Will return product view page URL if product has required options - * - * @param \Magento\Catalog\Model\Product $product -@@ -473,7 +474,9 @@ class AbstractProduct extends \Magento\Framework\View\Element\Template - } - - /** -- * @param null $type -+ * Get the renderer that will be used to render the details block -+ * -+ * @param string|null $type - * @return bool|\Magento\Framework\View\Element\AbstractBlock - */ - public function getDetailsRenderer($type = null) -@@ -489,6 +492,8 @@ class AbstractProduct extends \Magento\Framework\View\Element\Template - } - - /** -+ * Return the list of details -+ * - * @return \Magento\Framework\View\Element\RendererList - */ - protected function getDetailsRendererList() -diff --git a/app/code/Magento/Catalog/Block/Product/Compare/ListCompare.php b/app/code/Magento/Catalog/Block/Product/Compare/ListCompare.php -index 6c54aa4e171..76f5dbd1bea 100644 ---- a/app/code/Magento/Catalog/Block/Product/Compare/ListCompare.php -+++ b/app/code/Magento/Catalog/Block/Product/Compare/ListCompare.php -@@ -122,12 +122,7 @@ class ListCompare extends \Magento\Catalog\Block\Product\AbstractProduct - */ - public function getAddToWishlistParams($product) - { -- $continueUrl = $this->urlEncoder->encode($this->getUrl('customer/account')); -- $urlParamName = Action::PARAM_NAME_URL_ENCODED; -- -- $continueUrlParams = [$urlParamName => $continueUrl]; -- -- return $this->_wishlistHelper->getAddParams($product, $continueUrlParams); -+ return $this->_wishlistHelper->getAddParams($product); - } - - /** -diff --git a/app/code/Magento/Catalog/Block/Product/Gallery.php b/app/code/Magento/Catalog/Block/Product/Gallery.php -index e7c7b81ec29..54f848a92e9 100644 ---- a/app/code/Magento/Catalog/Block/Product/Gallery.php -+++ b/app/code/Magento/Catalog/Block/Product/Gallery.php -@@ -16,6 +16,8 @@ use Magento\Framework\App\Filesystem\DirectoryList; - use Magento\Framework\Data\Collection; - - /** -+ * Product gallery block -+ * - * @api - * @since 100.0.2 - */ -@@ -43,6 +45,8 @@ class Gallery extends \Magento\Framework\View\Element\Template - } - - /** -+ * Prepare layout -+ * - * @return $this - */ - protected function _prepareLayout() -@@ -52,6 +56,8 @@ class Gallery extends \Magento\Framework\View\Element\Template - } - - /** -+ * Get product -+ * - * @return Product - */ - public function getProduct() -@@ -60,6 +66,8 @@ class Gallery extends \Magento\Framework\View\Element\Template - } - - /** -+ * Get gallery collection -+ * - * @return Collection - */ - public function getGalleryCollection() -@@ -68,6 +76,8 @@ class Gallery extends \Magento\Framework\View\Element\Template - } - - /** -+ * Get current image -+ * - * @return Image|null - */ - public function getCurrentImage() -@@ -85,6 +95,8 @@ class Gallery extends \Magento\Framework\View\Element\Template - } - - /** -+ * Get image url -+ * - * @return string - */ - public function getImageUrl() -@@ -93,6 +105,8 @@ class Gallery extends \Magento\Framework\View\Element\Template - } - - /** -+ * Get image file -+ * - * @return mixed - */ - public function getImageFile() -@@ -115,7 +129,7 @@ class Gallery extends \Magento\Framework\View\Element\Template - if ($size[0] > 600) { - return 600; - } else { -- return $size[0]; -+ return (int) $size[0]; - } - } - } -@@ -124,6 +138,8 @@ class Gallery extends \Magento\Framework\View\Element\Template - } - - /** -+ * Get previous image -+ * - * @return Image|false - */ - public function getPreviousImage() -@@ -143,6 +159,8 @@ class Gallery extends \Magento\Framework\View\Element\Template - } - - /** -+ * Get next image -+ * - * @return Image|false - */ - public function getNextImage() -@@ -166,6 +184,8 @@ class Gallery extends \Magento\Framework\View\Element\Template - } - - /** -+ * Get previous image url -+ * - * @return false|string - */ - public function getPreviousImageUrl() -@@ -178,6 +198,8 @@ class Gallery extends \Magento\Framework\View\Element\Template - } - - /** -+ * Get next image url -+ * - * @return false|string - */ - public function getNextImageUrl() -diff --git a/app/code/Magento/Catalog/Block/Product/Image.php b/app/code/Magento/Catalog/Block/Product/Image.php -index 20a556ab414..7a7f9c0affc 100644 ---- a/app/code/Magento/Catalog/Block/Product/Image.php -+++ b/app/code/Magento/Catalog/Block/Product/Image.php -@@ -6,6 +6,8 @@ - namespace Magento\Catalog\Block\Product; - - /** -+ * Product image block -+ * - * @api - * @method string getImageUrl() - * @method string getWidth() -@@ -13,6 +15,7 @@ namespace Magento\Catalog\Block\Product; - * @method string getLabel() - * @method float getRatio() - * @method string getCustomAttributes() -+ * @method string getClass() - * @since 100.0.2 - */ - class Image extends \Magento\Framework\View\Element\Template -diff --git a/app/code/Magento/Catalog/Block/Product/ImageFactory.php b/app/code/Magento/Catalog/Block/Product/ImageFactory.php -index f9a576367dd..aa303af656a 100644 ---- a/app/code/Magento/Catalog/Block/Product/ImageFactory.php -+++ b/app/code/Magento/Catalog/Block/Product/ImageFactory.php -@@ -77,16 +77,29 @@ class ImageFactory - { - $result = []; - foreach ($attributes as $name => $value) { -- $result[] = $name . '="' . $value . '"'; -+ if ($name != 'class') { -+ $result[] = $name . '="' . $value . '"'; -+ } - } - return !empty($result) ? implode(' ', $result) : ''; - } - -+ /** -+ * Retrieve image class for HTML element -+ * -+ * @param array $attributes -+ * @return string -+ */ -+ private function getClass(array $attributes): string -+ { -+ return $attributes['class'] ?? 'product-image-photo'; -+ } -+ - /** - * Calculate image ratio - * -- * @param $width -- * @param $height -+ * @param int $width -+ * @param int $height - * @return float - */ - private function getRatio(int $width, int $height): float -@@ -98,8 +111,9 @@ class ImageFactory - } - - /** -- * @param Product $product -+ * Get image label - * -+ * @param Product $product - * @param string $imageType - * @return string - */ -@@ -114,6 +128,7 @@ class ImageFactory - - /** - * Create image block from product -+ * - * @param Product $product - * @param string $imageId - * @param array|null $attributes -@@ -154,6 +169,7 @@ class ImageFactory - 'label' => $this->getLabel($product, $imageMiscParams['image_type']), - 'ratio' => $this->getRatio($imageMiscParams['image_width'], $imageMiscParams['image_height']), - 'custom_attributes' => $this->getStringCustomAttributes($attributes), -+ 'class' => $this->getClass($attributes), - 'product_id' => $product->getId() - ], - ]; -diff --git a/app/code/Magento/Catalog/Block/Product/ListProduct.php b/app/code/Magento/Catalog/Block/Product/ListProduct.php -index c1b255c762d..144cb682e2d 100644 ---- a/app/code/Magento/Catalog/Block/Product/ListProduct.php -+++ b/app/code/Magento/Catalog/Block/Product/ListProduct.php -@@ -178,8 +178,9 @@ class ListProduct extends AbstractProduct implements IdentityInterface - } - - /** -- * Need use as _prepareLayout - but problem in declaring collection from -- * another block (was problem with search result) -+ * Need use as _prepareLayout - but problem in declaring collection from another block. -+ * (was problem with search result) -+ * - * @return $this - */ - protected function _beforeToHtml() -@@ -188,7 +189,9 @@ class ListProduct extends AbstractProduct implements IdentityInterface - - $this->addToolbarBlock($collection); - -- $collection->load(); -+ if (!$collection->isLoaded()) { -+ $collection->load(); -+ } - - return parent::_beforeToHtml(); - } -@@ -262,6 +265,8 @@ class ListProduct extends AbstractProduct implements IdentityInterface - } - - /** -+ * Set collection. -+ * - * @param AbstractCollection $collection - * @return $this - */ -@@ -272,7 +277,9 @@ class ListProduct extends AbstractProduct implements IdentityInterface - } - - /** -- * @param array|string|integer| Element $code -+ * Add attribute. -+ * -+ * @param array|string|integer|Element $code - * @return $this - */ - public function addAttribute($code) -@@ -282,6 +289,8 @@ class ListProduct extends AbstractProduct implements IdentityInterface - } - - /** -+ * Get price block template. -+ * - * @return mixed - */ - public function getPriceBlockTemplate() -@@ -364,13 +373,15 @@ class ListProduct extends AbstractProduct implements IdentityInterface - return [ - 'action' => $url, - 'data' => [ -- 'product' => $product->getEntityId(), -+ 'product' => (int) $product->getEntityId(), - ActionInterface::PARAM_NAME_URL_ENCODED => $this->urlHelper->getEncodedUrl($url), - ] - ]; - } - - /** -+ * Get product price. -+ * - * @param Product $product - * @return string - */ -@@ -396,8 +407,8 @@ class ListProduct extends AbstractProduct implements IdentityInterface - } - - /** -- * Specifies that price rendering should be done for the list of products -- * i.e. rendering happens in the scope of product list, but not single product -+ * Specifies that price rendering should be done for the list of products. -+ * (rendering happens in the scope of product list, but not single product) - * - * @return Render - */ -diff --git a/app/code/Magento/Catalog/Block/Product/ProductList/Crosssell.php b/app/code/Magento/Catalog/Block/Product/ProductList/Crosssell.php -index 0c547f81c85..596cd7cc5bd 100644 ---- a/app/code/Magento/Catalog/Block/Product/ProductList/Crosssell.php -+++ b/app/code/Magento/Catalog/Block/Product/ProductList/Crosssell.php -@@ -9,6 +9,9 @@ - */ - namespace Magento\Catalog\Block\Product\ProductList; - -+/** -+ * Crosssell block for product -+ */ - class Crosssell extends \Magento\Catalog\Block\Product\AbstractProduct - { - /** -@@ -25,7 +28,7 @@ class Crosssell extends \Magento\Catalog\Block\Product\AbstractProduct - */ - protected function _prepareData() - { -- $product = $this->_coreRegistry->registry('product'); -+ $product = $this->getProduct(); - /* @var $product \Magento\Catalog\Model\Product */ - - $this->_itemCollection = $product->getCrossSellProductCollection()->addAttributeToSelect( -@@ -43,6 +46,7 @@ class Crosssell extends \Magento\Catalog\Block\Product\AbstractProduct - - /** - * Before rendering html process -+ * - * Prepare items collection - * - * @return \Magento\Catalog\Block\Product\ProductList\Crosssell -diff --git a/app/code/Magento/Catalog/Block/Product/ProductList/Related.php b/app/code/Magento/Catalog/Block/Product/ProductList/Related.php -index 219922f9e46..08861951154 100644 ---- a/app/code/Magento/Catalog/Block/Product/ProductList/Related.php -+++ b/app/code/Magento/Catalog/Block/Product/ProductList/Related.php -@@ -46,7 +46,7 @@ class Related extends \Magento\Catalog\Block\Product\AbstractProduct implements - protected $_checkoutCart; - - /** -- * @var \Magento\Framework\Module\Manager -+ * @var \Magento\Framework\Module\ModuleManagerInterface - */ - protected $moduleManager; - -@@ -55,7 +55,7 @@ class Related extends \Magento\Catalog\Block\Product\AbstractProduct implements - * @param \Magento\Checkout\Model\ResourceModel\Cart $checkoutCart - * @param \Magento\Catalog\Model\Product\Visibility $catalogProductVisibility - * @param \Magento\Checkout\Model\Session $checkoutSession -- * @param \Magento\Framework\Module\Manager $moduleManager -+ * @param \Magento\Framework\Module\ModuleManagerInterface $moduleManager - * @param array $data - */ - public function __construct( -@@ -63,7 +63,7 @@ class Related extends \Magento\Catalog\Block\Product\AbstractProduct implements - \Magento\Checkout\Model\ResourceModel\Cart $checkoutCart, - \Magento\Catalog\Model\Product\Visibility $catalogProductVisibility, - \Magento\Checkout\Model\Session $checkoutSession, -- \Magento\Framework\Module\Manager $moduleManager, -+ \Magento\Framework\Module\ModuleManagerInterface $moduleManager, - array $data = [] - ) { - $this->_checkoutCart = $checkoutCart; -@@ -77,11 +77,13 @@ class Related extends \Magento\Catalog\Block\Product\AbstractProduct implements - } - - /** -+ * Prepare data -+ * - * @return $this - */ - protected function _prepareData() - { -- $product = $this->_coreRegistry->registry('product'); -+ $product = $this->getProduct(); - /* @var $product \Magento\Catalog\Model\Product */ - - $this->_itemCollection = $product->getRelatedProductCollection()->addAttributeToSelect( -@@ -103,6 +105,8 @@ class Related extends \Magento\Catalog\Block\Product\AbstractProduct implements - } - - /** -+ * Before to html handler -+ * - * @return $this - */ - protected function _beforeToHtml() -@@ -112,6 +116,8 @@ class Related extends \Magento\Catalog\Block\Product\AbstractProduct implements - } - - /** -+ * Get collection items -+ * - * @return Collection - */ - public function getItems() -diff --git a/app/code/Magento/Catalog/Block/Product/ProductList/Toolbar.php b/app/code/Magento/Catalog/Block/Product/ProductList/Toolbar.php -index 0b8d2d6c89e..c530ba4785a 100644 ---- a/app/code/Magento/Catalog/Block/Product/ProductList/Toolbar.php -+++ b/app/code/Magento/Catalog/Block/Product/ProductList/Toolbar.php -@@ -7,6 +7,8 @@ namespace Magento\Catalog\Block\Product\ProductList; - - use Magento\Catalog\Helper\Product\ProductList; - use Magento\Catalog\Model\Product\ProductList\Toolbar as ToolbarModel; -+use Magento\Catalog\Model\Product\ProductList\ToolbarMemorizer; -+use Magento\Framework\App\ObjectManager; - - /** - * Product list toolbar -@@ -77,6 +79,7 @@ class Toolbar extends \Magento\Framework\View\Element\Template - - /** - * @var bool $_paramsMemorizeAllowed -+ * @deprecated - */ - protected $_paramsMemorizeAllowed = true; - -@@ -96,6 +99,7 @@ class Toolbar extends \Magento\Framework\View\Element\Template - * Catalog session - * - * @var \Magento\Catalog\Model\Session -+ * @deprecated - */ - protected $_catalogSession; - -@@ -104,6 +108,11 @@ class Toolbar extends \Magento\Framework\View\Element\Template - */ - protected $_toolbarModel; - -+ /** -+ * @var ToolbarMemorizer -+ */ -+ private $toolbarMemorizer; -+ - /** - * @var ProductList - */ -@@ -119,6 +128,16 @@ class Toolbar extends \Magento\Framework\View\Element\Template - */ - protected $_postDataHelper; - -+ /** -+ * @var \Magento\Framework\App\Http\Context -+ */ -+ private $httpContext; -+ -+ /** -+ * @var \Magento\Framework\Data\Form\FormKey -+ */ -+ private $formKey; -+ - /** - * @param \Magento\Framework\View\Element\Template\Context $context - * @param \Magento\Catalog\Model\Session $catalogSession -@@ -128,6 +147,11 @@ class Toolbar extends \Magento\Framework\View\Element\Template - * @param ProductList $productListHelper - * @param \Magento\Framework\Data\Helper\PostHelper $postDataHelper - * @param array $data -+ * @param ToolbarMemorizer|null $toolbarMemorizer -+ * @param \Magento\Framework\App\Http\Context|null $httpContext -+ * @param \Magento\Framework\Data\Form\FormKey|null $formKey -+ * -+ * @SuppressWarnings(PHPMD.ExcessiveParameterList) - */ - public function __construct( - \Magento\Framework\View\Element\Template\Context $context, -@@ -137,7 +161,10 @@ class Toolbar extends \Magento\Framework\View\Element\Template - \Magento\Framework\Url\EncoderInterface $urlEncoder, - ProductList $productListHelper, - \Magento\Framework\Data\Helper\PostHelper $postDataHelper, -- array $data = [] -+ array $data = [], -+ ToolbarMemorizer $toolbarMemorizer = null, -+ \Magento\Framework\App\Http\Context $httpContext = null, -+ \Magento\Framework\Data\Form\FormKey $formKey = null - ) { - $this->_catalogSession = $catalogSession; - $this->_catalogConfig = $catalogConfig; -@@ -145,6 +172,15 @@ class Toolbar extends \Magento\Framework\View\Element\Template - $this->urlEncoder = $urlEncoder; - $this->_productListHelper = $productListHelper; - $this->_postDataHelper = $postDataHelper; -+ $this->toolbarMemorizer = $toolbarMemorizer ?: ObjectManager::getInstance()->get( -+ ToolbarMemorizer::class -+ ); -+ $this->httpContext = $httpContext ?: ObjectManager::getInstance()->get( -+ \Magento\Framework\App\Http\Context::class -+ ); -+ $this->formKey = $formKey ?: ObjectManager::getInstance()->get( -+ \Magento\Framework\Data\Form\FormKey::class -+ ); - parent::__construct($context, $data); - } - -@@ -152,6 +188,7 @@ class Toolbar extends \Magento\Framework\View\Element\Template - * Disable list state params memorizing - * - * @return $this -+ * @deprecated - */ - public function disableParamsMemorizing() - { -@@ -165,6 +202,7 @@ class Toolbar extends \Magento\Framework\View\Element\Template - * @param string $param parameter name - * @param mixed $value parameter value - * @return $this -+ * @deprecated - */ - protected function _memorizeParam($param, $value) - { -@@ -244,13 +282,13 @@ class Toolbar extends \Magento\Framework\View\Element\Template - $defaultOrder = $keys[0]; - } - -- $order = $this->_toolbarModel->getOrder(); -+ $order = $this->toolbarMemorizer->getOrder(); - if (!$order || !isset($orders[$order])) { - $order = $defaultOrder; - } - -- if ($order != $defaultOrder) { -- $this->_memorizeParam('sort_order', $order); -+ if ($this->toolbarMemorizer->isMemorizingAllowed()) { -+ $this->httpContext->setValue(ToolbarModel::ORDER_PARAM_NAME, $order, $defaultOrder); - } - - $this->setData('_current_grid_order', $order); -@@ -270,13 +308,13 @@ class Toolbar extends \Magento\Framework\View\Element\Template - } - - $directions = ['asc', 'desc']; -- $dir = strtolower($this->_toolbarModel->getDirection()); -+ $dir = strtolower($this->toolbarMemorizer->getDirection()); - if (!$dir || !in_array($dir, $directions)) { - $dir = $this->_direction; - } - -- if ($dir != $this->_direction) { -- $this->_memorizeParam('sort_direction', $dir); -+ if ($this->toolbarMemorizer->isMemorizingAllowed()) { -+ $this->httpContext->setValue(ToolbarModel::DIRECTION_PARAM_NAME, $dir, $this->_direction); - } - - $this->setData('_current_grid_direction', $dir); -@@ -392,6 +430,8 @@ class Toolbar extends \Magento\Framework\View\Element\Template - } - - /** -+ * Get pager encoded url. -+ * - * @param array $params - * @return string - */ -@@ -412,11 +452,15 @@ class Toolbar extends \Magento\Framework\View\Element\Template - return $mode; - } - $defaultMode = $this->_productListHelper->getDefaultViewMode($this->getModes()); -- $mode = $this->_toolbarModel->getMode(); -+ $mode = $this->toolbarMemorizer->getMode(); - if (!$mode || !isset($this->_availableMode[$mode])) { - $mode = $defaultMode; - } - -+ if ($this->toolbarMemorizer->isMemorizingAllowed()) { -+ $this->httpContext->setValue(ToolbarModel::MODE_PARAM_NAME, $mode, $defaultMode); -+ } -+ - $this->setData('_current_grid_mode', $mode); - return $mode; - } -@@ -568,13 +612,13 @@ class Toolbar extends \Magento\Framework\View\Element\Template - $defaultLimit = $keys[0]; - } - -- $limit = $this->_toolbarModel->getLimit(); -+ $limit = $this->toolbarMemorizer->getLimit(); - if (!$limit || !isset($limits[$limit])) { - $limit = $defaultLimit; - } - -- if ($limit != $defaultLimit) { -- $this->_memorizeParam('limit_page', $limit); -+ if ($this->toolbarMemorizer->isMemorizingAllowed()) { -+ $this->httpContext->setValue(ToolbarModel::LIMIT_PARAM_NAME, $limit, $defaultLimit); - } - - $this->setData('_current_limit', $limit); -@@ -582,6 +626,8 @@ class Toolbar extends \Magento\Framework\View\Element\Template - } - - /** -+ * Check if limit is current used in toolbar. -+ * - * @param int $limit - * @return bool - */ -@@ -591,6 +637,8 @@ class Toolbar extends \Magento\Framework\View\Element\Template - } - - /** -+ * Pager number of items from which products started on current page. -+ * - * @return int - */ - public function getFirstNum() -@@ -600,6 +648,8 @@ class Toolbar extends \Magento\Framework\View\Element\Template - } - - /** -+ * Pager number of items products finished on current page. -+ * - * @return int - */ - public function getLastNum() -@@ -609,6 +659,8 @@ class Toolbar extends \Magento\Framework\View\Element\Template - } - - /** -+ * Total number of products in current category. -+ * - * @return int - */ - public function getTotalNum() -@@ -617,6 +669,8 @@ class Toolbar extends \Magento\Framework\View\Element\Template - } - - /** -+ * Check if current page is the first. -+ * - * @return bool - */ - public function isFirstPage() -@@ -625,6 +679,8 @@ class Toolbar extends \Magento\Framework\View\Element\Template - } - - /** -+ * Return last page number. -+ * - * @return int - */ - public function getLastPageNum() -@@ -692,6 +748,8 @@ class Toolbar extends \Magento\Framework\View\Element\Template - 'orderDefault' => $this->getOrderField(), - 'limitDefault' => $this->_productListHelper->getDefaultLimitPerPageValue($defaultMode), - 'url' => $this->getPagerUrl(), -+ 'formKey' => $this->formKey->getFormKey(), -+ 'post' => $this->toolbarMemorizer->isMemorizingAllowed() ? true : false - ]; - $options = array_replace_recursive($options, $customOptions); - return json_encode(['productListToolbarForm' => $options]); -diff --git a/app/code/Magento/Catalog/Block/Product/ProductList/Upsell.php b/app/code/Magento/Catalog/Block/Product/ProductList/Upsell.php -index 10aebf27057..d888f44a6fb 100644 ---- a/app/code/Magento/Catalog/Block/Product/ProductList/Upsell.php -+++ b/app/code/Magento/Catalog/Block/Product/ProductList/Upsell.php -@@ -7,7 +7,6 @@ - namespace Magento\Catalog\Block\Product\ProductList; - - use Magento\Catalog\Model\ResourceModel\Product\Collection; --use Magento\Framework\View\Element\AbstractBlock; - - /** - * Catalog product upsell items block -@@ -61,7 +60,7 @@ class Upsell extends \Magento\Catalog\Block\Product\AbstractProduct implements - protected $_checkoutCart; - - /** -- * @var \Magento\Framework\Module\Manager -+ * @var \Magento\Framework\Module\ModuleManagerInterface - */ - protected $moduleManager; - -@@ -70,7 +69,7 @@ class Upsell extends \Magento\Catalog\Block\Product\AbstractProduct implements - * @param \Magento\Checkout\Model\ResourceModel\Cart $checkoutCart - * @param \Magento\Catalog\Model\Product\Visibility $catalogProductVisibility - * @param \Magento\Checkout\Model\Session $checkoutSession -- * @param \Magento\Framework\Module\Manager $moduleManager -+ * @param \Magento\Framework\Module\ModuleManagerInterface $moduleManager - * @param array $data - */ - public function __construct( -@@ -78,7 +77,7 @@ class Upsell extends \Magento\Catalog\Block\Product\AbstractProduct implements - \Magento\Checkout\Model\ResourceModel\Cart $checkoutCart, - \Magento\Catalog\Model\Product\Visibility $catalogProductVisibility, - \Magento\Checkout\Model\Session $checkoutSession, -- \Magento\Framework\Module\Manager $moduleManager, -+ \Magento\Framework\Module\ModuleManagerInterface $moduleManager, - array $data = [] - ) { - $this->_checkoutCart = $checkoutCart; -@@ -92,11 +91,13 @@ class Upsell extends \Magento\Catalog\Block\Product\AbstractProduct implements - } - - /** -+ * Prepare data -+ * - * @return $this - */ - protected function _prepareData() - { -- $product = $this->_coreRegistry->registry('product'); -+ $product = $this->getProduct(); - /* @var $product \Magento\Catalog\Model\Product */ - $this->_itemCollection = $product->getUpSellProductCollection()->setPositionOrder()->addStoreFilter(); - if ($this->moduleManager->isEnabled('Magento_Checkout')) { -@@ -122,6 +123,8 @@ class Upsell extends \Magento\Catalog\Block\Product\AbstractProduct implements - } - - /** -+ * Before to html handler -+ * - * @return $this - */ - protected function _beforeToHtml() -@@ -131,6 +134,8 @@ class Upsell extends \Magento\Catalog\Block\Product\AbstractProduct implements - } - - /** -+ * Get items collection -+ * - * @return Collection - */ - public function getItemCollection() -@@ -146,6 +151,8 @@ class Upsell extends \Magento\Catalog\Block\Product\AbstractProduct implements - } - - /** -+ * Get collection items -+ * - * @return \Magento\Framework\DataObject[] - */ - public function getItems() -@@ -157,6 +164,8 @@ class Upsell extends \Magento\Catalog\Block\Product\AbstractProduct implements - } - - /** -+ * Get row count -+ * - * @return float - */ - public function getRowCount() -@@ -165,18 +174,22 @@ class Upsell extends \Magento\Catalog\Block\Product\AbstractProduct implements - } - - /** -+ * Set column count -+ * - * @param string $columns - * @return $this - */ - public function setColumnCount($columns) - { -- if (intval($columns) > 0) { -- $this->_columnCount = intval($columns); -+ if ((int) $columns > 0) { -+ $this->_columnCount = (int) $columns; - } - return $this; - } - - /** -+ * Get column count -+ * - * @return int - */ - public function getColumnCount() -@@ -185,6 +198,8 @@ class Upsell extends \Magento\Catalog\Block\Product\AbstractProduct implements - } - - /** -+ * Reset items iterator -+ * - * @return void - */ - public function resetItemsIterator() -@@ -194,6 +209,8 @@ class Upsell extends \Magento\Catalog\Block\Product\AbstractProduct implements - } - - /** -+ * Get iterable item -+ * - * @return mixed - */ - public function getIterableItem() -@@ -205,6 +222,7 @@ class Upsell extends \Magento\Catalog\Block\Product\AbstractProduct implements - - /** - * Set how many items we need to show in upsell block -+ * - * Notice: this parameter will be also applied - * - * @param string $type -@@ -213,13 +231,15 @@ class Upsell extends \Magento\Catalog\Block\Product\AbstractProduct implements - */ - public function setItemLimit($type, $limit) - { -- if (intval($limit) > 0) { -- $this->_itemLimits[$type] = intval($limit); -+ if ((int) $limit > 0) { -+ $this->_itemLimits[$type] = (int) $limit; - } - return $this; - } - - /** -+ * Get item limit -+ * - * @param string $type - * @return array|int - */ -diff --git a/app/code/Magento/Catalog/Block/Product/View.php b/app/code/Magento/Catalog/Block/Product/View.php -index 8055d17a64a..ed6278c2b58 100644 ---- a/app/code/Magento/Catalog/Block/Product/View.php -+++ b/app/code/Magento/Catalog/Block/Product/View.php -@@ -169,8 +169,7 @@ class View extends AbstractProduct implements \Magento\Framework\DataObject\Iden - } - - /** -- * Get JSON encoded configuration array which can be used for JS dynamic -- * price calculation depending on product options -+ * Get JSON encoded configuration which can be used for JS dynamic price calculation depending on product options - * - * @return string - */ -@@ -188,24 +187,25 @@ class View extends AbstractProduct implements \Magento\Framework\DataObject\Iden - } - - $tierPrices = []; -- $tierPricesList = $product->getPriceInfo()->getPrice('tier_price')->getTierPriceList(); -+ $priceInfo = $product->getPriceInfo(); -+ $tierPricesList = $priceInfo->getPrice('tier_price')->getTierPriceList(); - foreach ($tierPricesList as $tierPrice) { -- $tierPrices[] = $tierPrice['price']->getValue(); -+ $tierPrices[] = $tierPrice['price']->getValue() * 1; - } - $config = [ -- 'productId' => $product->getId(), -+ 'productId' => (int)$product->getId(), - 'priceFormat' => $this->_localeFormat->getPriceFormat(), - 'prices' => [ - 'oldPrice' => [ -- 'amount' => $product->getPriceInfo()->getPrice('regular_price')->getAmount()->getValue(), -+ 'amount' => $priceInfo->getPrice('regular_price')->getAmount()->getValue() * 1, - 'adjustments' => [] - ], - 'basePrice' => [ -- 'amount' => $product->getPriceInfo()->getPrice('final_price')->getAmount()->getBaseAmount(), -+ 'amount' => $priceInfo->getPrice('final_price')->getAmount()->getBaseAmount() * 1, - 'adjustments' => [] - ], - 'finalPrice' => [ -- 'amount' => $product->getPriceInfo()->getPrice('final_price')->getAmount()->getValue(), -+ 'amount' => $priceInfo->getPrice('final_price')->getAmount()->getValue() * 1, - 'adjustments' => [] - ] - ], -@@ -262,6 +262,7 @@ class View extends AbstractProduct implements \Magento\Framework\DataObject\Iden - - /** - * Get default qty - either as preconfigured, or as 1. -+ * - * Also restricts it by minimal qty. - * - * @param null|\Magento\Catalog\Model\Product $product -@@ -323,10 +324,7 @@ class View extends AbstractProduct implements \Magento\Framework\DataObject\Iden - public function getIdentities() - { - $identities = $this->getProduct()->getIdentities(); -- $category = $this->_coreRegistry->registry('current_category'); -- if ($category) { -- $identities[] = Category::CACHE_TAG . '_' . $category->getId(); -- } -+ - return $identities; - } - -diff --git a/app/code/Magento/Catalog/Block/Product/View/Attributes.php b/app/code/Magento/Catalog/Block/Product/View/Attributes.php -index cb59d86a745..5b9777cbfd1 100644 ---- a/app/code/Magento/Catalog/Block/Product/View/Attributes.php -+++ b/app/code/Magento/Catalog/Block/Product/View/Attributes.php -@@ -16,6 +16,8 @@ use Magento\Framework\Phrase; - use Magento\Framework\Pricing\PriceCurrencyInterface; - - /** -+ * Attributes attributes block -+ * - * @api - * @since 100.0.2 - */ -@@ -56,6 +58,8 @@ class Attributes extends \Magento\Framework\View\Element\Template - } - - /** -+ * Returns a Product -+ * - * @return Product - */ - public function getProduct() -@@ -88,9 +92,9 @@ class Attributes extends \Magento\Framework\View\Element\Template - $value = $this->priceCurrency->convertAndFormat($value); - } - -- if (is_string($value) && strlen($value)) { -+ if (is_string($value) && strlen(trim($value))) { - $data[$attribute->getAttributeCode()] = [ -- 'label' => __($attribute->getStoreLabel()), -+ 'label' => $attribute->getStoreLabel(), - 'value' => $value, - 'code' => $attribute->getAttributeCode(), - ]; -diff --git a/app/code/Magento/Catalog/Block/Product/View/Details.php b/app/code/Magento/Catalog/Block/Product/View/Details.php -new file mode 100644 -index 00000000000..e76c5bf2013 ---- /dev/null -+++ b/app/code/Magento/Catalog/Block/Product/View/Details.php -@@ -0,0 +1,47 @@ -+<?php -+/** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+ -+declare(strict_types=1); -+ -+namespace Magento\Catalog\Block\Product\View; -+ -+/** -+ * Product details block. -+ * -+ * Holds a group of blocks to show as tabs. -+ * -+ * @api -+ */ -+class Details extends \Magento\Framework\View\Element\Template -+{ -+ /** -+ * Get sorted child block names. -+ * -+ * @param string $groupName -+ * @param string $callback -+ * @throws \Magento\Framework\Exception\LocalizedException -+ * -+ * @return array -+ */ -+ public function getGroupSortedChildNames(string $groupName, string $callback): array -+ { -+ $groupChildNames = $this->getGroupChildNames($groupName, $callback); -+ $layout = $this->getLayout(); -+ -+ $childNamesSortOrder = []; -+ -+ foreach ($groupChildNames as $childName) { -+ $alias = $layout->getElementAlias($childName); -+ $sortOrder = (int)$this->getChildData($alias, 'sort_order') ?? 0; -+ -+ $childNamesSortOrder[$sortOrder] = $childName; -+ } -+ -+ ksort($childNamesSortOrder, SORT_NUMERIC); -+ -+ return $childNamesSortOrder; -+ } -+} -diff --git a/app/code/Magento/Catalog/Block/Product/View/Gallery.php b/app/code/Magento/Catalog/Block/Product/View/Gallery.php -index ab01fc6d134..b6a58c07c42 100644 ---- a/app/code/Magento/Catalog/Block/Product/View/Gallery.php -+++ b/app/code/Magento/Catalog/Block/Product/View/Gallery.php -@@ -23,6 +23,8 @@ use Magento\Framework\Json\EncoderInterface; - use Magento\Framework\Stdlib\ArrayUtils; - - /** -+ * Product gallery block -+ * - * @api - * @since 100.0.2 - */ -@@ -135,16 +137,18 @@ class Gallery extends AbstractView - $imagesItems = []; - /** @var DataObject $image */ - foreach ($this->getGalleryImages() as $image) { -- $imageItem = new DataObject([ -- 'thumb' => $image->getData('small_image_url'), -- 'img' => $image->getData('medium_image_url'), -- 'full' => $image->getData('large_image_url'), -- 'caption' => $image->getData('label'), -- 'position' => $image->getData('position'), -- 'isMain' => $this->isMainImage($image), -- 'type' => str_replace('external-', '', $image->getMediaType()), -- 'videoUrl' => $image->getVideoUrl(), -- ]); -+ $imageItem = new DataObject( -+ [ -+ 'thumb' => $image->getData('small_image_url'), -+ 'img' => $image->getData('medium_image_url'), -+ 'full' => $image->getData('large_image_url'), -+ 'caption' => ($image->getLabel() ?: $this->getProduct()->getName()), -+ 'position' => $image->getData('position'), -+ 'isMain' => $this->isMainImage($image), -+ 'type' => str_replace('external-', '', $image->getMediaType()), -+ 'videoUrl' => $image->getVideoUrl(), -+ ] -+ ); - foreach ($this->getGalleryImagesConfig()->getItems() as $imageConfig) { - $imageItem->setData( - $imageConfig->getData('json_object_key'), -@@ -196,6 +200,8 @@ class Gallery extends AbstractView - } - - /** -+ * Returns image attribute -+ * - * @param string $imageId - * @param string $attributeName - * @param string $default -@@ -203,9 +209,9 @@ class Gallery extends AbstractView - */ - public function getImageAttribute($imageId, $attributeName, $default = null) - { -- $attributes = -- $this->getConfigView()->getMediaAttributes('Magento_Catalog', Image::MEDIA_TYPE_CONFIG_NODE, $imageId); -- return isset($attributes[$attributeName]) ? $attributes[$attributeName] : $default; -+ $attributes = $this->getConfigView() -+ ->getMediaAttributes('Magento_Catalog', Image::MEDIA_TYPE_CONFIG_NODE, $imageId); -+ return $attributes[$attributeName] ?? $default; - } - - /** -@@ -222,6 +228,8 @@ class Gallery extends AbstractView - } - - /** -+ * Returns image gallery config object -+ * - * @return Collection - */ - private function getGalleryImagesConfig() -diff --git a/app/code/Magento/Catalog/Block/Product/View/GalleryOptions.php b/app/code/Magento/Catalog/Block/Product/View/GalleryOptions.php -new file mode 100644 -index 00000000000..0384c9cd9ac ---- /dev/null -+++ b/app/code/Magento/Catalog/Block/Product/View/GalleryOptions.php -@@ -0,0 +1,156 @@ -+<?php -+/** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+namespace Magento\Catalog\Block\Product\View; -+ -+use Magento\Framework\View\Element\Block\ArgumentInterface; -+use Magento\Framework\Serialize\Serializer\Json; -+use Magento\Catalog\Block\Product\Context; -+use Magento\Framework\Stdlib\ArrayUtils; -+ -+/** -+ * Gallery options block. -+ */ -+class GalleryOptions extends AbstractView implements ArgumentInterface -+{ -+ /** -+ * @var Json -+ */ -+ private $jsonSerializer; -+ -+ /** -+ * @var Gallery -+ */ -+ private $gallery; -+ -+ /** -+ * @param Context $context -+ * @param ArrayUtils $arrayUtils -+ * @param Json $jsonSerializer -+ * @param Gallery $gallery -+ * @param array $data -+ */ -+ public function __construct( -+ Context $context, -+ ArrayUtils $arrayUtils, -+ Json $jsonSerializer, -+ Gallery $gallery, -+ array $data = [] -+ ) { -+ $this->gallery = $gallery; -+ $this->jsonSerializer = $jsonSerializer; -+ parent::__construct($context, $arrayUtils, $data); -+ } -+ -+ /** -+ * Retrieve gallery options in JSON format -+ * -+ * @return string -+ * @SuppressWarnings(PHPMD.CyclomaticComplexity) -+ * @SuppressWarnings(PHPMD.NPathComplexity) -+ * @SuppressWarnings(PHPMD.ElseExpression) -+ */ -+ public function getOptionsJson() -+ { -+ $optionItems = null; -+ -+ //Special case for gallery/nav which can be the string "thumbs/false/dots" -+ if (is_bool($this->getVar("gallery/nav"))) { -+ $optionItems['nav'] = $this->getVar("gallery/nav") ? 'true' : 'false'; -+ } else { -+ $optionItems['nav'] = $this->escapeHtml($this->getVar("gallery/nav")); -+ } -+ -+ $optionItems['loop'] = $this->getVar("gallery/loop"); -+ $optionItems['keyboard'] = $this->getVar("gallery/keyboard"); -+ $optionItems['arrows'] = $this->getVar("gallery/arrows"); -+ $optionItems['allowfullscreen'] = $this->getVar("gallery/allowfullscreen"); -+ $optionItems['showCaption'] = $this->getVar("gallery/caption"); -+ $optionItems['width'] = (int)$this->escapeHtml( -+ $this->gallery->getImageAttribute('product_page_image_medium', 'width') -+ ); -+ $optionItems['thumbwidth'] = (int)$this->escapeHtml( -+ $this->gallery->getImageAttribute('product_page_image_small', 'width') -+ ); -+ -+ if ($this->gallery->getImageAttribute('product_page_image_small', 'height') || -+ $this->gallery->getImageAttribute('product_page_image_small', 'width')) { -+ $optionItems['thumbheight'] = (int)$this->escapeHtml( -+ $this->gallery->getImageAttribute('product_page_image_small', 'height') ?: -+ $this->gallery->getImageAttribute('product_page_image_small', 'width') -+ ); -+ } -+ -+ if ($this->gallery->getImageAttribute('product_page_image_medium', 'height') || -+ $this->gallery->getImageAttribute('product_page_image_medium', 'width')) { -+ $optionItems['height'] = (int)$this->escapeHtml( -+ $this->gallery->getImageAttribute('product_page_image_medium', 'height') ?: -+ $this->gallery->getImageAttribute('product_page_image_medium', 'width') -+ ); -+ } -+ -+ if ($this->getVar("gallery/transition/duration")) { -+ $optionItems['transitionduration'] = -+ (int)$this->escapeHtml($this->getVar("gallery/transition/duration")); -+ } -+ -+ $optionItems['transition'] = $this->escapeHtml($this->getVar("gallery/transition/effect")); -+ $optionItems['navarrows'] = $this->getVar("gallery/navarrows"); -+ $optionItems['navtype'] = $this->escapeHtml($this->getVar("gallery/navtype")); -+ $optionItems['navdir'] = $this->escapeHtml($this->getVar("gallery/navdir")); -+ -+ if ($this->getVar("gallery/thumbmargin")) { -+ $optionItems['thumbmargin'] = (int)$this->escapeHtml($this->getVar("gallery/thumbmargin")); -+ } -+ -+ return $this->jsonSerializer->serialize($optionItems); -+ } -+ -+ /** -+ * Retrieve gallery fullscreen options in JSON format -+ * -+ * @return string -+ * @SuppressWarnings(PHPMD.CyclomaticComplexity) -+ * @SuppressWarnings(PHPMD.NPathComplexity) -+ * @SuppressWarnings(PHPMD.ElseExpression) -+ */ -+ public function getFSOptionsJson() -+ { -+ $fsOptionItems = null; -+ -+ //Special case for gallery/nav which can be the string "thumbs/false/dots" -+ if (is_bool($this->getVar("gallery/fullscreen/nav"))) { -+ $fsOptionItems['nav'] = $this->getVar("gallery/fullscreen/nav") ? 'true' : 'false'; -+ } else { -+ $fsOptionItems['nav'] = $this->escapeHtml($this->getVar("gallery/fullscreen/nav")); -+ } -+ -+ $fsOptionItems['loop'] = $this->getVar("gallery/fullscreen/loop"); -+ $fsOptionItems['navdir'] = $this->escapeHtml($this->getVar("gallery/fullscreen/navdir")); -+ $fsOptionItems['navarrows'] = $this->getVar("gallery/fullscreen/navarrows"); -+ $fsOptionItems['navtype'] = $this->escapeHtml($this->getVar("gallery/fullscreen/navtype")); -+ $fsOptionItems['arrows'] = $this->getVar("gallery/fullscreen/arrows"); -+ $fsOptionItems['showCaption'] = $this->getVar("gallery/fullscreen/caption"); -+ -+ if ($this->getVar("gallery/fullscreen/transition/duration")) { -+ $fsOptionItems['transitionduration'] = (int)$this->escapeHtml( -+ $this->getVar("gallery/fullscreen/transition/duration") -+ ); -+ } -+ -+ $fsOptionItems['transition'] = $this->escapeHtml($this->getVar("gallery/fullscreen/transition/effect")); -+ -+ if ($this->getVar("gallery/fullscreen/keyboard")) { -+ $fsOptionItems['keyboard'] = $this->getVar("gallery/fullscreen/keyboard"); -+ } -+ -+ if ($this->getVar("gallery/fullscreen/thumbmargin")) { -+ $fsOptionItems['thumbmargin'] = -+ (int)$this->escapeHtml($this->getVar("gallery/fullscreen/thumbmargin")); -+ } -+ -+ return $this->jsonSerializer->serialize($fsOptionItems); -+ } -+} -diff --git a/app/code/Magento/Catalog/Block/Product/View/Options.php b/app/code/Magento/Catalog/Block/Product/View/Options.php -index 0720c018f6a..c457b20cd09 100644 ---- a/app/code/Magento/Catalog/Block/Product/View/Options.php -+++ b/app/code/Magento/Catalog/Block/Product/View/Options.php -@@ -4,16 +4,15 @@ - * See COPYING.txt for license details. - */ - --/** -- * Product options block -- * -- * @author Magento Core Team <core@magentocommerce.com> -- */ - namespace Magento\Catalog\Block\Product\View; - - use Magento\Catalog\Model\Product; -+use Magento\Catalog\Model\Product\Option\Value; - - /** -+ * Product options block -+ * -+ * @author Magento Core Team <core@magentocommerce.com> - * @api - * @SuppressWarnings(PHPMD.CouplingBetweenObjects) - * @since 100.0.2 -@@ -121,6 +120,8 @@ class Options extends \Magento\Framework\View\Element\Template - } - - /** -+ * Get group of option. -+ * - * @param string $type - * @return string - */ -@@ -142,6 +143,8 @@ class Options extends \Magento\Framework\View\Element\Template - } - - /** -+ * Check if block has options. -+ * - * @return bool - */ - public function hasOptions() -@@ -160,7 +163,10 @@ class Options extends \Magento\Framework\View\Element\Template - */ - protected function _getPriceConfiguration($option) - { -- $optionPrice = $this->pricingHelper->currency($option->getPrice(true), false, false); -+ $optionPrice = $option->getPrice(true); -+ if ($option->getPriceType() !== Value::TYPE_PERCENT) { -+ $optionPrice = $this->pricingHelper->currency($optionPrice, false, false); -+ } - $data = [ - 'prices' => [ - 'oldPrice' => [ -@@ -195,7 +201,7 @@ class Options extends \Magento\Framework\View\Element\Template - ], - ], - 'type' => $option->getPriceType(), -- 'name' => $option->getTitle() -+ 'name' => $option->getTitle(), - ]; - return $data; - } -@@ -231,7 +237,7 @@ class Options extends \Magento\Framework\View\Element\Template - //pass the return array encapsulated in an object for the other modules to be able to alter it eg: weee - $this->_eventManager->dispatch('catalog_product_option_price_configuration_after', ['configObj' => $configObj]); - -- $config=$configObj->getConfig(); -+ $config = $configObj->getConfig(); - - return $this->_jsonEncoder->encode($config); - } -diff --git a/app/code/Magento/Catalog/Block/Product/View/Options/AbstractOptions.php b/app/code/Magento/Catalog/Block/Product/View/Options/AbstractOptions.php -index 181211a0fc4..030b6e1d220 100644 ---- a/app/code/Magento/Catalog/Block/Product/View/Options/AbstractOptions.php -+++ b/app/code/Magento/Catalog/Block/Product/View/Options/AbstractOptions.php -@@ -9,11 +9,15 @@ - * - * @author Magento Core Team <core@magentocommerce.com> - */ -+ - namespace Magento\Catalog\Block\Product\View\Options; - - use Magento\Catalog\Pricing\Price\CustomOptionPriceInterface; - - /** -+ * Product options section abstract block. -+ * -+ * phpcs:disable Magento2.Classes.AbstractApi - * @api - * @since 100.0.2 - */ -@@ -46,7 +50,7 @@ abstract class AbstractOptions extends \Magento\Framework\View\Element\Template - /** - * @param \Magento\Framework\View\Element\Template\Context $context - * @param \Magento\Framework\Pricing\Helper\Data $pricingHelper -- * @param \Magento\Catalog\Helper\Data $catalogData, -+ * @param \Magento\Catalog\Helper\Data $catalogData - * @param array $data - */ - public function __construct( -@@ -123,6 +127,8 @@ abstract class AbstractOptions extends \Magento\Framework\View\Element\Template - } - - /** -+ * Retrieve formatted price. -+ * - * @return string - * - * @deprecated -@@ -134,7 +140,7 @@ abstract class AbstractOptions extends \Magento\Framework\View\Element\Template - } - - /** -- * Return formated price -+ * Return formatted price - * - * @param array $value - * @param bool $flag -diff --git a/app/code/Magento/Catalog/Block/Product/View/Options/Type/Select.php b/app/code/Magento/Catalog/Block/Product/View/Options/Type/Select.php -index 7df9b972e15..d9d663b32f4 100644 ---- a/app/code/Magento/Catalog/Block/Product/View/Options/Type/Select.php -+++ b/app/code/Magento/Catalog/Block/Product/View/Options/Type/Select.php -@@ -3,8 +3,17 @@ - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -+ - namespace Magento\Catalog\Block\Product\View\Options\Type; - -+use Magento\Catalog\Model\Product\Option; -+use Magento\Catalog\Block\Product\View\Options\Type\Select\CheckableFactory; -+use Magento\Catalog\Block\Product\View\Options\Type\Select\MultipleFactory; -+use Magento\Framework\App\ObjectManager; -+use Magento\Framework\View\Element\Template\Context; -+use Magento\Framework\Pricing\Helper\Data; -+use Magento\Catalog\Helper\Data as CatalogHelper; -+ - /** - * Product options text type block - * -@@ -13,169 +22,60 @@ namespace Magento\Catalog\Block\Product\View\Options\Type; - */ - class Select extends \Magento\Catalog\Block\Product\View\Options\AbstractOptions - { -+ /** -+ * @var CheckableFactory -+ */ -+ private $checkableFactory; -+ /** -+ * @var MultipleFactory -+ */ -+ private $multipleFactory; -+ -+ /** -+ * Select constructor. -+ * @param Context $context -+ * @param Data $pricingHelper -+ * @param CatalogHelper $catalogData -+ * @param array $data -+ * @param CheckableFactory|null $checkableFactory -+ * @param MultipleFactory|null $multipleFactory -+ */ -+ public function __construct( -+ Context $context, -+ Data $pricingHelper, -+ CatalogHelper $catalogData, -+ array $data = [], -+ CheckableFactory $checkableFactory = null, -+ MultipleFactory $multipleFactory = null -+ ) { -+ parent::__construct($context, $pricingHelper, $catalogData, $data); -+ $this->checkableFactory = $checkableFactory ?: ObjectManager::getInstance()->get(CheckableFactory::class); -+ $this->multipleFactory = $multipleFactory ?: ObjectManager::getInstance()->get(MultipleFactory::class); -+ } -+ - /** - * Return html for control element - * - * @return string -- * @SuppressWarnings(PHPMD.CyclomaticComplexity) -- * @SuppressWarnings(PHPMD.NPathComplexity) -- * @SuppressWarnings(PHPMD.ExcessiveMethodLength) - */ - public function getValuesHtml() - { -- $_option = $this->getOption(); -- $configValue = $this->getProduct()->getPreconfiguredValues()->getData('options/' . $_option->getId()); -- $store = $this->getProduct()->getStore(); -- -- $this->setSkipJsReloadPrice(1); -- // Remove inline prototype onclick and onchange events -- -- if ($_option->getType() == \Magento\Catalog\Api\Data\ProductCustomOptionInterface::OPTION_TYPE_DROP_DOWN || -- $_option->getType() == \Magento\Catalog\Api\Data\ProductCustomOptionInterface::OPTION_TYPE_MULTIPLE -+ $option = $this->getOption(); -+ $optionType = $option->getType(); -+ if ($optionType === Option::OPTION_TYPE_DROP_DOWN || -+ $optionType === Option::OPTION_TYPE_MULTIPLE - ) { -- $require = $_option->getIsRequire() ? ' required' : ''; -- $extraParams = ''; -- $select = $this->getLayout()->createBlock( -- \Magento\Framework\View\Element\Html\Select::class -- )->setData( -- [ -- 'id' => 'select_' . $_option->getId(), -- 'class' => $require . ' product-custom-option admin__control-select' -- ] -- ); -- if ($_option->getType() == \Magento\Catalog\Api\Data\ProductCustomOptionInterface::OPTION_TYPE_DROP_DOWN) { -- $select->setName('options[' . $_option->getId() . ']')->addOption('', __('-- Please Select --')); -- } else { -- $select->setName('options[' . $_option->getId() . '][]'); -- $select->setClass('multiselect admin__control-multiselect' . $require . ' product-custom-option'); -- } -- foreach ($_option->getValues() as $_value) { -- $priceStr = $this->_formatPrice( -- [ -- 'is_percent' => $_value->getPriceType() == 'percent', -- 'pricing_value' => $_value->getPrice($_value->getPriceType() == 'percent'), -- ], -- false -- ); -- $select->addOption( -- $_value->getOptionTypeId(), -- $_value->getTitle() . ' ' . strip_tags($priceStr) . '', -- ['price' => $this->pricingHelper->currencyByStore($_value->getPrice(true), $store, false)] -- ); -- } -- if ($_option->getType() == \Magento\Catalog\Api\Data\ProductCustomOptionInterface::OPTION_TYPE_MULTIPLE) { -- $extraParams = ' multiple="multiple"'; -- } -- if (!$this->getSkipJsReloadPrice()) { -- $extraParams .= ' onchange="opConfig.reloadPrice()"'; -- } -- $extraParams .= ' data-selector="' . $select->getName() . '"'; -- $select->setExtraParams($extraParams); -- -- if ($configValue) { -- $select->setValue($configValue); -- } -- -- return $select->getHtml(); -+ $optionBlock = $this->multipleFactory->create(); - } -- -- if ($_option->getType() == \Magento\Catalog\Api\Data\ProductCustomOptionInterface::OPTION_TYPE_RADIO || -- $_option->getType() == \Magento\Catalog\Api\Data\ProductCustomOptionInterface::OPTION_TYPE_CHECKBOX -+ if ($optionType === Option::OPTION_TYPE_RADIO || -+ $optionType === Option::OPTION_TYPE_CHECKBOX - ) { -- $selectHtml = '<div class="options-list nested" id="options-' . $_option->getId() . '-list">'; -- $require = $_option->getIsRequire() ? ' required' : ''; -- $arraySign = ''; -- switch ($_option->getType()) { -- case \Magento\Catalog\Api\Data\ProductCustomOptionInterface::OPTION_TYPE_RADIO: -- $type = 'radio'; -- $class = 'radio admin__control-radio'; -- if (!$_option->getIsRequire()) { -- $selectHtml .= '<div class="field choice admin__field admin__field-option">' . -- '<input type="radio" id="options_' . -- $_option->getId() . -- '" class="' . -- $class . -- ' product-custom-option" name="options[' . -- $_option->getId() . -- ']"' . -- ' data-selector="options[' . $_option->getId() . ']"' . -- ($this->getSkipJsReloadPrice() ? '' : ' onclick="opConfig.reloadPrice()"') . -- ' value="" checked="checked" /><label class="label admin__field-label" for="options_' . -- $_option->getId() . -- '"><span>' . -- __('None') . '</span></label></div>'; -- } -- break; -- case \Magento\Catalog\Api\Data\ProductCustomOptionInterface::OPTION_TYPE_CHECKBOX: -- $type = 'checkbox'; -- $class = 'checkbox admin__control-checkbox'; -- $arraySign = '[]'; -- break; -- } -- $count = 1; -- foreach ($_option->getValues() as $_value) { -- $count++; -- -- $priceStr = $this->_formatPrice( -- [ -- 'is_percent' => $_value->getPriceType() == 'percent', -- 'pricing_value' => $_value->getPrice($_value->getPriceType() == 'percent'), -- ] -- ); -- -- $htmlValue = $_value->getOptionTypeId(); -- if ($arraySign) { -- $checked = is_array($configValue) && in_array($htmlValue, $configValue) ? 'checked' : ''; -- } else { -- $checked = $configValue == $htmlValue ? 'checked' : ''; -- } -- -- $dataSelector = 'options[' . $_option->getId() . ']'; -- if ($arraySign) { -- $dataSelector .= '[' . $htmlValue . ']'; -- } -- -- $selectHtml .= '<div class="field choice admin__field admin__field-option' . -- $require . -- '">' . -- '<input type="' . -- $type . -- '" class="' . -- $class . -- ' ' . -- $require . -- ' product-custom-option"' . -- ($this->getSkipJsReloadPrice() ? '' : ' onclick="opConfig.reloadPrice()"') . -- ' name="options[' . -- $_option->getId() . -- ']' . -- $arraySign . -- '" id="options_' . -- $_option->getId() . -- '_' . -- $count . -- '" value="' . -- $htmlValue . -- '" ' . -- $checked . -- ' data-selector="' . $dataSelector . '"' . -- ' price="' . -- $this->pricingHelper->currencyByStore($_value->getPrice(true), $store, false) . -- '" />' . -- '<label class="label admin__field-label" for="options_' . -- $_option->getId() . -- '_' . -- $count . -- '"><span>' . -- $_value->getTitle() . -- '</span> ' . -- $priceStr . -- '</label>'; -- $selectHtml .= '</div>'; -- } -- $selectHtml .= '</div>'; -- -- return $selectHtml; -+ $optionBlock = $this->checkableFactory->create(); - } -+ return $optionBlock -+ ->setOption($option) -+ ->setProduct($this->getProduct()) -+ ->setSkipJsReloadPrice(1) -+ ->_toHtml(); - } - } -diff --git a/app/code/Magento/Catalog/Block/Product/View/Options/Type/Select/Checkable.php b/app/code/Magento/Catalog/Block/Product/View/Options/Type/Select/Checkable.php -new file mode 100644 -index 00000000000..3d856f85dbd ---- /dev/null -+++ b/app/code/Magento/Catalog/Block/Product/View/Options/Type/Select/Checkable.php -@@ -0,0 +1,68 @@ -+<?php -+/** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+ -+declare(strict_types=1); -+ -+namespace Magento\Catalog\Block\Product\View\Options\Type\Select; -+ -+use Magento\Catalog\Api\Data\ProductCustomOptionValuesInterface; -+use Magento\Catalog\Block\Product\View\Options\AbstractOptions; -+use Magento\Catalog\Model\Product\Option; -+ -+/** -+ * Represent needed logic for checkbox and radio button option types -+ */ -+class Checkable extends AbstractOptions -+{ -+ /** -+ * @var string -+ */ -+ protected $_template = 'Magento_Catalog::product/composite/fieldset/options/view/checkable.phtml'; -+ -+ /** -+ * Returns formated price -+ * -+ * @param ProductCustomOptionValuesInterface $value -+ * @return string -+ */ -+ public function formatPrice(ProductCustomOptionValuesInterface $value): string -+ { -+ /** @noinspection PhpMethodParametersCountMismatchInspection */ -+ return parent::_formatPrice( -+ [ -+ 'is_percent' => $value->getPriceType() === 'percent', -+ 'pricing_value' => $value->getPrice($value->getPriceType() === 'percent') -+ ] -+ ); -+ } -+ -+ /** -+ * Returns current currency for store -+ * -+ * @param ProductCustomOptionValuesInterface $value -+ * @return float|string -+ */ -+ public function getCurrencyByStore(ProductCustomOptionValuesInterface $value) -+ { -+ /** @noinspection PhpMethodParametersCountMismatchInspection */ -+ return $this->pricingHelper->currencyByStore( -+ $value->getPrice(true), -+ $this->getProduct()->getStore(), -+ false -+ ); -+ } -+ -+ /** -+ * Returns preconfigured value for given option -+ * -+ * @param Option $option -+ * @return string|array|null -+ */ -+ public function getPreconfiguredValue(Option $option) -+ { -+ return $this->getProduct()->getPreconfiguredValues()->getData('options/' . $option->getId()); -+ } -+} -diff --git a/app/code/Magento/Catalog/Block/Product/View/Options/Type/Select/Multiple.php b/app/code/Magento/Catalog/Block/Product/View/Options/Type/Select/Multiple.php -new file mode 100644 -index 00000000000..09a931dfa06 ---- /dev/null -+++ b/app/code/Magento/Catalog/Block/Product/View/Options/Type/Select/Multiple.php -@@ -0,0 +1,112 @@ -+<?php -+/** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+ -+declare(strict_types=1); -+ -+namespace Magento\Catalog\Block\Product\View\Options\Type\Select; -+ -+use Magento\Catalog\Block\Product\View\Options\AbstractOptions; -+use Magento\Catalog\Model\Product\Option; -+use Magento\Framework\View\Element\Html\Select; -+ -+/** -+ * Represent needed logic for dropdown and multi-select -+ */ -+class Multiple extends AbstractOptions -+{ -+ /** -+ * @inheritdoc -+ * -+ * @return string -+ * @throws \Magento\Framework\Exception\LocalizedException -+ */ -+ protected function _toHtml() -+ { -+ $option = $this->getOption(); -+ $optionType = $option->getType(); -+ $configValue = $this->getProduct()->getPreconfiguredValues()->getData('options/' . $option->getId()); -+ $require = $option->getIsRequire() ? ' required' : ''; -+ $extraParams = ''; -+ /** @var Select $select */ -+ $select = $this->getLayout()->createBlock( -+ Select::class -+ )->setData( -+ [ -+ 'id' => 'select_' . $option->getId(), -+ 'class' => $require . ' product-custom-option admin__control-select' -+ ] -+ ); -+ $select = $this->insertSelectOption($select, $option); -+ $select = $this->processSelectOption($select, $option); -+ if ($optionType === Option::OPTION_TYPE_MULTIPLE) { -+ $extraParams = ' multiple="multiple"'; -+ } -+ if (!$this->getSkipJsReloadPrice()) { -+ $extraParams .= ' onchange="opConfig.reloadPrice()"'; -+ } -+ $extraParams .= ' data-selector="' . $select->getName() . '"'; -+ $select->setExtraParams($extraParams); -+ if ($configValue) { -+ $select->setValue($configValue); -+ } -+ return $select->getHtml(); -+ } -+ -+ /** -+ * Returns select with inserted option give as a parameter -+ * -+ * @param Select $select -+ * @param Option $option -+ * @return Select -+ */ -+ private function insertSelectOption(Select $select, Option $option): Select -+ { -+ $require = $option->getIsRequire() ? ' required' : ''; -+ if ($option->getType() === Option::OPTION_TYPE_DROP_DOWN) { -+ $select->setName('options[' . $option->getId() . ']')->addOption('', __('-- Please Select --')); -+ } else { -+ $select->setName('options[' . $option->getId() . '][]'); -+ $select->setClass('multiselect admin__control-multiselect' . $require . ' product-custom-option'); -+ } -+ -+ return $select; -+ } -+ -+ /** -+ * Returns select with formated option prices -+ * -+ * @param Select $select -+ * @param Option $option -+ * @return Select -+ */ -+ private function processSelectOption(Select $select, Option $option): Select -+ { -+ $store = $this->getProduct()->getStore(); -+ foreach ($option->getValues() as $_value) { -+ $isPercentPriceType = $_value->getPriceType() === 'percent'; -+ $priceStr = $this->_formatPrice( -+ [ -+ 'is_percent' => $isPercentPriceType, -+ 'pricing_value' => $_value->getPrice($isPercentPriceType) -+ ], -+ false -+ ); -+ $select->addOption( -+ $_value->getOptionTypeId(), -+ $_value->getTitle() . ' ' . strip_tags($priceStr) . '', -+ [ -+ 'price' => $this->pricingHelper->currencyByStore( -+ $_value->getPrice(true), -+ $store, -+ false -+ ) -+ ] -+ ); -+ } -+ -+ return $select; -+ } -+} -diff --git a/app/code/Magento/Catalog/Block/Product/Widget/NewWidget.php b/app/code/Magento/Catalog/Block/Product/Widget/NewWidget.php -index 704271b58f4..b4c24231a74 100644 ---- a/app/code/Magento/Catalog/Block/Product/Widget/NewWidget.php -+++ b/app/code/Magento/Catalog/Block/Product/Widget/NewWidget.php -@@ -139,7 +139,7 @@ class NewWidget extends \Magento\Catalog\Block\Product\NewProduct implements \Ma - [ - $this->getDisplayType(), - $this->getProductsPerPage(), -- intval($this->getRequest()->getParam($this->getData('page_var_name'), 1)), -+ (int) $this->getRequest()->getParam($this->getData('page_var_name'), 1), - $this->serializer->serialize($this->getRequest()->getParams()) - ] - ); -diff --git a/app/code/Magento/Catalog/Block/Ui/ProductViewCounter.php b/app/code/Magento/Catalog/Block/Ui/ProductViewCounter.php -index da35b566d7e..dd2e23e67f3 100644 ---- a/app/code/Magento/Catalog/Block/Ui/ProductViewCounter.php -+++ b/app/code/Magento/Catalog/Block/Ui/ProductViewCounter.php -@@ -20,8 +20,8 @@ use Magento\Store\Model\StoreManager; - /** - * Reports Viewed Products Counter - * -- * The main responsilibity of this class is provide necessary data to track viewed products -- * by customer on frontend and data to synchornize this tracks with backend -+ * The main responsibility of this class is provide necessary data to track viewed products -+ * by customer on frontend and data to synchronize this tracks with backend - * - * @api - * @since 101.1.0 -@@ -109,6 +109,8 @@ class ProductViewCounter extends Template - * - * @return string {JSON encoded data} - * @since 101.1.0 -+ * @throws \Magento\Framework\Exception\LocalizedException -+ * @throws \Magento\Framework\Exception\NoSuchEntityException - */ - public function getCurrentProductData() - { -diff --git a/app/code/Magento/Catalog/Console/Command/PriceIndexerDimensionsModeSetCommand.php b/app/code/Magento/Catalog/Console/Command/PriceIndexerDimensionsModeSetCommand.php -deleted file mode 100644 -index e331f4d2a2f..00000000000 ---- a/app/code/Magento/Catalog/Console/Command/PriceIndexerDimensionsModeSetCommand.php -+++ /dev/null -@@ -1,198 +0,0 @@ --<?php --/** -- * Copyright © Magento, Inc. All rights reserved. -- * See COPYING.txt for license details. -- */ --declare(strict_types=1); -- --namespace Magento\Catalog\Console\Command; -- --use Magento\Catalog\Model\Indexer\Product\Price\DimensionModeConfiguration; --use Symfony\Component\Console\Input\InputInterface; --use Symfony\Component\Console\Output\OutputInterface; --use Symfony\Component\Console\Input\InputOption; --use Symfony\Component\Console\Input\InputArgument; --use Magento\Framework\Exception\LocalizedException; --use Magento\Indexer\Console\Command\AbstractIndexerCommand; --use Magento\Framework\App\ObjectManagerFactory; --use Magento\Catalog\Model\Indexer\Product\Price\ModeSwitcher; --use Magento\Framework\App\Config\ScopeConfigInterface; --use Magento\Framework\App\Config\ConfigResource\ConfigInterface; --use Magento\Framework\App\Cache\TypeListInterface; -- --/** -- * Command to change price indexer dimensions mode -- * -- * @SuppressWarnings(PHPMD.CouplingBetweenObjects) -- */ --class PriceIndexerDimensionsModeSetCommand extends AbstractIndexerCommand --{ -- const INPUT_KEY_MODE = 'mode'; -- -- /** -- * ScopeConfigInterface -- * -- * @var ScopeConfigInterface -- */ -- private $configReader; -- -- /** -- * ConfigInterface -- * -- * @var ConfigInterface -- */ -- private $configWriter; -- -- /** -- * TypeListInterface -- * -- * @var TypeListInterface -- */ -- private $cacheTypeList; -- -- /** -- * ModeSwitcher -- * -- * @var ModeSwitcher -- */ -- private $modeSwitcher; -- -- /** -- * @param ObjectManagerFactory $objectManagerFactory -- * @param ScopeConfigInterface $configReader -- * @param ConfigInterface $configWriter -- * @param TypeListInterface $cacheTypeList -- * @param ModeSwitcher $modeSwitcher -- */ -- public function __construct( -- ObjectManagerFactory $objectManagerFactory, -- ScopeConfigInterface $configReader, -- ConfigInterface $configWriter, -- TypeListInterface $cacheTypeList, -- ModeSwitcher $modeSwitcher -- ) { -- $this->configReader = $configReader; -- $this->configWriter = $configWriter; -- $this->cacheTypeList = $cacheTypeList; -- $this->modeSwitcher = $modeSwitcher; -- parent::__construct($objectManagerFactory); -- } -- -- /** -- * {@inheritdoc} -- */ -- protected function configure() -- { -- $this->setName('indexer:set-dimensions-mode:catalog_product_price') -- ->setDescription('Set Indexer Dimensions Mode') -- ->setDefinition($this->getInputList()); -- -- parent::configure(); -- } -- -- /** -- * {@inheritdoc} -- */ -- protected function execute(InputInterface $input, OutputInterface $output) -- { -- $errors = $this->validate($input); -- -- if ($errors) { -- throw new \InvalidArgumentException(implode(PHP_EOL, $errors)); -- } -- -- $returnValue = \Magento\Framework\Console\Cli::RETURN_SUCCESS; -- -- $indexer = $this->getObjectManager()->get(\Magento\Indexer\Model\Indexer::class); -- $indexer->load(\Magento\Catalog\Model\Indexer\Product\Price\Processor::INDEXER_ID); -- -- try { -- $currentMode = $input->getArgument(self::INPUT_KEY_MODE); -- $previousMode = $this->configReader->getValue(ModeSwitcher::XML_PATH_PRICE_DIMENSIONS_MODE) ?: -- DimensionModeConfiguration::DIMENSION_NONE; -- -- if ($previousMode !== $currentMode) { -- //Create new tables and move data -- $this->modeSwitcher->createTables($currentMode); -- $this->modeSwitcher->moveData($currentMode, $previousMode); -- -- //Change config options -- $this->configWriter->saveConfig(ModeSwitcher::XML_PATH_PRICE_DIMENSIONS_MODE, $currentMode); -- $this->cacheTypeList->cleanType('config'); -- $indexer->invalidate(); -- -- //Delete old tables -- $this->modeSwitcher->dropTables($previousMode); -- -- $output->writeln( -- 'Dimensions mode for indexer ' . $indexer->getTitle() . ' was changed from \'' -- . $previousMode . '\' to \'' . $currentMode . '\'' -- ); -- } else { -- $output->writeln('Dimensions mode for indexer ' . $indexer->getTitle() . ' has not been changed'); -- } -- } catch (LocalizedException $e) { -- $output->writeln($e->getMessage() . PHP_EOL); -- // we must have an exit code higher than zero to indicate something was wrong -- $returnValue = \Magento\Framework\Console\Cli::RETURN_FAILURE; -- } catch (\Exception $e) { -- $output->writeln($indexer->getTitle() . " indexer process unknown error:" . PHP_EOL); -- $output->writeln($e->getMessage() . PHP_EOL); -- // we must have an exit code higher than zero to indicate something was wrong -- $returnValue = \Magento\Framework\Console\Cli::RETURN_FAILURE; -- } -- -- return $returnValue; -- } -- -- /** -- * Get list of arguments for the command -- * -- * @return InputOption[] -- */ -- public function getInputList(): array -- { -- $modeOptions[] = new InputArgument( -- self::INPUT_KEY_MODE, -- InputArgument::REQUIRED, -- 'Indexer dimensions mode ['. DimensionModeConfiguration::DIMENSION_NONE -- . '|' . DimensionModeConfiguration::DIMENSION_WEBSITE -- . '|' . DimensionModeConfiguration::DIMENSION_CUSTOMER_GROUP -- . '|' . DimensionModeConfiguration::DIMENSION_WEBSITE_AND_CUSTOMER_GROUP .']' -- ); -- return $modeOptions; -- } -- -- /** -- * Check if all admin options are provided -- * -- * @param InputInterface $input -- * @return string[] -- */ -- public function validate(InputInterface $input): array -- { -- $errors = []; -- -- $acceptedModeValues = ' Accepted values for ' . self::INPUT_KEY_MODE . ' are \'' -- . DimensionModeConfiguration::DIMENSION_NONE . '\', \'' -- . DimensionModeConfiguration::DIMENSION_WEBSITE . '\', \'' -- . DimensionModeConfiguration::DIMENSION_CUSTOMER_GROUP . '\', \'' -- . DimensionModeConfiguration::DIMENSION_WEBSITE_AND_CUSTOMER_GROUP . '\''; -- -- $inputMode = $input->getArgument(self::INPUT_KEY_MODE); -- if (!$inputMode) { -- $errors[] = 'Missing argument \'' . self::INPUT_KEY_MODE .'\'.' . $acceptedModeValues; -- } elseif (!in_array( -- $inputMode, -- [ -- DimensionModeConfiguration::DIMENSION_NONE, -- DimensionModeConfiguration::DIMENSION_WEBSITE, -- DimensionModeConfiguration::DIMENSION_CUSTOMER_GROUP, -- DimensionModeConfiguration::DIMENSION_WEBSITE_AND_CUSTOMER_GROUP -- ] -- )) { -- $errors[] = $acceptedModeValues; -- } -- return $errors; -- } --} -diff --git a/app/code/Magento/Catalog/Controller/Adminhtml/Category/Add.php b/app/code/Magento/Catalog/Controller/Adminhtml/Category/Add.php -index 6456b6d578b..733e270174e 100644 ---- a/app/code/Magento/Catalog/Controller/Adminhtml/Category/Add.php -+++ b/app/code/Magento/Catalog/Controller/Adminhtml/Category/Add.php -@@ -6,12 +6,14 @@ - */ - namespace Magento\Catalog\Controller\Adminhtml\Category; - -+use Magento\Framework\App\Action\HttpGetActionInterface as HttpGetActionInterface; -+ - /** - * Class Add Category - * - * @package Magento\Catalog\Controller\Adminhtml\Category - */ --class Add extends \Magento\Catalog\Controller\Adminhtml\Category -+class Add extends \Magento\Catalog\Controller\Adminhtml\Category implements HttpGetActionInterface - { - /** - * Forward factory for result -diff --git a/app/code/Magento/Catalog/Controller/Adminhtml/Category/CategoriesJson.php b/app/code/Magento/Catalog/Controller/Adminhtml/Category/CategoriesJson.php -index b8865f2de8d..752257f5b90 100644 ---- a/app/code/Magento/Catalog/Controller/Adminhtml/Category/CategoriesJson.php -+++ b/app/code/Magento/Catalog/Controller/Adminhtml/Category/CategoriesJson.php -@@ -6,7 +6,9 @@ - */ - namespace Magento\Catalog\Controller\Adminhtml\Category; - --class CategoriesJson extends \Magento\Catalog\Controller\Adminhtml\Category -+use Magento\Framework\App\Action\HttpPostActionInterface as HttpPostActionInterface; -+ -+class CategoriesJson extends \Magento\Catalog\Controller\Adminhtml\Category implements HttpPostActionInterface - { - /** - * @var \Magento\Framework\Controller\Result\JsonFactory -diff --git a/app/code/Magento/Catalog/Controller/Adminhtml/Category/Delete.php b/app/code/Magento/Catalog/Controller/Adminhtml/Category/Delete.php -index 0a54475b15f..39122d139c9 100644 ---- a/app/code/Magento/Catalog/Controller/Adminhtml/Category/Delete.php -+++ b/app/code/Magento/Catalog/Controller/Adminhtml/Category/Delete.php -@@ -6,7 +6,9 @@ - */ - namespace Magento\Catalog\Controller\Adminhtml\Category; - --class Delete extends \Magento\Catalog\Controller\Adminhtml\Category -+use Magento\Framework\App\Action\HttpPostActionInterface; -+ -+class Delete extends \Magento\Catalog\Controller\Adminhtml\Category implements HttpPostActionInterface - { - /** - * @var \Magento\Catalog\Api\CategoryRepositoryInterface -diff --git a/app/code/Magento/Catalog/Controller/Adminhtml/Category/Edit.php b/app/code/Magento/Catalog/Controller/Adminhtml/Category/Edit.php -index 6ff478e49a3..0450ff1607a 100644 ---- a/app/code/Magento/Catalog/Controller/Adminhtml/Category/Edit.php -+++ b/app/code/Magento/Catalog/Controller/Adminhtml/Category/Edit.php -@@ -6,7 +6,9 @@ - */ - namespace Magento\Catalog\Controller\Adminhtml\Category; - --class Edit extends \Magento\Catalog\Controller\Adminhtml\Category -+use Magento\Framework\App\Action\HttpGetActionInterface as HttpGetActionInterface; -+ -+class Edit extends \Magento\Catalog\Controller\Adminhtml\Category implements HttpGetActionInterface - { - /** - * @var \Magento\Framework\Controller\Result\JsonFactory -diff --git a/app/code/Magento/Catalog/Controller/Adminhtml/Category/Image/Upload.php b/app/code/Magento/Catalog/Controller/Adminhtml/Category/Image/Upload.php -index 4cc0f2d89d1..d1efa0014d4 100644 ---- a/app/code/Magento/Catalog/Controller/Adminhtml/Category/Image/Upload.php -+++ b/app/code/Magento/Catalog/Controller/Adminhtml/Category/Image/Upload.php -@@ -5,12 +5,13 @@ - */ - namespace Magento\Catalog\Controller\Adminhtml\Category\Image; - -+use Magento\Framework\App\Action\HttpPostActionInterface; - use Magento\Framework\Controller\ResultFactory; - - /** - * Class Upload - */ --class Upload extends \Magento\Backend\App\Action -+class Upload extends \Magento\Backend\App\Action implements HttpPostActionInterface - { - /** - * Image uploader -@@ -54,14 +55,6 @@ class Upload extends \Magento\Backend\App\Action - - try { - $result = $this->imageUploader->saveFileToTmpDir($imageId); -- -- $result['cookie'] = [ -- 'name' => $this->_getSession()->getName(), -- 'value' => $this->_getSession()->getSessionId(), -- 'lifetime' => $this->_getSession()->getCookieLifetime(), -- 'path' => $this->_getSession()->getCookiePath(), -- 'domain' => $this->_getSession()->getCookieDomain(), -- ]; - } catch (\Exception $e) { - $result = ['error' => $e->getMessage(), 'errorcode' => $e->getCode()]; - } -diff --git a/app/code/Magento/Catalog/Controller/Adminhtml/Category/Index.php b/app/code/Magento/Catalog/Controller/Adminhtml/Category/Index.php -index 902d71775a3..a5be6223bee 100644 ---- a/app/code/Magento/Catalog/Controller/Adminhtml/Category/Index.php -+++ b/app/code/Magento/Catalog/Controller/Adminhtml/Category/Index.php -@@ -6,7 +6,12 @@ - */ - namespace Magento\Catalog\Controller\Adminhtml\Category; - --class Index extends \Magento\Catalog\Controller\Adminhtml\Category -+use Magento\Framework\App\Action\HttpGetActionInterface as HttpGetActionInterface; -+ -+/** -+ * Controller for category listing -+ */ -+class Index extends \Magento\Catalog\Controller\Adminhtml\Category implements HttpGetActionInterface - { - /** - * @var \Magento\Backend\Model\View\Result\ForwardFactory -diff --git a/app/code/Magento/Catalog/Controller/Adminhtml/Category/Move.php b/app/code/Magento/Catalog/Controller/Adminhtml/Category/Move.php -index df2c80eda14..082101ff078 100644 ---- a/app/code/Magento/Catalog/Controller/Adminhtml/Category/Move.php -+++ b/app/code/Magento/Catalog/Controller/Adminhtml/Category/Move.php -@@ -6,7 +6,12 @@ - */ - namespace Magento\Catalog\Controller\Adminhtml\Category; - --class Move extends \Magento\Catalog\Controller\Adminhtml\Category -+use Magento\Framework\App\Action\HttpPostActionInterface as HttpPostActionInterface; -+ -+/** -+ * Move category admin controller -+ */ -+class Move extends \Magento\Catalog\Controller\Adminhtml\Category implements HttpPostActionInterface - { - /** - * @var \Magento\Framework\Controller\Result\JsonFactory -@@ -26,7 +31,7 @@ class Move extends \Magento\Catalog\Controller\Adminhtml\Category - /** - * @param \Magento\Backend\App\Action\Context $context - * @param \Magento\Framework\Controller\Result\JsonFactory $resultJsonFactory -- * @param \Magento\Framework\View\LayoutFactory $layoutFactory, -+ * @param \Magento\Framework\View\LayoutFactory $layoutFactory - * @param \Psr\Log\LoggerInterface $logger - */ - public function __construct( -@@ -44,7 +49,7 @@ class Move extends \Magento\Catalog\Controller\Adminhtml\Category - /** - * Move category action - * -- * @return \Magento\Framework\Controller\Result\Raw -+ * @return \Magento\Framework\Controller\Result\Json - */ - public function execute() - { -diff --git a/app/code/Magento/Catalog/Controller/Adminhtml/Category/RefreshPath.php b/app/code/Magento/Catalog/Controller/Adminhtml/Category/RefreshPath.php -index 9384397b67f..e3d40bee214 100644 ---- a/app/code/Magento/Catalog/Controller/Adminhtml/Category/RefreshPath.php -+++ b/app/code/Magento/Catalog/Controller/Adminhtml/Category/RefreshPath.php -@@ -1,12 +1,16 @@ - <?php - /** -- * - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ - namespace Magento\Catalog\Controller\Adminhtml\Category; - --class RefreshPath extends \Magento\Catalog\Controller\Adminhtml\Category -+use Magento\Framework\App\Action\HttpGetActionInterface as HttpGetActionInterface; -+ -+/** -+ * Class RefreshPath -+ */ -+class RefreshPath extends \Magento\Catalog\Controller\Adminhtml\Category implements HttpGetActionInterface - { - /** - * @var \Magento\Framework\Controller\Result\JsonFactory -@@ -42,6 +46,7 @@ class RefreshPath extends \Magento\Catalog\Controller\Adminhtml\Category - 'id' => $categoryId, - 'path' => $category->getPath(), - 'parentId' => $category->getParentId(), -+ 'level' => $category->getLevel() - ]); - } - } -diff --git a/app/code/Magento/Catalog/Controller/Adminhtml/Category/Save.php b/app/code/Magento/Catalog/Controller/Adminhtml/Category/Save.php -index aa9ae88754b..77518fd9bf5 100644 ---- a/app/code/Magento/Catalog/Controller/Adminhtml/Category/Save.php -+++ b/app/code/Magento/Catalog/Controller/Adminhtml/Category/Save.php -@@ -6,6 +6,7 @@ - - namespace Magento\Catalog\Controller\Adminhtml\Category; - -+use Magento\Framework\App\Action\HttpPostActionInterface as HttpPostActionInterface; - use Magento\Catalog\Api\Data\CategoryAttributeInterface; - use Magento\Store\Model\StoreManagerInterface; - -@@ -14,7 +15,7 @@ use Magento\Store\Model\StoreManagerInterface; - * - * @SuppressWarnings(PHPMD.CouplingBetweenObjects) - */ --class Save extends \Magento\Catalog\Controller\Adminhtml\Category -+class Save extends \Magento\Catalog\Controller\Adminhtml\Category implements HttpPostActionInterface - { - /** - * @var \Magento\Framework\Controller\Result\RawFactory -@@ -146,6 +147,7 @@ class Save extends \Magento\Catalog\Controller\Adminhtml\Category - $parentCategory = $this->getParentCategory($parentId, $storeId); - $category->setPath($parentCategory->getPath()); - $category->setParentId($parentCategory->getId()); -+ $category->setLevel(null); - } - - /** -diff --git a/app/code/Magento/Catalog/Controller/Adminhtml/Category/Validate.php b/app/code/Magento/Catalog/Controller/Adminhtml/Category/Validate.php -index 4eda49068ac..66a5fb1008a 100644 ---- a/app/code/Magento/Catalog/Controller/Adminhtml/Category/Validate.php -+++ b/app/code/Magento/Catalog/Controller/Adminhtml/Category/Validate.php -@@ -5,10 +5,14 @@ - */ - namespace Magento\Catalog\Controller\Adminhtml\Category; - -+use Magento\Framework\App\Action\HttpGetActionInterface; -+use Magento\Framework\App\Action\HttpPostActionInterface as HttpPostActionInterface; -+use Magento\Catalog\Controller\Adminhtml\Category as CategoryAction; -+ - /** - * Catalog category validate - */ --class Validate extends \Magento\Catalog\Controller\Adminhtml\Category -+class Validate extends CategoryAction implements HttpGetActionInterface, HttpPostActionInterface - { - /** - * @var \Magento\Framework\Controller\Result\JsonFactory -diff --git a/app/code/Magento/Catalog/Controller/Adminhtml/Product/Action/Attribute/Edit.php b/app/code/Magento/Catalog/Controller/Adminhtml/Product/Action/Attribute/Edit.php -index 7eb391dedf8..3cba09b1e8e 100644 ---- a/app/code/Magento/Catalog/Controller/Adminhtml/Product/Action/Attribute/Edit.php -+++ b/app/code/Magento/Catalog/Controller/Adminhtml/Product/Action/Attribute/Edit.php -@@ -6,10 +6,18 @@ - */ - namespace Magento\Catalog\Controller\Adminhtml\Product\Action\Attribute; - -+use Magento\Framework\App\Action\HttpGetActionInterface; -+use Magento\Framework\App\Action\HttpPostActionInterface; - use Magento\Ui\Component\MassAction\Filter; - use Magento\Catalog\Model\ResourceModel\Product\CollectionFactory; -+use Magento\Catalog\Controller\Adminhtml\Product\Action\Attribute as AttributeAction; - --class Edit extends \Magento\Catalog\Controller\Adminhtml\Product\Action\Attribute -+/** -+ * Form for mass updatings products' attributes. -+ * Can be accessed by GET since it's a form, -+ * can be accessed by POST since it's used as a processor of a mass-action button. -+ */ -+class Edit extends AttributeAction implements HttpGetActionInterface, HttpPostActionInterface - { - /** - * @var \Magento\Framework\View\Result\PageFactory -diff --git a/app/code/Magento/Catalog/Controller/Adminhtml/Product/Action/Attribute/Save.php b/app/code/Magento/Catalog/Controller/Adminhtml/Product/Action/Attribute/Save.php -index 0fbf9054ef1..342bbc388f8 100644 ---- a/app/code/Magento/Catalog/Controller/Adminhtml/Product/Action/Attribute/Save.php -+++ b/app/code/Magento/Catalog/Controller/Adminhtml/Product/Action/Attribute/Save.php -@@ -6,84 +6,80 @@ - */ - namespace Magento\Catalog\Controller\Adminhtml\Product\Action\Attribute; - -+use Magento\AsynchronousOperations\Api\Data\OperationInterface; -+use Magento\Framework\App\Action\HttpPostActionInterface; - use Magento\Backend\App\Action; -+use Magento\Framework\Stdlib\DateTime\TimezoneInterface; - - /** - * Class Save - * @SuppressWarnings(PHPMD.CouplingBetweenObjects) - */ --class Save extends \Magento\Catalog\Controller\Adminhtml\Product\Action\Attribute -+class Save extends \Magento\Catalog\Controller\Adminhtml\Product\Action\Attribute implements HttpPostActionInterface - { - /** -- * @var \Magento\Catalog\Model\Indexer\Product\Flat\Processor -+ * @var \Magento\Framework\Bulk\BulkManagementInterface - */ -- protected $_productFlatIndexerProcessor; -+ private $bulkManagement; - - /** -- * @var \Magento\Catalog\Model\Indexer\Product\Price\Processor -+ * @var \Magento\AsynchronousOperations\Api\Data\OperationInterfaceFactory - */ -- protected $_productPriceIndexerProcessor; -+ private $operationFactory; - - /** -- * Catalog product -- * -- * @var \Magento\Catalog\Helper\Product -+ * @var \Magento\Framework\DataObject\IdentityGeneratorInterface - */ -- protected $_catalogProduct; -+ private $identityService; - - /** -- * @var \Magento\CatalogInventory\Api\Data\StockItemInterfaceFactory -+ * @var \Magento\Framework\Serialize\SerializerInterface - */ -- protected $stockItemFactory; -+ private $serializer; - - /** -- * Stock Indexer -- * -- * @var \Magento\CatalogInventory\Model\Indexer\Stock\Processor -+ * @var \Magento\Authorization\Model\UserContextInterface - */ -- protected $_stockIndexerProcessor; -+ private $userContext; - - /** -- * @var \Magento\Framework\Api\DataObjectHelper -+ * @var int - */ -- protected $dataObjectHelper; -+ private $bulkSize; - - /** - * @param Action\Context $context - * @param \Magento\Catalog\Helper\Product\Edit\Action\Attribute $attributeHelper -- * @param \Magento\Catalog\Model\Indexer\Product\Flat\Processor $productFlatIndexerProcessor -- * @param \Magento\Catalog\Model\Indexer\Product\Price\Processor $productPriceIndexerProcessor -- * @param \Magento\CatalogInventory\Model\Indexer\Stock\Processor $stockIndexerProcessor -- * @param \Magento\Catalog\Helper\Product $catalogProduct -- * @param \Magento\CatalogInventory\Api\Data\StockItemInterfaceFactory $stockItemFactory -- * @param \Magento\Framework\Api\DataObjectHelper $dataObjectHelper -+ * @param \Magento\Framework\Bulk\BulkManagementInterface $bulkManagement -+ * @param \Magento\AsynchronousOperations\Api\Data\OperationInterfaceFactory $operartionFactory -+ * @param \Magento\Framework\DataObject\IdentityGeneratorInterface $identityService -+ * @param \Magento\Framework\Serialize\SerializerInterface $serializer -+ * @param \Magento\Authorization\Model\UserContextInterface $userContext -+ * @param int $bulkSize - */ - public function __construct( - Action\Context $context, - \Magento\Catalog\Helper\Product\Edit\Action\Attribute $attributeHelper, -- \Magento\Catalog\Model\Indexer\Product\Flat\Processor $productFlatIndexerProcessor, -- \Magento\Catalog\Model\Indexer\Product\Price\Processor $productPriceIndexerProcessor, -- \Magento\CatalogInventory\Model\Indexer\Stock\Processor $stockIndexerProcessor, -- \Magento\Catalog\Helper\Product $catalogProduct, -- \Magento\CatalogInventory\Api\Data\StockItemInterfaceFactory $stockItemFactory, -- \Magento\Framework\Api\DataObjectHelper $dataObjectHelper -+ \Magento\Framework\Bulk\BulkManagementInterface $bulkManagement, -+ \Magento\AsynchronousOperations\Api\Data\OperationInterfaceFactory $operartionFactory, -+ \Magento\Framework\DataObject\IdentityGeneratorInterface $identityService, -+ \Magento\Framework\Serialize\SerializerInterface $serializer, -+ \Magento\Authorization\Model\UserContextInterface $userContext, -+ int $bulkSize = 100 - ) { -- $this->_productFlatIndexerProcessor = $productFlatIndexerProcessor; -- $this->_productPriceIndexerProcessor = $productPriceIndexerProcessor; -- $this->_stockIndexerProcessor = $stockIndexerProcessor; -- $this->_catalogProduct = $catalogProduct; -- $this->stockItemFactory = $stockItemFactory; - parent::__construct($context, $attributeHelper); -- $this->dataObjectHelper = $dataObjectHelper; -+ $this->bulkManagement = $bulkManagement; -+ $this->operationFactory = $operartionFactory; -+ $this->identityService = $identityService; -+ $this->serializer = $serializer; -+ $this->userContext = $userContext; -+ $this->bulkSize = $bulkSize; - } - - /** - * Update product attributes - * -- * @return \Magento\Backend\Model\View\Result\Redirect -- * @SuppressWarnings(PHPMD.CyclomaticComplexity) -- * @SuppressWarnings(PHPMD.NPathComplexity) -- * @SuppressWarnings(PHPMD.ExcessiveMethodLength) -+ * @return \Magento\Framework\Controller\Result\Redirect - */ - public function execute() - { -@@ -92,128 +88,184 @@ class Save extends \Magento\Catalog\Controller\Adminhtml\Product\Action\Attribut - } - - /* Collect Data */ -- $inventoryData = $this->getRequest()->getParam('inventory', []); - $attributesData = $this->getRequest()->getParam('attributes', []); - $websiteRemoveData = $this->getRequest()->getParam('remove_website_ids', []); - $websiteAddData = $this->getRequest()->getParam('add_website_ids', []); - -- /* Prepare inventory data item options (use config settings) */ -- $options = $this->_objectManager->get(\Magento\CatalogInventory\Api\StockConfigurationInterface::class) -- ->getConfigItemOptions(); -- foreach ($options as $option) { -- if (isset($inventoryData[$option]) && !isset($inventoryData['use_config_' . $option])) { -- $inventoryData['use_config_' . $option] = 0; -- } -- } -+ $storeId = $this->attributeHelper->getSelectedStoreId(); -+ $websiteId = $this->attributeHelper->getStoreWebsiteId($storeId); -+ $productIds = $this->attributeHelper->getProductIds(); -+ -+ $attributesData = $this->sanitizeProductAttributes($attributesData); - - try { -- $storeId = $this->attributeHelper->getSelectedStoreId(); -- if ($attributesData) { -- $dateFormat = $this->_objectManager->get(\Magento\Framework\Stdlib\DateTime\TimezoneInterface::class) -- ->getDateFormat(\IntlDateFormatter::SHORT); -- -- foreach ($attributesData as $attributeCode => $value) { -- $attribute = $this->_objectManager->get(\Magento\Eav\Model\Config::class) -- ->getAttribute(\Magento\Catalog\Model\Product::ENTITY, $attributeCode); -- if (!$attribute->getAttributeId()) { -- unset($attributesData[$attributeCode]); -- continue; -- } -- if ($attribute->getBackendType() == 'datetime') { -- if (!empty($value)) { -- $filterInput = new \Zend_Filter_LocalizedToNormalized(['date_format' => $dateFormat]); -- $filterInternal = new \Zend_Filter_NormalizedToLocalized( -- ['date_format' => \Magento\Framework\Stdlib\DateTime::DATE_INTERNAL_FORMAT] -- ); -- $value = $filterInternal->filter($filterInput->filter($value)); -- } else { -- $value = null; -- } -- $attributesData[$attributeCode] = $value; -- } elseif ($attribute->getFrontendInput() == 'multiselect') { -- // Check if 'Change' checkbox has been checked by admin for this attribute -- $isChanged = (bool)$this->getRequest()->getPost('toggle_' . $attributeCode); -- if (!$isChanged) { -- unset($attributesData[$attributeCode]); -- continue; -- } -- if (is_array($value)) { -- $value = implode(',', $value); -- } -- $attributesData[$attributeCode] = $value; -- } -- } -+ $this->publish($attributesData, $websiteRemoveData, $websiteAddData, $storeId, $websiteId, $productIds); -+ $this->messageManager->addSuccessMessage(__('Message is added to queue')); -+ } catch (\Magento\Framework\Exception\LocalizedException $e) { -+ $this->messageManager->addErrorMessage($e->getMessage()); -+ } catch (\Exception $e) { -+ $this->messageManager->addExceptionMessage( -+ $e, -+ __('Something went wrong while updating the product(s) attributes.') -+ ); -+ } - -- $this->_objectManager->get(\Magento\Catalog\Model\Product\Action::class) -- ->updateAttributes($this->attributeHelper->getProductIds(), $attributesData, $storeId); -- } -+ return $this->resultRedirectFactory->create()->setPath('catalog/product/', ['store' => $storeId]); -+ } - -- if ($inventoryData) { -- // TODO why use ObjectManager? -- /** @var \Magento\CatalogInventory\Api\StockRegistryInterface $stockRegistry */ -- $stockRegistry = $this->_objectManager -- ->create(\Magento\CatalogInventory\Api\StockRegistryInterface::class); -- /** @var \Magento\CatalogInventory\Api\StockItemRepositoryInterface $stockItemRepository */ -- $stockItemRepository = $this->_objectManager -- ->create(\Magento\CatalogInventory\Api\StockItemRepositoryInterface::class); -- foreach ($this->attributeHelper->getProductIds() as $productId) { -- $stockItemDo = $stockRegistry->getStockItem( -- $productId, -- $this->attributeHelper->getStoreWebsiteId($storeId) -- ); -- if (!$stockItemDo->getProductId()) { -- $inventoryData['product_id'] = $productId; -- } -- -- $stockItemId = $stockItemDo->getId(); -- $this->dataObjectHelper->populateWithArray( -- $stockItemDo, -- $inventoryData, -- \Magento\CatalogInventory\Api\Data\StockItemInterface::class -+ /** -+ * Sanitize product attributes -+ * -+ * @param array $attributesData -+ * -+ * @return array -+ */ -+ private function sanitizeProductAttributes($attributesData) -+ { -+ $dateFormat = $this->_objectManager->get(TimezoneInterface::class)->getDateFormat(\IntlDateFormatter::SHORT); -+ $config = $this->_objectManager->get(\Magento\Eav\Model\Config::class); -+ -+ foreach ($attributesData as $attributeCode => $value) { -+ $attribute = $config->getAttribute(\Magento\Catalog\Model\Product::ENTITY, $attributeCode); -+ if (!$attribute->getAttributeId()) { -+ unset($attributesData[$attributeCode]); -+ continue; -+ } -+ if ($attribute->getBackendType() === 'datetime') { -+ if (!empty($value)) { -+ $filterInput = new \Zend_Filter_LocalizedToNormalized(['date_format' => $dateFormat]); -+ $filterInternal = new \Zend_Filter_NormalizedToLocalized( -+ ['date_format' => \Magento\Framework\Stdlib\DateTime::DATE_INTERNAL_FORMAT] - ); -- $stockItemDo->setItemId($stockItemId); -- $stockItemRepository->save($stockItemDo); -+ $value = $filterInternal->filter($filterInput->filter($value)); -+ } else { -+ $value = null; - } -- $this->_stockIndexerProcessor->reindexList($this->attributeHelper->getProductIds()); -- } -- -- if ($websiteAddData || $websiteRemoveData) { -- /* @var $actionModel \Magento\Catalog\Model\Product\Action */ -- $actionModel = $this->_objectManager->get(\Magento\Catalog\Model\Product\Action::class); -- $productIds = $this->attributeHelper->getProductIds(); -- -- if ($websiteRemoveData) { -- $actionModel->updateWebsites($productIds, $websiteRemoveData, 'remove'); -+ $attributesData[$attributeCode] = $value; -+ } elseif ($attribute->getFrontendInput() === 'multiselect') { -+ // Check if 'Change' checkbox has been checked by admin for this attribute -+ $isChanged = (bool)$this->getRequest()->getPost('toggle_' . $attributeCode); -+ if (!$isChanged) { -+ unset($attributesData[$attributeCode]); -+ continue; - } -- if ($websiteAddData) { -- $actionModel->updateWebsites($productIds, $websiteAddData, 'add'); -+ if (is_array($value)) { -+ $value = implode(',', $value); - } -- -- $this->_eventManager->dispatch('catalog_product_to_website_change', ['products' => $productIds]); -+ $attributesData[$attributeCode] = $value; - } -+ } -+ return $attributesData; -+ } - -- $this->messageManager->addSuccessMessage( -- __('A total of %1 record(s) were updated.', count($this->attributeHelper->getProductIds())) -- ); -- -- $this->_productFlatIndexerProcessor->reindexList($this->attributeHelper->getProductIds()); -+ /** -+ * Schedule new bulk -+ * -+ * @param array $attributesData -+ * @param array $websiteRemoveData -+ * @param array $websiteAddData -+ * @param int $storeId -+ * @param int $websiteId -+ * @param array $productIds -+ * @throws \Magento\Framework\Exception\LocalizedException -+ * -+ * @return void -+ */ -+ private function publish( -+ $attributesData, -+ $websiteRemoveData, -+ $websiteAddData, -+ $storeId, -+ $websiteId, -+ $productIds -+ ):void { -+ $productIdsChunks = array_chunk($productIds, $this->bulkSize); -+ $bulkUuid = $this->identityService->generateId(); -+ $bulkDescription = __('Update attributes for ' . count($productIds) . ' selected products'); -+ $operations = []; -+ foreach ($productIdsChunks as $productIdsChunk) { -+ if ($websiteRemoveData || $websiteAddData) { -+ $dataToUpdate = [ -+ 'website_assign' => $websiteAddData, -+ 'website_detach' => $websiteRemoveData -+ ]; -+ $operations[] = $this->makeOperation( -+ 'Update website assign', -+ 'product_action_attribute.website.update', -+ $dataToUpdate, -+ $storeId, -+ $websiteId, -+ $productIdsChunk, -+ $bulkUuid -+ ); -+ } - -- if ($this->_catalogProduct->isDataForPriceIndexerWasChanged($attributesData) -- || !empty($websiteRemoveData) -- || !empty($websiteAddData) -- ) { -- $this->_productPriceIndexerProcessor->reindexList($this->attributeHelper->getProductIds()); -+ if ($attributesData) { -+ $operations[] = $this->makeOperation( -+ 'Update product attributes', -+ 'product_action_attribute.update', -+ $attributesData, -+ $storeId, -+ $websiteId, -+ $productIdsChunk, -+ $bulkUuid -+ ); - } -- } catch (\Magento\Framework\Exception\LocalizedException $e) { -- $this->messageManager->addErrorMessage($e->getMessage()); -- } catch (\Exception $e) { -- $this->messageManager->addExceptionMessage( -- $e, -- __('Something went wrong while updating the product(s) attributes.') -+ } -+ -+ if (!empty($operations)) { -+ $result = $this->bulkManagement->scheduleBulk( -+ $bulkUuid, -+ $operations, -+ $bulkDescription, -+ $this->userContext->getUserId() - ); -+ if (!$result) { -+ throw new \Magento\Framework\Exception\LocalizedException( -+ __('Something went wrong while processing the request.') -+ ); -+ } - } -+ } -+ -+ /** -+ * Make asynchronous operation -+ * -+ * @param string $meta -+ * @param string $queue -+ * @param array $dataToUpdate -+ * @param int $storeId -+ * @param int $websiteId -+ * @param array $productIds -+ * @param int $bulkUuid -+ * -+ * @return OperationInterface -+ */ -+ private function makeOperation( -+ $meta, -+ $queue, -+ $dataToUpdate, -+ $storeId, -+ $websiteId, -+ $productIds, -+ $bulkUuid -+ ): OperationInterface { -+ $dataToEncode = [ -+ 'meta_information' => $meta, -+ 'product_ids' => $productIds, -+ 'store_id' => $storeId, -+ 'website_id' => $websiteId, -+ 'attributes' => $dataToUpdate -+ ]; -+ $data = [ -+ 'data' => [ -+ 'bulk_uuid' => $bulkUuid, -+ 'topic_name' => $queue, -+ 'serialized_data' => $this->serializer->serialize($dataToEncode), -+ 'status' => \Magento\Framework\Bulk\OperationInterface::STATUS_TYPE_OPEN, -+ ] -+ ]; - -- return $this->resultRedirectFactory->create() -- ->setPath('catalog/product/', ['store' => $this->attributeHelper->getSelectedStoreId()]); -+ return $this->operationFactory->create($data); - } - } -diff --git a/app/code/Magento/Catalog/Controller/Adminhtml/Product/Action/Attribute/Validate.php b/app/code/Magento/Catalog/Controller/Adminhtml/Product/Action/Attribute/Validate.php -index a873f08d082..30a6629dd1c 100644 ---- a/app/code/Magento/Catalog/Controller/Adminhtml/Product/Action/Attribute/Validate.php -+++ b/app/code/Magento/Catalog/Controller/Adminhtml/Product/Action/Attribute/Validate.php -@@ -6,7 +6,11 @@ - */ - namespace Magento\Catalog\Controller\Adminhtml\Product\Action\Attribute; - --class Validate extends \Magento\Catalog\Controller\Adminhtml\Product\Action\Attribute -+use Magento\Framework\App\Action\HttpGetActionInterface; -+use Magento\Framework\App\Action\HttpPostActionInterface as HttpPostActionInterface; -+use Magento\Catalog\Controller\Adminhtml\Product\Action\Attribute as AttributeAction; -+ -+class Validate extends AttributeAction implements HttpGetActionInterface, HttpPostActionInterface - { - /** - * @var \Magento\Framework\Controller\Result\JsonFactory -diff --git a/app/code/Magento/Catalog/Controller/Adminhtml/Product/AddAttributeToTemplate.php b/app/code/Magento/Catalog/Controller/Adminhtml/Product/AddAttributeToTemplate.php -index bbef1de28e5..09eacbbf073 100644 ---- a/app/code/Magento/Catalog/Controller/Adminhtml/Product/AddAttributeToTemplate.php -+++ b/app/code/Magento/Catalog/Controller/Adminhtml/Product/AddAttributeToTemplate.php -@@ -6,8 +6,10 @@ - - namespace Magento\Catalog\Controller\Adminhtml\Product; - -+use Magento\Backend\App\Action\Context; - use Magento\Catalog\Api\AttributeSetRepositoryInterface; - use Magento\Catalog\Api\Data\ProductAttributeInterface; -+use Magento\Catalog\Controller\Adminhtml\Product; - use Magento\Eav\Api\AttributeGroupRepositoryInterface; - use Magento\Eav\Api\AttributeManagementInterface; - use Magento\Eav\Api\AttributeRepositoryInterface; -@@ -16,8 +18,14 @@ use Magento\Eav\Api\Data\AttributeGroupInterfaceFactory; - use Magento\Eav\Api\Data\AttributeInterface; - use Magento\Eav\Api\Data\AttributeSetInterface; - use Magento\Framework\Api\SearchCriteriaBuilder; -+use Magento\Framework\App\Action\HttpPostActionInterface; -+use Magento\Framework\Controller\Result\Json; -+use Magento\Framework\Controller\Result\JsonFactory; -+use Magento\Framework\DataObject; - use Magento\Framework\Exception\LocalizedException; -+use Magento\Framework\App\ObjectManager; - use Psr\Log\LoggerInterface; -+use Magento\Framework\Exception\NoSuchEntityException; - use Magento\Framework\Api\ExtensionAttributesFactory; - - /** -@@ -25,10 +33,10 @@ use Magento\Framework\Api\ExtensionAttributesFactory; - * - * @SuppressWarnings(PHPMD.CouplingBetweenObjects) - */ --class AddAttributeToTemplate extends \Magento\Catalog\Controller\Adminhtml\Product -+class AddAttributeToTemplate extends Product implements HttpPostActionInterface - { - /** -- * @var \Magento\Framework\Controller\Result\JsonFactory -+ * @var JsonFactory - */ - protected $resultJsonFactory; - -@@ -75,33 +83,34 @@ class AddAttributeToTemplate extends \Magento\Catalog\Controller\Adminhtml\Produ - /** - * Constructor - * -- * @param \Magento\Backend\App\Action\Context $context -+ * @param Context $context - * @param Builder $productBuilder -- * @param \Magento\Framework\Controller\Result\JsonFactory $resultJsonFactory -- * @param \Magento\Eav\Api\Data\AttributeGroupInterfaceFactory|null $attributeGroupFactory -+ * @param JsonFactory $resultJsonFactory -+ * @param AttributeGroupInterfaceFactory|null $attributeGroupFactory - * @SuppressWarnings(PHPMD.ExcessiveParameterList) -+ * @SuppressWarnings(PHPMD.LongVariable) - */ - public function __construct( -- \Magento\Backend\App\Action\Context $context, -- \Magento\Catalog\Controller\Adminhtml\Product\Builder $productBuilder, -- \Magento\Framework\Controller\Result\JsonFactory $resultJsonFactory, -- \Magento\Eav\Api\Data\AttributeGroupInterfaceFactory $attributeGroupFactory = null -+ Context $context, -+ Builder $productBuilder, -+ JsonFactory $resultJsonFactory, -+ AttributeGroupInterfaceFactory $attributeGroupFactory = null - ) { - parent::__construct($context, $productBuilder); - $this->resultJsonFactory = $resultJsonFactory; -- $this->attributeGroupFactory = $attributeGroupFactory ?: \Magento\Framework\App\ObjectManager::getInstance() -- ->get(\Magento\Eav\Api\Data\AttributeGroupInterfaceFactory::class); -+ $this->attributeGroupFactory = $attributeGroupFactory ?: ObjectManager::getInstance() -+ ->get(AttributeGroupInterfaceFactory::class); - } - - /** - * Add attribute to attribute set - * -- * @return \Magento\Framework\Controller\Result\Json -+ * @return Json - */ - public function execute() - { - $request = $this->getRequest(); -- $response = new \Magento\Framework\DataObject(); -+ $response = new DataObject(); - $response->setError(false); - - try { -@@ -124,12 +133,12 @@ class AddAttributeToTemplate extends \Magento\Catalog\Controller\Adminhtml\Produ - ->getItems(); - - if (!$attributeGroupItems) { -- throw new \Magento\Framework\Exception\NoSuchEntityException; -+ throw new NoSuchEntityException; - } - - /** @var AttributeGroupInterface $attributeGroup */ - $attributeGroup = reset($attributeGroupItems); -- } catch (\Magento\Framework\Exception\NoSuchEntityException $e) { -+ } catch (NoSuchEntityException $e) { - /** @var AttributeGroupInterface $attributeGroup */ - $attributeGroup = $this->attributeGroupFactory->create(); - } -@@ -176,101 +185,114 @@ class AddAttributeToTemplate extends \Magento\Catalog\Controller\Adminhtml\Produ - * Adding basic filters - * - * @return SearchCriteriaBuilder -- * @throws \Magento\Framework\Exception\LocalizedException -+ * @throws LocalizedException - */ - private function getBasicAttributeSearchCriteriaBuilder() - { -- $attributeIds = (array)$this->getRequest()->getParam('attributeIds', []); -+ $attributeIds = (array) $this->getRequest()->getParam('attributeIds', []); - - if (empty($attributeIds['selected'])) { - throw new LocalizedException(__('Attributes were missing and must be specified.')); - } - - return $this->getSearchCriteriaBuilder() -- ->addFilter('attribute_set_id', new \Zend_Db_Expr('null'), 'is') - ->addFilter('attribute_id', [$attributeIds['selected']], 'in'); - } - - /** -+ * Get AttributeRepositoryInterface -+ * - * @return AttributeRepositoryInterface - */ - private function getAttributeRepository() - { - if (null === $this->attributeRepository) { -- $this->attributeRepository = \Magento\Framework\App\ObjectManager::getInstance() -- ->get(\Magento\Eav\Api\AttributeRepositoryInterface::class); -+ $this->attributeRepository = ObjectManager::getInstance() -+ ->get(AttributeRepositoryInterface::class); - } - return $this->attributeRepository; - } - - /** -+ * Get AttributeSetRepositoryInterface -+ * - * @return AttributeSetRepositoryInterface - */ - private function getAttributeSetRepository() - { - if (null === $this->attributeSetRepository) { -- $this->attributeSetRepository = \Magento\Framework\App\ObjectManager::getInstance() -- ->get(\Magento\Catalog\Api\AttributeSetRepositoryInterface::class); -+ $this->attributeSetRepository = ObjectManager::getInstance() -+ ->get(AttributeSetRepositoryInterface::class); - } - return $this->attributeSetRepository; - } - - /** -+ * Get AttributeGroupInterface -+ * - * @return AttributeGroupRepositoryInterface - */ - private function getAttributeGroupRepository() - { - if (null === $this->attributeGroupRepository) { -- $this->attributeGroupRepository = \Magento\Framework\App\ObjectManager::getInstance() -- ->get(\Magento\Eav\Api\AttributeGroupRepositoryInterface::class); -+ $this->attributeGroupRepository = ObjectManager::getInstance() -+ ->get(AttributeGroupRepositoryInterface::class); - } - return $this->attributeGroupRepository; - } - - /** -+ * Get SearchCriteriaBuilder -+ * - * @return SearchCriteriaBuilder - */ - private function getSearchCriteriaBuilder() - { - if (null === $this->searchCriteriaBuilder) { -- $this->searchCriteriaBuilder = \Magento\Framework\App\ObjectManager::getInstance() -- ->get(\Magento\Framework\Api\SearchCriteriaBuilder::class); -+ $this->searchCriteriaBuilder = ObjectManager::getInstance() -+ ->get(SearchCriteriaBuilder::class); - } - return $this->searchCriteriaBuilder; - } - - /** -+ * Get AttributeManagementInterface -+ * - * @return AttributeManagementInterface - */ - private function getAttributeManagement() - { - if (null === $this->attributeManagement) { -- $this->attributeManagement = \Magento\Framework\App\ObjectManager::getInstance() -- ->get(\Magento\Eav\Api\AttributeManagementInterface::class); -+ $this->attributeManagement = ObjectManager::getInstance() -+ ->get(AttributeManagementInterface::class); - } - return $this->attributeManagement; - } - - /** -+ * Get LoggerInterface -+ * - * @return LoggerInterface - */ - private function getLogger() - { - if (null === $this->logger) { -- $this->logger = \Magento\Framework\App\ObjectManager::getInstance() -- ->get(\Psr\Log\LoggerInterface::class); -+ $this->logger = ObjectManager::getInstance() -+ ->get(LoggerInterface::class); - } - return $this->logger; - } - - /** -+ * Get ExtensionAttributesFactory. -+ * - * @return ExtensionAttributesFactory - */ - private function getExtensionAttributesFactory() - { - if (null === $this->extensionAttributesFactory) { -- $this->extensionAttributesFactory = \Magento\Framework\App\ObjectManager::getInstance() -- ->get(\Magento\Framework\Api\ExtensionAttributesFactory::class); -+ $this->extensionAttributesFactory = ObjectManager::getInstance() -+ ->get(ExtensionAttributesFactory::class); - } - return $this->extensionAttributesFactory; - } -diff --git a/app/code/Magento/Catalog/Controller/Adminhtml/Product/Attribute/Delete.php b/app/code/Magento/Catalog/Controller/Adminhtml/Product/Attribute/Delete.php -index bef6aee0e2a..faa9e4ddf49 100644 ---- a/app/code/Magento/Catalog/Controller/Adminhtml/Product/Attribute/Delete.php -+++ b/app/code/Magento/Catalog/Controller/Adminhtml/Product/Attribute/Delete.php -@@ -6,7 +6,9 @@ - */ - namespace Magento\Catalog\Controller\Adminhtml\Product\Attribute; - --class Delete extends \Magento\Catalog\Controller\Adminhtml\Product\Attribute -+use Magento\Framework\App\Action\HttpPostActionInterface; -+ -+class Delete extends \Magento\Catalog\Controller\Adminhtml\Product\Attribute implements HttpPostActionInterface - { - /** - * @return \Magento\Backend\Model\View\Result\Redirect -diff --git a/app/code/Magento/Catalog/Controller/Adminhtml/Product/Attribute/Edit.php b/app/code/Magento/Catalog/Controller/Adminhtml/Product/Attribute/Edit.php -index a99cbdbade1..a41cd71aea4 100644 ---- a/app/code/Magento/Catalog/Controller/Adminhtml/Product/Attribute/Edit.php -+++ b/app/code/Magento/Catalog/Controller/Adminhtml/Product/Attribute/Edit.php -@@ -6,7 +6,9 @@ - */ - namespace Magento\Catalog\Controller\Adminhtml\Product\Attribute; - --class Edit extends \Magento\Catalog\Controller\Adminhtml\Product\Attribute -+use Magento\Framework\App\Action\HttpGetActionInterface as HttpGetActionInterface; -+ -+class Edit extends \Magento\Catalog\Controller\Adminhtml\Product\Attribute implements HttpGetActionInterface - { - /** - * @return \Magento\Framework\Controller\ResultInterface -diff --git a/app/code/Magento/Catalog/Controller/Adminhtml/Product/Attribute/Index.php b/app/code/Magento/Catalog/Controller/Adminhtml/Product/Attribute/Index.php -index 9bdc54b289c..34267121f9b 100644 ---- a/app/code/Magento/Catalog/Controller/Adminhtml/Product/Attribute/Index.php -+++ b/app/code/Magento/Catalog/Controller/Adminhtml/Product/Attribute/Index.php -@@ -6,7 +6,9 @@ - */ - namespace Magento\Catalog\Controller\Adminhtml\Product\Attribute; - --class Index extends \Magento\Catalog\Controller\Adminhtml\Product\Attribute -+use Magento\Framework\App\Action\HttpGetActionInterface as HttpGetActionInterface; -+ -+class Index extends \Magento\Catalog\Controller\Adminhtml\Product\Attribute implements HttpGetActionInterface - { - /** - * @return \Magento\Backend\Model\View\Result\Page -diff --git a/app/code/Magento/Catalog/Controller/Adminhtml/Product/Attribute/NewAction.php b/app/code/Magento/Catalog/Controller/Adminhtml/Product/Attribute/NewAction.php -index e954d973059..fdfde7e8060 100644 ---- a/app/code/Magento/Catalog/Controller/Adminhtml/Product/Attribute/NewAction.php -+++ b/app/code/Magento/Catalog/Controller/Adminhtml/Product/Attribute/NewAction.php -@@ -6,7 +6,9 @@ - */ - namespace Magento\Catalog\Controller\Adminhtml\Product\Attribute; - --class NewAction extends \Magento\Catalog\Controller\Adminhtml\Product\Attribute -+use Magento\Framework\App\Action\HttpGetActionInterface as HttpGetActionInterface; -+ -+class NewAction extends \Magento\Catalog\Controller\Adminhtml\Product\Attribute implements HttpGetActionInterface - { - /** - * @var \Magento\Backend\Model\View\Result\ForwardFactory -diff --git a/app/code/Magento/Catalog/Controller/Adminhtml/Product/Attribute/Save.php b/app/code/Magento/Catalog/Controller/Adminhtml/Product/Attribute/Save.php -index 817de6828e4..853cc652703 100644 ---- a/app/code/Magento/Catalog/Controller/Adminhtml/Product/Attribute/Save.php -+++ b/app/code/Magento/Catalog/Controller/Adminhtml/Product/Attribute/Save.php -@@ -7,12 +7,14 @@ - - namespace Magento\Catalog\Controller\Adminhtml\Product\Attribute; - -+use Magento\Framework\App\Action\HttpPostActionInterface as HttpPostActionInterface; - use Magento\Backend\App\Action\Context; - use Magento\Backend\Model\View\Result\Redirect; - use Magento\Catalog\Api\Data\ProductAttributeInterface; - use Magento\Catalog\Controller\Adminhtml\Product\Attribute; - use Magento\Catalog\Helper\Product; - use Magento\Catalog\Model\Product\Attribute\Frontend\Inputtype\Presentation; -+use Magento\Framework\Serialize\Serializer\FormData; - use Magento\Catalog\Model\Product\AttributeSet\BuildFactory; - use Magento\Catalog\Model\ResourceModel\Eav\AttributeFactory; - use Magento\Eav\Model\Adminhtml\System\Config\Source\Inputtype\Validator; -@@ -31,9 +33,11 @@ use Magento\Framework\View\LayoutFactory; - use Magento\Framework\View\Result\PageFactory; - - /** -+ * Product attribute save controller. -+ * - * @SuppressWarnings(PHPMD.CouplingBetweenObjects) - */ --class Save extends Attribute -+class Save extends Attribute implements HttpPostActionInterface - { - /** - * @var BuildFactory -@@ -75,6 +79,11 @@ class Save extends Attribute - */ - private $presentation; - -+ /** -+ * @var FormData|null -+ */ -+ private $formDataSerializer; -+ - /** - * @param Context $context - * @param FrontendInterface $attributeLabelCache -@@ -88,6 +97,7 @@ class Save extends Attribute - * @param Product $productHelper - * @param LayoutFactory $layoutFactory - * @param Presentation|null $presentation -+ * @param FormData|null $formDataSerializer - * @SuppressWarnings(PHPMD.ExcessiveParameterList) - */ - public function __construct( -@@ -102,7 +112,8 @@ class Save extends Attribute - FilterManager $filterManager, - Product $productHelper, - LayoutFactory $layoutFactory, -- Presentation $presentation = null -+ Presentation $presentation = null, -+ FormData $formDataSerializer = null - ) { - parent::__construct($context, $attributeLabelCache, $coreRegistry, $resultPageFactory); - $this->buildFactory = $buildFactory; -@@ -113,19 +124,38 @@ class Save extends Attribute - $this->groupCollectionFactory = $groupCollectionFactory; - $this->layoutFactory = $layoutFactory; - $this->presentation = $presentation ?: ObjectManager::getInstance()->get(Presentation::class); -+ $this->formDataSerializer = $formDataSerializer -+ ?: ObjectManager::getInstance()->get(FormData::class); - } - - /** -+ * @inheritdoc -+ * - * @return Redirect - * @SuppressWarnings(PHPMD.CyclomaticComplexity) - * @SuppressWarnings(PHPMD.NPathComplexity) - * @SuppressWarnings(PHPMD.ExcessiveMethodLength) -+ * @throws \Zend_Validate_Exception - */ - public function execute() - { -+ try { -+ $optionData = $this->formDataSerializer -+ ->unserialize($this->getRequest()->getParam('serialized_options', '[]')); -+ } catch (\InvalidArgumentException $e) { -+ $message = __("The attribute couldn't be saved due to an error. Verify your information and try again. " -+ . "If the error persists, please try again later."); -+ $this->messageManager->addErrorMessage($message); -+ return $this->returnResult('catalog/*/edit', ['_current' => true], ['error' => true]); -+ } -+ - $data = $this->getRequest()->getPostValue(); -+ $data = array_replace_recursive( -+ $data, -+ $optionData -+ ); -+ - if ($data) { -- $this->preprocessOptionsData($data); - $setId = $this->getRequest()->getParam('set'); - - $attributeSet = null; -@@ -134,7 +164,7 @@ class Save extends Attribute - $name = trim($name); - - try { -- /** @var $attributeSet Set */ -+ /** @var Set $attributeSet */ - $attributeSet = $this->buildFactory->create() - ->setEntityTypeId($this->_entityTypeId) - ->setSkeletonId($setId) -@@ -156,7 +186,7 @@ class Save extends Attribute - - $attributeId = $this->getRequest()->getParam('attribute_id'); - -- /** @var $model ProductAttributeInterface */ -+ /** @var ProductAttributeInterface $model */ - $model = $this->attributeFactory->create(); - if ($attributeId) { - $model->load($attributeId); -@@ -165,30 +195,11 @@ class Save extends Attribute - ? $model->getAttributeCode() - : $this->getRequest()->getParam('attribute_code'); - $attributeCode = $attributeCode ?: $this->generateCode($this->getRequest()->getParam('frontend_label')[0]); -- if (strlen($attributeCode) > 0) { -- $validatorAttrCode = new \Zend_Validate_Regex( -- ['pattern' => '/^[a-z\x{600}-\x{6FF}][a-z\x{600}-\x{6FF}_0-9]{0,30}$/u'] -- ); -- if (!$validatorAttrCode->isValid($attributeCode)) { -- $this->messageManager->addErrorMessage( -- __( -- 'Attribute code "%1" is invalid. Please use only letters (a-z), ' . -- 'numbers (0-9) or underscore(_) in this field, first character should be a letter.', -- $attributeCode -- ) -- ); -- return $this->returnResult( -- 'catalog/*/edit', -- ['attribute_id' => $attributeId, '_current' => true], -- ['error' => true] -- ); -- } -- } - $data['attribute_code'] = $attributeCode; - - //validate frontend_input - if (isset($data['frontend_input'])) { -- /** @var $inputType Validator */ -+ /** @var Validator $inputType */ - $inputType = $this->validatorFactory->create(); - if (!$inputType->isValid($data['frontend_input'])) { - foreach ($inputType->getMessages() as $message) { -@@ -229,14 +240,14 @@ class Save extends Attribute - $data['backend_model'] = $this->productHelper->getAttributeBackendModelByInputType( - $data['frontend_input'] - ); -+ -+ if ($model->getIsUserDefined() === null) { -+ $data['backend_type'] = $model->getBackendTypeByInput($data['frontend_input']); -+ } - } - - $data += ['is_filterable' => 0, 'is_filterable_in_search' => 0]; - -- if ($model->getIsUserDefined() === null || $model->getIsUserDefined() != 0) { -- $data['backend_type'] = $model->getBackendTypeByInput($data['frontend_input']); -- } -- - $defaultValueField = $model->getDefaultValueByInput($data['frontend_input']); - if ($defaultValueField) { - $data['default_value'] = $this->getRequest()->getParam($defaultValueField); -@@ -316,28 +327,8 @@ class Save extends Attribute - } - - /** -- * Extract options data from serialized options field and append to data array. -- * -- * This logic is required to overcome max_input_vars php limit -- * that may vary and/or be inaccessible to change on different instances. -+ * Provides an initialized Result object. - * -- * @param array $data -- * @return void -- */ -- private function preprocessOptionsData(&$data) -- { -- if (isset($data['serialized_options'])) { -- $serializedOptions = json_decode($data['serialized_options'], JSON_OBJECT_AS_ARRAY); -- foreach ($serializedOptions as $serializedOption) { -- $option = []; -- parse_str($serializedOption, $option); -- $data = array_replace_recursive($data, $option); -- } -- } -- unset($data['serialized_options']); -- } -- -- /** - * @param string $path - * @param array $params - * @param array $response -diff --git a/app/code/Magento/Catalog/Controller/Adminhtml/Product/Attribute/Validate.php b/app/code/Magento/Catalog/Controller/Adminhtml/Product/Attribute/Validate.php -index db452113ada..c74a382724a 100644 ---- a/app/code/Magento/Catalog/Controller/Adminhtml/Product/Attribute/Validate.php -+++ b/app/code/Magento/Catalog/Controller/Adminhtml/Product/Attribute/Validate.php -@@ -7,9 +7,20 @@ - - namespace Magento\Catalog\Controller\Adminhtml\Product\Attribute; - -+use Magento\Catalog\Controller\Adminhtml\Product\Attribute as AttributeAction; -+use Magento\Eav\Model\Validator\Attribute\Code as AttributeCodeValidator; -+use Magento\Framework\App\Action\HttpGetActionInterface; -+use Magento\Framework\App\Action\HttpPostActionInterface as HttpPostActionInterface; -+use Magento\Framework\App\ObjectManager; - use Magento\Framework\DataObject; -+use Magento\Framework\Serialize\Serializer\FormData; - --class Validate extends \Magento\Catalog\Controller\Adminhtml\Product\Attribute -+/** -+ * Product attribute validate controller. -+ * -+ * @SuppressWarnings(PHPMD.CouplingBetweenObjects) -+ */ -+class Validate extends AttributeAction implements HttpGetActionInterface, HttpPostActionInterface - { - const DEFAULT_MESSAGE_KEY = 'message'; - -@@ -28,6 +39,16 @@ class Validate extends \Magento\Catalog\Controller\Adminhtml\Product\Attribute - */ - private $multipleAttributeList; - -+ /** -+ * @var FormData|null -+ */ -+ private $formDataSerializer; -+ -+ /** -+ * @var AttributeCodeValidator -+ */ -+ private $attributeCodeValidator; -+ - /** - * Constructor - * -@@ -38,6 +59,8 @@ class Validate extends \Magento\Catalog\Controller\Adminhtml\Product\Attribute - * @param \Magento\Framework\Controller\Result\JsonFactory $resultJsonFactory - * @param \Magento\Framework\View\LayoutFactory $layoutFactory - * @param array $multipleAttributeList -+ * @param FormData|null $formDataSerializer -+ * @param AttributeCodeValidator|null $attributeCodeValidator - */ - public function __construct( - \Magento\Backend\App\Action\Context $context, -@@ -46,15 +69,24 @@ class Validate extends \Magento\Catalog\Controller\Adminhtml\Product\Attribute - \Magento\Framework\View\Result\PageFactory $resultPageFactory, - \Magento\Framework\Controller\Result\JsonFactory $resultJsonFactory, - \Magento\Framework\View\LayoutFactory $layoutFactory, -- array $multipleAttributeList = [] -+ array $multipleAttributeList = [], -+ FormData $formDataSerializer = null, -+ AttributeCodeValidator $attributeCodeValidator = null - ) { - parent::__construct($context, $attributeLabelCache, $coreRegistry, $resultPageFactory); - $this->resultJsonFactory = $resultJsonFactory; - $this->layoutFactory = $layoutFactory; - $this->multipleAttributeList = $multipleAttributeList; -+ $this->formDataSerializer = $formDataSerializer ?: ObjectManager::getInstance() -+ ->get(FormData::class); -+ $this->attributeCodeValidator = $attributeCodeValidator ?: ObjectManager::getInstance()->get( -+ AttributeCodeValidator::class -+ ); - } - - /** -+ * @inheritdoc -+ * - * @return \Magento\Framework\Controller\ResultInterface - * @SuppressWarnings(PHPMD.NPathComplexity) - * @SuppressWarnings(PHPMD.CyclomaticComplexity) -@@ -63,6 +95,15 @@ class Validate extends \Magento\Catalog\Controller\Adminhtml\Product\Attribute - { - $response = new DataObject(); - $response->setError(false); -+ try { -+ $optionsData = $this->formDataSerializer -+ ->unserialize($this->getRequest()->getParam('serialized_options', '[]')); -+ } catch (\InvalidArgumentException $e) { -+ $message = __("The attribute couldn't be validated due to an error. Verify your information and try again. " -+ . "If the error persists, please try again later."); -+ $this->setMessageToResponse($response, [$message]); -+ $response->setError(true); -+ } - - $attributeCode = $this->getRequest()->getParam('attribute_code'); - $frontendLabel = $this->getRequest()->getParam('frontend_label'); -@@ -75,7 +116,7 @@ class Validate extends \Magento\Catalog\Controller\Adminhtml\Product\Attribute - $attributeCode - ); - -- if ($attribute->getId() && !$attributeId) { -+ if ($attribute->getId() && !$attributeId || $attributeCode === 'product_type' || $attributeCode === 'type_id') { - $message = strlen($this->getRequest()->getParam('attribute_code')) - ? __('An attribute with this code already exists.') - : __('An attribute with the same code (%1) already exists.', $attributeCode); -@@ -85,6 +126,12 @@ class Validate extends \Magento\Catalog\Controller\Adminhtml\Product\Attribute - $response->setError(true); - $response->setProductAttribute($attribute->toArray()); - } -+ -+ if (!$this->attributeCodeValidator->isValid($attributeCode)) { -+ $this->setMessageToResponse($response, $this->attributeCodeValidator->getMessages()); -+ $response->setError(true); -+ } -+ - if ($this->getRequest()->has('new_attribute_set_name')) { - $setName = $this->getRequest()->getParam('new_attribute_set_name'); - /** @var $attributeSet \Magento\Eav\Model\Entity\Attribute\Set */ -@@ -102,10 +149,10 @@ class Validate extends \Magento\Catalog\Controller\Adminhtml\Product\Attribute - } - - $multipleOption = $this->getRequest()->getParam("frontend_input"); -- $multipleOption = null == $multipleOption ? 'select' : $multipleOption; -+ $multipleOption = (null === $multipleOption) ? 'select' : $multipleOption; - -- if (isset($this->multipleAttributeList[$multipleOption]) && !(null == ($multipleOption))) { -- $options = $this->getRequest()->getParam($this->multipleAttributeList[$multipleOption]); -+ if (isset($this->multipleAttributeList[$multipleOption])) { -+ $options = $optionsData[$this->multipleAttributeList[$multipleOption]] ?? null; - $this->checkUniqueOption( - $response, - $options -@@ -123,7 +170,8 @@ class Validate extends \Magento\Catalog\Controller\Adminhtml\Product\Attribute - } - - /** -- * Throws Exception if not unique values into options -+ * Throws Exception if not unique values into options. -+ * - * @param array $optionsValues - * @param array $deletedOptions - * @return bool -@@ -132,7 +180,7 @@ class Validate extends \Magento\Catalog\Controller\Adminhtml\Product\Attribute - { - $adminValues = []; - foreach ($optionsValues as $optionKey => $values) { -- if (!(isset($deletedOptions[$optionKey]) and $deletedOptions[$optionKey] === '1')) { -+ if (!(isset($deletedOptions[$optionKey]) && $deletedOptions[$optionKey] === '1')) { - $adminValues[] = reset($values); - } - } -@@ -157,6 +205,8 @@ class Validate extends \Magento\Catalog\Controller\Adminhtml\Product\Attribute - } - - /** -+ * Performs checking the uniqueness of the attribute options. -+ * - * @param DataObject $response - * @param array|null $options - * @return $this -diff --git a/app/code/Magento/Catalog/Controller/Adminhtml/Product/Builder.php b/app/code/Magento/Catalog/Controller/Adminhtml/Product/Builder.php -index 4fa61b2b372..78ad9f42387 100644 ---- a/app/code/Magento/Catalog/Controller/Adminhtml/Product/Builder.php -+++ b/app/code/Magento/Catalog/Controller/Adminhtml/Product/Builder.php -@@ -3,15 +3,26 @@ - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -+declare(strict_types=1); -+ - namespace Magento\Catalog\Controller\Adminhtml\Product; - -+use Magento\Catalog\Api\Data\ProductInterface; - use Magento\Catalog\Model\ProductFactory; - use Magento\Cms\Model\Wysiwyg as WysiwygModel; - use Magento\Framework\App\RequestInterface; - use Magento\Store\Model\StoreFactory; - use Psr\Log\LoggerInterface as Logger; - use Magento\Framework\Registry; -+use Magento\Catalog\Api\ProductRepositoryInterface; -+use Magento\Catalog\Model\Product; -+use Magento\Catalog\Model\Product\Type as ProductTypes; - -+/** -+ * Build a product based on a request -+ * -+ * @SuppressWarnings(PHPMD.CouplingBetweenObjects) -+ */ - class Builder - { - /** -@@ -39,6 +50,11 @@ class Builder - */ - protected $storeFactory; - -+ /** -+ * @var ProductRepositoryInterface -+ */ -+ private $productRepository; -+ - /** - * Constructor - * -@@ -47,13 +63,15 @@ class Builder - * @param Registry $registry - * @param WysiwygModel\Config $wysiwygConfig - * @param StoreFactory|null $storeFactory -+ * @param ProductRepositoryInterface|null $productRepository - */ - public function __construct( - ProductFactory $productFactory, - Logger $logger, - Registry $registry, - WysiwygModel\Config $wysiwygConfig, -- StoreFactory $storeFactory = null -+ StoreFactory $storeFactory = null, -+ ProductRepositoryInterface $productRepository = null - ) { - $this->productFactory = $productFactory; - $this->logger = $logger; -@@ -61,47 +79,77 @@ class Builder - $this->wysiwygConfig = $wysiwygConfig; - $this->storeFactory = $storeFactory ?: \Magento\Framework\App\ObjectManager::getInstance() - ->get(\Magento\Store\Model\StoreFactory::class); -+ $this->productRepository = $productRepository ?: \Magento\Framework\App\ObjectManager::getInstance() -+ ->get(ProductRepositoryInterface::class); - } - - /** - * Build product based on user request - * - * @param RequestInterface $request -- * @return \Magento\Catalog\Model\Product -+ * @return ProductInterface -+ * @throws \RuntimeException -+ * @throws \Magento\Framework\Exception\LocalizedException - */ -- public function build(RequestInterface $request) -+ public function build(RequestInterface $request): ProductInterface - { -- $productId = (int)$request->getParam('id'); -- /** @var $product \Magento\Catalog\Model\Product */ -- $product = $this->productFactory->create(); -- $product->setStoreId($request->getParam('store', 0)); -- $store = $this->storeFactory->create(); -- $store->load($request->getParam('store', 0)); -- -+ $productId = (int) $request->getParam('id'); -+ $storeId = $request->getParam('store', 0); -+ $attributeSetId = (int) $request->getParam('set'); - $typeId = $request->getParam('type'); -- if (!$productId && $typeId) { -- $product->setTypeId($typeId); -- } - -- $product->setData('_edit_mode', true); - if ($productId) { - try { -- $product->load($productId); -+ $product = $this->productRepository->getById($productId, true, $storeId); -+ if ($attributeSetId) { -+ $product->setAttributeSetId($attributeSetId); -+ } - } catch (\Exception $e) { -- $product->setTypeId(\Magento\Catalog\Model\Product\Type::DEFAULT_TYPE); -+ $product = $this->createEmptyProduct(ProductTypes::DEFAULT_TYPE, $attributeSetId, $storeId); - $this->logger->critical($e); - } -+ } else { -+ $product = $this->createEmptyProduct($typeId, $attributeSetId, $storeId); - } - -- $setId = (int)$request->getParam('set'); -- if ($setId) { -- $product->setAttributeSetId($setId); -- } -+ $store = $this->storeFactory->create(); -+ $store->load($storeId); - - $this->registry->register('product', $product); - $this->registry->register('current_product', $product); - $this->registry->register('current_store', $store); -- $this->wysiwygConfig->setStoreId($request->getParam('store')); -+ -+ $this->wysiwygConfig->setStoreId($storeId); -+ -+ return $product; -+ } -+ -+ /** -+ * Create a product with the given properties -+ * -+ * @param int $typeId -+ * @param int $attributeSetId -+ * @param int $storeId -+ * @return \Magento\Catalog\Model\Product -+ */ -+ private function createEmptyProduct($typeId, $attributeSetId, $storeId): Product -+ { -+ /** @var $product \Magento\Catalog\Model\Product */ -+ $product = $this->productFactory->create(); -+ $product->setData('_edit_mode', true); -+ -+ if ($typeId !== null) { -+ $product->setTypeId($typeId); -+ } -+ -+ if ($storeId !== null) { -+ $product->setStoreId($storeId); -+ } -+ -+ if ($attributeSetId) { -+ $product->setAttributeSetId($attributeSetId); -+ } -+ - return $product; - } - } -diff --git a/app/code/Magento/Catalog/Controller/Adminhtml/Product/Crosssell.php b/app/code/Magento/Catalog/Controller/Adminhtml/Product/Crosssell.php -index fa94a2fb7be..e51d3ffe94a 100644 ---- a/app/code/Magento/Catalog/Controller/Adminhtml/Product/Crosssell.php -+++ b/app/code/Magento/Catalog/Controller/Adminhtml/Product/Crosssell.php -@@ -6,7 +6,17 @@ - */ - namespace Magento\Catalog\Controller\Adminhtml\Product; - --class Crosssell extends \Magento\Catalog\Controller\Adminhtml\Product -+use Magento\Catalog\Controller\Adminhtml\Product; -+use Magento\Framework\App\Action\HttpPostActionInterface; -+ -+/** -+ * Class Crosssell -+ * -+ * @package Magento\Catalog\Controller\Adminhtml\Product -+ * @deprecated Not used since cross-sell products grid moved to UI components. -+ * @see Magento_Catalog::view/adminhtml/ui_component/crosssell_product_listing.xml -+ */ -+class Crosssell extends Product implements HttpPostActionInterface - { - /** - * @var \Magento\Framework\View\Result\LayoutFactory -diff --git a/app/code/Magento/Catalog/Controller/Adminhtml/Product/CrosssellGrid.php b/app/code/Magento/Catalog/Controller/Adminhtml/Product/CrosssellGrid.php -index daaf2c21ee2..5039d0c052b 100644 ---- a/app/code/Magento/Catalog/Controller/Adminhtml/Product/CrosssellGrid.php -+++ b/app/code/Magento/Catalog/Controller/Adminhtml/Product/CrosssellGrid.php -@@ -6,7 +6,17 @@ - */ - namespace Magento\Catalog\Controller\Adminhtml\Product; - --class CrosssellGrid extends \Magento\Catalog\Controller\Adminhtml\Product -+use Magento\Catalog\Controller\Adminhtml\Product; -+use Magento\Framework\App\Action\HttpPostActionInterface; -+ -+/** -+ * Class CrosssellGrid -+ * -+ * @package Magento\Catalog\Controller\Adminhtml\Product -+ * @deprecated Not used since cross-sell products grid moved to UI components. -+ * @see Magento_Catalog::view/adminhtml/ui_component/crosssell_product_listing.xml -+ */ -+class CrosssellGrid extends Product implements HttpPostActionInterface - { - /** - * @var \Magento\Framework\View\Result\LayoutFactory -diff --git a/app/code/Magento/Catalog/Controller/Adminhtml/Product/Edit.php b/app/code/Magento/Catalog/Controller/Adminhtml/Product/Edit.php -index 1b9316a95ad..c31ceabcda6 100644 ---- a/app/code/Magento/Catalog/Controller/Adminhtml/Product/Edit.php -+++ b/app/code/Magento/Catalog/Controller/Adminhtml/Product/Edit.php -@@ -6,7 +6,9 @@ - */ - namespace Magento\Catalog\Controller\Adminhtml\Product; - --class Edit extends \Magento\Catalog\Controller\Adminhtml\Product -+use Magento\Framework\App\Action\HttpGetActionInterface as HttpGetActionInterface; -+ -+class Edit extends \Magento\Catalog\Controller\Adminhtml\Product implements HttpGetActionInterface - { - /** - * Array of actions which can be processed without secret key validation -diff --git a/app/code/Magento/Catalog/Controller/Adminhtml/Product/Gallery/Upload.php b/app/code/Magento/Catalog/Controller/Adminhtml/Product/Gallery/Upload.php -index b5660ea8793..ff7311e9317 100644 ---- a/app/code/Magento/Catalog/Controller/Adminhtml/Product/Gallery/Upload.php -+++ b/app/code/Magento/Catalog/Controller/Adminhtml/Product/Gallery/Upload.php -@@ -6,9 +6,10 @@ - */ - namespace Magento\Catalog\Controller\Adminhtml\Product\Gallery; - -+use Magento\Framework\App\Action\HttpPostActionInterface as HttpPostActionInterface; - use Magento\Framework\App\Filesystem\DirectoryList; - --class Upload extends \Magento\Backend\App\Action -+class Upload extends \Magento\Backend\App\Action implements HttpPostActionInterface - { - /** - * Authorization level of a basic admin session -diff --git a/app/code/Magento/Catalog/Controller/Adminhtml/Product/GridOnly.php b/app/code/Magento/Catalog/Controller/Adminhtml/Product/GridOnly.php -index 40e62895caf..51aaa8c178e 100644 ---- a/app/code/Magento/Catalog/Controller/Adminhtml/Product/GridOnly.php -+++ b/app/code/Magento/Catalog/Controller/Adminhtml/Product/GridOnly.php -@@ -1,12 +1,16 @@ - <?php - /** -- * - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ - namespace Magento\Catalog\Controller\Adminhtml\Product; - --class GridOnly extends \Magento\Catalog\Controller\Adminhtml\Product -+use Magento\Framework\App\Action\HttpGetActionInterface; -+ -+/** -+ * Get specified tab grid controller. -+ */ -+class GridOnly extends \Magento\Catalog\Controller\Adminhtml\Product implements HttpGetActionInterface - { - /** - * @var \Magento\Framework\Controller\Result\RawFactory -@@ -47,7 +51,7 @@ class GridOnly extends \Magento\Catalog\Controller\Adminhtml\Product - $this->productBuilder->build($this->getRequest()); - - $block = $this->getRequest()->getParam('gridOnlyBlock'); -- $blockClassSuffix = str_replace(' ', '_', ucwords(str_replace('_', ' ', $block))); -+ $blockClassSuffix = ucwords($block, '_'); - - /** @var \Magento\Framework\Controller\Result\Raw $resultRaw */ - $resultRaw = $this->resultRawFactory->create(); -diff --git a/app/code/Magento/Catalog/Controller/Adminhtml/Product/Index.php b/app/code/Magento/Catalog/Controller/Adminhtml/Product/Index.php -index ea66ecf6b16..7755a512eb9 100644 ---- a/app/code/Magento/Catalog/Controller/Adminhtml/Product/Index.php -+++ b/app/code/Magento/Catalog/Controller/Adminhtml/Product/Index.php -@@ -6,7 +6,9 @@ - */ - namespace Magento\Catalog\Controller\Adminhtml\Product; - --class Index extends \Magento\Catalog\Controller\Adminhtml\Product -+use Magento\Framework\App\Action\HttpGetActionInterface as HttpGetActionInterface; -+ -+class Index extends \Magento\Catalog\Controller\Adminhtml\Product implements HttpGetActionInterface - { - /** - * @var \Magento\Framework\View\Result\PageFactory -diff --git a/app/code/Magento/Catalog/Controller/Adminhtml/Product/Initialization/Helper.php b/app/code/Magento/Catalog/Controller/Adminhtml/Product/Initialization/Helper.php -index d82f4a04fb2..f11d16755ef 100644 ---- a/app/code/Magento/Catalog/Controller/Adminhtml/Product/Initialization/Helper.php -+++ b/app/code/Magento/Catalog/Controller/Adminhtml/Product/Initialization/Helper.php -@@ -19,6 +19,8 @@ use Magento\Framework\App\ObjectManager; - use Magento\Catalog\Controller\Adminhtml\Product\Initialization\Helper\AttributeFilter; - - /** -+ * Product helper -+ * - * @api - * @SuppressWarnings(PHPMD.CouplingBetweenObjects) - * @since 100.0.2 -@@ -365,6 +367,8 @@ class Helper - } - - /** -+ * Get link resolver instance -+ * - * @return LinkResolver - * @deprecated 101.0.0 - */ -@@ -377,6 +381,8 @@ class Helper - } - - /** -+ * Get DateTimeFilter instance -+ * - * @return \Magento\Framework\Stdlib\DateTime\Filter\DateTime - * @deprecated 101.0.0 - */ -@@ -391,6 +397,7 @@ class Helper - - /** - * Remove ids of non selected websites from $websiteIds array and return filtered data -+ * - * $websiteIds parameter expects array with website ids as keys and 1 (selected) or 0 (non selected) as values - * Only one id (default website ID) will be set to $websiteIds array when the single store mode is turned on - * -@@ -463,6 +470,7 @@ class Helper - private function convertSpecialFromDateStringToObject($productData) - { - if (isset($productData['special_from_date']) && $productData['special_from_date'] != '') { -+ $productData['special_from_date'] = $this->getDateTimeFilter()->filter($productData['special_from_date']); - $productData['special_from_date'] = new \DateTime($productData['special_from_date']); - } - -diff --git a/app/code/Magento/Catalog/Controller/Adminhtml/Product/Initialization/Helper/AttributeFilter.php b/app/code/Magento/Catalog/Controller/Adminhtml/Product/Initialization/Helper/AttributeFilter.php -index 188b0b22f33..49165c85f85 100644 ---- a/app/code/Magento/Catalog/Controller/Adminhtml/Product/Initialization/Helper/AttributeFilter.php -+++ b/app/code/Magento/Catalog/Controller/Adminhtml/Product/Initialization/Helper/AttributeFilter.php -@@ -46,6 +46,8 @@ class AttributeFilter - } - - /** -+ * Reset "Use Config Settings" to false in product data. -+ * - * @param Product $product - * @param string $attributeCode - * @param array $productData -@@ -62,6 +64,8 @@ class AttributeFilter - } - - /** -+ * Prepare default attribute data for product. -+ * - * @param array $attributeList - * @param string $attributeCode - * @param array $productData -@@ -73,7 +77,7 @@ class AttributeFilter - /** @var \Magento\Catalog\Model\ResourceModel\Eav\Attribute $attribute */ - $attribute = $attributeList[$attributeCode]; - $attributeType = $attribute->getBackendType(); -- // For non-numberic types set the attributeValue to 'false' to trigger their removal from the db -+ // For non-numeric types set the attributeValue to 'false' to trigger their removal from the db - if ($attributeType === 'varchar' || $attributeType === 'text' || $attributeType === 'datetime') { - $attribute->setIsRequired(false); - $productData[$attributeCode] = false; -@@ -86,6 +90,8 @@ class AttributeFilter - } - - /** -+ * Check, whether attribute should not be updated. -+ * - * @param Product $product - * @param array $useDefaults - * @param string $attribute -diff --git a/app/code/Magento/Catalog/Controller/Adminhtml/Product/MassDelete.php b/app/code/Magento/Catalog/Controller/Adminhtml/Product/MassDelete.php -index f32c6edd573..8fceba3c45e 100644 ---- a/app/code/Magento/Catalog/Controller/Adminhtml/Product/MassDelete.php -+++ b/app/code/Magento/Catalog/Controller/Adminhtml/Product/MassDelete.php -@@ -6,13 +6,14 @@ - */ - namespace Magento\Catalog\Controller\Adminhtml\Product; - -+use Magento\Framework\App\Action\HttpPostActionInterface as HttpPostActionInterface; - use Magento\Framework\Controller\ResultFactory; - use Magento\Backend\App\Action\Context; - use Magento\Ui\Component\MassAction\Filter; - use Magento\Catalog\Model\ResourceModel\Product\CollectionFactory; - use Magento\Catalog\Api\ProductRepositoryInterface; - --class MassDelete extends \Magento\Catalog\Controller\Adminhtml\Product -+class MassDelete extends \Magento\Catalog\Controller\Adminhtml\Product implements HttpPostActionInterface - { - /** - * Massactions filter -diff --git a/app/code/Magento/Catalog/Controller/Adminhtml/Product/MassStatus.php b/app/code/Magento/Catalog/Controller/Adminhtml/Product/MassStatus.php -index e3623aabfa1..9d7273fb3f2 100644 ---- a/app/code/Magento/Catalog/Controller/Adminhtml/Product/MassStatus.php -+++ b/app/code/Magento/Catalog/Controller/Adminhtml/Product/MassStatus.php -@@ -6,6 +6,7 @@ - */ - namespace Magento\Catalog\Controller\Adminhtml\Product; - -+use Magento\Framework\App\Action\HttpPostActionInterface as HttpPostActionInterface; - use Magento\Backend\App\Action; - use Magento\Catalog\Controller\Adminhtml\Product; - use Magento\Framework\Controller\ResultFactory; -@@ -13,9 +14,10 @@ use Magento\Ui\Component\MassAction\Filter; - use Magento\Catalog\Model\ResourceModel\Product\CollectionFactory; - - /** -+ * Updates status for a batch of products. - * @SuppressWarnings(PHPMD.CouplingBetweenObjects) - */ --class MassStatus extends \Magento\Catalog\Controller\Adminhtml\Product -+class MassStatus extends \Magento\Catalog\Controller\Adminhtml\Product implements HttpPostActionInterface - { - /** - * @var \Magento\Catalog\Model\Indexer\Product\Price\Processor -@@ -86,7 +88,7 @@ class MassStatus extends \Magento\Catalog\Controller\Adminhtml\Product - $filterRequest = $this->getRequest()->getParam('filters', null); - $status = (int) $this->getRequest()->getParam('status'); - -- if (null !== $storeId && null !== $filterRequest) { -+ if (null === $storeId && null !== $filterRequest) { - $storeId = (isset($filterRequest['store_id'])) ? (int) $filterRequest['store_id'] : 0; - } - -diff --git a/app/code/Magento/Catalog/Controller/Adminhtml/Product/NewAction.php b/app/code/Magento/Catalog/Controller/Adminhtml/Product/NewAction.php -index 0b027105cd7..0b1ef98c386 100644 ---- a/app/code/Magento/Catalog/Controller/Adminhtml/Product/NewAction.php -+++ b/app/code/Magento/Catalog/Controller/Adminhtml/Product/NewAction.php -@@ -6,11 +6,12 @@ - */ - namespace Magento\Catalog\Controller\Adminhtml\Product; - -+use Magento\Framework\App\Action\HttpGetActionInterface as HttpGetActionInterface; - use Magento\Backend\App\Action; - use Magento\Catalog\Controller\Adminhtml\Product; - use Magento\Framework\App\ObjectManager; - --class NewAction extends \Magento\Catalog\Controller\Adminhtml\Product -+class NewAction extends \Magento\Catalog\Controller\Adminhtml\Product implements HttpGetActionInterface - { - /** - * @var Initialization\StockDataFilter -diff --git a/app/code/Magento/Catalog/Controller/Adminhtml/Product/Related.php b/app/code/Magento/Catalog/Controller/Adminhtml/Product/Related.php -index 4bc15d14a29..f54f8d469c3 100644 ---- a/app/code/Magento/Catalog/Controller/Adminhtml/Product/Related.php -+++ b/app/code/Magento/Catalog/Controller/Adminhtml/Product/Related.php -@@ -7,7 +7,17 @@ - */ - namespace Magento\Catalog\Controller\Adminhtml\Product; - --class Related extends \Magento\Catalog\Controller\Adminhtml\Product -+use Magento\Catalog\Controller\Adminhtml\Product; -+use Magento\Framework\App\Action\HttpPostActionInterface; -+ -+/** -+ * Class Related -+ * -+ * @package Magento\Catalog\Controller\Adminhtml\Product -+ * @deprecated Not used since related products grid moved to UI components. -+ * @see Magento_Catalog::view/adminhtml/ui_component/related_product_listing.xml -+ */ -+class Related extends Product implements HttpPostActionInterface - { - /** - * @var \Magento\Framework\View\Result\LayoutFactory -@@ -29,6 +39,8 @@ class Related extends \Magento\Catalog\Controller\Adminhtml\Product - } - - /** -+ * Execute -+ * - * @return \Magento\Framework\View\Result\Layout - */ - public function execute() -diff --git a/app/code/Magento/Catalog/Controller/Adminhtml/Product/RelatedGrid.php b/app/code/Magento/Catalog/Controller/Adminhtml/Product/RelatedGrid.php -index b2fc7ebe1eb..b1092bba0d3 100644 ---- a/app/code/Magento/Catalog/Controller/Adminhtml/Product/RelatedGrid.php -+++ b/app/code/Magento/Catalog/Controller/Adminhtml/Product/RelatedGrid.php -@@ -7,6 +7,15 @@ - */ - namespace Magento\Catalog\Controller\Adminhtml\Product; - --class RelatedGrid extends Related -+use Magento\Framework\App\Action\HttpPostActionInterface; -+ -+/** -+ * Class RelatedGrid -+ * -+ * @package Magento\Catalog\Controller\Adminhtml\Product -+ * @deprecated Not used since related products grid moved to UI components. -+ * @see Magento_Catalog::view/adminhtml/ui_component/related_product_listing.xml -+ */ -+class RelatedGrid extends Related implements HttpPostActionInterface - { - } -diff --git a/app/code/Magento/Catalog/Controller/Adminhtml/Product/Reload.php b/app/code/Magento/Catalog/Controller/Adminhtml/Product/Reload.php -index ff87e7f5741..a0963e60d88 100644 ---- a/app/code/Magento/Catalog/Controller/Adminhtml/Product/Reload.php -+++ b/app/code/Magento/Catalog/Controller/Adminhtml/Product/Reload.php -@@ -5,12 +5,13 @@ - */ - namespace Magento\Catalog\Controller\Adminhtml\Product; - -+use Magento\Framework\App\Action\HttpPostActionInterface as HttpPostActionInterface; - use Magento\Framework\Controller\ResultFactory; - - /** - * Backend reload of product create/edit form - */ --class Reload extends \Magento\Catalog\Controller\Adminhtml\Product -+class Reload extends \Magento\Catalog\Controller\Adminhtml\Product implements HttpPostActionInterface - { - /** - * {@inheritdoc} -diff --git a/app/code/Magento/Catalog/Controller/Adminhtml/Product/Save.php b/app/code/Magento/Catalog/Controller/Adminhtml/Product/Save.php -index ff3ce60d927..825d0ee032d 100644 ---- a/app/code/Magento/Catalog/Controller/Adminhtml/Product/Save.php -+++ b/app/code/Magento/Catalog/Controller/Adminhtml/Product/Save.php -@@ -7,7 +7,9 @@ - - namespace Magento\Catalog\Controller\Adminhtml\Product; - -+use Magento\Framework\App\Action\HttpPostActionInterface as HttpPostActionInterface; - use Magento\Backend\App\Action; -+use Magento\Catalog\Api\Data\ProductInterface; - use Magento\Catalog\Controller\Adminhtml\Product; - use Magento\Store\Model\StoreManagerInterface; - use Magento\Framework\App\Request\DataPersistorInterface; -@@ -16,7 +18,7 @@ use Magento\Framework\App\Request\DataPersistorInterface; - * Class Save - * @SuppressWarnings(PHPMD.CouplingBetweenObjects) - */ --class Save extends \Magento\Catalog\Controller\Adminhtml\Product -+class Save extends \Magento\Catalog\Controller\Adminhtml\Product implements HttpPostActionInterface - { - /** - * @var Initialization\Helper -@@ -53,6 +55,16 @@ class Save extends \Magento\Catalog\Controller\Adminhtml\Product - */ - private $storeManager; - -+ /** -+ * @var \Magento\Framework\Escaper|null -+ */ -+ private $escaper; -+ -+ /** -+ * @var null|\Psr\Log\LoggerInterface -+ */ -+ private $logger; -+ - /** - * Save constructor. - * -@@ -62,6 +74,8 @@ class Save extends \Magento\Catalog\Controller\Adminhtml\Product - * @param \Magento\Catalog\Model\Product\Copier $productCopier - * @param \Magento\Catalog\Model\Product\TypeTransitionManager $productTypeManager - * @param \Magento\Catalog\Api\ProductRepositoryInterface $productRepository -+ * @param \Magento\Framework\Escaper|null $escaper -+ * @param \Psr\Log\LoggerInterface|null $logger - */ - public function __construct( - \Magento\Backend\App\Action\Context $context, -@@ -69,13 +83,17 @@ class Save extends \Magento\Catalog\Controller\Adminhtml\Product - Initialization\Helper $initializationHelper, - \Magento\Catalog\Model\Product\Copier $productCopier, - \Magento\Catalog\Model\Product\TypeTransitionManager $productTypeManager, -- \Magento\Catalog\Api\ProductRepositoryInterface $productRepository -+ \Magento\Catalog\Api\ProductRepositoryInterface $productRepository, -+ \Magento\Framework\Escaper $escaper = null, -+ \Psr\Log\LoggerInterface $logger = null - ) { - $this->initializationHelper = $initializationHelper; - $this->productCopier = $productCopier; - $this->productTypeManager = $productTypeManager; - $this->productRepository = $productRepository; - parent::__construct($context, $productBuilder); -+ $this->escaper = $escaper ?? $this->_objectManager->get(\Magento\Framework\Escaper::class); -+ $this->logger = $logger ?? $this->_objectManager->get(\Psr\Log\LoggerInterface::class); - } - - /** -@@ -102,7 +120,6 @@ class Save extends \Magento\Catalog\Controller\Adminhtml\Product - $this->productBuilder->build($this->getRequest()) - ); - $this->productTypeManager->processProduct($product); -- - if (isset($data['product'][$product->getIdFieldName()])) { - throw new \Magento\Framework\Exception\LocalizedException( - __('The product was unable to be saved. Please try again.') -@@ -110,6 +127,7 @@ class Save extends \Magento\Catalog\Controller\Adminhtml\Product - } - - $originalSku = $product->getSku(); -+ $canSaveCustomOptions = $product->getCanSaveCustomOptions(); - $product->save(); - $this->handleImageRemoveError($data, $product->getId()); - $this->getCategoryLinkManagement()->assignProductToCategories( -@@ -119,21 +137,17 @@ class Save extends \Magento\Catalog\Controller\Adminhtml\Product - $productId = $product->getEntityId(); - $productAttributeSetId = $product->getAttributeSetId(); - $productTypeId = $product->getTypeId(); -- -- $this->copyToStores($data, $productId); -- -+ $extendedData = $data; -+ $extendedData['can_save_custom_options'] = $canSaveCustomOptions; -+ $this->copyToStores($extendedData, $productId); - $this->messageManager->addSuccessMessage(__('You saved the product.')); - $this->getDataPersistor()->clear('catalog_product'); - if ($product->getSku() != $originalSku) { - $this->messageManager->addNoticeMessage( - __( - 'SKU for product %1 has been changed to %2.', -- $this->_objectManager->get( -- \Magento\Framework\Escaper::class -- )->escapeHtml($product->getName()), -- $this->_objectManager->get( -- \Magento\Framework\Escaper::class -- )->escapeHtml($product->getSku()) -+ $this->escaper->escapeHtml($product->getName()), -+ $this->escaper->escapeHtml($product->getSku()) - ) - ); - } -@@ -143,17 +157,21 @@ class Save extends \Magento\Catalog\Controller\Adminhtml\Product - ); - - if ($redirectBack === 'duplicate') { -+ $product->unsetData('quantity_and_stock_status'); - $newProduct = $this->productCopier->copy($product); -+ $this->checkUniqueAttributes($product); - $this->messageManager->addSuccessMessage(__('You duplicated the product.')); - } - } catch (\Magento\Framework\Exception\LocalizedException $e) { -- $this->_objectManager->get(\Psr\Log\LoggerInterface::class)->critical($e); -+ $this->logger->critical($e); - $this->messageManager->addExceptionMessage($e); -+ $data = isset($product) ? $this->persistMediaData($product, $data) : $data; - $this->getDataPersistor()->set('catalog_product', $data); - $redirectBack = $productId ? true : 'new'; - } catch (\Exception $e) { -- $this->_objectManager->get(\Psr\Log\LoggerInterface::class)->critical($e); -+ $this->logger->critical($e); - $this->messageManager->addErrorMessage($e->getMessage()); -+ $data = isset($product) ? $this->persistMediaData($product, $data) : $data; - $this->getDataPersistor()->set('catalog_product', $data); - $redirectBack = $productId ? true : 'new'; - } -@@ -186,6 +204,7 @@ class Save extends \Magento\Catalog\Controller\Adminhtml\Product - - /** - * Notify customer when image was not deleted in specific case. -+ * - * TODO: temporary workaround must be eliminated in MAGETWO-45306 - * - * @param array $postData -@@ -239,6 +258,7 @@ class Save extends \Magento\Catalog\Controller\Adminhtml\Product - ->setStoreId($copyFrom) - ->load($productId) - ->setStoreId($copyTo) -+ ->setCanSaveCustomOptions($data['can_save_custom_options']) - ->setCopyFromView(true) - ->save(); - } -@@ -250,6 +270,8 @@ class Save extends \Magento\Catalog\Controller\Adminhtml\Product - } - - /** -+ * Get categoryLinkManagement in a backward compatible way. -+ * - * @return \Magento\Catalog\Api\CategoryLinkManagementInterface - */ - private function getCategoryLinkManagement() -@@ -262,6 +284,8 @@ class Save extends \Magento\Catalog\Controller\Adminhtml\Product - } - - /** -+ * Get storeManager in a backward compatible way. -+ * - * @return StoreManagerInterface - * @deprecated 101.0.0 - */ -@@ -288,4 +312,57 @@ class Save extends \Magento\Catalog\Controller\Adminhtml\Product - - return $this->dataPersistor; - } -+ -+ /** -+ * Persist media gallery on error, in order to show already saved images on next run. -+ * -+ * @param ProductInterface $product -+ * @param array $data -+ * @return array -+ */ -+ private function persistMediaData(ProductInterface $product, array $data) -+ { -+ $mediaGallery = $product->getData('media_gallery'); -+ if (!empty($mediaGallery['images'])) { -+ foreach ($mediaGallery['images'] as $key => $image) { -+ if (!isset($image['new_file'])) { -+ //Remove duplicates. -+ unset($mediaGallery['images'][$key]); -+ } -+ } -+ $data['product']['media_gallery'] = $mediaGallery; -+ $fields = [ -+ 'image', -+ 'small_image', -+ 'thumbnail', -+ 'swatch_image', -+ ]; -+ foreach ($fields as $field) { -+ $data['product'][$field] = $product->getData($field); -+ } -+ } -+ -+ return $data; -+ } -+ -+ /** -+ * Check unique attributes and add error to message manager -+ * -+ * @param \Magento\Catalog\Model\Product $product -+ */ -+ private function checkUniqueAttributes(\Magento\Catalog\Model\Product $product) -+ { -+ $uniqueLabels = []; -+ foreach ($product->getAttributes() as $attribute) { -+ if ($attribute->getIsUnique() && $attribute->getIsUserDefined() -+ && $product->getData($attribute->getAttributeCode()) !== null -+ ) { -+ $uniqueLabels[] = $attribute->getDefaultFrontendLabel(); -+ } -+ } -+ if ($uniqueLabels) { -+ $uniqueLabels = implode('", "', $uniqueLabels); -+ $this->messageManager->addErrorMessage(__('The value of attribute(s) "%1" must be unique', $uniqueLabels)); -+ } -+ } - } -diff --git a/app/code/Magento/Catalog/Controller/Adminhtml/Product/Search.php b/app/code/Magento/Catalog/Controller/Adminhtml/Product/Search.php -index c7c71b2f560..316983298a1 100644 ---- a/app/code/Magento/Catalog/Controller/Adminhtml/Product/Search.php -+++ b/app/code/Magento/Catalog/Controller/Adminhtml/Product/Search.php -@@ -9,11 +9,12 @@ declare(strict_types=1); - namespace Magento\Catalog\Controller\Adminhtml\Product; - - use Magento\Catalog\Api\Data\ProductInterface; -+use Magento\Framework\App\Action\HttpGetActionInterface; - - /** - * Controller to search product for ui-select component - */ --class Search extends \Magento\Backend\App\Action -+class Search extends \Magento\Backend\App\Action implements HttpGetActionInterface - { - /** - * Authorization level of a basic admin session -@@ -48,6 +49,8 @@ class Search extends \Magento\Backend\App\Action - } - - /** -+ * Execute product search. -+ * - * @return \Magento\Framework\Controller\ResultInterface - */ - public function execute() : \Magento\Framework\Controller\ResultInterface -diff --git a/app/code/Magento/Catalog/Controller/Adminhtml/Product/Set/Add.php b/app/code/Magento/Catalog/Controller/Adminhtml/Product/Set/Add.php -index 480c30322a0..bfe474abba1 100644 ---- a/app/code/Magento/Catalog/Controller/Adminhtml/Product/Set/Add.php -+++ b/app/code/Magento/Catalog/Controller/Adminhtml/Product/Set/Add.php -@@ -6,7 +6,9 @@ - */ - namespace Magento\Catalog\Controller\Adminhtml\Product\Set; - --class Add extends \Magento\Catalog\Controller\Adminhtml\Product\Set -+use Magento\Framework\App\Action\HttpGetActionInterface as HttpGetActionInterface; -+ -+class Add extends \Magento\Catalog\Controller\Adminhtml\Product\Set implements HttpGetActionInterface - { - /** - * @var \Magento\Framework\View\Result\PageFactory -diff --git a/app/code/Magento/Catalog/Controller/Adminhtml/Product/Set/Delete.php b/app/code/Magento/Catalog/Controller/Adminhtml/Product/Set/Delete.php -index f2695311732..771cc83f79e 100644 ---- a/app/code/Magento/Catalog/Controller/Adminhtml/Product/Set/Delete.php -+++ b/app/code/Magento/Catalog/Controller/Adminhtml/Product/Set/Delete.php -@@ -6,7 +6,9 @@ - */ - namespace Magento\Catalog\Controller\Adminhtml\Product\Set; - --class Delete extends \Magento\Catalog\Controller\Adminhtml\Product\Set -+use Magento\Framework\App\Action\HttpPostActionInterface; -+ -+class Delete extends \Magento\Catalog\Controller\Adminhtml\Product\Set implements HttpPostActionInterface - { - /** - * @var \Magento\Eav\Api\AttributeSetRepositoryInterface -diff --git a/app/code/Magento/Catalog/Controller/Adminhtml/Product/Set/Edit.php b/app/code/Magento/Catalog/Controller/Adminhtml/Product/Set/Edit.php -index ec540180b03..6f6870cb084 100644 ---- a/app/code/Magento/Catalog/Controller/Adminhtml/Product/Set/Edit.php -+++ b/app/code/Magento/Catalog/Controller/Adminhtml/Product/Set/Edit.php -@@ -6,7 +6,9 @@ - */ - namespace Magento\Catalog\Controller\Adminhtml\Product\Set; - --class Edit extends \Magento\Catalog\Controller\Adminhtml\Product\Set -+use Magento\Framework\App\Action\HttpGetActionInterface as HttpGetActionInterface; -+ -+class Edit extends \Magento\Catalog\Controller\Adminhtml\Product\Set implements HttpGetActionInterface - { - /** - * @var \Magento\Framework\View\Result\PageFactory -diff --git a/app/code/Magento/Catalog/Controller/Adminhtml/Product/Set/Index.php b/app/code/Magento/Catalog/Controller/Adminhtml/Product/Set/Index.php -index 29f7dff4f0d..aadf724f600 100644 ---- a/app/code/Magento/Catalog/Controller/Adminhtml/Product/Set/Index.php -+++ b/app/code/Magento/Catalog/Controller/Adminhtml/Product/Set/Index.php -@@ -6,7 +6,9 @@ - */ - namespace Magento\Catalog\Controller\Adminhtml\Product\Set; - --class Index extends \Magento\Catalog\Controller\Adminhtml\Product\Set -+use Magento\Framework\App\Action\HttpGetActionInterface as HttpGetActionInterface; -+ -+class Index extends \Magento\Catalog\Controller\Adminhtml\Product\Set implements HttpGetActionInterface - { - /** - * @var \Magento\Framework\View\Result\PageFactory -diff --git a/app/code/Magento/Catalog/Controller/Adminhtml/Product/Set/Save.php b/app/code/Magento/Catalog/Controller/Adminhtml/Product/Set/Save.php -index c5dd9ce6d8e..83620de25b0 100644 ---- a/app/code/Magento/Catalog/Controller/Adminhtml/Product/Set/Save.php -+++ b/app/code/Magento/Catalog/Controller/Adminhtml/Product/Set/Save.php -@@ -6,12 +6,13 @@ - */ - namespace Magento\Catalog\Controller\Adminhtml\Product\Set; - -+use Magento\Framework\App\Action\HttpPostActionInterface as HttpPostActionInterface; - use Magento\Framework\App\ObjectManager; - - /** - * @SuppressWarnings(PHPMD.CouplingBetweenObjects) - */ --class Save extends \Magento\Catalog\Controller\Adminhtml\Product\Set -+class Save extends \Magento\Catalog\Controller\Adminhtml\Product\Set implements HttpPostActionInterface - { - /** - * @var \Magento\Framework\View\LayoutFactory -diff --git a/app/code/Magento/Catalog/Controller/Adminhtml/Product/Upsell.php b/app/code/Magento/Catalog/Controller/Adminhtml/Product/Upsell.php -index 614bddd0ebc..1cec8e86787 100644 ---- a/app/code/Magento/Catalog/Controller/Adminhtml/Product/Upsell.php -+++ b/app/code/Magento/Catalog/Controller/Adminhtml/Product/Upsell.php -@@ -6,7 +6,17 @@ - */ - namespace Magento\Catalog\Controller\Adminhtml\Product; - --class Upsell extends \Magento\Catalog\Controller\Adminhtml\Product -+use Magento\Catalog\Controller\Adminhtml\Product; -+use Magento\Framework\App\Action\HttpPostActionInterface; -+ -+/** -+ * Class Upsell -+ * -+ * @package Magento\Catalog\Controller\Adminhtml\Product -+ * @deprecated Not used since upsell products grid moved to UI components. -+ * @see Magento_Catalog::view/adminhtml/ui_component/upsell_product_listing.xml -+ */ -+class Upsell extends Product implements HttpPostActionInterface - { - /** - * @var \Magento\Framework\View\Result\LayoutFactory -diff --git a/app/code/Magento/Catalog/Controller/Adminhtml/Product/UpsellGrid.php b/app/code/Magento/Catalog/Controller/Adminhtml/Product/UpsellGrid.php -index 50beb588b15..581531e7c93 100644 ---- a/app/code/Magento/Catalog/Controller/Adminhtml/Product/UpsellGrid.php -+++ b/app/code/Magento/Catalog/Controller/Adminhtml/Product/UpsellGrid.php -@@ -6,7 +6,17 @@ - */ - namespace Magento\Catalog\Controller\Adminhtml\Product; - --class UpsellGrid extends \Magento\Catalog\Controller\Adminhtml\Product -+use Magento\Catalog\Controller\Adminhtml\Product; -+use Magento\Framework\App\Action\HttpPostActionInterface; -+ -+/** -+ * Class UpsellGrid -+ * -+ * @package Magento\Catalog\Controller\Adminhtml\Product -+ * @deprecated Not used since upsell products grid moved to UI components. -+ * @see Magento_Catalog::view/adminhtml/ui_component/upsell_product_listing.xml -+ */ -+class UpsellGrid extends Product implements HttpPostActionInterface - { - /** - * @var \Magento\Framework\View\Result\LayoutFactory -diff --git a/app/code/Magento/Catalog/Controller/Adminhtml/Product/Validate.php b/app/code/Magento/Catalog/Controller/Adminhtml/Product/Validate.php -index e131bfe38c5..77c9cfcd40f 100644 ---- a/app/code/Magento/Catalog/Controller/Adminhtml/Product/Validate.php -+++ b/app/code/Magento/Catalog/Controller/Adminhtml/Product/Validate.php -@@ -6,6 +6,8 @@ - */ - namespace Magento\Catalog\Controller\Adminhtml\Product; - -+use Magento\Framework\App\Action\HttpGetActionInterface; -+use Magento\Framework\App\Action\HttpPostActionInterface as HttpPostActionInterface; - use Magento\Backend\App\Action; - use Magento\Catalog\Controller\Adminhtml\Product; - use Magento\Framework\App\ObjectManager; -@@ -16,7 +18,7 @@ use Magento\Store\Model\StoreManagerInterface; - * - * @SuppressWarnings(PHPMD.CouplingBetweenObjects) - */ --class Validate extends \Magento\Catalog\Controller\Adminhtml\Product -+class Validate extends Product implements HttpPostActionInterface, HttpGetActionInterface - { - /** - * @var \Magento\Framework\Stdlib\DateTime\Filter\Date -diff --git a/app/code/Magento/Catalog/Controller/Category/View.php b/app/code/Magento/Catalog/Controller/Category/View.php -index 226e5725050..da3d99a8d27 100644 ---- a/app/code/Magento/Catalog/Controller/Category/View.php -+++ b/app/code/Magento/Catalog/Controller/Category/View.php -@@ -7,53 +7,73 @@ - namespace Magento\Catalog\Controller\Category; - - use Magento\Catalog\Api\CategoryRepositoryInterface; -+use Magento\Catalog\Model\Category; -+use Magento\Catalog\Model\Design; - use Magento\Catalog\Model\Layer\Resolver; -+use Magento\Catalog\Model\Product\ProductList\ToolbarMemorizer; -+use Magento\Catalog\Model\Session; -+use Magento\CatalogUrlRewrite\Model\CategoryUrlPathGenerator; -+use Magento\Framework\App\Action\Action; -+use Magento\Framework\App\Action\Context; -+use Magento\Framework\App\Action\HttpGetActionInterface; -+use Magento\Framework\App\Action\HttpPostActionInterface; -+use Magento\Framework\App\ActionInterface; -+use Magento\Framework\Controller\Result\ForwardFactory; -+use Magento\Framework\Controller\ResultInterface; -+use Magento\Framework\DataObject; -+use Magento\Framework\Exception\LocalizedException; - use Magento\Framework\Exception\NoSuchEntityException; -+use Magento\Framework\Registry; -+use Magento\Framework\View\Result\Page; - use Magento\Framework\View\Result\PageFactory; -+use Magento\Store\Model\StoreManagerInterface; -+use Psr\Log\LoggerInterface; - - /** -+ * View a category on storefront. Needs to be accessible by POST because of the store switching. -+ * - * @SuppressWarnings(PHPMD.CouplingBetweenObjects) - */ --class View extends \Magento\Framework\App\Action\Action -+class View extends Action implements HttpGetActionInterface, HttpPostActionInterface - { - /** - * Core registry - * -- * @var \Magento\Framework\Registry -+ * @var Registry - */ - protected $_coreRegistry = null; - - /** - * Catalog session - * -- * @var \Magento\Catalog\Model\Session -+ * @var Session - */ - protected $_catalogSession; - - /** - * Catalog design - * -- * @var \Magento\Catalog\Model\Design -+ * @var Design - */ - protected $_catalogDesign; - - /** -- * @var \Magento\Store\Model\StoreManagerInterface -+ * @var StoreManagerInterface - */ - protected $_storeManager; - - /** -- * @var \Magento\CatalogUrlRewrite\Model\CategoryUrlPathGenerator -+ * @var CategoryUrlPathGenerator - */ - protected $categoryUrlPathGenerator; - - /** -- * @var \Magento\Framework\View\Result\PageFactory -+ * @var PageFactory - */ - protected $resultPageFactory; - - /** -- * @var \Magento\Framework\Controller\Result\ForwardFactory -+ * @var ForwardFactory - */ - protected $resultForwardFactory; - -@@ -69,32 +89,39 @@ class View extends \Magento\Framework\App\Action\Action - */ - protected $categoryRepository; - -+ /** -+ * @var ToolbarMemorizer -+ */ -+ private $toolbarMemorizer; -+ - /** - * Constructor - * -- * @param \Magento\Framework\App\Action\Context $context -- * @param \Magento\Catalog\Model\Design $catalogDesign -- * @param \Magento\Catalog\Model\Session $catalogSession -- * @param \Magento\Framework\Registry $coreRegistry -- * @param \Magento\Store\Model\StoreManagerInterface $storeManager -- * @param \Magento\CatalogUrlRewrite\Model\CategoryUrlPathGenerator $categoryUrlPathGenerator -- * @param \Magento\Framework\View\Result\PageFactory $resultPageFactory -- * @param \Magento\Framework\Controller\Result\ForwardFactory $resultForwardFactory -+ * @param Context $context -+ * @param Design $catalogDesign -+ * @param Session $catalogSession -+ * @param Registry $coreRegistry -+ * @param StoreManagerInterface $storeManager -+ * @param CategoryUrlPathGenerator $categoryUrlPathGenerator -+ * @param PageFactory $resultPageFactory -+ * @param ForwardFactory $resultForwardFactory - * @param Resolver $layerResolver - * @param CategoryRepositoryInterface $categoryRepository -+ * @param ToolbarMemorizer|null $toolbarMemorizer - * @SuppressWarnings(PHPMD.ExcessiveParameterList) - */ - public function __construct( -- \Magento\Framework\App\Action\Context $context, -- \Magento\Catalog\Model\Design $catalogDesign, -- \Magento\Catalog\Model\Session $catalogSession, -- \Magento\Framework\Registry $coreRegistry, -- \Magento\Store\Model\StoreManagerInterface $storeManager, -- \Magento\CatalogUrlRewrite\Model\CategoryUrlPathGenerator $categoryUrlPathGenerator, -+ Context $context, -+ Design $catalogDesign, -+ Session $catalogSession, -+ Registry $coreRegistry, -+ StoreManagerInterface $storeManager, -+ CategoryUrlPathGenerator $categoryUrlPathGenerator, - PageFactory $resultPageFactory, -- \Magento\Framework\Controller\Result\ForwardFactory $resultForwardFactory, -+ ForwardFactory $resultForwardFactory, - Resolver $layerResolver, -- CategoryRepositoryInterface $categoryRepository -+ CategoryRepositoryInterface $categoryRepository, -+ ToolbarMemorizer $toolbarMemorizer = null - ) { - parent::__construct($context); - $this->_storeManager = $storeManager; -@@ -106,12 +133,13 @@ class View extends \Magento\Framework\App\Action\Action - $this->resultForwardFactory = $resultForwardFactory; - $this->layerResolver = $layerResolver; - $this->categoryRepository = $categoryRepository; -+ $this->toolbarMemorizer = $toolbarMemorizer ?: $context->getObjectManager()->get(ToolbarMemorizer::class); - } - - /** - * Initialize requested category object - * -- * @return \Magento\Catalog\Model\Category|bool -+ * @return Category|bool - */ - protected function _initCategory() - { -@@ -130,13 +158,14 @@ class View extends \Magento\Framework\App\Action\Action - } - $this->_catalogSession->setLastVisitedCategoryId($category->getId()); - $this->_coreRegistry->register('current_category', $category); -+ $this->toolbarMemorizer->memorizeParams(); - try { - $this->_eventManager->dispatch( - 'catalog_controller_category_init_after', - ['category' => $category, 'controller_action' => $this] - ); -- } catch (\Magento\Framework\Exception\LocalizedException $e) { -- $this->_objectManager->get(\Psr\Log\LoggerInterface::class)->critical($e); -+ } catch (LocalizedException $e) { -+ $this->_objectManager->get(LoggerInterface::class)->critical($e); - return false; - } - -@@ -146,13 +175,12 @@ class View extends \Magento\Framework\App\Action\Action - /** - * Category view action - * -- * @return \Magento\Framework\Controller\ResultInterface -- * @SuppressWarnings(PHPMD.CyclomaticComplexity) -- * @SuppressWarnings(PHPMD.NPathComplexity) -+ * @return ResultInterface -+ * @throws NoSuchEntityException - */ - public function execute() - { -- if ($this->_request->getParam(\Magento\Framework\App\ActionInterface::PARAM_NAME_URL_ENCODED)) { -+ if ($this->_request->getParam(ActionInterface::PARAM_NAME_URL_ENCODED)) { - return $this->resultRedirectFactory->create()->setUrl($this->_redirect->getRedirectUrl()); - } - $category = $this->_initCategory(); -@@ -173,29 +201,18 @@ class View extends \Magento\Framework\App\Action\Action - $page->getConfig()->setPageLayout($settings->getPageLayout()); - } - -- $hasChildren = $category->hasChildren(); -- if ($category->getIsAnchor()) { -- $type = $hasChildren ? 'layered' : 'layered_without_children'; -- } else { -- $type = $hasChildren ? 'default' : 'default_without_children'; -- } -+ $pageType = $this->getPageType($category); - -- if (!$hasChildren) { -+ if (!$category->hasChildren()) { - // Two levels removed from parent. Need to add default page type. -- $parentType = strtok($type, '_'); -- $page->addPageLayoutHandles(['type' => $parentType], null, false); -+ $parentPageType = strtok($pageType, '_'); -+ $page->addPageLayoutHandles(['type' => $parentPageType], null, false); - } -- $page->addPageLayoutHandles(['type' => $type], null, false); -+ $page->addPageLayoutHandles(['type' => $pageType], null, false); - $page->addPageLayoutHandles(['id' => $category->getId()]); - - // apply custom layout update once layout is loaded -- $layoutUpdates = $settings->getLayoutUpdates(); -- if ($layoutUpdates && is_array($layoutUpdates)) { -- foreach ($layoutUpdates as $layoutUpdate) { -- $page->addUpdate($layoutUpdate); -- $page->addPageLayoutHandles(['layout_update' => md5($layoutUpdate)], null, false); -- } -- } -+ $this->applyLayoutUpdates($page, $settings); - - $page->getConfig()->addBodyClass('page-products') - ->addBodyClass('categorypath-' . $this->categoryUrlPathGenerator->getUrlPath($category)) -@@ -206,4 +223,40 @@ class View extends \Magento\Framework\App\Action\Action - return $this->resultForwardFactory->create()->forward('noroute'); - } - } -+ -+ /** -+ * Get page type based on category -+ * -+ * @param Category $category -+ * @return string -+ */ -+ private function getPageType(Category $category) : string -+ { -+ $hasChildren = $category->hasChildren(); -+ if ($category->getIsAnchor()) { -+ return $hasChildren ? 'layered' : 'layered_without_children'; -+ } -+ -+ return $hasChildren ? 'default' : 'default_without_children'; -+ } -+ -+ /** -+ * Apply custom layout updates -+ * -+ * @param Page $page -+ * @param DataObject $settings -+ * @return void -+ */ -+ private function applyLayoutUpdates( -+ Page $page, -+ DataObject $settings -+ ) { -+ $layoutUpdates = $settings->getLayoutUpdates(); -+ if ($layoutUpdates && is_array($layoutUpdates)) { -+ foreach ($layoutUpdates as $layoutUpdate) { -+ $page->addUpdate($layoutUpdate); -+ $page->addPageLayoutHandles(['layout_update' => sha1($layoutUpdate)], null, false); -+ } -+ } -+ } - } -diff --git a/app/code/Magento/Catalog/Controller/Index/Index.php b/app/code/Magento/Catalog/Controller/Index/Index.php -index eae3325df9f..bd00c972049 100644 ---- a/app/code/Magento/Catalog/Controller/Index/Index.php -+++ b/app/code/Magento/Catalog/Controller/Index/Index.php -@@ -5,12 +5,17 @@ - */ - namespace Magento\Catalog\Controller\Index; - --class Index extends \Magento\Framework\App\Action\Action -+use Magento\Framework\App\Action\HttpGetActionInterface; -+ -+/** -+ * Catalog index page controller. -+ */ -+class Index extends \Magento\Framework\App\Action\Action implements HttpGetActionInterface - { - /** - * Index action - * -- * @return $this -+ * @return \Magento\Framework\Controller\Result\Redirect - */ - public function execute() - { -diff --git a/app/code/Magento/Catalog/Controller/Product/Compare.php b/app/code/Magento/Catalog/Controller/Product/Compare.php -index 1ee146e5aaa..084a82f87d6 100644 ---- a/app/code/Magento/Catalog/Controller/Product/Compare.php -+++ b/app/code/Magento/Catalog/Controller/Product/Compare.php -@@ -6,6 +6,7 @@ - namespace Magento\Catalog\Controller\Product; - - use Magento\Catalog\Api\ProductRepositoryInterface; -+use Magento\Framework\App\Action\HttpGetActionInterface; - use Magento\Framework\Data\Form\FormKey\Validator; - use Magento\Framework\View\Result\PageFactory; - -@@ -15,7 +16,7 @@ use Magento\Framework\View\Result\PageFactory; - * @SuppressWarnings(PHPMD.LongVariable) - * @SuppressWarnings(PHPMD.CouplingBetweenObjects) - */ --abstract class Compare extends \Magento\Framework\App\Action\Action -+abstract class Compare extends \Magento\Framework\App\Action\Action implements HttpGetActionInterface - { - /** - * Customer id -@@ -139,4 +140,15 @@ abstract class Compare extends \Magento\Framework\App\Action\Action - $this->_customerId = $customerId; - return $this; - } -+ -+ /** -+ * @inheritdoc -+ */ -+ public function execute() -+ { -+ $resultRedirect = $this->resultRedirectFactory->create(); -+ $resultRedirect->setPath('catalog/product_compare'); -+ -+ return $resultRedirect; -+ } - } -diff --git a/app/code/Magento/Catalog/Controller/Product/Compare/Add.php b/app/code/Magento/Catalog/Controller/Product/Compare/Add.php -index eb9cc831255..d99901c915a 100644 ---- a/app/code/Magento/Catalog/Controller/Product/Compare/Add.php -+++ b/app/code/Magento/Catalog/Controller/Product/Compare/Add.php -@@ -6,9 +6,10 @@ - */ - namespace Magento\Catalog\Controller\Product\Compare; - -+use Magento\Framework\App\Action\HttpPostActionInterface as HttpPostActionInterface; - use Magento\Framework\Exception\NoSuchEntityException; - --class Add extends \Magento\Catalog\Controller\Product\Compare -+class Add extends \Magento\Catalog\Controller\Product\Compare implements HttpPostActionInterface - { - /** - * Add item to compare list -diff --git a/app/code/Magento/Catalog/Controller/Product/Compare/Clear.php b/app/code/Magento/Catalog/Controller/Product/Compare/Clear.php -index 568fbf1d056..2703e9869bd 100644 ---- a/app/code/Magento/Catalog/Controller/Product/Compare/Clear.php -+++ b/app/code/Magento/Catalog/Controller/Product/Compare/Clear.php -@@ -6,9 +6,10 @@ - */ - namespace Magento\Catalog\Controller\Product\Compare; - -+use Magento\Framework\App\Action\HttpPostActionInterface as HttpPostActionInterface; - use Magento\Framework\Controller\ResultFactory; - --class Clear extends \Magento\Catalog\Controller\Product\Compare -+class Clear extends \Magento\Catalog\Controller\Product\Compare implements HttpPostActionInterface - { - /** - * Remove all items from comparison list -diff --git a/app/code/Magento/Catalog/Controller/Product/Compare/Index.php b/app/code/Magento/Catalog/Controller/Product/Compare/Index.php -index 3eba058318a..c0aa32a56ed 100644 ---- a/app/code/Magento/Catalog/Controller/Product/Compare/Index.php -+++ b/app/code/Magento/Catalog/Controller/Product/Compare/Index.php -@@ -6,6 +6,7 @@ - */ - namespace Magento\Catalog\Controller\Product\Compare; - -+use Magento\Framework\App\Action\HttpGetActionInterface as HttpGetActionInterface; - use Magento\Catalog\Api\ProductRepositoryInterface; - use Magento\Framework\Data\Form\FormKey\Validator; - use Magento\Framework\View\Result\PageFactory; -@@ -13,7 +14,7 @@ use Magento\Framework\View\Result\PageFactory; - /** - * @SuppressWarnings(PHPMD.CouplingBetweenObjects) - */ --class Index extends \Magento\Catalog\Controller\Product\Compare -+class Index extends \Magento\Catalog\Controller\Product\Compare implements HttpGetActionInterface - { - /** - * @var \Magento\Framework\Url\DecoderInterface -diff --git a/app/code/Magento/Catalog/Controller/Product/Compare/Remove.php b/app/code/Magento/Catalog/Controller/Product/Compare/Remove.php -index 2acbe5ce4d5..eac0ddf94af 100644 ---- a/app/code/Magento/Catalog/Controller/Product/Compare/Remove.php -+++ b/app/code/Magento/Catalog/Controller/Product/Compare/Remove.php -@@ -6,9 +6,10 @@ - */ - namespace Magento\Catalog\Controller\Product\Compare; - -+use Magento\Framework\App\Action\HttpPostActionInterface as HttpPostActionInterface; - use Magento\Framework\Exception\NoSuchEntityException; - --class Remove extends \Magento\Catalog\Controller\Product\Compare -+class Remove extends \Magento\Catalog\Controller\Product\Compare implements HttpPostActionInterface - { - /** - * Remove item from compare list -diff --git a/app/code/Magento/Catalog/Controller/Product/View.php b/app/code/Magento/Catalog/Controller/Product/View.php -index ed437361fdd..024123e1515 100644 ---- a/app/code/Magento/Catalog/Controller/Product/View.php -+++ b/app/code/Magento/Catalog/Controller/Product/View.php -@@ -6,10 +6,16 @@ - */ - namespace Magento\Catalog\Controller\Product; - -+use Magento\Framework\App\Action\HttpPostActionInterface as HttpPostActionInterface; -+use Magento\Framework\App\Action\HttpGetActionInterface as HttpGetActionInterface; - use Magento\Framework\App\Action\Context; - use Magento\Framework\View\Result\PageFactory; -+use Magento\Catalog\Controller\Product as ProductAction; - --class View extends \Magento\Catalog\Controller\Product -+/** -+ * View a product on storefront. Needs to be accessible by POST because of the store switching. -+ */ -+class View extends ProductAction implements HttpGetActionInterface, HttpPostActionInterface - { - /** - * @var \Magento\Catalog\Helper\Product\View -diff --git a/app/code/Magento/Catalog/Cron/DeleteOutdatedPriceValues.php b/app/code/Magento/Catalog/Cron/DeleteOutdatedPriceValues.php -index 7cc3eb9e3d2..25f6d0c3236 100644 ---- a/app/code/Magento/Catalog/Cron/DeleteOutdatedPriceValues.php -+++ b/app/code/Magento/Catalog/Cron/DeleteOutdatedPriceValues.php -@@ -12,6 +12,8 @@ use Magento\Catalog\Api\Data\ProductAttributeInterface; - use Magento\Store\Model\Store; - - /** -+ * Cron job for removing outdated prices. -+ * - * Cron operation is responsible for deleting all product prices on WEBSITE level - * in case 'Catalog Price Scope' configuration parameter is set to GLOBAL. - */ -@@ -76,7 +78,7 @@ class DeleteOutdatedPriceValues - /** - * Checks if price scope config option explicitly equal to global value. - * -- * Such strict comparision is required to prevent price deleting when -+ * Such strict comparison is required to prevent price deleting when - * price scope config option is null for some reason. - * - * @return bool -diff --git a/app/code/Magento/Catalog/Cron/FrontendActionsFlush.php b/app/code/Magento/Catalog/Cron/FrontendActionsFlush.php -index 6e7699abb47..99e9898eab3 100644 ---- a/app/code/Magento/Catalog/Cron/FrontendActionsFlush.php -+++ b/app/code/Magento/Catalog/Cron/FrontendActionsFlush.php -@@ -57,8 +57,7 @@ class FrontendActionsFlush - ]; - } - -- return isset($configuration['lifetime']) ? -- (int) $configuration['lifetime'] : FrontendStorageConfigurationInterface::DEFAULT_LIFETIME; -+ return (int)$configuration['lifetime'] ?? FrontendStorageConfigurationInterface::DEFAULT_LIFETIME; - } - - /** -diff --git a/app/code/Magento/Catalog/Helper/Data.php b/app/code/Magento/Catalog/Helper/Data.php -index ae20cda4607..3e967636328 100644 ---- a/app/code/Magento/Catalog/Helper/Data.php -+++ b/app/code/Magento/Catalog/Helper/Data.php -@@ -7,6 +7,7 @@ namespace Magento\Catalog\Helper; - - use Magento\Catalog\Api\CategoryRepositoryInterface; - use Magento\Catalog\Api\ProductRepositoryInterface; -+use Magento\Store\Model\ScopeInterface; - use Magento\Customer\Model\Session as CustomerSession; - use Magento\Framework\Exception\NoSuchEntityException; - use Magento\Framework\Pricing\PriceCurrencyInterface; -@@ -273,7 +274,8 @@ class Data extends \Magento\Framework\App\Helper\AbstractHelper - - /** - * Return current category path or get it from current category -- * and creating array of categories|product paths for breadcrumbs -+ * -+ * Creating array of categories|product paths for breadcrumbs - * - * @return array - */ -@@ -382,6 +384,7 @@ class Data extends \Magento\Framework\App\Helper\AbstractHelper - - /** - * Split SKU of an item by dashes and spaces -+ * - * Words will not be broken, unless this length is greater than $length - * - * @param string $sku -@@ -410,14 +413,15 @@ class Data extends \Magento\Framework\App\Helper\AbstractHelper - /** - * Retrieve Catalog Price Scope - * -- * @return int -+ * @return int|null - */ -- public function getPriceScope() -+ public function getPriceScope(): ?int - { -- return $this->scopeConfig->getValue( -+ $priceScope = $this->scopeConfig->getValue( - self::XML_PATH_PRICE_SCOPE, -- \Magento\Store\Model\ScopeInterface::SCOPE_STORE -+ ScopeInterface::SCOPE_STORE - ); -+ return isset($priceScope) ? (int)$priceScope : null; - } - - /** -@@ -439,7 +443,7 @@ class Data extends \Magento\Framework\App\Helper\AbstractHelper - { - return $this->scopeConfig->isSetFlag( - self::CONFIG_USE_STATIC_URLS, -- \Magento\Store\Model\ScopeInterface::SCOPE_STORE -+ ScopeInterface::SCOPE_STORE - ); - } - -@@ -454,7 +458,7 @@ class Data extends \Magento\Framework\App\Helper\AbstractHelper - { - return $this->scopeConfig->isSetFlag( - self::CONFIG_PARSE_URL_DIRECTIVES, -- \Magento\Store\Model\ScopeInterface::SCOPE_STORE, -+ ScopeInterface::SCOPE_STORE, - $this->_storeId - ); - } -@@ -472,6 +476,7 @@ class Data extends \Magento\Framework\App\Helper\AbstractHelper - - /** - * Whether to display items count for each filter option -+ * - * @param int $storeId Store view ID - * @return bool - */ -@@ -479,12 +484,14 @@ class Data extends \Magento\Framework\App\Helper\AbstractHelper - { - return $this->scopeConfig->isSetFlag( - self::XML_PATH_DISPLAY_PRODUCT_COUNT, -- \Magento\Store\Model\ScopeInterface::SCOPE_STORE, -+ ScopeInterface::SCOPE_STORE, - $storeId - ); - } - - /** -+ * Convert tax address array to address data object with country id and postcode -+ * - * @param array $taxAddress - * @return \Magento\Customer\Api\Data\AddressInterface|null - */ -diff --git a/app/code/Magento/Catalog/Helper/Image.php b/app/code/Magento/Catalog/Helper/Image.php -index 4f128d639b2..9b8d0ad75a8 100644 ---- a/app/code/Magento/Catalog/Helper/Image.php -+++ b/app/code/Magento/Catalog/Helper/Image.php -@@ -6,6 +6,7 @@ - namespace Magento\Catalog\Helper; - - use Magento\Framework\App\Helper\AbstractHelper; -+use Magento\Framework\View\Element\Block\ArgumentInterface; - - /** - * Catalog image helper -@@ -14,7 +15,7 @@ use Magento\Framework\App\Helper\AbstractHelper; - * @SuppressWarnings(PHPMD.TooManyFields) - * @since 100.0.2 - */ --class Image extends AbstractHelper -+class Image extends AbstractHelper implements ArgumentInterface - { - /** - * Media config node -@@ -298,6 +299,7 @@ class Image extends AbstractHelper - * - * @param int $quality - * @return $this -+ * @deprecated - */ - public function setQuality($quality) - { -@@ -406,7 +408,8 @@ class Image extends AbstractHelper - - /** - * Add watermark to image -- * size param in format 100x200 -+ * -+ * Size param in format 100x200 - * - * @param string $fileName - * @param string $position -@@ -533,6 +536,8 @@ class Image extends AbstractHelper - } - - /** -+ * Save changes -+ * - * @return $this - */ - public function save() -@@ -553,6 +558,8 @@ class Image extends AbstractHelper - } - - /** -+ * Getter for placeholder url -+ * - * @param null|string $placeholder - * @return string - */ -@@ -655,7 +662,8 @@ class Image extends AbstractHelper - - /** - * Set watermark size -- * param size in format 100x200 -+ * -+ * Param size in format 100x200 - * - * @param string $size - * @return $this -@@ -757,7 +765,7 @@ class Image extends AbstractHelper - protected function parseSize($string) - { - $size = explode('x', strtolower($string)); -- if (sizeof($size) == 2) { -+ if (count($size) == 2) { - return ['width' => $size[0] > 0 ? $size[0] : null, 'height' => $size[1] > 0 ? $size[1] : null]; - } - return false; -@@ -859,7 +867,7 @@ class Image extends AbstractHelper - */ - protected function getAttribute($name) - { -- return isset($this->attributes[$name]) ? $this->attributes[$name] : null; -+ return $this->attributes[$name] ?? null; - } - - /** -diff --git a/app/code/Magento/Catalog/Helper/Output.php b/app/code/Magento/Catalog/Helper/Output.php -index ad0f9508cb6..33e261dc353 100644 ---- a/app/code/Magento/Catalog/Helper/Output.php -+++ b/app/code/Magento/Catalog/Helper/Output.php -@@ -116,7 +116,7 @@ class Output extends \Magento\Framework\App\Helper\AbstractHelper - public function getHandlers($method) - { - $method = strtolower($method); -- return isset($this->_handlers[$method]) ? $this->_handlers[$method] : []; -+ return $this->_handlers[$method] ?? []; - } - - /** -diff --git a/app/code/Magento/Catalog/Helper/Product.php b/app/code/Magento/Catalog/Helper/Product.php -index c0d5d1d9e45..73b5e4af78d 100644 ---- a/app/code/Magento/Catalog/Helper/Product.php -+++ b/app/code/Magento/Catalog/Helper/Product.php -@@ -14,6 +14,7 @@ use Magento\Store\Model\Store; - /** - * Catalog category helper - * @SuppressWarnings(PHPMD.CouplingBetweenObjects) -+ * @SuppressWarnings(PHPMD.CookieAndSessionMisuse) - */ - class Product extends \Magento\Framework\Url\Helper\Data - { -@@ -268,6 +269,8 @@ class Product extends \Magento\Framework\Url\Helper\Data - } - - /** -+ * Retrieve email to friend url -+ * - * @param ModelProduct $product - * @return string - */ -@@ -282,6 +285,8 @@ class Product extends \Magento\Framework\Url\Helper\Data - } - - /** -+ * Get statuses -+ * - * @return array - */ - public function getStatuses() -@@ -476,6 +481,7 @@ class Product extends \Magento\Framework\Url\Helper\Data - - /** - * Prepares product options by buyRequest: retrieves values and assigns them as default. -+ * - * Also parses and adds product management related values - e.g. qty - * - * @param ModelProduct $product -@@ -493,6 +499,7 @@ class Product extends \Magento\Framework\Url\Helper\Data - - /** - * Process $buyRequest and sets its options before saving configuration to some product item. -+ * - * This method is used to attach additional parameters to processed buyRequest. - * - * $params holds parameters of what operation must be performed: -@@ -541,8 +548,6 @@ class Product extends \Magento\Framework\Url\Helper\Data - /** - * Set flag that shows if Magento has to check product to be saleable (enabled and/or inStock) - * -- * For instance, during order creation in the backend admin has ability to add any products to order -- * - * @param bool $skipSaleableCheck - * @return Product - */ -diff --git a/app/code/Magento/Catalog/Helper/Product/Compare.php b/app/code/Magento/Catalog/Helper/Product/Compare.php -index 90d98874e00..d6d35c5c76d 100644 ---- a/app/code/Magento/Catalog/Helper/Product/Compare.php -+++ b/app/code/Magento/Catalog/Helper/Product/Compare.php -@@ -166,7 +166,15 @@ class Compare extends \Magento\Framework\Url\Helper\Data - */ - public function getPostDataParams($product) - { -- return $this->postHelper->getPostData($this->getAddUrl(), ['product' => $product->getId()]); -+ $params = ['product' => $product->getId()]; -+ $requestingPageUrl = $this->_getRequest()->getParam('requesting_page_url'); -+ -+ if (!empty($requestingPageUrl)) { -+ $encodedUrl = $this->urlEncoder->encode($requestingPageUrl); -+ $params[\Magento\Framework\App\ActionInterface::PARAM_NAME_URL_ENCODED] = $encodedUrl; -+ } -+ -+ return $this->postHelper->getPostData($this->getAddUrl(), $params); - } - - /** -diff --git a/app/code/Magento/Catalog/Helper/Product/Configuration.php b/app/code/Magento/Catalog/Helper/Product/Configuration.php -index 9b47e299009..5b8f6fad6e1 100644 ---- a/app/code/Magento/Catalog/Helper/Product/Configuration.php -+++ b/app/code/Magento/Catalog/Helper/Product/Configuration.php -@@ -55,6 +55,7 @@ class Configuration extends AbstractHelper implements ConfigurationInterface - * @param \Magento\Framework\Filter\FilterManager $filter - * @param \Magento\Framework\Stdlib\StringUtils $string - * @param Json $serializer -+ * @param Escaper $escaper - */ - public function __construct( - \Magento\Framework\App\Helper\Context $context, -diff --git a/app/code/Magento/Catalog/Helper/Product/ProductList.php b/app/code/Magento/Catalog/Helper/Product/ProductList.php -index fbea73a6324..3aa6aeed377 100644 ---- a/app/code/Magento/Catalog/Helper/Product/ProductList.php -+++ b/app/code/Magento/Catalog/Helper/Product/ProductList.php -@@ -42,6 +42,7 @@ class ProductList - - /** - * @param \Magento\Framework\App\Config\ScopeConfigInterface $scopeConfig -+ * @param \Magento\Framework\Registry $coreRegistry - */ - public function __construct( - \Magento\Framework\App\Config\ScopeConfigInterface $scopeConfig, -diff --git a/app/code/Magento/Catalog/Helper/Product/View.php b/app/code/Magento/Catalog/Helper/Product/View.php -index 5753910c125..74f40a18971 100644 ---- a/app/code/Magento/Catalog/Helper/Product/View.php -+++ b/app/code/Magento/Catalog/Helper/Product/View.php -@@ -10,7 +10,9 @@ use Magento\Framework\View\Result\Page as ResultPage; - - /** - * Catalog category helper -+ * - * @SuppressWarnings(PHPMD.CouplingBetweenObjects) -+ * @SuppressWarnings(PHPMD.CookieAndSessionMisuse) - */ - class View extends \Magento\Framework\App\Helper\AbstractHelper - { -@@ -105,19 +107,16 @@ class View extends \Magento\Framework\App\Helper\AbstractHelper - * - * @param \Magento\Framework\View\Result\Page $resultPage - * @param \Magento\Catalog\Model\Product $product -- * @return \Magento\Framework\View\Result\Page -+ * @return $this - */ - private function preparePageMetadata(ResultPage $resultPage, $product) - { - $pageLayout = $resultPage->getLayout(); - $pageConfig = $resultPage->getConfig(); - -- $title = $product->getMetaTitle(); -- if ($title) { -- $pageConfig->getTitle()->set($title); -- } else { -- $pageConfig->getTitle()->set($product->getName()); -- } -+ $metaTitle = $product->getMetaTitle(); -+ $pageConfig->setMetaTitle($metaTitle); -+ $pageConfig->getTitle()->set($metaTitle ?: $product->getName()); - - $keyword = $product->getMetaKeyword(); - $currentCategory = $this->_coreRegistry->registry('current_category'); -diff --git a/app/code/Magento/Catalog/Model/AbstractModel.php b/app/code/Magento/Catalog/Model/AbstractModel.php -index 007635b1243..78a49cd1e8b 100644 ---- a/app/code/Magento/Catalog/Model/AbstractModel.php -+++ b/app/code/Magento/Catalog/Model/AbstractModel.php -@@ -179,7 +179,7 @@ abstract class AbstractModel extends \Magento\Framework\Model\AbstractExtensible - * - * @param string|array $key - * @param mixed $value -- * @return \Magento\Framework\DataObject -+ * @return $this - */ - public function setData($key, $value = null) - { -@@ -282,9 +282,9 @@ abstract class AbstractModel extends \Magento\Framework\Model\AbstractExtensible - * - * Default value existing is flag for using store value in data - * -- * @param string $attributeCode -- * @param mixed $value -- * @return $this -+ * @param string $attributeCode -+ * @param mixed $value -+ * @return $this - * - * @deprecated 101.0.0 - */ -@@ -332,11 +332,10 @@ abstract class AbstractModel extends \Magento\Framework\Model\AbstractExtensible - } - - /** -- * Set attribute code flag if attribute has value in current store and does not use -- * value of default store as value -+ * Set attribute code flag if attribute has value in current store and does not use value of default store as value - * -- * @param string $attributeCode -- * @return $this -+ * @param string $attributeCode -+ * @return $this - * - * @deprecated 101.0.0 - */ -diff --git a/app/code/Magento/Catalog/Model/Api/SearchCriteria/CollectionProcessor/ConditionProcessor/ConditionBuilder/EavAttributeCondition.php b/app/code/Magento/Catalog/Model/Api/SearchCriteria/CollectionProcessor/ConditionProcessor/ConditionBuilder/EavAttributeCondition.php -index d3c84e69c95..e296c8d3b89 100644 ---- a/app/code/Magento/Catalog/Model/Api/SearchCriteria/CollectionProcessor/ConditionProcessor/ConditionBuilder/EavAttributeCondition.php -+++ b/app/code/Magento/Catalog/Model/Api/SearchCriteria/CollectionProcessor/ConditionProcessor/ConditionBuilder/EavAttributeCondition.php -@@ -58,22 +58,38 @@ class EavAttributeCondition implements CustomConditionInterface - $conditionValue = $this->mapConditionValue($conditionType, $filter->getValue()); - - // NOTE: store scope was ignored intentionally to perform search across all stores -- $attributeSelect = $this->resourceConnection->getConnection() -- ->select() -- ->from( -- [$tableAlias => $attribute->getBackendTable()], -- $tableAlias . '.' . $attribute->getEntityIdField() -- )->where( -- $this->resourceConnection->getConnection()->prepareSqlCondition( -- $tableAlias . '.' . $attribute->getIdFieldName(), -- ['eq' => $attribute->getAttributeId()] -- ) -- )->where( -- $this->resourceConnection->getConnection()->prepareSqlCondition( -- $tableAlias . '.value', -- [$conditionType => $conditionValue] -- ) -- ); -+ if ($conditionType == 'is_null') { -+ $entityResourceModel = $attribute->getEntity(); -+ $attributeSelect = $this->resourceConnection->getConnection() -+ ->select() -+ ->from( -+ [Collection::MAIN_TABLE_ALIAS => $entityResourceModel->getEntityTable()], -+ Collection::MAIN_TABLE_ALIAS . '.' . $entityResourceModel->getEntityIdField() -+ )->joinLeft( -+ [$tableAlias => $attribute->getBackendTable()], -+ $tableAlias . '.' . $attribute->getEntityIdField() . '=' . Collection::MAIN_TABLE_ALIAS . -+ '.' . $entityResourceModel->getEntityIdField() . ' AND ' . $tableAlias . '.' . -+ $attribute->getIdFieldName() . '=' . $attribute->getAttributeId(), -+ '' -+ )->where($tableAlias . '.value is null'); -+ } else { -+ $attributeSelect = $this->resourceConnection->getConnection() -+ ->select() -+ ->from( -+ [$tableAlias => $attribute->getBackendTable()], -+ $tableAlias . '.' . $attribute->getEntityIdField() -+ )->where( -+ $this->resourceConnection->getConnection()->prepareSqlCondition( -+ $tableAlias . '.' . $attribute->getIdFieldName(), -+ ['eq' => $attribute->getAttributeId()] -+ ) -+ )->where( -+ $this->resourceConnection->getConnection()->prepareSqlCondition( -+ $tableAlias . '.value', -+ [$conditionType => $conditionValue] -+ ) -+ ); -+ } - - return $this->resourceConnection - ->getConnection() -@@ -86,6 +102,8 @@ class EavAttributeCondition implements CustomConditionInterface - } - - /** -+ * Get attribute entity by its code -+ * - * @param string $field - * @return Attribute - * @throws \Magento\Framework\Exception\LocalizedException -diff --git a/app/code/Magento/Catalog/Model/Api/SearchCriteria/CollectionProcessor/ConditionProcessor/ConditionBuilder/NativeAttributeCondition.php b/app/code/Magento/Catalog/Model/Api/SearchCriteria/CollectionProcessor/ConditionProcessor/ConditionBuilder/NativeAttributeCondition.php -index d072acf4c71..71b9a9c4703 100644 ---- a/app/code/Magento/Catalog/Model/Api/SearchCriteria/CollectionProcessor/ConditionProcessor/ConditionBuilder/NativeAttributeCondition.php -+++ b/app/code/Magento/Catalog/Model/Api/SearchCriteria/CollectionProcessor/ConditionProcessor/ConditionBuilder/NativeAttributeCondition.php -@@ -77,7 +77,7 @@ class NativeAttributeCondition implements CustomConditionInterface - ]; - } - -- return isset($conditionsMap[$conditionType]) ? $conditionsMap[$conditionType] : $conditionType; -+ return $conditionsMap[$conditionType] ?? $conditionType; - } - - /** -diff --git a/app/code/Magento/Catalog/Model/Api/SearchCriteria/CollectionProcessor/ConditionProcessor/ProductCategoryCondition.php b/app/code/Magento/Catalog/Model/Api/SearchCriteria/CollectionProcessor/ConditionProcessor/ProductCategoryCondition.php -index f70bab73d08..66a9132ae44 100644 ---- a/app/code/Magento/Catalog/Model/Api/SearchCriteria/CollectionProcessor/ConditionProcessor/ProductCategoryCondition.php -+++ b/app/code/Magento/Catalog/Model/Api/SearchCriteria/CollectionProcessor/ConditionProcessor/ProductCategoryCondition.php -@@ -38,6 +38,7 @@ class ProductCategoryCondition implements CustomConditionInterface - - /** - * @param \Magento\Framework\App\ResourceConnection $resourceConnection -+ * @param \Magento\Catalog\Model\CategoryRepository $categoryRepository - */ - public function __construct( - \Magento\Framework\App\ResourceConnection $resourceConnection, -@@ -104,7 +105,7 @@ class ProductCategoryCondition implements CustomConditionInterface - } - } - -- return array_unique(array_merge($categoryIds, ...$childCategoryIds)); -+ return array_map('intval', array_unique(array_merge($categoryIds, ...$childCategoryIds))); - } - - /** -diff --git a/app/code/Magento/Catalog/Model/Attribute/Backend/Consumer.php b/app/code/Magento/Catalog/Model/Attribute/Backend/Consumer.php -new file mode 100644 -index 00000000000..dc24a309048 ---- /dev/null -+++ b/app/code/Magento/Catalog/Model/Attribute/Backend/Consumer.php -@@ -0,0 +1,163 @@ -+<?php -+/** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+declare(strict_types=1); -+ -+namespace Magento\Catalog\Model\Attribute\Backend; -+ -+use Magento\Framework\Exception\LocalizedException; -+use Magento\Framework\EntityManager\EntityManager; -+use Magento\Framework\Exception\NoSuchEntityException; -+use Magento\Framework\Exception\TemporaryStateExceptionInterface; -+use Magento\Framework\Bulk\OperationInterface; -+ -+/** -+ * Consumer for export message. -+ * @SuppressWarnings(PHPMD.CouplingBetweenObjects) -+ */ -+class Consumer -+{ -+ /** -+ * @var \Psr\Log\LoggerInterface -+ */ -+ private $logger; -+ -+ /** -+ * @var \Magento\Catalog\Model\Indexer\Product\Flat\Processor -+ */ -+ private $productFlatIndexerProcessor; -+ -+ /** -+ * @var \Magento\Catalog\Model\Indexer\Product\Price\Processor -+ */ -+ private $productPriceIndexerProcessor; -+ -+ /** -+ * @var \Magento\Catalog\Helper\Product -+ */ -+ private $catalogProduct; -+ -+ /** -+ * @var \Magento\Catalog\Model\Product\Action -+ */ -+ private $productAction; -+ -+ /** -+ * @var \Magento\Framework\Serialize\SerializerInterface -+ */ -+ private $serializer; -+ -+ /** -+ * @var \Magento\Framework\Bulk\OperationManagementInterface -+ */ -+ private $operationManagement; -+ /** -+ * @var EntityManager -+ */ -+ private $entityManager; -+ -+ /** -+ * @param \Magento\Catalog\Helper\Product $catalogProduct -+ * @param \Magento\Catalog\Model\Indexer\Product\Flat\Processor $productFlatIndexerProcessor -+ * @param \Magento\Catalog\Model\Indexer\Product\Price\Processor $productPriceIndexerProcessor -+ * @param \Magento\Framework\Bulk\OperationManagementInterface $operationManagement -+ * @param \Magento\Catalog\Model\Product\Action $action -+ * @param \Psr\Log\LoggerInterface $logger -+ * @param \Magento\Framework\Serialize\SerializerInterface $serializer -+ * @param EntityManager $entityManager -+ */ -+ public function __construct( -+ \Magento\Catalog\Helper\Product $catalogProduct, -+ \Magento\Catalog\Model\Indexer\Product\Flat\Processor $productFlatIndexerProcessor, -+ \Magento\Catalog\Model\Indexer\Product\Price\Processor $productPriceIndexerProcessor, -+ \Magento\Framework\Bulk\OperationManagementInterface $operationManagement, -+ \Magento\Catalog\Model\Product\Action $action, -+ \Psr\Log\LoggerInterface $logger, -+ \Magento\Framework\Serialize\SerializerInterface $serializer, -+ EntityManager $entityManager -+ ) { -+ $this->catalogProduct = $catalogProduct; -+ $this->productFlatIndexerProcessor = $productFlatIndexerProcessor; -+ $this->productPriceIndexerProcessor = $productPriceIndexerProcessor; -+ $this->productAction = $action; -+ $this->logger = $logger; -+ $this->serializer = $serializer; -+ $this->operationManagement = $operationManagement; -+ $this->entityManager = $entityManager; -+ } -+ -+ /** -+ * Process -+ * -+ * @param \Magento\AsynchronousOperations\Api\Data\OperationInterface $operation -+ * @throws \Exception -+ * -+ * @return void -+ */ -+ public function process(\Magento\AsynchronousOperations\Api\Data\OperationInterface $operation) -+ { -+ try { -+ $serializedData = $operation->getSerializedData(); -+ $data = $this->serializer->unserialize($serializedData); -+ $this->execute($data); -+ } catch (\Zend_Db_Adapter_Exception $e) { -+ $this->logger->critical($e->getMessage()); -+ if ($e instanceof \Magento\Framework\DB\Adapter\LockWaitException -+ || $e instanceof \Magento\Framework\DB\Adapter\DeadlockException -+ || $e instanceof \Magento\Framework\DB\Adapter\ConnectionException -+ ) { -+ $status = OperationInterface::STATUS_TYPE_RETRIABLY_FAILED; -+ $errorCode = $e->getCode(); -+ $message = $e->getMessage(); -+ } else { -+ $status = OperationInterface::STATUS_TYPE_NOT_RETRIABLY_FAILED; -+ $errorCode = $e->getCode(); -+ $message = __( -+ 'Sorry, something went wrong during product attributes update. Please see log for details.' -+ ); -+ } -+ } catch (NoSuchEntityException $e) { -+ $this->logger->critical($e->getMessage()); -+ $status = ($e instanceof TemporaryStateExceptionInterface) -+ ? OperationInterface::STATUS_TYPE_RETRIABLY_FAILED -+ : OperationInterface::STATUS_TYPE_NOT_RETRIABLY_FAILED; -+ $errorCode = $e->getCode(); -+ $message = $e->getMessage(); -+ } catch (LocalizedException $e) { -+ $this->logger->critical($e->getMessage()); -+ $status = OperationInterface::STATUS_TYPE_NOT_RETRIABLY_FAILED; -+ $errorCode = $e->getCode(); -+ $message = $e->getMessage(); -+ } catch (\Exception $e) { -+ $this->logger->critical($e->getMessage()); -+ $status = OperationInterface::STATUS_TYPE_NOT_RETRIABLY_FAILED; -+ $errorCode = $e->getCode(); -+ $message = __('Sorry, something went wrong during product attributes update. Please see log for details.'); -+ } -+ -+ $operation->setStatus($status ?? OperationInterface::STATUS_TYPE_COMPLETE) -+ ->setErrorCode($errorCode ?? null) -+ ->setResultMessage($message ?? null); -+ -+ $this->entityManager->save($operation); -+ } -+ -+ /** -+ * Execute -+ * -+ * @param array $data -+ * -+ * @return void -+ */ -+ private function execute($data): void -+ { -+ $this->productAction->updateAttributes($data['product_ids'], $data['attributes'], $data['store_id']); -+ if ($this->catalogProduct->isDataForPriceIndexerWasChanged($data['attributes'])) { -+ $this->productPriceIndexerProcessor->reindexList($data['product_ids']); -+ } -+ -+ $this->productFlatIndexerProcessor->reindexList($data['product_ids']); -+ } -+} -diff --git a/app/code/Magento/Catalog/Model/Attribute/Backend/ConsumerWebsiteAssign.php b/app/code/Magento/Catalog/Model/Attribute/Backend/ConsumerWebsiteAssign.php -new file mode 100644 -index 00000000000..32ba39d9afd ---- /dev/null -+++ b/app/code/Magento/Catalog/Model/Attribute/Backend/ConsumerWebsiteAssign.php -@@ -0,0 +1,168 @@ -+<?php -+/** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+declare(strict_types=1); -+ -+namespace Magento\Catalog\Model\Attribute\Backend; -+ -+use Magento\Framework\Exception\LocalizedException; -+use Magento\Framework\Exception\NoSuchEntityException; -+use Magento\Framework\Exception\TemporaryStateExceptionInterface; -+use Magento\Framework\Bulk\OperationInterface; -+use Magento\Framework\EntityManager\EntityManager; -+ -+/** -+ * Consumer for export message. -+ * @SuppressWarnings(PHPMD.CouplingBetweenObjects) -+ */ -+class ConsumerWebsiteAssign -+{ -+ /** -+ * @var \Psr\Log\LoggerInterface -+ */ -+ private $logger; -+ -+ /** -+ * @var \Magento\Catalog\Model\Indexer\Product\Flat\Processor -+ */ -+ private $productFlatIndexerProcessor; -+ -+ /** -+ * @var \Magento\Catalog\Model\Product\Action -+ */ -+ private $productAction; -+ -+ /** -+ * @var \Magento\Framework\Serialize\SerializerInterface -+ */ -+ private $serializer; -+ -+ /** -+ * @var \Magento\Catalog\Model\Indexer\Product\Price\Processor -+ */ -+ private $productPriceIndexerProcessor; -+ -+ /** -+ * @var EntityManager -+ */ -+ private $entityManager; -+ -+ /** -+ * @param \Magento\Catalog\Model\Indexer\Product\Flat\Processor $productFlatIndexerProcessor -+ * @param \Magento\Catalog\Model\Indexer\Product\Price\Processor $productPriceIndexerProcessor -+ * @param \Magento\Catalog\Model\Product\Action $action -+ * @param \Psr\Log\LoggerInterface $logger -+ * @param \Magento\Framework\Serialize\SerializerInterface $serializer -+ * @param EntityManager $entityManager -+ */ -+ public function __construct( -+ \Magento\Catalog\Model\Indexer\Product\Flat\Processor $productFlatIndexerProcessor, -+ \Magento\Catalog\Model\Indexer\Product\Price\Processor $productPriceIndexerProcessor, -+ \Magento\Catalog\Model\Product\Action $action, -+ \Psr\Log\LoggerInterface $logger, -+ \Magento\Framework\Serialize\SerializerInterface $serializer, -+ EntityManager $entityManager -+ ) { -+ $this->productFlatIndexerProcessor = $productFlatIndexerProcessor; -+ $this->productAction = $action; -+ $this->logger = $logger; -+ $this->serializer = $serializer; -+ $this->productPriceIndexerProcessor = $productPriceIndexerProcessor; -+ $this->entityManager = $entityManager; -+ } -+ -+ /** -+ * Process -+ * -+ * @param \Magento\AsynchronousOperations\Api\Data\OperationInterface $operation -+ * @throws \Exception -+ * -+ * @return void -+ */ -+ public function process(\Magento\AsynchronousOperations\Api\Data\OperationInterface $operation) -+ { -+ try { -+ $serializedData = $operation->getSerializedData(); -+ $data = $this->serializer->unserialize($serializedData); -+ $this->execute($data); -+ } catch (\Zend_Db_Adapter_Exception $e) { -+ $this->logger->critical($e->getMessage()); -+ if ($e instanceof \Magento\Framework\DB\Adapter\LockWaitException -+ || $e instanceof \Magento\Framework\DB\Adapter\DeadlockException -+ || $e instanceof \Magento\Framework\DB\Adapter\ConnectionException -+ ) { -+ $status = OperationInterface::STATUS_TYPE_RETRIABLY_FAILED; -+ $errorCode = $e->getCode(); -+ $message = __($e->getMessage()); -+ } else { -+ $status = OperationInterface::STATUS_TYPE_NOT_RETRIABLY_FAILED; -+ $errorCode = $e->getCode(); -+ $message = __( -+ 'Sorry, something went wrong during product attributes update. Please see log for details.' -+ ); -+ } -+ } catch (NoSuchEntityException $e) { -+ $this->logger->critical($e->getMessage()); -+ $status = ($e instanceof TemporaryStateExceptionInterface) -+ ? OperationInterface::STATUS_TYPE_RETRIABLY_FAILED -+ : OperationInterface::STATUS_TYPE_NOT_RETRIABLY_FAILED; -+ $errorCode = $e->getCode(); -+ $message = $e->getMessage(); -+ } catch (LocalizedException $e) { -+ $this->logger->critical($e->getMessage()); -+ $status = OperationInterface::STATUS_TYPE_NOT_RETRIABLY_FAILED; -+ $errorCode = $e->getCode(); -+ $message = $e->getMessage(); -+ } catch (\Exception $e) { -+ $this->logger->critical($e->getMessage()); -+ $status = OperationInterface::STATUS_TYPE_NOT_RETRIABLY_FAILED; -+ $errorCode = $e->getCode(); -+ $message = __('Sorry, something went wrong during product attributes update. Please see log for details.'); -+ } -+ -+ $operation->setStatus($status ?? OperationInterface::STATUS_TYPE_COMPLETE) -+ ->setErrorCode($errorCode ?? null) -+ ->setResultMessage($message ?? null); -+ -+ $this->entityManager->save($operation); -+ } -+ -+ /** -+ * Update website in products -+ * -+ * @param array $productIds -+ * @param array $websiteRemoveData -+ * @param array $websiteAddData -+ * -+ * @return void -+ */ -+ private function updateWebsiteInProducts($productIds, $websiteRemoveData, $websiteAddData): void -+ { -+ if ($websiteRemoveData) { -+ $this->productAction->updateWebsites($productIds, $websiteRemoveData, 'remove'); -+ } -+ if ($websiteAddData) { -+ $this->productAction->updateWebsites($productIds, $websiteAddData, 'add'); -+ } -+ } -+ -+ /** -+ * Execute -+ * -+ * @param array $data -+ * -+ * @return void -+ */ -+ private function execute($data): void -+ { -+ $this->updateWebsiteInProducts( -+ $data['product_ids'], -+ $data['attributes']['website_detach'], -+ $data['attributes']['website_assign'] -+ ); -+ $this->productPriceIndexerProcessor->reindexList($data['product_ids']); -+ $this->productFlatIndexerProcessor->reindexList($data['product_ids']); -+ } -+} -diff --git a/app/code/Magento/Catalog/Model/Category.php b/app/code/Magento/Catalog/Model/Category.php -index 4f605d02062..d911bec0aaa 100644 ---- a/app/code/Magento/Catalog/Model/Category.php -+++ b/app/code/Magento/Catalog/Model/Category.php -@@ -7,11 +7,8 @@ namespace Magento\Catalog\Model; - - use Magento\Catalog\Api\CategoryRepositoryInterface; - use Magento\Catalog\Api\Data\CategoryInterface; --use Magento\Catalog\Model\Entity\GetCategoryCustomAttributeCodes; - use Magento\CatalogUrlRewrite\Model\CategoryUrlRewriteGenerator; --use Magento\Eav\Model\Entity\GetCustomAttributeCodesInterface; - use Magento\Framework\Api\AttributeValueFactory; --use Magento\Framework\App\ObjectManager; - use Magento\Framework\Convert\ConvertArray; - use Magento\Framework\Exception\NoSuchEntityException; - use Magento\Framework\Profiler; -@@ -214,11 +211,6 @@ class Category extends \Magento\Catalog\Model\AbstractModel implements - */ - protected $metadataService; - -- /** -- * @var GetCustomAttributeCodesInterface -- */ -- private $getCustomAttributeCodes; -- - /** - * @param \Magento\Framework\Model\Context $context - * @param \Magento\Framework\Registry $registry -@@ -241,7 +233,6 @@ class Category extends \Magento\Catalog\Model\AbstractModel implements - * @param \Magento\Framework\Model\ResourceModel\AbstractResource $resource - * @param \Magento\Framework\Data\Collection\AbstractDb $resourceCollection - * @param array $data -- * @param GetCustomAttributeCodesInterface|null $getCustomAttributeCodes - * @SuppressWarnings(PHPMD.ExcessiveParameterList) - */ - public function __construct( -@@ -265,8 +256,7 @@ class Category extends \Magento\Catalog\Model\AbstractModel implements - CategoryRepositoryInterface $categoryRepository, - \Magento\Framework\Model\ResourceModel\AbstractResource $resource = null, - \Magento\Framework\Data\Collection\AbstractDb $resourceCollection = null, -- array $data = [], -- GetCustomAttributeCodesInterface $getCustomAttributeCodes = null -+ array $data = [] - ) { - $this->metadataService = $metadataService; - $this->_treeModel = $categoryTreeResource; -@@ -281,9 +271,6 @@ class Category extends \Magento\Catalog\Model\AbstractModel implements - $this->urlFinder = $urlFinder; - $this->indexerRegistry = $indexerRegistry; - $this->categoryRepository = $categoryRepository; -- $this->getCustomAttributeCodes = $getCustomAttributeCodes ?? ObjectManager::getInstance()->get( -- GetCategoryCustomAttributeCodes::class -- ); - parent::__construct( - $context, - $registry, -@@ -313,14 +300,20 @@ class Category extends \Magento\Catalog\Model\AbstractModel implements - } - - /** -- * {@inheritdoc} -+ * @inheritdoc - */ - protected function getCustomAttributesCodes() - { -- return $this->getCustomAttributeCodes->execute($this->metadataService); -+ if ($this->customAttributesCodes === null) { -+ $this->customAttributesCodes = $this->getEavAttributesCodes($this->metadataService); -+ $this->customAttributesCodes = array_diff($this->customAttributesCodes, CategoryInterface::ATTRIBUTES); -+ } -+ return $this->customAttributesCodes; - } - - /** -+ * Returns model resource -+ * - * @throws \Magento\Framework\Exception\LocalizedException - * @return \Magento\Catalog\Model\ResourceModel\Category - * @deprecated because resource models should be used directly -@@ -571,7 +564,7 @@ class Category extends \Magento\Catalog\Model\AbstractModel implements - * - * If store id is underfined for category return current active store id - * -- * @return integer -+ * @return int - */ - public function getStoreId() - { -@@ -657,6 +650,8 @@ class Category extends \Magento\Catalog\Model\AbstractModel implements - } - - /** -+ * Returns image url -+ * - * @param string $attributeCode - * @return bool|string - * @throws \Magento\Framework\Exception\LocalizedException -@@ -717,7 +712,7 @@ class Category extends \Magento\Catalog\Model\AbstractModel implements - return $parentId; - } - $parentIds = $this->getParentIds(); -- return intval(array_pop($parentIds)); -+ return (int) array_pop($parentIds); - } - - /** -@@ -805,6 +800,7 @@ class Category extends \Magento\Catalog\Model\AbstractModel implements - - /** - * Retrieve Stores where isset category Path -+ * - * Return comma separated string - * - * @return string -@@ -835,6 +831,7 @@ class Category extends \Magento\Catalog\Model\AbstractModel implements - - /** - * Get array categories ids which are part of category path -+ * - * Result array contain id of current category because it is part of the path - * - * @return array -@@ -1038,7 +1035,8 @@ class Category extends \Magento\Catalog\Model\AbstractModel implements - - /** - * Retrieve Available Product Listing Sort By -- * code as key, value - name -+ * -+ * Code as key, value - name - * - * @return array - */ -@@ -1121,10 +1119,15 @@ class Category extends \Magento\Catalog\Model\AbstractModel implements - } - } - $productIndexer = $this->indexerRegistry->get(Indexer\Category\Product::INDEXER_ID); -- if (!$productIndexer->isScheduled() -- && (!empty($this->getAffectedProductIds()) || $this->dataHasChangedFor('is_anchor')) -- ) { -- $productIndexer->reindexList($this->getPathIds()); -+ -+ if (!empty($this->getAffectedProductIds()) -+ || $this->dataHasChangedFor('is_anchor') -+ || $this->dataHasChangedFor('is_active')) { -+ if (!$productIndexer->isScheduled()) { -+ $productIndexer->reindexList($this->getPathIds()); -+ } else { -+ $productIndexer->invalidate(); -+ } - } - } - -@@ -1149,16 +1152,27 @@ class Category extends \Magento\Catalog\Model\AbstractModel implements - $identities = [ - self::CACHE_TAG . '_' . $this->getId(), - ]; -- if (!$this->getId() || $this->hasDataChanges() -- || $this->isDeleted() || $this->dataHasChangedFor(self::KEY_INCLUDE_IN_MENU) -- ) { -+ -+ if ($this->hasDataChanges()) { -+ $identities[] = Product::CACHE_PRODUCT_CATEGORY_TAG . '_' . $this->getId(); -+ } -+ -+ if ($this->dataHasChangedFor('is_anchor') || $this->dataHasChangedFor('is_active')) { -+ foreach ($this->getPathIds() as $id) { -+ $identities[] = Product::CACHE_PRODUCT_CATEGORY_TAG . '_' . $id; -+ } -+ } -+ -+ if (!$this->getId() || $this->isDeleted() || $this->dataHasChangedFor(self::KEY_INCLUDE_IN_MENU)) { - $identities[] = self::CACHE_TAG; - $identities[] = Product::CACHE_PRODUCT_CATEGORY_TAG . '_' . $this->getId(); - } -- return $identities; -+ return array_unique($identities); - } - - /** -+ * Returns path -+ * - * @codeCoverageIgnoreStart - * @return string|null - */ -@@ -1168,6 +1182,8 @@ class Category extends \Magento\Catalog\Model\AbstractModel implements - } - - /** -+ * Returns position -+ * - * @return int|null - */ - public function getPosition() -@@ -1176,6 +1192,8 @@ class Category extends \Magento\Catalog\Model\AbstractModel implements - } - - /** -+ * Returns children count -+ * - * @return int - */ - public function getChildrenCount() -@@ -1184,6 +1202,8 @@ class Category extends \Magento\Catalog\Model\AbstractModel implements - } - - /** -+ * Returns created at -+ * - * @return string|null - */ - public function getCreatedAt() -@@ -1192,6 +1212,8 @@ class Category extends \Magento\Catalog\Model\AbstractModel implements - } - - /** -+ * Returns updated at -+ * - * @return string|null - */ - public function getUpdatedAt() -@@ -1200,6 +1222,8 @@ class Category extends \Magento\Catalog\Model\AbstractModel implements - } - - /** -+ * Returns is active -+ * - * @return bool - * @SuppressWarnings(PHPMD.BooleanGetMethodName) - */ -@@ -1209,6 +1233,8 @@ class Category extends \Magento\Catalog\Model\AbstractModel implements - } - - /** -+ * Returns category id -+ * - * @return int|null - */ - public function getCategoryId() -@@ -1217,6 +1243,8 @@ class Category extends \Magento\Catalog\Model\AbstractModel implements - } - - /** -+ * Returns display mode -+ * - * @return string|null - */ - public function getDisplayMode() -@@ -1225,6 +1253,8 @@ class Category extends \Magento\Catalog\Model\AbstractModel implements - } - - /** -+ * Returns is include in menu -+ * - * @return bool|null - */ - public function getIncludeInMenu() -@@ -1233,6 +1263,8 @@ class Category extends \Magento\Catalog\Model\AbstractModel implements - } - - /** -+ * Returns url key -+ * - * @return string|null - */ - public function getUrlKey() -@@ -1241,6 +1273,8 @@ class Category extends \Magento\Catalog\Model\AbstractModel implements - } - - /** -+ * Returns children data -+ * - * @return \Magento\Catalog\Api\Data\CategoryTreeInterface[]|null - */ - public function getChildrenData() -@@ -1356,6 +1390,8 @@ class Category extends \Magento\Catalog\Model\AbstractModel implements - } - - /** -+ * Set updated at -+ * - * @param string $updatedAt - * @return $this - */ -@@ -1365,6 +1401,8 @@ class Category extends \Magento\Catalog\Model\AbstractModel implements - } - - /** -+ * Set created at -+ * - * @param string $createdAt - * @return $this - */ -@@ -1374,6 +1412,8 @@ class Category extends \Magento\Catalog\Model\AbstractModel implements - } - - /** -+ * Set path -+ * - * @param string $path - * @return $this - */ -@@ -1383,6 +1423,8 @@ class Category extends \Magento\Catalog\Model\AbstractModel implements - } - - /** -+ * Set available sort by -+ * - * @param string[]|string $availableSortBy - * @return $this - */ -@@ -1392,6 +1434,8 @@ class Category extends \Magento\Catalog\Model\AbstractModel implements - } - - /** -+ * Set include in menu -+ * - * @param bool $includeInMenu - * @return $this - */ -@@ -1412,6 +1456,8 @@ class Category extends \Magento\Catalog\Model\AbstractModel implements - } - - /** -+ * Set children data -+ * - * @param \Magento\Catalog\Api\Data\CategoryTreeInterface[] $childrenData - * @return $this - */ -@@ -1421,7 +1467,7 @@ class Category extends \Magento\Catalog\Model\AbstractModel implements - } - - /** -- * {@inheritdoc} -+ * @inheritdoc - * - * @return \Magento\Catalog\Api\Data\CategoryExtensionInterface|null - */ -@@ -1431,7 +1477,7 @@ class Category extends \Magento\Catalog\Model\AbstractModel implements - } - - /** -- * {@inheritdoc} -+ * @inheritdoc - * - * @param \Magento\Catalog\Api\Data\CategoryExtensionInterface $extensionAttributes - * @return $this -diff --git a/app/code/Magento/Catalog/Model/Category/Attribute/Source/Layout.php b/app/code/Magento/Catalog/Model/Category/Attribute/Source/Layout.php -index 1890ea0f7d9..20ea899a3d0 100644 ---- a/app/code/Magento/Catalog/Model/Category/Attribute/Source/Layout.php -+++ b/app/code/Magento/Catalog/Model/Category/Attribute/Source/Layout.php -@@ -17,6 +17,12 @@ class Layout extends \Magento\Eav\Model\Entity\Attribute\Source\AbstractSource - */ - protected $pageLayoutBuilder; - -+ /** -+ * @inheritdoc -+ * @deprecated since the cache is now handled by \Magento\Theme\Model\PageLayout\Config\Builder::$configFiles -+ */ -+ protected $_options = null; -+ - /** - * @param \Magento\Framework\View\Model\PageLayout\Config\BuilderInterface $pageLayoutBuilder - */ -@@ -26,14 +32,14 @@ class Layout extends \Magento\Eav\Model\Entity\Attribute\Source\AbstractSource - } - - /** -- * {@inheritdoc} -+ * @inheritdoc - */ - public function getAllOptions() - { -- if (!$this->_options) { -- $this->_options = $this->pageLayoutBuilder->getPageLayoutsConfig()->toOptionArray(); -- array_unshift($this->_options, ['value' => '', 'label' => __('No layout updates')]); -- } -- return $this->_options; -+ $options = $this->pageLayoutBuilder->getPageLayoutsConfig()->toOptionArray(); -+ array_unshift($options, ['value' => '', 'label' => __('No layout updates')]); -+ $this->_options = $options; -+ -+ return $options; - } - } -diff --git a/app/code/Magento/Catalog/Model/Category/FileInfo.php b/app/code/Magento/Catalog/Model/Category/FileInfo.php -index 9715bb2b161..cd3592ef40a 100644 ---- a/app/code/Magento/Catalog/Model/Category/FileInfo.php -+++ b/app/code/Magento/Catalog/Model/Category/FileInfo.php -@@ -43,6 +43,11 @@ class FileInfo - */ - private $baseDirectory; - -+ /** -+ * @var ReadInterface -+ */ -+ private $pubDirectory; -+ - /** - * @param Filesystem $filesystem - * @param Mime $mime -@@ -82,6 +87,20 @@ class FileInfo - return $this->baseDirectory; - } - -+ /** -+ * Get Pub Directory read instance -+ * -+ * @return ReadInterface -+ */ -+ private function getPubDirectory() -+ { -+ if (!isset($this->pubDirectory)) { -+ $this->pubDirectory = $this->filesystem->getDirectoryRead(DirectoryList::PUB); -+ } -+ -+ return $this->pubDirectory; -+ } -+ - /** - * Retrieve MIME type of requested file - * -@@ -135,7 +154,7 @@ class FileInfo - { - $filePath = ltrim($fileName, '/'); - -- $mediaDirectoryRelativeSubpath = $this->getMediaDirectoryPathRelativeToBaseDirectoryPath(); -+ $mediaDirectoryRelativeSubpath = $this->getMediaDirectoryPathRelativeToBaseDirectoryPath($filePath); - $isFileNameBeginsWithMediaDirectoryPath = $this->isBeginsWithMediaDirectoryPath($fileName); - - // if the file is not using a relative path, it resides in the catalog/category media directory -@@ -160,7 +179,7 @@ class FileInfo - { - $filePath = ltrim($fileName, '/'); - -- $mediaDirectoryRelativeSubpath = $this->getMediaDirectoryPathRelativeToBaseDirectoryPath(); -+ $mediaDirectoryRelativeSubpath = $this->getMediaDirectoryPathRelativeToBaseDirectoryPath($filePath); - $isFileNameBeginsWithMediaDirectoryPath = strpos($filePath, $mediaDirectoryRelativeSubpath) === 0; - - return $isFileNameBeginsWithMediaDirectoryPath; -@@ -169,14 +188,22 @@ class FileInfo - /** - * Get media directory subpath relative to base directory path - * -+ * @param string $filePath - * @return string - */ -- private function getMediaDirectoryPathRelativeToBaseDirectoryPath() -+ private function getMediaDirectoryPathRelativeToBaseDirectoryPath(string $filePath = '') - { -- $baseDirectoryPath = $this->getBaseDirectory()->getAbsolutePath(); -+ $baseDirectory = $this->getBaseDirectory(); -+ $baseDirectoryPath = $baseDirectory->getAbsolutePath(); - $mediaDirectoryPath = $this->getMediaDirectory()->getAbsolutePath(); -+ $pubDirectoryPath = $this->getPubDirectory()->getAbsolutePath(); - - $mediaDirectoryRelativeSubpath = substr($mediaDirectoryPath, strlen($baseDirectoryPath)); -+ $pubDirectory = $baseDirectory->getRelativePath($pubDirectoryPath); -+ -+ if (strpos($mediaDirectoryRelativeSubpath, $pubDirectory) === 0 && strpos($filePath, $pubDirectory) !== 0) { -+ $mediaDirectoryRelativeSubpath = substr($mediaDirectoryRelativeSubpath, strlen($pubDirectory)); -+ } - - return $mediaDirectoryRelativeSubpath; - } -diff --git a/app/code/Magento/Catalog/Model/Category/Link/SaveHandler.php b/app/code/Magento/Catalog/Model/Category/Link/SaveHandler.php -index f22c6903a23..4ea06d4e34d 100644 ---- a/app/code/Magento/Catalog/Model/Category/Link/SaveHandler.php -+++ b/app/code/Magento/Catalog/Model/Category/Link/SaveHandler.php -@@ -6,7 +6,6 @@ - namespace Magento\Catalog\Model\Category\Link; - - use Magento\Catalog\Api\Data\CategoryLinkInterface; --use Magento\Catalog\Model\Indexer\Product\Category; - use Magento\Framework\EntityManager\Operation\ExtensionInterface; - - /** -@@ -40,6 +39,8 @@ class SaveHandler implements ExtensionInterface - } - - /** -+ * Execute -+ * - * @param object $entity - * @param array $arguments - * @return object -@@ -78,6 +79,8 @@ class SaveHandler implements ExtensionInterface - } - - /** -+ * Get category links positions -+ * - * @param object $entity - * @return array - */ -@@ -106,27 +109,19 @@ class SaveHandler implements ExtensionInterface - */ - private function mergeCategoryLinks($newCategoryPositions, $oldCategoryPositions) - { -- $result = []; - if (empty($newCategoryPositions)) { -- return $result; -+ return []; - } - -+ $categoryPositions = array_combine(array_column($oldCategoryPositions, 'category_id'), $oldCategoryPositions); - foreach ($newCategoryPositions as $newCategoryPosition) { -- $key = array_search( -- $newCategoryPosition['category_id'], -- array_column($oldCategoryPositions, 'category_id') -- ); -- -- if ($key === false) { -- $result[] = $newCategoryPosition; -- } elseif (isset($oldCategoryPositions[$key]) -- && $oldCategoryPositions[$key]['position'] != $newCategoryPosition['position'] -- ) { -- $result[] = $newCategoryPositions[$key]; -- unset($oldCategoryPositions[$key]); -+ $categoryId = $newCategoryPosition['category_id']; -+ if (!isset($categoryPositions[$categoryId])) { -+ $categoryPositions[$categoryId] = ['category_id' => $categoryId]; - } -+ $categoryPositions[$categoryId]['position'] = $newCategoryPosition['position']; - } -- $result = array_merge($result, $oldCategoryPositions); -+ $result = array_values($categoryPositions); - - return $result; - } -diff --git a/app/code/Magento/Catalog/Model/Category/Product/PositionResolver.php b/app/code/Magento/Catalog/Model/Category/Product/PositionResolver.php -index 1e07c0cdd92..44bf153f836 100644 ---- a/app/code/Magento/Catalog/Model/Category/Product/PositionResolver.php -+++ b/app/code/Magento/Catalog/Model/Category/Product/PositionResolver.php -@@ -43,6 +43,8 @@ class PositionResolver extends \Magento\Framework\Model\ResourceModel\Db\Abstrac - $categoryId - )->order( - 'ccp.position ' . \Magento\Framework\DB\Select::SQL_ASC -+ )->order( -+ 'ccp.product_id ' . \Magento\Framework\DB\Select::SQL_DESC - ); - - return array_flip($connection->fetchCol($select)); -diff --git a/app/code/Magento/Catalog/Model/Category/StoreCategories.php b/app/code/Magento/Catalog/Model/Category/StoreCategories.php -new file mode 100644 -index 00000000000..0d7afcd4a50 ---- /dev/null -+++ b/app/code/Magento/Catalog/Model/Category/StoreCategories.php -@@ -0,0 +1,64 @@ -+<?php -+/** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+declare(strict_types=1); -+ -+namespace Magento\Catalog\Model\Category; -+ -+use Magento\Catalog\Api\CategoryRepositoryInterface; -+use Magento\Catalog\Model\Category; -+use Magento\Store\Api\GroupRepositoryInterface; -+ -+/** -+ * Fetcher for associated with store group categories. -+ */ -+class StoreCategories -+{ -+ /** -+ * @var CategoryRepositoryInterface -+ */ -+ private $categoryRepository; -+ -+ /** -+ * @var GroupRepositoryInterface -+ */ -+ private $groupRepository; -+ -+ /** -+ * @param CategoryRepositoryInterface $categoryRepository -+ * @param GroupRepositoryInterface $groupRepository -+ */ -+ public function __construct( -+ CategoryRepositoryInterface $categoryRepository, -+ GroupRepositoryInterface $groupRepository -+ ) { -+ $this->categoryRepository = $categoryRepository; -+ $this->groupRepository = $groupRepository; -+ } -+ -+ /** -+ * Get all category ids for store. -+ * -+ * @param int|null $storeGroupId -+ * @return int[] -+ * @throws \Magento\Framework\Exception\NoSuchEntityException -+ */ -+ public function getCategoryIds(?int $storeGroupId = null): array -+ { -+ $rootCategoryId = $storeGroupId -+ ? $this->groupRepository->get($storeGroupId)->getRootCategoryId() -+ : Category::TREE_ROOT_ID; -+ /** @var Category $rootCategory */ -+ $rootCategory = $this->categoryRepository->get($rootCategoryId); -+ $categoriesIds = array_map( -+ function ($value) { -+ return (int) $value; -+ }, -+ (array) $rootCategory->getAllChildren(true) -+ ); -+ -+ return $categoriesIds; -+ } -+} -diff --git a/app/code/Magento/Catalog/Model/Category/Tree.php b/app/code/Magento/Catalog/Model/Category/Tree.php -index 6080f74d5fa..0a9cb25d7b0 100644 ---- a/app/code/Magento/Catalog/Model/Category/Tree.php -+++ b/app/code/Magento/Catalog/Model/Category/Tree.php -@@ -32,27 +32,40 @@ class Tree - */ - protected $treeFactory; - -+ /** -+ * @var \Magento\Catalog\Model\ResourceModel\Category\TreeFactory -+ */ -+ private $treeResourceFactory; -+ - /** - * @param \Magento\Catalog\Model\ResourceModel\Category\Tree $categoryTree - * @param \Magento\Store\Model\StoreManagerInterface $storeManager - * @param \Magento\Catalog\Model\ResourceModel\Category\Collection $categoryCollection - * @param \Magento\Catalog\Api\Data\CategoryTreeInterfaceFactory $treeFactory -+ * @param \Magento\Catalog\Model\ResourceModel\Category\TreeFactory|null $treeResourceFactory - */ - public function __construct( - \Magento\Catalog\Model\ResourceModel\Category\Tree $categoryTree, - \Magento\Store\Model\StoreManagerInterface $storeManager, - \Magento\Catalog\Model\ResourceModel\Category\Collection $categoryCollection, -- \Magento\Catalog\Api\Data\CategoryTreeInterfaceFactory $treeFactory -+ \Magento\Catalog\Api\Data\CategoryTreeInterfaceFactory $treeFactory, -+ \Magento\Catalog\Model\ResourceModel\Category\TreeFactory $treeResourceFactory = null - ) { - $this->categoryTree = $categoryTree; - $this->storeManager = $storeManager; - $this->categoryCollection = $categoryCollection; - $this->treeFactory = $treeFactory; -+ $this->treeResourceFactory = $treeResourceFactory ?? \Magento\Framework\App\ObjectManager::getInstance() -+ ->get(\Magento\Catalog\Model\ResourceModel\Category\TreeFactory::class); - } - - /** -+ * Get root node by category. -+ * - * @param \Magento\Catalog\Model\Category|null $category - * @return Node|null -+ * @throws \Magento\Framework\Exception\LocalizedException -+ * @throws \Magento\Framework\Exception\NoSuchEntityException - */ - public function getRootNode($category = null) - { -@@ -71,13 +84,18 @@ class Tree - } - - /** -+ * Get node by category. -+ * - * @param \Magento\Catalog\Model\Category $category - * @return Node -+ * @throws \Magento\Framework\Exception\LocalizedException -+ * @throws \Magento\Framework\Exception\NoSuchEntityException - */ - protected function getNode(\Magento\Catalog\Model\Category $category) - { - $nodeId = $category->getId(); -- $node = $this->categoryTree->loadNode($nodeId); -+ $categoryTree = $this->treeResourceFactory->create(); -+ $node = $categoryTree->loadNode($nodeId); - $node->loadChildren(); - $this->prepareCollection(); - $this->categoryTree->addCollectionData($this->categoryCollection); -@@ -85,7 +103,11 @@ class Tree - } - - /** -+ * Prepare category collection. -+ * - * @return void -+ * @throws \Magento\Framework\Exception\LocalizedException -+ * @throws \Magento\Framework\Exception\NoSuchEntityException - */ - protected function prepareCollection() - { -@@ -104,6 +126,8 @@ class Tree - } - - /** -+ * Get tree by node. -+ * - * @param \Magento\Framework\Data\Tree\Node $node - * @param int $depth - * @param int $currentLevel -@@ -127,6 +151,8 @@ class Tree - } - - /** -+ * Get node children. -+ * - * @param \Magento\Framework\Data\Tree\Node $node - * @param int $depth - * @param int $currentLevel -diff --git a/app/code/Magento/Catalog/Model/CategoryList.php b/app/code/Magento/Catalog/Model/CategoryList.php -index 790ea6b921f..cab8e013d9b 100644 ---- a/app/code/Magento/Catalog/Model/CategoryList.php -+++ b/app/code/Magento/Catalog/Model/CategoryList.php -@@ -15,6 +15,9 @@ use Magento\Framework\Api\ExtensionAttribute\JoinProcessorInterface; - use Magento\Framework\Api\SearchCriteriaInterface; - use Magento\Framework\Api\SearchCriteria\CollectionProcessorInterface; - -+/** -+ * Class for getting category list. -+ */ - class CategoryList implements CategoryListInterface - { - /** -@@ -64,7 +67,7 @@ class CategoryList implements CategoryListInterface - } - - /** -- * {@inheritdoc} -+ * @inheritdoc - */ - public function getList(SearchCriteriaInterface $searchCriteria) - { -diff --git a/app/code/Magento/Catalog/Model/Config.php b/app/code/Magento/Catalog/Model/Config.php -index d2ffd6f4400..5dce940308a 100644 ---- a/app/code/Magento/Catalog/Model/Config.php -+++ b/app/code/Magento/Catalog/Model/Config.php -@@ -381,7 +381,7 @@ class Config extends \Magento\Eav\Model\Config - - $this->loadProductTypes(); - -- return isset($this->_productTypesById[$id]) ? $this->_productTypesById[$id] : false; -+ return $this->_productTypesById[$id] ?? false; - } - - /** -diff --git a/app/code/Magento/Catalog/Model/Config/CatalogClone/Media/Image.php b/app/code/Magento/Catalog/Model/Config/CatalogClone/Media/Image.php -index e2b0a915740..10675a7b7c7 100644 ---- a/app/code/Magento/Catalog/Model/Config/CatalogClone/Media/Image.php -+++ b/app/code/Magento/Catalog/Model/Config/CatalogClone/Media/Image.php -@@ -5,6 +5,9 @@ - */ - namespace Magento\Catalog\Model\Config\CatalogClone\Media; - -+use Magento\Framework\Escaper; -+use Magento\Framework\App\ObjectManager; -+ - /** - * Clone model for media images related config fields - * -@@ -26,6 +29,11 @@ class Image extends \Magento\Framework\App\Config\Value - */ - protected $_attributeCollectionFactory; - -+ /** -+ * @var Escaper -+ */ -+ private $escaper; -+ - /** - * @param \Magento\Framework\Model\Context $context - * @param \Magento\Framework\Registry $registry -@@ -36,6 +44,9 @@ class Image extends \Magento\Framework\App\Config\Value - * @param \Magento\Framework\Model\ResourceModel\AbstractResource $resource - * @param \Magento\Framework\Data\Collection\AbstractDb $resourceCollection - * @param array $data -+ * @param Escaper|null $escaper -+ * -+ * @SuppressWarnings(PHPMD.ExcessiveParameterList) - */ - public function __construct( - \Magento\Framework\Model\Context $context, -@@ -46,8 +57,10 @@ class Image extends \Magento\Framework\App\Config\Value - \Magento\Eav\Model\Config $eavConfig, - \Magento\Framework\Model\ResourceModel\AbstractResource $resource = null, - \Magento\Framework\Data\Collection\AbstractDb $resourceCollection = null, -- array $data = [] -+ array $data = [], -+ Escaper $escaper = null - ) { -+ $this->escaper = $escaper ?? ObjectManager::getInstance()->get(Escaper::class); - $this->_attributeCollectionFactory = $attributeCollectionFactory; - $this->_eavConfig = $eavConfig; - parent::__construct($context, $registry, $config, $cacheTypeList, $resource, $resourceCollection, $data); -@@ -71,10 +84,9 @@ class Image extends \Magento\Framework\App\Config\Value - $prefixes = []; - - foreach ($collection as $attribute) { -- /* @var $attribute \Magento\Eav\Model\Entity\Attribute */ - $prefixes[] = [ - 'field' => $attribute->getAttributeCode() . '_', -- 'label' => $attribute->getFrontend()->getLabel(), -+ 'label' => $this->escaper->escapeHtml($attribute->getFrontend()->getLabel()), - ]; - } - -diff --git a/app/code/Magento/Catalog/Model/Design.php b/app/code/Magento/Catalog/Model/Design.php -index bd7cdabb408..853bbeac8eb 100644 ---- a/app/code/Magento/Catalog/Model/Design.php -+++ b/app/code/Magento/Catalog/Model/Design.php -@@ -5,6 +5,8 @@ - */ - namespace Magento\Catalog\Model; - -+use \Magento\Framework\TranslateInterface; -+ - /** - * Catalog Custom Category design Model - * -@@ -31,14 +33,20 @@ class Design extends \Magento\Framework\Model\AbstractModel - */ - protected $_localeDate; - -+ /** -+ * @var TranslateInterface -+ */ -+ private $translator; -+ - /** - * @param \Magento\Framework\Model\Context $context - * @param \Magento\Framework\Registry $registry - * @param \Magento\Framework\Stdlib\DateTime\TimezoneInterface $localeDate - * @param \Magento\Framework\View\DesignInterface $design -- * @param \Magento\Framework\Model\ResourceModel\AbstractResource $resource -- * @param \Magento\Framework\Data\Collection\AbstractDb $resourceCollection -+ * @param \Magento\Framework\Model\ResourceModel\AbstractResource|null $resource -+ * @param \Magento\Framework\Data\Collection\AbstractDb|null $resourceCollection - * @param array $data -+ * @param TranslateInterface|null $translator - */ - public function __construct( - \Magento\Framework\Model\Context $context, -@@ -47,10 +55,13 @@ class Design extends \Magento\Framework\Model\AbstractModel - \Magento\Framework\View\DesignInterface $design, - \Magento\Framework\Model\ResourceModel\AbstractResource $resource = null, - \Magento\Framework\Data\Collection\AbstractDb $resourceCollection = null, -- array $data = [] -+ array $data = [], -+ TranslateInterface $translator = null - ) { - $this->_localeDate = $localeDate; - $this->_design = $design; -+ $this->translator = $translator ?: -+ \Magento\Framework\App\ObjectManager::getInstance()->get(TranslateInterface::class); - parent::__construct($context, $registry, $resource, $resourceCollection, $data); - } - -@@ -63,6 +74,7 @@ class Design extends \Magento\Framework\Model\AbstractModel - public function applyCustomDesign($design) - { - $this->_design->setDesignTheme($design); -+ $this->translator->loadData(null, true); - return $this; - } - -diff --git a/app/code/Magento/Catalog/Model/Entity/GetCategoryCustomAttributeCodes.php b/app/code/Magento/Catalog/Model/Entity/GetCategoryCustomAttributeCodes.php -deleted file mode 100644 -index b2b9199cc56..00000000000 ---- a/app/code/Magento/Catalog/Model/Entity/GetCategoryCustomAttributeCodes.php -+++ /dev/null -@@ -1,37 +0,0 @@ --<?php --/** -- * Copyright © Magento, Inc. All rights reserved. -- * See COPYING.txt for license details. -- */ -- --namespace Magento\Catalog\Model\Entity; -- --use Magento\Catalog\Api\Data\CategoryInterface; --use Magento\Eav\Model\Entity\GetCustomAttributeCodesInterface; --use Magento\Framework\Api\MetadataServiceInterface; -- --class GetCategoryCustomAttributeCodes implements GetCustomAttributeCodesInterface --{ -- /** -- * @var GetCustomAttributeCodesInterface -- */ -- private $baseCustomAttributeCodes; -- -- /** -- * @param GetCustomAttributeCodesInterface $baseCustomAttributeCodes -- */ -- public function __construct( -- GetCustomAttributeCodesInterface $baseCustomAttributeCodes -- ) { -- $this->baseCustomAttributeCodes = $baseCustomAttributeCodes; -- } -- -- /** -- * @inheritdoc -- */ -- public function execute(MetadataServiceInterface $metadataService): array -- { -- $customAttributesCodes = $this->baseCustomAttributeCodes->execute($metadataService); -- return array_diff($customAttributesCodes, CategoryInterface::ATTRIBUTES); -- } --} -diff --git a/app/code/Magento/Catalog/Model/Entity/GetProductCustomAttributeCodes.php b/app/code/Magento/Catalog/Model/Entity/GetProductCustomAttributeCodes.php -deleted file mode 100644 -index 23678ffcf48..00000000000 ---- a/app/code/Magento/Catalog/Model/Entity/GetProductCustomAttributeCodes.php -+++ /dev/null -@@ -1,37 +0,0 @@ --<?php --/** -- * Copyright © Magento, Inc. All rights reserved. -- * See COPYING.txt for license details. -- */ -- --namespace Magento\Catalog\Model\Entity; -- --use Magento\Catalog\Api\Data\ProductInterface; --use Magento\Eav\Model\Entity\GetCustomAttributeCodesInterface; --use Magento\Framework\Api\MetadataServiceInterface; -- --class GetProductCustomAttributeCodes implements GetCustomAttributeCodesInterface --{ -- /** -- * @var GetCustomAttributeCodesInterface -- */ -- private $baseCustomAttributeCodes; -- -- /** -- * @param GetCustomAttributeCodesInterface $baseCustomAttributeCodes -- */ -- public function __construct( -- GetCustomAttributeCodesInterface $baseCustomAttributeCodes -- ) { -- $this->baseCustomAttributeCodes = $baseCustomAttributeCodes; -- } -- -- /** -- * @inheritdoc -- */ -- public function execute(MetadataServiceInterface $metadataService): array -- { -- $customAttributesCodes = $this->baseCustomAttributeCodes->execute($metadataService); -- return array_diff($customAttributesCodes, ProductInterface::ATTRIBUTES); -- } --} -diff --git a/app/code/Magento/Catalog/Model/FilterProductCustomAttribute.php b/app/code/Magento/Catalog/Model/FilterProductCustomAttribute.php -new file mode 100644 -index 00000000000..497ed2fd499 ---- /dev/null -+++ b/app/code/Magento/Catalog/Model/FilterProductCustomAttribute.php -@@ -0,0 +1,38 @@ -+<?php -+/** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+declare(strict_types=1); -+ -+namespace Magento\Catalog\Model; -+ -+/** -+ * Filter custom attributes for product using the blacklist -+ */ -+class FilterProductCustomAttribute -+{ -+ /** -+ * @var array -+ */ -+ private $blackList; -+ -+ /** -+ * @param array $blackList -+ */ -+ public function __construct(array $blackList = []) -+ { -+ $this->blackList = $blackList; -+ } -+ -+ /** -+ * Delete custom attribute -+ * -+ * @param array $attributes set objects attributes @example ['attribute_code'=>'attribute_object'] -+ * @return array -+ */ -+ public function execute(array $attributes): array -+ { -+ return array_diff_key($attributes, array_flip($this->blackList)); -+ } -+} -diff --git a/app/code/Magento/Catalog/Model/ImageExtractor.php b/app/code/Magento/Catalog/Model/ImageExtractor.php -index d2c11a37629..1cb1f305a22 100644 ---- a/app/code/Magento/Catalog/Model/ImageExtractor.php -+++ b/app/code/Magento/Catalog/Model/ImageExtractor.php -@@ -9,6 +9,9 @@ use Magento\Catalog\Helper\Image; - use Magento\Catalog\Model\Product\Attribute\Backend\Media\ImageEntryConverter; - use Magento\Framework\View\Xsd\Media\TypeDataExtractorInterface; - -+/** -+ * Image extractor from xml configuration -+ */ - class ImageExtractor implements TypeDataExtractorInterface - { - /** -@@ -17,6 +20,7 @@ class ImageExtractor implements TypeDataExtractorInterface - * @param \DOMElement $mediaNode - * @param string $mediaParentTag - * @return array -+ * @SuppressWarnings(PHPMD.CyclomaticComplexity) - */ - public function process(\DOMElement $mediaNode, $mediaParentTag) - { -@@ -32,12 +36,22 @@ class ImageExtractor implements TypeDataExtractorInterface - continue; - } - $attributeTagName = $attribute->tagName; -- if ($attributeTagName === 'background') { -- $nodeValue = $this->processImageBackground($attribute->nodeValue); -- } elseif ($attributeTagName === 'width' || $attributeTagName === 'height') { -- $nodeValue = intval($attribute->nodeValue); -+ if ((bool)$attribute->getAttribute('xsi:nil') !== true) { -+ if ($attributeTagName === 'background') { -+ $nodeValue = $this->processImageBackground($attribute->nodeValue); -+ } elseif ($attributeTagName === 'width' || $attributeTagName === 'height') { -+ $nodeValue = (int) $attribute->nodeValue; -+ } elseif ($attributeTagName === 'constrain' -+ || $attributeTagName === 'aspect_ratio' -+ || $attributeTagName === 'frame' -+ || $attributeTagName === 'transparency' -+ ) { -+ $nodeValue = in_array($attribute->nodeValue, [true, 1, 'true', '1'], true) ?? false; -+ } else { -+ $nodeValue = $attribute->nodeValue; -+ } - } else { -- $nodeValue = !in_array($attribute->nodeValue, ['false', '0']); -+ $nodeValue = null; - } - $result[$mediaParentTag][$moduleNameImage][Image::MEDIA_TYPE_CONFIG_NODE][$imageId][$attribute->tagName] - = $nodeValue; -diff --git a/app/code/Magento/Catalog/Model/ImageUploader.php b/app/code/Magento/Catalog/Model/ImageUploader.php -index ce92a2c1d95..b5ca0895d6d 100644 ---- a/app/code/Magento/Catalog/Model/ImageUploader.php -+++ b/app/code/Magento/Catalog/Model/ImageUploader.php -@@ -67,14 +67,9 @@ class ImageUploader - /** - * List of allowed image mime types - * -- * @var array -+ * @var string[] - */ -- private $allowedMimeTypes = [ -- 'image/jpg', -- 'image/jpeg', -- 'image/gif', -- 'image/png', -- ]; -+ private $allowedMimeTypes; - - /** - * ImageUploader constructor -@@ -87,6 +82,7 @@ class ImageUploader - * @param string $baseTmpPath - * @param string $basePath - * @param string[] $allowedExtensions -+ * @param string[] $allowedMimeTypes - */ - public function __construct( - \Magento\MediaStorage\Helper\File\Storage\Database $coreFileStorageDatabase, -@@ -96,7 +92,8 @@ class ImageUploader - \Psr\Log\LoggerInterface $logger, - $baseTmpPath, - $basePath, -- $allowedExtensions -+ $allowedExtensions, -+ $allowedMimeTypes = [] - ) { - $this->coreFileStorageDatabase = $coreFileStorageDatabase; - $this->mediaDirectory = $filesystem->getDirectoryWrite(\Magento\Framework\App\Filesystem\DirectoryList::MEDIA); -@@ -106,6 +103,7 @@ class ImageUploader - $this->baseTmpPath = $baseTmpPath; - $this->basePath = $basePath; - $this->allowedExtensions = $allowedExtensions; -+ $this->allowedMimeTypes = $allowedMimeTypes; - } - - /** -@@ -165,7 +163,7 @@ class ImageUploader - } - - /** -- * Retrieve base path -+ * Retrieve allowed extensions - * - * @return string[] - */ -diff --git a/app/code/Magento/Catalog/Model/Indexer/Category/Flat/AbstractAction.php b/app/code/Magento/Catalog/Model/Indexer/Category/Flat/AbstractAction.php -index 4b16a4810c0..1506ccf6963 100644 ---- a/app/code/Magento/Catalog/Model/Indexer/Category/Flat/AbstractAction.php -+++ b/app/code/Magento/Catalog/Model/Indexer/Category/Flat/AbstractAction.php -@@ -7,8 +7,10 @@ - namespace Magento\Catalog\Model\Indexer\Category\Flat; - - use Magento\Framework\App\ResourceConnection; --use Magento\Framework\EntityManager\MetadataPool; - -+/** -+ * Abstract action class for category flat indexers. -+ */ - class AbstractAction - { - /** -@@ -111,7 +113,7 @@ class AbstractAction - public function getMainStoreTable($storeId = \Magento\Store\Model\Store::DEFAULT_STORE_ID) - { - if (is_string($storeId)) { -- $storeId = intval($storeId); -+ $storeId = (int) $storeId; - } - - $suffix = sprintf('store_%d', $storeId); -@@ -131,7 +133,7 @@ class AbstractAction - $table = $this->connection->newTable( - $tableName - )->setComment( -- sprintf("Catalog Category Flat", $tableName) -+ 'Catalog Category Flat' - ); - - //Adding columns -@@ -379,7 +381,7 @@ class AbstractAction - $linkField = $this->getCategoryMetadata()->getLinkField(); - foreach ($attributesType as $type) { - foreach ($this->getAttributeTypeValues($type, $entityIds, $storeId) as $row) { -- if (isset($row[$linkField]) && isset($row['attribute_id'])) { -+ if (isset($row[$linkField], $row['attribute_id'])) { - $attributeId = $row['attribute_id']; - if (isset($attributes[$attributeId])) { - $attributeCode = $attributes[$attributeId]['attribute_code']; -@@ -497,6 +499,8 @@ class AbstractAction - } - - /** -+ * Get category metadata instance. -+ * - * @return \Magento\Framework\EntityManager\EntityMetadata - */ - private function getCategoryMetadata() -@@ -510,6 +514,8 @@ class AbstractAction - } - - /** -+ * Get skip static columns instance. -+ * - * @return array - */ - private function getSkipStaticColumns() -diff --git a/app/code/Magento/Catalog/Model/Indexer/Category/Flat/Action/Full.php b/app/code/Magento/Catalog/Model/Indexer/Category/Flat/Action/Full.php -index 64a8f930d83..a62e3d8f83b 100644 ---- a/app/code/Magento/Catalog/Model/Indexer/Category/Flat/Action/Full.php -+++ b/app/code/Magento/Catalog/Model/Indexer/Category/Flat/Action/Full.php -@@ -5,6 +5,9 @@ - */ - namespace Magento\Catalog\Model\Indexer\Category\Flat\Action; - -+/** -+ * Class for full reindex flat categories -+ */ - class Full extends \Magento\Catalog\Model\Indexer\Category\Flat\AbstractAction - { - /** -@@ -92,6 +95,7 @@ class Full extends \Magento\Catalog\Model\Indexer\Category\Flat\AbstractAction - - /** - * Create table and add attributes as fields for specified store. -+ * - * This routine assumes that DDL operations are allowed - * - * @param int $store -@@ -109,6 +113,7 @@ class Full extends \Magento\Catalog\Model\Indexer\Category\Flat\AbstractAction - - /** - * Create category flat tables and add attributes as fields. -+ * - * Tables are created only if DDL operations are allowed - * - * @param \Magento\Store\Model\Store[] $stores if empty, create tables for all stores of the application -@@ -167,6 +172,44 @@ class Full extends \Magento\Catalog\Model\Indexer\Category\Flat\AbstractAction - return $this; - } - -+ /** -+ * Retrieve all actual Catalog Product Flat Table names -+ * -+ * @return string[] -+ */ -+ private function getActualStoreTablesForCategoryFlat(): array -+ { -+ $actualStoreTables = []; -+ foreach ($this->storeManager->getStores() as $store) { -+ $actualStoreTables[] = sprintf( -+ '%s_store_%s', -+ $this->connection->getTableName('catalog_category_flat'), -+ $store->getId() -+ ); -+ } -+ -+ return $actualStoreTables; -+ } -+ -+ /** -+ * Delete all category flat tables for not existing stores -+ * -+ * @return void -+ */ -+ private function deleteAbandonedStoreCategoryFlatTables(): void -+ { -+ $existentTables = $this->connection->getTables( -+ $this->connection->getTableName('catalog_category_flat_store_%') -+ ); -+ $actualStoreTables = $this->getActualStoreTablesForCategoryFlat(); -+ -+ $tablesToDelete = array_diff($existentTables, $actualStoreTables); -+ -+ foreach ($tablesToDelete as $table) { -+ $this->connection->dropTable($table); -+ } -+ } -+ - /** - * Transactional rebuild flat data from eav - * -@@ -182,7 +225,7 @@ class Full extends \Magento\Catalog\Model\Indexer\Category\Flat\AbstractAction - $stores = $this->storeManager->getStores(); - $this->populateFlatTables($stores); - $this->switchTables($stores); -- -+ $this->deleteAbandonedStoreCategoryFlatTables(); - $this->allowTableChanges = true; - - return $this; -diff --git a/app/code/Magento/Catalog/Model/Indexer/Category/Product/AbstractAction.php b/app/code/Magento/Catalog/Model/Indexer/Category/Product/AbstractAction.php -index 6a499883ac7..178f4172ce6 100644 ---- a/app/code/Magento/Catalog/Model/Indexer/Category/Product/AbstractAction.php -+++ b/app/code/Magento/Catalog/Model/Indexer/Category/Product/AbstractAction.php -@@ -123,6 +123,11 @@ abstract class AbstractAction - */ - private $queryGenerator; - -+ /** -+ * @var int -+ */ -+ private $currentStoreId = 0; -+ - /** - * @param ResourceConnection $resource - * @param \Magento\Store\Model\StoreManagerInterface $storeManager -@@ -164,6 +169,7 @@ abstract class AbstractAction - { - foreach ($this->storeManager->getStores() as $store) { - if ($this->getPathFromCategoryId($store->getRootCategoryId())) { -+ $this->currentStoreId = $store->getId(); - $this->reindexRootCategory($store); - $this->reindexAnchorCategories($store); - $this->reindexNonAnchorCategories($store); -@@ -586,6 +592,8 @@ abstract class AbstractAction - } - - /** -+ * Get temporary table name -+ * - * Get temporary table name for concurrent indexing in persistent connection - * Temp table name is NOT shared between action instances and each action has it's own temp tree index - * -@@ -597,7 +605,7 @@ abstract class AbstractAction - if (empty($this->tempTreeIndexTableName)) { - $this->tempTreeIndexTableName = $this->connection->getTableName('temp_catalog_category_tree_index') - . '_' -- . substr(md5(time() . random_int(0, 999999999)), 0, 8); -+ . substr(sha1(time() . random_int(0, 999999999)), 0, 8); - } - - return $this->tempTreeIndexTableName; -@@ -641,7 +649,6 @@ abstract class AbstractAction - ['child_id'], - ['type' => \Magento\Framework\DB\Adapter\AdapterInterface::INDEX_TYPE_INDEX] - ); -- - // Drop the temporary table in case it already exists on this (persistent?) connection. - $this->connection->dropTemporaryTable($temporaryName); - $this->connection->createTemporaryTable($temporaryTable); -@@ -659,11 +666,31 @@ abstract class AbstractAction - */ - protected function fillTempCategoryTreeIndex($temporaryName) - { -+ $isActiveAttributeId = $this->config->getAttribute( -+ \Magento\Catalog\Model\Category::ENTITY, -+ 'is_active' -+ )->getId(); -+ $categoryMetadata = $this->metadataPool->getMetadata(\Magento\Catalog\Api\Data\CategoryInterface::class); -+ $categoryLinkField = $categoryMetadata->getLinkField(); - $selects = $this->prepareSelectsByRange( - $this->connection->select() - ->from( - ['c' => $this->getTable('catalog_category_entity')], - ['entity_id', 'path'] -+ )->joinInner( -+ ['ccacd' => $this->getTable('catalog_category_entity_int')], -+ 'ccacd.' . $categoryLinkField . ' = c.' . $categoryLinkField . ' AND ccacd.store_id = 0' . -+ ' AND ccacd.attribute_id = ' . $isActiveAttributeId, -+ [] -+ )->joinLeft( -+ ['ccacs' => $this->getTable('catalog_category_entity_int')], -+ 'ccacs.' . $categoryLinkField . ' = c.' . $categoryLinkField -+ . ' AND ccacs.attribute_id = ccacd.attribute_id AND ccacs.store_id = ' . -+ $this->currentStoreId, -+ [] -+ )->where( -+ $this->connection->getIfNullSql('ccacs.value', 'ccacd.value') . ' = ?', -+ 1 - ), - 'entity_id' - ); -diff --git a/app/code/Magento/Catalog/Model/Indexer/Category/Product/Action/Full.php b/app/code/Magento/Catalog/Model/Indexer/Category/Product/Action/Full.php -index f8121b55dbf..eb59acb56c3 100644 ---- a/app/code/Magento/Catalog/Model/Indexer/Category/Product/Action/Full.php -+++ b/app/code/Magento/Catalog/Model/Indexer/Category/Product/Action/Full.php -@@ -3,33 +3,46 @@ - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -+ -+declare(strict_types=1); -+ - namespace Magento\Catalog\Model\Indexer\Category\Product\Action; - -+use Magento\Catalog\Api\Data\ProductInterface; -+use Magento\Catalog\Model\Config; -+use Magento\Catalog\Model\Indexer\Category\Product\AbstractAction; - use Magento\Catalog\Model\ResourceModel\Indexer\ActiveTableSwitcher; -+use Magento\Framework\App\ObjectManager; -+use Magento\Framework\DB\Adapter\AdapterInterface; - use Magento\Framework\DB\Query\Generator as QueryGenerator; - use Magento\Framework\App\ResourceConnection; -+use Magento\Framework\DB\Select; -+use Magento\Framework\EntityManager\MetadataPool; -+use Magento\Framework\Indexer\BatchProviderInterface; -+use Magento\Framework\Indexer\BatchSizeManagementInterface; - use Magento\Indexer\Model\ProcessManager; -+use Magento\Store\Model\Store; -+use Magento\Store\Model\StoreManagerInterface; - - /** - * Class Full reindex action - * -- * @package Magento\Catalog\Model\Indexer\Category\Product\Action - * @SuppressWarnings(PHPMD.CouplingBetweenObjects) - */ --class Full extends \Magento\Catalog\Model\Indexer\Category\Product\AbstractAction -+class Full extends AbstractAction - { - /** -- * @var \Magento\Framework\Indexer\BatchSizeManagementInterface -+ * @var BatchSizeManagementInterface - */ - private $batchSizeManagement; - - /** -- * @var \Magento\Framework\Indexer\BatchProviderInterface -+ * @var BatchProviderInterface - */ - private $batchProvider; - - /** -- * @var \Magento\Framework\EntityManager\MetadataPool -+ * @var MetadataPool - */ - protected $metadataPool; - -@@ -52,25 +65,25 @@ class Full extends \Magento\Catalog\Model\Indexer\Category\Product\AbstractActio - - /** - * @param ResourceConnection $resource -- * @param \Magento\Store\Model\StoreManagerInterface $storeManager -- * @param \Magento\Catalog\Model\Config $config -+ * @param StoreManagerInterface $storeManager -+ * @param Config $config - * @param QueryGenerator|null $queryGenerator -- * @param \Magento\Framework\Indexer\BatchSizeManagementInterface|null $batchSizeManagement -- * @param \Magento\Framework\Indexer\BatchProviderInterface|null $batchProvider -- * @param \Magento\Framework\EntityManager\MetadataPool|null $metadataPool -+ * @param BatchSizeManagementInterface|null $batchSizeManagement -+ * @param BatchProviderInterface|null $batchProvider -+ * @param MetadataPool|null $metadataPool - * @param int|null $batchRowsCount - * @param ActiveTableSwitcher|null $activeTableSwitcher - * @param ProcessManager $processManager - * @SuppressWarnings(PHPMD.ExcessiveParameterList) - */ - public function __construct( -- \Magento\Framework\App\ResourceConnection $resource, -- \Magento\Store\Model\StoreManagerInterface $storeManager, -- \Magento\Catalog\Model\Config $config, -+ ResourceConnection $resource, -+ StoreManagerInterface $storeManager, -+ Config $config, - QueryGenerator $queryGenerator = null, -- \Magento\Framework\Indexer\BatchSizeManagementInterface $batchSizeManagement = null, -- \Magento\Framework\Indexer\BatchProviderInterface $batchProvider = null, -- \Magento\Framework\EntityManager\MetadataPool $metadataPool = null, -+ BatchSizeManagementInterface $batchSizeManagement = null, -+ BatchProviderInterface $batchProvider = null, -+ MetadataPool $metadataPool = null, - $batchRowsCount = null, - ActiveTableSwitcher $activeTableSwitcher = null, - ProcessManager $processManager = null -@@ -81,15 +94,15 @@ class Full extends \Magento\Catalog\Model\Indexer\Category\Product\AbstractActio - $config, - $queryGenerator - ); -- $objectManager = \Magento\Framework\App\ObjectManager::getInstance(); -+ $objectManager = ObjectManager::getInstance(); - $this->batchSizeManagement = $batchSizeManagement ?: $objectManager->get( -- \Magento\Framework\Indexer\BatchSizeManagementInterface::class -+ BatchSizeManagementInterface::class - ); - $this->batchProvider = $batchProvider ?: $objectManager->get( -- \Magento\Framework\Indexer\BatchProviderInterface::class -+ BatchProviderInterface::class - ); - $this->metadataPool = $metadataPool ?: $objectManager->get( -- \Magento\Framework\EntityManager\MetadataPool::class -+ MetadataPool::class - ); - $this->batchRowsCount = $batchRowsCount; - $this->activeTableSwitcher = $activeTableSwitcher ?: $objectManager->get(ActiveTableSwitcher::class); -@@ -97,33 +110,39 @@ class Full extends \Magento\Catalog\Model\Indexer\Category\Product\AbstractActio - } - - /** -+ * Create the store tables -+ * - * @return void - */ -- private function createTables() -+ private function createTables(): void - { - foreach ($this->storeManager->getStores() as $store) { -- $this->tableMaintainer->createTablesForStore($store->getId()); -+ $this->tableMaintainer->createTablesForStore((int)$store->getId()); - } - } - - /** -+ * Truncates the replica tables -+ * - * @return void - */ -- private function clearReplicaTables() -+ private function clearReplicaTables(): void - { - foreach ($this->storeManager->getStores() as $store) { -- $this->connection->truncateTable($this->tableMaintainer->getMainReplicaTable($store->getId())); -+ $this->connection->truncateTable($this->tableMaintainer->getMainReplicaTable((int)$store->getId())); - } - } - - /** -+ * Switches the active table -+ * - * @return void - */ -- private function switchTables() -+ private function switchTables(): void - { - $tablesToSwitch = []; - foreach ($this->storeManager->getStores() as $store) { -- $tablesToSwitch[] = $this->tableMaintainer->getMainTable($store->getId()); -+ $tablesToSwitch[] = $this->tableMaintainer->getMainTable((int)$store->getId()); - } - $this->activeTableSwitcher->switchTable($this->connection, $tablesToSwitch); - } -@@ -133,12 +152,13 @@ class Full extends \Magento\Catalog\Model\Indexer\Category\Product\AbstractActio - * - * @return $this - */ -- public function execute() -+ public function execute(): self - { - $this->createTables(); - $this->clearReplicaTables(); - $this->reindex(); - $this->switchTables(); -+ - return $this; - } - -@@ -147,7 +167,7 @@ class Full extends \Magento\Catalog\Model\Indexer\Category\Product\AbstractActio - * - * @return void - */ -- protected function reindex() -+ protected function reindex(): void - { - $userFunctions = []; - -@@ -165,9 +185,9 @@ class Full extends \Magento\Catalog\Model\Indexer\Category\Product\AbstractActio - /** - * Execute indexation by store - * -- * @param \Magento\Store\Model\Store $store -+ * @param Store $store - */ -- private function reindexStore($store) -+ private function reindexStore($store): void - { - $this->reindexRootCategory($store); - $this->reindexAnchorCategories($store); -@@ -177,31 +197,31 @@ class Full extends \Magento\Catalog\Model\Indexer\Category\Product\AbstractActio - /** - * Publish data from tmp to replica table - * -- * @param \Magento\Store\Model\Store $store -+ * @param Store $store - * @return void - */ -- private function publishData($store) -+ private function publishData($store): void - { -- $select = $this->connection->select()->from($this->tableMaintainer->getMainTmpTable($store->getId())); -+ $select = $this->connection->select()->from($this->tableMaintainer->getMainTmpTable((int)$store->getId())); - $columns = array_keys( -- $this->connection->describeTable($this->tableMaintainer->getMainReplicaTable($store->getId())) -+ $this->connection->describeTable($this->tableMaintainer->getMainReplicaTable((int)$store->getId())) - ); -- $tableName = $this->tableMaintainer->getMainReplicaTable($store->getId()); -+ $tableName = $this->tableMaintainer->getMainReplicaTable((int)$store->getId()); - - $this->connection->query( - $this->connection->insertFromSelect( - $select, - $tableName, - $columns, -- \Magento\Framework\DB\Adapter\AdapterInterface::INSERT_ON_DUPLICATE -+ AdapterInterface::INSERT_ON_DUPLICATE - ) - ); - } - - /** -- * {@inheritdoc} -+ * @inheritdoc - */ -- protected function reindexRootCategory(\Magento\Store\Model\Store $store) -+ protected function reindexRootCategory(Store $store): void - { - if ($this->isIndexRootCategoryNeeded()) { - $this->reindexCategoriesBySelect($this->getAllProducts($store), 'cp.entity_id IN (?)', $store); -@@ -211,10 +231,10 @@ class Full extends \Magento\Catalog\Model\Indexer\Category\Product\AbstractActio - /** - * Reindex products of anchor categories - * -- * @param \Magento\Store\Model\Store $store -+ * @param Store $store - * @return void - */ -- protected function reindexAnchorCategories(\Magento\Store\Model\Store $store) -+ protected function reindexAnchorCategories(Store $store): void - { - $this->reindexCategoriesBySelect($this->getAnchorCategoriesSelect($store), 'ccp.product_id IN (?)', $store); - } -@@ -222,10 +242,10 @@ class Full extends \Magento\Catalog\Model\Indexer\Category\Product\AbstractActio - /** - * Reindex products of non anchor categories - * -- * @param \Magento\Store\Model\Store $store -+ * @param Store $store - * @return void - */ -- protected function reindexNonAnchorCategories(\Magento\Store\Model\Store $store) -+ protected function reindexNonAnchorCategories(Store $store): void - { - $this->reindexCategoriesBySelect($this->getNonAnchorCategoriesSelect($store), 'ccp.product_id IN (?)', $store); - } -@@ -233,40 +253,42 @@ class Full extends \Magento\Catalog\Model\Indexer\Category\Product\AbstractActio - /** - * Reindex categories using given SQL select and condition. - * -- * @param \Magento\Framework\DB\Select $basicSelect -+ * @param Select $basicSelect - * @param string $whereCondition -- * @param \Magento\Store\Model\Store $store -+ * @param Store $store - * @return void - */ -- private function reindexCategoriesBySelect(\Magento\Framework\DB\Select $basicSelect, $whereCondition, $store) -+ private function reindexCategoriesBySelect(Select $basicSelect, $whereCondition, $store): void - { -- $this->tableMaintainer->createMainTmpTable($store->getId()); -+ $this->tableMaintainer->createMainTmpTable((int)$store->getId()); - -- $entityMetadata = $this->metadataPool->getMetadata(\Magento\Catalog\Api\Data\ProductInterface::class); -+ $entityMetadata = $this->metadataPool->getMetadata(ProductInterface::class); - $columns = array_keys( -- $this->connection->describeTable($this->tableMaintainer->getMainTmpTable($store->getId())) -+ $this->connection->describeTable($this->tableMaintainer->getMainTmpTable((int)$store->getId())) - ); - $this->batchSizeManagement->ensureBatchSize($this->connection, $this->batchRowsCount); -- $batches = $this->batchProvider->getBatches( -- $this->connection, -- $entityMetadata->getEntityTable(), -+ -+ $select = $this->connection->select(); -+ $select->distinct(true); -+ $select->from(['e' => $entityMetadata->getEntityTable()], $entityMetadata->getIdentifierField()); -+ -+ $batchQueries = $this->prepareSelectsByRange( -+ $select, - $entityMetadata->getIdentifierField(), -- $this->batchRowsCount -+ (int)$this->batchRowsCount - ); -- foreach ($batches as $batch) { -- $this->connection->delete($this->tableMaintainer->getMainTmpTable($store->getId())); -+ -+ foreach ($batchQueries as $query) { -+ $this->connection->delete($this->tableMaintainer->getMainTmpTable((int)$store->getId())); -+ $entityIds = $this->connection->fetchCol($query); - $resultSelect = clone $basicSelect; -- $select = $this->connection->select(); -- $select->distinct(true); -- $select->from(['e' => $entityMetadata->getEntityTable()], $entityMetadata->getIdentifierField()); -- $entityIds = $this->batchProvider->getBatchIds($this->connection, $select, $batch); - $resultSelect->where($whereCondition, $entityIds); - $this->connection->query( - $this->connection->insertFromSelect( - $resultSelect, -- $this->tableMaintainer->getMainTmpTable($store->getId()), -+ $this->tableMaintainer->getMainTmpTable((int)$store->getId()), - $columns, -- \Magento\Framework\DB\Adapter\AdapterInterface::INSERT_ON_DUPLICATE -+ AdapterInterface::INSERT_ON_DUPLICATE - ) - ); - $this->publishData($store); -diff --git a/app/code/Magento/Catalog/Model/Indexer/Product/Category/Action/Rows.php b/app/code/Magento/Catalog/Model/Indexer/Product/Category/Action/Rows.php -index 182f04de4ab..cb708695255 100644 ---- a/app/code/Magento/Catalog/Model/Indexer/Product/Category/Action/Rows.php -+++ b/app/code/Magento/Catalog/Model/Indexer/Product/Category/Action/Rows.php -@@ -17,6 +17,8 @@ use Magento\Framework\Indexer\CacheContext; - use Magento\Store\Model\StoreManagerInterface; - - /** -+ * Category rows indexer. -+ * - * @SuppressWarnings(PHPMD.CouplingBetweenObjects) - */ - class Rows extends \Magento\Catalog\Model\Indexer\Category\Product\AbstractAction -@@ -161,7 +163,7 @@ class Rows extends \Magento\Catalog\Model\Indexer\Category\Product\AbstractActio - $this->getIndexTable($store->getId()), - ['product_id IN (?)' => $this->limitationByProducts] - ); -- }; -+ } - } - - /** -@@ -213,6 +215,7 @@ class Rows extends \Magento\Catalog\Model\Indexer\Category\Product\AbstractActio - /** - * Returns a list of category ids which are assigned to product ids in the index - * -+ * @param array $productIds - * @return \Magento\Framework\Indexer\CacheContext - */ - private function getCategoryIdsFromIndex(array $productIds) -@@ -228,7 +231,7 @@ class Rows extends \Magento\Catalog\Model\Indexer\Category\Product\AbstractActio - ->distinct() - ) - ); -- }; -+ } - $parentCategories = $categoryIds; - foreach ($categoryIds as $categoryId) { - $parentIds = explode('/', $this->getPathFromCategoryId($categoryId)); -diff --git a/app/code/Magento/Catalog/Model/Indexer/Product/Eav/Action/Full.php b/app/code/Magento/Catalog/Model/Indexer/Product/Eav/Action/Full.php -index 802176092d1..ed8f692885d 100644 ---- a/app/code/Magento/Catalog/Model/Indexer/Product/Eav/Action/Full.php -+++ b/app/code/Magento/Catalog/Model/Indexer/Product/Eav/Action/Full.php -@@ -7,26 +7,41 @@ declare(strict_types=1); - - namespace Magento\Catalog\Model\Indexer\Product\Eav\Action; - -+use Magento\Catalog\Api\Data\ProductInterface; -+use Magento\Catalog\Model\Indexer\Product\Eav\AbstractAction; - use Magento\Catalog\Model\ResourceModel\Indexer\ActiveTableSwitcher; -+use Magento\Catalog\Model\ResourceModel\Product\Indexer\Eav\BatchSizeCalculator; -+use Magento\Catalog\Model\ResourceModel\Product\Indexer\Eav\DecimalFactory; -+use Magento\Catalog\Model\ResourceModel\Product\Indexer\Eav\SourceFactory; -+use Magento\Framework\App\Config\ScopeConfigInterface; -+use Magento\Framework\App\ObjectManager; -+use Magento\Framework\DB\Adapter\AdapterInterface; -+use Magento\Framework\DB\Query\BatchIteratorInterface; -+use Magento\Framework\DB\Query\Generator as QueryGenerator; -+use Magento\Framework\EntityManager\MetadataPool; -+use Magento\Framework\Exception\LocalizedException; -+use Magento\Framework\Indexer\BatchProviderInterface; -+use Magento\Store\Model\ScopeInterface; - - /** - * Class Full reindex action -+ * - * @SuppressWarnings(PHPMD.CouplingBetweenObjects) - */ --class Full extends \Magento\Catalog\Model\Indexer\Product\Eav\AbstractAction -+class Full extends AbstractAction - { - /** -- * @var \Magento\Framework\EntityManager\MetadataPool -+ * @var MetadataPool - */ - private $metadataPool; - - /** -- * @var \Magento\Framework\Indexer\BatchProviderInterface -+ * @var BatchProviderInterface - */ - private $batchProvider; - - /** -- * @var \Magento\Catalog\Model\ResourceModel\Product\Indexer\Eav\BatchSizeCalculator -+ * @var BatchSizeCalculator - */ - private $batchSizeCalculator; - -@@ -36,44 +51,54 @@ class Full extends \Magento\Catalog\Model\Indexer\Product\Eav\AbstractAction - private $activeTableSwitcher; - - /** -- * @var \Magento\Framework\App\Config\ScopeConfigInterface -+ * @var ScopeConfigInterface - */ - private $scopeConfig; - - /** -- * @param \Magento\Catalog\Model\ResourceModel\Product\Indexer\Eav\DecimalFactory $eavDecimalFactory -- * @param \Magento\Catalog\Model\ResourceModel\Product\Indexer\Eav\SourceFactory $eavSourceFactory -- * @param \Magento\Framework\EntityManager\MetadataPool|null $metadataPool -- * @param \Magento\Framework\Indexer\BatchProviderInterface|null $batchProvider -- * @param \Magento\Catalog\Model\ResourceModel\Product\Indexer\Eav\BatchSizeCalculator $batchSizeCalculator -+ * @var QueryGenerator|null -+ */ -+ private $batchQueryGenerator; -+ -+ /** -+ * @param DecimalFactory $eavDecimalFactory -+ * @param SourceFactory $eavSourceFactory -+ * @param MetadataPool|null $metadataPool -+ * @param BatchProviderInterface|null $batchProvider -+ * @param BatchSizeCalculator $batchSizeCalculator - * @param ActiveTableSwitcher|null $activeTableSwitcher -- * @param \Magento\Framework\App\Config\ScopeConfigInterface|null $scopeConfig -+ * @param ScopeConfigInterface|null $scopeConfig -+ * @param QueryGenerator|null $batchQueryGenerator - */ - public function __construct( -- \Magento\Catalog\Model\ResourceModel\Product\Indexer\Eav\DecimalFactory $eavDecimalFactory, -- \Magento\Catalog\Model\ResourceModel\Product\Indexer\Eav\SourceFactory $eavSourceFactory, -- \Magento\Framework\EntityManager\MetadataPool $metadataPool = null, -- \Magento\Framework\Indexer\BatchProviderInterface $batchProvider = null, -- \Magento\Catalog\Model\ResourceModel\Product\Indexer\Eav\BatchSizeCalculator $batchSizeCalculator = null, -+ DecimalFactory $eavDecimalFactory, -+ SourceFactory $eavSourceFactory, -+ MetadataPool $metadataPool = null, -+ BatchProviderInterface $batchProvider = null, -+ BatchSizeCalculator $batchSizeCalculator = null, - ActiveTableSwitcher $activeTableSwitcher = null, -- \Magento\Framework\App\Config\ScopeConfigInterface $scopeConfig = null -+ ScopeConfigInterface $scopeConfig = null, -+ QueryGenerator $batchQueryGenerator = null - ) { -- $this->scopeConfig = $scopeConfig ?: \Magento\Framework\App\ObjectManager::getInstance()->get( -- \Magento\Framework\App\Config\ScopeConfigInterface::class -+ $this->scopeConfig = $scopeConfig ?: ObjectManager::getInstance()->get( -+ ScopeConfigInterface::class - ); - parent::__construct($eavDecimalFactory, $eavSourceFactory, $scopeConfig); -- $this->metadataPool = $metadataPool ?: \Magento\Framework\App\ObjectManager::getInstance()->get( -- \Magento\Framework\EntityManager\MetadataPool::class -+ $this->metadataPool = $metadataPool ?: ObjectManager::getInstance()->get( -+ MetadataPool::class - ); -- $this->batchProvider = $batchProvider ?: \Magento\Framework\App\ObjectManager::getInstance()->get( -- \Magento\Framework\Indexer\BatchProviderInterface::class -+ $this->batchProvider = $batchProvider ?: ObjectManager::getInstance()->get( -+ BatchProviderInterface::class - ); -- $this->batchSizeCalculator = $batchSizeCalculator ?: \Magento\Framework\App\ObjectManager::getInstance()->get( -- \Magento\Catalog\Model\ResourceModel\Product\Indexer\Eav\BatchSizeCalculator::class -+ $this->batchSizeCalculator = $batchSizeCalculator ?: ObjectManager::getInstance()->get( -+ BatchSizeCalculator::class - ); -- $this->activeTableSwitcher = $activeTableSwitcher ?: \Magento\Framework\App\ObjectManager::getInstance()->get( -+ $this->activeTableSwitcher = $activeTableSwitcher ?: ObjectManager::getInstance()->get( - ActiveTableSwitcher::class - ); -+ $this->batchQueryGenerator = $batchQueryGenerator ?: ObjectManager::getInstance()->get( -+ QueryGenerator::class -+ ); - } - - /** -@@ -81,10 +106,10 @@ class Full extends \Magento\Catalog\Model\Indexer\Product\Eav\AbstractAction - * - * @param array|int|null $ids - * @return void -- * @throws \Magento\Framework\Exception\LocalizedException -+ * @throws LocalizedException - * @SuppressWarnings(PHPMD.UnusedFormalParameter) - */ -- public function execute($ids = null) -+ public function execute($ids = null): void - { - if (!$this->isEavIndexerEnabled()) { - return; -@@ -94,20 +119,21 @@ class Full extends \Magento\Catalog\Model\Indexer\Product\Eav\AbstractAction - $connection = $indexer->getConnection(); - $mainTable = $this->activeTableSwitcher->getAdditionalTableName($indexer->getMainTable()); - $connection->truncateTable($mainTable); -- $entityMetadata = $this->metadataPool->getMetadata(\Magento\Catalog\Api\Data\ProductInterface::class); -- $batches = $this->batchProvider->getBatches( -- $connection, -- $entityMetadata->getEntityTable(), -+ $entityMetadata = $this->metadataPool->getMetadata(ProductInterface::class); -+ -+ $select = $connection->select(); -+ $select->distinct(true); -+ $select->from(['e' => $entityMetadata->getEntityTable()], $entityMetadata->getIdentifierField()); -+ -+ $batchQueries = $this->batchQueryGenerator->generate( - $entityMetadata->getIdentifierField(), -- $this->batchSizeCalculator->estimateBatchSize($connection, $indexerName) -+ $select, -+ $this->batchSizeCalculator->estimateBatchSize($connection, $indexerName), -+ BatchIteratorInterface::NON_UNIQUE_FIELD_ITERATOR - ); - -- foreach ($batches as $batch) { -- /** @var \Magento\Framework\DB\Select $select */ -- $select = $connection->select(); -- $select->distinct(true); -- $select->from(['e' => $entityMetadata->getEntityTable()], $entityMetadata->getIdentifierField()); -- $entityIds = $this->batchProvider->getBatchIds($connection, $select, $batch); -+ foreach ($batchQueries as $query) { -+ $entityIds = $connection->fetchCol($query); - if (!empty($entityIds)) { - $indexer->reindexEntities($this->processRelations($indexer, $entityIds, true)); - $this->syncData($indexer, $mainTable); -@@ -116,14 +142,14 @@ class Full extends \Magento\Catalog\Model\Indexer\Product\Eav\AbstractAction - $this->activeTableSwitcher->switchTable($indexer->getConnection(), [$indexer->getMainTable()]); - } - } catch (\Exception $e) { -- throw new \Magento\Framework\Exception\LocalizedException(__($e->getMessage()), $e); -+ throw new LocalizedException(__($e->getMessage()), $e); - } - } - - /** - * @inheritdoc - */ -- protected function syncData($indexer, $destinationTable, $ids = null) -+ protected function syncData($indexer, $destinationTable, $ids = null): void - { - $connection = $indexer->getConnection(); - $connection->beginTransaction(); -@@ -136,7 +162,7 @@ class Full extends \Magento\Catalog\Model\Indexer\Product\Eav\AbstractAction - $select, - $destinationTable, - $targetColumns, -- \Magento\Framework\DB\Adapter\AdapterInterface::INSERT_ON_DUPLICATE -+ AdapterInterface::INSERT_ON_DUPLICATE - ); - $connection->query($query); - $connection->commit(); -@@ -155,7 +181,7 @@ class Full extends \Magento\Catalog\Model\Indexer\Product\Eav\AbstractAction - { - $eavIndexerStatus = $this->scopeConfig->getValue( - self::ENABLE_EAV_INDEXER, -- \Magento\Store\Model\ScopeInterface::SCOPE_STORE -+ ScopeInterface::SCOPE_STORE - ); - - return (bool)$eavIndexerStatus; -diff --git a/app/code/Magento/Catalog/Model/Indexer/Product/Flat/AbstractAction.php b/app/code/Magento/Catalog/Model/Indexer/Product/Flat/AbstractAction.php -index 3b63a90d4c3..ebad10e1976 100644 ---- a/app/code/Magento/Catalog/Model/Indexer/Product/Flat/AbstractAction.php -+++ b/app/code/Magento/Catalog/Model/Indexer/Product/Flat/AbstractAction.php -@@ -169,8 +169,7 @@ abstract class AbstractAction - } - - /** -- * Retrieve Product Type Instances -- * as key - type code, value - instance model -+ * Retrieve Product Type Instances as key - type code, value - instance model - * - * @return array - */ -@@ -213,17 +212,19 @@ abstract class AbstractAction - ) { - $columns = $this->_productIndexerHelper->getFlatColumns(); - $fieldList = array_keys($columns); -- unset($columns['entity_id']); -- unset($columns['child_id']); -- unset($columns['is_child']); -+ unset( -+ $columns['entity_id'], -+ $columns['child_id'], -+ $columns['is_child'] -+ ); - /** @var $select \Magento\Framework\DB\Select */ - $select = $this->_connection->select()->from( - ['t' => $this->_productIndexerHelper->getTable($relation->getTable())], -- [$relation->getChildFieldName(), new \Zend_Db_Expr('1')] -+ ['entity_table.entity_id', $relation->getChildFieldName(), new \Zend_Db_Expr('1')] - )->join( - ['entity_table' => $this->_connection->getTableName('catalog_product_entity')], -- 'entity_table.' . $metadata->getLinkField() . 't.' . $relation->getParentFieldName(), -- [$relation->getParentFieldName() => 'entity_table.entity_id'] -+ "entity_table.{$metadata->getLinkField()} = t.{$relation->getParentFieldName()}", -+ [] - )->join( - ['e' => $this->_productIndexerHelper->getFlatTableName($storeId)], - "e.entity_id = t.{$relation->getChildFieldName()}", -@@ -232,10 +233,10 @@ abstract class AbstractAction - if ($relation->getWhere() !== null) { - $select->where($relation->getWhere()); - } -- if ($productIds !== null) { -+ if (!empty($productIds)) { - $cond = [ - $this->_connection->quoteInto("{$relation->getChildFieldName()} IN(?)", $productIds), -- $this->_connection->quoteInto("entity_table.entity_id IN(?)", $productIds), -+ $this->_connection->quoteInto('entity_table.entity_id IN(?)', $productIds), - ]; - - $select->where(implode(' OR ', $cond)); -@@ -273,15 +274,11 @@ abstract class AbstractAction - $select = $this->_connection->select()->distinct( - true - )->from( -- ['t' => $this->_productIndexerHelper->getTable($relation->getTable())], -- [] -- )->join( -- ['entity_table' => $this->_connection->getTableName('catalog_product_entity')], -- 'entity_table.' . $metadata->getLinkField() . 't.' . $relation->getParentFieldName(), -- [$relation->getParentFieldName() => 'entity_table.entity_id'] -+ $this->_productIndexerHelper->getTable($relation->getTable()), -+ $relation->getParentFieldName() - ); - $joinLeftCond = [ -- "e.entity_id = entity_table.entity_id", -+ "e.{$metadata->getLinkField()} = t.{$relation->getParentFieldName()}", - "e.child_id = t.{$relation->getChildFieldName()}", - ]; - if ($relation->getWhere() !== null) { -@@ -302,7 +299,7 @@ abstract class AbstractAction - 'e.is_child = ?', - 1 - )->where( -- 'e.entity_id IN(?)', -+ "e.{$metadata->getLinkField()} IN(?)", - $entitySelect - )->where( - "t.{$relation->getChildFieldName()} IS NULL" -@@ -335,6 +332,8 @@ abstract class AbstractAction - } - - /** -+ * Get Metadata Pool -+ * - * @return \Magento\Framework\EntityManager\MetadataPool - */ - private function getMetadataPool() -diff --git a/app/code/Magento/Catalog/Model/Indexer/Product/Flat/Action/Eraser.php b/app/code/Magento/Catalog/Model/Indexer/Product/Flat/Action/Eraser.php -index 8182e6f07fa..c4d807667bf 100644 ---- a/app/code/Magento/Catalog/Model/Indexer/Product/Flat/Action/Eraser.php -+++ b/app/code/Magento/Catalog/Model/Indexer/Product/Flat/Action/Eraser.php -@@ -8,7 +8,13 @@ - namespace Magento\Catalog\Model\Indexer\Product\Flat\Action; - - use Magento\Framework\App\ResourceConnection; -+use Magento\Catalog\Model\Product\Attribute\Source\Status; -+use Magento\Framework\EntityManager\MetadataPool; -+use Magento\Store\Model\Store; - -+/** -+ * Flat item eraser. Used to clear items from the catalog flat table. -+ */ - class Eraser - { - /** -@@ -26,19 +32,28 @@ class Eraser - */ - protected $storeManager; - -+ /** -+ * @var MetadataPool -+ */ -+ private $metadataPool; -+ - /** - * @param \Magento\Framework\App\ResourceConnection $resource - * @param \Magento\Catalog\Helper\Product\Flat\Indexer $productHelper - * @param \Magento\Store\Model\StoreManagerInterface $storeManager -+ * @param MetadataPool|null $metadataPool - */ - public function __construct( - \Magento\Framework\App\ResourceConnection $resource, - \Magento\Catalog\Helper\Product\Flat\Indexer $productHelper, -- \Magento\Store\Model\StoreManagerInterface $storeManager -+ \Magento\Store\Model\StoreManagerInterface $storeManager, -+ MetadataPool $metadataPool = null - ) { - $this->productIndexerHelper = $productHelper; - $this->connection = $resource->getConnection(); - $this->storeManager = $storeManager; -+ $this->metadataPool = $metadataPool ?: -+ \Magento\Framework\App\ObjectManager::getInstance()->get(MetadataPool::class); - } - - /** -@@ -50,12 +65,7 @@ class Eraser - */ - public function removeDeletedProducts(array &$ids, $storeId) - { -- $select = $this->connection->select()->from( -- $this->productIndexerHelper->getTable('catalog_product_entity') -- )->where( -- 'entity_id IN(?)', -- $ids -- ); -+ $select = $this->getSelectForProducts($ids); - $result = $this->connection->query($select); - - $existentProducts = []; -@@ -69,6 +79,69 @@ class Eraser - $this->deleteProductsFromStore($productsToDelete, $storeId); - } - -+ /** -+ * Remove products with "Disabled" status from the flat table(s). -+ * -+ * @param array $ids -+ * @param int $storeId -+ * @return void -+ */ -+ public function removeDisabledProducts(array &$ids, $storeId) -+ { -+ /* @var $statusAttribute \Magento\Eav\Model\Entity\Attribute */ -+ $statusAttribute = $this->productIndexerHelper->getAttribute('status'); -+ -+ /** @var \Magento\Framework\EntityManager\EntityMetadataInterface $metadata */ -+ $metadata = $this->metadataPool->getMetadata(\Magento\Catalog\Api\Data\ProductInterface::class); -+ -+ $select = $this->getSelectForProducts($ids); -+ $select->joinLeft( -+ ['status_global_attr' => $statusAttribute->getBackendTable()], -+ ' status_global_attr.attribute_id = ' . (int)$statusAttribute->getAttributeId() -+ . ' AND status_global_attr.store_id = ' . Store::DEFAULT_STORE_ID -+ . ' AND status_global_attr.' . $statusAttribute->getEntityIdField() . '=' -+ . 'product_table.' . $metadata->getLinkField(), -+ [] -+ ); -+ $select->joinLeft( -+ ['status_attr' => $statusAttribute->getBackendTable()], -+ ' status_attr.attribute_id = ' . (int)$statusAttribute->getAttributeId() -+ . ' AND status_attr.store_id = ' . $storeId -+ . ' AND status_attr.' . $statusAttribute->getEntityIdField() . '=' -+ . 'product_table.' . $metadata->getLinkField(), -+ [] -+ ); -+ $select->where('IFNULL(status_attr.value, status_global_attr.value) = ?', Status::STATUS_DISABLED); -+ -+ $result = $this->connection->query($select); -+ -+ $disabledProducts = []; -+ foreach ($result->fetchAll() as $product) { -+ $disabledProducts[] = $product['entity_id']; -+ } -+ -+ if (!empty($disabledProducts)) { -+ $ids = array_diff($ids, $disabledProducts); -+ $this->deleteProductsFromStore($disabledProducts, $storeId); -+ } -+ } -+ -+ /** -+ * Get Select object for existed products. -+ * -+ * @param array $ids -+ * @return \Magento\Framework\DB\Select -+ */ -+ private function getSelectForProducts(array $ids) -+ { -+ $productTable = $this->productIndexerHelper->getTable('catalog_product_entity'); -+ $select = $this->connection->select() -+ ->from(['product_table' => $productTable]) -+ ->columns('entity_id') -+ ->where('product_table.entity_id IN(?)', $ids); -+ return $select; -+ } -+ - /** - * Delete products from flat table(s) - * -diff --git a/app/code/Magento/Catalog/Model/Indexer/Product/Flat/Action/Indexer.php b/app/code/Magento/Catalog/Model/Indexer/Product/Flat/Action/Indexer.php -index 3a161129928..c14bc0dd7e5 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 @@ namespace Magento\Catalog\Model\Indexer\Product\Flat\Action; - use Magento\Catalog\Api\Data\ProductInterface; - use Magento\Framework\App\ResourceConnection; - use Magento\Framework\EntityManager\MetadataPool; -+use Magento\Store\Model\Store; - - /** - * Class Indexer -@@ -53,42 +54,39 @@ class Indexer - * @param int $storeId - * @param int $productId - * @param string $valueFieldSuffix -- * @return \Magento\Catalog\Model\Indexer\Product\Flat -+ * @return $this - * @SuppressWarnings(PHPMD.CyclomaticComplexity) - * @SuppressWarnings(PHPMD.ExcessiveMethodLength) -+ * @SuppressWarnings(PHPMD.NPathComplexity) - */ - public function write($storeId, $productId, $valueFieldSuffix = '') - { - $flatTable = $this->_productIndexerHelper->getFlatTableName($storeId); -+ $entityTableName = $this->_productIndexerHelper->getTable('catalog_product_entity'); - - $attributes = $this->_productIndexerHelper->getAttributes(); - $eavAttributes = $this->_productIndexerHelper->getTablesStructure($attributes); - $updateData = []; - $describe = $this->_connection->describeTable($flatTable); -+ $metadata = $this->getMetadataPool()->getMetadata(ProductInterface::class); -+ $linkField = $metadata->getLinkField(); - - foreach ($eavAttributes as $tableName => $tableColumns) { - $columnsChunks = array_chunk($tableColumns, self::ATTRIBUTES_CHUNK_SIZE, true); - - foreach ($columnsChunks as $columns) { - $select = $this->_connection->select(); -- $selectValue = $this->_connection->select(); -- $keyColumns = [ -- 'entity_id' => 'e.entity_id', -- 'attribute_id' => 't.attribute_id', -- 'value' => $this->_connection->getIfNullSql('`t2`.`value`', '`t`.`value`'), -- ]; -- -- if ($tableName != $this->_productIndexerHelper->getTable('catalog_product_entity')) { -+ -+ if ($tableName != $entityTableName) { - $valueColumns = []; - $ids = []; - $select->from( -- ['e' => $this->_productIndexerHelper->getTable('catalog_product_entity')], -- $keyColumns -- ); -- -- $selectValue->from( -- ['e' => $this->_productIndexerHelper->getTable('catalog_product_entity')], -- $keyColumns -+ ['e' => $entityTableName], -+ [ -+ 'entity_id' => 'e.entity_id', -+ 'attribute_id' => 't.attribute_id', -+ 'value' => 't.value' -+ ] - ); - - /** @var $attribute \Magento\Catalog\Model\ResourceModel\Eav\Attribute */ -@@ -97,40 +95,35 @@ class Indexer - $ids[$attribute->getId()] = $columnName; - } - } -- $linkField = $this->getMetadataPool()->getMetadata(ProductInterface::class)->getLinkField(); -- $select->joinLeft( -+ $select->joinInner( - ['t' => $tableName], - sprintf('e.%s = t.%s ', $linkField, $linkField) . $this->_connection->quoteInto( - ' AND t.attribute_id IN (?)', - array_keys($ids) -- ) . ' AND t.store_id = 0', -- [] -- )->joinLeft( -- ['t2' => $tableName], -- sprintf('t.%s = t2.%s ', $linkField, $linkField) . -- ' AND t.attribute_id = t2.attribute_id ' . -- $this->_connection->quoteInto( -- ' AND t2.store_id = ?', -- $storeId -- ), -+ ) . ' AND ' . $this->_connection->quoteInto('t.store_id IN(?)', [ -+ Store::DEFAULT_STORE_ID, -+ $storeId -+ ]), - [] - )->where( - 'e.entity_id = ' . $productId -- )->where( -- 't.attribute_id IS NOT NULL' -- ); -+ )->order('t.store_id ASC'); - $cursor = $this->_connection->query($select); - while ($row = $cursor->fetch(\Zend_Db::FETCH_ASSOC)) { - $updateData[$ids[$row['attribute_id']]] = $row['value']; - $valueColumnName = $ids[$row['attribute_id']] . $valueFieldSuffix; - if (isset($describe[$valueColumnName])) { -- $valueColumns[$row['value']] = $valueColumnName; -+ $valueColumns[$row['attribute_id']] = [ -+ 'value' => $row['value'], -+ 'column_name' => $valueColumnName -+ ]; - } - } - - //Update not simple attributes (eg. dropdown) - if (!empty($valueColumns)) { -- $valueIds = array_keys($valueColumns); -+ $valueIds = array_column($valueColumns, 'value'); -+ $optionIdToAttributeName = array_column($valueColumns, 'column_name', 'value'); - - $select = $this->_connection->select()->from( - ['t' => $this->_productIndexerHelper->getTable('eav_attribute_option_value')], -@@ -139,14 +132,14 @@ class Indexer - $this->_connection->quoteInto('t.option_id IN (?)', $valueIds) - )->where( - $this->_connection->quoteInto('t.store_id IN(?)', [ -- \Magento\Store\Model\Store::DEFAULT_STORE_ID, -+ Store::DEFAULT_STORE_ID, - $storeId - ]) - ) - ->order('t.store_id ASC'); - $cursor = $this->_connection->query($select); - while ($row = $cursor->fetch(\Zend_Db::FETCH_ASSOC)) { -- $valueColumnName = $valueColumns[$row['option_id']]; -+ $valueColumnName = $optionIdToAttributeName[$row['option_id']]; - if (isset($describe[$valueColumnName])) { - $updateData[$valueColumnName] = $row['value']; - } -@@ -156,8 +149,9 @@ class Indexer - $columnNames = array_keys($columns); - $columnNames[] = 'attribute_set_id'; - $columnNames[] = 'type_id'; -+ $columnNames[] = $linkField; - $select->from( -- ['e' => $this->_productIndexerHelper->getTable('catalog_product_entity')], -+ ['e' => $entityTableName], - $columnNames - )->where( - 'e.entity_id = ' . $productId -@@ -165,6 +159,7 @@ class Indexer - $cursor = $this->_connection->query($select); - $row = $cursor->fetch(\Zend_Db::FETCH_ASSOC); - if (!empty($row)) { -+ $linkFieldId = $linkField; - foreach ($row as $columnName => $value) { - $updateData[$columnName] = $value; - } -@@ -175,7 +170,9 @@ class Indexer - - if (!empty($updateData)) { - $updateData += ['entity_id' => $productId]; -- $updateData += ['row_id' => $productId]; -+ if ($linkField !== $metadata->getIdentifierField()) { -+ $updateData += [$linkField => $linkFieldId]; -+ } - $updateFields = []; - foreach ($updateData as $key => $value) { - $updateFields[$key] = $key; -@@ -187,6 +184,8 @@ class Indexer - } - - /** -+ * Get MetadataPool instance -+ * - * @return \Magento\Framework\EntityManager\MetadataPool - */ - private function getMetadataPool() -diff --git a/app/code/Magento/Catalog/Model/Indexer/Product/Flat/Action/Row.php b/app/code/Magento/Catalog/Model/Indexer/Product/Flat/Action/Row.php -index b5dbdb68606..64a7c4be4e0 100644 ---- a/app/code/Magento/Catalog/Model/Indexer/Product/Flat/Action/Row.php -+++ b/app/code/Magento/Catalog/Model/Indexer/Product/Flat/Action/Row.php -@@ -5,16 +5,20 @@ - */ - namespace Magento\Catalog\Model\Indexer\Product\Flat\Action; - -+use Magento\Catalog\Api\Data\ProductInterface; - use Magento\Catalog\Model\Indexer\Product\Flat\FlatTableBuilder; - use Magento\Catalog\Model\Indexer\Product\Flat\TableBuilder; -+use Magento\Framework\EntityManager\MetadataPool; - - /** -- * Class Row reindex action -+ * Class Row reindex action. -+ * -+ * @SuppressWarnings(PHPMD.CouplingBetweenObjects) - */ - class Row extends \Magento\Catalog\Model\Indexer\Product\Flat\AbstractAction - { - /** -- * @var \Magento\Catalog\Model\Indexer\Product\Flat\Action\Indexer -+ * @var Indexer - */ - protected $flatItemWriter; - -@@ -23,6 +27,11 @@ class Row extends \Magento\Catalog\Model\Indexer\Product\Flat\AbstractAction - */ - protected $flatItemEraser; - -+ /** -+ * @var MetadataPool -+ */ -+ private $metadataPool; -+ - /** - * @param \Magento\Framework\App\ResourceConnection $resource - * @param \Magento\Store\Model\StoreManagerInterface $storeManager -@@ -32,6 +41,7 @@ class Row extends \Magento\Catalog\Model\Indexer\Product\Flat\AbstractAction - * @param FlatTableBuilder $flatTableBuilder - * @param Indexer $flatItemWriter - * @param Eraser $flatItemEraser -+ * @param MetadataPool|null $metadataPool - */ - public function __construct( - \Magento\Framework\App\ResourceConnection $resource, -@@ -41,7 +51,8 @@ class Row extends \Magento\Catalog\Model\Indexer\Product\Flat\AbstractAction - TableBuilder $tableBuilder, - FlatTableBuilder $flatTableBuilder, - Indexer $flatItemWriter, -- Eraser $flatItemEraser -+ Eraser $flatItemEraser, -+ MetadataPool $metadataPool = null - ) { - parent::__construct( - $resource, -@@ -53,6 +64,8 @@ class Row extends \Magento\Catalog\Model\Indexer\Product\Flat\AbstractAction - ); - $this->flatItemWriter = $flatItemWriter; - $this->flatItemEraser = $flatItemEraser; -+ $this->metadataPool = $metadataPool ?: -+ \Magento\Framework\App\ObjectManager::getInstance()->get(MetadataPool::class); - } - - /** -@@ -70,24 +83,50 @@ class Row extends \Magento\Catalog\Model\Indexer\Product\Flat\AbstractAction - ); - } - $ids = [$id]; -- foreach ($this->_storeManager->getStores() as $store) { -+ $linkField = $this->metadataPool->getMetadata(ProductInterface::class)->getLinkField(); -+ -+ $stores = $this->_storeManager->getStores(); -+ foreach ($stores as $store) { - $tableExists = $this->_isFlatTableExists($store->getId()); - if ($tableExists) { - $this->flatItemEraser->removeDeletedProducts($ids, $store->getId()); -+ $this->flatItemEraser->removeDisabledProducts($ids, $store->getId()); - } -- if (isset($ids[0])) { -+ -+ /* @var $status \Magento\Eav\Model\Entity\Attribute */ -+ $status = $this->_productIndexerHelper->getAttribute(ProductInterface::STATUS); -+ $statusTable = $status->getBackend()->getTable(); -+ $catalogProductEntityTable = $this->_productIndexerHelper->getTable('catalog_product_entity'); -+ $statusConditions = [ -+ 's.store_id IN(0,' . (int)$store->getId() . ')', -+ 's.attribute_id = ' . (int)$status->getId(), -+ 'e.entity_id = ' . (int)$id, -+ ]; -+ $select = $this->_connection->select(); -+ $select->from(['e' => $catalogProductEntityTable], ['s.value']) -+ ->where(implode(' AND ', $statusConditions)) -+ ->joinLeft(['s' => $statusTable], "e.{$linkField} = s.{$linkField}", []) -+ ->order('s.store_id DESC') -+ ->limit(1); -+ $result = $this->_connection->query($select); -+ $status = $result->fetchColumn(0); -+ -+ if ($status == \Magento\Catalog\Model\Product\Attribute\Source\Status::STATUS_ENABLED) { - if (!$tableExists) { - $this->_flatTableBuilder->build( - $store->getId(), -- [$ids[0]], -+ $ids, - $this->_valueFieldSuffix, - $this->_tableDropSuffix, - false - ); - } -- $this->flatItemWriter->write($store->getId(), $ids[0], $this->_valueFieldSuffix); -+ $this->flatItemWriter->write($store->getId(), $id, $this->_valueFieldSuffix); -+ } else { -+ $this->flatItemEraser->deleteProductsFromStore($id, $store->getId()); - } - } -+ - return $this; - } - } -diff --git a/app/code/Magento/Catalog/Model/Indexer/Product/Flat/FlatTableBuilder.php b/app/code/Magento/Catalog/Model/Indexer/Product/Flat/FlatTableBuilder.php -index fbe0d4b550f..2252b3e3d55 100644 ---- a/app/code/Magento/Catalog/Model/Indexer/Product/Flat/FlatTableBuilder.php -+++ b/app/code/Magento/Catalog/Model/Indexer/Product/Flat/FlatTableBuilder.php -@@ -10,7 +10,8 @@ use Magento\Framework\App\ResourceConnection; - use Magento\Framework\EntityManager\MetadataPool; - - /** -- * Class FlatTableBuilder -+ * Class for building flat index -+ * - * @SuppressWarnings(PHPMD.CouplingBetweenObjects) - */ - class FlatTableBuilder -@@ -346,12 +347,21 @@ class FlatTableBuilder - } - - //Update not simple attributes (eg. dropdown) -- if (isset($flatColumns[$attributeCode . $valueFieldSuffix])) { -- $select = $this->_connection->select()->joinInner( -- ['t' => $this->_productIndexerHelper->getTable('eav_attribute_option_value')], -- 't.option_id = et.' . $attributeCode . ' AND t.store_id=' . $storeId, -- [$attributeCode . $valueFieldSuffix => 't.value'] -- ); -+ $columnName = $attributeCode . $valueFieldSuffix; -+ if (isset($flatColumns[$columnName])) { -+ $columnValue = $this->_connection->getIfNullSql('ts.value', 't0.value'); -+ $select = $this->_connection->select(); -+ $select->joinLeft( -+ ['t0' => $this->_productIndexerHelper->getTable('eav_attribute_option_value')], -+ 't0.option_id = et.' . $attributeCode . ' AND t0.store_id = 0', -+ [] -+ )->joinLeft( -+ ['ts' => $this->_productIndexerHelper->getTable('eav_attribute_option_value')], -+ 'ts.option_id = et.' . $attributeCode . ' AND ts.store_id = ' . $storeId, -+ [] -+ )->columns( -+ [$columnName => $columnValue] -+ )->where($columnValue . ' IS NOT NULL'); - if (!empty($changedIds)) { - $select->where($this->_connection->quoteInto('et.entity_id IN (?)', $changedIds)); - } -@@ -374,6 +384,8 @@ class FlatTableBuilder - } - - /** -+ * Get metadata pool -+ * - * @return \Magento\Framework\EntityManager\MetadataPool - */ - private function getMetadataPool() -diff --git a/app/code/Magento/Catalog/Model/Indexer/Product/Flat/TableBuilder.php b/app/code/Magento/Catalog/Model/Indexer/Product/Flat/TableBuilder.php -index a32379b8c0a..e6c098ab025 100644 ---- a/app/code/Magento/Catalog/Model/Indexer/Product/Flat/TableBuilder.php -+++ b/app/code/Magento/Catalog/Model/Indexer/Product/Flat/TableBuilder.php -@@ -7,6 +7,9 @@ namespace Magento\Catalog\Model\Indexer\Product\Flat; - - use Magento\Catalog\Model\Indexer\Product\Flat\Table\BuilderInterfaceFactory; - -+/** -+ * Class TableBuilder -+ */ - class TableBuilder - { - /** -@@ -112,7 +115,7 @@ class TableBuilder - /** - * Create empty temporary table with given columns list - * -- * @param string $tableName Table name -+ * @param string $tableName Table name - * @param array $columns array('columnName' => \Magento\Catalog\Model\ResourceModel\Eav\Attribute, ...) - * @param string $valueFieldSuffix - * -@@ -137,13 +140,23 @@ class TableBuilder - ); - $flatColumns = $this->_productIndexerHelper->getFlatColumns(); - -- $temporaryTableBuilder->addColumn('entity_id', \Magento\Framework\DB\Ddl\Table::TYPE_INTEGER); -+ $temporaryTableBuilder->addColumn( -+ 'entity_id', -+ \Magento\Framework\DB\Ddl\Table::TYPE_INTEGER, -+ null, -+ ['unsigned'=>true] -+ ); - - $temporaryTableBuilder->addColumn('type_id', \Magento\Framework\DB\Ddl\Table::TYPE_TEXT); - - $temporaryTableBuilder->addColumn('attribute_set_id', \Magento\Framework\DB\Ddl\Table::TYPE_INTEGER); - -- $valueTemporaryTableBuilder->addColumn('entity_id', \Magento\Framework\DB\Ddl\Table::TYPE_INTEGER); -+ $valueTemporaryTableBuilder->addColumn( -+ 'entity_id', -+ \Magento\Framework\DB\Ddl\Table::TYPE_INTEGER, -+ null, -+ ['unsigned'=>true] -+ ); - - /** @var $attribute \Magento\Catalog\Model\ResourceModel\Eav\Attribute */ - foreach ($columns as $columnName => $attribute) { -@@ -198,9 +211,10 @@ class TableBuilder - * Fill temporary entity table - * - * @param string $tableName -- * @param array $columns -- * @param array $changedIds -+ * @param array $columns -+ * @param array $changedIds - * @return void -+ * @throws \Exception - */ - protected function _fillTemporaryEntityTable($tableName, array $columns, array $changedIds = []) - { -@@ -244,11 +258,12 @@ class TableBuilder - * Fill temporary table by data from products EAV attributes by type - * - * @param string $tableName -- * @param array $tableColumns -- * @param array $changedIds -+ * @param array $tableColumns -+ * @param array $changedIds - * @param string $valueFieldSuffix - * @param int $storeId - * @return void -+ * @throws \Exception - */ - protected function _fillTemporaryTable( - $tableName, -@@ -289,12 +304,16 @@ class TableBuilder - - /** @var $attribute \Magento\Catalog\Model\ResourceModel\Eav\Attribute */ - foreach ($columnsList as $columnName => $attribute) { -- $countTableName = 't' . $iterationNum++; -+ $countTableName = 't' . ($iterationNum++); - $joinCondition = sprintf( -- 'e.%3$s = %1$s.%3$s AND %1$s.attribute_id = %2$d AND %1$s.store_id = 0', -+ 'e.%3$s = %1$s.%3$s' . -+ ' AND %1$s.attribute_id = %2$d' . -+ ' AND (%1$s.store_id = %4$d' . -+ ' OR %1$s.store_id = 0)', - $countTableName, - $attribute->getId(), -- $metadata->getLinkField() -+ $metadata->getLinkField(), -+ $storeId - ); - - $select->joinLeft( -@@ -308,9 +327,10 @@ class TableBuilder - $columnValueName = $attributeCode . $valueFieldSuffix; - if (isset($flatColumns[$columnValueName])) { - $valueJoinCondition = sprintf( -- 'e.%1$s = %2$s.option_id AND %2$s.store_id = 0', -+ 'e.%1$s = %2$s.option_id AND (%2$s.store_id = %3$d OR %2$s.store_id = 0)', - $attributeCode, -- $countTableName -+ $countTableName, -+ $storeId - ); - $selectValue->joinLeft( - [ -@@ -345,6 +365,8 @@ class TableBuilder - } - - /** -+ * Get Metadata Pool -+ * - * @return \Magento\Framework\EntityManager\MetadataPool - * @deprecated 101.1.0 - */ -diff --git a/app/code/Magento/Catalog/Model/Indexer/Product/Price/Action/Full.php b/app/code/Magento/Catalog/Model/Indexer/Product/Price/Action/Full.php -index 1a757515706..858eba3ab21 100644 ---- a/app/code/Magento/Catalog/Model/Indexer/Product/Price/Action/Full.php -+++ b/app/code/Magento/Catalog/Model/Indexer/Product/Price/Action/Full.php -@@ -3,41 +3,64 @@ - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -+ -+declare(strict_types=1); -+ - namespace Magento\Catalog\Model\Indexer\Product\Price\Action; - -+use Magento\Catalog\Model\Indexer\Product\Price\AbstractAction; -+use Magento\Catalog\Model\Indexer\Product\Price\DimensionCollectionFactory; -+use Magento\Catalog\Model\Indexer\Product\Price\TableMaintainer; -+use Magento\Catalog\Model\Product\Type; -+use Magento\Catalog\Model\ResourceModel\Indexer\ActiveTableSwitcher; -+use Magento\Catalog\Model\ResourceModel\Product\Indexer\Price\BatchSizeCalculator; -+use Magento\Catalog\Model\ResourceModel\Product\Indexer\Price\DefaultPrice; -+use Magento\Catalog\Model\ResourceModel\Product\Indexer\Price\Factory; -+use Magento\Directory\Model\CurrencyFactory; -+use Magento\Framework\App\Config\ScopeConfigInterface; - use Magento\Framework\App\ObjectManager; - use Magento\Catalog\Model\ResourceModel\Product\Indexer\Price\PriceInterface; -+use Magento\Framework\DB\Adapter\AdapterInterface; -+use Magento\Framework\DB\Query\BatchIterator; -+use Magento\Framework\DB\Query\Generator as QueryGenerator; -+use Magento\Framework\DB\Select; - use Magento\Framework\EntityManager\EntityMetadataInterface; - use Magento\Catalog\Api\Data\ProductInterface; -+use Magento\Framework\EntityManager\MetadataPool; - use Magento\Framework\Exception\LocalizedException; -+use Magento\Framework\Indexer\BatchProviderInterface; - use Magento\Framework\Indexer\DimensionalIndexerInterface; - use Magento\Customer\Model\Indexer\CustomerGroupDimensionProvider; -+use Magento\Framework\Stdlib\DateTime; -+use Magento\Framework\Stdlib\DateTime\TimezoneInterface; -+use Magento\Indexer\Model\ProcessManager; - use Magento\Store\Model\Indexer\WebsiteDimensionProvider; -+use Magento\Store\Model\StoreManagerInterface; - - /** - * Class Full reindex action - * - * @SuppressWarnings(PHPMD.CouplingBetweenObjects) - */ --class Full extends \Magento\Catalog\Model\Indexer\Product\Price\AbstractAction -+class Full extends AbstractAction - { - /** -- * @var \Magento\Framework\EntityManager\MetadataPool -+ * @var MetadataPool - */ - private $metadataPool; - - /** -- * @var \Magento\Catalog\Model\ResourceModel\Product\Indexer\Price\BatchSizeCalculator -+ * @var BatchSizeCalculator - */ - private $batchSizeCalculator; - - /** -- * @var \Magento\Framework\Indexer\BatchProviderInterface -+ * @var BatchProviderInterface - */ - private $batchProvider; - - /** -- * @var \Magento\Catalog\Model\ResourceModel\Indexer\ActiveTableSwitcher -+ * @var ActiveTableSwitcher - */ - private $activeTableSwitcher; - -@@ -47,54 +70,61 @@ class Full extends \Magento\Catalog\Model\Indexer\Product\Price\AbstractAction - private $productMetaDataCached; - - /** -- * @var \Magento\Catalog\Model\Indexer\Product\Price\DimensionCollectionFactory -+ * @var DimensionCollectionFactory - */ - private $dimensionCollectionFactory; - - /** -- * @var \Magento\Catalog\Model\Indexer\Product\Price\TableMaintainer -+ * @var TableMaintainer - */ - private $dimensionTableMaintainer; - - /** -- * @var \Magento\Indexer\Model\ProcessManager -+ * @var ProcessManager - */ - private $processManager; - - /** -- * @param \Magento\Framework\App\Config\ScopeConfigInterface $config -- * @param \Magento\Store\Model\StoreManagerInterface $storeManager -- * @param \Magento\Directory\Model\CurrencyFactory $currencyFactory -- * @param \Magento\Framework\Stdlib\DateTime\TimezoneInterface $localeDate -- * @param \Magento\Framework\Stdlib\DateTime $dateTime -- * @param \Magento\Catalog\Model\Product\Type $catalogProductType -- * @param \Magento\Catalog\Model\ResourceModel\Product\Indexer\Price\Factory $indexerPriceFactory -- * @param \Magento\Catalog\Model\ResourceModel\Product\Indexer\Price\DefaultPrice $defaultIndexerResource -- * @param \Magento\Framework\EntityManager\MetadataPool|null $metadataPool -- * @param \Magento\Catalog\Model\ResourceModel\Product\Indexer\Price\BatchSizeCalculator|null $batchSizeCalculator -- * @param \Magento\Framework\Indexer\BatchProviderInterface|null $batchProvider -- * @param \Magento\Catalog\Model\ResourceModel\Indexer\ActiveTableSwitcher|null $activeTableSwitcher -- * @param \Magento\Catalog\Model\Indexer\Product\Price\DimensionCollectionFactory|null $dimensionCollectionFactory -- * @param \Magento\Catalog\Model\Indexer\Product\Price\TableMaintainer|null $dimensionTableMaintainer -- * @param \Magento\Indexer\Model\ProcessManager $processManager -+ * @var QueryGenerator|null -+ */ -+ private $batchQueryGenerator; -+ -+ /** -+ * @param ScopeConfigInterface $config -+ * @param StoreManagerInterface $storeManager -+ * @param CurrencyFactory $currencyFactory -+ * @param TimezoneInterface $localeDate -+ * @param DateTime $dateTime -+ * @param Type $catalogProductType -+ * @param Factory $indexerPriceFactory -+ * @param DefaultPrice $defaultIndexerResource -+ * @param MetadataPool|null $metadataPool -+ * @param BatchSizeCalculator|null $batchSizeCalculator -+ * @param BatchProviderInterface|null $batchProvider -+ * @param ActiveTableSwitcher|null $activeTableSwitcher -+ * @param DimensionCollectionFactory|null $dimensionCollectionFactory -+ * @param TableMaintainer|null $dimensionTableMaintainer -+ * @param ProcessManager $processManager -+ * @param QueryGenerator|null $batchQueryGenerator - * @SuppressWarnings(PHPMD.ExcessiveParameterList) - */ - public function __construct( -- \Magento\Framework\App\Config\ScopeConfigInterface $config, -- \Magento\Store\Model\StoreManagerInterface $storeManager, -- \Magento\Directory\Model\CurrencyFactory $currencyFactory, -- \Magento\Framework\Stdlib\DateTime\TimezoneInterface $localeDate, -- \Magento\Framework\Stdlib\DateTime $dateTime, -- \Magento\Catalog\Model\Product\Type $catalogProductType, -- \Magento\Catalog\Model\ResourceModel\Product\Indexer\Price\Factory $indexerPriceFactory, -- \Magento\Catalog\Model\ResourceModel\Product\Indexer\Price\DefaultPrice $defaultIndexerResource, -- \Magento\Framework\EntityManager\MetadataPool $metadataPool = null, -- \Magento\Catalog\Model\ResourceModel\Product\Indexer\Price\BatchSizeCalculator $batchSizeCalculator = null, -- \Magento\Framework\Indexer\BatchProviderInterface $batchProvider = null, -- \Magento\Catalog\Model\ResourceModel\Indexer\ActiveTableSwitcher $activeTableSwitcher = null, -- \Magento\Catalog\Model\Indexer\Product\Price\DimensionCollectionFactory $dimensionCollectionFactory = null, -- \Magento\Catalog\Model\Indexer\Product\Price\TableMaintainer $dimensionTableMaintainer = null, -- \Magento\Indexer\Model\ProcessManager $processManager = null -+ ScopeConfigInterface $config, -+ StoreManagerInterface $storeManager, -+ CurrencyFactory $currencyFactory, -+ TimezoneInterface $localeDate, -+ DateTime $dateTime, -+ Type $catalogProductType, -+ Factory $indexerPriceFactory, -+ DefaultPrice $defaultIndexerResource, -+ MetadataPool $metadataPool = null, -+ BatchSizeCalculator $batchSizeCalculator = null, -+ BatchProviderInterface $batchProvider = null, -+ ActiveTableSwitcher $activeTableSwitcher = null, -+ DimensionCollectionFactory $dimensionCollectionFactory = null, -+ TableMaintainer $dimensionTableMaintainer = null, -+ ProcessManager $processManager = null, -+ QueryGenerator $batchQueryGenerator = null - ) { - parent::__construct( - $config, -@@ -107,26 +137,27 @@ class Full extends \Magento\Catalog\Model\Indexer\Product\Price\AbstractAction - $defaultIndexerResource - ); - $this->metadataPool = $metadataPool ?: ObjectManager::getInstance()->get( -- \Magento\Framework\EntityManager\MetadataPool::class -+ MetadataPool::class - ); - $this->batchSizeCalculator = $batchSizeCalculator ?: ObjectManager::getInstance()->get( -- \Magento\Catalog\Model\ResourceModel\Product\Indexer\Price\BatchSizeCalculator::class -+ BatchSizeCalculator::class - ); - $this->batchProvider = $batchProvider ?: ObjectManager::getInstance()->get( -- \Magento\Framework\Indexer\BatchProviderInterface::class -+ BatchProviderInterface::class - ); - $this->activeTableSwitcher = $activeTableSwitcher ?: ObjectManager::getInstance()->get( -- \Magento\Catalog\Model\ResourceModel\Indexer\ActiveTableSwitcher::class -+ ActiveTableSwitcher::class - ); - $this->dimensionCollectionFactory = $dimensionCollectionFactory ?: ObjectManager::getInstance()->get( -- \Magento\Catalog\Model\Indexer\Product\Price\DimensionCollectionFactory::class -+ DimensionCollectionFactory::class - ); - $this->dimensionTableMaintainer = $dimensionTableMaintainer ?: ObjectManager::getInstance()->get( -- \Magento\Catalog\Model\Indexer\Product\Price\TableMaintainer::class -+ TableMaintainer::class - ); - $this->processManager = $processManager ?: ObjectManager::getInstance()->get( -- \Magento\Indexer\Model\ProcessManager::class -+ ProcessManager::class - ); -+ $this->batchQueryGenerator = $batchQueryGenerator ?? ObjectManager::getInstance()->get(QueryGenerator::class); - } - - /** -@@ -137,13 +168,13 @@ class Full extends \Magento\Catalog\Model\Indexer\Product\Price\AbstractAction - * @throws \Exception - * @SuppressWarnings(PHPMD.UnusedFormalParameter) - */ -- public function execute($ids = null) -+ public function execute($ids = null): void - { - try { - //Prepare indexer tables before full reindex - $this->prepareTables(); - -- /** @var \Magento\Catalog\Model\ResourceModel\Product\Indexer\Price\DefaultPrice $indexer */ -+ /** @var DefaultPrice $indexer */ - foreach ($this->getTypeIndexers(true) as $typeId => $priceIndexer) { - if ($priceIndexer instanceof DimensionalIndexerInterface) { - //New price reindex mechanism -@@ -170,7 +201,7 @@ class Full extends \Magento\Catalog\Model\Indexer\Product\Price\AbstractAction - * @return void - * @throws \Exception - */ -- private function prepareTables() -+ private function prepareTables(): void - { - $this->_defaultIndexerResource->getTableStrategy()->setUseIdxTable(false); - -@@ -185,7 +216,7 @@ class Full extends \Magento\Catalog\Model\Indexer\Product\Price\AbstractAction - * @return void - * @throws \Exception - */ -- private function truncateReplicaTables() -+ private function truncateReplicaTables(): void - { - foreach ($this->dimensionCollectionFactory->create() as $dimension) { - $dimensionTable = $this->dimensionTableMaintainer->getMainReplicaTable($dimension); -@@ -202,12 +233,12 @@ class Full extends \Magento\Catalog\Model\Indexer\Product\Price\AbstractAction - * @return void - * @throws \Exception - */ -- private function reindexProductTypeWithDimensions(DimensionalIndexerInterface $priceIndexer, string $typeId) -+ private function reindexProductTypeWithDimensions(DimensionalIndexerInterface $priceIndexer, string $typeId): void - { - $userFunctions = []; - foreach ($this->dimensionCollectionFactory->create() as $dimensions) { - $userFunctions[] = function () use ($priceIndexer, $dimensions, $typeId) { -- return $this->reindexByBatches($priceIndexer, $dimensions, $typeId); -+ $this->reindexByBatches($priceIndexer, $dimensions, $typeId); - }; - } - $this->processManager->execute($userFunctions); -@@ -223,10 +254,13 @@ class Full extends \Magento\Catalog\Model\Indexer\Product\Price\AbstractAction - * @return void - * @throws \Exception - */ -- private function reindexByBatches(DimensionalIndexerInterface $priceIndexer, array $dimensions, string $typeId) -- { -+ private function reindexByBatches( -+ DimensionalIndexerInterface $priceIndexer, -+ array $dimensions, -+ string $typeId -+ ): void { - foreach ($this->getBatchesForIndexer($typeId) as $batch) { -- $this->reindexByBatchWithDimensions($priceIndexer, $batch, $dimensions, $typeId); -+ $this->reindexByBatchWithDimensions($priceIndexer, $batch, $dimensions); - } - } - -@@ -235,16 +269,20 @@ class Full extends \Magento\Catalog\Model\Indexer\Product\Price\AbstractAction - * - * @param string $typeId - * -- * @return \Generator -+ * @return BatchIterator - * @throws \Exception - */ -- private function getBatchesForIndexer(string $typeId) -+ private function getBatchesForIndexer(string $typeId): BatchIterator - { - $connection = $this->_defaultIndexerResource->getConnection(); -- return $this->batchProvider->getBatches( -- $connection, -- $this->getProductMetaData()->getEntityTable(), -+ $entityMetadata = $this->getProductMetaData(); -+ $select = $connection->select(); -+ $select->distinct(true); -+ $select->from(['e' => $entityMetadata->getEntityTable()], $entityMetadata->getIdentifierField()); -+ -+ return $this->batchQueryGenerator->generate( - $this->getProductMetaData()->getIdentifierField(), -+ $select, - $this->batchSizeCalculator->estimateBatchSize( - $connection, - $typeId -@@ -256,20 +294,18 @@ class Full extends \Magento\Catalog\Model\Indexer\Product\Price\AbstractAction - * Reindex by batch for new 'Dimensional' price indexer - * - * @param DimensionalIndexerInterface $priceIndexer -- * @param array $batch -+ * @param Select $batchQuery - * @param array $dimensions -- * @param string $typeId - * - * @return void - * @throws \Exception - */ - private function reindexByBatchWithDimensions( - DimensionalIndexerInterface $priceIndexer, -- array $batch, -- array $dimensions, -- string $typeId -- ) { -- $entityIds = $this->getEntityIdsFromBatch($typeId, $batch); -+ Select $batchQuery, -+ array $dimensions -+ ): void { -+ $entityIds = $this->getEntityIdsFromBatch($batchQuery); - - if (!empty($entityIds)) { - $this->dimensionTableMaintainer->createMainTmpTable($dimensions); -@@ -295,10 +331,10 @@ class Full extends \Magento\Catalog\Model\Indexer\Product\Price\AbstractAction - * @return void - * @throws \Exception - */ -- private function reindexProductType(PriceInterface $priceIndexer, string $typeId) -+ private function reindexProductType(PriceInterface $priceIndexer, string $typeId): void - { - foreach ($this->getBatchesForIndexer($typeId) as $batch) { -- $this->reindexBatch($priceIndexer, $batch, $typeId); -+ $this->reindexBatch($priceIndexer, $batch); - } - } - -@@ -306,15 +342,13 @@ class Full extends \Magento\Catalog\Model\Indexer\Product\Price\AbstractAction - * Reindex by batch for old price indexer - * - * @param PriceInterface $priceIndexer -- * @param array $batch -- * @param string $typeId -- * -+ * @param Select $batch - * @return void - * @throws \Exception - */ -- private function reindexBatch(PriceInterface $priceIndexer, array $batch, string $typeId) -+ private function reindexBatch(PriceInterface $priceIndexer, Select $batch): void - { -- $entityIds = $this->getEntityIdsFromBatch($typeId, $batch); -+ $entityIds = $this->getEntityIdsFromBatch($batch); - - if (!empty($entityIds)) { - // Temporary table will created if not exists -@@ -339,27 +373,15 @@ class Full extends \Magento\Catalog\Model\Indexer\Product\Price\AbstractAction - /** - * Get Entity Ids from batch - * -- * @param string $typeId -- * @param array $batch -- * -+ * @param Select $batch - * @return array - * @throws \Exception - */ -- private function getEntityIdsFromBatch(string $typeId, array $batch) -+ private function getEntityIdsFromBatch(Select $batch): array - { - $connection = $this->_defaultIndexerResource->getConnection(); - -- // Get entity ids from batch -- $select = $connection -- ->select() -- ->distinct(true) -- ->from( -- ['e' => $this->getProductMetaData()->getEntityTable()], -- $this->getProductMetaData()->getIdentifierField() -- ) -- ->where('type_id = ?', $typeId); -- -- return $this->batchProvider->getBatchIds($connection, $select, $batch); -+ return $connection->fetchCol($batch); - } - - /** -@@ -368,7 +390,7 @@ class Full extends \Magento\Catalog\Model\Indexer\Product\Price\AbstractAction - * @return EntityMetadataInterface - * @throws \Exception - */ -- private function getProductMetaData() -+ private function getProductMetaData(): EntityMetadataInterface - { - if ($this->productMetaDataCached === null) { - $this->productMetaDataCached = $this->metadataPool->getMetadata(ProductInterface::class); -@@ -383,7 +405,7 @@ class Full extends \Magento\Catalog\Model\Indexer\Product\Price\AbstractAction - * @return string - * @throws \Exception - */ -- private function getReplicaTable() -+ private function getReplicaTable(): string - { - return $this->activeTableSwitcher->getAdditionalTableName( - $this->_defaultIndexerResource->getMainTable() -@@ -394,8 +416,9 @@ class Full extends \Magento\Catalog\Model\Indexer\Product\Price\AbstractAction - * Replacement of tables from replica to main - * - * @return void -+ * @throws \Zend_Db_Statement_Exception - */ -- private function switchTables() -+ private function switchTables(): void - { - // Switch dimension tables - $mainTablesByDimension = []; -@@ -417,13 +440,14 @@ class Full extends \Magento\Catalog\Model\Indexer\Product\Price\AbstractAction - - /** - * Move data from old price indexer mechanism to new indexer mechanism by dimensions. -+ * - * Used only for backward compatibility - * - * @param array $dimensions -- * - * @return void -+ * @throws \Zend_Db_Statement_Exception - */ -- private function moveDataFromReplicaTableToReplicaTables(array $dimensions) -+ private function moveDataFromReplicaTableToReplicaTables(array $dimensions): void - { - if (!$dimensions) { - return; -@@ -455,17 +479,17 @@ class Full extends \Magento\Catalog\Model\Indexer\Product\Price\AbstractAction - $select, - $replicaTablesByDimension, - [], -- \Magento\Framework\DB\Adapter\AdapterInterface::INSERT_ON_DUPLICATE -+ AdapterInterface::INSERT_ON_DUPLICATE - ) - ); - } - - /** -- * @deprecated -+ * Retrieves the index table that should be used - * -- * @inheritdoc -+ * @deprecated - */ -- protected function getIndexTargetTable() -+ protected function getIndexTargetTable(): string - { - return $this->activeTableSwitcher->getAdditionalTableName($this->_defaultIndexerResource->getMainTable()); - } -diff --git a/app/code/Magento/Catalog/Model/Indexer/Product/Price/DimensionModeConfiguration.php b/app/code/Magento/Catalog/Model/Indexer/Product/Price/DimensionModeConfiguration.php -index 9b8eb55b7aa..7a4d8e31346 100644 ---- a/app/code/Magento/Catalog/Model/Indexer/Product/Price/DimensionModeConfiguration.php -+++ b/app/code/Magento/Catalog/Model/Indexer/Product/Price/DimensionModeConfiguration.php -@@ -4,7 +4,6 @@ - * See COPYING.txt for license details. - */ - declare(strict_types=1); -- - namespace Magento\Catalog\Model\Indexer\Product\Price; - - use Magento\Framework\App\Config\ScopeConfigInterface; -@@ -41,6 +40,7 @@ class DimensionModeConfiguration - CustomerGroupDimensionProvider::DIMENSION_NAME - ], - ]; -+ - /** - * @var ScopeConfigInterface - */ -@@ -59,12 +59,23 @@ class DimensionModeConfiguration - $this->scopeConfig = $scopeConfig; - } - -+ /** -+ * Return dimension modes configuration. -+ * -+ * @return array -+ */ -+ public function getDimensionModes(): array -+ { -+ return $this->modesMapping; -+ } -+ - /** - * Get names of dimensions which used for provided mode. - * By default return dimensions for current enabled mode - * - * @param string|null $mode - * @return string[] -+ * @throws \InvalidArgumentException - */ - public function getDimensionConfiguration(string $mode = null): array - { -@@ -82,7 +93,7 @@ class DimensionModeConfiguration - private function getCurrentMode(): string - { - if (null === $this->currentMode) { -- $this->currentMode = $this->scopeConfig->getValue(ModeSwitcher::XML_PATH_PRICE_DIMENSIONS_MODE) -+ $this->currentMode = $this->scopeConfig->getValue(ModeSwitcherConfiguration::XML_PATH_PRICE_DIMENSIONS_MODE) - ?: self::DIMENSION_NONE; - } - -diff --git a/app/code/Magento/Catalog/Model/Indexer/Product/Price/ModeSwitcher.php b/app/code/Magento/Catalog/Model/Indexer/Product/Price/ModeSwitcher.php -index 81a170fc0a3..c418f2e1f25 100644 ---- a/app/code/Magento/Catalog/Model/Indexer/Product/Price/ModeSwitcher.php -+++ b/app/code/Magento/Catalog/Model/Indexer/Product/Price/ModeSwitcher.php -@@ -10,14 +10,14 @@ namespace Magento\Catalog\Model\Indexer\Product\Price; - use Magento\Framework\Search\Request\Dimension; - use Magento\Store\Model\Indexer\WebsiteDimensionProvider; - use Magento\Customer\Model\Indexer\CustomerGroupDimensionProvider; -+use Magento\Indexer\Model\DimensionModes; -+use Magento\Indexer\Model\DimensionMode; - - /** - * Class to prepare new tables for new indexer mode - */ --class ModeSwitcher -+class ModeSwitcher implements \Magento\Indexer\Model\ModeSwitcherInterface - { -- const XML_PATH_PRICE_DIMENSIONS_MODE = 'indexer/catalog_product_price/dimensions_mode'; -- - /** - * TableMaintainer - * -@@ -38,15 +38,60 @@ class ModeSwitcher - private $dimensionsArray; - - /** -- * @param \Magento\Catalog\Model\Indexer\Product\Price\TableMaintainer $tableMaintainer -- * @param \Magento\Catalog\Model\Indexer\Product\Price\DimensionCollectionFactory $dimensionCollectionFactory -+ * @var \Magento\Catalog\Model\Indexer\Product\Price\DimensionModeConfiguration -+ */ -+ private $dimensionModeConfiguration; -+ -+ /** -+ * @var ModeSwitcherConfiguration -+ */ -+ private $modeSwitcherConfiguration; -+ -+ /** -+ * @param TableMaintainer $tableMaintainer -+ * @param DimensionCollectionFactory $dimensionCollectionFactory -+ * @param DimensionModeConfiguration $dimensionModeConfiguration -+ * @param ModeSwitcherConfiguration $modeSwitcherConfiguration - */ - public function __construct( -- \Magento\Catalog\Model\Indexer\Product\Price\TableMaintainer $tableMaintainer, -- \Magento\Catalog\Model\Indexer\Product\Price\DimensionCollectionFactory $dimensionCollectionFactory -+ TableMaintainer $tableMaintainer, -+ DimensionCollectionFactory $dimensionCollectionFactory, -+ DimensionModeConfiguration $dimensionModeConfiguration, -+ ModeSwitcherConfiguration $modeSwitcherConfiguration - ) { - $this->tableMaintainer = $tableMaintainer; - $this->dimensionCollectionFactory = $dimensionCollectionFactory; -+ $this->dimensionModeConfiguration = $dimensionModeConfiguration; -+ $this->modeSwitcherConfiguration = $modeSwitcherConfiguration; -+ } -+ -+ /** -+ * @inheritdoc -+ */ -+ public function getDimensionModes(): DimensionModes -+ { -+ $dimensionsList = []; -+ foreach ($this->dimensionModeConfiguration->getDimensionModes() as $dimension => $modes) { -+ $dimensionsList[] = new DimensionMode($dimension, $modes); -+ } -+ -+ return new DimensionModes($dimensionsList); -+ } -+ -+ /** -+ * @inheritdoc -+ */ -+ public function switchMode(string $currentMode, string $previousMode) -+ { -+ //Create new tables and move data -+ $this->createTables($currentMode); -+ $this->moveData($currentMode, $previousMode); -+ -+ //Change config options -+ $this->modeSwitcherConfiguration->saveMode($currentMode); -+ -+ //Delete old tables -+ $this->dropTables($previousMode); - } - - /** -@@ -120,7 +165,7 @@ class ModeSwitcher - * - * @param string $mode - * -- * @return array -+ * @return \Magento\Framework\Indexer\MultiDimensionProvider - */ - private function getDimensionsArray(string $mode): \Magento\Framework\Indexer\MultiDimensionProvider - { -diff --git a/app/code/Magento/Catalog/Model/Indexer/Product/Price/ModeSwitcherConfiguration.php b/app/code/Magento/Catalog/Model/Indexer/Product/Price/ModeSwitcherConfiguration.php -new file mode 100644 -index 00000000000..ae00ec51f29 ---- /dev/null -+++ b/app/code/Magento/Catalog/Model/Indexer/Product/Price/ModeSwitcherConfiguration.php -@@ -0,0 +1,70 @@ -+<?php -+/** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+declare(strict_types=1); -+ -+namespace Magento\Catalog\Model\Indexer\Product\Price; -+ -+use Magento\Framework\App\Config\ConfigResource\ConfigInterface; -+use Magento\Framework\App\Cache\TypeListInterface; -+use Magento\Indexer\Model\Indexer; -+ -+/** -+ * Class to configure indexers and system config after modes has been switched -+ */ -+class ModeSwitcherConfiguration -+{ -+ const XML_PATH_PRICE_DIMENSIONS_MODE = 'indexer/catalog_product_price/dimensions_mode'; -+ -+ /** -+ * ConfigInterface -+ * -+ * @var ConfigInterface -+ */ -+ private $configWriter; -+ -+ /** -+ * TypeListInterface -+ * -+ * @var TypeListInterface -+ */ -+ private $cacheTypeList; -+ -+ /** -+ * @var Indexer $indexer -+ */ -+ private $indexer; -+ -+ /** -+ * @param ConfigInterface $configWriter -+ * @param TypeListInterface $cacheTypeList -+ * @param Indexer $indexer -+ */ -+ public function __construct( -+ ConfigInterface $configWriter, -+ TypeListInterface $cacheTypeList, -+ Indexer $indexer -+ ) { -+ $this->configWriter = $configWriter; -+ $this->cacheTypeList = $cacheTypeList; -+ $this->indexer = $indexer; -+ } -+ -+ /** -+ * Save switcher mode and invalidate reindex. -+ * -+ * @param string $mode -+ * @return void -+ * @throws \InvalidArgumentException -+ */ -+ public function saveMode(string $mode) -+ { -+ //Change config options -+ $this->configWriter->saveConfig(self::XML_PATH_PRICE_DIMENSIONS_MODE, $mode); -+ $this->cacheTypeList->cleanType('config'); -+ $this->indexer->load(\Magento\Catalog\Model\Indexer\Product\Price\Processor::INDEXER_ID); -+ $this->indexer->invalidate(); -+ } -+} -diff --git a/app/code/Magento/Catalog/Model/Layer/Filter/AbstractFilter.php b/app/code/Magento/Catalog/Model/Layer/Filter/AbstractFilter.php -index d21a8666ec0..f2e2e67f944 100644 ---- a/app/code/Magento/Catalog/Model/Layer/Filter/AbstractFilter.php -+++ b/app/code/Magento/Catalog/Model/Layer/Filter/AbstractFilter.php -@@ -139,7 +139,7 @@ abstract class AbstractFilter extends \Magento\Framework\DataObject implements F - } - - /** -- * Get fiter items count -+ * Get filter items count - * - * @return int - */ -diff --git a/app/code/Magento/Catalog/Model/Layer/Filter/Decimal.php b/app/code/Magento/Catalog/Model/Layer/Filter/Decimal.php -index dac2632ff6d..d76711cb21d 100644 ---- a/app/code/Magento/Catalog/Model/Layer/Filter/Decimal.php -+++ b/app/code/Magento/Catalog/Model/Layer/Filter/Decimal.php -@@ -32,8 +32,8 @@ class Decimal extends \Magento\Catalog\Model\Layer\Filter\AbstractFilter - * @param \Magento\Store\Model\StoreManagerInterface $storeManager - * @param \Magento\Catalog\Model\Layer $layer - * @param \Magento\Catalog\Model\Layer\Filter\Item\DataBuilder $itemDataBuilder -- * @param \Magento\Catalog\Model\ResourceModel\Layer\Filter\DecimalFactory $filterDecimalFactory - * @param \Magento\Framework\Pricing\PriceCurrencyInterface $priceCurrency -+ * @param \Magento\Catalog\Model\Layer\Filter\DataProvider\DecimalFactory $dataProviderFactory - * @param array $data - */ - public function __construct( -diff --git a/app/code/Magento/Catalog/Model/Layer/Filter/Item/DataBuilder.php b/app/code/Magento/Catalog/Model/Layer/Filter/Item/DataBuilder.php -index 4d2878b0b1e..07c9c2eaa24 100644 ---- a/app/code/Magento/Catalog/Model/Layer/Filter/Item/DataBuilder.php -+++ b/app/code/Magento/Catalog/Model/Layer/Filter/Item/DataBuilder.php -@@ -4,11 +4,11 @@ - * See COPYING.txt for license details. - */ - -+namespace Magento\Catalog\Model\Layer\Filter\Item; -+ - /** - * Item Data Builder - */ --namespace Magento\Catalog\Model\Layer\Filter\Item; -- - class DataBuilder - { - /** -@@ -29,7 +29,7 @@ class DataBuilder - * Add Item Data - * - * @param string $label -- * @param string $label -+ * @param string $value - * @param int $count - * @return void - */ -diff --git a/app/code/Magento/Catalog/Model/Plugin/ProductRepository/TransactionWrapper.php b/app/code/Magento/Catalog/Model/Plugin/ProductRepository/TransactionWrapper.php -index c88215d9235..f51b2e4f90a 100644 ---- a/app/code/Magento/Catalog/Model/Plugin/ProductRepository/TransactionWrapper.php -+++ b/app/code/Magento/Catalog/Model/Plugin/ProductRepository/TransactionWrapper.php -@@ -7,6 +7,9 @@ - */ - namespace Magento\Catalog\Model\Plugin\ProductRepository; - -+/** -+ * Transaction wrapper for product repository CRUD. -+ */ - class TransactionWrapper - { - /** -@@ -24,8 +27,10 @@ class TransactionWrapper - } - - /** -+ * Transaction wrapper for save action. -+ * - * @param \Magento\Catalog\Api\ProductRepositoryInterface $subject -- * @param callable $proceed -+ * @param \Closure $proceed - * @param \Magento\Catalog\Api\Data\ProductInterface $product - * @param bool $saveOptions - * @return \Magento\Catalog\Api\Data\ProductInterface -@@ -51,8 +56,10 @@ class TransactionWrapper - } - - /** -+ * Transaction wrapper for delete action. -+ * - * @param \Magento\Catalog\Api\ProductRepositoryInterface $subject -- * @param callable $proceed -+ * @param \Closure $proceed - * @param \Magento\Catalog\Api\Data\ProductInterface $product - * @return bool - * @throws \Exception -@@ -76,8 +83,10 @@ class TransactionWrapper - } - - /** -+ * Transaction wrapper for delete by id action. -+ * - * @param \Magento\Catalog\Api\ProductRepositoryInterface $subject -- * @param callable $proceed -+ * @param \Closure $proceed - * @param string $productSku - * @return bool - * @throws \Exception -diff --git a/app/code/Magento/Catalog/Model/Product.php b/app/code/Magento/Catalog/Model/Product.php -index 90af9bee270..61544f8fb57 100644 ---- a/app/code/Magento/Catalog/Model/Product.php -+++ b/app/code/Magento/Catalog/Model/Product.php -@@ -9,9 +9,8 @@ use Magento\Catalog\Api\CategoryRepositoryInterface; - use Magento\Catalog\Api\Data\ProductAttributeMediaGalleryEntryInterface; - use Magento\Catalog\Api\Data\ProductInterface; - use Magento\Catalog\Api\ProductLinkRepositoryInterface; --use Magento\Catalog\Model\Entity\GetProductCustomAttributeCodes; - use Magento\Catalog\Model\Product\Attribute\Backend\Media\EntryConverterPool; --use Magento\Eav\Model\Entity\GetCustomAttributeCodesInterface; -+use Magento\Catalog\Model\FilterProductCustomAttribute; - use Magento\Framework\Api\AttributeValueFactory; - use Magento\Framework\App\Filesystem\DirectoryList; - use Magento\Framework\App\ObjectManager; -@@ -176,7 +175,7 @@ class Product extends \Magento\Catalog\Model\AbstractModel implements - protected $_catalogProduct = null; - - /** -- * @var \Magento\Framework\Module\Manager -+ * @var \Magento\Framework\Module\ModuleManagerInterface - */ - protected $moduleManager; - -@@ -278,6 +277,7 @@ class Product extends \Magento\Catalog\Model\AbstractModel implements - - /** - * @var \Magento\Catalog\Api\ProductAttributeRepositoryInterface -+ * @deprecated Not used anymore due to performance issue (loaded all product attributes) - */ - protected $metadataService; - -@@ -345,12 +345,15 @@ class Product extends \Magento\Catalog\Model\AbstractModel implements - protected $linkTypeProvider; - - /** -- * @var GetCustomAttributeCodesInterface -+ * @var \Magento\Eav\Model\Config - */ -- private $getCustomAttributeCodes; -+ private $eavConfig; -+ /** -+ * @var FilterProductCustomAttribute|null -+ */ -+ private $filterCustomAttribute; - - /** -- * Product constructor. - * @param \Magento\Framework\Model\Context $context - * @param \Magento\Framework\Registry $registry - * @param \Magento\Framework\Api\ExtensionAttributesFactory $extensionFactory -@@ -366,7 +369,7 @@ class Product extends \Magento\Catalog\Model\AbstractModel implements - * @param Product\Attribute\Source\Status $catalogProductStatus - * @param Product\Media\Config $catalogProductMediaConfig - * @param Product\Type $catalogProductType -- * @param \Magento\Framework\Module\Manager $moduleManager -+ * @param \Magento\Framework\Module\ModuleManagerInterface $moduleManager - * @param \Magento\Catalog\Helper\Product $catalogProduct - * @param ResourceModel\Product $resource - * @param ResourceModel\Product\Collection $resourceCollection -@@ -386,7 +389,8 @@ class Product extends \Magento\Catalog\Model\AbstractModel implements - * @param \Magento\Framework\Api\DataObjectHelper $dataObjectHelper - * @param \Magento\Framework\Api\ExtensionAttribute\JoinProcessorInterface $joinProcessor - * @param array $data -- * @param GetCustomAttributeCodesInterface|null $getCustomAttributeCodes -+ * @param \Magento\Eav\Model\Config|null $config -+ * @param FilterProductCustomAttribute|null $filterCustomAttribute - * @SuppressWarnings(PHPMD.ExcessiveParameterList) - * @SuppressWarnings(PHPMD.UnusedFormalParameter) - */ -@@ -406,7 +410,7 @@ class Product extends \Magento\Catalog\Model\AbstractModel implements - \Magento\Catalog\Model\Product\Attribute\Source\Status $catalogProductStatus, - \Magento\Catalog\Model\Product\Media\Config $catalogProductMediaConfig, - Product\Type $catalogProductType, -- \Magento\Framework\Module\Manager $moduleManager, -+ \Magento\Framework\Module\ModuleManagerInterface $moduleManager, - \Magento\Catalog\Helper\Product $catalogProduct, - \Magento\Catalog\Model\ResourceModel\Product $resource, - \Magento\Catalog\Model\ResourceModel\Product\Collection $resourceCollection, -@@ -426,7 +430,8 @@ class Product extends \Magento\Catalog\Model\AbstractModel implements - \Magento\Framework\Api\DataObjectHelper $dataObjectHelper, - \Magento\Framework\Api\ExtensionAttribute\JoinProcessorInterface $joinProcessor, - array $data = [], -- GetCustomAttributeCodesInterface $getCustomAttributeCodes = null -+ \Magento\Eav\Model\Config $config = null, -+ FilterProductCustomAttribute $filterCustomAttribute = null - ) { - $this->metadataService = $metadataService; - $this->_itemOptionFactory = $itemOptionFactory; -@@ -455,9 +460,6 @@ class Product extends \Magento\Catalog\Model\AbstractModel implements - $this->mediaGalleryEntryConverterPool = $mediaGalleryEntryConverterPool; - $this->dataObjectHelper = $dataObjectHelper; - $this->joinProcessor = $joinProcessor; -- $this->getCustomAttributeCodes = $getCustomAttributeCodes ?? ObjectManager::getInstance()->get( -- GetProductCustomAttributeCodes::class -- ); - parent::__construct( - $context, - $registry, -@@ -468,6 +470,9 @@ class Product extends \Magento\Catalog\Model\AbstractModel implements - $resourceCollection, - $data - ); -+ $this->eavConfig = $config ?? ObjectManager::getInstance()->get(\Magento\Eav\Model\Config::class); -+ $this->filterCustomAttribute = $filterCustomAttribute -+ ?? ObjectManager::getInstance()->get(FilterProductCustomAttribute::class); - } - - /** -@@ -482,6 +487,7 @@ class Product extends \Magento\Catalog\Model\AbstractModel implements - - /** - * Get resource instance -+ * phpcs:disable Generic.CodeAnalysis.UselessOverridingMethod - * - * @throws \Magento\Framework\Exception\LocalizedException - * @return \Magento\Catalog\Model\ResourceModel\Product -@@ -493,11 +499,29 @@ class Product extends \Magento\Catalog\Model\AbstractModel implements - } - - /** -- * {@inheritdoc} -+ * Get a list of custom attribute codes that belongs to product attribute set. -+ * -+ * If attribute set not specified for product will return all product attribute codes -+ * -+ * @return string[] - */ - protected function getCustomAttributesCodes() - { -- return $this->getCustomAttributeCodes->execute($this->metadataService); -+ if ($this->customAttributesCodes === null) { -+ $this->customAttributesCodes = array_diff( -+ array_keys( -+ $this->filterCustomAttribute->execute( -+ $this->eavConfig->getEntityAttributes( -+ self::ENTITY, -+ $this -+ ) -+ ) -+ ), -+ ProductInterface::ATTRIBUTES -+ ); -+ } -+ -+ return $this->customAttributesCodes; - } - - /** -@@ -508,9 +532,9 @@ class Product extends \Magento\Catalog\Model\AbstractModel implements - public function getStoreId() - { - if ($this->hasData(self::STORE_ID)) { -- return $this->getData(self::STORE_ID); -+ return (int)$this->getData(self::STORE_ID); - } -- return $this->_storeManager->getStore()->getId(); -+ return (int)$this->_storeManager->getStore()->getId(); - } - - /** -@@ -566,8 +590,9 @@ class Product extends \Magento\Catalog\Model\AbstractModel implements - } - - /** -- * @codeCoverageIgnoreStart - * Get visibility status -+ * -+ * @codeCoverageIgnoreStart - * @see \Magento\Catalog\Model\Product\Visibility - * - * @return int -@@ -644,6 +669,7 @@ class Product extends \Magento\Catalog\Model\AbstractModel implements - - /** - * Retrieve type instance of the product. -+ * - * Type instance implements product type depended logic and is a singleton shared by all products of the same type. - * - * @return \Magento\Catalog\Model\Product\Type\AbstractType -@@ -697,7 +723,7 @@ class Product extends \Magento\Catalog\Model\AbstractModel implements - public function getCategoryId() - { - $category = $this->_registry->registry('current_category'); -- if ($category) { -+ if ($category && in_array($category->getId(), $this->getCategoryIds())) { - return $category->getId(); - } - return false; -@@ -792,6 +818,9 @@ class Product extends \Magento\Catalog\Model\AbstractModel implements - if (!$this->hasStoreIds()) { - $storeIds = []; - if ($websiteIds = $this->getWebsiteIds()) { -+ if ($this->_storeManager->isSingleStoreMode()) { -+ $websiteIds = array_keys($websiteIds); -+ } - foreach ($websiteIds as $websiteId) { - $websiteStores = $this->_storeManager->getWebsite($websiteId)->getStoreIds(); - $storeIds = array_merge($storeIds, $websiteStores); -@@ -804,9 +833,10 @@ class Product extends \Magento\Catalog\Model\AbstractModel implements - - /** - * Retrieve product attributes -- * if $groupId is null - retrieve all product attributes - * -- * @param int $groupId Retrieve attributes of the specified group -+ * If $groupId is null - retrieve all product attributes -+ * -+ * @param int $groupId Retrieve attributes of the specified group - * @param bool $skipSuper Not used - * @return \Magento\Eav\Model\Entity\Attribute\AbstractAttribute[] - * @SuppressWarnings(PHPMD.UnusedFormalParameter) -@@ -898,10 +928,11 @@ class Product extends \Magento\Catalog\Model\AbstractModel implements - - /** - * Check/set if options can be affected when saving product -+ * - * If value specified, it will be set. - * -- * @param bool $value -- * @return bool -+ * @param bool $value -+ * @return bool - */ - public function canAffectOptions($value = null) - { -@@ -958,7 +989,7 @@ class Product extends \Magento\Catalog\Model\AbstractModel implements - */ - public function getQty() - { -- return $this->getData('qty'); -+ return (float)$this->getData('qty'); - } - - /** -@@ -1018,9 +1049,11 @@ class Product extends \Magento\Catalog\Model\AbstractModel implements - - /** - * Clear cache related with product and protect delete from not admin -+ * - * Register indexing event before delete product - * - * @return \Magento\Catalog\Model\Product -+ * @throws \Magento\Framework\Exception\LocalizedException - */ - public function beforeDelete() - { -@@ -1134,8 +1167,7 @@ class Product extends \Magento\Catalog\Model\AbstractModel implements - /** - * Get formatted by currency product price - * -- * @return array|double -- * -+ * @return array|double* - * @deprecated - * @see getFormattedPrice() - */ -@@ -1527,12 +1559,13 @@ class Product extends \Magento\Catalog\Model\AbstractModel implements - /** - * Add image to media gallery - * -- * @param string $file file path of image in file system -- * @param string|array $mediaAttribute code of attribute with type 'media_image', -- * leave blank if image should be only in gallery -- * @param boolean $move if true, it will move source file -- * @param boolean $exclude mark image as disabled in product page view -+ * @param string $file file path of image in file system -+ * @param string|array $mediaAttribute code of attribute with type 'media_image', -+ * leave blank if image should be only in gallery -+ * @param bool $move if true, it will move source file -+ * @param bool $exclude mark image as disabled in product page view - * @return \Magento\Catalog\Model\Product -+ * @throws \Magento\Framework\Exception\LocalizedException - */ - public function addImageToMediaGallery($file, $mediaAttribute = null, $move = false, $exclude = true) - { -@@ -1693,7 +1726,6 @@ class Product extends \Magento\Catalog\Model\AbstractModel implements - - /** - * Check is a virtual product -- * Data helper wrapper - * - * @return bool - */ -@@ -1783,11 +1815,11 @@ class Product extends \Magento\Catalog\Model\AbstractModel implements - } - - /** -- * Save current attribute with code $code and assign new value -+ * Save current attribute with code $code and assign new value. - * -- * @param string $code Attribute code -- * @param mixed $value New attribute value -- * @param int $store Store ID -+ * @param string $code Attribute code -+ * @param mixed $value New attribute value -+ * @param int $store Store ID - * @return void - */ - public function addAttributeUpdate($code, $value, $store) -@@ -1857,6 +1889,7 @@ class Product extends \Magento\Catalog\Model\AbstractModel implements - - /** - * Custom function for other modules -+ * - * @return string - */ - public function getGiftMessageAvailable() -@@ -1975,6 +2008,8 @@ class Product extends \Magento\Catalog\Model\AbstractModel implements - } - - /** -+ * Set product options -+ * - * @param \Magento\Catalog\Api\Data\ProductCustomOptionInterface[] $options - * @return $this - */ -@@ -1998,10 +2033,10 @@ class Product extends \Magento\Catalog\Model\AbstractModel implements - /** - * Add custom option information to product - * -- * @param string $code Option code -- * @param mixed $value Value of the option -- * @param int|Product $product Product ID -- * @return $this -+ * @param string $code Option code -+ * @param mixed $value Value of the option -+ * @param int|Product|null $product Product ID -+ * @return $this - */ - public function addCustomOption($code, $value, $product = null) - { -@@ -2195,6 +2230,7 @@ class Product extends \Magento\Catalog\Model\AbstractModel implements - - /** - * Prepare product custom options. -+ * - * To be sure that all product custom options does not has ID and has product instance - * - * @return \Magento\Catalog\Model\Product -@@ -2529,9 +2565,9 @@ class Product extends \Magento\Catalog\Model\AbstractModel implements - } - - /** -- * {@inheritdoc} -+ * @inheritdoc - * -- * @return \Magento\Catalog\Api\Data\ProductExtensionInterface -+ * @return \Magento\Framework\Api\ExtensionAttributesInterface - */ - public function getExtensionAttributes() - { -@@ -2539,7 +2575,7 @@ class Product extends \Magento\Catalog\Model\AbstractModel implements - } - - /** -- * {@inheritdoc} -+ * @inheritdoc - * - * @param \Magento\Catalog\Api\Data\ProductExtensionInterface $extensionAttributes - * @return $this -@@ -2552,8 +2588,11 @@ class Product extends \Magento\Catalog\Model\AbstractModel implements - //@codeCoverageIgnoreEnd - - /** -+ * Convert Image to ProductAttributeMediaGalleryEntryInterface -+ * - * @param array $mediaGallery - * @return \Magento\Catalog\Api\Data\ProductAttributeMediaGalleryEntryInterface[] -+ * @throws \Magento\Framework\Exception\LocalizedException - */ - protected function convertToMediaGalleryInterface(array $mediaGallery) - { -@@ -2569,7 +2608,10 @@ class Product extends \Magento\Catalog\Model\AbstractModel implements - } - - /** -+ * Get media gallery entries -+ * - * @return \Magento\Catalog\Api\Data\ProductAttributeMediaGalleryEntryInterface[]|null -+ * @throws \Magento\Framework\Exception\LocalizedException - */ - public function getMediaGalleryEntries() - { -@@ -2583,8 +2625,11 @@ class Product extends \Magento\Catalog\Model\AbstractModel implements - } - - /** -+ * Set media gallery entries -+ * - * @param ProductAttributeMediaGalleryEntryInterface[] $mediaGalleryEntries - * @return $this -+ * @throws \Magento\Framework\Exception\LocalizedException - */ - public function setMediaGalleryEntries(array $mediaGalleryEntries = null) - { -@@ -2625,6 +2670,8 @@ class Product extends \Magento\Catalog\Model\AbstractModel implements - } - - /** -+ * Get link repository -+ * - * @return ProductLinkRepositoryInterface - */ - private function getLinkRepository() -@@ -2637,6 +2684,8 @@ class Product extends \Magento\Catalog\Model\AbstractModel implements - } - - /** -+ * Get media gallery processor -+ * - * @return Product\Gallery\Processor - */ - private function getMediaGalleryProcessor() -diff --git a/app/code/Magento/Catalog/Model/Product/Action.php b/app/code/Magento/Catalog/Model/Product/Action.php -index f78048424b4..3863cf24572 100644 ---- a/app/code/Magento/Catalog/Model/Product/Action.php -+++ b/app/code/Magento/Catalog/Model/Product/Action.php -@@ -168,5 +168,7 @@ class Action extends \Magento\Framework\Model\AbstractModel - if (!$categoryIndexer->isScheduled()) { - $categoryIndexer->reindexList(array_unique($productIds)); - } -+ -+ $this->_eventManager->dispatch('catalog_product_to_website_change', ['products' => $productIds]); - } - } -diff --git a/app/code/Magento/Catalog/Model/Product/Attribute/Backend/Boolean.php b/app/code/Magento/Catalog/Model/Product/Attribute/Backend/Boolean.php -index c2108b0273b..be1e523960f 100644 ---- a/app/code/Magento/Catalog/Model/Product/Attribute/Backend/Boolean.php -+++ b/app/code/Magento/Catalog/Model/Product/Attribute/Backend/Boolean.php -@@ -3,6 +3,8 @@ - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -+declare(strict_types=1); -+ - namespace Magento\Catalog\Model\Product\Attribute\Backend; - - use Magento\Catalog\Model\Product\Attribute\Source\Boolean as BooleanSource; -@@ -25,7 +27,9 @@ class Boolean extends \Magento\Eav\Model\Entity\Attribute\Backend\AbstractBacken - $attributeCode = $this->getAttribute()->getName(); - if ($object->getData('use_config_' . $attributeCode)) { - $object->setData($attributeCode, BooleanSource::VALUE_USE_CONFIG); -+ return $this; - } -- return $this; -+ -+ return parent::beforeSave($object); - } - } -diff --git a/app/code/Magento/Catalog/Model/Product/Attribute/Backend/GroupPrice/AbstractGroupPrice.php b/app/code/Magento/Catalog/Model/Product/Attribute/Backend/GroupPrice/AbstractGroupPrice.php -index 3779cab431c..e26717e4727 100644 ---- a/app/code/Magento/Catalog/Model/Product/Attribute/Backend/GroupPrice/AbstractGroupPrice.php -+++ b/app/code/Magento/Catalog/Model/Product/Attribute/Backend/GroupPrice/AbstractGroupPrice.php -@@ -247,6 +247,8 @@ abstract class AbstractGroupPrice extends Price - } - - /** -+ * Validate price. -+ * - * @param array $priceRow - * @return void - * @throws \Magento\Framework\Exception\LocalizedException -@@ -312,6 +314,8 @@ abstract class AbstractGroupPrice extends Price - } - - /** -+ * Get website id. -+ * - * @param int $storeId - * @return int|null - */ -@@ -327,6 +331,8 @@ abstract class AbstractGroupPrice extends Price - } - - /** -+ * Set price data. -+ * - * @param \Magento\Catalog\Model\Product $object - * @param array $priceData - */ -@@ -373,122 +379,16 @@ abstract class AbstractGroupPrice extends Price - * - * @param \Magento\Catalog\Model\Product $object - * @return $this -- * @SuppressWarnings(PHPMD.CyclomaticComplexity) -- * @SuppressWarnings(PHPMD.NPathComplexity) -- * @SuppressWarnings(PHPMD.ExcessiveMethodLength) -+ * @SuppressWarnings(PHPMD.UnusedFormalParameter) - */ - public function afterSave($object) - { -- $websiteId = $this->_storeManager->getStore($object->getStoreId())->getWebsiteId(); -- $isGlobal = $this->getAttribute()->isScopeGlobal() || $websiteId == 0; -- -- $priceRows = $object->getData($this->getAttribute()->getName()); -- if (null === $priceRows) { -- return $this; -- } -- -- $priceRows = array_filter((array)$priceRows); -- -- $old = []; -- $new = []; -- -- // prepare original data for compare -- $origPrices = $object->getOrigData($this->getAttribute()->getName()); -- if (!is_array($origPrices)) { -- $origPrices = []; -- } -- foreach ($origPrices as $data) { -- if ($data['website_id'] > 0 || $data['website_id'] == '0' && $isGlobal) { -- $key = implode( -- '-', -- array_merge( -- [$data['website_id'], $data['cust_group']], -- $this->_getAdditionalUniqueFields($data) -- ) -- ); -- $old[$key] = $data; -- } -- } -- -- // prepare data for save -- foreach ($priceRows as $data) { -- $hasEmptyData = false; -- foreach ($this->_getAdditionalUniqueFields($data) as $field) { -- if (empty($field)) { -- $hasEmptyData = true; -- break; -- } -- } -- -- if ($hasEmptyData || !isset($data['cust_group']) || !empty($data['delete'])) { -- continue; -- } -- if ($this->getAttribute()->isScopeGlobal() && $data['website_id'] > 0) { -- continue; -- } -- if (!$isGlobal && (int)$data['website_id'] == 0) { -- continue; -- } -- -- $key = implode( -- '-', -- array_merge([$data['website_id'], $data['cust_group']], $this->_getAdditionalUniqueFields($data)) -- ); -- -- $useForAllGroups = $data['cust_group'] == $this->_groupManagement->getAllCustomersGroup()->getId(); -- $customerGroupId = !$useForAllGroups ? $data['cust_group'] : 0; -- $new[$key] = array_merge( -- $this->getAdditionalFields($data), -- [ -- 'website_id' => $data['website_id'], -- 'all_groups' => $useForAllGroups ? 1 : 0, -- 'customer_group_id' => $customerGroupId, -- 'value' => isset($data['price']) ? $data['price'] : null, -- ], -- $this->_getAdditionalUniqueFields($data) -- ); -- } -- -- $delete = array_diff_key($old, $new); -- $insert = array_diff_key($new, $old); -- $update = array_intersect_key($new, $old); -- -- $isChanged = false; -- $productId = $object->getData($this->getMetadataPool()->getMetadata(ProductInterface::class)->getLinkField()); -- -- if (!empty($delete)) { -- foreach ($delete as $data) { -- $this->_getResource()->deletePriceData($productId, null, $data['price_id']); -- $isChanged = true; -- } -- } -- -- if (!empty($insert)) { -- foreach ($insert as $data) { -- $price = new \Magento\Framework\DataObject($data); -- $price->setData( -- $this->getMetadataPool()->getMetadata(ProductInterface::class)->getLinkField(), -- $productId -- ); -- $this->_getResource()->savePriceData($price); -- -- $isChanged = true; -- } -- } -- -- if (!empty($update)) { -- $isChanged |= $this->updateValues($update, $old); -- } -- -- if ($isChanged) { -- $valueChangedKey = $this->getAttribute()->getName() . '_changed'; -- $object->setData($valueChangedKey, 1); -- } -- - return $this; - } - - /** -+ * Update values. -+ * - * @param array $valuesToUpdate - * @param array $oldValues - * @return boolean -@@ -544,6 +444,8 @@ abstract class AbstractGroupPrice extends Price - } - - /** -+ * Get metadata pool. -+ * - * @return \Magento\Framework\EntityManager\MetadataPool - */ - private function getMetadataPool() -diff --git a/app/code/Magento/Catalog/Model/Product/Attribute/Backend/Sku.php b/app/code/Magento/Catalog/Model/Product/Attribute/Backend/Sku.php -index a652d0ef902..98738e055ca 100644 ---- a/app/code/Magento/Catalog/Model/Product/Attribute/Backend/Sku.php -+++ b/app/code/Magento/Catalog/Model/Product/Attribute/Backend/Sku.php -@@ -4,15 +4,13 @@ - * See COPYING.txt for license details. - */ - --/** -- * Catalog product SKU backend attribute model -- * -- * @author Magento Core Team <core@magentocommerce.com> -- */ - namespace Magento\Catalog\Model\Product\Attribute\Backend; - - use Magento\Catalog\Model\Product; - -+/** -+ * Catalog product SKU backend attribute model. -+ */ - class Sku extends \Magento\Eav\Model\Entity\Attribute\Backend\AbstractBackend - { - /** -@@ -97,6 +95,7 @@ class Sku extends \Magento\Eav\Model\Entity\Attribute\Backend\AbstractBackend - public function beforeSave($object) - { - $this->_generateUniqueSku($object); -+ $this->trimValue($object); - return parent::beforeSave($object); - } - -@@ -127,4 +126,19 @@ class Sku extends \Magento\Eav\Model\Entity\Attribute\Backend\AbstractBackend - $data = $connection->fetchOne($select, $bind); - return abs((int)str_replace($value, '', $data)); - } -+ -+ /** -+ * Remove extra spaces from attribute value before save. -+ * -+ * @param Product $object -+ * @return void -+ */ -+ private function trimValue($object) -+ { -+ $attrCode = $this->getAttribute()->getAttributeCode(); -+ $value = $object->getData($attrCode); -+ if ($value) { -+ $object->setData($attrCode, trim($value)); -+ } -+ } - } -diff --git a/app/code/Magento/Catalog/Model/Product/Attribute/Backend/TierPrice/AbstractHandler.php b/app/code/Magento/Catalog/Model/Product/Attribute/Backend/TierPrice/AbstractHandler.php -new file mode 100644 -index 00000000000..fc0f090937d ---- /dev/null -+++ b/app/code/Magento/Catalog/Model/Product/Attribute/Backend/TierPrice/AbstractHandler.php -@@ -0,0 +1,101 @@ -+<?php -+/** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+declare(strict_types=1); -+ -+namespace Magento\Catalog\Model\Product\Attribute\Backend\TierPrice; -+ -+use Magento\Framework\EntityManager\Operation\ExtensionInterface; -+use Magento\Store\Model\StoreManagerInterface; -+use Magento\Catalog\Api\ProductAttributeRepositoryInterface; -+use Magento\Catalog\Api\Data\ProductInterface; -+use Magento\Customer\Api\GroupManagementInterface; -+use Magento\Framework\EntityManager\MetadataPool; -+use Magento\Catalog\Model\ResourceModel\Product\Attribute\Backend\Tierprice; -+ -+/** -+ * Tier price data abstract handler. -+ */ -+abstract class AbstractHandler implements ExtensionInterface -+{ -+ /** -+ * @var \Magento\Customer\Api\GroupManagementInterface -+ */ -+ protected $groupManagement; -+ -+ /** -+ * @param \Magento\Customer\Api\GroupManagementInterface $groupManagement -+ */ -+ public function __construct( -+ GroupManagementInterface $groupManagement -+ ) { -+ $this->groupManagement = $groupManagement; -+ } -+ -+ /** -+ * Get additional tier price fields. -+ * -+ * @param array $objectArray -+ * @return array -+ */ -+ protected function getAdditionalFields(array $objectArray): array -+ { -+ $percentageValue = $this->getPercentage($objectArray); -+ -+ return [ -+ 'value' => $percentageValue ? null : $objectArray['price'], -+ 'percentage_value' => $percentageValue ?: null, -+ ]; -+ } -+ -+ /** -+ * Check whether price has percentage value. -+ * -+ * @param array $priceRow -+ * @return float|null -+ */ -+ protected function getPercentage(array $priceRow): ?float -+ { -+ return isset($priceRow['percentage_value']) && is_numeric($priceRow['percentage_value']) -+ ? (float)$priceRow['percentage_value'] -+ : null; -+ } -+ -+ /** -+ * Prepare tier price data by provided price row data. -+ * -+ * @param array $data -+ * @return array -+ * @throws \Magento\Framework\Exception\LocalizedException -+ */ -+ protected function prepareTierPrice(array $data): array -+ { -+ $useForAllGroups = (int)$data['cust_group'] === $this->groupManagement->getAllCustomersGroup()->getId(); -+ $customerGroupId = $useForAllGroups ? 0 : $data['cust_group']; -+ $tierPrice = array_merge( -+ $this->getAdditionalFields($data), -+ [ -+ 'website_id' => $data['website_id'], -+ 'all_groups' => (int)$useForAllGroups, -+ 'customer_group_id' => $customerGroupId, -+ 'value' => $data['price'] ?? null, -+ 'qty' => $this->parseQty($data['price_qty']), -+ ] -+ ); -+ -+ return $tierPrice; -+ } -+ -+ /** -+ * Parse quantity value into float. -+ * -+ * @param mixed $value -+ * @return float|int -+ */ -+ protected function parseQty($value) -+ { -+ return $value * 1; -+ } -+} -diff --git a/app/code/Magento/Catalog/Model/Product/Attribute/Backend/TierPrice/SaveHandler.php b/app/code/Magento/Catalog/Model/Product/Attribute/Backend/TierPrice/SaveHandler.php -new file mode 100644 -index 00000000000..9cb2ac01458 ---- /dev/null -+++ b/app/code/Magento/Catalog/Model/Product/Attribute/Backend/TierPrice/SaveHandler.php -@@ -0,0 +1,112 @@ -+<?php -+/** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+declare(strict_types=1); -+ -+namespace Magento\Catalog\Model\Product\Attribute\Backend\TierPrice; -+ -+use Magento\Framework\EntityManager\Operation\ExtensionInterface; -+use Magento\Store\Model\StoreManagerInterface; -+use Magento\Catalog\Api\ProductAttributeRepositoryInterface; -+use Magento\Catalog\Api\Data\ProductInterface; -+use Magento\Customer\Api\GroupManagementInterface; -+use Magento\Framework\EntityManager\MetadataPool; -+use Magento\Catalog\Model\ResourceModel\Product\Attribute\Backend\Tierprice; -+ -+/** -+ * Process tier price data for handled new product -+ */ -+class SaveHandler extends AbstractHandler -+{ -+ /** -+ * @var \Magento\Store\Model\StoreManagerInterface -+ */ -+ private $storeManager; -+ -+ /** -+ * @var \Magento\Catalog\Api\ProductAttributeRepositoryInterface -+ */ -+ private $attributeRepository; -+ -+ /** -+ * @var \Magento\Framework\EntityManager\MetadataPool -+ */ -+ private $metadataPoll; -+ -+ /** -+ * @var \Magento\Catalog\Model\ResourceModel\Product\Attribute\Backend\Tierprice -+ */ -+ private $tierPriceResource; -+ -+ /** -+ * @param \Magento\Store\Model\StoreManagerInterface $storeManager -+ * @param \Magento\Catalog\Api\ProductAttributeRepositoryInterface $attributeRepository -+ * @param \Magento\Customer\Api\GroupManagementInterface $groupManagement -+ * @param \Magento\Framework\EntityManager\MetadataPool $metadataPool -+ * @param \Magento\Catalog\Model\ResourceModel\Product\Attribute\Backend\Tierprice $tierPriceResource -+ */ -+ public function __construct( -+ StoreManagerInterface $storeManager, -+ ProductAttributeRepositoryInterface $attributeRepository, -+ GroupManagementInterface $groupManagement, -+ MetadataPool $metadataPool, -+ Tierprice $tierPriceResource -+ ) { -+ parent::__construct($groupManagement); -+ -+ $this->storeManager = $storeManager; -+ $this->attributeRepository = $attributeRepository; -+ $this->metadataPoll = $metadataPool; -+ $this->tierPriceResource = $tierPriceResource; -+ } -+ -+ /** -+ * Set tier price data for product entity -+ * -+ * @param \Magento\Catalog\Api\Data\ProductInterface|object $entity -+ * @param array $arguments -+ * @return \Magento\Catalog\Api\Data\ProductInterface|object -+ * @throws \Magento\Framework\Exception\InputException -+ * @SuppressWarnings(PHPMD.UnusedFormalParameter) -+ */ -+ public function execute($entity, $arguments = []) -+ { -+ $attribute = $this->attributeRepository->get('tier_price'); -+ $priceRows = $entity->getData($attribute->getName()); -+ if (null !== $priceRows) { -+ if (!is_array($priceRows)) { -+ throw new \Magento\Framework\Exception\InputException( -+ __('Tier prices data should be array, but actually other type is received') -+ ); -+ } -+ $websiteId = $this->storeManager->getStore($entity->getStoreId())->getWebsiteId(); -+ $isGlobal = $attribute->isScopeGlobal() || $websiteId === 0; -+ $identifierField = $this->metadataPoll->getMetadata(ProductInterface::class)->getLinkField(); -+ $priceRows = array_filter($priceRows); -+ $productId = (int) $entity->getData($identifierField); -+ -+ // prepare and save data -+ foreach ($priceRows as $data) { -+ $isPriceWebsiteGlobal = (int)$data['website_id'] === 0; -+ if ($isGlobal === $isPriceWebsiteGlobal -+ || !empty($data['price_qty']) -+ || isset($data['cust_group']) -+ ) { -+ $tierPrice = $this->prepareTierPrice($data); -+ $price = new \Magento\Framework\DataObject($tierPrice); -+ $price->setData( -+ $identifierField, -+ $productId -+ ); -+ $this->tierPriceResource->savePriceData($price); -+ $valueChangedKey = $attribute->getName() . '_changed'; -+ $entity->setData($valueChangedKey, 1); -+ } -+ } -+ } -+ -+ return $entity; -+ } -+} -diff --git a/app/code/Magento/Catalog/Model/Product/Attribute/Backend/TierPrice/UpdateHandler.php b/app/code/Magento/Catalog/Model/Product/Attribute/Backend/TierPrice/UpdateHandler.php -new file mode 100644 -index 00000000000..f1943bc1088 ---- /dev/null -+++ b/app/code/Magento/Catalog/Model/Product/Attribute/Backend/TierPrice/UpdateHandler.php -@@ -0,0 +1,270 @@ -+<?php -+/** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+declare(strict_types=1); -+ -+namespace Magento\Catalog\Model\Product\Attribute\Backend\TierPrice; -+ -+use Magento\Catalog\Api\Data\ProductInterface; -+use Magento\Framework\App\ObjectManager; -+use Magento\Framework\Locale\FormatInterface; -+use Magento\Store\Model\StoreManagerInterface; -+use Magento\Catalog\Api\ProductAttributeRepositoryInterface; -+use Magento\Customer\Api\GroupManagementInterface; -+use Magento\Framework\EntityManager\MetadataPool; -+use Magento\Catalog\Model\ResourceModel\Product\Attribute\Backend\Tierprice; -+ -+/** -+ * Process tier price data for handled existing product. -+ */ -+class UpdateHandler extends AbstractHandler -+{ -+ /** -+ * @var \Magento\Store\Model\StoreManagerInterface -+ */ -+ private $storeManager; -+ -+ /** -+ * @var \Magento\Catalog\Api\ProductAttributeRepositoryInterface -+ */ -+ private $attributeRepository; -+ -+ /** -+ * @var \Magento\Framework\EntityManager\MetadataPool -+ */ -+ private $metadataPoll; -+ -+ /** -+ * @var \Magento\Catalog\Model\ResourceModel\Product\Attribute\Backend\Tierprice -+ */ -+ private $tierPriceResource; -+ -+ /** -+ * @var FormatInterface -+ */ -+ private $localeFormat; -+ -+ /** -+ * @param \Magento\Store\Model\StoreManagerInterface $storeManager -+ * @param \Magento\Catalog\Api\ProductAttributeRepositoryInterface $attributeRepository -+ * @param \Magento\Customer\Api\GroupManagementInterface $groupManagement -+ * @param \Magento\Framework\EntityManager\MetadataPool $metadataPool -+ * @param \Magento\Catalog\Model\ResourceModel\Product\Attribute\Backend\Tierprice $tierPriceResource -+ * @param FormatInterface|null $localeFormat -+ */ -+ public function __construct( -+ StoreManagerInterface $storeManager, -+ ProductAttributeRepositoryInterface $attributeRepository, -+ GroupManagementInterface $groupManagement, -+ MetadataPool $metadataPool, -+ Tierprice $tierPriceResource, -+ FormatInterface $localeFormat = null -+ ) { -+ parent::__construct($groupManagement); -+ -+ $this->storeManager = $storeManager; -+ $this->attributeRepository = $attributeRepository; -+ $this->metadataPoll = $metadataPool; -+ $this->tierPriceResource = $tierPriceResource; -+ $this->localeFormat = $localeFormat ?: ObjectManager::getInstance()->get(FormatInterface::class); -+ } -+ -+ /** -+ * Perform action on relation/extension attribute. -+ * -+ * @param \Magento\Catalog\Api\Data\ProductInterface|object $entity -+ * @param array $arguments -+ * @return \Magento\Catalog\Api\Data\ProductInterface|object -+ * @throws \Magento\Framework\Exception\InputException -+ * @SuppressWarnings(PHPMD.UnusedFormalParameter) -+ */ -+ public function execute($entity, $arguments = []) -+ { -+ $attribute = $this->attributeRepository->get('tier_price'); -+ $priceRows = $entity->getData($attribute->getName()); -+ if (null !== $priceRows) { -+ if (!is_array($priceRows)) { -+ throw new \Magento\Framework\Exception\InputException( -+ __('Tier prices data should be array, but actually other type is received') -+ ); -+ } -+ $websiteId = (int)$this->storeManager->getStore($entity->getStoreId())->getWebsiteId(); -+ $isGlobal = $attribute->isScopeGlobal() || $websiteId === 0; -+ $identifierField = $this->metadataPoll->getMetadata(ProductInterface::class)->getLinkField(); -+ $productId = (int)$entity->getData($identifierField); -+ -+ // prepare original data to compare -+ $origPrices = []; -+ $originalId = $entity->getOrigData($identifierField); -+ if (empty($originalId) || $entity->getData($identifierField) == $originalId) { -+ $origPrices = $entity->getOrigData($attribute->getName()); -+ } -+ -+ $old = $this->prepareOldTierPriceToCompare($origPrices); -+ // prepare data for save -+ $new = $this->prepareNewDataForSave($priceRows, $isGlobal); -+ -+ $delete = array_diff_key($old, $new); -+ $insert = array_diff_key($new, $old); -+ $update = array_intersect_key($new, $old); -+ -+ $isAttributeChanged = $this->deleteValues($productId, $delete); -+ $isAttributeChanged |= $this->insertValues($productId, $insert); -+ $isAttributeChanged |= $this->updateValues($update, $old); -+ -+ if ($isAttributeChanged) { -+ $valueChangedKey = $attribute->getName() . '_changed'; -+ $entity->setData($valueChangedKey, 1); -+ } -+ } -+ -+ return $entity; -+ } -+ -+ /** -+ * Update existing tier prices for processed product -+ * -+ * @param array $valuesToUpdate -+ * @param array $oldValues -+ * @return bool -+ */ -+ private function updateValues(array $valuesToUpdate, array $oldValues): bool -+ { -+ $isChanged = false; -+ foreach ($valuesToUpdate as $key => $value) { -+ if ((!empty($value['value']) -+ && (float)$oldValues[$key]['price'] !== $this->localeFormat->getNumber($value['value']) -+ ) || $this->getPercentage($oldValues[$key]) !== $this->getPercentage($value) -+ ) { -+ $price = new \Magento\Framework\DataObject( -+ [ -+ 'value_id' => $oldValues[$key]['price_id'], -+ 'value' => $value['value'], -+ 'percentage_value' => $this->getPercentage($value) -+ ] -+ ); -+ $this->tierPriceResource->savePriceData($price); -+ $isChanged = true; -+ } -+ } -+ -+ return $isChanged; -+ } -+ -+ /** -+ * Insert new tier prices for processed product -+ * -+ * @param int $productId -+ * @param array $valuesToInsert -+ * @return bool -+ */ -+ private function insertValues(int $productId, array $valuesToInsert): bool -+ { -+ $isChanged = false; -+ $identifierField = $this->metadataPoll->getMetadata(ProductInterface::class)->getLinkField(); -+ foreach ($valuesToInsert as $data) { -+ $price = new \Magento\Framework\DataObject($data); -+ $price->setData( -+ $identifierField, -+ $productId -+ ); -+ $this->tierPriceResource->savePriceData($price); -+ $isChanged = true; -+ } -+ -+ return $isChanged; -+ } -+ -+ /** -+ * Delete tier price values for processed product -+ * -+ * @param int $productId -+ * @param array $valuesToDelete -+ * @return bool -+ */ -+ private function deleteValues(int $productId, array $valuesToDelete): bool -+ { -+ $isChanged = false; -+ foreach ($valuesToDelete as $data) { -+ $this->tierPriceResource->deletePriceData($productId, null, $data['price_id']); -+ $isChanged = true; -+ } -+ -+ return $isChanged; -+ } -+ -+ /** -+ * Get generated price key based on price data -+ * -+ * @param array $priceData -+ * @return string -+ */ -+ private function getPriceKey(array $priceData): string -+ { -+ $qty = $this->parseQty($priceData['price_qty']); -+ $key = implode( -+ '-', -+ array_merge([$priceData['website_id'], $priceData['cust_group']], [$qty]) -+ ); -+ -+ return $key; -+ } -+ -+ /** -+ * Check by id is website global -+ * -+ * @param int $websiteId -+ * @return bool -+ */ -+ private function isWebsiteGlobal(int $websiteId): bool -+ { -+ return $websiteId === 0; -+ } -+ -+ /** -+ * Prepare old data to compare. -+ * -+ * @param array|null $origPrices -+ * @return array -+ */ -+ private function prepareOldTierPriceToCompare(?array $origPrices): array -+ { -+ $old = []; -+ if (is_array($origPrices)) { -+ foreach ($origPrices as $data) { -+ $key = $this->getPriceKey($data); -+ $old[$key] = $data; -+ } -+ } -+ -+ return $old; -+ } -+ -+ /** -+ * Prepare new data for save. -+ * -+ * @param array $priceRows -+ * @param bool $isGlobal -+ * @return array -+ * @throws \Magento\Framework\Exception\LocalizedException -+ */ -+ private function prepareNewDataForSave(array $priceRows, bool $isGlobal = true): array -+ { -+ $new = []; -+ $priceRows = array_filter($priceRows); -+ foreach ($priceRows as $data) { -+ if (empty($data['delete']) -+ && (!empty($data['price_qty']) -+ || isset($data['cust_group']) -+ || $isGlobal === $this->isWebsiteGlobal((int)$data['website_id'])) -+ ) { -+ $key = $this->getPriceKey($data); -+ $new[$key] = $this->prepareTierPrice($data); -+ } -+ } -+ -+ return $new; -+ } -+} -diff --git a/app/code/Magento/Catalog/Model/Product/Attribute/Backend/Tierprice.php b/app/code/Magento/Catalog/Model/Product/Attribute/Backend/Tierprice.php -index 92b9a2e4239..db967052cb7 100644 ---- a/app/code/Magento/Catalog/Model/Product/Attribute/Backend/Tierprice.php -+++ b/app/code/Magento/Catalog/Model/Product/Attribute/Backend/Tierprice.php -@@ -13,6 +13,9 @@ namespace Magento\Catalog\Model\Product\Attribute\Backend; - - use Magento\Catalog\Model\Attribute\ScopeOverriddenValue; - -+/** -+ * Backend model for Tierprice attribute -+ */ - class Tierprice extends \Magento\Catalog\Model\Product\Attribute\Backend\GroupPrice\AbstractGroupPrice - { - /** -@@ -159,6 +162,7 @@ class Tierprice extends \Magento\Catalog\Model\Product\Attribute\Backend\GroupPr - */ - protected function modifyPriceData($object, $data) - { -+ /** @var \Magento\Catalog\Model\Product $object */ - $data = parent::modifyPriceData($object, $data); - $price = $object->getPrice(); - foreach ($data as $key => $tierPrice) { -@@ -172,6 +176,10 @@ class Tierprice extends \Magento\Catalog\Model\Product\Attribute\Backend\GroupPr - } - - /** -+ * Update Price values in DB -+ * -+ * Updates price values in DB from array comparing to old values. Returns bool if updated -+ * - * @param array $valuesToUpdate - * @param array $oldValues - * @return boolean -diff --git a/app/code/Magento/Catalog/Model/Product/Attribute/DataProvider.php b/app/code/Magento/Catalog/Model/Product/Attribute/DataProvider.php -index 2bb10d3b31a..893000544a7 100644 ---- a/app/code/Magento/Catalog/Model/Product/Attribute/DataProvider.php -+++ b/app/code/Magento/Catalog/Model/Product/Attribute/DataProvider.php -@@ -113,27 +113,28 @@ class DataProvider extends \Magento\Ui\DataProvider\AbstractDataProvider - */ - private function customizeFrontendLabels($meta) - { -+ $labelConfigs = []; -+ - foreach ($this->storeRepository->getList() as $store) { - $storeId = $store->getId(); - - if (!$storeId) { - continue; - } -- -- $meta['manage-titles']['children'] = [ -- 'frontend_label[' . $storeId . ']' => $this->arrayManager->set( -- 'arguments/data/config', -- [], -- [ -- 'formElement' => Input::NAME, -- 'componentType' => Field::NAME, -- 'label' => $store->getName(), -- 'dataType' => Text::NAME, -- 'dataScope' => 'frontend_label[' . $storeId . ']' -- ] -- ), -- ]; -+ $labelConfigs['frontend_label[' . $storeId . ']'] = $this->arrayManager->set( -+ 'arguments/data/config', -+ [], -+ [ -+ 'formElement' => Input::NAME, -+ 'componentType' => Field::NAME, -+ 'label' => $store->getName(), -+ 'dataType' => Text::NAME, -+ 'dataScope' => 'frontend_label[' . $storeId . ']' -+ ] -+ ); - } -+ $meta['manage-titles']['children'] = $labelConfigs; -+ - return $meta; - } - -diff --git a/app/code/Magento/Catalog/Model/Product/Attribute/OptionManagement.php b/app/code/Magento/Catalog/Model/Product/Attribute/OptionManagement.php -index f2039a5002d..8b638feafaa 100644 ---- a/app/code/Magento/Catalog/Model/Product/Attribute/OptionManagement.php -+++ b/app/code/Magento/Catalog/Model/Product/Attribute/OptionManagement.php -@@ -8,6 +8,9 @@ namespace Magento\Catalog\Model\Product\Attribute; - - use Magento\Framework\Exception\InputException; - -+/** -+ * Option management model for product attribute. -+ */ - class OptionManagement implements \Magento\Catalog\Api\ProductAttributeOptionManagementInterface - { - /** -@@ -25,7 +28,7 @@ class OptionManagement implements \Magento\Catalog\Api\ProductAttributeOptionMan - } - - /** -- * {@inheritdoc} -+ * @inheritdoc - */ - public function getItems($attributeCode) - { -@@ -36,7 +39,7 @@ class OptionManagement implements \Magento\Catalog\Api\ProductAttributeOptionMan - } - - /** -- * {@inheritdoc} -+ * @inheritdoc - */ - public function add($attributeCode, $option) - { -@@ -47,7 +50,7 @@ class OptionManagement implements \Magento\Catalog\Api\ProductAttributeOptionMan - /** @var \Magento\Eav\Api\Data\AttributeOptionInterface $attributeOption */ - $attributeOption = $attributeOption->getLabel(); - }); -- if (in_array($option->getLabel(), $currentOptions)) { -+ if (in_array($option->getLabel(), $currentOptions, true)) { - return false; - } - } -@@ -59,7 +62,7 @@ class OptionManagement implements \Magento\Catalog\Api\ProductAttributeOptionMan - } - - /** -- * {@inheritdoc} -+ * @inheritdoc - */ - public function delete($attributeCode, $optionId) - { -diff --git a/app/code/Magento/Catalog/Model/Product/Attribute/Repository.php b/app/code/Magento/Catalog/Model/Product/Attribute/Repository.php -index 270a2f22967..99edfe5bc72 100644 ---- a/app/code/Magento/Catalog/Model/Product/Attribute/Repository.php -+++ b/app/code/Magento/Catalog/Model/Product/Attribute/Repository.php -@@ -11,6 +11,8 @@ use Magento\Framework\Exception\InputException; - use Magento\Framework\Exception\NoSuchEntityException; - - /** -+ * Product attribute repository -+ * - * @SuppressWarnings(PHPMD.CouplingBetweenObjects) - */ - class Repository implements \Magento\Catalog\Api\ProductAttributeRepositoryInterface -@@ -78,7 +80,7 @@ class Repository implements \Magento\Catalog\Api\ProductAttributeRepositoryInter - } - - /** -- * {@inheritdoc} -+ * @inheritdoc - */ - public function get($attributeCode) - { -@@ -89,7 +91,7 @@ class Repository implements \Magento\Catalog\Api\ProductAttributeRepositoryInter - } - - /** -- * {@inheritdoc} -+ * @inheritdoc - */ - public function getList(\Magento\Framework\Api\SearchCriteriaInterface $searchCriteria) - { -@@ -100,12 +102,17 @@ class Repository implements \Magento\Catalog\Api\ProductAttributeRepositoryInter - } - - /** -- * {@inheritdoc} -+ * @inheritdoc - * @SuppressWarnings(PHPMD.CyclomaticComplexity) - * @SuppressWarnings(PHPMD.NPathComplexity) - */ - public function save(\Magento\Catalog\Api\Data\ProductAttributeInterface $attribute) - { -+ $attribute->setEntityTypeId( -+ $this->eavConfig -+ ->getEntityType(\Magento\Catalog\Api\Data\ProductAttributeInterface::ENTITY_TYPE_CODE) -+ ->getId() -+ ); - if ($attribute->getAttributeId()) { - $existingModel = $this->get($attribute->getAttributeCode()); - -@@ -119,16 +126,7 @@ class Repository implements \Magento\Catalog\Api\ProductAttributeRepositoryInter - $attribute->setIsUserDefined($existingModel->getIsUserDefined()); - $attribute->setFrontendInput($existingModel->getFrontendInput()); - -- if (is_array($attribute->getFrontendLabels())) { -- $defaultFrontendLabel = $attribute->getDefaultFrontendLabel(); -- $frontendLabel[0] = !empty($defaultFrontendLabel) -- ? $defaultFrontendLabel -- : $existingModel->getDefaultFrontendLabel(); -- foreach ($attribute->getFrontendLabels() as $item) { -- $frontendLabel[$item->getStoreId()] = $item->getLabel(); -- } -- $attribute->setDefaultFrontendLabel($frontendLabel); -- } -+ $this->updateDefaultFrontendLabel($attribute, $existingModel); - } else { - $attribute->setAttributeId(null); - -@@ -136,22 +134,10 @@ class Repository implements \Magento\Catalog\Api\ProductAttributeRepositoryInter - throw InputException::requiredField('frontend_label'); - } - -- $frontendLabels = []; -- if ($attribute->getDefaultFrontendLabel()) { -- $frontendLabels[0] = $attribute->getDefaultFrontendLabel(); -- } -- if ($attribute->getFrontendLabels() && is_array($attribute->getFrontendLabels())) { -- foreach ($attribute->getFrontendLabels() as $label) { -- $frontendLabels[$label->getStoreId()] = $label->getLabel(); -- } -- if (!isset($frontendLabels[0]) || !$frontendLabels[0]) { -- throw InputException::invalidFieldValue('frontend_label', null); -- } -+ $frontendLabel = $this->updateDefaultFrontendLabel($attribute, null); - -- $attribute->setDefaultFrontendLabel($frontendLabels); -- } - $attribute->setAttributeCode( -- $attribute->getAttributeCode() ?: $this->generateCode($frontendLabels[0]) -+ $attribute->getAttributeCode() ?: $this->generateCode($frontendLabel) - ); - $this->validateCode($attribute->getAttributeCode()); - $this->validateFrontendInput($attribute->getFrontendInput()); -@@ -165,11 +151,6 @@ class Repository implements \Magento\Catalog\Api\ProductAttributeRepositoryInter - $attribute->setBackendModel( - $this->productHelper->getAttributeBackendModelByInputType($attribute->getFrontendInput()) - ); -- $attribute->setEntityTypeId( -- $this->eavConfig -- ->getEntityType(\Magento\Catalog\Api\Data\ProductAttributeInterface::ENTITY_TYPE_CODE) -- ->getId() -- ); - $attribute->setIsUserDefined(1); - } - if (!empty($attribute->getData(AttributeInterface::OPTIONS))) { -@@ -201,7 +182,7 @@ class Repository implements \Magento\Catalog\Api\ProductAttributeRepositoryInter - } - - /** -- * {@inheritdoc} -+ * @inheritdoc - */ - public function delete(\Magento\Catalog\Api\Data\ProductAttributeInterface $attribute) - { -@@ -210,7 +191,7 @@ class Repository implements \Magento\Catalog\Api\ProductAttributeRepositoryInter - } - - /** -- * {@inheritdoc} -+ * @inheritdoc - */ - public function deleteById($attributeCode) - { -@@ -221,7 +202,7 @@ class Repository implements \Magento\Catalog\Api\ProductAttributeRepositoryInter - } - - /** -- * {@inheritdoc} -+ * @inheritdoc - * @SuppressWarnings(PHPMD.UnusedFormalParameter) - */ - public function getCustomAttributesMetadata($dataObjectClassName = null) -@@ -275,4 +256,52 @@ class Repository implements \Magento\Catalog\Api\ProductAttributeRepositoryInter - throw InputException::invalidFieldValue('frontend_input', $frontendInput); - } - } -+ -+ /** -+ * This method sets default frontend value using given default frontend value or frontend value from admin store -+ * if default frontend value is not presented. -+ * If both default frontend label and admin store frontend label are not given it throws exception -+ * for attribute creation process or sets existing attribute value for attribute update action. -+ * -+ * @param \Magento\Catalog\Api\Data\ProductAttributeInterface $attribute -+ * @param \Magento\Catalog\Api\Data\ProductAttributeInterface|null $existingModel -+ * @return string|null -+ * @throws InputException -+ */ -+ private function updateDefaultFrontendLabel($attribute, $existingModel) -+ { -+ $frontendLabel = $attribute->getDefaultFrontendLabel(); -+ if (empty($frontendLabel)) { -+ $frontendLabel = $this->extractAdminStoreFrontendLabel($attribute); -+ if (empty($frontendLabel)) { -+ if ($existingModel) { -+ $frontendLabel = $existingModel->getDefaultFrontendLabel(); -+ } else { -+ throw InputException::invalidFieldValue('frontend_label', null); -+ } -+ } -+ $attribute->setDefaultFrontendLabel($frontendLabel); -+ } -+ return $frontendLabel; -+ } -+ -+ /** -+ * This method extracts frontend label from FrontendLabel object for admin store. -+ * -+ * @param \Magento\Catalog\Api\Data\ProductAttributeInterface $attribute -+ * @return string|null -+ */ -+ private function extractAdminStoreFrontendLabel($attribute) -+ { -+ $frontendLabel = []; -+ $frontendLabels = $attribute->getFrontendLabels(); -+ if (isset($frontendLabels[0]) -+ && $frontendLabels[0] instanceof \Magento\Eav\Api\Data\AttributeFrontendLabelInterface -+ ) { -+ foreach ($attribute->getFrontendLabels() as $label) { -+ $frontendLabel[$label->getStoreId()] = $label->getLabel(); -+ } -+ } -+ return $frontendLabel[0] ?? null; -+ } - } -diff --git a/app/code/Magento/Catalog/Model/Product/Attribute/Source/Layout.php b/app/code/Magento/Catalog/Model/Product/Attribute/Source/Layout.php -index 63b1444d1db..dbc7535dccf 100644 ---- a/app/code/Magento/Catalog/Model/Product/Attribute/Source/Layout.php -+++ b/app/code/Magento/Catalog/Model/Product/Attribute/Source/Layout.php -@@ -17,6 +17,12 @@ class Layout extends \Magento\Eav\Model\Entity\Attribute\Source\AbstractSource - */ - protected $pageLayoutBuilder; - -+ /** -+ * @inheritdoc -+ * @deprecated since the cache is now handled by \Magento\Theme\Model\PageLayout\Config\Builder::$configFiles -+ */ -+ protected $_options = null; -+ - /** - * @param \Magento\Framework\View\Model\PageLayout\Config\BuilderInterface $pageLayoutBuilder - */ -@@ -26,14 +32,14 @@ class Layout extends \Magento\Eav\Model\Entity\Attribute\Source\AbstractSource - } - - /** -- * @return array -+ * @inheritdoc - */ - public function getAllOptions() - { -- if (!$this->_options) { -- $this->_options = $this->pageLayoutBuilder->getPageLayoutsConfig()->toOptionArray(); -- array_unshift($this->_options, ['value' => '', 'label' => __('No layout updates')]); -- } -- return $this->_options; -+ $options = $this->pageLayoutBuilder->getPageLayoutsConfig()->toOptionArray(); -+ array_unshift($options, ['value' => '', 'label' => __('No layout updates')]); -+ $this->_options = $options; -+ -+ return $options; - } - } -diff --git a/app/code/Magento/Catalog/Model/Product/Compare/Item.php b/app/code/Magento/Catalog/Model/Product/Compare/Item.php -index 3b7e47a46a0..fd07380bebd 100644 ---- a/app/code/Magento/Catalog/Model/Product/Compare/Item.php -+++ b/app/code/Magento/Catalog/Model/Product/Compare/Item.php -@@ -158,8 +158,8 @@ class Item extends \Magento\Framework\Model\AbstractModel implements \Magento\Fr - { - if ($product instanceof Product) { - $this->setProductId($product->getId()); -- } elseif (intval($product)) { -- $this->setProductId(intval($product)); -+ } elseif ((int) $product) { -+ $this->setProductId((int) $product); - } - - return $this; -diff --git a/app/code/Magento/Catalog/Model/Product/Copier.php b/app/code/Magento/Catalog/Model/Product/Copier.php -index e94104ae473..44ebdf0f1f2 100644 ---- a/app/code/Magento/Catalog/Model/Product/Copier.php -+++ b/app/code/Magento/Catalog/Model/Product/Copier.php -@@ -1,14 +1,20 @@ - <?php - /** -- * Catalog product copier. Creates product duplicate -- * - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ - namespace Magento\Catalog\Model\Product; - - use Magento\Catalog\Api\Data\ProductInterface; -+use Magento\Catalog\Model\Product; - -+/** -+ * Catalog product copier. -+ * -+ * Creates product duplicate. -+ * -+ * @SuppressWarnings(PHPMD.CouplingBetweenObjects) -+ */ - class Copier - { - /** -@@ -49,7 +55,7 @@ class Copier - * @param \Magento\Catalog\Model\Product $product - * @return \Magento\Catalog\Model\Product - */ -- public function copy(\Magento\Catalog\Model\Product $product) -+ public function copy(Product $product) - { - $product->getWebsiteIds(); - $product->getCategoryIds(); -@@ -70,21 +76,9 @@ class Copier - $duplicate->setUpdatedAt(null); - $duplicate->setId(null); - $duplicate->setStoreId(\Magento\Store\Model\Store::DEFAULT_STORE_ID); -- - $this->copyConstructor->build($product, $duplicate); -- $isDuplicateSaved = false; -- do { -- $urlKey = $duplicate->getUrlKey(); -- $urlKey = preg_match('/(.*)-(\d+)$/', $urlKey, $matches) -- ? $matches[1] . '-' . ($matches[2] + 1) -- : $urlKey . '-1'; -- $duplicate->setUrlKey($urlKey); -- try { -- $duplicate->save(); -- $isDuplicateSaved = true; -- } catch (\Magento\Framework\Exception\AlreadyExistsException $e) { -- } -- } while (!$isDuplicateSaved); -+ $this->setDefaultUrl($product, $duplicate); -+ $this->setStoresUrl($product, $duplicate); - $this->getOptionRepository()->duplicate($product, $duplicate); - $product->getResource()->duplicate( - $product->getData($metadata->getLinkField()), -@@ -94,6 +88,83 @@ class Copier - } - - /** -+ * Set default URL. -+ * -+ * @param Product $product -+ * @param Product $duplicate -+ * @return void -+ */ -+ private function setDefaultUrl(Product $product, Product $duplicate) : void -+ { -+ $duplicate->setStoreId(\Magento\Store\Model\Store::DEFAULT_STORE_ID); -+ $resource = $product->getResource(); -+ $attribute = $resource->getAttribute('url_key'); -+ $productId = $product->getId(); -+ $urlKey = $resource->getAttributeRawValue($productId, 'url_key', \Magento\Store\Model\Store::DEFAULT_STORE_ID); -+ do { -+ $urlKey = $this->modifyUrl($urlKey); -+ $duplicate->setUrlKey($urlKey); -+ } while (!$attribute->getEntity()->checkAttributeUniqueValue($attribute, $duplicate)); -+ $duplicate->setData('url_path', null); -+ $duplicate->save(); -+ } -+ -+ /** -+ * Set URL for each store. -+ * -+ * @param Product $product -+ * @param Product $duplicate -+ * @return void -+ */ -+ private function setStoresUrl(Product $product, Product $duplicate) : void -+ { -+ $storeIds = $duplicate->getStoreIds(); -+ $productId = $product->getId(); -+ $productResource = $product->getResource(); -+ $defaultUrlKey = $productResource->getAttributeRawValue( -+ $productId, -+ 'url_key', -+ \Magento\Store\Model\Store::DEFAULT_STORE_ID -+ ); -+ $duplicate->setData('save_rewrites_history', false); -+ foreach ($storeIds as $storeId) { -+ $isDuplicateSaved = false; -+ $duplicate->setStoreId($storeId); -+ $urlKey = $productResource->getAttributeRawValue($productId, 'url_key', $storeId); -+ if ($urlKey === $defaultUrlKey) { -+ continue; -+ } -+ do { -+ $urlKey = $this->modifyUrl($urlKey); -+ $duplicate->setUrlKey($urlKey); -+ $duplicate->setData('url_path', null); -+ try { -+ $duplicate->save(); -+ $isDuplicateSaved = true; -+ // phpcs:ignore Magento2.CodeAnalysis.EmptyBlock -+ } catch (\Magento\Framework\Exception\AlreadyExistsException $e) { -+ } -+ } while (!$isDuplicateSaved); -+ } -+ $duplicate->setStoreId(\Magento\Store\Model\Store::DEFAULT_STORE_ID); -+ } -+ -+ /** -+ * Modify URL key. -+ * -+ * @param string $urlKey -+ * @return string -+ */ -+ private function modifyUrl(string $urlKey) : string -+ { -+ return preg_match('/(.*)-(\d+)$/', $urlKey, $matches) -+ ? $matches[1] . '-' . ($matches[2] + 1) -+ : $urlKey . '-1'; -+ } -+ -+ /** -+ * Returns product option repository. -+ * - * @return Option\Repository - * @deprecated 101.0.0 - */ -@@ -107,6 +178,8 @@ class Copier - } - - /** -+ * Returns metadata pool. -+ * - * @return \Magento\Framework\EntityManager\MetadataPool - * @deprecated 101.0.0 - */ -diff --git a/app/code/Magento/Catalog/Model/Product/Gallery/CreateHandler.php b/app/code/Magento/Catalog/Model/Product/Gallery/CreateHandler.php -index bd28b65bb79..e06e85e90a2 100644 ---- a/app/code/Magento/Catalog/Model/Product/Gallery/CreateHandler.php -+++ b/app/code/Magento/Catalog/Model/Product/Gallery/CreateHandler.php -@@ -102,6 +102,8 @@ class CreateHandler implements ExtensionInterface - } - - /** -+ * Execute create handler -+ * - * @param object $product - * @param array $arguments - * @return object -@@ -167,23 +169,19 @@ class CreateHandler implements ExtensionInterface - if (empty($attrData) && empty($clearImages) && empty($newImages) && empty($existImages)) { - continue; - } -- if (in_array($attrData, $clearImages)) { -- $product->setData($mediaAttrCode, 'no_selection'); -- } -- -- if (in_array($attrData, array_keys($newImages))) { -- $product->setData($mediaAttrCode, $newImages[$attrData]['new_file']); -- $product->setData($mediaAttrCode . '_label', $newImages[$attrData]['label']); -- } -- -- if (in_array($attrData, array_keys($existImages)) && isset($existImages[$attrData]['label'])) { -- $product->setData($mediaAttrCode . '_label', $existImages[$attrData]['label']); -- } -- if (!empty($product->getData($mediaAttrCode))) { -- $product->addAttributeUpdate( -+ $this->processMediaAttribute( -+ $product, -+ $mediaAttrCode, -+ $clearImages, -+ $newImages -+ ); -+ if (in_array($mediaAttrCode, ['image', 'small_image', 'thumbnail'])) { -+ $this->processMediaAttributeLabel( -+ $product, - $mediaAttrCode, -- $product->getData($mediaAttrCode), -- $product->getStoreId() -+ $clearImages, -+ $newImages, -+ $existImages - ); - } - } -@@ -208,6 +206,8 @@ class CreateHandler implements ExtensionInterface - } - - /** -+ * Returns media gallery attribute instance -+ * - * @return \Magento\Catalog\Api\Data\ProductAttributeInterface - * @since 101.0.0 - */ -@@ -223,17 +223,22 @@ class CreateHandler implements ExtensionInterface - } - - /** -+ * Process delete images -+ * - * @param \Magento\Catalog\Model\Product $product - * @param array $images - * @return void - * @SuppressWarnings(PHPMD.UnusedFormalParameter) - * @since 101.0.0 -+ * phpcs:disable Magento2.CodeAnalysis.EmptyBlock - */ - protected function processDeletedImages($product, array &$images) - { - } - - /** -+ * Process images -+ * - * @param \Magento\Catalog\Model\Product $product - * @param array $images - * @return void -@@ -296,6 +301,8 @@ class CreateHandler implements ExtensionInterface - } - - /** -+ * Duplicate attribute -+ * - * @param \Magento\Catalog\Model\Product $product - * @return $this - * @since 101.0.0 -@@ -312,7 +319,7 @@ class CreateHandler implements ExtensionInterface - - $this->resourceModel->duplicate( - $this->getAttribute()->getAttributeId(), -- isset($mediaGalleryData['duplicate']) ? $mediaGalleryData['duplicate'] : [], -+ $mediaGalleryData['duplicate'] ?? [], - $product->getOriginalLinkId(), - $product->getData($this->metadata->getLinkField()) - ); -@@ -364,6 +371,8 @@ class CreateHandler implements ExtensionInterface - } - - /** -+ * Returns file name according to tmp name -+ * - * @param string $file - * @return string - * @since 101.0.0 -@@ -392,6 +401,7 @@ class CreateHandler implements ExtensionInterface - $destinationFile = $forTmp - ? $this->mediaDirectory->getAbsolutePath($this->mediaConfig->getTmpMediaPath($file)) - : $this->mediaDirectory->getAbsolutePath($this->mediaConfig->getMediaPath($file)); -+ // phpcs:disable Magento2.Functions.DiscouragedFunction - $destFile = dirname($file) . '/' . FileUploader::getNewFileName($destinationFile); - } - -@@ -412,6 +422,7 @@ class CreateHandler implements ExtensionInterface - $destinationFile = $this->getUniqueFileName($file); - - if (!$this->mediaDirectory->isFile($this->mediaConfig->getMediaPath($file))) { -+ // phpcs:ignore Magento2.Exceptions.DirectThrow - throw new \Exception(); - } - -@@ -429,6 +440,7 @@ class CreateHandler implements ExtensionInterface - } - - return str_replace('\\', '/', $destinationFile); -+ // phpcs:ignore Magento2.Exceptions.ThrowCatch - } catch (\Exception $e) { - $file = $this->mediaConfig->getMediaPath($file); - throw new \Magento\Framework\Exception\LocalizedException( -@@ -449,4 +461,81 @@ class CreateHandler implements ExtensionInterface - } - return $this->mediaAttributeCodes; - } -+ -+ /** -+ * Process media attribute -+ * -+ * @param \Magento\Catalog\Model\Product $product -+ * @param string $mediaAttrCode -+ * @param array $clearImages -+ * @param array $newImages -+ */ -+ private function processMediaAttribute( -+ \Magento\Catalog\Model\Product $product, -+ $mediaAttrCode, -+ array $clearImages, -+ array $newImages -+ ) { -+ $attrData = $product->getData($mediaAttrCode); -+ if (in_array($attrData, $clearImages)) { -+ $product->setData($mediaAttrCode, 'no_selection'); -+ } -+ -+ if (in_array($attrData, array_keys($newImages))) { -+ $product->setData($mediaAttrCode, $newImages[$attrData]['new_file']); -+ } -+ if (!empty($product->getData($mediaAttrCode))) { -+ $product->addAttributeUpdate( -+ $mediaAttrCode, -+ $product->getData($mediaAttrCode), -+ $product->getStoreId() -+ ); -+ } -+ } -+ -+ /** -+ * Process media attribute label -+ * -+ * @param \Magento\Catalog\Model\Product $product -+ * @param string $mediaAttrCode -+ * @param array $clearImages -+ * @param array $newImages -+ * @param array $existImages -+ */ -+ private function processMediaAttributeLabel( -+ \Magento\Catalog\Model\Product $product, -+ $mediaAttrCode, -+ array $clearImages, -+ array $newImages, -+ array $existImages -+ ) { -+ $resetLabel = false; -+ $attrData = $product->getData($mediaAttrCode); -+ if (in_array($attrData, $clearImages)) { -+ $product->setData($mediaAttrCode . '_label', null); -+ $resetLabel = true; -+ } -+ -+ if (in_array($attrData, array_keys($newImages))) { -+ $product->setData($mediaAttrCode . '_label', $newImages[$attrData]['label']); -+ } -+ -+ if (in_array($attrData, array_keys($existImages)) && isset($existImages[$attrData]['label'])) { -+ $product->setData($mediaAttrCode . '_label', $existImages[$attrData]['label']); -+ } -+ -+ if ($attrData === 'no_selection' && !empty($product->getData($mediaAttrCode . '_label'))) { -+ $product->setData($mediaAttrCode . '_label', null); -+ $resetLabel = true; -+ } -+ if (!empty($product->getData($mediaAttrCode . '_label')) -+ || $resetLabel === true -+ ) { -+ $product->addAttributeUpdate( -+ $mediaAttrCode . '_label', -+ $product->getData($mediaAttrCode . '_label'), -+ $product->getStoreId() -+ ); -+ } -+ } - } -diff --git a/app/code/Magento/Catalog/Model/Product/Gallery/GalleryManagement.php b/app/code/Magento/Catalog/Model/Product/Gallery/GalleryManagement.php -index 4d274a071d0..c993e51c8bc 100644 ---- a/app/code/Magento/Catalog/Model/Product/Gallery/GalleryManagement.php -+++ b/app/code/Magento/Catalog/Model/Product/Gallery/GalleryManagement.php -@@ -1,6 +1,5 @@ - <?php - /** -- * - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -@@ -8,13 +7,16 @@ - namespace Magento\Catalog\Model\Product\Gallery; - - use Magento\Catalog\Api\Data\ProductAttributeMediaGalleryEntryInterface; --use Magento\Catalog\Api\Data\ProductInterface as Product; - use Magento\Framework\Exception\InputException; - use Magento\Framework\Exception\NoSuchEntityException; - use Magento\Framework\Exception\StateException; - use Magento\Framework\Api\ImageContentValidatorInterface; - - /** -+ * Class GalleryManagement -+ * -+ * Provides implementation of api interface ProductAttributeMediaGalleryManagementInterface -+ * - * @SuppressWarnings(PHPMD.CouplingBetweenObjects) - */ - class GalleryManagement implements \Magento\Catalog\Api\ProductAttributeMediaGalleryManagementInterface -@@ -44,7 +46,7 @@ class GalleryManagement implements \Magento\Catalog\Api\ProductAttributeMediaGal - } - - /** -- * {@inheritdoc} -+ * @inheritdoc - */ - public function create($sku, ProductAttributeMediaGalleryEntryInterface $entry) - { -@@ -54,7 +56,7 @@ class GalleryManagement implements \Magento\Catalog\Api\ProductAttributeMediaGal - if (!$this->contentValidator->isValid($entryContent)) { - throw new InputException(__('The image content is invalid. Verify the content and try again.')); - } -- $product = $this->productRepository->get($sku); -+ $product = $this->productRepository->get($sku, true); - - $existingMediaGalleryEntries = $product->getMediaGalleryEntries(); - $existingEntryIds = []; -@@ -69,8 +71,10 @@ class GalleryManagement implements \Magento\Catalog\Api\ProductAttributeMediaGal - $product->setMediaGalleryEntries($existingMediaGalleryEntries); - try { - $product = $this->productRepository->save($product); -+ // phpcs:ignore Magento2.Exceptions.ThrowCatch - } catch (InputException $inputException) { - throw $inputException; -+ // phpcs:ignore Magento2.Exceptions.ThrowCatch - } catch (\Exception $e) { - throw new StateException(__("The product can't be saved.")); - } -@@ -84,11 +88,11 @@ class GalleryManagement implements \Magento\Catalog\Api\ProductAttributeMediaGal - } - - /** -- * {@inheritdoc} -+ * @inheritdoc - */ - public function update($sku, ProductAttributeMediaGalleryEntryInterface $entry) - { -- $product = $this->productRepository->get($sku); -+ $product = $this->productRepository->get($sku, true); - $existingMediaGalleryEntries = $product->getMediaGalleryEntries(); - if ($existingMediaGalleryEntries == null) { - throw new NoSuchEntityException( -@@ -103,7 +107,10 @@ class GalleryManagement implements \Magento\Catalog\Api\ProductAttributeMediaGal - - if ($existingEntry->getId() == $entry->getId()) { - $found = true; -- if ($entry->getFile()) { -+ -+ $file = $entry->getContent(); -+ -+ if ($file && $file->getBase64EncodedData() || $entry->getFile()) { - $entry->setId(null); - } - $existingMediaGalleryEntries[$key] = $entry; -@@ -125,11 +132,11 @@ class GalleryManagement implements \Magento\Catalog\Api\ProductAttributeMediaGal - } - - /** -- * {@inheritdoc} -+ * @inheritdoc - */ - public function remove($sku, $entryId) - { -- $product = $this->productRepository->get($sku); -+ $product = $this->productRepository->get($sku, true); - $existingMediaGalleryEntries = $product->getMediaGalleryEntries(); - if ($existingMediaGalleryEntries == null) { - throw new NoSuchEntityException( -@@ -155,7 +162,7 @@ class GalleryManagement implements \Magento\Catalog\Api\ProductAttributeMediaGal - } - - /** -- * {@inheritdoc} -+ * @inheritdoc - */ - public function get($sku, $entryId) - { -@@ -176,7 +183,7 @@ class GalleryManagement implements \Magento\Catalog\Api\ProductAttributeMediaGal - } - - /** -- * {@inheritdoc} -+ * @inheritdoc - */ - public function getList($sku) - { -diff --git a/app/code/Magento/Catalog/Model/Product/Gallery/Processor.php b/app/code/Magento/Catalog/Model/Product/Gallery/Processor.php -index c6c7fbda7e9..e1b788bc394 100644 ---- a/app/code/Magento/Catalog/Model/Product/Gallery/Processor.php -+++ b/app/code/Magento/Catalog/Model/Product/Gallery/Processor.php -@@ -6,9 +6,10 @@ - - namespace Magento\Catalog\Model\Product\Gallery; - -+use Magento\Framework\Api\Data\ImageContentInterface; - use Magento\Framework\App\Filesystem\DirectoryList; - use Magento\Framework\Exception\LocalizedException; --use Magento\Framework\Filesystem\DriverInterface; -+use Magento\Framework\App\ObjectManager; - - /** - * Catalog product Media Gallery attribute processor. -@@ -56,28 +57,39 @@ class Processor - */ - protected $resourceModel; - -+ /** -+ * @var \Magento\Framework\File\Mime -+ */ -+ private $mime; -+ - /** - * @param \Magento\Catalog\Api\ProductAttributeRepositoryInterface $attributeRepository - * @param \Magento\MediaStorage\Helper\File\Storage\Database $fileStorageDb - * @param \Magento\Catalog\Model\Product\Media\Config $mediaConfig - * @param \Magento\Framework\Filesystem $filesystem - * @param \Magento\Catalog\Model\ResourceModel\Product\Gallery $resourceModel -+ * @param \Magento\Framework\File\Mime|null $mime -+ * @throws \Magento\Framework\Exception\FileSystemException - */ - public function __construct( - \Magento\Catalog\Api\ProductAttributeRepositoryInterface $attributeRepository, - \Magento\MediaStorage\Helper\File\Storage\Database $fileStorageDb, - \Magento\Catalog\Model\Product\Media\Config $mediaConfig, - \Magento\Framework\Filesystem $filesystem, -- \Magento\Catalog\Model\ResourceModel\Product\Gallery $resourceModel -+ \Magento\Catalog\Model\ResourceModel\Product\Gallery $resourceModel, -+ \Magento\Framework\File\Mime $mime = null - ) { - $this->attributeRepository = $attributeRepository; - $this->fileStorageDb = $fileStorageDb; - $this->mediaConfig = $mediaConfig; - $this->mediaDirectory = $filesystem->getDirectoryWrite(DirectoryList::MEDIA); - $this->resourceModel = $resourceModel; -+ $this->mime = $mime ?: ObjectManager::getInstance()->get(\Magento\Framework\File\Mime::class); - } - - /** -+ * Return media_gallery attribute -+ * - * @return \Magento\Catalog\Api\Data\ProductAttributeInterface - * @since 101.0.0 - */ -@@ -145,6 +157,7 @@ class Processor - throw new LocalizedException(__("The image doesn't exist.")); - } - -+ // phpcs:ignore Magento2.Functions.DiscouragedFunction - $pathinfo = pathinfo($file); - $imgExtensions = ['jpg', 'jpeg', 'gif', 'png']; - if (!isset($pathinfo['extension']) || !in_array(strtolower($pathinfo['extension']), $imgExtensions)) { -@@ -183,6 +196,13 @@ class Processor - $attrCode = $this->getAttribute()->getAttributeCode(); - $mediaGalleryData = $product->getData($attrCode); - $position = 0; -+ -+ $absoluteFilePath = $this->mediaDirectory->getAbsolutePath($destinationFile); -+ $imageMimeType = $this->mime->getMimeType($absoluteFilePath); -+ $imageContent = $this->mediaDirectory->readFile($absoluteFilePath); -+ $imageBase64 = base64_encode($imageContent); -+ $imageName = $pathinfo['filename']; -+ - if (!is_array($mediaGalleryData)) { - $mediaGalleryData = ['images' => []]; - } -@@ -197,9 +217,17 @@ class Processor - $mediaGalleryData['images'][] = [ - 'file' => $fileName, - 'position' => $position, -- 'media_type' => 'image', - 'label' => '', - 'disabled' => (int)$exclude, -+ 'media_type' => 'image', -+ 'types' => $mediaAttribute, -+ 'content' => [ -+ 'data' => [ -+ ImageContentInterface::NAME => $imageName, -+ ImageContentInterface::BASE64_ENCODED_DATA => $imageBase64, -+ ImageContentInterface::TYPE => $imageMimeType, -+ ] -+ ] - ]; - - $product->setData($attrCode, $mediaGalleryData); -@@ -358,7 +386,8 @@ class Processor - } - - /** -- * get media attribute codes -+ * Get media attribute codes -+ * - * @return array - * @since 101.0.0 - */ -@@ -368,6 +397,8 @@ class Processor - } - - /** -+ * Trim .tmp ending from filename -+ * - * @param string $file - * @return string - * @since 101.0.0 -@@ -422,6 +453,7 @@ class Processor - $destinationFile = $forTmp - ? $this->mediaDirectory->getAbsolutePath($this->mediaConfig->getTmpMediaPath($file)) - : $this->mediaDirectory->getAbsolutePath($this->mediaConfig->getMediaPath($file)); -+ // phpcs:ignore Magento2.Functions.DiscouragedFunction - $destFile = dirname($file) . '/' - . \Magento\MediaStorage\Model\File\Uploader::getNewFileName($destinationFile); - } -@@ -464,7 +496,7 @@ class Processor - /** - * Retrieve data for update attribute - * -- * @param \Magento\Catalog\Model\Product $object -+ * @param \Magento\Catalog\Model\Product $object - * @return array - * @since 101.0.0 - */ -@@ -489,7 +521,6 @@ class Processor - /** - * Attribute value is not to be saved in a conventional way, separate table is used to store the complex value - * -- * {@inheritdoc} - * @since 101.0.0 - */ - public function isScalar() -diff --git a/app/code/Magento/Catalog/Model/Product/Gallery/ReadHandler.php b/app/code/Magento/Catalog/Model/Product/Gallery/ReadHandler.php -index c785d08e64b..a3726207b30 100644 ---- a/app/code/Magento/Catalog/Model/Product/Gallery/ReadHandler.php -+++ b/app/code/Magento/Catalog/Model/Product/Gallery/ReadHandler.php -@@ -47,6 +47,8 @@ class ReadHandler implements ExtensionInterface - } - - /** -+ * Execute read handler for catalog product gallery -+ * - * @param Product $entity - * @param array $arguments - * @return object -@@ -55,9 +57,6 @@ class ReadHandler implements ExtensionInterface - */ - public function execute($entity, $arguments = []) - { -- $value = []; -- $value['images'] = []; -- - $mediaEntries = $this->resourceModel->loadProductGalleryByAttributeId( - $entity, - $this->getAttribute()->getAttributeId() -@@ -72,6 +71,8 @@ class ReadHandler implements ExtensionInterface - } - - /** -+ * Add media data to product -+ * - * @param Product $product - * @param array $mediaEntries - * @return void -@@ -79,40 +80,18 @@ class ReadHandler implements ExtensionInterface - */ - public function addMediaDataToProduct(Product $product, array $mediaEntries) - { -- $attrCode = $this->getAttribute()->getAttributeCode(); -- $value = []; -- $value['images'] = []; -- $value['values'] = []; -- -- foreach ($mediaEntries as $mediaEntry) { -- $mediaEntry = $this->substituteNullsWithDefaultValues($mediaEntry); -- $value['images'][$mediaEntry['value_id']] = $mediaEntry; -- } -- $product->setData($attrCode, $value); -- } -- -- /** -- * @param array $rawData -- * @return array -- */ -- private function substituteNullsWithDefaultValues(array $rawData) -- { -- $processedData = []; -- foreach ($rawData as $key => $rawValue) { -- if (null !== $rawValue) { -- $processedValue = $rawValue; -- } elseif (isset($rawData[$key . '_default'])) { -- $processedValue = $rawData[$key . '_default']; -- } else { -- $processedValue = null; -- } -- $processedData[$key] = $processedValue; -- } -- -- return $processedData; -+ $product->setData( -+ $this->getAttribute()->getAttributeCode(), -+ [ -+ 'images' => array_column($mediaEntries, null, 'value_id'), -+ 'values' => [] -+ ] -+ ); - } - - /** -+ * Get attribute -+ * - * @return \Magento\Catalog\Api\Data\ProductAttributeInterface - * @since 101.0.0 - */ -@@ -126,6 +105,8 @@ class ReadHandler implements ExtensionInterface - } - - /** -+ * Find default value -+ * - * @param string $key - * @param string[] &$image - * @return string -diff --git a/app/code/Magento/Catalog/Model/Product/Gallery/UpdateHandler.php b/app/code/Magento/Catalog/Model/Product/Gallery/UpdateHandler.php -index 8ad8dcb4812..189135776b6 100644 ---- a/app/code/Magento/Catalog/Model/Product/Gallery/UpdateHandler.php -+++ b/app/code/Magento/Catalog/Model/Product/Gallery/UpdateHandler.php -@@ -16,7 +16,8 @@ use Magento\Framework\EntityManager\Operation\ExtensionInterface; - class UpdateHandler extends \Magento\Catalog\Model\Product\Gallery\CreateHandler - { - /** -- * {@inheritdoc} -+ * @inheritdoc -+ * - * @since 101.0.0 - */ - protected function processDeletedImages($product, array &$images) -@@ -31,7 +32,7 @@ class UpdateHandler extends \Magento\Catalog\Model\Product\Gallery\CreateHandler - - foreach ($images as &$image) { - if (!empty($image['removed'])) { -- if (!empty($image['value_id']) && !isset($picturesInOtherStores[$image['file']])) { -+ if (!empty($image['value_id'])) { - if (preg_match('/\.\.(\\\|\/)/', $image['file'])) { - continue; - } -@@ -52,7 +53,8 @@ class UpdateHandler extends \Magento\Catalog\Model\Product\Gallery\CreateHandler - } - - /** -- * {@inheritdoc} -+ * @inheritdoc -+ * - * @since 101.0.0 - */ - protected function processNewImage($product, array &$image) -@@ -79,6 +81,8 @@ class UpdateHandler extends \Magento\Catalog\Model\Product\Gallery\CreateHandler - } - - /** -+ * Retrieve store ids from product. -+ * - * @param \Magento\Catalog\Model\Product $product - * @return array - * @since 101.0.0 -@@ -97,6 +101,8 @@ class UpdateHandler extends \Magento\Catalog\Model\Product\Gallery\CreateHandler - } - - /** -+ * Remove deleted images. -+ * - * @param array $files - * @return null - * @since 101.0.0 -diff --git a/app/code/Magento/Catalog/Model/Product/Image.php b/app/code/Magento/Catalog/Model/Product/Image.php -index 09ba68ddbe2..a0be36c5a32 100644 ---- a/app/code/Magento/Catalog/Model/Product/Image.php -+++ b/app/code/Magento/Catalog/Model/Product/Image.php -@@ -15,6 +15,8 @@ use Magento\Framework\Serialize\SerializerInterface; - use Magento\Catalog\Model\Product\Image\ParamsBuilder; - - /** -+ * Image operations -+ * - * @method string getFile() - * @method string getLabel() - * @method string getPosition() -@@ -24,6 +26,11 @@ use Magento\Catalog\Model\Product\Image\ParamsBuilder; - */ - class Image extends \Magento\Framework\Model\AbstractModel - { -+ /** -+ * Config path for the jpeg image quality value -+ */ -+ const XML_PATH_JPEG_QUALITY = 'system/upload_configuration/jpeg_quality'; -+ - /** - * @var int - */ -@@ -38,8 +45,9 @@ class Image extends \Magento\Framework\Model\AbstractModel - * Default quality value (for JPEG images only). - * - * @var int -+ * @deprecated use config setting with path self::XML_PATH_JPEG_QUALITY - */ -- protected $_quality = 80; -+ protected $_quality = null; - - /** - * @var bool -@@ -203,13 +211,13 @@ class Image extends \Magento\Framework\Model\AbstractModel - * @param \Magento\Framework\Image\Factory $imageFactory - * @param \Magento\Framework\View\Asset\Repository $assetRepo - * @param \Magento\Framework\View\FileSystem $viewFileSystem -+ * @param ImageFactory $viewAssetImageFactory -+ * @param PlaceholderFactory $viewAssetPlaceholderFactory - * @param \Magento\Framework\App\Config\ScopeConfigInterface $scopeConfig - * @param \Magento\Framework\Model\ResourceModel\AbstractResource $resource - * @param \Magento\Framework\Data\Collection\AbstractDb $resourceCollection - * @param array $data -- * @param ImageFactory|null $viewAssetImageFactory -- * @param PlaceholderFactory|null $viewAssetPlaceholderFactory -- * @param SerializerInterface|null $serializer -+ * @param SerializerInterface $serializer - * @param ParamsBuilder $paramsBuilder - * @SuppressWarnings(PHPMD.ExcessiveParameterList) - * @SuppressWarnings(PHPMD.UnusedLocalVariable) -@@ -249,6 +257,8 @@ class Image extends \Magento\Framework\Model\AbstractModel - } - - /** -+ * Set image width property -+ * - * @param int $width - * @return $this - */ -@@ -259,6 +269,8 @@ class Image extends \Magento\Framework\Model\AbstractModel - } - - /** -+ * Get image width property -+ * - * @return int - */ - public function getWidth() -@@ -267,6 +279,8 @@ class Image extends \Magento\Framework\Model\AbstractModel - } - - /** -+ * Set image height property -+ * - * @param int $height - * @return $this - */ -@@ -277,6 +291,8 @@ class Image extends \Magento\Framework\Model\AbstractModel - } - - /** -+ * Get image height property -+ * - * @return int - */ - public function getHeight() -@@ -289,6 +305,7 @@ class Image extends \Magento\Framework\Model\AbstractModel - * - * @param int $quality - * @return $this -+ * @deprecated use config setting with path self::XML_PATH_JPEG_QUALITY - */ - public function setQuality($quality) - { -@@ -303,10 +320,14 @@ class Image extends \Magento\Framework\Model\AbstractModel - */ - public function getQuality() - { -- return $this->_quality; -+ return $this->_quality === null -+ ? $this->_scopeConfig->getValue(self::XML_PATH_JPEG_QUALITY) -+ : $this->_quality; - } - - /** -+ * Set _keepAspectRatio property -+ * - * @param bool $keep - * @return $this - */ -@@ -317,6 +338,8 @@ class Image extends \Magento\Framework\Model\AbstractModel - } - - /** -+ * Set _keepFrame property -+ * - * @param bool $keep - * @return $this - */ -@@ -327,6 +350,8 @@ class Image extends \Magento\Framework\Model\AbstractModel - } - - /** -+ * Set _keepTransparency -+ * - * @param bool $keep - * @return $this - */ -@@ -337,6 +362,8 @@ class Image extends \Magento\Framework\Model\AbstractModel - } - - /** -+ * Set _constrainOnly -+ * - * @param bool $flag - * @return $this - */ -@@ -347,6 +374,8 @@ class Image extends \Magento\Framework\Model\AbstractModel - } - - /** -+ * Set background color -+ * - * @param int[] $rgbArray - * @return $this - */ -@@ -357,6 +386,8 @@ class Image extends \Magento\Framework\Model\AbstractModel - } - - /** -+ * Set size -+ * - * @param string $size - * @return $this - */ -@@ -411,6 +442,8 @@ class Image extends \Magento\Framework\Model\AbstractModel - } - - /** -+ * Get base filename -+ * - * @return string - */ - public function getBaseFile() -@@ -419,6 +452,8 @@ class Image extends \Magento\Framework\Model\AbstractModel - } - - /** -+ * Get new file -+ * - * @deprecated 101.1.0 - * @return bool|string - */ -@@ -438,6 +473,8 @@ class Image extends \Magento\Framework\Model\AbstractModel - } - - /** -+ * Set image processor -+ * - * @param MagentoImage $processor - * @return $this - */ -@@ -448,6 +485,8 @@ class Image extends \Magento\Framework\Model\AbstractModel - } - - /** -+ * Get image processor -+ * - * @return MagentoImage - */ - public function getImageProcessor() -@@ -461,11 +500,13 @@ class Image extends \Magento\Framework\Model\AbstractModel - $this->_processor->keepTransparency($this->_keepTransparency); - $this->_processor->constrainOnly($this->_constrainOnly); - $this->_processor->backgroundColor($this->_backgroundColor); -- $this->_processor->quality($this->_quality); -+ $this->_processor->quality($this->getQuality()); - return $this->_processor; - } - - /** -+ * Resize image -+ * - * @see \Magento\Framework\Image\Adapter\AbstractAdapter - * @return $this - */ -@@ -479,12 +520,14 @@ class Image extends \Magento\Framework\Model\AbstractModel - } - - /** -+ * Rotate image -+ * - * @param int $angle - * @return $this - */ - public function rotate($angle) - { -- $angle = intval($angle); -+ $angle = (int) $angle; - $this->getImageProcessor()->rotate($angle); - return $this; - } -@@ -505,7 +548,8 @@ class Image extends \Magento\Framework\Model\AbstractModel - - /** - * Add watermark to image -- * size param in format 100x200 -+ * -+ * Size param in format 100x200 - * - * @param string $file - * @param string $position -@@ -564,6 +608,8 @@ class Image extends \Magento\Framework\Model\AbstractModel - } - - /** -+ * Save file -+ * - * @return $this - */ - public function saveFile() -@@ -578,6 +624,8 @@ class Image extends \Magento\Framework\Model\AbstractModel - } - - /** -+ * Get url -+ * - * @return string - */ - public function getUrl() -@@ -586,6 +634,8 @@ class Image extends \Magento\Framework\Model\AbstractModel - } - - /** -+ * Set destination subdir -+ * - * @param string $dir - * @return $this - */ -@@ -596,6 +646,8 @@ class Image extends \Magento\Framework\Model\AbstractModel - } - - /** -+ * Get destination subdir -+ * - * @return string - */ - public function getDestinationSubdir() -@@ -604,6 +656,8 @@ class Image extends \Magento\Framework\Model\AbstractModel - } - - /** -+ * Check is image cached -+ * - * @return bool - */ - public function isCached() -@@ -636,7 +690,8 @@ class Image extends \Magento\Framework\Model\AbstractModel - - /** - * Get relative watermark file path -- * or false if file not found -+ * -+ * Return false if file not found - * - * @return string | bool - */ -@@ -771,7 +826,10 @@ class Image extends \Magento\Framework\Model\AbstractModel - } - - /** -+ * Clear cache -+ * - * @return void -+ * @throws \Magento\Framework\Exception\FileSystemException - */ - public function clearCache() - { -@@ -784,6 +842,7 @@ class Image extends \Magento\Framework\Model\AbstractModel - - /** - * First check this file on FS -+ * - * If it doesn't exist - try to download it from DB - * - * @param string $filename -@@ -802,6 +861,7 @@ class Image extends \Magento\Framework\Model\AbstractModel - - /** - * Return resized product image information -+ * - * @return array - * @throws NotLoadInfoImageException - */ -@@ -843,7 +903,7 @@ class Image extends \Magento\Framework\Model\AbstractModel - 'transparency' => $this->_keepTransparency, - 'background' => $this->_backgroundColor, - 'angle' => $this->_angle, -- 'quality' => $this->_quality -+ 'quality' => $this->getQuality() - ] - ); - } -diff --git a/app/code/Magento/Catalog/Model/Product/Image/ParamsBuilder.php b/app/code/Magento/Catalog/Model/Product/Image/ParamsBuilder.php -index dd8d352fece..4a55714a27e 100644 ---- a/app/code/Magento/Catalog/Model/Product/Image/ParamsBuilder.php -+++ b/app/code/Magento/Catalog/Model/Product/Image/ParamsBuilder.php -@@ -10,17 +10,13 @@ namespace Magento\Catalog\Model\Product\Image; - use Magento\Framework\App\Config\ScopeConfigInterface; - use Magento\Framework\View\ConfigInterface; - use Magento\Store\Model\ScopeInterface; -+use Magento\Catalog\Model\Product\Image; - - /** - * Builds parameters array used to build Image Asset - */ - class ParamsBuilder - { -- /** -- * @var int -- */ -- private $defaultQuality = 80; -- - /** - * @var array - */ -@@ -69,6 +65,8 @@ class ParamsBuilder - } - - /** -+ * Build image params -+ * - * @param array $imageArguments - * @return array - * @SuppressWarnings(PHPMD.NPathComplexity) -@@ -89,6 +87,8 @@ class ParamsBuilder - } - - /** -+ * Overwrite default values -+ * - * @param array $imageArguments - * @return array - */ -@@ -100,11 +100,12 @@ class ParamsBuilder - $transparency = $imageArguments['transparency'] ?? $this->defaultKeepTransparency; - $background = $imageArguments['background'] ?? $this->defaultBackground; - $angle = $imageArguments['angle'] ?? $this->defaultAngle; -+ $quality = (int) $this->scopeConfig->getValue(Image::XML_PATH_JPEG_QUALITY); - - return [ - 'background' => (array) $background, - 'angle' => $angle, -- 'quality' => $this->defaultQuality, -+ 'quality' => $quality, - 'keep_aspect_ratio' => (bool) $aspectRatio, - 'keep_frame' => (bool) $frame, - 'keep_transparency' => (bool) $transparency, -@@ -113,6 +114,8 @@ class ParamsBuilder - } - - /** -+ * Get watermark -+ * - * @param string $type - * @return array - */ -@@ -153,13 +156,12 @@ class ParamsBuilder - - /** - * Get frame from product_image_white_borders -+ * - * @return bool - */ - private function hasDefaultFrame(): bool - { -- return (bool) $this->viewConfig->getViewConfig()->getVarValue( -- 'Magento_Catalog', -- 'product_image_white_borders' -- ); -+ return (bool) $this->viewConfig->getViewConfig(['area' => \Magento\Framework\App\Area::AREA_FRONTEND]) -+ ->getVarValue('Magento_Catalog', 'product_image_white_borders'); - } - } -diff --git a/app/code/Magento/Catalog/Model/Product/Media/Config.php b/app/code/Magento/Catalog/Model/Product/Media/Config.php -index 72936d31739..33af93db13b 100644 ---- a/app/code/Magento/Catalog/Model/Product/Media/Config.php -+++ b/app/code/Magento/Catalog/Model/Product/Media/Config.php -@@ -10,11 +10,9 @@ use Magento\Eav\Model\Entity\Attribute; - use Magento\Store\Model\StoreManagerInterface; - - /** -- * Catalog product media config -+ * Catalog product media config. - * - * @api -- * -- * @author Magento Core Team <core@magentocommerce.com> - * @since 100.0.2 - */ - class Config implements ConfigInterface -@@ -31,6 +29,11 @@ class Config implements ConfigInterface - */ - private $attributeHelper; - -+ /** -+ * @var string[] -+ */ -+ private $mediaAttributeCodes; -+ - /** - * @param StoreManagerInterface $storeManager - */ -@@ -40,8 +43,7 @@ class Config implements ConfigInterface - } - - /** -- * Filesystem directory path of product images -- * relatively to media folder -+ * Get filesystem directory path for product images relative to the media directory. - * - * @return string - */ -@@ -51,8 +53,7 @@ class Config implements ConfigInterface - } - - /** -- * Web-based directory path of product images -- * relatively to media folder -+ * Get web-based directory path for product images relative to the media directory. - * - * @return string - */ -@@ -62,7 +63,7 @@ class Config implements ConfigInterface - } - - /** -- * @return string -+ * @inheritdoc - */ - public function getBaseMediaPath() - { -@@ -70,7 +71,7 @@ class Config implements ConfigInterface - } - - /** -- * @return string -+ * @inheritdoc - */ - public function getBaseMediaUrl() - { -@@ -79,8 +80,7 @@ class Config implements ConfigInterface - } - - /** -- * Filesystem directory path of temporary product images -- * relatively to media folder -+ * Filesystem directory path of temporary product images relative to the media directory. - * - * @return string - */ -@@ -90,6 +90,8 @@ class Config implements ConfigInterface - } - - /** -+ * Get temporary base media URL. -+ * - * @return string - */ - public function getBaseTmpMediaUrl() -@@ -100,8 +102,7 @@ class Config implements ConfigInterface - } - - /** -- * @param string $file -- * @return string -+ * @inheritdoc - */ - public function getMediaUrl($file) - { -@@ -109,8 +110,7 @@ class Config implements ConfigInterface - } - - /** -- * @param string $file -- * @return string -+ * @inheritdoc - */ - public function getMediaPath($file) - { -@@ -118,6 +118,8 @@ class Config implements ConfigInterface - } - - /** -+ * Get temporary media URL. -+ * - * @param string $file - * @return string - */ -@@ -127,8 +129,7 @@ class Config implements ConfigInterface - } - - /** -- * Part of URL of temporary product images -- * relatively to media folder -+ * Part of URL of temporary product images relative to the media directory. - * - * @param string $file - * @return string -@@ -139,7 +140,7 @@ class Config implements ConfigInterface - } - - /** -- * Part of URL of product images relatively to media folder -+ * Part of URL of product images relatively to media folder. - * - * @param string $file - * @return string -@@ -150,6 +151,8 @@ class Config implements ConfigInterface - } - - /** -+ * Get path to the temporary media. -+ * - * @param string $file - * @return string - */ -@@ -159,6 +162,8 @@ class Config implements ConfigInterface - } - - /** -+ * Process file path. -+ * - * @param string $file - * @return string - */ -@@ -168,15 +173,23 @@ class Config implements ConfigInterface - } - - /** -+ * Get codes of media attribute. -+ * - * @return array - * @since 100.0.4 - */ - public function getMediaAttributeCodes() - { -- return $this->getAttributeHelper()->getAttributeCodesByFrontendType('media_image'); -+ if (!isset($this->mediaAttributeCodes)) { -+ // the in-memory object-level caching allows to prevent unnecessary calls to the DB -+ $this->mediaAttributeCodes = $this->getAttributeHelper()->getAttributeCodesByFrontendType('media_image'); -+ } -+ return $this->mediaAttributeCodes; - } - - /** -+ * Get attribute helper. -+ * - * @return Attribute - */ - private function getAttributeHelper() -diff --git a/app/code/Magento/Catalog/Model/Product/Option.php b/app/code/Magento/Catalog/Model/Product/Option.php -index acfc454883e..b4a4ec08d39 100644 ---- a/app/code/Magento/Catalog/Model/Product/Option.php -+++ b/app/code/Magento/Catalog/Model/Product/Option.php -@@ -323,7 +323,7 @@ class Option extends AbstractExtensibleModel implements ProductCustomOptionInter - self::OPTION_TYPE_TIME => self::OPTION_GROUP_DATE, - ]; - -- return isset($optionGroupsToTypes[$type]) ? $optionGroupsToTypes[$type] : ''; -+ return $optionGroupsToTypes[$type] ?? ''; - } - - /** -diff --git a/app/code/Magento/Catalog/Model/Product/Option/Repository.php b/app/code/Magento/Catalog/Model/Product/Option/Repository.php -index 9dc9695daff..bb4e247de32 100644 ---- a/app/code/Magento/Catalog/Model/Product/Option/Repository.php -+++ b/app/code/Magento/Catalog/Model/Product/Option/Repository.php -@@ -14,6 +14,8 @@ use Magento\Framework\EntityManager\HydratorPool; - use Magento\Framework\App\ObjectManager; - - /** -+ * Product custom options repository -+ * - * @SuppressWarnings(PHPMD.CouplingBetweenObjects) - */ - class Repository implements \Magento\Catalog\Api\ProductCustomOptionRepositoryInterface -@@ -83,7 +85,7 @@ class Repository implements \Magento\Catalog\Api\ProductCustomOptionRepositoryIn - } - - /** -- * {@inheritdoc} -+ * @inheritdoc - */ - public function getList($sku) - { -@@ -92,7 +94,7 @@ class Repository implements \Magento\Catalog\Api\ProductCustomOptionRepositoryIn - } - - /** -- * {@inheritdoc} -+ * @inheritdoc - */ - public function getProductOptions(ProductInterface $product, $requiredOnly = false) - { -@@ -104,7 +106,7 @@ class Repository implements \Magento\Catalog\Api\ProductCustomOptionRepositoryIn - } - - /** -- * {@inheritdoc} -+ * @inheritdoc - */ - public function get($sku, $optionId) - { -@@ -117,7 +119,7 @@ class Repository implements \Magento\Catalog\Api\ProductCustomOptionRepositoryIn - } - - /** -- * {@inheritdoc} -+ * @inheritdoc - */ - public function delete(\Magento\Catalog\Api\Data\ProductCustomOptionInterface $entity) - { -@@ -126,7 +128,7 @@ class Repository implements \Magento\Catalog\Api\ProductCustomOptionRepositoryIn - } - - /** -- * {@inheritdoc} -+ * @inheritdoc - */ - public function duplicate( - \Magento\Catalog\Api\Data\ProductInterface $product, -@@ -142,7 +144,7 @@ class Repository implements \Magento\Catalog\Api\ProductCustomOptionRepositoryIn - } - - /** -- * {@inheritdoc} -+ * @inheritdoc - */ - public function save(\Magento\Catalog\Api\Data\ProductCustomOptionInterface $option) - { -@@ -184,7 +186,7 @@ class Repository implements \Magento\Catalog\Api\ProductCustomOptionRepositoryIn - } - - /** -- * {@inheritdoc} -+ * @inheritdoc - */ - public function deleteByIdentifier($sku, $optionId) - { -@@ -209,8 +211,8 @@ class Repository implements \Magento\Catalog\Api\ProductCustomOptionRepositoryIn - /** - * Mark original values for removal if they are absent among new values - * -- * @param $newValues array -- * @param $originalValues \Magento\Catalog\Model\Product\Option\Value[] -+ * @param array $newValues -+ * @param \Magento\Catalog\Model\Product\Option\Value[] $originalValues - * @return array - */ - protected function markRemovedValues($newValues, $originalValues) -@@ -234,6 +236,8 @@ class Repository implements \Magento\Catalog\Api\ProductCustomOptionRepositoryIn - } - - /** -+ * Get hydrator pool -+ * - * @return \Magento\Framework\EntityManager\HydratorPool - * @deprecated 101.0.0 - */ -diff --git a/app/code/Magento/Catalog/Model/Product/Option/SaveHandler.php b/app/code/Magento/Catalog/Model/Product/Option/SaveHandler.php -index c4a2d60414a..9cb6cda4d0a 100644 ---- a/app/code/Magento/Catalog/Model/Product/Option/SaveHandler.php -+++ b/app/code/Magento/Catalog/Model/Product/Option/SaveHandler.php -@@ -3,6 +3,8 @@ - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -+declare(strict_types=1); -+ - namespace Magento\Catalog\Model\Product\Option; - - use Magento\Catalog\Api\ProductCustomOptionRepositoryInterface as OptionRepository; -@@ -28,6 +30,8 @@ class SaveHandler implements ExtensionInterface - } - - /** -+ * Perform action on relation/extension attribute -+ * - * @param object $entity - * @param array $arguments - * @return \Magento\Catalog\Api\Data\ProductInterface|object -@@ -35,6 +39,10 @@ class SaveHandler implements ExtensionInterface - */ - public function execute($entity, $arguments = []) - { -+ if ($entity->getOptionsSaved()) { -+ return $entity; -+ } -+ - $options = $entity->getOptions(); - $optionIds = []; - -@@ -52,11 +60,26 @@ class SaveHandler implements ExtensionInterface - } - } - if ($options) { -- foreach ($options as $option) { -- $this->optionRepository->save($option); -- } -+ $this->processOptionsSaving($options, (bool)$entity->dataHasChangedFor('sku'), (string)$entity->getSku()); - } - - return $entity; - } -+ -+ /** -+ * Save custom options -+ * -+ * @param array $options -+ * @param bool $hasChangedSku -+ * @param string $newSku -+ */ -+ private function processOptionsSaving(array $options, bool $hasChangedSku, string $newSku) -+ { -+ foreach ($options as $option) { -+ if ($hasChangedSku && $option->hasData('product_sku')) { -+ $option->setProductSku($newSku); -+ } -+ $this->optionRepository->save($option); -+ } -+ } - } -diff --git a/app/code/Magento/Catalog/Model/Product/Option/Type/Date.php b/app/code/Magento/Catalog/Model/Product/Option/Type/Date.php -index 7517459da65..2b4739ebeb7 100644 ---- a/app/code/Magento/Catalog/Model/Product/Option/Type/Date.php -+++ b/app/code/Magento/Catalog/Model/Product/Option/Type/Date.php -@@ -12,6 +12,7 @@ use Magento\Catalog\Api\Data\ProductCustomOptionInterface; - * Catalog product option date type - * - * @author Magento Core Team <core@magentocommerce.com> -+ * @SuppressWarnings(PHPMD.CookieAndSessionMisuse) - */ - class Date extends \Magento\Catalog\Model\Product\Option\Type\DefaultType - { -@@ -102,11 +103,11 @@ class Date extends \Magento\Catalog\Model\Product\Option\Type\DefaultType - $this->setUserValue( - [ - 'date' => isset($value['date']) ? $value['date'] : '', -- 'year' => isset($value['year']) ? intval($value['year']) : 0, -- 'month' => isset($value['month']) ? intval($value['month']) : 0, -- 'day' => isset($value['day']) ? intval($value['day']) : 0, -- 'hour' => isset($value['hour']) ? intval($value['hour']) : 0, -- 'minute' => isset($value['minute']) ? intval($value['minute']) : 0, -+ 'year' => isset($value['year']) ? (int) $value['year'] : 0, -+ 'month' => isset($value['month']) ? (int) $value['month'] : 0, -+ 'day' => isset($value['day']) ? (int) $value['day'] : 0, -+ 'hour' => isset($value['hour']) ? (int) $value['hour'] : 0, -+ 'minute' => isset($value['minute']) ? (int) $value['minute'] : 0, - 'day_part' => isset($value['day_part']) ? $value['day_part'] : '', - 'date_internal' => isset($value['date_internal']) ? $value['date_internal'] : '', - ] -@@ -147,7 +148,6 @@ class Date extends \Magento\Catalog\Model\Product\Option\Type\DefaultType - public function prepareForCart() - { - if ($this->getIsValid() && $this->getUserValue() !== null) { -- $option = $this->getOption(); - $value = $this->getUserValue(); - - if (isset($value['date_internal']) && $value['date_internal'] != '') { -diff --git a/app/code/Magento/Catalog/Model/Product/Option/Type/DefaultType.php b/app/code/Magento/Catalog/Model/Product/Option/Type/DefaultType.php -index 2390a049fbe..c388be8b6f3 100644 ---- a/app/code/Magento/Catalog/Model/Product/Option/Type/DefaultType.php -+++ b/app/code/Magento/Catalog/Model/Product/Option/Type/DefaultType.php -@@ -279,7 +279,7 @@ class DefaultType extends \Magento\Framework\DataObject - */ - public function getCustomizedView($optionInfo) - { -- return isset($optionInfo['value']) ? $optionInfo['value'] : $optionInfo; -+ return $optionInfo['value'] ?? $optionInfo; - } - - /** -diff --git a/app/code/Magento/Catalog/Model/Product/Option/Type/File/ExistingValidate.php b/app/code/Magento/Catalog/Model/Product/Option/Type/File/ExistingValidate.php -new file mode 100644 -index 00000000000..c9afdf023b3 ---- /dev/null -+++ b/app/code/Magento/Catalog/Model/Product/Option/Type/File/ExistingValidate.php -@@ -0,0 +1,52 @@ -+<?php -+/** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+ -+declare(strict_types=1); -+ -+namespace Magento\Catalog\Model\Product\Option\Type\File; -+ -+/** -+ * Validator for existing (already saved) files. -+ */ -+class ExistingValidate extends \Zend_Validate -+{ -+ /** -+ * @inheritDoc -+ * -+ * @param string $value File's full path. -+ * @param string|null $originalName Original file's name (when uploaded). -+ */ -+ public function isValid($value, string $originalName = null) -+ { -+ $this->_messages = []; -+ $this->_errors = []; -+ -+ if (!is_string($value)) { -+ $this->_messages[] = __('Full file path is expected.')->render(); -+ return false; -+ } -+ -+ $result = true; -+ $fileInfo = null; -+ if ($originalName) { -+ $fileInfo = ['name' => $originalName]; -+ } -+ foreach ($this->_validators as $element) { -+ $validator = $element['instance']; -+ if ($validator->isValid($value, $fileInfo)) { -+ continue; -+ } -+ $result = false; -+ $messages = $validator->getMessages(); -+ $this->_messages = array_merge($this->_messages, $messages); -+ $this->_errors = array_merge($this->_errors, array_keys($messages)); -+ if ($element['breakChainOnFailure']) { -+ break; -+ } -+ } -+ return $result; -+ } -+} -diff --git a/app/code/Magento/Catalog/Model/Product/Option/Type/File/ValidateFactory.php b/app/code/Magento/Catalog/Model/Product/Option/Type/File/ValidateFactory.php -index 32c901afe8e..a7add0ad87b 100644 ---- a/app/code/Magento/Catalog/Model/Product/Option/Type/File/ValidateFactory.php -+++ b/app/code/Magento/Catalog/Model/Product/Option/Type/File/ValidateFactory.php -@@ -6,13 +6,18 @@ - - namespace Magento\Catalog\Model\Product\Option\Type\File; - -+/** -+ * Class ValidateFactory. Creates Validator with type "ExistingValidate" -+ */ - class ValidateFactory - { - /** -+ * Main factory method -+ * - * @return \Zend_Validate - */ - public function create() - { -- return new \Zend_Validate(); -+ return new ExistingValidate(); - } - } -diff --git a/app/code/Magento/Catalog/Model/Product/Option/Type/File/ValidatorFile.php b/app/code/Magento/Catalog/Model/Product/Option/Type/File/ValidatorFile.php -index d6a5cb1cbc2..fef4999a117 100644 ---- a/app/code/Magento/Catalog/Model/Product/Option/Type/File/ValidatorFile.php -+++ b/app/code/Magento/Catalog/Model/Product/Option/Type/File/ValidatorFile.php -@@ -10,8 +10,12 @@ use Magento\Catalog\Model\Product; - use Magento\Framework\App\Filesystem\DirectoryList; - use Magento\Catalog\Model\Product\Exception as ProductException; - use Magento\Framework\Exception\LocalizedException; -+use Magento\Framework\Math\Random; -+use Magento\Framework\App\ObjectManager; - - /** -+ * Validator class. Represents logic for validation file given from product option -+ * - * @SuppressWarnings(PHPMD.CouplingBetweenObjects) - */ - class ValidatorFile extends Validator -@@ -63,11 +67,19 @@ class ValidatorFile extends Validator - protected $isImageValidator; - - /** -+ * @var Random -+ */ -+ private $random; -+ -+ /** -+ * Constructor method -+ * - * @param \Magento\Framework\App\Config\ScopeConfigInterface $scopeConfig - * @param \Magento\Framework\Filesystem $filesystem - * @param \Magento\Framework\File\Size $fileSize - * @param \Magento\Framework\HTTP\Adapter\FileTransferFactory $httpFactory - * @param \Magento\Framework\Validator\File\IsImage $isImageValidator -+ * @param Random|null $random - * @throws \Magento\Framework\Exception\FileSystemException - */ - public function __construct( -@@ -75,16 +87,21 @@ class ValidatorFile extends Validator - \Magento\Framework\Filesystem $filesystem, - \Magento\Framework\File\Size $fileSize, - \Magento\Framework\HTTP\Adapter\FileTransferFactory $httpFactory, -- \Magento\Framework\Validator\File\IsImage $isImageValidator -+ \Magento\Framework\Validator\File\IsImage $isImageValidator, -+ Random $random = null - ) { - $this->mediaDirectory = $filesystem->getDirectoryWrite(DirectoryList::MEDIA); - $this->filesystem = $filesystem; - $this->httpFactory = $httpFactory; - $this->isImageValidator = $isImageValidator; -+ $this->random = $random -+ ?? ObjectManager::getInstance()->get(Random::class); - parent::__construct($scopeConfig, $filesystem, $fileSize); - } - - /** -+ * Setter method for the product -+ * - * @param Product $product - * @return $this - */ -@@ -95,6 +112,8 @@ class ValidatorFile extends Validator - } - - /** -+ * Validation method -+ * - * @param \Magento\Framework\DataObject $processingParams - * @param \Magento\Catalog\Model\Product\Option $option - * @return array -@@ -154,8 +173,6 @@ class ValidatorFile extends Validator - $userValue = []; - - if ($upload->isUploaded($file) && $upload->isValid($file)) { -- $extension = pathinfo(strtolower($fileInfo['name']), PATHINFO_EXTENSION); -- - $fileName = \Magento\MediaStorage\Model\File\Uploader::getCorrectFileName($fileInfo['name']); - $dispersion = \Magento\MediaStorage\Model\File\Uploader::getDispersionPath($fileName); - -@@ -163,7 +180,8 @@ class ValidatorFile extends Validator - - $tmpDirectory = $this->filesystem->getDirectoryRead(DirectoryList::SYS_TMP); - $fileHash = md5($tmpDirectory->readFile($tmpDirectory->getRelativePath($fileInfo['tmp_name']))); -- $filePath .= '/' . $fileHash . '.' . $extension; -+ $fileRandomName = $this->random->getRandomString(32); -+ $filePath .= '/' .$fileRandomName; - $fileFullPath = $this->mediaDirectory->getAbsolutePath($this->quotePath . $filePath); - - $upload->addFilter(new \Zend_Filter_File_Rename(['target' => $fileFullPath, 'overwrite' => true])); -@@ -243,6 +261,8 @@ class ValidatorFile extends Validator - } - - /** -+ * Validate contents length method -+ * - * @return bool - * @todo need correctly name - */ -diff --git a/app/code/Magento/Catalog/Model/Product/Option/Type/File/ValidatorInfo.php b/app/code/Magento/Catalog/Model/Product/Option/Type/File/ValidatorInfo.php -index 37e4c7b310a..100ad37273c 100644 ---- a/app/code/Magento/Catalog/Model/Product/Option/Type/File/ValidatorInfo.php -+++ b/app/code/Magento/Catalog/Model/Product/Option/Type/File/ValidatorInfo.php -@@ -6,6 +6,9 @@ - - namespace Magento\Catalog\Model\Product\Option\Type\File; - -+/** -+ * Validator for existing files. -+ */ - class ValidatorInfo extends Validator - { - /** -@@ -34,6 +37,8 @@ class ValidatorInfo extends Validator - protected $fileRelativePath; - - /** -+ * Construct method -+ * - * @param \Magento\Framework\App\Config\ScopeConfigInterface $scopeConfig - * @param \Magento\Framework\Filesystem $filesystem - * @param \Magento\Framework\File\Size $fileSize -@@ -53,6 +58,8 @@ class ValidatorInfo extends Validator - } - - /** -+ * Setter method for property "useQuotePath" -+ * - * @param mixed $useQuotePath - * @return $this - */ -@@ -63,6 +70,8 @@ class ValidatorInfo extends Validator - } - - /** -+ * Validate method for the option value depends on an option -+ * - * @param array $optionValue - * @param \Magento\Catalog\Model\Product\Option $option - * @return bool -@@ -90,7 +99,7 @@ class ValidatorInfo extends Validator - } - - $result = false; -- if ($validatorChain->isValid($this->fileFullPath)) { -+ if ($validatorChain->isValid($this->fileFullPath, $optionValue['title'])) { - $result = $this->rootDirectory->isReadable($this->fileRelativePath) - && isset($optionValue['secret_key']) - && $this->buildSecretKey($this->fileRelativePath) == $optionValue['secret_key']; -@@ -109,6 +118,8 @@ class ValidatorInfo extends Validator - } - - /** -+ * Method for creation secret key for the given file -+ * - * @param string $fileRelativePath - * @return string - */ -@@ -118,6 +129,8 @@ class ValidatorInfo extends Validator - } - - /** -+ * Calculates path for the file -+ * - * @param array $optionValue - * @return void - */ -diff --git a/app/code/Magento/Catalog/Model/Product/Option/Type/Select.php b/app/code/Magento/Catalog/Model/Product/Option/Type/Select.php -index 4a257a47810..31e178f0bd9 100644 ---- a/app/code/Magento/Catalog/Model/Product/Option/Type/Select.php -+++ b/app/code/Magento/Catalog/Model/Product/Option/Type/Select.php -@@ -10,6 +10,8 @@ use Magento\Framework\Exception\LocalizedException; - - /** - * Catalog product option select type -+ * -+ * @SuppressWarnings(PHPMD.CookieAndSessionMisuse) - */ - class Select extends \Magento\Catalog\Model\Product\Option\Type\DefaultType - { -@@ -30,23 +32,35 @@ class Select extends \Magento\Catalog\Model\Product\Option\Type\DefaultType - */ - protected $string; - -+ /** -+ * @var array -+ */ -+ private $singleSelectionTypes; -+ - /** - * @param \Magento\Checkout\Model\Session $checkoutSession - * @param \Magento\Framework\App\Config\ScopeConfigInterface $scopeConfig - * @param \Magento\Framework\Stdlib\StringUtils $string - * @param \Magento\Framework\Escaper $escaper - * @param array $data -+ * @param array $singleSelectionTypes - */ - public function __construct( - \Magento\Checkout\Model\Session $checkoutSession, - \Magento\Framework\App\Config\ScopeConfigInterface $scopeConfig, - \Magento\Framework\Stdlib\StringUtils $string, - \Magento\Framework\Escaper $escaper, -- array $data = [] -+ array $data = [], -+ array $singleSelectionTypes = [] - ) { - $this->string = $string; - $this->_escaper = $escaper; - parent::__construct($checkoutSession, $scopeConfig, $data); -+ -+ $this->singleSelectionTypes = $singleSelectionTypes ?: [ -+ 'drop_down' => \Magento\Catalog\Api\Data\ProductCustomOptionInterface::OPTION_TYPE_DROP_DOWN, -+ 'radio' => \Magento\Catalog\Api\Data\ProductCustomOptionInterface::OPTION_TYPE_RADIO, -+ ]; - } - - /** -@@ -71,7 +85,7 @@ class Select extends \Magento\Catalog\Model\Product\Option\Type\DefaultType - } - if (!$this->_isSingleSelection()) { - $valuesCollection = $option->getOptionValuesByOptionId($value, $this->getProduct()->getStoreId())->load(); -- $valueCount = is_array($value) ? count($value) : 1; -+ $valueCount = is_array($value) ? count($value) : 0; - if ($valuesCollection->count() != $valueCount) { - $this->setIsValid(false); - throw new LocalizedException( -@@ -310,10 +324,6 @@ class Select extends \Magento\Catalog\Model\Product\Option\Type\DefaultType - */ - protected function _isSingleSelection() - { -- $single = [ -- \Magento\Catalog\Api\Data\ProductCustomOptionInterface::OPTION_TYPE_DROP_DOWN, -- \Magento\Catalog\Api\Data\ProductCustomOptionInterface::OPTION_TYPE_RADIO, -- ]; -- return in_array($this->getOption()->getType(), $single); -+ return in_array($this->getOption()->getType(), $this->singleSelectionTypes, true); - } - } -diff --git a/app/code/Magento/Catalog/Model/Product/Option/Validator/DefaultValidator.php b/app/code/Magento/Catalog/Model/Product/Option/Validator/DefaultValidator.php -index ee508e30cc9..08455430cca 100644 ---- a/app/code/Magento/Catalog/Model/Product/Option/Validator/DefaultValidator.php -+++ b/app/code/Magento/Catalog/Model/Product/Option/Validator/DefaultValidator.php -@@ -9,6 +9,9 @@ namespace Magento\Catalog\Model\Product\Option\Validator; - use Magento\Catalog\Model\Product\Option; - use Zend_Validate_Exception; - -+/** -+ * Product option default validator -+ */ - class DefaultValidator extends \Magento\Framework\Validator\AbstractValidator - { - /** -@@ -25,13 +28,20 @@ class DefaultValidator extends \Magento\Framework\Validator\AbstractValidator - */ - protected $priceTypes; - -+ /** -+ * @var \Magento\Framework\Locale\FormatInterface -+ */ -+ private $localeFormat; -+ - /** - * @param \Magento\Catalog\Model\ProductOptions\ConfigInterface $productOptionConfig - * @param \Magento\Catalog\Model\Config\Source\Product\Options\Price $priceConfig -+ * @param \Magento\Framework\Locale\FormatInterface|null $localeFormat - */ - public function __construct( - \Magento\Catalog\Model\ProductOptions\ConfigInterface $productOptionConfig, -- \Magento\Catalog\Model\Config\Source\Product\Options\Price $priceConfig -+ \Magento\Catalog\Model\Config\Source\Product\Options\Price $priceConfig, -+ \Magento\Framework\Locale\FormatInterface $localeFormat = null - ) { - foreach ($productOptionConfig->getAll() as $option) { - foreach ($option['types'] as $type) { -@@ -42,6 +52,9 @@ class DefaultValidator extends \Magento\Framework\Validator\AbstractValidator - foreach ($priceConfig->toOptionArray() as $item) { - $this->priceTypes[] = $item['value']; - } -+ -+ $this->localeFormat = $localeFormat ?: \Magento\Framework\App\ObjectManager::getInstance() -+ ->get(\Magento\Framework\Locale\FormatInterface::class); - } - - /** -@@ -134,11 +147,11 @@ class DefaultValidator extends \Magento\Framework\Validator\AbstractValidator - */ - protected function validateOptionValue(Option $option) - { -- return $this->isInRange($option->getPriceType(), $this->priceTypes); -+ return $this->isInRange($option->getPriceType(), $this->priceTypes) && $this->isNumber($option->getPrice()); - } - - /** -- * Check whether value is empty -+ * Check whether the value is empty - * - * @param mixed $value - * @return bool -@@ -149,7 +162,7 @@ class DefaultValidator extends \Magento\Framework\Validator\AbstractValidator - } - - /** -- * Check whether value is in range -+ * Check whether the value is in range - * - * @param string $value - * @param array $range -@@ -161,13 +174,24 @@ class DefaultValidator extends \Magento\Framework\Validator\AbstractValidator - } - - /** -- * Check whether value is not negative -+ * Check whether the value is negative - * - * @param string $value - * @return bool - */ - protected function isNegative($value) - { -- return intval($value) < 0; -+ return $this->localeFormat->getNumber($value) < 0; -+ } -+ -+ /** -+ * Check whether the value is a number -+ * -+ * @param string $value -+ * @return bool -+ */ -+ public function isNumber($value) -+ { -+ return is_numeric($this->localeFormat->getNumber($value)); - } - } -diff --git a/app/code/Magento/Catalog/Model/Product/Option/Validator/Pool.php b/app/code/Magento/Catalog/Model/Product/Option/Validator/Pool.php -index 1e006542495..2256f031098 100644 ---- a/app/code/Magento/Catalog/Model/Product/Option/Validator/Pool.php -+++ b/app/code/Magento/Catalog/Model/Product/Option/Validator/Pool.php -@@ -29,6 +29,6 @@ class Pool - */ - public function get($type) - { -- return isset($this->validators[$type]) ? $this->validators[$type] : $this->validators['default']; -+ return $this->validators[$type] ?? $this->validators['default']; - } - } -diff --git a/app/code/Magento/Catalog/Model/Product/Option/Validator/Select.php b/app/code/Magento/Catalog/Model/Product/Option/Validator/Select.php -index 44756890b6e..209531f5998 100644 ---- a/app/code/Magento/Catalog/Model/Product/Option/Validator/Select.php -+++ b/app/code/Magento/Catalog/Model/Product/Option/Validator/Select.php -@@ -8,6 +8,9 @@ namespace Magento\Catalog\Model\Product\Option\Validator; - - use Magento\Catalog\Model\Product\Option; - -+/** -+ * Select validator class -+ */ - class Select extends DefaultValidator - { - /** -@@ -83,7 +86,7 @@ class Select extends DefaultValidator - if (!$priceType && !$price) { - return true; - } -- if (!$this->isInRange($priceType, $this->priceTypes)) { -+ if (!$this->isInRange($priceType, $this->priceTypes) || !$this->isNumber($price)) { - return false; - } - -diff --git a/app/code/Magento/Catalog/Model/Product/Price/TierPriceStorage.php b/app/code/Magento/Catalog/Model/Product/Price/TierPriceStorage.php -index 1bddd2d07cd..3ee064670a4 100644 ---- a/app/code/Magento/Catalog/Model/Product/Price/TierPriceStorage.php -+++ b/app/code/Magento/Catalog/Model/Product/Price/TierPriceStorage.php -@@ -97,7 +97,7 @@ class TierPriceStorage implements \Magento\Catalog\Api\TierPriceStorageInterface - } - - /** -- * {@inheritdoc} -+ * @inheritdoc - */ - public function get(array $skus) - { -@@ -107,7 +107,7 @@ class TierPriceStorage implements \Magento\Catalog\Api\TierPriceStorageInterface - } - - /** -- * {@inheritdoc} -+ * @inheritdoc - */ - public function update(array $prices) - { -@@ -128,7 +128,7 @@ class TierPriceStorage implements \Magento\Catalog\Api\TierPriceStorageInterface - } - - /** -- * {@inheritdoc} -+ * @inheritdoc - */ - public function replace(array $prices) - { -@@ -144,7 +144,7 @@ class TierPriceStorage implements \Magento\Catalog\Api\TierPriceStorageInterface - } - - /** -- * {@inheritdoc} -+ * @inheritdoc - */ - public function delete(array $prices) - { -@@ -171,16 +171,17 @@ class TierPriceStorage implements \Magento\Catalog\Api\TierPriceStorageInterface - $ids = $this->retrieveAffectedIds($skus); - $rawPrices = $this->tierPricePersistence->get($ids); - $prices = []; -- -- $linkField = $this->tierPricePersistence->getEntityLinkField(); -- $skuByIdLookup = $this->buildSkuByIdLookup($skus); -- foreach ($rawPrices as $rawPrice) { -- $sku = $skuByIdLookup[$rawPrice[$linkField]]; -- $price = $this->tierPriceFactory->create($rawPrice, $sku); -- if ($groupBySku) { -- $prices[$sku][] = $price; -- } else { -- $prices[] = $price; -+ if ($rawPrices) { -+ $linkField = $this->tierPricePersistence->getEntityLinkField(); -+ $skuByIdLookup = $this->buildSkuByIdLookup($skus); -+ foreach ($rawPrices as $rawPrice) { -+ $sku = $skuByIdLookup[$rawPrice[$linkField]]; -+ $price = $this->tierPriceFactory->create($rawPrice, $sku); -+ if ($groupBySku) { -+ $prices[$sku][] = $price; -+ } else { -+ $prices[] = $price; -+ } - } - } - -diff --git a/app/code/Magento/Catalog/Model/Product/PriceModifier.php b/app/code/Magento/Catalog/Model/Product/PriceModifier.php -index 48d53b46145..c4d5bdfedcd 100644 ---- a/app/code/Magento/Catalog/Model/Product/PriceModifier.php -+++ b/app/code/Magento/Catalog/Model/Product/PriceModifier.php -@@ -9,6 +9,9 @@ namespace Magento\Catalog\Model\Product; - use Magento\Framework\Exception\CouldNotSaveException; - use Magento\Framework\Exception\NoSuchEntityException; - -+/** -+ * Product form price modifier -+ */ - class PriceModifier - { - /** -@@ -26,6 +29,8 @@ class PriceModifier - } - - /** -+ * Remove tier price -+ * - * @param \Magento\Catalog\Model\Product $product - * @param int|string $customerGroupId - * @param int $qty -@@ -46,11 +51,11 @@ class PriceModifier - - foreach ($prices as $key => $tierPrice) { - if ($customerGroupId == 'all' && $tierPrice['price_qty'] == $qty -- && $tierPrice['all_groups'] == 1 && intval($tierPrice['website_id']) === intval($websiteId) -+ && $tierPrice['all_groups'] == 1 && (int) $tierPrice['website_id'] === (int) $websiteId - ) { - unset($prices[$key]); - } elseif ($tierPrice['price_qty'] == $qty && $tierPrice['cust_group'] == $customerGroupId -- && intval($tierPrice['website_id']) === intval($websiteId) -+ && (int) $tierPrice['website_id'] === (int) $websiteId - ) { - unset($prices[$key]); - } -diff --git a/app/code/Magento/Catalog/Model/Product/ProductFrontendAction/Synchronizer.php b/app/code/Magento/Catalog/Model/Product/ProductFrontendAction/Synchronizer.php -index 3ec8e968aa2..24775a791e5 100644 ---- a/app/code/Magento/Catalog/Model/Product/ProductFrontendAction/Synchronizer.php -+++ b/app/code/Magento/Catalog/Model/Product/ProductFrontendAction/Synchronizer.php -@@ -16,6 +16,8 @@ use Magento\Customer\Model\Visitor; - use Magento\Framework\EntityManager\EntityManager; - - /** -+ * A Product Widget Synchronizer. -+ * - * Service which allows to sync product widget information, such as product id with db. In order to reuse this info - * on different devices - */ -@@ -85,9 +87,10 @@ class Synchronizer - } - - /** -- * Find lifetime in configuration. Configuration is hold in Stores Configuration -- * Also this configuration is generated by: -- * @see \Magento\Catalog\Model\Widget\RecentlyViewedStorageConfiguration -+ * Finds lifetime in configuration. -+ * -+ * Configuration is hold in Stores Configuration. Also this configuration is generated by -+ * {@see Magento\Catalog\Model\Widget\RecentlyViewedStorageConfiguration} - * - * @param string $namespace - * @return int -@@ -108,6 +111,8 @@ class Synchronizer - } - - /** -+ * Filters actions. -+ * - * In order to avoid suspicious actions, we need to filter them in DESC order, and slice only items that - * can be persisted in database. - * -@@ -138,7 +143,9 @@ class Synchronizer - $productIds = []; - - foreach ($actions as $action) { -- $productIds[] = $action['product_id']; -+ if (isset($action['product_id'])) { -+ $productIds[] = $action['product_id']; -+ } - } - - return $productIds; -@@ -159,33 +166,37 @@ class Synchronizer - $customerId = $this->session->getCustomerId(); - $visitorId = $this->visitor->getId(); - $collection = $this->getActionsByType($typeId); -- $collection->addFieldToFilter('product_id', $this->getProductIdsByActions($productsData)); -- -- /** -- * Note that collection is also filtered by visitor id and customer id -- * This collection shouldn't be flushed when visitor has products and then login -- * It can remove only products for visitor, or only products for customer -- * -- * ['product_id' => 'added_at'] -- * @var ProductFrontendActionInterface $item -- */ -- foreach ($collection as $item) { -- $this->entityManager->delete($item); -- } -- -- foreach ($productsData as $productId => $productData) { -- /** @var ProductFrontendActionInterface $action */ -- $action = $this->productFrontendActionFactory->create([ -- 'data' => [ -- 'visitor_id' => $customerId ? null : $visitorId, -- 'customer_id' => $this->session->getCustomerId(), -- 'added_at' => $productData['added_at'], -- 'product_id' => $productId, -- 'type_id' => $typeId -- ] -- ]); -- -- $this->entityManager->save($action); -+ $productIds = $this->getProductIdsByActions($productsData); -+ -+ if ($productIds) { -+ $collection->addFieldToFilter('product_id', $productIds); -+ -+ /** -+ * Note that collection is also filtered by visitor id and customer id -+ * This collection shouldn't be flushed when visitor has products and then login -+ * It can remove only products for visitor, or only products for customer -+ * -+ * ['product_id' => 'added_at'] -+ * @var ProductFrontendActionInterface $item -+ */ -+ foreach ($collection as $item) { -+ $this->entityManager->delete($item); -+ } -+ -+ foreach ($productsData as $productId => $productData) { -+ /** @var ProductFrontendActionInterface $action */ -+ $action = $this->productFrontendActionFactory->create([ -+ 'data' => [ -+ 'visitor_id' => $customerId ? null : $visitorId, -+ 'customer_id' => $this->session->getCustomerId(), -+ 'added_at' => $productData['added_at'], -+ 'product_id' => $productId, -+ 'type_id' => $typeId -+ ] -+ ]); -+ -+ $this->entityManager->save($action); -+ } - } - } - -diff --git a/app/code/Magento/Catalog/Model/Product/ProductList/ToolbarMemorizer.php b/app/code/Magento/Catalog/Model/Product/ProductList/ToolbarMemorizer.php -new file mode 100644 -index 00000000000..9c1a781d594 ---- /dev/null -+++ b/app/code/Magento/Catalog/Model/Product/ProductList/ToolbarMemorizer.php -@@ -0,0 +1,179 @@ -+<?php -+/** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+ -+declare(strict_types=1); -+ -+namespace Magento\Catalog\Model\Product\ProductList; -+ -+use Magento\Catalog\Model\Session as CatalogSession; -+use Magento\Framework\App\Config\ScopeConfigInterface; -+ -+/** -+ * Class ToolbarMemorizer -+ * -+ * Responds for saving toolbar settings to catalog session -+ */ -+class ToolbarMemorizer -+{ -+ /** -+ * XML PATH to enable/disable saving toolbar parameters to session -+ */ -+ const XML_PATH_CATALOG_REMEMBER_PAGINATION = 'catalog/frontend/remember_pagination'; -+ -+ /** -+ * @var CatalogSession -+ */ -+ private $catalogSession; -+ -+ /** -+ * @var Toolbar -+ */ -+ private $toolbarModel; -+ -+ /** -+ * @var ScopeConfigInterface -+ */ -+ private $scopeConfig; -+ -+ /** -+ * @var string|bool -+ */ -+ private $order; -+ -+ /** -+ * @var string|bool -+ */ -+ private $direction; -+ -+ /** -+ * @var string|bool -+ */ -+ private $mode; -+ -+ /** -+ * @var string|bool -+ */ -+ private $limit; -+ -+ /** -+ * @var bool -+ */ -+ private $isMemorizingAllowed; -+ -+ /** -+ * @param Toolbar $toolbarModel -+ * @param CatalogSession $catalogSession -+ * @param ScopeConfigInterface $scopeConfig -+ */ -+ public function __construct( -+ Toolbar $toolbarModel, -+ CatalogSession $catalogSession, -+ ScopeConfigInterface $scopeConfig -+ ) { -+ $this->toolbarModel = $toolbarModel; -+ $this->catalogSession = $catalogSession; -+ $this->scopeConfig = $scopeConfig; -+ } -+ -+ /** -+ * Get sort order -+ * -+ * @return string|bool -+ */ -+ public function getOrder() -+ { -+ if ($this->order === null) { -+ $this->order = $this->toolbarModel->getOrder() ?? -+ ($this->isMemorizingAllowed() ? $this->catalogSession->getData(Toolbar::ORDER_PARAM_NAME) : null); -+ } -+ return $this->order; -+ } -+ -+ /** -+ * Get sort direction -+ * -+ * @return string|bool -+ */ -+ public function getDirection() -+ { -+ if ($this->direction === null) { -+ $this->direction = $this->toolbarModel->getDirection() ?? -+ ($this->isMemorizingAllowed() ? $this->catalogSession->getData(Toolbar::DIRECTION_PARAM_NAME) : null); -+ } -+ return $this->direction; -+ } -+ -+ /** -+ * Get sort mode -+ * -+ * @return string|bool -+ */ -+ public function getMode() -+ { -+ if ($this->mode === null) { -+ $this->mode = $this->toolbarModel->getMode() ?? -+ ($this->isMemorizingAllowed() ? $this->catalogSession->getData(Toolbar::MODE_PARAM_NAME) : null); -+ } -+ return $this->mode; -+ } -+ -+ /** -+ * Get products per page limit -+ * -+ * @return string|bool -+ */ -+ public function getLimit() -+ { -+ if ($this->limit === null) { -+ $this->limit = $this->toolbarModel->getLimit() ?? -+ ($this->isMemorizingAllowed() ? $this->catalogSession->getData(Toolbar::LIMIT_PARAM_NAME) : null); -+ } -+ return $this->limit; -+ } -+ -+ /** -+ * Method to save all catalog parameters in catalog session -+ * -+ * @return void -+ */ -+ public function memorizeParams() -+ { -+ if (!$this->catalogSession->getParamsMemorizeDisabled() && $this->isMemorizingAllowed()) { -+ $this->memorizeParam(Toolbar::ORDER_PARAM_NAME, $this->getOrder()) -+ ->memorizeParam(Toolbar::DIRECTION_PARAM_NAME, $this->getDirection()) -+ ->memorizeParam(Toolbar::MODE_PARAM_NAME, $this->getMode()) -+ ->memorizeParam(Toolbar::LIMIT_PARAM_NAME, $this->getLimit()); -+ } -+ } -+ -+ /** -+ * Check configuration for enabled/disabled toolbar memorizing -+ * -+ * @return bool -+ */ -+ public function isMemorizingAllowed() -+ { -+ if ($this->isMemorizingAllowed === null) { -+ $this->isMemorizingAllowed = $this->scopeConfig->isSetFlag(self::XML_PATH_CATALOG_REMEMBER_PAGINATION); -+ } -+ return $this->isMemorizingAllowed; -+ } -+ -+ /** -+ * Memorize parameter value for session -+ * -+ * @param string $param parameter name -+ * @param mixed $value parameter value -+ * @return $this -+ */ -+ private function memorizeParam($param, $value) -+ { -+ if ($value && $this->catalogSession->getData($param) != $value) { -+ $this->catalogSession->setData($param, $value); -+ } -+ return $this; -+ } -+} -diff --git a/app/code/Magento/Catalog/Model/Product/TierPriceManagement.php b/app/code/Magento/Catalog/Model/Product/TierPriceManagement.php -index 822959bfc85..f2da1e77027 100644 ---- a/app/code/Magento/Catalog/Model/Product/TierPriceManagement.php -+++ b/app/code/Magento/Catalog/Model/Product/TierPriceManagement.php -@@ -15,6 +15,8 @@ use Magento\Framework\Exception\InputException; - use Magento\Framework\Exception\TemporaryStateExceptionInterface; - - /** -+ * Product tier price management -+ * - * @SuppressWarnings(PHPMD.CouplingBetweenObjects) - */ - class TierPriceManagement implements \Magento\Catalog\Api\ProductTierPriceManagementInterface -@@ -82,7 +84,7 @@ class TierPriceManagement implements \Magento\Catalog\Api\ProductTierPriceManage - } - - /** -- * {@inheritdoc} -+ * @inheritdoc - * @SuppressWarnings(PHPMD.CyclomaticComplexity) - * @SuppressWarnings(PHPMD.NPathComplexity) - */ -@@ -148,7 +150,7 @@ class TierPriceManagement implements \Magento\Catalog\Api\ProductTierPriceManage - } - - /** -- * {@inheritdoc} -+ * @inheritdoc - */ - public function remove($sku, $customerGroupId, $qty) - { -@@ -163,7 +165,7 @@ class TierPriceManagement implements \Magento\Catalog\Api\ProductTierPriceManage - } - - /** -- * {@inheritdoc} -+ * @inheritdoc - */ - public function getList($sku, $customerGroupId) - { -@@ -181,7 +183,7 @@ class TierPriceManagement implements \Magento\Catalog\Api\ProductTierPriceManage - - $prices = []; - foreach ($product->getData('tier_price') as $price) { -- if ((is_numeric($customerGroupId) && intval($price['cust_group']) === intval($customerGroupId)) -+ if ((is_numeric($customerGroupId) && (int) $price['cust_group'] === (int) $customerGroupId) - || ($customerGroupId === 'all' && $price['all_groups']) - ) { - /** @var \Magento\Catalog\Api\Data\ProductTierPriceInterface $tierPrice */ -diff --git a/app/code/Magento/Catalog/Model/Product/Type.php b/app/code/Magento/Catalog/Model/Product/Type.php -index dc3971397ac..4c973be20de 100644 ---- a/app/code/Magento/Catalog/Model/Product/Type.php -+++ b/app/code/Magento/Catalog/Model/Product/Type.php -@@ -232,7 +232,7 @@ class Type implements OptionSourceInterface - public function getOptionText($optionId) - { - $options = $this->getOptionArray(); -- return isset($options[$optionId]) ? $options[$optionId] : null; -+ return $options[$optionId] ?? null; - } - - /** -@@ -285,7 +285,7 @@ class Type implements OptionSourceInterface - - $types = $this->getTypes(); - foreach ($types as $typeId => $typeInfo) { -- $priority = isset($typeInfo['index_priority']) ? abs(intval($typeInfo['index_priority'])) : 0; -+ $priority = isset($typeInfo['index_priority']) ? abs((int) $typeInfo['index_priority']) : 0; - if (!empty($typeInfo['composite'])) { - $compositePriority[$typeId] = $priority; - } else { -@@ -307,7 +307,7 @@ class Type implements OptionSourceInterface - } - - /** -- * {@inheritdoc} -+ * @inheritdoc - */ - public function toOptionArray() - { -diff --git a/app/code/Magento/Catalog/Model/Product/Type/AbstractType.php b/app/code/Magento/Catalog/Model/Product/Type/AbstractType.php -index 1b5cf37f6cb..e6804d9246f 100644 ---- a/app/code/Magento/Catalog/Model/Product/Type/AbstractType.php -+++ b/app/code/Magento/Catalog/Model/Product/Type/AbstractType.php -@@ -935,7 +935,7 @@ abstract class AbstractType - */ - public function prepareQuoteItemQty($qty, $product) - { -- return floatval($qty); -+ return (float)$qty; - } - - /** -diff --git a/app/code/Magento/Catalog/Model/Product/Type/FrontSpecialPrice.php b/app/code/Magento/Catalog/Model/Product/Type/FrontSpecialPrice.php -new file mode 100644 -index 00000000000..dabfdb74f01 ---- /dev/null -+++ b/app/code/Magento/Catalog/Model/Product/Type/FrontSpecialPrice.php -@@ -0,0 +1,133 @@ -+<?php -+/** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+declare(strict_types=1); -+ -+namespace Magento\Catalog\Model\Product\Type; -+ -+use Magento\Store\Model\Store; -+use Magento\Catalog\Model\ResourceModel\Product\Price\SpecialPrice; -+use Magento\Catalog\Api\Data\SpecialPriceInterface; -+use Magento\Store\Api\Data\WebsiteInterface; -+ -+/** -+ * Product special price model. -+ * -+ * @SuppressWarnings(PHPMD.CookieAndSessionMisuse) -+ * @SuppressWarnings(PHPMD.CouplingBetweenObjects) -+ * -+ * @deprecated -+ * @see \Magento\Catalog\Model\Product\Type\Price -+ */ -+class FrontSpecialPrice extends Price -+{ -+ /** -+ * @var SpecialPrice -+ */ -+ private $specialPrice; -+ -+ /** -+ * @param \Magento\CatalogRule\Model\ResourceModel\RuleFactory $ruleFactory -+ * @param \Magento\Store\Model\StoreManagerInterface $storeManager -+ * @param \Magento\Framework\Stdlib\DateTime\TimezoneInterface $localeDate -+ * @param \Magento\Customer\Model\Session $customerSession -+ * @param \Magento\Framework\Event\ManagerInterface $eventManager -+ * @param \Magento\Framework\Pricing\PriceCurrencyInterface $priceCurrency -+ * @param \Magento\Customer\Api\GroupManagementInterface $groupManagement -+ * @param \Magento\Catalog\Api\Data\ProductTierPriceInterfaceFactory $tierPriceFactory -+ * @param \Magento\Framework\App\Config\ScopeConfigInterface $config -+ * @param SpecialPrice $specialPrice -+ * @SuppressWarnings(PHPMD.ExcessiveParameterList) -+ */ -+ public function __construct( -+ \Magento\CatalogRule\Model\ResourceModel\RuleFactory $ruleFactory, -+ \Magento\Store\Model\StoreManagerInterface $storeManager, -+ \Magento\Framework\Stdlib\DateTime\TimezoneInterface $localeDate, -+ \Magento\Customer\Model\Session $customerSession, -+ \Magento\Framework\Event\ManagerInterface $eventManager, -+ \Magento\Framework\Pricing\PriceCurrencyInterface $priceCurrency, -+ \Magento\Customer\Api\GroupManagementInterface $groupManagement, -+ \Magento\Catalog\Api\Data\ProductTierPriceInterfaceFactory $tierPriceFactory, -+ \Magento\Framework\App\Config\ScopeConfigInterface $config, -+ SpecialPrice $specialPrice -+ ) { -+ $this->specialPrice = $specialPrice; -+ parent::__construct( -+ $ruleFactory, -+ $storeManager, -+ $localeDate, -+ $customerSession, -+ $eventManager, -+ $priceCurrency, -+ $groupManagement, -+ $tierPriceFactory, -+ $config -+ ); -+ } -+ -+ /** -+ * @inheritdoc -+ * -+ * @deprecated -+ */ -+ protected function _applySpecialPrice($product, $finalPrice) -+ { -+ if (!$product->getSpecialPrice()) { -+ return $finalPrice; -+ } -+ -+ $specialPrices = $this->getSpecialPrices($product); -+ $specialPrice = !(empty($specialPrices)) ? min($specialPrices) : $product->getSpecialPrice(); -+ -+ $specialPrice = $this->calculateSpecialPrice( -+ $finalPrice, -+ $specialPrice, -+ $product->getSpecialFromDate(), -+ $product->getSpecialToDate(), -+ WebsiteInterface::ADMIN_CODE -+ ); -+ $product->setData('special_price', $specialPrice); -+ -+ return $specialPrice; -+ } -+ -+ /** -+ * Get special prices. -+ * -+ * @param mixed $product -+ * @return array -+ */ -+ private function getSpecialPrices($product): array -+ { -+ $allSpecialPrices = $this->specialPrice->get([$product->getSku()]); -+ $specialPrices = []; -+ foreach ($allSpecialPrices as $price) { -+ if ($this->isSuitableSpecialPrice($product, $price)) { -+ $specialPrices[] = $price['value']; -+ } -+ } -+ -+ return $specialPrices; -+ } -+ -+ /** -+ * Price is suitable from default and current store + start and end date are equal. -+ * -+ * @param mixed $product -+ * @param array $price -+ * @return bool -+ */ -+ private function isSuitableSpecialPrice($product, array $price): bool -+ { -+ $priceStoreId = $price[Store::STORE_ID]; -+ if (($priceStoreId == Store::DEFAULT_STORE_ID || $product->getStoreId() == $priceStoreId) -+ && $price[SpecialPriceInterface::PRICE_FROM] == $product->getSpecialFromDate() -+ && $price[SpecialPriceInterface::PRICE_TO] == $product->getSpecialToDate()) { -+ return true; -+ } -+ -+ return false; -+ } -+} -diff --git a/app/code/Magento/Catalog/Model/Product/Type/Price.php b/app/code/Magento/Catalog/Model/Product/Type/Price.php -index f6caa299d66..b30624b79dd 100644 ---- a/app/code/Magento/Catalog/Model/Product/Type/Price.php -+++ b/app/code/Magento/Catalog/Model/Product/Type/Price.php -@@ -11,12 +11,14 @@ use Magento\Framework\Pricing\PriceCurrencyInterface; - use Magento\Store\Model\Store; - use Magento\Catalog\Api\Data\ProductTierPriceExtensionFactory; - use Magento\Framework\App\ObjectManager; -+use Magento\Store\Api\Data\WebsiteInterface; - - /** - * Product type price model - * - * @api - * @SuppressWarnings(PHPMD.CouplingBetweenObjects) -+ * @SuppressWarnings(PHPMD.CookieAndSessionMisuse) - * @since 100.0.2 - */ - class Price -@@ -184,6 +186,8 @@ class Price - } - - /** -+ * Retrieve final price for child product -+ * - * @param Product $product - * @param float $productQty - * @param Product $childProduct -@@ -428,6 +432,8 @@ class Price - } - - /** -+ * Retrieve customer group id from product -+ * - * @param Product $product - * @return int - */ -@@ -453,7 +459,7 @@ class Price - $product->getSpecialPrice(), - $product->getSpecialFromDate(), - $product->getSpecialToDate(), -- $product->getStore() -+ WebsiteInterface::ADMIN_CODE - ); - } - -@@ -601,7 +607,7 @@ class Price - $specialPrice, - $specialPriceFrom, - $specialPriceTo, -- $sId -+ WebsiteInterface::ADMIN_CODE - ); - - if ($rulePrice === false) { -diff --git a/app/code/Magento/Catalog/Model/Product/Url.php b/app/code/Magento/Catalog/Model/Product/Url.php -index c291dc33fed..2760b0f9fdd 100644 ---- a/app/code/Magento/Catalog/Model/Product/Url.php -+++ b/app/code/Magento/Catalog/Model/Product/Url.php -@@ -7,6 +7,7 @@ namespace Magento\Catalog\Model\Product; - - use Magento\UrlRewrite\Model\UrlFinderInterface; - use Magento\UrlRewrite\Service\V1\Data\UrlRewrite; -+use Magento\Framework\App\Config\ScopeConfigInterface; - - /** - * Product Url model -@@ -45,6 +46,11 @@ class Url extends \Magento\Framework\DataObject - */ - protected $urlFinder; - -+ /** -+ * @var \Magento\Framework\App\Config\ScopeConfigInterface -+ */ -+ private $scopeConfig; -+ - /** - * @param \Magento\Framework\UrlFactory $urlFactory - * @param \Magento\Store\Model\StoreManagerInterface $storeManager -@@ -52,6 +58,7 @@ class Url extends \Magento\Framework\DataObject - * @param \Magento\Framework\Session\SidResolverInterface $sidResolver - * @param UrlFinderInterface $urlFinder - * @param array $data -+ * @param ScopeConfigInterface|null $scopeConfig - */ - public function __construct( - \Magento\Framework\UrlFactory $urlFactory, -@@ -59,7 +66,8 @@ class Url extends \Magento\Framework\DataObject - \Magento\Framework\Filter\FilterManager $filter, - \Magento\Framework\Session\SidResolverInterface $sidResolver, - UrlFinderInterface $urlFinder, -- array $data = [] -+ array $data = [], -+ ScopeConfigInterface $scopeConfig = null - ) { - parent::__construct($data); - $this->urlFactory = $urlFactory; -@@ -67,16 +75,8 @@ class Url extends \Magento\Framework\DataObject - $this->filter = $filter; - $this->sidResolver = $sidResolver; - $this->urlFinder = $urlFinder; -- } -- -- /** -- * Retrieve URL Instance -- * -- * @return \Magento\Framework\UrlInterface -- */ -- private function getUrlInstance() -- { -- return $this->urlFactory->create(); -+ $this->scopeConfig = $scopeConfig ?: -+ \Magento\Framework\App\ObjectManager::getInstance()->get(ScopeConfigInterface::class); - } - - /** -@@ -157,10 +157,16 @@ class Url extends \Magento\Framework\DataObject - UrlRewrite::ENTITY_TYPE => \Magento\CatalogUrlRewrite\Model\ProductUrlRewriteGenerator::ENTITY_TYPE, - UrlRewrite::STORE_ID => $storeId, - ]; -- if ($categoryId) { -- $filterData[UrlRewrite::METADATA]['category_id'] = $categoryId; -- } -+ $useCategories = $this->scopeConfig->getValue( -+ \Magento\Catalog\Helper\Product::XML_PATH_PRODUCT_URL_USE_CATEGORY, -+ \Magento\Store\Model\ScopeInterface::SCOPE_STORE -+ ); -+ -+ $filterData[UrlRewrite::METADATA]['category_id'] -+ = $categoryId && $useCategories ? $categoryId : ''; -+ - $rewrite = $this->urlFinder->findOneByData($filterData); -+ - if ($rewrite) { - $requestPath = $rewrite->getRequestPath(); - $product->setRequestPath($requestPath); -@@ -194,6 +200,7 @@ class Url extends \Magento\Framework\DataObject - $routeParams['_query'] = []; - } - -- return $this->getUrlInstance()->setScope($storeId)->getUrl($routePath, $routeParams); -+ $url = $this->urlFactory->create()->setScope($storeId); -+ return $url->getUrl($routePath, $routeParams); - } - } -diff --git a/app/code/Magento/Catalog/Model/Product/Visibility.php b/app/code/Magento/Catalog/Model/Product/Visibility.php -index c863526898a..c05bda7838d 100644 ---- a/app/code/Magento/Catalog/Model/Product/Visibility.php -+++ b/app/code/Magento/Catalog/Model/Product/Visibility.php -@@ -55,7 +55,7 @@ class Visibility extends \Magento\Framework\DataObject implements OptionSourceIn - /** - * Retrieve visible in catalog ids array - * -- * @return string[] -+ * @return int[] - */ - public function getVisibleInCatalogIds() - { -@@ -65,7 +65,7 @@ class Visibility extends \Magento\Framework\DataObject implements OptionSourceIn - /** - * Retrieve visible in search ids array - * -- * @return string[] -+ * @return int[] - */ - public function getVisibleInSearchIds() - { -@@ -75,7 +75,7 @@ class Visibility extends \Magento\Framework\DataObject implements OptionSourceIn - /** - * Retrieve visible in site ids array - * -- * @return string[] -+ * @return int[] - */ - public function getVisibleInSiteIds() - { -@@ -86,6 +86,7 @@ class Visibility extends \Magento\Framework\DataObject implements OptionSourceIn - * Retrieve option array - * - * @return array -+ * phpcs:disable Magento2.Functions.StaticFunction - */ - public static function getOptionArray() - { -@@ -134,6 +135,7 @@ class Visibility extends \Magento\Framework\DataObject implements OptionSourceIn - $options = self::getOptionArray(); - return isset($options[$optionId]) ? $options[$optionId] : null; - } -+ //phpcs:enable Magento2.Functions.StaticFunction - - /** - * Retrieve flat column definition -@@ -251,7 +253,7 @@ class Visibility extends \Magento\Framework\DataObject implements OptionSourceIn - } - - /** -- * {@inheritdoc} -+ * @inheritdoc - */ - public function toOptionArray() - { -diff --git a/app/code/Magento/Catalog/Model/Product/Website/ReadHandler.php b/app/code/Magento/Catalog/Model/Product/Website/ReadHandler.php -index e81cdedd6d3..8acb4a6593a 100644 ---- a/app/code/Magento/Catalog/Model/Product/Website/ReadHandler.php -+++ b/app/code/Magento/Catalog/Model/Product/Website/ReadHandler.php -@@ -9,6 +9,9 @@ use Magento\Catalog\Api\Data\ProductInterface; - use Magento\Catalog\Model\ResourceModel\Product\Website\Link as ProductWebsiteLink; - use Magento\Framework\EntityManager\Operation\ExtensionInterface; - -+/** -+ * Add websites ids to product extension attributes. -+ */ - class ReadHandler implements ExtensionInterface - { - /** -@@ -18,7 +21,7 @@ class ReadHandler implements ExtensionInterface - - /** - * ReadHandler constructor. -- * @param ProductWebsiteLink $resourceModel -+ * @param ProductWebsiteLink $productWebsiteLink - */ - public function __construct( - ProductWebsiteLink $productWebsiteLink -@@ -27,6 +30,8 @@ class ReadHandler implements ExtensionInterface - } - - /** -+ * Add website ids to product extension attributes, if no set. -+ * - * @param ProductInterface $product - * @param array $arguments - * @SuppressWarnings(PHPMD.UnusedFormalParameter) -diff --git a/app/code/Magento/Catalog/Model/ProductCategoryList.php b/app/code/Magento/Catalog/Model/ProductCategoryList.php -index 5bbae772d5c..c3a88a505c5 100644 ---- a/app/code/Magento/Catalog/Model/ProductCategoryList.php -+++ b/app/code/Magento/Catalog/Model/ProductCategoryList.php -@@ -80,7 +80,10 @@ class ProductCategoryList - Select::SQL_UNION_ALL - ); - -- $this->categoryIdList[$productId] = $this->productResource->getConnection()->fetchCol($unionSelect); -+ $this->categoryIdList[$productId] = array_map( -+ 'intval', -+ $this->productResource->getConnection()->fetchCol($unionSelect) -+ ); - } - - return $this->categoryIdList[$productId]; -diff --git a/app/code/Magento/Catalog/Model/ProductIdLocator.php b/app/code/Magento/Catalog/Model/ProductIdLocator.php -index 2d9af6829ad..2d382164f26 100644 ---- a/app/code/Magento/Catalog/Model/ProductIdLocator.php -+++ b/app/code/Magento/Catalog/Model/ProductIdLocator.php -@@ -37,23 +37,43 @@ class ProductIdLocator implements \Magento\Catalog\Model\ProductIdLocatorInterfa - */ - private $idsBySku = []; - -+ /** -+ * Batch size to iterate collection -+ * -+ * @var int -+ */ -+ private $batchSize; -+ - /** - * @param \Magento\Framework\EntityManager\MetadataPool $metadataPool - * @param \Magento\Catalog\Model\ResourceModel\Product\CollectionFactory $collectionFactory -- * @param string $limitIdsBySkuValues -+ * @param string $idsLimit -+ * @param int $batchSize defines how many items can be processed by one iteration - */ - public function __construct( - \Magento\Framework\EntityManager\MetadataPool $metadataPool, - \Magento\Catalog\Model\ResourceModel\Product\CollectionFactory $collectionFactory, -- $idsLimit -+ $idsLimit, -+ int $batchSize = 5000 - ) { - $this->metadataPool = $metadataPool; - $this->collectionFactory = $collectionFactory; - $this->idsLimit = (int)$idsLimit; -+ $this->batchSize = $batchSize; - } - - /** -- * {@inheritdoc} -+ * @inheritdoc -+ * -+ * Load product items by provided products SKUs. -+ * Products collection will be iterated by pages with the $this->batchSize as a page size (for a cases when to many -+ * products SKUs were provided in parameters. -+ * Loaded products will be chached in the $this->idsBySku variable, but in the end of the method these storage will -+ * be truncated to $idsLimit quantity. -+ * As a result array with the products data will be returned with the following scheme: -+ * $data['product_sku']['link_field_value' => 'product_type'] -+ * -+ * @throws \Exception - */ - public function retrieveProductIdsBySkus(array $skus) - { -@@ -72,8 +92,16 @@ class ProductIdLocator implements \Magento\Catalog\Model\ProductIdLocatorInterfa - $linkField = $this->metadataPool->getMetadata(\Magento\Catalog\Api\Data\ProductInterface::class) - ->getLinkField(); - -- foreach ($collection as $item) { -- $this->idsBySku[strtolower(trim($item->getSku()))][$item->getData($linkField)] = $item->getTypeId(); -+ $collection->setPageSize($this->batchSize); -+ $pages = $collection->getLastPageNumber(); -+ for ($currentPage = 1; $currentPage <= $pages; $currentPage++) { -+ $collection->setCurPage($currentPage); -+ foreach ($collection->getItems() as $item) { -+ $sku = strtolower(trim($item->getSku())); -+ $itemIdentifier = $item->getData($linkField); -+ $this->idsBySku[$sku][$itemIdentifier] = $item->getTypeId(); -+ } -+ $collection->clear(); - } - } - -diff --git a/app/code/Magento/Catalog/Model/ProductLink/CollectionProvider.php b/app/code/Magento/Catalog/Model/ProductLink/CollectionProvider.php -index bc212adae2c..b96aff148e7 100644 ---- a/app/code/Magento/Catalog/Model/ProductLink/CollectionProvider.php -+++ b/app/code/Magento/Catalog/Model/ProductLink/CollectionProvider.php -@@ -9,6 +9,9 @@ namespace Magento\Catalog\Model\ProductLink; - use Magento\Catalog\Model\ProductLink\Converter\ConverterPool; - use Magento\Framework\Exception\NoSuchEntityException; - -+/** -+ * Provides a collection of linked product items (crosssells, related, upsells, ...) -+ */ - class CollectionProvider - { - /** -@@ -47,22 +50,20 @@ class CollectionProvider - - $products = $this->providers[$type]->getLinkedProducts($product); - $converter = $this->converterPool->getConverter($type); -- $output = []; - $sorterItems = []; - foreach ($products as $item) { -- $output[$item->getId()] = $converter->convert($item); -+ $itemId = $item->getId(); -+ $sorterItems[$itemId] = $converter->convert($item); -+ $sorterItems[$itemId]['position'] = $sorterItems[$itemId]['position'] ?? 0; - } - -- foreach ($output as $item) { -- $itemPosition = $item['position']; -- if (!isset($sorterItems[$itemPosition])) { -- $sorterItems[$itemPosition] = $item; -- } else { -- $newPosition = $itemPosition + 1; -- $sorterItems[$newPosition] = $item; -- } -- } -- ksort($sorterItems); -+ usort($sorterItems, function ($itemA, $itemB) { -+ $posA = (int)$itemA['position']; -+ $posB = (int)$itemB['position']; -+ -+ return $posA <=> $posB; -+ }); -+ - return $sorterItems; - } - } -diff --git a/app/code/Magento/Catalog/Model/ProductLink/Repository.php b/app/code/Magento/Catalog/Model/ProductLink/Repository.php -index 5bac99dbebb..98977de7eff 100644 ---- a/app/code/Magento/Catalog/Model/ProductLink/Repository.php -+++ b/app/code/Magento/Catalog/Model/ProductLink/Repository.php -@@ -10,6 +10,7 @@ use Magento\Catalog\Api\Data\ProductLinkInterfaceFactory; - use Magento\Catalog\Api\Data\ProductLinkExtensionFactory; - use Magento\Catalog\Model\Product\Initialization\Helper\ProductLinks as LinksInitializer; - use Magento\Catalog\Model\Product\LinkTypeProvider; -+use Magento\Framework\Api\SimpleDataObjectConverter; - use Magento\Framework\Exception\CouldNotSaveException; - use Magento\Framework\Exception\NoSuchEntityException; - use Magento\Framework\EntityManager\MetadataPool; -@@ -170,7 +171,7 @@ class Repository implements \Magento\Catalog\Api\ProductLinkRepositoryInterface - foreach ($item['custom_attributes'] as $option) { - $name = $option['attribute_code']; - $value = $option['value']; -- $setterName = 'set'.ucfirst($name); -+ $setterName = 'set' . SimpleDataObjectConverter::snakeCaseToUpperCamelCase($name); - // Check if setter exists - if (method_exists($productLinkExtension, $setterName)) { - call_user_func([$productLinkExtension, $setterName], $value); -diff --git a/app/code/Magento/Catalog/Model/ProductRender.php b/app/code/Magento/Catalog/Model/ProductRender.php -index 702c04b910d..5efb0343cd9 100644 ---- a/app/code/Magento/Catalog/Model/ProductRender.php -+++ b/app/code/Magento/Catalog/Model/ProductRender.php -@@ -206,7 +206,7 @@ class ProductRender extends \Magento\Framework\Model\AbstractExtensibleModel imp - * Set an extension attributes object. - * - * @param \Magento\Catalog\Api\Data\ProductRenderExtensionInterface $extensionAttributes -- * @return $this -+ * @return void - */ - public function setExtensionAttributes( - \Magento\Catalog\Api\Data\ProductRenderExtensionInterface $extensionAttributes -diff --git a/app/code/Magento/Catalog/Model/ProductRender/Image.php b/app/code/Magento/Catalog/Model/ProductRender/Image.php -index 774199a0dbf..5e024938d37 100644 ---- a/app/code/Magento/Catalog/Model/ProductRender/Image.php -+++ b/app/code/Magento/Catalog/Model/ProductRender/Image.php -@@ -9,14 +9,16 @@ namespace Magento\Catalog\Model\ProductRender; - use Magento\Catalog\Api\Data\ProductRender\ImageInterface; - - /** -- * @inheritdoc -+ * Product image renderer model. - */ - class Image extends \Magento\Framework\Model\AbstractExtensibleModel implements - ImageInterface - { - /** -+ * Set url to image. -+ * - * @param string $url -- * @return @return void -+ * @return void - */ - public function setUrl($url) - { -@@ -34,6 +36,8 @@ class Image extends \Magento\Framework\Model\AbstractExtensibleModel implements - } - - /** -+ * Retrieve image code. -+ * - * @return string - */ - public function getCode() -@@ -42,6 +46,8 @@ class Image extends \Magento\Framework\Model\AbstractExtensibleModel implements - } - - /** -+ * Set image code. -+ * - * @param string $code - * @return void - */ -@@ -51,6 +57,8 @@ class Image extends \Magento\Framework\Model\AbstractExtensibleModel implements - } - - /** -+ * Set image height. -+ * - * @param string $height - * @return void - */ -@@ -60,6 +68,8 @@ class Image extends \Magento\Framework\Model\AbstractExtensibleModel implements - } - - /** -+ * Retrieve image height. -+ * - * @return float - */ - public function getHeight() -@@ -68,6 +78,8 @@ class Image extends \Magento\Framework\Model\AbstractExtensibleModel implements - } - - /** -+ * Retrieve image width. -+ * - * @return float - */ - public function getWidth() -@@ -76,6 +88,8 @@ class Image extends \Magento\Framework\Model\AbstractExtensibleModel implements - } - - /** -+ * Set image width. -+ * - * @param string $width - * @return void - */ -@@ -85,6 +99,8 @@ class Image extends \Magento\Framework\Model\AbstractExtensibleModel implements - } - - /** -+ * Retrieve image label. -+ * - * @return string - */ - public function getLabel() -@@ -93,6 +109,8 @@ class Image extends \Magento\Framework\Model\AbstractExtensibleModel implements - } - - /** -+ * Set image label. -+ * - * @param string $label - * @return void - */ -@@ -102,6 +120,8 @@ class Image extends \Magento\Framework\Model\AbstractExtensibleModel implements - } - - /** -+ * Retrieve image width after image resize. -+ * - * @return float - */ - public function getResizedWidth() -@@ -110,6 +130,8 @@ class Image extends \Magento\Framework\Model\AbstractExtensibleModel implements - } - - /** -+ * Set image width after image resize. -+ * - * @param string $width - * @return void - */ -@@ -119,6 +141,8 @@ class Image extends \Magento\Framework\Model\AbstractExtensibleModel implements - } - - /** -+ * Set image height after image resize. -+ * - * @param string $height - * @return void - */ -@@ -128,6 +152,8 @@ class Image extends \Magento\Framework\Model\AbstractExtensibleModel implements - } - - /** -+ * Retrieve image height after image resize. -+ * - * @return float - */ - public function getResizedHeight() -@@ -149,7 +175,7 @@ class Image extends \Magento\Framework\Model\AbstractExtensibleModel implements - * Set an extension attributes object. - * - * @param \Magento\Catalog\Api\Data\ProductRender\ImageExtensionInterface $extensionAttributes -- * @return $this -+ * @return void - */ - public function setExtensionAttributes( - \Magento\Catalog\Api\Data\ProductRender\ImageExtensionInterface $extensionAttributes -diff --git a/app/code/Magento/Catalog/Model/ProductRenderList.php b/app/code/Magento/Catalog/Model/ProductRenderList.php -index a3d906cf10c..d1f60c09863 100644 ---- a/app/code/Magento/Catalog/Model/ProductRenderList.php -+++ b/app/code/Magento/Catalog/Model/ProductRenderList.php -@@ -17,8 +17,8 @@ use Magento\Framework\Data\CollectionModifierInterface; - - /** - * Provide product render information (this information should be enough for rendering product on front) -- * for one or few products - * -+ * Render information provided for one or few products - */ - class ProductRenderList implements ProductRenderListInterface - { -@@ -64,7 +64,6 @@ class ProductRenderList implements ProductRenderListInterface - * @param ProductRenderSearchResultsFactory $searchResultFactory - * @param ProductRenderFactory $productRenderDtoFactory - * @param Config $config -- * @param Product\Visibility $productVisibility - * @param CollectionModifier $collectionModifier - * @param array $productAttributes - */ -diff --git a/app/code/Magento/Catalog/Model/ProductRepository.php b/app/code/Magento/Catalog/Model/ProductRepository.php -index ef2c99c5cb4..e961db42d99 100644 ---- a/app/code/Magento/Catalog/Model/ProductRepository.php -+++ b/app/code/Magento/Catalog/Model/ProductRepository.php -@@ -1,16 +1,18 @@ - <?php - /** -- * - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ - - namespace Magento\Catalog\Model; - -+use Magento\Catalog\Api\CategoryLinkManagementInterface; -+use Magento\Catalog\Api\Data\ProductExtension; - use Magento\Catalog\Api\Data\ProductInterface; - use Magento\Catalog\Model\Product\Gallery\MimeTypeExtensionMap; -+use Magento\Catalog\Model\ProductRepository\MediaGalleryProcessor; - use Magento\Catalog\Model\ResourceModel\Product\Collection; --use Magento\Framework\Api\Data\ImageContentInterface; -+use Magento\Eav\Model\Entity\Attribute\Exception as AttributeException; - use Magento\Framework\Api\Data\ImageContentInterfaceFactory; - use Magento\Framework\Api\ImageContentValidatorInterface; - use Magento\Framework\Api\ImageProcessorInterface; -@@ -18,14 +20,17 @@ use Magento\Framework\Api\SearchCriteria\CollectionProcessorInterface; - use Magento\Framework\DB\Adapter\ConnectionException; - use Magento\Framework\DB\Adapter\DeadlockException; - use Magento\Framework\DB\Adapter\LockWaitException; -+use Magento\Framework\EntityManager\Operation\Read\ReadExtensions; - use Magento\Framework\Exception\CouldNotSaveException; - use Magento\Framework\Exception\InputException; - use Magento\Framework\Exception\LocalizedException; - use Magento\Framework\Exception\NoSuchEntityException; - use Magento\Framework\Exception\StateException; -+use Magento\Framework\Exception\TemporaryState\CouldNotSaveException as TemporaryCouldNotSaveException; - use Magento\Framework\Exception\ValidatorException; - - /** -+ * Product Repository. - * @SuppressWarnings(PHPMD.CouplingBetweenObjects) - * @SuppressWarnings(PHPMD.TooManyFields) - */ -@@ -117,11 +122,15 @@ class ProductRepository implements \Magento\Catalog\Api\ProductRepositoryInterfa - protected $fileSystem; - - /** -+ * @deprecated -+ * - * @var ImageContentInterfaceFactory - */ - protected $contentFactory; - - /** -+ * @deprecated -+ * - * @var ImageProcessorInterface - */ - protected $imageProcessor; -@@ -132,10 +141,17 @@ class ProductRepository implements \Magento\Catalog\Api\ProductRepositoryInterfa - protected $extensionAttributesJoinProcessor; - - /** -+ * @deprecated -+ * - * @var \Magento\Catalog\Model\Product\Gallery\Processor - */ - protected $mediaGalleryProcessor; - -+ /** -+ * @var MediaGalleryProcessor -+ */ -+ private $mediaProcessor; -+ - /** - * @var CollectionProcessorInterface - */ -@@ -151,6 +167,16 @@ class ProductRepository implements \Magento\Catalog\Api\ProductRepositoryInterfa - */ - private $serializer; - -+ /** -+ * @var ReadExtensions -+ */ -+ private $readExtensions; -+ -+ /** -+ * @var CategoryLinkManagementInterface -+ */ -+ private $linkManagement; -+ - /** - * ProductRepository constructor. - * @param ProductFactory $productFactory -@@ -176,6 +202,8 @@ class ProductRepository implements \Magento\Catalog\Api\ProductRepositoryInterfa - * @param CollectionProcessorInterface $collectionProcessor [optional] - * @param \Magento\Framework\Serialize\Serializer\Json|null $serializer - * @param int $cacheLimit [optional] -+ * @param ReadExtensions $readExtensions -+ * @param CategoryLinkManagementInterface $linkManagement - * @SuppressWarnings(PHPMD.ExcessiveParameterList) - * @SuppressWarnings(PHPMD.UnusedFormalParameter) - */ -@@ -202,7 +230,9 @@ class ProductRepository implements \Magento\Catalog\Api\ProductRepositoryInterfa - \Magento\Framework\Api\ExtensionAttribute\JoinProcessorInterface $extensionAttributesJoinProcessor, - CollectionProcessorInterface $collectionProcessor = null, - \Magento\Framework\Serialize\Serializer\Json $serializer = null, -- $cacheLimit = 1000 -+ $cacheLimit = 1000, -+ ReadExtensions $readExtensions = null, -+ CategoryLinkManagementInterface $linkManagement = null - ) { - $this->productFactory = $productFactory; - $this->collectionFactory = $collectionFactory; -@@ -225,10 +255,14 @@ class ProductRepository implements \Magento\Catalog\Api\ProductRepositoryInterfa - $this->serializer = $serializer ?: \Magento\Framework\App\ObjectManager::getInstance() - ->get(\Magento\Framework\Serialize\Serializer\Json::class); - $this->cacheLimit = (int)$cacheLimit; -+ $this->readExtensions = $readExtensions ?: \Magento\Framework\App\ObjectManager::getInstance() -+ ->get(ReadExtensions::class); -+ $this->linkManagement = $linkManagement ?: \Magento\Framework\App\ObjectManager::getInstance() -+ ->get(CategoryLinkManagementInterface::class); - } - - /** -- * {@inheritdoc} -+ * @inheritdoc - */ - public function get($sku, $editMode = false, $storeId = null, $forceReload = false) - { -@@ -258,7 +292,7 @@ class ProductRepository implements \Magento\Catalog\Api\ProductRepositoryInterfa - } - - /** -- * {@inheritdoc} -+ * @inheritdoc - */ - public function getById($productId, $editMode = false, $storeId = null, $forceReload = false) - { -@@ -306,10 +340,10 @@ class ProductRepository implements \Magento\Catalog\Api\ProductRepositoryInterfa - * Add product to internal cache and truncate cache if it has more than cacheLimit elements. - * - * @param string $cacheKey -- * @param \Magento\Catalog\Api\Data\ProductInterface $product -+ * @param ProductInterface $product - * @return void - */ -- private function cacheProduct($cacheKey, \Magento\Catalog\Api\Data\ProductInterface $product) -+ private function cacheProduct($cacheKey, ProductInterface $product) - { - $this->instancesById[$product->getId()][$cacheKey] = $product; - $this->saveProductInLocalCache($product, $cacheKey); -@@ -326,7 +360,7 @@ class ProductRepository implements \Magento\Catalog\Api\ProductRepositoryInterfa - * - * @param array $productData - * @param bool $createNew -- * @return \Magento\Catalog\Api\Data\ProductInterface|Product -+ * @return ProductInterface|Product - * @throws NoSuchEntityException - */ - protected function initializeProductData(array $productData, $createNew) -@@ -335,8 +369,11 @@ class ProductRepository implements \Magento\Catalog\Api\ProductRepositoryInterfa - if ($createNew) { - $product = $this->productFactory->create(); - $this->assignProductToWebsites($product); -+ } elseif (!empty($productData['id'])) { -+ $this->removeProductFromLocalCacheById($productData['id']); -+ $product = $this->getById($productData['id']); - } else { -- $this->removeProductFromLocalCache($productData['sku']); -+ $this->removeProductFromLocalCacheBySku($productData['sku']); - $product = $this->get($productData['sku']); - } - -@@ -348,6 +385,8 @@ class ProductRepository implements \Magento\Catalog\Api\ProductRepositoryInterfa - } - - /** -+ * Assign product to websites. -+ * - * @param \Magento\Catalog\Model\Product $product - * @return void - */ -@@ -363,6 +402,11 @@ class ProductRepository implements \Magento\Catalog\Api\ProductRepositoryInterfa - } - - /** -+ * Process new gallery media entry. -+ * -+ * @deprecated -+ * @see MediaGalleryProcessor::processNewMediaGalleryEntry() -+ * - * @param ProductInterface $product - * @param array $newEntry - * @return $this -@@ -374,52 +418,20 @@ class ProductRepository implements \Magento\Catalog\Api\ProductRepositoryInterfa - ProductInterface $product, - array $newEntry - ) { -- /** @var ImageContentInterface $contentDataObject */ -- $contentDataObject = $newEntry['content']; -- -- /** @var \Magento\Catalog\Model\Product\Media\Config $mediaConfig */ -- $mediaConfig = $product->getMediaConfig(); -- $mediaTmpPath = $mediaConfig->getBaseTmpMediaPath(); -+ $this->getMediaGalleryProcessor()->processNewMediaGalleryEntry($product, $newEntry); - -- $relativeFilePath = $this->imageProcessor->processImageContent($mediaTmpPath, $contentDataObject); -- $tmpFilePath = $mediaConfig->getTmpMediaShortUrl($relativeFilePath); -- -- if (!$product->hasGalleryAttribute()) { -- throw new StateException( -- __("The product that was requested doesn't exist. Verify the product and try again.") -- ); -- } -- -- $imageFileUri = $this->getMediaGalleryProcessor()->addImage( -- $product, -- $tmpFilePath, -- isset($newEntry['types']) ? $newEntry['types'] : [], -- true, -- isset($newEntry['disabled']) ? $newEntry['disabled'] : true -- ); -- // Update additional fields that are still empty after addImage call -- $this->getMediaGalleryProcessor()->updateImage( -- $product, -- $imageFileUri, -- [ -- 'label' => $newEntry['label'], -- 'position' => $newEntry['position'], -- 'disabled' => $newEntry['disabled'], -- 'media_type' => $newEntry['media_type'], -- ] -- ); - return $this; - } - - /** - * Process product links, creating new links, updating and deleting existing links - * -- * @param \Magento\Catalog\Api\Data\ProductInterface $product -+ * @param ProductInterface $product - * @param \Magento\Catalog\Api\Data\ProductLinkInterface[] $newLinks - * @return $this - * @throws NoSuchEntityException - */ -- private function processLinks(\Magento\Catalog\Api\Data\ProductInterface $product, $newLinks) -+ private function processLinks(ProductInterface $product, $newLinks) - { - if ($newLinks === null) { - // If product links were not specified, don't do anything -@@ -482,82 +494,28 @@ class ProductRepository implements \Magento\Catalog\Api\ProductRepositoryInterfa - * @return $this - * @throws InputException - * @throws StateException -+ * @throws LocalizedException - * @SuppressWarnings(PHPMD.CyclomaticComplexity) - */ - protected function processMediaGallery(ProductInterface $product, $mediaGalleryEntries) - { -- $existingMediaGallery = $product->getMediaGallery('images'); -- $newEntries = []; -- $entriesById = []; -- if (!empty($existingMediaGallery)) { -- foreach ($mediaGalleryEntries as $entry) { -- if (isset($entry['value_id'])) { -- $entriesById[$entry['value_id']] = $entry; -- } else { -- $newEntries[] = $entry; -- } -- } -- foreach ($existingMediaGallery as $key => &$existingEntry) { -- if (isset($entriesById[$existingEntry['value_id']])) { -- $updatedEntry = $entriesById[$existingEntry['value_id']]; -- if ($updatedEntry['file'] === null) { -- unset($updatedEntry['file']); -- } -- $existingMediaGallery[$key] = array_merge($existingEntry, $updatedEntry); -- } else { -- //set the removed flag -- $existingEntry['removed'] = true; -- } -- } -- $product->setData('media_gallery', ["images" => $existingMediaGallery]); -- } else { -- $newEntries = $mediaGalleryEntries; -- } -- -- $images = (array)$product->getMediaGallery('images'); -- $images = $this->determineImageRoles($product, $images); -- -- $this->getMediaGalleryProcessor()->clearMediaAttribute($product, array_keys($product->getMediaAttributes())); -- -- foreach ($images as $image) { -- if (!isset($image['removed']) && !empty($image['types'])) { -- $this->getMediaGalleryProcessor()->setMediaAttribute($product, $image['types'], $image['file']); -- } -- } -+ $this->getMediaGalleryProcessor()->processMediaGallery($product, $mediaGalleryEntries); - -- foreach ($newEntries as $newEntry) { -- if (!isset($newEntry['content'])) { -- throw new InputException(__('The image content is invalid. Verify the content and try again.')); -- } -- /** @var ImageContentInterface $contentDataObject */ -- $contentDataObject = $this->contentFactory->create() -- ->setName($newEntry['content']['data'][ImageContentInterface::NAME]) -- ->setBase64EncodedData($newEntry['content']['data'][ImageContentInterface::BASE64_ENCODED_DATA]) -- ->setType($newEntry['content']['data'][ImageContentInterface::TYPE]); -- $newEntry['content'] = $contentDataObject; -- $this->processNewMediaGalleryEntry($product, $newEntry); -- -- $finalGallery = $product->getData('media_gallery'); -- $newEntryId = key(array_diff_key($product->getData('media_gallery')['images'], $entriesById)); -- $newEntry = array_replace_recursive($newEntry, $finalGallery['images'][$newEntryId]); -- $entriesById[$newEntryId] = $newEntry; -- $finalGallery['images'][$newEntryId] = $newEntry; -- $product->setData('media_gallery', $finalGallery); -- } - return $this; - } - - /** -- * {@inheritdoc} -+ * @inheritdoc - * @SuppressWarnings(PHPMD.CyclomaticComplexity) - * @SuppressWarnings(PHPMD.NPathComplexity) - */ -- public function save(\Magento\Catalog\Api\Data\ProductInterface $product, $saveOptions = false) -+ public function save(ProductInterface $product, $saveOptions = false) - { -+ $assignToCategories = false; - $tierPrices = $product->getData('tier_price'); - - try { -- $existingProduct = $this->get($product->getSku()); -+ $existingProduct = $product->getId() ? $this->getById($product->getId()) : $this->get($product->getSku()); - - $product->setData( - $this->resourceModel->getLinkField(), -@@ -566,12 +524,19 @@ class ProductRepository implements \Magento\Catalog\Api\ProductRepositoryInterfa - if (!$product->hasData(Product::STATUS)) { - $product->setStatus($existingProduct->getStatus()); - } -+ -+ /** @var ProductExtension $extensionAttributes */ -+ $extensionAttributes = $product->getExtensionAttributes(); -+ if (empty($extensionAttributes->__toArray())) { -+ $product->setExtensionAttributes($existingProduct->getExtensionAttributes()); -+ $assignToCategories = true; -+ } - } catch (NoSuchEntityException $e) { - $existingProduct = null; - } - - $productDataArray = $this->extensibleDataObjectConverter -- ->toNestedArray($product, [], \Magento\Catalog\Api\Data\ProductInterface::class); -+ ->toNestedArray($product, [], ProductInterface::class); - $productDataArray = array_replace($productDataArray, $product->getData()); - $ignoreLinksFlag = $product->getData('ignore_links_flag'); - $productLinks = null; -@@ -597,79 +562,50 @@ class ProductRepository implements \Magento\Catalog\Api\ProductRepositoryInterfa - ); - } - -- try { -- if ($tierPrices !== null) { -- $product->setData('tier_price', $tierPrices); -- } -- $this->removeProductFromLocalCache($product->getSku()); -- unset($this->instancesById[$product->getId()]); -- $this->resourceModel->save($product); -- } catch (ConnectionException $exception) { -- throw new \Magento\Framework\Exception\TemporaryState\CouldNotSaveException( -- __('Database connection error'), -- $exception, -- $exception->getCode() -- ); -- } catch (DeadlockException $exception) { -- throw new \Magento\Framework\Exception\TemporaryState\CouldNotSaveException( -- __('Database deadlock found when trying to get lock'), -- $exception, -- $exception->getCode() -- ); -- } catch (LockWaitException $exception) { -- throw new \Magento\Framework\Exception\TemporaryState\CouldNotSaveException( -- __('Database lock wait timeout exceeded'), -- $exception, -- $exception->getCode() -- ); -- } catch (\Magento\Eav\Model\Entity\Attribute\Exception $exception) { -- throw \Magento\Framework\Exception\InputException::invalidFieldValue( -- $exception->getAttributeCode(), -- $product->getData($exception->getAttributeCode()), -- $exception -- ); -- } catch (ValidatorException $e) { -- throw new CouldNotSaveException(__($e->getMessage())); -- } catch (LocalizedException $e) { -- throw $e; -- } catch (\Exception $e) { -- throw new \Magento\Framework\Exception\CouldNotSaveException( -- __('The product was unable to be saved. Please try again.'), -- $e -+ if ($tierPrices !== null) { -+ $product->setData('tier_price', $tierPrices); -+ } -+ -+ $this->saveProduct($product); -+ if ($assignToCategories === true && $product->getCategoryIds()) { -+ $this->linkManagement->assignProductToCategories( -+ $product->getSku(), -+ $product->getCategoryIds() - ); - } -- $this->removeProductFromLocalCache($product->getSku()); -- unset($this->instancesById[$product->getId()]); -+ $this->removeProductFromLocalCacheBySku($product->getSku()); -+ $this->removeProductFromLocalCacheById($product->getId()); - - return $this->get($product->getSku(), false, $product->getStoreId()); - } - - /** -- * {@inheritdoc} -+ * @inheritdoc - */ -- public function delete(\Magento\Catalog\Api\Data\ProductInterface $product) -+ public function delete(ProductInterface $product) - { - $sku = $product->getSku(); - $productId = $product->getId(); - try { -- $this->removeProductFromLocalCache($product->getSku()); -- unset($this->instancesById[$product->getId()]); -+ $this->removeProductFromLocalCacheBySku($product->getSku()); -+ $this->removeProductFromLocalCacheById($product->getId()); - $this->resourceModel->delete($product); - } catch (ValidatorException $e) { -- throw new CouldNotSaveException(__($e->getMessage())); -+ throw new CouldNotSaveException(__($e->getMessage()), $e); - } catch (\Exception $e) { - throw new \Magento\Framework\Exception\StateException( -- __('The "%1" product couldn\'t be removed.', $sku) -+ __('The "%1" product couldn\'t be removed.', $sku), -+ $e - ); - } -- $this->removeProductFromLocalCache($sku); -- unset($this->instancesById[$productId]); -+ $this->removeProductFromLocalCacheBySku($sku); -+ $this->removeProductFromLocalCacheById($productId); - - return true; - } - - /** -- * {@inheritdoc} -+ * @inheritdoc - */ - public function deleteById($sku) - { -@@ -678,7 +614,7 @@ class ProductRepository implements \Magento\Catalog\Api\ProductRepositoryInterfa - } - - /** -- * {@inheritdoc} -+ * @inheritdoc - */ - public function getList(\Magento\Framework\Api\SearchCriteriaInterface $searchCriteria) - { -@@ -695,6 +631,7 @@ class ProductRepository implements \Magento\Catalog\Api\ProductRepositoryInterfa - $collection->load(); - - $collection->addCategoryIds(); -+ $this->addExtensionAttributes($collection); - $searchResult = $this->searchResultsFactory->create(); - $searchResult->setSearchCriteria($searchCriteria); - $searchResult->setItems($collection->getItems()); -@@ -705,7 +642,7 @@ class ProductRepository implements \Magento\Catalog\Api\ProductRepositoryInterfa - $this->getCacheKey( - [ - false, -- $product->hasData(\Magento\Catalog\Model\Product::STORE_ID) ? $product->getStoreId() : null -+ $product->getStoreId() - ] - ), - $product -@@ -715,6 +652,20 @@ class ProductRepository implements \Magento\Catalog\Api\ProductRepositoryInterfa - return $searchResult; - } - -+ /** -+ * Add extension attributes to loaded items. -+ * -+ * @param Collection $collection -+ * @return Collection -+ */ -+ private function addExtensionAttributes(Collection $collection) : Collection -+ { -+ foreach ($collection->getItems() as $item) { -+ $this->readExtensions->execute($item); -+ } -+ return $collection; -+ } -+ - /** - * Helper function that adds a FilterGroup to the collection. - * -@@ -760,41 +711,18 @@ class ProductRepository implements \Magento\Catalog\Api\ProductRepositoryInterfa - } - - /** -- * Ascertain image roles, if they are not set against the gallery entries -+ * Retrieve media gallery processor. - * -- * @param ProductInterface $product -- * @param array $images -- * @return array -- */ -- private function determineImageRoles(ProductInterface $product, array $images) : array -- { -- $imagesWithRoles = []; -- foreach ($images as $image) { -- if (!isset($image['types'])) { -- $image['types'] = []; -- if (isset($image['file'])) { -- foreach (array_keys($product->getMediaAttributes()) as $attribute) { -- if ($image['file'] == $product->getData($attribute)) { -- $image['types'][] = $attribute; -- } -- } -- } -- } -- $imagesWithRoles[] = $image; -- } -- return $imagesWithRoles; -- } -- -- /** -- * @return Product\Gallery\Processor -+ * @return MediaGalleryProcessor - */ - private function getMediaGalleryProcessor() - { -- if (null === $this->mediaGalleryProcessor) { -- $this->mediaGalleryProcessor = \Magento\Framework\App\ObjectManager::getInstance() -- ->get(\Magento\Catalog\Model\Product\Gallery\Processor::class); -+ if (null === $this->mediaProcessor) { -+ $this->mediaProcessor = \Magento\Framework\App\ObjectManager::getInstance() -+ ->get(MediaGalleryProcessor::class); - } -- return $this->mediaGalleryProcessor; -+ -+ return $this->mediaProcessor; - } - - /** -@@ -828,25 +756,36 @@ class ProductRepository implements \Magento\Catalog\Api\ProductRepositoryInterfa - } - - /** -- * Removes product in the local cache. -+ * Removes product in the local cache by sku. - * - * @param string $sku - * @return void - */ -- private function removeProductFromLocalCache(string $sku) :void -+ private function removeProductFromLocalCacheBySku(string $sku): void - { - $preparedSku = $this->prepareSku($sku); - unset($this->instances[$preparedSku]); - } - - /** -- * Saves product in the local cache. -+ * Removes product in the local cache by id. -+ * -+ * @param string|null $id -+ * @return void -+ */ -+ private function removeProductFromLocalCacheById(?string $id): void -+ { -+ unset($this->instancesById[$id]); -+ } -+ -+ /** -+ * Saves product in the local cache by sku. - * - * @param Product $product - * @param string $cacheKey - * @return void - */ -- private function saveProductInLocalCache(Product $product, string $cacheKey) : void -+ private function saveProductInLocalCache(Product $product, string $cacheKey): void - { - $preparedSku = $this->prepareSku($product->getSku()); - $this->instances[$preparedSku][$cacheKey] = $product; -@@ -862,4 +801,56 @@ class ProductRepository implements \Magento\Catalog\Api\ProductRepositoryInterfa - { - return mb_strtolower(trim($sku)); - } -+ -+ /** -+ * Save product resource model. -+ * -+ * @param ProductInterface|Product $product -+ * @throws TemporaryCouldNotSaveException -+ * @throws InputException -+ * @throws CouldNotSaveException -+ * @throws LocalizedException -+ */ -+ private function saveProduct($product): void -+ { -+ try { -+ $this->removeProductFromLocalCacheBySku($product->getSku()); -+ $this->removeProductFromLocalCacheById($product->getId()); -+ $this->resourceModel->save($product); -+ } catch (ConnectionException $exception) { -+ throw new TemporaryCouldNotSaveException( -+ __('Database connection error'), -+ $exception, -+ $exception->getCode() -+ ); -+ } catch (DeadlockException $exception) { -+ throw new TemporaryCouldNotSaveException( -+ __('Database deadlock found when trying to get lock'), -+ $exception, -+ $exception->getCode() -+ ); -+ } catch (LockWaitException $exception) { -+ throw new TemporaryCouldNotSaveException( -+ __('Database lock wait timeout exceeded'), -+ $exception, -+ $exception->getCode() -+ ); -+ } catch (AttributeException $exception) { -+ throw InputException::invalidFieldValue( -+ $exception->getAttributeCode(), -+ $product->getData($exception->getAttributeCode()), -+ $exception -+ ); -+ } catch (ValidatorException $e) { -+ throw new CouldNotSaveException(__($e->getMessage())); -+ } catch (LocalizedException $e) { -+ throw $e; -+ // phpcs:disable Magento2.Exceptions.ThrowCatch -+ } catch (\Exception $e) { -+ throw new CouldNotSaveException( -+ __('The product was unable to be saved. Please try again.'), -+ $e -+ ); -+ } -+ } - } -diff --git a/app/code/Magento/Catalog/Model/ProductRepository/MediaGalleryProcessor.php b/app/code/Magento/Catalog/Model/ProductRepository/MediaGalleryProcessor.php -new file mode 100644 -index 00000000000..fdcf2956dbd ---- /dev/null -+++ b/app/code/Magento/Catalog/Model/ProductRepository/MediaGalleryProcessor.php -@@ -0,0 +1,239 @@ -+<?php -+/** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+declare(strict_types=1); -+ -+namespace Magento\Catalog\Model\ProductRepository; -+ -+use Magento\Catalog\Api\Data\ProductInterface; -+use Magento\Catalog\Model\Product\Gallery\Processor; -+use Magento\Catalog\Model\Product\Media\Config; -+use Magento\Framework\Api\Data\ImageContentInterface; -+use Magento\Framework\Api\Data\ImageContentInterfaceFactory; -+use Magento\Framework\Api\ImageProcessorInterface; -+use Magento\Framework\Exception\InputException; -+use Magento\Framework\Exception\LocalizedException; -+use Magento\Framework\Exception\StateException; -+ -+/** -+ * Process Media gallery data for ProductRepository before save product. -+ */ -+class MediaGalleryProcessor -+{ -+ /** -+ * Catalog gallery processor. -+ * -+ * @var Processor -+ */ -+ private $processor; -+ -+ /** -+ * Image content data object factory. -+ * -+ * @var ImageContentInterfaceFactory -+ */ -+ private $contentFactory; -+ -+ /** -+ * Image processor. -+ * -+ * @var ImageProcessorInterface -+ */ -+ private $imageProcessor; -+ -+ /** -+ * @param Processor $processor -+ * @param ImageContentInterfaceFactory $contentFactory -+ * @param ImageProcessorInterface $imageProcessor -+ */ -+ public function __construct( -+ Processor $processor, -+ ImageContentInterfaceFactory $contentFactory, -+ ImageProcessorInterface $imageProcessor -+ ) { -+ $this->processor = $processor; -+ $this->contentFactory = $contentFactory; -+ $this->imageProcessor = $imageProcessor; -+ } -+ -+ /** -+ * Process Media gallery data before save product. -+ * -+ * Compare Media Gallery Entries Data with existing Media Gallery -+ * * If Media entry has not value_id set it as new -+ * * If Existing entry 'value_id' absent in Media Gallery set 'removed' flag -+ * * Merge Existing and new media gallery -+ * -+ * @param ProductInterface $product contains only existing media gallery items -+ * @param array $mediaGalleryEntries array which contains all media gallery items -+ * @return void -+ * @throws InputException -+ * @throws StateException -+ * @throws LocalizedException -+ */ -+ public function processMediaGallery(ProductInterface $product, array $mediaGalleryEntries) :void -+ { -+ $existingMediaGallery = $product->getMediaGallery('images'); -+ $newEntries = []; -+ $entriesById = []; -+ if (!empty($existingMediaGallery)) { -+ foreach ($mediaGalleryEntries as $entry) { -+ if (isset($entry['value_id'])) { -+ $entriesById[$entry['value_id']] = $entry; -+ } else { -+ $newEntries[] = $entry; -+ } -+ } -+ foreach ($existingMediaGallery as $key => &$existingEntry) { -+ if (isset($entriesById[$existingEntry['value_id']])) { -+ $updatedEntry = $entriesById[$existingEntry['value_id']]; -+ if ($updatedEntry['file'] === null) { -+ unset($updatedEntry['file']); -+ } -+ $existingMediaGallery[$key] = array_merge($existingEntry, $updatedEntry); -+ } else { -+ //set the removed flag -+ $existingEntry['removed'] = true; -+ } -+ } -+ $product->setData('media_gallery', ["images" => $existingMediaGallery]); -+ } else { -+ $newEntries = $mediaGalleryEntries; -+ } -+ -+ $images = (array)$product->getMediaGallery('images'); -+ $images = $this->determineImageRoles($product, $images); -+ -+ $this->processor->clearMediaAttribute($product, array_keys($product->getMediaAttributes())); -+ -+ $this->processMediaAttributes($product, $images); -+ $this->processEntries($product, $newEntries, $entriesById); -+ } -+ -+ /** -+ * Process new gallery media entry. -+ * -+ * @param ProductInterface $product -+ * @param array $newEntry -+ * @return void -+ * @throws InputException -+ * @throws StateException -+ * @throws LocalizedException -+ */ -+ public function processNewMediaGalleryEntry( -+ ProductInterface $product, -+ array $newEntry -+ ) :void { -+ /** @var ImageContentInterface $contentDataObject */ -+ $contentDataObject = $newEntry['content']; -+ -+ /** @var Config $mediaConfig */ -+ $mediaConfig = $product->getMediaConfig(); -+ $mediaTmpPath = $mediaConfig->getBaseTmpMediaPath(); -+ -+ $relativeFilePath = $this->imageProcessor->processImageContent($mediaTmpPath, $contentDataObject); -+ $tmpFilePath = $mediaConfig->getTmpMediaShortUrl($relativeFilePath); -+ -+ if (!$product->hasGalleryAttribute()) { -+ throw new StateException( -+ __("The product that was requested doesn't exist. Verify the product and try again.") -+ ); -+ } -+ -+ $imageFileUri = $this->processor->addImage( -+ $product, -+ $tmpFilePath, -+ isset($newEntry['types']) ? $newEntry['types'] : [], -+ true, -+ isset($newEntry['disabled']) ? $newEntry['disabled'] : true -+ ); -+ // Update additional fields that are still empty after addImage call -+ $this->processor->updateImage( -+ $product, -+ $imageFileUri, -+ [ -+ 'label' => $newEntry['label'], -+ 'position' => $newEntry['position'], -+ 'disabled' => $newEntry['disabled'], -+ 'media_type' => $newEntry['media_type'], -+ ] -+ ); -+ } -+ -+ /** -+ * Ascertain image roles, if they are not set against the gallery entries. -+ * -+ * @param ProductInterface $product -+ * @param array $images -+ * @return array -+ */ -+ private function determineImageRoles(ProductInterface $product, array $images) : array -+ { -+ $imagesWithRoles = []; -+ foreach ($images as $image) { -+ if (!isset($image['types'])) { -+ $image['types'] = []; -+ if (isset($image['file'])) { -+ foreach (array_keys($product->getMediaAttributes()) as $attribute) { -+ if ($image['file'] == $product->getData($attribute)) { -+ $image['types'][] = $attribute; -+ } -+ } -+ } -+ } -+ $imagesWithRoles[] = $image; -+ } -+ -+ return $imagesWithRoles; -+ } -+ -+ /** -+ * Convert entries into product media gallery data and set to product. -+ * -+ * @param ProductInterface $product -+ * @param array $newEntries -+ * @param array $entriesById -+ * @throws InputException -+ * @throws LocalizedException -+ * @throws StateException -+ */ -+ private function processEntries(ProductInterface $product, array $newEntries, array $entriesById): void -+ { -+ foreach ($newEntries as $newEntry) { -+ if (!isset($newEntry['content'])) { -+ throw new InputException(__('The image content is invalid. Verify the content and try again.')); -+ } -+ /** @var ImageContentInterface $contentDataObject */ -+ $contentDataObject = $this->contentFactory->create() -+ ->setName($newEntry['content']['data'][ImageContentInterface::NAME]) -+ ->setBase64EncodedData($newEntry['content']['data'][ImageContentInterface::BASE64_ENCODED_DATA]) -+ ->setType($newEntry['content']['data'][ImageContentInterface::TYPE]); -+ $newEntry['content'] = $contentDataObject; -+ $this->processNewMediaGalleryEntry($product, $newEntry); -+ -+ $finalGallery = $product->getData('media_gallery'); -+ $newEntryId = key(array_diff_key($product->getData('media_gallery')['images'], $entriesById)); -+ $newEntry = array_replace_recursive($newEntry, $finalGallery['images'][$newEntryId]); -+ $entriesById[$newEntryId] = $newEntry; -+ $finalGallery['images'][$newEntryId] = $newEntry; -+ $product->setData('media_gallery', $finalGallery); -+ } -+ } -+ -+ /** -+ * Set media attribute values. -+ * -+ * @param ProductInterface $product -+ * @param array $images -+ */ -+ private function processMediaAttributes(ProductInterface $product, array $images): void -+ { -+ foreach ($images as $image) { -+ if (empty($image['removed']) && !empty($image['types'])) { -+ $this->processor->setMediaAttribute($product, $image['types'], $image['file']); -+ } -+ } -+ } -+} -diff --git a/app/code/Magento/Catalog/Model/ResourceModel/AbstractCollection.php b/app/code/Magento/Catalog/Model/ResourceModel/AbstractCollection.php -index d4f5fdd5137..2896849b76c 100644 ---- a/app/code/Magento/Catalog/Model/ResourceModel/AbstractCollection.php -+++ b/app/code/Magento/Catalog/Model/ResourceModel/AbstractCollection.php -@@ -7,6 +7,7 @@ namespace Magento\Catalog\Model\ResourceModel; - - /** - * Flat abstract collection -+ * - * @SuppressWarnings(PHPMD.CouplingBetweenObjects) - */ - abstract class AbstractCollection extends \Magento\Framework\Model\ResourceModel\Db\VersionControl\Collection -@@ -34,7 +35,7 @@ abstract class AbstractCollection extends \Magento\Framework\Model\ResourceModel - } - - /** -- * get select count sql -+ * Get select count sql - * - * @return \Magento\Framework\DB\Select - */ -@@ -69,6 +70,7 @@ abstract class AbstractCollection extends \Magento\Framework\Model\ResourceModel - - /** - * Add attribute to select result set. -+ * - * Backward compatibility with EAV collection - * - * @param string $attribute -@@ -82,6 +84,7 @@ abstract class AbstractCollection extends \Magento\Framework\Model\ResourceModel - - /** - * Specify collection select filter by attribute value -+ * - * Backward compatibility with EAV collection - * - * @param string|\Magento\Eav\Model\Entity\Attribute $attribute -@@ -96,6 +99,7 @@ abstract class AbstractCollection extends \Magento\Framework\Model\ResourceModel - - /** - * Specify collection select order by attribute value -+ * - * Backward compatibility with EAV collection - * - * @param string $attribute -@@ -110,6 +114,7 @@ abstract class AbstractCollection extends \Magento\Framework\Model\ResourceModel - - /** - * Set collection page start and records to show -+ * - * Backward compatibility with EAV collection - * - * @param int $pageNum -@@ -124,11 +129,12 @@ abstract class AbstractCollection extends \Magento\Framework\Model\ResourceModel - - /** - * Create all ids retrieving select with limitation -+ * - * Backward compatibility with EAV collection - * - * @param int $limit - * @param int $offset -- * @return \Magento\Eav\Model\Entity\Collection\AbstractCollection -+ * @return \Magento\Framework\DB\Select - */ - protected function _getAllIdsSelect($limit = null, $offset = null) - { -@@ -144,6 +150,7 @@ abstract class AbstractCollection extends \Magento\Framework\Model\ResourceModel - - /** - * Retrieve all ids for collection -+ * - * Backward compatibility with EAV collection - * - * @param int $limit -diff --git a/app/code/Magento/Catalog/Model/ResourceModel/AbstractResource.php b/app/code/Magento/Catalog/Model/ResourceModel/AbstractResource.php -index b9e629912a5..3d7f863b7c0 100644 ---- a/app/code/Magento/Catalog/Model/ResourceModel/AbstractResource.php -+++ b/app/code/Magento/Catalog/Model/ResourceModel/AbstractResource.php -@@ -7,6 +7,10 @@ - namespace Magento\Catalog\Model\ResourceModel; - - use Magento\Eav\Model\Entity\Attribute\AbstractAttribute; -+use Magento\Eav\Model\Entity\Attribute\Backend\AbstractBackend; -+use Magento\Eav\Model\Entity\Attribute\Frontend\AbstractFrontend; -+use Magento\Eav\Model\Entity\Attribute\Source\AbstractSource; -+use Magento\Eav\Model\Entity\Attribute\UniqueValidationInterface; - - /** - * Catalog entity abstract model -@@ -37,16 +41,18 @@ abstract class AbstractResource extends \Magento\Eav\Model\Entity\AbstractEntity - * @param \Magento\Store\Model\StoreManagerInterface $storeManager - * @param \Magento\Catalog\Model\Factory $modelFactory - * @param array $data -+ * @param UniqueValidationInterface|null $uniqueValidator - */ - public function __construct( - \Magento\Eav\Model\Entity\Context $context, - \Magento\Store\Model\StoreManagerInterface $storeManager, - \Magento\Catalog\Model\Factory $modelFactory, -- $data = [] -+ $data = [], -+ UniqueValidationInterface $uniqueValidator = null - ) { - $this->_storeManager = $storeManager; - $this->_modelFactory = $modelFactory; -- parent::__construct($context, $data); -+ parent::__construct($context, $data, $uniqueValidator); - } - - /** -@@ -86,16 +92,14 @@ abstract class AbstractResource extends \Magento\Eav\Model\Entity\AbstractEntity - /** - * Check whether attribute instance (attribute, backend, frontend or source) has method and applicable - * -- * @param AbstractAttribute|\Magento\Eav\Model\Entity\Attribute\Backend\AbstractBackend -- * |\Magento\Eav\Model\Entity\Attribute\Frontend\AbstractFrontend -- * |\Magento\Eav\Model\Entity\Attribute\Source\AbstractSource $instance -+ * @param AbstractAttribute|AbstractBackend|AbstractFrontend|AbstractSource $instance - * @param string $method - * @param array $args array of arguments - * @return boolean - */ - protected function _isCallableAttributeInstance($instance, $method, $args) - { -- if ($instance instanceof \Magento\Eav\Model\Entity\Attribute\Backend\AbstractBackend -+ if ($instance instanceof AbstractBackend - && ($method == 'beforeSave' || $method == 'afterSave') - ) { - $attributeCode = $instance->getAttribute()->getAttributeCode(); -@@ -112,6 +116,7 @@ abstract class AbstractResource extends \Magento\Eav\Model\Entity\AbstractEntity - - /** - * Retrieve select object for loading entity attributes values -+ * - * Join attribute store value - * - * @param \Magento\Framework\DataObject $object -@@ -244,6 +249,7 @@ abstract class AbstractResource extends \Magento\Eav\Model\Entity\AbstractEntity - - /** - * Check if attribute present for non default Store View. -+ * - * Prevent "delete" query locking in a case when nothing to delete - * - * @param AbstractAttribute $attribute -@@ -485,7 +491,7 @@ abstract class AbstractResource extends \Magento\Eav\Model\Entity\AbstractEntity - * Retrieve attribute's raw value from DB. - * - * @param int $entityId -- * @param int|string|array $attribute atrribute's ids or codes -+ * @param int|string|array $attribute attribute's ids or codes - * @param int|\Magento\Store\Model\Store $store - * @return bool|string|array - * @SuppressWarnings(PHPMD.CyclomaticComplexity) -diff --git a/app/code/Magento/Catalog/Model/ResourceModel/Category.php b/app/code/Magento/Catalog/Model/ResourceModel/Category.php -index fa68ae3f865..536fda7e093 100644 ---- a/app/code/Magento/Catalog/Model/ResourceModel/Category.php -+++ b/app/code/Magento/Catalog/Model/ResourceModel/Category.php -@@ -16,6 +16,8 @@ use Magento\Framework\DataObject; - use Magento\Framework\EntityManager\EntityManager; - - /** -+ * Resource model for category entity -+ * - * @SuppressWarnings(PHPMD.CouplingBetweenObjects) - */ - class Category extends AbstractResource -@@ -198,7 +200,7 @@ class Category extends AbstractResource - * delete child categories - * - * @param \Magento\Framework\DataObject $object -- * @return $this -+ * @return void - */ - protected function _beforeDelete(\Magento\Framework\DataObject $object) - { -@@ -249,7 +251,8 @@ class Category extends AbstractResource - - /** - * Process category data before saving -- * prepare path and increment children count for parent categories -+ * -+ * Prepare path and increment children count for parent categories - * - * @param \Magento\Framework\DataObject $object - * @return $this -@@ -298,7 +301,8 @@ class Category extends AbstractResource - - /** - * Process category data after save category object -- * save related products ids and update path value -+ * -+ * Save related products ids and update path value - * - * @param \Magento\Framework\DataObject $object - * @return $this -@@ -482,15 +486,27 @@ class Category extends AbstractResource - $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); - } - - /** -- * Get chlden categories count -+ * Get children categories count - * - * @param int $categoryId - * @return int -@@ -664,7 +680,7 @@ class Category extends AbstractResource - $bind = ['category_id' => (int)$category->getId()]; - $counts = $this->getConnection()->fetchOne($select, $bind); - -- return intval($counts); -+ return (int) $counts; - } - - /** -@@ -862,6 +878,7 @@ class Category extends AbstractResource - - /** - * Check category is forbidden to delete. -+ * - * If category is root and assigned to store group return false - * - * @param integer $categoryId -@@ -918,7 +935,7 @@ class Category extends AbstractResource - $childrenCount = $this->getChildrenCount($category->getId()) + 1; - $table = $this->getEntityTable(); - $connection = $this->getConnection(); -- $levelFiled = $connection->quoteIdentifier('level'); -+ $levelField = $connection->quoteIdentifier('level'); - $pathField = $connection->quoteIdentifier('path'); - - /** -@@ -958,7 +975,7 @@ class Category extends AbstractResource - $newPath . '/' - ) . ')' - ), -- 'level' => new \Zend_Db_Expr($levelFiled . ' + ' . $levelDisposition) -+ 'level' => new \Zend_Db_Expr($levelField . ' + ' . $levelDisposition) - ], - [$pathField . ' LIKE ?' => $category->getPath() . '/%'] - ); -@@ -982,6 +999,7 @@ class Category extends AbstractResource - - /** - * Process positions of old parent category children and new parent category children. -+ * - * Get position for moved category - * - * @param \Magento\Catalog\Model\Category $category -@@ -1062,7 +1080,7 @@ class Category extends AbstractResource - } - - /** -- * {@inheritdoc} -+ * @inheritdoc - */ - public function delete($object) - { -@@ -1088,6 +1106,8 @@ class Category extends AbstractResource - } - - /** -+ * Returns EntityManager object -+ * - * @return EntityManager - */ - private function getEntityManager() -@@ -1100,6 +1120,8 @@ class Category extends AbstractResource - } - - /** -+ * Returns AggregateCount object -+ * - * @return Category\AggregateCount - */ - private function getAggregateCount() -diff --git a/app/code/Magento/Catalog/Model/ResourceModel/Category/Collection.php b/app/code/Magento/Catalog/Model/ResourceModel/Category/Collection.php -index 46bb74513b5..657daca1305 100644 ---- a/app/code/Magento/Catalog/Model/ResourceModel/Category/Collection.php -+++ b/app/code/Magento/Catalog/Model/ResourceModel/Category/Collection.php -@@ -82,7 +82,6 @@ class Collection extends \Magento\Catalog\Model\ResourceModel\Collection\Abstrac - * @param \Magento\Store\Model\StoreManagerInterface $storeManager - * @param \Magento\Framework\DB\Adapter\AdapterInterface $connection - * @param \Magento\Framework\App\Config\ScopeConfigInterface $scopeConfig -- * - * @SuppressWarnings(PHPMD.ExcessiveParameterList) - */ - public function __construct( -@@ -323,9 +322,7 @@ class Collection extends \Magento\Catalog\Model\ResourceModel\Collection\Abstrac - 'main_table.category_id=e.entity_id', - [] - )->where( -- 'e.entity_id = :entity_id' -- )->orWhere( -- 'e.path LIKE :c_path' -+ '(e.entity_id = :entity_id OR e.path LIKE :c_path)' - ); - if ($websiteId) { - $select->join( -diff --git a/app/code/Magento/Catalog/Model/ResourceModel/Category/Flat.php b/app/code/Magento/Catalog/Model/ResourceModel/Category/Flat.php -index 01e4b072b03..05950531e21 100644 ---- a/app/code/Magento/Catalog/Model/ResourceModel/Category/Flat.php -+++ b/app/code/Magento/Catalog/Model/ResourceModel/Category/Flat.php -@@ -173,7 +173,7 @@ class Flat extends \Magento\Indexer\Model\ResourceModel\AbstractResource - public function getMainStoreTable($storeId = \Magento\Store\Model\Store::DEFAULT_STORE_ID) - { - if (is_string($storeId)) { -- $storeId = intval($storeId); -+ $storeId = (int) $storeId; - } - - if ($storeId) { -@@ -699,8 +699,20 @@ class Flat extends \Magento\Indexer\Model\ResourceModel\AbstractResource - $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/Flat/Collection.php b/app/code/Magento/Catalog/Model/ResourceModel/Category/Flat/Collection.php -index 3b3005f1ce6..03e33365b77 100644 ---- a/app/code/Magento/Catalog/Model/ResourceModel/Category/Flat/Collection.php -+++ b/app/code/Magento/Catalog/Model/ResourceModel/Category/Flat/Collection.php -@@ -12,11 +12,13 @@ use Magento\Framework\Data\Collection\Db\FetchStrategyInterface; - use Magento\Framework\Model\ResourceModel\Db\AbstractDb; - use Psr\Log\LoggerInterface as Logger; - use Magento\Store\Model\StoreManagerInterface; -+use Magento\Store\Model\ScopeInterface; - - /** - * Catalog category flat collection - * - * @author Magento Core Team <core@magentocommerce.com> -+ * @SuppressWarnings(PHPMD.CouplingBetweenObjects) - */ - class Collection extends \Magento\Framework\Model\ResourceModel\Db\Collection\AbstractCollection - { -@@ -48,12 +50,20 @@ class Collection extends \Magento\Framework\Model\ResourceModel\Db\Collection\Ab - */ - protected $_storeId; - -+ /** -+ * Core store config -+ * -+ * @var \Magento\Framework\App\Config\ScopeConfigInterface -+ */ -+ private $scopeConfig; -+ - /** - * @param \Magento\Framework\Data\Collection\EntityFactory $entityFactory - * @param Logger $logger - * @param FetchStrategyInterface $fetchStrategy - * @param ManagerInterface $eventManager - * @param \Magento\Store\Model\StoreManagerInterface $storeManager -+ * @param \Magento\Framework\App\Config\ScopeConfigInterface $scopeConfig - * @param \Magento\Framework\DB\Adapter\AdapterInterface $connection - * @param AbstractDb $resource - */ -@@ -63,10 +73,12 @@ class Collection extends \Magento\Framework\Model\ResourceModel\Db\Collection\Ab - FetchStrategyInterface $fetchStrategy, - ManagerInterface $eventManager, - StoreManagerInterface $storeManager, -+ \Magento\Framework\App\Config\ScopeConfigInterface $scopeConfig, - \Magento\Framework\DB\Adapter\AdapterInterface $connection = null, - AbstractDb $resource = null - ) { - $this->_storeManager = $storeManager; -+ $this->scopeConfig = $scopeConfig; - parent::__construct($entityFactory, $logger, $fetchStrategy, $eventManager, $connection, $resource); - } - -@@ -387,4 +399,21 @@ class Collection extends \Magento\Framework\Model\ResourceModel\Db\Collection\Ab - $this->setCurPage($pageNum)->setPageSize($pageSize); - return $this; - } -+ -+ /** -+ * Add navigation max depth filter -+ * -+ * @return $this -+ */ -+ public function addNavigationMaxDepthFilter() -+ { -+ $navigationMaxDepth = (int)$this->scopeConfig->getValue( -+ 'catalog/navigation/max_depth', -+ ScopeInterface::SCOPE_STORE -+ ); -+ if ($navigationMaxDepth > 0) { -+ $this->addLevelFilter($navigationMaxDepth); -+ } -+ return $this; -+ } - } -diff --git a/app/code/Magento/Catalog/Model/ResourceModel/Category/StateDependentCollectionFactory.php b/app/code/Magento/Catalog/Model/ResourceModel/Category/StateDependentCollectionFactory.php -new file mode 100644 -index 00000000000..fc476ab6ff2 ---- /dev/null -+++ b/app/code/Magento/Catalog/Model/ResourceModel/Category/StateDependentCollectionFactory.php -@@ -0,0 +1,55 @@ -+<?php -+/** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+declare(strict_types=1); -+namespace Magento\Catalog\Model\ResourceModel\Category; -+ -+/** -+ * Factory class for state dependent category collection -+ */ -+class StateDependentCollectionFactory -+{ -+ /** -+ * Object Manager instance -+ * -+ * @var \Magento\Framework\ObjectManagerInterface -+ */ -+ private $objectManager; -+ -+ /** -+ * Catalog category flat state -+ * -+ * @var \Magento\Catalog\Model\Indexer\Category\Flat\State -+ */ -+ private $catalogCategoryFlatState; -+ -+ /** -+ * Factory constructor -+ * -+ * @param \Magento\Framework\ObjectManagerInterface $objectManager -+ * @param \Magento\Catalog\Model\Indexer\Category\Flat\State $catalogCategoryFlatState -+ */ -+ public function __construct( -+ \Magento\Framework\ObjectManagerInterface $objectManager, -+ \Magento\Catalog\Model\Indexer\Category\Flat\State $catalogCategoryFlatState -+ ) { -+ $this->objectManager = $objectManager; -+ $this->catalogCategoryFlatState = $catalogCategoryFlatState; -+ } -+ -+ /** -+ * Create class instance with specified parameters -+ * -+ * @param array $data -+ * @return \Magento\Framework\Data\Collection\AbstractDb -+ */ -+ public function create(array $data = []) -+ { -+ return $this->objectManager->create( -+ ($this->catalogCategoryFlatState->isAvailable()) ? Flat\Collection::class : Collection::class, -+ $data -+ ); -+ } -+} -diff --git a/app/code/Magento/Catalog/Model/ResourceModel/Collection/AbstractCollection.php b/app/code/Magento/Catalog/Model/ResourceModel/Collection/AbstractCollection.php -index 9ab863cde27..3a0d47fe573 100644 ---- a/app/code/Magento/Catalog/Model/ResourceModel/Collection/AbstractCollection.php -+++ b/app/code/Magento/Catalog/Model/ResourceModel/Collection/AbstractCollection.php -@@ -7,6 +7,7 @@ namespace Magento\Catalog\Model\ResourceModel\Collection; - - /** - * Catalog EAV collection resource abstract model -+ * - * Implement using different stores for retrieve attribute values - * - * @api -@@ -42,7 +43,6 @@ class AbstractCollection extends \Magento\Eav\Model\Entity\Collection\AbstractCo - * @param \Magento\Framework\Validator\UniversalFactory $universalFactory - * @param \Magento\Store\Model\StoreManagerInterface $storeManager - * @param \Magento\Framework\DB\Adapter\AdapterInterface $connection -- * - * @SuppressWarnings(PHPMD.ExcessiveParameterList) - */ - public function __construct( -@@ -205,10 +205,7 @@ class AbstractCollection extends \Magento\Eav\Model\Entity\Collection\AbstractCo - } - - /** -- * @param \Magento\Framework\DB\Select $select -- * @param string $table -- * @param string $type -- * @return \Magento\Framework\DB\Select -+ * @inheritdoc - */ - protected function _addLoadAttributesSelectValues($select, $table, $type) - { -diff --git a/app/code/Magento/Catalog/Model/ResourceModel/Eav/Attribute.php b/app/code/Magento/Catalog/Model/ResourceModel/Eav/Attribute.php -index 8f8e9f6bfed..d56cc40ad0f 100644 ---- a/app/code/Magento/Catalog/Model/ResourceModel/Eav/Attribute.php -+++ b/app/code/Magento/Catalog/Model/ResourceModel/Eav/Attribute.php -@@ -167,6 +167,8 @@ class Attribute extends \Magento\Eav\Model\Entity\Attribute implements - } - - /** -+ * Init model -+ * - * @return void - */ - protected function _construct() -@@ -234,6 +236,8 @@ class Attribute extends \Magento\Eav\Model\Entity\Attribute implements - ) { - $this->_indexerEavProcessor->markIndexerAsInvalid(); - } -+ -+ $this->_source = null; - - return parent::afterSave(); - } -@@ -362,6 +366,7 @@ class Attribute extends \Magento\Eav\Model\Entity\Attribute implements - - /** - * Retrieve apply to products array -+ * - * Return empty array if applied to all products - * - * @return string[] -@@ -478,7 +483,7 @@ class Attribute extends \Magento\Eav\Model\Entity\Attribute implements - $backendType = $this->getOrigData('backend_type'); - $frontendInput = $this->getOrigData('frontend_input'); - -- if ($backendType == 'int' && $frontendInput == 'select') { -+ if ($backendType == 'int' && ($frontendInput == 'select' || $frontendInput == 'boolean')) { - return true; - } elseif ($backendType == 'varchar' && $frontendInput == 'multiselect') { - return true; -@@ -507,8 +512,8 @@ class Attribute extends \Magento\Eav\Model\Entity\Attribute implements - } - - /** -+ * @inheritdoc - * @codeCoverageIgnoreStart -- * {@inheritdoc} - */ - public function getIsWysiwygEnabled() - { -@@ -516,7 +521,7 @@ class Attribute extends \Magento\Eav\Model\Entity\Attribute implements - } - - /** -- * {@inheritdoc} -+ * @inheritdoc - */ - public function getIsHtmlAllowedOnFront() - { -@@ -524,7 +529,7 @@ class Attribute extends \Magento\Eav\Model\Entity\Attribute implements - } - - /** -- * {@inheritdoc} -+ * @inheritdoc - */ - public function getUsedForSortBy() - { -@@ -532,7 +537,7 @@ class Attribute extends \Magento\Eav\Model\Entity\Attribute implements - } - - /** -- * {@inheritdoc} -+ * @inheritdoc - */ - public function getIsFilterable() - { -@@ -540,7 +545,7 @@ class Attribute extends \Magento\Eav\Model\Entity\Attribute implements - } - - /** -- * {@inheritdoc} -+ * @inheritdoc - */ - public function getIsFilterableInSearch() - { -@@ -548,7 +553,7 @@ class Attribute extends \Magento\Eav\Model\Entity\Attribute implements - } - - /** -- * {@inheritdoc} -+ * @inheritdoc - */ - public function getIsUsedInGrid() - { -@@ -556,7 +561,7 @@ class Attribute extends \Magento\Eav\Model\Entity\Attribute implements - } - - /** -- * {@inheritdoc} -+ * @inheritdoc - */ - public function getIsVisibleInGrid() - { -@@ -564,7 +569,7 @@ class Attribute extends \Magento\Eav\Model\Entity\Attribute implements - } - - /** -- * {@inheritdoc} -+ * @inheritdoc - */ - public function getIsFilterableInGrid() - { -@@ -572,7 +577,7 @@ class Attribute extends \Magento\Eav\Model\Entity\Attribute implements - } - - /** -- * {@inheritdoc} -+ * @inheritdoc - */ - public function getPosition() - { -@@ -580,7 +585,7 @@ class Attribute extends \Magento\Eav\Model\Entity\Attribute implements - } - - /** -- * {@inheritdoc} -+ * @inheritdoc - */ - public function getIsSearchable() - { -@@ -588,7 +593,7 @@ class Attribute extends \Magento\Eav\Model\Entity\Attribute implements - } - - /** -- * {@inheritdoc} -+ * @inheritdoc - */ - public function getIsVisibleInAdvancedSearch() - { -@@ -596,7 +601,7 @@ class Attribute extends \Magento\Eav\Model\Entity\Attribute implements - } - - /** -- * {@inheritdoc} -+ * @inheritdoc - */ - public function getIsComparable() - { -@@ -604,7 +609,7 @@ class Attribute extends \Magento\Eav\Model\Entity\Attribute implements - } - - /** -- * {@inheritdoc} -+ * @inheritdoc - */ - public function getIsUsedForPromoRules() - { -@@ -612,7 +617,7 @@ class Attribute extends \Magento\Eav\Model\Entity\Attribute implements - } - - /** -- * {@inheritdoc} -+ * @inheritdoc - */ - public function getIsVisibleOnFront() - { -@@ -620,7 +625,7 @@ class Attribute extends \Magento\Eav\Model\Entity\Attribute implements - } - - /** -- * {@inheritdoc} -+ * @inheritdoc - */ - public function getUsedInProductListing() - { -@@ -628,7 +633,7 @@ class Attribute extends \Magento\Eav\Model\Entity\Attribute implements - } - - /** -- * {@inheritdoc} -+ * @inheritdoc - */ - public function getIsVisible() - { -@@ -638,7 +643,7 @@ class Attribute extends \Magento\Eav\Model\Entity\Attribute implements - //@codeCoverageIgnoreEnd - - /** -- * {@inheritdoc} -+ * @inheritdoc - */ - public function getScope() - { -@@ -720,7 +725,7 @@ class Attribute extends \Magento\Eav\Model\Entity\Attribute implements - /** - * Set apply to value for the element - * -- * @param string []|string -+ * @param string[]|string $applyTo - * @return $this - */ - public function setApplyTo($applyTo) -@@ -829,7 +834,7 @@ class Attribute extends \Magento\Eav\Model\Entity\Attribute implements - } - - /** -- * {@inheritdoc} -+ * @inheritdoc - */ - public function afterDelete() - { -@@ -840,9 +845,14 @@ class Attribute extends \Magento\Eav\Model\Entity\Attribute implements - /** - * @inheritdoc - * @since 100.0.9 -+ * -+ * @SuppressWarnings(PHPMD.SerializationAware) -+ * @deprecated Do not use PHP serialization. - */ - public function __sleep() - { -+ trigger_error('Using PHP serialization is deprecated', E_USER_DEPRECATED); -+ - $this->unsetData('entity_type'); - return array_diff( - parent::__sleep(), -@@ -853,9 +863,14 @@ class Attribute extends \Magento\Eav\Model\Entity\Attribute implements - /** - * @inheritdoc - * @since 100.0.9 -+ * -+ * @SuppressWarnings(PHPMD.SerializationAware) -+ * @deprecated Do not use PHP serialization. - */ - public function __wakeup() - { -+ trigger_error('Using PHP serialization is deprecated', E_USER_DEPRECATED); -+ - parent::__wakeup(); - $objectManager = \Magento\Framework\App\ObjectManager::getInstance(); - $this->_indexerEavProcessor = $objectManager->get(\Magento\Catalog\Model\Indexer\Product\Flat\Processor::class); -diff --git a/app/code/Magento/Catalog/Model/ResourceModel/Layer/Filter/Price.php b/app/code/Magento/Catalog/Model/ResourceModel/Layer/Filter/Price.php -index 3699e29f820..585da2af529 100644 ---- a/app/code/Magento/Catalog/Model/ResourceModel/Layer/Filter/Price.php -+++ b/app/code/Magento/Catalog/Model/ResourceModel/Layer/Filter/Price.php -@@ -112,7 +112,7 @@ class Price extends \Magento\Framework\Model\ResourceModel\Db\AbstractDb - /** - * Check and set correct variable values to prevent SQL-injections - */ -- $range = floatval($range); -+ $range = (float)$range; - if ($range == 0) { - $range = 1; - } -diff --git a/app/code/Magento/Catalog/Model/ResourceModel/Product.php b/app/code/Magento/Catalog/Model/ResourceModel/Product.php -index d71ec238819..99a7efe6c98 100644 ---- a/app/code/Magento/Catalog/Model/ResourceModel/Product.php -+++ b/app/code/Magento/Catalog/Model/ResourceModel/Product.php -@@ -8,6 +8,9 @@ namespace Magento\Catalog\Model\ResourceModel; - use Magento\Catalog\Model\ResourceModel\Product\Website\Link as ProductWebsiteLink; - use Magento\Framework\App\ObjectManager; - use Magento\Catalog\Model\Indexer\Category\Product\TableMaintainer; -+use Magento\Eav\Model\Entity\Attribute\UniqueValidationInterface; -+use Magento\Framework\EntityManager\EntityManager; -+use Magento\Framework\Model\AbstractModel; - - /** - * Product entity resource model -@@ -43,7 +46,7 @@ class Product extends AbstractResource - /** - * Category collection factory - * -- * @var \Magento\Catalog\Model\ResourceModel\Category\CollectionFactory -+ * @var Category\CollectionFactory - */ - protected $_categoryCollectionFactory; - -@@ -63,7 +66,7 @@ class Product extends AbstractResource - protected $typeFactory; - - /** -- * @var \Magento\Framework\EntityManager\EntityManager -+ * @var EntityManager - * @since 101.0.0 - */ - protected $entityManager; -@@ -80,7 +83,7 @@ class Product extends AbstractResource - protected $availableCategoryIdsCache = []; - - /** -- * @var \Magento\Catalog\Model\ResourceModel\Product\CategoryLink -+ * @var Product\CategoryLink - */ - private $productCategoryLink; - -@@ -101,6 +104,7 @@ class Product extends AbstractResource - * @param \Magento\Catalog\Model\Product\Attribute\DefaultAttributes $defaultAttributes - * @param array $data - * @param TableMaintainer|null $tableMaintainer -+ * @param UniqueValidationInterface|null $uniqueValidator - * - * @SuppressWarnings(PHPMD.ExcessiveParameterList) - */ -@@ -108,14 +112,15 @@ class Product extends AbstractResource - \Magento\Eav\Model\Entity\Context $context, - \Magento\Store\Model\StoreManagerInterface $storeManager, - \Magento\Catalog\Model\Factory $modelFactory, -- \Magento\Catalog\Model\ResourceModel\Category\CollectionFactory $categoryCollectionFactory, -+ Category\CollectionFactory $categoryCollectionFactory, - Category $catalogCategory, - \Magento\Framework\Event\ManagerInterface $eventManager, - \Magento\Eav\Model\Entity\Attribute\SetFactory $setFactory, - \Magento\Eav\Model\Entity\TypeFactory $typeFactory, - \Magento\Catalog\Model\Product\Attribute\DefaultAttributes $defaultAttributes, - $data = [], -- TableMaintainer $tableMaintainer = null -+ TableMaintainer $tableMaintainer = null, -+ UniqueValidationInterface $uniqueValidator = null - ) { - $this->_categoryCollectionFactory = $categoryCollectionFactory; - $this->_catalogCategory = $catalogCategory; -@@ -127,7 +132,8 @@ class Product extends AbstractResource - $context, - $storeManager, - $modelFactory, -- $data -+ $data, -+ $uniqueValidator - ); - $this->connectionName = 'catalog'; - $this->tableMaintainer = $tableMaintainer ?: ObjectManager::getInstance()->get(TableMaintainer::class); -@@ -232,7 +238,7 @@ class Product extends AbstractResource - /** - * Retrieve product category identifiers - * -- * @param \Magento\Catalog\Model\Product $product -+ * @param \Magento\Catalog\Model\Product $product - * @return array - */ - public function getCategoryIds($product) -@@ -244,7 +250,7 @@ class Product extends AbstractResource - /** - * Get product identifier by sku - * -- * @param string $sku -+ * @param string $sku - * @return int|false - */ - public function getIdBySku($sku) -@@ -289,7 +295,7 @@ class Product extends AbstractResource - } - - /** -- * {@inheritdoc} -+ * @inheritdoc - */ - public function delete($object) - { -@@ -344,11 +350,11 @@ class Product extends AbstractResource - * Get collection of product categories - * - * @param \Magento\Catalog\Model\Product $product -- * @return \Magento\Catalog\Model\ResourceModel\Category\Collection -+ * @return Category\Collection - */ - public function getCategoryCollection($product) - { -- /** @var \Magento\Catalog\Model\ResourceModel\Category\Collection $collection */ -+ /** @var Category\Collection $collection */ - $collection = $this->_categoryCollectionFactory->create(); - $collection->joinField( - 'product_id', -@@ -424,18 +430,26 @@ class Product extends AbstractResource - /** - * Check availability display product in category - * -- * @param \Magento\Catalog\Model\Product $product -+ * @param \Magento\Catalog\Model\Product|int $product - * @param int $categoryId - * @return string - */ - public function canBeShowInCategory($product, $categoryId) - { -+ if ($product instanceof \Magento\Catalog\Model\Product) { -+ $productId = $product->getEntityId(); -+ $storeId = $product->getStoreId(); -+ } else { -+ $productId = $product; -+ $storeId = $this->_storeManager->getStore()->getId(); -+ } -+ - $select = $this->getConnection()->select()->from( -- $this->tableMaintainer->getMainTable($product->getStoreId()), -+ $this->tableMaintainer->getMainTable($storeId), - 'product_id' - )->where( - 'product_id = ?', -- (int)$product->getEntityId() -+ (int)$productId - )->where( - 'category_id = ?', - (int)$categoryId -@@ -593,7 +607,7 @@ class Product extends AbstractResource - } - - /** -- * {@inheritdoc} -+ * @inheritdoc - */ - public function validate($object) - { -@@ -610,7 +624,7 @@ class Product extends AbstractResource - /** - * Reset firstly loaded attributes - * -- * @param \Magento\Framework\Model\AbstractModel $object -+ * @param AbstractModel $object - * @param integer $entityId - * @param array|null $attributes - * @return $this -@@ -633,7 +647,7 @@ class Product extends AbstractResource - } - - /** -- * {@inheritdoc} -+ * @inheritdoc - * @SuppressWarnings(PHPMD.UnusedLocalVariable) - * @since 101.0.0 - */ -@@ -663,30 +677,34 @@ class Product extends AbstractResource - /** - * Save entity's attributes into the object's resource - * -- * @param \Magento\Framework\Model\AbstractModel $object -+ * @param AbstractModel $object - * @return $this - * @throws \Exception - * @since 101.0.0 - */ -- public function save(\Magento\Framework\Model\AbstractModel $object) -+ public function save(AbstractModel $object) - { - $this->getEntityManager()->save($object); - return $this; - } - - /** -- * @return \Magento\Framework\EntityManager\EntityManager -+ * Retrieve entity manager object -+ * -+ * @return EntityManager - */ - private function getEntityManager() - { - if (null === $this->entityManager) { -- $this->entityManager = \Magento\Framework\App\ObjectManager::getInstance() -- ->get(\Magento\Framework\EntityManager\EntityManager::class); -+ $this->entityManager = ObjectManager::getInstance() -+ ->get(EntityManager::class); - } - return $this->entityManager; - } - - /** -+ * Retrieve ProductWebsiteLink object -+ * - * @deprecated 101.1.0 - * @return ProductWebsiteLink - */ -@@ -696,23 +714,26 @@ class Product extends AbstractResource - } - - /** -+ * Retrieve CategoryLink object -+ * - * @deprecated 101.1.0 -- * @return \Magento\Catalog\Model\ResourceModel\Product\CategoryLink -+ * @return Product\CategoryLink - */ - private function getProductCategoryLink() - { - if (null === $this->productCategoryLink) { -- $this->productCategoryLink = \Magento\Framework\App\ObjectManager::getInstance() -- ->get(\Magento\Catalog\Model\ResourceModel\Product\CategoryLink::class); -+ $this->productCategoryLink = ObjectManager::getInstance() -+ ->get(Product\CategoryLink::class); - } - return $this->productCategoryLink; - } - - /** - * Extends parent method to be appropriate for product. -+ * - * Store id is required to correctly identify attribute value we are working with. - * -- * {@inheritdoc} -+ * @inheritdoc - * @since 101.1.0 - */ - protected function getAttributeRow($entity, $object, $attribute) -diff --git a/app/code/Magento/Catalog/Model/ResourceModel/Product/BaseSelectProcessorInterface.php b/app/code/Magento/Catalog/Model/ResourceModel/Product/BaseSelectProcessorInterface.php -index d97f6bebf4e..da3c4fb4417 100644 ---- a/app/code/Magento/Catalog/Model/ResourceModel/Product/BaseSelectProcessorInterface.php -+++ b/app/code/Magento/Catalog/Model/ResourceModel/Product/BaseSelectProcessorInterface.php -@@ -9,6 +9,7 @@ use Magento\Framework\DB\Select; - - /** - * Interface BaseSelectProcessorInterface -+ * - * @api - * @since 101.0.3 - */ -@@ -20,6 +21,8 @@ interface BaseSelectProcessorInterface - const PRODUCT_TABLE_ALIAS = 'child'; - - /** -+ * Process the select statement -+ * - * @param Select $select - * @return Select - * @since 101.0.3 -diff --git a/app/code/Magento/Catalog/Model/ResourceModel/Product/CategoryLink.php b/app/code/Magento/Catalog/Model/ResourceModel/Product/CategoryLink.php -index b54c19a1115..cf5760b0c33 100644 ---- a/app/code/Magento/Catalog/Model/ResourceModel/Product/CategoryLink.php -+++ b/app/code/Magento/Catalog/Model/ResourceModel/Product/CategoryLink.php -@@ -93,6 +93,8 @@ class CategoryLink - } - - /** -+ * Get category link metadata -+ * - * @return \Magento\Framework\EntityManager\EntityMetadataInterface - */ - private function getCategoryLinkMetadata() -@@ -114,16 +116,22 @@ class CategoryLink - private function processCategoryLinks($newCategoryPositions, &$oldCategoryPositions) - { - $result = ['changed' => [], 'updated' => []]; -+ -+ $oldCategoryPositions = array_values($oldCategoryPositions); - foreach ($newCategoryPositions as $newCategoryPosition) { -- $key = array_search( -- $newCategoryPosition['category_id'], -- array_column($oldCategoryPositions, 'category_id') -- ); -+ $key = false; -+ -+ foreach ($oldCategoryPositions as $oldKey => $oldCategoryPosition) { -+ if ((int)$oldCategoryPosition['category_id'] === (int)$newCategoryPosition['category_id']) { -+ $key = $oldKey; -+ break; -+ } -+ } - - if ($key === false) { - $result['changed'][] = $newCategoryPosition; - } elseif ($oldCategoryPositions[$key]['position'] != $newCategoryPosition['position']) { -- $result['updated'][] = $newCategoryPositions[$key]; -+ $result['updated'][] = $newCategoryPosition; - unset($oldCategoryPositions[$key]); - } - } -@@ -132,6 +140,8 @@ class CategoryLink - } - - /** -+ * Update category links -+ * - * @param ProductInterface $product - * @param array $insertLinks - * @param bool $insert -@@ -175,6 +185,8 @@ class CategoryLink - } - - /** -+ * Delete category links -+ * - * @param ProductInterface $product - * @param array $deleteLinks - * @return array -diff --git a/app/code/Magento/Catalog/Model/ResourceModel/Product/Collection.php b/app/code/Magento/Catalog/Model/ResourceModel/Product/Collection.php -index 4384effc4e3..dbd6a7a2e10 100644 ---- a/app/code/Magento/Catalog/Model/ResourceModel/Product/Collection.php -+++ b/app/code/Magento/Catalog/Model/ResourceModel/Product/Collection.php -@@ -7,20 +7,21 @@ - namespace Magento\Catalog\Model\ResourceModel\Product; - - use Magento\Catalog\Api\Data\ProductInterface; -+use Magento\Catalog\Model\Indexer\Category\Product\TableMaintainer; -+use Magento\Catalog\Model\Indexer\Product\Price\PriceTableResolver; - use Magento\Catalog\Model\Product\Attribute\Source\Status as ProductStatus; - use Magento\Catalog\Model\Product\Gallery\ReadHandler as GalleryReadHandler; - use Magento\Catalog\Model\ResourceModel\Product\Collection\ProductLimitationFactory; - use Magento\CatalogUrlRewrite\Model\ProductUrlRewriteGenerator; -+use Magento\CatalogUrlRewrite\Model\Storage\DbStorage; - use Magento\Customer\Api\GroupManagementInterface; - use Magento\Customer\Model\Indexer\CustomerGroupDimensionProvider; - use Magento\Framework\App\ObjectManager; - use Magento\Framework\DB\Select; - use Magento\Framework\EntityManager\MetadataPool; --use Magento\Catalog\Model\Indexer\Product\Price\PriceTableResolver; -+use Magento\Framework\Indexer\DimensionFactory; - use Magento\Store\Model\Indexer\WebsiteDimensionProvider; - use Magento\Store\Model\Store; --use Magento\Catalog\Model\Indexer\Category\Product\TableMaintainer; --use Magento\Framework\Indexer\DimensionFactory; - - /** - * Product collection -@@ -31,6 +32,7 @@ use Magento\Framework\Indexer\DimensionFactory; - * @SuppressWarnings(PHPMD.ExcessiveClassComplexity) - * @SuppressWarnings(PHPMD.NumberOfChildren) - * @SuppressWarnings(PHPMD.CouplingBetweenObjects) -+ * @SuppressWarnings(PHPMD.CookieAndSessionMisuse) - * @since 100.0.2 - */ - class Collection extends \Magento\Catalog\Model\ResourceModel\Collection\AbstractCollection -@@ -189,7 +191,7 @@ class Collection extends \Magento\Catalog\Model\ResourceModel\Collection\Abstrac - /** - * Catalog data - * -- * @var \Magento\Framework\Module\Manager -+ * @var \Magento\Framework\Module\ModuleManagerInterface - */ - protected $moduleManager = null; - -@@ -290,8 +292,19 @@ class Collection extends \Magento\Catalog\Model\ResourceModel\Collection\Abstrac - */ - private $dimensionFactory; - -+ /** -+ * @var \Magento\Framework\DataObject -+ */ -+ private $emptyItem; -+ -+ /** -+ * @var DbStorage -+ */ -+ private $urlFinder; -+ - /** - * Collection constructor -+ * - * @param \Magento\Framework\Data\Collection\EntityFactory $entityFactory - * @param \Psr\Log\LoggerInterface $logger - * @param \Magento\Framework\Data\Collection\Db\FetchStrategyInterface $fetchStrategy -@@ -302,7 +315,7 @@ class Collection extends \Magento\Catalog\Model\ResourceModel\Collection\Abstrac - * @param \Magento\Catalog\Model\ResourceModel\Helper $resourceHelper - * @param \Magento\Framework\Validator\UniversalFactory $universalFactory - * @param \Magento\Store\Model\StoreManagerInterface $storeManager -- * @param \Magento\Framework\Module\Manager $moduleManager -+ * @param \Magento\Framework\Module\ModuleManagerInterface $moduleManager - * @param \Magento\Catalog\Model\Indexer\Product\Flat\State $catalogProductFlatState - * @param \Magento\Framework\App\Config\ScopeConfigInterface $scopeConfig - * @param \Magento\Catalog\Model\Product\OptionFactory $productOptionFactory -@@ -317,6 +330,7 @@ class Collection extends \Magento\Catalog\Model\ResourceModel\Collection\Abstrac - * @param TableMaintainer|null $tableMaintainer - * @param PriceTableResolver|null $priceTableResolver - * @param DimensionFactory|null $dimensionFactory -+ * - * @SuppressWarnings(PHPMD.ExcessiveParameterList) - */ - public function __construct( -@@ -330,7 +344,7 @@ class Collection extends \Magento\Catalog\Model\ResourceModel\Collection\Abstrac - \Magento\Catalog\Model\ResourceModel\Helper $resourceHelper, - \Magento\Framework\Validator\UniversalFactory $universalFactory, - \Magento\Store\Model\StoreManagerInterface $storeManager, -- \Magento\Framework\Module\Manager $moduleManager, -+ \Magento\Framework\Module\ModuleManagerInterface $moduleManager, - \Magento\Catalog\Model\Indexer\Product\Flat\State $catalogProductFlatState, - \Magento\Framework\App\Config\ScopeConfigInterface $scopeConfig, - \Magento\Catalog\Model\Product\OptionFactory $productOptionFactory, -@@ -380,6 +394,19 @@ class Collection extends \Magento\Catalog\Model\ResourceModel\Collection\Abstrac - ?: ObjectManager::getInstance()->get(DimensionFactory::class); - } - -+ /** -+ * Retrieve urlFinder -+ * -+ * @return GalleryReadHandler -+ */ -+ private function getUrlFinder() -+ { -+ if ($this->urlFinder === null) { -+ $this->urlFinder = ObjectManager::getInstance()->get(DbStorage::class); -+ } -+ return $this->urlFinder; -+ } -+ - /** - * Get cloned Select after dispatching 'catalog_prepare_price_select' event - * -@@ -434,7 +461,7 @@ class Collection extends \Magento\Catalog\Model\ResourceModel\Collection\Abstrac - */ - public function getPriceExpression($select) - { -- //@todo: Add caching of price expresion -+ //@todo: Add caching of price expression - $this->_preparePriceExpressionParameters($select); - return $this->_priceExpression; - } -@@ -474,8 +501,7 @@ class Collection extends \Magento\Catalog\Model\ResourceModel\Collection\Abstrac - } - - /** -- * Retrieve is flat enabled flag -- * Return always false if magento run admin -+ * Retrieve is flat enabled. Return always false if magento run admin. - * - * @return bool - */ -@@ -506,8 +532,7 @@ class Collection extends \Magento\Catalog\Model\ResourceModel\Collection\Abstrac - } - - /** -- * Standard resource collection initialization -- * Needed for child classes -+ * Standard resource collection initialization. Needed for child classes. - * - * @param string $model - * @param string $entityModel -@@ -546,14 +571,16 @@ class Collection extends \Magento\Catalog\Model\ResourceModel\Collection\Abstrac - } - - /** -- * Retrieve collection empty item -- * Redeclared for specifying id field name without getting resource model inside model -+ * Get collection empty item. Redeclared for specifying id field name without getting resource model inside model. - * - * @return \Magento\Framework\DataObject - */ - public function getNewEmptyItem() - { -- $object = parent::getNewEmptyItem(); -+ if (null === $this->emptyItem) { -+ $this->emptyItem = parent::getNewEmptyItem(); -+ } -+ $object = clone $this->emptyItem; - if ($this->isEnabledFlat()) { - $object->setIdFieldName($this->getEntity()->getIdFieldName()); - } -@@ -633,8 +660,7 @@ class Collection extends \Magento\Catalog\Model\ResourceModel\Collection\Abstrac - } - - /** -- * Add attribute to entities in collection -- * If $attribute=='*' select all attributes -+ * Add attribute to entities in collection. If $attribute=='*' select all attributes. - * - * @param array|string|integer|\Magento\Framework\App\Config\Element $attribute - * @param bool|string $joinType -@@ -670,8 +696,7 @@ class Collection extends \Magento\Catalog\Model\ResourceModel\Collection\Abstrac - } - - /** -- * Processing collection items after loading -- * Adding url rewrites, minimal prices, final prices, tax percents -+ * Processing collection items after loading. Adding url rewrites, minimal prices, final prices, tax percents. - * - * @return $this - */ -@@ -682,6 +707,7 @@ class Collection extends \Magento\Catalog\Model\ResourceModel\Collection\Abstrac - } - - $this->_prepareUrlDataObject(); -+ $this->prepareStoreId(); - - if (count($this)) { - $this->_eventManager->dispatch('catalog_product_collection_load_after', ['collection' => $this]); -@@ -690,6 +716,23 @@ class Collection extends \Magento\Catalog\Model\ResourceModel\Collection\Abstrac - return $this; - } - -+ /** -+ * Add Store ID to products from collection. -+ * -+ * @return $this -+ */ -+ protected function prepareStoreId() -+ { -+ if ($this->getStoreId() !== null) { -+ /** @var $item \Magento\Catalog\Model\Product */ -+ foreach ($this->_items as $item) { -+ $item->setStoreId($this->getStoreId()); -+ } -+ } -+ -+ return $this; -+ } -+ - /** - * Prepare Url Data object - * -@@ -756,8 +799,7 @@ class Collection extends \Magento\Catalog\Model\ResourceModel\Collection\Abstrac - } - - /** -- * Adding product website names to result collection -- * Add for each product websites information -+ * Adding product website names to result collection. Add for each product websites information. - * - * @return $this - */ -@@ -768,7 +810,7 @@ class Collection extends \Magento\Catalog\Model\ResourceModel\Collection\Abstrac - } - - /** -- * {@inheritdoc} -+ * @inheritdoc - */ - public function load($printQuery = false, $logQuery = false) - { -@@ -784,7 +826,7 @@ class Collection extends \Magento\Catalog\Model\ResourceModel\Collection\Abstrac - } - - /** -- * Processs adding product website names to result collection -+ * Process adding product website names to result collection - * - * @return $this - */ -@@ -819,14 +861,14 @@ class Collection extends \Magento\Catalog\Model\ResourceModel\Collection\Abstrac - foreach ($this as $product) { - if (isset($productWebsites[$product->getId()])) { - $product->setData('websites', $productWebsites[$product->getId()]); -+ $product->setData('website_ids', $productWebsites[$product->getId()]); - } - } - return $this; - } - - /** -- * Add store availability filter. Include availability product -- * for store website -+ * Add store availability filter. Include availability product for store website. - * - * @param null|string|bool|int|Store $store - * @return $this -@@ -1115,11 +1157,11 @@ class Collection extends \Magento\Catalog\Model\ResourceModel\Collection\Abstrac - /** - * Get SQL for get record count - * -- * @param null $select -+ * @param Select $select - * @param bool $resetLeftJoins -- * @return \Magento\Framework\DB\Select -+ * @return Select - */ -- protected function _getSelectCountSql($select = null, $resetLeftJoins = true) -+ protected function _getSelectCountSql(?Select $select = null, $resetLeftJoins = true) - { - $this->_renderFilters(); - $countSelect = $select === null ? $this->_getClearSelect() : $this->_buildClearSelect($select); -@@ -1357,8 +1399,7 @@ class Collection extends \Magento\Catalog\Model\ResourceModel\Collection\Abstrac - } - - /** -- * Add URL rewrites data to product -- * If collection loadded - run processing else set flag -+ * Add URL rewrites data to product. If collection loadded - run processing else set flag. - * - * @param int|string $categoryId - * @return $this -@@ -1395,39 +1436,21 @@ class Collection extends \Magento\Catalog\Model\ResourceModel\Collection\Abstrac - foreach ($this->getItems() as $item) { - $productIds[] = $item->getEntityId(); - } -- if (!$productIds) { -- return; -- } -- -- $select = $this->getConnection() -- ->select() -- ->from(['u' => $this->getTable('url_rewrite')], ['u.entity_id', 'u.request_path']) -- ->where('u.store_id = ?', $this->_storeManager->getStore($this->getStoreId())->getId()) -- ->where('u.is_autogenerated = 1') -- ->where('u.entity_type = ?', ProductUrlRewriteGenerator::ENTITY_TYPE) -- ->where('u.entity_id IN(?)', $productIds); - -+ $filter = [ -+ 'entity_type' => 'product', -+ 'entity_id' => $productIds, -+ 'store_id' => $this->getStoreId(), -+ 'is_autogenerated' => 1 -+ ]; - if ($this->_urlRewriteCategory) { -- $select->joinInner( -- ['cu' => $this->getTable('catalog_url_rewrite_product_category')], -- 'u.url_rewrite_id=cu.url_rewrite_id' -- )->where('cu.category_id IN (?)', $this->_urlRewriteCategory); -- } -- -- // more priority is data with category id -- $urlRewrites = []; -- -- foreach ($this->getConnection()->fetchAll($select) as $row) { -- if (!isset($urlRewrites[$row['entity_id']])) { -- $urlRewrites[$row['entity_id']] = $row['request_path']; -- } -+ $filter['metadata']['category_id'] = $this->_urlRewriteCategory; - } - -- foreach ($this->getItems() as $item) { -- if (isset($urlRewrites[$item->getEntityId()])) { -- $item->setData('request_path', $urlRewrites[$item->getEntityId()]); -- } else { -- $item->setData('request_path', false); -+ $rewrites = $this->getUrlFinder()->findAllByData($filter); -+ foreach ($rewrites as $rewrite) { -+ if ($item = $this->getItemById($rewrite->getEntityId())) { -+ $item->setData('request_path', $rewrite->getRequestPath()); - } - } - } -@@ -1518,7 +1541,7 @@ class Collection extends \Magento\Catalog\Model\ResourceModel\Collection\Abstrac - /** - * Add attribute to filter - * -- * @param \Magento\Eav\Model\Entity\Attribute\AbstractAttribute|string $attribute -+ * @param \Magento\Eav\Model\Entity\Attribute\AbstractAttribute|string|array $attribute - * @param array $condition - * @param string $joinType - * @return $this -@@ -1555,33 +1578,17 @@ class Collection extends \Magento\Catalog\Model\ResourceModel\Collection\Abstrac - $this->_allIdsCache = null; - - if (is_string($attribute) && $attribute == 'is_saleable') { -- $columns = $this->getSelect()->getPart(\Magento\Framework\DB\Select::COLUMNS); -- foreach ($columns as $columnEntry) { -- list($correlationName, $column, $alias) = $columnEntry; -- if ($alias == 'is_saleable') { -- if ($column instanceof \Zend_Db_Expr) { -- $field = $column; -- } else { -- $connection = $this->getSelect()->getConnection(); -- if (empty($correlationName)) { -- $field = $connection->quoteColumnAs($column, $alias, true); -- } else { -- $field = $connection->quoteColumnAs([$correlationName, $column], $alias, true); -- } -- } -- $this->getSelect()->where("{$field} = ?", $condition); -- break; -- } -- } -- -- return $this; -+ $this->addIsSaleableAttributeToFilter($condition); -+ } elseif (is_string($attribute) && $attribute == 'tier_price') { -+ $this->addTierPriceAttributeToFilter($attribute, $condition); - } else { - return parent::addAttributeToFilter($attribute, $condition, $joinType); - } - } - - /** -- * {@inheritdoc} -+ * @inheritdoc -+ * - * @since 101.0.0 - */ - protected function getEntityPkName(\Magento\Eav\Model\Entity\AbstractEntity $entity) -@@ -1792,7 +1799,8 @@ class Collection extends \Magento\Catalog\Model\ResourceModel\Collection\Abstrac - } - $conditions[] = $this->getConnection()->quoteInto( - 'product_website.website_id IN(?)', -- $filters['website_ids'] -+ $filters['website_ids'], -+ 'int' - ); - } elseif (isset( - $filters['store_id'] -@@ -1804,7 +1812,7 @@ class Collection extends \Magento\Catalog\Model\ResourceModel\Collection\Abstrac - ) { - $joinWebsite = true; - $websiteId = $this->_storeManager->getStore($filters['store_id'])->getWebsiteId(); -- $conditions[] = $this->getConnection()->quoteInto('product_website.website_id = ?', $websiteId); -+ $conditions[] = $this->getConnection()->quoteInto('product_website.website_id = ?', $websiteId, 'int'); - } - - $fromPart = $this->getSelect()->getPart(\Magento\Framework\DB\Select::FROM); -@@ -1947,7 +1955,7 @@ class Collection extends \Magento\Catalog\Model\ResourceModel\Collection\Abstrac - } - // Set additional field filters - foreach ($this->_priceDataFieldFilters as $filterData) { -- $select->where(call_user_func_array('sprintf', $filterData)); -+ $select->where(sprintf(...$filterData)); - } - } else { - $fromPart['price_index']['joinCondition'] = $joinCond; -@@ -2000,12 +2008,16 @@ class Collection extends \Magento\Catalog\Model\ResourceModel\Collection\Abstrac - - $conditions = [ - 'cat_index.product_id=e.entity_id', -- $this->getConnection()->quoteInto('cat_index.store_id=?', $filters['store_id']), -+ $this->getConnection()->quoteInto('cat_index.store_id=?', $filters['store_id'], 'int'), - ]; - if (isset($filters['visibility']) && !isset($filters['store_table'])) { -- $conditions[] = $this->getConnection()->quoteInto('cat_index.visibility IN(?)', $filters['visibility']); -+ $conditions[] = $this->getConnection()->quoteInto( -+ 'cat_index.visibility IN(?)', -+ $filters['visibility'], -+ 'int' -+ ); - } -- $conditions[] = $this->getConnection()->quoteInto('cat_index.category_id=?', $filters['category_id']); -+ $conditions[] = $this->getConnection()->quoteInto('cat_index.category_id=?', $filters['category_id'], 'int'); - if (isset($filters['category_is_anchor'])) { - $conditions[] = $this->getConnection()->quoteInto('cat_index.is_parent=?', $filters['category_is_anchor']); - } -@@ -2188,7 +2200,7 @@ class Collection extends \Magento\Catalog\Model\ResourceModel\Collection\Abstrac - $this->getLinkField() . ' IN(?)', - $productIds - )->order( -- $this->getLinkField() -+ 'qty' - ); - return $select; - } -@@ -2248,7 +2260,7 @@ class Collection extends \Magento\Catalog\Model\ResourceModel\Collection\Abstrac - public function addPriceDataFieldFilter($comparisonFormat, $fields) - { - if (!preg_match('/^%s( (<|>|=|<=|>=|<>) %s)*$/', $comparisonFormat)) { -- throw new \Exception('Invalid comparison format.'); -+ throw new \InvalidArgumentException('Invalid comparison format.'); - } - - if (!is_array($fields)) { -@@ -2343,7 +2355,10 @@ class Collection extends \Magento\Catalog\Model\ResourceModel\Collection\Abstrac - } - - /** -+ * Retrieve Media gallery resource. -+ * - * @deprecated 101.0.1 -+ * - * @return \Magento\Catalog\Model\ResourceModel\Product\Gallery - */ - private function getMediaGalleryResource() -@@ -2449,4 +2464,71 @@ class Collection extends \Magento\Catalog\Model\ResourceModel\Collection\Abstrac - - return $this->_pricesCount; - } -+ -+ /** -+ * Add is_saleable attribute to filter -+ * -+ * @param array|null $condition -+ * @return $this -+ */ -+ private function addIsSaleableAttributeToFilter(?array $condition): self -+ { -+ $columns = $this->getSelect()->getPart(Select::COLUMNS); -+ foreach ($columns as $columnEntry) { -+ list($correlationName, $column, $alias) = $columnEntry; -+ if ($alias == 'is_saleable') { -+ if ($column instanceof \Zend_Db_Expr) { -+ $field = $column; -+ } else { -+ $connection = $this->getSelect()->getConnection(); -+ if (empty($correlationName)) { -+ $field = $connection->quoteColumnAs($column, $alias, true); -+ } else { -+ $field = $connection->quoteColumnAs([$correlationName, $column], $alias, true); -+ } -+ } -+ $this->getSelect()->where("{$field} = ?", $condition); -+ break; -+ } -+ } -+ -+ return $this; -+ } -+ -+ /** -+ * Add tier price attribute to filter -+ * -+ * @param string $attribute -+ * @param array|null $condition -+ * @return $this -+ */ -+ private function addTierPriceAttributeToFilter(string $attribute, ?array $condition): self -+ { -+ $attrCode = $attribute; -+ $connection = $this->getConnection(); -+ $attrTable = $this->_getAttributeTableAlias($attrCode); -+ $entity = $this->getEntity(); -+ $fKey = 'e.' . $this->getEntityPkName($entity); -+ $pKey = $attrTable . '.' . $this->getEntityPkName($entity); -+ $attribute = $entity->getAttribute($attrCode); -+ $attrFieldName = $attrTable . '.value'; -+ $fKey = $connection->quoteColumnAs($fKey, null); -+ $pKey = $connection->quoteColumnAs($pKey, null); -+ -+ $condArr = ["{$pKey} = {$fKey}"]; -+ $this->getSelect()->join( -+ [$attrTable => $this->getTable('catalog_product_entity_tier_price')], -+ '(' . implode(') AND (', $condArr) . ')', -+ [$attrCode => $attrFieldName] -+ ); -+ $this->removeAttributeToSelect($attrCode); -+ $this->_filterAttributes[$attrCode] = $attribute->getId(); -+ $this->_joinFields[$attrCode] = ['table' => '', 'field' => $attrFieldName]; -+ $field = $this->_getAttributeTableAlias($attrCode) . '.value'; -+ $conditionSql = $this->_getConditionSql($field, $condition); -+ $this->getSelect()->where($conditionSql, null, Select::TYPE_CONDITION); -+ $this->_totalRecords = null; -+ -+ return $this; -+ } - } -diff --git a/app/code/Magento/Catalog/Model/ResourceModel/Product/Compare/Item/Collection.php b/app/code/Magento/Catalog/Model/ResourceModel/Product/Compare/Item/Collection.php -index 7c78dbca5a0..dc3411743a0 100644 ---- a/app/code/Magento/Catalog/Model/ResourceModel/Product/Compare/Item/Collection.php -+++ b/app/code/Magento/Catalog/Model/ResourceModel/Product/Compare/Item/Collection.php -@@ -12,6 +12,7 @@ namespace Magento\Catalog\Model\ResourceModel\Product\Compare\Item; - * @author Magento Core Team <core@magentocommerce.com> - * @SuppressWarnings(PHPMD.LongVariable) - * @SuppressWarnings(PHPMD.CouplingBetweenObjects) -+ * @SuppressWarnings(PHPMD.CookieAndSessionMisuse) - * @since 100.0.2 - */ - class Collection extends \Magento\Catalog\Model\ResourceModel\Product\Collection -@@ -63,7 +64,7 @@ class Collection extends \Magento\Catalog\Model\ResourceModel\Product\Collection - * @param \Magento\Catalog\Model\ResourceModel\Helper $resourceHelper - * @param \Magento\Framework\Validator\UniversalFactory $universalFactory - * @param \Magento\Store\Model\StoreManagerInterface $storeManager -- * @param \Magento\Framework\Module\Manager $moduleManager -+ * @param \Magento\Framework\Module\ModuleManagerInterface $moduleManager - * @param \Magento\Catalog\Model\Indexer\Product\Flat\State $catalogProductFlatState - * @param \Magento\Framework\App\Config\ScopeConfigInterface $scopeConfig - * @param \Magento\Catalog\Model\Product\OptionFactory $productOptionFactory -@@ -75,7 +76,6 @@ class Collection extends \Magento\Catalog\Model\ResourceModel\Product\Collection - * @param \Magento\Catalog\Model\ResourceModel\Product\Compare\Item $catalogProductCompareItem - * @param \Magento\Catalog\Helper\Product\Compare $catalogProductCompare - * @param \Magento\Framework\DB\Adapter\AdapterInterface $connection -- * - * @SuppressWarnings(PHPMD.ExcessiveParameterList) - */ - public function __construct( -@@ -89,7 +89,7 @@ class Collection extends \Magento\Catalog\Model\ResourceModel\Product\Collection - \Magento\Catalog\Model\ResourceModel\Helper $resourceHelper, - \Magento\Framework\Validator\UniversalFactory $universalFactory, - \Magento\Store\Model\StoreManagerInterface $storeManager, -- \Magento\Framework\Module\Manager $moduleManager, -+ \Magento\Framework\Module\ModuleManagerInterface $moduleManager, - \Magento\Catalog\Model\Indexer\Product\Flat\State $catalogProductFlatState, - \Magento\Framework\App\Config\ScopeConfigInterface $scopeConfig, - \Magento\Catalog\Model\Product\OptionFactory $productOptionFactory, -@@ -403,6 +403,7 @@ class Collection extends \Magento\Catalog\Model\ResourceModel\Product\Collection - - /** - * Retrieve is flat enabled flag -+ * - * Overwrite disable flat for compared item if required EAV resource - * - * @return bool -diff --git a/app/code/Magento/Catalog/Model/ResourceModel/Product/Gallery.php b/app/code/Magento/Catalog/Model/ResourceModel/Product/Gallery.php -index 2868392f852..a9741cd8e1e 100644 ---- a/app/code/Magento/Catalog/Model/ResourceModel/Product/Gallery.php -+++ b/app/code/Magento/Catalog/Model/ResourceModel/Product/Gallery.php -@@ -3,6 +3,7 @@ - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -+ - namespace Magento\Catalog\Model\ResourceModel\Product; - - use Magento\Store\Model\Store; -@@ -49,7 +50,8 @@ class Gallery extends \Magento\Framework\Model\ResourceModel\Db\AbstractDb - } - - /** -- * {@inheritdoc} -+ * @inheritdoc -+ * - * @since 101.0.0 - */ - protected function _construct() -@@ -58,7 +60,8 @@ class Gallery extends \Magento\Framework\Model\ResourceModel\Db\AbstractDb - } - - /** -- * {@inheritdoc} -+ * @inheritdoc -+ * - * @since 101.0.0 - */ - public function getConnection() -@@ -67,6 +70,8 @@ class Gallery extends \Magento\Framework\Model\ResourceModel\Db\AbstractDb - } - - /** -+ * Load data from table by valueId -+ * - * @param string $tableNameAlias - * @param array $ids - * @param int|null $storeId -@@ -111,6 +116,8 @@ class Gallery extends \Magento\Framework\Model\ResourceModel\Db\AbstractDb - } - - /** -+ * Load product gallery by attributeId -+ * - * @param \Magento\Catalog\Model\Product $product - * @param int $attributeId - * @return array -@@ -132,6 +139,8 @@ class Gallery extends \Magento\Framework\Model\ResourceModel\Db\AbstractDb - } - - /** -+ * Create base load select -+ * - * @param int $entityId - * @param int $storeId - * @param int $attributeId -@@ -141,7 +150,7 @@ class Gallery extends \Magento\Framework\Model\ResourceModel\Db\AbstractDb - */ - protected function createBaseLoadSelect($entityId, $storeId, $attributeId) - { -- $select = $this->createBatchBaseSelect($storeId, $attributeId); -+ $select = $this->createBatchBaseSelect($storeId, $attributeId); - - $select = $select->where( - 'entity.' . $this->metadata->getLinkField() . ' = ?', -@@ -151,6 +160,8 @@ class Gallery extends \Magento\Framework\Model\ResourceModel\Db\AbstractDb - } - - /** -+ * Create batch base select -+ * - * @param int $storeId - * @param int $attributeId - * @return \Magento\Framework\DB\Select -@@ -190,7 +201,7 @@ class Gallery extends \Magento\Framework\Model\ResourceModel\Db\AbstractDb - 'value.' . $linkField . ' = entity.' . $linkField, - ] - ), -- ['label', 'position', 'disabled'] -+ [] - )->joinLeft( - ['default_value' => $this->getTable(self::GALLERY_VALUE_TABLE)], - implode( -@@ -201,8 +212,15 @@ class Gallery extends \Magento\Framework\Model\ResourceModel\Db\AbstractDb - 'default_value.' . $linkField . ' = entity.' . $linkField, - ] - ), -- ['label_default' => 'label', 'position_default' => 'position', 'disabled_default' => 'disabled'] -- )->where( -+ [] -+ )->columns([ -+ 'label' => $this->getConnection()->getIfNullSql('`value`.`label`', '`default_value`.`label`'), -+ 'position' => $this->getConnection()->getIfNullSql('`value`.`position`', '`default_value`.`position`'), -+ 'disabled' => $this->getConnection()->getIfNullSql('`value`.`disabled`', '`default_value`.`disabled`'), -+ 'label_default' => 'default_value.label', -+ 'position_default' => 'default_value.position', -+ 'disabled_default' => 'default_value.disabled' -+ ])->where( - $mainTableAlias . '.attribute_id = ?', - $attributeId - )->where( -@@ -240,6 +258,8 @@ class Gallery extends \Magento\Framework\Model\ResourceModel\Db\AbstractDb - } - - /** -+ * Get main table alias -+ * - * @return string - * @since 101.0.0 - */ -@@ -249,6 +269,8 @@ class Gallery extends \Magento\Framework\Model\ResourceModel\Db\AbstractDb - } - - /** -+ * Bind value to entity -+ * - * @param int $valueId - * @param int $entityId - * @return int -@@ -266,6 +288,8 @@ class Gallery extends \Magento\Framework\Model\ResourceModel\Db\AbstractDb - } - - /** -+ * Save data row -+ * - * @param string $table - * @param array $data - * @param array $fields -@@ -355,9 +379,9 @@ class Gallery extends \Magento\Framework\Model\ResourceModel\Db\AbstractDb - $conditions = implode( - ' AND ', - [ -- $this->getConnection()->quoteInto('value_id = ?', (int) $valueId), -- $this->getConnection()->quoteInto($this->metadata->getLinkField() . ' = ?', (int) $entityId), -- $this->getConnection()->quoteInto('store_id = ?', (int) $storeId) -+ $this->getConnection()->quoteInto('value_id = ?', (int)$valueId), -+ $this->getConnection()->quoteInto($this->metadata->getLinkField() . ' = ?', (int)$entityId), -+ $this->getConnection()->quoteInto('store_id = ?', (int)$storeId) - ] - ); - -@@ -385,7 +409,7 @@ class Gallery extends \Magento\Framework\Model\ResourceModel\Db\AbstractDb - - $select = $this->getConnection()->select()->from( - [$this->getMainTableAlias() => $this->getMainTable()], -- ['value_id', 'value'] -+ ['value_id', 'value', 'media_type', 'disabled'] - )->joinInner( - ['entity' => $this->getTable(self::GALLERY_VALUE_TO_ENTITY_TABLE)], - $this->getMainTableAlias() . '.value_id = entity.value_id', -@@ -402,16 +426,16 @@ class Gallery extends \Magento\Framework\Model\ResourceModel\Db\AbstractDb - - // 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 123f358be40..0f324194b7f 100644 ---- a/app/code/Magento/Catalog/Model/ResourceModel/Product/Image.php -+++ b/app/code/Magento/Catalog/Model/ResourceModel/Product/Image.php -@@ -12,6 +12,9 @@ use Magento\Framework\DB\Query\Generator; - use Magento\Framework\DB\Select; - use Magento\Framework\App\ResourceConnection; - -+/** -+ * Class for retrieval of all product images -+ */ - class Image - { - /** -@@ -51,7 +54,7 @@ class Image - } - - /** -- * Returns product images -+ * Get all product images. - * - * @return \Generator - */ -@@ -72,16 +75,63 @@ class Image - } - - /** -- * Get the number of unique pictures of products -+ * Get used product images. -+ * -+ * @return \Generator -+ */ -+ public function getUsedProductImages(): \Generator -+ { -+ $batchSelectIterator = $this->batchQueryGenerator->generate( -+ 'value_id', -+ $this->getUsedImagesSelect(), -+ $this->batchSize, -+ \Magento\Framework\DB\Query\BatchIteratorInterface::NON_UNIQUE_FIELD_ITERATOR -+ ); -+ -+ foreach ($batchSelectIterator as $select) { -+ foreach ($this->connection->fetchAll($select) as $key => $value) { -+ yield $key => $value; -+ } -+ } -+ } -+ -+ /** -+ * Get the number of unique images 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); - } - - /** -+ * Get the number of unique and used images of products. -+ * -+ * @return int -+ */ -+ public function getCountUsedProductImages(): int -+ { -+ $select = $this->getUsedImagesSelect() -+ ->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 -@@ -94,4 +144,24 @@ class Image - 'disabled = 0' - ); - } -+ -+ /** -+ * Return select to fetch all used product images. -+ * -+ * @return Select -+ */ -+ private function getUsedImagesSelect(): Select -+ { -+ return $this->connection->select()->distinct() -+ ->from( -+ ['images' => $this->resourceConnection->getTableName(Gallery::GALLERY_TABLE)], -+ 'value as filepath' -+ )->joinInner( -+ ['image_value' => $this->resourceConnection->getTableName(Gallery::GALLERY_VALUE_TABLE)], -+ 'images.value_id = image_value.value_id', -+ [] -+ )->where( -+ 'images.disabled = 0 AND image_value.disabled = 0' -+ ); -+ } - } -diff --git a/app/code/Magento/Catalog/Model/ResourceModel/Product/Indexer/Eav/AbstractEav.php b/app/code/Magento/Catalog/Model/ResourceModel/Product/Indexer/Eav/AbstractEav.php -index c33ea7c781a..e024f0d30f1 100644 ---- a/app/code/Magento/Catalog/Model/ResourceModel/Product/Indexer/Eav/AbstractEav.php -+++ b/app/code/Magento/Catalog/Model/ResourceModel/Product/Indexer/Eav/AbstractEav.php -@@ -24,13 +24,11 @@ abstract class AbstractEav extends \Magento\Catalog\Model\ResourceModel\Product\ - protected $_eventManager = null; - - /** -- * AbstractEav constructor. - * @param \Magento\Framework\Model\ResourceModel\Db\Context $context - * @param \Magento\Framework\Indexer\Table\StrategyInterface $tableStrategy - * @param \Magento\Eav\Model\Config $eavConfig - * @param \Magento\Framework\Event\ManagerInterface $eventManager -- * @param null $connectionName -- * @param \Magento\Indexer\Model\Indexer\StateFactory|null $stateFactory -+ * @param string $connectionName - */ - public function __construct( - \Magento\Framework\Model\ResourceModel\Db\Context $context, -@@ -70,7 +68,6 @@ abstract class AbstractEav extends \Magento\Catalog\Model\ResourceModel\Product\ - /** - * Rebuild index data by entities - * -- * - * @param int|array $processIds - * @return $this - * @throws \Exception -@@ -88,8 +85,8 @@ abstract class AbstractEav extends \Magento\Catalog\Model\ResourceModel\Product\ - - /** - * Rebuild index data by attribute id -- * If attribute is not indexable remove data by attribute - * -+ * If attribute is not indexable remove data by attribute - * - * @param int $attributeId - * @param bool $isIndexable -@@ -245,7 +242,8 @@ abstract class AbstractEav extends \Magento\Catalog\Model\ResourceModel\Product\ - - /** - * Retrieve condition for retrieve indexable attribute select -- * the catalog/eav_attribute table must have alias is ca -+ * -+ * The catalog/eav_attribute table must have alias is ca - * - * @return string - */ -diff --git a/app/code/Magento/Catalog/Model/ResourceModel/Product/Indexer/Eav/Source.php b/app/code/Magento/Catalog/Model/ResourceModel/Product/Indexer/Eav/Source.php -index 5b68730209b..7730d7cc9a7 100644 ---- a/app/code/Magento/Catalog/Model/ResourceModel/Product/Indexer/Eav/Source.php -+++ b/app/code/Magento/Catalog/Model/ResourceModel/Product/Indexer/Eav/Source.php -@@ -7,6 +7,7 @@ namespace Magento\Catalog\Model\ResourceModel\Product\Indexer\Eav; - - use Magento\Catalog\Model\Product\Attribute\Source\Status as ProductStatus; - use Magento\Catalog\Api\Data\ProductInterface; -+use Magento\Catalog\Api\Data\ProductAttributeInterface; - - /** - * Catalog Product Eav Select and Multiply Select Attributes Indexer resource model -@@ -24,6 +25,16 @@ class Source extends AbstractEav - */ - protected $_resourceHelper; - -+ /** -+ * @var \Magento\Eav\Api\AttributeRepositoryInterface -+ */ -+ private $attributeRepository; -+ -+ /** -+ * @var \Magento\Framework\Api\SearchCriteriaBuilder -+ */ -+ private $criteriaBuilder; -+ - /** - * Construct - * -@@ -33,6 +44,8 @@ class Source extends AbstractEav - * @param \Magento\Framework\Event\ManagerInterface $eventManager - * @param \Magento\Catalog\Model\ResourceModel\Helper $resourceHelper - * @param null|string $connectionName -+ * @param \Magento\Eav\Api\AttributeRepositoryInterface|null $attributeRepository -+ * @param \Magento\Framework\Api\SearchCriteriaBuilder|null $criteriaBuilder - */ - public function __construct( - \Magento\Framework\Model\ResourceModel\Db\Context $context, -@@ -40,7 +53,9 @@ class Source extends AbstractEav - \Magento\Eav\Model\Config $eavConfig, - \Magento\Framework\Event\ManagerInterface $eventManager, - \Magento\Catalog\Model\ResourceModel\Helper $resourceHelper, -- $connectionName = null -+ $connectionName = null, -+ \Magento\Eav\Api\AttributeRepositoryInterface $attributeRepository = null, -+ \Magento\Framework\Api\SearchCriteriaBuilder $criteriaBuilder = null - ) { - parent::__construct( - $context, -@@ -50,6 +65,12 @@ class Source extends AbstractEav - $connectionName - ); - $this->_resourceHelper = $resourceHelper; -+ $this->attributeRepository = $attributeRepository -+ ?: \Magento\Framework\App\ObjectManager::getInstance() -+ ->get(\Magento\Eav\Api\AttributeRepositoryInterface::class); -+ $this->criteriaBuilder = $criteriaBuilder -+ ?: \Magento\Framework\App\ObjectManager::getInstance() -+ ->get(\Magento\Framework\Api\SearchCriteriaBuilder::class); - } - - /** -@@ -84,7 +105,7 @@ class Source extends AbstractEav - if ($multiSelect == true) { - $select->where('ea.backend_type = ?', 'varchar')->where('ea.frontend_input = ?', 'multiselect'); - } else { -- $select->where('ea.backend_type = ?', 'int')->where('ea.frontend_input = ?', 'select'); -+ $select->where('ea.backend_type = ?', 'int')->where('ea.frontend_input IN( ? )', ['select', 'boolean']); - } - - return $this->getConnection()->fetchCol($select); -@@ -234,6 +255,10 @@ class Source extends AbstractEav - $options[$row['attribute_id']][$row['option_id']] = true; - } - -+ // Retrieve any custom source model options -+ $sourceModelOptions = $this->getMultiSelectAttributeWithSourceModels($attrIds); -+ $options = array_replace_recursive($options, $sourceModelOptions); -+ - // prepare get multiselect values query - $productValueExpression = $connection->getCheckSql('pvs.value_id > 0', 'pvs.value', 'pvd.value'); - $select = $connection->select()->from( -@@ -297,6 +322,39 @@ class Source extends AbstractEav - return $this; - } - -+ /** -+ * Get options for multiselect attributes using custom source models -+ * Based on @maderlock's fix from: -+ * https://github.com/magento/magento2/issues/417#issuecomment-265146285 -+ * -+ * @param array $attrIds -+ * -+ * @return array -+ */ -+ private function getMultiSelectAttributeWithSourceModels($attrIds) -+ { -+ // Add options from custom source models -+ $this->criteriaBuilder -+ ->addFilter('attribute_id', $attrIds, 'in') -+ ->addFilter('source_model', true, 'notnull'); -+ $criteria = $this->criteriaBuilder->create(); -+ $attributes = $this->attributeRepository->getList( -+ ProductAttributeInterface::ENTITY_TYPE_CODE, -+ $criteria -+ )->getItems(); -+ -+ $options = []; -+ foreach ($attributes as $attribute) { -+ $sourceModelOptions = $attribute->getOptions(); -+ // Add options to list used below -+ foreach ($sourceModelOptions as $option) { -+ $options[$attribute->getAttributeId()][$option->getValue()] = true; -+ } -+ } -+ -+ return $options; -+ } -+ - /** - * Save a data to temporary source index table - * -@@ -330,6 +388,8 @@ class Source extends AbstractEav - } - - /** -+ * Save data from select -+ * - * @param \Magento\Framework\DB\Select $select - * @param array $options - * @return void -diff --git a/app/code/Magento/Catalog/Model/ResourceModel/Product/Indexer/Price/CustomOptionPriceModifier.php b/app/code/Magento/Catalog/Model/ResourceModel/Product/Indexer/Price/CustomOptionPriceModifier.php -index 47fc6802d7e..463da8762b7 100644 ---- a/app/code/Magento/Catalog/Model/ResourceModel/Product/Indexer/Price/CustomOptionPriceModifier.php -+++ b/app/code/Magento/Catalog/Model/ResourceModel/Product/Indexer/Price/CustomOptionPriceModifier.php -@@ -127,6 +127,8 @@ class CustomOptionPriceModifier implements PriceModifierInterface - } - - /** -+ * Check if custom options exist. -+ * - * @param IndexTableStructure $priceTable - * @return bool - * @throws \Exception -@@ -154,6 +156,8 @@ class CustomOptionPriceModifier implements PriceModifierInterface - } - - /** -+ * Get connection. -+ * - * @return \Magento\Framework\DB\Adapter\AdapterInterface - */ - private function getConnection() -@@ -211,7 +215,7 @@ class CustomOptionPriceModifier implements PriceModifierInterface - } else { - $select->joinLeft( - ['otps' => $this->getTable('catalog_product_option_type_price')], -- 'otps.option_type_id = otpd.option_type_id AND otpd.store_id = cwd.default_store_id', -+ 'otps.option_type_id = otpd.option_type_id AND otps.store_id = cwd.default_store_id', - [] - ); - -@@ -373,6 +377,8 @@ class CustomOptionPriceModifier implements PriceModifierInterface - } - - /** -+ * Get select for update. -+ * - * @param string $sourceTable - * @return \Magento\Framework\DB\Select - */ -@@ -402,6 +408,8 @@ class CustomOptionPriceModifier implements PriceModifierInterface - } - - /** -+ * Get table name. -+ * - * @param string $tableName - * @return string - */ -@@ -411,6 +419,8 @@ class CustomOptionPriceModifier implements PriceModifierInterface - } - - /** -+ * Is price scope global. -+ * - * @return bool - */ - private function isPriceGlobal(): bool -diff --git a/app/code/Magento/Catalog/Model/ResourceModel/Product/Indexer/Price/DefaultPrice.php b/app/code/Magento/Catalog/Model/ResourceModel/Product/Indexer/Price/DefaultPrice.php -index 168fa8f50ac..9643f4c3a71 100644 ---- a/app/code/Magento/Catalog/Model/ResourceModel/Product/Indexer/Price/DefaultPrice.php -+++ b/app/code/Magento/Catalog/Model/ResourceModel/Product/Indexer/Price/DefaultPrice.php -@@ -10,6 +10,7 @@ use Magento\Framework\Indexer\DimensionalIndexerInterface; - - /** - * Default Product Type Price Indexer Resource model -+ * - * For correctly work need define product type id - * - * @api -@@ -39,7 +40,7 @@ class DefaultPrice extends AbstractIndexer implements PriceInterface - /** - * Core data - * -- * @var \Magento\Framework\Module\Manager -+ * @var \Magento\Framework\Module\ModuleManagerInterface - */ - protected $moduleManager; - -@@ -72,7 +73,7 @@ class DefaultPrice extends AbstractIndexer implements PriceInterface - * @param \Magento\Framework\Indexer\Table\StrategyInterface $tableStrategy - * @param \Magento\Eav\Model\Config $eavConfig - * @param \Magento\Framework\Event\ManagerInterface $eventManager -- * @param \Magento\Framework\Module\Manager $moduleManager -+ * @param \Magento\Framework\Module\ModuleManagerInterface $moduleManager - * @param string|null $connectionName - * @param IndexTableStructureFactory $indexTableStructureFactory - * @param PriceModifierInterface[] $priceModifiers -@@ -82,7 +83,7 @@ class DefaultPrice extends AbstractIndexer implements PriceInterface - \Magento\Framework\Indexer\Table\StrategyInterface $tableStrategy, - \Magento\Eav\Model\Config $eavConfig, - \Magento\Framework\Event\ManagerInterface $eventManager, -- \Magento\Framework\Module\Manager $moduleManager, -+ \Magento\Framework\Module\ModuleManagerInterface $moduleManager, - $connectionName = null, - IndexTableStructureFactory $indexTableStructureFactory = null, - array $priceModifiers = [] -@@ -208,6 +209,8 @@ class DefaultPrice extends AbstractIndexer implements PriceInterface - } - - /** -+ * Reindex prices. -+ * - * @param null|int|array $entityIds - * @return \Magento\Catalog\Model\ResourceModel\Product\Indexer\Price\DefaultPrice - */ -@@ -256,7 +259,8 @@ class DefaultPrice extends AbstractIndexer implements PriceInterface - $tableName = $this->_getDefaultFinalPriceTable(); - $this->getConnection()->delete($tableName); - -- $finalPriceTable = $this->indexTableStructureFactory->create([ -+ $finalPriceTable = $this->indexTableStructureFactory->create( -+ [ - 'tableName' => $tableName, - 'entityField' => 'entity_id', - 'customerGroupField' => 'customer_group_id', -@@ -267,7 +271,8 @@ class DefaultPrice extends AbstractIndexer implements PriceInterface - 'minPriceField' => 'min_price', - 'maxPriceField' => 'max_price', - 'tierPriceField' => 'tier_price', -- ]); -+ ] -+ ); - - return $finalPriceTable; - } -@@ -462,11 +467,13 @@ class DefaultPrice extends AbstractIndexer implements PriceInterface - ); - $tierPrice = $this->getTotalTierPriceExpression($price); - $tierPriceExpr = $connection->getIfNullSql($tierPrice, $maxUnsignedBigint); -- $finalPrice = $connection->getLeastSql([ -+ $finalPrice = $connection->getLeastSql( -+ [ - $price, - $specialPriceExpr, - $tierPriceExpr, -- ]); -+ ] -+ ); - - $select->columns( - [ -@@ -604,7 +611,7 @@ class DefaultPrice extends AbstractIndexer implements PriceInterface - [] - )->joinLeft( - ['otps' => $this->getTable('catalog_product_option_type_price')], -- 'otps.option_type_id = otpd.option_type_id AND otpd.store_id = cs.store_id', -+ 'otps.option_type_id = otpd.option_type_id AND otps.store_id = cs.store_id', - [] - )->group( - ['i.entity_id', 'i.customer_group_id', 'i.website_id', 'o.option_id'] -@@ -802,6 +809,8 @@ class DefaultPrice extends AbstractIndexer implements PriceInterface - } - - /** -+ * Check if product exists. -+ * - * @return bool - */ - protected function hasEntity() -@@ -823,6 +832,8 @@ class DefaultPrice extends AbstractIndexer implements PriceInterface - } - - /** -+ * Get total tier price expression. -+ * - * @param \Zend_Db_Expr $priceExpression - * @return \Zend_Db_Expr - */ -@@ -841,7 +852,8 @@ class DefaultPrice extends AbstractIndexer implements PriceInterface - ] - ), - 'NULL', -- $this->getConnection()->getLeastSql([ -+ $this->getConnection()->getLeastSql( -+ [ - $this->getConnection()->getIfNullSql( - $this->getTierPriceExpressionForTable('tier_price_1', $priceExpression), - $maxUnsignedBigint -@@ -858,11 +870,14 @@ class DefaultPrice extends AbstractIndexer implements PriceInterface - $this->getTierPriceExpressionForTable('tier_price_4', $priceExpression), - $maxUnsignedBigint - ), -- ]) -+ ] -+ ) - ); - } - - /** -+ * Get tier price expression for table. -+ * - * @param string $tableAlias - * @param \Zend_Db_Expr $priceExpression - * @return \Zend_Db_Expr -diff --git a/app/code/Magento/Catalog/Model/ResourceModel/Product/Indexer/Price/Query/BaseFinalPrice.php b/app/code/Magento/Catalog/Model/ResourceModel/Product/Indexer/Price/Query/BaseFinalPrice.php -index 0005ac8dea5..a3f463d53e7 100644 ---- a/app/code/Magento/Catalog/Model/ResourceModel/Product/Indexer/Price/Query/BaseFinalPrice.php -+++ b/app/code/Magento/Catalog/Model/ResourceModel/Product/Indexer/Price/Query/BaseFinalPrice.php -@@ -16,6 +16,7 @@ use Magento\Store\Model\Indexer\WebsiteDimensionProvider; - - /** - * Prepare base select for Product Price index limited by specified dimensions: website and customer group -+ * - * @SuppressWarnings(PHPMD.CouplingBetweenObjects) - */ - class BaseFinalPrice -@@ -36,7 +37,7 @@ class BaseFinalPrice - private $joinAttributeProcessor; - - /** -- * @var \Magento\Framework\Module\Manager -+ * @var \Magento\Framework\Module\ModuleManagerInterface - */ - private $moduleManager; - -@@ -66,16 +67,17 @@ class BaseFinalPrice - private $metadataPool; - - /** -- * BaseFinalPrice constructor. - * @param \Magento\Framework\App\ResourceConnection $resource - * @param JoinAttributeProcessor $joinAttributeProcessor -- * @param \Magento\Framework\Module\Manager $moduleManager -+ * @param \Magento\Framework\Module\ModuleManagerInterface $moduleManager -+ * @param \Magento\Framework\Event\ManagerInterface $eventManager -+ * @param \Magento\Framework\EntityManager\MetadataPool $metadataPool - * @param string $connectionName - */ - public function __construct( - \Magento\Framework\App\ResourceConnection $resource, - JoinAttributeProcessor $joinAttributeProcessor, -- \Magento\Framework\Module\Manager $moduleManager, -+ \Magento\Framework\Module\ModuleManagerInterface $moduleManager, - \Magento\Framework\Event\ManagerInterface $eventManager, - \Magento\Framework\EntityManager\MetadataPool $metadataPool, - $connectionName = 'indexer' -@@ -89,6 +91,8 @@ class BaseFinalPrice - } - - /** -+ * Build query for base final price. -+ * - * @param Dimension[] $dimensions - * @param string $productType - * @param array $entityIds -@@ -196,11 +200,13 @@ class BaseFinalPrice - ); - $tierPrice = $this->getTotalTierPriceExpression($price); - $tierPriceExpr = $connection->getIfNullSql($tierPrice, $maxUnsignedBigint); -- $finalPrice = $connection->getLeastSql([ -+ $finalPrice = $connection->getLeastSql( -+ [ - $price, - $specialPriceExpr, - $tierPriceExpr, -- ]); -+ ] -+ ); - - $select->columns( - [ -@@ -217,11 +223,8 @@ class BaseFinalPrice - $select->where("e.type_id = ?", $productType); - - if ($entityIds !== null) { -- if (count($entityIds) > 1) { -- $select->where(sprintf('e.entity_id BETWEEN %s AND %s', min($entityIds), max($entityIds))); -- } else { -- $select->where('e.entity_id = ?', $entityIds); -- } -+ $select->where(sprintf('e.entity_id BETWEEN %s AND %s', min($entityIds), max($entityIds))); -+ $select->where('e.entity_id IN(?)', $entityIds); - } - - /** -@@ -261,7 +264,8 @@ class BaseFinalPrice - ] - ), - 'NULL', -- $this->getConnection()->getLeastSql([ -+ $this->getConnection()->getLeastSql( -+ [ - $this->getConnection()->getIfNullSql( - $this->getTierPriceExpressionForTable('tier_price_1', $priceExpression), - $maxUnsignedBigint -@@ -278,14 +282,15 @@ class BaseFinalPrice - $this->getTierPriceExpressionForTable('tier_price_4', $priceExpression), - $maxUnsignedBigint - ), -- ]) -+ ] -+ ) - ); - } - - /** - * Get tier price expression for table - * -- * @param $tableAlias -+ * @param string $tableAlias - * @param \Zend_Db_Expr $priceExpression - * @return \Zend_Db_Expr - */ -@@ -305,7 +310,7 @@ class BaseFinalPrice - /** - * Get connection - * -- * return \Magento\Framework\DB\Adapter\AdapterInterface -+ * @return \Magento\Framework\DB\Adapter\AdapterInterface - * @throws \DomainException - */ - private function getConnection(): \Magento\Framework\DB\Adapter\AdapterInterface -diff --git a/app/code/Magento/Catalog/Model/ResourceModel/Product/Indexer/TemporaryTableStrategy.php b/app/code/Magento/Catalog/Model/ResourceModel/Product/Indexer/TemporaryTableStrategy.php -index 54673cb01bb..89daab28859 100644 ---- a/app/code/Magento/Catalog/Model/ResourceModel/Product/Indexer/TemporaryTableStrategy.php -+++ b/app/code/Magento/Catalog/Model/ResourceModel/Product/Indexer/TemporaryTableStrategy.php -@@ -30,7 +30,7 @@ class TemporaryTableStrategy implements \Magento\Framework\Indexer\Table\Strateg - - /** - * TemporaryTableStrategy constructor. -- * @param \Magento\Framework\Indexer\Table\Strategy $strategy -+ * @param \Magento\Framework\Indexer\Table\StrategyInterface $strategy - * @param \Magento\Framework\App\ResourceConnection $resource - */ - public function __construct( -@@ -66,9 +66,10 @@ class TemporaryTableStrategy implements \Magento\Framework\Indexer\Table\Strateg - } - - /** -- * Create temporary index table based on memory table -+ * Create temporary index table based on memory table{@inheritdoc} - * -- * {@inheritdoc} -+ * @param string $tablePrefix -+ * @return string - */ - public function prepareTableName($tablePrefix) - { -diff --git a/app/code/Magento/Catalog/Model/ResourceModel/Product/Link/DeleteHandler.php b/app/code/Magento/Catalog/Model/ResourceModel/Product/Link/DeleteHandler.php -index 024c87c9fc8..a554ff2641d 100644 ---- a/app/code/Magento/Catalog/Model/ResourceModel/Product/Link/DeleteHandler.php -+++ b/app/code/Magento/Catalog/Model/ResourceModel/Product/Link/DeleteHandler.php -@@ -60,9 +60,11 @@ class DeleteHandler - } - - /** -+ * Delete linked product. -+ * - * @param string $entityType - * @param object $entity -- * @return object -+ * @return void - * @throws CouldNotDeleteException - * @throws NoSuchEntityException - * @SuppressWarnings(PHPMD.UnusedFormalParameter) -diff --git a/app/code/Magento/Catalog/Model/ResourceModel/Product/LinkedProductSelectBuilderByBasePrice.php b/app/code/Magento/Catalog/Model/ResourceModel/Product/LinkedProductSelectBuilderByBasePrice.php -index 8841b6059c4..841fe17bdcf 100644 ---- a/app/code/Magento/Catalog/Model/ResourceModel/Product/LinkedProductSelectBuilderByBasePrice.php -+++ b/app/code/Magento/Catalog/Model/ResourceModel/Product/LinkedProductSelectBuilderByBasePrice.php -@@ -11,6 +11,9 @@ use Magento\Framework\App\ObjectManager; - use Magento\Framework\DB\Select; - use Magento\Store\Model\Store; - -+/** -+ * Provide Select object for retrieve product id with minimal price. -+ */ - class LinkedProductSelectBuilderByBasePrice implements LinkedProductSelectBuilderInterface - { - /** -@@ -69,7 +72,7 @@ class LinkedProductSelectBuilderByBasePrice implements LinkedProductSelectBuilde - } - - /** -- * {@inheritdoc} -+ * @inheritdoc - */ - public function build($productId) - { -@@ -85,7 +88,7 @@ class LinkedProductSelectBuilderByBasePrice implements LinkedProductSelectBuilde - [] - )->joinInner( - [BaseSelectProcessorInterface::PRODUCT_TABLE_ALIAS => $productTable], -- sprintf('%s.entity_id = link.child_id', BaseSelectProcessorInterface::PRODUCT_TABLE_ALIAS, $linkField), -+ sprintf('%s.entity_id = link.child_id', BaseSelectProcessorInterface::PRODUCT_TABLE_ALIAS), - ['entity_id'] - )->joinInner( - ['t' => $priceAttribute->getBackendTable()], -diff --git a/app/code/Magento/Catalog/Model/ResourceModel/Product/Option.php b/app/code/Magento/Catalog/Model/ResourceModel/Product/Option.php -index 179da06b599..2238ad91550 100644 ---- a/app/code/Magento/Catalog/Model/ResourceModel/Product/Option.php -+++ b/app/code/Magento/Catalog/Model/ResourceModel/Product/Option.php -@@ -6,6 +6,10 @@ - namespace Magento\Catalog\Model\ResourceModel\Product; - - use Magento\Catalog\Api\Data\ProductInterface; -+use Magento\Framework\DataObject; -+use Magento\Framework\Model\AbstractModel; -+use Magento\Store\Model\ScopeInterface; -+use Magento\Store\Model\Store; - - /** - * Catalog product custom option resource model -@@ -76,10 +80,10 @@ class Option extends \Magento\Framework\Model\ResourceModel\Db\AbstractDb - /** - * Save options store data - * -- * @param \Magento\Framework\Model\AbstractModel $object -+ * @param AbstractModel $object - * @return \Magento\Framework\Model\ResourceModel\Db\AbstractDb - */ -- protected function _afterSave(\Magento\Framework\Model\AbstractModel $object) -+ protected function _afterSave(AbstractModel $object) - { - $this->_saveValuePrices($object); - $this->_saveValueTitles($object); -@@ -90,136 +94,38 @@ class Option extends \Magento\Framework\Model\ResourceModel\Db\AbstractDb - /** - * Save value prices - * -- * @param \Magento\Framework\Model\AbstractModel $object -+ * @param AbstractModel $object - * @return $this -- * @SuppressWarnings(PHPMD.CyclomaticComplexity) -- * @SuppressWarnings(PHPMD.ExcessiveMethodLength) - */ -- protected function _saveValuePrices(\Magento\Framework\Model\AbstractModel $object) -+ protected function _saveValuePrices(AbstractModel $object) - { -- $priceTable = $this->getTable('catalog_product_option_price'); -- $connection = $this->getConnection(); -- - /* - * Better to check param 'price' and 'price_type' for saving. - * If there is not price skip saving price - */ -- - if (in_array($object->getType(), $this->getPriceTypes())) { -- //save for store_id = 0 -+ // save for store_id = 0 - if (!$object->getData('scope', 'price')) { -- $statement = $connection->select()->from( -- $priceTable, -- 'option_id' -- )->where( -- 'option_id = ?', -- $object->getId() -- )->where( -- 'store_id = ?', -- \Magento\Store\Model\Store::DEFAULT_STORE_ID -- ); -- $optionId = $connection->fetchOne($statement); -- -- if ($optionId) { -- $data = $this->_prepareDataForTable( -- new \Magento\Framework\DataObject( -- ['price' => $object->getPrice(), 'price_type' => $object->getPriceType()] -- ), -- $priceTable -- ); -- -- $connection->update( -- $priceTable, -- $data, -- [ -- 'option_id = ?' => $object->getId(), -- 'store_id = ?' => \Magento\Store\Model\Store::DEFAULT_STORE_ID -- ] -- ); -- } else { -- $data = $this->_prepareDataForTable( -- new \Magento\Framework\DataObject( -- [ -- 'option_id' => $object->getId(), -- 'store_id' => \Magento\Store\Model\Store::DEFAULT_STORE_ID, -- 'price' => $object->getPrice(), -- 'price_type' => $object->getPriceType(), -- ] -- ), -- $priceTable -- ); -- $connection->insert($priceTable, $data); -- } -+ $this->savePriceByStore($object, Store::DEFAULT_STORE_ID); - } - - $scope = (int)$this->_config->getValue( -- \Magento\Store\Model\Store::XML_PATH_PRICE_SCOPE, -+ Store::XML_PATH_PRICE_SCOPE, - \Magento\Store\Model\ScopeInterface::SCOPE_STORE - ); - -- if ($object->getStoreId() != '0' && $scope == \Magento\Store\Model\Store::PRICE_SCOPE_WEBSITE) { -- $baseCurrency = $this->_config->getValue( -- \Magento\Directory\Model\Currency::XML_PATH_CURRENCY_BASE, -- 'default' -- ); -- -+ if ($object->getStoreId() != '0' && $scope == Store::PRICE_SCOPE_WEBSITE) { - $storeIds = $this->_storeManager->getStore($object->getStoreId())->getWebsite()->getStoreIds(); -- if (is_array($storeIds)) { -- foreach ($storeIds as $storeId) { -- if ($object->getPriceType() == 'fixed') { -- $storeCurrency = $this->_storeManager->getStore($storeId)->getBaseCurrencyCode(); -- $rate = $this->_currencyFactory->create()->load($baseCurrency)->getRate($storeCurrency); -- if (!$rate) { -- $rate = 1; -- } -- $newPrice = $object->getPrice() * $rate; -- } else { -- $newPrice = $object->getPrice(); -- } -- -- $statement = $connection->select()->from( -- $priceTable -- )->where( -- 'option_id = ?', -- $object->getId() -- )->where( -- 'store_id = ?', -- $storeId -- ); -- -- if ($connection->fetchOne($statement)) { -- $data = $this->_prepareDataForTable( -- new \Magento\Framework\DataObject( -- ['price' => $newPrice, 'price_type' => $object->getPriceType()] -- ), -- $priceTable -- ); -- -- $connection->update( -- $priceTable, -- $data, -- ['option_id = ?' => $object->getId(), 'store_id = ?' => $storeId] -- ); -- } else { -- $data = $this->_prepareDataForTable( -- new \Magento\Framework\DataObject( -- [ -- 'option_id' => $object->getId(), -- 'store_id' => $storeId, -- 'price' => $newPrice, -- 'price_type' => $object->getPriceType(), -- ] -- ), -- $priceTable -- ); -- $connection->insert($priceTable, $data); -- } -- } -+ if (empty($storeIds)) { -+ return $this; - } -- } elseif ($scope == \Magento\Store\Model\Store::PRICE_SCOPE_WEBSITE && $object->getData('scope', 'price') -- ) { -- $connection->delete( -- $priceTable, -+ foreach ($storeIds as $storeId) { -+ $newPrice = $this->calculateStorePrice($object, $storeId); -+ $this->savePriceByStore($object, (int)$storeId, $newPrice); -+ } -+ } elseif ($scope == Store::PRICE_SCOPE_WEBSITE && $object->getData('scope', 'price')) { -+ $this->getConnection()->delete( -+ $this->getTable('catalog_product_option_price'), - ['option_id = ?' => $object->getId(), 'store_id = ?' => $object->getStoreId()] - ); - } -@@ -228,31 +134,114 @@ class Option extends \Magento\Framework\Model\ResourceModel\Db\AbstractDb - return $this; - } - -+ /** -+ * Save option price by store -+ * -+ * @param AbstractModel $object -+ * @param int $storeId -+ * @param float|null $newPrice -+ */ -+ private function savePriceByStore(AbstractModel $object, int $storeId, float $newPrice = null): void -+ { -+ $priceTable = $this->getTable('catalog_product_option_price'); -+ $connection = $this->getConnection(); -+ $price = $newPrice === null ? $object->getPrice() : $newPrice; -+ -+ $statement = $connection->select()->from($priceTable, 'option_id') -+ ->where('option_id = ?', $object->getId()) -+ ->where('store_id = ?', $storeId); -+ $optionId = $connection->fetchOne($statement); -+ -+ if (!$optionId) { -+ $data = $this->_prepareDataForTable( -+ new DataObject( -+ [ -+ 'option_id' => $object->getId(), -+ 'store_id' => $storeId, -+ 'price' => $price, -+ 'price_type' => $object->getPriceType(), -+ ] -+ ), -+ $priceTable -+ ); -+ $connection->insert($priceTable, $data); -+ } else { -+ // skip to update the default price when the store price is saving -+ if ($storeId === Store::DEFAULT_STORE_ID && (int)$object->getStoreId() !== $storeId) { -+ return; -+ } -+ -+ $data = $this->_prepareDataForTable( -+ new DataObject( -+ [ -+ 'price' => $price, -+ 'price_type' => $object->getPriceType() -+ ] -+ ), -+ $priceTable -+ ); -+ -+ $connection->update( -+ $priceTable, -+ $data, -+ [ -+ 'option_id = ?' => $object->getId(), -+ 'store_id = ?' => $storeId -+ ] -+ ); -+ } -+ } -+ -+ /** -+ * Calculate price by store -+ * -+ * @param AbstractModel $object -+ * @param int $storeId -+ * @return float -+ */ -+ private function calculateStorePrice(AbstractModel $object, int $storeId): float -+ { -+ $price = $object->getPrice(); -+ if ($object->getPriceType() == 'fixed') { -+ $website = $this->_storeManager->getStore($storeId)->getWebsite(); -+ $websiteBaseCurrency = $this->_config->getValue( -+ \Magento\Directory\Model\Currency::XML_PATH_CURRENCY_BASE, -+ ScopeInterface::SCOPE_WEBSITE, -+ $website -+ ); -+ $storeCurrency = $this->_storeManager->getStore($storeId)->getBaseCurrencyCode(); -+ $rate = $this->_currencyFactory->create()->load($websiteBaseCurrency)->getRate($storeCurrency); -+ $price = $object->getPrice() * ($rate ?: 1); -+ } -+ -+ return (float)$price; -+ } -+ - /** - * Save titles - * -- * @param \Magento\Framework\Model\AbstractModel $object -+ * @param AbstractModel $object - * @return void - * @SuppressWarnings(PHPMD.CyclomaticComplexity) - */ -- protected function _saveValueTitles(\Magento\Framework\Model\AbstractModel $object) -+ protected function _saveValueTitles(AbstractModel $object) - { - $connection = $this->getConnection(); - $titleTableName = $this->getTable('catalog_product_option_title'); -- foreach ([\Magento\Store\Model\Store::DEFAULT_STORE_ID, $object->getStoreId()] as $storeId) { -+ foreach ([Store::DEFAULT_STORE_ID, $object->getStoreId()] as $storeId) { - $existInCurrentStore = $this->getColFromOptionTable($titleTableName, (int)$object->getId(), (int)$storeId); -- $existInDefaultStore = (int)$storeId == \Magento\Store\Model\Store::DEFAULT_STORE_ID ? -+ $existInDefaultStore = (int)$storeId == Store::DEFAULT_STORE_ID ? - $existInCurrentStore : - $this->getColFromOptionTable( - $titleTableName, - (int)$object->getId(), -- \Magento\Store\Model\Store::DEFAULT_STORE_ID -+ Store::DEFAULT_STORE_ID - ); - - if ($object->getTitle()) { - $isDeleteStoreTitle = (bool)$object->getData('is_delete_store_title'); - if ($existInCurrentStore) { -- if ($isDeleteStoreTitle && (int)$storeId != \Magento\Store\Model\Store::DEFAULT_STORE_ID) { -+ if ($isDeleteStoreTitle && (int)$storeId != Store::DEFAULT_STORE_ID) { - $connection->delete($titleTableName, ['option_title_id = ?' => $existInCurrentStore]); - } elseif ($object->getStoreId() == $storeId) { - $data = $this->_prepareDataForTable( -@@ -270,9 +259,9 @@ class Option extends \Magento\Framework\Model\ResourceModel\Db\AbstractDb - } - } else { - // we should insert record into not default store only of if it does not exist in default store -- if (($storeId == \Magento\Store\Model\Store::DEFAULT_STORE_ID && !$existInDefaultStore) || -+ if (($storeId == Store::DEFAULT_STORE_ID && !$existInDefaultStore) || - ( -- $storeId != \Magento\Store\Model\Store::DEFAULT_STORE_ID && -+ $storeId != Store::DEFAULT_STORE_ID && - !$existInCurrentStore && - !$isDeleteStoreTitle - ) -@@ -291,7 +280,7 @@ class Option extends \Magento\Framework\Model\ResourceModel\Db\AbstractDb - } - } - } else { -- if ($object->getId() && $object->getStoreId() > \Magento\Store\Model\Store::DEFAULT_STORE_ID -+ if ($object->getId() && $object->getStoreId() > Store::DEFAULT_STORE_ID - && $storeId - ) { - $connection->delete( -@@ -470,7 +459,7 @@ class Option extends \Magento\Framework\Model\ResourceModel\Db\AbstractDb - 'option_title_default.option_id=product_option.option_id', - $connection->quoteInto( - 'option_title_default.store_id = ?', -- \Magento\Store\Model\Store::DEFAULT_STORE_ID -+ Store::DEFAULT_STORE_ID - ) - ] - ); -@@ -517,7 +506,7 @@ class Option extends \Magento\Framework\Model\ResourceModel\Db\AbstractDb - 'option_title_default.option_type_id=option_type.option_type_id', - $connection->quoteInto( - 'option_title_default.store_id = ?', -- \Magento\Store\Model\Store::DEFAULT_STORE_ID -+ Store::DEFAULT_STORE_ID - ) - ] - ); -@@ -582,6 +571,8 @@ class Option extends \Magento\Framework\Model\ResourceModel\Db\AbstractDb - } - - /** -+ * Get Metadata Pool -+ * - * @return \Magento\Framework\EntityManager\MetadataPool - */ - private function getMetadataPool() -diff --git a/app/code/Magento/Catalog/Model/ResourceModel/Product/Option/Value.php b/app/code/Magento/Catalog/Model/ResourceModel/Product/Option/Value.php -index ce0a9b6e461..494dbac02d7 100644 ---- a/app/code/Magento/Catalog/Model/ResourceModel/Product/Option/Value.php -+++ b/app/code/Magento/Catalog/Model/ResourceModel/Product/Option/Value.php -@@ -17,11 +17,12 @@ use Magento\Framework\Model\ResourceModel\Db\Context; - use Magento\Store\Model\ScopeInterface; - use Magento\Store\Model\Store; - use Magento\Store\Model\StoreManagerInterface; -+use Magento\Catalog\Helper\Data; - - /** - * Catalog product custom option resource model - * -- * @author Magento Core Team <core@magentocommerce.com> -+ * @SuppressWarnings(PHPMD.CouplingBetweenObjects) - */ - class Value extends AbstractDb - { -@@ -51,6 +52,11 @@ class Value extends AbstractDb - */ - private $localeFormat; - -+ /** -+ * @var Data -+ */ -+ private $dataHelper; -+ - /** - * Class constructor - * -@@ -59,17 +65,21 @@ class Value extends AbstractDb - * @param StoreManagerInterface $storeManager - * @param ScopeConfigInterface $config - * @param string $connectionName -+ * @param Data $dataHelper - */ - public function __construct( - Context $context, - CurrencyFactory $currencyFactory, - StoreManagerInterface $storeManager, - ScopeConfigInterface $config, -- $connectionName = null -+ $connectionName = null, -+ Data $dataHelper = null - ) { - $this->_currencyFactory = $currencyFactory; - $this->_storeManager = $storeManager; - $this->_config = $config; -+ $this->dataHelper = $dataHelper ?: ObjectManager::getInstance() -+ ->get(Data::class); - parent::__construct($context, $connectionName); - } - -@@ -85,6 +95,7 @@ class Value extends AbstractDb - - /** - * Proceed operations after object is saved -+ * - * Save options store data - * - * @param AbstractModel $object -@@ -130,7 +141,7 @@ class Value extends AbstractDb - $optionTypeId = $this->getConnection()->fetchOne($select); - - if ($optionTypeId) { -- if ($object->getStoreId() == '0') { -+ if ($object->getStoreId() == '0' || $this->dataHelper->isPriceGlobal()) { - $bind = ['price' => $price, 'price_type' => $priceType]; - $where = [ - 'option_type_id = ?' => $optionTypeId, -@@ -160,19 +171,22 @@ class Value extends AbstractDb - && isset($objectPrice) - && $object->getStoreId() != Store::DEFAULT_STORE_ID - ) { -- $baseCurrency = $this->_config->getValue( -+ $website = $this->_storeManager->getStore($object->getStoreId())->getWebsite(); -+ -+ $websiteBaseCurrency = $this->_config->getValue( - Currency::XML_PATH_CURRENCY_BASE, -- 'default' -+ ScopeInterface::SCOPE_WEBSITE, -+ $website - ); - -- $storeIds = $this->_storeManager->getStore($object->getStoreId())->getWebsite()->getStoreIds(); -+ $storeIds = $website->getStoreIds(); - if (is_array($storeIds)) { - foreach ($storeIds as $storeId) { - if ($priceType == 'fixed') { - $storeCurrency = $this->_storeManager->getStore($storeId)->getBaseCurrencyCode(); - /** @var $currencyModel Currency */ - $currencyModel = $this->_currencyFactory->create(); -- $currencyModel->load($baseCurrency); -+ $currencyModel->load($websiteBaseCurrency); - $rate = $currencyModel->getRate($storeCurrency); - if (!$rate) { - $rate = 1; -diff --git a/app/code/Magento/Catalog/Model/ResourceModel/Product/StatusBaseSelectProcessor.php b/app/code/Magento/Catalog/Model/ResourceModel/Product/StatusBaseSelectProcessor.php -index c7829ab3a31..c5c656b7265 100644 ---- a/app/code/Magento/Catalog/Model/ResourceModel/Product/StatusBaseSelectProcessor.php -+++ b/app/code/Magento/Catalog/Model/ResourceModel/Product/StatusBaseSelectProcessor.php -@@ -13,6 +13,7 @@ use Magento\Framework\DB\Select; - use Magento\Framework\EntityManager\MetadataPool; - use Magento\Store\Api\StoreResolverInterface; - use Magento\Store\Model\Store; -+use Magento\Store\Model\StoreManagerInterface; - - /** - * Class StatusBaseSelectProcessor -@@ -30,28 +31,32 @@ class StatusBaseSelectProcessor implements BaseSelectProcessorInterface - private $metadataPool; - - /** -- * @var StoreResolverInterface -+ * @var StoreManagerInterface - */ -- private $storeResolver; -+ private $storeManager; - - /** - * @param Config $eavConfig - * @param MetadataPool $metadataPool - * @param StoreResolverInterface $storeResolver -+ * @param StoreManagerInterface $storeManager -+ * -+ * @SuppressWarnings(PHPMD.UnusedFormalParameter) - */ - public function __construct( - Config $eavConfig, - MetadataPool $metadataPool, -- StoreResolverInterface $storeResolver -+ StoreResolverInterface $storeResolver, -+ StoreManagerInterface $storeManager = null - ) { - $this->eavConfig = $eavConfig; - $this->metadataPool = $metadataPool; -- $this->storeResolver = $storeResolver; -+ $this->storeManager = $storeManager ?: \Magento\Framework\App\ObjectManager::getInstance() -+ ->get(StoreManagerInterface::class); - } - - /** -- * @param Select $select -- * @return Select -+ * @inheritdoc - */ - public function process(Select $select) - { -@@ -70,7 +75,7 @@ class StatusBaseSelectProcessor implements BaseSelectProcessorInterface - ['status_attr' => $statusAttribute->getBackendTable()], - "status_attr.{$linkField} = " . self::PRODUCT_TABLE_ALIAS . ".{$linkField}" - . ' AND status_attr.attribute_id = ' . (int)$statusAttribute->getAttributeId() -- . ' AND status_attr.store_id = ' . $this->storeResolver->getCurrentStoreId(), -+ . ' AND status_attr.store_id = ' . $this->storeManager->getStore()->getId(), - [] - ); - -diff --git a/app/code/Magento/Catalog/Model/Rss/Category.php b/app/code/Magento/Catalog/Model/Rss/Category.php -index a58569d1b59..653d86b177a 100644 ---- a/app/code/Magento/Catalog/Model/Rss/Category.php -+++ b/app/code/Magento/Catalog/Model/Rss/Category.php -@@ -6,8 +6,7 @@ - namespace Magento\Catalog\Model\Rss; - - /** -- * Class Category -- * @package Magento\Catalog\Model\Rss -+ * Rss Category model. - */ - class Category - { -@@ -42,9 +41,11 @@ class Category - } - - /** -+ * Get products for given category. -+ * - * @param \Magento\Catalog\Model\Category $category - * @param int $storeId -- * @return $this -+ * @return \Magento\Catalog\Model\ResourceModel\Product\Collection - */ - public function getProductCollection(\Magento\Catalog\Model\Category $category, $storeId) - { -diff --git a/app/code/Magento/Catalog/Model/Template/Filter.php b/app/code/Magento/Catalog/Model/Template/Filter.php -index 1eb30ff95a4..8cd61415b95 100644 ---- a/app/code/Magento/Catalog/Model/Template/Filter.php -+++ b/app/code/Magento/Catalog/Model/Template/Filter.php -@@ -66,7 +66,7 @@ class Filter extends \Magento\Framework\Filter\Template - * Set use absolute links flag - * - * @param bool $flag -- * @return \Magento\Email\Model\Template\Filter -+ * @return $this - */ - public function setUseAbsoluteLinks($flag) - { -@@ -76,10 +76,11 @@ class Filter extends \Magento\Framework\Filter\Template - - /** - * Setter whether SID is allowed in store directive -+ * - * Doesn't set anything intentionally, since SID is not allowed in any kind of emails - * - * @param bool $flag -- * @return \Magento\Email\Model\Template\Filter -+ * @return $this - */ - public function setUseSessionInUrl($flag) - { -@@ -132,6 +133,7 @@ class Filter extends \Magento\Framework\Filter\Template - - /** - * Retrieve store URL directive -+ * - * Support url and direct_url properties - * - * @param array $construction -diff --git a/app/code/Magento/Catalog/Observer/CategoryProductIndexer.php b/app/code/Magento/Catalog/Observer/CategoryProductIndexer.php -new file mode 100644 -index 00000000000..ca87efaa874 ---- /dev/null -+++ b/app/code/Magento/Catalog/Observer/CategoryProductIndexer.php -@@ -0,0 +1,42 @@ -+<?php -+/** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+declare(strict_types=1); -+ -+namespace Magento\Catalog\Observer; -+ -+use Magento\Catalog\Model\Indexer\Category\Product\Processor; -+use Magento\Framework\Event\Observer; -+use Magento\Framework\Event\ObserverInterface; -+ -+/** -+ * Checks if a category has changed products and depends on indexer configuration. -+ */ -+class CategoryProductIndexer implements ObserverInterface -+{ -+ /** -+ * @var Processor -+ */ -+ private $processor; -+ -+ /** -+ * @param Processor $processor -+ */ -+ public function __construct(Processor $processor) -+ { -+ $this->processor = $processor; -+ } -+ -+ /** -+ * @inheritdoc -+ */ -+ public function execute(Observer $observer): void -+ { -+ $productIds = $observer->getEvent()->getProductIds(); -+ if (!empty($productIds) && $this->processor->isIndexerScheduled()) { -+ $this->processor->markIndexerAsInvalid(); -+ } -+ } -+} -diff --git a/app/code/Magento/Catalog/Observer/SetSpecialPriceStartDate.php b/app/code/Magento/Catalog/Observer/SetSpecialPriceStartDate.php -index a597b8fddda..a898075befd 100644 ---- a/app/code/Magento/Catalog/Observer/SetSpecialPriceStartDate.php -+++ b/app/code/Magento/Catalog/Observer/SetSpecialPriceStartDate.php -@@ -3,9 +3,12 @@ - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -+declare(strict_types=1); -+ - namespace Magento\Catalog\Observer; - - use Magento\Framework\Event\ObserverInterface; -+use Magento\Framework\Stdlib\DateTime\TimezoneInterface; - - /** - * Set value for Special Price start date -@@ -13,21 +16,20 @@ use Magento\Framework\Event\ObserverInterface; - class SetSpecialPriceStartDate implements ObserverInterface - { - /** -- * @var \Magento\Framework\Stdlib\DateTime\TimezoneInterface -+ * @var TimezoneInterface - */ - private $localeDate; - - /** -- * @param \Magento\Framework\Stdlib\DateTime\TimezoneInterface $localeDate -- * @codeCoverageIgnore -+ * @param TimezoneInterface $localeDate - */ -- public function __construct(\Magento\Framework\Stdlib\DateTime\TimezoneInterface $localeDate) -+ public function __construct(TimezoneInterface $localeDate) - { - $this->localeDate = $localeDate; - } - - /** -- * Set the current date to Special Price From attribute if it empty -+ * Set the current date to Special Price From attribute if it's empty. - * - * @param \Magento\Framework\Event\Observer $observer - * @return $this -@@ -36,10 +38,9 @@ class SetSpecialPriceStartDate implements ObserverInterface - { - /** @var $product \Magento\Catalog\Model\Product */ - $product = $observer->getEvent()->getProduct(); -- if ($product->getSpecialPrice() && !$product->getSpecialFromDate()) { -- $product->setData('special_from_date', $this->localeDate->date()); -+ if ($product->getSpecialPrice() && $product->getSpecialFromDate() === null) { -+ $product->setData('special_from_date', $this->localeDate->date()->setTime(0, 0)); - } -- - return $this; - } - } -diff --git a/app/code/Magento/Catalog/Plugin/Block/Topmenu.php b/app/code/Magento/Catalog/Plugin/Block/Topmenu.php -index 8cbe235e05f..35ce327c74e 100644 ---- a/app/code/Magento/Catalog/Plugin/Block/Topmenu.php -+++ b/app/code/Magento/Catalog/Plugin/Block/Topmenu.php -@@ -14,6 +14,11 @@ use Magento\Framework\Data\Tree\Node; - */ - class Topmenu - { -+ /** -+ * Cache tag for menu block -+ */ -+ private $cacheTag = "top_menu"; -+ - /** - * Catalog category - * -@@ -22,7 +27,7 @@ class Topmenu - protected $catalogCategory; - - /** -- * @var \Magento\Catalog\Model\ResourceModel\Category\CollectionFactory -+ * @var \Magento\Catalog\Model\ResourceModel\Category\StateDependentCollectionFactory - */ - private $collectionFactory; - -@@ -40,13 +45,13 @@ class Topmenu - * Initialize dependencies. - * - * @param \Magento\Catalog\Helper\Category $catalogCategory -- * @param \Magento\Catalog\Model\ResourceModel\Category\CollectionFactory $categoryCollectionFactory -+ * @param \Magento\Catalog\Model\ResourceModel\Category\StateDependentCollectionFactory $categoryCollectionFactory - * @param \Magento\Store\Model\StoreManagerInterface $storeManager - * @param \Magento\Catalog\Model\Layer\Resolver $layerResolver - */ - public function __construct( - \Magento\Catalog\Helper\Category $catalogCategory, -- \Magento\Catalog\Model\ResourceModel\Category\CollectionFactory $categoryCollectionFactory, -+ \Magento\Catalog\Model\ResourceModel\Category\StateDependentCollectionFactory $categoryCollectionFactory, - \Magento\Store\Model\StoreManagerInterface $storeManager, - \Magento\Catalog\Model\Layer\Resolver $layerResolver - ) { -@@ -119,6 +124,7 @@ class Topmenu - $subject->addIdentity(Category::CACHE_TAG); - $rootId = $this->storeManager->getStore()->getRootCategoryId(); - $storeId = $this->storeManager->getStore()->getId(); -+ $currentCategory = $this->getCurrentCategory(); - /** @var \Magento\Catalog\Model\ResourceModel\Category\Collection $collection */ - $collection = $this->getCategoryTree($storeId, $rootId); - $mapping = [$rootId => $subject->getMenu()]; // use nodes stack to avoid recursion -@@ -128,6 +134,9 @@ class Topmenu - } - $subject->addIdentity(Category::CACHE_TAG . '_' . $category->getId()); - } -+ if ($currentCategory) { -+ $subject->addIdentity($this->cacheTag . '_' . Category::CACHE_TAG . '_' . $currentCategory->getId()); -+ } - } - - /** -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 00000000000..6add542b155 ---- /dev/null -+++ b/app/code/Magento/Catalog/Plugin/Framework/App/Action/ContextPlugin.php -@@ -0,0 +1,73 @@ -+<?php -+/** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+ -+declare(strict_types=1); -+ -+namespace Magento\Catalog\Plugin\Framework\App\Action; -+ -+use Magento\Catalog\Model\Product\ProductList\Toolbar as ToolbarModel; -+use Magento\Catalog\Model\Product\ProductList\ToolbarMemorizer; -+use Magento\Catalog\Model\Session as CatalogSession; -+use Magento\Framework\App\Http\Context as HttpContext; -+ -+/** -+ * Before dispatch plugin for all frontend controllers to update http context. -+ */ -+class ContextPlugin -+{ -+ /** -+ * @var ToolbarMemorizer -+ */ -+ private $toolbarMemorizer; -+ -+ /** -+ * @var CatalogSession -+ */ -+ private $catalogSession; -+ -+ /** -+ * @var HttpContext -+ */ -+ private $httpContext; -+ -+ /** -+ * @param ToolbarMemorizer $toolbarMemorizer -+ * @param CatalogSession $catalogSession -+ * @param HttpContext $httpContext -+ */ -+ public function __construct( -+ ToolbarMemorizer $toolbarMemorizer, -+ CatalogSession $catalogSession, -+ HttpContext $httpContext -+ ) { -+ $this->toolbarMemorizer = $toolbarMemorizer; -+ $this->catalogSession = $catalogSession; -+ $this->httpContext = $httpContext; -+ } -+ -+ /** -+ * Update http context with catalog sensitive information. -+ * -+ * @return void -+ */ -+ public function beforeDispatch() -+ { -+ if ($this->toolbarMemorizer->isMemorizingAllowed()) { -+ $params = [ -+ ToolbarModel::ORDER_PARAM_NAME, -+ ToolbarModel::DIRECTION_PARAM_NAME, -+ ToolbarModel::MODE_PARAM_NAME, -+ ToolbarModel::LIMIT_PARAM_NAME -+ ]; -+ foreach ($params as $param) { -+ $paramValue = $this->catalogSession->getData($param); -+ if ($paramValue) { -+ $this->httpContext->setValue($param, $paramValue, false); -+ } -+ } -+ } -+ } -+} -diff --git a/app/code/Magento/Catalog/Plugin/Model/Attribute/Backend/AttributeValidation.php b/app/code/Magento/Catalog/Plugin/Model/Attribute/Backend/AttributeValidation.php -index 597a1466a12..eca4d468950 100644 ---- a/app/code/Magento/Catalog/Plugin/Model/Attribute/Backend/AttributeValidation.php -+++ b/app/code/Magento/Catalog/Plugin/Model/Attribute/Backend/AttributeValidation.php -@@ -7,6 +7,9 @@ namespace Magento\Catalog\Plugin\Model\Attribute\Backend; - - use Magento\Store\Model\Store; - -+/** -+ * Attribute validation -+ */ - class AttributeValidation - { - /** -@@ -14,6 +17,11 @@ class AttributeValidation - */ - private $storeManager; - -+ /** -+ * @var array -+ */ -+ private $allowedEntityTypes; -+ - /** - * @param \Magento\Store\Model\StoreManagerInterface $storeManager - * @param array $allowedEntityTypes -@@ -27,9 +35,12 @@ class AttributeValidation - } - - /** -+ * Around validate -+ * - * @param \Magento\Eav\Model\Entity\Attribute\Backend\AbstractBackend $subject - * @param \Closure $proceed - * @param \Magento\Framework\DataObject $entity -+ * @throws \Magento\Framework\Exception\NoSuchEntityException - * @return bool - */ - public function aroundValidate( -@@ -41,7 +52,7 @@ class AttributeValidation - return $entity instanceof $allowedEntity; - }, $this->allowedEntityTypes))); - -- if ($isAllowedType && $this->storeManager->getStore()->getId() !== Store::DEFAULT_STORE_ID) { -+ if ($isAllowedType && (int) $this->storeManager->getStore()->getId() !== Store::DEFAULT_STORE_ID) { - $attrCode = $subject->getAttribute()->getAttributeCode(); - // Null is meaning "no value" which should be overridden by value from default scope - if (array_key_exists($attrCode, $entity->getData()) && $entity->getData($attrCode) === null) { -diff --git a/app/code/Magento/Catalog/Plugin/Model/Product/Option/UpdateProductCustomOptionsAttributes.php b/app/code/Magento/Catalog/Plugin/Model/Product/Option/UpdateProductCustomOptionsAttributes.php -new file mode 100644 -index 00000000000..dd750cfbc69 ---- /dev/null -+++ b/app/code/Magento/Catalog/Plugin/Model/Product/Option/UpdateProductCustomOptionsAttributes.php -@@ -0,0 +1,56 @@ -+<?php -+/** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+declare(strict_types=1); -+ -+namespace Magento\Catalog\Plugin\Model\Product\Option; -+ -+/** -+ * Plugin for updating product 'has_options' and 'required_options' attributes -+ */ -+class UpdateProductCustomOptionsAttributes -+{ -+ /** -+ * @var \Magento\Catalog\Api\ProductRepositoryInterface -+ */ -+ private $productRepository; -+ -+ /** -+ * @param \Magento\Catalog\Api\ProductRepositoryInterface $productRepository -+ */ -+ public function __construct(\Magento\Catalog\Api\ProductRepositoryInterface $productRepository) -+ { -+ $this->productRepository = $productRepository; -+ } -+ -+ /** -+ * Update product 'has_options' and 'required_options' attributes after option save -+ * -+ * @param \Magento\Catalog\Api\ProductCustomOptionRepositoryInterface $subject -+ * @param \Magento\Catalog\Api\Data\ProductCustomOptionInterface $option -+ * -+ * @return \Magento\Catalog\Api\Data\ProductCustomOptionInterface -+ * @SuppressWarnings(PHPMD.UnusedFormalParameter) -+ */ -+ public function afterSave( -+ \Magento\Catalog\Api\ProductCustomOptionRepositoryInterface $subject, -+ \Magento\Catalog\Api\Data\ProductCustomOptionInterface $option -+ ) { -+ $product = $this->productRepository->get($option->getProductSku()); -+ if (!$product->getHasOptions() || -+ ($option->getIsRequire() && !$product->getRequiredOptions())) { -+ $product->setCanSaveCustomOptions(true); -+ $product->setOptionsSaved(true); -+ $currentOptions = array_filter($product->getOptions(), function ($iOption) use ($option) { -+ return $option->getOptionId() != $iOption->getOptionId(); -+ }); -+ $currentOptions[] = $option; -+ $product->setOptions($currentOptions); -+ $product->save(); -+ } -+ -+ return $option; -+ } -+} -diff --git a/app/code/Magento/Catalog/Plugin/Model/ResourceModel/Config.php b/app/code/Magento/Catalog/Plugin/Model/ResourceModel/Config.php -index dfa06b6ebe6..b942f5570f5 100644 ---- a/app/code/Magento/Catalog/Plugin/Model/ResourceModel/Config.php -+++ b/app/code/Magento/Catalog/Plugin/Model/ResourceModel/Config.php -@@ -8,6 +8,9 @@ namespace Magento\Catalog\Plugin\Model\ResourceModel; - use Magento\Framework\App\ObjectManager; - use Magento\Framework\Serialize\SerializerInterface; - -+/** -+ * Config cache plugin. -+ */ - class Config - { - /**#@+ -@@ -46,8 +49,10 @@ class Config - } - - /** -+ * Cache attribute used in listing. -+ * - * @param \Magento\Catalog\Model\ResourceModel\Config $config -- * @param callable $proceed -+ * @param \Closure $proceed - * @return array - */ - public function aroundGetAttributesUsedInListing( -@@ -73,8 +78,10 @@ class Config - } - - /** -+ * Cache attributes used for sorting. -+ * - * @param \Magento\Catalog\Model\ResourceModel\Config $config -- * @param callable $proceed -+ * @param \Closure $proceed - * @return array - */ - public function aroundGetAttributesUsedForSortBy( -diff --git a/app/code/Magento/Catalog/Plugin/Model/ResourceModel/ReadSnapshotPlugin.php b/app/code/Magento/Catalog/Plugin/Model/ResourceModel/ReadSnapshotPlugin.php -index 4dae4ec68ef..ff4d2f93c91 100644 ---- a/app/code/Magento/Catalog/Plugin/Model/ResourceModel/ReadSnapshotPlugin.php -+++ b/app/code/Magento/Catalog/Plugin/Model/ResourceModel/ReadSnapshotPlugin.php -@@ -58,7 +58,9 @@ class ReadSnapshotPlugin - $globalAttributes = []; - $attributesMap = []; - $eavEntityType = $metadata->getEavEntityType(); -- $attributes = (null === $eavEntityType) ? [] : $this->config->getEntityAttributes($eavEntityType); -+ $attributes = null === $eavEntityType -+ ? [] -+ : $this->config->getEntityAttributes($eavEntityType, new \Magento\Framework\DataObject($entityData)); - - /** @var \Magento\Eav\Model\Entity\Attribute\AbstractAttribute $attribute */ - foreach ($attributes as $attribute) { -diff --git a/app/code/Magento/Catalog/Pricing/Price/BasePrice.php b/app/code/Magento/Catalog/Pricing/Price/BasePrice.php -index 54a13be864d..77368517a31 100644 ---- a/app/code/Magento/Catalog/Pricing/Price/BasePrice.php -+++ b/app/code/Magento/Catalog/Pricing/Price/BasePrice.php -@@ -30,7 +30,7 @@ class BasePrice extends AbstractPrice - $this->value = false; - foreach ($this->priceInfo->getPrices() as $price) { - if ($price instanceof BasePriceProviderInterface && $price->getValue() !== false) { -- $this->value = min($price->getValue(), $this->value ?: $price->getValue()); -+ $this->value = min($price->getValue(), $this->value !== false ? $this->value: $price->getValue()); - } - } - } -diff --git a/app/code/Magento/Catalog/Pricing/Price/MinimalTierPriceCalculator.php b/app/code/Magento/Catalog/Pricing/Price/MinimalTierPriceCalculator.php -index 387ef9416ef..a5e573caa38 100644 ---- a/app/code/Magento/Catalog/Pricing/Price/MinimalTierPriceCalculator.php -+++ b/app/code/Magento/Catalog/Pricing/Price/MinimalTierPriceCalculator.php -@@ -29,8 +29,10 @@ class MinimalTierPriceCalculator implements MinimalPriceCalculatorInterface - } - - /** -- * Get raw value of "as low as" as a minimal among tier prices -- * {@inheritdoc} -+ * Get raw value of "as low as" as a minimal among tier prices{@inheritdoc} -+ * -+ * @param SaleableInterface $saleableItem -+ * @return float|null - */ - public function getValue(SaleableInterface $saleableItem) - { -@@ -49,8 +51,10 @@ class MinimalTierPriceCalculator implements MinimalPriceCalculatorInterface - } - - /** -- * Return calculated amount object that keeps "as low as" value -- * {@inheritdoc} -+ * Return calculated amount object that keeps "as low as" value{@inheritdoc} -+ * -+ * @param SaleableInterface $saleableItem -+ * @return AmountInterface|null - */ - public function getAmount(SaleableInterface $saleableItem) - { -@@ -58,6 +62,6 @@ class MinimalTierPriceCalculator implements MinimalPriceCalculatorInterface - - return $value === null - ? null -- : $this->calculator->getAmount($value, $saleableItem); -+ : $this->calculator->getAmount($value, $saleableItem, 'tax'); - } - } -diff --git a/app/code/Magento/Catalog/Pricing/Price/RegularPrice.php b/app/code/Magento/Catalog/Pricing/Price/RegularPrice.php -index 1397ceb6bf7..2c4e332e712 100644 ---- a/app/code/Magento/Catalog/Pricing/Price/RegularPrice.php -+++ b/app/code/Magento/Catalog/Pricing/Price/RegularPrice.php -@@ -29,7 +29,7 @@ class RegularPrice extends AbstractPrice implements BasePriceProviderInterface - if ($this->value === null) { - $price = $this->product->getPrice(); - $priceInCurrentCurrency = $this->priceCurrency->convertAndRound($price); -- $this->value = $priceInCurrentCurrency ? floatval($priceInCurrentCurrency) : 0; -+ $this->value = $priceInCurrentCurrency ? (float)$priceInCurrentCurrency : 0; - } - return $this->value; - } -diff --git a/app/code/Magento/Catalog/Pricing/Price/SpecialPrice.php b/app/code/Magento/Catalog/Pricing/Price/SpecialPrice.php -index b1bfc6ff4ad..77c48fdb166 100644 ---- a/app/code/Magento/Catalog/Pricing/Price/SpecialPrice.php -+++ b/app/code/Magento/Catalog/Pricing/Price/SpecialPrice.php -@@ -11,6 +11,7 @@ use Magento\Framework\Pricing\Adjustment\CalculatorInterface; - use Magento\Framework\Pricing\Price\AbstractPrice; - use Magento\Framework\Pricing\Price\BasePriceProviderInterface; - use Magento\Framework\Stdlib\DateTime\TimezoneInterface; -+use Magento\Store\Api\Data\WebsiteInterface; - - /** - * Special price model -@@ -46,6 +47,8 @@ class SpecialPrice extends AbstractPrice implements SpecialPriceInterface, BaseP - } - - /** -+ * Retrieve special price. -+ * - * @return bool|float - */ - public function getValue() -@@ -96,19 +99,19 @@ class SpecialPrice extends AbstractPrice implements SpecialPriceInterface, BaseP - } - - /** -- * @return bool -+ * @inheritdoc - */ - public function isScopeDateInInterval() - { - return $this->localeDate->isScopeDateInInterval( -- $this->product->getStore(), -+ WebsiteInterface::ADMIN_CODE, - $this->getSpecialFromDate(), - $this->getSpecialToDate() - ); - } - - /** -- * @return bool -+ * @inheritdoc - */ - public function isPercentageDiscount() - { -diff --git a/app/code/Magento/Catalog/Pricing/Price/TierPrice.php b/app/code/Magento/Catalog/Pricing/Price/TierPrice.php -index 74f98c2e66a..f250927889c 100644 ---- a/app/code/Magento/Catalog/Pricing/Price/TierPrice.php -+++ b/app/code/Magento/Catalog/Pricing/Price/TierPrice.php -@@ -80,7 +80,7 @@ class TierPrice extends AbstractPrice implements TierPriceInterface, BasePricePr - GroupManagementInterface $groupManagement, - CustomerGroupRetrieverInterface $customerGroupRetriever = null - ) { -- $quantity = floatval($quantity) ? $quantity : 1; -+ $quantity = (float)$quantity ? $quantity : 1; - parent::__construct($saleableItem, $quantity, $calculator, $priceCurrency); - $this->customerSession = $customerSession; - $this->groupManagement = $groupManagement; -diff --git a/app/code/Magento/Catalog/Pricing/Render/PriceBox.php b/app/code/Magento/Catalog/Pricing/Render/PriceBox.php -index 190168ed583..678b45ce97e 100644 ---- a/app/code/Magento/Catalog/Pricing/Render/PriceBox.php -+++ b/app/code/Magento/Catalog/Pricing/Render/PriceBox.php -@@ -3,6 +3,8 @@ - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -+declare(strict_types=1); -+ - namespace Magento\Catalog\Pricing\Render; - - use Magento\Catalog\Model\Product; -@@ -71,7 +73,9 @@ class PriceBox extends PriceBoxRender - * - * @param int $length - * @param string|null $chars -+ * - * @return string -+ * @throws \Magento\Framework\Exception\LocalizedException - */ - public function getRandomString($length, $chars = null) - { -@@ -93,4 +97,21 @@ class PriceBox extends PriceBoxRender - } - return true; - } -+ -+ /** -+ * Format percent -+ * -+ * @param float $percent -+ * -+ * @return string -+ */ -+ public function formatPercent(float $percent): string -+ { -+ /*First rtrim - trim zeros. So, 10.00 -> 10.*/ -+ /*Second rtrim - trim dot. So, 10. -> 10*/ -+ return rtrim( -+ rtrim(number_format($percent, 2), '0'), -+ '.' -+ ); -+ } - } -diff --git a/app/code/Magento/Catalog/Setup/CategorySetup.php b/app/code/Magento/Catalog/Setup/CategorySetup.php -index 27138793282..f8542454bef 100644 ---- a/app/code/Magento/Catalog/Setup/CategorySetup.php -+++ b/app/code/Magento/Catalog/Setup/CategorySetup.php -@@ -10,7 +10,6 @@ namespace Magento\Catalog\Setup; - use Magento\Catalog\Block\Adminhtml\Category\Helper\Pricestep; - use Magento\Catalog\Block\Adminhtml\Category\Helper\Sortby\Available; - use Magento\Catalog\Block\Adminhtml\Category\Helper\Sortby\DefaultSortby; --use Magento\Catalog\Block\Adminhtml\Product\Helper\Form\BaseImage; - use Magento\Catalog\Block\Adminhtml\Product\Helper\Form\Category as CategoryFormHelper; - use Magento\Catalog\Block\Adminhtml\Product\Helper\Form\Weight as WeightFormHelper; - use Magento\Catalog\Model\Attribute\Backend\Customlayoutupdate; -@@ -54,6 +53,8 @@ use Magento\Catalog\Model\Product\Type; - use Magento\Theme\Model\Theme\Source\Theme; - - /** -+ * Setup category with default entities. -+ * - * @SuppressWarnings(PHPMD.CouplingBetweenObjects) - */ - class CategorySetup extends EavSetup -@@ -593,7 +594,6 @@ class CategorySetup extends EavSetup - 'label' => 'Base Image', - 'input' => 'media_image', - 'frontend' => ImageFrontendModel::class, -- 'input_renderer' => BaseImage::class, - 'required' => false, - 'sort_order' => 0, - 'global' => ScopedAttributeInterface::SCOPE_STORE, -@@ -626,7 +626,6 @@ class CategorySetup extends EavSetup - 'type' => 'varchar', - 'label' => 'Media Gallery', - 'input' => 'gallery', -- 'backend' => Media::class, - 'required' => false, - 'sort_order' => 4, - 'group' => 'Images', -diff --git a/app/code/Magento/Catalog/Setup/Patch/Data/InstallDefaultPriceIndexerDimensionsMode.php b/app/code/Magento/Catalog/Setup/Patch/Data/InstallDefaultPriceIndexerDimensionsMode.php -deleted file mode 100644 -index 9cb41738325..00000000000 ---- a/app/code/Magento/Catalog/Setup/Patch/Data/InstallDefaultPriceIndexerDimensionsMode.php -+++ /dev/null -@@ -1,79 +0,0 @@ --<?php --/** -- * Copyright © Magento, Inc. All rights reserved. -- * See COPYING.txt for license details. -- */ --declare(strict_types=1); -- --namespace Magento\Catalog\Setup\Patch\Data; -- --use Magento\Catalog\Model\Indexer\Product\Price\ModeSwitcher; --use Magento\Framework\Setup\ModuleDataSetupInterface; --use Magento\Framework\Setup\Patch\DataPatchInterface; --use Magento\Framework\Setup\Patch\PatchVersionInterface; --use Magento\Catalog\Model\Indexer\Product\Price\DimensionModeConfiguration; -- --/** -- * Class InstallDefaultCategories data patch. -- * -- * @package Magento\Catalog\Setup\Patch -- */ --class InstallDefaultPriceIndexerDimensionsMode implements DataPatchInterface, PatchVersionInterface --{ -- /** -- * @var ModuleDataSetupInterface -- */ -- private $moduleDataSetup; -- -- /** -- * PatchInitial constructor. -- * @param ModuleDataSetupInterface $moduleDataSetup -- */ -- public function __construct( -- ModuleDataSetupInterface $moduleDataSetup -- ) { -- $this->moduleDataSetup = $moduleDataSetup; -- } -- -- /** -- * {@inheritdoc} -- */ -- public function apply() -- { -- $configTable = $this->moduleDataSetup->getTable('core_config_data'); -- -- $this->moduleDataSetup->getConnection()->insert( -- $configTable, -- [ -- 'scope' => 'default', -- 'scope_id' => 0, -- 'value' => DimensionModeConfiguration::DIMENSION_NONE, -- 'path' => ModeSwitcher::XML_PATH_PRICE_DIMENSIONS_MODE -- ] -- ); -- } -- -- /** -- * {@inheritdoc} -- */ -- public static function getDependencies() -- { -- return []; -- } -- -- /** -- * {@inheritdoc} -- */ -- public function getAliases() -- { -- return []; -- } -- -- /** -- * {@inheritdoc} -- */ -- public static function getVersion() -- { -- return '2.2.6'; -- } --} -diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AddProductToCartActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AddProductToCartActionGroup.xml -index 44c960dc376..b47e13a1182 100644 ---- a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AddProductToCartActionGroup.xml -+++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AddProductToCartActionGroup.xml -@@ -6,14 +6,25 @@ - */ - --> - <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="AddSimpleProductToCart"> - <arguments> - <argument name="product" defaultValue="product"/> - </arguments> -- <amOnPage url="/{{product.name}}.html" stepKey="navigateProductPage"/> -+ <amOnPage url="{{StorefrontProductPage.url(product.custom_attributes[url_key])}}" stepKey="goToProductPage"/> -+ <waitForPageLoad stepKey="waitForProductPage"/> - <click selector="{{StorefrontProductPageSection.addToCartBtn}}" stepKey="addToCart"/> -- <waitForElementVisible selector="{{StorefrontProductPageSection.successMsg}}" time="30" stepKey="waitForProductAdded"/> -- <see selector="{{StorefrontCategoryMainSection.SuccessMsg}}" userInput="You added {{product.name}} to your shopping cart." stepKey="seeAddedToCartMessage"/> -+ <waitForElementNotVisible selector="{{StorefrontProductActionSection.addToCartButtonTitleIsAdding}}" stepKey="waitForElementNotVisibleAddToCartButtonTitleIsAdding"/> -+ <waitForElementNotVisible selector="{{StorefrontProductActionSection.addToCartButtonTitleIsAdded}}" stepKey="waitForElementNotVisibleAddToCartButtonTitleIsAdded"/> -+ <waitForElementVisible selector="{{StorefrontProductActionSection.addToCartButtonTitleIsAddToCart}}" stepKey="waitForElementVisibleAddToCartButtonTitleIsAddToCart"/> -+ <waitForPageLoad stepKey="waitForPageLoad"/> -+ <waitForElementVisible selector="{{StorefrontMessagesSection.success}}" time="30" stepKey="waitForProductAddedMessage"/> -+ <see selector="{{StorefrontMessagesSection.success}}" userInput="You added {{product.name}} to your shopping cart." stepKey="seeAddToCartSuccessMessage"/> -+ </actionGroup> -+ <actionGroup name="StorefrontAddSimpleProductWithQtyActionGroup" extends="AddSimpleProductToCart"> -+ <arguments> -+ <argument name="quantity" type="string" defaultValue="1"/> -+ </arguments> -+ <fillField userInput="{{quantity}}" selector="{{StorefrontProductPageSection.qtyInput}}" stepKey="fillProductQty" after="goToProductPage"/> - </actionGroup> - </actionGroups> -diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AddWebsiteToProductActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AddWebsiteToProductActionGroup.xml -new file mode 100644 -index 00000000000..9a4d15a0909 ---- /dev/null -+++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AddWebsiteToProductActionGroup.xml -@@ -0,0 +1,23 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> -+ <actionGroup name="AddWebsiteToProductActionGroup"> -+ <arguments> -+ <argument name="website" type="string"/> -+ </arguments> -+ <scrollTo selector="{{ProductInWebsitesSection.sectionHeader}}" stepKey="scrollToProductInWebsiteSectionHeader"/> -+ <click selector="{{ProductInWebsitesSection.sectionHeader}}" stepKey="clickProductInWebsiteSectionHeader"/> -+ <checkOption selector="{{ProductInWebsitesSection.website(website)}}" stepKey="checkWebsite"/> -+ <scrollToTopOfPage stepKey="scrollToTopOfAdminProductFormSection"/> -+ <click selector="{{AdminProductFormSection.save}}" stepKey="clickSaveButton"/> -+ <waitForPageLoad stepKey="waitForSimpleProductSaved"/> -+ <see selector="{{AdminProductFormSection.successMessage}}" userInput="You saved the product." stepKey="seeAssertProductSaveSuccessMessage"/> -+ </actionGroup> -+</actionGroups> -\ No newline at end of file -diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminAddAdvancedPricingToTheProductActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminAddAdvancedPricingToTheProductActionGroup.xml -new file mode 100644 -index 00000000000..a8d2f7d9860 ---- /dev/null -+++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminAddAdvancedPricingToTheProductActionGroup.xml -@@ -0,0 +1,34 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> -+ <!-- You must already be on the product form > Advanced Inventory; -+ Action group can be used for customer group price and tier price --> -+ <actionGroup name="AdminAddAdvancedPricingToTheProductActionGroup"> -+ <arguments> -+ <argument name="index" type="string"/> -+ <argument name="groupPrice" type="entity"/> -+ </arguments> -+ <waitForPageLoad stepKey="waitForPageLoad"/> -+ <click selector="{{AdminProductFormSection.advancedPricingLink}}" stepKey="clickAdvancedPricingLink"/> -+ <click selector="{{AdminProductFormAdvancedPricingSection.addCustomerGroupPrice}}" stepKey="clickCustomerGroupPriceAddButton"/> -+ <selectOption selector="{{AdminProductFormAdvancedPricingSection.productTierPriceWebsiteSelect(index)}}" userInput="{{groupPrice.website_id}}" stepKey="selectProductTierPriceWebsiteInput"/> -+ <selectOption selector="{{AdminProductFormAdvancedPricingSection.productTierPriceCustGroupSelect(index)}}" userInput="{{groupPrice.customer_group}}" stepKey="selectProductTierPriceCustomerGroupInput"/> -+ <fillField selector="{{AdminProductFormAdvancedPricingSection.productTierPriceQtyInput(index)}}" userInput="{{groupPrice.quantity}}" stepKey="fillProductTierPriceQuantityInput"/> -+ <fillField selector="{{AdminProductFormAdvancedPricingSection.productTierPriceFixedPriceInput(index)}}" userInput="{{groupPrice.price}}" stepKey="selectProductTierPriceFixedPrice"/> -+ <click selector="{{AdminProductFormAdvancedPricingSection.doneButton}}" stepKey="clickDoneButton"/> -+ </actionGroup> -+ -+ <!-- Customer group is selected in different way for B2B --> -+ <actionGroup name="AdminAddAdvancedPricingToTheProductExtendedActionGroup" extends="AdminAddAdvancedPricingToTheProductActionGroup"> -+ <remove keyForRemoval="selectProductTierPriceCustomerGroupInput"/> -+ <click selector="{{AdminProductFormAdvancedPricingSection.productTierPriceCustGroupSelect(index)}}" stepKey="clickProductTierPriceCustGroupSelect" after="selectProductTierPriceWebsiteInput"/> -+ <waitForElement selector="{{AdminProductFormAdvancedPricingSection.productTierPriceGroupOrCatalogOption(groupPrice.customer_group)}}" time="30" stepKey="waitProductTierPriceGroupOrCatalogOption" after="clickProductTierPriceCustGroupSelect"/> -+ <click selector="{{AdminProductFormAdvancedPricingSection.productTierPriceGroupOrCatalogOption(groupPrice.customer_group)}}" stepKey="clickAllGroupsOption" after="waitProductTierPriceGroupOrCatalogOption"/> -+ </actionGroup> -+</actionGroups> -diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminAddMinimumAdvertisedPriceActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminAddMinimumAdvertisedPriceActionGroup.xml -new file mode 100644 -index 00000000000..f0eef98748f ---- /dev/null -+++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminAddMinimumAdvertisedPriceActionGroup.xml -@@ -0,0 +1,21 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> -+ <actionGroup name="AdminAddMinimumAdvertisedPriceActionGroup"> -+ <arguments> -+ <argument name="msrpData" type="entity"/> -+ </arguments> -+ <waitForPageLoad stepKey="waitForPageLoad"/> -+ <click selector="{{AdminProductFormSection.advancedPricingLink}}" stepKey="clickAdvancedPricingLink"/> -+ <waitForElementVisible selector="{{AdminProductFormAdvancedPricingSection.msrp}}" stepKey="waitSpecialPrice"/> -+ <fillField selector="{{AdminProductFormAdvancedPricingSection.msrp}}" userInput="{{msrpData.msrp}}" stepKey="fillMinimumAdvertisedPrice"/> -+ <selectOption selector="{{AdminProductFormAdvancedPricingSection.msrpType}}" userInput="{{msrpData.msrp_display_actual_price_type}}" stepKey="selectPriceType"/> -+ <click selector="{{AdminProductFormAdvancedPricingSection.doneButton}}" stepKey="clickDone"/> -+ <waitForElementNotVisible selector="{{AdminProductFormAdvancedPricingSection.msrp}}" stepKey="waitForCloseModalWindow"/> -+ </actionGroup> -+</actionGroups> -diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminAddProductCustomOptionActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminAddProductCustomOptionActionGroup.xml -new file mode 100644 -index 00000000000..8e50ac6edc6 ---- /dev/null -+++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminAddProductCustomOptionActionGroup.xml -@@ -0,0 +1,24 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> -+ <!-- Add custom option, title and type --> -+ <actionGroup name="AdminAddProductCustomOptionActionGroup"> -+ <arguments> -+ <argument name="customOptionTitle" type="string"/> -+ <argument name="customOptionType" type="string"/> -+ </arguments> -+ <scrollTo selector="{{AdminProductCustomizableOptionsSection.customizableOptions}}" stepKey="scrollToCustomizableOptionsSection"/> -+ <waitForPageLoad stepKey="waitForScrolling"/> -+ <click stepKey="clickAddOptions" selector="{{AdminProductCustomizableOptionsSection.addOptionBtn}}"/> -+ <waitForPageLoad stepKey="waitForAddProductPageLoad"/> -+ <fillField stepKey="fillInOptionTitle" selector="{{AdminProductCustomizableOptionsSection.lastOptionTitle}}" userInput="{{customOptionTitle}}"/> -+ <click stepKey="clickOptionTypeParent" selector="{{AdminProductCustomizableOptionsSection.lastOptionTypeParent}}"/> -+ <waitForPageLoad stepKey="waitForDropdownOpen"/> -+ <click stepKey="clickOptionType" selector="{{AdminProductCustomizableOptionsSection.optionType(customOptionType)}}"/> -+ </actionGroup> -+</actionGroups> -diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminAddTitleAndPriceValueToCustomOptionActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminAddTitleAndPriceValueToCustomOptionActionGroup.xml -new file mode 100644 -index 00000000000..3a03eb60555 ---- /dev/null -+++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminAddTitleAndPriceValueToCustomOptionActionGroup.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"> -+ <!-- Add value, value title and value price to custom options (type: drop-down, checkbox, multiple select, radio buttons) --> -+ <actionGroup name="AdminAddTitleAndPriceValueToCustomOptionActionGroup"> -+ <arguments> -+ <argument name="optionValue" type="entity"/> -+ </arguments> -+ <click stepKey="clickAddValue" selector="{{AdminProductCustomizableOptionsSection.addValue}}"/> -+ <fillField stepKey="fillInValueTitle" selector="{{AdminProductCustomizableOptionsSection.valueTitle}}" userInput="{{optionValue.title}}"/> -+ <fillField stepKey="fillInValuePrice" selector="{{AdminProductCustomizableOptionsSection.valuePrice}}" userInput="{{optionValue.price}}"/> -+ </actionGroup> -+</actionGroups> -diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminAnchorCategoryActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminAnchorCategoryActionGroup.xml -new file mode 100644 -index 00000000000..62e9a4b6615 ---- /dev/null -+++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminAnchorCategoryActionGroup.xml -@@ -0,0 +1,29 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> -+ <actionGroup name="AdminAnchorCategoryActionGroup"> -+ <arguments> -+ <argument name="categoryName" type="string"/> -+ </arguments> -+ <!--Open Category page--> -+ <amOnPage url="{{AdminCategoryPage.url}}" stepKey="openAdminCategoryIndexPage"/> -+ <waitForPageLoad stepKey="waitForPageToLoaded"/> -+ <click selector="{{AdminCategorySidebarTreeSection.expandAll}}" stepKey="clickOnExpandTree"/> -+ <waitForPageLoad stepKey="waitForCategoryToLoad"/> -+ <click selector="{{AdminCategorySidebarTreeSection.categoryInTree(categoryName)}}" stepKey="selectCategory"/> -+ <waitForPageLoad stepKey="waitForPageToLoad"/> -+ -+ <!--Enable Anchor for category --> -+ <scrollTo selector="{{CategoryDisplaySettingsSection.DisplaySettingTab}}" x="0" y="-80" stepKey="scrollToDisplaySetting"/> -+ <click selector="{{CategoryDisplaySettingsSection.DisplaySettingTab}}" stepKey="selectDisplaySetting"/> -+ <checkOption selector="{{CategoryDisplaySettingsSection.anchor}}" stepKey="enableAnchor"/> -+ <click selector="{{AdminCategoryMainActionsSection.SaveButton}}" stepKey="saveSubCategory"/> -+ </actionGroup> -+</actionGroups> -\ No newline at end of file -diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminAssignCategoryToProductAndSaveActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminAssignCategoryToProductAndSaveActionGroup.xml -new file mode 100644 -index 00000000000..0cb7c4885ac ---- /dev/null -+++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminAssignCategoryToProductAndSaveActionGroup.xml -@@ -0,0 +1,24 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> -+ <actionGroup name="AdminAssignCategoryToProductAndSaveActionGroup"> -+ <arguments> -+ <argument name="categoryName" type="string"/> -+ </arguments> -+ <!-- on edit Product page catalog/product/edit/id/{{product_id}}/ --> -+ <click selector="{{AdminProductFormSection.categoriesDropdown}}" stepKey="openDropDown"/> -+ <checkOption selector="{{AdminProductFormSection.selectCategory(categoryName)}}" stepKey="selectCategory"/> -+ <click selector="{{AdminProductFormSection.done}}" stepKey="clickDone"/> -+ <waitForPageLoad stepKey="waitForApplyCategory"/> -+ <click selector="{{AdminProductFormSection.save}}" stepKey="clickSave"/> -+ <waitForPageLoad stepKey="waitForSavingProduct"/> -+ <see userInput="You saved the product." selector="{{CatalogProductsSection.messageSuccessSavedProduct}}" stepKey="seeSuccessMessage"/> -+ </actionGroup> -+</actionGroups> -\ No newline at end of file -diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminAssignImageRolesActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminAssignImageRolesActionGroup.xml -new file mode 100644 -index 00000000000..90ceb1e4a1f ---- /dev/null -+++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminAssignImageRolesActionGroup.xml -@@ -0,0 +1,24 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> -+ <actionGroup name="AdminAssignImageRolesActionGroup"> -+ <arguments> -+ <argument name="image"/> -+ </arguments> -+ <conditionalClick selector="{{AdminProductImagesSection.productImagesToggleState('closed')}}" dependentSelector="{{AdminProductImagesSection.productImagesToggleState('open')}}" visible="false" stepKey="clickSectionImage"/> -+ <click selector="{{AdminProductImagesSection.imageFile(image.fileName)}}" stepKey="clickProductImage"/> -+ <waitForElementVisible selector="{{AdminProductImagesSection.altText}}" stepKey="seeAltTextSection"/> -+ <checkOption selector="{{AdminProductImagesSection.roleBase}}" stepKey="checkRoleBase"/> -+ <checkOption selector="{{AdminProductImagesSection.roleSmall}}" stepKey="checkRoleSmall"/> -+ <checkOption selector="{{AdminProductImagesSection.roleThumbnail}}" stepKey="checkRoleThumbnail"/> -+ <checkOption selector="{{AdminProductImagesSection.roleSwatch}}" stepKey="checkRoleSwatch"/> -+ <click selector="{{AdminSlideOutDialogSection.closeButton}}" stepKey="clickCloseButton"/> -+ </actionGroup> -+</actionGroups> -diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminCategoryActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminCategoryActionGroup.xml -index 7c04e9bd83d..90d732c9654 100644 ---- a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminCategoryActionGroup.xml -+++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminCategoryActionGroup.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"> - <!--Create a new category--> - <actionGroup name="CreateCategory"> - <arguments> -@@ -154,7 +154,7 @@ - - <fillField stepKey="fillCategoryName" selector="{{AdminProductCategoryCreationSection.nameInput}}" userInput="{{categoryName}}"/> - -- <!-- Search and select a parent catagory for the product --> -+ <!-- Search and select a parent category for the product --> - <click stepKey="clickParentCategory" selector="{{AdminProductCategoryCreationSection.parentCategory}}"/> - <waitForPageLoad stepKey="waitForDropDownVisible"/> - <fillField stepKey="searchForParent" userInput="{{parentCategoryName}}" selector="{{AdminProductCategoryCreationSection.parentSearch}}"/> -@@ -231,14 +231,89 @@ - <waitForPageLoad stepKey="waitForStoreViewChangeLoad"/> - </actionGroup> - -+ <actionGroup name="switchCategoryToAllStoreView"> -+ <arguments> -+ <argument name="CatName"/> -+ </arguments> -+ <click selector="{{AdminCategorySidebarTreeSection.categoryInTree(CatName)}}" stepKey="navigateToCreatedCategory" /> -+ <waitForPageLoad stepKey="waitForPageLoad1"/> -+ <waitForLoadingMaskToDisappear stepKey="waitForSpinner1"/> -+ <scrollToTopOfPage stepKey="scrollToToggle"/> -+ <click selector="{{AdminCategoryMainActionsSection.CategoryStoreViewDropdownToggle}}" stepKey="openStoreViewDropDown"/> -+ <click selector="{{AdminCategoryMainActionsSection.allStoreViews}}" stepKey="clickStoreViewByName"/> -+ <see selector="{{AdminCategoryMainActionsSection.storeSwitcher}}" userInput="All Store Views" stepKey="seeAllStoreView"/> -+ <waitForPageLoad stepKey="waitForPageLoad2"/> -+ <waitForLoadingMaskToDisappear stepKey="waitForSpinner2"/> -+ <click selector="{{AdminCategoryMainActionsSection.CategoryStoreViewModalAccept}}" stepKey="selectStoreViewAccept"/> -+ <waitForPageLoad stepKey="waitForStoreViewChangeLoad"/> -+ </actionGroup> -+ - <actionGroup name="navigateToCreatedCategory"> - <arguments> - <argument name="Category"/> - </arguments> - <amOnPage url="{{AdminCategoryPage.page}}" stepKey="amOnCategoryPage"/> - <waitForPageLoad stepKey="waitForPageLoad1"/> -- <click selector="{{AdminCategorySidebarTreeSection.categoryInTree(Category.Name)}}" stepKey="navigateToCreatedCategory" /> -+ <click selector="{{AdminCategorySidebarTreeSection.expandAll}}" stepKey="expandAll"/> - <waitForPageLoad stepKey="waitForPageLoad2"/> -+ <click selector="{{AdminCategorySidebarTreeSection.categoryInTree(Category.Name)}}" stepKey="navigateToCreatedCategory" /> - <waitForLoadingMaskToDisappear stepKey="waitForSpinner" /> - </actionGroup> -+ -+ <actionGroup name="ChangeSeoUrlKey"> -+ <arguments> -+ <argument name="value" type="string"/> -+ </arguments> -+ <click selector="{{AdminCategorySEOSection.SectionHeader}}" stepKey="openSeoSection"/> -+ <fillField selector="{{AdminCategorySEOSection.UrlKeyInput}}" userInput="{{value}}" stepKey="enterURLKey"/> -+ <click selector="{{AdminCategoryMainActionsSection.SaveButton}}" stepKey="saveCategory"/> -+ <seeElement selector="{{AdminCategoryMessagesSection.SuccessMessage}}" stepKey="assertSuccessMessage"/> -+ </actionGroup> -+ -+ <actionGroup name="ChangeSeoUrlKeyForSubCategory"> -+ <arguments> -+ <argument name="value" type="string"/> -+ </arguments> -+ <click selector="{{AdminCategorySEOSection.SectionHeader}}" stepKey="openSeoSection"/> -+ <uncheckOption selector="{{AdminCategorySEOSection.UrlKeyDefaultValueCheckbox}}" stepKey="uncheckDefaultValue"/> -+ <fillField selector="{{AdminCategorySEOSection.UrlKeyInput}}" userInput="{{value}}" stepKey="enterURLKey"/> -+ <click selector="{{AdminCategoryMainActionsSection.SaveButton}}" stepKey="saveCategory"/> -+ <seeElement selector="{{AdminCategoryMessagesSection.SuccessMessage}}" stepKey="assertSuccessMessage"/> -+ </actionGroup> -+ <actionGroup name="OpenCategoryFromCategoryTree"> -+ <arguments> -+ <argument name="category" type="string"/> -+ </arguments> -+ <amOnPage url="{{AdminCategoryPage.url}}" stepKey="openAdminCategoryIndexPage"/> -+ <waitForPageLoad stepKey="waitForCategoryPageToLoad"/> -+ <click selector="{{AdminCategorySidebarTreeSection.expandAll}}" stepKey="clickOnExpandTree"/> -+ <waitForPageLoad stepKey="waitForCategoryToLoad"/> -+ <click selector="{{AdminCategorySidebarTreeSection.categoryInTree(category)}}" stepKey="selectCategory"/> -+ <waitForPageLoad stepKey="waitForPageToLoad"/> -+ <waitForElementVisible selector="{{AdminCategoryContentSection.categoryPageTitle}}" stepKey="waitForCategoryTitle"/> -+ </actionGroup> -+ <actionGroup name="AdminAssignProductToCategory"> -+ <arguments> -+ <argument name="productId" type="string"/> -+ <argument name="categoryName" type="string"/> -+ </arguments> -+ <amOnPage url="{{AdminProductEditPage.url(productId)}}" stepKey="amOnPage"/> -+ <searchAndMultiSelectOption selector="{{AdminProductFormSection.categoriesDropdown}}" parameterArray="[{{categoryName}}]" stepKey="selectCategory"/> -+ <click selector="{{AdminProductFormActionSection.saveButton}}" stepKey="clickOnSaveButton"/> -+ <see selector="{{AdminMessagesSection.success}}" userInput="You saved the product." stepKey="seeSaveProductMessage"/> -+ </actionGroup> -+ <actionGroup name="FillCategoryNameAndUrlKeyAndSave"> -+ <arguments> -+ <argument name="categoryName" type="string"/> -+ <argument name="categoryUrlKey" type="string"/> -+ </arguments> -+ <fillField selector="{{AdminCategoryBasicFieldSection.CategoryNameInput}}" userInput="{{categoryName}}" stepKey="enterCategoryName"/> -+ <scrollTo selector="{{AdminCategorySEOSection.SectionHeader}}" stepKey="scrollToSearchEngineOptimization"/> -+ <click selector="{{AdminCategorySEOSection.SectionHeader}}" stepKey="openSEO"/> -+ <waitForPageLoad stepKey="waitForPageToLoad"/> -+ <fillField selector="{{AdminCategorySEOSection.UrlKeyInput}}" userInput="{{categoryUrlKey}}" stepKey="enterURLKey"/> -+ <scrollToTopOfPage stepKey="scrollToTheTopOfPage"/> -+ <click selector="{{AdminCategoryMainActionsSection.SaveButton}}" stepKey="saveCategory"/> -+ <waitForPageLoad stepKey="waitForPageToLoad1"/> -+ </actionGroup> - </actionGroups> -diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminChangeProductSEOSettingsActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminChangeProductSEOSettingsActionGroup.xml -new file mode 100644 -index 00000000000..ec0beac8655 ---- /dev/null -+++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminChangeProductSEOSettingsActionGroup.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="AdminChangeProductSEOSettingsActionGroup"> -+ <arguments> -+ <argument name="productName" defaultValue="_defaultProduct.name"/> -+ </arguments> -+ <click selector="{{AdminProductSEOSection.sectionHeader}}" stepKey="clickSearchEngineOptimizationTab"/> -+ <waitForPageLoad stepKey="waitForTabOpen"/> -+ <fillField selector="{{AdminProductSEOSection.urlKeyInput}}" userInput="{{productName}}" stepKey="setUrlKeyInput"/> -+ <fillField selector="{{AdminProductSEOSection.metaTitleInput}}" userInput="{{productName}}" stepKey="setMetaTitleInput"/> -+ <fillField selector="{{AdminProductSEOSection.metaKeywordsInput}}" userInput="{{productName}}" stepKey="setMetaKeywordsInput"/> -+ <fillField selector="{{AdminProductSEOSection.metaDescriptionInput}}" userInput="{{productName}}" stepKey="setMetaDescriptionInput"/> -+ </actionGroup> -+</actionGroups> -diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminClickOnAdvancedInventoryLinkActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminClickOnAdvancedInventoryLinkActionGroup.xml -new file mode 100644 -index 00000000000..58164ce5f89 ---- /dev/null -+++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminClickOnAdvancedInventoryLinkActionGroup.xml -@@ -0,0 +1,16 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> -+ <!-- ActionGroup click on Advanced Inventory in product form; -+ You must already be on the product form page --> -+ <actionGroup name="AdminClickOnAdvancedInventoryLinkActionGroup"> -+ <click selector="{{AdminProductFormSection.advancedInventoryLink}}" stepKey="clickOnAdvancedInventoryLink"/> -+ <waitForPageLoad stepKey="waitForAdvancedInventoryPageToLoad"/> -+ </actionGroup> -+</actionGroups> -diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminCreateCustomDropDownOptionsActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminCreateCustomDropDownOptionsActionGroup.xml -new file mode 100644 -index 00000000000..a674647a5c8 ---- /dev/null -+++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminCreateCustomDropDownOptionsActionGroup.xml -@@ -0,0 +1,33 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> -+ <actionGroup name="AdminCreateCustomDropDownOptionsActionGroup"> -+ <!-- ActionGroup will add a single custom option to a product -+ You must already be on the product creation page --> -+ <arguments> -+ <argument name="customOptionName" type="string"/> -+ <argument name="firstOption" type="entity"/> -+ <argument name="secondOption" type="entity"/> -+ </arguments> -+ <click stepKey="clickAddOptions" selector="{{AdminProductCustomizableOptionsSection.addOptionBtn}}"/> -+ <waitForPageLoad stepKey="waitForAddProductPageLoad"/> -+ <!-- Fill in the option and select the type of drop down --> -+ <fillField stepKey="fillInOptionTitle" selector="{{AdminProductCustomizableOptionsSection.lastOptionTitle}}" userInput="{{customOptionName}}"/> -+ <click stepKey="clickOptionTypeParent" selector="{{AdminProductCustomizableOptionsSection.lastOptionTypeParent}}"/> -+ <waitForPageLoad stepKey="waitForDropdownOpen"/> -+ <click stepKey="clickOptionType" selector="{{AdminProductCustomizableOptionsSection.optionType('Drop-down')}}"/> -+ <!-- Add option based on the parameter --> -+ <click stepKey="clickAddFirstValue" selector="{{AdminProductCustomizableOptionsSection.addValue}}"/> -+ <fillField stepKey="fillInFirstOptionValueTitle" selector="{{AdminProductCustomizableOptionsSection.valueTitle}}" userInput="{{firstOption.title}}"/> -+ <fillField stepKey="fillInFirstOptionValuePrice" selector="{{AdminProductCustomizableOptionsSection.valuePrice}}" userInput="{{firstOption.price}}"/> -+ <click stepKey="clickAddSecondValue" selector="{{AdminProductCustomizableOptionsSection.addValue}}"/> -+ <fillField stepKey="fillInSecondOptionValueTitle" selector="{{AdminProductCustomizableOptionsSection.valueTitle}}" userInput="{{secondOption.title}}"/> -+ <fillField stepKey="fillInSecondOptionValuePrice" selector="{{AdminProductCustomizableOptionsSection.valuePrice}}" userInput="{{secondOption.price}}"/> -+ </actionGroup> -+</actionGroups> -diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminCreateRootCategoryActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminCreateRootCategoryActionGroup.xml -index e7d9a63484b..a99420bcf95 100644 ---- a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminCreateRootCategoryActionGroup.xml -+++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminCreateRootCategoryActionGroup.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"> - <!--Create a new root category--> - <actionGroup name="AdminCreateRootCategory"> - <arguments> -diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminCreateWidgetActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminCreateWidgetActionGroup.xml -new file mode 100644 -index 00000000000..dd66919640a ---- /dev/null -+++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminCreateWidgetActionGroup.xml -@@ -0,0 +1,17 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> -+ <actionGroup name="AdminCreateRecentlyProductsWidgetActionGroup" extends="AdminCreateWidgetActionGroup"> -+ <selectOption selector="{{AdminCatalogProductWidgetSection.productAttributesToShow}}" parameterArray="['Name', 'Image', 'Price']" stepKey="selectAllProductAttributes"/> -+ <selectOption selector="{{AdminCatalogProductWidgetSection.productButtonsToShow}}" parameterArray="['Add to Cart', 'Add to Compare', 'Add to Wishlist']" stepKey="selectAllProductButtons"/> -+ <click selector="{{AdminMainActionsSection.save}}" stepKey="clickSaveWidget"/> -+ <waitForElementVisible selector="{{AdminMessagesSection.successMessage}}" stepKey="waitForSuccessMessageAppears"/> -+ <see selector="{{AdminMessagesSection.successMessage}}" userInput="The widget instance has been saved" stepKey="seeSuccess"/> -+ </actionGroup> -+</actionGroups> -diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminFillAdvancedInventoryOutOfStockThresholdActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminFillAdvancedInventoryOutOfStockThresholdActionGroup.xml -new file mode 100644 -index 00000000000..d0116467be5 ---- /dev/null -+++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminFillAdvancedInventoryOutOfStockThresholdActionGroup.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"> -+ <!-- You must already be on the product form > Advanced Inventory --> -+ <actionGroup name="AdminFillAdvancedInventoryOutOfStockThresholdActionGroup"> -+ <arguments> -+ <argument name="qty" type="string"/> -+ </arguments> -+ <uncheckOption selector="{{AdminProductFormAdvancedInventorySection.minQtyConfigSetting}}" stepKey="uncheckMiniQtyCheckBox"/> -+ <fillField selector="{{AdminProductFormAdvancedInventorySection.outOfStockThreshold}}" userInput="{{qty}}" stepKey="fillMiniAllowedQty"/> -+ </actionGroup> -+</actionGroups> -diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminFillAdvancedInventoryQtyActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminFillAdvancedInventoryQtyActionGroup.xml -new file mode 100644 -index 00000000000..1faa3f04d36 ---- /dev/null -+++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminFillAdvancedInventoryQtyActionGroup.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"> -+ <!-- You must already be on the product form > Advanced Inventory --> -+ <actionGroup name="AdminFillAdvancedInventoryQtyActionGroup"> -+ <arguments> -+ <argument name="qty" type="string"/> -+ </arguments> -+ <fillField selector="{{AdminProductFormAdvancedInventorySection.advancedInventoryQty}}" userInput="{{qty}}" stepKey="fillQty"/> -+ </actionGroup> -+</actionGroups> -diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminFillProductAttributePropertiesActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminFillProductAttributePropertiesActionGroup.xml -new file mode 100644 -index 00000000000..cd850f8a700 ---- /dev/null -+++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminFillProductAttributePropertiesActionGroup.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="AdminFillProductAttributePropertiesActionGroup"> -+ <arguments> -+ <argument name="attributeName" type="string"/> -+ <argument name="attributeType" type="string"/> -+ </arguments> -+ <fillField selector="{{AttributePropertiesSection.DefaultLabel}}" userInput="{{attributeName}}" stepKey="fillDefaultLabel"/> -+ <selectOption selector="{{AttributePropertiesSection.InputType}}" userInput="{{attributeType}}" stepKey="selectInputType"/> -+ </actionGroup> -+</actionGroups> -diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminOpenAttributeSetByNameActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminOpenAttributeSetByNameActionGroup.xml -new file mode 100644 -index 00000000000..1ac7ac5e54b ---- /dev/null -+++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminOpenAttributeSetByNameActionGroup.xml -@@ -0,0 +1,17 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> -+ <actionGroup name="AdminOpenAttributeSetByNameActionGroup"> -+ <arguments> -+ <argument name="attributeSetName" type="string" defaultValue="Default"/> -+ </arguments> -+ <click selector="{{AdminProductAttributeSetGridSection.AttributeSetName(attributeSetName)}}" stepKey="chooseAttributeSet"/> -+ <waitForPageLoad stepKey="waitForAttributeSetPageLoad"/> -+ </actionGroup> -+</actionGroups> -diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminOpenAttributeSetGridPageActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminOpenAttributeSetGridPageActionGroup.xml -new file mode 100644 -index 00000000000..c6f0c3332b1 ---- /dev/null -+++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminOpenAttributeSetGridPageActionGroup.xml -@@ -0,0 +1,14 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> -+ <actionGroup name="AdminOpenAttributeSetGridPageActionGroup"> -+ <amOnPage url="{{AdminProductAttributeSetGridPage.url}}" stepKey="goToAttributeSetPage"/> -+ <waitForPageLoad stepKey="waitForAttributeSetPageLoad"/> -+ </actionGroup> -+</actionGroups> -diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminOpenNewProductFormPageActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminOpenNewProductFormPageActionGroup.xml -new file mode 100644 -index 00000000000..fe859fab526 ---- /dev/null -+++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminOpenNewProductFormPageActionGroup.xml -@@ -0,0 +1,19 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> -+ <actionGroup name="AdminOpenNewProductFormPageActionGroup"> -+ <arguments> -+ <argument name="productType" type="string" defaultValue="simple" /> -+ <argument name="attributeSetId" type="string" defaultValue="{{defaultAttributeSet.attribute_set_id}}" /> -+ </arguments> -+ -+ <amOnPage url="{{AdminProductCreatePage.url(attributeSetId, productType)}}" stepKey="openProductNewPage" /> -+ <waitForPageLoad stepKey="waitForPageLoad" /> -+ </actionGroup> -+</actionGroups> -diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminOpenProductAttributePageActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminOpenProductAttributePageActionGroup.xml -new file mode 100644 -index 00000000000..b6be3fb172d ---- /dev/null -+++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminOpenProductAttributePageActionGroup.xml -@@ -0,0 +1,14 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> -+ <actionGroup name="AdminOpenProductAttributePageActionGroup"> -+ <amOnPage url="{{AdminProductAttributeGridPage.url}}" stepKey="goToAttributePage"/> -+ <waitForPageLoad stepKey="waitForAttributePageLoad"/> -+ </actionGroup> -+</actionGroups> -diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminOpenProductIndexPageActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminOpenProductIndexPageActionGroup.xml -new file mode 100644 -index 00000000000..ca1303f180c ---- /dev/null -+++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminOpenProductIndexPageActionGroup.xml -@@ -0,0 +1,14 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> -+ <actionGroup name="AdminOpenProductIndexPageActionGroup"> -+ <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="goToProductIndexPage"/> -+ <waitForPageLoad stepKey="waitForProductIndexPageLoad"/> -+ </actionGroup> -+</actionGroups> -diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminProductActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminProductActionGroup.xml -index bca6ae2b60b..c1cb69373b0 100644 ---- a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminProductActionGroup.xml -+++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminProductActionGroup.xml -@@ -6,7 +6,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"> - <!--Navigate to create product page from product grid page--> - <actionGroup name="goToCreateProductPage"> - <arguments> -@@ -15,16 +15,25 @@ - <click selector="{{AdminProductGridActionSection.addProductToggle}}" stepKey="clickAddProductToggle"/> - <waitForElementVisible selector="{{AdminProductGridActionSection.addTypeProduct(product.type_id)}}" stepKey="waitForAddProductDropdown" time="30"/> - <click selector="{{AdminProductGridActionSection.addTypeProduct(product.type_id)}}" stepKey="clickAddProductType"/> -- <waitForPageLoad stepKey="waitForCreateProductPageLoad"/> -+ <waitForPageLoad time="30" stepKey="waitForCreateProductPageLoad"/> - <seeInCurrentUrl url="{{AdminProductCreatePage.url(AddToDefaultSet.attributeSetId, product.type_id)}}" stepKey="seeNewProductUrl"/> - <see selector="{{AdminHeaderSection.pageTitle}}" userInput="New Product" stepKey="seeNewProductTitle"/> - </actionGroup> -+ -+ <!--Navigate to create product page directly via ID--> -+ <actionGroup name="goToProductPageViaID"> -+ <arguments> -+ <argument name="productId" type="string"/> -+ </arguments> -+ <amOnPage url="{{AdminProductEditPage.url(productId)}}" stepKey="goToProduct"/> -+ </actionGroup> - -- <!--Fill main fields in create product form--> -+ <!-- Fill main fields in create product form using a product entity --> - <actionGroup name="fillMainProductForm"> - <arguments> - <argument name="product" defaultValue="_defaultProduct"/> - </arguments> -+ <scrollToTopOfPage stepKey="scrollToTopOfPage"/> - <fillField selector="{{AdminProductFormSection.productName}}" userInput="{{product.name}}" stepKey="fillProductName"/> - <fillField selector="{{AdminProductFormSection.productSku}}" userInput="{{product.sku}}" stepKey="fillProductSku"/> - <fillField selector="{{AdminProductFormSection.productPrice}}" userInput="{{product.price}}" stepKey="fillProductPrice"/> -@@ -34,6 +43,25 @@ - <fillField selector="{{AdminProductFormSection.productWeight}}" userInput="{{product.weight}}" stepKey="fillProductWeight"/> - </actionGroup> - -+ <!-- Fill main fields in create product form using strings for flexibility --> -+ <actionGroup name="FillMainProductFormByString"> -+ <arguments> -+ <argument name="productName" type="string"/> -+ <argument name="productSku" type="string"/> -+ <argument name="productPrice" type="string"/> -+ <argument name="productQuantity" type="string"/> -+ <argument name="productStatus" type="string"/> -+ <argument name="productWeight" type="string"/> -+ </arguments> -+ <fillField selector="{{AdminProductFormSection.productName}}" userInput="{{productName}}" stepKey="fillProductName"/> -+ <fillField selector="{{AdminProductFormSection.productSku}}" userInput="{{productSku}}" stepKey="fillProductSku"/> -+ <fillField selector="{{AdminProductFormSection.productPrice}}" userInput="{{productPrice}}" stepKey="fillProductPrice"/> -+ <fillField selector="{{AdminProductFormSection.productQuantity}}" userInput="{{productQuantity}}" stepKey="fillProductQty"/> -+ <selectOption selector="{{AdminProductFormSection.productStockStatus}}" userInput="{{productStatus}}" stepKey="selectStockStatus"/> -+ <selectOption selector="{{AdminProductFormSection.productWeightSelect}}" userInput="This item has weight" stepKey="selectWeight"/> -+ <fillField selector="{{AdminProductFormSection.productWeight}}" userInput="{{productWeight}}" stepKey="fillProductWeight"/> -+ </actionGroup> -+ - <!--Fill main fields in create product form with no weight, useful for virtual and downloadable products --> - <actionGroup name="fillMainProductFormNoWeight"> - <arguments> -@@ -70,10 +98,21 @@ - - <!--Save product and see success message--> - <actionGroup name="saveProductForm"> -+ <scrollToTopOfPage stepKey="scrollTopPageProduct"/> -+ <waitForElementVisible selector="{{AdminProductFormActionSection.saveButton}}" stepKey="waitForSaveProductButton" /> - <click selector="{{AdminProductFormActionSection.saveButton}}" stepKey="clickSaveProduct"/> -+ <waitForPageLoad stepKey="waitForProductToSave"/> - <see selector="{{AdminProductMessagesSection.successMessage}}" userInput="You saved the product." stepKey="seeSaveConfirmation"/> - </actionGroup> - -+ <actionGroup name="toggleProductEnabled"> -+ <click selector="{{AdminProductFormSection.enableProductLabel}}" stepKey="toggleEnabled"/> -+ </actionGroup> -+ <!-- Save product but do not expect a success message --> -+ <actionGroup name="SaveProductFormNoSuccessCheck" extends="saveProductForm"> -+ <remove keyForRemoval="seeSaveConfirmation"/> -+ </actionGroup> -+ - <!--Upload image for product--> - <actionGroup name="addProductImage"> - <arguments> -@@ -159,7 +198,7 @@ - - <click selector="{{AdminProductCustomizableOptionsSection.customizableOptions}}" stepKey="openCustomOptionsSection"/> - <click selector="{{AdminProductCustomizableOptionsSection.addOptionBtn}}" stepKey="clickAddOption"/> -- <fillField userInput="option1" selector="{{AdminProductCustomizableOptionsSection.optionTitleInput}}" stepKey="fillOptionTitle"/> -+ <fillField selector="{{AdminProductCustomizableOptionsSection.optionTitleInput('0')}}" userInput="option1" stepKey="fillOptionTitle"/> - <click selector="{{AdminProductCustomizableOptionsSection.optionTypeOpenDropDown}}" stepKey="openTypeDropDown"/> - <click selector="{{AdminProductCustomizableOptionsSection.optionTypeTextField}}" stepKey="selectTypeTextField"/> - <fillField userInput="20" selector="{{AdminProductCustomizableOptionsSection.maxCharactersInput}}" stepKey="fillMaxChars"/> -@@ -173,6 +212,44 @@ - <seeInField userInput="{{simpleProduct.urlKey}}" selector="{{AdminProductSEOSection.urlKeyInput}}" stepKey="assertFieldUrlKey"/> - </actionGroup> - -+ <actionGroup name="ProductSetWebsite"> -+ <arguments> -+ <argument name="website" type="string"/> -+ </arguments> -+ <scrollTo selector="{{ProductInWebsitesSection.sectionHeader}}" stepKey="scrollToWebsites"/> -+ <conditionalClick selector="{{ProductInWebsitesSection.sectionHeader}}" dependentSelector="{{ProductInWebsitesSection.website(website)}}" visible="false" stepKey="clickToOpenProductInWebsite"/> -+ <waitForPageLoad stepKey="waitForPageOpened"/> -+ <click selector="{{ProductInWebsitesSection.website(website)}}" stepKey="selectWebsite"/> -+ <click selector="{{AdminProductFormAdvancedPricingSection.save}}" stepKey="clickSaveProduct"/> -+ <waitForPageLoad time='60' stepKey="waitForProducrSaved"/> -+ <waitForElementVisible selector="{{AdminMessagesSection.success}}" stepKey="waitForSaveSuccessMessage"/> -+ <see selector="{{AdminMessagesSection.success}}" userInput="You saved the product." stepKey="seeSaveSuccessMessage"/> -+ </actionGroup> -+ -+ <actionGroup name="ProductSetAdvancedPricing"> -+ <arguments> -+ <argument name="website" type="string" defaultValue=""/> -+ <argument name="group" type="string" defaultValue="Retailer"/> -+ <argument name="quantity" type="string" defaultValue="1"/> -+ <argument name="price" type="string" defaultValue="Discount"/> -+ <argument name="amount" type="string" defaultValue="45"/> -+ </arguments> -+ <click selector="{{AdminProductFormSection.advancedPricingLink}}" stepKey="clickOnAdvancedPricingButton"/> -+ <waitForElement selector="{{AdminProductFormAdvancedPricingSection.customerGroupPriceAddButton}}" stepKey="waitForCustomerGroupPriceAddButton"/> -+ <click selector="{{AdminProductFormAdvancedPricingSection.customerGroupPriceAddButton}}" stepKey="addCustomerGroupAllGroupsQty1PriceDiscountAnd10percent"/> -+ <waitForElement selector="{{AdminProductFormAdvancedPricingSection.productTierPriceCustGroupSelect('0')}}" stepKey="waitForSelectCustomerGroupNameAttribute2"/> -+ <selectOption selector="{{AdminProductFormAdvancedPricingSection.productTierPriceWebsiteSelect('0')}}" userInput="{{website}}" stepKey="selectProductWebsiteValue"/> -+ <selectOption selector="{{AdminProductFormAdvancedPricingSection.productTierPriceCustGroupSelect('0')}}" userInput="{{group}}" stepKey="selectProductCustomGroupValue"/> -+ <fillField selector="{{AdminProductFormAdvancedPricingSection.productTierPriceQtyInput('0')}}" userInput="{{quantity}}" stepKey="fillProductTierPriceQtyInput"/> -+ <selectOption selector="{{AdminProductFormAdvancedPricingSection.productTierPriceValueTypeSelect('0')}}" userInput="{{price}}" stepKey="selectProductTierPriceValueType"/> -+ <fillField selector="{{AdminProductFormAdvancedPricingSection.productTierPricePercentageValuePriceInput('0')}}" userInput="{{amount}}" stepKey="selectProductTierPricePriceInput"/> -+ <click selector="{{AdminProductFormAdvancedPricingSection.doneButton}}" stepKey="clickDoneButton"/> -+ <waitForPageLoad stepKey="WaitForProductSave"/> -+ <click selector="{{AdminProductFormAdvancedPricingSection.save}}" stepKey="clickSaveProduct1"/> -+ <waitForPageLoad time="60" stepKey="WaitForProductSave1"/> -+ <see userInput="You saved the product." stepKey="seeSaveConfirmation"/> -+ </actionGroup> -+ - <!--Assert text in Related, Up-Sell or Cross-Sell section in Admin Product page--> - <actionGroup name="AssertTextInAdminProductRelatedUpSellCrossSellSection"> - <arguments> -@@ -184,6 +261,44 @@ - <see selector="{{element}}" userInput="{{expectedText}}" stepKey="assertText"/> - </actionGroup> - -+ <!--Related products--> -+ <actionGroup name="addRelatedProductBySku"> -+ <arguments> -+ <argument name="sku"/> -+ </arguments> -+ <!--Scroll up to avoid error--> -+ <scrollTo selector="{{AdminProductFormRelatedUpSellCrossSellSection.relatedDropdown}}" x="0" y="-100" stepKey="scrollTo"/> -+ <conditionalClick selector="{{AdminProductFormRelatedUpSellCrossSellSection.relatedDropdown}}" dependentSelector="{{AdminProductFormRelatedUpSellCrossSellSection.relatedDependent}}" visible="false" stepKey="openDropDownIfClosedRelatedUpSellCrossSell"/> -+ <click selector="{{AdminProductFormRelatedUpSellCrossSellSection.AddRelatedProductsButton}}" stepKey="clickAddRelatedProductButton"/> -+ <conditionalClick selector="{{AdminProductGridFilterSection.clearFilters}}" dependentSelector="{{AdminProductGridFilterSection.clearFilters}}" visible="true" stepKey="clickClearFilters"/> -+ <click selector="{{AdminProductGridFilterSection.filters}}" stepKey="openProductFilters"/> -+ <fillField selector="{{AdminProductGridFilterSection.skuFilter}}" userInput="{{sku}}" stepKey="fillProductSkuFilter"/> -+ <click selector="{{AdminProductGridFilterSection.applyFilters}}" stepKey="clickApplyFilters"/> -+ <waitForPageLoad stepKey="waitForPageToLoad"/> -+ <click selector="{{AdminProductModalSlideGridSection.productGridXRowYColumnButton('1', '1')}}" stepKey="selectProduct"/> -+ <click selector="{{AdminAddRelatedProductsModalSection.AddSelectedProductsButton}}" stepKey="addRelatedProductSelected"/> -+ <waitForElementNotVisible selector="{{AdminAddRelatedProductsModalSection.AddSelectedProductsButton}}" stepKey="waitForElementNotVisible"/> -+ </actionGroup> -+ -+ <!--Click AddCrossSellProducts and adds product by SKU--> -+ <actionGroup name="addCrossSellProductBySku"> -+ <arguments> -+ <argument name="sku"/> -+ </arguments> -+ <!--Scroll up to avoid error--> -+ <scrollTo selector="{{AdminProductFormRelatedUpSellCrossSellSection.relatedDropdown}}" x="0" y="-100" stepKey="scrollTo"/> -+ <conditionalClick selector="{{AdminProductFormRelatedUpSellCrossSellSection.relatedDropdown}}" dependentSelector="{{AdminProductFormRelatedUpSellCrossSellSection.relatedDependent}}" visible="false" stepKey="openDropDownIfClosedRelatedUpSellCrossSell"/> -+ <click selector="{{AdminProductFormRelatedUpSellCrossSellSection.AddCrossSellProductsButton}}" stepKey="clickAddCrossSellButton"/> -+ <conditionalClick selector="{{AdminProductGridFilterSection.clearFilters}}" dependentSelector="{{AdminProductGridFilterSection.clearFilters}}" visible="true" stepKey="clickClearFilters"/> -+ <click selector="{{AdminProductGridFilterSection.filters}}" stepKey="openProductFilters"/> -+ <fillField selector="{{AdminProductGridFilterSection.skuFilter}}" userInput="{{sku}}" stepKey="fillProductSkuFilter"/> -+ <click selector="{{AdminProductGridFilterSection.applyFilters}}" stepKey="clickApplyFilters"/> -+ <waitForPageLoad stepKey="waitForPageToLoad"/> -+ <click selector="{{AdminProductModalSlideGridSection.productGridXRowYColumnButton('1', '1')}}" stepKey="selectProduct"/> -+ <click selector="{{AdminProductCrossSellModalSection.addSelectedProducts}}" stepKey="addRelatedProductSelected"/> -+ <waitForPageLoad stepKey="waitForModalDisappear"/> -+ </actionGroup> -+ - <!--Add special price to product in Admin product page--> - <actionGroup name="AddSpecialPriceToProductActionGroup"> - <arguments> -@@ -191,9 +306,236 @@ - </arguments> - <waitForPageLoad stepKey="waitForPageLoad"/> - <click selector="{{AdminProductFormSection.advancedPricingLink}}" stepKey="clickAdvancedPricingLink"/> -+ <waitForPageLoad stepKey="waitForAdvancedPricingModal"/> - <waitForElementVisible selector="{{AdminProductFormAdvancedPricingSection.specialPrice}}" stepKey="waitSpecialPrice"/> - <fillField userInput="{{price}}" selector="{{AdminProductFormAdvancedPricingSection.specialPrice}}" stepKey="fillSpecialPrice"/> - <click selector="{{AdminProductFormAdvancedPricingSection.doneButton}}" stepKey="clickDone"/> -+ <waitForPageLoad stepKey="waitForAdvancedPricingModalGone"/> - <waitForElementNotVisible selector="{{AdminProductFormAdvancedPricingSection.specialPrice}}" stepKey="waitForCloseModalWindow"/> - </actionGroup> -+ -+ <!--Select Product In Websites--> -+ <actionGroup name="SelectProductInWebsitesActionGroup"> -+ <arguments> -+ <argument name="website" type="string"/> -+ </arguments> -+ <scrollTo selector="{{ProductInWebsitesSection.sectionHeader}}" stepKey="scrollToWebsites"/> -+ <conditionalClick selector="{{ProductInWebsitesSection.sectionHeader}}" dependentSelector="{{AdminProductContentSection.sectionHeaderShow}}" visible="false" stepKey="expandSection"/> -+ <waitForPageLoad stepKey="waitForPageOpened"/> -+ <checkOption selector="{{ProductInWebsitesSection.website(website)}}" stepKey="selectWebsite"/> -+ </actionGroup> -+ -+ <actionGroup name="AdminProductAddSpecialPrice"> -+ <arguments> -+ <argument name="specialPrice" type="string"/> -+ </arguments> -+ <click selector="{{AdminProductFormSection.advancedPricingLink}}" stepKey="clickAdvancedPricingLink1"/> -+ <waitForElementVisible selector="{{AdminProductFormAdvancedPricingSection.specialPrice}}" stepKey="waitSpecialPrice1"/> -+ <click selector="{{AdminProductFormAdvancedPricingSection.useDefaultPrice}}" stepKey="checkUseDefault"/> -+ <fillField userInput="{{specialPrice}}" selector="{{AdminProductFormAdvancedPricingSection.specialPrice}}" stepKey="fillSpecialPrice"/> -+ <click selector="{{AdminProductFormAdvancedPricingSection.doneButton}}" stepKey="clickDone"/> -+ <waitForElementNotVisible selector="{{AdminProductFormAdvancedPricingSection.specialPrice}}" stepKey="waitForCloseModalWindow"/> -+ <click selector="{{AdminProductFormActionSection.saveButton}}" stepKey="clickOnSaveButton"/> -+ <seeElement selector="{{AdminProductMessagesSection.successMessage}}" stepKey="seeSaveProductMessage"/> -+ </actionGroup> -+ -+ <!--Switch to New Store view--> -+ <actionGroup name="SwitchToTheNewStoreView"> -+ <arguments> -+ <argument name="storeViewName" type="string"/> -+ </arguments> -+ <scrollTo selector="{{AdminProductContentSection.pageHeader}}" stepKey="scrollToUp"/> -+ <waitForElementVisible selector="{{AdminProductFormActionSection.changeStoreButton}}" stepKey="waitForElementBecomeVisible"/> -+ <click selector="{{AdminProductFormActionSection.changeStoreButton}}" stepKey="clickStoreviewSwitcher"/> -+ <click selector="{{AdminProductFormActionSection.selectStoreView(storeViewName)}}" stepKey="chooseStoreView"/> -+ <click selector="{{AdminConfirmationModalSection.ok}}" stepKey="acceptStoreSwitchingMessage"/> -+ <waitForPageLoad stepKey="waitForPageLoad"/> -+ </actionGroup> -+ -+ <!--Create a Simple Product--> -+ <actionGroup name="createSimpleProductAndAddToWebsite"> -+ <arguments> -+ <argument name="product"/> -+ <argument name="website" type="string"/> -+ </arguments> -+ <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="navigateToCatalogProductGrid"/> -+ <waitForPageLoad stepKey="waitForProductGrid"/> -+ <click selector="{{AdminProductGridActionSection.addProductToggle}}" stepKey="clickAddProductDropdown"/> -+ <click selector="{{AdminProductGridActionSection.addSimpleProduct}}" stepKey="clickAddSimpleProduct"/> -+ <fillField userInput="{{product.name}}" selector="{{AdminProductFormSection.productName}}" stepKey="fillProductName"/> -+ <fillField userInput="{{product.sku}}" selector="{{AdminProductFormSection.productSku}}" stepKey="fillProductSKU"/> -+ <fillField userInput="{{product.price}}" selector="{{AdminProductFormSection.productPrice}}" stepKey="fillProductPrice"/> -+ <fillField userInput="{{product.quantity}}" selector="{{AdminProductFormSection.productQuantity}}" stepKey="fillProductQuantity"/> -+ <click selector="{{ProductInWebsitesSection.sectionHeader}}" stepKey="openProductInWebsites"/> -+ <click selector="{{ProductInWebsitesSection.website(website)}}" stepKey="selectWebsite"/> -+ <click selector="{{AdminProductFormActionSection.saveButton}}" stepKey="clickSave"/> -+ <waitForLoadingMaskToDisappear stepKey="waitForProductPageSave"/> -+ <seeElement selector="{{AdminProductMessagesSection.successMessage}}" stepKey="seeSaveProductMessage"/> -+ </actionGroup> -+ -+ <actionGroup name="CreatedProductConnectToWebsite"> -+ <arguments> -+ <argument name="website"/> -+ <argument name="product"/> -+ </arguments> -+ <click stepKey="openProduct" selector="{{AdminProductGridActionSection.productName(product.sku)}}"/> -+ <waitForPageLoad stepKey="waitForProductPage"/> -+ <scrollTo selector="{{ProductInWebsitesSection.sectionHeader}}" stepKey="ScrollToWebsites"/> -+ <click selector="{{ProductInWebsitesSection.sectionHeader}}" stepKey="openWebsitesList"/> -+ <waitForPageLoad stepKey="waitForWebsitesList"/> -+ <click selector="{{ProductInWebsitesSection.website(website.name)}}" stepKey="SelectWebsite"/> -+ <click selector="{{AdminProductFormAdvancedPricingSection.save}}" stepKey="clickSaveProduct"/> -+ <waitForPageLoad stepKey="waitForSave"/> -+ </actionGroup> -+ -+ <!--Check tier price with a discount percentage on product--> -+ <actionGroup name="AssertDiscountsPercentageOfProducts"> -+ <arguments> -+ <argument name="amount" type="string" defaultValue="45"/> -+ </arguments> -+ <click selector="{{AdminProductFormSection.advancedPricingLink}}" stepKey="clickOnAdvancedPricingButton"/> -+ <waitForElement selector="{{AdminProductFormAdvancedPricingSection.customerGroupPriceAddButton}}" stepKey="waitForCustomerGroupPriceAddButton"/> -+ <grabValueFrom selector="{{AdminProductFormAdvancedPricingSection.productTierPricePercentageValuePriceInput('0')}}" stepKey="grabProductTierPriceInput"/> -+ <assertEquals stepKey="assertProductTierPriceInput"> -+ <expectedResult type="string">{{amount}}</expectedResult> -+ <actualResult type="string">$grabProductTierPriceInput</actualResult> -+ </assertEquals> -+ </actionGroup> -+ -+ <!-- This action group goes to the product index page, opens the drop down and clicks the specified product type for adding a product --> -+ <actionGroup name="GoToSpecifiedCreateProductPage"> -+ <arguments> -+ <argument type="string" name="productType" defaultValue="simple"/> -+ </arguments> -+ <comment userInput="actionGroup:GoToSpecifiedCreateProductPage" stepKey="actionGroupComment"/> -+ <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="navigateToProductIndex"/> -+ <click selector="{{AdminProductGridActionSection.addProductToggle}}" stepKey="clickAddProductDropdown"/> -+ <click selector="{{AdminProductGridActionSection.addTypeProduct(productType)}}" stepKey="clickAddProduct"/> -+ <waitForPageLoad stepKey="waitForFormToLoad"/> -+ </actionGroup> -+ -+ <!-- This action group simply navigates to the product catalog page --> -+ <actionGroup name="GoToProductCatalogPage"> -+ <comment userInput="actionGroup:GoToProductCatalogPage" stepKey="actionGroupComment"/> -+ <amOnPage url="{{AdminCatalogProductPage.url}}" stepKey="GoToCatalogProductPage"/> -+ <waitForPageLoad stepKey="WaitForPageToLoad"/> -+ </actionGroup> -+ -+ <actionGroup name="SetProductUrlKey"> -+ <arguments> -+ <argument name="product" defaultValue="_defaultProduct"/> -+ </arguments> -+ <click selector="{{AdminProductSEOSection.sectionHeader}}" stepKey="openSeoSection"/> -+ <fillField userInput="{{product.urlKey}}" selector="{{AdminProductSEOSection.urlKeyInput}}" stepKey="fillUrlKey"/> -+ </actionGroup> -+ -+ <actionGroup name="SetProductUrlKeyByString"> -+ <arguments> -+ <argument name="urlKey" type="string"/> -+ </arguments> -+ <click selector="{{AdminProductSEOSection.sectionHeader}}" stepKey="openSeoSection"/> -+ <fillField userInput="{{urlKey}}" selector="{{AdminProductSEOSection.urlKeyInput}}" stepKey="fillUrlKey"/> -+ </actionGroup> -+ -+ <actionGroup name="SetCategoryByName"> -+ <arguments> -+ <argument name="categoryName" type="string"/> -+ </arguments> -+ <searchAndMultiSelectOption selector="{{AdminProductFormSection.categoriesDropdown}}" parameterArray="[{{categoryName}}]" stepKey="searchAndSelectCategory"/> -+ </actionGroup> -+ <!--Remove category from product in ProductFrom Page--> -+ <actionGroup name="removeCategoryFromProduct"> -+ <arguments> -+ <argument name="categoryName" type="string" defaultValue="{{_defaultCategory.name}}"/> -+ </arguments> -+ <click selector="{{AdminProductFormSection.categoriesDropdown}}" stepKey="clickCategoriesDropDown"/> -+ <click selector="{{AdminProductFormSection.unselectCategories(categoryName)}}" stepKey="unselectCategories"/> -+ <click selector="{{AdminProductFormSection.done}}" stepKey="clickOnDoneAdvancedCategory"/> -+ </actionGroup> -+ -+ <actionGroup name="expandAdminProductSection"> -+ <arguments> -+ <argument name="sectionSelector" defaultValue="{{AdminProductContentSection.sectionHeader}}" type="string"/> -+ <argument name="sectionDependentSelector" defaultValue="{{AdminProductContentSection.sectionHeaderShow}}" type="string"/> -+ </arguments> -+ <scrollToTopOfPage stepKey="scrollToTopOfPage"/> -+ <waitForElementVisible time="30" selector="{{sectionSelector}}" stepKey="waitForSection"/> -+ <conditionalClick selector="{{sectionSelector}}" dependentSelector="{{sectionDependentSelector}}" visible="false" stepKey="expandSection"/> -+ <waitForPageLoad time="30" stepKey="waitForSectionToExpand"/> -+ </actionGroup> -+ <actionGroup name="navigateToCreatedProductEditPage"> -+ <arguments> -+ <argument name="product" defaultValue="_defaultProduct"/> -+ </arguments> -+ <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="goToAdminProductIndexPage"/> -+ <waitForPageLoad stepKey="waitForProductIndexPageToLoad"/> -+ <conditionalClick selector="{{AdminProductGridFilterSection.clearFilters}}" dependentSelector="{{AdminProductGridFilterSection.clearFilters}}" visible="true" stepKey="clickClearFilters"/> -+ <waitForPageLoad stepKey="waitForClearFilters"/> -+ <dontSeeElement selector="{{AdminProductGridFilterSection.clearFilters}}" stepKey="dontSeeClearFilters"/> -+ <click selector="{{AdminProductGridFilterSection.viewDropdown}}" stepKey="openViewBookmarksTab"/> -+ <click selector="{{AdminProductGridFilterSection.viewBookmark('Default View')}}" stepKey="resetToDefaultGridView"/> -+ <waitForPageLoad stepKey="waitForResetToDefaultView"/> -+ <see selector="{{AdminProductGridFilterSection.viewDropdown}}" userInput="Default View" stepKey="seeDefaultViewSelected"/> -+ <click selector="{{AdminProductGridFilterSection.filters}}" stepKey="openProductFilters"/> -+ <fillField selector="{{AdminProductGridFilterSection.skuFilter}}" userInput="{{product.sku}}" stepKey="fillProductSkuFilter"/> -+ <click selector="{{AdminProductGridFilterSection.applyFilters}}" stepKey="clickApplyFilters"/> -+ <waitForPageLoad stepKey="waitForFilterOnGrid"/> -+ <click selector="{{AdminProductGridSection.selectRowBasedOnName(product.name)}}" stepKey="clickProduct"/> -+ <waitForPageLoad stepKey="waitForProductEditPageLoad"/> -+ <waitForElementVisible selector="{{AdminProductFormBundleSection.productSku}}" stepKey="waitForProductSKUField"/> -+ <seeInField selector="{{AdminProductFormBundleSection.productSku}}" userInput="{{product.sku}}" stepKey="seeProductSKU"/> -+ </actionGroup> -+ <actionGroup name="addUpSellProductBySku" extends="addRelatedProductBySku"> -+ <click selector="{{AdminProductFormRelatedUpSellCrossSellSection.AddUpSellProductsButton}}" stepKey="clickAddRelatedProductButton"/> -+ <conditionalClick selector="{{AdminAddUpSellProductsModalSection.Modal}} {{AdminProductGridFilterSection.clearFilters}}" dependentSelector="{{AdminAddUpSellProductsModalSection.Modal}} {{AdminProductGridFilterSection.clearFilters}}" visible="true" stepKey="clickClearFilters"/> -+ <click selector="{{AdminAddUpSellProductsModalSection.Modal}} {{AdminProductGridFilterSection.filters}}" stepKey="openProductFilters"/> -+ <fillField selector="{{AdminAddUpSellProductsModalSection.Modal}} {{AdminProductGridFilterSection.skuFilter}}" userInput="{{sku}}" stepKey="fillProductSkuFilter"/> -+ <click selector="{{AdminAddUpSellProductsModalSection.Modal}} {{AdminProductGridFilterSection.applyFilters}}" stepKey="clickApplyFilters"/> -+ <waitForPageLoad stepKey="waitForPageToLoad"/> -+ <click selector="{{AdminAddUpSellProductsModalSection.Modal}}{{AdminProductModalSlideGridSection.productGridXRowYColumnButton('1', '1')}}" stepKey="selectProduct"/> -+ <click selector="{{AdminAddUpSellProductsModalSection.AddSelectedProductsButton}}" stepKey="addRelatedProductSelected"/> -+ <waitForPageLoad stepKey="waitForPageToLoad1"/> -+ </actionGroup> -+ <actionGroup name="adminProductAdvancedPricingNewCustomerGroupPrice"> -+ <arguments> -+ <argument name="qty" type="string" defaultValue="2"/> -+ <argument name="priceType" type="string" defaultValue="Discount"/> -+ <argument name="discount" type="string" defaultValue="75"/> -+ <argument name="customerGroup" type="string" defaultValue="0"/> -+ </arguments> -+ <click selector="{{AdminProductFormAdvancedPricingSection.customerGroupPriceAddButton}}" stepKey="addCustomerGroup"/> -+ <fillField selector="{{AdminProductFormAdvancedPricingSection.productTierPriceQtyInput(customerGroup)}}" userInput="{{qty}}" stepKey="fillProductTierPriceQtyInput1"/> -+ <selectOption selector="{{AdminProductFormAdvancedPricingSection.productTierPriceValueTypeSelect(customerGroup)}}" userInput="{{priceType}}" stepKey="selectProductTierPriceValueType1"/> -+ <fillField selector="{{AdminProductFormAdvancedPricingSection.productTierPricePercentageValuePriceInput(customerGroup)}}" userInput="{{discount}}" stepKey="selectProductTierPricePriceInput"/> -+ <click selector="{{AdminProductFormAdvancedPricingSection.doneButton}}" stepKey="clickDoneButton1"/> -+ </actionGroup> -+ <actionGroup name="AdminSetProductDisabled"> -+ <conditionalClick selector="{{AdminProductFormSection.enableProductLabel}}" dependentSelector="{{AdminProductFormSection.productStatusValue('1')}}" visible="true" stepKey="disableProduct"/> -+ <seeElement selector="{{AdminProductFormSection.productStatusValue('2')}}" stepKey="assertThatProductSetToDisabled"/> -+ </actionGroup> -+ -+ <!-- You are on product Edit Page --> -+ <!-- Assert checkbox available for website in Product In Websites --> -+ <actionGroup name="AssertWebsiteIsAvailableInProductWebsites"> -+ <arguments> -+ <argument name="website" type="string"/> -+ </arguments> -+ <scrollTo selector="{{ProductInWebsitesSection.sectionHeader}}" stepKey="scrollToProductInWebsitesSection"/> -+ <conditionalClick selector="{{ProductInWebsitesSection.sectionHeader}}" dependentSelector="{{ProductInWebsitesSection.sectionHeaderOpened}}" visible="false" stepKey="expandProductWebsitesSection"/> -+ <seeElement selector="{{ProductInWebsitesSection.website(website)}}" stepKey="seeCheckboxForWebsite"/> -+ </actionGroup> -+ -+ <!-- You are on product Edit Page --> -+ <!-- Assert checkbox not available for website in Product In Websites --> -+ <actionGroup name="AssertWebsiteIsNotAvailableInProductWebsites" extends="AssertWebsiteIsAvailableInProductWebsites"> -+ <remove keyForRemoval="seeCheckboxForWebsite"/> -+ <dontSeeElement selector="{{ProductInWebsitesSection.website(website)}}" after="expandProductWebsitesSection" stepKey="dontSeeCheckboxForWebsite"/> -+ </actionGroup> -+ -+ <!-- You are on product Edit Page --> -+ <!-- Assert checkbox Is checked for website in Product In Websites --> -+ <actionGroup name="AssertProductIsAssignedToWebsite" extends="AssertWebsiteIsAvailableInProductWebsites"> -+ <remove keyForRemoval="seeCheckboxForWebsite"/> -+ <seeCheckboxIsChecked selector="{{ProductInWebsitesSection.website(website)}}" after="expandProductWebsitesSection" stepKey="seeCustomWebsiteIsChecked"/> -+ </actionGroup> - </actionGroups> -diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminProductAttributeActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminProductAttributeActionGroup.xml -index 3f4ee180fc6..ed0c4387cde 100644 ---- a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminProductAttributeActionGroup.xml -+++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminProductAttributeActionGroup.xml -@@ -7,14 +7,268 @@ - --> - - <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="navigateToCreatedProductAttribute"> - <arguments> - <argument name="ProductAttribute"/> - </arguments> - <amOnPage url="{{AdminProductAttributeGridPage.url}}" stepKey="navigateToProductAttributeGrid"/> -+ <fillField selector="{{AdminProductAttributeGridSection.FilterByAttributeCode}}" -+ userInput="{{ProductAttribute.attribute_code}}" stepKey="setAttributeCode"/> -+ <click selector="{{AdminProductAttributeGridSection.Search}}" stepKey="searchForAttributeFromTheGrid"/> -+ <click selector="{{AdminProductAttributeGridSection.FirstRow}}" stepKey="clickOnAttributeRow"/> -+ <waitForPageLoad stepKey="waitForPageLoad2" /> -+ </actionGroup> -+ <actionGroup name="navigateToEditProductAttribute"> -+ <arguments> -+ <argument name="ProductAttribute" type="string"/> -+ </arguments> -+ <amOnPage url="{{AdminProductAttributeGridPage.url}}" stepKey="navigateToProductAttributeGrid"/> -+ <fillField selector="{{AdminProductAttributeGridSection.GridFilterFrontEndLabel}}" userInput="{{ProductAttribute}}" stepKey="navigateToAttributeEditPage1" /> -+ <click selector="{{AdminProductAttributeGridSection.Search}}" stepKey="navigateToAttributeEditPage2" /> -+ <waitForPageLoad stepKey="waitForPageLoad2" /> -+ <click selector="{{AdminProductAttributeGridSection.FirstRow}}" stepKey="navigateToAttributeEditPage3" /> -+ <waitForPageLoad stepKey="waitForPageLoad3" /> -+ </actionGroup> -+ -+ <actionGroup name="AdminCreateAttributeFromProductPage"> -+ <arguments> -+ <argument name="attributeName" type="string"/> -+ <argument name="attributeType" type="string" defaultValue="TextField"/> -+ </arguments> -+ <click selector="{{AdminProductFormSection.addAttributeBtn}}" stepKey="clickAddAttributeBtn"/> -+ <see userInput="Select Attribute" stepKey="checkNewAttributePopUpAppeared"/> -+ <click selector="{{AdminProductFormAttributeSection.createNewAttribute}}" stepKey="clickCreateNewAttribute"/> -+ <fillField selector="{{AdminProductFormNewAttributeSection.attributeLabel}}" userInput="{{attributeName}}" stepKey="fillAttributeLabel"/> -+ <selectOption selector="{{AdminProductFormNewAttributeSection.attributeType}}" userInput="{{attributeType}}" stepKey="selectAttributeType"/> -+ <click selector="{{AdminProductFormNewAttributeSection.saveAttribute}}" stepKey="saveAttribute"/> -+ </actionGroup> -+ <actionGroup name="AdminCreateAttributeFromProductPageWithScope" extends="AdminCreateAttributeFromProductPage" insertAfter="selectAttributeType"> -+ <arguments> -+ <argument name="scope" type="string" defaultValue="Store View"/> -+ </arguments> -+ <conditionalClick selector="{{AdminProductFormNewAttributeAdvancedSection.sectionHeader}}" dependentSelector="{{AdminProductFormNewAttributeAdvancedSection.scope}}" visible="false" stepKey="openAttributeAdvancedSection"/> -+ <selectOption selector="{{AdminProductFormNewAttributeAdvancedSection.scope}}" userInput="{{scope}}" stepKey="selectScope"/> -+ </actionGroup> -+ -+ <actionGroup name="AdminCreateAttributeWithValueWithTwoStoreViesFromProductPage" extends="AdminCreateAttributeFromProductPage"> -+ <remove keyForRemoval="saveAttribute"/> -+ <arguments> -+ <argument name="firstStoreViewName" type="string"/> -+ <argument name="secondStoreViewName" type="string"/> -+ </arguments> -+ <click selector="{{AdminProductFormNewAttributeSection.addValue}}" stepKey="addValue" after="selectAttributeType"/> -+ <seeElement selector="{{AdminProductFormNewAttributeSection.optionViewName(firstStoreViewName))}}" stepKey="seeFirstStoreView"/> -+ <seeElement selector="{{AdminProductFormNewAttributeSection.optionViewName(firstStoreViewName))}}" stepKey="seeSecondStoreView"/> -+ <fillField selector="{{AdminProductFormNewAttributeSection.optionValue('1'))}}" userInput="default" stepKey="fillDefaultStoreView"/> -+ <fillField selector="{{AdminProductFormNewAttributeSection.optionValue('2'))}}" userInput="admin" stepKey="fillAdminStoreView"/> -+ <fillField selector="{{AdminProductFormNewAttributeSection.optionValue('3'))}}" userInput="view1" stepKey="fillFirstStoreView"/> -+ <fillField selector="{{AdminProductFormNewAttributeSection.optionValue('4'))}}" userInput="view2" stepKey="fillSecondStoreView"/> -+ -+ <!--Check store view in Manage Titles section--> -+ <click selector="{{AdminProductFormNewAttributeSection.manageTitlesHeader}}" stepKey="openManageTitlesSection"/> -+ <seeElement selector="{{AdminProductFormNewAttributeSection.manageTitlesViewName(customStoreEN.name)}}" stepKey="seeFirstStoreViewName"/> -+ <seeElement selector="{{AdminProductFormNewAttributeSection.manageTitlesViewName(customStoreFR.name)}}" stepKey="seeSecondStoreViewName"/> -+ <click selector="{{AdminProductFormNewAttributeSection.saveAttribute}}" stepKey="saveAttribute1"/> -+ </actionGroup> -+ -+ <!-- Creates attribute and attribute set from the product page--> -+ <actionGroup name="AdminProductPageCreateAttributeSetWithAttribute"> -+ <arguments> -+ <argument name="attributeName" type="string"/> -+ <argument name="attributeSetName" type="string"/> -+ <argument name="attributeType" type="string" defaultValue="TextField"/> -+ </arguments> -+ <click selector="{{AdminProductFormSection.addAttributeBtn}}" stepKey="clickAddAttributeBtn"/> -+ <waitForPageLoad stepKey="waitForSidePanel"/> -+ <see userInput="Select Attribute" stepKey="checkNewAttributePopUpAppeared"/> -+ <click selector="{{AdminProductFormAttributeSection.createNewAttribute}}" stepKey="clickCreateNewAttribute"/> -+ <fillField selector="{{AdminProductFormNewAttributeSection.attributeLabel}}" userInput="{{attributeName}}" stepKey="fillAttributeLabel"/> -+ <selectOption selector="{{AdminProductFormNewAttributeSection.attributeType}}" userInput="{{attributeType}}" stepKey="selectAttributeType"/> -+ <click selector="{{AdminProductFormNewAttributeSection.saveInNewSet}}" stepKey="saveAttribute"/> -+ <fillField selector="{{AdminProductFormNewAttributeNewSetSection.setName}}" userInput="{{attributeSetName}}" stepKey="fillSetName"/> -+ <click selector="{{AdminProductFormNewAttributeNewSetSection.accept}}" stepKey="acceptPopup"/> -+ <waitForLoadingMaskToDisappear stepKey="waitForLoadingToFinish"/> -+ <!-- Product page will hang if there is no reload--> -+ <reloadPage stepKey="reloadPage"/> -+ <waitForPageLoad stepKey="waitForReload"/> -+ </actionGroup> -+ -+ <!-- Create attribute and set with given search weight and defaultValue from the Edit Product Page --> -+ <actionGroup name="AdminCreateAttributeWithSearchWeight" extends="AdminProductPageCreateAttributeSetWithAttribute" insertAfter="selectAttributeType"> -+ <arguments> -+ <argument name="weight" type="string" defaultValue="1"/> -+ <argument name="defaultValue" type="string" defaultValue="default"/> -+ </arguments> -+ <click selector="{{AdminProductFormNewAttributeAdvancedSection.sectionHeader}}" stepKey="openAdvancedSection"/> -+ <fillField selector="{{AdminProductFormNewAttributeAdvancedSection.defaultValue}}" userInput="{{defaultValue}}" stepKey="inputDefault"/> -+ <click selector="{{AdminProductFormNewAttributeStorefrontSection.sectionHeader}}" stepKey="openStorefrontSection"/> -+ <checkOption selector="{{AdminProductFormNewAttributeStorefrontSection.useInSearch}}" stepKey="checkUseInSearch"/> -+ <waitForElementVisible selector="{{AdminProductFormNewAttributeStorefrontSection.searchWeight}}" stepKey="waitForSearchWeight"/> -+ <selectOption selector="{{AdminProductFormNewAttributeStorefrontSection.searchWeight}}" userInput="{{weight}}" stepKey="selectWeight"/> -+ </actionGroup> -+ -+ <actionGroup name="AdminProductPageSelectAttributeSet"> -+ <arguments> -+ <argument name="attributeSetName" type="string"/> -+ </arguments> -+ <click stepKey="openDropdown" selector="{{AdminProductFormSection.attributeSet}}"/> -+ <fillField stepKey="filter" selector="{{AdminProductFormSection.attributeSetFilter}}" userInput="{{attributeSetName}}"/> -+ <click stepKey="clickResult" selector="{{AdminProductFormSection.attributeSetFilterResult}}"/> -+ </actionGroup> -+ -+ <actionGroup name="AdminProductPageFillTextAttributeValueByName"> -+ <arguments> -+ <argument name="attributeName" type="string"/> -+ <argument name="value" type="string"/> -+ </arguments> -+ <click stepKey="openSection" selector="{{AdminProductAttributeSection.attributeSectionHeader}}"/> -+ <fillField stepKey="fillValue" selector="{{AdminProductAttributeSection.textAttributeByName(attributeName)}}" userInput="{{value}}"/> -+ </actionGroup> -+ -+ <actionGroup name="changeUseForPromoRuleConditionsProductAttribute"> -+ <arguments> -+ <argument name="option" type="string"/> -+ </arguments> -+ <click selector="{{StorefrontPropertiesSection.StoreFrontPropertiesTab}}" stepKey="clickStoreFrontPropertiesTab"/> -+ <waitForPageLoad stepKey="waitForPageLoad"/> -+ <selectOption selector="{{StorefrontPropertiesSection.useForPromoRuleConditions}}" userInput="{{option}}" stepKey="changeOption"/> -+ <click selector="{{AttributePropertiesSection.Save}}" stepKey="saveAttribute"/> -+ <see selector="{{AdminMessagesSection.success}}" userInput="You saved the product attribute." stepKey="successMessage"/> -+ </actionGroup> -+ <actionGroup name="deleteProductAttribute" extends="navigateToCreatedProductAttribute"> -+ <click selector="{{AttributePropertiesSection.DeleteAttribute}}" stepKey="deleteAttribute"/> -+ <click selector="{{ModalConfirmationSection.OkButton}}" stepKey="ClickOnDeleteButton"/> -+ <waitForPageLoad stepKey="waitForPageLoad"/> -+ <seeElement selector="{{AdminProductMessagesSection.successMessage}}" -+ stepKey="waitForSuccessMessage"/> -+ </actionGroup> -+ <actionGroup name="deleteProductAttributeByLabel"> -+ <arguments> -+ <argument name="ProductAttribute"/> -+ </arguments> -+ <amOnPage url="{{AdminProductAttributeGridPage.url}}" stepKey="navigateToProductAttributeGrid"/> -+ <fillField selector="{{AdminProductAttributeGridSection.FilterByAttributeCode}}" userInput="{{ProductAttribute.default_label}}" stepKey="setAttributeCode"/> -+ <click selector="{{AdminProductAttributeGridSection.Search}}" stepKey="searchForAttributeFromTheGrid"/> -+ <click selector="{{AdminProductAttributeGridSection.FirstRow}}" stepKey="clickOnAttributeRow"/> -+ <waitForPageLoad stepKey="waitForPageLoad"/> -+ <click selector="{{AttributePropertiesSection.DeleteAttribute}}" stepKey="deleteAttribute"/> -+ <click selector="{{ModalConfirmationSection.OkButton}}" stepKey="ClickOnDeleteButton"/> - <waitForPageLoad stepKey="waitForPageLoad1"/> -- <click selector="{{AdminProductAttributeGridSection.AttributeCode(ProductAttribute.attribute_code)}}" stepKey="navigateToAttributeEditPage1" /> -+ <seeElement selector="{{AdminProductMessagesSection.successMessage}}" stepKey="waitForSuccessMessage"/> -+ </actionGroup> -+ <!-- Delete product attribute by Attribute Code --> -+ <actionGroup name="deleteProductAttributeByAttributeCode"> -+ <arguments> -+ <argument name="ProductAttributeCode" type="string"/> -+ </arguments> -+ <amOnPage url="{{AdminProductAttributeGridPage.url}}" stepKey="navigateToProductAttributeGrid"/> -+ <fillField selector="{{AdminProductAttributeGridSection.FilterByAttributeCode}}" userInput="{{ProductAttributeCode}}" stepKey="setAttributeCode"/> -+ <click selector="{{AdminProductAttributeGridSection.Search}}" stepKey="searchForAttributeFromTheGrid"/> -+ <click selector="{{AdminProductAttributeGridSection.FirstRow}}" stepKey="clickOnAttributeRow"/> - <waitForPageLoad stepKey="waitForPageLoad2" /> -+ <click selector="{{AttributePropertiesSection.DeleteAttribute}}" stepKey="deleteAttribute"/> -+ <click selector="{{ModalConfirmationSection.OkButton}}" stepKey="ClickOnDeleteButton"/> -+ <waitForPageLoad stepKey="waitForPageLoad"/> -+ <seeElement selector="{{AdminProductMessagesSection.successMessage}}" stepKey="waitForSuccessMessage"/> -+ </actionGroup> -+ <!--Filter product attribute by Attribute Code --> -+ <actionGroup name="filterProductAttributeByAttributeCode"> -+ <arguments> -+ <argument name="ProductAttributeCode" type="string"/> -+ </arguments> -+ <click selector="{{AdminProductAttributeGridSection.ResetFilter}}" stepKey="resetFiltersOnGrid"/> -+ <fillField selector="{{AdminProductAttributeGridSection.FilterByAttributeCode}}" userInput="{{ProductAttributeCode}}" stepKey="setAttributeCode"/> -+ <waitForPageLoad stepKey="waitForUserInput"/> -+ <click selector="{{AdminProductAttributeGridSection.Search}}" stepKey="searchForAttributeFromTheGrid"/> -+ </actionGroup> -+ <!--Filter product attribute by Default Label --> -+ <actionGroup name="filterProductAttributeByDefaultLabel"> -+ <arguments> -+ <argument name="productAttributeLabel" type="string"/> -+ </arguments> -+ <click selector="{{AdminProductAttributeGridSection.ResetFilter}}" stepKey="resetFiltersOnGrid"/> -+ <fillField selector="{{AdminProductAttributeGridSection.GridFilterFrontEndLabel}}" userInput="{{productAttributeLabel}}" stepKey="setDefaultLabel"/> -+ <waitForPageLoad stepKey="waitForUserInput"/> -+ <click selector="{{AdminProductAttributeGridSection.Search}}" stepKey="searchForAttributeFromTheGrid"/> -+ </actionGroup> -+ <actionGroup name="saveProductAttribute"> -+ <waitForElementVisible selector="{{AttributePropertiesSection.Save}}" stepKey="waitForSaveButton"/> -+ <click selector="{{AttributePropertiesSection.Save}}" stepKey="clickSaveButton"/> -+ <waitForPageLoad stepKey="waitForAttributeToSave"/> -+ <seeElement selector="{{AdminProductMessagesSection.successMessage}}" stepKey="seeSuccessMessage"/> -+ </actionGroup> -+ <actionGroup name="confirmChangeInputTypeModal"> -+ <waitForElementVisible selector="{{AdminEditProductAttributesSection.ProductDataMayBeLostConfirmButton}}" stepKey="waitForChangeInputTypeButton"/> -+ <click selector="{{AdminEditProductAttributesSection.ProductDataMayBeLostConfirmButton}}" stepKey="clickChangeInputTypeButton"/> -+ <waitForElementNotVisible selector="{{AdminEditProductAttributesSection.ProductDataMayBeLostModal}}" stepKey="waitForChangeInputTypeModalGone"/> -+ </actionGroup> -+ <actionGroup name="saveProductAttributeInUse"> -+ <waitForElementVisible selector="{{AttributePropertiesSection.Save}}" stepKey="waitForSaveButton"/> -+ <click selector="{{AttributePropertiesSection.Save}}" stepKey="clickSaveButton"/> -+ <waitForPageLoad stepKey="waitForAttributeToSave"/> -+ <seeElement selector="{{AdminProductMessagesSection.successMessage}}" stepKey="seeSuccessMessage"/> -+ </actionGroup> -+ -+ <!--Clicks Add Attribute and adds the given attribute--> -+ <actionGroup name="addProductAttributeInProductModal"> -+ <arguments> -+ <argument name="attributeCode" type="string"/> -+ </arguments> -+ <click stepKey="addAttribute" selector="{{AdminProductFormActionSection.addAttributeButton}}"/> -+ <conditionalClick selector="{{AdminProductAddAttributeModalSection.clearFilters}}" dependentSelector="{{AdminProductAddAttributeModalSection.clearFilters}}" visible="true" stepKey="clearFilters"/> -+ <click stepKey="clickFilters" selector="{{AdminProductAddAttributeModalSection.filters}}"/> -+ <fillField stepKey="fillCode" selector="{{AdminProductAddAttributeModalSection.attributeCodeFilter}}" userInput="{{attributeCode}}"/> -+ <click stepKey="clickApply" selector="{{AdminProductAddAttributeModalSection.applyFilters}}"/> -+ <waitForPageLoad stepKey="waitForFilters"/> -+ <checkOption selector="{{AdminProductAddAttributeModalSection.firstRowCheckBox}}" stepKey="checkAttribute"/> -+ <click stepKey="addSelected" selector="{{AdminProductAddAttributeModalSection.addSelected}}"/> -+ </actionGroup> -+ -+ <!--Clicks createNewAttribute and fills out form--> -+ <actionGroup name="createProductAttribute"> -+ <arguments> -+ <argument name="attribute" type="entity" defaultValue="productAttributeWysiwyg"/> -+ </arguments> -+ <click stepKey="createNewAttribute" selector="{{AdminProductAttributeGridSection.createNewAttributeBtn}}"/> -+ <fillField stepKey="fillDefaultLabel" selector="{{AttributePropertiesSection.DefaultLabel}}" userInput="{{attribute.attribute_code}}"/> -+ <selectOption selector="{{AttributePropertiesSection.InputType}}" stepKey="checkInputType" userInput="{{attribute.frontend_input}}"/> -+ <selectOption selector="{{AttributePropertiesSection.ValueRequired}}" stepKey="checkRequired" userInput="{{attribute.is_required_admin}}"/> -+ <click stepKey="saveAttribute" selector="{{AttributePropertiesSection.Save}}"/> -+ </actionGroup> -+ -+ <!-- Inputs text default value and attribute code--> -+ <actionGroup name="createProductAttributeWithTextField" extends="createProductAttribute" insertAfter="checkRequired"> -+ <click stepKey="openAdvancedProperties" selector="{{AdvancedAttributePropertiesSection.AdvancedAttributePropertiesSectionToggle}}"/> -+ <fillField stepKey="fillCode" selector="{{AdvancedAttributePropertiesSection.AttributeCode}}" userInput="{{attribute.attribute_code}}"/> -+ <fillField stepKey="fillDefaultValue" selector="{{AdvancedAttributePropertiesSection.DefaultValueText}}" userInput="{{attribute.default_value}}"/> -+ </actionGroup> -+ -+ <!-- Inputs date default value and attribute code--> -+ <actionGroup name="createProductAttributeWithDateField" extends="createProductAttribute" insertAfter="checkRequired"> -+ <arguments> -+ <argument name="date" type="string"/> -+ </arguments> -+ <click stepKey="openAdvancedProperties" selector="{{AdvancedAttributePropertiesSection.AdvancedAttributePropertiesSectionToggle}}"/> -+ <fillField stepKey="fillCode" selector="{{AdvancedAttributePropertiesSection.AttributeCode}}" userInput="{{attribute.attribute_code}}"/> -+ <fillField stepKey="fillDefaultValue" selector="{{AdvancedAttributePropertiesSection.DefaultValueDate}}" userInput="{{date}}"/> -+ </actionGroup> -+ -+ <!-- Creates dropdown option at row without saving--> -+ <actionGroup name="createAttributeDropdownNthOption"> -+ <arguments> -+ <argument name="row" type="string"/> -+ <argument name="adminName" type="string"/> -+ <argument name="frontName" type="string"/> -+ </arguments> -+ <click stepKey="clickAddOptions" selector="{{AttributePropertiesSection.dropdownAddOptions}}"/> -+ <waitForPageLoad stepKey="waitForNewOption"/> -+ <fillField stepKey="fillAdmin" selector="{{AttributePropertiesSection.dropdownNthOptionAdmin(row)}}" userInput="{{adminName}}"/> -+ <fillField stepKey="fillStoreView" selector="{{AttributePropertiesSection.dropdownNthOptionDefaultStoreView(row)}}" userInput="{{frontName}}"/> -+ </actionGroup> -+ -+ <!-- Creates dropdown option at row as default--> -+ <actionGroup name="createAttributeDropdownNthOptionAsDefault" extends="createAttributeDropdownNthOption"> -+ <checkOption selector="{{AttributePropertiesSection.dropdownNthOptionIsDefault(row)}}" stepKey="setAsDefault" after="fillStoreView"/> - </actionGroup> - </actionGroups> -diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminProductAttributeSetActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminProductAttributeSetActionGroup.xml -index 33f4ccac2b9..019d103a31c 100644 ---- a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminProductAttributeSetActionGroup.xml -+++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminProductAttributeSetActionGroup.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"> - <actionGroup name="AssignAttributeToGroup"> - <arguments> - <argument name="group" type="string"/> -@@ -34,4 +34,64 @@ - <click selector="{{AdminProductAttributeSetActionSection.save}}" stepKey="clickSave"/> - <see userInput="You saved the attribute set" selector="{{AdminMessagesSection.success}}" stepKey="successMessage"/> - </actionGroup> -+ <actionGroup name="CreateDefaultAttributeSet"> -+ <!--Generic atrribute set creation--> -+ <arguments> -+ <argument name="label" type="string"/> -+ </arguments> -+ <amOnPage url="{{AdminProductAttributeSetGridPage.url}}" stepKey="goToAttributeSets"/> -+ <waitForPageLoad stepKey="wait1"/> -+ <click selector="{{AdminProductAttributeSetGridSection.addAttributeSetBtn}}" stepKey="clickAddAttributeSet"/> -+ <fillField selector="{{AdminProductAttributeSetSection.name}}" userInput="{{label}}" stepKey="fillName"/> -+ <click selector="{{AdminProductAttributeSetSection.saveBtn}}" stepKey="clickSave1"/> -+ </actionGroup> -+ <actionGroup name="goToAttributeGridPage"> -+ <amOnPage url="{{AdminProductAttributeSetGridPage.url}}" stepKey="goToAttributeSets"/> -+ </actionGroup> -+ <actionGroup name="goToAttributeSetByName"> -+ <arguments> -+ <argument name="name" type="string"/> -+ </arguments> -+ <click selector="{{AdminProductAttributeSetGridSection.resetFilter}}" stepKey="clickResetButton"/> -+ <fillField selector="{{AdminProductAttributeSetGridSection.filter}}" userInput="{{name}}" stepKey="filterByName"/> -+ <click selector="{{AdminProductAttributeSetGridSection.searchBtn}}" stepKey="clickSearch"/> -+ <click selector="{{AdminProductAttributeSetGridSection.nthRow('1')}}" stepKey="clickFirstRow"/> -+ <waitForPageLoad stepKey="waitForPageLoad"/> -+ </actionGroup> -+ <!-- Filter By Attribute Label --> -+ <actionGroup name="filterProductAttributeByAttributeLabel"> -+ <arguments> -+ <argument name="productAttributeLabel" type="string"/> -+ </arguments> -+ <fillField selector="{{AdminProductAttributeGridSection.attributeLabelFilter}}" userInput="{{productAttributeLabel}}" stepKey="setAttributeLabel"/> -+ <waitForPageLoad stepKey="waitForUserInput"/> -+ <click selector="{{AdminProductAttributeGridSection.Search}}" stepKey="searchForAttributeFromTheGrid"/> -+ </actionGroup> -+ <actionGroup name="FilterProductAttributeSetGridByAttributeSetName"> -+ <arguments> -+ <argument name="name" type="string"/> -+ </arguments> -+ <click selector="{{AdminProductAttributeSetGridSection.resetFilter}}" stepKey="clickResetButton"/> -+ <fillField selector="{{AdminProductAttributeSetGridSection.filter}}" userInput="{{name}}" stepKey="filterByName"/> -+ <click selector="{{AdminProductAttributeSetGridSection.searchBtn}}" stepKey="clickSearch"/> -+ <click selector="{{AdminProductAttributeSetGridSection.nthRow('1')}}" stepKey="clickFirstRow"/> -+ <waitForPageLoad time="30" stepKey="waitForPageLoad1"/> -+ </actionGroup> -+ -+ <!-- Delete attribute set --> -+ <actionGroup name="deleteAttributeSetByLabel"> -+ <arguments> -+ <argument name="label" type="string"/> -+ </arguments> -+ <amOnPage url="{{AdminProductAttributeSetGridPage.url}}" stepKey="goToAttributeSets"/> -+ <waitForPageLoad stepKey="waitForAttributeSetPageLoad"/> -+ <fillField selector="{{AdminProductAttributeSetGridSection.filter}}" userInput="{{label}}" stepKey="filterByName"/> -+ <click selector="{{AdminProductAttributeSetGridSection.searchBtn}}" stepKey="clickSearch"/> -+ <click selector="{{AdminProductAttributeSetGridSection.nthRow('1')}}" stepKey="clickFirstRow"/> -+ <waitForPageLoad stepKey="waitForClick"/> -+ <click selector="{{AdminProductAttributeSetSection.deleteBtn}}" stepKey="clickDelete"/> -+ <click selector="{{AdminProductAttributeSetSection.modalOk}}" stepKey="confirmDelete"/> -+ <waitForPageLoad stepKey="waitForDeleteToFinish"/> -+ <see selector="{{AdminCategoryMessagesSection.SuccessMessage}}" userInput="The attribute set has been removed." stepKey="seeDeleteMessage"/> -+ </actionGroup> - </actionGroups> -diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminProductGridActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminProductGridActionGroup.xml -index c2620bc5a36..3e967cb9c69 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 @@ - --> - - <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"> - <!--Reset the product grid to the default view--> - <actionGroup name="resetProductGridToDefaultView"> - <conditionalClick selector="{{AdminProductGridFilterSection.clearFilters}}" dependentSelector="{{AdminProductGridFilterSection.clearFilters}}" visible="true" stepKey="clickClearFilters"/> -@@ -144,6 +144,18 @@ - <click selector="{{AdminProductGridFilterSection.clearFilters}}" stepKey="clickClearFiltersAfter"/> - </actionGroup> - -+ <!-- Filter product grid by sku, name --> -+ <actionGroup name="filterProductGridBySkuAndName"> -+ <arguments> -+ <argument name="product" defaultValue="_defaultProduct"/> -+ </arguments> -+ <conditionalClick selector="{{AdminProductGridFilterSection.clearFilters}}" dependentSelector="{{AdminProductGridFilterSection.clearFilters}}" visible="true" stepKey="clickClearFiltersInitial"/> -+ <click selector="{{AdminProductGridFilterSection.filters}}" stepKey="openProductFilters"/> -+ <fillField selector="{{AdminProductGridFilterSection.skuFilter}}" userInput="{{product.sku}}" stepKey="fillProductSkuFilter"/> -+ <fillField selector="{{AdminProductGridFilterSection.nameFilter}}" userInput="{{product.name}}" stepKey="fillProductNameFilter"/> -+ <click selector="{{AdminProductGridFilterSection.applyFilters}}" stepKey="clickApplyFilters"/> -+ </actionGroup> -+ - <!--Delete a product by filtering grid and using delete action--> - <actionGroup name="deleteProductUsingProductGrid"> - <arguments> -@@ -155,6 +167,7 @@ - <conditionalClick selector="{{AdminProductGridFilterSection.clearFilters}}" dependentSelector="{{AdminProductGridFilterSection.clearFilters}}" visible="true" stepKey="clickClearFiltersInitial"/> - <click selector="{{AdminProductGridFilterSection.filters}}" stepKey="openProductFilters"/> - <fillField selector="{{AdminProductGridFilterSection.skuFilter}}" userInput="{{product.sku}}" stepKey="fillProductSkuFilter"/> -+ <fillField selector="{{AdminProductGridFilterSection.nameFilter}}" userInput="{{product.name}}" stepKey="fillProductNameFilter"/> - <click selector="{{AdminProductGridFilterSection.applyFilters}}" stepKey="clickApplyFilters"/> - <see selector="{{AdminProductGridSection.productGridCell('1', 'SKU')}}" userInput="{{product.sku}}" stepKey="seeProductSkuInGrid"/> - <click selector="{{AdminProductGridSection.multicheckDropdown}}" stepKey="openMulticheckDropdown"/> -@@ -164,6 +177,13 @@ - <waitForElementVisible selector="{{AdminProductGridConfirmActionSection.title}}" stepKey="waitForConfirmModal"/> - <click selector="{{AdminProductGridConfirmActionSection.ok}}" stepKey="confirmProductDelete"/> - </actionGroup> -+ <!--Delete all products by filtering grid and using mass delete action--> -+ <actionGroup name="deleteAllDuplicateProductUsingProductGrid" extends="deleteProductUsingProductGrid"> -+ <arguments> -+ <argument name="product"/> -+ </arguments> -+ <remove keyForRemoval="seeProductSkuInGrid"/> -+ </actionGroup> - - <!--Delete a product by filtering grid and using delete action--> - <actionGroup name="deleteProductBySku"> -@@ -172,8 +192,7 @@ - </arguments> - <!--TODO use other action group for filtering grid when MQE-539 is implemented --> - <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="visitAdminProductPage"/> -- <waitForPageLoad time="60" stepKey="waitForPageLoadInitial"/> -- <conditionalClick selector="{{AdminProductGridFilterSection.clearFilters}}" dependentSelector="{{AdminProductGridFilterSection.clearFilters}}" visible="true" stepKey="clickClearFiltersInitial"/> -+ <conditionalClick selector="{{AdminDataGridHeaderSection.clearFilters}}" dependentSelector="{{AdminDataGridHeaderSection.clearFilters}}" visible="true" stepKey="clickClearFilters"/> - <click selector="{{AdminProductGridFilterSection.filters}}" stepKey="openProductFilters"/> - <fillField selector="{{AdminProductGridFilterSection.skuFilter}}" userInput="{{sku}}" stepKey="fillProductSkuFilter"/> - <click selector="{{AdminProductGridFilterSection.applyFilters}}" stepKey="clickApplyFilters"/> -@@ -182,8 +201,20 @@ - <click selector="{{AdminProductGridSection.multicheckOption('Select All')}}" stepKey="selectAllProductInFilteredGrid"/> - <click selector="{{AdminProductGridSection.bulkActionDropdown}}" stepKey="clickActionDropdown"/> - <click selector="{{AdminProductGridSection.bulkActionOption('Delete')}}" stepKey="clickDeleteAction"/> -- <waitForElementVisible selector="{{AdminProductGridConfirmActionSection.title}}" stepKey="waitForConfirmModal"/> -- <click selector="{{AdminProductGridConfirmActionSection.ok}}" stepKey="confirmProductDelete"/> -+ <waitForElementVisible selector="{{AdminConfirmationModalSection.ok}}" stepKey="waitForConfirmModal"/> -+ <click selector="{{AdminConfirmationModalSection.ok}}" stepKey="confirmProductDelete"/> -+ <see selector="{{AdminMessagesSection.success}}" userInput="record(s) have been deleted." stepKey="seeSuccessMessage"/> -+ </actionGroup> -+ -+ <actionGroup name="deleteProductByName" extends="deleteProductBySku"> -+ <arguments> -+ <argument name="sku" type="string" defaultValue=""/> -+ <argument name="name" type="string"/> -+ </arguments> -+ <remove keyForRemoval="fillProductSkuFilter"/> -+ <fillField selector="{{AdminProductGridFilterSection.nameFilter}}" userInput="{{name}}" stepKey="fillProductSkuFilter" after="openProductFilters"/> -+ <remove keyForRemoval="seeProductSkuInGrid"/> -+ <see selector="{{AdminProductGridSection.productGridCell('1', 'Name')}}" userInput="{{name}}" stepKey="seeProductNameInGrid" after="clickApplyFilters"/> - </actionGroup> - - <!--Open product for edit by clicking row X and column Y in product grid--> -@@ -206,4 +237,65 @@ - <conditionalClick selector="{{AdminProductGridTableHeaderSection.id('descend')}}" dependentSelector="{{AdminProductGridTableHeaderSection.id('ascend')}}" visible="false" stepKey="sortById"/> - <waitForPageLoad stepKey="waitForPageLoad"/> - </actionGroup> -+ -+ <!--Disabled a product by filtering grid and using change status action--> -+ <actionGroup name="ChangeStatusProductUsingProductGridActionGroup"> -+ <arguments> -+ <argument name="product"/> -+ <argument name="status" defaultValue="Enable" type="string" /> -+ </arguments> -+ <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="visitAdminProductPage"/> -+ <waitForPageLoad time="60" stepKey="waitForPageLoadInitial"/> -+ <conditionalClick selector="{{AdminProductGridFilterSection.clearFilters}}" dependentSelector="{{AdminProductGridFilterSection.clearFilters}}" visible="true" stepKey="clickClearFiltersInitial"/> -+ <click selector="{{AdminProductGridFilterSection.filters}}" stepKey="openProductFilters"/> -+ <fillField selector="{{AdminProductGridFilterSection.skuFilter}}" userInput="{{product.sku}}" stepKey="fillProductSkuFilter"/> -+ <click selector="{{AdminProductGridFilterSection.applyFilters}}" stepKey="clickApplyFilters"/> -+ <see selector="{{AdminProductGridSection.productGridCell('1', 'SKU')}}" userInput="{{product.sku}}" stepKey="seeProductSkuInGrid"/> -+ <click selector="{{AdminProductGridSection.multicheckDropdown}}" stepKey="openMulticheckDropdown"/> -+ <click selector="{{AdminProductGridSection.multicheckOption('Select All')}}" stepKey="selectAllProductInFilteredGrid"/> -+ -+ <click selector="{{AdminProductGridSection.bulkActionDropdown}}" stepKey="clickActionDropdown"/> -+ <click selector="{{AdminProductGridSection.bulkActionOption('Change status')}}" stepKey="clickChangeStatusAction"/> -+ <click selector="{{AdminProductGridSection.changeStatus('status')}}" stepKey="clickChangeStatusDisabled"/> -+ <waitForPageLoad stepKey="waitForStatusToBeChanged"/> -+ <see selector="{{AdminMessagesSection.success}}" userInput="A total of 1 record(s) have been updated." stepKey="seeSuccessMessage"/> -+ <waitForLoadingMaskToDisappear stepKey="waitForMaskToDisappear"/> -+ <conditionalClick selector="{{AdminProductGridFilterSection.clearFilters}}" dependentSelector="{{AdminProductGridFilterSection.clearFilters}}" visible="true" stepKey="clickClearFiltersInitial2"/> -+ </actionGroup> -+ -+ <actionGroup name="NavigateToAndResetProductGridToDefaultView" extends="resetProductGridToDefaultView"> -+ <amOnPage url="{{AdminProductIndexPage.url}}" before="clickClearFilters" stepKey="goToAdminProductIndexPage"/> -+ <waitForPageLoad after="goToAdminProductIndexPage" stepKey="waitForProductIndexPageToLoad"/> -+ </actionGroup> -+ <actionGroup name="NavigateToAndResetProductAttributeGridToDefaultView"> -+ <amOnPage url="{{AdminProductAttributeGridPage.url}}" stepKey="navigateToProductAttributeGrid"/> -+ <waitForPageLoad stepKey="waitForPageLoad"/> -+ <conditionalClick selector="{{AdminProductGridFilterSection.clearFilters}}" dependentSelector="{{AdminProductGridFilterSection.clearFilters}}" visible="true" stepKey="clickClearFilters"/> -+ <waitForPageLoad stepKey="waitForGridLoad"/> -+ </actionGroup> -+ <!--Filter and select the the product --> -+ <actionGroup name="filterAndSelectProduct"> -+ <arguments> -+ <argument name="productSku" type="string"/> -+ </arguments> -+ <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="visitAdminProductPage"/> -+ <waitForPageLoad stepKey="waitForProductIndexPageToLoad"/> -+ <conditionalClick selector="{{AdminProductGridFilterSection.clearFilters}}" dependentSelector="{{AdminProductGridFilterSection.clearFilters}}" visible="true" stepKey="clickClearFilters"/> -+ <click selector="{{AdminProductGridFilterSection.filters}}" stepKey="openProductFilters"/> -+ <fillField selector="{{AdminProductGridFilterSection.skuFilter}}" userInput="{{productSku}}" stepKey="fillProductSkuFilter"/> -+ <click selector="{{AdminProductGridFilterSection.applyFilters}}" stepKey="clickApplyFilters"/> -+ <waitForElementNotVisible selector="{{AdminProductGridSection.loadingMask}}" stepKey="waitForFilteredGridLoad" time="30"/> -+ <click stepKey="openSelectedProduct" selector="{{AdminProductGridSection.productRowBySku(productSku)}}"/> -+ <waitForPageLoad stepKey="waitForProductToLoad"/> -+ <waitForElementVisible selector="{{AdminHeaderSection.pageTitle}}" stepKey="waitForProductTitle"/> -+ </actionGroup> -+ -+ <actionGroup name="deleteProductsIfTheyExist"> -+ <conditionalClick selector="{{AdminProductGridSection.multicheckDropdown}}" dependentSelector="{{AdminProductGridSection.firstProductRow}}" visible="true" stepKey="openMulticheckDropdown"/> -+ <conditionalClick selector="{{AdminProductGridSection.multicheckOption('Select All')}}" dependentSelector="{{AdminProductGridSection.firstProductRow}}" visible="true" stepKey="selectAllProductInFilteredGrid"/> -+ <click selector="{{AdminProductGridSection.bulkActionDropdown}}" stepKey="clickActionDropdown"/> -+ <click selector="{{AdminProductGridSection.bulkActionOption('Delete')}}" stepKey="clickDeleteAction"/> -+ <waitForElementVisible selector="{{AdminProductGridConfirmActionSection.ok}}" stepKey="waitForModalPopUp"/> -+ <click selector="{{AdminProductGridConfirmActionSection.ok}}" stepKey="confirmProductDelete"/> -+ </actionGroup> - </actionGroups> -diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminSetProductDesignSettingsActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminSetProductDesignSettingsActionGroup.xml -new file mode 100644 -index 00000000000..7998cdc9373 ---- /dev/null -+++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminSetProductDesignSettingsActionGroup.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="AdminSetProductDesignSettingsActionGroup"> -+ <arguments> -+ <argument name="designSettings" defaultValue="simpleBlankDesign"/> -+ </arguments> -+ <click selector="{{ProductDesignSection.DesignTab}}" stepKey="clickDesignTab"/> -+ <waitForPageLoad stepKey="waitForTabOpen"/> -+ <selectOption selector="{{ProductDesignSection.LayoutDropdown}}" userInput="{{designSettings.page_layout}}" stepKey="setLayout"/> -+ <selectOption selector="{{ProductDesignSection.productOptionsContainer}}" userInput="{{designSettings.options_container}}" stepKey="setDisplayProductOptions"/> -+ </actionGroup> -+</actionGroups> -diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminSubmitAdvancedInventoryFormActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminSubmitAdvancedInventoryFormActionGroup.xml -new file mode 100644 -index 00000000000..e27454fb604 ---- /dev/null -+++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminSubmitAdvancedInventoryFormActionGroup.xml -@@ -0,0 +1,15 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> -+ <!-- Click done button; -+ You must already be on the product form > Advanced Inventory --> -+ <actionGroup name="AdminSubmitAdvancedInventoryFormActionGroup"> -+ <click stepKey="clickOnDoneButton" selector="{{AdminProductFormAdvancedInventorySection.doneButton}}"/> -+ </actionGroup> -+</actionGroups> -diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminSwitchProductGiftMessageStatusActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminSwitchProductGiftMessageStatusActionGroup.xml -new file mode 100644 -index 00000000000..4d650a727ac ---- /dev/null -+++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminSwitchProductGiftMessageStatusActionGroup.xml -@@ -0,0 +1,21 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> -+ <actionGroup name="AdminSwitchProductGiftMessageStatusActionGroup"> -+ <arguments> -+ <argument name="status" defaultValue="0"/> -+ </arguments> -+ <click selector="{{AdminProductGiftOptionsSection.giftOptions}}" stepKey="clickToExpandGiftOptionsTab"/> -+ <waitForPageLoad stepKey="waitForGiftOptionsOpen"/> -+ <uncheckOption selector="{{AdminProductGiftOptionsSection.useConfigSettingsMessage}}" stepKey="uncheckConfigSettingsMessage"/> -+ <click selector="{{AdminProductGiftOptionsSection.toggleProductGiftMessage}}" stepKey="clickToGiftMessageSwitcher"/> -+ <seeElement selector="{{AdminProductGiftOptionsSection.giftMessageStatus('status')}}" stepKey="assertGiftMessageStatus"/> -+ </actionGroup> -+</actionGroups> -diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminUnassignCategoryOnProductAndSaveActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminUnassignCategoryOnProductAndSaveActionGroup.xml -new file mode 100644 -index 00000000000..35816f3cd67 ---- /dev/null -+++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminUnassignCategoryOnProductAndSaveActionGroup.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="AdminUnassignCategoryOnProductAndSaveActionGroup"> -+ <arguments> -+ <argument name="categoryName" type="string"/> -+ </arguments> -+ <!-- on edit Product page catalog/product/edit/id/{{product_id}}/ --> -+ <click selector="{{AdminProductFormSection.unselectCategories(categoryName)}}" stepKey="clearCategory"/> -+ <waitForPageLoad stepKey="waitForDelete"/> -+ <click selector="{{AdminProductFormSection.save}}" stepKey="clickSave"/> -+ <waitForPageLoad stepKey="waitForSavingProduct"/> -+ <see userInput="You saved the product." selector="{{CatalogProductsSection.messageSuccessSavedProduct}}" stepKey="seeSuccessMessage"/> -+ </actionGroup> -+</actionGroups> -\ No newline at end of file -diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AssertAttributeDeletionErrorMessageActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AssertAttributeDeletionErrorMessageActionGroup.xml -new file mode 100644 -index 00000000000..9402ac05d79 ---- /dev/null -+++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AssertAttributeDeletionErrorMessageActionGroup.xml -@@ -0,0 +1,16 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> -+ <actionGroup name="AssertAttributeDeletionErrorMessageActionGroup"> -+ <waitForElementVisible selector="{{AdminProductMessagesSection.errorMessage}}" stepKey="waitForErrorMessage"/> -+ <see selector="{{AdminProductMessagesSection.errorMessage}}" userInput="This attribute is used in configurable products." stepKey="deleteProductAttributeFailureMessage"/> -+ </actionGroup> -+</actionGroups> -+ -diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AssertDontSeeProductDetailsOnStorefrontActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AssertDontSeeProductDetailsOnStorefrontActionGroup.xml -new file mode 100644 -index 00000000000..3d0d16d1050 ---- /dev/null -+++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AssertDontSeeProductDetailsOnStorefrontActionGroup.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="AssertDontSeeProductDetailsOnStorefrontActionGroup"> -+ <arguments> -+ <argument name="productNumber" type="string"/> -+ <argument name="productInfo" type="string"/> -+ </arguments> -+ <dontSee selector="{{StorefrontCategoryProductSection.ProductInfoByNumber(productNumber)}}" userInput="{{productInfo}}" stepKey="seeProductInfo"/> -+ </actionGroup> -+</actionGroups> -\ No newline at end of file -diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AssertErrorMessageAfterDeletingWebsiteActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AssertErrorMessageAfterDeletingWebsiteActionGroup.xml -new file mode 100644 -index 00000000000..2ae224c71a9 ---- /dev/null -+++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AssertErrorMessageAfterDeletingWebsiteActionGroup.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="AssertErrorMessageAfterDeletingWebsiteActionGroup"> -+ <arguments> -+ <argument name="errorMessage" type="string"/> -+ </arguments> -+ <amOnPage url="{{ProductCatalogPage.url}}" stepKey="OpenProductCatalogPage"/> -+ <waitForPageLoad stepKey="waitForProductCatalogPageLoad"/> -+ <click selector="{{AdminCategoryModalSection.ok}}" stepKey="clickOkButtonFromAdminCategoryModalSection"/> -+ <see selector="{{AdminCategoryMessagesSection.errorMessage}}" userInput="{{errorMessage}}" stepKey="seeAssertErrorMessage"/> -+ </actionGroup> -+</actionGroups> -\ No newline at end of file -diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AssertProductAttributePresenceInCatalogProductGridActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AssertProductAttributePresenceInCatalogProductGridActionGroup.xml -new file mode 100644 -index 00000000000..25390e6b4a8 ---- /dev/null -+++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AssertProductAttributePresenceInCatalogProductGridActionGroup.xml -@@ -0,0 +1,19 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> -+ <actionGroup name="AssertProductAttributePresenceInCatalogProductGridActionGroup"> -+ <arguments> -+ <argument name="productAttribute" type="entity"/> -+ </arguments> -+ <waitForPageLoad stepKey="waitForCatalogProductGridPageLoad"/> -+ <seeElement selector="{{AdminGridHeaders.headerByName(productAttribute.label)}}" stepKey="seeAttributeInHeaders"/> -+ </actionGroup> -+</actionGroups> -+ -diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AssertProductDetailsOnStorefrontActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AssertProductDetailsOnStorefrontActionGroup.xml -new file mode 100644 -index 00000000000..68c6e92b93f ---- /dev/null -+++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AssertProductDetailsOnStorefrontActionGroup.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="AssertProductDetailsOnStorefrontActionGroup"> -+ <arguments> -+ <argument name="productNumber" type="string"/> -+ <argument name="productInfo" type="string"/> -+ </arguments> -+ <see selector="{{StorefrontCategoryProductSection.ProductInfoByNumber(productNumber)}}" userInput="{{productInfo}}" stepKey="seeProductInfo"/> -+ </actionGroup> -+</actionGroups> -\ No newline at end of file -diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AssertProductInStorefrontCategoryPageActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AssertProductInStorefrontCategoryPageActionGroup.xml -index 4eca49dc28b..8b657fa1b8a 100644 ---- a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AssertProductInStorefrontCategoryPageActionGroup.xml -+++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AssertProductInStorefrontCategoryPageActionGroup.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"> - <actionGroup name="AssertProductInStorefrontCategoryPage"> - <arguments> - <argument name="category"/> -diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AssertProductInStorefrontProductPageActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AssertProductInStorefrontProductPageActionGroup.xml -index 59c874b8481..391a1a7d670 100644 ---- a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AssertProductInStorefrontProductPageActionGroup.xml -+++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AssertProductInStorefrontProductPageActionGroup.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"> - <actionGroup name="AssertProductInStorefrontProductPage"> - <arguments> - <argument name="product"/> -diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AssertProductInfoOnEditPageActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AssertProductInfoOnEditPageActionGroup.xml -new file mode 100644 -index 00000000000..7917fe68aae ---- /dev/null -+++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AssertProductInfoOnEditPageActionGroup.xml -@@ -0,0 +1,22 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> -+ <actionGroup name="AssertProductInfoOnEditPageActionGroup" extends="OpenEditProductOnBackendActionGroup"> -+ <arguments> -+ <argument name="product" type="entity"/> -+ </arguments> -+ <waitForPageLoad stepKey="waitForProductToLoad"/> -+ <seeInField selector="{{AdminProductFormSection.productName}}" userInput="{{product.name}}" stepKey="seeProductName"/> -+ <seeInField selector="{{AdminProductFormSection.productSku}}" userInput="{{product.sku}}" stepKey="seeProductSku"/> -+ <seeInField selector="{{AdminProductFormSection.productPrice}}" userInput="{{product.price}}" stepKey="seeProductPrice"/> -+ <seeInField selector="{{AdminProductFormSection.productQuantity}}" userInput="{{product.quantity}}" stepKey="seeProductQuantity"/> -+ <seeInField selector="{{AdminProductFormSection.productStockStatus}}" userInput="{{product.status}}" stepKey="seeProductStockStatus"/> -+ </actionGroup> -+</actionGroups> -diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AssertProductOnAdminGridActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AssertProductOnAdminGridActionGroup.xml -new file mode 100644 -index 00000000000..963c9d9f1c9 ---- /dev/null -+++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AssertProductOnAdminGridActionGroup.xml -@@ -0,0 +1,17 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> -+ <actionGroup name="AssertProductOnAdminGridActionGroup" extends="viewProductInAdminGrid"> -+ <arguments> -+ <argument name="product" defaultValue="_defaultProduct"/> -+ </arguments> -+ <remove keyForRemoval="clickClearFiltersAfter"/> -+ </actionGroup> -+</actionGroups> -diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AssertStorefrontCustomProductAttributeActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AssertStorefrontCustomProductAttributeActionGroup.xml -new file mode 100644 -index 00000000000..e27efb41a7e ---- /dev/null -+++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AssertStorefrontCustomProductAttributeActionGroup.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="AssertStorefrontCustomProductAttributeActionGroup"> -+ <arguments> -+ <argument name="attributeLabel" type="string"/> -+ <argument name="attributeValue" type="string"/> -+ </arguments> -+ <see userInput="{{attributeLabel}}" selector="{{StorefrontProductMoreInformationSection.customAttributeLabel(attributeLabel)}}" stepKey="seeAttributeLabel" /> -+ <see userInput="{{attributeValue}}" selector="{{StorefrontProductMoreInformationSection.customAttributeValue(attributeLabel)}}" stepKey="seeAttributeValue" /> -+ </actionGroup> -+</actionGroups> -diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AssertStorefrontProductPricesActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AssertStorefrontProductPricesActionGroup.xml -new file mode 100644 -index 00000000000..6e90fe7324d ---- /dev/null -+++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AssertStorefrontProductPricesActionGroup.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="AssertStorefrontProductPricesActionGroup"> -+ <arguments> -+ <argument name="productPrice" type="string"/> -+ <argument name="productFinalPrice" type="string"/> -+ </arguments> -+ <waitForPageLoad stepKey="waitForStorefrontProductPageLoad"/> -+ <see selector="{{StorefrontProductInfoMainSection.productPrice}}" stepKey="productPriceAmount" userInput="{{productPrice}}"/> -+ <see selector="{{StorefrontProductInfoMainSection.updatedPrice}}" stepKey="productFinalPriceAmount" userInput="{{productFinalPrice}}"/> -+ </actionGroup> -+</actionGroups> -\ No newline at end of file -diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AssertSubTotalOnStorefrontMinicartActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AssertSubTotalOnStorefrontMinicartActionGroup.xml -new file mode 100644 -index 00000000000..26693771bd1 ---- /dev/null -+++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AssertSubTotalOnStorefrontMinicartActionGroup.xml -@@ -0,0 +1,23 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> -+ <actionGroup name="AssertSubTotalOnStorefrontMiniCartActionGroup"> -+ <arguments> -+ <argument name="subTotal" type="string"/> -+ </arguments> -+ <waitForElementVisible selector="{{StorefrontMinicartSection.showCart}}" stepKey="waitForShowCartButtonVisible"/> -+ <click selector="{{StorefrontMinicartSection.showCart}}" stepKey="clickOnMiniCart"/> -+ <grabTextFrom selector="{{StorefrontMinicartSection.miniCartSubtotalField}}" stepKey="grabTextFromMiniCartSubtotalField"/> -+ <assertEquals message="Mini shopping cart should contain subtotal {{subTotal}}" stepKey="assertSubtotalFieldFromMiniShoppingCart1"> -+ <expectedResult type="string">{{subTotal}}</expectedResult> -+ <actualResult type="variable">grabTextFromMiniCartSubtotalField</actualResult> -+ </assertEquals> -+ </actionGroup> -+</actionGroups> -\ No newline at end of file -diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/CheckItemInLayeredNavigationActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/CheckItemInLayeredNavigationActionGroup.xml -index 304f38e2279..f2a7a0acffe 100644 ---- a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/CheckItemInLayeredNavigationActionGroup.xml -+++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/CheckItemInLayeredNavigationActionGroup.xml -@@ -6,7 +6,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"> -+<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> - <actionGroup name="CheckItemInLayeredNavigationActionGroup"> - <arguments> - <argument name="itemType"/> -diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/CheckProductsOrderActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/CheckProductsOrderActionGroup.xml -new file mode 100644 -index 00000000000..f7cd2e70762 ---- /dev/null -+++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/CheckProductsOrderActionGroup.xml -@@ -0,0 +1,23 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> -+ -+ <actionGroup name="CompareTwoProductsOrder"> -+ <arguments> -+ <argument name="product_1"/> -+ <argument name="product_2"/> -+ </arguments> -+ <amOnPage url="{{StorefrontHomePage.url}}" stepKey="goToHomePage"/> -+ <waitForPageLoad stepKey="waitForPageLoad5"/> -+ <grabAttributeFrom selector="{{StorefrontCategoryProductSection.ProductImageByNumber('1')}}" userInput="alt" stepKey="grabFirstProductName1_1"/> -+ <assertEquals expected="{{product_1.name}}" actual="($grabFirstProductName1_1)" message="notExpectedOrder" stepKey="compare1"/> -+ <grabAttributeFrom selector="{{StorefrontCategoryProductSection.ProductImageByNumber('2')}}" userInput="alt" stepKey="grabFirstProductName2_2"/> -+ <assertEquals expected="{{product_2.name}}" actual="($grabFirstProductName2_2)" message="notExpectedOrder" stepKey="compare2"/> -+ </actionGroup> -+</actionGroups> -diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/CreateNewProductActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/CreateNewProductActionGroup.xml -new file mode 100644 -index 00000000000..53de47f8106 ---- /dev/null -+++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/CreateNewProductActionGroup.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="CreateNewProductActionGroup"> -+ <click stepKey="openCatalog" selector="{{AdminMenuSection.catalog}}"/> -+ <waitForPageLoad stepKey="waitForCatalogSubmenu" time="5"/> -+ <click stepKey="clickOnProducts" selector="{{CatalogSubmenuSection.products}}"/> -+ <waitForPageLoad stepKey="waitForProductsPage" time="10"/> -+ <click stepKey="addProduct" selector="{{ProductsPageSection.addProductButton}}"/> -+ <waitForPageLoad stepKey="waitForNewProductPage" time="10"/> -+ <fillField stepKey="FillProductName" selector="{{NewProductPageSection.productName}}" userInput="{{NewProductData.ProductName}}"/> -+ <fillField stepKey="FillPrice" selector="{{NewProductPageSection.price}}" userInput="{{NewProductData.Price}}"/> -+ <fillField stepKey="FillQuantity" selector="{{NewProductPageSection.quantity}}" userInput="{{NewProductData.Quantity}}"/> -+ <click stepKey="Save" selector="{{NewProductPageSection.saveButton}}"/> -+ <waitForElementVisible stepKey="waitForSuccessfullyCreatedMessage" selector="{{NewProductPageSection.createdSuccessMessage}}" time="10"/> -+ <waitForPageLoad stepKey="waitForPageLoad" time="10"/> -+ </actionGroup> -+</actionGroups> -diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/CustomOptionsActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/CustomOptionsActionGroup.xml -index 6b47479d41c..7723f32d140 100644 ---- a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/CustomOptionsActionGroup.xml -+++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/CustomOptionsActionGroup.xml -@@ -6,7 +6,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"> -+<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> - <actionGroup name="CreateCustomRadioOptions"> - <!-- ActionGroup will add a single custom option to a product --> - <!-- You must already be on the product creation page --> -@@ -15,7 +15,6 @@ - <argument name="productOption"/> - <argument name="productOption2"/> - </arguments> -- - <click stepKey="clickAddOptions" selector="{{AdminProductCustomizableOptionsSection.addOptionBtn}}"/> - <waitForPageLoad stepKey="waitForAddProductPageLoad"/> - -@@ -48,10 +47,97 @@ - <fillField selector="{{AdminProductCustomizableOptionsSection.lastOptionTitle}}" userInput="{{option.title}}" stepKey="fillTitle"/> - <click selector="{{AdminProductCustomizableOptionsSection.lastOptionTypeParent}}" stepKey="openTypeSelect"/> - <click selector="{{AdminProductCustomizableOptionsSection.optionType('File')}}" stepKey="selectTypeFile"/> -- <waitForElementVisible selector="{{AdminProductCustomizableOptionsSection.optionPrice}}" stepKey="waitForElements"/> -- <fillField selector="{{AdminProductCustomizableOptionsSection.optionPrice}}" userInput="{{option.price}}" stepKey="fillPrice"/> -- <selectOption selector="{{AdminProductCustomizableOptionsSection.optionPriceType}}" userInput="{{option.price_type}}" stepKey="selectPriceType"/> -- <fillField selector="{{AdminProductCustomizableOptionsSection.optionFileExtensions}}" userInput="{{option.file_extension}}" stepKey="fillCompatibleExtensions"/> -+ <waitForElementVisible selector="{{AdminProductCustomizableOptionsSection.optionPriceByTitle(option.title)}}" stepKey="waitForElements"/> -+ <fillField selector="{{AdminProductCustomizableOptionsSection.optionPriceByTitle(option.title)}}" userInput="{{option.price}}" stepKey="fillPrice"/> -+ <selectOption selector="{{AdminProductCustomizableOptionsSection.optionPriceTypeByTitle(option.title)}}" userInput="{{option.price_type}}" stepKey="selectPriceType"/> -+ <fillField selector="{{AdminProductCustomizableOptionsSection.optionFileExtensionByTitle(option.title)}}" userInput="{{option.file_extension}}" stepKey="fillCompatibleExtensions"/> -+ </actionGroup> -+ <actionGroup name="AddProductCustomOptionField"> -+ <arguments> -+ <argument name="option" defaultValue="ProductOptionField"/> -+ </arguments> -+ <scrollTo selector="{{AdminProductCustomizableOptionsSection.customizableOptions}}" stepKey="scrollToAddButtonOption"/> -+ <conditionalClick selector="{{AdminProductCustomizableOptionsSection.customizableOptions}}" dependentSelector="{{AdminProductCustomizableOptionsSection.addOptionBtn}}" visible="false" stepKey="openCustomOptionSection"/> -+ <click selector="{{AdminProductCustomizableOptionsSection.addOptionBtn}}" stepKey="clickAddOption"/> -+ <waitForElementVisible selector="{{AdminProductCustomizableOptionsSection.lastOptionTitle}}" stepKey="waitForOption"/> -+ <fillField selector="{{AdminProductCustomizableOptionsSection.lastOptionTitle}}" userInput="{{option.title}}" stepKey="fillTitle"/> -+ <click selector="{{AdminProductCustomizableOptionsSection.lastOptionTypeParent}}" stepKey="openTypeSelect"/> -+ <click selector="{{AdminProductCustomizableOptionsSection.optionType('Field')}}" stepKey="selectTypeFile"/> -+ <waitForElementVisible selector="{{AdminProductCustomizableOptionsSection.optionPriceByTitle(option.title)}}" stepKey="waitForElements"/> -+ <fillField selector="{{AdminProductCustomizableOptionsSection.optionPriceByTitle(option.title)}}" userInput="{{option.price}}" stepKey="fillPrice"/> -+ <selectOption selector="{{AdminProductCustomizableOptionsSection.optionPriceTypeByTitle(option.title)}}" userInput="{{option.price_type}}" stepKey="selectPriceType"/> -+ <fillField selector="{{AdminProductCustomizableOptionsSection.optionSkuByTitle(option.title)}}" userInput="{{option.title}}" stepKey="fillSku"/> -+ </actionGroup> -+ <actionGroup name="importProductCustomizableOptions"> -+ <arguments> -+ <argument name="productName" type="string"/> -+ </arguments> -+ <click selector="{{AdminProductCustomizableOptionsSection.importOptions}}" stepKey="clickImportOptions"/> -+ <waitForElementVisible selector="{{AdminProductImportOptionsSection.selectProductTitle}}" stepKey="waitForTitleVisible"/> -+ <conditionalClick selector="{{AdminProductImportOptionsSection.resetFiltersButton}}" dependentSelector="{{AdminProductImportOptionsSection.resetFiltersButton}}" visible="true" stepKey="clickResetFilters"/> -+ <click selector="{{AdminProductImportOptionsSection.filterButton}}" stepKey="clickFilterButton"/> -+ <waitForElementVisible selector="{{AdminProductImportOptionsSection.nameField}}" stepKey="waitForNameField"/> -+ <fillField selector="{{AdminProductImportOptionsSection.nameField}}" userInput="{{productName}}" stepKey="fillProductName"/> -+ <click selector="{{AdminProductImportOptionsSection.applyFiltersButton}}" stepKey="clickApplyFilters"/> -+ <checkOption selector="{{AdminProductImportOptionsSection.firstRowItemCheckbox}}" stepKey="checkProductCheckbox"/> -+ <click selector="{{AdminProductImportOptionsSection.importButton}}" stepKey="clickImport"/> -+ </actionGroup> -+ <actionGroup name="resetImportOptionFilter"> -+ <conditionalClick selector="{{AdminProductCustomizableOptionsSection.customizableOptions}}" dependentSelector="{{AdminProductCustomizableOptionsSection.addOptionBtn}}" visible="false" stepKey="openCustomOptionSection"/> -+ <click selector="{{AdminProductCustomizableOptionsSection.importOptions}}" stepKey="clickImportOptions"/> -+ <click selector="{{AdminProductImportOptionsSection.resetFiltersButton}}" stepKey="clickResetFilterButton"/> -+ </actionGroup> -+ <actionGroup name="checkCustomizableOptionImport"> -+ <arguments> -+ <argument name="option" defaultValue="ProductOptionField"/> -+ <argument name="optionIndex" type="string"/> -+ </arguments> -+ <grabValueFrom selector="{{AdminProductCustomizableOptionsSection.optionTitleInput(optionIndex)}}" stepKey="grabOptionTitle"/> -+ <grabValueFrom selector="{{AdminProductCustomizableOptionsSection.optionPrice(optionIndex)}}" stepKey="grabOptionPrice"/> -+ <grabValueFrom selector="{{AdminProductCustomizableOptionsSection.optionSku(optionIndex)}}" stepKey="grabOptionSku"/> -+ <assertEquals expected="{{option.title}}" expectedType="string" actual="$grabOptionTitle" stepKey="assertOptionTitle"/> -+ <assertEquals expected="{{option.price}}" expectedType="string" actual="$grabOptionPrice" stepKey="assertOptionPrice"/> -+ <assertEquals expected="{{option.title}}" expectedType="string" actual="$grabOptionSku" stepKey="assertOptionSku"/> -+ </actionGroup> -+ <!-- Assumes we are on product edit page --> -+ <actionGroup name="AdminDeleteAllProductCustomOptions"> -+ <conditionalClick selector="{{AdminProductCustomizableOptionsSection.customizableOptions}}" dependentSelector="{{AdminProductCustomizableOptionsSection.checkIfCustomizableOptionsTabOpen}}" visible="true" stepKey="expandContentTab"/> -+ <waitForPageLoad time="10" stepKey="waitCustomizableOptionsTabOpened"/> -+ <executeInSelenium function="function($webdriver) use ($I) { -+ $buttons = $webdriver->findElements(\Facebook\WebDriver\WebDriverBy::cssSelector('[data-index=\'options\'] [data-index=\'delete_button\']')); -+ while(!empty($buttons)) { -+ $button = reset($buttons); -+ $I->executeJS('arguments[0].scrollIntoView(false)', [$button]); -+ $button->click(); -+ $webdriver->wait()->until(\Facebook\WebDriver\WebDriverExpectedCondition::stalenessOf($button)); -+ $buttons = $webdriver->findElements(\Facebook\WebDriver\WebDriverBy::cssSelector('[data-index=\'options\'] [data-index=\'delete_button\']')); -+ } -+ }" stepKey="deleteCustomOptions"/> -+ <dontSeeElement selector="{{AdminProductCustomizableOptionsSection.customOptionButtonDelete}}" stepKey="assertNoCustomOptions"/> -+ </actionGroup> -+ <!-- Assumes we are on product edit page --> -+ <actionGroup name="AdminAssertProductHasNoCustomOptions"> -+ <conditionalClick selector="{{AdminProductCustomizableOptionsSection.customizableOptions}}" dependentSelector="{{AdminProductCustomizableOptionsSection.checkIfCustomizableOptionsTabOpen}}" visible="true" stepKey="expandContentTab"/> -+ <waitForPageLoad time="10" stepKey="waitCustomizableOptionsTabOpened"/> -+ <dontSeeElement selector="{{AdminProductCustomizableOptionsSection.customOption}}" stepKey="assertNoCustomOptions"/> -+ </actionGroup> -+ <!-- Assumes we are on product edit page --> -+ <actionGroup name="AdminAssertProductHasNoCustomOption" extends="AdminAssertProductCustomOptionVisible"> -+ <remove keyForRemoval="assertCustomOptionVisible"/> -+ <dontSeeElement selector="{{AdminProductCustomizableOptionsSection.fillOptionTitle(option.title)}}" after="waitCustomizableOptionsTabOpened" stepKey="assertNoCustomOption"/> -+ </actionGroup> -+ <!-- Assumes we are on product edit page --> -+ <actionGroup name="AdminAssertProductCustomOptionVisible"> -+ <arguments> -+ <argument name="option" defaultValue="ProductOptionField"/> -+ </arguments> -+ <conditionalClick selector="{{AdminProductCustomizableOptionsSection.customizableOptions}}" dependentSelector="{{AdminProductCustomizableOptionsSection.checkIfCustomizableOptionsTabOpen}}" visible="true" stepKey="expandContentTab"/> -+ <waitForPageLoad time="10" stepKey="waitCustomizableOptionsTabOpened"/> -+ <seeElement selector="{{AdminProductCustomizableOptionsSection.fillOptionTitle(option.title)}}" stepKey="assertCustomOptionVisible"/> -+ </actionGroup> -+ <!-- Assumes we are on product edit page --> -+ <actionGroup name="AdminDeleteProductCustomOption" extends="AdminAssertProductCustomOptionVisible"> -+ <remove keyForRemoval="assertCustomOptionVisible"/> -+ <click selector="{{AdminProductCustomizableOptionsSection.deleteCustomOptions(option.title)}}" after="waitCustomizableOptionsTabOpened" stepKey="clickDeleteCustomOption"/> - </actionGroup> -- - </actionGroups> -diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/DeleteProductActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/DeleteProductActionGroup.xml -new file mode 100644 -index 00000000000..7491b39aa8f ---- /dev/null -+++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/DeleteProductActionGroup.xml -@@ -0,0 +1,27 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> -+ <actionGroup name="DeleteProductActionGroup"> -+ <arguments> -+ <argument name="productName" defaultValue=""/> -+ </arguments> -+ <click stepKey="openCatalog" selector="{{AdminMenuSection.catalog}}"/> -+ <waitForPageLoad stepKey="waitForCatalogSubmenu" time="5"/> -+ <click stepKey="clickOnProducts" selector="{{CatalogSubmenuSection.products}}"/> -+ <waitForPageLoad stepKey="waitForProductsPage" time="10"/> -+ <click stepKey="TickCheckbox" selector="{{ProductsPageSection.checkboxForProduct(productName)}}"/> -+ <click stepKey="OpenActions" selector="{{ProductsPageSection.actions}}"/> -+ <waitForAjaxLoad stepKey="waitForDelete" time="5"/> -+ <click stepKey="ChooseDelete" selector="{{ProductsPageSection.delete}}"/> -+ <waitForPageLoad stepKey="waitForDeleteItemPopup" time="10"/> -+ <click stepKey="clickOnOk" selector="{{ProductsPageSection.ok}}"/> -+ <waitForElementVisible stepKey="waitForSuccessfullyDeletedMessage" selector="{{ProductsPageSection.deletedSuccessMessage}}" time="10"/> -+ </actionGroup> -+</actionGroups> -diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/DeleteProductAttributeByAttributeCodeActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/DeleteProductAttributeByAttributeCodeActionGroup.xml -new file mode 100644 -index 00000000000..575cbdcd9b6 ---- /dev/null -+++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/DeleteProductAttributeByAttributeCodeActionGroup.xml -@@ -0,0 +1,20 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> -+<actionGroup name="DeleteProductAttributeByAttributeCodeActionGroup"> -+ <arguments> -+ <argument name="productAttributeCode" type="string"/> -+ </arguments> -+ <waitForPageLoad stepKey="waitForViewAdminProductAttributeLoad" time="30" /> -+ <click selector="{{AttributePropertiesSection.DeleteAttribute}}" stepKey="deleteAttribute"/> -+ <click selector="{{ModalConfirmationSection.OkButton}}" stepKey="clickOnConfirmOk"/> -+ <waitForPageLoad stepKey="waitForViewProductAttributePageLoad"/> -+</actionGroup> -+</actionGroups> -diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/FilterProductInGridByStoreViewAndNameActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/FilterProductInGridByStoreViewAndNameActionGroup.xml -new file mode 100644 -index 00000000000..d5d378ad11b ---- /dev/null -+++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/FilterProductInGridByStoreViewAndNameActionGroup.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="FilterProductInGridByStoreViewAndNameActionGroup"> -+ <arguments> -+ <argument name="storeView" type="string"/> -+ <argument name="productName" type="string"/> -+ </arguments> -+ <amOnPage url="{{ProductCatalogPage.url}}" stepKey="OpenProductCatalogPageToSearchProduct"/> -+ <waitForPageLoad stepKey="waitForProductCatalogPageToLoad"/> -+ <conditionalClick selector="{{AdminProductGridFilterSection.clearAll}}" dependentSelector="{{AdminProductGridFilterSection.clearAll}}" visible="true" stepKey="clickClearAll"/> -+ <click selector="{{AdminProductGridFilterSection.filters}}" stepKey="clickFiltersButton"/> -+ <click selector="{{AdminProductGridFilterSection.storeViewDropdown(storeView)}}" stepKey="clickStoreViewDropdown"/> -+ <fillField selector="{{AdminProductGridFilterSection.nameFilter}}" userInput="{{productName}}" stepKey="fillProductNameInNameFilter"/> -+ <click selector="{{AdminProductGridFilterSection.applyFilters}}" stepKey="clickApplyFiltersButton"/> -+ <see selector="{{AdminProductGridFilterSection.nthRow('1')}}" userInput="{{productName}}" stepKey="seeFirstRowToVerifyProductVisibleInGrid"/> -+ </actionGroup> -+</actionGroups> -\ No newline at end of file -diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/MoveCategoryActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/MoveCategoryActionGroup.xml -index ae9dc0557a9..7bb9aa60ca6 100644 ---- a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/MoveCategoryActionGroup.xml -+++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/MoveCategoryActionGroup.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"> - <actionGroup name="MoveCategoryActionGroup"> - <arguments> - <argument name="childCategory"/> -diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/OpenEditProductOnBackendActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/OpenEditProductOnBackendActionGroup.xml -index 07fba7cc6be..ea2543cd5c2 100644 ---- a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/OpenEditProductOnBackendActionGroup.xml -+++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/OpenEditProductOnBackendActionGroup.xml -@@ -6,12 +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"> -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> - <actionGroup name="OpenEditProductOnBackendActionGroup"> - <arguments> - <argument name="product" defaultValue="product"/> - </arguments> -- <click selector="{{AdminProductGridSection.firstRow}}" stepKey="clickOnProductRow"/> -+ <click selector="{{AdminProductGridSection.productRowBySku(product.sku)}}" stepKey="clickOnProductRow"/> - <waitForPageLoad time="30" stepKey="waitForProductPageLoad"/> - <seeInField userInput="{{product.sku}}" selector="{{AdminProductFormSection.productSku}}" stepKey="seeProductSkuOnEditProductPage"/> - </actionGroup> -diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/OpenProductAttributeFromSearchResultInGridActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/OpenProductAttributeFromSearchResultInGridActionGroup.xml -new file mode 100644 -index 00000000000..31b024c82a9 ---- /dev/null -+++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/OpenProductAttributeFromSearchResultInGridActionGroup.xml -@@ -0,0 +1,19 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> -+ <actionGroup name="OpenProductAttributeFromSearchResultInGridActionGroup" extends="SearchAttributeByCodeOnProductAttributeGridActionGroup"> -+ <arguments> -+ <argument name="productAttributeCode" type="string"/> -+ </arguments> -+ <waitForElementVisible selector="{{AdminProductAttributeGridSection.AttributeCode(productAttributeCode)}}" stepKey="waitForAdminProductAttributeGridLoad"/> -+ <click selector="{{AdminProductAttributeGridSection.AttributeCode(productAttributeCode)}}" stepKey="clickAttributeToView"/> -+ <waitForPageLoad stepKey="waitForViewAdminProductAttributeLoad" /> -+ </actionGroup> -+</actionGroups> -diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/OpenProductFromCategoryPageActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/OpenProductFromCategoryPageActionGroup.xml -index e8794ab895c..c460dcbfbec 100644 ---- a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/OpenProductFromCategoryPageActionGroup.xml -+++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/OpenProductFromCategoryPageActionGroup.xml -@@ -5,7 +5,7 @@ - * See COPYING.txt for license details. - --> - --<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Test/etc/actionGroupSchema.xsd"> -+<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> - <actionGroup name="OpenProductFromCategoryPageActionGroup"> - <arguments> - <argument name="category"/> -diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/OpenStoreFrontProductPageActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/OpenStoreFrontProductPageActionGroup.xml -new file mode 100644 -index 00000000000..4bfd5673e4a ---- /dev/null -+++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/OpenStoreFrontProductPageActionGroup.xml -@@ -0,0 +1,17 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> -+ <actionGroup name="OpenStoreFrontProductPageActionGroup"> -+ <arguments> -+ <argument name="productUrlKey" type="string"/> -+ </arguments> -+ <amOnPage url="{{StorefrontProductPage.url(productUrlKey)}}" stepKey="amOnProductPage"/> -+ <waitForPageLoad stepKey="waitForProductPageLoad"/> -+ </actionGroup> -+</actionGroups> -diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/OpenStorefrontProductPageByProductNameActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/OpenStorefrontProductPageByProductNameActionGroup.xml -new file mode 100644 -index 00000000000..c25cdc403f8 ---- /dev/null -+++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/OpenStorefrontProductPageByProductNameActionGroup.xml -@@ -0,0 +1,17 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> -+ <actionGroup name="OpenStorefrontProductPageByProductNameActionGroup"> -+ <arguments> -+ <argument name="productName" type="string" defaultValue="{{_defaultProduct.name}}"/> -+ </arguments> -+ <amOnPage url="{{productName}}.html" stepKey="amOnProductPage"/> -+ <waitForPageLoad stepKey="waitForProductPageLoad"/> -+ </actionGroup> -+</actionGroups> -diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/RestoreLayoutSettingActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/RestoreLayoutSettingActionGroup.xml -index 53acfe2b437..2f9d38516bd 100644 ---- a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/RestoreLayoutSettingActionGroup.xml -+++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/RestoreLayoutSettingActionGroup.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"> - <actionGroup name="RestoreLayoutSetting"> - <selectOption selector="{{DefaultLayoutsSection.categoryLayout}}" userInput="No layout updates" stepKey="selectNoLayoutUpdates1" after="expandDefaultLayouts"/> - <selectOption selector="{{DefaultLayoutsSection.productLayout}}" userInput="No layout updates" stepKey="selectNoLayoutUpdates2" before="clickSaveConfig"/> -diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/SearchAndMultiselectActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/SearchAndMultiselectActionGroup.xml -index 943fe803232..53e7ea3589d 100644 ---- a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/SearchAndMultiselectActionGroup.xml -+++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/SearchAndMultiselectActionGroup.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"> - <actionGroup name="searchAndMultiSelectActionGroup"> - <arguments> - <argument name="dropDownSelector" /> -diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/SearchAttributeByCodeOnProductAttributeGridActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/SearchAttributeByCodeOnProductAttributeGridActionGroup.xml -new file mode 100644 -index 00000000000..67cdd8192fc ---- /dev/null -+++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/SearchAttributeByCodeOnProductAttributeGridActionGroup.xml -@@ -0,0 +1,24 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> -+ <actionGroup name="SearchAttributeByCodeOnProductAttributeGridActionGroup"> -+ <arguments> -+ <argument name="productAttributeCode" type="string"/> -+ </arguments> -+ <amOnPage url="{{AdminProductAttributeGridPage.url}}" stepKey="navigateToProductAttributeGrid"/> -+ <waitForPageLoad stepKey="waitForAdminProductAttributeGridLoad"/> -+ <click selector="{{AdminProductAttributeGridSection.ResetFilter}}" stepKey="resetFiltersOnGrid"/> -+ <waitForPageLoad stepKey="waitForAdminProductAttributeGridSectionLoad"/> -+ <fillField selector="{{AdminProductAttributeGridSection.FilterByAttributeCode}}" userInput="{{productAttributeCode}}" stepKey="setAttributeCode"/> -+ <click selector="{{AdminProductAttributeGridSection.Search}}" stepKey="searchForAttributeFromTheGrid"/> -+ <waitForLoadingMaskToDisappear stepKey="waitForLoadingMaskOnGridToDisappear"/> -+ <see selector="{{AdminProductAttributeGridSection.attributeCodeColumn}}" userInput="{{productAttributeCode}}" stepKey="seeAttributeCodeInGrid"/> -+ </actionGroup> -+</actionGroups> -diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/SearchForProductOnBackendActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/SearchForProductOnBackendActionGroup.xml -index 5fbc9c5d7fc..aca9ba24c11 100644 ---- a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/SearchForProductOnBackendActionGroup.xml -+++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/SearchForProductOnBackendActionGroup.xml -@@ -6,16 +6,30 @@ - */ - --> - <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="SearchForProductOnBackendActionGroup"> - <arguments> - <argument name="product" defaultValue="product"/> - </arguments> - <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="navigateToProductIndex"/> -- <waitForPageLoad time="30" stepKey="waitForProductsPageToLoad"/> -+ <waitForPageLoad time="60" stepKey="waitForProductsPageToLoad"/> - <click selector="{{AdminProductFiltersSection.filtersButton}}" stepKey="openFiltersSectionOnProductsPage"/> - <conditionalClick selector="{{AdminProductFiltersSection.clearFiltersButton}}" dependentSelector="{{AdminProductFiltersSection.clearFiltersButton}}" visible="true" stepKey="cleanFiltersIfTheySet"/> - <fillField userInput="{{product.sku}}" selector="{{AdminProductFiltersSection.skuInput}}" stepKey="fillSkuFieldOnFiltersSection"/> - <click selector="{{AdminProductFiltersSection.apply}}" stepKey="clickApplyFiltersButton"/> - </actionGroup> -+ -+ <actionGroup name="SearchForProductOnBackendByNameActionGroup" extends="SearchForProductOnBackendActionGroup"> -+ <arguments> -+ <argument name="productName" type="string"/> -+ </arguments> -+ <remove keyForRemoval="fillSkuFieldOnFiltersSection"/> -+ <fillField userInput="{{productName}}" selector="{{AdminProductFiltersSection.nameInput}}" after="cleanFiltersIfTheySet" stepKey="fillNameFieldOnFiltersSection"/> -+ </actionGroup> -+ -+ <actionGroup name="ClearProductsFilterActionGroup"> -+ <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="navigateToProductIndex"/> -+ <waitForPageLoad time="30" stepKey="waitForProductsPageToLoad"/> -+ <conditionalClick selector="{{AdminProductFiltersSection.clearFiltersButton}}" dependentSelector="{{AdminProductFiltersSection.clearFiltersButton}}" visible="true" stepKey="cleanFiltersIfTheySet"/> -+ </actionGroup> - </actionGroups> -diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontAddProductToCartWithQtyActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontAddProductToCartWithQtyActionGroup.xml -new file mode 100644 -index 00000000000..5432d547e80 ---- /dev/null -+++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontAddProductToCartWithQtyActionGroup.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="StorefrontAddProductToCartWithQtyActionGroup"> -+ <arguments> -+ <argument name="productQty" type="string"/> -+ </arguments> -+ <waitForPageLoad stepKey="waitForStorefrontProductPageLoad"/> -+ <fillField selector="{{StorefrontProductPageSection.qtyInput}}" userInput="{{productQty}}" stepKey="fillProduct1Quantity"/> -+ <click selector="{{StorefrontProductActionSection.addToCart}}" stepKey="clickOnAddToCartButton"/> -+ <waitForPageLoad stepKey="waitForProductToAddInCart"/> -+ <waitForElementVisible selector="{{StorefrontMessagesSection.success}}" stepKey="waitForSuccessMessage"/> -+ <seeElement selector="{{StorefrontProductPageSection.successMsg}}" stepKey="seeSuccessSaveMessage"/> -+ </actionGroup> -+</actionGroups> -diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontAddToCartCustomOptionsProductPageActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontAddToCartCustomOptionsProductPageActionGroup.xml -index 105a5c58788..080b374c60b 100644 ---- a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontAddToCartCustomOptionsProductPageActionGroup.xml -+++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontAddToCartCustomOptionsProductPageActionGroup.xml -@@ -6,7 +6,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"> - <!--Click Add to Cart button in storefront product page--> - <actionGroup name="StorefrontAddToCartCustomOptionsProductPageActionGroup"> - <arguments> -@@ -14,6 +14,7 @@ - </arguments> - <click selector="{{StorefrontProductActionSection.addToCart}}" stepKey="addToCart"/> - <waitForPageLoad stepKey="waitForPageLoad"/> -+ <waitForElementVisible selector="{{StorefrontMessagesSection.success}}" stepKey="waitForSuccessMessage"/> - <see selector="{{StorefrontMessagesSection.success}}" userInput="You added {{productName}} to your shopping cart." stepKey="seeAddToCartSuccessMessage"/> - </actionGroup> - </actionGroups> -diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontAssertCustomOptionByTitleActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontAssertCustomOptionByTitleActionGroup.xml -new file mode 100644 -index 00000000000..bc922a40b05 ---- /dev/null -+++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontAssertCustomOptionByTitleActionGroup.xml -@@ -0,0 +1,16 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> -+ <actionGroup name="StorefrontAssertCustomOptionByTitleActionGroup"> -+ <arguments> -+ <argument name="title" type="string"/> -+ </arguments> -+ <seeElement selector="{{StorefrontProductInfoMainSection.customOptionByTitle(title)}}" stepKey="seeCustomOption"/> -+ </actionGroup> -+</actionGroups> -diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontAssertGiftMessageFieldsActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontAssertGiftMessageFieldsActionGroup.xml -new file mode 100644 -index 00000000000..a5f1b92862b ---- /dev/null -+++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontAssertGiftMessageFieldsActionGroup.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="StorefrontAssertGiftMessageFieldsActionGroup"> -+ <waitForElementVisible selector="{{StorefrontProductCartGiftOptionSection.giftOptions}}" stepKey="waitForCartGiftOptionVisible"/> -+ <click selector="{{StorefrontProductCartGiftOptionSection.giftOptions}}" stepKey="clickGiftOptionBtn"/> -+ <seeElement selector="{{StorefrontProductCartGiftOptionSection.fieldTo}}" stepKey="seeFieldTo"/> -+ <seeElement selector="{{StorefrontProductCartGiftOptionSection.fieldFrom}}" stepKey="seeFieldFrom"/> -+ <seeElement selector="{{StorefrontProductCartGiftOptionSection.message}}" stepKey="seeMessageArea"/> -+ <seeElement selector="{{StorefrontProductCartGiftOptionSection.update}}" stepKey="seeUpdateButton"/> -+ <seeElement selector="{{StorefrontProductCartGiftOptionSection.cancel}}" stepKey="seeCancelButton"/> -+ </actionGroup> -+</actionGroups> -diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontAssertProductImagesOnProductPageActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontAssertProductImagesOnProductPageActionGroup.xml -new file mode 100644 -index 00000000000..1bb7c179dfc ---- /dev/null -+++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontAssertProductImagesOnProductPageActionGroup.xml -@@ -0,0 +1,22 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+/** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+--> -+ -+<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> -+ <actionGroup name="StorefrontAssertProductImagesOnProductPageActionGroup"> -+ <arguments> -+ <argument name="productImage" type="string" defaultValue="Magento_Catalog/images/product/placeholder/image.jpg" /> -+ </arguments> -+ <waitForElementNotVisible selector="{{StorefrontProductMediaSection.gallerySpinner}}" stepKey="waitGallerySpinnerDisappear" /> -+ <seeElement selector="{{StorefrontProductMediaSection.gallery}}" stepKey="seeProductGallery" /> -+ <seeElement selector="{{StorefrontProductMediaSection.productImage(productImage)}}" stepKey="seeProductImage" /> -+ <click selector="{{StorefrontProductMediaSection.productImage(productImage)}}" stepKey="openFullscreenImage" /> -+ <waitForPageLoad stepKey="waitForGalleryLoaded" /> -+ <seeElement selector="{{StorefrontProductMediaSection.productImageFullscreen(productImage)}}" stepKey="seeFullscreenProductImage" /> -+ <click selector="{{StorefrontProductMediaSection.closeFullscreenImage}}" stepKey="closeFullScreenImage" /> -+ <waitForPageLoad stepKey="waitForGalleryDisappear" /> -+ </actionGroup> -+</actionGroups> -diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontAssertProductInWidgetActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontAssertProductInWidgetActionGroup.xml -new file mode 100644 -index 00000000000..c25b73bab21 ---- /dev/null -+++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontAssertProductInWidgetActionGroup.xml -@@ -0,0 +1,37 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> -+ <!-- Check the product in recently viewed widget --> -+ <actionGroup name="StorefrontAssertProductInRecentlyViewedWidgetActionGroup"> -+ <arguments> -+ <argument name="product"/> -+ </arguments> -+ <waitForElementVisible selector="{{StorefrontWidgetsSection.widgetRecentlyViewedProductsGrid}}" stepKey="waitWidgetRecentlyViewedProductsGrid"/> -+ <see selector="{{StorefrontWidgetsSection.widgetRecentlyViewedProductsGrid}}" userInput="{{product.name}}" stepKey="seeProductInRecentlyViewedWidget"/> -+ </actionGroup> -+ -+ <!-- Check the product in recently compared widget --> -+ <actionGroup name="StorefrontAssertProductInRecentlyComparedWidgetActionGroup"> -+ <arguments> -+ <argument name="product"/> -+ </arguments> -+ <waitForElementVisible selector="{{StorefrontWidgetsSection.widgetRecentlyComparedProductsGrid}}" stepKey="waitWidgetRecentlyComparedProductsGrid"/> -+ <see selector="{{StorefrontWidgetsSection.widgetRecentlyComparedProductsGrid}}" userInput="{{product.name}}" stepKey="seeProductInRecentlyComparedWidget"/> -+ </actionGroup> -+ -+ <!-- Check the product in recently ordered widget --> -+ <actionGroup name="StorefrontAssertProductInRecentlyOrderedWidgetActionGroup"> -+ <arguments> -+ <argument name="product"/> -+ </arguments> -+ <waitForElementVisible selector="{{StorefrontWidgetsSection.widgetRecentlyOrderedProductsGrid}}" stepKey="waitWidgetRecentlyOrderedProductsGrid"/> -+ <see selector="{{StorefrontWidgetsSection.widgetRecentlyOrderedProductsGrid}}" userInput="{{product.name}}" stepKey="seeProductInRecentlyOrderedWidget"/> -+ </actionGroup> -+</actionGroups> -\ No newline at end of file -diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontAssertProductNameOnProductMainPageActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontAssertProductNameOnProductMainPageActionGroup.xml -new file mode 100644 -index 00000000000..43a34448c8a ---- /dev/null -+++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontAssertProductNameOnProductMainPageActionGroup.xml -@@ -0,0 +1,16 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+/** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+--> -+ -+<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> -+ <actionGroup name="StorefrontAssertProductNameOnProductMainPageActionGroup"> -+ <arguments> -+ <argument name="productName" type="string"/> -+ </arguments> -+ <waitForPageLoad stepKey="waitForTheProductPageToLoad"/> -+ <see selector="{{StorefrontCategoryMainSection.productName}}" userInput="{{productName}}" stepKey="seeProductName"/> -+ </actionGroup> -+</actionGroups> -diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontAssertProductNameOnProductPageActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontAssertProductNameOnProductPageActionGroup.xml -new file mode 100644 -index 00000000000..6cb156723b2 ---- /dev/null -+++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontAssertProductNameOnProductPageActionGroup.xml -@@ -0,0 +1,15 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+/** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+--> -+ -+<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> -+ <actionGroup name="StorefrontAssertProductNameOnProductPageActionGroup"> -+ <arguments> -+ <argument name="productName" type="string"/> -+ </arguments> -+ <see selector="{{StorefrontProductInfoMainSection.productName}}" userInput="{{productName}}" stepKey="seeProductName" /> -+ </actionGroup> -+</actionGroups> -diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontAssertProductPriceOnProductPageActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontAssertProductPriceOnProductPageActionGroup.xml -new file mode 100644 -index 00000000000..3c62ef89e58 ---- /dev/null -+++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontAssertProductPriceOnProductPageActionGroup.xml -@@ -0,0 +1,15 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+/** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+--> -+ -+<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> -+ <actionGroup name="StorefrontAssertProductPriceOnProductPageActionGroup"> -+ <arguments> -+ <argument name="productPrice" type="string"/> -+ </arguments> -+ <see selector="{{StorefrontProductInfoMainSection.price}}" userInput="{{productPrice}}" stepKey="seeProductPrice" /> -+ </actionGroup> -+</actionGroups> -diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontAssertProductSkuOnProductPageActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontAssertProductSkuOnProductPageActionGroup.xml -new file mode 100644 -index 00000000000..85d3927a6d6 ---- /dev/null -+++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontAssertProductSkuOnProductPageActionGroup.xml -@@ -0,0 +1,15 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+/** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+--> -+ -+<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> -+ <actionGroup name="StorefrontAssertProductSkuOnProductPageActionGroup"> -+ <arguments> -+ <argument name="productSku" type="string"/> -+ </arguments> -+ <see selector="{{StorefrontProductInfoMainSection.productSku}}" userInput="{{productSku}}" stepKey="seeProductSku" /> -+ </actionGroup> -+</actionGroups> -diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontAssertProductSpecialPriceOnProductPageActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontAssertProductSpecialPriceOnProductPageActionGroup.xml -new file mode 100644 -index 00000000000..9fefa71f102 ---- /dev/null -+++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontAssertProductSpecialPriceOnProductPageActionGroup.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="StorefrontAssertProductSpecialPriceOnProductPageActionGroup"> -+ <arguments> -+ <argument name="product" type="entity"/> -+ <argument name="specialPrice" type="string"/> -+ </arguments> -+ <amOnPage url="{{StorefrontProductPage.url(product.name)}}" stepKey="onFirstProductPage"/> -+ <waitForPageLoad stepKey="waitForFirstProductPage"/> -+ <waitForElementVisible selector="{{StorefrontProductInfoMainSection.specialPriceValue}}" stepKey="waitForProductSpecialPrice"/> -+ <grabTextFrom selector="{{StorefrontProductInfoMainSection.specialPriceValue}}" stepKey="grabProductSpecialPrice"/> -+ <assertEquals actual="$grabProductSpecialPrice" expectedType="string" expected="{{specialPrice}}" stepKey="assertProductPriceValuesAreEqual"/> -+ </actionGroup> -+</actionGroups> -diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontCategoryActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontCategoryActionGroup.xml -index 4376e78242f..7e79182616f 100644 ---- a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontCategoryActionGroup.xml -+++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontCategoryActionGroup.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"> - <!-- Go to storefront category product page by given parameters --> - <actionGroup name="GoToStorefrontCategoryPageByParameters"> - <arguments> -@@ -15,12 +15,28 @@ - <argument name="mode" type="string"/> - <argument name="numOfProductsPerPage" type="string"/> - <argument name="sortBy" type="string" defaultValue="position"/> -+ <argument name="sort" type="string" defaultValue="asc"/> - </arguments> - <!-- Go to storefront category page --> -- <amOnPage url="{{StorefrontCategoryPage.url(category)}}?product_list_limit={{numOfProductsPerPage}}&product_list_mode={{mode}}&product_list_order={{sortBy}}" stepKey="onCategoryPage"/> -+ <amOnPage url="{{StorefrontCategoryPage.url(category)}}?product_list_limit={{numOfProductsPerPage}}&product_list_mode={{mode}}&product_list_order={{sortBy}}&product_list_dir={{sort}}" stepKey="onCategoryPage"/> - <waitForPageLoad stepKey="waitForPageLoad"/> - </actionGroup> - -+ <actionGroup name="VerifyCategoryPageParameters"> -+ <arguments> -+ <argument name="category"/> -+ <argument name="mode" type="string"/> -+ <argument name="numOfProductsPerPage" type="string"/> -+ <argument name="sortBy" type="string" defaultValue="position"/> -+ </arguments> -+ <seeInCurrentUrl url="/{{category.custom_attributes[url_key]}}.html" stepKey="checkUrl"/> -+ <seeInTitle userInput="{{category.name}}" stepKey="assertCategoryNameInTitle"/> -+ <see userInput="{{category.name}}" selector="{{StorefrontCategoryMainSection.CategoryTitle}}" stepKey="assertCategoryName"/> -+ <see userInput="{{mode}}" selector="{{StorefrontCategoryMainSection.modeGridIsActive}}" stepKey="assertViewMode"/> -+ <see userInput="{{numOfProductsPerPage}}" selector="{{StorefrontCategoryMainSection.perPage}}" stepKey="assertNumberOfProductsPerPage"/> -+ <see userInput="{{sortBy}}" selector="{{StorefrontCategoryMainSection.sortedBy}}" stepKey="assertSortedBy"/> -+ </actionGroup> -+ - <!-- Check the category page --> - <actionGroup name="StorefrontCheckCategoryActionGroup"> - <arguments> -@@ -46,8 +62,30 @@ - <seeElement selector="{{StorefrontCategoryProductSection.ProductAddToCartByName(product.name)}}" stepKey="AssertAddToCart" /> - </actionGroup> - -+ <actionGroup name="StorefrontCheckAddToCartButtonAbsence"> -+ <arguments> -+ <argument name="product" defaultValue="_defaultProduct"/> -+ </arguments> -+ <moveMouseOver selector="{{StorefrontCategoryProductSection.ProductInfoByName(product.name)}}" stepKey="moveMouseOverProduct" /> -+ <dontSeeElement selector="{{StorefrontCategoryProductSection.ProductAddToCartByName(product.name)}}" stepKey="checkAddToCartButtonAbsence"/> -+ </actionGroup> - <actionGroup name="StorefrontSwitchCategoryViewToListMode"> - <click selector="{{StorefrontCategoryMainSection.modeListButton}}" stepKey="switchCategoryViewToListMode"/> - <waitForElement selector="{{StorefrontCategoryMainSection.CategoryTitle}}" time="30" stepKey="waitForCategoryReload"/> - </actionGroup> -+ -+ <actionGroup name="GoToSubCategoryPage"> -+ <arguments> -+ <argument name="parentCategory"/> -+ <argument name="subCategory"/> -+ <argument name="urlPath" type="string"/> -+ </arguments> -+ <moveMouseOver selector="{{StorefrontHeaderSection.NavigationCategoryByName(parentCategory.name)}}" stepKey="moveMouseOnMainCategory"/> -+ <waitForElementVisible selector="{{StorefrontHeaderSection.NavigationCategoryByName(subCategory.name)}}" stepKey="waitForSubCategoryVisible"/> -+ <click selector="{{StorefrontHeaderSection.NavigationCategoryByName(subCategory.name)}}" stepKey="goToCategory"/> -+ <waitForPageLoad stepKey="waitForPageLoad"/> -+ <seeInCurrentUrl url="{{urlPath}}.html" stepKey="checkUrl"/> -+ <seeInTitle userInput="{{subCategory.name}}" stepKey="assertCategoryNameInTitle"/> -+ <see userInput="{{subCategory.name}}" selector="{{StorefrontCategoryMainSection.CategoryTitle}}" stepKey="assertCategoryName"/> -+ </actionGroup> - </actionGroups> -diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontCheckProductPriceInCategoryActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontCheckProductPriceInCategoryActionGroup.xml -new file mode 100644 -index 00000000000..5c975998ab9 ---- /dev/null -+++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontCheckProductPriceInCategoryActionGroup.xml -@@ -0,0 +1,16 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> -+ <!-- You must already be on the category page --> -+ <actionGroup name="StorefrontCheckProductPriceInCategoryActionGroup" extends="StorefrontCheckCategorySimpleProduct"> -+ <remove keyForRemoval="AssertProductPrice"/> -+ <see userInput="{{product.price}}" selector="{{StorefrontCategoryProductSection.ProductPriceByName(product.name)}}" stepKey="AssertProductPrice"/> -+ </actionGroup> -+</actionGroups> -diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontClickAddToCartOnProductPageActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontClickAddToCartOnProductPageActionGroup.xml -new file mode 100644 -index 00000000000..1dcbc738c76 ---- /dev/null -+++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontClickAddToCartOnProductPageActionGroup.xml -@@ -0,0 +1,15 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> -+ <actionGroup name="StorefrontClickAddToCartOnProductPageActionGroup"> -+ <click selector="{{StorefrontProductActionSection.addToCart}}" stepKey="addToCart" /> -+ <waitForPageLoad stepKey="waitForAddToCart"/> -+ <waitForElementVisible selector="{{StorefrontMessagesSection.success}}" stepKey="waitForSuccessMessage" /> -+ </actionGroup> -+</actionGroups> -diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontCompareActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontCompareActionGroup.xml -index 7af1cacfb3d..04e15da9177 100644 ---- a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontCompareActionGroup.xml -+++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontCompareActionGroup.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"> - <!-- Add Product to Compare from the category page and check message --> - <actionGroup name="StorefrontAddCategoryProductToCompareActionGroup"> - <arguments> -diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontGoToCategoryPageActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontGoToCategoryPageActionGroup.xml -new file mode 100644 -index 00000000000..e8be0db38fe ---- /dev/null -+++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontGoToCategoryPageActionGroup.xml -@@ -0,0 +1,27 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> -+ <actionGroup name="StorefrontGoToCategoryPageActionGroup"> -+ <arguments> -+ <argument name="categoryName" type="string"/> -+ </arguments> -+ <amOnPage url="{{StorefrontHomePage.url}}" stepKey="onFrontend"/> -+ <waitForPageLoad stepKey="waitForStorefrontPageLoad"/> -+ <click selector="{{StorefrontHeaderSection.NavigationCategoryByName(categoryName)}}" stepKey="toCategory"/> -+ <waitForPageLoad stepKey="waitForCategoryPage"/> -+ </actionGroup> -+ <actionGroup name="StorefrontGoToSubCategoryPageActionGroup" extends="StorefrontGoToCategoryPageActionGroup"> -+ <arguments> -+ <argument name="subCategoryName" type="string"/> -+ </arguments> -+ <moveMouseOver selector="{{StorefrontHeaderSection.NavigationCategoryByName(categoryName)}}" stepKey="toCategory"/> -+ <click selector="{{StorefrontHeaderSection.NavigationCategoryByName(subCategoryName)}}" stepKey="openSubCategory" after="toCategory"/> -+ </actionGroup> -+</actionGroups> -\ No newline at end of file -diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontOpenHomePageActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontOpenHomePageActionGroup.xml -new file mode 100644 -index 00000000000..692d1f4266b ---- /dev/null -+++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontOpenHomePageActionGroup.xml -@@ -0,0 +1,13 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+/** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+--> -+ -+<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> -+ <actionGroup name="StorefrontOpenHomePageActionGroup"> -+ <amOnPage url="{{StorefrontHomePage.url}}" stepKey="goToStorefrontPage"/> -+ <waitForPageLoad stepKey="waitForStorefrontPageLoad"/> -+ </actionGroup> -+</actionGroups> -diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontOpenProductPageActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontOpenProductPageActionGroup.xml -new file mode 100644 -index 00000000000..f5fabae5fc4 ---- /dev/null -+++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontOpenProductPageActionGroup.xml -@@ -0,0 +1,16 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+/** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+--> -+ -+<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> -+ <actionGroup name="StorefrontOpenProductPageActionGroup"> -+ <arguments> -+ <argument name="productUrl" type="string"/> -+ </arguments> -+ <amOnPage url="{{StorefrontProductPage.url(productUrl)}}" stepKey="openProductPage"/> -+ <waitForPageLoad stepKey="waitForProductPageLoaded"/> -+ </actionGroup> -+</actionGroups> -diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontProductActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontProductActionGroup.xml -index eb672cd162e..5f0d03597da 100644 ---- a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontProductActionGroup.xml -+++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontProductActionGroup.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"> - <!-- Check the simple product on the product page --> - <actionGroup name="StorefrontCheckSimpleProduct"> - <arguments> -diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontProductPageActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontProductPageActionGroup.xml -index d46b8950445..e6392118f79 100644 ---- a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontProductPageActionGroup.xml -+++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontProductPageActionGroup.xml -@@ -6,19 +6,29 @@ - */ - --> - <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"> - <!--Click Add to Cart button in storefront product page--> - <actionGroup name="addToCartFromStorefrontProductPage"> - <arguments> - <argument name="productName"/> - </arguments> - <click selector="{{StorefrontProductActionSection.addToCart}}" stepKey="addToCart"/> -+ <waitForPageLoad stepKey="waitForAddToCart"/> - <waitForElementNotVisible selector="{{StorefrontProductActionSection.addToCartButtonTitleIsAdding}}" stepKey="waitForElementNotVisibleAddToCartButtonTitleIsAdding"/> - <waitForElementNotVisible selector="{{StorefrontProductActionSection.addToCartButtonTitleIsAdded}}" stepKey="waitForElementNotVisibleAddToCartButtonTitleIsAdded"/> - <waitForPageLoad stepKey="waitForPageLoad"/> -+ <waitForElementVisible selector="{{StorefrontMessagesSection.success}}" stepKey="waitForSuccessMessage"/> - <see selector="{{StorefrontMessagesSection.success}}" userInput="You added {{productName}} to your shopping cart." stepKey="seeAddToCartSuccessMessage"/> - </actionGroup> - -+ <actionGroup name="AddProductWithQtyToCartFromStorefrontProductPage" extends="addToCartFromStorefrontProductPage"> -+ <arguments> -+ <argument name="productName" type="string"/> -+ <argument name="productQty" type="string"/> -+ </arguments> -+ <fillField selector="{{StorefrontProductActionSection.quantity}}" userInput="{{productQty}}" stepKey="fillProductQuantity" before="addToCart"/> -+ </actionGroup> -+ - <!--Verify text length validation hint with multiple inputs--> - <actionGroup name="testDynamicValidationHint"> - <arguments> -diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontSelectCustomOptionDropDownAndAssertPricesActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontSelectCustomOptionDropDownAndAssertPricesActionGroup.xml -new file mode 100644 -index 00000000000..39793cb8f68 ---- /dev/null -+++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontSelectCustomOptionDropDownAndAssertPricesActionGroup.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="StorefrontSelectCustomOptionDropDownAndAssertPricesActionGroup" extends="AssertStorefrontProductPricesActionGroup"> -+ <arguments> -+ <argument name="customOption" type="string"/> -+ <argument name="productPrice" type="string"/> -+ <argument name="productFinalPrice" type="string"/> -+ </arguments> -+ <selectOption selector="{{StorefrontProductPageSection.customOptionDropDown}}" userInput="{{customOption}}" stepKey="selectCustomOption" before="productPriceAmount"/> -+ </actionGroup> -+</actionGroups> -diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontSelectCustomOptionRadioAndAssertPricesActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontSelectCustomOptionRadioAndAssertPricesActionGroup.xml -new file mode 100644 -index 00000000000..6f7bdc46640 ---- /dev/null -+++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontSelectCustomOptionRadioAndAssertPricesActionGroup.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="StorefrontSelectCustomOptionRadioAndAssertPricesActionGroup" extends="AssertStorefrontProductPricesActionGroup"> -+ <arguments> -+ <argument name="customOption" type="entity"/> -+ <argument name="customOptionValue" type="entity"/> -+ <argument name="productPrice" type="string"/> -+ <argument name="productFinalPrice" type="string"/> -+ </arguments> -+ <click selector="{{StorefrontProductInfoMainSection.productAttributeOptionsRadioButtons(customOption.title, customOptionValue.price)}}" stepKey="clickRadioButtonsProductOption" before="productPriceAmount"/> -+ </actionGroup> -+</actionGroups> -diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/VerifyProductTypeOrderActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/VerifyProductTypeOrderActionGroup.xml -new file mode 100644 -index 00000000000..aec21f3bc48 ---- /dev/null -+++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/VerifyProductTypeOrderActionGroup.xml -@@ -0,0 +1,17 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> -+ <actionGroup name="VerifyProductTypeOrder"> -+ <click selector="{{AdminProductGridActionSection.addProductToggle}}" stepKey="clickAddProductDropdown"/> -+ <waitForPageLoad stepKey="waitForLoad"/> -+ <seeElement stepKey="seeSimpleInOrder" selector="{{AdminProductDropdownOrderSection.simpleProduct}}"/> -+ <seeElement stepKey="seeVirtualInOrder" selector="{{AdminProductDropdownOrderSection.virtualProduct}}"/> -+ </actionGroup> -+</actionGroups> -diff --git a/app/code/Magento/Catalog/Test/Mftf/Data/AdminMenuData.xml b/app/code/Magento/Catalog/Test/Mftf/Data/AdminMenuData.xml -new file mode 100644 -index 00000000000..24e1fe9cf5e ---- /dev/null -+++ b/app/code/Magento/Catalog/Test/Mftf/Data/AdminMenuData.xml -@@ -0,0 +1,36 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<entities xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataProfileSchema.xsd"> -+ <entity name="AdminMenuCatalog"> -+ <data key="pageTitle">Catalog</data> -+ <data key="title">Catalog</data> -+ <data key="dataUiId">magento-catalog-catalog</data> -+ </entity> -+ <entity name="AdminMenuCatalogCategories"> -+ <data key="pageTitle">Default Category (ID: 2)</data> -+ <data key="title">Categories</data> -+ <data key="dataUiId">magento-catalog-catalog-categories</data> -+ </entity> -+ <entity name="AdminMenuCatalogProducts"> -+ <data key="pageTitle">Products</data> -+ <data key="title">Products</data> -+ <data key="dataUiId">magento-catalog-catalog-products</data> -+ </entity> -+ <entity name="AdminMenuStoresAttributesAttributeSet"> -+ <data key="pageTitle">Attribute Sets</data> -+ <data key="title">Attribute Set</data> -+ <data key="dataUiId">magento-catalog-catalog-attributes-sets</data> -+ </entity> -+ <entity name="AdminMenuStoresAttributesProduct"> -+ <data key="pageTitle">Product Attributes</data> -+ <data key="title">Product</data> -+ <data key="dataUiId">magento-catalog-catalog-attributes-attributes</data> -+ </entity> -+</entities> -diff --git a/app/code/Magento/Catalog/Test/Mftf/Data/AttributeSetData.xml b/app/code/Magento/Catalog/Test/Mftf/Data/AttributeSetData.xml -new file mode 100644 -index 00000000000..6e1b25fb9cd ---- /dev/null -+++ b/app/code/Magento/Catalog/Test/Mftf/Data/AttributeSetData.xml -@@ -0,0 +1,16 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<entities xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataProfileSchema.xsd"> -+ <entity name="defaultAttributeSet"> -+ <data key="attribute_set_id">4</data> -+ <data key="attribute_set_name">Default</data> -+ <data key="sort_order">0</data> -+ </entity> -+</entities> -diff --git a/app/code/Magento/Catalog/Test/Mftf/Data/CatalogAttributeGroupData.xml b/app/code/Magento/Catalog/Test/Mftf/Data/CatalogAttributeGroupData.xml -new file mode 100644 -index 00000000000..4413cbcf86a ---- /dev/null -+++ b/app/code/Magento/Catalog/Test/Mftf/Data/CatalogAttributeGroupData.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="customGroup"> -+ <data key="name">Custom Group</data> -+ </entity> -+ <entity name="emptyGroup"> -+ <data key="name">Empty Group</data> -+ </entity> -+</entities> -diff --git a/app/code/Magento/Catalog/Test/Mftf/Data/CatalogAttributeSetData.xml b/app/code/Magento/Catalog/Test/Mftf/Data/CatalogAttributeSetData.xml -new file mode 100644 -index 00000000000..d78c03a51dd ---- /dev/null -+++ b/app/code/Magento/Catalog/Test/Mftf/Data/CatalogAttributeSetData.xml -@@ -0,0 +1,16 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<entities xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataProfileSchema.xsd"> -+ <entity name="CatalogAttributeSet" type="CatalogAttributeSet"> -+ <data key="attribute_set_name" unique="suffix">test_set_</data> -+ <data key="attributeGroupId">7</data> -+ <data key="skeletonId">4</data> -+ </entity> -+</entities> -\ No newline at end of file -diff --git a/app/code/Magento/Catalog/Test/Mftf/Data/CatalogInventoryConfigData.xml b/app/code/Magento/Catalog/Test/Mftf/Data/CatalogInventoryConfigData.xml -new file mode 100644 -index 00000000000..c9b67e0db43 ---- /dev/null -+++ b/app/code/Magento/Catalog/Test/Mftf/Data/CatalogInventoryConfigData.xml -@@ -0,0 +1,33 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<entities xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataProfileSchema.xsd"> -+ <entity name="CatalogInventoryOptionsShowOutOfStockEnable"> -+ <data key="path">cataloginventory/options/show_out_of_stock</data> -+ <data key="label">Yes</data> -+ <data key="value">1</data> -+ </entity> -+ <entity name="CatalogInventoryOptionsShowOutOfStockDisable"> -+ <!-- Magento default value --> -+ <data key="path">cataloginventory/options/show_out_of_stock</data> -+ <data key="label">No</data> -+ <data key="value">0</data> -+ </entity> -+ <entity name="CatalogInventoryItemOptionsBackordersEnable"> -+ <data key="path">cataloginventory/item_options/backorders</data> -+ <data key="label">Yes</data> -+ <data key="value">1</data> -+ </entity> -+ <entity name="CatalogInventoryItemOptionsBackordersDisable"> -+ <!-- Magento default value --> -+ <data key="path">cataloginventory/item_options/backorders</data> -+ <data key="label">No</data> -+ <data key="value">0</data> -+ </entity> -+</entities> -diff --git a/app/code/Magento/Catalog/Test/Mftf/Data/CatalogPriceData.xml b/app/code/Magento/Catalog/Test/Mftf/Data/CatalogPriceData.xml -new file mode 100644 -index 00000000000..0f7f4da1b68 ---- /dev/null -+++ b/app/code/Magento/Catalog/Test/Mftf/Data/CatalogPriceData.xml -@@ -0,0 +1,32 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<entities xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataProfileSchema.xsd"> -+ <entity name="CatalogPriceScopeWebsite" type="catalog_price_config_state"> -+ <requiredEntity type="scope">scopeWebsite</requiredEntity> -+ <requiredEntity type="default_product_price">defaultProductPrice</requiredEntity> -+ </entity> -+ <entity name="scopeWebsite" type="scope"> -+ <data key="value">1</data> -+ </entity> -+ <entity name="defaultProductPrice" type="default_product_price"> -+ <data key="value">0</data> -+ </entity> -+ -+ <entity name="DefaultConfigCatalogPrice" type="catalog_price_config_state"> -+ <requiredEntity type="scope">scopeGlobal</requiredEntity> -+ <requiredEntity type="default_product_price">defaultProductPrice</requiredEntity> -+ </entity> -+ <entity name="scopeGlobal" type="scope"> -+ <data key="value">0</data> -+ </entity> -+ <entity name="defaultProductPrice" type="default_product_price"> -+ <data key="value"/> -+ </entity> -+</entities> -diff --git a/app/code/Magento/Catalog/Test/Mftf/Data/CatalogRecentlyProductsConfigData.xml b/app/code/Magento/Catalog/Test/Mftf/Data/CatalogRecentlyProductsConfigData.xml -new file mode 100644 -index 00000000000..d1e469deaeb ---- /dev/null -+++ b/app/code/Magento/Catalog/Test/Mftf/Data/CatalogRecentlyProductsConfigData.xml -@@ -0,0 +1,25 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<entities xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataProfileSchema.xsd"> -+ <entity name="EnableSynchronizeWidgetProductsWithBackendStorage" type="catalog_recently_products"> -+ <requiredEntity type="synchronize_with_backend">EnableCatalogRecentlyProductsSynchronize</requiredEntity> -+ </entity> -+ -+ <entity name="EnableCatalogRecentlyProductsSynchronize" type="synchronize_with_backend"> -+ <data key="value">1</data> -+ </entity> -+ -+ <entity name="DisableSynchronizeWidgetProductsWithBackendStorage" type="catalog_recently_products"> -+ <requiredEntity type="synchronize_with_backend">DefaultCatalogRecentlyProductsSynchronize</requiredEntity> -+ </entity> -+ -+ <entity name="DefaultCatalogRecentlyProductsSynchronize" type="synchronize_with_backend"> -+ <data key="value">0</data> -+ </entity> -+</entities> -diff --git a/app/code/Magento/Catalog/Test/Mftf/Data/CatalogSpecialPriceData.xml b/app/code/Magento/Catalog/Test/Mftf/Data/CatalogSpecialPriceData.xml -new file mode 100644 -index 00000000000..31783526932 ---- /dev/null -+++ b/app/code/Magento/Catalog/Test/Mftf/Data/CatalogSpecialPriceData.xml -@@ -0,0 +1,20 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<entities xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataProfileSchema.xsd"> -+ <entity name="specialProductPrice" type="catalogSpecialPrice"> -+ <data key="price">99.99</data> -+ <data key="store_id">0</data> -+ <var key="sku" entityType="product2" entityKey="sku" /> -+ </entity> -+ <entity name="specialProductPrice2" type="catalogSpecialPrice"> -+ <data key="price">55.55</data> -+ <data key="store_id">0</data> -+ <var key="sku" entityType="product" entityKey="sku" /> -+ </entity> -+</entities> -diff --git a/app/code/Magento/Catalog/Test/Mftf/Data/CatalogStorefrontConfigData.xml b/app/code/Magento/Catalog/Test/Mftf/Data/CatalogStorefrontConfigData.xml -new file mode 100644 -index 00000000000..abf01f00dbb ---- /dev/null -+++ b/app/code/Magento/Catalog/Test/Mftf/Data/CatalogStorefrontConfigData.xml -@@ -0,0 +1,63 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<entities xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataProfileSchema.xsd"> -+ <entity name="RememberPaginationCatalogStorefrontConfig" type="catalog_storefront_config"> -+ <requiredEntity type="grid_per_page_values">GridPerPageValues</requiredEntity> -+ <requiredEntity type="remember_pagination">RememberCategoryPagination</requiredEntity> -+ </entity> -+ -+ <entity name="GridPerPageValues" type="grid_per_page_values"> -+ <data key="value">9,12,20,24</data> -+ </entity> -+ -+ <entity name="RememberCategoryPagination" type="remember_pagination"> -+ <data key="value">1</data> -+ </entity> -+ -+ <entity name="DefaultCatalogStorefrontConfiguration" type="default_catalog_storefront_config"> -+ <requiredEntity type="catalogStorefrontFlagZero">DefaultCatalogStorefrontFlagZero</requiredEntity> -+ <data key="list_allow_all">DefaultListAllowAll</data> -+ <data key="flat_catalog_product">DefaultFlatCatalogProduct</data> -+ </entity> -+ -+ <entity name="DefaultCatalogStorefrontFlagZero" type="catalogStorefrontFlagZero"> -+ <data key="value">0</data> -+ </entity> -+ -+ <entity name="DefaultListAllowAll" type="list_allow_all"> -+ <data key="value">0</data> -+ </entity> -+ -+ <entity name="DefaultFlatCatalogProduct" type="flat_catalog_product"> -+ <data key="value">0</data> -+ </entity> -+ -+ <entity name="UseFlatCatalogCategoryAndProduct" type="catalog_storefront_config"> -+ <requiredEntity type="flat_catalog_product">UseFlatCatalogProduct</requiredEntity> -+ <requiredEntity type="flat_catalog_category">UseFlatCatalogCategory</requiredEntity> -+ </entity> -+ -+ <entity name="UseFlatCatalogProduct" type="flat_catalog_product"> -+ <data key="value">1</data> -+ </entity> -+ -+ <entity name="UseFlatCatalogCategory" type="flat_catalog_category"> -+ <data key="value">1</data> -+ </entity> -+ -+ <entity name="DefaultFlatCatalogCategoryAndProduct" type="catalog_storefront_config"> -+ <requiredEntity type="flat_catalog_product">DefaultFlatCatalogProduct</requiredEntity> -+ <requiredEntity type="flat_catalog_category">DefaultFlatCatalogCategory</requiredEntity> -+ </entity> -+ -+ <entity name="DefaultFlatCatalogCategory" type="flat_catalog_category"> -+ <data key="value">0</data> -+ </entity> -+</entities> -diff --git a/app/code/Magento/Catalog/Test/Mftf/Data/CategoryData.xml b/app/code/Magento/Catalog/Test/Mftf/Data/CategoryData.xml -index 42351741d9f..13951a0d197 100644 ---- a/app/code/Magento/Catalog/Test/Mftf/Data/CategoryData.xml -+++ b/app/code/Magento/Catalog/Test/Mftf/Data/CategoryData.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="_defaultCategory" type="category"> - <data key="name" unique="suffix">simpleCategory</data> - <data key="name_lwr" unique="suffix">simplecategory</data> -@@ -42,4 +42,79 @@ - <data key="is_active">true</data> - <var key="parent_id" entityType="category" entityKey="id" /> - </entity> -+ <entity name="FirstLevelSubCat" type="category"> -+ <data key="name" unique="suffix">FirstLevelSubCategory</data> -+ <data key="name_lwr" unique="suffix">firstlevelsubcategory</data> -+ </entity> -+ <entity name="SecondLevelSubCat" type="category"> -+ <data key="name" unique="suffix">SecondLevelSubCategory</data> -+ <data key="name_lwr" unique="suffix">secondlevelsubcategory</data> -+ </entity> -+ <entity name="ThirdLevelSubCat" type="category"> -+ <data key="name" unique="suffix">ThirdLevelSubCategory</data> -+ <data key="name_lwr" unique="suffix">subcategory</data> -+ </entity> -+ <entity name="FourthLevelSubCat" type="category"> -+ <data key="name" unique="suffix">FourthLevelSubCategory</data> -+ <data key="name_lwr" unique="suffix">subcategory</data> -+ </entity> -+ <entity name="FifthLevelCat" type="category"> -+ <data key="name" unique="suffix">FifthLevelCategory</data> -+ <data key="name_lwr" unique="suffix">category</data> -+ </entity> -+ <entity name="SimpleRootSubCategory" type="category"> -+ <data key="name" unique="suffix">SimpleRootSubCategory</data> -+ <data key="name_lwr" unique="suffix">simplerootsubcategory</data> -+ <data key="is_active">true</data> -+ <data key="include_in_menu">true</data> -+ <data key="url_key" unique="suffix">simplerootsubcategory</data> -+ <var key="parent_id" entityType="category" entityKey="id" /> -+ </entity> -+ <entity name="SubCategory" type="category"> -+ <data key="name" unique="suffix">SubCategory</data> -+ <data key="name_lwr" unique="suffix">subcategory</data> -+ <data key="is_active">true</data> -+ <data key="include_in_menu">true</data> -+ </entity> -+ <entity name="Two_nested_categories" type="category"> -+ <data key="name" unique="suffix">SecondLevel</data> -+ <data key="url_key" unique="suffix">secondlevel</data> -+ <data key="name_lwr" unique="suffix">secondlevel</data> -+ <data key="is_active">true</data> -+ <data key="include_in_menu">true</data> -+ <var key="parent_id" entityType="category" entityKey="id" /> -+ </entity> -+ <entity name="Three_nested_categories" type="category"> -+ <data key="name" unique="suffix">ThirdLevel</data> -+ <data key="url_key" unique="suffix">thirdlevel</data> -+ <data key="name_lwr" unique="suffix">thirdlevel</data> -+ <data key="is_active">true</data> -+ <data key="include_in_menu">true</data> -+ <var key="parent_id" entityType="category" entityKey="id" /> -+ </entity> -+ <entity name="CatNotIncludeInMenu" type="category"> -+ <data key="name" unique="suffix">NotInclMenu</data> -+ <data key="name_lwr" unique="suffix">notinclemenu</data> -+ <data key="is_active">true</data> -+ <data key="include_in_menu">false</data> -+ </entity> -+ <entity name="CatNotActive" type="category"> -+ <data key="name" unique="suffix">NotActive</data> -+ <data key="name_lwr" unique="suffix">notactive</data> -+ <data key="is_active">false</data> -+ <data key="include_in_menu">true</data> -+ </entity> -+ <entity name="CatInactiveNotInMenu" type="category"> -+ <data key="name" unique="suffix">InactiveNotInMenu</data> -+ <data key="name_lwr" unique="suffix">inactivenotinmenu</data> -+ <data key="is_active">false</data> -+ <data key="include_in_menu">false</data> -+ </entity> -+ <!-- Category from file "prepared-for-sample-data.csv"--> -+ <entity name="Gear" type="category"> -+ <data key="name">Gear</data> -+ <data key="name_lwr">gear</data> -+ <data key="is_active">true</data> -+ <data key="include_in_menu">true</data> -+ </entity> - </entities> -diff --git a/app/code/Magento/Catalog/Test/Mftf/Data/ConstData.xml b/app/code/Magento/Catalog/Test/Mftf/Data/ConstData.xml -index 8ae57f92399..d09880f14af 100644 ---- a/app/code/Magento/Catalog/Test/Mftf/Data/ConstData.xml -+++ b/app/code/Magento/Catalog/Test/Mftf/Data/ConstData.xml -@@ -7,10 +7,14 @@ - --> - - <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"> - <!-- @TODO: Get rid off this workaround and its usages after MQE-498 is implemented --> - <entity name="CONST" type="CONST"> - <data key="one">1</data> - <data key="two">2</data> - </entity> -+ <entity name="prodNameWithSpecChars"> -+ <data key="trademark">"Pursuit Lumaflex™ Tone Band"</data> -+ <data key="skumark">"x™"</data> -+ </entity> - </entities> -diff --git a/app/code/Magento/Catalog/Test/Mftf/Data/CustomAttributeData.xml b/app/code/Magento/Catalog/Test/Mftf/Data/CustomAttributeData.xml -index e93138fecfd..389c41abf0b 100644 ---- a/app/code/Magento/Catalog/Test/Mftf/Data/CustomAttributeData.xml -+++ b/app/code/Magento/Catalog/Test/Mftf/Data/CustomAttributeData.xml -@@ -6,7 +6,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="CustomAttributeCategoryUrlKey" type="custom_attribute"> - <data key="attribute_code">url_key</data> - <data key="value" unique="suffix">category</data> -diff --git a/app/code/Magento/Catalog/Test/Mftf/Data/FrontendLabelData.xml b/app/code/Magento/Catalog/Test/Mftf/Data/FrontendLabelData.xml -index 2423383bc19..a2bdaa7dbc6 100644 ---- a/app/code/Magento/Catalog/Test/Mftf/Data/FrontendLabelData.xml -+++ b/app/code/Magento/Catalog/Test/Mftf/Data/FrontendLabelData.xml -@@ -7,9 +7,17 @@ - --> - - <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="ProductAttributeFrontendLabel" type="FrontendLabel"> - <data key="store_id">0</data> - <data key="label" unique="suffix">attribute</data> - </entity> -+ <entity name="ProductAttributeFrontendLabelTwo" type="FrontendLabel"> -+ <data key="store_id">0</data> -+ <data key="label" unique="suffix">attributeTwo</data> -+ </entity> -+ <entity name="ProductAttributeFrontendLabelThree" type="FrontendLabel"> -+ <data key="store_id">0</data> -+ <data key="label" unique="suffix">attributeThree</data> -+ </entity> - </entities> -diff --git a/app/code/Magento/Catalog/Test/Mftf/Data/GroupPriceData.xml b/app/code/Magento/Catalog/Test/Mftf/Data/GroupPriceData.xml -new file mode 100644 -index 00000000000..ae4736a1ca2 ---- /dev/null -+++ b/app/code/Magento/Catalog/Test/Mftf/Data/GroupPriceData.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="simpleGroupPrice" type="data"> -+ <data key="price">90.00</data> -+ <data key="price_type">fixed</data> -+ <data key="website_id">0</data> -+ <data key="customer_group">ALL GROUPS</data> -+ <data key="quantity">1</data> -+ </entity> -+</entities> -diff --git a/app/code/Magento/Catalog/Test/Mftf/Data/ImageContentData.xml b/app/code/Magento/Catalog/Test/Mftf/Data/ImageContentData.xml -index c674a8fc144..1f4b1470098 100644 ---- a/app/code/Magento/Catalog/Test/Mftf/Data/ImageContentData.xml -+++ b/app/code/Magento/Catalog/Test/Mftf/Data/ImageContentData.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="TestImageContent" type="ImageContent"> - <data key="base64_encoded_data">/9j/4AAQSkZJRgABAQAAAQABAAD/2wCEAAgGBgcGBQgHBwcJCQgKDBQNDAsLDBkSEw8UHRofHh0aHBwgJC4nICIsIxwcKDcpLDAxNDQ0Hyc5PTgyPC4zNDIBCQkJDAsMGA0NGDIhHCEyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMv/AABEIAGAAYAMBIgACEQEDEQH/xACXAAEBAAMBAQEBAAAAAAAAAAAABgMEBQgCAQcQAAEDAQUFBgQDCQAAAAAAAAABAgMEBQYRFpESMTZV0QchcnOzwhMUIkEygaE1QlFSYXGCsbIBAAEFAQAAAAAAAAAAAAAAAAACAwQGBwERAAECAwMLBAMBAAAAAAAAAAEAAgMEERMhkRQxMzRBUVJTcXKxBRJhoSKBwUL/2gAMAwEAAhEDEQA/AP7+AYKysp7Po5aurlbFBEmL3u3NQ6ASaBdArcFnBN5/urzqn0d0Gf7q86p9HdCRkUzy3YFOWEXhOCpATef7q86p9HdBn+6vOqfR3QMimeW7AosIvCcFSAm8/wB1edU+jugz/dXnVPo7oGRTPLdgUWEXhOCpATef7q86p9HdBn+6vOqfR3QMimeW7AosIvCcFSA1bPtGktWiZWUM7Z6d6qjZG7lwXBf1Q2iO5paaOFCmyCDQoTd/uBLX8n3IUhN3+4EtfyfchIk9Zh9w8pyBpW9QvN4Bwbcsujis+pq2Q4Tq5HbW0u9XJj3Y4fc0ibjPgQjEY0GgJNTS4brj/FaIz3Q2FwFafNP4V3gc1aWz7FY+rjhVrsNjBrlcrsV3Iir/ABPxtqzRyM+boJKeJ7kakm2jkRV3Yom4TlbYf4xrnfFSBuqaCn7ouWwbc+4/FT90XTBz57RlbVvpqWjdUSRoiyfWjUbju71MUlqSyWdVPjpnsqIUVJI3ORFZ3fix+4OnoLSRU3V2HZnANKEjcEGOwVG74OxdUGjZM1RNQROqIlYuw3Zcr9pXpgn1f0xN4kQYgiww8bU4xwe0OG1eg+y7gCg8cvqOLEjuy7gCg8cvqOLEzT1HXIvcfKq0zpn9ShN3+4EtfyfchSE3f7gS1/J9yCJPWYfcPKTA0reoXm85l4P2HUf4/wDSHTPmSOOZiskY17F3tcmKKaXMwjGgvhj/AECMQrTFZ72ObvC5lvxq+gjeivRsUzXvVn4kb34qmpozxWc+NjVtWtqPiOREjbMj1Vf7YFHvMMdLTxP244ImP/maxEUhzMhaxC8UvABrXZuoR9pmLL+9xddfvXNrfkVtJyPqJaOpRiL8VHbKPT8+5THFVS1FnWnE+VKhsUbmsmamG3i1e78jsSwQzoiTRRyIm5HtRf8AZ9MjZGxGMY1rU/damCHTJPMQuDgAa5q31G0VpdnrnuRYO9xNaA1+/r9rUsmeGazqdscrHuZExHo1cVauH30U3THFBDBtfBijj2t+w1Ex0MhMgMcyG1r843J+GC1oDs69B9l3AFB45fUcWJHdl3AFB45fUcWJm3qOuRe4+VV5nTP6lCbv9wJa/k+5CkJu/wBwJa/k+5BEnrMPuHlJgaVvULzeADUlbUAAIQAAhAACF6D7LuAKDxy+o4sSO7LuAKDxy+o4sTMPUdci9x8qqTOmf1KE3f7gS1/J9yFITd/uBLX8n3IIk9Zh9w8pMDSt6hebwAakragABCAAEIAAQvQfZdwBQeOX1HFiR3ZdwBQeOX1HFiZh6jrkXuPlVSZ0z+pQwVlHT2hRy0lXE2WCVMHsduchnBEBINQmQaXhTeQLq8lp9XdRkC6vJafV3UpASMtmeY7Epy3i8RxU3kC6vJafV3UZAuryWn1d1KQBlszzHYlFvF4jipvIF1eS0+ruoyBdXktPq7qUgDLZnmOxKLeLxHFTeQLq8lp9XdRkC6vJafV3UpAGWzPMdiUW8XiOK1bPs6ksqiZR0MDYKdiqrY27kxXFf1U2gCO5xcauNSmySTUr/9k=</data> - <data key="type">image/jpeg</data> -diff --git a/app/code/Magento/Catalog/Test/Mftf/Data/ImageData.xml b/app/code/Magento/Catalog/Test/Mftf/Data/ImageData.xml -new file mode 100644 -index 00000000000..a2391dda548 ---- /dev/null -+++ b/app/code/Magento/Catalog/Test/Mftf/Data/ImageData.xml -@@ -0,0 +1,28 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<entities xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataProfileSchema.xsd"> -+ <entity name="placeholderBaseImage" type="imageFile"> -+ <data key="file">adobe-base.jpg</data> -+ <data key="name">adobe-base</data> -+ <data key="extension">jpg</data> -+ </entity> -+ -+ <entity name="placeholderSmallImage" type="imageFile"> -+ <data key="file">adobe-small.jpg</data> -+ <data key="name">adobe-small</data> -+ <data key="extension">jpg</data> -+ </entity> -+ -+ <entity name="placeholderThumbnailImage" type="imageFile"> -+ <data key="file">adobe-thumb.jpg</data> -+ <data key="name">adobe-thumb</data> -+ <data key="extension">jpg</data> -+ </entity> -+</entities> -diff --git a/app/code/Magento/Catalog/Test/Mftf/Data/NewProductData.xml b/app/code/Magento/Catalog/Test/Mftf/Data/NewProductData.xml -new file mode 100644 -index 00000000000..4479805cb12 ---- /dev/null -+++ b/app/code/Magento/Catalog/Test/Mftf/Data/NewProductData.xml -@@ -0,0 +1,16 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<entities xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataProfileSchema.xsd"> -+ <entity name="NewProductData" type="braintree_config_state"> -+ <data key="ProductName">ProductTest</data> -+ <data key="Price">100</data> -+ <data key="Quantity">100</data> -+ </entity> -+</entities> -diff --git a/app/code/Magento/Catalog/Test/Mftf/Data/ProductAttributeData.xml b/app/code/Magento/Catalog/Test/Mftf/Data/ProductAttributeData.xml -index f67370dcff2..02e5ae5ae36 100644 ---- a/app/code/Magento/Catalog/Test/Mftf/Data/ProductAttributeData.xml -+++ b/app/code/Magento/Catalog/Test/Mftf/Data/ProductAttributeData.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="productAttributeWysiwyg" type="ProductAttribute"> - <data key="attribute_code" unique="suffix">attribute</data> - <data key="frontend_input">textarea</data> -@@ -52,6 +52,41 @@ - <data key="used_for_sort_by">true</data> - <requiredEntity type="FrontendLabel">ProductAttributeFrontendLabel</requiredEntity> - </entity> -+ <entity name="productAttributeWithTwoOptionsNotVisible" type="ProductAttribute"> -+ <data key="attribute_code" unique="suffix">test_attr_</data> -+ <data key="frontend_input">select</data> -+ <data key="scope">global</data> -+ <data key="is_required">false</data> -+ <data key="is_unique">false</data> -+ <data key="is_searchable">false</data> -+ <data key="is_visible">true</data> -+ <data key="is_visible_in_advanced_search">false</data> -+ <data key="is_visible_on_front">false</data> -+ <data key="is_filterable">false</data> -+ <data key="is_filterable_in_search">false</data> -+ <data key="used_in_product_listing">false</data> -+ <data key="is_used_for_promo_rules">false</data> -+ <data key="is_comparable">false</data> -+ <data key="is_used_in_grid">false</data> -+ <data key="is_visible_in_grid">false</data> -+ <data key="is_filterable_in_grid">false</data> -+ <data key="used_for_sort_by">false</data> -+ <requiredEntity type="FrontendLabel">ProductAttributeFrontendLabel</requiredEntity> -+ </entity> -+ <entity name="hiddenDropdownAttributeWithOptions" extends="productAttributeWithTwoOptions"> -+ <data key="is_searchable">false</data> -+ <data key="is_visible_in_advanced_search">false</data> -+ <data key="is_visible_on_front">false</data> -+ <data key="is_filterable">false</data> -+ <data key="is_filterable_in_search">false</data> -+ <data key="used_in_product_listing">false</data> -+ <data key="is_used_for_promo_rules">false</data> -+ <data key="is_comparable">false</data> -+ <data key="is_used_in_grid">false</data> -+ <data key="is_visible_in_grid">false</data> -+ <data key="is_filterable_in_grid">false</data> -+ <data key="used_for_sort_by">false</data> -+ </entity> - <entity name="productDropDownAttribute" type="ProductAttribute"> - <data key="attribute_code" unique="suffix">attribute</data> - <data key="frontend_input">select</data> -@@ -73,6 +108,27 @@ - <data key="used_for_sort_by">true</data> - <requiredEntity type="FrontendLabel">ProductAttributeFrontendLabel</requiredEntity> - </entity> -+ <entity name="productDropDownAttributeNotSearchable" type="ProductAttribute"> -+ <data key="attribute_code" unique="suffix">attribute</data> -+ <data key="frontend_input">select</data> -+ <data key="scope">global</data> -+ <data key="is_required">false</data> -+ <data key="is_unique">false</data> -+ <data key="is_searchable">false</data> -+ <data key="is_visible">true</data> -+ <data key="is_visible_in_advanced_search">true</data> -+ <data key="is_visible_on_front">true</data> -+ <data key="is_filterable">true</data> -+ <data key="is_filterable_in_search">true</data> -+ <data key="used_in_product_listing">true</data> -+ <data key="is_used_for_promo_rules">true</data> -+ <data key="is_comparable">true</data> -+ <data key="is_used_in_grid">true</data> -+ <data key="is_visible_in_grid">true</data> -+ <data key="is_filterable_in_grid">true</data> -+ <data key="used_for_sort_by">true</data> -+ <requiredEntity type="FrontendLabel">ProductAttributeFrontendLabel</requiredEntity> -+ </entity> - <entity name="productAttributeWithDropdownTwoOptions" type="ProductAttribute"> - <data key="attribute_code">testattribute</data> - <data key="frontend_input">select</data> -@@ -115,4 +171,176 @@ - <data key="used_for_sort_by">true</data> - <requiredEntity type="FrontendLabel">ProductAttributeFrontendLabel</requiredEntity> - </entity> -+ <entity name="productAttributeMultiselectTwoOptionsNotSearchable" type="ProductAttribute"> -+ <data key="attribute_code" unique="suffix">attribute</data> -+ <data key="frontend_input">multiselect</data> -+ <data key="scope">global</data> -+ <data key="is_required">false</data> -+ <data key="is_unique">false</data> -+ <data key="is_searchable">false</data> -+ <data key="is_visible">true</data> -+ <data key="is_visible_in_advanced_search">true</data> -+ <data key="is_visible_on_front">true</data> -+ <data key="is_filterable">true</data> -+ <data key="is_filterable_in_search">true</data> -+ <data key="used_in_product_listing">true</data> -+ <data key="is_used_for_promo_rules">true</data> -+ <data key="is_comparable">true</data> -+ <data key="is_used_in_grid">true</data> -+ <data key="is_visible_in_grid">true</data> -+ <data key="is_filterable_in_grid">true</data> -+ <data key="used_for_sort_by">true</data> -+ <requiredEntity type="FrontendLabel">ProductAttributeFrontendLabel</requiredEntity> -+ </entity> -+ <entity name="newsFromDate" type="ProductAttribute"> -+ <data key="attribute_code">news_from_date</data> -+ <data key="default_frontend_label">Set Product as New from Date</data> -+ <data key="frontend_input">date</data> -+ <data key="is_required">false</data> -+ <data key="is_user_defined">true</data> -+ <data key="scope">website</data> -+ <data key="is_unique">false</data> -+ <data key="is_searchable">false</data> -+ <data key="is_visible">false</data> -+ <data key="is_visible_on_front">false</data> -+ <data key="is_filterable">false</data> -+ <data key="is_filterable_in_search">false</data> -+ <data key="used_in_product_listing">true</data> -+ <data key="is_used_for_promo_rules">false</data> -+ <data key="is_comparable">false</data> -+ <data key="is_used_in_grid">true</data> -+ <data key="is_filterable_in_grid">true</data> -+ <data key="used_for_sort_by">false</data> -+ <requiredEntity type="FrontendLabel">ProductAttributeFrontendLabel</requiredEntity> -+ </entity> -+ <entity name="newProductAttribute" type="ProductAttribute"> -+ <data key="attribute_code" unique="suffix">attribute</data> -+ <data key="frontend_input">text</data> -+ <data key="scope">global</data> -+ <data key="is_required">false</data> -+ <data key="is_unique">false</data> -+ <data key="is_searchable">true</data> -+ <data key="is_visible">true</data> -+ <data key="is_visible_in_advanced_search">true</data> -+ <data key="is_visible_on_front">true</data> -+ <data key="is_filterable">true</data> -+ <data key="is_filterable_in_search">true</data> -+ <data key="used_in_product_listing">true</data> -+ <data key="is_used_for_promo_rules">true</data> -+ <data key="is_comparable">true</data> -+ <data key="is_used_in_grid">true</data> -+ <data key="is_visible_in_grid">true</data> -+ <data key="is_filterable_in_grid">true</data> -+ <data key="used_for_sort_by">true</data> -+ <requiredEntity type="FrontendLabel">ProductAttributeFrontendLabel</requiredEntity> -+ </entity> -+ <entity name="productYesNoAttribute" type="ProductAttribute"> -+ <data key="attribute_code" unique="suffix">attribute</data> -+ <data key="frontend_input">boolean</data> -+ <data key="scope">global</data> -+ <data key="is_required">false</data> -+ <data key="is_unique">false</data> -+ <data key="is_searchable">true</data> -+ <data key="is_visible">true</data> -+ <data key="is_visible_in_advanced_search">true</data> -+ <data key="is_visible_on_front">true</data> -+ <data key="is_filterable">true</data> -+ <data key="is_filterable_in_search">true</data> -+ <data key="used_in_product_listing">true</data> -+ <data key="is_used_for_promo_rules">true</data> -+ <data key="is_comparable">true</data> -+ <data key="is_used_in_grid">true</data> -+ <data key="is_visible_in_grid">true</data> -+ <data key="is_filterable_in_grid">true</data> -+ <data key="used_for_sort_by">true</data> -+ <requiredEntity type="FrontendLabel">ProductAttributeFrontendLabel</requiredEntity> -+ </entity> -+ <entity name="productAttributeText" type="ProductAttribute"> -+ <data key="attribute_code" unique="suffix">attribute</data> -+ <data key="frontend_input">text</data> -+ <data key="scope">global</data> -+ <data key="is_required">false</data> -+ <data key="is_unique">false</data> -+ <data key="is_searchable">false</data> -+ <data key="is_visible">true</data> -+ <data key="backend_type">text</data> -+ <data key="is_wysiwyg_enabled">false</data> -+ <data key="is_visible_in_advanced_search">false</data> -+ <data key="is_visible_on_front">true</data> -+ <data key="is_filterable">false</data> -+ <data key="is_filterable_in_search">false</data> -+ <data key="used_in_product_listing">false</data> -+ <data key="is_used_for_promo_rules">false</data> -+ <data key="is_comparable">true</data> -+ <data key="is_used_in_grid">false</data> -+ <data key="is_visible_in_grid">false</data> -+ <data key="is_filterable_in_grid">false</data> -+ <data key="used_for_sort_by">false</data> -+ <requiredEntity type="FrontendLabel">ProductAttributeFrontendLabel</requiredEntity> -+ </entity> -+ <entity name="textProductAttribute" extends="productAttributeWysiwyg" type="ProductAttribute"> -+ <data key="frontend_input">text</data> -+ <data key="default_value" unique="suffix">defaultValue</data> -+ <data key="is_required_admin">No</data> -+ </entity> -+ <entity name="dateProductAttribute" extends="productAttributeWysiwyg" type="ProductAttribute"> -+ <data key="frontend_input">date</data> -+ <data key="is_required_admin">No</data> -+ </entity> -+ <entity name="priceProductAttribute" extends="productAttributeWysiwyg" type="ProductAttribute"> -+ <data key="frontend_input">date</data> -+ <data key="is_required_admin">No</data> -+ </entity> -+ <entity name="dropdownProductAttribute" extends="productAttributeWysiwyg" type="ProductAttribute"> -+ <data key="frontend_input">select</data> -+ <data key="frontend_input_admin">Dropdown</data> -+ <data key="is_required_admin">No</data> -+ <data key="option1_admin" unique="suffix">opt1Admin</data> -+ <data key="option1_frontend" unique="suffix">opt1Front</data> -+ <data key="option2_admin" unique="suffix">opt2Admin</data> -+ <data key="option2_frontend" unique="suffix">opt2Front</data> -+ <data key="option3_admin" unique="suffix">opt3Admin</data> -+ <data key="option3_frontend" unique="suffix">opt3Front</data> -+ </entity> -+ <entity name="multiselectProductAttribute" extends="productAttributeWysiwyg" type="ProductAttribute"> -+ <data key="frontend_input">multiselect</data> -+ <data key="frontend_input_admin">Multiple Select</data> -+ <data key="is_required_admin">No</data> -+ <data key="option1_admin" unique="suffix">opt1Admin</data> -+ <data key="option1_frontend" unique="suffix">opt1Front</data> -+ <data key="option2_admin" unique="suffix">opt2Admin</data> -+ <data key="option2_frontend" unique="suffix">opt2Front</data> -+ <data key="option3_admin" unique="suffix">opt3Admin</data> -+ <data key="option3_frontend" unique="suffix">opt3Front</data> -+ </entity> -+ <entity name="dropdownProductAttributeWithQuote" extends="productAttributeWysiwyg" type="ProductAttribute"> -+ <data key="frontend_input">select</data> -+ <data key="frontend_input_admin">Dropdown</data> -+ <data key="is_required_admin">No</data> -+ <data key="option1_admin" unique="suffix">opt1'Admin</data> -+ <data key="option1_frontend" unique="suffix">opt1'Front</data> -+ </entity> -+ <entity name="productTextEditorAttribute" type="ProductAttribute"> -+ <data key="attribute_code" unique="suffix">attribute</data> -+ <data key="frontend_input">texteditor</data> -+ <data key="scope">global</data> -+ <data key="is_required">false</data> -+ <data key="is_unique">false</data> -+ <data key="is_searchable">true</data> -+ <data key="is_visible">true</data> -+ <data key="backend_type">text</data> -+ <data key="is_wysiwyg_enabled">true</data> -+ <data key="is_visible_in_advanced_search">true</data> -+ <data key="is_visible_on_front">true</data> -+ <data key="is_filterable">true</data> -+ <data key="is_filterable_in_search">true</data> -+ <data key="used_in_product_listing">true</data> -+ <data key="is_used_for_promo_rules">true</data> -+ <data key="is_comparable">true</data> -+ <data key="is_used_in_grid">true</data> -+ <data key="is_visible_in_grid">true</data> -+ <data key="is_filterable_in_grid">true</data> -+ <data key="used_for_sort_by">true</data> -+ <requiredEntity type="FrontendLabel">ProductAttributeFrontendLabel</requiredEntity> -+ </entity> - </entities> -diff --git a/app/code/Magento/Catalog/Test/Mftf/Data/ProductAttributeMediaGalleryEntryData.xml b/app/code/Magento/Catalog/Test/Mftf/Data/ProductAttributeMediaGalleryEntryData.xml -index 60b38812e4c..98c9a70e6aa 100644 ---- a/app/code/Magento/Catalog/Test/Mftf/Data/ProductAttributeMediaGalleryEntryData.xml -+++ b/app/code/Magento/Catalog/Test/Mftf/Data/ProductAttributeMediaGalleryEntryData.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="ApiProductAttributeMediaGalleryEntryTestImage" type="ProductAttributeMediaGalleryEntry"> - <data key="media_type">image</data> - <data key="label" unique="suffix">Test Image </data> -diff --git a/app/code/Magento/Catalog/Test/Mftf/Data/ProductAttributeOptionData.xml b/app/code/Magento/Catalog/Test/Mftf/Data/ProductAttributeOptionData.xml -index 15c2dc8bbeb..fcb56cf298a 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 @@ - --> - - <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="productAttributeOption1" type="ProductAttributeOption"> - <var key="attribute_code" entityKey="attribute_code" entityType="ProductAttribute"/> - <data key="label" unique="suffix">option1</data> -@@ -65,4 +65,25 @@ - <data key="is_default">false</data> - <data key="sort_order">0</data> - </entity> -+ <entity name="ProductAttributeOption7" type="ProductAttributeOption"> -+ <var key="attribute_code" entityKey="attribute_code" entityType="ProductAttribute"/> -+ <data key="label" unique="suffix">Green</data> -+ <data key="is_default">false</data> -+ <data key="sort_order">3</data> -+ <requiredEntity type="StoreLabel">Option7Store0</requiredEntity> -+ <requiredEntity type="StoreLabel">Option8Store1</requiredEntity> -+ </entity> -+ <entity name="ProductAttributeOption8" type="ProductAttributeOption"> -+ <var key="attribute_code" entityKey="attribute_code" entityType="ProductAttribute"/> -+ <data key="label" unique="suffix">Red</data> -+ <data key="is_default">false</data> -+ <data key="sort_order">3</data> -+ <requiredEntity type="StoreLabel">Option9Store0</requiredEntity> -+ <requiredEntity type="StoreLabel">Option10Store1</requiredEntity> -+ </entity> -+ <entity name="ProductAttributeOption8" type="ProductAttributeOption"> -+ <var key="attribute_code" entityKey="attribute_code" entityType="ProductAttribute"/> -+ <data key="label" unique="suffix">White</data> -+ <data key="value" unique="suffix">white</data> -+ </entity> - </entities> -diff --git a/app/code/Magento/Catalog/Test/Mftf/Data/ProductAttributeSetData.xml b/app/code/Magento/Catalog/Test/Mftf/Data/ProductAttributeSetData.xml -index 68c0a54ff88..6d4314a6d86 100644 ---- a/app/code/Magento/Catalog/Test/Mftf/Data/ProductAttributeSetData.xml -+++ b/app/code/Magento/Catalog/Test/Mftf/Data/ProductAttributeSetData.xml -@@ -7,11 +7,23 @@ - --> - - <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="AddToDefaultSet" type="ProductAttributeSet"> - <var key="attributeCode" entityKey="attribute_code" entityType="ProductAttribute"/> - <data key="attributeSetId">4</data> - <data key="attributeGroupId">7</data> - <data key="sortOrder">0</data> - </entity> -+ <entity name="AddToDefaultSetSortOrder1" type="ProductAttributeSet"> -+ <var key="attributeCode" entityKey="attribute_code" entityType="ProductAttribute"/> -+ <data key="attributeSetId">4</data> -+ <data key="attributeGroupId">7</data> -+ <data key="sortOrder">1</data> -+ </entity> -+ <entity name="AddToSetBlank" type="ProductAttributeSet"> -+ <var key="attributeCode" entityKey="attribute_code" entityType="ProductAttribute"/> -+ <data key="attributeSetId">0</data> -+ <data key="attributeGroupId">0</data> -+ <data key="sortOrder">0</data> -+ </entity> - </entities> -diff --git a/app/code/Magento/Catalog/Test/Mftf/Data/ProductData.xml b/app/code/Magento/Catalog/Test/Mftf/Data/ProductData.xml -index 0df091eb5f8..0b6a1e7d044 100644 ---- a/app/code/Magento/Catalog/Test/Mftf/Data/ProductData.xml -+++ b/app/code/Magento/Catalog/Test/Mftf/Data/ProductData.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="_defaultProduct" type="product"> - <data key="sku" unique="suffix">testSku</data> - <data key="type_id">simple</data> -@@ -18,6 +18,7 @@ - <data key="urlKey" unique="suffix">testurlkey</data> - <data key="status">1</data> - <data key="quantity">100</data> -+ <data key="weight">1</data> - <requiredEntity type="product_extension_attribute">EavStockItem</requiredEntity> - <requiredEntity type="custom_attribute_array">CustomAttributeCategoryIds</requiredEntity> - </entity> -@@ -34,6 +35,13 @@ - <requiredEntity type="product_extension_attribute">EavStockItem</requiredEntity> - <requiredEntity type="custom_attribute_array">CustomAttributeCategoryIds</requiredEntity> - </entity> -+ <entity name="ApiSimpleProductWithSpecCharInName" type="product" extends="ApiSimpleProduct"> -+ <data key="name">Pursuit Lumaflex&trade; Tone Band</data> -+ <data key="sku" unique="suffix">x&trade;</data> -+ </entity> -+ <entity name="ApiSimpleProductWithCustomPrice" type="product" extends="ApiSimpleProduct"> -+ <data key="price">100</data> -+ </entity> - <entity name="ApiSimpleProductUpdateDescription" type="product2"> - <requiredEntity type="custom_attribute">ApiProductDescription</requiredEntity> - <requiredEntity type="custom_attribute">ApiProductShortDescription</requiredEntity> -@@ -56,6 +64,48 @@ - <requiredEntity type="product_extension_attribute">EavStockItem</requiredEntity> - <requiredEntity type="custom_attribute_array">CustomAttributeCategoryIds</requiredEntity> - </entity> -+ <entity name="SimpleProductAfterImport1" type="product"> -+ <data key="sku">SimpleProductForTest1</data> -+ <data key="type_id">simple</data> -+ <data key="attribute_set_id">4</data> -+ <data key="name">SimpleProductAfterImport1</data> -+ <data key="price">250.00</data> -+ <data key="visibility">4</data> -+ <data key="status">1</data> -+ <data key="quantity">100</data> -+ <data key="urlKey">simple-product-for-test-1</data> -+ <data key="weight">1</data> -+ <requiredEntity type="product_extension_attribute">EavStockItem</requiredEntity> -+ <requiredEntity type="custom_attribute_array">CustomAttributeCategoryIds</requiredEntity> -+ </entity> -+ <entity name="SimpleProductAfterImport2" type="product"> -+ <data key="sku">SimpleProductForTest2</data> -+ <data key="type_id">simple</data> -+ <data key="attribute_set_id">4</data> -+ <data key="name">SimpleProductAfterImport2</data> -+ <data key="price">300.00</data> -+ <data key="visibility">4</data> -+ <data key="status">1</data> -+ <data key="quantity">100</data> -+ <data key="urlKey">simple-product-for-test-2</data> -+ <data key="weight">1</data> -+ <requiredEntity type="product_extension_attribute">EavStockItem</requiredEntity> -+ <requiredEntity type="custom_attribute_array">CustomAttributeCategoryIds</requiredEntity> -+ </entity> -+ <entity name="SimpleProductAfterImport3" type="product"> -+ <data key="sku">SimpleProductForTest3</data> -+ <data key="type_id">simple</data> -+ <data key="attribute_set_id">4</data> -+ <data key="name">SimpleProductAfterImport3</data> -+ <data key="price">350.00</data> -+ <data key="visibility">4</data> -+ <data key="status">1</data> -+ <data key="quantity">100</data> -+ <data key="urlKey">simple-product-for-test-3</data> -+ <data key="weight">1</data> -+ <requiredEntity type="product_extension_attribute">EavStockItem</requiredEntity> -+ <requiredEntity type="custom_attribute_array">CustomAttributeCategoryIds</requiredEntity> -+ </entity> - <entity name="SimpleProduct2" type="product"> - <data key="sku" unique="suffix">SimpleProduct</data> - <data key="type_id">simple</data> -@@ -92,6 +142,65 @@ - <data key="quantity">0</data> - <requiredEntity type="custom_attribute_array">CustomAttributeCategoryIds</requiredEntity> - </entity> -+ <entity name="SimpleOutOfStockProduct" type="product"> -+ <data key="sku" unique="suffix">testSku</data> -+ <data key="type_id">simple</data> -+ <data key="attribute_set_id">4</data> -+ <data key="visibility">4</data> -+ <data key="name" unique="suffix">OutOfStockProduct</data> -+ <data key="price">123.00</data> -+ <data key="urlKey" unique="suffix">testurlkey</data> -+ <data key="status">1</data> -+ <data key="quantity">0</data> -+ </entity> -+ <entity name="SimpleProductInStockQuantityZero" type="product"> -+ <data key="sku" unique="suffix">testSku</data> -+ <data key="type_id">simple</data> -+ <data key="attribute_set_id">4</data> -+ <data key="visibility">4</data> -+ <data key="name" unique="suffix">SimpleProductInStockQuantityZero</data> -+ <data key="price">123.00</data> -+ <data key="urlKey" unique="suffix">SimpleProductInStockQuantityZero</data> -+ <data key="status">1</data> -+ <data key="quantity">0</data> -+ <requiredEntity type="product_extension_attribute">EavStock0</requiredEntity> -+ </entity> -+ <!-- Simple Product Disabled --> -+ <entity name="SimpleProductOffline" type="product2"> -+ <data key="sku" unique="suffix">testSku</data> -+ <data key="type_id">simple</data> -+ <data key="attribute_set_id">4</data> -+ <data key="visibility">4</data> -+ <data key="name" unique="suffix">SimpleOffline</data> -+ <data key="price">123.00</data> -+ <data key="status">2</data> -+ <data key="quantity">100</data> -+ <data key="urlKey" unique="suffix">testurlkey</data> -+ <requiredEntity type="product_extension_attribute">EavStockItem</requiredEntity> -+ <requiredEntity type="custom_attribute">CustomAttributeProductAttribute</requiredEntity> -+ </entity> -+ <entity name="SimpleProductDisabled" type="product"> -+ <data key="sku" unique="suffix">simple_product_disabled</data> -+ <data key="type_id">simple</data> -+ <data key="attribute_set_id">4</data> -+ <data key="name" unique="suffix">Simple Product Disabled</data> -+ <data key="price">123.00</data> -+ <data key="visibility">4</data> -+ <data key="status">2</data> -+ <data key="quantity">1001</data> -+ <requiredEntity type="product_extension_attribute">EavStockItem</requiredEntity> -+ </entity> -+ <entity name="SimpleProductNotVisibleIndividually" type="product"> -+ <data key="sku" unique="suffix">simple_product_not_visible_individually</data> -+ <data key="type_id">simple</data> -+ <data key="attribute_set_id">4</data> -+ <data key="name" unique="suffix">Simple Product Not Visible Individually</data> -+ <data key="price">123.00</data> -+ <data key="visibility">1</data> -+ <data key="status">1</data> -+ <data key="quantity">1000</data> -+ <requiredEntity type="product_extension_attribute">EavStockItem</requiredEntity> -+ </entity> - <entity name="NewSimpleProduct" type="product"> - <data key="price">321.00</data> - </entity> -@@ -106,6 +215,18 @@ - <requiredEntity type="product_extension_attribute">EavStockItem</requiredEntity> - <requiredEntity type="custom_attribute">CustomAttributeProductAttribute</requiredEntity> - </entity> -+ <entity name="ApiSimpleOutOfStock" type="product2"> -+ <data key="sku" unique="suffix">api-simple-product</data> -+ <data key="type_id">simple</data> -+ <data key="attribute_set_id">4</data> -+ <data key="visibility">4</data> -+ <data key="name" unique="suffix">Api Simple Out Of Stock Product</data> -+ <data key="price">123.00</data> -+ <data key="urlKey" unique="suffix">api-simple-product</data> -+ <data key="status">1</data> -+ <data key="quantity">100</data> -+ <requiredEntity type="custom_attribute">CustomAttributeProductAttribute</requiredEntity> -+ </entity> - <entity name="ApiSimpleOne" type="product2"> - <data key="sku" unique="suffix">api-simple-product</data> - <data key="type_id">simple</data> -@@ -119,6 +240,9 @@ - <requiredEntity type="product_extension_attribute">EavStockItem</requiredEntity> - <requiredEntity type="custom_attribute">CustomAttributeProductAttribute</requiredEntity> - </entity> -+ <entity name="ApiSimpleProductWithShortSKU" type="product2" extends="ApiSimpleOne"> -+ <data key="sku" unique="suffix">pr</data> -+ </entity> - <entity name="ApiSimpleOneHidden" type="product2"> - <data key="sku" unique="suffix">api-simple-product</data> - <data key="type_id">simple</data> -@@ -145,6 +269,15 @@ - <requiredEntity type="product_extension_attribute">EavStockItem</requiredEntity> - <requiredEntity type="custom_attribute">CustomAttributeProductAttribute</requiredEntity> - </entity> -+ <entity name="ApiSimpleProductWithPrice50" type="product2" extends="ApiSimpleOne"> -+ <data key="price">50</data> -+ </entity> -+ <entity name="ApiSimpleProductWithPrice60" type="product2" extends="ApiSimpleTwo"> -+ <data key="price">60</data> -+ </entity> -+ <entity name="ApiSimpleProductWithPrice70" type="product2" extends="SimpleOne"> -+ <data key="price">70</data> -+ </entity> - <entity name="ApiSimpleTwoHidden" type="product2"> - <data key="sku" unique="suffix">api-simple-product-two</data> - <data key="type_id">simple</data> -@@ -180,6 +313,9 @@ - <requiredEntity type="product_extension_attribute">EavStockItem</requiredEntity> - <requiredEntity type="custom_attribute">CustomAttributeProductUrlKey</requiredEntity> - </entity> -+ <entity name="SetProductVisibilityHidden" type="product2"> -+ <data key="visibility">1</data> -+ </entity> - <entity name="ProductImage" type="uploadImage"> - <data key="title" unique="suffix">Image1</data> - <data key="price">1.00</data> -@@ -206,6 +342,15 @@ - <data key="filename">magento-again</data> - <data key="file_extension">jpg</data> - </entity> -+ <entity name="TestImageAdobe" type="image"> -+ <data key="title" unique="suffix">magento-adobe</data> -+ <data key="price">1.00</data> -+ <data key="file_type">Upload File</data> -+ <data key="shareable">Yes</data> -+ <data key="file">adobe-base.jpg</data> -+ <data key="filename">adobe-base</data> -+ <data key="file_extension">jpg</data> -+ </entity> - <entity name="ProductWithUnicode" type="product"> - <data key="sku" unique="suffix">霁产品</data> - <data key="type_id">simple</data> -@@ -256,7 +401,7 @@ - <data key="status">1</data> - <data key="quantity">100</data> - <data key="weight">0</data> -- <requiredEntity type="product_extension_attribute">EavStockItem</requiredEntity> -+ <requiredEntity type="product_extension_attribute">EavStock100</requiredEntity> - <requiredEntity type="custom_attribute_array">CustomAttributeCategoryIds</requiredEntity> - </entity> - <entity name="productWithDescription" type="product"> -@@ -301,6 +446,20 @@ - <requiredEntity type="product_extension_attribute">EavStockItem</requiredEntity> - <requiredEntity type="custom_attribute_array">CustomAttributeCategoryIds</requiredEntity> - </entity> -+ <entity name="SimpleProductWithCustomAttributeSet" type="product"> -+ <data key="sku" unique="suffix">testSku</data> -+ <data key="type_id">simple</data> -+ <var key="attribute_set_id" entityKey="attribute_set_id" entityType="CatalogAttributeSet"/> -+ <data key="visibility">4</data> -+ <data key="name" unique="suffix">testProductName</data> -+ <data key="price">123.00</data> -+ <data key="urlKey" unique="suffix">testurlkey</data> -+ <data key="status">1</data> -+ <data key="weight">1</data> -+ <data key="quantity">100</data> -+ <requiredEntity type="product_extension_attribute">EavStockItem</requiredEntity> -+ <requiredEntity type="custom_attribute_array">CustomAttributeCategoryIds</requiredEntity> -+ </entity> - <entity name="productWithOptions" type="product"> - <var key="sku" entityType="product" entityKey="sku" /> - <data key="file">magento.jpg</data> -@@ -315,10 +474,44 @@ - <requiredEntity type="product_option">ProductOptionDateTime</requiredEntity> - <requiredEntity type="product_option">ProductOptionTime</requiredEntity> - </entity> -+ <entity name="productWithOptionRadiobutton" type="product"> -+ <var key="sku" entityType="product" entityKey="sku" /> -+ <requiredEntity type="product_option">ProductOptionRadiobuttonWithTwoFixedOptions</requiredEntity> -+ </entity> -+ <entity name="productWithCustomOptions" type="product"> -+ <var key="sku" entityType="product" entityKey="sku" /> -+ <data key="file">magento.jpg</data> -+ <requiredEntity type="product_option">ProductOptionDropDown2</requiredEntity> -+ </entity> -+ <entity name="productWithFixedOptions" type="product"> -+ <var key="sku" entityType="product" entityKey="sku" /> -+ <data key="file">magento.jpg</data> -+ <requiredEntity type="product_option">ProductOptionRadioButton2</requiredEntity> -+ </entity> - <entity name="productWithOptions2" type="product"> - <var key="sku" entityType="product" entityKey="sku" /> - <requiredEntity type="product_option">ProductOptionDropDownWithLongValuesTitle</requiredEntity> - </entity> -+ <entity name="productWithDropdownOption" type="product"> -+ <var key="sku" entityType="product" entityKey="sku" /> -+ <requiredEntity type="product_option">ProductOptionValueDropdown</requiredEntity> -+ </entity> -+ <entity name="productWithDropdownAndFieldOptions" type="product"> -+ <var key="sku" entityType="product" entityKey="sku" /> -+ <requiredEntity type="product_option">ProductOptionValueDropdown</requiredEntity> -+ <requiredEntity type="product_option">ProductOptionField</requiredEntity> -+ </entity> -+ <entity name="ProductWithTextFieldAndAreaOptions" type="product"> -+ <var key="sku" entityType="product" entityKey="sku" /> -+ <requiredEntity type="product_option">ProductOptionField</requiredEntity> -+ <requiredEntity type="product_option">ProductOptionArea</requiredEntity> -+ </entity> -+ <entity name="ProductWithTextFieldAndAreaAndFileOptions" type="product"> -+ <var key="sku" entityType="product" entityKey="sku" /> -+ <requiredEntity type="product_option">ProductOptionField</requiredEntity> -+ <requiredEntity type="product_option">ProductOptionArea</requiredEntity> -+ <requiredEntity type="product_option">ProductOptionFile</requiredEntity> -+ </entity> - <entity name="ApiVirtualProductWithDescription" type="product"> - <data key="sku" unique="suffix">api-virtual-product</data> - <data key="type_id">virtual</data> -@@ -419,4 +612,557 @@ - <data key="status">1</data> - <requiredEntity type="product_extension_attribute">EavStock100</requiredEntity> - </entity> -+ <entity name="simpleProductForMassUpdate" type="product"> -+ <data key="sku" unique="suffix">testSku</data> -+ <data key="type_id">simple</data> -+ <data key="attribute_set_id">4</data> -+ <data key="visibility">4</data> -+ <data key="name" unique="suffix">massUpdateProductName</data> -+ <data key="keyword">massUpdateProductName</data> -+ <data key="price">123.00</data> -+ <data key="urlKey" unique="suffix">masstesturlkey</data> -+ <data key="status">1</data> -+ <data key="quantity">100</data> -+ <data key="weight">1</data> -+ <requiredEntity type="product_extension_attribute">EavStockItem</requiredEntity> -+ <requiredEntity type="custom_attribute_array">CustomAttributeCategoryIds</requiredEntity> -+ </entity> -+ <entity name="simpleProductForMassUpdate2" type="product"> -+ <data key="sku" unique="suffix">testSku</data> -+ <data key="type_id">simple</data> -+ <data key="attribute_set_id">4</data> -+ <data key="visibility">4</data> -+ <data key="name" unique="suffix">massUpdateProductName</data> -+ <data key="keyword">massUpdateProductName</data> -+ <data key="price">123.00</data> -+ <data key="urlKey" unique="suffix">masstesturlkey</data> -+ <data key="status">1</data> -+ <data key="quantity">100</data> -+ <data key="weight">1</data> -+ <requiredEntity type="product_extension_attribute">EavStockItem</requiredEntity> -+ <requiredEntity type="custom_attribute_array">CustomAttributeCategoryIds</requiredEntity> -+ </entity> -+ <entity name="ApiSimpleSingleQty" type="product2"> -+ <data key="sku" unique="suffix">api-simple-product</data> -+ <data key="type_id">simple</data> -+ <data key="attribute_set_id">4</data> -+ <data key="visibility">4</data> -+ <data key="name" unique="suffix">Api Simple Product</data> -+ <data key="price">123.00</data> -+ <data key="urlKey" unique="suffix">api-simple-product</data> -+ <data key="status">1</data> -+ <data key="quantity">1</data> -+ <requiredEntity type="product_extension_attribute">EavStock1</requiredEntity> -+ <requiredEntity type="custom_attribute">CustomAttributeProductAttribute</requiredEntity> -+ </entity> -+ <entity name="virtualProductWithRequiredFields" type="product"> -+ <data key="name" unique="suffix">virtualProduct</data> -+ <data key="sku" unique="suffix">virtualsku</data> -+ <data key="price">10</data> -+ <data key="visibility">Catalog, Search</data> -+ <data key="urlKey" unique="suffix">virtualproduct</data> -+ <data key="type_id">virtual</data> -+ </entity> -+ <entity name="virtualProductBigQty" type="product"> -+ <data key="name" unique="suffix">VirtualProduct</data> -+ <data key="sku" unique="suffix">virtual_sku</data> -+ <data key="price">100.00</data> -+ <data key="productTaxClass">None</data> -+ <data key="quantity">999</data> -+ <data key="status">In Stock</data> -+ <data key="visibility">Catalog, Search</data> -+ <data key="urlKey" unique="suffix">virtual-product</data> -+ <data key="type_id">virtual</data> -+ </entity> -+ <entity name="virtualProductGeneralGroup" type="product"> -+ <data key="name" unique="suffix">VirtualProduct</data> -+ <data key="sku" unique="suffix">virtual_sku</data> -+ <data key="price">100.00</data> -+ <data key="productTaxClass">None</data> -+ <data key="quantity">999</data> -+ <data key="status">In Stock</data> -+ <data key="visibility">Catalog, Search</data> -+ <data key="urlKey" unique="suffix">virtual-product</data> -+ <data key="type_id">virtual</data> -+ </entity> -+ <entity name="virtualProductCustomImportOptions" type="product"> -+ <data key="name" unique="suffix">VirtualProduct</data> -+ <data key="sku" unique="suffix">virtual_sku</data> -+ <data key="price">9,000.00</data> -+ <data key="quantity">999</data> -+ <data key="status">In Stock</data> -+ <data key="visibility">Catalog, Search</data> -+ <data key="urlKey" unique="suffix">virtual-product</data> -+ <data key="storefrontStatus">IN STOCK</data> -+ <data key="type_id">virtual</data> -+ </entity> -+ <entity name="virtualProductWithoutManageStock" type="product"> -+ <data key="name" unique="suffix">VirtualProduct</data> -+ <data key="sku" unique="suffix">virtual_sku</data> -+ <data key="price">100.00</data> -+ <data key="quantity">999</data> -+ <data key="urlKey" unique="suffix">virtual-product</data> -+ <data key="special_price">90.00</data> -+ <data key="storefrontStatus">IN STOCK</data> -+ <data key="type_id">virtual</data> -+ </entity> -+ <entity name="virtualProductOutOfStock" type="product"> -+ <data key="name" unique="suffix">VirtualProduct</data> -+ <data key="sku" unique="suffix">virtual_sku</data> -+ <data key="price">9,000.00</data> -+ <data key="quantity">999</data> -+ <data key="status">Out of Stock</data> -+ <data key="visibility">Catalog, Search</data> -+ <data key="urlKey" unique="suffix">virtual-product</data> -+ <data key="storefrontStatus">OUT OF STOCK</data> -+ <data key="type_id">virtual</data> -+ </entity> -+ <entity name="virtualProductAssignToCategory" type="product"> -+ <data key="name" unique="suffix">VirtualProduct</data> -+ <data key="sku" unique="suffix">virtual_sku</data> -+ <data key="price">10.00</data> -+ <data key="quantity">999</data> -+ <data key="urlKey" unique="suffix">virtual-product</data> -+ <data key="type_id">virtual</data> -+ </entity> -+ <entity name="updateVirtualProductRegularPriceInStock" type="product"> -+ <data key="name" unique="suffix">VirtualProduct</data> -+ <data key="sku" unique="suffix">virtual_sku</data> -+ <data key="price">120.00</data> -+ <data key="productTaxClass">None</data> -+ <data key="quantity">999</data> -+ <data key="status">In Stock</data> -+ <data key="storefrontStatus">IN STOCK</data> -+ <data key="visibility">Search</data> -+ <data key="urlKey" unique="suffix">virtual-product</data> -+ <data key="type_id">virtual</data> -+ </entity> -+ <entity name="updateVirtualProductWithTierPriceInStock" type="product"> -+ <data key="name" unique="suffix">VirtualProduct</data> -+ <data key="sku" unique="suffix">virtual_sku</data> -+ <data key="price">99.99</data> -+ <data key="productTaxClass">None</data> -+ <data key="quantity">999</data> -+ <data key="status">In Stock</data> -+ <data key="storefrontStatus">IN STOCK</data> -+ <data key="visibility">Catalog</data> -+ <data key="urlKey" unique="suffix">virtual-product</data> -+ <data key="type_id">virtual</data> -+ </entity> -+ <entity name="updateVirtualProductRegularPrice99OutOfStock" type="product"> -+ <data key="name" unique="suffix">VirtualProduct</data> -+ <data key="sku" unique="suffix">virtual_sku</data> -+ <data key="price">99.99</data> -+ <data key="productTaxClass">Taxable Goods</data> -+ <data key="status">Out of Stock</data> -+ <data key="storefrontStatus">OUT OF STOCK</data> -+ <data key="visibility">Search</data> -+ <data key="urlKey" unique="suffix">virtual-product</data> -+ <data key="type_id">virtual</data> -+ </entity> -+ <entity name="defaultSimpleProduct" type="product"> -+ <data key="name" unique="suffix">Testp</data> -+ <data key="sku" unique="suffix">testsku</data> -+ <data key="type_id">simple</data> -+ <data key="attribute_set_id">4</data> -+ <data key="visibility">4</data> -+ <data key="price">560.00</data> -+ <data key="urlKey" unique="suffix">testurl-</data> -+ <data key="status">1</data> -+ <data key="quantity">25</data> -+ <data key="weight">1</data> -+ <requiredEntity type="product_extension_attribute">EavStock100</requiredEntity> -+ </entity> -+ <entity name="ProductWithLongNameSku" extends="ApiSimpleProduct"> -+ <data key="name" unique="suffix">Product With Long Name And Sku - But not too long</data> -+ <data key="sku" unique="suffix">Product With Long Name And Sku - But not too long</data> -+ </entity> -+ <entity name="PaginationProduct" type="product"> -+ <data key="name" unique="suffix">pagi</data> -+ <data key="sku" unique="suffix">pagisku</data> -+ <data key="type_id">simple</data> -+ <data key="attribute_set_id">4</data> -+ <data key="visibility">4</data> -+ <data key="price">780.00</data> -+ <data key="urlKey" unique="suffix">pagiurl-</data> -+ <data key="status">1</data> -+ <data key="quantity">50</data> -+ <data key="weight">5</data> -+ <requiredEntity type="product_extension_attribute">EavStock100</requiredEntity> -+ </entity> -+ <entity name="Magento3" type="image"> -+ <data key="title" unique="suffix">Magento3</data> -+ <data key="price">1.00</data> -+ <data key="file_type">Upload File</data> -+ <data key="shareable">Yes</data> -+ <data key="file">magento3.jpg</data> -+ <data key="filename">magento3</data> -+ <data key="file_extension">jpg</data> -+ </entity> -+ <entity name="updateVirtualProductRegularPrice" type="product"> -+ <data key="name" unique="suffix">VirtualProduct</data> -+ <data key="sku" unique="suffix">virtual_sku</data> -+ <data key="price">99.99</data> -+ <data key="productTaxClass">None</data> -+ <data key="quantity">999</data> -+ <data key="status">In Stock</data> -+ <data key="storefrontStatus">IN STOCK</data> -+ <data key="visibility">Catalog</data> -+ <data key="urlKey" unique="suffix">virtual-product</data> -+ <data key="type_id">virtual</data> -+ </entity> -+ <entity name="updateVirtualProductRegularPrice5OutOfStock" type="product"> -+ <data key="name" unique="suffix">VirtualProduct</data> -+ <data key="sku" unique="suffix">virtual_sku</data> -+ <data key="price">5.00</data> -+ <data key="productTaxClass">None</data> -+ <data key="status">Out of Stock</data> -+ <data key="storefrontStatus">OUT OF STOCK</data> -+ <data key="visibility">Catalog</data> -+ <data key="urlKey" unique="suffix">virtual-product</data> -+ <data key="type_id">virtual</data> -+ </entity> -+ <entity name="updateVirtualProductSpecialPrice" type="product"> -+ <data key="name" unique="suffix">VirtualProduct</data> -+ <data key="sku" unique="suffix">virtual_sku</data> -+ <data key="price">120.00</data> -+ <data key="productTaxClass">Taxable Goods</data> -+ <data key="quantity">999</data> -+ <data key="status">In Stock</data> -+ <data key="storefrontStatus">IN STOCK</data> -+ <data key="visibility">Catalog, Search</data> -+ <data key="urlKey" unique="suffix">virtual-product</data> -+ <data key="special_price">45.00</data> -+ <data key="type_id">virtual</data> -+ </entity> -+ <entity name="updateVirtualProductSpecialPriceOutOfStock" type="product"> -+ <data key="name" unique="suffix">VirtualProduct</data> -+ <data key="sku" unique="suffix">virtual_sku</data> -+ <data key="price">99.99</data> -+ <data key="productTaxClass">None</data> -+ <data key="status">Out of Stock</data> -+ <data key="storefrontStatus">OUT OF STOCK</data> -+ <data key="visibility">Catalog, Search</data> -+ <data key="urlKey" unique="suffix">virtual-product</data> -+ <data key="special_price">45.00</data> -+ <data key="type_id">virtual</data> -+ </entity> -+ <entity name="updateVirtualProductTierPriceInStock" type="product"> -+ <data key="name" unique="suffix">VirtualProduct</data> -+ <data key="sku" unique="suffix">virtual_sku</data> -+ <data key="price">145.00</data> -+ <data key="productTaxClass">Taxable Goods</data> -+ <data key="quantity">999</data> -+ <data key="status">In Stock</data> -+ <data key="storefrontStatus">IN STOCK</data> -+ <data key="visibility">Catalog, Search</data> -+ <data key="urlKey" unique="suffix">virtual-product</data> -+ <data key="type_id">virtual</data> -+ </entity> -+ <entity name="updateVirtualTierPriceOutOfStock" type="product"> -+ <data key="name" unique="suffix">VirtualProduct</data> -+ <data key="sku" unique="suffix">virtual_sku</data> -+ <data key="price">185.00</data> -+ <data key="productTaxClass">None</data> -+ <data key="quantity">999</data> -+ <data key="status">Out of Stock</data> -+ <data key="storefrontStatus">OUT OF STOCK</data> -+ <data key="visibility">Catalog, Search</data> -+ <data key="urlKey" unique="suffix">virtual-product</data> -+ <data key="type_id">virtual</data> -+ </entity> -+ <entity name="simpleProductRegularPrice325InStock" type="product"> -+ <data key="urlKey" unique="suffix">test-simple-product</data> -+ <data key="name" unique="suffix">TestSimpleProduct</data> -+ <data key="sku" unique="suffix">test_simple_product_sku</data> -+ <data key="price">325.02</data> -+ <data key="quantity">89</data> -+ <data key="status">In Stock</data> -+ <data key="storefrontStatus">IN STOCK</data> -+ <data key="weight">89.0000</data> -+ <data key="visibility">Search</data> -+ <data key="type_id">simple</data> -+ <requiredEntity type="product_extension_attribute">EavStock100</requiredEntity> -+ </entity> -+ <entity name="simpleProductRegularPrice32503OutOfStock" type="product"> -+ <data key="urlKey" unique="suffix">test-simple-product</data> -+ <data key="name" unique="suffix">TestSimpleProduct</data> -+ <data key="sku" unique="suffix">test_simple_product_sku</data> -+ <data key="price">325.03</data> -+ <data key="quantity">25</data> -+ <data key="status">Out of Stock</data> -+ <data key="storefrontStatus">OUT OF STOCK</data> -+ <data key="weight">125.0000</data> -+ <data key="type_id">simple</data> -+ <requiredEntity type="product_extension_attribute">EavStock100</requiredEntity> -+ </entity> -+ <entity name="simpleProductRegularPrice245InStock" type="product"> -+ <data key="urlKey" unique="suffix">test-simple-product</data> -+ <data key="name" unique="suffix">TestSimpleProduct</data> -+ <data key="sku" unique="suffix">test_simple_product_sku</data> -+ <data key="price">245.00</data> -+ <data key="quantity">200</data> -+ <data key="status">In Stock</data> -+ <data key="storefrontStatus">IN STOCK</data> -+ <data key="weight">120.0000</data> -+ <data key="visibility">Catalog, Search</data> -+ <data key="type_id">simple</data> -+ <requiredEntity type="product_extension_attribute">EavStock100</requiredEntity> -+ </entity> -+ <entity name="simpleProductRegularPrice32501InStock" type="product"> -+ <data key="urlKey" unique="suffix">test-simple-product</data> -+ <data key="name" unique="suffix">TestSimpleProduct</data> -+ <data key="sku" unique="suffix">test_simple_product_sku</data> -+ <data key="price">325.01</data> -+ <data key="quantity">125</data> -+ <data key="status">In Stock</data> -+ <data key="storefrontStatus">IN STOCK</data> -+ <data key="weight">25.0000</data> -+ <data key="visibility">Catalog</data> -+ <data key="type_id">simple</data> -+ <requiredEntity type="product_extension_attribute">EavStock100</requiredEntity> -+ </entity> -+ <entity name="simpleProductTierPrice300InStock" type="product"> -+ <data key="urlKey" unique="suffix">test-simple-product</data> -+ <data key="name" unique="suffix">TestSimpleProduct</data> -+ <data key="sku" unique="suffix">test_simple_product_sku</data> -+ <data key="price">300.00</data> -+ <data key="quantity">34</data> -+ <data key="status">In Stock</data> -+ <data key="storefrontStatus">IN STOCK</data> -+ <data key="weight">1</data> -+ <data key="weightSelect">This item has weight</data> -+ <data key="type_id">simple</data> -+ <requiredEntity type="product_extension_attribute">EavStock100</requiredEntity> -+ </entity> -+ <entity name="simpleProductEnabledFlat" type="product"> -+ <data key="urlKey" unique="suffix">test-simple-product</data> -+ <data key="name" unique="suffix">TestSimpleProduct</data> -+ <data key="sku" unique="suffix">test_simple_product_sku</data> -+ <data key="price">1.99</data> -+ <data key="productTaxClass">Taxable Goods</data> -+ <data key="quantity">1000</data> -+ <data key="status">1</data> -+ <data key="storefrontStatus">IN STOCK</data> -+ <data key="weight">1</data> -+ <data key="weightSelect">This item has weight</data> -+ <data key="visibility">Catalog, Search</data> -+ <data key="type_id">simple</data> -+ <requiredEntity type="product_extension_attribute">EavStock100</requiredEntity> -+ </entity> -+ <entity name="simpleProductRegularPriceCustomOptions" type="product"> -+ <data key="urlKey" unique="suffix">test-simple-product</data> -+ <data key="name" unique="suffix">TestSimpleProduct</data> -+ <data key="sku" unique="suffix">test_simple_product_sku</data> -+ <data key="price">245.00</data> -+ <data key="storefront_new_cartprice">343.00</data> -+ <data key="quantity">200</data> -+ <data key="status">In Stock</data> -+ <data key="storefrontStatus">IN STOCK</data> -+ <data key="weight">120.0000</data> -+ <data key="type_id">simple</data> -+ <requiredEntity type="product_extension_attribute">EavStock100</requiredEntity> -+ </entity> -+ <entity name="simpleProductDisabled" type="product"> -+ <data key="urlKey" unique="suffix">test-simple-product</data> -+ <data key="name" unique="suffix">TestSimpleProduct</data> -+ <data key="sku" unique="suffix">test_simple_product_sku</data> -+ <data key="price">74.00</data> -+ <data key="quantity">87</data> -+ <data key="status">In Stock</data> -+ <data key="weight">333.0000</data> -+ <data key="type_id">simple</data> -+ <requiredEntity type="product_extension_attribute">EavStock100</requiredEntity> -+ </entity> -+ <entity name="simpleProductNotVisibleIndividually" type="product"> -+ <data key="urlKey" unique="suffix">test-simple-product</data> -+ <data key="name" unique="suffix">TestSimpleProduct</data> -+ <data key="sku" unique="suffix">test_simple_product_sku</data> -+ <data key="price">325.00</data> -+ <data key="quantity">123</data> -+ <data key="status">In Stock</data> -+ <data key="weight">129.0000</data> -+ <data key="visibility">Not Visible Individually</data> -+ <data key="type_id">simple</data> -+ <requiredEntity type="product_extension_attribute">EavStock100</requiredEntity> -+ </entity> -+ <entity name="simpleProductDataOverriding" type="product"> -+ <data key="urlKey" unique="suffix">test-simple-product</data> -+ <data key="name" unique="suffix">TestSimpleProduct</data> -+ <data key="sku" unique="suffix">test_simple_product_sku</data> -+ <data key="price">9.99</data> -+ <data key="type_id">simple</data> -+ <requiredEntity type="product_extension_attribute">EavStock100</requiredEntity> -+ </entity> -+ <entity name="nameAndAttributeSkuMaskSimpleProduct" type="product"> -+ <data key="urlKey" unique="suffix">simple-product</data> -+ <data key="name" unique="suffix">SimpleProduct</data> -+ <data key="price">10000.00</data> -+ <data key="quantity">657</data> -+ <data key="weight">50</data> -+ <data key="country_of_manufacture">UA</data> -+ <data key="country_of_manufacture_label">Ukraine</data> -+ <data key="type_id">simple</data> -+ <data key="status">1</data> -+ <requiredEntity type="product_extension_attribute">EavStock100</requiredEntity> -+ </entity> -+ <entity name="ProductShortDescription" type="ProductAttribute"> -+ <data key="attribute_code">short_description</data> -+ </entity> -+ <entity name="AddToDefaultSetTopOfContentSection" type="ProductAttributeSet"> -+ <var key="attributeCode" entityKey="attribute_code" entityType="ProductAttribute"/> -+ <data key="attributeSetId">4</data> -+ <data key="attributeGroupId">13</data> -+ <data key="sortOrder">0</data> -+ </entity> -+ <entity name="productAlphabeticalA" type="product" extends="_defaultProduct"> -+ <data key="name" unique="suffix">AAA Product</data> -+ </entity> -+ <entity name="productAlphabeticalB" type="product" extends="_defaultProduct"> -+ <data key="name" unique="suffix">BBB Product</data> -+ </entity> -+ <entity name="productWithSpecialCharacters" type="product" extends="_defaultProduct"> -+ <data key="name" unique="suffix">Product "!@#$%^&*()+:;\|}{][?=~` </data> -+ <data key="nameWithSafeChars" unique="suffix">|}{][?=~` </data> -+ </entity> -+ <entity name="productWith130CharName" type="product" extends="_defaultProduct"> -+ <data key="name" unique="suffix">ProductWith128Chars 1234567891123456789112345678911234567891123456789112345678911234567891123456789112345678 endnums</data> -+ </entity> -+ <entity name="simpleProductDefault" type="product"> -+ <data key="sku" unique="suffix">sku_simple_product_</data> -+ <data key="type_id">simple</data> -+ <data key="attribute_set_id">4</data> -+ <data key="visibility">4</data> -+ <data key="name" unique="suffix">Simple Product </data> -+ <data key="price">560</data> -+ <data key="urlKey" unique="suffix">simple-product-</data> -+ <data key="status">1</data> -+ <data key="quantity">25</data> -+ <data key="weight">1</data> -+ <data key="product_has_weight">1</data> -+ <data key="is_in_stock">1</data> -+ <data key="tax_class_id">2</data> -+ <requiredEntity type="product_extension_attribute">EavStockItem</requiredEntity> -+ <requiredEntity type="custom_attribute_array">CustomAttributeCategoryIds</requiredEntity> -+ </entity> -+ <entity name="simpleProductWithoutCategory" type="product"> -+ <data key="sku" unique="suffix">sku_simple_product_</data> -+ <data key="type_id">simple</data> -+ <data key="attribute_set_id">4</data> -+ <data key="visibility">4</data> -+ <data key="name" unique="suffix">SimpleProduct</data> -+ <data key="price">560</data> -+ <data key="urlKey" unique="suffix">simple-product-</data> -+ <data key="status">1</data> -+ <data key="quantity">25</data> -+ <data key="weight">1</data> -+ <data key="product_has_weight">1</data> -+ <data key="is_in_stock">1</data> -+ <data key="tax_class_id">2</data> -+ <requiredEntity type="product_extension_attribute">EavStockItem</requiredEntity> -+ </entity> -+ <entity name="defaultSimpleProductWeight200" type="product"> -+ <data key="name" unique="suffix">Testp</data> -+ <data key="sku" unique="suffix">testsku</data> -+ <data key="type_id">simple</data> -+ <data key="attribute_set_id">4</data> -+ <data key="visibility">4</data> -+ <data key="price">560.00</data> -+ <data key="urlKey" unique="suffix">testurl-</data> -+ <data key="status">1</data> -+ <data key="quantity">25</data> -+ <data key="weight">200</data> -+ <requiredEntity type="product_extension_attribute">EavStock100</requiredEntity> -+ </entity> -+ <entity name="SimpleProductWithTwoOption" type="product"> -+ <var key="sku" entityType="product" entityKey="sku" /> -+ <requiredEntity type="product_option">ProductOptionMultiSelect</requiredEntity> -+ </entity> -+ <entity name="SimpleProductWithOption" type="product"> -+ <var key="sku" entityType="product" entityKey="sku" /> -+ <requiredEntity type="product_option">ProductOptionPercentPriceDropDown</requiredEntity> -+ </entity> -+ <entity name="SimpleProductWithDescription" type="product"> -+ <data key="sku" unique="suffix">productwithdescription</data> -+ <data key="type_id">simple</data> -+ <data key="attribute_set_id">4</data> -+ <data key="visibility">4</data> -+ <data key="name" unique="suffix">ProductWithDescription</data> -+ <data key="price">123.00</data> -+ <data key="urlKey" unique="suffix">productwithdescription</data> -+ <data key="status">1</data> -+ <data key="quantity">100</data> -+ <requiredEntity type="product_extension_attribute">EavStockItem</requiredEntity> -+ <requiredEntity type="custom_attribute_array">ApiProductDescription</requiredEntity> -+ </entity> -+ <entity name="SimpleProductWithSpecialPrice" type="product"> -+ <data key="sku" unique="suffix">SimpleProductWithSpecialPrice</data> -+ <data key="type_id">simple</data> -+ <data key="attribute_set_id">4</data> -+ <data key="name" unique="suffix">SimpleProduct</data> -+ <data key="price">100.00</data> -+ <data key="special_price">90.00</data> -+ <data key="visibility">4</data> -+ <data key="status">1</data> -+ <data key="quantity">86</data> -+ <data key="urlKey" unique="suffix">simpleproduct</data> -+ <data key="weight">1</data> -+ <requiredEntity type="product_extension_attribute">EavStockItem</requiredEntity> -+ <requiredEntity type="custom_attribute_array">CustomAttributeCategoryIds</requiredEntity> -+ </entity> -+ <entity name="SimpleProductWithSpecialPriceSecond" type="product"> -+ <data key="sku" unique="suffix">SimpleProductWithSpecialPriceSecond</data> -+ <data key="type_id">simple</data> -+ <data key="attribute_set_id">4</data> -+ <data key="name" unique="suffix">SimpleProduct</data> -+ <data key="price">150.00</data> -+ <data key="special_price">110.00</data> -+ <data key="visibility">4</data> -+ <data key="status">1</data> -+ <data key="quantity">86</data> -+ <data key="urlKey" unique="suffix">simpleproduct</data> -+ <data key="weight">1</data> -+ <requiredEntity type="product_extension_attribute">EavStockItem</requiredEntity> -+ <requiredEntity type="custom_attribute_array">CustomAttributeCategoryIds</requiredEntity> -+ </entity> -+ <entity name="SimpleProduct_100" type="product"> -+ <data key="sku" unique="suffix">testSku</data> -+ <data key="type_id">simple</data> -+ <data key="attribute_set_id">4</data> -+ <data key="visibility">4</data> -+ <data key="name" unique="suffix">testProductName</data> -+ <data key="price">100.00</data> -+ <data key="urlKey" unique="suffix">testurlkey</data> -+ <data key="status">1</data> -+ <data key="quantity">777</data> -+ <data key="weight">1</data> -+ <requiredEntity type="product_extension_attribute">EavStock777</requiredEntity> -+ <requiredEntity type="custom_attribute_array">CustomAttributeCategoryIds</requiredEntity> -+ </entity> -+ <entity name="ApiSimpleOneQty10" type="product2"> -+ <data key="sku" unique="suffix">api-simple-product</data> -+ <data key="type_id">simple</data> -+ <data key="attribute_set_id">4</data> -+ <data key="visibility">4</data> -+ <data key="name" unique="suffix">Api Simple Product</data> -+ <data key="price">40.00</data> -+ <data key="urlKey" unique="suffix">api-simple-product</data> -+ <data key="status">1</data> -+ <data key="quantity">10</data> -+ <requiredEntity type="product_extension_attribute">EavStock10</requiredEntity> -+ <requiredEntity type="custom_attribute">CustomAttributeProductAttribute</requiredEntity> -+ </entity> -+ <entity name="ApiSimpleTwoQty10" type="product2"> -+ <data key="sku" unique="suffix">api-simple-product-two</data> -+ <data key="type_id">simple</data> -+ <data key="attribute_set_id">4</data> -+ <data key="visibility">4</data> -+ <data key="name" unique="suffix">Api Simple Product Two</data> -+ <data key="price">40.00</data> -+ <data key="urlKey" unique="suffix">api-simple-product-two</data> -+ <data key="status">1</data> -+ <data key="quantity">10</data> -+ <requiredEntity type="product_extension_attribute">EavStock10</requiredEntity> -+ <requiredEntity type="custom_attribute">CustomAttributeProductAttribute</requiredEntity> -+ </entity> - </entities> -diff --git a/app/code/Magento/Catalog/Test/Mftf/Data/ProductDesignData.xml b/app/code/Magento/Catalog/Test/Mftf/Data/ProductDesignData.xml -new file mode 100644 -index 00000000000..42e85ab2698 ---- /dev/null -+++ b/app/code/Magento/Catalog/Test/Mftf/Data/ProductDesignData.xml -@@ -0,0 +1,21 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<entities xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataProfileSchema.xsd"> -+ <entity name="simpleBlankDesign" type="product"> -+ <data key="custom_design">Magento Blank</data> -+ <data key="page_layout">2 columns with left bar</data> -+ <data key="options_container">Product Info Column</data> -+ </entity> -+ <entity name="simpleLumaDesign" type="product"> -+ <data key="custom_design">Magento Luma</data> -+ <data key="page_layout">Empty</data> -+ <data key="options_container">Block after Info Column</data> -+ </entity> -+</entities> -diff --git a/app/code/Magento/Catalog/Test/Mftf/Data/ProductExtensionAttributeData.xml b/app/code/Magento/Catalog/Test/Mftf/Data/ProductExtensionAttributeData.xml -index 88ff2bbace4..5a6a0b5dd95 100644 ---- a/app/code/Magento/Catalog/Test/Mftf/Data/ProductExtensionAttributeData.xml -+++ b/app/code/Magento/Catalog/Test/Mftf/Data/ProductExtensionAttributeData.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="EavStockItem" type="product_extension_attribute"> - <requiredEntity type="stock_item">Qty_1000</requiredEntity> - </entity> -@@ -17,4 +17,13 @@ - <entity name="EavStock10" type="product_extension_attribute"> - <requiredEntity type="stock_item">Qty_10</requiredEntity> - </entity> -+ <entity name="EavStock1" type="product_extension_attribute"> -+ <requiredEntity type="stock_item">Qty_1</requiredEntity> -+ </entity> -+ <entity name="EavStock0" type="product_extension_attribute"> -+ <requiredEntity type="stock_item">Qty_0</requiredEntity> -+ </entity> -+ <entity name="EavStock777" type="product_extension_attribute"> -+ <requiredEntity type="stock_item">Qty_777</requiredEntity> -+ </entity> - </entities> -diff --git a/app/code/Magento/Catalog/Test/Mftf/Data/ProductGridData.xml b/app/code/Magento/Catalog/Test/Mftf/Data/ProductGridData.xml -index b123800a6cc..71c8af318e9 100644 ---- a/app/code/Magento/Catalog/Test/Mftf/Data/ProductGridData.xml -+++ b/app/code/Magento/Catalog/Test/Mftf/Data/ProductGridData.xml -@@ -7,9 +7,12 @@ - --> - - <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="PriceFilterRange" type="filter"> - <data key="from">10</data> - <data key="to">100</data> - </entity> -+ <entity name="ProductPerPage"> -+ <data key="productCount">1</data> -+ </entity> - </entities> -diff --git a/app/code/Magento/Catalog/Test/Mftf/Data/ProductLinkData.xml b/app/code/Magento/Catalog/Test/Mftf/Data/ProductLinkData.xml -new file mode 100644 -index 00000000000..000bb209500 ---- /dev/null -+++ b/app/code/Magento/Catalog/Test/Mftf/Data/ProductLinkData.xml -@@ -0,0 +1,19 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<entities xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataProfileSchema.xsd"> -+ <entity name="RelatedProductLink" type="product_link"> -+ <var key="sku" entityKey="sku" entityType="product2"/> -+ <var key="linked_product_sku" entityKey="sku" entityType="product"/> -+ <data key="link_type">related</data> -+ <data key="linked_product_type">simple</data> -+ <data key="position">1</data> -+ <requiredEntity type="product_link_extension_attribute">Qty1000</requiredEntity> -+ </entity> -+</entities> -diff --git a/app/code/Magento/Catalog/Test/Mftf/Data/ProductLinksData.xml b/app/code/Magento/Catalog/Test/Mftf/Data/ProductLinksData.xml -new file mode 100644 -index 00000000000..bd4f807880a ---- /dev/null -+++ b/app/code/Magento/Catalog/Test/Mftf/Data/ProductLinksData.xml -@@ -0,0 +1,14 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<entities xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataProfileSchema.xsd"> -+ <entity name="OneRelatedProductLink" type="product_links"> -+ <requiredEntity type="product_link">RelatedProductLink</requiredEntity> -+ </entity> -+</entities> -diff --git a/app/code/Magento/Catalog/Test/Mftf/Data/ProductOptionData.xml b/app/code/Magento/Catalog/Test/Mftf/Data/ProductOptionData.xml -index 903bf03535a..720087917aa 100644 ---- a/app/code/Magento/Catalog/Test/Mftf/Data/ProductOptionData.xml -+++ b/app/code/Magento/Catalog/Test/Mftf/Data/ProductOptionData.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="ProductOptionField" type="product_option"> - <var key="product_sku" entityType="product" entityKey="sku" /> - <data key="title">OptionField</data> -@@ -38,6 +38,16 @@ - <data key="price_type">percent</data> - <data key="max_characters">0</data> - </entity> -+ <entity name="ProductOptionAreaFixed" type="product_option"> -+ <var key="product_sku" entityType="product" entityKey="sku" /> -+ <data key="title">OptionArea</data> -+ <data key="type">area</data> -+ <data key="is_require">false</data> -+ <data key="sort_order">2</data> -+ <data key="price">100</data> -+ <data key="price_type">fixed</data> -+ <data key="max_characters">0</data> -+ </entity> - <entity name="ProductOptionFile" type="product_option"> - <var key="product_sku" entityType="product" entityKey="sku" /> - <data key="title">OptionFile</data> -@@ -59,6 +69,16 @@ - <requiredEntity type="product_option_value">ProductOptionValueDropdown1</requiredEntity> - <requiredEntity type="product_option_value">ProductOptionValueDropdown2</requiredEntity> - </entity> -+ <entity name="ProductOptionDropDown2" type="product_option"> -+ <var key="product_sku" entityType="product" entityKey="sku" /> -+ <data key="title">OptionDropDown</data> -+ <data key="type">drop_down</data> -+ <data key="sort_order">4</data> -+ <data key="is_require">false</data> -+ <requiredEntity type="product_option_value">ProductOptionValueDropdown1</requiredEntity> -+ <requiredEntity type="product_option_value">ProductOptionValueDropdown3</requiredEntity> -+ </entity> -+ - <entity name="ProductOptionDropDownWithLongValuesTitle" type="product_option"> - <var key="product_sku" entityType="product" entityKey="sku" /> - <data key="title">OptionDropDownWithLongTitles</data> -@@ -68,6 +88,15 @@ - <requiredEntity type="product_option_value">ProductOptionValueDropdownLongTitle1</requiredEntity> - <requiredEntity type="product_option_value">ProductOptionValueDropdownLongTitle2</requiredEntity> - </entity> -+ <entity name="ProductOptionValueDropdown" type="product_option"> -+ <var key="product_sku" entityType="product" entityKey="sku" /> -+ <data key="title">OptionDropDown</data> -+ <data key="type">drop_down</data> -+ <data key="sort_order">4</data> -+ <data key="is_require">true</data> -+ <requiredEntity type="product_option_value">ProductOptionValueWithSkuDropdown1</requiredEntity> -+ <requiredEntity type="product_option_value">ProductOptionValueWithSkuDropdown2</requiredEntity> -+ </entity> - <entity name="ProductOptionRadiobutton" type="product_option"> - <var key="product_sku" entityType="product" entityKey="sku" /> - <data key="title">OptionRadioButtons</data> -@@ -77,6 +106,24 @@ - <requiredEntity type="product_option_value">ProductOptionValueRadioButtons1</requiredEntity> - <requiredEntity type="product_option_value">ProductOptionValueRadioButtons2</requiredEntity> - </entity> -+ <entity name="ProductOptionRadioButton2" type="product_option"> -+ <var key="product_sku" entityType="product" entityKey="sku" /> -+ <data key="title">OptionRadioButtons</data> -+ <data key="type">radio</data> -+ <data key="sort_order">4</data> -+ <data key="is_require">false</data> -+ <requiredEntity type="product_option_value">ProductOptionValueRadioButtons1</requiredEntity> -+ <requiredEntity type="product_option_value">ProductOptionValueRadioButtons4</requiredEntity> -+ </entity> -+ <entity name="ProductOptionRadiobuttonWithTwoFixedOptions" type="product_option"> -+ <var key="product_sku" entityType="product" entityKey="sku" /> -+ <data key="title">OptionRadioButtons</data> -+ <data key="type">radio</data> -+ <data key="sort_order">5</data> -+ <data key="is_require">true</data> -+ <requiredEntity type="product_option_value">ProductOptionValueRadioButtons1</requiredEntity> -+ <requiredEntity type="product_option_value">ProductOptionValueRadioButtons3</requiredEntity> -+ </entity> - <entity name="ProductOptionCheckbox" type="product_option"> - <var key="product_sku" entityType="product" entityKey="sku" /> - <data key="title">OptionCheckbox</data> -@@ -121,4 +168,18 @@ - <data key="price">0.00</data> - <data key="price_type">percent</data> - </entity> -+ <entity name="ProductOptionPercentPriceDropDown" type="product_option"> -+ <var key="product_sku" entityType="product" entityKey="sku" /> -+ <data key="title">OptionDropDown</data> -+ <data key="type">drop_down</data> -+ <data key="sort_order">4</data> -+ <data key="is_require">true</data> -+ <requiredEntity type="product_option_value">ProductOptionPercentPriceValueDropdown</requiredEntity> -+ </entity> -+ <entity name="ProductOptionFieldSecond" extends="ProductOptionField"> -+ <data key="title" unique="suffix">fifth option</data> -+ </entity> -+ <entity name="ProductOptionFileSecond" extends="ProductOptionFile"> -+ <data key="title" unique="suffix">fourth option</data> -+ </entity> - </entities> -diff --git a/app/code/Magento/Catalog/Test/Mftf/Data/ProductOptionValueData.xml b/app/code/Magento/Catalog/Test/Mftf/Data/ProductOptionValueData.xml -index 28dd2553218..e7389943573 100644 ---- a/app/code/Magento/Catalog/Test/Mftf/Data/ProductOptionValueData.xml -+++ b/app/code/Magento/Catalog/Test/Mftf/Data/ProductOptionValueData.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="ProductOptionValueDropdown1" type="product_option_value"> - <data key="title">OptionValueDropDown1</data> - <data key="sort_order">1</data> -@@ -32,6 +32,24 @@ - <data key="price">99.99</data> - <data key="price_type">percent</data> - </entity> -+ <entity name="ProductOptionValueDropdown3" type="product_option_value"> -+ <data key="title">OptionValueDropDown3</data> -+ <data key="sort_order">2</data> -+ <data key="price">10</data> -+ <data key="price_type">percent</data> -+ </entity> -+ <entity name="ProductOptionValueRadioButtons4" type="product_option_value"> -+ <data key="title">OptionValueRadioButtons4</data> -+ <data key="sort_order">1</data> -+ <data key="price">9.99</data> -+ <data key="price_type">fixed</data> -+ </entity> -+ <entity name="ProductOptionValueRadioButtons3" type="product_option_value"> -+ <data key="title">OptionValueRadioButtons3</data> -+ <data key="sort_order">3</data> -+ <data key="price">10</data> -+ <data key="price_type">fixed</data> -+ </entity> - <entity name="ProductOptionValueCheckbox" type="product_option_value"> - <data key="title">OptionValueCheckbox</data> - <data key="sort_order">1</data> -@@ -62,4 +80,25 @@ - <data key="price">20</data> - <data key="price_type">percent</data> - </entity> -+ <entity name="ProductOptionPercentPriceValueDropdown" type="product_option_value"> -+ <data key="title">40 Percent</data> -+ <data key="sort_order">0</data> -+ <data key="price">40</data> -+ <data key="price_type">percent</data> -+ <data key="sku">sku_drop_down_row_1</data> -+ </entity> -+ <entity name="ProductOptionValueWithSkuDropdown1" type="product_option_value"> -+ <data key="title">ProductOptionValueWithSkuDropdown1</data> -+ <data key="sort_order">1</data> -+ <data key="price">10</data> -+ <data key="price_type">fixed</data> -+ <data key="sku">product_option_value_sku_dropdown_1_</data> -+ </entity> -+ <entity name="ProductOptionValueWithSkuDropdown2" type="product_option_value"> -+ <data key="title">ProductOptionValueWithSkuDropdown2</data> -+ <data key="sort_order">1</data> -+ <data key="price">10</data> -+ <data key="price_type">fixed</data> -+ <data key="sku">product_option_value_sku_dropdown_2_</data> -+ </entity> - </entities> -diff --git a/app/code/Magento/Catalog/Test/Mftf/Data/SimpleProductOptionData.xml b/app/code/Magento/Catalog/Test/Mftf/Data/SimpleProductOptionData.xml -new file mode 100644 -index 00000000000..157a4d41026 ---- /dev/null -+++ b/app/code/Magento/Catalog/Test/Mftf/Data/SimpleProductOptionData.xml -@@ -0,0 +1,20 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<entities xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataProfileSchema.xsd"> -+ <entity name="simpleProductCustomizableOption"> -+ <data key="title" unique="suffix">Test3 option</data> -+ <data key="type">Drop-down</data> -+ <data key="is_required">1</data> -+ <data key="option_0_title" unique="suffix">40 Percent</data> -+ <data key="option_0_price">40.00</data> -+ <data key="option_0_price_type">Percent</data> -+ <data key="option_0_sku" unique="suffix">sku_drop_down_row_1</data> -+ </entity> -+</entities> -\ No newline at end of file -diff --git a/app/code/Magento/Catalog/Test/Mftf/Data/StockItemData.xml b/app/code/Magento/Catalog/Test/Mftf/Data/StockItemData.xml -index 4fae51de86c..32f4dc1404d 100644 ---- a/app/code/Magento/Catalog/Test/Mftf/Data/StockItemData.xml -+++ b/app/code/Magento/Catalog/Test/Mftf/Data/StockItemData.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="Qty_1000" type="stock_item"> - <data key="qty">1000</data> - <data key="is_in_stock">true</data> -@@ -28,4 +28,16 @@ - <data key="qty">101</data> - <data key="is_in_stock">true</data> - </entity> -+ <entity name="Qty_1" type="stock_item"> -+ <data key="qty">1</data> -+ <data key="is_in_stock">true</data> -+ </entity> -+ <entity name="Qty_0" type="stock_item"> -+ <data key="qty">0</data> -+ <data key="is_in_stock">true</data> -+ </entity> -+ <entity name="Qty_777" type="stock_item"> -+ <data key="qty">777</data> -+ <data key="is_in_stock">true</data> -+ </entity> - </entities> -diff --git a/app/code/Magento/Catalog/Test/Mftf/Data/StoreLabelData.xml b/app/code/Magento/Catalog/Test/Mftf/Data/StoreLabelData.xml -index a703e56beda..0e51995ac72 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 @@ - --> - - <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="Option1Store0" type="StoreLabel"> - <data key="store_id">0</data> - <data key="label">option1</data> -@@ -56,4 +56,20 @@ - <data key="store_id">1</data> - <data key="label">option6</data> - </entity> -+ <entity name="Option7Store0" type="StoreLabel"> -+ <data key="store_id">0</data> -+ <data key="label">Green</data> -+ </entity> -+ <entity name="Option8Store1" type="StoreLabel"> -+ <data key="store_id">1</data> -+ <data key="label">Green</data> -+ </entity> -+ <entity name="Option9Store0" type="StoreLabel"> -+ <data key="store_id">0</data> -+ <data key="label">Red</data> -+ </entity> -+ <entity name="Option10Store1" type="StoreLabel"> -+ <data key="store_id">1</data> -+ <data key="label">Red</data> -+ </entity> - </entities> -diff --git a/app/code/Magento/Catalog/Test/Mftf/Data/TierPriceData.xml b/app/code/Magento/Catalog/Test/Mftf/Data/TierPriceData.xml -new file mode 100644 -index 00000000000..0c88c666a20 ---- /dev/null -+++ b/app/code/Magento/Catalog/Test/Mftf/Data/TierPriceData.xml -@@ -0,0 +1,67 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<entities xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataProfileSchema.xsd"> -+ <entity name="testDataTierPrice" type="data"> -+ <data key="goldenPrice1">$676.50</data> -+ <data key="goldenPrice2">$615.00</data> -+ </entity> -+ <entity name="customStoreTierPrice" type="data"> -+ <data key="name">secondStore</data> -+ <data key="code">second_store</data> -+ </entity> -+ <entity name="customStoreView" type="data"> -+ <data key="name">secondStoreView</data> -+ <data key="code">second_store_view</data> -+ </entity> -+ <entity name="tierPriceOnVirtualProduct" type="data"> -+ <data key="website">All Websites [USD]</data> -+ <data key="customer_group">ALL GROUPS</data> -+ <data key="price">90.00</data> -+ <data key="qty">2</data> -+ </entity> -+ <entity name="tierPriceOnGeneralGroup" type="data"> -+ <data key="website">All Websites [USD]</data> -+ <data key="customer_group">General</data> -+ <data key="price">80.00</data> -+ <data key="qty">2</data> -+ </entity> -+ <entity name="tierPriceOnDefault" type="data"> -+ <data key="website_0">All Websites [USD]</data> -+ <data key="customer_group_0">ALL GROUPS</data> -+ <data key="price_0">15.00</data> -+ <data key="qty_0">3</data> -+ <data key="website_1">All Websites [USD]</data> -+ <data key="customer_group_1">ALL GROUPS</data> -+ <data key="price_1">24.00</data> -+ <data key="qty_1">15</data> -+ </entity> -+ <entity name="tierPriceHighCostSimpleProduct" type="data"> -+ <data key="website">All Websites [USD]</data> -+ <data key="customer_group">ALL GROUPS</data> -+ <data key="price">500,000.00</data> -+ <data key="qty">1</data> -+ </entity> -+ <entity name="tierProductPrice" type="catalogTierPrice"> -+ <data key="price">90.00</data> -+ <data key="price_type">fixed</data> -+ <data key="website_id">0</data> -+ <data key="customer_group">ALL GROUPS</data> -+ <data key="quantity">2</data> -+ <var key="sku" entityType="product2" entityKey="sku" /> -+ </entity> -+ <entity name="tierProductPriceDefault" type="catalogTierPrice"> -+ <data key="price">90.00</data> -+ <data key="price_type">fixed</data> -+ <data key="website_id">0</data> -+ <data key="customer_group">ALL GROUPS</data> -+ <data key="quantity">30</data> -+ <var key="sku" entityType="product" entityKey="sku" /> -+ </entity> -+</entities> -\ No newline at end of file -diff --git a/app/code/Magento/Catalog/Test/Mftf/Data/VirtualProductOptionData.xml b/app/code/Magento/Catalog/Test/Mftf/Data/VirtualProductOptionData.xml -new file mode 100644 -index 00000000000..fe1d49e4daa ---- /dev/null -+++ b/app/code/Magento/Catalog/Test/Mftf/Data/VirtualProductOptionData.xml -@@ -0,0 +1,60 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<entities xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataProfileSchema.xsd"> -+ <entity name="virtualProductCustomizableOption1"> -+ <data key="title" unique="suffix">Test1 option </data> -+ <data key="is_required">1</data> -+ <data key="type">Field</data> -+ <data key="option_0_price">120.03</data> -+ <data key="option_0_price_type">Fixed</data> -+ <data key="option_0_sku" unique="suffix">sku1_</data> -+ <data key="option_0_max_characters">45</data> -+ </entity> -+ <entity name="virtualProductCustomizableOption2"> -+ <data key="title" unique="suffix">Test2 option </data> -+ <data key="is_required">1</data> -+ <data key="type">Field</data> -+ <data key="option_0_price">120.03</data> -+ <data key="option_0_price_type">Fixed</data> -+ <data key="option_0_sku" unique="suffix">sku2_</data> -+ <data key="option_0_max_characters">45</data> -+ </entity> -+ <entity name="virtualProductCustomizableOption3"> -+ <data key="title" unique="suffix">Test3 option </data> -+ <data key="is_required">1</data> -+ <data key="type">Drop-down</data> -+ <data key="option_0_title" unique="suffix">Test3-1 </data> -+ <data key="option_0_price">110.01</data> -+ <data key="option_0_expected_price">9,900.90</data> -+ <data key="option_0_price_type">Percent</data> -+ <data key="option_0_sku" unique="suffix">sku3-1_</data> -+ <data key="option_0_sort_order">0</data> -+ <data key="option_1_title" unique="suffix">Test3-2 </data> -+ <data key="option_1_price">210.02</data> -+ <data key="option_1_price_type">Fixed</data> -+ <data key="option_1_sku" unique="suffix">sku3-2_</data> -+ <data key="option_1_sort_order">1</data> -+ </entity> -+ <entity name="virtualProductCustomizableOption4"> -+ <data key="title" unique="suffix">Test4 option </data> -+ <data key="is_required">1</data> -+ <data key="type">Drop-down</data> -+ <data key="option_0_title" unique="suffix">Test4-1 </data> -+ <data key="option_0_price">10.01</data> -+ <data key="option_0_price_type">Percent</data> -+ <data key="option_0_sku" unique="suffix">sku4-1_</data> -+ <data key="option_0_sort_order">0</data> -+ <data key="option_1_title" unique="suffix">Test4-2 </data> -+ <data key="option_1_price">20.02</data> -+ <data key="option_1_price_type">Fixed</data> -+ <data key="option_1_sku" unique="suffix">sku4-2_</data> -+ <data key="option_1_sort_order">1</data> -+ </entity> -+</entities> -diff --git a/app/code/Magento/Catalog/Test/Mftf/Data/WYSIWYGConfigData.xml b/app/code/Magento/Catalog/Test/Mftf/Data/WYSIWYGConfigData.xml -new file mode 100644 -index 00000000000..7bb8cf5f4db ---- /dev/null -+++ b/app/code/Magento/Catalog/Test/Mftf/Data/WYSIWYGConfigData.xml -@@ -0,0 +1,23 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<entities xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataProfileSchema.xsd"> -+ <entity name="EnableWYSIWYG"> -+ <data key="path">cms/wysiwyg/enabled</data> -+ <data key="scope_id">0</data> -+ <data key="label">Yes</data> -+ <data key="value">enabled</data> -+ </entity> -+ <entity name="EnableTinyMCE4"> -+ <data key="path">cms/wysiwyg/editor</data> -+ <data key="scope_id">0</data> -+ <data key="label">Yes</data> -+ <data key="value">mage/adminhtml/wysiwyg/tiny_mce/tinymce4Adapter</data> -+ </entity> -+</entities> -diff --git a/app/code/Magento/Catalog/Test/Mftf/Data/WidgetsData.xml b/app/code/Magento/Catalog/Test/Mftf/Data/WidgetsData.xml -new file mode 100644 -index 00000000000..18564ff101f ---- /dev/null -+++ b/app/code/Magento/Catalog/Test/Mftf/Data/WidgetsData.xml -@@ -0,0 +1,35 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<entities xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataProfileSchema.xsd"> -+ <entity name="ProductLinkWidget" extends="ProductsListWidget"> -+ <data key="type">Catalog Product Link</data> -+ <data key="template">Product Link Block Template</data> -+ </entity> -+ <entity name="RecentlyComparedProductsWidget" type="widget"> -+ <data key="type">Recently Compared Products</data> -+ <data key="design_theme">Magento Luma</data> -+ <data key="name" unique="suffix">Recently Compared Products</data> -+ <array key="store_ids"> -+ <item>All Store Views</item> -+ </array> -+ <data key="display_on">All Pages</data> -+ <data key="container">Sidebar Additional</data> -+ </entity> -+ <entity name="RecentlyViewedProductsWidget" type="widget"> -+ <data key="type">Recently Viewed Products</data> -+ <data key="design_theme">Magento Luma</data> -+ <data key="name" unique="suffix">Recently Viewed Products</data> -+ <array key="store_ids"> -+ <item>All Store Views</item> -+ </array> -+ <data key="display_on">All Pages</data> -+ <data key="container">Sidebar Additional</data> -+ </entity> -+</entities> -diff --git a/app/code/Magento/Catalog/Test/Mftf/Metadata/catalog_attribute_set-meta.xml b/app/code/Magento/Catalog/Test/Mftf/Metadata/catalog_attribute_set-meta.xml -new file mode 100644 -index 00000000000..9ef7b507812 ---- /dev/null -+++ b/app/code/Magento/Catalog/Test/Mftf/Metadata/catalog_attribute_set-meta.xml -@@ -0,0 +1,25 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<operations xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataOperation.xsd"> -+ <operation name="AddCatalogAttributeToAttributeSet" dataType="CatalogAttributeSet" type="create" auth="adminOauth" url="/V1/products/attribute-sets" method="POST"> -+ <contentType>application/json</contentType> -+ <object key="attributeSet" dataType="CatalogAttributeSet"> -+ <field key="attribute_set_name">string</field> -+ <field key="sort_order">integer</field> -+ </object> -+ <field key="skeletonId">integer</field> -+ </operation> -+ <operation name="DeleteCatalogAttributeFromAttributeSet" dataType="CatalogAttributeSet" type="delete" auth="adminOauth" url="/V1/products/attribute-sets/{attribute_set_id}" method="DELETE"> -+ <contentType>application/json</contentType> -+ </operation> -+ <operation name="GetCatalogAttributesFromDefaultSet" dataType="CatalogAttributeSet" type="get" auth="adminOauth" url="/V1/products/attribute-sets/{attribute_set_id}" method="GET"> -+ <contentType>application/json</contentType> -+ </operation> -+</operations> -\ No newline at end of file -diff --git a/app/code/Magento/Catalog/Test/Mftf/Metadata/catalog_configuration-meta.xml b/app/code/Magento/Catalog/Test/Mftf/Metadata/catalog_configuration-meta.xml -new file mode 100644 -index 00000000000..b1f2b43220b ---- /dev/null -+++ b/app/code/Magento/Catalog/Test/Mftf/Metadata/catalog_configuration-meta.xml -@@ -0,0 +1,118 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+<operations xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataOperation.xsd"> -+ <operation name="CatalogStorefrontConfiguration" dataType="catalog_storefront_config" type="create" auth="adminFormKey" url="/admin/system_config/save/section/catalog/" method="POST"> -+ <object key="groups" dataType="catalog_storefront_config"> -+ <object key="frontend" dataType="catalog_storefront_config"> -+ <object key="fields" dataType="catalog_storefront_config"> -+ <object key="list_mode" dataType="list_mode"> -+ <field key="value">string</field> -+ </object> -+ <object key="grid_per_page_values" dataType="grid_per_page_values"> -+ <field key="value">string</field> -+ </object> -+ <object key="grid_per_page" dataType="grid_per_page"> -+ <field key="value">string</field> -+ </object> -+ <object key="list_per_page_values" dataType="list_per_page_values"> -+ <field key="value">string</field> -+ </object> -+ <object key="list_per_page" dataType="list_per_page"> -+ <field key="value">string</field> -+ </object> -+ <object key="default_sort_by" dataType="default_sort_by"> -+ <field key="value">string</field> -+ </object> -+ <object key="list_allow_all" dataType="list_allow_all"> -+ <field key="value">integer</field> -+ </object> -+ <object key="remember_pagination" dataType="remember_pagination"> -+ <field key="value">integer</field> -+ </object> -+ <object key="flat_catalog_category" dataType="flat_catalog_category"> -+ <field key="value">integer</field> -+ </object> -+ <object key="flat_catalog_product" dataType="flat_catalog_product"> -+ <field key="value">integer</field> -+ </object> -+ <object key="swatches_per_product" dataType="swatches_per_product"> -+ <field key="value">string</field> -+ </object> -+ <object key="show_swatches_in_product_list" dataType="show_swatches_in_product_list"> -+ <field key="value">integer</field> -+ </object> -+ </object> -+ </object> -+ </object> -+ </operation> -+ -+ <operation name="DefaultCatalogStorefrontConfiguration" dataType="default_catalog_storefront_config" type="create" auth="adminFormKey" url="/admin/system_config/save/section/catalog/" method="POST"> -+ <object key="groups" dataType="default_catalog_storefront_config"> -+ <object key="frontend" dataType="default_catalog_storefront_config"> -+ <object key="fields" dataType="default_catalog_storefront_config"> -+ <object key="list_mode" dataType="default_catalog_storefront_config"> -+ <object key="inherit" dataType="catalogStorefrontFlagZero"> -+ <field key="value">integer</field> -+ </object> -+ </object> -+ <object key="grid_per_page_values" dataType="default_catalog_storefront_config"> -+ <object key="inherit" dataType="catalogStorefrontFlagZero"> -+ <field key="value">integer</field> -+ </object> -+ </object> -+ <object key="grid_per_page" dataType="default_catalog_storefront_config"> -+ <object key="inherit" dataType="catalogStorefrontFlagZero"> -+ <field key="value">integer</field> -+ </object> -+ </object> -+ <object key="list_per_page_values" dataType="default_catalog_storefront_config"> -+ <object key="inherit" dataType="catalogStorefrontFlagZero"> -+ <field key="value">integer</field> -+ </object> -+ </object> -+ <object key="list_per_page" dataType="default_catalog_storefront_config"> -+ <object key="inherit" dataType="catalogStorefrontFlagZero"> -+ <field key="value">integer</field> -+ </object> -+ </object> -+ <object key="default_sort_by" dataType="default_catalog_storefront_config"> -+ <object key="inherit" dataType="catalogStorefrontFlagZero"> -+ <field key="value">integer</field> -+ </object> -+ </object> -+ <object key="remember_pagination" dataType="default_catalog_storefront_config"> -+ <object key="inherit" dataType="catalogStorefrontFlagZero"> -+ <field key="value">integer</field> -+ </object> -+ </object> -+ <object key="flat_catalog_category" dataType="default_catalog_storefront_config"> -+ <object key="inherit" dataType="catalogStorefrontFlagZero"> -+ <field key="value">integer</field> -+ </object> -+ </object> -+ <object key="swatches_per_product" dataType="default_catalog_storefront_config"> -+ <object key="inherit" dataType="catalogStorefrontFlagZero"> -+ <field key="value">integer</field> -+ </object> -+ </object> -+ <object key="show_swatches_in_product_list" dataType="default_catalog_storefront_config"> -+ <object key="inherit" dataType="catalogStorefrontFlagZero"> -+ <field key="value">integer</field> -+ </object> -+ </object> -+ <object key="list_allow_all" dataType="list_allow_all"> -+ <field key="value">integer</field> -+ </object> -+ <object key="flat_catalog_product" dataType="flat_catalog_product"> -+ <field key="value">integer</field> -+ </object> -+ </object> -+ </object> -+ </object> -+ </operation> -+</operations> -diff --git a/app/code/Magento/Catalog/Test/Mftf/Metadata/catalog_price-meta.xml b/app/code/Magento/Catalog/Test/Mftf/Metadata/catalog_price-meta.xml -new file mode 100644 -index 00000000000..1ee57c89b2b ---- /dev/null -+++ b/app/code/Magento/Catalog/Test/Mftf/Metadata/catalog_price-meta.xml -@@ -0,0 +1,25 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<operations xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataOperation.xsd"> -+ <operation name="CatalogPriceConfigState" dataType="catalog_price_config_state" type="create" auth="adminFormKey" url="/admin/system_config/save/section/catalog/" method="POST"> -+ <object key="groups" dataType="catalog_price_config_state"> -+ <object key="price" dataType="catalog_price_config_state"> -+ <object key="fields" dataType="catalog_price_config_state"> -+ <object key="scope" dataType="scope"> -+ <field key="value">string</field> -+ </object> -+ <object key="default_product_price" dataType="default_product_price"> -+ <field key="value">string</field> -+ </object> -+ </object> -+ </object> -+ </object> -+ </operation> -+</operations> -diff --git a/app/code/Magento/Catalog/Test/Mftf/Metadata/catalog_recently_products-meta.xml b/app/code/Magento/Catalog/Test/Mftf/Metadata/catalog_recently_products-meta.xml -new file mode 100644 -index 00000000000..0fe4f154d5e ---- /dev/null -+++ b/app/code/Magento/Catalog/Test/Mftf/Metadata/catalog_recently_products-meta.xml -@@ -0,0 +1,21 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+<operations xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataOperation.xsd"> -+ <operation name="CatalogRecentlyProductsConfiguration" dataType="catalog_recently_products" type="create" -+ auth="adminFormKey" url="/admin/system_config/save/section/catalog/" method="POST" successRegex="/messages-message-success/"> -+ <object key="groups" dataType="catalog_recently_products"> -+ <object key="recently_products" dataType="catalog_recently_products"> -+ <object key="fields" dataType="catalog_recently_products"> -+ <object key="synchronize_with_backend" dataType="synchronize_with_backend"> -+ <field key="value">integer</field> -+ </object> -+ </object> -+ </object> -+ </object> -+ </operation> -+</operations> -diff --git a/app/code/Magento/Catalog/Test/Mftf/Metadata/catalog_special_price-meta.xml b/app/code/Magento/Catalog/Test/Mftf/Metadata/catalog_special_price-meta.xml -new file mode 100644 -index 00000000000..354277ad056 ---- /dev/null -+++ b/app/code/Magento/Catalog/Test/Mftf/Metadata/catalog_special_price-meta.xml -@@ -0,0 +1,22 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<operations xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataOperation.xsd"> -+ <operation name="catalogSpecialPrice" dataType="catalogSpecialPrice" type="create" auth="adminOauth" url="/V1/products/special-price" method="POST"> -+ <contentType>application/json</contentType> -+ <object key="prices" dataType="catalogSpecialPrice"> -+ <object dataType="catalogSpecialPrice" key="0"> -+ <field key="price">number</field> -+ <field key="store_id">integer</field> -+ <field key="sku">string</field> -+ <field key="price_from">string</field> -+ <field key="price_to">string</field> -+ </object> -+ </object> -+ </operation> -+</operations> -diff --git a/app/code/Magento/Catalog/Test/Mftf/Metadata/catalog_tier_price-meta.xml b/app/code/Magento/Catalog/Test/Mftf/Metadata/catalog_tier_price-meta.xml -new file mode 100644 -index 00000000000..7aa7530b0fd ---- /dev/null -+++ b/app/code/Magento/Catalog/Test/Mftf/Metadata/catalog_tier_price-meta.xml -@@ -0,0 +1,23 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<operations xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataOperation.xsd"> -+ <operation name="catalogTierPrice" dataType="catalogTierPrice" type="create" auth="adminOauth" url="/V1/products/tier-prices" method="POST"> -+ <contentType>application/json</contentType> -+ <object key="prices" dataType="catalogTierPrice"> -+ <object dataType="catalogTierPrice" key="0"> -+ <field key="price">number</field> -+ <field key="price_type">string</field> -+ <field key="website_id">integer</field> -+ <field key="sku">string</field> -+ <field key="customer_group">string</field> -+ <field key="quantity">integer</field> -+ </object> -+ </object> -+ </operation> -+</operations> -diff --git a/app/code/Magento/Catalog/Test/Mftf/Metadata/category-meta.xml b/app/code/Magento/Catalog/Test/Mftf/Metadata/category-meta.xml -index 0880315db5d..ae491aefc10 100644 ---- a/app/code/Magento/Catalog/Test/Mftf/Metadata/category-meta.xml -+++ b/app/code/Magento/Catalog/Test/Mftf/Metadata/category-meta.xml -@@ -6,7 +6,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"> -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataOperation.xsd"> - <operation name="CreateCategory" dataType="category" type="create" auth="adminOauth" url="/V1/categories" method="POST"> - <contentType>application/json</contentType> - <object key="category" dataType="category"> -diff --git a/app/code/Magento/Catalog/Test/Mftf/Metadata/custom_attribute-meta.xml b/app/code/Magento/Catalog/Test/Mftf/Metadata/custom_attribute-meta.xml -index aed9b7a9798..a37bb36eb65 100644 ---- a/app/code/Magento/Catalog/Test/Mftf/Metadata/custom_attribute-meta.xml -+++ b/app/code/Magento/Catalog/Test/Mftf/Metadata/custom_attribute-meta.xml -@@ -7,7 +7,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"> -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataOperation.xsd"> - <operation name="CreateCustomAttribute" dataType="custom_attribute" type="create"> - <field key="attribute_code">string</field> - <field key="value">string</field> -diff --git a/app/code/Magento/Catalog/Test/Mftf/Metadata/empty_extension_attribute-meta.xml b/app/code/Magento/Catalog/Test/Mftf/Metadata/empty_extension_attribute-meta.xml -index d8410593cb5..7faac6c3b6d 100644 ---- a/app/code/Magento/Catalog/Test/Mftf/Metadata/empty_extension_attribute-meta.xml -+++ b/app/code/Magento/Catalog/Test/Mftf/Metadata/empty_extension_attribute-meta.xml -@@ -7,7 +7,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"> -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataOperation.xsd"> - <operation name="CreateEmptyExtensionAttribute" dataType="empty_extension_attribute" type="create"> - </operation> - <operation name="UpdateEmptyExtensionAttribute" dataType="empty_extension_attribute" type="update"> -diff --git a/app/code/Magento/Catalog/Test/Mftf/Metadata/frontend_label-meta.xml b/app/code/Magento/Catalog/Test/Mftf/Metadata/frontend_label-meta.xml -index d0bcbd3e5db..063b8c2e5ac 100644 ---- a/app/code/Magento/Catalog/Test/Mftf/Metadata/frontend_label-meta.xml -+++ b/app/code/Magento/Catalog/Test/Mftf/Metadata/frontend_label-meta.xml -@@ -7,7 +7,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"> -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataOperation.xsd"> - <operation name="CreateFrontendLabel" dataType="FrontendLabel" type="create"> - <field key="store_id">integer</field> - <field key="label">string</field> -diff --git a/dev/tests/acceptance/tests/functional/Magento/FunctionalTest/Framework/Metadata/image_content-meta.xml b/app/code/Magento/Catalog/Test/Mftf/Metadata/image_content-meta.xml -similarity index 100% -rename from dev/tests/acceptance/tests/functional/Magento/FunctionalTest/Framework/Metadata/image_content-meta.xml -rename to app/code/Magento/Catalog/Test/Mftf/Metadata/image_content-meta.xml -diff --git a/app/code/Magento/Catalog/Test/Mftf/Metadata/product-meta.xml b/app/code/Magento/Catalog/Test/Mftf/Metadata/product-meta.xml -index 212de2b39d3..9ece47c01fc 100644 ---- a/app/code/Magento/Catalog/Test/Mftf/Metadata/product-meta.xml -+++ b/app/code/Magento/Catalog/Test/Mftf/Metadata/product-meta.xml -@@ -7,7 +7,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"> -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataOperation.xsd"> - <operation name="CreateProduct" dataType="product" type="create" auth="adminOauth" url="/V1/products" method="POST"> - <contentType>application/json</contentType> - <object dataType="product" key="product"> -diff --git a/app/code/Magento/Catalog/Test/Mftf/Metadata/product_attribute-meta.xml b/app/code/Magento/Catalog/Test/Mftf/Metadata/product_attribute-meta.xml -index 93396352ba5..1e9aa3bc219 100644 ---- a/app/code/Magento/Catalog/Test/Mftf/Metadata/product_attribute-meta.xml -+++ b/app/code/Magento/Catalog/Test/Mftf/Metadata/product_attribute-meta.xml -@@ -7,7 +7,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"> -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataOperation.xsd"> - <operation name="CreateProductAttribute" dataType="ProductAttribute" type="create" auth="adminOauth" url="/V1/products/attributes" method="POST"> - <contentType>application/json</contentType> - <object dataType="ProductAttribute" key="attribute"> -diff --git a/app/code/Magento/Catalog/Test/Mftf/Metadata/product_attribute_media_gallery_entry-meta.xml b/app/code/Magento/Catalog/Test/Mftf/Metadata/product_attribute_media_gallery_entry-meta.xml -index 8033e8c33a3..521e864702e 100644 ---- a/app/code/Magento/Catalog/Test/Mftf/Metadata/product_attribute_media_gallery_entry-meta.xml -+++ b/app/code/Magento/Catalog/Test/Mftf/Metadata/product_attribute_media_gallery_entry-meta.xml -@@ -7,7 +7,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"> -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataOperation.xsd"> - <operation name="CreateProductAttributeMediaGalleryEntry" dataType="ProductAttributeMediaGalleryEntry" type="create" auth="adminOauth" url="/V1/products/{sku}/media" method="POST"> - <contentType>application/json</contentType> - <object key="entry" dataType="ProductAttributeMediaGalleryEntry"> -diff --git a/app/code/Magento/Catalog/Test/Mftf/Metadata/product_attribute_option-meta.xml b/app/code/Magento/Catalog/Test/Mftf/Metadata/product_attribute_option-meta.xml -index 176afa8d58d..467ff9a48eb 100644 ---- a/app/code/Magento/Catalog/Test/Mftf/Metadata/product_attribute_option-meta.xml -+++ b/app/code/Magento/Catalog/Test/Mftf/Metadata/product_attribute_option-meta.xml -@@ -7,7 +7,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"> -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataOperation.xsd"> - <operation name="CreateProductAttributeOption" dataType="ProductAttributeOption" type="create" auth="adminOauth" url="/V1/products/attributes/{attribute_code}/options" method="POST"> - <contentType>application/json</contentType> - <object dataType="ProductAttributeOption" key="option"> -diff --git a/app/code/Magento/Catalog/Test/Mftf/Metadata/product_attribute_set-meta.xml b/app/code/Magento/Catalog/Test/Mftf/Metadata/product_attribute_set-meta.xml -index eef82b07aaf..6f04c48e792 100644 ---- a/app/code/Magento/Catalog/Test/Mftf/Metadata/product_attribute_set-meta.xml -+++ b/app/code/Magento/Catalog/Test/Mftf/Metadata/product_attribute_set-meta.xml -@@ -7,7 +7,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"> -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataOperation.xsd"> - <operation name="AddProductAttributeToAttributeSet" dataType="ProductAttributeSet" type="create" auth="adminOauth" url="/V1/products/attribute-sets/attributes" method="POST"> - <contentType>application/json</contentType> - <field key="attributeSetId">integer</field> -diff --git a/app/code/Magento/Catalog/Test/Mftf/Metadata/product_extension_attribute-meta.xml b/app/code/Magento/Catalog/Test/Mftf/Metadata/product_extension_attribute-meta.xml -index 8d0d1e66c81..127a754c888 100644 ---- a/app/code/Magento/Catalog/Test/Mftf/Metadata/product_extension_attribute-meta.xml -+++ b/app/code/Magento/Catalog/Test/Mftf/Metadata/product_extension_attribute-meta.xml -@@ -7,7 +7,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"> -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataOperation.xsd"> - <operation name="CreateProductExtensionAttribute" dataType="product_extension_attribute" type="create"> - <field key="stock_item">stock_item</field> - </operation> -diff --git a/app/code/Magento/Catalog/Test/Mftf/Metadata/product_link-meta.xml b/app/code/Magento/Catalog/Test/Mftf/Metadata/product_link-meta.xml -index 5e631b2ea3a..a2fcbb1417d 100644 ---- a/app/code/Magento/Catalog/Test/Mftf/Metadata/product_link-meta.xml -+++ b/app/code/Magento/Catalog/Test/Mftf/Metadata/product_link-meta.xml -@@ -7,7 +7,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"> -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataOperation.xsd"> - <operation name="CreateProductLink" dataType="product_link" type="create"> - <field key="sku">string</field> - <field key="link_type">string</field> -diff --git a/app/code/Magento/Catalog/Test/Mftf/Metadata/product_link_extension_attribute-meta.xml b/app/code/Magento/Catalog/Test/Mftf/Metadata/product_link_extension_attribute-meta.xml -index 07ea02f5b7a..90888463ef8 100644 ---- a/app/code/Magento/Catalog/Test/Mftf/Metadata/product_link_extension_attribute-meta.xml -+++ b/app/code/Magento/Catalog/Test/Mftf/Metadata/product_link_extension_attribute-meta.xml -@@ -7,7 +7,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"> -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataOperation.xsd"> - <operation name="CreateProductLinkExtensionAttribute" dataType="product_link_extension_attribute" type="create"> - <contentType>application/json</contentType> - <field key="qty">integer</field> -diff --git a/app/code/Magento/Catalog/Test/Mftf/Metadata/product_links-meta.xml b/app/code/Magento/Catalog/Test/Mftf/Metadata/product_links-meta.xml -index 56b3ee25ef7..450ea99b9d0 100644 ---- a/app/code/Magento/Catalog/Test/Mftf/Metadata/product_links-meta.xml -+++ b/app/code/Magento/Catalog/Test/Mftf/Metadata/product_links-meta.xml -@@ -7,7 +7,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"> -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataOperation.xsd"> - <operation name="CreateProductLinks" dataType="product_links" type="create" auth="adminOauth" url="/V1/products/{sku}/links" method="POST"> - <contentType>application/json</contentType> - <array key="items"> -diff --git a/app/code/Magento/Catalog/Test/Mftf/Metadata/product_option-meta.xml b/app/code/Magento/Catalog/Test/Mftf/Metadata/product_option-meta.xml -index adc5a33507a..6464c2988ad 100644 ---- a/app/code/Magento/Catalog/Test/Mftf/Metadata/product_option-meta.xml -+++ b/app/code/Magento/Catalog/Test/Mftf/Metadata/product_option-meta.xml -@@ -7,7 +7,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"> -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataOperation.xsd"> - <operation name="CreateProductOption" dataType="product_option" type="create"> - <field key="product_sku">string</field> - <field key="option_id">integer</field> -diff --git a/app/code/Magento/Catalog/Test/Mftf/Metadata/product_option_value-meta.xml b/app/code/Magento/Catalog/Test/Mftf/Metadata/product_option_value-meta.xml -index f4273f57968..bce77bc3a26 100644 ---- a/app/code/Magento/Catalog/Test/Mftf/Metadata/product_option_value-meta.xml -+++ b/app/code/Magento/Catalog/Test/Mftf/Metadata/product_option_value-meta.xml -@@ -7,7 +7,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"> -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataOperation.xsd"> - <operation name="CreateProductOptionValue" dataType="product_option_value" type="create"> - <field key="title">string</field> - <field key="sort_order">integer</field> -diff --git a/app/code/Magento/Catalog/Test/Mftf/Metadata/stock_item-meta.xml b/app/code/Magento/Catalog/Test/Mftf/Metadata/stock_item-meta.xml -index e7e79d69055..6ec5f2c8051 100644 ---- a/app/code/Magento/Catalog/Test/Mftf/Metadata/stock_item-meta.xml -+++ b/app/code/Magento/Catalog/Test/Mftf/Metadata/stock_item-meta.xml -@@ -7,7 +7,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"> -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataOperation.xsd"> - <operation name="CreateStockItem" dataType="stock_item" type="create"> - <field key="qty">integer</field> - <field key="is_in_stock">boolean</field> -diff --git a/app/code/Magento/Catalog/Test/Mftf/Metadata/store_label-meta.xml b/app/code/Magento/Catalog/Test/Mftf/Metadata/store_label-meta.xml -index abb9b003dc5..584ba5eebb5 100644 ---- a/app/code/Magento/Catalog/Test/Mftf/Metadata/store_label-meta.xml -+++ b/app/code/Magento/Catalog/Test/Mftf/Metadata/store_label-meta.xml -@@ -7,7 +7,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"> -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataOperation.xsd"> - <operation name="CreateStoreLabel" dataType="StoreLabel" type="create"> - <field key="store_id">integer</field> - <field key="label">string</field> -diff --git a/app/code/Magento/Catalog/Test/Mftf/Metadata/validation_rule-meta.xml b/app/code/Magento/Catalog/Test/Mftf/Metadata/validation_rule-meta.xml -index c568e52b2ab..aa120491ece 100644 ---- a/app/code/Magento/Catalog/Test/Mftf/Metadata/validation_rule-meta.xml -+++ b/app/code/Magento/Catalog/Test/Mftf/Metadata/validation_rule-meta.xml -@@ -7,7 +7,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"> -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataOperation.xsd"> - <operation name="CreateValidationRule" dataType="validation_rule" type="create"> - <field key="key">string</field> - <field key="value">string</field> -diff --git a/app/code/Magento/Catalog/Test/Mftf/Page/AdminCategoryEditPage.xml b/app/code/Magento/Catalog/Test/Mftf/Page/AdminCategoryEditPage.xml -index cfefa8cb2c4..e1c8e5c75e9 100644 ---- a/app/code/Magento/Catalog/Test/Mftf/Page/AdminCategoryEditPage.xml -+++ b/app/code/Magento/Catalog/Test/Mftf/Page/AdminCategoryEditPage.xml -@@ -7,7 +7,7 @@ - --> - - <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="AdminCategoryEditPage" url="catalog/category/edit/id/{{categoryId}}/" area="admin" module="Catalog" parameterized="true"> - <section name="AdminCategorySidebarActionSection"/> - <section name="AdminCategoryMainActionsSection"/> -diff --git a/app/code/Magento/Catalog/Test/Mftf/Page/AdminCategoryPage.xml b/app/code/Magento/Catalog/Test/Mftf/Page/AdminCategoryPage.xml -index 7cabe0e18f0..f7d8abf8b2f 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 @@ - --> - - <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="AdminCategoryPage" url="catalog/category/" area="admin" module="Catalog"> -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/PageObject.xsd"> -+ <page name="AdminCategoryPage" url="catalog/category/" area="admin" module="Magento_Catalog"> - <section name="AdminCategorySidebarActionSection"/> - <section name="AdminCategoryMainActionsSection"/> - <section name="AdminCategorySidebarTreeSection"/> -diff --git a/app/code/Magento/Catalog/Test/Mftf/Page/AdminNewWidgetPage.xml b/app/code/Magento/Catalog/Test/Mftf/Page/AdminNewWidgetPage.xml -new file mode 100644 -index 00000000000..dd5d5aef08a ---- /dev/null -+++ b/app/code/Magento/Catalog/Test/Mftf/Page/AdminNewWidgetPage.xml -@@ -0,0 +1,15 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<pages xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/PageObject.xsd"> -+ <page name="AdminNewWidgetPage" url="admin/widget_instance/new/" area="admin" module="Magento_Widget"> -+ <section name="AdminNewWidgetSelectProductPopupSection"/> -+ <section name="AdminCatalogProductWidgetSection"/> -+ </page> -+</pages> -diff --git a/app/code/Magento/Catalog/Test/Mftf/Page/AdminProductAttributeFormPage.xml b/app/code/Magento/Catalog/Test/Mftf/Page/AdminProductAttributeFormPage.xml -index b04aff5f161..fab87f90f86 100644 ---- a/app/code/Magento/Catalog/Test/Mftf/Page/AdminProductAttributeFormPage.xml -+++ b/app/code/Magento/Catalog/Test/Mftf/Page/AdminProductAttributeFormPage.xml -@@ -6,7 +6,7 @@ - */ - --> - <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="ProductAttributePage" url="catalog/product_attribute/new/" area="admin" module="Catalog"> - <section name="AdminCreateProductAttributeSection"/> - </page> -diff --git a/app/code/Magento/Catalog/Test/Mftf/Page/AdminProductAttributeGridPage.xml b/app/code/Magento/Catalog/Test/Mftf/Page/AdminProductAttributeGridPage.xml -index a5de7453d9c..e6aafa53601 100644 ---- a/app/code/Magento/Catalog/Test/Mftf/Page/AdminProductAttributeGridPage.xml -+++ b/app/code/Magento/Catalog/Test/Mftf/Page/AdminProductAttributeGridPage.xml -@@ -6,7 +6,7 @@ - */ - --> - <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="AdminProductAttributeGridPage" url="catalog/product_attribute" area="admin" module="Catalog"> - <section name="AdminProductAttributeGridSection"/> - </page> -diff --git a/app/code/Magento/Catalog/Test/Mftf/Page/AdminProductAttributeSetEditPage.xml b/app/code/Magento/Catalog/Test/Mftf/Page/AdminProductAttributeSetEditPage.xml -index 4034f2ab075..3e89cbc8262 100644 ---- a/app/code/Magento/Catalog/Test/Mftf/Page/AdminProductAttributeSetEditPage.xml -+++ b/app/code/Magento/Catalog/Test/Mftf/Page/AdminProductAttributeSetEditPage.xml -@@ -6,7 +6,7 @@ - */ - --> - <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="AdminProductAttributeSetEditPage" url="catalog/product_set/edit/id" area="admin" module="Catalog"> - <section name="AdminProductAttributeSetEditSection"/> - </page> -diff --git a/app/code/Magento/Catalog/Test/Mftf/Page/AdminProductAttributeSetGridPage.xml b/app/code/Magento/Catalog/Test/Mftf/Page/AdminProductAttributeSetGridPage.xml -index 0d879768eb4..d55e71adca2 100644 ---- a/app/code/Magento/Catalog/Test/Mftf/Page/AdminProductAttributeSetGridPage.xml -+++ b/app/code/Magento/Catalog/Test/Mftf/Page/AdminProductAttributeSetGridPage.xml -@@ -6,7 +6,7 @@ - */ - --> - <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="AdminProductAttributeSetGridPage" url="catalog/product_set/" area="admin" module="ProductAttributeSet"> - <section name="AdminProductAttributeSetGridSection"/> - </page> -diff --git a/app/code/Magento/Catalog/Test/Mftf/Page/AdminProductAttributesEditPage.xml b/app/code/Magento/Catalog/Test/Mftf/Page/AdminProductAttributesEditPage.xml -index 4918041d2cd..66475a93b75 100644 ---- a/app/code/Magento/Catalog/Test/Mftf/Page/AdminProductAttributesEditPage.xml -+++ b/app/code/Magento/Catalog/Test/Mftf/Page/AdminProductAttributesEditPage.xml -@@ -6,7 +6,7 @@ - */ - --> - <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="ProductAttributesEditPage" url="catalog/product_action_attribute/edit/" area="admin" module="Catalog"> - <section name="AdminEditProductAttributesSection"/> - </page> -diff --git a/app/code/Magento/Catalog/Test/Mftf/Page/AdminProductCreatePage.xml b/app/code/Magento/Catalog/Test/Mftf/Page/AdminProductCreatePage.xml -index be7c44e378f..e4c4ece5ac6 100644 ---- a/app/code/Magento/Catalog/Test/Mftf/Page/AdminProductCreatePage.xml -+++ b/app/code/Magento/Catalog/Test/Mftf/Page/AdminProductCreatePage.xml -@@ -7,7 +7,7 @@ - --> - - <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="AdminProductCreatePage" url="catalog/product/new/set/{{set}}/type/{{type}}/" area="admin" module="Magento_Catalog" parameterized="true"> - <section name="AdminProductFormSection"/> - <section name="AdminProductFormActionSection"/> -@@ -15,7 +15,10 @@ - <section name="AdminProductImagesSection"/> - <section name="AdminAddProductsToOptionPanel"/> - <section name="AdminProductMessagesSection"/> -+ <section name="AdminProductAttributesSection"/> - <section name="AdminProductFormRelatedUpSellCrossSellSection"/> - <section name="AdminProductFormAdvancedPricingSection"/> -+ <section name="AdminProductFormAdvancedInventorySection"/> -+ <section name="AdminAddAttributeModalSection"/> - </page> - </pages> -diff --git a/app/code/Magento/Catalog/Test/Mftf/Page/AdminProductDeletePage.xml b/app/code/Magento/Catalog/Test/Mftf/Page/AdminProductDeletePage.xml -new file mode 100644 -index 00000000000..1ce53a0ebd5 ---- /dev/null -+++ b/app/code/Magento/Catalog/Test/Mftf/Page/AdminProductDeletePage.xml -@@ -0,0 +1,14 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<pages xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/PageObject.xsd"> -+ <page name="AdminProductDeletePage" url="catalog/product/delete/id/{{productId}}/" area="admin" module="Magento_Catalog" parameterized="true"> -+ <!-- This page object only exists for the url. Use the AdminProductCreatePage for selectors. --> -+ </page> -+</pages> -diff --git a/app/code/Magento/Catalog/Test/Mftf/Page/AdminProductEditPage.xml b/app/code/Magento/Catalog/Test/Mftf/Page/AdminProductEditPage.xml -index 9312d4dfcfb..c9debf8bf3b 100644 ---- a/app/code/Magento/Catalog/Test/Mftf/Page/AdminProductEditPage.xml -+++ b/app/code/Magento/Catalog/Test/Mftf/Page/AdminProductEditPage.xml -@@ -7,7 +7,7 @@ - --> - - <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="AdminProductEditPage" url="catalog/product/edit/id/{{productId}}/" area="admin" module="Magento_Catalog" parameterized="true"> - <!-- This page object only exists for the url. Use the AdminProductCreatePage for selectors. --> - </page> -diff --git a/app/code/Magento/Catalog/Test/Mftf/Page/AdminProductIndexPage.xml b/app/code/Magento/Catalog/Test/Mftf/Page/AdminProductIndexPage.xml -index 66cd6911762..a6edf06f2c1 100644 ---- a/app/code/Magento/Catalog/Test/Mftf/Page/AdminProductIndexPage.xml -+++ b/app/code/Magento/Catalog/Test/Mftf/Page/AdminProductIndexPage.xml -@@ -6,7 +6,7 @@ - */ - --> - <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="AdminProductIndexPage" url="catalog/product/index" area="admin" module="Magento_Catalog"> - <section name="AdminProductGridActionSection" /> - <section name="AdminProductGridFilterSection" /> -diff --git a/app/code/Magento/Catalog/Test/Mftf/Page/ProductCatalogPage.xml b/app/code/Magento/Catalog/Test/Mftf/Page/ProductCatalogPage.xml -index 742b46fcaf7..012aeaaf14e 100644 ---- a/app/code/Magento/Catalog/Test/Mftf/Page/ProductCatalogPage.xml -+++ b/app/code/Magento/Catalog/Test/Mftf/Page/ProductCatalogPage.xml -@@ -7,7 +7,7 @@ - --> - - <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="ProductCatalogPage" url="/catalog/product/" area="admin" module="Magento_Catalog"> - <section name="ProductCatalogPageSection"/> - </page> -diff --git a/app/code/Magento/Catalog/Test/Mftf/Page/StorefrontCategoryPage.xml b/app/code/Magento/Catalog/Test/Mftf/Page/StorefrontCategoryPage.xml -index c5b9fe86955..469c153d38b 100644 ---- a/app/code/Magento/Catalog/Test/Mftf/Page/StorefrontCategoryPage.xml -+++ b/app/code/Magento/Catalog/Test/Mftf/Page/StorefrontCategoryPage.xml -@@ -7,7 +7,7 @@ - --> - - <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="StorefrontCategoryPage" url="/{{var1}}.html" area="storefront" module="Catalog" parameterized="true"> - <section name="StorefrontCategoryMainSection"/> - <section name="WYSIWYGToolbarSection"/> -diff --git a/app/code/Magento/Catalog/Test/Mftf/Page/StorefrontProductComparePage.xml b/app/code/Magento/Catalog/Test/Mftf/Page/StorefrontProductComparePage.xml -index f0599a021d4..5451d920224 100644 ---- a/app/code/Magento/Catalog/Test/Mftf/Page/StorefrontProductComparePage.xml -+++ b/app/code/Magento/Catalog/Test/Mftf/Page/StorefrontProductComparePage.xml -@@ -7,7 +7,7 @@ - --> - - <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="StorefrontProductComparePage" url="catalog/product_compare/index" module="Magento_Catalog" area="storefront"> - <section name="StorefrontProductCompareMainSection" /> - </page> -diff --git a/app/code/Magento/Catalog/Test/Mftf/Page/StorefrontProductPage.xml b/app/code/Magento/Catalog/Test/Mftf/Page/StorefrontProductPage.xml -index 8fd59585938..75e3210cad7 100644 ---- a/app/code/Magento/Catalog/Test/Mftf/Page/StorefrontProductPage.xml -+++ b/app/code/Magento/Catalog/Test/Mftf/Page/StorefrontProductPage.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="StorefrontProductPage" url="/{{var1}}.html" area="storefront" module="Catalog" parameterized="true"> -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/PageObject.xsd"> -+ <page name="StorefrontProductPage" url="/{{var1}}.html" area="storefront" module="Magento_Catalog" parameterized="true"> - <section name="StorefrontProductInfoMainSection" /> - <section name="StorefrontProductInfoDetailsSection" /> - <section name="WYSIWYGToolbarSection"/> -diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/AdminAddProductsToOptionPanelSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/AdminAddProductsToOptionPanelSection.xml -index 4541ad25af2..069a8b28698 100644 ---- a/app/code/Magento/Catalog/Test/Mftf/Section/AdminAddProductsToOptionPanelSection.xml -+++ b/app/code/Magento/Catalog/Test/Mftf/Section/AdminAddProductsToOptionPanelSection.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="AdminAddProductsToOptionPanel"> - <element name="addSelectedProducts" type="button" selector=".product_form_product_form_bundle-items_modal button.action-primary" timeout="30"/> - <element name="filters" type="button" selector=".product_form_product_form_bundle-items_modal button[data-action='grid-filter-expand']" timeout="30"/> -diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/AdminCatalogProductWidgetSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/AdminCatalogProductWidgetSection.xml -new file mode 100644 -index 00000000000..3261db1f63f ---- /dev/null -+++ b/app/code/Magento/Catalog/Test/Mftf/Section/AdminCatalogProductWidgetSection.xml -@@ -0,0 +1,15 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> -+ <section name="AdminCatalogProductWidgetSection"> -+ <element name="productAttributesToShow" type="multiselect" selector="select[name='parameters[show_attributes][]']"/> -+ <element name="productButtonsToShow" type="multiselect" selector="select[name='parameters[show_buttons][]']"/> -+ </section> -+</sections> -\ No newline at end of file -diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/AdminCategoryBasicFieldSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/AdminCategoryBasicFieldSection.xml -index 3ed3763da19..977e63b9ec9 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 @@ - --> - - <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="AdminCategoryBasicFieldSection"> - <element name="IncludeInMenu" type="checkbox" selector="input[name='include_in_menu']"/> - <element name="includeInMenuLabel" type="text" selector="input[name='include_in_menu']+label"/> -@@ -16,9 +16,13 @@ - <element name="enableCategoryLabel" type="text" selector="input[name='is_active']+label"/> - <element name="enableUseDefault" type="checkbox" selector="input[name='use_default[is_active]']"/> - <element name="CategoryNameInput" type="input" selector="input[name='name']"/> -+ <element name="RequiredFieldIndicator" type="text" selector=" return window.getComputedStyle(document.querySelector('._required[data-index=name]>.admin__field-label span'), ':after').getPropertyValue('content');"/> -+ <element name="RequiredFieldIndicatorColor" type="text" selector=" return window.getComputedStyle(document.querySelector('._required[data-index=name]>.admin__field-label span'), ':after').getPropertyValue('color');"/> - <element name="categoryNameUseDefault" type="checkbox" selector="input[name='use_default[name]']"/> - <element name="ContentTab" type="input" selector="input[name='name']"/> - <element name="FieldError" type="text" selector=".admin__field-error[data-bind='attr: {for: {{field}}}, text: error']" parameterized="true"/> -+ <element name="panelFieldControl" type="input" selector='//aside//div[@data-index="{{arg1}}"]/descendant::*[@name="{{arg2}}"]' parameterized="true"/> -+ <element name="productsInCategory" type="input" selector="div[data-index='assign_products']" timeout="30"/> - </section> - <section name="CategoryContentSection"> - <element name="SelectFromGalleryBtn" type="button" selector="//label[text()='Select from Gallery']"/> -@@ -29,6 +33,20 @@ - <element name="DesignTab" type="button" selector="//strong[@class='admin__collapsible-title']//span[text()='Design']"/> - <element name="LayoutDropdown" type="select" selector="select[name='page_layout']"/> - </section> -+ <section name="CategoryDisplaySettingsSection"> -+ <element name="DisplaySettingTab" type="button" selector="//strong[@class='admin__collapsible-title']//span[text()='Display Settings']"/> -+ <element name="layeredNavigationPriceInput" type="input" selector="input[name='filter_price_range']"/> -+ <element name="FieldError" type="text" selector=".admin__field-error[data-bind='attr: {for: {{field}}}, text: error']" parameterized="true"/> -+ <element name="filterPriceRangeUseConfig" type="checkbox" selector="input[name='use_config[filter_price_range]']"/> -+ <element name="RequiredFieldIndicator" type="text" selector=" return window.getComputedStyle(document.querySelector('._required[data-index={{arg1}}]>.admin__field-label span'), ':after').getPropertyValue('content');" parameterized="true"/> -+ <element name="displayMode" type="button" selector="select[name='display_mode']"/> -+ <element name="anchor" type="checkbox" selector="input[name='is_anchor']"/> -+ <element name="productListCheckBox" type="checkbox" selector="input[name='use_config[available_sort_by]']" /> -+ <element name="productList" type="text" selector="select[name='available_sort_by']"/> -+ <element name="defaultProductLisCheckBox" type="checkbox" selector="input[name='use_config[default_sort_by]']"/> -+ <element name="defaultProductList" type="text" selector="select[name='default_sort_by']"/> -+ <element name="layeredNavigationPriceCheckBox" type="checkbox" selector="input[name='use_config[filter_price_range]']"/> -+ </section> - <section name="CatalogWYSIWYGSection"> - <element name="ShowHideBtn" type="button" selector="#togglecategory_form_description"/> - <element name="TinyMCE4" type="text" selector=".mce-branding-powered-by"/> -diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/AdminCategoryContentSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/AdminCategoryContentSection.xml -index 59537274f23..e3d22490467 100644 ---- a/app/code/Magento/Catalog/Test/Mftf/Section/AdminCategoryContentSection.xml -+++ b/app/code/Magento/Catalog/Test/Mftf/Section/AdminCategoryContentSection.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="AdminCategoryContentSection"> - <element name="sectionHeader" type="button" selector="div[data-index='content']" timeout="30"/> - <element name="uploadButton" type="button" selector="//*[@class='file-uploader-area']/label[text()='Upload']"/> -@@ -15,5 +15,14 @@ - <element name="uploadImageFile" type="input" selector=".file-uploader-area>input"/> - <element name="imageFileName" type="text" selector=".file-uploader-filename"/> - <element name="removeImageButton" type="button" selector=".file-uploader-summary .action-remove"/> -+ <element name="AddCMSBlock" type="select" selector="//*[@name='landing_page']"/> -+ <element name="description" type="input" selector="//*[@name='description']"/> -+ <element name="content" type="button" selector="div[data-index='content'"/> -+ <element name="categoryInTree" type="button" selector="//li[contains(@class, 'x-tree-node')]//div[contains(.,'{{categoryName}}') and contains(@class, 'no-active-category')]" parameterized="true" /> -+ <element name="categoryPageTitle" type="text" selector="h1.page-title" /> -+ <element name="activeCategoryInTree" type="button" selector="//li[contains(@class, 'x-tree-node')]//div[contains(.,'{{categoryName}}') and contains(@class, 'active-category')]" parameterized="true" /> -+ <element name="productTableColumnName" type="input" selector="#catalog_category_products_filter_name"/> -+ <element name="productTableRow" type="button" selector="#catalog_category_products_table tbody tr"/> -+ <element name="productSearch" type="button" selector="//button[@data-action='grid-filter-apply']" timeout="30"/> - </section> - </sections> -diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/AdminCategoryDisplaySettingsSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/AdminCategoryDisplaySettingsSection.xml -new file mode 100644 -index 00000000000..daa00eb0a27 ---- /dev/null -+++ b/app/code/Magento/Catalog/Test/Mftf/Section/AdminCategoryDisplaySettingsSection.xml -@@ -0,0 +1,15 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> -+ <section name="AdminCategoryDisplaySettingsSection"> -+ <element name="settingsHeader" type="button" selector="//*[contains(text(),'Display Settings')]" timeout="30"/> -+ <element name="displayMode" type="button" selector="//*[@name='display_mode']"/> -+ </section> -+</sections> -diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/AdminCategoryMainActionsSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/AdminCategoryMainActionsSection.xml -index 60a6d852bf6..e8adede5b2d 100644 ---- a/app/code/Magento/Catalog/Test/Mftf/Section/AdminCategoryMainActionsSection.xml -+++ b/app/code/Magento/Catalog/Test/Mftf/Section/AdminCategoryMainActionsSection.xml -@@ -7,12 +7,14 @@ - --> - - <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="AdminCategoryMainActionsSection"> - <element name="SaveButton" type="button" selector=".page-actions-inner #save" timeout="30"/> - <element name="DeleteButton" type="button" selector=".page-actions-inner #delete" timeout="30"/> - <element name="CategoryStoreViewDropdownToggle" type="button" selector="#store-change-button"/> - <element name="CategoryStoreViewOption" type="button" selector="//div[contains(@class, 'store-switcher')]//a[normalize-space()='{{store}}']" parameterized="true"/> - <element name="CategoryStoreViewModalAccept" type="button" selector=".modal-popup.confirm._show .action-accept"/> -+ <element name="allStoreViews" type="button" selector=".store-switcher .store-switcher-all" timeout="30"/> -+ <element name="storeSwitcher" type="text" selector=".store-switcher"/> - </section> - </sections> -diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/AdminCategoryMessagesSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/AdminCategoryMessagesSection.xml -index 1214cfd2eb2..ea4f4bf53eb 100644 ---- a/app/code/Magento/Catalog/Test/Mftf/Section/AdminCategoryMessagesSection.xml -+++ b/app/code/Magento/Catalog/Test/Mftf/Section/AdminCategoryMessagesSection.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="AdminCategoryMessagesSection"> - <element name="SuccessMessage" type="text" selector=".message-success"/> -+ <element name="errorMessage" type="text" selector="//div[@class='message message-error error']"/> - </section> - </sections> -diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/AdminCategoryModalSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/AdminCategoryModalSection.xml -index 03b9d767785..85b8dc894a1 100644 ---- a/app/code/Magento/Catalog/Test/Mftf/Section/AdminCategoryModalSection.xml -+++ b/app/code/Magento/Catalog/Test/Mftf/Section/AdminCategoryModalSection.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="AdminCategoryModalSection"> - <element name="message" type="text" selector="aside.confirm div.modal-content"/> - <element name="title" type="text" selector="aside.confirm .modal-header .modal-title"/> -diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/AdminCategoryProductsGridSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/AdminCategoryProductsGridSection.xml -index 540a97fd04e..df79ec61ef7 100644 ---- a/app/code/Magento/Catalog/Test/Mftf/Section/AdminCategoryProductsGridSection.xml -+++ b/app/code/Magento/Catalog/Test/Mftf/Section/AdminCategoryProductsGridSection.xml -@@ -7,12 +7,15 @@ - --> - - <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="AdminCategoryProductsGridSection"> - <element name="rowProductId" type="text" selector="#catalog_category_products_table tbody tr:nth-of-type({{row}}) .col-id" parameterized="true"/> - <element name="rowProductName" type="text" selector="#catalog_category_products_table tbody tr:nth-of-type({{row}}) .col-name" parameterized="true"/> - <element name="rowProductSku" type="text" selector="#catalog_category_products_table tbody tr:nth-of-type({{row}}) .col-sku" parameterized="true"/> - <element name="rowPrice" type="text" selector="#catalog_category_products_table tbody tr:nth-of-type({{row}}) .col-price" parameterized="true"/> - <element name="rowPosition" type="input" selector="#catalog_category_products_table tbody tr:nth-of-type({{row}}) .col-position .position input" timeout="30" parameterized="true"/> -+ <element name="productGridNameProduct" type="text" selector="//table[@id='catalog_category_products_table']//td[contains(., '{{productName}}')]" parameterized="true"/> -+ <element name="productVisibility" type="select" selector="//*[@name='product[visibility]']"/> -+ <element name="productSelectAll" type="checkbox" selector="input.admin__control-checkbox"/> - </section> --</sections> -\ No newline at end of file -+</sections> -diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/AdminCategoryProductsSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/AdminCategoryProductsSection.xml -index dc254bdf159..e9ff40f98bb 100644 ---- a/app/code/Magento/Catalog/Test/Mftf/Section/AdminCategoryProductsSection.xml -+++ b/app/code/Magento/Catalog/Test/Mftf/Section/AdminCategoryProductsSection.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="AdminCategoryProductsSection"> - <element name="sectionHeader" type="button" selector="div[data-index='assign_products']" timeout="30"/> -+ <element name="addProducts" type="button" selector="#catalog_category_add_product_tabs" timeout="30"/> - </section> - </sections> -\ No newline at end of file -diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/AdminCategorySEOSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/AdminCategorySEOSection.xml -index 35852abe350..b5d5d61f646 100644 ---- a/app/code/Magento/Catalog/Test/Mftf/Section/AdminCategorySEOSection.xml -+++ b/app/code/Magento/Catalog/Test/Mftf/Section/AdminCategorySEOSection.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="AdminCategorySEOSection"> - <element name="SectionHeader" type="button" selector="div[data-index='search_engine_optimization']" timeout="30"/> - <element name="UrlKeyInput" type="input" selector="input[name='url_key']"/> -diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/AdminCategorySidebarActionSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/AdminCategorySidebarActionSection.xml -index e53a9989d66..0a1901f1fda 100644 ---- a/app/code/Magento/Catalog/Test/Mftf/Section/AdminCategorySidebarActionSection.xml -+++ b/app/code/Magento/Catalog/Test/Mftf/Section/AdminCategorySidebarActionSection.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="AdminCategorySidebarActionSection"> - <element name="AddRootCategoryButton" type="button" selector="#add_root_category_button" timeout="30"/> - <element name="AddSubcategoryButton" type="button" selector="#add_subcategory_button" timeout="30"/> -diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/AdminCategorySidebarTreeSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/AdminCategorySidebarTreeSection.xml -index 524fac78bc1..ce7d962f3ec 100644 ---- a/app/code/Magento/Catalog/Test/Mftf/Section/AdminCategorySidebarTreeSection.xml -+++ b/app/code/Magento/Catalog/Test/Mftf/Section/AdminCategorySidebarTreeSection.xml -@@ -7,14 +7,17 @@ - --> - - <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="AdminCategorySidebarTreeSection"> - <element name="collapseAll" type="button" selector=".tree-actions a:first-child"/> - <element name="expandAll" type="button" selector=".tree-actions a:last-child"/> - <element name="categoryTreeRoot" type="text" selector="div.x-tree-root-node>li.x-tree-node:first-of-type>div.x-tree-node-el:first-of-type" timeout="30"/> - <element name="categoryInTree" type="text" selector="//a/span[contains(text(), '{{name}}')]" parameterized="true" timeout="30"/> -- <element name="categoryInTreeUnderRoot" type="text" selector="//div[@class='x-tree-root-node']/li/ul/li[@class='x-tree-node']/div/a/span[contains(text(), '{{name}}')]" parameterized="true"/> -+ <element name="categoryHighlighted" type="text" selector="//ul[@id='ui-id-2']//li//a/span[contains(text(),'{{name}}')]/../.." parameterized="true" timeout="30"/> -+ <element name="categoryNotHighlighted" type="text" selector="ul[id=\'ui-id-2\'] li[class~=\'active\']" timeout="30"/> -+ <element name="categoryInTreeUnderRoot" type="text" selector="//li/ul/li[@class='x-tree-node']/div/a/span[contains(text(), '{{name}}')]" parameterized="true"/> - <element name="lastCreatedCategory" type="block" selector=".x-tree-root-ct li li:last-child" /> - <element name="treeContainer" type="block" selector=".tree-holder" /> -+ <element name="expandRootCategory" type="text" selector="img.x-tree-elbow-end-plus"/> - </section> - </sections> -diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/AdminCategoryWarningMessagesPopupSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/AdminCategoryWarningMessagesPopupSection.xml -index 82b3b76df3d..4c16e9081f4 100644 ---- a/app/code/Magento/Catalog/Test/Mftf/Section/AdminCategoryWarningMessagesPopupSection.xml -+++ b/app/code/Magento/Catalog/Test/Mftf/Section/AdminCategoryWarningMessagesPopupSection.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"> -+<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> - <section name="AdminCategoryWarningMessagesPopupSection"> - <element name="warningMessage" type="text" selector=".modal-inner-wrap .modal-content .message.message-notice"/> - <element name="cancelButton" type="button" selector=".modal-inner-wrap .action-secondary"/> -diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/AdminCreateNewProductAttributeSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/AdminCreateNewProductAttributeSection.xml -new file mode 100644 -index 00000000000..2de7bf19fd3 ---- /dev/null -+++ b/app/code/Magento/Catalog/Test/Mftf/Section/AdminCreateNewProductAttributeSection.xml -@@ -0,0 +1,33 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> -+ <section name="AdminCreateNewProductAttributeSection"> -+ <element name="saveAttribute" type="button" selector="#save"/> -+ <element name="defaultLabel" type="input" selector="input[name='frontend_label[0]']"/> -+ <element name="inputType" type="select" selector="select[name='frontend_input']" timeout="30"/> -+ <element name="addValue" type="button" selector="//button[contains(@data-action,'add_new_row')]" timeout="30"/> -+ <element name="defaultStoreView" type="input" selector="//input[contains(@name,'option[value][option_{{row}}][1]')]" parameterized="true"/> -+ <element name="adminOption" type="input" selector="//input[contains(@name,'option[value][option_{{row}}][0]')]" parameterized="true"/> -+ <element name="defaultRadioButton" type="radio" selector="//tr[{{row}}]//input[contains(@name,'default[]')]" parameterized="true"/> -+ <element name="isRequired" type="checkbox" selector="//input[contains(@name,'is_required')]/..//label"/> -+ <element name="advancedAttributeProperties" type="text" selector="//div[contains(@data-index,'advanced_fieldset')]"/> -+ <element name="attributeCode" type="input" selector="//*[@class='admin__fieldset-wrapper-content admin__collapsible-content _show']//input[@name='attribute_code']"/> -+ <element name="scope" type="select" selector="//*[@class='admin__fieldset-wrapper-content admin__collapsible-content _show']//select[@name='is_global']" timeout="30"/> -+ <element name="defaultValue" type="input" selector="//*[@class='admin__fieldset-wrapper-content admin__collapsible-content _show']//input[@name='default_value_text']"/> -+ <element name="isUnique" type="checkbox" selector="//input[contains(@name, 'is_unique')]/..//label"/> -+ <element name="storefrontProperties" type="text" selector="//div[contains(@data-index,'front_fieldset')]"/> -+ <element name="inSearch" type="checkbox" selector="//input[contains(@name, 'is_searchable')]/..//label"/> -+ <element name="advancedSearch" type="checkbox" selector="//input[contains(@name, 'is_visible_in_advanced_search')]/..//label"/> -+ <element name="isComparable" type="checkbox" selector="//input[contains(@name, 'is_comparable')]/..//label"/> -+ <element name="allowHtmlTags" type="checkbox" selector="//input[contains(@name, 'is_html_allowed_on_front')]/..//label"/> -+ <element name="visibleOnStorefront" type="checkbox" selector="//input[contains(@name, 'is_visible_on_front')]/..//label"/> -+ <element name="sortProductListing" type="checkbox" selector="//input[contains(@name, 'is_visible_on_front')]/..//label"/> -+ </section> -+</sections> -diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/AdminCreateProductAttributeSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/AdminCreateProductAttributeSection.xml -index 377bc18d647..9b75f7e6908 100644 ---- a/app/code/Magento/Catalog/Test/Mftf/Section/AdminCreateProductAttributeSection.xml -+++ b/app/code/Magento/Catalog/Test/Mftf/Section/AdminCreateProductAttributeSection.xml -@@ -7,22 +7,48 @@ - --> - - <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="AttributePropertiesSection"> -+ <element name="propertiesTab" type="button" selector="#product_attribute_tabs_main"/> - <element name="DefaultLabel" type="input" selector="#attribute_label"/> - <element name="InputType" type="select" selector="#frontend_input"/> - <element name="ValueRequired" type="select" selector="#is_required"/> -+ <element name="UpdateProductPreviewImage" type="select" selector="[name='update_product_preview_image']"/> - <element name="AdvancedProperties" type="button" selector="#advanced_fieldset-wrapper"/> - <element name="DefaultValue" type="input" selector="#default_value_text"/> - <element name="Scope" type="select" selector="#is_global"/> - <element name="Save" type="button" selector="#save" timeout="30"/> -+ <element name="DeleteAttribute" type="button" selector="#delete" timeout="30"/> - <element name="SaveAndEdit" type="button" selector="#save_and_edit_button" timeout="30"/> - <element name="TinyMCE4" type="button" selector="//span[text()='Default Value']/parent::label/following-sibling::div//div[@class='mce-branding-powered-by']"/> - <element name="checkIfTabOpen" selector="//div[@id='advanced_fieldset-wrapper' and not(contains(@class,'opened'))]" type="button"/> -+ <element name="useInLayeredNavigation" type="select" selector="#is_filterable"/> -+ <element name="addSwatch" type="button" selector="#add_new_swatch_text_option_button"/> -+ <element name="dropdownAddOptions" type="button" selector="#add_new_option_button"/> -+ <!-- Manage Options nth child--> -+ <element name="dropdownNthOptionIsDefault" type="checkbox" selector="tbody[data-role='options-container'] tr:nth-child({{var}}) .input-radio" parameterized="true"/> -+ <element name="dropdownNthOptionAdmin" type="textarea" selector="tbody[data-role='options-container'] tr:nth-child({{var}}) td:nth-child(3) input" parameterized="true"/> -+ <element name="dropdownNthOptionDefaultStoreView" type="textarea" selector="tbody[data-role='options-container'] tr:nth-child({{var}}) td:nth-child(4) input" parameterized="true"/> -+ <element name="dropdownNthOptionDelete" type="button" selector="tbody[data-role='options-container'] tr:nth-child({{var}}) button[title='Delete']" parameterized="true"/> -+ </section> -+ <section name="AttributeDeleteModalSection"> -+ <element name="confirm" type="button" selector=".modal-popup.confirm .action-accept"/> -+ <element name="cancel" type="button" selector=".modal-popup.confirm .action-dismiss"/> -+ </section> -+ <section name="AttributeManageSwatchSection"> -+ <element name="swatchField" type="input" selector="//th[contains(@class, 'col-swatch')]/span[contains(text(), '{{arg}}')]/ancestor::thead/following-sibling::tbody//input[@placeholder='Swatch']" parameterized="true"/> -+ <element name="descriptionField" type="input" selector="//th[contains(@class, 'col-swatch')]/span[contains(text(), '{{arg}}')]/ancestor::thead/following-sibling::tbody//input[@placeholder='Description']" parameterized="true"/> -+ </section> -+ <section name="AttributeOptionsSection"> -+ <element name="AddOption" type="button" selector="#add_new_option_button"/> - </section> - <section name="StorefrontPropertiesSection"> -+ <element name="PageTitle" type="text" selector="//span[text()='Storefront Properties']" /> - <element name="StoreFrontPropertiesTab" selector="#product_attribute_tabs_front" type="button"/> - <element name="EnableWYSIWYG" type="select" selector="#enabled"/> -+ <element name="useForPromoRuleConditions" type="select" selector="#is_used_for_promo_rules"/> -+ <element name="StorefrontPropertiesSectionToggle" type="button" selector="#front_fieldset-wrapper"/> -+ <element name="visibleOnCatalogPagesOnStorefront" type="select" selector="#is_visible_on_front"/> - </section> - <section name="WYSIWYGProductAttributeSection"> - <element name="ShowHideBtn" type="button" selector="#toggledefault_value_texteditor"/> -@@ -60,4 +86,20 @@ - <element name="SpecialCharacter" type="button" selector=".mce-i-charmap" /> - <element name="TextArea" type="input" selector="#default_value_textarea" /> - </section> -+ <section name="AdvancedAttributePropertiesSection"> -+ <element name="AdvancedAttributePropertiesSectionToggle" -+ type="button" selector="#advanced_fieldset-wrapper"/> -+ <element name="AttributeCode" type="text" selector="#attribute_code"/> -+ <element name="DefaultValueText" type="textarea" selector="#default_value_text"/> -+ <element name="DefaultValueTextArea" type="textarea" selector="#default_value_textarea"/> -+ <element name="DefaultValueDate" type="textarea" selector="#default_value_date"/> -+ <element name="DefaultValueYesNo" type="textarea" selector="#default_value_yesno"/> -+ <element name="Scope" type="select" selector="#is_global"/> -+ <element name="UniqueValue" type="select" selector="#is_unique"/> -+ <element name="AddToColumnOptions" type="select" selector="#is_used_in_grid"/> -+ <element name="UseInFilterOptions" type="select" selector="#is_filterable_in_grid"/> -+ <element name="UseInProductListing" type="select" selector="#used_in_product_listing"/> -+ <element name="UseInSearch" type="select" selector="#is_searchable"/> -+ <element name="VisibleInAdvancedSearch" type="select" selector="#is_visible_in_advanced_search"/> -+ </section> - </sections> -diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/AdminEditProductAttributesSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/AdminEditProductAttributesSection.xml -index 703e9e7ec70..b243fbfd603 100644 ---- a/app/code/Magento/Catalog/Test/Mftf/Section/AdminEditProductAttributesSection.xml -+++ b/app/code/Magento/Catalog/Test/Mftf/Section/AdminEditProductAttributesSection.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="AdminEditProductAttributesSection"> - <element name="AttributeName" type="text" selector="#name"/> - <element name="ChangeAttributeNameToggle" type="checkbox" selector="#toggle_name"/> -@@ -18,5 +18,9 @@ - <element name="AttributeDescription" type="text" selector="#description"/> - <element name="ChangeAttributeDescriptionToggle" type="checkbox" selector="#toggle_description"/> - <element name="Save" type="button" selector="button[title=Save]" timeout="30"/> -+ <element name="ProductDataMayBeLostModal" type="button" selector="//aside[contains(@class,'_show')]//header[contains(.,'Product data may be lost')]"/> -+ <element name="ProductDataMayBeLostConfirmButton" type="button" selector="//aside[contains(@class,'_show')]//button[.='Change Input Type']"/> -+ <element name="defaultLabel" type="text" selector="//td[contains(text(), '{{attributeName}}')]/following-sibling::td[contains(@class, 'col-frontend_label')]" parameterized="true"/> -+ <element name="formByStoreId" type="block" selector="//form[contains(@action,'store/{{store_id}}')]" parameterized="true"/> - </section> - </sections> -diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/AdminNewWidgetSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/AdminNewWidgetSection.xml -new file mode 100644 -index 00000000000..5329ad48c8f ---- /dev/null -+++ b/app/code/Magento/Catalog/Test/Mftf/Section/AdminNewWidgetSection.xml -@@ -0,0 +1,14 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> -+ <section name="AdminNewWidgetSection"> -+ <element name="selectProduct" type="button" selector=".btn-chooser" timeout="30"/> -+ </section> -+</sections> -diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/AdminNewWidgetSelectProductPopupSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/AdminNewWidgetSelectProductPopupSection.xml -new file mode 100644 -index 00000000000..0da67849f85 ---- /dev/null -+++ b/app/code/Magento/Catalog/Test/Mftf/Section/AdminNewWidgetSelectProductPopupSection.xml -@@ -0,0 +1,15 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> -+ <section name="AdminNewWidgetSelectProductPopupSection"> -+ <element name="filterBySku" type="input" selector=".data-grid-filters input[name='chooser_sku']"/> -+ <element name="firstRow" type="select" selector=".even>td" timeout="20"/> -+ </section> -+</sections> -diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductAddAttributeModalSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductAddAttributeModalSection.xml -new file mode 100644 -index 00000000000..a3c98e43b45 ---- /dev/null -+++ b/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductAddAttributeModalSection.xml -@@ -0,0 +1,19 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> -+ <section name="AdminProductAddAttributeModalSection"> -+ <element name="addSelected" type="button" selector=".product_form_product_form_add_attribute_modal .page-main-actions .action-primary" timeout="30"/> -+ <element name="filters" type="button" selector=".product_form_product_form_add_attribute_modal button[data-action='grid-filter-expand']" timeout="30"/> -+ <element name="attributeCodeFilter" type="textarea" selector=".product_form_product_form_add_attribute_modal input[name='attribute_code']"/> -+ <element name="clearFilters" type="button" selector=".product_form_product_form_add_attribute_modal .action-clear" timeout="30"/> -+ <element name="firstRowCheckBox" type="input" selector=".product_form_product_form_add_attribute_modal .data-grid-checkbox-cell input"/> -+ <element name="applyFilters" type="button" selector=".product_form_product_form_add_attribute_modal .admin__data-grid-filters-footer .action-secondary" timeout="30"/> -+ </section> -+</sections> -diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductAttributeGridSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductAttributeGridSection.xml -index caf34a9f355..5efd04eacb7 100644 ---- a/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductAttributeGridSection.xml -+++ b/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductAttributeGridSection.xml -@@ -7,14 +7,21 @@ - --> - - <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="AdminProductAttributeGridSection"> - <element name="AttributeCode" type="text" selector="//td[contains(text(),'{{var1}}')]" parameterized="true" timeout="30"/> -- <element name="createNewAttributeBtn" type="button" selector="button[data-index='add_new_attribute_button']"/> -+ <element name="createNewAttributeBtn" type="button" selector="#add"/> - <element name="GridFilterFrontEndLabel" type="input" selector="#attributeGrid_filter_frontend_label"/> - <element name="Search" type="button" selector="button[data-action=grid-filter-apply]" timeout="30"/> - <element name="ResetFilter" type="button" selector="button[data-action='grid-filter-reset']" timeout="30"/> -- <element name="FirstRow" type="button" selector="//*[@id='attributeGrid_table']/tbody/tr[1]"/> -+ <element name="FirstRow" type="button" selector="//*[@id='attributeGrid_table']/tbody/tr[1]" timeout="30"/> - <element name="FilterByAttributeCode" type="input" selector="#attributeGrid_filter_attribute_code"/> -+ <element name="attributeLabelFilter" type="input" selector="//input[@name='frontend_label']"/> -+ <element name="attributeCodeColumn" type="text" selector="//div[@id='attributeGrid']//td[contains(@class,'col-attr-code col-attribute_code')]"/> -+ <element name="defaultLabelColumn" type="text" selector="//div[@id='attributeGrid']//td[contains(@class,'col-label col-frontend_label')]"/> -+ <element name="isVisibleColumn" type="text" selector="//div[@id='attributeGrid']//td[contains(@class,'a-center col-is_visible')]"/> -+ <element name="scopeColumn" type="text" selector="//div[@id='attributeGrid']//td[contains(@class,'a-center col-is_global')]"/> -+ <element name="isSearchableColumn" type="text" selector="//div[@id='attributeGrid']//td[contains(@class,'a-center col-is_searchable')]"/> -+ <element name="isComparableColumn" type="text" selector="//div[@id='attributeGrid']//td[contains(@class,'a-center col-is_comparable')]"/> - </section> - </sections> -diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductAttributeOptionsSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductAttributeOptionsSection.xml -new file mode 100644 -index 00000000000..5f1112eef36 ---- /dev/null -+++ b/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductAttributeOptionsSection.xml -@@ -0,0 +1,15 @@ -+<?xml version="1.0"?> -+<!-- -+/** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> -+ <section name="DropdownAttributeOptionsSection"> -+ <element name="nthOptionAdminLabel" type="input" -+ selector="(//*[@id='manage-options-panel']//tr[{{var}}]//input[contains(@name, 'option[value]')])[1]" parameterized="true"/> -+ <element name="deleteButton" type="button" selector="(//td[@class='col-delete'])[1]" timeout="30"/> -+ </section> -+</sections> -\ No newline at end of file -diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductAttributeSetActionSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductAttributeSetActionSection.xml -index 4c309584d4d..e165b51ef18 100644 ---- a/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductAttributeSetActionSection.xml -+++ b/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductAttributeSetActionSection.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="AdminProductAttributeSetActionSection"> - <element name="save" type="button" selector="button[title='Save']" timeout="30"/> - <element name="reset" type="button" selector="button[title='Reset']" timeout="30"/> -diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductAttributeSetEditSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductAttributeSetEditSection.xml -index a2193bcafbb..df8915a4998 100644 ---- a/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductAttributeSetEditSection.xml -+++ b/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductAttributeSetEditSection.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="AdminProductAttributeSetEditSection"> - <!-- Groups Column --> - <element name="groupTree" type="block" selector="#tree-div1"/> -@@ -17,9 +17,16 @@ - <element name="assignedAttribute" type="text" selector="//*[@id='tree-div1']//span[text()='{{attributeName}}']" parameterized="true"/> - <element name="xThLineItemYthAttributeGroup" type="text" selector="//*[@id='tree-div1']/ul/div/li[{{y}}]//li[{{x}}]" parameterized="true"/> - <element name="xThLineItemAttributeGroup" type="text" selector="//*[@id='tree-div1']//span[text()='{{groupName}}']/parent::*/parent::*/parent::*//li[{{x}}]//a/span" parameterized="true"/> -+ <element name="attributesInGroup" type="text" selector="//span[text()='{{GroupName}}']/../../following-sibling::ul/li" parameterized="true"/> - <!-- Unassigned Attributes Column --> - <element name="unassignedAttributesTree" type="block" selector="#tree-div2"/> - <element name="unassignedAttribute" type="text" selector="//*[@id='tree-div2']//span[text()='{{attributeName}}']" parameterized="true"/> - <element name="xThLineItemUnassignedAttribute" type="text" selector="//*[@id='tree-div2']//li[{{x}}]//a/span" parameterized="true"/> -+ <!-- Buttons --> -+ <element name="AddNewGroup" type="button" selector="button[data-ui-id='adminhtml-catalog-product-set-edit-add-group-button']"/> -+ <!-- Modal Window Add New Group --> -+ <element name="newGroupName" type="input" selector="input[data-role='promptField']"/> -+ <element name="buttonOk" type="button" selector=".modal-footer .action-primary.action-accept"/> -+ <element name="errorLabel" type="text" selector="label.mage-error"/> - </section> - </sections> -diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductAttributeSetGridSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductAttributeSetGridSection.xml -index 08724222a38..3fad50adb77 100644 ---- a/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductAttributeSetGridSection.xml -+++ b/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductAttributeSetGridSection.xml -@@ -7,12 +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="AdminProductAttributeSetGridSection"> - <element name="filter" type="input" selector="#setGrid_filter_set_name"/> - <element name="searchBtn" type="button" selector="#container button[title='Search']" timeout="30"/> - <element name="nthRow" type="block" selector="#setGrid_table tbody tr:nth-of-type({{var1}})" parameterized="true"/> - <element name="AttributeSetName" type="text" selector="//td[contains(text(), '{{var1}}')]" parameterized="true"/> - <element name="addAttributeSetBtn" type="button" selector="button.add-set" timeout="30"/> -+ <element name="resetFilter" type="button" selector="button[data-action='grid-filter-reset']" timeout="30"/> - </section> - </sections> -diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductAttributeSetSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductAttributeSetSection.xml -index 9e320d9e8b0..8f635214fff 100644 ---- a/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductAttributeSetSection.xml -+++ b/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductAttributeSetSection.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="AdminProductAttributeSetSection"> - <element name="name" type="input" selector="#attribute_set_name"/> - <element name="basedOn" type="select" selector="#skeleton_set"/> -@@ -29,6 +29,6 @@ - </section> - <section name="ModifyAttributes"> - <!-- Parameter is the attribute name --> -- <element name="nthExistingAttribute" type="select" selector="//*[text()='{{attributeName}}']/../..//select" parameterized="true"/> -+ <element name="nthExistingAttribute" type="select" selector="//*[text()='{{attributeName}}']/../../..//select" parameterized="true"/> - </section> - </sections> -diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductAttributesSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductAttributesSection.xml -new file mode 100644 -index 00000000000..46a516b538f ---- /dev/null -+++ b/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductAttributesSection.xml -@@ -0,0 +1,17 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> -+ <section name="AdminProductAttributesSection"> -+ <element name="sectionHeader" type="button" selector="div[data-index='attributes']" timeout="30"/> -+ <element name="attributeLabelByCode" type="text" selector="div[data-index='{{var}}'] .admin__field-label span" parameterized="true"/> -+ <element name="attributeTextInputByCode" type="text" selector="div[data-index='{{var}}'] .admin__field-control input" parameterized="true"/> -+ <element name="attributeDropdownByCode" type="text" selector="div[data-index='{{var}}'] .admin__field-control select" parameterized="true"/> -+ </section> -+</sections> -diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductCategoryCreationSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductCategoryCreationSection.xml -index 81290bf281a..755add18ec1 100644 ---- a/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductCategoryCreationSection.xml -+++ b/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductCategoryCreationSection.xml -@@ -7,16 +7,14 @@ - --> - - <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="AdminProductCategoryCreationSection"> - <element name="firstExampleProduct" type="button" selector=".data-row:nth-of-type(1)"/> - <element name="newCategory" type="button" selector="//button/span[text()='New Category']"/> -- - <element name="nameInput" type="input" selector="input[name='name']"/> -- - <element name="parentCategory" type="block" selector=".product_form_product_form_create_category_modal div[data-role='selected-option']"/> - <element name="parentSearch" type="input" selector="aside input[data-role='advanced-select-text']"/> - <element name="parentSearchResult" type="block" selector="aside .admin__action-multiselect-menu-inner"/> -- <element name="createCategory" type="button" selector="#save"/> -+ <element name="createCategory" type="button" selector="#save" timeout="30"/> - </section> - </sections> -diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductContentSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductContentSection.xml -index b73c630d6d9..fafae5d5355 100644 ---- a/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductContentSection.xml -+++ b/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductContentSection.xml -@@ -7,10 +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="AdminProductContentSection"> - <element name="sectionHeader" type="button" selector="div[data-index='content']" timeout="30"/> -+ <element name="sectionHeaderShow" type="button" selector="div[data-index='content']._show" timeout="30"/> - <element name="descriptionTextArea" type="textarea" selector="#product_form_description"/> - <element name="shortDescriptionTextArea" type="textarea" selector="#product_form_short_description"/> -+ <element name="sectionHeaderIfNotShowing" type="button" selector="//div[@data-index='content']//div[contains(@class, '_hide')]"/> -+ <element name="pageHeader" type="textarea" selector="//*[@class='page-header row']"/> - </section> - </sections> -diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductCrossSellModalSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductCrossSellModalSection.xml -new file mode 100644 -index 00000000000..803d72d7a7e ---- /dev/null -+++ b/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductCrossSellModalSection.xml -@@ -0,0 +1,14 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> -+ <section name="AdminProductCrossSellModalSection"> -+ <element name="addSelectedProducts" type="button" selector=".product_form_product_form_related_crosssell_modal .action-primary"/> -+ </section> -+</sections> -diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductCustomizableOptionsSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductCustomizableOptionsSection.xml -index 80ed5cb2d7e..6d4d5d86ef7 100644 ---- a/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductCustomizableOptionsSection.xml -+++ b/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductCustomizableOptionsSection.xml -@@ -7,40 +7,64 @@ - --> - - <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="AdminProductCustomizableOptionsSection"> -- <element name="checkIfCustomizableOptionsTabOpen" type="text" selector="//span[text()='Customizable Options']/parent::strong/parent::*[@data-state-collapsible='closed']"/> -+ <element name="checkIfCustomizableOptionsTabOpen" type="text" selector="//span[text()='Customizable Options']/parent::strong/parent::*[@data-state-collapsible='closed']" timeout="30"/> - <element name="customizableOptions" type="text" selector="//strong[contains(@class, 'admin__collapsible-title')]/span[text()='Customizable Options']"/> - <element name="useDefaultOptionTitle" type="text" selector="[data-index='options'] tr.data-row [data-index='title'] [name^='options_use_default']"/> - <element name="useDefaultOptionTitleByIndex" type="text" selector="[data-index='options'] [data-index='values'] tr[data-repeat-index='{{var1}}'] [name^='options_use_default']" parameterized="true"/> -- <element name="addOptionBtn" type="button" selector="button[data-index='button_add']"/> -- <element name="fillOptionTitle" type="input" selector="//span[text()='{{var1}}']/parent::div/parent::div/parent::div//label[text()='Option Title']/parent::span/parent::div//input[@class='admin__control-text']" parameterized="true"/> -- <element name="optionTitleInput" type="input" selector="input[name='product[options][0][title]']"/> -- <element name="optionTypeOpenDropDown" type="button" selector=".admin__dynamic-rows[data-index='options'] .action-select"/> -- <element name="optionTypeTextField" type="button" selector=".admin__dynamic-rows[data-index='options'] .action-menu._active li li"/> -+ <element name="addOptionBtn" type="button" selector="button[data-index='button_add']" timeout="30"/> -+ <element name="fillOptionTitle" type="input" selector="//span[text()='{{var1}}']/parent::div/parent::div/parent::div//span[text()='Option Title']/parent::label/parent::div/parent::div//input[@class='admin__control-text']" parameterized="true"/> -+ <element name="optionTitleInput" type="input" selector="input[name='product[options][{{index}}][title]']" parameterized="true"/> -+ <element name="optionTypeOpenDropDown" type="button" selector=".admin__dynamic-rows[data-index='options'] .action-select" timeout="30"/> -+ <element name="optionTypeTextField" type="button" selector=".admin__dynamic-rows[data-index='options'] .action-menu._active li li" timeout="30"/> - <element name="maxCharactersInput" type="input" selector="input[name='product[options][0][max_characters]']"/> -+ <element name="deleteCustomOptions" type="button" selector="//div[contains(@class, 'fieldset-wrapper-title')]//span[contains(., '{{optionTitle}}')]/parent::div/parent::div//button[@class='action-delete']" parameterized="true" timeout="30"/> -+ <element name="customOption" type="block" selector="[data-index='options'] tbody tr.data-row"/> -+ <element name="customOptionButtonDelete" type="button" selector="[data-index='options'] [data-index='delete_button']"/> - -- <element name="checkSelect" type="select" selector="//span[text()='{{var1}}']/parent::div/parent::div/parent::div//label[text()='Option Type']/parent::span/parent::div//div[@data-role='selected-option']" parameterized="true"/> -- <element name="checkDropDown" type="select" selector="//span[text()='{{var1}}']/parent::div/parent::div/parent::div//parent::label/parent::span/parent::div//li[@class='admin__action-multiselect-menu-inner-item']//label[text()='Drop-down']" parameterized="true"/> -+ <element name="optionTypeDropDown" type="select" selector="//table[@data-index='options']//tr[{{index}}]//div[@data-index='type']//div[contains(@class, 'action-select-wrap')]" parameterized="true" timeout="30"/> -+ <element name="optionTypeItem" type="select" selector="//table[@data-index='options']//tr[{{index}}]//div[@data-index='type']//*[contains(@class, 'action-menu-item')]//*[contains(., '{{optionValue}}')]" parameterized="true" timeout="30"/> -+ <element name="checkSelect" type="select" selector="//span[text()='{{var1}}']/parent::div/parent::div/parent::div//span[text()='Option Type']/parent::label/parent::div/parent::div//div[@data-role='selected-option']" parameterized="true"/> -+ <element name="checkOptionType" type="select" selector="//span[text()='{{optionTitle}}']/parent::div/parent::div/parent::div//parent::label/parent::div/parent::div//li[@class='admin__action-multiselect-menu-inner-item']//label[text()='{{optionType}}']" parameterized="true"/> -+ <element name="checkDropDown" type="select" selector="//span[text()='{{var1}}']/parent::div/parent::div/parent::div//parent::label/parent::div/parent::div//li[@class='admin__action-multiselect-menu-inner-item']//label[text()='Drop-down']" parameterized="true"/> - <element name="clickAddValue" type="button" selector="//span[text()='{{var1}}']/parent::div/parent::div/parent::div//tfoot//button" parameterized="true"/> -- <element name="fillOptionValueTitle" type="input" selector="//span[text()='{{var1}}']/parent::div/parent::div/parent::div//tbody/tr[@data-repeat-index='{{var2}}']//label[text()='Title']/parent::span/parent::div//div[@class='admin__field-control']/input" parameterized="true"/> -+ <element name="fillOptionValueTitle" type="input" selector="//span[text()='{{var1}}']/parent::div/parent::div/parent::div//tbody/tr[@data-repeat-index='{{var2}}']//span[text()='Title']/parent::label/parent::div/parent::div//div[@class='admin__field-control']/input" parameterized="true"/> - <element name="fillOptionValuePrice" type="input" selector="//span[text()='{{var1}}']/parent::div/parent::div/parent::div//tbody/tr[@data-repeat-index='{{var2}}']//span[text()='Price']/parent::label/parent::div//div[@class='admin__control-addon']/input" parameterized="true"/> -- <element name="clickSelectPriceType" type="select" selector="//span[text()='{{var1}}']/parent::div/parent::div/parent::div//tbody//tr[@data-repeat-index='{{var2}}']//label[text()='Price Type']/parent::span/parent::div//select" parameterized="true"/> -- <element name="checkboxUseDefaultTitle" type="checkbox" selector="//span[text()='Option Title']/parent::label/parent::div/div//input[@type='checkbox']"/> -+ <element name="clickSelectPriceType" type="select" selector="//span[text()='{{var1}}']/parent::div/parent::div/parent::div//tbody//tr[@data-repeat-index='{{var2}}']//span[text()='Price Type']/parent::label/parent::div/parent::div//select" parameterized="true"/> -+ <element name="checkboxUseDefaultTitle" type="checkbox" selector="//span[text()='Option Title']/parent::label/parent::div/parent::div/div//input[@type='checkbox']"/> - <element name="checkboxUseDefaultOption" type="checkbox" selector="//table[@data-index='values']//tbody//tr[@data-repeat-index='{{var1}}']//div[@class='admin__field-control']//input[@type='checkbox']" parameterized="true"/> -+ <element name="requiredCheckBox" type="checkbox" selector="input[name='product[options][{{index}}][is_require]']" parameterized="true" /> -+ <element name="fillOptionValueSku" type="input" selector="//span[text()='{{var1}}']/parent::div/parent::div/parent::div//tbody/tr[@data-repeat-index='{{var2}}']//span[text()='SKU']/parent::label/parent::div/parent::div//div[@class='admin__field-control']/input" parameterized="true"/> -+ <element name="fillOptionCompatibleFileExtensions" type="input" selector="input[name='product[options][{{index}}][file_extension]']" parameterized="true"/> - - <!-- Elements that make it easier to select the most recently added element --> -+ <element name="optionPriceByTitle" type="input" selector="//*[@data-index='options']//*[@data-role='collapsible-title' and contains(., '{{optionTitle}}')]/ancestor::tr//*[@data-index='price']//input" parameterized="true"/> -+ <element name="optionPriceTypeByTitle" type="select" selector="//*[@data-index='options']//*[@data-role='collapsible-title' and contains(., '{{optionTitle}}')]/ancestor::tr//*[@data-index='price_type']//select" parameterized="true"/> -+ <element name="optionSkuByTitle" type="input" selector="//*[@data-index='options']//*[@data-role='collapsible-title' and contains(., '{{optionTitle}}')]/ancestor::tr//*[@data-index='sku']//input" parameterized="true"/> -+ <element name="optionFileExtensionByTitle" type="input" selector="//*[@data-index='options']//*[@data-role='collapsible-title' and contains(., '{{optionTitle}}')]/ancestor::tr//*[@data-index='file_extension']//input" parameterized="true"/> - <element name="lastOptionTitle" type="input" selector="//*[@data-index='custom_options']//*[@data-index='options']/tbody/tr[last()]//*[contains(@class, '_required')]//input" /> - <element name="lastOptionTypeParent" type="block" selector="//*[@data-index='custom_options']//*[@data-index='options']/tbody/tr[last()]//*[contains(@class, 'admin__action-multiselect-text')]" /> -+ <element name="lastOptionPrice" type="input" selector="//*[@data-index='custom_options']//*[@data-index='options']/tbody/tr[last()]//*[contains(@name, '[price]')]"/> -+ <element name="lastOptionPriceType" type="select" selector="//*[@data-index='custom_options']//*[@data-index='options']/tbody/tr[last()]//*[contains(@name, '[price_type]')]"/> - <!-- var 1 represents the option type that you want to select, i.e "radio buttons" --> - <element name="optionType" type="block" selector="//*[@data-index='custom_options']//label[text()='{{var1}}'][ancestor::*[contains(@class, '_active')]]" parameterized="true" /> -- <element name="addValue" type="button" selector="//*[@data-index='custom_options']//*[@data-index='options']/tbody/tr[last()]//*[@data-action='add_new_row']" /> -+ <element name="addValue" type="button" selector="//*[@data-index='custom_options']//*[@data-index='options']/tbody/tr[last()]//*[@data-action='add_new_row']" timeout="30"/> - <element name="valueTitle" type="input" selector="//*[@data-index='custom_options']//*[@data-index='options']/tbody/tr[last()]//*[contains(@class, 'admin__control-table')]//tbody/tr[last()]//*[@data-index='title']//input" /> - <element name="valuePrice" type="input" selector="//*[@data-index='custom_options']//*[@data-index='options']/tbody/tr[last()]//*[contains(@class, 'admin__control-table')]//tbody/tr[last()]//*[@data-index='price']//input" /> -- -- <element name="optionPrice" type="input" selector="//*[@data-index='custom_options']//*[@data-index='options']/tbody/tr[last()]//*[@name='product[options][0][price]']"/> -- <element name="optionPriceType" type="select" selector="//*[@data-index='custom_options']//*[@data-index='options']/tbody/tr[last()]//*[@name='product[options][0][price_type]']"/> -- <element name="optionSku" type="input" selector="//*[@data-index='custom_options']//*[@data-index='options']/tbody/tr[last()]//*[@name='product[options][0][sku]']"/> -- <element name="optionFileExtensions" type="input" selector="//*[@data-index='custom_options']//*[@data-index='options']/tbody/tr[last()]//*[@name='product[options][0][file_extension]']"/> -+ <element name="optionPrice" type="input" selector="//*[@data-index='custom_options']//*[@data-index='options']/tbody/tr//*[@name='product[options][{{index}}][price]']" parameterized="true"/> -+ <element name="optionPriceType" type="select" selector="//*[@data-index='custom_options']//*[@data-index='options']/tbody/tr//*[@name='product[options][{{var}}][price_type]']" parameterized="true"/> -+ <element name="optionSku" type="input" selector="//*[@data-index='custom_options']//*[@data-index='options']/tbody/tr//*[@name='product[options][{{index}}][sku]']" parameterized="true"/> -+ <element name="optionFileExtensions" type="input" selector="//*[@data-index='custom_options']//*[@data-index='options']/tbody/tr//*[@name='product[options][{{index}}][file_extension]']" parameterized="true"/> -+ <element name="importOptions" type="button" selector="//button[@data-index='button_import']" timeout="30"/> -+ </section> -+ <section name="AdminProductImportOptionsSection"> -+ <element name="selectProductTitle" type="text" selector="//h1[contains(text(), 'Select Product')]" timeout="30"/> -+ <element name="filterButton" type="button" selector="//button[@data-action='grid-filter-expand']" timeout="30"/> -+ <element name="nameField" type="input" selector="//input[@name='name']" timeout="30"/> -+ <element name="applyFiltersButton" type="button" selector="//button[@data-action='grid-filter-apply']" timeout="30"/> -+ <element name="resetFiltersButton" type="button" selector="//button[@data-action='grid-filter-reset']" timeout="30"/> -+ <element name="firstRowItemCheckbox" type="input" selector="//input[@data-action='select-row']" timeout="30"/> -+ <element name="importButton" type="button" selector="//button[contains(@class, 'action-primary')]/span[contains(text(), 'Import')]" timeout="30"/> - </section> --</sections> -\ No newline at end of file -+</sections> -diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductDropdownOrderSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductDropdownOrderSection.xml -new file mode 100644 -index 00000000000..b58fb2316f9 ---- /dev/null -+++ b/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductDropdownOrderSection.xml -@@ -0,0 +1,15 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> -+ <section name="AdminProductDropdownOrderSection"> -+ <element name="simpleProduct" type="text" selector="//li[not(preceding-sibling::li)]//span[@title='Simple Product']"/> -+ <element name="virtualProduct" type="text" selector="//li[not(preceding-sibling::li[span[@title='Bundle Product']]) and not(preceding-sibling::li[span[@title='Downloadable Product']]) and not(following-sibling::li[span[@title='Simple Product']]) and not(following-sibling::li[span[@title='Configurable Product']]) and not(following-sibling::li[span[@title='Grouped Product']])]/span[@title='Virtual Product']"/> -+ </section> -+</sections> -diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductFiltersSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductFiltersSection.xml -index 4c6c1020e19..06ff54b2a39 100644 ---- a/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductFiltersSection.xml -+++ b/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductFiltersSection.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:Page/etc/SectionObject.xsd"> - <section name="AdminProductFiltersSection"> - <element name="filtersButton" type="button" selector="#container > div > div.admin__data-grid-header > div:nth-child(1) > div.data-grid-filters-actions-wrap > div > button"/> - <element name="clearFiltersButton" type="button" selector="//div[@class='admin__data-grid-header']//button[@class='action-tertiary action-clear']" timeout="10"/> -@@ -15,7 +15,7 @@ - <element name="apply" type="button" selector="button[data-action=grid-filter-apply]" timeout="30"/> - <element name="filter" type="button" selector="//div[@class='data-grid-filters-action-wrap']/button" timeout="30"/> - <element name="typeDropDown" type="multiselect" selector="//select[@name='type_id']" timeout="30"/> -- <element name="bundleOption" type="multiselect" selector="//select[@name='type_id']/option[4]" timeout="30"/> -+ <element name="bundleOption" type="multiselect" selector="//select[@name='type_id']/option[@value='bundle']" timeout="30"/> - <element name="applyFilters" type="button" selector="//button[@class='action-secondary']" timeout="30"/> - <element name="allCheckbox" type="checkbox" selector="//div[@data-role='grid-wrapper']//label[@data-bind='attr: {for: ko.uid}']" timeout="30"/> - <element name="actions" type="button" selector="//div[@class='action-select-wrap']/button" timeout="30"/> -@@ -28,5 +28,7 @@ - <element name="priceOfFirstRow" type="text" selector="//tr[@data-repeat-index='0']//div[contains(., '{{var1}}')]" parameterized="true"/> - <element name="AllProductsNotOfBundleType" type="text" selector="//td[5]/div[text() != 'Bundle Product']"/> - <element name="attributeSetOfFirstRow" type="text" selector="//tr[@data-repeat-index='0']//div[contains(., '{{var1}}')]" parameterized="true"/> -+ <element name="storeViewDropDown" type="multiselect" selector="//select[@name='store_id']" timeout="30"/> -+ <element name="storeViewOption" type="multiselect" selector="//select[@name='store_id']/option[contains(text(),'{{var1}}')]" parameterized="true" timeout="30"/> - </section> - </sections> -diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductFormActionSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductFormActionSection.xml -index a314f9cd0dd..1652546b0ac 100644 ---- a/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductFormActionSection.xml -+++ b/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductFormActionSection.xml -@@ -7,13 +7,16 @@ - --> - - <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="AdminProductFormActionSection"> - <element name="backButton" type="button" selector="#back" timeout="30"/> -+ <element name="addAttributeButton" type="button" selector="#addAttribute" timeout="30"/> - <element name="saveButton" type="button" selector="#save-button" timeout="30"/> - <element name="saveArrow" type="button" selector="button[data-ui-id='save-button-dropdown']" timeout="30"/> - <element name="saveAndClose" type="button" selector="span[title='Save & Close']" timeout="30"/> - <element name="changeStoreButton" type="button" selector="#store-change-button" timeout="10"/> - <element name="selectStoreView" type="button" selector="//ul[@data-role='stores-list']/li/a[normalize-space(.)='{{var1}}']" timeout="10" parameterized="true"/> -+ <element name="selectTaxClass" type="select" selector="select[name='product[tax_class_id]']"/> -+ <element name="saveAndDuplicate" type="button" selector="span[id='save_and_duplicate']" timeout="30"/> - </section> - </sections> -diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductFormAdvancedInventorySection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductFormAdvancedInventorySection.xml -new file mode 100644 -index 00000000000..4196a86fe25 ---- /dev/null -+++ b/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductFormAdvancedInventorySection.xml -@@ -0,0 +1,34 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> -+ <section name="AdminProductFormAdvancedInventorySection"> -+ <element name="enableQtyIncrements" type="select" selector="//*[@name='product[stock_data][enable_qty_increments]']"/> -+ <element name="enableQtyIncrementsOptions" type="select" selector="//*[@name='product[stock_data][enable_qty_increments]']//option[contains(@value, '{{var1}}')]" parameterized="true"/> -+ <element name="enableQtyIncrementsUseConfigSettings" type="checkbox" selector="//input[@name='product[stock_data][use_config_enable_qty_inc]']"/> -+ <element name="qtyUsesDecimals" type="select" selector="//*[@name='product[stock_data][is_qty_decimal]']"/> -+ <element name="qtyUsesDecimalsOptions" type="select" selector="//*[@name='product[stock_data][is_qty_decimal]']//option[contains(@value, '{{var1}}')]" parameterized="true"/> -+ <element name="qtyIncrements" type="input" selector="//input[@name='product[stock_data][qty_increments]']"/> -+ <element name="qtyIncrementsUseConfigSettings" type="checkbox" selector="//input[@name='product[stock_data][use_config_qty_increments]']"/> -+ <element name="doneButton" type="button" selector="//aside[contains(@class,'product_form_product_form_advanced_inventory_modal')]//button[contains(@data-role,'action')]" timeout="5"/> -+ <element name="useConfigSettings" type="checkbox" selector="//input[@name='product[stock_data][use_config_manage_stock]']"/> -+ <element name="manageStock" type="select" selector="//*[@name='product[stock_data][manage_stock]']" timeout="30"/> -+ <element name="advancedInventoryCloseButton" type="button" selector=".product_form_product_form_advanced_inventory_modal button.action-close" timeout="30"/> -+ <element name="miniQtyConfigSetting" type="checkbox" selector="//*[@name='product[stock_data][use_config_min_sale_qty]']"/> -+ <element name="miniQtyAllowedInCart" type="input" selector="//*[@name='product[stock_data][min_sale_qty]']"/> -+ <element name="maxiQtyConfigSetting" type="checkbox" selector="//*[@name='product[stock_data][use_config_max_sale_qty]']"/> -+ <element name="maxiQtyAllowedInCart" type="input" selector="//*[@name='product[stock_data][max_sale_qty]']"/> -+ <element name="notifyBelowQtyConfigSetting" type="checkbox" selector="//*[@name='product[stock_data][use_config_notify_stock_qty]']"/> -+ <element name="notifyBelowQty" type="input" selector="//*[@name='product[stock_data][notify_stock_qty]']"/> -+ <element name="advancedInventoryQty" type="input" selector="//div[@class='modal-inner-wrap']//input[@name='product[quantity_and_stock_status][qty]']"/> -+ <element name="advancedInventoryStockStatus" type="select" selector="//div[@class='modal-inner-wrap']//select[@name='product[quantity_and_stock_status][is_in_stock]']"/> -+ <element name="outOfStockThreshold" type="select" selector="//*[@name='product[stock_data][min_qty]']" timeout="30"/> -+ <element name="minQtyConfigSetting" type="checkbox" selector="//input[@name='product[stock_data][use_config_min_qty]']" timeout="30"/> -+ </section> -+</sections> -diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductFormAdvancedPricingSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductFormAdvancedPricingSection.xml -index 1042b1e5a54..77b89a07fb7 100644 ---- a/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductFormAdvancedPricingSection.xml -+++ b/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductFormAdvancedPricingSection.xml -@@ -7,19 +7,23 @@ - --> - - <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="AdminProductFormAdvancedPricingSection"> - <element name="customerGroupPriceAddButton" type="button" selector="[data-action='add_new_row']" timeout="30"/> -+ <element name="addCustomerGroupPrice" type="button" selector="//span[text()='Add']/ancestor::button" timeout="30"/> - <element name="customerGroupPriceDeleteButton" type="button" selector="[data-action='remove_row']" timeout="30"/> - <element name="advancedPricingCloseButton" type="button" selector=".product_form_product_form_advanced_pricing_modal button.action-close" timeout="30"/> - <element name="productTierPriceWebsiteSelect" type="select" selector="[name='product[tier_price][{{var1}}][website_id]']" parameterized="true"/> - <element name="productTierPriceCustGroupSelect" type="select" selector="[name='product[tier_price][{{var1}}][cust_group]']" parameterized="true"/> -+ <element name="productTierPriceCustGroupSelectOptions" type="select" selector="[name='product[tier_price][{{var1}}][cust_group]'] option" parameterized="true"/> - <element name="productTierPriceQtyInput" type="input" selector="[name='product[tier_price][{{var1}}][price_qty]']" parameterized="true"/> - <element name="productTierPriceValueTypeSelect" type="select" selector="[name='product[tier_price][{{var1}}][value_type]']" parameterized="true"/> - <element name="productTierPriceFixedPriceInput" type="input" selector="[name='product[tier_price][{{var1}}][price]']" parameterized="true"/> - <element name="productTierPricePercentageValuePriceInput" type="input" selector="[name='product[tier_price][{{var1}}][percentage_value]']" parameterized="true"/> - <element name="specialPrice" type="input" selector="input[name='product[special_price]']"/> -- <element name="doneButton" type="button" selector=".product_form_product_form_advanced_pricing_modal button.action-primary" timeout="5"/> -- <element name="save" type="button" selector="#save-button"/> -+ <element name="doneButton" type="button" selector=".product_form_product_form_advanced_pricing_modal button.action-primary" timeout="30"/> -+ <element name="msrp" type="input" selector="//input[@name='product[msrp]']" timeout="30"/> -+ <element name="msrpType" type="select" selector="//select[@name='product[msrp_display_actual_price_type]']" timeout="30"/> -+ <element name="save" type="button" selector="#save-button" timeout="30"/> - </section> - </sections> -diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductFormAttributeSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductFormAttributeSection.xml -new file mode 100644 -index 00000000000..b0aee1795dc ---- /dev/null -+++ b/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductFormAttributeSection.xml -@@ -0,0 +1,40 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> -+ <section name="AdminProductFormAttributeSection"> -+ <element name="createNewAttribute" type="button" selector="//button[@data-index='add_new_attribute_button']" timeout="30"/> -+ </section> -+ <section name="AdminProductFormNewAttributeSection"> -+ <element name="attributeLabel" type="button" selector="//input[@name='frontend_label[0]']" timeout="30"/> -+ <element name="attributeType" type="select" selector="//select[@name='frontend_input']" timeout="30"/> -+ <element name="addValue" type="button" selector="//button[@data-action='add_new_row']" timeout="30"/> -+ <element name="optionViewName" type="text" selector="//table[@data-index='attribute_options_select']//span[contains(text(), '{{arg}}')]" parameterized="true" timeout="30"/> -+ <element name="optionValue" type="input" selector="(//input[contains(@name, 'option[value]')])[{{arg}}]" timeout="30" parameterized="true"/> -+ <element name="manageTitlesHeader" type="button" selector="//div[@class='fieldset-wrapper-title']//span[contains(text(), 'Manage Titles')]" timeout="30"/> -+ <element name="manageTitlesViewName" type="text" selector="//div[@data-index='manage-titles']//span[contains(text(), '{{arg}}')]" timeout="30" parameterized="true"/> -+ <element name="saveAttribute" type="button" selector="button#save" timeout="30"/> -+ <element name="saveInNewSet" type="button" selector="button#saveInNewSet" timeout="10"/> -+ </section> -+ <section name="AdminProductFormNewAttributeAdvancedSection"> -+ <element name="sectionHeader" type="button" selector="div[data-index='advanced_fieldset']"/> -+ <element name="defaultValue" type="textarea" selector="input[name='default_value_text']"/> -+ <element name="scope" type="select" selector="//div[@data-index='advanced_fieldset']//select[@name='is_global']"/> -+ </section> -+ <section name="AdminProductFormNewAttributeStorefrontSection"> -+ <element name="sectionHeader" type="button" selector="div[data-index='front_fieldset']"/> -+ <element name="useInSearch" type="checkbox" selector="div[data-index='is_searchable'] .admin__field-control label"/> -+ <element name="searchWeight" type="select" selector="select[name='search_weight']"/> -+ </section> -+ <section name="AdminProductFormNewAttributeNewSetSection"> -+ <element name="setName" type="button" selector="//div[contains(@class, 'modal-inner-wrap') and .//*[contains(., 'Enter Name for New Attribute Set')]]//input[contains(@class, 'admin__control-text')]"/> -+ <element name="accept" type="button" selector="//div[contains(@class, 'modal-inner-wrap') and .//*[contains(., 'Enter Name for New Attribute Set')]]//button[contains(@class, 'action-accept')]"/> -+ <element name="cancel" type="button" selector="//div[contains(@class, 'modal-inner-wrap') and .//*[contains(., 'Enter Name for New Attribute Set')]]//button[contains(@class, 'action-dismiss')]"/> -+ </section> -+</sections> -diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductFormChangeStoreSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductFormChangeStoreSection.xml -index 8c9e92d912b..04e5445c8ab 100644 ---- a/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductFormChangeStoreSection.xml -+++ b/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductFormChangeStoreSection.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:Page/etc/SectionObject.xsd"> - <section name="AdminProductFormChangeStoreSection"> - <element name="storeSelector" type="button" selector="//a[contains(text(),'{{var1}}')]" parameterized="true"/> - <element name="acceptButton" type="button" selector="button[class='action-primary action-accept']" timeout="30"/> -diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductFormSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductFormSection.xml -index 604eba61a05..0dd9e6d948c 100644 ---- a/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductFormSection.xml -+++ b/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductFormSection.xml -@@ -6,25 +6,41 @@ - */ - --> - <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="AdminProductFormSection"> - <element name="attributeSet" type="select" selector="div[data-index='attribute_set_id'] .admin__field-control"/> - <element name="attributeSetFilter" type="input" selector="div[data-index='attribute_set_id'] .admin__field-control input" timeout="30"/> - <element name="attributeSetFilterResult" type="input" selector="div[data-index='attribute_set_id'] .action-menu-item._last" timeout="30"/> -+ <element name="attributeSetFilterResultByName" type="text" selector="//label/span[text() = '{{var}}']" timeout="30" parameterized="true"/> - <element name="productName" type="input" selector=".admin__field[data-index=name] input"/> -+ <element name="productNameDisabled" type="input" selector=".admin__field[data-index=name] input[disabled=true]"/> -+ <element name="RequiredNameIndicator" type="text" selector=" return window.getComputedStyle(document.querySelector('._required[data-index=name]>.admin__field-label span'), ':after').getPropertyValue('content');"/> -+ <element name="RequiredSkuIndicator" type="text" selector=" return window.getComputedStyle(document.querySelector('._required[data-index=sku]>.admin__field-label span'), ':after').getPropertyValue('content');"/> - <element name="productSku" type="input" selector=".admin__field[data-index=sku] input"/> -- <element name="enableProductAttributeLabel" type="text" selector="//label[text()='Enable Product']"/> -- <element name="enableProductAttributeLabelWrapper" type="text" selector="//label[text()='Enable Product']/parent::span"/> -+ <element name="productSkuDisabled" type="input" selector=".admin__field[data-index=sku] input[disabled=true]"/> -+ <element name="enableProductAttributeLabel" type="text" selector="//span[text()='Enable Product']/parent::label"/> -+ <element name="enableProductAttributeLabelWrapper" type="text" selector="//span[text()='Enable Product']/parent::label/parent::div"/> - <element name="productStatus" type="checkbox" selector="input[name='product[status]']"/> -+ <element name="productStatusValue" type="checkbox" selector="input[name='product[status]'][value='{{value}}']" timeout="30" parameterized="true"/> -+ <element name="productStatusDisabled" type="checkbox" selector="input[name='product[status]'][disabled]"/> - <element name="enableProductLabel" type="checkbox" selector="input[name='product[status]']+label"/> - <element name="productStatusUseDefault" type="checkbox" selector="input[name='use_default[status]']"/> - <element name="productNameUseDefault" type="checkbox" selector="input[name='use_default[name]']"/> - <element name="productPrice" type="input" selector=".admin__field[data-index=price] input"/> -+ <element name="productPriceDisabled" type="input" selector=".admin__field[data-index=price] input[disabled=true]"/> -+ <element name="productPriceUseDefault" type="checkbox" selector=".admin__field[data-index=price] [name='use_default[price]']"/> -+ <element name="productTaxClass" type="select" selector="//*[@name='product[tax_class_id]']"/> -+ <element name="productTaxClassDisabled" type="select" selector="select[name='product[tax_class_id]'][disabled=true]"/> - <element name="productTaxClassUseDefault" type="checkbox" selector="input[name='use_default[tax_class_id]']"/> -- <element name="advancedPricingLink" type="button" selector="button[data-index='advanced_pricing_button']"/> -- <element name="categoriesDropdown" type="multiselect" selector="div[data-index='category_ids']"/> -+ <element name="advancedPricingLink" type="button" selector="button[data-index='advanced_pricing_button']" timeout="30"/> -+ <element name="currentCategory" type="text" selector=".admin__action-multiselect-crumb > span"/> -+ <element name="categoriesDropdown" type="multiselect" selector="div[data-index='category_ids']" timeout="30"/> -+ <element name="unselectCategories" type="button" selector="//span[@class='admin__action-multiselect-crumb']/span[contains(.,'{{category}}')]/../button[@data-action='remove-selected-item']" parameterized="true" timeout="30"/> - <element name="productQuantity" type="input" selector=".admin__field[data-index=qty] input"/> -- <element name="productStockStatus" type="select" selector="select[name='product[quantity_and_stock_status][is_in_stock]']"/> -+ <element name="advancedInventoryLink" type="button" selector="//button[contains(@data-index, 'advanced_inventory_button')]" timeout="30"/> -+ <element name="productStockStatus" type="select" selector="select[name='product[quantity_and_stock_status][is_in_stock]']" timeout="30"/> -+ <element name="productStockStatusDisabled" type="select" selector="select[name='product[quantity_and_stock_status][is_in_stock]'][disabled=true]"/> -+ <element name="stockStatus" type="select" selector="[data-index='product-details'] select[name='product[quantity_and_stock_status][is_in_stock]']"/> - <element name="productWeight" type="input" selector=".admin__field[data-index=weight] input"/> - <element name="productWeightSelect" type="select" selector="select[name='product[product_has_weight]']"/> - <element name="contentTab" type="button" selector="//strong[contains(@class, 'admin__collapsible-title')]/span[text()='Content']"/> -@@ -32,30 +48,52 @@ - <element name="priceFieldError" type="text" selector="//input[@name='product[price]']/parent::div/parent::div/label[@class='admin__field-error']"/> - <element name="addAttributeBtn" type="button" selector="#addAttribute"/> - <element name="createNewAttributeBtn" type="button" selector="button[data-index='add_new_attribute_button']"/> -- <element name="save" type="button" selector="#save"/> -+ <element name="save" type="button" selector="#save-button" timeout="30"/> -+ <element name="saveNewAttribute" type="button" selector="//aside[contains(@class, 'create_new_attribute_modal')]//button[@id='save']"/> -+ <element name="successMessage" type="text" selector="#messages"/> - <element name="attributeTab" type="button" selector="//strong[contains(@class, 'admin__collapsible-title')]/span[text()='Attributes']"/> - <element name="attributeLabel" type="input" selector="//input[@name='frontend_label[0]']"/> - <element name="frontendInput" type="select" selector="select[name = 'frontend_input']"/> - <element name="productFormTab" type="button" selector="//strong[@class='admin__collapsible-title']/span[contains(text(), '{{tabName}}')]" parameterized="true"/> - <element name="productFormTabState" type="text" selector="//strong[@class='admin__collapsible-title']/span[contains(text(), '{{tabName}}')]/parent::*/parent::*[@data-state-collapsible='{{state}}']" parameterized="true"/> - <element name="visibility" type="select" selector="//select[@name='product[visibility]']"/> -+ <element name="visibilityDisabled" type="select" selector="select[name='product[visibility]'][disabled=true]"/> - <element name="visibilityUseDefault" type="checkbox" selector="//input[@name='use_default[visibility]']"/> - <element name="divByDataIndex" type="input" selector="div[data-index='{{var}}']" parameterized="true"/> -- <element name="attributeLabelByText" type="text" selector="//*[@class='admin__field']//label[text()='{{attributeLabel}}']" parameterized="true"/> -+ <element name="setProductAsNewFrom" type="input" selector="input[name='product[news_from_date]']"/> -+ <element name="setProductAsNewTo" type="input" selector="input[name='product[news_to_date]']"/> -+ <element name="attributeLabelByText" type="text" selector="//*[@class='admin__field']//span[text()='{{attributeLabel}}']" parameterized="true"/> -+ <element name="attributeRequiredInput" type="input" selector="//input[contains(@name, 'product[{{attributeCode}}]')]" parameterized="true"/> -+ <element name="attributeFieldError" type="text" selector="//*[@class='admin__field _required _error']/..//label[contains(.,'This is a required field.')]"/> -+ <element name="customSelectField" type="select" selector="//select[@name='product[{{var}}]']" parameterized="true"/> -+ <element name="searchCategory" type="input" selector="//*[@data-index='category_ids']//input[contains(@class, 'multiselect-search')]" timeout="30"/> -+ <element name="selectCategory" type="input" selector="//*[@data-index='category_ids']//label[contains(., '{{categoryName}}')]" parameterized="true" timeout="30"/> -+ <element name="done" type="button" selector="//*[@data-index='category_ids']//button[@data-action='close-advanced-select']" timeout="30"/> -+ <element name="selectMultipleCategories" type="input" selector="//*[@data-index='container_category_ids']//*[contains(@class, '_selected')]"/> -+ <element name="countryOfManufacture" type="select" selector="select[name='product[country_of_manufacture]']"/> -+ <element name="newAddedAttribute" type="text" selector="//fieldset[@class='admin__fieldset']//div[contains(@data-index,'{{attributeCode}}')]" parameterized="true"/> -+ <element name="newCategoryButton" type="button" selector="button[data-index='create_category_button']" timeout="30"/> -+ <element name="footerBlock" type="block" selector="//footer"/> - </section> - <section name="ProductInWebsitesSection"> - <element name="sectionHeader" type="button" selector="div[data-index='websites']" timeout="30"/> -+ <element name="sectionHeaderOpened" type="button" selector="[data-index='websites']._show" timeout="30"/> - <element name="website" type="checkbox" selector="//label[contains(text(), '{{var1}}')]/parent::div//input[@type='checkbox']" parameterized="true"/> -+ <element name="websiteChecked" type="checkbox" selector="//label[contains(text(), '{{var1}}')]/parent::div//input[@type='checkbox'][@value='1']" parameterized="true"/> - </section> - <section name="ProductDesignSection"> - <element name="DesignTab" type="button" selector="//strong[@class='admin__collapsible-title']//span[text()='Design']"/> - <element name="LayoutDropdown" type="select" selector="select[name='product[page_layout]']"/> -+ <element name="productOptionsContainer" type="select" selector="select[name='product[options_container]']"/> - </section> - <section name="AdminProductFormRelatedUpSellCrossSellSection"> -+ <element name="relatedProductsHeader" type="button" selector=".admin__collapsible-block-wrapper[data-index='related']" timeout="30"/> - <element name="AddRelatedProductsButton" type="button" selector="button[data-index='button_related']" timeout="30"/> -+ <element name="addUpSellProduct" type="button" selector="button[data-index='button_upsell']" timeout="30"/> - </section> - <section name="AdminAddRelatedProductsModalSection"> -- <element name="AddSelectedProductsButton" type="button" selector="//aside[contains(@class, 'product_form_product_form_related_related_modal')]//button/span[contains(text(), 'Add Selected Products')]" timeout="30"/> -+ <element name="AddSelectedProductsButton" type="button" selector="//aside[contains(@class, 'related_modal')]//button[contains(@class, 'action-primary')]" timeout="30"/> -+ <element name="AddUpSellProductsButton" type="button" selector="//aside[contains(@class, 'upsell_modal')]//button[contains(@class, 'action-primary')]" timeout="30"/> - </section> - <section name="ProductWYSIWYGSection"> - <element name="Switcher" type="button" selector="//select[@id='dropdown-switcher']"/> -@@ -99,24 +137,24 @@ - <element name="Numlist" type="button" selector="//div[@id='editorproduct_form_description']//i[@class='mce-ico mce-i-bullist']" /> - <element name="Bullet" type="button" selector="//div[@id='editorproduct_form_description']//i[@class='mce-ico mce-i-numlist']" /> - <element name="InsertLink" type="button" selector="//div[@id='editorproduct_form_description']//i[@class='mce-ico mce-i-link']" /> -- <element name="InsertImageIcon" type="button" selector="//div[@id='editorproduct_form_description']//i[@class='mce-ico mce-i-image']" /> -+ <element name="InsertImageIcon" type="button" selector="//div[@id='editorproduct_form_description']//i[@class='mce-ico mce-i-image']" timeout="30"/> - <element name="InsertTable" type="button" selector="//div[@id='editorproduct_form_description']//i[@class='mce-ico mce-i-table']" /> - <element name="SpecialCharacter" type="button" selector="//div[@id='editorproduct_form_description']//i[@class='mce-ico mce-i-charmap']" /> -- <element name="Browse" type="button" selector=".mce-i-browse"/> -- <element name="BrowseUploadImage" type="file" selector=".fileupload" /> -+ <element name="Browse" type="button" selector=".mce-i-browse" timeout="30"/> -+ <element name="BrowseUploadImage" type="file" selector=".fileupload" timeout="30"/> - <element name="image" type="text" selector="//small[text()='{{var1}}']" parameterized="true"/> - <element name="imageSelected" type="text" selector="//small[text()='{{var1}}']/parent::*[@class='filecnt selected']" parameterized="true"/> - <element name="ImageSource" type="input" selector=".mce-combobox.mce-abs-layout-item.mce-last.mce-has-open" /> - <element name="ImageDescription" type="input" selector=".mce-textbox.mce-abs-layout-item.mce-last" /> - <element name="Height" type="input" selector=".mce-textbox.mce-abs-layout-item.mce-first" /> - <element name="UploadImage" type="file" selector=".fileupload" /> -- <element name="OkBtn" type="button" selector="//span[text()='Ok']"/> -- <element name="InsertFile" type="text" selector="#insert_files"/> -- <element name="CreateFolder" type="button" selector="#new_folder" /> -- <element name="DeleteSelectedBtn" type="text" selector="#delete_files"/> -- <element name="CancelBtn" type="button" selector="#cancel" /> -+ <element name="OkBtn" type="button" selector="//button//span[text()='Ok']"/> -+ <element name="InsertFile" type="text" selector="#insert_files" timeout="30"/> -+ <element name="CreateFolder" type="button" selector="#new_folder" timeout="30"/> -+ <element name="DeleteSelectedBtn" type="text" selector="#delete_files" timeout="30"/> -+ <element name="CancelBtn" type="button" selector=".page-actions #cancel" /> - <element name="FolderName" type="button" selector="input[data-role='promptField']" /> -- <element name="AcceptFolderName" type="button" selector=".action-primary.action-accept" /> -+ <element name="AcceptFolderName" type="button" selector=".action-primary.action-accept" timeout="30"/> - <element name="StorageRootArrow" type="button" selector="#root > .jstree-icon" /> - <element name="checkIfArrowExpand" type="button" selector="//li[@id='root' and contains(@class,'jstree-closed')]" /> - <element name="WysiwygArrow" type="button" selector="#d3lzaXd5Zw-- > .jstree-icon" /> -@@ -137,21 +175,21 @@ - <element name="Numlist" type="button" selector="//div[@id='editorproduct_form_short_description']//i[@class='mce-ico mce-i-bullist']" /> - <element name="Bullet" type="button" selector="//div[@id='editorproduct_form_short_description']//i[@class='mce-ico mce-i-numlist']" /> - <element name="InsertLink" type="button" selector="//div[@id='editorproduct_form_short_description']//i[@class='mce-ico mce-i-link']" /> -- <element name="InsertImageIcon" type="button" selector="//div[@id='editorproduct_form_short_description']//i[@class='mce-ico mce-i-image']" /> -+ <element name="InsertImageIcon" type="button" selector="//div[@id='editorproduct_form_short_description']//i[@class='mce-ico mce-i-image']" timeout="30"/> - <element name="InsertTable" type="button" selector="//div[@id='editorproduct_form_short_description']//i[@class='mce-ico mce-i-table']" /> - <element name="SpecialCharacter" type="button" selector="//div[@id='editorproduct_form_short_description']//i[@class='mce-ico mce-i-charmap']"/> - <element name="Browse" type="button" selector=".mce-i-browse"/> -- <element name="BrowseUploadImage" type="file" selector=".fileupload" /> -+ <element name="BrowseUploadImage" type="file" selector=".fileupload" timeout="30" /> - <element name="image" type="text" selector="//small[text()='{{var1}}']" parameterized="true"/> - <element name="imageSelected" type="text" selector="//small[text()='{{var1}}']/parent::*[@class='filecnt selected']" parameterized="true"/> - <element name="ImageSource" type="input" selector=".mce-combobox.mce-abs-layout-item.mce-last.mce-has-open" /> - <element name="ImageDescription" type="input" selector=".mce-textbox.mce-abs-layout-item.mce-last" /> - <element name="Height" type="input" selector=".mce-textbox.mce-abs-layout-item.mce-first" /> - <element name="UploadImage" type="file" selector=".fileupload" /> -- <element name="OkBtn" type="button" selector="//span[text()='Ok']"/> -+ <element name="OkBtn" type="button" selector="//span[text()='Ok']" timeout="30"/> - <element name="InsertFile" type="text" selector="#insert_files"/> - <element name="CreateFolder" type="button" selector="#new_folder" /> -- <element name="DeleteSelectedBtn" type="text" selector="#delete_files"/> -+ <element name="DeleteSelectedBtn" type="text" selector="#delete_files" timeout="30"/> - <element name="CancelBtn" type="button" selector="#cancel" /> - <element name="FolderName" type="button" selector="input[data-role='promptField']" /> - <element name="AcceptFolderName" type="button" selector=".action-primary.action-accept" /> -@@ -159,8 +197,25 @@ - <element name="checkIfArrowExpand" type="button" selector="//li[@id='root' and contains(@class,'jstree-closed')]" /> - <element name="confirmDelete" type="button" selector=".action-primary.action-accept" /> - </section> -+ <section name="ProductDescriptionWysiwygSection"> -+ <element name="EditArea" type="text" selector="#editorproduct_form_description .mce-edit-area"/> -+ <element name="attributeEditArea" type="textarea" selector="#product_form_{{attributeCode}}" parameterized="true" timeout="30"/> -+ </section> -+ <section name="ProductShortDescriptionWysiwygSection"> -+ <element name="EditArea" type="text" selector="#editorproduct_form_short_description .mce-edit-area"/> -+ </section> - <section name="AdminProductFormAdvancedPricingSection"> - <element name="specialPrice" type="input" selector="input[name='product[special_price]']"/> -- <element name="doneButton" type="button" selector=".product_form_product_form_advanced_pricing_modal button.action-primary"/> -+ <element name="doneButton" type="button" selector=".product_form_product_form_advanced_pricing_modal button.action-primary" timeout="30"/> -+ <element name="useDefaultPrice" type="checkbox" selector="//input[@name='product[special_price]']/parent::div/following-sibling::div/input[@name='use_default[special_price]']"/> -+ </section> -+ <section name="AdminProductAttributeSection"> -+ <element name="attributeSectionHeader" type="button" selector="//div[@data-index='attributes']" timeout="30"/> -+ <element name="textAttributeByCode" type="text" selector="//input[@name='product[{{arg}}]']" parameterized="true"/> -+ <element name="textAttributeByName" type="text" selector="//div[@data-index='attributes']//fieldset[contains(@class, 'admin__field') and .//*[contains(.,'{{var}}')]]//input" parameterized="true"/> -+ <element name="dropDownAttribute" type="select" selector="//select[@name='product[{{arg}}]']" parameterized="true" timeout="30"/> -+ <element name="attributeSection" type="block" selector="//div[@data-index='attributes']/div[contains(@class, 'admin__collapsible-content _show')]" timeout="30"/> -+ <element name="attributeGroupByName" type="button" selector="//div[@class='fieldset-wrapper-title']//span[text()='{{group}}']" parameterized="true"/> -+ <element name="attributeByGroupAndName" type="text" selector="//div[@class='fieldset-wrapper-title']//span[text()='{{group}}']/../../following-sibling::div//span[contains(text(),'attribute')]" parameterized="true"/> - </section> - </sections> -diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductGiftOptionsSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductGiftOptionsSection.xml -new file mode 100644 -index 00000000000..63b745e5227 ---- /dev/null -+++ b/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductGiftOptionsSection.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="AdminProductGiftOptionsSection"> -+ <element name="giftOptions" type="text" selector="div[data-index='gift-options']"/> -+ <element name="useConfigSettingsMessage" type="checkbox" selector="[name='product[use_config_gift_message_available]']"/> -+ <element name="toggleProductGiftMessage" type="button" selector="input[name='product[gift_message_available]']+label"/> -+ <element name="giftMessageStatus" type="checkbox" selector="input[name='product[gift_message_available]'][value='{{status}}']" parameterized="true"/> -+ </section> -+</sections> -diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductGridActionSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductGridActionSection.xml -index 4ce9580405a..66e6f17be34 100644 ---- a/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductGridActionSection.xml -+++ b/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductGridActionSection.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="AdminProductGridActionSection"> -- <element name="addProductToggle" type="button" selector=".action-toggle.primary.add"/> -+ <element name="addProductBtn" type="button" selector="#add_new_product-button" timeout="30"/> -+ <element name="addProductToggle" type="button" selector=".action-toggle.primary.add" timeout="30"/> - <element name="addSimpleProduct" type="button" selector=".item[data-ui-id='products-list-add-new-product-button-item-simple']" timeout="30"/> - <element name="addGroupedProduct" type="button" selector=".item[data-ui-id='products-list-add-new-product-button-item-grouped']" timeout="30"/> - <element name="addVirtualProduct" type="button" selector=".item[data-ui-id='products-list-add-new-product-button-item-virtual']" timeout="30"/> -diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductGridConfirmActionSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductGridConfirmActionSection.xml -index d8567df81b6..5bf73076e14 100644 ---- a/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductGridConfirmActionSection.xml -+++ b/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductGridConfirmActionSection.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="AdminProductGridConfirmActionSection"> - <element name="title" type="text" selector=".modal-popup.confirm h1.modal-title"/> - <element name="message" type="text" selector=".modal-popup.confirm div.modal-content"/> -diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductGridFilterSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductGridFilterSection.xml -index 4683576bf95..3b6f24c0f25 100644 ---- a/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductGridFilterSection.xml -+++ b/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductGridFilterSection.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="AdminProductGridFilterSection"> - <element name="filters" type="button" selector="button[data-action='grid-filter-expand']"/> - <element name="clearAll" type="button" selector=".admin__data-grid-header .admin__data-grid-filters-current._show .action-clear" timeout="30"/> -@@ -28,8 +28,13 @@ - <element name="priceFilterTo" type="input" selector="input.admin__control-text[name='price[to]']"/> - <element name="typeFilter" type="select" selector="select.admin__control-select[name='type_id']"/> - <element name="statusFilter" type="select" selector="select.admin__control-select[name='status']"/> -+ <element name="firstRowBySku" type="button" selector="//div[text()='{{var}}']/ancestor::tr" parameterized="true" timeout="30"/> - <element name="newFromDateFilter" type="input" selector="input.admin__control-text[name='news_from_date[from]']"/> - <element name="keywordSearch" type="input" selector="input#fulltext"/> - <element name="keywordSearchButton" type="button" selector=".data-grid-search-control-wrap button.action-submit" timeout="30"/> -+ <element name="nthRow" type="block" selector=".data-row:nth-of-type({{var}})" parameterized="true" timeout="30"/> -+ <element name="productCount" type="text" selector="#catalog_category_products-total-count"/> -+ <element name="productPerPage" type="select" selector="#catalog_category_products_page-limit"/> -+ <element name="storeViewDropdown" type="text" selector="//select[@name='store_id']/option[contains(.,'{{storeView}}')]" parameterized="true"/> - </section> - </sections> -diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductGridPaginationSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductGridPaginationSection.xml -index 9ef89e1260f..fbcfabfa02f 100644 ---- a/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductGridPaginationSection.xml -+++ b/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductGridPaginationSection.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="AdminProductGridPaginationSection"> - <element name="perPageDropdown" type="select" selector=".admin__data-grid-pager-wrap .selectmenu"/> - <element name="perPageOption" type="button" selector="//div[@class='admin__data-grid-pager-wrap']//div[@class='selectmenu-items _active']//li//button[text()='{{label}}']" parameterized="true"/> -diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductGridSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductGridSection.xml -index 98afed124c6..07dd26381fe 100644 ---- a/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductGridSection.xml -+++ b/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductGridSection.xml -@@ -6,8 +6,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="AdminProductGridSection"> -+ <element name="productRowBySku" type="block" selector="//td[count(../../..//th[./*[.='SKU']]/preceding-sibling::th) + 1][./*[.='{{sku}}']]" parameterized="true" /> -+ <element name="productRowCheckboxBySku" type="block" selector="//td[count(../../..//th[./*[.='SKU']]/preceding-sibling::th) + 1][./*[.='{{sku}}']]/../td//input[@data-action='select-row']" parameterized="true" /> - <element name="loadingMask" type="text" selector=".admin__data-grid-loading-mask[data-component*='product_listing']"/> - <element name="columnHeader" type="button" selector="//div[@data-role='grid-wrapper']//table[contains(@class, 'data-grid')]/thead/tr/th[contains(@class, 'data-grid-th')]/span[text() = '{{label}}']" parameterized="true" timeout="30"/> - <element name="column" type="text" selector="//tr//td[count(//div[@data-role='grid-wrapper']//tr//th[contains(., '{{column}}')]/preceding-sibling::th) +1 ]" parameterized="true"/> -@@ -15,6 +17,8 @@ - <element name="productGridElement2" type="text" selector="#addselector" /> - <element name="productGridRows" type="text" selector="table.data-grid tr.data-row"/> - <element name="firstProductRow" type="text" selector="table.data-grid tr.data-row:first-of-type"/> -+ <element name="firstProductRowName" type="text" selector="table.data-grid tr.data-row:first-of-type > td:nth-of-type(4)"/> -+ <element name="firstProductRowEditButton" type="button" selector="table.data-grid tr.data-row td .action-menu-item:first-of-type"/> - <element name="productThumbnail" type="text" selector="table.data-grid tr:nth-child({{row}}) td.data-grid-thumbnail-cell > img" parameterized="true"/> - <element name="productThumbnailBySrc" type="text" selector="img.admin__control-thumbnail[src*='{{pattern}}']" parameterized="true"/> - <element name="productGridCell" type="text" selector="//tr[{{row}}]//td[count(//div[@data-role='grid-wrapper']//tr//th[contains(., '{{column}}')]/preceding-sibling::th) +1 ]" parameterized="true"/> -@@ -28,6 +32,8 @@ - <element name="firstRow" type="button" selector="tr.data-row:nth-of-type(1)"/> - <element name="productGridCheckboxOnRow" type="checkbox" selector="//*[@id='container']//tr[{{row}}]/td[1]//input" parameterized="true"/> - <element name="productGridNameProduct" type="input" selector="//tbody//tr//td//div[contains(., '{{var1}}')]" parameterized="true" timeout="30"/> -+ <element name="productGridContentsOnRow" type="checkbox" selector="//*[@id='container']//tr[{{row}}]/td" parameterized="true"/> - <element name="selectRowBasedOnName" type="input" selector="//td/div[text()='{{var1}}']" parameterized="true"/> -+ <element name="changeStatus" type="button" selector="//div[contains(@class,'admin__data-grid-header-row') and contains(@class, 'row')]//div[contains(@class, 'action-menu-item')]//ul/li/span[text() = '{{status}}']" parameterized="true"/> - </section> - </sections> -diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductGridTableHeaderSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductGridTableHeaderSection.xml -index fc6ccea20d3..7341a6ded7a 100644 ---- a/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductGridTableHeaderSection.xml -+++ b/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductGridTableHeaderSection.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="AdminProductGridTableHeaderSection"> - <element name="id" type="button" selector=".//*[@class='sticky-header']/following-sibling::*//th[@class='data-grid-th _sortable _draggable _{{order}}']/span[text()='ID']" parameterized="true"/> - </section> -diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductImagePlaceholderConfigSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductImagePlaceholderConfigSection.xml -new file mode 100644 -index 00000000000..7558b13d624 ---- /dev/null -+++ b/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductImagePlaceholderConfigSection.xml -@@ -0,0 +1,37 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> -+ <section name="AdminProductImagePlaceholderConfigSection"> -+ <element name="sectionHeader" type="text" selector="#catalog_placeholder-head"/> -+ <!--Base image placeholder--> -+ <element name="baseImageInput" type="file" selector="#catalog_placeholder_image_placeholder" timeout="10"/> -+ <element name="baseImageDelete" type="checkbox" selector="#catalog_placeholder_image_placeholder_delete"/> -+ <element name="baseImage" type="text" selector="#catalog_placeholder_image_placeholder_image"/> -+ <element name="baseImageBySrc" type="text" selector="#catalog_placeholder_image_placeholder_image[src*='{{var}}']" parameterized="true"/> -+ -+ <!--Small image placeholder--> -+ <element name="smallImageInput" type="file" selector="#catalog_placeholder_small_image_placeholder" timeout="10"/> -+ <element name="smallImageDelete" type="checkbox" selector="#catalog_placeholder_small_image_placeholder_delete"/> -+ <element name="smallImage" type="text" selector="#catalog_placeholder_small_image_placeholder_image"/> -+ <element name="smallImageBySrc" type="text" selector="#catalog_placeholder_small_image_placeholder_image[src*='{{var}}']" parameterized="true"/> -+ -+ <!--Swatch image placeholder--> -+ <element name="swatchImageInput" type="file" selector="#catalog_placeholder_swatch_image_placeholder" timeout="10"/> -+ <element name="swatchImageDelete" type="checkbox" selector="#catalog_placeholder_swatch_image_placeholder_delete"/> -+ <element name="swatchImage" type="text" selector="#catalog_placeholder_swatch_image_placeholder_image"/> -+ <element name="swatchImageBySrc" type="text" selector="#catalog_placeholder_swatch_image_placeholder_image[src*='{{var}}']" parameterized="true"/> -+ -+ <!--Thumbnail image placeholder--> -+ <element name="thumbnailImageInput" type="file" selector="#catalog_placeholder_thumbnail_placeholder" timeout="10"/> -+ <element name="thumbnailImageDelete" type="checkbox" selector="#catalog_placeholder_thumbnail_placeholder_delete"/> -+ <element name="thumbnailImage" type="text" selector="#catalog_placeholder_thumbnail_placeholder_image"/> -+ <element name="thumbnailImageBySrc" type="text" selector="#catalog_placeholder_thumbnail_placeholder_image[src*='{{var}}']" parameterized="true"/> -+ </section> -+</sections> -diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductImagesSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductImagesSection.xml -index ce10b1e52ae..89eb1ed678c 100644 ---- a/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductImagesSection.xml -+++ b/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductImagesSection.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="AdminProductImagesSection"> - <element name="productImagesToggle" type="button" selector="div[data-index=gallery] .admin__collapsible-title"/> - <element name="imageFileUpload" type="input" selector="#fileupload"/> -@@ -16,6 +16,7 @@ - <element name="removeImageButton" type="button" selector=".action-remove"/> - <element name="modalOkBtn" type="button" selector="button.action-primary.action-accept"/> - <element name="uploadProgressBar" type="text" selector=".uploader .file-row"/> -+ <element name="productImagesToggleState" type="button" selector="[data-index='gallery'] > [data-state-collapsible='{{status}}']" parameterized="true"/> - - <element name="nthProductImage" type="button" selector="#media_gallery_content > div:nth-child({{var}}) img.product-image" parameterized="true"/> - <element name="nthRemoveImageBtn" type="button" selector="#media_gallery_content > div:nth-child({{var}}) button.action-remove" parameterized="true"/> -@@ -32,4 +33,4 @@ - <element name="isThumbnailSelected" type="button" selector="//div[contains(@class, 'field-image-role')]//ul/li[contains(@class, 'selected')]/label[normalize-space(.) = 'Thumbnail']"/> - <element name="isSwatchSelected" type="button" selector="//div[contains(@class, 'field-image-role')]//ul/li[contains(@class, 'selected')]/label[normalize-space(.) = 'Swatch']"/> - </section> --</sections> -\ No newline at end of file -+</sections> -diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductMessagesSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductMessagesSection.xml -index 5f2e6bd6cf7..59fbeee142d 100644 ---- a/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductMessagesSection.xml -+++ b/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductMessagesSection.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="AdminProductMessagesSection"> - <element name="successMessage" type="text" selector=".message-success"/> - <element name="errorMessage" type="text" selector=".message.message-error.error"/> -diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductModalSlideGridSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductModalSlideGridSection.xml -index bef213e6cda..dbdc8202694 100644 ---- a/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductModalSlideGridSection.xml -+++ b/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductModalSlideGridSection.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="AdminProductModalSlideGridSection"> - <element name="productGridXRowYColumnButton" type="input" selector=".modal-slide table.data-grid tr.data-row:nth-child({{row}}) td:nth-child({{column}})" parameterized="true" timeout="30"/> -+ <element name="productRowCheckboxBySku" type="input" selector="//td[count(../../..//th[./*[.='SKU']]/preceding-sibling::th) + 1][./*[.='{{sku}}']]/../td//input[@data-action='select-row']" parameterized="true" /> - </section> - </sections> -diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductRelatedUpSellCrossSellSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductRelatedUpSellCrossSellSection.xml -index 636a7b5c85e..f3b0d3a895c 100644 ---- a/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductRelatedUpSellCrossSellSection.xml -+++ b/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductRelatedUpSellCrossSellSection.xml -@@ -7,11 +7,23 @@ - --> - - <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="AdminProductFormRelatedUpSellCrossSellSection"> -+ <element name="sectionHeader" type="block" selector=".fieldset-wrapper.admin__collapsible-block-wrapper[data-index='related']"/> - <element name="AddRelatedProductsButton" type="button" selector="button[data-index='button_related']" timeout="30"/> -+ <element name="AddUpSellProductsButton" type="button" selector="button[data-index='button_upsell']" timeout="30"/> -+ <element name="AddCrossSellProductsButton" type="button" selector="button[data-index='button_crosssell']" timeout="30"/> - <element name="relatedProductSectionText" type="text" selector=".fieldset-wrapper.admin__fieldset-section[data-index='related']"/> - <element name="upSellProductSectionText" type="text" selector=".fieldset-wrapper.admin__fieldset-section[data-index='upsell']"/> - <element name="crossSellProductSectionText" type="text" selector=".fieldset-wrapper.admin__fieldset-section[data-index='crosssell']"/> -+ <element name="relatedDropdown" type="block" selector="//div[@data-index='related']" timeout="30"/> -+ <element name="relatedDependent" type="block" selector="//div[@data-index='related']//div[contains(@class, '_show')]"/> -+ <element name="selectedRelatedProduct" type="block" selector="//span[@data-index='name']"/> -+ <element name="removeRelatedProduct" type="button" selector="//span[text()='Related Products']//..//..//..//span[text()='{{productName}}']//..//..//..//..//..//button[@class='action-delete']" parameterized="true"/> -+ <element name="selectedProductSku" type="text" selector="//div[@data-index='{{section}}']//span[@data-index='sku']" parameterized="true" timeout="30"/> -+ </section> -+ <section name="AdminAddUpSellProductsModalSection"> -+ <element name="Modal" type="button" selector=".product_form_product_form_related_upsell_modal"/> -+ <element name="AddSelectedProductsButton" type="button" selector="//aside[contains(@class, 'product_form_product_form_related_upsell_modal')]//button/span[contains(text(), 'Add Selected Products')]" timeout="30"/> - </section> - </sections> -diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductSEOSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductSEOSection.xml -index 1d49d053636..8685e84a347 100644 ---- a/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductSEOSection.xml -+++ b/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductSEOSection.xml -@@ -7,9 +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="AdminProductSEOSection"> - <element name="sectionHeader" type="button" selector="div[data-index='search-engine-optimization']" timeout="30"/> - <element name="urlKeyInput" type="input" selector="input[name='product[url_key]']"/> -+ <element name="useDefaultUrl" type="checkbox" selector="input[name='use_default[url_key]']"/> -+ <element name="metaTitleInput" type="input" selector="input[name='product[meta_title]']"/> -+ <element name="metaKeywordsInput" type="textarea" selector="textarea[name='product[meta_keyword]']"/> -+ <element name="metaDescriptionInput" type="textarea" selector="textarea[name='product[meta_description]']"/> - </section> - </sections> -diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/AdminUpdateAttributesSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/AdminUpdateAttributesSection.xml -index 3048f0e3f56..53af1d5bd6e 100644 ---- a/app/code/Magento/Catalog/Test/Mftf/Section/AdminUpdateAttributesSection.xml -+++ b/app/code/Magento/Catalog/Test/Mftf/Section/AdminUpdateAttributesSection.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="AdminUpdateAttributesSection"> - <element name="saveButton" type="button" selector="button[title='Save']" timeout="30"/> - -@@ -38,4 +38,8 @@ - - <element name="description" type="input" selector="#description"/> - </section> -+ <section name="AdminUpdateAttributesWebsiteSection"> -+ <element name="website" type="button" selector="#attributes_update_tabs_websites"/> -+ <element name="addProductToWebsite" type="checkbox" selector="#add-products-to-website-content .website-checkbox"/> -+ </section> - </sections> -diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/CatalogSubmenuSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/CatalogSubmenuSection.xml -new file mode 100644 -index 00000000000..84a81c5204a ---- /dev/null -+++ b/app/code/Magento/Catalog/Test/Mftf/Section/CatalogSubmenuSection.xml -@@ -0,0 +1,14 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> -+ <section name="CatalogSubmenuSection"> -+ <element name="products" type="button" selector="//li[@id='menu-magento-catalog-catalog']//li[@data-ui-id='menu-magento-catalog-catalog-products']"/> -+ </section> -+</sections> -diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/NewProductPageSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/NewProductPageSection.xml -new file mode 100644 -index 00000000000..b98bd47b541 ---- /dev/null -+++ b/app/code/Magento/Catalog/Test/Mftf/Section/NewProductPageSection.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="NewProductPageSection"> -+ <element name="productName" type="input" selector="//input[@name='product[name]']"/> -+ <element name="sku" type="input" selector="//input[@name='product[sku]']"/> -+ <element name="price" type="input" selector="//input[@name='product[price]']"/> -+ <element name="quantity" type="input" selector="//input[@name='product[quantity_and_stock_status][qty]']"/> -+ <element name="saveButton" type="button" selector="//button[@id='save-button']"/> -+ <element name="createdSuccessMessage" type="button" selector="//div[@data-ui-id='messages-message-success']"/> -+ </section> -+</sections> -diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/ProductsPageSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/ProductsPageSection.xml -new file mode 100644 -index 00000000000..ea37eb59b67 ---- /dev/null -+++ b/app/code/Magento/Catalog/Test/Mftf/Section/ProductsPageSection.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="ProductsPageSection"> -+ <element name="addProductButton" type="button" selector="//button[@id='add_new_product-button']"/> -+ <element name="checkboxForProduct" type="button" selector="//*[contains(text(),'{{args}}')]/parent::td/preceding-sibling::td/label[@class='data-grid-checkbox-cell-inner']" parameterized="true"/> -+ <element name="actions" type="button" selector="//div[@class='col-xs-2']/div[@class='action-select-wrap']/button[@class='action-select']"/> -+ <element name="delete" type="button" selector="//*[contains(@class,'admin__data-grid-header-row row row-gutter')]//*[text()='Delete']"/> -+ <element name="ok" type="button" selector="//button[@data-role='action']//span[text()='OK']"/> -+ <element name="deletedSuccessMessage" type="button" selector="//*[@class='message message-success success']"/> -+ </section> -+</sections> -diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontCategoryBottomToolbarSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontCategoryBottomToolbarSection.xml -new file mode 100644 -index 00000000000..7ce795c78f2 ---- /dev/null -+++ b/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontCategoryBottomToolbarSection.xml -@@ -0,0 +1,16 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> -+ <section name="StorefrontCategoryBottomToolbarSection"> -+ <element name="nextPage" type="button" selector=".//*[@class='toolbar toolbar-products'][2]//a[contains(@class, 'next')]" timeout="30"/> -+ <element name="previousPage" type="button" selector=".//*[@class='toolbar toolbar-products'][2]//a[contains(@class, 'previous')]" timeout="30"/> -+ <element name="pageNumber" type="text" selector="//*[@class='toolbar toolbar-products'][2]//a[contains(@class, 'page')]//span[2][contains(text() ,'{{var1}}')]" parameterized="true"/> -+ <element name="perPage" type="select" selector="//*[@class='toolbar toolbar-products'][2]//select[@id='limiter']"/> -+ </section> -+</sections> -diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontCategoryFilterSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontCategoryFilterSection.xml -index 631cb36e168..ddec4428f90 100644 ---- a/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontCategoryFilterSection.xml -+++ b/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontCategoryFilterSection.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="StorefrontCategoryFilterSection"> - <element name="CategoryFilter" type="button" selector="//main//div[@class='filter-options']//div[contains(text(), 'Category')]"/> - <element name="CategoryByName" type="button" selector="//main//div[@class='filter-options']//li[@class='item']//a[contains(text(), '{{var1}}')]" parameterized="true"/> -diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontCategoryMainSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontCategoryMainSection.xml -index 2a6003d837b..ac7a15daf56 100644 ---- a/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontCategoryMainSection.xml -+++ b/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontCategoryMainSection.xml -@@ -7,24 +7,32 @@ - --> - - <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="StorefrontCategoryMainSection"> -+ <element name="perPage" type="select" selector="//*[@id='authenticationPopup']/following-sibling::div[3]//*[@id='limiter']"/> -+ <element name="sortedBy" type="select" selector="//*[@id='authenticationPopup']/following-sibling::div[1]//*[@id='sorter']"/> -+ <element name="modeGridIsActive" type="text" selector="//*[@id='authenticationPopup']/following-sibling::div[1]//*[@class='modes']/strong[@class='modes-mode active mode-grid']/span"/> - <element name="modeListButton" type="button" selector="#mode-list"/> - <element name="CategoryTitle" type="text" selector="#page-title-heading span"/> - <element name="ProductItemInfo" type="button" selector=".product-item-info"/> - <element name="specifiedProductItemInfo" type="button" selector="//a[@class='product-item-link'][contains(text(), '{{var1}}')]" parameterized="true"/> - <element name="AddToCartBtn" type="button" selector="button.action.tocart.primary"/> -+ <element name="addToCartProductBySku" type="button" selector="//form[@data-product-sku='{{productSku}}']//button[contains(@class, 'tocart')]" parameterized="true" /> - <element name="SuccessMsg" type="button" selector="div.message-success"/> - <element name="productCount" type="text" selector="#toolbar-amount"/> - <element name="CatalogDescription" type="text" selector="//div[@class='category-description']//p"/> - <element name="mediaDescription" type="text" selector="img[alt='{{var1}}']" parameterized="true"/> - <element name="imageSource" type="text" selector="//img[contains(@src,'{{var1}}')]" parameterized="true"/> - <element name="productImage" type="text" selector="img.product-image-photo"/> -- <element name="productLink" type="text" selector="a.product-item-link"/> -+ <element name="productLink" type="text" selector="a.product-item-link" timeout="30"/> - <element name="productLinkByHref" type="text" selector="a.product-item-link[href$='{{var1}}.html']" parameterized="true"/> -- <element name="productPrice" type="text" selector="div.price-box.price-final_price"/> -+ <element name="productPrice" type="text" selector=".price-final_price"/> - <element name="categoryImage" type="text" selector=".category-image"/> - <element name="emptyProductMessage" type="block" selector=".message.info.empty>div"/> - <element name="lineProductName" type="text" selector=".products.list.items.product-items li:nth-of-type({{line}}) .product-item-link" timeout="30" parameterized="true"/> -+ <element name="asLowAs" type="input" selector="//*[@class='price-box price-final_price']/a/span[@class='price-container price-final_price tax weee']"/> -+ <element name="productsList" type="block" selector="#maincontent .column.main"/> -+ <element name="productName" type="text" selector=".product-item-name"/> -+ <element name="productOptionList" type="text" selector="#narrow-by-list"/> - </section> - </sections> -diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontCategoryProductSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontCategoryProductSection.xml -index 19b3a5cc127..4114d64eb39 100644 ---- a/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontCategoryProductSection.xml -+++ b/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontCategoryProductSection.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="StorefrontCategoryProductSection"> - <element name="ProductTitleByNumber" type="button" selector="//main//li[{{var1}}]//a[@class='product-item-link']" parameterized="true"/> - <element name="ProductPriceByNumber" type="text" selector="//main//li[{{var1}}]//span[@class='price']" parameterized="true"/> -@@ -16,10 +16,13 @@ - <element name="ProductInfoByNumber" type="text" selector="//main//li[{{var1}}]//div[@class='product-item-info']" parameterized="true"/> - <element name="ProductAddToCompareByNumber" type="text" selector="//main//li[{{var1}}]//a[contains(@class, 'tocompare')]" parameterized="true"/> - <element name="listedProduct" type="block" selector="ol li:nth-child({{productPositionInList}}) img" parameterized="true"/> -+ <element name="ProductImageByNumber" type="button" selector="//main//li[{{var1}}]//img" parameterized="true"/> - <element name="categoryListView" type="button" selector="a[title='List']" timeout="30"/> - - <element name="ProductTitleByName" type="button" selector="//main//li//a[contains(text(), '{{var1}}')]" parameterized="true"/> - <element name="ProductPriceByName" type="text" selector="//main//li[.//a[contains(text(), '{{var1}}')]]//span[@class='price']" parameterized="true"/> -+ <element name="ProductCatalogRuleSpecialPriceTitleByName" type="text" selector="//div[descendant::*[contains(text(), '{{var1}}')]]//*[contains(@class, 'special-price')]" parameterized="true"/> -+ <element name="ProductCatalogRulePriceTitleByName" type="text" selector="//div[descendant::*[contains(text(), '{{var1}}')]]//*[contains(@class, 'price-label')]" parameterized="true"/> - <element name="ProductImageByName" type="text" selector="//main//li[.//a[contains(text(), '{{var1}}')]]//img[@class='product-image-photo']" parameterized="true"/> - <element name="ProductImageBySrc" type="text" selector=".products-grid img[src*='{{pattern}}']" parameterized="true"/> - <element name="ProductInfoByName" type="text" selector="//main//li[.//a[contains(text(), '{{var1}}')]]//div[@class='product-item-info']" parameterized="true"/> -@@ -28,7 +31,9 @@ - <element name="productPriceLabel" type="text" selector="//span[@class='price-label'][contains(text(),'{{var1}}')]" parameterized="true"/> - <element name="productPriceLinkAfterLabel" type="text" selector="//span[@class='price-label'][contains(text(),'{{var1}}')]/following::span[contains(text(), '{{var2}}')]" parameterized="true"/> - <element name="ProductAddToCartByName" type="button" selector="//main//li[.//a[contains(text(), '{{var1}}')]]//a[contains(@class, 'tocart')]" parameterized="true"/> -- <element name="ProductAddToCompareByName" type="text" selector="//main//li[.//a[contains(text(), '{{var1}}')]]//a[contains(@class, 'tocompare')]" parameterized="true"/> -+ <!--<element name="ProductAddToCompareByName" type="text" selector="//main//li[.//a[contains(text(), '{{var1}}')]]//a[contains(@class, 'tocompare')]" parameterized="true"/>--> -+ <element name="ProductAddToCompareByName" type="text" selector="//*[contains(@class,'product-item-info')][descendant::a[contains(text(), '{{var1}}')]]//a[contains(@class, 'tocompare')]" parameterized="true"/> - <element name="ProductImageByNameAndSrc" type="text" selector="//main//li[.//a[contains(text(), '{{var1}}')]]//img[contains(@src, '{{src}}')]" parameterized="true"/> -+ <element name="ProductStockUnavailable" type="text" selector="//*[text()='Out of stock']"/> - </section> - </sections> -diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontCategorySidebarSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontCategorySidebarSection.xml -index 9cd35f65c29..1b7bbd58eea 100644 ---- a/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontCategorySidebarSection.xml -+++ b/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontCategorySidebarSection.xml -@@ -6,11 +6,14 @@ - */ - --> - --<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="StorefrontCategorySidebarSection"> - <element name="filterOptionsTitle" type="text" selector="//div[@class='filter-options-title' and contains(text(), '{{var1}}')]" parameterized="true"/> - <element name="filterOptions" type="text" selector=".filter-options-content .items"/> - <element name="filterOption" type="text" selector=".filter-options-content .item"/> - <element name="optionQty" type="text" selector=".filter-options-content .item .count"/> - </section> -+ <section name="StorefrontCategorySidebarMobileSection"> -+ <element name="shopByButton" type="button" selector="//div[contains(@class, 'filter-title')]/strong[contains(text(), 'Shop By')]"/> -+ </section> - </sections> -\ No newline at end of file -diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontCategoryTopToolbarSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontCategoryTopToolbarSection.xml -index 2b44bf1db7e..e063b5fc8c1 100644 ---- a/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontCategoryTopToolbarSection.xml -+++ b/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontCategoryTopToolbarSection.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="StorefrontCategoryTopToolbarSection"> - <element name="gridMode" type="button" selector=".//*[@class='toolbar toolbar-products'][1]//*[@id='mode-grid']" timeout="30"/> - <element name="listMode" type="button" selector=".//*[@class='toolbar toolbar-products'][1]//*[@id='mode-list']" timeout="30"/> - <element name="sortByDropdown" type="select" selector=".//*[@class='toolbar toolbar-products'][1]//*[@id='sorter']" timeout="30"/> -+ <element name="sortDirectionAsc" type="button" selector=".//*[@class='toolbar toolbar-products'][1]//a[contains(@class, 'sort-asc')]" timeout="30"/> -+ <element name="sortDirectionDesc" type="button" selector=".//*[@class='toolbar toolbar-products'][1]//a[contains(@class, 'sort-desc')]" timeout="30"/> - </section> - </sections> -diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontComparisonSidebarSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontComparisonSidebarSection.xml -index 0fdda3eaae9..d097d6bbc46 100644 ---- a/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontComparisonSidebarSection.xml -+++ b/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontComparisonSidebarSection.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="StorefrontComparisonSidebarSection"> - <element name="Compare" type="button" selector="//main//div[contains(@class, 'block-compare')]//a[contains(@class, 'action compare')]"/> - <element name="ClearAll" type="button" selector="//main//div[contains(@class, 'block-compare')]//a[contains(@class, 'action clear')]"/> -diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontFooterSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontFooterSection.xml -index cf956004ae4..1c937637ad8 100644 ---- a/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontFooterSection.xml -+++ b/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontFooterSection.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:Page/etc/SectionObject.xsd"> - <section name="StorefrontFooterSection"> - <element name="switchStoreButton" type="button" selector="#switcher-store-trigger"/> - <element name="storeLink" type="button" selector="//ul[@class='dropdown switcher-dropdown']//a[contains(text(),'{{var1}}')]" parameterized="true" timeout="30"/> -diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontHeaderSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontHeaderSection.xml -index 6b0130eefc3..52a377ad264 100644 ---- a/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontHeaderSection.xml -+++ b/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontHeaderSection.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="StorefrontHeaderSection"> -- <element name="NavigationCategoryByName" type="button" selector="//nav//a[span[contains(., '{{var1}}')]]" parameterized="true"/> -+ <element name="NavigationCategoryByName" type="button" selector="//nav//a[span[contains(., '{{var1}}')]]" parameterized="true" timeout="30"/> - </section> - </sections> -diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontMessagesSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontMessagesSection.xml -index 1a9406b9975..c58479a7b73 100644 ---- a/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontMessagesSection.xml -+++ b/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontMessagesSection.xml -@@ -7,8 +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="StorefrontMessagesSection"> - <element name="success" type="text" selector="div.message-success.success.message"/> -+ <element name="error" type="text" selector="div.message-error.error.message"/> -+ <element name="noticeMessage" type="text" selector="div.message.notice div"/> - </section> - </sections> -diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontMiniCartSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontMiniCartSection.xml -new file mode 100644 -index 00000000000..b2cd0f5f9af ---- /dev/null -+++ b/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontMiniCartSection.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="StorefrontMiniCartSection"> -+ <element name="quantity" type="button" selector="span.counter-number"/> -+ <element name="show" type="button" selector="a.showcart"/> -+ <element name="goToCheckout" type="button" selector="#top-cart-btn-checkout" timeout="30"/> -+ <element name="emptyMiniCart" type="text" selector="//div[@class='minicart-wrapper']//span[@class='counter qty empty']/../.."/> -+ </section> -+</sections> -diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontNavigationSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontNavigationSection.xml -index ad575b640bd..292b2d7008b 100644 ---- a/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontNavigationSection.xml -+++ b/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontNavigationSection.xml -@@ -6,10 +6,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"> -+<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> - <section name="StorefrontNavigationSection"> - <element name="topCategory" type="button" selector="//a[contains(@class,'level-top')]/span[contains(text(),'{{var1}}')]" parameterized="true"/> - <element name="subCategory" type="button" selector="//ul[contains(@class,'submenu')]//span[contains(text(),'{{var1}}')]" parameterized="true"/> - <element name="breadcrumbs" type="textarea" selector=".items"/> -+ <element name="categoryBreadcrumbs" type="textarea" selector=".breadcrumbs li"/> -+ <element name="categoryBreadcrumbsByNumber" type="textarea" selector=".breadcrumbs li:nth-of-type({{number}})" parameterized="true"/> - </section> - </sections> -diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontProducRelatedProductsSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontProducRelatedProductsSection.xml -index e15723582db..f4db37b6775 100644 ---- a/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontProducRelatedProductsSection.xml -+++ b/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontProducRelatedProductsSection.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="StorefrontProductRelatedProductsSection"> - <element name="relatedProductsActionsHeaderText" type="text" selector=".block.related .block-actions" /> - <element name="relatedProductsListSectionText" type="text" selector=".block.related .products.wrapper.grid.products-grid.products-related" /> -diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontProductActionSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontProductActionSection.xml -index 65d6b7c5f61..5ee754904b7 100644 ---- a/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontProductActionSection.xml -+++ b/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontProductActionSection.xml -@@ -6,10 +6,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="StorefrontProductActionSection"> - <element name="quantity" type="input" selector="#qty"/> -- <element name="addToCart" type="button" selector="#product-addtocart-button"/> -+ <element name="addToCart" type="button" selector="#product-addtocart-button" timeout="60"/> - <element name="addToCartButtonTitleIsAdding" type="text" selector="//button/span[text()='Adding...']"/> - <element name="addToCartButtonTitleIsAdded" type="text" selector="//button/span[text()='Added']"/> - <element name="addToCartButtonTitleIsAddToCart" type="text" selector="//button/span[text()='Add to Cart']"/> -diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontProductCompareMainSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontProductCompareMainSection.xml -index 728f9a5a174..ad31be6b277 100644 ---- a/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontProductCompareMainSection.xml -+++ b/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontProductCompareMainSection.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="StorefrontProductCompareMainSection"> - <element name="PageName" type="text" selector="//*[@id='maincontent']//h1//span"/> - <element name="ProductLinkByName" type="button" selector="//*[@id='product-comparison']//tr//strong[@class='product-item-name']/a[contains(text(), '{{var1}}')]" parameterized="true"/> -diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontProductInfoDetailsSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontProductInfoDetailsSection.xml -index 5688811cb96..0745c0d0819 100644 ---- a/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontProductInfoDetailsSection.xml -+++ b/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontProductInfoDetailsSection.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="StorefrontProductInfoDetailsSection"> - <element name="productNameForReview" type="text" selector=".legend.review-legend>strong" /> - <element name="detailsTab" type="button" selector="#tab-label-description-title" /> -diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontProductInfoMainSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontProductInfoMainSection.xml -index 42ca3836e6c..fd412d3c7de 100644 ---- a/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontProductInfoMainSection.xml -+++ b/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontProductInfoMainSection.xml -@@ -7,19 +7,21 @@ - --> - - <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="StorefrontProductInfoMainSection"> - <element name="stock" type="input" selector=".stock.available"/> - <element name="productName" type="text" selector=".base"/> - <element name="productSku" type="text" selector=".product.attribute.sku>.value"/> - <element name="productPriceLabel" type="text" selector=".price-label"/> -- <element name="productPrice" type="text" selector="div.price-box.price-final_price"/> -+ <element name="price" type="text" selector=".product-info-main [data-price-type='finalPrice']"/> -+ <element name="productPrice" type="text" selector=".price-final_price"/> - <element name="qty" type="input" selector="#qty"/> - <element name="specialPrice" type="text" selector=".special-price"/> -+ <element name="specialPriceAmount" type="text" selector=".special-price span.price"/> - <element name="updatedPrice" type="text" selector="div.price-box.price-final_price [data-price-type='finalPrice'] .price"/> - <element name="oldPrice" type="text" selector=".old-price"/> - <element name="oldPriceTag" type="text" selector=".old-price .price-label"/> -- <element name="oldPriceAmount" type="text" selector=".old-price .price"/> -+ <element name="oldPriceAmount" type="text" selector=".old-price span.price"/> - <element name="productStockStatus" type="text" selector=".stock[title=Availability]>span"/> - <element name="productImage" type="text" selector="//*[@id='maincontent']//div[@class='gallery-placeholder']//img[@class='fotorama__img']"/> - <element name="productImageSrc" type="text" selector="//*[@id='maincontent']//div[@class='gallery-placeholder']//img[contains(@src, '{{src}}')]" parameterized="true"/> -@@ -28,14 +30,18 @@ - <element name="productOptionAreaInput" type="textarea" selector="//*[@id='product-options-wrapper']//div[@class='fieldset']//label[contains(.,'{{var1}}')]/../div[@class='control']//textarea" parameterized="true"/> - <element name="productOptionFile" type="file" selector="//*[@id='product-options-wrapper']//div[@class='fieldset']//label[contains(.,'OptionFile')]/../div[@class='control']//input[@type='file']" parameterized="true"/> - <element name="productOptionSelect" type="select" selector="//*[@id='product-options-wrapper']//div[@class='fieldset']//label[contains(.,'{{var1}}')]/../div[@class='control']//select" parameterized="true"/> -- -+ <element name="asLowAs" type="input" selector="span[class='price-wrapper '] "/> -+ <element name="specialPriceValue" type="text" selector="//span[@class='special-price']//span[@class='price']"/> -+ <element name="mapPrice" type="text" selector="//div[@class='price-box price-final_price']//span[contains(@class, 'price-msrp_price')]"/> -+ <element name="clickForPriceLink" type="text" selector="//div[@class='price-box price-final_price']//a[contains(text(), 'Click for price')]"/> - - <!-- The parameter is the nth custom option that you want to get --> - <element name="nthCustomOption" type="block" selector="//*[@id='product-options-wrapper']/*[@class='fieldset']/*[contains(@class, 'field')][{{customOptionNum}}]" parameterized="true" /> -+ - <!-- The 1st parameter is the nth custom option, the 2nd parameter is the nth value in the option --> - <element name="nthCustomOptionInput" type="radio" selector="//*[@id='product-options-wrapper']/*[@class='fieldset']/*[contains(@class, 'field')][{{customOptionNum}}]//*[contains(@class, 'admin__field-option')][{{customOptionValueNum}}]//input" parameterized="true" /> -+ <element name="customOptionByTitle" type="text" selector="//span[text()='{{title}}']/ancestor::div[contains(@class, 'field ') and contains(@class, 'required')]" parameterized="true" timeout="30"/> - <element name="productOptionRadioButtonsCheckbox" type="checkbox" selector="//*[@id='product-options-wrapper']//div[@class='fieldset']//label[contains(.,'{{var1}}')]/../div[@class='control']//input[@price='{{var2}}']" parameterized="true"/> -- - <element name="productOptionDataMonth" type="date" selector="//*[@id='product-options-wrapper']//div[@class='fieldset']//legend[contains(.,'{{var1}}')]/../div[@class='control']//select[@data-calendar-role='month']" parameterized="true"/> - <element name="productOptionDataDay" type="date" selector="//*[@id='product-options-wrapper']//div[@class='fieldset']//legend[contains(.,'{{var1}}')]/../div[@class='control']//select[@data-calendar-role='day']" parameterized="true"/> - <element name="productOptionDataYear" type="date" selector="//*[@id='product-options-wrapper']//div[@class='fieldset']//legend[contains(.,'{{var1}}')]/../div[@class='control']//select[@data-calendar-role='year']" parameterized="true"/> -@@ -49,7 +55,6 @@ - - <!-- Only one of Upload/Url Inputs are available for File and Sample depending on the value of the corresponding TypeSelector --> - <element name="addLinkFileUploadFile" type="file" selector="//*[@id='product-options-wrapper']//div[@class='fieldset']//label[contains(.,'{{var1}}')]/../div[@class='control']//input[@type='file']" parameterized="true" /> -- - <element name="productShortDescription" type="text" selector="//div[@class='product attribute overview']//div[@class='value']"/> - <element name="productAttributeTitle1" type="text" selector="#product-options-wrapper div[tabindex='0'] label"/> - <element name="productAttributeOptions1" type="select" selector="#product-options-wrapper div[tabindex='0'] option"/> -@@ -66,8 +71,37 @@ - <element name="productOptionDropDownOptionTitle" type="text" selector="//label[contains(.,'{{var1}}')]/../div[@class='control']//select//option[contains(.,'{{var2}}')]" parameterized="true"/> - - <!-- Tier price selectors --> -+ <element name="tierPriceText" type="text" selector=".prices-tier li[class='item']" /> - <element name="productTierPriceByForTextLabel" type="text" selector="//ul[contains(@class, 'prices-tier')]//li[{{var1}}][contains(text(),'Buy {{var2}} for')]" parameterized="true"/> - <element name="productTierPriceAmount" type="text" selector="//ul[contains(@class, 'prices-tier')]//li[{{var1}}]//span[contains(text(), '{{var2}}')]" parameterized="true"/> - <element name="productTierPriceSavePercentageAmount" type="text" selector="//ul[contains(@class, 'prices-tier')]//li[{{var1}}]//span[contains(@class, 'percent')][contains(text(), '{{var2}}')]" parameterized="true"/> -+ -+ <!-- Special price selectors --> -+ <element name="productSpecialPrice" type="text" selector="//span[@data-price-type='finalPrice']/span"/> -+ <element name="specialProductText" type="text" selector="//span[text()='Regular Price']"/> -+ <element name="oldProductPrice" type="text" selector="//span[@data-price-type='oldPrice']/span"/> -+ -+ <!-- Customizable Option selectors --> -+ <element name="allCustomOptionLabels" type="text" selector="#product-options-wrapper label"/> -+ <element name="customOptionLabel" type="text" selector="//label[contains(., '{{customOptionTitle}}')]" parameterized="true"/> -+ <element name="customSelectOptions" type="select" selector="#{{selectId}} option" parameterized="true"/> -+ <element name="requiredCustomInput" type="text" selector="//div[contains(.,'{{customOptionTitle}}') and contains(@class, 'required') and .//input[@aria-required='true']]" parameterized="true"/> -+ <element name="requiredCustomSelect" type="select" selector="//div[contains(.,'{{customOptionTitle}}') and contains(@class, 'required') and .//select[@aria-required='true']]" parameterized="true"/> -+ <element name="requiredCustomField" type="text" selector="//div[@class='field required']/label/span[contains(.,'{{optionTitle}}')]//../../div/div[contains(.,'This is a required field.')]" parameterized="true"/> -+ <element name="requiredCustomFile" type="text" selector="//div[@class='field file required']/label/span[contains(.,'{{OptionFileTitle}}')]//../../div/div[contains(.,'This is a required field.')]" parameterized="true"/> -+ <element name="requiredCustomTextArea" type="text" selector="//div[@class='field textarea required']/label/span[contains(.,'{{OptionAreaTitle}}')]//../../div/div[contains(.,'This is a required field.')]" parameterized="true"/> -+ <element name="requiredCustomDate" type="text" selector="//div[@class='field date required']//span[text()='{{OptionDateTitle}}']//../../div/div[contains(.,'This is a required field.')]" parameterized="true"/> -+ <element name="customOptionField" type="input" selector="//input[contains(@class,'input-text product-custom-option')]"/> -+ <element name="customOptionTextArea" type="textarea" selector="//textarea[contains(@class,'product-custom-option')]"/> -+ <element name="customOptionDropDown" type="select" selector="//select[contains(@class,' required product-custom-option admin__control-select')]/option[contains(.,'{{option}}')]" parameterized="true"/> -+ <element name="customRadioOption" type="checkbox" selector="//div/input[@type='radio']/../label/span"/> -+ <element name="customOptionCheckBox" type="checkbox" selector="//div/input[@type='checkbox']/../label/span[contains(.,'{{option}}')]" parameterized="true"/> -+ <element name="customMultiSelectOption" type="select" selector="//select[contains(@class,'multiselect admin__control-multiselect required product-custom-option')]/option[contains(.,'{{option'}})]" parameterized="true"/> -+ <element name="customOptionMonth" type="date" selector="//div[@class='field date required']//span[text()='{{option}}']/../..//div/select[@data-calendar-role='month']" parameterized="true"/> -+ <element name="customOptionDay" type="date" selector="//div[@class='field date required']//span[text()='{{option}}']/../..//div/select[@data-calendar-role='day']" parameterized="true"/> -+ <element name="customOptionYear" type="date" selector="//div[@class='field date required']//span[text()='{{option}}']/../..//div/select[@data-calendar-role='year']" parameterized="true"/> -+ <element name="customOptionHour" type="date" selector="//div[@class='field date required']//span[text()='{{option}}']/../..//div/select[@data-calendar-role='hour']" parameterized="true"/> -+ <element name="customOptionMinute" type="date" selector="//div[@class='field date required']//span[text()='{{option}}']/../..//div/select[@data-calendar-role='minute']" parameterized="true"/> -+ <element name="customOptionDayPart" type="date" selector="//div[@class='field date required']//span[text()='{{option}}']/../..//div/select[@data-calendar-role='day_part']" parameterized="true"/> - </section> - </sections> -diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontProductMediaSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontProductMediaSection.xml -index 0273b39f48a..ea10e12fb73 100644 ---- a/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontProductMediaSection.xml -+++ b/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontProductMediaSection.xml -@@ -7,8 +7,14 @@ - --> - - <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="StorefrontProductMediaSection"> -+ <element name="gallerySpinner" type="block" selector="#maincontent .fotorama__spinner--show" /> -+ <element name="gallery" type="block" selector="[data-gallery-role='gallery']" /> -+ <element name="productImage" type="text" selector="//*[@data-gallery-role='gallery' and not(contains(@class, 'fullscreen'))]//img[contains(@src, '{{filename}}') and not(contains(@class, 'full'))]" parameterized="true" /> -+ <element name="productImageFullscreen" type="text" selector="//*[@data-gallery-role='gallery' and contains(@class, 'fullscreen')]//img[contains(@src, '{{filename}}') and contains(@class, 'full')]" parameterized="true" /> -+ <element name="closeFullscreenImage" type="button" selector="//*[@data-gallery-role='gallery' and contains(@class, 'fullscreen')]//*[@data-gallery-role='fotorama__fullscreen-icon']" /> - <element name="imageFile" type="text" selector="//*[@class='product media']//img[contains(@src, '{{filename}}')]" parameterized="true"/> -+ <element name="productImageActive" type="text" selector=".product.media div[data-active=true] > img[src*='{{filename}}']" parameterized="true"/> - </section> - </sections> -diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontProductMoreInformationSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontProductMoreInformationSection.xml -index fc2102e073d..7706c5f244b 100644 ---- a/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontProductMoreInformationSection.xml -+++ b/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontProductMoreInformationSection.xml -@@ -7,10 +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="StorefrontProductMoreInformationSection"> - <element name="moreInformation" type="button" selector="#tab-label-additional-title" timeout="30"/> - <element name="moreInformationTextArea" type="textarea" selector="#additional"/> -+ <element name="moreInformationSpecs" type="text" selector="#product-attribute-specs-table"/> -+ <element name="customAttributeLabel" type="text" selector="//th[./following-sibling::td[@data-th='{{attributeCode}}']]" parameterized="true" /> -+ <element name="customAttributeValue" type="text" selector="//td[@data-th='{{attributeCode}}']" parameterized="true" /> - <element name="attributeLabel" type="text" selector=".col.label"/> - <element name="attributeValue" type="text" selector=".col.data"/> - </section> -diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontProductPageDesignSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontProductPageDesignSection.xml -new file mode 100644 -index 00000000000..1f41ddefd0b ---- /dev/null -+++ b/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontProductPageDesignSection.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="StorefrontProductPageDesignSection"> -+ <element name="layoutTwoColumnsLeft" type="block" selector=".page-layout-2columns-left"/> -+ <element name="layoutEmpty" type="block" selector=".page-layout-empty"/> -+ </section> -+</sections> -diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontProductPageSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontProductPageSection.xml -index 2de616819b2..78818dd37a5 100644 ---- a/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontProductPageSection.xml -+++ b/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontProductPageSection.xml -@@ -7,16 +7,23 @@ - --> - - <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="StorefrontProductPageSection"> -- <element name="qtyInput" type="button" selector="input.input-text.qty"/> -+ <element name="qtyInput" type="button" selector="input.input-text.qty" timeout="30"/> - <element name="addToCartBtn" type="button" selector="button.action.tocart.primary" timeout="30"/> -- <element name="successMsg" type="button" selector="div.message-success"/> -- <element name="errorMsg" type="button" selector="div.message-error"/> -+ <element name="successMsg" type="button" selector="div.message-success" timeout="30"/> -+ <element name="errorMsg" type="button" selector="div.message-error" timeout="30"/> - <element name="alertMessage" type="text" selector=".page.messages [role=alert]"/> -- <element name="messagesBlock" type="text" selector=".page.messages"/> -+ <element name="messagesBlock" type="text" selector=".page.messages" timeout="30"/> - <element name="addToWishlist" type="button" selector="//a[@class='action towishlist']" timeout="30"/> - <element name="customTextOptionInput" type="input" selector=".input-text.product-custom-option"/> - <element name="charCounter" type="text" selector=".character-counter"/> -+ <element name="tax" type="input" selector=".totals-tax .amount .price"/> -+ <element name="subTotal" type="input" selector="span[data-th='Subtotal']"/> -+ <element name="shipping" type="input" selector="span[data-th='Shipping']"/> -+ <element name="orderTotal" type="input" selector=".grand.totals .amount .price"/> -+ <element name="customOptionDropDown" type="select" selector="//*[@id='product-options-wrapper']//select[contains(@class, 'product-custom-option admin__control-select')]"/> -+ <element name="qtyInputWithProduct" type="input" selector="//tr//strong[contains(.,'{{productName}}')]/../../td[@class='col qty']//input" parameterized="true"/> -+ <element name="customOptionRadio" type="input" selector="//span[contains(text(),'{{customOption}}')]/../../input" parameterized="true"/> - </section> --</sections> -+</sections> -\ No newline at end of file -diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontProductUpSellProductsSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontProductUpSellProductsSection.xml -new file mode 100644 -index 00000000000..f00abbe3c58 ---- /dev/null -+++ b/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontProductUpSellProductsSection.xml -@@ -0,0 +1,15 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> -+ <section name="StorefrontProductUpSellProductsSection"> -+ <element name="upSellHeading" type="text" selector="#block-upsell-heading"/> -+ <element name="upSellProducts" type="text" selector="div.upsell .product-item-name"/> -+ </section> -+</sections> -diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontWidgetsSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontWidgetsSection.xml -new file mode 100644 -index 00000000000..87aab45bd8c ---- /dev/null -+++ b/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontWidgetsSection.xml -@@ -0,0 +1,16 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> -+ <section name="StorefrontWidgetsSection"> -+ <element name="widgetRecentlyViewedProductsGrid" type="block" selector=".block.widget.block-viewed-products-grid"/> -+ <element name="widgetRecentlyComparedProductsGrid" type="block" selector=".block.widget.block-compared-products-grid"/> -+ <element name="widgetRecentlyOrderedProductsGrid" type="block" selector=".block.block-reorder"/> -+ </section> -+</sections> -\ No newline at end of file -diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AddOutOfStockProductToCompareListTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AddOutOfStockProductToCompareListTest.xml -new file mode 100644 -index 00000000000..044b38a19c4 ---- /dev/null -+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AddOutOfStockProductToCompareListTest.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="AddOutOfStockProductToCompareListTest"> -+ <annotations> -+ <features value="Catalog"/> -+ <stories value="Product Comparison for products Out of Stock"/> -+ <title value="Add Product that is Out of Stock product to Product Comparison"/> -+ <description value="Customer should be able to add Product that is Out Of Stock to the Product Comparison"/> -+ <severity value="MAJOR"/> -+ <testCaseId value="MAGETWO-98644"/> -+ <useCaseId value="MAGETWO-98522"/> -+ <group value="Catalog"/> -+ </annotations> -+ <before> -+ <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> -+ <magentoCLI command="config:set cataloginventory/options/show_out_of_stock 0" stepKey="displayOutOfStockNo"/> -+ <magentoCLI command="cache:flush" stepKey="flushCache"/> -+ <createData entity="SimpleSubCategory" stepKey="category"/> -+ <createData entity="SimpleProduct4" stepKey="product"> -+ <requiredEntity createDataKey="category"/> -+ </createData> -+ </before> -+ <after> -+ <magentoCLI command="config:set cataloginventory/options/show_out_of_stock 0" stepKey="displayOutOfStockNo2"/> -+ <magentoCLI command="cache:flush" stepKey="flushCache"/> -+ <deleteData createDataKey="product" stepKey="deleteProduct"/> -+ <deleteData createDataKey="category" stepKey="deleteCategory"/> -+ <actionGroup ref="logout" stepKey="logout"/> -+ </after> -+ <!--Open product page--> -+ <comment userInput="Open product page" stepKey="openProdPage"/> -+ <amOnPage url="{{StorefrontProductPage.url($$product.name$$)}}" stepKey="goToSimpleProductPage"/> -+ <waitForPageLoad stepKey="waitForSimpleProductPage"/> -+ <!--'Add to compare' link is not available--> -+ <comment userInput="'Add to compare' link is not available" stepKey="addToCompareLinkAvailability"/> -+ <dontSeeElement selector="{{StorefrontProductInfoMainSection.productAddToCompare}}" stepKey="dontSeeAddToCompareLink"/> -+ <!--Turn on 'out on stock' config--> -+ <comment userInput="Turn on 'out of stock' config" stepKey="onOutOfStockConfig"/> -+ <magentoCLI command="config:set cataloginventory/options/show_out_of_stock 1" stepKey="displayOutOfStockYes"/> -+ <!--Clear cache and reindex--> -+ <comment userInput="Clear cache and reindex" stepKey="cleanCache"/> -+ <magentoCLI command="indexer:reindex" stepKey="reindex"/> -+ <magentoCLI command="cache:flush" stepKey="flushCache"/> -+ <!--Open product page--> -+ <comment userInput="Open product page" stepKey="openProductPage"/> -+ <amOnPage url="{{StorefrontProductPage.url($$product.name$$)}}" stepKey="goToSimpleProductPage2"/> -+ <waitForPageLoad stepKey="waitForSimpleProductPage2"/> -+ <!--Click on 'Add to Compare' link--> -+ <comment userInput="Click on 'Add to Compare' link" stepKey="clickOnAddToCompareLink"/> -+ <click selector="{{StorefrontProductInfoMainSection.productAddToCompare}}" stepKey="clickOnAddToCompare"/> -+ <waitForPageLoad stepKey="waitForProdAddToCmpList"/> -+ <!--Assert success message--> -+ <comment userInput="Assert success message" stepKey="assertSuccessMsg"/> -+ <grabTextFrom selector="{{AdminProductMessagesSection.successMessage}}" stepKey="grabTextFromSuccessMessage"/> -+ <assertEquals expected='You added product $$product.name$$ to the comparison list.' expectedType="string" actual="($grabTextFromSuccessMessage)" stepKey="assertSuccessMessage"/> -+ <!--See product in the comparison list--> -+ <comment userInput="See product in the comparison list" stepKey="seeProductInComparisonList"/> -+ <amOnPage url="{{StorefrontProductComparePage.url}}" stepKey="navigateToComparePage"/> -+ <waitForPageLoad stepKey="waitForStorefrontProductComparePageLoad"/> -+ <seeElement selector="{{StorefrontProductCompareMainSection.ProductLinkByName($product.name$)}}" stepKey="seeProductInCompareList"/> -+ <!--Go to Category page and delete product from comparison list--> -+ <comment userInput="Go to Category page and delete product from comparison list" stepKey="deleteProdFromCmpList"/> -+ <amOnPage url="{{StorefrontCategoryPage.url($$category.name$$)}}" stepKey="onCategoryPage"/> -+ <waitForPageLoad time="30" stepKey="waitForPageLoad1"/> -+ <click selector="{{StorefrontComparisonSidebarSection.ClearAll}}" stepKey="clickClearAll"/> -+ <waitForPageLoad time="30" stepKey="waitForConfirmPageLoad"/> -+ <click selector="{{AdminDeleteRoleSection.confirm}}" stepKey="confirmProdDelate"/> -+ <waitForPageLoad time="30" stepKey="waitForConfirmLoad"/> -+ <!--Add product to compare list from Category page--> -+ <comment userInput="Add product to compare list fom Category page" stepKey="addToCmpFromCategPage"/> -+ <moveMouseOver selector="{{StorefrontCategoryMainSection.ProductItemInfo}}" stepKey="hoverOverProduct"/> -+ <click selector="{{StorefrontProductInfoMainSection.productAddToCompare}}" stepKey="clickAddToCompare"/> -+ <waitForPageLoad stepKey="waitProdAddingToCmpList"/> -+ <!--Assert success message--> -+ <comment userInput="Assert success message" stepKey="assertSuccessMsg2"/> -+ <grabTextFrom selector="{{AdminProductMessagesSection.successMessage}}" stepKey="grabTextFromSuccessMessage2"/> -+ <assertEquals expected='You added product $$product.name$$ to the comparison list.' expectedType="string" actual="($grabTextFromSuccessMessage)" stepKey="assertSuccessMessage2"/> -+ <!--Check that product displays on add to compare widget--> -+ <comment userInput="Check that product displays on add to compare widget" stepKey="checkProdNameOnWidget"/> -+ <seeElement selector="{{StorefrontComparisonSidebarSection.ProductTitleByName($$product.name$$)}}" stepKey="seeProdNameOnCmpWidget"/> -+ <!--See product in the compare page--> -+ <comment userInput="See product in the compare page" stepKey="seeProductInComparePage"/> -+ <amOnPage url="{{StorefrontProductComparePage.url}}" stepKey="navigateToComparePage2"/> -+ <waitForPageLoad stepKey="waitForStorefrontProductComparePageLoad2"/> -+ <seeElement selector="{{StorefrontProductCompareMainSection.ProductLinkByName($product.name$)}}" stepKey="seeProductInCompareList2"/> -+ </test> -+</tests> -diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AddToCartCrossSellTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AddToCartCrossSellTest.xml -new file mode 100644 -index 00000000000..53bb12fda48 ---- /dev/null -+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AddToCartCrossSellTest.xml -@@ -0,0 +1,93 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> -+ <test name="AddToCartCrossSellTest"> -+ <annotations> -+ <features value="Catalog"/> -+ <stories value="Promote Products as Cross-Sells"/> -+ <title value="Admin should be able to add cross-sell to products."/> -+ <description value="Create products, add products to cross sells, and check that they appear in the Shopping Cart page."/> -+ <severity value="MAJOR"/> -+ <testCaseId value="MC-9143"/> -+ <group value="Catalog"/> -+ </annotations> -+ <before> -+ <createData entity="SimpleSubCategory" stepKey="category1"/> -+ <createData entity="_defaultProduct" stepKey="simpleProduct1"> -+ <requiredEntity createDataKey="category1"/> -+ </createData> -+ <createData entity="_defaultProduct" stepKey="simpleProduct2"> -+ <requiredEntity createDataKey="category1"/> -+ </createData> -+ <createData entity="_defaultProduct" stepKey="simpleProduct3"> -+ <requiredEntity createDataKey="category1"/> -+ </createData> -+ -+ <actionGroup ref="LoginAsAdmin" stepKey="logInAsAdmin"/> -+ </before> -+ <after> -+ <actionGroup ref="logout" stepKey="logoutFromAdmin"/> -+ -+ <deleteData createDataKey="simpleProduct1" stepKey="deleteSimp1"/> -+ <deleteData createDataKey="simpleProduct2" stepKey="deleteSimp2"/> -+ <deleteData createDataKey="simpleProduct3" stepKey="deleteSimp3"/> -+ <deleteData createDataKey="category1" stepKey="deleteCategory"/> -+ </after> -+ -+ <!-- Go to simpleProduct1, add simpleProduct2 and simpleProduct3 as cross-sell--> -+ <amOnPage url="{{AdminProductEditPage.url($simpleProduct1.id$)}}" stepKey="goToProduct1"/> -+ <click stepKey="openHeader1" selector="{{AdminProductFormRelatedUpSellCrossSellSection.sectionHeader}}"/> -+ -+ <actionGroup ref="addCrossSellProductBySku" stepKey="addProduct2ToSimp1"> -+ <argument name="sku" value="$simpleProduct2.sku$"/> -+ </actionGroup> -+ <actionGroup ref="addCrossSellProductBySku" stepKey="addProduct3ToSimp1"> -+ <argument name="sku" value="$simpleProduct3.sku$"/> -+ </actionGroup> -+ <click selector="{{AdminProductFormActionSection.saveButton}}" stepKey="clickSave"/> -+ <waitForPageLoad stepKey="waitForPageLoad1"/> -+ -+ <!-- Go to simpleProduct3, add simpleProduct1 and simpleProduct2 as cross-sell--> -+ <amOnPage url="{{AdminProductEditPage.url($simpleProduct3.id$)}}" stepKey="goToProduct3"/> -+ <click stepKey="openHeader2" selector="{{AdminProductFormRelatedUpSellCrossSellSection.sectionHeader}}"/> -+ -+ <actionGroup ref="addCrossSellProductBySku" stepKey="addProduct1ToSimp3"> -+ <argument name="sku" value="$simpleProduct1.sku$"/> -+ </actionGroup> -+ <actionGroup ref="addCrossSellProductBySku" stepKey="addProduct2ToSimp3"> -+ <argument name="sku" value="$simpleProduct2.sku$"/> -+ </actionGroup> -+ <click selector="{{AdminProductFormActionSection.saveButton}}" stepKey="clickSave2"/> -+ <waitForPageLoad stepKey="waitForPageLoad2"/> -+ -+ <!-- Go to frontend, add simpleProduct1 to cart--> -+ <actionGroup ref="AddSimpleProductToCart" stepKey="addSimp1ToCart"> -+ <argument name="product" value="$simpleProduct1$"/> -+ </actionGroup> -+ -+ <!-- Check that cart page contains cross-sell to simpleProduct2 and simpleProduct3--> -+ <amOnPage url="{{CheckoutCartPage.url}}" stepKey="goToCart1"/> -+ <waitForPageLoad stepKey="waitForCartToLoad"/> -+ <waitForElementVisible selector="{{CheckoutCartCrossSellSection.products}}" stepKey="waitForCrossSellLoading"/> -+ <see stepKey="seeProduct2InCrossSell" selector="{{CheckoutCartCrossSellSection.products}}" userInput="$simpleProduct2.name$"/> -+ <see stepKey="seeProduct3InCrossSell" selector="{{CheckoutCartCrossSellSection.products}}" userInput="$simpleProduct3.name$"/> -+ -+ <!-- Add simpleProduct3 to cart, check cross-sell contains product2 but not product3--> -+ <click stepKey="addSimp3ToCart" selector="{{CheckoutCartCrossSellSection.productRowByName($simpleProduct3.name$)}}{{CheckoutCartCrossSellSection.addToCart}}"/> -+ <waitForPageLoad stepKey="waitForCartToLoad2"/> -+ <see stepKey="seeProduct2StillInCrossSell" selector="{{CheckoutCartCrossSellSection.products}}" userInput="$simpleProduct2.name$"/> -+ <dontSee stepKey="dontSeeProduct3InCrossSell" selector="{{CheckoutCartCrossSellSection.products}}" userInput="$simpleProduct3.name$"/> -+ -+ <!-- Add simpleProduct2 to cart, check cross-sell doesn't contain product 2 anymore.--> -+ <click stepKey="addSimp2ToCart" selector="{{CheckoutCartCrossSellSection.productRowByName($simpleProduct2.name$)}}{{CheckoutCartCrossSellSection.addToCart}}"/> -+ <waitForPageLoad stepKey="waitForCartToLoad3"/> -+ <dontSee stepKey="dontSeeProduct2InCrossSell" selector="{{CheckoutCartCrossSellSection.products}}" userInput="$simpleProduct2.name$"/> -+ </test> -+</tests> -diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminAddDefaultImageSimpleProductTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminAddDefaultImageSimpleProductTest.xml -index c9b6e033a2f..117f094ee06 100644 ---- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminAddDefaultImageSimpleProductTest.xml -+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminAddDefaultImageSimpleProductTest.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="AdminAddDefaultImageSimpleProductTest"> - <annotations> - <features value="Catalog"/> -@@ -43,7 +43,9 @@ - <actionGroup ref="saveProductForm" stepKey="saveSimpleProduct"/> - - <!-- Assert product image in admin product form --> -- <actionGroup ref="assertProductImageAdminProductPage" stepKey="assertProductImageAdminProductPage"/> -+ <actionGroup ref="assertProductImageAdminProductPage" stepKey="assertProductImageAdminProductPage"> -+ <argument name="image" value="MagentoLogo"/> -+ </actionGroup> - - <!-- Assert product in storefront product page --> - <actionGroup ref="AssertProductInStorefrontProductPage" stepKey="AssertProductInStorefrontProductPage"> -diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminAddDefaultImageVirtualProductTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminAddDefaultImageVirtualProductTest.xml -index add917199e2..3f857c25892 100644 ---- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminAddDefaultImageVirtualProductTest.xml -+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminAddDefaultImageVirtualProductTest.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="AdminAddRemoveProductImageVirtualProductTest"> - <annotations> - <features value="Catalog"/> -diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminAddDefaultVideoSimpleProductTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminAddDefaultVideoSimpleProductTest.xml -new file mode 100644 -index 00000000000..f657fbbdae6 ---- /dev/null -+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminAddDefaultVideoSimpleProductTest.xml -@@ -0,0 +1,48 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> -+ <test name="AdminAddDefaultVideoSimpleProductTest"> -+ <annotations> -+ <features value="Catalog"/> -+ <stories value="Add/remove images and videos for all product types and category"/> -+ <title value="Admin should be able to add default product video for a Simple Product"/> -+ <description value="Admin should be able to add default product video for a Simple Product"/> -+ <severity value="MAJOR"/> -+ <testCaseId value="MC-111"/> -+ <group value="Catalog"/> -+ </annotations> -+ <before> -+ <actionGroup ref="EnableAdminAccountSharingActionGroup" stepKey="enableAdminAccountSharing"/> -+ <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> -+ </before> -+ <after> -+ <amOnPage url="{{AdminLogoutPage.url}}" stepKey="amOnLogoutPage"/> -+ </after> -+ -+ <!-- Create product --> -+ <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="adminProductIndexPageAdd"/> -+ <waitForPageLoad stepKey="waitForProductIndexPageLoad"/> -+ <actionGroup ref="resetProductGridToDefaultView" stepKey="resetProductGridColumnsInitial"/> -+ <actionGroup ref="goToCreateProductPage" stepKey="goToCreateProductPage"> -+ <argument name="product" value="ApiSimpleProduct"/> -+ </actionGroup> -+ <actionGroup ref="fillMainProductFormNoWeight" stepKey="fillMainProductForm"> -+ <argument name="product" value="ApiSimpleProduct"/> -+ </actionGroup> -+ -+ <!-- Save product --> -+ <actionGroup ref="saveProductForm" stepKey="saveProductForm"/> -+ -+ <!-- Assert product in storefront product page --> -+ <actionGroup ref="AssertProductInStorefrontProductPage" stepKey="AssertProductInStorefrontProductPage"> -+ <argument name="product" value="ApiSimpleProduct"/> -+ </actionGroup> -+ </test> -+</tests> -diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminAddDefaultVideoVirtualProductTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminAddDefaultVideoVirtualProductTest.xml -new file mode 100644 -index 00000000000..eab36bc90dc ---- /dev/null -+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminAddDefaultVideoVirtualProductTest.xml -@@ -0,0 +1,34 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> -+ <test name="AdminAddDefaultVideoVirtualProductTest" extends="AdminAddDefaultVideoSimpleProductTest"> -+ <annotations> -+ <features value="Catalog"/> -+ <stories value="Add/remove images and videos for all product types and category"/> -+ <title value="Admin should be able to add default product video for a Virtual Product"/> -+ <description value="Admin should be able to add default product video for a Virtual Product"/> -+ <severity value="MAJOR"/> -+ <testCaseId value="MC-109"/> -+ <group value="Catalog"/> -+ </annotations> -+ -+ <!-- Replacing steps in base AdminAddDefaultVideoSimpleProductTest --> -+ -+ <actionGroup ref="goToCreateProductPage" stepKey="goToCreateProductPage"> -+ <argument name="product" value="defaultVirtualProduct"/> -+ </actionGroup> -+ <actionGroup ref="fillMainProductFormNoWeight" stepKey="fillMainProductForm"> -+ <argument name="product" value="defaultVirtualProduct"/> -+ </actionGroup> -+ <actionGroup ref="AssertProductInStorefrontProductPage" stepKey="AssertProductInStorefrontProductPage"> -+ <argument name="product" value="defaultVirtualProduct"/> -+ </actionGroup> -+ </test> -+</tests> -diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminAddImageForCategoryTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminAddImageForCategoryTest.xml -index 6ee72877a0d..8ac0cfa512b 100644 ---- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminAddImageForCategoryTest.xml -+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminAddImageForCategoryTest.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="AdminAddImageForCategoryTest"> - <annotations> - <features value="Catalog"/> -diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminAddImageToWYSIWYGCatalogTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminAddImageToWYSIWYGCatalogTest.xml -index 479247ade8c..50d192a27e4 100644 ---- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminAddImageToWYSIWYGCatalogTest.xml -+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminAddImageToWYSIWYGCatalogTest.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="AdminAddImageToWYSIWYGCatalogTest"> - <before> - <actionGroup ref="LoginActionGroup" stepKey="login"/> -diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminAddImageToWYSIWYGProductTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminAddImageToWYSIWYGProductTest.xml -index e9d17b5c70d..f3d3e653b26 100644 ---- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminAddImageToWYSIWYGProductTest.xml -+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminAddImageToWYSIWYGProductTest.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="AdminAddImageToWYSIWYGProductTest"> - <annotations> - <features value="Catalog"/> -@@ -16,82 +16,80 @@ - <description value="Admin should be able to add image to WYSIWYG Editor on Product Page"/> - <severity value="CRITICAL"/> - <testCaseId value="MAGETWO-84375"/> -+ <skip> -+ <issueId value="MC-17232"/> -+ </skip> - </annotations> - <before> - <actionGroup ref="LoginActionGroup" stepKey="login"/> - <actionGroup ref="EnabledWYSIWYG" stepKey="enableWYSIWYG"/> - <actionGroup ref="SwitchToVersion4ActionGroup" stepKey="switchToTinyMCE4" /> - </before> -+ <after> -+ <actionGroup ref="DisabledWYSIWYG" stepKey="disableWYSIWYG"/> -+ <actionGroup ref="logout" stepKey="logout"/> -+ </after> -+ - <amOnPage url="{{AdminProductCreatePage.url(AddToDefaultSet.attributeSetId, 'simple')}}" stepKey="navigateToNewProduct"/> -- <waitForPageLoad stepKey="waitForPageLoad"/> -- <fillField userInput="{{_defaultProduct.name}}" selector="{{AdminProductFormSection.productName}}" stepKey="fillName"/> -- <fillField userInput="{{_defaultProduct.price}}" selector="{{AdminProductFormSection.productPrice}}" stepKey="fillPrice"/> -- <fillField userInput="{{_defaultProduct.sku}}" selector="{{AdminProductFormSection.productSku}}" stepKey="fillSKU"/> -- <fillField userInput="{{_defaultProduct.quantity}}" selector="{{AdminProductFormSection.productQuantity}}" stepKey="fillQuantity"/> -- <scrollTo selector="{{AdminProductFormSection.productQuantity}}" stepKey="scrollToQty" /> -+ <waitForPageLoad stepKey="waitForPageLoadProductCreatePage"/> -+ <actionGroup ref="fillMainProductForm" stepKey="fillBasicProductInfo" /> -+ - <click selector="{{AdminProductFormSection.contentTab}}" stepKey="clickContentTab" /> - <waitForElementVisible selector="{{ProductDescriptionWYSIWYGToolbarSection.TinyMCE4}}" stepKey="waitForDescription" /> - <click selector="{{ProductDescriptionWYSIWYGToolbarSection.InsertImageIcon}}" stepKey="clickInsertImageIcon1" /> -- <waitForPageLoad stepKey="waitForPageLoad1" /> - <click selector="{{ProductDescriptionWYSIWYGToolbarSection.Browse}}" stepKey="clickBrowse1" /> -- <waitForPageLoad stepKey="waitForPageLoad2" /> -- <waitForLoadingMaskToDisappear stepKey="waitForLoading1" /> -- <waitForLoadingMaskToDisappear stepKey="waitForLoading2" /> -+ <waitForLoadingMaskToDisappear stepKey="waitForBrowseModal" /> - <see selector="{{ProductDescriptionWYSIWYGToolbarSection.CancelBtn}}" userInput="Cancel" stepKey="seeCancelBtn1" /> - <see selector="{{ProductDescriptionWYSIWYGToolbarSection.CreateFolder}}" userInput="Create Folder" stepKey="seeCreateFolderBtn1" /> -- <dontSeeElement selector="{{ProductDescriptionWYSIWYGToolbarSection.InsertFile}}" stepKey="dontSeeAddSelectedBtn" /> -- <click selector="{{ProductDescriptionWYSIWYGToolbarSection.CreateFolder}}" stepKey="createFolder1"/> -- <waitForElementVisible selector="{{ProductDescriptionWYSIWYGToolbarSection.FolderName}}" stepKey="waitForPopUp1" /> -+ <dontSeeElement selector="{{ProductDescriptionWYSIWYGToolbarSection.InsertFile}}" stepKey="dontSeeAddSelectedBtn1" /> -+ <click selector="{{ProductDescriptionWYSIWYGToolbarSection.CreateFolder}}" stepKey="createFolder1" /> -+ <waitForElement selector="{{ProductDescriptionWYSIWYGToolbarSection.FolderName}}" stepKey="waitForPopUp1" /> - <fillField selector="{{ProductDescriptionWYSIWYGToolbarSection.FolderName}}" userInput="{{ImageFolder.name}}" stepKey="fillFolderName1" /> - <click selector="{{ProductDescriptionWYSIWYGToolbarSection.AcceptFolderName}}" stepKey="acceptFolderName11" /> -- <waitForLoadingMaskToDisappear stepKey="waitForLoading3" /> - <conditionalClick selector="{{ProductDescriptionWYSIWYGToolbarSection.StorageRootArrow}}" dependentSelector="{{ProductDescriptionWYSIWYGToolbarSection.checkIfArrowExpand}}" stepKey="clickStorageRootArrowIfClosed" visible="true"/> - <conditionalClick selector="{{ProductDescriptionWYSIWYGToolbarSection.WysiwygArrow}}" dependentSelector="{{ProductDescriptionWYSIWYGToolbarSection.checkIfWysiwygArrowExpand}}" stepKey="clickWysiwygArrowIfClosed" visible="true"/> - <waitForText userInput="{{ImageFolder.name}}" stepKey="waitForNewFolder1" /> - <click userInput="{{ImageFolder.name}}" stepKey="clickOnCreatedFolder1" /> -- <waitForLoadingMaskToDisappear stepKey="waitForLoading4" /> -+ <waitForLoadingMaskToDisappear stepKey="waitForLoading4"/> - <attachFile selector="{{ProductDescriptionWYSIWYGToolbarSection.BrowseUploadImage}}" userInput="{{ImageUpload1.value}}" stepKey="uploadImage1"/> -- <waitForLoadingMaskToDisappear stepKey="waitForLoading5" /> -+ <waitForLoadingMaskToDisappear stepKey="waitForFileUpload1"/> - <waitForElementVisible selector="{{ProductDescriptionWYSIWYGToolbarSection.image(ImageUpload1.value)}}" stepKey="waitForUploadImage1" /> - <seeElement selector="{{ProductDescriptionWYSIWYGToolbarSection.imageSelected(ImageUpload1.value)}}" stepKey="seeImageSelected1" /> - <see selector="{{ProductDescriptionWYSIWYGToolbarSection.DeleteSelectedBtn}}" userInput="Delete Selected" stepKey="seeDeleteBtn1"/> - <click selector="{{ProductDescriptionWYSIWYGToolbarSection.DeleteSelectedBtn}}" stepKey="clickDeleteSelected1" /> -- <waitForText userInput="OK" stepKey="waitForConfirm1" /> -- <click selector="{{ProductDescriptionWYSIWYGToolbarSection.confirmDelete}}" stepKey="confirmDelete1" /> -+ <waitForElementVisible selector="{{AdminConfirmationModalSection.ok}}" stepKey="waitForConfirmDelete1"/> -+ <click selector="{{AdminConfirmationModalSection.ok}}" stepKey="confirmDelete1" /> - <waitForElementNotVisible selector="{{ProductDescriptionWYSIWYGToolbarSection.image(ImageUpload1.value)}}" stepKey="waitForImageDeleted1" /> - <dontSeeElement selector="{{ProductDescriptionWYSIWYGToolbarSection.image(ImageUpload1.value)}}" stepKey="dontSeeImage1" /> -- <dontSeeElement selector="{{ProductDescriptionWYSIWYGToolbarSection.InsertFile}}" stepKey="dontSeeAddSelectedBtn1" /> -+ <dontSeeElement selector="{{ProductDescriptionWYSIWYGToolbarSection.InsertFile}}" stepKey="dontSeeAddSelectedBtn2" /> - <attachFile selector="{{ProductDescriptionWYSIWYGToolbarSection.BrowseUploadImage}}" userInput="{{ImageUpload1.value}}" stepKey="uploadImage2"/> -- <waitForLoadingMaskToDisappear stepKey="waitForLoading6" /> -+ <waitForLoadingMaskToDisappear stepKey="waitForFileUpload2"/> - <waitForElementVisible selector="{{ProductDescriptionWYSIWYGToolbarSection.image(ImageUpload1.value)}}" stepKey="waitForUploadImage2" /> - <click selector="{{ProductDescriptionWYSIWYGToolbarSection.InsertFile}}" stepKey="clickInsertBtn1" /> -- <waitForLoadingMaskToDisappear stepKey="waitForLoading7" /> -- <waitForElementVisible selector="{{ProductDescriptionWYSIWYGToolbarSection.OkBtn}}" stepKey="waitForOkBtn1" /> -+ <waitForElementVisible selector="{{ProductDescriptionWYSIWYGToolbarSection.ImageDescription}}" stepKey="waitForImageDescriptionButton1" /> - <fillField selector="{{ProductDescriptionWYSIWYGToolbarSection.ImageDescription}}" userInput="{{ImageUpload1.content}}" stepKey="fillImageDescription1" /> - <fillField selector="{{ProductDescriptionWYSIWYGToolbarSection.Height}}" userInput="{{ImageUpload1.height}}" stepKey="fillImageHeight1" /> - <click selector="{{ProductDescriptionWYSIWYGToolbarSection.OkBtn}}" stepKey="clickOkBtn1" /> -- <waitForPageLoad stepKey="waitForPageLoad3"/> - <scrollTo selector="{{ProductDescriptionWYSIWYGToolbarSection.TinyMCE4}}" stepKey="scrollToTinyMCE4" /> - <click selector="{{ProductShortDescriptionWYSIWYGToolbarSection.InsertImageIcon}}" stepKey="clickInsertImageIcon2" /> -- <waitForPageLoad stepKey="waitForPageLoad4" /> - <click selector="{{ProductShortDescriptionWYSIWYGToolbarSection.Browse}}" stepKey="clickBrowse2" /> -- <waitForPageLoad stepKey="waitForPageLoad5" /> -- <waitForLoadingMaskToDisappear stepKey="waitForLoading8" /> -+ <waitForElementVisible selector="{{ProductDescriptionWYSIWYGToolbarSection.CancelBtn}}" stepKey="waitForCancelButton2"/> - <see selector="{{ProductShortDescriptionWYSIWYGToolbarSection.CancelBtn}}" userInput="Cancel" stepKey="seeCancelBtn2" /> -+ <waitForLoadingMaskToDisappear stepKey="waitForLoading13"/> - <see selector="{{ProductShortDescriptionWYSIWYGToolbarSection.CreateFolder}}" userInput="Create Folder" stepKey="seeCreateFolderBtn2" /> -- <dontSeeElement selector="{{ProductShortDescriptionWYSIWYGToolbarSection.InsertFile}}" stepKey="dontSeeAddSelectedBtn2" /> -+ <waitForLoadingMaskToDisappear stepKey="waitForLoading14"/> -+ <dontSeeElement selector="{{ProductShortDescriptionWYSIWYGToolbarSection.InsertFile}}" stepKey="dontSeeAddSelectedBtn3" /> - <attachFile selector="{{ProductShortDescriptionWYSIWYGToolbarSection.BrowseUploadImage}}" userInput="{{ImageUpload3.value}}" stepKey="uploadImage3"/> -+ <waitForLoadingMaskToDisappear stepKey="waitForFileUpload3"/> - <waitForElementVisible selector="{{ProductShortDescriptionWYSIWYGToolbarSection.image(ImageUpload3.value)}}" stepKey="waitForUploadImage3" /> -- <waitForLoadingMaskToDisappear stepKey="waitForLoading9" /> -- <wait time="3" stepKey="waitMore" /> - <waitForElement selector="{{ProductShortDescriptionWYSIWYGToolbarSection.DeleteSelectedBtn}}" stepKey="waitForDeletebtn" /> - <see selector="{{ProductShortDescriptionWYSIWYGToolbarSection.DeleteSelectedBtn}}" userInput="Delete Selected" stepKey="seeDeleteBtn2"/> - <click selector="{{ProductShortDescriptionWYSIWYGToolbarSection.DeleteSelectedBtn}}" stepKey="clickDeleteSelected2" /> -- <waitForText userInput="OK" stepKey="waitForConfirm3" /> -- <click selector="{{ProductShortDescriptionWYSIWYGToolbarSection.confirmDelete}}" stepKey="confirmDelete2" /> -- <dontSeeElement selector="{{ProductDescriptionWYSIWYGToolbarSection.InsertFile}}" stepKey="dontSeeAddSelectedBtn3" /> -+ <waitForElementVisible selector="{{AdminConfirmationModalSection.ok}}" stepKey="waitForConfirm3"/> -+ <click selector="{{AdminConfirmationModalSection.ok}}" stepKey="confirmDelete2" /> -+ <dontSeeElement selector="{{ProductDescriptionWYSIWYGToolbarSection.InsertFile}}" stepKey="dontSeeAddSelectedBtn4" /> - <attachFile selector="{{ProductShortDescriptionWYSIWYGToolbarSection.BrowseUploadImage}}" userInput="{{ImageUpload3.value}}" stepKey="uploadImage4"/> -- <waitForLoadingMaskToDisappear stepKey="waitForLoading10" /> -+ <waitForLoadingMaskToDisappear stepKey="waitForFileUpload4"/> - <waitForElementVisible selector="{{ProductShortDescriptionWYSIWYGToolbarSection.image(ImageUpload3.value)}}" stepKey="waitForUploadImage4" /> - <click selector="{{ProductShortDescriptionWYSIWYGToolbarSection.InsertFile}}" stepKey="clickInsertBtn" /> - <waitForLoadingMaskToDisappear stepKey="waitForLoading11" /> -@@ -107,9 +105,5 @@ - <seeElement selector="{{StorefrontProductInfoMainSection.mediaDescription}}" stepKey="assertMediaDescription"/> - <seeElementInDOM selector="{{StorefrontCategoryMainSection.imageSource(ImageUpload3.fileName)}}" stepKey="assertMediaSource3"/> - <seeElementInDOM selector="{{StorefrontCategoryMainSection.imageSource(ImageUpload1.fileName)}}" stepKey="assertMediaSource1"/> -- <after> -- <actionGroup ref="DisabledWYSIWYG" stepKey="disableWYSIWYG"/> -- <actionGroup ref="logout" stepKey="logout"/> -- </after> - </test> - </tests> -diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminAddInStockProductToTheCartTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminAddInStockProductToTheCartTest.xml -new file mode 100644 -index 00000000000..feb4fffd12f ---- /dev/null -+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminAddInStockProductToTheCartTest.xml -@@ -0,0 +1,88 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> -+ <test name="AdminAddInStockProductToTheCartTest"> -+ <annotations> -+ <stories value="Manage products"/> -+ <title value="Add In Stock Product to Cart"/> -+ <description value="Login as admin and add In Stock product to the cart"/> -+ <severity value="CRITICAL"/> -+ <testCaseId value="MC-11065"/> -+ <group value="mtf_migrated"/> -+ </annotations> -+ <before> -+ <!-- Login as admin --> -+ <actionGroup ref="LoginAsAdmin" stepKey="login"/> -+ <!--Create Category--> -+ <createData entity="SimpleSubCategory" stepKey="createCategory"/> -+ <!--Create Simple Product--> -+ <createData entity="SimpleProduct" stepKey="createSimpleProduct"> -+ <requiredEntity createDataKey="createCategory"/> -+ </createData> -+ </before> -+ <after> -+ <!--Delete created entity --> -+ <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> -+ <deleteData createDataKey="createSimpleProduct" stepKey="deleteSimpleProduct"/> -+ <actionGroup ref="logout" stepKey="logout"/> -+ </after> -+ <!--Open Product Index Page and filter the product--> -+ <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="navigateToProductIndex"/> -+ <waitForPageLoad stepKey="waitForProductIndexPageToLoad"/> -+ <actionGroup ref="filterProductGridBySku" stepKey="filterProduct"> -+ <argument name="product" value="SimpleProduct"/> -+ </actionGroup> -+ <!-- Update product Advanced Inventory setting --> -+ <click stepKey="openSelectedProduct" selector="{{AdminProductGridSection.productRowBySku($$createSimpleProduct.sku$$)}}"/> -+ <waitForPageLoad stepKey="waitForProductToLoad"/> -+ <click selector="{{AdminProductFormSection.advancedInventoryLink}}" stepKey="clickOnAdvancedInventoryLink"/> -+ <waitForPageLoad stepKey="waitForAdvancedInventoryPageToLoad"/> -+ <uncheckOption selector="{{AdminProductFormAdvancedInventorySection.useConfigSettings}}" stepKey="uncheckConfigSetting"/> -+ <selectOption selector="{{AdminProductFormAdvancedInventorySection.manageStock}}" userInput="Yes" stepKey="clickOnManageStock"/> -+ <fillField selector="{{AdminProductFormAdvancedInventorySection.advancedInventoryQty}}" userInput="5" stepKey="fillProductQty"/> -+ <uncheckOption selector="{{AdminProductFormAdvancedInventorySection.miniQtyConfigSetting}}" stepKey="uncheckMiniQtyCheckBox"/> -+ <fillField selector="{{AdminProductFormAdvancedInventorySection.miniQtyAllowedInCart}}" userInput="1" stepKey="fillMiniAllowedQty"/> -+ <uncheckOption selector="{{AdminProductFormAdvancedInventorySection.maxiQtyConfigSetting}}" stepKey="uncheckMaxQtyCheckBox"/> -+ <fillField selector="{{AdminProductFormAdvancedInventorySection.maxiQtyAllowedInCart}}" userInput="10000" stepKey="fillMaxAllowedQty"/> -+ <selectOption selector="{{AdminProductFormAdvancedInventorySection.qtyUsesDecimals}}" userInput="Yes" stepKey="selectQuatityUsesDecimal"/> -+ <uncheckOption selector="{{AdminProductFormAdvancedInventorySection.notifyBelowQtyConfigSetting}}" stepKey="uncheckNotifyBelowQtyheckBox"/> -+ <fillField selector="{{AdminProductFormAdvancedInventorySection.notifyBelowQty}}" userInput="1" stepKey="fillNotifyBelowQty"/> -+ <selectOption selector="{{AdminProductFormAdvancedInventorySection.advancedInventoryStockStatus}}" userInput="In Stock" stepKey="selectOutOfStock"/> -+ <click stepKey="clickOnDoneButton" selector="{{AdminProductFormAdvancedInventorySection.doneButton}}"/> -+ <waitForPageLoad stepKey="waitForProductPageToLoad"/> -+ <click selector="{{AdminProductFormActionSection.saveButton}}" stepKey="clickOnSaveButton"/> -+ <waitForLoadingMaskToDisappear stepKey="waitForLoading"/> -+ <see selector="{{AdminCategoryMessagesSection.SuccessMessage}}" userInput="You saved the product." stepKey="messageYouSavedTheProductIsShown"/> -+ <!--Verify product is visible in category front page --> -+ <amOnPage url="$$createCategory.name$$.html" stepKey="openCategoryStoreFrontPage"/> -+ <waitForPageLoad stepKey="waitForCategoryPageToLoad"/> -+ <seeElement selector="{{StorefrontHeaderSection.NavigationCategoryByName(SimpleSubCategory.name)}}" stepKey="seeCategoryInFrontPage"/> -+ <click selector="{{StorefrontHeaderSection.NavigationCategoryByName(SimpleSubCategory.name)}}" stepKey="clickOnCategory"/> -+ <see selector="{{StorefrontCategoryMainSection.productName}}" userInput="{{SimpleProduct.name}}" stepKey="seeProductNameInCategoryPage"/> -+ <!--Verify Product In Store Front--> -+ <amOnPage url="$$createSimpleProduct.name$$.html" stepKey="goToStorefrontPage"/> -+ <waitForPageLoad stepKey="waitForProductFrontPageToLoad"/> -+ <see selector="{{StorefrontProductInfoMainSection.productName}}" userInput="{{SimpleProduct.name}}" stepKey="seeProductNameInStoreFront"/> -+ <see selector="{{StorefrontProductInfoMainSection.productPrice}}" userInput="{{SimpleProduct.price}}" stepKey="seeProductPriceInStoreFront"/> -+ <see selector="{{StorefrontProductInfoMainSection.productSku}}" userInput="{{SimpleProduct.sku}}" stepKey="seeProductSkuInStoreFront"/> -+ <see selector="{{StorefrontProductInfoMainSection.productStockStatus}}" userInput="In Stock" stepKey="seeProductStatusInStoreFront"/> -+ <!--Add Product to the cart--> -+ <fillField selector="{{StorefrontProductPageSection.qtyInput}}" userInput="1" stepKey="fillProductQuantity"/> -+ <click selector="{{StorefrontProductActionSection.addToCart}}" stepKey="clickOnAddToCartButton"/> -+ <waitForPageLoad stepKey="waitForProductToAddInCart"/> -+ <waitForElementVisible selector="{{StorefrontMessagesSection.success}}" stepKey="waitForSuccessMessage"/> -+ <seeElement selector="{{StorefrontProductPageSection.successMsg}}" stepKey="seeSuccessSaveMessage"/> -+ <seeElement selector="{{StorefrontMinicartSection.quantity(1)}}" stepKey="seeAddedProductQuantityInCart"/> -+ <click selector="{{StorefrontMinicartSection.showCart}}" stepKey="clickOnMiniCart"/> -+ <see selector="{{StorefrontMinicartSection.miniCartItemsText}}" userInput="{{SimpleProduct.name}}" stepKey="seeProductNameInMiniCart"/> -+ <see selector="{{StorefrontMinicartSection.miniCartItemsText}}" userInput="{{SimpleProduct.price}}" stepKey="seeProductPriceInMiniCart"/> -+ <seeElement selector="{{StorefrontMinicartSection.goToCheckout}}" stepKey="seeCheckOutButtonInMiniCart"/> -+ </test> -+</tests> -diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminApplyTierPriceToProductTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminApplyTierPriceToProductTest.xml -index 902f51c4a15..545e7c10379 100644 ---- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminApplyTierPriceToProductTest.xml -+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminApplyTierPriceToProductTest.xml -@@ -5,11 +5,11 @@ - * See COPYING.txt for license details. - */ - --> --<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -- xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Test/etc/testSchema.xsd"> -+<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> - <test name="AdminApplyTierPriceToProductTest"> - <annotations> -- <features value="Apply tier price to a product"/> -+ <features value="Catalog"/> -+ <stories value="Apply tier price to a product"/> - <title value="You should be able to apply tier price to a product."/> - <description value="You should be able to apply tier price to a product."/> - <severity value="CRITICAL"/> -@@ -235,9 +235,10 @@ - <scrollToTopOfPage stepKey="scrollToTopOfPage5"/> - <click selector="{{AdminProductFormSection.advancedPricingLink}}" stepKey="clickOnAdvancedPricingButton5"/> - <waitForElement selector="{{AdminProductFormAdvancedPricingSection.customerGroupPriceDeleteButton}}" stepKey="waitForcustomerGroupPriceDeleteButton"/> -- <click selector="{{AdminProductFormAdvancedPricingSection.customerGroupPriceDeleteButton}}" stepKey="deleteFirstRowOfCustomerGroupPrice"/> -- <click selector="{{AdminProductFormAdvancedPricingSection.customerGroupPriceDeleteButton}}" stepKey="deleteSecondRowOfCustomerGroupPrice"/> -- <click selector="{{AdminProductFormAdvancedPricingSection.doneButton}}" stepKey="clickDoneButton5"/> -+ <scrollTo selector="(//*[contains(@class, 'product_form_product_form_advanced_pricing_modal')]//tr//button[@data-action='remove_row'])[1]" x="30" y="0" stepKey="scrollToDeleteFirstRowOfCustomerGroupPrice" /> -+ <click selector="(//tr//button[@data-action='remove_row'])[1]" userInput=".product_form_product_form_advanced_pricing_modal" stepKey="deleteFirstRowOfCustomerGroupPrice"/> -+ <click selector="//tr//button[@data-action='remove_row']" userInput=".product_form_product_form_advanced_pricing_modal" stepKey="deleteSecondRowOfCustomerGroupPrice"/> -+ <click selector="{{AdminProductFormAdvancedPricingSection.doneButton}}" userInput=".product_form_product_form_advanced_pricing_modal" stepKey="clickDoneButton5"/> - <actionGroup ref="saveProductForm" stepKey="saveProduct5"/> - <scrollToTopOfPage stepKey="scrollToTopOfPage6"/> - <click selector="{{AdminProductFormSection.advancedPricingLink}}" stepKey="clickOnAdvancedPricingButton6"/> -@@ -268,4 +269,56 @@ - <actualResult type="variable">grabTextFromMiniCartSubtotalField2</actualResult> - </assertEquals> - </test> -+ <test name="AdminApplyTierPriceToProductWithPercentageDiscountTest"> -+ <annotations> -+ <features value="Catalog"/> -+ <stories value="MC-5517 - System tries to save 0 in Advanced Pricing which is invalid for Discount field"/> -+ <title value="You should be able to apply tier price to a product with float percent discount."/> -+ <description value="You should be able to apply tier price to a product with float percent discount."/> -+ <severity value="AVERAGE"/> -+ <testCaseId value="MAGETWO-96881"/> -+ <group value="product"/> -+ </annotations> -+ <before> -+ <createData entity="_defaultCategory" stepKey="createCategory"/> -+ <createData entity="SimpleProduct" stepKey="createSimpleProduct"> -+ <requiredEntity createDataKey="createCategory"/> -+ <field key="price">100</field> -+ </createData> -+ </before> -+ <after> -+ <deleteData createDataKey="createSimpleProduct" stepKey="deleteSimpleProduct"/> -+ <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> -+ <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="navigateToProductIndex"/> -+ <waitForPageLoad time="30" stepKey="waitForProductIndexPageLoad"/> -+ <actionGroup ref="resetProductGridToDefaultView" stepKey="resetGridToDefaultKeywordSearch"/> -+ <actionGroup ref="logout" stepKey="logoutFromAdmin"/> -+ </after> -+ <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> -+ <actionGroup ref="SearchForProductOnBackendActionGroup" stepKey="searchForSimpleProduct"> -+ <argument name="product" value="$$createSimpleProduct$$"/> -+ </actionGroup> -+ <actionGroup ref="OpenEditProductOnBackendActionGroup" stepKey="openEditProduct1"> -+ <argument name="product" value="$$createSimpleProduct$$"/> -+ </actionGroup> -+ <scrollToTopOfPage stepKey="scrollToTopOfPage"/> -+ <click selector="{{AdminProductFormSection.advancedPricingLink}}" stepKey="clickOnAdvancedPricingButton"/> -+ <waitForElement selector="{{AdminProductFormAdvancedPricingSection.customerGroupPriceAddButton}}" stepKey="waitForCustomerGroupPriceAddButton"/> -+ <click selector="{{AdminProductFormAdvancedPricingSection.customerGroupPriceAddButton}}" stepKey="addCustomerGroupAllGroupsQty1PriceDiscountAndpercent"/> -+ <fillField selector="{{AdminProductFormAdvancedPricingSection.productTierPriceQtyInput('0')}}" userInput="1" stepKey="fillProductTierPriceQtyInput"/> -+ <selectOption selector="{{AdminProductFormAdvancedPricingSection.productTierPriceValueTypeSelect('0')}}" userInput="Discount" stepKey="selectProductTierPriceValueType"/> -+ <fillField selector="{{AdminProductFormAdvancedPricingSection.productTierPricePercentageValuePriceInput('0')}}" userInput="0.1" stepKey="selectProductTierPricePriceInput"/> -+ <click selector="{{AdminProductFormAdvancedPricingSection.doneButton}}" stepKey="clickDoneButton"/> -+ <actionGroup ref="saveProductForm" stepKey="saveProduct1"/> -+ <amOnPage url="{{StorefrontProductPage.url($$createSimpleProduct.sku$$)}}" stepKey="goProductPageOnStorefront"/> -+ <waitForPageLoad time="30" stepKey="waitForPageLoad1"/> -+ <seeElement selector="{{StorefrontCategoryProductSection.productPriceFinal('99.90')}}" stepKey="assertProductFinalPriceProductPage"/> -+ <seeElement selector="{{StorefrontCategoryProductSection.productPriceLabel('Regular Price')}}" stepKey="assertRegularPriceProductPage"/> -+ <seeElement selector="{{StorefrontCategoryProductSection.productPriceOld('100')}}" stepKey="assertRegularPriceAmountProductPage"/> -+ <amOnPage url="{{StorefrontCategoryPage.url($$createCategory.name$$)}}" stepKey="navigateToCategoryPage"/> -+ <waitForPageLoad time="30" stepKey="waitForPageLoad2"/> -+ <seeElement selector="{{StorefrontCategoryProductSection.productPriceFinal('99.90')}}" stepKey="assertProductFinalPriceCategoryPage"/> -+ <seeElement selector="{{StorefrontCategoryProductSection.productPriceLabel('Regular Price')}}" stepKey="assertRegularPriceLabelCategoryPage"/> -+ <seeElement selector="{{StorefrontCategoryProductSection.productPriceOld('100')}}" stepKey="assertRegularPriceAmountCategoryPage"/> -+ </test> - </tests> -diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminAssignProductAttributeToAttributeSetTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminAssignProductAttributeToAttributeSetTest.xml -index aed667db1f7..4261721d360 100644 ---- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminAssignProductAttributeToAttributeSetTest.xml -+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminAssignProductAttributeToAttributeSetTest.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="AdminAssignProductAttributeToAttributeSetTest"> - <annotations> - <features value="Catalog"/> -diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminBackorderAllowedAddProductToCartTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminBackorderAllowedAddProductToCartTest.xml -new file mode 100644 -index 00000000000..88c524eff38 ---- /dev/null -+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminBackorderAllowedAddProductToCartTest.xml -@@ -0,0 +1,53 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> -+ <test name="AdminBackorderAllowedAddProductToCartTest"> -+ <annotations> -+ <stories value="Manage products"/> -+ <title value="Add Product to Cart, Backorder Allowed"/> -+ <description value="Customer should be able to add products to cart when that products quantity is zero"/> -+ <severity value="CRITICAL"/> -+ <testCaseId value="MC-11063"/> -+ <group value="mtf_migrated"/> -+ </annotations> -+ -+ <before> -+ <!-- Create a product that is "In Stock" but has quantity zero --> -+ <createData entity="SimpleProductInStockQuantityZero" stepKey="createProduct"/> -+ -+ <!-- Configure Magento to show out of stock products and to allow backorders --> -+ <magentoCLI command="config:set {{CatalogInventoryOptionsShowOutOfStockEnable.path}} {{CatalogInventoryOptionsShowOutOfStockEnable.value}}" stepKey="setConfigShowOutOfStockTrue"/> -+ <magentoCLI command="config:set {{CatalogInventoryItemOptionsBackordersEnable.path}} {{CatalogInventoryItemOptionsBackordersEnable.value}}" stepKey="setConfigAllowBackordersTrue"/> -+ </before> -+ -+ <after> -+ <!-- Set Magento back to default configuration --> -+ <magentoCLI command="config:set {{CatalogInventoryOptionsShowOutOfStockDisable.path}} {{CatalogInventoryOptionsShowOutOfStockDisable.value}}" stepKey="setConfigShowOutOfStockFalse"/> -+ <magentoCLI command="config:set {{CatalogInventoryItemOptionsBackordersDisable.path}} {{CatalogInventoryItemOptionsBackordersDisable.value}}" stepKey="setConfigAllowBackordersFalse"/> -+ <deleteData createDataKey="createProduct" stepKey="deleteProduct"/> -+ </after> -+ -+ <!-- Go to the storefront and add the product to the cart --> -+ <actionGroup ref="AddSimpleProductToCart" stepKey="gotoAndAddProductToCart"> -+ <argument name="product" value="$$createProduct$$"/> -+ </actionGroup> -+ -+ <!-- Go to the cart page and verify we see the product --> -+ <amOnPage url="{{CheckoutCartPage.url}}" stepKey="gotoCart"/> -+ <waitForPageLoad stepKey="waitForCartLoad"/> -+ <actionGroup ref="AssertStorefrontCheckoutCartItemsActionGroup" stepKey="assertProductItemInCheckOutCart"> -+ <argument name="productName" value="$$createProduct.name$$"/> -+ <argument name="productSku" value="$$createProduct.sku$$"/> -+ <argument name="productPrice" value="$$createProduct.price$$"/> -+ <argument name="subtotal" value="$$createProduct.price$$" /> -+ <argument name="qty" value="1"/> -+ </actionGroup> -+ </test> -+</tests> -diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCatalogCategoriesNavigateMenuTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCatalogCategoriesNavigateMenuTest.xml -new file mode 100644 -index 00000000000..a51df86d032 ---- /dev/null -+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCatalogCategoriesNavigateMenuTest.xml -@@ -0,0 +1,36 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> -+ <test name="AdminCatalogCategoriesNavigateMenuTest"> -+ <annotations> -+ <features value="Catalog"/> -+ <stories value="Menu Navigation"/> -+ <title value="Admin catalog categories navigate menu test"/> -+ <description value="Admin should be able to navigate to Catalog > Categories"/> -+ <severity value="CRITICAL"/> -+ <testCaseId value="MC-14131"/> -+ <group value="menu"/> -+ <group value="mtf_migrated"/> -+ </annotations> -+ <before> -+ <actionGroup ref="LoginAsAdmin" stepKey="LoginAsAdmin"/> -+ </before> -+ <after> -+ <actionGroup ref="logout" stepKey="logout"/> -+ </after> -+ <actionGroup ref="AdminNavigateMenuActionGroup" stepKey="navigateToCategoriesPage"> -+ <argument name="menuUiId" value="{{AdminMenuCatalog.dataUiId}}"/> -+ <argument name="submenuUiId" value="{{AdminMenuCatalogCategories.dataUiId}}"/> -+ </actionGroup> -+ <actionGroup ref="AdminAssertPageTitleActionGroup" stepKey="seePageTitle"> -+ <argument name="title" value="{{AdminMenuCatalogCategories.pageTitle}}"/> -+ </actionGroup> -+ </test> -+</tests> -diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCatalogProductsNavigateMenuTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCatalogProductsNavigateMenuTest.xml -new file mode 100644 -index 00000000000..1d9400bf81e ---- /dev/null -+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCatalogProductsNavigateMenuTest.xml -@@ -0,0 +1,36 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> -+ <test name="AdminCatalogProductsNavigateMenuTest"> -+ <annotations> -+ <features value="Catalog"/> -+ <stories value="Menu Navigation"/> -+ <title value="Admin catalog products navigate menu test"/> -+ <description value="Admin should be able to navigate to Catalog > Products"/> -+ <severity value="CRITICAL"/> -+ <testCaseId value="MC-14130"/> -+ <group value="menu"/> -+ <group value="mtf_migrated"/> -+ </annotations> -+ <before> -+ <actionGroup ref="LoginAsAdmin" stepKey="LoginAsAdmin"/> -+ </before> -+ <after> -+ <actionGroup ref="logout" stepKey="logout"/> -+ </after> -+ <actionGroup ref="AdminNavigateMenuActionGroup" stepKey="navigateToCatalogProductsPage"> -+ <argument name="menuUiId" value="{{AdminMenuCatalog.dataUiId}}"/> -+ <argument name="submenuUiId" value="{{AdminMenuCatalogProducts.dataUiId}}"/> -+ </actionGroup> -+ <actionGroup ref="AdminAssertPageTitleActionGroup" stepKey="seePageTitle"> -+ <argument name="title" value="{{AdminMenuCatalogProducts.pageTitle}}"/> -+ </actionGroup> -+ </test> -+</tests> -diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminChangeProductAttributeSet.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminChangeProductAttributeSet.xml -new file mode 100644 -index 00000000000..bcfab6ccfdf ---- /dev/null -+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminChangeProductAttributeSet.xml -@@ -0,0 +1,66 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> -+ <test name="AdminChangeProductAttributeSet"> -+ <annotations> -+ <features value="Checkout"/> -+ <stories value="The required product attribute is not displayed when change attribute set"/> -+ <title value="Attributes from the selected attribute set should be shown"/> -+ <description value="Attributes from the selected attribute set should be shown"/> -+ <severity value="CRITICAL"/> -+ <testCaseId value="MAGETWO-98452"/> -+ <useCaseId value="MAGETWO-98357"/> -+ <group value="catalog"/> -+ </annotations> -+ <before> -+ <createData entity="SimpleSubCategory" stepKey="createCategory"/> -+ <createData entity="_defaultProduct" stepKey="createSimpleProduct"> -+ <requiredEntity createDataKey="createCategory"/> -+ </createData> -+ -+ <createData entity="productAttributeWithTwoOptions" stepKey="createProductAttribute"/> -+ <createData entity="productAttributeOption1" stepKey="createProductAttributeOption1"> -+ <requiredEntity createDataKey="createProductAttribute"/> -+ </createData> -+ <createData entity="productAttributeOption2" stepKey="createProductAttributeOption2"> -+ <requiredEntity createDataKey="createProductAttribute"/> -+ </createData> -+ <createData entity="CatalogAttributeSet" stepKey="createAttributeSet"/> -+ -+ <actionGroup ref="LoginAsAdmin" stepKey="login"/> -+ <amOnPage url="{{AdminProductAttributeSetEditPage.url}}/$$createAttributeSet.attribute_set_id$$/" stepKey="onAttributeSetEdit"/> -+ <actionGroup ref="AssignAttributeToGroup" stepKey="assignAttributeToGroup"> -+ <argument name="group" value="Product Details"/> -+ <argument name="attribute" value="$$createProductAttribute.attribute_code$$"/> -+ </actionGroup> -+ <actionGroup ref="SaveAttributeSet" stepKey="SaveAttributeSet"/> -+ </before> -+ <after> -+ <deleteData createDataKey="createSimpleProduct" stepKey="deleteProduct"/> -+ <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> -+ <deleteData createDataKey="createProductAttribute" stepKey="deleteProductAttribute"/> -+ <deleteData createDataKey="createAttributeSet" stepKey="deleteAttributeSet"/> -+ <actionGroup ref="ClearProductsFilterActionGroup" stepKey="clearProductsFilter"/> -+ </after> -+ -+ <actionGroup ref="SearchForProductOnBackendActionGroup" stepKey="searchForSimpleProduct"> -+ <argument name="product" value="$$createSimpleProduct$$"/> -+ </actionGroup> -+ -+ <actionGroup ref="OpenEditProductOnBackendActionGroup" stepKey="openEditProduct1"> -+ <argument name="product" value="$$createSimpleProduct$$"/> -+ </actionGroup> -+ -+ <click selector="{{AdminProductFormSection.attributeSet}}" stepKey="startEditAttrSet"/> -+ <fillField selector="{{AdminProductFormSection.attributeSetFilter}}" userInput="$$createAttributeSet.attribute_set_name$$" stepKey="searchForAttrSet"/> -+ <click selector="{{AdminProductFormSection.attributeSetFilterResult}}" stepKey="selectAttrSet"/> -+ -+ <waitForText userInput="$$createProductAttribute.default_frontend_label$$" stepKey="seeAttributeInForm"/> -+ </test> -+</tests> -diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCheckConfigurableProductPriceWithDisabledChildProductTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCheckConfigurableProductPriceWithDisabledChildProductTest.xml -new file mode 100644 -index 00000000000..86978a4121a ---- /dev/null -+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCheckConfigurableProductPriceWithDisabledChildProductTest.xml -@@ -0,0 +1,180 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> -+ <test name="AdminCheckConfigurableProductPriceWithDisabledChildProductTest"> -+ <annotations> -+ <stories value="Configurable Product"/> -+ <title value="Check Price for Configurable Product when One Child is Disabled, Others are Enabled"/> -+ <description value="Login as admin and check the configurable product price when one child product is disabled "/> -+ <severity value="CRITICAL"/> -+ <testCaseId value="MC-13749"/> -+ <group value="mtf_migrated"/> -+ </annotations> -+ <before> -+ <!-- Login as Admin --> -+ <actionGroup ref="LoginAsAdmin" stepKey="loginToAdminPanel"/> -+ -+ <!-- Create Default Category --> -+ <createData entity="_defaultCategory" stepKey="createCategory"/> -+ -+ <!-- Create an attribute with three options to be used in the first child product --> -+ <createData entity="productAttributeWithTwoOptions" stepKey="createConfigProductAttribute"/> -+ <createData entity="productAttributeOption1" stepKey="createConfigProductAttributeOption1"> -+ <requiredEntity createDataKey="createConfigProductAttribute"/> -+ </createData> -+ <createData entity="productAttributeOption2" stepKey="createConfigProductAttributeOption2"> -+ <requiredEntity createDataKey="createConfigProductAttribute"/> -+ </createData> -+ <createData entity="productAttributeOption3" stepKey="createConfigProductAttributeOption3"> -+ <requiredEntity createDataKey="createConfigProductAttribute"/> -+ </createData> -+ -+ <!-- Add the attribute just created to default attribute set --> -+ <createData entity="AddToDefaultSet" stepKey="createConfigAddToAttributeSet"> -+ <requiredEntity createDataKey="createConfigProductAttribute"/> -+ </createData> -+ -+ <!-- Get the first option of the attribute created --> -+ <getData entity="ProductAttributeOptionGetter" index="1" stepKey="getConfigAttributeOption1"> -+ <requiredEntity createDataKey="createConfigProductAttribute"/> -+ </getData> -+ -+ <!-- Get the second option of the attribute created --> -+ <getData entity="ProductAttributeOptionGetter" index="2" stepKey="getConfigAttributeOption2"> -+ <requiredEntity createDataKey="createConfigProductAttribute"/> -+ </getData> -+ -+ <!-- Get the third option of the attribute created --> -+ <getData entity="ProductAttributeOptionGetter" index="3" stepKey="getConfigAttributeOption3"> -+ <requiredEntity createDataKey="createConfigProductAttribute"/> -+ </getData> -+ -+ <!-- Create Configurable product --> -+ <createData entity="BaseConfigurableProduct" stepKey="createConfigProduct"> -+ <requiredEntity createDataKey="createCategory"/> -+ </createData> -+ -+ <!-- Create a simple product and give it the attribute with the first option --> -+ <createData entity="ApiSimpleOne" stepKey="createConfigChildProduct1"> -+ <requiredEntity createDataKey="createConfigProductAttribute"/> -+ <requiredEntity createDataKey="getConfigAttributeOption1"/> -+ <field key="price">10.00</field> -+ </createData> -+ -+ <!--Create a simple product and give it the attribute with the second option --> -+ <createData entity="ApiSimpleTwo" stepKey="createConfigChildProduct2"> -+ <requiredEntity createDataKey="createConfigProductAttribute"/> -+ <requiredEntity createDataKey="getConfigAttributeOption2"/> -+ <field key="price">20.00</field> -+ </createData> -+ -+ <!--Create a simple product and give it the attribute with the Third option --> -+ <createData entity="ApiSimpleTwo" stepKey="createConfigChildProduct3"> -+ <requiredEntity createDataKey="createConfigProductAttribute"/> -+ <requiredEntity createDataKey="getConfigAttributeOption3"/> -+ <field key="price">30.00</field> -+ </createData> -+ -+ <!-- Create the configurable product --> -+ <createData entity="ConfigurableProductThreeOptions" stepKey="createConfigProductOption"> -+ <requiredEntity createDataKey="createConfigProduct"/> -+ <requiredEntity createDataKey="createConfigProductAttribute"/> -+ <requiredEntity createDataKey="getConfigAttributeOption1"/> -+ <requiredEntity createDataKey="getConfigAttributeOption2"/> -+ <requiredEntity createDataKey="getConfigAttributeOption3"/> -+ </createData> -+ -+ <!-- Add the first simple product to the configurable product --> -+ <createData entity="ConfigurableProductAddChild" stepKey="createConfigProductAddChild1"> -+ <requiredEntity createDataKey="createConfigProduct"/> -+ <requiredEntity createDataKey="createConfigChildProduct1"/> -+ </createData> -+ -+ <!-- Add the second simple product to the configurable product --> -+ <createData entity="ConfigurableProductAddChild" stepKey="createConfigProductAddChild2"> -+ <requiredEntity createDataKey="createConfigProduct"/> -+ <requiredEntity createDataKey="createConfigChildProduct2"/> -+ </createData> -+ -+ <!-- Add the third simple product to the configurable product --> -+ <createData entity="ConfigurableProductAddChild" stepKey="createConfigProductAddChild3"> -+ <requiredEntity createDataKey="createConfigProduct"/> -+ <requiredEntity createDataKey="createConfigChildProduct3"/> -+ </createData> -+ </before> -+ <after> -+ <!-- Delete Created Data --> -+ <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> -+ <deleteData createDataKey="createConfigProduct" stepKey="deleteConfigProduct"/> -+ <deleteData createDataKey="createConfigChildProduct1" stepKey="deleteConfigChildProduct1"/> -+ <deleteData createDataKey="createConfigChildProduct2" stepKey="deleteConfigChildProduct2"/> -+ <deleteData createDataKey="createConfigChildProduct3" stepKey="deleteConfigChildProduct3"/> -+ <deleteData createDataKey="createConfigProductAttribute" stepKey="deleteAttribute"/> -+ <actionGroup ref="logout" stepKey="logout"/> -+ </after> -+ -+ <!-- Open Product in Store Front Page --> -+ <amOnPage url="$$createConfigProduct.sku$$.html" stepKey="openProductInStoreFront"/> -+ <waitForPageLoad stepKey="waitForProductToLoad"/> -+ -+ <!-- Verify category,Configurable product and initial price --> -+ <seeElement selector="{{StorefrontHeaderSection.NavigationCategoryByName($$createCategory.name$$)}}" stepKey="seeCategoryInFrontPage"/> -+ <see selector="{{StorefrontProductInfoMainSection.productName}}" userInput="$$createConfigProduct.name$$" stepKey="seeProductNameInStoreFront"/> -+ <see selector="{{StorefrontProductInfoMainSection.productPrice}}" userInput="$$createConfigChildProduct1.price$$" stepKey="seeInitialPriceInStoreFront"/> -+ <see selector="{{StorefrontProductInfoMainSection.productSku}}" userInput="$$createConfigProduct.sku$$" stepKey="seeProductSkuInStoreFront"/> -+ <see selector="{{StorefrontProductInfoMainSection.productStockStatus}}" userInput="In Stock" stepKey="seeProductStatusInStoreFront"/> -+ -+ <!-- Verify First Child Product attribute option is displayed --> -+ <see selector="{{StorefrontProductInfoMainSection.productOptionSelect($$createConfigProductAttribute.default_value$$)}}" userInput="$$getConfigAttributeOption1.label$$" stepKey="seeOption1"/> -+ -+ <!-- Select product Attribute option1, option2 and option3 and verify changes in the price --> -+ <selectOption selector="{{StorefrontProductInfoMainSection.productOptionSelect($$createConfigProductAttribute.default_value$$)}}" userInput="$$getConfigAttributeOption1.label$$" stepKey="selectOption1"/> -+ <see selector="{{StorefrontProductInfoMainSection.productPrice}}" userInput="$$createConfigChildProduct1.price$$" stepKey="seeChildProduct1PriceInStoreFront"/> -+ <selectOption selector="{{StorefrontProductInfoMainSection.productOptionSelect($$createConfigProductAttribute.default_value$$)}}" userInput="$$getConfigAttributeOption2.label$$" stepKey="selectOption2"/> -+ <see selector="{{StorefrontProductInfoMainSection.productPrice}}" userInput="$$createConfigChildProduct2.price$$" stepKey="seeChildProduct2PriceInStoreFront"/> -+ <selectOption selector="{{StorefrontProductInfoMainSection.productOptionSelect($$createConfigProductAttribute.default_value$$)}}" userInput="$$getConfigAttributeOption3.label$$" stepKey="selectOption3"/> -+ <see selector="{{StorefrontProductInfoMainSection.productPrice}}" userInput="$$createConfigChildProduct3.price$$" stepKey="seeChildProduct3PriceInStoreFront"/> -+ -+ <!-- Open Product Index Page and Filter First Child product --> -+ <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="navigateToProductIndex"/> -+ <waitForPageLoad stepKey="waitForProductIndexPageToLoad"/> -+ <actionGroup ref="filterProductGridBySku" stepKey="filterProduct"> -+ <argument name="product" value="ApiSimpleOne"/> -+ </actionGroup> -+ <click selector="{{AdminProductGridFilterSection.nthRow('1')}}" stepKey="selectFirstRow"/> -+ <waitForPageLoad stepKey="waitForProductPageToLoad"/> -+ -+ <!-- Disable the product --> -+ <click selector="{{AdminProductFormSection.enableProductLabel}}" stepKey="disableProduct"/> -+ <click selector="{{AdminProductFormActionSection.saveButton}}" stepKey="clickOnSaveButton"/> -+ <waitForLoadingMaskToDisappear stepKey="waitForLoading"/> -+ <see selector="{{AdminCategoryMessagesSection.SuccessMessage}}" userInput="You saved the product." stepKey="messageYouSavedTheProductIsShown"/> -+ -+ <!-- Open Product Store Front Page --> -+ <amOnPage url="$$createConfigProduct.sku$$.html" stepKey="openProductInStoreFront1"/> -+ <waitForPageLoad stepKey="waitForProductToLoad1"/> -+ -+ <!-- Verify category,configurable product and updated price --> -+ <seeElement selector="{{StorefrontHeaderSection.NavigationCategoryByName($$createCategory.name$$)}}" stepKey="seeCategoryInFrontPage1"/> -+ <see selector="{{StorefrontProductInfoMainSection.productName}}" userInput="$$createConfigProduct.name$$" stepKey="seeProductNameInStoreFront1"/> -+ <see selector="{{StorefrontProductInfoMainSection.productPrice}}" userInput="$$createConfigChildProduct2.price$$" stepKey="seeUpdatedProductPriceInStoreFront"/> -+ <see selector="{{StorefrontProductInfoMainSection.productSku}}" userInput="$$createConfigProduct.sku$$" stepKey="seeProductSkuInStoreFront1"/> -+ <see selector="{{StorefrontProductInfoMainSection.productStockStatus}}" userInput="In Stock" stepKey="seeProductStatusInStoreFront1"/> -+ -+ <!-- Verify product Attribute Option1 is not displayed --> -+ <dontSee selector="{{StorefrontProductInfoMainSection.productOptionSelect($$createConfigProductAttribute.default_value$$)}}" userInput="$$getConfigAttributeOption1.label$$" stepKey="dontSeeOption1"/> -+ -+ <!--Select product Attribute option2 and option3 and verify changes in the price --> -+ <selectOption selector="{{StorefrontProductInfoMainSection.productOptionSelect($$createConfigProductAttribute.default_value$$)}}" userInput="$$getConfigAttributeOption2.label$$" stepKey="selectTheOption2"/> -+ <see selector="{{StorefrontProductInfoMainSection.productPrice}}" userInput="$$createConfigChildProduct2.price$$" stepKey="seeSecondChildProductPriceInStoreFront"/> -+ <selectOption selector="{{StorefrontProductInfoMainSection.productOptionSelect($$createConfigProductAttribute.default_value$$)}}" userInput="$$getConfigAttributeOption3.label$$" stepKey="selectTheOption3"/> -+ <see selector="{{StorefrontProductInfoMainSection.productPrice}}" userInput="$$createConfigChildProduct3.price$$" stepKey="seeThirdProductPriceInStoreFront"/> -+ </test> -+</tests> -diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCheckConfigurableProductPriceWithOutOfStockChildProductTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCheckConfigurableProductPriceWithOutOfStockChildProductTest.xml -new file mode 100644 -index 00000000000..8d41b276334 ---- /dev/null -+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCheckConfigurableProductPriceWithOutOfStockChildProductTest.xml -@@ -0,0 +1,25 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> -+ <test name="AdminCheckConfigurableProductPriceWithOutOfStockChildProductTest" extends="AdminCheckConfigurableProductPriceWithDisabledChildProductTest"> -+ <annotations> -+ <stories value="Configurable Product"/> -+ <title value="Check Price for Configurable Product when Child is Out of Stock"/> -+ <description value="Login as admin and check the configurable product price when one child product is out of stock "/> -+ <severity value="CRITICAL"/> -+ <testCaseId value="MC-13750"/> -+ <group value="mtf_migrated"/> -+ </annotations> -+ -+ <scrollTo selector="{{AdminProductFormSection.productQuantity}}" stepKey="scrollToProductQuantity" after="waitForProductPageToLoad"/> -+ <remove keyForRemoval="disableProduct"/> -+ <selectOption selector="{{AdminProductFormSection.productStockStatus}}" userInput="Out of Stock" stepKey="selectOutOfStock" after="scrollToProductQuantity"/> -+ </test> -+</tests> -diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCheckInactiveAndNotIncludeInMenuCategoryAndSubcategoryIsNotVisibleInNavigationTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCheckInactiveAndNotIncludeInMenuCategoryAndSubcategoryIsNotVisibleInNavigationTest.xml -new file mode 100644 -index 00000000000..fd22142fcb0 ---- /dev/null -+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCheckInactiveAndNotIncludeInMenuCategoryAndSubcategoryIsNotVisibleInNavigationTest.xml -@@ -0,0 +1,53 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> -+ <test name="AdminCheckInactiveAndNotIncludeInMenuCategoryAndSubcategoryIsNotVisibleInNavigationTest"> -+ <annotations> -+ <stories value="Create category"/> -+ <title value="Inactive Category and subcategory are not visible on navigation menu, Include in Menu = No"/> -+ <description value="Login as admin and verify inactive and inactive include in menu category and subcategory is not visible in navigation menu"/> -+ <severity value="CRITICAL"/> -+ <testCaseId value="MC-13638"/> -+ <group value="mtf_migrated"/> -+ </annotations> -+ <before> -+ <!--Login as admin --> -+ <actionGroup ref="LoginAsAdmin" stepKey="loginToAdminPanel"/> -+ <!--Create Parent Inactive and Not Include In Menu Category --> -+ <createData entity="CatInactiveNotInMenu" stepKey="createCategory"/> -+ </before> -+ -+ <after> -+ <!--Delete created data--> -+ <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> -+ <actionGroup ref="logout" stepKey="logout"/> -+ </after> -+ <!--Open Category Page--> -+ <amOnPage url="{{AdminCategoryPage.url}}" stepKey="openAdminCategoryIndexPage"/> -+ <waitForPageLoad stepKey="waitForPageToLoaded"/> -+ <!--Create subcategory under parent category --> -+ <click selector="{{AdminCategorySidebarTreeSection.expandAll}}" stepKey="clickOnExpandTree"/> -+ <waitForPageLoad stepKey="waitForCategoryToLoad"/> -+ <click selector="{{AdminCategorySidebarTreeSection.categoryInTree($$createCategory.name$$)}}" stepKey="selectCategory"/> -+ <waitForPageLoad stepKey="waitForPageToLoad"/> -+ <click selector="{{AdminCategorySidebarActionSection.AddSubcategoryButton}}" stepKey="clickOnAddSubCategoryButton"/> -+ <checkOption selector="{{AdminCategoryBasicFieldSection.EnableCategory}}" stepKey="enableCategory"/> -+ <checkOption selector="{{AdminCategoryBasicFieldSection.IncludeInMenu}}" stepKey="enableIncludeInMenu"/> -+ <fillField selector="{{AdminCategoryBasicFieldSection.CategoryNameInput}}" userInput="{{SimpleSubCategory.name}}" stepKey="addSubCategoryName"/> -+ <click selector="{{AdminCategoryMainActionsSection.SaveButton}}" stepKey="saveSubCategory"/> -+ <waitForPageLoad stepKey="waitForSecondCategoryToSave"/> -+ <seeElement selector="{{AdminCategoryMessagesSection.SuccessMessage}}" stepKey="seeSuccessMessage"/> -+ <!-- Verify Parent Category and Sub category is not visible in navigation menu --> -+ <amOnPage url="$$createCategory.name_lwr$$/{{SimpleSubCategory.name_lwr}}.html" stepKey="openCategoryStoreFrontPage"/> -+ <waitForPageLoad stepKey="waitForCategoryStoreFrontPageToLoad"/> -+ <dontSeeElement selector="{{StorefrontHeaderSection.NavigationCategoryByName($$createCategory.name$$)}}" stepKey="dontSeeCategoryOnStoreNavigationBar"/> -+ <dontSeeElement selector="{{StorefrontHeaderSection.NavigationCategoryByName(SimpleSubCategory.name)}}" stepKey="dontSeeSubCategoryOnStoreNavigation"/> -+ </test> -+</tests> -diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCheckInactiveCategoryAndSubcategoryIsNotVisibleInNavigationMenuTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCheckInactiveCategoryAndSubcategoryIsNotVisibleInNavigationMenuTest.xml -new file mode 100644 -index 00000000000..b6c76d65772 ---- /dev/null -+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCheckInactiveCategoryAndSubcategoryIsNotVisibleInNavigationMenuTest.xml -@@ -0,0 +1,52 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> -+ <test name="AdminCheckInactiveCategoryAndSubcategoryIsNotVisibleInNavigationMenuTest"> -+ <annotations> -+ <stories value="Create category"/> -+ <title value="Inactive Category and subcategory are not visible on navigation menu, Include in Menu = Yes"/> -+ <description value="Login as admin and verify inactive category and subcategory is not visible in navigation menu"/> -+ <severity value="CRITICAL"/> -+ <testCaseId value="MC-13637"/> -+ <group value="mtf_migrated"/> -+ </annotations> -+ <before> -+ <!--Login as Admin --> -+ <actionGroup ref="LoginAsAdmin" stepKey="loginToAdminPanel"/> -+ <!--Create Parent Inactive Category --> -+ <createData entity="CatNotActive" stepKey="createCategory"/> -+ </before> -+ <after> -+ <!--Delete created data--> -+ <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> -+ <actionGroup ref="logout" stepKey="logout"/> -+ </after> -+ <!--Open Category Page--> -+ <amOnPage url="{{AdminCategoryPage.url}}" stepKey="openAdminCategoryIndexPage"/> -+ <waitForPageLoad stepKey="waitForPageToLoaded"/> -+ <!--Create subcategory under parent category --> -+ <click selector="{{AdminCategorySidebarTreeSection.expandAll}}" stepKey="clickOnExpandTree"/> -+ <waitForPageLoad stepKey="waitForCategoryToLoad"/> -+ <click selector="{{AdminCategorySidebarTreeSection.categoryInTree($$createCategory.name$$)}}" stepKey="selectCategory"/> -+ <waitForPageLoad stepKey="waitForPageToLoad"/> -+ <click selector="{{AdminCategorySidebarActionSection.AddSubcategoryButton}}" stepKey="clickOnAddSubCategoryButton"/> -+ <checkOption selector="{{AdminCategoryBasicFieldSection.EnableCategory}}" stepKey="enableCategory"/> -+ <checkOption selector="{{AdminCategoryBasicFieldSection.IncludeInMenu}}" stepKey="enableIncludeInMenu"/> -+ <fillField selector="{{AdminCategoryBasicFieldSection.CategoryNameInput}}" userInput="{{SimpleSubCategory.name}}" stepKey="addSubCategoryName"/> -+ <click selector="{{AdminCategoryMainActionsSection.SaveButton}}" stepKey="saveSubCategory"/> -+ <waitForPageLoad stepKey="waitForSecondCategoryToSave"/> -+ <seeElement selector="{{AdminCategoryMessagesSection.SuccessMessage}}" stepKey="seeSuccessMessage"/> -+ <!-- Verify Parent Category and Sub category is not visible in navigation menu --> -+ <amOnPage url="$$createCategory.name_lwr$$/{{SimpleSubCategory.name_lwr}}.html" stepKey="openCategoryStoreFrontPage"/> -+ <waitForPageLoad stepKey="waitForCategoryStoreFrontPageToLoad"/> -+ <dontSeeElement selector="{{StorefrontHeaderSection.NavigationCategoryByName($$createCategory.name$$)}}" stepKey="dontSeeCategoryOnStoreNavigationBar"/> -+ <dontSeeElement selector="{{StorefrontHeaderSection.NavigationCategoryByName(SimpleSubCategory.name)}}" stepKey="dontSeeSubCategoryOnStoreNavigation"/> -+ </test> -+</tests> -diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCheckInactiveIncludeInMenuCategoryAndSubcategoryIsNotVisibleInNavigationTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCheckInactiveIncludeInMenuCategoryAndSubcategoryIsNotVisibleInNavigationTest.xml -new file mode 100644 -index 00000000000..c9cd9acd970 ---- /dev/null -+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCheckInactiveIncludeInMenuCategoryAndSubcategoryIsNotVisibleInNavigationTest.xml -@@ -0,0 +1,53 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> -+ <test name="AdminCheckInactiveIncludeInMenuCategoryAndSubcategoryIsNotVisibleInNavigationTest"> -+ <annotations> -+ <stories value="Create category"/> -+ <title value="Active Category and subcategory are not visible on navigation menu, Include in Menu = No"/> -+ <description value="Login as admin and verify inactive include in menu category and subcategory is not visible in navigation menu"/> -+ <severity value="CRITICAL"/> -+ <testCaseId value="MC-13636"/> -+ <group value="mtf_migrated"/> -+ </annotations> -+ <before> -+ <!--Login as Admin --> -+ <actionGroup ref="LoginAsAdmin" stepKey="loginToAdminPanel"/> -+ <!--Create inactive Include In Menu Parent Category --> -+ <createData entity="CatNotIncludeInMenu" stepKey="createCategory"/> -+ </before> -+ -+ <after> -+ <!--Delete created data--> -+ <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> -+ <actionGroup ref="logout" stepKey="logout"/> -+ </after> -+ <!--Open Category Page--> -+ <amOnPage url="{{AdminCategoryPage.url}}" stepKey="openAdminCategoryIndexPage"/> -+ <waitForPageLoad stepKey="waitForPageToLoaded"/> -+ <!--Create subcategory under parent category --> -+ <click selector="{{AdminCategorySidebarTreeSection.expandAll}}" stepKey="clickOnExpandTree"/> -+ <waitForPageLoad stepKey="waitForCategoryToLoad"/> -+ <click selector="{{AdminCategorySidebarTreeSection.categoryInTree($$createCategory.name$$)}}" stepKey="selectCategory"/> -+ <waitForPageLoad stepKey="waitForPageToLoad"/> -+ <click selector="{{AdminCategorySidebarActionSection.AddSubcategoryButton}}" stepKey="clickOnAddSubCategoryButton"/> -+ <fillField selector="{{AdminCategoryBasicFieldSection.CategoryNameInput}}" userInput="{{SimpleSubCategory.name}}" stepKey="addSubCategoryName"/> -+ <checkOption selector="{{AdminCategoryBasicFieldSection.EnableCategory}}" stepKey="enableCategory"/> -+ <checkOption selector="{{AdminCategoryBasicFieldSection.IncludeInMenu}}" stepKey="enableIncludeInMenu"/> -+ <click selector="{{AdminCategoryMainActionsSection.SaveButton}}" stepKey="saveSubCategory"/> -+ <waitForPageLoad stepKey="waitForSecondCategoryToSave"/> -+ <seeElement selector="{{AdminCategoryMessagesSection.SuccessMessage}}" stepKey="seeSuccessMessage"/> -+ <!-- Verify Parent Category and Sub category is not visible in navigation menu --> -+ <amOnPage url="$$createCategory.name_lwr$$/{{SimpleSubCategory.name_lwr}}.html" stepKey="openCategoryStoreFrontPage"/> -+ <waitForPageLoad stepKey="waitForCategoryStoreFrontPageToLoad"/> -+ <dontSeeElement selector="{{StorefrontHeaderSection.NavigationCategoryByName($$createCategory.name$$)}}" stepKey="dontSeeCategoryOnStoreNavigationBar"/> -+ <dontSeeElement selector="{{StorefrontHeaderSection.NavigationCategoryByName(SimpleSubCategory.name)}}" stepKey="dontSeeSubCategoryOnStoreNavigation"/> -+ </test> -+</tests> -diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCheckOutOfStockProductIsNotVisibleInCategoryTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCheckOutOfStockProductIsNotVisibleInCategoryTest.xml -new file mode 100644 -index 00000000000..ee8b48a94b2 ---- /dev/null -+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCheckOutOfStockProductIsNotVisibleInCategoryTest.xml -@@ -0,0 +1,75 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> -+ <test name="AdminCheckOutOfStockProductIsNotVisibleInCategoryTest"> -+ <annotations> -+ <stories value="Manage products"/> -+ <title value="Out of Stock Product is Not Visible in Category"/> -+ <description value="Login as admin and check out of stock product is not visible in category"/> -+ <severity value="CRITICAL"/> -+ <testCaseId value="MC-11064"/> -+ <group value="mtf_migrated"/> -+ </annotations> -+ <before> -+ <!-- Login as admin --> -+ <actionGroup ref="LoginAsAdmin" stepKey="login"/> -+ <!--Create Category--> -+ <createData entity="SimpleSubCategory" stepKey="createCategory"/> -+ <!--Create Simple Product--> -+ <createData entity="SimpleProduct" stepKey="createSimpleProduct"> -+ <requiredEntity createDataKey="createCategory"/> -+ </createData> -+ </before> -+ <after> -+ <!-- Delete created entity --> -+ <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> -+ <deleteData createDataKey="createSimpleProduct" stepKey="deleteSimpleProduct"/> -+ <actionGroup ref="logout" stepKey="logout"/> -+ </after> -+ <!--Open Product Index Page and filter the product--> -+ <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="navigateToProductIndex"/> -+ <waitForPageLoad stepKey="waitForProductIndexPageToLoad"/> -+ <actionGroup ref="filterProductGridBySku" stepKey="filterProduct"> -+ <argument name="product" value="SimpleProduct"/> -+ </actionGroup> -+ <!-- Update product Advanced Inventory Setting --> -+ <click stepKey="openSelectedProduct" selector="{{AdminProductGridSection.productRowBySku($$createSimpleProduct.sku$$)}}"/> -+ <waitForPageLoad stepKey="waitForProductToLoad"/> -+ <click selector="{{AdminProductFormSection.advancedInventoryLink}}" stepKey="clickOnAdvancedInventoryLink"/> -+ <waitForPageLoad stepKey="waitForAdvancedInventoryPageToLoad"/> -+ <uncheckOption selector="{{AdminProductFormAdvancedInventorySection.useConfigSettings}}" stepKey="uncheckConfigSetting"/> -+ <selectOption selector="{{AdminProductFormAdvancedInventorySection.manageStock}}" userInput="Yes" stepKey="clickOnManageStock"/> -+ <fillField selector="{{AdminProductFormAdvancedInventorySection.advancedInventoryQty}}" userInput="5" stepKey="fillProductQty"/> -+ <uncheckOption selector="{{AdminProductFormAdvancedInventorySection.miniQtyConfigSetting}}" stepKey="uncheckMiniQtyCheckBox"/> -+ <fillField selector="{{AdminProductFormAdvancedInventorySection.miniQtyAllowedInCart}}" userInput="1" stepKey="fillMiniAllowedQty"/> -+ <uncheckOption selector="{{AdminProductFormAdvancedInventorySection.maxiQtyConfigSetting}}" stepKey="uncheckMaxQtyCheckBox"/> -+ <fillField selector="{{AdminProductFormAdvancedInventorySection.maxiQtyAllowedInCart}}" userInput="10000" stepKey="fillMaxAllowedQty"/> -+ <selectOption selector="{{AdminProductFormAdvancedInventorySection.qtyUsesDecimals}}" userInput="Yes" stepKey="selectQuatityUsesDecimal"/> -+ <uncheckOption selector="{{AdminProductFormAdvancedInventorySection.notifyBelowQtyConfigSetting}}" stepKey="uncheckNotifyBelowQtyheckBox"/> -+ <fillField selector="{{AdminProductFormAdvancedInventorySection.notifyBelowQty}}" userInput="1" stepKey="fillNotifyBelowQty"/> -+ <selectOption selector="{{AdminProductFormAdvancedInventorySection.advancedInventoryStockStatus}}" userInput="Out of Stock" stepKey="selectOutOfStock"/> -+ <click selector="{{AdminProductFormAdvancedInventorySection.doneButton}}" stepKey="clickOnDoneButton"/> -+ <waitForPageLoad stepKey="waitForProductPageToSave"/> -+ <click selector="{{AdminProductFormActionSection.saveButton}}" stepKey="clickOnSaveButton"/> -+ <waitForLoadingMaskToDisappear stepKey="waitForLoading"/> -+ <see selector="{{AdminCategoryMessagesSection.SuccessMessage}}" userInput="You saved the product." stepKey="messageYouSavedTheProductIsShown"/> -+ <!--Verify product is not visible in category store front page --> -+ <amOnPage url="$$createCategory.name$$.html" stepKey="openCategoryStoreFrontPage"/> -+ <waitForPageLoad stepKey="waitForCategoryPageToLoad"/> -+ <seeElement selector="{{StorefrontHeaderSection.NavigationCategoryByName(SimpleSubCategory.name)}}" stepKey="seeCategoryInFrontPage"/> -+ <click selector="{{StorefrontHeaderSection.NavigationCategoryByName(SimpleSubCategory.name)}}" stepKey="clickOnCategory"/> -+ <dontSee selector="{{StorefrontCategoryMainSection.productName}}" userInput="{{SimpleProduct.name}}" stepKey="dontSeeProductInCategoryPage"/> -+ <!--Verify Product In Store Front--> -+ <amOnPage url="$$createSimpleProduct.name$$.html" stepKey="goToProductStorefrontPage"/> -+ <waitForPageLoad stepKey="waitForProductPageTobeLoaded"/> -+ <see selector="{{StorefrontProductInfoMainSection.productName}}" userInput="{{SimpleProduct.name}}" stepKey="seeProductNameInStoreFront"/> -+ <see selector="{{StorefrontProductInfoMainSection.productStockStatus}}" userInput="Out of stock" stepKey="seeProductStatusIsOutOfStock"/> -+ </test> -+</tests> -diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCheckOutOfStockProductIsVisibleInCategoryTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCheckOutOfStockProductIsVisibleInCategoryTest.xml -new file mode 100644 -index 00000000000..e1cb45be22b ---- /dev/null -+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCheckOutOfStockProductIsVisibleInCategoryTest.xml -@@ -0,0 +1,73 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> -+ <test name="AdminCheckOutOfStockProductIsVisibleInCategoryTest"> -+ <annotations> -+ <stories value="Manage products"/> -+ <title value="Out of Stock Product is Visible in Category"/> -+ <description value="Login as admin and check out of stock product is visible in category"/> -+ <severity value="CRITICAL"/> -+ <testCaseId value="MC-11067"/> -+ <group value="mtf_migrated"/> -+ </annotations> -+ <before> -+ <!--Set Display out of stock product--> -+ <magentoCLI stepKey="setDisplayOutOfStockProduct" command="config:set cataloginventory/options/show_out_of_stock 1" /> -+ <!-- Login as admin --> -+ <actionGroup ref="LoginAsAdmin" stepKey="login"/> -+ <!--Create Category--> -+ <createData entity="SimpleSubCategory" stepKey="createCategory"/> -+ <!--Create Simple Product--> -+ <createData entity="SimpleProduct" stepKey="createSimpleProduct"> -+ <requiredEntity createDataKey="createCategory"/> -+ </createData> -+ </before> -+ <after> -+ <!-- Delete created entity --> -+ <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> -+ <deleteData createDataKey="createSimpleProduct" stepKey="deleteSimpleProduct"/> -+ <actionGroup ref="logout" stepKey="logout"/> -+ <magentoCLI stepKey="setDisplayOutOfStockProduct" command="config:set cataloginventory/options/show_out_of_stock 0" /> -+ </after> -+ <!--Open Product Index Page and filter the product--> -+ <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="navigateToProductIndex"/> -+ <waitForPageLoad stepKey="waitForProductIndexPageToLoad"/> -+ <actionGroup ref="filterProductGridBySku" stepKey="filterProduct"> -+ <argument name="product" value="SimpleProduct"/> -+ </actionGroup> -+ <!-- Update product Advanced Inventory Setting --> -+ <click stepKey="openSelectedProduct" selector="{{AdminProductGridSection.productRowBySku($$createSimpleProduct.sku$$)}}"/> -+ <waitForPageLoad stepKey="waitForProductToLoad"/> -+ <click selector="{{AdminProductFormSection.advancedInventoryLink}}" stepKey="clickOnAdvancedInventoryLink"/> -+ <waitForPageLoad stepKey="waitForAdvancedInventoryPageToLoad"/> -+ <uncheckOption selector="{{AdminProductFormAdvancedInventorySection.useConfigSettings}}" stepKey="uncheckConfigSetting"/> -+ <selectOption selector="{{AdminProductFormAdvancedInventorySection.manageStock}}" userInput="Yes" stepKey="clickOnManageStock"/> -+ <fillField selector="{{AdminProductFormAdvancedInventorySection.advancedInventoryQty}}" userInput="5" stepKey="fillProductQty"/> -+ <uncheckOption selector="{{AdminProductFormAdvancedInventorySection.miniQtyConfigSetting}}" stepKey="uncheckMiniQtyCheckBox"/> -+ <fillField selector="{{AdminProductFormAdvancedInventorySection.miniQtyAllowedInCart}}" userInput="1" stepKey="fillMiniAllowedQty"/> -+ <uncheckOption selector="{{AdminProductFormAdvancedInventorySection.maxiQtyConfigSetting}}" stepKey="uncheckMaxQtyCheckBox"/> -+ <fillField selector="{{AdminProductFormAdvancedInventorySection.maxiQtyAllowedInCart}}" userInput="10000" stepKey="fillMaxAllowedQty"/> -+ <selectOption selector="{{AdminProductFormAdvancedInventorySection.qtyUsesDecimals}}" userInput="Yes" stepKey="selectQuantityUsesDecimal"/> -+ <uncheckOption selector="{{AdminProductFormAdvancedInventorySection.notifyBelowQtyConfigSetting}}" stepKey="uncheckNotifyBelowQtyCheckBox"/> -+ <fillField selector="{{AdminProductFormAdvancedInventorySection.notifyBelowQty}}" userInput="1" stepKey="fillNotifyBelowQty"/> -+ <selectOption selector="{{AdminProductFormAdvancedInventorySection.advancedInventoryStockStatus}}" userInput="Out of Stock" stepKey="selectOutOfStock"/> -+ <click stepKey="clickOnDoneButton" selector="{{AdminProductFormAdvancedInventorySection.doneButton}}"/> -+ <waitForPageLoad stepKey="waitForProductPageToLoad"/> -+ <click selector="{{AdminProductFormActionSection.saveButton}}" stepKey="clickOnSaveButton"/> -+ <waitForLoadingMaskToDisappear stepKey="waitForLoading"/> -+ <see selector="{{AdminCategoryMessagesSection.SuccessMessage}}" userInput="You saved the product." stepKey="messageYouSavedTheProductIsShown"/> -+ <!--Verify product is visible in category front page --> -+ <amOnPage url="$$createCategory.name$$.html" stepKey="openCategoryStoreFrontPage"/> -+ <waitForPageLoad stepKey="waitForCategoryPageToLoad"/> -+ <seeElement selector="{{StorefrontHeaderSection.NavigationCategoryByName(SimpleSubCategory.name)}}" stepKey="seeCategoryInFrontPage"/> -+ <click selector="{{StorefrontHeaderSection.NavigationCategoryByName(SimpleSubCategory.name)}}" stepKey="clickOnCategory"/> -+ <see selector="{{StorefrontCategoryMainSection.productName}}" userInput="{{SimpleProduct.name}}" stepKey="seeProductNameInCategoryPage"/> -+ </test> -+</tests> -diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCheckPaginationInStorefrontTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCheckPaginationInStorefrontTest.xml -new file mode 100644 -index 00000000000..f40a62c164e ---- /dev/null -+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCheckPaginationInStorefrontTest.xml -@@ -0,0 +1,176 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> -+ <test name="AdminCheckPaginationInStorefrontTest"> -+ <annotations> -+ <stories value="Create flat catalog product"/> -+ <title value="Verify that pagination works when Flat Category is enabled"/> -+ <description value="Login as admin, create flat catalog product and check pagination"/> -+ <testCaseId value="MC-6051"/> -+ <severity value="CRITICAL"/> -+ <group value="mtf_migrated"/> -+ <group value="Catalog"/> -+ </annotations> -+ <before> -+ <magentoCLI stepKey="setFlatCatalogCategory" command="config:set catalog/frontend/flat_catalog_category 1 "/> -+ <magentoCLI stepKey="setFlatCatalogProduct" command="config:set catalog/frontend/flat_catalog_product 1 "/> -+ <createData entity="_defaultCategory" stepKey="createDefaultCategory"/> -+ <createData entity="PaginationProduct" stepKey="simpleProduct1"/> -+ <createData entity="PaginationProduct" stepKey="simpleProduct2"/> -+ <createData entity="PaginationProduct" stepKey="simpleProduct3"/> -+ <createData entity="PaginationProduct" stepKey="simpleProduct4"/> -+ <createData entity="PaginationProduct" stepKey="simpleProduct5"/> -+ <createData entity="PaginationProduct" stepKey="simpleProduct6"/> -+ <createData entity="PaginationProduct" stepKey="simpleProduct7"/> -+ <createData entity="PaginationProduct" stepKey="simpleProduct8"/> -+ <createData entity="PaginationProduct" stepKey="simpleProduct9"/> -+ <createData entity="PaginationProduct" stepKey="simpleProduct10"/> -+ <createData entity="PaginationProduct" stepKey="simpleProduct11"/> -+ <createData entity="PaginationProduct" stepKey="simpleProduct12"/> -+ <createData entity="PaginationProduct" stepKey="simpleProduct13"/> -+ <createData entity="PaginationProduct" stepKey="simpleProduct14"/> -+ <createData entity="PaginationProduct" stepKey="simpleProduct15"/> -+ <createData entity="PaginationProduct" stepKey="simpleProduct16"/> -+ <createData entity="PaginationProduct" stepKey="simpleProduct17"/> -+ <createData entity="PaginationProduct" stepKey="simpleProduct18"/> -+ <createData entity="PaginationProduct" stepKey="simpleProduct19"/> -+ <createData entity="PaginationProduct" stepKey="simpleProduct20"/> -+ <actionGroup ref="LoginAsAdmin" stepKey="loginToAdminPanel"/> -+ </before> -+ <after> -+ <magentoCLI stepKey="setFlatCatalogCategory" command="config:set catalog/frontend/flat_catalog_category 0" /> -+ <magentoCLI stepKey="setFlatCatalogProduct" command="config:set catalog/frontend/flat_catalog_product 0" /> -+ <deleteData createDataKey="createDefaultCategory" stepKey="deleteCategory"/> -+ <deleteData createDataKey="simpleProduct1" stepKey="deleteSimpleProduct1"/> -+ <deleteData createDataKey="simpleProduct2" stepKey="deleteSimpleProduct2"/> -+ <deleteData createDataKey="simpleProduct3" stepKey="deleteSimpleProduct3"/> -+ <deleteData createDataKey="simpleProduct4" stepKey="deleteSimpleProduct4"/> -+ <deleteData createDataKey="simpleProduct5" stepKey="deleteSimpleProduct5"/> -+ <deleteData createDataKey="simpleProduct6" stepKey="deleteSimpleProduct6"/> -+ <deleteData createDataKey="simpleProduct7" stepKey="deleteSimpleProduct7"/> -+ <deleteData createDataKey="simpleProduct8" stepKey="deleteSimpleProduct8"/> -+ <deleteData createDataKey="simpleProduct9" stepKey="deleteSimpleProduct9"/> -+ <deleteData createDataKey="simpleProduct10" stepKey="deleteSimpleProduct10"/> -+ <deleteData createDataKey="simpleProduct11" stepKey="deleteSimpleProduct11"/> -+ <deleteData createDataKey="simpleProduct12" stepKey="deleteSimpleProduct12"/> -+ <deleteData createDataKey="simpleProduct13" stepKey="deleteSimpleProduct13"/> -+ <deleteData createDataKey="simpleProduct14" stepKey="deleteSimpleProduct14"/> -+ <deleteData createDataKey="simpleProduct15" stepKey="deleteSimpleProduct15"/> -+ <deleteData createDataKey="simpleProduct16" stepKey="deleteSimpleProduct16"/> -+ <deleteData createDataKey="simpleProduct17" stepKey="deleteSimpleProduct17"/> -+ <deleteData createDataKey="simpleProduct18" stepKey="deleteSimpleProduct18"/> -+ <deleteData createDataKey="simpleProduct19" stepKey="deleteSimpleProduct19"/> -+ <deleteData createDataKey="simpleProduct20" stepKey="deleteSimpleProduct20"/> -+ <actionGroup ref="logout" stepKey="logout"/> -+ </after> -+ -+ <!--Open Category Page and select created category--> -+ <amOnPage url="{{AdminCategoryPage.url}}" stepKey="openAdminCategoryIndexPage"/> -+ <waitForPageLoad stepKey="waitForPageToLoad1"/> -+ <click selector="{{AdminCategorySidebarTreeSection.expandAll}}" stepKey="clickOnExpandTree"/> -+ <waitForPageLoad stepKey="waitForPageToLoad0"/> -+ <click selector="{{AdminCategorySidebarTreeSection.categoryInTree(_defaultCategory.name)}}" stepKey="selectCreatedCategory"/> -+ <waitForPageLoad stepKey="waitForPageToLoaded2"/> -+ -+ <!--Select Products--> -+ <scrollTo selector="{{AdminCategoryBasicFieldSection.productsInCategory}}" x="0" y="-80" stepKey="scrollToProductInCategory"/> -+ <click selector="{{AdminCategoryBasicFieldSection.productsInCategory}}" stepKey="clickOnProductInCategory"/> -+ <waitForPageLoad stepKey="waitForProductsToLoad"/> -+ <scrollTo selector="{{CatalogProductsSection.resetFilter}}" stepKey="scrollToResetFilter"/> -+ <waitForElementVisible selector="{{CatalogProductsSection.resetFilter}}" time="30" stepKey="waitForResetButtonToVisible"/> -+ <click selector="{{CatalogProductsSection.resetFilter}}" stepKey="clickOnResetFilter"/> -+ <waitForPageLoad stepKey="waitForPageToLoad3"/> -+ <selectOption selector="{{AdminProductGridFilterSection.productPerPage}}" userInput="20" stepKey="selectPagePerView"/> -+ <fillField selector="{{AdminCategoryContentSection.productTableColumnName}}" userInput="pagi" stepKey="selectProduct1"/> -+ <click selector="{{AdminCategoryContentSection.productSearch}}" stepKey="clickSearchButton"/> -+ <waitForPageLoad stepKey="waitFroPageToLoad1"/> -+ <see selector="{{AdminProductGridFilterSection.productCount}}" userInput="20" stepKey="seeNumberOfProductsFound"/> -+ <click selector="{{AdminCategoryProductsGridSection.productSelectAll}}" stepKey="selectSelectAll"/> -+ <click selector="{{AdminCategoryMainActionsSection.SaveButton}}" stepKey="clickSaveButton"/> -+ <waitForPageLoad stepKey="waitForCategorySaved"/> -+ <see selector="{{AdminCategoryMessagesSection.SuccessMessage}}" userInput="You saved the category." stepKey="assertSuccessMessage"/> -+ <waitForPageLoad stepKey="waitForPageTitleToBeSaved"/> -+ -+ <!--Open Category Store Front Page--> -+ <amOnPage url="{{_defaultCategory.name}}.html" stepKey="goToStorefront"/> -+ <waitForPageLoad stepKey="waitForCategoryPageToLoad"/> -+ <seeElement selector="{{StorefrontHeaderSection.NavigationCategoryByName(_defaultCategory.name)}}" stepKey="seeCategoryOnNavigation"/> -+ <click selector="{{StorefrontHeaderSection.NavigationCategoryByName(_defaultCategory.name)}}" stepKey="selectCategory"/> -+ <waitForPageLoad stepKey="waitForProductToLoad"/> -+ -+ <!--Select 9 items per page and verify number of products displayed in each page --> -+ <conditionalClick selector="{{StorefrontCategoryTopToolbarSection.gridMode}}" visible="true" dependentSelector="{{StorefrontCategoryTopToolbarSection.gridMode}}" stepKey="seeProductGridIsActive"/> -+ <scrollTo selector="{{StorefrontCategoryBottomToolbarSection.perPage}}" stepKey="scrollToBottomToolbarSection"/> -+ <selectOption selector="{{StorefrontCategoryBottomToolbarSection.perPage}}" userInput="9" stepKey="selectPerPageOption"/> -+ -+ <!--Verify number of products displayed in First Page --> -+ <seeNumberOfElements selector="{{StorefrontCategoryMainSection.productLink}}" userInput="9" stepKey="seeNumberOfProductsInFirstPage"/> -+ -+ <!--Verify number of products displayed in Second Page --> -+ <scrollTo selector="{{StorefrontCategoryBottomToolbarSection.nextPage}}" stepKey="scrollToNextButton"/> -+ <click selector="{{StorefrontCategoryBottomToolbarSection.nextPage}}" stepKey="clickOnNextPage"/> -+ <waitForPageLoad stepKey="waitForPageToLoad4"/> -+ <seeNumberOfElements selector="{{StorefrontCategoryMainSection.productLink}}" userInput="9" stepKey="seeNumberOfProductsInSecondPage"/> -+ -+ <!--Verify number of products displayed in third Page --> -+ <scrollTo selector="{{StorefrontCategoryBottomToolbarSection.nextPage}}" stepKey="scrollToNextButton1"/> -+ <click selector="{{StorefrontCategoryBottomToolbarSection.nextPage}}" stepKey="clickOnNextPage1"/> -+ <waitForPageLoad stepKey="waitForPageToLoad2"/> -+ <seeNumberOfElements selector="{{StorefrontCategoryMainSection.productLink}}" userInput="2" stepKey="seeNumberOfProductsInThirdPage"/> -+ -+ <!--Change Pages using Previous Page selector and verify number of products displayed in each page--> -+ <scrollTo selector="{{StorefrontCategoryBottomToolbarSection.previousPage}}" stepKey="scrollToPreviousPage"/> -+ <click selector="{{StorefrontCategoryBottomToolbarSection.previousPage}}" stepKey="clickOnPreviousPage1"/> -+ <waitForPageLoad stepKey="waitForPageToLoad5"/> -+ <seeNumberOfElements selector="{{StorefrontCategoryMainSection.productLink}}" userInput="9" stepKey="seeNumberOfProductsInSecondPage1"/> -+ <scrollTo selector="{{StorefrontCategoryBottomToolbarSection.previousPage}}" stepKey="scrollToPreviousPage1"/> -+ <click selector="{{StorefrontCategoryBottomToolbarSection.previousPage}}" stepKey="clickOnPreviousPage2"/> -+ <waitForPageLoad stepKey="waitForPageToLoad6"/> -+ <seeNumberOfElements selector="{{StorefrontCategoryMainSection.productLink}}" userInput="9" stepKey="seeNumberOfProductsInFirstPage1"/> -+ -+ <!--Select Pages by using page Number and verify number of products displayed--> -+ <scrollTo selector="{{StorefrontCategoryBottomToolbarSection.nextPage}}" stepKey="scrollToPreviousPage2"/> -+ <click selector="{{StorefrontCategoryBottomToolbarSection.pageNumber('2')}}" stepKey="clickOnPage2"/> -+ <waitForPageLoad stepKey="waitForPageToLoad7"/> -+ <seeNumberOfElements selector="{{StorefrontCategoryMainSection.productLink}}" userInput="9" stepKey="seeNumberOfProductsInSecondPage2"/> -+ -+ <!--Select Third Page using page number--> -+ <scrollTo selector="{{StorefrontCategoryBottomToolbarSection.nextPage}}" stepKey="scrollToPreviousPage3"/> -+ <click selector="{{StorefrontCategoryBottomToolbarSection.pageNumber('3')}}" stepKey="clickOnThirdPage"/> -+ <waitForPageLoad stepKey="waitForPageToLoad8"/> -+ <seeNumberOfElements selector="{{StorefrontCategoryMainSection.productLink}}" userInput="2" stepKey="seeNumberOfProductsInThirdPage2"/> -+ -+ <!--Select First Page using page number--> -+ <scrollTo selector="{{StorefrontCategoryBottomToolbarSection.previousPage}}" stepKey="scrollToPreviousPage4"/> -+ <click selector="{{StorefrontCategoryBottomToolbarSection.pageNumber('1')}}" stepKey="clickOnFirstPage"/> -+ <waitForPageLoad stepKey="waitForPageToLoad9"/> -+ <seeNumberOfElements selector="{{StorefrontCategoryMainSection.productLink}}" userInput="9" stepKey="seeNumberOfProductsFirstPage2"/> -+ -+ <!--Select 15 items per page and verify number of products displayed in each page --> -+ <scrollTo selector="{{StorefrontCategoryBottomToolbarSection.perPage}}" stepKey="scrollToPerPage"/> -+ <selectOption selector="{{StorefrontCategoryBottomToolbarSection.perPage}}" userInput="15" stepKey="selectPerPageOption1"/> -+ <waitForPageLoad stepKey="waitForPageToLoad10"/> -+ <seeNumberOfElements selector="{{StorefrontCategoryMainSection.productLink}}" userInput="15" stepKey="seeNumberOfProductsInFirstPage3"/> -+ <scrollTo selector="{{StorefrontCategoryBottomToolbarSection.nextPage}}" stepKey="scrollToNextButton2"/> -+ <click selector="{{StorefrontCategoryBottomToolbarSection.nextPage}}" stepKey="clickOnNextPage2"/> -+ <waitForPageLoad stepKey="waitForPageToLoad11"/> -+ <seeNumberOfElements selector="{{StorefrontCategoryMainSection.productLink}}" userInput="5" stepKey="seeNumberOfProductsInSecondPage3"/> -+ -+ <!--Select First Page using page number--> -+ <scrollTo selector="{{StorefrontCategoryBottomToolbarSection.pageNumber('1')}}" stepKey="scrollToPreviousPage5"/> -+ <click selector="{{StorefrontCategoryBottomToolbarSection.pageNumber('1')}}" stepKey="clickOnFirstPage2"/> -+ <waitForPageLoad stepKey="waitForPageToLoad13"/> -+ -+ <!--Select 30 items per page and verify number of products displayed in each page --> -+ <scrollTo selector="{{StorefrontCategoryBottomToolbarSection.perPage}}" stepKey="scrollToPerPage4"/> -+ <selectOption selector="{{StorefrontCategoryBottomToolbarSection.perPage}}" userInput="30" stepKey="selectPerPageOption2"/> -+ <waitForPageLoad stepKey="waitForPageToLoad12"/> -+ <seeNumberOfElements selector="{{StorefrontCategoryMainSection.productLink}}" userInput="20" stepKey="seeNumberOfProductsInFirstPage4"/> -+ </test> -+</tests> -diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCheckSubCategoryIsNotVisibleInNavigationMenuTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCheckSubCategoryIsNotVisibleInNavigationMenuTest.xml -new file mode 100644 -index 00000000000..f5872ac3efc ---- /dev/null -+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCheckSubCategoryIsNotVisibleInNavigationMenuTest.xml -@@ -0,0 +1,52 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> -+ <test name="AdminCheckSubCategoryIsNotVisibleInNavigationMenuTest"> -+ <annotations> -+ <stories value="Create category"/> -+ <title value="Active category is visible on navigation menu while subcategory is not visible on navigation menu, Include in Menu = Yes"/> -+ <description value="Login as admin and verify subcategory is not visible in navigation menu"/> -+ <severity value="CRITICAL"/> -+ <testCaseId value="MC-13635"/> -+ <group value="mtf_migrated"/> -+ </annotations> -+ <before> -+ <!--Login as Admin --> -+ <actionGroup ref="LoginAsAdmin" stepKey="loginToAdminPanel"/> -+ <!--Create Parent Category --> -+ <createData entity="_defaultCategory" stepKey="createCategory"/> -+ </before> -+ <after> -+ <!--Delete created data--> -+ <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> -+ <actionGroup ref="logout" stepKey="logout"/> -+ </after> -+ <!--Open Category Page--> -+ <amOnPage url="{{AdminCategoryPage.url}}" stepKey="openAdminCategoryIndexPage"/> -+ <waitForPageLoad stepKey="waitForPageToLoaded"/> -+ <!--Create subcategory under parent category --> -+ <click selector="{{AdminCategorySidebarTreeSection.expandAll}}" stepKey="clickOnExpandTree"/> -+ <waitForPageLoad stepKey="waitForCategoryToLoad"/> -+ <click selector="{{AdminCategorySidebarTreeSection.categoryInTree($$createCategory.name$$)}}" stepKey="selectCategory"/> -+ <waitForPageLoad stepKey="waitForPageToLoad"/> -+ <click selector="{{AdminCategorySidebarActionSection.AddSubcategoryButton}}" stepKey="clickOnAddSubCategoryButton"/> -+ <fillField selector="{{AdminCategoryBasicFieldSection.CategoryNameInput}}" userInput="{{SimpleSubCategory.name}}" stepKey="addSubCategoryName"/> -+ <checkOption selector="{{AdminCategoryBasicFieldSection.EnableCategory}}" stepKey="enableCategory"/> -+ <checkOption selector="{{AdminCategoryBasicFieldSection.IncludeInMenu}}" stepKey="enableIncludeInMenu"/> -+ <click selector="{{AdminCategoryMainActionsSection.SaveButton}}" stepKey="saveSubCategory"/> -+ <waitForPageLoad stepKey="waitForSecondCategoryToSave"/> -+ <seeElement selector="{{AdminCategoryMessagesSection.SuccessMessage}}" stepKey="seeSuccessMessage"/> -+ <!-- Verify Parent Category is visible in navigation menu and Sub category is not visible in navigation menu --> -+ <amOnPage url="$$createCategory.name_lwr$$/{{SimpleSubCategory.name_lwr}}.html" stepKey="openCategoryStoreFrontPage"/> -+ <waitForPageLoad stepKey="waitForCategoryStoreFrontPageToLoad"/> -+ <seeElement selector="{{StorefrontHeaderSection.NavigationCategoryByName($$createCategory.name$$)}}" stepKey="seeCategoryOnStoreNavigationBar"/> -+ <dontSeeElement selector="{{StorefrontHeaderSection.NavigationCategoryByName(SimpleSubCategory.name)}}" stepKey="dontSeeSubCategoryOnStoreNavigation"/> -+ </test> -+</tests> -diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCloneProductWithDuplicateUrlTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCloneProductWithDuplicateUrlTest.xml -new file mode 100644 -index 00000000000..295351b00bf ---- /dev/null -+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCloneProductWithDuplicateUrlTest.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="AdminCloneProductWithDuplicateUrlTest"> -+ <annotations> -+ <features value="Catalog"/> -+ <title value="Cloning a product with duplicate URL key"/> -+ <description value="Check product cloning with duplicate URL key"/> -+ <severity value="AVERAGE"/> -+ <testCaseId value="MAGETWO-98992"/> -+ <useCaseId value="MAGETWO-98708"/> -+ <group value="catalog"/> -+ </annotations> -+ <before> -+ <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> -+ <!--Create category and product--> -+ <comment userInput="Create category and product" stepKey="commentCreateCategoryAndProduct"/> -+ <createData entity="SimpleProduct2" stepKey="createSimpleProduct"/> -+ </before> -+ <after> -+ <!--Delete created data--> -+ <comment userInput="Delete created data" stepKey="commentDeleteCreatedData"/> -+ <actionGroup ref="deleteAllDuplicateProductUsingProductGrid" stepKey="deleteAllDuplicateProducts"> -+ <argument name="product" value="$$createSimpleProduct$$"/> -+ </actionGroup> -+ <actionGroup ref="resetProductGridToDefaultView" stepKey="resetFiltersIfExist"/> -+ <actionGroup ref="logout" stepKey="logoutOfAdmin"/> -+ </after> -+ <amOnPage url="{{AdminProductEditPage.url($$createSimpleProduct.id$$)}}" stepKey="goToProductEditPage"/> -+ <waitForPageLoad stepKey="waitForSimpleProductPageLoad"/> -+ <!--Save and duplicated the product once--> -+ <comment userInput="Save and duplicated the product once" stepKey="commentSaveAndDuplicateProduct"/> -+ <actionGroup ref="AdminFormSaveAndDuplicate" stepKey="saveAndDuplicateProductFormFirstTime"/> -+ <conditionalClick selector="{{AdminProductSEOSection.sectionHeader}}" dependentSelector="{{AdminProductSEOSection.urlKeyInput}}" visible="false" stepKey="openSEOSection"/> -+ <grabValueFrom selector="{{AdminProductSEOSection.urlKeyInput}}" stepKey="grabDuplicatedProductUrlKey"/> -+ <assertContains expected="$$createSimpleProduct.custom_attributes[url_key]$$" actual="$grabDuplicatedProductUrlKey" stepKey="assertDuplicatedProductUrlKey"/> -+ <assertContains expectedType="string" expected="-1" actual="$grabDuplicatedProductUrlKey" stepKey="assertDuplicatedProductUrlKey1"/> -+ <!--Add duplicated product to the simple product--> -+ <comment userInput="Add duplicated product to the simple product" stepKey="commentAddProduct"/> -+ <amOnPage url="{{AdminProductEditPage.url($$createSimpleProduct.id$$)}}" stepKey="goToSimpleProductPage"/> -+ <waitForPageLoad stepKey="waitForSimpleProductPageLoad1"/> -+ <actionGroup ref="addCrossSellProductBySku" stepKey="addCrossSellProduct"> -+ <argument name="sku" value="$$createSimpleProduct.sku$$"/> -+ </actionGroup> -+ <actionGroup ref="addRelatedProductBySku" stepKey="addRelatedProduct"> -+ <argument name="sku" value="$$createSimpleProduct.sku$$"/> -+ </actionGroup> -+ <actionGroup ref="addUpSellProductBySku" stepKey="addUpSellProduct"> -+ <argument name="sku" value="$$createSimpleProduct.sku$$"/> -+ </actionGroup> -+ <actionGroup ref="saveProductForm" stepKey="clickSaveProduct"/> -+ <conditionalClick selector="{{AdminProductFormRelatedUpSellCrossSellSection.sectionHeader}}" dependentSelector="{{AdminProductFormRelatedUpSellCrossSellSection.AddRelatedProductsButton}}" visible="false" stepKey="openProductRUSSection"/> -+ <see selector="{{AdminProductFormRelatedUpSellCrossSellSection.selectedProductSku('related')}}" userInput="$$createSimpleProduct.sku$$-1" stepKey="seeRelatedProduct"/> -+ <see selector="{{AdminProductFormRelatedUpSellCrossSellSection.selectedProductSku('upsell')}}" userInput="$$createSimpleProduct.sku$$-1" stepKey="seeUpSellProduct"/> -+ <see selector="{{AdminProductFormRelatedUpSellCrossSellSection.selectedProductSku('crosssell')}}" userInput="$$createSimpleProduct.sku$$-1" stepKey="seeCrossSellProduct"/> -+ <!--Save and duplicated the product second time--> -+ <comment userInput="Save and duplicated the product second time" stepKey="commentSaveAndDuplicateProduct1"/> -+ <amOnPage url="{{AdminProductEditPage.url($$createSimpleProduct.id$$)}}" stepKey="goToProductEditPage1"/> -+ <waitForPageLoad stepKey="waitForSimpleProductPageLoad2"/> -+ <actionGroup ref="AdminFormSaveAndDuplicate" stepKey="saveAndDuplicateProductFormSecondTime"/> -+ <conditionalClick selector="{{AdminProductSEOSection.sectionHeader}}" dependentSelector="{{AdminProductSEOSection.urlKeyInput}}" visible="false" stepKey="openProductSEOSection"/> -+ <waitForElementVisible selector="{{AdminProductSEOSection.urlKeyInput}}" stepKey="waitForUrlKeyField"/> -+ <grabValueFrom selector="{{AdminProductSEOSection.urlKeyInput}}" stepKey="grabSecondDuplicatedProductUrlKey"/> -+ <assertContains expected="$$createSimpleProduct.custom_attributes[url_key]$$" actual="$grabSecondDuplicatedProductUrlKey" stepKey="assertSecondDuplicatedProductUrlKey"/> -+ <assertContains expectedType="string" expected="-2" actual="$grabSecondDuplicatedProductUrlKey" stepKey="assertSecondDuplicatedProductUrlKey1"/> -+ <conditionalClick selector="{{AdminProductFormRelatedUpSellCrossSellSection.sectionHeader}}" dependentSelector="{{AdminProductFormRelatedUpSellCrossSellSection.AddRelatedProductsButton}}" visible="false" stepKey="openProductRUSSection1"/> -+ <waitForElementVisible selector="{{AdminProductFormRelatedUpSellCrossSellSection.selectedProductSku('related')}}" stepKey="waitForSelectedProductSku"/> -+ <see selector="{{AdminProductFormRelatedUpSellCrossSellSection.selectedProductSku('related')}}" userInput="$$createSimpleProduct.sku$$-1" stepKey="seeRelatedProductForDuplicated"/> -+ <see selector="{{AdminProductFormRelatedUpSellCrossSellSection.selectedProductSku('upsell')}}" userInput="$$createSimpleProduct.sku$$-1" stepKey="seeUpSellProductForDuplicated"/> -+ <see selector="{{AdminProductFormRelatedUpSellCrossSellSection.selectedProductSku('crosssell')}}" userInput="$$createSimpleProduct.sku$$-1" stepKey="seeCrossSellProductForDuplicated"/> -+ </test> -+</tests> -diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminConfigureProductImagePlaceholderTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminConfigureProductImagePlaceholderTest.xml -new file mode 100644 -index 00000000000..4d97dee56f0 ---- /dev/null -+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminConfigureProductImagePlaceholderTest.xml -@@ -0,0 +1,145 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> -+ <test name="AdminConfigureProductImagePlaceholderTest"> -+ -+ <annotations> -+ <features value="Configuration"/> -+ <stories value="Configure product placeholder images"/> -+ <title value="Admin is able to configure product placeholder images"/> -+ <description value="Admin should be able to configure the images used for product image placeholders. The configured placeholders should be seen on the frontend when an image has no image."/> -+ <severity value="MAJOR"/> -+ <testCaseId value="MC-5005"/> -+ <group value="configuration"/> -+ </annotations> -+ -+ <before> -+ <createData entity="ApiCategory" stepKey="category"/> -+ <!--Create product with no images--> -+ <createData entity="ApiSimpleProduct" stepKey="productNoImages"> -+ <requiredEntity createDataKey="category"/> -+ </createData> -+ <!--Create product with small, base, and thumbnail image--> -+ <createData entity="ApiSimpleProduct" stepKey="productWithImages"> -+ <requiredEntity createDataKey="category"/> -+ </createData> -+ <createData entity="ApiProductAttributeMediaGalleryEntryTestImage" stepKey="productImage"> -+ <requiredEntity createDataKey="productWithImages"/> -+ </createData> -+ </before> -+ -+ <after> -+ <!--Unset product image placeholders--> -+ <amOnPage url="{{CatalogConfigPage.url}}" stepKey="goToCatalogConfigurationPageAfter"/> -+ <waitForPageLoad stepKey="waitForConfigurationPageLoadAfter"/> -+ <conditionalClick selector="{{AdminProductImagePlaceholderConfigSection.sectionHeader}}" dependentSelector="{{AdminProductImagePlaceholderConfigSection.baseImageInput}}" visible="false" stepKey="openPlaceholderSectionAfter"/> -+ <waitForElementVisible selector="{{AdminProductImagePlaceholderConfigSection.baseImageInput}}" stepKey="waitForPlaceholderSectionOpenAfter"/> -+ <!--Delete base placeholder--> -+ <checkOption selector="{{AdminProductImagePlaceholderConfigSection.baseImageDelete}}" stepKey="checkDeleteBasePlaceholder"/> -+ <!--Delete small placeholder--> -+ <checkOption selector="{{AdminProductImagePlaceholderConfigSection.smallImageDelete}}" stepKey="checkDeleteSmallPlaceholder"/> -+ <!--Delete thumbnail placeholder--> -+ <checkOption selector="{{AdminProductImagePlaceholderConfigSection.thumbnailImageDelete}}" stepKey="checkDeleteThumbnailPlaceholder"/> -+ <!--Save config to delete placeholders--> -+ <click selector="{{AdminMainActionsSection.save}}" stepKey="saveConfigWithPlaceholders"/> -+ <!--See placeholders are empty--> -+ <conditionalClick selector="{{AdminProductImagePlaceholderConfigSection.sectionHeader}}" dependentSelector="{{AdminProductImagePlaceholderConfigSection.baseImageInput}}" visible="false" stepKey="openPlaceholderSection2"/> -+ <waitForElementVisible selector="{{AdminProductImagePlaceholderConfigSection.baseImageInput}}" stepKey="waitForPlaceholderSectionOpen2"/> -+ <dontSeeElement selector="{{AdminProductImagePlaceholderConfigSection.baseImage}}" stepKey="dontSeeBaseImageSet"/> -+ <dontSeeElement selector="{{AdminProductImagePlaceholderConfigSection.smallImage}}" stepKey="dontSeeSmallImageSet"/> -+ <dontSeeElement selector="{{AdminProductImagePlaceholderConfigSection.thumbnailImage}}" stepKey="dontSeeThumbnailImageSet"/> -+ <dontSeeElement selector="{{AdminProductImagePlaceholderConfigSection.swatchImage}}" stepKey="dontSeeSwatchImageSet"/> -+ -+ <!--Delete prerequisite entities--> -+ <deleteData createDataKey="category" stepKey="deleteCategory"/> -+ <deleteData createDataKey="productNoImages" stepKey="deleteProductNoImages"/> -+ <deleteData createDataKey="productWithImages" stepKey="deleteProductWithImages"/> -+ </after> -+ -+ <actionGroup ref="LoginAsAdmin" stepKey="loginToAdminArea"/> -+ -+ <!--Admin area: configure Product Image Placeholders--> -+ <comment userInput="Configure product image placeholders in store config" stepKey="configurePlaceholderComment"/> -+ <amOnPage url="{{CatalogConfigPage.url}}" stepKey="goToCatalogConfigurationPage"/> -+ <waitForPageLoad stepKey="waitForConfigurationPageLoad1"/> -+ <conditionalClick selector="{{AdminProductImagePlaceholderConfigSection.sectionHeader}}" dependentSelector="{{AdminProductImagePlaceholderConfigSection.baseImageInput}}" visible="false" stepKey="openPlaceholderSection1"/> -+ <waitForElementVisible selector="{{AdminProductImagePlaceholderConfigSection.baseImageInput}}" stepKey="waitForPlaceholderSectionOpen1"/> -+ <!--Set base placeholder--> -+ <attachFile selector="{{AdminProductImagePlaceholderConfigSection.baseImageInput}}" userInput="{{placeholderBaseImage.file}}" stepKey="uploadBasePlaceholder"/> -+ <!--Set small placeholder--> -+ <attachFile selector="{{AdminProductImagePlaceholderConfigSection.smallImageInput}}" userInput="{{placeholderSmallImage.file}}" stepKey="uploadSmallPlaceholder"/> -+ <!--Set thumbnail placeholder--> -+ <attachFile selector="{{AdminProductImagePlaceholderConfigSection.thumbnailImageInput}}" userInput="{{placeholderThumbnailImage.file}}" stepKey="uploadThumbnailPlaceholder"/> -+ <!--Save config with placeholders--> -+ <click selector="{{AdminMainActionsSection.save}}" stepKey="saveConfigWithPlaceholders"/> -+ <!--See images are saved--> -+ <conditionalClick selector="{{AdminProductImagePlaceholderConfigSection.sectionHeader}}" dependentSelector="{{AdminProductImagePlaceholderConfigSection.baseImageInput}}" visible="false" stepKey="openPlaceholderSection2"/> -+ <waitForElementVisible selector="{{AdminProductImagePlaceholderConfigSection.baseImageInput}}" stepKey="waitForPlaceholderSectionOpen2"/> -+ <seeElement selector="{{AdminProductImagePlaceholderConfigSection.baseImageBySrc(placeholderBaseImage.name)}}" stepKey="seeBasePlaceholderSet"/> -+ <seeElement selector="{{AdminProductImagePlaceholderConfigSection.smallImageBySrc(placeholderSmallImage.name)}}" stepKey="seeSmallPlaceholderSet"/> -+ <seeElement selector="{{AdminProductImagePlaceholderConfigSection.thumbnailImageBySrc(placeholderThumbnailImage.name)}}" stepKey="seeThumbnailPlaceholderSet"/> -+ <dontSeeElement selector="{{AdminProductImagePlaceholderConfigSection.swatchImage}}" stepKey="dontSeeSwatchImageSet"/> -+ -+ <!--See correct placeholder images on category page--> -+ <comment userInput="Check placeholder images on the storefront" stepKey="checkStorefrontComment"/> -+ <amOnPage url="$$category.name$$.html" stepKey="goToCategoryStorefront1"/> -+ <waitForPageLoad stepKey="waitForStorefrontCategory1"/> -+ <!--Product with no images uses placeholder--> -+ <seeElement selector="{{StorefrontCategoryProductSection.ProductImageByName($$productNoImages.name$$)}}" stepKey="seeProductNoImagesInCategory"/> -+ <grabAttributeFrom selector="{{StorefrontCategoryProductSection.ProductImageByName($$productNoImages.name$$)}}" userInput="src" stepKey="getSmallPlaceholderImageSrc"/> -+ <assertContains stepKey="checkSmallPlaceholderImage"> -+ <actualResult type="variable">$getSmallPlaceholderImageSrc</actualResult> -+ <expectedResult type="string">{{placeholderSmallImage.name}}</expectedResult> -+ </assertContains> -+ <!--Product with images does not use placeholder--> -+ <seeElement selector="{{StorefrontCategoryProductSection.ProductTitleByName($$productWithImages.name$$)}}" stepKey="seeProductWithImagesInCategory"/> -+ <grabAttributeFrom selector="{{StorefrontCategoryProductSection.ProductImageByName($$productWithImages.name$$)}}" userInput="src" stepKey="getSmallNonPlaceholderImageSrc"/> -+ <assertNotContains stepKey="checkSmallPlaceholderImageNotUsed"> -+ <actualResult type="variable">$getSmallNonPlaceholderImageSrc</actualResult> -+ <expectedResult type="string">{{placeholderSmallImage.name}}</expectedResult> -+ </assertNotContains> -+ -+ <!--Check base image on product page--> -+ <!--Product which is using placeholder--> -+ <click selector="{{StorefrontCategoryProductSection.ProductImageByName($$productNoImages.name$$)}}" stepKey="goToProductNoImages"/> -+ <waitForPageLoad stepKey="waitForProductPageLoad1"/> -+ <seeInCurrentUrl url="$$productNoImages.sku$$" stepKey="seeCorrectProductPage1"/> -+ <seeElement selector="{{StorefrontProductMediaSection.imageFile(placeholderBaseImage.name)}}" stepKey="seeBasePlaceholderImage"/> -+ <click selector="{{StorefrontProductPageSection.addToCartBtn}}" stepKey="addProductToCart1"/> -+ <waitForElementVisible selector="{{StorefrontProductPageSection.successMsg}}" stepKey="waitForProductAdded1"/> -+ <click selector="{{StorefrontMinicartSection.showCart}}" stepKey="openMiniCart1"/> -+ <grabAttributeFrom selector="{{StorefrontMinicartSection.productImageByName($$productNoImages.name$$)}}" userInput="src" stepKey="getThumbnailPlaceholderImageSrc"/> -+ <assertContains stepKey="checkThumbnailPlaceholderImage"> -+ <actualResult type="variable">$getThumbnailPlaceholderImageSrc</actualResult> -+ <expectedResult type="string">{{placeholderThumbnailImage.name}}</expectedResult> -+ </assertContains> -+ <actionGroup ref="removeProductFromMiniCart" stepKey="removeProductFromCart1"> -+ <argument name="productName" value="$$productNoImages.name$$"/> -+ </actionGroup> -+ <!--Product which is NOT using placeholder--> -+ <amOnPage url="$$category.name$$.html" stepKey="goToCategoryStorefront2"/> -+ <waitForPageLoad stepKey="waitForStorefrontCategory2"/> -+ <click selector="{{StorefrontCategoryProductSection.ProductImageByName($$productWithImages.name$$)}}" stepKey="goToProductWithImages"/> -+ <waitForPageLoad stepKey="waitForProductPageLoad2"/> -+ <seeInCurrentUrl url="$$productWithImages.sku$$" stepKey="seeCorrectProductPage2"/> -+ <dontSeeElement selector="{{StorefrontProductMediaSection.imageFile(placeholderBaseImage.name)}}" stepKey="dontSeeBasePlaceholderImage"/> -+ <click selector="{{StorefrontProductPageSection.addToCartBtn}}" stepKey="addProductToCart2"/> -+ <waitForElementVisible selector="{{StorefrontProductPageSection.successMsg}}" stepKey="waitForProductAdded2"/> -+ <click selector="{{StorefrontMinicartSection.showCart}}" stepKey="openMiniCart2"/> -+ <grabAttributeFrom selector="{{StorefrontMinicartSection.productImageByName($$productWithImages.name$$)}}" userInput="src" stepKey="getThumbnailImageSrc"/> -+ <assertNotContains stepKey="checkThumbnailImage"> -+ <actualResult type="variable">$getThumbnailImageSrc</actualResult> -+ <expectedResult type="string">{{placeholderThumbnailImage.name}}</expectedResult> -+ </assertNotContains> -+ <actionGroup ref="removeProductFromMiniCart" stepKey="removeProductFromCart2"> -+ <argument name="productName" value="$$productWithImages.name$$"/> -+ </actionGroup> -+ </test> -+</tests> -diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateAndEditSimpleProductSettingsTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateAndEditSimpleProductSettingsTest.xml -new file mode 100644 -index 00000000000..e3b0ae37463 ---- /dev/null -+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateAndEditSimpleProductSettingsTest.xml -@@ -0,0 +1,136 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> -+ <test name="AdminCreateAndEditSimpleProductSettingsTest"> -+ <annotations> -+ <features value="Catalog"/> -+ <stories value="Create/Edit simple product in Admin"/> -+ <title value="Admin should be able to set/edit other product information when creating/editing a simple product"/> -+ <description value="Admin should be able to set/edit product information when creating/editing a simple product"/> -+ <severity value="CRITICAL"/> -+ <testCaseId value="MC-3241"/> -+ <group value="Catalog"/> -+ </annotations> -+ <before> -+ <!-- Login as admin --> -+ <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> -+ -+ <!-- Create related products --> -+ <createData entity="SimpleProduct2" stepKey="createFirstRelatedProduct"/> -+ <createData entity="SimpleProduct2" stepKey="createSecondRelatedProduct"/> -+ <createData entity="SimpleProduct2" stepKey="createThirdRelatedProduct"/> -+ </before> -+ <after> -+ <!-- Delete related products --> -+ <deleteData createDataKey="createFirstRelatedProduct" stepKey="deleteFirstRelatedProduct"/> -+ <deleteData createDataKey="createSecondRelatedProduct" stepKey="deleteSecondRelatedProduct"/> -+ <deleteData createDataKey="createThirdRelatedProduct" stepKey="deleteThirdRelatedProduct"/> -+ -+ <!-- Log out --> -+ <actionGroup ref="logout" stepKey="logout"/> -+ </after> -+ -+ <!-- Create new simple product --> -+ <actionGroup ref="GoToSpecifiedCreateProductPage" stepKey="createSimpleProduct"/> -+ -+ <!-- Fill all main fields --> -+ <actionGroup ref="fillMainProductForm" stepKey="fillAllNecessaryFields"/> -+ -+ <!-- Add two related products --> -+ <actionGroup ref="addRelatedProductBySku" stepKey="addFirstRelatedProduct"> -+ <argument name="sku" value="$$createFirstRelatedProduct.sku$$"/> -+ </actionGroup> -+ <actionGroup ref="addRelatedProductBySku" stepKey="addSecondRelatedProduct"> -+ <argument name="sku" value="$$createSecondRelatedProduct.sku$$"/> -+ </actionGroup> -+ -+ <!-- Set Design settings for the product --> -+ <actionGroup ref="AdminSetProductDesignSettingsActionGroup" stepKey="setProductDesignSettings"/> -+ -+ <!-- Set Gift Options settings for the product --> -+ <actionGroup ref="AdminSwitchProductGiftMessageStatusActionGroup" stepKey="enableGiftMessageSettings"> -+ <argument name="status" value="1"/> -+ </actionGroup> -+ -+ <!-- Save product form --> -+ <actionGroup ref="saveProductForm" stepKey="clickSaveButton"/> -+ -+ <!-- Open product page --> -+ <actionGroup ref="StorefrontOpenProductPageActionGroup" stepKey="openStorefrontProductPage"> -+ <argument name="productUrl" value="{{_defaultProduct.name}}"/> -+ </actionGroup> -+ -+ <!-- Assert related products at the storefront --> -+ <seeElement selector="{{StorefrontProductRelatedProductsSection.relatedProductName($$createFirstRelatedProduct.name$$)}}" stepKey="seeFirstRelatedProductInStorefront"/> -+ <seeElement selector="{{StorefrontProductRelatedProductsSection.relatedProductName($$createSecondRelatedProduct.name$$)}}" stepKey="seeSecondRelatedProductInStorefront"/> -+ -+ <!-- Assert product design settings "left bar is present at product page with 2 columns" --> -+ <seeElement selector="{{StorefrontProductPageDesignSection.layoutTwoColumnsLeft}}" stepKey="seeDesignChanges"/> -+ -+ <!-- Assert Gift Option product settings is present --> -+ <actionGroup ref="StorefrontAddProductToCartActionGroup" stepKey="addProductToCart"> -+ <argument name="product" value="_defaultProduct"/> -+ <argument name="productCount" value="1"/> -+ </actionGroup> -+ <actionGroup ref="StorefrontOpenCartFromMinicartActionGroup" stepKey="openShoppingCart"/> -+ <actionGroup ref="StorefrontAssertGiftMessageFieldsActionGroup" stepKey="assertGiftMessageFieldsArePresent"/> -+ -+ <!-- Open created product --> -+ <actionGroup ref="SearchForProductOnBackendActionGroup" stepKey="searchForSimpleProduct"> -+ <argument name="product" value="_defaultProduct"/> -+ </actionGroup> -+ <actionGroup ref="OpenEditProductOnBackendActionGroup" stepKey="openEditProduct"> -+ <argument name="product" value="_defaultProduct"/> -+ </actionGroup> -+ -+ <!-- Edit product Search Engine Optimization settings --> -+ <actionGroup ref="AdminChangeProductSEOSettingsActionGroup" stepKey="editProductSEOSettings"> -+ <argument name="productName" value="SimpleProduct.name"/> -+ </actionGroup> -+ -+ <!-- Edit related products --> -+ <actionGroup ref="addRelatedProductBySku" stepKey="addThirdRelatedProduct"> -+ <argument name="sku" value="$$createThirdRelatedProduct.sku$$"/> -+ </actionGroup> -+ <click selector="{{AdminProductFormRelatedUpSellCrossSellSection.removeRelatedProduct($$createFirstRelatedProduct.sku$$)}}" stepKey="removeFirstRelatedProduct"/> -+ -+ <!-- Edit Design settings for the product --> -+ <actionGroup ref="AdminSetProductDesignSettingsActionGroup" stepKey="editProductDesignSettings"> -+ <argument name="designSettings" value="simpleLumaDesign"/> -+ </actionGroup> -+ -+ <!-- Edit Gift Option product settings --> -+ <actionGroup ref="AdminSwitchProductGiftMessageStatusActionGroup" stepKey="disableGiftMessageSettings"/> -+ -+ <!-- Save product form --> -+ <actionGroup ref="saveProductForm" stepKey="clickSaveProduct"/> -+ -+ <!-- Verify Url Key after changing --> -+ <actionGroup ref="StorefrontOpenProductPageActionGroup" stepKey="openProductPage"> -+ <argument name="productUrl" value="{{SimpleProduct.name}}"/> -+ </actionGroup> -+ -+ <!-- Assert related products at the storefront --> -+ <seeElement selector="{{StorefrontProductRelatedProductsSection.relatedProductName($$createSecondRelatedProduct.name$$)}}" stepKey="seeSecondRelatedProduct"/> -+ <seeElement selector="{{StorefrontProductRelatedProductsSection.relatedProductName($$createThirdRelatedProduct.name$$)}}" stepKey="seeThirdRelatedProduct"/> -+ -+ <!-- Assert product design settings "Layout empty" --> -+ <seeElement selector="{{StorefrontProductPageDesignSection.layoutEmpty}}" stepKey="seeNewDesignChanges"/> -+ -+ <!-- Assert Gift Option product settings --> -+ <actionGroup ref="StorefrontOpenCartFromMinicartActionGroup" stepKey="openCart"/> -+ <dontSeeElement selector="{{StorefrontProductCartGiftOptionSection.giftOptions}}" stepKey="dontSeeGiftOptionBtn"/> -+ -+ <!-- Delete created simple product --> -+ <actionGroup ref="deleteProductUsingProductGrid" stepKey="deleteProduct"> -+ <argument name="product" value="_defaultProduct"/> -+ </actionGroup> -+ </test> -+</tests> -diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateAndSwitchProductType.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateAndSwitchProductType.xml -new file mode 100755 -index 00000000000..4deca735046 ---- /dev/null -+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateAndSwitchProductType.xml -@@ -0,0 +1,83 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> -+ <test name="AdminCreateSimpleProductSwitchToVirtualTest"> -+ <annotations> -+ <features value="Catalog"/> -+ <stories value="Product Type Switching"/> -+ <title value="Admin should be able to switch a new product from simple to virtual"/> -+ <description value="After selecting a simple product when adding Admin should be switch to virtual implicitly"/> -+ <severity value="CRITICAL"/> -+ <testCaseId value="MC-10925"/> -+ <group value="catalog"/> -+ <group value="mtf_migrated"/> -+ </annotations> -+ <before> -+ <createData entity="_defaultCategory" stepKey="createPreReqCategory"/> -+ </before> -+ <after> -+ <actionGroup ref="GoToProductCatalogPage" stepKey="goToProductCatalogPage"/> -+ <actionGroup ref="deleteProductUsingProductGrid" stepKey="deleteSimpleProduct"> -+ <argument name="product" value="_defaultProduct"/> -+ </actionGroup> -+ <actionGroup ref="resetProductGridToDefaultView" stepKey="resetSearch"/> -+ <amOnPage url="admin/admin/auth/logout/" stepKey="amOnLogoutPage"/> -+ <deleteData createDataKey="createPreReqCategory" stepKey="deletePreReqCategory"/> -+ </after> -+ -+ <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin1"/> -+ <!-- Open Dropdown and select simple product option --> -+ <comment stepKey="beforeOpenProductFillForm" userInput="Selecting Product from the Add Product Dropdown"/> -+ <actionGroup ref="GoToSpecifiedCreateProductPage" stepKey="openProductFillForm"> -+ <argument name="productType" value="simple"/> -+ </actionGroup> -+ -+ <!-- Fill form for Virtual Product Type --> -+ <comment stepKey="beforeFillProductForm" userInput="Filling Product Form"/> -+ <actionGroup ref="fillMainProductFormNoWeight" stepKey="fillProductForm"> -+ <argument name="product" value="_defaultProduct"/> -+ </actionGroup> -+ <actionGroup ref="SetProductUrlKey" stepKey="setProductUrl"> -+ <argument name="product" value="_defaultProduct"/> -+ </actionGroup> -+ <actionGroup ref="saveProductForm" stepKey="saveProductForm"/> -+ <!-- Check that product was added with implicit type change --> -+ <comment stepKey="beforeVerify" userInput="Verify Product Type Assigned Correctly"/> -+ <actionGroup ref="GoToProductCatalogPage" stepKey="goToProductCatalogPage"/> -+ <actionGroup ref="resetProductGridToDefaultView" stepKey="resetSearch"/> -+ <actionGroup ref="filterProductGridByName" stepKey="searchForProduct"> -+ <argument name="product" value="_defaultProduct"/> -+ </actionGroup> -+ <see selector="{{AdminProductGridSection.productGridCell('1', 'Type')}}" userInput="Virtual Product" stepKey="seeProductTypeInGrid"/> -+ <actionGroup ref="AssertProductInStorefrontProductPage" stepKey="AssertProductInStorefrontProductPage"> -+ <argument name="product" value="_defaultProduct"/> -+ </actionGroup> -+ </test> -+ <test name="AdminCreateVirtualProductSwitchToSimpleTest" extends="AdminCreateSimpleProductSwitchToVirtualTest"> -+ <annotations> -+ <features value="Catalog"/> -+ <stories value="Product Type Switching"/> -+ <title value="Admin should be able to switch a new product from virtual to simple"/> -+ <description value="After selecting a virtual product when adding Admin should be switch to simple implicitly"/> -+ <severity value="CRITICAL"/> -+ <testCaseId value="MC-10928"/> -+ <group value="catalog"/> -+ <group value="mtf_migrated"/> -+ </annotations> -+ <actionGroup ref="GoToSpecifiedCreateProductPage" stepKey="openProductFillForm"> -+ <argument name="productType" value="virtual"/> -+ </actionGroup> -+ <!-- Fill form for Virtual Product Type --> -+ <actionGroup ref="fillMainProductForm" stepKey="fillProductForm"> -+ <argument name="product" value="_defaultProduct"/> -+ </actionGroup> -+ <see selector="{{AdminProductGridSection.productGridCell('1', 'Type')}}" userInput="Simple Product" stepKey="seeProductTypeInGrid"/> -+ </test> -+</tests> -diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateAttributeSetEntityTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateAttributeSetEntityTest.xml -new file mode 100644 -index 00000000000..d9e410a9a30 ---- /dev/null -+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateAttributeSetEntityTest.xml -@@ -0,0 +1,70 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> -+ <test name="AdminCreateAttributeSetEntityTest"> -+ <annotations> -+ <features value="Catalog"/> -+ <stories value="Create Attribute Set"/> -+ <title value="Create attribute set with new product attribute"/> -+ <description value="Admin should be able to create attribute set with new product attribute"/> -+ <testCaseId value="MC-10884"/> -+ <severity value="CRITICAL"/> -+ <group value="mtf_migrated"/> -+ </annotations> -+ <before> -+ <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> -+ <createData entity="productAttributeWysiwyg" stepKey="createProductAttribute"/> -+ <createData entity="CatalogAttributeSet" stepKey="createAttributeSet"/> -+ </before> -+ <after> -+ <deleteData createDataKey="createProductAttribute" stepKey="deleteProductAttribute"/> -+ <deleteData createDataKey="createAttributeSet" stepKey="deleteAttributeSet"/> -+ </after> -+ -+ <amOnPage url="{{AdminProductAttributeSetGridPage.url}}" stepKey="goToAttributeSets"/> -+ <waitForPageLoad stepKey="waitForPageLoad"/> -+ <actionGroup ref="goToAttributeSetByName" stepKey="filterProductAttributeSetGridByLabel"> -+ <argument name="name" value="$$createAttributeSet.attribute_set_name$$"/> -+ </actionGroup> -+ -+ <!-- Assert created attribute in an unassigned attributes --> -+ <see userInput="$$createProductAttribute.attribute_code$$" selector="{{AdminProductAttributeSetEditSection.unassignedAttributesTree}}" stepKey="seeAttributeInUnassignedAttr"/> -+ -+ <!-- Assign attribute in the group --> -+ <actionGroup ref="AssignAttributeToGroup" stepKey="assignAttributeToGroup"> -+ <argument name="group" value="Product Details"/> -+ <argument name="attribute" value="$$createProductAttribute.attribute_code$$"/> -+ </actionGroup> -+ <see userInput="$$createProductAttribute.attribute_code$$" selector="{{AdminProductAttributeSetEditSection.groupTree}}" stepKey="seeAttributeInGroup"/> -+ <actionGroup ref="SaveAttributeSet" stepKey="SaveAttributeSet"/> -+ <amOnPage url="{{AdminProductAttributeSetGridPage.url}}" stepKey="goToAttributeSets2"/> -+ <waitForPageLoad stepKey="waitForPageLoad2"/> -+ -+ <!-- Assert an attribute in the group--> -+ <actionGroup ref="goToAttributeSetByName" stepKey="filterProductAttributeSetGridByLabel2"> -+ <argument name="name" value="$$createAttributeSet.attribute_set_name$$"/> -+ </actionGroup> -+ <see userInput="$$createProductAttribute.attribute_code$$" selector="{{AdminProductAttributeSetEditSection.groupTree}}" stepKey="seeAttributeInGroup2"/> -+ -+ <!-- Assert attribute can be used in product creation --> -+ <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="navigateToCatalogProductGrid"/> -+ <waitForPageLoad stepKey="waitForPageLoad3"/> -+ <click selector="{{AdminProductGridActionSection.addProductToggle}}" stepKey="clickAddProductDropdown"/> -+ <click selector="{{AdminProductGridActionSection.addSimpleProduct}}" stepKey="clickAddSimpleProduct"/> -+ -+ <!-- Switch from default attribute set to new attribute set --> -+ <click selector="{{AdminProductFormSection.attributeSet}}" stepKey="startEditAttrSet"/> -+ <fillField selector="{{AdminProductFormSection.attributeSetFilter}}" userInput="$$createAttributeSet.attribute_set_name$$" stepKey="searchForAttrSet"/> -+ <click selector="{{AdminProductFormSection.attributeSetFilterResult}}" stepKey="selectAttrSet"/> -+ -+ <!-- See new attribute set --> -+ <see selector="{{AdminProductFormSection.attributeSet}}" userInput="$$createAttributeSet.attribute_set_name$$" stepKey="seeAttributeSetName"/> -+ </test> -+</tests> -diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateCategoryFromProductPageTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateCategoryFromProductPageTest.xml -index d5483f772f0..a5150a0fb7f 100644 ---- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateCategoryFromProductPageTest.xml -+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateCategoryFromProductPageTest.xml -@@ -7,11 +7,12 @@ - --> - - <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="AdminCreateCategoryFromProductPageTest"> - <annotations> - <features value="Catalog"/> - <stories value="Create/Edit Category in Admin"/> -+ <title value="Admin should be able to create category from the product page"/> - <description value="Admin should be able to create category from the product page" /> - <severity value="AVERAGE"/> - <testCaseId value="MC-234"/> -diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateCategoryTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateCategoryTest.xml -index 7e3e2cd918f..15171fe3713 100644 ---- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateCategoryTest.xml -+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateCategoryTest.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="AdminCreateCategoryTest"> - <annotations> - <features value="Catalog"/> -@@ -69,4 +69,35 @@ - <waitForElementVisible selector="{{CategoryDesignSection.LayoutDropdown}}" stepKey="waitForLayoutDropDown" /> - <seeOptionIsSelected selector="{{CategoryDesignSection.LayoutDropdown}}" userInput="2 columns with right bar" stepKey="see2ColumnsSelected" /> - </test> -+ <test name="AdminCategoryFormDisplaySettingsUIValidationTest"> -+ <annotations> -+ <features value="Catalog"/> -+ <stories value="Default layout configuration MAGETWO-88793"/> -+ <title value="Category should not be saved once layered navigation price step field is left empty"/> -+ <description value="Once the Config setting is unchecked Category should not be saved with layered navigation price field left empty"/> -+ <severity value="AVERAGE"/> -+ <testCaseId value="MAGETWO-95797"/> -+ <group value="category"/> -+ </annotations> -+ <before> -+ <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> -+ </before> -+ <after> -+ <actionGroup ref="logout" stepKey="logout"/> -+ </after> -+ <amOnPage url="{{AdminCategoryPage.url}}" stepKey="navigateToCategoryPage"/> -+ <waitForPageLoad time="30" stepKey="waitForPageLoad1"/> -+ <click selector="{{AdminCategorySidebarActionSection.AddSubcategoryButton}}" stepKey="clickOnAddSubCategory"/> -+ <fillField selector="{{AdminCategoryBasicFieldSection.CategoryNameInput}}" userInput="{{SimpleSubCategory.name}}" stepKey="enterCategoryName"/> -+ <click selector="{{CategoryDisplaySettingsSection.DisplaySettingTab}}" stepKey="clickOnDisplaySettingsTab"/> -+ <waitForElementVisible selector="{{CategoryDisplaySettingsSection.filterPriceRangeUseConfig}}" stepKey="wait"/> -+ <scrollTo selector="{{CategoryDisplaySettingsSection.layeredNavigationPriceInput}}" stepKey="scrollToLayeredNavigationField"/> -+ <uncheckOption selector="{{CategoryDisplaySettingsSection.filterPriceRangeUseConfig}}" stepKey="uncheckConfigSetting"/> -+ <click selector="{{AdminCategoryMainActionsSection.SaveButton}}" stepKey="saveCategory"/> -+ <see selector="{{AdminCategoryBasicFieldSection.FieldError('uid')}}" userInput="This is a required field." stepKey="seeErrorMessage"/> -+ <!-- Verify that the Layered navigation price step field has the required indicator --> -+ <comment userInput="Check if Layered navigation price field has required indicator icon" stepKey="comment" /> -+ <executeJS function="{{CategoryDisplaySettingsSection.RequiredFieldIndicator('filter_price_range')}}" stepKey="getRequiredFieldIndicator"/> -+ <assertEquals expected='"*"' expectedType="string" actualType="variable" actual="getRequiredFieldIndicator" message="pass" stepKey="assertRequiredFieldIndicator1"/> -+ </test> - </tests> -diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateCategoryWithAnchorFieldTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateCategoryWithAnchorFieldTest.xml -new file mode 100644 -index 00000000000..9115004ad95 ---- /dev/null -+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateCategoryWithAnchorFieldTest.xml -@@ -0,0 +1,75 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> -+ <test name="AdminCreateCategoryWithAnchorFieldTest"> -+ <annotations> -+ <stories value="Create categories"/> -+ <title value="Create anchor subcategory with all fields"/> -+ <description value="Login as admin and create anchor subcategory with all fields"/> -+ <testCaseId value="MC-5267"/> -+ <severity value="CRITICAL"/> -+ <group value="Catalog"/> -+ <group value="mtf_migrated"/> -+ </annotations> -+ <before> -+ <actionGroup ref="LoginAsAdmin" stepKey="loginToAdminPanel"/> -+ <createData entity="_defaultBlock" stepKey="createDefaultCMSBlock"/> -+ <createData entity="defaultSimpleProduct" stepKey="simpleProduct" /> -+ </before> -+ <after> -+ <actionGroup ref="DeleteCategory" stepKey="deleteCategory"/> -+ <actionGroup ref="logout" stepKey="logout"/> -+ <deleteData createDataKey="createDefaultCMSBlock" stepKey="deleteDefaultCMSBlock"/> -+ <deleteData stepKey="deleteSimpleProduct" createDataKey="simpleProduct"/> -+ </after> -+ <!--Create SubCategory--> -+ <amOnPage url="{{AdminCategoryPage.url}}" stepKey="openAdminCategoryIndexPage"/> -+ <waitForPageLoad stepKey="waitForPageToLoaded"/> -+ <click selector="{{AdminCategorySidebarActionSection.AddSubcategoryButton}}" stepKey="clickOnAddSubCategoryButton"/> -+ <fillField selector="{{AdminCategoryBasicFieldSection.CategoryNameInput}}" userInput="{{_defaultCategory.name}}" stepKey="fillCategoryName"/> -+ <checkOption selector="{{AdminCategoryBasicFieldSection.EnableCategory}}" stepKey="enableCategory"/> -+ <!--Select Content and fill the options--> -+ <scrollTo selector="{{AdminCategoryContentSection.sectionHeader}}" x="0" y="-80" stepKey="scrollToContent"/> -+ <click selector="{{AdminCategoryContentSection.sectionHeader}}" stepKey="selectContent"/> -+ <scrollTo selector="{{AdminCategoryContentSection.AddCMSBlock}}" x="0" y="-80" stepKey="scrollToAddCMSBlock"/> -+ <selectOption selector="{{AdminCategoryContentSection.AddCMSBlock}}" userInput="$$createDefaultCMSBlock.title$$" stepKey="selectCMSBlock"/> -+ <!--Select Display Setting and fill the options--> -+ <scrollTo selector="{{CategoryDisplaySettingsSection.DisplaySettingTab}}" x="0" y="-80" stepKey="scrollToDisplaySetting"/> -+ <click selector="{{CategoryDisplaySettingsSection.DisplaySettingTab}}" stepKey="selectDisplaySetting"/> -+ <selectOption selector="{{CategoryDisplaySettingsSection.displayMode}}" userInput="PRODUCTS_AND_PAGE" stepKey="selectdisplayMode"/> -+ <checkOption selector="{{CategoryDisplaySettingsSection.anchor}}" stepKey="enableAnchor"/> -+ <click selector="{{CategoryDisplaySettingsSection.productListCheckBox}}" stepKey="enableTheAvailableProductList"/> -+ <selectOption selector="{{CategoryDisplaySettingsSection.productList}}" parameterArray="['Position', 'Product Name', 'Price']" stepKey="selectPrice"/> -+ <scrollTo selector="{{CategoryDisplaySettingsSection.defaultProductLisCheckBox}}" x="0" y="-80" stepKey="scrollToDefaultProductList"/> -+ <click selector="{{CategoryDisplaySettingsSection.defaultProductLisCheckBox}}" stepKey="enableTheDefaultProductList"/> -+ <selectOption selector="{{CategoryDisplaySettingsSection.defaultProductList}}" userInput="name" stepKey="selectProductName"/> -+ <scrollTo selector="{{CategoryDisplaySettingsSection.layeredNavigationPriceCheckBox}}" x="0" y="-80" stepKey="scrollToLayeredNavPrice"/> -+ <click selector="{{CategoryDisplaySettingsSection.layeredNavigationPriceCheckBox}}" stepKey="enableLayeredNavigationPrice"/> -+ <fillField selector="{{CategoryDisplaySettingsSection.layeredNavigationPriceInput}}" userInput="5.5" stepKey="fillThePrice"/> -+ <!--Search the products and select the category products--> -+ <scrollTo selector="{{AdminCategoryBasicFieldSection.productsInCategory}}" x="0" y="-80" stepKey="scrollToProductInCategory"/> -+ <click selector="{{AdminCategoryBasicFieldSection.productsInCategory}}" stepKey="clickOnProductInCategory"/> -+ <fillField selector="{{AdminCategoryContentSection.productTableColumnName}}" userInput="$$simpleProduct.name$$" stepKey="selectProduct"/> -+ <click selector="{{AdminCategoryContentSection.productSearch}}" stepKey="clickSearchButton"/> -+ <click selector="{{AdminCategoryContentSection.productTableRow}}" stepKey="selectProductFromTableRow"/> -+ <click selector="{{AdminCategoryMainActionsSection.SaveButton}}" stepKey="clickSaveButton"/> -+ <waitForPageLoad stepKey="waitForCategorySaved"/> -+ <see selector="{{AdminCategoryMessagesSection.SuccessMessage}}" userInput="You saved the category." stepKey="assertSuccessMessage"/> -+ <waitForPageLoad stepKey="waitForPageTitleToBeSaved"/> -+ <!--Verify the Category Title--> -+ <see selector="{{AdminCategoryContentSection.categoryPageTitle}}" userInput="{{_defaultCategory.name}}" stepKey="seePageTitle" /> -+ <!--Verify Product in store front page--> -+ <amOnPage url="{{StorefrontCategoryPage.url(_defaultCategory.name_lwr)}}" stepKey="amOnCategoryPage"/> -+ <waitForPageLoad stepKey="waitForPageToBeLoaded"/> -+ <see selector="{{StorefrontCategoryMainSection.CategoryTitle}}" userInput="{{_defaultCategory.name}}" stepKey="seeCategoryPageTitle"/> -+ <seeElement selector="{{StorefrontHeaderSection.NavigationCategoryByName(_defaultCategory.name)}}" stepKey="seeCategoryOnNavigation"/> -+ <waitForPageLoad stepKey="waitForProductToLoad"/> -+ <seeElement selector="{{StorefrontCategoryMainSection.productLinkByHref($$simpleProduct.urlKey$$)}}" stepKey="seeProductInCategory"/> -+ </test> -+</tests> -diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateCategoryWithCustomRootCategoryTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateCategoryWithCustomRootCategoryTest.xml -new file mode 100644 -index 00000000000..e8c6da476a3 ---- /dev/null -+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateCategoryWithCustomRootCategoryTest.xml -@@ -0,0 +1,76 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> -+ <test name="AdminCreateCategoryWithCustomRootCategoryTest"> -+ <annotations> -+ <stories value="Create categories"/> -+ <title value="Create category in the custom root category that is used for custom website"/> -+ <description value="Login as admin and create a root category with nested sub category and verify category in store front "/> -+ <testCaseId value="MC-5272"/> -+ <severity value="CRITICAL"/> -+ <group value="mtf_migrated"/> -+ <group value="Catalog"/> -+ </annotations> -+ <before> -+ <actionGroup ref="LoginAsAdmin" stepKey="loginToAdminPanel"/> -+ </before> -+ <after> -+ <amOnPage url="{{AdminSystemStorePage.url}}" stepKey="navigateToStoresIndex"/> -+ <waitForPageLoad stepKey="waitStoreIndexPageLoad" /> -+ <actionGroup ref="DeleteCustomStoreActionGroup" stepKey="deleteCustomStore"> -+ <argument name="storeGroupName" value="customStore.name"/> -+ </actionGroup> -+ <actionGroup ref="DeleteCategory" stepKey="deleteCreatedNewRootCategory"> -+ <argument name="categoryEntity" value="NewRootCategory"/> -+ </actionGroup> -+ <actionGroup ref="logout" stepKey="logout"/> -+ </after> -+ <amOnPage url="{{AdminCategoryPage.url}}" stepKey="openAdminCategoryIndexPage"/> -+ <waitForPageLoad stepKey="waitForCategoryIndexPageToBeLoaded"/> -+ <!--Create Root Category--> -+ <actionGroup ref="AdminCreateRootCategory" stepKey="createNewRootCategory"> -+ <argument name="categoryEntity" value="NewRootCategory"/> -+ </actionGroup> -+ <!--Create subcategory--> -+ <scrollToTopOfPage stepKey="scrollToTopOfPage"/> -+ <click selector="{{AdminCategorySidebarTreeSection.categoryInTree(NewRootCategory.name)}}" stepKey="clickOnCreatedNewRootCategory"/> -+ <scrollToTopOfPage stepKey="scrollToTopOfPage1"/> -+ <actionGroup ref="CreateCategory" stepKey="createSubcategory"> -+ <argument name="categoryEntity" value="SimpleSubCategory"/> -+ </actionGroup> -+ <!--Create a Store--> -+ <amOnPage url="{{AdminSystemStorePage.url}}" stepKey="amOnAdminSystemStorePage"/> -+ <waitForPageLoad stepKey="waitForSystemStorePage"/> -+ <click selector="{{AdminStoresMainActionsSection.createStoreButton}}" stepKey="selectCreateStore"/> -+ <fillField userInput="{{customStore.name}}" selector="{{AdminNewStoreGroupSection.storeGrpNameTextField}}" stepKey="fillStoreName"/> -+ <fillField userInput="{{customStore.code}}" selector="{{AdminNewStoreGroupSection.storeGrpCodeTextField}}" stepKey="fillStoreCode"/> -+ <selectOption userInput="{{NewRootCategory.name}}" selector="{{AdminNewStoreGroupSection.storeRootCategoryDropdown}}" stepKey="selectStoreStatus"/> -+ <click selector="{{AdminStoresMainActionsSection.saveButton}}" stepKey="clickSaveStoreButton"/> -+ <!--Create a Store View--> -+ <click selector="{{AdminStoresMainActionsSection.createStoreViewButton}}" stepKey="selectCreateStoreView"/> -+ <click selector="{{AdminNewStoreSection.storeGrpDropdown}}" stepKey="clickDropDown"/> -+ <selectOption userInput="{{customStore.name}}" selector="{{AdminNewStoreSection.storeGrpDropdown}}" stepKey="selectStoreViewStatus"/> -+ <fillField userInput="{{customStore.name}}" selector="{{AdminNewStoreSection.storeNameTextField}}" stepKey="fillStoreViewName"/> -+ <fillField userInput="{{customStore.code}}" selector="{{AdminNewStoreSection.storeCodeTextField}}" stepKey="fillStoreViewCode"/> -+ <selectOption selector="{{AdminNewStoreSection.statusDropdown}}" userInput="Enabled" stepKey="enableStatus"/> -+ <click selector="{{AdminStoresMainActionsSection.saveButton}}" stepKey="clickSaveStoreViewButton"/> -+ <waitForElementVisible selector="{{AdminConfirmationModalSection.ok}}" stepKey="waitForModal" /> -+ <see selector="{{AdminConfirmationModalSection.title}}" userInput="Warning message" stepKey="seeWarning" /> -+ <click selector="{{AdminConfirmationModalSection.ok}}" stepKey="dismissModal" /> -+ <waitForElementNotVisible selector="{{AdminNewStoreViewActionsSection.loadingMask}}" stepKey="waitForElementVisible"/> -+ <!--Go to store front page--> -+ <amOnPage url="/{{NewRootCategory.name}}/{{SimpleSubCategory.name}}.html" stepKey="seeTheCategoryInStoreFrontPage"/> -+ <waitForPageLoad time="60" stepKey="waitForStoreFrontPageLoad"/> -+ <!--Verify subcategory displayed in store front page--> -+ <click selector="{{StorefrontFooterSection.switchStoreButton}}" stepKey="selectMainWebsite"/> -+ <click selector="{{StorefrontFooterSection.storeLink(customStore.name)}}" stepKey="selectCustomStore"/> -+ <waitForPageLoad stepKey="waitForCategoryToLoad"/> -+ <seeElement selector="{{StorefrontHeaderSection.NavigationCategoryByName(SimpleSubCategory.name)}}" stepKey="seeSubCategoryInStoreFrontPage"/> -+ </test> -+</tests> -diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateCategoryWithFiveNestingTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateCategoryWithFiveNestingTest.xml -new file mode 100644 -index 00000000000..530bafaef24 ---- /dev/null -+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateCategoryWithFiveNestingTest.xml -@@ -0,0 +1,86 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> -+ <test name="AdminCreateCategoryWithFiveNestingTest"> -+ <annotations> -+ <stories value="Create categories"/> -+ <title value="Create category with five nesting"/> -+ <description value="Login as admin and create nested sub category and verify the subcategory displayed in store front page "/> -+ <testCaseId value="MC-5271"/> -+ <severity value="CRITICAL"/> -+ <group value="Catalog"/> -+ <group value="mtf_migrated"/> -+ </annotations> -+ <before> -+ <actionGroup ref="LoginAsAdmin" stepKey="loginToAdminPanel"/> -+ </before> -+ <after> -+ <amOnPage url="{{AdminCategoryPage.url}}" stepKey="goToCategoryPage"/> -+ <waitForPageLoad time="60" stepKey="waitForCategoryPageLoad"/> -+ <click selector="{{AdminCategorySidebarTreeSection.categoryInTree(FirstLevelSubCat.name)}}" stepKey="clickCategoryLink"/> -+ <click selector="{{AdminCategoryMainActionsSection.DeleteButton}}" stepKey="clickDelete"/> -+ <waitForElementVisible selector="{{AdminCategoryModalSection.message}}" stepKey="waitForConfirmationModal"/> -+ <see selector="{{AdminCategoryModalSection.message}}" userInput="Are you sure you want to delete this category?" stepKey="seeDeleteConfirmationMessage"/> -+ <click selector="{{AdminCategoryModalSection.ok}}" stepKey="confirmDelete"/> -+ <waitForPageLoad time="60" stepKey="waitForDeleteToFinish"/> -+ <see selector="You deleted the category." stepKey="seeDeleteSuccess"/> -+ <click selector="{{AdminCategorySidebarTreeSection.expandAll}}" stepKey="expandToSeeAllCategories"/> -+ <dontSee selector="{{AdminCategorySidebarTreeSection.categoryInTree(FirstLevelSubCat.name)}}" stepKey="dontSeeCategoryInTree"/> -+ <actionGroup ref="logout" stepKey="logout"/> -+ </after> -+ <!--Create Category with Five Nesting --> -+ <amOnPage url="{{AdminCategoryPage.url}}" stepKey="openAdminCategoryIndexPage"/> -+ <waitForPageLoad stepKey="waitForCategoryIndexPageToBeLoaded"/> -+ <!--Create Nested First Category--> -+ <click selector="{{AdminCategorySidebarActionSection.AddSubcategoryButton}}" stepKey="clickOnAddSubCategoryButton"/> -+ <checkOption selector="{{AdminCategoryBasicFieldSection.EnableCategory}}" stepKey="enableCategory"/> -+ <fillField selector="{{AdminCategoryBasicFieldSection.CategoryNameInput}}" userInput="{{FirstLevelSubCat.name}}" stepKey="fillFirstSubCategoryName"/> -+ <click selector="{{AdminCategoryMainActionsSection.SaveButton}}" stepKey="saveFirstSubCategory"/> -+ <waitForPageLoad stepKey="waitForSFirstSubCategorySaved"/> -+ <!-- Verify success message --> -+ <see selector="{{AdminCategoryMessagesSection.SuccessMessage}}" userInput="You saved the category." stepKey="assertSuccessMessage"/> -+ <!--Create Nested Second Sub Category--> -+ <click selector="{{AdminCategorySidebarActionSection.AddSubcategoryButton}}" stepKey="clickOnAddSubCategoryButton1"/> -+ <fillField selector="{{AdminCategoryBasicFieldSection.CategoryNameInput}}" userInput="{{SecondLevelSubCat.name}}" stepKey="fillSecondSubCategoryName"/> -+ <click selector="{{AdminCategoryMainActionsSection.SaveButton}}" stepKey="saveSecondSubCategory"/> -+ <waitForPageLoad stepKey="waitForSecondCategory"/> -+ <!-- Verify success message --> -+ <see selector="{{AdminCategoryMessagesSection.SuccessMessage}}" userInput="You saved the category." stepKey="assertSuccessMessage1"/> -+ <!--Create Nested Third Sub Category/>--> -+ <click selector="{{AdminCategorySidebarActionSection.AddSubcategoryButton}}" stepKey="clickOnAddSubCategoryButton2"/> -+ <fillField selector="{{AdminCategoryBasicFieldSection.CategoryNameInput}}" userInput="{{ThirdLevelSubCat.name}}" stepKey="fillThirdSubCategoryName"/> -+ <click selector="{{AdminCategoryMainActionsSection.SaveButton}}" stepKey="saveThirdSubCategory"/> -+ <waitForPageLoad stepKey="waitForThirdCategorySaved"/> -+ <!-- Verify success message --> -+ <see selector="{{AdminCategoryMessagesSection.SuccessMessage}}" userInput="You saved the category." stepKey="assertSuccessMessage2"/> -+ <!--Create Nested fourth Sub Category />--> -+ <click selector="{{AdminCategorySidebarActionSection.AddSubcategoryButton}}" stepKey="clickOnAddSubCategoryButton3"/> -+ <fillField selector="{{AdminCategoryBasicFieldSection.CategoryNameInput}}" userInput="{{FourthLevelSubCat.name}}" stepKey="fillFourthSubCategoryName"/> -+ <click selector="{{AdminCategoryMainActionsSection.SaveButton}}" stepKey="saveFourthSubCategory"/> -+ <waitForPageLoad stepKey="waitForFourthCategorySaved"/> -+ <!-- Verify success message --> -+ <see selector="{{AdminCategoryMessagesSection.SuccessMessage}}" userInput="You saved the category." stepKey="assertSuccessMessage3"/> -+ <!--Create Nested fifth Sub Category />--> -+ <click selector="{{AdminCategorySidebarActionSection.AddSubcategoryButton}}" stepKey="clickOnAddSubCategoryButton4"/> -+ <fillField selector="{{AdminCategoryBasicFieldSection.CategoryNameInput}}" userInput="{{FifthLevelCat.name}}" stepKey="fillFifthSubCategoryName"/> -+ <click selector="{{AdminCategoryMainActionsSection.SaveButton}}" stepKey="saveFifthLevelCategory"/> -+ <waitForPageLoad stepKey="waitForFifthCategorySaved"/> -+ <!-- Verify success message --> -+ <see selector="{{AdminCategoryMessagesSection.SuccessMessage}}" userInput="You saved the category." stepKey="assertSuccessMessage4"/> -+ <amOnPage url="/{{FirstLevelSubCat.name}}/{{SecondLevelSubCat.name}}/{{ThirdLevelSubCat.name}}/{{FourthLevelSubCat.name}}/{{FifthLevelCat.name}}.html" stepKey="seeTheCategoryInStoreFrontPage"/> -+ <waitForPageLoad time="60" stepKey="waitForStoreFrontPageLoad"/> -+ <!--<Verify category displayed in store front page--> -+ <grabMultiple selector=".breadcrumbs li" stepKey="breadcrumbs"/> -+ <assertEquals stepKey="verifyTheCategoryInStoreFrontPage"> -+ <expectedResult type="array">['Home', {{FirstLevelSubCat.name}}, {{SecondLevelSubCat.name}}, {{ThirdLevelSubCat.name}}, {{FourthLevelSubCat.name}}, {{FifthLevelCat.name}} ]</expectedResult> -+ <actualResult type="variable">breadcrumbs</actualResult> -+ </assertEquals> -+ </test> -+</tests> -diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateCategoryWithInactiveCategoryTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateCategoryWithInactiveCategoryTest.xml -new file mode 100644 -index 00000000000..96f945da138 ---- /dev/null -+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateCategoryWithInactiveCategoryTest.xml -@@ -0,0 +1,46 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> -+ <test name="AdminCreateCategoryWithInactiveCategoryTest"> -+ <annotations> -+ <stories value="Create categories"/> -+ <title value="Create disabled subcategory"/> -+ <description value="Login as admin and create category with inactivated enable category option"/> -+ <testCaseId value="MC-5268"/> -+ <severity value="CRITICAL"/> -+ <group value="Catalog"/> -+ <group value="mtf_migrated"/> -+ </annotations> -+ <before> -+ <actionGroup ref="LoginAsAdmin" stepKey="loginToAdminPanel"/> -+ </before> -+ <after> -+ <actionGroup ref="DeleteCategory" stepKey="deleteCategory"/> -+ <actionGroup ref="logout" stepKey="logout"/> -+ </after> -+ <!-- Create In active Category --> -+ <amOnPage url="{{AdminCategoryPage.url}}" stepKey="openAdminCategoryIndexPage"/> -+ <waitForPageLoad stepKey="waitForCategoryIndexPageToBeLoaded"/> -+ <click selector="{{AdminCategorySidebarActionSection.AddSubcategoryButton}}" stepKey="clickOnAddSubCategoryButton"/> -+ <click selector="{{AdminCategoryBasicFieldSection.enableCategoryLabel}}" stepKey="disableCategory"/> -+ <checkOption selector="{{AdminCategoryBasicFieldSection.IncludeInMenu}}" stepKey="enableIncludeInMenu"/> -+ <fillField selector="{{AdminCategoryBasicFieldSection.CategoryNameInput}}" userInput="{{_defaultCategory.name}}" stepKey="fillCategoryName"/> -+ <click selector="{{AdminCategoryMainActionsSection.SaveButton}}" stepKey="clickSaveButton"/> -+ <waitForPageLoad stepKey="waitForCategorySaved"/> -+ <see selector="{{AdminCategoryMessagesSection.SuccessMessage}}" userInput="You saved the category." stepKey="assertSuccessMessage"/> -+ <see selector="{{AdminCategoryContentSection.categoryPageTitle}}" userInput="{{_defaultCategory.name}}" stepKey="seePageTitle" /> -+ <dontSeeCheckboxIsChecked selector="{{AdminCategoryBasicFieldSection.EnableCategory}}" stepKey="dontCategoryIsChecked"/> -+ <!--Verify InActive Category is created--> -+ <seeElement selector="{{AdminCategoryContentSection.categoryInTree(_defaultCategory.name)}}" stepKey="seeCategoryInTree" /> -+ <!--Verify Category is not listed store front page--> -+ <amOnPage url="{{StorefrontCategoryPage.url(_defaultCategory.name)}}" stepKey="amOnCategoryPage"/> -+ <waitForPageLoad stepKey="waitForPageToBeLoaded"/> -+ <dontSeeElement selector="{{StorefrontHeaderSection.NavigationCategoryByName(_defaultCategory.name)}}" stepKey="dontSeeCategoryOnStoreFrontPage"/> -+ </test> -+</tests> -diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateCategoryWithInactiveIncludeInMenuTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateCategoryWithInactiveIncludeInMenuTest.xml -new file mode 100644 -index 00000000000..c983089163f ---- /dev/null -+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateCategoryWithInactiveIncludeInMenuTest.xml -@@ -0,0 +1,47 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> -+ <test name="AdminCreateCategoryWithInactiveIncludeInMenuTest"> -+ <annotations> -+ <stories value="Create categories"/> -+ <title value="Create not included in menu subcategory"/> -+ <description value="Login as admin and create category with inactivated include in menu option"/> -+ <testCaseId value="MC-5269"/> -+ <severity value="CRITICAL"/> -+ <group value="Catalog"/> -+ <group value="mtf_migrated"/> -+ </annotations> -+ <before> -+ <actionGroup ref="LoginAsAdmin" stepKey="loginToAdminPanel"/> -+ </before> -+ <after> -+ <actionGroup ref="DeleteCategory" stepKey="deleteCategory"/> -+ <actionGroup ref="logout" stepKey="logout"/> -+ </after> -+ <!--Create Category with not included in menu Subcategory --> -+ <amOnPage url="{{AdminCategoryPage.url}}" stepKey="openAdminCategoryIndexPage"/> -+ <waitForPageLoad stepKey="waitForCategoryIndexPageToBeLoaded"/> -+ <click selector="{{AdminCategorySidebarActionSection.AddSubcategoryButton}}" stepKey="clickOnAddSubCategoryButton"/> -+ <checkOption selector="{{AdminCategoryBasicFieldSection.EnableCategory}}" stepKey="enableCategory"/> -+ <click selector="{{AdminCategoryBasicFieldSection.includeInMenuLabel}}" stepKey="disableIncludeInMenu"/> -+ <fillField selector="{{AdminCategoryBasicFieldSection.CategoryNameInput}}" userInput="{{_defaultCategory.name}}" stepKey="fillCategoryName"/> -+ <click selector="{{AdminCategoryMainActionsSection.SaveButton}}" stepKey="clickSaveButton"/> -+ <waitForPageLoad stepKey="waitForCategorySaved"/> -+ <see selector="{{AdminCategoryMessagesSection.SuccessMessage}}" userInput="You saved the category." stepKey="assertSuccessMessage"/> -+ <waitForPageLoad stepKey="waitForPageSaved"/> -+ <see selector="{{AdminCategoryContentSection.categoryPageTitle}}" userInput="{{_defaultCategory.name}}" stepKey="seePageTitle" /> -+ <!--Verify Category is created/>--> -+ <seeElement selector="{{AdminCategoryContentSection.activeCategoryInTree(_defaultCategory.name)}}" stepKey="seeCategoryInTree" /> -+ <!--Verify Category in store front page menu/>--> -+ <amOnPage url="{{StorefrontCategoryPage.url(_defaultCategory.name)}}" stepKey="amOnCategoryPage"/> -+ <waitForPageLoad stepKey="waitForPageToBeLoaded"/> -+ <see selector="{{StorefrontCategoryMainSection.CategoryTitle}}" userInput="{{_defaultCategory.name}}" stepKey="seeCategoryPageTitle"/> -+ <dontSeeElement selector="{{StorefrontHeaderSection.NavigationCategoryByName(_defaultCategory.name)}}" stepKey="dontSeeCategoryOnNavigation"/> -+ </test> -+</tests> -diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateCategoryWithProductsGridFilter.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateCategoryWithProductsGridFilter.xml -new file mode 100644 -index 00000000000..79eec02a828 ---- /dev/null -+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateCategoryWithProductsGridFilter.xml -@@ -0,0 +1,88 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> -+ <test name="AdminCreateCategoryWithProductsGridFilter"> -+ <annotations> -+ <stories value="Create categories"/> -+ <title value="Apply category products grid filter"/> -+ <description value="Login as admin and create default product and product with grid filter"/> -+ <testCaseId value="MC-5273"/> -+ <severity value="CRITICAL"/> -+ <group value="mtf_migrated"/> -+ <group value="Catalog"/> -+ </annotations> -+ <before> -+ <actionGroup ref="LoginAsAdmin" stepKey="loginToAdminPanel"/> -+ </before> -+ <after> -+ <actionGroup ref="DeleteCategory" stepKey="deleteCategory"/> -+ <actionGroup ref="deleteProductUsingProductGrid" stepKey="deleteProduct1"> -+ <argument name="product" value="defaultSimpleProduct"/> -+ </actionGroup> -+ <actionGroup ref="deleteProductUsingProductGrid" stepKey="deleteProduct2"> -+ <argument name="product" value="SimpleProduct"/> -+ </actionGroup> -+ <actionGroup ref="NavigateToAndResetProductGridToDefaultView" stepKey="NavigateToAndResetProductGridToDefaultView"/> -+ <actionGroup ref="logout" stepKey="logout"/> -+ </after> -+ <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="amOnProductList"/> -+ <waitForPageLoad stepKey="waitForProductList"/> -+ <!--Create Default Product--> -+ <click selector="{{AdminProductGridActionSection.addProductBtn}}" stepKey="clickAddDefaultProduct"/> -+ <fillField selector="{{AdminProductFormSection.productName}}" userInput="{{SimpleProduct.name}}" stepKey="fillDefaultProductName"/> -+ <fillField selector="{{AdminProductFormSection.productSku}}" userInput="{{SimpleProduct.sku}}" stepKey="fillDefaultProductSku"/> -+ <fillField selector="{{AdminProductFormSection.productPrice}}" userInput="{{SimpleProduct.price}}" stepKey="fillDefaultProductPrice"/> -+ <scrollTo selector="{{AdminProductFormBundleSection.seoDropdown}}" stepKey="scrollToSearchEngine"/> -+ <click selector="{{AdminProductFormBundleSection.seoDropdown}}" stepKey="selectSearchEngineOptimization"/> -+ <fillField selector="{{AdminProductFormBundleSection.urlKey}}" userInput="{{SimpleProduct.urlKey}}" stepKey="fillUrlKey"/> -+ <click selector="{{AdminProductFormActionSection.saveButton}}" stepKey="clickSaveDefaultProduct"/> -+ <waitForPageLoad stepKey="waitForPDefaultProductSaved"/> -+ <see selector="{{AdminCategoryMessagesSection.SuccessMessage}}" userInput="You saved the product." stepKey="successMessageYouSavedTheProductIsShown"/> -+ <!--Create product with grid filter Not Visible Individually--> -+ <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="ProductList"/> -+ <waitForPageLoad stepKey="waitForProductPage"/> -+ <click selector="{{AdminProductGridActionSection.addProductBtn}}" stepKey="clickAddFilterProduct"/> -+ <fillField selector="{{AdminProductFormSection.productName}}" userInput="{{defaultSimpleProduct.name}}" stepKey="fillProductName"/> -+ <fillField selector="{{AdminProductFormSection.productSku}}" userInput="{{defaultSimpleProduct.sku}}" stepKey="fillProductSku"/> -+ <fillField selector="{{AdminProductFormSection.productPrice}}" userInput="{{defaultSimpleProduct.price}}" stepKey="fillProductPrice"/> -+ <selectOption selector="{{AdminCategoryProductsGridSection.productVisibility}}" userInput="Not Visible Individually" stepKey="selectProductVisibility"/> -+ <scrollTo selector="{{AdminProductFormBundleSection.seoDropdown}}" stepKey="scrollToSearchEngineOptimization"/> -+ <click selector="{{AdminProductFormBundleSection.seoDropdown}}" stepKey="selectSearchEngineOptimization1"/> -+ <fillField selector="{{AdminProductFormBundleSection.urlKey}}" userInput="{{defaultSimpleProduct.urlKey}}" stepKey="fillUrlKey1"/> -+ <click selector="{{AdminProductFormActionSection.saveButton}}" stepKey="clickSaveProduct"/> -+ <waitForPageLoad stepKey="waitForProductSaved"/> -+ <see selector="{{AdminCategoryMessagesSection.SuccessMessage}}" userInput="You saved the product." stepKey="messageYouSavedTheProductIsShown"/> -+ <amOnPage url="{{AdminCategoryPage.url}}" stepKey="openAdminCategoryIndexPage"/> -+ <waitForPageLoad stepKey="waitForPageToLoaded"/> -+ <!--Create sub category--> -+ <click selector="{{AdminCategorySidebarActionSection.AddSubcategoryButton}}" stepKey="clickOnAddSubCategoryButton"/> -+ <checkOption selector="{{AdminCategoryBasicFieldSection.EnableCategory}}" stepKey="enableCategory"/> -+ <fillField selector="{{AdminCategoryBasicFieldSection.CategoryNameInput}}" userInput="{{_defaultCategory.name}}" stepKey="fillCategoryName"/> -+ <!--Select the default product and product with grid filter--> -+ <scrollTo selector="{{AdminCategoryBasicFieldSection.productsInCategory}}" x="0" y="-80" stepKey="scrollToProductInCategory"/> -+ <click selector="{{AdminCategoryBasicFieldSection.productsInCategory}}" stepKey="clickOnProductInCategory"/> -+ <fillField selector="{{AdminCategoryContentSection.productTableColumnName}}" userInput="{{SimpleProduct.name}}" stepKey="selectProduct"/> -+ <click selector="{{AdminCategoryContentSection.productSearch}}" stepKey="clickSearchButton"/> -+ <click selector="{{AdminCategoryContentSection.productTableRow}}" stepKey="selectProductFromRow"/> -+ <fillField selector="{{AdminCategoryContentSection.productTableColumnName}}" userInput="{{defaultSimpleProduct.name}}" stepKey="selectDefaultProduct"/> -+ <click selector="{{AdminCategoryContentSection.productSearch}}" stepKey="clickSearchButton1"/> -+ <click selector="{{AdminCategoryContentSection.productTableRow}}" stepKey="selectDefaultProductFromTableRow"/> -+ <click selector="{{AdminCategoryMainActionsSection.SaveButton}}" stepKey="clickSaveButton"/> -+ <waitForPageLoad stepKey="WaitForCategorySaved"/> -+ <see selector="{{AdminCategoryMessagesSection.SuccessMessage}}" userInput="You saved the category." stepKey="successMessageYouSavedTheCategory"/> -+ <!--Verify product with grid filter is not not visible--> -+ <amOnPage url="{{StorefrontProductPage.url(defaultSimpleProduct.urlKey)}}" stepKey="seeOnProductPage"/> -+ <waitForPageLoad stepKey="waitForStoreFrontProductPageToLoad"/> -+ <dontSee selector="{{StorefrontProductInfoMainSection.productName}}" userInput="{{defaultSimpleProduct.name}}" stepKey="dontSeeProductInStoreFrontPage"/> -+ <!--Verify product in Store Front Page--> -+ <amOnPage url="{{StorefrontProductPage.url(SimpleProduct.urlKey)}}" stepKey="seeDefaultProductPage"/> -+ <waitForPageLoad stepKey="waitForStoreFrontProductPageToLoad1"/> -+ <see selector="{{StorefrontProductInfoMainSection.productName}}" userInput="{{SimpleProduct.name}}" stepKey="seeProductInStoreFrontPage"/> -+ </test> -+</tests> -diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateCategoryWithRequiredFieldsTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateCategoryWithRequiredFieldsTest.xml -new file mode 100644 -index 00000000000..1b6c9707b06 ---- /dev/null -+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateCategoryWithRequiredFieldsTest.xml -@@ -0,0 +1,45 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> -+ <test name="AdminCreateCategoryWithRequiredFieldsTest"> -+ <annotations> -+ <stories value="Create categories"/> -+ <title value="Create Category from Category page with Required Fields Only"/> -+ <description value="Login as an admin and create a category with required fields."/> -+ <testCaseId value="MC-5265"/> -+ <severity value="CRITICAL"/> -+ <group value="Catalog"/> -+ <group value="mtf_migrated"/> -+ </annotations> -+ <before> -+ <actionGroup ref="LoginAsAdmin" stepKey="loginToAdminPanel"/> -+ </before> -+ <after> -+ <actionGroup ref="DeleteCategory" stepKey="deleteCategory"/> -+ <actionGroup ref="logout" stepKey="logout"/> -+ </after> -+ <!-- Create subcategory with required fields --> -+ <amOnPage url="{{AdminCategoryPage.url}}" stepKey="openAdminCategoryIndexPage"/> -+ <waitForPageLoad stepKey="waitForCategoryIndexPageToBeLoaded"/> -+ <click selector="{{AdminCategorySidebarActionSection.AddSubcategoryButton}}" stepKey="clickOnAddSubCategoryButton"/> -+ <fillField selector="{{AdminCategoryBasicFieldSection.CategoryNameInput}}" userInput="{{_defaultCategory.name}}" stepKey="fillCategoryName"/> -+ <checkOption selector="{{AdminCategoryBasicFieldSection.EnableCategory}}" stepKey="enableCategory"/> -+ <click selector="{{AdminCategoryMainActionsSection.SaveButton}}" stepKey="clickSaveButton"/> -+ <waitForPageLoad stepKey="waitForCategorySaved"/> -+ <!-- Verify success message --> -+ <see selector="{{AdminCategoryMessagesSection.SuccessMessage}}" userInput="You saved the category." stepKey="assertSuccessMessage"/> -+ <!-- Verify subcategory created with required fields --> -+ <see selector="{{AdminCategoryContentSection.categoryPageTitle}}" userInput="{{_defaultCategory.name}}" stepKey="seePageTitle" /> -+ <seeElement selector="{{AdminCategoryContentSection.activeCategoryInTree(_defaultCategory.name)}}" stepKey="seeCategoryInTree" /> -+ <!--Verify Category is listed in store front page menu/>--> -+ <amOnPage url="{{StorefrontCategoryPage.url(_defaultCategory.name)}}" stepKey="amOnCategoryPage"/> -+ <waitForPageLoad stepKey="waitForPageToBeLoaded"/> -+ <see selector="{{StorefrontCategoryMainSection.CategoryTitle}}" userInput="{{_defaultCategory.name}}" stepKey="seeCategoryPageTitle"/> -+ </test> -+</tests> -diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateCustomProductAttributeWithDropdownFieldTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateCustomProductAttributeWithDropdownFieldTest.xml -new file mode 100644 -index 00000000000..a3f543e9cf3 ---- /dev/null -+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateCustomProductAttributeWithDropdownFieldTest.xml -@@ -0,0 +1,142 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> -+ <test name="AdminCreateCustomProductAttributeWithDropdownFieldTest"> -+ <annotations> -+ <stories value="Create product Attribute"/> -+ <title value="Create Custom Product Attribute Dropdown Field (Not Required) from Product Page"/> -+ <description value="login as admin and create configurable product attribute with Dropdown field"/> -+ <severity value="CRITICAL"/> -+ <testCaseId value="MC-10905"/> -+ <group value="mtf_migrated"/> -+ <skip> -+ <issueId value="MC-15474"/> -+ </skip> -+ </annotations> -+ -+ <before> -+ <!-- Login as admin --> -+ <actionGroup ref="LoginAsAdmin" stepKey="login"/> -+ -+ <!--Create Category--> -+ <createData entity="SimpleSubCategory" stepKey="createCategory"/> -+ -+ <!--Create Configurable Product--> -+ <createData entity="ApiConfigurableProduct" stepKey="createConfigProduct"> -+ <requiredEntity createDataKey="createCategory"/> -+ </createData> -+ </before> -+ <after> -+ <!--Delete created entity --> -+ <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> -+ <deleteData createDataKey="createConfigProduct" stepKey="deleteConfigProduct"/> -+ -+ <actionGroup ref="deleteProductAttribute" stepKey="deleteCreatedAttribute"> -+ <argument name="ProductAttribute" value="newProductAttribute"/> -+ </actionGroup> -+ -+ <actionGroup ref="logout" stepKey="logout"/> -+ </after> -+ -+ <!-- Open Product Index Page--> -+ <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="navigateToProductIndex"/> -+ <waitForPageLoad stepKey="waitForProductIndexPageToLoad"/> -+ -+ <!-- Select Created Product--> -+ <actionGroup ref="filterProductGridBySku" stepKey="filterProductGridBySku"> -+ <argument name="product" value="$$createConfigProduct$$"/> -+ </actionGroup> -+ <click stepKey="openFirstProduct" selector="{{AdminProductGridSection.productRowBySku($$createConfigProduct.sku$$)}}"/> -+ <waitForPageLoad stepKey="waitForProductToLoad"/> -+ -+ <fillField selector="{{AdminProductFormSection.productQuantity}}" userInput="100" stepKey="fillProductQty"/> -+ <selectOption selector="{{AdminProductFormSection.productStockStatus}}" userInput="In Stock" stepKey="selectStockStatus"/> -+ -+ <!-- Create New Product Attribute --> -+ <click selector="{{AdminProductFormSection.addAttributeBtn}}" stepKey="clickOnAddAttribute"/> -+ <waitForPageLoad stepKey="waitForAttributePageToLoad"/> -+ <click selector="{{AdminProductFormSection.createNewAttributeBtn}}" stepKey="clickCreateNewAttributeButton"/> -+ <waitForPageLoad stepKey="waitForNewAttributePageToLoad"/> -+ <waitForElementVisible selector="{{AdminCreateNewProductAttributeSection.defaultLabel}}" stepKey="waitForDefaultLabelToBeVisible"/> -+ <fillField selector="{{AdminCreateNewProductAttributeSection.defaultLabel}}" userInput="{{ProductAttributeFrontendLabel.label}}" stepKey="fillAttributeLabel"/> -+ <selectOption selector="{{AdminCreateNewProductAttributeSection.inputType}}" userInput="Dropdown" stepKey="selectInputType"/> -+ <waitForElementVisible selector="{{AdminCreateNewProductAttributeSection.addValue}}" stepKey="waitForAddValueButtonToVisible"/> -+ <click selector="{{AdminCreateNewProductAttributeSection.addValue}}" stepKey="clickOnAddValueButton"/> -+ <waitForElementVisible selector="{{AdminCreateNewProductAttributeSection.defaultStoreView('0')}}" stepKey="waitForDefaultStoreViewToVisible"/> -+ <fillField selector="{{AdminCreateNewProductAttributeSection.defaultStoreView('0')}}" userInput="{{ProductAttributeOption8.label}}" stepKey="fillDefaultStoreView"/> -+ <fillField selector="{{AdminCreateNewProductAttributeSection.adminOption('0')}}" userInput="{{ProductAttributeOption8.value}}" stepKey="fillAdminField"/> -+ <checkOption selector="{{AdminCreateNewProductAttributeSection.defaultRadioButton('1')}}" stepKey="selectRadioButton"/> -+ <click selector="{{AdminCreateNewProductAttributeSection.advancedAttributeProperties}}" stepKey="clickOnAdvancedAttributeProperties"/> -+ <waitForElementVisible selector="{{AdminCreateNewProductAttributeSection.attributeCode}}" stepKey="waitForAttributeCodeToVisible"/> -+ <scrollTo selector="{{AdminCreateNewProductAttributeSection.attributeCode}}" stepKey="scrollToAttributeCode"/> -+ <fillField selector="{{AdminCreateNewProductAttributeSection.attributeCode}}" userInput="{{newProductAttribute.attribute_code}}" stepKey="fillAttributeCode"/> -+ <scrollTo selector="{{AdminCreateNewProductAttributeSection.isUnique}}" stepKey="scrollToIsUniqueOption"/> -+ <checkOption selector="{{AdminCreateNewProductAttributeSection.isUnique}}" stepKey="enableIsUniqueOption"/> -+ <scrollTo selector="{{AdminCreateNewProductAttributeSection.advancedAttributeProperties}}" stepKey="scrollToAdvancedAttributeProperties"/> -+ <click selector="{{AdminCreateNewProductAttributeSection.advancedAttributeProperties}}" stepKey="clickOnAdvancedAttributeProperties1"/> -+ <scrollTo selector="{{AdminCreateNewProductAttributeSection.storefrontProperties}}" stepKey="scrollToStorefrontProperties"/> -+ <click selector="{{AdminCreateNewProductAttributeSection.storefrontProperties}}" stepKey="clickOnStorefrontProperties"/> -+ <waitForPageLoad stepKey="waitForStoreFrontPropertiesTodiaplay"/> -+ <scrollTo selector="{{AdminCreateNewProductAttributeSection.sortProductListing}}" x="0" y="-80" stepKey="scroll1ToSortProductListing"/> -+ <checkOption selector="{{AdminCreateNewProductAttributeSection.inSearch}}" stepKey="enableInSearchOption"/> -+ <checkOption selector="{{AdminCreateNewProductAttributeSection.advancedSearch}}" stepKey="enableAdvancedSearch"/> -+ <checkOption selector="{{AdminCreateNewProductAttributeSection.isComparable}}" stepKey="enableComparableOption"/> -+ <checkOption selector="{{AdminCreateNewProductAttributeSection.allowHtmlTags}}" stepKey="enableAllowHtmlTags"/> -+ <checkOption selector="{{AdminCreateNewProductAttributeSection.visibleOnStorefront}}" stepKey="enableVisibleOnStorefront"/> -+ <checkOption selector="{{AdminCreateNewProductAttributeSection.sortProductListing}}" stepKey="enableSortProductListing"/> -+ <scrollToTopOfPage stepKey="scrollToTopOfPage"/> -+ <click selector="{{AdminCreateNewProductAttributeSection.saveAttribute}}" stepKey="clickOnSaveAttribute"/> -+ <waitForPageLoad stepKey="waitForAttributeToSave"/> -+ <click selector="{{AdminProductFormSection.save}}" stepKey="saveTheProduct"/> -+ <waitForPageLoad stepKey="waitForProductToSave"/> -+ <see selector="{{AdminCategoryMessagesSection.SuccessMessage}}" userInput="You saved the product." stepKey="messageYouSavedTheProductIsShown"/> -+ -+ <!--Verify product attribute added in product form --> -+ <scrollTo selector="{{AdminProductFormSection.contentTab}}" stepKey="scrollToContentTab"/> -+ <waitForElementVisible selector="{{AdminProductFormSection.attributeTab}}" stepKey="waitForAttributeToVisible"/> -+ <click selector="{{AdminProductFormSection.attributeTab}}" stepKey="clickOnAttribute"/> -+ <seeElement selector="{{AdminProductFormSection.attributeLabelByText(ProductAttributeFrontendLabel.label)}}" stepKey="seeAttributeLabelInProductForm"/> -+ -+ <!--Verify Product Attribute in Attribute Form --> -+ <amOnPage url="{{AdminProductAttributeGridPage.url}}" stepKey="navigateToProductAttributeGrid"/> -+ <fillField selector="{{AdminProductAttributeGridSection.FilterByAttributeCode}}" userInput="{{newProductAttribute.attribute_code}}" stepKey="setAttributeCode"/> -+ <click selector="{{AdminProductAttributeGridSection.Search}}" stepKey="searchForAttributeFromTheGrid"/> -+ <waitForPageLoad stepKey="waitForPageLoad" /> -+ <see selector="{{AdminProductAttributeGridSection.attributeCodeColumn}}" userInput="{{newProductAttribute.attribute_code}}" stepKey="seeAttributeCode"/> -+ <see selector="{{AdminProductAttributeGridSection.defaultLabelColumn}}" userInput="{{ProductAttributeFrontendLabel.label}}" stepKey="seeDefaultLabel"/> -+ <see selector="{{AdminProductAttributeGridSection.isVisibleColumn}}" userInput="Yes" stepKey="seeIsVisibleColumn"/> -+ <see selector="{{AdminProductAttributeGridSection.isSearchableColumn}}" userInput="Yes" stepKey="seeSearchableColumn"/> -+ <see selector="{{AdminProductAttributeGridSection.isComparableColumn}}" userInput="Yes" stepKey="seeComparableColumn"/> -+ -+ <!--Verify Product Attribute is present in Category Store Front Page --> -+ <amOnPage url="$$createCategory.name$$.html" stepKey="goToStorefrontPage"/> -+ <waitForPageLoad stepKey="waitForProductFrontPageToLoad"/> -+ <click selector="{{StorefrontHeaderSection.NavigationCategoryByName(SimpleSubCategory.name)}}" stepKey="clickOnCategory"/> -+ <waitForPageLoad stepKey="waitForPageToLoad"/> -+ <click selector="{{StorefrontCategoryMainSection.productLink}}" stepKey="openSearchedProduct"/> -+ <waitForPageLoad stepKey="waitForProductToLoad1"/> -+ <see selector="{{StorefrontProductInfoMainSection.productName}}" userInput="$$createConfigProduct.name$$" stepKey="seeProductNameInStoreFront"/> -+ <see selector="{{StorefrontProductInfoMainSection.productPrice}}" userInput="$$createConfigProduct.price$$" stepKey="seeProductPriceInStoreFront"/> -+ <see selector="{{StorefrontProductInfoMainSection.productSku}}" userInput="$$createConfigProduct.sku$$" stepKey="seeProductSkuInStoreFront"/> -+ <scrollTo selector="{{StorefrontProductMoreInformationSection.moreInformation}}" stepKey="scrollToMoreInformation"/> -+ <see selector="{{StorefrontProductMoreInformationSection.attributeLabel}}" userInput="{{ProductAttributeFrontendLabel.label}}" stepKey="seeAttributeLabel"/> -+ <see selector="{{StorefrontProductMoreInformationSection.attributeValue}}" userInput="{{ProductAttributeOption8.value}}" stepKey="seeAttributeValue"/> -+ -+ <!--Verify Product Attribute present in search page --> -+ <amOnPage url="$$createCategory.name$$.html" stepKey="goToStorefrontPage1"/> -+ <waitForPageLoad stepKey="waitForProductFrontPageToLoad1"/> -+ <fillField selector="{{StorefrontQuickSearchResultsSection.searchTextBox}}" userInput="{{ProductAttributeOption8.value}}" stepKey="fillAttribute"/> -+ <waitForPageLoad stepKey="waitForSearchTextBox"/> -+ <click selector="{{StorefrontQuickSearchResultsSection.searchTextBoxButton}}" stepKey="clickSearchTextBoxButton"/> -+ <waitForPageLoad stepKey="waitForSearchResultToLoad"/> -+ <see selector="{{StorefrontCategoryMainSection.productName}}" userInput="$$createConfigProduct.name$$" stepKey="seeProductNameInCategoryPage"/> -+ <see selector="{{StorefrontCategoryMainSection.productOptionList}}" userInput="{{ProductAttributeFrontendLabel.label}}" stepKey="seeProductAttributeOptionInCategoryPage"/> -+ </test> -+</tests> -diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateDropdownProductAttributeTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateDropdownProductAttributeTest.xml -new file mode 100644 -index 00000000000..525f81de6c4 ---- /dev/null -+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateDropdownProductAttributeTest.xml -@@ -0,0 +1,72 @@ -+<?xml version="1.0"?> -+<!-- -+/** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> -+ <test name="AdminCreateDropdownProductAttributeTest"> -+ <annotations> -+ <features value="Catalog"/> -+ <stories value="Create/configure Dropdown product attribute"/> -+ <title value="Admin should be able to create dropdown product attribute"/> -+ <description value="Admin should be able to create dropdown product attribute"/> -+ <severity value="CRITICAL"/> -+ <testCaseId value="MC-4982"/> -+ <group value="Catalog"/> -+ </annotations> -+ <before> -+ <actionGroup ref="LoginAsAdmin" stepKey="login"/> -+ </before> -+ <after> -+ <!-- Remove attribute --> -+ <actionGroup ref="deleteProductAttribute" stepKey="deleteAttribute"> -+ <argument name="ProductAttribute" value="productDropDownAttribute"/> -+ </actionGroup> -+ <actionGroup ref="logout" stepKey="logout"/> -+ </after> -+ -+ <amOnPage url="{{ProductAttributePage.url}}" stepKey="navigateToNewProductAttributePage"/> -+ <waitForPageLoad stepKey="waitForPageLoad"/> -+ -+ <!-- Set attribute properties --> -+ <fillField selector="{{AttributePropertiesSection.DefaultLabel}}" -+ userInput="{{ProductAttributeFrontendLabel.label}}" stepKey="fillDefaultLabel"/> -+ <selectOption selector="{{AttributePropertiesSection.InputType}}" -+ userInput="{{productDropDownAttribute.frontend_input}}" stepKey="fillInputType"/> -+ -+ <!-- Set advanced attribute properties --> -+ <click selector="{{AdvancedAttributePropertiesSection.AdvancedAttributePropertiesSectionToggle}}" -+ stepKey="showAdvancedAttributePropertiesSection"/> -+ <waitForElementVisible selector="{{AdvancedAttributePropertiesSection.AttributeCode}}" -+ stepKey="waitForSlideOut"/> -+ <fillField selector="{{AdvancedAttributePropertiesSection.AttributeCode}}" -+ userInput="{{productDropDownAttribute.attribute_code}}" -+ stepKey="fillAttributeCode"/> -+ -+ <!-- Add new attribute options --> -+ <click selector="{{AttributeOptionsSection.AddOption}}" stepKey="clickAddOption1"/> -+ <fillField selector="{{DropdownAttributeOptionsSection.nthOptionAdminLabel('1')}}" -+ userInput="Fish and Chips" stepKey="fillAdminValue1"/> -+ -+ <click selector="{{AttributeOptionsSection.AddOption}}" stepKey="clickAddOption2"/> -+ <fillField selector="{{DropdownAttributeOptionsSection.nthOptionAdminLabel('2')}}" -+ userInput="Fish & Chips" stepKey="fillAdminValue2"/> -+ -+ <!-- Save the new product attribute --> -+ <click selector="{{AttributePropertiesSection.Save}}" stepKey="clickSave1"/> -+ <waitForPageLoad stepKey="waitForGridPageLoad1"/> -+ <seeElement selector="{{AdminProductMessagesSection.successMessage}}" -+ stepKey="waitForSuccessMessage"/> -+ -+ <actionGroup ref="navigateToCreatedProductAttribute" stepKey="navigateToAttribute"> -+ <argument name="ProductAttribute" value="productDropDownAttribute"/> -+ </actionGroup> -+ <!-- Check attribute data --> -+ <grabValueFrom selector="{{DropdownAttributeOptionsSection.nthOptionAdminLabel('2')}}" -+ stepKey="secondOptionAdminLabel"/> -+ <assertEquals actual="$secondOptionAdminLabel" expected="'Fish & Chips'" -+ stepKey="assertSecondOption"/> -+ </test> -+</tests> -\ No newline at end of file -diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateDropdownProductAttributeVisibleInStorefrontAdvancedSearchFormTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateDropdownProductAttributeVisibleInStorefrontAdvancedSearchFormTest.xml -new file mode 100644 -index 00000000000..1bc69be642a ---- /dev/null -+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateDropdownProductAttributeVisibleInStorefrontAdvancedSearchFormTest.xml -@@ -0,0 +1,87 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> -+ <test name="AdminCreateDropdownProductAttributeVisibleInStorefrontAdvancedSearchFormTest"> -+ <annotations> -+ <features value="Catalog"/> -+ <stories value="Create product Dropdown attribute and check its visibility on frontend in Advanced Search form"/> -+ <title value="AdminCreateDropdownProductAttributeVisibleInStorefrontAdvancedSearchFormTest"/> -+ <description value="Admin should able to create product Dropdown attribute and check its visibility on frontend in Advanced Search form"/> -+ <testCaseId value="MC-10827"/> -+ <severity value="CRITICAL"/> -+ <group value="mtf_migrated"/> -+ </annotations> -+ -+ <before> -+ <!-- Create product attribute with 2 options --> -+ <createData entity="productDropDownAttributeNotSearchable" stepKey="attribute"/> -+ <createData entity="productAttributeOption1" stepKey="option1"> -+ <requiredEntity createDataKey="attribute"/> -+ </createData> -+ <createData entity="productAttributeOption2" stepKey="option2"> -+ <requiredEntity createDataKey="attribute"/> -+ </createData> -+ -+ <!-- Create product attribute set --> -+ <createData entity="CatalogAttributeSet" stepKey="createAttributeSet"/> -+ <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> -+ </before> -+ <after> -+ <actionGroup ref="logout" stepKey="logout"/> -+ </after> -+ -+ <!-- Filter product attribute set by attribute set name --> -+ <amOnPage url="{{AdminProductAttributeSetGridPage.url}}" stepKey="amOnAttributeSetPage"/> -+ <actionGroup ref="FilterProductAttributeSetGridByAttributeSetName" stepKey="filterProductAttrSetGridByAttrSetName"> -+ <argument name="name" value="$$createAttributeSet.attribute_set_name$$"/> -+ </actionGroup> -+ -+ <!-- Assert created attribute in an unassigned attributes --> -+ <see userInput="$$attribute.attribute_code$$" selector="{{AdminProductAttributeSetEditSection.unassignedAttributesTree}}" stepKey="seeAttributeInUnassignedAttr"/> -+ -+ <!-- Assign attribute in the group --> -+ <actionGroup ref="AssignAttributeToGroup" stepKey="assignAttributeToGroup"> -+ <argument name="group" value="Product Details"/> -+ <argument name="attribute" value="$$attribute.attribute_code$$"/> -+ </actionGroup> -+ <see userInput="$$attribute.attribute_code$$" selector="{{AdminProductAttributeSetEditSection.groupTree}}" stepKey="seeAttributeInGroup"/> -+ <actionGroup ref="SaveAttributeSet" stepKey="saveAttributeSet"/> -+ -+ <!-- Go to Product Attribute Grid page --> -+ <amOnPage url="{{AdminProductAttributeGridPage.url}}" stepKey="navigateToProductAttributeGrid"/> -+ <fillField selector="{{AdminProductAttributeGridSection.FilterByAttributeCode}}" userInput="$$attribute.attribute_code$$" stepKey="fillAttrCodeField" /> -+ <click selector="{{AdminProductAttributeGridSection.Search}}" stepKey="clickSearchBtn" /> -+ <click selector="{{AdminProductAttributeGridSection.FirstRow}}" stepKey="chooseFirstRow" /> -+ -+ <!-- Change attribute property: Frontend Label --> -+ <fillField selector="{{AttributePropertiesSection.DefaultLabel}}" userInput="{{productDropDownAttribute.attribute_code}}" stepKey="fillDefaultLabel"/> -+ -+ <!-- Change attribute property: Use in Search >Yes --> -+ <scrollToTopOfPage stepKey="scrollToTabs"/> -+ <click selector="{{StorefrontPropertiesSection.StoreFrontPropertiesTab}}" stepKey="clickStorefrontPropertiesTab"/> -+ <waitForPageLoad stepKey="waitForPageLoad1"/> -+ <selectOption selector="{{AdvancedAttributePropertiesSection.UseInSearch}}" userInput="Yes" stepKey="seeInSearch"/> -+ -+ <!-- Change attribute property: Visible In Advanced Search >No --> -+ <waitForPageLoad stepKey="waitForPageLoad2"/> -+ <selectOption selector="{{AdvancedAttributePropertiesSection.VisibleInAdvancedSearch}}" userInput="No" stepKey="dontSeeInAdvancedSearch"/> -+ -+ <!-- Save the new product attributes --> -+ <click selector="{{AttributePropertiesSection.Save}}" stepKey="clickSave"/> -+ <seeElement selector="{{AdminProductMessagesSection.successMessage}}" stepKey="seeSuccessMessage"/> -+ -+ <!-- Flash cache --> -+ <magentoCLI command="cache:flush" stepKey="flushCache"/> -+ -+ <!-- Go to store's advanced catalog search page --> -+ <actionGroup ref="GoToStoreViewAdvancedCatalogSearchActionGroup" stepKey="GoToStoreViewAdvancedCatalogSearchActionGroup"/> -+ <dontSeeElement selector="{{StorefrontCatalogSearchAdvancedFormSection.AttributeByCode('$$attribute.attribute_code$$')}}" stepKey="dontSeeAttribute"/> -+ </test> -+</tests> -diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateDuplicateCategoryTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateDuplicateCategoryTest.xml -new file mode 100644 -index 00000000000..37ec4e0d325 ---- /dev/null -+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateDuplicateCategoryTest.xml -@@ -0,0 +1,41 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> -+ <test name="AdminCreateDuplicateCategoryTest"> -+ <annotations> -+ <stories value="Create category"/> -+ <title value="Create Duplicate Category With Already Existed UrlKey"/> -+ <description value="Login as admin and create duplicate category"/> -+ <testCaseId value="MC-14702"/> -+ <severity value="CRITICAL"/> -+ <group value="mtf_migrated"/> -+ </annotations> -+ -+ <before> -+ <actionGroup ref="LoginAsAdmin" stepKey="loginToAdminPanel"/> -+ <createData entity="SimpleSubCategory" stepKey="category"/> -+ </before> -+ <after> -+ <deleteData createDataKey="category" stepKey="deleteCategory"/> -+ <actionGroup ref="logout" stepKey="logout"/> -+ </after> -+ -+ <!-- Open Category Page and select Add category --> -+ <actionGroup ref="goToCreateCategoryPage" stepKey="goToCategoryPage"/> -+ -+ <!-- Fill the Category form with same name and urlKey as initially created category(SimpleSubCategory) --> -+ <actionGroup ref="FillCategoryNameAndUrlKeyAndSave" stepKey="fillCategoryForm"> -+ <argument name="categoryName" value="$$category.name$$"/> -+ <argument name="categoryUrlKey" value="$$category.custom_attributes[url_key]$$"/> -+ </actionGroup> -+ -+ <!-- Assert error message --> -+ <see selector="{{AdminCategoryMessagesSection.errorMessage}}" userInput="The value specified in the URL Key field would generate a URL that already exists." stepKey="seeErrorMessage"/> -+ </test> -+</tests> -diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateDuplicateProductTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateDuplicateProductTest.xml -new file mode 100644 -index 00000000000..575bb56912b ---- /dev/null -+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateDuplicateProductTest.xml -@@ -0,0 +1,61 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> -+ <test name="AdminCreateDuplicateProductTest"> -+ <annotations> -+ <stories value="Create Product"/> -+ <title value="Create Duplicate Product With Existed Subcategory Name And UrlKey"/> -+ <description value="Login as admin and create duplicate Product"/> -+ <testCaseId value="MC-14714"/> -+ <severity value="CRITICAL"/> -+ <group value="mtf_migrated"/> -+ </annotations> -+ -+ <before> -+ <createData entity="SubCategory" stepKey="category"/> -+ <createData entity="Two_nested_categories" stepKey="subCategory"> -+ <requiredEntity createDataKey="category"/> -+ </createData> -+ <actionGroup ref="LoginAsAdmin" stepKey="login"/> -+ </before> -+ -+ <after> -+ <deleteData createDataKey="subCategory" stepKey="deleteSubCategory"/> -+ <deleteData createDataKey="category" stepKey="deleteCategory"/> -+ <actionGroup ref="logout" stepKey="logout"/> -+ </after> -+ -+ <!-- Go to new simple product page --> -+ <actionGroup ref="GoToSpecifiedCreateProductPage" stepKey="goToCreateProductPage"/> -+ -+ <!-- Fill the main fields in the form --> -+ <actionGroup ref="FillMainProductFormByString" stepKey="fillMainProductForm"> -+ <argument name="productName" value="$$subCategory.name$$"/> -+ <argument name="productSku" value="{{defaultSimpleProduct.sku}}"/> -+ <argument name="productPrice" value="{{defaultSimpleProduct.price}}"/> -+ <argument name="productQuantity" value="{{defaultSimpleProduct.quantity}}"/> -+ <argument name="productStatus" value="{{defaultSimpleProduct.status}}"/> -+ <argument name="productWeight" value="{{defaultSimpleProduct.weight}}"/> -+ </actionGroup> -+ -+ <!-- Select the category that we created in the before block --> -+ <actionGroup ref="SetCategoryByName" stepKey="setCategory"> -+ <argument name="categoryName" value="$$category.name$$"/> -+ </actionGroup> -+ -+ <!-- Set the url key to match the subcategory created in the before block --> -+ <actionGroup ref="SetProductUrlKeyByString" stepKey="fillUrlKey"> -+ <argument name="urlKey" value="$$subCategory.custom_attributes[url_key]$$"/> -+ </actionGroup> -+ -+ <!-- Save the product and expect to see an error message --> -+ <actionGroup ref="SaveProductFormNoSuccessCheck" stepKey="tryToSaveProduct"/> -+ <see selector="{{AdminProductFormSection.successMessage}}" userInput="The value specified in the URL Key field would generate a URL that already exists." stepKey="seeErrorMessage"/> -+ </test> -+</tests> -diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateInactiveFlatCategoryAndUpdateAsInactiveTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateInactiveFlatCategoryAndUpdateAsInactiveTest.xml -new file mode 100644 -index 00000000000..21b3dba7140 ---- /dev/null -+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateInactiveFlatCategoryAndUpdateAsInactiveTest.xml -@@ -0,0 +1,90 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> -+ <test name="AdminCreateInactiveFlatCategoryAndUpdateAsInactiveTest"> -+ <annotations> -+ <stories value="Create category"/> -+ <title value="Flat Catalog - Update Inactive Category as Inactive, Should Not be Visible on Storefront"/> -+ <description value="Login as admin and create inactive flat category and update category as inactive and verify category is not visible in store front"/> -+ <severity value="CRITICAL"/> -+ <testCaseId value="MC-11009"/> -+ <group value="mtf_migrated"/> -+ </annotations> -+ <before> -+ <!--Login as admin --> -+ <actionGroup ref="LoginAsAdmin" stepKey="loginToAdminPanel"/> -+ <!--Create category--> -+ <createData entity="CatNotActive" stepKey="createCategory"/> -+ <!-- Create First StoreView --> -+ <actionGroup ref="CreateStoreView" stepKey="createCustomStoreViewEn"> -+ <argument name="storeView" value="customStoreEN"/> -+ </actionGroup> -+ <!-- Create Second StoreView --> -+ <actionGroup ref="CreateStoreView" stepKey="createCustomStoreViewFr"> -+ <argument name="storeView" value="customStoreFR"/> -+ </actionGroup> -+ <!--Run full reindex and clear caches --> -+ <magentoCLI command="indexer:reindex" stepKey="reindex"/> -+ <magentoCLI command="cache:flush" stepKey="flushCache"/> -+ <!--Enable Flat Catalog Category --> -+ <magentoCLI stepKey="setFlatCatalogCategory" command="config:set catalog/frontend/flat_catalog_category 1"/> -+ <!--Open Index Management Page and Select Index mode "Update by Schedule" --> -+ <magentoCLI stepKey="setIndexerMode" command="indexer:set-mode" arguments="schedule" /> -+ <!-- Run cron twice --> -+ <magentoCLI command="cron:run" stepKey="runCron1"/> -+ <magentoCLI command="cron:run" stepKey="runCron2"/> -+ </before> -+ <after> -+ <magentoCLI stepKey="setFlatCatalogCategory" command="config:set catalog/frontend/flat_catalog_category 0 "/> -+ <magentoCLI stepKey="setIndexerMode" command="indexer:set-mode" arguments="realtime" /> -+ <magentoCLI stepKey="indexerReindex" command="indexer:reindex" /> -+ <deleteData stepKey="deleteCategory" createDataKey="createCategory"/> -+ <actionGroup ref="AdminDeleteStoreViewActionGroup" stepKey="deleteStoreViewEn"> -+ <argument name="customStore" value="customStoreEN"/> -+ </actionGroup> -+ <actionGroup ref="AdminDeleteStoreViewActionGroup" stepKey="deleteStoreViewFr"> -+ <argument name="customStore" value="customStoreFR"/> -+ </actionGroup> -+ <actionGroup ref="logout" stepKey="logout"/> -+ </after> -+ <!-- Select created category and make category inactive--> -+ <amOnPage url="{{AdminCategoryPage.url}}" stepKey="openAdminCategoryIndexPage"/> -+ <waitForPageLoad stepKey="waitForPageToLoad"/> -+ <click selector="{{AdminCategorySidebarTreeSection.expandAll}}" stepKey="clickOnExpandTree"/> -+ <click selector="{{AdminCategorySidebarTreeSection.categoryInTree(CatNotActive.name)}}" stepKey="selectCreatedCategory"/> -+ <waitForPageLoad stepKey="waitForCategoryPageToLoad"/> -+ <click selector="{{AdminCategoryMainActionsSection.SaveButton}}" stepKey="saveSubCategory"/> -+ <waitForPageLoad stepKey="waitForSecondCategoryToSave"/> -+ <seeElement selector="{{AdminCategoryMessagesSection.SuccessMessage}}" stepKey="seeSuccessMessage"/> -+ <see selector="{{AdminCategoryContentSection.categoryPageTitle}}" userInput="{{CatNotActive.name}}" stepKey="seeUpdatedCategoryTitle"/> -+ <dontSeeCheckboxIsChecked selector="{{AdminCategoryBasicFieldSection.enableCategoryLabel}}" stepKey="verifyInactiveCategory"/> -+ <!--Run full reindex and clear caches --> -+ <magentoCLI command="indexer:reindex" stepKey="reindex"/> -+ <magentoCLI command="cache:flush" stepKey="flushCache"/> -+ <!--Open Index Management Page --> -+ <amOnPage url="{{AdminIndexManagementPage.url}}" stepKey="openIndexManagementPage"/> -+ <waitForPageLoad stepKey="waitForIndexPageToBeLoaded"/> -+ <see stepKey="seeIndexStatus" selector="{{AdminIndexManagementSection.indexerStatus('Category Flat Data')}}" userInput="Ready"/> -+ <!--Verify Category In Store Front--> -+ <amOnPage url="/$$createCategory.name$$.html" stepKey="openCategoryPage1"/> -+ <waitForPageLoad stepKey="waitForCategoryStoreFrontPageToLoad"/> -+ <seeElement selector="{{StorefrontBundledSection.pageNotFound}}" stepKey="seeWhoopsOurBadMessage"/> -+ <!--Verify category is not visible in First Store View --> -+ <click stepKey="selectStoreSwitcher" selector="{{StorefrontHeaderSection.storeViewSwitcher}}"/> -+ <click stepKey="selectForstStoreView" selector="{{StorefrontHeaderSection.storeViewList(customStoreEN.name)}}"/> -+ <waitForPageLoad stepKey="waitForFirstStoreView"/> -+ <dontSeeElement selector="{{StorefrontHeaderSection.NavigationCategoryByName($$createCategory.name$$)}}" stepKey="dontSeeCategoryOnNavigation"/> -+ <!--Verify category is not visible in Second Store View --> -+ <click stepKey="selectStoreSwitcher1" selector="{{StorefrontHeaderSection.storeViewSwitcher}}"/> -+ <click stepKey="selectSecondStoreView" selector="{{StorefrontHeaderSection.storeViewList(customStoreFR.name)}}"/> -+ <waitForPageLoad stepKey="waitForSecondStoreView"/> -+ <dontSeeElement selector="{{StorefrontHeaderSection.NavigationCategoryByName($$createCategory.name$$)}}" stepKey="dontSeeCategoryOnNavigation1"/> -+ </test> -+</tests> -diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateInactiveFlatCategoryTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateInactiveFlatCategoryTest.xml -new file mode 100644 -index 00000000000..aa3dba85dfa ---- /dev/null -+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateInactiveFlatCategoryTest.xml -@@ -0,0 +1,92 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> -+ <test name="AdminCreateInactiveFlatCategoryTest"> -+ <annotations> -+ <stories value="Create category"/> -+ <title value="Flat Catalog - Create Category as Inactive, Should Not be Visible on Storefront"/> -+ <description value="Login as admin and create flat Inactive category and verify category is not visible in store front"/> -+ <severity value="CRITICAL"/> -+ <testCaseId value="MC-11007"/> -+ <group value="mtf_migrated"/> -+ </annotations> -+ <before> -+ <!--Login as admin --> -+ <actionGroup ref="LoginAsAdmin" stepKey="loginToAdminPanel"/> -+ <!--Create category--> -+ <createData entity="SimpleSubCategory" stepKey="createCategory"/> -+ <!-- Create First StoreView --> -+ <actionGroup ref="CreateStoreView" stepKey="createCustomStoreViewEn"> -+ <argument name="storeView" value="customStoreEN"/> -+ </actionGroup> -+ <!-- Create Second StoreView --> -+ <actionGroup ref="CreateStoreView" stepKey="createCustomStoreViewFr"> -+ <argument name="storeView" value="customStoreFR"/> -+ </actionGroup> -+ <!--Run full reindex and clear caches --> -+ <magentoCLI command="indexer:reindex" stepKey="reindex"/> -+ <magentoCLI command="cache:flush" stepKey="flushCache"/> -+ <!--Enable Flat Catalog Category --> -+ <magentoCLI stepKey="setFlatCatalogCategory" command="config:set catalog/frontend/flat_catalog_category 1"/> -+ <!--Open Index Management Page and Select Index mode "Update by Schedule" --> -+ <magentoCLI stepKey="setIndexerMode" command="indexer:set-mode" arguments="schedule" /> -+ <!-- Run cron twice --> -+ <magentoCLI command="cron:run" stepKey="runCron1"/> -+ <magentoCLI command="cron:run" stepKey="runCron2"/> -+ </before> -+ <after> -+ <magentoCLI stepKey="setFlatCatalogCategory" command="config:set catalog/frontend/flat_catalog_category 0 "/> -+ <magentoCLI stepKey="setIndexerMode" command="indexer:set-mode" arguments="realtime" /> -+ <magentoCLI stepKey="indexerReindex" command="indexer:reindex" /> -+ <deleteData stepKey="deleteCategory" createDataKey="createCategory"/> -+ <actionGroup ref="AdminDeleteStoreViewActionGroup" stepKey="deleteStoreViewEn"> -+ <argument name="customStore" value="customStoreEN"/> -+ </actionGroup> -+ <actionGroup ref="AdminDeleteStoreViewActionGroup" stepKey="deleteStoreViewFr"> -+ <argument name="customStore" value="customStoreFR"/> -+ </actionGroup> -+ <actionGroup ref="logout" stepKey="logout"/> -+ </after> -+ <!-- Select created category and make category inactive--> -+ <amOnPage url="{{AdminCategoryPage.url}}" stepKey="openAdminCategoryIndexPage"/> -+ <waitForPageLoad stepKey="waitForPageToLoad"/> -+ <click selector="{{AdminCategorySidebarTreeSection.expandAll}}" stepKey="clickOnExpandTree"/> -+ <click selector="{{AdminCategorySidebarTreeSection.categoryInTree(SimpleSubCategory.name)}}" stepKey="selectCreatedCategory"/> -+ <waitForPageLoad stepKey="waitForCategoryPageToLoad"/> -+ <click selector="{{AdminCategoryBasicFieldSection.enableCategoryLabel}}" stepKey="disableActiveCategory"/> -+ <click selector="{{AdminCategoryMainActionsSection.SaveButton}}" stepKey="saveSubCategory"/> -+ <waitForPageLoad stepKey="waitForSecondCategoryToSave"/> -+ <seeElement selector="{{AdminCategoryMessagesSection.SuccessMessage}}" stepKey="seeSuccessMessage"/> -+ <see selector="{{AdminCategoryContentSection.categoryPageTitle}}" userInput="{{SimpleSubCategory.name}}" stepKey="seeUpdatedCategoryTitle"/> -+ <dontSeeCheckboxIsChecked selector="{{AdminCategoryBasicFieldSection.enableCategoryLabel}}" stepKey="verifyInactiveIncludeInMenu"/> -+ <!--Run full reindex and clear caches --> -+ <magentoCLI command="indexer:reindex" stepKey="reindex"/> -+ <magentoCLI command="cache:flush" stepKey="flushCache"/> -+ <!--Open Index Management Page --> -+ <amOnPage url="{{AdminIndexManagementPage.url}}" stepKey="openIndexManagementPage"/> -+ <waitForPageLoad stepKey="waitForIndexPageToBeLoaded"/> -+ <see stepKey="seeIndexStatus" selector="{{AdminIndexManagementSection.indexerStatus('Category Flat Data')}}" userInput="Ready"/> -+ <!--Verify Category In Store Front--> -+ <amOnPage url="/$$createCategory.name$$.html" stepKey="openCategoryPage1"/> -+ <waitForPageLoad stepKey="waitForCategoryStoreFrontPageToLoad"/> -+ <!--Verify category is not visible in First Store View --> -+ <click stepKey="selectStoreSwitcher" selector="{{StorefrontHeaderSection.storeViewSwitcher}}"/> -+ <click stepKey="selectForstStoreView" selector="{{StorefrontHeaderSection.storeViewList(customStoreEN.name)}}"/> -+ <waitForPageLoad stepKey="waitForFirstStoreView"/> -+ <dontSeeElement selector="{{StorefrontHeaderSection.NavigationCategoryByName($$createCategory.name$$)}}" stepKey="dontSeeCategoryOnNavigation"/> -+ <seeElement selector="{{StorefrontBundledSection.pageNotFound}}" stepKey="seeWhoopsOurBadMessage"/> -+ <!--Verify category is not visible in Second Store View --> -+ <click stepKey="selectStoreSwitcher1" selector="{{StorefrontHeaderSection.storeViewSwitcher}}"/> -+ <click stepKey="selectSecondStoreView" selector="{{StorefrontHeaderSection.storeViewList(customStoreFR.name)}}"/> -+ <waitForPageLoad stepKey="waitForSecondstoreView"/> -+ <dontSeeElement selector="{{StorefrontHeaderSection.NavigationCategoryByName($$createCategory.name$$)}}" stepKey="dontSeeCategoryOnNavigation1"/> -+ <seeElement selector="{{StorefrontBundledSection.pageNotFound}}" stepKey="seeWhoopsOurBadMessage1"/> -+ </test> -+</tests> -diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateInactiveInMenuFlatCategoryTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateInactiveInMenuFlatCategoryTest.xml -new file mode 100644 -index 00000000000..37417cd7fdb ---- /dev/null -+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateInactiveInMenuFlatCategoryTest.xml -@@ -0,0 +1,91 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> -+ <test name="AdminCreateInactiveInMenuFlatCategoryTest"> -+ <annotations> -+ <stories value="Create category"/> -+ <title value="Flat Catalog - Exclude Category from Navigation Menu"/> -+ <description value="Login as admin and create inactive Include In Menu flat category and verify category is not displayed in Navigation Menu"/> -+ <severity value="CRITICAL"/> -+ <testCaseId value="MC-11008"/> -+ <group value="mtf_migrated"/> -+ </annotations> -+ <before> -+ <!--Login as admin --> -+ <actionGroup ref="LoginAsAdmin" stepKey="loginToAdminPanel"/> -+ <!--Create category--> -+ <createData entity="SimpleSubCategory" stepKey="category"/> -+ <!-- Create First StoreView --> -+ <actionGroup ref="CreateStoreView" stepKey="createCustomStoreViewEn"> -+ <argument name="storeView" value="customStoreEN"/> -+ </actionGroup> -+ <!-- Create Second StoreView --> -+ <actionGroup ref="CreateStoreView" stepKey="createCustomStoreViewFr"> -+ <argument name="storeView" value="customStoreFR"/> -+ </actionGroup> -+ <!--Run full reindex and clear caches --> -+ <magentoCLI command="indexer:reindex" stepKey="reindex"/> -+ <magentoCLI command="cache:flush" stepKey="flushCache"/> -+ <!--Enable Flat Catalog Category --> -+ <magentoCLI stepKey="setFlatCatalogCategory" command="config:set catalog/frontend/flat_catalog_category 1"/> -+ <!--Open Index Management Page and Select Index mode "Update by Schedule" --> -+ <magentoCLI stepKey="setIndexerMode" command="indexer:set-mode" arguments="schedule" /> -+ <!-- Run cron twice --> -+ <magentoCLI command="cron:run" stepKey="runCron1"/> -+ <magentoCLI command="cron:run" stepKey="runCron2"/> -+ </before> -+ <after> -+ <magentoCLI stepKey="setFlatCatalogCategory" command="config:set catalog/frontend/flat_catalog_category 0 "/> -+ <magentoCLI stepKey="setIndexerMode" command="indexer:set-mode" arguments="realtime" /> -+ <magentoCLI stepKey="indexerReindex" command="indexer:reindex" /> -+ <actionGroup ref="AdminDeleteStoreViewActionGroup" stepKey="deleteStoreViewEn"> -+ <argument name="customStore" value="customStoreEN"/> -+ </actionGroup> -+ <actionGroup ref="AdminDeleteStoreViewActionGroup" stepKey="deleteStoreViewFr"> -+ <argument name="customStore" value="customStoreFR"/> -+ </actionGroup> -+ <deleteData createDataKey="category" stepKey="deleteCategory"/> -+ <actionGroup ref="logout" stepKey="logout"/> -+ </after> -+ <!-- Select created category and disable Include In Menu option--> -+ <amOnPage url="{{AdminCategoryPage.url}}" stepKey="openAdminCategoryIndexPage"/> -+ <waitForPageLoad stepKey="waitForPageToLoad"/> -+ <click selector="{{AdminCategorySidebarTreeSection.expandAll}}" stepKey="clickOnExpandTree"/> -+ <click selector="{{AdminCategorySidebarTreeSection.categoryInTree(SimpleSubCategory.name)}}" stepKey="selectCreatedCategory"/> -+ <waitForPageLoad stepKey="waitForCategoryPageToLoad"/> -+ <click selector="{{AdminCategoryBasicFieldSection.includeInMenuLabel}}" stepKey="disableIcludeInMenuOption"/> -+ <click selector="{{AdminCategoryMainActionsSection.SaveButton}}" stepKey="saveSubCategory"/> -+ <waitForPageLoad stepKey="waitForSecondCategoryToSave"/> -+ <!--Verify category is saved and Include In Menu Option is disabled in Category Page --> -+ <seeElement selector="{{AdminCategoryMessagesSection.SuccessMessage}}" stepKey="seeSuccessMessage"/> -+ <see selector="{{AdminCategoryContentSection.categoryPageTitle}}" userInput="{{SimpleSubCategory.name}}" stepKey="seeUpdatedCategoryTitle"/> -+ <dontSeeCheckboxIsChecked selector="{{AdminCategoryBasicFieldSection.includeInMenuLabel}}" stepKey="verifyInactiveIncludeInMenu"/> -+ <!--Run full reindex and clear caches --> -+ <magentoCLI command="indexer:reindex" stepKey="reindex"/> -+ <magentoCLI command="cache:flush" stepKey="flushCache"/> -+ <!--Open Index Management Page --> -+ <amOnPage url="{{AdminIndexManagementPage.url}}" stepKey="openIndexManagementPage"/> -+ <waitForPageLoad stepKey="waitForIndexPageToBeLoaded"/> -+ <see stepKey="seeIndexStatus" selector="{{AdminIndexManagementSection.indexerStatus('Category Flat Data')}}" userInput="Ready"/> -+ <!--Verify Category In Store Front--> -+ <amOnPage url="/$$category.name$$.html" stepKey="openCategoryPage1"/> -+ <waitForPageLoad stepKey="waitForCategoryStoreFrontPageToLoad"/> -+ <!--Verify category is not displayed in navigation menu in First Store View --> -+ <click stepKey="selectStoreSwitcher" selector="{{StorefrontHeaderSection.storeViewSwitcher}}"/> -+ <click stepKey="selectForstStoreView" selector="{{StorefrontHeaderSection.storeViewList(customStoreEN.name)}}"/> -+ <waitForPageLoad stepKey="waitForFirstStoreView"/> -+ <dontSeeElement selector="{{StorefrontHeaderSection.NavigationCategoryByName($$category.name$$)}}" stepKey="seeCategoryOnNavigation"/> -+ <!--Verify category is not displayed in navigation menu in Second Store View --> -+ <click stepKey="selectStoreSwitcher1" selector="{{StorefrontHeaderSection.storeViewSwitcher}}"/> -+ <click stepKey="selectSecondStoreView" selector="{{StorefrontHeaderSection.storeViewList(customStoreFR.name)}}"/> -+ <waitForPageLoad stepKey="waitForSecondstoreView"/> -+ <dontSeeElement selector="{{StorefrontHeaderSection.NavigationCategoryByName($$category.name$$)}}" stepKey="seeCategoryOnNavigation1"/> -+ </test> -+</tests> -diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateMultipleSelectProductAttributeVisibleInStorefrontAdvancedSearchFormTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateMultipleSelectProductAttributeVisibleInStorefrontAdvancedSearchFormTest.xml -new file mode 100644 -index 00000000000..4e096b7ebb1 ---- /dev/null -+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateMultipleSelectProductAttributeVisibleInStorefrontAdvancedSearchFormTest.xml -@@ -0,0 +1,93 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> -+ <test name="AdminCreateMultipleSelectProductAttributeVisibleInStorefrontAdvancedSearchFormTest"> -+ <annotations> -+ <features value="Catalog"/> -+ <stories value="Create Multiple Select product attribute and check its visibility in Advanced Search form"/> -+ <title value="Create product attribute of type Multiple Select and check its visibility on frontend in Advanced Search form"/> -+ <description value="Admin should be able to create product attribute of type Multiple Select and check its visibility on frontend in Advanced Search form"/> -+ <testCaseId value="MC-10828"/> -+ <severity value="CRITICAL"/> -+ <group value="mtf_migrated"/> -+ </annotations> -+ -+ <before> -+ <!-- Create a multiple select product attribute with two options --> -+ <createData entity="productAttributeMultiselectTwoOptionsNotSearchable" stepKey="attribute"/> -+ <createData entity="productAttributeOption1" stepKey="option1"> -+ <requiredEntity createDataKey="attribute"/> -+ </createData> -+ <createData entity="productAttributeOption2" stepKey="option2"> -+ <requiredEntity createDataKey="attribute"/> -+ </createData> -+ -+ <!-- Create product attribute set --> -+ <createData entity="CatalogAttributeSet" stepKey="createAttributeSet"/> -+ <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> -+ </before> -+ <after> -+ <!-- Delete product attribute --> -+ <deleteData createDataKey="attribute" stepKey="deleteProductAttribute"/> -+ -+ <!-- Delete product attribute set --> -+ <deleteData createDataKey="createAttributeSet" stepKey="deleteAttributeSet"/> -+ -+ <actionGroup ref="logout" stepKey="logout"/> -+ </after> -+ -+ <!-- Filter product attribute set by attribute set name --> -+ <amOnPage url="{{AdminProductAttributeSetGridPage.url}}" stepKey="amOnAttributeSetPage"/> -+ <actionGroup ref="FilterProductAttributeSetGridByAttributeSetName" stepKey="filterProductAttrSetGridByAttrSetName"> -+ <argument name="name" value="$$createAttributeSet.attribute_set_name$$"/> -+ </actionGroup> -+ -+ <!-- Assert created attribute in an unassigned attributes --> -+ <see userInput="$$attribute.attribute_code$$" selector="{{AdminProductAttributeSetEditSection.unassignedAttributesTree}}" stepKey="seeAttributeInUnassignedAttr"/> -+ -+ <!-- Assign attribute in the group --> -+ <actionGroup ref="AssignAttributeToGroup" stepKey="assignAttributeToGroup"> -+ <argument name="group" value="Product Details"/> -+ <argument name="attribute" value="$$attribute.attribute_code$$"/> -+ </actionGroup> -+ <see userInput="$$attribute.attribute_code$$" selector="{{AdminProductAttributeSetEditSection.groupTree}}" stepKey="seeAttributeInGroup"/> -+ <actionGroup ref="SaveAttributeSet" stepKey="saveAttributeSet"/> -+ -+ <!-- Go to Product Attribute Grid page --> -+ <amOnPage url="{{AdminProductAttributeGridPage.url}}" stepKey="navigateToProductAttributeGrid"/> -+ <fillField selector="{{AdminProductAttributeGridSection.FilterByAttributeCode}}" userInput="$$attribute.attribute_code$$" stepKey="fillAttrCodeField" /> -+ <click selector="{{AdminProductAttributeGridSection.Search}}" stepKey="clickSearchBtn" /> -+ <click selector="{{AdminProductAttributeGridSection.FirstRow}}" stepKey="chooseFirstRow" /> -+ -+ <!-- Change attribute property: Frontend Label --> -+ <fillField selector="{{AttributePropertiesSection.DefaultLabel}}" userInput="{{productDropDownAttribute.attribute_code}}" stepKey="fillDefaultLabel"/> -+ -+ <!-- Change attribute property: Use in Search >Yes --> -+ <scrollToTopOfPage stepKey="scrollToTabs"/> -+ <click selector="{{StorefrontPropertiesSection.StoreFrontPropertiesTab}}" stepKey="clickStorefrontPropertiesTab"/> -+ <waitForPageLoad stepKey="waitForPageLoad1"/> -+ <selectOption selector="{{AdvancedAttributePropertiesSection.UseInSearch}}" userInput="Yes" stepKey="seeInSearch"/> -+ -+ <!-- Change attribute property: Visible In Advanced Search >No --> -+ <waitForPageLoad stepKey="waitForPageLoad2"/> -+ <selectOption selector="{{AdvancedAttributePropertiesSection.VisibleInAdvancedSearch}}" userInput="No" stepKey="dontSeeInAdvancedSearch"/> -+ -+ <!-- Save the new product attributes --> -+ <click selector="{{AttributePropertiesSection.Save}}" stepKey="clickSave"/> -+ <seeElement selector="{{AdminProductMessagesSection.successMessage}}" stepKey="seeSuccessMessage"/> -+ -+ <!-- Flash cache --> -+ <magentoCLI command="cache:flush" stepKey="flushCache"/> -+ -+ <!-- Go to store's advanced catalog search page --> -+ <actionGroup ref="GoToStoreViewAdvancedCatalogSearchActionGroup" stepKey="GoToStoreViewAdvancedCatalogSearchActionGroup"/> -+ <dontSeeElement selector="{{StorefrontCatalogSearchAdvancedFormSection.AttributeByCode('$$attribute.attribute_code$$')}}" stepKey="dontSeeAttribute"/> -+ </test> -+</tests> -diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateNewAttributeFromProductTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateNewAttributeFromProductTest.xml -new file mode 100644 -index 00000000000..02615ca5dd2 ---- /dev/null -+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateNewAttributeFromProductTest.xml -@@ -0,0 +1,69 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> -+ <test name="AdminCreateNewAttributeFromProductTest"> -+ <annotations> -+ <features value="Catalog"/> -+ <stories value="Product attributes"/> -+ <title value="Check that New Attribute from Product is create"/> -+ <description value="Check that New Attribute from Product is create"/> -+ <severity value="MAJOR"/> -+ <testCaseId value="MC-12296"/> -+ <useCaseId value="MAGETWO-59055"/> -+ <group value="Catalog"/> -+ </annotations> -+ <before> -+ <actionGroup ref="LoginAsAdmin" stepKey="login"/> -+ -+ <!--Create product--> -+ <createData entity="SimpleProduct2" stepKey="createProduct"/> -+ </before> -+ <after> -+ <!--Delete create data--> -+ <deleteData createDataKey="createProduct" stepKey="deleteProduct"/> -+ -+ <!--Delete store views--> -+ <actionGroup ref="AdminDeleteStoreViewActionGroup" stepKey="deleteFirstStoreView"> -+ <argument name="customStore" value="customStoreEN"/> -+ </actionGroup> -+ <actionGroup ref="AdminDeleteStoreViewActionGroup" stepKey="deleteSecondStoreView"> -+ <argument name="customStore" value="customStoreFR"/> -+ </actionGroup> -+ -+ <!--Delete Attribute--> -+ <actionGroup ref="deleteProductAttribute" stepKey="deleteAttribute"> -+ <argument name="ProductAttribute" value="productDropDownAttribute"/> -+ </actionGroup> -+ -+ <actionGroup ref="logout" stepKey="logout"/> -+ </after> -+ -+ <!--Create 2 store views--> -+ <actionGroup ref="AdminCreateStoreViewActionGroup" stepKey="createFirstStoreView"> -+ <argument name="customStore" value="customStoreEN"/> -+ </actionGroup> -+ <actionGroup ref="AdminCreateStoreViewActionGroup" stepKey="createSecondStoreView"> -+ <argument name="customStore" value="customStoreFR"/> -+ </actionGroup> -+ -+ <!--Go to created product page and create new attribute--> -+ <amOnPage url="{{AdminProductEditPage.url($$createProduct.id$$)}}" stepKey="openAdminEditPage"/> -+ <actionGroup ref="AdminCreateAttributeWithValueWithTwoStoreViesFromProductPage" stepKey="createAttribute"> -+ <argument name="attributeName" value="{{productDropDownAttribute.attribute_code}}"/> -+ <argument name="attributeType" value="Dropdown"/> -+ <argument name="firstStoreViewName" value="{{customStoreEN.name}}"/> -+ <argument name="secondStoreViewName" value="{{customStoreFR.name}}"/> -+ </actionGroup> -+ -+ <!--Check attribute existence in product page attribute section--> -+ <conditionalClick selector="{{AdminProductAttributeSection.attributeSectionHeader}}" dependentSelector="{{AdminProductAttributeSection.attributeSection}}" visible="false" stepKey="openAttributeSection"/> -+ <seeElement selector="{{AdminProductAttributeSection.dropDownAttribute(productDropDownAttribute.attribute_code)}}" stepKey="seeNewAttributeInProductPage"/> -+ </test> -+</tests> -diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateNewGroupForAttributeSetTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateNewGroupForAttributeSetTest.xml -new file mode 100644 -index 00000000000..3219bca233b ---- /dev/null -+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateNewGroupForAttributeSetTest.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="AdminCreateNewGroupForAttributeSetTest"> -+ <annotations> -+ <stories value="Edit attribute set"/> -+ <title value="Admin should be able to create new group in an Attribute Set"/> -+ <description value="The test verifies creating a new group in an attribute set and a validation message in case of empty group name"/> -+ <severity value="CRITICAL"/> -+ <testCaseId value="MC-170"/> -+ <group value="Catalog"/> -+ </annotations> -+ <before> -+ <!-- Create a custom attribute set and custom product attribute --> -+ <createData entity="CatalogAttributeSet" stepKey="createAttributeSet"/> -+ <createData entity="productAttributeWithTwoOptions" stepKey="createConfigProductAttribute"/> -+ -+ <!-- Login to Admin --> -+ <actionGroup ref="LoginAsAdmin" stepKey="LoginAsAdmin"/> -+ </before> -+ <after> -+ <deleteData createDataKey="createConfigProductAttribute" stepKey="deleteConfigProductAttribute"/> -+ <actionGroup ref="logout" stepKey="logout"/> -+ </after> -+ -+ <!-- Navigate to Stores > Attributes > Attribute Set --> -+ <amOnPage url="{{AdminProductAttributeSetGridPage.url}}" stepKey="goToAttributeSetPage"/> -+ <waitForPageLoad stepKey="waitForPageLoad"/> -+ -+ <!-- Search and open Attribute Set from preconditions --> -+ <actionGroup ref="goToAttributeSetByName" stepKey="searchAttribute"> -+ <argument name="name" value="$$createAttributeSet.attribute_set_name$$"/> -+ </actionGroup> -+ -+ <!-- Click 'Add New': Show 'New Group' Modal --> -+ <click selector="{{AdminProductAttributeSetEditSection.AddNewGroup}}" stepKey="clickAddNew"/> -+ <waitForAjaxLoad stepKey="waitForAjax"/> -+ -+ <!-- Fill 'name' for new group and click 'Ok': Name = <empty> --> -+ <fillField userInput="" selector="{{AdminProductAttributeSetEditSection.newGroupName}}" stepKey="fillName"/> -+ <click selector="{{AdminProductAttributeSetEditSection.buttonOk}}" stepKey="clickOk"/> -+ -+ <!-- Error message 'This is a required field.' is displayed --> -+ <see userInput="This is a required field." selector="{{AdminProductAttributeSetEditSection.errorLabel}}" stepKey="seeErrorMessage"/> -+ -+ <!-- Fill 'name' for new group and click 'Ok': Name = Custom group --> -+ <fillField userInput="{{customGroup.name}}" selector="{{AdminProductAttributeSetEditSection.newGroupName}}" stepKey="fillCustomGroupName"/> -+ <click selector="{{AdminProductAttributeSetEditSection.buttonOk}}" stepKey="clickButtonOk"/> -+ -+ <!-- Group is created and displayed in 'Groups' block --> -+ <seeElement selector="{{AdminProductAttributeSetEditSection.attributeGroup(customGroup.name)}}" stepKey="assertCustomGroup"/> -+ -+ <!-- Move custom Product Attribute to new 'Custom group' Group --> -+ <waitForAjaxLoad stepKey="waitForAjaxLoad"/> -+ <click selector="{{AdminProductAttributeSetEditSection.attributeGroupExtender(customGroup.name)}}" stepKey="click"/> -+ <waitForPageLoad stepKey="waitForPageLoadAfterClick"/> -+ <dragAndDrop selector1="{{AdminProductAttributeSetEditSection.unassignedAttribute($$createConfigProductAttribute.attribute_code$$)}}" selector2="{{AdminProductAttributeSetEditSection.attributeGroupExtender(customGroup.name)}}" stepKey="moveAttribute"/> -+ <waitForPageLoad stepKey="waitForDragAndDrop"/> -+ -+ <!-- Attribute is displayed in the new group --> -+ <see userInput="$$createConfigProductAttribute.attribute_code$$" selector="{{AdminProductAttributeSetEditSection.groupTree}}" stepKey="seeAttribute"/> -+ -+ <!-- Click 'Save' --> -+ <actionGroup ref="SaveAttributeSet" stepKey="saveAttribute"/> -+ -+ <actionGroup ref="goToAttributeSetByName" stepKey="backTohAttributeSet"> -+ <argument name="name" value="$$createAttributeSet.attribute_set_name$$"/> -+ </actionGroup> -+ -+ <!-- Create another group: Name = Empty group --> -+ <click selector="{{AdminProductAttributeSetEditSection.AddNewGroup}}" stepKey="clickAddEmptyGroup"/> -+ <waitForAjaxLoad stepKey="waitForLoad"/> -+ -+ <fillField userInput="{{emptyGroup.name}}" selector="{{AdminProductAttributeSetEditSection.newGroupName}}" stepKey="fillGroupName"/> -+ <click selector="{{AdminProductAttributeSetEditSection.buttonOk}}" stepKey="clickOnOk"/> -+ <waitForPageLoad stepKey="waitForNewGroup"/> -+ -+ <!-- Empty group is created. No attributes are assigned to it. --> -+ <seeElement selector="{{AdminProductAttributeSetEditSection.attributeGroup(emptyGroup.name)}}" stepKey="assertEmptyGroup"/> -+ <dontSeeElement selector="{{AdminProductAttributeSetEditSection.attributesInGroup(emptyGroup.name)}}" stepKey="seeNoAttributes"/> -+ -+ <!-- Navigate to Catalog > Products --> -+ <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="amOnProductPage"/> -+ <waitForPageLoad stepKey="waitForProductPageLoad"/> -+ -+ <!-- Start to create a new simple product with the custom attribute set from the preconditions --> -+ <click selector="{{AdminProductGridActionSection.addProductBtn}}" stepKey="clickAddProduct"/> -+ <waitForPageLoad stepKey="waitForNewProductPage"/> -+ -+ <actionGroup ref="AdminProductPageSelectAttributeSet" stepKey="selectAttribute"> -+ <argument name="attributeSetName" value="$$createAttributeSet.attribute_set_name$$"/> -+ </actionGroup> -+ -+ <!-- New Section 'Custom group' is present in form. The section contains the attribute from preconditions --> -+ <seeElement selector="{{AdminProductAttributeSection.attributeGroupByName(customGroup.name)}}" stepKey="seeSectionCustomGroup"/> -+ <click selector="{{AdminProductAttributeSection.attributeGroupByName(customGroup.name)}}" stepKey="openCustomGroupSection"/> -+ <waitForAjaxLoad stepKey="waitForOpenSection"/> -+ <scrollTo selector="{{AdminProductFormSection.footerBlock}}" stepKey="scrollToFooter"/> -+ <seeElement selector="{{AdminProductAttributeSection.attributeByGroupAndName(customGroup.name)}}" stepKey="seeAttributePresent"/> -+ -+ <!-- Empty section is absent in Product Form --> -+ <dontSeeElement selector="{{AdminProductAttributeSection.attributeGroupByName(emptyGroup.name)}}" stepKey="dontSeeEmptyGroup"/> -+ </test> -+</tests> -diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateProductAttributeFromProductPageTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateProductAttributeFromProductPageTest.xml -new file mode 100644 -index 00000000000..5c798db29b9 ---- /dev/null -+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateProductAttributeFromProductPageTest.xml -@@ -0,0 +1,133 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> -+ <test name="AdminCreateProductAttributeFromProductPageTest"> -+ <annotations> -+ <stories value="Create product Attribute"/> -+ <title value="Create Product Attribute from Product Page"/> -+ <description value="Login as admin and create new product attribute from product page with Text Field"/> -+ <severity value="CRITICAL"/> -+ <testCaseId value="MC-10899"/> -+ <group value="mtf_migrated"/> -+ </annotations> -+ -+ <before> -+ <!-- Login as admin --> -+ <actionGroup ref="LoginAsAdmin" stepKey="login"/> -+ -+ <!--Create Category--> -+ <createData entity="SimpleSubCategory" stepKey="createCategory"/> -+ -+ <!--Create Simple Product--> -+ <createData entity="SimpleProduct" stepKey="createSimpleProduct"> -+ <requiredEntity createDataKey="createCategory"/> -+ </createData> -+ </before> -+ <after> -+ <!--Delete created entity --> -+ <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> -+ -+ <!--<deleteData createDataKey="createSimpleProduct" stepKey="deleteSimpleProduct"/>--> -+ <actionGroup ref="deleteProductAttribute" stepKey="deleteCreatedAttribute"> -+ <argument name="ProductAttribute" value="newProductAttribute"/> -+ </actionGroup> -+ -+ <actionGroup ref="logout" stepKey="logout"/> -+ </after> -+ -+ <!-- Open Product Index Page--> -+ <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="navigateToProductIndex"/> -+ <waitForPageLoad stepKey="waitForProductIndexPageToLoad"/> -+ -+ <!-- Select Created Product--> -+ <actionGroup ref="filterProductGridBySku" stepKey="filterProductGridBySku"> -+ <argument name="product" value="$$createSimpleProduct$$"/> -+ </actionGroup> -+ <click stepKey="openFirstProduct" selector="{{AdminProductGridSection.productRowBySku($$createSimpleProduct.sku$$)}}"/> -+ <waitForPageLoad stepKey="waitForProductToLoad"/> -+ -+ <fillField selector="{{AdminProductFormSection.productQuantity}}" userInput="100" stepKey="fillProductQty"/> -+ <selectOption selector="{{AdminProductFormSection.productStockStatus}}" userInput="In Stock" stepKey="selectStockStatus"/> -+ -+ <!-- Create New Product Attribute --> -+ <click selector="{{AdminProductFormSection.addAttributeBtn}}" stepKey="clickOnAddAttribute"/> -+ <waitForPageLoad stepKey="waitForAttributePageToLoad"/> -+ <click selector="{{AdminProductFormSection.createNewAttributeBtn}}" stepKey="clickCreateNewAttributeButton"/> -+ <waitForPageLoad stepKey="waitForNewAttributePageToLoad"/> -+ <waitForElementVisible selector="{{AdminCreateNewProductAttributeSection.defaultLabel}}" stepKey="waitForDefaultLabelToBeVisible"/> -+ <fillField selector="{{AdminCreateNewProductAttributeSection.defaultLabel}}" userInput="{{ProductAttributeFrontendLabel.label}}" stepKey="fillAttributeLabel"/> -+ <selectOption selector="{{AdminCreateNewProductAttributeSection.inputType}}" userInput="Text Field" stepKey="selectTextField"/> -+ <click selector="{{AdminCreateNewProductAttributeSection.advancedAttributeProperties}}" stepKey="clickOnAdvancedAttributeProperties"/> -+ <waitForElementVisible selector="{{AdminCreateNewProductAttributeSection.attributeCode}}" stepKey="waitForAttributeCodeToVisible"/> -+ <scrollTo selector="{{AdminCreateNewProductAttributeSection.attributeCode}}" stepKey="scrollToAttributeCode"/> -+ <fillField selector="{{AdminCreateNewProductAttributeSection.attributeCode}}" userInput="{{newProductAttribute.attribute_code}}" stepKey="fillAttributeCode"/> -+ <fillField selector="{{AdminCreateNewProductAttributeSection.defaultValue}}" userInput="{{ProductAttributeOption8.value}}" stepKey="fillDefaultValue"/> -+ <scrollTo selector="{{AdminCreateNewProductAttributeSection.isUnique}}" stepKey="scrollToIsUniqueOption"/> -+ <checkOption selector="{{AdminCreateNewProductAttributeSection.isUnique}}" stepKey="enableIsUniqueOption"/> -+ <scrollTo selector="{{AdminCreateNewProductAttributeSection.advancedAttributeProperties}}" stepKey="scrollToAdvancedAttributeProperties"/> -+ <click selector="{{AdminCreateNewProductAttributeSection.advancedAttributeProperties}}" stepKey="clickOnAdvancedAttributeProperties1"/> -+ <scrollTo selector="{{AdminCreateNewProductAttributeSection.storefrontProperties}}" stepKey="scrollToStorefrontProperties"/> -+ <click selector="{{AdminCreateNewProductAttributeSection.storefrontProperties}}" stepKey="clickOnStorefrontProperties"/> -+ <waitForPageLoad stepKey="waitForStoreFrontToLoad"/> -+ <scrollTo stepKey="scroll1" selector="{{AdminCreateNewProductAttributeSection.sortProductListing}}" x="0" y="-80"/> -+ <checkOption selector="{{AdminCreateNewProductAttributeSection.inSearch}}" stepKey="enableInSearchOption"/> -+ <checkOption selector="{{AdminCreateNewProductAttributeSection.advancedSearch}}" stepKey="enableAdvancedSearch"/> -+ <checkOption selector="{{AdminCreateNewProductAttributeSection.isComparable}}" stepKey="enableIsUComparableption"/> -+ <checkOption selector="{{AdminCreateNewProductAttributeSection.allowHtmlTags}}" stepKey="enableAllowHtmlTags"/> -+ <checkOption selector="{{AdminCreateNewProductAttributeSection.visibleOnStorefront}}" stepKey="enableVisibleOnStorefront"/> -+ <checkOption selector="{{AdminCreateNewProductAttributeSection.sortProductListing}}" stepKey="enableSortProductListing"/> -+ <scrollToTopOfPage stepKey="scrollToTopOfPage"/> -+ <click selector="{{AdminCreateNewProductAttributeSection.saveAttribute}}" stepKey="clickOnSaveAttribute"/> -+ <waitForPageLoad stepKey="waitForAttributeToSave"/> -+ <click selector="{{AdminProductFormSection.save}}" stepKey="saveTheProduct"/> -+ <waitForPageLoad stepKey="waitForProductToSave"/> -+ <see selector="{{AdminCategoryMessagesSection.SuccessMessage}}" userInput="You saved the product." stepKey="messageYouSavedTheProductIsShown"/> -+ -+ <!--Verify product attribute added in product form --> -+ <scrollTo selector="{{AdminProductFormSection.contentTab}}" stepKey="scrollToContentTab"/> -+ <waitForElementVisible selector="{{AdminProductFormSection.attributeTab}}" stepKey="waitForAttributeToVisible"/> -+ <click selector="{{AdminProductFormSection.attributeTab}}" stepKey="clickOnAttribute"/> -+ <seeElement selector="{{AdminProductFormSection.attributeLabelByText(ProductAttributeFrontendLabel.label)}}" stepKey="seeAttributeLabelInProductForm"/> -+ -+ <!--Verify Product Attribute in Attribute Form --> -+ <amOnPage url="{{AdminProductAttributeGridPage.url}}" stepKey="navigateToProductAttributeGrid"/> -+ <fillField selector="{{AdminProductAttributeGridSection.FilterByAttributeCode}}" userInput="{{newProductAttribute.attribute_code}}" stepKey="setAttributeCode"/> -+ <click selector="{{AdminProductAttributeGridSection.Search}}" stepKey="searchForAttributeFromTheGrid"/> -+ <waitForPageLoad stepKey="waitForPageLoad" /> -+ <see selector="{{AdminProductAttributeGridSection.attributeCodeColumn}}" userInput="{{newProductAttribute.attribute_code}}" stepKey="seeAttributeCode"/> -+ <see selector="{{AdminProductAttributeGridSection.defaultLabelColumn}}" userInput="{{ProductAttributeFrontendLabel.label}}" stepKey="seeDefaultLabel"/> -+ <see selector="{{AdminProductAttributeGridSection.isVisibleColumn}}" userInput="Yes" stepKey="seeIsVisibleColumn"/> -+ <see selector="{{AdminProductAttributeGridSection.isSearchableColumn}}" userInput="Yes" stepKey="seeSearchableColumn"/> -+ <see selector="{{AdminProductAttributeGridSection.isComparableColumn}}" userInput="Yes" stepKey="seeComparableColumn"/> -+ -+ <!--Verify Product Attribute is present in Category Store Front Page --> -+ <amOnPage url="$$createCategory.name$$.html" stepKey="goToStorefrontPage"/> -+ <waitForPageLoad stepKey="waitForProductFrontPageToLoad"/> -+ <click selector="{{StorefrontHeaderSection.NavigationCategoryByName(SimpleSubCategory.name)}}" stepKey="clickOnCategory"/> -+ <waitForPageLoad stepKey="waitForCategoryPageToLoad"/> -+ <click selector="{{StorefrontCategoryMainSection.productLink}}" stepKey="openSearchedProduct"/> -+ <waitForPageLoad stepKey="waitForProductToLoad1"/> -+ <see selector="{{StorefrontProductInfoMainSection.productName}}" userInput="{{SimpleProduct.name}}" stepKey="seeProductNameInStoreFront"/> -+ <see selector="{{StorefrontProductInfoMainSection.productPrice}}" userInput="{{SimpleProduct.price}}" stepKey="seeProductPriceInStoreFront"/> -+ <see selector="{{StorefrontProductInfoMainSection.productSku}}" userInput="{{SimpleProduct.sku}}" stepKey="seeProductSkuInStoreFront"/> -+ <scrollTo selector="{{StorefrontProductMoreInformationSection.moreInformation}}" stepKey="scrollToMoreInformation"/> -+ <see selector="{{StorefrontProductMoreInformationSection.attributeLabel}}" userInput="{{ProductAttributeFrontendLabel.label}}" stepKey="seeAttributeLabel"/> -+ <see selector="{{StorefrontProductMoreInformationSection.attributeValue}}" userInput="{{ProductAttributeOption8.value}}" stepKey="seeAttributeValue"/> -+ -+ <!--Verify Product Attribute present in search page --> -+ <amOnPage url="$$createCategory.name$$.html" stepKey="goToStorefrontPage1"/> -+ <waitForPageLoad stepKey="waitForProductFrontPageToLoad1"/> -+ <fillField selector="{{StorefrontQuickSearchResultsSection.searchTextBox}}" userInput="{{ProductAttributeOption8.value}}" stepKey="fillAttribute"/> -+ <waitForPageLoad stepKey="waitForSearchTextBox"/> -+ <click selector="{{StorefrontQuickSearchResultsSection.searchTextBoxButton}}" stepKey="clickSearchTextBoxButton"/> -+ <waitForPageLoad stepKey="waitForSearch"/> -+ <see selector="{{StorefrontCategoryMainSection.productName}}" userInput="{{SimpleProduct.name}}" stepKey="seeProductNameInCategoryPage"/> -+ </test> -+</tests> -diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateProductAttributeRequiredTextFieldTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateProductAttributeRequiredTextFieldTest.xml -new file mode 100644 -index 00000000000..d4d6496e018 ---- /dev/null -+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateProductAttributeRequiredTextFieldTest.xml -@@ -0,0 +1,92 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> -+ <test name="AdminCreateProductAttributeRequiredTextFieldTest"> -+ <annotations> -+ <stories value="Manage products"/> -+ <title value="Create Custom Product Attribute Text Field (Required) from Product Page"/> -+ <description value="Login as admin and create product attribute with Text Field and Required option"/> -+ <severity value="CRITICAL"/> -+ <testCaseId value="MC-10906"/> -+ <group value="mtf_migrated"/> -+ </annotations> -+ -+ <before> -+ <!-- Login as admin --> -+ <actionGroup ref="LoginAsAdmin" stepKey="login"/> -+ -+ <!--Create Category--> -+ <createData entity="SimpleSubCategory" stepKey="createCategory"/> -+ -+ <!--Create Simple Product--> -+ <createData entity="SimpleProduct" stepKey="createSimpleProduct"> -+ <requiredEntity createDataKey="createCategory"/> -+ </createData> -+ </before> -+ <after> -+ <!--Delete created entity --> -+ <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> -+ <deleteData createDataKey="createSimpleProduct" stepKey="deleteSimpleProduct"/> -+ <actionGroup ref="deleteProductAttribute" stepKey="deleteCreatedAttribute"> -+ <argument name="ProductAttribute" value="newProductAttribute"/> -+ </actionGroup> -+ <actionGroup ref="logout" stepKey="logout"/> -+ </after> -+ -+ <!-- Open Product Index Page--> -+ <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="navigateToProductIndex"/> -+ <waitForPageLoad stepKey="waitForProductIndexPageToLoad"/> -+ -+ <!-- Select Created Product--> -+ <actionGroup ref="filterProductGridBySku" stepKey="filterProductGridBySku"> -+ <argument name="product" value="$$createSimpleProduct$$"/> -+ </actionGroup> -+ <click stepKey="openFirstProduct" selector="{{AdminProductGridSection.productRowBySku($$createSimpleProduct.sku$$)}}"/> -+ <waitForPageLoad stepKey="waitForProductToLoad"/> -+ -+ <fillField selector="{{AdminProductFormSection.productQuantity}}" userInput="100" stepKey="fillProductQty"/> -+ <selectOption selector="{{AdminProductFormSection.productStockStatus}}" userInput="In Stock" stepKey="selectStockStatus"/> -+ -+ <!-- Create Product Attribute --> -+ <click selector="{{AdminProductFormSection.addAttributeBtn}}" stepKey="clickOnAddAttribute"/> -+ <waitForPageLoad stepKey="waitForAttributePageToLoad"/> -+ <click selector="{{AdminProductFormSection.createNewAttributeBtn}}" stepKey="clickCreateNewAttributeButton"/> -+ <waitForPageLoad stepKey="waitForNewAttributePageToLoad"/> -+ <waitForElementVisible selector="{{AdminCreateNewProductAttributeSection.defaultLabel}}" stepKey="waitForDefaultLabelToBeVisible"/> -+ <fillField selector="{{AdminCreateNewProductAttributeSection.defaultLabel}}" userInput="{{ProductAttributeFrontendLabel.label}}" stepKey="fillAttributeLabel"/> -+ <selectOption selector="{{AdminCreateNewProductAttributeSection.inputType}}" userInput="Text Field" stepKey="selectTextField"/> -+ <checkOption selector="{{AdminCreateNewProductAttributeSection.isRequired}}" stepKey="enableIsRequiredOption"/> -+ <click selector="{{AdminCreateNewProductAttributeSection.advancedAttributeProperties}}" stepKey="clickOnAdvancedAttributeProperties"/> -+ <waitForElementVisible selector="{{AdminCreateNewProductAttributeSection.attributeCode}}" stepKey="waitForAttributeCodeToVisible"/> -+ <scrollTo selector="{{AdminCreateNewProductAttributeSection.attributeCode}}" stepKey="scrollToAttributeCode"/> -+ <fillField selector="{{AdminCreateNewProductAttributeSection.attributeCode}}" userInput="{{newProductAttribute.attribute_code}}" stepKey="fillAttributeCode"/> -+ <selectOption selector="{{AdminCreateNewProductAttributeSection.scope}}" userInput="Global" stepKey="selectScope"/> -+ <fillField selector="{{AdminCreateNewProductAttributeSection.defaultValue}}" userInput="{{ProductAttributeOption8.value}}" stepKey="fillDefaultValue"/> -+ <scrollTo selector="{{AdminCreateNewProductAttributeSection.isUnique}}" stepKey="scrollToIsUniqueOption"/> -+ <checkOption selector="{{AdminCreateNewProductAttributeSection.isUnique}}" stepKey="enableIsUniqueOption"/> -+ <scrollToTopOfPage stepKey="scrollToTopOfPage"/> -+ <click selector="{{AdminCreateNewProductAttributeSection.saveAttribute}}" stepKey="clickOnSaveAttribute"/> -+ <waitForPageLoad stepKey="waitForAttributeToSave"/> -+ <click selector="{{AdminProductFormSection.save}}" stepKey="saveTheProduct"/> -+ <waitForPageLoad stepKey="waitForProductToSave"/> -+ -+ <!--Verify product attribute added in product form and Is Required message displayed--> -+ <scrollTo selector="{{AdminProductFormSection.contentTab}}" stepKey="scrollToContentTab"/> -+ <waitForElementVisible selector="{{AdminProductFormSection.attributeTab}}" stepKey="waitForAttributeToVisible"/> -+ <seeElement selector="{{AdminProductFormSection.attributeFieldError}}" stepKey="seeAttributeInputFiledErrorMessage"/> -+ -+ <!--Fill the Required field and save the product --> -+ <fillField selector="{{AdminProductFormSection.attributeRequiredInput(newProductAttribute.attribute_code)}}" userInput="attribute" stepKey="fillTheAttributeRequiredInputField"/> -+ <scrollToTopOfPage stepKey="scrollToTopOfProductFormPage"/> -+ <click selector="{{AdminProductFormSection.save}}" stepKey="saveTheProduct1"/> -+ <waitForPageLoad stepKey="waitForProductToSave1"/> -+ <see selector="{{AdminCategoryMessagesSection.SuccessMessage}}" userInput="You saved the product." stepKey="messageYouSavedTheProductIsShown"/> -+ </test> -+</tests> -diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateProductCustomAttributeSet.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateProductCustomAttributeSet.xml -index 038c8fd2263..713e1b7d6df 100644 ---- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateProductCustomAttributeSet.xml -+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateProductCustomAttributeSet.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="AdminCreateProductCustomAttributeSet"> - <annotations> - <features value="Catalog"/> -diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateProductDuplicateUrlkeyTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateProductDuplicateUrlkeyTest.xml -index 0340eea852a..6658ad36d71 100644 ---- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateProductDuplicateUrlkeyTest.xml -+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateProductDuplicateUrlkeyTest.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="AdminCreateProductDuplicateUrlkeyTest"> - <annotations> - <features value="Catalog"/> -@@ -40,4 +40,46 @@ - <click selector="{{AdminProductFormActionSection.saveButton}}" stepKey="saveProduct"/> - <see userInput="The value specified in the URL Key field would generate a URL that already exists" selector="{{AdminProductMessagesSection.errorMessage}}" stepKey="assertErrorMessage"/> - </test> -+ <test name="AdminCreateProductDuplicateProductTest"> -+ <annotations> -+ <features value="Catalog"/> -+ <stories value="Validation Errors"/> -+ <title value="No validation errors when trying to duplicate product twice"/> -+ <description value="No validation errors when trying to duplicate product twice"/> -+ <severity value="MAJOR"/> -+ <testCaseId value="MC-5472"/> -+ <group value="product"/> -+ </annotations> -+ <before> -+ <createData entity="_defaultCategory" stepKey="createCategory"/> -+ <createData entity="SimpleProduct" stepKey="createSimpleProduct"> -+ <requiredEntity createDataKey="createCategory"/> -+ </createData> -+ </before> -+ <after> -+ <!--Delete all products by filtering grid and using mass delete action--> -+ <actionGroup ref="deleteAllDuplicateProductUsingProductGrid" stepKey="deleteAllDuplicateProducts"> -+ <argument name="product" value="$$createSimpleProduct$$"/> -+ </actionGroup> -+ <deleteData createDataKey="createCategory" stepKey="deletePreReqCatalog" /> -+ <actionGroup ref="logout" stepKey="logout"/> -+ </after> -+ <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> -+ <actionGroup ref="SearchForProductOnBackendActionGroup" stepKey="searchForSimpleProduct1"> -+ <argument name="product" value="$$createSimpleProduct$$"/> -+ </actionGroup> -+ <actionGroup ref="OpenEditProductOnBackendActionGroup" stepKey="openEditProduct1"> -+ <argument name="product" value="$$createSimpleProduct$$"/> -+ </actionGroup> -+ <!--Save and duplicated the product once--> -+ <actionGroup ref="AdminFormSaveAndDuplicate" stepKey="saveAndDuplicateProductForm1"/> -+ <actionGroup ref="SearchForProductOnBackendActionGroup" stepKey="searchForSimpleProduct2"> -+ <argument name="product" value="$$createSimpleProduct$$"/> -+ </actionGroup> -+ <actionGroup ref="OpenEditProductOnBackendActionGroup" stepKey="openEditProduct2"> -+ <argument name="product" value="$$createSimpleProduct$$"/> -+ </actionGroup> -+ <!--Save and duplicated the product second time--> -+ <actionGroup ref="AdminFormSaveAndDuplicate" stepKey="saveAndDuplicateProductForm2"/> -+ </test> - </tests> -diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateRootCategoryAndSubcategoriesTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateRootCategoryAndSubcategoriesTest.xml -index dbe5a90d592..11d919ddefa 100644 ---- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateRootCategoryAndSubcategoriesTest.xml -+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateRootCategoryAndSubcategoriesTest.xml -@@ -7,10 +7,11 @@ - --> - - <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="AdminCreateRootCategoryAndSubcategoriesTest"> - <annotations> - <features value="Catalog"/> -+ <stories value="Create categories"/> - <title value="Admin should be able to create a Root Category and a Subcategory"/> - <description value="Admin should be able to create a Root Category and a Subcategory"/> - <severity value="CRITICAL"/> -diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateRootCategoryRequiredFieldsTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateRootCategoryRequiredFieldsTest.xml -new file mode 100644 -index 00000000000..f98f9acc469 ---- /dev/null -+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateRootCategoryRequiredFieldsTest.xml -@@ -0,0 +1,42 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> -+ <test name="AdminCreateRootCategoryRequiredFieldsTest"> -+ <annotations> -+ <stories value="Create categories"/> -+ <features value="Catalog"/> -+ <title value="Create Root Category from Category Page"/> -+ <description value="Create Root Category from Category Page"/> -+ <testCaseId value="MC-5263"/> -+ <severity value="CRITICAL"/> -+ <group value="catalog"/> -+ <group value="mtf_migrated"/> -+ </annotations> -+ <before> -+ <actionGroup ref = "LoginAsAdmin" stepKey="LoginToAdminPanel"/> -+ </before> -+ <after> -+ <actionGroup ref="DeleteCategory" stepKey="deleteCategory"> -+ <argument name="categoryEntity" value="_defaultCategory" /> -+ </actionGroup> -+ <actionGroup ref="logout" stepKey="logout" /> -+ </after> -+ -+ <amOnPage url="{{AdminCategoryPage.url}}" stepKey="OpenAdminCatergoryIndexPage"/> -+ <click selector="{{AdminCategorySidebarActionSection.AddRootCategoryButton}}" stepKey="ClickOnAddRootButton"/> -+ <fillField selector="{{AdminCategoryBasicFieldSection.CategoryNameInput}}" userInput="{{_defaultCategory.name}}" stepKey="FillCategoryField"/> -+ <checkOption selector="{{AdminCategoryBasicFieldSection.EnableCategory}}" stepKey="EnableCheckOption"/> -+ <click selector="{{AdminCategoryMainActionsSection.SaveButton}}" stepKey="ClickSaveButton"/> -+ <waitForPageLoad stepKey="WaitForCategorySaved"/> -+ <see selector="{{AdminCategoryMessagesSection.SuccessMessage}}" userInput="You saved the category." stepKey="AssertSuccessMessage"/> -+ <seeCheckboxIsChecked selector="{{AdminCategoryBasicFieldSection.EnableCategory}}" stepKey="SeeCheckBoxisSelected"/> -+ <seeInField selector="{{AdminCategoryBasicFieldSection.CategoryNameInput}}" userInput="{{_defaultCategory.name}}" stepKey="SeedFieldInput"/> -+ </test> -+</tests> -diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateSimpleProductTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateSimpleProductTest.xml -index b2e6119ef4e..6096ee1fa39 100644 ---- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateSimpleProductTest.xml -+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateSimpleProductTest.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="AdminCreateSimpleProductTest"> - <annotations> - <features value="Catalog"/> -diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateSimpleProductWithCountryOfManufactureAttributeSKUMaskTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateSimpleProductWithCountryOfManufactureAttributeSKUMaskTest.xml -new file mode 100644 -index 00000000000..3487de65617 ---- /dev/null -+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateSimpleProductWithCountryOfManufactureAttributeSKUMaskTest.xml -@@ -0,0 +1,61 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> -+ <test name="AdminCreateSimpleProductWithCountryOfManufactureAttributeSKUMaskTest"> -+ <annotations> -+ <stories value="Create simple product"/> -+ <title value="Create simple product with (Country of Manufacture) Attribute SKU Mask"/> -+ <description value="Test log in to Create simple product and Create simple product with (Country of Manufacture) Attribute SKU Mask"/> -+ <testCaseId value="MC-11024"/> -+ <severity value="CRITICAL"/> -+ <group value="catalog"/> -+ <group value="mtf_migrated"/> -+ </annotations> -+ -+ <before> -+ <magentoCLI stepKey="setCountryOfManufacture" command="config:set catalog/fields_masks/sku" arguments="{{name}}-{{country_of_manufacture}}"/> -+ <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> -+ </before> -+ <after> -+ <magentoCLI stepKey="setName" command="config:set catalog/fields_masks/sku" arguments="{{name}}"/> -+ <actionGroup ref="deleteProductBySku" stepKey="deleteCreatedProduct"> -+ <argument name="sku" value="{{nameAndAttributeSkuMaskSimpleProduct.name}}-{{nameAndAttributeSkuMaskSimpleProduct.country_of_manufacture_label}}" /> -+ </actionGroup> -+ <actionGroup ref="logout" stepKey="logout"/> -+ </after> -+ -+ <amOnPage url="{{ProductCatalogPage.url}}" stepKey="openProductCatalogPage"/> -+ <waitForPageLoad stepKey="waitForProductCatalogPage"/> -+ <click selector="{{AdminProductGridActionSection.addProductToggle}}" stepKey="clickAddProductToggle"/> -+ <waitForPageLoad stepKey="waitForProductToggleToSelectSimpleProduct"/> -+ <click selector="{{AdminProductGridActionSection.addSimpleProduct}}" stepKey="clickSimpleProductFromDropDownList"/> -+ -+ <!-- Create simple product with country of manufacture attribute --> -+ <fillField selector="{{AdminProductFormSection.productName}}" userInput="{{nameAndAttributeSkuMaskSimpleProduct.name}}" stepKey="fillSimpleProductName"/> -+ <selectOption selector="{{AdminProductFormSection.countryOfManufacture}}" userInput="{{nameAndAttributeSkuMaskSimpleProduct.country_of_manufacture_label}}" stepKey="selectCountryOfManufacture"/> -+ <fillField selector="{{AdminProductFormSection.productPrice}}" userInput="{{nameAndAttributeSkuMaskSimpleProduct.price}}" stepKey="fillSimpleProductPrice"/> -+ <fillField selector="{{AdminProductFormSection.productWeight}}" userInput="{{nameAndAttributeSkuMaskSimpleProduct.weight}}" stepKey="fillSimpleProductWeight"/> -+ <fillField selector="{{AdminProductFormSection.productQuantity}}" userInput="{{nameAndAttributeSkuMaskSimpleProduct.quantity}}" stepKey="fillSimpleProductQuantity"/> -+ <click selector="{{AdminProductFormSection.save}}" stepKey="clickSaveButton"/> -+ <waitForPageLoad stepKey="waitForSimpleProductToSave"/> -+ <!-- Verify customer see success message --> -+ <see selector="{{AdminProductFormSection.successMessage}}" userInput="You saved the product." stepKey="seeAssertSimpleProductSaveSuccessMessage"/> -+ -+ <!-- Search created simple product(from above step) in the grid page to verify sku masked as name and country of manufacture --> -+ <amOnPage url="{{ProductCatalogPage.url}}" stepKey="OpenProductCatalogPageToSearchCreatedSimpleProduct"/> -+ <waitForPageLoad stepKey="waitForProductCatalogPageToLoad"/> -+ <conditionalClick selector="{{AdminProductGridFilterSection.clearAll}}" dependentSelector="{{AdminProductGridFilterSection.clearAll}}" visible="true" stepKey="clickClearAll"/> -+ <click selector="{{AdminProductGridFilterSection.filters}}" stepKey="clickFiltersButton"/> -+ <fillField selector="{{AdminProductGridFilterSection.skuFilter}}" userInput="{{nameAndAttributeSkuMaskSimpleProduct.name}}-{{nameAndAttributeSkuMaskSimpleProduct.country_of_manufacture_label}}" stepKey="fillSkuFilterFieldWithNameAndCountryOfManufactureInput" /> -+ <click selector="{{AdminProductGridFilterSection.applyFilters}}" stepKey="clickApplyFiltersButton"/> -+ <waitForPageLoad stepKey="waitForProductSearchAfterApplyingFilters"/> -+ <see selector="{{AdminProductGridSection.firstProductRow}}" userInput="{{nameAndAttributeSkuMaskSimpleProduct.name}}-{{nameAndAttributeSkuMaskSimpleProduct.country_of_manufacture_label}}" stepKey="seeSimpleProductSkuMaskedAsNameAndCountryOfManufacture"/> -+ </test> -+</tests> -diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateSimpleProductWithUnicodeTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateSimpleProductWithUnicodeTest.xml -index 486fea9d91f..896a28d0298 100644 ---- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateSimpleProductWithUnicodeTest.xml -+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateSimpleProductWithUnicodeTest.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="AdminCreateSimpleProductWithUnicodeTest"> - <annotations> - <features value="Catalog"/> -diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateTextEditorProductAttributeTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateTextEditorProductAttributeTest.xml -new file mode 100644 -index 00000000000..fc7482c3531 ---- /dev/null -+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateTextEditorProductAttributeTest.xml -@@ -0,0 +1,129 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> -+ <test name="AdminCreateTextEditorProductAttributeTest"> -+ <annotations> -+ <features value="Catalog"/> -+ <stories value="Create product Attribute"/> -+ <title value="Admin create text editor product attribute test"/> -+ <description value="Create text editor product attribute with TinyMCE4 enabled"/> -+ <severity value="CRITICAL"/> -+ <testCaseId value="MC-6338"/> -+ <group value="catalog"/> -+ </annotations> -+ <before> -+ <!-- Enable WYSIWYG editor --> -+ <magentoCLI command="config:set {{EnableWYSIWYG.path}} {{EnableWYSIWYG.value}}" stepKey="enableWYSIWYG"/> -+ -+ <!-- Enable TinyMCE 4 --> -+ <magentoCLI command="config:set {{EnableTinyMCE4.path}} {{EnableTinyMCE4.value}}" stepKey="enableTinyMCE4"/> -+ -+ <!-- Login as admin --> -+ <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> -+ </before> -+ <after> -+ <!-- Delete attribute --> -+ <actionGroup ref="deleteProductAttribute" stepKey="deleteAttribute"> -+ <argument name="ProductAttribute" value="productTextEditorAttribute"/> -+ </actionGroup> -+ -+ <!-- Delete product --> -+ <actionGroup ref="deleteProductUsingProductGrid" stepKey="deleteProduct"> -+ <argument name="product" value="_defaultProduct"/> -+ </actionGroup> -+ -+ <!-- Log out --> -+ <actionGroup ref="logout" stepKey="logout"/> -+ </after> -+ -+ <!-- Go to Stores > Product, click "Add New Attribute" --> -+ <actionGroup ref="AdminOpenProductAttributePageActionGroup" stepKey="openProductAttributePage"/> -+ <click selector="{{AdminProductAttributeGridSection.createNewAttributeBtn}}" stepKey="createNewAttribute"/> -+ -+ <!-- Input value for Default Label. Verify dropdown of "Catalog Input Type for Store Owner" --> -+ <actionGroup ref="AdminFillProductAttributePropertiesActionGroup" stepKey="fillAttributeProperties"> -+ <argument name="attributeName" value="{{productTextEditorAttribute.attribute_code}}"/> -+ <argument name="attributeType" value="{{productTextEditorAttribute.frontend_input}}"/> -+ </actionGroup> -+ -+ <!-- Input value for "Catalog Input Type for Store Owner" --> -+ <selectOption selector="{{AttributePropertiesSection.InputType}}" userInput="{{productAttributeWysiwyg.frontend_input}}" stepKey="updateInputType"/> -+ -+ <!-- Click on "Storefront Properties" tab on left menu --> -+ <click selector="{{StorefrontPropertiesSection.StoreFrontPropertiesTab}}" stepKey="clickStorefrontPropertiesTab"/> -+ <dontSeeElement selector="{{StorefrontPropertiesSection.EnableWYSIWYG}}" stepKey="dontSeeWYSIWYGEnableField"/> -+ -+ <!-- Selection for "Visible on Catalog Pages on Storefront" --> -+ <selectOption selector="{{StorefrontPropertiesSection.visibleOnCatalogPagesOnStorefront}}" userInput="Yes" stepKey="enableVisibleOnStorefront"/> -+ <scrollToTopOfPage stepKey="scrollToPageTop"/> -+ -+ <!-- Go back to "Properties" tab on left menu --> -+ <click selector="{{AttributePropertiesSection.propertiesTab}}" stepKey="clickPropertiesTab"/> -+ -+ <!-- Updated value for "Catalog Input Type for Store Owner" --> -+ <selectOption selector="{{AttributePropertiesSection.InputType}}" userInput="{{productTextEditorAttribute.frontend_input}}" stepKey="returnInputType"/> -+ -+ <!-- Save Product Attribute --> -+ <actionGroup ref="saveProductAttribute" stepKey="saveAttribute"/> -+ -+ <!-- Go to Store > Attribute Set --> -+ <actionGroup ref="AdminOpenAttributeSetGridPageActionGroup" stepKey="openAttributeSetPage"/> -+ -+ <!-- From grid, click on attribute set Default --> -+ <actionGroup ref="AdminOpenAttributeSetByNameActionGroup" stepKey="openDefaultAttributeSet"/> -+ -+ <!-- Add Product Attribute to Default attribute by dragging and dropping this to the 'Project Details' folder. Then Save. --> -+ <actionGroup ref="AssignAttributeToGroup" stepKey="assignAttributeToGroup"> -+ <argument name="group" value="Product Details"/> -+ <argument name="attribute" value="{{productTextEditorAttribute.attribute_code}}"/> -+ </actionGroup> -+ <actionGroup ref="SaveAttributeSet" stepKey="saveAttributeSet"/> -+ -+ <!-- Go Catalog > Product to create new product page --> -+ <actionGroup ref="AdminOpenProductIndexPageActionGroup" stepKey="goToProductIndexPage"/> -+ <actionGroup ref="goToCreateProductPage" stepKey="goToCreateProduct"> -+ <argument name="product" value="_defaultProduct"/> -+ </actionGroup> -+ -+ <!-- On product page, select Attribute Set: "Default" --> -+ <actionGroup ref="AdminProductPageSelectAttributeSet" stepKey="selectAttributeSet"> -+ <argument name="attributeSetName" value="Default"/> -+ </actionGroup> -+ -+ <!-- Created product attribute appear on product form --> -+ <seeElement selector="{{AdminProductFormSection.attributeLabelByText(productTextEditorAttribute.attribute_code)}}" stepKey="seeAttributeLabelInProductForm"/> -+ -+ <!-- TinyMCE 4 is displayed in WYSIWYG content area --> -+ <seeElement selector="{{TinyMCESection.TinyMCE4}}" stepKey="seeTinyMCE4"/> -+ -+ <!-- Verify toolbar menu --> -+ <actionGroup ref="VerifyTinyMCEActionGroup" stepKey="verifyToolbarMenu"/> -+ -+ <!-- Click Show/Hide button and see Insert Image button --> -+ <scrollToTopOfPage stepKey="scrollToTop"/> -+ <click selector="{{ProductAttributeWYSIWYGSection.showHideBtn(productTextEditorAttribute.attribute_code)}}" stepKey="clickShowHideBtn"/> -+ <waitForElementVisible selector="{{TinyMCESection.InsertImageBtn}}" stepKey="waitForInsertImageBtn"/> -+ -+ <!-- Add content into attribute --> -+ <fillField selector="{{ProductDescriptionWysiwygSection.attributeEditArea(productTextEditorAttribute.attribute_code)}}" userInput="This content from product page" stepKey="setContent"/> -+ -+ <!-- Fill up all required fields for product form --> -+ <actionGroup ref="fillMainProductForm" stepKey="fillProductForm"/> -+ <actionGroup ref="saveProductForm" stepKey="saveProductForm"/> -+ -+ <!-- Assert product attribute on Storefront --> -+ <actionGroup ref="OpenStorefrontProductPageByProductNameActionGroup" stepKey="openProductPage"/> -+ <scrollTo stepKey="scrollToMoreInformation" selector="{{StorefrontProductMoreInformationSection.moreInformationSpecs}}" /> -+ <actionGroup ref="AssertStorefrontCustomProductAttributeActionGroup" stepKey="checkAttributeInMoreInformationTab"> -+ <argument name="attributeLabel" value="{{productTextEditorAttribute.attribute_code}}"/> -+ <argument name="attributeValue" value="This content from product page"/> -+ </actionGroup> -+ </test> -+</tests> -diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateVirtualProductFillingRequiredFieldsOnlyTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateVirtualProductFillingRequiredFieldsOnlyTest.xml -new file mode 100644 -index 00000000000..c3fe666c84f ---- /dev/null -+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateVirtualProductFillingRequiredFieldsOnlyTest.xml -@@ -0,0 +1,56 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> -+ <test name="AdminCreateVirtualProductFillingRequiredFieldsOnlyTest"> -+ <annotations> -+ <stories value="Create virtual product"/> -+ <title value="Create virtual product filling required fields only"/> -+ <description value="Test log in to Create virtual product and Create virtual product filling required fields only"/> -+ <testCaseId value="MC-6031"/> -+ <severity value="CRITICAL"/> -+ <group value="catalog"/> -+ <group value="mtf_migrated"/> -+ </annotations> -+ <before> -+ <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> -+ </before> -+ <after> -+ <actionGroup ref="logout" stepKey="logout"/> -+ </after> -+ -+ <amOnPage url="{{ProductCatalogPage.url}}" stepKey="OpenProductCatalogPage"/> -+ <waitForPageLoad stepKey="waitForProductCatalogPage"/> -+ <click selector="{{AdminProductGridActionSection.addProductToggle}}" stepKey="clickAddProductToggle"/> -+ <waitForPageLoad stepKey="waitForProductToggleToSelectProduct"/> -+ <click selector="{{AdminProductGridActionSection.addVirtualProduct}}" stepKey="clickVirtualProduct"/> -+ -+ <!-- Create virtual product with required fields only --> -+ <fillField selector="{{AdminProductFormSection.productName}}" userInput="{{virtualProductWithRequiredFields.name}}" stepKey="fillProductName"/> -+ <fillField selector="{{AdminProductFormSection.productSku}}" userInput="{{virtualProductWithRequiredFields.sku}}" stepKey="fillProductSku"/> -+ <fillField selector="{{AdminProductFormSection.productPrice}}" userInput="{{virtualProductWithRequiredFields.price}}" stepKey="fillProductPrice"/> -+ <click selector="{{AdminProductFormSection.save}}" stepKey="clickSaveButton"/> -+ <waitForPageLoad stepKey="waitForVirtualProductSaved" /> -+ -+ <!-- Verify we see success message --> -+ <see selector="{{AdminProductFormSection.successMessage}}" userInput="You saved the product." stepKey="seeAssertVirtualProductSuccessMessage"/> -+ -+ <!-- Verify we see created virtual product(from the above step) on the product grid page --> -+ <amOnPage url="{{ProductCatalogPage.url}}" stepKey="OpenProductCatalogPage1"/> -+ <waitForPageLoad stepKey="waitForProductCatalogPage1"/> -+ <conditionalClick selector="{{AdminProductGridFilterSection.clearAll}}" dependentSelector="{{AdminProductGridFilterSection.clearAll}}" visible="true" stepKey="clickSelector"/> -+ <click selector="{{AdminProductGridFilterSection.filters}}" stepKey="clickFilter"/> -+ <fillField selector="{{AdminProductGridFilterSection.nameFilter}}" userInput="{{virtualProductWithRequiredFields.name}}" stepKey="fillProductName1"/> -+ <fillField selector="{{AdminProductGridFilterSection.skuFilter}}" userInput="{{virtualProductWithRequiredFields.sku}}" stepKey="fillVirtualProductSku"/> -+ <click selector="{{AdminProductGridFilterSection.applyFilters}}" stepKey="clickSearch2"/> -+ <waitForPageLoad stepKey="waitForProductSearch"/> -+ <seeInField selector="{{AdminProductGridFilterSection.nameFilter}}" userInput="{{virtualProductWithRequiredFields.name}}" stepKey="seeVirtualProductName"/> -+ <seeInField selector="{{AdminProductGridFilterSection.skuFilter}}" userInput="{{virtualProductWithRequiredFields.sku}}" stepKey="seeVirtualProductSku"/> -+ </test> -+</tests> -diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateVirtualProductOutOfStockWithTierPriceTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateVirtualProductOutOfStockWithTierPriceTest.xml -new file mode 100644 -index 00000000000..26ad7a46a73 ---- /dev/null -+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateVirtualProductOutOfStockWithTierPriceTest.xml -@@ -0,0 +1,97 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> -+ <test name="AdminCreateVirtualProductOutOfStockWithTierPriceTest"> -+ <annotations> -+ <stories value="Create virtual product"/> -+ <title value="Create virtual product out of stock with tier price"/> -+ <description value="Test log in to Create virtual product and Create virtual product out of stock with tier price"/> -+ <testCaseId value="MC-6036"/> -+ <severity value="CRITICAL"/> -+ <group value="catalog"/> -+ <group value="mtf_migrated"/> -+ </annotations> -+ <before> -+ <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> -+ <createData entity="SimpleSubCategory" stepKey="categoryEntity"/> -+ </before> -+ <after> -+ <deleteData stepKey="deleteSimpleSubCategory" createDataKey="categoryEntity"/> -+ <actionGroup ref="logout" stepKey="logout"/> -+ </after> -+ -+ <amOnPage url="{{ProductCatalogPage.url}}" stepKey="OpenProductCatalogPage"/> -+ <waitForPageLoad stepKey="waitForProductCatalogPage"/> -+ <click selector="{{AdminProductGridActionSection.addProductToggle}}" stepKey="clickAddProductToggle"/> -+ <waitForPageLoad stepKey="waitForProductToggleToSelectProduct"/> -+ <click selector="{{AdminProductGridActionSection.addVirtualProduct}}" stepKey="clickVirtualProduct"/> -+ -+ <!-- Create virtual product out of stock with tier price --> -+ <fillField selector="{{AdminProductFormSection.productName}}" userInput="{{virtualProductOutOfStock.name}}" stepKey="fillProductName"/> -+ <fillField selector="{{AdminProductFormSection.productSku}}" userInput="{{virtualProductOutOfStock.sku}}" stepKey="fillProductSku"/> -+ <fillField selector="{{AdminProductFormSection.productPrice}}" userInput="{{virtualProductOutOfStock.price}}" stepKey="fillProductPrice"/> -+ <click selector="{{AdminProductFormSection.advancedPricingLink}}" stepKey="clickAdvancedPricingLink"/> -+ <click selector="{{AdminProductFormAdvancedPricingSection.customerGroupPriceAddButton}}" stepKey="clickCustomerGroupPriceAddButton"/> -+ <selectOption selector="{{AdminProductFormAdvancedPricingSection.productTierPriceWebsiteSelect('0')}}" userInput="{{tierPriceOnDefault.website_0}}" stepKey="selectProductTierPriceWebsite"/> -+ <selectOption selector="{{AdminProductFormAdvancedPricingSection.productTierPriceCustGroupSelect('0')}}" userInput="{{tierPriceOnDefault.customer_group_0}}" stepKey="selectProductTierPriceCustGroup"/> -+ <fillField selector="{{AdminProductFormAdvancedPricingSection.productTierPriceQtyInput('0')}}" userInput="{{tierPriceOnDefault.qty_0}}" stepKey="fillProductTierPriceQuantityInput"/> -+ <fillField selector="{{AdminProductFormAdvancedPricingSection.productTierPriceFixedPriceInput('0')}}" userInput="{{tierPriceOnDefault.price_0}}" stepKey="selectProductTierPriceFixedPrice"/> -+ <click selector="{{AdminProductFormAdvancedPricingSection.customerGroupPriceAddButton}}" stepKey="clickCustomerGroupPriceAddButtonToAddAnotherRow"/> -+ <selectOption selector="{{AdminProductFormAdvancedPricingSection.productTierPriceWebsiteSelect('1')}}" userInput="{{tierPriceOnDefault.website_1}}" stepKey="clickProductTierPriceWebsite1"/> -+ <selectOption selector="{{AdminProductFormAdvancedPricingSection.productTierPriceCustGroupSelect('1')}}" userInput="{{tierPriceOnDefault.customer_group_1}}" stepKey="clickProductTierPriceCustGroup1"/> -+ <fillField selector="{{AdminProductFormAdvancedPricingSection.productTierPriceQtyInput('1')}}" userInput="{{tierPriceOnDefault.qty_1}}" stepKey="fillProductTierPriceQuantityInput1"/> -+ <fillField selector="{{AdminProductFormAdvancedPricingSection.productTierPriceFixedPriceInput('1')}}" userInput="{{tierPriceOnDefault.price_1}}" stepKey="selectProductTierPriceFixedPrice1"/> -+ <click selector="{{AdminProductFormAdvancedPricingSection.doneButton}}" stepKey="clickDoneButton"/> -+ <fillField selector="{{AdminProductFormSection.productQuantity}}" userInput="{{virtualProductOutOfStock.quantity}}" stepKey="fillVirtualProductQuantity"/> -+ <selectOption selector="{{AdminProductFormSection.stockStatus}}" userInput="{{virtualProductOutOfStock.status}}" stepKey="selectStockStatusOutOfStock"/> -+ <click selector="{{AdminProductFormSection.categoriesDropdown}}" stepKey="clickCategoriesDropDown"/> -+ <fillField selector="{{AdminProductFormSection.searchCategory}}" userInput="$$categoryEntity.name$$" stepKey="fillSearchCategory" /> -+ <click selector="{{AdminProductFormSection.selectCategory($$categoryEntity.name$$)}}" stepKey="clickOnCategory"/> -+ <click selector="{{AdminProductFormSection.done}}" stepKey="clickOnDoneAdvancedCategorySelect"/> -+ <click selector="{{AdminProductSEOSection.sectionHeader}}" stepKey="clickAdminProductSEOSection"/> -+ <fillField selector="{{AdminProductSEOSection.urlKeyInput}}" userInput="{{virtualProductOutOfStock.urlKey}}" stepKey="fillUrlKey"/> -+ <scrollToTopOfPage stepKey="scrollToTopOfAdminProductFormSection"/> -+ <click selector="{{AdminProductFormSection.save}}" stepKey="clickSaveButton"/> -+ <waitForPageLoad stepKey="waitForVirtualProductSaved"/> -+ -+ <!-- Verify we see success message --> -+ <see selector="{{AdminProductFormSection.successMessage}}" userInput="You saved the product." stepKey="seeAssertVirtualProductSuccessMessage"/> -+ -+ <!-- Verify we see created virtual product out of stock with tier price on the storefront page --> -+ <amOnPage url="{{StorefrontProductPage.url(virtualProductOutOfStock.urlKey)}}" stepKey="goToProductPage"/> -+ <waitForPageLoad stepKey="waitForStoreFrontProductPageToLoad"/> -+ <see selector="{{StorefrontProductInfoMainSection.productName}}" userInput="{{virtualProductOutOfStock.name}}" stepKey="seeVirtualProductNameOnStoreFrontPage"/> -+ <see selector="{{StorefrontProductInfoMainSection.productSku}}" userInput="{{virtualProductOutOfStock.sku}}" stepKey="seeVirtualProductSku"/> -+ -+ <!-- Verify customer see product tier price on product page --> -+ <grabTextFrom selector="{{StorefrontProductInfoMainSection.productTierPriceByForTextLabel('1', tierPriceOnDefault.qty_0)}}" stepKey="firstTierPriceText"/> -+ <assertEquals stepKey="assertTierPriceTextOnProductPage1"> -+ <expectedResult type="string">Buy {{tierPriceOnDefault.qty_0}} for ${{tierPriceOnDefault.price_0}} each and save 100%</expectedResult> -+ <actualResult type="variable">firstTierPriceText</actualResult> -+ </assertEquals> -+ <grabTextFrom selector="{{StorefrontProductInfoMainSection.productTierPriceByForTextLabel('2', tierPriceOnDefault.qty_1)}}" stepKey="secondTierPriceText"/> -+ <assertEquals stepKey="assertTierPriceTextOnProductPage2"> -+ <expectedResult type="string">Buy {{tierPriceOnDefault.qty_1}} for ${{tierPriceOnDefault.price_1}} each and save 100%</expectedResult> -+ <actualResult type="variable">secondTierPriceText</actualResult> -+ </assertEquals> -+ -+ <!-- Verify customer see product out of stock status on product page --> -+ <grabTextFrom selector="{{StorefrontProductInfoMainSection.productStockStatus}}" stepKey="productStockAvailableStatus"/> -+ <assertEquals stepKey="assertStockAvailableOnProductPage"> -+ <expectedResult type="string">{{virtualProductOutOfStock.storefrontStatus}}</expectedResult> -+ <actualResult type="variable">productStockAvailableStatus</actualResult> -+ </assertEquals> -+ <grabTextFrom selector="{{StorefrontProductInfoMainSection.productPrice}}" stepKey="productPriceAmount"/> -+ <assertEquals stepKey="assertOldPriceTextOnProductPage"> -+ <expectedResult type="string">${{virtualProductOutOfStock.price}}</expectedResult> -+ <actualResult type="variable">productPriceAmount</actualResult> -+ </assertEquals> -+ </test> -+</tests> -diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateVirtualProductWithCustomOptionsSuiteAndImportOptionsTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateVirtualProductWithCustomOptionsSuiteAndImportOptionsTest.xml -new file mode 100644 -index 00000000000..17769c79677 ---- /dev/null -+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateVirtualProductWithCustomOptionsSuiteAndImportOptionsTest.xml -@@ -0,0 +1,160 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> -+ <test name="AdminCreateVirtualProductWithCustomOptionsSuiteAndImportOptionsTest"> -+ <annotations> -+ <stories value="Create virtual product"/> -+ <title value="Create virtual product with custom options suite and import options"/> -+ <description value="Test log in to Create virtual product and Create virtual product with custom options suite and import options"/> -+ <testCaseId value="MC-6034"/> -+ <severity value="CRITICAL"/> -+ <group value="catalog"/> -+ <group value="mtf_migrated"/> -+ </annotations> -+ <before> -+ <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> -+ <createData entity="SimpleSubCategory" stepKey="categoryEntity"/> -+ </before> -+ <after> -+ <deleteData stepKey="deleteSimpleSubCategory" createDataKey="categoryEntity"/> -+ <actionGroup ref="logout" stepKey="logout"/> -+ </after> -+ -+ <amOnPage url="{{ProductCatalogPage.url}}" stepKey="OpenProductCatalogPage"/> -+ <waitForPageLoad stepKey="waitForProductCatalogPage"/> -+ <click selector="{{AdminProductGridActionSection.addProductToggle}}" stepKey="clickAddProductToggle"/> -+ <waitForPageLoad stepKey="waitForProductToggleToSelectProduct"/> -+ <click selector="{{AdminProductGridActionSection.addVirtualProduct}}" stepKey="clickVirtualProduct"/> -+ -+ <!-- Create virtual product with custom options suite and import options --> -+ <fillField selector="{{AdminProductFormSection.productName}}" userInput="{{virtualProductCustomImportOptions.name}}" stepKey="fillProductName"/> -+ <fillField selector="{{AdminProductFormSection.productSku}}" userInput="{{virtualProductCustomImportOptions.sku}}" stepKey="fillProductSku"/> -+ <fillField selector="{{AdminProductFormSection.productPrice}}" userInput="{{virtualProductCustomImportOptions.price}}" stepKey="fillProductPrice"/> -+ <fillField selector="{{AdminProductFormSection.productQuantity}}" userInput="{{virtualProductCustomImportOptions.quantity}}" stepKey="fillProductQuantity"/> -+ <click selector="{{AdminProductFormSection.productStockStatus}}" stepKey="clickProductStockStatus"/> -+ <click selector="{{AdminProductFormSection.categoriesDropdown}}" stepKey="clickCategoriesDropDown"/> -+ <fillField selector="{{AdminProductFormSection.searchCategory}}" userInput="$$categoryEntity.name$$" stepKey="fillSearchCategory" /> -+ <click selector="{{AdminProductFormSection.selectCategory($$categoryEntity.name$$)}}" stepKey="clickOnCategory"/> -+ <click selector="{{AdminProductFormSection.done}}" stepKey="clickOnDoneAdvancedCategorySelect"/> -+ <click selector="{{AdminProductSEOSection.sectionHeader}}" stepKey="clickAdminProductSEOSection"/> -+ <fillField selector="{{AdminProductSEOSection.urlKeyInput}}" userInput="{{virtualProductCustomImportOptions.urlKey}}" stepKey="fillUrlKey"/> -+ <click selector="{{AdminProductCustomizableOptionsSection.checkIfCustomizableOptionsTabOpen}}" stepKey="clickAdminProductCustomizableOption"/> -+ -+ <!-- Create virtual product with customizable options dataSet1 --> -+ <click selector="{{AdminProductCustomizableOptionsSection.addOptionBtn}}" stepKey="clickAddOptionButton"/> -+ <waitForPageLoad stepKey="waitForFirstOption"/> -+ <fillField selector="{{AdminProductCustomizableOptionsSection.optionTitleInput('0')}}" userInput="{{virtualProductCustomizableOption1.title}}" stepKey="fillOptionTitleForFirstDataSet"/> -+ <click selector="{{AdminProductCustomizableOptionsSection.optionTypeDropDown('1')}}" stepKey="selectOptionTypeDropDownFirstDataSet"/> -+ <click selector="{{AdminProductCustomizableOptionsSection.optionTypeItem('1', virtualProductCustomizableOption1.type)}}" stepKey="selectOptionFieldFromDropDownForFirstDataSet"/> -+ <checkOption selector="{{AdminProductCustomizableOptionsSection.requiredCheckBox('0')}}" stepKey="checkRequiredCheckBoxForFirstDataSet"/> -+ <fillField selector="{{AdminProductCustomizableOptionsSection.optionPrice('0')}}" userInput="{{virtualProductCustomizableOption1.option_0_price}}" stepKey="fillOptionPriceForFirstDataSet"/> -+ <selectOption selector="{{AdminProductCustomizableOptionsSection.optionPriceType('0')}}" userInput="{{virtualProductCustomizableOption1.option_0_price_type}}" stepKey="selectOptionPriceTypeForFirstDataSet"/> -+ <fillField selector="{{AdminProductCustomizableOptionsSection.optionSku('0')}}" userInput="{{virtualProductCustomizableOption1.option_0_sku}}" stepKey="fillOptionSkuForFirstDataSet"/> -+ <fillField selector="{{AdminProductCustomizableOptionsSection.maxCharactersInput('0')}}" userInput="{{virtualProductCustomizableOption1.option_0_max_characters}}" stepKey="fillOptionMaxCharactersForFirstDataSet"/> -+ -+ <!-- Create virtual product with customizable options dataSet2 --> -+ <click selector="{{AdminProductCustomizableOptionsSection.addOptionBtn}}" stepKey="clickAddOptionButtonForSecondDataSet"/> -+ <waitForPageLoad stepKey="waitForSecondDataSetToLoad"/> -+ <fillField selector="{{AdminProductCustomizableOptionsSection.optionTitleInput('1')}}" userInput="{{virtualProductCustomizableOption2.title}}" stepKey="fillOptionTitleForSecondDataSet"/> -+ <click selector="{{AdminProductCustomizableOptionsSection.optionTypeDropDown('2')}}" stepKey="selectOptionTypeDropDownSecondDataSet"/> -+ <click selector="{{AdminProductCustomizableOptionsSection.optionTypeItem('2', virtualProductCustomizableOption2.type)}}" stepKey="selectOptionFieldFromDropDownForSecondDataSet"/> -+ <checkOption selector="{{AdminProductCustomizableOptionsSection.requiredCheckBox('1')}}" stepKey="checkRequiredCheckBoxForSecondDataSet"/> -+ <fillField selector="{{AdminProductCustomizableOptionsSection.optionPrice('1')}}" userInput="{{virtualProductCustomizableOption2.option_0_price}}" stepKey="fillOptionPriceForSecondDataSet"/> -+ <selectOption selector="{{AdminProductCustomizableOptionsSection.optionPriceType('1')}}" userInput="{{virtualProductCustomizableOption2.option_0_price_type}}" stepKey="selectOptionPriceTypeForSecondDataSet"/> -+ <fillField selector="{{AdminProductCustomizableOptionsSection.optionSku('1')}}" userInput="{{virtualProductCustomizableOption2.option_0_sku}}" stepKey="fillOptionSkuForSecondDataSet"/> -+ <fillField selector="{{AdminProductCustomizableOptionsSection.maxCharactersInput('1')}}" userInput="{{virtualProductCustomizableOption2.option_0_max_characters}}" stepKey="fillOptionMaxCharactersForSecondDataSet"/> -+ -+ <!-- Create virtual product with customizable options dataSet3 --> -+ <click selector="{{AdminProductCustomizableOptionsSection.addOptionBtn}}" stepKey="clickAddOptionButtonForThirdSetOfData"/> -+ <waitForPageLoad stepKey="waitForThirdSetOfDataToLoad"/> -+ <fillField selector="{{AdminProductCustomizableOptionsSection.optionTitleInput('2')}}" userInput="{{virtualProductCustomizableOption3.title}}" stepKey="fillOptionTitleForThirdDataSet"/> -+ <click selector="{{AdminProductCustomizableOptionsSection.optionTypeDropDown('3')}}" stepKey="selectOptionTypeDropDownForThirdDataSet"/> -+ <click selector="{{AdminProductCustomizableOptionsSection.optionTypeItem('3', virtualProductCustomizableOption3.type)}}" stepKey="selectOptionFieldFromDropDownForThirdDataSet"/> -+ <checkOption selector="{{AdminProductCustomizableOptionsSection.requiredCheckBox('2')}}" stepKey="checkRequiredCheckBoxForThirdDataSet"/> -+ <click selector="{{AdminProductCustomizableOptionsSection.addValue}}" stepKey="clickAddOptionButtonForThirdDataSetToAddFirstRow"/> -+ <fillField selector="{{AdminProductCustomizableOptionsSection.fillOptionValueTitle(virtualProductCustomizableOption3.title,'0')}}" userInput="{{virtualProductCustomizableOption3.option_0_title}}" stepKey="fillOptionTitleForThirdDataSetFirstRow"/> -+ <fillField selector="{{AdminProductCustomizableOptionsSection.fillOptionValuePrice(virtualProductCustomizableOption3.title,'0')}}" userInput="{{virtualProductCustomizableOption3.option_0_price}}" stepKey="fillOptionPriceForThirdDataSetFirstRow"/> -+ <selectOption selector="{{AdminProductCustomizableOptionsSection.clickSelectPriceType(virtualProductCustomizableOption3.title,'0')}}" userInput="{{virtualProductCustomizableOption3.option_0_price_type}}" stepKey="selectOptionPriceTypeForThirdDataSetFirstRow"/> -+ <fillField selector="{{AdminProductCustomizableOptionsSection.fillOptionValueSku(virtualProductCustomizableOption3.title,'0')}}" userInput="{{virtualProductCustomizableOption3.option_0_sku}}" stepKey="fillOptionSkuForThirdDataSetFirstRow"/> -+ <click selector="{{AdminProductCustomizableOptionsSection.addValue}}" stepKey="clickAddOptionButtonForThirdDataSetToAddSecondRow"/> -+ <fillField selector="{{AdminProductCustomizableOptionsSection.fillOptionValueTitle(virtualProductCustomizableOption3.title,'1')}}" userInput="{{virtualProductCustomizableOption3.option_1_title}}" stepKey="fillOptionTitleForThirdDataSetSecondRow"/> -+ <fillField selector="{{AdminProductCustomizableOptionsSection.fillOptionValuePrice(virtualProductCustomizableOption3.title,'1')}}" userInput="{{virtualProductCustomizableOption3.option_1_price}}" stepKey="fillOptionPriceForThirdDataSetSecondRow"/> -+ <selectOption selector="{{AdminProductCustomizableOptionsSection.clickSelectPriceType(virtualProductCustomizableOption3.title,'1')}}" userInput="{{virtualProductCustomizableOption3.option_1_price_type}}" stepKey="selectOptionPriceTypeForThirdDataSetSecondRow"/> -+ <fillField selector="{{AdminProductCustomizableOptionsSection.fillOptionValueSku(virtualProductCustomizableOption3.title,'1')}}" userInput="{{virtualProductCustomizableOption3.option_1_sku}}" stepKey="fillOptionSkuForThirdDataSetSecondRow"/> -+ -+ <!-- Create virtual product with customizable options dataSet4 --> -+ <scrollToTopOfPage stepKey="scrollToAddOptionButton"/> -+ <click selector="{{AdminProductCustomizableOptionsSection.addOptionBtn}}" stepKey="clickAddOptionButtonForFourthDataSet"/> -+ <waitForPageLoad stepKey="waitForFourthDataSetToLoad"/> -+ <fillField selector="{{AdminProductCustomizableOptionsSection.optionTitleInput('3')}}" userInput="{{virtualProductCustomizableOption4.title}}" stepKey="fillOptionTitleForFourthDataSet"/> -+ <click selector="{{AdminProductCustomizableOptionsSection.optionTypeDropDown('4')}}" stepKey="selectOptionTypeDropDownForFourthSetOfData"/> -+ <click selector="{{AdminProductCustomizableOptionsSection.optionTypeItem('4', virtualProductCustomizableOption4.type)}}" stepKey="selectOptionFieldFromDropDownForFourthDataSet"/> -+ <checkOption selector="{{AdminProductCustomizableOptionsSection.requiredCheckBox('3')}}" stepKey="checkRequiredCheckBoxForFourthDataSet"/> -+ <click selector="{{AdminProductCustomizableOptionsSection.addValue}}" stepKey="clickAddOptionButtonForFourthDataSetToAddFirstRow"/> -+ <fillField selector="{{AdminProductCustomizableOptionsSection.fillOptionValueTitle(virtualProductCustomizableOption4.title,'0')}}" userInput="{{virtualProductCustomizableOption4.option_0_title}}" stepKey="fillOptionTitleForFourthDataSetFirstRow"/> -+ <fillField selector="{{AdminProductCustomizableOptionsSection.fillOptionValuePrice(virtualProductCustomizableOption4.title,'0')}}" userInput="{{virtualProductCustomizableOption4.option_0_price}}" stepKey="fillOptionPriceForFourthDataSetFirstRow"/> -+ <selectOption selector="{{AdminProductCustomizableOptionsSection.clickSelectPriceType(virtualProductCustomizableOption4.title,'0')}}" userInput="{{virtualProductCustomizableOption4.option_0_price_type}}" stepKey="selectOptionPriceTypeForFourthDataSetFirstRow"/> -+ <fillField selector="{{AdminProductCustomizableOptionsSection.fillOptionValueSku(virtualProductCustomizableOption4.title,'0')}}" userInput="{{virtualProductCustomizableOption4.option_0_sku}}" stepKey="fillOptionSkuForFourthDataSetFirstRow"/> -+ <click selector="{{AdminProductCustomizableOptionsSection.addValue}}" stepKey="clickAddOptionButtonForFourthDataSetToAddSecondRow"/> -+ <fillField selector="{{AdminProductCustomizableOptionsSection.fillOptionValueTitle(virtualProductCustomizableOption4.title,'1')}}" userInput="{{virtualProductCustomizableOption4.option_1_title}}" stepKey="fillOptionTitleForFourthDataSetSecondRow"/> -+ <fillField selector="{{AdminProductCustomizableOptionsSection.fillOptionValuePrice(virtualProductCustomizableOption4.title,'1')}}" userInput="{{virtualProductCustomizableOption4.option_1_price}}" stepKey="fillOptionPriceForFourthDataSetSecondRow"/> -+ <selectOption selector="{{AdminProductCustomizableOptionsSection.clickSelectPriceType(virtualProductCustomizableOption4.title,'1')}}" userInput="{{virtualProductCustomizableOption4.option_1_price_type}}" stepKey="selectOptionPriceTypeForFourthDataSetSecondRow"/> -+ <fillField selector="{{AdminProductCustomizableOptionsSection.fillOptionValueSku(virtualProductCustomizableOption4.title,'1')}}" userInput="{{virtualProductCustomizableOption4.option_1_sku}}" stepKey="fillOptionSkuForFourthDataSetSecondRow"/> -+ <scrollToTopOfPage stepKey="scrollToTopOfAdminProductFormSection"/> -+ <click selector="{{AdminProductFormSection.save}}" stepKey="clickSaveButton"/> -+ <waitForPageLoad stepKey="waitForVirtualProductSaved"/> -+ -+ <!-- Verify we see success message --> -+ <see selector="{{AdminProductFormSection.successMessage}}" userInput="You saved the product." stepKey="seeAssertVirtualProductSuccessMessage"/> -+ -+ <!-- Verify customer see created virtual product with custom options suite and import options(from above step) on storefront page and is searchable by sku --> -+ <amOnPage url="{{StorefrontProductPage.url(virtualProductCustomImportOptions.urlKey)}}" stepKey="goToProductPage"/> -+ <waitForPageLoad stepKey="waitForStoreFrontProductPageLoad"/> -+ <fillField selector="{{StorefrontQuickSearchResultsSection.searchTextBox}}" userInput="{{virtualProductCustomImportOptions.sku}}" stepKey="fillVirtualProductName"/> -+ <waitForPageLoad stepKey="waitForSearchTextBox"/> -+ <click selector="{{StorefrontQuickSearchResultsSection.searchTextBoxButton}}" stepKey="clickSearchTextBoxButton"/> -+ <waitForPageLoad stepKey="waitForSearch"/> -+ <see selector="{{StorefrontQuickSearchResultsSection.productLink}}" userInput="{{virtualProductCustomImportOptions.name}}" stepKey="seeVirtualProductName"/> -+ <click selector="{{StorefrontQuickSearchResultsSection.productLink}}" stepKey="openSearchedProduct"/> -+ -+ <!-- Verify we see created virtual product with custom options suite and import options on the storefront page --> -+ <see selector="{{StorefrontProductInfoMainSection.productName}}" userInput="{{virtualProductCustomImportOptions.name}}" stepKey="seeVirtualProductNameOnStoreFrontPage"/> -+ <see selector="{{StorefrontProductInfoMainSection.productSku}}" userInput="{{virtualProductCustomImportOptions.sku}}" stepKey="seeVirtualProductSku"/> -+ <grabTextFrom selector="{{StorefrontProductInfoMainSection.productStockStatus}}" stepKey="productStockAvailableStatus"/> -+ <assertEquals stepKey="assertStockAvailableOnProductPage"> -+ <expectedResult type="string">{{virtualProductCustomImportOptions.storefrontStatus}}</expectedResult> -+ <actualResult type="variable">productStockAvailableStatus</actualResult> -+ </assertEquals> -+ <grabTextFrom selector="{{StorefrontProductInfoMainSection.productPrice}}" stepKey="productPriceAmount"/> -+ <assertEquals stepKey="assertOldPriceTextOnProductPage"> -+ <expectedResult type="string">${{virtualProductCustomImportOptions.price}}</expectedResult> -+ <actualResult type="variable">productPriceAmount</actualResult> -+ </assertEquals> -+ -+ <!--Verify we see customizable options are Required --> -+ <seeElement selector="{{StorefrontProductInfoMainSection.requiredCustomInput(virtualProductCustomizableOption1.title)}}" stepKey="verifyFirstCustomOptionIsRequired" /> -+ <seeElement selector="{{StorefrontProductInfoMainSection.requiredCustomInput(virtualProductCustomizableOption2.title)}}" stepKey="verifySecondCustomOptionIsRequired" /> -+ <seeElement selector="{{StorefrontProductInfoMainSection.requiredCustomSelect(virtualProductCustomizableOption3.title)}}" stepKey="verifyThirdCustomOptionIsRequired" /> -+ <seeElement selector="{{StorefrontProductInfoMainSection.requiredCustomSelect(virtualProductCustomizableOption4.title)}}" stepKey="verifyFourthCustomOptionIsRequired" /> -+ -+ <!--Verify we see customizable option titles and prices --> -+ <grabMultiple selector="{{StorefrontProductInfoMainSection.allCustomOptionLabels}}" stepKey="allCustomOptionLabels" /> -+ <assertEquals stepKey="verifyLabels"> -+ <actualResult type="variable">allCustomOptionLabels</actualResult> -+ <expectedResult type="array">[{{virtualProductCustomizableOption1.title}} + ${{virtualProductCustomizableOption1.option_0_price}}, {{virtualProductCustomizableOption2.title}} + ${{virtualProductCustomizableOption2.option_0_price}}, {{virtualProductCustomizableOption3.title}}, {{virtualProductCustomizableOption4.title}}]</expectedResult> -+ </assertEquals> -+ <grabAttributeFrom userInput="for" selector="{{StorefrontProductInfoMainSection.customOptionLabel(virtualProductCustomizableOption4.title)}}" stepKey="fourthOptionId" /> -+ <grabMultiple selector="{{StorefrontProductInfoMainSection.customSelectOptions({$fourthOptionId})}}" stepKey="grabFourthOptions" /> -+ <assertEquals stepKey="assertFourthSelectOptions"> -+ <actualResult type="variable">grabFourthOptions</actualResult> -+ <expectedResult type="array">['-- Please Select --', {{virtualProductCustomizableOption4.option_0_title}} +$900.90, {{virtualProductCustomizableOption4.option_1_title}} +$20.02]</expectedResult> -+ </assertEquals> -+ </test> -+</tests> -diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateVirtualProductWithTierPriceForGeneralGroupTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateVirtualProductWithTierPriceForGeneralGroupTest.xml -new file mode 100644 -index 00000000000..78247f49435 ---- /dev/null -+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateVirtualProductWithTierPriceForGeneralGroupTest.xml -@@ -0,0 +1,132 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> -+ <test name="AdminCreateVirtualProductWithTierPriceForGeneralGroupTest"> -+ <annotations> -+ <stories value="Create virtual product"/> -+ <title value="Create virtual product with tier price for General group"/> -+ <description value="Test log in to Create virtual product and Create virtual product with tier price for General group"/> -+ <testCaseId value="MC-6033"/> -+ <severity value="CRITICAL"/> -+ <group value="catalog"/> -+ <group value="mtf_migrated"/> -+ </annotations> -+ <before> -+ <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> -+ <createData entity="SimpleSubCategory" stepKey="categoryEntity"/> -+ <createData entity="Simple_US_CA_Customer" stepKey="customer" /> -+ </before> -+ <after> -+ <deleteData stepKey="deleteSimpleSubCategory" createDataKey="categoryEntity"/> -+ <deleteData stepKey="deleteCustomer" createDataKey="customer"/> -+ <actionGroup ref="logout" stepKey="logout"/> -+ </after> -+ -+ <amOnPage url="{{ProductCatalogPage.url}}" stepKey="OpenProductCatalogPage"/> -+ <waitForPageLoad stepKey="waitForProductCatalogPage"/> -+ <click selector="{{AdminProductGridActionSection.addProductToggle}}" stepKey="clickAddProductToggle"/> -+ <waitForPageLoad stepKey="waitForProductToggleToSelectProduct"/> -+ <click selector="{{AdminProductGridActionSection.addVirtualProduct}}" stepKey="clickVirtualProduct"/> -+ -+ <!-- Create virtual product with tier price for general group --> -+ <fillField selector="{{AdminProductFormSection.productName}}" userInput="{{virtualProductGeneralGroup.name}}" stepKey="fillProductName"/> -+ <fillField selector="{{AdminProductFormSection.productSku}}" userInput="{{virtualProductGeneralGroup.sku}}" stepKey="fillProductSku"/> -+ <fillField selector="{{AdminProductFormSection.productPrice}}" userInput="{{virtualProductGeneralGroup.price}}" stepKey="fillProductPrice"/> -+ <click selector="{{AdminProductFormSection.advancedPricingLink}}" stepKey="clickAdvancedPricingLink"/> -+ <click selector="{{AdminProductFormAdvancedPricingSection.customerGroupPriceAddButton}}" stepKey="clickCustomerGroupPriceAddButton"/> -+ <selectOption selector="{{AdminProductFormAdvancedPricingSection.productTierPriceWebsiteSelect('0')}}" userInput="{{tierPriceOnGeneralGroup.website}}" stepKey="selectProductTierPriceWebsite"/> -+ <selectOption selector="{{AdminProductFormAdvancedPricingSection.productTierPriceCustGroupSelect('0')}}" userInput="{{tierPriceOnGeneralGroup.customer_group}}" stepKey="selectProductTierPriceGroup"/> -+ <scrollTo selector="{{AdminProductFormAdvancedPricingSection.productTierPriceQtyInput('0')}}" x="50" y="0" stepKey="scrollToProductTierPriceQuantityInputTextBox"/> -+ <fillField selector="{{AdminProductFormAdvancedPricingSection.productTierPriceQtyInput('0')}}" userInput="{{tierPriceOnGeneralGroup.qty}}" stepKey="fillProductTierPriceQuantityInput"/> -+ <fillField selector="{{AdminProductFormAdvancedPricingSection.productTierPriceFixedPriceInput('0')}}" userInput="{{tierPriceOnGeneralGroup.price}}" stepKey="fillProductTierPriceFixedPrice"/> -+ <click selector="{{AdminProductFormAdvancedPricingSection.doneButton}}" stepKey="clickDoneButton"/> -+ <selectOption selector="{{AdminProductFormSection.productTaxClass}}" userInput="{{virtualProductGeneralGroup.productTaxClass}}" stepKey="selectProductTaxClass"/> -+ <fillField selector="{{AdminProductFormSection.productQuantity}}" userInput="{{virtualProductGeneralGroup.quantity}}" stepKey="fillProductQuantity"/> -+ <click selector="{{AdminProductFormSection.productStockStatus}}" stepKey="clickProductStockStatus"/> -+ <click selector="{{AdminProductFormSection.categoriesDropdown}}" stepKey="clickCategoriesDropDown"/> -+ <fillField selector="{{AdminProductFormSection.searchCategory}}" userInput="$$categoryEntity.name$$" stepKey="fillSearchCategory" /> -+ <click selector="{{AdminProductFormSection.selectCategory($$categoryEntity.name$$)}}" stepKey="clickOnCategory"/> -+ <click selector="{{AdminProductFormSection.done}}" stepKey="clickOnDoneAdvancedCategorySelect"/> -+ <selectOption selector="{{AdminProductFormSection.visibility}}" userInput="{{virtualProductGeneralGroup.visibility}}" stepKey="selectVisibility"/> -+ <click selector="{{AdminProductSEOSection.sectionHeader}}" stepKey="clickAdminProductSEOSectionHeader"/> -+ <fillField selector="{{AdminProductSEOSection.urlKeyInput}}" userInput="{{virtualProductGeneralGroup.urlKey}}" stepKey="fillUrlKeyInput"/> -+ <scrollToTopOfPage stepKey="scrollToTopOfAdminProductFormSection"/> -+ <click selector="{{AdminProductFormSection.save}}" stepKey="clickSaveButton"/> -+ <waitForPageLoad stepKey="waitForVirtualProductSaved"/> -+ -+ <!-- Verify we see success message --> -+ <see selector="{{AdminProductFormSection.successMessage}}" userInput="You saved the product." stepKey="seeAssertVirtualProductSuccessMessage"/> -+ -+ <amOnPage url="{{ProductCatalogPage.url}}" stepKey="OpenProductCatalogPage1"/> -+ <waitForPageLoad stepKey="waitForProductCatalogPage1"/> -+ <conditionalClick selector="{{AdminProductGridFilterSection.clearAll}}" dependentSelector="{{AdminProductGridFilterSection.clearAll}}" visible="true" stepKey="checkRetailCustomerTaxClass" /> -+ <fillField selector="{{AdminProductGridFilterSection.keywordSearch}}" userInput="{{virtualProductGeneralGroup.name}}" stepKey="fillVirtualProductName"/> -+ <click selector="{{AdminProductGridFilterSection.keywordSearchButton}}" stepKey="clickKeywordSearchButton"/> -+ <waitForPageLoad stepKey="waitForProductSearch"/> -+ <click selector="{{AdminProductGridFilterSection.nthRow('1')}}" stepKey="clickFirstRowToVerifyCreatedVirtualProduct"/> -+ <waitForPageLoad stepKey="waitUntilProductIsOpened"/> -+ -+ <!-- Verify we see created virtual product with tier price for general group(from the above step) in the product form page --> -+ <seeInField selector="{{AdminProductFormSection.productName}}" userInput="{{virtualProductGeneralGroup.name}}" stepKey="seeProductName"/> -+ <seeInField selector="{{AdminProductFormSection.productSku}}" userInput="{{virtualProductGeneralGroup.sku}}" stepKey="seeProductSku"/> -+ <seeInField selector="{{AdminProductFormSection.productPrice}}" userInput="{{virtualProductGeneralGroup.price}}" stepKey="seeProductPrice"/> -+ <click selector="{{AdminProductFormSection.advancedPricingLink}}" stepKey="clickAdvancedPricingLink1"/> -+ <seeOptionIsSelected selector="{{AdminProductFormAdvancedPricingSection.productTierPriceWebsiteSelect('0')}}" userInput="{{tierPriceOnGeneralGroup.website}}" stepKey="seeProductTierPriceWebsite"/> -+ <seeOptionIsSelected selector="{{AdminProductFormAdvancedPricingSection.productTierPriceCustGroupSelect('0')}}" userInput="{{tierPriceOnGeneralGroup.customer_group}}" stepKey="seeProductTierPriceGroup"/> -+ <seeInField selector="{{AdminProductFormAdvancedPricingSection.productTierPriceQtyInput('0')}}" userInput="{{tierPriceOnGeneralGroup.qty}}" stepKey="seeProductTierPriceQuantityInput"/> -+ <seeInField selector="{{AdminProductFormAdvancedPricingSection.productTierPriceFixedPriceInput('0')}}" userInput="{{tierPriceOnGeneralGroup.price}}" stepKey="seeProductTierPriceFixedPrice"/> -+ <click selector="{{AdminProductFormAdvancedPricingSection.advancedPricingCloseButton}}" stepKey="clickAdvancedPricingCloseButton"/> -+ <seeInField selector="{{AdminProductFormSection.productTaxClass}}" userInput="{{virtualProductGeneralGroup.productTaxClass}}" stepKey="seeProductTaxClass"/> -+ <seeInField selector="{{AdminProductFormSection.productQuantity}}" userInput="{{virtualProductGeneralGroup.quantity}}" stepKey="seeProductQuantity"/> -+ <seeInField selector="{{AdminProductFormSection.productStockStatus}}" userInput="{{virtualProductGeneralGroup.status}}" stepKey="seeProductStockStatus"/> -+ <click selector="{{AdminProductFormSection.categoriesDropdown}}" stepKey="clickCategoriesDropDownToVerify"/> -+ <grabMultiple selector="{{AdminProductFormSection.selectMultipleCategories}}" stepKey="selectedCategories" /> -+ <assertEquals stepKey="assertSelectedCategories"> -+ <actualResult type="variable">selectedCategories</actualResult> -+ <expectedResult type="array">[$$categoryEntity.name$$]</expectedResult> -+ </assertEquals> -+ <click selector="{{AdminProductFormSection.done}}" stepKey="clickOnDoneOnCategorySelect"/> -+ <seeInField selector="{{AdminProductFormSection.visibility}}" userInput="{{virtualProductGeneralGroup.visibility}}" stepKey="seeVisibility"/> -+ <scrollTo selector="{{AdminProductSEOSection.sectionHeader}}" x="0" y="-80" stepKey="scrollToAdminProductSEOSection1"/> -+ <click selector="{{AdminProductSEOSection.sectionHeader}}" stepKey="clickAdminProductSEOSection1"/> -+ <seeInField selector="{{AdminProductSEOSection.urlKeyInput}}" userInput="{{virtualProductGeneralGroup.urlKey}}" stepKey="seeUrlKey"/> -+ -+ <!--Verify customer see created virtual product on category page --> -+ <amOnPage url="{{StorefrontCategoryPage.url($$categoryEntity.name$$)}}" stepKey="openCategoryPage"/> -+ <waitForPageLoad stepKey="waitForCategoryPageToLoad"/> -+ <see selector="{{StorefrontCategoryMainSection.productLink}}" userInput="{{virtualProductGeneralGroup.name}}" stepKey="seeVirtualProductNameOnCategoryPage"/> -+ -+ <!-- Verify customer see created virtual product with tier price for general group(from above step) in storefront page with customer --> -+ <actionGroup ref="LoginToStorefrontActionGroup" stepKey="loginAsCustomer"> -+ <argument name="Customer" value="$$customer$$" /> -+ </actionGroup> -+ <amOnPage url="{{StorefrontHomePage.url}}" stepKey="goToStorefront"/> -+ <waitForPageLoad stepKey="waitForStoreFrontProductPageLoad"/> -+ <fillField selector="{{StorefrontQuickSearchResultsSection.searchTextBox}}" userInput="{{virtualProductGeneralGroup.name}}" stepKey="fillVirtualProductNameInSearchTextBox"/> -+ <waitForPageLoad stepKey="waitForSearchTextBox"/> -+ <click selector="{{StorefrontQuickSearchResultsSection.searchTextBoxButton}}" stepKey="clickSearchTextBoxButton"/> -+ <waitForPageLoad stepKey="waitForSearch"/> -+ <see selector="{{StorefrontQuickSearchResultsSection.productLink}}" userInput="{{virtualProductGeneralGroup.name}}" stepKey="seeVirtualProductName"/> -+ <grabTextFrom selector="{{StorefrontQuickSearchResultsSection.asLowAsLabel}}" stepKey="tierPriceTextOnStorefrontPage"/> -+ -+ <!-- Verify customer see created virtual product with tier price --> -+ <assertEquals stepKey="assertTierPriceTextOnCategoryPage"> -+ <expectedResult type="string">As low as ${{tierPriceOnGeneralGroup.price}}</expectedResult> -+ <actualResult type="variable">tierPriceTextOnStorefrontPage</actualResult> -+ </assertEquals> -+ <click selector="{{StorefrontQuickSearchResultsSection.productLink}}" stepKey="openSearchedProduct"/> -+ <waitForPageLoad stepKey="waitForProductPageToBeLoaded"/> -+ <grabTextFrom selector="{{StorefrontProductInfoMainSection.tierPriceText}}" stepKey="tierPriceText"/> -+ <assertEquals stepKey="assertTierPriceTextOnProductPage"> -+ <expectedResult type="string">Buy {{tierPriceOnGeneralGroup.qty}} for ${{tierPriceOnGeneralGroup.price}} each and save 20%</expectedResult> -+ <actualResult type="variable">tierPriceText</actualResult> -+ </assertEquals> -+ </test> -+</tests> -diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateVirtualProductWithTierPriceTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateVirtualProductWithTierPriceTest.xml -new file mode 100644 -index 00000000000..6ef2569945f ---- /dev/null -+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateVirtualProductWithTierPriceTest.xml -@@ -0,0 +1,123 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> -+ <test name="AdminCreateVirtualProductWithTierPriceTest"> -+ <annotations> -+ <stories value="Create virtual product"/> -+ <title value="Create virtual product with tier price"/> -+ <description value="Test log in to Create virtual product and Create virtual product with tier price"/> -+ <testCaseId value="MC-6032"/> -+ <severity value="CRITICAL"/> -+ <group value="catalog"/> -+ <group value="mtf_migrated"/> -+ </annotations> -+ <before> -+ <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> -+ <createData entity="SimpleSubCategory" stepKey="categoryEntity"/> -+ </before> -+ <after> -+ <deleteData stepKey="deleteSimpleSubCategory" createDataKey="categoryEntity"/> -+ <actionGroup ref="logout" stepKey="logout"/> -+ </after> -+ -+ <amOnPage url="{{ProductCatalogPage.url}}" stepKey="OpenProductCatalogPage"/> -+ <waitForPageLoad stepKey="waitForProductCatalogPage"/> -+ <click selector="{{AdminProductGridActionSection.addProductToggle}}" stepKey="clickAddProductToggle"/> -+ <waitForPageLoad stepKey="waitForProductToggleToSelectProduct"/> -+ <click selector="{{AdminProductGridActionSection.addVirtualProduct}}" stepKey="clickVirtualProduct"/> -+ -+ <!-- Create virtual product with tier price --> -+ <fillField selector="{{AdminProductFormSection.productName}}" userInput="{{virtualProductBigQty.name}}" stepKey="fillProductName"/> -+ <fillField selector="{{AdminProductFormSection.productSku}}" userInput="{{virtualProductBigQty.sku}}" stepKey="fillProductSku"/> -+ <fillField selector="{{AdminProductFormSection.productPrice}}" userInput="{{virtualProductBigQty.price}}" stepKey="fillProductPrice"/> -+ <click selector="{{AdminProductFormSection.advancedPricingLink}}" stepKey="clickAdvancedPricingLink"/> -+ <click selector="{{AdminProductFormAdvancedPricingSection.customerGroupPriceAddButton}}" stepKey="clickCustomerGroupPriceAddButton"/> -+ <scrollTo selector="{{AdminProductFormAdvancedPricingSection.productTierPriceQtyInput('0')}}" x="50" y="0" stepKey="scrollToProductTierPriceQuantityInputTextBox"/> -+ <fillField selector="{{AdminProductFormAdvancedPricingSection.productTierPriceQtyInput('0')}}" userInput="{{tierPriceOnVirtualProduct.qty}}" stepKey="fillProductTierPriceQuantityInput"/> -+ <fillField selector="{{AdminProductFormAdvancedPricingSection.productTierPriceFixedPriceInput('0')}}" userInput="{{tierPriceOnVirtualProduct.price}}" stepKey="selectProductTierPriceFixedPrice"/> -+ <click selector="{{AdminProductFormAdvancedPricingSection.doneButton}}" stepKey="clickDoneButton"/> -+ <selectOption selector="{{AdminProductFormSection.productTaxClass}}" userInput="{{virtualProductBigQty.productTaxClass}}" stepKey="selectProductTaxClass"/> -+ <fillField selector="{{AdminProductFormSection.productQuantity}}" userInput="{{virtualProductBigQty.quantity}}" stepKey="fillProductQuantity"/> -+ <selectOption selector="{{AdminProductFormSection.stockStatus}}" userInput="{{virtualProductBigQty.status}}" stepKey="selectStockStatusInStock"/> -+ <click selector="{{AdminProductFormSection.categoriesDropdown}}" stepKey="clickCategoriesDropDown"/> -+ <fillField selector="{{AdminProductFormSection.searchCategory}}" userInput="$$categoryEntity.name$$" stepKey="fillSearchCategory" /> -+ <click selector="{{AdminProductFormSection.selectCategory($$categoryEntity.name$$)}}" stepKey="clickOnCategory"/> -+ <click selector="{{AdminProductFormSection.done}}" stepKey="clickOnDoneAdvancedCategorySelect"/> -+ <selectOption selector="{{AdminProductFormSection.visibility}}" userInput="{{virtualProductBigQty.visibility}}" stepKey="selectVisibility"/> -+ <click selector="{{AdminProductSEOSection.sectionHeader}}" stepKey="clickAdminProductSEOSection"/> -+ <fillField selector="{{AdminProductSEOSection.urlKeyInput}}" userInput="{{virtualProductBigQty.urlKey}}" stepKey="fillUrlKey"/> -+ <scrollToTopOfPage stepKey="scrollToTopOfAdminProductFormSection"/> -+ <click selector="{{AdminProductFormSection.save}}" stepKey="clickSaveButton"/> -+ <waitForPageLoad stepKey="waitForVirtualProductSaved"/> -+ -+ <!-- Verify we see success message --> -+ <see selector="{{AdminProductFormSection.successMessage}}" userInput="You saved the product." stepKey="seeAssertVirtualProductSuccessMessage"/> -+ -+ <amOnPage url="{{ProductCatalogPage.url}}" stepKey="OpenProductCatalogPage1"/> -+ <waitForPageLoad stepKey="waitForProductCatalogPage1"/> -+ <conditionalClick selector="{{AdminProductGridFilterSection.clearAll}}" dependentSelector="{{AdminProductGridFilterSection.clearAll}}" visible="true" stepKey="checkRetailCustomerTaxClass" /> -+ <fillField selector="{{AdminProductGridFilterSection.keywordSearch}}" userInput="{{virtualProductBigQty.name}}" stepKey="fillProductName1"/> -+ <click selector="{{AdminProductGridFilterSection.keywordSearchButton}}" stepKey="clickKeywordSearchButton"/> -+ <waitForPageLoad stepKey="waitForProductSearch"/> -+ <click selector="{{AdminProductGridFilterSection.nthRow('1')}}" stepKey="clickFirstRowToVerifyCreatedVirtualProduct"/> -+ <waitForPageLoad stepKey="waitUntilProductIsOpened" /> -+ -+ <!-- Verify we see created virtual product with tier price(from the above step) in the product form page --> -+ <seeInField selector="{{AdminProductFormSection.productName}}" userInput="{{virtualProductBigQty.name}}" stepKey="seeProductName"/> -+ <seeInField selector="{{AdminProductFormSection.productSku}}" userInput="{{virtualProductBigQty.sku}}" stepKey="seeProductSku"/> -+ <seeInField selector="{{AdminProductFormSection.productPrice}}" userInput="{{virtualProductBigQty.price}}" stepKey="seeProductPrice"/> -+ <click selector="{{AdminProductFormSection.advancedPricingLink}}" stepKey="clickAdvancedPricingLink1"/> -+ <seeInField selector="{{AdminProductFormAdvancedPricingSection.productTierPriceQtyInput('0')}}" userInput="{{tierPriceOnVirtualProduct.qty}}" stepKey="seeProductTierPriceQuantityInput"/> -+ <seeInField selector="{{AdminProductFormAdvancedPricingSection.productTierPriceFixedPriceInput('0')}}" userInput="{{tierPriceOnVirtualProduct.price}}" stepKey="seeProductTierPriceFixedPrice"/> -+ <click selector="{{AdminProductFormAdvancedPricingSection.advancedPricingCloseButton}}" stepKey="clickAdvancedPricingCloseButton"/> -+ <seeInField selector="{{AdminProductFormSection.productTaxClass}}" userInput="{{virtualProductBigQty.productTaxClass}}" stepKey="seeProductTaxClass"/> -+ <seeInField selector="{{AdminProductFormSection.productQuantity}}" userInput="{{virtualProductBigQty.quantity}}" stepKey="seeProductQuantity"/> -+ <seeInField selector="{{AdminProductFormSection.productStockStatus}}" userInput="{{virtualProductBigQty.status}}" stepKey="seeProductStockStatus"/> -+ <click selector="{{AdminProductFormSection.categoriesDropdown}}" stepKey="clickCategoriesDropDownToVerify"/> -+ <grabMultiple selector="{{AdminProductFormSection.selectMultipleCategories}}" stepKey="selectedCategories" /> -+ <assertEquals stepKey="assertSelectedCategories"> -+ <actualResult type="variable">selectedCategories</actualResult> -+ <expectedResult type="array">[$$categoryEntity.name$$]</expectedResult> -+ </assertEquals> -+ <click selector="{{AdminProductFormSection.done}}" stepKey="clickOnDoneOnCategorySelect"/> -+ <seeInField selector="{{AdminProductFormSection.visibility}}" userInput="{{virtualProductBigQty.visibility}}" stepKey="seeVisibility"/> -+ <scrollTo selector="{{AdminProductSEOSection.sectionHeader}}" x="0" y="-80" stepKey="scrollToAdminProductSEOSection1"/> -+ <click selector="{{AdminProductSEOSection.sectionHeader}}" stepKey="clickAdminProductSEOSection1"/> -+ <seeInField selector="{{AdminProductSEOSection.urlKeyInput}}" userInput="{{virtualProductBigQty.urlKey}}" stepKey="seeUrlKey"/> -+ -+ <!--Verify customer see created virtual product on category page --> -+ <amOnPage url="{{StorefrontCategoryPage.url($$categoryEntity.name$$)}}" stepKey="openCategoryPage"/> -+ <waitForPageLoad stepKey="waitForCategoryPageToLoad"/> -+ <see selector="{{StorefrontCategoryMainSection.productLink}}" userInput="{{virtualProductBigQty.name}}" stepKey="seeVirtualProductNameOnCategoryPage"/> -+ -+ <!-- Verify customer see created virtual product with tier price(from above step) on storefront page and is searchable by sku --> -+ <amOnPage url="{{StorefrontHomePage.url}}" stepKey="goToStorefront"/> -+ <waitForPageLoad stepKey="waitForStoreFrontProductPageLoad"/> -+ <fillField selector="{{StorefrontQuickSearchResultsSection.searchTextBox}}" userInput="{{virtualProductBigQty.sku}}" stepKey="fillVirtualProductName"/> -+ <waitForPageLoad stepKey="waitForSearchTextBox"/> -+ <click selector="{{StorefrontQuickSearchResultsSection.searchTextBoxButton}}" stepKey="clickSearchTextBoxButton"/> -+ <waitForPageLoad stepKey="waitForSearch"/> -+ <see selector="{{StorefrontQuickSearchResultsSection.productLink}}" userInput="{{virtualProductBigQty.name}}" stepKey="seeVirtualProductName"/> -+ <grabTextFrom selector="{{StorefrontQuickSearchResultsSection.asLowAsLabel}}" stepKey="tierPriceTextOnStorefrontPage"/> -+ <assertEquals stepKey="assertTierPriceTextOnCategoryPage"> -+ <expectedResult type="string">As low as ${{tierPriceOnVirtualProduct.price}}</expectedResult> -+ <actualResult type="variable">tierPriceTextOnStorefrontPage</actualResult> -+ </assertEquals> -+ <click selector="{{StorefrontQuickSearchResultsSection.productLink}}" stepKey="openSearchedProduct"/> -+ <waitForPageLoad stepKey="waitForProductPageToBeLoaded" /> -+ -+ <!-- Verify customer see product tier price on product page --> -+ <grabTextFrom selector="{{StorefrontProductInfoMainSection.tierPriceText}}" stepKey="tierPriceText"/> -+ <assertEquals stepKey="assertTierPriceTextOnProductPage"> -+ <expectedResult type="string">Buy {{tierPriceOnVirtualProduct.qty}} for ${{tierPriceOnVirtualProduct.price}} each and save 10%</expectedResult> -+ <actualResult type="variable">tierPriceText</actualResult> -+ </assertEquals> -+ </test> -+</tests> -diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateVirtualProductWithoutManageStockTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateVirtualProductWithoutManageStockTest.xml -new file mode 100644 -index 00000000000..cb41b0292d3 ---- /dev/null -+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateVirtualProductWithoutManageStockTest.xml -@@ -0,0 +1,88 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> -+ <test name="AdminCreateVirtualProductWithoutManageStockTest"> -+ <annotations> -+ <stories value="Create virtual product"/> -+ <title value="Create virtual product without manage stock"/> -+ <description value="Test log in to Create virtual product and Create virtual product without manage stock"/> -+ <testCaseId value="MC-6035"/> -+ <severity value="CRITICAL"/> -+ <group value="catalog"/> -+ <group value="mtf_migrated"/> -+ </annotations> -+ <before> -+ <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> -+ <createData entity="SimpleSubCategory" stepKey="categoryEntity"/> -+ </before> -+ <after> -+ <deleteData stepKey="deleteSimpleSubCategory" createDataKey="categoryEntity"/> -+ <actionGroup ref="logout" stepKey="logout"/> -+ </after> -+ -+ <amOnPage url="{{ProductCatalogPage.url}}" stepKey="OpenProductCatalogPage"/> -+ <waitForPageLoad stepKey="waitForProductCatalogPage"/> -+ <click selector="{{AdminProductGridActionSection.addProductToggle}}" stepKey="clickAddProductToggle"/> -+ <waitForPageLoad stepKey="waitForProductToggleToSelectProduct"/> -+ <click selector="{{AdminProductGridActionSection.addVirtualProduct}}" stepKey="clickVirtualProduct"/> -+ -+ <!-- Create virtual product without manage stock --> -+ <fillField selector="{{AdminProductFormSection.productName}}" userInput="{{virtualProductWithoutManageStock.name}}" stepKey="fillProductName"/> -+ <fillField selector="{{AdminProductFormSection.productSku}}" userInput="{{virtualProductWithoutManageStock.sku}}" stepKey="fillProductSku"/> -+ <fillField selector="{{AdminProductFormSection.productPrice}}" userInput="{{virtualProductWithoutManageStock.price}}" stepKey="fillProductPrice"/> -+ <click selector="{{AdminProductFormSection.advancedPricingLink}}" stepKey="clickAdvancedPricingLink"/> -+ <fillField selector="{{AdminProductFormAdvancedPricingSection.specialPrice}}" userInput="{{virtualProductWithoutManageStock.special_price}}" stepKey="fillSpecialPrice"/> -+ <click selector="{{AdminProductFormAdvancedPricingSection.doneButton}}" stepKey="clickDoneButton"/> -+ <fillField selector="{{AdminProductFormSection.productQuantity}}" userInput="{{virtualProductWithoutManageStock.quantity}}" stepKey="fillProductQuantity"/> -+ <click selector="{{AdminProductFormSection.advancedInventoryLink}}" stepKey="clickAdvancedInventoryLink"/> -+ <click selector="{{AdminProductFormAdvancedInventorySection.manageStock}}" stepKey="clickManageStock"/> -+ <checkOption selector="{{AdminProductFormAdvancedInventorySection.useConfigSettings}}" stepKey="CheckUseConfigSettingsCheckBox"/> -+ <click selector="{{AdminProductFormAdvancedInventorySection.doneButton}}" stepKey="clickDoneButtonOnAdvancedInventorySection"/> -+ <click selector="{{AdminProductFormSection.categoriesDropdown}}" stepKey="clickCategoriesDropDown"/> -+ <fillField selector="{{AdminProductFormSection.searchCategory}}" userInput="$$categoryEntity.name$$" stepKey="fillSearchCategory" /> -+ <click selector="{{AdminProductFormSection.selectCategory($$categoryEntity.name$$)}}" stepKey="clickOnCategory"/> -+ <click selector="{{AdminProductFormSection.done}}" stepKey="clickOnDoneAdvancedCategorySelect"/> -+ <click selector="{{AdminProductSEOSection.sectionHeader}}" stepKey="clickAdminProductSEOSection"/> -+ <fillField selector="{{AdminProductSEOSection.urlKeyInput}}" userInput="{{virtualProductWithoutManageStock.urlKey}}" stepKey="fillUrlKey"/> -+ <scrollToTopOfPage stepKey="scrollToTopOfAdminProductFormSection"/> -+ <click selector="{{AdminProductFormSection.save}}" stepKey="clickSaveButton"/> -+ <waitForPageLoad stepKey="waitForVirtualProductSaved"/> -+ -+ <!-- Verify we see success message --> -+ <see selector="{{AdminProductFormSection.successMessage}}" userInput="You saved the product." stepKey="seeAssertVirtualProductSuccessMessage"/> -+ -+ <!-- Verify customer see created virtual product without manage stock on the storefront page --> -+ <amOnPage url="{{StorefrontProductPage.url(virtualProductWithoutManageStock.urlKey)}}" stepKey="goToProductPage"/> -+ <waitForPageLoad stepKey="waitForStoreFrontPageToLoad"/> -+ <see selector="{{StorefrontProductInfoMainSection.productName}}" userInput="{{virtualProductWithoutManageStock.name}}" stepKey="seeVirtualProductNameOnStoreFrontPage"/> -+ <see selector="{{StorefrontProductInfoMainSection.productSku}}" userInput="{{virtualProductWithoutManageStock.sku}}" stepKey="seeVirtualProductSku"/> -+ -+ <!-- Verify customer see product special price on the storefront page --> -+ <grabTextFrom selector="{{StorefrontProductInfoMainSection.specialPriceAmount}}" stepKey="specialPriceAmount"/> -+ <assertEquals stepKey="assertSpecialPriceTextOnProductPage"> -+ <expectedResult type="string">${{virtualProductWithoutManageStock.special_price}}</expectedResult> -+ <actualResult type="variable">specialPriceAmount</actualResult> -+ </assertEquals> -+ -+ <!-- Verify customer see product old price on the storefront page --> -+ <grabTextFrom selector="{{StorefrontProductInfoMainSection.oldPriceAmount}}" stepKey="oldPriceAmount"/> -+ <assertEquals stepKey="assertOldPriceTextOnProductPage"> -+ <expectedResult type="string">${{virtualProductWithoutManageStock.price}}</expectedResult> -+ <actualResult type="variable">oldPriceAmount</actualResult> -+ </assertEquals> -+ -+ <!-- Verify customer see product in stock status on the storefront page --> -+ <grabTextFrom selector="{{StorefrontProductInfoMainSection.productStockStatus}}" stepKey="productStockAvailableStatus"/> -+ <assertEquals stepKey="assertStockAvailableOnProductPage"> -+ <expectedResult type="string">{{virtualProductWithoutManageStock.storefrontStatus}}</expectedResult> -+ <actualResult type="variable">productStockAvailableStatus</actualResult> -+ </assertEquals> -+ </test> -+</tests> -diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteAttributeSetTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteAttributeSetTest.xml -new file mode 100644 -index 00000000000..cbe2f40e0dd ---- /dev/null -+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteAttributeSetTest.xml -@@ -0,0 +1,56 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> -+ <test name="AdminDeleteAttributeSetTest"> -+ <annotations> -+ <features value="Catalog"/> -+ <stories value="Attribute sets"/> -+ <title value="Delete Attribute Set"/> -+ <description value="Admin should be able to delete an attribute set"/> -+ <testCaseId value="MC-10889"/> -+ <severity value="CRITICAL"/> -+ <group value="mtf_migrated"/> -+ </annotations> -+ <before> -+ <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> -+ <createData entity="_defaultCategory" stepKey="createCategory"/> -+ <createData entity="CatalogAttributeSet" stepKey="createAttributeSet"/> -+ <createData entity="SimpleProductWithCustomAttributeSet" stepKey="SimpleProductWithCustomAttributeSet"> -+ <requiredEntity createDataKey="createCategory"/> -+ <requiredEntity createDataKey="createAttributeSet"/> -+ </createData> -+ </before> -+ <after> -+ <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> -+ <actionGroup ref="logout" stepKey="logout"/> -+ </after> -+ <amOnPage url="{{AdminProductAttributeSetGridPage.url}}" stepKey="goToAttributeSetsPage"/> -+ <fillField selector="{{AdminProductAttributeSetGridSection.filter}}" userInput="$$createAttributeSet.attribute_set_name$$" stepKey="filterByAttributeName"/> -+ <!-- Filter the grid to find created below attribute set --> -+ <click selector="{{AdminProductAttributeSetGridSection.searchBtn}}" stepKey="clickSearch"/> -+ <click selector="{{AdminProductAttributeSetGridSection.nthRow('1')}}" stepKey="clickFirstRow"/> -+ <!-- Delete attribute set and confirm the modal --> -+ <click selector="{{AdminProductAttributeSetSection.deleteBtn}}" stepKey="clickDelete"/> -+ <click selector="{{AdminProductAttributeSetSection.modalOk}}" stepKey="confirmDelete"/> -+ <waitForPageLoad stepKey="waitForDeleteToFinish"/> -+ <see selector="{{AdminCategoryMessagesSection.SuccessMessage}}" userInput="The attribute set has been removed." stepKey="deleteMessage"/> -+ <!-- Assert the attribute set is not in the grid --> -+ <fillField selector="{{AdminProductAttributeSetGridSection.filter}}" userInput="$$createAttributeSet.attribute_set_name$$" stepKey="filterByAttributeName2"/> -+ <click selector="{{AdminProductAttributeSetGridSection.searchBtn}}" stepKey="clickSearch2"/> -+ <see selector="{{AdminDataGridTableSection.dataGridEmpty}}" userInput="We couldn't find any records." stepKey="assertDataGridEmptyMessage"/> -+ <!-- Search for the product by sku and name on the product page --> -+ <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="goToAdminProductIndex"/> -+ <waitForPageLoad stepKey="waitForAdminProductIndex"/> -+ <actionGroup ref="filterProductGridBySkuAndName" stepKey="filerProductsBySkuAndName"> -+ <argument name="product" value="SimpleProductWithCustomAttributeSet"/> -+ </actionGroup> -+ <!-- Should not see the product --> -+ <see selector="{{AdminDataGridTableSection.dataGridEmpty}}" userInput="We couldn't find any records." stepKey="assertDataGridEmptyMessage2"/> -+ </test> -+</tests> -diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteConfigurableChildProductsTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteConfigurableChildProductsTest.xml -new file mode 100644 -index 00000000000..0df9dd0b575 ---- /dev/null -+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteConfigurableChildProductsTest.xml -@@ -0,0 +1,121 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> -+ <test name="AdminDeleteConfigurableChildProductsTest"> -+ <annotations> -+ <stories value="Configurable Product"/> -+ <title value="Configurable Product should not be visible on storefront after child products are deleted"/> -+ <description value="Login as admin, delete configurable child product and verify product displays out of stock in store front"/> -+ <severity value="CRITICAL"/> -+ <testCaseId value="MC-13684"/> -+ <group value="mtf_migrated"/> -+ </annotations> -+ <before> -+ <!--Set Display Out Of Stock Product --> -+ <magentoCLI stepKey="setDisplayOutOfStockProduct" command="config:set cataloginventory/options/show_out_of_stock 0 "/> -+ <!--Login as Admin --> -+ <actionGroup ref="LoginAsAdmin" stepKey="loginToAdminPanel"/> -+ <!--Create Default Category --> -+ <createData entity="_defaultCategory" stepKey="createCategory"/> -+ <!-- Create an attribute with two options to be used in the first child product --> -+ <createData entity="productAttributeWithTwoOptions" stepKey="createConfigProductAttribute"/> -+ <createData entity="productAttributeOption1" stepKey="createConfigProductAttributeOption1"> -+ <requiredEntity createDataKey="createConfigProductAttribute"/> -+ </createData> -+ <createData entity="productAttributeOption2" stepKey="createConfigProductAttributeOption2"> -+ <requiredEntity createDataKey="createConfigProductAttribute"/> -+ </createData> -+ <!-- Add the attribute just created to default attribute set --> -+ <createData entity="AddToDefaultSet" stepKey="createConfigAddToAttributeSet"> -+ <requiredEntity createDataKey="createConfigProductAttribute"/> -+ </createData> -+ <!-- Get the first option of the attribute created --> -+ <getData entity="ProductAttributeOptionGetter" index="1" stepKey="getConfigAttributeOption1"> -+ <requiredEntity createDataKey="createConfigProductAttribute"/> -+ </getData> -+ <!-- Get the second option of the attribute created --> -+ <getData entity="ProductAttributeOptionGetter" index="2" stepKey="getConfigAttributeOption2"> -+ <requiredEntity createDataKey="createConfigProductAttribute"/> -+ </getData> -+ <!-- Create Configurable product --> -+ <createData entity="BaseConfigurableProduct" stepKey="createConfigProduct"> -+ <requiredEntity createDataKey="createCategory"/> -+ </createData> -+ <!-- Create a simple product and give it the attribute with the first option --> -+ <createData entity="ApiSimpleOne" stepKey="createConfigChildProduct1"> -+ <requiredEntity createDataKey="createConfigProductAttribute"/> -+ <requiredEntity createDataKey="getConfigAttributeOption1"/> -+ </createData> -+ <!--Create a simple product and give it the attribute with the second option --> -+ <createData entity="ApiSimpleTwo" stepKey="createConfigChildProduct2"> -+ <requiredEntity createDataKey="createConfigProductAttribute"/> -+ <requiredEntity createDataKey="getConfigAttributeOption2"/> -+ </createData> -+ <!-- Create the configurable product --> -+ <createData entity="ConfigurableProductTwoOptions" stepKey="createConfigProductOption"> -+ <requiredEntity createDataKey="createConfigProduct"/> -+ <requiredEntity createDataKey="createConfigProductAttribute"/> -+ <requiredEntity createDataKey="getConfigAttributeOption1"/> -+ <requiredEntity createDataKey="getConfigAttributeOption2"/> -+ </createData> -+ <!-- Add the first simple product to the configurable product --> -+ <createData entity="ConfigurableProductAddChild" stepKey="createConfigProductAddChild1"> -+ <requiredEntity createDataKey="createConfigProduct"/> -+ <requiredEntity createDataKey="createConfigChildProduct1"/> -+ </createData> -+ <!-- Add the second simple product to the configurable product --> -+ <createData entity="ConfigurableProductAddChild" stepKey="createConfigProductAddChild2"> -+ <requiredEntity createDataKey="createConfigProduct"/> -+ <requiredEntity createDataKey="createConfigChildProduct2"/> -+ </createData> -+ </before> -+ <after> -+ <!--Delete Created Data--> -+ <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> -+ <deleteData createDataKey="createConfigProduct" stepKey="deleteConfigProduct"/> -+ <deleteData createDataKey="createConfigProductAttribute" stepKey="deleteAttribute"/> -+ <actionGroup ref="logout" stepKey="logout"/> -+ </after> -+ <!--Open Product in Store Front Page --> -+ <amOnPage url="$$createConfigProduct.sku$$.html" stepKey="openProductInStoreFront"/> -+ <waitForPageLoad stepKey="waitForProductToLoad"/> -+ <!--Verify Product is visible and In Stock --> -+ <seeElement selector="{{StorefrontHeaderSection.NavigationCategoryByName($$createCategory.name$$)}}" stepKey="seeCategoryInFrontPage"/> -+ <see selector="{{StorefrontProductInfoMainSection.productName}}" userInput="$$createConfigProduct.name$$" stepKey="seeProductNameInStoreFront"/> -+ <see selector="{{StorefrontProductInfoMainSection.productPrice}}" userInput="$$createConfigProduct.price$$" stepKey="seeProductPriceInStoreFront"/> -+ <see selector="{{StorefrontProductInfoMainSection.productSku}}" userInput="$$createConfigProduct.sku$$" stepKey="seeProductSkuInStoreFront"/> -+ <see selector="{{StorefrontProductInfoMainSection.productStockStatus}}" userInput="In Stock" stepKey="seeProductStatusInStoreFront"/> -+ <see selector="{{StorefrontProductInfoMainSection.productAttributeTitle1}}" userInput="$$createConfigProductAttribute.default_value$$" stepKey="seeProductAttributeLabel"/> -+ <seeElement selector="{{StorefrontProductInfoMainSection.productAttributeOptions1}}" stepKey="seeProductAttributeOptions"/> -+ <!-- Delete Child products --> -+ <actionGroup ref="deleteProductBySku" stepKey="deleteFirstChildProduct"> -+ <argument name="sku" value="$$createConfigChildProduct1.sku$$"/> -+ </actionGroup> -+ <actionGroup ref="deleteProductBySku" stepKey="deleteSecondChildProduct"> -+ <argument name="sku" value="$$createConfigChildProduct2.sku$$"/> -+ </actionGroup> -+ <!--Verify product is not visible in category store front page --> -+ <amOnPage url="$$createCategory.name$$.html" stepKey="openCategoryStoreFrontPage"/> -+ <waitForPageLoad stepKey="waitForCategoryPageToLoad"/> -+ <seeElement selector="{{StorefrontHeaderSection.NavigationCategoryByName($$createCategory.name$$)}}" stepKey="seeCategoryInStoreFrontPage"/> -+ <click selector="{{StorefrontHeaderSection.NavigationCategoryByName($$createCategory.name$$)}}" stepKey="clickOnCategory"/> -+ <dontSee selector="{{StorefrontCategoryMainSection.productName}}" userInput="$$createConfigProduct.name$$" stepKey="dontSeeProductInCategoryPage"/> -+ <!--Open Product Store Front Page and Verify Product is Out Of Stock --> -+ <amOnPage url="$$createConfigProduct.sku$$.html" stepKey="openProductInStoreFront1"/> -+ <waitForPageLoad stepKey="waitForProductToLoad1"/> -+ <seeElement selector="{{StorefrontHeaderSection.NavigationCategoryByName($$createCategory.name$$)}}" stepKey="seeCategoryInFrontPage1"/> -+ <see selector="{{StorefrontProductInfoMainSection.productName}}" userInput="$$createConfigProduct.name$$" stepKey="seeProductNameInStoreFront1"/> -+ <dontSee selector="{{StorefrontProductInfoMainSection.productPrice}}" userInput="$$createConfigProduct.price$$" stepKey="dontSeeProductPriceInStoreFront"/> -+ <see selector="{{StorefrontProductInfoMainSection.productSku}}" userInput="$$createConfigProduct.sku$$" stepKey="seeProductSkuInStoreFront1"/> -+ <see selector="{{StorefrontProductInfoMainSection.productStockStatus}}" userInput="OUT OF STOCK" stepKey="seeProductStatusInStoreFront1"/> -+ <dontSee selector="{{StorefrontProductInfoMainSection.productAttributeTitle1}}" userInput="$$createConfigProductAttribute.default_value$$" stepKey="dontSeeProductAttributeLabel"/> -+ <dontSeeElement selector="{{StorefrontProductInfoMainSection.productAttributeOptions1}}" stepKey="dontSeeProductAttributeOptions"/> -+ </test> -+</tests> -diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteDropdownProductAttributeFromAttributeSetTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteDropdownProductAttributeFromAttributeSetTest.xml -new file mode 100644 -index 00000000000..3841c061c26 ---- /dev/null -+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteDropdownProductAttributeFromAttributeSetTest.xml -@@ -0,0 +1,72 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> -+ <test name="AdminDeleteDropdownProductAttributeFromAttributeSetTest"> -+ <annotations> -+ <stories value="Delete product attributes"/> -+ <title value="Delete Product Attribute, Dropdown Type, from Attribute Set"/> -+ <description value="Login as admin and delete dropdown type product attribute from attribute set"/> -+ <severity value="CRITICAL"/> -+ <testCaseId value="MC-10885"/> -+ <group value="mtf_migrated"/> -+ </annotations> -+ <before> -+ <!-- Login as admin --> -+ <actionGroup ref="LoginAsAdmin" stepKey="login"/> -+ <!-- Create Dropdown Product Attribute --> -+ <createData entity="productDropDownAttribute" stepKey="attribute"/> -+ <!-- Create Attribute set --> -+ <createData entity="CatalogAttributeSet" stepKey="createAttributeSet"/> -+ </before> -+ <after> -+ <!--Delete Created Data --> -+ <deleteData createDataKey="createAttributeSet" stepKey="deleteAttributeSet"/> -+ <actionGroup ref="logout" stepKey="logout"/> -+ </after> -+ <!-- Open Product Attribute Set Page --> -+ <amOnPage url="{{AdminProductAttributeSetGridPage.url}}" stepKey="goToAttributeSets"/> -+ <waitForPageLoad stepKey="waitForProductAttributeSetPageToLoad"/> -+ <click selector="{{AdminProductAttributeSetGridSection.resetFilter}}" stepKey="clickOnResetFilter"/> -+ <!-- Filter created Product Attribute Set --> -+ <fillField selector="{{AdminProductAttributeSetGridSection.filter}}" userInput="$$createAttributeSet.attribute_set_name$$" stepKey="fillAttributeSetName"/> -+ <click selector="{{AdminProductAttributeSetGridSection.searchBtn}}" stepKey="clickOnSearchButton"/> -+ <waitForPageLoad stepKey="waitForPageToLoad"/> -+ <click selector="{{AdminProductAttributeSetGridSection.AttributeSetName($$createAttributeSet.attribute_set_name$$)}}" stepKey="clickOnAttributeSet"/> -+ <waitForPageLoad stepKey="waitForAttributeSetEditPageToLoad"/> -+ <!--Assign Attribute to the Group and save the attribute set --> -+ <actionGroup ref="AssignAttributeToGroup" stepKey="assignAttribute"> -+ <argument name="group" value="Product Details"/> -+ <argument name="attribute" value="$$attribute.attribute_code$$"/> -+ </actionGroup> -+ <click selector="{{AdminProductAttributeSetActionSection.save}}" stepKey="clickOnSaveButton"/> -+ <waitForPageLoad stepKey="waitForPageToSave"/> -+ <see userInput="You saved the attribute set" selector="{{AdminMessagesSection.success}}" stepKey="successMessage"/> -+ <!--Delete product attribute from product attribute grid --> -+ <actionGroup ref="deleteProductAttributeByAttributeCode" stepKey="deleteProductAttribute"> -+ <argument name="ProductAttributeCode" value="$$attribute.attribute_code$$"/> -+ </actionGroup> -+ <!--Confirm Attribute is not present in Product Attribute Grid --> -+ <actionGroup ref="filterProductAttributeByAttributeCode" stepKey="filterAttribute"> -+ <argument name="ProductAttributeCode" value="$$attribute.attribute_code$$"/> -+ </actionGroup> -+ <see selector="{{AdminProductAttributeGridSection.FirstRow}}" userInput="We couldn't find any records." stepKey="seeEmptyRow"/> -+ <!-- Verify Attribute is not present in Product Attribute Set Page --> -+ <amOnPage url="{{AdminProductAttributeSetGridPage.url}}" stepKey="goToAttributeSets1"/> -+ <waitForPageLoad stepKey="waitForProductAttributeSetPageToLoad1"/> -+ <click selector="{{AdminProductAttributeSetGridSection.resetFilter}}" stepKey="clickOnResetFilter1"/> -+ <fillField selector="{{AdminProductAttributeSetGridSection.filter}}" userInput="$$createAttributeSet.attribute_set_name$$" stepKey="fillAttributeSetName1"/> -+ <click selector="{{AdminProductAttributeSetGridSection.searchBtn}}" stepKey="clickOnSearchButton1"/> -+ <waitForPageLoad stepKey="waitForPageToLoad1"/> -+ <click selector="{{AdminProductAttributeSetGridSection.AttributeSetName($$createAttributeSet.attribute_set_name$$)}}" stepKey="clickOnAttributeSet1"/> -+ <waitForPageLoad stepKey="waitForAttributeSetEditPageToLoad1"/> -+ <dontSee userInput="$$attribute.attribute_code$$" selector="{{AdminProductAttributeSetEditSection.groupTree}}" stepKey="dontSeeAttributeInAttributeGroupTree"/> -+ <dontSee userInput="$$attribute.attribute_code$$" selector="{{AdminProductAttributeSetEditSection.unassignedAttributesTree}}" stepKey="dontSeeAttributeInUnassignedAttributeTree"/> -+ </test> -+</tests> -diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteProductAttributeTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteProductAttributeTest.xml -new file mode 100644 -index 00000000000..d0036a2adea ---- /dev/null -+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteProductAttributeTest.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="DeleteProductAttributeTest"> -+ <annotations> -+ <features value="Catalog"/> -+ <stories value="Product attributes"/> -+ <title value="Delete Product Attribute"/> -+ <description value="Admin should able to delete a product attribute"/> -+ <testCaseId value="MC-10887"/> -+ <severity value="CRITICAL"/> -+ <group value="mtf_migrated"/> -+ </annotations> -+ <before> -+ <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> -+ <createData entity="productAttributeWysiwyg" stepKey="createProductAttribute"/> -+ </before> -+ <after> -+ <actionGroup ref="logout" stepKey="logout"/> -+ </after> -+ <actionGroup ref="deleteProductAttributeByAttributeCode" stepKey="deleteProductAttribute"> -+ <argument name="ProductAttributeCode" value="$$createProductAttribute.attribute_code$$"/> -+ </actionGroup> -+ <!-- Assert the product attribute is not in the grid by Attribute code --> -+ <actionGroup ref="filterProductAttributeByAttributeCode" stepKey="filterByAttributeCode"> -+ <argument name="ProductAttributeCode" value="$$createProductAttribute.attribute_code$$"/> -+ </actionGroup> -+ <see selector="{{AdminDataGridTableSection.dataGridEmpty}}" userInput="We couldn't find any records." stepKey="assertDataGridEmptyMessage"/> -+ <!--Assert the product attribute is not in the grid by Default Label --> -+ <actionGroup ref="filterProductAttributeByDefaultLabel" stepKey="filterByDefaultLabel"> -+ <argument name="productAttributeLabel" value="$$createProductAttribute.default_frontend_label$$"/> -+ </actionGroup> -+ <see selector="{{AdminDataGridTableSection.dataGridEmpty}}" userInput="We couldn't find any records." stepKey="assertDataGridEmptyMessage2"/> -+ <!--Go to the Catalog > Products page and create Simple Product --> -+ <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="amOnProductList"/> -+ <waitForPageLoad stepKey="waitForProductList"/> -+ <click selector="{{AdminProductGridActionSection.addProductToggle}}" stepKey="toggleAddProductBtn"/> -+ <click selector="{{AdminProductGridActionSection.addSimpleProduct}}" stepKey="chooseAddSimpleProduct"/> -+ <waitForPageLoad stepKey="waitForProductAdded"/> -+ <!-- Press Add Attribute button --> -+ <click selector="{{AdminProductFormSection.addAttributeBtn}}" stepKey="clickAddAttributeBtn"/> -+ <waitForPageLoad stepKey="waitForAttributeAdded"/> -+ <!-- Filter By Attribute Label on Add Attribute Page --> -+ <click selector="{{AdminProductFiltersSection.filter}}" stepKey="clickOnFilter"/> -+ <actionGroup ref="filterProductAttributeByAttributeLabel" stepKey="filterByAttributeLabel"> -+ <argument name="productAttributeLabel" value="$$createProductAttribute.default_frontend_label$$"/> -+ </actionGroup> -+ <see selector="{{AdminDataGridTableSection.dataGridEmpty}}" userInput="We couldn't find any records." stepKey="assertDataGridEmptyMessage3"/> -+ <!-- Filter By Attribute Code on Export > Products page --> -+ <amOnPage url="{{AdminExportIndexPage.url}}" stepKey="navigateToSystemExport"/> -+ <selectOption selector="{{AdminExportMainSection.entityType}}" userInput="Products" stepKey="selectProductsOption"/> -+ <waitForElementVisible selector="{{AdminExportMainSection.entityAttributes}}" stepKey="waitForElementVisible"/> -+ <click selector="{{AdminExportAttributeSection.resetFilter}}" stepKey="resetFilter"/> -+ <fillField selector="{{AdminExportAttributeSection.filterByAttributeCode}}" userInput="$$createProductAttribute.attribute_code$$" stepKey="setAttributeCode"/> -+ <waitForPageLoad stepKey="waitForUserInput"/> -+ <click selector="{{AdminExportAttributeSection.search}}" stepKey="searchForAttribute"/> -+ <see selector="{{AdminDataGridTableSection.dataGridEmpty}}" userInput="We couldn't find any records." stepKey="assertDataGridEmptyMessage4"/> -+ </test> -+</tests> -diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteProductWithCustomOptionTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteProductWithCustomOptionTest.xml -new file mode 100644 -index 00000000000..7f6a1333b72 ---- /dev/null -+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteProductWithCustomOptionTest.xml -@@ -0,0 +1,54 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> -+ <test name="AdminDeleteProductWithCustomOptionTest"> -+ <annotations> -+ <features value="Catalog"/> -+ <stories value="Delete products"/> -+ <title value="Delete Product with Custom Option"/> -+ <description value="Admin should be able to delete a product with custom option"/> -+ <severity value="CRITICAL"/> -+ <testCaseId value="MC-11015"/> -+ <group value="mtf_migrated"/> -+ </annotations> -+ <before> -+ <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> -+ <createData entity="_defaultCategory" stepKey="createCategory"/> -+ <createData entity="_defaultProduct" stepKey="createSimpleProduct"> -+ <requiredEntity createDataKey="createCategory"/> -+ </createData> -+ <updateData createDataKey="createSimpleProduct" entity="productWithOptions2" stepKey="updateProductWithCustomOption"/> -+ </before> -+ <after> -+ <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> -+ <actionGroup ref="logout" stepKey="logout"/> -+ </after> -+ <actionGroup ref="deleteProductUsingProductGrid" stepKey="deleteSimpleProductFilteredBySkuAndName"> -+ <argument name="product" value="$$createSimpleProduct$$"/> -+ </actionGroup> -+ <see selector="{{AdminCategoryMessagesSection.SuccessMessage}}" userInput="A total of 1 record(s) have been deleted." stepKey="deleteMessage"/> -+ <!--Verify product on product page --> -+ <amOnPage url="{{StorefrontProductPage.url($$createSimpleProduct.name$$)}}" stepKey="amOnSimpleProductPage"/> -+ <see selector="{{StorefrontProductInfoMainSection.productName}}" userInput="Whoops, our bad..." stepKey="seeWhoops"/> -+ <!-- Search for the product by sku --> -+ <fillField selector="{{StorefrontQuickSearchSection.searchPhrase}}" userInput="$$createSimpleProduct.sku$$" stepKey="fillSearchBarByProductSku"/> -+ <waitForPageLoad stepKey="waitForSearchButton"/> -+ <click selector="{{StorefrontQuickSearchSection.searchButton}}" stepKey="clickSearchButton"/> -+ <waitForPageLoad stepKey="waitForSearchResults"/> -+ <!-- Should not see any search results --> -+ <dontSee userInput="$$createSimpleProduct.sku$$" selector="{{StorefrontCatalogSearchMainSection.searchResults}}" stepKey="dontSeeProduct"/> -+ <see selector="{{StorefrontCatalogSearchMainSection.message}}" userInput="Your search returned no results." stepKey="seeCantFindProductOneMessage"/> -+ <!-- Go to the category page that we created in the before block --> -+ <amOnPage url="{{StorefrontCategoryPage.url($$createCategory.name$$)}}" stepKey="onCategoryPage"/> -+ <!-- Should not see the product --> -+ <dontSee userInput="$$createSimpleProduct.name$$" selector="{{StorefrontCategoryMainSection.productsList}}" stepKey="dontSeeProductInCategory"/> -+ <see selector="{{StorefrontCategoryMainSection.emptyProductMessage}}" userInput="We can't find products matching the selection." stepKey="seeEmptyProductMessage"/> -+ </test> -+</tests> -diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteRootCategoryAssignedToStoreTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteRootCategoryAssignedToStoreTest.xml -new file mode 100644 -index 00000000000..e4b269dff96 ---- /dev/null -+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteRootCategoryAssignedToStoreTest.xml -@@ -0,0 +1,51 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> -+ <test name="AdminDeleteRootCategoryAssignedToStoreTest"> -+ <annotations> -+ <stories value="Delete categories"/> -+ <title value="Cannot delete root category assigned to some store"/> -+ <description value="Login as admin and root category can not be deleted when category is assigned with any store."/> -+ <testCaseId value="MC-6050"/> -+ <severity value="CRITICAL"/> -+ <group value="Catalog"/> -+ <group value="mtf_migrated"/> -+ </annotations> -+ <before> -+ <actionGroup ref="LoginAsAdmin" stepKey="loginToAdminPanel"/> -+ <createData entity="NewRootCategory" stepKey="rootCategory" /> -+ </before> -+ <after> -+ <actionGroup ref="DeleteCustomStoreActionGroup" stepKey="deleteCreatedStore"> -+ <argument name="storeGroupName" value="customStore.code"/> -+ </actionGroup> -+ <deleteData createDataKey="rootCategory" stepKey="deleteRootCategory"/> -+ <actionGroup ref="logout" stepKey="logout"/> -+ </after> -+ -+ <amOnPage url="{{AdminSystemStorePage.url}}" stepKey="amOnAdminSystemStorePage"/> -+ <waitForPageLoad stepKey="waitForSystemStorePage"/> -+ <click selector="{{AdminStoresMainActionsSection.createStoreButton}}" stepKey="selectCreateStore"/> -+ <fillField userInput="{{customStore.name}}" selector="{{AdminNewStoreGroupSection.storeGrpNameTextField}}" stepKey="fillStoreName"/> -+ <fillField userInput="{{customStore.code}}" selector="{{AdminNewStoreGroupSection.storeGrpCodeTextField}}" stepKey="fillStoreCode"/> -+ <selectOption userInput="{{NewRootCategory.name}}" selector="{{AdminNewStoreGroupSection.storeRootCategoryDropdown}}" stepKey="selectStoreStatus"/> -+ <click selector="{{AdminStoresMainActionsSection.saveButton}}" stepKey="clickSaveStoreButton"/> -+ <see userInput="You saved the store." stepKey="seeSaveMessage"/> -+ -+ <!--Verify Delete Root Category can not be deleted--> -+ <amOnPage url="{{AdminCategoryPage.url}}" stepKey="openAdminCategoryIndexPage1"/> -+ <waitForPageLoad stepKey="waitForCategoryIndexPageToBeLoaded1"/> -+ <scrollToTopOfPage stepKey="scrollToTopOfPage2"/> -+ <click selector="{{AdminCategorySidebarTreeSection.expandAll}}" stepKey="expandToSeeAllCategories"/> -+ <click selector="{{AdminCategorySidebarTreeSection.categoryInTree(NewRootCategory.name))}}" stepKey="clickRootCategoryInTree"/> -+ -+ <!--Verify Delete button is not displayed--> -+ <dontSeeElement selector="{{AdminCategoryMainActionsSection.DeleteButton}}" stepKey="dontSeeDeleteButton"/> -+ </test> -+</tests> -\ No newline at end of file -diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteRootCategoryTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteRootCategoryTest.xml -new file mode 100644 -index 00000000000..e7ab14c7794 ---- /dev/null -+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteRootCategoryTest.xml -@@ -0,0 +1,44 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> -+ <test name="AdminDeleteRootCategoryTest"> -+ <annotations> -+ <stories value="Delete categories"/> -+ <title value="Can delete a root category not assigned to any store"/> -+ <description value="Login as admin and delete a root category not assigned to any store"/> -+ <testCaseId value="MC-6048"/> -+ <severity value="CRITICAL"/> -+ <group value="Catalog"/> -+ <group value="mtf_migrated"/> -+ </annotations> -+ <before> -+ <actionGroup ref="LoginAsAdmin" stepKey="loginToAdminPanel"/> -+ <createData entity="NewRootCategory" stepKey="rootCategory" /> -+ </before> -+ <after> -+ <actionGroup ref="logout" stepKey="logout"/> -+ </after> -+ -+ <!--Verify Created root Category--> -+ <amOnPage url="{{AdminCategoryPage.url}}" stepKey="openAdminCategoryIndexPage"/> -+ <waitForPageLoad stepKey="waitForCategoryIndexPageToBeLoaded"/> -+ <click selector="{{AdminCategorySidebarTreeSection.expandAll}}" stepKey="expandToSeeAllCategories"/> -+ <waitForPageLoad stepKey="waitForPageToLoad"/> -+ <seeElement selector="{{AdminCategoryBasicFieldSection.CategoryNameInput(NewRootCategory.name)}}" stepKey="seeRootCategory"/> -+ -+ <!--Delete Root Category--> -+ <deleteData createDataKey="rootCategory" stepKey="deleteRootCategory"/> -+ -+ <!--Verify Root Category is not listed in backend--> -+ <amOnPage url="{{AdminCategoryPage.url}}" stepKey="openAdminCategoryIndexPage1"/> -+ <waitForPageLoad stepKey="waitForCategoryIndexPageToBeLoaded1"/> -+ <click selector="{{AdminCategorySidebarTreeSection.expandAll}}" stepKey="expandToSeeAllCategories1"/> -+ <dontSee selector="{{AdminCategoryBasicFieldSection.CategoryNameInput}}" userInput="{{NewRootCategory.name}}" stepKey="dontSeeRootCategory"/> -+ </test> -+</tests> -\ No newline at end of file -diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteRootSubCategoryTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteRootSubCategoryTest.xml -new file mode 100644 -index 00000000000..6df571f403a ---- /dev/null -+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteRootSubCategoryTest.xml -@@ -0,0 +1,90 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> -+ <test name="AdminDeleteRootSubCategoryTest"> -+ <annotations> -+ <stories value="Delete categories"/> -+ <title value="Can delete a subcategory"/> -+ <description value="Login as admin and delete a root sub category"/> -+ <testCaseId value="MC-6049"/> -+ <severity value="CRITICAL"/> -+ <group value="Catalog"/> -+ <group value="mtf_migrated"/> -+ </annotations> -+ <before> -+ <actionGroup ref="LoginAsAdmin" stepKey="loginToAdminPanel"/> -+ <createData entity="NewRootCategory" stepKey="rootCategory" /> -+ <createData entity="SimpleRootSubCategory" stepKey="category"> -+ <requiredEntity createDataKey="rootCategory"/> -+ </createData> -+ </before> -+ <after> -+ <actionGroup ref="DeleteCustomStoreActionGroup" stepKey="deleteCreatedStore"> -+ <argument name="storeGroupName" value="customStore.code"/> -+ </actionGroup> -+ <deleteData createDataKey="rootCategory" stepKey="deleteRootCategory"/> -+ <actionGroup ref="logout" stepKey="logout"/> -+ </after> -+ -+ <!--Create a Store--> -+ <amOnPage url="{{AdminSystemStorePage.url}}" stepKey="amOnAdminSystemStorePage"/> -+ <waitForPageLoad stepKey="waitForSystemStorePage"/> -+ <click selector="{{AdminStoresMainActionsSection.createStoreButton}}" stepKey="selectCreateStore"/> -+ <fillField userInput="{{customStore.name}}" selector="{{AdminNewStoreGroupSection.storeGrpNameTextField}}" stepKey="fillStoreName"/> -+ <fillField userInput="{{customStore.code}}" selector="{{AdminNewStoreGroupSection.storeGrpCodeTextField}}" stepKey="fillStoreCode"/> -+ <selectOption userInput="{{NewRootCategory.name}}" selector="{{AdminNewStoreGroupSection.storeRootCategoryDropdown}}" stepKey="selectStoreStatus"/> -+ <click selector="{{AdminStoresMainActionsSection.saveButton}}" stepKey="clickSaveStoreButton"/> -+ <see userInput="You saved the store." stepKey="seeSaveMessage"/> -+ -+ <!--Create a Store View--> -+ <click selector="{{AdminStoresMainActionsSection.createStoreViewButton}}" stepKey="selectCreateStoreView"/> -+ <click selector="{{AdminNewStoreSection.storeGrpDropdown}}" stepKey="clickDropDown"/> -+ <selectOption userInput="{{customStore.name}}" selector="{{AdminNewStoreSection.storeGrpDropdown}}" stepKey="selectStoreViewStatus"/> -+ <fillField userInput="{{customStore.name}}" selector="{{AdminNewStoreSection.storeNameTextField}}" stepKey="fillStoreViewName"/> -+ <fillField userInput="{{customStore.code}}" selector="{{AdminNewStoreSection.storeCodeTextField}}" stepKey="fillStoreViewCode"/> -+ <selectOption selector="{{AdminNewStoreSection.statusDropdown}}" userInput="Enabled" stepKey="enableStatus"/> -+ <click selector="{{AdminStoresMainActionsSection.saveButton}}" stepKey="clickSaveStoreViewButton"/> -+ <waitForElementVisible selector="{{AdminConfirmationModalSection.ok}}" stepKey="waitForModal" /> -+ <see selector="{{AdminConfirmationModalSection.title}}" userInput="Warning message" stepKey="seeWarning" /> -+ <click selector="{{AdminConfirmationModalSection.ok}}" stepKey="dismissModal" /> -+ <waitForElementNotVisible selector="{{AdminNewStoreViewActionsSection.loadingMask}}" stepKey="waitForElementVisible"/> -+ <see userInput="You saved the store view." stepKey="seeSaveMessage1"/> -+ -+ <!--Go To store front page--> -+ <amOnPage url="/{{NewRootCategory.name}}/{{SimpleRootSubCategory.name}}.html" stepKey="seeTheCategoryInStoreFrontPage"/> -+ <waitForPageLoad time="60" stepKey="waitForStoreFrontPageLoad"/> -+ -+ <!--Verify subcategory displayed in store front--> -+ <click selector="{{StorefrontFooterSection.switchStoreButton}}" stepKey="selectMainWebsite"/> -+ <click selector="{{StorefrontFooterSection.storeLink(customStore.name)}}" stepKey="selectMainWebsite1"/> -+ <waitForPageLoad stepKey="waitForCategoryToLoad"/> -+ <seeElement selector="{{StorefrontHeaderSection.NavigationCategoryByName(SimpleRootSubCategory.name)}}" stepKey="seeSubCategoryInStoreFront"/> -+ -+ <!--Delete SubCategory--> -+ <deleteData createDataKey="category" stepKey="deleteCategory"/> -+ -+ <!--Verify Sub Category is absent in backend --> -+ <amOnPage url="{{AdminCategoryPage.url}}" stepKey="openAdminCategoryIndexPage"/> -+ <waitForPageLoad stepKey="waitForCategoryIndexPageToBeLoaded"/> -+ <click selector="{{AdminCategorySidebarTreeSection.expandAll}}" stepKey="expandToSeeAllCategories2"/> -+ <dontSee selector="{{AdminCategorySidebarTreeSection.categoryInTree(SimpleRootSubCategory.name)}}" stepKey="dontSeeCategoryInTree"/> -+ -+ <!--Verify Sub Category is not present in Store Front--> -+ <amOnPage url="/{{NewRootCategory.name}}/{{SimpleSubCategory.name}}.html" stepKey="seeTheCategoryInStoreFrontPage1"/> -+ <waitForPageLoad time="60" stepKey="waitForStoreFrontPageLoad2"/> -+ <dontSeeElement selector="{{StorefrontHeaderSection.NavigationCategoryByName(SimpleSubCategory.name)}}" stepKey="dontSeeSubCategoryInStoreFront"/> -+ -+ <!--Verify in Category is not in Url Rewrite grid--> -+ <amOnPage url="{{AdminUrlRewriteIndexPage.url}}" stepKey="openUrlRewriteIndexPage"/> -+ <waitForPageLoad stepKey="waitForUrlRewritePageTopLoad"/> -+ <fillField selector="{{AdminUrlRewriteIndexSection.requestPathFilter}}" userInput="{{SimpleRootSubCategory.url_key}}" stepKey="fillRequestPath"/> -+ <click selector="{{AdminUrlRewriteIndexSection.searchButton}}" stepKey="clickOnSearchButton"/> -+ <see selector="{{AdminUrlRewriteIndexSection.emptyRecordMessage}}" userInput="We couldn't find any records." stepKey="seeEmptyRow"/> -+ </test> -+</tests> -\ No newline at end of file -diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteSimpleProductTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteSimpleProductTest.xml -new file mode 100644 -index 00000000000..7c460a3dfc5 ---- /dev/null -+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteSimpleProductTest.xml -@@ -0,0 +1,53 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> -+ <test name="AdminDeleteSimpleProductTest"> -+ <annotations> -+ <features value="Catalog"/> -+ <stories value="Delete products"/> -+ <title value="Delete Simple Product"/> -+ <description value="Admin should be able to delete a simple product"/> -+ <severity value="CRITICAL"/> -+ <testCaseId value="MC-11013"/> -+ <group value="mtf_migrated"/> -+ </annotations> -+ <before> -+ <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> -+ <createData entity="_defaultCategory" stepKey="createCategory"/> -+ <createData entity="_defaultProduct" stepKey="createSimpleProduct"> -+ <requiredEntity createDataKey="createCategory"/> -+ </createData> -+ </before> -+ <after> -+ <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> -+ <actionGroup ref="logout" stepKey="logout"/> -+ </after> -+ <actionGroup ref="deleteProductUsingProductGrid" stepKey="deleteSimpleProductFilteredBySkuAndName"> -+ <argument name="product" value="$$createSimpleProduct$$"/> -+ </actionGroup> -+ <see selector="{{AdminCategoryMessagesSection.SuccessMessage}}" userInput="A total of 1 record(s) have been deleted." stepKey="deleteMessage"/> -+ <!--Verify product on Product Page --> -+ <amOnPage url="{{StorefrontProductPage.url($$createSimpleProduct.name$$)}}" stepKey="amOnSimpleProductPage"/> -+ <see selector="{{StorefrontProductInfoMainSection.productName}}" userInput="Whoops, our bad..." stepKey="seeWhoops"/> -+ <!-- Search for the product by sku --> -+ <fillField selector="{{StorefrontQuickSearchSection.searchPhrase}}" userInput="$$createSimpleProduct.sku$$" stepKey="fillSearchBarByProductSku"/> -+ <waitForPageLoad stepKey="waitForSearchButton"/> -+ <click selector="{{StorefrontQuickSearchSection.searchButton}}" stepKey="clickSearchButton"/> -+ <waitForPageLoad stepKey="waitForSearchResults"/> -+ <!-- Should not see any search results --> -+ <dontSee userInput="$$createSimpleProduct.sku$$" selector="{{StorefrontCatalogSearchMainSection.searchResults}}" stepKey="dontSeeProduct"/> -+ <see selector="{{StorefrontCatalogSearchMainSection.message}}" userInput="Your search returned no results." stepKey="seeCantFindProductOneMessage"/> -+ <!-- Go to the category page that we created in the before block --> -+ <amOnPage url="{{StorefrontCategoryPage.url($$createCategory.name$$)}}" stepKey="onCategoryPage"/> -+ <!-- Should not see the product --> -+ <dontSee userInput="$$createSimpleProduct.name$$" selector="{{StorefrontCategoryMainSection.productsList}}" stepKey="dontSeeProductInCategory"/> -+ <see selector="{{StorefrontCategoryMainSection.emptyProductMessage}}" userInput="We can't find products matching the selection." stepKey="seeEmptyProductMessage"/> -+ </test> -+</tests> -diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteSystemProductAttributeTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteSystemProductAttributeTest.xml -new file mode 100644 -index 00000000000..6de1a5cd359 ---- /dev/null -+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteSystemProductAttributeTest.xml -@@ -0,0 +1,34 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> -+ <test name="AdminDeleteSystemProductAttributeTest"> -+ <annotations> -+ <features value="Catalog"/> -+ <stories value="Delete System Product Attribute"/> -+ <title value="Delete System Product Attribute"/> -+ <description value="Admin should not be able to see Delete Attribute button"/> -+ <testCaseId value="MC-10893"/> -+ <severity value="CRITICAL"/> -+ <group value="mtf_migrated"/> -+ </annotations> -+ <before> -+ <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> -+ </before> -+ <after> -+ <actionGroup ref="logout" stepKey="logout"/> -+ </after> -+ <amOnPage url="{{AdminProductAttributeGridPage.url}}" stepKey="navigateToProductAttribute"/> -+ <waitForPageLoad stepKey="waitForPageLoad"/> -+ <click selector="{{AdminProductAttributeGridSection.ResetFilter}}" stepKey="resetFiltersOnGrid"/> -+ <fillField selector="{{AdminProductAttributeGridSection.FilterByAttributeCode}}" userInput="{{newsFromDate.attribute_code}}" stepKey="setAttributeCode"/> -+ <click selector="{{AdminProductAttributeGridSection.Search}}" stepKey="searchForAttributeFromTheGrid"/> -+ <click selector="{{AdminProductAttributeGridSection.FirstRow}}" stepKey="clickOnAttributeRow"/> -+ <dontSeeElement selector="{{AttributePropertiesSection.DeleteAttribute}}" stepKey="dontSeeDeleteAttributeBtn" /> -+ </test> -+</tests> -diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteTextFieldProductAttributeFromAttributeSetTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteTextFieldProductAttributeFromAttributeSetTest.xml -new file mode 100644 -index 00000000000..c3cafb17c5e ---- /dev/null -+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteTextFieldProductAttributeFromAttributeSetTest.xml -@@ -0,0 +1,88 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> -+ <test name="AdminDeleteTextFieldProductAttributeFromAttributeSetTest"> -+ <annotations> -+ <stories value="Delete product attributes"/> -+ <title value="Delete Product Attribute, Text Field, from Attribute Set"/> -+ <description value="Login as admin and delete Text Field type product attribute from attribute set"/> -+ <severity value="CRITICAL"/> -+ <testCaseId value="MC-10886"/> -+ <group value="mtf_migrated"/> -+ </annotations> -+ <before> -+ <!-- Login as admin --> -+ <actionGroup ref="LoginAsAdmin" stepKey="login"/> -+ <!-- Create Product Attribute and assign to Default Product Attribute Set --> -+ <createData entity="newProductAttribute" stepKey="attribute"/> -+ <createData entity="AddToDefaultSet" stepKey="addToDefaultAttributeSet"> -+ <requiredEntity createDataKey="attribute"/> -+ </createData> -+ <!-- Create Simple Product --> -+ <createData entity="SimpleProduct2" stepKey="createSimpleProduct"/> -+ </before> -+ <after> -+ <!--Delete cteated Data --> -+ <deleteData createDataKey="createSimpleProduct" stepKey="deleteSimplaeProduct"/> -+ <actionGroup ref="logout" stepKey="logout"/> -+ </after> -+ <!-- Open Product Attribute Set Page --> -+ <amOnPage url="{{AdminProductAttributeSetGridPage.url}}" stepKey="goToAttributeSets"/> -+ <waitForPageLoad stepKey="waitForProductAttributeSetPageToLoad"/> -+ <click selector="{{AdminProductAttributeSetGridSection.resetFilter}}" stepKey="clickOnResetFilter"/> -+ <!--Select Default Product Attribute Set --> -+ <fillField selector="{{AdminProductAttributeSetGridSection.filter}}" userInput="Default" stepKey="fillAttributeSetName"/> -+ <click selector="{{AdminProductAttributeSetGridSection.searchBtn}}" stepKey="clickOnSearchButton"/> -+ <waitForPageLoad stepKey="waitForPageToLoad"/> -+ <click selector="{{AdminProductAttributeSetGridSection.nthRow('1')}}" stepKey="clickFirstRow"/> -+ <waitForPageLoad stepKey="waitForAttributeSetEditPageToLoad"/> -+ <see selector="{{AdminProductAttributeSetEditSection.groupTree}}" userInput="$$attribute.attribute_code$$" stepKey="seeAttributeInAttributeGroupTree"/> -+ <!--Open Product Index Page and filter the product--> -+ <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="navigateToProductIndex"/> -+ <waitForPageLoad stepKey="waitForProductIndexPageToLoad"/> -+ <actionGroup ref="filterProductGridBySku" stepKey="filterProduct"> -+ <argument name="product" value="SimpleProduct2"/> -+ </actionGroup> -+ <!--Verify Created Product Attribute displayed in Product page --> -+ <click stepKey="openSelectedProduct" selector="{{AdminProductGridSection.productRowBySku($$createSimpleProduct.sku$$)}}"/> -+ <waitForPageLoad stepKey="waitForProductToLoad"/> -+ <seeElement selector="{{AdminProductFormSection.newAddedAttribute($$attribute.attribute_code$$)}}" stepKey="seeProductAttributeIsAdded"/> -+ <!--Delete product attribute from product attribute grid --> -+ <actionGroup ref="deleteProductAttributeByAttributeCode" stepKey="deleteProductAttribute"> -+ <argument name="ProductAttributeCode" value="$$attribute.attribute_code$$"/> -+ </actionGroup> -+ <!-- Confirm attribute is not present in product attribute grid --> -+ <actionGroup ref="filterProductAttributeByAttributeCode" stepKey="filterAttribute"> -+ <argument name="ProductAttributeCode" value="$$attribute.attribute_code$$"/> -+ </actionGroup> -+ <see stepKey="seeEmptyRow" selector="{{AdminProductAttributeGridSection.FirstRow}}" userInput="We couldn't find any records."/> -+ <!-- Verify Attribute is not present in Product Attribute Set Page --> -+ <amOnPage url="{{AdminProductAttributeSetGridPage.url}}" stepKey="goToAttributeSets1"/> -+ <waitForPageLoad stepKey="waitForProductAttributeSetPageToLoad1"/> -+ <click selector="{{AdminProductAttributeSetGridSection.resetFilter}}" stepKey="clickOnResetFilter1"/> -+ <fillField selector="{{AdminProductAttributeSetGridSection.filter}}" userInput="Default" stepKey="fillAttributeSetName1"/> -+ <click selector="{{AdminProductAttributeSetGridSection.searchBtn}}" stepKey="clickOnSearchButton1"/> -+ <waitForPageLoad stepKey="waitForPageToLoad1"/> -+ <click selector="{{AdminProductAttributeSetGridSection.nthRow('1')}}" stepKey="clickFirstRow1"/> -+ <waitForPageLoad stepKey="waitForAttributeSetEditPageToLoad1"/> -+ <dontSee userInput="$$attribute.attribute_code$$" selector="{{AdminProductAttributeSetEditSection.groupTree}}" stepKey="dontSeeAttributeInAttributeGroupTree"/> -+ <dontSee userInput="$$attribute.attribute_code$$" selector="{{AdminProductAttributeSetEditSection.unassignedAttributesTree}}" stepKey="dontSeeAttributeInUnassignedAttributeTree"/> -+ <!--Verify Product Attribute is not present in Product Index Page --> -+ <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="openProductIndexPage"/> -+ <waitForPageLoad stepKey="waitForProductIndexPageToLoad1"/> -+ <actionGroup ref="filterProductGridBySku" stepKey="filterProduct1"> -+ <argument name="product" value="SimpleProduct2"/> -+ </actionGroup> -+ <!--Verify Product Attribute is not present in Product page --> -+ <click selector="{{AdminProductGridSection.productRowBySku($$createSimpleProduct.sku$$)}}" stepKey="openSelectedProduct1"/> -+ <waitForPageLoad stepKey="waitForProductPageToLoad"/> -+ <dontSeeElement selector="{{AdminProductFormSection.newAddedAttribute($$attribute.attribute_code$$)}}" stepKey="dontSeeProductAttribute"/> -+ </test> -+</tests> -diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteVirtualProductTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteVirtualProductTest.xml -new file mode 100644 -index 00000000000..413d53d1c37 ---- /dev/null -+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteVirtualProductTest.xml -@@ -0,0 +1,54 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> -+ <test name="AdminDeleteVirtualProductTest"> -+ <annotations> -+ <features value="Catalog"/> -+ <stories value="Delete products"/> -+ <title value="Delete Virtual Product"/> -+ <description value="Admin should be able to delete a virtual product"/> -+ <severity value="CRITICAL"/> -+ <testCaseId value="MC-11014"/> -+ <group value="mtf_migrated"/> -+ </annotations> -+ <before> -+ <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> -+ <createData entity="_defaultCategory" stepKey="createCategory"/> -+ <createData entity="defaultVirtualProduct" stepKey="createVirtualProduct"> -+ <requiredEntity createDataKey="createCategory"/> -+ </createData> -+ </before> -+ <after> -+ <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> -+ <actionGroup ref="logout" stepKey="logout"/> -+ </after> -+ -+ <actionGroup ref="deleteProductUsingProductGrid" stepKey="deleteVirtualProductFilteredBySkuAndName"> -+ <argument name="product" value="$$createVirtualProduct$$"/> -+ </actionGroup> -+ <see selector="{{AdminCategoryMessagesSection.SuccessMessage}}" userInput="A total of 1 record(s) have been deleted." stepKey="deleteMessage"/> -+ <!--Verify product on product page --> -+ <amOnPage url="{{StorefrontProductPage.url($$createVirtualProduct.name$$)}}" stepKey="amOnVirtualProductPage"/> -+ <see selector="{{StorefrontProductInfoMainSection.productName}}" userInput="Whoops, our bad..." stepKey="seeWhoops"/> -+ <!-- Search for the product by sku --> -+ <fillField selector="{{StorefrontQuickSearchSection.searchPhrase}}" userInput="$$createVirtualProduct.sku$$" stepKey="fillSearchBarByProductSku"/> -+ <waitForPageLoad stepKey="waitForSearchButton"/> -+ <click selector="{{StorefrontQuickSearchSection.searchButton}}" stepKey="clickSearchButton"/> -+ <waitForPageLoad stepKey="waitForSearchResults"/> -+ <!-- Should not see any search results --> -+ <dontSee userInput="$$createVirtualProduct.sku$$" selector="{{StorefrontCatalogSearchMainSection.searchResults}}" stepKey="dontSeeProduct"/> -+ <see selector="{{StorefrontCatalogSearchMainSection.message}}" userInput="Your search returned no results." stepKey="seeCantFindProductOneMessage"/> -+ <!-- Go to the category page that we created in the before block --> -+ <amOnPage url="{{StorefrontCategoryPage.url($$createCategory.name$$)}}" stepKey="onCategoryPage"/> -+ <!-- Should not see the product --> -+ <dontSee userInput="$$createVirtualProduct.name$$" selector="{{StorefrontCategoryMainSection.productsList}}" stepKey="dontSeeProductInCategory"/> -+ <see selector="{{StorefrontCategoryMainSection.emptyProductMessage}}" userInput="We can't find products matching the selection." stepKey="seeEmptyProductMessage"/> -+ </test> -+</tests> -diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminEditTextEditorProductAttributeTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminEditTextEditorProductAttributeTest.xml -index 71f2ffecb46..53040993beb 100644 ---- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminEditTextEditorProductAttributeTest.xml -+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminEditTextEditorProductAttributeTest.xml -@@ -6,16 +6,16 @@ - */ - --> - <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="AdminEditTextEditorProductAttributeTest"> - <annotations> - <features value="Catalog"/> - <stories value="MAGETWO-51484-Input type configuration for custom Product Attributes"/> - <group value="Catalog"/> -- <title value="Admin should be able to switch between two versions of TinyMCE"/> -- <description value="Admin should be able to switch between two versions of TinyMCE"/> -+ <title value="Admin are able to change Input Type of Text Editor product attribute"/> -+ <description value="Admin are able to change Input Type of Text Editor product attribute"/> - <severity value="CRITICAL"/> -- <testCaseId value="MAGETWO-85745"/> -+ <testCaseId value="MC-6215"/> - </annotations> - <before> - <actionGroup ref="LoginActionGroup" stepKey="loginGetFromGeneralFile"/> -@@ -26,10 +26,9 @@ - <requiredEntity createDataKey="myProductAttributeCreation"/> - </createData> - </before> -- <amOnPage url="{{AdminProductAttributeGridPage.url}}" stepKey="navigateToProductAttributeGrid1"/> -- <waitForPageLoad stepKey="waitForPageLoad1"/> -- <click selector="{{AdminProductAttributeGridSection.AttributeCode($$myProductAttributeCreation.attribute_code$$)}}" stepKey="navigateToAttributeEditPage1" /> -- <waitForPageLoad stepKey="waitForPageLoad2" /> -+ <actionGroup ref="navigateToCreatedProductAttribute" stepKey="navigateToAttribute"> -+ <argument name="ProductAttribute" value="productAttributeWysiwyg"/> -+ </actionGroup> - <seeOptionIsSelected selector="{{AttributePropertiesSection.InputType}}" userInput="Text Editor" stepKey="seeTextEditorSelected" /> - <see selector="{{AttributePropertiesSection.InputType}}" userInput="Text Area" stepKey="seeTextArea1" /> - <selectOption selector="{{AttributePropertiesSection.InputType}}" userInput="Text Area" stepKey="selectTextArea" /> -diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminFilterByNameByStoreViewOnProductGridTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminFilterByNameByStoreViewOnProductGridTest.xml -new file mode 100644 -index 00000000000..f3ec225540c ---- /dev/null -+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminFilterByNameByStoreViewOnProductGridTest.xml -@@ -0,0 +1,45 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> -+ <test name="AdminFilterByNameByStoreViewOnProductGridTest"> -+ <annotations> -+ <features value="Catalog"/> -+ <stories value="Filter products"/> -+ <title value="Product grid filtering by store view level attribute"/> -+ <description value="Verify that products grid can be filtered on all store view level by attribute"/> -+ <severity value="MAJOR"/> -+ <testCaseId value="MAGETWO-98755"/> -+ <useCaseId value="MAGETWO-98335"/> -+ <group value="catalog"/> -+ </annotations> -+ <before> -+ <createData entity="SimpleProduct2" stepKey="createSimpleProduct"/> -+ <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> -+ </before> -+ <after> -+ <deleteData createDataKey="createSimpleProduct" stepKey="deleteSimpleProduct"/> -+ <actionGroup ref="ClearProductsFilterActionGroup" stepKey="clearProductsFilter"/> -+ <actionGroup ref="logout" stepKey="logout"/> -+ </after> -+ <amOnPage url="{{AdminProductEditPage.url($$createSimpleProduct.id$$)}}" stepKey="goToEditPage"/> -+ <actionGroup ref="AdminSwitchStoreViewActionGroup" stepKey="switchToDefaultStoreView"> -+ <argument name="storeView" value="_defaultStore.name"/> -+ </actionGroup> -+ <scrollToTopOfPage stepKey="scrollToTopOfAdminProductFormSection"/> -+ <click selector="{{AdminProductFormSection.productNameUseDefault}}" stepKey="uncheckUseDefault"/> -+ <fillField selector="{{AdminProductFormSection.productName}}" userInput="{{SimpleProduct.name}}" stepKey="fillNewName"/> -+ <actionGroup ref="saveProductForm" stepKey="saveSimpleProduct"/> -+ <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="navigateToProductIndex"/> -+ <actionGroup ref="filterProductGridByName" stepKey="filterGridByName"> -+ <argument name="product" value="SimpleProduct"/> -+ </actionGroup> -+ <see selector="{{AdminProductGridSection.firstProductRow}}" userInput="{{SimpleProduct2.name}}" stepKey="seeProductNameInGrid"/> -+ </test> -+</tests> -\ No newline at end of file -diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminFilteringCategoryProductsUsingScopeSelectorTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminFilteringCategoryProductsUsingScopeSelectorTest.xml -new file mode 100644 -index 00000000000..5c434ecabf8 ---- /dev/null -+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminFilteringCategoryProductsUsingScopeSelectorTest.xml -@@ -0,0 +1,160 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> -+ <test name="AdminFilteringCategoryProductsUsingScopeSelectorTest"> -+ <annotations> -+ <stories value="Filtering Category Products"/> -+ <title value="Filtering Category Products using scope selector"/> -+ <description value="Filtering Category Products using scope selector"/> -+ <severity value="MAJOR"/> -+ <testCaseId value="MAGETWO-48850"/> -+ <group value="catalog"/> -+ </annotations> -+ <before> -+ <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> -+ <!--Create website, Store and Store View--> -+ <actionGroup ref="AdminCreateWebsiteActionGroup" stepKey="createSecondWebsite"> -+ <argument name="newWebsiteName" value="{{secondCustomWebsite.name}}"/> -+ <argument name="websiteCode" value="{{secondCustomWebsite.code}}"/> -+ </actionGroup> -+ <actionGroup ref="AdminCreateNewStoreGroupActionGroup" stepKey="createSecondStoreGroup"> -+ <argument name="website" value="{{secondCustomWebsite.name}}"/> -+ <argument name="storeGroupName" value="{{SecondStoreGroupUnique.name}}"/> -+ <argument name="storeGroupCode" value="{{SecondStoreGroupUnique.code}}"/> -+ </actionGroup> -+ <actionGroup ref="AdminCreateStoreViewActionGroup" stepKey="createSecondStoreView"> -+ <argument name="StoreGroup" value="SecondStoreGroupUnique"/> -+ <argument name="customStore" value="SecondStoreUnique"/> -+ </actionGroup> -+ -+ <!--Create Simple Product and Category --> -+ <createData entity="_defaultCategory" stepKey="createCategory"/> -+ <createData entity="_defaultProduct" stepKey="createProduct0"> -+ <requiredEntity createDataKey="createCategory"/> -+ </createData> -+ <createData entity="_defaultProduct" stepKey="createProduct1"> -+ <requiredEntity createDataKey="createCategory"/> -+ </createData> -+ <createData entity="_defaultProduct" stepKey="createProduct2"> -+ <requiredEntity createDataKey="createCategory"/> -+ </createData> -+ <createData entity="_defaultProduct" stepKey="createProduct12"> -+ <requiredEntity createDataKey="createCategory"/> -+ </createData> -+ -+ <!-- Set filter to product name and product0 not assigned to any website--> -+ <actionGroup ref="SearchForProductOnBackendActionGroup" stepKey="searchForProduct0"> -+ <argument name="product" value="$$createProduct0$$"/> -+ </actionGroup> -+ <actionGroup ref="OpenEditProductOnBackendActionGroup" stepKey="clickOpenProductForEdit0"> -+ <argument name="product" value="$$createProduct0$$"/> -+ </actionGroup> -+ <scrollTo selector="{{ProductInWebsitesSection.sectionHeader}}" stepKey="scrollToWebsitesSection"/> -+ <click selector="{{ProductInWebsitesSection.sectionHeader}}" stepKey="clickToOpenWebsiteSection"/> -+ <waitForPageLoad stepKey="waitForToOpenedWebsiteSection"/> -+ <uncheckOption selector="{{ProductInWebsitesSection.website('Main Website')}}" stepKey="uncheckWebsite"/> -+ <actionGroup ref="saveProductForm" stepKey="saveProduct1"/> -+ -+ <!-- Set filter to product name and product2 in website 2 only --> -+ <actionGroup ref="SearchForProductOnBackendActionGroup" stepKey="searchForProduct2"> -+ <argument name="product" value="$$createProduct2$$"/> -+ </actionGroup> -+ <actionGroup ref="OpenEditProductOnBackendActionGroup" stepKey="clickOpenProductForEdit2"> -+ <argument name="product" value="$$createProduct2$$"/> -+ </actionGroup> -+ <actionGroup ref="SelectProductInWebsitesActionGroup" stepKey="selectProductInWebsites"> -+ <argument name="website" value="{{secondCustomWebsite.name}}"/> -+ </actionGroup> -+ <uncheckOption selector="{{ProductInWebsitesSection.website('Main Website')}}" stepKey="uncheckWebsite1"/> -+ <actionGroup ref="saveProductForm" stepKey="saveProduct2"/> -+ -+ <!-- Set filter to product name and product12 assigned to both websites 1 and 2 --> -+ <actionGroup ref="SearchForProductOnBackendActionGroup" stepKey="searchForProduct12"> -+ <argument name="product" value="$$createProduct12$$"/> -+ </actionGroup> -+ <actionGroup ref="OpenEditProductOnBackendActionGroup" stepKey="clickOpenProductForEdit12"> -+ <argument name="product" value="$$createProduct12$$"/> -+ </actionGroup> -+ <actionGroup ref="SelectProductInWebsitesActionGroup" stepKey="selectProductInWebsites1"> -+ <argument name="website" value="{{secondCustomWebsite.name}}"/> -+ </actionGroup> -+ <actionGroup ref="saveProductForm" stepKey="saveProduct3"/> -+ </before> -+ <after> -+ <actionGroup ref="AdminDeleteWebsiteActionGroup" stepKey="deleteWebsite"> -+ <argument name="websiteName" value="{{secondCustomWebsite.name}}"/> -+ </actionGroup> -+ <actionGroup ref="ClearProductsFilterActionGroup" stepKey="clearProductsFilter"/> -+ <deleteData createDataKey="createProduct0" stepKey="deleteProduct"/> -+ <deleteData createDataKey="createProduct1" stepKey="deleteProduct1"/> -+ <deleteData createDataKey="createProduct2" stepKey="deleteProduct2"/> -+ <deleteData createDataKey="createProduct12" stepKey="deleteProduct3"/> -+ <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> -+ <actionGroup ref="logout" stepKey="logout"/> -+ </after> -+ <!-- Step 1-2: Open Category page and Set scope selector to All Store Views--> -+ <amOnPage url="{{AdminCategoryPage.url}}" stepKey="goToCategoryPage"/> -+ <click selector="{{AdminCategorySidebarTreeSection.categoryInTree($$createCategory.name$$)}}" -+ stepKey="clickCategoryName"/> -+ <click selector="{{AdminCategoryProductsSection.sectionHeader}}" stepKey="openProductSection"/> -+ <grabTextFrom selector="{{AdminCategorySidebarTreeSection.categoryInTree($$createCategory.name$$)}}" -+ stepKey="grabTextFromCategory"/> -+ <assertRegExp expected="/\(4\)$/" expectedType="string" actual="$grabTextFromCategory" actualType="variable" -+ message="wrongCountProductOnAllStoreViews" stepKey="checkCountProducts"/> -+ <see selector="{{AdminCategoryProductsGridSection.productGridNameProduct($$createProduct0.name$$)}}" -+ userInput="$$createProduct0.name$$" stepKey="seeProductName"/> -+ <see selector="{{AdminCategoryProductsGridSection.productGridNameProduct($$createProduct1.name$$)}}" -+ userInput="$$createProduct1.name$$" stepKey="seeProductName1"/> -+ <see selector="{{AdminCategoryProductsGridSection.productGridNameProduct($$createProduct2.name$$)}}" -+ userInput="$$createProduct2.name$$" stepKey="seeProductName2"/> -+ <see selector="{{AdminCategoryProductsGridSection.productGridNameProduct($$createProduct12.name$$)}}" -+ userInput="$$createProduct12.name$$" stepKey="seeProductName3"/> -+ -+ <!-- Step 3: Set scope selector to Website1( Storeview for the Website 1) --> -+ <scrollToTopOfPage stepKey="scrollToTopOfPage"/> -+ <actionGroup ref="AdminSwitchStoreViewActionGroup" stepKey="swichToDefaultStoreView"> -+ <argument name="storeView" value="_defaultStore.name"/> -+ </actionGroup> -+ <grabTextFrom selector="{{AdminCategorySidebarTreeSection.categoryInTree($$createCategory.name$$)}}" -+ stepKey="grabTextFromCategory1"/> -+ <assertRegExp expected="/\(2\)$/" expectedType="string" actual="$grabTextFromCategory1" actualType="variable" -+ message="wrongCountProductOnWebsite1" stepKey="checkCountProducts1"/> -+ <click selector="{{AdminCategoryProductsSection.sectionHeader}}" stepKey="openProductSection1"/> -+ <see selector="{{AdminCategoryProductsGridSection.productGridNameProduct($$createProduct1.name$$)}}" -+ userInput="$$createProduct1.name$$" stepKey="seeProductName4"/> -+ <see selector="{{AdminCategoryProductsGridSection.productGridNameProduct($$createProduct12.name$$)}}" -+ userInput="$$createProduct12.name$$" stepKey="seeProductName5"/> -+ <waitForText userInput="$$createCategory.name$$ (2)" stepKey="seeCorrectProductCount"/> -+ <dontSee selector="{{AdminCategoryProductsGridSection.productGridNameProduct($$createProduct0.name$$)}}" -+ userInput="$$createProduct0.name$$" stepKey="dontSeeProductName"/> -+ <dontSee selector="{{AdminCategoryProductsGridSection.productGridNameProduct($$createProduct2.name$$)}}" -+ userInput="$$createProduct2.name$$" stepKey="dontSeeProductName1"/> -+ -+ <!-- Step 4: Set scope selector to Website2 ( StoreView for Website 2) --> -+ <scrollToTopOfPage stepKey="scrollToTopOfPage1"/> -+ <actionGroup ref="AdminSwitchStoreViewActionGroup" stepKey="swichToSecondStoreView"> -+ <argument name="storeView" value="SecondStoreUnique.name"/> -+ </actionGroup> -+ <click selector="{{AdminCategoryProductsSection.sectionHeader}}" stepKey="openProductSection2"/> -+ <grabTextFrom selector="{{AdminCategorySidebarTreeSection.categoryInTree($$createCategory.name$$)}}" -+ stepKey="grabTextFromCategory2"/> -+ <assertRegExp expected="/\(2\)$/" expectedType="string" actual="$grabTextFromCategory2" actualType="variable" -+ message="wrongCountProductOnWebsite2" stepKey="checkCountProducts2"/> -+ <see selector="{{AdminCategoryProductsGridSection.productGridNameProduct($$createProduct2.name$$)}}" -+ userInput="$$createProduct2.name$$" stepKey="seeProductName6"/> -+ <see selector="{{AdminCategoryProductsGridSection.productGridNameProduct($$createProduct12.name$$)}}" -+ userInput="$$createProduct12.name$$" stepKey="seeProductName7"/> -+ <waitForText userInput="$$createCategory.name$$ (2)" stepKey="seeCorrectProductCount2"/> -+ <dontSee selector="{{AdminCategoryProductsGridSection.productGridNameProduct($$createProduct0.name$$)}}" -+ userInput="$$createProduct0.name$$" stepKey="dontSeeProductName2"/> -+ <dontSee selector="{{AdminCategoryProductsGridSection.productGridNameProduct($$createProduct2.name$$)}}" -+ userInput="$$createProduct1.name$$" stepKey="dontSeeProductName3"/> -+ </test> -+</tests> -diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminGridPageNumberAfterSaveAndCloseActionTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminGridPageNumberAfterSaveAndCloseActionTest.xml -new file mode 100644 -index 00000000000..672205d935d ---- /dev/null -+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminGridPageNumberAfterSaveAndCloseActionTest.xml -@@ -0,0 +1,73 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> -+ <test name="AdminGridPageNumberAfterSaveAndCloseActionTest"> -+ <annotations> -+ <features value="Catalog"/> -+ <stories value="Catalog grid"/> -+ <title value="Checking Catalog grid page number after Save and Close action"/> -+ <description value="Checking Catalog grid page number after Save and Close action"/> -+ <severity value="MAJOR"/> -+ <testCaseId value="MAGETWO-96164"/> -+ <useCaseId value="MAGETWO-96127"/> -+ <group value="Catalog"/> -+ <skip> -+ <issueId value="MC-17332"/> -+ </skip> -+ </annotations> -+ <before> -+ <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> -+ <!--Clear product grid--> -+ <comment userInput="Clear product grid" stepKey="commentClearProductGrid"/> -+ <amOnPage url="{{ProductCatalogPage.url}}" stepKey="goToProductCatalog"/> -+ <waitForPageLoad stepKey="waitForProductIndexPage"/> -+ <conditionalClick selector="{{AdminProductGridFilterSection.clearFilters}}" -+ dependentSelector="{{AdminProductGridFilterSection.clearFilters}}" visible="true" stepKey="clickClearFilters"/> -+ <waitForLoadingMaskToDisappear stepKey="waitForGridLoad"/> -+ <actionGroup ref="deleteProductsIfTheyExist" stepKey="deleteProductIfTheyExist"/> -+ <createData stepKey="category1" entity="SimpleSubCategory"/> -+ <createData stepKey="product1" entity="SimpleProduct"> -+ <requiredEntity createDataKey="category1"/> -+ </createData> -+ <createData stepKey="category2" entity="SimpleSubCategory"/> -+ <createData stepKey="product2" entity="SimpleProduct"> -+ <requiredEntity createDataKey="category2"/> -+ </createData> -+ </before> -+ <after> -+ <amOnPage url="{{ProductCatalogPage.url}}" stepKey="goToProductCatalog"/> -+ <waitForPageLoad stepKey="waitForProductIndexPage"/> -+ <click selector="{{AdminDataGridPaginationSection.previousPage}}" stepKey="clickPrevPageOrderGrid"/> -+ <actionGroup ref="adminDataGridDeleteCustomPerPage" stepKey="deleteCustomAddedPerPage"> -+ <argument name="perPage" value="ProductPerPage.productCount"/> -+ </actionGroup> -+ <deleteData stepKey="deleteCategory1" createDataKey="category1"/> -+ <deleteData stepKey="deleteProduct1" createDataKey="product1"/> -+ <deleteData stepKey="deleteCategory2" createDataKey="category2"/> -+ <deleteData stepKey="deleteProduct2" createDataKey="product2"/> -+ <actionGroup ref="logout" stepKey="logout"/> -+ </after> -+ <amOnPage url="{{ProductCatalogPage.url}}" stepKey="goToProductCatalog"/> -+ <waitForPageLoad stepKey="waitForProductIndexPage"/> -+ <actionGroup ref="adminDataGridSelectCustomPerPage" stepKey="select1OrderPerPage"> -+ <argument name="perPage" value="ProductPerPage.productCount"/> -+ </actionGroup> -+ <!--Go to the next page and edit the product--> -+ <comment userInput="Go to the next page and edit the product" stepKey="commentEdiProduct"/> -+ <click selector="{{AdminDataGridPaginationSection.nextPage}}" stepKey="clickNextPageOrderGrid"/> -+ <waitForPageLoad stepKey="waitForPageLoad"/> -+ <actionGroup ref="OpenEditProductOnBackendActionGroup" stepKey="openEditProduct2"> -+ <argument name="product" value="$$product2$$"/> -+ </actionGroup> -+ <actionGroup ref="AdminFormSaveAndClose" stepKey="saveAndCloseProduct"/> -+ <waitForPageLoad stepKey="waitForPageLoad1"/> -+ <seeInField selector="{{AdminDataGridPaginationSection.currentPage}}" userInput="2" stepKey="seeOnSecondPageOrderGrid"/> -+ </test> -+</tests> -diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminImportCustomizableOptionToProductWithSKUTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminImportCustomizableOptionToProductWithSKUTest.xml -new file mode 100644 -index 00000000000..b1f00a2f51a ---- /dev/null -+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminImportCustomizableOptionToProductWithSKUTest.xml -@@ -0,0 +1,84 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> -+ <test name="AdminImportCustomizableOptionToProductWithSKUTest"> -+ <annotations> -+ <features value="Catalog"/> -+ <stories value="Customizable options"/> -+ <title value="Import customizable options to a product with existing SKU"/> -+ <description value="Import customizable options to a product with existing SKU"/> -+ <severity value="MAJOR"/> -+ <testCaseId value="MAGETWO-98211"/> -+ <useCaseId value="MAGETWO-70232"/> -+ <group value="catalog"/> -+ <skip> -+ <issueId value="MC-17140"/> -+ </skip> -+ </annotations> -+ <before> -+ <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> -+ <!--Create category--> -+ <comment userInput="Create category" stepKey="commentCreateCategory"/> -+ <createData entity="ApiCategory" stepKey="createCategory"/> -+ <!-- Create two product --> -+ <comment userInput="Create two product" stepKey="commentCreateTwoProduct"/> -+ <createData entity="SimpleProduct2" stepKey="createFirstProduct"/> -+ <createData entity="ApiSimpleProduct" stepKey="createSecondProduct"> -+ <requiredEntity createDataKey="createCategory"/> -+ </createData> -+ </before> -+ <after> -+ <!--Delete second product with changed sku--> -+ <comment userInput="Delete second product with changed sku" stepKey="commentDeleteProduct"/> -+ <actionGroup ref="deleteProductBySku" stepKey="deleteSecondProduct"> -+ <argument name="sku" value="$$createFirstProduct.sku$$-1"/> -+ </actionGroup> -+ <!--Delete created data--> -+ <comment userInput="Delete created data" stepKey="commentDeleteCreatedData"/> -+ <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> -+ <deleteData createDataKey="createFirstProduct" stepKey="deleteFirstProduct"/> -+ <actionGroup ref="logout" stepKey="logoutOfAdmin"/> -+ </after> -+ <!--Go to product page --> -+ <comment userInput="Go to product page" stepKey="commentGoToProductPage"/> -+ <amOnPage url="{{AdminProductEditPage.url($$createFirstProduct.id$$)}}" stepKey="goToProductEditPage"/> -+ <waitForPageLoad stepKey="waitForProductEditPageLoad"/> -+ <actionGroup ref="AddProductCustomOptionField" stepKey="addCutomOption1"> -+ <argument name="option" value="ProductOptionField"/> -+ </actionGroup> -+ <actionGroup ref="AddProductCustomOptionField" stepKey="addCutomOption2"> -+ <argument name="option" value="ProductOptionField2"/> -+ </actionGroup> -+ <actionGroup ref="saveProductForm" stepKey="saveProduct"/> -+ <!--Change second product sku to first product sku--> -+ <comment userInput="Change second product sku to first product sku" stepKey="commentChangeSecondProduct"/> -+ <amOnPage url="{{AdminProductEditPage.url($$createSecondProduct.id$$)}}" stepKey="goToProductEditPage1"/> -+ <waitForPageLoad stepKey="waitForProductEditPageLoad1"/> -+ <fillField selector="{{AdminProductFormSection.productSku}}" userInput="$$createFirstProduct.sku$$" stepKey="fillProductSku1"/> -+ <!--Import customizable options and check--> -+ <comment userInput="Import customizable options and check" stepKey="commentImportOptions"/> -+ <conditionalClick selector="{{AdminProductCustomizableOptionsSection.customizableOptions}}" dependentSelector="{{AdminProductCustomizableOptionsSection.addOptionBtn}}" visible="false" stepKey="openCustomOptionSection"/> -+ <actionGroup ref="importProductCustomizableOptions" stepKey="importOptions"> -+ <argument name="productName" value="$$createFirstProduct.name$$"/> -+ </actionGroup> -+ <actionGroup ref="checkCustomizableOptionImport" stepKey="checkFirstOptionImport"> -+ <argument name="option" value="ProductOptionField"/> -+ <argument name="optionIndex" value="0"/> -+ </actionGroup> -+ <actionGroup ref="checkCustomizableOptionImport" stepKey="checkSecondOptionImport"> -+ <argument name="option" value="ProductOptionField2"/> -+ <argument name="optionIndex" value="1"/> -+ </actionGroup> -+ <!--Save product and check sku changed message--> -+ <comment userInput="Save product and check sku changed message" stepKey="commentSAveProductAndCheck"/> -+ <actionGroup ref="saveProductForm" stepKey="saveProduct1"/> -+ <see userInput="SKU for product $$createSecondProduct.name$$ has been changed to $$createFirstProduct.sku$$-1." stepKey="seeSkuChangedMessage"/> -+ </test> -+</tests> -diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminMassChangeProductsStatusTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminMassChangeProductsStatusTest.xml -index 2edca19deeb..8d5121cf214 100644 ---- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminMassChangeProductsStatusTest.xml -+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminMassChangeProductsStatusTest.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="AdminMassChangeProductsStatusTest"> - <annotations> - <features value="Catalog"/> -diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminMassProductPriceUpdateTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminMassProductPriceUpdateTest.xml -new file mode 100644 -index 00000000000..4d581bae700 ---- /dev/null -+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminMassProductPriceUpdateTest.xml -@@ -0,0 +1,79 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> -+ <test name="AdminMassProductPriceUpdateTest"> -+ <annotations> -+ <stories value="Mass product update "/> -+ <features value="Catalog"/> -+ <title value="Mass update simple product price"/> -+ <description value="Login as admin and update mass product price"/> -+ <testCaseId value="MC-8510"/> -+ <severity value="CRITICAL"/> -+ <group value="mtf_migrated"/> -+ </annotations> -+ <before> -+ <actionGroup ref="LoginAsAdmin" stepKey="loginToAdminPanel"/> -+ <createData entity="defaultSimpleProduct" stepKey="simpleProduct1"/> -+ <createData entity="defaultSimpleProduct" stepKey="simpleProduct2"/> -+ </before> -+ <after> -+ <deleteData createDataKey="simpleProduct1" stepKey="deleteSimpleProduct1"/> -+ <deleteData createDataKey="simpleProduct2" stepKey="deleteSimpleProduct2"/> -+ <actionGroup ref="logout" stepKey="logout"/> -+ </after> -+ -+ <!--Open Product Index Page--> -+ <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="navigateToProductIndex"/> -+ <waitForPageLoad stepKey="waitForProductIndexPageToLoad"/> -+ -+ <!--Search products using keyword --> -+ <actionGroup ref="searchProductGridByKeyword2" stepKey="searchByKeyword"> -+ <argument name="keyword" value="Testp"/> -+ </actionGroup> -+ -+ <!--Sort Products by ID in descending order--> -+ <actionGroup ref="sortProductsByIdDescending" stepKey="sortProductsByIdDescending"/> -+ -+ <!--Select products--> -+ <checkOption selector="{{AdminProductGridSection.productRowCheckboxBySku($$simpleProduct1.sku$$)}}" stepKey="selectFirstProduct"/> -+ <checkOption selector="{{AdminProductGridSection.productRowCheckboxBySku($$simpleProduct2.sku$$)}}" stepKey="selectSecondProduct"/> -+ -+ <!-- Update product price--> -+ <click selector="{{AdminProductGridSection.bulkActionDropdown}}" stepKey="clickDropdown"/> -+ <click selector="{{AdminProductGridSection.bulkActionOption('Update attributes')}}" stepKey="clickChangeStatus"/> -+ <waitForPageLoad stepKey="waitForProductAttributePageToLoad"/> -+ <scrollTo stepKey="scrollToPriceCheckBox" selector="{{AdminEditProductAttributesSection.ChangeAttributePriceToggle}}" x="0" y="-160"/> -+ <click selector="{{AdminEditProductAttributesSection.ChangeAttributePriceToggle}}" stepKey="selectPriceCheckBox"/> -+ <fillField stepKey="fillPrice" selector="{{AdminEditProductAttributesSection.AttributePrice}}" userInput="90.99"/> -+ <click stepKey="clickOnSaveButton" selector="{{AdminEditProductAttributesSection.Save}}"/> -+ <waitForPageLoad stepKey="waitForUpdatedProductToSave" /> -+ <see selector="{{AdminProductMessagesSection.successMessage}}" userInput="Message is added to queue" stepKey="seeAttributeUpateSuccessMsg"/> -+ -+ <!-- Run cron twice --> -+ <magentoCLI command="cron:run" stepKey="runCron1"/> -+ <magentoCLI command="cron:run" stepKey="runCron2"/> -+ <reloadPage stepKey="refreshPage"/> -+ <waitForPageLoad stepKey="waitFormToReload1"/> -+ -+ <!--Verify product name, sku and updated price--> -+ <click stepKey="openFirstProduct" selector="{{AdminProductGridSection.productRowBySku($$simpleProduct1.sku$$)}}"/> -+ <waitForPageLoad stepKey="waitForFirstProductToLoad"/> -+ <seeInField stepKey="seeFirstProductNameInField" selector="{{AdminProductFormSection.productName}}" userInput="$$simpleProduct1.name$$"/> -+ <seeInField stepKey="seeFirstProductSkuInField" selector="{{AdminProductFormSection.productSku}}" userInput="$$simpleProduct1.sku$$"/> -+ <seeInField stepKey="seeFirstProductPriceInField" selector="{{AdminProductFormSection.productPrice}}" userInput="90.99"/> -+ <click stepKey="clickOnBackButton" selector="{{AdminGridMainControls.back}}"/> -+ <waitForPageLoad stepKey="waitForProductsToLoad"/> -+ <click stepKey="openSecondProduct" selector="{{AdminProductGridSection.productRowBySku($$simpleProduct2.sku$$)}}"/> -+ <waitForPageLoad stepKey="waitForSecondProductToLoad"/> -+ <seeInField stepKey="seeSecondProductNameInField" selector="{{AdminProductFormSection.productName}}" userInput="$$simpleProduct2.name$$"/> -+ <seeInField stepKey="seeSecondProductSkuInField" selector="{{AdminProductFormSection.productSku}}" userInput="$$simpleProduct2.sku$$"/> -+ <seeInField stepKey="seeSecondProductPriceInField" selector="{{AdminProductFormSection.productPrice}}" userInput="90.99"/> -+ </test> -+</tests> -diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminMassUpdateProductAttributesGlobalScopeTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminMassUpdateProductAttributesGlobalScopeTest.xml -index 7c3e31e90c0..87e0bf3d2e9 100644 ---- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminMassUpdateProductAttributesGlobalScopeTest.xml -+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminMassUpdateProductAttributesGlobalScopeTest.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="AdminMassUpdateProductAttributesGlobalScopeTest"> - <annotations> - <features value="Catalog"/> -@@ -18,6 +18,9 @@ - <testCaseId value="MC-56"/> - <group value="Catalog"/> - <group value="Product Attributes"/> -+ <skip> -+ <issueId value="MC-17140"/> -+ </skip> - </annotations> - <before> - <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> -@@ -58,7 +61,13 @@ - <click selector="{{AdminEditProductAttributesSection.ChangeAttributePriceToggle}}" stepKey="toggleToChangePrice"/> - <fillField selector="{{AdminEditProductAttributesSection.AttributePrice}}" userInput="$$createProductOne.price$$0" stepKey="fillAttributeNameField"/> - <click selector="{{AdminEditProductAttributesSection.Save}}" stepKey="save"/> -- <see selector="{{AdminProductMessagesSection.successMessage}}" userInput="A total of 2 record(s) were updated." stepKey="seeAttributeUpateSuccessMsg"/> -+ <see selector="{{AdminProductMessagesSection.successMessage}}" userInput="Message is added to queue" stepKey="seeAttributeUpateSuccessMsg"/> -+ -+ <!-- Run cron twice --> -+ <magentoCLI command="cron:run" stepKey="runCron1"/> -+ <magentoCLI command="cron:run" stepKey="runCron2"/> -+ <reloadPage stepKey="refreshPage"/> -+ <waitForPageLoad stepKey="waitFormToReload1"/> - - <!-- Assert on storefront default view --> - <actionGroup ref="GoToStoreViewAdvancedCatalogSearchActionGroup" stepKey="GoToStoreViewAdvancedCatalogSearchActionGroupDefault"/> -diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminMassUpdateProductAttributesMissingRequiredFieldTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminMassUpdateProductAttributesMissingRequiredFieldTest.xml -index be0bc2bf52a..fe0e46369c5 100644 ---- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminMassUpdateProductAttributesMissingRequiredFieldTest.xml -+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminMassUpdateProductAttributesMissingRequiredFieldTest.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="AdminMassUpdateProductAttributesMissingRequiredFieldTest"> - <annotations> - <features value="Catalog"/> -diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminMassUpdateProductAttributesStoreViewScopeTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminMassUpdateProductAttributesStoreViewScopeTest.xml -index f7a04709b76..bee13bec370 100644 ---- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminMassUpdateProductAttributesStoreViewScopeTest.xml -+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminMassUpdateProductAttributesStoreViewScopeTest.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="AdminMassUpdateProductAttributesStoreViewScopeTest"> - <annotations> - <features value="Catalog"/> -@@ -52,7 +52,13 @@ - <click selector="{{AdminEditProductAttributesSection.ChangeAttributeDescriptionToggle}}" stepKey="toggleToChangeDescription"/> - <fillField selector="{{AdminEditProductAttributesSection.AttributeDescription}}" userInput="Updated $$createProductOne.custom_attributes[description]$$" stepKey="fillAttributeDescriptionField"/> - <click selector="{{AdminEditProductAttributesSection.Save}}" stepKey="save"/> -- <see selector="{{AdminProductMessagesSection.successMessage}}" userInput="A total of 2 record(s) were updated." stepKey="seeAttributeUpateSuccessMsg"/> -+ <see selector="{{AdminProductMessagesSection.successMessage}}" userInput="Message is added to queue" stepKey="seeAttributeUpateSuccessMsg"/> -+ -+ <!-- Run cron twice --> -+ <magentoCLI command="cron:run" stepKey="runCron1"/> -+ <magentoCLI command="cron:run" stepKey="runCron2"/> -+ <reloadPage stepKey="refreshPage"/> -+ <waitForPageLoad stepKey="waitFormToReload1"/> - - <!-- Assert on storefront default view --> - <actionGroup ref="GoToStoreViewAdvancedCatalogSearchActionGroup" stepKey="GoToStoreViewAdvancedCatalogSearchActionGroupDefault"/> -diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminMassUpdateProductStatusStoreViewScopeTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminMassUpdateProductStatusStoreViewScopeTest.xml -new file mode 100644 -index 00000000000..e9b54e3f1a3 ---- /dev/null -+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminMassUpdateProductStatusStoreViewScopeTest.xml -@@ -0,0 +1,151 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> -+ <test name="AdminMassUpdateProductStatusStoreViewScopeTest"> -+ <annotations> -+ <features value="Catalog"/> -+ <stories value="Mass update product status"/> -+ <title value="Admin should be able to mass update product statuses in store view scope"/> -+ <description value="Admin should be able to mass update product statuses in store view scope"/> -+ <severity value="AVERAGE"/> -+ <testCaseId value="MAGETWO-59361"/> -+ <group value="Catalog"/> -+ <group value="Product Attributes"/> -+ </annotations> -+ <before> -+ <actionGroup ref="LoginActionGroup" stepKey="loginAsAdmin"/> -+ -+ <!--Create Website --> -+ <actionGroup ref="AdminCreateWebsiteActionGroup" stepKey="createAdditionalWebsite"> -+ <argument name="newWebsiteName" value="Second Website"/> -+ <argument name="websiteCode" value="second_website"/> -+ </actionGroup> -+ -+ <!--Create Store --> -+ <actionGroup ref="AdminCreateNewStoreGroupActionGroup" stepKey="createNewStore"> -+ <argument name="website" value="Second Website"/> -+ <argument name="storeGroupName" value="Second Store"/> -+ <argument name="storeGroupCode" value="second_store"/> -+ </actionGroup> -+ -+ <!--Create Store view --> -+ <amOnPage url="{{AdminSystemStorePage.url}}" stepKey="amOnAdminSystemStorePage"/> -+ <waitForPageLoad stepKey="waitForSystemStorePage"/> -+ <click selector="{{AdminStoresMainActionsSection.createStoreViewButton}}" stepKey="createStoreViewButton"/> -+ <waitForPageLoad stepKey="waitForProductPageLoad"/> -+ <waitForElementVisible selector="//legend[contains(., 'Store View Information')]" stepKey="waitForNewStorePageToOpen"/> -+ <selectOption userInput="Second Store" selector="{{AdminNewStoreSection.storeGrpDropdown}}" stepKey="selectStoreGroup"/> -+ <fillField userInput="Second Store View" selector="{{AdminNewStoreSection.storeNameTextField}}" stepKey="fillStoreViewName"/> -+ <fillField userInput="second_store_view" selector="{{AdminNewStoreSection.storeCodeTextField}}" stepKey="fillStoreViewCode"/> -+ <selectOption selector="{{AdminNewStoreSection.statusDropdown}}" userInput="1" stepKey="enableStoreViewStatus"/> -+ <click selector="{{AdminNewStoreViewActionsSection.saveButton}}" stepKey="clickSaveStoreView" /> -+ <waitForElementVisible selector="{{AdminConfirmationModalSection.ok}}" stepKey="waitForModal" /> -+ <see selector="{{AdminConfirmationModalSection.title}}" userInput="Warning message" stepKey="seeWarning" /> -+ <click selector="{{AdminConfirmationModalSection.ok}}" stepKey="dismissModal" /> -+ <waitForPageLoad stepKey="waitForPageLoad2" time="180" /> -+ <waitForElementVisible selector="{{AdminStoresGridSection.storeFilterTextField}}" time="150" stepKey="waitForPageReolad"/> -+ <see userInput="You saved the store view." stepKey="seeSavedMessage" /> -+ -+ <!--Create a Simple Product 1 --> -+ <actionGroup ref="createSimpleProductAndAddToWebsite" stepKey="createSimpleProduct1"> -+ <argument name="product" value="simpleProductForMassUpdate"/> -+ <argument name="website" value="Second Website"/> -+ </actionGroup> -+ -+ <!--Create a Simple Product 2 --> -+ <actionGroup ref="createSimpleProductAndAddToWebsite" stepKey="createSimpleProduct2"> -+ <argument name="product" value="simpleProductForMassUpdate2"/> -+ <argument name="website" value="Second Website"/> -+ </actionGroup> -+ </before> -+ <after> -+ <!--Delete website --> -+ <actionGroup ref="AdminDeleteWebsiteActionGroup" stepKey="deleteSecondWebsite"> -+ <argument name="websiteName" value="Second Website"/> -+ </actionGroup> -+ <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="navigateToProductIndex"/> -+ -+ <!--Delete Products --> -+ <actionGroup ref="DeleteProductActionGroup" stepKey="deleteProduct1"> -+ <argument name="productName" value="simpleProductForMassUpdate.name"/> -+ </actionGroup> -+ <actionGroup ref="DeleteProductActionGroup" stepKey="deleteProduct2"> -+ <argument name="productName" value="simpleProductForMassUpdate2.name"/> -+ </actionGroup> -+ <actionGroup ref="logout" stepKey="amOnLogoutPage"/> -+ </after> -+ -+ <!-- Search and select products --> -+ <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="navigateToProductIndex"/> -+ <waitForPageLoad stepKey="waitForProductIndexPageLoad"/> -+ <actionGroup ref="searchProductGridByKeyword2" stepKey="searchByKeyword"> -+ <argument name="keyword" value="{{simpleProductForMassUpdate.keyword}}"/> -+ </actionGroup> -+ <actionGroup ref="sortProductsByIdDescending" stepKey="sortProductsByIdDescending"/> -+ -+ <!-- Filter to Second Store View --> -+ <actionGroup ref="AdminFilterStoreViewActionGroup" stepKey="filterStoreView" > -+ <argument name="customStore" value="'Second Store View'" /> -+ </actionGroup> -+ -+ <!-- Select Product 2 --> -+ <click selector="{{AdminProductGridSection.productGridCheckboxOnRow('2')}}" stepKey="clickCheckbox2"/> -+ -+ <!-- Mass update attributes --> -+ <click selector="{{AdminProductGridSection.bulkActionDropdown}}" stepKey="clickDropdown"/> -+ <click selector="{{AdminProductGridSection.bulkActionOption('Change status')}}" stepKey="clickOption"/> -+ <click selector="{{AdminProductGridSection.bulkActionOption('Disable')}}" stepKey="clickDisabled"/> -+ <waitForPageLoad stepKey="waitForBulkUpdatePage"/> -+ -+ <!-- Verify Product Statuses --> -+ <see selector="{{AdminProductGridSection.productGridContentsOnRow('1')}}" userInput="Enabled" stepKey="checkIfProduct1IsEnabled"/> -+ <see selector="{{AdminProductGridSection.productGridContentsOnRow('2')}}" userInput="Disabled" stepKey="checkIfProduct2IsDisabled"/> -+ -+ <!-- Filter to Default Store View --> -+ <actionGroup ref="AdminFilterStoreViewActionGroup" stepKey="filterDefaultStoreView"> -+ <argument name="customStore" value="'Default'" /> -+ </actionGroup> -+ -+ <!-- Verify Product Statuses --> -+ <see selector="{{AdminProductGridSection.productGridContentsOnRow('1')}}" userInput="Enabled" stepKey="checkIfDefaultViewProduct1IsEnabled"/> -+ <see selector="{{AdminProductGridSection.productGridContentsOnRow('2')}}" userInput="Enabled" stepKey="checkIfDefaultViewProduct2IsEnabled"/> -+ -+ <!-- Assert on storefront default view --> -+ <actionGroup ref="GoToStoreViewAdvancedCatalogSearchActionGroup" stepKey="GoToStoreViewAdvancedCatalogSearchActionGroupDefault"/> -+ <actionGroup ref="StorefrontAdvancedCatalogSearchByProductNameAndDescriptionActionGroup" stepKey="searchByNameDefault"> -+ <argument name="name" value="{{simpleProductForMassUpdate.keyword}}"/> -+ <argument name="description" value=""/> -+ </actionGroup> -+ <actionGroup ref="StorefrontCheckAdvancedSearchResultActionGroup" stepKey="StorefrontCheckAdvancedSearchResultDefault"/> -+ <see userInput="2 items" selector="{{StorefrontCatalogSearchAdvancedResultMainSection.itemFound}}" stepKey="seeInDefault"/> -+ -+ <!-- Enable the product in Default store view --> -+ <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="navigateToProductIndex2"/> -+ <waitForPageLoad stepKey="waitForProductIndexPageLoad2"/> -+ <click selector="{{AdminProductGridSection.productGridCheckboxOnRow('1')}}" stepKey="clickCheckboxDefaultStoreView"/> -+ <click selector="{{AdminProductGridSection.productGridCheckboxOnRow('2')}}" stepKey="clickCheckboxDefaultStoreView2"/> -+ -+ <!-- Mass update attributes --> -+ <click selector="{{AdminProductGridSection.bulkActionDropdown}}" stepKey="clickDropdownDefaultStoreView"/> -+ <click selector="{{AdminProductGridSection.bulkActionOption('Change status')}}" stepKey="clickOptionDefaultStoreView"/> -+ <click selector="{{AdminProductGridSection.bulkActionOption('Disable')}}" stepKey="clickDisabledDefaultStoreView"/> -+ <waitForPageLoad stepKey="waitForBulkUpdatePageDefaultStoreView"/> -+ <see selector="{{AdminProductGridSection.productGridContentsOnRow('1')}}" userInput="Disabled" stepKey="checkIfProduct2IsDisabledDefaultStoreView"/> -+ -+ <!-- Assert on storefront default view --> -+ <actionGroup ref="GoToStoreViewAdvancedCatalogSearchActionGroup" stepKey="GoToStoreViewAdvancedCatalogSearchActionGroupDefault2"/> -+ <actionGroup ref="StorefrontAdvancedCatalogSearchByProductNameAndDescriptionActionGroup" stepKey="searchByNameDefault2"> -+ <argument name="name" value="{{simpleProductForMassUpdate.name}}"/> -+ <argument name="description" value=""/> -+ </actionGroup> -+ <actionGroup ref="StorefrontCheckAdvancedSearchResultActionGroup" stepKey="StorefrontCheckAdvancedSearchResultDefault2"/> -+ <see userInput="We can't find any items matching these search criteria." selector="{{StorefrontCatalogSearchAdvancedResultMainSection.message}}" stepKey="seeInDefault2"/> -+ </test> -+</tests> -diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminMoveAnchoredCategoryTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminMoveAnchoredCategoryTest.xml -index d29fde8590c..551b3437cb8 100644 ---- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminMoveAnchoredCategoryTest.xml -+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminMoveAnchoredCategoryTest.xml -@@ -6,10 +6,11 @@ - */ - --> - --<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"> -+<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> - <test name="AdminMoveAnchoredCategoryTest"> - <annotations> - <features value="Catalog"/> -+ <stories value="Edit categories"/> - <title value="Admin should be able to move a category via categories tree and changes should be applied on frontend without a forced cache cleaning"/> - <description value="Admin should be able to move a category via categories tree and changes should be applied on frontend without a forced cache cleaning"/> - <severity value="CRITICAL"/> -diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminMoveAnchoredCategoryToDefaultCategoryTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminMoveAnchoredCategoryToDefaultCategoryTest.xml -new file mode 100644 -index 00000000000..247711295a5 ---- /dev/null -+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminMoveAnchoredCategoryToDefaultCategoryTest.xml -@@ -0,0 +1,123 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> -+ <test name="AdminMoveAnchoredCategoryToDefaultCategoryTest"> -+ <annotations> -+ <stories value="Move categories"/> -+ <title value="Move default anchored subcategory with anchored parent to default subcategory"/> -+ <description value="Login as admin,move anchored subcategory with anchored parent to default subcategory"/> -+ <testCaseId value="MC-6493"/> -+ <severity value="CRITICAL"/> -+ <group value="mtf_migrated"/> -+ <features value="Catalog"/> -+ </annotations> -+ <before> -+ <actionGroup ref="LoginAsAdmin" stepKey="loginToAdminPanel"/> -+ <createData entity="_defaultCategory" stepKey="createDefaultCategory"/> -+ <createData entity="defaultSimpleProduct" stepKey="simpleProduct"/> -+ </before> -+ <after> -+ <deleteData createDataKey="createDefaultCategory" stepKey="deleteDefaultCategory"/> -+ <deleteData createDataKey="simpleProduct" stepKey="deleteSimpleProduct"/> -+ <actionGroup ref="logout" stepKey="logout"/> -+ </after> -+ -+ <!--Open Category Page--> -+ <amOnPage url="{{AdminCategoryPage.url}}" stepKey="openAdminCategoryIndexPage"/> -+ <waitForPageLoad stepKey="waitForPageToLoaded"/> -+ <click selector="{{AdminCategorySidebarTreeSection.expandAll}}" stepKey="clickOnExpandTree"/> -+ <waitForPageLoad stepKey="waitForCategoryToLoad"/> -+ <click selector="{{AdminCategorySidebarTreeSection.categoryInTree(_defaultCategory.name)}}" stepKey="selectCategory"/> -+ <waitForPageLoad stepKey="waitForPageToLoad"/> -+ -+ <!--Enable Anchor for _defaultCategory Category--> -+ <scrollTo selector="{{CategoryDisplaySettingsSection.DisplaySettingTab}}" x="0" y="-80" stepKey="scrollToDisplaySetting"/> -+ <click selector="{{CategoryDisplaySettingsSection.DisplaySettingTab}}" stepKey="selectDisplaySetting"/> -+ <checkOption selector="{{CategoryDisplaySettingsSection.anchor}}" stepKey="enableAnchor"/> -+ <click selector="{{AdminCategoryMainActionsSection.SaveButton}}" stepKey="saveSubCategory"/> -+ <waitForPageLoad stepKey="waitForSecondCategoryToSave"/> -+ <seeElement selector="{{AdminCategoryMessagesSection.SuccessMessage}}" stepKey="seeSuccessMessage"/> -+ -+ <!--Enable Anchor for FirstLevelSubCat Category--> -+ <click selector="{{AdminCategorySidebarActionSection.AddSubcategoryButton}}" stepKey="clickOnAddSubCategoryButton"/> -+ <fillField selector="{{AdminCategoryBasicFieldSection.CategoryNameInput}}" userInput="{{FirstLevelSubCat.name}}" stepKey="addSubCategoryName"/> -+ <scrollTo selector="{{CategoryDisplaySettingsSection.DisplaySettingTab}}" x="0" y="-80" stepKey="scrollToDisplaySetting1"/> -+ <click selector="{{CategoryDisplaySettingsSection.DisplaySettingTab}}" stepKey="selectDisplaySetting1"/> -+ <checkOption selector="{{CategoryDisplaySettingsSection.anchor}}" stepKey="enableAnchor1"/> -+ <click selector="{{AdminCategoryMainActionsSection.SaveButton}}" stepKey="saveSubCategory1"/> -+ <waitForPageLoad stepKey="waitForSecondCategoryToSave1"/> -+ <seeElement selector="{{AdminCategoryMessagesSection.SuccessMessage}}" stepKey="seeSuccessMessage1"/> -+ -+ <!--Enable Anchor for SimpleSubCategory Category and add products to the Category--> -+ <click selector="{{AdminCategorySidebarActionSection.AddSubcategoryButton}}" stepKey="clickOnAddSubCategoryButton1"/> -+ <fillField selector="{{AdminCategoryBasicFieldSection.CategoryNameInput}}" userInput="{{SimpleSubCategory.name}}" stepKey="addSubCategoryName1"/> -+ <scrollTo selector="{{CategoryDisplaySettingsSection.DisplaySettingTab}}" x="0" y="-80" stepKey="scrollToDisplaySetting2"/> -+ <click selector="{{CategoryDisplaySettingsSection.DisplaySettingTab}}" stepKey="selectDisplaySetting2"/> -+ <checkOption selector="{{CategoryDisplaySettingsSection.anchor}}" stepKey="enableAnchor2"/> -+ <scrollTo selector="{{AdminCategoryBasicFieldSection.productsInCategory}}" x="0" y="-80" stepKey="scrollToProductInCategory1"/> -+ <click selector="{{AdminCategoryBasicFieldSection.productsInCategory}}" stepKey="clickOnProductInCategory"/> -+ <fillField selector="{{AdminCategoryContentSection.productTableColumnName}}" userInput="$$simpleProduct.name$$" stepKey="selectProduct"/> -+ <click selector="{{AdminCategoryContentSection.productSearch}}" stepKey="clickSearchButton"/> -+ <click selector="{{AdminCategoryContentSection.productTableRow}}" stepKey="selectProductFromTableRow"/> -+ <click selector="{{AdminCategoryMainActionsSection.SaveButton}}" stepKey="saveSubCategory2"/> -+ <waitForPageLoad stepKey="waitForSecondCategoryToSave2"/> -+ <seeElement selector="{{AdminCategoryMessagesSection.SuccessMessage}}" stepKey="seeSuccessMessage2"/> -+ -+ <!--Open Category in store front page--> -+ <amOnPage url="/$$createDefaultCategory.name$$/{{FirstLevelSubCat.name}}/{{SimpleSubCategory.name}}.html" stepKey="seeTheCategoryInStoreFrontPage"/> -+ <waitForPageLoad stepKey="waitForStoreFrontPageLoad"/> -+ <seeElement selector="{{StorefrontHeaderSection.NavigationCategoryByName(_defaultCategory.name)}}" stepKey="seeDefaultCategoryOnStoreNavigationBar"/> -+ <dontSeeElement selector="{{StorefrontHeaderSection.NavigationCategoryByName(SimpleSubCategory.name)}}" stepKey="dontSeeSubCategoryOnStoreNavigationBar"/> -+ -+ <!--<Verify breadcrumbs in store front page--> -+ <grabMultiple selector="{{StorefrontNavigationSection.categoryBreadcrumbs}}" stepKey="breadcrumbs"/> -+ <assertEquals stepKey="verifyTheCategoryInStoreFrontPage"> -+ <expectedResult type="array">['Home', $$createDefaultCategory.name$$,{{FirstLevelSubCat.name}}, {{SimpleSubCategory.name}}]</expectedResult> -+ <actualResult type="variable">breadcrumbs</actualResult> -+ </assertEquals> -+ -+ <!--Verify Product displayed in category store front page--> -+ <click selector="{{StorefrontCategoryMainSection.productLink}}" stepKey="openSearchedProduct"/> -+ <waitForPageLoad stepKey="waitForProductToLoad1"/> -+ <see selector="{{StorefrontProductInfoMainSection.productName}}" userInput="{{defaultSimpleProduct.name}}" stepKey="assertProductName"/> -+ -+ <!--Open Category Page--> -+ <amOnPage url="{{AdminCategoryPage.url}}" stepKey="openAdminCategoryIndexPage1"/> -+ <waitForPageLoad stepKey="waitForPageToLoad1"/> -+ <click selector="{{AdminCategorySidebarTreeSection.expandAll}}" stepKey="clickOnExpandTree2"/> -+ <waitForPageLoad stepKey="waitForPageToLoad2"/> -+ -+ <!--Move SubCategory under Default Category--> -+ <dragAndDrop selector1="{{AdminCategorySidebarTreeSection.categoryInTree(SimpleSubCategory.name)}}" selector2="{{AdminCategorySidebarTreeSection.categoryInTree('Default Category')}}" stepKey="moveCategory"/> -+ <see selector="{{AdminCategoryModalSection.message}}" userInput="This operation can take a long time" stepKey="seeWarningMessage"/> -+ <click selector="{{AdminCategoryModalSection.ok}}" stepKey="clickOkButtonOnWarningPopup"/> -+ <waitForPageLoad stepKey="waitForPageToLoad3"/> -+ <see selector="{{AdminCategoryMessagesSection.SuccessMessage}}" userInput="You moved the category." stepKey="seeSuccessMoveMessage"/> -+ <amOnPage url="/{{SimpleSubCategory.name}}.html" stepKey="seeTheCategoryInStoreFrontPage1"/> -+ <waitForPageLoad stepKey="waitForStoreFrontPageLoad1"/> -+ -+ <!--Verify breadcrumbs in store front page after the move--> -+ <grabMultiple selector="{{StorefrontNavigationSection.categoryBreadcrumbs}}" stepKey="breadcrumbsAfterMove"/> -+ <assertEquals stepKey="verifyBreadcrumbsInFrontPageAfterMove"> -+ <expectedResult type="array">['Home',{{SimpleSubCategory.name}}]</expectedResult> -+ <actualResult type="variable">breadcrumbsAfterMove</actualResult> -+ </assertEquals> -+ -+ <!--Open Category in store front--> -+ <amOnPage url="{{StorefrontCategoryPage.url(SimpleSubCategory.name)}}" stepKey="amOnCategoryPage"/> -+ <waitForPageLoad stepKey="waitForPageToBeLoaded"/> -+ <seeElement selector="{{StorefrontCategoryMainSection.CategoryTitle(SimpleSubCategory.name)}}" stepKey="seeCategoryInTitle"/> -+ <seeElement selector="{{StorefrontHeaderSection.NavigationCategoryByName(SimpleSubCategory.name)}}" stepKey="seeCategoryOnStoreNavigationBarAfterMove"/> -+ <click selector="{{StorefrontCategoryMainSection.productLink}}" stepKey="openSearchedProduct1"/> -+ <waitForPageLoad stepKey="waitForProductToLoad2"/> -+ -+ <!--Verify product name on Store Front--> -+ <see selector="{{StorefrontProductInfoMainSection.productName}}" userInput="{{defaultSimpleProduct.name}}" stepKey="assertProductNameAfterMove"/> -+ </test> -+</tests> -diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminMoveCategoryAndCheckUrlRewritesTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminMoveCategoryAndCheckUrlRewritesTest.xml -new file mode 100644 -index 00000000000..ba6e6a43674 ---- /dev/null -+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminMoveCategoryAndCheckUrlRewritesTest.xml -@@ -0,0 +1,113 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> -+ <test name="AdminMoveCategoryAndCheckUrlRewritesTest"> -+ <annotations> -+ <stories value="Move categories"/> -+ <title value="URL Rewrites for subcategories during creation and move"/> -+ <description value="Login as admin, move category from one to another and check category url rewrites"/> -+ <testCaseId value="MC-6494"/> -+ <features value="Catalog"/> -+ <severity value="CRITICAL"/> -+ <group value="mtf_migrated"/> -+ </annotations> -+ <before> -+ <actionGroup ref="LoginAsAdmin" stepKey="loginToAdminPanel"/> -+ <createData entity="_defaultCategory" stepKey="createDefaultCategory"/> -+ </before> -+ <after> -+ <deleteData createDataKey="createDefaultCategory" stepKey="deleteCategory"/> -+ <actionGroup ref="logout" stepKey="logout"/> -+ </after> -+ -+ <!--Open category page--> -+ <amOnPage url="{{AdminCategoryPage.url}}" stepKey="openAdminCategoryIndexPage"/> -+ <waitForPageLoad stepKey="waitForPageToLoaded"/> -+ <click selector="{{AdminCategorySidebarTreeSection.expandAll}}" stepKey="clickOnExpandTree"/> -+ <waitForPageLoad stepKey="waitForCategoryToLoad"/> -+ <click selector="{{AdminCategorySidebarTreeSection.categoryInTree(_defaultCategory.name)}}" stepKey="selectCategory"/> -+ <waitForPageLoad stepKey="waitForPageToLoad"/> -+ -+ <!--Create second level category--> -+ <click selector="{{AdminCategorySidebarActionSection.AddSubcategoryButton}}" stepKey="clickOnAddSubCategoryButton"/> -+ <fillField selector="{{AdminCategoryBasicFieldSection.CategoryNameInput}}" userInput="{{SubCategory.name}}" stepKey="addSubCategoryName"/> -+ <click selector="{{AdminCategoryMainActionsSection.SaveButton}}" stepKey="saveSubCategory1"/> -+ <waitForPageLoad stepKey="waitForSecondCategoryToSave1"/> -+ <seeElement selector="{{AdminCategoryMessagesSection.SuccessMessage}}" stepKey="seeSuccessMessage1"/> -+ -+ <!--Create third level category under second level category--> -+ <click selector="{{AdminCategorySidebarActionSection.AddSubcategoryButton}}" stepKey="clickOnAddSubCategoryButton1"/> -+ <fillField selector="{{AdminCategoryBasicFieldSection.CategoryNameInput}}" userInput="{{SimpleSubCategory.name}}" stepKey="addSubCategoryName1"/> -+ <click selector="{{AdminCategoryMainActionsSection.SaveButton}}" stepKey="saveSubCategory2"/> -+ <waitForPageLoad stepKey="waitForSecondCategoryToSave2"/> -+ <seeElement selector="{{AdminCategoryMessagesSection.SuccessMessage}}" stepKey="seeSuccessMessage2"/> -+ <grabFromCurrentUrl stepKey="categoryId" regex="#\/([0-9]*)?\/$#" /> -+ -+ <!--Open Url Rewrite Page--> -+ <amOnPage url="{{AdminUrlRewriteIndexPage.url}}" stepKey="openUrlRewriteIndexPage"/> -+ <waitForPageLoad stepKey="waitForUrlRewritePage"/> -+ -+ <!--Search third level category Redirect Path, Target Path and Redirect Type--> -+ <fillField selector="{{AdminUrlRewriteIndexSection.requestPathFilter}}" userInput="{{SimpleSubCategory.name_lwr}}" stepKey="fillRedirectPathFilter"/> -+ <click selector="{{AdminUrlRewriteIndexSection.searchButton}}" stepKey="clickOnSearchButton"/> -+ <waitForPageLoad stepKey="waitForPageToLoad0"/> -+ -+ <!--Verify Category RedirectType--> -+ <see stepKey="verifyTheRedirectType" selector="{{AdminUrlRewriteIndexSection.redirectTypeColumn('1')}}" userInput="No" /> -+ -+ <!--Verify Redirect Path --> -+ <see selector="{{AdminUrlRewriteIndexSection.requestPathColumn('1')}}" userInput="{{_defaultCategory.name_lwr}}2/{{SubCategory.name_lwr}}/{{SimpleSubCategory.name_lwr}}.html" stepKey="verifyTheRedirectPath"/> -+ -+ <!--Verify Category Target Path--> -+ <see stepKey="verifyTheTargetPath" selector="{{AdminUrlRewriteIndexSection.targetPathColumn('1')}}" userInput="catalog/category/view/id/{$categoryId}"/> -+ -+ <!--Open Category Page --> -+ <amOnPage url="{{AdminCategoryPage.url}}" stepKey="openAdminCategoryIndexPage1"/> -+ <waitForPageLoad stepKey="waitForPageToLoad1"/> -+ <click selector="{{AdminCategorySidebarTreeSection.expandAll}}" stepKey="clickOnExpandTree2"/> -+ <waitForPageLoad stepKey="waitForPageToLoad2"/> -+ -+ <!--Move the third level category under first level category --> -+ <dragAndDrop selector1="{{AdminCategorySidebarTreeSection.categoryInTree(SimpleSubCategory.name)}}" selector2="{{AdminCategorySidebarTreeSection.categoryInTree(_defaultCategory.name)}}" stepKey="moveCategory"/> -+ <see selector="{{AdminCategoryModalSection.message}}" userInput="This operation can take a long time" stepKey="seeWarningMessage"/> -+ <click selector="{{AdminCategoryModalSection.ok}}" stepKey="clickOkButtonOnWarningPopup"/> -+ <waitForPageLoad stepKey="waitForPageToLoad3"/> -+ <see selector="{{AdminCategoryMessagesSection.SuccessMessage}}" userInput="You moved the category." stepKey="seeSuccessMoveMessage"/> -+ -+ <!--Open Url Rewrite page --> -+ <amOnPage url="{{AdminUrlRewriteIndexPage.url}}" stepKey="openUrlRewriteIndexPage1"/> -+ <waitForPageLoad stepKey="waitForUrlRewritePage1"/> -+ <fillField selector="{{AdminUrlRewriteIndexSection.requestPathFilter}}" userInput="{{_defaultCategory.name_lwr}}2/{{SimpleSubCategory.name_lwr}}.html" stepKey="fillCategoryUrlKey1"/> -+ <click selector="{{AdminUrlRewriteIndexSection.searchButton}}" stepKey="clickOnSearchButton1"/> -+ <waitForPageLoad stepKey="waitForPageToLoad4"/> -+ -+ <!--Verify new Redirect Path after move --> -+ <see stepKey="verifyTheRequestPathAfterMove" selector="{{AdminUrlRewriteIndexSection.requestPathColumn('1')}}" userInput="{{_defaultCategory.name_lwr}}2/{{SimpleSubCategory.name_lwr}}.html" /> -+ -+ <!--Verify new Target Path after move --> -+ <see stepKey="verifyTheTargetPathAfterMove" selector="{{AdminUrlRewriteIndexSection.targetPathColumn('1')}}" userInput="catalog/category/view/id/{$categoryId}" /> -+ -+ <!--Verify new RedirectType after move --> -+ <see stepKey="verifyTheRedirectTypeAfterMove" selector="{{AdminUrlRewriteIndexSection.redirectTypeColumn('1')}}" userInput="No" /> -+ -+ <!--Verify before move Redirect Path displayed with associated Target Path and Redirect Type--> -+ <fillField selector="{{AdminUrlRewriteIndexSection.requestPathFilter}}" userInput="{{SimpleSubCategory.name_lwr}}" stepKey="fillCategoryUrlKey2"/> -+ <click selector="{{AdminUrlRewriteIndexSection.searchButton}}" stepKey="clickOnSearchButton2"/> -+ <waitForPageLoad stepKey="waitForPageToLoad5"/> -+ <see stepKey="verifyTheRedirectTypeAfterMove1" selector="{{AdminUrlRewriteIndexSection.redirectTypeColumn('1')}}" userInput="Permanent (301)" /> -+ <see stepKey="verifyTheRequestPathAfterMove1" selector="{{AdminUrlRewriteIndexSection.requestPathColumn('1')}}" userInput="{{_defaultCategory.name_lwr}}2/{{SubCategory.name_lwr}}/{{SimpleSubCategory.name_lwr}}.html" /> -+ <see stepKey="verifyTheTargetPathAfterMove1" selector="{{AdminUrlRewriteIndexSection.targetPathColumn('1')}}" userInput="{{_defaultCategory.name_lwr}}2/{{SimpleSubCategory.name_lwr}}.html" /> -+ -+ <!--Verify before move Redirect Path directs to the category page--> -+ <amOnPage url="{{_defaultCategory.name_lwr}}2/{{SubCategory.name_lwr}}/{{SimpleSubCategory.name_lwr}}.html" stepKey="openCategoryStoreFrontPage"/> -+ <waitForPageLoad stepKey="waitForCategoryPageToLoad"/> -+ <seeElement selector="{{StorefrontHeaderSection.NavigationCategoryByName(_defaultCategory.name)}}" stepKey="seeCategoryOnStoreNavigationBar"/> -+ <seeElement selector="{{StorefrontCategoryMainSection.CategoryTitle(SimpleSubCategory.name)}}" stepKey="seeCategoryInTitle"/> -+ </test> -+</tests> -diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminMoveCategoryFromParentAnchoredCategoryTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminMoveCategoryFromParentAnchoredCategoryTest.xml -new file mode 100644 -index 00000000000..d17078d794b ---- /dev/null -+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminMoveCategoryFromParentAnchoredCategoryTest.xml -@@ -0,0 +1,117 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> -+ <test name="AdminMoveCategoryFromParentAnchoredCategoryTest"> -+ <annotations> -+ <stories value="Move categories"/> -+ <title value="Move default subcategory with anchored parent to default subcategory"/> -+ <description value="Login as admin,move subcategory with anchored parent to default subcategory"/> -+ <testCaseId value="MC-6492"/> -+ <severity value="CRITICAL"/> -+ <group value="mtf_migrated"/> -+ <features value="Catalog"/> -+ </annotations> -+ <before> -+ <actionGroup ref="LoginAsAdmin" stepKey="loginToAdminPanel"/> -+ <createData entity="defaultSimpleProduct" stepKey="simpleProduct"/> -+ <createData entity="_defaultCategory" stepKey="createDefaultCategory"/> -+ </before> -+ <after> -+ <deleteData createDataKey="simpleProduct" stepKey="deleteSimpleProduct"/> -+ <deleteData createDataKey="createDefaultCategory" stepKey="deleteDefaultCategory"/> -+ <actionGroup ref="DeleteCategory" stepKey="deleteCategory"> -+ <argument name="categoryEntity" value="SimpleSubCategory"/> -+ </actionGroup> -+ <actionGroup ref="logout" stepKey="logout"/> -+ </after> -+ -+ <!--Open Category page--> -+ <amOnPage url="{{AdminCategoryPage.url}}" stepKey="openAdminCategoryIndexPage"/> -+ <waitForPageLoad stepKey="waitForPageToLoaded"/> -+ <click selector="{{AdminCategorySidebarTreeSection.expandAll}}" stepKey="clickOnExpandTree"/> -+ <waitForPageLoad stepKey="waitForCategoryToLoad"/> -+ <click selector="{{AdminCategorySidebarTreeSection.categoryInTree(_defaultCategory.name)}}" stepKey="selectCategory"/> -+ <waitForPageLoad stepKey="waitForPageToLoad"/> -+ -+ <!--Enable Anchor for _defaultCategory category --> -+ <scrollTo selector="{{CategoryDisplaySettingsSection.DisplaySettingTab}}" x="0" y="-80" stepKey="scrollToDisplaySetting"/> -+ <click selector="{{CategoryDisplaySettingsSection.DisplaySettingTab}}" stepKey="selectDisplaySetting"/> -+ <checkOption selector="{{CategoryDisplaySettingsSection.anchor}}" stepKey="enableAnchor"/> -+ <click selector="{{AdminCategoryMainActionsSection.SaveButton}}" stepKey="saveSubCategory"/> -+ -+ <!--Create a Subcategory under _defaultCategory category--> -+ <click selector="{{AdminCategorySidebarActionSection.AddSubcategoryButton}}" stepKey="clickOnAddSubCategoryButton"/> -+ <fillField selector="{{AdminCategoryBasicFieldSection.CategoryNameInput}}" userInput="{{SimpleSubCategory.name}}" stepKey="addSubCategoryName"/> -+ -+ <!--Add a product to SimpleSubCategory category--> -+ <scrollTo selector="{{AdminCategoryBasicFieldSection.productsInCategory}}" x="0" y="-80" stepKey="scrollToProductInCategory"/> -+ <click selector="{{AdminCategoryBasicFieldSection.productsInCategory}}" stepKey="clickOnProductInCategory"/> -+ <fillField selector="{{AdminCategoryContentSection.productTableColumnName}}" userInput="$$simpleProduct.name$$" stepKey="selectProduct"/> -+ <click selector="{{AdminCategoryContentSection.productSearch}}" stepKey="clickSearchButton"/> -+ <click selector="{{AdminCategoryContentSection.productTableRow}}" stepKey="selectProductFromTableRow"/> -+ <click selector="{{AdminCategoryMainActionsSection.SaveButton}}" stepKey="saveSubCategory1"/> -+ <waitForPageLoad stepKey="waitForSecondCategoryToSave"/> -+ <seeElement selector="{{AdminCategoryMessagesSection.SuccessMessage}}" stepKey="seeSuccessMessage"/> -+ -+ <!--Verify category displayed in store front page--> -+ <amOnPage url="/$$createDefaultCategory.name$$/{{SimpleSubCategory.name}}.html" stepKey="seeTheCategoryInStoreFrontPage"/> -+ <waitForPageLoad stepKey="waitForStoreFrontPageLoad"/> -+ <seeElement selector="{{StorefrontHeaderSection.NavigationCategoryByName(_defaultCategory.name)}}" stepKey="seeDefaultCategoryOnStoreNavigationBar"/> -+ <dontSeeElement selector="{{StorefrontHeaderSection.NavigationCategoryByName(SimpleSubCategory.name)}}" stepKey="dontSeeSubCategoryOnStoreNavigationBar"/> -+ -+ <!--Check category breadcrumbs in store front page--> -+ <grabMultiple selector="{{StorefrontNavigationSection.categoryBreadcrumbs}}" stepKey="breadcrumbs"/> -+ <assertEquals stepKey="verifyTheCategoryInStoreFrontPage"> -+ <expectedResult type="array">['Home', $$createDefaultCategory.name$$,{{SimpleSubCategory.name}}]</expectedResult> -+ <actualResult type="variable">breadcrumbs</actualResult> -+ </assertEquals> -+ -+ <!--Verify Product displayed in category store front page--> -+ <click selector="{{StorefrontCategoryMainSection.productLink}}" stepKey="openSearchedProduct"/> -+ <waitForPageLoad stepKey="waitForProductToLoad1"/> -+ <see selector="{{StorefrontProductInfoMainSection.productName}}" userInput="{{defaultSimpleProduct.name}}" stepKey="assertProductName"/> -+ -+ <!--Open Category Page--> -+ <amOnPage url="{{AdminCategoryPage.url}}" stepKey="openAdminCategoryIndexPage1"/> -+ <waitForPageLoad stepKey="waitForPageToLoad1"/> -+ <click selector="{{AdminCategorySidebarTreeSection.expandAll}}" stepKey="clickOnExpandTree2"/> -+ <waitForPageLoad stepKey="waitForPageToLoad2"/> -+ -+ <!--Move SubCategory under Default Category--> -+ <dragAndDrop selector1="{{AdminCategorySidebarTreeSection.categoryInTree(SimpleSubCategory.name)}}" selector2="{{AdminCategorySidebarTreeSection.categoryInTree('Default Category')}}" stepKey="moveCategory"/> -+ <see selector="{{AdminCategoryModalSection.message}}" userInput="This operation can take a long time" stepKey="seeWarningMessage"/> -+ <click selector="{{AdminCategoryModalSection.ok}}" stepKey="clickOkButtonOnWarningPopup"/> -+ <waitForPageLoad stepKey="waitForPageToLoad3"/> -+ <see selector="{{AdminCategoryMessagesSection.SuccessMessage}}" userInput="You moved the category." stepKey="seeSuccessMoveMessage"/> -+ -+ <!--Open category in store front page--> -+ <amOnPage url="/{{SimpleSubCategory.name}}.html" stepKey="seeTheCategoryInStoreFrontPage1"/> -+ <waitForPageLoad stepKey="waitForStoreFrontPageLoad1"/> -+ -+ <!--Verify breadcrumbs after the move in store front page--> -+ <grabMultiple selector="{{StorefrontNavigationSection.categoryBreadcrumbs}}" stepKey="breadcrumbsAfterMove"/> -+ <assertEquals stepKey="verifyBreadcrumbsInFrontPageAfterMove"> -+ <expectedResult type="array">['Home',{{SimpleSubCategory.name}}]</expectedResult> -+ <actualResult type="variable">breadcrumbsAfterMove</actualResult> -+ </assertEquals> -+ -+ <!--Open category store front page --> -+ <amOnPage url="{{StorefrontCategoryPage.url(SimpleSubCategory.name)}}" stepKey="amOnCategoryPage"/> -+ <waitForPageLoad stepKey="waitForPageToBeLoaded"/> -+ -+ <!--Verify Category in store front--> -+ <seeElement selector="{{StorefrontCategoryMainSection.CategoryTitle(SimpleSubCategory.name)}}" stepKey="seeCategoryInTitle"/> -+ <seeElement selector="{{StorefrontHeaderSection.NavigationCategoryByName(SimpleSubCategory.name)}}" stepKey="seeSubCategoryOnStoreNavigationBarAfterMove"/> -+ <click selector="{{StorefrontCategoryMainSection.productLink}}" stepKey="openSearchedProduct1"/> -+ <waitForPageLoad stepKey="waitForProductToLoad2"/> -+ -+ <!--Verify product name on Store Front--> -+ <see selector="{{StorefrontProductInfoMainSection.productName}}" userInput="{{defaultSimpleProduct.name}}" stepKey="assertProductNameAfterMove"/> -+ </test> -+</tests> -diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminMoveCategoryToAnotherPositionInCategoryTreeTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminMoveCategoryToAnotherPositionInCategoryTreeTest.xml -new file mode 100644 -index 00000000000..9831f73e078 ---- /dev/null -+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminMoveCategoryToAnotherPositionInCategoryTreeTest.xml -@@ -0,0 +1,116 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> -+ <test name="AdminMoveCategoryToAnotherPositionInCategoryTreeTest"> -+ <annotations> -+ <stories value="Move categories"/> -+ <title value="Move Category to Another Position in Category Tree"/> -+ <description value="Test log in to Move Category and Move Category to Another Position in Category Tree"/> -+ <testCaseId value="MC-13612"/> -+ <severity value="CRITICAL"/> -+ <group value="catalog"/> -+ <group value="mtf_migrated"/> -+ </annotations> -+ -+ <before> -+ <actionGroup ref="LoginAsAdmin" stepKey="loginToAdminPanel"/> -+ <createData entity="_defaultCategory" stepKey="createDefaultCategory"/> -+ </before> -+ <after> -+ <deleteData createDataKey="createDefaultCategory" stepKey="deleteDefaultCategory"/> -+ <actionGroup ref="DeleteCategory" stepKey="SecondLevelSubCat"> -+ <argument name="categoryEntity" value="SecondLevelSubCat"/> -+ </actionGroup> -+ <actionGroup ref="logout" stepKey="logout"/> -+ </after> -+ -+ <!-- Open Category Page --> -+ <amOnPage url="{{AdminCategoryPage.url}}" stepKey="openAdminCategoryIndexPage"/> -+ <waitForPageLoad stepKey="waitForPageToLoaded"/> -+ <click selector="{{AdminCategorySidebarTreeSection.expandAll}}" stepKey="clickExpandTree"/> -+ <waitForPageLoad stepKey="waitForCategoryToLoad"/> -+ <click selector="{{AdminCategorySidebarTreeSection.categoryInTree(_defaultCategory.name)}}" stepKey="selectCategory"/> -+ <waitForPageLoad stepKey="waitForPageToLoad"/> -+ <!-- Create three level deep sub Category --> -+ <click selector="{{AdminCategorySidebarActionSection.AddSubcategoryButton}}" stepKey="clickAddSubCategoryButton"/> -+ <fillField selector="{{AdminCategoryBasicFieldSection.CategoryNameInput}}" userInput="{{FirstLevelSubCat.name}}" stepKey="fillSubCategoryName"/> -+ <click selector="{{AdminCategoryMainActionsSection.SaveButton}}" stepKey="saveFirstLevelSubCategory"/> -+ <waitForPageLoad stepKey="waitForFirstLevelCategoryToSave"/> -+ <seeElement selector="{{AdminCategoryMessagesSection.SuccessMessage}}" stepKey="seeSuccessMessage"/> -+ <click selector="{{AdminCategorySidebarActionSection.AddSubcategoryButton}}" stepKey="clickOnAddSubCategoryButtonAgain"/> -+ <fillField selector="{{AdminCategoryBasicFieldSection.CategoryNameInput}}" userInput="{{SecondLevelSubCat.name}}" stepKey="fillSecondLevelSubCategoryName"/> -+ <click selector="{{AdminCategoryMainActionsSection.SaveButton}}" stepKey="saveSecondLevelSubCategory"/> -+ <waitForPageLoad stepKey="waitForSecondLevelCategoryToSave"/> -+ <seeElement selector="{{AdminCategoryMessagesSection.SuccessMessage}}" stepKey="seeSaveSuccessMessage"/> -+ <grabFromCurrentUrl stepKey="categoryId" regex="#\/([0-9]*)?\/$#" /> -+ -+ <!-- Move Category to another position in category tree, but click cancel button --> -+ <dragAndDrop selector1="{{AdminCategorySidebarTreeSection.categoryInTree(SecondLevelSubCat.name)}}" selector2="{{AdminCategorySidebarTreeSection.categoryInTree('Default Category')}}" stepKey="moveCategory"/> -+ <see selector="{{AdminCategoryModalSection.message}}" userInput="This operation can take a long time" stepKey="seeWarningMessage"/> -+ <click selector="{{AdminCategoryModalSection.cancel}}" stepKey="clickCancelButtonOnWarningPopup"/> -+ <!-- Verify Category in store front page after clicking cancel button --> -+ <amOnPage url="/$$createDefaultCategory.name$$/{{FirstLevelSubCat.name}}/{{SecondLevelSubCat.name}}.html" stepKey="seeTheCategoryInStoreFrontPage"/> -+ <waitForPageLoad stepKey="waitForStoreFrontPageLoad"/> -+ <seeElement selector="{{StorefrontHeaderSection.NavigationCategoryByName(_defaultCategory.name)}}" stepKey="seeDefaultCategoryOnStoreNavigationBar"/> -+ <dontSeeElement selector="{{StorefrontHeaderSection.NavigationCategoryByName(SimpleSubCategory.name)}}" stepKey="dontSeeSubCategoryOnStoreNavigationBar"/> -+ <!-- Verify breadcrumbs in store front page after clicking cancel button --> -+ <grabMultiple selector="{{StorefrontNavigationSection.categoryBreadcrumbs}}" stepKey="breadcrumbs"/> -+ <assertEquals stepKey="verifyTheCategoryInStoreFrontPage"> -+ <expectedResult type="array">['Home', $$createDefaultCategory.name$$,{{FirstLevelSubCat.name}},{{SecondLevelSubCat.name}}]</expectedResult> -+ <actualResult type="variable">breadcrumbs</actualResult> -+ </assertEquals> -+ -+ <!-- Move Category to another position in category tree and click ok button--> -+ <amOnPage url="{{AdminCategoryPage.url}}" stepKey="openTheAdminCategoryIndexPage"/> -+ <waitForPageLoad stepKey="waitTillPageLoad"/> -+ <click selector="{{AdminCategorySidebarTreeSection.expandAll}}" stepKey="clickOnExpandTree"/> -+ <waitForPageLoad stepKey="waitForPageLoad"/> -+ <dragAndDrop selector1="{{AdminCategorySidebarTreeSection.categoryInTree(SecondLevelSubCat.name)}}" selector2="{{AdminCategorySidebarTreeSection.categoryInTree('Default Category')}}" stepKey="DragCategory"/> -+ <see selector="{{AdminCategoryModalSection.message}}" userInput="This operation can take a long time" stepKey="seeWarningMessageForOneMoreTime"/> -+ <click selector="{{AdminCategoryModalSection.ok}}" stepKey="clickOkButtonOnWarningPopup"/> -+ <waitForPageLoad stepKey="waitTheForPageToLoad"/> -+ <see selector="{{AdminCategoryMessagesSection.SuccessMessage}}" userInput="You moved the category." stepKey="seeSuccessMoveMessage"/> -+ <amOnPage url="/{{SimpleSubCategory.name}}.html" stepKey="seeCategoryNameInStoreFrontPage"/> -+ <waitForPageLoad stepKey="waitForStoreFrontPageToLoad"/> -+ <!-- Verify Category in store front after moving category to another position in category tree --> -+ <amOnPage url="{{StorefrontCategoryPage.url(SecondLevelSubCat.name)}}" stepKey="amOnCategoryPage"/> -+ <waitForPageLoad stepKey="waitForPageToBeLoaded"/> -+ <seeElement selector="{{StorefrontCategoryMainSection.CategoryTitle(SecondLevelSubCat.name)}}" stepKey="seeCategoryInTitle"/> -+ <seeElement selector="{{StorefrontHeaderSection.NavigationCategoryByName(SecondLevelSubCat.name)}}" stepKey="seeCategoryOnStoreNavigationBarAfterMove"/> -+ <!-- Verify breadcrumbs in store front page after moving category to another position in category tree --> -+ <click selector="{{StorefrontHeaderSection.NavigationCategoryByName(SecondLevelSubCat.name)}}" stepKey="clickCategoryOnNavigation"/> -+ <waitForPageLoad stepKey="waitForCategoryLoad"/> -+ <grabMultiple selector="{{StorefrontNavigationSection.categoryBreadcrumbs}}" stepKey="breadcrumbsAfterMove"/> -+ <assertEquals stepKey="verifyBreadcrumbsInFrontPageAfterMove"> -+ <expectedResult type="array">['Home',{{SecondLevelSubCat.name}}]</expectedResult> -+ <actualResult type="variable">breadcrumbsAfterMove</actualResult> -+ </assertEquals> -+ -+ <!-- Open Url Rewrite page and see the url rewrite for the moved category --> -+ <amOnPage url="{{AdminUrlRewriteIndexPage.url}}" stepKey="openUrlRewriteIndexPage"/> -+ <waitForPageLoad stepKey="waitForUrlRewritePageLoad"/> -+ <fillField selector="{{AdminUrlRewriteIndexSection.requestPathFilter}}" userInput="{{SecondLevelSubCat.name_lwr}}.html" stepKey="fillCategoryUrlKey"/> -+ <click selector="{{AdminUrlRewriteIndexSection.searchButton}}" stepKey="clickOnSearchButton"/> -+ <waitForPageLoad stepKey="waitForUrlPageToLoad"/> -+ <!-- Verify new Redirect Path after move --> -+ <see selector="{{AdminUrlRewriteIndexSection.requestPathColumn('2')}}" userInput="{{SecondLevelSubCat.name_lwr}}.html" stepKey="verifyTheRequestPathAfterMove"/> -+ <!-- Verify new Target Path after move --> -+ <see selector="{{AdminUrlRewriteIndexSection.targetPathColumn('2')}}" userInput="catalog/category/view/id/{$categoryId}" stepKey="verifyTheTargetPathAfterMove"/> -+ <!-- Verify new RedirectType after move --> -+ <see selector="{{AdminUrlRewriteIndexSection.redirectTypeColumn('2')}}" userInput="No" stepKey="verifyTheRedirectTypeAfterMove"/> -+ <!-- Verify before move Redirect Path displayed with associated Target Path and Redirect Type--> -+ <fillField selector="{{AdminUrlRewriteIndexSection.requestPathFilter}}" userInput="{{SecondLevelSubCat.name_lwr}}" stepKey="fillTheCategoryUrlKey"/> -+ <click selector="{{AdminUrlRewriteIndexSection.searchButton}}" stepKey="clickOnSearchButton2"/> -+ <waitForPageLoad stepKey="waitForSearch"/> -+ <see selector="{{AdminUrlRewriteIndexSection.redirectTypeColumn('1')}}" userInput="Permanent (301)" stepKey="verifyTheRedirectTypeBeforeMove"/> -+ <see selector="{{AdminUrlRewriteIndexSection.requestPathColumn('1')}}" userInput="{{_defaultCategory.name_lwr}}2/{{FirstLevelSubCat.name_lwr}}/{{SecondLevelSubCat.name_lwr}}.html" stepKey="verifyTheRequestPathBeforeMove"/> -+ <see selector="{{AdminUrlRewriteIndexSection.targetPathColumn('1')}}" userInput="{{SecondLevelSubCat.name_lwr}}.html" stepKey="verifyTheTargetPathBeforeMove"/> -+ </test> -+</tests> -diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminMoveProductBetweenCategoriesTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminMoveProductBetweenCategoriesTest.xml -new file mode 100644 -index 00000000000..da985fc2ce3 ---- /dev/null -+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminMoveProductBetweenCategoriesTest.xml -@@ -0,0 +1,227 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> -+ <test name="AdminMoveProductBetweenCategoriesTest"> -+ <annotations> -+ <stories value="Move Product"/> -+ <title value="Move Product between Categories (Cron is ON, 'Update by Schedule' Mode)"/> -+ <description value="Verifies correctness of showing data (products, categories) on Storefront after moving an anchored category in terms of products/categories association"/> -+ <severity value="CRITICAL"/> -+ <testCaseId value="MC-11296"/> -+ <group value="catalog"/> -+ </annotations> -+ -+ <before> -+ <actionGroup ref="LoginAsAdmin" stepKey="LoginAsAdmin"/> -+ <createData entity="defaultSimpleProduct" stepKey="simpleProduct"/> -+ <createData entity="_defaultCategory" stepKey="createAnchoredCategory1"/> -+ <createData entity="_defaultCategory" stepKey="createSecondCategory"/> -+ -+ <!-- Switch "Category Product" and "Product Category" indexers to "Update by Schedule" mode --> -+ <amOnPage url="{{AdminIndexManagementPage.url}}" stepKey="onIndexManagement"/> -+ <waitForPageLoad stepKey="waitForManagementPage"/> -+ -+ <actionGroup ref="AdminSwitchIndexerToActionModeActionGroup" stepKey="switchCategoryProduct"> -+ <argument name="indexerValue" value="catalog_category_product"/> -+ </actionGroup> -+ <actionGroup ref="AdminSwitchIndexerToActionModeActionGroup" stepKey="switchProductCategory"> -+ <argument name="indexerValue" value="catalog_product_category"/> -+ </actionGroup> -+ </before> -+ -+ <after> -+ <!-- Switch "Category Product" and "Product Category" indexers to "Update by Save" mode --> -+ <amOnPage url="{{AdminIndexManagementPage.url}}" stepKey="onIndexManagement"/> -+ <waitForPageLoad stepKey="waitForManagementPage"/> -+ -+ <actionGroup ref="AdminSwitchIndexerToActionModeActionGroup" stepKey="switchCategoryProduct"> -+ <argument name="indexerValue" value="catalog_category_product"/> -+ <argument name="action" value="Update on Save"/> -+ </actionGroup> -+ <actionGroup ref="AdminSwitchIndexerToActionModeActionGroup" stepKey="switchProductCategory"> -+ <argument name="indexerValue" value="catalog_product_category"/> -+ <argument name="action" value="Update on Save"/> -+ </actionGroup> -+ -+ <deleteData createDataKey="simpleProduct" stepKey="deleteProduct"/> -+ <deleteData createDataKey="createSecondCategory" stepKey="deleteSecondCategory"/> -+ <deleteData createDataKey="createAnchoredCategory1" stepKey="deleteAnchoredCategory1"/> -+ <actionGroup ref="logout" stepKey="logout"/> -+ </after> -+ <!-- Create the anchored category <Cat1_anchored> --> -+ <actionGroup ref="AdminAnchorCategoryActionGroup" stepKey="anchorCategory"> -+ <argument name="categoryName" value="$$createAnchoredCategory1.name$$"/> -+ </actionGroup> -+ -+ <!-- Create subcategory <Sub1> of the anchored category --> -+ <click selector="{{AdminCategorySidebarActionSection.AddSubcategoryButton}}" stepKey="clickOnAddSubCategoryButton"/> -+ <fillField selector="{{AdminCategoryBasicFieldSection.CategoryNameInput}}" userInput="{{SimpleSubCategory.name}}" stepKey="addSubCategoryName"/> -+ <click selector="{{AdminCategoryMainActionsSection.SaveButton}}" stepKey="saveSubCategory1"/> -+ <waitForPageLoad stepKey="waitForSecondCategoryToSave"/> -+ <seeElement selector="{{AdminCategoryMessagesSection.SuccessMessage}}" stepKey="seeSaveSuccessMessage"/> -+ -+ <!-- Assign <product1> to the <Sub1> --> -+ <amOnPage url="{{AdminProductEditPage.url($$simpleProduct.id$$)}}" stepKey="goToProduct"/> -+ <waitForPageLoad stepKey="waitForProductPageLoad"/> -+ <click selector="{{AdminProductFormSection.categoriesDropdown}}" stepKey="activateDropDownCategory"/> -+ <fillField userInput="{{SimpleSubCategory.name}}" selector="{{AdminProductFormSection.searchCategory}}" stepKey="fillSearch"/> -+ <waitForPageLoad stepKey="waitForSubCategory"/> -+ <click selector="{{AdminProductFormSection.selectCategory(SimpleSubCategory.name)}}" stepKey="selectSub1Category"/> -+ <click selector="{{AdminProductFormSection.done}}" stepKey="clickDone"/> -+ <waitForPageLoad stepKey="waitForApplyCategory"/> -+ <click selector="{{AdminProductFormSection.save}}" stepKey="clickSave"/> -+ <waitForPageLoad stepKey="waitForSavingChanges"/> -+ -+ <!-- Enable `Use Categories Path for Product URLs` on Stores -> Configuration -> Catalog -> Catalog -> Search Engine Optimization --> -+ <amOnPage url="{{AdminCatalogSearchConfigurationPage.url}}" stepKey="onConfigPage"/> -+ <waitForPageLoad stepKey="waitForLoading"/> -+ <conditionalClick selector="{{AdminCatalogSearchEngineConfigurationSection.searchEngineOptimization}}" dependentSelector="{{AdminCatalogSearchEngineConfigurationSection.openedEngineOptimization}}" visible="false" stepKey="clickEngineOptimization"/> -+ <uncheckOption selector="{{AdminCatalogSearchEngineConfigurationSection.systemValueUseCategoriesPath}}" stepKey="uncheckDefault"/> -+ <selectOption userInput="Yes" selector="{{AdminCatalogSearchEngineConfigurationSection.selectUseCategoriesPatForProductUrls}}" stepKey="selectYes"/> -+ <click selector="{{AdminConfigSection.saveButton}}" stepKey="saveConfig"/> -+ <waitForPageLoad stepKey="waitForSaving"/> -+ <see selector="{{AdminIndexManagementSection.successMessage}}" userInput="You saved the configuration." stepKey="seeMessage"/> -+ -+ <!-- Navigate to the Catalog > Products --> -+ <amOnPage url="{{AdminCatalogProductPage.url}}" stepKey="onCatalogProductPage"/> -+ <waitForPageLoad stepKey="waitForProductPage"/> -+ -+ <!-- Click on <product1>: Product page opens--> -+ <actionGroup ref="filterProductGridByName" stepKey="filterProduct"> -+ <argument name="product" value="$$simpleProduct$$"/> -+ </actionGroup> -+ <click selector="{{AdminProductGridSection.productGridNameProduct($$simpleProduct.name$$)}}" stepKey="clickProduct1"/> -+ <waitForPageLoad stepKey="waitForProductLoad"/> -+ -+ <!-- Clear "Categories" field and assign the product to <Cat2> and save the product --> -+ <grabTextFrom selector="{{AdminProductFormSection.currentCategory}}" stepKey="grabNameSubCategory"/> -+ <click selector="{{AdminProductFormSection.unselectCategories(SimpleSubCategory.name)}}" stepKey="removeCategory"/> -+ <click selector="{{AdminProductFormSection.categoriesDropdown}}" stepKey="openDropDown"/> -+ <checkOption selector="{{AdminProductFormSection.selectCategory($$createSecondCategory.name$$)}}" stepKey="selectCategory"/> -+ <click selector="{{AdminProductFormSection.done}}" stepKey="pressButtonDone"/> -+ <waitForPageLoad stepKey="waitForApplyCategory2"/> -+ <click selector="{{AdminProductFormSection.save}}" stepKey="pushButtonSave"/> -+ <waitForPageLoad stepKey="waitForSavingProduct"/> -+ -+ <!--Product is saved --> -+ <see userInput="You saved the product." selector="{{CatalogProductsSection.messageSuccessSavedProduct}}" stepKey="seeSuccessMessage"/> -+ -+ <!-- Run cron --> -+ <magentoCLI command="cron:run" stepKey="runCron"/> -+ -+ <!-- Clear invalidated cache on System>Tools>Cache Management page --> -+ <amOnPage url="{{AdminCacheManagementPage.url}}" stepKey="onCachePage"/> -+ <waitForPageLoad stepKey="waitForCacheManagementPage"/> -+ -+ <checkOption selector="{{AdminCacheManagementSection.configurationCheckbox}}" stepKey="checkConfigCache"/> -+ <checkOption selector="{{AdminCacheManagementSection.pageCacheCheckbox}}" stepKey="checkPageCache"/> -+ -+ <selectOption userInput="Refresh" selector="{{AdminCacheManagementSection.massActionSelect}}" stepKey="selectRefresh"/> -+ <waitForElementVisible selector="{{AdminCacheManagementSection.massActionSubmit}}" stepKey="waitSubmitButton"/> -+ <click selector="{{AdminCacheManagementSection.massActionSubmit}}" stepKey="clickSubmit"/> -+ <waitForPageLoad stepKey="waitForRefresh"/> -+ -+ <see userInput="2 cache type(s) refreshed." stepKey="seeCacheRefreshedMessage"/> -+ <actionGroup ref="logout" stepKey="logout"/> -+ -+ <!-- Open frontend --> -+ <amOnPage url="{{StorefrontHomePage.url}}" stepKey="onFrontend"/> -+ <waitForPageLoad stepKey="waitForStorefrontPageLoad"/> -+ -+ <!-- Open <Cat2> from navigation menu --> -+ <click selector="{{StorefrontHeaderSection.NavigationCategoryByName($$createSecondCategory.name$$)}}" stepKey="openCat2"/> -+ <waitForPageLoad stepKey="waitForCategory2Page"/> -+ -+ <!-- # <Cat 2> should open # <product1> should be present on the page --> -+ <see userInput="$$createSecondCategory.name$$" selector="{{StorefrontCategoryMainSection.CategoryTitle}}" stepKey="seeCategoryName"/> -+ <see userInput="$$simpleProduct.name$$" selector="{{StorefrontCategoryMainSection.productName}}" stepKey="seeProduct"/> -+ -+ <!-- Open <product1> --> -+ <click selector="{{StorefrontCategoryMainSection.productLinkByHref($$simpleProduct.urlKey$$)}}" stepKey="openProduct"/> -+ <waitForPageLoad stepKey="waitForProductPageLoading"/> -+ -+ <!-- # Product page should open successfully # Breadcrumb for product should be like <Cat 2> --> -+ <see selector="{{StorefrontProductInfoMainSection.productName}}" userInput="$$simpleProduct.name$$" stepKey="seeProductName"/> -+ <see userInput="$$createSecondCategory.name$$" selector="{{StorefrontNavigationSection.categoryBreadcrumbs}}" stepKey="seeCategoryInBreadcrumbs"/> -+ -+ <!-- Open <Cat1_anchored> category --> -+ <click selector="{{StorefrontNavigationSection.topCategory($$createAnchoredCategory1.name$$)}}" stepKey="clickCat1"/> -+ <waitForPageLoad stepKey="waitForCategory1PageLoad"/> -+ -+ <!-- # Category should open successfully # <product1> should be absent on the page --> -+ <see userInput="$$createAnchoredCategory1.name$$" selector="{{StorefrontCategoryMainSection.CategoryTitle}}" stepKey="seeCategory1Name"/> -+ <see userInput="We can't find products matching the selection." stepKey="seeEmptyNotice"/> -+ <dontSee userInput="$$simpleProduct.name$$" selector="{{StorefrontCategoryMainSection.productName}}" stepKey="dontseeProduct"/> -+ -+ <!-- Log in to the backend: Admin user is logged in--> -+ <actionGroup ref="LoginAsAdmin" stepKey="LoginAdmin"/> -+ -+ <!-- Navigate to the Catalog > Products: Navigate to the Catalog>Products --> -+ <amOnPage url="{{AdminCatalogProductPage.url}}" stepKey="amOnProductPage"/> -+ <waitForPageLoad stepKey="waitForProductsPage"/> -+ -+ <!-- Click on <product1> --> -+ <actionGroup ref="filterAndSelectProduct" stepKey="openSimpleProduct"> -+ <argument name="productSku" value="$$simpleProduct.sku$$"/> -+ </actionGroup> -+ -+ <!-- Clear "Categories" field and assign the product to <Sub1> and save the product --> -+ <click selector="{{AdminProductFormSection.unselectCategories($$createSecondCategory.name$$)}}" stepKey="clearCategory"/> -+ <click selector="{{AdminProductFormSection.categoriesDropdown}}" stepKey="activateDropDown"/> -+ <fillField userInput="{$grabNameSubCategory}" selector="{{AdminProductFormSection.searchCategory}}" stepKey="fillSearchField"/> -+ <waitForPageLoad stepKey="waitForSearchSubCategory"/> -+ <click selector="{{AdminProductFormSection.selectCategory({$grabNameSubCategory})}}" stepKey="selectSubCategory"/> -+ <click selector="{{AdminProductFormSection.done}}" stepKey="clickButtonDone"/> -+ <waitForPageLoad stepKey="waitForCategoryApply"/> -+ <click selector="{{AdminProductFormSection.save}}" stepKey="clickButtonSave"/> -+ <waitForPageLoad stepKey="waitForSaveChanges"/> -+ -+ <!-- Product is saved successfully --> -+ <see userInput="You saved the product." selector="{{CatalogProductsSection.messageSuccessSavedProduct}}" stepKey="seeSaveMessage"/> -+ -+ <!-- Run cron --> -+ <magentoCLI command="cron:run" stepKey="runCron2"/> -+ -+ <!-- Open frontend --> -+ <amOnPage url="{{StorefrontHomePage.url}}" stepKey="onFrontendPage"/> -+ <waitForPageLoad stepKey="waitForFrontPageLoad"/> -+ -+ <!-- Open <Cat2> from navigation menu --> -+ <click selector="{{StorefrontHeaderSection.NavigationCategoryByName($$createSecondCategory.name$$)}}" stepKey="openSecondCategory"/> -+ <waitForPageLoad stepKey="waitForSecondCategoryPage"/> -+ -+ <!-- # <Cat 2> should open # <product1> should be absent on the page --> -+ <see userInput="$$createSecondCategory.name$$" selector="{{StorefrontCategoryMainSection.CategoryTitle}}" stepKey="seeSecondCategory1Name"/> -+ <dontSee userInput="$$simpleProduct.name$$" selector="{{StorefrontCategoryMainSection.productName}}" stepKey="dontseeSimpleProduct"/> -+ -+ <!-- Click on <Cat1_anchored> category --> -+ <click selector="{{StorefrontHeaderSection.NavigationCategoryByName($$createAnchoredCategory1.name$$)}}" stepKey="clickAnchoredCategory"/> -+ <waitForPageLoad stepKey="waitForAnchoredCategoryPage"/> -+ -+ <!-- # Category should open successfully # <product1> should be present on the page --> -+ <see userInput="$$createAnchoredCategory1.name$$" selector="{{StorefrontCategoryMainSection.CategoryTitle}}" stepKey="see1CategoryName"/> -+ <see selector="{{StorefrontCategoryMainSection.productName}}" userInput="$$simpleProduct.name$$" stepKey="seeProductNameOnCategory1Page"/> -+ -+ <!-- Breadcrumb for product should be like <Cat1_anchored>/<product> (if you clicks from anchor category) --> -+ <see userInput="$$createAnchoredCategory1.name$$" selector="{{StorefrontNavigationSection.categoryBreadcrumbs}}" stepKey="seeCat1inBreadcrumbs"/> -+ <dontSee userInput="{$grabNameSubCategory}" selector="{{StorefrontNavigationSection.categoryBreadcrumbs}}" stepKey="dontSeeSubCategoryInBreadCrumbs"/> -+ -+ <!-- <Cat1_anchored>/<Sub1>/<product> (if you clicks from Sub1 category) --> -+ <moveMouseOver selector="{{StorefrontHeaderSection.NavigationCategoryByName($$createAnchoredCategory1.name$$)}}" stepKey="hoverCategory1"/> -+ <click selector="{{StorefrontHeaderSection.NavigationCategoryByName({$grabNameSubCategory})}}" stepKey="clickSubCat"/> -+ <waitForPageLoad stepKey="waitForSubCategoryPageLoad"/> -+ -+ <see userInput="{$grabNameSubCategory}" selector="{{StorefrontCategoryMainSection.CategoryTitle}}" stepKey="seeSubCategoryName"/> -+ <see selector="{{StorefrontCategoryMainSection.productName}}" userInput="$$simpleProduct.name$$" stepKey="seeProductNameOnSubCategoryPage"/> -+ -+ <see userInput="{$grabNameSubCategory}" selector="{{StorefrontNavigationSection.categoryBreadcrumbs}}" stepKey="seeSubCategoryInBreadcrumbs"/> -+ <see userInput="$$createAnchoredCategory1.name$$" selector="{{StorefrontNavigationSection.categoryBreadcrumbs}}" stepKey="seeCat1InBreadcrumbs"/> -+ </test> -+</tests> -diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminMultipleWebsitesUseDefaultValuesTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminMultipleWebsitesUseDefaultValuesTest.xml -index 012c956c2db..264615ff673 100644 ---- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminMultipleWebsitesUseDefaultValuesTest.xml -+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminMultipleWebsitesUseDefaultValuesTest.xml -@@ -7,10 +7,11 @@ - --> - - <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="AdminMultipleWebsitesUseDefaultValuesTest"> - <annotations> - <features value="Catalog"/> -+ <stories value="Create websites"/> - <title value="Use Default Value checkboxes should be checked for new website scope"/> - <description value="Use Default Value checkboxes for product attribute should be checked for new website scope"/> - <severity value="MAJOR"/> -diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminNavigateMultipleUpSellProductsTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminNavigateMultipleUpSellProductsTest.xml -new file mode 100644 -index 00000000000..bcd4ca85312 ---- /dev/null -+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminNavigateMultipleUpSellProductsTest.xml -@@ -0,0 +1,183 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> -+ <test name="AdminNavigateMultipleUpSellProductsTest"> -+ <annotations> -+ <stories value="Up Sell products"/> -+ <title value="Promote Multiple Products (Simple, Configurable) as Up-Sell Products"/> -+ <description value="Login as admin and add simple and configurable Products as Up-Sell products"/> -+ <testCaseId value="MC-8902"/> -+ <severity value="CRITICAL"/> -+ <group value="mtf_migrated"/> -+ </annotations> -+ -+ <before> -+ <!--Create Simple Products--> -+ <createData entity="SimpleSubCategory" stepKey="createCategory1"/> -+ <createData entity="SimpleProduct" stepKey="createSimpleProduct"> -+ <requiredEntity createDataKey="createCategory1"/> -+ </createData> -+ <createData entity="SimpleSubCategory" stepKey="createCategory2"/> -+ <createData entity="SimpleProduct" stepKey="createSimpleProduct1"> -+ <requiredEntity createDataKey="createCategory2"/> -+ </createData> -+ -+ <!-- Create the configurable product with product Attribute options--> -+ <createData entity="ApiCategory" stepKey="createCategory"/> -+ <createData entity="ApiConfigurableProduct" stepKey="createConfigProduct"> -+ <requiredEntity createDataKey="createCategory"/> -+ </createData> -+ <createData entity="productAttributeWithTwoOptions" stepKey="createConfigProductAttribute"/> -+ <createData entity="productAttributeOption1" stepKey="createConfigProductAttributeOption1"> -+ <requiredEntity createDataKey="createConfigProductAttribute"/> -+ </createData> -+ <createData entity="productAttributeOption2" stepKey="createConfigProductAttributeOption2"> -+ <requiredEntity createDataKey="createConfigProductAttribute"/> -+ </createData> -+ <createData entity="AddToDefaultSet" stepKey="delete"> -+ <requiredEntity createDataKey="createConfigProductAttribute"/> -+ </createData> -+ <getData entity="ProductAttributeOptionGetter" index="1" stepKey="getConfigAttributeOption1"> -+ <requiredEntity createDataKey="createConfigProductAttribute"/> -+ </getData> -+ <getData entity="ProductAttributeOptionGetter" index="2" stepKey="getConfigAttributeOption2"> -+ <requiredEntity createDataKey="createConfigProductAttribute"/> -+ </getData> -+ <createData entity="ApiSimpleOne" stepKey="createConfigChildProduct1"> -+ <requiredEntity createDataKey="createConfigProductAttribute"/> -+ <requiredEntity createDataKey="getConfigAttributeOption1"/> -+ </createData> -+ <createData entity="ApiSimpleTwo" stepKey="createConfigChildProduct2"> -+ <requiredEntity createDataKey="createConfigProductAttribute"/> -+ <requiredEntity createDataKey="getConfigAttributeOption2"/> -+ </createData> -+ <createData entity="ConfigurableProductTwoOptions" stepKey="createConfigProductOption"> -+ <requiredEntity createDataKey="createConfigProduct"/> -+ <requiredEntity createDataKey="createConfigProductAttribute"/> -+ <requiredEntity createDataKey="getConfigAttributeOption1"/> -+ <requiredEntity createDataKey="getConfigAttributeOption2"/> -+ </createData> -+ <createData entity="ConfigurableProductAddChild" stepKey="createConfigProductAddChild1"> -+ <requiredEntity createDataKey="createConfigProduct"/> -+ <requiredEntity createDataKey="createConfigChildProduct1"/> -+ </createData> -+ <createData entity="ConfigurableProductAddChild" stepKey="createConfigProductAddChild2"> -+ <requiredEntity createDataKey="createConfigProduct"/> -+ <requiredEntity createDataKey="createConfigChildProduct2"/> -+ </createData> -+ -+ <!--Login as admin--> -+ <actionGroup ref="LoginAsAdmin" stepKey="login"/> -+ </before> -+ <after> -+ <!--Logout as admin--> -+ <actionGroup ref="logout" stepKey="logout"/> -+ -+ <!--Delete created data--> -+ <deleteData createDataKey="createConfigProduct" stepKey="deleteConfigProduct"/> -+ <deleteData createDataKey="createSimpleProduct1" stepKey="deleteSimpleProduct1"/> -+ <deleteData createDataKey="createSimpleProduct" stepKey="deleteSimpleProduct"/> -+ <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> -+ <deleteData createDataKey="createCategory1" stepKey="deleteSubCategory1"/> -+ <deleteData createDataKey="createCategory2" stepKey="deleteCategory2"/> -+ <deleteData createDataKey="createConfigChildProduct2" stepKey="deletecreateConfigChildProduct2"/> -+ <deleteData createDataKey="createConfigChildProduct1" stepKey="deletecreateConfigChildProduct1"/> -+ <deleteData createDataKey="createConfigProductAttribute" stepKey="deleteConfigProductAttribute"/> -+ </after> -+ -+ <!--Open Product Index Page--> -+ <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="navigateToProductIndex"/> -+ <waitForPageLoad stepKey="waitForProductIndexPageToLoad"/> -+ -+ <!--Select SimpleProduct --> -+ <actionGroup ref="filterProductGridBySku" stepKey="findCreatedProduct"> -+ <argument name="product" value="$$createSimpleProduct$$"/> -+ </actionGroup> -+ <click stepKey="openFirstProduct" selector="{{AdminProductGridSection.productRowBySku($$createSimpleProduct.sku$$)}}"/> -+ <waitForPageLoad stepKey="waitForProductToLoad"/> -+ -+ <!--Add SimpleProduct1 and ConfigProduct as Up sell products--> -+ <click stepKey="clickOnRelatedProducts" selector="{{AdminProductFormRelatedUpSellCrossSellSection.relatedProductsHeader}}"/> -+ <click stepKey="clickOnAddUpSellProducts" selector="{{AdminProductFormRelatedUpSellCrossSellSection.addUpSellProduct}}"/> -+ <actionGroup ref="filterProductGridBySku2" stepKey="filterProduct"> -+ <argument name="sku" value="$$createSimpleProduct1.sku$$"/> -+ </actionGroup> -+ <waitForPageLoad stepKey="waitForTheProductToLoad"/> -+ <checkOption selector="{{AdminAddProductsToGroupPanel.firstCheckbox}}" stepKey="selectTheSimpleProduct2"/> -+ <click stepKey="addSelectedProduct" selector="{{AdminAddRelatedProductsModalSection.AddUpSellProductsButton}}"/> -+ <waitForPageLoad stepKey="waitForProductToBeAdded"/> -+ <click stepKey="clickOnAddUpSellProductsButton" selector="{{AdminProductFormRelatedUpSellCrossSellSection.addUpSellProduct}}"/> -+ <actionGroup ref="filterProductGridBySku2" stepKey="filterConfigurableProduct"> -+ <argument name="sku" value="$$createConfigProduct.sku$$"/> -+ </actionGroup> -+ <waitForPageLoad stepKey="waitForTheConfigProductToLoad"/> -+ <checkOption selector="{{AdminAddProductsToGroupPanel.firstCheckbox}}" stepKey="selectTheConfigProduct"/> -+ <click stepKey="addSelectedProductButton" selector="{{AdminAddRelatedProductsModalSection.AddUpSellProductsButton}}"/> -+ <waitForPageLoad stepKey="waitForConfigProductToBeAdded"/> -+ <click stepKey="clickOnRelatedProducts1" selector="{{AdminProductFormRelatedUpSellCrossSellSection.relatedProductsHeader}}"/> -+ <click stepKey="clickOnSaveButton" selector="{{AdminProductFormActionSection.saveButton}}"/> -+ <waitForPageLoad stepKey="waitForLoading1"/> -+ <see selector="{{AdminCategoryMessagesSection.SuccessMessage}}" userInput="You saved the product." stepKey="messageYouSavedTheProductIsShown"/> -+ -+ <!--Go to Product Index Page --> -+ <click stepKey="clickOnBackButton" selector="{{AdminGridMainControls.back}}"/> -+ <waitForPageLoad stepKey="waitForProductsToBeLoaded"/> -+ -+ <!--Select Configurable Product--> -+ <actionGroup ref="filterProductGridBySku" stepKey="findConfigProduct"> -+ <argument name="product" value="$$createConfigProduct$$"/> -+ </actionGroup> -+ <click stepKey="openConfigProduct" selector="{{AdminProductGridSection.productRowBySku($$createConfigProduct.sku$$)}}"/> -+ <waitForPageLoad stepKey="waitForConfigProductToLoad"/> -+ -+ <!--Add SimpleProduct1 as Up Sell Product--> -+ <click stepKey="clickOnRelatedProductsHeader" selector="{{AdminProductFormRelatedUpSellCrossSellSection.relatedProductsHeader}}"/> -+ <click stepKey="clickOnAddUpSellProductsButton1" selector="{{AdminProductFormRelatedUpSellCrossSellSection.addUpSellProduct}}"/> -+ <actionGroup ref="filterProductGridBySku2" stepKey="filterSimpleProduct2"> -+ <argument name="sku" value="$$createSimpleProduct1.sku$$"/> -+ </actionGroup> -+ <waitForPageLoad stepKey="waitForTheSimpleProduct2ToBeLoaded"/> -+ <checkOption selector="{{AdminAddProductsToGroupPanel.firstCheckbox}}" stepKey="selectSimpleProduct1"/> -+ <click stepKey="addSimpleProduct2" selector="{{AdminAddRelatedProductsModalSection.AddUpSellProductsButton}}"/> -+ <waitForPageLoad stepKey="waitForSimpleProductToBeAdded"/> -+ <scrollTo selector="{{AdminProductFormActionSection.saveButton}}" stepKey="scrollToTheSaveButton"/> -+ <click stepKey="clickOnSaveButton1" selector="{{AdminProductFormActionSection.saveButton}}"/> -+ <waitForPageLoad stepKey="waitForLoading2"/> -+ <see selector="{{AdminCategoryMessagesSection.SuccessMessage}}" userInput="You saved the product." stepKey="messageYouSavedTheProductIsShown1"/> -+ <waitForPageLoad stepKey="waitForUpdatesTobeSaved1"/> -+ -+ <!--Go to SimpleProduct store front page--> -+ <amOnPage url="$$createSimpleProduct.sku$$.html" stepKey="goToSimpleProductFrontPage"/> -+ <waitForPageLoad stepKey="waitForProduct"/> -+ <see stepKey="seeProductName" userInput="$$createSimpleProduct.sku$$" selector="{{StorefrontProductInfoMainSection.productName}}"/> -+ <scrollTo stepKey="scrollToTheUpSellHeading" selector="{{StorefrontProductUpSellProductsSection.upSellHeading}}"/> -+ -+ <!--Verify Up Sell Products displayed in SimpleProduct page--> -+ <see stepKey="seeTheUpSellHeading" selector="{{StorefrontProductUpSellProductsSection.upSellHeading}}" userInput="We found other products you might like!"/> -+ <see stepKey="seeSimpleProduct1" selector="{{StorefrontProductUpSellProductsSection.upSellProducts}}" userInput="$$createSimpleProduct1.name$$"/> -+ <see stepKey="seeConfigProduct" selector="{{StorefrontProductUpSellProductsSection.upSellProducts}}" userInput="$$createConfigProduct.name$$"/> -+ -+ <!--Go to Config Product store front page--> -+ <amOnPage url="$$createConfigProduct.sku$$.html" stepKey="goToConfigProductFrontPage"/> -+ <waitForPageLoad stepKey="waitForConfigProductToBeLoaded"/> -+ <scrollTo stepKey="scrollToTheUpSellHeading1" selector="{{StorefrontProductUpSellProductsSection.upSellHeading}}"/> -+ -+ <!--Verify Up Sell Products displayed in ConfigProduct page--> -+ <see stepKey="seeTheUpSellHeading1" selector="{{StorefrontProductUpSellProductsSection.upSellHeading}}" userInput="We found other products you might like!"/> -+ <see stepKey="seeSimpleProduct2" selector="{{StorefrontProductUpSellProductsSection.upSellProducts}}" userInput="$$createSimpleProduct1.name$$"/> -+ -+ <!--Go to SimpleProduct1 store front page--> -+ <amOnPage url="$$createSimpleProduct1.sku$$.html" stepKey="goToSimpleProduct1FrontPage"/> -+ <waitForPageLoad stepKey="waitForSimpleProduct1ToBeLoaded"/> -+ -+ <!--Verify No Up Sell Products displayed in SimplProduct1 page--> -+ <dontSee stepKey="dontSeeTheUpSellHeading1" selector="{{StorefrontProductUpSellProductsSection.upSellHeading}}" userInput="We found other products you might like!"/> -+ </test> -+</tests> -diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminProductCategoryIndexerInUpdateOnScheduleModeTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminProductCategoryIndexerInUpdateOnScheduleModeTest.xml -new file mode 100644 -index 00000000000..ec0d86ac066 ---- /dev/null -+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminProductCategoryIndexerInUpdateOnScheduleModeTest.xml -@@ -0,0 +1,314 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> -+ <test name="AdminProductCategoryIndexerInUpdateOnScheduleModeTest"> -+ <annotations> -+ <stories value="Product Categories Indexer"/> -+ <title value="Product Categories Indexer in Update on Schedule mode"/> -+ <description value="The test verifies that in Update on Schedule mode if displaying of category products on Storefront changes due to product properties change, -+ the changes are NOT applied immediately, but applied only after cron runs (twice)."/> -+ <severity value="CRITICAL"/> -+ <testCaseId value="MC-11146"/> -+ <group value="catalog"/> -+ <group value="indexer"/> -+ </annotations> -+ <before> -+ <actionGroup ref="LoginAsAdmin" stepKey="LoginAsAdmin"/> -+ <!-- Create category A without products --> -+ <createData entity="_defaultCategory" stepKey="createCategoryA"/> -+ -+ <!-- Create product A1 not assigned to any category --> -+ <createData entity="simpleProductWithoutCategory" stepKey="createProductA1"/> -+ -+ <!-- Create anchor category B with subcategory C--> -+ <createData entity="_defaultCategory" stepKey="createCategoryB"/> -+ <createData entity="SubCategoryWithParent" stepKey="createCategoryC"> -+ <requiredEntity createDataKey="createCategoryB"/> -+ </createData> -+ -+ <!-- Assign product B1 to category B --> -+ <createData entity="ApiSimpleProduct" stepKey="createProductB1"> -+ <requiredEntity createDataKey="createCategoryB"/> -+ </createData> -+ -+ <!-- Assign product C1 to category C --> -+ <createData entity="ApiSimpleProduct" stepKey="createProductC1"> -+ <requiredEntity createDataKey="createCategoryC"/> -+ </createData> -+ -+ <!-- Assign product C2 to category C --> -+ <createData entity="ApiSimpleProduct" stepKey="createProductC2"> -+ <requiredEntity createDataKey="createCategoryC"/> -+ </createData> -+ -+ <!-- Switch indexers to "Update by Schedule" mode --> -+ <actionGroup ref="AdminSwitchAllIndexerToActionModeActionGroup" stepKey="onUpdateBySchedule"> -+ <argument name="action" value="Update by Schedule"/> -+ </actionGroup> -+ </before> -+ <after> -+ <!-- Switch indexers to "Update on Save" mode --> -+ <actionGroup ref="AdminSwitchAllIndexerToActionModeActionGroup" stepKey="onUpdateOnSave"> -+ <argument name="action" value="Update on Save"/> -+ </actionGroup> -+ <!-- Delete data --> -+ <deleteData createDataKey="createProductA1" stepKey="deleteProductA1"/> -+ <deleteData createDataKey="createProductB1" stepKey="deleteProductB1"/> -+ <deleteData createDataKey="createProductC1" stepKey="deleteProductC1"/> -+ <deleteData createDataKey="createProductC2" stepKey="deleteProductC2"/> -+ <deleteData createDataKey="createCategoryA" stepKey="deleteCategoryA"/> -+ <deleteData createDataKey="createCategoryC" stepKey="deleteCategoryC"/> -+ <deleteData createDataKey="createCategoryB" stepKey="deleteCategoryB"/> -+ <actionGroup ref="logout" stepKey="logout"/> -+ </after> -+ -+ <!-- Case: change product category from product page --> -+ <!-- 1. Open Admin > Catalog > Products > Product A1 --> -+ <amOnPage url="{{AdminProductEditPage.url($$createProductA1.id$$)}}" stepKey="goToProductA1"/> -+ <waitForPageLoad stepKey="waitForProductPageLoad"/> -+ -+ <!-- 2. Assign category A to product A1. Save product --> -+ <actionGroup ref="AdminAssignCategoryToProductAndSaveActionGroup" stepKey="assignProduct"> -+ <argument name="categoryName" value="$$createCategoryA.name$$"/> -+ </actionGroup> -+ -+ <!-- 3. Open category A on Storefront --> -+ <actionGroup ref="StorefrontGoToCategoryPageActionGroup" stepKey="goToCategoryA"> -+ <argument name="categoryName" value="$$createCategoryA.name$$"/> -+ </actionGroup> -+ -+ <!-- The category is still empty --> -+ <see userInput="$$createCategoryA.name$$" selector="{{StorefrontCategoryMainSection.CategoryTitle}}" stepKey="seeCategoryA1Name"/> -+ <see userInput="We can't find products matching the selection." stepKey="seeEmptyNotice"/> -+ <dontSee userInput="$$createProductA1.name$$" selector="{{StorefrontCategoryMainSection.productName}}" stepKey="dontseeProductA1"/> -+ -+ <!-- 4. Run cron to reindex --> -+ <wait time="60" stepKey="waitForChanges"/> -+ <magentoCLI command="cron:run" stepKey="runCron"/> -+ -+ <!-- 5. Open category A on Storefront again --> -+ <reloadPage stepKey="reloadCategoryA"/> -+ -+ <!-- Category A displays product A1 now --> -+ <see userInput="$$createCategoryA.name$$" selector="{{StorefrontCategoryMainSection.CategoryTitle}}" stepKey="seeTitleCategoryA1"/> -+ <see userInput="$$createProductA1.name$$" selector="{{StorefrontCategoryMainSection.productName}}" stepKey="seeProductA1"/> -+ -+ <!--6. Open Admin > Catalog > Products > Product A1. Unassign category A from product A1 --> -+ <amOnPage url="{{AdminProductEditPage.url($$createProductA1.id$$)}}" stepKey="OnPageProductA1"/> -+ <waitForPageLoad stepKey="waitForProductA1PageLoad"/> -+ <actionGroup ref="AdminUnassignCategoryOnProductAndSaveActionGroup" stepKey="unassignCategoryA"> -+ <argument name="categoryName" value="$$createCategoryA.name$$"/> -+ </actionGroup> -+ -+ <!-- 7. Open category A on Storefront --> -+ <actionGroup ref="StorefrontGoToCategoryPageActionGroup" stepKey="toCategoryA"> -+ <argument name="categoryName" value="$$createCategoryA.name$$"/> -+ </actionGroup> -+ -+ <!-- Category A still contains product A1 --> -+ <see userInput="$$createCategoryA.name$$" selector="{{StorefrontCategoryMainSection.CategoryTitle}}" stepKey="seeCategoryAOnPage"/> -+ <see userInput="$$createProductA1.name$$" selector="{{StorefrontCategoryMainSection.productName}}" stepKey="seeNameProductA1"/> -+ -+ <!-- 8. Run cron reindex (Ensure that at least one minute passed since last cron run) --> -+ <wait time="60" stepKey="waitOneMinute"/> -+ <magentoCLI command="cron:run" stepKey="runCron1"/> -+ -+ <!-- 9. Open category A on Storefront again --> -+ <reloadPage stepKey="refreshCategoryAPage"/> -+ -+ <!-- Category A is empty now --> -+ <see userInput="$$createCategoryA.name$$" selector="{{StorefrontCategoryMainSection.CategoryTitle}}" stepKey="seeOnPageCategoryAName"/> -+ <see userInput="We can't find products matching the selection." stepKey="seeOnPageEmptyNotice"/> -+ <dontSee userInput="$$createProductA1.name$$" selector="{{StorefrontCategoryMainSection.productName}}" stepKey="dontseeProductA1OnPage"/> -+ -+ <!-- Case: change product status --> -+ <!-- 10. Open category B on Storefront --> -+ <actionGroup ref="StorefrontGoToCategoryPageActionGroup" stepKey="toCategoryB"> -+ <argument name="categoryName" value="$$createCategoryB.name$$"/> -+ </actionGroup> -+ -+ <!-- Category B displays product B1, C1 and C2 --> -+ <see userInput="$$createCategoryB.name$$" selector="{{StorefrontCategoryMainSection.CategoryTitle}}" stepKey="seeCategoryBOnPage"/> -+ <see userInput="$$createProductB1.name$$" selector="{{StorefrontCategoryMainSection.productName}}" stepKey="seeNameProductB1"/> -+ <see userInput="$$createProductC1.name$$" selector="{{StorefrontCategoryMainSection.productName}}" stepKey="seeNameProductC1"/> -+ <see userInput="$$createProductC2.name$$" selector="{{StorefrontCategoryMainSection.productName}}" stepKey="seeNameProductC2"/> -+ -+ <!-- 11. Open product C1 in Admin. Make it disabled (Enable Product = No)--> -+ <amOnPage url="{{AdminProductEditPage.url($$createProductC1.id$$)}}" stepKey="goToProductC1"/> -+ <waitForPageLoad stepKey="waitForProductC1PageLoad"/> -+ <click selector="{{AdminProductFormSection.enableProductLabel}}" stepKey="clickOffEnableToggleAgain"/> -+ <!-- Saved successfully --> -+ <actionGroup ref="saveProductForm" stepKey="saveProductC1"/> -+ -+ <!-- 12. Open category B on Storefront --> -+ <actionGroup ref="StorefrontGoToCategoryPageActionGroup" stepKey="toCategoryBStorefront"> -+ <argument name="categoryName" value="$$createCategoryB.name$$"/> -+ </actionGroup> -+ -+ <!-- Category B displays product B1, C1 and C2 --> -+ <see userInput="$$createCategoryB.name$$" selector="{{StorefrontCategoryMainSection.CategoryTitle}}" stepKey="categoryBOnPage"/> -+ <see userInput="$$createProductB1.name$$" selector="{{StorefrontCategoryMainSection.productName}}" stepKey="seeProductB1"/> -+ <see userInput="$$createProductC1.name$$" selector="{{StorefrontCategoryMainSection.productName}}" stepKey="seeProductC1"/> -+ <see userInput="$$createProductC2.name$$" selector="{{StorefrontCategoryMainSection.productName}}" stepKey="seeProductC2"/> -+ -+ <!-- 13. Open category C on Storefront --> -+ <actionGroup ref="StorefrontGoToSubCategoryPageActionGroup" stepKey="goToCategoryC"> -+ <argument name="categoryName" value="$$createCategoryB.name$$"/> -+ <argument name="subCategoryName" value="$$createCategoryC.name$$"/> -+ </actionGroup> -+ -+ <!-- Category C still displays products C1 and C2 --> -+ <see userInput="$$createCategoryC.name$$" selector="{{StorefrontCategoryMainSection.CategoryTitle}}" stepKey="categoryCOnPage"/> -+ <see userInput="$$createProductC1.name$$" selector="{{StorefrontCategoryMainSection.productName}}" stepKey="seeProductC1inCategoryC1"/> -+ <see userInput="$$createProductC2.name$$" selector="{{StorefrontCategoryMainSection.productName}}" stepKey="seeProductC2InCategoryC2"/> -+ -+ <!-- 14. Run cron to reindex (Ensure that at least one minute passed since last cron run) --> -+ <wait time="60" stepKey="waitMinute"/> -+ <magentoCLI command="cron:run" stepKey="runCron2"/> -+ -+ <!-- 15. Open category B on Storefront --> -+ <actionGroup ref="StorefrontGoToCategoryPageActionGroup" stepKey="onPageCategoryB"> -+ <argument name="categoryName" value="$$createCategoryB.name$$"/> -+ </actionGroup> -+ -+ <!-- Category B displays product B1 and C2 only--> -+ <see userInput="$$createCategoryB.name$$" selector="{{StorefrontCategoryMainSection.CategoryTitle}}" stepKey="seeTitleCategoryBOnPage"/> -+ <see userInput="$$createProductB1.name$$" selector="{{StorefrontCategoryMainSection.productName}}" stepKey="seeOnCategoryBPageProductB1"/> -+ <dontSee userInput="$$createProductC1.name$$" selector="{{StorefrontCategoryMainSection.productName}}" stepKey="dontSeeOnCategoryBPageProductC1"/> -+ <see userInput="$$createProductC2.name$$" selector="{{StorefrontCategoryMainSection.productName}}" stepKey="seeOnCategoryBPageProductC2"/> -+ -+ <!-- 16. Open category C on Storefront --> -+ <actionGroup ref="StorefrontGoToSubCategoryPageActionGroup" stepKey="openCategoryC"> -+ <argument name="categoryName" value="$$createCategoryB.name$$"/> -+ <argument name="subCategoryName" value="$$createCategoryC.name$$"/> -+ </actionGroup> -+ -+ <!-- Category C displays only product C2 now --> -+ <see userInput="$$createCategoryC.name$$" selector="{{StorefrontCategoryMainSection.CategoryTitle}}" stepKey="seeTitleOnCategoryCPage"/> -+ <dontSee userInput="$$createProductC1.name$$" selector="{{StorefrontCategoryMainSection.productName}}" stepKey="dontSeeOnCategoryCPageProductC1"/> -+ <see userInput="$$createProductC2.name$$" selector="{{StorefrontCategoryMainSection.productName}}" stepKey="seeOnCategoryCPageProductC2"/> -+ -+ <!-- 17. Repeat steps 10-16, but enable products instead. --> -+ <!-- 17.11 Open product C1 in Admin. Make it enabled --> -+ <amOnPage url="{{AdminProductEditPage.url($$createProductC1.id$$)}}" stepKey="goToEditProductC1"/> -+ <waitForPageLoad stepKey="waitForProductC1Page"/> -+ <click selector="{{AdminProductFormSection.enableProductLabel}}" stepKey="clickOnEnableToggleAgain"/> -+ -+ <!-- Saved successfully --> -+ <actionGroup ref="saveProductForm" stepKey="saveChangedProductC1"/> -+ -+ <!-- 17.12. Open category B on Storefront --> -+ <actionGroup ref="StorefrontGoToCategoryPageActionGroup" stepKey="openCategoryB"> -+ <argument name="categoryName" value="$$createCategoryB.name$$"/> -+ </actionGroup> -+ -+ <!-- Category B displays product B1 and C2 --> -+ <see userInput="$$createCategoryB.name$$" selector="{{StorefrontCategoryMainSection.CategoryTitle}}" stepKey="titleCategoryBOnPage"/> -+ <see userInput="$$createProductB1.name$$" selector="{{StorefrontCategoryMainSection.productName}}" stepKey="seeCategoryBPageProductB1"/> -+ <dontSee userInput="$$createProductC1.name$$" selector="{{StorefrontCategoryMainSection.productName}}" stepKey="dontSeeCategoryBPageProductC1"/> -+ <see userInput="$$createProductC2.name$$" selector="{{StorefrontCategoryMainSection.productName}}" stepKey="seeCategoryBPageProductC2"/> -+ -+ <!-- 17.13. Open category C on Storefront --> -+ <actionGroup ref="StorefrontGoToSubCategoryPageActionGroup" stepKey="openToCategoryC"> -+ <argument name="categoryName" value="$$createCategoryB.name$$"/> -+ <argument name="subCategoryName" value="$$createCategoryC.name$$"/> -+ </actionGroup> -+ -+ <!-- Category C displays product C2 --> -+ <see userInput="$$createCategoryC.name$$" selector="{{StorefrontCategoryMainSection.CategoryTitle}}" stepKey="titleOnCategoryCPage"/> -+ <dontSee userInput="$$createProductC1.name$$" selector="{{StorefrontCategoryMainSection.productName}}" stepKey="dontSeeCategoryCPageProductC1"/> -+ <see userInput="$$createProductC2.name$$" selector="{{StorefrontCategoryMainSection.productName}}" stepKey="seeCategoryCPageProductC2"/> -+ -+ <!-- 17.14. Run cron to reindex (Ensure that at least one minute passed since last cron run) --> -+ <wait time="60" stepKey="waitForOneMinute"/> -+ <magentoCLI command="cron:run" stepKey="runCron3"/> -+ -+ <!-- 17.15. Open category B on Storefront --> -+ <actionGroup ref="StorefrontGoToCategoryPageActionGroup" stepKey="openPageCategoryB"> -+ <argument name="categoryName" value="$$createCategoryB.name$$"/> -+ </actionGroup> -+ -+ <!-- Category B displays products B1, C1, C2 again, but only after reindex. --> -+ <see userInput="$$createCategoryB.name$$" selector="{{StorefrontCategoryMainSection.CategoryTitle}}" stepKey="onPageSeeCategoryBTitle"/> -+ <see userInput="$$createProductB1.name$$" selector="{{StorefrontCategoryMainSection.productName}}" stepKey="onPageSeeCategoryBProductB1"/> -+ <see userInput="$$createProductC1.name$$" selector="{{StorefrontCategoryMainSection.productName}}" stepKey="onPageSeeCategoryBProductC1"/> -+ <see userInput="$$createProductC2.name$$" selector="{{StorefrontCategoryMainSection.productName}}" stepKey="onPageSeeCategoryBProductC2"/> -+ -+ <!-- 17.16. Open category C on Storefront --> -+ <actionGroup ref="StorefrontGoToSubCategoryPageActionGroup" stepKey="openOnStorefrontCategoryC"> -+ <argument name="categoryName" value="$$createCategoryB.name$$"/> -+ <argument name="subCategoryName" value="$$createCategoryC.name$$"/> -+ </actionGroup> -+ -+ <!-- Category C displays products C1, C2 again, but only after reindex.--> -+ <see userInput="$$createCategoryC.name$$" selector="{{StorefrontCategoryMainSection.CategoryTitle}}" stepKey="onPageSeeCategoryCTitle"/> -+ <see userInput="$$createProductC1.name$$" selector="{{StorefrontCategoryMainSection.productName}}" stepKey="onPageSeeCategoryCProductC1"/> -+ <see userInput="$$createProductC2.name$$" selector="{{StorefrontCategoryMainSection.productName}}" stepKey="onPageSeeCategoryCProductC2"/> -+ -+ <!-- Case: change product visibility --> -+ <!-- 18. Repeat steps 10-17 but change product Visibility instead of product status --> -+ <!-- 18.11 Open product C1 in Admin. Make it enabled --> -+ <amOnPage url="{{AdminProductEditPage.url($$createProductC1.id$$)}}" stepKey="editProductC1"/> -+ <waitForPageLoad stepKey="waitProductC1Page"/> -+ <selectOption selector="{{AdminProductFormBundleSection.visibilityDropDown}}" userInput="Search" -+ stepKey="changeVisibility"/> -+ -+ <!-- Saved successfully --> -+ <actionGroup ref="saveProductForm" stepKey="productC1Saved"/> -+ -+ <!-- 18.12. Open category B on Storefront --> -+ <actionGroup ref="StorefrontGoToCategoryPageActionGroup" stepKey="goPageCategoryB"> -+ <argument name="categoryName" value="$$createCategoryB.name$$"/> -+ </actionGroup> -+ -+ <!-- Category B displays products B1, C1, C2 again, but only after reindex. --> -+ <see userInput="$$createCategoryB.name$$" selector="{{StorefrontCategoryMainSection.CategoryTitle}}" stepKey="seeCategoryBTitle"/> -+ <see userInput="$$createProductB1.name$$" selector="{{StorefrontCategoryMainSection.productName}}" stepKey="seeCategoryBProductB1"/> -+ <see userInput="$$createProductC1.name$$" selector="{{StorefrontCategoryMainSection.productName}}" stepKey="seeCategoryBProductC1"/> -+ <see userInput="$$createProductC2.name$$" selector="{{StorefrontCategoryMainSection.productName}}" stepKey="seeCategoryBProductC2"/> -+ -+ <!-- 18.13. Open category C on Storefront --> -+ <actionGroup ref="StorefrontGoToSubCategoryPageActionGroup" stepKey="goPageCategoryC"> -+ <argument name="categoryName" value="$$createCategoryB.name$$"/> -+ <argument name="subCategoryName" value="$$createCategoryC.name$$"/> -+ </actionGroup> -+ -+ <!-- Category C displays products C1, C2 again, but only after reindex.--> -+ <see userInput="$$createCategoryC.name$$" selector="{{StorefrontCategoryMainSection.CategoryTitle}}" stepKey="seeCategoryCTitle"/> -+ <see userInput="$$createProductC1.name$$" selector="{{StorefrontCategoryMainSection.productName}}" stepKey="seeOnCategoryCProductC1"/> -+ <see userInput="$$createProductC2.name$$" selector="{{StorefrontCategoryMainSection.productName}}" stepKey="seeOnCategoryCProductC2"/> -+ -+ <!-- 18.14. Run cron to reindex (Ensure that at least one minute passed since last cron run) --> -+ <wait time="60" stepKey="waitExtraMinute"/> -+ <magentoCLI command="cron:run" stepKey="runCron4"/> -+ -+ <!-- 18.15. Open category B on Storefront --> -+ <actionGroup ref="StorefrontGoToCategoryPageActionGroup" stepKey="navigateToPageCategoryB"> -+ <argument name="categoryName" value="$$createCategoryB.name$$"/> -+ </actionGroup> -+ -+ <!-- Category B displays product B1 and C2 only--> -+ <see userInput="$$createCategoryB.name$$" selector="{{StorefrontCategoryMainSection.CategoryTitle}}" stepKey="seeTitleCategoryB"/> -+ <see userInput="$$createProductB1.name$$" selector="{{StorefrontCategoryMainSection.productName}}" stepKey="seeTitleProductB1"/> -+ <dontSee userInput="$$createProductC1.name$$" selector="{{StorefrontCategoryMainSection.productName}}" stepKey="dontseeCategoryBProductC1"/> -+ <see userInput="$$createProductC2.name$$" selector="{{StorefrontCategoryMainSection.productName}}" stepKey="seeTitleProductC2"/> -+ -+ <!-- 18.18. Open category C on Storefront --> -+ <actionGroup ref="StorefrontGoToSubCategoryPageActionGroup" stepKey="navigateToPageCategoryC"> -+ <argument name="categoryName" value="$$createCategoryB.name$$"/> -+ <argument name="subCategoryName" value="$$createCategoryC.name$$"/> -+ </actionGroup> -+ -+ <!-- Category C displays product C2 again, but only after reindex.--> -+ <see userInput="$$createCategoryC.name$$" selector="{{StorefrontCategoryMainSection.CategoryTitle}}" stepKey="seeTitleCategoryC"/> -+ <dontSee userInput="$$createProductC1.name$$" selector="{{StorefrontCategoryMainSection.productName}}" stepKey="dontSeeOnCategoryCProductC1"/> -+ <see userInput="$$createProductC2.name$$" selector="{{StorefrontCategoryMainSection.productName}}" stepKey="seeOnPageTitleProductC2"/> -+ </test> -+</tests> -diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminProductGridFilteringByDateAttributeTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminProductGridFilteringByDateAttributeTest.xml -index b079d35296e..2884cb26cf8 100644 ---- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminProductGridFilteringByDateAttributeTest.xml -+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminProductGridFilteringByDateAttributeTest.xml -@@ -7,9 +7,11 @@ - --> - - <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="AdminProductGridFilteringByDateAttributeTest"> - <annotations> -+ <features value="Catalog"/> -+ <stories value="Filter products"/> - <title value="Verify Set Product as new Filter input on Product Grid doesn't getreset to currentDate"/> - <description value="Data input in the new from date filter field should not change"/> - <severity value="MAJOR"/> -diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminProductImageAssignmentForMultipleStoresTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminProductImageAssignmentForMultipleStoresTest.xml -new file mode 100644 -index 00000000000..8149bc34087 ---- /dev/null -+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminProductImageAssignmentForMultipleStoresTest.xml -@@ -0,0 +1,139 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> -+ <test name="AdminProductImageAssignmentForMultipleStoresTest"> -+ <annotations> -+ <features value="Catalog"/> -+ <stories value="Product image assignment for multiple stores"/> -+ <title value="Product image assignment for multiple stores"/> -+ <description value="Product image assignment for multiple stores"/> -+ <severity value="MAJOR"/> -+ <testCaseId value="MAGETWO-58718"/> -+ <group value="product"/> -+ <group value="WYSIWYGDisabled"/> -+ <skip> -+ <issueId value="MC-13841"/> -+ </skip> -+ </annotations> -+ <before> -+ <!-- Login Admin --> -+ <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> -+ <!-- Create Store View English --> -+ <actionGroup ref="AdminCreateStoreViewActionGroup" stepKey="createStoreViewEn"> -+ <argument name="customStore" value="customStoreEN"/> -+ </actionGroup> -+ <!-- Create Store View France --> -+ <actionGroup ref="AdminCreateStoreViewActionGroup" stepKey="createStoreViewFr"> -+ <argument name="customStore" value="customStoreFR"/> -+ </actionGroup> -+ <!-- Create Category and Simple Product --> -+ <createData entity="_defaultCategory" stepKey="createCategory"/> -+ <createData entity="_defaultProduct" stepKey="createSimpleProduct"> -+ <requiredEntity createDataKey="createCategory"/> -+ <field key="price">100</field> -+ </createData> -+ </before> -+ <after> -+ <!-- Delete Store View English --> -+ <actionGroup ref="AdminDeleteStoreViewActionGroup" stepKey="deleteStoreViewEn"> -+ <argument name="customStore" value="customStoreEN"/> -+ </actionGroup> -+ <!-- Delete Store View France --> -+ <actionGroup ref="AdminDeleteStoreViewActionGroup" stepKey="deleteStoreViewFr"> -+ <argument name="customStore" value="customStoreFR"/> -+ </actionGroup> -+ <!-- Clear Filter Store --> -+ <actionGroup ref="clearFiltersAdminDataGrid" stepKey="resetFiltersOnStorePage"/> -+ <!-- Delete Category and Simple Product --> -+ <deleteData createDataKey="createSimpleProduct" stepKey="deleteProduct"/> -+ <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> -+ <!-- Clear Filter Product --> -+ <actionGroup ref="AdminClearFiltersActionGroup" stepKey="clearProductFilters"/> -+ <!-- Logout Admin --> -+ <actionGroup ref="logout" stepKey="logoutOfAdmin"/> -+ </after> -+ <!-- Search Product and Open Edit --> -+ <actionGroup ref="SearchForProductOnBackendActionGroup" stepKey="searchProduct"> -+ <argument name="product" value="$$createSimpleProduct$$"/> -+ </actionGroup> -+ <actionGroup ref="OpenEditProductOnBackendActionGroup" stepKey="openEditProduct"> -+ <argument name="product" value="$$createSimpleProduct$$"/> -+ </actionGroup> -+ -+ <!-- Switch to the English store view --> -+ <actionGroup ref="AdminSwitchStoreViewActionGroup" stepKey="switchStoreViewEnglishProduct"> -+ <argument name="storeView" value="customStoreEN.name"/> -+ </actionGroup> -+ -+ <!-- Upload Image English --> -+ <actionGroup ref="addProductImage" stepKey="uploadImageEnglish"/> -+ <actionGroup ref="saveProductForm" stepKey="saveProduct1"/> -+ -+ <!-- Switch to the French store view --> -+ <actionGroup ref="AdminSwitchStoreViewActionGroup" stepKey="switchStoreViewFrenchProduct"> -+ <argument name="storeView" value="customStoreFR.name"/> -+ </actionGroup> -+ -+ <!-- Upload Image French --> -+ <actionGroup ref="addProductImage" stepKey="uploadImageFrench"> -+ <argument name="image" value="Magento3"/> -+ </actionGroup> -+ <actionGroup ref="AdminAssignImageRolesActionGroup" stepKey="assignImageRole1"> -+ <argument name="image" value="Magento3"/> -+ </actionGroup> -+ <actionGroup ref="saveProductForm" stepKey="saveProduct2"/> -+ -+ <!-- Switch to the All store view --> -+ <actionGroup ref="AdminSwitchToAllStoreViewActionGroup" stepKey="switchAllStoreViewProduct"/> -+ -+ <!-- Upload Image All Store View --> -+ <actionGroup ref="addProductImage" stepKey="uploadImageAllStoreView"> -+ <argument name="image" value="TestImageNew"/> -+ </actionGroup> -+ <actionGroup ref="AdminAssignImageRolesActionGroup" stepKey="assignImageRole"> -+ <argument name="image" value="TestImageNew"/> -+ </actionGroup> -+ -+ <!-- Change any product data product description --> -+ <click selector="{{AdminProductContentSection.sectionHeader}}" stepKey="openDescriptionDropDown"/> -+ <fillField selector="{{AdminProductContentSection.descriptionTextArea}}" userInput="This is the long description" stepKey="fillLongDescription"/> -+ <fillField selector="{{AdminProductContentSection.shortDescriptionTextArea}}" userInput="This is the short description" stepKey="fillShortDescription"/> -+ <actionGroup ref="saveProductForm" stepKey="saveProduct"/> -+ -+ <!-- Go to Product Page and see Default Store View--> -+ <amOnPage url="{{StorefrontProductPage.url($$createSimpleProduct.custom_attributes[url_key]$$)}}" stepKey="goToDefaultStorefrontProductPage"/> -+ <waitForElementVisible selector="{{StorefrontProductMediaSection.productImageActive(TestImageNew.filename)}}" time="30" stepKey="waitImageToBeLoaded"/> -+ <seeElement selector="{{StorefrontProductMediaSection.productImageActive(TestImageNew.filename)}}" stepKey="seeActiveImageDefault"/> -+ -+ <!-- English Switch Store View and see English Store View --> -+ <actionGroup ref="StorefrontSwitchStoreViewActionGroup" stepKey="switchStoreViewEnglish"> -+ <argument name="storeView" value="customStoreEN"/> -+ </actionGroup> -+ <click selector="{{StorefrontHeaderSection.NavigationCategoryByName($$createCategory.name$$)}}" stepKey="openCategoryPage"/> -+ <waitForPageLoad time="30" stepKey="waitForCategoryPage"/> -+ <seeElement selector="{{StorefrontCategoryProductSection.ProductImageBySrc(ProductImage.fileName)}}" stepKey="seeThumb"/> -+ <click selector="{{StorefrontCategoryProductSection.ProductTitleByName($$createSimpleProduct.name$$)}}" stepKey="openProductPage"/> -+ <waitForPageLoad time="30" stepKey="waitForProductPage"/> -+ <waitForElementVisible selector="{{StorefrontProductMediaSection.productImageActive(ProductImage.filename)}}" time="30" stepKey="waitImageToBeLoaded2"/> -+ <seeElement selector="{{StorefrontProductMediaSection.productImageActive(ProductImage.filename)}}" stepKey="seeActiveImageEnglish"/> -+ -+ <!-- Switch France Store View and see France Store View --> -+ <actionGroup ref="StorefrontSwitchStoreViewActionGroup" stepKey="switchStoreViewFrance"> -+ <argument name="storeView" value="customStoreFR"/> -+ </actionGroup> -+ <click selector="{{StorefrontHeaderSection.NavigationCategoryByName($$createCategory.name$$)}}" stepKey="openCategoryPage1"/> -+ <waitForPageLoad time="30" stepKey="waitForCategoryPage1"/> -+ <seeElement selector="{{StorefrontCategoryProductSection.ProductImageBySrc(Magento3.fileName)}}" stepKey="seeThumb1"/> -+ <click selector="{{StorefrontCategoryProductSection.ProductTitleByName($$createSimpleProduct.name$$)}}" stepKey="openProductPage1"/> -+ <waitForPageLoad time="30" stepKey="waitForProductPage1"/> -+ <waitForElementVisible selector="{{StorefrontProductMediaSection.productImageActive(Magento3.filename)}}" time="30" stepKey="waitImageToBeLoaded3"/> -+ <seeElement selector="{{StorefrontProductMediaSection.productImageActive(Magento3.filename)}}" stepKey="seeActiveImageFrance"/> -+ </test> -+</tests> -diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminProductStatusAttributeDisabledByDefaultTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminProductStatusAttributeDisabledByDefaultTest.xml -index c8be44eb73c..a882c6e7817 100644 ---- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminProductStatusAttributeDisabledByDefaultTest.xml -+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminProductStatusAttributeDisabledByDefaultTest.xml -@@ -7,9 +7,11 @@ - --> - - <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="AdminProductStatusAttributeDisabledByDefaultTest"> - <annotations> -+ <features value="Catalog"/> -+ <stories value="Create products"/> - <title value="Verify the default option value for product Status attribute is set correctly during product creation"/> - <description value="The default option value for product Status attribute is set correctly during product creation"/> - <severity value="MAJOR"/> -diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminRemoveCustomOptionsFromProductTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminRemoveCustomOptionsFromProductTest.xml -new file mode 100644 -index 00000000000..b5f212d1144 ---- /dev/null -+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminRemoveCustomOptionsFromProductTest.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="AdminRemoveCustomOptionsFromProductTest"> -+ <annotations> -+ <features value="Catalog"/> -+ <stories value="Create product with custom options"/> -+ <title value="Remove custom options from product"/> -+ <description value="Remove custom options from product"/> -+ <severity value="MAJOR"/> -+ <testCaseId value="MC-11512"/> -+ <group value="catalog"/> -+ </annotations> -+ <before> -+ <createData entity="SimpleProduct2" stepKey="createProduct"/> -+ <updateData entity="ProductWithTextFieldAndAreaAndFileOptions" createDataKey="createProduct" stepKey="updateProductWithOptions"> -+ <requiredEntity createDataKey="createProduct"/> -+ </updateData> -+ </before> -+ <after> -+ <deleteData createDataKey="createProduct" stepKey="deleteProductWithOptions"/> -+ <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="navigateToProductIndex"/> -+ <actionGroup ref="clearFiltersAdminDataGrid" stepKey="clearProductFilter"/> -+ <actionGroup ref="logout" stepKey="logout"/> -+ </after> -+ <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> -+ <!-- Edit Simple Product --> -+ <amOnPage url="{{AdminProductEditPage.url($$createProduct.id$$)}}" stepKey="goToProduct"/> -+ <!-- See 3 options are present --> -+ <actionGroup ref="AdminAssertProductCustomOptionVisible" stepKey="assertCustomOptionsField"> -+ <argument name="option" value="ProductOptionField"/> -+ </actionGroup> -+ <actionGroup ref="AdminAssertProductCustomOptionVisible" stepKey="assertCustomOptionsArea"> -+ <argument name="option" value="ProductOptionArea"/> -+ </actionGroup> -+ <actionGroup ref="AdminAssertProductCustomOptionVisible" stepKey="assertCustomOptionsFile"> -+ <argument name="option" value="ProductOptionFile"/> -+ </actionGroup> -+ <!-- Click delete "Area" and "File" options --> -+ <actionGroup ref="AdminDeleteProductCustomOption" stepKey="deleteCustomOptionArea"> -+ <argument name="option" value="ProductOptionArea"/> -+ </actionGroup> -+ <actionGroup ref="AdminDeleteProductCustomOption" stepKey="deleteCustomOptionFile"> -+ <argument name="option" value="ProductOptionFile"/> -+ </actionGroup> -+ <actionGroup ref="AdminAssertProductCustomOptionVisible" stepKey="assertVisibleCustomOptionField"> -+ <argument name="option" value="ProductOptionField"/> -+ </actionGroup> -+ <actionGroup ref="saveProductForm" stepKey="saveProduct"/> -+ <!-- See only "Field option" left Also we shouldn't see any other options --> -+ <actionGroup ref="AdminAssertProductCustomOptionVisible" stepKey="assertVisibleSecondCustomOptionField"> -+ <argument name="option" value="ProductOptionField"/> -+ </actionGroup> -+ <actionGroup ref="AdminAssertProductHasNoCustomOption" stepKey="assertNoCustomOptionsFile"> -+ <argument name="option" value="ProductOptionFileSecond"/> -+ </actionGroup> -+ <actionGroup ref="AdminAssertProductHasNoCustomOption" stepKey="assertNoCustomOptionsField"> -+ <argument name="option" value="ProductOptionFieldSecond"/> -+ </actionGroup> -+ <!-- Click Add option "File" --> -+ <actionGroup ref="AddProductCustomOptionFile" stepKey="createAddOptionFile"> -+ <argument name="option" value="ProductOptionFileSecond"/> -+ </actionGroup> -+ <!-- Click Add option "Field" --> -+ <actionGroup ref="AddProductCustomOptionField" stepKey="createCustomOptionField"> -+ <argument name="option" value="ProductOptionFieldSecond"/> -+ </actionGroup> -+ <actionGroup ref="saveProductForm" stepKey="saveProductWithNewlyAddedOptions"/> -+ <!-- See 3 options are present --> -+ <actionGroup ref="AdminAssertProductCustomOptionVisible" stepKey="assertPresentCustomOptionField"> -+ <argument name="option" value="ProductOptionField"/> -+ </actionGroup> -+ <actionGroup ref="AdminAssertProductCustomOptionVisible" stepKey="assertPresenceOfFileOption"> -+ <argument name="option" value="ProductOptionFileSecond"/> -+ </actionGroup> -+ <actionGroup ref="AdminAssertProductCustomOptionVisible" stepKey="assertPresenceOfFieldOption"> -+ <argument name="option" value="ProductOptionFieldSecond"/> -+ </actionGroup> -+ <!-- Delete All options and See no more options present on the page --> -+ <actionGroup ref="AdminDeleteAllProductCustomOptions" stepKey="deleteAllCustomOptions"/> -+ <!-- Product successfully saved and it has no options --> -+ <actionGroup ref="saveProductForm" stepKey="saveProductWithoutCustomOptions"/> -+ <actionGroup ref="AdminAssertProductHasNoCustomOptions" stepKey="assertNoCustomOptions"/> -+ </test> -+</tests> -diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminRemoveDefaultImageSimpleProductTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminRemoveDefaultImageSimpleProductTest.xml -index 4a3b370c7da..9760dc579b1 100644 ---- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminRemoveDefaultImageSimpleProductTest.xml -+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminRemoveDefaultImageSimpleProductTest.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="AdminRemoveDefaultImageSimpleProductTest"> - <annotations> - <features value="Catalog"/> -diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminRemoveDefaultImageVirtualProductTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminRemoveDefaultImageVirtualProductTest.xml -index e249c8e8cea..a740b700c30 100644 ---- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminRemoveDefaultImageVirtualProductTest.xml -+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminRemoveDefaultImageVirtualProductTest.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="AdminRemoveDefaultImageVirtualProductTest"> - <annotations> - <features value="Catalog"/> -diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminRemoveDefaultVideoSimpleProductTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminRemoveDefaultVideoSimpleProductTest.xml -new file mode 100644 -index 00000000000..876eedb9347 ---- /dev/null -+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminRemoveDefaultVideoSimpleProductTest.xml -@@ -0,0 +1,51 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> -+ <test name="AdminRemoveDefaultVideoSimpleProductTest"> -+ <annotations> -+ <features value="Catalog"/> -+ <stories value="Add/remove images and videos for all product types and category"/> -+ <title value="Admin should be able to remove default product video from a Simple Product"/> -+ <description value="Admin should be able to remove default product video from a Simple Product"/> -+ <severity value="MAJOR"/> -+ <testCaseId value="MC-206"/> -+ <group value="Catalog"/> -+ </annotations> -+ <before> -+ <actionGroup ref="EnableAdminAccountSharingActionGroup" stepKey="enableAdminAccountSharing"/> -+ <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> -+ </before> -+ <after> -+ <amOnPage url="{{AdminLogoutPage.url}}" stepKey="amOnLogoutPage"/> -+ </after> -+ -+ <!-- Create product --> -+ <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="adminProductIndexPageAdd"/> -+ <waitForPageLoad stepKey="waitForProductIndexPageLoad"/> -+ <actionGroup ref="EnableAdminAccountSharingActionGroup" stepKey="enableAdminAccountSharing"/> -+ <actionGroup ref="goToCreateProductPage" stepKey="goToCreateProductPage"> -+ <argument name="product" value="ApiSimpleProduct"/> -+ </actionGroup> -+ <actionGroup ref="fillMainProductFormNoWeight" stepKey="fillMainProductForm"> -+ <argument name="product" value="ApiSimpleProduct"/> -+ </actionGroup> -+ -+ <!-- Save product --> -+ <actionGroup ref="saveProductForm" stepKey="saveProductForm"/> -+ -+ <!-- Save product --> -+ <actionGroup ref="saveProductForm" stepKey="saveProductFormAfterRemove"/> -+ -+ <!-- Assert product in storefront product page --> -+ <actionGroup ref="AssertProductInStorefrontProductPage" stepKey="AssertProductInStorefrontProductPage"> -+ <argument name="product" value="ApiSimpleProduct"/> -+ </actionGroup> -+ </test> -+</tests> -diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminRemoveDefaultVideoVirtualProductTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminRemoveDefaultVideoVirtualProductTest.xml -new file mode 100644 -index 00000000000..8b3b38d0ece ---- /dev/null -+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminRemoveDefaultVideoVirtualProductTest.xml -@@ -0,0 +1,34 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> -+ <test name="AdminRemoveDefaultVideoVirtualProductTest" extends="AdminRemoveDefaultVideoSimpleProductTest"> -+ <annotations> -+ <features value="Catalog"/> -+ <stories value="Add/remove images and videos for all product types and category"/> -+ <title value="Admin should be able to remove default product video from a Virtual Product"/> -+ <description value="Admin should be able to remove default product video from a Virtual Product"/> -+ <severity value="MAJOR"/> -+ <testCaseId value="MC-204"/> -+ <group value="Catalog"/> -+ </annotations> -+ -+ <!-- Replacing steps in base AdminRemoveDefaultVideoSimpleProductTest --> -+ -+ <actionGroup ref="goToCreateProductPage" stepKey="goToCreateProductPage"> -+ <argument name="product" value="defaultVirtualProduct"/> -+ </actionGroup> -+ <actionGroup ref="fillMainProductFormNoWeight" stepKey="fillMainProductForm"> -+ <argument name="product" value="defaultVirtualProduct"/> -+ </actionGroup> -+ <actionGroup ref="AssertProductInStorefrontProductPage" stepKey="AssertProductInStorefrontProductPage"> -+ <argument name="product" value="defaultVirtualProduct"/> -+ </actionGroup> -+ </test> -+</tests> -diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminRemoveImageAffectsAllScopesTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminRemoveImageAffectsAllScopesTest.xml -new file mode 100644 -index 00000000000..8316f54c15a ---- /dev/null -+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminRemoveImageAffectsAllScopesTest.xml -@@ -0,0 +1,131 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> -+ <test name="AdminRemoveImageAffectsAllScopesTest"> -+ <annotations> -+ <features value="Catalog"/> -+ <stories value="MAGETWO-66442: Changes in default scope not effect product images in other scopes"/> -+ <title value="Effect of product images changes in default scope to other scopes"/> -+ <description value="Product image should be deleted from all scopes"/> -+ <severity value="MAJOR"/> -+ <testCaseId value="MAGETWO-94265"/> -+ <group value="Catalog"/> -+ </annotations> -+ <before> -+ <!--Create 2 websites (with stores, store views)--> -+ <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> -+ <createData entity="_defaultCategory" stepKey="category"/> -+ <createData entity="_defaultProduct" stepKey="product"> -+ <requiredEntity createDataKey="category"/> -+ </createData> -+ <actionGroup ref="AdminCreateWebsiteActionGroup" stepKey="createWebsite"> -+ <argument name="newWebsiteName" value="FirstWebSite"/> -+ <argument name="websiteCode" value="FirstWebSiteCode"/> -+ </actionGroup> -+ <actionGroup ref="AdminCreateNewStoreGroupActionGroup" stepKey="createNewStore" after="createWebsite"> -+ <argument name="website" value="FirstWebSite"/> -+ <argument name="storeGroupName" value="NewStore"/> -+ <argument name="storeGroupCode" value="Base1"/> -+ </actionGroup> -+ <actionGroup ref="AdminCreateStoreViewActionGroup" stepKey="createCustomStoreView" after="createNewStore"> -+ <argument name="StoreGroup" value="staticFirstStoreGroup"/> -+ <argument name="customStore" value="staticStore"/> -+ </actionGroup> -+ -+ <actionGroup ref="AdminCreateWebsiteActionGroup" stepKey="createSecondWebsite" after="createCustomStoreView"> -+ <argument name="newWebsiteName" value="SecondWebSite"/> -+ <argument name="websiteCode" value="SecondWebSiteCode"/> -+ </actionGroup> -+ <actionGroup ref="AdminCreateNewStoreGroupActionGroup" stepKey="createSecondStore" after="createSecondWebsite"> -+ <argument name="website" value="SecondWebSite"/> -+ <argument name="storeGroupName" value="SecondStore"/> -+ <argument name="storeGroupCode" value="Base2"/> -+ </actionGroup> -+ <actionGroup ref="AdminCreateStoreViewActionGroup" stepKey="createCustomStoreView2" after="createSecondStore"> -+ <argument name="StoreGroup" value="staticStoreGroup"/> -+ <argument name="customStore" value="staticSecondStore"/> -+ </actionGroup> -+ </before> -+ -+ <after> -+ <actionGroup ref="ResetWebUrlOptions" stepKey="resetUrlOption"/> -+ <actionGroup ref="AdminDeleteWebsiteActionGroup" stepKey="deleteWebsite"> -+ <argument name="websiteName" value="FirstWebSite"/> -+ </actionGroup> -+ -+ <actionGroup ref="AdminDeleteWebsiteActionGroup" stepKey="deleteSecondWebsite"> -+ <argument name="websiteName" value="SecondWebSite"/> -+ </actionGroup> -+ <deleteData createDataKey="category" stepKey="deletePreReqCategory"/> -+ <deleteData createDataKey="product" stepKey="deleteFirstProduct"/> -+ <magentoCLI stepKey="reindex" command="indexer:reindex"/> -+ <magentoCLI stepKey="flushCache" command="cache:flush"/> -+ <actionGroup ref="logout" stepKey="logout"/> -+ </after> -+ -+ <!--Create product--> -+ <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="navigateToProductIndex"/> -+ <waitForPageLoad stepKey="waitForProductIndexPage"/> -+ <actionGroup ref="resetProductGridToDefaultView" stepKey="resetProductGridColumnsInitial"/> -+ -+ <!--Open created product--> -+ <click selector="{{AdminProductGridSection.productGridNameProduct($$product.name$$)}}" stepKey="createdProduct"/> -+ <waitForPageLoad stepKey="waitForOpenedCreatedProduct"/> -+ -+ <!-- Add image to product --> -+ <actionGroup ref="addProductImage" stepKey="addFirstImageForProduct"> -+ <argument name="image" value="TestImageNew"/> -+ </actionGroup> -+ -+ <!-- Add second image to product --> -+ <actionGroup ref="addProductImage" stepKey="addSecondImageForProduct"> -+ <argument name="image" value="MagentoLogo"/> -+ </actionGroup> -+ <!--"Product in Websites": select both Websites--> -+ <actionGroup ref="ProductSetWebsite" stepKey="ProductSetWebsite1"> -+ <argument name="website" value="FirstWebSite"/> -+ </actionGroup> -+ <actionGroup ref="ProductSetWebsite" stepKey="ProductSetWebsite2"> -+ <argument name="website" value="SecondWebSite"/> -+ </actionGroup> -+ -+ <!--Go to "Catalog" -> "Products". Open created product--> -+ <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="navigateToProductPage"/> -+ <waitForPageLoad stepKey="waitForProductPageLoaded"/> -+ <click selector="{{AdminProductGridSection.productGridNameProduct($$product.name$$)}}" stepKey="openCreatedProduct"/> -+ <waitForPageLoad stepKey="waitForCreatedProductOpened"/> -+ -+ <!--Delete Image 1--> -+ <actionGroup ref="removeProductImage" stepKey="removeProductImage"/> -+ -+ <!--Click "Save" in the upper right corner--> -+ <actionGroup ref="saveProductForm" stepKey="saveProductFormAfterRemove"/> -+ -+ <!--Switch to "Store view 1"--> -+ <actionGroup ref="SwitchToTheNewStoreView" stepKey="selectStoreView"> -+ <argument name="storeViewName" value="Store View"/> -+ </actionGroup> -+ -+ <!-- Assert product first image not in admin product form --> -+ <actionGroup ref="assertProductImageNotInAdminProductPage" stepKey="assertProductImageNotInAdminProductPage"> -+ <argument name="image" value="TestImageNew"/> -+ </actionGroup> -+ -+ <!--Switch to "Store view 2"--> -+ <actionGroup ref="SwitchToTheNewStoreView" stepKey="selectSecondStoreView"> -+ <argument name="storeViewName" value="Second Store View"/> -+ </actionGroup> -+ -+ <!-- Verify that Image 1 is deleted from the Second Store View list --> -+ <actionGroup ref="assertProductImageNotInAdminProductPage" stepKey="assertProductImageNotInSecondStoreViewPage"> -+ <argument name="image" value="TestImageNew"/> -+ </actionGroup> -+ </test> -+</tests> -diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminRemoveImageFromCategoryTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminRemoveImageFromCategoryTest.xml -index c68a848fb0f..fb33e183799 100644 ---- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminRemoveImageFromCategoryTest.xml -+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminRemoveImageFromCategoryTest.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="AdminRemoveImageFromCategoryTest"> - <annotations> - <features value="Catalog"/> -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 00000000000..240a5492355 ---- /dev/null -+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminRequiredFieldsHaveRequiredFieldIndicatorTest.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="AdminRequiredFieldsHaveRequiredFieldIndicatorTest"> -+ <annotations> -+ <stories value="Verify the presence of required field indicators across different pages in Magento Admin"/> -+ <title value="Required fields should have the required asterisk indicator "/> -+ <description value="Verify that Required fields should have the required indicator icon next to the field name"/> -+ <severity value="MAJOR"/> -+ <testCaseId value="MAGETWO-94330"/> -+ <group value="Catalog"/> -+ </annotations> -+ <after> -+ <amOnPage url="admin/admin/auth/logout/" stepKey="amOnLogoutPage"/> -+ </after> -+ -+ <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin1"/> -+ <amOnPage url="{{AdminCategoryPage.url}}" stepKey="navigateToCategoryPage"/> -+ <waitForElementVisible selector="{{AdminCategorySidebarActionSection.AddSubcategoryButton}}" stepKey="waitForAddSubCategoryVisible"/> -+ <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" message="pass" stepKey="assertRequiredFieldIndicator1"/> -+ -+ <executeJS function="{{AdminCategoryBasicFieldSection.RequiredFieldIndicatorColor}}" stepKey="getRequiredFieldIndicatorColor"/> -+ <assertEquals expected="rgb(226, 38, 38)" expectedType="string" actualType="variable" actual="getRequiredFieldIndicatorColor" message="pass" stepKey="assertRequiredFieldIndicator2"/> -+ -+ <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" message="pass" stepKey="assertRequiredFieldIndicator3"/> -+ <executeJS function="{{AdminProductFormSection.RequiredSkuIndicator}}" stepKey="productSkuRequiredFieldIndicator"/> -+ <assertEquals expected='"*"' expectedType="string" actualType="variable" actual="productSkuRequiredFieldIndicator" message="pass" stepKey="assertRequiredFieldIndicator4"/> -+ -+ <!-- Verify that the CMS page have the required field name indicator next to Page Title --> -+ <amOnPage url="{{CmsPagesPage.url}}" stepKey="amOnPagePagesGrid"/> -+ <waitForPageLoad stepKey="waitForPageLoad1"/> -+ <click selector="{{CmsPagesPageActionsSection.addNewPageButton}}" stepKey="clickAddNewPage"/> -+ <executeJS function="{{CmsNewPagePageBasicFieldsSection.RequiredFieldIndicator}}" stepKey="pageTitleRequiredFieldIndicator"/> -+ <assertEquals expected='"*"' expectedType="string" actualType="variable" actual="pageTitleRequiredFieldIndicator" message="pass" stepKey="assertRequiredFieldIndicator5"/> -+ -+ </test> -+</tests> -diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminRestrictedUserAddCategoryFromProductPageTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminRestrictedUserAddCategoryFromProductPageTest.xml -new file mode 100644 -index 00000000000..cd401b7a465 ---- /dev/null -+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminRestrictedUserAddCategoryFromProductPageTest.xml -@@ -0,0 +1,110 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> -+ <test name="AdminRestrictedUserAddCategoryFromProductPageTest"> -+ <annotations> -+ <features value="Catalog"/> -+ <title value="Adding new category from product page by restricted user"/> -+ <description value="Adding new category from product page by restricted user"/> -+ <severity value="MAJOR"/> -+ <testCaseId value="MC-17229"/> -+ <useCaseId value="MAGETWO-69893"/> -+ <group value="catalog"/> -+ </annotations> -+ <before> -+ <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> -+ <!--Create category--> -+ <comment userInput="Create category" stepKey="commentCreateCategory"/> -+ <createData entity="SimpleSubCategory" stepKey="createCategory"/> -+ </before> -+ <after> -+ <!--Delete created product--> -+ <comment userInput="Delete created product" stepKey="commentDeleteProduct"/> -+ <actionGroup ref="deleteProductBySku" stepKey="deleteProduct"> -+ <argument name="sku" value="{{_defaultProduct.sku}}"/> -+ </actionGroup> -+ <actionGroup ref="resetProductGridToDefaultView" stepKey="resetFiltersIfExist"/> -+ <actionGroup ref="logout" stepKey="logoutOfUser"/> -+ <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> -+ <!--Delete created data--> -+ <comment userInput="Delete created data" stepKey="commentDeleteCreatedData"/> -+ <amOnPage url="{{AdminRolesPage.url}}" stepKey="navigateToUserRoleGrid" /> -+ <waitForPageLoad stepKey="waitForRolesGridLoad" /> -+ <actionGroup ref="AdminDeleteRoleActionGroup" stepKey="deleteUserRole"> -+ <argument name="role" value="adminRole"/> -+ </actionGroup> -+ <amOnPage url="{{AdminUsersPage.url}}" stepKey="goToAllUsersPage"/> -+ <waitForPageLoad stepKey="waitForUsersGridLoad" /> -+ <actionGroup ref="AdminDeleteNewUserActionGroup" stepKey="deleteUser"> -+ <argument name="userName" value="{{admin2.username}}"/> -+ </actionGroup> -+ <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> -+ <actionGroup ref="logout" stepKey="logoutOfAdmin"/> -+ </after> -+ <!--Create user role--> -+ <comment userInput="Create user role" stepKey="commentCreateUserRole"/> -+ <actionGroup ref="AdminFillUserRoleRequiredData" stepKey="fillUserRoleRequiredData"> -+ <argument name="User" value="adminRole"/> -+ <argument name="restrictedRole" value="Stores"/> -+ </actionGroup> -+ <click selector="{{AdminEditRoleInfoSection.roleResourcesTab}}" stepKey="clickRoleResourcesTab" /> -+ <actionGroup ref="AdminAddRestrictedRole" stepKey="addRestrictedRoleStores"> -+ <argument name="User" value="adminRole"/> -+ <argument name="restrictedRole" value="Stores"/> -+ </actionGroup> -+ <actionGroup ref="AdminAddRestrictedRole" stepKey="addRestrictedRoleProducts"> -+ <argument name="User" value="adminRole"/> -+ <argument name="restrictedRole" value="Products"/> -+ </actionGroup> -+ <click selector="{{AdminEditRoleInfoSection.saveButton}}" stepKey="clickSaveRoleButton" /> -+ <see userInput="You saved the role." stepKey="seeUserRoleSavedMessage"/> -+ <!--Create user and assign role to it--> -+ <comment userInput="Create user and assign role to it" stepKey="commentCreateUser"/> -+ <actionGroup ref="AdminCreateUserActionGroup" stepKey="createAdminUser"> -+ <argument name="role" value="adminRole"/> -+ <argument name="User" value="admin2"/> -+ </actionGroup> -+ <!--Log out of admin and login with newly created user--> -+ <comment userInput="Log out of admin and login with newly created user" stepKey="commentLoginWithNewUser"/> -+ <actionGroup ref="logout" stepKey="logoutOfAdmin"/> -+ <actionGroup ref="LoginAsAdmin" stepKey="loginAsNewUser"> -+ <argument name="adminUser" value="admin2"/> -+ </actionGroup> -+ <!--Go to create product page--> -+ <comment userInput="Go to create product page" stepKey="commentGoCreateProductPage"/> -+ <actionGroup ref="goToCreateProductPage" stepKey="goToCreateProductPage"/> -+ <dontSeeElement selector="{{AdminProductFormSection.newCategoryButton}}" stepKey="dontSeeNewCategoryButton"/> -+ <!--Fill product data and assign to category--> -+ <comment userInput="Fill product data and assign to category" stepKey="commentFillProductData"/> -+ <actionGroup ref="fillMainProductForm" stepKey="fillMainProductForm"/> -+ <actionGroup ref="SetCategoryByName" stepKey="addCategoryToProduct"> -+ <argument name="categoryName" value="$$createCategory.name$$"/> -+ </actionGroup> -+ <actionGroup ref="saveProductForm" stepKey="saveProduct"/> -+ <!--Assert that category exist in field--> -+ <comment userInput="Assert that category exist in field" stepKey="commentAssertion"/> -+ <grabTextFrom selector="{{AdminProductFormSection.categoriesDropdown}}" stepKey="grabCategoryName"/> -+ <assertContains stepKey="assertThatCategory"> -+ <expectedResult type="variable">$$createCategory.name$$</expectedResult> -+ <actualResult type="variable">$grabCategoryName</actualResult> -+ </assertContains> -+ <!--Remove the category from the product and assert that it removed--> -+ <comment userInput="Remove the category from the product and assert that it removed" stepKey="assertCategoryRemoved"/> -+ <actionGroup ref="removeCategoryFromProduct" stepKey="removeCategoryFromProduct"> -+ <argument name="categoryName" value="$$createCategory.name$$"/> -+ </actionGroup> -+ <actionGroup ref="saveProductForm" stepKey="saveProductAfterRemovingCategory"/> -+ <grabTextFrom selector="{{AdminProductFormSection.categoriesDropdown}}" stepKey="grabCategoryFieldContent"/> -+ <assertNotContains stepKey="assertThatCategoryRemoved"> -+ <expectedResult type="variable">$$createCategory.name$$</expectedResult> -+ <actualResult type="variable">$grabCategoryFieldContent</actualResult> -+ </assertNotContains> -+ </test> -+</tests> -diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminSimpleProductEditUiTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminSimpleProductEditUiTest.xml -index 2b9733fd2fa..bc5a0319bae 100644 ---- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminSimpleProductEditUiTest.xml -+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminSimpleProductEditUiTest.xml -@@ -7,13 +7,16 @@ - --> - - <tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -- xsi:noNamespaceSchemaLocation="../../../../../../vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Test/etc/testSchema.xsd"> -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> - <test name="AdminSimpleProductUiValidationTest"> - <annotations> -+ <features value="Catalog"/> -+ <stories value="Edit products"/> - <title value="UI elements on the simple product edit screen should be organized as expected"/> - <description value="Admin should be able to use simple product UI in expected manner"/> - <testCaseId value="MAGETWO-92835"/> - <group value="Catalog"/> -+ <severity value="AVERAGE"/> - </annotations> - - <before> -diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminSimpleProductImagesTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminSimpleProductImagesTest.xml -index b51f6a6e432..1cd0e15780c 100644 ---- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminSimpleProductImagesTest.xml -+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminSimpleProductImagesTest.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="AdminSimpleProductImagesTest"> - <annotations> - <features value="Catalog"/> -diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminSimpleProductSetEditContentTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminSimpleProductSetEditContentTest.xml -new file mode 100644 -index 00000000000..f5e5911352c ---- /dev/null -+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminSimpleProductSetEditContentTest.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="AdminSimpleProductSetEditContentTest"> -+ <annotations> -+ <features value="Catalog"/> -+ <stories value="Create/edit simple product"/> -+ <title value="Admin should be able to set/edit product Content when editing a simple product"/> -+ <description value="Admin should be able to set/edit product Content when editing a simple product"/> -+ <severity value="CRITICAL"/> -+ <testCaseId value="MC-3422"/> -+ <group value="Catalog"/> -+ <group value="WYSIWYGDisabled"/> -+ </annotations> -+ <before> -+ <!--Admin Login--> -+ <actionGroup stepKey="loginToAdminPanel" ref="LoginAsAdmin"/> -+ <actionGroup ref="DisabledWYSIWYG" stepKey="disableWYSIWYG"/> -+ </before> -+ <after> -+ <!-- Delete simple product --> -+ <actionGroup ref="deleteProductUsingProductGrid" stepKey="deleteProduct"> -+ <argument name="product" value="SimpleProduct"/> -+ </actionGroup> -+ <!--Admin Logout--> -+ <actionGroup ref="logout" stepKey="logout"/> -+ </after> -+ -+ <!-- Create product --> -+ <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="navigateToProductIndex"/> -+ <waitForPageLoad stepKey="waitForProductIndexPage"/> -+ <actionGroup ref="goToCreateProductPage" stepKey="goToCreateProduct"> -+ <argument name="product" value="SimpleProduct"/> -+ </actionGroup> -+ <actionGroup ref="fillMainProductFormNoWeight" stepKey="fillProductForm"> -+ <argument name="product" value="SimpleProduct"/> -+ </actionGroup> -+ -+ <!--Add content--> -+ <!--A generic scroll scrolls past this element, in doing this it fails to execute certain actions on the element and others below it. By scrolling slightly above it it resolves this issue.--> -+ <scrollTo selector="{{AdminProductContentSection.sectionHeader}}" x="0" y="-100" stepKey="scrollTo"/> -+ <click selector="{{AdminProductContentSection.sectionHeader}}" stepKey="openDescriptionDropDown"/> -+ <fillField selector="{{AdminProductContentSection.descriptionTextArea}}" userInput="This is the long description" stepKey="fillLongDescription"/> -+ <fillField selector="{{AdminProductContentSection.shortDescriptionTextArea}}" userInput="This is the short description" stepKey="fillShortDescription"/> -+ -+ <!--save the product--> -+ <click selector="{{AdminProductFormActionSection.saveButton}}" stepKey="clickSaveButton"/> -+ <see selector="{{AdminCategoryMessagesSection.SuccessMessage}}" userInput="You saved the product." stepKey="messageYouSavedTheProductIsShown"/> -+ -+ <!--Edit content--> -+ <click selector="{{AdminProductContentSection.sectionHeader}}" stepKey="openDescriptionDropDownEdit"/> -+ <scrollTo selector="{{AdminProductContentSection.sectionHeader}}" stepKey="scrollToEdit"/> -+ <fillField selector="{{AdminProductContentSection.descriptionTextArea}}" userInput="EDIT ~ This is the long description ~ EDIT" stepKey="editLongDescription"/> -+ <fillField selector="{{AdminProductContentSection.shortDescriptionTextArea}}" userInput="EDIT ~ This is the short description ~ EDIT" stepKey="editShortDescription"/> -+ -+ <!--save the product--> -+ <click selector="{{AdminProductFormActionSection.saveButton}}" stepKey="clickSaveButtonAfterEdit"/> -+ <see selector="{{AdminCategoryMessagesSection.SuccessMessage}}" userInput="You saved the product." stepKey="messageYouSavedTheProductIsShownAgain"/> -+ -+ <!--Checking content admin--> -+ <click selector="{{AdminProductContentSection.sectionHeader}}" stepKey="openDescriptionDropDownAgain"/> -+ <scrollTo selector="{{AdminProductContentSection.sectionHeader}}" stepKey="scrollToAgain"/> -+ <seeInField selector="{{AdminProductContentSection.descriptionTextArea}}" userInput="EDIT ~ This is the long description ~ EDIT" stepKey="seeLongDescriptionAdmin"/> -+ <seeInField selector="{{AdminProductContentSection.shortDescriptionTextArea}}" userInput="EDIT ~ This is the short description ~ EDIT" stepKey="seeShortDescriptionAdmin"/> -+ -+ <!--Checking content storefront--> -+ <amOnPage url="{{SimpleProduct.sku}}.html" stepKey="goToStorefront"/> -+ <waitForPageLoad stepKey="waitForStorefront"/> -+ <see selector="{{StorefrontProductInfoMainSection.productDescription}}" userInput="EDIT ~ This is the long description ~ EDIT" stepKey="seeLongDescriptionStorefront"/> -+ <see selector="{{StorefrontProductInfoMainSection.productShortDescription}}" userInput="EDIT ~ This is the short description ~ EDIT" stepKey="seeShortDescriptionStorefront"/> -+ </test> -+</tests> -diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminSimpleSetEditRelatedProductsTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminSimpleSetEditRelatedProductsTest.xml -new file mode 100644 -index 00000000000..b06502ce94c ---- /dev/null -+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminSimpleSetEditRelatedProductsTest.xml -@@ -0,0 +1,77 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> -+ <test name="AdminSimpleSetEditRelatedProductsTest"> -+ <annotations> -+ <features value="Catalog"/> -+ <stories value="Create/edit simple product"/> -+ <title value="Admin should be able to set/edit Related Products information when editing a simple product"/> -+ <description value="Admin should be able to set/edit Related Products information when editing a simple product"/> -+ <severity value="CRITICAL"/> -+ <testCaseId value="MC-3411"/> -+ <group value="Catalog"/> -+ </annotations> -+ <before> -+ <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> -+ <createData entity="SimpleProduct2" stepKey="simpleProduct0"/> -+ <createData entity="SimpleProduct2" stepKey="simpleProduct1"/> -+ </before> -+ <after> -+ <!-- Delete simple product --> -+ <actionGroup ref="deleteProductUsingProductGrid" stepKey="deleteProduct"> -+ <argument name="product" value="SimpleProduct3"/> -+ </actionGroup> -+ <amOnPage url="{{AdminLogoutPage.url}}" stepKey="amOnLogoutPage"/> -+ <deleteData createDataKey="simpleProduct0" stepKey="deleteSimpleProduct0"/> -+ <deleteData createDataKey="simpleProduct1" stepKey="deleteSimpleProduct1"/> -+ </after> -+ -+ <!--Create product--> -+ <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="navigateToProductIndex"/> -+ <waitForPageLoad stepKey="waitForProductIndexPage"/> -+ <actionGroup ref="goToCreateProductPage" stepKey="goToCreateProduct"> -+ <argument name="product" value="SimpleProduct3"/> -+ </actionGroup> -+ <actionGroup ref="fillMainProductFormNoWeight" stepKey="fillProductForm"> -+ <argument name="product" value="SimpleProduct3"/> -+ </actionGroup> -+ -+ <!--Add related product--> -+ <actionGroup ref="addRelatedProductBySku" stepKey="addRelatedProduct0"> -+ <argument name="sku" value="$$simpleProduct0.sku$$"/> -+ </actionGroup> -+ -+ <!--Save the product--> -+ <click selector="{{AdminProductFormActionSection.saveButton}}" stepKey="clickSaveButton"/> -+ <see selector="{{AdminCategoryMessagesSection.SuccessMessage}}" userInput="You saved the product." stepKey="messageYouSavedTheProductIsShown"/> -+ -+ <!--Add another related product--> -+ <actionGroup ref="addRelatedProductBySku" stepKey="addRelatedProduct1"> -+ <argument name="sku" value="$$simpleProduct1.sku$$"/> -+ </actionGroup> -+ -+ <!--Remove previous related product--> -+ <click selector="{{AdminProductFormRelatedUpSellCrossSellSection.removeRelatedProduct($$simpleProduct0.sku$$)}}" stepKey="removeRelatedProduct"/> -+ -+ <!--Save the product--> -+ <click selector="{{AdminProductFormActionSection.saveButton}}" stepKey="clickSaveButtonAfterEdit"/> -+ <see selector="{{AdminCategoryMessagesSection.SuccessMessage}}" userInput="You saved the product." stepKey="messageYouSavedTheProductIsShownAgain"/> -+ -+ <!--See related product in admin--> -+ <scrollTo selector="{{AdminProductFormRelatedUpSellCrossSellSection.relatedDropdown}}" stepKey="scrollTo"/> -+ <conditionalClick selector="{{AdminProductFormRelatedUpSellCrossSellSection.relatedDropdown}}" dependentSelector="{{AdminProductFormRelatedUpSellCrossSellSection.relatedDependent}}" visible="false" stepKey="openDropDownIfClosedRelatedSee"/> -+ <see selector="{{AdminProductFormRelatedUpSellCrossSellSection.selectedRelatedProduct}}" userInput="$$simpleProduct1.sku$$" stepKey="seeRelatedProduct"/> -+ -+ <!--See related product in storefront--> -+ <amOnPage url="{{SimpleProduct3.sku}}.html" stepKey="goToStorefront"/> -+ <waitForPageLoad stepKey="waitForStorefront"/> -+ <seeElement selector="{{StorefrontProductRelatedProductsSection.relatedProductName($$simpleProduct1.sku$$)}}" stepKey="seeRelatedProductInStorefront"/> -+ </test> -+</tests> -diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminSortingByWebsitesTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminSortingByWebsitesTest.xml -new file mode 100644 -index 00000000000..eb014ca7f88 ---- /dev/null -+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminSortingByWebsitesTest.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="AdminSortingByWebsitesTest"> -+ <annotations> -+ <stories value="View sorting by websites"/> -+ <title value="Sorting by websites in Admin"/> -+ <description value="Sorting products by websites in Admin"/> -+ <severity value="AVERAGE"/> -+ </annotations> -+ <before> -+ <createData entity="_defaultCategory" stepKey="createCategory"/> -+ <createData entity="_defaultProduct" stepKey="productAssignedToCustomWebsite"> -+ <requiredEntity createDataKey="createCategory"/> -+ </createData> -+ <createData entity="SimpleProduct" stepKey="productAssignedToMainWebsite"> -+ <requiredEntity createDataKey="createCategory"/> -+ </createData> -+ -+ <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> -+ <!--Create new website --> -+ <actionGroup ref="AdminCreateWebsiteActionGroup" stepKey="createAdditionalWebsite"> -+ <argument name="newWebsiteName" value="{{customWebsite.name}}"/> -+ <argument name="websiteCode" value="{{customWebsite.code}}"/> -+ </actionGroup> -+ <actionGroup ref="EnableWebUrlOptions" stepKey="addStoreCodeToUrls"/> -+ <magentoCLI command="cache:flush" stepKey="flushCacheAfterEnableWebUrlOptions"/> -+ </before> -+ <after> -+ <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> -+ <deleteData createDataKey="productAssignedToCustomWebsite" stepKey="deleteProductAssignedToCustomWebsite"/> -+ <deleteData createDataKey="productAssignedToMainWebsite" stepKey="deleteProductAssignedToMainWebsite"/> -+ <actionGroup ref="AdminDeleteWebsiteActionGroup" stepKey="deleteTestWebsite"> -+ <argument name="websiteName" value="{{customWebsite.name}}"/> -+ </actionGroup> -+ <actionGroup ref="ResetWebUrlOptions" stepKey="resetUrlOption"/> -+ <magentoCLI command="indexer:reindex" stepKey="reindex"/> -+ <magentoCLI command="cache:flush" stepKey="flushCache"/> -+ <actionGroup ref="logout" stepKey="logout"/> -+ </after> -+ -+ <!--Assign Custom Website to Simple Product --> -+ <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="navigateToCatalogProductGrid"/> -+ <waitForPageLoad stepKey="waitForCatalogProductGrid"/> -+ -+ <conditionalClick selector="{{AdminProductGridFilterSection.clearFilters}}" dependentSelector="{{AdminProductGridFilterSection.clearFilters}}" visible="true" stepKey="clickClearFiltersInitial"/> -+ <actionGroup ref="OpenEditProductOnBackendActionGroup" stepKey="assignCustomWebsiteToProduct"> -+ <argument name="product" value="$$productAssignedToCustomWebsite$$"/> -+ </actionGroup> -+ <scrollTo selector="{{ProductInWebsitesSection.sectionHeader}}" stepKey="scrollToWebsites"/> -+ <conditionalClick selector="{{ProductInWebsitesSection.sectionHeader}}" dependentSelector="{{AdminProductContentSection.sectionHeaderShow}}" visible="false" stepKey="expandSection"/> -+ <waitForPageLoad stepKey="waitForPageOpened"/> -+ <uncheckOption selector="{{ProductInWebsitesSection.website(_defaultWebsite.name)}}" stepKey="deselectMainWebsite"/> -+ <checkOption selector="{{ProductInWebsitesSection.website(customWebsite.name)}}" stepKey="selectWebsite"/> -+ -+ <click selector="{{AdminProductFormActionSection.saveButton}}" stepKey="clickSave"/> -+ <waitForLoadingMaskToDisappear stepKey="waitForProductPageToSaveAgain"/> -+ <seeElement selector="{{AdminProductMessagesSection.successMessage}}" stepKey="seeSaveProductMessageAgain"/> -+ -+ <!--Navigate To Product Grid To Check Website Sorting--> -+ <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="navigateToCatalogProductGridToSortByWebsite"/> -+ <waitForPageLoad stepKey="waitForCatalogProductGridLoaded"/> -+ -+ <!--Sorting works (By Websites) ASC--> -+ <click selector="{{AdminProductGridSection.columnHeader('Websites')}}" stepKey="clickWebsitesHeaderToSortAsc"/> -+ <see selector="{{AdminProductGridSection.productGridContentsOnRow('1')}}" userInput="Main Website" stepKey="checkIfProduct1WebsitesAsc"/> -+ -+ <!--Sorting works (By Websites) DESC--> -+ <click selector="{{AdminProductGridSection.columnHeader('Websites')}}" stepKey="clickWebsitesHeaderToSortDesc"/> -+ <see selector="{{AdminProductGridSection.productGridContentsOnRow('1')}}" userInput="{{customWebsite.name}}" stepKey="checkIfProduct1WebsitesDesc"/> -+ </test> -+</tests> -diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminStoresAttributeSetNavigateMenuTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminStoresAttributeSetNavigateMenuTest.xml -new file mode 100644 -index 00000000000..ed29c281b80 ---- /dev/null -+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminStoresAttributeSetNavigateMenuTest.xml -@@ -0,0 +1,36 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> -+ <test name="AdminStoresAttributeSetNavigateMenuTest"> -+ <annotations> -+ <features value="Catalog"/> -+ <stories value="Menu Navigation"/> -+ <title value="Admin stores attribute set navigate menu test"/> -+ <description value="Admin should be able to navigate to Stores > Attribute Set"/> -+ <severity value="CRITICAL"/> -+ <testCaseId value="MC-14133"/> -+ <group value="menu"/> -+ <group value="mtf_migrated"/> -+ </annotations> -+ <before> -+ <actionGroup ref="LoginAsAdmin" stepKey="LoginAsAdmin"/> -+ </before> -+ <after> -+ <actionGroup ref="logout" stepKey="logout"/> -+ </after> -+ <actionGroup ref="AdminNavigateMenuActionGroup" stepKey="navigateToAttributeSetPage"> -+ <argument name="menuUiId" value="{{AdminMenuStores.dataUiId}}"/> -+ <argument name="submenuUiId" value="{{AdminMenuStoresAttributesAttributeSet.dataUiId}}"/> -+ </actionGroup> -+ <actionGroup ref="AdminAssertPageTitleActionGroup" stepKey="seePageTitle"> -+ <argument name="title" value="{{AdminMenuStoresAttributesAttributeSet.pageTitle}}"/> -+ </actionGroup> -+ </test> -+</tests> -diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminStoresProductNavigateMenuTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminStoresProductNavigateMenuTest.xml -new file mode 100644 -index 00000000000..28a33c4f20c ---- /dev/null -+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminStoresProductNavigateMenuTest.xml -@@ -0,0 +1,36 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> -+ <test name="AdminStoresProductNavigateMenuTest"> -+ <annotations> -+ <features value="Catalog"/> -+ <stories value="Menu Navigation"/> -+ <title value="Admin stores product navigate menu test"/> -+ <description value="Admin should be able to navigate to Stores > Product"/> -+ <severity value="CRITICAL"/> -+ <testCaseId value="MC-14132"/> -+ <group value="menu"/> -+ <group value="mtf_migrated"/> -+ </annotations> -+ <before> -+ <actionGroup ref="LoginAsAdmin" stepKey="LoginAsAdmin"/> -+ </before> -+ <after> -+ <actionGroup ref="logout" stepKey="logout"/> -+ </after> -+ <actionGroup ref="AdminNavigateMenuActionGroup" stepKey="navigateToProductAttributePage"> -+ <argument name="menuUiId" value="{{AdminMenuStores.dataUiId}}"/> -+ <argument name="submenuUiId" value="{{AdminMenuStoresAttributesProduct.dataUiId}}"/> -+ </actionGroup> -+ <actionGroup ref="AdminAssertPageTitleActionGroup" stepKey="seePageTitle"> -+ <argument name="title" value="{{AdminMenuStoresAttributesProduct.pageTitle}}"/> -+ </actionGroup> -+ </test> -+</tests> -diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminTierPriceNotAvailableForProductOptionsWithoutTierPriceTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminTierPriceNotAvailableForProductOptionsWithoutTierPriceTest.xml -new file mode 100644 -index 00000000000..4bcb82372e8 ---- /dev/null -+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminTierPriceNotAvailableForProductOptionsWithoutTierPriceTest.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="AdminTierPriceNotAvailableForProductOptionsWithoutTierPriceTest"> -+ <annotations> -+ <features value="Catalog"/> -+ <stories value="Tier price"/> -+ <title value="Check that 'tier price' block not available for simple product from options without 'tier price'"/> -+ <description value="Check that 'tier price' block not available for simple product from options without 'tier price'"/> -+ <severity value="MAJOR"/> -+ <testCaseId value="MAGETWO-97050"/> -+ <useCaseId value="MAGETWO-96842"/> -+ <group value="catalog"/> -+ </annotations> -+ <before> -+ <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> -+ -+ <!--Create category--> -+ <createData entity="SimpleSubCategory" stepKey="createCategory"/> -+ -+ <!-- Create the configurable product based on the data in the /data folder --> -+ <createData entity="ApiConfigurableProduct" stepKey="createConfigProduct"> -+ <requiredEntity createDataKey="createCategory"/> -+ </createData> -+ -+ <!-- Make the configurable product have two options, that are children of the default attribute set --> -+ <createData entity="productAttributeWithTwoOptions" stepKey="createConfigProductAttribute"/> -+ <createData entity="productAttributeOption1" stepKey="createConfigProductAttributeOption1"> -+ <requiredEntity createDataKey="createConfigProductAttribute"/> -+ </createData> -+ <createData entity="productAttributeOption2" stepKey="createConfigProductAttributeOption2"> -+ <requiredEntity createDataKey="createConfigProductAttribute"/> -+ </createData> -+ <createData entity="AddToDefaultSet" stepKey="createConfigAddToAttributeSet"> -+ <requiredEntity createDataKey="createConfigProductAttribute"/> -+ </createData> -+ <getData entity="ProductAttributeOptionGetter" index="1" stepKey="getConfigAttributeOption1"> -+ <requiredEntity createDataKey="createConfigProductAttribute"/> -+ </getData> -+ <getData entity="ProductAttributeOptionGetter" index="2" stepKey="getConfigAttributeOption2"> -+ <requiredEntity createDataKey="createConfigProductAttribute"/> -+ </getData> -+ -+ <!-- Create the 2 children that will be a part of the configurable product --> -+ <createData entity="ApiSimpleOne" stepKey="createConfigChildProduct1"> -+ <requiredEntity createDataKey="createConfigProductAttribute"/> -+ <requiredEntity createDataKey="getConfigAttributeOption1"/> -+ </createData> -+ <createData entity="ApiSimpleTwo" stepKey="createConfigChildProduct2"> -+ <requiredEntity createDataKey="createConfigProductAttribute"/> -+ <requiredEntity createDataKey="getConfigAttributeOption2"/> -+ </createData> -+ -+ <!-- Assign the two products to the configurable product --> -+ <createData entity="ConfigurableProductTwoOptions" stepKey="createConfigProductOption"> -+ <requiredEntity createDataKey="createConfigProduct"/> -+ <requiredEntity createDataKey="createConfigProductAttribute"/> -+ <requiredEntity createDataKey="getConfigAttributeOption1"/> -+ <requiredEntity createDataKey="getConfigAttributeOption2"/> -+ </createData> -+ <createData entity="ConfigurableProductAddChild" stepKey="createConfigProductAddChild1"> -+ <requiredEntity createDataKey="createConfigProduct"/> -+ <requiredEntity createDataKey="createConfigChildProduct1"/> -+ </createData> -+ <createData entity="ConfigurableProductAddChild" stepKey="createConfigProductAddChild2"> -+ <requiredEntity createDataKey="createConfigProduct"/> -+ <requiredEntity createDataKey="createConfigChildProduct2"/> -+ </createData> -+ </before> -+ <after> -+ <!--Delete created data--> -+ <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> -+ <deleteData createDataKey="createConfigProduct" stepKey="deleteConfigProduct"/> -+ <deleteData createDataKey="createConfigChildProduct1" stepKey="deleteConfigChildProduct1"/> -+ <deleteData createDataKey="createConfigChildProduct2" stepKey="deleteConfigChildProduct2"/> -+ <deleteData createDataKey="createConfigProductAttribute" stepKey="deleteConfigProductAttribute"/> -+ -+ <actionGroup ref="logout" stepKey="logoutOfAdmin"/> -+ </after> -+ -+ <!--Go to storefront product page an check price box css--> -+ <amOnPage url="{{StorefrontProductPage.url($$createConfigProduct.custom_attributes[url_key]$$)}}" stepKey="navigateToSimpleProductPage"/> -+ <waitForPageLoad stepKey="waitForStoreFrontLoad"/> -+ <selectOption selector="{{StorefrontProductInfoMainSection.productAttributeOptionsSelectButton}}" userInput="$$getConfigAttributeOption1.value$$" stepKey="selectOption"/> -+ <grabAttributeFrom selector="{{StorefrontProductInfoMainSection.productPrice}}" userInput="class" stepKey="grabGrabPriceClass"/> -+ <assertNotContains actual="$grabGrabPriceClass" expected=".price-box .price-tier_price" expectedType="string" stepKey="assertNotEquals"/> -+ </test> -+</tests> -diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUnassignProductAttributeFromAttributeSetTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUnassignProductAttributeFromAttributeSetTest.xml -index 5a07b11a204..a33c7bb1287 100644 ---- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUnassignProductAttributeFromAttributeSetTest.xml -+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUnassignProductAttributeFromAttributeSetTest.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="AdminUnassignProductAttributeFromAttributeSetTest"> - <annotations> - <features value="Catalog"/> -@@ -17,33 +17,25 @@ - <severity value="CRITICAL"/> - <testCaseId value="MC-194"/> - <group value="Catalog"/> -- <skip> -- <issueId value="MAGETWO-92780"/> -- </skip> - </annotations> - <before> - <createData entity="productDropDownAttribute" stepKey="attribute"/> -- - <createData entity="productAttributeOption1" stepKey="option1"> - <requiredEntity createDataKey="attribute"/> - </createData> - <createData entity="productAttributeOption2" stepKey="option2"> - <requiredEntity createDataKey="attribute"/> - </createData> -- - <createData entity="AddToDefaultSet" stepKey="addToDefaultSet"> - <requiredEntity createDataKey="attribute"/> - </createData> -- - <createData entity="ApiProductWithDescription" stepKey="product"/> -- - <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> - </before> - <after> - <deleteData createDataKey="attribute" stepKey="deleteAttribute"/> - <actionGroup ref="logout" stepKey="logout"/> - </after> -- - <!-- Assert attribute presence in storefront product additional information --> - <amOnPage url="/$$product.custom_attributes[url_key]$$.html" stepKey="onProductPage1"/> - <waitForPageLoad stepKey="wait1"/> -@@ -64,6 +56,8 @@ - <see userInput="$$attribute.attribute_code$$" selector="{{AdminProductAttributeSetEditSection.unassignedAttributesTree}}" stepKey="seeAttributeInUnassigned"/> - <!-- Save attribute set --> - <actionGroup ref="SaveAttributeSet" stepKey="SaveAttributeSet"/> -+ <!-- Clear cache --> -+ <actionGroup ref="ClearPageCacheActionGroup" stepKey="clearPageCacheActionGroup"/> - <!-- Go to create new product page --> - <amOnPage url="{{AdminProductCreatePage.url(AddToDefaultSet.attributeSetId, 'simple')}}" stepKey="navigateToNewProduct"/> - <waitForPageLoad stepKey="wait2"/> -@@ -72,8 +66,7 @@ - <!-- Assert removed attribute not presence in storefront product additional information --> - <amOnPage url="/$$product.custom_attributes[url_key]$$.html" stepKey="onProductPage2"/> - <waitForPageLoad stepKey="wait3"/> -- <actionGroup ref="checkAttributeNotInMoreInformationTab" stepKey="checkAttributeNotInMoreInformationTab"> -- <argument name="attributeLabel" value="$$attribute.attribute[frontend_labels][0][label]$$"/> -- </actionGroup> -+ <dontSeeElement selector="{{StorefrontProductMoreInformationSection.moreInformation}}" stepKey="dontSeeProductAttribute"/> -+ <dontSee userInput="$$attribute.attribute[frontend_labels][0][label]$$" selector="{{StorefrontProductMoreInformationSection.moreInformationTextArea}}" stepKey="dontSeeAttributeLabel"/> - </test> - </tests> -diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateCategoryAndCheckDefaultUrlKeyOnStoreViewTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateCategoryAndCheckDefaultUrlKeyOnStoreViewTest.xml -new file mode 100644 -index 00000000000..d8d462f850f ---- /dev/null -+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateCategoryAndCheckDefaultUrlKeyOnStoreViewTest.xml -@@ -0,0 +1,77 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> -+ <test name="AdminUpdateCategoryAndCheckDefaultUrlKeyOnStoreViewTest"> -+ <annotations> -+ <stories value="Update categories"/> -+ <title value="Update category, check default URL key on the custom store view"/> -+ <description value="Login as admin and update category and check default URL Key on custom store view"/> -+ <testCaseId value="MC-6063"/> -+ <severity value="CRITICAL"/> -+ <group value="Catalog"/> -+ <group value="mtf_migrated"/> -+ </annotations> -+ <before> -+ <actionGroup ref="LoginAsAdmin" stepKey="loginToAdminPanel"/> -+ <createData entity="NewRootCategory" stepKey="rootCategory"/> -+ <createData entity="SimpleRootSubCategory" stepKey="category"> -+ <requiredEntity createDataKey="rootCategory"/> -+ </createData> -+ </before> -+ <after> -+ <actionGroup ref="DeleteCustomStoreActionGroup" stepKey="deleteCustomStore"> -+ <argument name="storeGroupName" value="customStore.name"/> -+ </actionGroup> -+ <deleteData createDataKey="rootCategory" stepKey="deleteRootCategory"/> -+ <actionGroup ref="logout" stepKey="logout"/> -+ </after> -+ -+ <!--Open Store Page --> -+ <amOnPage url="{{AdminSystemStorePage.url}}" stepKey="amOnAdminSystemStorePage"/> -+ <waitForPageLoad stepKey="waitForSystemStorePage"/> -+ -+ <!--Create Custom Store --> -+ <click selector="{{AdminStoresMainActionsSection.createStoreButton}}" stepKey="selectCreateStore"/> -+ <fillField userInput="{{customStore.name}}" selector="{{AdminNewStoreGroupSection.storeGrpNameTextField}}" stepKey="fillStoreName"/> -+ <fillField userInput="{{customStore.code}}" selector="{{AdminNewStoreGroupSection.storeGrpCodeTextField}}" stepKey="fillStoreCode"/> -+ <selectOption userInput="{{NewRootCategory.name}}" selector="{{AdminNewStoreGroupSection.storeRootCategoryDropdown}}" stepKey="selectStoreStatus"/> -+ <click selector="{{AdminStoresMainActionsSection.saveButton}}" stepKey="clickSaveStoreButton"/> -+ -+ <!--Create Store View--> -+ <actionGroup ref="AdminCreateStoreViewActionGroup" stepKey="createStoreView"> -+ <argument name="StoreGroup" value="customStore"/> -+ <argument name="customStore" value="customStore"/> -+ </actionGroup> -+ -+ <!--Update Category--> -+ <amOnPage url="{{AdminCategoryPage.url}}" stepKey="openAdminCategoryIndexPage"/> -+ <waitForPageLoad stepKey="waitForPageToLoaded"/> -+ <click selector="{{AdminCategorySidebarTreeSection.expandAll}}" stepKey="clickOnExpandTree"/> -+ <click selector="{{AdminCategorySidebarTreeSection.categoryInTree(SimpleRootSubCategory.name)}}" stepKey="selectCategory"/> -+ <waitForPageLoad stepKey="waitForPageToLoad"/> -+ <fillField selector="{{AdminCategoryBasicFieldSection.CategoryNameInput}}" userInput="{{_defaultCategory.name}}" stepKey="updateCategoryName"/> -+ <click selector="{{AdminCategoryMainActionsSection.SaveButton}}" stepKey="saveUpdatedCategory"/> -+ <waitForPageLoad stepKey="waitForCateforyToSave"/> -+ <seeElement selector="{{AdminCategoryMessagesSection.SuccessMessage}}" stepKey="seeSuccessMessage"/> -+ <waitForPageLoad stepKey="waitForPageToLoad1"/> -+ <scrollTo selector="{{AdminCategorySEOSection.SectionHeader}}" x="0" y="-80" stepKey="scrollToSearchEngineOptimization1"/> -+ <click selector="{{AdminCategorySEOSection.SectionHeader}}" stepKey="selectSearchEngineOptimization1"/> -+ <waitForPageLoad stepKey="waitForPageToLoad2"/> -+ <seeInField selector="{{AdminCategorySEOSection.UrlKeyInput}}" stepKey="seeCategoryUrlKey" userInput="{{SimpleRootSubCategory.name_lwr}}2" /> -+ <!--Open Category in Store Front Page--> -+ <amOnPage url="/{{NewRootCategory.name}}/{{_defaultCategory.name}}.html" stepKey="seeTheCategoryInStoreFront"/> -+ <waitForPageLoad stepKey="waitForCategoryPageToLoad"/> -+ <click selector="{{StorefrontFooterSection.switchStoreButton}}" stepKey="clickSwitchStoreButtonOnDefaultStore"/> -+ <click selector="{{StorefrontFooterSection.storeLink(customStore.name)}}" stepKey="selectSecondStoreToSwitchOn"/> -+ <seeElement selector="{{StorefrontHeaderSection.NavigationCategoryByName(_defaultCategory.name)}}" stepKey="seeUpdatedCatergoryInStoreFront"/> -+ <click selector="{{StorefrontHeaderSection.NavigationCategoryByName(_defaultCategory.name)}}" stepKey="selectCategoryOnStoreFront"/> -+ <waitForPageLoad stepKey="waitForProductToLoad"/> -+ <seeElement selector="{{StorefrontCategoryMainSection.CategoryTitle(_defaultCategory.name)}}" stepKey="seeTheUpdatedCategoryTitle"/> -+ </test> -+</tests> -diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateCategoryAndMakeInactiveTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateCategoryAndMakeInactiveTest.xml -new file mode 100644 -index 00000000000..479249ca678 ---- /dev/null -+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateCategoryAndMakeInactiveTest.xml -@@ -0,0 +1,58 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> -+ <test name="AdminUpdateCategoryAndMakeInactiveTest"> -+ <annotations> -+ <stories value="Update categories"/> -+ <title value="Update category, make inactive"/> -+ <description value="Login as admin and update category and make it Inactive"/> -+ <testCaseId value="MC-6060"/> -+ <severity value="CRITICAL"/> -+ <group value="Catalog"/> -+ <group value="mtf_migrated"/> -+ </annotations> -+ <before> -+ <actionGroup ref="LoginAsAdmin" stepKey="loginToAdminPanel"/> -+ <createData entity="_defaultCategory" stepKey="createDefaultCategory"/> -+ </before> -+ <after> -+ <deleteData createDataKey="createDefaultCategory" stepKey="deleteCreatedCategory"/> -+ <actionGroup ref="logout" stepKey="logout"/> -+ </after> -+ -+ <!--Open category page--> -+ <amOnPage url="{{AdminCategoryPage.url}}" stepKey="openAdminCategoryIndexPage"/> -+ <waitForPageLoad stepKey="waitForPageToLoaded"/> -+ -+ <!--Update category and make category inactive--> -+ <click selector="{{AdminCategorySidebarTreeSection.expandAll}}" stepKey="clickOnExpandTree"/> -+ <click selector="{{AdminCategorySidebarTreeSection.categoryInTree(_defaultCategory.name)}}" stepKey="selectCreatedCategory"/> -+ <click selector="{{AdminCategoryBasicFieldSection.enableCategoryLabel}}" stepKey="disableCategory"/> -+ <click selector="{{AdminCategoryMainActionsSection.SaveButton}}" stepKey="clickSaveButton"/> -+ <waitForPageLoad stepKey="waitForCategorySaved"/> -+ <see selector="{{AdminCategoryMessagesSection.SuccessMessage}}" userInput="You saved the category." stepKey="assertSuccessMessage"/> -+ <see selector="{{AdminCategoryContentSection.categoryPageTitle}}" userInput="{{_defaultCategory.name}}" stepKey="seePageTitle" /> -+ <dontSeeCheckboxIsChecked selector="{{AdminCategoryBasicFieldSection.EnableCategory}}" stepKey="dontCategoryIsChecked"/> -+ -+ <!--Verify Inactive Category is store front page--> -+ <amOnPage url="{{StorefrontCategoryPage.url(_defaultCategory.name)}}" stepKey="amOnCategoryPage"/> -+ <waitForPageLoad stepKey="waitForPageToBeLoaded"/> -+ <dontSeeElement selector="{{StorefrontHeaderSection.NavigationCategoryByName(_defaultCategory.name)}}" stepKey="dontSeeCategoryOnStoreNavigationBar"/> -+ <waitForPageLoad time="15" stepKey="wait"/> -+ -+ <!--Verify Inactive Category in category page --> -+ <amOnPage url="{{AdminCategoryPage.url}}" stepKey="openAdminCategoryIndexPage1"/> -+ <waitForPageLoad stepKey="waitForPageToLoaded1"/> -+ <click selector="{{AdminCategorySidebarTreeSection.expandAll}}" stepKey="clickOnExpandTree1"/> -+ <seeElement selector="{{AdminCategoryContentSection.categoryInTree(_defaultCategory.name)}}" stepKey="assertCategoryInTree" /> -+ <click selector="{{AdminCategorySidebarTreeSection.categoryInTree(_defaultCategory.name)}}" stepKey="selectCreatedCategory1"/> -+ <see selector="{{AdminCategoryContentSection.categoryPageTitle}}" userInput="{{_defaultCategory.name}}" stepKey="seeCategoryPageTitle1" /> -+ <dontSeeCheckboxIsChecked selector="{{AdminCategoryBasicFieldSection.EnableCategory}}" stepKey="assertCategoryIsInactive"/> -+ </test> -+</tests> -diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateCategoryNameWithStoreViewTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateCategoryNameWithStoreViewTest.xml -new file mode 100644 -index 00000000000..2cb4a6b6dd4 ---- /dev/null -+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateCategoryNameWithStoreViewTest.xml -@@ -0,0 +1,82 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> -+ <test name="AdminUpdateCategoryNameWithStoreViewTest"> -+ <annotations> -+ <stories value="Update categories"/> -+ <title value="Update category, with custom store view"/> -+ <description value="Login as admin and update category name with custom Store View"/> -+ <testCaseId value="MC-6061"/> -+ <severity value="CRITICAL"/> -+ <group value="Catalog"/> -+ <group value="mtf_migrated"/> -+ </annotations> -+ <before> -+ <actionGroup ref="LoginAsAdmin" stepKey="loginToAdminPanel"/> -+ <createData entity="NewRootCategory" stepKey="rootCategory"/> -+ <createData entity="SimpleRootSubCategory" stepKey="category"> -+ <requiredEntity createDataKey="rootCategory"/> -+ </createData> -+ </before> -+ <after> -+ <actionGroup ref="DeleteCustomStoreActionGroup" stepKey="deleteCustomStore"> -+ <argument name="storeGroupName" value="customStore.name"/> -+ </actionGroup> -+ <deleteData createDataKey="rootCategory" stepKey="deleteRootCategory"/> -+ <actionGroup ref="logout" stepKey="logout"/> -+ </after> -+ -+ <!--Open store page --> -+ <amOnPage url="{{AdminSystemStorePage.url}}" stepKey="amOnAdminSystemStorePage"/> -+ <waitForPageLoad stepKey="waitForSystemStorePage"/> -+ -+ <!--Create Custom Store --> -+ <click selector="{{AdminStoresMainActionsSection.createStoreButton}}" stepKey="selectCreateStore"/> -+ <fillField userInput="{{customStore.name}}" selector="{{AdminNewStoreGroupSection.storeGrpNameTextField}}" stepKey="fillStoreName"/> -+ <fillField userInput="{{customStore.code}}" selector="{{AdminNewStoreGroupSection.storeGrpCodeTextField}}" stepKey="fillStoreCode"/> -+ <selectOption userInput="{{NewRootCategory.name}}" selector="{{AdminNewStoreGroupSection.storeRootCategoryDropdown}}" stepKey="selectStoreStatus"/> -+ <click selector="{{AdminStoresMainActionsSection.saveButton}}" stepKey="clickSaveStoreButton"/> -+ -+ <!--Create Store View--> -+ <actionGroup ref="AdminCreateStoreViewActionGroup" stepKey="createStoreView"> -+ <argument name="StoreGroup" value="customStore"/> -+ <argument name="customStore" value="customStore"/> -+ </actionGroup> -+ -+ <!--Verify created SubCAtegory is present on Store Front --> -+ <amOnPage url="/{{NewRootCategory.name}}/{{SimpleRootSubCategory.name}}.html" stepKey="seeTheCategoryInStoreFront"/> -+ <waitForPageLoad stepKey="waitForPageToLoad"/> -+ <click selector="{{StorefrontFooterSection.switchStoreButton}}" stepKey="ClickSwitchStoreButtonOnDefaultStore"/> -+ <click selector="{{StorefrontFooterSection.storeLink(customStore.name)}}" stepKey="SelectSecondStoreToSwitchOn"/> -+ <seeElement selector="{{StorefrontHeaderSection.NavigationCategoryByName(SimpleRootSubCategory.name)}}" stepKey="seeCatergoryInStoreFront"/> -+ -+ <!--Open Category Page--> -+ <amOnPage url="{{AdminCategoryPage.url}}" stepKey="openAdminCategoryIndexPage"/> -+ <waitForPageLoad stepKey="waitForPageToLoaded"/> -+ -+ <!--Update Category--> -+ <click selector="{{AdminCategorySidebarTreeSection.expandAll}}" stepKey="expandToSeeAllCategories"/> -+ <click selector="{{AdminCategorySidebarTreeSection.categoryInTreeUnderRoot(SimpleRootSubCategory.name)}}" stepKey="clickOnSubcategoryIsUndeRootCategory"/> -+ <waitForPageLoad stepKey="waitForPageToLoad1"/> -+ <fillField selector="{{AdminCategoryBasicFieldSection.CategoryNameInput}}" userInput="{{_defaultCategory.name}}" stepKey="updateCategoryName"/> -+ <click selector="{{AdminCategoryMainActionsSection.SaveButton}}" stepKey="saveUpdatedCategory"/> -+ <waitForPageLoad stepKey="waitForCateforyToSave"/> -+ <seeElement selector="{{AdminCategoryMessagesSection.SuccessMessage}}" stepKey="seeSuccessMessage"/> -+ -+ <!--Verify the Category is not present in Store Front--> -+ <amOnPage url="/{{NewRootCategory.name}}/{{SimpleRootSubCategory.name}}.html" stepKey="seeTheCategoryInStoreFront1"/> -+ <waitForPageLoad stepKey="waitForPageToLoaded2"/> -+ <dontSeeElement selector="{{StorefrontHeaderSection.NavigationCategoryByName(SimpleRootSubCategory.name)}}" stepKey="dontSeeCatergoryInStoreFront"/> -+ -+ <!--Verify the Updated Category is present in Store Front--> -+ <amOnPage url="/{{NewRootCategory.name}}/{{_defaultCategory.name}}.html" stepKey="seeTheUpdatedCategoryInStoreFront"/> -+ <waitForPageLoad stepKey="waitForPageToLoaded3"/> -+ <seeElement selector="{{StorefrontHeaderSection.NavigationCategoryByName(_defaultCategory.name)}}" stepKey="seeUpdatedCatergoryInStoreFront"/> -+ </test> -+</tests> -diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateCategoryStoreUrlKeyTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateCategoryStoreUrlKeyTest.xml -index 013b1b6d381..2ff83afa15e 100644 ---- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateCategoryStoreUrlKeyTest.xml -+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateCategoryStoreUrlKeyTest.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="AdminUpdateCategoryStoreUrlKeyTest"> - <annotations> - <features value="SEO-friendly URL Key Update"/> -diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateCategoryUrlKeyWithStoreViewTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateCategoryUrlKeyWithStoreViewTest.xml -new file mode 100644 -index 00000000000..e7c4a8a093e ---- /dev/null -+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateCategoryUrlKeyWithStoreViewTest.xml -@@ -0,0 +1,84 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> -+ <test name="AdminUpdateCategoryUrlKeyWithStoreViewTest"> -+ <annotations> -+ <stories value="Update categories"/> -+ <title value="Update category, URL key with custom store view"/> -+ <description value="Login as admin and update category URL Key with store view"/> -+ <testCaseId value="MC-6062"/> -+ <severity value="CRITICAL"/> -+ <group value="Catalog"/> -+ <group value="mtf_migrated"/> -+ </annotations> -+ <before> -+ <actionGroup ref="LoginAsAdmin" stepKey="loginToAdminPanel"/> -+ <createData entity="NewRootCategory" stepKey="rootCategory"/> -+ <createData entity="SimpleRootSubCategory" stepKey="category"> -+ <requiredEntity createDataKey="rootCategory"/> -+ </createData> -+ </before> -+ <after> -+ <actionGroup ref="DeleteCustomStoreActionGroup" stepKey="deleteCustomStore"> -+ <argument name="storeGroupName" value="customStore.name"/> -+ </actionGroup> -+ <deleteData createDataKey="rootCategory" stepKey="deleteRootCategory"/> -+ <actionGroup ref="logout" stepKey="logout"/> -+ </after> -+ -+ <!--Open Store Page --> -+ <amOnPage url="{{AdminSystemStorePage.url}}" stepKey="amOnAdminSystemStorePage"/> -+ <waitForPageLoad stepKey="waitForSystemStorePage"/> -+ -+ <!--Create Custom Store --> -+ <click selector="{{AdminStoresMainActionsSection.createStoreButton}}" stepKey="selectCreateStore"/> -+ <fillField userInput="{{customStore.name}}" selector="{{AdminNewStoreGroupSection.storeGrpNameTextField}}" stepKey="fillStoreName"/> -+ <fillField userInput="{{customStore.code}}" selector="{{AdminNewStoreGroupSection.storeGrpCodeTextField}}" stepKey="fillStoreCode"/> -+ <selectOption userInput="{{NewRootCategory.name}}" selector="{{AdminNewStoreGroupSection.storeRootCategoryDropdown}}" stepKey="selectStoreStatus"/> -+ <click selector="{{AdminStoresMainActionsSection.saveButton}}" stepKey="clickSaveStoreButton"/> -+ -+ <!--Create Store View--> -+ <actionGroup ref="AdminCreateStoreViewActionGroup" stepKey="createStoreView"> -+ <argument name="StoreGroup" value="customStore"/> -+ <argument name="customStore" value="customStore"/> -+ </actionGroup> -+ -+ <!--Verify Category in Store View--> -+ <amOnPage url="/{{NewRootCategory.name}}/{{SimpleRootSubCategory.name}}.html" stepKey="seeTheCategoryInStoreFront"/> -+ <waitForPageLoad stepKey="waitForSystemStorePage1"/> -+ <click selector="{{StorefrontFooterSection.switchStoreButton}}" stepKey="ClickSwitchStoreButtonOnDefaultStore"/> -+ <click selector="{{StorefrontFooterSection.storeLink(customStore.name)}}" stepKey="SelectSecondStoreToSwitchOn"/> -+ <seeElement selector="{{StorefrontHeaderSection.NavigationCategoryByName(SimpleRootSubCategory.name)}}" stepKey="seeCatergoryInStoreFront"/> -+ <click selector="{{StorefrontHeaderSection.NavigationCategoryByName(SimpleRootSubCategory.name)}}" stepKey="selectCategory"/> -+ <waitForPageLoad stepKey="waitForProductToLoad"/> -+ -+ <!--Update URL Key--> -+ <amOnPage url="{{AdminCategoryPage.url}}" stepKey="openAdminCategoryIndexPage"/> -+ <waitForPageLoad stepKey="waitForPageToLoaded2"/> -+ <click selector="{{AdminCategorySidebarTreeSection.expandAll}}" stepKey="clickOnExpandTree"/> -+ <click selector="{{AdminCategorySidebarTreeSection.categoryInTree(SimpleRootSubCategory.name)}}" stepKey="selectCategory1"/> -+ <scrollTo selector="{{AdminCategorySEOSection.SectionHeader}}" stepKey="scrollToSearchEngineOptimization"/> -+ <click selector="{{AdminCategorySEOSection.SectionHeader}}" stepKey="openSeoSection"/> -+ <clearField selector="{{AdminCategorySEOSection.UrlKeyInput}}" stepKey="clearUrlKeyField"/> -+ <fillField selector="{{AdminCategorySEOSection.UrlKeyInput}}" userInput="newurlkey" stepKey="enterURLKey"/> -+ <click selector="{{AdminCategoryMainActionsSection.SaveButton}}" stepKey="saveCategoryAfterFirstSeoUpdate"/> -+ <seeElement selector="{{AdminCategoryMessagesSection.SuccessMessage}}" stepKey="assertSuccessMessage"/> -+ <waitForPageLoad stepKey="waitForPageToLoad"/> -+ -+ <!--Open Category Store Front Page--> -+ <amOnPage url="/{{NewRootCategory.name}}/{{SimpleRootSubCategory.name}}.html" stepKey="seeTheCategoryInStoreFront1"/> -+ <waitForPageLoad stepKey="waitForSystemStorePage3"/> -+ <seeElement selector="{{StorefrontHeaderSection.NavigationCategoryByName(SimpleRootSubCategory.name)}}" stepKey="seeCategoryOnNavigation1"/> -+ <click selector="{{StorefrontHeaderSection.NavigationCategoryByName(SimpleRootSubCategory.name)}}" stepKey="selectCategory2"/> -+ <waitForPageLoad stepKey="waitForProductToLoad1"/> -+ -+ <!--Verify Updated URLKey is present--> -+ <seeInCurrentUrl stepKey="verifyUpdatedUrlKey" url="newurlkey.html"/> -+ </test> -+</tests> -diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateCategoryWithInactiveIncludeInMenuTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateCategoryWithInactiveIncludeInMenuTest.xml -new file mode 100644 -index 00000000000..3fea9c0eed7 ---- /dev/null -+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateCategoryWithInactiveIncludeInMenuTest.xml -@@ -0,0 +1,83 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> -+ <test name="AdminUpdateCategoryWithInactiveIncludeInMenuTest"> -+ <annotations> -+ <stories value="Update categories"/> -+ <title value="Update category, name description urlkey metatitle exclude from menu"/> -+ <description value="Login as admin and update category name, description, urlKey, metatitle and exclude from menu"/> -+ <testCaseId value="MC-6058"/> -+ <severity value="CRITICAL"/> -+ <group value="Catalog"/> -+ <group value="mtf_migrated"/> -+ <group value="WYSIWYGDisabled"/> -+ </annotations> -+ <before> -+ <actionGroup ref="LoginAsAdmin" stepKey="loginToAdminPanel"/> -+ <createData entity="_defaultCategory" stepKey="createDefaultCategory"/> -+ </before> -+ <after> -+ <deleteData createDataKey="createDefaultCategory" stepKey="deleteCategory"/> -+ <actionGroup ref="logout" stepKey="logout"/> -+ </after> -+ -+ <!--Open Category Page--> -+ <amOnPage url="{{AdminCategoryPage.url}}" stepKey="openAdminCategoryIndexPage"/> -+ <waitForPageLoad stepKey="waitForPageToLoaded"/> -+ -+ <!--Update Category name,description, urlKey, meta title and disable Include in Menu--> -+ <click selector="{{AdminCategorySidebarTreeSection.expandAll}}" stepKey="clickOnExpandTree"/> -+ <click selector="{{AdminCategorySidebarTreeSection.categoryInTree(_defaultCategory.name)}}" stepKey="selectCreatedCategory"/> -+ <fillField selector="{{AdminCategoryBasicFieldSection.CategoryNameInput}}" userInput="{{SimpleRootSubCategory.name}}" stepKey="fillCategoryName"/> -+ <checkOption selector="{{AdminCategoryBasicFieldSection.EnableCategory}}" stepKey="enableCategory"/> -+ <click selector="{{AdminCategoryBasicFieldSection.includeInMenuLabel}}" stepKey="disableIncludeInMenu"/> -+ <scrollTo selector="{{AdminCategoryContentSection.sectionHeader}}" x="0" y="-80" stepKey="scrollToContent"/> -+ <click selector="{{AdminCategoryContentSection.sectionHeader}}" stepKey="selectContent"/> -+ <fillField selector="{{AdminCategoryContentSection.description}}" userInput="Updated category Description Fields" stepKey="fillUpdatedDescription"/> -+ <scrollTo selector="{{AdminCategorySEOSection.SectionHeader}}" x="0" y="-80" stepKey="scrollToSearchEngineOptimization"/> -+ <click selector="{{AdminCategorySEOSection.SectionHeader}}" stepKey="selectSearchEngineOptimization"/> -+ <fillField selector="{{AdminCategorySEOSection.UrlKeyInput}}" userInput="{{SimpleRootSubCategory.url_key}}" stepKey="fillUpdatedUrlKey"/> -+ <fillField selector="{{AdminCategorySEOSection.MetaTitleInput}}" userInput="{{SimpleRootSubCategory.name}}" stepKey="fillUpdatedMetaTitle"/> -+ <click selector="{{AdminCategoryMainActionsSection.SaveButton}}" stepKey="clickSaveButton"/> -+ <waitForPageLoad stepKey="waitForCategorySaved"/> -+ <see selector="{{AdminCategoryMessagesSection.SuccessMessage}}" userInput="You saved the category." stepKey="assertSuccessMessage"/> -+ -+ <!--Open UrlRewrite Page--> -+ <amOnPage url="{{AdminUrlRewriteIndexPage.url}}" stepKey="openUrlRewriteIndexPage"/> -+ <waitForPageLoad stepKey="waitForUrlRewritePage"/> -+ -+ <!--Verify Updated Category UrlKey--> -+ <fillField selector="{{AdminUrlRewriteIndexSection.requestPathFilter}}" userInput="{{SimpleRootSubCategory.url_key}}" stepKey="fillUpdatedCategoryUrlKey"/> -+ <click selector="{{AdminUrlRewriteIndexSection.searchButton}}" stepKey="clickOnSearchButton"/> -+ <waitForPageLoad stepKey="waitForPageToLoad"/> -+ <see stepKey="seeCategoryUrlKey" selector="{{AdminUrlRewriteIndexSection.requestPathColumn('1')}}" userInput="{{SimpleRootSubCategory.url_key}}.html" /> -+ <!--Verify Updated Category UrlKey directs to category Store Front--> -+ <amOnPage url="{{SimpleRootSubCategory.url_key}}.html" stepKey="seeTheCategoryInStoreFrontPage"/> -+ <waitForPageLoad time="60" stepKey="waitForStoreFrontPageLoad"/> -+ <seeElement selector="{{StorefrontCategoryMainSection.CategoryTitle(SimpleRootSubCategory.name)}}" stepKey="seeUpdatedCategoryInStoreFrontPage"/> -+ -+ <!--Verify Updated fields in Category Page--> -+ <amOnPage url="{{AdminCategoryPage.url}}" stepKey="openAdminCategoryIndexPage1"/> -+ <waitForPageLoad stepKey="waitForPageToLoaded1"/> -+ <click selector="{{AdminCategorySidebarTreeSection.expandAll}}" stepKey="clickOnExpandTree1"/> -+ <click selector="{{AdminCategorySidebarTreeSection.categoryInTree(SimpleRootSubCategory.name)}}" stepKey="selectCreatedCategory1"/> -+ <waitForPageLoad stepKey="waitForCategoryPageToLoad"/> -+ <see selector="{{AdminCategoryContentSection.categoryPageTitle}}" userInput="{{SimpleRootSubCategory.name}}" stepKey="seeUpdatedCategoryTitle"/> -+ <dontSeeCheckboxIsChecked selector="{{AdminCategoryBasicFieldSection.includeInMenuLabel}}" stepKey="verifyInactiveIncludeInMenu"/> -+ <seeInField selector="{{AdminCategoryBasicFieldSection.CategoryNameInput}}" userInput="{{SimpleRootSubCategory.name}}" stepKey="seeUpdatedCategoryName"/> -+ <scrollTo selector="{{AdminCategoryContentSection.sectionHeader}}" x="0" y="-80" stepKey="scrollToContent1"/> -+ <click selector="{{AdminCategoryContentSection.sectionHeader}}" stepKey="selectContent1"/> -+ <scrollTo selector="{{AdminCategoryContentSection.description}}" stepKey="scrollToDescription1"/> -+ <seeInField stepKey="seeUpdatedDiscription" selector="{{AdminCategoryContentSection.description}}" userInput="Updated category Description Fields"/> -+ <scrollTo selector="{{AdminCategorySEOSection.SectionHeader}}" x="0" y="-80" stepKey="scrollToSearchEngineOptimization1"/> -+ <click selector="{{AdminCategorySEOSection.SectionHeader}}" stepKey="selectSearchEngineOptimization1"/> -+ <seeInField stepKey="seeUpdatedUrlKey" selector="{{AdminCategorySEOSection.UrlKeyInput}}" userInput="{{SimpleRootSubCategory.url_key}}"/> -+ <seeInField stepKey="seeUpdatedMetaTitleInput" selector="{{AdminCategorySEOSection.MetaTitleInput}}" userInput="{{SimpleRootSubCategory.name}}"/> -+ </test> -+</tests> -diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateCategoryWithProductsTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateCategoryWithProductsTest.xml -new file mode 100644 -index 00000000000..1cb01ac11cb ---- /dev/null -+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateCategoryWithProductsTest.xml -@@ -0,0 +1,84 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> -+ <test name="AdminUpdateCategoryWithProductsTest"> -+ <annotations> -+ <stories value="Update categories"/> -+ <title value="Update category, sort products by default sorting"/> -+ <description value="Login as admin, update category and sort products"/> -+ <testCaseId value="MC-6059"/> -+ <severity value="CRITICAL"/> -+ <group value="Catalog"/> -+ <group value="mtf_migrated"/> -+ </annotations> -+ <before> -+ <actionGroup ref="LoginAsAdmin" stepKey="loginToAdminPanel"/> -+ <createData entity="defaultSimpleProduct" stepKey="simpleProduct" /> -+ <createData entity="_defaultCategory" stepKey="createCategory"/> -+ </before> -+ <after> -+ <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> -+ <deleteData createDataKey="simpleProduct" stepKey="deleteSimpleProduct"/> -+ <actionGroup ref="logout" stepKey="logout"/> -+ </after> -+ -+ <!--Open Category Page--> -+ <amOnPage url="{{AdminCategoryPage.url}}" stepKey="openAdminCategoryIndexPage"/> -+ <waitForPageLoad stepKey="waitForPageToLoaded"/> -+ <click selector="{{AdminCategorySidebarTreeSection.expandAll}}" stepKey="clickOnExpandTree"/> -+ <click selector="{{AdminCategorySidebarTreeSection.categoryInTree(_defaultCategory.name)}}" stepKey="selectCreatedCategory"/> -+ <checkOption selector="{{AdminCategoryBasicFieldSection.EnableCategory}}" stepKey="enableCategory"/> -+ -+ <!--Update Product Display Setting--> -+ <scrollTo selector="{{CategoryDisplaySettingsSection.DisplaySettingTab}}" x="0" y="-80" stepKey="scrollToDisplaySetting"/> -+ <click selector="{{CategoryDisplaySettingsSection.DisplaySettingTab}}" stepKey="selectDisplaySetting"/> -+ <scrollToTopOfPage stepKey="scfrollToTop"/> -+ <click selector="{{CategoryDisplaySettingsSection.productListCheckBox}}" stepKey="enableTheAvailableProductList"/> -+ <selectOption selector="{{CategoryDisplaySettingsSection.productList}}" parameterArray="['Product Name', 'Price']" stepKey="selectPrice"/> -+ <scrollTo selector="{{CategoryDisplaySettingsSection.defaultProductLisCheckBox}}" x="0" y="-80" stepKey="scrollToDefaultProductList"/> -+ <click selector="{{CategoryDisplaySettingsSection.defaultProductLisCheckBox}}" stepKey="enableTheDefaultProductList"/> -+ <selectOption selector="{{CategoryDisplaySettingsSection.defaultProductList}}" userInput="name" stepKey="selectProductName"/> -+ -+ <!--Add Products in Category--> -+ <scrollTo selector="{{AdminCategoryBasicFieldSection.productsInCategory}}" x="0" y="-80" stepKey="scrollToProductInCategory"/> -+ <click selector="{{AdminCategoryBasicFieldSection.productsInCategory}}" stepKey="clickOnProductInCategory"/> -+ <scrollToTopOfPage stepKey="scrollOnTopOfPage"/> -+ <click selector="{{CatalogProductsSection.resetFilter}}" stepKey="clickOnResetFilter"/> -+ <waitForPageLoad stepKey="waitForPageToLoad"/> -+ <fillField selector="{{AdminCategoryContentSection.productTableColumnName}}" userInput="$$simpleProduct.name$$" stepKey="selectProduct1"/> -+ <click selector="{{AdminCategoryContentSection.productSearch}}" stepKey="clickSearchButton"/> -+ <waitForPageLoad stepKey="waitFroPageToLoad1"/> -+ <scrollTo selector="{{AdminCategoryContentSection.productTableRow}}" stepKey="scrollToTableRow"/> -+ <click selector="{{AdminCategoryContentSection.productTableRow}}" stepKey="selectProduct1FromTableRow"/> -+ <click selector="{{AdminCategoryMainActionsSection.SaveButton}}" stepKey="clickSaveButton"/> -+ <waitForPageLoad stepKey="waitForCategorySaved"/> -+ <see selector="{{AdminCategoryMessagesSection.SuccessMessage}}" userInput="You saved the category." stepKey="assertSuccessMessage"/> -+ <waitForPageLoad stepKey="waitForPageTitleToBeSaved"/> -+ -+ <!--Verify Category Title--> -+ <see selector="{{AdminCategoryContentSection.categoryPageTitle}}" userInput="{{_defaultCategory.name}}" stepKey="seePageTitle" /> -+ -+ <!--Verify Category in store front page--> -+ <amOnPage url="{{StorefrontCategoryPage.url(_defaultCategory.name)}}" stepKey="seeDefaultProductPage"/> -+ <waitForPageLoad stepKey="waitForPageToBeLoaded"/> -+ <seeElement selector="{{StorefrontHeaderSection.NavigationCategoryByName(_defaultCategory.name)}}" stepKey="seeCategoryOnNavigation"/> -+ <click selector="{{StorefrontHeaderSection.NavigationCategoryByName(_defaultCategory.name)}}" stepKey="selectCategory"/> -+ <waitForPageLoad stepKey="waitForProductToLoad"/> -+ -+ <!--Verify Product in Category--> -+ <seeElement stepKey="seeProductsInCategory" selector="{{StorefrontCategoryMainSection.productLink}}"/> -+ <click selector="{{StorefrontCategoryMainSection.productLink}}" stepKey="openSearchedProduct"/> -+ <waitForPageLoad stepKey="waitForProductToLoad1"/> -+ -+ <!--Verify product name and price on Store Front--> -+ <see selector="{{StorefrontProductInfoMainSection.productName}}" userInput="{{defaultSimpleProduct.name}}" stepKey="assertProductName"/> -+ <see selector="{{StorefrontProductInfoMainSection.productPrice}}" userInput="{{defaultSimpleProduct.price}}" stepKey="assertProductPrice"/> -+ </test> -+</tests> -+ -diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateFlatCategoryAndAddProductsTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateFlatCategoryAndAddProductsTest.xml -new file mode 100644 -index 00000000000..8872ea98eb5 ---- /dev/null -+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateFlatCategoryAndAddProductsTest.xml -@@ -0,0 +1,103 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> -+ <test name="AdminUpdateFlatCategoryAndAddProductsTest"> -+ <annotations> -+ <stories value="Update category"/> -+ <title value="Flat Catalog - Assign Simple Product to Category"/> -+ <description value="Login as admin, update flat category by adding a simple product"/> -+ <testCaseId value="MC-11012"/> -+ <severity value="CRITICAL"/> -+ <group value="Catalog"/> -+ <group value="mtf_migrated"/> -+ </annotations> -+ <before> -+ <actionGroup ref="LoginAsAdmin" stepKey="loginToAdminPanel"/> -+ <!-- Create Simple Product --> -+ <createData entity="SimpleSubCategory" stepKey="category"/> -+ <!-- Create category --> -+ <createData entity="defaultSimpleProduct" stepKey="createSimpleProduct"/> -+ <!-- Create First StoreView --> -+ <actionGroup ref="CreateStoreView" stepKey="createCustomStoreViewEn"> -+ <argument name="storeView" value="customStoreEN"/> -+ </actionGroup> -+ <!-- Create Second StoreView --> -+ <actionGroup ref="CreateStoreView" stepKey="createCustomStoreViewFr"> -+ <argument name="storeView" value="customStoreFR"/> -+ </actionGroup> -+ <!--Run full reindex and clear caches --> -+ <magentoCLI command="cache:flush" stepKey="flushCache"/> -+ <magentoCLI command="indexer:reindex" stepKey="reindex"/> -+ <!--Enable Flat Catalog Category --> -+ <magentoCLI stepKey="setFlatCatalogCategory" command="config:set catalog/frontend/flat_catalog_category 1"/> -+ <!--Open Index Management Page and Select Index mode "Update by Schedule" --> -+ <magentoCLI stepKey="setIndexerMode" command="indexer:set-mode" arguments="schedule" /> -+ <!-- Run cron twice --> -+ <magentoCLI command="cron:run" stepKey="runCron1"/> -+ <magentoCLI command="cron:run" stepKey="runCron2"/> -+ </before> -+ <after> -+ <magentoCLI stepKey="setFlatCatalogCategory" command="config:set catalog/frontend/flat_catalog_category 0 "/> -+ <magentoCLI stepKey="setIndexersMode" command="indexer:set-mode" arguments="realtime" /> -+ <magentoCLI stepKey="indexerReindex" command="indexer:reindex" /> -+ <actionGroup ref="AdminDeleteStoreViewActionGroup" stepKey="deleteStoreViewEn"> -+ <argument name="customStore" value="customStoreEN"/> -+ </actionGroup> -+ <actionGroup ref="AdminDeleteStoreViewActionGroup" stepKey="deleteStoreViewFr"> -+ <argument name="customStore" value="customStoreFR"/> -+ </actionGroup> -+ <deleteData createDataKey="category" stepKey="deleteCategory"/> -+ <deleteData createDataKey="createSimpleProduct" stepKey="deleteSimpleProduct"/> -+ <actionGroup ref="logout" stepKey="logout"/> -+ </after> -+ <!-- Select Created Category--> -+ <magentoCLI command="indexer:reindex" stepKey="reindexBeforeFlow"/> -+ <amOnPage url="{{AdminCategoryPage.url}}" stepKey="openAdminCategoryIndexPage"/> -+ <waitForPageLoad stepKey="waitForPageToLoad"/> -+ <click selector="{{AdminCategorySidebarTreeSection.expandAll}}" stepKey="clickOnExpandTree"/> -+ <click selector="{{AdminCategorySidebarTreeSection.categoryInTree(SimpleSubCategory.name)}}" stepKey="selectCreatedCategory"/> -+ <waitForPageLoad stepKey="waitForTheCategoryPageToLoaded"/> -+ <!--Add Products in Category--> -+ <scrollTo selector="{{AdminCategoryBasicFieldSection.productsInCategory}}" x="0" y="-80" stepKey="scrollToProductInCategory"/> -+ <click selector="{{AdminCategoryBasicFieldSection.productsInCategory}}" stepKey="clickOnProductInCategory"/> -+ <scrollToTopOfPage stepKey="scrollOnTopOfPage"/> -+ <conditionalClick selector="{{CatalogProductsSection.resetFilter}}" dependentSelector="{{CatalogProductsSection.resetFilter}}" visible="true" stepKey="clickOnResetFilter"/> -+ <waitForPageLoad stepKey="waitForProductsToLoad"/> -+ <fillField selector="{{AdminCategoryContentSection.productTableColumnName}}" userInput="$$createSimpleProduct.name$$" stepKey="selectProduct"/> -+ <click selector="{{AdminCategoryContentSection.productSearch}}" stepKey="clickSearchButton"/> -+ <waitForPageLoad stepKey="waitFroPageToLoad1"/> -+ <scrollTo selector="{{AdminCategoryContentSection.productTableRow}}" stepKey="scrollToTableRow"/> -+ <click selector="{{AdminCategoryContentSection.productTableRow}}" stepKey="selectProductFromTableRow"/> -+ <click selector="{{AdminCategoryMainActionsSection.SaveButton}}" stepKey="saveSubCategory"/> -+ <waitForPageLoad stepKey="waitForSecondCategoryToSave"/> -+ <seeElement selector="{{AdminCategoryMessagesSection.SuccessMessage}}" stepKey="seeSuccessMessage"/> -+ <!--Open Index Management Page and verify flat categoryIndex status--> -+ <magentoCLI command="indexer:reindex" stepKey="reindex"/> -+ <magentoCLI command="cache:flush" stepKey="flushCache"/> -+ <!--Open Index Management Page --> -+ <amOnPage url="{{AdminIndexManagementPage.url}}" stepKey="openIndexManagementPage"/> -+ <waitForPageLoad stepKey="waitForIndexPageToLoad"/> -+ <see stepKey="seeCategoryIndexStatus" selector="{{AdminIndexManagementSection.indexerStatus('Category Flat Data')}}" userInput="Ready"/> -+ <!--Verify Product In Store Front--> -+ <amOnPage url="$$createSimpleProduct.name$$.html" stepKey="goToStorefrontPage"/> -+ <waitForPageLoad stepKey="waitForPageToBeLoaded"/> -+ <!--Verify product and category is visible in First Store View --> -+ <click stepKey="selectStoreSwitcher" selector="{{StorefrontHeaderSection.storeViewSwitcher}}"/> -+ <click stepKey="selectFirstStoreView" selector="{{StorefrontHeaderSection.storeViewList(customStoreEN.name)}}"/> -+ <waitForPageLoad stepKey="waitForFirstStoreView"/> -+ <seeElement selector="{{StorefrontHeaderSection.NavigationCategoryByName($$category.name$$)}}" stepKey="seeCategoryOnNavigation"/> -+ <see selector="{{StorefrontProductInfoMainSection.productName}}" userInput="{{defaultSimpleProduct.name}}" stepKey="assertProductName"/> -+ <!--Verify product and category is visible in Second Store View --> -+ <click stepKey="selectStoreSwitcher1" selector="{{StorefrontHeaderSection.storeViewSwitcher}}"/> -+ <click stepKey="selectSecondStoreView" selector="{{StorefrontHeaderSection.storeViewList(customStoreFR.name)}}"/> -+ <waitForPageLoad stepKey="waitForSecondStoreView"/> -+ <seeElement selector="{{StorefrontHeaderSection.NavigationCategoryByName($$category.name$$)}}" stepKey="seeCategoryOnNavigation1"/> -+ <see selector="{{StorefrontProductInfoMainSection.productName}}" userInput="{{defaultSimpleProduct.name}}" stepKey="seeProductName"/> -+ </test> -+</tests> -\ No newline at end of file -diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateFlatCategoryIncludeInNavigationTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateFlatCategoryIncludeInNavigationTest.xml -new file mode 100644 -index 00000000000..55273033706 ---- /dev/null -+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateFlatCategoryIncludeInNavigationTest.xml -@@ -0,0 +1,91 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> -+ <test name="AdminUpdateFlatCategoryAndIncludeInMenuTest"> -+ <annotations> -+ <stories value="Update category"/> -+ <title value="Flat Catalog - Update Category, Include in Navigation Menu"/> -+ <description value="Login as admin and update flat category by enabling Include in Menu"/> -+ <testCaseId value="MC-11011"/> -+ <severity value="CRITICAL"/> -+ <group value="Catalog"/> -+ <group value="mtf_migrated"/> -+ </annotations> -+ <before> -+ <actionGroup ref="LoginAsAdmin" stepKey="loginToAdminPanel"/> -+ <!--Create category--> -+ <createData entity="CatNotIncludeInMenu" stepKey="createCategory"/> -+ <!-- Create First StoreView --> -+ <actionGroup ref="CreateStoreView" stepKey="createCustomStoreViewEn"> -+ <argument name="storeView" value="customStoreEN"/> -+ </actionGroup> -+ <!-- Create Second StoreView --> -+ <actionGroup ref="CreateStoreView" stepKey="createCustomStoreViewFr"> -+ <argument name="storeView" value="customStoreFR"/> -+ </actionGroup> -+ <!--Run full reindex and clear caches --> -+ <magentoCLI command="indexer:reindex" stepKey="reindex"/> -+ <magentoCLI command="cache:flush" stepKey="flushCache"/> -+ <!--Enable Flat Catalog Category --> -+ <magentoCLI stepKey="setFlatCatalogCategory" command="config:set catalog/frontend/flat_catalog_category 1"/> -+ <!--Open Index Management Page and Select Index mode "Update by Schedule" --> -+ <magentoCLI stepKey="setIndexerMode" command="indexer:set-mode" arguments="schedule" /> -+ <!-- Run cron twice --> -+ <magentoCLI command="cron:run" stepKey="runCron1"/> -+ <magentoCLI command="cron:run" stepKey="runCron2"/> -+ </before> -+ <after> -+ <magentoCLI stepKey="setFlatCatalogCategory" command="config:set catalog/frontend/flat_catalog_category 0 "/> -+ <magentoCLI stepKey="setIndexersMode" command="indexer:set-mode" arguments="realtime" /> -+ <magentoCLI stepKey="indexerReindex" command="indexer:reindex" /> -+ <deleteData stepKey="deleteCategory" createDataKey="createCategory"/> -+ <actionGroup ref="AdminDeleteStoreViewActionGroup" stepKey="deleteStoreViewEn"> -+ <argument name="customStore" value="customStoreEN"/> -+ </actionGroup> -+ <actionGroup ref="AdminDeleteStoreViewActionGroup" stepKey="deleteStoreViewFr"> -+ <argument name="customStore" value="customStoreFR"/> -+ </actionGroup> -+ <actionGroup ref="logout" stepKey="logout"/> -+ </after> -+ <!--Verify Category is not listed in navigation menu--> -+ <amOnPage url="/{{CatNotIncludeInMenu.name_lwr}}.html" stepKey="openCategoryPage"/> -+ <waitForPageLoad time="60" stepKey="waitForPageToBeLoaded"/> -+ <dontSee selector="{{StorefrontHeaderSection.NavigationCategoryByName(CatNotIncludeInMenu.name)}}" stepKey="dontSeeCategoryOnNavigation"/> -+ <!-- Select created category and enable Include In Menu option--> -+ <amOnPage url="{{AdminCategoryPage.url}}" stepKey="openAdminCategoryIndexPage"/> -+ <waitForPageLoad stepKey="waitForPageToLoad"/> -+ <click selector="{{AdminCategorySidebarTreeSection.expandAll}}" stepKey="clickOnExpandTree"/> -+ <click selector="{{AdminCategorySidebarTreeSection.categoryInTree(CatNotIncludeInMenu.name)}}" stepKey="selectCreatedCategory"/> -+ <waitForPageLoad stepKey="waitForCategoryPageToLoad"/> -+ <click selector="{{AdminCategoryBasicFieldSection.includeInMenuLabel}}" stepKey="enableIncludeInMenuOption"/> -+ <click selector="{{AdminCategoryMainActionsSection.SaveButton}}" stepKey="saveSubCategory"/> -+ <waitForPageLoad stepKey="waitForSecondCategoryToSave"/> -+ <seeElement selector="{{AdminCategoryMessagesSection.SuccessMessage}}" stepKey="seeSuccessMessage"/> -+ <!--Run full reindex and clear caches --> -+ <magentoCLI command="indexer:reindex" stepKey="reindex"/> -+ <magentoCLI command="cache:flush" stepKey="flushCache"/> -+ <!--Open Index Management Page --> -+ <amOnPage url="{{AdminIndexManagementPage.url}}" stepKey="openIndexManagementPage"/> -+ <waitForPageLoad stepKey="waitForIndexPageToBeLoaded"/> -+ <see stepKey="seeIndexStatus" selector="{{AdminIndexManagementSection.indexerStatus('Category Flat Data')}}" userInput="Ready"/> -+ <!--Verify Category In Store Front--> -+ <amOnPage url="/$$createCategory.name$$.html" stepKey="openCategoryPage1"/> -+ <waitForPageLoad stepKey="waitForCategoryStoreFrontPageToLoad"/> -+ <!--Verify category is visible in First Store View --> -+ <click stepKey="selectStoreSwitcher" selector="{{StorefrontHeaderSection.storeViewSwitcher}}"/> -+ <click stepKey="selectForstStoreView" selector="{{StorefrontHeaderSection.storeViewList(customStoreEN.name)}}"/> -+ <waitForPageLoad stepKey="waitForFirstStoreView"/> -+ <seeElement selector="{{StorefrontHeaderSection.NavigationCategoryByName($$createCategory.name$$)}}" stepKey="seeCategoryOnNavigation"/> -+ <!--Verify category is visible in Second Store View --> -+ <click stepKey="selectStoreSwitcher1" selector="{{StorefrontHeaderSection.storeViewSwitcher}}"/> -+ <click stepKey="selectSecondStoreView" selector="{{StorefrontHeaderSection.storeViewList(customStoreFR.name)}}"/> -+ <waitForPageLoad stepKey="waitForSecondstoreView"/> -+ <seeElement selector="{{StorefrontHeaderSection.NavigationCategoryByName($$createCategory.name$$)}}" stepKey="seeCategoryOnNavigation1"/> -+ </test> -+</tests> -\ No newline at end of file -diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateFlatCategoryNameAndDescriptionTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateFlatCategoryNameAndDescriptionTest.xml -new file mode 100644 -index 00000000000..fcbc0cb2052 ---- /dev/null -+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateFlatCategoryNameAndDescriptionTest.xml -@@ -0,0 +1,102 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> -+ <test name="AdminUpdateFlatCategoryNameAndDescriptionTest"> -+ <annotations> -+ <stories value="Update category"/> -+ <title value="Flat Catalog - Update Category Name and Description"/> -+ <description value="Login as admin and update flat category name and description"/> -+ <testCaseId value="MC-11010"/> -+ <severity value="CRITICAL"/> -+ <group value="Catalog"/> -+ <group value="mtf_migrated"/> -+ <group value="WYSIWYGDisabled"/> -+ </annotations> -+ <before> -+ <actionGroup ref="LoginAsAdmin" stepKey="loginToAdminPanel"/> -+ <!--Create category--> -+ <createData entity="_defaultCategory" stepKey="createCategory"/> -+ <!-- Create First StoreView --> -+ <actionGroup ref="CreateStoreView" stepKey="createCustomStoreViewEn"> -+ <argument name="storeView" value="customStoreEN"/> -+ </actionGroup> -+ <!-- Create Second StoreView --> -+ <actionGroup ref="CreateStoreView" stepKey="createCustomStoreViewFr"> -+ <argument name="storeView" value="customStoreFR"/> -+ </actionGroup> -+ <!--Run full reindex and clear caches --> -+ <magentoCLI command="indexer:reindex" stepKey="reindex"/> -+ <magentoCLI command="cache:flush" stepKey="flushCache"/> -+ <!--Enable Flat Catalog Category --> -+ <magentoCLI stepKey="setFlatCatalogCategory" command="config:set catalog/frontend/flat_catalog_category 1"/> -+ <!--Open Index Management Page and Select Index mode "Update by Schedule" --> -+ <magentoCLI stepKey="setIndexerMode" command="indexer:set-mode" arguments="schedule" /> -+ <!-- Run cron twice --> -+ <magentoCLI command="cron:run" stepKey="runCron1"/> -+ <magentoCLI command="cron:run" stepKey="runCron2"/> -+ </before> -+ <after> -+ <magentoCLI stepKey="setFlatCatalogCategory" command="config:set catalog/frontend/flat_catalog_category 0 "/> -+ <magentoCLI stepKey="setIndexerMode" command="indexer:set-mode" arguments="realtime" /> -+ <magentoCLI stepKey="indexerReindex" command="indexer:reindex" /> -+ <deleteData stepKey="deleteCategory" createDataKey="createCategory" /> -+ <actionGroup ref="AdminDeleteStoreViewActionGroup" stepKey="deleteStoreViewEn"> -+ <argument name="customStore" value="customStoreEN"/> -+ </actionGroup> -+ <actionGroup ref="AdminDeleteStoreViewActionGroup" stepKey="deleteStoreViewFr"> -+ <argument name="customStore" value="customStoreFR"/> -+ </actionGroup> -+ <actionGroup ref="logout" stepKey="logout"/> -+ </after> -+ <!-- Select Created Category--> -+ <amOnPage url="{{AdminCategoryPage.url}}" stepKey="openAdminCategoryIndexPage"/> -+ <waitForPageLoad stepKey="waitForPageToLoad"/> -+ <click selector="{{AdminCategorySidebarTreeSection.expandAll}}" stepKey="clickOnExpandTree"/> -+ <click selector="{{AdminCategorySidebarTreeSection.categoryInTree(_defaultCategory.name)}}" stepKey="selectCreatedCategory"/> -+ <waitForPageLoad stepKey="waitForPageToLoaded"/> -+ <!--Update Category Name and Description --> -+ <fillField selector="{{AdminCategoryBasicFieldSection.CategoryNameInput}}" userInput="{{SimpleSubCategory.name}}" stepKey="addSubCategoryName"/> -+ <scrollTo selector="{{AdminCategoryContentSection.sectionHeader}}" x="0" y="-80" stepKey="scrollToContent"/> -+ <click selector="{{AdminCategoryContentSection.sectionHeader}}" stepKey="selectContent"/> -+ <fillField selector="{{AdminCategoryContentSection.description}}" userInput="Updated category Description Fields" stepKey="fillUpdatedDescription"/> -+ <click selector="{{AdminCategoryMainActionsSection.SaveButton}}" stepKey="saveSubCategory"/> -+ <waitForPageLoad stepKey="waitForSecondCategoryToSave"/> -+ <seeElement selector="{{AdminCategoryMessagesSection.SuccessMessage}}" stepKey="seeSuccessMessage"/> -+ <!--Run full reindex and clear caches --> -+ <magentoCLI command="indexer:reindex" stepKey="reindex"/> -+ <magentoCLI command="cache:flush" stepKey="flushCache"/> -+ <!--Open Index Management Page --> -+ <amOnPage url="{{AdminIndexManagementPage.url}}" stepKey="openIndexManagementPage"/> -+ <waitForPageLoad stepKey="waitForIndexPageToLoad"/> -+ <see stepKey="seeIndexStatus" selector="{{AdminIndexManagementSection.indexerStatus('Category Flat Data')}}" userInput="READY"/> -+ <!--Verify Category In Store Front--> -+ <amOnPage url="{{SimpleSubCategory.name}}.html" stepKey="goToStorefrontPage"/> -+ <waitForPageLoad stepKey="waitForPageToBeLoaded"/> -+ <!--Verify category is visible in First Store View --> -+ <click stepKey="selectStoreSwitcher" selector="{{StorefrontHeaderSection.storeViewSwitcher}}"/> -+ <click stepKey="selectFirstStoreView" selector="{{StorefrontHeaderSection.storeViewList(customStoreEN.name)}}"/> -+ <waitForPageLoad stepKey="waitForFirstStoreView"/> -+ <seeElement selector="{{StorefrontHeaderSection.NavigationCategoryByName(SimpleSubCategory.name)}}" stepKey="seeCategoryOnNavigation"/> -+ <!--Verify category is visible in Second Store View --> -+ <click stepKey="selectStoreSwitcher1" selector="{{StorefrontHeaderSection.storeViewSwitcher}}"/> -+ <click stepKey="selectSecondStoreView" selector="{{StorefrontHeaderSection.storeViewList(customStoreFR.name)}}"/> -+ <waitForPageLoad stepKey="waitForSecondStoreView"/> -+ <seeElement selector="{{StorefrontHeaderSection.NavigationCategoryByName(SimpleSubCategory.name)}}" stepKey="seeCategoryOnNavigation1"/> -+ <!-- Verify Updated Category Name and description on Category Page--> -+ <amOnPage url="{{AdminCategoryPage.url}}" stepKey="openAdminCategoryIndexPage1"/> -+ <waitForPageLoad stepKey="waitForCategoryPageToLoad"/> -+ <click selector="{{AdminCategorySidebarTreeSection.expandAll}}" stepKey="clickOnExpandTree1"/> -+ <click selector="{{AdminCategorySidebarTreeSection.categoryInTree(SimpleSubCategory.name)}}" stepKey="selectUpdatedCategory"/> -+ <waitForPageLoad stepKey="waitForUpdatedCategoryPageToLoad"/> -+ <seeInField selector="{{AdminCategoryBasicFieldSection.CategoryNameInput}}" userInput="{{SimpleSubCategory.name}}" stepKey="seeUpdatedSubCategoryName"/> -+ <scrollTo selector="{{AdminCategoryContentSection.sectionHeader}}" x="0" y="-80" stepKey="scrollToContent1"/> -+ <click selector="{{AdminCategoryContentSection.sectionHeader}}" stepKey="selectContent1"/> -+ <seeInField selector="{{AdminCategoryContentSection.description}}" userInput="Updated category Description Fields" stepKey="seeUpdatedDescription"/> -+ </test> -+</tests> -\ No newline at end of file -diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductNameToVerifyDataOverridingOnStoreViewLevelTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductNameToVerifyDataOverridingOnStoreViewLevelTest.xml -new file mode 100644 -index 00000000000..80f0c8ad10e ---- /dev/null -+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductNameToVerifyDataOverridingOnStoreViewLevelTest.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="AdminUpdateSimpleProductNameToVerifyDataOverridingOnStoreViewLevelTest"> -+ <annotations> -+ <stories value="Update Simple Product"/> -+ <title value="Update Simple Product Name to Verify Data Overriding on Store View Level"/> -+ <description value="Test log in to Update Simple Product and Update Simple Product Name to Verify Data Overriding on Store View Level"/> -+ <testCaseId value="MC-10821"/> -+ <severity value="CRITICAL"/> -+ <group value="catalog"/> -+ <group value="mtf_migrated"/> -+ </annotations> -+ <before> -+ <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> -+ <actionGroup ref="CreateStoreView" stepKey="createCustomStoreViewFr"> -+ <argument name="storeView" value="customStoreFR"/> -+ </actionGroup> -+ <createData entity="SimpleSubCategory" stepKey="initialCategoryEntity"/> -+ <createData entity="defaultSimpleProduct" stepKey="initialSimpleProduct"> -+ <requiredEntity createDataKey="initialCategoryEntity"/> -+ </createData> -+ <createData entity="SimpleSubCategory" stepKey="categoryEntity"/> -+ </before> -+ <after> -+ <deleteData stepKey="deleteSimpleSubCategory" createDataKey="initialCategoryEntity"/> -+ <deleteData stepKey="deleteSimpleSubCategory2" createDataKey="categoryEntity"/> -+ <actionGroup ref="deleteProductBySku" stepKey="deleteCreatedProduct"> -+ <argument name="sku" value="{{defaultSimpleProduct.sku}}"/> -+ </actionGroup> -+ <actionGroup ref="AdminDeleteStoreViewActionGroup" stepKey="deleteStoreView"> -+ <argument name="customStore" value="customStoreFR"/> -+ </actionGroup> -+ <actionGroup ref="logout" stepKey="logout"/> -+ </after> -+ -+ <!-- Search default simple product in grid --> -+ <amOnPage url="{{ProductCatalogPage.url}}" stepKey="OpenProductCatalogPage"/> -+ <waitForPageLoad stepKey="waitForProductCatalogPage"/> -+ <actionGroup ref="filterProductGridBySku2" stepKey="filterProductGrid"> -+ <argument name="sku" value="$$initialSimpleProduct.sku$$"/> -+ </actionGroup> -+ <click selector="{{AdminProductGridFilterSection.nthRow('1')}}" stepKey="clickFirstRowToOpenDefaultSimpleProduct"/> -+ <waitForPageLoad stepKey="waitUntilProductIsOpened"/> -+ -+ <!-- Assign simple product to created store view --> -+ <click selector="{{AdminCategoryMainActionsSection.CategoryStoreViewDropdownToggle}}" stepKey="clickCategoryStoreViewDropdownToggle"/> -+ <waitForPageLoad stepKey="waitForStoreViewDropdown"/> -+ <waitForElementVisible selector="{{AdminCategoryMainActionsSection.CategoryStoreViewOption(customStoreFR.name)}}" stepKey="waitForStoreViewOption"/> -+ <click selector="{{AdminCategoryMainActionsSection.CategoryStoreViewOption(customStoreFR.name)}}" stepKey="selectCategoryStoreViewOption"/> -+ <waitForPageLoad stepKey="waitForAcceptModal"/> -+ <waitForElementVisible selector="{{AdminProductFormChangeStoreSection.acceptButton}}" stepKey="waitForAcceptButton"/> -+ <click selector="{{AdminProductFormChangeStoreSection.acceptButton}}" stepKey="clickAcceptButton"/> -+ <waitForPageLoad stepKey="waitForThePageToLoad"/> -+ <waitForElementNotVisible selector="{{AdminProductFormChangeStoreSection.acceptButton}}" stepKey="waitForAcceptButtonGone"/> -+ <uncheckOption selector="{{AdminProductFormSection.productNameUseDefault}}" stepKey="uncheckProductStatus"/> -+ -+ <!-- Update default simple product with name --> -+ <fillField selector="{{AdminProductFormSection.productName}}" userInput="{{simpleProductDataOverriding.name}}" stepKey="fillSimpleProductName"/> -+ <click selector="{{AdminProductFormSection.save}}" stepKey="clickSaveButton"/> -+ <waitForPageLoad stepKey="waitForSimpleProductSave"/> -+ -+ <!-- Verify customer see success message --> -+ <see selector="{{AdminProductFormSection.successMessage}}" userInput="You saved the product." stepKey="seeAssertSimpleProductSaveSuccessMessage"/> -+ -+ <!--Verify customer see default simple product name on magento storefront page --> -+ <amOnPage url="{{StorefrontProductPage.url($$initialSimpleProduct.custom_attributes[url_key]$$)}}" stepKey="goToMagentoStorefrontPage"/> -+ <waitForPageLoad stepKey="waitForStoreFrontProductPageLoad"/> -+ <fillField selector="{{StorefrontQuickSearchResultsSection.searchTextBox}}" userInput="$$initialSimpleProduct.sku$$" stepKey="fillDefaultSimpleProductSkuInSearchTextBox"/> -+ <waitForPageLoad stepKey="waitForSearchTextBox"/> -+ <click selector="{{StorefrontQuickSearchResultsSection.searchTextBoxButton}}" stepKey="clickSearchTextBoxButton"/> -+ <waitForPageLoad stepKey="waitForSearch"/> -+ <see selector="{{StorefrontQuickSearchResultsSection.productLink}}" userInput="$$initialSimpleProduct.name$$" stepKey="seeDefaultProductName"/> -+ -+ <!--Verify customer see simple product with updated name on magento storefront page under store view section --> -+ <click selector="{{StorefrontHeaderSection.storeViewSwitcher}}" stepKey="clickStoreViewSwitcher"/> -+ <waitForPageLoad stepKey="waitForStoreSwitcherLoad"/> -+ <click selector="{{StorefrontHeaderSection.storeView(customStoreFR.name)}}" stepKey="clickStoreViewOption"/> -+ <fillField selector="{{StorefrontQuickSearchResultsSection.searchTextBox}}" userInput="$$initialSimpleProduct.sku$$" stepKey="fillDefaultSimpleProductSkuInSearch"/> -+ <waitForPageLoad stepKey="waitForSearchTextBoxLoad"/> -+ <click selector="{{StorefrontQuickSearchResultsSection.searchTextBoxButton}}" stepKey="clickSearchTextButton"/> -+ <waitForPageLoad stepKey="waitForTextSearchLoad"/> -+ <see selector="{{StorefrontQuickSearchResultsSection.productLink}}" userInput="{{simpleProductDataOverriding.name}}" stepKey="seeUpdatedSimpleProductName"/> -+ </test> -+</tests> -diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductPriceToVerifyDataOverridingOnStoreViewLevelTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductPriceToVerifyDataOverridingOnStoreViewLevelTest.xml -new file mode 100644 -index 00000000000..f698b3d89ff ---- /dev/null -+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductPriceToVerifyDataOverridingOnStoreViewLevelTest.xml -@@ -0,0 +1,90 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> -+ <test name="AdminUpdateSimpleProductPriceToVerifyDataOverridingOnStoreViewLevelTest"> -+ <annotations> -+ <stories value="Update Simple Product"/> -+ <title value="Update Simple Product Price to Verify Data Overriding on Store View Level"/> -+ <description value="Test log in to Update Simple Product and Update Simple Product Price to Verify Data Overriding on Store View Level"/> -+ <testCaseId value="MC-10823"/> -+ <severity value="CRITICAL"/> -+ <group value="catalog"/> -+ <group value="mtf_migrated"/> -+ </annotations> -+ <before> -+ <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> -+ <actionGroup ref="CreateStoreView" stepKey="createCustomStoreViewFr"> -+ <argument name="storeView" value="customStoreFR"/> -+ </actionGroup> -+ <createData entity="SimpleSubCategory" stepKey="initialCategoryEntity"/> -+ <createData entity="defaultSimpleProduct" stepKey="initialSimpleProduct"> -+ <requiredEntity createDataKey="initialCategoryEntity"/> -+ </createData> -+ <createData entity="SimpleSubCategory" stepKey="categoryEntity"/> -+ </before> -+ <after> -+ <deleteData stepKey="deleteSimpleSubCategory" createDataKey="initialCategoryEntity"/> -+ <deleteData stepKey="deleteSimpleSubCategory2" createDataKey="categoryEntity"/> -+ <actionGroup ref="deleteProductBySku" stepKey="deleteCreatedProduct"> -+ <argument name="sku" value="{{defaultSimpleProduct.sku}}"/> -+ </actionGroup> -+ <actionGroup ref="AdminDeleteStoreViewActionGroup" stepKey="deleteStoreView"> -+ <argument name="customStore" value="customStoreFR"/> -+ </actionGroup> -+ <actionGroup ref="logout" stepKey="logout"/> -+ </after> -+ -+ <!-- Search default simple product in grid --> -+ <amOnPage url="{{ProductCatalogPage.url}}" stepKey="OpenProductCatalogPage"/> -+ <waitForPageLoad stepKey="waitForProductCatalogPage"/> -+ <actionGroup ref="filterProductGridBySku2" stepKey="filterProductGrid"> -+ <argument name="sku" value="$$initialSimpleProduct.sku$$"/> -+ </actionGroup> -+ <click selector="{{AdminProductGridFilterSection.nthRow('1')}}" stepKey="clickFirstRowToOpenDefaultSimpleProduct"/> -+ <waitForPageLoad stepKey="waitUntilProductIsOpened"/> -+ -+ <!-- Assign simple product to created store view --> -+ <click selector="{{AdminCategoryMainActionsSection.CategoryStoreViewDropdownToggle}}" stepKey="clickCategoryStoreViewDropdownToggle"/> -+ <waitForPageLoad stepKey="waitForPageLoad"/> -+ <waitForElementVisible selector="{{AdminCategoryMainActionsSection.CategoryStoreViewOption(customStoreFR.name)}}" stepKey="waitForSelectCategoryStoreViewOption"/> -+ <click selector="{{AdminCategoryMainActionsSection.CategoryStoreViewOption(customStoreFR.name)}}" stepKey="selectCategoryStoreViewOption"/> -+ <waitForPageLoad stepKey="waitForPageLoad1"/> -+ <waitForElementVisible selector="{{AdminProductFormChangeStoreSection.acceptButton}}" stepKey="waitForAcceptButton"/> -+ <click selector="{{AdminProductFormChangeStoreSection.acceptButton}}" stepKey="clickAcceptButton"/> -+ <waitForPageLoad stepKey="waitForPageToLoad"/> -+ -+ <!-- Update default simple product with price --> -+ <fillField selector="{{AdminProductFormSection.productPrice}}" userInput="{{simpleProductDataOverriding.price}}" stepKey="fillSimpleProductPrice"/> -+ <click selector="{{AdminProductFormSection.save}}" stepKey="clickSaveButton"/> -+ <waitForPageLoad stepKey="waitForSimpleProductSave"/> -+ -+ <!-- Verify customer see success message --> -+ <see selector="{{AdminProductFormSection.successMessage}}" userInput="You saved the product." stepKey="seeAssertSimpleProductSaveSuccessMessage"/> -+ -+ <!-- Verify customer see simple product with updated price on magento storefront page --> -+ <amOnPage url="{{StorefrontProductPage.url($$initialSimpleProduct.custom_attributes[url_key]$$)}}" stepKey="goToMagentoStorefrontPage"/> -+ <waitForPageLoad stepKey="waitForStoreFrontProductPageLoad"/> -+ <fillField selector="{{StorefrontQuickSearchResultsSection.searchTextBox}}" userInput="$$initialSimpleProduct.sku$$" stepKey="fillDefaultSimpleProductSkuInSearchTextBox"/> -+ <waitForPageLoad stepKey="waitForSearchTextBox"/> -+ <click selector="{{StorefrontQuickSearchResultsSection.searchTextBoxButton}}" stepKey="clickSearchTextBoxButton"/> -+ <waitForPageLoad stepKey="waitForSearch"/> -+ <see selector="{{StorefrontQuickSearchResultsSection.regularPrice}}" userInput="{{simpleProductDataOverriding.price}}" stepKey="seeUpdatedProductPriceOnStorefrontPage"/> -+ -+ <!-- Verify customer see simple product with updated price on magento storefront page under store view section --> -+ <click selector="{{StorefrontHeaderSection.storeViewSwitcher}}" stepKey="clickStoreViewSwitcher"/> -+ <waitForPageLoad stepKey="waitForStoreSwitcherLoad"/> -+ <click selector="{{StorefrontHeaderSection.storeView(customStoreFR.name)}}" stepKey="clickStoreViewOption"/> -+ <fillField selector="{{StorefrontQuickSearchResultsSection.searchTextBox}}" userInput="$$initialSimpleProduct.sku$$" stepKey="fillDefaultSimpleProductSkuInSearch"/> -+ <waitForPageLoad stepKey="waitForSearchTextBoxLoad"/> -+ <click selector="{{StorefrontQuickSearchResultsSection.searchTextBoxButton}}" stepKey="clickSearchTextButton"/> -+ <waitForPageLoad stepKey="waitForTextSearchLoad"/> -+ <see selector="{{StorefrontQuickSearchResultsSection.regularPrice}}" userInput="{{simpleProductDataOverriding.price}}" stepKey="seeUpdatedProductPriceOnStorefrontPageUnderStoreViewSection"/> -+ </test> -+</tests> -diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductTieredPriceTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductTieredPriceTest.xml -new file mode 100644 -index 00000000000..d151bae3ee1 ---- /dev/null -+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductTieredPriceTest.xml -@@ -0,0 +1,146 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> -+ <test name="AdminUpdateSimpleProductTieredPriceTest"> -+ <annotations> -+ <stories value="Update Simple Product"/> -+ <title value="Update Simple Product Tiered Price"/> -+ <description value="Test log in to Update Simple Product and Update Simple Product Tiered Price"/> -+ <testCaseId value="MC-10824"/> -+ <severity value="CRITICAL"/> -+ <group value="catalog"/> -+ <group value="mtf_migrated"/> -+ <skip> -+ <issueId value="MC-17181"/> -+ </skip> -+ </annotations> -+ <before> -+ <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> -+ <createData entity="SimpleSubCategory" stepKey="initialCategoryEntity"/> -+ <createData entity="defaultSimpleProduct" stepKey="initialSimpleProduct"> -+ <requiredEntity createDataKey="initialCategoryEntity"/> -+ </createData> -+ <createData entity="SimpleSubCategory" stepKey="categoryEntity"/> -+ </before> -+ <after> -+ <deleteData stepKey="deleteSimpleSubCategory" createDataKey="initialCategoryEntity"/> -+ <deleteData stepKey="deleteSimpleSubCategory2" createDataKey="categoryEntity"/> -+ <actionGroup ref="deleteProductBySku" stepKey="deleteCreatedProduct"> -+ <argument name="sku" value="{{simpleProductTierPrice300InStock.sku}}"/> -+ </actionGroup> -+ <actionGroup ref="logout" stepKey="logout"/> -+ </after> -+ -+ <!-- Search default simple product in the grid --> -+ <amOnPage url="{{ProductCatalogPage.url}}" stepKey="OpenProductCatalogPage"/> -+ <waitForPageLoad stepKey="waitForProductCatalogPage"/> -+ <actionGroup ref="filterProductGridBySku2" stepKey="filterProductGrid"> -+ <argument name="sku" value="$$initialSimpleProduct.sku$$"/> -+ </actionGroup> -+ <click selector="{{AdminProductGridFilterSection.nthRow('1')}}" stepKey="clickFirstRowToOpenDefaultSimpleProduct"/> -+ <waitForPageLoad stepKey="waitUntilProductIsOpened"/> -+ -+ <!-- Update simple product with tier price(in stock) --> -+ <fillField selector="{{AdminProductFormSection.productName}}" userInput="{{simpleProductTierPrice300InStock.name}}" stepKey="fillSimpleProductName"/> -+ <fillField selector="{{AdminProductFormSection.productSku}}" userInput="{{simpleProductTierPrice300InStock.sku}}" stepKey="fillSimpleProductSku"/> -+ <fillField selector="{{AdminProductFormSection.productPrice}}" userInput="{{simpleProductTierPrice300InStock.price}}" stepKey="fillSimpleProductPrice"/> -+ -+ <!-- Press enter to validate advanced pricing link --> -+ <pressKey selector="{{AdminProductFormSection.productPrice}}" parameterArray="[\Facebook\WebDriver\WebDriverKeys::ENTER]" stepKey="pressEnterKey"/> -+ <click selector="{{AdminProductFormSection.advancedPricingLink}}" stepKey="clickAdvancedPricingLink"/> -+ <click selector="{{AdminProductFormAdvancedPricingSection.customerGroupPriceAddButton}}" stepKey="clickCustomerGroupPriceAddButton"/> -+ <scrollTo selector="{{AdminProductFormAdvancedPricingSection.productTierPriceQtyInput('0')}}" x="50" y="0" stepKey="scrollToProductTierPriceQuantityInputTextBox"/> -+ <selectOption selector="{{AdminProductFormAdvancedPricingSection.productTierPriceWebsiteSelect('0')}}" userInput="{{tierPriceHighCostSimpleProduct.website}}" stepKey="selectProductTierPriceWebsiteInput"/> -+ <selectOption selector="{{AdminProductFormAdvancedPricingSection.productTierPriceCustGroupSelect('0')}}" userInput="{{tierPriceHighCostSimpleProduct.customer_group}}" stepKey="selectProductTierPriceCustomerGroupInput"/> -+ <fillField selector="{{AdminProductFormAdvancedPricingSection.productTierPriceQtyInput('0')}}" userInput="{{tierPriceHighCostSimpleProduct.qty}}" stepKey="fillProductTierPriceQuantityInput"/> -+ <fillField selector="{{AdminProductFormAdvancedPricingSection.productTierPriceFixedPriceInput('0')}}" userInput="{{tierPriceHighCostSimpleProduct.price}}" stepKey="selectProductTierPriceFixedPrice"/> -+ <click selector="{{AdminProductFormAdvancedPricingSection.doneButton}}" stepKey="clickDoneButton"/> -+ <fillField selector="{{AdminProductFormSection.productQuantity}}" userInput="{{simpleProductTierPrice300InStock.quantity}}" stepKey="fillSimpleProductQuantity"/> -+ <selectOption selector="{{AdminProductFormSection.stockStatus}}" userInput="{{simpleProductTierPrice300InStock.status}}" stepKey="selectStockStatusInStock"/> -+ <fillField selector="{{AdminProductFormSection.productWeight}}" userInput="{{simpleProductTierPrice300InStock.weight}}" stepKey="fillSimpleProductWeight"/> -+ <selectOption selector="{{AdminProductFormSection.productWeightSelect}}" userInput="{{simpleProductTierPrice300InStock.weightSelect}}" stepKey="selectProductWeight"/> -+ <click selector="{{AdminProductFormSection.categoriesDropdown}}" stepKey="clickCategoriesDropDown"/> -+ <fillField selector="{{AdminProductFormSection.searchCategory}}" userInput="$$initialCategoryEntity.name$$" stepKey="fillSearchForInitialCategory" /> -+ <click selector="{{AdminProductFormSection.selectCategory($$initialCategoryEntity.name$$)}}" stepKey="unselectInitialCategory"/> -+ <fillField selector="{{AdminProductFormSection.searchCategory}}" userInput="$$categoryEntity.name$$" stepKey="fillSearchCategory" /> -+ <click selector="{{AdminProductFormSection.selectCategory($$categoryEntity.name$$)}}" stepKey="clickOnCategory"/> -+ <click selector="{{AdminProductFormSection.done}}" stepKey="clickOnDoneAdvancedCategorySelect"/> -+ <click selector="{{AdminProductSEOSection.sectionHeader}}" stepKey="clickAdminProductSEOSection"/> -+ <fillField selector="{{AdminProductSEOSection.urlKeyInput}}" userInput="{{simpleProductTierPrice300InStock.urlKey}}" stepKey="fillUrlKey"/> -+ <scrollToTopOfPage stepKey="scrollToTopOfAdminProductFormSection"/> -+ <click selector="{{AdminProductFormSection.save}}" stepKey="clickSaveButton"/> -+ <waitForPageLoad stepKey="waitForSimpleProductSave"/> -+ -+ <!-- Verify customer see success message --> -+ <see selector="{{AdminProductFormSection.successMessage}}" userInput="You saved the product." stepKey="seeAssertSimpleProductSaveSuccessMessage"/> -+ -+ <!-- Search updated simple product(from above step) in the grid page --> -+ <amOnPage url="{{ProductCatalogPage.url}}" stepKey="OpenProductCatalogPageToSearchUpdatedSimpleProduct"/> -+ <waitForPageLoad stepKey="waitForProductCatalogPageToLoad"/> -+ <conditionalClick selector="{{AdminProductGridFilterSection.clearAll}}" dependentSelector="{{AdminProductGridFilterSection.clearAll}}" visible="true" stepKey="clickClearAll"/> -+ <click selector="{{AdminProductGridFilterSection.filters}}" stepKey="clickFiltersButton"/> -+ <fillField selector="{{AdminProductGridFilterSection.nameFilter}}" userInput="{{simpleProductTierPrice300InStock.name}}" stepKey="fillSimpleProductNameInNameFilter"/> -+ <fillField selector="{{AdminProductGridFilterSection.skuFilter}}" userInput="{{simpleProductTierPrice300InStock.sku}}" stepKey="fillProductSku"/> -+ <click selector="{{AdminProductGridFilterSection.applyFilters}}" stepKey="clickApplyFiltersButton"/> -+ <click selector="{{AdminProductGridFilterSection.nthRow('1')}}" stepKey="clickFirstRowToVerifyUpdatedSimpleProductVisibleInGrid"/> -+ <waitForPageLoad stepKey="waitUntilSimpleProductPageIsOpened"/> -+ -+ <!-- Verify customer see updated simple product in the product form page --> -+ <seeInField selector="{{AdminProductFormSection.productName}}" userInput="{{simpleProductTierPrice300InStock.name}}" stepKey="seeSimpleProductName"/> -+ <seeInField selector="{{AdminProductFormSection.productSku}}" userInput="{{simpleProductTierPrice300InStock.sku}}" stepKey="seeSimpleProductSku"/> -+ <seeInField selector="{{AdminProductFormSection.productPrice}}" userInput="{{simpleProductTierPrice300InStock.price}}" stepKey="seeSimpleProductPrice"/> -+ <click selector="{{AdminProductFormSection.advancedPricingLink}}" stepKey="clickAdvancedPricingLink1"/> -+ <seeInField selector="{{AdminProductFormAdvancedPricingSection.productTierPriceWebsiteSelect('0')}}" userInput="{{tierPriceHighCostSimpleProduct.website}}" stepKey="seeProductTierPriceWebsiteInput"/> -+ <seeInField selector="{{AdminProductFormAdvancedPricingSection.productTierPriceCustGroupSelect('0')}}" userInput="{{tierPriceHighCostSimpleProduct.customer_group}}" stepKey="seeProductTierPriceCustomerGroupInput"/> -+ <seeInField selector="{{AdminProductFormAdvancedPricingSection.productTierPriceQtyInput('0')}}" userInput="{{tierPriceHighCostSimpleProduct.qty}}" stepKey="seeProductTierPriceQuantityInput"/> -+ <seeInField selector="{{AdminProductFormAdvancedPricingSection.productTierPriceFixedPriceInput('0')}}" userInput="{{tierPriceHighCostSimpleProduct.price}}" stepKey="seeProductTierPriceFixedPrice"/> -+ <click selector="{{AdminProductFormAdvancedPricingSection.advancedPricingCloseButton}}" stepKey="clickAdvancedPricingCloseButton"/> -+ <seeInField selector="{{AdminProductFormSection.productQuantity}}" userInput="{{simpleProductTierPrice300InStock.quantity}}" stepKey="seeSimpleProductQuantity"/> -+ <seeInField selector="{{AdminProductFormSection.productStockStatus}}" userInput="{{simpleProductTierPrice300InStock.status}}" stepKey="seeSimpleProductStockStatus"/> -+ <seeInField selector="{{AdminProductFormSection.productWeight}}" userInput="{{simpleProductTierPrice300InStock.weight}}" stepKey="seeSimpleProductWeight"/> -+ <seeInField selector="{{AdminProductFormSection.productWeightSelect}}" userInput="{{simpleProductTierPrice300InStock.weightSelect}}" stepKey="seeSimpleProductWeightSelect"/> -+ <click selector="{{AdminProductFormSection.categoriesDropdown}}" stepKey="clickCategoriesDropDownToVerify"/> -+ <see selector="{{AdminProductFormSection.selectMultipleCategories}}" userInput="$$categoryEntity.name$$" stepKey="seeSelectedCategories"/> -+ <scrollTo selector="{{AdminProductSEOSection.sectionHeader}}" x="0" y="-80" stepKey="scrollToAdminProductSEOSection1"/> -+ <click selector="{{AdminProductSEOSection.sectionHeader}}" stepKey="clickAdminProductSEOSection1"/> -+ <seeInField selector="{{AdminProductSEOSection.urlKeyInput}}" userInput="{{simpleProductTierPrice300InStock.urlKey}}" stepKey="seeUrlKey"/> -+ -+ <!--Verify customer see updated simple product link on category page --> -+ <amOnPage url="{{StorefrontCategoryPage.url($$categoryEntity.name$$)}}" stepKey="openCategoryPage"/> -+ <waitForPageLoad stepKey="waitForCategoryPageLoad"/> -+ <see selector="{{StorefrontCategoryMainSection.productLink}}" userInput="{{simpleProductTierPrice300InStock.name}}" stepKey="seeSimpleProductNameOnCategoryPage"/> -+ -+ <!-- Verify customer see updated simple product (from the above step) on the storefront page --> -+ <amOnPage url="{{StorefrontProductPage.url(simpleProductTierPrice300InStock.urlKey)}}" stepKey="goToProductPage"/> -+ <waitForPageLoad stepKey="waitForStorefrontProductPageLoad"/> -+ <see selector="{{StorefrontProductInfoMainSection.productName}}" userInput="{{simpleProductTierPrice300InStock.name}}" stepKey="seeSimpleProductNameOnStoreFrontPage"/> -+ <see selector="{{StorefrontProductInfoMainSection.productPrice}}" userInput="{{simpleProductTierPrice300InStock.price}}" stepKey="seeSimpleProductPriceOnStoreFrontPage"/> -+ <see selector="{{StorefrontProductInfoMainSection.productSku}}" userInput="{{simpleProductTierPrice300InStock.sku}}" stepKey="seeProductSku"/> -+ <grabTextFrom selector="{{StorefrontProductInfoMainSection.productStockStatus}}" stepKey="productStockAvailableStatus"/> -+ <assertEquals stepKey="assertStockAvailableOnProductPage"> -+ <expectedResult type="string">{{simpleProductTierPrice300InStock.storefrontStatus}}</expectedResult> -+ <actualResult type="variable">productStockAvailableStatus</actualResult> -+ </assertEquals> -+ <grabTextFrom selector="{{StorefrontProductInfoMainSection.productPrice}}" stepKey="productPriceAmount"/> -+ <assertEquals stepKey="assertOldPriceTextOnProductPage"> -+ <expectedResult type="string">${{simpleProductTierPrice300InStock.price}}</expectedResult> -+ <actualResult type="variable">productPriceAmount</actualResult> -+ </assertEquals> -+ -+ <!--Verify customer see updated simple product link on magento storefront page and is searchable by sku --> -+ <amOnPage url="{{StorefrontProductPage.url(simpleProductTierPrice300InStock.urlKey)}}" stepKey="goToMagentoStorefrontPage"/> -+ <waitForPageLoad stepKey="waitForStoreFrontProductPageLoad"/> -+ <fillField selector="{{StorefrontQuickSearchResultsSection.searchTextBox}}" userInput="{{simpleProductTierPrice300InStock.sku}}" stepKey="fillSimpleProductSkuInSearchTextBox"/> -+ <waitForPageLoad stepKey="waitForSearchTextBox"/> -+ <click selector="{{StorefrontQuickSearchResultsSection.searchTextBoxButton}}" stepKey="clickSearchTextBoxButton"/> -+ <waitForPageLoad stepKey="waitForSearch"/> -+ <see selector="{{StorefrontQuickSearchResultsSection.productLink}}" userInput="{{simpleProductTierPrice300InStock.name}}" stepKey="seeProductName"/> -+ </test> -+</tests> -diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductWithRegularPriceInStockDisabledProductTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductWithRegularPriceInStockDisabledProductTest.xml -new file mode 100644 -index 00000000000..6e8f1ba6f12 ---- /dev/null -+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductWithRegularPriceInStockDisabledProductTest.xml -@@ -0,0 +1,93 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> -+ <test name="AdminUpdateSimpleProductWithRegularPriceInStockDisabledProductTest"> -+ <annotations> -+ <stories value="Update Simple Product"/> -+ <title value="Update Simple Product with Regular Price (In Stock), Disabled Product"/> -+ <description value="Test log in to Update Simple Product and Update Simple Product with Regular Price (In Stock), Disabled Product"/> -+ <testCaseId value="MC-10816"/> -+ <severity value="CRITICAL"/> -+ <group value="catalog"/> -+ <group value="mtf_migrated"/> -+ </annotations> -+ <before> -+ <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> -+ <createData entity="SimpleSubCategory" stepKey="initialCategoryEntity"/> -+ <createData entity="defaultSimpleProduct" stepKey="initialSimpleProduct"> -+ <requiredEntity createDataKey="initialCategoryEntity"/> -+ </createData> -+ </before> -+ <after> -+ <deleteData stepKey="deleteSimpleSubCategory" createDataKey="initialCategoryEntity"/> -+ <actionGroup ref="deleteProductBySku" stepKey="deleteCreatedProduct"> -+ <argument name="sku" value="{{simpleProductDisabled.sku}}"/> -+ </actionGroup> -+ <actionGroup ref="logout" stepKey="logout"/> -+ </after> -+ -+ <!-- Search default simple product in the grid page --> -+ <amOnPage url="{{ProductCatalogPage.url}}" stepKey="OpenProductCatalogPage"/> -+ <waitForPageLoad stepKey="waitForProductCatalogPage"/> -+ <actionGroup ref="filterProductGridBySku2" stepKey="filterProductGrid"> -+ <argument name="sku" value="$$initialSimpleProduct.sku$$"/> -+ </actionGroup> -+ <click selector="{{AdminProductGridFilterSection.nthRow('1')}}" stepKey="clickFirstRowToOpenDefaultSimpleProduct"/> -+ <waitForPageLoad stepKey="waitUntilProductIsOpened"/> -+ -+ <!-- Update simple product with regular price(in stock) --> -+ <fillField selector="{{AdminProductFormSection.productName}}" userInput="{{simpleProductDisabled.name}}" stepKey="fillSimpleProductName"/> -+ <fillField selector="{{AdminProductFormSection.productSku}}" userInput="{{simpleProductDisabled.sku}}" stepKey="fillSimpleProductSku"/> -+ <fillField selector="{{AdminProductFormSection.productPrice}}" userInput="{{simpleProductDisabled.price}}" stepKey="fillSimpleProductPrice"/> -+ <fillField selector="{{AdminProductFormSection.productQuantity}}" userInput="{{simpleProductDisabled.quantity}}" stepKey="fillSimpleProductQuantity"/> -+ <selectOption selector="{{AdminProductFormSection.stockStatus}}" userInput="{{simpleProductDisabled.status}}" stepKey="selectStockStatusInStock"/> -+ <fillField selector="{{AdminProductFormSection.productWeight}}" userInput="{{simpleProductDisabled.weight}}" stepKey="fillSimpleProductWeight"/> -+ <click selector="{{AdminProductSEOSection.sectionHeader}}" stepKey="clickAdminProductSEOSection"/> -+ <fillField selector="{{AdminProductSEOSection.urlKeyInput}}" userInput="{{simpleProductDisabled.urlKey}}" stepKey="fillUrlKey"/> -+ <scrollToTopOfPage stepKey="scrollToTopOfAdminProductFormSection"/> -+ <click selector="{{AdminProductFormSection.enableProductLabel}}" stepKey="clickEnableProductLabelToDisableProduct"/> -+ <click selector="{{AdminProductFormSection.save}}" stepKey="clickSaveButton"/> -+ <waitForPageLoad stepKey="waitForSimpleProductSave"/> -+ -+ <!-- Verify customer see success message --> -+ <see selector="{{AdminProductFormSection.successMessage}}" userInput="You saved the product." stepKey="seeAssertSimpleProductSaveSuccessMessage"/> -+ -+ <!-- Search updated simple product(from above step) in the grid --> -+ <amOnPage url="{{ProductCatalogPage.url}}" stepKey="OpenProductCatalogPageToSearchUpdatedSimpleProduct"/> -+ <waitForPageLoad stepKey="waitForProductCatalogPageToLoad"/> -+ <conditionalClick selector="{{AdminProductGridFilterSection.clearAll}}" dependentSelector="{{AdminProductGridFilterSection.clearAll}}" visible="true" stepKey="clickClearAll"/> -+ <click selector="{{AdminProductGridFilterSection.filters}}" stepKey="clickFiltersButton"/> -+ <fillField selector="{{AdminProductGridFilterSection.nameFilter}}" userInput="{{simpleProductDisabled.name}}" stepKey="fillSimpleProductNameInNameFilter"/> -+ <fillField selector="{{AdminProductGridFilterSection.skuFilter}}" userInput="{{simpleProductDisabled.sku}}" stepKey="fillProductSku"/> -+ <click selector="{{AdminProductGridFilterSection.applyFilters}}" stepKey="clickApplyFiltersButton"/> -+ <click selector="{{AdminProductGridFilterSection.nthRow('1')}}" stepKey="clickFirstRowToVerifyUpdatedSimpleProductVisibleInGrid"/> -+ <waitForPageLoad stepKey="waitUntilSimpleProductPageIsOpened"/> -+ -+ <!-- Verify customer see updated simple product in the product form page --> -+ <seeInField selector="{{AdminProductFormSection.productName}}" userInput="{{simpleProductDisabled.name}}" stepKey="seeSimpleProductName"/> -+ <seeInField selector="{{AdminProductFormSection.productSku}}" userInput="{{simpleProductDisabled.sku}}" stepKey="seeSimpleProductSku"/> -+ <seeInField selector="{{AdminProductFormSection.productPrice}}" userInput="{{simpleProductDisabled.price}}" stepKey="seeSimpleProductPrice"/> -+ <seeInField selector="{{AdminProductFormSection.productQuantity}}" userInput="{{simpleProductDisabled.quantity}}" stepKey="seeSimpleProductQuantity"/> -+ <seeInField selector="{{AdminProductFormSection.productStockStatus}}" userInput="{{simpleProductDisabled.status}}" stepKey="seeSimpleProductStockStatus"/> -+ <seeInField selector="{{AdminProductFormSection.productWeight}}" userInput="{{simpleProductDisabled.weight}}" stepKey="seeSimpleProductWeight"/> -+ <scrollTo selector="{{AdminProductSEOSection.sectionHeader}}" x="0" y="-80" stepKey="scrollToAdminProductSEOSectionHeader"/> -+ <click selector="{{AdminProductSEOSection.sectionHeader}}" stepKey="clickAdminProductSEOSectionHeader"/> -+ <seeInField selector="{{AdminProductSEOSection.urlKeyInput}}" userInput="{{simpleProductDisabled.urlKey}}" stepKey="seeSimpleProductUrlKey"/> -+ -+ <!--Verify customer don't see updated simple product link on magento storefront page --> -+ <amOnPage url="{{StorefrontProductPage.url(simpleProductDisabled.urlKey)}}" stepKey="goToMagentoStorefrontPage"/> -+ <waitForPageLoad stepKey="waitForStoreFrontProductPageLoad"/> -+ <fillField selector="{{StorefrontQuickSearchResultsSection.searchTextBox}}" userInput="{{simpleProductDisabled.sku}}" stepKey="fillSimpleProductSkuInSearchTextBox"/> -+ <waitForPageLoad stepKey="waitForSearchTextBox"/> -+ <click selector="{{StorefrontQuickSearchResultsSection.searchTextBoxButton}}" stepKey="clickSearchTextBoxButton"/> -+ <waitForPageLoad stepKey="waitForSearch"/> -+ <dontSee selector="{{StorefrontQuickSearchResultsSection.productLink}}" userInput="{{simpleProductDisabled.name}}" stepKey="dontSeeProductNameOnStorefrontPage"/> -+ </test> -+</tests> -diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductWithRegularPriceInStockEnabledFlatTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductWithRegularPriceInStockEnabledFlatTest.xml -new file mode 100644 -index 00000000000..d30500de64a ---- /dev/null -+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductWithRegularPriceInStockEnabledFlatTest.xml -@@ -0,0 +1,144 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> -+ <test name="AdminUpdateSimpleProductWithRegularPriceInStockEnabledFlatTest"> -+ <annotations> -+ <stories value="Update Simple Product"/> -+ <title value="Update Simple Product with Regular Price (In Stock) Enabled Flat"/> -+ <description value="Test log in to Update Simple Product and Update Simple Product with Regular Price (In Stock) Enabled Flat"/> -+ <testCaseId value="MC-10818"/> -+ <severity value="CRITICAL"/> -+ <group value="catalog"/> -+ <group value="mtf_migrated"/> -+ <skip> -+ <issueId value="MC-17181"/> -+ </skip> -+ </annotations> -+ <before> -+ <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> -+ <magentoCLI stepKey="setFlatCatalogProduct" command="config:set catalog/frontend/flat_catalog_product 1"/> -+ <createData entity="SimpleSubCategory" stepKey="initialCategoryEntity"/> -+ <createData entity="defaultSimpleProduct" stepKey="initialSimpleProduct"> -+ <requiredEntity createDataKey="initialCategoryEntity"/> -+ </createData> -+ <createData entity="SimpleSubCategory" stepKey="categoryEntity"/> -+ </before> -+ <after> -+ <deleteData stepKey="deleteSimpleSubCategory" createDataKey="initialCategoryEntity"/> -+ <deleteData stepKey="deleteSimpleSubCategory2" createDataKey="categoryEntity"/> -+ <actionGroup ref="deleteProductBySku" stepKey="deleteCreatedProduct"> -+ <argument name="sku" value="{{simpleProductEnabledFlat.sku}}"/> -+ </actionGroup> -+ <actionGroup ref="logout" stepKey="logout"/> -+ <magentoCLI stepKey="unsetFlatCatalogProduct" command="config:set catalog/frontend/flat_catalog_product 0"/> -+ </after> -+ -+ <!-- Search default simple product in the grid page --> -+ <amOnPage url="{{ProductCatalogPage.url}}" stepKey="OpenProductCatalogPage"/> -+ <waitForPageLoad stepKey="waitForProductCatalogPage"/> -+ <actionGroup ref="filterProductGridBySku2" stepKey="filterProductGrid"> -+ <argument name="sku" value="$$initialSimpleProduct.sku$$"/> -+ </actionGroup> -+ <click selector="{{AdminProductGridFilterSection.nthRow('1')}}" stepKey="clickFirstRowToOpenDefaultSimpleProduct"/> -+ <waitForPageLoad stepKey="waitUntilProductIsOpened"/> -+ -+ <!-- Update simple product with regular price --> -+ <fillField selector="{{AdminProductFormSection.productName}}" userInput="{{simpleProductEnabledFlat.name}}" stepKey="fillSimpleProductName"/> -+ <fillField selector="{{AdminProductFormSection.productSku}}" userInput="{{simpleProductEnabledFlat.sku}}" stepKey="fillSimpleProductSku"/> -+ <fillField selector="{{AdminProductFormSection.productPrice}}" userInput="{{simpleProductEnabledFlat.price}}" stepKey="fillSimpleProductPrice"/> -+ <selectOption selector="{{AdminProductFormSection.productTaxClass}}" userInput="{{simpleProductEnabledFlat.productTaxClass}}" stepKey="selectProductTaxClass"/> -+ <fillField selector="{{AdminProductFormSection.productQuantity}}" userInput="{{simpleProductEnabledFlat.quantity}}" stepKey="fillSimpleProductQuantity"/> -+ <click selector="{{AdminProductFormSection.advancedInventoryLink}}" stepKey="clickAdvancedInventoryLink"/> -+ <waitForPageLoad stepKey="waitForAdvancedInventoryPage"/> -+ <conditionalClick selector="{{AdminProductFormAdvancedInventorySection.useConfigSettings}}" dependentSelector="{{AdminProductFormAdvancedInventorySection.useConfigSettings}}" visible="true" stepKey="checkUseConfigSettingsCheckBox"/> -+ <selectOption selector="{{AdminProductFormAdvancedInventorySection.manageStock}}" userInput="No" stepKey="selectManageStock"/> -+ <click selector="{{AdminProductFormAdvancedInventorySection.doneButton}}" stepKey="clickDoneButtonOnAdvancedInventorySection"/> -+ <selectOption selector="{{AdminProductFormSection.stockStatus}}" userInput="{{simpleProductEnabledFlat.status}}" stepKey="selectStockStatusInStock"/> -+ <fillField selector="{{AdminProductFormSection.productWeight}}" userInput="{{simpleProductEnabledFlat.weight}}" stepKey="fillSimpleProductWeight"/> -+ <selectOption selector="{{AdminProductFormSection.productWeightSelect}}" userInput="{{simpleProductEnabledFlat.weightSelect}}" stepKey="selectProductWeight"/> -+ <click selector="{{AdminProductFormSection.categoriesDropdown}}" stepKey="clickCategoriesDropDown"/> -+ <fillField selector="{{AdminProductFormSection.searchCategory}}" userInput="$$initialCategoryEntity.name$$" stepKey="fillSearchForInitialCategory" /> -+ <click selector="{{AdminProductFormSection.selectCategory($$initialCategoryEntity.name$$)}}" stepKey="unselectInitialCategory"/> -+ <fillField selector="{{AdminProductFormSection.searchCategory}}" userInput="$$categoryEntity.name$$" stepKey="fillSearchCategory" /> -+ <click selector="{{AdminProductFormSection.selectCategory($$categoryEntity.name$$)}}" stepKey="clickOnCategory"/> -+ <click selector="{{AdminProductFormSection.done}}" stepKey="clickOnDoneAdvancedCategorySelect"/> -+ <selectOption selector="{{AdminProductFormSection.visibility}}" userInput="{{simpleProductEnabledFlat.visibility}}" stepKey="selectVisibility"/> -+ <click selector="{{AdminProductSEOSection.sectionHeader}}" stepKey="clickAdminProductSEOSection"/> -+ <fillField selector="{{AdminProductSEOSection.urlKeyInput}}" userInput="{{simpleProductEnabledFlat.urlKey}}" stepKey="fillUrlKey"/> -+ <scrollToTopOfPage stepKey="scrollToTopOfAdminProductFormSection"/> -+ <click selector="{{AdminProductFormSection.save}}" stepKey="clickSaveButton"/> -+ <waitForPageLoad stepKey="waitForSimpleProductSave"/> -+ -+ <!-- Verify customer see success message --> -+ <see selector="{{AdminProductFormSection.successMessage}}" userInput="You saved the product." stepKey="seeAssertSimpleProductSaveSuccessMessage"/> -+ -+ <!-- Search updated simple product(from above step) in the grid page --> -+ <amOnPage url="{{ProductCatalogPage.url}}" stepKey="OpenProductCatalogPageToSearchUpdatedSimpleProduct"/> -+ <waitForPageLoad stepKey="waitForProductCatalogPageToLoad"/> -+ <conditionalClick selector="{{AdminProductGridFilterSection.clearAll}}" dependentSelector="{{AdminProductGridFilterSection.clearAll}}" visible="true" stepKey="clickClearAll"/> -+ <click selector="{{AdminProductGridFilterSection.filters}}" stepKey="clickFiltersButton"/> -+ <fillField selector="{{AdminProductGridFilterSection.nameFilter}}" userInput="{{simpleProductEnabledFlat.name}}" stepKey="fillSimpleProductNameInNameFilter"/> -+ <fillField selector="{{AdminProductGridFilterSection.skuFilter}}" userInput="{{simpleProductEnabledFlat.sku}}" stepKey="fillProductSku"/> -+ <click selector="{{AdminProductGridFilterSection.applyFilters}}" stepKey="clickApplyFiltersButton"/> -+ <click selector="{{AdminProductGridFilterSection.nthRow('1')}}" stepKey="clickFirstRowToVerifyUpdatedSimpleProductVisibleInGrid"/> -+ <waitForPageLoad stepKey="waitUntilSimpleProductPageIsOpened"/> -+ -+ <!-- Verify customer see updated simple product in the product form page --> -+ <seeInField selector="{{AdminProductFormSection.productName}}" userInput="{{simpleProductEnabledFlat.name}}" stepKey="seeSimpleProductName"/> -+ <seeInField selector="{{AdminProductFormSection.productSku}}" userInput="{{simpleProductEnabledFlat.sku}}" stepKey="seeSimpleProductSku"/> -+ <seeInField selector="{{AdminProductFormSection.productPrice}}" userInput="{{simpleProductEnabledFlat.price}}" stepKey="seeSimpleProductPrice"/> -+ <seeInField selector="{{AdminProductFormSection.productTaxClass}}" userInput="{{simpleProductEnabledFlat.productTaxClass}}" stepKey="seeProductTaxClass"/> -+ <seeInField selector="{{AdminProductFormSection.productQuantity}}" userInput="{{simpleProductEnabledFlat.quantity}}" stepKey="seeSimpleProductQuantity"/> -+ <click selector="{{AdminProductFormSection.advancedInventoryLink}}" stepKey="clickTheAdvancedInventoryLink"/> -+ <waitForPageLoad stepKey="waitForAdvancedInventoryPageLoad"/> -+ <see selector="{{AdminProductFormAdvancedInventorySection.manageStock}}" userInput="No" stepKey="seeManageStock"/> -+ <click selector="{{AdminProductFormAdvancedInventorySection.advancedInventoryCloseButton}}" stepKey="clickDoneButtonOnAdvancedInventory"/> -+ <seeInField selector="{{AdminProductFormSection.productStockStatus}}" userInput="{{simpleProductEnabledFlat.status}}" stepKey="seeSimpleProductStockStatus"/> -+ <seeInField selector="{{AdminProductFormSection.productWeight}}" userInput="{{simpleProductEnabledFlat.weight}}" stepKey="seeSimpleProductWeight"/> -+ <seeInField selector="{{AdminProductFormSection.productWeightSelect}}" userInput="{{simpleProductEnabledFlat.weightSelect}}" stepKey="seeSimpleProductWeightSelect"/> -+ <click selector="{{AdminProductFormSection.categoriesDropdown}}" stepKey="clickCategoriesDropDownToVerify"/> -+ <see selector="{{AdminProductFormSection.selectMultipleCategories}}" userInput="$$categoryEntity.name$$" stepKey="seeSelectedCategories" /> -+ <seeInField selector="{{AdminProductFormSection.visibility}}" userInput="{{simpleProductEnabledFlat.visibility}}" stepKey="seeVisibility"/> -+ <scrollTo selector="{{AdminProductSEOSection.sectionHeader}}" x="0" y="-80" stepKey="scrollToAdminProductSEOSection1"/> -+ <click selector="{{AdminProductSEOSection.sectionHeader}}" stepKey="clickAdminProductSEOSection1"/> -+ <seeInField selector="{{AdminProductSEOSection.urlKeyInput}}" userInput="{{simpleProductEnabledFlat.urlKey}}" stepKey="seeUrlKey"/> -+ -+ <!--Verify customer see updated simple product link on category page --> -+ <amOnPage url="{{StorefrontCategoryPage.url($$categoryEntity.name$$)}}" stepKey="openCategoryPage"/> -+ <waitForPageLoad stepKey="waitForCategoryPageLoad"/> -+ <see selector="{{StorefrontCategoryMainSection.productLink}}" userInput="{{simpleProductEnabledFlat.name}}" stepKey="seeSimpleProductNameOnCategoryPage"/> -+ -+ <!-- Verify customer see updated simple product (from the above step) on the storefront page --> -+ <amOnPage url="{{StorefrontProductPage.url(simpleProductEnabledFlat.urlKey)}}" stepKey="goToProductPage"/> -+ <waitForPageLoad stepKey="waitForStorefrontProductPageLoad"/> -+ <see selector="{{StorefrontProductInfoMainSection.productName}}" userInput="{{simpleProductEnabledFlat.name}}" stepKey="seeSimpleProductNameOnStoreFrontPage"/> -+ <see selector="{{StorefrontProductInfoMainSection.productPrice}}" userInput="{{simpleProductEnabledFlat.price}}" stepKey="seeSimpleProductPriceOnStoreFrontPage"/> -+ <see selector="{{StorefrontProductInfoMainSection.productSku}}" userInput="{{simpleProductEnabledFlat.sku}}" stepKey="seeSimpleProductSkuOnStoreFrontPage"/> -+ <grabTextFrom selector="{{StorefrontProductInfoMainSection.productStockStatus}}" stepKey="productStockAvailableStatus"/> -+ <assertEquals stepKey="assertStockAvailableOnProductPage"> -+ <expectedResult type="string">{{simpleProductEnabledFlat.storefrontStatus}}</expectedResult> -+ <actualResult type="variable">productStockAvailableStatus</actualResult> -+ </assertEquals> -+ <grabTextFrom selector="{{StorefrontProductInfoMainSection.productPrice}}" stepKey="productPriceAmount"/> -+ <assertEquals stepKey="assertOldPriceTextOnProductPage"> -+ <expectedResult type="string">${{simpleProductEnabledFlat.price}}</expectedResult> -+ <actualResult type="variable">productPriceAmount</actualResult> -+ </assertEquals> -+ -+ <!--Verify customer see updated simple product link on magento storefront page and is searchable by sku --> -+ <amOnPage url="{{StorefrontProductPage.url(simpleProductEnabledFlat.urlKey)}}" stepKey="goToMagentoStorefrontPage"/> -+ <waitForPageLoad stepKey="waitForStoreFrontProductPageLoad"/> -+ <fillField selector="{{StorefrontQuickSearchResultsSection.searchTextBox}}" userInput="{{simpleProductEnabledFlat.sku}}" stepKey="fillSimpleProductSkuInSearchTextBox"/> -+ <waitForPageLoad stepKey="waitForSearchTextBox"/> -+ <click selector="{{StorefrontQuickSearchResultsSection.searchTextBoxButton}}" stepKey="clickSearchTextBoxButton"/> -+ <waitForPageLoad stepKey="waitForSearch"/> -+ <see selector="{{StorefrontQuickSearchResultsSection.productLink}}" userInput="{{simpleProductEnabledFlat.name}}" stepKey="seeSimpleProductNameOnMagentoStorefrontPage"/> -+ </test> -+</tests> -diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductWithRegularPriceInStockNotVisibleIndividuallyTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductWithRegularPriceInStockNotVisibleIndividuallyTest.xml -new file mode 100644 -index 00000000000..cb7b3d6278a ---- /dev/null -+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductWithRegularPriceInStockNotVisibleIndividuallyTest.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="AdminUpdateSimpleProductWithRegularPriceInStockNotVisibleIndividuallyTest"> -+ <annotations> -+ <stories value="Update Simple Product"/> -+ <title value="Update Simple Product with Regular Price (In Stock) Not Visible Individually"/> -+ <description value="Test log in to Update Simple Product and Update Simple Product with Regular Price (In Stock) Not Visible Individually"/> -+ <testCaseId value="MC-10803"/> -+ <severity value="CRITICAL"/> -+ <group value="catalog"/> -+ <group value="mtf_migrated"/> -+ <skip> -+ <issueId value="MC-17181"/> -+ </skip> -+ </annotations> -+ <before> -+ <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> -+ <createData entity="SimpleSubCategory" stepKey="initialCategoryEntity"/> -+ <createData entity="defaultSimpleProduct" stepKey="initialSimpleProduct"> -+ <requiredEntity createDataKey="initialCategoryEntity"/> -+ </createData> -+ <createData entity="SimpleSubCategory" stepKey="categoryEntity"/> -+ </before> -+ <after> -+ <deleteData stepKey="deleteSimpleSubCategory" createDataKey="initialCategoryEntity"/> -+ <deleteData stepKey="deleteSimpleSubCategory2" createDataKey="categoryEntity"/> -+ <actionGroup ref="deleteProductBySku" stepKey="deleteCreatedProduct"> -+ <argument name="sku" value="{{simpleProductNotVisibleIndividually.sku}}"/> -+ </actionGroup> -+ <actionGroup ref="logout" stepKey="logout"/> -+ </after> -+ -+ <!-- Search default simple product in the grid page --> -+ <amOnPage url="{{ProductCatalogPage.url}}" stepKey="OpenProductCatalogPage"/> -+ <waitForPageLoad stepKey="waitForProductCatalogPage"/> -+ <actionGroup ref="filterProductGridBySku2" stepKey="filterProductGrid"> -+ <argument name="sku" value="$$initialSimpleProduct.sku$$"/> -+ </actionGroup> -+ <click selector="{{AdminProductGridFilterSection.nthRow('1')}}" stepKey="clickFirstRowToOpenDefaultSimpleProduct"/> -+ <waitForPageLoad stepKey="waitUntilProductIsOpened"/> -+ -+ <!-- Update simple product with regular price(in stock) --> -+ <fillField selector="{{AdminProductFormSection.productName}}" userInput="{{simpleProductNotVisibleIndividually.name}}" stepKey="fillSimpleProductName"/> -+ <fillField selector="{{AdminProductFormSection.productSku}}" userInput="{{simpleProductNotVisibleIndividually.sku}}" stepKey="fillSimpleProductSku"/> -+ <fillField selector="{{AdminProductFormSection.productPrice}}" userInput="{{simpleProductNotVisibleIndividually.price}}" stepKey="fillSimpleProductPrice"/> -+ <fillField selector="{{AdminProductFormSection.productQuantity}}" userInput="{{simpleProductNotVisibleIndividually.quantity}}" stepKey="fillSimpleProductQuantity"/> -+ <selectOption selector="{{AdminProductFormSection.stockStatus}}" userInput="{{simpleProductNotVisibleIndividually.status}}" stepKey="selectStockStatusInStock"/> -+ <fillField selector="{{AdminProductFormSection.productWeight}}" userInput="{{simpleProductNotVisibleIndividually.weight}}" stepKey="fillSimpleProductWeight"/> -+ <click selector="{{AdminProductFormSection.categoriesDropdown}}" stepKey="clickCategoriesDropDown"/> -+ <fillField selector="{{AdminProductFormSection.searchCategory}}" userInput="$$initialCategoryEntity.name$$" stepKey="fillSearchForInitialCategory" /> -+ <click selector="{{AdminProductFormSection.selectCategory($$initialCategoryEntity.name$$)}}" stepKey="unselectInitialCategory"/> -+ <fillField selector="{{AdminProductFormSection.searchCategory}}" userInput="$$categoryEntity.name$$" stepKey="fillSearchCategory"/> -+ <click selector="{{AdminProductFormSection.selectCategory($$categoryEntity.name$$)}}" stepKey="clickOnCategory"/> -+ <click selector="{{AdminProductFormSection.done}}" stepKey="clickOnDoneAdvancedCategorySelect"/> -+ <selectOption selector="{{AdminProductFormSection.visibility}}" userInput="{{simpleProductNotVisibleIndividually.visibility}}" stepKey="selectVisibility"/> -+ <click selector="{{AdminProductSEOSection.sectionHeader}}" stepKey="clickAdminProductSEOSection"/> -+ <fillField selector="{{AdminProductSEOSection.urlKeyInput}}" userInput="{{simpleProductNotVisibleIndividually.urlKey}}" stepKey="fillSimpleProductUrlKey"/> -+ <scrollToTopOfPage stepKey="scrollToTopOfAdminProductFormSection"/> -+ <click selector="{{AdminProductFormSection.save}}" stepKey="clickSaveButton"/> -+ <waitForPageLoad stepKey="waitForSimpleProductSave"/> -+ -+ <!-- Verify customer see success message --> -+ <see selector="{{AdminProductFormSection.successMessage}}" userInput="You saved the product." stepKey="seeAssertSimpleProductSaveSuccessMessage"/> -+ -+ <!-- Search updated simple product(from above step) in the grid page --> -+ <amOnPage url="{{ProductCatalogPage.url}}" stepKey="OpenProductCatalogPageToSearchUpdatedSimpleProduct"/> -+ <waitForPageLoad stepKey="waitForProductCatalogPageToLoad"/> -+ <conditionalClick selector="{{AdminProductGridFilterSection.clearAll}}" dependentSelector="{{AdminProductGridFilterSection.clearAll}}" visible="true" stepKey="clickClearAll"/> -+ <click selector="{{AdminProductGridFilterSection.filters}}" stepKey="clickFiltersButton"/> -+ <fillField selector="{{AdminProductGridFilterSection.nameFilter}}" userInput="{{simpleProductNotVisibleIndividually.name}}" stepKey="fillSimpleProductNameInNameFilter"/> -+ <fillField selector="{{AdminProductGridFilterSection.skuFilter}}" userInput="{{simpleProductNotVisibleIndividually.sku}}" stepKey="fillProductSku"/> -+ <click selector="{{AdminProductGridFilterSection.applyFilters}}" stepKey="clickApplyFiltersButton"/> -+ <click selector="{{AdminProductGridFilterSection.nthRow('1')}}" stepKey="clickFirstRowToVerifyUpdatedSimpleProductVisibleInGrid"/> -+ <waitForPageLoad stepKey="waitUntilSimpleProductPageIsOpened"/> -+ -+ <!-- Verify customer see updated simple product in the product form page --> -+ <seeInField selector="{{AdminProductFormSection.productName}}" userInput="{{simpleProductNotVisibleIndividually.name}}" stepKey="seeSimpleProductName"/> -+ <seeInField selector="{{AdminProductFormSection.productSku}}" userInput="{{simpleProductNotVisibleIndividually.sku}}" stepKey="seeSimpleProductSku"/> -+ <seeInField selector="{{AdminProductFormSection.productPrice}}" userInput="{{simpleProductNotVisibleIndividually.price}}" stepKey="seeSimpleProductPrice"/> -+ <seeInField selector="{{AdminProductFormSection.productQuantity}}" userInput="{{simpleProductNotVisibleIndividually.quantity}}" stepKey="seeSimpleProductQuantity"/> -+ <seeInField selector="{{AdminProductFormSection.productStockStatus}}" userInput="{{simpleProductNotVisibleIndividually.status}}" stepKey="seeSimpleProductStockStatus"/> -+ <seeInField selector="{{AdminProductFormSection.productWeight}}" userInput="{{simpleProductNotVisibleIndividually.weight}}" stepKey="seeSimpleProductWeight"/> -+ <click selector="{{AdminProductFormSection.categoriesDropdown}}" stepKey="clickCategoriesDropDownToVerify"/> -+ <see selector="{{AdminProductFormSection.selectMultipleCategories}}" userInput="$$categoryEntity.name$$" stepKey="seeSelectedCategories" /> -+ <seeInField selector="{{AdminProductFormSection.visibility}}" userInput="{{simpleProductNotVisibleIndividually.visibility}}" stepKey="seeSimpleProductVisibility"/> -+ <scrollTo selector="{{AdminProductSEOSection.sectionHeader}}" x="0" y="-80" stepKey="scrollToAdminProductSEOSection"/> -+ <click selector="{{AdminProductSEOSection.sectionHeader}}" stepKey="clickAdminProductSEOSectionHeader"/> -+ <seeInField selector="{{AdminProductSEOSection.urlKeyInput}}" userInput="{{simpleProductNotVisibleIndividually.urlKey}}" stepKey="seeSimpleProductUrlKey"/> -+ -+ <!--Verify customer don't see updated simple product link on magento storefront page --> -+ <amOnPage url="{{StorefrontProductPage.url(simpleProductNotVisibleIndividually.urlKey)}}" stepKey="goToMagentoStorefrontPage"/> -+ <waitForPageLoad stepKey="waitForStoreFrontProductPageLoad"/> -+ <fillField selector="{{StorefrontQuickSearchResultsSection.searchTextBox}}" userInput="{{simpleProductNotVisibleIndividually.sku}}" stepKey="fillSimpleProductSkuInSearchTextBox"/> -+ <waitForPageLoad stepKey="waitForSearchTextBox"/> -+ <click selector="{{StorefrontQuickSearchResultsSection.searchTextBoxButton}}" stepKey="clickSearchTextBoxButton"/> -+ <waitForPageLoad stepKey="waitForSearch"/> -+ <dontSee selector="{{StorefrontQuickSearchResultsSection.productLink}}" userInput="{{simpleProductNotVisibleIndividually.name}}" stepKey="dontSeeSimpleProductNameOnMagentoStorefrontPage"/> -+ </test> -+</tests> -diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductWithRegularPriceInStockUnassignFromCategoryTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductWithRegularPriceInStockUnassignFromCategoryTest.xml -new file mode 100644 -index 00000000000..3433a091173 ---- /dev/null -+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductWithRegularPriceInStockUnassignFromCategoryTest.xml -@@ -0,0 +1,68 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> -+ <test name="AdminUpdateSimpleProductWithRegularPriceInStockUnassignFromCategoryTest"> -+ <annotations> -+ <stories value="Update Simple Product"/> -+ <title value="Update Simple Product with Regular Price (In Stock) Unassign from Category"/> -+ <description value="Test log in to Update Simple Product and Update Simple Product with Regular Price (In Stock) Unassign from Category"/> -+ <testCaseId value="MC-10817"/> -+ <severity value="CRITICAL"/> -+ <group value="catalog"/> -+ <group value="mtf_migrated"/> -+ </annotations> -+ <before> -+ <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> -+ <createData entity="SimpleSubCategory" stepKey="initialCategoryEntity"/> -+ <createData entity="_defaultProduct" stepKey="initialSimpleProduct"> -+ <requiredEntity createDataKey="initialCategoryEntity"/> -+ </createData> -+ </before> -+ <after> -+ <deleteData stepKey="deleteSimpleSubCategory" createDataKey="initialCategoryEntity"/> -+ <actionGroup ref="deleteProductBySku" stepKey="deleteCreatedProduct"> -+ <argument name="sku" value="$$initialSimpleProduct.sku$$"/> -+ </actionGroup> -+ <actionGroup ref="logout" stepKey="logout"/> -+ </after> -+ -+ <!--Search default simple product in the grid page --> -+ <amOnPage url="{{ProductCatalogPage.url}}" stepKey="OpenProductCatalogPage"/> -+ <waitForPageLoad stepKey="waitForProductCatalogPage"/> -+ <actionGroup ref="filterProductGridBySku2" stepKey="filterProductGrid"> -+ <argument name="sku" value="$$initialSimpleProduct.sku$$"/> -+ </actionGroup> -+ <click selector="{{AdminProductGridFilterSection.nthRow('1')}}" stepKey="clickFirstRowToOpenDefaultSimpleProduct"/> -+ <waitForPageLoad stepKey="waitUntilProductIsOpened"/> -+ -+ <!-- Update simple product by unselecting categories --> -+ <scrollTo selector="{{AdminProductFormSection.productStockStatus}}" stepKey="scroll"/> -+ <click selector="{{AdminProductFormSection.categoriesDropdown}}" stepKey="clickCategoriesDropDown"/> -+ <click selector="{{AdminProductFormSection.unselectCategories($$initialCategoryEntity.name$$)}}" stepKey="unselectCategories"/> -+ <click selector="{{AdminProductFormSection.done}}" stepKey="clickOnDoneAdvancedCategory"/> -+ <scrollToTopOfPage stepKey="scrollToTopOfAdminProductFormSection"/> -+ <click selector="{{AdminProductFormSection.save}}" stepKey="clickSaveButton"/> -+ <waitForPageLoad stepKey="waitForSimpleProductSave"/> -+ -+ <!-- Verify customer see success message --> -+ <see selector="{{AdminProductFormSection.successMessage}}" userInput="You saved the product." stepKey="seeAssertSimpleProductSaveSuccessMessage"/> -+ -+ <!--Search default simple product in the grid page --> -+ <amOnPage url="{{AdminCategoryPage.url}}" stepKey="OpenCategoryCatalogPage"/> -+ <waitForPageLoad stepKey="waitForCategoryCatalogPage"/> -+ <click selector="{{AdminCategorySidebarTreeSection.expandAll}}" stepKey="clickExpandTree"/> -+ <waitForPageLoad stepKey="waitForCategoryToLoad"/> -+ <click selector="{{AdminCategorySidebarTreeSection.categoryInTree($$initialCategoryEntity.name$$)}}" stepKey="selectCategory"/> -+ <waitForPageLoad stepKey="waitForPageToLoad"/> -+ <click selector="{{AdminCategoryProductsSection.sectionHeader}}" stepKey="clickAdminCategoryProductSection"/> -+ <waitForPageLoad stepKey="waitForSectionHeaderToLoad"/> -+ <dontSee selector="{{AdminCategoryProductsGridSection.rowProductName($$initialSimpleProduct.name$$)}}" stepKey="dontSeeProductNameOnCategoryCatalogPage"/> -+ </test> -+</tests> -diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductWithRegularPriceInStockVisibleInCatalogAndSearchTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductWithRegularPriceInStockVisibleInCatalogAndSearchTest.xml -new file mode 100644 -index 00000000000..75b4a9728d0 ---- /dev/null -+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductWithRegularPriceInStockVisibleInCatalogAndSearchTest.xml -@@ -0,0 +1,129 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> -+ <test name="AdminUpdateSimpleProductWithRegularPriceInStockVisibleInCatalogAndSearchTest"> -+ <annotations> -+ <stories value="Update Simple Product"/> -+ <title value="Update Simple Product with Regular Price (In Stock) Visible in Catalog and Search"/> -+ <description value="Test log in to Update Simple Product and Update Simple Product with Regular Price (In Stock) Visible in Catalog and Search"/> -+ <testCaseId value="MC-10802"/> -+ <severity value="CRITICAL"/> -+ <group value="catalog"/> -+ <group value="mtf_migrated"/> -+ <skip> -+ <issueId value="MC-17181"/> -+ </skip> -+ </annotations> -+ <before> -+ <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> -+ <createData entity="SimpleSubCategory" stepKey="initialCategoryEntity"/> -+ <createData entity="defaultSimpleProduct" stepKey="initialSimpleProduct"> -+ <requiredEntity createDataKey="initialCategoryEntity"/> -+ </createData> -+ <createData entity="SimpleSubCategory" stepKey="categoryEntity"/> -+ </before> -+ <after> -+ <deleteData stepKey="deleteSimpleSubCategory" createDataKey="initialCategoryEntity"/> -+ <deleteData stepKey="deleteSimpleSubCategory2" createDataKey="categoryEntity"/> -+ <actionGroup ref="deleteProductBySku" stepKey="deleteCreatedProduct"> -+ <argument name="sku" value="{{simpleProductRegularPrice245InStock.sku}}"/> -+ </actionGroup> -+ <actionGroup ref="logout" stepKey="logout"/> -+ </after> -+ -+ <!-- Search default simple product in the grid page --> -+ <amOnPage url="{{ProductCatalogPage.url}}" stepKey="OpenProductCatalogPage"/> -+ <waitForPageLoad stepKey="waitForProductCatalogPage"/> -+ <actionGroup ref="filterProductGridBySku2" stepKey="filterProductGrid"> -+ <argument name="sku" value="$$initialSimpleProduct.sku$$"/> -+ </actionGroup> -+ <click selector="{{AdminProductGridFilterSection.nthRow('1')}}" stepKey="clickFirstRowToOpenDefaultSimpleProduct"/> -+ <waitForPageLoad stepKey="waitUntilProductIsOpened"/> -+ -+ <!-- Update simple product with regular price(in stock) --> -+ <fillField selector="{{AdminProductFormSection.productName}}" userInput="{{simpleProductRegularPrice245InStock.name}}" stepKey="fillSimpleProductName"/> -+ <fillField selector="{{AdminProductFormSection.productSku}}" userInput="{{simpleProductRegularPrice245InStock.sku}}" stepKey="fillSimpleProductSku"/> -+ <fillField selector="{{AdminProductFormSection.productPrice}}" userInput="{{simpleProductRegularPrice245InStock.price}}" stepKey="fillSimpleProductPrice"/> -+ <fillField selector="{{AdminProductFormSection.productQuantity}}" userInput="{{simpleProductRegularPrice245InStock.quantity}}" stepKey="fillSimpleProductQuantity"/> -+ <selectOption selector="{{AdminProductFormSection.stockStatus}}" userInput="{{simpleProductRegularPrice245InStock.status}}" stepKey="selectStockStatusInStock"/> -+ <fillField selector="{{AdminProductFormSection.productWeight}}" userInput="{{simpleProductRegularPrice245InStock.weight}}" stepKey="fillSimpleProductWeight"/> -+ <click selector="{{AdminProductFormSection.categoriesDropdown}}" stepKey="clickCategoriesDropDown"/> -+ <fillField selector="{{AdminProductFormSection.searchCategory}}" userInput="$$initialCategoryEntity.name$$" stepKey="fillSearchForInitialCategory" /> -+ <click selector="{{AdminProductFormSection.selectCategory($$initialCategoryEntity.name$$)}}" stepKey="unselectInitialCategory"/> -+ <fillField selector="{{AdminProductFormSection.searchCategory}}" userInput="$$categoryEntity.name$$" stepKey="fillSearchCategory" /> -+ <click selector="{{AdminProductFormSection.selectCategory($$categoryEntity.name$$)}}" stepKey="clickOnCategory"/> -+ <click selector="{{AdminProductFormSection.done}}" stepKey="clickOnDoneAdvancedCategorySelect"/> -+ <selectOption selector="{{AdminProductFormSection.visibility}}" userInput="{{simpleProductRegularPrice245InStock.visibility}}" stepKey="selectVisibility"/> -+ <click selector="{{AdminProductSEOSection.sectionHeader}}" stepKey="clickAdminProductSEOSection"/> -+ <fillField selector="{{AdminProductSEOSection.urlKeyInput}}" userInput="{{simpleProductRegularPrice245InStock.urlKey}}" stepKey="fillSimpleProductUrlKey"/> -+ <scrollToTopOfPage stepKey="scrollToTopOfAdminProductFormSection"/> -+ <click selector="{{AdminProductFormSection.save}}" stepKey="clickSaveButton"/> -+ <waitForPageLoad stepKey="waitForSimpleProductSave"/> -+ -+ <!-- Verify customer see success message --> -+ <see selector="{{AdminProductFormSection.successMessage}}" userInput="You saved the product." stepKey="seeAssertSimpleProductSaveSuccessMessage"/> -+ -+ <!-- Search updated simple product(from above step) in the grid page --> -+ <amOnPage url="{{ProductCatalogPage.url}}" stepKey="OpenProductCatalogPageToSearchUpdatedSimpleProduct"/> -+ <waitForPageLoad stepKey="waitForProductCatalogPageToLoad"/> -+ <conditionalClick selector="{{AdminProductGridFilterSection.clearAll}}" dependentSelector="{{AdminProductGridFilterSection.clearAll}}" visible="true" stepKey="clickClearAll"/> -+ <click selector="{{AdminProductGridFilterSection.filters}}" stepKey="clickFiltersButton"/> -+ <fillField selector="{{AdminProductGridFilterSection.nameFilter}}" userInput="{{simpleProductRegularPrice245InStock.name}}" stepKey="fillSimpleProductNameInNameFilter"/> -+ <fillField selector="{{AdminProductGridFilterSection.skuFilter}}" userInput="{{simpleProductRegularPrice245InStock.sku}}" stepKey="fillProductSku"/> -+ <click selector="{{AdminProductGridFilterSection.applyFilters}}" stepKey="clickApplyFiltersButton"/> -+ <click selector="{{AdminProductGridFilterSection.nthRow('1')}}" stepKey="clickFirstRowToVerifyUpdatedSimpleProductVisibleInGrid"/> -+ <waitForPageLoad stepKey="waitUntilSimpleProductPageIsOpened"/> -+ -+ <!-- Verify customer see updated simple product in the product form page --> -+ <seeInField selector="{{AdminProductFormSection.productName}}" userInput="{{simpleProductRegularPrice245InStock.name}}" stepKey="seeSimpleProductName"/> -+ <seeInField selector="{{AdminProductFormSection.productSku}}" userInput="{{simpleProductRegularPrice245InStock.sku}}" stepKey="seeSimpleProductSku"/> -+ <seeInField selector="{{AdminProductFormSection.productPrice}}" userInput="{{simpleProductRegularPrice245InStock.price}}" stepKey="seeSimpleProductPrice"/> -+ <seeInField selector="{{AdminProductFormSection.productQuantity}}" userInput="{{simpleProductRegularPrice245InStock.quantity}}" stepKey="seeSimpleProductQuantity"/> -+ <seeInField selector="{{AdminProductFormSection.productStockStatus}}" userInput="{{simpleProductRegularPrice245InStock.status}}" stepKey="seeSimpleProductStockStatus"/> -+ <seeInField selector="{{AdminProductFormSection.productWeight}}" userInput="{{simpleProductRegularPrice245InStock.weight}}" stepKey="seeSimpleProductWeight"/> -+ <click selector="{{AdminProductFormSection.categoriesDropdown}}" stepKey="clickCategoriesDropDownToVerify"/> -+ <see selector="{{AdminProductFormSection.selectMultipleCategories}}" userInput="$$categoryEntity.name$$" stepKey="selectedCategories" /> -+ <seeInField selector="{{AdminProductFormSection.visibility}}" userInput="{{simpleProductRegularPrice245InStock.visibility}}" stepKey="seeVisibility"/> -+ <scrollTo selector="{{AdminProductSEOSection.sectionHeader}}" x="0" y="-80" stepKey="scrollToAdminProductSEOSection1"/> -+ <click selector="{{AdminProductSEOSection.sectionHeader}}" stepKey="clickAdminProductSEOSection1"/> -+ <seeInField selector="{{AdminProductSEOSection.urlKeyInput}}" userInput="{{simpleProductRegularPrice245InStock.urlKey}}" stepKey="seeUrlKey"/> -+ -+ <!--Verify customer see updated simple product link on category page --> -+ <amOnPage url="{{StorefrontCategoryPage.url($$categoryEntity.name$$)}}" stepKey="openCategoryPage"/> -+ <waitForPageLoad stepKey="waitForCategoryPageLoad"/> -+ <see selector="{{StorefrontCategoryMainSection.productLink}}" userInput="{{simpleProductRegularPrice245InStock.name}}" stepKey="seeSimpleProductNameOnCategoryPage"/> -+ -+ <!-- Verify customer see updated simple product (from the above step) on the storefront page --> -+ <amOnPage url="{{StorefrontProductPage.url(simpleProductRegularPrice245InStock.urlKey)}}" stepKey="goToProductPage"/> -+ <waitForPageLoad stepKey="waitForStorefrontProductPageLoad"/> -+ <see selector="{{StorefrontProductInfoMainSection.productName}}" userInput="{{simpleProductRegularPrice245InStock.name}}" stepKey="seeSimpleProductNameOnStoreFrontPage"/> -+ <see selector="{{StorefrontProductInfoMainSection.productPrice}}" userInput="{{simpleProductRegularPrice245InStock.price}}" stepKey="seeSimpleProductPriceOnStoreFrontPage"/> -+ <see selector="{{StorefrontProductInfoMainSection.productSku}}" userInput="{{simpleProductRegularPrice245InStock.sku}}" stepKey="seeSimpleProductSkuOnStoreFrontPage"/> -+ <grabTextFrom selector="{{StorefrontProductInfoMainSection.productStockStatus}}" stepKey="productStockAvailableStatus"/> -+ <assertEquals stepKey="assertStockAvailableOnProductPage"> -+ <expectedResult type="string">{{simpleProductRegularPrice245InStock.storefrontStatus}}</expectedResult> -+ <actualResult type="variable">productStockAvailableStatus</actualResult> -+ </assertEquals> -+ <grabTextFrom selector="{{StorefrontProductInfoMainSection.productPrice}}" stepKey="productPriceAmount"/> -+ <assertEquals stepKey="assertOldPriceTextOnProductPage"> -+ <expectedResult type="string">${{simpleProductRegularPrice245InStock.price}}</expectedResult> -+ <actualResult type="variable">productPriceAmount</actualResult> -+ </assertEquals> -+ -+ <!--Verify customer see updated simple product link on magento storefront page and is searchable by sku --> -+ <amOnPage url="{{StorefrontProductPage.url(simpleProductRegularPrice245InStock.urlKey)}}" stepKey="goToMagentoStorefrontPage"/> -+ <waitForPageLoad stepKey="waitForStoreFrontProductPageLoad"/> -+ <fillField selector="{{StorefrontQuickSearchResultsSection.searchTextBox}}" userInput="{{simpleProductRegularPrice245InStock.sku}}" stepKey="fillSimpleProductSkuInSearchTextBox"/> -+ <waitForPageLoad stepKey="waitForSearchTextBox"/> -+ <click selector="{{StorefrontQuickSearchResultsSection.searchTextBoxButton}}" stepKey="clickSearchTextBoxButton"/> -+ <waitForPageLoad stepKey="waitForSearch"/> -+ <see selector="{{StorefrontQuickSearchResultsSection.productLink}}" userInput="{{simpleProductRegularPrice245InStock.name}}" stepKey="seeSimpleProductNameOnStorefrontPage"/> -+ </test> -+</tests> -diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductWithRegularPriceInStockVisibleInCatalogOnlyTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductWithRegularPriceInStockVisibleInCatalogOnlyTest.xml -new file mode 100644 -index 00000000000..f8b0b17c062 ---- /dev/null -+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductWithRegularPriceInStockVisibleInCatalogOnlyTest.xml -@@ -0,0 +1,129 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> -+ <test name="AdminUpdateSimpleProductWithRegularPriceInStockVisibleInCatalogOnlyTest"> -+ <annotations> -+ <stories value="Update Simple Product"/> -+ <title value="Update Simple Product with Regular Price (In Stock) Visible in Catalog Only"/> -+ <description value="Test log in to Update Simple Product and Update Simple Product with Regular Price (In Stock) Visible in Catalog Only"/> -+ <testCaseId value="MC-10804"/> -+ <severity value="CRITICAL"/> -+ <group value="catalog"/> -+ <group value="mtf_migrated"/> -+ <skip> -+ <issueId value="MC-17181"/> -+ </skip> -+ </annotations> -+ <before> -+ <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> -+ <createData entity="SimpleSubCategory" stepKey="initialCategoryEntity"/> -+ <createData entity="defaultSimpleProduct" stepKey="initialSimpleProduct"> -+ <requiredEntity createDataKey="initialCategoryEntity"/> -+ </createData> -+ <createData entity="SimpleSubCategory" stepKey="categoryEntity"/> -+ </before> -+ <after> -+ <deleteData stepKey="deleteSimpleSubCategory" createDataKey="initialCategoryEntity"/> -+ <deleteData stepKey="deleteSimpleSubCategory2" createDataKey="categoryEntity"/> -+ <actionGroup ref="deleteProductBySku" stepKey="deleteCreatedProduct"> -+ <argument name="sku" value="{{simpleProductRegularPrice32501InStock.sku}}"/> -+ </actionGroup> -+ <actionGroup ref="logout" stepKey="logout"/> -+ </after> -+ -+ <!-- Search default simple product in the grid --> -+ <amOnPage url="{{ProductCatalogPage.url}}" stepKey="OpenProductCatalogPage"/> -+ <waitForPageLoad stepKey="waitForProductCatalogPage"/> -+ <actionGroup ref="filterProductGridBySku2" stepKey="filterProductGrid"> -+ <argument name="sku" value="$$initialSimpleProduct.sku$$"/> -+ </actionGroup> -+ <click selector="{{AdminProductGridFilterSection.nthRow('1')}}" stepKey="clickFirstRowToOpenDefaultSimpleProduct"/> -+ <waitForPageLoad stepKey="waitUntilProductIsOpened"/> -+ -+ <!-- Update simple product with regular price(in stock) --> -+ <fillField selector="{{AdminProductFormSection.productName}}" userInput="{{simpleProductRegularPrice32501InStock.name}}" stepKey="fillSimpleProductName"/> -+ <fillField selector="{{AdminProductFormSection.productSku}}" userInput="{{simpleProductRegularPrice32501InStock.sku}}" stepKey="fillSimpleProductSku"/> -+ <fillField selector="{{AdminProductFormSection.productPrice}}" userInput="{{simpleProductRegularPrice32501InStock.price}}" stepKey="fillSimpleProductPrice"/> -+ <fillField selector="{{AdminProductFormSection.productQuantity}}" userInput="{{simpleProductRegularPrice32501InStock.quantity}}" stepKey="fillSimpleProductQuantity"/> -+ <selectOption selector="{{AdminProductFormSection.stockStatus}}" userInput="{{simpleProductRegularPrice32501InStock.status}}" stepKey="selectStockStatusInStock"/> -+ <fillField selector="{{AdminProductFormSection.productWeight}}" userInput="{{simpleProductRegularPrice32501InStock.weight}}" stepKey="fillSimpleProductWeight"/> -+ <click selector="{{AdminProductFormSection.categoriesDropdown}}" stepKey="clickCategoriesDropDown"/> -+ <fillField selector="{{AdminProductFormSection.searchCategory}}" userInput="$$initialCategoryEntity.name$$" stepKey="fillSearchForInitialCategory"/> -+ <click selector="{{AdminProductFormSection.selectCategory($$initialCategoryEntity.name$$)}}" stepKey="unselectInitialCategory"/> -+ <fillField selector="{{AdminProductFormSection.searchCategory}}" userInput="$$categoryEntity.name$$" stepKey="fillSearchCategory"/> -+ <click selector="{{AdminProductFormSection.selectCategory($$categoryEntity.name$$)}}" stepKey="clickOnCategory"/> -+ <click selector="{{AdminProductFormSection.done}}" stepKey="clickOnDoneAdvancedCategorySelect"/> -+ <selectOption selector="{{AdminProductFormSection.visibility}}" userInput="{{simpleProductRegularPrice32501InStock.visibility}}" stepKey="selectVisibility"/> -+ <click selector="{{AdminProductSEOSection.sectionHeader}}" stepKey="clickAdminProductSEOSection"/> -+ <fillField selector="{{AdminProductSEOSection.urlKeyInput}}" userInput="{{simpleProductRegularPrice32501InStock.urlKey}}" stepKey="fillUrlKey"/> -+ <scrollToTopOfPage stepKey="scrollToTopOfAdminProductFormSection"/> -+ <click selector="{{AdminProductFormSection.save}}" stepKey="clickSaveButton"/> -+ <waitForPageLoad stepKey="waitForSimpleProductSave"/> -+ -+ <!-- Verify customer see success message --> -+ <see selector="{{AdminProductFormSection.successMessage}}" userInput="You saved the product." stepKey="seeAssertSimpleProductSaveSuccessMessage"/> -+ -+ <!-- Search updated simple product(from above step) in the grid page --> -+ <amOnPage url="{{ProductCatalogPage.url}}" stepKey="OpenProductCatalogPageToSearchUpdatedSimpleProduct"/> -+ <waitForPageLoad stepKey="waitForProductCatalogPageToLoad"/> -+ <conditionalClick selector="{{AdminProductGridFilterSection.clearAll}}" dependentSelector="{{AdminProductGridFilterSection.clearAll}}" visible="true" stepKey="clickClearAll"/> -+ <click selector="{{AdminProductGridFilterSection.filters}}" stepKey="clickFiltersButton"/> -+ <fillField selector="{{AdminProductGridFilterSection.nameFilter}}" userInput="{{simpleProductRegularPrice32501InStock.name}}" stepKey="fillSimpleProductNameInNameFilter"/> -+ <fillField selector="{{AdminProductGridFilterSection.skuFilter}}" userInput="{{simpleProductRegularPrice32501InStock.sku}}" stepKey="fillProductSku"/> -+ <click selector="{{AdminProductGridFilterSection.applyFilters}}" stepKey="clickApplyFiltersButton"/> -+ <click selector="{{AdminProductGridFilterSection.nthRow('1')}}" stepKey="clickFirstRowToVerifyUpdatedSimpleProductVisibleInGrid"/> -+ <waitForPageLoad stepKey="waitUntilSimpleProductPageIsOpened"/> -+ -+ <!-- Verify customer see updated simple product in the product form page --> -+ <seeInField selector="{{AdminProductFormSection.productName}}" userInput="{{simpleProductRegularPrice32501InStock.name}}" stepKey="seeSimpleProductName"/> -+ <seeInField selector="{{AdminProductFormSection.productSku}}" userInput="{{simpleProductRegularPrice32501InStock.sku}}" stepKey="seeSimpleProductSku"/> -+ <seeInField selector="{{AdminProductFormSection.productPrice}}" userInput="{{simpleProductRegularPrice32501InStock.price}}" stepKey="seeSimpleProductPrice"/> -+ <seeInField selector="{{AdminProductFormSection.productQuantity}}" userInput="{{simpleProductRegularPrice32501InStock.quantity}}" stepKey="seeSimpleProductQuantity"/> -+ <seeInField selector="{{AdminProductFormSection.productStockStatus}}" userInput="{{simpleProductRegularPrice32501InStock.status}}" stepKey="seeSimpleProductStockStatus"/> -+ <seeInField selector="{{AdminProductFormSection.productWeight}}" userInput="{{simpleProductRegularPrice32501InStock.weight}}" stepKey="seeSimpleProductWeight"/> -+ <click selector="{{AdminProductFormSection.categoriesDropdown}}" stepKey="clickCategoriesDropDownToVerify"/> -+ <see selector="{{AdminProductFormSection.selectMultipleCategories}}" userInput="$$categoryEntity.name$$" stepKey="selectedCategories" /> -+ <seeInField selector="{{AdminProductFormSection.visibility}}" userInput="{{simpleProductRegularPrice32501InStock.visibility}}" stepKey="seeVisibility"/> -+ <scrollTo selector="{{AdminProductSEOSection.sectionHeader}}" x="0" y="-80" stepKey="scrollToAdminProductSEOSection1"/> -+ <click selector="{{AdminProductSEOSection.sectionHeader}}" stepKey="clickAdminProductSEOSection1"/> -+ <seeInField selector="{{AdminProductSEOSection.urlKeyInput}}" userInput="{{simpleProductRegularPrice32501InStock.urlKey}}" stepKey="seeUrlKey"/> -+ -+ <!--Verify customer see updated simple product link on category page --> -+ <amOnPage url="{{StorefrontCategoryPage.url($$categoryEntity.name$$)}}" stepKey="openCategoryPage"/> -+ <waitForPageLoad stepKey="waitForCategoryPageLoad"/> -+ <see selector="{{StorefrontCategoryMainSection.productLink}}" userInput="{{simpleProductRegularPrice32501InStock.name}}" stepKey="seeSimpleProductNameOnCategoryPage"/> -+ -+ <!-- Verify customer see updated simple product (from the above step) on the storefront page --> -+ <amOnPage url="{{StorefrontProductPage.url(simpleProductRegularPrice32501InStock.urlKey)}}" stepKey="goToProductPage"/> -+ <waitForPageLoad stepKey="waitForStorefrontProductPageLoad"/> -+ <see selector="{{StorefrontProductInfoMainSection.productName}}" userInput="{{simpleProductRegularPrice32501InStock.name}}" stepKey="seeSimpleProductNameOnStoreFrontPage"/> -+ <see selector="{{StorefrontProductInfoMainSection.productPrice}}" userInput="{{simpleProductRegularPrice32501InStock.price}}" stepKey="seeSimpleProductPriceOnStoreFrontPage"/> -+ <see selector="{{StorefrontProductInfoMainSection.productSku}}" userInput="{{simpleProductRegularPrice32501InStock.sku}}" stepKey="seeProductSku"/> -+ <grabTextFrom selector="{{StorefrontProductInfoMainSection.productStockStatus}}" stepKey="productStockAvailableStatus"/> -+ <assertEquals stepKey="assertStockAvailableOnProductPage"> -+ <expectedResult type="string">{{simpleProductRegularPrice32501InStock.storefrontStatus}}</expectedResult> -+ <actualResult type="variable">productStockAvailableStatus</actualResult> -+ </assertEquals> -+ <grabTextFrom selector="{{StorefrontProductInfoMainSection.productPrice}}" stepKey="productPriceAmount"/> -+ <assertEquals stepKey="assertOldPriceTextOnProductPage"> -+ <expectedResult type="string">${{simpleProductRegularPrice32501InStock.price}}</expectedResult> -+ <actualResult type="variable">productPriceAmount</actualResult> -+ </assertEquals> -+ -+ <!--Verify customer don't see updated simple product link on magento storefront page and is searchable by sku --> -+ <amOnPage url="{{StorefrontProductPage.url(simpleProductRegularPrice32501InStock.urlKey)}}" stepKey="goToMagentoStorefrontPage"/> -+ <waitForPageLoad stepKey="waitForStoreFrontProductPageLoad"/> -+ <fillField selector="{{StorefrontQuickSearchResultsSection.searchTextBox}}" userInput="{{simpleProductRegularPrice32501InStock.sku}}" stepKey="fillSimpleProductSkuInSearchTextBox"/> -+ <waitForPageLoad stepKey="waitForSearchTextBox"/> -+ <click selector="{{StorefrontQuickSearchResultsSection.searchTextBoxButton}}" stepKey="clickSearchTextBoxButton"/> -+ <waitForPageLoad stepKey="waitForSearch"/> -+ <dontSee selector="{{StorefrontQuickSearchResultsSection.productLink}}" userInput="{{simpleProductRegularPrice32501InStock.name}}" stepKey="dontSeeProductName"/> -+ </test> -+</tests> -diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductWithRegularPriceInStockVisibleInSearchOnlyTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductWithRegularPriceInStockVisibleInSearchOnlyTest.xml -new file mode 100644 -index 00000000000..ee2a2514c9c ---- /dev/null -+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductWithRegularPriceInStockVisibleInSearchOnlyTest.xml -@@ -0,0 +1,129 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> -+ <test name="AdminUpdateSimpleProductWithRegularPriceInStockVisibleInSearchOnlyTest"> -+ <annotations> -+ <stories value="Update Simple Product"/> -+ <title value="Update Simple Product with Regular Price (In Stock) Visible in Search Only"/> -+ <description value="Test log in to Update Simple Product and Update Simple Product with Regular Price (In Stock) Visible in Search Only"/> -+ <testCaseId value="MC-10805"/> -+ <severity value="CRITICAL"/> -+ <group value="catalog"/> -+ <group value="mtf_migrated"/> -+ <skip> -+ <issueId value="MC-17181"/> -+ </skip> -+ </annotations> -+ <before> -+ <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> -+ <createData entity="SimpleSubCategory" stepKey="initialCategoryEntity"/> -+ <createData entity="defaultSimpleProduct" stepKey="initialSimpleProduct"> -+ <requiredEntity createDataKey="initialCategoryEntity"/> -+ </createData> -+ <createData entity="SimpleSubCategory" stepKey="categoryEntity"/> -+ </before> -+ <after> -+ <deleteData stepKey="deleteSimpleSubCategory" createDataKey="initialCategoryEntity"/> -+ <deleteData stepKey="deleteSimpleSubCategory2" createDataKey="categoryEntity"/> -+ <actionGroup ref="deleteProductBySku" stepKey="deleteCreatedProduct"> -+ <argument name="sku" value="{{simpleProductRegularPrice325InStock.sku}}"/> -+ </actionGroup> -+ <actionGroup ref="logout" stepKey="logout"/> -+ </after> -+ -+ <!-- Search default simple product in the grid --> -+ <amOnPage url="{{ProductCatalogPage.url}}" stepKey="OpenProductCatalogPage"/> -+ <waitForPageLoad stepKey="waitForProductCatalogPage"/> -+ <actionGroup ref="filterProductGridBySku2" stepKey="filterProductGrid"> -+ <argument name="sku" value="$$initialSimpleProduct.sku$$"/> -+ </actionGroup> -+ <click selector="{{AdminProductGridFilterSection.nthRow('1')}}" stepKey="clickFirstRowToOpenDefaultSimpleProduct"/> -+ <waitForPageLoad stepKey="waitUntilProductIsOpened"/> -+ -+ <!-- Update simple product with regular price(in stock) --> -+ <fillField selector="{{AdminProductFormSection.productName}}" userInput="{{simpleProductRegularPrice325InStock.name}}" stepKey="fillSimpleProductName"/> -+ <fillField selector="{{AdminProductFormSection.productSku}}" userInput="{{simpleProductRegularPrice325InStock.sku}}" stepKey="fillSimpleProductSku"/> -+ <fillField selector="{{AdminProductFormSection.productPrice}}" userInput="{{simpleProductRegularPrice325InStock.price}}" stepKey="fillSimpleProductPrice"/> -+ <fillField selector="{{AdminProductFormSection.productQuantity}}" userInput="{{simpleProductRegularPrice325InStock.quantity}}" stepKey="fillSimpleProductQuantity"/> -+ <selectOption selector="{{AdminProductFormSection.stockStatus}}" userInput="{{simpleProductRegularPrice325InStock.status}}" stepKey="selectStockStatusInStock"/> -+ <fillField selector="{{AdminProductFormSection.productWeight}}" userInput="{{simpleProductRegularPrice325InStock.weight}}" stepKey="fillSimpleProductWeight"/> -+ <click selector="{{AdminProductFormSection.categoriesDropdown}}" stepKey="clickCategoriesDropDown"/> -+ <fillField selector="{{AdminProductFormSection.searchCategory}}" userInput="$$initialCategoryEntity.name$$" stepKey="fillSearchForInitialCategory" /> -+ <click selector="{{AdminProductFormSection.selectCategory($$initialCategoryEntity.name$$)}}" stepKey="unselectInitialCategory"/> -+ <fillField selector="{{AdminProductFormSection.searchCategory}}" userInput="$$categoryEntity.name$$" stepKey="fillSearchCategory" /> -+ <click selector="{{AdminProductFormSection.selectCategory($$categoryEntity.name$$)}}" stepKey="clickOnCategory"/> -+ <click selector="{{AdminProductFormSection.done}}" stepKey="clickOnDoneAdvancedCategorySelect"/> -+ <selectOption selector="{{AdminProductFormSection.visibility}}" userInput="{{simpleProductRegularPrice325InStock.visibility}}" stepKey="selectVisibility"/> -+ <click selector="{{AdminProductSEOSection.sectionHeader}}" stepKey="clickAdminProductSEOSection"/> -+ <fillField selector="{{AdminProductSEOSection.urlKeyInput}}" userInput="{{simpleProductRegularPrice325InStock.urlKey}}" stepKey="fillUrlKey"/> -+ <scrollToTopOfPage stepKey="scrollToTopOfAdminProductFormSection"/> -+ <click selector="{{AdminProductFormSection.save}}" stepKey="clickSaveButton"/> -+ <waitForPageLoad stepKey="waitForSimpleProductSave"/> -+ -+ <!-- Verify customer see success message --> -+ <see selector="{{AdminProductFormSection.successMessage}}" userInput="You saved the product." stepKey="seeAssertSimpleProductSaveSuccessMessage"/> -+ -+ <!-- Search updated simple product(from above step) in the grid page --> -+ <amOnPage url="{{ProductCatalogPage.url}}" stepKey="OpenProductCatalogPageToSearchUpdatedSimpleProduct"/> -+ <waitForPageLoad stepKey="waitForProductCatalogPageToLoad"/> -+ <conditionalClick selector="{{AdminProductGridFilterSection.clearAll}}" dependentSelector="{{AdminProductGridFilterSection.clearAll}}" visible="true" stepKey="clickClearAll"/> -+ <click selector="{{AdminProductGridFilterSection.filters}}" stepKey="clickFiltersButton"/> -+ <fillField selector="{{AdminProductGridFilterSection.nameFilter}}" userInput="{{simpleProductRegularPrice325InStock.name}}" stepKey="fillSimpleProductNameInNameFilter"/> -+ <fillField selector="{{AdminProductGridFilterSection.skuFilter}}" userInput="{{simpleProductRegularPrice325InStock.sku}}" stepKey="fillProductSku"/> -+ <click selector="{{AdminProductGridFilterSection.applyFilters}}" stepKey="clickApplyFiltersButton"/> -+ <click selector="{{AdminProductGridFilterSection.nthRow('1')}}" stepKey="clickFirstRowToVerifyUpdatedSimpleProductVisibleInGrid"/> -+ <waitForPageLoad stepKey="waitUntilSimpleProductPageIsOpened"/> -+ -+ <!-- Verify customer see updated simple product in the product form page --> -+ <seeInField selector="{{AdminProductFormSection.productName}}" userInput="{{simpleProductRegularPrice325InStock.name}}" stepKey="seeSimpleProductName"/> -+ <seeInField selector="{{AdminProductFormSection.productSku}}" userInput="{{simpleProductRegularPrice325InStock.sku}}" stepKey="seeSimpleProductSku"/> -+ <seeInField selector="{{AdminProductFormSection.productPrice}}" userInput="{{simpleProductRegularPrice325InStock.price}}" stepKey="seeSimpleProductPrice"/> -+ <seeInField selector="{{AdminProductFormSection.productQuantity}}" userInput="{{simpleProductRegularPrice325InStock.quantity}}" stepKey="seeSimpleProductQuantity"/> -+ <seeInField selector="{{AdminProductFormSection.productStockStatus}}" userInput="{{simpleProductRegularPrice325InStock.status}}" stepKey="seeSimpleProductStockStatus"/> -+ <seeInField selector="{{AdminProductFormSection.productWeight}}" userInput="{{simpleProductRegularPrice325InStock.weight}}" stepKey="seeSimpleProductWeight"/> -+ <click selector="{{AdminProductFormSection.categoriesDropdown}}" stepKey="clickCategoriesDropDownToVerify"/> -+ <see selector="{{AdminProductFormSection.selectMultipleCategories}}" userInput="$$categoryEntity.name$$" stepKey="selectedCategories" /> -+ <seeInField selector="{{AdminProductFormSection.visibility}}" userInput="{{simpleProductRegularPrice325InStock.visibility}}" stepKey="seeVisibility"/> -+ <scrollTo selector="{{AdminProductSEOSection.sectionHeader}}" x="0" y="-80" stepKey="scrollToAdminProductSEOSection1"/> -+ <click selector="{{AdminProductSEOSection.sectionHeader}}" stepKey="clickAdminProductSEOSection1"/> -+ <seeInField selector="{{AdminProductSEOSection.urlKeyInput}}" userInput="{{simpleProductRegularPrice325InStock.urlKey}}" stepKey="seeUrlKey"/> -+ -+ <!--Verify customer don't see updated simple product link on category page --> -+ <amOnPage url="{{StorefrontCategoryPage.url($$categoryEntity.name$$)}}" stepKey="openCategoryPage"/> -+ <waitForPageLoad stepKey="waitForCategoryPageLoad"/> -+ <dontSee selector="{{StorefrontCategoryMainSection.productLink}}" userInput="{{simpleProductRegularPrice325InStock.name}}" stepKey="dontSeeSimpleProductNameOnCategoryPage"/> -+ -+ <!-- Verify customer see updated simple product (from the above step) on the storefront page --> -+ <amOnPage url="{{StorefrontProductPage.url(simpleProductRegularPrice325InStock.urlKey)}}" stepKey="goToProductPage"/> -+ <waitForPageLoad stepKey="waitForStorefrontProductPageLoad"/> -+ <see selector="{{StorefrontProductInfoMainSection.productName}}" userInput="{{simpleProductRegularPrice325InStock.name}}" stepKey="seeSimpleProductNameOnStoreFrontPage"/> -+ <see selector="{{StorefrontProductInfoMainSection.productPrice}}" userInput="{{simpleProductRegularPrice325InStock.price}}" stepKey="seeSimpleProductPriceOnStoreFrontPage"/> -+ <see selector="{{StorefrontProductInfoMainSection.productSku}}" userInput="{{simpleProductRegularPrice325InStock.sku}}" stepKey="seeProductSku"/> -+ <grabTextFrom selector="{{StorefrontProductInfoMainSection.productStockStatus}}" stepKey="productStockAvailableStatus"/> -+ <assertEquals stepKey="assertStockAvailableOnProductPage"> -+ <expectedResult type="string">{{simpleProductRegularPrice325InStock.storefrontStatus}}</expectedResult> -+ <actualResult type="variable">productStockAvailableStatus</actualResult> -+ </assertEquals> -+ <grabTextFrom selector="{{StorefrontProductInfoMainSection.productPrice}}" stepKey="productPriceAmount"/> -+ <assertEquals stepKey="assertOldPriceTextOnProductPage"> -+ <expectedResult type="string">${{simpleProductRegularPrice325InStock.price}}</expectedResult> -+ <actualResult type="variable">productPriceAmount</actualResult> -+ </assertEquals> -+ -+ <!--Verify customer see updated simple product link on magento storefront page and is searchable by sku --> -+ <amOnPage url="{{StorefrontProductPage.url(simpleProductRegularPrice325InStock.urlKey)}}" stepKey="goToMagentoStorefrontPage"/> -+ <waitForPageLoad stepKey="waitForStoreFrontProductPageLoad"/> -+ <fillField selector="{{StorefrontQuickSearchResultsSection.searchTextBox}}" userInput="{{simpleProductRegularPrice325InStock.sku}}" stepKey="fillSimpleProductSkuInSearchTextBox"/> -+ <waitForPageLoad stepKey="waitForSearchTextBox"/> -+ <click selector="{{StorefrontQuickSearchResultsSection.searchTextBoxButton}}" stepKey="clickSearchTextBoxButton"/> -+ <waitForPageLoad stepKey="waitForSearch"/> -+ <see selector="{{StorefrontQuickSearchResultsSection.productLink}}" userInput="{{simpleProductRegularPrice325InStock.name}}" stepKey="seeProductName"/> -+ </test> -+</tests> -diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductWithRegularPriceInStockWithCustomOptionsTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductWithRegularPriceInStockWithCustomOptionsTest.xml -new file mode 100644 -index 00000000000..7921ed6c3e4 ---- /dev/null -+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductWithRegularPriceInStockWithCustomOptionsTest.xml -@@ -0,0 +1,163 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> -+ <test name="AdminUpdateSimpleProductWithRegularPriceInStockWithCustomOptionsTest"> -+ <annotations> -+ <stories value="Update Simple Product"/> -+ <title value="Update Simple Product with Regular Price (In Stock) with Custom Options"/> -+ <description value="Test log in to Update Simple Product and Update Simple Product with Regular Price (In Stock) with Custom Options"/> -+ <testCaseId value="MC-10819"/> -+ <severity value="CRITICAL"/> -+ <group value="catalog"/> -+ <group value="mtf_migrated"/> -+ <skip> -+ <issueId value="MC-17181"/> -+ </skip> -+ </annotations> -+ <before> -+ <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> -+ <createData entity="SimpleSubCategory" stepKey="initialCategoryEntity"/> -+ <createData entity="defaultSimpleProduct" stepKey="initialSimpleProduct"> -+ <requiredEntity createDataKey="initialCategoryEntity"/> -+ </createData> -+ <createData entity="SimpleSubCategory" stepKey="categoryEntity"/> -+ </before> -+ <after> -+ <deleteData stepKey="deleteSimpleSubCategory" createDataKey="initialCategoryEntity"/> -+ <deleteData stepKey="deleteSimpleSubCategory2" createDataKey="categoryEntity"/> -+ <actionGroup ref="deleteProductBySku" stepKey="deleteCreatedProduct"> -+ <argument name="sku" value="{{simpleProductRegularPriceCustomOptions.sku}}"/> -+ </actionGroup> -+ <actionGroup ref="logout" stepKey="logout"/> -+ </after> -+ -+ <!-- Search default simple product in the grid --> -+ <amOnPage url="{{ProductCatalogPage.url}}" stepKey="OpenProductCatalogPage"/> -+ <waitForPageLoad stepKey="waitForProductCatalogPage"/> -+ <actionGroup ref="filterProductGridBySku2" stepKey="filterProductGrid"> -+ <argument name="sku" value="$$initialSimpleProduct.sku$$"/> -+ </actionGroup> -+ <click selector="{{AdminProductGridFilterSection.nthRow('1')}}" stepKey="clickFirstRowToOpenDefaultSimpleProduct"/> -+ <waitForPageLoad stepKey="waitUntilProductIsOpened"/> -+ -+ <!-- Update simple product with regular price --> -+ <fillField selector="{{AdminProductFormSection.productName}}" userInput="{{simpleProductRegularPriceCustomOptions.name}}" stepKey="fillSimpleProductName"/> -+ <fillField selector="{{AdminProductFormSection.productSku}}" userInput="{{simpleProductRegularPriceCustomOptions.sku}}" stepKey="fillSimpleProductSku"/> -+ <fillField selector="{{AdminProductFormSection.productPrice}}" userInput="{{simpleProductRegularPriceCustomOptions.price}}" stepKey="fillSimpleProductPrice"/> -+ <fillField selector="{{AdminProductFormSection.productQuantity}}" userInput="{{simpleProductRegularPriceCustomOptions.quantity}}" stepKey="fillSimpleProductQuantity"/> -+ <selectOption selector="{{AdminProductFormSection.stockStatus}}" userInput="{{simpleProductRegularPriceCustomOptions.status}}" stepKey="selectStockStatusInStock"/> -+ <fillField selector="{{AdminProductFormSection.productWeight}}" userInput="{{simpleProductRegularPriceCustomOptions.weight}}" stepKey="fillSimpleProductWeight"/> -+ <click selector="{{AdminProductFormSection.categoriesDropdown}}" stepKey="clickCategoriesDropDown"/> -+ <fillField selector="{{AdminProductFormSection.searchCategory}}" userInput="$$initialCategoryEntity.name$$" stepKey="fillSearchForInitialCategory"/> -+ <click selector="{{AdminProductFormSection.selectCategory($$initialCategoryEntity.name$$)}}" stepKey="unselectInitialCategory"/> -+ <fillField selector="{{AdminProductFormSection.searchCategory}}" userInput="$$categoryEntity.name$$" stepKey="fillSearchCategory"/> -+ <click selector="{{AdminProductFormSection.selectCategory($$categoryEntity.name$$)}}" stepKey="clickOnCategory"/> -+ <click selector="{{AdminProductFormSection.done}}" stepKey="clickOnDoneAdvancedCategorySelect"/> -+ <click selector="{{AdminProductSEOSection.sectionHeader}}" stepKey="clickAdminProductSEOSection"/> -+ <fillField selector="{{AdminProductSEOSection.urlKeyInput}}" userInput="{{simpleProductRegularPriceCustomOptions.urlKey}}" stepKey="fillUrlKey"/> -+ <click selector="{{AdminProductCustomizableOptionsSection.checkIfCustomizableOptionsTabOpen}}" stepKey="clickAdminProductCustomizableOption"/> -+ -+ <!-- Create simple product with customizable option --> -+ <click selector="{{AdminProductCustomizableOptionsSection.addOptionBtn}}" stepKey="clickAddOptionButton"/> -+ <waitForPageLoad stepKey="waitForDataToLoad"/> -+ <fillField selector="{{AdminProductCustomizableOptionsSection.optionTitleInput('0')}}" userInput="{{simpleProductCustomizableOption.title}}" stepKey="fillOptionTitle"/> -+ <click selector="{{AdminProductCustomizableOptionsSection.optionTypeDropDown('1')}}" stepKey="selectOptionTypeDropDown"/> -+ <click selector="{{AdminProductCustomizableOptionsSection.optionTypeItem('1', simpleProductCustomizableOption.type)}}" stepKey="selectOptionFieldFromDropDown"/> -+ <checkOption selector="{{AdminProductCustomizableOptionsSection.requiredCheckBox('0')}}" stepKey="checkRequiredCheckBox"/> -+ <click selector="{{AdminProductCustomizableOptionsSection.addValue}}" stepKey="clickAddValueButton"/> -+ <fillField selector="{{AdminProductCustomizableOptionsSection.fillOptionValueTitle(simpleProductCustomizableOption.title,'0')}}" userInput="{{simpleProductCustomizableOption.option_0_title}}" stepKey="fillOptionTitleForCustomizableOption"/> -+ <fillField selector="{{AdminProductCustomizableOptionsSection.fillOptionValuePrice(simpleProductCustomizableOption.title,'0')}}" userInput="{{simpleProductCustomizableOption.option_0_price}}" stepKey="fillOptionPrice"/> -+ <selectOption selector="{{AdminProductCustomizableOptionsSection.clickSelectPriceType(simpleProductCustomizableOption.title,'0')}}" userInput="{{simpleProductCustomizableOption.option_0_price_type}}" stepKey="selectOptionPriceType"/> -+ <fillField selector="{{AdminProductCustomizableOptionsSection.fillOptionValueSku(simpleProductCustomizableOption.title,'0')}}" userInput="{{simpleProductCustomizableOption.option_0_sku}}" stepKey="fillOptionSku"/> -+ <scrollToTopOfPage stepKey="scrollToTopOfAdminProductFormSection"/> -+ <click selector="{{AdminProductFormSection.save}}" stepKey="clickSaveButton"/> -+ <waitForPageLoad stepKey="waitForSimpleProductSave"/> -+ -+ <!--Verify customer see success message--> -+ <see selector="{{AdminProductFormSection.successMessage}}" userInput="You saved the product." stepKey="seeAssertSimpleProductSaveSuccessMessage"/> -+ -+ <!--Search updated simple product(from above step) in the grid page--> -+ <amOnPage url="{{ProductCatalogPage.url}}" stepKey="OpenProductCatalogPageToSearchUpdatedSimpleProduct"/> -+ <waitForPageLoad stepKey="waitForProductCatalogPageToLoad"/> -+ <conditionalClick selector="{{AdminProductGridFilterSection.clearAll}}" dependentSelector="{{AdminProductGridFilterSection.clearAll}}" visible="true" stepKey="clickClearAll"/> -+ <click selector="{{AdminProductGridFilterSection.filters}}" stepKey="clickFiltersButton"/> -+ <fillField selector="{{AdminProductGridFilterSection.nameFilter}}" userInput="{{simpleProductRegularPriceCustomOptions.name}}" stepKey="fillSimpleProductNameInNameFilter"/> -+ <fillField selector="{{AdminProductGridFilterSection.skuFilter}}" userInput="{{simpleProductRegularPriceCustomOptions.sku}}" stepKey="fillProductSku"/> -+ <click selector="{{AdminProductGridFilterSection.applyFilters}}" stepKey="clickApplyFiltersButton"/> -+ <click selector="{{AdminProductGridFilterSection.nthRow('1')}}" stepKey="clickFirstRowToVerifyUpdatedSimpleProductVisibleInGrid"/> -+ <waitForPageLoad stepKey="waitUntilSimpleProductPageIsOpened"/> -+ -+ <!-- Verify customer see updated simple product in the product form page --> -+ <seeInField selector="{{AdminProductFormSection.productName}}" userInput="{{simpleProductRegularPriceCustomOptions.name}}" stepKey="seeSimpleProductName"/> -+ <seeInField selector="{{AdminProductFormSection.productSku}}" userInput="{{simpleProductRegularPriceCustomOptions.sku}}" stepKey="seeSimpleProductSku"/> -+ <seeInField selector="{{AdminProductFormSection.productPrice}}" userInput="{{simpleProductRegularPriceCustomOptions.price}}" stepKey="seeSimpleProductPrice"/> -+ <seeInField selector="{{AdminProductFormSection.productQuantity}}" userInput="{{simpleProductRegularPriceCustomOptions.quantity}}" stepKey="seeSimpleProductQuantity"/> -+ <seeInField selector="{{AdminProductFormSection.productStockStatus}}" userInput="{{simpleProductRegularPriceCustomOptions.status}}" stepKey="seeSimpleProductStockStatus"/> -+ <seeInField selector="{{AdminProductFormSection.productWeight}}" userInput="{{simpleProductRegularPriceCustomOptions.weight}}" stepKey="seeSimpleProductWeight"/> -+ <click selector="{{AdminProductFormSection.categoriesDropdown}}" stepKey="clickCategoriesDropDownToVerify"/> -+ <see selector="{{AdminProductFormSection.selectMultipleCategories}}" userInput="$$categoryEntity.name$$" stepKey="selectedCategories" /> -+ <scrollTo selector="{{AdminProductSEOSection.sectionHeader}}" x="0" y="-80" stepKey="scrollToAdminProductSEOSection1"/> -+ <click selector="{{AdminProductSEOSection.sectionHeader}}" stepKey="clickAdminProductSEOSection1"/> -+ <seeInField selector="{{AdminProductSEOSection.urlKeyInput}}" userInput="{{simpleProductRegularPriceCustomOptions.urlKey}}" stepKey="seeUrlKey"/> -+ <click selector="{{AdminProductCustomizableOptionsSection.checkIfCustomizableOptionsTabOpen}}" stepKey="clickAdminProductCustomizableOptionToSeeValues"/> -+ -+ <!-- Verify simple product with customizable options --> -+ <click selector="{{AdminProductCustomizableOptionsSection.addOptionBtn}}" stepKey="clickAddOptionButtonForCustomizableOption"/> -+ <waitForPageLoad stepKey="waitForPageLoad"/> -+ <seeInField selector="{{AdminProductCustomizableOptionsSection.optionTitleInput('0')}}" userInput="{{simpleProductCustomizableOption.title}}" stepKey="seeOptionTitleForCustomizableOption"/> -+ <click selector="{{AdminProductCustomizableOptionsSection.optionTypeDropDown('1')}}" stepKey="selectOptionTypeDropDownForCustomizableOption"/> -+ <click selector="{{AdminProductCustomizableOptionsSection.optionTypeItem('1', simpleProductCustomizableOption.type)}}" stepKey="selectOptionFieldFromDropDownForCustomizableOption"/> -+ <checkOption selector="{{AdminProductCustomizableOptionsSection.requiredCheckBox('0')}}" stepKey="checkRequiredCheckBoxForTheThirdDataSet"/> -+ <seeInField selector="{{AdminProductCustomizableOptionsSection.fillOptionValueTitle(simpleProductCustomizableOption.title,'0')}}" userInput="{{simpleProductCustomizableOption.option_0_title}}" stepKey="seeOptionTitle"/> -+ <seeInField selector="{{AdminProductCustomizableOptionsSection.fillOptionValuePrice(simpleProductCustomizableOption.title,'0')}}" userInput="{{simpleProductCustomizableOption.option_0_price}}" stepKey="seeOptionPrice"/> -+ <seeOptionIsSelected selector="{{AdminProductCustomizableOptionsSection.clickSelectPriceType(simpleProductCustomizableOption.title,'0')}}" userInput="{{simpleProductCustomizableOption.option_0_price_type}}" stepKey="selectOptionValuePriceType"/> -+ <seeInField selector="{{AdminProductCustomizableOptionsSection.fillOptionValueSku(simpleProductCustomizableOption.title,'0')}}" userInput="{{simpleProductCustomizableOption.option_0_sku}}" stepKey="seeOptionSku"/> -+ -+ <!-- Verify customer see updated simple product (from the above step) on the storefront page --> -+ <amOnPage url="{{StorefrontProductPage.url(simpleProductRegularPriceCustomOptions.urlKey)}}" stepKey="goToProductPage"/> -+ <waitForPageLoad stepKey="waitForStorefrontProductPageLoad"/> -+ <see selector="{{StorefrontProductInfoMainSection.productName}}" userInput="{{simpleProductRegularPriceCustomOptions.name}}" stepKey="seeSimpleProductNameOnStoreFrontPage"/> -+ <see selector="{{StorefrontProductInfoMainSection.productPrice}}" userInput="{{simpleProductRegularPriceCustomOptions.price}}" stepKey="seeSimpleProductPriceOnStoreFrontPage"/> -+ <see selector="{{StorefrontProductInfoMainSection.productSku}}" userInput="{{simpleProductRegularPriceCustomOptions.sku}}" stepKey="seeSimpleProductSkuOnStoreFrontPage"/> -+ <grabTextFrom selector="{{StorefrontProductInfoMainSection.productStockStatus}}" stepKey="productStockAvailableStatus"/> -+ <assertEquals stepKey="assertStockAvailableOnProductPage"> -+ <expectedResult type="string">{{simpleProductRegularPriceCustomOptions.storefrontStatus}}</expectedResult> -+ <actualResult type="variable">productStockAvailableStatus</actualResult> -+ </assertEquals> -+ <grabTextFrom selector="{{StorefrontProductInfoMainSection.productPrice}}" stepKey="productPriceAmount"/> -+ <assertEquals stepKey="assertOldPriceTextOnProductPage"> -+ <expectedResult type="string">${{simpleProductRegularPriceCustomOptions.price}}</expectedResult> -+ <actualResult type="variable">productPriceAmount</actualResult> -+ </assertEquals> -+ -+ <!--Verify customer see customizable options are Required --> -+ <seeElement selector="{{StorefrontProductInfoMainSection.requiredCustomSelect(simpleProductCustomizableOption.title)}}" stepKey="verifyFirstCustomOptionIsRequired"/> -+ -+ <!--Verify customer see customizable option titles and prices --> -+ <grabAttributeFrom userInput="for" selector="{{StorefrontProductInfoMainSection.customOptionLabel(simpleProductCustomizableOption.title)}}" stepKey="simpleOptionId"/> -+ <grabMultiple selector="{{StorefrontProductInfoMainSection.customSelectOptions({$simpleOptionId})}}" stepKey="grabFourthOptions"/> -+ <assertEquals stepKey="assertFourthSelectOptions"> -+ <actualResult type="variable">grabFourthOptions</actualResult> -+ <expectedResult type="array">['-- Please Select --', {{simpleProductCustomizableOption.option_0_title}} +$98.00]</expectedResult> -+ </assertEquals> -+ -+ <!-- Verify added Product in cart --> -+ <selectOption selector="{{StorefrontProductPageSection.customOptionDropDown}}" userInput="{{simpleProductCustomizableOption.option_0_title}} +$98.00" stepKey="selectCustomOption"/> -+ <fillField selector="{{StorefrontProductPageSection.qtyInput}}" userInput="1" stepKey="fillProductQuantity"/> -+ <click selector="{{StorefrontProductActionSection.addToCart}}" stepKey="clickOnAddToCartButton"/> -+ <waitForPageLoad stepKey="waitForProductToAddInCart"/> -+ <waitForElementVisible selector="{{StorefrontMessagesSection.success}}" stepKey="waitForSuccessMessage"/> -+ <seeElement selector="{{StorefrontProductPageSection.successMsg}}" stepKey="seeYouAddedSimpleprod4ToYourShoppingCartSuccessSaveMessage"/> -+ <seeElement selector="{{StorefrontMinicartSection.quantity(1)}}" stepKey="seeAddedProductQuantityInCart"/> -+ <click selector="{{StorefrontMinicartSection.showCart}}" stepKey="clickOnMiniCart"/> -+ <see selector="{{StorefrontMinicartSection.miniCartItemsText}}" userInput="{{simpleProductRegularPriceCustomOptions.name}}" stepKey="seeProductNameInMiniCart"/> -+ <see selector="{{StorefrontMinicartSection.miniCartItemsText}}" userInput="{{simpleProductRegularPriceCustomOptions.storefront_new_cartprice}}" stepKey="seeProductPriceInMiniCart"/> -+ </test> -+</tests> -diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductWithRegularPriceOutOfStockTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductWithRegularPriceOutOfStockTest.xml -new file mode 100644 -index 00000000000..0125b4c1e71 ---- /dev/null -+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductWithRegularPriceOutOfStockTest.xml -@@ -0,0 +1,127 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> -+ <test name="AdminUpdateSimpleProductWithRegularPriceOutOfStockTest"> -+ <annotations> -+ <stories value="Update Simple Product"/> -+ <title value="Update Simple Product with Regular Price (Out of Stock)"/> -+ <description value="Test log in to Update Simple Product and Update Simple Product with Regular Price (Out of Stock)"/> -+ <testCaseId value="MC-10806"/> -+ <severity value="CRITICAL"/> -+ <group value="catalog"/> -+ <group value="mtf_migrated"/> -+ <skip> -+ <issueId value="MC-17181"/> -+ </skip> -+ </annotations> -+ <before> -+ <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> -+ <createData entity="SimpleSubCategory" stepKey="initialCategoryEntity"/> -+ <createData entity="defaultSimpleProduct" stepKey="initialSimpleProduct"> -+ <requiredEntity createDataKey="initialCategoryEntity"/> -+ </createData> -+ <createData entity="SimpleSubCategory" stepKey="categoryEntity"/> -+ </before> -+ <after> -+ <deleteData stepKey="deleteSimpleSubCategory" createDataKey="initialCategoryEntity"/> -+ <deleteData stepKey="deleteSimpleSubCategory2" createDataKey="categoryEntity"/> -+ <actionGroup ref="deleteProductBySku" stepKey="deleteCreatedProduct"> -+ <argument name="sku" value="{{simpleProductRegularPrice32503OutOfStock.sku}}"/> -+ </actionGroup> -+ <actionGroup ref="logout" stepKey="logout"/> -+ </after> -+ -+ <!-- Search default simple product in the grid --> -+ <amOnPage url="{{ProductCatalogPage.url}}" stepKey="OpenProductCatalogPage"/> -+ <waitForPageLoad stepKey="waitForProductCatalogPage"/> -+ <actionGroup ref="filterProductGridBySku2" stepKey="filterProductGrid"> -+ <argument name="sku" value="$$initialSimpleProduct.sku$$"/> -+ </actionGroup> -+ <click selector="{{AdminProductGridFilterSection.nthRow('1')}}" stepKey="clickFirstRowToOpenDefaultSimpleProduct"/> -+ <waitForPageLoad stepKey="waitUntilProductIsOpened"/> -+ -+ <!-- Update simple product with regular price(out of stock) --> -+ <fillField selector="{{AdminProductFormSection.productName}}" userInput="{{simpleProductRegularPrice32503OutOfStock.name}}" stepKey="fillSimpleProductName"/> -+ <fillField selector="{{AdminProductFormSection.productSku}}" userInput="{{simpleProductRegularPrice32503OutOfStock.sku}}" stepKey="fillSimpleProductSku"/> -+ <fillField selector="{{AdminProductFormSection.productPrice}}" userInput="{{simpleProductRegularPrice32503OutOfStock.price}}" stepKey="fillSimpleProductPrice"/> -+ <fillField selector="{{AdminProductFormSection.productQuantity}}" userInput="{{simpleProductRegularPrice32503OutOfStock.quantity}}" stepKey="fillSimpleProductQuantity"/> -+ <selectOption selector="{{AdminProductFormSection.stockStatus}}" userInput="{{simpleProductRegularPrice32503OutOfStock.status}}" stepKey="selectStockStatusInStock"/> -+ <fillField selector="{{AdminProductFormSection.productWeight}}" userInput="{{simpleProductRegularPrice32503OutOfStock.weight}}" stepKey="fillSimpleProductWeight"/> -+ <click selector="{{AdminProductFormSection.categoriesDropdown}}" stepKey="clickCategoriesDropDown"/> -+ <fillField selector="{{AdminProductFormSection.searchCategory}}" userInput="$$initialCategoryEntity.name$$" stepKey="fillSearchForInitialCategory"/> -+ <click selector="{{AdminProductFormSection.selectCategory($$initialCategoryEntity.name$$)}}" stepKey="unselectInitialCategory"/> -+ <fillField selector="{{AdminProductFormSection.searchCategory}}" userInput="$$categoryEntity.name$$" stepKey="fillSearchCategory"/> -+ <click selector="{{AdminProductFormSection.selectCategory($$categoryEntity.name$$)}}" stepKey="clickOnCategory"/> -+ <click selector="{{AdminProductFormSection.done}}" stepKey="clickOnDoneAdvancedCategorySelect"/> -+ <click selector="{{AdminProductSEOSection.sectionHeader}}" stepKey="clickAdminProductSEOSection"/> -+ <fillField selector="{{AdminProductSEOSection.urlKeyInput}}" userInput="{{simpleProductRegularPrice32503OutOfStock.urlKey}}" stepKey="fillUrlKey"/> -+ <scrollToTopOfPage stepKey="scrollToTopOfAdminProductFormSection"/> -+ <click selector="{{AdminProductFormSection.save}}" stepKey="clickSaveButton"/> -+ <waitForPageLoad stepKey="waitForSimpleProductSave"/> -+ -+ <!-- Verify customer see success message --> -+ <see selector="{{AdminProductFormSection.successMessage}}" userInput="You saved the product." stepKey="seeAssertSimpleProductSaveSuccessMessage"/> -+ -+ <!-- Search updated simple product(from above step) in the grid page --> -+ <amOnPage url="{{ProductCatalogPage.url}}" stepKey="OpenProductCatalogPageToSearchUpdatedSimpleProduct"/> -+ <waitForPageLoad stepKey="waitForProductCatalogPageToLoad"/> -+ <conditionalClick selector="{{AdminProductGridFilterSection.clearAll}}" dependentSelector="{{AdminProductGridFilterSection.clearAll}}" visible="true" stepKey="clickClearAll"/> -+ <click selector="{{AdminProductGridFilterSection.filters}}" stepKey="clickFiltersButton"/> -+ <fillField selector="{{AdminProductGridFilterSection.nameFilter}}" userInput="{{simpleProductRegularPrice32503OutOfStock.name}}" stepKey="fillSimpleProductNameInNameFilter"/> -+ <fillField selector="{{AdminProductGridFilterSection.skuFilter}}" userInput="{{simpleProductRegularPrice32503OutOfStock.sku}}" stepKey="fillProductSku"/> -+ <click selector="{{AdminProductGridFilterSection.applyFilters}}" stepKey="clickApplyFiltersButton"/> -+ <click selector="{{AdminProductGridFilterSection.nthRow('1')}}" stepKey="clickFirstRowToVerifyUpdatedSimpleProductVisibleInGrid"/> -+ <waitForPageLoad stepKey="waitUntilSimpleProductPageIsOpened"/> -+ -+ <!-- Verify customer see updated simple product in the product form page --> -+ <seeInField selector="{{AdminProductFormSection.productName}}" userInput="{{simpleProductRegularPrice32503OutOfStock.name}}" stepKey="seeSimpleProductName"/> -+ <seeInField selector="{{AdminProductFormSection.productSku}}" userInput="{{simpleProductRegularPrice32503OutOfStock.sku}}" stepKey="seeSimpleProductSku"/> -+ <seeInField selector="{{AdminProductFormSection.productPrice}}" userInput="{{simpleProductRegularPrice32503OutOfStock.price}}" stepKey="seeSimpleProductPrice"/> -+ <seeInField selector="{{AdminProductFormSection.productQuantity}}" userInput="{{simpleProductRegularPrice32503OutOfStock.quantity}}" stepKey="seeSimpleProductQuantity"/> -+ <seeInField selector="{{AdminProductFormSection.productStockStatus}}" userInput="{{simpleProductRegularPrice32503OutOfStock.status}}" stepKey="seeSimpleProductStockStatus"/> -+ <seeInField selector="{{AdminProductFormSection.productWeight}}" userInput="{{simpleProductRegularPrice32503OutOfStock.weight}}" stepKey="seeSimpleProductWeight"/> -+ <click selector="{{AdminProductFormSection.categoriesDropdown}}" stepKey="clickCategoriesDropDownToVerify"/> -+ <see selector="{{AdminProductFormSection.selectMultipleCategories}}" userInput="$$categoryEntity.name$$" stepKey="selectedCategories" /> -+ <scrollTo selector="{{AdminProductSEOSection.sectionHeader}}" x="0" y="-80" stepKey="scrollToAdminProductSEOSection1"/> -+ <click selector="{{AdminProductSEOSection.sectionHeader}}" stepKey="clickAdminProductSEOSection1"/> -+ <seeInField selector="{{AdminProductSEOSection.urlKeyInput}}" userInput="{{simpleProductRegularPrice32503OutOfStock.urlKey}}" stepKey="seeUrlKey"/> -+ -+ <!--Verify customer don't see updated simple product link on category page --> -+ <amOnPage url="{{StorefrontCategoryPage.url($$categoryEntity.name$$)}}" stepKey="openCategoryPage"/> -+ <waitForPageLoad stepKey="waitForCategoryPageLoad"/> -+ <dontSee selector="{{StorefrontCategoryMainSection.productLink}}" userInput="{{simpleProductRegularPrice32503OutOfStock.name}}" stepKey="dontSeeSimpleProductNameOnCategoryPage"/> -+ -+ <!-- Verify customer see updated simple product (from the above step) on the storefront page --> -+ <amOnPage url="{{StorefrontProductPage.url(simpleProductRegularPrice32503OutOfStock.urlKey)}}" stepKey="goToProductPage"/> -+ <waitForPageLoad stepKey="waitForStorefrontProductPageLoad"/> -+ <see selector="{{StorefrontProductInfoMainSection.productName}}" userInput="{{simpleProductRegularPrice32503OutOfStock.name}}" stepKey="seeSimpleProductNameOnStoreFrontPage"/> -+ <see selector="{{StorefrontProductInfoMainSection.productPrice}}" userInput="{{simpleProductRegularPrice32503OutOfStock.price}}" stepKey="seeSimpleProductPriceOnStoreFrontPage"/> -+ <see selector="{{StorefrontProductInfoMainSection.productSku}}" userInput="{{simpleProductRegularPrice32503OutOfStock.sku}}" stepKey="seeSimpleProductSkuOnStoreFrontPage"/> -+ <grabTextFrom selector="{{StorefrontProductInfoMainSection.productStockStatus}}" stepKey="productStockAvailableStatus"/> -+ <assertEquals stepKey="assertStockAvailableOnProductPage"> -+ <expectedResult type="string">{{simpleProductRegularPrice32503OutOfStock.storefrontStatus}}</expectedResult> -+ <actualResult type="variable">productStockAvailableStatus</actualResult> -+ </assertEquals> -+ <grabTextFrom selector="{{StorefrontProductInfoMainSection.productPrice}}" stepKey="productPriceAmount"/> -+ <assertEquals stepKey="assertOldPriceTextOnProductPage"> -+ <expectedResult type="string">${{simpleProductRegularPrice32503OutOfStock.price}}</expectedResult> -+ <actualResult type="variable">productPriceAmount</actualResult> -+ </assertEquals> -+ -+ <!--Verify customer don't see updated simple product link on magento storefront page and is searchable by sku --> -+ <amOnPage url="{{StorefrontProductPage.url(simpleProductRegularPrice32503OutOfStock.urlKey)}}" stepKey="goToMagentoStorefrontPage"/> -+ <waitForPageLoad stepKey="waitForStoreFrontProductPageLoad"/> -+ <fillField selector="{{StorefrontQuickSearchResultsSection.searchTextBox}}" userInput="{{simpleProductRegularPrice32503OutOfStock.sku}}" stepKey="fillSimpleProductSkuInSearchTextBox"/> -+ <waitForPageLoad stepKey="waitForSearchTextBox"/> -+ <click selector="{{StorefrontQuickSearchResultsSection.searchTextBoxButton}}" stepKey="clickSearchTextBoxButton"/> -+ <waitForPageLoad stepKey="waitForSearch"/> -+ <dontSee selector="{{StorefrontQuickSearchResultsSection.productLink}}" userInput="{{simpleProductRegularPrice32503OutOfStock.name}}" stepKey="dontSeeProductName"/> -+ </test> -+</tests> -diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateTopCategoryUrlWithNoRedirectTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateTopCategoryUrlWithNoRedirectTest.xml -new file mode 100644 -index 00000000000..4dea6663e61 ---- /dev/null -+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateTopCategoryUrlWithNoRedirectTest.xml -@@ -0,0 +1,88 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> -+ <test name="AdminUpdateTopCategoryUrlWithNoRedirectTest"> -+ <annotations> -+ <stories value="Update category"/> -+ <title value="Update top category url and do not create redirect"/> -+ <description value="Login as admin and update top category url and do not create redirect"/> -+ <testCaseId value="MC-6056"/> -+ <severity value="CRITICAL"/> -+ <group value="Catalog"/> -+ <group value="mtf_migrated"/> -+ </annotations> -+ -+ <before> -+ <!-- Create three level nested category --> -+ <createData entity="_defaultCategory" stepKey="createDefaultCategory"/> -+ <createData entity="Two_nested_categories" stepKey="createTwoLevelNestedCategories"> -+ <requiredEntity createDataKey="createDefaultCategory"/> -+ </createData> -+ <createData entity="Three_nested_categories" stepKey="createThreeLevelNestedCategories"> -+ <requiredEntity createDataKey="createTwoLevelNestedCategories"/> -+ </createData> -+ -+ <!-- Login as admin --> -+ <actionGroup ref="LoginAsAdmin" stepKey="login"/> -+ </before> -+ <after> -+ <actionGroup ref="logout" stepKey="logout"/> -+ -+ <deleteData createDataKey="createThreeLevelNestedCategories" stepKey="deleteThreeNestedCategories"/> -+ <deleteData createDataKey="createTwoLevelNestedCategories" stepKey="deleteTwoLevelNestedCategory"/> -+ <deleteData createDataKey="createDefaultCategory" stepKey="deleteDefaultCategory"/> -+ </after> -+ -+ <!-- Open Category page --> -+ <amOnPage url="{{AdminCategoryPage.url}}" stepKey="openAdminCategoryIndexPage"/> -+ <waitForPageLoad stepKey="waitForPageToLoaded"/> -+ -+ <!-- Open 3rd Level category --> -+ <click selector="{{AdminCategorySidebarTreeSection.expandAll}}" stepKey="clickOnExpandTree"/> -+ <waitForPageLoad stepKey="waitForCategoryToLoad"/> -+ <click selector="{{AdminCategorySidebarTreeSection.categoryInTree($$createThreeLevelNestedCategories.name$$)}}" stepKey="selectCategory"/> -+ <waitForPageLoad stepKey="waitForPageToLoad"/> -+ -+ <!--Update category UrlKey and uncheck permanent redirect for old URL --> -+ <scrollTo selector="{{AdminCategorySEOSection.SectionHeader}}" x="0" y="-80" stepKey="scrollToSearchEngineOptimization1"/> -+ <click selector="{{AdminCategorySEOSection.SectionHeader}}" stepKey="selectSearchEngineOptimization"/> -+ <waitForPageLoad stepKey="waitForPageToLoad1"/> -+ <fillField selector="{{AdminCategorySEOSection.UrlKeyInput}}" userInput="updatedurl" stepKey="updateUrlKey"/> -+ <uncheckOption selector="{{AdminCategorySEOSection.UrlKeyRedirectCheckbox}}" stepKey="uncheckPermanentRedirectCheckBox"/> -+ <click selector="{{AdminCategorySEOSection.SectionHeader}}" stepKey="selectSearchEngineOptimization1"/> -+ <click selector="{{AdminCategoryMainActionsSection.SaveButton}}" stepKey="saveUpdatedCategory"/> -+ <waitForPageLoad stepKey="waitForCategoryToSave"/> -+ <seeElement selector="{{AdminCategoryMessagesSection.SuccessMessage}}" stepKey="seeSuccessMessage"/> -+ -+ <!-- Get Category Id --> -+ <grabFromCurrentUrl stepKey="categoryId" regex="#\/([0-9]*)?\/$#"/> -+ -+ <!-- Open Url Rewrite Page --> -+ <amOnPage url="{{AdminUrlRewriteIndexPage.url}}" stepKey="openUrlRewriteIndexPage"/> -+ <waitForPageLoad stepKey="waitForUrlRewritePage"/> -+ -+ <!-- Verify third level category's Redirect Path, Target Path and Redirect Type after the URL Update --> -+ <click selector="{{AdminUrlRewriteIndexSection.resetButton}}" stepKey="clickOnResetButton"/> -+ <waitForPageLoad stepKey="waitForPageToLoad0"/> -+ <fillField selector="{{AdminUrlRewriteIndexSection.requestPathFilter}}" userInput="updatedurl" stepKey="fillUpdatedUrlInRedirectPathFilter"/> -+ <click selector="{{AdminUrlRewriteIndexSection.searchButton}}" stepKey="clickOnSearchButton"/> -+ <waitForPageLoad stepKey="waitForPageToLoad2"/> -+ <see stepKey="seeTheRedirectType" selector="{{AdminUrlRewriteIndexSection.redirectTypeColumn('1')}}" userInput="No" /> -+ <see stepKey="seeTheTargetPath" selector="{{AdminUrlRewriteIndexSection.targetPathColumn('1')}}" userInput="catalog/category/view/id/{$categoryId}"/> -+ <see selector="{{AdminUrlRewriteIndexSection.requestPathColumn('1')}}" userInput="$$createDefaultCategory.name$$/$$createTwoLevelNestedCategories.name$$/updatedurl.html" stepKey="seeTheRedirectPath"/> -+ -+ <!-- Verify third level category's old URL path doesn't show redirect path--> -+ <click selector="{{AdminUrlRewriteIndexSection.resetButton}}" stepKey="clickOnResetButton1"/> -+ <waitForPageLoad stepKey="waitForPageToLoad3"/> -+ <fillField selector="{{AdminUrlRewriteIndexSection.requestPathFilter}}" userInput="{{Three_nested_categories.name_lwr}}" stepKey="fillOldUrlInRedirectPathFilter"/> -+ <click selector="{{AdminUrlRewriteIndexSection.searchButton}}" stepKey="clickOnSearchButton1"/> -+ <waitForPageLoad stepKey="waitForPageToLoad4"/> -+ <see stepKey="seeEmptyRecodsMessage" selector="{{AdminUrlRewriteIndexSection.emptyRecords}}" userInput="We couldn't find any records."/> -+ </test> -+</tests> -\ No newline at end of file -diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateTopCategoryUrlWithRedirectTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateTopCategoryUrlWithRedirectTest.xml -new file mode 100644 -index 00000000000..ee1ed5f97ed ---- /dev/null -+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateTopCategoryUrlWithRedirectTest.xml -@@ -0,0 +1,88 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> -+ <test name="AdminUpdateTopCategoryUrlWithRedirectTest"> -+ <annotations> -+ <stories value="Update category"/> -+ <title value="Update top category url and create redirect"/> -+ <description value="Login as admin and update top category url and create redirect"/> -+ <testCaseId value="MC-6057"/> -+ <severity value="CRITICAL"/> -+ <group value="Catalog"/> -+ <group value="mtf_migrated"/> -+ </annotations> -+ -+ <before> -+ <!-- Login as admin --> -+ <actionGroup ref="LoginAsAdmin" stepKey="login"/> -+ <!-- Create three level nested category --> -+ <createData entity="_defaultCategory" stepKey="createDefaultCategory"/> -+ <createData entity="Two_nested_categories" stepKey="createTwoLevelNestedCategories"> -+ <requiredEntity createDataKey="createDefaultCategory"/> -+ </createData> -+ <createData entity="Three_nested_categories" stepKey="createThreeLevelNestedCategories"> -+ <requiredEntity createDataKey="createTwoLevelNestedCategories"/> -+ </createData> -+ </before> -+ <after> -+ <deleteData createDataKey="createThreeLevelNestedCategories" stepKey="deleteThreeNestedCategories"/> -+ <deleteData createDataKey="createTwoLevelNestedCategories" stepKey="deleteTwoLevelNestedCategory"/> -+ <deleteData createDataKey="createDefaultCategory" stepKey="deleteDefaultCategory"/> -+ <actionGroup ref="logout" stepKey="logout"/> -+ </after> -+ -+ <!-- Open Category page --> -+ <amOnPage url="{{AdminCategoryPage.url}}" stepKey="openAdminCategoryIndexPage"/> -+ <waitForPageLoad stepKey="waitForPageToLoaded"/> -+ -+ <!-- Open 3rd Level category --> -+ <click selector="{{AdminCategorySidebarTreeSection.expandAll}}" stepKey="clickOnExpandTree"/> -+ <waitForPageLoad stepKey="waitForCategoryToLoad"/> -+ <click selector="{{AdminCategorySidebarTreeSection.categoryInTree($$createThreeLevelNestedCategories.name$$)}}" stepKey="selectCategory"/> -+ <waitForPageLoad stepKey="waitForPageToLoad"/> -+ -+ <!--Update category UrlKey and check permanent redirect for old URL --> -+ <scrollTo selector="{{AdminCategorySEOSection.SectionHeader}}" x="0" y="-80" stepKey="scrollToSearchEngineOptimization1"/> -+ <click selector="{{AdminCategorySEOSection.SectionHeader}}" stepKey="selectSearchEngineOptimization"/> -+ <waitForPageLoad stepKey="waitForPageToLoad1"/> -+ <fillField selector="{{AdminCategorySEOSection.UrlKeyInput}}" userInput="updateredirecturl" stepKey="updateUrlKey"/> -+ <checkOption selector="{{AdminCategorySEOSection.UrlKeyRedirectCheckbox}}" stepKey="checkPermanentRedirectCheckBox"/> -+ <click selector="{{AdminCategorySEOSection.SectionHeader}}" stepKey="selectSearchEngineOptimization1"/> -+ <click selector="{{AdminCategoryMainActionsSection.SaveButton}}" stepKey="saveUpdatedCategory"/> -+ <waitForPageLoad stepKey="waitForCategoryToSave"/> -+ <seeElement selector="{{AdminCategoryMessagesSection.SuccessMessage}}" stepKey="seeSuccessMessage"/> -+ -+ <!-- Get Category ID --> -+ <grabFromCurrentUrl stepKey="categoryId" regex="#\/([0-9]*)?\/$#"/> -+ -+ <!-- Open Url Rewrite Page --> -+ <amOnPage url="{{AdminUrlRewriteIndexPage.url}}" stepKey="openUrlRewriteIndexPage"/> -+ <waitForPageLoad stepKey="waitForUrlRewritePage"/> -+ -+ <!-- Verify third level category's Redirect Path, Target Path and Redirect Type after the URL update --> -+ <click selector="{{AdminUrlRewriteIndexSection.resetButton}}" stepKey="clickOnResetButton"/> -+ <waitForPageLoad stepKey="waitForPageToLoad2"/> -+ <fillField selector="{{AdminUrlRewriteIndexSection.requestPathFilter}}" userInput="updateredirecturl" stepKey="fillUpdatedURLInRedirectPathFilter"/> -+ <click selector="{{AdminUrlRewriteIndexSection.searchButton}}" stepKey="clickOnSearchButton"/> -+ <waitForPageLoad stepKey="waitForPageToLoad3"/> -+ <see selector="{{AdminUrlRewriteIndexSection.redirectTypeColumn('1')}}" userInput="No" stepKey="seeTheRedirectType"/> -+ <see selector="{{AdminUrlRewriteIndexSection.targetPathColumn('1')}}" userInput="catalog/category/view/id/{$categoryId}" stepKey="seeTheTargetPath"/> -+ <see selector="{{AdminUrlRewriteIndexSection.requestPathColumn('1')}}" userInput="$$createDefaultCategory.name$$/$$createTwoLevelNestedCategories.name$$/updateredirecturl.html" stepKey="seeTheRedirectPath"/> -+ -+ <!-- Verify third level category's Redirect path, Target Path and Redirect type for old URL --> -+ <click selector="{{AdminUrlRewriteIndexSection.resetButton}}" stepKey="clickOnResetButton1"/> -+ <waitForPageLoad stepKey="waitForPageToLoad4"/> -+ <fillField selector="{{AdminUrlRewriteIndexSection.requestPathFilter}}" userInput="$$createThreeLevelNestedCategories.name$$" stepKey="fillOldUrlInRedirectPathFilter"/> -+ <click selector="{{AdminUrlRewriteIndexSection.searchButton}}" stepKey="clickOnSearchButton1"/> -+ <waitForPageLoad stepKey="waitForPageToLoad5"/> -+ <see selector="{{AdminUrlRewriteIndexSection.requestPathColumn('1')}}" userInput="$$createDefaultCategory.name$$/$$createTwoLevelNestedCategories.name$$/$$createThreeLevelNestedCategories.name$$.html" stepKey="seeTheRedirectPathForOldUrl"/> -+ <see selector="{{AdminUrlRewriteIndexSection.targetPathColumn('1')}}" userInput="$$createDefaultCategory.name$$/$$createTwoLevelNestedCategories.name$$/updateredirecturl.html" stepKey="seeTheTargetPathForOldUrl"/> -+ <see selector="{{AdminUrlRewriteIndexSection.redirectTypeColumn('1')}}" userInput="Permanent (301)" stepKey="seeTheRedirectTypeForOldUrl"/> -+ </test> -+</tests> -\ No newline at end of file -diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateVirtualProductWithRegularPriceInStockVisibleInCategoryOnlyTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateVirtualProductWithRegularPriceInStockVisibleInCategoryOnlyTest.xml -new file mode 100644 -index 00000000000..8ac56d09e5b ---- /dev/null -+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateVirtualProductWithRegularPriceInStockVisibleInCategoryOnlyTest.xml -@@ -0,0 +1,157 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> -+ <test name="AdminUpdateVirtualProductWithRegularPriceInStockVisibleInCategoryOnlyTest"> -+ <annotations> -+ <stories value="Update Virtual Product"/> -+ <title value="Update Virtual Product with Regular Price (In Stock) Visible in Category Only"/> -+ <description value="Test log in to Update Virtual Product and Update Virtual Product with Regular Price (In Stock) Visible in Category Only"/> -+ <testCaseId value="MC-6495"/> -+ <severity value="CRITICAL"/> -+ <group value="catalog"/> -+ <group value="mtf_migrated"/> -+ </annotations> -+ -+ <before> -+ <actionGroup ref = "LoginAsAdmin" stepKey="loginAsAdmin"/> -+ <createData entity="SimpleSubCategory" stepKey="initialCategoryEntity"/> -+ <createData entity="defaultVirtualProduct" stepKey="initialVirtualProduct"> -+ <requiredEntity createDataKey="initialCategoryEntity"/> -+ </createData> -+ <createData entity="SimpleSubCategory" stepKey="categoryEntity"/> -+ </before> -+ <after> -+ <deleteData stepKey="deleteSimpleSubCategory" createDataKey="initialCategoryEntity"/> -+ <deleteData stepKey="deleteSimpleSubCategory2" createDataKey="categoryEntity"/> -+ <actionGroup ref="logout" stepKey="logout"/> -+ </after> -+ -+ <!-- Search default virtual product in the grid page --> -+ <amOnPage url="{{ProductCatalogPage.url}}" stepKey="OpenProductCatalogPage1"/> -+ <waitForPageLoad stepKey="waitForProductCatalogPage1"/> -+ <conditionalClick selector="{{AdminProductGridFilterSection.clearAll}}" dependentSelector="{{AdminProductGridFilterSection.clearAll}}" visible="true" stepKey="clickClearAllFilter" /> -+ <fillField selector="{{AdminProductGridFilterSection.keywordSearch}}" userInput="$$initialVirtualProduct.name$$" stepKey="fillVirtualProductNameInKeywordSearch"/> -+ <click selector="{{AdminProductGridFilterSection.keywordSearchButton}}" stepKey="clickKeywordSearchButton"/> -+ <waitForPageLoad stepKey="waitForProductSearch"/> -+ <click selector="{{AdminProductGridFilterSection.nthRow('1')}}" stepKey="clickFirstRowToVerifyCreatedVirtualProduct"/> -+ <waitForPageLoad stepKey="waitUntilProductIsOpened"/> -+ -+ <!-- Update virtual product with regular price --> -+ <fillField selector="{{AdminProductFormSection.productName}}" userInput="{{updateVirtualProductRegularPrice.name}}" stepKey="fillProductName"/> -+ <fillField selector="{{AdminProductFormSection.productSku}}" userInput="{{updateVirtualProductRegularPrice.sku}}" stepKey="fillProductSku"/> -+ <fillField selector="{{AdminProductFormSection.productPrice}}" userInput="{{updateVirtualProductRegularPrice.price}}" stepKey="fillProductPrice"/> -+ <!-- Press enter to validate advanced pricing link --> -+ <pressKey selector="{{AdminProductFormSection.productPrice}}" parameterArray="[\Facebook\WebDriver\WebDriverKeys::ENTER]" stepKey="pressEnterKey"/> -+ <click selector="{{AdminProductFormSection.advancedPricingLink}}" stepKey="clickAdvancedPricingLink"/> -+ <click selector="{{AdminProductFormAdvancedPricingSection.customerGroupPriceAddButton}}" stepKey="clickCustomerGroupPriceAddButton"/> -+ <scrollTo selector="{{AdminProductFormAdvancedPricingSection.productTierPriceQtyInput('0')}}" x="50" y="0" stepKey="scrollToProductTierPriceQuantityInputTextBox"/> -+ <selectOption selector="{{AdminProductFormAdvancedPricingSection.productTierPriceWebsiteSelect('0')}}" userInput="{{tierPriceOnVirtualProduct.website}}" stepKey="selectProductTierPriceWebsiteInput"/> -+ <selectOption selector="{{AdminProductFormAdvancedPricingSection.productTierPriceCustGroupSelect('0')}}" userInput="{{tierPriceOnVirtualProduct.customer_group}}" stepKey="selectProductTierPriceCustomerGroupInput"/> -+ <fillField selector="{{AdminProductFormAdvancedPricingSection.productTierPriceQtyInput('0')}}" userInput="{{tierPriceOnVirtualProduct.qty}}" stepKey="fillProductTierPriceQuantityInput"/> -+ <fillField selector="{{AdminProductFormAdvancedPricingSection.productTierPriceFixedPriceInput('0')}}" userInput="{{tierPriceOnVirtualProduct.price}}" stepKey="selectProductTierPriceFixedPrice"/> -+ <click selector="{{AdminProductFormAdvancedPricingSection.doneButton}}" stepKey="clickDoneButton"/> -+ <selectOption selector="{{AdminProductFormSection.productTaxClass}}" userInput="{{updateVirtualProductRegularPrice.productTaxClass}}" stepKey="selectProductStockClass"/> -+ <fillField selector="{{AdminProductFormSection.productQuantity}}" userInput="{{updateVirtualProductRegularPrice.quantity}}" stepKey="fillProductQuantity"/> -+ <selectOption selector="{{AdminProductFormSection.stockStatus}}" userInput="{{updateVirtualProductRegularPrice.status}}" stepKey="selectStockStatusInStock"/> -+ <click selector="{{AdminProductFormSection.categoriesDropdown}}" stepKey="clickCategoriesDropDown"/> -+ <fillField selector="{{AdminProductFormSection.searchCategory}}" userInput="$$initialCategoryEntity.name$$" stepKey="fillSearchForInitialCategory" /> -+ <waitForPageLoad stepKey="waitForCategory1"/> -+ <click selector="{{AdminProductFormSection.selectCategory($$initialCategoryEntity.name$$)}}" stepKey="unselectInitialCategory"/> -+ <fillField selector="{{AdminProductFormSection.searchCategory}}" userInput="$$categoryEntity.name$$" stepKey="fillSearchCategory" /> -+ <waitForPageLoad stepKey="waitForCategory2"/> -+ <click selector="{{AdminProductFormSection.selectCategory($$categoryEntity.name$$)}}" stepKey="clickOnCategory"/> -+ <click selector="{{AdminProductFormSection.done}}" stepKey="clickOnDoneAdvancedCategorySelect"/> -+ <selectOption selector="{{AdminProductFormSection.visibility}}" userInput="{{updateVirtualProductRegularPrice.visibility}}" stepKey="selectVisibility"/> -+ <click selector="{{AdminProductSEOSection.sectionHeader}}" stepKey="clickAdminProductSEOSection"/> -+ <fillField selector="{{AdminProductSEOSection.urlKeyInput}}" userInput="{{updateVirtualProductRegularPrice.urlKey}}" stepKey="fillUrlKey"/> -+ <scrollToTopOfPage stepKey="scrollToTopOfAdminProductFormSection"/> -+ <click selector="{{AdminProductFormSection.save}}" stepKey="clickSaveButton"/> -+ <waitForPageLoad stepKey="waitForVirtualProductSaved"/> -+ <!-- Verify we see success message --> -+ <see selector="{{AdminProductFormSection.successMessage}}" userInput="You saved the product." stepKey="seeAssertVirtualProductSaveSuccessMessage"/> -+ -+ <!-- Search updated virtual product(from above step) in the grid page --> -+ <amOnPage url="{{ProductCatalogPage.url}}" stepKey="OpenProductCatalogPageToSearchUpdatedVirtualProduct"/> -+ <waitForPageLoad stepKey="waitForProductCatalogPageToLoad"/> -+ <conditionalClick selector="{{AdminProductGridFilterSection.clearAll}}" dependentSelector="{{AdminProductGridFilterSection.clearAll}}" visible="true" stepKey="clickClearAll"/> -+ <click selector="{{AdminProductGridFilterSection.filters}}" stepKey="clickFiltersButton"/> -+ <fillField selector="{{AdminProductGridFilterSection.nameFilter}}" userInput="{{updateVirtualProductRegularPrice.name}}" stepKey="fillVirtualProductNameInNameFilter"/> -+ <fillField selector="{{AdminProductGridFilterSection.skuFilter}}" userInput="{{updateVirtualProductRegularPrice.sku}}" stepKey="fillVirtualProductSku"/> -+ <click selector="{{AdminProductGridFilterSection.applyFilters}}" stepKey="clickApplyFiltersButton"/> -+ <click selector="{{AdminProductGridFilterSection.nthRow('1')}}" stepKey="clickFirstRowToVerifyUpdatedVirtualProductVisibleInGrid"/> -+ <waitForPageLoad stepKey="waitUntilVirtualProductPageIsOpened"/> -+ -+ <!-- Verify we see created virtual product with tier price(from the above step) in the product form page --> -+ <seeInField selector="{{AdminProductFormSection.productName}}" userInput="{{updateVirtualProductRegularPrice.name}}" stepKey="seeProductName"/> -+ <seeInField selector="{{AdminProductFormSection.productSku}}" userInput="{{updateVirtualProductRegularPrice.sku}}" stepKey="seeProductSku"/> -+ <seeInField selector="{{AdminProductFormSection.productPrice}}" userInput="{{updateVirtualProductRegularPrice.price}}" stepKey="seeProductPrice"/> -+ <click selector="{{AdminProductFormSection.advancedPricingLink}}" stepKey="clickAdvancedPricingLink1"/> -+ <seeInField selector="{{AdminProductFormAdvancedPricingSection.productTierPriceWebsiteSelect('0')}}" userInput="{{tierPriceOnVirtualProduct.website}}" stepKey="seeProductTierPriceWebsiteInput"/> -+ <seeInField selector="{{AdminProductFormAdvancedPricingSection.productTierPriceCustGroupSelect('0')}}" userInput="{{tierPriceOnVirtualProduct.customer_group}}" stepKey="seeProductTierPriceCustomerGroupInput"/> -+ <seeInField selector="{{AdminProductFormAdvancedPricingSection.productTierPriceQtyInput('0')}}" userInput="{{tierPriceOnVirtualProduct.qty}}" stepKey="seeProductTierPriceQuantityInput"/> -+ <seeInField selector="{{AdminProductFormAdvancedPricingSection.productTierPriceFixedPriceInput('0')}}" userInput="{{tierPriceOnVirtualProduct.price}}" stepKey="seeProductTierPriceFixedPrice"/> -+ <click selector="{{AdminProductFormAdvancedPricingSection.advancedPricingCloseButton}}" stepKey="clickAdvancedPricingCloseButton"/> -+ <seeInField selector="{{AdminProductFormSection.productTaxClass}}" userInput="{{updateVirtualProductRegularPrice.productTaxClass}}" stepKey="seeProductTaxClass"/> -+ <seeInField selector="{{AdminProductFormSection.productQuantity}}" userInput="{{updateVirtualProductRegularPrice.quantity}}" stepKey="seeProductQuantity"/> -+ <seeInField selector="{{AdminProductFormSection.productStockStatus}}" userInput="{{updateVirtualProductRegularPrice.status}}" stepKey="seeProductStockStatus"/> -+ <click selector="{{AdminProductFormSection.categoriesDropdown}}" stepKey="clickCategoriesDropDownToVerify"/> -+ <grabMultiple selector="{{AdminProductFormSection.selectMultipleCategories}}" stepKey="selectedCategories" /> -+ <assertEquals stepKey="assertSelectedCategories"> -+ <actualResult type="variable">selectedCategories</actualResult> -+ <expectedResult type="array">[$$categoryEntity.name$$]</expectedResult> -+ </assertEquals> -+ <click selector="{{AdminProductFormSection.done}}" stepKey="clickOnDoneOnCategorySelect"/> -+ <seeInField selector="{{AdminProductFormSection.visibility}}" userInput="{{updateVirtualProductRegularPrice.visibility}}" stepKey="seeVisibility"/> -+ <scrollTo selector="{{AdminProductSEOSection.sectionHeader}}" x="0" y="-80" stepKey="scrollToAdminProductSEOSection1"/> -+ <click selector="{{AdminProductSEOSection.sectionHeader}}" stepKey="clickAdminProductSEOSection1"/> -+ <seeInField selector="{{AdminProductSEOSection.urlKeyInput}}" userInput="{{updateVirtualProductRegularPrice.urlKey}}" stepKey="seeUrlKey"/> -+ -+ <!-- Verify customer don't see updated virtual product link on storefront page and is searchable by sku --> -+ <amOnPage url="{{StorefrontProductPage.url(updateVirtualProductRegularPrice.urlKey)}}" stepKey="goToProductPage"/> -+ <waitForPageLoad stepKey="waitForStoreFrontProductPageLoad"/> -+ <fillField selector="{{StorefrontQuickSearchResultsSection.searchTextBox}}" userInput="{{updateVirtualProductRegularPrice.sku}}" stepKey="fillVirtualProductSkuOnStorefrontPage"/> -+ <waitForPageLoad stepKey="waitForSearchTextBox"/> -+ <click selector="{{StorefrontQuickSearchResultsSection.searchTextBoxButton}}" stepKey="clickSearchTextBoxButton"/> -+ <waitForPageLoad stepKey="waitForSearch"/> -+ <dontSee selector="{{StorefrontQuickSearchResultsSection.productLink}}" userInput="{{updateVirtualProductRegularPrice.name}}" stepKey="dontSeeVirtualProductName"/> -+ -+ <!-- Verify customer see updated virtual product in category page --> -+ <amOnPage url="{{StorefrontCategoryPage.url($$categoryEntity.name$$)}}" stepKey="openCategoryPage"/> -+ <waitForPageLoad stepKey="waitForCategoryPageLoad"/> -+ <see selector="{{StorefrontCategoryMainSection.productLink}}" userInput="{{updateVirtualProductRegularPrice.name}}" stepKey="seeVirtualProductLinkOnCategoryPage"/> -+ <grabTextFrom selector="{{StorefrontCategoryMainSection.asLowAs}}" stepKey="tierPriceTextOnCategoryPage"/> -+ <assertEquals stepKey="assertTierPriceTextOnCategoryPage"> -+ <expectedResult type="string">As low as ${{tierPriceOnVirtualProduct.price}}</expectedResult> -+ <actualResult type="variable">tierPriceTextOnCategoryPage</actualResult> -+ </assertEquals> -+ -+ <!-- Verify customer see updated virtual product and tier price on product page --> -+ <amOnPage url="{{StorefrontProductPage.url(updateVirtualProductRegularPrice.urlKey)}}" stepKey="goToStorefrontProductPage"/> -+ <waitForPageLoad stepKey="waitForStoreFrontProductPageToLoad"/> -+ <see selector="{{StorefrontProductInfoMainSection.productName}}" userInput="{{updateVirtualProductRegularPrice.name}}" stepKey="seeVirtualProductNameOnStoreFrontPage"/> -+ <see selector="{{StorefrontProductInfoMainSection.productPrice}}" userInput="{{updateVirtualProductRegularPrice.price}}" stepKey="seeVirtualProductPriceOnStoreFrontPage"/> -+ <see selector="{{StorefrontProductInfoMainSection.productSku}}" userInput="{{updateVirtualProductRegularPrice.sku}}" stepKey="seeVirtualProductSku"/> -+ <grabTextFrom selector="{{StorefrontProductInfoMainSection.productStockStatus}}" stepKey="productStockAvailableStatus"/> -+ <grabTextFrom selector="{{StorefrontProductInfoMainSection.tierPriceText}}" stepKey="tierPriceText"/> -+ <assertEquals stepKey="assertTierPriceTextOnProductPage"> -+ <expectedResult type="string">Buy {{tierPriceOnVirtualProduct.qty}} for ${{tierPriceOnVirtualProduct.price}} each and save 10%</expectedResult> -+ <actualResult type="variable">tierPriceText</actualResult> -+ </assertEquals> -+ <assertEquals stepKey="assertStockAvailableOnProductPage"> -+ <expectedResult type="string">{{updateVirtualProductRegularPrice.storefrontStatus}}</expectedResult> -+ <actualResult type="variable">productStockAvailableStatus</actualResult> -+ </assertEquals> -+ <grabTextFrom selector="{{StorefrontProductInfoMainSection.productPrice}}" stepKey="productPriceAmount"/> -+ <assertEquals stepKey="assertOldPriceTextOnProductPage"> -+ <expectedResult type="string">${{updateVirtualProductRegularPrice.price}}</expectedResult> -+ <actualResult type="variable">productPriceAmount</actualResult> -+ </assertEquals> -+ </test> -+</tests> -diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateVirtualProductWithRegularPriceInStockWithCustomOptionsVisibleInSearchOnlyTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateVirtualProductWithRegularPriceInStockWithCustomOptionsVisibleInSearchOnlyTest.xml -new file mode 100644 -index 00000000000..f621813f4f8 ---- /dev/null -+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateVirtualProductWithRegularPriceInStockWithCustomOptionsVisibleInSearchOnlyTest.xml -@@ -0,0 +1,251 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> -+ <test name="AdminUpdateVirtualProductWithRegularPriceInStockWithCustomOptionsVisibleInSearchOnlyTest"> -+ <annotations> -+ <stories value="Update Virtual Product"/> -+ <title value="Update Virtual Product with Regular Price (In Stock) with Custom Options Visible in Search Only"/> -+ <description value="Test log in to Update Virtual Product and Update Virtual Product with Regular Price (In Stock) with Custom Options Visible in Search Only"/> -+ <testCaseId value="MC-6641"/> -+ <severity value="CRITICAL"/> -+ <group value="catalog"/> -+ <group value="mtf_migrated"/> -+ </annotations> -+ -+ <before> -+ <actionGroup ref = "LoginAsAdmin" stepKey="loginAsAdmin"/> -+ <createData entity="SimpleSubCategory" stepKey="initialCategoryEntity"/> -+ <createData entity="defaultVirtualProduct" stepKey="initialVirtualProduct"> -+ <requiredEntity createDataKey="initialCategoryEntity"/> -+ </createData> -+ <createData entity="SimpleSubCategory" stepKey="categoryEntity"/> -+ </before> -+ <after> -+ <deleteData stepKey="deleteSimpleSubCategory" createDataKey="initialCategoryEntity"/> -+ <deleteData stepKey="deleteSimpleSubCategory2" createDataKey="categoryEntity"/> -+ <actionGroup ref="logout" stepKey="logout"/> -+ </after> -+ -+ <!-- Search default virtual product in the grid page --> -+ <amOnPage url="{{ProductCatalogPage.url}}" stepKey="OpenProductCatalogPage1"/> -+ <waitForPageLoad stepKey="waitForProductCatalogPage1"/> -+ <conditionalClick selector="{{AdminProductGridFilterSection.clearAll}}" dependentSelector="{{AdminProductGridFilterSection.clearAll}}" visible="true" stepKey="clickClearAllFilter" /> -+ <fillField selector="{{AdminProductGridFilterSection.keywordSearch}}" userInput="$$initialVirtualProduct.name$$" stepKey="fillVirtualProductNameInKeywordSearch"/> -+ <click selector="{{AdminProductGridFilterSection.keywordSearchButton}}" stepKey="clickKeywordSearchButton"/> -+ <waitForPageLoad stepKey="waitForProductSearch"/> -+ <click selector="{{AdminProductGridFilterSection.nthRow('1')}}" stepKey="clickFirstRowToVerifyCreatedVirtualProduct"/> -+ <waitForPageLoad stepKey="waitUntilProductIsOpened"/> -+ -+ <!-- Update virtual product with regular price(in stock) --> -+ <fillField selector="{{AdminProductFormSection.productName}}" userInput="{{updateVirtualProductRegularPriceInStock.name}}" stepKey="fillProductName"/> -+ <fillField selector="{{AdminProductFormSection.productSku}}" userInput="{{updateVirtualProductRegularPriceInStock.sku}}" stepKey="fillProductSku"/> -+ <fillField selector="{{AdminProductFormSection.productPrice}}" userInput="{{updateVirtualProductRegularPriceInStock.price}}" stepKey="fillProductPrice"/> -+ <selectOption selector="{{AdminProductFormSection.productTaxClass}}" userInput="{{updateVirtualProductRegularPriceInStock.productTaxClass}}" stepKey="selectProductTaxClass"/> -+ <fillField selector="{{AdminProductFormSection.productQuantity}}" userInput="{{updateVirtualProductRegularPriceInStock.quantity}}" stepKey="fillProductQuantity"/> -+ <selectOption selector="{{AdminProductFormSection.stockStatus}}" userInput="{{updateVirtualProductRegularPriceInStock.status}}" stepKey="selectStockStatusInStock"/> -+ <click selector="{{AdminProductFormSection.categoriesDropdown}}" stepKey="clickCategoriesDropDown"/> -+ <fillField selector="{{AdminProductFormSection.searchCategory}}" userInput="$$initialCategoryEntity.name$$" stepKey="fillSearchForInitialCategory" /> -+ <waitForPageLoad stepKey="waitForCategory1"/> -+ <click selector="{{AdminProductFormSection.selectCategory($$initialCategoryEntity.name$$)}}" stepKey="unselectInitialCategory"/> -+ <fillField selector="{{AdminProductFormSection.searchCategory}}" userInput="$$categoryEntity.name$$" stepKey="fillSearchCategory" /> -+ <waitForPageLoad stepKey="waitForCategory2"/> -+ <click selector="{{AdminProductFormSection.selectCategory($$categoryEntity.name$$)}}" stepKey="clickOnCategory"/> -+ <click selector="{{AdminProductFormSection.done}}" stepKey="clickOnDoneAdvancedCategorySelect"/> -+ <selectOption selector="{{AdminProductFormSection.visibility}}" userInput="{{updateVirtualProductRegularPriceInStock.visibility}}" stepKey="selectVisibility"/> -+ <click selector="{{AdminProductSEOSection.sectionHeader}}" stepKey="clickAdminProductSEOSection"/> -+ <fillField selector="{{AdminProductSEOSection.urlKeyInput}}" userInput="{{updateVirtualProductRegularPriceInStock.urlKey}}" stepKey="fillUrlKey"/> -+ <click selector="{{AdminProductCustomizableOptionsSection.checkIfCustomizableOptionsTabOpen}}" stepKey="clickAdminProductCustomizableOption"/> -+ <!-- Create virtual product with customizable options dataSet1 --> -+ <click selector="{{AdminProductCustomizableOptionsSection.addOptionBtn}}" stepKey="clickAddOptionButton"/> -+ <waitForPageLoad stepKey="waitForFirstOption"/> -+ <fillField selector="{{AdminProductCustomizableOptionsSection.optionTitleInput('0')}}" userInput="{{virtualProductCustomizableOption1.title}}" stepKey="fillOptionTitleForFirstDataSet"/> -+ <click selector="{{AdminProductCustomizableOptionsSection.optionTypeDropDown('1')}}" stepKey="selectOptionTypeDropDownFirstDataSet"/> -+ <click selector="{{AdminProductCustomizableOptionsSection.optionTypeItem('1', virtualProductCustomizableOption1.type)}}" stepKey="selectOptionFieldFromDropDownForFirstDataSet"/> -+ <checkOption selector="{{AdminProductCustomizableOptionsSection.requiredCheckBox('0')}}" stepKey="checkRequiredCheckBoxForFirstDataSet"/> -+ <fillField selector="{{AdminProductCustomizableOptionsSection.optionPrice('0')}}" userInput="{{virtualProductCustomizableOption1.option_0_price}}" stepKey="fillOptionPriceForFirstDataSet"/> -+ <selectOption selector="{{AdminProductCustomizableOptionsSection.optionPriceType('0')}}" userInput="{{virtualProductCustomizableOption1.option_0_price_type}}" stepKey="selectOptionPriceTypeForFirstDataSet"/> -+ <fillField selector="{{AdminProductCustomizableOptionsSection.optionSku('0')}}" userInput="{{virtualProductCustomizableOption1.option_0_sku}}" stepKey="fillOptionSkuForFirstDataSet"/> -+ <fillField selector="{{AdminProductCustomizableOptionsSection.maxCharactersInput('0')}}" userInput="{{virtualProductCustomizableOption1.option_0_max_characters}}" stepKey="fillOptionMaxCharactersForFirstDataSet"/> -+ <!--Create virtual product with customizable options dataSet2 --> -+ <click selector="{{AdminProductCustomizableOptionsSection.addOptionBtn}}" stepKey="clickAddOptionButtonForSecondDataSet"/> -+ <waitForPageLoad stepKey="waitForSecondDataSetToLoad"/> -+ <fillField selector="{{AdminProductCustomizableOptionsSection.optionTitleInput('1')}}" userInput="{{virtualProductCustomizableOption2.title}}" stepKey="fillOptionTitleForSecondDataSet"/> -+ <click selector="{{AdminProductCustomizableOptionsSection.optionTypeDropDown('2')}}" stepKey="selectOptionTypeDropDownSecondDataSet"/> -+ <click selector="{{AdminProductCustomizableOptionsSection.optionTypeItem('2', virtualProductCustomizableOption2.type)}}" stepKey="selectOptionFieldFromDropDownForSecondDataSet"/> -+ <checkOption selector="{{AdminProductCustomizableOptionsSection.requiredCheckBox('1')}}" stepKey="checkRequiredCheckBoxForSecondDataSet"/> -+ <fillField selector="{{AdminProductCustomizableOptionsSection.optionPrice('1')}}" userInput="{{virtualProductCustomizableOption2.option_0_price}}" stepKey="fillOptionPriceForSecondDataSet"/> -+ <selectOption selector="{{AdminProductCustomizableOptionsSection.optionPriceType('1')}}" userInput="{{virtualProductCustomizableOption2.option_0_price_type}}" stepKey="selectOptionPriceTypeForSecondDataSet"/> -+ <fillField selector="{{AdminProductCustomizableOptionsSection.optionSku('1')}}" userInput="{{virtualProductCustomizableOption2.option_0_sku}}" stepKey="fillOptionSkuForSecondDataSet"/> -+ <fillField selector="{{AdminProductCustomizableOptionsSection.maxCharactersInput('1')}}" userInput="{{virtualProductCustomizableOption2.option_0_max_characters}}" stepKey="fillOptionMaxCharactersForSecondDataSet"/> -+ <!-- Create virtual product with customizable options dataSet3 --> -+ <click selector="{{AdminProductCustomizableOptionsSection.addOptionBtn}}" stepKey="clickAddOptionButtonForThirdSetOfData"/> -+ <waitForPageLoad stepKey="waitForThirdSetOfDataToLoad"/> -+ <fillField selector="{{AdminProductCustomizableOptionsSection.optionTitleInput('2')}}" userInput="{{virtualProductCustomizableOption3.title}}" stepKey="fillOptionTitleForThirdDataSet"/> -+ <click selector="{{AdminProductCustomizableOptionsSection.optionTypeDropDown('3')}}" stepKey="selectOptionTypeDropDownForThirdDataSet"/> -+ <click selector="{{AdminProductCustomizableOptionsSection.optionTypeItem('3', virtualProductCustomizableOption3.type)}}" stepKey="selectOptionFieldFromDropDownForThirdDataSet"/> -+ <checkOption selector="{{AdminProductCustomizableOptionsSection.requiredCheckBox('2')}}" stepKey="checkRequiredCheckBoxForThirdDataSet"/> -+ <click selector="{{AdminProductCustomizableOptionsSection.addValue}}" stepKey="clickAddOptionButtonForThirdDataSetToAddFirstRow"/> -+ <fillField selector="{{AdminProductCustomizableOptionsSection.fillOptionValueTitle(virtualProductCustomizableOption3.title,'0')}}" userInput="{{virtualProductCustomizableOption3.option_0_title}}" stepKey="fillOptionTitleForThirdDataSetFirstRow"/> -+ <fillField selector="{{AdminProductCustomizableOptionsSection.fillOptionValuePrice(virtualProductCustomizableOption3.title,'0')}}" userInput="{{virtualProductCustomizableOption3.option_0_price}}" stepKey="fillOptionPriceForThirdDataSetFirstRow"/> -+ <selectOption selector="{{AdminProductCustomizableOptionsSection.clickSelectPriceType(virtualProductCustomizableOption3.title,'0')}}" userInput="{{virtualProductCustomizableOption3.option_0_price_type}}" stepKey="selectOptionPriceTypeForThirdDataSetFirstRow"/> -+ <fillField selector="{{AdminProductCustomizableOptionsSection.fillOptionValueSku(virtualProductCustomizableOption3.title,'0')}}" userInput="{{virtualProductCustomizableOption3.option_0_sku}}" stepKey="fillOptionSkuForThirdDataSetFirstRow"/> -+ <click selector="{{AdminProductCustomizableOptionsSection.addValue}}" stepKey="clickAddOptionButtonForThirdDataSetToAddSecondRow"/> -+ <fillField selector="{{AdminProductCustomizableOptionsSection.fillOptionValueTitle(virtualProductCustomizableOption3.title,'1')}}" userInput="{{virtualProductCustomizableOption3.option_1_title}}" stepKey="fillOptionTitleForThirdDataSetSecondRow"/> -+ <fillField selector="{{AdminProductCustomizableOptionsSection.fillOptionValuePrice(virtualProductCustomizableOption3.title,'1')}}" userInput="{{virtualProductCustomizableOption3.option_1_price}}" stepKey="fillOptionPriceForThirdDataSetSecondRow"/> -+ <selectOption selector="{{AdminProductCustomizableOptionsSection.clickSelectPriceType(virtualProductCustomizableOption3.title,'1')}}" userInput="{{virtualProductCustomizableOption3.option_1_price_type}}" stepKey="selectOptionPriceTypeForThirdDataSetSecondRow"/> -+ <fillField selector="{{AdminProductCustomizableOptionsSection.fillOptionValueSku(virtualProductCustomizableOption3.title,'1')}}" userInput="{{virtualProductCustomizableOption3.option_1_sku}}" stepKey="fillOptionSkuForThirdDataSetSecondRow"/> -+ <!-- Create virtual product with customizable options dataSet4 --> -+ <scrollToTopOfPage stepKey="scrollToAddOptionButton"/> -+ <click selector="{{AdminProductCustomizableOptionsSection.addOptionBtn}}" stepKey="clickAddOptionButtonForFourthDataSet"/> -+ <waitForPageLoad stepKey="waitForFourthDataSetToLoad"/> -+ <fillField selector="{{AdminProductCustomizableOptionsSection.optionTitleInput('3')}}" userInput="{{virtualProductCustomizableOption4.title}}" stepKey="fillOptionTitleForFourthDataSet"/> -+ <click selector="{{AdminProductCustomizableOptionsSection.optionTypeDropDown('4')}}" stepKey="selectOptionTypeDropDownForFourthSetOfData"/> -+ <click selector="{{AdminProductCustomizableOptionsSection.optionTypeItem('4', virtualProductCustomizableOption4.type)}}" stepKey="selectOptionFieldFromDropDownForFourthDataSet"/> -+ <checkOption selector="{{AdminProductCustomizableOptionsSection.requiredCheckBox('3')}}" stepKey="checkRequiredCheckBoxForFourthDataSet"/> -+ <click selector="{{AdminProductCustomizableOptionsSection.addValue}}" stepKey="clickAddOptionButtonForFourthDataSetToAddFirstRow"/> -+ <fillField selector="{{AdminProductCustomizableOptionsSection.fillOptionValueTitle(virtualProductCustomizableOption4.title,'0')}}" userInput="{{virtualProductCustomizableOption4.option_0_title}}" stepKey="fillOptionTitleForFourthDataSetFirstRow"/> -+ <fillField selector="{{AdminProductCustomizableOptionsSection.fillOptionValuePrice(virtualProductCustomizableOption4.title,'0')}}" userInput="{{virtualProductCustomizableOption4.option_0_price}}" stepKey="fillOptionPriceForFourthDataSetFirstRow"/> -+ <selectOption selector="{{AdminProductCustomizableOptionsSection.clickSelectPriceType(virtualProductCustomizableOption4.title,'0')}}" userInput="{{virtualProductCustomizableOption4.option_0_price_type}}" stepKey="selectOptionPriceTypeForFourthDataSetFirstRow"/> -+ <fillField selector="{{AdminProductCustomizableOptionsSection.fillOptionValueSku(virtualProductCustomizableOption4.title,'0')}}" userInput="{{virtualProductCustomizableOption4.option_0_sku}}" stepKey="fillOptionSkuForFourthDataSetFirstRow"/> -+ <click selector="{{AdminProductCustomizableOptionsSection.addValue}}" stepKey="clickAddOptionButtonForFourthDataSetToAddSecondRow"/> -+ <fillField selector="{{AdminProductCustomizableOptionsSection.fillOptionValueTitle(virtualProductCustomizableOption4.title,'1')}}" userInput="{{virtualProductCustomizableOption4.option_1_title}}" stepKey="fillOptionTitleForFourthDataSetSecondRow"/> -+ <fillField selector="{{AdminProductCustomizableOptionsSection.fillOptionValuePrice(virtualProductCustomizableOption4.title,'1')}}" userInput="{{virtualProductCustomizableOption4.option_1_price}}" stepKey="fillOptionPriceForFourthDataSetSecondRow"/> -+ <selectOption selector="{{AdminProductCustomizableOptionsSection.clickSelectPriceType(virtualProductCustomizableOption4.title,'1')}}" userInput="{{virtualProductCustomizableOption4.option_1_price_type}}" stepKey="selectOptionPriceTypeForFourthDataSetSecondRow"/> -+ <fillField selector="{{AdminProductCustomizableOptionsSection.fillOptionValueSku(virtualProductCustomizableOption4.title,'1')}}" userInput="{{virtualProductCustomizableOption4.option_1_sku}}" stepKey="fillOptionSkuForFourthDataSetSecondRow"/> -+ <scrollToTopOfPage stepKey="scrollToTopOfAdminProductFormSection"/> -+ <click selector="{{AdminProductFormSection.save}}" stepKey="clickSaveButton"/> -+ <waitForPageLoad stepKey="waitForVirtualProductSaved"/> -+ <!-- Verify we see success message --> -+ <see selector="{{AdminProductFormSection.successMessage}}" userInput="You saved the product." stepKey="seeAssertVirtualProductSaveSuccessMessage"/> -+ -+ <!-- Search updated virtual product(from above step) in the grid page --> -+ <amOnPage url="{{ProductCatalogPage.url}}" stepKey="OpenProductCatalogPageToSearchUpdatedVirtualProduct"/> -+ <waitForPageLoad stepKey="waitForProductCatalogPageToLoad"/> -+ <conditionalClick selector="{{AdminProductGridFilterSection.clearAll}}" dependentSelector="{{AdminProductGridFilterSection.clearAll}}" visible="true" stepKey="clickClearAll"/> -+ <click selector="{{AdminProductGridFilterSection.filters}}" stepKey="clickFiltersButton"/> -+ <fillField selector="{{AdminProductGridFilterSection.nameFilter}}" userInput="{{updateVirtualProductRegularPriceInStock.name}}" stepKey="fillVirtualProductNameInNameFilter"/> -+ <fillField selector="{{AdminProductGridFilterSection.skuFilter}}" userInput="{{updateVirtualProductRegularPriceInStock.sku}}" stepKey="fillVirtualProductSku"/> -+ <click selector="{{AdminProductGridFilterSection.applyFilters}}" stepKey="clickApplyFiltersButton"/> -+ <click selector="{{AdminProductGridFilterSection.nthRow('1')}}" stepKey="clickFirstRowToVerifyUpdatedVirtualProductVisibleInGrid"/> -+ <waitForPageLoad stepKey="waitUntilVirtualProductPageIsOpened"/> -+ -+ <!-- Verify we customer see updated virtual product in the product form page --> -+ <seeInField selector="{{AdminProductFormSection.productName}}" userInput="{{updateVirtualProductRegularPriceInStock.name}}" stepKey="seeProductName"/> -+ <seeInField selector="{{AdminProductFormSection.productSku}}" userInput="{{updateVirtualProductRegularPriceInStock.sku}}" stepKey="seeProductSku"/> -+ <seeInField selector="{{AdminProductFormSection.productPrice}}" userInput="{{updateVirtualProductRegularPriceInStock.price}}" stepKey="seeProductPrice"/> -+ <seeInField selector="{{AdminProductFormSection.productTaxClass}}" userInput="{{updateVirtualProductRegularPriceInStock.productTaxClass}}" stepKey="seeProductTaxClass"/> -+ <seeInField selector="{{AdminProductFormSection.productQuantity}}" userInput="{{updateVirtualProductRegularPriceInStock.quantity}}" stepKey="seeProductQuantity"/> -+ <seeInField selector="{{AdminProductFormSection.productStockStatus}}" userInput="{{updateVirtualProductRegularPriceInStock.status}}" stepKey="seeProductStockStatus"/> -+ <click selector="{{AdminProductFormSection.categoriesDropdown}}" stepKey="clickCategoriesDropDownToVerify"/> -+ <grabMultiple selector="{{AdminProductFormSection.selectMultipleCategories}}" stepKey="selectedCategories" /> -+ <assertEquals stepKey="assertSelectedCategories"> -+ <actualResult type="variable">selectedCategories</actualResult> -+ <expectedResult type="array">[$$categoryEntity.name$$]</expectedResult> -+ </assertEquals> -+ <click selector="{{AdminProductFormSection.done}}" stepKey="clickOnDoneOnCategorySelect"/> -+ <seeInField selector="{{AdminProductFormSection.visibility}}" userInput="{{updateVirtualProductRegularPriceInStock.visibility}}" stepKey="seeVisibility"/> -+ <scrollTo selector="{{AdminProductSEOSection.sectionHeader}}" x="0" y="-80" stepKey="scrollToAdminProductSEOSection1"/> -+ <click selector="{{AdminProductSEOSection.sectionHeader}}" stepKey="clickAdminProductSEOSection1"/> -+ <seeInField selector="{{AdminProductSEOSection.urlKeyInput}}" userInput="{{updateVirtualProductRegularPriceInStock.urlKey}}" stepKey="seeUrlKey"/> -+ <click selector="{{AdminProductCustomizableOptionsSection.checkIfCustomizableOptionsTabOpen}}" stepKey="clickAdminProductCustomizableOptionToSeeValues"/> -+ <!-- Create virtual product with customizable options dataSet1 --> -+ <click selector="{{AdminProductCustomizableOptionsSection.addOptionBtn}}" stepKey="clickAddOptionButton1"/> -+ <waitForPageLoad stepKey="waitForFirstOptionToLoad"/> -+ <seeInField selector="{{AdminProductCustomizableOptionsSection.optionTitleInput('0')}}" userInput="{{virtualProductCustomizableOption1.title}}" stepKey="seeOptionTitleForFirstDataSet"/> -+ <click selector="{{AdminProductCustomizableOptionsSection.optionTypeDropDown('1')}}" stepKey="selectOptionTypeDropDownFirstDataSet1"/> -+ <click selector="{{AdminProductCustomizableOptionsSection.optionTypeItem('1', virtualProductCustomizableOption1.type)}}" stepKey="selectOptionFieldFromDropDownForFirstDataSet1"/> -+ <checkOption selector="{{AdminProductCustomizableOptionsSection.requiredCheckBox('0')}}" stepKey="checkRequiredCheckBoxForFirstDataSet1"/> -+ <seeInField selector="{{AdminProductCustomizableOptionsSection.optionPrice('0')}}" userInput="{{virtualProductCustomizableOption1.option_0_price}}" stepKey="seeOptionPriceForFirstDataSet"/> -+ <seeOptionIsSelected selector="{{AdminProductCustomizableOptionsSection.optionPriceType('0')}}" userInput="{{virtualProductCustomizableOption1.option_0_price_type}}" stepKey="selectOptionPriceTypeForFirstDataSet1"/> -+ <seeInField selector="{{AdminProductCustomizableOptionsSection.optionSku('0')}}" userInput="{{virtualProductCustomizableOption1.option_0_sku}}" stepKey="seeOptionSkuForFirstDataSet"/> -+ <seeInField selector="{{AdminProductCustomizableOptionsSection.maxCharactersInput('0')}}" userInput="{{virtualProductCustomizableOption1.option_0_max_characters}}" stepKey="seeOptionMaxCharactersForFirstDataSet"/> -+ <!--Create virtual product with customizable options dataSet2 --> -+ <click selector="{{AdminProductCustomizableOptionsSection.addOptionBtn}}" stepKey="clickAddOptionButtonForSecondDataSetToSeeFields"/> -+ <waitForPageLoad stepKey="waitForTheSecondDataSetToLoad"/> -+ <seeInField selector="{{AdminProductCustomizableOptionsSection.optionTitleInput('1')}}" userInput="{{virtualProductCustomizableOption2.title}}" stepKey="seeOptionTitleForSecondDataSet"/> -+ <click selector="{{AdminProductCustomizableOptionsSection.optionTypeDropDown('2')}}" stepKey="selectOptionTypeDropDownSecondDataSet2"/> -+ <click selector="{{AdminProductCustomizableOptionsSection.optionTypeItem('2', virtualProductCustomizableOption2.type)}}" stepKey="selectOptionFieldFromDropDownForSecondDataSet2"/> -+ <checkOption selector="{{AdminProductCustomizableOptionsSection.requiredCheckBox('1')}}" stepKey="checkRequiredCheckBoxForTheSecondDataSet"/> -+ <seeInField selector="{{AdminProductCustomizableOptionsSection.optionPrice('1')}}" userInput="{{virtualProductCustomizableOption2.option_0_price}}" stepKey="seeOptionPriceForSecondDataSet"/> -+ <seeOptionIsSelected selector="{{AdminProductCustomizableOptionsSection.optionPriceType('1')}}" userInput="{{virtualProductCustomizableOption2.option_0_price_type}}" stepKey="selectOptionPriceTypeForTheSecondDataSet"/> -+ <seeInField selector="{{AdminProductCustomizableOptionsSection.optionSku('1')}}" userInput="{{virtualProductCustomizableOption2.option_0_sku}}" stepKey="seeOptionSkuForSecondDataSet"/> -+ <seeInField selector="{{AdminProductCustomizableOptionsSection.maxCharactersInput('1')}}" userInput="{{virtualProductCustomizableOption2.option_0_max_characters}}" stepKey="seeOptionMaxCharactersForSecondDataSet"/> -+ <!-- Create virtual product with customizable options dataSet3 --> -+ <click selector="{{AdminProductCustomizableOptionsSection.addOptionBtn}}" stepKey="clickAddOptionButtonForTheThirdSetOfData"/> -+ <waitForPageLoad stepKey="waitForTheThirdSetOfDataToLoad"/> -+ <seeInField selector="{{AdminProductCustomizableOptionsSection.optionTitleInput('2')}}" userInput="{{virtualProductCustomizableOption3.title}}" stepKey="seeOptionTitleForThirdDataSet"/> -+ <click selector="{{AdminProductCustomizableOptionsSection.optionTypeDropDown('3')}}" stepKey="selectOptionTypeDropDownForTheThirdDataSet"/> -+ <click selector="{{AdminProductCustomizableOptionsSection.optionTypeItem('3', virtualProductCustomizableOption3.type)}}" stepKey="selectOptionFieldFromDropDownForTheThirdDataSet"/> -+ <checkOption selector="{{AdminProductCustomizableOptionsSection.requiredCheckBox('2')}}" stepKey="checkRequiredCheckBoxForTheThirdDataSet"/> -+ <seeInField selector="{{AdminProductCustomizableOptionsSection.fillOptionValueTitle(virtualProductCustomizableOption3.title,'0')}}" userInput="{{virtualProductCustomizableOption3.option_0_title}}" stepKey="seeOptionTitleForThirdDataSetFirstRow"/> -+ <seeInField selector="{{AdminProductCustomizableOptionsSection.fillOptionValuePrice(virtualProductCustomizableOption3.title,'0')}}" userInput="{{virtualProductCustomizableOption3.option_0_price}}" stepKey="seeOptionPriceForThirdDataSetFirstRow"/> -+ <seeOptionIsSelected selector="{{AdminProductCustomizableOptionsSection.clickSelectPriceType(virtualProductCustomizableOption3.title,'0')}}" userInput="{{virtualProductCustomizableOption3.option_0_price_type}}" stepKey="selectOptionPriceTypeForTheThirdDataSetFirstRow"/> -+ <seeInField selector="{{AdminProductCustomizableOptionsSection.fillOptionValueSku(virtualProductCustomizableOption3.title,'0')}}" userInput="{{virtualProductCustomizableOption3.option_0_sku}}" stepKey="seeOptionSkuForThirdDataSetFirstRow"/> -+ <seeInField selector="{{AdminProductCustomizableOptionsSection.fillOptionValueTitle(virtualProductCustomizableOption3.title,'1')}}" userInput="{{virtualProductCustomizableOption3.option_1_title}}" stepKey="seeOptionTitleForThirdDataSetSecondRow"/> -+ <seeInField selector="{{AdminProductCustomizableOptionsSection.fillOptionValuePrice(virtualProductCustomizableOption3.title,'1')}}" userInput="{{virtualProductCustomizableOption3.option_1_price}}" stepKey="seeOptionPriceForThirdDataSetSecondRow"/> -+ <selectOption selector="{{AdminProductCustomizableOptionsSection.clickSelectPriceType(virtualProductCustomizableOption3.title,'1')}}" userInput="{{virtualProductCustomizableOption3.option_1_price_type}}" stepKey="selectOptionPriceTypeForTheThirdDataSetSecondRow"/> -+ <seeInField selector="{{AdminProductCustomizableOptionsSection.fillOptionValueSku(virtualProductCustomizableOption3.title,'1')}}" userInput="{{virtualProductCustomizableOption3.option_1_sku}}" stepKey="seeOptionSkuForThirdDataSetSecondRow"/> -+ <!-- Create virtual product with customizable options dataSet4 --> -+ <scrollToTopOfPage stepKey="scrollToTheAddOptionButton"/> -+ <click selector="{{AdminProductCustomizableOptionsSection.addOptionBtn}}" stepKey="clickAddOptionButtonForTheFourthDataSet"/> -+ <waitForPageLoad stepKey="waitForTheFourthDataSetToLoad"/> -+ <fillField selector="{{AdminProductCustomizableOptionsSection.optionTitleInput('3')}}" userInput="{{virtualProductCustomizableOption4.title}}" stepKey="fillOptionTitleForTheFourthDataSet"/> -+ <click selector="{{AdminProductCustomizableOptionsSection.optionTypeDropDown('4')}}" stepKey="selectOptionTypeDropDownForTheFourthSetOfData"/> -+ <click selector="{{AdminProductCustomizableOptionsSection.optionTypeItem('4', virtualProductCustomizableOption4.type)}}" stepKey="selectOptionFieldFromDropDownForTheFourthDataSet"/> -+ <checkOption selector="{{AdminProductCustomizableOptionsSection.requiredCheckBox('3')}}" stepKey="checkRequiredCheckBoxForTheFourthDataSet"/> -+ <seeInField selector="{{AdminProductCustomizableOptionsSection.fillOptionValueTitle(virtualProductCustomizableOption4.title,'0')}}" userInput="{{virtualProductCustomizableOption4.option_0_title}}" stepKey="seeOptionTitleForFourthDataSetFirstRow"/> -+ <seeInField selector="{{AdminProductCustomizableOptionsSection.fillOptionValuePrice(virtualProductCustomizableOption4.title,'0')}}" userInput="{{virtualProductCustomizableOption4.option_0_price}}" stepKey="seeOptionPriceForFourthDataSetFirstRow"/> -+ <seeOptionIsSelected selector="{{AdminProductCustomizableOptionsSection.clickSelectPriceType(virtualProductCustomizableOption4.title,'0')}}" userInput="{{virtualProductCustomizableOption4.option_0_price_type}}" stepKey="selectOptionPriceTypeForTheFourthDataSetFirstRow"/> -+ <seeInField selector="{{AdminProductCustomizableOptionsSection.fillOptionValueSku(virtualProductCustomizableOption4.title,'0')}}" userInput="{{virtualProductCustomizableOption4.option_0_sku}}" stepKey="seeOptionSkuForFourthDataSetFirstRow"/> -+ <seeInField selector="{{AdminProductCustomizableOptionsSection.fillOptionValueTitle(virtualProductCustomizableOption4.title,'1')}}" userInput="{{virtualProductCustomizableOption4.option_1_title}}" stepKey="seeOptionTitleForFourthDataSetSecondRow"/> -+ <seeInField selector="{{AdminProductCustomizableOptionsSection.fillOptionValuePrice(virtualProductCustomizableOption4.title,'1')}}" userInput="{{virtualProductCustomizableOption4.option_1_price}}" stepKey="seeOptionPriceForFourthDataSetSecondRow"/> -+ <selectOption selector="{{AdminProductCustomizableOptionsSection.clickSelectPriceType(virtualProductCustomizableOption4.title,'1')}}" userInput="{{virtualProductCustomizableOption4.option_1_price_type}}" stepKey="selectOptionPriceTypeForTheFourthDataSetSecondRow"/> -+ <seeInField selector="{{AdminProductCustomizableOptionsSection.fillOptionValueSku(virtualProductCustomizableOption4.title,'1')}}" userInput="{{virtualProductCustomizableOption4.option_1_sku}}" stepKey="seeOptionSkuForFourthDataSetSecondRow"/> -+ -+ <!--Verify customer don't see updated virtual product link on category page --> -+ <amOnPage url="{{StorefrontCategoryPage.url($$categoryEntity.name$$)}}" stepKey="openCategoryPage"/> -+ <waitForPageLoad stepKey="waitForCategoryPageLoad"/> -+ <dontSee selector="{{StorefrontCategoryMainSection.productLink}}" userInput="{{updateVirtualProductRegularPriceInStock.name}}" stepKey="dontSeeVirtualProductNameOnCategoryPage"/> -+ -+ <!-- Verify customer see updated virtual product (from the above step) on the storefront page --> -+ <amOnPage url="{{StorefrontProductPage.url(updateVirtualProductRegularPriceInStock.urlKey)}}" stepKey="goToProductPage"/> -+ <waitForPageLoad stepKey="waitForStorefrontProductPageLoad"/> -+ <see selector="{{StorefrontProductInfoMainSection.productName}}" userInput="{{updateVirtualProductRegularPriceInStock.name}}" stepKey="seeVirtualProductNameOnStoreFrontPage"/> -+ <see selector="{{StorefrontProductInfoMainSection.productPrice}}" userInput="{{updateVirtualProductRegularPriceInStock.price}}" stepKey="seeVirtualProductPriceOnStoreFrontPage"/> -+ <see selector="{{StorefrontProductInfoMainSection.productSku}}" userInput="{{updateVirtualProductRegularPriceInStock.sku}}" stepKey="seeVirtualProductSku"/> -+ <grabTextFrom selector="{{StorefrontProductInfoMainSection.productStockStatus}}" stepKey="productStockAvailableStatus"/> -+ <assertEquals stepKey="assertStockAvailableOnProductPage"> -+ <expectedResult type="string">{{updateVirtualProductRegularPriceInStock.storefrontStatus}}</expectedResult> -+ <actualResult type="variable">productStockAvailableStatus</actualResult> -+ </assertEquals> -+ <grabTextFrom selector="{{StorefrontProductInfoMainSection.productPrice}}" stepKey="productPriceAmount"/> -+ <assertEquals stepKey="assertOldPriceTextOnProductPage"> -+ <expectedResult type="string">${{updateVirtualProductRegularPriceInStock.price}}</expectedResult> -+ <actualResult type="variable">productPriceAmount</actualResult> -+ </assertEquals> -+ <!--Verify we customer see customizable options are Required --> -+ <seeElement selector="{{StorefrontProductInfoMainSection.requiredCustomInput(virtualProductCustomizableOption1.title)}}" stepKey="verifyFirstCustomOptionIsRequired" /> -+ <seeElement selector="{{StorefrontProductInfoMainSection.requiredCustomInput(virtualProductCustomizableOption2.title)}}" stepKey="verifySecondCustomOptionIsRequired" /> -+ <seeElement selector="{{StorefrontProductInfoMainSection.requiredCustomSelect(virtualProductCustomizableOption3.title)}}" stepKey="verifyThirdCustomOptionIsRequired" /> -+ <seeElement selector="{{StorefrontProductInfoMainSection.requiredCustomSelect(virtualProductCustomizableOption4.title)}}" stepKey="verifyFourthCustomOptionIsRequired" /> -+ <!--Verify customer see customizable option titles and prices --> -+ <grabMultiple selector="{{StorefrontProductInfoMainSection.allCustomOptionLabels}}" stepKey="allCustomOptionLabels" /> -+ <assertEquals stepKey="verifyLabels"> -+ <actualResult type="variable">allCustomOptionLabels</actualResult> -+ <expectedResult type="array">[{{virtualProductCustomizableOption1.title}} + ${{virtualProductCustomizableOption1.option_0_price}}, {{virtualProductCustomizableOption2.title}} + ${{virtualProductCustomizableOption2.option_0_price}}, {{virtualProductCustomizableOption3.title}}, {{virtualProductCustomizableOption4.title}}]</expectedResult> -+ </assertEquals> -+ <grabAttributeFrom userInput="for" selector="{{StorefrontProductInfoMainSection.customOptionLabel(virtualProductCustomizableOption4.title)}}" stepKey="fourthOptionId" /> -+ <grabMultiple selector="{{StorefrontProductInfoMainSection.customSelectOptions({$fourthOptionId})}}" stepKey="grabFourthOptions" /> -+ <assertEquals stepKey="assertFourthSelectOptions"> -+ <actualResult type="variable">grabFourthOptions</actualResult> -+ <expectedResult type="array">['-- Please Select --', {{virtualProductCustomizableOption4.option_0_title}} +$12.01, {{virtualProductCustomizableOption4.option_1_title}} +$20.02]</expectedResult> -+ </assertEquals> -+ </test> -+</tests> -diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateVirtualProductWithRegularPriceOutOfStockVisibleInCategoryAndSearchTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateVirtualProductWithRegularPriceOutOfStockVisibleInCategoryAndSearchTest.xml -new file mode 100644 -index 00000000000..a2a4f658602 ---- /dev/null -+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateVirtualProductWithRegularPriceOutOfStockVisibleInCategoryAndSearchTest.xml -@@ -0,0 +1,100 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> -+ <test name="AdminUpdateVirtualProductWithRegularPriceOutOfStockVisibleInCategoryAndSearchTest"> -+ <annotations> -+ <stories value="Update Virtual Product"/> -+ <title value="Update Virtual Product with Regular Price (Out of Stock) Visible in Category and Search"/> -+ <description value="Test log in to Update Virtual Product and Update Virtual Product with Regular Price (Out of Stock) Visible in Category and Search"/> -+ <testCaseId value="MC-7433"/> -+ <severity value="CRITICAL"/> -+ <group value="catalog"/> -+ <group value="mtf_migrated"/> -+ </annotations> -+ -+ <before> -+ <actionGroup ref = "LoginAsAdmin" stepKey="loginAsAdmin"/> -+ <createData entity="SimpleSubCategory" stepKey="initialCategoryEntity"/> -+ <createData entity="defaultVirtualProduct" stepKey="initialVirtualProduct"> -+ <requiredEntity createDataKey="initialCategoryEntity"/> -+ </createData> -+ <createData entity="SimpleSubCategory" stepKey="categoryEntity"/> -+ </before> -+ <after> -+ <deleteData stepKey="deleteSimpleSubCategory" createDataKey="initialCategoryEntity"/> -+ <deleteData stepKey="deleteSimpleSubCategory2" createDataKey="categoryEntity"/> -+ <actionGroup ref="logout" stepKey="logout"/> -+ </after> -+ -+ <!-- Search default virtual product in the grid page --> -+ <amOnPage url="{{ProductCatalogPage.url}}" stepKey="OpenProductCatalogPage1"/> -+ <waitForPageLoad stepKey="waitForProductCatalogPage1"/> -+ <conditionalClick selector="{{AdminProductGridFilterSection.clearAll}}" dependentSelector="{{AdminProductGridFilterSection.clearAll}}" visible="true" stepKey="clickClearAllFilter" /> -+ <fillField selector="{{AdminProductGridFilterSection.keywordSearch}}" userInput="$$initialVirtualProduct.name$$" stepKey="fillVirtualProductNameInKeywordSearch"/> -+ <click selector="{{AdminProductGridFilterSection.keywordSearchButton}}" stepKey="clickKeywordSearchButton"/> -+ <waitForPageLoad stepKey="waitForProductSearch"/> -+ <click selector="{{AdminProductGridFilterSection.nthRow('1')}}" stepKey="clickFirstRowToVerifyCreatedVirtualProduct"/> -+ <waitForPageLoad stepKey="waitUntilProductIsOpened"/> -+ -+ <!-- Update virtual product with regular price(out of stock) --> -+ <fillField selector="{{AdminProductFormSection.productName}}" userInput="{{updateVirtualProductRegularPrice5OutOfStock.name}}" stepKey="fillProductName"/> -+ <fillField selector="{{AdminProductFormSection.productSku}}" userInput="{{updateVirtualProductRegularPrice5OutOfStock.sku}}" stepKey="fillProductSku"/> -+ <fillField selector="{{AdminProductFormSection.productPrice}}" userInput="{{updateVirtualProductRegularPrice5OutOfStock.price}}" stepKey="fillProductPrice"/> -+ <selectOption selector="{{AdminProductFormSection.productTaxClass}}" userInput="{{updateVirtualProductRegularPrice5OutOfStock.productTaxClass}}" stepKey="selectProductStockClass"/> -+ <selectOption selector="{{AdminProductFormSection.stockStatus}}" userInput="{{updateVirtualProductRegularPrice5OutOfStock.status}}" stepKey="selectStockStatusInStock"/> -+ <selectOption selector="{{AdminProductFormSection.visibility}}" userInput="{{updateVirtualProductRegularPrice5OutOfStock.visibility}}" stepKey="selectVisibility"/> -+ <click selector="{{AdminProductSEOSection.sectionHeader}}" stepKey="clickAdminProductSEOSection"/> -+ <fillField selector="{{AdminProductSEOSection.urlKeyInput}}" userInput="{{updateVirtualProductRegularPrice5OutOfStock.urlKey}}" stepKey="fillUrlKey"/> -+ <scrollToTopOfPage stepKey="scrollToTopOfAdminProductFormSection"/> -+ <click selector="{{AdminProductFormSection.save}}" stepKey="clickSaveButton"/> -+ <waitForPageLoad stepKey="waitForVirtualProductSaved"/> -+ <!-- Verify we see success message --> -+ <see selector="{{AdminProductFormSection.successMessage}}" userInput="You saved the product." stepKey="seeAssertVirtualProductSaveSuccessMessage"/> -+ -+ <!-- Search updated virtual product(from above step) in the grid page --> -+ <amOnPage url="{{ProductCatalogPage.url}}" stepKey="OpenProductCatalogPageToSearchUpdatedVirtualProduct"/> -+ <waitForPageLoad stepKey="waitForProductCatalogPageToLoad"/> -+ <conditionalClick selector="{{AdminProductGridFilterSection.clearAll}}" dependentSelector="{{AdminProductGridFilterSection.clearAll}}" visible="true" stepKey="clickClearAll"/> -+ <click selector="{{AdminProductGridFilterSection.filters}}" stepKey="clickFiltersButton"/> -+ <fillField selector="{{AdminProductGridFilterSection.nameFilter}}" userInput="{{updateVirtualProductRegularPrice5OutOfStock.name}}" stepKey="fillVirtualProductNameInNameFilter"/> -+ <fillField selector="{{AdminProductGridFilterSection.skuFilter}}" userInput="{{updateVirtualProductRegularPrice5OutOfStock.sku}}" stepKey="fillVirtualProductSku"/> -+ <click selector="{{AdminProductGridFilterSection.applyFilters}}" stepKey="clickApplyFiltersButton"/> -+ <click selector="{{AdminProductGridFilterSection.nthRow('1')}}" stepKey="clickFirstRowToVerifyUpdatedVirtualProductVisibleInGrid"/> -+ <waitForPageLoad stepKey="waitUntilVirtualProductPageIsOpened"/> -+ -+ <!-- Verify customer see updated virtual product with regular price(out of stock) in the product form page --> -+ <seeInField selector="{{AdminProductFormSection.productName}}" userInput="{{updateVirtualProductRegularPrice5OutOfStock.name}}" stepKey="seeProductName"/> -+ <seeInField selector="{{AdminProductFormSection.productSku}}" userInput="{{updateVirtualProductRegularPrice5OutOfStock.sku}}" stepKey="seeProductSku"/> -+ <seeInField selector="{{AdminProductFormSection.productPrice}}" userInput="{{updateVirtualProductRegularPrice5OutOfStock.price}}" stepKey="seeProductPrice"/> -+ <seeInField selector="{{AdminProductFormSection.productTaxClass}}" userInput="{{updateVirtualProductRegularPrice5OutOfStock.productTaxClass}}" stepKey="seeProductTaxClass"/> -+ <seeInField selector="{{AdminProductFormSection.productStockStatus}}" userInput="{{updateVirtualProductRegularPrice5OutOfStock.status}}" stepKey="seeProductStockStatus"/> -+ <seeInField selector="{{AdminProductFormSection.visibility}}" userInput="{{updateVirtualProductRegularPrice5OutOfStock.visibility}}" stepKey="seeVisibility"/> -+ <scrollTo selector="{{AdminProductSEOSection.sectionHeader}}" x="0" y="-80" stepKey="scrollToAdminProductSEOSection1"/> -+ <click selector="{{AdminProductSEOSection.sectionHeader}}" stepKey="clickAdminProductSEOSection1"/> -+ <seeInField selector="{{AdminProductSEOSection.urlKeyInput}}" userInput="{{updateVirtualProductRegularPrice5OutOfStock.urlKey}}" stepKey="seeUrlKey"/> -+ -+ <!--Verify customer see updated virtual product with regular price(out of stock) on product storefront page --> -+ <amOnPage url="{{StorefrontProductPage.url(updateVirtualProductRegularPrice5OutOfStock.urlKey)}}" stepKey="goToProductPage"/> -+ <waitForPageLoad stepKey="waitForStorefrontProductPageLoad"/> -+ <see selector="{{StorefrontProductInfoMainSection.productName}}" userInput="{{updateVirtualProductRegularPrice5OutOfStock.name}}" stepKey="seeVirtualProductNameOnStoreFrontPage"/> -+ <see selector="{{StorefrontProductInfoMainSection.productPrice}}" userInput="{{updateVirtualProductRegularPrice5OutOfStock.price}}" stepKey="seeVirtualProductPriceOnStoreFrontPage"/> -+ <see selector="{{StorefrontProductInfoMainSection.productSku}}" userInput="{{updateVirtualProductRegularPrice5OutOfStock.sku}}" stepKey="seeVirtualProductSku"/> -+ <grabTextFrom selector="{{StorefrontProductInfoMainSection.productStockStatus}}" stepKey="productStockAvailableStatus"/> -+ <assertEquals stepKey="assertStockAvailableOnProductPage"> -+ <expectedResult type="string">{{updateVirtualProductRegularPrice5OutOfStock.storefrontStatus}}</expectedResult> -+ <actualResult type="variable">productStockAvailableStatus</actualResult> -+ </assertEquals> -+ <grabTextFrom selector="{{StorefrontProductInfoMainSection.productPrice}}" stepKey="productPriceAmount"/> -+ <assertEquals stepKey="assertOldPriceTextOnProductPage"> -+ <expectedResult type="string">${{updateVirtualProductRegularPrice5OutOfStock.price}}</expectedResult> -+ <actualResult type="variable">productPriceAmount</actualResult> -+ </assertEquals> -+ </test> -+</tests> -diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateVirtualProductWithRegularPriceOutOfStockVisibleInCategoryOnlyTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateVirtualProductWithRegularPriceOutOfStockVisibleInCategoryOnlyTest.xml -new file mode 100644 -index 00000000000..ffbad0752b7 ---- /dev/null -+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateVirtualProductWithRegularPriceOutOfStockVisibleInCategoryOnlyTest.xml -@@ -0,0 +1,129 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> -+ <test name="AdminUpdateVirtualProductWithRegularPriceOutOfStockVisibleInCategoryOnlyTest"> -+ <annotations> -+ <stories value="Update Virtual Product"/> -+ <title value="Update Virtual Product with Regular Price (Out of Stock) Visible in Category Only"/> -+ <description value="Test log in to Update Virtual Product and Update Virtual Product with Regular Price (Out of Stock) Visible in Category Only"/> -+ <testCaseId value="MC-6503"/> -+ <severity value="CRITICAL"/> -+ <group value="catalog"/> -+ <group value="mtf_migrated"/> -+ </annotations> -+ -+ <before> -+ <actionGroup ref = "LoginAsAdmin" stepKey="loginAsAdmin"/> -+ <createData entity="SimpleSubCategory" stepKey="initialCategoryEntity"/> -+ <createData entity="defaultVirtualProduct" stepKey="initialVirtualProduct"> -+ <requiredEntity createDataKey="initialCategoryEntity"/> -+ </createData> -+ <createData entity="SimpleSubCategory" stepKey="categoryEntity"/> -+ </before> -+ <after> -+ <deleteData stepKey="deleteSimpleSubCategory" createDataKey="initialCategoryEntity"/> -+ <deleteData stepKey="deleteSimpleSubCategory2" createDataKey="categoryEntity"/> -+ <actionGroup ref="logout" stepKey="logout"/> -+ </after> -+ -+ <!-- Search default virtual product in the grid page --> -+ <amOnPage url="{{ProductCatalogPage.url}}" stepKey="OpenProductCatalogPage1"/> -+ <waitForPageLoad stepKey="waitForProductCatalogPage1"/> -+ <conditionalClick selector="{{AdminProductGridFilterSection.clearAll}}" dependentSelector="{{AdminProductGridFilterSection.clearAll}}" visible="true" stepKey="clickclearAllFilter" /> -+ <fillField selector="{{AdminProductGridFilterSection.keywordSearch}}" userInput="$$initialVirtualProduct.name$$" stepKey="fillVirtualProductNameInKeywordSearch"/> -+ <click selector="{{AdminProductGridFilterSection.keywordSearchButton}}" stepKey="clickKeywordSearchButton"/> -+ <waitForPageLoad stepKey="waitForProductSearch"/> -+ <click selector="{{AdminProductGridFilterSection.nthRow('1')}}" stepKey="clickFirstRowToVerifyCreatedVirtualProduct"/> -+ <waitForPageLoad stepKey="waitUntilProductIsOpened"/> -+ -+ <!-- Update virtual product with regular price(out of stock) --> -+ <fillField selector="{{AdminProductFormSection.productName}}" userInput="{{updateVirtualProductRegularPrice5OutOfStock.name}}" stepKey="fillProductName"/> -+ <fillField selector="{{AdminProductFormSection.productSku}}" userInput="{{updateVirtualProductRegularPrice5OutOfStock.sku}}" stepKey="fillProductSku"/> -+ <fillField selector="{{AdminProductFormSection.productPrice}}" userInput="{{updateVirtualProductRegularPrice5OutOfStock.price}}" stepKey="fillProductPrice"/> -+ <selectOption selector="{{AdminProductFormSection.productTaxClass}}" userInput="{{updateVirtualProductRegularPrice5OutOfStock.productTaxClass}}" stepKey="selectProductStockClass"/> -+ <selectOption selector="{{AdminProductFormSection.stockStatus}}" userInput="{{updateVirtualProductRegularPrice5OutOfStock.status}}" stepKey="selectStockStatusInStock"/> -+ <click selector="{{AdminProductFormSection.categoriesDropdown}}" stepKey="clickCategoriesDropDown"/> -+ <fillField selector="{{AdminProductFormSection.searchCategory}}" userInput="$$initialCategoryEntity.name$$" stepKey="fillSearchForInitialCategory" /> -+ <waitForPageLoad stepKey="waitForCategory1"/> -+ <click selector="{{AdminProductFormSection.selectCategory($$initialCategoryEntity.name$$)}}" stepKey="unselectInitialCategory"/> -+ <fillField selector="{{AdminProductFormSection.searchCategory}}" userInput="$$categoryEntity.name$$" stepKey="fillSearchCategory" /> -+ <waitForPageLoad stepKey="waitForCategory2"/> -+ <click selector="{{AdminProductFormSection.selectCategory($$categoryEntity.name$$)}}" stepKey="clickOnCategory"/> -+ <click selector="{{AdminProductFormSection.done}}" stepKey="clickOnDoneAdvancedCategorySelect"/> -+ <selectOption selector="{{AdminProductFormSection.visibility}}" userInput="{{updateVirtualProductRegularPrice5OutOfStock.visibility}}" stepKey="selectVisibility"/> -+ <click selector="{{AdminProductSEOSection.sectionHeader}}" stepKey="clickAdminProductSEOSection"/> -+ <fillField selector="{{AdminProductSEOSection.urlKeyInput}}" userInput="{{updateVirtualProductRegularPrice5OutOfStock.urlKey}}" stepKey="fillUrlKey"/> -+ <scrollToTopOfPage stepKey="scrollToTopOfAdminProductFormSection"/> -+ <click selector="{{AdminProductFormSection.save}}" stepKey="clickSaveButton"/> -+ <waitForPageLoad stepKey="waitForVirtualProductSaved"/> -+ <!-- Verify we see success message --> -+ <see selector="{{AdminProductFormSection.successMessage}}" userInput="You saved the product." stepKey="seeAssertVirtualProductSaveSuccessMessage"/> -+ -+ <!-- Search updated virtual product(from above step) in the grid page --> -+ <amOnPage url="{{ProductCatalogPage.url}}" stepKey="OpenProductCatalogPageToSearchUpdatedVirtualProduct"/> -+ <waitForPageLoad stepKey="waitForProductCatalogPageToLoad"/> -+ <conditionalClick selector="{{AdminProductGridFilterSection.clearAll}}" dependentSelector="{{AdminProductGridFilterSection.clearAll}}" visible="true" stepKey="clickClearAll"/> -+ <click selector="{{AdminProductGridFilterSection.filters}}" stepKey="clickFiltersButton"/> -+ <fillField selector="{{AdminProductGridFilterSection.nameFilter}}" userInput="{{updateVirtualProductRegularPrice5OutOfStock.name}}" stepKey="fillVirtualProductNameInNameFilter"/> -+ <fillField selector="{{AdminProductGridFilterSection.skuFilter}}" userInput="{{updateVirtualProductRegularPrice5OutOfStock.sku}}" stepKey="fillVirtualProductSku"/> -+ <click selector="{{AdminProductGridFilterSection.applyFilters}}" stepKey="clickApplyFiltersButton"/> -+ <click selector="{{AdminProductGridFilterSection.nthRow('1')}}" stepKey="clickFirstRowToVerifyUpdatedVirtualProductVisibleInGrid"/> -+ <waitForPageLoad stepKey="waitUntilVirtualProductPageIsOpened"/> -+ -+ <!-- Verify we see updated virtual product with regular price(from the above step) in the product form page --> -+ <seeInField selector="{{AdminProductFormSection.productName}}" userInput="{{updateVirtualProductRegularPrice5OutOfStock.name}}" stepKey="seeProductName"/> -+ <seeInField selector="{{AdminProductFormSection.productSku}}" userInput="{{updateVirtualProductRegularPrice5OutOfStock.sku}}" stepKey="seeProductSku"/> -+ <seeInField selector="{{AdminProductFormSection.productPrice}}" userInput="{{updateVirtualProductRegularPrice5OutOfStock.price}}" stepKey="seeProductPrice"/> -+ <seeInField selector="{{AdminProductFormSection.productTaxClass}}" userInput="{{updateVirtualProductRegularPrice5OutOfStock.productTaxClass}}" stepKey="seeProductTaxClass"/> -+ <seeInField selector="{{AdminProductFormSection.productStockStatus}}" userInput="{{updateVirtualProductRegularPrice5OutOfStock.status}}" stepKey="seeProductStockStatus"/> -+ <click selector="{{AdminProductFormSection.categoriesDropdown}}" stepKey="clickCategoriesDropDownToVerify"/> -+ <grabMultiple selector="{{AdminProductFormSection.selectMultipleCategories}}" stepKey="selectedCategories" /> -+ <assertEquals stepKey="assertSelectedCategories"> -+ <actualResult type="variable">selectedCategories</actualResult> -+ <expectedResult type="array">[$$categoryEntity.name$$]</expectedResult> -+ </assertEquals> -+ <click selector="{{AdminProductFormSection.done}}" stepKey="clickOnDoneOnCategorySelect"/> -+ <seeInField selector="{{AdminProductFormSection.visibility}}" userInput="{{updateVirtualProductRegularPrice5OutOfStock.visibility}}" stepKey="seeVisibility"/> -+ <scrollTo selector="{{AdminProductSEOSection.sectionHeader}}" x="0" y="-80" stepKey="scrollToAdminProductSEOSection1"/> -+ <click selector="{{AdminProductSEOSection.sectionHeader}}" stepKey="clickAdminProductSEOSection1"/> -+ <seeInField selector="{{AdminProductSEOSection.urlKeyInput}}" userInput="{{updateVirtualProductRegularPrice5OutOfStock.urlKey}}" stepKey="seeUrlKey"/> -+ -+ <!--Verify customer don't see updated virtual product link(from above step) on category page --> -+ <amOnPage url="{{StorefrontCategoryPage.url($$categoryEntity.name$$)}}" stepKey="openCategoryPage"/> -+ <waitForPageLoad stepKey="waitForCategoryPageLoad"/> -+ <dontSee selector="{{StorefrontCategoryMainSection.productLink}}" userInput="{{updateVirtualProductRegularPrice5OutOfStock.name}}" stepKey="dontseeVirtualProductNameOnCategoryPage"/> -+ -+ <!--Verify customer see updated virtual product (from above step) on product storefront page --> -+ <amOnPage url="{{StorefrontProductPage.url(updateVirtualProductRegularPrice5OutOfStock.urlKey)}}" stepKey="goToProductPage"/> -+ <waitForPageLoad stepKey="waitForStorefrontProductPageLoad"/> -+ <see selector="{{StorefrontProductInfoMainSection.productName}}" userInput="{{updateVirtualProductRegularPrice5OutOfStock.name}}" stepKey="seeVirtualProductNameOnStoreFrontPage"/> -+ <see selector="{{StorefrontProductInfoMainSection.productPrice}}" userInput="{{updateVirtualProductRegularPrice5OutOfStock.price}}" stepKey="seeVirtualProductPriceOnStoreFrontPage"/> -+ <see selector="{{StorefrontProductInfoMainSection.productSku}}" userInput="{{updateVirtualProductRegularPrice5OutOfStock.sku}}" stepKey="seeVirtualProductSku"/> -+ <grabTextFrom selector="{{StorefrontProductInfoMainSection.productStockStatus}}" stepKey="productStockAvailableStatus"/> -+ <assertEquals stepKey="assertStockAvailableOnProductPage"> -+ <expectedResult type="string">{{updateVirtualProductRegularPrice5OutOfStock.storefrontStatus}}</expectedResult> -+ <actualResult type="variable">productStockAvailableStatus</actualResult> -+ </assertEquals> -+ <grabTextFrom selector="{{StorefrontProductInfoMainSection.productPrice}}" stepKey="productPriceAmount"/> -+ <assertEquals stepKey="assertOldPriceTextOnProductPage"> -+ <expectedResult type="string">${{updateVirtualProductRegularPrice5OutOfStock.price}}</expectedResult> -+ <actualResult type="variable">productPriceAmount</actualResult> -+ </assertEquals> -+ -+ <!--Verify customer don't see updated virtual product link(from above step) on magento storefront page and is searchable by sku --> -+ <amOnPage url="{{StorefrontProductPage.url(updateVirtualProductRegularPrice5OutOfStock.urlKey)}}" stepKey="goToMagentoStorefrontPage"/> -+ <waitForPageLoad stepKey="waitForStoreFrontProductPageLoad"/> -+ <fillField selector="{{StorefrontQuickSearchResultsSection.searchTextBox}}" userInput="{{updateVirtualProductRegularPrice5OutOfStock.sku}}" stepKey="fillVirtualProductName"/> -+ <waitForPageLoad stepKey="waitForSearchTextBox"/> -+ <click selector="{{StorefrontQuickSearchResultsSection.searchTextBoxButton}}" stepKey="clickSearchTextBoxButton"/> -+ <waitForPageLoad stepKey="waitForSearch"/> -+ <dontSee selector="{{StorefrontQuickSearchResultsSection.productLink}}" userInput="{{updateVirtualProductRegularPrice5OutOfStock.name}}" stepKey="dontSeeVirtualProductName"/> -+ </test> -+</tests> -diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateVirtualProductWithRegularPriceOutOfStockVisibleInSearchOnlyTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateVirtualProductWithRegularPriceOutOfStockVisibleInSearchOnlyTest.xml -new file mode 100644 -index 00000000000..aa3184994da ---- /dev/null -+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateVirtualProductWithRegularPriceOutOfStockVisibleInSearchOnlyTest.xml -@@ -0,0 +1,112 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> -+ <test name="AdminUpdateVirtualProductWithRegularPriceOutOfStockVisibleInSearchOnlyTest"> -+ <annotations> -+ <stories value="Update Virtual Product"/> -+ <title value="Update Virtual Product with Regular Price (Out of Stock) Visible in Search Only"/> -+ <description value="Test log in to Update Virtual Product and Update Virtual Product with Regular Price (Out of Stock) Visible in Search Only"/> -+ <testCaseId value="MC-6498"/> -+ <severity value="CRITICAL"/> -+ <group value="catalog"/> -+ <group value="mtf_migrated"/> -+ </annotations> -+ -+ <before> -+ <actionGroup ref = "LoginAsAdmin" stepKey="loginAsAdmin"/> -+ <createData entity="SimpleSubCategory" stepKey="initialCategoryEntity"/> -+ <createData entity="defaultVirtualProduct" stepKey="initialVirtualProduct"> -+ <requiredEntity createDataKey="initialCategoryEntity"/> -+ </createData> -+ </before> -+ <after> -+ <deleteData stepKey="deleteSimpleSubCategory" createDataKey="initialCategoryEntity"/> -+ <actionGroup ref="logout" stepKey="logout"/> -+ </after> -+ -+ <!-- Search default virtual product in the grid page --> -+ <amOnPage url="{{ProductCatalogPage.url}}" stepKey="OpenProductCatalogPage1"/> -+ <waitForPageLoad stepKey="waitForProductCatalogPage1"/> -+ <conditionalClick selector="{{AdminProductGridFilterSection.clearAll}}" dependentSelector="{{AdminProductGridFilterSection.clearAll}}" visible="true" stepKey="clickClearAllFilter" /> -+ <fillField selector="{{AdminProductGridFilterSection.keywordSearch}}" userInput="$$initialVirtualProduct.name$$" stepKey="fillVirtualProductNameInKeywordSearch"/> -+ <click selector="{{AdminProductGridFilterSection.keywordSearchButton}}" stepKey="clickKeywordSearchButton"/> -+ <waitForPageLoad stepKey="waitForProductSearch"/> -+ <click selector="{{AdminProductGridFilterSection.nthRow('1')}}" stepKey="clickFirstRowToVerifyCreatedVirtualProduct"/> -+ <waitForPageLoad stepKey="waitUntilProductIsOpened"/> -+ -+ <!-- Update virtual product with regular price(out of stock) --> -+ <fillField selector="{{AdminProductFormSection.productName}}" userInput="{{updateVirtualProductRegularPrice99OutOfStock.name}}" stepKey="fillProductName"/> -+ <fillField selector="{{AdminProductFormSection.productSku}}" userInput="{{updateVirtualProductRegularPrice99OutOfStock.sku}}" stepKey="fillProductSku"/> -+ <fillField selector="{{AdminProductFormSection.productPrice}}" userInput="{{updateVirtualProductRegularPrice99OutOfStock.price}}" stepKey="fillProductPrice"/> -+ <selectOption selector="{{AdminProductFormSection.productTaxClass}}" userInput="{{updateVirtualProductRegularPrice99OutOfStock.productTaxClass}}" stepKey="selectProductStockClass"/> -+ <selectOption selector="{{AdminProductFormSection.stockStatus}}" userInput="{{updateVirtualProductRegularPrice99OutOfStock.status}}" stepKey="selectStockStatusInStock"/> -+ <selectOption selector="{{AdminProductFormSection.visibility}}" userInput="{{updateVirtualProductRegularPrice99OutOfStock.visibility}}" stepKey="selectVisibility"/> -+ <click selector="{{AdminProductSEOSection.sectionHeader}}" stepKey="clickAdminProductSEOSection"/> -+ <fillField selector="{{AdminProductSEOSection.urlKeyInput}}" userInput="{{updateVirtualProductRegularPrice99OutOfStock.urlKey}}" stepKey="fillUrlKey"/> -+ <scrollToTopOfPage stepKey="scrollToTopOfAdminProductFormSection"/> -+ <click selector="{{AdminProductFormSection.save}}" stepKey="clickSaveButton"/> -+ <waitForPageLoad stepKey="waitForVirtualProductSaved"/> -+ <!-- Verify we see success message --> -+ <see selector="{{AdminProductFormSection.successMessage}}" userInput="You saved the product." stepKey="seeAssertVirtualProductSaveSuccessMessage"/> -+ -+ <!-- Search updated virtual product(from above step) in the grid --> -+ <amOnPage url="{{ProductCatalogPage.url}}" stepKey="OpenProductCatalogPageToSearchUpdatedVirtualProduct"/> -+ <waitForPageLoad stepKey="waitForProductCatalogPageToLoad"/> -+ <conditionalClick selector="{{AdminProductGridFilterSection.clearAll}}" dependentSelector="{{AdminProductGridFilterSection.clearAll}}" visible="true" stepKey="clickClearAll"/> -+ <click selector="{{AdminProductGridFilterSection.filters}}" stepKey="clickFiltersButton"/> -+ <fillField selector="{{AdminProductGridFilterSection.nameFilter}}" userInput="{{updateVirtualProductRegularPrice99OutOfStock.name}}" stepKey="fillVirtualProductNameInNameFilter"/> -+ <fillField selector="{{AdminProductGridFilterSection.skuFilter}}" userInput="{{updateVirtualProductRegularPrice99OutOfStock.sku}}" stepKey="fillVirtualProductSku"/> -+ <click selector="{{AdminProductGridFilterSection.applyFilters}}" stepKey="clickApplyFiltersButton"/> -+ <click selector="{{AdminProductGridFilterSection.nthRow('1')}}" stepKey="clickFirstRowToVerifyUpdatedVirtualProductVisibleInGrid"/> -+ <waitForPageLoad stepKey="waitUntilVirtualProductPageIsOpened"/> -+ -+ <!-- Verify customer see updated virtual product with regular price in the product form page --> -+ <seeInField selector="{{AdminProductFormSection.productName}}" userInput="{{updateVirtualProductRegularPrice99OutOfStock.name}}" stepKey="seeProductName"/> -+ <seeInField selector="{{AdminProductFormSection.productSku}}" userInput="{{updateVirtualProductRegularPrice99OutOfStock.sku}}" stepKey="seeProductSku"/> -+ <seeInField selector="{{AdminProductFormSection.productPrice}}" userInput="{{updateVirtualProductRegularPrice99OutOfStock.price}}" stepKey="seeProductPrice"/> -+ <seeInField selector="{{AdminProductFormSection.productTaxClass}}" userInput="{{updateVirtualProductRegularPrice99OutOfStock.productTaxClass}}" stepKey="seeProductTaxClass"/> -+ <seeInField selector="{{AdminProductFormSection.productStockStatus}}" userInput="{{updateVirtualProductRegularPrice99OutOfStock.status}}" stepKey="seeProductStockStatus"/> -+ <seeInField selector="{{AdminProductFormSection.visibility}}" userInput="{{updateVirtualProductRegularPrice99OutOfStock.visibility}}" stepKey="seeVisibility"/> -+ <scrollTo selector="{{AdminProductSEOSection.sectionHeader}}" x="0" y="-80" stepKey="scrollToAdminProductSEOSection1"/> -+ <click selector="{{AdminProductSEOSection.sectionHeader}}" stepKey="clickAdminProductSEOSection1"/> -+ <seeInField selector="{{AdminProductSEOSection.urlKeyInput}}" userInput="{{updateVirtualProductRegularPrice99OutOfStock.urlKey}}" stepKey="seeUrlKey"/> -+ -+ <!--Verify customer see updated virtual product on storefront page by url key --> -+ <amOnPage url="{{StorefrontProductPage.url(updateVirtualProductRegularPrice99OutOfStock.urlKey)}}" stepKey="goToProductPage"/> -+ <waitForPageLoad stepKey="waitForStorefrontProductPageLoad"/> -+ <see selector="{{StorefrontProductInfoMainSection.productName}}" userInput="{{updateVirtualProductRegularPrice99OutOfStock.name}}" stepKey="seeVirtualProductNameOnStoreFrontPage"/> -+ <see selector="{{StorefrontProductInfoMainSection.productPrice}}" userInput="{{updateVirtualProductRegularPrice99OutOfStock.price}}" stepKey="seeVirtualProductPriceOnStoreFrontPage"/> -+ <see selector="{{StorefrontProductInfoMainSection.productSku}}" userInput="{{updateVirtualProductRegularPrice99OutOfStock.sku}}" stepKey="seeVirtualProductSku"/> -+ <grabTextFrom selector="{{StorefrontProductInfoMainSection.productStockStatus}}" stepKey="productStockAvailableStatus"/> -+ <assertEquals stepKey="assertStockAvailableOnProductPage"> -+ <expectedResult type="string">{{updateVirtualProductRegularPrice99OutOfStock.storefrontStatus}}</expectedResult> -+ <actualResult type="variable">productStockAvailableStatus</actualResult> -+ </assertEquals> -+ <grabTextFrom selector="{{StorefrontProductInfoMainSection.productPrice}}" stepKey="productPriceAmount"/> -+ <assertEquals stepKey="assertOldPriceTextOnProductPage"> -+ <expectedResult type="string">${{updateVirtualProductRegularPrice99OutOfStock.price}}</expectedResult> -+ <actualResult type="variable">productPriceAmount</actualResult> -+ </assertEquals> -+ -+ <!--Verify customer don't see updated virtual product link on magento storefront page --> -+ <amOnPage url="{{StorefrontProductPage.url(updateVirtualProductRegularPrice99OutOfStock.urlKey)}}" stepKey="goToMagentoStorefrontPage"/> -+ <waitForPageLoad stepKey="waitForStoreFrontProductPageLoad"/> -+ <fillField selector="{{StorefrontQuickSearchResultsSection.searchTextBox}}" userInput="{{updateVirtualProductRegularPrice99OutOfStock.sku}}" stepKey="fillVirtualProductName"/> -+ <waitForPageLoad stepKey="waitForSearchTextBox"/> -+ <click selector="{{StorefrontQuickSearchResultsSection.searchTextBoxButton}}" stepKey="clickSearchTextBoxButton"/> -+ <waitForPageLoad stepKey="waitForSearch"/> -+ <dontSee selector="{{StorefrontQuickSearchResultsSection.productLink}}" userInput="{{updateVirtualProductRegularPrice99OutOfStock.name}}" stepKey="dontSeeVirtualProductLinkOnStorefrontPage"/> -+ -+ <!--Verify customer don't see updated virtual product link on category page --> -+ <amOnPage url="{{StorefrontCategoryPage.url($$initialCategoryEntity.name$$)}}" stepKey="openCategoryPage"/> -+ <waitForPageLoad stepKey="waitForCategoryPageLoad"/> -+ <dontSee selector="{{StorefrontCategoryMainSection.productLink}}" userInput="{{updateVirtualProductRegularPrice99OutOfStock.name}}" stepKey="dontSeeVirtualProductLinkOnCategoryPage"/> -+ </test> -+</tests> -diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateVirtualProductWithSpecialPriceInStockVisibleInCategoryAndSearchTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateVirtualProductWithSpecialPriceInStockVisibleInCategoryAndSearchTest.xml -new file mode 100644 -index 00000000000..3101c1e4603 ---- /dev/null -+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateVirtualProductWithSpecialPriceInStockVisibleInCategoryAndSearchTest.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="AdminUpdateVirtualProductWithSpecialPriceInStockVisibleInCategoryAndSearchTest"> -+ <annotations> -+ <stories value="Update Virtual Product"/> -+ <title value="Update Virtual Product with Special Price (In Stock) Visible in Category and Search"/> -+ <description value="Test log in to Update Virtual Product and Update Virtual Product with Special Price (In Stock) Visible in Category and Search"/> -+ <testCaseId value="MC-6496"/> -+ <severity value="CRITICAL"/> -+ <group value="catalog"/> -+ <group value="mtf_migrated"/> -+ </annotations> -+ -+ <before> -+ <actionGroup ref = "LoginAsAdmin" stepKey="loginAsAdmin"/> -+ <createData entity="SimpleSubCategory" stepKey="initialCategoryEntity"/> -+ <createData entity="defaultVirtualProduct" stepKey="initialVirtualProduct"> -+ <requiredEntity createDataKey="initialCategoryEntity"/> -+ </createData> -+ <createData entity="SimpleSubCategory" stepKey="categoryEntity"/> -+ </before> -+ <after> -+ <deleteData stepKey="deleteSimpleSubCategory" createDataKey="initialCategoryEntity"/> -+ <deleteData stepKey="deleteSimpleSubCategory2" createDataKey="categoryEntity"/> -+ <actionGroup ref="logout" stepKey="logout"/> -+ </after> -+ -+ <!-- Search default virtual product in the grid page --> -+ <amOnPage url="{{ProductCatalogPage.url}}" stepKey="OpenProductCatalogPage"/> -+ <waitForPageLoad stepKey="waitForProductCatalogPage"/> -+ <conditionalClick selector="{{AdminProductGridFilterSection.clearAll}}" dependentSelector="{{AdminProductGridFilterSection.clearAll}}" visible="true" stepKey="clickClearAllFilter" /> -+ <fillField selector="{{AdminProductGridFilterSection.keywordSearch}}" userInput="$$initialVirtualProduct.name$$" stepKey="fillVirtualProductNameInKeywordSearch"/> -+ <click selector="{{AdminProductGridFilterSection.keywordSearchButton}}" stepKey="clickKeywordSearchButton"/> -+ <waitForPageLoad stepKey="waitForProductSearch"/> -+ <click selector="{{AdminProductGridFilterSection.nthRow('1')}}" stepKey="clickFirstRowToVerifyCreatedVirtualProduct"/> -+ <waitForPageLoad stepKey="waitUntilProductIsOpened"/> -+ -+ <!-- Update virtual product with special price --> -+ <fillField selector="{{AdminProductFormSection.productName}}" userInput="{{updateVirtualProductSpecialPrice.name}}" stepKey="fillProductName"/> -+ <fillField selector="{{AdminProductFormSection.productSku}}" userInput="{{updateVirtualProductSpecialPrice.sku}}" stepKey="fillProductSku"/> -+ <fillField selector="{{AdminProductFormSection.productPrice}}" userInput="{{updateVirtualProductSpecialPrice.price}}" stepKey="fillProductPrice"/> -+ <!-- Press enter to validate advanced pricing link --> -+ <pressKey selector="{{AdminProductFormSection.productPrice}}" parameterArray="[\Facebook\WebDriver\WebDriverKeys::ENTER]" stepKey="pressEnterKey"/> -+ <click selector="{{AdminProductFormSection.advancedPricingLink}}" stepKey="clickAdvancedPricingLink"/> -+ <fillField selector="{{AdminProductFormAdvancedPricingSection.specialPrice}}" userInput="{{updateVirtualProductSpecialPrice.special_price}}" stepKey="fillSpecialPrice"/> -+ <click selector="{{AdminProductFormAdvancedPricingSection.doneButton}}" stepKey="clickDoneButton"/> -+ <selectOption selector="{{AdminProductFormSection.productTaxClass}}" userInput="{{updateVirtualProductSpecialPrice.productTaxClass}}" stepKey="selectProductStockClass"/> -+ <fillField selector="{{AdminProductFormSection.productQuantity}}" userInput="{{updateVirtualProductSpecialPrice.quantity}}" stepKey="fillProductQuantity"/> -+ <selectOption selector="{{AdminProductFormSection.stockStatus}}" userInput="{{updateVirtualProductSpecialPrice.status}}" stepKey="selectStockStatusInStock"/> -+ <click selector="{{AdminProductFormSection.categoriesDropdown}}" stepKey="clickCategoriesDropDown"/> -+ <fillField selector="{{AdminProductFormSection.searchCategory}}" userInput="$$initialCategoryEntity.name$$" stepKey="fillSearchForInitialCategory" /> -+ <waitForPageLoad stepKey="waitForCategory1"/> -+ <click selector="{{AdminProductFormSection.selectCategory($$initialCategoryEntity.name$$)}}" stepKey="unselectInitialCategory"/> -+ <fillField selector="{{AdminProductFormSection.searchCategory}}" userInput="$$categoryEntity.name$$" stepKey="fillSearchCategory" /> -+ <waitForPageLoad stepKey="waitForCategory2"/> -+ <click selector="{{AdminProductFormSection.selectCategory($$categoryEntity.name$$)}}" stepKey="clickOnCategory"/> -+ <click selector="{{AdminProductFormSection.done}}" stepKey="clickOnDoneAdvancedCategorySelect"/> -+ <selectOption selector="{{AdminProductFormSection.visibility}}" userInput="{{updateVirtualProductSpecialPrice.visibility}}" stepKey="selectVisibility"/> -+ <click selector="{{AdminProductSEOSection.sectionHeader}}" stepKey="clickAdminProductSEOSection"/> -+ <fillField selector="{{AdminProductSEOSection.urlKeyInput}}" userInput="{{updateVirtualProductSpecialPrice.urlKey}}" stepKey="fillUrlKey"/> -+ <scrollToTopOfPage stepKey="scrollToTopOfAdminProductFormSection"/> -+ <click selector="{{AdminProductFormSection.save}}" stepKey="clickSaveButton"/> -+ <waitForPageLoad stepKey="waitForVirtualProductSaved"/> -+ <!-- Verify we see success message --> -+ <see selector="{{AdminProductFormSection.successMessage}}" userInput="You saved the product." stepKey="seeAssertVirtualProductSaveSuccessMessage"/> -+ -+ <!-- Search updated virtual product(from above step) in the grid page --> -+ <amOnPage url="{{ProductCatalogPage.url}}" stepKey="OpenProductCatalogPageToSearchUpdatedVirtualProduct"/> -+ <waitForPageLoad stepKey="waitForProductCatalogPageToLoad"/> -+ <conditionalClick selector="{{AdminProductGridFilterSection.clearAll}}" dependentSelector="{{AdminProductGridFilterSection.clearAll}}" visible="true" stepKey="clickClearAll"/> -+ <click selector="{{AdminProductGridFilterSection.filters}}" stepKey="clickFiltersButton"/> -+ <fillField selector="{{AdminProductGridFilterSection.nameFilter}}" userInput="{{updateVirtualProductSpecialPrice.name}}" stepKey="fillVirtualProductNameInNameFilter"/> -+ <fillField selector="{{AdminProductGridFilterSection.skuFilter}}" userInput="{{updateVirtualProductSpecialPrice.sku}}" stepKey="fillVirtualProductSku"/> -+ <click selector="{{AdminProductGridFilterSection.applyFilters}}" stepKey="clickApplyFiltersButton"/> -+ <click selector="{{AdminProductGridFilterSection.nthRow('1')}}" stepKey="clickFirstRowToVerifyUpdatedVirtualProductVisibleInGrid"/> -+ <waitForPageLoad stepKey="waitUntilVirtualProductPageIsOpened"/> -+ <!-- Verify customer see updated virtual product with special price(from the above step) in the product form page --> -+ <seeInField selector="{{AdminProductFormSection.productName}}" userInput="{{updateVirtualProductSpecialPrice.name}}" stepKey="seeProductName"/> -+ <seeInField selector="{{AdminProductFormSection.productSku}}" userInput="{{updateVirtualProductSpecialPrice.sku}}" stepKey="seeProductSku"/> -+ <seeInField selector="{{AdminProductFormSection.productPrice}}" userInput="{{updateVirtualProductSpecialPrice.price}}" stepKey="seeProductPrice"/> -+ <click selector="{{AdminProductFormSection.advancedPricingLink}}" stepKey="clickAdvancedPricingLink1"/> -+ <seeInField selector="{{AdminProductFormAdvancedPricingSection.specialPrice}}" userInput="{{updateVirtualProductSpecialPrice.special_price}}" stepKey="seeSpecialPrice"/> -+ <click selector="{{AdminProductFormAdvancedPricingSection.advancedPricingCloseButton}}" stepKey="clickAdvancedPricingCloseButton"/> -+ <seeInField selector="{{AdminProductFormSection.productTaxClass}}" userInput="{{updateVirtualProductSpecialPrice.productTaxClass}}" stepKey="seeProductTaxClass"/> -+ <seeInField selector="{{AdminProductFormSection.productQuantity}}" userInput="{{updateVirtualProductSpecialPrice.quantity}}" stepKey="seeProductQuantity"/> -+ <see selector="{{AdminProductFormSection.productStockStatus}}" userInput="{{updateVirtualProductSpecialPrice.status}}" stepKey="seeProductStockStatus"/> -+ <click selector="{{AdminProductFormSection.categoriesDropdown}}" stepKey="clickCategoriesDropDownToVerify"/> -+ <grabMultiple selector="{{AdminProductFormSection.selectMultipleCategories}}" stepKey="selectedCategories" /> -+ <assertEquals stepKey="assertSelectedCategories"> -+ <actualResult type="variable">selectedCategories</actualResult> -+ <expectedResult type="array">[$$categoryEntity.name$$]</expectedResult> -+ </assertEquals> -+ <click selector="{{AdminProductFormSection.done}}" stepKey="clickOnDoneOnCategorySelect"/> -+ <see selector="{{AdminProductFormSection.visibility}}" userInput="{{updateVirtualProductSpecialPrice.visibility}}" stepKey="seeVisibility"/> -+ <scrollTo selector="{{AdminProductSEOSection.sectionHeader}}" x="0" y="-80" stepKey="scrollToAdminProductSEOSection1"/> -+ <click selector="{{AdminProductSEOSection.sectionHeader}}" stepKey="clickAdminProductSEOSection1"/> -+ <seeInField selector="{{AdminProductSEOSection.urlKeyInput}}" userInput="{{updateVirtualProductSpecialPrice.urlKey}}" stepKey="seeUrlKey"/> -+ -+ <!--Verify customer see updated virtual product link on category page --> -+ <amOnPage url="{{StorefrontCategoryPage.url($$categoryEntity.name$$)}}" stepKey="openCategoryPage"/> -+ <waitForPageLoad stepKey="waitForCategoryPageLoad"/> -+ <see selector="{{StorefrontCategoryMainSection.productLink}}" userInput="{{updateVirtualProductSpecialPrice.name}}" stepKey="seeVirtualProductNameOnCategoryPage"/> -+ -+ <!-- Verify customer see updated virtual product on the magento storefront page and is searchable by sku --> -+ <amOnPage url="{{StorefrontProductPage.url(updateVirtualProductSpecialPrice.urlKey)}}" stepKey="goToProductPage"/> -+ <waitForPageLoad stepKey="waitForStoreFrontProductPageLoad"/> -+ <fillField selector="{{StorefrontQuickSearchResultsSection.searchTextBox}}" userInput="{{updateVirtualProductSpecialPrice.sku}}" stepKey="fillVirtualProductName"/> -+ <waitForPageLoad stepKey="waitForSearchTextBox"/> -+ <click selector="{{StorefrontQuickSearchResultsSection.searchTextBoxButton}}" stepKey="clickSearchTextBoxButton"/> -+ <waitForPageLoad stepKey="waitForSearch"/> -+ <see selector="{{StorefrontQuickSearchResultsSection.productLink}}" userInput="{{updateVirtualProductSpecialPrice.name}}" stepKey="seeVirtualProductName"/> -+ -+ <!--Verify customer see updated virtual product with special price(from above step) on product storefront page by url key --> -+ <amOnPage url="{{StorefrontProductPage.url(updateVirtualProductSpecialPrice.urlKey)}}" stepKey="goToProductStorefrontPage"/> -+ <waitForPageLoad stepKey="waitForStorefrontProductPageLoad"/> -+ <see selector="{{StorefrontProductInfoMainSection.productName}}" userInput="{{updateVirtualProductSpecialPrice.name}}" stepKey="seeVirtualProductNameOnStoreFrontPage"/> -+ <see selector="{{StorefrontProductInfoMainSection.productSku}}" userInput="{{updateVirtualProductSpecialPrice.sku}}" stepKey="seeVirtualProductSku"/> -+ <!-- Verify customer see virtual product special price on the storefront page --> -+ <grabTextFrom selector="{{StorefrontProductInfoMainSection.specialPriceAmount}}" stepKey="specialPriceAmount"/> -+ <assertEquals stepKey="assertSpecialPriceTextOnProductPage"> -+ <expectedResult type="string">${{updateVirtualProductSpecialPrice.special_price}}</expectedResult> -+ <actualResult type="variable">specialPriceAmount</actualResult> -+ </assertEquals> -+ <!-- Verify customer see virtual product old price on the storefront page --> -+ <grabTextFrom selector="{{StorefrontProductInfoMainSection.oldPriceAmount}}" stepKey="oldPriceAmount"/> -+ <assertEquals stepKey="assertOldPriceTextOnProductPage"> -+ <expectedResult type="string">${{updateVirtualProductSpecialPrice.price}}</expectedResult> -+ <actualResult type="variable">oldPriceAmount</actualResult> -+ </assertEquals> -+ <!-- Verify customer see virtual product in stock status on the storefront page --> -+ <grabTextFrom selector="{{StorefrontProductInfoMainSection.productStockStatus}}" stepKey="productStockAvailableStatus"/> -+ <assertEquals stepKey="assertStockAvailableOnProductPage"> -+ <expectedResult type="string">{{updateVirtualProductSpecialPrice.storefrontStatus}}</expectedResult> -+ <actualResult type="variable">productStockAvailableStatus</actualResult> -+ </assertEquals> -+ </test> -+</tests> -diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateVirtualProductWithSpecialPriceOutOfStockVisibleInCategoryAndSearchTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateVirtualProductWithSpecialPriceOutOfStockVisibleInCategoryAndSearchTest.xml -new file mode 100644 -index 00000000000..58978c31b5b ---- /dev/null -+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateVirtualProductWithSpecialPriceOutOfStockVisibleInCategoryAndSearchTest.xml -@@ -0,0 +1,137 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> -+ <test name="UpdateVirtualProductWithSpecialPriceOutOfStockVisibleInCategoryAndSearchTest"> -+ <annotations> -+ <stories value="Update Virtual Product"/> -+ <title value="Update Virtual Product with Special Price (Out of Stock) Visible in Category and Search"/> -+ <description value="Test log in to Update Virtual Product and Update Virtual Product with Special Price (Out of Stock) Visible in Category and Search"/> -+ <testCaseId value="MC-6505"/> -+ <severity value="CRITICAL"/> -+ <group value="catalog"/> -+ <group value="mtf_migrated"/> -+ </annotations> -+ -+ <before> -+ <actionGroup ref = "LoginAsAdmin" stepKey="loginAsAdmin"/> -+ <createData entity="SimpleSubCategory" stepKey="initialCategoryEntity"/> -+ <createData entity="defaultVirtualProduct" stepKey="initialVirtualProduct"> -+ <requiredEntity createDataKey="initialCategoryEntity"/> -+ </createData> -+ <createData entity="SimpleSubCategory" stepKey="categoryEntity"/> -+ </before> -+ <after> -+ <deleteData stepKey="deleteSimpleSubCategory" createDataKey="initialCategoryEntity"/> -+ <deleteData stepKey="deleteSimpleSubCategory2" createDataKey="categoryEntity"/> -+ <actionGroup ref="logout" stepKey="logout"/> -+ </after> -+ -+ <!-- Search default virtual product in the grid page --> -+ <amOnPage url="{{ProductCatalogPage.url}}" stepKey="OpenProductCatalogPage1"/> -+ <waitForPageLoad stepKey="waitForProductCatalogPage1"/> -+ <conditionalClick selector="{{AdminProductGridFilterSection.clearAll}}" dependentSelector="{{AdminProductGridFilterSection.clearAll}}" visible="true" stepKey="clickClearAllFilter" /> -+ <fillField selector="{{AdminProductGridFilterSection.keywordSearch}}" userInput="$$initialVirtualProduct.name$$" stepKey="fillVirtualProductNameInKeywordSearch"/> -+ <click selector="{{AdminProductGridFilterSection.keywordSearchButton}}" stepKey="clickKeywordSearchButton"/> -+ <waitForPageLoad stepKey="waitForProductSearch"/> -+ <click selector="{{AdminProductGridFilterSection.nthRow('1')}}" stepKey="clickFirstRowToVerifyCreatedVirtualProduct"/> -+ <waitForPageLoad stepKey="waitUntilProductIsOpened"/> -+ -+ <!-- Update virtual product with special price(out of stock) --> -+ <fillField selector="{{AdminProductFormSection.productName}}" userInput="{{updateVirtualProductSpecialPriceOutOfStock.name}}" stepKey="fillProductName"/> -+ <fillField selector="{{AdminProductFormSection.productSku}}" userInput="{{updateVirtualProductSpecialPriceOutOfStock.sku}}" stepKey="fillProductSku"/> -+ <fillField selector="{{AdminProductFormSection.productPrice}}" userInput="{{updateVirtualProductSpecialPriceOutOfStock.price}}" stepKey="fillProductPrice"/> -+ <!-- Press enter to validate advanced pricing link --> -+ <pressKey selector="{{AdminProductFormSection.productPrice}}" parameterArray="[\Facebook\WebDriver\WebDriverKeys::ENTER]" stepKey="pressEnterKey"/> -+ <click selector="{{AdminProductFormSection.advancedPricingLink}}" stepKey="clickAdvancedPricingLink"/> -+ <fillField selector="{{AdminProductFormAdvancedPricingSection.specialPrice}}" userInput="{{updateVirtualProductSpecialPriceOutOfStock.special_price}}" stepKey="fillSpecialPrice"/> -+ <click selector="{{AdminProductFormAdvancedPricingSection.doneButton}}" stepKey="clickDoneButton"/> -+ <selectOption selector="{{AdminProductFormSection.productTaxClass}}" userInput="{{updateVirtualProductSpecialPriceOutOfStock.productTaxClass}}" stepKey="selectProductStockClass"/> -+ <selectOption selector="{{AdminProductFormSection.stockStatus}}" userInput="{{updateVirtualProductSpecialPriceOutOfStock.status}}" stepKey="selectStockStatusInStock"/> -+ <click selector="{{AdminProductFormSection.categoriesDropdown}}" stepKey="clickCategoriesDropDown"/> -+ <fillField selector="{{AdminProductFormSection.searchCategory}}" userInput="$$initialCategoryEntity.name$$" stepKey="fillSearchForInitialCategory" /> -+ <waitForPageLoad stepKey="waitForCategory1"/> -+ <click selector="{{AdminProductFormSection.selectCategory($$initialCategoryEntity.name$$)}}" stepKey="unselectInitialCategory"/> -+ <fillField selector="{{AdminProductFormSection.searchCategory}}" userInput="$$categoryEntity.name$$" stepKey="fillSearchCategory" /> -+ <waitForPageLoad stepKey="waitForCategory2"/> -+ <click selector="{{AdminProductFormSection.selectCategory($$categoryEntity.name$$)}}" stepKey="clickOnCategory"/> -+ <click selector="{{AdminProductFormSection.done}}" stepKey="clickOnDoneAdvancedCategorySelect"/> -+ <selectOption selector="{{AdminProductFormSection.visibility}}" userInput="{{updateVirtualProductSpecialPriceOutOfStock.visibility}}" stepKey="selectVisibility"/> -+ <click selector="{{AdminProductSEOSection.sectionHeader}}" stepKey="clickAdminProductSEOSection"/> -+ <fillField selector="{{AdminProductSEOSection.urlKeyInput}}" userInput="{{updateVirtualProductSpecialPriceOutOfStock.urlKey}}" stepKey="fillUrlKey"/> -+ <scrollToTopOfPage stepKey="scrollToTopOfAdminProductFormSection"/> -+ <click selector="{{AdminProductFormSection.save}}" stepKey="clickSaveButton"/> -+ <waitForPageLoad stepKey="waitForVirtualProductSaved"/> -+ <!-- Verify we see success message --> -+ <see selector="{{AdminProductFormSection.successMessage}}" userInput="You saved the product." stepKey="seeAssertVirtualProductSaveSuccessMessage"/> -+ -+ <!-- Search updated virtual product with special price(out of stock) in the grid --> -+ <amOnPage url="{{ProductCatalogPage.url}}" stepKey="OpenProductCatalogPageToSearchUpdatedVirtualProduct"/> -+ <waitForPageLoad stepKey="waitForProductCatalogPageToLoad"/> -+ <conditionalClick selector="{{AdminProductGridFilterSection.clearAll}}" dependentSelector="{{AdminProductGridFilterSection.clearAll}}" visible="true" stepKey="clickClearAll"/> -+ <click selector="{{AdminProductGridFilterSection.filters}}" stepKey="clickFiltersButton"/> -+ <fillField selector="{{AdminProductGridFilterSection.nameFilter}}" userInput="{{updateVirtualProductSpecialPriceOutOfStock.name}}" stepKey="fillVirtualProductNameInNameFilter"/> -+ <fillField selector="{{AdminProductGridFilterSection.skuFilter}}" userInput="{{updateVirtualProductSpecialPriceOutOfStock.sku}}" stepKey="fillVirtualProductSku"/> -+ <click selector="{{AdminProductGridFilterSection.applyFilters}}" stepKey="clickApplyFiltersButton"/> -+ <click selector="{{AdminProductGridFilterSection.nthRow('1')}}" stepKey="clickFirstRowToVerifyUpdatedVirtualProductVisibleInGrid"/> -+ <waitForPageLoad stepKey="waitUntilVirtualProductPageIsOpened"/> -+ <!-- Verify customer see updated virtual product with special price(out of stock) in the product form page --> -+ <seeInField selector="{{AdminProductFormSection.productName}}" userInput="{{updateVirtualProductSpecialPriceOutOfStock.name}}" stepKey="seeProductName"/> -+ <seeInField selector="{{AdminProductFormSection.productSku}}" userInput="{{updateVirtualProductSpecialPriceOutOfStock.sku}}" stepKey="seeProductSku"/> -+ <seeInField selector="{{AdminProductFormSection.productPrice}}" userInput="{{updateVirtualProductSpecialPriceOutOfStock.price}}" stepKey="seeProductPrice"/> -+ <click selector="{{AdminProductFormSection.advancedPricingLink}}" stepKey="clickAdvancedPricingLink1"/> -+ <seeInField selector="{{AdminProductFormAdvancedPricingSection.specialPrice}}" userInput="{{updateVirtualProductSpecialPriceOutOfStock.special_price}}" stepKey="seeSpecialPrice"/> -+ <click selector="{{AdminProductFormAdvancedPricingSection.advancedPricingCloseButton}}" stepKey="clickAdvancedPricingCloseButton"/> -+ <seeInField selector="{{AdminProductFormSection.productTaxClass}}" userInput="{{updateVirtualProductSpecialPriceOutOfStock.productTaxClass}}" stepKey="seeProductTaxClass"/> -+ <see selector="{{AdminProductFormSection.productStockStatus}}" userInput="{{updateVirtualProductSpecialPriceOutOfStock.status}}" stepKey="seeProductStockStatus"/> -+ <click selector="{{AdminProductFormSection.categoriesDropdown}}" stepKey="clickCategoriesDropDownToVerify"/> -+ <grabMultiple selector="{{AdminProductFormSection.selectMultipleCategories}}" stepKey="selectedCategories" /> -+ <assertEquals stepKey="assertSelectedCategories"> -+ <actualResult type="variable">selectedCategories</actualResult> -+ <expectedResult type="array">[$$categoryEntity.name$$]</expectedResult> -+ </assertEquals> -+ <click selector="{{AdminProductFormSection.done}}" stepKey="clickOnDoneOnCategorySelect"/> -+ <see selector="{{AdminProductFormSection.visibility}}" userInput="{{updateVirtualProductSpecialPriceOutOfStock.visibility}}" stepKey="seeVisibility"/> -+ <scrollTo selector="{{AdminProductSEOSection.sectionHeader}}" x="0" y="-80" stepKey="scrollToAdminProductSEOSection1"/> -+ <click selector="{{AdminProductSEOSection.sectionHeader}}" stepKey="clickAdminProductSEOSection1"/> -+ <seeInField selector="{{AdminProductSEOSection.urlKeyInput}}" userInput="{{updateVirtualProductSpecialPriceOutOfStock.urlKey}}" stepKey="seeUrlKey"/> -+ -+ <!--Verify customer see updated virtual product with special price(out of stock) on product storefront page --> -+ <amOnPage url="{{StorefrontProductPage.url(updateVirtualProductSpecialPriceOutOfStock.urlKey)}}" stepKey="goToProductPage"/> -+ <waitForPageLoad stepKey="waitForStorefrontProductPageLoad"/> -+ <see selector="{{StorefrontProductInfoMainSection.productName}}" userInput="{{updateVirtualProductSpecialPriceOutOfStock.name}}" stepKey="seeVirtualProductNameOnStoreFrontPage"/> -+ <see selector="{{StorefrontProductInfoMainSection.productPrice}}" userInput="{{updateVirtualProductSpecialPriceOutOfStock.price}}" stepKey="seeVirtualProductPriceOnStoreFrontPage"/> -+ <see selector="{{StorefrontProductInfoMainSection.productSku}}" userInput="{{updateVirtualProductSpecialPriceOutOfStock.sku}}" stepKey="seeVirtualProductSku"/> -+ <grabTextFrom selector="{{StorefrontProductInfoMainSection.productStockStatus}}" stepKey="productStockAvailableStatus"/> -+ <assertEquals stepKey="assertStockAvailableOnProductPage"> -+ <expectedResult type="string">{{updateVirtualProductSpecialPriceOutOfStock.storefrontStatus}}</expectedResult> -+ <actualResult type="variable">productStockAvailableStatus</actualResult> -+ </assertEquals> -+ <grabTextFrom selector="{{StorefrontProductInfoMainSection.oldPriceAmount}}" stepKey="productPriceAmount"/> -+ <assertEquals stepKey="assertOldPriceTextOnProductPage"> -+ <expectedResult type="string">${{updateVirtualProductSpecialPriceOutOfStock.price}}</expectedResult> -+ <actualResult type="variable">productPriceAmount</actualResult> -+ </assertEquals> -+ <!--Verify customer see virtual product with special price on the storefront page--> -+ <grabTextFrom selector="{{StorefrontProductInfoMainSection.specialPriceAmount}}" stepKey="specialPriceAmount"/> -+ <assertEquals stepKey="assertSpecialPriceTextOnProductPage"> -+ <expectedResult type="string">${{updateVirtualProductSpecialPriceOutOfStock.special_price}}</expectedResult> -+ <actualResult type="variable">specialPriceAmount</actualResult> -+ </assertEquals> -+ -+ <!--Verify customer don't see updated virtual product link on magento storefront page and is searchable by sku --> -+ <amOnPage url="{{StorefrontProductPage.url(updateVirtualProductSpecialPriceOutOfStock.urlKey)}}" stepKey="goToMagentoStorefrontPage"/> -+ <waitForPageLoad stepKey="waitForStoreFrontProductPageLoad"/> -+ <fillField selector="{{StorefrontQuickSearchResultsSection.searchTextBox}}" userInput="{{updateVirtualProductSpecialPriceOutOfStock.sku}}" stepKey="fillVirtualProductName"/> -+ <waitForPageLoad stepKey="waitForSearchTextBox"/> -+ <click selector="{{StorefrontQuickSearchResultsSection.searchTextBoxButton}}" stepKey="clickSearchTextBoxButton"/> -+ <waitForPageLoad stepKey="waitForSearch"/> -+ <dontSee selector="{{StorefrontQuickSearchResultsSection.productLink}}" userInput="{{updateVirtualProductSpecialPriceOutOfStock.name}}" stepKey="dontSeeVirtualProductNameOnStorefrontPage"/> -+ </test> -+</tests> -diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateVirtualProductWithTierPriceInStockVisibleInCategoryAndSearchTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateVirtualProductWithTierPriceInStockVisibleInCategoryAndSearchTest.xml -new file mode 100644 -index 00000000000..d28e9ddbb12 ---- /dev/null -+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateVirtualProductWithTierPriceInStockVisibleInCategoryAndSearchTest.xml -@@ -0,0 +1,157 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> -+ <test name="UpdateVirtualProductWithTierPriceInStockVisibleInCategoryAndSearchTest"> -+ <annotations> -+ <stories value="Update Virtual Product"/> -+ <title value="Update Virtual Product with Tier Price (In Stock) Visible in Category and Search"/> -+ <description value="Test log in to Update Virtual Product and Update Virtual Product with Tier Price (In Stock) Visible in Category and Search"/> -+ <testCaseId value="MC-6504"/> -+ <severity value="CRITICAL"/> -+ <group value="catalog"/> -+ <group value="mtf_migrated"/> -+ </annotations> -+ -+ <before> -+ <actionGroup ref = "LoginAsAdmin" stepKey="loginAsAdmin"/> -+ <createData entity="SimpleSubCategory" stepKey="initialCategoryEntity"/> -+ <createData entity="defaultVirtualProduct" stepKey="initialVirtualProduct"> -+ <requiredEntity createDataKey="initialCategoryEntity"/> -+ </createData> -+ <createData entity="SimpleSubCategory" stepKey="categoryEntity"/> -+ </before> -+ <after> -+ <deleteData stepKey="deleteSimpleSubCategory" createDataKey="initialCategoryEntity"/> -+ <deleteData stepKey="deleteSimpleSubCategory2" createDataKey="categoryEntity"/> -+ <actionGroup ref="logout" stepKey="logout"/> -+ </after> -+ -+ <!-- Search default virtual product in the grid --> -+ <amOnPage url="{{ProductCatalogPage.url}}" stepKey="OpenProductCatalogPage1"/> -+ <waitForPageLoad stepKey="waitForProductCatalogPage1"/> -+ <conditionalClick selector="{{AdminProductGridFilterSection.clearAll}}" dependentSelector="{{AdminProductGridFilterSection.clearAll}}" visible="true" stepKey="clickClearAllFilter" /> -+ <fillField selector="{{AdminProductGridFilterSection.keywordSearch}}" userInput="$$initialVirtualProduct.name$$" stepKey="fillVirtualProductNameInKeywordSearch"/> -+ <click selector="{{AdminProductGridFilterSection.keywordSearchButton}}" stepKey="clickKeywordSearchButton"/> -+ <waitForPageLoad stepKey="waitForProductSearch"/> -+ <click selector="{{AdminProductGridFilterSection.nthRow('1')}}" stepKey="clickFirstRowToVerifyCreatedVirtualProduct"/> -+ <waitForPageLoad stepKey="waitUntilProductIsOpened"/> -+ -+ <!-- Update virtual product with tier price --> -+ <fillField selector="{{AdminProductFormSection.productName}}" userInput="{{updateVirtualProductTierPriceInStock.name}}" stepKey="fillProductName"/> -+ <fillField selector="{{AdminProductFormSection.productSku}}" userInput="{{updateVirtualProductTierPriceInStock.sku}}" stepKey="fillProductSku"/> -+ <fillField selector="{{AdminProductFormSection.productPrice}}" userInput="{{updateVirtualProductTierPriceInStock.price}}" stepKey="fillProductPrice"/> -+ <!-- Press enter to validate advanced pricing link --> -+ <pressKey selector="{{AdminProductFormSection.productPrice}}" parameterArray="[\Facebook\WebDriver\WebDriverKeys::ENTER]" stepKey="pressEnterKey"/> -+ <click selector="{{AdminProductFormSection.advancedPricingLink}}" stepKey="clickAdvancedPricingLink"/> -+ <click selector="{{AdminProductFormAdvancedPricingSection.customerGroupPriceAddButton}}" stepKey="clickCustomerGroupPriceAddButton"/> -+ <scrollTo selector="{{AdminProductFormAdvancedPricingSection.productTierPriceQtyInput('0')}}" x="50" y="0" stepKey="scrollToProductTierPriceQuantityInputTextBox"/> -+ <selectOption selector="{{AdminProductFormAdvancedPricingSection.productTierPriceWebsiteSelect('0')}}" userInput="{{tierPriceOnVirtualProduct.website}}" stepKey="selectProductTierPriceWebsiteInput"/> -+ <selectOption selector="{{AdminProductFormAdvancedPricingSection.productTierPriceCustGroupSelect('0')}}" userInput="{{tierPriceOnVirtualProduct.customer_group}}" stepKey="selectProductTierPriceCustomerGroupInput"/> -+ <fillField selector="{{AdminProductFormAdvancedPricingSection.productTierPriceQtyInput('0')}}" userInput="{{tierPriceOnVirtualProduct.qty}}" stepKey="fillProductTierPriceQuantityInput"/> -+ <fillField selector="{{AdminProductFormAdvancedPricingSection.productTierPriceFixedPriceInput('0')}}" userInput="{{tierPriceOnVirtualProduct.price}}" stepKey="selectProductTierPriceFixedPrice"/> -+ <click selector="{{AdminProductFormAdvancedPricingSection.doneButton}}" stepKey="clickDoneButton"/> -+ <selectOption selector="{{AdminProductFormSection.productTaxClass}}" userInput="{{updateVirtualProductTierPriceInStock.productTaxClass}}" stepKey="selectProductStockClass"/> -+ <fillField selector="{{AdminProductFormSection.productQuantity}}" userInput="{{updateVirtualProductTierPriceInStock.quantity}}" stepKey="fillProductQuantity"/> -+ <selectOption selector="{{AdminProductFormSection.stockStatus}}" userInput="{{updateVirtualProductTierPriceInStock.status}}" stepKey="selectStockStatusInStock"/> -+ <click selector="{{AdminProductFormSection.categoriesDropdown}}" stepKey="clickCategoriesDropDown"/> -+ <fillField selector="{{AdminProductFormSection.searchCategory}}" userInput="$$initialCategoryEntity.name$$" stepKey="fillSearchForInitialCategory" /> -+ <waitForPageLoad stepKey="waitForCategory1"/> -+ <click selector="{{AdminProductFormSection.selectCategory($$initialCategoryEntity.name$$)}}" stepKey="unselectInitialCategory"/> -+ <fillField selector="{{AdminProductFormSection.searchCategory}}" userInput="$$categoryEntity.name$$" stepKey="fillSearchCategory" /> -+ <waitForPageLoad stepKey="waitForCategory2"/> -+ <click selector="{{AdminProductFormSection.selectCategory($$categoryEntity.name$$)}}" stepKey="clickOnCategory"/> -+ <click selector="{{AdminProductFormSection.done}}" stepKey="clickOnDoneAdvancedCategorySelect"/> -+ <selectOption selector="{{AdminProductFormSection.visibility}}" userInput="{{updateVirtualProductTierPriceInStock.visibility}}" stepKey="selectVisibility"/> -+ <click selector="{{AdminProductSEOSection.sectionHeader}}" stepKey="clickAdminProductSEOSection"/> -+ <fillField selector="{{AdminProductSEOSection.urlKeyInput}}" userInput="{{updateVirtualProductTierPriceInStock.urlKey}}" stepKey="fillUrlKey"/> -+ <scrollToTopOfPage stepKey="scrollToTopOfAdminProductFormSection"/> -+ <click selector="{{AdminProductFormSection.save}}" stepKey="clickSaveButton"/> -+ <waitForPageLoad stepKey="waitForVirtualProductSaved"/> -+ <!-- Verify we see success message --> -+ <see selector="{{AdminProductFormSection.successMessage}}" userInput="You saved the product." stepKey="seeAssertVirtualProductSaveSuccessMessage"/> -+ -+ <!-- Search updated virtual product(from above step) in the grid --> -+ <amOnPage url="{{ProductCatalogPage.url}}" stepKey="OpenProductCatalogPageToSearchUpdatedVirtualProduct"/> -+ <waitForPageLoad stepKey="waitForProductCatalogPageToLoad"/> -+ <conditionalClick selector="{{AdminProductGridFilterSection.clearAll}}" dependentSelector="{{AdminProductGridFilterSection.clearAll}}" visible="true" stepKey="clickClearAll"/> -+ <click selector="{{AdminProductGridFilterSection.filters}}" stepKey="clickFiltersButton"/> -+ <fillField selector="{{AdminProductGridFilterSection.nameFilter}}" userInput="{{updateVirtualProductTierPriceInStock.name}}" stepKey="fillVirtualProductNameInNameFilter"/> -+ <fillField selector="{{AdminProductGridFilterSection.skuFilter}}" userInput="{{updateVirtualProductTierPriceInStock.sku}}" stepKey="fillVirtualProductSku"/> -+ <click selector="{{AdminProductGridFilterSection.applyFilters}}" stepKey="clickApplyFiltersButton"/> -+ <click selector="{{AdminProductGridFilterSection.nthRow('1')}}" stepKey="clickFirstRowToVerifyUpdatedVirtualProductVisibleInGrid"/> -+ <waitForPageLoad stepKey="waitUntilVirtualProductPageIsOpened"/> -+ -+ <!-- Verify customer see updated virtual product with tier price(from the above step) in the product form page --> -+ <seeInField selector="{{AdminProductFormSection.productName}}" userInput="{{updateVirtualProductTierPriceInStock.name}}" stepKey="seeProductName"/> -+ <seeInField selector="{{AdminProductFormSection.productSku}}" userInput="{{updateVirtualProductTierPriceInStock.sku}}" stepKey="seeProductSku"/> -+ <seeInField selector="{{AdminProductFormSection.productPrice}}" userInput="{{updateVirtualProductTierPriceInStock.price}}" stepKey="seeProductPrice"/> -+ <click selector="{{AdminProductFormSection.advancedPricingLink}}" stepKey="clickAdvancedPricingLink1"/> -+ <seeInField selector="{{AdminProductFormAdvancedPricingSection.productTierPriceWebsiteSelect('0')}}" userInput="{{tierPriceOnVirtualProduct.website}}" stepKey="seeProductTierPriceWebsiteInput"/> -+ <seeInField selector="{{AdminProductFormAdvancedPricingSection.productTierPriceCustGroupSelect('0')}}" userInput="{{tierPriceOnVirtualProduct.customer_group}}" stepKey="seeProductTierPriceCustomerGroupInput"/> -+ <seeInField selector="{{AdminProductFormAdvancedPricingSection.productTierPriceQtyInput('0')}}" userInput="{{tierPriceOnVirtualProduct.qty}}" stepKey="seeProductTierPriceQuantityInput"/> -+ <seeInField selector="{{AdminProductFormAdvancedPricingSection.productTierPriceFixedPriceInput('0')}}" userInput="{{tierPriceOnVirtualProduct.price}}" stepKey="seeProductTierPriceFixedPrice"/> -+ <click selector="{{AdminProductFormAdvancedPricingSection.advancedPricingCloseButton}}" stepKey="clickAdvancedPricingCloseButton"/> -+ <seeInField selector="{{AdminProductFormSection.productTaxClass}}" userInput="{{updateVirtualProductTierPriceInStock.productTaxClass}}" stepKey="seeProductTaxClass"/> -+ <seeInField selector="{{AdminProductFormSection.productQuantity}}" userInput="{{updateVirtualProductTierPriceInStock.quantity}}" stepKey="seeProductQuantity"/> -+ <seeInField selector="{{AdminProductFormSection.productStockStatus}}" userInput="{{updateVirtualProductTierPriceInStock.status}}" stepKey="seeProductStockStatus"/> -+ <click selector="{{AdminProductFormSection.categoriesDropdown}}" stepKey="clickCategoriesDropDownToVerify"/> -+ <grabMultiple selector="{{AdminProductFormSection.selectMultipleCategories}}" stepKey="selectedCategories" /> -+ <assertEquals stepKey="assertSelectedCategories"> -+ <actualResult type="variable">selectedCategories</actualResult> -+ <expectedResult type="array">[$$categoryEntity.name$$]</expectedResult> -+ </assertEquals> -+ <click selector="{{AdminProductFormSection.done}}" stepKey="clickOnDoneOnCategorySelect"/> -+ <seeInField selector="{{AdminProductFormSection.visibility}}" userInput="{{updateVirtualProductTierPriceInStock.visibility}}" stepKey="seeVisibility"/> -+ <scrollTo selector="{{AdminProductSEOSection.sectionHeader}}" x="0" y="-80" stepKey="scrollToAdminProductSEOSection1"/> -+ <click selector="{{AdminProductSEOSection.sectionHeader}}" stepKey="clickAdminProductSEOSection1"/> -+ <seeInField selector="{{AdminProductSEOSection.urlKeyInput}}" userInput="{{updateVirtualProductTierPriceInStock.urlKey}}" stepKey="seeUrlKey"/> -+ -+ <!--Verify customer see updated virtual product link on category page --> -+ <amOnPage url="{{StorefrontCategoryPage.url($$categoryEntity.name$$)}}" stepKey="openCategoryPage"/> -+ <waitForPageLoad stepKey="waitForCategoryPageLoad"/> -+ <see selector="{{StorefrontCategoryMainSection.productLink}}" userInput="{{updateVirtualProductTierPriceInStock.name}}" stepKey="seeVirtualProductLinkOnCategoryPage"/> -+ -+ <!--Verify customer see updated virtual product with tier price on product storefront page --> -+ <amOnPage url="{{StorefrontProductPage.url(updateVirtualProductTierPriceInStock.urlKey)}}" stepKey="goToProductPage"/> -+ <waitForPageLoad stepKey="waitForStorefrontProductPageLoad"/> -+ <see selector="{{StorefrontProductInfoMainSection.productName}}" userInput="{{updateVirtualProductTierPriceInStock.name}}" stepKey="seeVirtualProductNameOnStoreFrontPage"/> -+ <see selector="{{StorefrontProductInfoMainSection.productPrice}}" userInput="{{updateVirtualProductTierPriceInStock.price}}" stepKey="seeVirtualProductPriceOnStoreFrontPage"/> -+ <see selector="{{StorefrontProductInfoMainSection.productSku}}" userInput="{{updateVirtualProductTierPriceInStock.sku}}" stepKey="seeVirtualProductSku"/> -+ <grabTextFrom selector="{{StorefrontProductInfoMainSection.productStockStatus}}" stepKey="productStockAvailableStatus"/> -+ <assertEquals stepKey="assertStockAvailableOnProductPage"> -+ <expectedResult type="string">{{updateVirtualProductTierPriceInStock.storefrontStatus}}</expectedResult> -+ <actualResult type="variable">productStockAvailableStatus</actualResult> -+ </assertEquals> -+ <grabTextFrom selector="{{StorefrontProductInfoMainSection.productPrice}}" stepKey="productPriceAmount"/> -+ <assertEquals stepKey="assertOldPriceTextOnProductPage"> -+ <expectedResult type="string">${{updateVirtualProductTierPriceInStock.price}}</expectedResult> -+ <actualResult type="variable">productPriceAmount</actualResult> -+ </assertEquals> -+ <grabTextFrom selector="{{StorefrontProductInfoMainSection.tierPriceText}}" stepKey="tierPriceText"/> -+ <assertEquals stepKey="assertTierPriceTextOnProductPage"> -+ <expectedResult type="string">Buy {{tierPriceOnVirtualProduct.qty}} for ${{tierPriceOnVirtualProduct.price}} each and save 38%</expectedResult> -+ <actualResult type="variable">tierPriceText</actualResult> -+ </assertEquals> -+ -+ <!--Verify customer see updated virtual product link on magento storefront page and is searchable by sku --> -+ <amOnPage url="{{StorefrontProductPage.url(updateVirtualProductTierPriceInStock.urlKey)}}" stepKey="goToMagentoStorefrontPage"/> -+ <waitForPageLoad stepKey="waitForStoreFrontProductPageLoad"/> -+ <fillField selector="{{StorefrontQuickSearchResultsSection.searchTextBox}}" userInput="{{updateVirtualProductTierPriceInStock.sku}}" stepKey="fillVirtualProductName"/> -+ <waitForPageLoad stepKey="waitForSearchTextBox"/> -+ <click selector="{{StorefrontQuickSearchResultsSection.searchTextBoxButton}}" stepKey="clickSearchTextBoxButton"/> -+ <waitForPageLoad stepKey="waitForSearch"/> -+ <see selector="{{StorefrontQuickSearchResultsSection.productLink}}" userInput="{{updateVirtualProductTierPriceInStock.name}}" stepKey="seeVirtualProductName"/> -+ <grabTextFrom selector="{{StorefrontQuickSearchResultsSection.asLowAsLabel}}" stepKey="tierPriceTextOnStorefrontPage"/> -+ <assertEquals stepKey="assertTierPriceTextOnCategoryPage"> -+ <expectedResult type="string">As low as ${{tierPriceOnVirtualProduct.price}}</expectedResult> -+ <actualResult type="variable">tierPriceTextOnStorefrontPage</actualResult> -+ </assertEquals> -+ </test> -+</tests> -diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateVirtualProductWithTierPriceInStockVisibleInCategoryOnlyTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateVirtualProductWithTierPriceInStockVisibleInCategoryOnlyTest.xml -new file mode 100644 -index 00000000000..22dd2b0054d ---- /dev/null -+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateVirtualProductWithTierPriceInStockVisibleInCategoryOnlyTest.xml -@@ -0,0 +1,153 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> -+ <test name="AdminUpdateVirtualProductWithTierPriceInStockVisibleInCategoryOnlyTest"> -+ <annotations> -+ <stories value="Update Virtual Product"/> -+ <title value="Update Virtual Product with Tier Price (In Stock) Visible in Category Only"/> -+ <description value="Test log in to Update Virtual Product and Update Virtual Product with Tier Price (In Stock) Visible in Category Only"/> -+ <testCaseId value="MC-7508"/> -+ <severity value="CRITICAL"/> -+ <group value="catalog"/> -+ <group value="mtf_migrated"/> -+ </annotations> -+ -+ <before> -+ <actionGroup ref = "LoginAsAdmin" stepKey="loginAsAdmin"/> -+ <createData entity="SimpleSubCategory" stepKey="initialCategoryEntity"/> -+ <createData entity="defaultVirtualProduct" stepKey="initialVirtualProduct"> -+ <requiredEntity createDataKey="initialCategoryEntity"/> -+ </createData> -+ <createData entity="SimpleSubCategory" stepKey="categoryEntity"/> -+ </before> -+ <after> -+ <deleteData stepKey="deleteSimpleSubCategory" createDataKey="initialCategoryEntity"/> -+ <deleteData stepKey="deleteSimpleSubCategory2" createDataKey="categoryEntity"/> -+ <actionGroup ref="logout" stepKey="logout"/> -+ </after> -+ -+ <!-- Search default virtual product in the grid page --> -+ <amOnPage url="{{ProductCatalogPage.url}}" stepKey="OpenProductCatalogPage1"/> -+ <waitForPageLoad stepKey="waitForProductCatalogPage1"/> -+ <conditionalClick selector="{{AdminProductGridFilterSection.clearAll}}" dependentSelector="{{AdminProductGridFilterSection.clearAll}}" visible="true" stepKey="clickClearAllFilter" /> -+ <fillField selector="{{AdminProductGridFilterSection.keywordSearch}}" userInput="$$initialVirtualProduct.name$$" stepKey="fillVirtualProductNameInKeywordSearch"/> -+ <click selector="{{AdminProductGridFilterSection.keywordSearchButton}}" stepKey="clickKeywordSearchButton"/> -+ <waitForPageLoad stepKey="waitForProductSearch"/> -+ <click selector="{{AdminProductGridFilterSection.nthRow('1')}}" stepKey="clickFirstRowToVerifyCreatedVirtualProduct"/> -+ <waitForPageLoad stepKey="waitUntilProductIsOpened"/> -+ -+ <!-- Update virtual product with tier price(in stock) --> -+ <fillField selector="{{AdminProductFormSection.productName}}" userInput="{{updateVirtualProductWithTierPriceInStock.name}}" stepKey="fillProductName"/> -+ <fillField selector="{{AdminProductFormSection.productSku}}" userInput="{{updateVirtualProductWithTierPriceInStock.sku}}" stepKey="fillProductSku"/> -+ <fillField selector="{{AdminProductFormSection.productPrice}}" userInput="{{updateVirtualProductWithTierPriceInStock.price}}" stepKey="fillProductPrice"/> -+ <!-- Press enter to validate advanced pricing link --> -+ <pressKey selector="{{AdminProductFormSection.productPrice}}" parameterArray="[\Facebook\WebDriver\WebDriverKeys::ENTER]" stepKey="pressEnterKey"/> -+ <click selector="{{AdminProductFormSection.advancedPricingLink}}" stepKey="clickAdvancedPricingLink"/> -+ <click selector="{{AdminProductFormAdvancedPricingSection.customerGroupPriceAddButton}}" stepKey="clickCustomerGroupPriceAddButton"/> -+ <scrollTo selector="{{AdminProductFormAdvancedPricingSection.productTierPriceQtyInput('0')}}" x="50" y="0" stepKey="scrollToProductTierPriceQuantityInputTextBox"/> -+ <selectOption selector="{{AdminProductFormAdvancedPricingSection.productTierPriceWebsiteSelect('0')}}" userInput="{{tierPriceOnVirtualProduct.website}}" stepKey="selectProductTierPriceWebsiteInput"/> -+ <selectOption selector="{{AdminProductFormAdvancedPricingSection.productTierPriceCustGroupSelect('0')}}" userInput="{{tierPriceOnVirtualProduct.customer_group}}" stepKey="selectProductTierPriceCustomerGroupInput"/> -+ <fillField selector="{{AdminProductFormAdvancedPricingSection.productTierPriceQtyInput('0')}}" userInput="{{tierPriceOnVirtualProduct.qty}}" stepKey="fillProductTierPriceQuantityInput"/> -+ <fillField selector="{{AdminProductFormAdvancedPricingSection.productTierPriceFixedPriceInput('0')}}" userInput="{{tierPriceOnVirtualProduct.price}}" stepKey="selectProductTierPriceFixedPrice"/> -+ <click selector="{{AdminProductFormAdvancedPricingSection.doneButton}}" stepKey="clickDoneButton"/> -+ <selectOption selector="{{AdminProductFormSection.productTaxClass}}" userInput="{{updateVirtualProductWithTierPriceInStock.productTaxClass}}" stepKey="selectProductStockClass"/> -+ <fillField selector="{{AdminProductFormSection.productQuantity}}" userInput="{{updateVirtualProductWithTierPriceInStock.quantity}}" stepKey="fillProductQuantity"/> -+ <selectOption selector="{{AdminProductFormSection.stockStatus}}" userInput="{{updateVirtualProductWithTierPriceInStock.status}}" stepKey="selectStockStatusInStock"/> -+ <click selector="{{AdminProductFormSection.categoriesDropdown}}" stepKey="clickCategoriesDropDown"/> -+ <fillField selector="{{AdminProductFormSection.searchCategory}}" userInput="$$initialCategoryEntity.name$$" stepKey="fillSearchForInitialCategory" /> -+ <waitForPageLoad stepKey="waitForCategory1"/> -+ <click selector="{{AdminProductFormSection.selectCategory($$initialCategoryEntity.name$$)}}" stepKey="unselectInitialCategory"/> -+ <fillField selector="{{AdminProductFormSection.searchCategory}}" userInput="$$categoryEntity.name$$" stepKey="fillSearchCategory" /> -+ <waitForPageLoad stepKey="waitForCategory2"/> -+ <click selector="{{AdminProductFormSection.selectCategory($$categoryEntity.name$$)}}" stepKey="clickOnCategory"/> -+ <click selector="{{AdminProductFormSection.done}}" stepKey="clickOnDoneAdvancedCategorySelect"/> -+ <selectOption selector="{{AdminProductFormSection.visibility}}" userInput="{{updateVirtualProductWithTierPriceInStock.visibility}}" stepKey="selectVisibility"/> -+ <click selector="{{AdminProductSEOSection.sectionHeader}}" stepKey="clickAdminProductSEOSection"/> -+ <fillField selector="{{AdminProductSEOSection.urlKeyInput}}" userInput="{{updateVirtualProductWithTierPriceInStock.urlKey}}" stepKey="fillUrlKey"/> -+ <scrollToTopOfPage stepKey="scrollToTopOfAdminProductFormSection"/> -+ <click selector="{{AdminProductFormSection.save}}" stepKey="clickSaveButton"/> -+ <waitForPageLoad stepKey="waitForVirtualProductSaved"/> -+ <!-- Verify we see success message --> -+ <see selector="{{AdminProductFormSection.successMessage}}" userInput="You saved the product." stepKey="seeAssertVirtualProductSaveSuccessMessage"/> -+ -+ <!-- Search updated virtual product(from above step) in the grid --> -+ <amOnPage url="{{ProductCatalogPage.url}}" stepKey="OpenProductCatalogPageToSearchUpdatedVirtualProduct"/> -+ <waitForPageLoad stepKey="waitForProductCatalogPageToLoad"/> -+ <conditionalClick selector="{{AdminProductGridFilterSection.clearAll}}" dependentSelector="{{AdminProductGridFilterSection.clearAll}}" visible="true" stepKey="clickClearAll"/> -+ <click selector="{{AdminProductGridFilterSection.filters}}" stepKey="clickFiltersButton"/> -+ <fillField selector="{{AdminProductGridFilterSection.nameFilter}}" userInput="{{updateVirtualProductWithTierPriceInStock.name}}" stepKey="fillVirtualProductNameInNameFilter"/> -+ <fillField selector="{{AdminProductGridFilterSection.skuFilter}}" userInput="{{updateVirtualProductWithTierPriceInStock.sku}}" stepKey="fillVirtualProductSku"/> -+ <click selector="{{AdminProductGridFilterSection.applyFilters}}" stepKey="clickApplyFiltersButton"/> -+ <click selector="{{AdminProductGridFilterSection.nthRow('1')}}" stepKey="clickFirstRowToVerifyUpdatedVirtualProductVisibleInGrid"/> -+ <waitForPageLoad stepKey="waitUntilVirtualProductPageIsOpened"/> -+ -+ <!-- Verify customer see updated virtual product with tier price(from the above step) in the product form page --> -+ <seeInField selector="{{AdminProductFormSection.productName}}" userInput="{{updateVirtualProductWithTierPriceInStock.name}}" stepKey="seeProductName"/> -+ <seeInField selector="{{AdminProductFormSection.productSku}}" userInput="{{updateVirtualProductWithTierPriceInStock.sku}}" stepKey="seeProductSku"/> -+ <seeInField selector="{{AdminProductFormSection.productPrice}}" userInput="{{updateVirtualProductWithTierPriceInStock.price}}" stepKey="seeProductPrice"/> -+ <click selector="{{AdminProductFormSection.advancedPricingLink}}" stepKey="clickAdvancedPricingLink1"/> -+ <seeInField selector="{{AdminProductFormAdvancedPricingSection.productTierPriceWebsiteSelect('0')}}" userInput="{{tierPriceOnVirtualProduct.website}}" stepKey="seeProductTierPriceWebsiteInput"/> -+ <seeInField selector="{{AdminProductFormAdvancedPricingSection.productTierPriceCustGroupSelect('0')}}" userInput="{{tierPriceOnVirtualProduct.customer_group}}" stepKey="seeProductTierPriceCustomerGroupInput"/> -+ <seeInField selector="{{AdminProductFormAdvancedPricingSection.productTierPriceQtyInput('0')}}" userInput="{{tierPriceOnVirtualProduct.qty}}" stepKey="seeProductTierPriceQuantityInput"/> -+ <seeInField selector="{{AdminProductFormAdvancedPricingSection.productTierPriceFixedPriceInput('0')}}" userInput="{{tierPriceOnVirtualProduct.price}}" stepKey="seeProductTierPriceFixedPrice"/> -+ <click selector="{{AdminProductFormAdvancedPricingSection.advancedPricingCloseButton}}" stepKey="clickAdvancedPricingCloseButton"/> -+ <seeInField selector="{{AdminProductFormSection.productTaxClass}}" userInput="{{updateVirtualProductWithTierPriceInStock.productTaxClass}}" stepKey="seeProductTaxClass"/> -+ <seeInField selector="{{AdminProductFormSection.productQuantity}}" userInput="{{updateVirtualProductWithTierPriceInStock.quantity}}" stepKey="seeProductQuantity"/> -+ <seeInField selector="{{AdminProductFormSection.productStockStatus}}" userInput="{{updateVirtualProductWithTierPriceInStock.status}}" stepKey="seeProductStockStatus"/> -+ <click selector="{{AdminProductFormSection.categoriesDropdown}}" stepKey="clickCategoriesDropDownToVerify"/> -+ <grabMultiple selector="{{AdminProductFormSection.selectMultipleCategories}}" stepKey="selectedCategories" /> -+ <assertEquals stepKey="assertSelectedCategories"> -+ <actualResult type="variable">selectedCategories</actualResult> -+ <expectedResult type="array">[$$categoryEntity.name$$]</expectedResult> -+ </assertEquals> -+ <click selector="{{AdminProductFormSection.done}}" stepKey="clickOnDoneOnCategorySelect"/> -+ <seeInField selector="{{AdminProductFormSection.visibility}}" userInput="{{updateVirtualProductWithTierPriceInStock.visibility}}" stepKey="seeVisibility"/> -+ <scrollTo selector="{{AdminProductSEOSection.sectionHeader}}" x="0" y="-80" stepKey="scrollToAdminProductSEOSection1"/> -+ <click selector="{{AdminProductSEOSection.sectionHeader}}" stepKey="clickAdminProductSEOSection1"/> -+ <seeInField selector="{{AdminProductSEOSection.urlKeyInput}}" userInput="{{updateVirtualProductWithTierPriceInStock.urlKey}}" stepKey="seeUrlKey"/> -+ -+ <!--Verify customer see updated virtual product link on category page --> -+ <amOnPage url="{{StorefrontCategoryPage.url($$categoryEntity.name$$)}}" stepKey="openCategoryPage"/> -+ <waitForPageLoad stepKey="waitForCategoryPageLoad"/> -+ <see selector="{{StorefrontCategoryMainSection.productLink}}" userInput="{{updateVirtualProductWithTierPriceInStock.name}}" stepKey="seeVirtualProductNameOnCategoryPage"/> -+ -+ <!--Verify customer see updated virtual product with tier price(from above step) on product storefront page --> -+ <amOnPage url="{{StorefrontProductPage.url(updateVirtualProductWithTierPriceInStock.urlKey)}}" stepKey="goToProductPage"/> -+ <waitForPageLoad stepKey="waitForStorefrontProductPageLoad"/> -+ <see selector="{{StorefrontProductInfoMainSection.productName}}" userInput="{{updateVirtualProductWithTierPriceInStock.name}}" stepKey="seeVirtualProductNameOnStoreFrontPage"/> -+ <see selector="{{StorefrontProductInfoMainSection.productPrice}}" userInput="{{updateVirtualProductWithTierPriceInStock.price}}" stepKey="seeVirtualProductPriceOnStoreFrontPage"/> -+ <see selector="{{StorefrontProductInfoMainSection.productSku}}" userInput="{{updateVirtualProductWithTierPriceInStock.sku}}" stepKey="seeVirtualProductSku"/> -+ <grabTextFrom selector="{{StorefrontProductInfoMainSection.productStockStatus}}" stepKey="productStockAvailableStatus"/> -+ <assertEquals stepKey="assertStockAvailableOnProductPage"> -+ <expectedResult type="string">{{updateVirtualProductWithTierPriceInStock.storefrontStatus}}</expectedResult> -+ <actualResult type="variable">productStockAvailableStatus</actualResult> -+ </assertEquals> -+ <grabTextFrom selector="{{StorefrontProductInfoMainSection.productPrice}}" stepKey="productPriceAmount"/> -+ <assertEquals stepKey="assertOldPriceTextOnProductPage"> -+ <expectedResult type="string">${{updateVirtualProductWithTierPriceInStock.price}}</expectedResult> -+ <actualResult type="variable">productPriceAmount</actualResult> -+ </assertEquals> -+ <!-- Verify customer see product tier price on product page --> -+ <grabTextFrom selector="{{StorefrontProductInfoMainSection.tierPriceText}}" stepKey="tierPriceText"/> -+ <assertEquals stepKey="assertTierPriceTextOnProductPage"> -+ <expectedResult type="string">Buy {{tierPriceOnVirtualProduct.qty}} for ${{tierPriceOnVirtualProduct.price}} each and save 10%</expectedResult> -+ <actualResult type="variable">tierPriceText</actualResult> -+ </assertEquals> -+ -+ <!--Verify customer don't see updated virtual product link on magento storefront page and is searchable by sku --> -+ <amOnPage url="{{StorefrontProductPage.url(updateVirtualProductWithTierPriceInStock.urlKey)}}" stepKey="goToMagentoStorefrontPage"/> -+ <waitForPageLoad stepKey="waitForStoreFrontProductPageLoad"/> -+ <fillField selector="{{StorefrontQuickSearchResultsSection.searchTextBox}}" userInput="{{updateVirtualProductTierPriceInStock.sku}}" stepKey="fillVirtualProductName"/> -+ <waitForPageLoad stepKey="waitForSearchTextBox"/> -+ <click selector="{{StorefrontQuickSearchResultsSection.searchTextBoxButton}}" stepKey="clickSearchTextBoxButton"/> -+ <waitForPageLoad stepKey="waitForSearch"/> -+ <dontSee selector="{{StorefrontQuickSearchResultsSection.productLink}}" userInput="{{updateVirtualProductWithTierPriceInStock.name}}" stepKey="dontSeeVirtualProductName"/> -+ </test> -+</tests> -diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateVirtualProductWithTierPriceOutOfStockVisibleInCategoryAndSearchTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateVirtualProductWithTierPriceOutOfStockVisibleInCategoryAndSearchTest.xml -new file mode 100644 -index 00000000000..29c7536d216 ---- /dev/null -+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateVirtualProductWithTierPriceOutOfStockVisibleInCategoryAndSearchTest.xml -@@ -0,0 +1,153 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> -+ <test name="AdminUpdateVirtualProductWithTierPriceOutOfStockVisibleInCategoryAndSearchTest"> -+ <annotations> -+ <stories value="Update Virtual Product"/> -+ <title value="Update Virtual Product with Tier Price (Out of Stock) Visible in Category and Search"/> -+ <description value="Test log in to Update Virtual Product and Update Virtual Product with Tier Price (Out of Stock) Visible in Category and Search"/> -+ <testCaseId value="MC-6499"/> -+ <severity value="CRITICAL"/> -+ <group value="catalog"/> -+ <group value="mtf_migrated"/> -+ </annotations> -+ -+ <before> -+ <actionGroup ref = "LoginAsAdmin" stepKey="loginAsAdmin"/> -+ <createData entity="SimpleSubCategory" stepKey="initialCategoryEntity"/> -+ <createData entity="defaultVirtualProduct" stepKey="initialVirtualProduct"> -+ <requiredEntity createDataKey="initialCategoryEntity"/> -+ </createData> -+ <createData entity="SimpleSubCategory" stepKey="categoryEntity"/> -+ </before> -+ <after> -+ <deleteData stepKey="deleteSimpleSubCategory" createDataKey="initialCategoryEntity"/> -+ <deleteData stepKey="deleteSimpleSubCategory2" createDataKey="categoryEntity"/> -+ <actionGroup ref="logout" stepKey="logout"/> -+ </after> -+ -+ <!-- Search default virtual product in the grid page --> -+ <amOnPage url="{{ProductCatalogPage.url}}" stepKey="OpenProductCatalogPage1"/> -+ <waitForPageLoad stepKey="waitForProductCatalogPage1"/> -+ <conditionalClick selector="{{AdminProductGridFilterSection.clearAll}}" dependentSelector="{{AdminProductGridFilterSection.clearAll}}" visible="true" stepKey="clickClearAllFilter" /> -+ <fillField selector="{{AdminProductGridFilterSection.keywordSearch}}" userInput="$$initialVirtualProduct.name$$" stepKey="fillVirtualProductNameInKeywordSearch"/> -+ <click selector="{{AdminProductGridFilterSection.keywordSearchButton}}" stepKey="clickKeywordSearchButton"/> -+ <waitForPageLoad stepKey="waitForProductSearch"/> -+ <click selector="{{AdminProductGridFilterSection.nthRow('1')}}" stepKey="clickFirstRowToVerifyCreatedVirtualProduct"/> -+ <waitForPageLoad stepKey="waitUntilProductIsOpened"/> -+ -+ <!-- Update virtual product with tier price(out of stock) --> -+ <fillField selector="{{AdminProductFormSection.productName}}" userInput="{{updateVirtualTierPriceOutOfStock.name}}" stepKey="fillProductName"/> -+ <fillField selector="{{AdminProductFormSection.productSku}}" userInput="{{updateVirtualTierPriceOutOfStock.sku}}" stepKey="fillProductSku"/> -+ <fillField selector="{{AdminProductFormSection.productPrice}}" userInput="{{updateVirtualTierPriceOutOfStock.price}}" stepKey="fillProductPrice"/> -+ <!-- Press enter to validate advanced pricing link --> -+ <pressKey selector="{{AdminProductFormSection.productPrice}}" parameterArray="[\Facebook\WebDriver\WebDriverKeys::ENTER]" stepKey="pressEnterKey"/> -+ <click selector="{{AdminProductFormSection.advancedPricingLink}}" stepKey="clickAdvancedPricingLink"/> -+ <click selector="{{AdminProductFormAdvancedPricingSection.customerGroupPriceAddButton}}" stepKey="clickCustomerGroupPriceAddButton"/> -+ <scrollTo selector="{{AdminProductFormAdvancedPricingSection.productTierPriceQtyInput('0')}}" x="50" y="0" stepKey="scrollToProductTierPriceQuantityInputTextBox"/> -+ <selectOption selector="{{AdminProductFormAdvancedPricingSection.productTierPriceWebsiteSelect('0')}}" userInput="{{tierPriceOnVirtualProduct.website}}" stepKey="selectProductTierPriceWebsiteInput"/> -+ <selectOption selector="{{AdminProductFormAdvancedPricingSection.productTierPriceCustGroupSelect('0')}}" userInput="{{tierPriceOnVirtualProduct.customer_group}}" stepKey="selectProductTierPriceCustomerGroupInput"/> -+ <fillField selector="{{AdminProductFormAdvancedPricingSection.productTierPriceQtyInput('0')}}" userInput="{{tierPriceOnVirtualProduct.qty}}" stepKey="fillProductTierPriceQuantityInput"/> -+ <fillField selector="{{AdminProductFormAdvancedPricingSection.productTierPriceFixedPriceInput('0')}}" userInput="{{tierPriceOnVirtualProduct.price}}" stepKey="selectProductTierPriceFixedPrice"/> -+ <click selector="{{AdminProductFormAdvancedPricingSection.doneButton}}" stepKey="clickDoneButton"/> -+ <selectOption selector="{{AdminProductFormSection.productTaxClass}}" userInput="{{updateVirtualTierPriceOutOfStock.productTaxClass}}" stepKey="selectProductStockClass"/> -+ <fillField selector="{{AdminProductFormSection.productQuantity}}" userInput="{{updateVirtualTierPriceOutOfStock.quantity}}" stepKey="fillProductQuantity"/> -+ <selectOption selector="{{AdminProductFormSection.stockStatus}}" userInput="{{updateVirtualTierPriceOutOfStock.status}}" stepKey="selectStockStatusInStock"/> -+ <click selector="{{AdminProductFormSection.categoriesDropdown}}" stepKey="clickCategoriesDropDown"/> -+ <fillField selector="{{AdminProductFormSection.searchCategory}}" userInput="$$initialCategoryEntity.name$$" stepKey="fillSearchForInitialCategory" /> -+ <waitForPageLoad stepKey="waitForCategory1"/> -+ <click selector="{{AdminProductFormSection.selectCategory($$initialCategoryEntity.name$$)}}" stepKey="unselectInitialCategory"/> -+ <fillField selector="{{AdminProductFormSection.searchCategory}}" userInput="$$categoryEntity.name$$" stepKey="fillSearchCategory" /> -+ <waitForPageLoad stepKey="waitForCategory2"/> -+ <click selector="{{AdminProductFormSection.selectCategory($$categoryEntity.name$$)}}" stepKey="clickOnCategory"/> -+ <click selector="{{AdminProductFormSection.done}}" stepKey="clickOnDoneAdvancedCategorySelect"/> -+ <selectOption selector="{{AdminProductFormSection.visibility}}" userInput="{{updateVirtualTierPriceOutOfStock.visibility}}" stepKey="selectVisibility"/> -+ <click selector="{{AdminProductSEOSection.sectionHeader}}" stepKey="clickAdminProductSEOSection"/> -+ <fillField selector="{{AdminProductSEOSection.urlKeyInput}}" userInput="{{updateVirtualTierPriceOutOfStock.urlKey}}" stepKey="fillUrlKey"/> -+ <scrollToTopOfPage stepKey="scrollToTopOfAdminProductFormSection"/> -+ <click selector="{{AdminProductFormSection.save}}" stepKey="clickSaveButton"/> -+ <waitForPageLoad stepKey="waitForVirtualProductSaved"/> -+ <!-- Verify we see success message --> -+ <see selector="{{AdminProductFormSection.successMessage}}" userInput="You saved the product." stepKey="seeAssertVirtualProductSaveSuccessMessage"/> -+ -+ <!-- Search updated virtual product(from above step) in the grid page --> -+ <amOnPage url="{{ProductCatalogPage.url}}" stepKey="OpenProductCatalogPageToSearchUpdatedVirtualProduct"/> -+ <waitForPageLoad stepKey="waitForProductCatalogPageToLoad"/> -+ <conditionalClick selector="{{AdminProductGridFilterSection.clearAll}}" dependentSelector="{{AdminProductGridFilterSection.clearAll}}" visible="true" stepKey="clickClearAll"/> -+ <click selector="{{AdminProductGridFilterSection.filters}}" stepKey="clickFiltersButton"/> -+ <fillField selector="{{AdminProductGridFilterSection.nameFilter}}" userInput="{{updateVirtualTierPriceOutOfStock.name}}" stepKey="fillVirtualProductNameInNameFilter"/> -+ <fillField selector="{{AdminProductGridFilterSection.skuFilter}}" userInput="{{updateVirtualTierPriceOutOfStock.sku}}" stepKey="fillVirtualProductSku"/> -+ <click selector="{{AdminProductGridFilterSection.applyFilters}}" stepKey="clickApplyFiltersButton"/> -+ <click selector="{{AdminProductGridFilterSection.nthRow('1')}}" stepKey="clickFirstRowToVerifyUpdatedVirtualProductVisibleInGrid"/> -+ <waitForPageLoad stepKey="waitUntilVirtualProductPageIsOpened"/> -+ -+ <!-- Verify we customer see updated virtual product with tier price(from the above step) in the product form page --> -+ <seeInField selector="{{AdminProductFormSection.productName}}" userInput="{{updateVirtualTierPriceOutOfStock.name}}" stepKey="seeProductName"/> -+ <seeInField selector="{{AdminProductFormSection.productSku}}" userInput="{{updateVirtualTierPriceOutOfStock.sku}}" stepKey="seeProductSku"/> -+ <seeInField selector="{{AdminProductFormSection.productPrice}}" userInput="{{updateVirtualTierPriceOutOfStock.price}}" stepKey="seeProductPrice"/> -+ <click selector="{{AdminProductFormSection.advancedPricingLink}}" stepKey="clickAdvancedPricingLink1"/> -+ <seeInField selector="{{AdminProductFormAdvancedPricingSection.productTierPriceWebsiteSelect('0')}}" userInput="{{tierPriceOnVirtualProduct.website}}" stepKey="seeProductTierPriceWebsiteInput"/> -+ <seeInField selector="{{AdminProductFormAdvancedPricingSection.productTierPriceCustGroupSelect('0')}}" userInput="{{tierPriceOnVirtualProduct.customer_group}}" stepKey="seeProductTierPriceCustomerGroupInput"/> -+ <seeInField selector="{{AdminProductFormAdvancedPricingSection.productTierPriceQtyInput('0')}}" userInput="{{tierPriceOnVirtualProduct.qty}}" stepKey="seeProductTierPriceQuantityInput"/> -+ <seeInField selector="{{AdminProductFormAdvancedPricingSection.productTierPriceFixedPriceInput('0')}}" userInput="{{tierPriceOnVirtualProduct.price}}" stepKey="seeProductTierPriceFixedPrice"/> -+ <click selector="{{AdminProductFormAdvancedPricingSection.advancedPricingCloseButton}}" stepKey="clickAdvancedPricingCloseButton"/> -+ <seeInField selector="{{AdminProductFormSection.productTaxClass}}" userInput="{{updateVirtualTierPriceOutOfStock.productTaxClass}}" stepKey="seeProductTaxClass"/> -+ <seeInField selector="{{AdminProductFormSection.productQuantity}}" userInput="{{updateVirtualTierPriceOutOfStock.quantity}}" stepKey="seeProductQuantity"/> -+ <seeInField selector="{{AdminProductFormSection.productStockStatus}}" userInput="{{updateVirtualTierPriceOutOfStock.status}}" stepKey="seeProductStockStatus"/> -+ <click selector="{{AdminProductFormSection.categoriesDropdown}}" stepKey="clickCategoriesDropDownToVerify"/> -+ <grabMultiple selector="{{AdminProductFormSection.selectMultipleCategories}}" stepKey="selectedCategories" /> -+ <assertEquals stepKey="assertSelectedCategories"> -+ <actualResult type="variable">selectedCategories</actualResult> -+ <expectedResult type="array">[$$categoryEntity.name$$]</expectedResult> -+ </assertEquals> -+ <click selector="{{AdminProductFormSection.done}}" stepKey="clickOnDoneOnCategorySelect"/> -+ <seeInField selector="{{AdminProductFormSection.visibility}}" userInput="{{updateVirtualTierPriceOutOfStock.visibility}}" stepKey="seeVisibility"/> -+ <scrollTo selector="{{AdminProductSEOSection.sectionHeader}}" x="0" y="-80" stepKey="scrollToAdminProductSEOSection1"/> -+ <click selector="{{AdminProductSEOSection.sectionHeader}}" stepKey="clickAdminProductSEOSection1"/> -+ <seeInField selector="{{AdminProductSEOSection.urlKeyInput}}" userInput="{{updateVirtualTierPriceOutOfStock.urlKey}}" stepKey="seeUrlKey"/> -+ -+ <!--Verify customer don't see updated virtual product link on category page --> -+ <amOnPage url="{{StorefrontCategoryPage.url($$categoryEntity.name$$)}}" stepKey="openCategoryPage"/> -+ <waitForPageLoad stepKey="waitForCategoryPageLoad"/> -+ <dontSee selector="{{StorefrontCategoryMainSection.productLink}}" userInput="{{updateVirtualTierPriceOutOfStock.name}}" stepKey="dontSeeVirtualProductNameOnCategoryPage"/> -+ -+ <!--Verify customer see updated virtual product with tier price(from above step) on product storefront page --> -+ <amOnPage url="{{StorefrontProductPage.url(updateVirtualTierPriceOutOfStock.urlKey)}}" stepKey="goToProductPage"/> -+ <waitForPageLoad stepKey="waitForStorefrontProductPageLoad"/> -+ <see selector="{{StorefrontProductInfoMainSection.productName}}" userInput="{{updateVirtualTierPriceOutOfStock.name}}" stepKey="seeVirtualProductNameOnStoreFrontPage"/> -+ <see selector="{{StorefrontProductInfoMainSection.productPrice}}" userInput="{{updateVirtualTierPriceOutOfStock.price}}" stepKey="seeVirtualProductPriceOnStoreFrontPage"/> -+ <see selector="{{StorefrontProductInfoMainSection.productSku}}" userInput="{{updateVirtualTierPriceOutOfStock.sku}}" stepKey="seeVirtualProductSku"/> -+ <grabTextFrom selector="{{StorefrontProductInfoMainSection.productStockStatus}}" stepKey="productStockAvailableStatus"/> -+ <assertEquals stepKey="assertStockAvailableOnProductPage"> -+ <expectedResult type="string">{{updateVirtualTierPriceOutOfStock.storefrontStatus}}</expectedResult> -+ <actualResult type="variable">productStockAvailableStatus</actualResult> -+ </assertEquals> -+ <grabTextFrom selector="{{StorefrontProductInfoMainSection.productPrice}}" stepKey="productPriceAmount"/> -+ <assertEquals stepKey="assertOldPriceTextOnProductPage"> -+ <expectedResult type="string">${{updateVirtualTierPriceOutOfStock.price}}</expectedResult> -+ <actualResult type="variable">productPriceAmount</actualResult> -+ </assertEquals> -+ <!-- Verify customer see product tier price on product page --> -+ <grabTextFrom selector="{{StorefrontProductInfoMainSection.tierPriceText}}" stepKey="tierPriceText"/> -+ <assertEquals stepKey="assertTierPriceTextOnProductPage"> -+ <expectedResult type="string">Buy {{tierPriceOnVirtualProduct.qty}} for ${{tierPriceOnVirtualProduct.price}} each and save 51%</expectedResult> -+ <actualResult type="variable">tierPriceText</actualResult> -+ </assertEquals> -+ -+ <!--Verify customer don't see updated virtual product link on magento storefront page and is searchable by sku --> -+ <amOnPage url="{{StorefrontProductPage.url(updateVirtualTierPriceOutOfStock.urlKey)}}" stepKey="goToMagentoStorefrontPage"/> -+ <waitForPageLoad stepKey="waitForStoreFrontProductPageLoad"/> -+ <fillField selector="{{StorefrontQuickSearchResultsSection.searchTextBox}}" userInput="{{updateVirtualTierPriceOutOfStock.sku}}" stepKey="fillVirtualProductName"/> -+ <waitForPageLoad stepKey="waitForSearchTextBox"/> -+ <click selector="{{StorefrontQuickSearchResultsSection.searchTextBoxButton}}" stepKey="clickSearchTextBoxButton"/> -+ <waitForPageLoad stepKey="waitForSearch"/> -+ <dontSee selector="{{StorefrontQuickSearchResultsSection.productLink}}" userInput="{{updateVirtualTierPriceOutOfStock.name}}" stepKey="dontSeeVirtualProductName"/> -+ </test> -+</tests> -diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminVerifyProductOrderTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminVerifyProductOrderTest.xml -new file mode 100644 -index 00000000000..a81c26b6e6e ---- /dev/null -+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminVerifyProductOrderTest.xml -@@ -0,0 +1,31 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> -+ <test name="AdminVerifyProductOrder"> -+ <annotations> -+ <features value="Catalog"/> -+ <stories value="Verify Product Order"/> -+ <title value="Admin should see product types in specified order"/> -+ <description value="Product Type Order should be Simple -> Configurable -> Grouped -> Virtual -> Bundle -> Downloadable -> EE"/> -+ <severity value="CRITICAL"/> -+ <testCaseId value="MC-14207"/> -+ <group value="mtf_migrated"/> -+ <group value="product"/> -+ </annotations> -+ <before> -+ <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin1"/> -+ </before> -+ <after> -+ <actionGroup ref="logout" stepKey="logout"/> -+ </after> -+ <actionGroup ref="GoToProductCatalogPage" stepKey="goToProductCatalogPage"/> -+ <actionGroup ref="VerifyProductTypeOrder" stepKey="verifyProductTypeOrder"/> -+ </test> -+</tests> -diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminVirtualProductSetEditContentTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminVirtualProductSetEditContentTest.xml -new file mode 100644 -index 00000000000..c9932de8080 ---- /dev/null -+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminVirtualProductSetEditContentTest.xml -@@ -0,0 +1,40 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> -+ <test name="AdminVirtualProductSetEditContentTest" extends="AdminSimpleProductSetEditContentTest"> -+ <annotations> -+ <features value="Catalog"/> -+ <stories value="Create/edit virtual product"/> -+ <title value="Admin should be able to set/edit product Content when editing a virtual product"/> -+ <description value="Admin should be able to set/edit product Content when editing a virtual product"/> -+ <severity value="CRITICAL"/> -+ <testCaseId value="MC-3425"/> -+ <group value="Catalog"/> -+ <group value="WYSIWYGDisabled"/> -+ </annotations> -+ <after> -+ <!-- Delete virtual product --> -+ <actionGroup ref="deleteProductUsingProductGrid" stepKey="deleteProduct"> -+ <argument name="product" value="defaultVirtualProduct"/> -+ </actionGroup> -+ </after> -+ -+ <!-- Create product --> -+ <actionGroup ref="goToCreateProductPage" stepKey="goToCreateProduct"> -+ <argument name="product" value="defaultVirtualProduct"/> -+ </actionGroup> -+ <actionGroup ref="fillMainProductFormNoWeight" stepKey="fillProductForm"> -+ <argument name="product" value="defaultVirtualProduct"/> -+ </actionGroup> -+ -+ <!--Checking content storefront--> -+ <amOnPage url="{{defaultVirtualProduct.sku}}.html" stepKey="goToStorefront"/> -+ </test> -+</tests> -diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminVirtualSetEditRelatedProductsTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminVirtualSetEditRelatedProductsTest.xml -new file mode 100644 -index 00000000000..e630545fff8 ---- /dev/null -+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminVirtualSetEditRelatedProductsTest.xml -@@ -0,0 +1,40 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> -+ <test name="AdminVirtualSetEditRelatedProductsTest" extends="AdminSimpleSetEditRelatedProductsTest"> -+ <annotations> -+ <features value="Catalog"/> -+ <stories value="Create/edit virtual product"/> -+ <title value="Admin should be able to set/edit Related Products information when editing a virtual product"/> -+ <description value="Admin should be able to set/edit Related Products information when editing a virtual product"/> -+ <severity value="CRITICAL"/> -+ <testCaseId value="MC-3415"/> -+ <group value="Catalog"/> -+ </annotations> -+ <before></before> -+ <after> -+ <!-- Delete virtual product --> -+ <actionGroup ref="deleteProductUsingProductGrid" stepKey="deleteProduct"> -+ <argument name="product" value="defaultVirtualProduct"/> -+ </actionGroup> -+ </after> -+ -+ <!-- Create product --> -+ <actionGroup ref="goToCreateProductPage" stepKey="goToCreateProduct"> -+ <argument name="product" value="defaultVirtualProduct"/> -+ </actionGroup> -+ <actionGroup ref="fillMainProductFormNoWeight" stepKey="fillProductForm"> -+ <argument name="product" value="defaultVirtualProduct"/> -+ </actionGroup> -+ -+ <!--See related product in storefront--> -+ <amOnPage url="{{defaultVirtualProduct.sku}}.html" stepKey="goToStorefront"/> -+ </test> -+</tests> -diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdvanceCatalogSearchSimpleProductTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdvanceCatalogSearchSimpleProductTest.xml -index 03d919e3291..a4c8b492d9d 100644 ---- a/app/code/Magento/Catalog/Test/Mftf/Test/AdvanceCatalogSearchSimpleProductTest.xml -+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdvanceCatalogSearchSimpleProductTest.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="AdvanceCatalogSearchSimpleProductByNameTest"> - <annotations> - <features value="Catalog"/> -diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdvanceCatalogSearchVirtualProductTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdvanceCatalogSearchVirtualProductTest.xml -new file mode 100644 -index 00000000000..84c3f81ef6d ---- /dev/null -+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdvanceCatalogSearchVirtualProductTest.xml -@@ -0,0 +1,81 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> -+ <test name="AdvanceCatalogSearchVirtualProductByNameTest" extends="AdvanceCatalogSearchSimpleProductByNameTest"> -+ <annotations> -+ <features value="Catalog"/> -+ <stories value="Advanced Catalog Product Search for all product types"/> -+ <title value="Guest customer should be able to advance search virtual product with product name"/> -+ <description value="Guest customer should be able to advance search virtual product with product name"/> -+ <severity value="MAJOR"/> -+ <testCaseId value="MC-137"/> -+ <group value="Catalog"/> -+ </annotations> -+ <before> -+ <createData entity="ApiVirtualProductWithDescription" stepKey="product"/> -+ </before> -+ </test> -+ <test name="AdvanceCatalogSearchVirtualProductBySkuTest" extends="AdvanceCatalogSearchSimpleProductBySkuTest"> -+ <annotations> -+ <features value="Catalog"/> -+ <stories value="Advanced Catalog Product Search for all product types"/> -+ <title value="Guest customer should be able to advance search virtual product with product sku"/> -+ <description value="Guest customer should be able to advance search virtual product with product sku"/> -+ <severity value="MAJOR"/> -+ <testCaseId value="MC-162"/> -+ <group value="Catalog"/> -+ </annotations> -+ <before> -+ <createData entity="ApiVirtualProductWithDescription" stepKey="product"/> -+ </before> -+ </test> -+ <test name="AdvanceCatalogSearchVirtualProductByDescriptionTest" extends="AdvanceCatalogSearchSimpleProductByDescriptionTest"> -+ <annotations> -+ <features value="Catalog"/> -+ <stories value="Advanced Catalog Product Search for all product types"/> -+ <title value="Guest customer should be able to advance search virtual product with product description"/> -+ <description value="Guest customer should be able to advance search virtual product with product description"/> -+ <severity value="MAJOR"/> -+ <testCaseId value="MC-163"/> -+ <group value="Catalog"/> -+ </annotations> -+ <before> -+ <createData entity="ApiVirtualProductWithDescription" stepKey="product"/> -+ </before> -+ </test> -+ <test name="AdvanceCatalogSearchVirtualProductByShortDescriptionTest" extends="AdvanceCatalogSearchSimpleProductByShortDescriptionTest"> -+ <annotations> -+ <features value="Catalog"/> -+ <stories value="Advanced Catalog Product Search for all product types"/> -+ <title value="Guest customer should be able to advance search virtual product with product short description"/> -+ <description value="Guest customer should be able to advance search virtual product with product short description"/> -+ <severity value="MAJOR"/> -+ <testCaseId value="MC-164"/> -+ <group value="Catalog"/> -+ </annotations> -+ <before> -+ <createData entity="ApiVirtualProductWithDescription" stepKey="product"/> -+ </before> -+ </test> -+ <test name="AdvanceCatalogSearchVirtualProductByPriceTest" extends="AdvanceCatalogSearchSimpleProductByPriceTest"> -+ <annotations> -+ <features value="Catalog"/> -+ <stories value="Advanced Catalog Product Search for all product types"/> -+ <title value="Guest customer should be able to advance search virtual product with product price"/> -+ <description value="Guest customer should be able to advance search virtual product with product price"/> -+ <severity value="MAJOR"/> -+ <testCaseId value="MC-165"/> -+ <group value="Catalog"/> -+ </annotations> -+ <before> -+ <createData entity="ApiVirtualProductWithDescription" stepKey="product"/> -+ </before> -+ </test> -+</tests> -diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/CheckCurrentCategoryIsHighlightedAndProductsDisplayed.xml b/app/code/Magento/Catalog/Test/Mftf/Test/CheckCurrentCategoryIsHighlightedAndProductsDisplayed.xml -new file mode 100644 -index 00000000000..d2fe983cb82 ---- /dev/null -+++ b/app/code/Magento/Catalog/Test/Mftf/Test/CheckCurrentCategoryIsHighlightedAndProductsDisplayed.xml -@@ -0,0 +1,72 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> -+ <test name="CheckCurrentCategoryIsHighlightedAndProductsDisplayed"> -+ <annotations> -+ <features value="Catalog"/> -+ <title value="Сheck that current category is highlighted and all products displayed for it"/> -+ <description value="Сheck that current category is highlighted and all products displayed for it"/> -+ <severity value="MAJOR"/> -+ <testCaseId value="MAGETWO-99028"/> -+ <useCaseId value="MAGETWO-98748"/> -+ <group value="Catalog"/> -+ </annotations> -+ <before> -+ <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> -+ <createData entity="SimpleSubCategory" stepKey="category1"/> -+ <createData entity="SimpleSubCategory" stepKey="category2"/> -+ <createData entity="SimpleSubCategory" stepKey="category3"/> -+ <createData entity="SimpleSubCategory" stepKey="category4"/> -+ <createData entity="SimpleProduct" stepKey="product1"> -+ <requiredEntity createDataKey="category1"/> -+ </createData> -+ <createData entity="SimpleProduct" stepKey="product2"> -+ <requiredEntity createDataKey="category1"/> -+ </createData> -+ </before> -+ <after> -+ <deleteData createDataKey="product1" stepKey="deleteProduct1"/> -+ <deleteData createDataKey="product2" stepKey="deleteProduct2"/> -+ <deleteData createDataKey="category1" stepKey="deleteCategory1"/> -+ <deleteData createDataKey="category2" stepKey="deleteCategory2"/> -+ <deleteData createDataKey="category3" stepKey="deleteCategory3"/> -+ <deleteData createDataKey="category4" stepKey="deleteCategory4"/> -+ <actionGroup ref="logout" stepKey="logout"/> -+ </after> -+ <!--Open Storefront home page--> -+ <comment userInput="Open Storefront home page" stepKey="openStorefrontHomePage"/> -+ <amOnPage url="{{StorefrontHomePage.url}}" stepKey="goToStorefrontHomePage"/> -+ <waitForPageLoad stepKey="waitForSimpleProductPage"/> -+ <!--Click on first category--> -+ <comment userInput="Click on first category" stepKey="openFirstCategoryPage"/> -+ <click selector="{{AdminCategorySidebarTreeSection.categoryInTree($$category1.name$$)}}" stepKey="clickCategory1Name"/> -+ <waitForPageLoad stepKey="waitForCategory1Page"/> -+ <!--Check if current category is highlighted and the others are not--> -+ <comment userInput="Check if current category is highlighted and the others are not" stepKey="checkCateg1NameIsHighlighted"/> -+ <grabAttributeFrom selector="{{AdminCategorySidebarTreeSection.categoryHighlighted($$category1.name$$)}}" userInput="class" stepKey="grabCategory1Class"/> -+ <assertContains expectedType="string" expected="active" actual="$grabCategory1Class" stepKey="assertCategory1IsHighlighted"/> -+ <executeJS function="return document.querySelectorAll('{{AdminCategorySidebarTreeSection.categoryNotHighlighted}}').length" stepKey="highlightedAmount"/> -+ <assertEquals expectedType="int" expected="1" actual="$highlightedAmount" stepKey="assertRestCategories1IsNotHighlighted"/> -+ <!--See products in the category page--> -+ <comment userInput="See products in the category page" stepKey="seeProductsInCategoryPage"/> -+ <seeElement selector="{{StorefrontCategoryMainSection.specifiedProductItemInfo($product1.name$)}}" stepKey="seeProduct1InCategoryPage"/> -+ <seeElement selector="{{StorefrontCategoryMainSection.specifiedProductItemInfo($product2.name$)}}" stepKey="seeProduct2InCategoryPage"/> -+ <!--Click on second category--> -+ <comment userInput="Click on second category" stepKey="openSecondCategoryPage"/> -+ <click selector="{{AdminCategorySidebarTreeSection.categoryInTree($$category2.name$$)}}" stepKey="clickCategory2Name"/> -+ <waitForPageLoad stepKey="waitForCategory2Page"/> -+ <!--Check if current category is highlighted and the others are not--> -+ <comment userInput="Check if current category is highlighted and the others are not" stepKey="checkCateg2NameIsHighlighted"/> -+ <grabAttributeFrom selector="{{AdminCategorySidebarTreeSection.categoryHighlighted($$category2.name$$)}}" userInput="class" stepKey="grabCategory2Class"/> -+ <assertContains expectedType="string" expected="active" actual="$grabCategory2Class" stepKey="assertCategory2IsHighlighted"/> -+ <executeJS function="return document.querySelectorAll('{{AdminCategorySidebarTreeSection.categoryNotHighlighted}}').length" stepKey="highlightedAmount2"/> -+ <assertEquals expectedType="int" expected="1" actual="$highlightedAmount2" stepKey="assertRestCategories1IsNotHighlighted2"/> -+ </test> -+</tests> -diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/CheckTierPricingOfProductsTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/CheckTierPricingOfProductsTest.xml -new file mode 100644 -index 00000000000..cee40241185 ---- /dev/null -+++ b/app/code/Magento/Catalog/Test/Mftf/Test/CheckTierPricingOfProductsTest.xml -@@ -0,0 +1,338 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> -+ <test name="CheckTierPricingOfProductsTest"> -+ <annotations> -+ <features value="Shopping Cart"/> -+ <stories value="MAGETWO-91697 - [Magento Cloud] 'Tier Pricing' of Products changes to 'Price' (without discount) after Updated Items and Quantities in the Order of B2B Store View."/> -+ <title value="Checking 'Tier Pricing' of Products and 'Price' (without discount) in the Order of B2B Store View"/> -+ <description value="Checking 'Tier Pricing' of Products and 'Price' (without discount) in the Order of B2B Store View"/> -+ <testCaseId value="MAGETWO-94111"/> -+ <severity value="CRITICAL"/> -+ <group value="Shopping Cart"/> -+ </annotations> -+ -+ <before> -+ <createData entity="_defaultCategory" stepKey="category"/> -+ <createData entity="SimpleProduct" stepKey="product1"> -+ <requiredEntity createDataKey="category"/> -+ </createData> -+ <createData entity="SimpleProduct" stepKey="product2"> -+ <requiredEntity createDataKey="category"/> -+ </createData> -+ <createData entity="SimpleProduct" stepKey="product3"> -+ <requiredEntity createDataKey="category"/> -+ </createData> -+ <createData entity="SimpleProduct" stepKey="product4"> -+ <requiredEntity createDataKey="category"/> -+ </createData> -+ <createData entity="Simple_US_Customer" stepKey="customer"/> -+ <!--Login as admin--> -+ <actionGroup ref="LoginAsAdmin" stepKey="LoginAsAdmin"/> -+ </before> -+ -+ <!--Create website, Sore adn Store View--> -+ <actionGroup ref="AdminCreateWebsiteActionGroup" stepKey="AdminCreateWebsite"> -+ <argument name="newWebsiteName" value="secondWebsite"/> -+ <argument name="websiteCode" value="second_website"/> -+ </actionGroup> -+ <actionGroup ref="AdminCreateNewStoreGroupActionGroup" stepKey="AdminCreateStore"> -+ <argument name="website" value="secondWebsite"/> -+ <argument name="storeGroupName" value="secondStore"/> -+ <argument name="storeGroupCode" value="second_store"/> -+ </actionGroup> -+ <actionGroup ref="AdminCreateStoreViewActionGroup" stepKey="AdminCreateStoreView"> -+ <argument name="StoreGroup" value="customStoreTierPrice"/> -+ <argument name="customStore" value="customStoreView"/> -+ </actionGroup> -+ <!--Set Configuration--> -+ <createData entity="CatalogPriceScopeWebsite" stepKey="paymentMethodsSettingConfig"/> -+ <!--Set advanced pricing for all 4 products--> -+ <actionGroup ref="SearchForProductOnBackendActionGroup" stepKey="searchForSimpleProduct1"> -+ <argument name="product" value="$$product1$$"/> -+ </actionGroup> -+ <actionGroup ref="OpenEditProductOnBackendActionGroup" stepKey="openEditProduct1"> -+ <argument name="product" value="$$product1$$"/> -+ </actionGroup> -+ <actionGroup ref="ProductSetWebsite" stepKey="ProductSetWebsite"> -+ <argument name="website" value="secondWebsite"/> -+ </actionGroup> -+ <actionGroup ref="ProductSetAdvancedPricing" stepKey="ProductSetAdvancedPricing1"> -+ <argument name="website" value="secondWebsite"/> -+ </actionGroup> -+ -+ <actionGroup ref="SearchForProductOnBackendActionGroup" stepKey="searchForSimpleProduct2"> -+ <argument name="product" value="$$product2$$"/> -+ </actionGroup> -+ <actionGroup ref="OpenEditProductOnBackendActionGroup" stepKey="openEditProduct2"> -+ <argument name="product" value="$$product2$$"/> -+ </actionGroup> -+ <actionGroup ref="ProductSetWebsite" stepKey="ProductSetWebsite2"> -+ <argument name="website" value="secondWebsite"/> -+ </actionGroup> -+ <actionGroup ref="ProductSetAdvancedPricing" stepKey="ProductSetAdvancedPricing2"> -+ <argument name="website" value="secondWebsite"/> -+ </actionGroup> -+ -+ <actionGroup ref="SearchForProductOnBackendActionGroup" stepKey="searchForSimpleProduct3"> -+ <argument name="product" value="$$product3$$"/> -+ </actionGroup> -+ <actionGroup ref="OpenEditProductOnBackendActionGroup" stepKey="openEditProduct3"> -+ <argument name="product" value="$$product3$$"/> -+ </actionGroup> -+ <actionGroup ref="ProductSetWebsite" stepKey="ProductSetWebsite3"> -+ <argument name="website" value="secondWebsite"/> -+ </actionGroup> -+ <actionGroup ref="ProductSetAdvancedPricing" stepKey="ProductSetAdvancedPricing3"> -+ <argument name="website" value="secondWebsite"/> -+ </actionGroup> -+ -+ <actionGroup ref="SearchForProductOnBackendActionGroup" stepKey="searchForSimpleProduct4"> -+ <argument name="product" value="$$product4$$"/> -+ </actionGroup> -+ <actionGroup ref="OpenEditProductOnBackendActionGroup" stepKey="openEditProduct4"> -+ <argument name="product" value="$$product4$$"/> -+ </actionGroup> -+ <actionGroup ref="ProductSetWebsite" stepKey="ProductSetWebsite4"> -+ <argument name="website" value="secondWebsite"/> -+ </actionGroup> -+ <actionGroup ref="ProductSetAdvancedPricing" stepKey="ProductSetAdvancedPricing4"> -+ <argument name="website" value="secondWebsite"/> -+ </actionGroup> -+ <actionGroup ref="ClearProductsFilterActionGroup" stepKey="ClearProductsFilterActionGroup"/> -+ -+ <!--Flush cache--> -+ <magentoCLI command="cache:flush" stepKey="cleanCache"/> -+ -+ -+ <!--Edit customer info--> -+ <actionGroup ref="OpenEditCustomerFromAdminActionGroup" stepKey="OpenEditCustomerFrom"> -+ <argument name="customer" value="$$customer$$"/> -+ </actionGroup> -+ <click selector="{{AdminCustomerAccountInformationSection.accountInformationButton}}" stepKey="ClickOnAccountInformationSection"/> -+ <waitForPageLoad stepKey="waitForPageOpened1"/> -+ <selectOption selector="{{AdminCustomerAccountInformationSection.group}}" userInput="Retailer" stepKey="Group"/> -+ <selectOption selector="{{AdminCustomerAccountInformationSection.storeView}}" userInput="secondStoreView" stepKey="clickToSelectStore"/> -+ <click selector="{{AdminCustomerAccountInformationSection.saveCustomer}}" stepKey="save"/> -+ <waitForPageLoad stepKey="waitForCustomersPage"/> -+ <see userInput="You saved the customer." stepKey="CustomerIsSaved"/> -+ -+ <amOnPage url="{{AdminCustomerPage.url}}" stepKey="navigateToCustomers"/> -+ <waitForPageLoad stepKey="waitForPageLoad1" /> -+ <click selector="{{AdminCustomerFiltersSection.clearAll}}" stepKey="ClearFilters"/> -+ <waitForPageLoad stepKey="waitForFiltersClear"/> -+ -+ <!--Create Cart Price Rule--> -+ <amOnPage url="{{AdminCartPriceRulesPage.url}}" stepKey="amOnCartPriceList"/> -+ <waitForPageLoad stepKey="waitForPriceList"/> -+ <click selector="{{AdminCartPriceRulesSection.addNewRuleButton}}" stepKey="clickAddNewRule"/> -+ <waitForPageLoad stepKey="waitForPageDiscountPageIsLoaded"/> -+ <fillField selector="{{AdminCartPriceRulesFormSection.ruleName}}" userInput="ship" stepKey="fillRuleName"/> -+ <selectOption selector="{{AdminCartPriceRulesFormSection.websites}}" userInput="secondWebsite" stepKey="selectWebsites"/> -+ <selectOption selector="{{AdminCartPriceRulesFormSection.customerGroups}}" userInput="Retailer" stepKey="selectCustomerGroup"/> -+ <selectOption selector="{{AdminCartPriceRulesFormSection.coupon}}" userInput="Specific Coupon" stepKey="selectCouponType"/> -+ <fillField selector="{{AdminCartPriceRulesFormSection.couponCode}}" userInput="ship" stepKey="setCode"/> -+ <fillField selector="{{AdminCartPriceRulesFormSection.userPerCustomer}}" userInput="0" stepKey="setUserPerCustomer"/> -+ <fillField selector="{{AdminCartPriceRulesFormSection.userPerCoupon}}" userInput="0" stepKey="setUserPerCoupon"/> -+ <fillField selector="{{AdminCartPriceRulesFormSection.priority}}" userInput="0" stepKey="setPriority"/> -+ <click selector="{{AdminCartPriceRulesFormSection.actionsHeader}}" stepKey="clickToExpandActions"/> -+ <selectOption selector="{{AdminCartPriceRulesFormSection.freeShipping}}" userInput="For shipment with matching items" stepKey="selectFreeShippingType"/> -+ <click selector="{{AdminCartPriceRulesFormSection.saveAndContinue}}" stepKey="clickSaveAndContinueButton"/> -+ <waitForPageLoad stepKey="waitForCartPriceRuleSaved"/> -+ <see userInput="You saved the rule." stepKey="RuleSaved"/> -+ -+ <!--Create new order--> -+ <actionGroup ref="navigateToNewOrderPageExistingCustomer" stepKey="CreateNewOrder"> -+ <argument name="customer" value="Simple_US_Customer"/> -+ <argument name="storeView" value="customStoreView"/> -+ </actionGroup> -+ -+ <click selector="{{OrdersGridSection.addProducts}}" stepKey="clickToAddProduct"/> -+ <waitForPageLoad stepKey="waitForProductsOpened"/> -+ <!--TEST CASE #1--> -+ <!--Add 3 products to order with specified quantity--> -+ <click selector="{{OrdersGridSection.selectProduct($$product1.name$$)}}" stepKey="selectProduct1"/> -+ <fillField selector="{{OrdersGridSection.setQuantity($$product1.name$$)}}" userInput="10" stepKey="AddProductQuantity1"/> -+ -+ <click selector="{{OrdersGridSection.selectProduct($$product2.name$$)}}" stepKey="selectProduct2"/> -+ <fillField selector="{{OrdersGridSection.setQuantity($$product2.name$$)}}" userInput="10" stepKey="AddProductQuantity2"/> -+ -+ <click selector="{{OrdersGridSection.selectProduct($$product3.name$$)}}" stepKey="selectProduct3"/> -+ <fillField selector="{{OrdersGridSection.setQuantity($$product3.name$$)}}" userInput="10" stepKey="AddProductQuantity3"/> -+ <click stepKey="addProductsToOrder" selector="{{OrdersGridSection.addProductsToOrder}}"/> -+ <waitForLoadingMaskToDisappear stepKey="wait6"/> -+ <!--Verify tier price values--> -+ <grabTextFrom selector="{{OrdersGridSection.productPrice($$product1.name$$)}}" stepKey="checkProductPrice1"/> -+ <assertEquals stepKey="verifyPrice1"> -+ <expectedResult type="string">{{testDataTierPrice.goldenPrice1}}</expectedResult> -+ <actualResult type="variable">$checkProductPrice1</actualResult> -+ </assertEquals> -+ -+ <grabTextFrom selector="{{OrdersGridSection.productPrice($$product2.name$$)}}" stepKey="checkProductPrice2"/> -+ <assertEquals stepKey="verifyPrice2"> -+ <expectedResult type="string">{{testDataTierPrice.goldenPrice1}}</expectedResult> -+ <actualResult type="variable">$checkProductPrice2</actualResult> -+ </assertEquals> -+ -+ <grabTextFrom selector="{{OrdersGridSection.productPrice($$product3.name$$)}}" stepKey="checkProductPrice3"/> -+ <assertEquals stepKey="verifyPrice3"> -+ <expectedResult type="string">{{testDataTierPrice.goldenPrice1}}</expectedResult> -+ <actualResult type="variable">$checkProductPrice3</actualResult> -+ </assertEquals> -+ -+ <!--Edit order and verify values--> -+ <waitForPageLoad stepKey="waitForPageLoaded2"/> -+ <click selector="{{OrdersGridSection.customPrice($$product1.name$$)}}" stepKey="ClickOnCustomPrice"/> -+ <fillField selector="{{OrdersGridSection.customQuantity($$product1.name$$)}}" userInput="5" stepKey="ClickOnQuantity"/> -+ <waitForLoadingMaskToDisappear stepKey="wait1"/> -+ <click selector="{{OrdersGridSection.update}}" stepKey="ClickToUpdate"/> -+ <waitForLoadingMaskToDisappear stepKey="wait2"/> -+ <grabTextFrom selector="{{OrdersGridSection.productPrice($$product1.name$$)}}" stepKey="checkProductPrice4"/> -+ <assertEquals stepKey="verifyPrice4"> -+ <expectedResult type="string">{{testDataTierPrice.goldenPrice2}}</expectedResult> -+ <actualResult type="variable">$checkProductPrice4</actualResult> -+ </assertEquals> -+ -+ <grabTextFrom selector="{{OrdersGridSection.productPrice($$product2.name$$)}}" stepKey="checkProductPrice5"/> -+ <assertEquals stepKey="verifyPrice5"> -+ <expectedResult type="string">{{testDataTierPrice.goldenPrice1}}</expectedResult> -+ <actualResult type="variable">$checkProductPrice5</actualResult> -+ </assertEquals> -+ <grabTextFrom selector="{{OrdersGridSection.productPrice($$product3.name$$)}}" stepKey="checkProductPrice6"/> -+ <assertEquals stepKey="verifyPrice6"> -+ <expectedResult type="string">{{testDataTierPrice.goldenPrice1}}</expectedResult> -+ <actualResult type="variable">$checkProductPrice3</actualResult> -+ </assertEquals> -+ -+ <!--Remove products from order--> -+ <selectOption selector="{{OrdersGridSection.removeItems($$product1.name$$)}}" userInput="Remove" stepKey="clickToRemove1"/> -+ <selectOption selector="{{OrdersGridSection.removeItems($$product2.name$$)}}" userInput="Remove" stepKey="clickToRemove2"/> -+ <selectOption selector="{{OrdersGridSection.removeItems($$product3.name$$)}}" userInput="Remove" stepKey="clickToRemove3"/> -+ <waitForLoadingMaskToDisappear stepKey="wait3"/> -+ <click selector="{{OrdersGridSection.update}}" stepKey="ClickToUpdate1"/> -+ <waitForPageLoad stepKey="WaitProductsDeleted"/> -+ -+ <!--TEST CASE #2--> -+ <!--Add 3 products to order with specified quantity--> -+ <scrollToTopOfPage stepKey="scrollToTopOfPage"/> -+ <click stepKey="clickToAddProduct1" selector="{{OrdersGridSection.addProducts}}"/> -+ <click selector="{{OrdersGridSection.selectProduct($$product1.name$$)}}" stepKey="selectProduct5"/> -+ <fillField selector="{{OrdersGridSection.setQuantity($$product1.name$$)}}" userInput="10" stepKey="AddProductQuantity5"/> -+ -+ <click selector="{{OrdersGridSection.selectProduct($$product2.name$$)}}" stepKey="selectProduct6"/> -+ <fillField selector="{{OrdersGridSection.setQuantity($$product2.name$$)}}" userInput="10" stepKey="AddProductQuantity6"/> -+ -+ <click selector="{{OrdersGridSection.selectProduct($$product3.name$$)}}" stepKey="selectProduct7"/> -+ <fillField selector="{{OrdersGridSection.setQuantity($$product3.name$$)}}" userInput="10" stepKey="AddProductQuantity7"/> -+ <click stepKey="addProductsToOrder1" selector="{{OrdersGridSection.addProductsToOrder}}"/> -+ <waitForLoadingMaskToDisappear stepKey="wait7"/> -+ <!--Verify tier price values--> -+ <grabTextFrom selector="{{OrdersGridSection.productPrice($$product1.name$$)}}" stepKey="checkProductPrice7"/> -+ <assertEquals stepKey="verifyPrice7"> -+ <expectedResult type="string">{{testDataTierPrice.goldenPrice1}}</expectedResult> -+ <actualResult type="variable">$checkProductPrice7</actualResult> -+ </assertEquals> -+ -+ <grabTextFrom selector="{{OrdersGridSection.productPrice($$product2.name$$)}}" stepKey="checkProductPrice8"/> -+ <assertEquals stepKey="verifyPrice8"> -+ <expectedResult type="string">{{testDataTierPrice.goldenPrice1}}</expectedResult> -+ <actualResult type="variable">$checkProductPrice8</actualResult> -+ </assertEquals> -+ -+ <grabTextFrom selector="{{OrdersGridSection.productPrice($$product3.name$$)}}" stepKey="checkProductPrice9"/> -+ <assertEquals stepKey="verifyPrice9"> -+ <expectedResult type="string">{{testDataTierPrice.goldenPrice1}}</expectedResult> -+ <actualResult type="variable">$checkProductPrice9</actualResult> -+ </assertEquals> -+ -+ <!--Add one more product and verify values--> -+ <waitForPageLoad stepKey="waitForPgeLoaded3"/> -+ <click selector="{{OrdersGridSection.addProducts}}" stepKey="clickToAddProduct2"/> -+ <waitForLoadingMaskToDisappear stepKey="wait8"/> -+ <click selector="{{OrdersGridSection.selectProduct($$product4.name$$)}}" stepKey="selectProduct8"/> -+ <fillField selector="{{OrdersGridSection.setQuantity($$product4.name$$)}}" userInput="10" stepKey="AddProductQuantity9"/> -+ <click selector="{{OrdersGridSection.addProductsToOrder}}" stepKey="addProductsToOrder2"/> -+ <waitForLoadingMaskToDisappear stepKey="wait9"/> -+ <grabTextFrom selector="{{OrdersGridSection.productPrice($$product4.name$$)}}" stepKey="checkProductPrice10"/> -+ <assertEquals stepKey="verifyPrice10"> -+ <expectedResult type="string">{{testDataTierPrice.goldenPrice1}}</expectedResult> -+ <actualResult type="variable">$checkProductPrice10</actualResult> -+ </assertEquals> -+ -+ <grabTextFrom selector="{{OrdersGridSection.productPrice($$product1.name$$)}}" stepKey="checkProductPrice11"/> -+ <assertEquals stepKey="verifyPrice11"> -+ <expectedResult type="string">{{testDataTierPrice.goldenPrice1}}</expectedResult> -+ <actualResult type="variable">$checkProductPrice11</actualResult> -+ </assertEquals> -+ -+ <grabTextFrom selector="{{OrdersGridSection.productPrice($$product2.name$$)}}" stepKey="checkProductPrice12"/> -+ <assertEquals stepKey="verifyPrice12"> -+ <expectedResult type="string">{{testDataTierPrice.goldenPrice1}}</expectedResult> -+ <actualResult type="variable">$checkProductPrice12</actualResult> -+ </assertEquals> -+ -+ <grabTextFrom selector="{{OrdersGridSection.productPrice($$product3.name$$)}}" stepKey="checkProductPrice13"/> -+ <assertEquals stepKey="verifyPrice13"> -+ <expectedResult type="string">{{testDataTierPrice.goldenPrice1}}</expectedResult> -+ <actualResult type="variable">$checkProductPrice13</actualResult> -+ </assertEquals> -+ -+ <selectOption selector="{{OrdersGridSection.removeItems($$product1.name$$)}}" userInput="Remove" stepKey="clickToRemove4"/> -+ <selectOption selector="{{OrdersGridSection.removeItems($$product2.name$$)}}" userInput="Remove" stepKey="clickToRemove5"/> -+ <selectOption selector="{{OrdersGridSection.removeItems($$product3.name$$)}}" userInput="Remove" stepKey="clickToRemove6"/> -+ <waitForLoadingMaskToDisappear stepKey="wait4"/> -+ <click selector="{{OrdersGridSection.update}}" stepKey="ClickToUpdate2"/> -+ <waitForLoadingMaskToDisappear stepKey="wait10"/> -+ -+ <!--TEST CASE #3--> -+ <waitForPageLoad stepKey="WaitProductsDeleted1"/> -+ <scrollToTopOfPage stepKey="scrollToTopOfPage1"/> -+ <click selector="{{OrdersGridSection.addProducts}}" stepKey="clickToAddProduct4" /> -+ <click selector="{{OrdersGridSection.selectProduct($$product1.name$$)}}" stepKey="selectProduct9"/> -+ <fillField selector="{{OrdersGridSection.setQuantity($$product1.name$$)}}" userInput="10" stepKey="AddProductQuantity10"/> -+ <click selector="{{OrdersGridSection.addProductsToOrder}}" stepKey="addProductsToOrder3"/> -+ <waitForLoadingMaskToDisappear stepKey="wait11"/> -+ <fillField selector="{{OrdersGridSection.applyCoupon}}" userInput="ship" stepKey="AddCouponCode"/> -+ <waitForLoadingMaskToDisappear stepKey="wait5"/> -+ <click selector="{{OrdersGridSection.update}}" stepKey="ClickToUpdate3"/> -+ <waitForLoadingMaskToDisappear stepKey="wait12"/> -+ <grabTextFrom selector="{{OrdersGridSection.productPrice($$product1.name$$)}}" stepKey="checkProductPrice14"/> -+ <grabTextFrom selector="{{OrdersGridSection.productPrice($$product4.name$$)}}" stepKey="checkProductPrice15"/> -+ <assertEquals stepKey="verifyPrice14"> -+ <expectedResult type="string">{{testDataTierPrice.goldenPrice1}}</expectedResult> -+ <actualResult type="variable">$checkProductPrice14</actualResult> -+ </assertEquals> -+ <assertEquals stepKey="verifyPrice15"> -+ <expectedResult type="string">{{testDataTierPrice.goldenPrice1}}</expectedResult> -+ <actualResult type="variable">$checkProductPrice15</actualResult> -+ </assertEquals> -+ -+ <after> -+ <deleteData createDataKey="product1" stepKey="deleteProduct1"/> -+ <deleteData createDataKey="product2" stepKey="deleteProduct2"/> -+ <deleteData createDataKey="product3" stepKey="deleteProduct3"/> -+ <deleteData createDataKey="product4" stepKey="deleteProduct4"/> -+ <deleteData createDataKey="category" stepKey="deleteCategory"/> -+ <deleteData createDataKey="customer" stepKey="deleteCustomer"/> -+ <createData entity="DefaultConfigCatalogPrice" stepKey="defaultConfigCatalogPrice"/> -+ <actionGroup ref="DeleteCartPriceRuleByName" stepKey="cleanUpRule"> -+ <argument name="ruleName" value="ship"/> -+ </actionGroup> -+ <actionGroup ref="AdminDeleteWebsiteActionGroup" stepKey="DeleteWebsite"> -+ <argument name="websiteName" value="secondWebsite"/> -+ </actionGroup> -+ <actionGroup ref="logout" stepKey="logout"/> -+ -+ <!--Do reindex and flush cache--> -+ <magentoCLI command="indexer:reindex" stepKey="reindex"/> -+ <magentoCLI command="cache:flush" stepKey="flushCache"/> -+ </after> -+ </test> -+</tests> -diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/ConfigurableOptionTextInputLengthValidationHint.xml b/app/code/Magento/Catalog/Test/Mftf/Test/ConfigurableOptionTextInputLengthValidationHint.xml -index c97d2c45be9..899f3e61b5b 100644 ---- a/app/code/Magento/Catalog/Test/Mftf/Test/ConfigurableOptionTextInputLengthValidationHint.xml -+++ b/app/code/Magento/Catalog/Test/Mftf/Test/ConfigurableOptionTextInputLengthValidationHint.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="ConfigurableOptionTextinputLengthValidationHintTest"> - <annotations> - <features value="Product Customizable Option"/> -diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/CreateProductAttributeEntityTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/CreateProductAttributeEntityTest.xml -new file mode 100644 -index 00000000000..d895993217e ---- /dev/null -+++ b/app/code/Magento/Catalog/Test/Mftf/Test/CreateProductAttributeEntityTest.xml -@@ -0,0 +1,425 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> -+ <test name="CreateProductAttributeEntityTextFieldTest"> -+ <annotations> -+ <features value="Catalog"/> -+ <stories value="Create Product Attributes"/> -+ <title value="Admin should be able to create a TextField product attribute"/> -+ <description value="Admin should be able to create a TextField product attribute"/> -+ <severity value="CRITICAL"/> -+ <testCaseId value="MC-10894"/> -+ <group value="Catalog"/> -+ <group value="mtf_migrated"/> -+ </annotations> -+ -+ <before> -+ <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> -+ </before> -+ <after> -+ <actionGroup ref="navigateToEditProductAttribute" stepKey="goToEditPage"> -+ <argument name="ProductAttribute" value="{{textProductAttribute.attribute_code}}"/> -+ </actionGroup> -+ <click stepKey="clickDelete" selector="{{AttributePropertiesSection.DeleteAttribute}}"/> -+ <click stepKey="clickOk" selector="{{AttributeDeleteModalSection.confirm}}"/> -+ <waitForPageLoad stepKey="waitForDeletion"/> -+ <actionGroup ref="logout" stepKey="logout"/> -+ </after> -+ -+ <!--Navigate to Stores > Attributes > Product.--> -+ <amOnPage url="{{AdminProductAttributeGridPage.url}}" stepKey="goToProductAttributes"/> -+ -+ <!--Create new Product Attribute as TextField, with code and default value.--> -+ <actionGroup ref="createProductAttributeWithTextField" stepKey="createAttribute"> -+ <argument name="attribute" value="textProductAttribute"/> -+ </actionGroup> -+ -+ <!--Navigate to Product Attribute.--> -+ <actionGroup ref="navigateToEditProductAttribute" stepKey="goToEditPage"> -+ <argument name="ProductAttribute" value="{{textProductAttribute.attribute_code}}"/> -+ </actionGroup> -+ -+ <!--Perform appropriate assertions against textProductAttribute entity--> -+ <seeInField stepKey="assertLabel" selector="{{AttributePropertiesSection.DefaultLabel}}" userInput="{{textProductAttribute.attribute_code}}"/> -+ <seeOptionIsSelected stepKey="assertInputType" selector="{{AttributePropertiesSection.InputType}}" userInput="{{textProductAttribute.frontend_input}}"/> -+ <seeOptionIsSelected stepKey="assertRequired" selector="{{AttributePropertiesSection.ValueRequired}}" userInput="{{textProductAttribute.is_required_admin}}"/> -+ <seeInField stepKey="assertAttrCode" selector="{{AdvancedAttributePropertiesSection.AttributeCode}}" userInput="{{textProductAttribute.attribute_code}}"/> -+ <seeInField stepKey="assertDefaultValue" selector="{{AdvancedAttributePropertiesSection.DefaultValueText}}" userInput="{{textProductAttribute.default_value}}"/> -+ -+ <!--Go to New Product page, add Attribute and check values--> -+ <amOnPage url="{{AdminProductCreatePage.url('4', 'simple')}}" stepKey="goToCreateSimpleProductPage"/> -+ <actionGroup ref="addProductAttributeInProductModal" stepKey="addAttributeToProduct"> -+ <argument name="attributeCode" value="{{textProductAttribute.attribute_code}}"/> -+ </actionGroup> -+ <click stepKey="openAttributes" selector="{{AdminProductAttributesSection.sectionHeader}}"/> -+ <waitForElementVisible selector="{{AdminProductAttributesSection.attributeTextInputByCode(textProductAttribute.attribute_code)}}" stepKey="waitforLabel"/> -+ <seeInField stepKey="checkDefaultValue" selector="{{AdminProductAttributesSection.attributeTextInputByCode(textProductAttribute.attribute_code)}}" userInput="{{textProductAttribute.default_value}}"/> -+ <see stepKey="checkLabel" selector="{{AdminProductAttributesSection.attributeLabelByCode(textProductAttribute.attribute_code)}}" userInput="{{textProductAttribute.attribute_code}}"/> -+ </test> -+ -+ <test name="CreateProductAttributeEntityDateTest"> -+ <annotations> -+ <features value="Catalog"/> -+ <stories value="Create Product Attributes"/> -+ <title value="Admin should be able to create a Date product attribute"/> -+ <description value="Admin should be able to create a Date product attribute"/> -+ <severity value="CRITICAL"/> -+ <testCaseId value="MC-10895"/> -+ <group value="Catalog"/> -+ <skip> -+ <issueId value="MC-13817"/> -+ </skip> -+ <group value="mtf_migrated"/> -+ </annotations> -+ -+ <before> -+ <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> -+ </before> -+ <after> -+ <actionGroup ref="navigateToEditProductAttribute" stepKey="goToEditPage"> -+ <argument name="ProductAttribute" value="{{dateProductAttribute.attribute_code}}"/> -+ </actionGroup> -+ <click stepKey="clickDelete" selector="{{AttributePropertiesSection.DeleteAttribute}}"/> -+ <click stepKey="clickOk" selector="{{AttributeDeleteModalSection.confirm}}"/> -+ <waitForPageLoad stepKey="waitForDeletion"/> -+ <actionGroup ref="logout" stepKey="logout"/> -+ </after> -+ -+ <!--Generate date for use as default value, needs to be MM/d/YYYY --> -+ <generateDate date="now" format="m/j/Y" stepKey="generateDefaultDate"/> -+ -+ <!--Navigate to Stores > Attributes > Product.--> -+ <amOnPage url="{{AdminProductAttributeGridPage.url}}" stepKey="goToProductAttributes"/> -+ -+ <!--Create new Product Attribute as TextField, with code and default value.--> -+ <actionGroup ref="createProductAttributeWithDateField" stepKey="createAttribute"> -+ <argument name="attribute" value="dateProductAttribute"/> -+ <argument name="date" value="{$generateDefaultDate}"/> -+ </actionGroup> -+ -+ <!--Navigate to Product Attribute.--> -+ <actionGroup ref="navigateToEditProductAttribute" stepKey="goToEditPage"> -+ <argument name="ProductAttribute" value="{{dateProductAttribute.attribute_code}}"/> -+ </actionGroup> -+ -+ <!--Perform appropriate assertions against textProductAttribute entity--> -+ <seeInField stepKey="assertLabel" selector="{{AttributePropertiesSection.DefaultLabel}}" userInput="{{dateProductAttribute.attribute_code}}"/> -+ <seeOptionIsSelected stepKey="assertInputType" selector="{{AttributePropertiesSection.InputType}}" userInput="{{dateProductAttribute.frontend_input}}"/> -+ <seeOptionIsSelected stepKey="assertRequired" selector="{{AttributePropertiesSection.ValueRequired}}" userInput="{{dateProductAttribute.is_required_admin}}"/> -+ <seeInField stepKey="assertAttrCode" selector="{{AdvancedAttributePropertiesSection.AttributeCode}}" userInput="{{dateProductAttribute.attribute_code}}"/> -+ <seeInField stepKey="assertDefaultValue" selector="{{AdvancedAttributePropertiesSection.DefaultValueDate}}" userInput="{$generateDefaultDate}"/> -+ -+ <!--Go to New Product page, add Attribute and check values--> -+ <amOnPage url="{{AdminProductCreatePage.url('4', 'simple')}}" stepKey="goToCreateSimpleProductPage"/> -+ <actionGroup ref="addProductAttributeInProductModal" stepKey="addAttributeToProduct"> -+ <argument name="attributeCode" value="{{dateProductAttribute.attribute_code}}"/> -+ </actionGroup> -+ <click stepKey="openAttributes" selector="{{AdminProductAttributesSection.sectionHeader}}"/> -+ <waitForElementVisible selector="{{AdminProductAttributesSection.attributeTextInputByCode(dateProductAttribute.attribute_code)}}" stepKey="waitforLabel"/> -+ <seeInField stepKey="checkDefaultValue" selector="{{AdminProductAttributesSection.attributeTextInputByCode(dateProductAttribute.attribute_code)}}" userInput="{$generateDefaultDate}"/> -+ <see stepKey="checkLabel" selector="{{AdminProductAttributesSection.attributeLabelByCode(dateProductAttribute.attribute_code)}}" userInput="{{dateProductAttribute.attribute_code}}"/> -+ </test> -+ -+ <test name="CreateProductAttributeEntityPriceTest"> -+ <annotations> -+ <features value="Catalog"/> -+ <stories value="Create Product Attributes"/> -+ <title value="Admin should be able to create a Price product attribute"/> -+ <description value="Admin should be able to create a Price product attribute"/> -+ <severity value="CRITICAL"/> -+ <testCaseId value="MC-10897"/> -+ <group value="Catalog"/> -+ <group value="mtf_migrated"/> -+ </annotations> -+ -+ <before> -+ <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> -+ </before> -+ <after> -+ <actionGroup ref="navigateToEditProductAttribute" stepKey="goToEditPage"> -+ <argument name="ProductAttribute" value="{{priceProductAttribute.attribute_code}}"/> -+ </actionGroup> -+ <click stepKey="clickDelete" selector="{{AttributePropertiesSection.DeleteAttribute}}"/> -+ <click stepKey="clickOk" selector="{{AttributeDeleteModalSection.confirm}}"/> -+ <waitForPageLoad stepKey="waitForDeletion"/> -+ <actionGroup ref="logout" stepKey="logout"/> -+ </after> -+ -+ <!--Navigate to Stores > Attributes > Product.--> -+ <amOnPage url="{{AdminProductAttributeGridPage.url}}" stepKey="goToProductAttributes"/> -+ -+ <!--Create new Product Attribute with Price--> -+ <actionGroup ref="createProductAttribute" stepKey="createAttribute"> -+ <argument name="attribute" value="priceProductAttribute"/> -+ </actionGroup> -+ -+ <!--Navigate to Product Attribute.--> -+ <actionGroup ref="navigateToEditProductAttribute" stepKey="goToEditPage"> -+ <argument name="ProductAttribute" value="{{priceProductAttribute.attribute_code}}"/> -+ </actionGroup> -+ -+ <!--Perform appropriate assertions against priceProductAttribute entity--> -+ <seeInField stepKey="assertLabel" selector="{{AttributePropertiesSection.DefaultLabel}}" userInput="{{priceProductAttribute.attribute_code}}"/> -+ <seeOptionIsSelected stepKey="assertInputType" selector="{{AttributePropertiesSection.InputType}}" userInput="{{priceProductAttribute.frontend_input}}"/> -+ <seeOptionIsSelected stepKey="assertRequired" selector="{{AttributePropertiesSection.ValueRequired}}" userInput="{{priceProductAttribute.is_required_admin}}"/> -+ <seeInField stepKey="assertAttrCode" selector="{{AdvancedAttributePropertiesSection.AttributeCode}}" userInput="{{priceProductAttribute.attribute_code}}"/> -+ -+ <!--Go to New Product page, add Attribute and check values--> -+ <amOnPage url="{{AdminProductCreatePage.url('4', 'simple')}}" stepKey="goToCreateSimpleProductPage"/> -+ <actionGroup ref="addProductAttributeInProductModal" stepKey="addAttributeToProduct"> -+ <argument name="attributeCode" value="{{priceProductAttribute.attribute_code}}"/> -+ </actionGroup> -+ <click stepKey="openAttributes" selector="{{AdminProductAttributesSection.sectionHeader}}"/> -+ <waitForElementVisible selector="{{AdminProductAttributesSection.attributeTextInputByCode(priceProductAttribute.attribute_code)}}" stepKey="waitforLabel"/> -+ <see stepKey="checkLabel" selector="{{AdminProductAttributesSection.attributeLabelByCode(priceProductAttribute.attribute_code)}}" userInput="{{priceProductAttribute.attribute_code}}"/> -+ </test> -+ -+ <test name="CreateProductAttributeEntityDropdownTest"> -+ <annotations> -+ <features value="Catalog"/> -+ <stories value="Create Product Attributes"/> -+ <title value="Admin should be able to create a Dropdown product attribute"/> -+ <description value="Admin should be able to create a Dropdown product attribute"/> -+ <severity value="CRITICAL"/> -+ <testCaseId value="MC-10896"/> -+ <group value="Catalog"/> -+ <group value="mtf_migrated"/> -+ </annotations> -+ -+ <before> -+ <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> -+ </before> -+ <after> -+ <actionGroup ref="navigateToEditProductAttribute" stepKey="goToEditPage"> -+ <argument name="ProductAttribute" value="{{dropdownProductAttribute.attribute_code}}"/> -+ </actionGroup> -+ <click stepKey="clickDelete" selector="{{AttributePropertiesSection.DeleteAttribute}}"/> -+ <click stepKey="clickOk" selector="{{AttributeDeleteModalSection.confirm}}"/> -+ <waitForPageLoad stepKey="waitForDeletion"/> -+ <actionGroup ref="logout" stepKey="logout"/> -+ </after> -+ -+ <!--Navigate to Stores > Attributes > Product.--> -+ <amOnPage url="{{AdminProductAttributeGridPage.url}}" stepKey="goToProductAttributes"/> -+ -+ <!--Create new Product Attribute as TextField, with code and default value.--> -+ <actionGroup ref="createProductAttribute" stepKey="createAttribute"> -+ <argument name="attribute" value="dropdownProductAttribute"/> -+ </actionGroup> -+ -+ <!--Navigate to Product Attribute, add Product Options and Save - 1--> -+ <actionGroup ref="navigateToEditProductAttribute" stepKey="goToEditPage1"> -+ <argument name="ProductAttribute" value="{{dropdownProductAttribute.attribute_code}}"/> -+ </actionGroup> -+ <actionGroup ref="createAttributeDropdownNthOption" stepKey="createOption1"> -+ <argument name="adminName" value="{{dropdownProductAttribute.option1_admin}}"/> -+ <argument name="frontName" value="{{dropdownProductAttribute.option1_frontend}}"/> -+ <argument name="row" value="1"/> -+ </actionGroup> -+ <actionGroup ref="createAttributeDropdownNthOption" stepKey="createOption2"> -+ <argument name="adminName" value="{{dropdownProductAttribute.option2_admin}}"/> -+ <argument name="frontName" value="{{dropdownProductAttribute.option2_frontend}}"/> -+ <argument name="row" value="2"/> -+ </actionGroup> -+ <actionGroup ref="createAttributeDropdownNthOptionAsDefault" stepKey="createOption3"> -+ <argument name="adminName" value="{{dropdownProductAttribute.option3_admin}}"/> -+ <argument name="frontName" value="{{dropdownProductAttribute.option3_frontend}}"/> -+ <argument name="row" value="3"/> -+ </actionGroup> -+ <click stepKey="saveAttribute" selector="{{AttributePropertiesSection.Save}}"/> -+ -+ <!--Perform appropriate assertions against dropdownProductAttribute entity--> -+ <actionGroup ref="navigateToEditProductAttribute" stepKey="goToEditPageForAssertions"> -+ <argument name="ProductAttribute" value="{{dropdownProductAttribute.attribute_code}}"/> -+ </actionGroup> -+ <seeInField stepKey="assertLabel" selector="{{AttributePropertiesSection.DefaultLabel}}" userInput="{{dropdownProductAttribute.attribute_code}}"/> -+ <seeOptionIsSelected stepKey="assertInputType" selector="{{AttributePropertiesSection.InputType}}" userInput="{{dropdownProductAttribute.frontend_input_admin}}"/> -+ <seeOptionIsSelected stepKey="assertRequired" selector="{{AttributePropertiesSection.ValueRequired}}" userInput="{{dropdownProductAttribute.is_required_admin}}"/> -+ <seeInField stepKey="assertAttrCode" selector="{{AdvancedAttributePropertiesSection.AttributeCode}}" userInput="{{dropdownProductAttribute.attribute_code}}"/> -+ -+ <!--Assert options are in order and with correct attributes--> -+ <seeInField stepKey="seeOption1Admin" selector="{{AttributePropertiesSection.dropdownNthOptionAdmin('1')}}" userInput="{{dropdownProductAttribute.option1_admin}}"/> -+ <seeInField stepKey="seeOption1StoreView" selector="{{AttributePropertiesSection.dropdownNthOptionDefaultStoreView('1')}}" userInput="{{dropdownProductAttribute.option1_frontend}}"/> -+ <dontSeeCheckboxIsChecked stepKey="dontSeeOption1Default" selector="{{AttributePropertiesSection.dropdownNthOptionIsDefault('1')}}"/> -+ <seeInField stepKey="seeOption2Admin" selector="{{AttributePropertiesSection.dropdownNthOptionAdmin('2')}}" userInput="{{dropdownProductAttribute.option2_admin}}"/> -+ <seeInField stepKey="seeOption2StoreView" selector="{{AttributePropertiesSection.dropdownNthOptionDefaultStoreView('2')}}" userInput="{{dropdownProductAttribute.option2_frontend}}"/> -+ <dontSeeCheckboxIsChecked stepKey="dontSeeOption2Default" selector="{{AttributePropertiesSection.dropdownNthOptionIsDefault('2')}}"/> -+ <seeInField stepKey="seeOption3Admin" selector="{{AttributePropertiesSection.dropdownNthOptionAdmin('3')}}" userInput="{{dropdownProductAttribute.option3_admin}}"/> -+ <seeInField stepKey="seeOption3StoreView" selector="{{AttributePropertiesSection.dropdownNthOptionDefaultStoreView('3')}}" userInput="{{dropdownProductAttribute.option3_frontend}}"/> -+ <seeCheckboxIsChecked stepKey="seeOption3Default" selector="{{AttributePropertiesSection.dropdownNthOptionIsDefault('3')}}"/> -+ -+ <!--Go to New Product page, add Attribute and check dropdown values--> -+ <amOnPage url="{{AdminProductCreatePage.url('4', 'simple')}}" stepKey="goToCreateSimpleProductPage"/> -+ <actionGroup ref="addProductAttributeInProductModal" stepKey="addAttributeToProduct"> -+ <argument name="attributeCode" value="{{dropdownProductAttribute.attribute_code}}"/> -+ </actionGroup> -+ <click stepKey="openAttributes" selector="{{AdminProductAttributesSection.sectionHeader}}"/> -+ <waitForElementVisible selector="{{AdminProductAttributesSection.attributeDropdownByCode(dropdownProductAttribute.attribute_code)}}" stepKey="waitforLabel"/> -+ <seeOptionIsSelected selector="{{AdminProductAttributesSection.attributeDropdownByCode(dropdownProductAttribute.attribute_code)}}" userInput="{{dropdownProductAttribute.option3_admin}}" stepKey="seeDefaultIsCorrect"/> -+ <see stepKey="seeOption1Available" selector="{{AdminProductAttributesSection.attributeDropdownByCode(dropdownProductAttribute.attribute_code)}}" userInput="{{dropdownProductAttribute.option1_admin}}"/> -+ <see stepKey="seeOption2Available" selector="{{AdminProductAttributesSection.attributeDropdownByCode(dropdownProductAttribute.attribute_code)}}" userInput="{{dropdownProductAttribute.option2_admin}}"/> -+ <see stepKey="seeOption3Available" selector="{{AdminProductAttributesSection.attributeDropdownByCode(dropdownProductAttribute.attribute_code)}}" userInput="{{dropdownProductAttribute.option3_admin}}"/> -+ </test> -+ -+ <test name="CreateProductAttributeEntityDropdownWithSingleQuoteTest"> -+ <annotations> -+ <features value="Catalog"/> -+ <stories value="Create Product Attributes"/> -+ <title value="Admin should be able to create a Dropdown product attribute containing a single quote"/> -+ <description value="Admin should be able to create a Dropdown product attribute containing a single quote"/> -+ <severity value="CRITICAL"/> -+ <testCaseId value="MC-10898"/> -+ <group value="Catalog"/> -+ <group value="mtf_migrated"/> -+ </annotations> -+ -+ <before> -+ <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> -+ </before> -+ <after> -+ <actionGroup ref="navigateToEditProductAttribute" stepKey="goToEditPage"> -+ <argument name="ProductAttribute" value="{{dropdownProductAttributeWithQuote.attribute_code}}"/> -+ </actionGroup> -+ <click stepKey="clickDelete" selector="{{AttributePropertiesSection.DeleteAttribute}}"/> -+ <click stepKey="clickOk" selector="{{AttributeDeleteModalSection.confirm}}"/> -+ <waitForPageLoad stepKey="waitForDeletion"/> -+ <actionGroup ref="logout" stepKey="logout"/> -+ </after> -+ -+ <!--Navigate to Stores > Attributes > Product.--> -+ <amOnPage url="{{AdminProductAttributeGridPage.url}}" stepKey="goToProductAttributes"/> -+ -+ <!--Create new Product Attribute as TextField, with code and default value.--> -+ <actionGroup ref="createProductAttribute" stepKey="createAttribute"> -+ <argument name="attribute" value="dropdownProductAttributeWithQuote"/> -+ </actionGroup> -+ -+ <!--Navigate to Product Attribute, add Product Option and Save - 1--> -+ <actionGroup ref="navigateToEditProductAttribute" stepKey="goToEditPage1"> -+ <argument name="ProductAttribute" value="{{dropdownProductAttributeWithQuote.attribute_code}}"/> -+ </actionGroup> -+ <actionGroup ref="createAttributeDropdownNthOptionAsDefault" stepKey="createOption1"> -+ <argument name="adminName" value="{{dropdownProductAttributeWithQuote.option1_admin}}"/> -+ <argument name="frontName" value="{{dropdownProductAttributeWithQuote.option1_frontend}}"/> -+ <argument name="row" value="1"/> -+ </actionGroup> -+ <click stepKey="saveAttribute" selector="{{AttributePropertiesSection.Save}}"/> -+ -+ <!--Perform appropriate assertions against dropdownProductAttribute entity--> -+ <actionGroup ref="navigateToEditProductAttribute" stepKey="goToEditPageForAssertions"> -+ <argument name="ProductAttribute" value="{{dropdownProductAttributeWithQuote.attribute_code}}"/> -+ </actionGroup> -+ <seeInField stepKey="assertLabel" selector="{{AttributePropertiesSection.DefaultLabel}}" userInput="{{dropdownProductAttributeWithQuote.attribute_code}}"/> -+ <seeOptionIsSelected stepKey="assertInputType" selector="{{AttributePropertiesSection.InputType}}" userInput="{{dropdownProductAttributeWithQuote.frontend_input_admin}}"/> -+ <seeOptionIsSelected stepKey="assertRequired" selector="{{AttributePropertiesSection.ValueRequired}}" userInput="{{dropdownProductAttributeWithQuote.is_required_admin}}"/> -+ <seeInField stepKey="assertAttrCode" selector="{{AdvancedAttributePropertiesSection.AttributeCode}}" userInput="{{dropdownProductAttributeWithQuote.attribute_code}}"/> -+ -+ <!--Assert options are in order and with correct attributes--> -+ <seeInField stepKey="seeOption1Admin" selector="{{AttributePropertiesSection.dropdownNthOptionAdmin('1')}}" userInput="{{dropdownProductAttributeWithQuote.option1_admin}}"/> -+ <seeInField stepKey="seeOption1StoreView" selector="{{AttributePropertiesSection.dropdownNthOptionDefaultStoreView('1')}}" userInput="{{dropdownProductAttributeWithQuote.option1_frontend}}"/> -+ <seeCheckboxIsChecked stepKey="seeOption1Default" selector="{{AttributePropertiesSection.dropdownNthOptionIsDefault('1')}}"/> -+ -+ <!--Go to New Product page, add Attribute and check dropdown values--> -+ <amOnPage url="{{AdminProductCreatePage.url('4', 'simple')}}" stepKey="goToCreateSimpleProductPage"/> -+ <actionGroup ref="addProductAttributeInProductModal" stepKey="addAttributeToProduct"> -+ <argument name="attributeCode" value="{{dropdownProductAttributeWithQuote.attribute_code}}"/> -+ </actionGroup> -+ <click stepKey="openAttributes" selector="{{AdminProductAttributesSection.sectionHeader}}"/> -+ <waitForElementVisible selector="{{AdminProductAttributesSection.attributeDropdownByCode(dropdownProductAttributeWithQuote.attribute_code)}}" stepKey="waitforLabel"/> -+ <seeOptionIsSelected selector="{{AdminProductAttributesSection.attributeDropdownByCode(dropdownProductAttributeWithQuote.attribute_code)}}" userInput="{{dropdownProductAttributeWithQuote.option1_admin}}" stepKey="seeDefaultIsCorrect"/> -+ <see stepKey="seeOption1Available" selector="{{AdminProductAttributesSection.attributeDropdownByCode(dropdownProductAttributeWithQuote.attribute_code)}}" userInput="{{dropdownProductAttributeWithQuote.option1_admin}}"/> -+ </test> -+ -+ <test name="CreateProductAttributeEntityMultiSelectTest"> -+ <annotations> -+ <features value="Catalog"/> -+ <stories value="Create Product Attributes"/> -+ <title value="Admin should be able to create a MultiSelect product attribute"/> -+ <description value="Admin should be able to create a MultiSelect product attribute"/> -+ <severity value="CRITICAL"/> -+ <testCaseId value="MC-10888"/> -+ <group value="Catalog"/> -+ <group value="mtf_migrated"/> -+ </annotations> -+ <before> -+ <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> -+ </before> -+ <after> -+ <actionGroup ref="navigateToEditProductAttribute" stepKey="goToEditPage"> -+ <argument name="ProductAttribute" value="{{multiselectProductAttribute.attribute_code}}"/> -+ </actionGroup> -+ <click stepKey="clickDelete" selector="{{AttributePropertiesSection.DeleteAttribute}}"/> -+ <click stepKey="clickOk" selector="{{AttributeDeleteModalSection.confirm}}"/> -+ <waitForPageLoad stepKey="waitForDeletion"/> -+ <actionGroup ref="logout" stepKey="logout"/> -+ </after> -+ -+ <!--Navigate to Stores > Attributes > Product.--> -+ <amOnPage url="{{AdminProductAttributeGridPage.url}}" stepKey="goToProductAttributes"/> -+ -+ <!--Create new Product Attribute as TextField, with code and default value.--> -+ <actionGroup ref="createProductAttribute" stepKey="createAttribute"> -+ <argument name="attribute" value="multiselectProductAttribute"/> -+ </actionGroup> -+ -+ <!--Navigate to Product Attribute, add Product Options and Save - 1--> -+ <actionGroup ref="navigateToEditProductAttribute" stepKey="goToEditPage1"> -+ <argument name="ProductAttribute" value="{{multiselectProductAttribute.attribute_code}}"/> -+ </actionGroup> -+ <actionGroup ref="createAttributeDropdownNthOption" stepKey="createOption1"> -+ <argument name="adminName" value="{{multiselectProductAttribute.option1_admin}}"/> -+ <argument name="frontName" value="{{multiselectProductAttribute.option1_frontend}}"/> -+ <argument name="row" value="1"/> -+ </actionGroup> -+ <actionGroup ref="createAttributeDropdownNthOption" stepKey="createOption2"> -+ <argument name="adminName" value="{{multiselectProductAttribute.option2_admin}}"/> -+ <argument name="frontName" value="{{multiselectProductAttribute.option2_frontend}}"/> -+ <argument name="row" value="2"/> -+ </actionGroup> -+ <actionGroup ref="createAttributeDropdownNthOptionAsDefault" stepKey="createOption3"> -+ <argument name="adminName" value="{{multiselectProductAttribute.option3_admin}}"/> -+ <argument name="frontName" value="{{multiselectProductAttribute.option3_frontend}}"/> -+ <argument name="row" value="3"/> -+ </actionGroup> -+ <click stepKey="saveAttribute" selector="{{AttributePropertiesSection.Save}}"/> -+ -+ <!--Perform appropriate assertions against multiselectProductAttribute entity--> -+ <actionGroup ref="navigateToEditProductAttribute" stepKey="goToEditPageForAssertions"> -+ <argument name="ProductAttribute" value="{{multiselectProductAttribute.attribute_code}}"/> -+ </actionGroup> -+ <seeInField stepKey="assertLabel" selector="{{AttributePropertiesSection.DefaultLabel}}" userInput="{{multiselectProductAttribute.attribute_code}}"/> -+ <seeOptionIsSelected stepKey="assertInputType" selector="{{AttributePropertiesSection.InputType}}" userInput="{{multiselectProductAttribute.frontend_input_admin}}"/> -+ <seeOptionIsSelected stepKey="assertRequired" selector="{{AttributePropertiesSection.ValueRequired}}" userInput="{{multiselectProductAttribute.is_required_admin}}"/> -+ <seeInField stepKey="assertAttrCode" selector="{{AdvancedAttributePropertiesSection.AttributeCode}}" userInput="{{multiselectProductAttribute.attribute_code}}"/> -+ -+ <!--Assert options are in order and with correct attributes--> -+ <seeInField stepKey="seeOption1Admin" selector="{{AttributePropertiesSection.dropdownNthOptionAdmin('1')}}" userInput="{{multiselectProductAttribute.option1_admin}}"/> -+ <seeInField stepKey="seeOption1StoreView" selector="{{AttributePropertiesSection.dropdownNthOptionDefaultStoreView('1')}}" userInput="{{multiselectProductAttribute.option1_frontend}}"/> -+ <dontSeeCheckboxIsChecked stepKey="dontSeeOption1Default" selector="{{AttributePropertiesSection.dropdownNthOptionIsDefault('1')}}"/> -+ <seeInField stepKey="seeOption2Admin" selector="{{AttributePropertiesSection.dropdownNthOptionAdmin('2')}}" userInput="{{multiselectProductAttribute.option2_admin}}"/> -+ <seeInField stepKey="seeOption2StoreView" selector="{{AttributePropertiesSection.dropdownNthOptionDefaultStoreView('2')}}" userInput="{{multiselectProductAttribute.option2_frontend}}"/> -+ <dontSeeCheckboxIsChecked stepKey="dontSeeOption2Default" selector="{{AttributePropertiesSection.dropdownNthOptionIsDefault('2')}}"/> -+ <seeInField stepKey="seeOption3Admin" selector="{{AttributePropertiesSection.dropdownNthOptionAdmin('3')}}" userInput="{{multiselectProductAttribute.option3_admin}}"/> -+ <seeInField stepKey="seeOption3StoreView" selector="{{AttributePropertiesSection.dropdownNthOptionDefaultStoreView('3')}}" userInput="{{multiselectProductAttribute.option3_frontend}}"/> -+ <seeCheckboxIsChecked stepKey="seeOption3Default" selector="{{AttributePropertiesSection.dropdownNthOptionIsDefault('3')}}"/> -+ -+ <!--Go to New Product page, add Attribute and check multiselect values--> -+ <amOnPage url="{{AdminProductCreatePage.url('4', 'simple')}}" stepKey="goToCreateSimpleProductPage"/> -+ <actionGroup ref="addProductAttributeInProductModal" stepKey="addAttributeToProduct"> -+ <argument name="attributeCode" value="{{multiselectProductAttribute.attribute_code}}"/> -+ </actionGroup> -+ <click stepKey="openAttributes" selector="{{AdminProductAttributesSection.sectionHeader}}"/> -+ <waitForElementVisible selector="{{AdminProductAttributesSection.attributeDropdownByCode(multiselectProductAttribute.attribute_code)}}" stepKey="waitforLabel"/> -+ <seeOptionIsSelected selector="{{AdminProductAttributesSection.attributeDropdownByCode(multiselectProductAttribute.attribute_code)}}" userInput="{{multiselectProductAttribute.option3_admin}}" stepKey="seeDefaultIsCorrect"/> -+ <see stepKey="seeOption1Available" selector="{{AdminProductAttributesSection.attributeDropdownByCode(multiselectProductAttribute.attribute_code)}}" userInput="{{multiselectProductAttribute.option1_admin}}"/> -+ <see stepKey="seeOption2Available" selector="{{AdminProductAttributesSection.attributeDropdownByCode(multiselectProductAttribute.attribute_code)}}" userInput="{{multiselectProductAttribute.option2_admin}}"/> -+ <see stepKey="seeOption3Available" selector="{{AdminProductAttributesSection.attributeDropdownByCode(multiselectProductAttribute.attribute_code)}}" userInput="{{multiselectProductAttribute.option3_admin}}"/> -+ </test> -+</tests> -diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/DeleteCategoriesTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/DeleteCategoriesTest.xml -index 4ec775f7ea2..5cae81b36a3 100644 ---- a/app/code/Magento/Catalog/Test/Mftf/Test/DeleteCategoriesTest.xml -+++ b/app/code/Magento/Catalog/Test/Mftf/Test/DeleteCategoriesTest.xml -@@ -7,10 +7,11 @@ - --> - - <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="DeleteCategoriesTest"> - <annotations> - <features value="Catalog"/> -+ <stories value="Delete categories"/> - <title value="Admin should be able to delete the default root category and subcategories and still see products in the storefront"/> - <description value="Admin should be able to delete the default root category and subcategories and still see products in the storefront"/> - <severity value="CRITICAL"/> -diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/DeleteUsedInConfigurableProductAttributeTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/DeleteUsedInConfigurableProductAttributeTest.xml -new file mode 100644 -index 00000000000..e79e4cea408 ---- /dev/null -+++ b/app/code/Magento/Catalog/Test/Mftf/Test/DeleteUsedInConfigurableProductAttributeTest.xml -@@ -0,0 +1,105 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> -+ <test name="DeleteUsedInConfigurableProductAttributeTest"> -+ <annotations> -+ <stories value="Delete product attribute"/> -+ <title value="Admin should not be able to delete product attribute used in configurable product"/> -+ <description value="Admin should not be able to delete product attribute used in configurable product"/> -+ <testCaseId value="MC-14061"/> -+ <severity value="CRITICAL"/> -+ <group value="Catalog"/> -+ <group value="mtf_migrated"/> -+ </annotations> -+ <before> -+ <!-- Create category --> -+ <createData entity="SimpleSubCategory" stepKey="categoryHandle"/> -+ -+ <!-- Create base configurable product--> -+ <createData entity="BaseConfigurableProduct" stepKey="baseConfigProductHandle"> -+ <requiredEntity createDataKey="categoryHandle"/> -+ </createData> -+ -+ <!-- Create Dropdown Product Attribute --> -+ <createData entity="productDropDownAttribute" stepKey="productAttributeHandle"/> -+ -+ <!--Create attribute options --> -+ <createData entity="productAttributeOption1" stepKey="productAttributeOption1Handle"> -+ <requiredEntity createDataKey="productAttributeHandle"/> -+ </createData> -+ <createData entity="productAttributeOption2" stepKey="productAttributeOption2Handle"> -+ <requiredEntity createDataKey="productAttributeHandle"/> -+ </createData> -+ -+ <!--Add to attribute to Default attribute set--> -+ <createData entity="AddToDefaultSet" stepKey="addToAttributeSetHandle"> -+ <requiredEntity createDataKey="productAttributeHandle"/> -+ </createData> -+ -+ <!-- Get handle of the attribute options --> -+ <getData entity="ProductAttributeOptionGetter" index="1" stepKey="getAttributeOption1Handle"> -+ <requiredEntity createDataKey="productAttributeHandle"/> -+ </getData> -+ <getData entity="ProductAttributeOptionGetter" index="2" stepKey="getAttributeOption2Handle"> -+ <requiredEntity createDataKey="productAttributeHandle"/> -+ </getData> -+ -+ <!--Create configurable product with the options --> -+ <createData entity="ConfigurableProductTwoOptions" stepKey="configProductOptionHandle"> -+ <requiredEntity createDataKey="baseConfigProductHandle"/> -+ <requiredEntity createDataKey="productAttributeHandle"/> -+ <requiredEntity createDataKey="getAttributeOption1Handle"/> -+ <requiredEntity createDataKey="getAttributeOption2Handle"/> -+ </createData> -+ -+ <!-- Login As Admin --> -+ <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> -+ </before> -+ <after> -+ <!-- Delete the configurable product created in the before block --> -+ <deleteData createDataKey="baseConfigProductHandle" stepKey="deleteConfig"/> -+ -+ <!-- Delete the category created in the before block --> -+ <deleteData createDataKey="categoryHandle" stepKey="deleteCategory"/> -+ -+ <!-- Delete configurable product attribute created in the before block --> -+ <deleteData createDataKey="productAttributeHandle" stepKey="deleteProductAttribute"/> -+ -+ <!-- Logout --> -+ <actionGroup ref="logout" stepKey="logoutFromAdmin"/> -+ </after> -+ <!-- Go to Stores > Attributes > Products. Search and select the product attribute that was used to create the configurable product--> -+ <actionGroup ref="OpenProductAttributeFromSearchResultInGridActionGroup" stepKey="openProductAttributeFromSearchResultInGrid"> -+ <argument name="productAttributeCode" value="$$productAttributeHandle.attribute_code$$"/> -+ </actionGroup> -+ -+ <!-- Click Delete Attribute button --> -+ <actionGroup ref="DeleteProductAttributeByAttributeCodeActionGroup" stepKey="deleteProductAttributeByAttributeCode"> -+ <argument name="productAttributeCode" value="$$productAttributeHandle.attribute_code$$"/> -+ </actionGroup> -+ -+ <!-- Should see error message: This attribute is used in configurable products. --> -+ <actionGroup ref="AssertAttributeDeletionErrorMessageActionGroup" stepKey="assertAttributeDeletionErrorMessage"/> -+ -+ <!-- Go back to the product attribute grid. Should see the product attribute in the grid --> -+ <actionGroup ref="SearchAttributeByCodeOnProductAttributeGridActionGroup" stepKey="searchAttributeByCodeOnProductAttributeGrid"> -+ <argument name="productAttributeCode" value="$$productAttributeHandle.attribute_code$$"/> -+ </actionGroup> -+ -+ <!-- Go to the Catalog > Products page and search configurable product created in before block.--> -+ <actionGroup ref="SearchForProductOnBackendActionGroup" stepKey="searchForProductOnBackend"> -+ <argument name="product" value="$$baseConfigProductHandle$$"/> -+ </actionGroup> -+ -+ <!--Should see the product attributes as expected --> -+ <actionGroup ref="AssertProductAttributePresenceInCatalogProductGridActionGroup" stepKey="assertProductAttributePresenceInCatalogProductGrid"> -+ <argument name="productAttribute" value="$$productAttributeHandle$$"/> -+ </actionGroup> -+ </test> -+</tests> -\ No newline at end of file -diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/EndToEndB2CAdminTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/EndToEndB2CAdminTest.xml -index d5d9de2648e..1419ca4cb42 100644 ---- a/app/code/Magento/Catalog/Test/Mftf/Test/EndToEndB2CAdminTest.xml -+++ b/app/code/Magento/Catalog/Test/Mftf/Test/EndToEndB2CAdminTest.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="EndToEndB2CAdminTest"> - <annotations> - <features value="End to End scenarios"/> -diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/EndToEndB2CGuestUserTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/EndToEndB2CGuestUserTest.xml -index e80de8122e8..e3924099d2f 100644 ---- a/app/code/Magento/Catalog/Test/Mftf/Test/EndToEndB2CGuestUserTest.xml -+++ b/app/code/Magento/Catalog/Test/Mftf/Test/EndToEndB2CGuestUserTest.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="EndToEndB2CGuestUserTest"> - <annotations> - <features value="End to End scenarios"/> -@@ -17,6 +17,9 @@ - <description value="User browses catalog, searches for product, adds product to cart, adds product to wishlist, compares products, uses coupon code and checks out."/> - <severity value="CRITICAL"/> - <testCaseId value="MAGETWO-87435"/> -+ <skip> -+ <issueId value="MC-16684"/> -+ </skip> - </annotations> - <before> - <resetCookie userInput="PHPSESSID" stepKey="resetCookieForCart"/> -diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/EndToEndB2CLoggedInUserTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/EndToEndB2CLoggedInUserTest.xml -index b8827616e3e..3f48c3ca811 100644 ---- a/app/code/Magento/Catalog/Test/Mftf/Test/EndToEndB2CLoggedInUserTest.xml -+++ b/app/code/Magento/Catalog/Test/Mftf/Test/EndToEndB2CLoggedInUserTest.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="EndToEndB2CLoggedInUserTest"> - <before> - <createData entity="ApiCategory" stepKey="createCategory"/> -diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/NewProductsListWidgetSimpleProductTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/NewProductsListWidgetSimpleProductTest.xml -new file mode 100644 -index 00000000000..9ee56c02c77 ---- /dev/null -+++ b/app/code/Magento/Catalog/Test/Mftf/Test/NewProductsListWidgetSimpleProductTest.xml -@@ -0,0 +1,44 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+/** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> -+ <test name="NewProductsListWidgetSimpleProductTest" extends="NewProductsListWidgetTest"> -+ <annotations> -+ <features value="Catalog"/> -+ <stories value="New products list widget"/> -+ <title value="Admin should be able to set Simple Product as new so that it shows up in the Catalog New Products List Widget"/> -+ <description value="Admin should be able to set Simple Product as new so that it shows up in the Catalog New Products List Widget"/> -+ <severity value="MAJOR"/> -+ <testCaseId value="MC-104"/> -+ <group value="Catalog"/> -+ <group value="WYSIWYGDisabled"/> -+ </annotations> -+ -+ <!-- A Cms page containing the New Products Widget gets created here via extends --> -+ -+ <!-- Create a Simple Product to appear in the widget --> -+ <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="amOnProductList"/> -+ <waitForPageLoad stepKey="waitForProductList"/> -+ <click selector="{{AdminProductGridActionSection.addProductBtn}}" stepKey="clickAddProduct"/> -+ <fillField selector="{{AdminProductFormSection.productName}}" userInput="{{_defaultProduct.name}}" stepKey="fillProductName"/> -+ <fillField selector="{{AdminProductFormSection.productSku}}" userInput="{{_defaultProduct.sku}}" stepKey="fillProductSku"/> -+ <fillField selector="{{AdminProductFormSection.productPrice}}" userInput="{{_defaultProduct.price}}" stepKey="fillProductPrice"/> -+ <fillField selector="{{AdminProductFormSection.productQuantity}}" userInput="100" stepKey="fillProductQuantity"/> -+ <fillField selector="{{AdminProductFormSection.setProductAsNewFrom}}" userInput="01/1/2000" stepKey="fillProductNewFrom"/> -+ <fillField selector="{{AdminProductFormSection.setProductAsNewTo}}" userInput="01/1/2099" stepKey="fillProductNewTo"/> -+ <click selector="{{AdminProductFormActionSection.saveButton}}" stepKey="clickSaveProduct"/> -+ -+ <!-- If PageCache is enabled, Cache clearing happens here via merge --> -+ -+ <!-- Check for product on the CMS page with the New Products widget --> -+ <amOnPage url="{{_newDefaultCmsPage.identifier}}" stepKey="amOnCmsPage"/> -+ <waitForPageLoad stepKey="waitForCmsPage"/> -+ <see selector="{{StorefrontCategoryMainSection.ProductItemInfo}}" userInput="{{_defaultProduct.name}}" stepKey="seeProductName"/> -+ </test> -+</tests> -diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/NewProductsListWidgetVirtualProductTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/NewProductsListWidgetVirtualProductTest.xml -new file mode 100644 -index 00000000000..a4e0d8708eb ---- /dev/null -+++ b/app/code/Magento/Catalog/Test/Mftf/Test/NewProductsListWidgetVirtualProductTest.xml -@@ -0,0 +1,48 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+/** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> -+ <test name="NewProductsListWidgetVirtualProductTest" extends="NewProductsListWidgetTest"> -+ <annotations> -+ <features value="Catalog"/> -+ <stories value="New products list widget"/> -+ <title value="Admin should be able to set Virtual Product as new so that it shows up in the Catalog New Products List Widget"/> -+ <description value="Admin should be able to set Virtual Product as new so that it shows up in the Catalog New Products List Widget"/> -+ <severity value="MAJOR"/> -+ <testCaseId value="MC-122"/> -+ <group value="Catalog"/> -+ <group value="WYSIWYGDisabled"/> -+ -+ </annotations> -+ -+ <!-- A Cms page containing the New Products Widget gets created here via extends --> -+ -+ <!-- Create a Virtual Product to appear in the widget --> -+ -+ <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="amOnProductList"/> -+ <waitForPageLoad stepKey="waitForProductList"/> -+ <click selector="{{AdminProductGridActionSection.addProductToggle}}" stepKey="toggleAddProductButton"/> -+ <click selector="{{AdminProductGridActionSection.addVirtualProduct}}" stepKey="clickAddVirtualProduct"/> -+ <fillField selector="{{AdminProductFormSection.productName}}" userInput="{{_defaultProduct.name}}" stepKey="fillProductName"/> -+ <fillField selector="{{AdminProductFormSection.productSku}}" userInput="{{_defaultProduct.sku}}" stepKey="fillProductSku"/> -+ <fillField selector="{{AdminProductFormSection.productPrice}}" userInput="123.45" stepKey="fillProductPrice"/> -+ <fillField selector="{{AdminProductFormSection.productQuantity}}" userInput="100" stepKey="fillProductQuantity"/> -+ <fillField selector="{{AdminProductFormSection.setProductAsNewFrom}}" userInput="01/1/2000" stepKey="fillProductNewFrom"/> -+ <fillField selector="{{AdminProductFormSection.setProductAsNewTo}}" userInput="01/1/2099" stepKey="fillProductNewTo"/> -+ <click selector="{{AdminProductFormActionSection.saveButton}}" stepKey="clickSaveProduct"/> -+ -+ <!-- If PageCache is enabled, Cache clearing happens here, via merge --> -+ -+ <!-- Check for product on the CMS page with the New Products widget --> -+ -+ <amOnPage url="{{_newDefaultCmsPage.identifier}}" stepKey="amOnCmsPage"/> -+ <waitForPageLoad stepKey="waitForCmsPage"/> -+ <see selector="{{StorefrontCategoryMainSection.ProductItemInfo}}" userInput="{{_defaultProduct.name}}" stepKey="seeProductName"/> -+ </test> -+</tests> -diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/ProductAvailableAfterEnablingSubCategoriesTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/ProductAvailableAfterEnablingSubCategoriesTest.xml -new file mode 100644 -index 00000000000..461ebde29fc ---- /dev/null -+++ b/app/code/Magento/Catalog/Test/Mftf/Test/ProductAvailableAfterEnablingSubCategoriesTest.xml -@@ -0,0 +1,56 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> -+ <test name="ProductAvailableAfterEnablingSubCategoriesTest"> -+ <annotations> -+ <features value="Catalog"/> -+ <stories value="Categories"/> -+ <title value="Check that parent categories are showing products after enabling subcategories after fully reindex"/> -+ <description value="Check that parent categories are showing products after enabling subcategories after fully reindex"/> -+ <severity value="MAJOR"/> -+ <testCaseId value="MAGETWO-97370"/> -+ <useCaseId value="MAGETWO-96846"/> -+ <group value="Catalog"/> -+ </annotations> -+ <before> -+ <createData entity="ApiCategory" stepKey="createCategory"/> -+ <createData entity="SubCategoryWithParent" stepKey="simpleSubCategory"> -+ <requiredEntity createDataKey="createCategory"/> -+ <field key="is_active">false</field> -+ </createData> -+ <createData entity="ApiSimpleProduct" stepKey="createSimpleProduct"> -+ <requiredEntity createDataKey="simpleSubCategory"/> -+ </createData> -+ -+ <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> -+ </before> -+ <after> -+ <deleteData createDataKey="createSimpleProduct" stepKey="deleteSimpleProduct"/> -+ <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> -+ <actionGroup ref="logout" stepKey="logout"/> -+ </after> -+ -+ <amOnPage url="$$createCategory.name$$.html" stepKey="goToCategoryStorefront2"/> -+ <waitForPageLoad stepKey="waitForCategoryStorefront"/> -+ <dontSeeElement selector="{{StorefrontCategoryProductSection.ProductImageByName($$createSimpleProduct.name$$)}}" stepKey="dontSeeCreatedProduct"/> -+ <amOnPage url="{{AdminCategoryPage.url}}" stepKey="onCategoryIndexPage"/> -+ <waitForPageLoad stepKey="waitForCategoryPageLoadAddProducts"/> -+ <click selector="{{AdminCategorySidebarTreeSection.expandAll}}" stepKey="expandAll"/> -+ <click selector="{{AdminCategorySidebarTreeSection.categoryInTree($$simpleSubCategory.name$$)}}" stepKey="clickOnCreatedSimpleSubCategoryBeforeDelete"/> -+ <waitForPageLoad stepKey="AdminCategoryEditPageLoad"/> -+ <click selector="{{AdminCategoryBasicFieldSection.enableCategoryLabel}}" stepKey="EnableCategory"/> -+ <click selector="{{AdminCategoryMainActionsSection.SaveButton}}" stepKey="saveCategoryWithProducts"/> -+ <waitForPageLoad stepKey="waitForCategorySaved"/> -+ <see userInput="You saved the category." stepKey="seeSuccessMessage"/> -+ <amOnPage url="$$createCategory.name$$.html" stepKey="goToCategoryStorefront"/> -+ <waitForPageLoad stepKey="waitForCategoryStorefrontPage"/> -+ <seeElement selector="{{StorefrontCategoryProductSection.ProductImageByName($$createSimpleProduct.name$$)}}" stepKey="seeCreatedProduct"/> -+ </test> -+</tests> -diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/SaveProductWithCustomOptionsSecondWebsiteTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/SaveProductWithCustomOptionsSecondWebsiteTest.xml -index 26f6eb7910f..e9e9eb01587 100644 ---- a/app/code/Magento/Catalog/Test/Mftf/Test/SaveProductWithCustomOptionsSecondWebsiteTest.xml -+++ b/app/code/Magento/Catalog/Test/Mftf/Test/SaveProductWithCustomOptionsSecondWebsiteTest.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="SaveProductWithCustomOptionsAdditionalWebsiteTest"> - <annotations> - <features value="Save a product with Custom Options and assign to a different website"/> -@@ -67,7 +67,7 @@ - <fillField userInput="{{_defaultProduct.price}}" selector="{{AdminProductFormSection.productPrice}}" stepKey="fillPrice"/> - <fillField userInput="{{_defaultProduct.quantity}}" selector="{{AdminProductFormSection.productQuantity}}" stepKey="fillQuantity"/> - -- <conditionalClick selector="{{AdminProductCustomizableOptionsSection.customizableOptions}}" dependentSelector="{{AdminProductCustomizableOptionsSection.checkIfCustomizableOptionsTabOpen}}" visible="true" stepKey="clickIfContentTabCloses2"/> -+ <conditionalClick selector="{{AdminProductCustomizableOptionsSection.customizableOptions}}" dependentSelector="{{AdminProductCustomizableOptionsSection.checkIfCustomizableOptionsTabOpen}}" visible="true" stepKey="clickIfContentTabCloses2"/> - <click selector="{{AdminProductCustomizableOptionsSection.addOptionBtn}}" stepKey="clickAddOption"/> - <waitForPageLoad stepKey="waitAfterAddOption"/> - <fillField selector="input[name='product[options][0][title]']" userInput="Radio Option" stepKey="fillOptionTitle"/> -diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/SimpleProductTwoCustomOptionsTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/SimpleProductTwoCustomOptionsTest.xml -index 084ae62788d..0049dcb5043 100644 ---- a/app/code/Magento/Catalog/Test/Mftf/Test/SimpleProductTwoCustomOptionsTest.xml -+++ b/app/code/Magento/Catalog/Test/Mftf/Test/SimpleProductTwoCustomOptionsTest.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="SimpleProductTwoCustomOptionsTest"> - <annotations> - <features value="Catalog"/> -diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontProductNameWithDoubleQuote.xml b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontProductNameWithDoubleQuote.xml -index 05964c5cce2..386633f0e94 100644 ---- a/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontProductNameWithDoubleQuote.xml -+++ b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontProductNameWithDoubleQuote.xml -@@ -7,16 +7,16 @@ - --> - - <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="StorefrontProductNameWithDoubleQuote"> - <annotations> -+ <features value="Catalog"/> -+ <stories value="Create products"/> - <title value="Product with double quote in name"/> - <description value="Product with a double quote in the name should appear correctly on the storefront"/> - <severity value="CRITICAL"/> - <group value="product"/> - <testCaseId value="MAGETWO-92384"/> -- <!-- Skipped due to MAGETWO-93261 --> -- <group value="skip"/> - </annotations> - <before> - <createData entity="_defaultCategory" stepKey="createCategory"/> -@@ -66,6 +66,7 @@ - <test name="StorefrontProductNameWithHTMLEntities"> - <annotations> - <features value="Catalog"/> -+ <stories value="Create product"/> - <title value=":Proudct with html special characters in name"/> - <description value="Product with html entities in the name should appear correctly on the PDP breadcrumbs on storefront"/> - <severity value="CRITICAL"/> -diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontProductWithEmptyAttributeTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontProductWithEmptyAttributeTest.xml -index 92013f6f9d0..1c1b47a6bde 100644 ---- a/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontProductWithEmptyAttributeTest.xml -+++ b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontProductWithEmptyAttributeTest.xml -@@ -7,9 +7,11 @@ - --> - - <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="StorefrontProductWithEmptyAttributeTest"> - <annotations> -+ <features value="Catalog"/> -+ <stories value="Create products"/> - <title value="Product attribute is not visible on storefront if it is empty"/> - <description value="Product attribute should not be visible on storefront if it is empty"/> - <severity value="MAJOR"/> -diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontProductsCompareWithEmptyAttributeTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontProductsCompareWithEmptyAttributeTest.xml -index c03241348e8..d7f98c4cdd3 100644 ---- a/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontProductsCompareWithEmptyAttributeTest.xml -+++ b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontProductsCompareWithEmptyAttributeTest.xml -@@ -7,9 +7,11 @@ - --> - - <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="StorefrontProductsCompareWithEmptyAttributeTest"> - <annotations> -+ <features value="Catalog"/> -+ <stories value="Product attributes"/> - <title value="Product attribute is not visible on product compare page if it is empty"/> - <description value="Product attribute should not be visible on the product compare page if it is empty for all products that are being compared, not even displayed as N/A"/> - <severity value="MAJOR"/> -diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontPurchaseProductCustomOptionsDifferentStoreViews.xml b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontPurchaseProductCustomOptionsDifferentStoreViews.xml -deleted file mode 100644 -index 7d843012d1d..00000000000 ---- a/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontPurchaseProductCustomOptionsDifferentStoreViews.xml -+++ /dev/null -@@ -1,291 +0,0 @@ --<?xml version="1.0" encoding="UTF-8"?> --<!-- -- /** -- * Copyright © Magento, Inc. All rights reserved. -- * See COPYING.txt for license details. -- */ ----> -- --<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -- xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Test/etc/testSchema.xsd"> -- <test name="StorefrontPurchaseProductCustomOptionsDifferentStoreViewsTest"> -- <annotations> -- <features value="Catalog"/> -- <title value="Admin should be able to sell products with different variants of their own"/> -- <description value="Admin should be able to sell products with different variants of their own"/> -- <severity value="CRITICAL"/> -- <testCaseId value="MAGETWO-58184"/> -- <group value="product"/> -- </annotations> -- -- <before> -- <!-- Create Customer --> -- -- <createData entity="Simple_US_Customer" stepKey="createCustomer"/> -- -- <!--Create Simple Product --> -- -- <createData entity="_defaultCategory" stepKey="createCategory"/> -- <createData entity="_defaultProduct" stepKey="createProduct"> -- <requiredEntity createDataKey="createCategory"/> -- <field key="price">100</field> -- </createData> -- -- <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin1"/> -- -- <!--Create storeView 1--> -- -- <actionGroup ref="AdminCreateStoreViewActionGroup" stepKey="createStoreView1"> -- <argument name="customStore" value="customStoreEN"/> -- </actionGroup> -- -- <!--Create storeView 2--> -- -- <actionGroup ref="AdminCreateStoreViewActionGroup" stepKey="createStoreView2"> -- <argument name="customStore" value="customStoreFR"/> -- </actionGroup> -- </before> -- -- <after> -- <deleteData createDataKey="createProduct" stepKey="deleteProduct"/> -- <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> -- <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> -- -- <!-- Delete Store View EN --> -- -- <actionGroup ref="AdminDeleteStoreViewActionGroup" stepKey="deleteStoreView1"> -- <argument name="customStore" value="customStoreEN"/> -- </actionGroup> -- -- <!-- Delete Store View FR --> -- -- <actionGroup ref="AdminDeleteStoreViewActionGroup" stepKey="deleteStoreView2"> -- <argument name="customStore" value="customStoreFR"/> -- </actionGroup> -- </after> -- -- <!-- Open Product Grid, Filter product and open --> -- -- <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="amOnProductGridPage"/> -- <waitForPageLoad time="30" stepKey="waitForPageLoad"/> -- -- <actionGroup ref="filterProductGridBySku" stepKey="filterGroupedProductOptions"> -- <argument name="product" value="_defaultProduct"/> -- </actionGroup> -- <click selector="{{AdminProductGridSection.productGridXRowYColumnButton('1', '2')}}" stepKey="openProductForEdit"/> -- <waitForPageLoad time="30" stepKey="waitForPageLoad1"/> -- -- <!-- Update Product with Option Value DropDown 1--> -- -- <conditionalClick selector="{{AdminProductCustomizableOptionsSection.customizableOptions}}" dependentSelector="{{AdminProductCustomizableOptionsSection.checkIfCustomizableOptionsTabOpen}}" visible="true" stepKey="clickIfContentTabCloses2"/> -- <click selector="{{AdminProductCustomizableOptionsSection.addOptionBtn}}" stepKey="checkAddOption1"/> -- <waitForPageLoad time="10" stepKey="waitForPageLoad7"/> -- <fillField selector="{{AdminProductCustomizableOptionsSection.fillOptionTitle('New Option')}}" userInput="Custom Options 1" stepKey="fillOptionTitle1"/> -- <click selector="{{AdminProductCustomizableOptionsSection.checkSelect('Custom Options 1')}}" stepKey="clickSelect1"/> -- <click selector="{{AdminProductCustomizableOptionsSection.checkDropDown('Custom Options 1')}}" stepKey="clickDropDown1"/> -- <click selector="{{AdminProductCustomizableOptionsSection.clickAddValue('Custom Options 1')}}" stepKey="clickAddValue1"/> -- <fillField selector="{{AdminProductCustomizableOptionsSection.fillOptionValueTitle('Custom Options 1', '0')}}" userInput="option1" stepKey="fillOptionValueTitle1"/> -- <fillField selector="{{AdminProductCustomizableOptionsSection.fillOptionValuePrice('Custom Options 1', '0')}}" userInput="5" stepKey="fillOptionValuePrice1"/> -- -- <!-- Update Product with Option Value 1 DropDown 1--> -- -- <click selector="{{AdminProductCustomizableOptionsSection.clickAddValue('Custom Options 1')}}" stepKey="clickAddValue2"/> -- <fillField selector="{{AdminProductCustomizableOptionsSection.fillOptionValueTitle('Custom Options 1', '1')}}" userInput="option2" stepKey="fillOptionValueTitle2"/> -- <fillField selector="{{AdminProductCustomizableOptionsSection.fillOptionValuePrice('Custom Options 1', '1')}}" userInput="50" stepKey="fillOptionValuePrice2"/> -- <selectOption selector="{{AdminProductCustomizableOptionsSection.clickSelectPriceType('Custom Options 1', '1')}}" userInput="percent" stepKey="clickSelectPriceType"/> -- <click selector="{{AdminProductFormActionSection.saveButton}}" stepKey="clickSaveButton1"/> -- -- <!-- Switcher to Store FR--> -- -- <click selector="{{AdminProductFormActionSection.changeStoreButton}}" stepKey="clickStoreSwitcher"/> -- <click selector="{{AdminProductFormActionSection.selectStoreView(customStoreFR.name)}}" stepKey="clickStoreView"/> -- <click selector="{{AdminConfirmationModalSection.ok}}" stepKey="acceptMessage"/> -- -- <!-- Open tab Customizable Options --> -- -- <waitForPageLoad time="10" stepKey="waitForPageLoad2"/> -- <conditionalClick selector="{{AdminProductCustomizableOptionsSection.customizableOptions}}" dependentSelector="{{AdminProductCustomizableOptionsSection.checkIfCustomizableOptionsTabOpen}}" visible="true" stepKey="clickIfContentTabCloses3"/> -- -- <!-- Update Option Customizable Options and Option Value 1--> -- -- <waitForPageLoad time="30" stepKey="waitForPageLoad8"/> -- <uncheckOption selector="{{AdminProductCustomizableOptionsSection.useDefaultOptionTitle}}" stepKey="uncheckUseDefaultOptionTitle"/> -- <fillField selector="{{AdminProductCustomizableOptionsSection.fillOptionTitle('Custom Options 1')}}" userInput="FR Custom Options 1" stepKey="fillOptionTitle2"/> -- <uncheckOption selector="{{AdminProductCustomizableOptionsSection.useDefaultOptionTitleByIndex('0')}}" stepKey="uncheckUseDefaultOptionValueTitle1"/> -- <fillField selector="{{AdminProductCustomizableOptionsSection.fillOptionValueTitle('FR Custom Options 1', '0')}}" userInput="FR option1" stepKey="fillOptionValueTitle3"/> -- -- <!-- Update Product with Option Value 1 DropDown 1--> -- -- <click selector="{{AdminProductCustomizableOptionsSection.useDefaultOptionTitleByIndex('1')}}" stepKey="clickHiddenRequireMessage"/> -- <uncheckOption selector="{{AdminProductCustomizableOptionsSection.useDefaultOptionTitleByIndex('1')}}" stepKey="uncheckUseDefaultOptionValueTitle2"/> -- <fillField selector="{{AdminProductCustomizableOptionsSection.fillOptionValueTitle('FR Custom Options 1', '1')}}" userInput="FR option2" stepKey="fillOptionValueTitle4"/> -- <click selector="{{AdminProductFormActionSection.saveButton}}" stepKey="clickSaveButton2"/> -- -- <!-- Login Customer Storefront --> -- -- <amOnPage url="{{StorefrontCustomerSignInPage.url}}" stepKey="amOnSignInPage"/> -- <waitForPageLoad stepKey="waitForSignInPage"/> -- <fillField userInput="$$createCustomer.email$$" selector="{{StorefrontCustomerSignInFormSection.emailField}}" stepKey="fillEmail"/> -- <fillField userInput="$$createCustomer.password$$" selector="{{StorefrontCustomerSignInFormSection.passwordField}}" stepKey="fillPassword"/> -- <click selector="{{StorefrontCustomerSignInFormSection.signInAccountButton}}" stepKey="clickSignInAccountButton"/> -- -- <!-- Go to Product Page --> -- -- <amOnPage url="{{StorefrontHomePage.url}}$$createProduct.custom_attributes[url_key]$$.html" stepKey="amOnProduct1Page"/> -- <waitForPageLoad stepKey="waitForProductPage"/> -- <seeElement selector="{{StorefrontProductInfoMainSection.productOptionDropDownTitle('Custom Options 1')}}" stepKey="seeProductOptionDropDownTitle"/> -- <seeElement selector="{{StorefrontProductInfoMainSection.productOptionDropDownOptionTitle('Custom Options 1', 'option1')}}" stepKey="seeproductOptionDropDownOptionTitle1"/> -- <seeElement selector="{{StorefrontProductInfoMainSection.productOptionDropDownOptionTitle('Custom Options 1', 'option2')}}" stepKey="seeproductOptionDropDownOptionTitle2"/> -- -- <selectOption userInput="5" selector="{{StorefrontProductInfoMainSection.productOptionSelect('Custom Options 1')}}" stepKey="selectProductOptionDropDown"/> -- -- <actionGroup ref="StorefrontAddToCartCustomOptionsProductPageActionGroup" stepKey="addToCartFromStorefrontProductPage1"> -- <argument name="productName" value="$$createProduct.name$$"/> -- </actionGroup> -- -- <selectOption userInput="50" selector="{{StorefrontProductInfoMainSection.productOptionSelect('Custom Options 1')}}" stepKey="selectProductOptionDropDown1"/> -- -- <actionGroup ref="StorefrontAddToCartCustomOptionsProductPageActionGroup" stepKey="addToCartFromStorefrontProductPage"> -- <argument name="productName" value="$$createProduct.name$$"/> -- </actionGroup> -- -- <!-- Checking the correctness of displayed custom options for user parameters on checkout --> -- -- <actionGroup ref="GoToCheckoutFromMinicartActionGroup" stepKey="goToCheckoutFromMinicart" /> -- -- <conditionalClick selector="{{CheckoutPaymentSection.cartItemsArea}}" dependentSelector="{{CheckoutPaymentSection.cartItemsArea}}" visible="true" stepKey="exposeMiniCart"/> -- -- <waitForLoadingMaskToDisappear stepKey="waitForLoadingMaskForCartItem"/> -- <waitForElement selector="{{CheckoutPaymentSection.cartItemsAreaActive}}" time="30" stepKey="waitForCartItemsAreaActive"/> -- -- <see selector="{{CheckoutPaymentSection.cartItems}}" userInput="$$createProduct.name$$" stepKey="seeProductInCart"/> -- -- <!-- See Custom options are displayed as option1 --> -- -- <conditionalClick selector="{{CheckoutPaymentSection.productOptionsByProductItemPrice('105')}}" dependentSelector="{{CheckoutPaymentSection.productOptionsActiveByProductItemPrice('105')}}" visible="false" stepKey="exposeProductOptions"/> -- <see selector="{{CheckoutPaymentSection.productOptionsActiveByProductItemPrice('105')}}" userInput="option1" stepKey="seeProductOptionValueDropdown1Input1"/> -- -- <!-- See Custom options are displayed as option2 --> -- -- <conditionalClick selector="{{CheckoutPaymentSection.productOptionsByProductItemPrice('150')}}" dependentSelector="{{CheckoutPaymentSection.productOptionsActiveByProductItemPrice('150')}}" visible="false" stepKey="exposeProductOptions1"/> -- <see selector="{{CheckoutPaymentSection.productOptionsActiveByProductItemPrice('150')}}" userInput="option2" stepKey="seeProductOptionValueDropdown1Input2"/> -- <click selector="{{CheckoutShippingSection.next}}" stepKey="clickNext"/> -- -- <!-- Place Order --> -- -- <waitForElement selector="{{CheckoutPaymentSection.placeOrder}}" time="30" stepKey="waitForPlaceOrderButton"/> -- <click selector="{{CheckoutPaymentSection.placeOrder}}" stepKey="clickPlaceOrder"/> -- -- <grabTextFrom selector="{{CheckoutSuccessMainSection.orderNumber22}}" stepKey="grabOrderNumber"/> -- -- <!-- Open Order --> -- -- <amOnPage url="{{AdminOrdersPage.url}}" stepKey="onOrdersPage"/> -- <waitForLoadingMaskToDisappear stepKey="waitForLoadingMaskToDisappearOnOrdersPage"/> -- <fillField selector="{{AdminOrdersGridSection.search}}" userInput="{$grabOrderNumber}" stepKey="fillOrderNum"/> -- <click selector="{{AdminOrdersGridSection.submitSearch}}" stepKey="submitSearchOrderNum"/> -- <waitForLoadingMaskToDisappear stepKey="waitForLoadingMaskToDisappearOnSearch"/> -- <click selector="{{AdminOrdersGridSection.firstRow}}" stepKey="clickOrderRow"/> -- -- <!-- Checking the correctness of displayed custom options for user parameters on Order --> -- -- <see selector="{{AdminOrderItemsOrderedSection.productNameOptions}}" userInput="option1" stepKey="seeAdminOrderProductOptionValueDropdown1"/> -- <see selector="{{AdminOrderItemsOrderedSection.productNameOptions}}" userInput="option2" stepKey="seeAdminOrderProductOptionValueDropdown2"/> -- -- <!-- Switch to FR Store View Storefront --> -- -- <amOnPage url="{{StorefrontHomePage.url}}" stepKey="amOnProduct4Page"/> -- <waitForPageLoad stepKey="waitForStorefrontHomePage"/> -- <click selector="{{StorefrontHeaderSection.storeViewSwitcher}}" stepKey="clickStoreViewSwitcher1"/> -- <waitForElementVisible selector="{{StorefrontHeaderSection.storeViewDropdown}}" stepKey="waitForStoreViewDropdown1"/> -- <click selector="{{StorefrontHeaderSection.storeViewOption(customStoreFR.code)}}" stepKey="selectStoreView1"/> -- <waitForPageLoad stepKey="waitForPageLoad4"/> -- -- <amOnPage url="{{StorefrontHomePage.url}}$$createProduct.custom_attributes[url_key]$$.html" stepKey="amOnProduct2Page"/> -- <waitForPageLoad stepKey="waitForProductPage2"/> -- <seeElement selector="{{StorefrontProductInfoMainSection.productOptionDropDownTitle('FR Custom Options 1')}}" stepKey="seeProductFrOptionDropDownTitle"/> -- <seeElement selector="{{StorefrontProductInfoMainSection.productOptionDropDownOptionTitle('FR Custom Options 1', 'FR option1')}}" stepKey="productFrOptionDropDownOptionTitle1"/> -- <seeElement selector="{{StorefrontProductInfoMainSection.productOptionDropDownOptionTitle('FR Custom Options 1', 'FR option2')}}" stepKey="productFrOptionDropDownOptionTitle2"/> -- -- <selectOption userInput="5" selector="{{StorefrontProductInfoMainSection.productOptionSelect('FR Custom Options 1')}}" stepKey="seeProductFrOptionDropDown"/> -- -- <actionGroup ref="StorefrontAddToCartCustomOptionsProductPageActionGroup" stepKey="addToCartFromStorefrontProductPage2"> -- <argument name="productName" value="$$createProduct.name$$"/> -- </actionGroup> -- -- <selectOption userInput="50" selector="{{StorefrontProductInfoMainSection.productOptionSelect('FR Custom Options 1')}}" stepKey="seeProductFrOptionDropDown1"/> -- -- <actionGroup ref="StorefrontAddToCartCustomOptionsProductPageActionGroup" stepKey="addToCartFromStorefrontProductPage3"> -- <argument name="productName" value="$$createProduct.name$$"/> -- </actionGroup> -- -- <!-- Checking the correctness of displayed custom options for user parameters on checkout --> -- -- <actionGroup ref="GoToCheckoutFromMinicartActionGroup" stepKey="goToCheckoutFromMinicart1" /> -- -- <conditionalClick selector="{{CheckoutPaymentSection.cartItemsArea}}" dependentSelector="{{CheckoutPaymentSection.cartItemsArea}}" visible="true" stepKey="exposeMiniCart1"/> -- -- <waitForLoadingMaskToDisappear stepKey="waitForLoadingMaskForCartItem1"/> -- <waitForElement selector="{{CheckoutPaymentSection.cartItemsAreaActive}}" time="30" stepKey="waitForCartItemsAreaActive1"/> -- -- <see selector="{{CheckoutPaymentSection.cartItems}}" userInput="$$createProduct.name$$" stepKey="seeProductInCar1t"/> -- -- <!-- See Custom options are displayed as option1 --> -- -- <conditionalClick selector="{{CheckoutPaymentSection.productOptionsByProductItemPrice('105')}}" dependentSelector="{{CheckoutPaymentSection.productOptionsActiveByProductItemPrice('105')}}" visible="false" stepKey="exposeProductOptions2"/> -- <see selector="{{CheckoutPaymentSection.productOptionsActiveByProductItemPrice('105')}}" userInput="FR option1" stepKey="seeProductFrOptionValueDropdown1Input2"/> -- -- <!-- See Custom options are displayed as option2 --> -- -- <conditionalClick selector="{{CheckoutPaymentSection.productOptionsByProductItemPrice('150')}}" dependentSelector="{{CheckoutPaymentSection.productOptionsActiveByProductItemPrice('150')}}" visible="false" stepKey="exposeProductOptions3"/> -- <see selector="{{CheckoutPaymentSection.productOptionsActiveByProductItemPrice('150')}}" userInput="FR option2" stepKey="seeProductFrOptionValueDropdown1Input3"/> -- <click selector="{{CheckoutShippingSection.next}}" stepKey="clickNext1"/> -- -- <!-- Place Order --> -- -- <waitForElement selector="{{CheckoutPaymentSection.placeOrder}}" time="30" stepKey="waitForPlaceOrderButton1"/> -- <click selector="{{CheckoutPaymentSection.placeOrder}}" stepKey="clickPlaceOrder1"/> -- -- <!-- Open Product Grid, Filter product and open --> -- -- <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="amOnProductGridPage1"/> -- <waitForPageLoad time="30" stepKey="waitForPageLoad5"/> -- -- <actionGroup ref="filterProductGridBySku" stepKey="filterGroupedProductOptions1"> -- <argument name="product" value="_defaultProduct"/> -- </actionGroup> -- <click selector="{{AdminProductGridSection.productGridXRowYColumnButton('1', '2')}}" stepKey="openProductForEdit1"/> -- <waitForPageLoad time="30" stepKey="waitForPageLoad6"/> -- -- <!-- Switcher to Store FR--> -- -- <click selector="{{AdminProductFormActionSection.changeStoreButton}}" stepKey="clickStoreSwitcher1"/> -- <click selector="{{AdminProductFormActionSection.selectStoreView(customStoreFR.name)}}" stepKey="clickStoreView1"/> -- <click selector="{{AdminConfirmationModalSection.ok}}" stepKey="acceptMessage1"/> -- -- <!-- Open tab Customizable Options --> -- -- <waitForPageLoad time="30" stepKey="waitForPageLoad9"/> -- <conditionalClick selector="{{AdminProductCustomizableOptionsSection.customizableOptions}}" dependentSelector="{{AdminProductCustomizableOptionsSection.checkIfCustomizableOptionsTabOpen}}" visible="true" stepKey="clickIfContentTabCloses4" /> -- -- <!-- Update Option Customizable Options and Option Value 1--> -- -- <waitForPageLoad time="30" stepKey="waitForPageLoad10"/> -- <checkOption selector="{{AdminProductCustomizableOptionsSection.useDefaultOptionTitle}}" stepKey="checkUseDefaultOptionTitle"/> -- <checkOption selector="{{AdminProductCustomizableOptionsSection.useDefaultOptionTitleByIndex('0')}}" stepKey="checkUseDefaultOptionValueTitle1"/> -- -- <!-- Update Product with Option Value 1 DropDown 1--> -- -- <waitForPageLoad time="30" stepKey="waitForPageLoad11"/> -- <checkOption selector="{{AdminProductCustomizableOptionsSection.useDefaultOptionTitleByIndex('1')}}" stepKey="checkUseDefaultOptionValueTitle2"/> -- <click selector="{{AdminProductFormActionSection.saveButton}}" stepKey="clickSaveButton3"/> -- -- <!--Go to Product Page--> -- -- <amOnPage url="{{StorefrontHomePage.url}}$$createProduct.custom_attributes[url_key]$$.html" stepKey="amOnProduct2Page2"/> -- <waitForPageLoad stepKey="waitForProductPage3"/> -- <seeElement selector="{{StorefrontProductInfoMainSection.productOptionDropDownTitle('Custom Options 1')}}" stepKey="seeProductOptionDropDownTitle1"/> -- <seeElement selector="{{StorefrontProductInfoMainSection.productOptionDropDownOptionTitle('Custom Options 1', 'option1')}}" stepKey="seeProductOptionDropDownOptionTitle3"/> -- <seeElement selector="{{StorefrontProductInfoMainSection.productOptionDropDownOptionTitle('Custom Options 1', 'option2')}}" stepKey="seeProductOptionDropDownOptionTitle4"/> -- </test> --</tests> -\ No newline at end of file -diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontPurchaseProductCustomOptionsDifferentStoreViewsTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontPurchaseProductCustomOptionsDifferentStoreViewsTest.xml -new file mode 100644 -index 00000000000..a0670bdee54 ---- /dev/null -+++ b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontPurchaseProductCustomOptionsDifferentStoreViewsTest.xml -@@ -0,0 +1,317 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> -+ <test name="StorefrontPurchaseProductCustomOptionsDifferentStoreViewsTest"> -+ <annotations> -+ <features value="Catalog"/> -+ <stories value="Custom options different storeviews"/> -+ <title value="Admin should be able to sell products with different variants of their own"/> -+ <description value="Admin should be able to sell products with different variants of their own"/> -+ <severity value="CRITICAL"/> -+ <testCaseId value="MC-16476"/> -+ <group value="product"/> -+ </annotations> -+ -+ <before> -+ <!-- Create Customer --> -+ -+ <createData entity="Simple_US_Customer" stepKey="createCustomer"/> -+ -+ <!--Create Simple Product --> -+ -+ <createData entity="_defaultCategory" stepKey="createCategory"/> -+ <createData entity="_defaultProduct" stepKey="createProduct"> -+ <requiredEntity createDataKey="createCategory"/> -+ <field key="price">100</field> -+ </createData> -+ -+ <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin1"/> -+ -+ <!--Create storeView 1--> -+ -+ <actionGroup ref="AdminCreateStoreViewActionGroup" stepKey="createStoreView1"> -+ <argument name="customStore" value="customStoreEN"/> -+ </actionGroup> -+ -+ <!--Create storeView 2--> -+ -+ <actionGroup ref="AdminCreateStoreViewActionGroup" stepKey="createStoreView2"> -+ <argument name="customStore" value="customStoreFR"/> -+ </actionGroup> -+ </before> -+ -+ <after> -+ <deleteData createDataKey="createProduct" stepKey="deleteProduct"/> -+ <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> -+ <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> -+ -+ <!-- Delete Store View EN --> -+ -+ <actionGroup ref="AdminDeleteStoreViewActionGroup" stepKey="deleteStoreView1"> -+ <argument name="customStore" value="customStoreEN"/> -+ </actionGroup> -+ -+ <!-- Delete Store View FR --> -+ -+ <actionGroup ref="AdminDeleteStoreViewActionGroup" stepKey="deleteStoreView2"> -+ <argument name="customStore" value="customStoreFR"/> -+ </actionGroup> -+ <actionGroup ref="clearFiltersAdminDataGrid" stepKey="clearWebsitesGridFilters"/> -+ -+ <actionGroup ref="AdminOrdersGridClearFiltersActionGroup" stepKey="clearOrdersGridFilter"/> -+ -+ <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="amOnProductGridPage"/> -+ <actionGroup ref="clearFiltersAdminDataGrid" stepKey="clearProductsGridFilters"/> -+ <actionGroup ref="logout" stepKey="logout"/> -+ <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="customerLogoutStorefront"/> -+ </after> -+ -+ <!-- Open Product Grid, Filter product and open --> -+ -+ <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="amOnProductGridPage"/> -+ <waitForPageLoad time="30" stepKey="waitForPageLoad1"/> -+ -+ <actionGroup ref="SearchForProductOnBackendActionGroup" stepKey="filterGroupedProductOptions"> -+ <argument name="product" value="_defaultProduct"/> -+ </actionGroup> -+ -+ <actionGroup ref="OpenEditProductOnBackendActionGroup" stepKey="openEditProductPage"> -+ <argument name="product" value="$$createProduct$$"/> -+ </actionGroup> -+ -+ <!-- Update Product with Option Value DropDown 1--> -+ <conditionalClick selector="{{AdminProductCustomizableOptionsSection.customizableOptions}}" dependentSelector="{{AdminProductCustomizableOptionsSection.checkIfCustomizableOptionsTabOpen}}" visible="true" stepKey="clickIfContentTabCloses2"/> -+ <click selector="{{AdminProductCustomizableOptionsSection.addOptionBtn}}" stepKey="checkAddOption1"/> -+ <waitForPageLoad time="10" stepKey="waitForPageLoad3"/> -+ <fillField selector="{{AdminProductCustomizableOptionsSection.fillOptionTitle('New Option')}}" userInput="Custom Options 1" stepKey="fillOptionTitle1"/> -+ <click selector="{{AdminProductCustomizableOptionsSection.checkSelect('Custom Options 1')}}" stepKey="clickSelect1"/> -+ <click selector="{{AdminProductCustomizableOptionsSection.checkDropDown('Custom Options 1')}}" stepKey="clickDropDown1"/> -+ <click selector="{{AdminProductCustomizableOptionsSection.clickAddValue('Custom Options 1')}}" stepKey="clickAddValue1"/> -+ <fillField selector="{{AdminProductCustomizableOptionsSection.fillOptionValueTitle('Custom Options 1', '0')}}" userInput="option1" stepKey="fillOptionValueTitle1"/> -+ <fillField selector="{{AdminProductCustomizableOptionsSection.fillOptionValuePrice('Custom Options 1', '0')}}" userInput="5" stepKey="fillOptionValuePrice1"/> -+ -+ <!-- Update Product with Option Value 1 DropDown 1--> -+ -+ <click selector="{{AdminProductCustomizableOptionsSection.clickAddValue('Custom Options 1')}}" stepKey="clickAddValue2"/> -+ <fillField selector="{{AdminProductCustomizableOptionsSection.fillOptionValueTitle('Custom Options 1', '1')}}" userInput="option2" stepKey="fillOptionValueTitle2"/> -+ <fillField selector="{{AdminProductCustomizableOptionsSection.fillOptionValuePrice('Custom Options 1', '1')}}" userInput="50" stepKey="fillOptionValuePrice2"/> -+ <selectOption selector="{{AdminProductCustomizableOptionsSection.clickSelectPriceType('Custom Options 1', '1')}}" userInput="percent" stepKey="clickSelectPriceType"/> -+ <click selector="{{AdminProductFormActionSection.saveButton}}" stepKey="clickSaveButton1"/> -+ -+ <!-- Switcher to Store FR--> -+ <actionGroup ref="AdminSwitchStoreViewActionGroup" stepKey="switchToStoreFR"> -+ <argument name="storeView" value="customStoreFR.name"/> -+ </actionGroup> -+ -+ <!-- Open tab Customizable Options --> -+ -+ <conditionalClick selector="{{AdminProductCustomizableOptionsSection.customizableOptions}}" dependentSelector="{{AdminProductCustomizableOptionsSection.checkIfCustomizableOptionsTabOpen}}" visible="true" stepKey="clickIfContentTabCloses3"/> -+ -+ <!-- Update Option Customizable Options and Option Value 1--> -+ -+ <waitForPageLoad time="30" stepKey="waitForPageLoad5"/> -+ <uncheckOption selector="{{AdminProductCustomizableOptionsSection.useDefaultOptionTitle}}" stepKey="uncheckUseDefaultOptionTitle"/> -+ <fillField selector="{{AdminProductCustomizableOptionsSection.fillOptionTitle('Custom Options 1')}}" userInput="FR Custom Options 1" stepKey="fillOptionTitle2"/> -+ <uncheckOption selector="{{AdminProductCustomizableOptionsSection.useDefaultOptionTitleByIndex('0')}}" stepKey="uncheckUseDefaultOptionValueTitle1"/> -+ <fillField selector="{{AdminProductCustomizableOptionsSection.fillOptionValueTitle('FR Custom Options 1', '0')}}" userInput="FR option1" stepKey="fillOptionValueTitle3"/> -+ -+ <!-- Update Product with Option Value 1 DropDown 1--> -+ -+ <click selector="{{AdminProductCustomizableOptionsSection.useDefaultOptionTitleByIndex('1')}}" stepKey="clickHiddenRequireMessage"/> -+ <uncheckOption selector="{{AdminProductCustomizableOptionsSection.useDefaultOptionTitleByIndex('1')}}" stepKey="uncheckUseDefaultOptionValueTitle2"/> -+ <fillField selector="{{AdminProductCustomizableOptionsSection.fillOptionValueTitle('FR Custom Options 1', '1')}}" userInput="FR option2" stepKey="fillOptionValueTitle4"/> -+ <click selector="{{AdminProductFormActionSection.saveButton}}" stepKey="clickSaveButton2"/> -+ -+ <!-- Login Customer Storefront --> -+ -+ <actionGroup ref="LoginToStorefrontActionGroup" stepKey="customerLogin"> -+ <argument name="Customer" value="$$createCustomer$$" /> -+ </actionGroup> -+ -+ <!-- Go to Product Page --> -+ -+ <amOnPage url="{{StorefrontHomePage.url}}$$createProduct.custom_attributes[url_key]$$.html" stepKey="amOnProduct1Page"/> -+ <waitForPageLoad time="30" stepKey="waitForPageLoad7"/> -+ -+ <seeElement selector="{{StorefrontProductInfoMainSection.productOptionDropDownTitle('Custom Options 1')}}" stepKey="seeProductOptionDropDownTitle"/> -+ <seeElement selector="{{StorefrontProductInfoMainSection.productOptionDropDownOptionTitle('Custom Options 1', 'option1')}}" stepKey="seeproductOptionDropDownOptionTitle1"/> -+ <seeElement selector="{{StorefrontProductInfoMainSection.productOptionDropDownOptionTitle('Custom Options 1', 'option2')}}" stepKey="seeproductOptionDropDownOptionTitle2"/> -+ -+ <selectOption userInput="5" selector="{{StorefrontProductInfoMainSection.productOptionSelect('Custom Options 1')}}" stepKey="selectProductOptionDropDown"/> -+ -+ <actionGroup ref="StorefrontAddToCartCustomOptionsProductPageActionGroup" stepKey="addToCartFromStorefrontProductPage1"> -+ <argument name="productName" value="$$createProduct.name$$"/> -+ </actionGroup> -+ -+ <selectOption userInput="50" selector="{{StorefrontProductInfoMainSection.productOptionSelect('Custom Options 1')}}" stepKey="selectProductOptionDropDown1"/> -+ -+ <actionGroup ref="StorefrontAddToCartCustomOptionsProductPageActionGroup" stepKey="addToCartFromStorefrontProductPage"> -+ <argument name="productName" value="$$createProduct.name$$"/> -+ </actionGroup> -+ -+ <!-- Checking the correctness of displayed custom options for user parameters on checkout --> -+ -+ <actionGroup ref="GoToCheckoutFromMinicartActionGroup" stepKey="goToCheckoutFromMinicart" /> -+ -+ <conditionalClick selector="{{CheckoutPaymentSection.cartItemsArea}}" dependentSelector="{{CheckoutPaymentSection.cartItemsArea}}" visible="true" stepKey="exposeMiniCart"/> -+ -+ <waitForLoadingMaskToDisappear stepKey="waitForLoadingMaskForCartItem"/> -+ <waitForElement selector="{{CheckoutPaymentSection.cartItemsAreaActive}}" time="30" stepKey="waitForCartItemsAreaActive"/> -+ -+ <see selector="{{CheckoutPaymentSection.cartItems}}" userInput="$$createProduct.name$$" stepKey="seeProductInCart"/> -+ -+ <!-- See Custom options are displayed as option1 --> -+ -+ <conditionalClick selector="{{CheckoutPaymentSection.productOptionsByProductItemPrice('105')}}" dependentSelector="{{CheckoutPaymentSection.productOptionsActiveByProductItemPrice('105')}}" visible="false" stepKey="exposeProductOptions"/> -+ <see selector="{{CheckoutPaymentSection.productOptionsActiveByProductItemPrice('105')}}" userInput="option1" stepKey="seeProductOptionValueDropdown1Input1"/> -+ -+ <!-- See Custom options are displayed as option2 --> -+ -+ <conditionalClick selector="{{CheckoutPaymentSection.productOptionsByProductItemPrice('150')}}" dependentSelector="{{CheckoutPaymentSection.productOptionsActiveByProductItemPrice('150')}}" visible="false" stepKey="exposeProductOptions1"/> -+ <see selector="{{CheckoutPaymentSection.productOptionsActiveByProductItemPrice('150')}}" userInput="option2" stepKey="seeProductOptionValueDropdown1Input2"/> -+ -+ <!-- Place Order --> -+ -+ <!--Select shipping method--> -+ <actionGroup ref="CheckoutSelectFlatRateShippingMethodActionGroup" stepKey="selectFlatRateShippingMethod"/> -+ <waitForElementVisible selector="{{CheckoutShippingSection.next}}" time="30" stepKey="waitForNextButton"/> -+ <click selector="{{CheckoutShippingSection.next}}" stepKey="clickNext"/> -+ <waitForLoadingMaskToDisappear stepKey="waitForLoadingMaskAfterClickNext"/> -+ <!--Select payment method--> -+ <actionGroup ref="CheckoutSelectCheckMoneyOrderPaymentActionGroup" stepKey="selectPaymentMethod"/> -+ <!-- Place Order --> -+ <actionGroup ref="CheckoutPlaceOrderActionGroup" stepKey="customerPlaceOrder"> -+ <argument name="orderNumberMessage" value="CONST.successCheckoutOrderNumberMessage"/> -+ <argument name="emailYouMessage" value="CONST.successCheckoutEmailYouMessage"/> -+ </actionGroup> -+ -+ <grabTextFrom selector="{{CheckoutSuccessMainSection.orderNumber22}}" stepKey="grabOrderNumber"/> -+ -+ <!-- Open Order --> -+ -+ <actionGroup ref="filterOrderGridById" stepKey="openOrdersGrid"> -+ <argument name="orderId" value="{$grabOrderNumber}"/> -+ </actionGroup> -+ <click selector="{{AdminOrdersGridSection.firstRow}}" stepKey="clickOrderRow"/> -+ <waitForPageLoad time="30" stepKey="waitForPageLoad10"/> -+ -+ <!-- Checking the correctness of displayed custom options for user parameters on Order --> -+ -+ <see selector="{{AdminOrderItemsOrderedSection.productNameOptions}}" userInput="option1" stepKey="seeAdminOrderProductOptionValueDropdown1"/> -+ <see selector="{{AdminOrderItemsOrderedSection.productNameOptions}}" userInput="option2" stepKey="seeAdminOrderProductOptionValueDropdown2"/> -+ -+ <!-- Switch to FR Store View Storefront --> -+ -+ <amOnPage url="{{StorefrontHomePage.url}}" stepKey="amOnProduct4Page"/> -+ -+ <actionGroup ref="StorefrontSwitchStoreViewActionGroup" stepKey="switchStore"> -+ <argument name="storeView" value="customStoreFR"/> -+ </actionGroup> -+ -+ <amOnPage url="{{StorefrontProductPage.url($$createProduct.custom_attributes[url_key]$$)}}" stepKey="amOnProduct2Page"/> -+ -+ <seeElement selector="{{StorefrontProductInfoMainSection.productOptionDropDownTitle('FR Custom Options 1')}}" stepKey="seeProductFrOptionDropDownTitle"/> -+ <seeElement selector="{{StorefrontProductInfoMainSection.productOptionDropDownOptionTitle('FR Custom Options 1', 'FR option1')}}" stepKey="productFrOptionDropDownOptionTitle1"/> -+ <seeElement selector="{{StorefrontProductInfoMainSection.productOptionDropDownOptionTitle('FR Custom Options 1', 'FR option2')}}" stepKey="productFrOptionDropDownOptionTitle2"/> -+ -+ <selectOption userInput="5" selector="{{StorefrontProductInfoMainSection.productOptionSelect('FR Custom Options 1')}}" stepKey="seeProductFrOptionDropDown"/> -+ -+ <actionGroup ref="StorefrontAddToCartCustomOptionsProductPageActionGroup" stepKey="addToCartFromStorefrontProductPage2"> -+ <argument name="productName" value="$$createProduct.name$$"/> -+ </actionGroup> -+ -+ <selectOption userInput="50" selector="{{StorefrontProductInfoMainSection.productOptionSelect('FR Custom Options 1')}}" stepKey="seeProductFrOptionDropDown1"/> -+ -+ <actionGroup ref="StorefrontAddToCartCustomOptionsProductPageActionGroup" stepKey="addToCartFromStorefrontProductPage3"> -+ <argument name="productName" value="$$createProduct.name$$"/> -+ </actionGroup> -+ -+ <!-- Checking the correctness of displayed custom options for user parameters on checkout --> -+ -+ <actionGroup ref="GoToCheckoutFromMinicartActionGroup" stepKey="goToCheckoutFromMinicart1" /> -+ -+ <conditionalClick selector="{{CheckoutPaymentSection.cartItemsArea}}" dependentSelector="{{CheckoutPaymentSection.cartItemsArea}}" visible="true" stepKey="exposeMiniCart1"/> -+ -+ <waitForLoadingMaskToDisappear stepKey="waitForLoadingMaskForCartItem1"/> -+ <waitForElement selector="{{CheckoutPaymentSection.cartItemsAreaActive}}" time="30" stepKey="waitForCartItemsAreaActive1"/> -+ -+ <see selector="{{CheckoutPaymentSection.cartItems}}" userInput="$$createProduct.name$$" stepKey="seeProductInCar1t"/> -+ -+ <!-- See Custom options are displayed as option1 --> -+ -+ <conditionalClick selector="{{CheckoutPaymentSection.productOptionsByProductItemPrice('105')}}" dependentSelector="{{CheckoutPaymentSection.productOptionsActiveByProductItemPrice('105')}}" visible="false" stepKey="exposeProductOptions2"/> -+ <see selector="{{CheckoutPaymentSection.productOptionsActiveByProductItemPrice('105')}}" userInput="FR option1" stepKey="seeProductFrOptionValueDropdown1Input2"/> -+ -+ <!-- See Custom options are displayed as option2 --> -+ -+ <conditionalClick selector="{{CheckoutPaymentSection.productOptionsByProductItemPrice('150')}}" dependentSelector="{{CheckoutPaymentSection.productOptionsActiveByProductItemPrice('150')}}" visible="false" stepKey="exposeProductOptions3"/> -+ <see selector="{{CheckoutPaymentSection.productOptionsActiveByProductItemPrice('150')}}" userInput="FR option2" stepKey="seeProductFrOptionValueDropdown1Input3"/> -+ -+ <!-- Place Order --> -+ -+ <!--Select shipping method--> -+ <actionGroup ref="CheckoutSelectFlatRateShippingMethodActionGroup" stepKey="selectFlatRateShippingMethod2"/> -+ <waitForElementVisible selector="{{CheckoutShippingSection.next}}" time="30" stepKey="waitForNextButton2"/> -+ <click selector="{{CheckoutShippingSection.next}}" stepKey="clickNext2"/> -+ <waitForLoadingMaskToDisappear stepKey="waitForLoadingMaskAfterClickNext2"/> -+ -+ <!--Select payment method--> -+ <actionGroup ref="CheckoutSelectCheckMoneyOrderPaymentActionGroup" stepKey="selectPaymentMethod2"/> -+ <!-- Place Order --> -+ <actionGroup ref="CheckoutPlaceOrderActionGroup" stepKey="customerPlaceOrder2"> -+ <argument name="orderNumberMessage" value="CONST.successCheckoutOrderNumberMessage"/> -+ <argument name="emailYouMessage" value="CONST.successCheckoutEmailYouMessage"/> -+ </actionGroup> -+ -+ <!-- Open Product Grid, Filter product and open --> -+ -+ <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="amOnProductGridPage1"/> -+ <waitForPageLoad time="30" stepKey="waitForPageLoad15"/> -+ -+ <actionGroup ref="SearchForProductOnBackendActionGroup" stepKey="filterGroupedProductOptions1"> -+ <argument name="product" value="_defaultProduct"/> -+ </actionGroup> -+ -+ <click selector="{{AdminProductGridSection.productGridXRowYColumnButton('1', '2')}}" stepKey="openProductForEdit1"/> -+ <waitForPageLoad time="30" stepKey="waitForPageLoad16"/> -+ -+ <!-- Switcher to Store FR--> -+ -+ <scrollToTopOfPage stepKey="scrollToTopOfPage2"/> -+ <click selector="{{AdminProductFormActionSection.changeStoreButton}}" stepKey="clickStoreSwitcher1"/> -+ <click selector="{{AdminProductFormActionSection.selectStoreView(customStoreFR.name)}}" stepKey="clickStoreView1"/> -+ <click selector="{{AdminConfirmationModalSection.ok}}" stepKey="acceptMessage1"/> -+ -+ <!-- Open tab Customizable Options --> -+ -+ <waitForPageLoad time="30" stepKey="waitForPageLoad17"/> -+ <conditionalClick selector="{{AdminProductCustomizableOptionsSection.customizableOptions}}" dependentSelector="{{AdminProductCustomizableOptionsSection.checkIfCustomizableOptionsTabOpen}}" visible="true" stepKey="clickIfContentTabCloses4" /> -+ -+ <!-- Update Option Customizable Options and Option Value 1--> -+ -+ <waitForPageLoad time="30" stepKey="waitForPageLoad18"/> -+ <checkOption selector="{{AdminProductCustomizableOptionsSection.useDefaultOptionTitle}}" stepKey="checkUseDefaultOptionTitle"/> -+ <checkOption selector="{{AdminProductCustomizableOptionsSection.useDefaultOptionTitleByIndex('0')}}" stepKey="checkUseDefaultOptionValueTitle1"/> -+ -+ <!-- Update Product with Option Value 1 DropDown 1--> -+ -+ <waitForPageLoad time="30" stepKey="waitForPageLoad19"/> -+ <checkOption selector="{{AdminProductCustomizableOptionsSection.useDefaultOptionTitleByIndex('1')}}" stepKey="checkUseDefaultOptionValueTitle2"/> -+ <click selector="{{AdminProductFormActionSection.saveButton}}" stepKey="clickSaveButton3"/> -+ -+ <!--Go to Product Page--> -+ -+ <amOnPage url="{{StorefrontProductPage.url($$createProduct.custom_attributes[url_key]$$)}}" stepKey="amOnProduct2Page2"/> -+ -+ <seeElement selector="{{StorefrontProductInfoMainSection.productOptionDropDownTitle('Custom Options 1')}}" stepKey="seeProductOptionDropDownTitle1"/> -+ <seeElement selector="{{StorefrontProductInfoMainSection.productOptionDropDownOptionTitle('Custom Options 1', 'option1')}}" stepKey="seeProductOptionDropDownOptionTitle3"/> -+ <seeElement selector="{{StorefrontProductInfoMainSection.productOptionDropDownOptionTitle('Custom Options 1', 'option2')}}" stepKey="seeProductOptionDropDownOptionTitle4"/> -+ </test> -+</tests> -diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontPurchaseProductWithCustomOptions.xml b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontPurchaseProductWithCustomOptions.xml -deleted file mode 100644 -index 6e43753220d..00000000000 ---- a/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontPurchaseProductWithCustomOptions.xml -+++ /dev/null -@@ -1,178 +0,0 @@ --<?xml version="1.0" encoding="UTF-8"?> --<!-- -- /** -- * Copyright © Magento, Inc. All rights reserved. -- * See COPYING.txt for license details. -- */ ----> -- --<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -- xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Test/etc/testSchema.xsd"> -- <test name="StorefrontPurchaseProductWithCustomOptions"> -- <annotations> -- <features value="Catalog"/> -- <stories value="Purchase a product with Custom Options of different types"/> -- <title value="Admin should be able to sell products with different variants of their own"/> -- <description value="Admin should be able to sell products with different variants of their own"/> -- <severity value="CRITICAL"/> -- <testCaseId value="MAGETWO-61717"/> -- <group value="Catalog"/> -- </annotations> -- <before> -- <createData entity="Simple_US_Customer" stepKey="createCustomer"/> -- </before> -- <after> -- <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> -- </after> -- -- <!--Create Simple Product with Custom Options--> -- -- <createData entity="_defaultCategory" stepKey="createCategory"/> -- <createData entity="_defaultProduct" stepKey="createProduct"> -- <requiredEntity createDataKey="createCategory"/> -- <field key="price">17</field> -- </createData> -- <updateData createDataKey="createProduct" entity="productWithOptions" stepKey="updateProductWithOption"/> -- -- <!-- Login Customer Storeront --> -- -- <amOnPage url="{{StorefrontCustomerSignInPage.url}}" stepKey="amOnSignInPage"/> -- <fillField userInput="$$createCustomer.email$$" selector="{{StorefrontCustomerSignInFormSection.emailField}}" stepKey="fillEmail"/> -- <fillField userInput="$$createCustomer.password$$" selector="{{StorefrontCustomerSignInFormSection.passwordField}}" stepKey="fillPassword"/> -- <click selector="{{StorefrontCustomerSignInFormSection.signInAccountButton}}" stepKey="clickSignInAccountButton"/> -- -- <!-- Checking the correctness of displayed prices for user parameters --> -- -- <amOnPage url="{{StorefrontHomePage.url}}$createProduct.custom_attributes[url_key]$.html" stepKey="amOnProduct3Page"/> -- -- <seeElement selector="{{StorefrontProductInfoMainSection.productAttributeOptionsPrice(ProductOptionField.title, ProductOptionField.price)}}" stepKey="checkFieldProductOption"/> -- <seeElement selector="{{StorefrontProductInfoMainSection.productAttributeOptionsPrice(ProductOptionArea.title, '1.7')}}" stepKey="checkAreaProductOption"/> -- <seeElement selector="{{StorefrontProductInfoMainSection.productAttributeOptionsDropDown(ProductOptionDropDown.title, ProductOptionValueDropdown1.price)}}" stepKey="checkDropDownProductOption"/> -- <seeElement selector="{{StorefrontProductInfoMainSection.productAttributeOptionsRadioButtons(ProductOptionRadiobutton.title, ProductOptionValueRadioButtons1.price)}}" stepKey="checkButtonsProductOption"/> -- <seeElement selector="{{StorefrontProductInfoMainSection.productAttributeOptionsCheckbox(ProductOptionCheckbox.title, '20.91')}}" stepKey="checkCheckboxProductOptiozn"/> -- <seeElement selector="{{StorefrontProductInfoMainSection.productAttributeOptionsMultiselect(ProductOptionMultiSelect.title, ProductOptionValueMultiSelect1.price)}}" stepKey="checkMultiSelectProductOption"/> -- <seeElement selector="{{StorefrontProductInfoMainSection.productAttributeOptionsData(ProductOptionDate.title, ProductOptionDate.price)}}" stepKey="checkDataProductOption"/> -- -- <!-- Adding items to the checkout --> -- -- <fillField userInput="OptionField" selector="{{StorefrontProductInfoMainSection.productOptionFieldInput(ProductOptionField.title)}}" stepKey="fillProductOptionInputField"/> -- <fillField userInput="OptionArea" selector="{{StorefrontProductInfoMainSection.productOptionAreaInput(ProductOptionArea.title)}}" stepKey="fillProductOptionInputArea"/> -- <attachFile userInput="{{productWithOptions.file}}" selector="{{StorefrontProductInfoMainSection.addLinkFileUploadFile(ProductOptionFile.title)}}" stepKey="fillUploadFile"/> -- <selectOption userInput="{{ProductOptionValueDropdown1.price}}" selector="{{StorefrontProductInfoMainSection.productOptionSelect(ProductOptionDropDown.title)}}" stepKey="seeProductOptionDropDown"/> -- <checkOption selector="{{StorefrontProductInfoMainSection.productOptionRadioButtonsCheckbox(ProductOptionRadiobutton.title, ProductOptionValueRadioButtons1.price)}}" stepKey="seeProductOptionRadioButtons"/> -- <checkOption selector="{{StorefrontProductInfoMainSection.productOptionRadioButtonsCheckbox(ProductOptionCheckbox.title, '20.91')}}" stepKey="seeProductOptionCheckbox"/> -- <selectOption userInput="{{ProductOptionValueMultiSelect1.price}}" selector="{{StorefrontProductInfoMainSection.productOptionSelect(ProductOptionMultiSelect.title)}}" stepKey="selectProductOptionMultiSelect"/> -- <selectOption userInput="01" selector="{{StorefrontProductInfoMainSection.productOptionDataMonth(ProductOptionDate.title)}}" stepKey="selectProductOptionDate"/> -- <selectOption userInput="01" selector="{{StorefrontProductInfoMainSection.productOptionDataDay(ProductOptionDate.title)}}" stepKey="selectProductOptionDate1"/> -- <selectOption userInput="2018" selector="{{StorefrontProductInfoMainSection.productOptionDataYear(ProductOptionDate.title)}}" stepKey="selectProductOptionDate2"/> -- <selectOption userInput="01" selector="{{StorefrontProductInfoMainSection.productOptionDateAndTimeMonth(ProductOptionDateTime.title)}}" stepKey="selectProductOptionDateAndTimeMonth"/> -- <selectOption userInput="01" selector="{{StorefrontProductInfoMainSection.productOptionDateAndTimeDay(ProductOptionDateTime.title)}}" stepKey="selectProductOptionDateAndTimeDay"/> -- <selectOption userInput="2018" selector="{{StorefrontProductInfoMainSection.productOptionDateAndTimeYear(ProductOptionDateTime.title)}}" stepKey="selectProductOptionDateAndTimeYear"/> -- <selectOption userInput="01" selector="{{StorefrontProductInfoMainSection.productOptionDateAndTimeHour(ProductOptionDateTime.title)}}" stepKey="selectProductOptionDateAndTimeHour"/> -- <selectOption userInput="00" selector="{{StorefrontProductInfoMainSection.productOptionDateAndTimeMinute(ProductOptionDateTime.title)}}" stepKey="selectProductOptionDateAndTimeMinute"/> -- <selectOption userInput="01" selector="{{StorefrontProductInfoMainSection.productOptionTimeHour(ProductOptionTime.title)}}" stepKey="selectProductOptionTimeHour"/> -- <selectOption userInput="00" selector="{{StorefrontProductInfoMainSection.productOptionTimeMinute(ProductOptionTime.title)}}" stepKey="selectProductOptionTimeMinute"/> -- -- <grabTextFrom selector="{{StorefrontProductInfoMainSection.productPrice}}" stepKey="finalProductPrice"/> -- -- <actionGroup ref="StorefrontAddToCartCustomOptionsProductPageActionGroup" stepKey="addToCartFromStorefrontProductPage"> -- <argument name="productName" value="$createProduct.name$"/> -- </actionGroup> -- -- <!-- Checking the correctness of displayed custom options for user parameters on checkout --> -- -- <actionGroup ref="GoToCheckoutFromMinicartActionGroup" stepKey="goToCheckoutFromMinicart" /> -- -- <conditionalClick selector="{{CheckoutPaymentSection.cartItemsArea}}" dependentSelector="{{CheckoutPaymentSection.cartItemsArea}}" visible="true" stepKey="exposeMiniCart"/> -- -- <waitForLoadingMaskToDisappear stepKey="waitForLoadingMaskForCartItem"/> -- <waitForElement selector="{{CheckoutPaymentSection.cartItemsAreaActive}}" time="30" stepKey="waitForCartItemsAreaActive"/> -- -- <see selector="{{CheckoutPaymentSection.cartItems}}" userInput="$createProduct.name$" stepKey="seeProductInCart"/> -- -- <conditionalClick selector="{{CheckoutPaymentSection.ProductOptionsByProductItemName($createProduct.name$)}}" dependentSelector="{{CheckoutPaymentSection.ProductOptionsActiveByProductItemName($createProduct.name$)}}" visible="false" stepKey="exposeProductOptions"/> -- -- <see selector="{{CheckoutPaymentSection.ProductOptionsActiveByProductItemName($createProduct.name$)}}" userInput="{{ProductOptionField.title}}" stepKey="seeProductOptionFieldInput1"/> -- <see selector="{{CheckoutPaymentSection.ProductOptionsActiveByProductItemName($createProduct.name$)}}" userInput="{{ProductOptionArea.title}}" stepKey="seeProductOptionAreaInput1"/> -- <see selector="{{CheckoutPaymentSection.ProductOptionsActiveByProductItemName($createProduct.name$)}}" userInput="{{productWithOptions.file}}" stepKey="seeProductOptionFileInput1"/> -- <see selector="{{CheckoutPaymentSection.ProductOptionsActiveByProductItemName($createProduct.name$)}}" userInput="{{ProductOptionValueDropdown1.title}}" stepKey="seeProductOptionValueDropdown1Input1"/> -- <see selector="{{CheckoutPaymentSection.ProductOptionsActiveByProductItemName($createProduct.name$)}}" userInput="{{ProductOptionValueRadioButtons1.title}}" stepKey="seeProductOptionValueRadioButtons1Input1"/> -- <see selector="{{CheckoutPaymentSection.ProductOptionsActiveByProductItemName($createProduct.name$)}}" userInput="{{ProductOptionValueCheckbox.title}}" stepKey="seeProductOptionValueCheckboxInput1" /> -- <see selector="{{CheckoutPaymentSection.ProductOptionsActiveByProductItemName($createProduct.name$)}}" userInput="{{ProductOptionValueMultiSelect1.title}}" stepKey="seeproductAttributeOptionsMultiselect1Input1" /> -- <see selector="{{CheckoutPaymentSection.ProductOptionsActiveByProductItemName($createProduct.name$)}}" userInput="Jan 1, 2018" stepKey="seeProductOptionDateAndTimeInput" /> -- <see selector="{{CheckoutPaymentSection.ProductOptionsActiveByProductItemName($createProduct.name$)}}" userInput="1/1/18, 1:00 AM" stepKey="seeProductOptionDataInput" /> -- <see selector="{{CheckoutPaymentSection.ProductOptionsActiveByProductItemName($createProduct.name$)}}" userInput="1:00 AM" stepKey="seeProductOptionTimeInput" /> -- -- <click selector="{{CheckoutShippingSection.next}}" stepKey="clickNext"/> -- -- <!-- Place Order --> -- -- <waitForElement selector="{{CheckoutPaymentSection.placeOrder}}" time="30" stepKey="waitForPlaceOrderButton"/> -- <click selector="{{CheckoutPaymentSection.placeOrder}}" stepKey="clickPlaceOrder"/> -- -- <grabTextFrom selector="{{CheckoutSuccessMainSection.orderNumber22}}" stepKey="grabOrderNumber"/> -- -- <!-- Login to Admin and open Order --> -- -- <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin1"/> -- -- <amOnPage url="{{AdminOrdersPage.url}}" stepKey="onOrdersPage"/> -- <waitForLoadingMaskToDisappear stepKey="waitForLoadingMaskToDisappearOnOrdersPage"/> -- <fillField selector="{{AdminOrdersGridSection.search}}" userInput="{$grabOrderNumber}" stepKey="fillOrderNum"/> -- <click selector="{{AdminOrdersGridSection.submitSearch}}" stepKey="submitSearchOrderNum"/> -- <waitForLoadingMaskToDisappear stepKey="waitForLoadingMaskToDisappearOnSearch"/> -- <click selector="{{AdminOrdersGridSection.firstRow}}" stepKey="clickOrderRow"/> -- -- <!-- Checking the correctness of displayed custom options for user parameters on Order --> -- -- <see selector="{{AdminOrderItemsOrderedSection.productNameOptions}}" userInput="{{ProductOptionField.title}}" stepKey="seeAdminOrderProductOptionField" /> -- <see selector="{{AdminOrderItemsOrderedSection.productNameOptions}}" userInput="{{ProductOptionArea.title}}" stepKey="seeAdminOrderProductOptionArea"/> -- <see selector="{{AdminOrderItemsOrderedSection.productNameOptions}}" userInput="{{productWithOptions.file}}" stepKey="seeAdminOrderProductOptionFile"/> -- <see selector="{{AdminOrderItemsOrderedSection.productNameOptions}}" userInput="{{ProductOptionValueDropdown1.title}}" stepKey="seeAdminOrderProductOptionValueDropdown1"/> -- <see selector="{{AdminOrderItemsOrderedSection.productNameOptions}}" userInput="{{ProductOptionValueRadioButtons1.title}}" stepKey="seeAdminOrderProductOptionValueRadioButton1"/> -- <see selector="{{AdminOrderItemsOrderedSection.productNameOptions}}" userInput="{{ProductOptionValueCheckbox.title}}" stepKey="seeAdminOrderProductOptionValueCheckbox" /> -- <see selector="{{AdminOrderItemsOrderedSection.productNameOptions}}" userInput="{{ProductOptionValueMultiSelect1.title}}" stepKey="seeAdminOrderproductAttributeOptionsMultiselect1" /> -- <see selector="{{AdminOrderItemsOrderedSection.productNameOptions}}" userInput="Jan 1, 2018" stepKey="seeAdminOrderProductOptionDateAndTime" /> -- <see selector="{{AdminOrderItemsOrderedSection.productNameOptions}}" userInput="1/1/18, 1:00 AM" stepKey="seeAdminOrderProductOptionData" /> -- <see selector="{{AdminOrderItemsOrderedSection.productNameOptions}}" userInput="1:00 AM" stepKey="seeAdminOrderProductOptionTime" /> -- -- <!-- Reorder and Checking the correctness of displayed custom options for user parameters on Order and correctness of displayed price Subtotal--> -- -- <click selector="{{AdminOrderDetailsMainActionsSection.reorder}}" stepKey="clickReorder"/> -- <click selector="{{AdminOrderFormActionSection.SubmitOrder}}" stepKey="trySubmitOrder"/> -- -- <see selector="{{AdminOrderItemsOrderedSection.productNameOptions}}" userInput="{{ProductOptionField.title}}" stepKey="seeAdminOrderProductOptionField1" /> -- <see selector="{{AdminOrderItemsOrderedSection.productNameOptions}}" userInput="{{ProductOptionArea.title}}" stepKey="seeAdminOrderProductOptionArea1"/> -- <see selector="{{AdminOrderItemsOrderedSection.productNameOptions}}" userInput="{{productWithOptions.file}}" stepKey="seeAdminOrderProductOptionFile1"/> -- <see selector="{{AdminOrderItemsOrderedSection.productNameOptions}}" userInput="{{ProductOptionValueDropdown1.title}}" stepKey="seeAdminOrderProductOptionValueDropdown11"/> -- <see selector="{{AdminOrderItemsOrderedSection.productNameOptions}}" userInput="{{ProductOptionValueRadioButtons1.title}}" stepKey="seeAdminOrderProductOptionValueRadioButton11"/> -- <see selector="{{AdminOrderItemsOrderedSection.productNameOptions}}" userInput="{{ProductOptionValueCheckbox.title}}" stepKey="seeAdminOrderProductOptionValueCheckbox1" /> -- <see selector="{{AdminOrderItemsOrderedSection.productNameOptions}}" userInput="{{ProductOptionValueMultiSelect1.title}}" stepKey="seeAdminOrderproductAttributeOptionsMultiselect11" /> -- <see selector="{{AdminOrderItemsOrderedSection.productNameOptions}}" userInput="Jan 1, 2018" stepKey="seeAdminOrderProductOptionDateAndTime1" /> -- <see selector="{{AdminOrderItemsOrderedSection.productNameOptions}}" userInput="1/1/18, 1:00 AM" stepKey="seeAdminOrderProductOptionData1" /> -- <see selector="{{AdminOrderItemsOrderedSection.productNameOptions}}" userInput="1:00 AM" stepKey="seeAdminOrderProductOptionTime1" /> -- -- <see selector="{{AdminOrderTotalSection.subTotal}}" userInput="{$finalProductPrice}" stepKey="seeOrderSubTotal"/> -- -- <!-- Go to Customer Order Page and Checking the correctness of displayed custom options for user parameters on Order --> -- -- <amOnPage url="{{StorefrontCustomerOrderViewPage.url({$grabOrderNumber})}}" stepKey="amOnProduct4Page"/> -- -- <see selector="{{StorefrontCustomerOrderSection.productCustomOptions($createProduct.name$, ProductOptionField.title, ProductOptionField.title)}}" userInput="{{ProductOptionField.title}}" stepKey="seeStorefontOrderProductOptionField1" /> -- <see selector="{{StorefrontCustomerOrderSection.productCustomOptions($createProduct.name$, ProductOptionArea.title, ProductOptionArea.title)}}" userInput="{{ProductOptionArea.title}}" stepKey="seeStorefontOrderProductOptionArea1"/> -- <see selector="{{StorefrontCustomerOrderSection.productCustomOptionsFile($createProduct.name$, ProductOptionFile.title, productWithOptions.file)}}" userInput="{{productWithOptions.file}}" stepKey="seeStorefontOrderProductOptionFile1"/> -- <see selector="{{StorefrontCustomerOrderSection.productCustomOptions($createProduct.name$, ProductOptionDropDown.title, ProductOptionValueDropdown1.title)}}" userInput="{{ProductOptionValueDropdown1.title}}" stepKey="seeStorefontOrderProductOptionValueDropdown11"/> -- <see selector="{{StorefrontCustomerOrderSection.productCustomOptions($createProduct.name$, ProductOptionRadiobutton.title, ProductOptionValueRadioButtons1.title)}}" userInput="{{ProductOptionValueRadioButtons1.title}}" stepKey="seeStorefontOrderProductOptionValueRadioButtons11"/> -- <see selector="{{StorefrontCustomerOrderSection.productCustomOptions($createProduct.name$, ProductOptionCheckbox.title, ProductOptionValueCheckbox.title)}}" userInput="{{ProductOptionValueCheckbox.title}}" stepKey="seeStorefontOrderProductOptionValueCheckbox1" /> -- <see selector="{{StorefrontCustomerOrderSection.productCustomOptions($createProduct.name$, ProductOptionMultiSelect.title, ProductOptionValueMultiSelect1.title)}}" userInput="{{ProductOptionValueMultiSelect1.title}}" stepKey="seeStorefontOrderproductAttributeOptionsMultiselect11" /> -- <see selector="{{StorefrontCustomerOrderSection.productCustomOptions($createProduct.name$, ProductOptionDate.title, 'Jan 1, 2018')}}" userInput="Jan 1, 2018" stepKey="seeStorefontOrderProductOptionDateAndTime1" /> -- <see selector="{{StorefrontCustomerOrderSection.productCustomOptions($createProduct.name$, ProductOptionDateTime.title, '1/1/18, 1:00 AM')}}" userInput="1/1/18, 1:00 AM" stepKey="seeStorefontOrderProductOptionData1" /> -- <see selector="{{StorefrontCustomerOrderSection.productCustomOptions($createProduct.name$, ProductOptionTime.title, '1:00 AM')}}" userInput="1:00 AM" stepKey="seeStorefontOrderProductOptionTime1" /> -- -- <!-- Delete product and category --> -- -- <deleteData createDataKey="createProduct" stepKey="deleteProduct"/> -- <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> -- -- </test> --</tests> -\ No newline at end of file -diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontPurchaseProductWithCustomOptionsTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontPurchaseProductWithCustomOptionsTest.xml -new file mode 100644 -index 00000000000..12a465188aa ---- /dev/null -+++ b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontPurchaseProductWithCustomOptionsTest.xml -@@ -0,0 +1,189 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> -+ <test name="StorefrontPurchaseProductWithCustomOptionsTest"> -+ <annotations> -+ <features value="Catalog"/> -+ <stories value="Purchase a product with Custom Options of different types"/> -+ <title value="Admin should be able to sell products with custom options of different types"/> -+ <description value="Admin should be able to sell products with custom options of different types"/> -+ <severity value="CRITICAL"/> -+ <testCaseId value="MC-16462"/> -+ <group value="Catalog"/> -+ </annotations> -+ <before> -+ <createData entity="Simple_US_Customer" stepKey="createCustomer"/> -+ <!--Create Simple Product with Custom Options--> -+ <createData entity="_defaultCategory" stepKey="createCategory"/> -+ <createData entity="_defaultProduct" stepKey="createProduct"> -+ <requiredEntity createDataKey="createCategory"/> -+ <field key="price">17</field> -+ </createData> -+ <updateData createDataKey="createProduct" entity="productWithOptions" stepKey="updateProductWithOption"/> -+ <!-- Logout customer before in case of it logged in from previous test --> -+ <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="customerLogoutStorefront"/> -+ </before> -+ <after> -+ <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> -+ <!-- Delete product and category --> -+ <deleteData createDataKey="createProduct" stepKey="deleteProduct"/> -+ <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> -+ <actionGroup ref="AdminOrdersGridClearFiltersActionGroup" stepKey="clearOrderListingFilters"/> -+ <actionGroup ref="logout" stepKey="logoutAdmin"/> -+ <!-- Logout customer --> -+ <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="customerLogoutStorefront"/> -+ </after> -+ -+ <!-- Login Customer Storefront --> -+ -+ <actionGroup ref="LoginToStorefrontActionGroup" stepKey="loginCustomerOnStorefront"> -+ <argument name="Customer" value="$$createCustomer$$"/> -+ </actionGroup> -+ -+ <!-- Checking the correctness of displayed prices for user parameters --> -+ -+ <amOnPage url="{{StorefrontProductPage.url($$createProduct.custom_attributes[url_key]$$)}}" stepKey="navigateToProductPage"/> -+ -+ <seeElement selector="{{StorefrontProductInfoMainSection.productAttributeOptionsPrice(ProductOptionField.title, ProductOptionField.price)}}" stepKey="checkFieldProductOption"/> -+ <seeElement selector="{{StorefrontProductInfoMainSection.productAttributeOptionsPrice(ProductOptionArea.title, '1.7')}}" stepKey="checkAreaProductOption"/> -+ <seeElement selector="{{StorefrontProductInfoMainSection.productAttributeOptionsDropDown(ProductOptionDropDown.title, ProductOptionValueDropdown1.price)}}" stepKey="checkDropDownProductOption"/> -+ <seeElement selector="{{StorefrontProductInfoMainSection.productAttributeOptionsRadioButtons(ProductOptionRadiobutton.title, ProductOptionValueRadioButtons1.price)}}" stepKey="checkButtonsProductOption"/> -+ <seeElement selector="{{StorefrontProductInfoMainSection.productAttributeOptionsCheckbox(ProductOptionCheckbox.title, '20.91')}}" stepKey="checkCheckboxProductOptiozn"/> -+ <seeElement selector="{{StorefrontProductInfoMainSection.productAttributeOptionsMultiselect(ProductOptionMultiSelect.title, ProductOptionValueMultiSelect1.price)}}" stepKey="checkMultiSelectProductOption"/> -+ <seeElement selector="{{StorefrontProductInfoMainSection.productAttributeOptionsData(ProductOptionDate.title, ProductOptionDate.price)}}" stepKey="checkDataProductOption"/> -+ -+ <!--Generate year--> -+ <generateDate date="Now" format="Y" stepKey="year"/> -+ <generateDate date="Now" format="y" stepKey="shortYear"/> -+ -+ <!-- Adding items to the checkout --> -+ -+ <fillField userInput="OptionField" selector="{{StorefrontProductInfoMainSection.productOptionFieldInput(ProductOptionField.title)}}" stepKey="fillProductOptionInputField"/> -+ <fillField userInput="OptionArea" selector="{{StorefrontProductInfoMainSection.productOptionAreaInput(ProductOptionArea.title)}}" stepKey="fillProductOptionInputArea"/> -+ <attachFile userInput="{{productWithOptions.file}}" selector="{{StorefrontProductInfoMainSection.addLinkFileUploadFile(ProductOptionFile.title)}}" stepKey="fillUploadFile"/> -+ <selectOption userInput="{{ProductOptionValueDropdown1.price}}" selector="{{StorefrontProductInfoMainSection.productOptionSelect(ProductOptionDropDown.title)}}" stepKey="seeProductOptionDropDown"/> -+ <checkOption selector="{{StorefrontProductInfoMainSection.productOptionRadioButtonsCheckbox(ProductOptionRadiobutton.title, ProductOptionValueRadioButtons1.price)}}" stepKey="seeProductOptionRadioButtons"/> -+ <checkOption selector="{{StorefrontProductInfoMainSection.productOptionRadioButtonsCheckbox(ProductOptionCheckbox.title, '20.91')}}" stepKey="seeProductOptionCheckbox"/> -+ <selectOption userInput="{{ProductOptionValueMultiSelect1.price}}" selector="{{StorefrontProductInfoMainSection.productOptionSelect(ProductOptionMultiSelect.title)}}" stepKey="selectProductOptionMultiSelect"/> -+ <selectOption userInput="01" selector="{{StorefrontProductInfoMainSection.productOptionDataMonth(ProductOptionDate.title)}}" stepKey="selectProductOptionDate"/> -+ <selectOption userInput="01" selector="{{StorefrontProductInfoMainSection.productOptionDataDay(ProductOptionDate.title)}}" stepKey="selectProductOptionDate1"/> -+ <selectOption userInput="$year" selector="{{StorefrontProductInfoMainSection.productOptionDataYear(ProductOptionDate.title)}}" stepKey="selectProductOptionDate2"/> -+ <selectOption userInput="01" selector="{{StorefrontProductInfoMainSection.productOptionDateAndTimeMonth(ProductOptionDateTime.title)}}" stepKey="selectProductOptionDateAndTimeMonth"/> -+ <selectOption userInput="01" selector="{{StorefrontProductInfoMainSection.productOptionDateAndTimeDay(ProductOptionDateTime.title)}}" stepKey="selectProductOptionDateAndTimeDay"/> -+ <selectOption userInput="$year" selector="{{StorefrontProductInfoMainSection.productOptionDateAndTimeYear(ProductOptionDateTime.title)}}" stepKey="selectProductOptionDateAndTimeYear"/> -+ <selectOption userInput="01" selector="{{StorefrontProductInfoMainSection.productOptionDateAndTimeHour(ProductOptionDateTime.title)}}" stepKey="selectProductOptionDateAndTimeHour"/> -+ <selectOption userInput="00" selector="{{StorefrontProductInfoMainSection.productOptionDateAndTimeMinute(ProductOptionDateTime.title)}}" stepKey="selectProductOptionDateAndTimeMinute"/> -+ <selectOption userInput="01" selector="{{StorefrontProductInfoMainSection.productOptionTimeHour(ProductOptionTime.title)}}" stepKey="selectProductOptionTimeHour"/> -+ <selectOption userInput="00" selector="{{StorefrontProductInfoMainSection.productOptionTimeMinute(ProductOptionTime.title)}}" stepKey="selectProductOptionTimeMinute"/> -+ -+ <grabTextFrom selector="{{StorefrontProductInfoMainSection.productPrice}}" stepKey="finalProductPrice"/> -+ -+ <actionGroup ref="StorefrontAddToCartCustomOptionsProductPageActionGroup" stepKey="addToCartFromStorefrontProductPage"> -+ <argument name="productName" value="$$createProduct.name$$"/> -+ </actionGroup> -+ -+ <!-- Checking the correctness of displayed custom options for user parameters on checkout --> -+ -+ <actionGroup ref="GoToCheckoutFromMinicartActionGroup" stepKey="goToCheckoutFromMinicart" /> -+ -+ <conditionalClick selector="{{CheckoutPaymentSection.cartItemsArea}}" dependentSelector="{{CheckoutPaymentSection.cartItemsArea}}" visible="true" stepKey="exposeMiniCart"/> -+ -+ <waitForLoadingMaskToDisappear stepKey="waitForLoadingMaskForCartItem"/> -+ <waitForElement selector="{{CheckoutPaymentSection.cartItemsAreaActive}}" time="30" stepKey="waitForCartItemsAreaActive"/> -+ -+ <see selector="{{CheckoutPaymentSection.cartItems}}" userInput="$$createProduct.name$$" stepKey="seeProductInCart"/> -+ -+ <conditionalClick selector="{{CheckoutPaymentSection.ProductOptionsByProductItemName($$createProduct.name$$)}}" dependentSelector="{{CheckoutPaymentSection.ProductOptionsActiveByProductItemName($$createProduct.name$$)}}" visible="false" stepKey="exposeProductOptions"/> -+ -+ <see selector="{{CheckoutPaymentSection.ProductOptionsActiveByProductItemName($$createProduct.name$$)}}" userInput="{{ProductOptionField.title}}" stepKey="seeProductOptionFieldInput1"/> -+ <see selector="{{CheckoutPaymentSection.ProductOptionsActiveByProductItemName($$createProduct.name$$)}}" userInput="{{ProductOptionArea.title}}" stepKey="seeProductOptionAreaInput1"/> -+ <see selector="{{CheckoutPaymentSection.ProductOptionsActiveByProductItemName($$createProduct.name$$)}}" userInput="{{productWithOptions.file}}" stepKey="seeProductOptionFileInput1"/> -+ <seeElement selector="{{CheckoutPaymentSection.ProductOptionLinkActiveByProductItemName($$createProduct.name$$, productWithOptions.file)}}" stepKey="seeProductOptionFileInputLink1"/> -+ <see selector="{{CheckoutPaymentSection.ProductOptionsActiveByProductItemName($$createProduct.name$$)}}" userInput="{{ProductOptionValueDropdown1.title}}" stepKey="seeProductOptionValueDropdown1Input1"/> -+ <see selector="{{CheckoutPaymentSection.ProductOptionsActiveByProductItemName($$createProduct.name$$)}}" userInput="{{ProductOptionValueRadioButtons1.title}}" stepKey="seeProductOptionValueRadioButtons1Input1"/> -+ <see selector="{{CheckoutPaymentSection.ProductOptionsActiveByProductItemName($$createProduct.name$$)}}" userInput="{{ProductOptionValueCheckbox.title}}" stepKey="seeProductOptionValueCheckboxInput1" /> -+ <see selector="{{CheckoutPaymentSection.ProductOptionsActiveByProductItemName($$createProduct.name$$)}}" userInput="{{ProductOptionValueMultiSelect1.title}}" stepKey="seeproductAttributeOptionsMultiselect1Input1" /> -+ <see selector="{{CheckoutPaymentSection.ProductOptionsActiveByProductItemName($$createProduct.name$$)}}" userInput="Jan 1, $year" stepKey="seeProductOptionDateAndTimeInput" /> -+ <see selector="{{CheckoutPaymentSection.ProductOptionsActiveByProductItemName($$createProduct.name$$)}}" userInput="1/1/$shortYear, 1:00 AM" stepKey="seeProductOptionDataInput" /> -+ <see selector="{{CheckoutPaymentSection.ProductOptionsActiveByProductItemName($$createProduct.name$$)}}" userInput="1:00 AM" stepKey="seeProductOptionTimeInput" /> -+ <!--Select shipping method--> -+ <actionGroup ref="CheckoutSelectFlatRateShippingMethodActionGroup" stepKey="selectFlatRateShippingMethod"/> -+ <waitForElement selector="{{CheckoutShippingSection.next}}" time="30" stepKey="waitForNextButton"/> -+ <click selector="{{CheckoutShippingSection.next}}" stepKey="clickNext"/> -+ <waitForLoadingMaskToDisappear stepKey="waitForLoadingMaskAfterClickNext"/> -+ <!--Select payment method--> -+ <actionGroup ref="CheckoutSelectCheckMoneyOrderPaymentActionGroup" stepKey="selectPaymentMethod"/> -+ <!-- Place Order --> -+ <waitForElementVisible selector="{{CheckoutPaymentSection.placeOrder}}" time="30" stepKey="waitForPlaceOrderButton"/> -+ <click selector="{{CheckoutPaymentSection.placeOrder}}" stepKey="clickPlaceOrder"/> -+ -+ <grabTextFrom selector="{{CheckoutSuccessMainSection.orderNumber22}}" stepKey="grabOrderNumber"/> -+ -+ <!-- Login to Admin and open Order --> -+ -+ <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin1"/> -+ -+ <actionGroup ref="filterOrderGridById" stepKey="filterByOrderId"> -+ <argument name="orderId" value="$grabOrderNumber"/> -+ </actionGroup> -+ <click selector="{{AdminDataGridTableSection.firstRow}}" stepKey="clickOrderRow"/> -+ <waitForPageLoad stepKey="waitForOrderPageOpened"/> -+ -+ <!-- Checking the correctness of displayed custom options for user parameters on Order --> -+ -+ <see selector="{{AdminOrderItemsOrderedSection.productNameOptions}}" userInput="{{ProductOptionField.title}}" stepKey="seeAdminOrderProductOptionField" /> -+ <see selector="{{AdminOrderItemsOrderedSection.productNameOptions}}" userInput="{{ProductOptionArea.title}}" stepKey="seeAdminOrderProductOptionArea"/> -+ <see selector="{{AdminOrderItemsOrderedSection.productNameOptions}}" userInput="{{productWithOptions.file}}" stepKey="seeAdminOrderProductOptionFile"/> -+ <seeElement selector="{{AdminOrderItemsOrderedSection.productNameOptionsLink(productWithOptions.file)}}" stepKey="seeAdminOrderProductOptionFileLink"/> -+ <see selector="{{AdminOrderItemsOrderedSection.productNameOptions}}" userInput="{{ProductOptionValueDropdown1.title}}" stepKey="seeAdminOrderProductOptionValueDropdown1"/> -+ <see selector="{{AdminOrderItemsOrderedSection.productNameOptions}}" userInput="{{ProductOptionValueRadioButtons1.title}}" stepKey="seeAdminOrderProductOptionValueRadioButton1"/> -+ <see selector="{{AdminOrderItemsOrderedSection.productNameOptions}}" userInput="{{ProductOptionValueCheckbox.title}}" stepKey="seeAdminOrderProductOptionValueCheckbox" /> -+ <see selector="{{AdminOrderItemsOrderedSection.productNameOptions}}" userInput="{{ProductOptionValueMultiSelect1.title}}" stepKey="seeAdminOrderproductAttributeOptionsMultiselect1" /> -+ <see selector="{{AdminOrderItemsOrderedSection.productNameOptions}}" userInput="Jan 1, $year" stepKey="seeAdminOrderProductOptionDateAndTime" /> -+ <see selector="{{AdminOrderItemsOrderedSection.productNameOptions}}" userInput="1/1/$shortYear, 1:00 AM" stepKey="seeAdminOrderProductOptionData" /> -+ <see selector="{{AdminOrderItemsOrderedSection.productNameOptions}}" userInput="1:00 AM" stepKey="seeAdminOrderProductOptionTime" /> -+ -+ <!-- Reorder and Checking the correctness of displayed custom options for user parameters on Order and correctness of displayed price Subtotal--> -+ -+ <click selector="{{AdminOrderDetailsMainActionsSection.reorder}}" stepKey="clickReorder"/> -+ <actionGroup ref="AdminCheckoutSelectCheckMoneyOrderBillingMethodActionGroup" stepKey="selectBillingMethod"/> -+ <click selector="{{AdminOrderFormActionSection.SubmitOrder}}" stepKey="trySubmitOrder"/> -+ -+ <see selector="{{AdminOrderItemsOrderedSection.productNameOptions}}" userInput="{{ProductOptionField.title}}" stepKey="seeAdminOrderProductOptionField1" /> -+ <see selector="{{AdminOrderItemsOrderedSection.productNameOptions}}" userInput="{{ProductOptionArea.title}}" stepKey="seeAdminOrderProductOptionArea1"/> -+ <see selector="{{AdminOrderItemsOrderedSection.productNameOptions}}" userInput="{{productWithOptions.file}}" stepKey="seeAdminOrderProductOptionFile1"/> -+ <seeElement selector="{{AdminOrderItemsOrderedSection.productNameOptionsLink(productWithOptions.file)}}" stepKey="seeAdminOrderProductOptionFileLink1"/> -+ <see selector="{{AdminOrderItemsOrderedSection.productNameOptions}}" userInput="{{ProductOptionValueDropdown1.title}}" stepKey="seeAdminOrderProductOptionValueDropdown11"/> -+ <see selector="{{AdminOrderItemsOrderedSection.productNameOptions}}" userInput="{{ProductOptionValueRadioButtons1.title}}" stepKey="seeAdminOrderProductOptionValueRadioButton11"/> -+ <see selector="{{AdminOrderItemsOrderedSection.productNameOptions}}" userInput="{{ProductOptionValueCheckbox.title}}" stepKey="seeAdminOrderProductOptionValueCheckbox1" /> -+ <see selector="{{AdminOrderItemsOrderedSection.productNameOptions}}" userInput="{{ProductOptionValueMultiSelect1.title}}" stepKey="seeAdminOrderproductAttributeOptionsMultiselect11" /> -+ <see selector="{{AdminOrderItemsOrderedSection.productNameOptions}}" userInput="Jan 1, $year" stepKey="seeAdminOrderProductOptionDateAndTime1" /> -+ <see selector="{{AdminOrderItemsOrderedSection.productNameOptions}}" userInput="1/1/$shortYear, 1:00 AM" stepKey="seeAdminOrderProductOptionData1" /> -+ <see selector="{{AdminOrderItemsOrderedSection.productNameOptions}}" userInput="1:00 AM" stepKey="seeAdminOrderProductOptionTime1" /> -+ -+ <see selector="{{AdminOrderTotalSection.subTotal}}" userInput="{$finalProductPrice}" stepKey="seeOrderSubTotal"/> -+ -+ <!-- Go to Customer Order Page and Checking the correctness of displayed custom options for user parameters on Order --> -+ -+ <amOnPage url="{{StorefrontCustomerOrderViewPage.url({$grabOrderNumber})}}" stepKey="amOnOrderPage"/> -+ -+ <see selector="{{StorefrontCustomerOrderSection.productCustomOptions($$createProduct.name$$, ProductOptionField.title, ProductOptionField.title)}}" userInput="{{ProductOptionField.title}}" stepKey="seeStorefontOrderProductOptionField1" /> -+ <see selector="{{StorefrontCustomerOrderSection.productCustomOptions($$createProduct.name$$, ProductOptionArea.title, ProductOptionArea.title)}}" userInput="{{ProductOptionArea.title}}" stepKey="seeStorefontOrderProductOptionArea1"/> -+ <see selector="{{StorefrontCustomerOrderSection.productCustomOptionsFile($$createProduct.name$$, ProductOptionFile.title, productWithOptions.file)}}" userInput="{{productWithOptions.file}}" stepKey="seeStorefontOrderProductOptionFile1"/> -+ <seeElement selector="{{StorefrontCustomerOrderSection.productCustomOptionsLink($createProduct.name$, ProductOptionFile.title, productWithOptions.file)}}" stepKey="seeStorefontOrderProductOptionFileLink1"/> -+ <see selector="{{StorefrontCustomerOrderSection.productCustomOptions($$createProduct.name$$, ProductOptionDropDown.title, ProductOptionValueDropdown1.title)}}" userInput="{{ProductOptionValueDropdown1.title}}" stepKey="seeStorefontOrderProductOptionValueDropdown11"/> -+ <see selector="{{StorefrontCustomerOrderSection.productCustomOptions($$createProduct.name$$, ProductOptionRadiobutton.title, ProductOptionValueRadioButtons1.title)}}" userInput="{{ProductOptionValueRadioButtons1.title}}" stepKey="seeStorefontOrderProductOptionValueRadioButtons11"/> -+ <see selector="{{StorefrontCustomerOrderSection.productCustomOptions($$createProduct.name$$, ProductOptionCheckbox.title, ProductOptionValueCheckbox.title)}}" userInput="{{ProductOptionValueCheckbox.title}}" stepKey="seeStorefontOrderProductOptionValueCheckbox1" /> -+ <see selector="{{StorefrontCustomerOrderSection.productCustomOptions($$createProduct.name$$, ProductOptionMultiSelect.title, ProductOptionValueMultiSelect1.title)}}" userInput="{{ProductOptionValueMultiSelect1.title}}" stepKey="seeStorefontOrderproductAttributeOptionsMultiselect11" /> -+ <see selector="{{StorefrontCustomerOrderSection.productCustomOptions($$createProduct.name$$, ProductOptionDate.title, 'Jan 1, $year')}}" userInput="Jan 1, $year" stepKey="seeStorefontOrderProductOptionDateAndTime1" /> -+ <see selector="{{StorefrontCustomerOrderSection.productCustomOptions($$createProduct.name$$, ProductOptionDateTime.title, '1/1/$shortYear, 1:00 AM')}}" userInput="1/1/$shortYear, 1:00 AM" stepKey="seeStorefontOrderProductOptionData1" /> -+ <see selector="{{StorefrontCustomerOrderSection.productCustomOptions($$createProduct.name$$, ProductOptionTime.title, '1:00 AM')}}" userInput="1:00 AM" stepKey="seeStorefontOrderProductOptionTime1" /> -+ </test> -+</tests> -diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontPurchaseProductWithCustomOptionsWithLongValuesTitle.xml b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontPurchaseProductWithCustomOptionsWithLongValuesTitle.xml -index 9e0f80eb9ea..294dcb8c1b8 100644 ---- a/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontPurchaseProductWithCustomOptionsWithLongValuesTitle.xml -+++ b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontPurchaseProductWithCustomOptionsWithLongValuesTitle.xml -@@ -7,16 +7,16 @@ - --> - - <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="StorefrontPurchaseProductWithCustomOptionsWithLongValuesTitle"> - <annotations> -+ <features value="Catalog"/> -+ <stories value="Custom options"/> - <group value="Catalog"/> - <title value="Admin should be able to see the full title of the selected custom option value in the order"/> - <description value="Admin should be able to see the full title of the selected custom option value in the order"/> - <severity value="MAJOR"/> - <testCaseId value="MC-3043"/> -- <group value="skip"/> -- <!-- Skip due to MQE-1128 --> - </annotations> - <before> - <!--Create Simple Product with Custom Options--> -@@ -33,6 +33,8 @@ - <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> - <deleteData createDataKey="createProduct" stepKey="deleteProduct"/> - <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> -+ <actionGroup ref="AdminOrdersGridClearFiltersActionGroup" stepKey="clearFilters"/> -+ <actionGroup ref="logout" stepKey="logout"/> - </after> - - <!-- Login Customer Storefront --> -@@ -71,11 +73,20 @@ - - <see selector="{{CheckoutPaymentSection.ProductOptionsActiveByProductItemName($$createProduct.name$$)}}" userInput="{{ProductOptionValueDropdownLongTitle1.title}}" stepKey="seeProductOptionValueDropdown1Input1"/> - -+ <!--Select shipping method--> -+ -+ <actionGroup ref="CheckoutSelectFlatRateShippingMethodActionGroup" stepKey="selectFlatRateShippingMethod"/> -+ <waitForElement selector="{{CheckoutShippingSection.next}}" time="30" stepKey="waitForNextButton"/> - <click selector="{{CheckoutShippingSection.next}}" stepKey="clickNext"/> -+ <waitForLoadingMaskToDisappear stepKey="waitForLoadingMaskAfterClickNext"/> -+ -+ <!-- Checkout select Check/Money Order payment --> -+ <actionGroup ref="CheckoutSelectCheckMoneyOrderPaymentActionGroup" stepKey="selectCheckMoneyPayment"/> - - <!-- Place Order --> - -- <waitForElement selector="{{CheckoutPaymentSection.placeOrder}}" time="30" stepKey="waitForPlaceOrderButton"/> -+ <actionGroup ref="CheckoutSelectCheckMoneyOrderPaymentActionGroup" stepKey="selectPaymentMethod"/> -+ <waitForElementVisible selector="{{CheckoutPaymentSection.placeOrder}}" time="30" stepKey="waitForPlaceOrderButton"/> - <click selector="{{CheckoutPaymentSection.placeOrder}}" stepKey="clickPlaceOrder"/> - - <grabTextFrom selector="{{CheckoutSuccessMainSection.orderNumber22}}" stepKey="grabOrderNumber"/> -@@ -86,10 +97,12 @@ - - <amOnPage url="{{AdminOrdersPage.url}}" stepKey="onOrdersPage"/> - <waitForLoadingMaskToDisappear stepKey="waitForLoadingMaskToDisappearOnOrdersPage"/> -+ <actionGroup ref="clearFiltersAdminDataGrid" stepKey="clearGridFilter"/> - <fillField selector="{{AdminOrdersGridSection.search}}" userInput="{$grabOrderNumber}" stepKey="fillOrderNum"/> - <click selector="{{AdminOrdersGridSection.submitSearch}}" stepKey="submitSearchOrderNum"/> - <waitForLoadingMaskToDisappear stepKey="waitForLoadingMaskToDisappearOnSearch"/> - <click selector="{{AdminOrdersGridSection.firstRow}}" stepKey="clickOrderRow"/> -+ <waitForPageLoad stepKey="waitForOrderPageToLoad"/> - - <!-- Checking the correctness of displayed custom options for user parameters on Order --> - -@@ -101,6 +114,6 @@ - </assertEquals> - <moveMouseOver selector="{{AdminOrderItemsOrderedSection.productNameOptions}} dd" stepKey="hoverProduct"/> - <waitForElementVisible selector="{{AdminOrderItemsOrderedSection.productNameOptions}} dd:nth-child(2)" stepKey="waitForCustomOptionValueFullName"/> -- <see selector="{{AdminOrderItemsOrderedSection.productNameOptions}}" userInput="{{ProductOptionValueDropdownLongTitle1.title}}" stepKey="seeAdminOrderProductOptionValueDropdown1"/> -+ <see selector="{{AdminOrderItemsOrderedSection.productNameOptions}}" userInput="Optisfvdklvfnkljvnfdklpvnfdjklfdvnjkvfdkjnvfdjkfvndj111 11Optisfvdklvfnkljvnfdklpvnfdjklfdvnjkvfdkjnvfdjkfvndj11111" stepKey="seeAdminOrderProductOptionValueDropdown1"/> - </test> - </tests> -diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontRememberCategoryPaginationTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontRememberCategoryPaginationTest.xml -new file mode 100644 -index 00000000000..0ed61b8636c ---- /dev/null -+++ b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontRememberCategoryPaginationTest.xml -@@ -0,0 +1,68 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> -+ <test name="StorefrontRememberCategoryPaginationTest"> -+ <annotations> -+ <title value="Verify that Number of Products per page retained when visiting a different category"/> -+ <stories value="MAGETWO-61478: Number of Products displayed per page not retained when visiting a different category"/> -+ <description value="Verify that Number of Products per page retained when visiting a different category"/> -+ <features value="Catalog"/> -+ <severity value="MAJOR"/> -+ <testCaseId value="MAGETWO-94210"/> -+ <group value="Catalog"/> -+ </annotations> -+ -+ <before> -+ <createData entity="_defaultCategory" stepKey="defaultCategory1"/> -+ <createData entity="SimpleProduct" stepKey="simpleProduct1"> -+ <requiredEntity createDataKey="defaultCategory1"/> -+ </createData> -+ -+ <createData entity="_defaultCategory" stepKey="defaultCategory2"/> -+ <createData entity="SimpleProduct" stepKey="simpleProduct2"> -+ <requiredEntity createDataKey="defaultCategory2"/> -+ </createData> -+ -+ <createData entity="RememberPaginationCatalogStorefrontConfig" stepKey="setRememberPaginationCatalogStorefrontConfig"/> -+ </before> -+ -+ <actionGroup ref="GoToStorefrontCategoryPageByParameters" stepKey="GoToStorefrontCategory1Page"> -+ <argument name="category" value="$$defaultCategory1.custom_attributes[url_key]$$"/> -+ <argument name="mode" value="grid"/> -+ <argument name="numOfProductsPerPage" value="12"/> -+ </actionGroup> -+ -+ <actionGroup ref="VerifyCategoryPageParameters" stepKey="verifyCategory1PageParameters"> -+ <argument name="category" value="$$defaultCategory1$$"/> -+ <argument name="mode" value="grid"/> -+ <argument name="numOfProductsPerPage" value="12"/> -+ </actionGroup> -+ -+ <amOnPage url="{{StorefrontCategoryPage.url($$defaultCategory2.name$$)}}" stepKey="navigateToCategory2Page"/> -+ <waitForPageLoad stepKey="waitForCategory2PageToLoad"/> -+ -+ <actionGroup ref="VerifyCategoryPageParameters" stepKey="verifyCategory2PageParameters"> -+ <argument name="category" value="$$defaultCategory2$$"/> -+ <argument name="mode" value="grid"/> -+ <argument name="numOfProductsPerPage" value="12"/> -+ </actionGroup> -+ -+ <after> -+ <createData entity="DefaultCatalogStorefrontConfiguration" stepKey="setDefaultCatalogStorefrontConfiguration"/> -+ -+ <deleteData createDataKey="simpleProduct1" stepKey="deleteProduct1"/> -+ <deleteData createDataKey="defaultCategory1" stepKey="deleteCategory1"/> -+ <deleteData createDataKey="simpleProduct2" stepKey="deleteProduct2"/> -+ <deleteData createDataKey="defaultCategory2" stepKey="deleteCategory2"/> -+ -+ <magentoCLI command="cache:flush" stepKey="flushCache"/> -+ -+ </after> -+ </test> -+</tests> -diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontSpecialPriceForDifferentTimezonesForWebsitesTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontSpecialPriceForDifferentTimezonesForWebsitesTest.xml -new file mode 100644 -index 00000000000..514e12bb355 ---- /dev/null -+++ b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontSpecialPriceForDifferentTimezonesForWebsitesTest.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="StorefrontSpecialPriceForDifferentTimezonesForWebsitesTest"> -+ <annotations> -+ <features value="Catalog"/> -+ <stories value="Special price"/> -+ <title value="Check that special price displayed when 'default config' scope timezone does not match 'website' scope timezone"/> -+ <description value="Check that special price displayed when 'default config' scope timezone does not match 'website' scope timezone"/> -+ <severity value="MAJOR"/> -+ <testCaseId value="MAGETWO-97508"/> -+ <useCaseId value="MAGETWO-96847"/> -+ <group value="Catalog"/> -+ </annotations> -+ <before> -+ <actionGroup ref="LoginAsAdmin" stepKey="login"/> -+ -+ <!--Create product--> -+ <createData entity="SimpleProduct2" stepKey="createProduct"/> -+ -+ <!--Create customer--> -+ <createData entity="Simple_US_Customer" stepKey="createCustomer"/> -+ </before> -+ <after> -+ <!--Delete create data--> -+ <deleteData createDataKey="createProduct" stepKey="deleteProduct"/> -+ <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> -+ -+ <actionGroup ref="logout" stepKey="logout"/> -+ </after> -+ -+ <!--Set timezone for default config--> -+ <amOnPage url="{{GeneralConfigurationPage.url}}" stepKey="goToGeneralConfig"/> -+ <waitForPageLoad stepKey="waitForConfigPage"/> -+ <conditionalClick selector="{{LocaleOptionsSection.sectionHeader}}" dependentSelector="{{LocaleOptionsSection.timezone}}" visible="false" stepKey="openLocaleSection"/> -+ <grabValueFrom selector="{{LocaleOptionsSection.timezone}}" stepKey="originalTimezone"/> -+ <selectOption selector="{{LocaleOptionsSection.timezone}}" userInput="Central European Standard Time (Europe/Paris)" stepKey="setTimezone"/> -+ <click selector="{{AdminMainActionsSection.save}}" stepKey="saveConfig"/> -+ -+ <!--Set timezone for Main Website--> -+ <amOnPage url="{{GeneralConfigurationPage.url}}" stepKey="goToGeneralConfig1"/> -+ <waitForPageLoad stepKey="waitForConfigPage1"/> -+ <actionGroup ref="AdminSwitchWebsiteActionGroup" stepKey="AdminSwitchStoreViewActionGroup"> -+ <argument name="website" value="_defaultWebsite"/> -+ </actionGroup> -+ <conditionalClick selector="{{LocaleOptionsSection.sectionHeader}}" dependentSelector="{{LocaleOptionsSection.timezone}}" visible="false" stepKey="openLocaleSection1"/> -+ <uncheckOption selector="{{LocaleOptionsSection.useDefault}}" stepKey="uncheckUseDefault"/> -+ <grabValueFrom selector="{{LocaleOptionsSection.timezone}}" stepKey="originalTimezone1"/> -+ <selectOption selector="{{LocaleOptionsSection.timezone}}" userInput="Greenwich Mean Time (Africa/Abidjan)" stepKey="setTimezone1"/> -+ <click selector="{{AdminMainActionsSection.save}}" stepKey="saveConfig1"/> -+ -+ <!--Set special price to created product--> -+ <amOnPage url="{{AdminProductEditPage.url($$createProduct.id$$)}}" stepKey="openAdminEditPage"/> -+ <actionGroup ref="AddSpecialPriceToProductActionGroup" stepKey="setSpecialPriceToCreatedProduct"> -+ <argument name="price" value="15"/> -+ </actionGroup> -+ <actionGroup ref="saveProductForm" stepKey="saveProductForm"/> -+ -+ <!--Login to storefront from customer and check price--> -+ <actionGroup ref="LoginToStorefrontActionGroup" stepKey="logInFromCustomer"> -+ <argument name="Customer" value="$$createCustomer$$"/> -+ </actionGroup> -+ -+ <!--Go to the product page and check special price--> -+ <amOnPage url="{{StorefrontProductPage.url($$createProduct.name$$)}}" stepKey="amOnSimpleProductPage"/> -+ <waitForPageLoad stepKey="waitForPageLoad"/> -+ <grabTextFrom selector="{{StorefrontProductInfoMainSection.specialPriceValue}}" stepKey="grabSpecialPrice"/> -+ <assertEquals expected='$15.00' expectedType="string" actual="$grabSpecialPrice" stepKey="assertSpecialPrice"/> -+ -+ <!--Reset timezone--> -+ <amOnPage url="{{GeneralConfigurationPage.url}}" stepKey="goToGeneralConfigReset"/> -+ <waitForPageLoad stepKey="waitForConfigPageReset"/> -+ <conditionalClick selector="{{LocaleOptionsSection.sectionHeader}}" dependentSelector="{{LocaleOptionsSection.timezone}}" visible="false" stepKey="openLocaleSectionReset"/> -+ <selectOption selector="{{LocaleOptionsSection.timezone}}" userInput="$originalTimezone" stepKey="resetTimezone"/> -+ <click selector="{{AdminMainActionsSection.save}}" stepKey="saveConfigReset"/> -+ -+ <!--Reset timezone--> -+ <amOnPage url="{{GeneralConfigurationPage.url}}" stepKey="goToGeneralConfigReset1"/> -+ <waitForPageLoad stepKey="waitForConfigPageReset1"/> -+ <actionGroup ref="AdminSwitchWebsiteActionGroup" stepKey="AdminSwitchStoreViewActionGroup1"> -+ <argument name="website" value="_defaultWebsite"/> -+ </actionGroup> -+ <conditionalClick selector="{{LocaleOptionsSection.sectionHeader}}" dependentSelector="{{LocaleOptionsSection.timezone}}" visible="false" stepKey="openLocaleSectionReset1"/> -+ <uncheckOption selector="{{LocaleOptionsSection.useDefault}}" stepKey="uncheckUseDefault1"/> -+ <selectOption selector="{{LocaleOptionsSection.timezone}}" userInput="$originalTimezone" stepKey="resetTimezone1"/> -+ <click selector="{{AdminMainActionsSection.save}}" stepKey="saveConfigReset1"/> -+ </test> -+</tests> -diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/TieredPricingAndQuantityIncrementsWorkWithDecimalinventoryTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/TieredPricingAndQuantityIncrementsWorkWithDecimalinventoryTest.xml -new file mode 100644 -index 00000000000..4d7c97b2645 ---- /dev/null -+++ b/app/code/Magento/Catalog/Test/Mftf/Test/TieredPricingAndQuantityIncrementsWorkWithDecimalinventoryTest.xml -@@ -0,0 +1,87 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> -+ <test name="TieredPricingAndQuantityIncrementsWorkWithDecimalinventoryTest"> -+ <annotations> -+ <features value="Catalog"/> -+ <stories value="Tiered pricing and quantity increments work with decimal inventory"/> -+ <title value="Tiered pricing and quantity increments work with decimal inventory"/> -+ <description value="Tiered pricing and quantity increments work with decimal inventory"/> -+ <severity value="CRITICAL"/> -+ <testCaseId value="MAGETWO-93973"/> -+ <group value="Catalog"/> -+ </annotations> -+ <before> -+ <createData entity="_defaultCategory" stepKey="createPreReqCategory"/> -+ <createData entity="SimpleProduct" stepKey="createPreReqSimpleProduct"> -+ <requiredEntity createDataKey="createPreReqCategory"/> -+ </createData> -+ </before> -+ <after> -+ <deleteData createDataKey="createPreReqCategory" stepKey="deletePreReqCategory"/> -+ <deleteData createDataKey="createPreReqSimpleProduct" stepKey="deletePreReqSimpleProduct"/> -+ </after> -+ <!--Step1. Login as admin. Go to Catalog > Products page. Filtering *prod1*. Open *prod1* to edit--> -+ <actionGroup ref="LoginAsAdmin" stepKey="LoginAsAdmin" /> -+ <actionGroup ref="SearchForProductOnBackendActionGroup" stepKey="filterGroupedProductOptions"> -+ <argument name="product" value="SimpleProduct"/> -+ </actionGroup> -+ <click selector="{{AdminProductGridSection.productGridNameProduct('$$createPreReqSimpleProduct.name$$')}}" -+ stepKey="clickOpenProductForEdit"/> -+ <waitForPageLoad time="30" stepKey="waitForProductEditOpen"/> -+ <!--Step2. Open *Advanced Inventory* pop-up (Click on *Advanced Inventory* link). Set *Qty Uses Decimals* to *Yes*. Click on button *Done* --> -+ <click selector="{{AdminProductFormSection.advancedInventoryLink}}" stepKey="clickOnAdvancedInventoryLink"/> -+ <scrollTo selector="{{AdminProductFormAdvancedInventorySection.qtyUsesDecimals}}" stepKey="scrollToQtyUsesDecimalsDropBox"/> -+ <click selector="{{AdminProductFormAdvancedInventorySection.qtyUsesDecimals}}" stepKey="clickOnQtyUsesDecimalsDropBox"/> -+ <click selector="{{AdminProductFormAdvancedInventorySection.qtyUsesDecimalsOptions('1')}}" stepKey="chooseYesOnQtyUsesDecimalsDropBox"/> -+ <click selector="{{AdminProductFormAdvancedInventorySection.doneButton}}" stepKey="clickOnDoneButton"/> -+ <!--Step3. Open *Advanced Pricing* pop-up (Click on *Advanced Pricing* link). Click on *Add* button. Fill *0.5* in *Quantity*--> -+ <scrollTo selector="{{AdminProductFormSection.productName}}" stepKey="scrollToProductName"/> -+ <click selector="{{AdminProductFormSection.advancedPricingLink}}" stepKey="clickOnAdvancedPricingLink1"/> -+ <waitForElement selector="{{AdminProductFormAdvancedPricingSection.customerGroupPriceAddButton}}" stepKey="waitForAddButton"/> -+ <click selector="{{AdminProductFormAdvancedPricingSection.customerGroupPriceAddButton}}" stepKey="clickOnCustomerGroupPriceAddButton"/> -+ <fillField selector="{{AdminProductFormAdvancedPricingSection.productTierPriceQtyInput('0')}}" userInput="0.5" stepKey="fillProductTierPriceQty"/> -+ <!--Step4. Close *Advanced Pricing* (Click on button *Done*). Save *prod1* (Click on button *Save*)--> -+ <click selector="{{AdminProductFormAdvancedPricingSection.doneButton}}" stepKey="clickOnDoneButton2"/> -+ <click selector="{{AdminProductFormActionSection.saveButton}}" stepKey="clickOnSaveButton"/> -+ -+ <!--The code should be uncommented after fix MAGETWO-96016--> -+ <!--<click selector="{{AdminProductFormSection.advancedPricingLink}}" stepKey="clickOnAdvancedPricingLink2"/>--> -+ <!--<seeInField userInput="0.5" selector="{{AdminProductFormAdvancedPricingSection.productTierPriceQtyInput('0')}}" stepKey="seeInField1"/>--> -+ <!--<click selector="{{AdminProductFormAdvancedPricingSection.advancedPricingCloseButton}}" stepKey="clickOnCloseButton"/>--> -+ -+ <!--Step5. Open *Advanced Inventory* pop-up. Set *Enable Qty Increments* to *Yes*. Fill *.5* in *Qty Increments*--> -+ <click selector="{{AdminProductFormSection.advancedInventoryLink}}" stepKey="clickOnAdvancedInventoryLink2"/> -+ <waitForPageLoad stepKey="waitForPageLoad"/> -+ <scrollTo selector="{{AdminProductFormAdvancedInventorySection.enableQtyIncrements}}" stepKey="scrollToEnableQtyIncrements"/> -+ <click selector="{{AdminProductFormAdvancedInventorySection.enableQtyIncrementsUseConfigSettings}}" stepKey="clickOnEnableQtyIncrementsUseConfigSettingsCheckbox"/> -+ <click selector="{{AdminProductFormAdvancedInventorySection.enableQtyIncrements}}" stepKey="clickOnEnableQtyIncrements"/> -+ <click selector="{{AdminProductFormAdvancedInventorySection.enableQtyIncrementsOptions(('1'))}}" stepKey="chooseYesOnEnableQtyIncrements"/> -+ <scrollTo selector="{{AdminProductFormAdvancedInventorySection.qtyIncrementsUseConfigSettings}}" stepKey="scrollToQtyIncrementsUseConfigSettings"/> -+ <click selector="{{AdminProductFormAdvancedInventorySection.qtyIncrementsUseConfigSettings}}" stepKey="clickOnQtyIncrementsUseConfigSettings"/> -+ <scrollTo selector="{{AdminProductFormAdvancedInventorySection.qtyIncrements}}" stepKey="scrollToQtyIncrements"/> -+ <fillField selector="{{AdminProductFormAdvancedInventorySection.qtyIncrements}}" userInput=".5" stepKey="fillQtyIncrements"/> -+ <!--Step6. Close *Advanced Inventory* (Click on button *Done*). Save *prod1* (Click on button *Save*) --> -+ <click selector="{{AdminProductFormAdvancedInventorySection.doneButton}}" stepKey="clickOnDoneButton3"/> -+ <click selector="{{AdminProductFormActionSection.saveButton}}" stepKey="clickOnSaveButton2"/> -+ <!--Step7. Open *Customer view* (Go to *Store Front*). Open *prod1* page (Find via search and click on product name) --> -+ <amOnPage url="{{StorefrontHomePage.url}}$$createPreReqSimpleProduct.custom_attributes[url_key]$$.html" stepKey="amOnProductPage"/> -+ <!--Step8. Fill *1.5* in *Qty*. Click on button *Add to Cart*--> -+ <fillField selector="{{StorefrontProductPageSection.qtyInput}}" userInput="1.5" stepKey="fillQty"/> -+ <click selector="{{StorefrontProductPageSection.addToCartBtn}}" stepKey="clickOnAddToCart"/> -+ <waitForElementVisible selector="{{StorefrontProductPageSection.successMsg}}" time="30" stepKey="waitForProductAdded"/> -+ <see selector="{{StorefrontCategoryMainSection.SuccessMsg}}" userInput="You added $$createPreReqSimpleProduct.name$$ to your shopping cart." stepKey="seeAddedToCartMessage"/> -+ <!--Step9. Click on *Cart* icon. Click on *View and Edit Cart* link. Change *Qty* value to *5.5*--> -+ <actionGroup ref="clickViewAndEditCartFromMiniCart" stepKey="goToShoppingCartFromMinicart"/> -+ <fillField selector="{{CheckoutCartProductSection.ProductQuantityByName(('$$createPreReqSimpleProduct.name$$'))}}" userInput="5.5" stepKey="fillQty2"/> -+ <click selector="{{CheckoutCartProductSection.updateShoppingCartButton}}" stepKey="clickOnUpdateShoppingCartButton"/> -+ <seeInField userInput="5.5" selector="{{CheckoutCartProductSection.ProductQuantityByName(('$$createPreReqSimpleProduct.name$$'))}}" stepKey="seeInField2"/> -+ </test> -+</tests> -diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/VerifyChildCategoriesShouldNotIncludeInMenuTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/VerifyChildCategoriesShouldNotIncludeInMenuTest.xml -index 51290ba12f2..455e9b58156 100644 ---- a/app/code/Magento/Catalog/Test/Mftf/Test/VerifyChildCategoriesShouldNotIncludeInMenuTest.xml -+++ b/app/code/Magento/Catalog/Test/Mftf/Test/VerifyChildCategoriesShouldNotIncludeInMenuTest.xml -@@ -7,10 +7,11 @@ - --> - - <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="VerifyChildCategoriesShouldNotIncludeInMenuTest"> - <annotations> - <features value="Catalog"/> -+ <stories value="Create categories"/> - <title value="Customer should not see categories that are not included in the menu"/> - <description value="Customer should not see categories that are not included in the menu"/> - <severity value="CRITICAL"/> -diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/VerifyDefaultWYSIWYGToolbarOnProductTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/VerifyDefaultWYSIWYGToolbarOnProductTest.xml -index 234148830bd..53bcac5b1d5 100644 ---- a/app/code/Magento/Catalog/Test/Mftf/Test/VerifyDefaultWYSIWYGToolbarOnProductTest.xml -+++ b/app/code/Magento/Catalog/Test/Mftf/Test/VerifyDefaultWYSIWYGToolbarOnProductTest.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="VerifyDefaultWYSIWYGToolbarOnProductTest"> - <annotations> - <features value="Catalog"/> -diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/VerifyTinyMCEv4IsNativeWYSIWYGOnCatalogTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/VerifyTinyMCEv4IsNativeWYSIWYGOnCatalogTest.xml -index 5fc0b6478ff..aad83cd0c2a 100644 ---- a/app/code/Magento/Catalog/Test/Mftf/Test/VerifyTinyMCEv4IsNativeWYSIWYGOnCatalogTest.xml -+++ b/app/code/Magento/Catalog/Test/Mftf/Test/VerifyTinyMCEv4IsNativeWYSIWYGOnCatalogTest.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="VerifyTinyMCEv4IsNativeWYSIWYGOnCatalogTest"> - <annotations> - <features value="Catalog"/> -diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/VerifyTinyMCEv4IsNativeWYSIWYGOnProductTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/VerifyTinyMCEv4IsNativeWYSIWYGOnProductTest.xml -index e2077f86767..29ed3af4f01 100644 ---- a/app/code/Magento/Catalog/Test/Mftf/Test/VerifyTinyMCEv4IsNativeWYSIWYGOnProductTest.xml -+++ b/app/code/Magento/Catalog/Test/Mftf/Test/VerifyTinyMCEv4IsNativeWYSIWYGOnProductTest.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="VerifyTinyMCEv4IsNativeWYSIWYGOnProductTest"> - <annotations> - <features value="Catalog"/> -@@ -32,16 +32,19 @@ - <click selector="{{AdminProductFormSection.contentTab}}" stepKey="clickContentTab" /> - <waitForElementVisible selector="{{ProductDescriptionWYSIWYGToolbarSection.TinyMCE4}}" stepKey="waitForDescription" /> - <seeElement selector="{{ProductDescriptionWYSIWYGToolbarSection.TinyMCE4}}" stepKey="TinyMCE4Description" /> -- <waitForElementVisible selector="{{ProductShortDescriptionWYSIWYGToolbarSection.TinyMCE4}}" stepKey="waitForShortDescription" /> -- <seeElement selector="{{ProductShortDescriptionWYSIWYGToolbarSection.TinyMCE4}}" stepKey="TinyMCE4ShortDescription" /> -+ <click selector="{{ProductDescriptionWysiwygSection.EditArea}}" stepKey="focusProductDescriptionWysiwyg"/> - <executeJS function="tinyMCE.get('product_form_description').setContent('Hello World!');" stepKey="executeJSFillContent1"/> -+ <waitForElementVisible selector="{{ProductShortDescriptionWYSIWYGToolbarSection.TinyMCE4}}" stepKey="waitForShortDescription" /> -+ <seeElement selector="{{ProductShortDescriptionWYSIWYGToolbarSection.TinyMCE4}}" stepKey="TinyMCE4ShortDescription" /> -+ <click selector="{{ProductShortDescriptionWysiwygSection.EditArea}}" stepKey="focusProductShortDescriptionWysiwyg"/> - <executeJS function="tinyMCE.get('product_form_short_description').setContent('Hello World! Short Content');" stepKey="executeJSFillContent2"/> -+ <scrollTo selector="{{ProductDescriptionWYSIWYGToolbarSection.showHideBtn}}" y="-150" x="0" stepKey="scrollToDesShowHideBtn1" /> - <click selector="{{ProductDescriptionWYSIWYGToolbarSection.showHideBtn}}" stepKey="clickShowHideBtn1" /> - <waitForElementVisible selector="{{ProductDescriptionWYSIWYGToolbarSection.InsertImageBtn}}" stepKey="waitForInsertImage1" /> - <see selector="{{ProductDescriptionWYSIWYGToolbarSection.InsertImageBtn}}" userInput="Insert Image..." stepKey="seeInsertImage1"/> - <dontSee selector="{{TinyMCESection.InsertWidgetBtn}}" stepKey="insertWidget1" /> - <dontSee selector="{{TinyMCESection.InsertVariableBtn}}" stepKey="insertVariable1" /> -- <scrollTo selector="{{ProductDescriptionWYSIWYGToolbarSection.showHideBtn}}" stepKey="scrollToDesShowHideBtn" /> -+ <scrollTo selector="{{ProductDescriptionWYSIWYGToolbarSection.showHideBtn}}" stepKey="scrollToDesShowHideBtn2" /> - <click selector="{{ProductShortDescriptionWYSIWYGToolbarSection.showHideBtn}}" stepKey="clickShowHideBtn2" /> - <waitForElementVisible selector="{{ProductShortDescriptionWYSIWYGToolbarSection.InsertImageBtn}}" stepKey="waitForInsertImage2" /> - <see selector="{{ProductShortDescriptionWYSIWYGToolbarSection.InsertImageBtn}}" userInput="Insert Image..." stepKey="seeInsertImage2"/> -diff --git a/app/code/Magento/Catalog/Test/Unit/Block/Adminhtml/Product/Edit/Tab/InventoryTest.php b/app/code/Magento/Catalog/Test/Unit/Block/Adminhtml/Product/Edit/Tab/InventoryTest.php -index 19c578e976c..2008d0b9414 100644 ---- a/app/code/Magento/Catalog/Test/Unit/Block/Adminhtml/Product/Edit/Tab/InventoryTest.php -+++ b/app/code/Magento/Catalog/Test/Unit/Block/Adminhtml/Product/Edit/Tab/InventoryTest.php -@@ -85,7 +85,7 @@ class InventoryTest extends \PHPUnit\Framework\TestCase - $this->backordersMock = $this->createMock(\Magento\CatalogInventory\Model\Source\Backorders::class); - $this->stockMock = $this->createMock(\Magento\CatalogInventory\Model\Source\Stock::class); - $this->coreRegistryMock = $this->createMock(\Magento\Framework\Registry::class); -- $this->moduleManager = $this->createMock(\Magento\Framework\Module\Manager::class); -+ $this->moduleManager = $this->createMock(\Magento\Framework\Module\ModuleManagerInterface::class); - $this->storeManagerMock = $this->getMockForAbstractClass( - \Magento\Store\Model\StoreManagerInterface::class, - [], -diff --git a/app/code/Magento/Catalog/Test/Unit/Block/Adminhtml/Product/Helper/Form/Gallery/ContentTest.php b/app/code/Magento/Catalog/Test/Unit/Block/Adminhtml/Product/Helper/Form/Gallery/ContentTest.php -index 804eef25ebd..9a2199859a1 100644 ---- a/app/code/Magento/Catalog/Test/Unit/Block/Adminhtml/Product/Helper/Form/Gallery/ContentTest.php -+++ b/app/code/Magento/Catalog/Test/Unit/Block/Adminhtml/Product/Helper/Form/Gallery/ContentTest.php -@@ -6,9 +6,14 @@ - namespace Magento\Catalog\Test\Unit\Block\Adminhtml\Product\Helper\Form\Gallery; - - use Magento\Catalog\Block\Adminhtml\Product\Helper\Form\Gallery\Content; --use Magento\Framework\Filesystem; -+use Magento\Catalog\Model\Entity\Attribute; -+use Magento\Catalog\Model\Product; - use Magento\Framework\Phrase; -+use Magento\MediaStorage\Helper\File\Storage\Database; - -+/** -+ * @SuppressWarnings(PHPMD.CouplingBetweenObjects) -+ */ - class ContentTest extends \PHPUnit\Framework\TestCase - { - /** -@@ -46,6 +51,11 @@ class ContentTest extends \PHPUnit\Framework\TestCase - */ - protected $imageHelper; - -+ /** -+ * @var \Magento\MediaStorage\Helper\File\Storage\Database|\PHPUnit_Framework_MockObject_MockObject -+ */ -+ protected $databaseMock; -+ - /** - * @var \Magento\Framework\TestFramework\Unit\Helper\ObjectManager - */ -@@ -67,13 +77,18 @@ class ContentTest extends \PHPUnit\Framework\TestCase - ->disableOriginalConstructor() - ->getMock(); - -+ $this->databaseMock = $this->getMockBuilder(Database::class) -+ ->disableOriginalConstructor() -+ ->getMock(); -+ - $this->objectManager = new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this); - $this->content = $this->objectManager->getObject( - \Magento\Catalog\Block\Adminhtml\Product\Helper\Form\Gallery\Content::class, - [ - 'mediaConfig' => $this->mediaConfigMock, - 'jsonEncoder' => $this->jsonEncoderMock, -- 'filesystem' => $this->fileSystemMock -+ 'filesystem' => $this->fileSystemMock, -+ 'fileStorageDatabase' => $this->databaseMock - ] - ); - } -@@ -139,6 +154,13 @@ class ContentTest extends \PHPUnit\Framework\TestCase - $this->readMock->expects($this->any())->method('stat')->willReturnMap($sizeMap); - $this->jsonEncoderMock->expects($this->once())->method('encode')->willReturnCallback('json_encode'); - -+ $this->readMock->expects($this->any()) -+ ->method('isFile') -+ ->will($this->returnValue(true)); -+ $this->databaseMock->expects($this->any()) -+ ->method('checkDbUsage') -+ ->will($this->returnValue(false)); -+ - $this->assertSame(json_encode($imagesResult), $this->content->getImagesJson()); - } - -@@ -206,6 +228,14 @@ class ContentTest extends \PHPUnit\Framework\TestCase - $this->fileSystemMock->expects($this->any())->method('getDirectoryRead')->willReturn($this->readMock); - $this->mediaConfigMock->expects($this->any())->method('getMediaUrl'); - $this->mediaConfigMock->expects($this->any())->method('getMediaPath'); -+ -+ $this->readMock->expects($this->any()) -+ ->method('isFile') -+ ->will($this->returnValue(true)); -+ $this->databaseMock->expects($this->any()) -+ ->method('checkDbUsage') -+ ->will($this->returnValue(false)); -+ - $this->readMock->expects($this->any())->method('stat')->willReturnOnConsecutiveCalls( - $this->throwException( - new \Magento\Framework\Exception\FileSystemException(new Phrase('test')) -@@ -219,4 +249,194 @@ class ContentTest extends \PHPUnit\Framework\TestCase - - $this->assertSame(json_encode($imagesResult), $this->content->getImagesJson()); - } -+ -+ /** -+ * Test GetImageTypes() will return value for given attribute from data persistor. -+ * -+ * @return void -+ */ -+ public function testGetImageTypesFromDataPersistor() -+ { -+ $attributeCode = 'thumbnail'; -+ $value = 'testImageValue'; -+ $scopeLabel = 'testScopeLabel'; -+ $label = 'testLabel'; -+ $name = 'testName'; -+ $expectedTypes = [ -+ $attributeCode => [ -+ 'code' => $attributeCode, -+ 'value' => $value, -+ 'label' => $label, -+ 'name' => $name, -+ ], -+ ]; -+ $product = $this->getMockBuilder(Product::class) -+ ->disableOriginalConstructor() -+ ->getMock(); -+ $product->expects($this->once()) -+ ->method('getData') -+ ->with($this->identicalTo($attributeCode)) -+ ->willReturn(null); -+ $mediaAttribute = $this->getMediaAttribute($label, $attributeCode); -+ $product->expects($this->once()) -+ ->method('getMediaAttributes') -+ ->willReturn([$mediaAttribute]); -+ $this->galleryMock->expects($this->exactly(2)) -+ ->method('getDataObject') -+ ->willReturn($product); -+ $this->galleryMock->expects($this->once()) -+ ->method('getImageValue') -+ ->with($this->identicalTo($attributeCode)) -+ ->willReturn($value); -+ $this->galleryMock->expects($this->once()) -+ ->method('getScopeLabel') -+ ->with($this->identicalTo($mediaAttribute)) -+ ->willReturn($scopeLabel); -+ $this->galleryMock->expects($this->once()) -+ ->method('getAttributeFieldName') -+ ->with($this->identicalTo($mediaAttribute)) -+ ->willReturn($name); -+ $this->getImageTypesAssertions($attributeCode, $scopeLabel, $expectedTypes); -+ } -+ -+ /** -+ * Test GetImageTypes() will return value for given attribute from product. -+ * -+ * @return void -+ */ -+ public function testGetImageTypesFromProduct() -+ { -+ $attributeCode = 'thumbnail'; -+ $value = 'testImageValue'; -+ $scopeLabel = 'testScopeLabel'; -+ $label = 'testLabel'; -+ $name = 'testName'; -+ $expectedTypes = [ -+ $attributeCode => [ -+ 'code' => $attributeCode, -+ 'value' => $value, -+ 'label' => $label, -+ 'name' => $name, -+ ], -+ ]; -+ $product = $this->getMockBuilder(Product::class) -+ ->disableOriginalConstructor() -+ ->getMock(); -+ $product->expects($this->once()) -+ ->method('getData') -+ ->with($this->identicalTo($attributeCode)) -+ ->willReturn($value); -+ $mediaAttribute = $this->getMediaAttribute($label, $attributeCode); -+ $product->expects($this->once()) -+ ->method('getMediaAttributes') -+ ->willReturn([$mediaAttribute]); -+ $this->galleryMock->expects($this->exactly(2)) -+ ->method('getDataObject') -+ ->willReturn($product); -+ $this->galleryMock->expects($this->never()) -+ ->method('getImageValue'); -+ $this->galleryMock->expects($this->once()) -+ ->method('getScopeLabel') -+ ->with($this->identicalTo($mediaAttribute)) -+ ->willReturn($scopeLabel); -+ $this->galleryMock->expects($this->once()) -+ ->method('getAttributeFieldName') -+ ->with($this->identicalTo($mediaAttribute)) -+ ->willReturn($name); -+ $this->getImageTypesAssertions($attributeCode, $scopeLabel, $expectedTypes); -+ } -+ -+ /** -+ * Perform assertions. -+ * -+ * @param string $attributeCode -+ * @param string $scopeLabel -+ * @param array $expectedTypes -+ * @return void -+ */ -+ private function getImageTypesAssertions(string $attributeCode, string $scopeLabel, array $expectedTypes) -+ { -+ $this->content->setElement($this->galleryMock); -+ $result = $this->content->getImageTypes(); -+ $scope = $result[$attributeCode]['scope']; -+ $this->assertSame($scopeLabel, $scope->getText()); -+ unset($result[$attributeCode]['scope']); -+ $this->assertSame($expectedTypes, $result); -+ } -+ -+ /** -+ * Get media attribute mock. -+ * -+ * @param string $label -+ * @param string $attributeCode -+ * @return \PHPUnit_Framework_MockObject_MockObject -+ */ -+ private function getMediaAttribute(string $label, string $attributeCode) -+ { -+ $frontend = $this->getMockBuilder(Product\Attribute\Frontend\Image::class) -+ ->disableOriginalConstructor() -+ ->getMock(); -+ $frontend->expects($this->once()) -+ ->method('getLabel') -+ ->willReturn($label); -+ $mediaAttribute = $this->getMockBuilder(Attribute::class) -+ ->disableOriginalConstructor() -+ ->getMock(); -+ $mediaAttribute->expects($this->any()) -+ ->method('getAttributeCode') -+ ->willReturn($attributeCode); -+ $mediaAttribute->expects($this->once()) -+ ->method('getFrontend') -+ ->willReturn($frontend); -+ -+ return $mediaAttribute; -+ } -+ -+ /** -+ * Test GetImagesJson() calls MediaStorage functions to obtain image from DB prior to stat call -+ * -+ * @return void -+ */ -+ public function testGetImagesJsonMediaStorageMode() -+ { -+ $images = [ -+ 'images' => [ -+ [ -+ 'value_id' => '0', -+ 'file' => 'file_1.jpg', -+ 'media_type' => 'image', -+ 'position' => '0' -+ ] -+ ] -+ ]; -+ -+ $mediaPath = [ -+ ['file_1.jpg', 'catalog/product/image_1.jpg'] -+ ]; -+ -+ $this->content->setElement($this->galleryMock); -+ -+ $this->galleryMock->expects($this->once()) -+ ->method('getImages') -+ ->willReturn($images); -+ $this->fileSystemMock->expects($this->once()) -+ ->method('getDirectoryRead') -+ ->willReturn($this->readMock); -+ $this->mediaConfigMock->expects($this->any()) -+ ->method('getMediaPath') -+ ->willReturnMap($mediaPath); -+ -+ $this->readMock->expects($this->any()) -+ ->method('isFile') -+ ->will($this->returnValue(false)); -+ $this->databaseMock->expects($this->any()) -+ ->method('checkDbUsage') -+ ->will($this->returnValue(true)); -+ -+ $this->databaseMock->expects($this->once()) -+ ->method('saveFileToFilesystem') -+ ->with('catalog/product/image_1.jpg'); -+ -+ $this->content->getImagesJson(); -+ } - } -diff --git a/app/code/Magento/Catalog/Test/Unit/Block/Adminhtml/Product/Helper/Form/GalleryTest.php b/app/code/Magento/Catalog/Test/Unit/Block/Adminhtml/Product/Helper/Form/GalleryTest.php -index 06e2368f308..1e04680676e 100644 ---- a/app/code/Magento/Catalog/Test/Unit/Block/Adminhtml/Product/Helper/Form/GalleryTest.php -+++ b/app/code/Magento/Catalog/Test/Unit/Block/Adminhtml/Product/Helper/Form/GalleryTest.php -@@ -5,6 +5,8 @@ - */ - namespace Magento\Catalog\Test\Unit\Block\Adminhtml\Product\Helper\Form; - -+use Magento\Framework\App\Request\DataPersistorInterface; -+ - class GalleryTest extends \PHPUnit\Framework\TestCase - { - /** -@@ -32,18 +34,27 @@ class GalleryTest extends \PHPUnit\Framework\TestCase - */ - protected $objectManager; - -+ /** -+ * @var DataPersistorInterface|\PHPUnit_Framework_MockObject_MockObject -+ */ -+ private $dataPersistorMock; -+ - public function setUp() - { - $this->registryMock = $this->createMock(\Magento\Framework\Registry::class); - $this->productMock = $this->createPartialMock(\Magento\Catalog\Model\Product::class, ['getData']); - $this->formMock = $this->createMock(\Magento\Framework\Data\Form::class); -- -+ $this->dataPersistorMock = $this->getMockBuilder(DataPersistorInterface::class) -+ ->disableOriginalConstructor() -+ ->setMethods(['get']) -+ ->getMockForAbstractClass(); - $this->objectManager = new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this); - $this->gallery = $this->objectManager->getObject( - \Magento\Catalog\Block\Adminhtml\Product\Helper\Form\Gallery::class, - [ - 'registry' => $this->registryMock, -- 'form' => $this->formMock -+ 'form' => $this->formMock, -+ 'dataPersistor' => $this->dataPersistorMock - ] - ); - } -@@ -70,6 +81,68 @@ class GalleryTest extends \PHPUnit\Framework\TestCase - $this->assertSame($mediaGallery, $this->gallery->getImages()); - } - -+ /** -+ * Test getImages() will try get data from data persistor, if it's absent in registry. -+ * -+ * @return void -+ */ -+ public function testGetImagesWithDataPersistor() -+ { -+ $product = [ -+ 'product' => [ -+ 'media_gallery' => [ -+ 'images' => [ -+ [ -+ 'value_id' => '1', -+ 'file' => 'image_1.jpg', -+ 'media_type' => 'image', -+ ], -+ [ -+ 'value_id' => '2', -+ 'file' => 'image_2.jpg', -+ 'media_type' => 'image', -+ ], -+ ], -+ ], -+ ], -+ ]; -+ $this->registryMock->expects($this->once())->method('registry')->willReturn($this->productMock); -+ $this->productMock->expects($this->once())->method('getData')->willReturn(null); -+ $this->dataPersistorMock->expects($this->once()) -+ ->method('get') -+ ->with($this->identicalTo('catalog_product')) -+ ->willReturn($product); -+ -+ $this->assertSame($product['product']['media_gallery'], $this->gallery->getImages()); -+ } -+ -+ /** -+ * Test get image value from data persistor in case it's absent in product from registry. -+ * -+ * @return void -+ */ -+ public function testGetImageValue() -+ { -+ $product = [ -+ 'product' => [ -+ 'media_gallery' => [ -+ 'images' => [ -+ 'value_id' => '1', -+ 'file' => 'image_1.jpg', -+ 'media_type' => 'image', -+ ], -+ ], -+ 'small' => 'testSmallImage', -+ 'thumbnail' => 'testThumbnail' -+ ] -+ ]; -+ $this->dataPersistorMock->expects($this->once()) -+ ->method('get') -+ ->with($this->identicalTo('catalog_product')) -+ ->willReturn($product); -+ $this->assertSame($product['product']['small'], $this->gallery->getImageValue('small')); -+ } -+ - public function testGetDataObject() - { - $this->registryMock->expects($this->once())->method('registry')->willReturn($this->productMock); -diff --git a/app/code/Magento/Catalog/Test/Unit/Block/Adminhtml/Rss/NotifyStockTest.php b/app/code/Magento/Catalog/Test/Unit/Block/Adminhtml/Rss/NotifyStockTest.php -index 1dd866f1fe2..da35d845468 100644 ---- a/app/code/Magento/Catalog/Test/Unit/Block/Adminhtml/Rss/NotifyStockTest.php -+++ b/app/code/Magento/Catalog/Test/Unit/Block/Adminhtml/Rss/NotifyStockTest.php -@@ -96,7 +96,12 @@ class NotifyStockTest extends \PHPUnit\Framework\TestCase - $this->urlBuilder->expects($this->once())->method('getUrl') - ->with('catalog/product/edit', ['id' => 1, '_secure' => true, '_nosecret' => true]) - ->will($this->returnValue('http://magento.com/catalog/product/edit/id/1')); -- $this->assertEquals($this->rssFeed, $this->block->getRssData()); -+ -+ $data = $this->block->getRssData(); -+ $this->assertTrue(is_string($data['title'])); -+ $this->assertTrue(is_string($data['description'])); -+ $this->assertTrue(is_string($data['entries'][0]['description'])); -+ $this->assertEquals($this->rssFeed, $data); - } - - public function testGetCacheLifetime() -diff --git a/app/code/Magento/Catalog/Test/Unit/Block/Product/ImageFactoryTest.php b/app/code/Magento/Catalog/Test/Unit/Block/Product/ImageFactoryTest.php -index 8a42865a3fe..95b06e40602 100644 ---- a/app/code/Magento/Catalog/Test/Unit/Block/Product/ImageFactoryTest.php -+++ b/app/code/Magento/Catalog/Test/Unit/Block/Product/ImageFactoryTest.php -@@ -145,7 +145,8 @@ class ImageFactoryTest extends \PHPUnit\Framework\TestCase - 'label' => 'test_image_label', - 'ratio' => 1, - 'custom_attributes' => '', -- 'product_id' => null -+ 'product_id' => null, -+ 'class' => 'product-image-photo' - ], - ], - ]; -@@ -190,6 +191,7 @@ class ImageFactoryTest extends \PHPUnit\Framework\TestCase - 'custom_attributes' => [ - 'name_1' => 'value_1', - 'name_2' => 'value_2', -+ 'class' => 'my-class' - ], - ], - 'expected' => [ -@@ -201,7 +203,8 @@ class ImageFactoryTest extends \PHPUnit\Framework\TestCase - 'label' => 'test_product_name', - 'ratio' => 0.5, // <== - 'custom_attributes' => 'name_1="value_1" name_2="value_2"', -- 'product_id' => null -+ 'product_id' => null, -+ 'class' => 'my-class' - ], - ], - ]; -diff --git a/app/code/Magento/Catalog/Test/Unit/Block/Product/ProductList/ToolbarTest.php b/app/code/Magento/Catalog/Test/Unit/Block/Product/ProductList/ToolbarTest.php -index ac963326dbf..884f4c543c8 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 @@ class ToolbarTest extends \PHPUnit\Framework\TestCase - '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 @@ class ToolbarTest extends \PHPUnit\Framework\TestCase - 'context' => $context, - 'catalogConfig' => $this->catalogConfig, - 'toolbarModel' => $this->model, -+ 'toolbarMemorizer' => $this->memorizer, - 'urlEncoder' => $this->urlEncoder, - 'productListHelper' => $this->productListHelper - ] -@@ -155,7 +171,7 @@ class ToolbarTest extends \PHPUnit\Framework\TestCase - 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 @@ class ToolbarTest extends \PHPUnit\Framework\TestCase - { - $direction = 'desc'; - -- $this->model->expects($this->once()) -+ $this->memorizer->expects($this->once()) - ->method('getDirection') - ->will($this->returnValue($direction)); - -@@ -183,7 +199,7 @@ class ToolbarTest extends \PHPUnit\Framework\TestCase - $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 @@ class ToolbarTest extends \PHPUnit\Framework\TestCase - $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 @@ class ToolbarTest extends \PHPUnit\Framework\TestCase - $this->productListHelper->expects($this->exactly(2)) - ->method('getAvailableLimit') - ->will($this->returnValue([10 => 10, 20 => 20])); -- $this->model->expects($this->once()) -+ $this->memorizer->expects($this->once()) - ->method('getLimit') - ->will($this->returnValue($limit)); - $this->pagerBlock->expects($this->once()) -diff --git a/app/code/Magento/Catalog/Test/Unit/Block/Product/View/GalleryOptionsTest.php b/app/code/Magento/Catalog/Test/Unit/Block/Product/View/GalleryOptionsTest.php -new file mode 100644 -index 00000000000..7ed8b13fce7 ---- /dev/null -+++ b/app/code/Magento/Catalog/Test/Unit/Block/Product/View/GalleryOptionsTest.php -@@ -0,0 +1,223 @@ -+<?php -+/** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+namespace Magento\Catalog\Test\Unit\Block\Product\View; -+ -+use Magento\Catalog\Block\Product\Context; -+use Magento\Catalog\Block\Product\View\Gallery; -+use Magento\Catalog\Block\Product\View\GalleryOptions; -+use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; -+use Magento\Framework\Escaper; -+use Magento\Framework\View\Config; -+use Magento\Framework\Config\View; -+use Magento\Framework\Serialize\Serializer\Json; -+ -+/** -+ * @SuppressWarnings(PHPMD.CouplingBetweenObjects) -+ */ -+class GalleryOptionsTest extends \PHPUnit\Framework\TestCase -+{ -+ /** -+ * @var GalleryOptions -+ */ -+ private $model; -+ -+ /** -+ * @var Gallery|\PHPUnit_Framework_MockObject_MockObject -+ */ -+ private $gallery; -+ -+ /** -+ * @var Context|\PHPUnit_Framework_MockObject_MockObject -+ */ -+ private $context; -+ -+ /** -+ * @var Json -+ */ -+ private $jsonSerializer; -+ -+ /** -+ * @var View|\PHPUnit_Framework_MockObject_MockObject -+ */ -+ private $configView; -+ -+ /** -+ * @var Config|\PHPUnit_Framework_MockObject_MockObject -+ */ -+ private $viewConfig; -+ -+ /** -+ * @var Escaper -+ */ -+ private $escaper; -+ -+ protected function setUp() -+ { -+ $objectManager = new ObjectManager($this); -+ -+ $this->escaper = $objectManager->getObject(Escaper::class); -+ $this->configView = $this->createMock(View::class); -+ -+ $this->viewConfig = $this->createConfiguredMock( -+ Config::class, -+ [ -+ 'getViewConfig' => $this->configView -+ ] -+ ); -+ -+ $this->context = $this->createConfiguredMock( -+ Context::class, -+ [ -+ 'getEscaper' => $this->escaper, -+ 'getViewConfig' => $this->viewConfig -+ ] -+ ); -+ -+ $this->gallery = $this->createMock(Gallery::class); -+ -+ $this->jsonSerializer = $objectManager->getObject( -+ Json::class -+ ); -+ -+ $this->model = $objectManager->getObject(GalleryOptions::class, [ -+ 'context' => $this->context, -+ 'jsonSerializer' => $this->jsonSerializer, -+ 'gallery' => $this->gallery -+ ]); -+ } -+ -+ public function testGetOptionsJson() -+ { -+ $configMap = [ -+ ['Magento_Catalog', 'gallery/nav', 'thumbs'], -+ ['Magento_Catalog', 'gallery/loop', false], -+ ['Magento_Catalog', 'gallery/keyboard', true], -+ ['Magento_Catalog', 'gallery/arrows', true], -+ ['Magento_Catalog', 'gallery/caption', false], -+ ['Magento_Catalog', 'gallery/allowfullscreen', true], -+ ['Magento_Catalog', 'gallery/navdir', 'horizontal'], -+ ['Magento_Catalog', 'gallery/navarrows', true], -+ ['Magento_Catalog', 'gallery/navtype', 'slides'], -+ ['Magento_Catalog', 'gallery/thumbmargin', '5'], -+ ['Magento_Catalog', 'gallery/transition/effect', 'slide'], -+ ['Magento_Catalog', 'gallery/transition/duration', '500'], -+ ]; -+ -+ $imageAttributesMap = [ -+ ['product_page_image_medium','height',null, 100], -+ ['product_page_image_medium','width',null, 200], -+ ['product_page_image_small','height',null, 300], -+ ['product_page_image_small','width',null, 400] -+ ]; -+ -+ $this->configView->expects($this->any()) -+ ->method('getVarValue') -+ ->will($this->returnValueMap($configMap)); -+ $this->gallery->expects($this->any()) -+ ->method('getImageAttribute') -+ ->will($this->returnValueMap($imageAttributesMap)); -+ -+ $json = $this->model->getOptionsJson(); -+ -+ $decodedJson = $this->jsonSerializer->unserialize($json); -+ -+ $this->assertSame('thumbs', $decodedJson['nav']); -+ $this->assertSame(false, $decodedJson['loop']); -+ $this->assertSame(true, $decodedJson['keyboard']); -+ $this->assertSame(true, $decodedJson['arrows']); -+ $this->assertSame(false, $decodedJson['showCaption']); -+ $this->assertSame(true, $decodedJson['allowfullscreen']); -+ $this->assertSame('horizontal', $decodedJson['navdir']); -+ $this->assertSame(true, $decodedJson['navarrows']); -+ $this->assertSame('slides', $decodedJson['navtype']); -+ $this->assertSame(5, $decodedJson['thumbmargin']); -+ $this->assertSame('slide', $decodedJson['transition']); -+ $this->assertSame(500, $decodedJson['transitionduration']); -+ $this->assertSame(100, $decodedJson['height']); -+ $this->assertSame(200, $decodedJson['width']); -+ $this->assertSame(300, $decodedJson['thumbheight']); -+ $this->assertSame(400, $decodedJson['thumbwidth']); -+ } -+ -+ public function testGetFSOptionsJson() -+ { -+ $configMap = [ -+ ['Magento_Catalog', 'gallery/fullscreen/nav', false], -+ ['Magento_Catalog', 'gallery/fullscreen/loop', true], -+ ['Magento_Catalog', 'gallery/fullscreen/keyboard', true], -+ ['Magento_Catalog', 'gallery/fullscreen/arrows', false], -+ ['Magento_Catalog', 'gallery/fullscreen/caption', true], -+ ['Magento_Catalog', 'gallery/fullscreen/navdir', 'vertical'], -+ ['Magento_Catalog', 'gallery/fullscreen/navarrows', false], -+ ['Magento_Catalog', 'gallery/fullscreen/navtype', 'thumbs'], -+ ['Magento_Catalog', 'gallery/fullscreen/thumbmargin', '10'], -+ ['Magento_Catalog', 'gallery/fullscreen/transition/effect', 'dissolve'], -+ ['Magento_Catalog', 'gallery/fullscreen/transition/duration', '300'] -+ ]; -+ -+ $this->configView->expects($this->any()) -+ ->method('getVarValue') -+ ->will($this->returnValueMap($configMap)); -+ -+ $json = $this->model->getFSOptionsJson(); -+ -+ $decodedJson = $this->jsonSerializer->unserialize($json); -+ -+ //Note, this tests the special case for nav variable set to false. It -+ //Should not be converted to boolean. -+ $this->assertSame('false', $decodedJson['nav']); -+ $this->assertSame(true, $decodedJson['loop']); -+ $this->assertSame(false, $decodedJson['arrows']); -+ $this->assertSame(true, $decodedJson['keyboard']); -+ $this->assertSame(true, $decodedJson['showCaption']); -+ $this->assertSame('vertical', $decodedJson['navdir']); -+ $this->assertSame(false, $decodedJson['navarrows']); -+ $this->assertSame(10, $decodedJson['thumbmargin']); -+ $this->assertSame('thumbs', $decodedJson['navtype']); -+ $this->assertSame('dissolve', $decodedJson['transition']); -+ $this->assertSame(300, $decodedJson['transitionduration']); -+ } -+ -+ public function testGetOptionsJsonOptionals() -+ { -+ $configMap = [ -+ ['Magento_Catalog', 'gallery/fullscreen/thumbmargin', false], -+ ['Magento_Catalog', 'gallery/fullscreen/transition/duration', false] -+ ]; -+ -+ $this->configView->expects($this->any()) -+ ->method('getVarValue') -+ ->will($this->returnValueMap($configMap)); -+ -+ $json = $this->model->getOptionsJson(); -+ -+ $decodedJson = $this->jsonSerializer->unserialize($json); -+ -+ $this->assertArrayNotHasKey('thumbmargin', $decodedJson); -+ $this->assertArrayNotHasKey('transitionduration', $decodedJson); -+ } -+ -+ public function testGetFSOptionsJsonOptionals() -+ { -+ $configMap = [ -+ ['Magento_Catalog', 'gallery/fullscreen/keyboard', false], -+ ['Magento_Catalog', 'gallery/fullscreen/thumbmargin', false], -+ ['Magento_Catalog', 'gallery/fullscreen/transition/duration', false] -+ ]; -+ -+ $this->configView->expects($this->any()) -+ ->method('getVarValue') -+ ->will($this->returnValueMap($configMap)); -+ -+ $json = $this->model->getFSOptionsJson(); -+ -+ $decodedJson = $this->jsonSerializer->unserialize($json); -+ -+ $this->assertArrayNotHasKey('thumbmargin', $decodedJson); -+ $this->assertArrayNotHasKey('keyboard', $decodedJson); -+ $this->assertArrayNotHasKey('transitionduration', $decodedJson); -+ } -+} -diff --git a/app/code/Magento/Catalog/Test/Unit/Block/Product/View/GalleryTest.php b/app/code/Magento/Catalog/Test/Unit/Block/Product/View/GalleryTest.php -index c9b7dc50beb..a81d8b1c9fc 100644 ---- a/app/code/Magento/Catalog/Test/Unit/Block/Product/View/GalleryTest.php -+++ b/app/code/Magento/Catalog/Test/Unit/Block/Product/View/GalleryTest.php -@@ -91,6 +91,94 @@ class GalleryTest extends \PHPUnit\Framework\TestCase - ]); - } - -+ public function testGetGalleryImagesJsonWithLabel() -+ { -+ $this->prepareGetGalleryImagesJsonMocks(); -+ $json = $this->model->getGalleryImagesJson(); -+ $decodedJson = json_decode($json, true); -+ $this->assertEquals('product_page_image_small_url', $decodedJson[0]['thumb']); -+ $this->assertEquals('product_page_image_medium_url', $decodedJson[0]['img']); -+ $this->assertEquals('product_page_image_large_url', $decodedJson[0]['full']); -+ $this->assertEquals('test_label', $decodedJson[0]['caption']); -+ $this->assertEquals('2', $decodedJson[0]['position']); -+ $this->assertEquals(false, $decodedJson[0]['isMain']); -+ $this->assertEquals('test_media_type', $decodedJson[0]['type']); -+ $this->assertEquals('test_video_url', $decodedJson[0]['videoUrl']); -+ } -+ -+ public function testGetGalleryImagesJsonWithoutLabel() -+ { -+ $this->prepareGetGalleryImagesJsonMocks(false); -+ $json = $this->model->getGalleryImagesJson(); -+ $decodedJson = json_decode($json, true); -+ $this->assertEquals('test_product_name', $decodedJson[0]['caption']); -+ } -+ -+ private function prepareGetGalleryImagesJsonMocks($hasLabel = true) -+ { -+ $storeMock = $this->getMockBuilder(\Magento\Store\Model\Store::class) -+ ->disableOriginalConstructor() -+ ->getMock(); -+ -+ $productMock = $this->getMockBuilder(\Magento\Catalog\Model\Product::class) -+ ->disableOriginalConstructor() -+ ->getMock(); -+ -+ $productTypeMock = $this->getMockBuilder(\Magento\Catalog\Model\Product\Type\AbstractType::class) -+ ->disableOriginalConstructor() -+ ->getMock(); -+ $productTypeMock->expects($this->any()) -+ ->method('getStoreFilter') -+ ->with($productMock) -+ ->willReturn($storeMock); -+ -+ $productMock->expects($this->any()) -+ ->method('getTypeInstance') -+ ->willReturn($productTypeMock); -+ $productMock->expects($this->any()) -+ ->method('getMediaGalleryImages') -+ ->willReturn($this->getImagesCollectionWithPopulatedDataObject($hasLabel)); -+ $productMock->expects($this->any()) -+ ->method('getName') -+ ->willReturn('test_product_name'); -+ -+ $this->registry->expects($this->any()) -+ ->method('registry') -+ ->with('product') -+ ->willReturn($productMock); -+ -+ $this->imageHelper = $this->getMockBuilder(\Magento\Catalog\Helper\Image::class) -+ ->setMethods(['init', 'setImageFile', 'getUrl']) -+ ->disableOriginalConstructor() -+ ->getMock(); -+ -+ $this->imageHelper->expects($this->any()) -+ ->method('init') -+ ->willReturnMap([ -+ [$productMock, 'product_page_image_small', [], $this->imageHelper], -+ [$productMock, 'product_page_image_medium_no_frame', [], $this->imageHelper], -+ [$productMock, 'product_page_image_large_no_frame', [], $this->imageHelper], -+ ]) -+ ->willReturnSelf(); -+ $this->imageHelper->expects($this->any()) -+ ->method('setImageFile') -+ ->with('test_file') -+ ->willReturnSelf(); -+ $this->urlBuilder->expects($this->at(0)) -+ ->method('getUrl') -+ ->willReturn('product_page_image_small_url'); -+ $this->urlBuilder->expects($this->at(1)) -+ ->method('getUrl') -+ ->willReturn('product_page_image_medium_url'); -+ $this->urlBuilder->expects($this->at(2)) -+ ->method('getUrl') -+ ->willReturn('product_page_image_large_url'); -+ -+ $this->galleryImagesConfigMock->expects($this->exactly(2)) -+ ->method('getItems') -+ ->willReturn($this->getGalleryImagesConfigItems()); -+ } -+ - public function testGetGalleryImages() - { - $productMock = $this->createMock(Product::class); -@@ -163,4 +251,30 @@ class GalleryTest extends \PHPUnit\Framework\TestCase - ]) - ]; - } -+ -+ /** -+ * @return \Magento\Framework\Data\Collection -+ */ -+ private function getImagesCollectionWithPopulatedDataObject($hasLabel) -+ { -+ $collectionMock = $this->getMockBuilder(\Magento\Framework\Data\Collection::class) -+ ->disableOriginalConstructor() -+ ->getMock(); -+ -+ $items = [ -+ new \Magento\Framework\DataObject([ -+ 'file' => 'test_file', -+ 'label' => ($hasLabel ? 'test_label' : ''), -+ 'position' => '2', -+ 'media_type' => 'external-test_media_type', -+ "video_url" => 'test_video_url' -+ ]), -+ ]; -+ -+ $collectionMock->expects($this->any()) -+ ->method('getIterator') -+ ->willReturn(new \ArrayIterator($items)); -+ -+ return $collectionMock; -+ } - } -diff --git a/app/code/Magento/Catalog/Test/Unit/Block/Product/ViewTest.php b/app/code/Magento/Catalog/Test/Unit/Block/Product/ViewTest.php -index af208c016c7..6563bdeb149 100644 ---- a/app/code/Magento/Catalog/Test/Unit/Block/Product/ViewTest.php -+++ b/app/code/Magento/Catalog/Test/Unit/Block/Product/ViewTest.php -@@ -8,6 +8,9 @@ - - namespace Magento\Catalog\Test\Unit\Block\Product; - -+/** -+ * Class ViewTest -+ */ - class ViewTest extends \PHPUnit\Framework\TestCase - { - /** -@@ -25,6 +28,9 @@ class ViewTest extends \PHPUnit\Framework\TestCase - */ - protected $registryMock; - -+ /** -+ * @inheritDoc -+ */ - protected function setUp() - { - $helper = new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this); -@@ -36,6 +42,9 @@ class ViewTest extends \PHPUnit\Framework\TestCase - ); - } - -+ /** -+ * @return void -+ */ - public function testShouldRenderQuantity() - { - $productMock = $this->createMock(\Magento\Catalog\Model\Product::class); -@@ -61,28 +70,26 @@ class ViewTest extends \PHPUnit\Framework\TestCase - $this->assertEquals(false, $this->view->shouldRenderQuantity()); - } - -+ /** -+ * @return void -+ */ - public function testGetIdentities() - { - $productTags = ['cat_p_1']; - $product = $this->createMock(\Magento\Catalog\Model\Product::class); -- $category = $this->createMock(\Magento\Catalog\Model\Category::class); - - $product->expects($this->once()) - ->method('getIdentities') - ->will($this->returnValue($productTags)); -- $category->expects($this->once()) -- ->method('getId') -- ->will($this->returnValue(1)); - $this->registryMock->expects($this->any()) - ->method('registry') - ->will( - $this->returnValueMap( - [ - ['product', $product], -- ['current_category', $category], - ] - ) - ); -- $this->assertEquals(['cat_p_1', 'cat_c_1'], $this->view->getIdentities()); -+ $this->assertEquals($productTags, $this->view->getIdentities()); - } - } -diff --git a/app/code/Magento/Catalog/Test/Unit/Controller/Adminhtml/Category/DeleteTest.php b/app/code/Magento/Catalog/Test/Unit/Controller/Adminhtml/Category/DeleteTest.php -index af1ded69871..196b4df5b47 100644 ---- a/app/code/Magento/Catalog/Test/Unit/Controller/Adminhtml/Category/DeleteTest.php -+++ b/app/code/Magento/Catalog/Test/Unit/Controller/Adminhtml/Category/DeleteTest.php -@@ -71,7 +71,7 @@ class DeleteTest extends \PHPUnit\Framework\TestCase - false, - true, - true, -- ['addSuccess'] -+ ['addSuccessMessage'] - ); - $this->categoryRepository = $this->createMock(\Magento\Catalog\Api\CategoryRepositoryInterface::class); - $context->expects($this->any()) -diff --git a/app/code/Magento/Catalog/Test/Unit/Controller/Adminhtml/Category/RefreshPathTest.php b/app/code/Magento/Catalog/Test/Unit/Controller/Adminhtml/Category/RefreshPathTest.php -index 45de62e218c..adf00333721 100644 ---- a/app/code/Magento/Catalog/Test/Unit/Controller/Adminhtml/Category/RefreshPathTest.php -+++ b/app/code/Magento/Catalog/Test/Unit/Controller/Adminhtml/Category/RefreshPathTest.php -@@ -66,8 +66,8 @@ class RefreshPathTest extends \PHPUnit\Framework\TestCase - */ - public function testExecute() : void - { -- $value = ['id' => 3, 'path' => '1/2/3', 'parentId' => 2]; -- $result = '{"id":3,"path":"1/2/3","parentId":"2"}'; -+ $value = ['id' => 3, 'path' => '1/2/3', 'parentId' => 2, 'level' => 2]; -+ $result = '{"id":3,"path":"1/2/3","parentId":"2","level":"2"}'; - - $requestMock = $this->getMockForAbstractClass(\Magento\Framework\App\RequestInterface::class); - -diff --git a/app/code/Magento/Catalog/Test/Unit/Controller/Adminhtml/Category/SaveTest.php b/app/code/Magento/Catalog/Test/Unit/Controller/Adminhtml/Category/SaveTest.php -index 53f7fa0ed7e..74173dc926d 100644 ---- a/app/code/Magento/Catalog/Test/Unit/Controller/Adminhtml/Category/SaveTest.php -+++ b/app/code/Magento/Catalog/Test/Unit/Controller/Adminhtml/Category/SaveTest.php -@@ -102,7 +102,7 @@ class SaveTest extends \PHPUnit\Framework\TestCase - false, - true, - true, -- ['addSuccess', 'getMessages'] -+ ['addSuccessMessage', 'getMessages'] - ); - - $this->save = $this->objectManager->getObject( -@@ -392,7 +392,7 @@ class SaveTest extends \PHPUnit\Framework\TestCase - $categoryMock->expects($this->once()) - ->method('save'); - $this->messageManagerMock->expects($this->once()) -- ->method('addSuccess') -+ ->method('addSuccessMessage') - ->with(__('You saved the category.')); - $categoryMock->expects($this->at(1)) - ->method('getId') -diff --git a/app/code/Magento/Catalog/Test/Unit/Controller/Adminhtml/Product/Action/Attribute/EditTest.php b/app/code/Magento/Catalog/Test/Unit/Controller/Adminhtml/Product/Action/Attribute/EditTest.php -index 5a977b79346..0ddd89afeac 100644 ---- a/app/code/Magento/Catalog/Test/Unit/Controller/Adminhtml/Product/Action/Attribute/EditTest.php -+++ b/app/code/Magento/Catalog/Test/Unit/Controller/Adminhtml/Product/Action/Attribute/EditTest.php -@@ -94,7 +94,7 @@ class EditTest extends \PHPUnit\Framework\TestCase - $messageManager = $this->getMockBuilder(\Magento\Framework\Message\ManagerInterface::class) - ->setMethods([]) - ->disableOriginalConstructor()->getMock(); -- $messageManager->expects($this->any())->method('addError')->willReturn(true); -+ $messageManager->expects($this->any())->method('addErrorMessage')->willReturn(true); - $this->context = $this->getMockBuilder(\Magento\Backend\App\Action\Context::class) - ->setMethods(['getRequest', 'getObjectManager', 'getMessageManager', 'getResultRedirectFactory']) - ->disableOriginalConstructor()->getMock(); -diff --git a/app/code/Magento/Catalog/Test/Unit/Controller/Adminhtml/Product/Action/Attribute/SaveTest.php b/app/code/Magento/Catalog/Test/Unit/Controller/Adminhtml/Product/Action/Attribute/SaveTest.php -deleted file mode 100644 -index c88a008efb1..00000000000 ---- a/app/code/Magento/Catalog/Test/Unit/Controller/Adminhtml/Product/Action/Attribute/SaveTest.php -+++ /dev/null -@@ -1,258 +0,0 @@ --<?php --/** -- * -- * Copyright © Magento, Inc. All rights reserved. -- * See COPYING.txt for license details. -- */ --namespace Magento\Catalog\Test\Unit\Controller\Adminhtml\Product\Action\Attribute; -- --/** -- * @SuppressWarnings(PHPMD.TooManyFields) -- * @SuppressWarnings(PHPMD.CouplingBetweenObjects) -- */ --class SaveTest extends \PHPUnit\Framework\TestCase --{ -- /** @var \Magento\Catalog\Controller\Adminhtml\Product\Action\Attribute\Save */ -- protected $object; -- -- /** @var \Magento\Catalog\Helper\Product\Edit\Action\Attribute|\PHPUnit_Framework_MockObject_MockObject */ -- protected $attributeHelper; -- -- /** @var \PHPUnit_Framework_MockObject_MockObject */ -- protected $dataObjectHelperMock; -- -- /** @var \Magento\CatalogInventory\Model\Indexer\Stock\Processor|\PHPUnit_Framework_MockObject_MockObject */ -- protected $stockIndexerProcessor; -- -- /** @var \Magento\Backend\App\Action\Context|\PHPUnit_Framework_MockObject_MockObject */ -- protected $context; -- -- /** @var \Magento\Framework\App\Request\Http|\PHPUnit_Framework_MockObject_MockObject */ -- protected $request; -- -- /** @var \Magento\Framework\App\Response\Http|\PHPUnit_Framework_MockObject_MockObject */ -- protected $response; -- -- /** @var \Magento\Framework\ObjectManagerInterface|\PHPUnit_Framework_MockObject_MockObject */ -- protected $objectManager; -- -- /** @var \Magento\Framework\Event\ManagerInterface|\PHPUnit_Framework_MockObject_MockObject */ -- protected $eventManager; -- -- /** @var \Magento\Framework\UrlInterface|\PHPUnit_Framework_MockObject_MockObject */ -- protected $url; -- -- /** @var \Magento\Framework\App\Response\RedirectInterface|\PHPUnit_Framework_MockObject_MockObject */ -- protected $redirect; -- -- /** @var \Magento\Framework\App\ActionFlag|\PHPUnit_Framework_MockObject_MockObject */ -- protected $actionFlag; -- -- /** @var \Magento\Framework\App\ViewInterface|\PHPUnit_Framework_MockObject_MockObject */ -- protected $view; -- -- /** @var \Magento\Framework\Message\ManagerInterface|\PHPUnit_Framework_MockObject_MockObject */ -- protected $messageManager; -- -- /** @var \Magento\Backend\Model\Session|\PHPUnit_Framework_MockObject_MockObject */ -- protected $session; -- -- /** @var \Magento\Framework\AuthorizationInterface|\PHPUnit_Framework_MockObject_MockObject */ -- protected $authorization; -- -- /** @var \Magento\Backend\Model\Auth|\PHPUnit_Framework_MockObject_MockObject */ -- protected $auth; -- -- /** @var \Magento\Backend\Helper\Data|\PHPUnit_Framework_MockObject_MockObject */ -- protected $helper; -- -- /** @var \Magento\Backend\Model\UrlInterface|\PHPUnit_Framework_MockObject_MockObject */ -- protected $backendUrl; -- -- /** @var \Magento\Framework\Data\Form\FormKey\Validator|\PHPUnit_Framework_MockObject_MockObject */ -- protected $formKeyValidator; -- -- /** @var \Magento\Framework\Locale\ResolverInterface|\PHPUnit_Framework_MockObject_MockObject */ -- protected $localeResolver; -- -- /** @var \Magento\Catalog\Model\Product|\PHPUnit_Framework_MockObject_MockObject */ -- protected $product; -- -- /** @var \Magento\CatalogInventory\Api\StockRegistryInterface|\PHPUnit_Framework_MockObject_MockObject */ -- protected $stockItemService; -- -- /** @var \PHPUnit_Framework_MockObject_MockObject */ -- protected $stockItem; -- -- /** @var \Magento\CatalogInventory\Api\StockConfigurationInterface|\PHPUnit_Framework_MockObject_MockObject */ -- protected $stockConfig; -- -- /** @var \PHPUnit_Framework_MockObject_MockObject */ -- protected $stockItemRepository; -- -- /** -- * @var \Magento\Backend\Model\View\Result\RedirectFactory|\PHPUnit_Framework_MockObject_MockObject -- */ -- protected $resultRedirectFactory; -- -- protected function setUp() -- { -- $this->attributeHelper = $this->createPartialMock( -- \Magento\Catalog\Helper\Product\Edit\Action\Attribute::class, -- ['getProductIds', 'getSelectedStoreId', 'getStoreWebsiteId'] -- ); -- -- $this->dataObjectHelperMock = $this->getMockBuilder(\Magento\Framework\Api\DataObjectHelper::class) -- ->disableOriginalConstructor() -- ->getMock(); -- -- $this->stockIndexerProcessor = $this->createPartialMock( -- \Magento\CatalogInventory\Model\Indexer\Stock\Processor::class, -- ['reindexList'] -- ); -- -- $resultRedirect = $this->getMockBuilder(\Magento\Backend\Model\View\Result\Redirect::class) -- ->disableOriginalConstructor() -- ->getMock(); -- -- $this->resultRedirectFactory = $this->getMockBuilder(\Magento\Backend\Model\View\Result\RedirectFactory::class) -- ->disableOriginalConstructor() -- ->setMethods(['create']) -- ->getMock(); -- $this->resultRedirectFactory->expects($this->atLeastOnce()) -- ->method('create') -- ->willReturn($resultRedirect); -- -- $this->prepareContext(); -- -- $this->object = (new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this))->getObject( -- \Magento\Catalog\Controller\Adminhtml\Product\Action\Attribute\Save::class, -- [ -- 'context' => $this->context, -- 'attributeHelper' => $this->attributeHelper, -- 'stockIndexerProcessor' => $this->stockIndexerProcessor, -- 'dataObjectHelper' => $this->dataObjectHelperMock, -- ] -- ); -- } -- -- /** -- * @SuppressWarnings(PHPMD.ExcessiveMethodLength) -- */ -- protected function prepareContext() -- { -- $this->stockItemRepository = $this->getMockBuilder( -- \Magento\CatalogInventory\Api\StockItemRepositoryInterface::class -- )->disableOriginalConstructor()->getMock(); -- -- $this->request = $this->getMockBuilder(\Magento\Framework\App\Request\Http::class) -- ->disableOriginalConstructor()->getMock(); -- $this->response = $this->createMock(\Magento\Framework\App\Response\Http::class); -- $this->objectManager = $this->createMock(\Magento\Framework\ObjectManagerInterface::class); -- $this->eventManager = $this->createMock(\Magento\Framework\Event\ManagerInterface::class); -- $this->url = $this->createMock(\Magento\Framework\UrlInterface::class); -- $this->redirect = $this->createMock(\Magento\Framework\App\Response\RedirectInterface::class); -- $this->actionFlag = $this->createMock(\Magento\Framework\App\ActionFlag::class); -- $this->view = $this->createMock(\Magento\Framework\App\ViewInterface::class); -- $this->messageManager = $this->createMock(\Magento\Framework\Message\ManagerInterface::class); -- $this->session = $this->createMock(\Magento\Backend\Model\Session::class); -- $this->authorization = $this->createMock(\Magento\Framework\AuthorizationInterface::class); -- $this->auth = $this->createMock(\Magento\Backend\Model\Auth::class); -- $this->helper = $this->createMock(\Magento\Backend\Helper\Data::class); -- $this->backendUrl = $this->createMock(\Magento\Backend\Model\UrlInterface::class); -- $this->formKeyValidator = $this->createMock(\Magento\Framework\Data\Form\FormKey\Validator::class); -- $this->localeResolver = $this->createMock(\Magento\Framework\Locale\ResolverInterface::class); -- -- $this->context = $this->context = $this->createPartialMock(\Magento\Backend\App\Action\Context::class, [ -- 'getRequest', -- 'getResponse', -- 'getObjectManager', -- 'getEventManager', -- 'getUrl', -- 'getRedirect', -- 'getActionFlag', -- 'getView', -- 'getMessageManager', -- 'getSession', -- 'getAuthorization', -- 'getAuth', -- 'getHelper', -- 'getBackendUrl', -- 'getFormKeyValidator', -- 'getLocaleResolver', -- 'getResultRedirectFactory' -- ]); -- $this->context->expects($this->any())->method('getRequest')->willReturn($this->request); -- $this->context->expects($this->any())->method('getResponse')->willReturn($this->response); -- $this->context->expects($this->any())->method('getObjectManager')->willReturn($this->objectManager); -- $this->context->expects($this->any())->method('getEventManager')->willReturn($this->eventManager); -- $this->context->expects($this->any())->method('getUrl')->willReturn($this->url); -- $this->context->expects($this->any())->method('getRedirect')->willReturn($this->redirect); -- $this->context->expects($this->any())->method('getActionFlag')->willReturn($this->actionFlag); -- $this->context->expects($this->any())->method('getView')->willReturn($this->view); -- $this->context->expects($this->any())->method('getMessageManager')->willReturn($this->messageManager); -- $this->context->expects($this->any())->method('getSession')->willReturn($this->session); -- $this->context->expects($this->any())->method('getAuthorization')->willReturn($this->authorization); -- $this->context->expects($this->any())->method('getAuth')->willReturn($this->auth); -- $this->context->expects($this->any())->method('getHelper')->willReturn($this->helper); -- $this->context->expects($this->any())->method('getBackendUrl')->willReturn($this->backendUrl); -- $this->context->expects($this->any())->method('getFormKeyValidator')->willReturn($this->formKeyValidator); -- $this->context->expects($this->any())->method('getLocaleResolver')->willReturn($this->localeResolver); -- $this->context->expects($this->any()) -- ->method('getResultRedirectFactory') -- ->willReturn($this->resultRedirectFactory); -- -- $this->product = $this->createPartialMock( -- \Magento\Catalog\Model\Product::class, -- ['isProductsHasSku', '__wakeup'] -- ); -- -- $this->stockItemService = $this->getMockBuilder(\Magento\CatalogInventory\Api\StockRegistryInterface::class) -- ->disableOriginalConstructor() -- ->setMethods(['getStockItem', 'saveStockItem']) -- ->getMockForAbstractClass(); -- $this->stockItem = $this->getMockBuilder(\Magento\CatalogInventory\Api\Data\StockItemInterface::class) -- ->setMethods(['getId', 'getProductId']) -- ->disableOriginalConstructor() -- ->getMockForAbstractClass(); -- -- $this->stockConfig = $this->getMockBuilder(\Magento\CatalogInventory\Api\StockConfigurationInterface::class) -- ->disableOriginalConstructor() -- ->getMockForAbstractClass(); -- -- $this->objectManager->expects($this->any())->method('create')->will($this->returnValueMap([ -- [\Magento\Catalog\Model\Product::class, [], $this->product], -- [\Magento\CatalogInventory\Api\StockRegistryInterface::class, [], $this->stockItemService], -- [\Magento\CatalogInventory\Api\StockItemRepositoryInterface::class, [], $this->stockItemRepository], -- ])); -- -- $this->objectManager->expects($this->any())->method('get')->will($this->returnValueMap([ -- [\Magento\CatalogInventory\Api\StockConfigurationInterface::class, $this->stockConfig], -- ])); -- } -- -- public function testExecuteThatProductIdsAreObtainedFromAttributeHelper() -- { -- $this->attributeHelper->expects($this->any())->method('getProductIds')->will($this->returnValue([5])); -- $this->attributeHelper->expects($this->any())->method('getSelectedStoreId')->will($this->returnValue([1])); -- $this->attributeHelper->expects($this->any())->method('getStoreWebsiteId')->will($this->returnValue(1)); -- $this->stockConfig->expects($this->any())->method('getConfigItemOptions')->will($this->returnValue([])); -- $this->dataObjectHelperMock->expects($this->any()) -- ->method('populateWithArray') -- ->with($this->stockItem, $this->anything(), \Magento\CatalogInventory\Api\Data\StockItemInterface::class) -- ->willReturnSelf(); -- $this->product->expects($this->any())->method('isProductsHasSku')->with([5])->will($this->returnValue(true)); -- $this->stockItemService->expects($this->any())->method('getStockItem')->with(5, 1) -- ->will($this->returnValue($this->stockItem)); -- $this->stockIndexerProcessor->expects($this->any())->method('reindexList')->with([5]); -- -- $this->request->expects($this->any())->method('getParam')->will($this->returnValueMap([ -- ['inventory', [], [7]], -- ])); -- -- $this->messageManager->expects($this->never())->method('addError'); -- $this->messageManager->expects($this->never())->method('addException'); -- -- $this->object->execute(); -- } --} -diff --git a/app/code/Magento/Catalog/Test/Unit/Controller/Adminhtml/Product/Attribute/SaveTest.php b/app/code/Magento/Catalog/Test/Unit/Controller/Adminhtml/Product/Attribute/SaveTest.php -index f493cbc88f1..30d3503e464 100644 ---- a/app/code/Magento/Catalog/Test/Unit/Controller/Adminhtml/Product/Attribute/SaveTest.php -+++ b/app/code/Magento/Catalog/Test/Unit/Controller/Adminhtml/Product/Attribute/SaveTest.php -@@ -5,7 +5,10 @@ - */ - namespace Magento\Catalog\Test\Unit\Controller\Adminhtml\Product\Attribute; - -+use Magento\Catalog\Api\Data\ProductAttributeInterface; - use Magento\Catalog\Controller\Adminhtml\Product\Attribute\Save; -+use Magento\Eav\Model\Validator\Attribute\Code as AttributeCodeValidator; -+use Magento\Framework\Serialize\Serializer\FormData; - use Magento\Catalog\Test\Unit\Controller\Adminhtml\Product\AttributeTest; - use Magento\Catalog\Model\Product\AttributeSet\BuildFactory; - use Magento\Catalog\Model\Product\AttributeSet\Build; -@@ -13,11 +16,14 @@ use Magento\Catalog\Model\ResourceModel\Eav\AttributeFactory; - use Magento\Eav\Api\Data\AttributeSetInterface; - use Magento\Eav\Model\Adminhtml\System\Config\Source\Inputtype\ValidatorFactory; - use Magento\Eav\Model\ResourceModel\Entity\Attribute\Group\CollectionFactory; -+use Magento\Framework\Controller\ResultFactory; - use Magento\Framework\Filter\FilterManager; - use Magento\Catalog\Helper\Product as ProductHelper; -+use Magento\Framework\View\Element\Messages; - use Magento\Framework\View\LayoutFactory; - use Magento\Backend\Model\View\Result\Redirect as ResultRedirect; - use Magento\Eav\Model\Adminhtml\System\Config\Source\Inputtype\Validator as InputTypeValidator; -+use Magento\Framework\View\LayoutInterface; - - /** - * @SuppressWarnings(PHPMD.CouplingBetweenObjects) -@@ -79,6 +85,21 @@ class SaveTest extends AttributeTest - */ - protected $inputTypeValidatorMock; - -+ /** -+ * @var FormData|\PHPUnit_Framework_MockObject_MockObject -+ */ -+ private $formDataSerializerMock; -+ -+ /** -+ * @var ProductAttributeInterface|\PHPUnit_Framework_MockObject_MockObject -+ */ -+ private $productAttributeMock; -+ -+ /** -+ * @var AttributeCodeValidator|\PHPUnit_Framework_MockObject_MockObject -+ */ -+ private $attributeCodeValidatorMock; -+ - protected function setUp() - { - parent::setUp(); -@@ -108,6 +129,7 @@ class SaveTest extends AttributeTest - ->disableOriginalConstructor() - ->getMock(); - $this->redirectMock = $this->getMockBuilder(ResultRedirect::class) -+ ->setMethods(['setData', 'setPath']) - ->disableOriginalConstructor() - ->getMock(); - $this->attributeSetMock = $this->getMockBuilder(AttributeSetInterface::class) -@@ -119,6 +141,15 @@ class SaveTest extends AttributeTest - $this->inputTypeValidatorMock = $this->getMockBuilder(InputTypeValidator::class) - ->disableOriginalConstructor() - ->getMock(); -+ $this->formDataSerializerMock = $this->getMockBuilder(FormData::class) -+ ->disableOriginalConstructor() -+ ->getMock(); -+ $this->attributeCodeValidatorMock = $this->getMockBuilder(AttributeCodeValidator::class) -+ ->disableOriginalConstructor() -+ ->getMock(); -+ $this->productAttributeMock = $this->getMockBuilder(ProductAttributeInterface::class) -+ ->setMethods(['getId', 'get']) -+ ->getMockForAbstractClass(); - - $this->buildFactoryMock->expects($this->any()) - ->method('create') -@@ -126,6 +157,9 @@ class SaveTest extends AttributeTest - $this->validatorFactoryMock->expects($this->any()) - ->method('create') - ->willReturn($this->inputTypeValidatorMock); -+ $this->attributeFactoryMock -+ ->method('create') -+ ->willReturn($this->productAttributeMock); - } - - /** -@@ -145,11 +179,24 @@ class SaveTest extends AttributeTest - 'validatorFactory' => $this->validatorFactoryMock, - 'groupCollectionFactory' => $this->groupCollectionFactoryMock, - 'layoutFactory' => $this->layoutFactoryMock, -+ 'formDataSerializer' => $this->formDataSerializerMock, -+ 'attributeCodeValidator' => $this->attributeCodeValidatorMock - ]); - } - - public function testExecuteWithEmptyData() - { -+ $this->requestMock->expects($this->any()) -+ ->method('getParam') -+ ->willReturnMap([ -+ ['isAjax', null, null], -+ ['serialized_options', '[]', ''], -+ ]); -+ $this->formDataSerializerMock -+ ->expects($this->once()) -+ ->method('unserialize') -+ ->with('') -+ ->willReturn([]); - $this->requestMock->expects($this->once()) - ->method('getPostValue') - ->willReturn([]); -@@ -170,6 +217,27 @@ class SaveTest extends AttributeTest - 'frontend_input' => 'test_frontend_input', - ]; - -+ $this->requestMock->expects($this->any()) -+ ->method('getParam') -+ ->willReturnMap([ -+ ['isAjax', null, null], -+ ['serialized_options', '[]', ''], -+ ]); -+ $this->formDataSerializerMock -+ ->expects($this->once()) -+ ->method('unserialize') -+ ->with('') -+ ->willReturn([]); -+ $this->productAttributeMock -+ ->method('getId') -+ ->willReturn(1); -+ $this->productAttributeMock -+ ->method('getAttributeCode') -+ ->willReturn('test_code'); -+ $this->attributeCodeValidatorMock -+ ->method('isValid') -+ ->with('test_code') -+ ->willReturn(true); - $this->requestMock->expects($this->once()) - ->method('getPostValue') - ->willReturn($data); -@@ -203,4 +271,80 @@ class SaveTest extends AttributeTest - - $this->assertInstanceOf(ResultRedirect::class, $this->getModel()->execute()); - } -+ -+ /** -+ * @throws \Magento\Framework\Exception\NotFoundException -+ */ -+ public function testExecuteWithOptionsDataError() -+ { -+ $serializedOptions = '{"key":"value"}'; -+ $message = "The attribute couldn't be saved due to an error. Verify your information and try again. " -+ . "If the error persists, please try again later."; -+ -+ $this->requestMock->expects($this->any()) -+ ->method('getParam') -+ ->willReturnMap([ -+ ['isAjax', null, true], -+ ['serialized_options', '[]', $serializedOptions], -+ ]); -+ $this->formDataSerializerMock -+ ->expects($this->once()) -+ ->method('unserialize') -+ ->with($serializedOptions) -+ ->willThrowException(new \InvalidArgumentException('Some exception')); -+ $this->messageManager -+ ->expects($this->once()) -+ ->method('addErrorMessage') -+ ->with($message); -+ $this->addReturnResultConditions('catalog/*/edit', ['_current' => true], ['error' => true]); -+ -+ $this->getModel()->execute(); -+ } -+ -+ /** -+ * @param string $path -+ * @param array $params -+ * @param array $response -+ * @return mixed -+ * @SuppressWarnings(PHPMD.UnusedFormalParameter) -+ */ -+ private function addReturnResultConditions(string $path = '', array $params = [], array $response = []) -+ { -+ $layoutMock = $this->getMockBuilder(LayoutInterface::class) -+ ->setMethods(['initMessages', 'getMessagesBlock']) -+ ->getMockForAbstractClass(); -+ $this->layoutFactoryMock -+ ->expects($this->once()) -+ ->method('create') -+ ->with() -+ ->willReturn($layoutMock); -+ $layoutMock -+ ->method('initMessages') -+ ->with(); -+ $messageBlockMock = $this->getMockBuilder(Messages::class) -+ ->disableOriginalConstructor() -+ ->getMock(); -+ $layoutMock -+ ->expects($this->once()) -+ ->method('getMessagesBlock') -+ ->willReturn($messageBlockMock); -+ $messageBlockMock -+ ->expects($this->once()) -+ ->method('getGroupedHtml') -+ ->willReturn('message1'); -+ $this->resultFactoryMock -+ ->expects($this->once()) -+ ->method('create') -+ ->with(ResultFactory::TYPE_JSON) -+ ->willReturn($this->redirectMock); -+ $response = array_merge($response, [ -+ 'messages' => ['message1'], -+ 'params' => $params, -+ ]); -+ $this->redirectMock -+ ->expects($this->once()) -+ ->method('setData') -+ ->with($response) -+ ->willReturnSelf(); -+ } - } -diff --git a/app/code/Magento/Catalog/Test/Unit/Controller/Adminhtml/Product/Attribute/ValidateTest.php b/app/code/Magento/Catalog/Test/Unit/Controller/Adminhtml/Product/Attribute/ValidateTest.php -index 9c747393cc7..742148b1bf7 100644 ---- a/app/code/Magento/Catalog/Test/Unit/Controller/Adminhtml/Product/Attribute/ValidateTest.php -+++ b/app/code/Magento/Catalog/Test/Unit/Controller/Adminhtml/Product/Attribute/ValidateTest.php -@@ -6,6 +6,8 @@ - namespace Magento\Catalog\Test\Unit\Controller\Adminhtml\Product\Attribute; - - use Magento\Catalog\Controller\Adminhtml\Product\Attribute\Validate; -+use Magento\Eav\Model\Validator\Attribute\Code as AttributeCodeValidator; -+use Magento\Framework\Serialize\Serializer\FormData; - use Magento\Catalog\Model\ResourceModel\Eav\Attribute; - use Magento\Catalog\Test\Unit\Controller\Adminhtml\Product\AttributeTest; - use Magento\Eav\Model\Entity\Attribute\Set as AttributeSet; -@@ -61,6 +63,16 @@ class ValidateTest extends AttributeTest - */ - protected $layoutMock; - -+ /** -+ * @var FormData|\PHPUnit_Framework_MockObject_MockObject -+ */ -+ private $formDataSerializerMock; -+ -+ /** -+ * @var AttributeCodeValidator|\PHPUnit_Framework_MockObject_MockObject -+ */ -+ private $attributeCodeValidatorMock; -+ - protected function setUp() - { - parent::setUp(); -@@ -86,6 +98,12 @@ class ValidateTest extends AttributeTest - ->getMock(); - $this->layoutMock = $this->getMockBuilder(LayoutInterface::class) - ->getMockForAbstractClass(); -+ $this->formDataSerializerMock = $this->getMockBuilder(FormData::class) -+ ->disableOriginalConstructor() -+ ->getMock(); -+ $this->attributeCodeValidatorMock = $this->getMockBuilder(AttributeCodeValidator::class) -+ ->disableOriginalConstructor() -+ ->getMock(); - - $this->contextMock->expects($this->any()) - ->method('getObjectManager') -@@ -100,25 +118,29 @@ class ValidateTest extends AttributeTest - return $this->objectManager->getObject( - Validate::class, - [ -- 'context' => $this->contextMock, -- 'attributeLabelCache' => $this->attributeLabelCacheMock, -- 'coreRegistry' => $this->coreRegistryMock, -- 'resultPageFactory' => $this->resultPageFactoryMock, -- 'resultJsonFactory' => $this->resultJsonFactoryMock, -- 'layoutFactory' => $this->layoutFactoryMock, -- 'multipleAttributeList' => ['select' => 'option'] -+ 'context' => $this->contextMock, -+ 'attributeLabelCache' => $this->attributeLabelCacheMock, -+ 'coreRegistry' => $this->coreRegistryMock, -+ 'resultPageFactory' => $this->resultPageFactoryMock, -+ 'resultJsonFactory' => $this->resultJsonFactoryMock, -+ 'layoutFactory' => $this->layoutFactoryMock, -+ 'multipleAttributeList' => ['select' => 'option'], -+ 'formDataSerializer' => $this->formDataSerializerMock, -+ 'attributeCodeValidator' => $this->attributeCodeValidatorMock, - ] - ); - } - - public function testExecute() - { -+ $serializedOptions = '{"key":"value"}'; - $this->requestMock->expects($this->any()) - ->method('getParam') - ->willReturnMap([ - ['frontend_label', null, 'test_frontend_label'], - ['attribute_code', null, 'test_attribute_code'], - ['new_attribute_set_name', null, 'test_attribute_set_name'], -+ ['serialized_options', '[]', $serializedOptions], - ]); - $this->objectManagerMock->expects($this->exactly(2)) - ->method('create') -@@ -129,6 +151,12 @@ class ValidateTest extends AttributeTest - $this->attributeMock->expects($this->once()) - ->method('loadByCode') - ->willReturnSelf(); -+ -+ $this->attributeCodeValidatorMock->expects($this->once()) -+ ->method('isValid') -+ ->with('test_attribute_code') -+ ->willReturn(true); -+ - $this->requestMock->expects($this->once()) - ->method('has') - ->with('new_attribute_set_name') -@@ -160,6 +188,7 @@ class ValidateTest extends AttributeTest - */ - public function testUniqueValidation(array $options, $isError) - { -+ $serializedOptions = '{"key":"value"}'; - $countFunctionCalls = ($isError) ? 6 : 5; - $this->requestMock->expects($this->exactly($countFunctionCalls)) - ->method('getParam') -@@ -167,10 +196,21 @@ class ValidateTest extends AttributeTest - ['frontend_label', null, null], - ['attribute_code', null, "test_attribute_code"], - ['new_attribute_set_name', null, 'test_attribute_set_name'], -- ['option', null, $options], -- ['message_key', null, Validate::DEFAULT_MESSAGE_KEY] -+ ['message_key', null, Validate::DEFAULT_MESSAGE_KEY], -+ ['serialized_options', '[]', $serializedOptions], - ]); - -+ $this->formDataSerializerMock -+ ->expects($this->once()) -+ ->method('unserialize') -+ ->with($serializedOptions) -+ ->willReturn($options); -+ -+ $this->attributeCodeValidatorMock->expects($this->once()) -+ ->method('isValid') -+ ->with('test_attribute_code') -+ ->willReturn(true); -+ - $this->objectManagerMock->expects($this->once()) - ->method('create') - ->willReturn($this->attributeMock); -@@ -203,67 +243,77 @@ class ValidateTest extends AttributeTest - return [ - 'no values' => [ - [ -- 'delete' => [ -- "option_0" => "", -- "option_1" => "", -- "option_2" => "", -- ] -+ 'option' => [ -+ 'delete' => [ -+ "option_0" => "", -+ "option_1" => "", -+ "option_2" => "", -+ ], -+ ], - ], false - ], - 'valid options' => [ - [ -- 'value' => [ -- "option_0" => [1, 0], -- "option_1" => [2, 0], -- "option_2" => [3, 0], -+ 'option' => [ -+ 'value' => [ -+ "option_0" => [1, 0], -+ "option_1" => [2, 0], -+ "option_2" => [3, 0], -+ ], -+ 'delete' => [ -+ "option_0" => "", -+ "option_1" => "", -+ "option_2" => "", -+ ], - ], -- 'delete' => [ -- "option_0" => "", -- "option_1" => "", -- "option_2" => "", -- ] - ], false - ], - 'duplicate options' => [ - [ -- 'value' => [ -- "option_0" => [1, 0], -- "option_1" => [1, 0], -- "option_2" => [3, 0], -+ 'option' => [ -+ 'value' => [ -+ "option_0" => [1, 0], -+ "option_1" => [1, 0], -+ "option_2" => [3, 0], -+ ], -+ 'delete' => [ -+ "option_0" => "", -+ "option_1" => "", -+ "option_2" => "", -+ ], - ], -- 'delete' => [ -- "option_0" => "", -- "option_1" => "", -- "option_2" => "", -- ] - ], true - ], - 'duplicate and deleted' => [ - [ -- 'value' => [ -- "option_0" => [1, 0], -- "option_1" => [1, 0], -- "option_2" => [3, 0], -+ 'option' => [ -+ 'value' => [ -+ "option_0" => [1, 0], -+ "option_1" => [1, 0], -+ "option_2" => [3, 0], -+ ], -+ 'delete' => [ -+ "option_0" => "", -+ "option_1" => "1", -+ "option_2" => "", -+ ], - ], -- 'delete' => [ -- "option_0" => "", -- "option_1" => "1", -- "option_2" => "", -- ] - ], false - ], - 'empty and deleted' => [ - [ -- 'value' => [ -- "option_0" => [1, 0], -- "option_1" => [2, 0], -- "option_2" => ["", ""], -+ 'option' => [ -+ 'value' => [ -+ "option_0" => [1, 0], -+ "option_1" => [2, 0], -+ "option_2" => ["", ""], -+ ], -+ 'delete' => [ -+ "option_0" => "", -+ "option_1" => "", -+ "option_2" => "1", -+ ], - ], -- 'delete' => [ -- "option_0" => "", -- "option_1" => "", -- "option_2" => "1", -- ] - ], false - ], - ]; -@@ -278,6 +328,7 @@ class ValidateTest extends AttributeTest - */ - public function testEmptyOption(array $options, $result) - { -+ $serializedOptions = '{"key":"value"}'; - $this->requestMock->expects($this->any()) - ->method('getParam') - ->willReturnMap([ -@@ -285,10 +336,16 @@ class ValidateTest extends AttributeTest - ['frontend_input', 'select', 'multipleselect'], - ['attribute_code', null, "test_attribute_code"], - ['new_attribute_set_name', null, 'test_attribute_set_name'], -- ['option', null, $options], - ['message_key', Validate::DEFAULT_MESSAGE_KEY, 'message'], -+ ['serialized_options', '[]', $serializedOptions], - ]); - -+ $this->formDataSerializerMock -+ ->expects($this->once()) -+ ->method('unserialize') -+ ->with($serializedOptions) -+ ->willReturn($options); -+ - $this->objectManagerMock->expects($this->once()) - ->method('create') - ->willReturn($this->attributeMock); -@@ -297,6 +354,11 @@ class ValidateTest extends AttributeTest - ->method('loadByCode') - ->willReturnSelf(); - -+ $this->attributeCodeValidatorMock->expects($this->once()) -+ ->method('isValid') -+ ->with('test_attribute_code') -+ ->willReturn(true); -+ - $this->resultJsonFactoryMock->expects($this->once()) - ->method('create') - ->willReturn($this->resultJson); -@@ -320,8 +382,10 @@ class ValidateTest extends AttributeTest - return [ - 'empty admin scope options' => [ - [ -- 'value' => [ -- "option_0" => [''], -+ 'option' => [ -+ 'value' => [ -+ "option_0" => [''], -+ ], - ], - ], - (object) [ -@@ -331,8 +395,10 @@ class ValidateTest extends AttributeTest - ], - 'not empty admin scope options' => [ - [ -- 'value' => [ -- "option_0" => ['asdads'], -+ 'option' => [ -+ 'value' => [ -+ "option_0" => ['asdads'], -+ ], - ], - ], - (object) [ -@@ -341,11 +407,13 @@ class ValidateTest extends AttributeTest - ], - 'empty admin scope options and deleted' => [ - [ -- 'value' => [ -- "option_0" => [''], -- ], -- 'delete' => [ -- 'option_0' => '1', -+ 'option' => [ -+ 'value' => [ -+ "option_0" => [''], -+ ], -+ 'delete' => [ -+ 'option_0' => '1', -+ ], - ], - ], - (object) [ -@@ -354,11 +422,13 @@ class ValidateTest extends AttributeTest - ], - 'empty admin scope options and not deleted' => [ - [ -- 'value' => [ -- "option_0" => [''], -- ], -- 'delete' => [ -- 'option_0' => '0', -+ 'option' => [ -+ 'value' => [ -+ "option_0" => [''], -+ ], -+ 'delete' => [ -+ 'option_0' => '0', -+ ], - ], - ], - (object) [ -@@ -368,4 +438,136 @@ class ValidateTest extends AttributeTest - ], - ]; - } -+ -+ /** -+ * @throws \Magento\Framework\Exception\NotFoundException -+ */ -+ public function testExecuteWithOptionsDataError() -+ { -+ $serializedOptions = '{"key":"value"}'; -+ $message = "The attribute couldn't be validated due to an error. Verify your information and try again. " -+ . "If the error persists, please try again later."; -+ $this->requestMock->expects($this->any()) -+ ->method('getParam') -+ ->willReturnMap([ -+ ['frontend_label', null, 'test_frontend_label'], -+ ['attribute_code', null, 'test_attribute_code'], -+ ['new_attribute_set_name', null, 'test_attribute_set_name'], -+ ['message_key', Validate::DEFAULT_MESSAGE_KEY, 'message'], -+ ['serialized_options', '[]', $serializedOptions], -+ ]); -+ -+ $this->formDataSerializerMock -+ ->expects($this->once()) -+ ->method('unserialize') -+ ->with($serializedOptions) -+ ->willThrowException(new \InvalidArgumentException('Some exception')); -+ -+ $this->objectManagerMock -+ ->method('create') -+ ->willReturnMap([ -+ [\Magento\Catalog\Model\ResourceModel\Eav\Attribute::class, [], $this->attributeMock], -+ [\Magento\Eav\Model\Entity\Attribute\Set::class, [], $this->attributeSetMock] -+ ]); -+ -+ $this->attributeCodeValidatorMock -+ ->method('isValid') -+ ->willReturn(true); -+ -+ $this->attributeMock -+ ->method('loadByCode') -+ ->willReturnSelf(); -+ $this->attributeSetMock -+ ->method('setEntityTypeId') -+ ->willReturnSelf(); -+ $this->resultJsonFactoryMock->expects($this->once()) -+ ->method('create') -+ ->willReturn($this->resultJson); -+ $this->resultJson->expects($this->once()) -+ ->method('setJsonData') -+ ->with(json_encode([ -+ 'error' => true, -+ 'message' => $message -+ ])) -+ ->willReturnSelf(); -+ -+ $this->getModel()->execute(); -+ } -+ -+ /** -+ * Test execute with an invalid attribute code -+ * -+ * @dataProvider provideInvalidAttributeCodes -+ * @param string $attributeCode -+ * @param $result -+ * @throws \Magento\Framework\Exception\NotFoundException -+ */ -+ public function testExecuteWithInvalidAttributeCode($attributeCode, $result) -+ { -+ $serializedOptions = '{"key":"value"}'; -+ $this->requestMock->expects($this->any()) -+ ->method('getParam') -+ ->willReturnMap([ -+ ['frontend_label', null, null], -+ ['frontend_input', 'select', 'multipleselect'], -+ ['attribute_code', null, $attributeCode], -+ ['new_attribute_set_name', null, 'test_attribute_set_name'], -+ ['message_key', Validate::DEFAULT_MESSAGE_KEY, 'message'], -+ ['serialized_options', '[]', $serializedOptions], -+ ]); -+ -+ $this->formDataSerializerMock -+ ->expects($this->once()) -+ ->method('unserialize') -+ ->with($serializedOptions) -+ ->willReturn(["key" => "value"]); -+ -+ $this->objectManagerMock->expects($this->once()) -+ ->method('create') -+ ->willReturn($this->attributeMock); -+ -+ $this->attributeMock->expects($this->once()) -+ ->method('loadByCode') -+ ->willReturnSelf(); -+ -+ $this->attributeCodeValidatorMock->expects($this->once()) -+ ->method('isValid') -+ ->with($attributeCode) -+ ->willReturn(false); -+ -+ $this->attributeCodeValidatorMock->expects($this->once()) -+ ->method('getMessages') -+ ->willReturn(['Invalid Attribute Code.']); -+ -+ $this->resultJsonFactoryMock->expects($this->once()) -+ ->method('create') -+ ->willReturn($this->resultJson); -+ -+ $this->resultJson->expects($this->once()) -+ ->method('setJsonData') -+ ->willReturnArgument(0); -+ -+ $response = $this->getModel()->execute(); -+ $responseObject = json_decode($response); -+ -+ $this->assertEquals($responseObject, $result); -+ } -+ -+ /** -+ * Providing invalid attribute codes -+ * -+ * @return array -+ */ -+ public function provideInvalidAttributeCodes() -+ { -+ return [ -+ 'invalid attribute code' => [ -+ '.attribute_code', -+ (object) [ -+ 'error' => true, -+ 'message' => 'Invalid Attribute Code.', -+ ] -+ ] -+ ]; -+ } - } -diff --git a/app/code/Magento/Catalog/Test/Unit/Controller/Adminhtml/Product/AttributeTest.php b/app/code/Magento/Catalog/Test/Unit/Controller/Adminhtml/Product/AttributeTest.php -index b85b03852b6..2a75773754f 100644 ---- a/app/code/Magento/Catalog/Test/Unit/Controller/Adminhtml/Product/AttributeTest.php -+++ b/app/code/Magento/Catalog/Test/Unit/Controller/Adminhtml/Product/AttributeTest.php -@@ -9,8 +9,10 @@ use Magento\Backend\App\Action\Context; - use Magento\Catalog\Controller\Adminhtml\Product\Attribute; - use Magento\Framework\App\RequestInterface; - use Magento\Framework\Cache\FrontendInterface; -+use Magento\Framework\Message\ManagerInterface; -+use Magento\Framework\ObjectManager\ObjectManager; - use Magento\Framework\Registry; --use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; -+use Magento\Framework\TestFramework\Unit\Helper\ObjectManager as ObjectManagerHelper; - use Magento\Framework\View\Result\PageFactory; - use Magento\Framework\Controller\ResultFactory; - -@@ -20,7 +22,7 @@ use Magento\Framework\Controller\ResultFactory; - class AttributeTest extends \PHPUnit\Framework\TestCase - { - /** -- * @var ObjectManager -+ * @var ObjectManagerHelper - */ - protected $objectManager; - -@@ -54,9 +56,14 @@ class AttributeTest extends \PHPUnit\Framework\TestCase - */ - protected $resultFactoryMock; - -+ /** -+ * @var ManagerInterface|\PHPUnit_Framework_MockObject_MockObject -+ */ -+ protected $messageManager; -+ - protected function setUp() - { -- $this->objectManager = new ObjectManager($this); -+ $this->objectManager = new ObjectManagerHelper($this); - $this->contextMock = $this->getMockBuilder(Context::class) - ->disableOriginalConstructor() - ->getMock(); -@@ -74,6 +81,9 @@ class AttributeTest extends \PHPUnit\Framework\TestCase - $this->resultFactoryMock = $this->getMockBuilder(ResultFactory::class) - ->disableOriginalConstructor() - ->getMock(); -+ $this->messageManager = $this->getMockBuilder(ManagerInterface::class) -+ ->disableOriginalConstructor() -+ ->getMock(); - - $this->contextMock->expects($this->any()) - ->method('getRequest') -@@ -81,6 +91,9 @@ class AttributeTest extends \PHPUnit\Framework\TestCase - $this->contextMock->expects($this->any()) - ->method('getResultFactory') - ->willReturn($this->resultFactoryMock); -+ $this->contextMock -+ ->method('getMessageManager') -+ ->willReturn($this->messageManager); - } - - /** -diff --git a/app/code/Magento/Catalog/Test/Unit/Controller/Adminhtml/Product/BuilderTest.php b/app/code/Magento/Catalog/Test/Unit/Controller/Adminhtml/Product/BuilderTest.php -index 4113ce636d6..c71fa90fb02 100644 ---- a/app/code/Magento/Catalog/Test/Unit/Controller/Adminhtml/Product/BuilderTest.php -+++ b/app/code/Magento/Catalog/Test/Unit/Controller/Adminhtml/Product/BuilderTest.php -@@ -14,6 +14,9 @@ use Magento\Catalog\Model\ProductFactory; - use Magento\Framework\Registry; - use Magento\Cms\Model\Wysiwyg\Config as WysiwygConfig; - use Magento\Framework\App\Request\Http; -+use Magento\Catalog\Api\ProductRepositoryInterface; -+use Magento\Framework\Exception\NoSuchEntityException; -+use Magento\Catalog\Model\Product\Type as ProductTypes; - - /** - * Class BuilderTest -@@ -67,6 +70,11 @@ class BuilderTest extends \PHPUnit\Framework\TestCase - */ - protected $storeFactoryMock; - -+ /** -+ * @var ProductRepositoryInterface|\PHPUnit_Framework_MockObject_MockObject -+ */ -+ protected $productRepositoryMock; -+ - /** - * @var StoreInterface|\PHPUnit_Framework_MockObject_MockObject - */ -@@ -90,9 +98,10 @@ class BuilderTest extends \PHPUnit\Framework\TestCase - ->setMethods(['load']) - ->getMockForAbstractClass(); - -- $this->storeFactoryMock->expects($this->any()) -- ->method('create') -- ->willReturn($this->storeMock); -+ $this->productRepositoryMock = $this->getMockBuilder(ProductRepositoryInterface::class) -+ ->setMethods(['getById']) -+ ->disableOriginalConstructor() -+ ->getMockForAbstractClass(); - - $this->builder = $this->objectManager->getObject( - Builder::class, -@@ -102,140 +111,198 @@ class BuilderTest extends \PHPUnit\Framework\TestCase - 'registry' => $this->registryMock, - 'wysiwygConfig' => $this->wysiwygConfigMock, - 'storeFactory' => $this->storeFactoryMock, -+ 'productRepository' => $this->productRepositoryMock - ] - ); - } - - public function testBuildWhenProductExistAndPossibleToLoadProduct() - { -+ $productId = 2; -+ $productType = 'type_id'; -+ $productStore = 'store'; -+ $productSet = 3; -+ - $valueMap = [ -- ['id', null, 2], -- ['store', 0, 'some_store'], -- ['type', null, 'type_id'], -- ['set', null, 3], -- ['store', null, 'store'], -+ ['id', null, $productId], -+ ['type', null, $productType], -+ ['set', null, $productSet], -+ ['store', 0, $productStore], - ]; -+ - $this->requestMock->expects($this->any()) - ->method('getParam') - ->willReturnMap($valueMap); -- $this->productFactoryMock->expects($this->once()) -+ -+ $this->productRepositoryMock->expects($this->any()) -+ ->method('getById') -+ ->with($productId, true, $productStore) -+ ->willReturn($this->productMock); -+ -+ $this->storeFactoryMock->expects($this->any()) - ->method('create') -- ->will($this->returnValue($this->productMock)); -- $this->productMock->expects($this->once()) -- ->method('setStoreId') -- ->with('some_store') -- ->willReturnSelf(); -- $this->productMock->expects($this->never()) -- ->method('setTypeId'); -- $this->productMock->expects($this->once()) -+ ->willReturn($this->storeMock); -+ -+ $this->storeMock->expects($this->any()) - ->method('load') -- ->with(2) -- ->will($this->returnSelf()); -- $this->productMock->expects($this->once()) -- ->method('setAttributeSetId') -- ->with(3) -- ->will($this->returnSelf()); -+ ->with($productStore) -+ ->willReturnSelf(); -+ - $registryValueMap = [ - ['product', $this->productMock, $this->registryMock], - ['current_product', $this->productMock, $this->registryMock], -+ ['current_store', $this->registryMock, $this->storeMock], - ]; -+ - $this->registryMock->expects($this->any()) - ->method('register') - ->willReturn($registryValueMap); -+ - $this->wysiwygConfigMock->expects($this->once()) - ->method('setStoreId') -- ->with('store'); -+ ->with($productStore); -+ - $this->assertEquals($this->productMock, $this->builder->build($this->requestMock)); - } - - public function testBuildWhenImpossibleLoadProduct() - { -+ $productId = 2; -+ $productType = 'type_id'; -+ $productStore = 'store'; -+ $productSet = 3; -+ - $valueMap = [ -- ['id', null, 15], -- ['store', 0, 'some_store'], -- ['type', null, 'type_id'], -- ['set', null, 3], -- ['store', null, 'store'], -+ ['id', null, $productId], -+ ['type', null, $productType], -+ ['set', null, $productSet], -+ ['store', 0, $productStore], - ]; -+ - $this->requestMock->expects($this->any()) - ->method('getParam') -- ->will($this->returnValueMap($valueMap)); -+ ->willReturnMap($valueMap); -+ -+ $this->productRepositoryMock->expects($this->any()) -+ ->method('getById') -+ ->with($productId, true, $productStore) -+ ->willThrowException(new NoSuchEntityException()); -+ - $this->productFactoryMock->expects($this->once()) - ->method('create') -- ->willReturn($this->productMock); -- $this->productMock->expects($this->once()) -- ->method('setStoreId') -- ->with('some_store') -- ->willReturnSelf(); -- $this->productMock->expects($this->once()) -+ ->will($this->returnValue($this->productMock)); -+ -+ $this->productMock->expects($this->any()) -+ ->method('setData') -+ ->with('_edit_mode', true); -+ -+ $this->productMock->expects($this->any()) - ->method('setTypeId') -- ->with(\Magento\Catalog\Model\Product\Type::DEFAULT_TYPE) -- ->willReturnSelf(); -- $this->productMock->expects($this->once()) -- ->method('load') -- ->with(15) -- ->willThrowException(new \Exception()); -+ ->with(ProductTypes::DEFAULT_TYPE); -+ -+ $this->productMock->expects($this->any()) -+ ->method('setStoreId') -+ ->with($productStore); -+ -+ $this->productMock->expects($this->any()) -+ ->method('setAttributeSetId') -+ ->with($productSet); -+ - $this->loggerMock->expects($this->once()) - ->method('critical'); -- $this->productMock->expects($this->once()) -- ->method('setAttributeSetId') -- ->with(3) -- ->will($this->returnSelf()); -+ -+ $this->storeFactoryMock->expects($this->any()) -+ ->method('create') -+ ->willReturn($this->storeMock); -+ -+ $this->storeMock->expects($this->any()) -+ ->method('load') -+ ->with($productStore) -+ ->willReturnSelf(); -+ - $registryValueMap = [ - ['product', $this->productMock, $this->registryMock], - ['current_product', $this->productMock, $this->registryMock], -+ ['current_store', $this->registryMock, $this->storeMock], - ]; -+ - $this->registryMock->expects($this->any()) - ->method('register') -- ->will($this->returnValueMap($registryValueMap)); -+ ->willReturn($registryValueMap); -+ - $this->wysiwygConfigMock->expects($this->once()) - ->method('setStoreId') -- ->with('store'); -+ ->with($productStore); -+ - $this->assertEquals($this->productMock, $this->builder->build($this->requestMock)); - } - - public function testBuildWhenProductNotExist() - { -+ $productId = 0; -+ $productType = 'type_id'; -+ $productStore = 'store'; -+ $productSet = 3; -+ - $valueMap = [ -- ['id', null, null], -- ['store', 0, 'some_store'], -- ['type', null, 'type_id'], -- ['set', null, 3], -- ['store', null, 'store'], -+ ['id', null, $productId], -+ ['type', null, $productType], -+ ['set', null, $productSet], -+ ['store', 0, $productStore], - ]; -+ - $this->requestMock->expects($this->any()) - ->method('getParam') -- ->will($this->returnValueMap($valueMap)); -+ ->willReturnMap($valueMap); -+ -+ $this->productRepositoryMock->expects($this->any()) -+ ->method('getById') -+ ->with($productId, true, $productStore) -+ ->willThrowException(new NoSuchEntityException()); -+ - $this->productFactoryMock->expects($this->once()) - ->method('create') -- ->willReturn($this->productMock); -- $this->productMock->expects($this->once()) -- ->method('setStoreId') -- ->with('some_store') -- ->willReturnSelf(); -- $productValueMap = [ -- ['type_id', $this->productMock], -- [\Magento\Catalog\Model\Product\Type::DEFAULT_TYPE, $this->productMock], -- ]; -+ ->will($this->returnValue($this->productMock)); -+ -+ $this->productMock->expects($this->any()) -+ ->method('setData') -+ ->with('_edit_mode', true); -+ - $this->productMock->expects($this->any()) - ->method('setTypeId') -- ->willReturnMap($productValueMap); -- $this->productMock->expects($this->never()) -- ->method('load'); -- $this->productMock->expects($this->once()) -+ ->with($productType); -+ -+ $this->productMock->expects($this->any()) -+ ->method('setStoreId') -+ ->with($productStore); -+ -+ $this->productMock->expects($this->any()) - ->method('setAttributeSetId') -- ->with(3) -- ->will($this->returnSelf()); -+ ->with($productSet); -+ -+ $this->storeFactoryMock->expects($this->any()) -+ ->method('create') -+ ->willReturn($this->storeMock); -+ -+ $this->storeMock->expects($this->any()) -+ ->method('load') -+ ->with($productStore) -+ ->willReturnSelf(); -+ - $registryValueMap = [ - ['product', $this->productMock, $this->registryMock], - ['current_product', $this->productMock, $this->registryMock], -+ ['current_store', $this->registryMock, $this->storeMock], - ]; -+ - $this->registryMock->expects($this->any()) - ->method('register') -- ->will($this->returnValueMap($registryValueMap)); -+ ->willReturn($registryValueMap); -+ - $this->wysiwygConfigMock->expects($this->once()) - ->method('setStoreId') -- ->with('store'); -+ ->with($productStore); -+ - $this->assertEquals($this->productMock, $this->builder->build($this->requestMock)); - } - } -diff --git a/app/code/Magento/Catalog/Test/Unit/Controller/Adminhtml/Product/Initialization/HelperTest.php b/app/code/Magento/Catalog/Test/Unit/Controller/Adminhtml/Product/Initialization/HelperTest.php -index ff44a91a649..c889c58e3df 100644 ---- a/app/code/Magento/Catalog/Test/Unit/Controller/Adminhtml/Product/Initialization/HelperTest.php -+++ b/app/code/Magento/Catalog/Test/Unit/Controller/Adminhtml/Product/Initialization/HelperTest.php -@@ -95,6 +95,11 @@ class HelperTest extends \PHPUnit\Framework\TestCase - */ - protected $attributeFilterMock; - -+ /** -+ * @var \PHPUnit_Framework_MockObject_MockObject -+ */ -+ private $dateTimeFilterMock; -+ - /** - * @inheritdoc - */ -@@ -170,6 +175,11 @@ class HelperTest extends \PHPUnit\Framework\TestCase - $resolverProperty = $helperReflection->getProperty('linkResolver'); - $resolverProperty->setAccessible(true); - $resolverProperty->setValue($this->helper, $this->linkResolverMock); -+ -+ $this->dateTimeFilterMock = $this->createMock(\Magento\Framework\Stdlib\DateTime\Filter\DateTime::class); -+ $dateTimeFilterProperty = $helperReflection->getProperty('dateTimeFilter'); -+ $dateTimeFilterProperty->setAccessible(true); -+ $dateTimeFilterProperty->setValue($this->helper, $this->dateTimeFilterMock); - } - - /** -@@ -211,6 +221,12 @@ class HelperTest extends \PHPUnit\Framework\TestCase - if (!empty($tierPrice)) { - $productData = array_merge($productData, ['tier_price' => $tierPrice]); - } -+ -+ $this->dateTimeFilterMock->expects($this->once()) -+ ->method('filter') -+ ->with($specialFromDate) -+ ->willReturn($specialFromDate); -+ - $attributeNonDate = $this->getMockBuilder(\Magento\Catalog\Model\ResourceModel\Eav\Attribute::class) - ->disableOriginalConstructor() - ->getMock(); -diff --git a/app/code/Magento/Catalog/Test/Unit/Controller/Category/ViewTest.php b/app/code/Magento/Catalog/Test/Unit/Controller/Category/ViewTest.php -index d93520297e4..60c6f2f1bd8 100644 ---- a/app/code/Magento/Catalog/Test/Unit/Controller/Category/ViewTest.php -+++ b/app/code/Magento/Catalog/Test/Unit/Controller/Category/ViewTest.php -@@ -124,7 +124,7 @@ class ViewTest extends \PHPUnit\Framework\TestCase - ->disableOriginalConstructor()->getMock(); - $this->pageConfig->expects($this->any())->method('addBodyClass')->will($this->returnSelf()); - -- $this->page = $this->getMockBuilder(\Magento\Framework\View\Page::class) -+ $this->page = $this->getMockBuilder(\Magento\Framework\View\Result\Page::class) - ->setMethods(['getConfig', 'initLayout', 'addPageLayoutHandles', 'getLayout', 'addUpdate']) - ->disableOriginalConstructor()->getMock(); - $this->page->expects($this->any())->method('getConfig')->will($this->returnValue($this->pageConfig)); -diff --git a/app/code/Magento/Catalog/Test/Unit/Model/Attribute/Backend/TierPrice/SaveHandlerTest.php b/app/code/Magento/Catalog/Test/Unit/Model/Attribute/Backend/TierPrice/SaveHandlerTest.php -new file mode 100644 -index 00000000000..fbb96933db5 ---- /dev/null -+++ b/app/code/Magento/Catalog/Test/Unit/Model/Attribute/Backend/TierPrice/SaveHandlerTest.php -@@ -0,0 +1,178 @@ -+<?php -+/** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+declare(strict_types=1); -+ -+namespace Magento\Catalog\Test\Unit\Model\Attribute\Backend\TierPrice; -+ -+use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; -+use Magento\Catalog\Model\Product\Attribute\Backend\TierPrice\SaveHandler; -+use Magento\Store\Model\StoreManagerInterface; -+use Magento\Catalog\Api\ProductAttributeRepositoryInterface; -+use Magento\Customer\Api\GroupManagementInterface; -+use Magento\Framework\EntityManager\MetadataPool; -+use Magento\Catalog\Model\ResourceModel\Product\Attribute\Backend\Tierprice; -+ -+/** -+ * Unit tests for \Magento\Catalog\Model\Product\Attribute\Backend\TierPrice\SaveHandler -+ * @SuppressWarnings(PHPMD.CouplingBetweenObjects) -+ */ -+class SaveHandlerTest extends \PHPUnit\Framework\TestCase -+{ -+ /** -+ * Magento\Framework\TestFramework\Unit\Helper\ObjectManager -+ */ -+ private $objectManager; -+ -+ /** -+ * @var SaveHandler|\PHPUnit_Framework_MockObject_MockObject -+ */ -+ private $saveHandler; -+ -+ /** -+ * @var StoreManagerInterface|\PHPUnit_Framework_MockObject_MockObject -+ */ -+ private $storeManager; -+ -+ /** -+ * @var ProductAttributeRepositoryInterface|\PHPUnit_Framework_MockObject_MockObject -+ */ -+ private $attributeRepository; -+ -+ /** -+ * @var GroupManagementInterface|\PHPUnit_Framework_MockObject_MockObject -+ */ -+ private $groupManagement; -+ -+ /** -+ * @var MetadataPool|\PHPUnit_Framework_MockObject_MockObject -+ */ -+ private $metadataPoll; -+ -+ /** -+ * @var Tierprice|\PHPUnit_Framework_MockObject_MockObject -+ */ -+ private $tierPriceResource; -+ -+ /** -+ * @inheritdoc -+ */ -+ protected function setUp() -+ { -+ $this->objectManager = new ObjectManager($this); -+ $this->storeManager = $this->getMockBuilder(StoreManagerInterface::class) -+ ->disableOriginalConstructor() -+ ->setMethods(['getStore']) -+ ->getMockForAbstractClass(); -+ $this->attributeRepository = $this->getMockBuilder(ProductAttributeRepositoryInterface::class) -+ ->disableOriginalConstructor() -+ ->setMethods(['get']) -+ ->getMockForAbstractClass(); -+ $this->groupManagement = $this->getMockBuilder(GroupManagementInterface::class) -+ ->disableOriginalConstructor() -+ ->setMethods(['getAllCustomersGroup']) -+ ->getMockForAbstractClass(); -+ $this->metadataPoll = $this->getMockBuilder(MetadataPool::class) -+ ->disableOriginalConstructor() -+ ->setMethods(['getMetadata']) -+ ->getMock(); -+ $this->tierPriceResource = $this->getMockBuilder(Tierprice::class) -+ ->disableOriginalConstructor() -+ ->setMethods([]) -+ ->getMock(); -+ -+ $this->saveHandler = $this->objectManager->getObject( -+ \Magento\Catalog\Model\Product\Attribute\Backend\TierPrice\SaveHandler::class, -+ [ -+ 'storeManager' => $this->storeManager, -+ 'attributeRepository' => $this->attributeRepository, -+ 'groupManagement' => $this->groupManagement, -+ 'metadataPoll' => $this->metadataPoll, -+ 'tierPriceResource' => $this->tierPriceResource -+ ] -+ ); -+ } -+ -+ public function testExecute(): void -+ { -+ $tierPrices = [ -+ ['website_id' => 0, 'price_qty' => 2, 'cust_group' => 0, 'price' => 10], -+ ['website_id' => 0, 'price_qty' => 3, 'cust_group' => 3200, 'price' => null, 'percentage_value' => 20] -+ ]; -+ $linkField = 'entity_id'; -+ $productId = 10; -+ -+ /** @var \PHPUnit_Framework_MockObject_MockObject $product */ -+ $product = $this->getMockBuilder(\Magento\Catalog\Api\Data\ProductInterface::class) -+ ->disableOriginalConstructor() -+ ->setMethods(['getData','setData', 'getStoreId']) -+ ->getMockForAbstractClass(); -+ $product->expects($this->atLeastOnce())->method('getData')->willReturnMap( -+ [ -+ ['tier_price', $tierPrices], -+ ['entity_id', $productId] -+ ] -+ ); -+ $product->expects($this->atLeastOnce())->method('getStoreId')->willReturn(0); -+ $product->expects($this->atLeastOnce())->method('setData')->with('tier_price_changed', 1); -+ $store = $this->getMockBuilder(\Magento\Store\Api\Data\StoreInterface::class) -+ ->disableOriginalConstructor() -+ ->setMethods(['getWebsiteId']) -+ ->getMockForAbstractClass(); -+ $store->expects($this->atLeastOnce())->method('getWebsiteId')->willReturn(0); -+ $this->storeManager->expects($this->atLeastOnce())->method('getStore')->willReturn($store); -+ /** @var \PHPUnit_Framework_MockObject_MockObject $attribute */ -+ $attribute = $this->getMockBuilder(\Magento\Catalog\Api\Data\ProductAttributeInterface::class) -+ ->disableOriginalConstructor() -+ ->setMethods(['getName', 'isScopeGlobal']) -+ ->getMockForAbstractClass(); -+ $attribute->expects($this->atLeastOnce())->method('getName')->willReturn('tier_price'); -+ $attribute->expects($this->atLeastOnce())->method('isScopeGlobal')->willReturn(true); -+ $this->attributeRepository->expects($this->atLeastOnce())->method('get')->with('tier_price') -+ ->willReturn($attribute); -+ $productMetadata = $this->getMockBuilder(\Magento\Framework\EntityManager\EntityMetadataInterface::class) -+ ->disableOriginalConstructor() -+ ->setMethods(['getLinkField']) -+ ->getMockForAbstractClass(); -+ $productMetadata->expects($this->atLeastOnce())->method('getLinkField')->willReturn($linkField); -+ $this->metadataPoll->expects($this->atLeastOnce())->method('getMetadata') -+ ->with(\Magento\Catalog\Api\Data\ProductInterface::class) -+ ->willReturn($productMetadata); -+ $customerGroup = $this->getMockBuilder(\Magento\Customer\Api\Data\GroupInterface::class) -+ ->disableOriginalConstructor() -+ ->setMethods(['getId']) -+ ->getMockForAbstractClass(); -+ $customerGroup->expects($this->atLeastOnce())->method('getId')->willReturn(3200); -+ $this->groupManagement->expects($this->atLeastOnce())->method('getAllCustomersGroup') -+ ->willReturn($customerGroup); -+ $this->tierPriceResource->expects($this->atLeastOnce())->method('savePriceData')->willReturnSelf(); -+ -+ $this->assertEquals($product, $this->saveHandler->execute($product)); -+ } -+ -+ /** -+ * @expectedException \Magento\Framework\Exception\InputException -+ * @expectedExceptionMessage Tier prices data should be array, but actually other type is received -+ */ -+ public function testExecuteWithException(): void -+ { -+ /** @var \PHPUnit_Framework_MockObject_MockObject $attribute */ -+ $attribute = $this->getMockBuilder(\Magento\Catalog\Api\Data\ProductAttributeInterface::class) -+ ->disableOriginalConstructor() -+ ->setMethods(['getName', 'isScopeGlobal']) -+ ->getMockForAbstractClass(); -+ $attribute->expects($this->atLeastOnce())->method('getName')->willReturn('tier_price'); -+ $this->attributeRepository->expects($this->atLeastOnce())->method('get')->with('tier_price') -+ ->willReturn($attribute); -+ /** @var \PHPUnit_Framework_MockObject_MockObject $product */ -+ $product = $this->getMockBuilder(\Magento\Catalog\Api\Data\ProductInterface::class) -+ ->disableOriginalConstructor() -+ ->setMethods(['getData','setData', 'getStoreId', 'getOrigData']) -+ ->getMockForAbstractClass(); -+ $product->expects($this->atLeastOnce())->method('getData')->with('tier_price')->willReturn(1); -+ -+ $this->saveHandler->execute($product); -+ } -+} -diff --git a/app/code/Magento/Catalog/Test/Unit/Model/Attribute/Backend/TierPrice/UpdateHandlerTest.php b/app/code/Magento/Catalog/Test/Unit/Model/Attribute/Backend/TierPrice/UpdateHandlerTest.php -new file mode 100644 -index 00000000000..cce00c50d37 ---- /dev/null -+++ b/app/code/Magento/Catalog/Test/Unit/Model/Attribute/Backend/TierPrice/UpdateHandlerTest.php -@@ -0,0 +1,192 @@ -+<?php -+/** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+declare(strict_types=1); -+ -+namespace Magento\Catalog\Test\Unit\Model\Attribute\Backend\TierPrice; -+ -+use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; -+use Magento\Catalog\Model\Product\Attribute\Backend\TierPrice\UpdateHandler; -+use Magento\Store\Model\StoreManagerInterface; -+use Magento\Catalog\Api\ProductAttributeRepositoryInterface; -+use Magento\Customer\Api\GroupManagementInterface; -+use Magento\Framework\EntityManager\MetadataPool; -+use Magento\Catalog\Model\ResourceModel\Product\Attribute\Backend\Tierprice; -+ -+/** -+ * Unit tests for \Magento\Catalog\Model\Product\Attribute\Backend\TierPrice\UpdateHandler -+ * @SuppressWarnings(PHPMD.CouplingBetweenObjects) -+ */ -+class UpdateHandlerTest extends \PHPUnit\Framework\TestCase -+{ -+ /** -+ * Magento\Framework\TestFramework\Unit\Helper\ObjectManager -+ */ -+ private $objectManager; -+ -+ /** -+ * @var UpdateHandler|\PHPUnit_Framework_MockObject_MockObject -+ */ -+ private $updateHandler; -+ -+ /** -+ * @var StoreManagerInterface|\PHPUnit_Framework_MockObject_MockObject -+ */ -+ private $storeManager; -+ -+ /** -+ * @var ProductAttributeRepositoryInterface|\PHPUnit_Framework_MockObject_MockObject -+ */ -+ private $attributeRepository; -+ -+ /** -+ * @var GroupManagementInterface|\PHPUnit_Framework_MockObject_MockObject -+ */ -+ private $groupManagement; -+ -+ /** -+ * @var MetadataPool|\PHPUnit_Framework_MockObject_MockObject -+ */ -+ private $metadataPoll; -+ -+ /** -+ * @var Tierprice|\PHPUnit_Framework_MockObject_MockObject -+ */ -+ private $tierPriceResource; -+ -+ /** -+ * @inheritdoc -+ */ -+ protected function setUp() -+ { -+ $this->objectManager = new ObjectManager($this); -+ $this->storeManager = $this->getMockBuilder(StoreManagerInterface::class) -+ ->disableOriginalConstructor() -+ ->setMethods(['getStore']) -+ ->getMockForAbstractClass(); -+ $this->attributeRepository = $this->getMockBuilder(ProductAttributeRepositoryInterface::class) -+ ->disableOriginalConstructor() -+ ->setMethods(['get']) -+ ->getMockForAbstractClass(); -+ $this->groupManagement = $this->getMockBuilder(GroupManagementInterface::class) -+ ->disableOriginalConstructor() -+ ->setMethods(['getAllCustomersGroup']) -+ ->getMockForAbstractClass(); -+ $this->metadataPoll = $this->getMockBuilder(MetadataPool::class) -+ ->disableOriginalConstructor() -+ ->setMethods(['getMetadata']) -+ ->getMock(); -+ $this->tierPriceResource = $this->getMockBuilder(Tierprice::class) -+ ->disableOriginalConstructor() -+ ->setMethods([]) -+ ->getMock(); -+ -+ $this->updateHandler = $this->objectManager->getObject( -+ \Magento\Catalog\Model\Product\Attribute\Backend\TierPrice\UpdateHandler::class, -+ [ -+ 'storeManager' => $this->storeManager, -+ 'attributeRepository' => $this->attributeRepository, -+ 'groupManagement' => $this->groupManagement, -+ 'metadataPoll' => $this->metadataPoll, -+ 'tierPriceResource' => $this->tierPriceResource -+ ] -+ ); -+ } -+ -+ public function testExecute(): void -+ { -+ $newTierPrices = [ -+ ['website_id' => 0, 'price_qty' => 2, 'cust_group' => 0, 'price' => 15], -+ ['website_id' => 0, 'price_qty' => 3, 'cust_group' => 3200, 'price' => null, 'percentage_value' => 20] -+ ]; -+ $priceIdToDelete = 2; -+ $originalTierPrices = [ -+ ['price_id' => 1, 'website_id' => 0, 'price_qty' => 2, 'cust_group' => 0, 'price' => 10], -+ ['price_id' => $priceIdToDelete, 'website_id' => 0, 'price_qty' => 4, 'cust_group' => 0, 'price' => 20], -+ ]; -+ $linkField = 'entity_id'; -+ $productId = 10; -+ -+ /** @var \PHPUnit_Framework_MockObject_MockObject $product */ -+ $product = $this->getMockBuilder(\Magento\Catalog\Api\Data\ProductInterface::class) -+ ->disableOriginalConstructor() -+ ->setMethods(['getData','setData', 'getStoreId', 'getOrigData']) -+ ->getMockForAbstractClass(); -+ $product->expects($this->atLeastOnce())->method('getData')->willReturnMap( -+ [ -+ ['tier_price', $newTierPrices], -+ ['entity_id', $productId] -+ ] -+ ); -+ $product->expects($this->atLeastOnce())->method('getOrigData') -+ ->willReturnMap( -+ [ -+ ['tier_price', $originalTierPrices], -+ ['entity_id', $productId] -+ ] -+ ); -+ $product->expects($this->atLeastOnce())->method('getStoreId')->willReturn(0); -+ $product->expects($this->atLeastOnce())->method('setData')->with('tier_price_changed', 1); -+ $store = $this->getMockBuilder(\Magento\Store\Api\Data\StoreInterface::class) -+ ->disableOriginalConstructor() -+ ->setMethods(['getWebsiteId']) -+ ->getMockForAbstractClass(); -+ $store->expects($this->atLeastOnce())->method('getWebsiteId')->willReturn(0); -+ $this->storeManager->expects($this->atLeastOnce())->method('getStore')->willReturn($store); -+ /** @var \PHPUnit_Framework_MockObject_MockObject $attribute */ -+ $attribute = $this->getMockBuilder(\Magento\Catalog\Api\Data\ProductAttributeInterface::class) -+ ->disableOriginalConstructor() -+ ->setMethods(['getName', 'isScopeGlobal']) -+ ->getMockForAbstractClass(); -+ $attribute->expects($this->atLeastOnce())->method('getName')->willReturn('tier_price'); -+ $attribute->expects($this->atLeastOnce())->method('isScopeGlobal')->willReturn(true); -+ $this->attributeRepository->expects($this->atLeastOnce())->method('get')->with('tier_price') -+ ->willReturn($attribute); -+ $productMetadata = $this->getMockBuilder(\Magento\Framework\EntityManager\EntityMetadataInterface::class) -+ ->disableOriginalConstructor() -+ ->setMethods(['getLinkField']) -+ ->getMockForAbstractClass(); -+ $productMetadata->expects($this->atLeastOnce())->method('getLinkField')->willReturn($linkField); -+ $this->metadataPoll->expects($this->atLeastOnce())->method('getMetadata') -+ ->with(\Magento\Catalog\Api\Data\ProductInterface::class) -+ ->willReturn($productMetadata); -+ $customerGroup = $this->getMockBuilder(\Magento\Customer\Api\Data\GroupInterface::class) -+ ->disableOriginalConstructor() -+ ->setMethods(['getId']) -+ ->getMockForAbstractClass(); -+ $customerGroup->expects($this->atLeastOnce())->method('getId')->willReturn(3200); -+ $this->groupManagement->expects($this->atLeastOnce())->method('getAllCustomersGroup') -+ ->willReturn($customerGroup); -+ $this->tierPriceResource->expects($this->exactly(2))->method('savePriceData')->willReturnSelf(); -+ $this->tierPriceResource->expects($this->once())->method('deletePriceData') -+ ->with($productId, null, $priceIdToDelete); -+ -+ $this->assertEquals($product, $this->updateHandler->execute($product)); -+ } -+ -+ /** -+ * @expectedException \Magento\Framework\Exception\InputException -+ * @expectedExceptionMessage Tier prices data should be array, but actually other type is received -+ */ -+ public function testExecuteWithException(): void -+ { -+ /** @var \PHPUnit_Framework_MockObject_MockObject $attribute */ -+ $attribute = $this->getMockBuilder(\Magento\Catalog\Api\Data\ProductAttributeInterface::class) -+ ->disableOriginalConstructor() -+ ->setMethods(['getName', 'isScopeGlobal']) -+ ->getMockForAbstractClass(); -+ $attribute->expects($this->atLeastOnce())->method('getName')->willReturn('tier_price'); -+ $this->attributeRepository->expects($this->atLeastOnce())->method('get')->with('tier_price') -+ ->willReturn($attribute); -+ /** @var \PHPUnit_Framework_MockObject_MockObject $product */ -+ $product = $this->getMockBuilder(\Magento\Catalog\Api\Data\ProductInterface::class) -+ ->disableOriginalConstructor() -+ ->setMethods(['getData','setData', 'getStoreId', 'getOrigData']) -+ ->getMockForAbstractClass(); -+ $product->expects($this->atLeastOnce())->method('getData')->with('tier_price')->willReturn(1); -+ -+ $this->updateHandler->execute($product); -+ } -+} -diff --git a/app/code/Magento/Catalog/Test/Unit/Model/Category/FileInfoTest.php b/app/code/Magento/Catalog/Test/Unit/Model/Category/FileInfoTest.php -index 8ca823127e6..6c6a69ec39c 100644 ---- a/app/code/Magento/Catalog/Test/Unit/Model/Category/FileInfoTest.php -+++ b/app/code/Magento/Catalog/Test/Unit/Model/Category/FileInfoTest.php -@@ -9,31 +9,41 @@ use Magento\Catalog\Model\Category\FileInfo; - use Magento\Framework\App\Filesystem\DirectoryList; - use Magento\Framework\File\Mime; - use Magento\Framework\Filesystem; --use Magento\Framework\Filesystem\Directory\WriteInterface; - use Magento\Framework\Filesystem\Directory\ReadInterface; -+use Magento\Framework\Filesystem\Directory\WriteInterface; -+use PHPUnit\Framework\MockObject\MockObject; -+use PHPUnit\Framework\TestCase; - --class FileInfoTest extends \PHPUnit\Framework\TestCase -+/** -+ * Test for Magento\Catalog\Model\Category\FileInfo class. -+ */ -+class FileInfoTest extends TestCase - { - /** -- * @var Filesystem|\PHPUnit_Framework_MockObject_MockObject -+ * @var Filesystem|MockObject - */ - private $filesystem; - - /** -- * @var Mime|\PHPUnit_Framework_MockObject_MockObject -+ * @var Mime|MockObject - */ - private $mime; - - /** -- * @var WriteInterface|\PHPUnit_Framework_MockObject_MockObject -+ * @var WriteInterface|MockObject - */ - private $mediaDirectory; - - /** -- * @var ReadInterface|\PHPUnit_Framework_MockObject_MockObject -+ * @var ReadInterface|MockObject - */ - private $baseDirectory; - -+ /** -+ * @var ReadInterface|MockObject -+ */ -+ private $pubDirectory; -+ - /** - * @var FileInfo - */ -@@ -44,30 +54,43 @@ class FileInfoTest extends \PHPUnit\Framework\TestCase - $this->mediaDirectory = $this->getMockBuilder(WriteInterface::class) - ->getMockForAbstractClass(); - -- $this->baseDirectory = $this->getMockBuilder(ReadInterface::class) -+ $this->baseDirectory = $baseDirectory = $this->getMockBuilder(ReadInterface::class) -+ ->getMockForAbstractClass(); -+ -+ $this->pubDirectory = $pubDirectory = $this->getMockBuilder(ReadInterface::class) - ->getMockForAbstractClass(); - - $this->filesystem = $this->getMockBuilder(Filesystem::class) - ->disableOriginalConstructor() - ->getMock(); -- $this->filesystem->expects($this->any()) -- ->method('getDirectoryWrite') -+ -+ $this->filesystem->method('getDirectoryWrite') - ->with(DirectoryList::MEDIA) - ->willReturn($this->mediaDirectory); - -- $this->filesystem->expects($this->any()) -- ->method('getDirectoryRead') -- ->with(DirectoryList::ROOT) -- ->willReturn($this->baseDirectory); -+ $this->filesystem->method('getDirectoryRead') -+ ->willReturnCallback( -+ function ($arg) use ($baseDirectory, $pubDirectory) { -+ if ($arg === DirectoryList::PUB) { -+ return $pubDirectory; -+ } -+ return $baseDirectory; -+ } -+ ); - - $this->mime = $this->getMockBuilder(Mime::class) - ->disableOriginalConstructor() - ->getMock(); - -- $this->baseDirectory->expects($this->any()) -- ->method('getAbsolutePath') -- ->with(null) -- ->willReturn('/a/b/c'); -+ $this->baseDirectory->method('getAbsolutePath') -+ ->willReturn('/a/b/c/'); -+ -+ $this->baseDirectory->method('getRelativePath') -+ ->with('/a/b/c/pub/') -+ ->willReturn('pub/'); -+ -+ $this->pubDirectory->method('getAbsolutePath') -+ ->willReturn('/a/b/c/pub/'); - - $this->model = new FileInfo( - $this->filesystem, -@@ -85,12 +108,12 @@ class FileInfoTest extends \PHPUnit\Framework\TestCase - $this->mediaDirectory->expects($this->at(0)) - ->method('getAbsolutePath') - ->with(null) -- ->willReturn('/a/b/c/pub/media'); -+ ->willReturn('/a/b/c/pub/media/'); - - $this->mediaDirectory->expects($this->at(1)) - ->method('getAbsolutePath') - ->with(null) -- ->willReturn('/a/b/c/pub/media'); -+ ->willReturn('/a/b/c/pub/media/'); - - $this->mediaDirectory->expects($this->at(2)) - ->method('getAbsolutePath') -@@ -113,13 +136,11 @@ class FileInfoTest extends \PHPUnit\Framework\TestCase - - $expected = ['size' => 1]; - -- $this->mediaDirectory->expects($this->any()) -- ->method('getAbsolutePath') -+ $this->mediaDirectory->method('getAbsolutePath') - ->with(null) -- ->willReturn('/a/b/c/pub/media'); -+ ->willReturn('/a/b/c/pub/media/'); - -- $this->mediaDirectory->expects($this->once()) -- ->method('stat') -+ $this->mediaDirectory->method('stat') - ->with($mediaPath . $fileName) - ->willReturn($expected); - -@@ -130,22 +151,52 @@ class FileInfoTest extends \PHPUnit\Framework\TestCase - $this->assertEquals(1, $result['size']); - } - -- public function testIsExist() -+ /** -+ * @param $fileName -+ * @param $fileMediaPath -+ * @dataProvider isExistProvider -+ */ -+ public function testIsExist($fileName, $fileMediaPath) - { -- $mediaPath = '/catalog/category'; -+ $this->mediaDirectory->method('getAbsolutePath') -+ ->willReturn('/a/b/c/pub/media/'); - -- $fileName = '/filename.ext1'; -- -- $this->mediaDirectory->expects($this->any()) -- ->method('getAbsolutePath') -- ->with(null) -- ->willReturn('/a/b/c/pub/media'); -- -- $this->mediaDirectory->expects($this->once()) -- ->method('isExist') -- ->with($mediaPath . $fileName) -+ $this->mediaDirectory->method('isExist') -+ ->with($fileMediaPath) - ->willReturn(true); - - $this->assertTrue($this->model->isExist($fileName)); - } -+ -+ public function isExistProvider() -+ { -+ return [ -+ ['/filename.ext1', '/catalog/category/filename.ext1'], -+ ['/pub/media/filename.ext1', 'filename.ext1'], -+ ['/media/filename.ext1', 'filename.ext1'] -+ ]; -+ } -+ -+ /** -+ * @param $fileName -+ * @param $expected -+ * @dataProvider isBeginsWithMediaDirectoryPathProvider -+ */ -+ public function testIsBeginsWithMediaDirectoryPath($fileName, $expected) -+ { -+ $this->mediaDirectory->method('getAbsolutePath') -+ ->willReturn('/a/b/c/pub/media/'); -+ -+ $this->assertEquals($expected, $this->model->isBeginsWithMediaDirectoryPath($fileName)); -+ } -+ -+ public function isBeginsWithMediaDirectoryPathProvider() -+ { -+ return [ -+ ['/pub/media/test/filename.ext1', true], -+ ['/media/test/filename.ext1', true], -+ ['/test/filename.ext1', false], -+ ['test2/filename.ext1', false] -+ ]; -+ } - } -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 1ff3a1bae5c..7ad8b1a0ab3 100644 ---- a/app/code/Magento/Catalog/Test/Unit/Model/Category/Product/PositionResolverTest.php -+++ b/app/code/Magento/Catalog/Test/Unit/Model/Category/Product/PositionResolverTest.php -@@ -107,7 +107,7 @@ class PositionResolverTest extends \PHPUnit\Framework\TestCase - $this->select->expects($this->once()) - ->method('where') - ->willReturnSelf(); -- $this->select->expects($this->once()) -+ $this->select->expects($this->exactly(2)) - ->method('order') - ->willReturnSelf(); - $this->select->expects($this->once()) -diff --git a/app/code/Magento/Catalog/Test/Unit/Model/Category/TreeTest.php b/app/code/Magento/Catalog/Test/Unit/Model/Category/TreeTest.php -index 9fb2adb2b8e..97c098ba0ff 100644 ---- a/app/code/Magento/Catalog/Test/Unit/Model/Category/TreeTest.php -+++ b/app/code/Magento/Catalog/Test/Unit/Model/Category/TreeTest.php -@@ -43,6 +43,11 @@ class TreeTest extends \PHPUnit\Framework\TestCase - */ - protected $node; - -+ /** -+ * @var \Magento\Catalog\Model\ResourceModel\Category\TreeFactory -+ */ -+ private $treeResourceFactoryMock; -+ - protected function setUp() - { - $this->objectManager = new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this); -@@ -59,6 +64,12 @@ class TreeTest extends \PHPUnit\Framework\TestCase - \Magento\Store\Model\StoreManagerInterface::class - )->disableOriginalConstructor()->getMock(); - -+ $this->treeResourceFactoryMock = $this->createMock( -+ \Magento\Catalog\Model\ResourceModel\Category\TreeFactory::class -+ ); -+ $this->treeResourceFactoryMock->method('create') -+ ->willReturn($this->categoryTreeMock); -+ - $methods = ['create']; - $this->treeFactoryMock = - $this->createPartialMock(\Magento\Catalog\Api\Data\CategoryTreeInterfaceFactory::class, $methods); -@@ -70,7 +81,8 @@ class TreeTest extends \PHPUnit\Framework\TestCase - 'categoryCollection' => $this->categoryCollection, - 'categoryTree' => $this->categoryTreeMock, - 'storeManager' => $this->storeManagerMock, -- 'treeFactory' => $this->treeFactoryMock -+ 'treeFactory' => $this->treeFactoryMock, -+ 'treeResourceFactory' => $this->treeResourceFactoryMock, - ] - ); - } -diff --git a/app/code/Magento/Catalog/Test/Unit/Model/CategoryTest.php b/app/code/Magento/Catalog/Test/Unit/Model/CategoryTest.php -index df01d1bafac..b4042d6b02c 100644 ---- a/app/code/Magento/Catalog/Test/Unit/Model/CategoryTest.php -+++ b/app/code/Magento/Catalog/Test/Unit/Model/CategoryTest.php -@@ -7,7 +7,6 @@ - namespace Magento\Catalog\Test\Unit\Model; - - use Magento\Catalog\Model\Indexer; --use Magento\Eav\Model\Entity\GetCustomAttributeCodesInterface; - - /** - * @SuppressWarnings(PHPMD.TooManyFields) -@@ -120,11 +119,6 @@ class CategoryTest extends \PHPUnit\Framework\TestCase - */ - private $objectManager; - -- /** -- * @var GetCustomAttributeCodesInterface|\PHPUnit_Framework_MockObject_MockObject -- */ -- private $getCustomAttributeCodes; -- - protected function setUp() - { - $this->objectManager = new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this); -@@ -165,10 +159,6 @@ class CategoryTest extends \PHPUnit\Framework\TestCase - ); - $this->attributeValueFactory = $this->getMockBuilder(\Magento\Framework\Api\AttributeValueFactory::class) - ->disableOriginalConstructor()->getMock(); -- $this->getCustomAttributeCodes = $this->getMockBuilder(GetCustomAttributeCodesInterface::class) -- ->setMethods(['execute']) -- ->disableOriginalConstructor() -- ->getMockForAbstractClass(); - - $this->category = $this->getCategoryModel(); - } -@@ -321,7 +311,6 @@ class CategoryTest extends \PHPUnit\Framework\TestCase - 'indexerRegistry' => $this->indexerRegistry, - 'metadataService' => $this->metadataServiceMock, - 'customAttributeFactory' => $this->attributeValueFactory, -- 'getCustomAttributeCodes' => $this->getCustomAttributeCodes - ] - ); - } -@@ -394,13 +383,16 @@ class CategoryTest extends \PHPUnit\Framework\TestCase - public function reindexFlatDisabledTestDataProvider() - { - return [ -- [false, null, null, null, 0], -- [true, null, null, null, 0], -- [false, [], null, null, 0], -- [false, ["1", "2"], null, null, 1], -- [false, null, 1, null, 1], -- [false, ["1", "2"], 0, 1, 1], -- [false, null, 1, 1, 0], -+ [false, null, null, null, null, null, 0], -+ [true, null, null, null, null, null, 0], -+ [false, [], null, null, null, null, 0], -+ [false, ["1", "2"], null, null, null, null, 1], -+ [false, null, 1, null, null, null, 1], -+ [false, ["1", "2"], 0, 1, null, null, 1], -+ [false, null, 1, 1, null, null, 0], -+ [false, ["1", "2"], null, null, 0, 1, 1], -+ [false, ["1", "2"], null, null, 1, 0, 1], -+ - ]; - } - -@@ -418,11 +410,16 @@ class CategoryTest extends \PHPUnit\Framework\TestCase - $affectedIds, - $isAnchorOrig, - $isAnchor, -+ $isActiveOrig, -+ $isActive, - $expectedProductReindexCall - ) { - $this->category->setAffectedProductIds($affectedIds); - $this->category->setData('is_anchor', $isAnchor); - $this->category->setOrigData('is_anchor', $isAnchorOrig); -+ $this->category->setData('is_active', $isActive); -+ $this->category->setOrigData('is_active', $isActiveOrig); -+ - $this->category->setAffectedProductIds($affectedIds); - - $pathIds = ['path/1/2', 'path/2/3']; -@@ -433,7 +430,7 @@ class CategoryTest extends \PHPUnit\Framework\TestCase - ->method('isFlatEnabled') - ->will($this->returnValue(false)); - -- $this->productIndexer->expects($this->exactly(1)) -+ $this->productIndexer - ->method('isScheduled') - ->willReturn($productScheduled); - $this->productIndexer->expects($this->exactly($expectedProductReindexCall)) -@@ -455,10 +452,20 @@ class CategoryTest extends \PHPUnit\Framework\TestCase - $initialCustomAttributeValue = 'initial description'; - $newCustomAttributeValue = 'new description'; - -- $this->getCustomAttributeCodes->expects($this->exactly(3)) -- ->method('execute') -- ->willReturn([$customAttributeCode]); -- $this->category->setData($interfaceAttributeCode, "sub"); -+ $interfaceAttribute = $this->createMock(\Magento\Framework\Api\MetadataObjectInterface::class); -+ $interfaceAttribute->expects($this->once()) -+ ->method('getAttributeCode') -+ ->willReturn($interfaceAttributeCode); -+ $colorAttribute = $this->createMock(\Magento\Framework\Api\MetadataObjectInterface::class); -+ $colorAttribute->expects($this->once()) -+ ->method('getAttributeCode') -+ ->willReturn($customAttributeCode); -+ $customAttributesMetadata = [$interfaceAttribute, $colorAttribute]; -+ -+ $this->metadataServiceMock->expects($this->once()) -+ ->method('getCustomAttributesMetadata') -+ ->willReturn($customAttributesMetadata); -+ $this->category->setData($interfaceAttributeCode, 10); - - //The description attribute is not set, expect empty custom attribute array - $this->assertEquals([], $this->category->getCustomAttributes()); -diff --git a/app/code/Magento/Catalog/Test/Unit/Model/CollectionProviderTest.php b/app/code/Magento/Catalog/Test/Unit/Model/CollectionProviderTest.php -index d8931cbbfcf..f0e17c7938b 100644 ---- a/app/code/Magento/Catalog/Test/Unit/Model/CollectionProviderTest.php -+++ b/app/code/Magento/Catalog/Test/Unit/Model/CollectionProviderTest.php -@@ -57,10 +57,14 @@ class CollectionProviderTest extends \PHPUnit\Framework\TestCase - $linkedProductOneMock = $this->createMock(Product::class); - $linkedProductTwoMock = $this->createMock(Product::class); - $linkedProductThreeMock = $this->createMock(Product::class); -+ $linkedProductFourMock = $this->createMock(Product::class); -+ $linkedProductFiveMock = $this->createMock(Product::class); - - $linkedProductOneMock->expects($this->once())->method('getId')->willReturn(1); - $linkedProductTwoMock->expects($this->once())->method('getId')->willReturn(2); - $linkedProductThreeMock->expects($this->once())->method('getId')->willReturn(3); -+ $linkedProductFourMock->expects($this->once())->method('getId')->willReturn(4); -+ $linkedProductFiveMock->expects($this->once())->method('getId')->willReturn(5); - - $this->converterPoolMock->expects($this->once()) - ->method('getConverter') -@@ -71,9 +75,11 @@ class CollectionProviderTest extends \PHPUnit\Framework\TestCase - [$linkedProductOneMock, ['name' => 'Product One', 'position' => 10]], - [$linkedProductTwoMock, ['name' => 'Product Two', 'position' => 2]], - [$linkedProductThreeMock, ['name' => 'Product Three', 'position' => 2]], -+ [$linkedProductFourMock, ['name' => 'Product Four', 'position' => null]], -+ [$linkedProductFiveMock, ['name' => 'Product Five']], - ]; - -- $this->converterMock->expects($this->exactly(3))->method('convert')->willReturnMap($map); -+ $this->converterMock->expects($this->exactly(5))->method('convert')->willReturnMap($map); - - $this->providerMock->expects($this->once()) - ->method('getLinkedProducts') -@@ -82,14 +88,18 @@ class CollectionProviderTest extends \PHPUnit\Framework\TestCase - [ - $linkedProductOneMock, - $linkedProductTwoMock, -- $linkedProductThreeMock -+ $linkedProductThreeMock, -+ $linkedProductFourMock, -+ $linkedProductFiveMock, - ] - ); - - $expectedResult = [ -- 2 => ['name' => 'Product Two', 'position' => 2], -- 3 => ['name' => 'Product Three', 'position' => 2], -- 10 => ['name' => 'Product One', 'position' => 10], -+ 0 => ['name' => 'Product Four', 'position' => 0], -+ 1 => ['name' => 'Product Five', 'position' => 0], -+ 2 => ['name' => 'Product Three', 'position' => 2], -+ 3 => ['name' => 'Product Two', 'position' => 2], -+ 4 => ['name' => 'Product One', 'position' => 10], - ]; - - $actualResult = $this->model->getCollection($this->productMock, 'crosssell'); -diff --git a/app/code/Magento/Catalog/Test/Unit/Model/Config/CatalogClone/Media/ImageTest.php b/app/code/Magento/Catalog/Test/Unit/Model/Config/CatalogClone/Media/ImageTest.php -index 5b1d3bf7943..23f0aec5b69 100644 ---- a/app/code/Magento/Catalog/Test/Unit/Model/Config/CatalogClone/Media/ImageTest.php -+++ b/app/code/Magento/Catalog/Test/Unit/Model/Config/CatalogClone/Media/ImageTest.php -@@ -9,6 +9,11 @@ use Magento\Catalog\Model\Product; - use Magento\Eav\Model\Entity\Attribute\Frontend\AbstractFrontend; - use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; - -+/** -+ * Tests \Magento\Catalog\Model\Config\CatalogClone\Media\Image. -+ * -+ * @SuppressWarnings(PHPMD.CouplingBetweenObjects) -+ */ - class ImageTest extends \PHPUnit\Framework\TestCase - { - /** -@@ -36,6 +41,14 @@ class ImageTest extends \PHPUnit\Framework\TestCase - */ - private $attribute; - -+ /** -+ * @var \Magento\Framework\Escaper|\PHPUnit_Framework_MockObject_MockObject -+ */ -+ private $escaperMock; -+ -+ /** -+ * @inheritdoc -+ */ - protected function setUp() - { - $this->eavConfig = $this->getMockBuilder(\Magento\Eav\Model\Config::class) -@@ -62,54 +75,79 @@ class ImageTest extends \PHPUnit\Framework\TestCase - ->disableOriginalConstructor() - ->getMock(); - -+ $this->escaperMock = $this->getMockBuilder( -+ \Magento\Framework\Escaper::class -+ ) -+ ->disableOriginalConstructor() -+ ->setMethods(['escapeHtml']) -+ ->getMock(); -+ - $helper = new ObjectManager($this); - $this->model = $helper->getObject( - \Magento\Catalog\Model\Config\CatalogClone\Media\Image::class, - [ - 'eavConfig' => $this->eavConfig, -- 'attributeCollectionFactory' => $this->attributeCollectionFactory -+ 'attributeCollectionFactory' => $this->attributeCollectionFactory, -+ 'escaper' => $this->escaperMock, - ] - ); - } - -- public function testGetPrefixes() -+ /** -+ * @param string $actualLabel -+ * @param string $expectedLabel -+ * @return void -+ * -+ * @dataProvider getPrefixesDataProvider -+ */ -+ public function testGetPrefixes(string $actualLabel, string $expectedLabel): void - { - $entityTypeId = 3; - /** @var \Magento\Eav\Model\Entity\Type|\PHPUnit_Framework_MockObject_MockObject $entityType */ - $entityType = $this->getMockBuilder(\Magento\Eav\Model\Entity\Type::class) - ->disableOriginalConstructor() - ->getMock(); -- $entityType->expects($this->once())->method('getId')->will($this->returnValue($entityTypeId)); -+ $entityType->expects($this->once())->method('getId')->willReturn($entityTypeId); - - /** @var AbstractFrontend|\PHPUnit_Framework_MockObject_MockObject $frontend */ - $frontend = $this->getMockBuilder(\Magento\Eav\Model\Entity\Attribute\Frontend\AbstractFrontend::class) - ->setMethods(['getLabel']) - ->disableOriginalConstructor() - ->getMockForAbstractClass(); -- $frontend->expects($this->once())->method('getLabel')->will($this->returnValue('testLabel')); -+ $frontend->expects($this->once())->method('getLabel')->willReturn($actualLabel); - -- $this->attributeCollection->expects($this->once())->method('setEntityTypeFilter')->with( -- $this->equalTo($entityTypeId) -- ); -- $this->attributeCollection->expects($this->once())->method('setFrontendInputTypeFilter')->with( -- $this->equalTo('media_image') -- ); -+ $this->attributeCollection->expects($this->once())->method('setEntityTypeFilter')->with($entityTypeId); -+ $this->attributeCollection->expects($this->once())->method('setFrontendInputTypeFilter')->with('media_image'); - -- $this->attribute->expects($this->once())->method('getAttributeCode')->will( -- $this->returnValue('attributeCode') -- ); -- $this->attribute->expects($this->once())->method('getFrontend')->will( -- $this->returnValue($frontend) -- ); -+ $this->attribute->expects($this->once())->method('getAttributeCode')->willReturn('attributeCode'); -+ $this->attribute->expects($this->once())->method('getFrontend')->willReturn($frontend); - -- $this->attributeCollection->expects($this->any())->method('getIterator')->will( -- $this->returnValue(new \ArrayIterator([$this->attribute])) -- ); -+ $this->attributeCollection->expects($this->any())->method('getIterator') -+ ->willReturn(new \ArrayIterator([$this->attribute])); -+ -+ $this->eavConfig->expects($this->any())->method('getEntityType')->with(Product::ENTITY) -+ ->willReturn($entityType); - -- $this->eavConfig->expects($this->any())->method('getEntityType')->with( -- $this->equalTo(Product::ENTITY) -- )->will($this->returnValue($entityType)); -+ $this->escaperMock->expects($this->once())->method('escapeHtml')->with($actualLabel) -+ ->willReturn($expectedLabel); - -- $this->assertEquals([['field' => 'attributeCode_', 'label' => 'testLabel']], $this->model->getPrefixes()); -+ $this->assertEquals([['field' => 'attributeCode_', 'label' => $expectedLabel]], $this->model->getPrefixes()); -+ } -+ -+ /** -+ * @return array -+ */ -+ public function getPrefixesDataProvider(): array -+ { -+ return [ -+ [ -+ 'actual_label' => 'testLabel', -+ 'expected_label' => 'testLabel', -+ ], -+ [ -+ 'actual_label' => '<media-image-attributelabel', -+ 'expected_label' => '<media-image-attributelabel', -+ ], -+ ]; - } - } -diff --git a/app/code/Magento/Catalog/Test/Unit/Model/Entity/GetCategoryCustomAttributeCodesTest.php b/app/code/Magento/Catalog/Test/Unit/Model/Entity/GetCategoryCustomAttributeCodesTest.php -deleted file mode 100644 -index 465063dccd3..00000000000 ---- a/app/code/Magento/Catalog/Test/Unit/Model/Entity/GetCategoryCustomAttributeCodesTest.php -+++ /dev/null -@@ -1,66 +0,0 @@ --<?php --/** -- * Copyright © Magento, Inc. All rights reserved. -- * See COPYING.txt for license details. -- */ -- --namespace Magento\Catalog\Test\Unit\Model\Entity; -- --use Magento\Catalog\Model\Entity\GetCategoryCustomAttributeCodes; --use Magento\Eav\Model\Entity\GetCustomAttributeCodesInterface; --use Magento\Framework\Api\MetadataServiceInterface; --use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; --use PHPUnit\Framework\TestCase; -- --/** -- * Provide tests for GetCategoryCustomAttributeCodes entity model. -- */ --class GetCategoryCustomAttributeCodesTest extends TestCase --{ -- /** -- * Test subject. -- * -- * @var GetCategoryCustomAttributeCodes -- */ -- private $getCategoryCustomAttributeCodes; -- -- /** -- * @var GetCustomAttributeCodesInterface|\PHPUnit_Framework_MockObject_MockObject -- */ -- private $baseCustomAttributeCodes; -- -- /** -- * @inheritdoc -- */ -- protected function setUp() -- { -- $this->baseCustomAttributeCodes = $this->getMockBuilder(GetCustomAttributeCodesInterface::class) -- ->disableOriginalConstructor() -- ->setMethods(['execute']) -- ->getMockForAbstractClass(); -- $objectManager = new ObjectManager($this); -- $this->getCategoryCustomAttributeCodes = $objectManager->getObject( -- GetCategoryCustomAttributeCodes::class, -- ['baseCustomAttributeCodes' => $this->baseCustomAttributeCodes] -- ); -- } -- -- /** -- * Test GetCategoryCustomAttributeCodes::execute() will return only custom category attribute codes. -- */ -- public function testExecute() -- { -- /** @var MetadataServiceInterface|\PHPUnit_Framework_MockObject_MockObject $metadataService */ -- $metadataService = $this->getMockBuilder(MetadataServiceInterface::class) -- ->disableOriginalConstructor() -- ->getMockForAbstractClass(); -- $this->baseCustomAttributeCodes->expects($this->once()) -- ->method('execute') -- ->with($this->identicalTo($metadataService)) -- ->willReturn(['test_custom_attribute_code', 'name']); -- $this->assertEquals( -- ['test_custom_attribute_code'], -- $this->getCategoryCustomAttributeCodes->execute($metadataService) -- ); -- } --} -diff --git a/app/code/Magento/Catalog/Test/Unit/Model/Entity/GetProductCustomAttributeCodesTest.php b/app/code/Magento/Catalog/Test/Unit/Model/Entity/GetProductCustomAttributeCodesTest.php -deleted file mode 100644 -index a37e1c6df09..00000000000 ---- a/app/code/Magento/Catalog/Test/Unit/Model/Entity/GetProductCustomAttributeCodesTest.php -+++ /dev/null -@@ -1,66 +0,0 @@ --<?php --/** -- * Copyright © Magento, Inc. All rights reserved. -- * See COPYING.txt for license details. -- */ -- --namespace Magento\Catalog\Test\Unit\Model\Entity; -- --use Magento\Catalog\Model\Entity\GetProductCustomAttributeCodes; --use Magento\Eav\Model\Entity\GetCustomAttributeCodesInterface; --use Magento\Framework\Api\MetadataServiceInterface; --use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; --use PHPUnit\Framework\TestCase; -- --/** -- * Provide tests for GetProductCustomAttributeCodes entity model. -- */ --class GetProductCustomAttributeCodesTest extends TestCase --{ -- /** -- * Test subject. -- * -- * @var GetProductCustomAttributeCodes -- */ -- private $getProductCustomAttributeCodes; -- -- /** -- * @var GetCustomAttributeCodesInterface|\PHPUnit_Framework_MockObject_MockObject -- */ -- private $baseCustomAttributeCodes; -- -- /** -- * @inheritdoc -- */ -- protected function setUp() -- { -- $this->baseCustomAttributeCodes = $this->getMockBuilder(GetCustomAttributeCodesInterface::class) -- ->disableOriginalConstructor() -- ->setMethods(['execute']) -- ->getMockForAbstractClass(); -- $objectManager = new ObjectManager($this); -- $this->getProductCustomAttributeCodes = $objectManager->getObject( -- GetProductCustomAttributeCodes::class, -- ['baseCustomAttributeCodes' => $this->baseCustomAttributeCodes] -- ); -- } -- -- /** -- * Test GetProductCustomAttributeCodes::execute() will return only custom product attribute codes. -- */ -- public function testExecute() -- { -- /** @var MetadataServiceInterface|\PHPUnit_Framework_MockObject_MockObject $metadataService */ -- $metadataService = $this->getMockBuilder(MetadataServiceInterface::class) -- ->disableOriginalConstructor() -- ->getMockForAbstractClass(); -- $this->baseCustomAttributeCodes->expects($this->once()) -- ->method('execute') -- ->with($this->identicalTo($metadataService)) -- ->willReturn(['test_custom_attribute_code', 'name']); -- $this->assertEquals( -- ['test_custom_attribute_code'], -- $this->getProductCustomAttributeCodes->execute($metadataService) -- ); -- } --} -diff --git a/app/code/Magento/Catalog/Test/Unit/Model/ImageUploaderTest.php b/app/code/Magento/Catalog/Test/Unit/Model/ImageUploaderTest.php -index c989f2dd474..6552e854400 100644 ---- a/app/code/Magento/Catalog/Test/Unit/Model/ImageUploaderTest.php -+++ b/app/code/Magento/Catalog/Test/Unit/Model/ImageUploaderTest.php -@@ -69,10 +69,17 @@ class ImageUploaderTest extends \PHPUnit\Framework\TestCase - /** - * Allowed extensions - * -- * @var string -+ * @var array - */ - private $allowedExtensions; - -+ /** -+ * Allowed mime types -+ * -+ * @var array -+ */ -+ private $allowedMimeTypes; -+ - protected function setUp() - { - $this->coreFileStorageDatabaseMock = $this->createMock( -@@ -97,6 +104,7 @@ class ImageUploaderTest extends \PHPUnit\Framework\TestCase - $this->baseTmpPath = 'base/tmp/'; - $this->basePath = 'base/real/'; - $this->allowedExtensions = ['.jpg']; -+ $this->allowedMimeTypes = ['image/jpg', 'image/jpeg', 'image/gif', 'image/png']; - - $this->imageUploader = - new \Magento\Catalog\Model\ImageUploader( -@@ -107,7 +115,8 @@ class ImageUploaderTest extends \PHPUnit\Framework\TestCase - $this->loggerMock, - $this->baseTmpPath, - $this->basePath, -- $this->allowedExtensions -+ $this->allowedExtensions, -+ $this->allowedMimeTypes - ); - } - -diff --git a/app/code/Magento/Catalog/Test/Unit/Model/Indexer/Product/Eav/Action/FullTest.php b/app/code/Magento/Catalog/Test/Unit/Model/Indexer/Product/Eav/Action/FullTest.php -index 90c3f999a6a..2e1cff834fd 100644 ---- a/app/code/Magento/Catalog/Test/Unit/Model/Indexer/Product/Eav/Action/FullTest.php -+++ b/app/code/Magento/Catalog/Test/Unit/Model/Indexer/Product/Eav/Action/FullTest.php -@@ -3,15 +3,29 @@ - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -+ -+declare(strict_types=1); -+ - namespace Magento\Catalog\Test\Unit\Model\Indexer\Product\Eav\Action; - -+use Magento\Catalog\Api\Data\ProductInterface; -+use Magento\Catalog\Model\Indexer\Product\Eav\Action\Full; - use Magento\Catalog\Model\ResourceModel\Indexer\ActiveTableSwitcher; -+use Magento\Catalog\Model\ResourceModel\Product\Indexer\Eav\Decimal; -+use Magento\Catalog\Model\ResourceModel\Product\Indexer\Eav\Source; -+use Magento\Framework\App\Config\ScopeConfigInterface; -+use Magento\Framework\DB\Adapter\AdapterInterface; -+use Magento\Framework\DB\Query\Generator; -+use Magento\Framework\DB\Select; -+use Magento\Framework\EntityManager\EntityMetadataInterface; -+use Magento\Framework\Exception\LocalizedException; - use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; - use Magento\Catalog\Model\ResourceModel\Product\Indexer\Eav\DecimalFactory; - use Magento\Catalog\Model\ResourceModel\Product\Indexer\Eav\SourceFactory; - use Magento\Framework\EntityManager\MetadataPool; - use Magento\Framework\Indexer\BatchProviderInterface; - use Magento\Catalog\Model\ResourceModel\Product\Indexer\Eav\BatchSizeCalculator; -+use PHPUnit\Framework\MockObject\MockObject as MockObject; - - /** - * @SuppressWarnings(PHPMD.CouplingBetweenObjects) -@@ -19,45 +33,50 @@ use Magento\Catalog\Model\ResourceModel\Product\Indexer\Eav\BatchSizeCalculator; - class FullTest extends \PHPUnit\Framework\TestCase - { - /** -- * @var \Magento\Catalog\Model\Indexer\Product\Eav\Action\Full|\PHPUnit_Framework_MockObject_MockObject -+ * @var Full|MockObject - */ - private $model; - - /** -- * @var DecimalFactory|\PHPUnit_Framework_MockObject_MockObject -+ * @var DecimalFactory|MockObject - */ - private $eavDecimalFactory; - - /** -- * @var SourceFactory|\PHPUnit_Framework_MockObject_MockObject -+ * @var SourceFactory|MockObject - */ - private $eavSourceFactory; - - /** -- * @var MetadataPool|\PHPUnit_Framework_MockObject_MockObject -+ * @var MetadataPool|MockObject - */ - private $metadataPool; - - /** -- * @var BatchProviderInterface|\PHPUnit_Framework_MockObject_MockObject -+ * @var BatchProviderInterface|MockObject - */ - private $batchProvider; - - /** -- * @var BatchSizeCalculator|\PHPUnit_Framework_MockObject_MockObject -+ * @var BatchSizeCalculator|MockObject - */ - private $batchSizeCalculator; - - /** -- * @var ActiveTableSwitcher|\PHPUnit_Framework_MockObject_MockObject -+ * @var ActiveTableSwitcher|MockObject - */ - private $activeTableSwitcher; - - /** -- * @var \Magento\Framework\App\Config\ScopeConfigInterface|\PHPUnit_Framework_MockObject_MockObject -+ * @var ScopeConfigInterface|MockObject - */ - private $scopeConfig; - -+ /** -+ * @var Generator -+ */ -+ private $batchQueryGenerator; -+ - /** - * @return void - */ -@@ -67,15 +86,16 @@ class FullTest extends \PHPUnit\Framework\TestCase - $this->eavSourceFactory = $this->createPartialMock(SourceFactory::class, ['create']); - $this->metadataPool = $this->createMock(MetadataPool::class); - $this->batchProvider = $this->getMockForAbstractClass(BatchProviderInterface::class); -+ $this->batchQueryGenerator = $this->createMock(Generator::class); - $this->batchSizeCalculator = $this->createMock(BatchSizeCalculator::class); - $this->activeTableSwitcher = $this->createMock(ActiveTableSwitcher::class); -- $this->scopeConfig = $this->getMockBuilder(\Magento\Framework\App\Config\ScopeConfigInterface::class) -+ $this->scopeConfig = $this->getMockBuilder(ScopeConfigInterface::class) - ->disableOriginalConstructor() - ->getMockForAbstractClass(); - - $objectManager = new ObjectManager($this); - $this->model = $objectManager->getObject( -- \Magento\Catalog\Model\Indexer\Product\Eav\Action\Full::class, -+ Full::class, - [ - 'eavDecimalFactory' => $this->eavDecimalFactory, - 'eavSourceFactory' => $this->eavSourceFactory, -@@ -83,7 +103,8 @@ class FullTest extends \PHPUnit\Framework\TestCase - 'batchProvider' => $this->batchProvider, - 'batchSizeCalculator' => $this->batchSizeCalculator, - 'activeTableSwitcher' => $this->activeTableSwitcher, -- 'scopeConfig' => $this->scopeConfig -+ 'scopeConfig' => $this->scopeConfig, -+ 'batchQueryGenerator' => $this->batchQueryGenerator, - ] - ); - } -@@ -96,15 +117,15 @@ class FullTest extends \PHPUnit\Framework\TestCase - $this->scopeConfig->expects($this->once())->method('getValue')->willReturn(1); - - $ids = [1, 2, 3]; -- $connectionMock = $this->getMockBuilder(\Magento\Framework\DB\Adapter\AdapterInterface::class) -+ $connectionMock = $this->getMockBuilder(AdapterInterface::class) - ->getMockForAbstractClass(); - - $connectionMock->expects($this->atLeastOnce())->method('describeTable')->willReturn(['id' => []]); -- $eavSource = $this->getMockBuilder(\Magento\Catalog\Model\ResourceModel\Product\Indexer\Eav\Source::class) -+ $eavSource = $this->getMockBuilder(Source::class) - ->disableOriginalConstructor() - ->getMock(); - -- $eavDecimal = $this->getMockBuilder(\Magento\Catalog\Model\ResourceModel\Product\Indexer\Eav\Decimal::class) -+ $eavDecimal = $this->getMockBuilder(Decimal::class) - ->disableOriginalConstructor() - ->getMock(); - -@@ -125,22 +146,28 @@ class FullTest extends \PHPUnit\Framework\TestCase - - $this->eavSourceFactory->expects($this->once())->method('create')->will($this->returnValue($eavDecimal)); - -- $entityMetadataMock = $this->getMockBuilder(\Magento\Framework\EntityManager\EntityMetadataInterface::class) -+ $entityMetadataMock = $this->getMockBuilder(EntityMetadataInterface::class) - ->getMockForAbstractClass(); - - $this->metadataPool->expects($this->atLeastOnce()) - ->method('getMetadata') -- ->with(\Magento\Catalog\Api\Data\ProductInterface::class) -+ ->with(ProductInterface::class) - ->willReturn($entityMetadataMock); - -- $this->batchProvider->expects($this->atLeastOnce()) -- ->method('getBatches') -- ->willReturn([['from' => 10, 'to' => 100]]); -- $this->batchProvider->expects($this->atLeastOnce()) -- ->method('getBatchIds') -+ // Super inefficient algorithm in some cases -+ $this->batchProvider->expects($this->never()) -+ ->method('getBatches'); -+ -+ $batchQuery = $this->createMock(Select::class); -+ -+ $connectionMock->method('fetchCol') -+ ->with($batchQuery) - ->willReturn($ids); - -- $selectMock = $this->getMockBuilder(\Magento\Framework\DB\Select::class) -+ $this->batchQueryGenerator->method('generate') -+ ->willReturn([$batchQuery]); -+ -+ $selectMock = $this->getMockBuilder(Select::class) - ->disableOriginalConstructor() - ->getMock(); - -@@ -153,7 +180,7 @@ class FullTest extends \PHPUnit\Framework\TestCase - - /** - * @return void -- * @throws \Magento\Framework\Exception\LocalizedException -+ * @throws LocalizedException - */ - public function testExecuteWithDisabledEavIndexer() - { -diff --git a/app/code/Magento/Catalog/Test/Unit/Model/Indexer/Product/Flat/Action/EraserTest.php b/app/code/Magento/Catalog/Test/Unit/Model/Indexer/Product/Flat/Action/EraserTest.php -index cc6f5d84ef0..e1e2816d442 100644 ---- a/app/code/Magento/Catalog/Test/Unit/Model/Indexer/Product/Flat/Action/EraserTest.php -+++ b/app/code/Magento/Catalog/Test/Unit/Model/Indexer/Product/Flat/Action/EraserTest.php -@@ -53,8 +53,14 @@ class EraserTest extends \PHPUnit\Framework\TestCase - { - $productsToDeleteIds = [1, 2]; - $select = $this->createMock(\Magento\Framework\DB\Select::class); -- $select->expects($this->once())->method('from')->with('catalog_product_entity')->will($this->returnSelf()); -- $select->expects($this->once())->method('where')->with('entity_id IN(?)', $productsToDeleteIds) -+ $select->expects($this->once()) -+ ->method('from') -+ ->with(['product_table' => 'catalog_product_entity']) -+ ->will($this->returnSelf()); -+ $select->expects($this->once())->method('columns')->with('entity_id')->will($this->returnSelf()); -+ $select->expects($this->once()) -+ ->method('where') -+ ->with('product_table.entity_id IN(?)', $productsToDeleteIds) - ->will($this->returnSelf()); - $products = [['entity_id' => 2]]; - $statement = $this->createMock(\Zend_Db_Statement_Interface::class); -diff --git a/app/code/Magento/Catalog/Test/Unit/Model/Indexer/Product/Flat/Action/RowTest.php b/app/code/Magento/Catalog/Test/Unit/Model/Indexer/Product/Flat/Action/RowTest.php -index 7b2a2ff304b..11d07872fef 100644 ---- a/app/code/Magento/Catalog/Test/Unit/Model/Indexer/Product/Flat/Action/RowTest.php -+++ b/app/code/Magento/Catalog/Test/Unit/Model/Indexer/Product/Flat/Action/RowTest.php -@@ -6,8 +6,13 @@ - - namespace Magento\Catalog\Test\Unit\Model\Indexer\Product\Flat\Action; - -+use Magento\Catalog\Api\Data\ProductInterface; - use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; -+use PHPUnit_Framework_MockObject_MockObject as MockObject; - -+/** -+ * @SuppressWarnings(PHPMD.CouplingBetweenObjects) -+ */ - class RowTest extends \PHPUnit\Framework\TestCase - { - /** -@@ -16,76 +21,116 @@ class RowTest extends \PHPUnit\Framework\TestCase - protected $model; - - /** -- * @var \PHPUnit_Framework_MockObject_MockObject -+ * @var MockObject - */ - protected $storeManager; - - /** -- * @var \PHPUnit_Framework_MockObject_MockObject -+ * @var MockObject - */ - protected $store; - - /** -- * @var \PHPUnit_Framework_MockObject_MockObject -+ * @var MockObject - */ - protected $productIndexerHelper; - - /** -- * @var \PHPUnit_Framework_MockObject_MockObject -+ * @var MockObject - */ - protected $resource; - - /** -- * @var \PHPUnit_Framework_MockObject_MockObject -+ * @var MockObject - */ - protected $connection; - - /** -- * @var \PHPUnit_Framework_MockObject_MockObject -+ * @var MockObject - */ - protected $flatItemWriter; - - /** -- * @var \PHPUnit_Framework_MockObject_MockObject -+ * @var MockObject - */ - protected $flatItemEraser; - - /** -- * @var \PHPUnit_Framework_MockObject_MockObject -+ * @var MockObject - */ - protected $flatTableBuilder; - -+ /** -+ * @inheritdoc -+ */ - protected function setUp() - { - $objectManager = new ObjectManager($this); - -+ $attributeTable = 'catalog_product_entity_int'; -+ $statusId = 22; - $this->connection = $this->createMock(\Magento\Framework\DB\Adapter\AdapterInterface::class); - $this->resource = $this->createMock(\Magento\Framework\App\ResourceConnection::class); - $this->resource->expects($this->any())->method('getConnection') - ->with('default') -- ->will($this->returnValue($this->connection)); -+ ->willReturn($this->connection); - $this->storeManager = $this->createMock(\Magento\Store\Model\StoreManagerInterface::class); - $this->store = $this->createMock(\Magento\Store\Model\Store::class); -- $this->store->expects($this->any())->method('getId')->will($this->returnValue('store_id_1')); -- $this->storeManager->expects($this->any())->method('getStores')->will($this->returnValue([$this->store])); -- $this->productIndexerHelper = $this->createMock(\Magento\Catalog\Helper\Product\Flat\Indexer::class); -+ $this->store->expects($this->any())->method('getId')->willReturn('store_id_1'); -+ $this->storeManager->expects($this->any())->method('getStores')->willReturn([$this->store]); - $this->flatItemEraser = $this->createMock(\Magento\Catalog\Model\Indexer\Product\Flat\Action\Eraser::class); - $this->flatItemWriter = $this->createMock(\Magento\Catalog\Model\Indexer\Product\Flat\Action\Indexer::class); - $this->flatTableBuilder = $this->createMock( - \Magento\Catalog\Model\Indexer\Product\Flat\FlatTableBuilder::class - ); -+ $this->productIndexerHelper = $this->createMock(\Magento\Catalog\Helper\Product\Flat\Indexer::class); -+ $statusAttributeMock = $this->getMockBuilder(\Magento\Eav\Model\Entity\Attribute::class) -+ ->disableOriginalConstructor() -+ ->getMock(); -+ $this->productIndexerHelper->expects($this->any())->method('getAttribute') -+ ->with('status') -+ ->willReturn($statusAttributeMock); -+ $backendMock = $this->getMockBuilder(\Magento\Eav\Model\Entity\Attribute\Backend\AbstractBackend::class) -+ ->disableOriginalConstructor() -+ ->getMock(); -+ $backendMock->expects($this->any())->method('getTable')->willReturn($attributeTable); -+ $statusAttributeMock->expects($this->any())->method('getBackend')->willReturn($backendMock); -+ $statusAttributeMock->expects($this->any())->method('getId')->willReturn($statusId); -+ $selectMock = $this->getMockBuilder(\Magento\Framework\DB\Select::class) -+ ->disableOriginalConstructor() -+ ->getMock(); -+ $this->connection->expects($this->any())->method('select')->willReturn($selectMock); -+ $selectMock->method('from') -+ ->willReturnSelf(); -+ $selectMock->method('joinLeft') -+ ->willReturnSelf(); -+ $selectMock->expects($this->any())->method('where')->willReturnSelf(); -+ $selectMock->expects($this->any())->method('order')->willReturnSelf(); -+ $selectMock->expects($this->any())->method('limit')->willReturnSelf(); -+ $pdoMock = $this->createMock(\Zend_Db_Statement_Pdo::class); -+ $this->connection->expects($this->any())->method('query')->with($selectMock)->willReturn($pdoMock); -+ $pdoMock->expects($this->any())->method('fetchColumn')->willReturn('1'); -+ -+ $metadataPool = $this->createMock(\Magento\Framework\EntityManager\MetadataPool::class); -+ $productMetadata = $this->getMockBuilder(\Magento\Framework\EntityManager\EntityMetadataInterface::class) -+ ->getMockForAbstractClass(); -+ $metadataPool->expects($this->any())->method('getMetadata')->with(ProductInterface::class) -+ ->willReturn($productMetadata); -+ $productMetadata->expects($this->any())->method('getLinkField')->willReturn('entity_id'); - - $this->model = $objectManager->getObject( - \Magento\Catalog\Model\Indexer\Product\Flat\Action\Row::class, - [ -- 'resource' => $this->resource, -- 'storeManager' => $this->storeManager, -- 'productHelper' => $this->productIndexerHelper, -- 'flatItemEraser' => $this->flatItemEraser, -- 'flatItemWriter' => $this->flatItemWriter, -- 'flatTableBuilder' => $this->flatTableBuilder -+ 'resource' => $this->resource, -+ 'storeManager' => $this->storeManager, -+ 'productHelper' => $this->productIndexerHelper, -+ 'flatItemEraser' => $this->flatItemEraser, -+ 'flatItemWriter' => $this->flatItemWriter, -+ 'flatTableBuilder' => $this->flatTableBuilder, - ] - ); -+ -+ $objectManager->setBackwardCompatibleProperty($this->model, 'metadataPool', $metadataPool); - } - - /** -@@ -100,9 +145,9 @@ class RowTest extends \PHPUnit\Framework\TestCase - public function testExecuteWithNonExistingFlatTablesCreatesTables() - { - $this->productIndexerHelper->expects($this->any())->method('getFlatTableName') -- ->will($this->returnValue('store_flat_table')); -+ ->willReturn('store_flat_table'); - $this->connection->expects($this->any())->method('isTableExists')->with('store_flat_table') -- ->will($this->returnValue(false)); -+ ->willReturn(false); - $this->flatItemEraser->expects($this->never())->method('removeDeletedProducts'); - $this->flatTableBuilder->expects($this->once())->method('build')->with('store_id_1', ['product_id_1']); - $this->flatItemWriter->expects($this->once())->method('write')->with('store_id_1', 'product_id_1'); -@@ -112,9 +157,9 @@ class RowTest extends \PHPUnit\Framework\TestCase - public function testExecuteWithExistingFlatTablesCreatesTables() - { - $this->productIndexerHelper->expects($this->any())->method('getFlatTableName') -- ->will($this->returnValue('store_flat_table')); -+ ->willReturn('store_flat_table'); - $this->connection->expects($this->any())->method('isTableExists')->with('store_flat_table') -- ->will($this->returnValue(true)); -+ ->willReturn(true); - $this->flatItemEraser->expects($this->once())->method('removeDeletedProducts'); - $this->flatTableBuilder->expects($this->never())->method('build')->with('store_id_1', ['product_id_1']); - $this->model->execute('product_id_1'); -diff --git a/app/code/Magento/Catalog/Test/Unit/Model/Layer/Filter/DataProvider/PriceTest.php b/app/code/Magento/Catalog/Test/Unit/Model/Layer/Filter/DataProvider/PriceTest.php -index f2c77627e38..8ca23df31cd 100644 ---- a/app/code/Magento/Catalog/Test/Unit/Model/Layer/Filter/DataProvider/PriceTest.php -+++ b/app/code/Magento/Catalog/Test/Unit/Model/Layer/Filter/DataProvider/PriceTest.php -@@ -152,7 +152,7 @@ class PriceTest extends \PHPUnit\Framework\TestCase - $this->productCollection->expects($this->once()) - ->method('getMaxPrice') - ->will($this->returnValue($maxPrice)); -- $this->assertSame(floatval($maxPrice), $this->target->getMaxPrice()); -+ $this->assertSame((float)$maxPrice, $this->target->getMaxPrice()); - } - - /** -diff --git a/app/code/Magento/Catalog/Test/Unit/Model/Product/Attribute/RepositoryTest.php b/app/code/Magento/Catalog/Test/Unit/Model/Product/Attribute/RepositoryTest.php -index 3cc6f94d58c..1b42b09e5dd 100644 ---- a/app/code/Magento/Catalog/Test/Unit/Model/Product/Attribute/RepositoryTest.php -+++ b/app/code/Magento/Catalog/Test/Unit/Model/Product/Attribute/RepositoryTest.php -@@ -10,7 +10,6 @@ namespace Magento\Catalog\Test\Unit\Model\Product\Attribute; - use Magento\Catalog\Api\Data\ProductAttributeInterface; - use Magento\Catalog\Model\Product\Attribute\Repository; - use Magento\Catalog\Model\ResourceModel\Eav\Attribute; --use Magento\Eav\Api\Data\AttributeFrontendLabelInterface; - - /** - * @SuppressWarnings(PHPMD.CouplingBetweenObjects) -@@ -72,6 +71,9 @@ class RepositoryTest extends \PHPUnit\Framework\TestCase - */ - private $optionManagementMock; - -+ /** -+ * @inheritdoc -+ */ - protected function setUp() - { - $this->attributeResourceMock = -@@ -116,6 +118,9 @@ class RepositoryTest extends \PHPUnit\Framework\TestCase - ); - } - -+ /** -+ * @return void -+ */ - public function testGet() - { - $attributeCode = 'some attribute code'; -@@ -128,6 +133,9 @@ class RepositoryTest extends \PHPUnit\Framework\TestCase - $this->model->get($attributeCode); - } - -+ /** -+ * @return void -+ */ - public function testGetList() - { - $searchCriteriaMock = $this->createMock(\Magento\Framework\Api\SearchCriteria::class); -@@ -141,6 +149,9 @@ class RepositoryTest extends \PHPUnit\Framework\TestCase - $this->model->getList($searchCriteriaMock); - } - -+ /** -+ * @return void -+ */ - public function testDelete() - { - $attributeMock = $this->createMock(\Magento\Catalog\Model\ResourceModel\Eav\Attribute::class); -@@ -149,6 +160,9 @@ class RepositoryTest extends \PHPUnit\Framework\TestCase - $this->assertEquals(true, $this->model->delete($attributeMock)); - } - -+ /** -+ * @return void -+ */ - public function testDeleteById() - { - $attributeCode = 'some attribute code'; -@@ -164,6 +178,9 @@ class RepositoryTest extends \PHPUnit\Framework\TestCase - $this->assertEquals(true, $this->model->deleteById($attributeCode)); - } - -+ /** -+ * @return void -+ */ - public function testGetCustomAttributesMetadata() - { - $searchCriteriaMock = $this->createMock(\Magento\Framework\Api\SearchCriteria::class); -@@ -235,15 +252,18 @@ class RepositoryTest extends \PHPUnit\Framework\TestCase - ); - $attributeMock->expects($this->once())->method('getAttributeId')->willReturn(null); - $attributeMock->expects($this->once())->method('setAttributeId')->with(null)->willReturnSelf(); -- $labelMock = $this->createMock(\Magento\Eav\Api\Data\AttributeFrontendLabelInterface::class); -- $attributeMock->expects($this->exactly(4))->method('getFrontendLabels')->willReturn([$labelMock]); -- $attributeMock->expects($this->exactly(2))->method('getDefaultFrontendLabel')->willReturn('test'); -+ $labelMock = $this->createMock(\Magento\Eav\Model\Entity\Attribute\FrontendLabel::class); -+ $attributeMock->expects($this->any())->method('getFrontendLabels')->willReturn([$labelMock]); -+ $attributeMock->expects($this->any())->method('getDefaultFrontendLabel')->willReturn(null); - $labelMock->expects($this->once())->method('getStoreId')->willReturn(0); - $labelMock->expects($this->once())->method('getLabel')->willReturn(null); - - $this->model->save($attributeMock); - } - -+ /** -+ * @return void -+ */ - public function testSaveDoesNotSaveAttributeOptionsIfOptionsAreAbsentInPayload() - { - $attributeId = 1; -@@ -260,7 +280,7 @@ class RepositoryTest extends \PHPUnit\Framework\TestCase - ->method('get') - ->with(ProductAttributeInterface::ENTITY_TYPE_CODE, $attributeCode) - ->willReturn($existingModelMock); -- -+ $existingModelMock->expects($this->once())->method('getDefaultFrontendLabel')->willReturn('default_label'); - // Attribute code must not be changed after attribute creation - $attributeMock->expects($this->once())->method('setAttributeCode')->with($attributeCode); - $this->attributeResourceMock->expects($this->once())->method('save')->with($attributeMock); -@@ -269,9 +289,12 @@ class RepositoryTest extends \PHPUnit\Framework\TestCase - $this->model->save($attributeMock); - } - -+ /** -+ * @return void -+ */ - public function testSaveSavesDefaultFrontendLabelIfItIsPresentInPayload() - { -- $labelMock = $this->createMock(AttributeFrontendLabelInterface::class); -+ $labelMock = $this->createMock(\Magento\Eav\Api\Data\AttributeFrontendLabelInterface::class); - $labelMock->expects($this->any())->method('getStoreId')->willReturn(1); - $labelMock->expects($this->any())->method('getLabel')->willReturn('Store Scope Label'); - -@@ -280,11 +303,12 @@ class RepositoryTest extends \PHPUnit\Framework\TestCase - $attributeMock = $this->createMock(Attribute::class); - $attributeMock->expects($this->any())->method('getAttributeCode')->willReturn($attributeCode); - $attributeMock->expects($this->any())->method('getAttributeId')->willReturn($attributeId); -- $attributeMock->expects($this->any())->method('getDefaultFrontendLabel')->willReturn('Default Label'); -+ $attributeMock->expects($this->any())->method('getDefaultFrontendLabel')->willReturn(null); - $attributeMock->expects($this->any())->method('getFrontendLabels')->willReturn([$labelMock]); - $attributeMock->expects($this->any())->method('getOptions')->willReturn([]); - - $existingModelMock = $this->createMock(Attribute::class); -+ $existingModelMock->expects($this->any())->method('getDefaultFrontendLabel')->willReturn('Default Label'); - $existingModelMock->expects($this->any())->method('getAttributeId')->willReturn($attributeId); - $existingModelMock->expects($this->any())->method('getAttributeCode')->willReturn($attributeCode); - -@@ -295,12 +319,7 @@ class RepositoryTest extends \PHPUnit\Framework\TestCase - - $attributeMock->expects($this->once()) - ->method('setDefaultFrontendLabel') -- ->with( -- [ -- 0 => 'Default Label', -- 1 => 'Store Scope Label' -- ] -- ); -+ ->with('Default Label'); - $this->attributeResourceMock->expects($this->once())->method('save')->with($attributeMock); - - $this->model->save($attributeMock); -diff --git a/app/code/Magento/Catalog/Test/Unit/Model/Product/CopierTest.php b/app/code/Magento/Catalog/Test/Unit/Model/Product/CopierTest.php -index 31fd0696db3..80b6db2a516 100644 ---- a/app/code/Magento/Catalog/Test/Unit/Model/Product/CopierTest.php -+++ b/app/code/Magento/Catalog/Test/Unit/Model/Product/CopierTest.php -@@ -6,9 +6,12 @@ - namespace Magento\Catalog\Test\Unit\Model\Product; - - use Magento\Catalog\Api\Data\ProductInterface; --use \Magento\Catalog\Model\Product\Copier; -+use Magento\Catalog\Model\Product; -+use Magento\Catalog\Model\Product\Copier; - - /** -+ * Test for Magento\Catalog\Model\Product\Copier class. -+ * - * @SuppressWarnings(PHPMD.CouplingBetweenObjects) - */ - class CopierTest extends \PHPUnit\Framework\TestCase -@@ -54,7 +57,7 @@ class CopierTest extends \PHPUnit\Framework\TestCase - \Magento\Catalog\Model\Product\Option\Repository::class - ); - $this->optionRepositoryMock; -- $this->productMock = $this->createMock(\Magento\Catalog\Model\Product::class); -+ $this->productMock = $this->createMock(Product::class); - $this->productMock->expects($this->any())->method('getEntityId')->willReturn(1); - - $this->metadata = $this->getMockBuilder(\Magento\Framework\EntityManager\EntityMetadata::class) -@@ -75,6 +78,9 @@ class CopierTest extends \PHPUnit\Framework\TestCase - ]); - } - -+ /** -+ * @SuppressWarnings(PHPMD.ExcessiveMethodLength) -+ */ - public function testCopy() - { - $stockItem = $this->getMockBuilder(\Magento\CatalogInventory\Api\Data\StockItemInterface::class) -@@ -102,11 +108,47 @@ class CopierTest extends \PHPUnit\Framework\TestCase - ['linkField', null, '1'], - ]); - -- $resourceMock = $this->createMock(\Magento\Catalog\Model\ResourceModel\Product::class); -- $this->productMock->expects($this->once())->method('getResource')->will($this->returnValue($resourceMock)); -+ $entityMock = $this->getMockForAbstractClass( -+ \Magento\Eav\Model\Entity\AbstractEntity::class, -+ [], -+ '', -+ false, -+ true, -+ true, -+ ['checkAttributeUniqueValue'] -+ ); -+ $entityMock->expects($this->any()) -+ ->method('checkAttributeUniqueValue') -+ ->willReturn(true); -+ -+ $attributeMock = $this->getMockForAbstractClass( -+ \Magento\Eav\Model\Entity\Attribute\AbstractAttribute::class, -+ [], -+ '', -+ false, -+ true, -+ true, -+ ['getEntity'] -+ ); -+ $attributeMock->expects($this->any()) -+ ->method('getEntity') -+ ->willReturn($entityMock); -+ -+ $resourceMock = $this->getMockBuilder(\Magento\Catalog\Model\ResourceModel\Product::class) -+ ->disableOriginalConstructor() -+ ->setMethods(['getAttributeRawValue', 'duplicate', 'getAttribute']) -+ ->getMock(); -+ $resourceMock->expects($this->any()) -+ ->method('getAttributeRawValue') -+ ->willReturn('urk-key-1'); -+ $resourceMock->expects($this->any()) -+ ->method('getAttribute') -+ ->willReturn($attributeMock); -+ -+ $this->productMock->expects($this->any())->method('getResource')->will($this->returnValue($resourceMock)); - - $duplicateMock = $this->createPartialMock( -- \Magento\Catalog\Model\Product::class, -+ Product::class, - [ - '__wakeup', - 'setData', -@@ -118,11 +160,11 @@ class CopierTest extends \PHPUnit\Framework\TestCase - 'setCreatedAt', - 'setUpdatedAt', - 'setId', -- 'setStoreId', - 'getEntityId', - 'save', - 'setUrlKey', -- 'getUrlKey', -+ 'setStoreId', -+ 'getStoreIds', - ] - ); - $this->productFactoryMock->expects($this->once())->method('create')->will($this->returnValue($duplicateMock)); -@@ -137,27 +179,22 @@ class CopierTest extends \PHPUnit\Framework\TestCase - )->with( - \Magento\Catalog\Model\Product\Attribute\Source\Status::STATUS_DISABLED - ); -+ $duplicateMock->expects($this->atLeastOnce())->method('setStoreId'); - $duplicateMock->expects($this->once())->method('setCreatedAt')->with(null); - $duplicateMock->expects($this->once())->method('setUpdatedAt')->with(null); - $duplicateMock->expects($this->once())->method('setId')->with(null); -- $duplicateMock->expects( -- $this->once() -- )->method( -- 'setStoreId' -- )->with( -- \Magento\Store\Model\Store::DEFAULT_STORE_ID -- ); -- $duplicateMock->expects($this->once())->method('setData')->with($productData); -+ $duplicateMock->expects($this->atLeastOnce())->method('getStoreIds')->willReturn([]); -+ $duplicateMock->expects($this->atLeastOnce())->method('setData')->willReturn($duplicateMock); - $this->copyConstructorMock->expects($this->once())->method('build')->with($this->productMock, $duplicateMock); -- $duplicateMock->expects($this->once())->method('getUrlKey')->willReturn('urk-key-1'); -- $duplicateMock->expects($this->once())->method('setUrlKey')->with('urk-key-2'); -+ $duplicateMock->expects($this->once())->method('setUrlKey')->with('urk-key-2')->willReturn($duplicateMock); - $duplicateMock->expects($this->once())->method('save'); - - $this->metadata->expects($this->any())->method('getLinkField')->willReturn('linkField'); - - $duplicateMock->expects($this->any())->method('getData')->willReturnMap([ - ['linkField', null, '2'], -- ]); $this->optionRepositoryMock->expects($this->once()) -+ ]); -+ $this->optionRepositoryMock->expects($this->once()) - ->method('duplicate') - ->with($this->productMock, $duplicateMock); - $resourceMock->expects($this->once())->method('duplicate')->with(1, 2); -diff --git a/app/code/Magento/Catalog/Test/Unit/Model/Product/Gallery/GalleryManagementTest.php b/app/code/Magento/Catalog/Test/Unit/Model/Product/Gallery/GalleryManagementTest.php -index 9fafbc9d967..1d12645019d 100644 ---- a/app/code/Magento/Catalog/Test/Unit/Model/Product/Gallery/GalleryManagementTest.php -+++ b/app/code/Magento/Catalog/Test/Unit/Model/Product/Gallery/GalleryManagementTest.php -@@ -266,7 +266,7 @@ class GalleryManagementTest extends \PHPUnit\Framework\TestCase - - /** - * @expectedException \Magento\Framework\Exception\NoSuchEntityException -- * @expectedExceptionText The image doesn't exist. Verify and try again. -+ * @expectedExceptionMessage The image doesn't exist. Verify and try again. - */ - public function testGetWithNonExistingImage() - { -diff --git a/app/code/Magento/Catalog/Test/Unit/Model/Product/Option/Validator/DefaultValidatorTest.php b/app/code/Magento/Catalog/Test/Unit/Model/Product/Option/Validator/DefaultValidatorTest.php -index da6b790fedf..7c2ec8abb76 100644 ---- a/app/code/Magento/Catalog/Test/Unit/Model/Product/Option/Validator/DefaultValidatorTest.php -+++ b/app/code/Magento/Catalog/Test/Unit/Model/Product/Option/Validator/DefaultValidatorTest.php -@@ -18,6 +18,11 @@ class DefaultValidatorTest extends \PHPUnit\Framework\TestCase - */ - protected $valueMock; - -+ /** -+ * @var \PHPUnit_Framework_MockObject_MockObject -+ */ -+ protected $localeFormatMock; -+ - /** - * @inheritdoc - */ -@@ -26,6 +31,8 @@ class DefaultValidatorTest extends \PHPUnit\Framework\TestCase - $configMock = $this->createMock(\Magento\Catalog\Model\ProductOptions\ConfigInterface::class); - $storeManagerMock = $this->createMock(\Magento\Store\Model\StoreManagerInterface::class); - $priceConfigMock = new \Magento\Catalog\Model\Config\Source\Product\Options\Price($storeManagerMock); -+ $this->localeFormatMock = $this->createMock(\Magento\Framework\Locale\FormatInterface::class); -+ - $config = [ - [ - 'label' => 'group label 1', -@@ -51,7 +58,8 @@ class DefaultValidatorTest extends \PHPUnit\Framework\TestCase - $configMock->expects($this->once())->method('getAll')->will($this->returnValue($config)); - $this->validator = new \Magento\Catalog\Model\Product\Option\Validator\DefaultValidator( - $configMock, -- $priceConfigMock -+ $priceConfigMock, -+ $this->localeFormatMock - ); - } - -@@ -63,10 +71,10 @@ class DefaultValidatorTest extends \PHPUnit\Framework\TestCase - { - $mess = ['option required fields' => 'Missed values for option required fields']; - return [ -- ['option_title', 'name 1.1', 'fixed', new \Magento\Framework\DataObject(['store_id' => 1]), [], true], -- ['option_title', 'name 1.1', 'fixed', new \Magento\Framework\DataObject(['store_id' => 0]), [], true], -- [null, 'name 1.1', 'fixed', new \Magento\Framework\DataObject(['store_id' => 1]), [], true], -- [null, 'name 1.1', 'fixed', new \Magento\Framework\DataObject(['store_id' => 0]), $mess, false], -+ ['option_title', 'name 1.1', 'fixed', 10, new \Magento\Framework\DataObject(['store_id' => 1]), [], true], -+ ['option_title', 'name 1.1', 'fixed', 10, new \Magento\Framework\DataObject(['store_id' => 0]), [], true], -+ [null, 'name 1.1', 'fixed', 10, new \Magento\Framework\DataObject(['store_id' => 1]), [], true], -+ [null, 'name 1.1', 'fixed', 10, new \Magento\Framework\DataObject(['store_id' => 0]), $mess, false], - ]; - } - -@@ -79,15 +87,18 @@ class DefaultValidatorTest extends \PHPUnit\Framework\TestCase - * @param bool $result - * @dataProvider isValidTitleDataProvider - */ -- public function testIsValidTitle($title, $type, $priceType, $product, $messages, $result) -+ public function testIsValidTitle($title, $type, $priceType, $price, $product, $messages, $result) - { -- $methods = ['getTitle', 'getType', 'getPriceType', '__wakeup', 'getProduct']; -+ $methods = ['getTitle', 'getType', 'getPriceType', 'getPrice', '__wakeup', 'getProduct']; - $valueMock = $this->createPartialMock(\Magento\Catalog\Model\Product\Option::class, $methods); - $valueMock->expects($this->once())->method('getTitle')->will($this->returnValue($title)); - $valueMock->expects($this->any())->method('getType')->will($this->returnValue($type)); - $valueMock->expects($this->once())->method('getPriceType')->will($this->returnValue($priceType)); -- // $valueMock->expects($this->once())->method('getPrice')->will($this->returnValue($price)); -+ $valueMock->expects($this->once())->method('getPrice')->will($this->returnValue($price)); - $valueMock->expects($this->once())->method('getProduct')->will($this->returnValue($product)); -+ -+ $this->localeFormatMock->expects($this->once())->method('getNumber')->will($this->returnValue($price)); -+ - $this->assertEquals($result, $this->validator->isValid($valueMock)); - $this->assertEquals($messages, $this->validator->getMessages()); - } -@@ -126,4 +137,43 @@ class DefaultValidatorTest extends \PHPUnit\Framework\TestCase - $this->assertFalse($this->validator->isValid($valueMock)); - $this->assertEquals($messages, $this->validator->getMessages()); - } -+ -+ /** -+ * Data provider for testValidationNegativePrice -+ * @return array -+ */ -+ public function validationPriceDataProvider() -+ { -+ return [ -+ ['option_title', 'name 1.1', 'fixed', -12, new \Magento\Framework\DataObject(['store_id' => 1])], -+ ['option_title', 'name 1.1', 'fixed', -12, new \Magento\Framework\DataObject(['store_id' => 0])], -+ ['option_title', 'name 1.1', 'fixed', 12, new \Magento\Framework\DataObject(['store_id' => 1])], -+ ['option_title', 'name 1.1', 'fixed', 12, new \Magento\Framework\DataObject(['store_id' => 0])] -+ ]; -+ } -+ -+ /** -+ * @param $title -+ * @param $type -+ * @param $priceType -+ * @param $price -+ * @param $product -+ * @dataProvider validationPriceDataProvider -+ */ -+ public function testValidationPrice($title, $type, $priceType, $price, $product) -+ { -+ $methods = ['getTitle', 'getType', 'getPriceType', 'getPrice', '__wakeup', 'getProduct']; -+ $valueMock = $this->createPartialMock(\Magento\Catalog\Model\Product\Option::class, $methods); -+ $valueMock->expects($this->once())->method('getTitle')->will($this->returnValue($title)); -+ $valueMock->expects($this->exactly(2))->method('getType')->will($this->returnValue($type)); -+ $valueMock->expects($this->once())->method('getPriceType')->will($this->returnValue($priceType)); -+ $valueMock->expects($this->once())->method('getPrice')->will($this->returnValue($price)); -+ $valueMock->expects($this->once())->method('getProduct')->will($this->returnValue($product)); -+ -+ $this->localeFormatMock->expects($this->once())->method('getNumber')->will($this->returnValue($price)); -+ -+ $messages = []; -+ $this->assertTrue($this->validator->isValid($valueMock)); -+ $this->assertEquals($messages, $this->validator->getMessages()); -+ } - } -diff --git a/app/code/Magento/Catalog/Test/Unit/Model/Product/Option/Validator/FileTest.php b/app/code/Magento/Catalog/Test/Unit/Model/Product/Option/Validator/FileTest.php -index 2de993c0755..e688da1c6aa 100644 ---- a/app/code/Magento/Catalog/Test/Unit/Model/Product/Option/Validator/FileTest.php -+++ b/app/code/Magento/Catalog/Test/Unit/Model/Product/Option/Validator/FileTest.php -@@ -18,6 +18,11 @@ class FileTest extends \PHPUnit\Framework\TestCase - */ - protected $valueMock; - -+ /** -+ * @var \PHPUnit_Framework_MockObject_MockObject -+ */ -+ protected $localeFormatMock; -+ - /** - * @inheritdoc - */ -@@ -26,6 +31,8 @@ class FileTest extends \PHPUnit\Framework\TestCase - $configMock = $this->createMock(\Magento\Catalog\Model\ProductOptions\ConfigInterface::class); - $storeManagerMock = $this->createMock(\Magento\Store\Model\StoreManagerInterface::class); - $priceConfigMock = new \Magento\Catalog\Model\Config\Source\Product\Options\Price($storeManagerMock); -+ $this->localeFormatMock = $this->createMock(\Magento\Framework\Locale\FormatInterface::class); -+ - $config = [ - [ - 'label' => 'group label 1', -@@ -53,7 +60,8 @@ class FileTest extends \PHPUnit\Framework\TestCase - $this->valueMock = $this->createPartialMock(\Magento\Catalog\Model\Product\Option::class, $methods); - $this->validator = new \Magento\Catalog\Model\Product\Option\Validator\File( - $configMock, -- $priceConfigMock -+ $priceConfigMock, -+ $this->localeFormatMock - ); - } - -@@ -70,6 +78,15 @@ class FileTest extends \PHPUnit\Framework\TestCase - ->willReturn(10); - $this->valueMock->expects($this->once())->method('getImageSizeX')->will($this->returnValue(10)); - $this->valueMock->expects($this->once())->method('getImageSizeY')->will($this->returnValue(15)); -+ $this->localeFormatMock->expects($this->at(0)) -+ ->method('getNumber') -+ ->with($this->equalTo(10)) -+ ->will($this->returnValue(10)); -+ $this->localeFormatMock -+ ->expects($this->at(2)) -+ ->method('getNumber') -+ ->with($this->equalTo(15)) -+ ->will($this->returnValue(15)); - $this->assertEmpty($this->validator->getMessages()); - $this->assertTrue($this->validator->isValid($this->valueMock)); - } -@@ -87,6 +104,16 @@ class FileTest extends \PHPUnit\Framework\TestCase - ->willReturn(10); - $this->valueMock->expects($this->once())->method('getImageSizeX')->will($this->returnValue(-10)); - $this->valueMock->expects($this->never())->method('getImageSizeY'); -+ $this->localeFormatMock->expects($this->at(0)) -+ ->method('getNumber') -+ ->with($this->equalTo(10)) -+ ->will($this->returnValue(10)); -+ $this->localeFormatMock -+ ->expects($this->at(1)) -+ ->method('getNumber') -+ ->with($this->equalTo(-10)) -+ ->will($this->returnValue(-10)); -+ - $messages = [ - 'option values' => 'Invalid option value', - ]; -@@ -107,6 +134,15 @@ class FileTest extends \PHPUnit\Framework\TestCase - ->willReturn(10); - $this->valueMock->expects($this->once())->method('getImageSizeX')->will($this->returnValue(10)); - $this->valueMock->expects($this->once())->method('getImageSizeY')->will($this->returnValue(-10)); -+ $this->localeFormatMock->expects($this->at(0)) -+ ->method('getNumber') -+ ->with($this->equalTo(10)) -+ ->will($this->returnValue(10)); -+ $this->localeFormatMock -+ ->expects($this->at(2)) -+ ->method('getNumber') -+ ->with($this->equalTo(-10)) -+ ->will($this->returnValue(-10)); - $messages = [ - 'option values' => 'Invalid option value', - ]; -diff --git a/app/code/Magento/Catalog/Test/Unit/Model/Product/Option/Validator/SelectTest.php b/app/code/Magento/Catalog/Test/Unit/Model/Product/Option/Validator/SelectTest.php -index b97783edf85..7fad5592a2d 100644 ---- a/app/code/Magento/Catalog/Test/Unit/Model/Product/Option/Validator/SelectTest.php -+++ b/app/code/Magento/Catalog/Test/Unit/Model/Product/Option/Validator/SelectTest.php -@@ -18,6 +18,11 @@ class SelectTest extends \PHPUnit\Framework\TestCase - */ - protected $valueMock; - -+ /** -+ * @var \PHPUnit_Framework_MockObject_MockObject -+ */ -+ protected $localeFormatMock; -+ - /** - * @inheritdoc - */ -@@ -26,6 +31,7 @@ class SelectTest extends \PHPUnit\Framework\TestCase - $configMock = $this->createMock(\Magento\Catalog\Model\ProductOptions\ConfigInterface::class); - $storeManagerMock = $this->createMock(\Magento\Store\Model\StoreManagerInterface::class); - $priceConfigMock = new \Magento\Catalog\Model\Config\Source\Product\Options\Price($storeManagerMock); -+ $this->localeFormatMock = $this->createMock(\Magento\Framework\Locale\FormatInterface::class); - $config = [ - [ - 'label' => 'group label 1', -@@ -53,7 +59,8 @@ class SelectTest extends \PHPUnit\Framework\TestCase - $this->valueMock = $this->createPartialMock(\Magento\Catalog\Model\Product\Option::class, $methods, []); - $this->validator = new \Magento\Catalog\Model\Product\Option\Validator\Select( - $configMock, -- $priceConfigMock -+ $priceConfigMock, -+ $this->localeFormatMock - ); - } - -@@ -69,6 +76,12 @@ class SelectTest extends \PHPUnit\Framework\TestCase - $this->valueMock->expects($this->never())->method('getPriceType'); - $this->valueMock->expects($this->never())->method('getPrice'); - $this->valueMock->expects($this->any())->method('getData')->with('values')->will($this->returnValue([$value])); -+ if (isset($value['price'])) { -+ $this->localeFormatMock -+ ->expects($this->once()) -+ ->method('getNumber') -+ ->will($this->returnValue($value['price'])); -+ } - $this->assertEquals($expectedResult, $this->validator->isValid($this->valueMock)); - } - -@@ -117,6 +130,7 @@ class SelectTest extends \PHPUnit\Framework\TestCase - ->method('getData') - ->with('values') - ->will($this->returnValue('invalid_data')); -+ - $messages = [ - 'option values' => 'Invalid option value', - ]; -@@ -159,6 +173,7 @@ class SelectTest extends \PHPUnit\Framework\TestCase - $this->valueMock->expects($this->never())->method('getPriceType'); - $this->valueMock->expects($this->never())->method('getPrice'); - $this->valueMock->expects($this->any())->method('getData')->with('values')->will($this->returnValue([$value])); -+ $this->localeFormatMock->expects($this->any())->method('getNumber')->will($this->returnValue($price)); - $messages = [ - 'option values' => 'Invalid option value', - ]; -diff --git a/app/code/Magento/Catalog/Test/Unit/Model/Product/Option/Validator/TextTest.php b/app/code/Magento/Catalog/Test/Unit/Model/Product/Option/Validator/TextTest.php -index 4881154728d..a3e6189f749 100644 ---- a/app/code/Magento/Catalog/Test/Unit/Model/Product/Option/Validator/TextTest.php -+++ b/app/code/Magento/Catalog/Test/Unit/Model/Product/Option/Validator/TextTest.php -@@ -18,6 +18,11 @@ class TextTest extends \PHPUnit\Framework\TestCase - */ - protected $valueMock; - -+ /** -+ * @var \PHPUnit_Framework_MockObject_MockObject -+ */ -+ protected $localeFormatMock; -+ - /** - * @inheritdoc - */ -@@ -26,6 +31,7 @@ class TextTest extends \PHPUnit\Framework\TestCase - $configMock = $this->createMock(\Magento\Catalog\Model\ProductOptions\ConfigInterface::class); - $storeManagerMock = $this->createMock(\Magento\Store\Model\StoreManagerInterface::class); - $priceConfigMock = new \Magento\Catalog\Model\Config\Source\Product\Options\Price($storeManagerMock); -+ $this->localeFormatMock = $this->createMock(\Magento\Framework\Locale\FormatInterface::class); - $config = [ - [ - 'label' => 'group label 1', -@@ -53,7 +59,8 @@ class TextTest extends \PHPUnit\Framework\TestCase - $this->valueMock = $this->createPartialMock(\Magento\Catalog\Model\Product\Option::class, $methods); - $this->validator = new \Magento\Catalog\Model\Product\Option\Validator\Text( - $configMock, -- $priceConfigMock -+ $priceConfigMock, -+ $this->localeFormatMock - ); - } - -@@ -69,6 +76,10 @@ class TextTest extends \PHPUnit\Framework\TestCase - $this->valueMock->method('getPrice') - ->willReturn(10); - $this->valueMock->expects($this->once())->method('getMaxCharacters')->will($this->returnValue(10)); -+ $this->localeFormatMock->expects($this->exactly(2)) -+ ->method('getNumber') -+ ->with($this->equalTo(10)) -+ ->will($this->returnValue(10)); - $this->assertTrue($this->validator->isValid($this->valueMock)); - $this->assertEmpty($this->validator->getMessages()); - } -@@ -85,6 +96,15 @@ class TextTest extends \PHPUnit\Framework\TestCase - $this->valueMock->method('getPrice') - ->willReturn(10); - $this->valueMock->expects($this->once())->method('getMaxCharacters')->will($this->returnValue(-10)); -+ $this->localeFormatMock->expects($this->at(0)) -+ ->method('getNumber') -+ ->with($this->equalTo(10)) -+ ->will($this->returnValue(10)); -+ $this->localeFormatMock -+ ->expects($this->at(1)) -+ ->method('getNumber') -+ ->with($this->equalTo(-10)) -+ ->will($this->returnValue(-10)); - $messages = [ - 'option values' => 'Invalid option value', - ]; -diff --git a/app/code/Magento/Catalog/Test/Unit/Model/Product/Price/TierPriceStorageTest.php b/app/code/Magento/Catalog/Test/Unit/Model/Product/Price/TierPriceStorageTest.php -index c9288790ed6..a97f2281125 100644 ---- a/app/code/Magento/Catalog/Test/Unit/Model/Product/Price/TierPriceStorageTest.php -+++ b/app/code/Magento/Catalog/Test/Unit/Model/Product/Price/TierPriceStorageTest.php -@@ -152,6 +152,30 @@ class TierPriceStorageTest extends \PHPUnit\Framework\TestCase - $this->assertEquals(2, count($prices)); - } - -+ /** -+ * Test get method without tierprices. -+ * -+ * @return void -+ */ -+ public function testGetWithoutTierPrices() -+ { -+ $skus = ['simple', 'virtual']; -+ $this->tierPriceValidator -+ ->expects($this->once()) -+ ->method('validateSkus') -+ ->with($skus) -+ ->willReturn($skus); -+ $this->productIdLocator->expects($this->atLeastOnce()) -+ ->method('retrieveProductIdsBySkus') -+ ->with(['simple', 'virtual']) -+ ->willReturn(['simple' => ['2' => 'simple'], 'virtual' => ['3' => 'virtual']]); -+ $this->tierPricePersistence->expects($this->once())->method('get')->willReturn([]); -+ $this->tierPricePersistence->expects($this->never())->method('getEntityLinkField'); -+ $this->tierPriceFactory->expects($this->never())->method('create'); -+ $prices = $this->tierPriceStorage->get($skus); -+ $this->assertEmpty($prices); -+ } -+ - /** - * Test update method. - * -diff --git a/app/code/Magento/Catalog/Test/Unit/Model/Product/PriceModifierTest.php b/app/code/Magento/Catalog/Test/Unit/Model/Product/PriceModifierTest.php -index 754d80302d4..6029a2b8200 100644 ---- a/app/code/Magento/Catalog/Test/Unit/Model/Product/PriceModifierTest.php -+++ b/app/code/Magento/Catalog/Test/Unit/Model/Product/PriceModifierTest.php -@@ -54,7 +54,7 @@ class PriceModifierTest extends \PHPUnit\Framework\TestCase - - /** - * @expectedException \Magento\Framework\Exception\NoSuchEntityException -- * @expectedMessage Tier price is unavailable for this product. -+ * @expectedExceptionMessage Product hasn't group price with such data: customerGroupId = '1', website = 1, qty = 3 - */ - public function testRemoveWhenTierPricesNotExists() - { -@@ -70,7 +70,7 @@ class PriceModifierTest extends \PHPUnit\Framework\TestCase - - /** - * @expectedException \Magento\Framework\Exception\NoSuchEntityException -- * @expectedMessage For current customerGroupId = '10' with 'qty' = 15 any tier price exist'. -+ * @expectedExceptionMessage Product hasn't group price with such data: customerGroupId = '10', website = 1, qty = 5 - */ - public function testRemoveTierPriceForNonExistingCustomerGroup() - { -@@ -81,7 +81,7 @@ class PriceModifierTest extends \PHPUnit\Framework\TestCase - ->will($this->returnValue($this->prices)); - $this->productMock->expects($this->never())->method('setData'); - $this->productRepositoryMock->expects($this->never())->method('save'); -- $this->priceModifier->removeTierPrice($this->productMock, 10, 15, 1); -+ $this->priceModifier->removeTierPrice($this->productMock, 10, 5, 1); - } - - public function testSuccessfullyRemoveTierPriceSpecifiedForAllGroups() -diff --git a/app/code/Magento/Catalog/Test/Unit/Model/Product/ProductFrontendAction/SynchronizerTest.php b/app/code/Magento/Catalog/Test/Unit/Model/Product/ProductFrontendAction/SynchronizerTest.php -index fce4a02622d..38bed83cb95 100644 ---- a/app/code/Magento/Catalog/Test/Unit/Model/Product/ProductFrontendAction/SynchronizerTest.php -+++ b/app/code/Magento/Catalog/Test/Unit/Model/Product/ProductFrontendAction/SynchronizerTest.php -@@ -80,6 +80,7 @@ class SynchronizerTest extends \PHPUnit\Framework\TestCase - - public function testFilterProductActions() - { -+ $typeId = 'recently_compared_product'; - $productsData = [ - 1 => [ - 'added_at' => 12, -@@ -87,7 +88,7 @@ class SynchronizerTest extends \PHPUnit\Framework\TestCase - ], - 2 => [ - 'added_at' => 13, -- 'product_id' => 2, -+ 'product_id' => '2', - ], - 3 => [ - 'added_at' => 14, -@@ -126,10 +127,12 @@ class SynchronizerTest extends \PHPUnit\Framework\TestCase - $collection->expects($this->once()) - ->method('addFilterByUserIdentities') - ->with(1, 34); -- $collection->expects($this->any()) -+ $collection->expects($this->at(1)) - ->method('addFieldToFilter') -- ->withConsecutive(['type_id'], ['product_id']); -- -+ ->with('type_id', $typeId); -+ $collection->expects($this->at(2)) -+ ->method('addFieldToFilter') -+ ->with('product_id', [1, 2]); - $iterator = new \IteratorIterator(new \ArrayIterator([$frontendAction])); - $collection->expects($this->once()) - ->method('getIterator') -diff --git a/app/code/Magento/Catalog/Test/Unit/Model/Product/TierPriceManagementTest.php b/app/code/Magento/Catalog/Test/Unit/Model/Product/TierPriceManagementTest.php -index f340d0b204b..ae479a9b34d 100644 ---- a/app/code/Magento/Catalog/Test/Unit/Model/Product/TierPriceManagementTest.php -+++ b/app/code/Magento/Catalog/Test/Unit/Model/Product/TierPriceManagementTest.php -@@ -195,7 +195,7 @@ class TierPriceManagementTest extends \PHPUnit\Framework\TestCase - - /** - * @expectedException \Magento\Framework\Exception\NoSuchEntityException -- * @message The product doesn't exist. Verify and try again. -+ * @expectedExceptionMessage No such entity. - */ - public function testDeleteTierPriceFromNonExistingProduct() - { -diff --git a/app/code/Magento/Catalog/Test/Unit/Model/ProductIdLocatorTest.php b/app/code/Magento/Catalog/Test/Unit/Model/ProductIdLocatorTest.php -index b730e12ca82..b9cb82274c8 100644 ---- a/app/code/Magento/Catalog/Test/Unit/Model/ProductIdLocatorTest.php -+++ b/app/code/Magento/Catalog/Test/Unit/Model/ProductIdLocatorTest.php -@@ -58,7 +58,16 @@ class ProductIdLocatorTest extends \PHPUnit\Framework\TestCase - { - $skus = ['sku_1', 'sku_2']; - $collection = $this->getMockBuilder(\Magento\Catalog\Model\ResourceModel\Product\Collection::class) -- ->setMethods(['getIterator', 'addFieldToFilter']) -+ ->setMethods( -+ [ -+ 'getItems', -+ 'addFieldToFilter', -+ 'setPageSize', -+ 'getLastPageNumber', -+ 'setCurPage', -+ 'clear' -+ ] -+ ) - ->disableOriginalConstructor()->getMock(); - $product = $this->getMockBuilder(\Magento\Catalog\Api\Data\ProductInterface::class) - ->setMethods(['getSku', 'getData', 'getTypeId']) -@@ -69,7 +78,11 @@ class ProductIdLocatorTest extends \PHPUnit\Framework\TestCase - $this->collectionFactory->expects($this->once())->method('create')->willReturn($collection); - $collection->expects($this->once())->method('addFieldToFilter') - ->with(\Magento\Catalog\Api\Data\ProductInterface::SKU, ['in' => $skus])->willReturnSelf(); -- $collection->expects($this->once())->method('getIterator')->willReturn(new \ArrayIterator([$product])); -+ $collection->expects($this->atLeastOnce())->method('getItems')->willReturn([$product]); -+ $collection->expects($this->atLeastOnce())->method('setPageSize')->willReturnSelf(); -+ $collection->expects($this->atLeastOnce())->method('getLastPageNumber')->willReturn(1); -+ $collection->expects($this->atLeastOnce())->method('setCurPage')->with(1)->willReturnSelf(); -+ $collection->expects($this->atLeastOnce())->method('clear')->willReturnSelf(); - $this->metadataPool - ->expects($this->once()) - ->method('getMetadata') -diff --git a/app/code/Magento/Catalog/Test/Unit/Model/ProductOptions/Config/_files/invalidProductOptionsXmlArray.php b/app/code/Magento/Catalog/Test/Unit/Model/ProductOptions/Config/_files/invalidProductOptionsXmlArray.php -index 034b04b6a75..cfb54c3aefd 100644 ---- a/app/code/Magento/Catalog/Test/Unit/Model/ProductOptions/Config/_files/invalidProductOptionsXmlArray.php -+++ b/app/code/Magento/Catalog/Test/Unit/Model/ProductOptions/Config/_files/invalidProductOptionsXmlArray.php -@@ -29,12 +29,12 @@ return [ - ], - ], - 'renderer_attribute_with_invalid_value' => [ -- '<?xml version="1.0"?><config><option name="name_one" renderer="true12"><inputType name="name_one"/>' . -+ '<?xml version="1.0"?><config><option name="name_one" renderer="123true"><inputType name="name_one"/>' . - '</option></config>', - [ -- "Element 'option', attribute 'renderer': [facet 'pattern'] The value 'true12' is not accepted by the " . -- "pattern '[a-zA-Z_\\\\]+'.\nLine: 1\n", -- "Element 'option', attribute 'renderer': 'true12' is not a valid value of the atomic" . -+ "Element 'option', attribute 'renderer': [facet 'pattern'] The value '123true' is not accepted by the " . -+ "pattern '([\\\\]?[a-zA-Z_][a-zA-Z0-9_]*)+'.\nLine: 1\n", -+ "Element 'option', attribute 'renderer': '123true' is not a valid value of the atomic" . - " type 'modelName'.\nLine: 1\n" - ], - ], -diff --git a/app/code/Magento/Catalog/Test/Unit/Model/ProductRepositoryTest.php b/app/code/Magento/Catalog/Test/Unit/Model/ProductRepositoryTest.php -index 3fc3587637d..0dc294e139d 100644 ---- a/app/code/Magento/Catalog/Test/Unit/Model/ProductRepositoryTest.php -+++ b/app/code/Magento/Catalog/Test/Unit/Model/ProductRepositoryTest.php -@@ -1,19 +1,49 @@ - <?php - /** -- * - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -+declare(strict_types=1); - - namespace Magento\Catalog\Test\Unit\Model; - --use Magento\Catalog\Api\Data\ProductAttributeInterface; -+use Magento\Catalog\Api\Data\ProductExtensionInterface; -+use Magento\Catalog\Api\Data\ProductSearchResultsInterface; -+use Magento\Catalog\Api\Data\ProductSearchResultsInterfaceFactory; -+use Magento\Catalog\Api\ProductAttributeRepositoryInterface; -+use Magento\Catalog\Controller\Adminhtml\Product\Initialization\Helper; -+use Magento\Catalog\Model\Product; -+use Magento\Catalog\Model\Product\Gallery\MimeTypeExtensionMap; -+use Magento\Catalog\Model\Product\Gallery\Processor; -+use Magento\Catalog\Model\Product\LinkTypeProvider; -+use Magento\Catalog\Model\Product\Media\Config; -+use Magento\Catalog\Model\Product\Option; -+use Magento\Catalog\Model\Product\Option\Value; -+use Magento\Catalog\Model\ProductFactory; -+use Magento\Catalog\Model\ProductLink\Link; -+use Magento\Catalog\Model\ProductRepository; -+use Magento\Catalog\Model\ResourceModel\Product\Collection; -+use Magento\Catalog\Model\ResourceModel\Product\CollectionFactory; - use Magento\Framework\Api\Data\ImageContentInterface; -+use Magento\Framework\Api\Data\ImageContentInterfaceFactory; -+use Magento\Framework\Api\ExtensibleDataObjectConverter; -+use Magento\Framework\Api\FilterBuilder; -+use Magento\Framework\Api\ImageContent; -+use Magento\Framework\Api\ImageContentValidator; -+use Magento\Framework\Api\ImageContentValidatorInterface; -+use Magento\Framework\Api\ImageProcessorInterface; - use Magento\Framework\Api\SearchCriteria\CollectionProcessorInterface; -+use Magento\Framework\Api\SearchCriteriaBuilder; -+use Magento\Framework\Api\SearchCriteriaInterface; - use Magento\Framework\DB\Adapter\ConnectionException; -+use Magento\Framework\Filesystem; - use Magento\Framework\Serialize\Serializer\Json; - use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; - use Magento\Store\Api\Data\StoreInterface; -+use Magento\Store\Model\Store; -+use Magento\Store\Model\StoreManagerInterface; -+use PHPUnit\Framework\TestCase; -+use PHPUnit_Framework_MockObject_MockObject as MockObject; - - /** - * Class ProductRepositoryTest -@@ -22,133 +52,133 @@ use Magento\Store\Api\Data\StoreInterface; - * @SuppressWarnings(PHPMD.CouplingBetweenObjects) - * @SuppressWarnings(PHPMD.ExcessiveMethodLength) - */ --class ProductRepositoryTest extends \PHPUnit\Framework\TestCase -+class ProductRepositoryTest extends TestCase - { - /** -- * @var \PHPUnit_Framework_MockObject_MockObject -+ * @var Product|MockObject - */ -- protected $productMock; -+ private $product; - - /** -- * @var \PHPUnit_Framework_MockObject_MockObject -+ * @var Product|MockObject - */ -- protected $initializedProductMock; -+ private $initializedProduct; - - /** -- * @var \Magento\Catalog\Model\ProductRepository -+ * @var ProductRepository - */ -- protected $model; -+ private $model; - - /** -- * @var \PHPUnit_Framework_MockObject_MockObject -+ * @var Helper|MockObject - */ -- protected $initializationHelperMock; -+ private $initializationHelper; - - /** -- * @var \PHPUnit_Framework_MockObject_MockObject -+ * @var Product|MockObject - */ -- protected $resourceModelMock; -+ private $resourceModel; - - /** -- * @var \PHPUnit_Framework_MockObject_MockObject -+ * @var ProductFactory|MockObject - */ -- protected $productFactoryMock; -+ private $productFactory; - - /** -- * @var \PHPUnit_Framework_MockObject_MockObject -+ * @var CollectionFactory|MockObject - */ -- protected $collectionFactoryMock; -+ private $collectionFactory; - - /** -- * @var \PHPUnit_Framework_MockObject_MockObject -+ * @var SearchCriteriaBuilder|MockObject - */ -- protected $searchCriteriaBuilderMock; -+ private $searchCriteriaBuilder; - - /** -- * @var \PHPUnit_Framework_MockObject_MockObject -+ * @var FilterBuilder|MockObject - */ -- protected $filterBuilderMock; -+ private $filterBuilder; - - /** -- * @var \PHPUnit_Framework_MockObject_MockObject -+ * @var ProductAttributeRepositoryInterface|MockObject - */ -- protected $metadataServiceMock; -+ private $metadataService; - - /** -- * @var \PHPUnit_Framework_MockObject_MockObject -+ * @var ProductSearchResultsInterfaceFactory|MockObject - */ -- protected $searchResultsFactoryMock; -+ private $searchResultsFactory; - - /** -- * @var \Magento\Eav\Model\Config|\PHPUnit_Framework_MockObject_MockObject -+ * @var ExtensibleDataObjectConverter|MockObject - */ -- protected $eavConfigMock; -- -- /** -- * @var \PHPUnit_Framework_MockObject_MockObject -- */ -- protected $extensibleDataObjectConverterMock; -+ private $extensibleDataObjectConverter; - - /** - * @var array data to create product - */ -- protected $productData = [ -+ private $productData = [ - 'sku' => 'exisiting', - 'name' => 'existing product', - ]; - - /** -- * @var \PHPUnit_Framework_MockObject_MockObject|\Magento\Framework\Filesystem -+ * @var Filesystem|MockObject -+ */ -+ private $fileSystem; -+ -+ /** -+ * @var MimeTypeExtensionMap|MockObject - */ -- protected $fileSystemMock; -+ private $mimeTypeExtensionMap; - - /** -- * @var \PHPUnit_Framework_MockObject_MockObject|\Magento\Catalog\Model\Product\Gallery\MimeTypeExtensionMap -+ * @var ImageContentInterfaceFactory|MockObject - */ -- protected $mimeTypeExtensionMapMock; -+ private $contentFactory; - - /** -- * @var \PHPUnit_Framework_MockObject_MockObject -+ * @var ImageContentValidator|MockObject - */ -- protected $contentFactoryMock; -+ private $contentValidator; - - /** -- * @var \PHPUnit_Framework_MockObject_MockObject|\Magento\Framework\Api\ImageContentValidator -+ * @var LinkTypeProvider|MockObject - */ -- protected $contentValidatorMock; -+ private $linkTypeProvider; - - /** -- * @var \PHPUnit_Framework_MockObject_MockObject -+ * @var ImageProcessorInterface|MockObject - */ -- protected $linkTypeProviderMock; -+ private $imageProcessor; - - /** -- * @var \PHPUnit_Framework_MockObject_MockObject|\Magento\Framework\Api\ImageProcessorInterface -+ * @var ObjectManager - */ -- protected $imageProcessorMock; -+ private $objectManager; - - /** -- * @var \Magento\Framework\TestFramework\Unit\Helper\ObjectManager -+ * @var StoreManagerInterface|MockObject - */ -- protected $objectManager; -+ private $storeManager; - - /** -- * @var \Magento\Store\Model\StoreManagerInterface|\PHPUnit_Framework_MockObject_MockObject -+ * @var Processor|MockObject - */ -- protected $storeManagerMock; -+ private $processor; - - /** -- * @var \Magento\Catalog\Model\Product\Gallery\Processor|\PHPUnit_Framework_MockObject_MockObject -+ * @var CollectionProcessorInterface|MockObject - */ -- protected $mediaGalleryProcessor; -+ private $collectionProcessor; - - /** -- * @var CollectionProcessorInterface|\PHPUnit_Framework_MockObject_MockObject -+ * @var ProductExtensionInterface|MockObject - */ -- private $collectionProcessorMock; -+ private $productExtension; - - /** -- * @var Json|\PHPUnit_Framework_MockObject_MockObject -+ * @var Json|MockObject - */ - private $serializerMock; - -@@ -164,13 +194,13 @@ class ProductRepositoryTest extends \PHPUnit\Framework\TestCase - */ - protected function setUp() - { -- $this->productFactoryMock = $this->createPartialMock( -- \Magento\Catalog\Model\ProductFactory::class, -+ $this->productFactory = $this->createPartialMock( -+ ProductFactory::class, - ['create', 'setData'] - ); - -- $this->productMock = $this->createPartialMock( -- \Magento\Catalog\Model\Product::class, -+ $this->product = $this->createPartialMock( -+ Product::class, - [ - 'getId', - 'getSku', -@@ -179,12 +209,14 @@ class ProductRepositoryTest extends \PHPUnit\Framework\TestCase - 'load', - 'setData', - 'getStoreId', -- 'getMediaGalleryEntries' -+ 'getMediaGalleryEntries', -+ 'getExtensionAttributes', -+ 'getCategoryIds' - ] - ); - -- $this->initializedProductMock = $this->createPartialMock( -- \Magento\Catalog\Model\Product::class, -+ $this->initializedProduct = $this->createPartialMock( -+ Product::class, - [ - 'getWebsiteIds', - 'setProductOptions', -@@ -199,66 +231,73 @@ class ProductRepositoryTest extends \PHPUnit\Framework\TestCase - 'validate', - 'save', - 'getMediaGalleryEntries', -+ 'getExtensionAttributes', -+ 'getCategoryIds' - ] - ); -- $this->initializedProductMock->expects($this->any()) -+ $this->initializedProduct->expects($this->any()) - ->method('hasGalleryAttribute') - ->willReturn(true); -- $this->filterBuilderMock = $this->createMock(\Magento\Framework\Api\FilterBuilder::class); -- $this->initializationHelperMock = $this->createMock( -- \Magento\Catalog\Controller\Adminhtml\Product\Initialization\Helper::class -- ); -- $this->collectionFactoryMock = $this->createPartialMock( -- \Magento\Catalog\Model\ResourceModel\Product\CollectionFactory::class, -- ['create'] -- ); -- $this->searchCriteriaBuilderMock = $this->createMock(\Magento\Framework\Api\SearchCriteriaBuilder::class); -- $this->metadataServiceMock = $this->createMock(\Magento\Catalog\Api\ProductAttributeRepositoryInterface::class); -- $this->searchResultsFactoryMock = $this->createPartialMock( -- \Magento\Catalog\Api\Data\ProductSearchResultsInterfaceFactory::class, -+ $this->filterBuilder = $this->createMock(FilterBuilder::class); -+ $this->initializationHelper = $this->createMock(Helper::class); -+ $this->collectionFactory = $this->createPartialMock(CollectionFactory::class, ['create']); -+ $this->searchCriteriaBuilder = $this->createMock(SearchCriteriaBuilder::class); -+ $this->metadataService = $this->createMock(ProductAttributeRepositoryInterface::class); -+ $this->searchResultsFactory = $this->createPartialMock( -+ ProductSearchResultsInterfaceFactory::class, - ['create'] - ); -- $this->resourceModelMock = $this->createMock(\Magento\Catalog\Model\ResourceModel\Product::class); -+ $this->resourceModel = $this->createMock(\Magento\Catalog\Model\ResourceModel\Product::class); - $this->objectManager = new ObjectManager($this); -- $this->extensibleDataObjectConverterMock = $this -- ->getMockBuilder(\Magento\Framework\Api\ExtensibleDataObjectConverter::class) -+ $this->extensibleDataObjectConverter = $this -+ ->getMockBuilder(ExtensibleDataObjectConverter::class) - ->setMethods(['toNestedArray']) - ->disableOriginalConstructor() - ->getMock(); -- $this->fileSystemMock = $this->getMockBuilder(\Magento\Framework\Filesystem::class) -+ $this->fileSystem = $this->getMockBuilder(Filesystem::class) - ->disableOriginalConstructor()->getMock(); -- $this->mimeTypeExtensionMapMock = -- $this->getMockBuilder(\Magento\Catalog\Model\Product\Gallery\MimeTypeExtensionMap::class)->getMock(); -- $this->contentFactoryMock = $this->createPartialMock( -- \Magento\Framework\Api\Data\ImageContentInterfaceFactory::class, -- ['create'] -- ); -- $this->contentValidatorMock = $this->getMockBuilder( -- \Magento\Framework\Api\ImageContentValidatorInterface::class -- ) -+ $this->mimeTypeExtensionMap = $this->getMockBuilder(MimeTypeExtensionMap::class)->getMock(); -+ $this->contentFactory = $this->createPartialMock(ImageContentInterfaceFactory::class, ['create']); -+ $this->contentValidator = $this->getMockBuilder(ImageContentValidatorInterface::class) - ->disableOriginalConstructor() - ->getMock(); -- $this->linkTypeProviderMock = $this->createPartialMock( -- \Magento\Catalog\Model\Product\LinkTypeProvider::class, -- ['getLinkTypes'] -- ); -- $this->imageProcessorMock = $this->createMock(\Magento\Framework\Api\ImageProcessorInterface::class); -+ $this->linkTypeProvider = $this->createPartialMock(LinkTypeProvider::class, ['getLinkTypes']); -+ $this->imageProcessor = $this->createMock(ImageProcessorInterface::class); - -- $this->storeManagerMock = $this->getMockBuilder(\Magento\Store\Model\StoreManagerInterface::class) -+ $this->storeManager = $this->getMockBuilder(StoreManagerInterface::class) - ->disableOriginalConstructor() - ->setMethods([]) - ->getMockForAbstractClass(); -- $storeMock = $this->getMockBuilder(\Magento\Store\Api\Data\StoreInterface::class) -+ $this->productExtension = $this->getMockBuilder(ProductExtensionInterface::class) -+ ->setMethods(['__toArray']) -+ ->disableOriginalConstructor() -+ ->getMockForAbstractClass(); -+ $this->productExtension -+ ->method('__toArray') -+ ->willReturn([]); -+ $this->product -+ ->method('getExtensionAttributes') -+ ->willReturn($this->productExtension); -+ $this->initializedProduct -+ ->method('getExtensionAttributes') -+ ->willReturn($this->productExtension); -+ $this->product -+ ->method('getCategoryIds') -+ ->willReturn([1, 2, 3, 4]); -+ $this->initializedProduct -+ ->method('getCategoryIds') -+ ->willReturn([1, 2, 3, 4]); -+ $storeMock = $this->getMockBuilder(StoreInterface::class) - ->disableOriginalConstructor() - ->setMethods([]) - ->getMockForAbstractClass(); - $storeMock->expects($this->any())->method('getWebsiteId')->willReturn('1'); -- $storeMock->expects($this->any())->method('getCode')->willReturn(\Magento\Store\Model\Store::ADMIN_CODE); -- $this->storeManagerMock->expects($this->any())->method('getStore')->willReturn($storeMock); -+ $storeMock->expects($this->any())->method('getCode')->willReturn(Store::ADMIN_CODE); -+ $this->storeManager->expects($this->any())->method('getStore')->willReturn($storeMock); - -- $this->mediaGalleryProcessor = $this->createMock(\Magento\Catalog\Model\Product\Gallery\Processor::class); -+ $this->processor = $this->createMock(Processor::class); - -- $this->collectionProcessorMock = $this->getMockBuilder(CollectionProcessorInterface::class) -+ $this->collectionProcessor = $this->getMockBuilder(CollectionProcessorInterface::class) - ->getMock(); - - $this->serializerMock = $this->getMockBuilder(Json::class)->getMock(); -@@ -272,31 +311,38 @@ class ProductRepositoryTest extends \PHPUnit\Framework\TestCase - ) - ); - -+ $mediaProcessor = $this->objectManager->getObject( -+ ProductRepository\MediaGalleryProcessor::class, -+ [ -+ 'processor' => $this->processor, -+ 'contentFactory' => $this->contentFactory, -+ 'imageProcessor' => $this->imageProcessor, -+ ] -+ ); - $this->model = $this->objectManager->getObject( -- \Magento\Catalog\Model\ProductRepository::class, -+ ProductRepository::class, - [ -- 'productFactory' => $this->productFactoryMock, -- 'initializationHelper' => $this->initializationHelperMock, -- 'resourceModel' => $this->resourceModelMock, -- 'filterBuilder' => $this->filterBuilderMock, -- 'collectionFactory' => $this->collectionFactoryMock, -- 'searchCriteriaBuilder' => $this->searchCriteriaBuilderMock, -- 'metadataServiceInterface' => $this->metadataServiceMock, -- 'searchResultsFactory' => $this->searchResultsFactoryMock, -- 'extensibleDataObjectConverter' => $this->extensibleDataObjectConverterMock, -- 'contentValidator' => $this->contentValidatorMock, -- 'fileSystem' => $this->fileSystemMock, -- 'contentFactory' => $this->contentFactoryMock, -- 'mimeTypeExtensionMap' => $this->mimeTypeExtensionMapMock, -- 'linkTypeProvider' => $this->linkTypeProviderMock, -- 'imageProcessor' => $this->imageProcessorMock, -- 'storeManager' => $this->storeManagerMock, -- 'mediaGalleryProcessor' => $this->mediaGalleryProcessor, -- 'collectionProcessor' => $this->collectionProcessorMock, -+ 'productFactory' => $this->productFactory, -+ 'initializationHelper' => $this->initializationHelper, -+ 'resourceModel' => $this->resourceModel, -+ 'filterBuilder' => $this->filterBuilder, -+ 'collectionFactory' => $this->collectionFactory, -+ 'searchCriteriaBuilder' => $this->searchCriteriaBuilder, -+ 'metadataServiceInterface' => $this->metadataService, -+ 'searchResultsFactory' => $this->searchResultsFactory, -+ 'extensibleDataObjectConverter' => $this->extensibleDataObjectConverter, -+ 'contentValidator' => $this->contentValidator, -+ 'fileSystem' => $this->fileSystem, -+ 'mimeTypeExtensionMap' => $this->mimeTypeExtensionMap, -+ 'linkTypeProvider' => $this->linkTypeProvider, -+ 'storeManager' => $this->storeManager, -+ 'mediaGalleryProcessor' => $this->processor, -+ 'collectionProcessor' => $this->collectionProcessor, - 'serializer' => $this->serializerMock, - 'cacheLimit' => $this->cacheLimit - ] - ); -+ $this->objectManager->setBackwardCompatibleProperty($this->model, 'mediaProcessor', $mediaProcessor); - } - - /** -@@ -305,50 +351,50 @@ class ProductRepositoryTest extends \PHPUnit\Framework\TestCase - */ - public function testGetAbsentProduct() - { -- $this->productFactoryMock->expects($this->once())->method('create') -- ->will($this->returnValue($this->productMock)); -- $this->resourceModelMock->expects($this->once())->method('getIdBySku')->with('test_sku') -+ $this->productFactory->expects($this->once())->method('create') -+ ->will($this->returnValue($this->product)); -+ $this->resourceModel->expects($this->once())->method('getIdBySku')->with('test_sku') - ->will($this->returnValue(null)); -- $this->productFactoryMock->expects($this->never())->method('setData'); -+ $this->productFactory->expects($this->never())->method('setData'); - $this->model->get('test_sku'); - } - - public function testCreateCreatesProduct() - { - $sku = 'test_sku'; -- $this->productFactoryMock->expects($this->once())->method('create') -- ->will($this->returnValue($this->productMock)); -- $this->resourceModelMock->expects($this->once())->method('getIdBySku')->with($sku) -+ $this->productFactory->expects($this->once())->method('create') -+ ->will($this->returnValue($this->product)); -+ $this->resourceModel->expects($this->once())->method('getIdBySku')->with($sku) - ->will($this->returnValue('test_id')); -- $this->productMock->expects($this->once())->method('load')->with('test_id'); -- $this->productMock->expects($this->once())->method('getSku')->willReturn($sku); -- $this->assertEquals($this->productMock, $this->model->get($sku)); -+ $this->product->expects($this->once())->method('load')->with('test_id'); -+ $this->product->expects($this->once())->method('getSku')->willReturn($sku); -+ $this->assertEquals($this->product, $this->model->get($sku)); - } - - public function testGetProductInEditMode() - { - $sku = 'test_sku'; -- $this->productFactoryMock->expects($this->once())->method('create') -- ->will($this->returnValue($this->productMock)); -- $this->resourceModelMock->expects($this->once())->method('getIdBySku')->with($sku) -+ $this->productFactory->expects($this->once())->method('create') -+ ->will($this->returnValue($this->product)); -+ $this->resourceModel->expects($this->once())->method('getIdBySku')->with($sku) - ->will($this->returnValue('test_id')); -- $this->productMock->expects($this->once())->method('setData')->with('_edit_mode', true); -- $this->productMock->expects($this->once())->method('load')->with('test_id'); -- $this->productMock->expects($this->once())->method('getSku')->willReturn($sku); -- $this->assertEquals($this->productMock, $this->model->get($sku, true)); -+ $this->product->expects($this->once())->method('setData')->with('_edit_mode', true); -+ $this->product->expects($this->once())->method('load')->with('test_id'); -+ $this->product->expects($this->once())->method('getSku')->willReturn($sku); -+ $this->assertEquals($this->product, $this->model->get($sku, true)); - } - - public function testGetBySkuWithSpace() - { - $trimmedSku = 'test_sku'; - $sku = 'test_sku '; -- $this->productFactoryMock->expects($this->once())->method('create') -- ->will($this->returnValue($this->productMock)); -- $this->resourceModelMock->expects($this->once())->method('getIdBySku')->with($sku) -+ $this->productFactory->expects($this->once())->method('create') -+ ->will($this->returnValue($this->product)); -+ $this->resourceModel->expects($this->once())->method('getIdBySku')->with($sku) - ->will($this->returnValue('test_id')); -- $this->productMock->expects($this->once())->method('load')->with('test_id'); -- $this->productMock->expects($this->once())->method('getSku')->willReturn($trimmedSku); -- $this->assertEquals($this->productMock, $this->model->get($sku)); -+ $this->product->expects($this->once())->method('load')->with('test_id'); -+ $this->product->expects($this->once())->method('getSku')->willReturn($trimmedSku); -+ $this->assertEquals($this->product, $this->model->get($sku)); - } - - public function testGetWithSetStoreId() -@@ -356,13 +402,13 @@ class ProductRepositoryTest extends \PHPUnit\Framework\TestCase - $productId = 123; - $sku = 'test-sku'; - $storeId = 7; -- $this->productFactoryMock->expects($this->once())->method('create')->willReturn($this->productMock); -- $this->resourceModelMock->expects($this->once())->method('getIdBySku')->with($sku)->willReturn($productId); -- $this->productMock->expects($this->once())->method('setData')->with('store_id', $storeId); -- $this->productMock->expects($this->once())->method('load')->with($productId); -- $this->productMock->expects($this->once())->method('getId')->willReturn($productId); -- $this->productMock->expects($this->once())->method('getSku')->willReturn($sku); -- $this->assertSame($this->productMock, $this->model->get($sku, false, $storeId)); -+ $this->productFactory->expects($this->once())->method('create')->willReturn($this->product); -+ $this->resourceModel->expects($this->once())->method('getIdBySku')->with($sku)->willReturn($productId); -+ $this->product->expects($this->once())->method('setData')->with('store_id', $storeId); -+ $this->product->expects($this->once())->method('load')->with($productId); -+ $this->product->expects($this->once())->method('getId')->willReturn($productId); -+ $this->product->expects($this->once())->method('getSku')->willReturn($sku); -+ $this->assertSame($this->product, $this->model->get($sku, false, $storeId)); - } - - /** -@@ -371,22 +417,22 @@ class ProductRepositoryTest extends \PHPUnit\Framework\TestCase - */ - public function testGetByIdAbsentProduct() - { -- $this->productFactoryMock->expects($this->once())->method('create') -- ->will($this->returnValue($this->productMock)); -- $this->productMock->expects($this->once())->method('load')->with('product_id'); -- $this->productMock->expects($this->once())->method('getId')->willReturn(null); -+ $this->productFactory->expects($this->once())->method('create') -+ ->will($this->returnValue($this->product)); -+ $this->product->expects($this->once())->method('load')->with('product_id'); -+ $this->product->expects($this->once())->method('getId')->willReturn(null); - $this->model->getById('product_id'); - } - - public function testGetByIdProductInEditMode() - { - $productId = 123; -- $this->productFactoryMock->method('create')->willReturn($this->productMock); -- $this->productMock->method('setData')->with('_edit_mode', true); -- $this->productMock->method('load')->with($productId); -- $this->productMock->expects($this->atLeastOnce())->method('getId')->willReturn($productId); -- $this->productMock->method('getSku')->willReturn('simple'); -- $this->assertEquals($this->productMock, $this->model->getById($productId, true)); -+ $this->productFactory->method('create')->willReturn($this->product); -+ $this->product->method('setData')->with('_edit_mode', true); -+ $this->product->method('load')->with($productId); -+ $this->product->expects($this->atLeastOnce())->method('getId')->willReturn($productId); -+ $this->product->method('getSku')->willReturn('simple'); -+ $this->assertEquals($this->product, $this->model->getById($productId, true)); - } - - /** -@@ -400,21 +446,21 @@ class ProductRepositoryTest extends \PHPUnit\Framework\TestCase - public function testGetByIdForCacheKeyGenerate($identifier, $editMode, $storeId) - { - $callIndex = 0; -- $this->productFactoryMock->expects($this->once())->method('create') -- ->will($this->returnValue($this->productMock)); -+ $this->productFactory->expects($this->once())->method('create') -+ ->will($this->returnValue($this->product)); - if ($editMode) { -- $this->productMock->expects($this->at($callIndex))->method('setData')->with('_edit_mode', $editMode); -+ $this->product->expects($this->at($callIndex))->method('setData')->with('_edit_mode', $editMode); - ++$callIndex; - } - if ($storeId !== null) { -- $this->productMock->expects($this->at($callIndex))->method('setData')->with('store_id', $storeId); -+ $this->product->expects($this->at($callIndex))->method('setData')->with('store_id', $storeId); - } -- $this->productMock->expects($this->once())->method('load')->with($identifier); -- $this->productMock->expects($this->atLeastOnce())->method('getId')->willReturn($identifier); -- $this->productMock->method('getSku')->willReturn('simple'); -- $this->assertEquals($this->productMock, $this->model->getById($identifier, $editMode, $storeId)); -+ $this->product->expects($this->once())->method('load')->with($identifier); -+ $this->product->expects($this->atLeastOnce())->method('getId')->willReturn($identifier); -+ $this->product->method('getSku')->willReturn('simple'); -+ $this->assertEquals($this->product, $this->model->getById($identifier, $editMode, $storeId)); - //Second invocation should just return from cache -- $this->assertEquals($this->productMock, $this->model->getById($identifier, $editMode, $storeId)); -+ $this->assertEquals($this->product, $this->model->getById($identifier, $editMode, $storeId)); - } - - /** -@@ -428,18 +474,18 @@ class ProductRepositoryTest extends \PHPUnit\Framework\TestCase - $editMode = false; - $storeId = 0; - -- $this->productFactoryMock->expects($this->exactly(2))->method('create') -- ->will($this->returnValue($this->productMock)); -- $this->productMock->expects($this->exactly(2))->method('load'); -+ $this->productFactory->expects($this->exactly(2))->method('create') -+ ->will($this->returnValue($this->product)); -+ $this->product->expects($this->exactly(2))->method('load'); - $this->serializerMock->expects($this->exactly(3))->method('serialize'); - -- $this->productMock->expects($this->exactly(4))->method('getId')->willReturn($identifier); -- $this->productMock->method('getSku')->willReturn('simple'); -- $this->assertEquals($this->productMock, $this->model->getById($identifier, $editMode, $storeId)); -+ $this->product->expects($this->exactly(4))->method('getId')->willReturn($identifier); -+ $this->product->method('getSku')->willReturn('simple'); -+ $this->assertEquals($this->product, $this->model->getById($identifier, $editMode, $storeId)); - //second invocation should just return from cache -- $this->assertEquals($this->productMock, $this->model->getById($identifier, $editMode, $storeId)); -+ $this->assertEquals($this->product, $this->model->getById($identifier, $editMode, $storeId)); - //force reload -- $this->assertEquals($this->productMock, $this->model->getById($identifier, $editMode, $storeId, true)); -+ $this->assertEquals($this->product, $this->model->getById($identifier, $editMode, $storeId, true)); - } - - /** -@@ -454,7 +500,7 @@ class ProductRepositoryTest extends \PHPUnit\Framework\TestCase - $productsCount = $this->cacheLimit * 2; - - $productMocks = $this->getProductMocksForReducedCache($productsCount); -- $productFactoryInvMock = $this->productFactoryMock->expects($this->exactly($productsCount)) -+ $productFactoryInvMock = $this->productFactory->expects($this->exactly($productsCount)) - ->method('create'); - call_user_func_array([$productFactoryInvMock, 'willReturnOnConsecutiveCalls'], $productMocks); - $this->serializerMock->expects($this->atLeastOnce())->method('serialize'); -@@ -479,14 +525,16 @@ class ProductRepositoryTest extends \PHPUnit\Framework\TestCase - $productMocks = []; - - for ($i = 1; $i <= $productsCount; $i++) { -- $productMock = $this->getMockBuilder(\Magento\Catalog\Model\Product::class) -+ $productMock = $this->getMockBuilder(Product::class) - ->disableOriginalConstructor() -- ->setMethods([ -+ ->setMethods( -+ [ - 'getId', - 'getSku', - 'load', - 'setData', -- ]) -+ ] -+ ) - ->getMock(); - $productMock->expects($this->once())->method('load'); - $productMock->expects($this->atLeastOnce())->method('getId')->willReturn($i); -@@ -509,86 +557,86 @@ class ProductRepositoryTest extends \PHPUnit\Framework\TestCase - $editMode = false; - $storeId = 0; - -- $this->productFactoryMock->expects($this->exactly(2))->method('create') -- ->will($this->returnValue($this->productMock)); -- $this->productMock->expects($this->exactly(2))->method('load'); -- $this->productMock->expects($this->exactly(2))->method('getId')->willReturn($sku); -- $this->resourceModelMock->expects($this->exactly(2))->method('getIdBySku') -+ $this->productFactory->expects($this->exactly(2))->method('create') -+ ->will($this->returnValue($this->product)); -+ $this->product->expects($this->exactly(2))->method('load'); -+ $this->product->expects($this->exactly(2))->method('getId')->willReturn($sku); -+ $this->resourceModel->expects($this->exactly(2))->method('getIdBySku') - ->with($sku)->willReturn($id); -- $this->productMock->expects($this->exactly(2))->method('getSku')->willReturn($sku); -+ $this->product->expects($this->exactly(2))->method('getSku')->willReturn($sku); - $this->serializerMock->expects($this->exactly(3))->method('serialize'); - -- $this->assertEquals($this->productMock, $this->model->get($sku, $editMode, $storeId)); -+ $this->assertEquals($this->product, $this->model->get($sku, $editMode, $storeId)); - //second invocation should just return from cache -- $this->assertEquals($this->productMock, $this->model->get($sku, $editMode, $storeId)); -+ $this->assertEquals($this->product, $this->model->get($sku, $editMode, $storeId)); - //force reload -- $this->assertEquals($this->productMock, $this->model->get($sku, $editMode, $storeId, true)); -+ $this->assertEquals($this->product, $this->model->get($sku, $editMode, $storeId, true)); - } - - public function testGetByIdWithSetStoreId() - { - $productId = 123; - $storeId = 1; -- $this->productFactoryMock->expects($this->atLeastOnce())->method('create') -- ->will($this->returnValue($this->productMock)); -- $this->productMock->expects($this->once())->method('setData')->with('store_id', $storeId); -- $this->productMock->expects($this->once())->method('load')->with($productId); -- $this->productMock->expects($this->atLeastOnce())->method('getId')->willReturn($productId); -- $this->productMock->method('getSku')->willReturn('simple'); -- $this->assertEquals($this->productMock, $this->model->getById($productId, false, $storeId)); -+ $this->productFactory->expects($this->atLeastOnce())->method('create') -+ ->will($this->returnValue($this->product)); -+ $this->product->expects($this->once())->method('setData')->with('store_id', $storeId); -+ $this->product->expects($this->once())->method('load')->with($productId); -+ $this->product->expects($this->atLeastOnce())->method('getId')->willReturn($productId); -+ $this->product->method('getSku')->willReturn('simple'); -+ $this->assertEquals($this->product, $this->model->getById($productId, false, $storeId)); - } - - public function testGetBySkuFromCacheInitializedInGetById() - { - $productId = 123; - $productSku = 'product_123'; -- $this->productFactoryMock->expects($this->once())->method('create') -- ->will($this->returnValue($this->productMock)); -- $this->productMock->expects($this->once())->method('load')->with($productId); -- $this->productMock->expects($this->atLeastOnce())->method('getId')->willReturn($productId); -- $this->productMock->expects($this->once())->method('getSku')->willReturn($productSku); -- $this->assertEquals($this->productMock, $this->model->getById($productId)); -- $this->assertEquals($this->productMock, $this->model->get($productSku)); -+ $this->productFactory->expects($this->once())->method('create') -+ ->will($this->returnValue($this->product)); -+ $this->product->expects($this->once())->method('load')->with($productId); -+ $this->product->expects($this->atLeastOnce())->method('getId')->willReturn($productId); -+ $this->product->expects($this->once())->method('getSku')->willReturn($productSku); -+ $this->assertEquals($this->product, $this->model->getById($productId)); -+ $this->assertEquals($this->product, $this->model->get($productSku)); - } - - public function testSaveExisting() - { -- $this->resourceModelMock->expects($this->any())->method('getIdBySku')->will($this->returnValue(100)); -- $this->productFactoryMock->expects($this->any()) -+ $this->resourceModel->expects($this->any())->method('getIdBySku')->will($this->returnValue(100)); -+ $this->productFactory->expects($this->any()) - ->method('create') -- ->will($this->returnValue($this->productMock)); -- $this->initializationHelperMock->expects($this->never())->method('initialize'); -- $this->resourceModelMock->expects($this->once())->method('validate')->with($this->productMock) -+ ->will($this->returnValue($this->product)); -+ $this->initializationHelper->expects($this->never())->method('initialize'); -+ $this->resourceModel->expects($this->once())->method('validate')->with($this->product) - ->willReturn(true); -- $this->resourceModelMock->expects($this->once())->method('save')->with($this->productMock)->willReturn(true); -- $this->extensibleDataObjectConverterMock -+ $this->resourceModel->expects($this->once())->method('save')->with($this->product)->willReturn(true); -+ $this->extensibleDataObjectConverter - ->expects($this->once()) - ->method('toNestedArray') - ->will($this->returnValue($this->productData)); -- $this->productMock->expects($this->atLeastOnce())->method('getSku')->willReturn($this->productData['sku']); -+ $this->product->expects($this->atLeastOnce())->method('getSku')->willReturn($this->productData['sku']); - -- $this->assertEquals($this->productMock, $this->model->save($this->productMock)); -+ $this->assertEquals($this->product, $this->model->save($this->product)); - } - - public function testSaveNew() - { -- $this->storeManagerMock->expects($this->any())->method('getWebsites')->willReturn([1 => 'default']); -- $this->resourceModelMock->expects($this->at(0))->method('getIdBySku')->will($this->returnValue(null)); -- $this->resourceModelMock->expects($this->at(3))->method('getIdBySku')->will($this->returnValue(100)); -- $this->productFactoryMock->expects($this->any()) -+ $this->storeManager->expects($this->any())->method('getWebsites')->willReturn([1 => 'default']); -+ $this->resourceModel->expects($this->at(0))->method('getIdBySku')->will($this->returnValue(null)); -+ $this->resourceModel->expects($this->at(3))->method('getIdBySku')->will($this->returnValue(100)); -+ $this->productFactory->expects($this->any()) - ->method('create') -- ->will($this->returnValue($this->productMock)); -- $this->initializationHelperMock->expects($this->never())->method('initialize'); -- $this->resourceModelMock->expects($this->once())->method('validate')->with($this->productMock) -+ ->will($this->returnValue($this->product)); -+ $this->initializationHelper->expects($this->never())->method('initialize'); -+ $this->resourceModel->expects($this->once())->method('validate')->with($this->product) - ->willReturn(true); -- $this->resourceModelMock->expects($this->once())->method('save')->with($this->productMock)->willReturn(true); -- $this->extensibleDataObjectConverterMock -+ $this->resourceModel->expects($this->once())->method('save')->with($this->product)->willReturn(true); -+ $this->extensibleDataObjectConverter - ->expects($this->once()) - ->method('toNestedArray') - ->will($this->returnValue($this->productData)); -- $this->productMock->method('getSku')->willReturn('simple'); -+ $this->product->method('getSku')->willReturn('simple'); - -- $this->assertEquals($this->productMock, $this->model->save($this->productMock)); -+ $this->assertEquals($this->product, $this->model->save($this->product)); - } - - /** -@@ -597,24 +645,24 @@ class ProductRepositoryTest extends \PHPUnit\Framework\TestCase - */ - public function testSaveUnableToSaveException() - { -- $this->storeManagerMock->expects($this->any())->method('getWebsites')->willReturn([1 => 'default']); -- $this->resourceModelMock->expects($this->exactly(1)) -+ $this->storeManager->expects($this->any())->method('getWebsites')->willReturn([1 => 'default']); -+ $this->resourceModel->expects($this->exactly(1)) - ->method('getIdBySku')->willReturn(null); -- $this->productFactoryMock->expects($this->exactly(2)) -+ $this->productFactory->expects($this->exactly(2)) - ->method('create') -- ->will($this->returnValue($this->productMock)); -- $this->initializationHelperMock->expects($this->never())->method('initialize'); -- $this->resourceModelMock->expects($this->once())->method('validate')->with($this->productMock) -+ ->will($this->returnValue($this->product)); -+ $this->initializationHelper->expects($this->never())->method('initialize'); -+ $this->resourceModel->expects($this->once())->method('validate')->with($this->product) - ->willReturn(true); -- $this->resourceModelMock->expects($this->once())->method('save')->with($this->productMock) -+ $this->resourceModel->expects($this->once())->method('save')->with($this->product) - ->willThrowException(new \Exception()); -- $this->extensibleDataObjectConverterMock -+ $this->extensibleDataObjectConverter - ->expects($this->once()) - ->method('toNestedArray') - ->will($this->returnValue($this->productData)); -- $this->productMock->method('getSku')->willReturn('simple'); -+ $this->product->method('getSku')->willReturn('simple'); - -- $this->model->save($this->productMock); -+ $this->model->save($this->product); - } - - /** -@@ -623,24 +671,24 @@ class ProductRepositoryTest extends \PHPUnit\Framework\TestCase - */ - public function testSaveException() - { -- $this->storeManagerMock->expects($this->any())->method('getWebsites')->willReturn([1 => 'default']); -- $this->resourceModelMock->expects($this->exactly(1))->method('getIdBySku')->will($this->returnValue(null)); -- $this->productFactoryMock->expects($this->exactly(2)) -+ $this->storeManager->expects($this->any())->method('getWebsites')->willReturn([1 => 'default']); -+ $this->resourceModel->expects($this->exactly(1))->method('getIdBySku')->will($this->returnValue(null)); -+ $this->productFactory->expects($this->exactly(2)) - ->method('create') -- ->will($this->returnValue($this->productMock)); -- $this->initializationHelperMock->expects($this->never())->method('initialize'); -- $this->resourceModelMock->expects($this->once())->method('validate')->with($this->productMock) -+ ->will($this->returnValue($this->product)); -+ $this->initializationHelper->expects($this->never())->method('initialize'); -+ $this->resourceModel->expects($this->once())->method('validate')->with($this->product) - ->willReturn(true); -- $this->resourceModelMock->expects($this->once())->method('save')->with($this->productMock) -+ $this->resourceModel->expects($this->once())->method('save')->with($this->product) - ->willThrowException(new \Magento\Eav\Model\Entity\Attribute\Exception(__('123'))); -- $this->productMock->expects($this->once())->method('getId')->willReturn(null); -- $this->extensibleDataObjectConverterMock -+ $this->product->expects($this->exactly(2))->method('getId')->willReturn(null); -+ $this->extensibleDataObjectConverter - ->expects($this->once()) - ->method('toNestedArray') - ->will($this->returnValue($this->productData)); -- $this->productMock->method('getSku')->willReturn('simple'); -+ $this->product->method('getSku')->willReturn('simple'); - -- $this->model->save($this->productMock); -+ $this->model->save($this->product); - } - - /** -@@ -649,22 +697,22 @@ class ProductRepositoryTest extends \PHPUnit\Framework\TestCase - */ - public function testSaveInvalidProductException() - { -- $this->storeManagerMock->expects($this->any())->method('getWebsites')->willReturn([1 => 'default']); -- $this->resourceModelMock->expects($this->exactly(1))->method('getIdBySku')->will($this->returnValue(null)); -- $this->productFactoryMock->expects($this->exactly(2)) -+ $this->storeManager->expects($this->any())->method('getWebsites')->willReturn([1 => 'default']); -+ $this->resourceModel->expects($this->exactly(1))->method('getIdBySku')->will($this->returnValue(null)); -+ $this->productFactory->expects($this->exactly(2)) - ->method('create') -- ->will($this->returnValue($this->productMock)); -- $this->initializationHelperMock->expects($this->never())->method('initialize'); -- $this->resourceModelMock->expects($this->once())->method('validate')->with($this->productMock) -+ ->will($this->returnValue($this->product)); -+ $this->initializationHelper->expects($this->never())->method('initialize'); -+ $this->resourceModel->expects($this->once())->method('validate')->with($this->product) - ->willReturn(['error1', 'error2']); -- $this->productMock->expects($this->never())->method('getId'); -- $this->extensibleDataObjectConverterMock -+ $this->product->expects($this->once())->method('getId')->willReturn(null); -+ $this->extensibleDataObjectConverter - ->expects($this->once()) - ->method('toNestedArray') - ->will($this->returnValue($this->productData)); -- $this->productMock->method('getSku')->willReturn('simple'); -+ $this->product->method('getSku')->willReturn('simple'); - -- $this->model->save($this->productMock); -+ $this->model->save($this->product); - } - - /** -@@ -673,36 +721,36 @@ class ProductRepositoryTest extends \PHPUnit\Framework\TestCase - */ - public function testSaveThrowsTemporaryStateExceptionIfDatabaseConnectionErrorOccurred() - { -- $this->storeManagerMock->expects($this->any())->method('getWebsites')->willReturn([1 => 'default']); -- $this->productFactoryMock->expects($this->any()) -+ $this->storeManager->expects($this->any())->method('getWebsites')->willReturn([1 => 'default']); -+ $this->productFactory->expects($this->any()) - ->method('create') -- ->will($this->returnValue($this->productMock)); -- $this->initializationHelperMock->expects($this->never()) -+ ->will($this->returnValue($this->product)); -+ $this->initializationHelper->expects($this->never()) - ->method('initialize'); -- $this->resourceModelMock->expects($this->once()) -+ $this->resourceModel->expects($this->once()) - ->method('validate') -- ->with($this->productMock) -+ ->with($this->product) - ->willReturn(true); -- $this->resourceModelMock->expects($this->once()) -+ $this->resourceModel->expects($this->once()) - ->method('save') -- ->with($this->productMock) -+ ->with($this->product) - ->willThrowException(new ConnectionException('Connection lost')); -- $this->extensibleDataObjectConverterMock -+ $this->extensibleDataObjectConverter - ->expects($this->once()) - ->method('toNestedArray') - ->will($this->returnValue($this->productData)); -- $this->productMock->method('getSku')->willReturn('simple'); -+ $this->product->method('getSku')->willReturn('simple'); - -- $this->model->save($this->productMock); -+ $this->model->save($this->product); - } - - public function testDelete() - { -- $this->productMock->expects($this->exactly(2))->method('getSku')->willReturn('product-42'); -- $this->productMock->expects($this->exactly(2))->method('getId')->willReturn(42); -- $this->resourceModelMock->expects($this->once())->method('delete')->with($this->productMock) -+ $this->product->expects($this->exactly(2))->method('getSku')->willReturn('product-42'); -+ $this->product->expects($this->exactly(2))->method('getId')->willReturn(42); -+ $this->resourceModel->expects($this->once())->method('delete')->with($this->product) - ->willReturn(true); -- $this->assertTrue($this->model->delete($this->productMock)); -+ $this->assertTrue($this->model->delete($this->product)); - } - - /** -@@ -711,47 +759,47 @@ class ProductRepositoryTest extends \PHPUnit\Framework\TestCase - */ - public function testDeleteException() - { -- $this->productMock->expects($this->exactly(2))->method('getSku')->willReturn('product-42'); -- $this->productMock->expects($this->exactly(2))->method('getId')->willReturn(42); -- $this->resourceModelMock->expects($this->once())->method('delete')->with($this->productMock) -+ $this->product->expects($this->exactly(2))->method('getSku')->willReturn('product-42'); -+ $this->product->expects($this->exactly(2))->method('getId')->willReturn(42); -+ $this->resourceModel->expects($this->once())->method('delete')->with($this->product) - ->willThrowException(new \Exception()); -- $this->model->delete($this->productMock); -+ $this->model->delete($this->product); - } - - public function testDeleteById() - { - $sku = 'product-42'; -- $this->productFactoryMock->expects($this->once())->method('create') -- ->will($this->returnValue($this->productMock)); -- $this->resourceModelMock->expects($this->once())->method('getIdBySku')->with($sku) -+ $this->productFactory->expects($this->once())->method('create') -+ ->will($this->returnValue($this->product)); -+ $this->resourceModel->expects($this->once())->method('getIdBySku')->with($sku) - ->will($this->returnValue('42')); -- $this->productMock->expects($this->once())->method('load')->with('42'); -- $this->productMock->expects($this->atLeastOnce())->method('getSku')->willReturn($sku); -+ $this->product->expects($this->once())->method('load')->with('42'); -+ $this->product->expects($this->atLeastOnce())->method('getSku')->willReturn($sku); - $this->assertTrue($this->model->deleteById($sku)); - } - - public function testGetList() - { -- $searchCriteriaMock = $this->createMock(\Magento\Framework\Api\SearchCriteriaInterface::class); -- $collectionMock = $this->createMock(\Magento\Catalog\Model\ResourceModel\Product\Collection::class); -- $this->collectionFactoryMock->expects($this->once())->method('create')->willReturn($collectionMock); -- $this->productMock->method('getSku')->willReturn('simple'); -+ $searchCriteriaMock = $this->createMock(SearchCriteriaInterface::class); -+ $collectionMock = $this->createMock(Collection::class); -+ $this->collectionFactory->expects($this->once())->method('create')->willReturn($collectionMock); -+ $this->product->method('getSku')->willReturn('simple'); - $collectionMock->expects($this->once())->method('addAttributeToSelect')->with('*'); - $collectionMock->expects($this->exactly(2))->method('joinAttribute')->withConsecutive( - ['status', 'catalog_product/status', 'entity_id', null, 'inner'], - ['visibility', 'catalog_product/visibility', 'entity_id', null, 'inner'] - ); -- $this->collectionProcessorMock->expects($this->once()) -+ $this->collectionProcessor->expects($this->once()) - ->method('process') - ->with($searchCriteriaMock, $collectionMock); - $collectionMock->expects($this->once())->method('load'); - $collectionMock->expects($this->once())->method('addCategoryIds'); -- $collectionMock->expects($this->atLeastOnce())->method('getItems')->willReturn([$this->productMock]); -+ $collectionMock->expects($this->atLeastOnce())->method('getItems')->willReturn([$this->product]); - $collectionMock->expects($this->once())->method('getSize')->willReturn(128); -- $searchResultsMock = $this->createMock(\Magento\Catalog\Api\Data\ProductSearchResultsInterface::class); -+ $searchResultsMock = $this->createMock(ProductSearchResultsInterface::class); - $searchResultsMock->expects($this->once())->method('setSearchCriteria')->with($searchCriteriaMock); -- $searchResultsMock->expects($this->once())->method('setItems')->with([$this->productMock]); -- $this->searchResultsFactoryMock->expects($this->once())->method('create')->willReturn($searchResultsMock); -+ $searchResultsMock->expects($this->once())->method('setItems')->with([$this->product]); -+ $this->searchResultsFactory->expects($this->once())->method('create')->willReturn($searchResultsMock); - $this->assertEquals($searchResultsMock, $this->model->getList($searchCriteriaMock)); - } - -@@ -821,28 +869,28 @@ class ProductRepositoryTest extends \PHPUnit\Framework\TestCase - */ - public function testSaveExistingWithOptions(array $newOptions, array $existingOptions, array $expectedData) - { -- $this->storeManagerMock->expects($this->any())->method('getWebsites')->willReturn([1 => 'default']); -- $this->resourceModelMock->expects($this->any())->method('getIdBySku')->will($this->returnValue(100)); -- $this->productFactoryMock->expects($this->any()) -+ $this->storeManager->expects($this->any())->method('getWebsites')->willReturn([1 => 'default']); -+ $this->resourceModel->expects($this->any())->method('getIdBySku')->will($this->returnValue(100)); -+ $this->productFactory->expects($this->any()) - ->method('create') -- ->will($this->returnValue($this->initializedProductMock)); -- $this->initializationHelperMock->expects($this->never())->method('initialize'); -- $this->resourceModelMock->expects($this->once())->method('validate')->with($this->initializedProductMock) -+ ->will($this->returnValue($this->initializedProduct)); -+ $this->initializationHelper->expects($this->never())->method('initialize'); -+ $this->resourceModel->expects($this->once())->method('validate')->with($this->initializedProduct) - ->willReturn(true); -- $this->resourceModelMock->expects($this->once())->method('save') -- ->with($this->initializedProductMock)->willReturn(true); -+ $this->resourceModel->expects($this->once())->method('save') -+ ->with($this->initializedProduct)->willReturn(true); - //option data - $this->productData['options'] = $newOptions; -- $this->extensibleDataObjectConverterMock -+ $this->extensibleDataObjectConverter - ->expects($this->once()) - ->method('toNestedArray') - ->will($this->returnValue($this->productData)); - -- $this->initializedProductMock->expects($this->atLeastOnce()) -+ $this->initializedProduct->expects($this->atLeastOnce()) - ->method('getSku')->willReturn($this->productData['sku']); -- $this->productMock->expects($this->atLeastOnce())->method('getSku')->willReturn($this->productData['sku']); -+ $this->product->expects($this->atLeastOnce())->method('getSku')->willReturn($this->productData['sku']); - -- $this->assertEquals($this->initializedProductMock, $this->model->save($this->productMock)); -+ $this->assertEquals($this->initializedProduct, $this->model->save($this->product)); - } - - /** -@@ -882,8 +930,8 @@ class ProductRepositoryTest extends \PHPUnit\Framework\TestCase - ], - ]; - -- /** @var \Magento\Catalog\Model\Product\Option|\PHPUnit_Framework_MockObject_MockObject $existingOption1 */ -- $existingOption1 = $this->getMockBuilder(\Magento\Catalog\Model\Product\Option::class) -+ /** @var Option|MockObject $existingOption1 */ -+ $existingOption1 = $this->getMockBuilder(Option::class) - ->disableOriginalConstructor() - ->setMethods(null) - ->getMock(); -@@ -893,8 +941,8 @@ class ProductRepositoryTest extends \PHPUnit\Framework\TestCase - "type" => "drop_down", - ] - ); -- /** @var \Magento\Catalog\Model\Product\Option\Value $existingOptionValue1 */ -- $existingOptionValue1 = $this->getMockBuilder(\Magento\Catalog\Model\Product\Option\Value::class) -+ /** @var Value $existingOptionValue1 */ -+ $existingOptionValue1 = $this->getMockBuilder(Value::class) - ->disableOriginalConstructor() - ->setMethods(null) - ->getMock(); -@@ -905,7 +953,7 @@ class ProductRepositoryTest extends \PHPUnit\Framework\TestCase - "price" => 5, - ] - ); -- $existingOptionValue2 = $this->getMockBuilder(\Magento\Catalog\Model\Product\Option\Value::class) -+ $existingOptionValue2 = $this->getMockBuilder(Value::class) - ->disableOriginalConstructor() - ->setMethods(null) - ->getMock(); -@@ -922,7 +970,7 @@ class ProductRepositoryTest extends \PHPUnit\Framework\TestCase - "9" => $existingOptionValue2, - ] - ); -- $existingOption2 = $this->getMockBuilder(\Magento\Catalog\Model\Product\Option::class) -+ $existingOption2 = $this->getMockBuilder(Option::class) - ->disableOriginalConstructor() - ->setMethods(null) - ->getMock(); -@@ -987,36 +1035,36 @@ class ProductRepositoryTest extends \PHPUnit\Framework\TestCase - * @param array $existingLinks - * @param array $expectedData - * @dataProvider saveWithLinksDataProvider -- * @throws \Magento\Framework\Exception\CouldNotSaveException -- * @throws \Magento\Framework\Exception\InputException -+ * @throws CouldNotSaveException -+ * @throws InputException - */ - public function testSaveWithLinks(array $newLinks, array $existingLinks, array $expectedData) - { -- $this->storeManagerMock->expects($this->any())->method('getWebsites')->willReturn([1 => 'default']); -- $this->resourceModelMock->expects($this->any())->method('getIdBySku')->will($this->returnValue(100)); -- $this->productFactoryMock->expects($this->any()) -+ $this->storeManager->expects($this->any())->method('getWebsites')->willReturn([1 => 'default']); -+ $this->resourceModel->expects($this->any())->method('getIdBySku')->will($this->returnValue(100)); -+ $this->productFactory->expects($this->any()) - ->method('create') -- ->will($this->returnValue($this->initializedProductMock)); -- $this->initializationHelperMock->expects($this->never())->method('initialize'); -- $this->resourceModelMock->expects($this->once())->method('validate')->with($this->initializedProductMock) -+ ->will($this->returnValue($this->initializedProduct)); -+ $this->initializationHelper->expects($this->never())->method('initialize'); -+ $this->resourceModel->expects($this->once())->method('validate')->with($this->initializedProduct) - ->willReturn(true); -- $this->resourceModelMock->expects($this->once())->method('save') -- ->with($this->initializedProductMock)->willReturn(true); -+ $this->resourceModel->expects($this->once())->method('save') -+ ->with($this->initializedProduct)->willReturn(true); - -- $this->initializedProductMock->setData("product_links", $existingLinks); -+ $this->initializedProduct->setData("product_links", $existingLinks); - - if (!empty($newLinks)) { - $linkTypes = ['related' => 1, 'upsell' => 4, 'crosssell' => 5, 'associated' => 3]; -- $this->linkTypeProviderMock->expects($this->once()) -+ $this->linkTypeProvider->expects($this->once()) - ->method('getLinkTypes') - ->willReturn($linkTypes); - -- $this->initializedProductMock->setData("ignore_links_flag", false); -- $this->resourceModelMock -+ $this->initializedProduct->setData("ignore_links_flag", false); -+ $this->resourceModel - ->expects($this->any())->method('getProductsIdsBySkus') - ->willReturn([$newLinks['linked_product_sku'] => $newLinks['linked_product_sku']]); - -- $inputLink = $this->objectManager->getObject(\Magento\Catalog\Model\ProductLink\Link::class); -+ $inputLink = $this->objectManager->getObject(Link::class); - $inputLink->setProductSku($newLinks['product_sku']); - $inputLink->setLinkType($newLinks['link_type']); - $inputLink->setLinkedProductSku($newLinks['linked_product_sku']); -@@ -1029,29 +1077,29 @@ class ProductRepositoryTest extends \PHPUnit\Framework\TestCase - - $this->productData['product_links'] = [$inputLink]; - -- $this->initializedProductMock->expects($this->any()) -+ $this->initializedProduct->expects($this->any()) - ->method('getProductLinks') - ->willReturn([$inputLink]); - } else { -- $this->resourceModelMock -+ $this->resourceModel - ->expects($this->any())->method('getProductsIdsBySkus') - ->willReturn([]); - - $this->productData['product_links'] = []; - -- $this->initializedProductMock->setData('ignore_links_flag', true); -- $this->initializedProductMock->expects($this->never()) -+ $this->initializedProduct->setData('ignore_links_flag', true); -+ $this->initializedProduct->expects($this->never()) - ->method('getProductLinks') - ->willReturn([]); - } - -- $this->extensibleDataObjectConverterMock -+ $this->extensibleDataObjectConverter - ->expects($this->at(0)) - ->method('toNestedArray') - ->will($this->returnValue($this->productData)); - - if (!empty($newLinks)) { -- $this->extensibleDataObjectConverterMock -+ $this->extensibleDataObjectConverter - ->expects($this->at(1)) - ->method('toNestedArray') - ->will($this->returnValue($newLinks)); -@@ -1060,7 +1108,7 @@ class ProductRepositoryTest extends \PHPUnit\Framework\TestCase - $outputLinks = []; - if (!empty($expectedData)) { - foreach ($expectedData as $link) { -- $outputLink = $this->objectManager->getObject(\Magento\Catalog\Model\ProductLink\Link::class); -+ $outputLink = $this->objectManager->getObject(Link::class); - $outputLink->setProductSku($link['product_sku']); - $outputLink->setLinkType($link['link_type']); - $outputLink->setLinkedProductSku($link['linked_product_sku']); -@@ -1075,18 +1123,18 @@ class ProductRepositoryTest extends \PHPUnit\Framework\TestCase - } - - if (!empty($outputLinks)) { -- $this->initializedProductMock->expects($this->once()) -+ $this->initializedProduct->expects($this->once()) - ->method('setProductLinks') - ->with($outputLinks); - } else { -- $this->initializedProductMock->expects($this->never()) -+ $this->initializedProduct->expects($this->never()) - ->method('setProductLinks'); - } -- $this->initializedProductMock->expects($this->atLeastOnce()) -+ $this->initializedProduct->expects($this->atLeastOnce()) - ->method('getSku')->willReturn($this->productData['sku']); - -- $results = $this->model->save($this->initializedProductMock); -- $this->assertEquals($this->initializedProductMock, $results); -+ $results = $this->model->save($this->initializedProduct); -+ $this->assertEquals($this->initializedProduct, $results); - } - - /** -@@ -1163,20 +1211,20 @@ class ProductRepositoryTest extends \PHPUnit\Framework\TestCase - - protected function setupProductMocksForSave() - { -- $this->resourceModelMock->expects($this->any())->method('getIdBySku')->will($this->returnValue(100)); -- $this->productFactoryMock->expects($this->any()) -+ $this->resourceModel->expects($this->any())->method('getIdBySku')->will($this->returnValue(100)); -+ $this->productFactory->expects($this->any()) - ->method('create') -- ->will($this->returnValue($this->initializedProductMock)); -- $this->initializationHelperMock->expects($this->never())->method('initialize'); -- $this->resourceModelMock->expects($this->once())->method('validate')->with($this->initializedProductMock) -+ ->will($this->returnValue($this->initializedProduct)); -+ $this->initializationHelper->expects($this->never())->method('initialize'); -+ $this->resourceModel->expects($this->once())->method('validate')->with($this->initializedProduct) - ->willReturn(true); -- $this->resourceModelMock->expects($this->once())->method('save') -- ->with($this->initializedProductMock)->willReturn(true); -+ $this->resourceModel->expects($this->once())->method('save') -+ ->with($this->initializedProduct)->willReturn(true); - } - - public function testSaveExistingWithNewMediaGalleryEntries() - { -- $this->storeManagerMock->expects($this->any())->method('getWebsites')->willReturn([1 => 'default']); -+ $this->storeManager->expects($this->any())->method('getWebsites')->willReturn([1 => 'default']); - $newEntriesData = [ - 'images' => [ - [ -@@ -1200,13 +1248,13 @@ class ProductRepositoryTest extends \PHPUnit\Framework\TestCase - $this->setupProductMocksForSave(); - //media gallery data - $this->productData['media_gallery'] = $newEntriesData; -- $this->extensibleDataObjectConverterMock -+ $this->extensibleDataObjectConverter - ->expects($this->once()) - ->method('toNestedArray') - ->will($this->returnValue($this->productData)); - -- $this->initializedProductMock->setData('media_gallery', $newEntriesData); -- $this->initializedProductMock->expects($this->any()) -+ $this->initializedProduct->setData('media_gallery', $newEntriesData); -+ $this->initializedProduct->expects($this->any()) - ->method('getMediaAttributes') - ->willReturn(["image" => "imageAttribute", "small_image" => "small_image_attribute"]); - -@@ -1214,40 +1262,40 @@ class ProductRepositoryTest extends \PHPUnit\Framework\TestCase - $mediaTmpPath = '/tmp'; - $absolutePath = '/a/b/filename.jpg'; - -- $this->mediaGalleryProcessor->expects($this->once())->method('clearMediaAttribute') -- ->with($this->initializedProductMock, ['image', 'small_image']); -+ $this->processor->expects($this->once())->method('clearMediaAttribute') -+ ->with($this->initializedProduct, ['image', 'small_image']); - -- $mediaConfigMock = $this->getMockBuilder(\Magento\Catalog\Model\Product\Media\Config::class) -+ $mediaConfigMock = $this->getMockBuilder(Config::class) - ->disableOriginalConstructor() - ->getMock(); - $mediaConfigMock->expects($this->once()) - ->method('getTmpMediaShortUrl') - ->with($absolutePath) - ->willReturn($mediaTmpPath . $absolutePath); -- $this->initializedProductMock->expects($this->once()) -+ $this->initializedProduct->expects($this->once()) - ->method('getMediaConfig') - ->willReturn($mediaConfigMock); - - //verify new entries -- $contentDataObject = $this->getMockBuilder(\Magento\Framework\Api\ImageContent::class) -+ $contentDataObject = $this->getMockBuilder(ImageContent::class) - ->disableOriginalConstructor() - ->setMethods(null) - ->getMock(); -- $this->contentFactoryMock->expects($this->once()) -+ $this->contentFactory->expects($this->once()) - ->method('create') - ->willReturn($contentDataObject); - -- $this->imageProcessorMock->expects($this->once()) -+ $this->imageProcessor->expects($this->once()) - ->method('processImageContent') - ->willReturn($absolutePath); - - $imageFileUri = "imageFileUri"; -- $this->mediaGalleryProcessor->expects($this->once())->method('addImage') -- ->with($this->initializedProductMock, $mediaTmpPath . $absolutePath, ['image', 'small_image'], true, false) -+ $this->processor->expects($this->once())->method('addImage') -+ ->with($this->initializedProduct, $mediaTmpPath . $absolutePath, ['image', 'small_image'], true, false) - ->willReturn($imageFileUri); -- $this->mediaGalleryProcessor->expects($this->once())->method('updateImage') -+ $this->processor->expects($this->once())->method('updateImage') - ->with( -- $this->initializedProductMock, -+ $this->initializedProduct, - $imageFileUri, - [ - 'label' => 'label_text', -@@ -1256,11 +1304,11 @@ class ProductRepositoryTest extends \PHPUnit\Framework\TestCase - 'media_type' => 'media_type', - ] - ); -- $this->initializedProductMock->expects($this->atLeastOnce()) -+ $this->initializedProduct->expects($this->atLeastOnce()) - ->method('getSku')->willReturn($this->productData['sku']); -- $this->productMock->expects($this->atLeastOnce())->method('getSku')->willReturn($this->productData['sku']); -+ $this->product->expects($this->atLeastOnce())->method('getSku')->willReturn($this->productData['sku']); - -- $this->model->save($this->productMock); -+ $this->model->save($this->product); - } - - /** -@@ -1276,38 +1324,40 @@ class ProductRepositoryTest extends \PHPUnit\Framework\TestCase - public function testSaveWithDifferentWebsites() - { - $storeMock = $this->createMock(StoreInterface::class); -- $this->resourceModelMock->expects($this->at(0))->method('getIdBySku')->will($this->returnValue(null)); -- $this->resourceModelMock->expects($this->at(3))->method('getIdBySku')->will($this->returnValue(100)); -- $this->productFactoryMock->expects($this->any()) -+ $this->resourceModel->expects($this->at(0))->method('getIdBySku')->will($this->returnValue(null)); -+ $this->resourceModel->expects($this->at(3))->method('getIdBySku')->will($this->returnValue(100)); -+ $this->productFactory->expects($this->any()) - ->method('create') -- ->will($this->returnValue($this->productMock)); -- $this->initializationHelperMock->expects($this->never())->method('initialize'); -- $this->resourceModelMock->expects($this->once())->method('validate')->with($this->productMock) -+ ->will($this->returnValue($this->product)); -+ $this->initializationHelper->expects($this->never())->method('initialize'); -+ $this->resourceModel->expects($this->once())->method('validate')->with($this->product) - ->willReturn(true); -- $this->resourceModelMock->expects($this->once())->method('save')->with($this->productMock)->willReturn(true); -- $this->extensibleDataObjectConverterMock -+ $this->resourceModel->expects($this->once())->method('save')->with($this->product)->willReturn(true); -+ $this->extensibleDataObjectConverter - ->expects($this->once()) - ->method('toNestedArray') - ->will($this->returnValue($this->productData)); -- $this->storeManagerMock->expects($this->any()) -+ $this->storeManager->expects($this->any()) - ->method('getStore') - ->willReturn($storeMock); -- $this->storeManagerMock->expects($this->once()) -+ $this->storeManager->expects($this->once()) - ->method('getWebsites') -- ->willReturn([ -+ ->willReturn( -+ [ - 1 => ['first'], - 2 => ['second'], - 3 => ['third'] -- ]); -- $this->productMock->expects($this->once())->method('setWebsiteIds')->willReturn([2,3]); -- $this->productMock->method('getSku')->willReturn('simple'); -+ ] -+ ); -+ $this->product->expects($this->once())->method('setWebsiteIds')->willReturn([2,3]); -+ $this->product->method('getSku')->willReturn('simple'); - -- $this->assertEquals($this->productMock, $this->model->save($this->productMock)); -+ $this->assertEquals($this->product, $this->model->save($this->product)); - } - - public function testSaveExistingWithMediaGalleryEntries() - { -- $this->storeManagerMock->expects($this->any())->method('getWebsites')->willReturn([1 => 'default']); -+ $this->storeManager->expects($this->any())->method('getWebsites')->willReturn([1 => 'default']); - //update one entry, delete one entry - $newEntries = [ - [ -@@ -1355,26 +1405,26 @@ class ProductRepositoryTest extends \PHPUnit\Framework\TestCase - $this->setupProductMocksForSave(); - //media gallery data - $this->productData['media_gallery']['images'] = $newEntries; -- $this->extensibleDataObjectConverterMock -+ $this->extensibleDataObjectConverter - ->expects($this->once()) - ->method('toNestedArray') - ->will($this->returnValue($this->productData)); - -- $this->initializedProductMock->setData('media_gallery', $existingMediaGallery); -- $this->initializedProductMock->expects($this->any()) -+ $this->initializedProduct->setData('media_gallery', $existingMediaGallery); -+ $this->initializedProduct->expects($this->any()) - ->method('getMediaAttributes') - ->willReturn(["image" => "filename1", "small_image" => "filename2"]); - -- $this->mediaGalleryProcessor->expects($this->once())->method('clearMediaAttribute') -- ->with($this->initializedProductMock, ['image', 'small_image']); -- $this->mediaGalleryProcessor->expects($this->once()) -+ $this->processor->expects($this->once())->method('clearMediaAttribute') -+ ->with($this->initializedProduct, ['image', 'small_image']); -+ $this->processor->expects($this->once()) - ->method('setMediaAttribute') -- ->with($this->initializedProductMock, ['image', 'small_image'], 'filename1'); -- $this->initializedProductMock->expects($this->atLeastOnce()) -+ ->with($this->initializedProduct, ['image', 'small_image'], 'filename1'); -+ $this->initializedProduct->expects($this->atLeastOnce()) - ->method('getSku')->willReturn($this->productData['sku']); -- $this->productMock->expects($this->atLeastOnce())->method('getSku')->willReturn($this->productData['sku']); -- $this->productMock->expects($this->any())->method('getMediaGalleryEntries')->willReturn(null); -- $this->model->save($this->productMock); -- $this->assertEquals($expectedResult, $this->initializedProductMock->getMediaGallery('images')); -+ $this->product->expects($this->atLeastOnce())->method('getSku')->willReturn($this->productData['sku']); -+ $this->product->expects($this->any())->method('getMediaGalleryEntries')->willReturn(null); -+ $this->model->save($this->product); -+ $this->assertEquals($expectedResult, $this->initializedProduct->getMediaGallery('images')); - } - } -diff --git a/app/code/Magento/Catalog/Test/Unit/Model/ProductTest.php b/app/code/Magento/Catalog/Test/Unit/Model/ProductTest.php -index 3a357df81fe..8bf8473080c 100644 ---- a/app/code/Magento/Catalog/Test/Unit/Model/ProductTest.php -+++ b/app/code/Magento/Catalog/Test/Unit/Model/ProductTest.php -@@ -6,15 +6,13 @@ - - namespace Magento\Catalog\Test\Unit\Model; - --use Magento\Catalog\Api\Data\ProductExtensionFactory; - use Magento\Catalog\Api\Data\ProductExtensionInterface; - use Magento\Catalog\Model\Product; --use Magento\Eav\Model\Entity\GetCustomAttributeCodesInterface; -+use Magento\Catalog\Model\Product\Attribute\Source\Status; - use Magento\Framework\Api\Data\ImageContentInterface; - use Magento\Framework\Api\ExtensibleDataInterface; - use Magento\Framework\Api\ExtensionAttributesFactory; - use Magento\Framework\TestFramework\Unit\Helper\ObjectManager as ObjectManagerHelper; --use Magento\Catalog\Model\Product\Attribute\Source\Status as Status; - - /** - * Product Test -@@ -42,7 +40,7 @@ class ProductTest extends \PHPUnit\Framework\TestCase - protected $model; - - /** -- * @var \Magento\Framework\Module\Manager|\PHPUnit_Framework_MockObject_MockObject -+ * @var \Magento\Framework\Module\ModuleManagerInterface|\PHPUnit_Framework_MockObject_MockObject - */ - protected $moduleManager; - -@@ -81,6 +79,11 @@ class ProductTest extends \PHPUnit\Framework\TestCase - */ - protected $_priceInfoMock; - -+ /** -+ * @var \Magento\Catalog\Model\FilterProductCustomAttribute|\PHPUnit_Framework_MockObject_MockObject -+ */ -+ private $filterCustomAttribute; -+ - /** - * @var \Magento\Store\Model\Store|\PHPUnit_Framework_MockObject_MockObject - */ -@@ -177,7 +180,7 @@ class ProductTest extends \PHPUnit\Framework\TestCase - /** - * @var \PHPUnit_Framework_MockObject_MockObject - */ -- private $extensionAttrbutes; -+ private $extensionAttributes; - - /** - * @var \PHPUnit_Framework_MockObject_MockObject -@@ -197,12 +200,12 @@ class ProductTest extends \PHPUnit\Framework\TestCase - /** - * @var ProductExtensionInterface|\PHPUnit_Framework_MockObject_MockObject - */ -- private $extensionAttributes; -+ private $productExtAttributes; - - /** -- * @var GetCustomAttributeCodesInterface|\PHPUnit_Framework_MockObject_MockObject -+ * @var \Magento\Eav\Model\Config|\PHPUnit_Framework_MockObject_MockObject - */ -- private $getCustomAttributeCodes; -+ private $eavConfig; - - /** - * @SuppressWarnings(PHPMD.ExcessiveMethodLength) -@@ -212,10 +215,10 @@ class ProductTest extends \PHPUnit\Framework\TestCase - $this->categoryIndexerMock = $this->getMockForAbstractClass(\Magento\Framework\Indexer\IndexerInterface::class); - - $this->moduleManager = $this->createPartialMock( -- \Magento\Framework\Module\Manager::class, -+ \Magento\Framework\Module\ModuleManagerInterface::class, - ['isEnabled'] - ); -- $this->extensionAttrbutes = $this->getMockBuilder(\Magento\Framework\Api\ExtensionAttributesInterface::class) -+ $this->extensionAttributes = $this->getMockBuilder(\Magento\Framework\Api\ExtensionAttributesInterface::class) - ->setMethods(['getWebsiteIds', 'setWebsiteIds']) - ->disableOriginalConstructor() - ->getMock(); -@@ -367,18 +370,19 @@ class ProductTest extends \PHPUnit\Framework\TestCase - ->setMethods(['create']) - ->getMock(); - $this->mediaConfig = $this->createMock(\Magento\Catalog\Model\Product\Media\Config::class); -+ $this->eavConfig = $this->createMock(\Magento\Eav\Model\Config::class); - -- $this->extensionAttributes = $this->getMockBuilder(ProductExtensionInterface::class) -+ $this->productExtAttributes = $this->getMockBuilder(ProductExtensionInterface::class) - ->setMethods(['getStockItem']) - ->getMockForAbstractClass(); - $this->extensionAttributesFactory - ->expects($this->any()) - ->method('create') -- ->willReturn($this->extensionAttributes); -- $this->getCustomAttributeCodes = $this->getMockBuilder(GetCustomAttributeCodesInterface::class) -- ->disableOriginalConstructor() -- ->setMethods(['execute']) -- ->getMockForAbstractClass(); -+ ->willReturn($this->productExtAttributes); -+ -+ $this->filterCustomAttribute = $this->createTestProxy( -+ \Magento\Catalog\Model\FilterProductCustomAttribute::class -+ ); - - $this->objectManagerHelper = new ObjectManagerHelper($this); - $this->model = $this->objectManagerHelper->getObject( -@@ -409,7 +413,8 @@ class ProductTest extends \PHPUnit\Framework\TestCase - '_filesystem' => $this->filesystemMock, - '_collectionFactory' => $this->collectionFactoryMock, - 'data' => ['id' => 1], -- 'getCustomAttributeCodes' => $this->getCustomAttributeCodes -+ 'eavConfig' => $this->eavConfig, -+ 'filterCustomAttribute' => $this->filterCustomAttribute - ] - ); - } -@@ -479,9 +484,11 @@ class ProductTest extends \PHPUnit\Framework\TestCase - - $abstractDbMock = $this->getMockBuilder(\Magento\Framework\Model\ResourceModel\Db\AbstractDb::class) - ->disableOriginalConstructor() -- ->setMethods([ -+ ->setMethods( -+ [ - 'getCategoryCollection', -- ]) -+ ] -+ ) - ->getMockForAbstractClass(); - $getCategoryCollectionMock = $this->createMock( - \Magento\Framework\Data\Collection::class -@@ -544,6 +551,7 @@ class ProductTest extends \PHPUnit\Framework\TestCase - - 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)); -@@ -552,7 +560,8 @@ class ProductTest extends \PHPUnit\Framework\TestCase - - 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)); -@@ -1210,8 +1219,10 @@ class ProductTest extends \PHPUnit\Framework\TestCase - - public function testGetMediaGalleryImagesMerging() - { -- $mediaEntries = [ -- 'images' => [ -+ $mediaEntries = -+ [ -+ 'images' => -+ [ - [ - 'value_id' => 1, - 'file' => 'imageFile.jpg', -@@ -1226,24 +1237,28 @@ class ProductTest extends \PHPUnit\Framework\TestCase - 'file' => 'smallImageFile.jpg', - 'media_type' => 'image', - ], -- ] -- ]; -- $expectedImageDataObject = new \Magento\Framework\DataObject([ -+ ] -+ ]; -+ $expectedImageDataObject = new \Magento\Framework\DataObject( -+ [ - 'value_id' => 1, - 'file' => 'imageFile.jpg', - 'media_type' => 'image', - 'url' => 'http://magento.dev/pub/imageFile.jpg', - 'id' => 1, - 'path' => '/var/www/html/pub/imageFile.jpg', -- ]); -- $expectedSmallImageDataObject = new \Magento\Framework\DataObject([ -+ ] -+ ); -+ $expectedSmallImageDataObject = new \Magento\Framework\DataObject( -+ [ - 'value_id' => 2, - 'file' => 'smallImageFile.jpg', - 'media_type' => 'image', - 'url' => 'http://magento.dev/pub/smallImageFile.jpg', - 'id' => 2, - 'path' => '/var/www/html/pub/smallImageFile.jpg', -- ]); -+ ] -+ ); - - $directoryMock = $this->createMock(\Magento\Framework\Filesystem\Directory\ReadInterface::class); - $directoryMock->method('getAbsolutePath')->willReturnOnConsecutiveCalls( -@@ -1276,15 +1291,16 @@ class ProductTest extends \PHPUnit\Framework\TestCase - - public function testGetCustomAttributes() - { -- $interfaceAttributeCode = 'price'; -+ $priceCode = 'price'; - $customAttributeCode = 'color'; - $initialCustomAttributeValue = 'red'; - $newCustomAttributeValue = 'blue'; -- -- $this->getCustomAttributeCodes->expects($this->exactly(3)) -- ->method('execute') -- ->willReturn([$customAttributeCode]); -- $this->model->setData($interfaceAttributeCode, 10); -+ $customAttributesMetadata = [$priceCode => 'attribute1', $customAttributeCode => 'attribute2']; -+ $this->metadataServiceMock->expects($this->never())->method('getCustomAttributesMetadata'); -+ $this->eavConfig->expects($this->once()) -+ ->method('getEntityAttributes') -+ ->willReturn($customAttributesMetadata); -+ $this->model->setData($priceCode, 10); - - //The color attribute is not set, expect empty custom attribute array - $this->assertEquals([], $this->model->getCustomAttributes()); -diff --git a/app/code/Magento/Catalog/Test/Unit/Model/ProductTypes/Config/_files/invalidProductTypesXmlArray.php b/app/code/Magento/Catalog/Test/Unit/Model/ProductTypes/Config/_files/invalidProductTypesXmlArray.php -index e1847bea53f..868252da819 100644 ---- a/app/code/Magento/Catalog/Test/Unit/Model/ProductTypes/Config/_files/invalidProductTypesXmlArray.php -+++ b/app/code/Magento/Catalog/Test/Unit/Model/ProductTypes/Config/_files/invalidProductTypesXmlArray.php -@@ -23,7 +23,7 @@ return [ - '<?xml version="1.0"?><config><type name="some_name" modelInstance="123" /></config>', - [ - "Element 'type', attribute 'modelInstance': [facet 'pattern'] The value '123' is not accepted by the" . -- " pattern '[a-zA-Z_\\\\]+'.\nLine: 1\n", -+ " pattern '([\\\\]?[a-zA-Z_][a-zA-Z0-9_]*)+'.\nLine: 1\n", - "Element 'type', attribute 'modelInstance': '123' is not a valid value of the atomic type" . - " 'modelName'.\nLine: 1\n" - ], -@@ -57,7 +57,7 @@ return [ - '<?xml version="1.0"?><config><type name="some_name"><priceModel instance="123123" /></type></config>', - [ - "Element 'priceModel', attribute 'instance': [facet 'pattern'] The value '123123' is not accepted " . -- "by the pattern '[a-zA-Z_\\\\]+'.\nLine: 1\n", -+ "by the pattern '([\\\\]?[a-zA-Z_][a-zA-Z0-9_]*)+'.\nLine: 1\n", - "Element 'priceModel', attribute 'instance': '123123' is not a valid value of the atomic type" . - " 'modelName'.\nLine: 1\n" - ], -@@ -66,7 +66,7 @@ return [ - '<?xml version="1.0"?><config><type name="some_name"><indexerModel instance="123" /></type></config>', - [ - "Element 'indexerModel', attribute 'instance': [facet 'pattern'] The value '123' is not accepted by " . -- "the pattern '[a-zA-Z_\\\\]+'.\nLine: 1\n", -+ "the pattern '([\\\\]?[a-zA-Z_][a-zA-Z0-9_]*)+'.\nLine: 1\n", - "Element 'indexerModel', attribute 'instance': '123' is not a valid value of the atomic type" . - " 'modelName'.\nLine: 1\n" - ], -@@ -83,7 +83,7 @@ return [ - '<?xml version="1.0"?><config><type name="some_name"><stockIndexerModel instance="1234"/></type></config>', - [ - "Element 'stockIndexerModel', attribute 'instance': [facet 'pattern'] The value '1234' is not " . -- "accepted by the pattern '[a-zA-Z_\\\\]+'.\nLine: 1\n", -+ "accepted by the pattern '([\\\\]?[a-zA-Z_][a-zA-Z0-9_]*)+'.\nLine: 1\n", - "Element 'stockIndexerModel', attribute 'instance': '1234' is not a valid value of the atomic " . - "type 'modelName'.\nLine: 1\n" - ], -diff --git a/app/code/Magento/Catalog/Test/Unit/Model/ProductTypes/Config/_files/valid_product_types_merged.xml b/app/code/Magento/Catalog/Test/Unit/Model/ProductTypes/Config/_files/valid_product_types_merged.xml -index 7edbc399a94..701338774ba 100644 ---- a/app/code/Magento/Catalog/Test/Unit/Model/ProductTypes/Config/_files/valid_product_types_merged.xml -+++ b/app/code/Magento/Catalog/Test/Unit/Model/ProductTypes/Config/_files/valid_product_types_merged.xml -@@ -15,6 +15,14 @@ - <stockIndexerModel instance="instance_name"/> - </type> - <type label="some_label" name="some_name2" modelInstance="model_name"> -+ <allowedSelectionTypes> -+ <type name="some_name" /> -+ </allowedSelectionTypes> -+ <priceModel instance="instance_name_with_digits_123" /> -+ <indexerModel instance="instance_name_with_digits_123" /> -+ <stockIndexerModel instance="instance_name_with_digits_123"/> -+ </type> -+ <type label="some_label" name="some_name3" modelInstance="model_name"> - <allowedSelectionTypes> - <type name="some_name" /> - </allowedSelectionTypes> -@@ -25,5 +33,6 @@ - <composableTypes> - <type name="some_name"/> - <type name="some_name2"/> -+ <type name="some_name3"/> - </composableTypes> - </config> -diff --git a/app/code/Magento/Catalog/Test/Unit/Model/ResourceModel/Product/CategoryLinkTest.php b/app/code/Magento/Catalog/Test/Unit/Model/ResourceModel/Product/CategoryLinkTest.php -index 9e2b1966029..5a1a5906ec4 100644 ---- a/app/code/Magento/Catalog/Test/Unit/Model/ResourceModel/Product/CategoryLinkTest.php -+++ b/app/code/Magento/Catalog/Test/Unit/Model/ResourceModel/Product/CategoryLinkTest.php -@@ -3,6 +3,7 @@ - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -+ - namespace Magento\Catalog\Test\Unit\Model\ResourceModel\Product; - - use Magento\Catalog\Model\ResourceModel\Product\CategoryLink; -@@ -129,9 +130,20 @@ class CategoryLinkTest extends \PHPUnit\Framework\TestCase - ); - } - -+ $expectedResult = []; -+ -+ foreach ($affectedIds as $type => $ids) { -+ $expectedResult = array_merge($expectedResult, $ids); -+ // Verify if the correct insert, update and/or delete actions are performed: -+ $this->setupExpectationsForConnection($type, $ids); -+ } -+ - $actualResult = $this->model->saveCategoryLinks($product, $newCategoryLinks); -+ - sort($actualResult); -- $this->assertEquals($affectedIds, $actualResult); -+ sort($expectedResult); -+ -+ $this->assertEquals($expectedResult, $actualResult); - } - - /** -@@ -151,7 +163,11 @@ class CategoryLinkTest extends \PHPUnit\Framework\TestCase - ['category_id' => 3, 'position' => 10], - ['category_id' => 4, 'position' => 20], - ], -- [], // Nothing to update - data not changed -+ [ -+ 'update' => [], -+ 'insert' => [], -+ 'delete' => [], -+ ], - ], - [ - [ -@@ -162,7 +178,11 @@ class CategoryLinkTest extends \PHPUnit\Framework\TestCase - ['category_id' => 3, 'position' => 10], - ['category_id' => 4, 'position' => 20], - ], -- [3, 4, 5], // 4 - updated position, 5 - added, 3 - deleted -+ [ -+ 'update' => [4], -+ 'insert' => [5], -+ 'delete' => [3], -+ ], - ], - [ - [ -@@ -173,7 +193,11 @@ class CategoryLinkTest extends \PHPUnit\Framework\TestCase - ['category_id' => 3, 'position' => 10], - ['category_id' => 4, 'position' => 20], - ], -- [3, 4], // 3 - updated position, 4 - deleted -+ [ -+ 'update' => [3], -+ 'insert' => [], -+ 'delete' => [4], -+ ], - ], - [ - [], -@@ -181,8 +205,80 @@ class CategoryLinkTest extends \PHPUnit\Framework\TestCase - ['category_id' => 3, 'position' => 10], - ['category_id' => 4, 'position' => 20], - ], -- [3, 4], // 3, 4 - deleted -+ [ -+ 'update' => [], -+ 'insert' => [], -+ 'delete' => [3, 4], -+ ], - ], -+ [ -+ [ -+ ['category_id' => 3, 'position' => 10], -+ ['category_id' => 4, 'position' => 20], -+ ], -+ [ -+ ['category_id' => 3, 'position' => 20], // swapped positions -+ ['category_id' => 4, 'position' => 10], // swapped positions -+ ], -+ [ -+ 'update' => [3, 4], -+ 'insert' => [], -+ 'delete' => [], -+ ], -+ ] - ]; - } -+ -+ /** -+ * @param $type -+ * @param $ids -+ */ -+ private function setupExpectationsForConnection($type, $ids): void -+ { -+ switch ($type) { -+ case 'insert': -+ $this->connectionMock -+ ->expects($this->exactly(empty($ids) ? 0 : 1)) -+ ->method('insertArray') -+ ->with( -+ $this->anything(), -+ $this->anything(), -+ $this->callback(function ($data) use ($ids) { -+ $foundIds = []; -+ foreach ($data as $row) { -+ $foundIds[] = $row['category_id']; -+ } -+ return $ids === $foundIds; -+ }) -+ ); -+ break; -+ case 'update': -+ $this->connectionMock -+ ->expects($this->exactly(empty($ids) ? 0 : 1)) -+ ->method('insertOnDuplicate') -+ ->with( -+ $this->anything(), -+ $this->callback(function ($data) use ($ids) { -+ $foundIds = []; -+ foreach ($data as $row) { -+ $foundIds[] = $row['category_id']; -+ } -+ return $ids === $foundIds; -+ }) -+ ); -+ break; -+ case 'delete': -+ $this->connectionMock -+ ->expects($this->exactly(empty($ids) ? 0 : 1)) -+ ->method('delete') -+ // Verify that the correct category ID's are touched: -+ ->with( -+ $this->anything(), -+ $this->callback(function ($data) use ($ids) { -+ return array_values($data)[1] === $ids; -+ }) -+ ); -+ break; -+ } -+ } - } -diff --git a/app/code/Magento/Catalog/Test/Unit/Model/ResourceModel/Product/CollectionTest.php b/app/code/Magento/Catalog/Test/Unit/Model/ResourceModel/Product/CollectionTest.php -index dbbb3fb2951..6370a4a7a27 100644 ---- a/app/code/Magento/Catalog/Test/Unit/Model/ResourceModel/Product/CollectionTest.php -+++ b/app/code/Magento/Catalog/Test/Unit/Model/ResourceModel/Product/CollectionTest.php -@@ -58,13 +58,18 @@ class CollectionTest extends \PHPUnit\Framework\TestCase - */ - private $storeManager; - -+ /** -+ * @var \Magento\Framework\Data\Collection\EntityFactory|\PHPUnit_Framework_MockObject_MockObject -+ */ -+ private $entityFactory; -+ - /** - * @SuppressWarnings(PHPMD.ExcessiveMethodLength) - */ - protected function setUp() - { - $this->objectManager = new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this); -- $entityFactory = $this->createMock(\Magento\Framework\Data\Collection\EntityFactory::class); -+ $this->entityFactory = $this->createMock(\Magento\Framework\Data\Collection\EntityFactory::class); - $logger = $this->getMockBuilder(\Psr\Log\LoggerInterface::class) - ->disableOriginalConstructor() - ->getMockForAbstractClass(); -@@ -93,7 +98,7 @@ class CollectionTest extends \PHPUnit\Framework\TestCase - ->disableOriginalConstructor() - ->setMethods(['getStore', 'getId', 'getWebsiteId']) - ->getMockForAbstractClass(); -- $moduleManager = $this->getMockBuilder(\Magento\Framework\Module\Manager::class) -+ $moduleManager = $this->getMockBuilder(\Magento\Framework\Module\ModuleManagerInterface::class) - ->disableOriginalConstructor() - ->getMock(); - $catalogProductFlatState = $this->getMockBuilder(\Magento\Catalog\Model\Indexer\Product\Flat\State::class) -@@ -120,32 +125,25 @@ class CollectionTest extends \PHPUnit\Framework\TestCase - $groupManagement = $this->getMockBuilder(\Magento\Customer\Api\GroupManagementInterface::class) - ->disableOriginalConstructor() - ->getMockForAbstractClass(); -- - $this->connectionMock = $this->getMockBuilder(\Magento\Framework\DB\Adapter\AdapterInterface::class) - ->setMethods(['getId']) - ->disableOriginalConstructor() - ->getMockForAbstractClass(); -- - $this->selectMock = $this->getMockBuilder(\Magento\Framework\DB\Select::class) - ->disableOriginalConstructor() - ->getMock(); -- - $this->entityMock = $this->getMockBuilder(\Magento\Eav\Model\Entity\AbstractEntity::class) - ->disableOriginalConstructor() - ->getMock(); -- - $this->galleryResourceMock = $this->getMockBuilder( - \Magento\Catalog\Model\ResourceModel\Product\Gallery::class - )->disableOriginalConstructor()->getMock(); -- - $this->metadataPoolMock = $this->getMockBuilder( - \Magento\Framework\EntityManager\MetadataPool::class - )->disableOriginalConstructor()->getMock(); -- - $this->galleryReadHandlerMock = $this->getMockBuilder( - \Magento\Catalog\Model\Product\Gallery\ReadHandler::class - )->disableOriginalConstructor()->getMock(); -- - $this->storeManager->expects($this->any())->method('getId')->willReturn(1); - $this->storeManager->expects($this->any())->method('getStore')->willReturnSelf(); - $universalFactory->expects($this->exactly(1))->method('create')->willReturnOnConsecutiveCalls( -@@ -168,7 +166,7 @@ class CollectionTest extends \PHPUnit\Framework\TestCase - $this->collection = $this->objectManager->getObject( - \Magento\Catalog\Model\ResourceModel\Product\Collection::class, - [ -- 'entityFactory' => $entityFactory, -+ 'entityFactory' => $this->entityFactory, - 'logger' => $logger, - 'fetchStrategy' => $fetchStrategy, - 'eventManager' => $eventManager, -@@ -318,7 +316,7 @@ class CollectionTest extends \PHPUnit\Framework\TestCase - [ '(customer_group_id=? AND all_groups=0) OR all_groups=1', $customerGroupId] - ) - ->willReturnSelf(); -- $select->expects($this->once())->method('order')->with('entity_id')->willReturnSelf(); -+ $select->expects($this->once())->method('order')->with('qty')->willReturnSelf(); - $this->connectionMock->expects($this->once()) - ->method('fetchAll') - ->with($select) -@@ -370,7 +368,7 @@ class CollectionTest extends \PHPUnit\Framework\TestCase - $select->expects($this->exactly(1))->method('where') - ->with('entity_id IN(?)', [1]) - ->willReturnSelf(); -- $select->expects($this->once())->method('order')->with('entity_id')->willReturnSelf(); -+ $select->expects($this->once())->method('order')->with('qty')->willReturnSelf(); - $this->connectionMock->expects($this->once()) - ->method('fetchAll') - ->with($select) -@@ -379,4 +377,20 @@ class CollectionTest extends \PHPUnit\Framework\TestCase - - $this->assertSame($this->collection, $this->collection->addTierPriceData()); - } -+ -+ /** -+ * Test for getNewEmptyItem() method -+ * -+ * @return void -+ */ -+ public function testGetNewEmptyItem() -+ { -+ $item = $this->getMockBuilder(\Magento\Catalog\Model\Product::class) -+ ->disableOriginalConstructor() -+ ->getMock(); -+ $this->entityFactory->expects($this->once())->method('create')->willReturn($item); -+ $firstItem = $this->collection->getNewEmptyItem(); -+ $secondItem = $this->collection->getNewEmptyItem(); -+ $this->assertEquals($firstItem, $secondItem); -+ } - } -diff --git a/app/code/Magento/Catalog/Test/Unit/Model/ResourceModel/Product/GalleryTest.php b/app/code/Magento/Catalog/Test/Unit/Model/ResourceModel/Product/GalleryTest.php -index dfed4e4f373..47ef3c99912 100644 ---- a/app/code/Magento/Catalog/Test/Unit/Model/ResourceModel/Product/GalleryTest.php -+++ b/app/code/Magento/Catalog/Test/Unit/Model/ResourceModel/Product/GalleryTest.php -@@ -281,6 +281,9 @@ class GalleryTest extends \PHPUnit\Framework\TestCase - $this->resource->bindValueToEntity($valueId, $entityId); - } - -+ /** -+ * @SuppressWarnings(PHPMD.ExcessiveMethodLength) -+ */ - public function testLoadGallery() - { - $productId = 5; -@@ -329,7 +332,8 @@ class GalleryTest extends \PHPUnit\Framework\TestCase - 'main.value_id = entity.value_id', - ['entity_id'] - )->willReturnSelf(); -- $this->product->expects($this->at(0))->method('getData')->with('entity_id')->willReturn($productId); -+ $this->product->expects($this->at(0))->method('getData') -+ ->with('entity_id')->willReturn($productId); - $this->product->expects($this->at(1))->method('getStoreId')->will($this->returnValue($storeId)); - $this->connection->expects($this->exactly(2))->method('quoteInto')->withConsecutive( - ['value.store_id = ?'], -@@ -338,26 +342,50 @@ class GalleryTest extends \PHPUnit\Framework\TestCase - 'value.store_id = ' . $storeId, - 'default_value.store_id = ' . 0 - ); -+ $this->connection->expects($this->any())->method('getIfNullSql')->will( -+ $this->returnValueMap([ -+ [ -+ '`value`.`label`', -+ '`default_value`.`label`', -+ 'IFNULL(`value`.`label`, `default_value`.`label`)' -+ ], -+ [ -+ '`value`.`position`', -+ '`default_value`.`position`', -+ 'IFNULL(`value`.`position`, `default_value`.`position`)' -+ ], -+ [ -+ '`value`.`disabled`', -+ '`default_value`.`disabled`', -+ 'IFNULL(`value`.`disabled`, `default_value`.`disabled`)' -+ ] -+ ]) -+ ); - $this->select->expects($this->at(2))->method('joinLeft')->with( - ['value' => $getTableReturnValue], - $quoteInfoReturnValue, -- [ -- 'label', -- 'position', -- 'disabled' -- ] -+ [] - )->willReturnSelf(); - $this->select->expects($this->at(3))->method('joinLeft')->with( - ['default_value' => $getTableReturnValue], - $quoteDefaultInfoReturnValue, -- ['label_default' => 'label', 'position_default' => 'position', 'disabled_default' => 'disabled'] -+ [] - )->willReturnSelf(); -- $this->select->expects($this->at(4))->method('where')->with( -+ $this->select->expects($this->at(4))->method('columns')->with([ -+ 'label' => 'IFNULL(`value`.`label`, `default_value`.`label`)', -+ 'position' => 'IFNULL(`value`.`position`, `default_value`.`position`)', -+ 'disabled' => 'IFNULL(`value`.`disabled`, `default_value`.`disabled`)', -+ 'label_default' => 'default_value.label', -+ 'position_default' => 'default_value.position', -+ 'disabled_default' => 'default_value.disabled' -+ ])->willReturnSelf(); -+ $this->select->expects($this->at(5))->method('where')->with( - 'main.attribute_id = ?', - $attributeId - )->willReturnSelf(); -- $this->select->expects($this->at(5))->method('where')->with('main.disabled = 0')->willReturnSelf(); -- $this->select->expects($this->at(7))->method('where') -+ $this->select->expects($this->at(6))->method('where') -+ ->with('main.disabled = 0')->willReturnSelf(); -+ $this->select->expects($this->at(8))->method('where') - ->with('entity.entity_id = ?', $productId) - ->willReturnSelf(); - $this->select->expects($this->once())->method('order') -diff --git a/app/code/Magento/Catalog/Test/Unit/Model/ResourceModel/Product/ImageTest.php b/app/code/Magento/Catalog/Test/Unit/Model/ResourceModel/Product/ImageTest.php -new file mode 100644 -index 00000000000..af2cb6f06ed ---- /dev/null -+++ b/app/code/Magento/Catalog/Test/Unit/Model/ResourceModel/Product/ImageTest.php -@@ -0,0 +1,358 @@ -+<?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 -+ * @package Magento\Catalog\Test\Unit\Model\ResourceModel\Product -+ */ -+class ImageTest extends \PHPUnit\Framework\TestCase -+{ -+ /** -+ * @var \Magento\Framework\TestFramework\Unit\Helper\ObjectManager -+ */ -+ protected $objectManager; -+ -+ /** -+ * @var AdapterInterface | MockObject -+ */ -+ protected $connectionMock; -+ -+ /** -+ * @var Generator | MockObject -+ */ -+ protected $generatorMock; -+ -+ /** -+ * @var ResourceConnection | MockObject -+ */ -+ protected $resourceMock; -+ -+ protected function setUp(): void -+ { -+ $this->objectManager = -+ new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this); -+ $this->connectionMock = $this->createMock(AdapterInterface::class); -+ $this->resourceMock = $this->createMock(ResourceConnection::class); -+ $this->resourceMock->method('getConnection') -+ ->willReturn($this->connectionMock); -+ $this->resourceMock->method('getTableName') -+ ->willReturnArgument(0); -+ $this->generatorMock = $this->createMock(Generator::class); -+ } -+ -+ /** -+ * @return MockObject -+ */ -+ protected function getVisibleImagesSelectMock(): MockObject -+ { -+ $selectMock = $this->getMockBuilder(Select::class) -+ ->disableOriginalConstructor() -+ ->getMock(); -+ $selectMock->expects($this->once()) -+ ->method('distinct') -+ ->willReturnSelf(); -+ $selectMock->expects($this->once()) -+ ->method('from') -+ ->with( -+ ['images' => Gallery::GALLERY_TABLE], -+ 'value as filepath' -+ )->willReturnSelf(); -+ $selectMock->expects($this->once()) -+ ->method('where') -+ ->with('disabled = 0') -+ ->willReturnSelf(); -+ -+ return $selectMock; -+ } -+ -+ /** -+ * @return MockObject -+ */ -+ protected function getUsedImagesSelectMock(): 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('joinInner') -+ ->with( -+ ['image_value' => Gallery::GALLERY_VALUE_TABLE], -+ 'images.value_id = image_value.value_id' -+ )->willReturnSelf(); -+ $selectMock->expects($this->once()) -+ ->method('where') -+ ->with('images.disabled = 0 AND image_value.disabled = 0') -+ ->willReturnSelf(); -+ -+ return $selectMock; -+ } -+ -+ /** -+ * @param int $imagesCount -+ * @dataProvider dataProvider -+ */ -+ public function testGetCountAllProductImages(int $imagesCount): void -+ { -+ $selectMock = $this->getVisibleImagesSelectMock(); -+ $selectMock->expects($this->exactly(2)) -+ ->method('reset') -+ ->withConsecutive( -+ ['columns'], -+ ['distinct'] -+ )->willReturnSelf(); -+ $selectMock->expects($this->once()) -+ ->method('columns') -+ ->with(new \Zend_Db_Expr('count(distinct value)')) -+ ->willReturnSelf(); -+ -+ $this->connectionMock->expects($this->once()) -+ ->method('select') -+ ->willReturn($selectMock); -+ $this->connectionMock->expects($this->once()) -+ ->method('fetchOne') -+ ->with($selectMock) -+ ->willReturn($imagesCount); -+ -+ $imageModel = $this->objectManager->getObject( -+ Image::class, -+ [ -+ 'generator' => $this->generatorMock, -+ 'resourceConnection' => $this->resourceMock -+ ] -+ ); -+ -+ $this->assertSame( -+ $imagesCount, -+ $imageModel->getCountAllProductImages() -+ ); -+ } -+ -+ /** -+ * @param int $imagesCount -+ * @dataProvider dataProvider -+ */ -+ public function testGetCountUsedProductImages(int $imagesCount): void -+ { -+ $selectMock = $this->getUsedImagesSelectMock(); -+ $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->getCountUsedProductImages() -+ ); -+ } -+ -+ /** -+ * @param int $imagesCount -+ * @param int $batchSize -+ * @dataProvider dataProvider -+ */ -+ public function testGetAllProductImages(int $imagesCount, int $batchSize): void -+ { -+ $this->connectionMock->expects($this->once()) -+ ->method('select') -+ ->willReturn($this->getVisibleImagesSelectMock()); -+ -+ $batchCount = (int)ceil($imagesCount / $batchSize); -+ $fetchResultsCallback = $this->getFetchResultCallbackForBatches($imagesCount, $batchSize); -+ $this->connectionMock->expects($this->exactly($batchCount)) -+ ->method('fetchAll') -+ ->will($this->returnCallback($fetchResultsCallback)); -+ -+ /** @var Select | MockObject $selectMock */ -+ $selectMock = $this->getMockBuilder(Select::class) -+ ->disableOriginalConstructor() -+ ->getMock(); -+ -+ $this->generatorMock->expects($this->once()) -+ ->method('generate') -+ ->with( -+ 'value_id', -+ $selectMock, -+ $batchSize, -+ BatchIteratorInterface::NON_UNIQUE_FIELD_ITERATOR -+ )->will( -+ $this->returnCallback( -+ $this->getBatchIteratorCallback($selectMock, $batchCount) -+ ) -+ ); -+ -+ $imageModel = $this->objectManager->getObject( -+ Image::class, -+ [ -+ 'generator' => $this->generatorMock, -+ 'resourceConnection' => $this->resourceMock, -+ 'batchSize' => $batchSize -+ ] -+ ); -+ -+ $this->assertCount($imagesCount, $imageModel->getAllProductImages()); -+ } -+ -+ /** -+ * @param int $imagesCount -+ * @param int $batchSize -+ * @dataProvider dataProvider -+ */ -+ public function testGetUsedProductImages(int $imagesCount, int $batchSize): void -+ { -+ $this->connectionMock->expects($this->once()) -+ ->method('select') -+ ->willReturn($this->getUsedImagesSelectMock()); -+ -+ $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) -+ ) -+ ); -+ -+ /** @var Image $imageModel */ -+ $imageModel = $this->objectManager->getObject( -+ Image::class, -+ [ -+ 'generator' => $this->generatorMock, -+ 'resourceConnection' => $this->resourceMock, -+ 'batchSize' => $batchSize -+ ] -+ ); -+ -+ $this->assertCount($imagesCount, $imageModel->getUsedProductImages()); -+ } -+ -+ /** -+ * @param int $imagesCount -+ * @param int $batchSize -+ * @return \Closure -+ */ -+ protected function getFetchResultCallbackForBatches( -+ int $imagesCount, -+ int $batchSize -+ ): \Closure { -+ $fetchResultsCallback = function () use (&$imagesCount, $batchSize) { -+ $batchSize = -+ ($imagesCount >= $batchSize) ? $batchSize : $imagesCount; -+ $imagesCount -= $batchSize; -+ -+ $getFetchResults = function ($batchSize): array { -+ $result = []; -+ $count = $batchSize; -+ while ($count) { -+ $count--; -+ $result[$count] = $count; -+ } -+ -+ return $result; -+ }; -+ -+ return $getFetchResults($batchSize); -+ }; -+ -+ return $fetchResultsCallback; -+ } -+ -+ /** -+ * @param Select | MockObject $selectMock -+ * @param int $batchCount -+ * @return \Closure -+ */ -+ protected function getBatchIteratorCallback( -+ MockObject $selectMock, -+ int $batchCount -+ ): \Closure { -+ $iteratorCallback = function () use ($batchCount, $selectMock): array { -+ $result = []; -+ $count = $batchCount; -+ while ($count) { -+ $count--; -+ $result[$count] = $selectMock; -+ } -+ -+ return $result; -+ }; -+ -+ return $iteratorCallback; -+ } -+ -+ /** -+ * Data Provider -+ * @return array -+ */ -+ public function dataProvider(): array -+ { -+ return [ -+ [300, 300], -+ [300, 100], -+ [139, 100], -+ [67, 10], -+ [154, 47], -+ [0, 100] -+ ]; -+ } -+} -diff --git a/app/code/Magento/Catalog/Test/Unit/Model/ResourceModel/Product/Indexer/Price/CompositeProductRowSizeEstimatorTest.php b/app/code/Magento/Catalog/Test/Unit/Model/ResourceModel/Product/Indexer/Price/CompositeProductRowSizeEstimatorTest.php -index 1c476443381..728044b89ca 100644 ---- a/app/code/Magento/Catalog/Test/Unit/Model/ResourceModel/Product/Indexer/Price/CompositeProductRowSizeEstimatorTest.php -+++ b/app/code/Magento/Catalog/Test/Unit/Model/ResourceModel/Product/Indexer/Price/CompositeProductRowSizeEstimatorTest.php -@@ -49,7 +49,6 @@ class CompositeProductRowSizeEstimatorTest extends \PHPUnit\Framework\TestCase - - public function testEstimateRowSize() - { -- $this->markTestSkipped('Unskip after MAGETWO-89738'); - $expectedResult = 40000000; - $maxRelatedProductCount = 10; - -diff --git a/app/code/Magento/Catalog/Test/Unit/Model/ResourceModel/Product/StatusBaseSelectProcessorTest.php b/app/code/Magento/Catalog/Test/Unit/Model/ResourceModel/Product/StatusBaseSelectProcessorTest.php -index a21883eb4a1..ee487041600 100644 ---- a/app/code/Magento/Catalog/Test/Unit/Model/ResourceModel/Product/StatusBaseSelectProcessorTest.php -+++ b/app/code/Magento/Catalog/Test/Unit/Model/ResourceModel/Product/StatusBaseSelectProcessorTest.php -@@ -16,7 +16,7 @@ use Magento\Framework\DB\Select; - use Magento\Framework\EntityManager\EntityMetadataInterface; - use Magento\Framework\EntityManager\MetadataPool; - use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; --use Magento\Store\Api\StoreResolverInterface; -+use Magento\Store\Model\StoreManagerInterface; - use Magento\Store\Model\Store; - - /** -@@ -35,9 +35,9 @@ class StatusBaseSelectProcessorTest extends \PHPUnit\Framework\TestCase - private $metadataPool; - - /** -- * @var StoreResolverInterface|\PHPUnit_Framework_MockObject_MockObject -+ * @var StoreManagerInterface|\PHPUnit_Framework_MockObject_MockObject - */ -- private $storeResolver; -+ private $storeManager; - - /** - * @var Select|\PHPUnit_Framework_MockObject_MockObject -@@ -53,13 +53,13 @@ class StatusBaseSelectProcessorTest extends \PHPUnit\Framework\TestCase - { - $this->eavConfig = $this->getMockBuilder(Config::class)->disableOriginalConstructor()->getMock(); - $this->metadataPool = $this->getMockBuilder(MetadataPool::class)->disableOriginalConstructor()->getMock(); -- $this->storeResolver = $this->getMockBuilder(StoreResolverInterface::class)->getMock(); -+ $this->storeManager = $this->getMockBuilder(StoreManagerInterface::class)->getMock(); - $this->select = $this->getMockBuilder(Select::class)->disableOriginalConstructor()->getMock(); - - $this->statusBaseSelectProcessor = (new ObjectManager($this))->getObject(StatusBaseSelectProcessor::class, [ - 'eavConfig' => $this->eavConfig, - 'metadataPool' => $this->metadataPool, -- 'storeResolver' => $this->storeResolver, -+ 'storeManager' => $this->storeManager, - ]); - } - -@@ -94,8 +94,14 @@ class StatusBaseSelectProcessorTest extends \PHPUnit\Framework\TestCase - ->with(Product::ENTITY, ProductInterface::STATUS) - ->willReturn($statusAttribute); - -- $this->storeResolver->expects($this->once()) -- ->method('getCurrentStoreId') -+ $storeMock = $this->getMockBuilder(\Magento\Store\Api\Data\StoreInterface::class)->getMock(); -+ -+ $this->storeManager->expects($this->once()) -+ ->method('getStore') -+ ->willReturn($storeMock); -+ -+ $storeMock->expects($this->once()) -+ ->method('getId') - ->willReturn($currentStoreId); - - $this->select->expects($this->at(0)) -diff --git a/app/code/Magento/Catalog/Test/Unit/Model/_files/converted_view.php b/app/code/Magento/Catalog/Test/Unit/Model/_files/converted_view.php -index 37b0e15cac6..e225ec0daef 100644 ---- a/app/code/Magento/Catalog/Test/Unit/Model/_files/converted_view.php -+++ b/app/code/Magento/Catalog/Test/Unit/Model/_files/converted_view.php -@@ -11,7 +11,41 @@ return [ - "type" => "swatch_thumb", - "width" => 75, - "height" => 75, -- "background" => [255, 25, 2] -+ "constrain" => false, -+ "aspect_ratio" => false, -+ "frame" => false, -+ "transparency" => false, -+ "background" => [255, 25, 2], -+ ], -+ "swatch_thumb_medium" => [ -+ "type" => "swatch_medium", -+ "width" => 750, -+ "height" => 750, -+ "constrain" => true, -+ "aspect_ratio" => true, -+ "frame" => true, -+ "transparency" => true, -+ "background" => [255, 25, 2], -+ ], -+ "swatch_thumb_large" => [ -+ "type" => "swatch_large", -+ "width" => 1080, -+ "height" => 720, -+ "constrain" => false, -+ "aspect_ratio" => false, -+ "frame" => false, -+ "transparency" => false, -+ "background" => [255, 25, 2], -+ ], -+ "swatch_thumb_small" => [ -+ "type" => "swatch_small", -+ "width" => 100, -+ "height" => 100, -+ "constrain" => true, -+ "aspect_ratio" => true, -+ "frame" => true, -+ "transparency" => true, -+ "background" => [255, 25, 2], - ] - ] - ] -diff --git a/app/code/Magento/Catalog/Test/Unit/Model/_files/valid_view.xml b/app/code/Magento/Catalog/Test/Unit/Model/_files/valid_view.xml -index 253abc5e2e4..ee4ddaad534 100644 ---- a/app/code/Magento/Catalog/Test/Unit/Model/_files/valid_view.xml -+++ b/app/code/Magento/Catalog/Test/Unit/Model/_files/valid_view.xml -@@ -11,6 +11,37 @@ - <image id="swatch_thumb_base" type="swatch_thumb"> - <width>75</width> - <height>75</height> -+ <constrain>false</constrain> -+ <aspect_ratio>false</aspect_ratio> -+ <frame>false</frame> -+ <transparency>false</transparency> -+ <background>[255, 25, 2]</background> -+ </image> -+ <image id="swatch_thumb_medium" type="swatch_medium"> -+ <width>750</width> -+ <height>750</height> -+ <constrain>true</constrain> -+ <aspect_ratio>true</aspect_ratio> -+ <frame>true</frame> -+ <transparency>true</transparency> -+ <background>[255, 25, 2]</background> -+ </image> -+ <image id="swatch_thumb_large" type="swatch_large"> -+ <width>1080</width> -+ <height>720</height> -+ <constrain>0</constrain> -+ <aspect_ratio>0</aspect_ratio> -+ <frame>0</frame> -+ <transparency>0</transparency> -+ <background>[255, 25, 2]</background> -+ </image> -+ <image id="swatch_thumb_small" type="swatch_small"> -+ <width>100</width> -+ <height>100</height> -+ <constrain>1</constrain> -+ <aspect_ratio>1</aspect_ratio> -+ <frame>1</frame> -+ <transparency>1</transparency> - <background>[255, 25, 2]</background> - </image> - </images> -diff --git a/app/code/Magento/Catalog/Test/Unit/Plugin/Block/TopmenuTest.php b/app/code/Magento/Catalog/Test/Unit/Plugin/Block/TopmenuTest.php -index 2d67db77d43..c5a3e5dab76 100644 ---- a/app/code/Magento/Catalog/Test/Unit/Plugin/Block/TopmenuTest.php -+++ b/app/code/Magento/Catalog/Test/Unit/Plugin/Block/TopmenuTest.php -@@ -87,7 +87,7 @@ class TopmenuTest extends \PHPUnit\Framework\TestCase - \Magento\Catalog\Model\ResourceModel\Category\Collection::class - ); - $this->categoryCollectionFactoryMock = $this->createPartialMock( -- \Magento\Catalog\Model\ResourceModel\Category\CollectionFactory::class, -+ \Magento\Catalog\Model\ResourceModel\Category\StateDependentCollectionFactory::class, - ['create'] - ); - -diff --git a/app/code/Magento/Catalog/Test/Unit/Plugin/Model/Attribute/Backend/AttributeValidationTest.php b/app/code/Magento/Catalog/Test/Unit/Plugin/Model/Attribute/Backend/AttributeValidationTest.php -new file mode 100644 -index 00000000000..463ecf88197 ---- /dev/null -+++ b/app/code/Magento/Catalog/Test/Unit/Plugin/Model/Attribute/Backend/AttributeValidationTest.php -@@ -0,0 +1,154 @@ -+<?php -+/** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+declare(strict_types=1); -+ -+namespace Magento\Catalog\Test\Unit\Plugin\Model\Attribute\Backend; -+ -+use Magento\Catalog\Plugin\Model\Attribute\Backend\AttributeValidation; -+use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; -+use Magento\Store\Model\StoreManagerInterface; -+use Magento\Store\Api\Data\StoreInterface; -+use Magento\Eav\Model\Entity\Attribute\Backend\AbstractBackend; -+use Magento\Eav\Model\Entity\Attribute\AbstractAttribute; -+use Magento\Framework\DataObject; -+ -+class AttributeValidationTest extends \PHPUnit\Framework\TestCase -+{ -+ /** -+ * @var AttributeValidation -+ */ -+ private $attributeValidation; -+ -+ /** -+ * @var StoreManagerInterface|\PHPUnit_Framework_MockObject_MockObject -+ */ -+ private $storeManagerMock; -+ -+ /** -+ * @var StoreInterface|\PHPUnit_Framework_MockObject_MockObject -+ */ -+ private $storeMock; -+ -+ /** -+ * @var array -+ */ -+ private $allowedEntityTypes; -+ -+ /** -+ * @var \Callable -+ */ -+ private $proceedMock; -+ -+ /** -+ * @var bool -+ */ -+ private $isProceedMockCalled = false; -+ -+ /** -+ * @var AbstractBackend|\PHPUnit_Framework_MockObject_MockObject -+ */ -+ private $subjectMock; -+ -+ /** -+ * @var AbstractAttribute|\PHPUnit_Framework_MockObject_MockObject -+ */ -+ private $attributeMock; -+ -+ /** -+ * @var DataObject|\PHPUnit_Framework_MockObject_MockObject -+ */ -+ private $entityMock; -+ -+ protected function setUp() -+ { -+ $objectManager = new ObjectManager($this); -+ -+ $this->attributeMock = $this->getMockBuilder(AbstractBackend::class) -+ ->setMethods(['getAttributeCode']) -+ ->getMockForAbstractClass(); -+ $this->subjectMock = $this->getMockBuilder(AbstractBackend::class) -+ ->setMethods(['getAttribute']) -+ ->getMockForAbstractClass(); -+ $this->subjectMock->expects($this->any()) -+ ->method('getAttribute') -+ ->willReturn($this->attributeMock); -+ -+ $this->storeMock = $this->getMockBuilder(StoreInterface::class) -+ ->setMethods(['getId']) -+ ->getMockForAbstractClass(); -+ $this->storeManagerMock = $this->getMockBuilder(StoreManagerInterface::class) -+ ->setMethods(['getStore']) -+ ->getMockForAbstractClass(); -+ $this->storeManagerMock->expects($this->any()) -+ ->method('getStore') -+ ->willReturn($this->storeMock); -+ -+ $this->entityMock = $this->getMockBuilder(DataObject::class) -+ ->setMethods(['getData']) -+ ->getMock(); -+ -+ $this->allowedEntityTypes = [$this->entityMock]; -+ -+ $this->proceedMock = function () { -+ $this->isProceedMockCalled = true; -+ }; -+ -+ $this->attributeValidation = $objectManager->getObject( -+ AttributeValidation::class, -+ [ -+ 'storeManager' => $this->storeManagerMock, -+ 'allowedEntityTypes' => $this->allowedEntityTypes, -+ ] -+ ); -+ } -+ -+ /** -+ * @param bool $shouldProceedRun -+ * @param bool $defaultStoreUsed -+ * @param null|int|string $storeId -+ * @dataProvider aroundValidateDataProvider -+ * @throws \Magento\Framework\Exception\NoSuchEntityException -+ * @return void -+ */ -+ public function testAroundValidate(bool $shouldProceedRun, bool $defaultStoreUsed, $storeId) -+ { -+ $this->isProceedMockCalled = false; -+ $attributeCode = 'code'; -+ -+ $this->storeMock->expects($this->once()) -+ ->method('getId') -+ ->willReturn($storeId); -+ if ($defaultStoreUsed) { -+ $this->attributeMock->expects($this->once()) -+ ->method('getAttributeCode') -+ ->willReturn($attributeCode); -+ $this->entityMock->expects($this->at(0)) -+ ->method('getData') -+ ->willReturn([$attributeCode => null]); -+ $this->entityMock->expects($this->at(1)) -+ ->method('getData') -+ ->with($attributeCode) -+ ->willReturn(null); -+ } -+ -+ $this->attributeValidation->aroundValidate($this->subjectMock, $this->proceedMock, $this->entityMock); -+ $this->assertSame($shouldProceedRun, $this->isProceedMockCalled); -+ } -+ -+ /** -+ * Data provider for testAroundValidate -+ * @return array -+ */ -+ public function aroundValidateDataProvider(): array -+ { -+ return [ -+ [true, false, '0'], -+ [true, false, 0], -+ [true, false, null], -+ [false, true, 1], -+ ]; -+ } -+} -diff --git a/app/code/Magento/Catalog/Test/Unit/Pricing/Price/TierPriceTest.php b/app/code/Magento/Catalog/Test/Unit/Pricing/Price/TierPriceTest.php -index 78041044909..ade8829b278 100644 ---- a/app/code/Magento/Catalog/Test/Unit/Pricing/Price/TierPriceTest.php -+++ b/app/code/Magento/Catalog/Test/Unit/Pricing/Price/TierPriceTest.php -@@ -393,4 +393,40 @@ class TierPriceTest extends \PHPUnit\Framework\TestCase - ['basePrice' => '20.80', 'tierPrice' => '18.72', 'savedPercent' => '10'] - ]; - } -+ -+ /** -+ * @param null|string|float $quantity -+ * @param float $expectedValue -+ * @dataProvider getQuantityDataProvider -+ */ -+ public function testGetQuantity($quantity, $expectedValue) -+ { -+ $tierPrice = new TierPrice( -+ $this->product, -+ $quantity, -+ $this->calculator, -+ $this->priceCurrencyMock, -+ $this->session, -+ $this->groupManagement, -+ $this->customerGroupRetriever -+ ); -+ -+ $this->assertEquals($expectedValue, $tierPrice->getQuantity()); -+ } -+ -+ /** -+ * @return array -+ */ -+ public function getQuantityDataProvider() -+ { -+ return [ -+ [null, 1], -+ ['one', 1], -+ ['', 1], -+ [4, 4], -+ [4.5, 4.5], -+ ['0.7', 0.7], -+ ['0.0000000', 1] -+ ]; -+ } - } -diff --git a/app/code/Magento/Catalog/Test/Unit/Ui/Component/ColumnFactoryTest.php b/app/code/Magento/Catalog/Test/Unit/Ui/Component/ColumnFactoryTest.php -new file mode 100644 -index 00000000000..774edcfeb6b ---- /dev/null -+++ b/app/code/Magento/Catalog/Test/Unit/Ui/Component/ColumnFactoryTest.php -@@ -0,0 +1,156 @@ -+<?php -+/** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+declare(strict_types=1); -+ -+namespace Magento\Catalog\Test\Unit\Ui\Component; -+ -+use PHPUnit\Framework\TestCase; -+use Magento\Catalog\Ui\Component\ColumnFactory; -+use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; -+use Magento\Catalog\Api\Data\ProductAttributeInterface; -+use Magento\Framework\View\Element\UiComponent\ContextInterface; -+use Magento\Framework\View\Element\UiComponentFactory; -+use Magento\Ui\Component\Listing\Columns\ColumnInterface; -+use Magento\Ui\Component\Filters\FilterModifier; -+ -+/** -+ * ColumnFactory test. -+ */ -+class ColumnFactoryTest extends TestCase -+{ -+ /** -+ * @var ColumnFactory -+ */ -+ private $columnFactory; -+ -+ /** -+ * @var ObjectManager -+ */ -+ private $objectManager; -+ -+ /** -+ * @var ProductAttributeInterface|\PHPUnit\Framework\MockObject\MockObject -+ */ -+ private $attribute; -+ -+ /** -+ * @var ContextInterface|\PHPUnit\Framework\MockObject\MockObject -+ */ -+ private $context; -+ -+ /** -+ * @var UiComponentFactory|\PHPUnit\Framework\MockObject\MockObject -+ */ -+ private $uiComponentFactory; -+ -+ /** -+ * @var ColumnInterface|\PHPUnit\Framework\MockObject\MockObject -+ */ -+ private $column; -+ -+ /** -+ * @inheritdoc -+ */ -+ protected function setUp(): void -+ { -+ $this->objectManager = new ObjectManager($this); -+ -+ $this->attribute = $this->getMockBuilder(ProductAttributeInterface::class) -+ ->setMethods(['usesSource']) -+ ->getMockForAbstractClass(); -+ $this->context = $this->createMock(ContextInterface::class); -+ $this->uiComponentFactory = $this->createMock(UiComponentFactory::class); -+ $this->column = $this->getMockForAbstractClass(ColumnInterface::class); -+ $this->uiComponentFactory->method('create') -+ ->willReturn($this->column); -+ -+ $this->columnFactory = $this->objectManager->getObject(ColumnFactory::class, [ -+ 'componentFactory' => $this->uiComponentFactory -+ ]); -+ } -+ -+ /** -+ * Tests the create method will return correct object. -+ * -+ * @return void -+ */ -+ public function testCreatedObject(): void -+ { -+ $this->context->method('getRequestParam') -+ ->with(FilterModifier::FILTER_MODIFIER, []) -+ ->willReturn([]); -+ -+ $object = $this->columnFactory->create($this->attribute, $this->context); -+ $this->assertEquals( -+ $this->column, -+ $object, -+ 'Object must be the same which the ui component factory creates.' -+ ); -+ } -+ -+ /** -+ * Tests create method with not filterable in grid attribute. -+ * -+ * @param array $filterModifiers -+ * @param null|string $filter -+ * -+ * @return void -+ * @dataProvider filterModifiersProvider -+ */ -+ public function testCreateWithNotFilterableInGridAttribute(array $filterModifiers, ?string $filter): void -+ { -+ $componentFactoryArgument = [ -+ 'data' => [ -+ 'config' => [ -+ 'label' => __(null), -+ 'dataType' => 'text', -+ 'add_field' => true, -+ 'visible' => null, -+ 'filter' => $filter, -+ 'component' => 'Magento_Ui/js/grid/columns/column', -+ ], -+ ], -+ 'context' => $this->context, -+ ]; -+ -+ $this->context->method('getRequestParam') -+ ->with(FilterModifier::FILTER_MODIFIER, []) -+ ->willReturn($filterModifiers); -+ $this->attribute->method('getIsFilterableInGrid') -+ ->willReturn(false); -+ $this->attribute->method('getAttributeCode') -+ ->willReturn('color'); -+ -+ $this->uiComponentFactory->expects($this->once()) -+ ->method('create') -+ ->with($this->anything(), $this->anything(), $componentFactoryArgument); -+ -+ $this->columnFactory->create($this->attribute, $this->context); -+ } -+ -+ /** -+ * Filter modifiers data provider. -+ * -+ * @return array -+ */ -+ public function filterModifiersProvider(): array -+ { -+ return [ -+ 'without' => [ -+ 'filter_modifiers' => [], -+ 'filter' => null, -+ ], -+ 'with' => [ -+ 'filter_modifiers' => [ -+ 'color' => [ -+ 'condition_type' => 'notnull', -+ ], -+ ], -+ 'filter' => 'text', -+ ], -+ ]; -+ } -+} -diff --git a/app/code/Magento/Catalog/Test/Unit/Ui/DataProvider/Product/Form/Modifier/AdvancedPricingTest.php b/app/code/Magento/Catalog/Test/Unit/Ui/DataProvider/Product/Form/Modifier/AdvancedPricingTest.php -index 1a23aaace6e..e9f9349100f 100644 ---- a/app/code/Magento/Catalog/Test/Unit/Ui/DataProvider/Product/Form/Modifier/AdvancedPricingTest.php -+++ b/app/code/Magento/Catalog/Test/Unit/Ui/DataProvider/Product/Form/Modifier/AdvancedPricingTest.php -@@ -11,7 +11,7 @@ use Magento\Customer\Api\Data\GroupInterface as CustomerGroupInterface; - use Magento\Customer\Api\GroupManagementInterface; - use Magento\Customer\Api\GroupRepositoryInterface; - use Magento\Framework\Api\SearchCriteriaBuilder; --use Magento\Framework\Module\Manager as ModuleManager; -+use \Magento\Framework\Module\ModuleManagerInterface as ModuleManager; - use Magento\Directory\Helper\Data as DirectoryHelper; - use Magento\Catalog\Model\ResourceModel\Product as ProductResource; - use Magento\Catalog\Model\ResourceModel\Eav\Attribute; -@@ -106,7 +106,9 @@ class AdvancedPricingTest extends AbstractModifierTest - */ - protected function createModel() - { -- return $this->objectManager->getObject(AdvancedPricing::class, [ -+ return $this->objectManager->getObject( -+ AdvancedPricing::class, -+ [ - 'locator' => $this->locatorMock, - 'storeManager' => $this->storeManagerMock, - 'groupRepository' => $this->groupRepositoryMock, -@@ -114,7 +116,8 @@ class AdvancedPricingTest extends AbstractModifierTest - 'searchCriteriaBuilder' => $this->searchCriteriaBuilderMock, - 'moduleManager' => $this->moduleManagerMock, - 'directoryHelper' => $this->directoryHelperMock -- ]); -+ ] -+ ); - } - - public function testModifyMeta() -diff --git a/app/code/Magento/Catalog/Test/Unit/Ui/DataProvider/Product/Form/Modifier/CategoriesTest.php b/app/code/Magento/Catalog/Test/Unit/Ui/DataProvider/Product/Form/Modifier/CategoriesTest.php -index cd6565f32ed..932b09f7df9 100644 ---- a/app/code/Magento/Catalog/Test/Unit/Ui/DataProvider/Product/Form/Modifier/CategoriesTest.php -+++ b/app/code/Magento/Catalog/Test/Unit/Ui/DataProvider/Product/Form/Modifier/CategoriesTest.php -@@ -3,6 +3,8 @@ - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -+declare(strict_types=1); -+ - namespace Magento\Catalog\Test\Unit\Ui\DataProvider\Product\Form\Modifier; - - use Magento\Catalog\Ui\DataProvider\Product\Form\Modifier\Categories; -@@ -12,6 +14,7 @@ use Magento\Framework\App\CacheInterface; - use Magento\Framework\DB\Helper as DbHelper; - use Magento\Framework\UrlInterface; - use Magento\Store\Model\Store; -+use Magento\Framework\AuthorizationInterface; - - /** - * Class CategoriesTest -@@ -45,6 +48,11 @@ class CategoriesTest extends AbstractModifierTest - */ - protected $categoryCollectionMock; - -+ /** -+ * @var AuthorizationInterface|\PHPUnit_Framework_MockObject_MockObject -+ */ -+ private $authorizationMock; -+ - protected function setUp() - { - parent::setUp(); -@@ -63,6 +71,9 @@ class CategoriesTest extends AbstractModifierTest - $this->categoryCollectionMock = $this->getMockBuilder(CategoryCollection::class) - ->disableOriginalConstructor() - ->getMock(); -+ $this->authorizationMock = $this->getMockBuilder(AuthorizationInterface::class) -+ ->disableOriginalConstructor() -+ ->getMock(); - - $this->categoryCollectionFactoryMock->expects($this->any()) - ->method('create') -@@ -86,11 +97,15 @@ class CategoriesTest extends AbstractModifierTest - */ - protected function createModel() - { -- return $this->objectManager->getObject(Categories::class, [ -- 'locator' => $this->locatorMock, -- 'categoryCollectionFactory' => $this->categoryCollectionFactoryMock, -- 'arrayManager' => $this->arrayManagerMock, -- ]); -+ return $this->objectManager->getObject( -+ Categories::class, -+ [ -+ 'locator' => $this->locatorMock, -+ 'categoryCollectionFactory' => $this->categoryCollectionFactoryMock, -+ 'arrayManager' => $this->arrayManagerMock, -+ 'authorization' => $this->authorizationMock -+ ] -+ ); - } - - public function testModifyData() -@@ -130,7 +145,9 @@ class CategoriesTest extends AbstractModifierTest - ], - ], - ]; -- -+ $this->authorizationMock->expects($this->exactly(2)) -+ ->method('isAllowed') -+ ->willReturn(true); - $this->arrayManagerMock->expects($this->any()) - ->method('findPath') - ->willReturn('path'); -@@ -154,38 +171,4 @@ class CategoriesTest extends AbstractModifierTest - { - return [[true], [false]]; - } -- -- public function testModifyMetaWithCaching() -- { -- $this->arrayManagerMock->expects($this->exactly(2)) -- ->method('findPath') -- ->willReturn(true); -- $cacheManager = $this->getMockBuilder(CacheInterface::class) -- ->getMockForAbstractClass(); -- $cacheManager->expects($this->once()) -- ->method('load') -- ->with(Categories::CATEGORY_TREE_ID . '_'); -- $cacheManager->expects($this->once()) -- ->method('save'); -- -- $modifier = $this->createModel(); -- $cacheContextProperty = new \ReflectionProperty( -- Categories::class, -- 'cacheManager' -- ); -- $cacheContextProperty->setAccessible(true); -- $cacheContextProperty->setValue($modifier, $cacheManager); -- -- $groupCode = 'test_group_code'; -- $meta = [ -- $groupCode => [ -- 'children' => [ -- 'category_ids' => [ -- 'sortOrder' => 10, -- ], -- ], -- ], -- ]; -- $modifier->modifyMeta($meta); -- } - } -diff --git a/app/code/Magento/Catalog/Test/Unit/Ui/DataProvider/Product/Form/Modifier/EavTest.php b/app/code/Magento/Catalog/Test/Unit/Ui/DataProvider/Product/Form/Modifier/EavTest.php -index 22bb712d42f..8cb59b1a2cc 100755 ---- a/app/code/Magento/Catalog/Test/Unit/Ui/DataProvider/Product/Form/Modifier/EavTest.php -+++ b/app/code/Magento/Catalog/Test/Unit/Ui/DataProvider/Product/Form/Modifier/EavTest.php -@@ -19,6 +19,7 @@ use Magento\Eav\Model\Entity\Attribute\Group; - use Magento\Catalog\Model\ResourceModel\Eav\Attribute as EavAttribute; - use Magento\Eav\Model\Entity\Type as EntityType; - use Magento\Eav\Model\ResourceModel\Entity\Attribute\Collection as AttributeCollection; -+use Magento\Eav\Model\ResourceModel\Entity\Attribute\CollectionFactory as AttributeCollectionFactory; - use Magento\Ui\DataProvider\Mapper\FormElement as FormElementMapper; - use Magento\Ui\DataProvider\Mapper\MetaProperties as MetaPropertiesMapper; - use Magento\Framework\Api\SearchCriteriaBuilder; -@@ -87,6 +88,11 @@ class EavTest extends AbstractModifierTest - */ - private $entityTypeMock; - -+ /** -+ * @var AttributeCollectionFactory|\PHPUnit_Framework_MockObject_MockObject -+ */ -+ private $attributeCollectionFactoryMock; -+ - /** - * @var AttributeCollection|\PHPUnit_Framework_MockObject_MockObject - */ -@@ -225,6 +231,10 @@ class EavTest extends AbstractModifierTest - $this->entityTypeMock = $this->getMockBuilder(EntityType::class) - ->disableOriginalConstructor() - ->getMock(); -+ $this->attributeCollectionFactoryMock = $this->getMockBuilder(AttributeCollectionFactory::class) -+ ->disableOriginalConstructor() -+ ->setMethods(['create']) -+ ->getMock(); - $this->attributeCollectionMock = $this->getMockBuilder(AttributeCollection::class) - ->disableOriginalConstructor() - ->getMock(); -@@ -360,7 +370,8 @@ class EavTest extends AbstractModifierTest - 'attributeRepository' => $this->attributeRepositoryMock, - 'arrayManager' => $this->arrayManagerMock, - 'eavAttributeFactory' => $this->eavAttributeFactoryMock, -- '_eventManager' => $this->eventManagerMock -+ '_eventManager' => $this->eventManagerMock, -+ 'attributeCollectionFactory' => $this->attributeCollectionFactoryMock - ]); - } - -@@ -374,84 +385,68 @@ class EavTest extends AbstractModifierTest - ] - ]; - -- $this->locatorMock->expects($this->any()) -- ->method('getProduct') -+ $this->attributeCollectionFactoryMock->expects($this->once())->method('create') -+ ->willReturn($this->attributeCollectionMock); -+ -+ $this->attributeCollectionMock->expects($this->any())->method('getItems') -+ ->willReturn([ -+ $this->eavAttributeMock -+ ]); -+ -+ $this->locatorMock->expects($this->any())->method('getProduct') - ->willReturn($this->productMock); - -- $this->productMock->expects($this->any()) -- ->method('getId') -+ $this->productMock->expects($this->any())->method('getId') - ->willReturn(1); -- $this->productMock->expects($this->once()) -- ->method('getAttributeSetId') -+ $this->productMock->expects($this->once())->method('getAttributeSetId') - ->willReturn(4); -- $this->productMock->expects($this->once()) -- ->method('getData') -+ $this->productMock->expects($this->once())->method('getData') - ->with(ProductAttributeInterface::CODE_PRICE)->willReturn('19.9900'); - -- $this->searchCriteriaBuilderMock->expects($this->any()) -- ->method('addFilter') -+ $this->searchCriteriaBuilderMock->expects($this->any())->method('addFilter') - ->willReturnSelf(); -- $this->searchCriteriaBuilderMock->expects($this->any()) -- ->method('create') -+ $this->searchCriteriaBuilderMock->expects($this->any())->method('create') - ->willReturn($this->searchCriteriaMock); -- $this->attributeGroupRepositoryMock->expects($this->any()) -- ->method('getList') -+ $this->attributeGroupRepositoryMock->expects($this->any())->method('getList') - ->willReturn($this->searchCriteriaMock); -- $this->searchCriteriaMock->expects($this->once()) -- ->method('getItems') -+ $this->searchCriteriaMock->expects($this->once())->method('getItems') - ->willReturn([$this->attributeGroupMock]); -- $this->sortOrderBuilderMock->expects($this->once()) -- ->method('setField') -+ $this->sortOrderBuilderMock->expects($this->once())->method('setField') - ->willReturnSelf(); -- $this->sortOrderBuilderMock->expects($this->once()) -- ->method('setAscendingDirection') -+ $this->sortOrderBuilderMock->expects($this->once())->method('setAscendingDirection') - ->willReturnSelf(); - $dataObjectMock = $this->createMock(\Magento\Framework\Api\AbstractSimpleObject::class); -- $this->sortOrderBuilderMock->expects($this->once()) -- ->method('create') -+ $this->sortOrderBuilderMock->expects($this->once())->method('create') - ->willReturn($dataObjectMock); - -- $this->searchCriteriaBuilderMock->expects($this->any()) -- ->method('addFilter') -+ $this->searchCriteriaBuilderMock->expects($this->any())->method('addFilter') - ->willReturnSelf(); -- $this->searchCriteriaBuilderMock->expects($this->once()) -- ->method('addSortOrder') -+ $this->searchCriteriaBuilderMock->expects($this->once())->method('addSortOrder') - ->willReturnSelf(); -- $this->searchCriteriaBuilderMock->expects($this->any()) -- ->method('create') -+ $this->searchCriteriaBuilderMock->expects($this->any())->method('create') - ->willReturn($this->searchCriteriaMock); - -- $this->attributeRepositoryMock->expects($this->once()) -- ->method('getList') -+ $this->attributeRepositoryMock->expects($this->once())->method('getList') - ->with($this->searchCriteriaMock) - ->willReturn($this->searchResultsMock); -- $this->eavAttributeMock->expects($this->any()) -- ->method('getAttributeGroupCode') -+ $this->eavAttributeMock->expects($this->any())->method('getAttributeGroupCode') - ->willReturn('product-details'); -- $this->eavAttributeMock->expects($this->once()) -- ->method('getApplyTo') -+ $this->eavAttributeMock->expects($this->once())->method('getApplyTo') - ->willReturn([]); -- $this->eavAttributeMock->expects($this->once()) -- ->method('getFrontendInput') -+ $this->eavAttributeMock->expects($this->once())->method('getFrontendInput') - ->willReturn('price'); -- $this->eavAttributeMock->expects($this->any()) -- ->method('getAttributeCode') -+ $this->eavAttributeMock->expects($this->any())->method('getAttributeCode') - ->willReturn(ProductAttributeInterface::CODE_PRICE); -- $this->searchResultsMock->expects($this->once()) -- ->method('getItems') -+ $this->searchResultsMock->expects($this->once())->method('getItems') - ->willReturn([$this->eavAttributeMock]); - -- $this->storeMock->expects(($this->once())) -- ->method('getBaseCurrencyCode') -+ $this->storeMock->expects(($this->once()))->method('getBaseCurrencyCode') - ->willReturn('en_US'); -- $this->storeManagerMock->expects($this->once()) -- ->method('getStore') -+ $this->storeManagerMock->expects($this->once())->method('getStore') - ->willReturn($this->storeMock); -- $this->currencyMock->expects($this->once()) -- ->method('toCurrency') -+ $this->currencyMock->expects($this->once())->method('toCurrency') - ->willReturn('19.99'); -- $this->currencyLocaleMock->expects($this->once()) -- ->method('getCurrency') -+ $this->currencyLocaleMock->expects($this->once())->method('getCurrency') - ->willReturn($this->currencyMock); - - $this->assertEquals($sourceData, $this->eav->modifyData([])); -diff --git a/app/code/Magento/Catalog/Test/Unit/Ui/DataProvider/Product/Form/Modifier/WebsitesTest.php b/app/code/Magento/Catalog/Test/Unit/Ui/DataProvider/Product/Form/Modifier/WebsitesTest.php -index c3096770729..829dc482441 100644 ---- a/app/code/Magento/Catalog/Test/Unit/Ui/DataProvider/Product/Form/Modifier/WebsitesTest.php -+++ b/app/code/Magento/Catalog/Test/Unit/Ui/DataProvider/Product/Form/Modifier/WebsitesTest.php -@@ -74,6 +74,9 @@ class WebsitesTest extends AbstractModifierTest - */ - protected $storeViewMock; - -+ /** -+ * @inheritdoc -+ */ - protected function setUp() - { - parent::setUp(); -@@ -90,14 +93,11 @@ class WebsitesTest extends AbstractModifierTest - ->disableOriginalConstructor() - ->getMock(); - $this->websiteRepositoryMock = $this->getMockBuilder(\Magento\Store\Api\WebsiteRepositoryInterface::class) -- ->setMethods(['getList', 'getDefault']) -+ ->setMethods(['getList']) - ->getMockForAbstractClass(); - $this->websiteRepositoryMock->expects($this->any()) - ->method('getDefault') - ->willReturn($this->websiteMock); -- $this->websiteRepositoryMock->expects($this->any()) -- ->method('getList') -- ->willReturn([$this->websiteMock, $this->secondWebsiteMock]); - $this->groupRepositoryMock = $this->getMockBuilder(\Magento\Store\Api\GroupRepositoryInterface::class) - ->setMethods(['getList']) - ->getMockForAbstractClass(); -@@ -111,8 +111,10 @@ class WebsitesTest extends AbstractModifierTest - ->method('getWebsiteIds') - ->willReturn($this->assignedWebsites); - $this->storeManagerMock = $this->getMockBuilder(\Magento\Store\Model\StoreManagerInterface::class) -- ->setMethods(['isSingleStoreMode']) -+ ->setMethods(['isSingleStoreMode', 'getWesites']) - ->getMockForAbstractClass(); -+ $this->storeManagerMock->method('getWebsites') -+ ->willReturn([$this->websiteMock, $this->secondWebsiteMock]); - $this->storeManagerMock->expects($this->any()) - ->method('isSingleStoreMode') - ->willReturn(false); -diff --git a/app/code/Magento/Catalog/Test/Unit/Ui/DataProvider/Product/Listing/Collector/ImageTest.php b/app/code/Magento/Catalog/Test/Unit/Ui/DataProvider/Product/Listing/Collector/ImageTest.php -index 12bc9acfa4c..009cd690d4c 100644 ---- a/app/code/Magento/Catalog/Test/Unit/Ui/DataProvider/Product/Listing/Collector/ImageTest.php -+++ b/app/code/Magento/Catalog/Test/Unit/Ui/DataProvider/Product/Listing/Collector/ImageTest.php -@@ -15,6 +15,7 @@ use Magento\Store\Model\StoreManagerInterface; - use Magento\Catalog\Helper\ImageFactory; - use Magento\Catalog\Api\Data\ProductRender\ImageInterface; - use Magento\Catalog\Helper\Image as ImageHelper; -+use Magento\Framework\View\DesignLoader; - - /** - * @SuppressWarnings(PHPMD.CouplingBetweenObjects) -@@ -33,6 +34,9 @@ class ImageTest extends \PHPUnit\Framework\TestCase - /** @var DesignInterface | \PHPUnit_Framework_MockObject_MockObject */ - private $design; - -+ /** @var DesignLoader | \PHPUnit_Framework_MockObject_MockObject*/ -+ private $designLoader; -+ - /** @var Image */ - private $model; - -@@ -60,13 +64,15 @@ class ImageTest extends \PHPUnit\Framework\TestCase - ->getMock(); - $this->storeManager = $this->createMock(StoreManagerInterface::class); - $this->design = $this->createMock(DesignInterface::class); -+ $this->designLoader = $this->createMock(DesignLoader::class); - $this->model = new Image( - $this->imageFactory, - $this->state, - $this->storeManager, - $this->design, - $this->imageInterfaceFactory, -- $this->imageCodes -+ $this->imageCodes, -+ $this->designLoader - ); - } - -diff --git a/app/code/Magento/Catalog/Test/Unit/Ui/DataProvider/Product/ProductCustomOptionsDataProviderTest.php b/app/code/Magento/Catalog/Test/Unit/Ui/DataProvider/Product/ProductCustomOptionsDataProviderTest.php -index 6d7c8814bd4..0e0cb676cdf 100644 ---- a/app/code/Magento/Catalog/Test/Unit/Ui/DataProvider/Product/ProductCustomOptionsDataProviderTest.php -+++ b/app/code/Magento/Catalog/Test/Unit/Ui/DataProvider/Product/ProductCustomOptionsDataProviderTest.php -@@ -54,7 +54,16 @@ class ProductCustomOptionsDataProviderTest extends \PHPUnit\Framework\TestCase - ->getMockForAbstractClass(); - $this->collectionMock = $this->getMockBuilder(AbstractCollection::class) - ->disableOriginalConstructor() -- ->setMethods(['load', 'getSelect', 'getTable', 'getIterator', 'isLoaded', 'toArray', 'getSize']) -+ ->setMethods([ -+ 'load', -+ 'getSelect', -+ 'getTable', -+ 'getIterator', -+ 'isLoaded', -+ 'toArray', -+ 'getSize', -+ 'setStoreId' -+ ]) - ->getMockForAbstractClass(); - $this->dbSelectMock = $this->getMockBuilder(DbSelect::class) - ->disableOriginalConstructor() -diff --git a/app/code/Magento/Catalog/Test/Unit/ViewModel/Product/BreadcrumbsTest.php b/app/code/Magento/Catalog/Test/Unit/ViewModel/Product/BreadcrumbsTest.php -index dbf1292e573..a4ccaffc8fb 100644 ---- a/app/code/Magento/Catalog/Test/Unit/ViewModel/Product/BreadcrumbsTest.php -+++ b/app/code/Magento/Catalog/Test/Unit/ViewModel/Product/BreadcrumbsTest.php -@@ -152,21 +152,21 @@ class BreadcrumbsTest extends \PHPUnit\Framework\TestCase - return [ - [ - $this->getObjectManager()->getObject(Product::class, ['data' => ['name' => 'Test ™']]), -- '{"breadcrumbs":{"categoryUrlSuffix":"."html","userCategoryPathInUrl":0,"product":"Test \u2122"}}', -+ '{"breadcrumbs":{"categoryUrlSuffix":"."html","useCategoryPathInUrl":0,"product":"Test \u2122"}}', - ], - [ - $this->getObjectManager()->getObject(Product::class, ['data' => ['name' => 'Test "']]), -- '{"breadcrumbs":{"categoryUrlSuffix":"."html","userCategoryPathInUrl":0,"product":"Test ""}}', -+ '{"breadcrumbs":{"categoryUrlSuffix":"."html","useCategoryPathInUrl":0,"product":"Test ""}}', - ], - [ - $this->getObjectManager()->getObject(Product::class, ['data' => ['name' => 'Test <b>x</b>']]), -- '{"breadcrumbs":{"categoryUrlSuffix":"."html","userCategoryPathInUrl":0,"product":' -+ '{"breadcrumbs":{"categoryUrlSuffix":"."html","useCategoryPathInUrl":0,"product":' - . '"Test <b>x<\/b>"}}', - ], - [ - $this->getObjectManager()->getObject(Product::class, ['data' => ['name' => 'Test \'abc\'']]), - '{"breadcrumbs":' -- . '{"categoryUrlSuffix":"."html","userCategoryPathInUrl":0,"product":"Test 'abc'"}}' -+ . '{"categoryUrlSuffix":"."html","useCategoryPathInUrl":0,"product":"Test 'abc'"}}' - ], - ]; - } -diff --git a/app/code/Magento/Catalog/Ui/Component/ColumnFactory.php b/app/code/Magento/Catalog/Ui/Component/ColumnFactory.php -index cbc67fee8a5..ea6b1fd47a0 100644 ---- a/app/code/Magento/Catalog/Ui/Component/ColumnFactory.php -+++ b/app/code/Magento/Catalog/Ui/Component/ColumnFactory.php -@@ -5,7 +5,11 @@ - */ - namespace Magento\Catalog\Ui\Component; - -+use Magento\Ui\Component\Filters\FilterModifier; -+ - /** -+ * Column Factory -+ * - * @api - * @since 100.0.2 - */ -@@ -47,20 +51,26 @@ class ColumnFactory - } - - /** -+ * Create Factory -+ * - * @param \Magento\Catalog\Api\Data\ProductAttributeInterface $attribute - * @param \Magento\Framework\View\Element\UiComponent\ContextInterface $context - * @param array $config -+ * - * @return \Magento\Ui\Component\Listing\Columns\ColumnInterface -+ * @throws \Magento\Framework\Exception\LocalizedException - */ - public function create($attribute, $context, array $config = []) - { -+ $filterModifiers = $context->getRequestParam(FilterModifier::FILTER_MODIFIER, []); -+ - $columnName = $attribute->getAttributeCode(); - $config = array_merge([ - 'label' => __($attribute->getDefaultFrontendLabel()), - 'dataType' => $this->getDataType($attribute), - 'add_field' => true, - 'visible' => $attribute->getIsVisibleInGrid(), -- 'filter' => ($attribute->getIsFilterableInGrid()) -+ 'filter' => ($attribute->getIsFilterableInGrid() || array_key_exists($columnName, $filterModifiers)) - ? $this->getFilterType($attribute->getFrontendInput()) - : null, - ], $config); -@@ -82,7 +92,10 @@ class ColumnFactory - } - - /** -+ * Get Js Component -+ * - * @param string $dataType -+ * - * @return string - */ - protected function getJsComponent($dataType) -@@ -91,14 +104,15 @@ class ColumnFactory - } - - /** -+ * Get Data Type -+ * - * @param \Magento\Catalog\Api\Data\ProductAttributeInterface $attribute -+ * - * @return string - */ - protected function getDataType($attribute) - { -- return isset($this->dataTypeMap[$attribute->getFrontendInput()]) -- ? $this->dataTypeMap[$attribute->getFrontendInput()] -- : $this->dataTypeMap['default']; -+ return $this->dataTypeMap[$attribute->getFrontendInput()] ?? $this->dataTypeMap['default']; - } - - /** -@@ -111,6 +125,6 @@ class ColumnFactory - { - $filtersMap = ['date' => 'dateRange']; - $result = array_replace_recursive($this->dataTypeMap, $filtersMap); -- return isset($result[$frontendInput]) ? $result[$frontendInput] : $result['default']; -+ return $result[$frontendInput] ?? $result['default']; - } - } -diff --git a/app/code/Magento/Catalog/Ui/Component/FilterFactory.php b/app/code/Magento/Catalog/Ui/Component/FilterFactory.php -index fcc500c8916..dd8eaffb0a6 100644 ---- a/app/code/Magento/Catalog/Ui/Component/FilterFactory.php -+++ b/app/code/Magento/Catalog/Ui/Component/FilterFactory.php -@@ -71,8 +71,6 @@ class FilterFactory - */ - protected function getFilterType($attribute) - { -- return isset($this->filterMap[$attribute->getFrontendInput()]) -- ? $this->filterMap[$attribute->getFrontendInput()] -- : $this->filterMap['default']; -+ return $this->filterMap[$attribute->getFrontendInput()] ?? $this->filterMap['default']; - } - } -diff --git a/app/code/Magento/Catalog/Ui/Component/Listing/Columns.php b/app/code/Magento/Catalog/Ui/Component/Listing/Columns.php -index c96498b054d..8ea6d8b9e5a 100644 ---- a/app/code/Magento/Catalog/Ui/Component/Listing/Columns.php -+++ b/app/code/Magento/Catalog/Ui/Component/Listing/Columns.php -@@ -80,6 +80,6 @@ class Columns extends \Magento\Ui\Component\Listing\Columns - */ - protected function getFilterType($frontendInput) - { -- return isset($this->filterMap[$frontendInput]) ? $this->filterMap[$frontendInput] : $this->filterMap['default']; -+ return $this->filterMap[$frontendInput] ?? $this->filterMap['default']; - } - } -diff --git a/app/code/Magento/Catalog/Ui/Component/Listing/Columns/Thumbnail.php b/app/code/Magento/Catalog/Ui/Component/Listing/Columns/Thumbnail.php -index d4dc9ddd7ca..09c9782fc0e 100644 ---- a/app/code/Magento/Catalog/Ui/Component/Listing/Columns/Thumbnail.php -+++ b/app/code/Magento/Catalog/Ui/Component/Listing/Columns/Thumbnail.php -@@ -9,6 +9,8 @@ use Magento\Framework\View\Element\UiComponentFactory; - use Magento\Framework\View\Element\UiComponent\ContextInterface; - - /** -+ * Class Thumbnail -+ * - * @api - * @since 100.0.2 - */ -@@ -67,6 +69,8 @@ class Thumbnail extends \Magento\Ui\Component\Listing\Columns\Column - } - - /** -+ * Get Alt -+ * - * @param array $row - * - * @return null|string -@@ -74,6 +78,6 @@ class Thumbnail extends \Magento\Ui\Component\Listing\Columns\Column - protected function getAlt($row) - { - $altField = $this->getData('config/altField') ?: self::ALT_FIELD; -- return isset($row[$altField]) ? $row[$altField] : null; -+ return $row[$altField] ?? null; - } - } -diff --git a/app/code/Magento/Catalog/Ui/Component/Listing/Columns/Websites.php b/app/code/Magento/Catalog/Ui/Component/Listing/Columns/Websites.php -index 5af0d71dc24..494b77724e5 100644 ---- a/app/code/Magento/Catalog/Ui/Component/Listing/Columns/Websites.php -+++ b/app/code/Magento/Catalog/Ui/Component/Listing/Columns/Websites.php -@@ -3,13 +3,19 @@ - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -+ -+declare(strict_types=1); -+ - namespace Magento\Catalog\Ui\Component\Listing\Columns; - --use Magento\Framework\View\Element\UiComponentFactory; -+use Magento\Framework\DB\Helper; - use Magento\Framework\View\Element\UiComponent\ContextInterface; -+use Magento\Framework\View\Element\UiComponentFactory; - use Magento\Store\Model\StoreManagerInterface; - - /** -+ * Websites listing column component. -+ * - * @api - * @since 100.0.2 - */ -@@ -20,6 +26,11 @@ class Websites extends \Magento\Ui\Component\Listing\Columns\Column - */ - const NAME = 'websites'; - -+ /** -+ * Data for concatenated website names value. -+ */ -+ private $websiteNames = 'website_names'; -+ - /** - * Store manager - * -@@ -27,26 +38,36 @@ class Websites extends \Magento\Ui\Component\Listing\Columns\Column - */ - protected $storeManager; - -+ /** -+ * @var \Magento\Framework\DB\Helper -+ */ -+ private $resourceHelper; -+ - /** - * @param ContextInterface $context - * @param UiComponentFactory $uiComponentFactory - * @param StoreManagerInterface $storeManager - * @param array $components - * @param array $data -+ * @param Helper $resourceHelper - */ - public function __construct( - ContextInterface $context, - UiComponentFactory $uiComponentFactory, - StoreManagerInterface $storeManager, - array $components = [], -- array $data = [] -+ array $data = [], -+ Helper $resourceHelper = null - ) { - parent::__construct($context, $uiComponentFactory, $components, $data); -+ $objectManager = \Magento\Framework\App\ObjectManager::getInstance(); - $this->storeManager = $storeManager; -+ $this->resourceHelper = $resourceHelper ?: $objectManager->get(Helper::class); - } - - /** -- * {@inheritdoc} -+ * @inheritdoc -+ * - * @deprecated 101.0.0 - */ - public function prepareDataSource(array $dataSource) -@@ -71,9 +92,10 @@ class Websites extends \Magento\Ui\Component\Listing\Columns\Column - - return $dataSource; - } -- -+ - /** -- * Prepare component configuration -+ * Prepare component configuration. -+ * - * @return void - */ - public function prepare() -@@ -83,4 +105,46 @@ class Websites extends \Magento\Ui\Component\Listing\Columns\Column - $this->_data['config']['componentDisabled'] = true; - } - } -+ -+ /** -+ * Apply sorting. -+ * -+ * @return void -+ */ -+ protected function applySorting() -+ { -+ $sorting = $this->getContext()->getRequestParam('sorting'); -+ $isSortable = $this->getData('config/sortable'); -+ if ($isSortable !== false -+ && !empty($sorting['field']) -+ && !empty($sorting['direction']) -+ && $sorting['field'] === $this->getName() -+ ) { -+ $collection = $this->getContext()->getDataProvider()->getCollection(); -+ $collection -+ ->joinField( -+ 'websites_ids', -+ 'catalog_product_website', -+ 'website_id', -+ 'product_id=entity_id', -+ null, -+ 'left' -+ ) -+ ->joinTable( -+ 'store_website', -+ 'website_id = websites_ids', -+ ['name'], -+ null, -+ 'left' -+ ) -+ ->groupByAttribute('entity_id'); -+ $this->resourceHelper->addGroupConcatColumn( -+ $collection->getSelect(), -+ $this->websiteNames, -+ 'name' -+ ); -+ -+ $collection->getSelect()->order($this->websiteNames . ' ' . $sorting['direction']); -+ } -+ } - } -diff --git a/app/code/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/AdvancedPricing.php b/app/code/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/AdvancedPricing.php -index 336aeffa105..9ad75b5fda9 100644 ---- a/app/code/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/AdvancedPricing.php -+++ b/app/code/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/AdvancedPricing.php -@@ -14,7 +14,7 @@ use Magento\Store\Model\StoreManagerInterface; - use Magento\Customer\Api\GroupManagementInterface; - use Magento\Customer\Api\GroupRepositoryInterface; - use Magento\Framework\Api\SearchCriteriaBuilder; --use Magento\Framework\Module\Manager as ModuleManager; -+use \Magento\Framework\Module\ModuleManagerInterface as ModuleManager; - use Magento\Ui\Component\Container; - use Magento\Ui\Component\Form\Element\DataType\Number; - use Magento\Ui\Component\Form\Element\DataType\Price; -@@ -139,7 +139,8 @@ class AdvancedPricing extends AbstractModifier - } - - /** -- * {@inheritdoc} -+ * @inheritdoc -+ * - * @since 101.0.0 - */ - public function modifyMeta(array $meta) -@@ -158,7 +159,8 @@ class AdvancedPricing extends AbstractModifier - } - - /** -- * {@inheritdoc} -+ * @inheritdoc -+ * - * @since 101.0.0 - */ - public function modifyData(array $data) -@@ -381,11 +383,15 @@ class AdvancedPricing extends AbstractModifier - ); - - $advancedPricingButton['arguments']['data']['config'] = [ -+ 'dataScope' => 'advanced_pricing_button', - 'displayAsLink' => true, - 'formElement' => Container::NAME, - 'componentType' => Container::NAME, - 'component' => 'Magento_Ui/js/form/components/button', - 'template' => 'ui/form/components/button/container', -+ 'imports' => [ -+ 'childError' => $this->scopeName . '.advanced_pricing_modal.advanced-pricing:error', -+ ], - 'actions' => [ - [ - 'targetName' => $this->scopeName . '.advanced_pricing_modal', -diff --git a/app/code/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/Attributes.php b/app/code/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/Attributes.php -index 683a96133ad..a6b9856a4a0 100644 ---- a/app/code/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/Attributes.php -+++ b/app/code/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/Attributes.php -@@ -67,7 +67,8 @@ class Attributes extends AbstractModifier - } - - /** -- * {@inheritdoc} -+ * @inheritdoc -+ * - * @since 101.0.0 - */ - public function modifyData(array $data) -@@ -76,6 +77,8 @@ class Attributes extends AbstractModifier - } - - /** -+ * Check if can add attributes on product form. -+ * - * @return boolean - */ - private function canAddAttributes() -@@ -89,7 +92,8 @@ class Attributes extends AbstractModifier - } - - /** -- * {@inheritdoc} -+ * @inheritdoc -+ * - * @since 101.0.0 - */ - public function modifyMeta(array $meta) -@@ -111,6 +115,8 @@ class Attributes extends AbstractModifier - } - - /** -+ * Modify meta customize attribute modal. -+ * - * @param array $meta - * @return array - */ -@@ -207,6 +213,8 @@ class Attributes extends AbstractModifier - } - - /** -+ * Modify meta to customize create attribute modal. -+ * - * @param array $meta - * @return array - */ -@@ -289,6 +297,8 @@ class Attributes extends AbstractModifier - } - - /** -+ * Modify meta to customize attribute grid. -+ * - * @param array $meta - * @return array - */ -@@ -309,7 +319,7 @@ class Attributes extends AbstractModifier - 'immediateUpdateBySelection' => true, - 'behaviourType' => 'edit', - 'externalFilterMode' => true, -- 'dataLinks' => ['imports' => false, 'exports' => true], -+ 'dataLinks' => ['imports' => false, 'exports' => false], - 'formProvider' => 'ns = ${ $.namespace }, index = product_form', - 'groupCode' => static::GROUP_CODE, - 'groupName' => static::GROUP_NAME, -diff --git a/app/code/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/Categories.php b/app/code/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/Categories.php -index ed737df708a..0b8f551988a 100644 ---- a/app/code/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/Categories.php -+++ b/app/code/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/Categories.php -@@ -3,6 +3,8 @@ - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -+declare(strict_types=1); -+ - namespace Magento\Catalog\Ui\DataProvider\Product\Form\Modifier; - - use Magento\Catalog\Model\Locator\LocatorInterface; -@@ -11,9 +13,11 @@ use Magento\Framework\App\ObjectManager; - use Magento\Framework\App\CacheInterface; - use Magento\Framework\DB\Helper as DbHelper; - use Magento\Catalog\Model\Category as CategoryModel; -+use Magento\Framework\Exception\LocalizedException; - use Magento\Framework\Serialize\SerializerInterface; - use Magento\Framework\UrlInterface; - use Magento\Framework\Stdlib\ArrayManager; -+use Magento\Framework\AuthorizationInterface; - - /** - * Data provider for categories field of product page -@@ -78,6 +82,11 @@ class Categories extends AbstractModifier - */ - private $serializer; - -+ /** -+ * @var AuthorizationInterface -+ */ -+ private $authorization; -+ - /** - * @param LocatorInterface $locator - * @param CategoryCollectionFactory $categoryCollectionFactory -@@ -85,6 +94,7 @@ class Categories extends AbstractModifier - * @param UrlInterface $urlBuilder - * @param ArrayManager $arrayManager - * @param SerializerInterface $serializer -+ * @param AuthorizationInterface $authorization - */ - public function __construct( - LocatorInterface $locator, -@@ -92,7 +102,8 @@ class Categories extends AbstractModifier - DbHelper $dbHelper, - UrlInterface $urlBuilder, - ArrayManager $arrayManager, -- SerializerInterface $serializer = null -+ SerializerInterface $serializer = null, -+ AuthorizationInterface $authorization = null - ) { - $this->locator = $locator; - $this->categoryCollectionFactory = $categoryCollectionFactory; -@@ -100,6 +111,7 @@ class Categories extends AbstractModifier - $this->urlBuilder = $urlBuilder; - $this->arrayManager = $arrayManager; - $this->serializer = $serializer ?: ObjectManager::getInstance()->get(SerializerInterface::class); -+ $this->authorization = $authorization ?: ObjectManager::getInstance()->get(AuthorizationInterface::class); - } - - /** -@@ -118,19 +130,31 @@ class Categories extends AbstractModifier - } - - /** -- * {@inheritdoc} -+ * @inheritdoc - * @since 101.0.0 - */ - public function modifyMeta(array $meta) - { -- $meta = $this->createNewCategoryModal($meta); -+ if ($this->isAllowed()) { -+ $meta = $this->createNewCategoryModal($meta); -+ } - $meta = $this->customizeCategoriesField($meta); - - return $meta; - } - - /** -- * {@inheritdoc} -+ * Check current user permission on category resource -+ * -+ * @return bool -+ */ -+ private function isAllowed() -+ { -+ return $this->authorization->isAllowed('Magento_Catalog::categories'); -+ } -+ -+ /** -+ * @inheritdoc - * @since 101.0.0 - */ - public function modifyData(array $data) -@@ -202,6 +226,7 @@ class Categories extends AbstractModifier - * - * @param array $meta - * @return array -+ * @throws LocalizedException - * @since 101.0.0 - */ - protected function customizeCategoriesField(array $meta) -@@ -214,87 +239,91 @@ class Categories extends AbstractModifier - return $meta; - } - -- $meta = $this->arrayManager->merge( -- $containerPath, -- $meta, -- [ -- 'arguments' => [ -- 'data' => [ -- 'config' => [ -- 'label' => __('Categories'), -- 'dataScope' => '', -- 'breakLine' => false, -- 'formElement' => 'container', -- 'componentType' => 'container', -- 'component' => 'Magento_Ui/js/form/components/group', -- 'scopeLabel' => __('[GLOBAL]'), -- 'disabled' => $this->locator->getProduct()->isLockedAttribute($fieldCode), -- ], -+ $value = [ -+ 'arguments' => [ -+ 'data' => [ -+ 'config' => [ -+ 'label' => __('Categories'), -+ 'dataScope' => '', -+ 'breakLine' => false, -+ 'formElement' => 'container', -+ 'componentType' => 'container', -+ 'component' => 'Magento_Ui/js/form/components/group', -+ 'scopeLabel' => __('[GLOBAL]'), -+ 'disabled' => $this->locator->getProduct()->isLockedAttribute($fieldCode), - ], - ], -- 'children' => [ -- $fieldCode => [ -- 'arguments' => [ -- 'data' => [ -+ ], -+ 'children' => [ -+ $fieldCode => [ -+ 'arguments' => [ -+ 'data' => [ -+ 'config' => [ -+ 'formElement' => 'select', -+ 'componentType' => 'field', -+ 'component' => 'Magento_Catalog/js/components/new-category', -+ 'filterOptions' => true, -+ 'chipsEnabled' => true, -+ 'disableLabel' => true, -+ 'levelsVisibility' => '1', -+ 'elementTmpl' => 'ui/grid/filters/elements/ui-select', -+ 'options' => $this->getCategoriesTree(), -+ 'listens' => [ -+ 'index=create_category:responseData' => 'setParsed', -+ 'newOption' => 'toggleOptionSelected' -+ ], - 'config' => [ -- 'formElement' => 'select', -- 'componentType' => 'field', -- 'component' => 'Magento_Catalog/js/components/new-category', -- 'filterOptions' => true, -- 'chipsEnabled' => true, -- 'disableLabel' => true, -- 'levelsVisibility' => '1', -- 'elementTmpl' => 'ui/grid/filters/elements/ui-select', -- 'options' => $this->getCategoriesTree(), -- 'listens' => [ -- 'index=create_category:responseData' => 'setParsed', -- 'newOption' => 'toggleOptionSelected' -- ], -- 'config' => [ -- 'dataScope' => $fieldCode, -- 'sortOrder' => 10, -- ], -+ 'dataScope' => $fieldCode, -+ 'sortOrder' => 10, - ], - ], - ], - ], -- 'create_category_button' => [ -- 'arguments' => [ -- 'data' => [ -- 'config' => [ -- 'title' => __('New Category'), -- 'formElement' => 'container', -- 'additionalClasses' => 'admin__field-small', -- 'componentType' => 'container', -- 'component' => 'Magento_Ui/js/form/components/button', -- 'template' => 'ui/form/components/button/container', -- 'actions' => [ -- [ -- 'targetName' => 'product_form.product_form.create_category_modal', -- 'actionName' => 'toggleModal', -- ], -- [ -- 'targetName' => -- 'product_form.product_form.create_category_modal.create_category', -- 'actionName' => 'render' -- ], -- [ -- 'targetName' => -- 'product_form.product_form.create_category_modal.create_category', -- 'actionName' => 'resetForm' -- ] -- ], -- 'additionalForGroup' => true, -- 'provider' => false, -- 'source' => 'product_details', -- 'displayArea' => 'insideGroup', -- 'sortOrder' => 20, -+ ], -+ ] -+ ]; -+ if ($this->isAllowed()) { -+ $value['children']['create_category_button'] = [ -+ 'arguments' => [ -+ 'data' => [ -+ 'config' => [ -+ 'title' => __('New Category'), -+ 'formElement' => 'container', -+ 'additionalClasses' => 'admin__field-small', -+ 'componentType' => 'container', -+ 'component' => 'Magento_Ui/js/form/components/button', -+ 'template' => 'ui/form/components/button/container', -+ 'actions' => [ -+ [ -+ 'targetName' => 'product_form.product_form.create_category_modal', -+ 'actionName' => 'toggleModal', - ], -+ [ -+ 'targetName' => -+ 'product_form.product_form.create_category_modal.create_category', -+ 'actionName' => 'render' -+ ], -+ [ -+ 'targetName' => -+ 'product_form.product_form.create_category_modal.create_category', -+ 'actionName' => 'resetForm' -+ ] - ], -- ] -- ] -+ 'additionalForGroup' => true, -+ 'provider' => false, -+ 'source' => 'product_details', -+ 'displayArea' => 'insideGroup', -+ 'sortOrder' => 20, -+ 'dataScope' => $fieldCode, -+ ], -+ ], - ] -- ] -+ ]; -+ } -+ $meta = $this->arrayManager->merge( -+ $containerPath, -+ $meta, -+ $value - ); - - return $meta; -@@ -305,20 +334,64 @@ class Categories extends AbstractModifier - * - * @param string|null $filter - * @return array -+ * @throws LocalizedException - * @since 101.0.0 - */ - protected function getCategoriesTree($filter = null) - { -- $categoryTree = $this->getCacheManager()->load(self::CATEGORY_TREE_ID . '_' . $filter); -- if ($categoryTree) { -- return $this->serializer->unserialize($categoryTree); -+ $storeId = (int) $this->locator->getStore()->getId(); -+ -+ $cachedCategoriesTree = $this->getCacheManager() -+ ->load($this->getCategoriesTreeCacheId($storeId, (string) $filter)); -+ if (!empty($cachedCategoriesTree)) { -+ return $this->serializer->unserialize($cachedCategoriesTree); - } - -- $storeId = $this->locator->getStore()->getId(); -+ $categoriesTree = $this->retrieveCategoriesTree( -+ $storeId, -+ $this->retrieveShownCategoriesIds($storeId, (string) $filter) -+ ); -+ -+ $this->getCacheManager()->save( -+ $this->serializer->serialize($categoriesTree), -+ $this->getCategoriesTreeCacheId($storeId, (string) $filter), -+ [ -+ \Magento\Catalog\Model\Category::CACHE_TAG, -+ \Magento\Framework\App\Cache\Type\Block::CACHE_TAG -+ ] -+ ); -+ -+ return $categoriesTree; -+ } -+ -+ /** -+ * Get cache id for categories tree. -+ * -+ * @param int $storeId -+ * @param string $filter -+ * @return string -+ */ -+ private function getCategoriesTreeCacheId(int $storeId, string $filter = '') : string -+ { -+ return self::CATEGORY_TREE_ID -+ . '_' . (string) $storeId -+ . '_' . $filter; -+ } -+ -+ /** -+ * Retrieve filtered list of categories id. -+ * -+ * @param int $storeId -+ * @param string $filter -+ * @return array -+ * @throws LocalizedException -+ */ -+ private function retrieveShownCategoriesIds(int $storeId, string $filter = '') : array -+ { - /* @var $matchingNamesCollection \Magento\Catalog\Model\ResourceModel\Category\Collection */ - $matchingNamesCollection = $this->categoryCollectionFactory->create(); - -- if ($filter !== null) { -+ if (!empty($filter)) { - $matchingNamesCollection->addAttributeToFilter( - 'name', - ['like' => $this->dbHelper->addLikeEscape($filter, ['position' => 'any'])] -@@ -338,6 +411,19 @@ class Categories extends AbstractModifier - } - } - -+ return $shownCategoriesIds; -+ } -+ -+ /** -+ * Retrieve tree of categories with attributes. -+ * -+ * @param int $storeId -+ * @param array $shownCategoriesIds -+ * @return array|null -+ * @throws LocalizedException -+ */ -+ private function retrieveCategoriesTree(int $storeId, array $shownCategoriesIds) : ?array -+ { - /* @var $collection \Magento\Catalog\Model\ResourceModel\Category\Collection */ - $collection = $this->categoryCollectionFactory->create(); - -@@ -364,15 +450,6 @@ class Categories extends AbstractModifier - $categoryById[$category->getParentId()]['optgroup'][] = &$categoryById[$category->getId()]; - } - -- $this->getCacheManager()->save( -- $this->serializer->serialize($categoryById[CategoryModel::TREE_ROOT_ID]['optgroup']), -- self::CATEGORY_TREE_ID . '_' . $filter, -- [ -- \Magento\Catalog\Model\Category::CACHE_TAG, -- \Magento\Framework\App\Cache\Type\Block::CACHE_TAG -- ] -- ); -- - return $categoryById[CategoryModel::TREE_ROOT_ID]['optgroup']; - } - } -diff --git a/app/code/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/CustomOptions.php b/app/code/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/CustomOptions.php -index 86f1db2022c..af43c84501f 100755 ---- a/app/code/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/CustomOptions.php -+++ b/app/code/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/CustomOptions.php -@@ -11,6 +11,7 @@ use Magento\Catalog\Model\ProductOptions\ConfigInterface; - use Magento\Catalog\Model\Config\Source\Product\Options\Price as ProductOptionsPrice; - use Magento\Framework\UrlInterface; - use Magento\Framework\Stdlib\ArrayManager; -+use Magento\Ui\Component\Form\Element\Hidden; - use Magento\Ui\Component\Modal; - use Magento\Ui\Component\Container; - use Magento\Ui\Component\DynamicRows; -@@ -867,7 +868,7 @@ class CustomOptions extends AbstractModifier - 'data' => [ - 'config' => [ - 'componentType' => Field::NAME, -- 'formElement' => Input::NAME, -+ 'formElement' => Hidden::NAME, - 'dataScope' => static::FIELD_SORT_ORDER_NAME, - 'dataType' => Number::NAME, - 'visible' => false, -diff --git a/app/code/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/Eav.php b/app/code/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/Eav.php -old mode 100755 -new mode 100644 -index 7cd81419c03..8326c3b5318 ---- a/app/code/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/Eav.php -+++ b/app/code/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/Eav.php -@@ -11,6 +11,7 @@ use Magento\Catalog\Api\ProductAttributeRepositoryInterface; - use Magento\Catalog\Model\Attribute\ScopeOverriddenValue; - use Magento\Catalog\Model\Locator\LocatorInterface; - use Magento\Catalog\Model\Product; -+use Magento\Catalog\Model\Product\Type as ProductType; - use Magento\Catalog\Model\ResourceModel\Eav\Attribute as EavAttribute; - use Magento\Catalog\Model\ResourceModel\Eav\AttributeFactory as EavAttributeFactory; - use Magento\Catalog\Ui\DataProvider\CatalogEavValidationRules; -@@ -33,6 +34,7 @@ use Magento\Ui\DataProvider\Mapper\FormElement as FormElementMapper; - use Magento\Ui\DataProvider\Mapper\MetaProperties as MetaPropertiesMapper; - use Magento\Catalog\Ui\DataProvider\Product\Form\Modifier\Eav\CompositeConfigProcessor; - use Magento\Framework\App\Config\ScopeConfigInterface; -+use Magento\Eav\Model\ResourceModel\Entity\Attribute\CollectionFactory as AttributeCollectionFactory; - - /** - * Class Eav -@@ -190,6 +192,17 @@ class Eav extends AbstractModifier - */ - private $localeCurrency; - -+ /** -+ * internal cache for attribute models -+ * @var array -+ */ -+ private $attributesCache = []; -+ -+ /** -+ * @var AttributeCollectionFactory -+ */ -+ private $attributeCollectionFactory; -+ - /** - * @var CompositeConfigProcessor - */ -@@ -223,6 +236,7 @@ class Eav extends AbstractModifier - * @param array $attributesToEliminate - * @param CompositeConfigProcessor|null $wysiwygConfigProcessor - * @param ScopeConfigInterface|null $scopeConfig -+ * @param AttributeCollectionFactory $attributeCollectionFactory - * @SuppressWarnings(PHPMD.ExcessiveParameterList) - */ - public function __construct( -@@ -246,7 +260,8 @@ class Eav extends AbstractModifier - $attributesToDisable = [], - $attributesToEliminate = [], - CompositeConfigProcessor $wysiwygConfigProcessor = null, -- ScopeConfigInterface $scopeConfig = null -+ ScopeConfigInterface $scopeConfig = null, -+ AttributeCollectionFactory $attributeCollectionFactory = null - ) { - $this->locator = $locator; - $this->catalogEavValidationRules = $catalogEavValidationRules; -@@ -271,10 +286,12 @@ class Eav extends AbstractModifier - ->get(CompositeConfigProcessor::class); - $this->scopeConfig = $scopeConfig ?: \Magento\Framework\App\ObjectManager::getInstance() - ->get(ScopeConfigInterface::class); -+ $this->attributeCollectionFactory = $attributeCollectionFactory -+ ?: \Magento\Framework\App\ObjectManager::getInstance()->get(AttributeCollectionFactory::class); - } - - /** -- * {@inheritdoc} -+ * @inheritdoc - * @since 101.0.0 - */ - public function modifyMeta(array $meta) -@@ -287,7 +304,7 @@ class Eav extends AbstractModifier - if ($attributes) { - $meta[$groupCode]['children'] = $this->getAttributesMeta($attributes, $groupCode); - $meta[$groupCode]['arguments']['data']['config']['componentType'] = Fieldset::NAME; -- $meta[$groupCode]['arguments']['data']['config']['label'] = __('%1', $group->getAttributeGroupName()); -+ $meta[$groupCode]['arguments']['data']['config']['label'] = __($group->getAttributeGroupName()); - $meta[$groupCode]['arguments']['data']['config']['collapsible'] = true; - $meta[$groupCode]['arguments']['data']['config']['dataScope'] = self::DATA_SCOPE_PRODUCT; - $meta[$groupCode]['arguments']['data']['config']['sortOrder'] = -@@ -385,7 +402,7 @@ class Eav extends AbstractModifier - } - - /** -- * {@inheritdoc} -+ * @inheritdoc - * @since 101.0.0 - */ - public function modifyData(array $data) -@@ -403,7 +420,7 @@ class Eav extends AbstractModifier - - foreach ($attributes as $attribute) { - if (null !== ($attributeValue = $this->setupAttributeData($attribute))) { -- if ($attribute->getFrontendInput() === 'price' && is_scalar($attributeValue)) { -+ if ($this->isPriceAttribute($attribute, $attributeValue)) { - $attributeValue = $this->formatPrice($attributeValue); - } - $data[$productId][self::DATA_SOURCE_DEFAULT][$attribute->getAttributeCode()] = $attributeValue; -@@ -414,6 +431,32 @@ class Eav extends AbstractModifier - return $data; - } - -+ /** -+ * Obtain if given attribute is a price -+ * -+ * @param \Magento\Catalog\Api\Data\ProductAttributeInterface $attribute -+ * @param string|integer $attributeValue -+ * @return bool -+ */ -+ private function isPriceAttribute(ProductAttributeInterface $attribute, $attributeValue) -+ { -+ return $attribute->getFrontendInput() === 'price' -+ && is_scalar($attributeValue) -+ && !$this->isBundleSpecialPrice($attribute); -+ } -+ -+ /** -+ * Obtain if current product is bundle and given attribute is special_price -+ * -+ * @param \Magento\Catalog\Api\Data\ProductAttributeInterface $attribute -+ * @return bool -+ */ -+ private function isBundleSpecialPrice(ProductAttributeInterface $attribute) -+ { -+ return $this->locator->getProduct()->getTypeId() === ProductType::TYPE_BUNDLE -+ && $attribute->getAttributeCode() === ProductAttributeInterface::CODE_SPECIAL_PRICE; -+ } -+ - /** - * Resolve data persistence - * -@@ -507,39 +550,59 @@ class Eav extends AbstractModifier - private function getAttributes() - { - if (!$this->attributes) { -- foreach ($this->getGroups() as $group) { -- $this->attributes[$this->calculateGroupCode($group)] = $this->loadAttributes($group); -- } -+ $this->attributes = $this->loadAttributesForGroups($this->getGroups()); - } - - return $this->attributes; - } - - /** -- * Loading product attributes from group -+ * Loads attributes for specified groups at once - * -- * @param AttributeGroupInterface $group -+ * @param AttributeGroupInterface[] $groups - * @return ProductAttributeInterface[] - */ -- private function loadAttributes(AttributeGroupInterface $group) -+ private function loadAttributesForGroups(array $groups) - { - $attributes = []; -+ $groupIds = []; -+ -+ foreach ($groups as $group) { -+ $groupIds[$group->getAttributeGroupId()] = $this->calculateGroupCode($group); -+ $attributes[$this->calculateGroupCode($group)] = []; -+ } -+ -+ $collection = $this->attributeCollectionFactory->create(); -+ $collection->setAttributeGroupFilter(array_keys($groupIds)); -+ -+ $mapAttributeToGroup = []; -+ -+ foreach ($collection->getItems() as $attribute) { -+ $mapAttributeToGroup[$attribute->getAttributeId()] = $attribute->getAttributeGroupId(); -+ } -+ - $sortOrder = $this->sortOrderBuilder - ->setField('sort_order') - ->setAscendingDirection() - ->create(); -+ - $searchCriteria = $this->searchCriteriaBuilder -- ->addFilter(AttributeGroupInterface::GROUP_ID, $group->getAttributeGroupId()) -+ ->addFilter(AttributeGroupInterface::GROUP_ID, array_keys($groupIds), 'in') - ->addFilter(ProductAttributeInterface::IS_VISIBLE, 1) - ->addSortOrder($sortOrder) - ->create(); -+ - $groupAttributes = $this->attributeRepository->getList($searchCriteria)->getItems(); -+ - $productType = $this->getProductType(); -+ - foreach ($groupAttributes as $attribute) { - $applyTo = $attribute->getApplyTo(); - $isRelated = !$applyTo || in_array($productType, $applyTo); - if ($isRelated) { -- $attributes[] = $attribute; -+ $attributeGroupId = $mapAttributeToGroup[$attribute->getAttributeId()]; -+ $attributeGroupCode = $groupIds[$attributeGroupId]; -+ $attributes[$attributeGroupCode][] = $attribute; - } - } - -@@ -613,7 +676,7 @@ class Eav extends AbstractModifier - // TODO: Refactor to $attribute->getOptions() when MAGETWO-48289 is done - $attributeModel = $this->getAttributeModel($attribute); - if ($attributeModel->usesSource()) { -- $options = $attributeModel->getSource()->getAllOptions(); -+ $options = $attributeModel->getSource()->getAllOptions(true, true); - $meta = $this->arrayManager->merge($configPath, $meta, [ - 'options' => $this->convertOptionsValueToString($options), - ]); -@@ -671,7 +734,8 @@ class Eav extends AbstractModifier - } - - /** -- * Returns attribute default value, based on db setting or setting in the system configuration -+ * Returns attribute default value, based on db setting or setting in the system configuration. -+ * - * @param ProductAttributeInterface $attribute - * @return null|string - */ -@@ -706,6 +770,8 @@ class Eav extends AbstractModifier - } - - /** -+ * Adds 'use default value' checkbox. -+ * - * @param ProductAttributeInterface $attribute - * @param array $meta - * @return array -@@ -854,7 +920,7 @@ class Eav extends AbstractModifier - { - $valueMap = $this->formElementMapper->getMappings(); - -- return isset($valueMap[$value]) ? $valueMap[$value] : $value; -+ return $valueMap[$value] ?? $value; - } - - /** -@@ -908,6 +974,9 @@ class Eav extends AbstractModifier - $attributeCode = $attribute->getAttributeCode(); - /** @var Product $product */ - $product = $this->locator->getProduct(); -+ if ($product->isLockedAttribute($attributeCode)) { -+ return false; -+ } - - if (isset($this->canDisplayUseDefault[$attributeCode])) { - return $this->canDisplayUseDefault[$attributeCode]; -@@ -942,7 +1011,13 @@ class Eav extends AbstractModifier - */ - private function getAttributeModel($attribute) - { -- return $this->eavAttributeFactory->create()->load($attribute->getAttributeId()); -+ $attributeId = $attribute->getAttributeId(); -+ -+ if (!array_key_exists($attributeId, $this->attributesCache)) { -+ $this->attributesCache[$attributeId] = $this->eavAttributeFactory->create()->load($attributeId); -+ } -+ -+ return $this->attributesCache[$attributeId]; - } - - /** -diff --git a/app/code/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/Eav/CompositeConfigProcessor.php b/app/code/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/Eav/CompositeConfigProcessor.php -index 5513af9d98e..fed94193225 100644 ---- a/app/code/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/Eav/CompositeConfigProcessor.php -+++ b/app/code/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/Eav/CompositeConfigProcessor.php -@@ -10,6 +10,9 @@ namespace Magento\Catalog\Ui\DataProvider\Product\Form\Modifier\Eav; - - use Psr\Log\LoggerInterface as Logger; - -+/** -+ * Process config for Wysiwyg. -+ */ - class CompositeConfigProcessor implements WysiwygConfigDataProcessorInterface - { - /** -@@ -24,6 +27,7 @@ class CompositeConfigProcessor implements WysiwygConfigDataProcessorInterface - - /** - * CompositeConfigProcessor constructor. -+ * @param Logger $logger - * @param array $eavWysiwygDataProcessors - */ - public function __construct(Logger $logger, array $eavWysiwygDataProcessors) -@@ -33,7 +37,7 @@ class CompositeConfigProcessor implements WysiwygConfigDataProcessorInterface - } - - /** -- * {@inheritdoc} -+ * @inheritdoc - */ - public function process(\Magento\Catalog\Api\Data\ProductAttributeInterface $attribute) - { -diff --git a/app/code/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/General.php b/app/code/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/General.php -index 98de8ea3476..26044eb91a3 100755 ---- a/app/code/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/General.php -+++ b/app/code/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/General.php -@@ -58,8 +58,11 @@ class General extends AbstractModifier - } - - /** -- * {@inheritdoc} -+ * Customize number fields for advanced price and weight fields. -+ * - * @since 101.0.0 -+ * @param array $data -+ * @return array - * @throws \Magento\Framework\Exception\NoSuchEntityException - */ - public function modifyData(array $data) -@@ -130,8 +133,11 @@ class General extends AbstractModifier - } - - /** -- * {@inheritdoc} -+ * Customize product form fields. -+ * - * @since 101.0.0 -+ * @param array $meta -+ * @return array - */ - public function modifyMeta(array $meta) - { -@@ -349,8 +355,10 @@ class General extends AbstractModifier - 'allowImport' => !$this->locator->getProduct()->getId(), - ]; - -- if (!in_array($listener, $textListeners)) { -- $importsConfig['elementTmpl'] = 'ui/form/element/input'; -+ if (in_array($listener, $textListeners)) { -+ $importsConfig['cols'] = 15; -+ $importsConfig['rows'] = 2; -+ $importsConfig['elementTmpl'] = 'ui/form/element/textarea'; - } - - $meta = $this->arrayManager->merge($listenerPath . static::META_CONFIG_PATH, $meta, $importsConfig); -@@ -361,7 +369,8 @@ class General extends AbstractModifier - $skuPath . static::META_CONFIG_PATH, - $meta, - [ -- 'autoImportIfEmpty' => true -+ 'autoImportIfEmpty' => true, -+ 'validation' => ['no-marginal-whitespace' => true] - ] - ); - -diff --git a/app/code/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/TierPrice.php b/app/code/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/TierPrice.php -index 0eddca33222..a529580e292 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 @@ class TierPrice extends AbstractModifier - } - - /** -- * {@inheritdoc} -+ * @inheritdoc - * @since 101.1.0 - */ - public function modifyData(array $data) -@@ -54,8 +54,11 @@ class TierPrice extends AbstractModifier - } - - /** -- * {@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 @@ class TierPrice extends AbstractModifier - 'dataType' => Price::NAME, - 'addbefore' => '%', - 'validation' => [ -- 'validate-number' => true, -- 'less-than-equals-to' => 100 -+ 'required-entry' => true, -+ 'validate-positive-percent-decimal' => true - ], - 'visible' => $firstOption - && $firstOption['value'] == ProductPriceOptionsInterface::VALUE_PERCENT, -diff --git a/app/code/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/Websites.php b/app/code/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/Websites.php -index bab36ce5fc4..b9d8fc56a91 100644 ---- a/app/code/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/Websites.php -+++ b/app/code/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/Websites.php -@@ -89,7 +89,7 @@ class Websites extends AbstractModifier - } - - /** -- * {@inheritdoc} -+ * @inheritdoc - * @since 101.0.0 - */ - public function modifyData(array $data) -@@ -117,7 +117,7 @@ class Websites extends AbstractModifier - } - - /** -- * {@inheritdoc} -+ * @inheritdoc - * @since 101.0.0 - */ - public function modifyMeta(array $meta) -@@ -165,7 +165,7 @@ class Websites extends AbstractModifier - $websitesList = $this->getWebsitesList(); - $isNewProduct = !$this->locator->getProduct()->getId(); - $tooltip = [ -- 'link' => 'http://docs.magento.com/m2/ce/user_guide/configuration/scope.html', -+ 'link' => 'https://docs.magento.com/m2/ce/user_guide/configuration/scope.html', - 'description' => __( - 'If your Magento installation has multiple websites, ' . - 'you can edit the scope to use the product on specific sites.' -@@ -331,6 +331,8 @@ class Websites extends AbstractModifier - } - - /** -+ * Returns websites options list. -+ * - * @return array - * @since 101.0.0 - */ -@@ -397,8 +399,9 @@ class Websites extends AbstractModifier - $this->websitesList = []; - $groupList = $this->groupRepository->getList(); - $storesList = $this->storeRepository->getList(); -+ $websiteList = $this->storeManager->getWebsites(true); - -- foreach ($this->websiteRepository->getList() as $website) { -+ foreach ($websiteList as $website) { - $websiteId = $website->getId(); - if (!$websiteId) { - continue; -diff --git a/app/code/Magento/Catalog/Ui/DataProvider/Product/Listing/Collector/Image.php b/app/code/Magento/Catalog/Ui/DataProvider/Product/Listing/Collector/Image.php -index 216bc16968f..4fcb87ab139 100644 ---- a/app/code/Magento/Catalog/Ui/DataProvider/Product/Listing/Collector/Image.php -+++ b/app/code/Magento/Catalog/Ui/DataProvider/Product/Listing/Collector/Image.php -@@ -14,15 +14,18 @@ use Magento\Catalog\Helper\ImageFactory; - use Magento\Catalog\Model\Product\Image\NotLoadInfoImageException; - use Magento\Catalog\Ui\DataProvider\Product\ProductRenderCollectorInterface; - use Magento\Framework\App\State; -+use Magento\Framework\View\Design\ThemeInterface; - use Magento\Framework\View\DesignInterface; - use Magento\Store\Model\StoreManager; - use Magento\Store\Model\StoreManagerInterface; -+use Magento\Framework\View\DesignLoader; - - /** - * Collect enough information about image rendering on front - * If you want to add new image, that should render on front you need - * to configure this class in di.xml - * -+ * @SuppressWarnings(PHPMD.CouplingBetweenObjects) - */ - class Image implements ProductRenderCollectorInterface - { -@@ -51,6 +54,7 @@ class Image implements ProductRenderCollectorInterface - - /** - * @var DesignInterface -+ * @deprecated 2.3.0 DesignLoader is used for design theme loading - */ - private $design; - -@@ -59,6 +63,11 @@ class Image implements ProductRenderCollectorInterface - */ - private $imageRenderInfoFactory; - -+ /** -+ * @var DesignLoader -+ */ -+ private $designLoader; -+ - /** - * Image constructor. - * @param ImageFactory $imageFactory -@@ -67,6 +76,7 @@ class Image implements ProductRenderCollectorInterface - * @param DesignInterface $design - * @param ImageInterfaceFactory $imageRenderInfoFactory - * @param array $imageCodes -+ * @param DesignLoader $designLoader - */ - public function __construct( - ImageFactory $imageFactory, -@@ -74,7 +84,8 @@ class Image implements ProductRenderCollectorInterface - StoreManagerInterface $storeManager, - DesignInterface $design, - ImageInterfaceFactory $imageRenderInfoFactory, -- array $imageCodes = [] -+ array $imageCodes = [], -+ DesignLoader $designLoader = null - ) { - $this->imageFactory = $imageFactory; - $this->imageCodes = $imageCodes; -@@ -82,6 +93,8 @@ class Image implements ProductRenderCollectorInterface - $this->storeManager = $storeManager; - $this->design = $design; - $this->imageRenderInfoFactory = $imageRenderInfoFactory; -+ $this->designLoader = $designLoader ?: \Magento\Framework\App\ObjectManager::getInstance() -+ ->get(DesignLoader::class); - } - - /** -@@ -92,6 +105,8 @@ class Image implements ProductRenderCollectorInterface - public function collect(ProductInterface $product, ProductRenderInterface $productRender) - { - $images = []; -+ /** @var ThemeInterface $currentTheme */ -+ $currentTheme = $this->design->getDesignTheme(); - - foreach ($this->imageCodes as $imageCode) { - /** @var ImageInterface $image */ -@@ -120,10 +135,13 @@ class Image implements ProductRenderCollectorInterface - $images[] = $image; - } - -+ $this->design->setDesignTheme($currentTheme); - $productRender->setImages($images); - } - - /** -+ * Callback for emulating image creation -+ * - * Callback in which we emulate initialize default design theme, depends on current store, be settings store id - * from render info - * -@@ -136,7 +154,7 @@ class Image implements ProductRenderCollectorInterface - public function emulateImageCreating(ProductInterface $product, $imageCode, $storeId, ImageInterface $image) - { - $this->storeManager->setCurrentStore($storeId); -- $this->design->setDefaultDesignTheme(); -+ $this->designLoader->load(); - - $imageHelper = $this->imageFactory->create(); - $imageHelper->init($product, $imageCode); -diff --git a/app/code/Magento/Catalog/Ui/DataProvider/Product/Listing/DataProvider.php b/app/code/Magento/Catalog/Ui/DataProvider/Product/Listing/DataProvider.php -index 3090734df01..4de0b94d068 100644 ---- a/app/code/Magento/Catalog/Ui/DataProvider/Product/Listing/DataProvider.php -+++ b/app/code/Magento/Catalog/Ui/DataProvider/Product/Listing/DataProvider.php -@@ -24,8 +24,6 @@ class DataProvider extends \Magento\Framework\View\Element\UiComponent\DataProvi - - /** - * @param string $name -- * @param string $primaryFieldName -- * @param string $requestFieldName - * @param Reporting $reporting - * @param SearchCriteriaBuilder $searchCriteriaBuilder - * @param RequestInterface $request -@@ -61,7 +59,7 @@ class DataProvider extends \Magento\Framework\View\Element\UiComponent\DataProvi - } - - /** -- * {@inheritdoc} -+ * @inheritdoc - */ - public function getData() - { -diff --git a/app/code/Magento/Catalog/Ui/DataProvider/Product/ProductCollection.php b/app/code/Magento/Catalog/Ui/DataProvider/Product/ProductCollection.php -index f4334bc25ef..ea8fc6f2d83 100644 ---- a/app/code/Magento/Catalog/Ui/DataProvider/Product/ProductCollection.php -+++ b/app/code/Magento/Catalog/Ui/DataProvider/Product/ProductCollection.php -@@ -5,10 +5,16 @@ - */ - namespace Magento\Catalog\Ui\DataProvider\Product; - -+use Magento\Catalog\Model\ResourceModel\Eav\Attribute; -+use Magento\Framework\Exception\LocalizedException; -+use Magento\Eav\Model\Entity\Attribute\AttributeInterface; -+ - /** - * Collection which is used for rendering product list in the backend. - * - * Used for product grid and customizes behavior of the default Product collection for grid needs. -+ * -+ * @SuppressWarnings(PHPMD.CookieAndSessionMisuse) - */ - class ProductCollection extends \Magento\Catalog\Model\ResourceModel\Product\Collection - { -@@ -25,4 +31,66 @@ class ProductCollection extends \Magento\Catalog\Model\ResourceModel\Product\Col - $this->_productLimitationFilters->setUsePriceIndex(false); - return $this->_productLimitationPrice(true); - } -+ -+ /** -+ * Add attribute filter to collection -+ * -+ * @param AttributeInterface|integer|string|array $attribute -+ * @param null|string|array $condition -+ * @param string $joinType -+ * @return $this -+ * @throws LocalizedException -+ */ -+ public function addAttributeToFilter($attribute, $condition = null, $joinType = 'inner') -+ { -+ $storeId = (int)$this->getStoreId(); -+ if ($attribute === 'is_saleable' -+ || is_array($attribute) -+ || $storeId !== $this->getDefaultStoreId() -+ ) { -+ return parent::addAttributeToFilter($attribute, $condition, $joinType); -+ } -+ -+ if ($attribute instanceof AttributeInterface) { -+ $attributeModel = $attribute; -+ } else { -+ $attributeModel = $this->getEntity()->getAttribute($attribute); -+ if ($attributeModel === false) { -+ throw new LocalizedException( -+ __('Invalid attribute identifier for filter (%1)', get_class($attribute)) -+ ); -+ } -+ } -+ -+ if ($attributeModel->isScopeGlobal() || $attributeModel->getBackend()->isStatic()) { -+ return parent::addAttributeToFilter($attribute, $condition, $joinType); -+ } -+ -+ $this->addAttributeToFilterAllStores($attributeModel, $condition); -+ -+ return $this; -+ } -+ -+ /** -+ * Add attribute to filter by all stores -+ * -+ * @param Attribute $attributeModel -+ * @param array $condition -+ * @return void -+ */ -+ private function addAttributeToFilterAllStores(Attribute $attributeModel, array $condition): void -+ { -+ $tableName = $this->getTable($attributeModel->getBackendTable()); -+ $entity = $this->getEntity(); -+ $fKey = 'e.' . $this->getEntityPkName($entity); -+ $pKey = $tableName . '.' . $this->getEntityPkName($entity); -+ $attributeId = $attributeModel->getAttributeId(); -+ $condition = "({$pKey} = {$fKey}) AND (" -+ . $this->_getConditionSql("{$tableName}.value", $condition) -+ . ') AND (' -+ . $this->_getConditionSql("{$tableName}.attribute_id", $attributeId) -+ . ')'; -+ $selectExistsInAllStores = $this->getConnection()->select()->from($tableName); -+ $this->getSelect()->exists($selectExistsInAllStores, $condition); -+ } - } -diff --git a/app/code/Magento/Catalog/Ui/DataProvider/Product/ProductDataProvider.php b/app/code/Magento/Catalog/Ui/DataProvider/Product/ProductDataProvider.php -index 200ecf89641..a518afc576d 100644 ---- a/app/code/Magento/Catalog/Ui/DataProvider/Product/ProductDataProvider.php -+++ b/app/code/Magento/Catalog/Ui/DataProvider/Product/ProductDataProvider.php -@@ -7,6 +7,7 @@ namespace Magento\Catalog\Ui\DataProvider\Product; - - use Magento\Catalog\Model\ResourceModel\Product\CollectionFactory; - use Magento\Framework\App\ObjectManager; -+use Magento\Store\Model\Store; - use Magento\Ui\DataProvider\Modifier\ModifierInterface; - use Magento\Ui\DataProvider\Modifier\PoolInterface; - -@@ -67,6 +68,7 @@ class ProductDataProvider extends \Magento\Ui\DataProvider\AbstractDataProvider - $this->addFieldStrategies = $addFieldStrategies; - $this->addFilterStrategies = $addFilterStrategies; - $this->modifiersPool = $modifiersPool ?: ObjectManager::getInstance()->get(PoolInterface::class); -+ $this->collection->setStoreId(Store::DEFAULT_STORE_ID); - } - - /** -@@ -110,7 +112,7 @@ class ProductDataProvider extends \Magento\Ui\DataProvider\AbstractDataProvider - } - - /** -- * {@inheritdoc} -+ * @inheritdoc - */ - public function addFilter(\Magento\Framework\Api\Filter $filter) - { -diff --git a/app/code/Magento/Catalog/Ui/DataProvider/Product/ProductRenderCollectorComposite.php b/app/code/Magento/Catalog/Ui/DataProvider/Product/ProductRenderCollectorComposite.php -index 359b1a1a948..7edf25ff20c 100644 ---- a/app/code/Magento/Catalog/Ui/DataProvider/Product/ProductRenderCollectorComposite.php -+++ b/app/code/Magento/Catalog/Ui/DataProvider/Product/ProductRenderCollectorComposite.php -@@ -9,8 +9,7 @@ use Magento\Catalog\Api\Data\ProductInterface; - use Magento\Catalog\Api\Data\ProductRenderInterface; - - /** -- * Composite, which holds collectors, that collect enought information for -- * product render -+ * Composite, which holds collectors, that collect enough information for product render - */ - class ProductRenderCollectorComposite implements ProductRenderCollectorInterface - { -diff --git a/app/code/Magento/Catalog/Ui/DataProvider/Product/ProductRenderCollectorInterface.php b/app/code/Magento/Catalog/Ui/DataProvider/Product/ProductRenderCollectorInterface.php -index 5d14cd21f7b..3f16e0a6617 100644 ---- a/app/code/Magento/Catalog/Ui/DataProvider/Product/ProductRenderCollectorInterface.php -+++ b/app/code/Magento/Catalog/Ui/DataProvider/Product/ProductRenderCollectorInterface.php -@@ -21,7 +21,6 @@ interface ProductRenderCollectorInterface - * - * @param ProductInterface $product - * @param ProductRenderInterface $productRender -- * @param array $data - * @return void - * @since 101.1.0 - */ -diff --git a/app/code/Magento/Catalog/ViewModel/Product/Breadcrumbs.php b/app/code/Magento/Catalog/ViewModel/Product/Breadcrumbs.php -index 95f2531e5fd..d1424d63793 100644 ---- a/app/code/Magento/Catalog/ViewModel/Product/Breadcrumbs.php -+++ b/app/code/Magento/Catalog/ViewModel/Product/Breadcrumbs.php -@@ -105,7 +105,7 @@ class Breadcrumbs extends DataObject implements ArgumentInterface - [ - 'breadcrumbs' => [ - 'categoryUrlSuffix' => $this->escaper->escapeHtml($this->getCategoryUrlSuffix()), -- 'userCategoryPathInUrl' => (int)$this->isCategoryUsedInProductUrl(), -+ 'useCategoryPathInUrl' => (int)$this->isCategoryUsedInProductUrl(), - 'product' => $this->escaper->escapeHtml($this->getProductName()) - ] - ], -diff --git a/app/code/Magento/Catalog/ViewModel/Product/Checker/AddToCompareAvailability.php b/app/code/Magento/Catalog/ViewModel/Product/Checker/AddToCompareAvailability.php -new file mode 100644 -index 00000000000..27829155af2 ---- /dev/null -+++ b/app/code/Magento/Catalog/ViewModel/Product/Checker/AddToCompareAvailability.php -@@ -0,0 +1,58 @@ -+<?php -+/** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+declare(strict_types=1); -+ -+namespace Magento\Catalog\ViewModel\Product\Checker; -+ -+use Magento\Framework\View\Element\Block\ArgumentInterface; -+use Magento\Catalog\Api\Data\ProductInterface; -+use Magento\CatalogInventory\Api\StockConfigurationInterface; -+ -+/** -+ * Check is available add to compare. -+ */ -+class AddToCompareAvailability implements ArgumentInterface -+{ -+ /** -+ * @var StockConfigurationInterface -+ */ -+ private $stockConfiguration; -+ -+ /** -+ * @param StockConfigurationInterface $stockConfiguration -+ */ -+ public function __construct(StockConfigurationInterface $stockConfiguration) -+ { -+ $this->stockConfiguration = $stockConfiguration; -+ } -+ -+ /** -+ * Is product available for comparison. -+ * -+ * @param ProductInterface $product -+ * @return bool -+ */ -+ public function isAvailableForCompare(ProductInterface $product): bool -+ { -+ return $this->isInStock($product) || $this->stockConfiguration->isShowOutOfStock(); -+ } -+ -+ /** -+ * Get is in stock status. -+ * -+ * @param ProductInterface $product -+ * @return bool -+ */ -+ private function isInStock(ProductInterface $product): bool -+ { -+ $quantityAndStockStatus = $product->getQuantityAndStockStatus(); -+ if (!$quantityAndStockStatus) { -+ return $product->isSalable(); -+ } -+ -+ return isset($quantityAndStockStatus['is_in_stock']) && $quantityAndStockStatus['is_in_stock']; -+ } -+} -diff --git a/app/code/Magento/Catalog/composer.json b/app/code/Magento/Catalog/composer.json -index 44d05193390..5c3ee3da8ca 100644 ---- a/app/code/Magento/Catalog/composer.json -+++ b/app/code/Magento/Catalog/composer.json -@@ -7,6 +7,8 @@ - "require": { - "php": "~7.1.3||~7.2.0", - "magento/framework": "*", -+ "magento/module-authorization": "*", -+ "magento/module-asynchronous-operations": "*", - "magento/module-backend": "*", - "magento/module-catalog-inventory": "*", - "magento/module-catalog-rule": "*", -diff --git a/app/code/Magento/Catalog/etc/acl.xml b/app/code/Magento/Catalog/etc/acl.xml -index 358a798fc75..4d4b7bdc672 100644 ---- a/app/code/Magento/Catalog/etc/acl.xml -+++ b/app/code/Magento/Catalog/etc/acl.xml -@@ -11,7 +11,9 @@ - <resource id="Magento_Backend::admin"> - <resource id="Magento_Catalog::catalog" title="Catalog" translate="title" sortOrder="30"> - <resource id="Magento_Catalog::catalog_inventory" title="Inventory" translate="title" sortOrder="10"> -- <resource id="Magento_Catalog::products" title="Products" translate="title" sortOrder="10" /> -+ <resource id="Magento_Catalog::products" title="Products" translate="title" sortOrder="10"> -+ <resource id="Magento_Catalog::update_attributes" title="Update Attributes" translate="title" /> -+ </resource> - <resource id="Magento_Catalog::categories" title="Categories" translate="title" sortOrder="20" /> - </resource> - </resource> -@@ -23,7 +25,6 @@ - </resource> - <resource id="Magento_Backend::stores_attributes"> - <resource id="Magento_Catalog::attributes_attributes" title="Product" translate="title" sortOrder="30" /> -- <resource id="Magento_Catalog::update_attributes" title="Update Attributes" translate="title" sortOrder="35" /> - <resource id="Magento_Catalog::sets" title="Attribute Set" translate="title" sortOrder="40"/> - </resource> - </resource> -diff --git a/app/code/Magento/Catalog/etc/adminhtml/di.xml b/app/code/Magento/Catalog/etc/adminhtml/di.xml -index 10251d35dff..c04cfb2dce0 100644 ---- a/app/code/Magento/Catalog/etc/adminhtml/di.xml -+++ b/app/code/Magento/Catalog/etc/adminhtml/di.xml -@@ -78,7 +78,7 @@ - <type name="Magento\Catalog\Model\ResourceModel\Attribute"> - <plugin name="invalidate_pagecache_after_attribute_save" type="Magento\Catalog\Plugin\Model\ResourceModel\Attribute\Save" /> - </type> -- <virtualType name="\Magento\Catalog\Ui\DataProvider\Product\ProductCollectionFactory" type="\Magento\Catalog\Model\ResourceModel\Product\CollectionFactory"> -+ <virtualType name="Magento\Catalog\Ui\DataProvider\Product\ProductCollectionFactory" type="Magento\Catalog\Model\ResourceModel\Product\CollectionFactory"> - <arguments> - <argument name="instanceName" xsi:type="string">\Magento\Catalog\Ui\DataProvider\Product\ProductCollection</argument> - </arguments> -@@ -220,4 +220,16 @@ - <argument name="filter" xsi:type="object">Magento\Catalog\Ui\DataProvider\Product\AddSearchKeyConditionToCollection</argument> - </arguments> - </type> -+ <type name="Magento\Catalog\Block\Adminhtml\Product\Edit\Action\Attribute\Tab\Attributes"> -+ <arguments> -+ <argument name="excludeFields" xsi:type="array"> -+ <item name="0" xsi:type="string">category_ids</item> -+ <item name="1" xsi:type="string">gallery</item> -+ <item name="2" xsi:type="string">image</item> -+ <item name="3" xsi:type="string">media_gallery</item> -+ <item name="4" xsi:type="string">quantity_and_stock_status</item> -+ <item name="5" xsi:type="string">tier_price</item> -+ </argument> -+ </arguments> -+ </type> - </config> -diff --git a/app/code/Magento/Catalog/etc/adminhtml/events.xml b/app/code/Magento/Catalog/etc/adminhtml/events.xml -index f4fd7fc3039..ad83f589823 100644 ---- a/app/code/Magento/Catalog/etc/adminhtml/events.xml -+++ b/app/code/Magento/Catalog/etc/adminhtml/events.xml -@@ -9,4 +9,7 @@ - <event name="cms_wysiwyg_images_static_urls_allowed"> - <observer name="catalog_wysiwyg" instance="Magento\Catalog\Observer\CatalogCheckIsUsingStaticUrlsAllowedObserver" /> - </event> -+ <event name="catalog_category_change_products"> -+ <observer name="category_product_indexer" instance="Magento\Catalog\Observer\CategoryProductIndexer"/> -+ </event> - </config> -diff --git a/app/code/Magento/Catalog/etc/adminhtml/system.xml b/app/code/Magento/Catalog/etc/adminhtml/system.xml -index 71a799fd224..a6dd6cbd2e9 100644 ---- a/app/code/Magento/Catalog/etc/adminhtml/system.xml -+++ b/app/code/Magento/Catalog/etc/adminhtml/system.xml -@@ -10,6 +10,9 @@ - <tab id="catalog" translate="label" sortOrder="200"> - <label>Catalog</label> - </tab> -+ <tab id="advanced" translate="label" sortOrder="999999"> -+ <label>Advanced</label> -+ </tab> - <section id="catalog" translate="label" type="text" sortOrder="40" showInDefault="1" showInWebsite="1" showInStore="1"> - <class>separator-top</class> - <label>Catalog</label> -@@ -56,7 +59,7 @@ - <field id="grid_per_page_values" translate="label comment" type="text" sortOrder="2" showInDefault="1" showInWebsite="1" showInStore="1" canRestore="1"> - <label>Products per Page on Grid Allowed Values</label> - <comment>Comma-separated.</comment> -- <validate>validate-per-page-value-list</validate> -+ <validate>validate-per-page-value-list required-entry</validate> - </field> - <field id="grid_per_page" translate="label comment" type="text" sortOrder="3" showInDefault="1" showInWebsite="1" showInStore="1" canRestore="1"> - <label>Products per Page on Grid Default Value</label> -@@ -66,7 +69,7 @@ - <field id="list_per_page_values" translate="label comment" type="text" sortOrder="4" showInDefault="1" showInWebsite="1" showInStore="1" canRestore="1"> - <label>Products per Page on List Allowed Values</label> - <comment>Comma-separated.</comment> -- <validate>validate-per-page-value-list</validate> -+ <validate>validate-per-page-value-list required-entry</validate> - </field> - <field id="list_per_page" translate="label comment" type="text" sortOrder="5" showInDefault="1" showInWebsite="1" showInStore="1" canRestore="1"> - <label>Products per Page on List Default Value</label> -@@ -83,8 +86,9 @@ - <backend_model>Magento\Catalog\Model\Indexer\Product\Flat\System\Config\Mode</backend_model> - <source_model>Magento\Config\Model\Config\Source\Yesno</source_model> - </field> -- <field id="default_sort_by" translate="label" type="select" sortOrder="6" showInDefault="1" showInWebsite="1" showInStore="1" canRestore="1"> -+ <field id="default_sort_by" translate="label comment" type="select" sortOrder="6" showInDefault="1" showInWebsite="1" showInStore="1" canRestore="1"> - <label>Product Listing Sort by</label> -+ <comment>Applies to category pages</comment> - <source_model>Magento\Catalog\Model\Config\Source\ListSort</source_model> - </field> - <field id="list_allow_all" translate="label comment" type="select" sortOrder="6" showInDefault="1" showInWebsite="1" showInStore="1"> -@@ -92,6 +96,11 @@ - <comment>Whether to show "All" option in the "Show X Per Page" dropdown</comment> - <source_model>Magento\Config\Model\Config\Source\Yesno</source_model> - </field> -+ <field id="remember_pagination" translate="label comment" type="select" sortOrder="7" showInDefault="1" showInWebsite="0" showInStore="0" canRestore="1"> -+ <label>Remember Category Pagination</label> -+ <comment>Changing may affect SEO and cache storage consumption.</comment> -+ <source_model>Magento\Config\Model\Config\Source\Yesno</source_model> -+ </field> - </group> - <group id="placeholder" translate="label" sortOrder="300" showInDefault="1" showInWebsite="1" showInStore="1"> - <label>Product Image Placeholders</label> -@@ -105,14 +114,14 @@ - </group> - <group id="seo" translate="label" type="text" sortOrder="500" showInDefault="1" showInWebsite="1" showInStore="1"> - <label>Search Engine Optimization</label> -- <field id="title_separator" translate="label" type="text" sortOrder="6" showInDefault="1" showInWebsite="1" showInStore="1" canRestore="1"> -+ <field id="title_separator" translate="label" type="text" sortOrder="7" showInDefault="1" showInWebsite="1" showInStore="1" canRestore="1"> - <label>Page Title Separator</label> - </field> -- <field id="category_canonical_tag" translate="label" type="select" sortOrder="7" showInDefault="1" showInWebsite="1" showInStore="1" canRestore="1"> -+ <field id="category_canonical_tag" translate="label" type="select" sortOrder="8" showInDefault="1" showInWebsite="1" showInStore="1" canRestore="1"> - <label>Use Canonical Link Meta Tag For Categories</label> - <source_model>Magento\Config\Model\Config\Source\Yesno</source_model> - </field> -- <field id="product_canonical_tag" translate="label" type="select" sortOrder="8" showInDefault="1" showInWebsite="1" showInStore="1" canRestore="1"> -+ <field id="product_canonical_tag" translate="label" type="select" sortOrder="9" showInDefault="1" showInWebsite="1" showInStore="1" canRestore="1"> - <label>Use Canonical Link Meta Tag For Products</label> - <source_model>Magento\Config\Model\Config\Source\Yesno</source_model> - </field> -@@ -193,5 +202,19 @@ - </field> - </group> - </section> -+ <section id="system" translate="label" type="text" sortOrder="900" showInDefault="1" showInWebsite="1" showInStore="1"> -+ <class>separator-top</class> -+ <label>System</label> -+ <tab>advanced</tab> -+ <resource>Magento_Config::config_system</resource> -+ <group id="upload_configuration" translate="label" type="text" sortOrder="1000" showInDefault="1" showInWebsite="1" showInStore="1"> -+ <label>Images Upload Configuration</label> -+ <field id="jpeg_quality" translate="label comment" type="text" sortOrder="100" showInDefault="1" showInWebsite="0" showInStore="0" canRestore="1"> -+ <label>Quality</label> -+ <validate>validate-digits validate-digits-range digits-range-1-100 required-entry</validate> -+ <comment>Jpeg quality for resized images 1-100%.</comment> -+ </field> -+ </group> -+ </section> - </system> - </config> -diff --git a/app/code/Magento/Catalog/etc/communication.xml b/app/code/Magento/Catalog/etc/communication.xml -new file mode 100644 -index 00000000000..1a957f6ac9f ---- /dev/null -+++ b/app/code/Magento/Catalog/etc/communication.xml -@@ -0,0 +1,15 @@ -+<?xml version="1.0"?> -+<!-- -+/** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:Communication/etc/communication.xsd"> -+ <topic name="product_action_attribute.update" request="Magento\AsynchronousOperations\Api\Data\OperationInterface"> -+ <handler name="product_action_attribute.update" type="Magento\Catalog\Model\Attribute\Backend\Consumer" method="process" /> -+ </topic> -+ <topic name="product_action_attribute.website.update" request="Magento\AsynchronousOperations\Api\Data\OperationInterface"> -+ <handler name="product_action_attribute.website.update" type="Magento\Catalog\Model\Attribute\Backend\ConsumerWebsiteAssign" method="process" /> -+ </topic> -+</config> -\ No newline at end of file -diff --git a/app/code/Magento/Catalog/etc/config.xml b/app/code/Magento/Catalog/etc/config.xml -index 1d92197e390..3a842166a38 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> -@@ -52,6 +53,11 @@ - <forbidden_extensions>php,exe</forbidden_extensions> - </custom_options> - </catalog> -+ <indexer> -+ <catalog_product_price> -+ <dimensions_mode>none</dimensions_mode> -+ </catalog_product_price> -+ </indexer> - <system> - <media_storage_configuration> - <allowed_resources> -@@ -61,6 +67,9 @@ - <product_custom_options_fodler>custom_options</product_custom_options_fodler> - </allowed_resources> - </media_storage_configuration> -+ <upload_configuration> -+ <jpeg_quality>80</jpeg_quality> -+ </upload_configuration> - </system> - <design> - <watermark> -diff --git a/app/code/Magento/Catalog/etc/crontab.xml b/app/code/Magento/Catalog/etc/crontab.xml -index c48f1307a09..74c60323530 100644 ---- a/app/code/Magento/Catalog/etc/crontab.xml -+++ b/app/code/Magento/Catalog/etc/crontab.xml -@@ -16,7 +16,7 @@ - <job name="catalog_product_outdated_price_values_cleanup" instance="Magento\Catalog\Cron\DeleteOutdatedPriceValues" method="execute"> - <schedule>* * * * *</schedule> - </job> -- <job name="catalog_product_frontend_actions_flush" instance="Magento\Catalog\Cron\DeleteOutdatedPriceValues" method="execute"> -+ <job name="catalog_product_frontend_actions_flush" instance="Magento\Catalog\Cron\FrontendActionsFlush" method="execute"> - <schedule>* * * * *</schedule> - </job> - <job name="catalog_product_attribute_value_synchronize" instance="Magento\Catalog\Cron\SynchronizeWebsiteAttributes" method="execute"> -diff --git a/app/code/Magento/Catalog/etc/db_schema.xml b/app/code/Magento/Catalog/etc/db_schema.xml -index 7b4e1a96add..6fef4ca6e91 100644 ---- a/app/code/Magento/Catalog/etc/db_schema.xml -+++ b/app/code/Magento/Catalog/etc/db_schema.xml -@@ -9,7 +9,7 @@ - xsi:noNamespaceSchemaLocation="urn:magento:framework:Setup/Declaration/Schema/etc/schema.xsd"> - <table name="catalog_product_entity" resource="default" engine="innodb" comment="Catalog Product Table"> - <column xsi:type="int" name="entity_id" padding="10" unsigned="true" nullable="false" identity="true" -- comment="Entity Id"/> -+ comment="Entity ID"/> - <column xsi:type="smallint" name="attribute_set_id" padding="5" unsigned="true" nullable="false" - identity="false" default="0" comment="Attribute Set ID"/> - <column xsi:type="varchar" name="type_id" nullable="false" length="32" default="simple" comment="Type ID"/> -@@ -22,13 +22,13 @@ - comment="Creation Time"/> - <column xsi:type="timestamp" name="updated_at" on_update="true" nullable="false" default="CURRENT_TIMESTAMP" - comment="Update Time"/> -- <constraint xsi:type="primary" name="PRIMARY"> -+ <constraint xsi:type="primary" referenceId="PRIMARY"> - <column name="entity_id"/> - </constraint> -- <index name="CATALOG_PRODUCT_ENTITY_ATTRIBUTE_SET_ID" indexType="btree"> -+ <index referenceId="CATALOG_PRODUCT_ENTITY_ATTRIBUTE_SET_ID" indexType="btree"> - <column name="attribute_set_id"/> - </index> -- <index name="CATALOG_PRODUCT_ENTITY_SKU" indexType="btree"> -+ <index referenceId="CATALOG_PRODUCT_ENTITY_SKU" indexType="btree"> - <column name="sku"/> - </index> - </table> -@@ -41,29 +41,29 @@ - <column xsi:type="smallint" name="store_id" padding="5" unsigned="true" nullable="false" identity="false" - default="0" comment="Store ID"/> - <column xsi:type="int" name="entity_id" padding="10" unsigned="true" nullable="false" identity="false" -- default="0"/> -+ default="0" comment="Entity ID"/> - <column xsi:type="datetime" name="value" on_update="false" nullable="true" comment="Value"/> -- <constraint xsi:type="primary" name="PRIMARY"> -+ <constraint xsi:type="primary" referenceId="PRIMARY"> - <column name="value_id"/> - </constraint> -- <constraint xsi:type="foreign" name="CAT_PRD_ENTT_DTIME_ATTR_ID_EAV_ATTR_ATTR_ID" -+ <constraint xsi:type="foreign" referenceId="CAT_PRD_ENTT_DTIME_ATTR_ID_EAV_ATTR_ATTR_ID" - table="catalog_product_entity_datetime" column="attribute_id" referenceTable="eav_attribute" - referenceColumn="attribute_id" onDelete="CASCADE"/> -- <constraint xsi:type="foreign" name="CAT_PRD_ENTT_DTIME_ENTT_ID_CAT_PRD_ENTT_ENTT_ID" -+ <constraint xsi:type="foreign" referenceId="CAT_PRD_ENTT_DTIME_ENTT_ID_CAT_PRD_ENTT_ENTT_ID" - table="catalog_product_entity_datetime" column="entity_id" referenceTable="catalog_product_entity" - referenceColumn="entity_id" onDelete="CASCADE"/> -- <constraint xsi:type="foreign" name="CATALOG_PRODUCT_ENTITY_DATETIME_STORE_ID_STORE_STORE_ID" -+ <constraint xsi:type="foreign" referenceId="CATALOG_PRODUCT_ENTITY_DATETIME_STORE_ID_STORE_STORE_ID" - table="catalog_product_entity_datetime" column="store_id" referenceTable="store" - referenceColumn="store_id" onDelete="CASCADE"/> -- <constraint xsi:type="unique" name="CATALOG_PRODUCT_ENTITY_DATETIME_ENTITY_ID_ATTRIBUTE_ID_STORE_ID"> -+ <constraint xsi:type="unique" referenceId="CATALOG_PRODUCT_ENTITY_DATETIME_ENTITY_ID_ATTRIBUTE_ID_STORE_ID"> - <column name="entity_id"/> - <column name="attribute_id"/> - <column name="store_id"/> - </constraint> -- <index name="CATALOG_PRODUCT_ENTITY_DATETIME_ATTRIBUTE_ID" indexType="btree"> -+ <index referenceId="CATALOG_PRODUCT_ENTITY_DATETIME_ATTRIBUTE_ID" indexType="btree"> - <column name="attribute_id"/> - </index> -- <index name="CATALOG_PRODUCT_ENTITY_DATETIME_STORE_ID" indexType="btree"> -+ <index referenceId="CATALOG_PRODUCT_ENTITY_DATETIME_STORE_ID" indexType="btree"> - <column name="store_id"/> - </index> - </table> -@@ -76,30 +76,30 @@ - <column xsi:type="smallint" name="store_id" padding="5" unsigned="true" nullable="false" identity="false" - default="0" comment="Store ID"/> - <column xsi:type="int" name="entity_id" padding="10" unsigned="true" nullable="false" identity="false" -- default="0"/> -- <column xsi:type="decimal" name="value" scale="4" precision="12" unsigned="false" nullable="true" -+ default="0" comment="Entity ID"/> -+ <column xsi:type="decimal" name="value" scale="6" precision="20" unsigned="false" nullable="true" - comment="Value"/> -- <constraint xsi:type="primary" name="PRIMARY"> -+ <constraint xsi:type="primary" referenceId="PRIMARY"> - <column name="value_id"/> - </constraint> -- <constraint xsi:type="foreign" name="CAT_PRD_ENTT_DEC_ATTR_ID_EAV_ATTR_ATTR_ID" -+ <constraint xsi:type="foreign" referenceId="CAT_PRD_ENTT_DEC_ATTR_ID_EAV_ATTR_ATTR_ID" - table="catalog_product_entity_decimal" column="attribute_id" referenceTable="eav_attribute" - referenceColumn="attribute_id" onDelete="CASCADE"/> -- <constraint xsi:type="foreign" name="CAT_PRD_ENTT_DEC_ENTT_ID_CAT_PRD_ENTT_ENTT_ID" -+ <constraint xsi:type="foreign" referenceId="CAT_PRD_ENTT_DEC_ENTT_ID_CAT_PRD_ENTT_ENTT_ID" - table="catalog_product_entity_decimal" column="entity_id" referenceTable="catalog_product_entity" - referenceColumn="entity_id" onDelete="CASCADE"/> -- <constraint xsi:type="foreign" name="CATALOG_PRODUCT_ENTITY_DECIMAL_STORE_ID_STORE_STORE_ID" -+ <constraint xsi:type="foreign" referenceId="CATALOG_PRODUCT_ENTITY_DECIMAL_STORE_ID_STORE_STORE_ID" - table="catalog_product_entity_decimal" column="store_id" referenceTable="store" - referenceColumn="store_id" onDelete="CASCADE"/> -- <constraint xsi:type="unique" name="CATALOG_PRODUCT_ENTITY_DECIMAL_ENTITY_ID_ATTRIBUTE_ID_STORE_ID"> -+ <constraint xsi:type="unique" referenceId="CATALOG_PRODUCT_ENTITY_DECIMAL_ENTITY_ID_ATTRIBUTE_ID_STORE_ID"> - <column name="entity_id"/> - <column name="attribute_id"/> - <column name="store_id"/> - </constraint> -- <index name="CATALOG_PRODUCT_ENTITY_DECIMAL_STORE_ID" indexType="btree"> -+ <index referenceId="CATALOG_PRODUCT_ENTITY_DECIMAL_STORE_ID" indexType="btree"> - <column name="store_id"/> - </index> -- <index name="CATALOG_PRODUCT_ENTITY_DECIMAL_ATTRIBUTE_ID" indexType="btree"> -+ <index referenceId="CATALOG_PRODUCT_ENTITY_DECIMAL_ATTRIBUTE_ID" indexType="btree"> - <column name="attribute_id"/> - </index> - </table> -@@ -112,30 +112,30 @@ - <column xsi:type="smallint" name="store_id" padding="5" unsigned="true" nullable="false" identity="false" - default="0" comment="Store ID"/> - <column xsi:type="int" name="entity_id" padding="10" unsigned="true" nullable="false" identity="false" -- default="0"/> -+ default="0" comment="Entity ID"/> - <column xsi:type="int" name="value" padding="11" unsigned="false" nullable="true" identity="false" - comment="Value"/> -- <constraint xsi:type="primary" name="PRIMARY"> -+ <constraint xsi:type="primary" referenceId="PRIMARY"> - <column name="value_id"/> - </constraint> -- <constraint xsi:type="foreign" name="CAT_PRD_ENTT_INT_ATTR_ID_EAV_ATTR_ATTR_ID" -+ <constraint xsi:type="foreign" referenceId="CAT_PRD_ENTT_INT_ATTR_ID_EAV_ATTR_ATTR_ID" - table="catalog_product_entity_int" column="attribute_id" referenceTable="eav_attribute" - referenceColumn="attribute_id" onDelete="CASCADE"/> -- <constraint xsi:type="foreign" name="CAT_PRD_ENTT_INT_ENTT_ID_CAT_PRD_ENTT_ENTT_ID" -+ <constraint xsi:type="foreign" referenceId="CAT_PRD_ENTT_INT_ENTT_ID_CAT_PRD_ENTT_ENTT_ID" - table="catalog_product_entity_int" column="entity_id" referenceTable="catalog_product_entity" - referenceColumn="entity_id" onDelete="CASCADE"/> -- <constraint xsi:type="foreign" name="CATALOG_PRODUCT_ENTITY_INT_STORE_ID_STORE_STORE_ID" -+ <constraint xsi:type="foreign" referenceId="CATALOG_PRODUCT_ENTITY_INT_STORE_ID_STORE_STORE_ID" - table="catalog_product_entity_int" column="store_id" referenceTable="store" - referenceColumn="store_id" onDelete="CASCADE"/> -- <constraint xsi:type="unique" name="CATALOG_PRODUCT_ENTITY_INT_ENTITY_ID_ATTRIBUTE_ID_STORE_ID"> -+ <constraint xsi:type="unique" referenceId="CATALOG_PRODUCT_ENTITY_INT_ENTITY_ID_ATTRIBUTE_ID_STORE_ID"> - <column name="entity_id"/> - <column name="attribute_id"/> - <column name="store_id"/> - </constraint> -- <index name="CATALOG_PRODUCT_ENTITY_INT_ATTRIBUTE_ID" indexType="btree"> -+ <index referenceId="CATALOG_PRODUCT_ENTITY_INT_ATTRIBUTE_ID" indexType="btree"> - <column name="attribute_id"/> - </index> -- <index name="CATALOG_PRODUCT_ENTITY_INT_STORE_ID" indexType="btree"> -+ <index referenceId="CATALOG_PRODUCT_ENTITY_INT_STORE_ID" indexType="btree"> - <column name="store_id"/> - </index> - </table> -@@ -148,29 +148,29 @@ - <column xsi:type="smallint" name="store_id" padding="5" unsigned="true" nullable="false" identity="false" - default="0" comment="Store ID"/> - <column xsi:type="int" name="entity_id" padding="10" unsigned="true" nullable="false" identity="false" -- default="0"/> -+ default="0" comment="Entity ID"/> - <column xsi:type="text" name="value" nullable="true" comment="Value"/> -- <constraint xsi:type="primary" name="PRIMARY"> -+ <constraint xsi:type="primary" referenceId="PRIMARY"> - <column name="value_id"/> - </constraint> -- <constraint xsi:type="foreign" name="CAT_PRD_ENTT_TEXT_ATTR_ID_EAV_ATTR_ATTR_ID" -+ <constraint xsi:type="foreign" referenceId="CAT_PRD_ENTT_TEXT_ATTR_ID_EAV_ATTR_ATTR_ID" - table="catalog_product_entity_text" column="attribute_id" referenceTable="eav_attribute" - referenceColumn="attribute_id" onDelete="CASCADE"/> -- <constraint xsi:type="foreign" name="CAT_PRD_ENTT_TEXT_ENTT_ID_CAT_PRD_ENTT_ENTT_ID" -+ <constraint xsi:type="foreign" referenceId="CAT_PRD_ENTT_TEXT_ENTT_ID_CAT_PRD_ENTT_ENTT_ID" - table="catalog_product_entity_text" column="entity_id" referenceTable="catalog_product_entity" - referenceColumn="entity_id" onDelete="CASCADE"/> -- <constraint xsi:type="foreign" name="CATALOG_PRODUCT_ENTITY_TEXT_STORE_ID_STORE_STORE_ID" -+ <constraint xsi:type="foreign" referenceId="CATALOG_PRODUCT_ENTITY_TEXT_STORE_ID_STORE_STORE_ID" - table="catalog_product_entity_text" column="store_id" referenceTable="store" - referenceColumn="store_id" onDelete="CASCADE"/> -- <constraint xsi:type="unique" name="CATALOG_PRODUCT_ENTITY_TEXT_ENTITY_ID_ATTRIBUTE_ID_STORE_ID"> -+ <constraint xsi:type="unique" referenceId="CATALOG_PRODUCT_ENTITY_TEXT_ENTITY_ID_ATTRIBUTE_ID_STORE_ID"> - <column name="entity_id"/> - <column name="attribute_id"/> - <column name="store_id"/> - </constraint> -- <index name="CATALOG_PRODUCT_ENTITY_TEXT_ATTRIBUTE_ID" indexType="btree"> -+ <index referenceId="CATALOG_PRODUCT_ENTITY_TEXT_ATTRIBUTE_ID" indexType="btree"> - <column name="attribute_id"/> - </index> -- <index name="CATALOG_PRODUCT_ENTITY_TEXT_STORE_ID" indexType="btree"> -+ <index referenceId="CATALOG_PRODUCT_ENTITY_TEXT_STORE_ID" indexType="btree"> - <column name="store_id"/> - </index> - </table> -@@ -183,29 +183,29 @@ - <column xsi:type="smallint" name="store_id" padding="5" unsigned="true" nullable="false" identity="false" - default="0" comment="Store ID"/> - <column xsi:type="int" name="entity_id" padding="10" unsigned="true" nullable="false" identity="false" -- default="0"/> -+ default="0" comment="Entity ID"/> - <column xsi:type="varchar" name="value" nullable="true" length="255" comment="Value"/> -- <constraint xsi:type="primary" name="PRIMARY"> -+ <constraint xsi:type="primary" referenceId="PRIMARY"> - <column name="value_id"/> - </constraint> -- <constraint xsi:type="foreign" name="CAT_PRD_ENTT_VCHR_ATTR_ID_EAV_ATTR_ATTR_ID" -+ <constraint xsi:type="foreign" referenceId="CAT_PRD_ENTT_VCHR_ATTR_ID_EAV_ATTR_ATTR_ID" - table="catalog_product_entity_varchar" column="attribute_id" referenceTable="eav_attribute" - referenceColumn="attribute_id" onDelete="CASCADE"/> -- <constraint xsi:type="foreign" name="CAT_PRD_ENTT_VCHR_ENTT_ID_CAT_PRD_ENTT_ENTT_ID" -+ <constraint xsi:type="foreign" referenceId="CAT_PRD_ENTT_VCHR_ENTT_ID_CAT_PRD_ENTT_ENTT_ID" - table="catalog_product_entity_varchar" column="entity_id" referenceTable="catalog_product_entity" - referenceColumn="entity_id" onDelete="CASCADE"/> -- <constraint xsi:type="foreign" name="CATALOG_PRODUCT_ENTITY_VARCHAR_STORE_ID_STORE_STORE_ID" -+ <constraint xsi:type="foreign" referenceId="CATALOG_PRODUCT_ENTITY_VARCHAR_STORE_ID_STORE_STORE_ID" - table="catalog_product_entity_varchar" column="store_id" referenceTable="store" - referenceColumn="store_id" onDelete="CASCADE"/> -- <constraint xsi:type="unique" name="CATALOG_PRODUCT_ENTITY_VARCHAR_ENTITY_ID_ATTRIBUTE_ID_STORE_ID"> -+ <constraint xsi:type="unique" referenceId="CATALOG_PRODUCT_ENTITY_VARCHAR_ENTITY_ID_ATTRIBUTE_ID_STORE_ID"> - <column name="entity_id"/> - <column name="attribute_id"/> - <column name="store_id"/> - </constraint> -- <index name="CATALOG_PRODUCT_ENTITY_VARCHAR_ATTRIBUTE_ID" indexType="btree"> -+ <index referenceId="CATALOG_PRODUCT_ENTITY_VARCHAR_ATTRIBUTE_ID" indexType="btree"> - <column name="attribute_id"/> - </index> -- <index name="CATALOG_PRODUCT_ENTITY_VARCHAR_STORE_ID" indexType="btree"> -+ <index referenceId="CATALOG_PRODUCT_ENTITY_VARCHAR_STORE_ID" indexType="btree"> - <column name="store_id"/> - </index> - </table> -@@ -218,40 +218,40 @@ - <column xsi:type="smallint" name="store_id" padding="5" unsigned="true" nullable="false" identity="false" - default="0" comment="Store ID"/> - <column xsi:type="int" name="entity_id" padding="10" unsigned="true" nullable="false" identity="false" -- default="0"/> -+ default="0" comment="Entity ID"/> - <column xsi:type="int" name="position" padding="11" unsigned="false" nullable="false" identity="false" - default="0" comment="Position"/> - <column xsi:type="varchar" name="value" nullable="true" length="255" comment="Value"/> -- <constraint xsi:type="primary" name="PRIMARY"> -+ <constraint xsi:type="primary" referenceId="PRIMARY"> - <column name="value_id"/> - </constraint> -- <constraint xsi:type="foreign" name="CAT_PRD_ENTT_GLR_ATTR_ID_EAV_ATTR_ATTR_ID" -+ <constraint xsi:type="foreign" referenceId="CAT_PRD_ENTT_GLR_ATTR_ID_EAV_ATTR_ATTR_ID" - table="catalog_product_entity_gallery" column="attribute_id" referenceTable="eav_attribute" - referenceColumn="attribute_id" onDelete="CASCADE"/> -- <constraint xsi:type="foreign" name="CAT_PRD_ENTT_GLR_ENTT_ID_CAT_PRD_ENTT_ENTT_ID" -+ <constraint xsi:type="foreign" referenceId="CAT_PRD_ENTT_GLR_ENTT_ID_CAT_PRD_ENTT_ENTT_ID" - table="catalog_product_entity_gallery" column="entity_id" referenceTable="catalog_product_entity" - referenceColumn="entity_id" onDelete="CASCADE"/> -- <constraint xsi:type="foreign" name="CATALOG_PRODUCT_ENTITY_GALLERY_STORE_ID_STORE_STORE_ID" -+ <constraint xsi:type="foreign" referenceId="CATALOG_PRODUCT_ENTITY_GALLERY_STORE_ID_STORE_STORE_ID" - table="catalog_product_entity_gallery" column="store_id" referenceTable="store" - referenceColumn="store_id" onDelete="CASCADE"/> -- <constraint xsi:type="unique" name="CATALOG_PRODUCT_ENTITY_GALLERY_ENTITY_ID_ATTRIBUTE_ID_STORE_ID"> -+ <constraint xsi:type="unique" referenceId="CATALOG_PRODUCT_ENTITY_GALLERY_ENTITY_ID_ATTRIBUTE_ID_STORE_ID"> - <column name="entity_id"/> - <column name="attribute_id"/> - <column name="store_id"/> - </constraint> -- <index name="CATALOG_PRODUCT_ENTITY_GALLERY_ENTITY_ID" indexType="btree"> -+ <index referenceId="CATALOG_PRODUCT_ENTITY_GALLERY_ENTITY_ID" indexType="btree"> - <column name="entity_id"/> - </index> -- <index name="CATALOG_PRODUCT_ENTITY_GALLERY_ATTRIBUTE_ID" indexType="btree"> -+ <index referenceId="CATALOG_PRODUCT_ENTITY_GALLERY_ATTRIBUTE_ID" indexType="btree"> - <column name="attribute_id"/> - </index> -- <index name="CATALOG_PRODUCT_ENTITY_GALLERY_STORE_ID" indexType="btree"> -+ <index referenceId="CATALOG_PRODUCT_ENTITY_GALLERY_STORE_ID" indexType="btree"> - <column name="store_id"/> - </index> - </table> - <table name="catalog_category_entity" resource="default" engine="innodb" comment="Catalog Category Table"> - <column xsi:type="int" name="entity_id" padding="10" unsigned="true" nullable="false" identity="true" -- comment="Entity Id"/> -+ comment="Entity ID"/> - <column xsi:type="smallint" name="attribute_set_id" padding="5" unsigned="true" nullable="false" - identity="false" default="0" comment="Attribute Set ID"/> - <column xsi:type="int" name="parent_id" padding="10" unsigned="true" nullable="false" identity="false" -@@ -267,13 +267,13 @@ - comment="Tree Level"/> - <column xsi:type="int" name="children_count" padding="11" unsigned="false" nullable="false" identity="false" - comment="Child Count"/> -- <constraint xsi:type="primary" name="PRIMARY"> -+ <constraint xsi:type="primary" referenceId="PRIMARY"> - <column name="entity_id"/> - </constraint> -- <index name="CATALOG_CATEGORY_ENTITY_LEVEL" indexType="btree"> -+ <index referenceId="CATALOG_CATEGORY_ENTITY_LEVEL" indexType="btree"> - <column name="level"/> - </index> -- <index name="CATALOG_CATEGORY_ENTITY_PATH" indexType="btree"> -+ <index referenceId="CATALOG_CATEGORY_ENTITY_PATH" indexType="btree"> - <column name="path"/> - </index> - </table> -@@ -286,32 +286,32 @@ - <column xsi:type="smallint" name="store_id" padding="5" unsigned="true" nullable="false" identity="false" - default="0" comment="Store ID"/> - <column xsi:type="int" name="entity_id" padding="10" unsigned="true" nullable="false" identity="false" -- default="0"/> -+ default="0" comment="Entity ID"/> - <column xsi:type="datetime" name="value" on_update="false" nullable="true" comment="Value"/> -- <constraint xsi:type="primary" name="PRIMARY"> -+ <constraint xsi:type="primary" referenceId="PRIMARY"> - <column name="value_id"/> - </constraint> -- <constraint xsi:type="foreign" name="CAT_CTGR_ENTT_DTIME_ATTR_ID_EAV_ATTR_ATTR_ID" -+ <constraint xsi:type="foreign" referenceId="CAT_CTGR_ENTT_DTIME_ATTR_ID_EAV_ATTR_ATTR_ID" - table="catalog_category_entity_datetime" column="attribute_id" referenceTable="eav_attribute" - referenceColumn="attribute_id" onDelete="CASCADE"/> -- <constraint xsi:type="foreign" name="CAT_CTGR_ENTT_DTIME_ENTT_ID_CAT_CTGR_ENTT_ENTT_ID" -+ <constraint xsi:type="foreign" referenceId="CAT_CTGR_ENTT_DTIME_ENTT_ID_CAT_CTGR_ENTT_ENTT_ID" - table="catalog_category_entity_datetime" column="entity_id" referenceTable="catalog_category_entity" - referenceColumn="entity_id" onDelete="CASCADE"/> -- <constraint xsi:type="foreign" name="CATALOG_CATEGORY_ENTITY_DATETIME_STORE_ID_STORE_STORE_ID" -+ <constraint xsi:type="foreign" referenceId="CATALOG_CATEGORY_ENTITY_DATETIME_STORE_ID_STORE_STORE_ID" - table="catalog_category_entity_datetime" column="store_id" referenceTable="store" - referenceColumn="store_id" onDelete="CASCADE"/> -- <constraint xsi:type="unique" name="CATALOG_CATEGORY_ENTITY_DATETIME_ENTITY_ID_ATTRIBUTE_ID_STORE_ID"> -+ <constraint xsi:type="unique" referenceId="CATALOG_CATEGORY_ENTITY_DATETIME_ENTITY_ID_ATTRIBUTE_ID_STORE_ID"> - <column name="entity_id"/> - <column name="attribute_id"/> - <column name="store_id"/> - </constraint> -- <index name="CATALOG_CATEGORY_ENTITY_DATETIME_ENTITY_ID" indexType="btree"> -+ <index referenceId="CATALOG_CATEGORY_ENTITY_DATETIME_ENTITY_ID" indexType="btree"> - <column name="entity_id"/> - </index> -- <index name="CATALOG_CATEGORY_ENTITY_DATETIME_ATTRIBUTE_ID" indexType="btree"> -+ <index referenceId="CATALOG_CATEGORY_ENTITY_DATETIME_ATTRIBUTE_ID" indexType="btree"> - <column name="attribute_id"/> - </index> -- <index name="CATALOG_CATEGORY_ENTITY_DATETIME_STORE_ID" indexType="btree"> -+ <index referenceId="CATALOG_CATEGORY_ENTITY_DATETIME_STORE_ID" indexType="btree"> - <column name="store_id"/> - </index> - </table> -@@ -324,33 +324,33 @@ - <column xsi:type="smallint" name="store_id" padding="5" unsigned="true" nullable="false" identity="false" - default="0" comment="Store ID"/> - <column xsi:type="int" name="entity_id" padding="10" unsigned="true" nullable="false" identity="false" -- default="0"/> -- <column xsi:type="decimal" name="value" scale="4" precision="12" unsigned="false" nullable="true" -+ default="0" comment="Entity ID"/> -+ <column xsi:type="decimal" name="value" scale="6" precision="20" unsigned="false" nullable="true" - comment="Value"/> -- <constraint xsi:type="primary" name="PRIMARY"> -+ <constraint xsi:type="primary" referenceId="PRIMARY"> - <column name="value_id"/> - </constraint> -- <constraint xsi:type="foreign" name="CAT_CTGR_ENTT_DEC_ATTR_ID_EAV_ATTR_ATTR_ID" -+ <constraint xsi:type="foreign" referenceId="CAT_CTGR_ENTT_DEC_ATTR_ID_EAV_ATTR_ATTR_ID" - table="catalog_category_entity_decimal" column="attribute_id" referenceTable="eav_attribute" - referenceColumn="attribute_id" onDelete="CASCADE"/> -- <constraint xsi:type="foreign" name="CAT_CTGR_ENTT_DEC_ENTT_ID_CAT_CTGR_ENTT_ENTT_ID" -+ <constraint xsi:type="foreign" referenceId="CAT_CTGR_ENTT_DEC_ENTT_ID_CAT_CTGR_ENTT_ENTT_ID" - table="catalog_category_entity_decimal" column="entity_id" referenceTable="catalog_category_entity" - referenceColumn="entity_id" onDelete="CASCADE"/> -- <constraint xsi:type="foreign" name="CATALOG_CATEGORY_ENTITY_DECIMAL_STORE_ID_STORE_STORE_ID" -+ <constraint xsi:type="foreign" referenceId="CATALOG_CATEGORY_ENTITY_DECIMAL_STORE_ID_STORE_STORE_ID" - table="catalog_category_entity_decimal" column="store_id" referenceTable="store" - referenceColumn="store_id" onDelete="CASCADE"/> -- <constraint xsi:type="unique" name="CATALOG_CATEGORY_ENTITY_DECIMAL_ENTITY_ID_ATTRIBUTE_ID_STORE_ID"> -+ <constraint xsi:type="unique" referenceId="CATALOG_CATEGORY_ENTITY_DECIMAL_ENTITY_ID_ATTRIBUTE_ID_STORE_ID"> - <column name="entity_id"/> - <column name="attribute_id"/> - <column name="store_id"/> - </constraint> -- <index name="CATALOG_CATEGORY_ENTITY_DECIMAL_ENTITY_ID" indexType="btree"> -+ <index referenceId="CATALOG_CATEGORY_ENTITY_DECIMAL_ENTITY_ID" indexType="btree"> - <column name="entity_id"/> - </index> -- <index name="CATALOG_CATEGORY_ENTITY_DECIMAL_ATTRIBUTE_ID" indexType="btree"> -+ <index referenceId="CATALOG_CATEGORY_ENTITY_DECIMAL_ATTRIBUTE_ID" indexType="btree"> - <column name="attribute_id"/> - </index> -- <index name="CATALOG_CATEGORY_ENTITY_DECIMAL_STORE_ID" indexType="btree"> -+ <index referenceId="CATALOG_CATEGORY_ENTITY_DECIMAL_STORE_ID" indexType="btree"> - <column name="store_id"/> - </index> - </table> -@@ -363,33 +363,33 @@ - <column xsi:type="smallint" name="store_id" padding="5" unsigned="true" nullable="false" identity="false" - default="0" comment="Store ID"/> - <column xsi:type="int" name="entity_id" padding="10" unsigned="true" nullable="false" identity="false" -- default="0"/> -+ default="0" comment="Entity ID"/> - <column xsi:type="int" name="value" padding="11" unsigned="false" nullable="true" identity="false" - comment="Value"/> -- <constraint xsi:type="primary" name="PRIMARY"> -+ <constraint xsi:type="primary" referenceId="PRIMARY"> - <column name="value_id"/> - </constraint> -- <constraint xsi:type="foreign" name="CAT_CTGR_ENTT_INT_ATTR_ID_EAV_ATTR_ATTR_ID" -+ <constraint xsi:type="foreign" referenceId="CAT_CTGR_ENTT_INT_ATTR_ID_EAV_ATTR_ATTR_ID" - table="catalog_category_entity_int" column="attribute_id" referenceTable="eav_attribute" - referenceColumn="attribute_id" onDelete="CASCADE"/> -- <constraint xsi:type="foreign" name="CAT_CTGR_ENTT_INT_ENTT_ID_CAT_CTGR_ENTT_ENTT_ID" -+ <constraint xsi:type="foreign" referenceId="CAT_CTGR_ENTT_INT_ENTT_ID_CAT_CTGR_ENTT_ENTT_ID" - table="catalog_category_entity_int" column="entity_id" referenceTable="catalog_category_entity" - referenceColumn="entity_id" onDelete="CASCADE"/> -- <constraint xsi:type="foreign" name="CATALOG_CATEGORY_ENTITY_INT_STORE_ID_STORE_STORE_ID" -+ <constraint xsi:type="foreign" referenceId="CATALOG_CATEGORY_ENTITY_INT_STORE_ID_STORE_STORE_ID" - table="catalog_category_entity_int" column="store_id" referenceTable="store" - referenceColumn="store_id" onDelete="CASCADE"/> -- <constraint xsi:type="unique" name="CATALOG_CATEGORY_ENTITY_INT_ENTITY_ID_ATTRIBUTE_ID_STORE_ID"> -+ <constraint xsi:type="unique" referenceId="CATALOG_CATEGORY_ENTITY_INT_ENTITY_ID_ATTRIBUTE_ID_STORE_ID"> - <column name="entity_id"/> - <column name="attribute_id"/> - <column name="store_id"/> - </constraint> -- <index name="CATALOG_CATEGORY_ENTITY_INT_ENTITY_ID" indexType="btree"> -+ <index referenceId="CATALOG_CATEGORY_ENTITY_INT_ENTITY_ID" indexType="btree"> - <column name="entity_id"/> - </index> -- <index name="CATALOG_CATEGORY_ENTITY_INT_ATTRIBUTE_ID" indexType="btree"> -+ <index referenceId="CATALOG_CATEGORY_ENTITY_INT_ATTRIBUTE_ID" indexType="btree"> - <column name="attribute_id"/> - </index> -- <index name="CATALOG_CATEGORY_ENTITY_INT_STORE_ID" indexType="btree"> -+ <index referenceId="CATALOG_CATEGORY_ENTITY_INT_STORE_ID" indexType="btree"> - <column name="store_id"/> - </index> - </table> -@@ -402,32 +402,32 @@ - <column xsi:type="smallint" name="store_id" padding="5" unsigned="true" nullable="false" identity="false" - default="0" comment="Store ID"/> - <column xsi:type="int" name="entity_id" padding="10" unsigned="true" nullable="false" identity="false" -- default="0"/> -+ default="0" comment="Entity ID"/> - <column xsi:type="text" name="value" nullable="true" comment="Value"/> -- <constraint xsi:type="primary" name="PRIMARY"> -+ <constraint xsi:type="primary" referenceId="PRIMARY"> - <column name="value_id"/> - </constraint> -- <constraint xsi:type="foreign" name="CAT_CTGR_ENTT_TEXT_ATTR_ID_EAV_ATTR_ATTR_ID" -+ <constraint xsi:type="foreign" referenceId="CAT_CTGR_ENTT_TEXT_ATTR_ID_EAV_ATTR_ATTR_ID" - table="catalog_category_entity_text" column="attribute_id" referenceTable="eav_attribute" - referenceColumn="attribute_id" onDelete="CASCADE"/> -- <constraint xsi:type="foreign" name="CAT_CTGR_ENTT_TEXT_ENTT_ID_CAT_CTGR_ENTT_ENTT_ID" -+ <constraint xsi:type="foreign" referenceId="CAT_CTGR_ENTT_TEXT_ENTT_ID_CAT_CTGR_ENTT_ENTT_ID" - table="catalog_category_entity_text" column="entity_id" referenceTable="catalog_category_entity" - referenceColumn="entity_id" onDelete="CASCADE"/> -- <constraint xsi:type="foreign" name="CATALOG_CATEGORY_ENTITY_TEXT_STORE_ID_STORE_STORE_ID" -+ <constraint xsi:type="foreign" referenceId="CATALOG_CATEGORY_ENTITY_TEXT_STORE_ID_STORE_STORE_ID" - table="catalog_category_entity_text" column="store_id" referenceTable="store" - referenceColumn="store_id" onDelete="CASCADE"/> -- <constraint xsi:type="unique" name="CATALOG_CATEGORY_ENTITY_TEXT_ENTITY_ID_ATTRIBUTE_ID_STORE_ID"> -+ <constraint xsi:type="unique" referenceId="CATALOG_CATEGORY_ENTITY_TEXT_ENTITY_ID_ATTRIBUTE_ID_STORE_ID"> - <column name="entity_id"/> - <column name="attribute_id"/> - <column name="store_id"/> - </constraint> -- <index name="CATALOG_CATEGORY_ENTITY_TEXT_ENTITY_ID" indexType="btree"> -+ <index referenceId="CATALOG_CATEGORY_ENTITY_TEXT_ENTITY_ID" indexType="btree"> - <column name="entity_id"/> - </index> -- <index name="CATALOG_CATEGORY_ENTITY_TEXT_ATTRIBUTE_ID" indexType="btree"> -+ <index referenceId="CATALOG_CATEGORY_ENTITY_TEXT_ATTRIBUTE_ID" indexType="btree"> - <column name="attribute_id"/> - </index> -- <index name="CATALOG_CATEGORY_ENTITY_TEXT_STORE_ID" indexType="btree"> -+ <index referenceId="CATALOG_CATEGORY_ENTITY_TEXT_STORE_ID" indexType="btree"> - <column name="store_id"/> - </index> - </table> -@@ -440,32 +440,32 @@ - <column xsi:type="smallint" name="store_id" padding="5" unsigned="true" nullable="false" identity="false" - default="0" comment="Store ID"/> - <column xsi:type="int" name="entity_id" padding="10" unsigned="true" nullable="false" identity="false" -- default="0"/> -+ default="0" comment="Entity ID"/> - <column xsi:type="varchar" name="value" nullable="true" length="255" comment="Value"/> -- <constraint xsi:type="primary" name="PRIMARY"> -+ <constraint xsi:type="primary" referenceId="PRIMARY"> - <column name="value_id"/> - </constraint> -- <constraint xsi:type="foreign" name="CAT_CTGR_ENTT_VCHR_ATTR_ID_EAV_ATTR_ATTR_ID" -+ <constraint xsi:type="foreign" referenceId="CAT_CTGR_ENTT_VCHR_ATTR_ID_EAV_ATTR_ATTR_ID" - table="catalog_category_entity_varchar" column="attribute_id" referenceTable="eav_attribute" - referenceColumn="attribute_id" onDelete="CASCADE"/> -- <constraint xsi:type="foreign" name="CAT_CTGR_ENTT_VCHR_ENTT_ID_CAT_CTGR_ENTT_ENTT_ID" -+ <constraint xsi:type="foreign" referenceId="CAT_CTGR_ENTT_VCHR_ENTT_ID_CAT_CTGR_ENTT_ENTT_ID" - table="catalog_category_entity_varchar" column="entity_id" referenceTable="catalog_category_entity" - referenceColumn="entity_id" onDelete="CASCADE"/> -- <constraint xsi:type="foreign" name="CATALOG_CATEGORY_ENTITY_VARCHAR_STORE_ID_STORE_STORE_ID" -+ <constraint xsi:type="foreign" referenceId="CATALOG_CATEGORY_ENTITY_VARCHAR_STORE_ID_STORE_STORE_ID" - table="catalog_category_entity_varchar" column="store_id" referenceTable="store" - referenceColumn="store_id" onDelete="CASCADE"/> -- <constraint xsi:type="unique" name="CATALOG_CATEGORY_ENTITY_VARCHAR_ENTITY_ID_ATTRIBUTE_ID_STORE_ID"> -+ <constraint xsi:type="unique" referenceId="CATALOG_CATEGORY_ENTITY_VARCHAR_ENTITY_ID_ATTRIBUTE_ID_STORE_ID"> - <column name="entity_id"/> - <column name="attribute_id"/> - <column name="store_id"/> - </constraint> -- <index name="CATALOG_CATEGORY_ENTITY_VARCHAR_ENTITY_ID" indexType="btree"> -+ <index referenceId="CATALOG_CATEGORY_ENTITY_VARCHAR_ENTITY_ID" indexType="btree"> - <column name="entity_id"/> - </index> -- <index name="CATALOG_CATEGORY_ENTITY_VARCHAR_ATTRIBUTE_ID" indexType="btree"> -+ <index referenceId="CATALOG_CATEGORY_ENTITY_VARCHAR_ATTRIBUTE_ID" indexType="btree"> - <column name="attribute_id"/> - </index> -- <index name="CATALOG_CATEGORY_ENTITY_VARCHAR_STORE_ID" indexType="btree"> -+ <index referenceId="CATALOG_CATEGORY_ENTITY_VARCHAR_STORE_ID" indexType="btree"> - <column name="store_id"/> - </index> - </table> -@@ -479,22 +479,22 @@ - default="0" comment="Product ID"/> - <column xsi:type="int" name="position" padding="11" unsigned="false" nullable="false" identity="false" - default="0" comment="Position"/> -- <constraint xsi:type="primary" name="PRIMARY"> -+ <constraint xsi:type="primary" referenceId="PRIMARY"> - <column name="entity_id"/> - <column name="category_id"/> - <column name="product_id"/> - </constraint> -- <constraint xsi:type="foreign" name="CAT_CTGR_PRD_PRD_ID_CAT_PRD_ENTT_ENTT_ID" table="catalog_category_product" -+ <constraint xsi:type="foreign" referenceId="CAT_CTGR_PRD_PRD_ID_CAT_PRD_ENTT_ENTT_ID" table="catalog_category_product" - column="product_id" referenceTable="catalog_product_entity" referenceColumn="entity_id" - onDelete="CASCADE"/> -- <constraint xsi:type="foreign" name="CAT_CTGR_PRD_CTGR_ID_CAT_CTGR_ENTT_ENTT_ID" -+ <constraint xsi:type="foreign" referenceId="CAT_CTGR_PRD_CTGR_ID_CAT_CTGR_ENTT_ENTT_ID" - table="catalog_category_product" column="category_id" referenceTable="catalog_category_entity" - referenceColumn="entity_id" onDelete="CASCADE"/> -- <constraint xsi:type="unique" name="CATALOG_CATEGORY_PRODUCT_CATEGORY_ID_PRODUCT_ID"> -+ <constraint xsi:type="unique" referenceId="CATALOG_CATEGORY_PRODUCT_CATEGORY_ID_PRODUCT_ID"> - <column name="category_id"/> - <column name="product_id"/> - </constraint> -- <index name="CATALOG_CATEGORY_PRODUCT_PRODUCT_ID" indexType="btree"> -+ <index referenceId="CATALOG_CATEGORY_PRODUCT_PRODUCT_ID" indexType="btree"> - <column name="product_id"/> - </index> - </table> -@@ -512,18 +512,18 @@ - default="0" comment="Store ID"/> - <column xsi:type="smallint" name="visibility" padding="5" unsigned="true" nullable="false" identity="false" - comment="Visibility"/> -- <constraint xsi:type="primary" name="PRIMARY"> -+ <constraint xsi:type="primary" referenceId="PRIMARY"> - <column name="category_id"/> - <column name="product_id"/> - <column name="store_id"/> - </constraint> -- <index name="CAT_CTGR_PRD_IDX_PRD_ID_STORE_ID_CTGR_ID_VISIBILITY" indexType="btree"> -+ <index referenceId="CAT_CTGR_PRD_IDX_PRD_ID_STORE_ID_CTGR_ID_VISIBILITY" indexType="btree"> - <column name="product_id"/> - <column name="store_id"/> - <column name="category_id"/> - <column name="visibility"/> - </index> -- <index name="CAT_CTGR_PRD_IDX_STORE_ID_CTGR_ID_VISIBILITY_IS_PARENT_POSITION" indexType="btree"> -+ <index referenceId="CAT_CTGR_PRD_IDX_STORE_ID_CTGR_ID_VISIBILITY_IS_PARENT_POSITION" indexType="btree"> - <column name="store_id"/> - <column name="category_id"/> - <column name="visibility"/> -@@ -542,29 +542,29 @@ - default="0" comment="Product ID"/> - <column xsi:type="smallint" name="store_id" padding="5" unsigned="true" nullable="true" identity="false" - comment="Store ID"/> -- <constraint xsi:type="primary" name="PRIMARY"> -+ <constraint xsi:type="primary" referenceId="PRIMARY"> - <column name="catalog_compare_item_id"/> - </constraint> -- <constraint xsi:type="foreign" name="CATALOG_COMPARE_ITEM_CUSTOMER_ID_CUSTOMER_ENTITY_ENTITY_ID" -+ <constraint xsi:type="foreign" referenceId="CATALOG_COMPARE_ITEM_CUSTOMER_ID_CUSTOMER_ENTITY_ENTITY_ID" - table="catalog_compare_item" column="customer_id" referenceTable="customer_entity" - referenceColumn="entity_id" onDelete="CASCADE"/> -- <constraint xsi:type="foreign" name="CATALOG_COMPARE_ITEM_PRODUCT_ID_CATALOG_PRODUCT_ENTITY_ENTITY_ID" -+ <constraint xsi:type="foreign" referenceId="CATALOG_COMPARE_ITEM_PRODUCT_ID_CATALOG_PRODUCT_ENTITY_ENTITY_ID" - table="catalog_compare_item" column="product_id" referenceTable="catalog_product_entity" - referenceColumn="entity_id" onDelete="CASCADE"/> -- <constraint xsi:type="foreign" name="CATALOG_COMPARE_ITEM_STORE_ID_STORE_STORE_ID" table="catalog_compare_item" -+ <constraint xsi:type="foreign" referenceId="CATALOG_COMPARE_ITEM_STORE_ID_STORE_STORE_ID" table="catalog_compare_item" - column="store_id" referenceTable="store" referenceColumn="store_id" onDelete="SET NULL"/> -- <index name="CATALOG_COMPARE_ITEM_PRODUCT_ID" indexType="btree"> -+ <index referenceId="CATALOG_COMPARE_ITEM_PRODUCT_ID" indexType="btree"> - <column name="product_id"/> - </index> -- <index name="CATALOG_COMPARE_ITEM_VISITOR_ID_PRODUCT_ID" indexType="btree"> -+ <index referenceId="CATALOG_COMPARE_ITEM_VISITOR_ID_PRODUCT_ID" indexType="btree"> - <column name="visitor_id"/> - <column name="product_id"/> - </index> -- <index name="CATALOG_COMPARE_ITEM_CUSTOMER_ID_PRODUCT_ID" indexType="btree"> -+ <index referenceId="CATALOG_COMPARE_ITEM_CUSTOMER_ID_PRODUCT_ID" indexType="btree"> - <column name="customer_id"/> - <column name="product_id"/> - </index> -- <index name="CATALOG_COMPARE_ITEM_STORE_ID" indexType="btree"> -+ <index referenceId="CATALOG_COMPARE_ITEM_STORE_ID" indexType="btree"> - <column name="store_id"/> - </index> - </table> -@@ -574,17 +574,17 @@ - comment="Product ID"/> - <column xsi:type="smallint" name="website_id" padding="5" unsigned="true" nullable="false" identity="false" - comment="Website ID"/> -- <constraint xsi:type="primary" name="PRIMARY"> -+ <constraint xsi:type="primary" referenceId="PRIMARY"> - <column name="product_id"/> - <column name="website_id"/> - </constraint> -- <constraint xsi:type="foreign" name="CATALOG_PRODUCT_WEBSITE_WEBSITE_ID_STORE_WEBSITE_WEBSITE_ID" -+ <constraint xsi:type="foreign" referenceId="CATALOG_PRODUCT_WEBSITE_WEBSITE_ID_STORE_WEBSITE_WEBSITE_ID" - table="catalog_product_website" column="website_id" referenceTable="store_website" - referenceColumn="website_id" onDelete="CASCADE"/> -- <constraint xsi:type="foreign" name="CAT_PRD_WS_PRD_ID_CAT_PRD_ENTT_ENTT_ID" table="catalog_product_website" -+ <constraint xsi:type="foreign" referenceId="CAT_PRD_WS_PRD_ID_CAT_PRD_ENTT_ENTT_ID" table="catalog_product_website" - column="product_id" referenceTable="catalog_product_entity" referenceColumn="entity_id" - onDelete="CASCADE"/> -- <index name="CATALOG_PRODUCT_WEBSITE_WEBSITE_ID" indexType="btree"> -+ <index referenceId="CATALOG_PRODUCT_WEBSITE_WEBSITE_ID" indexType="btree"> - <column name="website_id"/> - </index> - </table> -@@ -593,7 +593,7 @@ - <column xsi:type="smallint" name="link_type_id" padding="5" unsigned="true" nullable="false" identity="true" - comment="Link Type ID"/> - <column xsi:type="varchar" name="code" nullable="true" length="32" comment="Code"/> -- <constraint xsi:type="primary" name="PRIMARY"> -+ <constraint xsi:type="primary" referenceId="PRIMARY"> - <column name="link_type_id"/> - </constraint> - </table> -@@ -607,27 +607,27 @@ - default="0" comment="Linked Product ID"/> - <column xsi:type="smallint" name="link_type_id" padding="5" unsigned="true" nullable="false" identity="false" - default="0" comment="Link Type ID"/> -- <constraint xsi:type="primary" name="PRIMARY"> -+ <constraint xsi:type="primary" referenceId="PRIMARY"> - <column name="link_id"/> - </constraint> -- <constraint xsi:type="foreign" name="CAT_PRD_LNK_LNKED_PRD_ID_CAT_PRD_ENTT_ENTT_ID" table="catalog_product_link" -+ <constraint xsi:type="foreign" referenceId="CAT_PRD_LNK_LNKED_PRD_ID_CAT_PRD_ENTT_ENTT_ID" table="catalog_product_link" - column="linked_product_id" referenceTable="catalog_product_entity" referenceColumn="entity_id" - onDelete="CASCADE"/> -- <constraint xsi:type="foreign" name="CATALOG_PRODUCT_LINK_PRODUCT_ID_CATALOG_PRODUCT_ENTITY_ENTITY_ID" -+ <constraint xsi:type="foreign" referenceId="CATALOG_PRODUCT_LINK_PRODUCT_ID_CATALOG_PRODUCT_ENTITY_ENTITY_ID" - table="catalog_product_link" column="product_id" referenceTable="catalog_product_entity" - referenceColumn="entity_id" onDelete="CASCADE"/> -- <constraint xsi:type="foreign" name="CAT_PRD_LNK_LNK_TYPE_ID_CAT_PRD_LNK_TYPE_LNK_TYPE_ID" -+ <constraint xsi:type="foreign" referenceId="CAT_PRD_LNK_LNK_TYPE_ID_CAT_PRD_LNK_TYPE_LNK_TYPE_ID" - table="catalog_product_link" column="link_type_id" referenceTable="catalog_product_link_type" - referenceColumn="link_type_id" onDelete="CASCADE"/> -- <constraint xsi:type="unique" name="CATALOG_PRODUCT_LINK_LINK_TYPE_ID_PRODUCT_ID_LINKED_PRODUCT_ID"> -+ <constraint xsi:type="unique" referenceId="CATALOG_PRODUCT_LINK_LINK_TYPE_ID_PRODUCT_ID_LINKED_PRODUCT_ID"> - <column name="link_type_id"/> - <column name="product_id"/> - <column name="linked_product_id"/> - </constraint> -- <index name="CATALOG_PRODUCT_LINK_PRODUCT_ID" indexType="btree"> -+ <index referenceId="CATALOG_PRODUCT_LINK_PRODUCT_ID" indexType="btree"> - <column name="product_id"/> - </index> -- <index name="CATALOG_PRODUCT_LINK_LINKED_PRODUCT_ID" indexType="btree"> -+ <index referenceId="CATALOG_PRODUCT_LINK_LINKED_PRODUCT_ID" indexType="btree"> - <column name="linked_product_id"/> - </index> - </table> -@@ -640,13 +640,13 @@ - <column xsi:type="varchar" name="product_link_attribute_code" nullable="true" length="32" - comment="Product Link Attribute Code"/> - <column xsi:type="varchar" name="data_type" nullable="true" length="32" comment="Data Type"/> -- <constraint xsi:type="primary" name="PRIMARY"> -+ <constraint xsi:type="primary" referenceId="PRIMARY"> - <column name="product_link_attribute_id"/> - </constraint> -- <constraint xsi:type="foreign" name="CAT_PRD_LNK_ATTR_LNK_TYPE_ID_CAT_PRD_LNK_TYPE_LNK_TYPE_ID" -+ <constraint xsi:type="foreign" referenceId="CAT_PRD_LNK_ATTR_LNK_TYPE_ID_CAT_PRD_LNK_TYPE_LNK_TYPE_ID" - table="catalog_product_link_attribute" column="link_type_id" - referenceTable="catalog_product_link_type" referenceColumn="link_type_id" onDelete="CASCADE"/> -- <index name="CATALOG_PRODUCT_LINK_ATTRIBUTE_LINK_TYPE_ID" indexType="btree"> -+ <index referenceId="CATALOG_PRODUCT_LINK_ATTRIBUTE_LINK_TYPE_ID" indexType="btree"> - <column name="link_type_id"/> - </index> - </table> -@@ -658,23 +658,23 @@ - identity="false" comment="Product Link Attribute ID"/> - <column xsi:type="int" name="link_id" padding="10" unsigned="true" nullable="false" identity="false" - comment="Link ID"/> -- <column xsi:type="decimal" name="value" scale="4" precision="12" unsigned="false" nullable="false" default="0" -+ <column xsi:type="decimal" name="value" scale="6" precision="20" unsigned="false" nullable="false" default="0" - comment="Value"/> -- <constraint xsi:type="primary" name="PRIMARY"> -+ <constraint xsi:type="primary" referenceId="PRIMARY"> - <column name="value_id"/> - </constraint> -- <constraint xsi:type="foreign" name="CAT_PRD_LNK_ATTR_DEC_LNK_ID_CAT_PRD_LNK_LNK_ID" -+ <constraint xsi:type="foreign" referenceId="CAT_PRD_LNK_ATTR_DEC_LNK_ID_CAT_PRD_LNK_LNK_ID" - table="catalog_product_link_attribute_decimal" column="link_id" - referenceTable="catalog_product_link" referenceColumn="link_id" onDelete="CASCADE"/> -- <constraint xsi:type="foreign" name="FK_AB2EFA9A14F7BCF1D5400056203D14B6" -+ <constraint xsi:type="foreign" referenceId="FK_AB2EFA9A14F7BCF1D5400056203D14B6" - table="catalog_product_link_attribute_decimal" column="product_link_attribute_id" - referenceTable="catalog_product_link_attribute" referenceColumn="product_link_attribute_id" - onDelete="CASCADE"/> -- <constraint xsi:type="unique" name="CAT_PRD_LNK_ATTR_DEC_PRD_LNK_ATTR_ID_LNK_ID"> -+ <constraint xsi:type="unique" referenceId="CAT_PRD_LNK_ATTR_DEC_PRD_LNK_ATTR_ID_LNK_ID"> - <column name="product_link_attribute_id"/> - <column name="link_id"/> - </constraint> -- <index name="CATALOG_PRODUCT_LINK_ATTRIBUTE_DECIMAL_LINK_ID" indexType="btree"> -+ <index referenceId="CATALOG_PRODUCT_LINK_ATTRIBUTE_DECIMAL_LINK_ID" indexType="btree"> - <column name="link_id"/> - </index> - </table> -@@ -688,21 +688,21 @@ - comment="Link ID"/> - <column xsi:type="int" name="value" padding="11" unsigned="false" nullable="false" identity="false" default="0" - comment="Value"/> -- <constraint xsi:type="primary" name="PRIMARY"> -+ <constraint xsi:type="primary" referenceId="PRIMARY"> - <column name="value_id"/> - </constraint> -- <constraint xsi:type="foreign" name="CAT_PRD_LNK_ATTR_INT_LNK_ID_CAT_PRD_LNK_LNK_ID" -+ <constraint xsi:type="foreign" referenceId="CAT_PRD_LNK_ATTR_INT_LNK_ID_CAT_PRD_LNK_LNK_ID" - table="catalog_product_link_attribute_int" column="link_id" referenceTable="catalog_product_link" - referenceColumn="link_id" onDelete="CASCADE"/> -- <constraint xsi:type="foreign" name="FK_D6D878F8BA2A4282F8DDED7E6E3DE35C" -+ <constraint xsi:type="foreign" referenceId="FK_D6D878F8BA2A4282F8DDED7E6E3DE35C" - table="catalog_product_link_attribute_int" column="product_link_attribute_id" - referenceTable="catalog_product_link_attribute" referenceColumn="product_link_attribute_id" - onDelete="CASCADE"/> -- <constraint xsi:type="unique" name="CAT_PRD_LNK_ATTR_INT_PRD_LNK_ATTR_ID_LNK_ID"> -+ <constraint xsi:type="unique" referenceId="CAT_PRD_LNK_ATTR_INT_PRD_LNK_ATTR_ID_LNK_ID"> - <column name="product_link_attribute_id"/> - <column name="link_id"/> - </constraint> -- <index name="CATALOG_PRODUCT_LINK_ATTRIBUTE_INT_LINK_ID" indexType="btree"> -+ <index referenceId="CATALOG_PRODUCT_LINK_ATTRIBUTE_INT_LINK_ID" indexType="btree"> - <column name="link_id"/> - </index> - </table> -@@ -715,21 +715,21 @@ - <column xsi:type="int" name="link_id" padding="10" unsigned="true" nullable="false" identity="false" - comment="Link ID"/> - <column xsi:type="varchar" name="value" nullable="true" length="255" comment="Value"/> -- <constraint xsi:type="primary" name="PRIMARY"> -+ <constraint xsi:type="primary" referenceId="PRIMARY"> - <column name="value_id"/> - </constraint> -- <constraint xsi:type="foreign" name="CAT_PRD_LNK_ATTR_VCHR_LNK_ID_CAT_PRD_LNK_LNK_ID" -+ <constraint xsi:type="foreign" referenceId="CAT_PRD_LNK_ATTR_VCHR_LNK_ID_CAT_PRD_LNK_LNK_ID" - table="catalog_product_link_attribute_varchar" column="link_id" - referenceTable="catalog_product_link" referenceColumn="link_id" onDelete="CASCADE"/> -- <constraint xsi:type="foreign" name="FK_DEE9C4DA61CFCC01DFCF50F0D79CEA51" -+ <constraint xsi:type="foreign" referenceId="FK_DEE9C4DA61CFCC01DFCF50F0D79CEA51" - table="catalog_product_link_attribute_varchar" column="product_link_attribute_id" - referenceTable="catalog_product_link_attribute" referenceColumn="product_link_attribute_id" - onDelete="CASCADE"/> -- <constraint xsi:type="unique" name="CAT_PRD_LNK_ATTR_VCHR_PRD_LNK_ATTR_ID_LNK_ID"> -+ <constraint xsi:type="unique" referenceId="CAT_PRD_LNK_ATTR_VCHR_PRD_LNK_ATTR_ID_LNK_ID"> - <column name="product_link_attribute_id"/> - <column name="link_id"/> - </constraint> -- <index name="CATALOG_PRODUCT_LINK_ATTRIBUTE_VARCHAR_LINK_ID" indexType="btree"> -+ <index referenceId="CATALOG_PRODUCT_LINK_ATTRIBUTE_VARCHAR_LINK_ID" indexType="btree"> - <column name="link_id"/> - </index> - </table> -@@ -738,42 +738,42 @@ - <column xsi:type="int" name="value_id" padding="11" unsigned="false" nullable="false" identity="true" - comment="Value ID"/> - <column xsi:type="int" name="entity_id" padding="10" unsigned="true" nullable="false" identity="false" -- default="0"/> -+ default="0" comment="Entity ID"/> - <column xsi:type="smallint" name="all_groups" padding="5" unsigned="true" nullable="false" identity="false" - default="1" comment="Is Applicable To All Customer Groups"/> - <column xsi:type="int" name="customer_group_id" padding="10" unsigned="true" nullable="false" identity="false" - default="0" comment="Customer Group ID"/> - <column xsi:type="decimal" name="qty" scale="4" precision="12" unsigned="false" nullable="false" default="1" - comment="QTY"/> -- <column xsi:type="decimal" name="value" scale="4" precision="12" unsigned="false" nullable="false" default="0" -+ <column xsi:type="decimal" name="value" scale="6" precision="20" unsigned="false" nullable="false" default="0" - comment="Value"/> - <column xsi:type="smallint" name="website_id" padding="5" unsigned="true" nullable="false" identity="false" - comment="Website ID"/> - <column xsi:type="decimal" name="percentage_value" scale="2" precision="5" unsigned="false" nullable="true" - comment="Percentage value"/> -- <constraint xsi:type="primary" name="PRIMARY"> -+ <constraint xsi:type="primary" referenceId="PRIMARY"> - <column name="value_id"/> - </constraint> -- <constraint xsi:type="foreign" name="CAT_PRD_ENTT_TIER_PRICE_CSTR_GROUP_ID_CSTR_GROUP_CSTR_GROUP_ID" -+ <constraint xsi:type="foreign" referenceId="CAT_PRD_ENTT_TIER_PRICE_CSTR_GROUP_ID_CSTR_GROUP_CSTR_GROUP_ID" - table="catalog_product_entity_tier_price" column="customer_group_id" referenceTable="customer_group" - referenceColumn="customer_group_id" onDelete="CASCADE"/> -- <constraint xsi:type="foreign" name="CAT_PRD_ENTT_TIER_PRICE_ENTT_ID_CAT_PRD_ENTT_ENTT_ID" -+ <constraint xsi:type="foreign" referenceId="CAT_PRD_ENTT_TIER_PRICE_ENTT_ID_CAT_PRD_ENTT_ENTT_ID" - table="catalog_product_entity_tier_price" column="entity_id" referenceTable="catalog_product_entity" - referenceColumn="entity_id" onDelete="CASCADE"/> -- <constraint xsi:type="foreign" name="CAT_PRD_ENTT_TIER_PRICE_WS_ID_STORE_WS_WS_ID" -+ <constraint xsi:type="foreign" referenceId="CAT_PRD_ENTT_TIER_PRICE_WS_ID_STORE_WS_WS_ID" - table="catalog_product_entity_tier_price" column="website_id" referenceTable="store_website" - referenceColumn="website_id" onDelete="CASCADE"/> -- <constraint xsi:type="unique" name="UNQ_E8AB433B9ACB00343ABB312AD2FAB087"> -+ <constraint xsi:type="unique" referenceId="UNQ_E8AB433B9ACB00343ABB312AD2FAB087"> - <column name="entity_id"/> - <column name="all_groups"/> - <column name="customer_group_id"/> - <column name="qty"/> - <column name="website_id"/> - </constraint> -- <index name="CATALOG_PRODUCT_ENTITY_TIER_PRICE_CUSTOMER_GROUP_ID" indexType="btree"> -+ <index referenceId="CATALOG_PRODUCT_ENTITY_TIER_PRICE_CUSTOMER_GROUP_ID" indexType="btree"> - <column name="customer_group_id"/> - </index> -- <index name="CATALOG_PRODUCT_ENTITY_TIER_PRICE_WEBSITE_ID" indexType="btree"> -+ <index referenceId="CATALOG_PRODUCT_ENTITY_TIER_PRICE_WEBSITE_ID" indexType="btree"> - <column name="website_id"/> - </index> - </table> -@@ -788,13 +788,13 @@ - comment="Media entry type"/> - <column xsi:type="smallint" name="disabled" padding="5" unsigned="true" nullable="false" identity="false" - default="0" comment="Visibility status"/> -- <constraint xsi:type="primary" name="PRIMARY"> -+ <constraint xsi:type="primary" referenceId="PRIMARY"> - <column name="value_id"/> - </constraint> -- <constraint xsi:type="foreign" name="CAT_PRD_ENTT_MDA_GLR_ATTR_ID_EAV_ATTR_ATTR_ID" -+ <constraint xsi:type="foreign" referenceId="CAT_PRD_ENTT_MDA_GLR_ATTR_ID_EAV_ATTR_ATTR_ID" - table="catalog_product_entity_media_gallery" column="attribute_id" referenceTable="eav_attribute" - referenceColumn="attribute_id" onDelete="CASCADE"/> -- <index name="CATALOG_PRODUCT_ENTITY_MEDIA_GALLERY_ATTRIBUTE_ID" indexType="btree"> -+ <index referenceId="CATALOG_PRODUCT_ENTITY_MEDIA_GALLERY_ATTRIBUTE_ID" indexType="btree"> - <column name="attribute_id"/> - </index> - </table> -@@ -805,7 +805,7 @@ - <column xsi:type="smallint" name="store_id" padding="5" unsigned="true" nullable="false" identity="false" - default="0" comment="Store ID"/> - <column xsi:type="int" name="entity_id" padding="10" unsigned="true" nullable="false" identity="false" -- default="0"/> -+ default="0" comment="Entity ID"/> - <column xsi:type="varchar" name="label" nullable="true" length="255" comment="Label"/> - <column xsi:type="int" name="position" padding="10" unsigned="true" nullable="true" identity="false" - comment="Position"/> -@@ -813,28 +813,33 @@ - default="0" comment="Is Disabled"/> - <column xsi:type="int" name="record_id" padding="10" unsigned="true" nullable="false" identity="true" - comment="Record Id"/> -- <constraint xsi:type="primary" name="PRIMARY"> -+ <constraint xsi:type="primary" referenceId="PRIMARY"> - <column name="record_id"/> - </constraint> -- <constraint xsi:type="foreign" name="CAT_PRD_ENTT_MDA_GLR_VAL_VAL_ID_CAT_PRD_ENTT_MDA_GLR_VAL_ID" -+ <constraint xsi:type="foreign" referenceId="CAT_PRD_ENTT_MDA_GLR_VAL_VAL_ID_CAT_PRD_ENTT_MDA_GLR_VAL_ID" - table="catalog_product_entity_media_gallery_value" column="value_id" - referenceTable="catalog_product_entity_media_gallery" referenceColumn="value_id" - onDelete="CASCADE"/> -- <constraint xsi:type="foreign" name="CAT_PRD_ENTT_MDA_GLR_VAL_STORE_ID_STORE_STORE_ID" -+ <constraint xsi:type="foreign" referenceId="CAT_PRD_ENTT_MDA_GLR_VAL_STORE_ID_STORE_STORE_ID" - table="catalog_product_entity_media_gallery_value" column="store_id" referenceTable="store" - referenceColumn="store_id" onDelete="CASCADE"/> -- <constraint xsi:type="foreign" name="CAT_PRD_ENTT_MDA_GLR_VAL_ENTT_ID_CAT_PRD_ENTT_ENTT_ID" -+ <constraint xsi:type="foreign" referenceId="CAT_PRD_ENTT_MDA_GLR_VAL_ENTT_ID_CAT_PRD_ENTT_ENTT_ID" - table="catalog_product_entity_media_gallery_value" column="entity_id" - referenceTable="catalog_product_entity" referenceColumn="entity_id" onDelete="CASCADE"/> -- <index name="CATALOG_PRODUCT_ENTITY_MEDIA_GALLERY_VALUE_STORE_ID" indexType="btree"> -+ <index referenceId="CATALOG_PRODUCT_ENTITY_MEDIA_GALLERY_VALUE_STORE_ID" indexType="btree"> - <column name="store_id"/> - </index> -- <index name="CATALOG_PRODUCT_ENTITY_MEDIA_GALLERY_VALUE_ENTITY_ID" indexType="btree"> -+ <index referenceId="CATALOG_PRODUCT_ENTITY_MEDIA_GALLERY_VALUE_ENTITY_ID" indexType="btree"> - <column name="entity_id"/> - </index> -- <index name="CATALOG_PRODUCT_ENTITY_MEDIA_GALLERY_VALUE_VALUE_ID" indexType="btree"> -+ <index referenceId="CATALOG_PRODUCT_ENTITY_MEDIA_GALLERY_VALUE_VALUE_ID" indexType="btree"> - <column name="value_id"/> - </index> -+ <index referenceId="CAT_PRD_ENTT_MDA_GLR_VAL_ENTT_ID_VAL_ID_STORE_ID" indexType="btree"> -+ <column name="entity_id"/> -+ <column name="value_id"/> -+ <column name="store_id"/> -+ </index> - </table> - <table name="catalog_product_option" resource="default" engine="innodb" comment="Catalog Product Option Table"> - <column xsi:type="int" name="option_id" padding="10" unsigned="true" nullable="false" identity="true" -@@ -854,13 +859,13 @@ - comment="Image Size Y"/> - <column xsi:type="int" name="sort_order" padding="10" unsigned="true" nullable="false" identity="false" - default="0" comment="Sort Order"/> -- <constraint xsi:type="primary" name="PRIMARY"> -+ <constraint xsi:type="primary" referenceId="PRIMARY"> - <column name="option_id"/> - </constraint> -- <constraint xsi:type="foreign" name="CAT_PRD_OPT_PRD_ID_CAT_PRD_ENTT_ENTT_ID" table="catalog_product_option" -+ <constraint xsi:type="foreign" referenceId="CAT_PRD_OPT_PRD_ID_CAT_PRD_ENTT_ENTT_ID" table="catalog_product_option" - column="product_id" referenceTable="catalog_product_entity" referenceColumn="entity_id" - onDelete="CASCADE"/> -- <index name="CATALOG_PRODUCT_OPTION_PRODUCT_ID" indexType="btree"> -+ <index referenceId="CATALOG_PRODUCT_OPTION_PRODUCT_ID" indexType="btree"> - <column name="product_id"/> - </index> - </table> -@@ -872,23 +877,23 @@ - default="0" comment="Option ID"/> - <column xsi:type="smallint" name="store_id" padding="5" unsigned="true" nullable="false" identity="false" - default="0" comment="Store ID"/> -- <column xsi:type="decimal" name="price" scale="4" precision="12" unsigned="false" nullable="false" default="0" -+ <column xsi:type="decimal" name="price" scale="6" precision="20" unsigned="false" nullable="false" default="0" - comment="Price"/> - <column xsi:type="varchar" name="price_type" nullable="false" length="7" default="fixed" comment="Price Type"/> -- <constraint xsi:type="primary" name="PRIMARY"> -+ <constraint xsi:type="primary" referenceId="PRIMARY"> - <column name="option_price_id"/> - </constraint> -- <constraint xsi:type="foreign" name="CAT_PRD_OPT_PRICE_OPT_ID_CAT_PRD_OPT_OPT_ID" -+ <constraint xsi:type="foreign" referenceId="CAT_PRD_OPT_PRICE_OPT_ID_CAT_PRD_OPT_OPT_ID" - table="catalog_product_option_price" column="option_id" referenceTable="catalog_product_option" - referenceColumn="option_id" onDelete="CASCADE"/> -- <constraint xsi:type="foreign" name="CATALOG_PRODUCT_OPTION_PRICE_STORE_ID_STORE_STORE_ID" -+ <constraint xsi:type="foreign" referenceId="CATALOG_PRODUCT_OPTION_PRICE_STORE_ID_STORE_STORE_ID" - table="catalog_product_option_price" column="store_id" referenceTable="store" - referenceColumn="store_id" onDelete="CASCADE"/> -- <constraint xsi:type="unique" name="CATALOG_PRODUCT_OPTION_PRICE_OPTION_ID_STORE_ID"> -+ <constraint xsi:type="unique" referenceId="CATALOG_PRODUCT_OPTION_PRICE_OPTION_ID_STORE_ID"> - <column name="option_id"/> - <column name="store_id"/> - </constraint> -- <index name="CATALOG_PRODUCT_OPTION_PRICE_STORE_ID" indexType="btree"> -+ <index referenceId="CATALOG_PRODUCT_OPTION_PRICE_STORE_ID" indexType="btree"> - <column name="store_id"/> - </index> - </table> -@@ -901,20 +906,20 @@ - <column xsi:type="smallint" name="store_id" padding="5" unsigned="true" nullable="false" identity="false" - default="0" comment="Store ID"/> - <column xsi:type="varchar" name="title" nullable="true" length="255" comment="Title"/> -- <constraint xsi:type="primary" name="PRIMARY"> -+ <constraint xsi:type="primary" referenceId="PRIMARY"> - <column name="option_title_id"/> - </constraint> -- <constraint xsi:type="foreign" name="CAT_PRD_OPT_TTL_OPT_ID_CAT_PRD_OPT_OPT_ID" -+ <constraint xsi:type="foreign" referenceId="CAT_PRD_OPT_TTL_OPT_ID_CAT_PRD_OPT_OPT_ID" - table="catalog_product_option_title" column="option_id" referenceTable="catalog_product_option" - referenceColumn="option_id" onDelete="CASCADE"/> -- <constraint xsi:type="foreign" name="CATALOG_PRODUCT_OPTION_TITLE_STORE_ID_STORE_STORE_ID" -+ <constraint xsi:type="foreign" referenceId="CATALOG_PRODUCT_OPTION_TITLE_STORE_ID_STORE_STORE_ID" - table="catalog_product_option_title" column="store_id" referenceTable="store" - referenceColumn="store_id" onDelete="CASCADE"/> -- <constraint xsi:type="unique" name="CATALOG_PRODUCT_OPTION_TITLE_OPTION_ID_STORE_ID"> -+ <constraint xsi:type="unique" referenceId="CATALOG_PRODUCT_OPTION_TITLE_OPTION_ID_STORE_ID"> - <column name="option_id"/> - <column name="store_id"/> - </constraint> -- <index name="CATALOG_PRODUCT_OPTION_TITLE_STORE_ID" indexType="btree"> -+ <index referenceId="CATALOG_PRODUCT_OPTION_TITLE_STORE_ID" indexType="btree"> - <column name="store_id"/> - </index> - </table> -@@ -927,13 +932,13 @@ - <column xsi:type="varchar" name="sku" nullable="true" length="64" comment="SKU"/> - <column xsi:type="int" name="sort_order" padding="10" unsigned="true" nullable="false" identity="false" - default="0" comment="Sort Order"/> -- <constraint xsi:type="primary" name="PRIMARY"> -+ <constraint xsi:type="primary" referenceId="PRIMARY"> - <column name="option_type_id"/> - </constraint> -- <constraint xsi:type="foreign" name="CAT_PRD_OPT_TYPE_VAL_OPT_ID_CAT_PRD_OPT_OPT_ID" -+ <constraint xsi:type="foreign" referenceId="CAT_PRD_OPT_TYPE_VAL_OPT_ID_CAT_PRD_OPT_OPT_ID" - table="catalog_product_option_type_value" column="option_id" referenceTable="catalog_product_option" - referenceColumn="option_id" onDelete="CASCADE"/> -- <index name="CATALOG_PRODUCT_OPTION_TYPE_VALUE_OPTION_ID" indexType="btree"> -+ <index referenceId="CATALOG_PRODUCT_OPTION_TYPE_VALUE_OPTION_ID" indexType="btree"> - <column name="option_id"/> - </index> - </table> -@@ -945,24 +950,24 @@ - default="0" comment="Option Type ID"/> - <column xsi:type="smallint" name="store_id" padding="5" unsigned="true" nullable="false" identity="false" - default="0" comment="Store ID"/> -- <column xsi:type="decimal" name="price" scale="4" precision="12" unsigned="false" nullable="false" default="0" -+ <column xsi:type="decimal" name="price" scale="6" precision="20" unsigned="false" nullable="false" default="0" - comment="Price"/> - <column xsi:type="varchar" name="price_type" nullable="false" length="7" default="fixed" comment="Price Type"/> -- <constraint xsi:type="primary" name="PRIMARY"> -+ <constraint xsi:type="primary" referenceId="PRIMARY"> - <column name="option_type_price_id"/> - </constraint> -- <constraint xsi:type="foreign" name="FK_B523E3378E8602F376CC415825576B7F" -+ <constraint xsi:type="foreign" referenceId="FK_B523E3378E8602F376CC415825576B7F" - table="catalog_product_option_type_price" column="option_type_id" - referenceTable="catalog_product_option_type_value" referenceColumn="option_type_id" - onDelete="CASCADE"/> -- <constraint xsi:type="foreign" name="CATALOG_PRODUCT_OPTION_TYPE_PRICE_STORE_ID_STORE_STORE_ID" -+ <constraint xsi:type="foreign" referenceId="CATALOG_PRODUCT_OPTION_TYPE_PRICE_STORE_ID_STORE_STORE_ID" - table="catalog_product_option_type_price" column="store_id" referenceTable="store" - referenceColumn="store_id" onDelete="CASCADE"/> -- <constraint xsi:type="unique" name="CATALOG_PRODUCT_OPTION_TYPE_PRICE_OPTION_TYPE_ID_STORE_ID"> -+ <constraint xsi:type="unique" referenceId="CATALOG_PRODUCT_OPTION_TYPE_PRICE_OPTION_TYPE_ID_STORE_ID"> - <column name="option_type_id"/> - <column name="store_id"/> - </constraint> -- <index name="CATALOG_PRODUCT_OPTION_TYPE_PRICE_STORE_ID" indexType="btree"> -+ <index referenceId="CATALOG_PRODUCT_OPTION_TYPE_PRICE_STORE_ID" indexType="btree"> - <column name="store_id"/> - </index> - </table> -@@ -975,21 +980,21 @@ - <column xsi:type="smallint" name="store_id" padding="5" unsigned="true" nullable="false" identity="false" - default="0" comment="Store ID"/> - <column xsi:type="varchar" name="title" nullable="true" length="255" comment="Title"/> -- <constraint xsi:type="primary" name="PRIMARY"> -+ <constraint xsi:type="primary" referenceId="PRIMARY"> - <column name="option_type_title_id"/> - </constraint> -- <constraint xsi:type="foreign" name="FK_C085B9CF2C2A302E8043FDEA1937D6A2" -+ <constraint xsi:type="foreign" referenceId="FK_C085B9CF2C2A302E8043FDEA1937D6A2" - table="catalog_product_option_type_title" column="option_type_id" - referenceTable="catalog_product_option_type_value" referenceColumn="option_type_id" - onDelete="CASCADE"/> -- <constraint xsi:type="foreign" name="CATALOG_PRODUCT_OPTION_TYPE_TITLE_STORE_ID_STORE_STORE_ID" -+ <constraint xsi:type="foreign" referenceId="CATALOG_PRODUCT_OPTION_TYPE_TITLE_STORE_ID_STORE_STORE_ID" - table="catalog_product_option_type_title" column="store_id" referenceTable="store" - referenceColumn="store_id" onDelete="CASCADE"/> -- <constraint xsi:type="unique" name="CATALOG_PRODUCT_OPTION_TYPE_TITLE_OPTION_TYPE_ID_STORE_ID"> -+ <constraint xsi:type="unique" referenceId="CATALOG_PRODUCT_OPTION_TYPE_TITLE_OPTION_TYPE_ID_STORE_ID"> - <column name="option_type_id"/> - <column name="store_id"/> - </constraint> -- <index name="CATALOG_PRODUCT_OPTION_TYPE_TITLE_STORE_ID" indexType="btree"> -+ <index referenceId="CATALOG_PRODUCT_OPTION_TYPE_TITLE_STORE_ID" indexType="btree"> - <column name="store_id"/> - </index> - </table> -@@ -1037,16 +1042,16 @@ - identity="false" default="0" comment="Is Visible in Grid"/> - <column xsi:type="smallint" name="is_filterable_in_grid" padding="5" unsigned="true" nullable="false" - identity="false" default="0" comment="Is Filterable in Grid"/> -- <constraint xsi:type="primary" name="PRIMARY"> -+ <constraint xsi:type="primary" referenceId="PRIMARY"> - <column name="attribute_id"/> - </constraint> -- <constraint xsi:type="foreign" name="CATALOG_EAV_ATTRIBUTE_ATTRIBUTE_ID_EAV_ATTRIBUTE_ATTRIBUTE_ID" -+ <constraint xsi:type="foreign" referenceId="CATALOG_EAV_ATTRIBUTE_ATTRIBUTE_ID_EAV_ATTRIBUTE_ATTRIBUTE_ID" - table="catalog_eav_attribute" column="attribute_id" referenceTable="eav_attribute" - referenceColumn="attribute_id" onDelete="CASCADE"/> -- <index name="CATALOG_EAV_ATTRIBUTE_USED_FOR_SORT_BY" indexType="btree"> -+ <index referenceId="CATALOG_EAV_ATTRIBUTE_USED_FOR_SORT_BY" indexType="btree"> - <column name="used_for_sort_by"/> - </index> -- <index name="CATALOG_EAV_ATTRIBUTE_USED_IN_PRODUCT_LISTING" indexType="btree"> -+ <index referenceId="CATALOG_EAV_ATTRIBUTE_USED_IN_PRODUCT_LISTING" indexType="btree"> - <column name="used_in_product_listing"/> - </index> - </table> -@@ -1055,17 +1060,17 @@ - comment="Parent ID"/> - <column xsi:type="int" name="child_id" padding="10" unsigned="true" nullable="false" identity="false" - comment="Child ID"/> -- <constraint xsi:type="primary" name="PRIMARY"> -+ <constraint xsi:type="primary" referenceId="PRIMARY"> - <column name="parent_id"/> - <column name="child_id"/> - </constraint> -- <constraint xsi:type="foreign" name="CAT_PRD_RELATION_CHILD_ID_CAT_PRD_ENTT_ENTT_ID" -+ <constraint xsi:type="foreign" referenceId="CAT_PRD_RELATION_CHILD_ID_CAT_PRD_ENTT_ENTT_ID" - table="catalog_product_relation" column="child_id" referenceTable="catalog_product_entity" - referenceColumn="entity_id" onDelete="CASCADE"/> -- <constraint xsi:type="foreign" name="CAT_PRD_RELATION_PARENT_ID_CAT_PRD_ENTT_ENTT_ID" -+ <constraint xsi:type="foreign" referenceId="CAT_PRD_RELATION_PARENT_ID_CAT_PRD_ENTT_ENTT_ID" - table="catalog_product_relation" column="parent_id" referenceTable="catalog_product_entity" - referenceColumn="entity_id" onDelete="CASCADE"/> -- <index name="CATALOG_PRODUCT_RELATION_CHILD_ID" indexType="btree"> -+ <index referenceId="CATALOG_PRODUCT_RELATION_CHILD_ID" indexType="btree"> - <column name="child_id"/> - </index> - </table> -@@ -1081,20 +1086,20 @@ - comment="Value"/> - <column xsi:type="int" name="source_id" padding="10" unsigned="true" nullable="false" identity="false" - default="0" comment="Original entity Id for attribute value"/> -- <constraint xsi:type="primary" name="CAT_PRD_IDX_EAV_ENTT_ID_ATTR_ID_STORE_ID_VAL_SOURCE_ID"> -+ <constraint xsi:type="primary" referenceId="CAT_PRD_IDX_EAV_ENTT_ID_ATTR_ID_STORE_ID_VAL_SOURCE_ID"> - <column name="entity_id"/> - <column name="attribute_id"/> - <column name="store_id"/> - <column name="value"/> - <column name="source_id"/> - </constraint> -- <index name="CATALOG_PRODUCT_INDEX_EAV_ATTRIBUTE_ID" indexType="btree"> -+ <index referenceId="CATALOG_PRODUCT_INDEX_EAV_ATTRIBUTE_ID" indexType="btree"> - <column name="attribute_id"/> - </index> -- <index name="CATALOG_PRODUCT_INDEX_EAV_STORE_ID" indexType="btree"> -+ <index referenceId="CATALOG_PRODUCT_INDEX_EAV_STORE_ID" indexType="btree"> - <column name="store_id"/> - </index> -- <index name="CATALOG_PRODUCT_INDEX_EAV_VALUE" indexType="btree"> -+ <index referenceId="CATALOG_PRODUCT_INDEX_EAV_VALUE" indexType="btree"> - <column name="value"/> - </index> - </table> -@@ -1110,20 +1115,20 @@ - comment="Value"/> - <column xsi:type="int" name="source_id" padding="10" unsigned="true" nullable="false" identity="false" - default="0" comment="Original entity Id for attribute value"/> -- <constraint xsi:type="primary" name="CAT_PRD_IDX_EAV_DEC_ENTT_ID_ATTR_ID_STORE_ID_VAL_SOURCE_ID"> -+ <constraint xsi:type="primary" referenceId="CAT_PRD_IDX_EAV_DEC_ENTT_ID_ATTR_ID_STORE_ID_VAL_SOURCE_ID"> - <column name="entity_id"/> - <column name="attribute_id"/> - <column name="store_id"/> - <column name="value"/> - <column name="source_id"/> - </constraint> -- <index name="CATALOG_PRODUCT_INDEX_EAV_DECIMAL_ATTRIBUTE_ID" indexType="btree"> -+ <index referenceId="CATALOG_PRODUCT_INDEX_EAV_DECIMAL_ATTRIBUTE_ID" indexType="btree"> - <column name="attribute_id"/> - </index> -- <index name="CATALOG_PRODUCT_INDEX_EAV_DECIMAL_STORE_ID" indexType="btree"> -+ <index referenceId="CATALOG_PRODUCT_INDEX_EAV_DECIMAL_STORE_ID" indexType="btree"> - <column name="store_id"/> - </index> -- <index name="CATALOG_PRODUCT_INDEX_EAV_DECIMAL_VALUE" indexType="btree"> -+ <index referenceId="CATALOG_PRODUCT_INDEX_EAV_DECIMAL_VALUE" indexType="btree"> - <column name="value"/> - </index> - </table> -@@ -1137,28 +1142,28 @@ - comment="Website ID"/> - <column xsi:type="smallint" name="tax_class_id" padding="5" unsigned="true" nullable="true" identity="false" - default="0" comment="Tax Class ID"/> -- <column xsi:type="decimal" name="price" scale="4" precision="12" unsigned="false" nullable="true" -+ <column xsi:type="decimal" name="price" scale="6" precision="20" unsigned="false" nullable="true" - comment="Price"/> -- <column xsi:type="decimal" name="final_price" scale="4" precision="12" unsigned="false" nullable="true" -+ <column xsi:type="decimal" name="final_price" scale="6" precision="20" unsigned="false" nullable="true" - comment="Final Price"/> -- <column xsi:type="decimal" name="min_price" scale="4" precision="12" unsigned="false" nullable="true" -+ <column xsi:type="decimal" name="min_price" scale="6" precision="20" unsigned="false" nullable="true" - comment="Min Price"/> -- <column xsi:type="decimal" name="max_price" scale="4" precision="12" unsigned="false" nullable="true" -+ <column xsi:type="decimal" name="max_price" scale="6" precision="20" unsigned="false" nullable="true" - comment="Max Price"/> -- <column xsi:type="decimal" name="tier_price" scale="4" precision="12" unsigned="false" nullable="true" -+ <column xsi:type="decimal" name="tier_price" scale="6" precision="20" unsigned="false" nullable="true" - comment="Tier Price"/> -- <constraint xsi:type="primary" name="PRIMARY"> -+ <constraint xsi:type="primary" referenceId="PRIMARY"> - <column name="entity_id"/> - <column name="customer_group_id"/> - <column name="website_id"/> - </constraint> -- <index name="CATALOG_PRODUCT_INDEX_PRICE_CUSTOMER_GROUP_ID" indexType="btree"> -+ <index referenceId="CATALOG_PRODUCT_INDEX_PRICE_CUSTOMER_GROUP_ID" indexType="btree"> - <column name="customer_group_id"/> - </index> -- <index name="CATALOG_PRODUCT_INDEX_PRICE_MIN_PRICE" indexType="btree"> -+ <index referenceId="CATALOG_PRODUCT_INDEX_PRICE_MIN_PRICE" indexType="btree"> - <column name="min_price"/> - </index> -- <index name="CAT_PRD_IDX_PRICE_WS_ID_CSTR_GROUP_ID_MIN_PRICE" indexType="btree"> -+ <index referenceId="CAT_PRD_IDX_PRICE_WS_ID_CSTR_GROUP_ID_MIN_PRICE" indexType="btree"> - <column name="website_id"/> - <column name="customer_group_id"/> - <column name="min_price"/> -@@ -1172,26 +1177,26 @@ - comment="Customer Group ID"/> - <column xsi:type="smallint" name="website_id" padding="5" unsigned="true" nullable="false" identity="false" - comment="Website ID"/> -- <column xsi:type="decimal" name="min_price" scale="4" precision="12" unsigned="false" nullable="true" -+ <column xsi:type="decimal" name="min_price" scale="6" precision="20" unsigned="false" nullable="true" - comment="Min Price"/> -- <constraint xsi:type="primary" name="PRIMARY"> -+ <constraint xsi:type="primary" referenceId="PRIMARY"> - <column name="entity_id"/> - <column name="customer_group_id"/> - <column name="website_id"/> - </constraint> -- <constraint xsi:type="foreign" name="CAT_PRD_IDX_TIER_PRICE_CSTR_GROUP_ID_CSTR_GROUP_CSTR_GROUP_ID" -+ <constraint xsi:type="foreign" referenceId="CAT_PRD_IDX_TIER_PRICE_CSTR_GROUP_ID_CSTR_GROUP_CSTR_GROUP_ID" - table="catalog_product_index_tier_price" column="customer_group_id" referenceTable="customer_group" - referenceColumn="customer_group_id" onDelete="CASCADE"/> -- <constraint xsi:type="foreign" name="CAT_PRD_IDX_TIER_PRICE_ENTT_ID_CAT_PRD_ENTT_ENTT_ID" -+ <constraint xsi:type="foreign" referenceId="CAT_PRD_IDX_TIER_PRICE_ENTT_ID_CAT_PRD_ENTT_ENTT_ID" - table="catalog_product_index_tier_price" column="entity_id" referenceTable="catalog_product_entity" - referenceColumn="entity_id" onDelete="CASCADE"/> -- <constraint xsi:type="foreign" name="CAT_PRD_IDX_TIER_PRICE_WS_ID_STORE_WS_WS_ID" -+ <constraint xsi:type="foreign" referenceId="CAT_PRD_IDX_TIER_PRICE_WS_ID_STORE_WS_WS_ID" - table="catalog_product_index_tier_price" column="website_id" referenceTable="store_website" - referenceColumn="website_id" onDelete="CASCADE"/> -- <index name="CATALOG_PRODUCT_INDEX_TIER_PRICE_CUSTOMER_GROUP_ID" indexType="btree"> -+ <index referenceId="CATALOG_PRODUCT_INDEX_TIER_PRICE_CUSTOMER_GROUP_ID" indexType="btree"> - <column name="customer_group_id"/> - </index> -- <index name="CATALOG_PRODUCT_INDEX_TIER_PRICE_WEBSITE_ID" indexType="btree"> -+ <index referenceId="CATALOG_PRODUCT_INDEX_TIER_PRICE_WEBSITE_ID" indexType="btree"> - <column name="website_id"/> - </index> - </table> -@@ -1203,12 +1208,12 @@ - comment="Default store id for website"/> - <column xsi:type="date" name="website_date" comment="Website Date"/> - <column xsi:type="float" name="rate" unsigned="false" nullable="true" default="1" comment="Rate"/> -- <constraint xsi:type="primary" name="PRIMARY"> -+ <constraint xsi:type="primary" referenceId="PRIMARY"> - <column name="website_id"/> - </constraint> -- <constraint xsi:type="foreign" name="CAT_PRD_IDX_WS_WS_ID_STORE_WS_WS_ID" table="catalog_product_index_website" -+ <constraint xsi:type="foreign" referenceId="CAT_PRD_IDX_WS_WS_ID_STORE_WS_WS_ID" table="catalog_product_index_website" - column="website_id" referenceTable="store_website" referenceColumn="website_id" onDelete="CASCADE"/> -- <index name="CATALOG_PRODUCT_INDEX_WEBSITE_WEBSITE_DATE" indexType="btree"> -+ <index referenceId="CATALOG_PRODUCT_INDEX_WEBSITE_WEBSITE_DATE" indexType="btree"> - <column name="website_date"/> - </index> - </table> -@@ -1222,11 +1227,11 @@ - default="0" comment="Customer Group ID"/> - <column xsi:type="smallint" name="website_id" padding="5" unsigned="true" nullable="false" identity="false" - comment="Website ID"/> -- <column xsi:type="decimal" name="price" scale="4" precision="12" unsigned="false" nullable="true" -+ <column xsi:type="decimal" name="price" scale="6" precision="20" unsigned="false" nullable="true" - comment="Price"/> -- <column xsi:type="decimal" name="tier_price" scale="4" precision="12" unsigned="false" nullable="true" -+ <column xsi:type="decimal" name="tier_price" scale="6" precision="20" unsigned="false" nullable="true" - comment="Tier Price"/> -- <constraint xsi:type="primary" name="PRIMARY"> -+ <constraint xsi:type="primary" referenceId="PRIMARY"> - <column name="parent_id"/> - <column name="child_id"/> - <column name="customer_group_id"/> -@@ -1243,11 +1248,11 @@ - default="0" comment="Customer Group ID"/> - <column xsi:type="smallint" name="website_id" padding="5" unsigned="true" nullable="false" identity="false" - comment="Website ID"/> -- <column xsi:type="decimal" name="price" scale="4" precision="12" unsigned="false" nullable="true" -+ <column xsi:type="decimal" name="price" scale="6" precision="20" unsigned="false" nullable="true" - comment="Price"/> -- <column xsi:type="decimal" name="tier_price" scale="4" precision="12" unsigned="false" nullable="true" -+ <column xsi:type="decimal" name="tier_price" scale="6" precision="20" unsigned="false" nullable="true" - comment="Tier Price"/> -- <constraint xsi:type="primary" name="PRIMARY"> -+ <constraint xsi:type="primary" referenceId="PRIMARY"> - <column name="parent_id"/> - <column name="child_id"/> - <column name="customer_group_id"/> -@@ -1262,13 +1267,13 @@ - default="0" comment="Customer Group ID"/> - <column xsi:type="smallint" name="website_id" padding="5" unsigned="true" nullable="false" identity="false" - comment="Website ID"/> -- <column xsi:type="decimal" name="min_price" scale="4" precision="12" unsigned="false" nullable="true" -+ <column xsi:type="decimal" name="min_price" scale="6" precision="20" unsigned="false" nullable="true" - comment="Min Price"/> -- <column xsi:type="decimal" name="max_price" scale="4" precision="12" unsigned="false" nullable="true" -+ <column xsi:type="decimal" name="max_price" scale="6" precision="20" unsigned="false" nullable="true" - comment="Max Price"/> -- <column xsi:type="decimal" name="tier_price" scale="4" precision="12" unsigned="false" nullable="true" -+ <column xsi:type="decimal" name="tier_price" scale="6" precision="20" unsigned="false" nullable="true" - comment="Tier Price"/> -- <constraint xsi:type="primary" name="PRIMARY"> -+ <constraint xsi:type="primary" referenceId="PRIMARY"> - <column name="entity_id"/> - <column name="customer_group_id"/> - <column name="website_id"/> -@@ -1282,13 +1287,13 @@ - default="0" comment="Customer Group ID"/> - <column xsi:type="smallint" name="website_id" padding="5" unsigned="true" nullable="false" identity="false" - comment="Website ID"/> -- <column xsi:type="decimal" name="min_price" scale="4" precision="12" unsigned="false" nullable="true" -+ <column xsi:type="decimal" name="min_price" scale="6" precision="20" unsigned="false" nullable="true" - comment="Min Price"/> -- <column xsi:type="decimal" name="max_price" scale="4" precision="12" unsigned="false" nullable="true" -+ <column xsi:type="decimal" name="max_price" scale="6" precision="20" unsigned="false" nullable="true" - comment="Max Price"/> -- <column xsi:type="decimal" name="tier_price" scale="4" precision="12" unsigned="false" nullable="true" -+ <column xsi:type="decimal" name="tier_price" scale="6" precision="20" unsigned="false" nullable="true" - comment="Tier Price"/> -- <constraint xsi:type="primary" name="PRIMARY"> -+ <constraint xsi:type="primary" referenceId="PRIMARY"> - <column name="entity_id"/> - <column name="customer_group_id"/> - <column name="website_id"/> -@@ -1304,19 +1309,19 @@ - comment="Website ID"/> - <column xsi:type="smallint" name="tax_class_id" padding="5" unsigned="true" nullable="true" identity="false" - default="0" comment="Tax Class ID"/> -- <column xsi:type="decimal" name="orig_price" scale="4" precision="12" unsigned="false" nullable="true" -+ <column xsi:type="decimal" name="orig_price" scale="6" precision="20" unsigned="false" nullable="true" - comment="Original Price"/> -- <column xsi:type="decimal" name="price" scale="4" precision="12" unsigned="false" nullable="true" -+ <column xsi:type="decimal" name="price" scale="6" precision="20" unsigned="false" nullable="true" - comment="Price"/> -- <column xsi:type="decimal" name="min_price" scale="4" precision="12" unsigned="false" nullable="true" -+ <column xsi:type="decimal" name="min_price" scale="6" precision="20" unsigned="false" nullable="true" - comment="Min Price"/> -- <column xsi:type="decimal" name="max_price" scale="4" precision="12" unsigned="false" nullable="true" -+ <column xsi:type="decimal" name="max_price" scale="6" precision="20" unsigned="false" nullable="true" - comment="Max Price"/> -- <column xsi:type="decimal" name="tier_price" scale="4" precision="12" unsigned="false" nullable="true" -+ <column xsi:type="decimal" name="tier_price" scale="6" precision="20" unsigned="false" nullable="true" - comment="Tier Price"/> -- <column xsi:type="decimal" name="base_tier" scale="4" precision="12" unsigned="false" nullable="true" -+ <column xsi:type="decimal" name="base_tier" scale="6" precision="20" unsigned="false" nullable="true" - comment="Base Tier"/> -- <constraint xsi:type="primary" name="PRIMARY"> -+ <constraint xsi:type="primary" referenceId="PRIMARY"> - <column name="entity_id"/> - <column name="customer_group_id"/> - <column name="website_id"/> -@@ -1332,19 +1337,19 @@ - comment="Website ID"/> - <column xsi:type="smallint" name="tax_class_id" padding="5" unsigned="true" nullable="true" identity="false" - default="0" comment="Tax Class ID"/> -- <column xsi:type="decimal" name="orig_price" scale="4" precision="12" unsigned="false" nullable="true" -+ <column xsi:type="decimal" name="orig_price" scale="6" precision="20" unsigned="false" nullable="true" - comment="Original Price"/> -- <column xsi:type="decimal" name="price" scale="4" precision="12" unsigned="false" nullable="true" -+ <column xsi:type="decimal" name="price" scale="6" precision="20" unsigned="false" nullable="true" - comment="Price"/> -- <column xsi:type="decimal" name="min_price" scale="4" precision="12" unsigned="false" nullable="true" -+ <column xsi:type="decimal" name="min_price" scale="6" precision="20" unsigned="false" nullable="true" - comment="Min Price"/> -- <column xsi:type="decimal" name="max_price" scale="4" precision="12" unsigned="false" nullable="true" -+ <column xsi:type="decimal" name="max_price" scale="6" precision="20" unsigned="false" nullable="true" - comment="Max Price"/> -- <column xsi:type="decimal" name="tier_price" scale="4" precision="12" unsigned="false" nullable="true" -+ <column xsi:type="decimal" name="tier_price" scale="6" precision="20" unsigned="false" nullable="true" - comment="Tier Price"/> -- <column xsi:type="decimal" name="base_tier" scale="4" precision="12" unsigned="false" nullable="true" -+ <column xsi:type="decimal" name="base_tier" scale="6" precision="20" unsigned="false" nullable="true" - comment="Base Tier"/> -- <constraint xsi:type="primary" name="PRIMARY"> -+ <constraint xsi:type="primary" referenceId="PRIMARY"> - <column name="entity_id"/> - <column name="customer_group_id"/> - <column name="website_id"/> -@@ -1358,13 +1363,13 @@ - default="0" comment="Customer Group ID"/> - <column xsi:type="smallint" name="website_id" padding="5" unsigned="true" nullable="false" identity="false" - comment="Website ID"/> -- <column xsi:type="decimal" name="min_price" scale="4" precision="12" unsigned="false" nullable="true" -+ <column xsi:type="decimal" name="min_price" scale="6" precision="20" unsigned="false" nullable="true" - comment="Min Price"/> -- <column xsi:type="decimal" name="max_price" scale="4" precision="12" unsigned="false" nullable="true" -+ <column xsi:type="decimal" name="max_price" scale="6" precision="20" unsigned="false" nullable="true" - comment="Max Price"/> -- <column xsi:type="decimal" name="tier_price" scale="4" precision="12" unsigned="false" nullable="true" -+ <column xsi:type="decimal" name="tier_price" scale="6" precision="20" unsigned="false" nullable="true" - comment="Tier Price"/> -- <constraint xsi:type="primary" name="PRIMARY"> -+ <constraint xsi:type="primary" referenceId="PRIMARY"> - <column name="entity_id"/> - <column name="customer_group_id"/> - <column name="website_id"/> -@@ -1378,13 +1383,13 @@ - default="0" comment="Customer Group ID"/> - <column xsi:type="smallint" name="website_id" padding="5" unsigned="true" nullable="false" identity="false" - comment="Website ID"/> -- <column xsi:type="decimal" name="min_price" scale="4" precision="12" unsigned="false" nullable="true" -+ <column xsi:type="decimal" name="min_price" scale="6" precision="20" unsigned="false" nullable="true" - comment="Min Price"/> -- <column xsi:type="decimal" name="max_price" scale="4" precision="12" unsigned="false" nullable="true" -+ <column xsi:type="decimal" name="max_price" scale="6" precision="20" unsigned="false" nullable="true" - comment="Max Price"/> -- <column xsi:type="decimal" name="tier_price" scale="4" precision="12" unsigned="false" nullable="true" -+ <column xsi:type="decimal" name="tier_price" scale="6" precision="20" unsigned="false" nullable="true" - comment="Tier Price"/> -- <constraint xsi:type="primary" name="PRIMARY"> -+ <constraint xsi:type="primary" referenceId="PRIMARY"> - <column name="entity_id"/> - <column name="customer_group_id"/> - <column name="website_id"/> -@@ -1400,13 +1405,13 @@ - comment="Website ID"/> - <column xsi:type="int" name="option_id" padding="10" unsigned="true" nullable="false" identity="false" - default="0" comment="Option ID"/> -- <column xsi:type="decimal" name="min_price" scale="4" precision="12" unsigned="false" nullable="true" -+ <column xsi:type="decimal" name="min_price" scale="6" precision="20" unsigned="false" nullable="true" - comment="Min Price"/> -- <column xsi:type="decimal" name="max_price" scale="4" precision="12" unsigned="false" nullable="true" -+ <column xsi:type="decimal" name="max_price" scale="6" precision="20" unsigned="false" nullable="true" - comment="Max Price"/> -- <column xsi:type="decimal" name="tier_price" scale="4" precision="12" unsigned="false" nullable="true" -+ <column xsi:type="decimal" name="tier_price" scale="6" precision="20" unsigned="false" nullable="true" - comment="Tier Price"/> -- <constraint xsi:type="primary" name="PRIMARY"> -+ <constraint xsi:type="primary" referenceId="PRIMARY"> - <column name="entity_id"/> - <column name="customer_group_id"/> - <column name="website_id"/> -@@ -1423,13 +1428,13 @@ - comment="Website ID"/> - <column xsi:type="int" name="option_id" padding="10" unsigned="true" nullable="false" identity="false" - default="0" comment="Option ID"/> -- <column xsi:type="decimal" name="min_price" scale="4" precision="12" unsigned="false" nullable="true" -+ <column xsi:type="decimal" name="min_price" scale="6" precision="20" unsigned="false" nullable="true" - comment="Min Price"/> -- <column xsi:type="decimal" name="max_price" scale="4" precision="12" unsigned="false" nullable="true" -+ <column xsi:type="decimal" name="max_price" scale="6" precision="20" unsigned="false" nullable="true" - comment="Max Price"/> -- <column xsi:type="decimal" name="tier_price" scale="4" precision="12" unsigned="false" nullable="true" -+ <column xsi:type="decimal" name="tier_price" scale="6" precision="20" unsigned="false" nullable="true" - comment="Tier Price"/> -- <constraint xsi:type="primary" name="PRIMARY"> -+ <constraint xsi:type="primary" referenceId="PRIMARY"> - <column name="entity_id"/> - <column name="customer_group_id"/> - <column name="website_id"/> -@@ -1448,20 +1453,20 @@ - comment="Value"/> - <column xsi:type="int" name="source_id" padding="10" unsigned="true" nullable="false" identity="false" - default="0" comment="Original entity Id for attribute value"/> -- <constraint xsi:type="primary" name="CAT_PRD_IDX_EAV_IDX_ENTT_ID_ATTR_ID_STORE_ID_VAL_SOURCE_ID"> -+ <constraint xsi:type="primary" referenceId="CAT_PRD_IDX_EAV_IDX_ENTT_ID_ATTR_ID_STORE_ID_VAL_SOURCE_ID"> - <column name="entity_id"/> - <column name="attribute_id"/> - <column name="store_id"/> - <column name="value"/> - <column name="source_id"/> - </constraint> -- <index name="CATALOG_PRODUCT_INDEX_EAV_IDX_ATTRIBUTE_ID" indexType="btree"> -+ <index referenceId="CATALOG_PRODUCT_INDEX_EAV_IDX_ATTRIBUTE_ID" indexType="btree"> - <column name="attribute_id"/> - </index> -- <index name="CATALOG_PRODUCT_INDEX_EAV_IDX_STORE_ID" indexType="btree"> -+ <index referenceId="CATALOG_PRODUCT_INDEX_EAV_IDX_STORE_ID" indexType="btree"> - <column name="store_id"/> - </index> -- <index name="CATALOG_PRODUCT_INDEX_EAV_IDX_VALUE" indexType="btree"> -+ <index referenceId="CATALOG_PRODUCT_INDEX_EAV_IDX_VALUE" indexType="btree"> - <column name="value"/> - </index> - </table> -@@ -1477,20 +1482,20 @@ - comment="Value"/> - <column xsi:type="int" name="source_id" padding="10" unsigned="true" nullable="false" identity="false" - default="0" comment="Original entity Id for attribute value"/> -- <constraint xsi:type="primary" name="CAT_PRD_IDX_EAV_TMP_ENTT_ID_ATTR_ID_STORE_ID_VAL_SOURCE_ID"> -+ <constraint xsi:type="primary" referenceId="CAT_PRD_IDX_EAV_TMP_ENTT_ID_ATTR_ID_STORE_ID_VAL_SOURCE_ID"> - <column name="entity_id"/> - <column name="attribute_id"/> - <column name="store_id"/> - <column name="value"/> - <column name="source_id"/> - </constraint> -- <index name="CATALOG_PRODUCT_INDEX_EAV_TMP_ATTRIBUTE_ID" indexType="hash"> -+ <index referenceId="CATALOG_PRODUCT_INDEX_EAV_TMP_ATTRIBUTE_ID" indexType="hash"> - <column name="attribute_id"/> - </index> -- <index name="CATALOG_PRODUCT_INDEX_EAV_TMP_STORE_ID" indexType="hash"> -+ <index referenceId="CATALOG_PRODUCT_INDEX_EAV_TMP_STORE_ID" indexType="hash"> - <column name="store_id"/> - </index> -- <index name="CATALOG_PRODUCT_INDEX_EAV_TMP_VALUE" indexType="hash"> -+ <index referenceId="CATALOG_PRODUCT_INDEX_EAV_TMP_VALUE" indexType="hash"> - <column name="value"/> - </index> - </table> -@@ -1506,20 +1511,20 @@ - comment="Value"/> - <column xsi:type="int" name="source_id" padding="10" unsigned="true" nullable="false" identity="false" - default="0" comment="Original entity Id for attribute value"/> -- <constraint xsi:type="primary" name="CAT_PRD_IDX_EAV_DEC_IDX_ENTT_ID_ATTR_ID_STORE_ID_VAL_SOURCE_ID"> -+ <constraint xsi:type="primary" referenceId="CAT_PRD_IDX_EAV_DEC_IDX_ENTT_ID_ATTR_ID_STORE_ID_VAL_SOURCE_ID"> - <column name="entity_id"/> - <column name="attribute_id"/> - <column name="store_id"/> - <column name="value"/> - <column name="source_id"/> - </constraint> -- <index name="CATALOG_PRODUCT_INDEX_EAV_DECIMAL_IDX_ATTRIBUTE_ID" indexType="btree"> -+ <index referenceId="CATALOG_PRODUCT_INDEX_EAV_DECIMAL_IDX_ATTRIBUTE_ID" indexType="btree"> - <column name="attribute_id"/> - </index> -- <index name="CATALOG_PRODUCT_INDEX_EAV_DECIMAL_IDX_STORE_ID" indexType="btree"> -+ <index referenceId="CATALOG_PRODUCT_INDEX_EAV_DECIMAL_IDX_STORE_ID" indexType="btree"> - <column name="store_id"/> - </index> -- <index name="CATALOG_PRODUCT_INDEX_EAV_DECIMAL_IDX_VALUE" indexType="btree"> -+ <index referenceId="CATALOG_PRODUCT_INDEX_EAV_DECIMAL_IDX_VALUE" indexType="btree"> - <column name="value"/> - </index> - </table> -@@ -1535,20 +1540,20 @@ - comment="Value"/> - <column xsi:type="int" name="source_id" padding="10" unsigned="true" nullable="false" identity="false" - default="0" comment="Original entity Id for attribute value"/> -- <constraint xsi:type="primary" name="CAT_PRD_IDX_EAV_DEC_TMP_ENTT_ID_ATTR_ID_STORE_ID_VAL_SOURCE_ID"> -+ <constraint xsi:type="primary" referenceId="CAT_PRD_IDX_EAV_DEC_TMP_ENTT_ID_ATTR_ID_STORE_ID_VAL_SOURCE_ID"> - <column name="entity_id"/> - <column name="attribute_id"/> - <column name="store_id"/> - <column name="value"/> - <column name="source_id"/> - </constraint> -- <index name="CATALOG_PRODUCT_INDEX_EAV_DECIMAL_TMP_ATTRIBUTE_ID" indexType="hash"> -+ <index referenceId="CATALOG_PRODUCT_INDEX_EAV_DECIMAL_TMP_ATTRIBUTE_ID" indexType="hash"> - <column name="attribute_id"/> - </index> -- <index name="CATALOG_PRODUCT_INDEX_EAV_DECIMAL_TMP_STORE_ID" indexType="hash"> -+ <index referenceId="CATALOG_PRODUCT_INDEX_EAV_DECIMAL_TMP_STORE_ID" indexType="hash"> - <column name="store_id"/> - </index> -- <index name="CATALOG_PRODUCT_INDEX_EAV_DECIMAL_TMP_VALUE" indexType="hash"> -+ <index referenceId="CATALOG_PRODUCT_INDEX_EAV_DECIMAL_TMP_VALUE" indexType="hash"> - <column name="value"/> - </index> - </table> -@@ -1562,28 +1567,28 @@ - comment="Website ID"/> - <column xsi:type="smallint" name="tax_class_id" padding="5" unsigned="true" nullable="true" identity="false" - default="0" comment="Tax Class ID"/> -- <column xsi:type="decimal" name="price" scale="4" precision="12" unsigned="false" nullable="true" -+ <column xsi:type="decimal" name="price" scale="6" precision="20" unsigned="false" nullable="true" - comment="Price"/> -- <column xsi:type="decimal" name="final_price" scale="4" precision="12" unsigned="false" nullable="true" -+ <column xsi:type="decimal" name="final_price" scale="6" precision="20" unsigned="false" nullable="true" - comment="Final Price"/> -- <column xsi:type="decimal" name="min_price" scale="4" precision="12" unsigned="false" nullable="true" -+ <column xsi:type="decimal" name="min_price" scale="6" precision="20" unsigned="false" nullable="true" - comment="Min Price"/> -- <column xsi:type="decimal" name="max_price" scale="4" precision="12" unsigned="false" nullable="true" -+ <column xsi:type="decimal" name="max_price" scale="6" precision="20" unsigned="false" nullable="true" - comment="Max Price"/> -- <column xsi:type="decimal" name="tier_price" scale="4" precision="12" unsigned="false" nullable="true" -+ <column xsi:type="decimal" name="tier_price" scale="6" precision="20" unsigned="false" nullable="true" - comment="Tier Price"/> -- <constraint xsi:type="primary" name="PRIMARY"> -+ <constraint xsi:type="primary" referenceId="PRIMARY"> - <column name="entity_id"/> - <column name="customer_group_id"/> - <column name="website_id"/> - </constraint> -- <index name="CATALOG_PRODUCT_INDEX_PRICE_IDX_CUSTOMER_GROUP_ID" indexType="btree"> -+ <index referenceId="CATALOG_PRODUCT_INDEX_PRICE_IDX_CUSTOMER_GROUP_ID" indexType="btree"> - <column name="customer_group_id"/> - </index> -- <index name="CATALOG_PRODUCT_INDEX_PRICE_IDX_WEBSITE_ID" indexType="btree"> -+ <index referenceId="CATALOG_PRODUCT_INDEX_PRICE_IDX_WEBSITE_ID" indexType="btree"> - <column name="website_id"/> - </index> -- <index name="CATALOG_PRODUCT_INDEX_PRICE_IDX_MIN_PRICE" indexType="btree"> -+ <index referenceId="CATALOG_PRODUCT_INDEX_PRICE_IDX_MIN_PRICE" indexType="btree"> - <column name="min_price"/> - </index> - </table> -@@ -1597,28 +1602,28 @@ - comment="Website ID"/> - <column xsi:type="smallint" name="tax_class_id" padding="5" unsigned="true" nullable="true" identity="false" - default="0" comment="Tax Class ID"/> -- <column xsi:type="decimal" name="price" scale="4" precision="12" unsigned="false" nullable="true" -+ <column xsi:type="decimal" name="price" scale="6" precision="20" unsigned="false" nullable="true" - comment="Price"/> -- <column xsi:type="decimal" name="final_price" scale="4" precision="12" unsigned="false" nullable="true" -+ <column xsi:type="decimal" name="final_price" scale="6" precision="20" unsigned="false" nullable="true" - comment="Final Price"/> -- <column xsi:type="decimal" name="min_price" scale="4" precision="12" unsigned="false" nullable="true" -+ <column xsi:type="decimal" name="min_price" scale="6" precision="20" unsigned="false" nullable="true" - comment="Min Price"/> -- <column xsi:type="decimal" name="max_price" scale="4" precision="12" unsigned="false" nullable="true" -+ <column xsi:type="decimal" name="max_price" scale="6" precision="20" unsigned="false" nullable="true" - comment="Max Price"/> -- <column xsi:type="decimal" name="tier_price" scale="4" precision="12" unsigned="false" nullable="true" -+ <column xsi:type="decimal" name="tier_price" scale="6" precision="20" unsigned="false" nullable="true" - comment="Tier Price"/> -- <constraint xsi:type="primary" name="PRIMARY"> -+ <constraint xsi:type="primary" referenceId="PRIMARY"> - <column name="entity_id"/> - <column name="customer_group_id"/> - <column name="website_id"/> - </constraint> -- <index name="CATALOG_PRODUCT_INDEX_PRICE_TMP_CUSTOMER_GROUP_ID" indexType="hash"> -+ <index referenceId="CATALOG_PRODUCT_INDEX_PRICE_TMP_CUSTOMER_GROUP_ID" indexType="hash"> - <column name="customer_group_id"/> - </index> -- <index name="CATALOG_PRODUCT_INDEX_PRICE_TMP_WEBSITE_ID" indexType="hash"> -+ <index referenceId="CATALOG_PRODUCT_INDEX_PRICE_TMP_WEBSITE_ID" indexType="hash"> - <column name="website_id"/> - </index> -- <index name="CATALOG_PRODUCT_INDEX_PRICE_TMP_MIN_PRICE" indexType="hash"> -+ <index referenceId="CATALOG_PRODUCT_INDEX_PRICE_TMP_MIN_PRICE" indexType="hash"> - <column name="min_price"/> - </index> - </table> -@@ -1636,12 +1641,12 @@ - default="0" comment="Store ID"/> - <column xsi:type="smallint" name="visibility" padding="5" unsigned="true" nullable="false" identity="false" - comment="Visibility"/> -- <constraint xsi:type="primary" name="PRIMARY"> -+ <constraint xsi:type="primary" referenceId="PRIMARY"> - <column name="category_id"/> - <column name="product_id"/> - <column name="store_id"/> - </constraint> -- <index name="CAT_CTGR_PRD_IDX_TMP_PRD_ID_CTGR_ID_STORE_ID" indexType="hash"> -+ <index referenceId="CAT_CTGR_PRD_IDX_TMP_PRD_ID_CTGR_ID_STORE_ID" indexType="hash"> - <column name="product_id"/> - <column name="category_id"/> - <column name="store_id"/> -@@ -1651,15 +1656,16 @@ - comment="Link Media value to Product entity table"> - <column xsi:type="int" name="value_id" padding="10" unsigned="true" nullable="false" identity="false" - comment="Value media Entry ID"/> -- <column xsi:type="int" name="entity_id" padding="10" unsigned="true" nullable="false" identity="false"/> -- <constraint xsi:type="foreign" name="FK_A6C6C8FAA386736921D3A7C4B50B1185" -+ <column xsi:type="int" name="entity_id" padding="10" unsigned="true" nullable="false" identity="false" -+ comment="Product Entity ID"/> -+ <constraint xsi:type="foreign" referenceId="FK_A6C6C8FAA386736921D3A7C4B50B1185" - table="catalog_product_entity_media_gallery_value_to_entity" column="value_id" - referenceTable="catalog_product_entity_media_gallery" referenceColumn="value_id" - onDelete="CASCADE"/> -- <constraint xsi:type="foreign" name="CAT_PRD_ENTT_MDA_GLR_VAL_TO_ENTT_ENTT_ID_CAT_PRD_ENTT_ENTT_ID" -+ <constraint xsi:type="foreign" referenceId="CAT_PRD_ENTT_MDA_GLR_VAL_TO_ENTT_ENTT_ID_CAT_PRD_ENTT_ENTT_ID" - table="catalog_product_entity_media_gallery_value_to_entity" column="entity_id" - referenceTable="catalog_product_entity" referenceColumn="entity_id" onDelete="CASCADE"/> -- <constraint xsi:type="unique" name="CAT_PRD_ENTT_MDA_GLR_VAL_TO_ENTT_VAL_ID_ENTT_ID"> -+ <constraint xsi:type="unique" referenceId="CAT_PRD_ENTT_MDA_GLR_VAL_TO_ENTT_VAL_ID_ENTT_ID"> - <column name="value_id"/> - <column name="entity_id"/> - </constraint> -@@ -1676,20 +1682,20 @@ - comment="Value"/> - <column xsi:type="int" name="source_id" padding="10" unsigned="true" nullable="false" identity="false" - default="0" comment="Original entity Id for attribute value"/> -- <constraint xsi:type="primary" name="PRIMARY"> -+ <constraint xsi:type="primary" referenceId="PRIMARY"> - <column name="entity_id"/> - <column name="attribute_id"/> - <column name="store_id"/> - <column name="value"/> - <column name="source_id"/> - </constraint> -- <index name="CATALOG_PRODUCT_INDEX_EAV_ATTRIBUTE_ID" indexType="btree"> -+ <index referenceId="CATALOG_PRODUCT_INDEX_EAV_ATTRIBUTE_ID" indexType="btree"> - <column name="attribute_id"/> - </index> -- <index name="CATALOG_PRODUCT_INDEX_EAV_STORE_ID" indexType="btree"> -+ <index referenceId="CATALOG_PRODUCT_INDEX_EAV_STORE_ID" indexType="btree"> - <column name="store_id"/> - </index> -- <index name="CATALOG_PRODUCT_INDEX_EAV_VALUE" indexType="btree"> -+ <index referenceId="CATALOG_PRODUCT_INDEX_EAV_VALUE" indexType="btree"> - <column name="value"/> - </index> - </table> -@@ -1705,20 +1711,20 @@ - comment="Value"/> - <column xsi:type="int" name="source_id" padding="10" unsigned="true" nullable="false" identity="false" - default="0" comment="Original entity Id for attribute value"/> -- <constraint xsi:type="primary" name="PRIMARY"> -+ <constraint xsi:type="primary" referenceId="PRIMARY"> - <column name="entity_id"/> - <column name="attribute_id"/> - <column name="store_id"/> - <column name="value"/> - <column name="source_id"/> - </constraint> -- <index name="CATALOG_PRODUCT_INDEX_EAV_DECIMAL_ATTRIBUTE_ID" indexType="btree"> -+ <index referenceId="CATALOG_PRODUCT_INDEX_EAV_DECIMAL_ATTRIBUTE_ID" indexType="btree"> - <column name="attribute_id"/> - </index> -- <index name="CATALOG_PRODUCT_INDEX_EAV_DECIMAL_STORE_ID" indexType="btree"> -+ <index referenceId="CATALOG_PRODUCT_INDEX_EAV_DECIMAL_STORE_ID" indexType="btree"> - <column name="store_id"/> - </index> -- <index name="CATALOG_PRODUCT_INDEX_EAV_DECIMAL_VALUE" indexType="btree"> -+ <index referenceId="CATALOG_PRODUCT_INDEX_EAV_DECIMAL_VALUE" indexType="btree"> - <column name="value"/> - </index> - </table> -@@ -1732,28 +1738,28 @@ - comment="Website ID"/> - <column xsi:type="smallint" name="tax_class_id" padding="5" unsigned="true" nullable="true" identity="false" - default="0" comment="Tax Class ID"/> -- <column xsi:type="decimal" name="price" scale="4" precision="12" unsigned="false" nullable="true" -+ <column xsi:type="decimal" name="price" scale="6" precision="20" unsigned="false" nullable="true" - comment="Price"/> -- <column xsi:type="decimal" name="final_price" scale="4" precision="12" unsigned="false" nullable="true" -+ <column xsi:type="decimal" name="final_price" scale="6" precision="20" unsigned="false" nullable="true" - comment="Final Price"/> -- <column xsi:type="decimal" name="min_price" scale="4" precision="12" unsigned="false" nullable="true" -+ <column xsi:type="decimal" name="min_price" scale="6" precision="20" unsigned="false" nullable="true" - comment="Min Price"/> -- <column xsi:type="decimal" name="max_price" scale="4" precision="12" unsigned="false" nullable="true" -+ <column xsi:type="decimal" name="max_price" scale="6" precision="20" unsigned="false" nullable="true" - comment="Max Price"/> -- <column xsi:type="decimal" name="tier_price" scale="4" precision="12" unsigned="false" nullable="true" -+ <column xsi:type="decimal" name="tier_price" scale="6" precision="20" unsigned="false" nullable="true" - comment="Tier Price"/> -- <constraint xsi:type="primary" name="PRIMARY"> -+ <constraint xsi:type="primary" referenceId="PRIMARY"> - <column name="entity_id"/> - <column name="customer_group_id"/> - <column name="website_id"/> - </constraint> -- <index name="CATALOG_PRODUCT_INDEX_PRICE_CUSTOMER_GROUP_ID" indexType="btree"> -+ <index referenceId="CATALOG_PRODUCT_INDEX_PRICE_CUSTOMER_GROUP_ID" indexType="btree"> - <column name="customer_group_id"/> - </index> -- <index name="CATALOG_PRODUCT_INDEX_PRICE_MIN_PRICE" indexType="btree"> -+ <index referenceId="CATALOG_PRODUCT_INDEX_PRICE_MIN_PRICE" indexType="btree"> - <column name="min_price"/> - </index> -- <index name="CAT_PRD_IDX_PRICE_WS_ID_CSTR_GROUP_ID_MIN_PRICE" indexType="btree"> -+ <index referenceId="CAT_PRD_IDX_PRICE_WS_ID_CSTR_GROUP_ID_MIN_PRICE" indexType="btree"> - <column name="website_id"/> - <column name="customer_group_id"/> - <column name="min_price"/> -@@ -1773,18 +1779,18 @@ - default="0" comment="Store ID"/> - <column xsi:type="smallint" name="visibility" padding="5" unsigned="true" nullable="false" identity="false" - comment="Visibility"/> -- <constraint xsi:type="primary" name="PRIMARY"> -+ <constraint xsi:type="primary" referenceId="PRIMARY"> - <column name="category_id"/> - <column name="product_id"/> - <column name="store_id"/> - </constraint> -- <index name="CAT_CTGR_PRD_IDX_PRD_ID_STORE_ID_CTGR_ID_VISIBILITY" indexType="btree"> -+ <index referenceId="CAT_CTGR_PRD_IDX_PRD_ID_STORE_ID_CTGR_ID_VISIBILITY" indexType="btree"> - <column name="product_id"/> - <column name="store_id"/> - <column name="category_id"/> - <column name="visibility"/> - </index> -- <index name="CAT_CTGR_PRD_IDX_STORE_ID_CTGR_ID_VISIBILITY_IS_PARENT_POSITION" indexType="btree"> -+ <index referenceId="CAT_CTGR_PRD_IDX_STORE_ID_CTGR_ID_VISIBILITY_IS_PARENT_POSITION" indexType="btree"> - <column name="store_id"/> - <column name="category_id"/> - <column name="visibility"/> -@@ -1805,21 +1811,21 @@ - comment="Product Id"/> - <column xsi:type="bigint" name="added_at" padding="20" unsigned="false" nullable="false" identity="false" - comment="Added At"/> -- <constraint xsi:type="primary" name="PRIMARY"> -+ <constraint xsi:type="primary" referenceId="PRIMARY"> - <column name="action_id"/> - </constraint> -- <constraint xsi:type="foreign" name="CAT_PRD_FRONTEND_ACTION_CSTR_ID_CSTR_ENTT_ENTT_ID" -+ <constraint xsi:type="foreign" referenceId="CAT_PRD_FRONTEND_ACTION_CSTR_ID_CSTR_ENTT_ENTT_ID" - table="catalog_product_frontend_action" column="customer_id" referenceTable="customer_entity" - referenceColumn="entity_id" onDelete="CASCADE"/> -- <constraint xsi:type="foreign" name="CAT_PRD_FRONTEND_ACTION_PRD_ID_CAT_PRD_ENTT_ENTT_ID" -+ <constraint xsi:type="foreign" referenceId="CAT_PRD_FRONTEND_ACTION_PRD_ID_CAT_PRD_ENTT_ENTT_ID" - table="catalog_product_frontend_action" column="product_id" referenceTable="catalog_product_entity" - referenceColumn="entity_id" onDelete="CASCADE" /> -- <constraint xsi:type="unique" name="CATALOG_PRODUCT_FRONTEND_ACTION_VISITOR_ID_PRODUCT_ID_TYPE_ID"> -+ <constraint xsi:type="unique" referenceId="CATALOG_PRODUCT_FRONTEND_ACTION_VISITOR_ID_PRODUCT_ID_TYPE_ID"> - <column name="visitor_id"/> - <column name="product_id"/> - <column name="type_id"/> - </constraint> -- <constraint xsi:type="unique" name="CATALOG_PRODUCT_FRONTEND_ACTION_CUSTOMER_ID_PRODUCT_ID_TYPE_ID"> -+ <constraint xsi:type="unique" referenceId="CATALOG_PRODUCT_FRONTEND_ACTION_CUSTOMER_ID_PRODUCT_ID_TYPE_ID"> - <column name="customer_id"/> - <column name="product_id"/> - <column name="type_id"/> -diff --git a/app/code/Magento/Catalog/etc/db_schema_whitelist.json b/app/code/Magento/Catalog/etc/db_schema_whitelist.json -index b1543a6a007..d4bd6927d43 100644 ---- a/app/code/Magento/Catalog/etc/db_schema_whitelist.json -+++ b/app/code/Magento/Catalog/etc/db_schema_whitelist.json -@@ -484,7 +484,8 @@ - "index": { - "CATALOG_PRODUCT_ENTITY_MEDIA_GALLERY_VALUE_STORE_ID": true, - "CATALOG_PRODUCT_ENTITY_MEDIA_GALLERY_VALUE_ENTITY_ID": true, -- "CATALOG_PRODUCT_ENTITY_MEDIA_GALLERY_VALUE_VALUE_ID": true -+ "CATALOG_PRODUCT_ENTITY_MEDIA_GALLERY_VALUE_VALUE_ID": true, -+ "CAT_PRD_ENTT_MDA_GLR_VAL_ENTT_ID_VAL_ID_STORE_ID": true - }, - "constraint": { - "PRIMARY": true, -@@ -1121,4 +1122,4 @@ - "CATALOG_PRODUCT_FRONTEND_ACTION_CUSTOMER_ID_PRODUCT_ID_TYPE_ID": true - } - } --} -\ No newline at end of file -+} -diff --git a/app/code/Magento/Catalog/etc/di.xml b/app/code/Magento/Catalog/etc/di.xml -index 44564da388f..e30577a3976 100644 ---- a/app/code/Magento/Catalog/etc/di.xml -+++ b/app/code/Magento/Catalog/etc/di.xml -@@ -72,6 +72,7 @@ - <preference for="Magento\Catalog\Model\Indexer\Product\Price\UpdateIndexInterface" type="Magento\Catalog\Model\Indexer\Product\Price\InvalidateIndex" /> - <preference for="Magento\Catalog\Model\Product\Gallery\ImagesConfigFactoryInterface" type="Magento\Catalog\Model\Product\Gallery\ImagesConfigFactory" /> - <preference for="Magento\Catalog\Model\Product\Configuration\Item\ItemResolverInterface" type="Magento\Catalog\Model\Product\Configuration\Item\ItemResolverComposite" /> -+ <preference for="Magento\Catalog\Api\Data\MassActionInterface" type="\Magento\Catalog\Model\MassAction" /> - <type name="Magento\Customer\Model\ResourceModel\Visitor"> - <plugin name="catalogLog" type="Magento\Catalog\Model\Plugin\Log" /> - </type> -@@ -144,12 +145,6 @@ - <arguments> - <argument name="catalogProductStatus" xsi:type="object">Magento\Catalog\Model\Product\Attribute\Source\Status\Proxy</argument> - <argument name="productLink" xsi:type="object">Magento\Catalog\Model\Product\Link\Proxy</argument> -- <argument name="getCustomAttributeCodes" xsi:type="object">Magento\Catalog\Model\Entity\GetProductCustomAttributeCodes</argument> -- </arguments> -- </type> -- <type name="Magento\Catalog\Model\Category"> -- <arguments> -- <argument name="getCustomAttributeCodes" xsi:type="object">Magento\Catalog\Model\Entity\GetCategoryCustomAttributeCodes</argument> - </arguments> - </type> - <type name="Magento\Catalog\Model\ResourceModel\Product\Collection"> -@@ -226,6 +221,12 @@ - <item name="gif" xsi:type="string">gif</item> - <item name="png" xsi:type="string">png</item> - </argument> -+ <argument name="allowedMimeTypes" xsi:type="array"> -+ <item name="jpg" xsi:type="string">image/jpg</item> -+ <item name="jpeg" xsi:type="string">image/jpeg</item> -+ <item name="gif" xsi:type="string">image/gif</item> -+ <item name="png" xsi:type="string">image/png</item> -+ </argument> - </arguments> - </virtualType> - <type name="Magento\Catalog\Controller\Adminhtml\Category\Image\Upload"> -@@ -563,7 +564,6 @@ - <arguments> - <argument name="commands" xsi:type="array"> - <item name="productAttributesCleanUp" xsi:type="object">Magento\Catalog\Console\Command\ProductAttributesCleanUp</item> -- <item name="setPriceDimensionsMode" xsi:type="object">Magento\Catalog\Console\Command\PriceIndexerDimensionsModeSetCommand</item> - </argument> - </arguments> - </type> -@@ -603,6 +603,13 @@ - </argument> - </arguments> - </type> -+ <type name="Magento\Sales\Model\Order\ProductOption"> -+ <arguments> -+ <argument name="processorPool" xsi:type="array"> -+ <item name="custom_options" xsi:type="object">Magento\Catalog\Model\ProductOptionProcessor</item> -+ </argument> -+ </arguments> -+ </type> - <type name="Magento\Framework\Model\Entity\RepositoryFactory"> - <arguments> - <argument name="entities" xsi:type="array"> -@@ -664,12 +671,14 @@ - <item name="mediaGalleryCreate" xsi:type="string">Magento\Catalog\Model\Product\Gallery\CreateHandler</item> - <item name="categoryProductLinksSave" xsi:type="string">Magento\Catalog\Model\Category\Link\SaveHandler</item> - <item name="websitePersistor" xsi:type="string">Magento\Catalog\Model\Product\Website\SaveHandler</item> -+ <item name="tierPriceCreator" xsi:type="string">Magento\Catalog\Model\Product\Attribute\Backend\TierPrice\SaveHandler</item> - </item> - <item name="update" xsi:type="array"> - <item name="optionUpdater" xsi:type="string">Magento\Catalog\Model\Product\Option\SaveHandler</item> - <item name="mediaGalleryUpdate" xsi:type="string">Magento\Catalog\Model\Product\Gallery\UpdateHandler</item> - <item name="categoryProductLinksSave" xsi:type="string">Magento\Catalog\Model\Category\Link\SaveHandler</item> - <item name="websitePersistor" xsi:type="string">Magento\Catalog\Model\Product\Website\SaveHandler</item> -+ <item name="tierPriceUpdater" xsi:type="string">Magento\Catalog\Model\Product\Attribute\Backend\TierPrice\UpdateHandler</item> - </item> - </item> - </argument> -@@ -1142,4 +1151,26 @@ - <argument name="productType" xsi:type="string">virtual</argument> - </arguments> - </virtualType> -+ <type name="Magento\Indexer\Console\Command\IndexerSetDimensionsModeCommand"> -+ <arguments> -+ <argument name="dimensionSwitchers" xsi:type="array"> -+ <item name="catalog_product_price" xsi:type="object">Magento\Catalog\Model\Indexer\Product\Price\ModeSwitcher</item> -+ </argument> -+ </arguments> -+ </type> -+ <type name="Magento\Indexer\Console\Command\IndexerShowDimensionsModeCommand"> -+ <arguments> -+ <argument name="indexers" xsi:type="array"> -+ <item name="catalog_product_price" xsi:type="string">catalog_product_price</item> -+ </argument> -+ </arguments> -+ </type> -+ <type name="Magento\Catalog\Model\Product\Option\Type\Select"> -+ <arguments> -+ <argument name="singleSelectionTypes" xsi:type="array"> -+ <item name="drop_down" xsi:type="const">Magento\Catalog\Api\Data\ProductCustomOptionInterface::OPTION_TYPE_DROP_DOWN</item> -+ <item name="radio" xsi:type="const">Magento\Catalog\Api\Data\ProductCustomOptionInterface::OPTION_TYPE_RADIO</item> -+ </argument> -+ </arguments> -+ </type> - </config> -diff --git a/app/code/Magento/Catalog/etc/frontend/di.xml b/app/code/Magento/Catalog/etc/frontend/di.xml -index 55098037191..ee9c5b29da8 100644 ---- a/app/code/Magento/Catalog/etc/frontend/di.xml -+++ b/app/code/Magento/Catalog/etc/frontend/di.xml -@@ -116,4 +116,8 @@ - <plugin name="get_catalog_category_product_index_table_name" type="Magento\Catalog\Model\Indexer\Category\Product\Plugin\TableResolver"/> - <plugin name="get_catalog_product_price_index_table_name" type="Magento\Catalog\Model\Indexer\Product\Price\Plugin\TableResolver"/> - </type> -+ <type name="Magento\Framework\App\Action\AbstractAction"> -+ <plugin name="catalog_app_action_dispatch_controller_context_plugin" -+ type="Magento\Catalog\Plugin\Framework\App\Action\ContextPlugin" /> -+ </type> - </config> -diff --git a/app/code/Magento/Catalog/etc/product_options.xsd b/app/code/Magento/Catalog/etc/product_options.xsd -index 3bc24a90992..734c8f378d5 100644 ---- a/app/code/Magento/Catalog/etc/product_options.xsd -+++ b/app/code/Magento/Catalog/etc/product_options.xsd -@@ -61,11 +61,11 @@ - <xs:simpleType name="modelName"> - <xs:annotation> - <xs:documentation> -- Model name can contain only [a-zA-Z_\\]. -+ Model name can contain only ([\\]?[a-zA-Z_][a-zA-Z0-9_]*)+. - </xs:documentation> - </xs:annotation> - <xs:restriction base="xs:string"> -- <xs:pattern value="[a-zA-Z_\\]+" /> -+ <xs:pattern value="([\\]?[a-zA-Z_][a-zA-Z0-9_]*)+" /> - </xs:restriction> - </xs:simpleType> - </xs:schema> -diff --git a/app/code/Magento/Catalog/etc/product_types_base.xsd b/app/code/Magento/Catalog/etc/product_types_base.xsd -index 6cc35fd7bee..dec952bcf49 100644 ---- a/app/code/Magento/Catalog/etc/product_types_base.xsd -+++ b/app/code/Magento/Catalog/etc/product_types_base.xsd -@@ -92,11 +92,11 @@ - <xs:simpleType name="modelName"> - <xs:annotation> - <xs:documentation> -- Model name can contain only [a-zA-Z_\\]. -+ Model name can contain only ([\\]?[a-zA-Z_][a-zA-Z0-9_]*)+. - </xs:documentation> - </xs:annotation> - <xs:restriction base="xs:string"> -- <xs:pattern value="[a-zA-Z_\\]+" /> -+ <xs:pattern value="([\\]?[a-zA-Z_][a-zA-Z0-9_]*)+" /> - </xs:restriction> - </xs:simpleType> - </xs:schema> -diff --git a/app/code/Magento/Catalog/etc/queue.xml b/app/code/Magento/Catalog/etc/queue.xml -new file mode 100644 -index 00000000000..137f34a5c1e ---- /dev/null -+++ b/app/code/Magento/Catalog/etc/queue.xml -@@ -0,0 +1,15 @@ -+<?xml version="1.0"?> -+<!-- -+/** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework-message-queue:etc/queue.xsd"> -+ <broker topic="product_action_attribute.update" exchange="magento-db" type="db"> -+ <queue name="product_action_attribute.update" consumer="product_action_attribute.update" consumerInstance="Magento\Framework\MessageQueue\Consumer" handler="Magento\Catalog\Model\Attribute\Backend\Consumer::process"/> -+ </broker> -+ <broker topic="product_action_attribute.website.update" exchange="magento-db" type="db"> -+ <queue name="product_action_attribute.website.update" consumer="product_action_attribute.website.update" consumerInstance="Magento\Framework\MessageQueue\Consumer" handler="Magento\Catalog\Model\Attribute\Backend\ConsumerWebsiteAssign::process"/> -+ </broker> -+</config> -\ No newline at end of file -diff --git a/app/code/Magento/Catalog/etc/queue_consumer.xml b/app/code/Magento/Catalog/etc/queue_consumer.xml -new file mode 100644 -index 00000000000..d9e66ae69c1 ---- /dev/null -+++ b/app/code/Magento/Catalog/etc/queue_consumer.xml -@@ -0,0 +1,11 @@ -+<?xml version="1.0"?> -+<!-- -+/** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework-message-queue:etc/consumer.xsd"> -+ <consumer name="product_action_attribute.update" queue="product_action_attribute.update" connection="db" maxMessages="5000" consumerInstance="Magento\Framework\MessageQueue\Consumer" handler="Magento\Catalog\Model\Attribute\Backend\Consumer::process" /> -+ <consumer name="product_action_attribute.website.update" queue="product_action_attribute.website.update" connection="db" maxMessages="5000" consumerInstance="Magento\Framework\MessageQueue\Consumer" handler="Magento\Catalog\Model\Attribute\Backend\ConsumerWebsiteAssign::process" /> -+</config> -\ No newline at end of file -diff --git a/app/code/Magento/Catalog/etc/queue_publisher.xml b/app/code/Magento/Catalog/etc/queue_publisher.xml -new file mode 100644 -index 00000000000..1606ea42ec0 ---- /dev/null -+++ b/app/code/Magento/Catalog/etc/queue_publisher.xml -@@ -0,0 +1,15 @@ -+<?xml version="1.0"?> -+<!-- -+/** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework-message-queue:etc/publisher.xsd"> -+ <publisher topic="product_action_attribute.update"> -+ <connection name="db" exchange="magento-db" /> -+ </publisher> -+ <publisher topic="product_action_attribute.website.update"> -+ <connection name="db" exchange="magento-db" /> -+ </publisher> -+</config> -\ No newline at end of file -diff --git a/app/code/Magento/Catalog/etc/queue_topology.xml b/app/code/Magento/Catalog/etc/queue_topology.xml -new file mode 100644 -index 00000000000..bdac891afbd ---- /dev/null -+++ b/app/code/Magento/Catalog/etc/queue_topology.xml -@@ -0,0 +1,13 @@ -+<?xml version="1.0"?> -+<!-- -+/** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework-message-queue:etc/topology.xsd"> -+ <exchange name="magento-db" type="topic" connection="db"> -+ <binding id="updateBinding" topic="product_action_attribute.update" destinationType="queue" destination="product_action_attribute.update"/> -+ <binding id="updateBindingWebsite" topic="product_action_attribute.website.update" destinationType="queue" destination="product_action_attribute.website.update"/> -+ </exchange> -+</config> -\ No newline at end of file -diff --git a/app/code/Magento/Catalog/etc/webapi_rest/di.xml b/app/code/Magento/Catalog/etc/webapi_rest/di.xml -index 2a5d60222e9..44cdd473bf7 100644 ---- a/app/code/Magento/Catalog/etc/webapi_rest/di.xml -+++ b/app/code/Magento/Catalog/etc/webapi_rest/di.xml -@@ -19,4 +19,7 @@ - <plugin name="get_catalog_category_product_index_table_name" type="Magento\Catalog\Model\Indexer\Category\Product\Plugin\TableResolver"/> - <plugin name="get_catalog_product_price_index_table_name" type="Magento\Catalog\Model\Indexer\Product\Price\Plugin\TableResolver"/> - </type> -+ <type name="Magento\Catalog\Api\ProductCustomOptionRepositoryInterface"> -+ <plugin name="updateProductCustomOptionsAttributes" type="Magento\Catalog\Plugin\Model\Product\Option\UpdateProductCustomOptionsAttributes"/> -+ </type> - </config> -diff --git a/app/code/Magento/Catalog/etc/webapi_soap/di.xml b/app/code/Magento/Catalog/etc/webapi_soap/di.xml -index 2a5d60222e9..44cdd473bf7 100644 ---- a/app/code/Magento/Catalog/etc/webapi_soap/di.xml -+++ b/app/code/Magento/Catalog/etc/webapi_soap/di.xml -@@ -19,4 +19,7 @@ - <plugin name="get_catalog_category_product_index_table_name" type="Magento\Catalog\Model\Indexer\Category\Product\Plugin\TableResolver"/> - <plugin name="get_catalog_product_price_index_table_name" type="Magento\Catalog\Model\Indexer\Product\Price\Plugin\TableResolver"/> - </type> -+ <type name="Magento\Catalog\Api\ProductCustomOptionRepositoryInterface"> -+ <plugin name="updateProductCustomOptionsAttributes" type="Magento\Catalog\Plugin\Model\Product\Option\UpdateProductCustomOptionsAttributes"/> -+ </type> - </config> -diff --git a/app/code/Magento/Catalog/i18n/en_US.csv b/app/code/Magento/Catalog/i18n/en_US.csv -index f2a3ab8f83f..ed27dfd646c 100644 ---- a/app/code/Magento/Catalog/i18n/en_US.csv -+++ b/app/code/Magento/Catalog/i18n/en_US.csv -@@ -233,7 +233,6 @@ Products,Products - "This attribute set no longer exists.","This attribute set no longer exists." - "You saved the attribute set.","You saved the attribute set." - "Something went wrong while saving the attribute set.","Something went wrong while saving the attribute set." --"You added product %1 to the comparison list.","You added product %1 to the comparison list." - "You cleared the comparison list.","You cleared the comparison list." - "Something went wrong clearing the comparison list.","Something went wrong clearing the comparison list." - "You removed product %1 from the comparison list.","You removed product %1 from the comparison list." -@@ -808,4 +807,5 @@ Details,Details - "Product Name or SKU", "Product Name or SKU" - "Start typing to find products", "Start typing to find products" - "Product with ID: (%1) doesn't exist", "Product with ID: (%1) doesn't exist" --"Category with ID: (%1) doesn't exist", "Category with ID: (%1) doesn't exist" -\ No newline at end of file -+"Category with ID: (%1) doesn't exist", "Category with ID: (%1) doesn't exist" -+"You added product %1 to the <a href=""%2"">comparison list</a>.","You added product %1 to the <a href=""%2"">comparison list</a>." -\ No newline at end of file -diff --git a/app/code/Magento/Catalog/view/adminhtml/layout/catalog_product_crosssell.xml b/app/code/Magento/Catalog/view/adminhtml/layout/catalog_product_crosssell.xml -index 4a27158be5f..d7aa058a1f4 100644 ---- a/app/code/Magento/Catalog/view/adminhtml/layout/catalog_product_crosssell.xml -+++ b/app/code/Magento/Catalog/view/adminhtml/layout/catalog_product_crosssell.xml -@@ -5,6 +5,10 @@ - * See COPYING.txt for license details. - */ - --> -+<!-- -+@deprecated Not used since cross-sell products grid moved to UI components. -+@see Magento_Catalog::view/adminhtml/ui_component/crosssell_product_listing.xml -+--> - <layout xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:View/Layout/etc/layout_generic.xsd"> - <container name="root" label="Root"> - <block class="Magento\Catalog\Block\Adminhtml\Product\Edit\Tab\Crosssell" name="catalog.product.edit.tab.crosssell"/> -diff --git a/app/code/Magento/Catalog/view/adminhtml/layout/catalog_product_crosssellgrid.xml b/app/code/Magento/Catalog/view/adminhtml/layout/catalog_product_crosssellgrid.xml -index b5efecf0d03..3ba4562c9d3 100644 ---- a/app/code/Magento/Catalog/view/adminhtml/layout/catalog_product_crosssellgrid.xml -+++ b/app/code/Magento/Catalog/view/adminhtml/layout/catalog_product_crosssellgrid.xml -@@ -5,6 +5,10 @@ - * See COPYING.txt for license details. - */ - --> -+<!-- -+@deprecated Not used since cross-sell products grid moved to UI components. -+@see Magento_Catalog::view/adminhtml/ui_component/crosssell_product_listing.xml -+--> - <layout xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:View/Layout/etc/layout_generic.xsd"> - <container name="root" label="Root"> - <block class="Magento\Catalog\Block\Adminhtml\Product\Edit\Tab\Crosssell" name="catalog.product.edit.tab.crosssell"/> -diff --git a/app/code/Magento/Catalog/view/adminhtml/layout/catalog_product_grid.xml b/app/code/Magento/Catalog/view/adminhtml/layout/catalog_product_grid.xml -index b42a78dfbe3..5d57c2d8f86 100644 ---- a/app/code/Magento/Catalog/view/adminhtml/layout/catalog_product_grid.xml -+++ b/app/code/Magento/Catalog/view/adminhtml/layout/catalog_product_grid.xml -@@ -8,6 +8,8 @@ - <layout xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:View/Layout/etc/layout_generic.xsd"> - <update handle="formkey"/> - <container name="root" label="Root"> -- <block class="Magento\Catalog\Block\Adminhtml\Product\Grid" name="admin.product.grid"/> -+ <block class="Magento\Catalog\Block\Adminhtml\Product\Grid" name="admin.product.grid"> -+ <block class="Magento\Framework\View\Element\Text\ListText" name="grid.bottom.links"/> -+ </block> - </container> - </layout> -diff --git a/app/code/Magento/Catalog/view/adminhtml/layout/catalog_product_related.xml b/app/code/Magento/Catalog/view/adminhtml/layout/catalog_product_related.xml -index 1340d40ef4f..c40c4a2818e 100644 ---- a/app/code/Magento/Catalog/view/adminhtml/layout/catalog_product_related.xml -+++ b/app/code/Magento/Catalog/view/adminhtml/layout/catalog_product_related.xml -@@ -5,6 +5,10 @@ - * See COPYING.txt for license details. - */ - --> -+<!-- -+@deprecated Not used since related products grid moved to UI components. -+@see Magento_Catalog::view/adminhtml/ui_component/related_product_listing.xml -+--> - <layout xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:View/Layout/etc/layout_generic.xsd"> - <container name="root" label="Root"> - <block class="Magento\Catalog\Block\Adminhtml\Product\Edit\Tab\Related" name="catalog.product.edit.tab.related"/> -diff --git a/app/code/Magento/Catalog/view/adminhtml/layout/catalog_product_relatedgrid.xml b/app/code/Magento/Catalog/view/adminhtml/layout/catalog_product_relatedgrid.xml -index 1ae83419ae6..38b791d88a0 100644 ---- a/app/code/Magento/Catalog/view/adminhtml/layout/catalog_product_relatedgrid.xml -+++ b/app/code/Magento/Catalog/view/adminhtml/layout/catalog_product_relatedgrid.xml -@@ -5,6 +5,10 @@ - * See COPYING.txt for license details. - */ - --> -+<!-- -+@deprecated Not used since related products grid moved to UI components. -+@see Magento_Catalog::view/adminhtml/ui_component/related_product_listing.xml -+--> - <layout xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:View/Layout/etc/layout_generic.xsd"> - <container name="root" label="Root"> - <block class="Magento\Catalog\Block\Adminhtml\Product\Edit\Tab\Related" name="catalog.product.edit.tab.related"/> -diff --git a/app/code/Magento/Catalog/view/adminhtml/layout/catalog_product_set_block.xml b/app/code/Magento/Catalog/view/adminhtml/layout/catalog_product_set_block.xml -index 44884897461..4e739660882 100644 ---- a/app/code/Magento/Catalog/view/adminhtml/layout/catalog_product_set_block.xml -+++ b/app/code/Magento/Catalog/view/adminhtml/layout/catalog_product_set_block.xml -@@ -11,7 +11,7 @@ - <block class="Magento\Backend\Block\Widget\Grid" name="adminhtml.catalog.product.set.grid" as="grid"> - <arguments> - <argument name="id" xsi:type="string">setGrid</argument> -- <argument name="dataSource" xsi:type="object">Magento\Eav\Model\ResourceModel\Entity\Attribute\Grid\Collection</argument> -+ <argument name="dataSource" xsi:type="object" shared="false">Magento\Eav\Model\ResourceModel\Entity\Attribute\Grid\Collection</argument> - <argument name="default_sort" xsi:type="string">set_name</argument> - <argument name="default_dir" xsi:type="string">ASC</argument> - <argument name="save_parameters_in_session" xsi:type="string">1</argument> -diff --git a/app/code/Magento/Catalog/view/adminhtml/layout/catalog_product_upsell.xml b/app/code/Magento/Catalog/view/adminhtml/layout/catalog_product_upsell.xml -index a9770ed3c18..eea44504119 100644 ---- a/app/code/Magento/Catalog/view/adminhtml/layout/catalog_product_upsell.xml -+++ b/app/code/Magento/Catalog/view/adminhtml/layout/catalog_product_upsell.xml -@@ -5,6 +5,10 @@ - * See COPYING.txt for license details. - */ - --> -+<!-- -+@deprecated Not used since upsell products grid moved to UI components. -+@see Magento_Catalog::view/adminhtml/ui_component/upsell_product_listing.xml -+--> - <layout xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:View/Layout/etc/layout_generic.xsd"> - <container name="root" label="Root"> - <block class="Magento\Catalog\Block\Adminhtml\Product\Edit\Tab\Upsell" name="catalog.product.edit.tab.upsell"/> -diff --git a/app/code/Magento/Catalog/view/adminhtml/layout/catalog_product_upsellgrid.xml b/app/code/Magento/Catalog/view/adminhtml/layout/catalog_product_upsellgrid.xml -index a4acf572cae..2c400746c64 100644 ---- a/app/code/Magento/Catalog/view/adminhtml/layout/catalog_product_upsellgrid.xml -+++ b/app/code/Magento/Catalog/view/adminhtml/layout/catalog_product_upsellgrid.xml -@@ -5,6 +5,10 @@ - * See COPYING.txt for license details. - */ - --> -+<!-- -+@deprecated Not used since upsell products grid moved to UI components. -+@see Magento_Catalog::view/adminhtml/ui_component/upsell_product_listing.xml -+--> - <layout xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:View/Layout/etc/layout_generic.xsd"> - <container name="root" label="Root"> - <block class="Magento\Catalog\Block\Adminhtml\Product\Edit\Tab\Upsell" name="catalog.product.edit.tab.upsell"/> -diff --git a/app/code/Magento/Catalog/view/adminhtml/templates/catalog/category/checkboxes/tree.phtml b/app/code/Magento/Catalog/view/adminhtml/templates/catalog/category/checkboxes/tree.phtml -index 00a1580923a..cea54e883d2 100644 ---- a/app/code/Magento/Catalog/view/adminhtml/templates/catalog/category/checkboxes/tree.phtml -+++ b/app/code/Magento/Catalog/view/adminhtml/templates/catalog/category/checkboxes/tree.phtml -@@ -4,7 +4,6 @@ - * See COPYING.txt for license details. - */ - --// @codingStandardsIgnoreFile - /** - * @var $block \Magento\Catalog\Block\Adminhtml\Category\Tree - */ -@@ -20,7 +19,7 @@ - "categoryCheckboxTree": { - "dataUrl": "<?= $block->escapeUrl($block->getLoadTreeUrl()) ?>", - "divId": "<?= /* @noEscape */ $divId ?>", -- "rootVisible": <?= /* @noEscape */ $block->getRoot()->getIsVisible() ? 'true' : 'false' ?>, -+ "rootVisible": false, - "useAjax": <?= $block->escapeHtml($block->getUseAjax()) ?>, - "currentNodeId": <?= (int)$block->getCategoryId() ?>, - "jsFormObject": "<?= /* @noEscape */ $block->getJsFormObject() ?>", -@@ -28,7 +27,7 @@ - "checked": "<?= $block->escapeHtml($block->getRoot()->getChecked()) ?>", - "allowdDrop": <?= /* @noEscape */ $block->getRoot()->getIsVisible() ? 'true' : 'false' ?>, - "rootId": <?= (int)$block->getRoot()->getId() ?>, -- "expanded": <?= (int)$block->getIsWasExpanded() ?>, -+ "expanded": true, - "categoryId": <?= (int)$block->getCategoryId() ?>, - "treeJson": <?= /* @noEscape */ $block->getTreeJson() ?> - } -diff --git a/app/code/Magento/Catalog/view/adminhtml/templates/catalog/category/edit.phtml b/app/code/Magento/Catalog/view/adminhtml/templates/catalog/category/edit.phtml -index f58b39a819a..c77b66733af 100644 ---- a/app/code/Magento/Catalog/view/adminhtml/templates/catalog/category/edit.phtml -+++ b/app/code/Magento/Catalog/view/adminhtml/templates/catalog/category/edit.phtml -@@ -5,18 +5,18 @@ - */ - - /** -- * Template for \Magento\Catalog\Block\Adminhtml\Category\Edit -+ * @var $block \Magento\Catalog\Block\Adminhtml\Category\Edit - */ - ?> - <div data-id="information-dialog-category" class="messages" style="display: none;"> - <div class="message message-notice"> -- <div><?= /* @escapeNotVerified */ __('This operation can take a long time') ?></div> -+ <div><?= $block->escapeHtml(__('This operation can take a long time')) ?></div> - </div> - </div> - <script type="text/x-magento-init"> - { - "*": { -- "categoryForm": {"refreshUrl": "<?= /* @escapeNotVerified */ $block->getRefreshPathUrl() ?>"} -+ "categoryForm": {"refreshUrl": "<?= $block->escapeJs($block->escapeUrl($block->getRefreshPathUrl())) ?>"} - } - } - </script> -diff --git a/app/code/Magento/Catalog/view/adminhtml/templates/catalog/category/edit/assign_products.phtml b/app/code/Magento/Catalog/view/adminhtml/templates/catalog/category/edit/assign_products.phtml -index 4691a709cad..af7aec12a57 100644 ---- a/app/code/Magento/Catalog/view/adminhtml/templates/catalog/category/edit/assign_products.phtml -+++ b/app/code/Magento/Catalog/view/adminhtml/templates/catalog/category/edit/assign_products.phtml -@@ -16,8 +16,8 @@ $gridJsObjectName = $blockGrid->getJsObjectName(); - { - "*": { - "Magento_Catalog/catalog/category/assign-products": { -- "selectedProducts": <?= /* @escapeNotVerified */ $block->getProductsJson() ?>, -- "gridJsObjectName": <?= /* @escapeNotVerified */ '"' . $gridJsObjectName . '"' ?: '{}' ?> -+ "selectedProducts": <?= /* @noEscape */ $block->getProductsJson() ?>, -+ "gridJsObjectName": <?= /* @noEscape */ '"' . $gridJsObjectName . '"' ?: '{}' ?> - } - } - } -diff --git a/app/code/Magento/Catalog/view/adminhtml/templates/catalog/category/tree.phtml b/app/code/Magento/Catalog/view/adminhtml/templates/catalog/category/tree.phtml -index 9865589556e..6b63a20134d 100644 ---- a/app/code/Magento/Catalog/view/adminhtml/templates/catalog/category/tree.phtml -+++ b/app/code/Magento/Catalog/view/adminhtml/templates/catalog/category/tree.phtml -@@ -4,27 +4,26 @@ - * See COPYING.txt for license details. - */ - --// @codingStandardsIgnoreFile -- -+/** @var $block \Magento\Catalog\Block\Adminhtml\Category\Tree */ - ?> - <div class="categories-side-col"> - <div class="sidebar-actions"> -- <?php if ($block->getRoot()): ?> -+ <?php if ($block->getRoot()) :?> - <?= $block->getAddRootButtonHtml() ?><br/> - <?= $block->getAddSubButtonHtml() ?> - <?php endif; ?> - </div> - <div class="tree-actions"> -- <?php if ($block->getRoot()): ?> -+ <?php if ($block->getRoot()) :?> - <?php //echo $block->getCollapseButtonHtml() ?> - <?php //echo $block->getExpandButtonHtml() ?> - <a href="#" -- onclick="tree.collapseTree(); return false;"><?= /* @escapeNotVerified */ __('Collapse All') ?></a> -+ onclick="tree.collapseTree(); return false;"><?= $block->escapeHtml(__('Collapse All')) ?></a> - <span class="separator">|</span> <a href="#" -- onclick="tree.expandTree(); return false;"><?= /* @escapeNotVerified */ __('Expand All') ?></a> -+ onclick="tree.expandTree(); return false;"><?= $block->escapeHtml(__('Expand All')) ?></a> - <?php endif; ?> - </div> -- <?php if ($block->getRoot()): ?> -+ <?php if ($block->getRoot()) :?> - <div class="tree-holder"> - <div id="tree-div" class="tree-wrapper"></div> - </div> -@@ -32,7 +31,7 @@ - - <div data-id="information-dialog-tree" class="messages" style="display: none;"> - <div class="message message-notice"> -- <div><?= /* @escapeNotVerified */ __('This operation can take a long time') ?></div> -+ <div><?= $block->escapeHtml(__('This operation can take a long time')) ?></div> - </div> - </div> - <script> -@@ -172,7 +171,7 @@ - - if (!this.collapsed) { - this.collapsed = true; -- this.loader.dataUrl = '<?= /* @escapeNotVerified */ $block->getLoadTreeUrl(false) ?>'; -+ this.loader.dataUrl = '<?= $block->escapeJs($block->escapeUrl($block->getLoadTreeUrl(false))) ?>'; - this.request(this.loader.dataUrl, false); - } - }, -@@ -181,7 +180,7 @@ - this.expandAll(); - if (this.collapsed) { - this.collapsed = false; -- this.loader.dataUrl = '<?= /* @escapeNotVerified */ $block->getLoadTreeUrl(true) ?>'; -+ this.loader.dataUrl = '<?= $block->escapeJs($block->escapeUrl($block->getLoadTreeUrl(true))) ?>'; - this.request(this.loader.dataUrl, false); - } - }, -@@ -216,7 +215,7 @@ - if (tree && switcherParams) { - var url; - if (switcherParams.useConfirm) { -- if (!confirm("<?= /* @escapeNotVerified */ __('Please confirm site switching. All data that hasn\'t been saved will be lost.') ?>")) { -+ if (!confirm("<?= $block->escapeJs(__('Please confirm site switching. All data that hasn\'t been saved will be lost.')) ?>")) { - return false; - } - } -@@ -248,7 +247,7 @@ - try { - response = JSON.parse(transport.responseText); - } catch (e) { -- console.warn('An error occured while parsing response'); -+ console.warn('An error occurred while parsing response'); - } - - if (!response || !response['parameters']) { -@@ -259,7 +258,7 @@ - } - }); - } else { -- var baseUrl = '<?= /* @escapeNotVerified */ $block->getEditUrl() ?>'; -+ var baseUrl = '<?= $block->escapeJs($block->escapeUrl($block->getEditUrl())) ?>'; - var urlExt = switcherParams.scopeParams + 'id/' + tree.currentNodeId + '/'; - url = parseSidUrl(baseUrl, urlExt); - setLocation(url); -@@ -296,17 +295,18 @@ - if (scopeParams) { - url = url + scopeParams; - } -- <?php if ($block->isClearEdit()): ?> -+ <?php if ($block->isClearEdit()) :?> - if (selectedNode) { - url = url + 'id/' + config.parameters.category_id; - } - <?php endif;?> - //updateContent(url); //commented since ajax requests replaced with http ones to load a category -+ jQuery('#tree-div').find('.x-tree-node-el').first().remove(); - } - - jQuery(function () { - categoryLoader = new Ext.tree.TreeLoader({ -- dataUrl: '<?= /* @escapeNotVerified */ $block->getLoadTreeUrl() ?>' -+ dataUrl: '<?= $block->escapeJs($block->escapeUrl($block->getLoadTreeUrl())) ?>' - }); - - categoryLoader.processResponse = function (response, parent, callback) { -@@ -388,26 +388,26 @@ - enableDD: true, - containerScroll: true, - selModel: new Ext.tree.CheckNodeMultiSelectionModel(), -- rootVisible: '<?= /* @escapeNotVerified */ $block->getRoot()->getIsVisible() ?>', -- useAjax: <?= /* @escapeNotVerified */ $block->getUseAjax() ?>, -- switchTreeUrl: '<?= /* @escapeNotVerified */ $block->getSwitchTreeUrl() ?>', -- editUrl: '<?= /* @escapeNotVerified */ $block->getEditUrl() ?>', -- currentNodeId: <?= /* @escapeNotVerified */ (int)$block->getCategoryId() ?>, -- baseUrl: '<?= /* @escapeNotVerified */ $block->getEditUrl() ?>' -+ rootVisible: '<?= (bool)$block->getRoot()->getIsVisible() ?>', -+ useAjax: <?= $block->escapeJs($block->getUseAjax()) ?>, -+ switchTreeUrl: '<?= $block->escapeJs($block->escapeUrl($block->getSwitchTreeUrl())) ?>', -+ editUrl: '<?= $block->escapeJs($block->escapeUrl($block->getEditUrl())) ?>', -+ currentNodeId: <?= (int)$block->getCategoryId() ?>, -+ baseUrl: '<?= $block->escapeJs($block->escapeUrl($block->getEditUrl())) ?>' - }; - - defaultLoadTreeParams = { - parameters: { -- text: <?= /* @escapeNotVerified */ json_encode(htmlentities($block->getRoot()->getName())) ?>, -+ text: <?= /* @noEscape */ json_encode(htmlentities($block->getRoot()->getName())) ?>, - draggable: false, -- allowDrop: <?php if ($block->getRoot()->getIsVisible()): ?>true<?php else : ?>false<?php endif; ?>, -+ allowDrop: <?php if ($block->getRoot()->getIsVisible()) :?>true<?php else :?>false<?php endif; ?>, - id: <?= (int)$block->getRoot()->getId() ?>, - expanded: <?= (int)$block->getIsWasExpanded() ?>, - store_id: <?= (int)$block->getStore()->getId() ?>, - category_id: <?= (int)$block->getCategoryId() ?>, - parent: <?= (int)$block->getRequest()->getParam('parent') ?> - }, -- data: <?= /* @escapeNotVerified */ $block->getTreeJson() ?> -+ data: <?= /* @noEscape */ $block->getTreeJson() ?> - }; - - reRenderTree(); -@@ -485,7 +485,7 @@ - click: function () { - (function ($) { - $.ajax({ -- url: '<?= /* @escapeNotVerified */ $block->getMoveUrl() ?>', -+ url: '<?= $block->escapeJs($block->escapeUrl($block->getMoveUrl())) ?>', - method: 'POST', - data: registry.get('pd'), - showLoader: true -diff --git a/app/code/Magento/Catalog/view/adminhtml/templates/catalog/category/widget/tree.phtml b/app/code/Magento/Catalog/view/adminhtml/templates/catalog/category/widget/tree.phtml -index dbe66ef1aec..e24d676974b 100644 ---- a/app/code/Magento/Catalog/view/adminhtml/templates/catalog/category/widget/tree.phtml -+++ b/app/code/Magento/Catalog/view/adminhtml/templates/catalog/category/widget/tree.phtml -@@ -3,24 +3,21 @@ - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -- --// @codingStandardsIgnoreFile -- - ?> - - <?php $_divId = 'tree' . $block->getId() ?> --<div id="<?= /* @escapeNotVerified */ $_divId ?>" class="tree"></div> -+<div id="<?= $block->escapeHtmlAttr($_divId) ?>" class="tree"></div> - <!--[if IE]> - <script id="ie-deferred-loader" defer="defer" src="//:"></script> - <![endif]--> - <script> - require(['jquery', "prototype", "extjs/ext-tree-checkbox"], function(jQuery){ - --var tree<?= /* @escapeNotVerified */ $block->getId() ?>; -+var tree<?= $block->escapeJs($block->getId()) ?>; - --var useMassaction = <?= /* @escapeNotVerified */ $block->getUseMassaction() ? 1 : 0 ?>; -+var useMassaction = <?= $block->getUseMassaction() ? 1 : 0 ?>; - --var isAnchorOnly = <?= /* @escapeNotVerified */ $block->getIsAnchorOnly() ? 1 : 0 ?>; -+var isAnchorOnly = <?= $block->getIsAnchorOnly() ? 1 : 0 ?>; - - Ext.tree.TreePanel.Enhanced = function(el, config) - { -@@ -44,8 +41,8 @@ Ext.extend(Ext.tree.TreePanel.Enhanced, Ext.tree.TreePanel, { - this.setRootNode(root); - - if (firstLoad) { -- <?php if ($block->getNodeClickListener()): ?> -- this.addListener('click', <?= /* @escapeNotVerified */ $block->getNodeClickListener() ?>.createDelegate(this)); -+ <?php if ($block->getNodeClickListener()) :?> -+ this.addListener('click', <?= /* @noEscape */ $block->getNodeClickListener() ?>.createDelegate(this)); - <?php endif; ?> - } - -@@ -58,10 +55,10 @@ Ext.extend(Ext.tree.TreePanel.Enhanced, Ext.tree.TreePanel, { - - jQuery(function() - { -- var emptyNodeAdded = <?= /* @escapeNotVerified */ ($block->getWithEmptyNode() ? 'false' : 'true') ?>; -+ var emptyNodeAdded = <?= ($block->getWithEmptyNode() ? 'false' : 'true') ?>; - - var categoryLoader = new Ext.tree.TreeLoader({ -- dataUrl: '<?= /* @escapeNotVerified */ $block->getLoadTreeUrl() ?>' -+ dataUrl: '<?= $block->escapeJs($block->escapeUrl($block->getLoadTreeUrl())) ?>' - }); - - categoryLoader.buildCategoryTree = function(parent, config) -@@ -80,7 +77,7 @@ jQuery(function() - // Add empty node to reset category filter - if(!emptyNodeAdded) { - var empty = Object.clone(_node); -- empty.text = '<?= /* @escapeNotVerified */ __('None') ?>'; -+ empty.text = '<?= $block->escapeJs(__('None')) ?>'; - empty.children = []; - empty.id = 'none'; - empty.path = '1/none'; -@@ -151,25 +148,25 @@ jQuery(function() - }; - - categoryLoader.on("beforeload", function(treeLoader, node) { -- $('<?= /* @escapeNotVerified */ $_divId ?>').fire('category:beforeLoad', {treeLoader:treeLoader}); -+ $('<?= $block->escapeJs($_divId) ?>').fire('category:beforeLoad', {treeLoader:treeLoader}); - treeLoader.baseParams.id = node.attributes.id; - }); - -- tree<?= /* @escapeNotVerified */ $block->getId() ?> = new Ext.tree.TreePanel.Enhanced('<?= /* @escapeNotVerified */ $_divId ?>', { -+ tree<?= $block->escapeJs($block->getId()) ?> = new Ext.tree.TreePanel.Enhanced('<?= $block->escapeJs($_divId) ?>', { - animate: false, - loader: categoryLoader, - enableDD: false, - containerScroll: true, -- rootVisible: '<?= /* @escapeNotVerified */ $block->getRoot()->getIsVisible() ?>', -+ rootVisible: false, - useAjax: true, - currentNodeId: <?= (int) $block->getCategoryId() ?>, - addNodeTo: false - }); - - if (useMassaction) { -- tree<?= /* @escapeNotVerified */ $block->getId() ?>.on('check', function(node) { -- $('<?= /* @escapeNotVerified */ $_divId ?>').fire('node:changed', {node:node}); -- }, tree<?= /* @escapeNotVerified */ $block->getId() ?>); -+ tree<?= $block->escapeJs($block->getId()) ?>.on('check', function(node) { -+ $('<?= $block->escapeJs($_divId) ?>').fire('node:changed', {node:node}); -+ }, tree<?= $block->escapeJs($block->getId()) ?>); - } - - // set the root node -@@ -177,11 +174,11 @@ jQuery(function() - text: 'Psw', - draggable: false, - id: <?= (int) $block->getRoot()->getId() ?>, -- expanded: <?= (int) $block->getIsWasExpanded() ?>, -+ expanded: true, - category_id: <?= (int) $block->getCategoryId() ?> - }; - -- tree<?= /* @escapeNotVerified */ $block->getId() ?>.loadTree({parameters:parameters, data:<?= /* @escapeNotVerified */ $block->getTreeJson() ?>},true); -+ tree<?= $block->escapeJs($block->getId()) ?>.loadTree({parameters:parameters, data:<?= /* @noEscape */ $block->getTreeJson() ?>},true); - - }); - -diff --git a/app/code/Magento/Catalog/view/adminhtml/templates/catalog/form/renderer/fieldset/element.phtml b/app/code/Magento/Catalog/view/adminhtml/templates/catalog/form/renderer/fieldset/element.phtml -index 680361eae44..cbda491a647 100644 ---- a/app/code/Magento/Catalog/view/adminhtml/templates/catalog/form/renderer/fieldset/element.phtml -+++ b/app/code/Magento/Catalog/view/adminhtml/templates/catalog/form/renderer/fieldset/element.phtml -@@ -3,19 +3,16 @@ - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -- --// @codingStandardsIgnoreFile -- - ?> - <?php --/** -- * @see \Magento\Catalog\Block\Adminhtml\Form\Renderer\Fieldset\Element -- */ -+// phpcs:disable Magento2.Templates.ThisInTemplate.FoundThis -+ -+/** @var $block \Magento\Catalog\Block\Adminhtml\Form\Renderer\Fieldset\Element */ - ?> - <?php - /* @var $block \Magento\Backend\Block\Widget\Form\Renderer\Fieldset\Element */ - $element = $block->getElement(); --$note = $element->getNote() ? '<div class="note admin__field-note">' . $element->getNote() . '</div>' : ''; -+$note = $element->getNote() ? '<div class="note admin__field-note">' . $block->escapeHtml($element->getNote()) . '</div>' : ''; - $elementBeforeLabel = $element->getExtType() == 'checkbox' || $element->getExtType() == 'radio'; - $addOn = $element->getBeforeElementHtml() || $element->getAfterElementHtml(); - $fieldId = ($element->getHtmlId()) ? ' id="attribute-' . $element->getHtmlId() . '-container"' : ''; -@@ -27,8 +24,8 @@ $fieldClass .= ($element->getRequired()) ? ' required' : ''; - $fieldClass .= ($note) ? ' with-note' : ''; - $fieldClass .= ($entity && $entity->getIsUserDefined()) ? ' user-defined type-' . $entity->getFrontendInput() : ''; - --$fieldAttributes = $fieldId . ' class="' . $fieldClass . '" ' -- . $block->getUiId('form-field', $element->getId()); -+$fieldAttributes = $fieldId . ' class="' . $block->escapeHtmlAttr($fieldClass) . '" ' -+ . $block->getUiId('form-field', $block->escapeHtmlAttr($element->getId())); - ?> - - <?php $block->checkFieldDisable() ?> -@@ -36,38 +33,38 @@ $fieldAttributes = $fieldId . ' class="' . $fieldClass . '" ' - $elementToggleCode = $element->getToggleCode() ? $element->getToggleCode() - : 'toggleValueElements(this, this.parentNode.parentNode.parentNode)'; - ?> --<?php if (!$element->getNoDisplay()): ?> -- <?php if ($element->getType() == 'hidden'): ?> -+<?php if (!$element->getNoDisplay()) :?> -+ <?php if ($element->getType() == 'hidden') :?> - <?= $element->getElementHtml() ?> -- <?php else: ?> -- <div<?= /* @escapeNotVerified */ $fieldAttributes ?> data-attribute-code="<?= $element->getHtmlId() ?>" -- data-apply-to="<?= $block->escapeHtml($this->helper('Magento\Framework\Json\Helper\Data')->jsonEncode( -+ <?php else :?> -+ <div<?= /* @noEscape */ $fieldAttributes ?> data-attribute-code="<?= $element->getHtmlId() ?>" -+ data-apply-to="<?= $block->escapeHtmlAttr($this->helper(Magento\Framework\Json\Helper\Data::class)->jsonEncode( - $element->hasEntityAttribute() ? $element->getEntityAttribute()->getApplyTo() : [] - ))?>" - > -- <?php if ($elementBeforeLabel): ?> -+ <?php if ($elementBeforeLabel) :?> - <?= $block->getElementHtml() ?> - <?= $element->getLabelHtml('', $block->getScopeLabel()) ?> -- <?= /* @escapeNotVerified */ $note ?> -- <?php else: ?> -+ <?= /* @noEscape */ $note ?> -+ <?php else :?> - <?= $element->getLabelHtml('', $block->getScopeLabel()) ?> - <div class="admin__field-control control"> -- <?= /* @escapeNotVerified */ ($addOn) ? '<div class="addon">' . $block->getElementHtml() . '</div>' : $block->getElementHtml() ?> -- <?= /* @escapeNotVerified */ $note ?> -+ <?= ($addOn) ? '<div class="addon">' . $block->getElementHtml() . '</div>' : $block->getElementHtml() ?> -+ <?= /* @noEscape */ $note ?> - </div> - <?php endif; ?> - <div class="field-service"> -- <?php if ($block->canDisplayUseDefault()): ?> -+ <?php if ($block->canDisplayUseDefault()) :?> - <label for="<?= $element->getHtmlId() ?>_default" class="choice use-default"> -- <input <?php if ($element->getReadonly()):?> disabled="disabled"<?php endif; ?> -+ <input <?php if ($element->getReadonly()) :?> disabled="disabled"<?php endif; ?> - type="checkbox" - name="use_default[]" - class="use-default-control" - id="<?= $element->getHtmlId() ?>_default" -- <?php if ($block->usedDefault()): ?> checked="checked"<?php endif; ?> -- onclick="<?= /* @escapeNotVerified */ $elementToggleCode ?>" -- value="<?= /* @escapeNotVerified */ $block->getAttributeCode() ?>"/> -- <span class="use-default-label"><?= /* @escapeNotVerified */ __('Use Default Value') ?></span> -+ <?php if ($block->usedDefault()) :?> checked="checked"<?php endif; ?> -+ onclick="<?= $block->escapeHtmlAttr($elementToggleCode) ?>" -+ value="<?= $block->escapeHtmlAttr($block->getAttributeCode()) ?>"/> -+ <span class="use-default-label"><?= $block->escapeHtml(__('Use Default Value')) ?></span> - </label> - <?php endif; ?> - </div> -diff --git a/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product.phtml b/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product.phtml -index ce4d8450f5e..9b9fff2cfc3 100644 ---- a/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product.phtml -+++ b/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product.phtml -@@ -3,9 +3,6 @@ - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -- --// @codingStandardsIgnoreFile -- - ?> - <?php - /** -diff --git a/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/attribute/form.phtml b/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/attribute/form.phtml -index 74cf8f5f3a7..e30b981ff36 100644 ---- a/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/attribute/form.phtml -+++ b/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/attribute/form.phtml -@@ -4,16 +4,14 @@ - * See COPYING.txt for license details. - */ - --// @codingStandardsIgnoreFile -- - ?> - <?php - /** - * @var $block \Magento\Backend\Block\Widget\Form\Container - */ - ?> --<?= /* @escapeNotVerified */ $block->getFormInitScripts() ?> --<div data-mage-init='{"floatingHeader": {}}' class="page-actions attribute-popup-actions" <?= /* @escapeNotVerified */ $block->getUiId('content-header') ?>> -+<?= /* @noEscape */ $block->getFormInitScripts() ?> -+<div data-mage-init='{"floatingHeader": {}}' class="page-actions attribute-popup-actions" <?= /* @noEscape */ $block->getUiId('content-header') ?>> - <?= $block->getButtonsHtml('header') ?> - </div> - -@@ -21,13 +19,13 @@ - <input name="form_key" type="hidden" value="<?= $block->escapeHtml($block->getFormKey()) ?>" /> - <?= $block->getChildHtml('form') ?> - </form> -- -- --<script> --require(['jquery', "mage/mage"], function(jQuery){ -- -- jQuery('#edit_form').mage('form').mage('validation', {validationUrl: '<?= /* @escapeNotVerified */ $block->getValidationUrl() ?>'}); -- --}); -+<script type="text/x-magento-init"> -+ { -+ "#edit_form": { -+ "Magento_Catalog/catalog/product/edit/attribute": { -+ "validationUrl": "<?= $block->escapeJs($block->escapeUrl($block->getValidationUrl())) ?>" -+ } -+ } -+ } - </script> --<?= /* @escapeNotVerified */ $block->getFormScripts() ?> -+<?= /* @noEscape */ $block->getFormScripts() ?> -diff --git a/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/attribute/js.phtml b/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/attribute/js.phtml -index 8a5f1919f78..f020eddc35d 100644 ---- a/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/attribute/js.phtml -+++ b/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/attribute/js.phtml -@@ -4,17 +4,17 @@ - * See COPYING.txt for license details. - */ - --// @codingStandardsIgnoreFile -- -+// phpcs:disable Magento2.Templates.ThisInTemplate.FoundThis - ?> - <script> - require([ - "jquery", - 'Magento_Ui/js/modal/alert', - 'Magento_Ui/js/modal/prompt', -+ 'uiRegistry', - "collapsable", - "prototype" --], function(jQuery, alert, prompt){ -+], function(jQuery, alert, prompt, registry){ - - function toggleApplyVisibility(select) { - if ($(select).value == 1) { -@@ -40,13 +40,21 @@ function getFrontTab() { - - function checkOptionsPanelVisibility(){ - if($('manage-options-panel')){ -- var panel = $('manage-options-panel').up('.fieldset'); -+ var panelId = 'manage-options-panel', -+ panel = $(panelId), -+ panelFieldSet = panel.up('.fieldset'), -+ activePanelClass = 'selected-type-options'; - - if($('frontend_input') && ($('frontend_input').value=='select' || $('frontend_input').value=='multiselect')){ -- panel.show(); -+ panelFieldSet.show(); -+ jQuery(panel).addClass(activePanelClass); -+ registry.get(panelId, function () { -+ jQuery('#' + panelId).trigger('render'); -+ }); - } - else { -- panel.hide(); -+ panelFieldSet.hide(); -+ jQuery(panel).removeClass(activePanelClass); - } - } - } -@@ -55,7 +63,7 @@ function bindAttributeInputType() - { - checkOptionsPanelVisibility(); - switchDefaultValueField(); -- if($('frontend_input') && ($('frontend_input').value=='select' || $('frontend_input').value=='multiselect' || $('frontend_input').value=='price')){ -+ if($('frontend_input') && ($('frontend_input').value=='boolean' || $('frontend_input').value=='select' || $('frontend_input').value=='multiselect' || $('frontend_input').value=='price')){ - if($('is_filterable') && !$('is_filterable').getAttribute('readonly')){ - $('is_filterable').disabled = false; - } -@@ -194,22 +202,22 @@ function switchDefaultValueField() - setRowVisibility('frontend_class', false); - break; - -- <?php foreach ($this->helper('Magento\Catalog\Helper\Data')->getAttributeHiddenFields() as $type => $fields): ?> -- case '<?= /* @escapeNotVerified */ $type ?>': -+ <?php foreach ($this->helper(Magento\Catalog\Helper\Data::class)->getAttributeHiddenFields() as $type => $fields) :?> -+ case '<?= $block->escapeJs($type) ?>': - var isFrontTabHidden = false; -- <?php foreach ($fields as $one): ?> -- <?php if ($one == '_front_fieldset'): ?> -+ <?php foreach ($fields as $one) :?> -+ <?php if ($one == '_front_fieldset') :?> - getFrontTab().hide(); - isFrontTabHidden = true; -- <?php elseif ($one == '_default_value'): ?> -+ <?php elseif ($one == '_default_value') :?> - defaultValueTextVisibility = - defaultValueTextareaVisibility = - defaultValueDateVisibility = - defaultValueYesnoVisibility = false; -- <?php elseif ($one == '_scope'): ?> -+ <?php elseif ($one == '_scope') :?> - scopeVisibility = false; -- <?php else: ?> -- setRowVisibility('<?= /* @escapeNotVerified */ $one ?>', false); -+ <?php else :?> -+ setRowVisibility('<?= $block->escapeJs($one) ?>', false); - <?php endif; ?> - <?php endforeach; ?> - -@@ -331,7 +339,7 @@ if ($('is_required')) { - - jQuery(function($) { - bindAttributeInputType(); -- // @todo: refactor collapsable component -+ // @todo: refactor collapsible component - $('.attribute-popup .collapse, [data-role="advanced_fieldset-content"]') - .collapsable() - .collapse('hide'); -diff --git a/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/attribute/labels.phtml b/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/attribute/labels.phtml -index f3d39257c26..1d5d251f00d 100644 ---- a/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/attribute/labels.phtml -+++ b/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/attribute/labels.phtml -@@ -4,15 +4,13 @@ - * See COPYING.txt for license details. - */ - --// @codingStandardsIgnoreFile -- - /** @var $block \Magento\Eav\Block\Adminhtml\Attribute\Edit\Options\Labels */ - ?> - - <div class="fieldset-wrapper admin__collapsible-block-wrapper opened" id="manage-titles-wrapper"> - <div class="fieldset-wrapper-title"> - <strong class="admin__collapsible-title" data-toggle="collapse" data-target="#manage-titles-content"> -- <span><?= /* @escapeNotVerified */ __('Manage Titles (Size, Color, etc.)') ?></span> -+ <span><?= $block->escapeHtml(__('Manage Titles (Size, Color, etc.)')) ?></span> - </strong> - </div> - <div class="fieldset-wrapper-content in collapse" id="manage-titles-content"> -@@ -21,17 +19,23 @@ - <table class="admin__control-table" id="attribute-labels-table"> - <thead> - <tr> -- <?php foreach ($block->getStores() as $_store): ?> -- <th class="col-store-view"><?= /* @escapeNotVerified */ $_store->getName() ?></th> -+ <?php foreach ($block->getStores() as $_store) :?> -+ <th class="col-store-view"><?= $block->escapeHtml($_store->getName()) ?></th> - <?php endforeach; ?> - </tr> - </thead> - <tbody> - <tr> - <?php $_labels = $block->getLabelValues() ?> -- <?php foreach ($block->getStores() as $_store): ?> -+ <?php foreach ($block->getStores() as $_store) :?> - <td class="col-store-view"> -- <input class="input-text<?php if ($_store->getId() == \Magento\Store\Model\Store::DEFAULT_STORE_ID): ?> required-option<?php endif; ?>" type="text" name="frontend_label[<?= /* @escapeNotVerified */ $_store->getId() ?>]" value="<?= $block->escapeHtml($_labels[$_store->getId()]) ?>"<?php if ($block->getReadOnly()):?> disabled="disabled"<?php endif;?>/> -+ <input class="input-text<?php if ($_store->getId() == \Magento\Store\Model\Store::DEFAULT_STORE_ID) :?> required-option<?php endif; ?>" -+ type="text" -+ name="frontend_label[<?= $block->escapeHtmlAttr($_store->getId()) ?>]" -+ value="<?= $block->escapeHtmlAttr($_labels[$_store->getId()]) ?>" -+ <?php if ($block->getReadOnly()) :?> -+ disabled="disabled" -+ <?php endif;?>/> - </td> - <?php endforeach; ?> - </tr> -diff --git a/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/attribute/options.phtml b/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/attribute/options.phtml -index f812a27f87a..e5f8a360c33 100644 ---- a/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/attribute/options.phtml -+++ b/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/attribute/options.phtml -@@ -4,8 +4,6 @@ - * See COPYING.txt for license details. - */ - --// @codingStandardsIgnoreFile -- - /** @var $block \Magento\Eav\Block\Adminhtml\Attribute\Edit\Options\Options */ - - $stores = $block->getStoresSortedBySortOrder(); -@@ -23,8 +21,8 @@ $stores = $block->getStoresSortedBySortOrder(); - <span><?= $block->escapeHtml(__('Is Default')) ?></span> - </th> - <?php -- foreach ($stores as $_store): ?> -- <th<?php if ($_store->getId() == \Magento\Store\Model\Store::DEFAULT_STORE_ID): ?> class="_required"<?php endif; ?>> -+ foreach ($stores as $_store) :?> -+ <th<?php if ($_store->getId() == \Magento\Store\Model\Store::DEFAULT_STORE_ID) :?> class="_required"<?php endif; ?>> - <span><?= $block->escapeHtml(__($_store->getName())) ?></span> - </th> - <?php endforeach; -@@ -43,7 +41,7 @@ $stores = $block->getStoresSortedBySortOrder(); - </tr> - <tr> - <th colspan="<?= (int) $storetotal ?>" class="col-actions-add"> -- <?php if (!$block->getReadOnly() && !$block->canManageOptionDefaultOnly()):?> -+ <?php if (!$block->getReadOnly() && !$block->canManageOptionDefaultOnly()) :?> - <button id="add_new_option_button" data-action="add_new_row" - title="<?= $block->escapeHtml(__('Add Option')) ?>" - type="button" class="action- scalable add"> -@@ -59,22 +57,22 @@ $stores = $block->getStoresSortedBySortOrder(); - <script id="row-template" type="text/x-magento-template"> - <tr <% if (data.rowClasses) { %>class="<%- data.rowClasses %>"<% } %>> - <td class="col-draggable"> -- <?php if (!$block->getReadOnly() && !$block->canManageOptionDefaultOnly()): ?> -+ <?php if (!$block->getReadOnly() && !$block->canManageOptionDefaultOnly()) :?> - <div data-role="draggable-handle" class="draggable-handle" - title="<?= $block->escapeHtml(__('Sort Option')) ?>"> - </div> - <?php endif; ?> -- <input data-role="order" type="hidden" name="option[order][<%- data.id %>]" value="<%- data.sort_order %>" <?php if ($block->getReadOnly() || $block->canManageOptionDefaultOnly()): ?> disabled="disabled"<?php endif; ?>/> -+ <input data-role="order" type="hidden" name="option[order][<%- data.id %>]" value="<%- data.sort_order %>" <?php if ($block->getReadOnly() || $block->canManageOptionDefaultOnly()) :?> disabled="disabled"<?php endif; ?>/> - </td> - <td class="col-default control-table-actions-cell"> -- <input class="input-radio" type="<%- data.intype %>" name="default[]" value="<%- data.id %>" <%- data.checked %><?php if ($block->getReadOnly()):?>disabled="disabled"<?php endif;?>/> -+ <input class="input-radio" type="<%- data.intype %>" name="default[]" value="<%- data.id %>" <%- data.checked %><?php if ($block->getReadOnly()) :?>disabled="disabled"<?php endif;?>/> - </td> -- <?php foreach ($stores as $_store): ?> -- <td class="col-<%- data.id %>"><input name="option[value][<%- data.id %>][<?= (int) $_store->getId() ?>]" value="<%- data.store<?= /* @noEscape */ (int) $_store->getId() ?> %>" class="input-text<?php if ($_store->getId() == \Magento\Store\Model\Store::DEFAULT_STORE_ID): ?> required-option required-unique<?php endif; ?>" type="text" <?php if ($block->getReadOnly() || $block->canManageOptionDefaultOnly()):?> disabled="disabled"<?php endif;?>/></td> -+ <?php foreach ($stores as $_store) :?> -+ <td class="col-<%- data.id %>"><input name="option[value][<%- data.id %>][<?= (int) $_store->getId() ?>]" value="<%- data.store<?= /* @noEscape */ (int) $_store->getId() ?> %>" class="input-text<?php if ($_store->getId() == \Magento\Store\Model\Store::DEFAULT_STORE_ID) :?> required-option required-unique<?php endif; ?>" type="text" <?php if ($block->getReadOnly() || $block->canManageOptionDefaultOnly()) :?> disabled="disabled"<?php endif;?>/></td> - <?php endforeach; ?> - <td id="delete_button_container_<%- data.id %>" class="col-delete"> - <input type="hidden" class="delete-flag" name="option[delete][<%- data.id %>]" value="" /> -- <?php if (!$block->getReadOnly() && !$block->canManageOptionDefaultOnly()):?> -+ <?php if (!$block->getReadOnly() && !$block->canManageOptionDefaultOnly()) :?> - <button id="delete_button_<%- data.id %>" title="<?= $block->escapeHtml(__('Delete')) ?>" type="button" - class="action- scalable delete delete-option" - > -@@ -86,9 +84,9 @@ $stores = $block->getStoresSortedBySortOrder(); - </script> - <?php - $values = []; -- foreach($block->getOptionValues() as $value) { -+ foreach ($block->getOptionValues() as $value) { - $value = $value->getData(); -- $values[] = is_array($value) ? array_map(function($str) { -+ $values[] = is_array($value) ? array_map(function ($str) { - return htmlspecialchars_decode($str, ENT_QUOTES); - }, $value) : $value; - } -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 54b945b48c1..dd1009cc5e0 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 -@@ -4,8 +4,7 @@ - * See COPYING.txt for license details. - */ - --// @codingStandardsIgnoreFile -- -+/** @var $block Magento\Catalog\Block\Adminhtml\Product\Attribute\Set\Main */ - ?> - <div class="attribute-set"> - -@@ -31,11 +30,11 @@ - </div> - <div class="attribute-set-col fieldset-wrapper"> - <div class="fieldset-wrapper-title"> -- <span class="title"><?= /* @escapeNotVerified */ __('Groups') ?></span> -+ <span class="title"><?= $block->escapeHtml(__('Groups')) ?></span> - </div> -- <?php if (!$block->getIsReadOnly()): ?> -- <?= /* @escapeNotVerified */ $block->getAddGroupButton() ?> <?= /* @escapeNotVerified */ $block->getDeleteGroupButton() ?> -- <p class="note-block"><?= /* @escapeNotVerified */ __('Double click on a group to rename it.') ?></p> -+ <?php if (!$block->getIsReadOnly()) :?> -+ <?= /* @noEscape */ $block->getAddGroupButton() ?> <?= /* @noEscape */ $block->getDeleteGroupButton() ?> -+ <p class="note-block"><?= $block->escapeHtml(__('Double click on a group to rename it.')) ?></p> - <?php endif; ?> - - <?= $block->getSetsFilterHtml() ?> -@@ -43,7 +42,7 @@ - </div> - <div class="attribute-set-col fieldset-wrapper"> - <div class="fieldset-wrapper-title"> -- <span class="title"><?= /* @escapeNotVerified */ __('Unassigned Attributes') ?></span> -+ <span class="title"><?= $block->escapeHtml(__('Unassigned Attributes')) ?></span> - </div> - <div id="tree-div2" class="attribute-set-tree"></div> - <script id="ie-deferred-loader" defer="defer" src="//:"></script> -@@ -58,8 +57,8 @@ - ], function(jQuery, prompt, alert){ - - //<![CDATA[ -- var allowDragAndDrop = <?= /* @escapeNotVerified */ ($block->getIsReadOnly() ? 'false' : 'true') ?>; -- var canEditGroups = <?= /* @escapeNotVerified */ ($block->getIsReadOnly() ? 'false' : 'true') ?>; -+ var allowDragAndDrop = <?= ($block->getIsReadOnly() ? 'false' : 'true') ?>; -+ var canEditGroups = <?= ($block->getIsReadOnly() ? 'false' : 'true') ?>; - - var TreePanels = function() { - // shorthand -@@ -80,13 +79,13 @@ - // set the root node - this.root = new Ext.tree.TreeNode({ - text: 'ROOT', -- allowDrug:false, -+ allowDrag:false, - allowDrop:true, - id:'1' - }); - - tree.setRootNode(this.root); -- buildCategoryTree(this.root, <?= /* @escapeNotVerified */ $block->getGroupTreeJson() ?>); -+ buildCategoryTree(this.root, <?= /* @noEscape */ $block->getGroupTreeJson() ?>); - // render the tree - tree.render(); - this.root.expand(false, false); -@@ -94,7 +93,7 @@ - - this.ge = new Ext.tree.TreeEditor(tree, { - allowBlank:false, -- blankText:'<?= /* @escapeNotVerified */ __('A name is required.') ?>', -+ blankText:'<?= $block->escapeJs(__('A name is required.')) ?>', - selectOnFocus:true, - cls:'folder' - }); -@@ -125,7 +124,7 @@ - id:'free' - }); - tree2.setRootNode(this.root2); -- buildCategoryTree(this.root2, <?= /* @escapeNotVerified */ $block->getAttributeTreeJson() ?>); -+ buildCategoryTree(this.root2, <?= /* @noEscape */ $block->getAttributeTreeJson() ?>); - - this.root2.addListener('beforeinsert', editSet.rightBeforeInsert); - this.root2.addListener('beforeappend', editSet.rightBeforeAppend); -@@ -188,20 +187,36 @@ - for( j in config[i].children ) { - if(config[i].children[j].id) { - newNode = new Ext.tree.TreeNode(config[i].children[j]); -- node.appendChild(newNode); -- newNode.addListener('click', editSet.unregister); -+ -+ if (typeof newNode.ui.onTextChange === 'function') { -+ newNode.ui.onTextChange = function (_3, _4, _5) { -+ if (this.rendered) { -+ this.textNode.innerText = _4; -+ } -+ } -+ } - } -+ node.appendChild(newNode); -+ newNode.addListener('click', editSet.unregister); - } - } - } - } - } - -- editSet = function() { -- return { -- register : function(node) { -- editSet.currentNode = node; -- }, -+ -+ editSet = function () { -+ return { -+ register: function (node) { -+ editSet.currentNode = node; -+ if (typeof node.ui.onTextChange === 'function') { -+ node.ui.onTextChange = function (_3, _4, _5) { -+ if (this.rendered) { -+ this.textNode.innerText = _4; -+ } -+ } -+ } -+ }, - - unregister : function() { - editSet.currentNode = false; -@@ -264,8 +279,8 @@ - - addGroup : function() { - prompt({ -- title: "<?= /* @escapeNotVerified */ __('Add New Group') ?>", -- content: "<?= /* @escapeNotVerified */ __('Please enter a new group name.') ?>", -+ title: "<?= $block->escapeJs($block->escapeHtml(__('Add New Group'))) ?>", -+ content: "<?= $block->escapeJs($block->escapeHtml(__('Please enter a new group name.'))) ?>", - value: "", - validation: true, - validationRules: ['required-entry'], -@@ -293,6 +308,14 @@ - allowDrag : true - }); - -+ if (typeof newNode.ui.onTextChange === 'function') { -+ newNode.ui.onTextChange = function (_3, _4, _5) { -+ if (this.rendered) { -+ this.textNode.innerText = _4; -+ } -+ } -+ } -+ - TreePanels.root.appendChild(newNode); - newNode.addListener('beforemove', editSet.groupBeforeMove); - newNode.addListener('beforeinsert', editSet.groupBeforeInsert); -@@ -322,7 +345,7 @@ - } - for (var i=0; i < TreePanels.root.childNodes.length; i++) { - if (TreePanels.root.childNodes[i].text.toLowerCase() == name.toLowerCase() && TreePanels.root.childNodes[i].id != exceptNodeId) { -- errorText = '<?= /* @escapeNotVerified */ __('An attribute group named "/name/" already exists.') ?>'; -+ errorText = '<?= $block->escapeJs(__('An attribute group named "/name/" already exists.')) ?>'; - alert({ - content: errorText.replace("/name/",name) - }); -@@ -350,7 +373,7 @@ - editSet.req.form_key = FORM_KEY; - } - var req = {data : Ext.util.JSON.encode(editSet.req)}; -- var con = new Ext.lib.Ajax.request('POST', '<?= /* @escapeNotVerified */ $block->getMoveUrl() ?>', {success:editSet.success,failure:editSet.failure}, req); -+ var con = new Ext.lib.Ajax.request('POST', '<?= $block->escapeJs($block->escapeUrl($block->getMoveUrl())) ?>', {success:editSet.success,failure:editSet.failure}, req); - }, - - success : function(o) { -@@ -425,7 +448,7 @@ - rightRemove : function(tree, nodeThis, node) { - if( nodeThis.firstChild == null && node.id != 'empty' ) { - var newNode = new Ext.tree.TreeNode({ -- text : '<?= /* @escapeNotVerified */ __('Empty') ?>', -+ text : '<?= $block->escapeJs(__('Empty')) ?>', - id : 'empty', - cls : 'folder', - is_user_defined : 1, -diff --git a/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/attribute/set/main/tree/attribute.phtml b/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/attribute/set/main/tree/attribute.phtml -index 223b3e9888e..75f04eae821 100644 ---- a/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/attribute/set/main/tree/attribute.phtml -+++ b/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/attribute/set/main/tree/attribute.phtml -@@ -2,4 +2,4 @@ - /** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. -- */; -+ */ -diff --git a/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/attribute/set/toolbar/add.phtml b/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/attribute/set/toolbar/add.phtml -index c1af14389fe..227ed4be81f 100644 ---- a/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/attribute/set/toolbar/add.phtml -+++ b/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/attribute/set/toolbar/add.phtml -@@ -8,7 +8,7 @@ - <script> - require(['jquery', "mage/mage"], function(jQuery){ - -- jQuery('#<?= /* @escapeNotVerified */ $block->getFormId() ?>').mage('form').mage('validation'); -+ jQuery('#<?= $block->escapeJs($block->getFormId()) ?>').mage('form').mage('validation'); - - }); - </script> -diff --git a/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/attribute/set/toolbar/main.phtml b/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/attribute/set/toolbar/main.phtml -index 902c6932f0a..c0928f4723b 100644 ---- a/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/attribute/set/toolbar/main.phtml -+++ b/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/attribute/set/toolbar/main.phtml -@@ -3,8 +3,5 @@ - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -- --// @codingStandardsIgnoreFile -- - ?> - <?= $block->getChildHtml('grid') ?> -diff --git a/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/composite/configure.phtml b/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/composite/configure.phtml -index 75027d5e043..32466a1dfa9 100644 ---- a/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/composite/configure.phtml -+++ b/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/composite/configure.phtml -@@ -3,10 +3,7 @@ - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -- --// @codingStandardsIgnoreFile -- -- ?> -+?> - <div id="product_composite_configure" class="product-configure-popup" style="display:none;"> - <iframe name="product_composite_configure_iframe" id="product_composite_configure_iframe" style="width:0; height:0; border:0px solid #fff; position:absolute; top:-1000px; left:-1000px" onload="window.productConfigure && productConfigure.onLoadIFrame()"></iframe> - <form action="" method="post" id="product_composite_configure_form" enctype="multipart/form-data" onsubmit="productConfigure.onConfirmBtn(); return false;" target="product_composite_configure_iframe"> -@@ -19,7 +16,7 @@ - <div id="product_composite_configure_form_confirmed" style="display:none;"></div> - </div> - <input type="hidden" name="as_js_varname" value="iFrameResponse" /> -- <input type="hidden" name="form_key" value="<?= /* @escapeNotVerified */ $block->getFormKey() ?>" /> -+ <input type="hidden" name="form_key" value="<?= $block->escapeHtmlAttr($block->getFormKey()) ?>" /> - </form> - <div id="product_composite_configure_confirmed" style="display:none;"></div> - -diff --git a/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/composite/fieldset/options.phtml b/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/composite/fieldset/options.phtml -index acc80fa6ea6..6a83ece3304 100644 ---- a/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/composite/fieldset/options.phtml -+++ b/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/composite/fieldset/options.phtml -@@ -3,24 +3,22 @@ - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -- --// @codingStandardsIgnoreFile -- - ?> - - <?php /* @var $block \Magento\Catalog\Block\Adminhtml\Product\Composite\Fieldset\Options */ ?> - <?php $options = $block->decorateArray($block->getOptions()); ?> --<?php if (count($options)): ?> -+<?php if (count($options)) :?> - --<?= $block->getChildHtml('options_js') ?> -+ <?= $block->getChildHtml('options_js') ?> - --<fieldset id="product_composite_configure_fields_options" class="fieldset admin__fieldset <?= $block->getIsLastFieldset() ? 'last-fieldset' : '' ?>"> -- <legend class="legend admin__legend"> -- <span><?= /* @escapeNotVerified */ __('Custom Options') ?></span> -- </legend><br> -- <?php foreach ($options as $option): ?> -- <?= $block->getOptionHtml($option) ?> -- <?php endforeach;?> --</fieldset> -+ <fieldset id="product_composite_configure_fields_options" -+ class="fieldset admin__fieldset <?= $block->getIsLastFieldset() ? 'last-fieldset' : '' ?>"> -+ <legend class="legend admin__legend"> -+ <span><?= $block->escapeHtml(__('Custom Options')) ?></span> -+ </legend><br> -+ <?php foreach ($options as $option) :?> -+ <?= $block->getOptionHtml($option) ?> -+ <?php endforeach;?> -+ </fieldset> - - <?php endif; ?> -diff --git a/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/composite/fieldset/options/type/date.phtml b/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/composite/fieldset/options/type/date.phtml -index 30c05c2ec68..8adffb75218 100644 ---- a/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/composite/fieldset/options/type/date.phtml -+++ b/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/composite/fieldset/options/type/date.phtml -@@ -3,82 +3,82 @@ - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -- --// @codingStandardsIgnoreFile -- - ?> - <?php /* @var $block \Magento\Catalog\Block\Product\View\Options\Type\Date */ ?> - <?php $_option = $block->getOption(); ?> --<?php $_optionId = $_option->getId(); ?> --<div class="admin__field field<?php if ($_option->getIsRequire()) echo ' required _required' ?>"> -- <label class="label admin__field-label"> -- <?= $block->escapeHtml($_option->getTitle()) ?> -- <?= /* @escapeNotVerified */ $block->getFormattedPrice() ?> -- </label> -- <div class="admin__field-control control"> -+<?php $_optionId = (int)$_option->getId(); ?> -+<div class="admin__field field<?= $_option->getIsRequire() ? ' required _required' : '' ?>"> -+ <label class="label admin__field-label"> -+ <?= $block->escapeHtml($_option->getTitle()) ?> -+ <?= /* @noEscape */ $block->getFormattedPrice() ?> -+ </label> -+ <div class="admin__field-control control"> - -- <?php if ($_option->getType() == \Magento\Catalog\Api\Data\ProductCustomOptionInterface::OPTION_TYPE_DATE_TIME -- || $_option->getType() == \Magento\Catalog\Api\Data\ProductCustomOptionInterface::OPTION_TYPE_DATE): ?> -+ <?php if ($_option->getType() == \Magento\Catalog\Api\Data\ProductCustomOptionInterface::OPTION_TYPE_DATE_TIME -+ || $_option->getType() == \Magento\Catalog\Api\Data\ProductCustomOptionInterface::OPTION_TYPE_DATE) :?> - -- <?= $block->getDateHtml() ?> -+ <?= $block->getDateHtml() ?> - -- <?php if (!$block->useCalendar()): ?> -- <script> --require([ -- "prototype", -- "Magento_Catalog/catalog/product/composite/configure" --], function(){ -+ <?php if (!$block->useCalendar()) :?> -+ <script> -+ require([ -+ "prototype", -+ "Magento_Catalog/catalog/product/composite/configure" -+ ], function(){ - -- window.dateOption = productConfigure.opConfig.dateOption; -- Event.observe('options_<?= /* @escapeNotVerified */ $_optionId ?>_month', 'change', dateOption.reloadMonth.bind(dateOption)); -- Event.observe('options_<?= /* @escapeNotVerified */ $_optionId ?>_year', 'change', dateOption.reloadMonth.bind(dateOption)); --}); --</script> -- <?php endif; ?> -+ window.dateOption = productConfigure.opConfig.dateOption; -+ Event.observe('options_<?= /* @noEscape */ $_optionId ?>_month', 'change', dateOption.reloadMonth.bind(dateOption)); -+ Event.observe('options_<?= /* @noEscape */ $_optionId ?>_year', 'change', dateOption.reloadMonth.bind(dateOption)); -+ }); -+ </script> -+ <?php endif; ?> - -- <?php endif; ?> -+ <?php endif; ?> - -- <?php if ($_option->getType() == \Magento\Catalog\Api\Data\ProductCustomOptionInterface::OPTION_TYPE_DATE_TIME -- || $_option->getType() == \Magento\Catalog\Api\Data\ProductCustomOptionInterface::OPTION_TYPE_TIME): ?> -- <span class="time-picker"><?= $block->getTimeHtml() ?></span> -- <?php endif; ?> -+ <?php if ($_option->getType() == \Magento\Catalog\Api\Data\ProductCustomOptionInterface::OPTION_TYPE_DATE_TIME -+ || $_option->getType() == \Magento\Catalog\Api\Data\ProductCustomOptionInterface::OPTION_TYPE_TIME) :?> -+ <span class="time-picker"><?= $block->getTimeHtml() ?></span> -+ <?php endif; ?> - -- <input type="hidden" name="validate_datetime_<?= /* @escapeNotVerified */ $_optionId ?>" class="validate-datetime-<?= /* @escapeNotVerified */ $_optionId ?>" value="" /> -- <script> --require([ -- "jquery", -- "mage/backend/validation" --], function(jQuery){ -+ <input type="hidden" -+ name="validate_datetime_<?= /* @noEscape */ $_optionId ?>" -+ class="validate-datetime-<?= /* @noEscape */ $_optionId ?>" -+ value="" /> -+ <script> -+ require([ -+ "jquery", -+ "mage/backend/validation" -+ ], function(jQuery){ - -- //<![CDATA[ --<?php if ($_option->getIsRequire()): ?> -- jQuery.validator.addMethod('validate-datetime-<?= /* @escapeNotVerified */ $_optionId ?>', function(v) { -- var dateTimeParts = jQuery('.datetime-picker[id^="options_<?= /* @escapeNotVerified */ $_optionId ?>"]'); -- for (var i=0; i < dateTimeParts.length; i++) { -- if (dateTimeParts[i].value == "") return false; -- } -- return true; -- }, '<?= $block->escapeJs(__('This is a required option.')) ?>'); --<?php else: ?> -- jQuery.validator.addMethod('validate-datetime-<?= /* @escapeNotVerified */ $_optionId ?>', function(v) { -- var dateTimeParts = jQuery('.datetime-picker[id^="options_<?= /* @escapeNotVerified */ $_optionId ?>"]'); -- var hasWithValue = false, hasWithNoValue = false; -- var pattern = /day_part$/i; -- for (var i=0; i < dateTimeParts.length; i++) { -- if (! pattern.test(dateTimeParts[i].id)) { -- if (dateTimeParts[i].value === "") { -- hasWithValue = true; -- } else { -- hasWithNoValue = true; -+ //<![CDATA[ -+ <?php if ($_option->getIsRequire()) :?> -+ jQuery.validator.addMethod('validate-datetime-<?= /* @noEscape */ $_optionId ?>', function(v) { -+ var dateTimeParts = jQuery('.datetime-picker[id^="options_<?= /* @noEscape */ $_optionId ?>"]'); -+ for (var i=0; i < dateTimeParts.length; i++) { -+ if (dateTimeParts[i].value == "") return false; - } -- } -- } -- return hasWithValue ^ hasWithNoValue; -- }, '<?= $block->escapeJs(__('The field isn\'t complete.')) ?>'); --<?php endif; ?> -- //]]> -- --}); --</script> -- </div> -+ return true; -+ }, '<?= $block->escapeJs(__('This is a required option.')) ?>'); -+ <?php else :?> -+ jQuery.validator.addMethod('validate-datetime-<?= /* @noEscape */ $_optionId ?>', function(v) { -+ var dateTimeParts = jQuery('.datetime-picker[id^="options_<?= /* @noEscape */ $_optionId ?>"]'); -+ var hasWithValue = false, hasWithNoValue = false; -+ var pattern = /day_part$/i; -+ for (var i=0; i < dateTimeParts.length; i++) { -+ if (! pattern.test(dateTimeParts[i].id)) { -+ if (dateTimeParts[i].value === "") { -+ hasWithValue = true; -+ } else { -+ hasWithNoValue = true; -+ } -+ } -+ } -+ return hasWithValue ^ hasWithNoValue; -+ }, '<?= $block->escapeJs(__('The field isn\'t complete.')) ?>'); -+ <?php endif; ?> -+ //]]> -+ -+ }); -+ </script> -+ </div> - </div> -diff --git a/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/composite/fieldset/options/type/file.phtml b/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/composite/fieldset/options/type/file.phtml -index 4ad7a95c919..da0b3b36d56 100644 ---- a/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/composite/fieldset/options/type/file.phtml -+++ b/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/composite/fieldset/options/type/file.phtml -@@ -3,15 +3,12 @@ - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -- --// @codingStandardsIgnoreFile -- - ?> - <?php /* @var $block \Magento\Catalog\Block\Product\View\Options\Type\File */ ?> - <?php $_option = $block->getOption(); ?> - <?php $_fileInfo = $block->getFileInfo(); ?> - <?php $_fileExists = $_fileInfo->hasData() ? true : false; ?> --<?php $_fileName = 'options_' . $_option->getId() . '_file'; ?> -+<?php $_fileName = 'options_' . (int)$_option->getId() . '_file'; ?> - <?php $_fieldNameAction = $_fileName . '_action'; ?> - <?php $_fieldValueAction = $_fileExists ? 'save_old' : 'save_new'; ?> - <?php $_fileNamed = $_fileName . '_name'; ?> -@@ -21,11 +18,11 @@ - require(['prototype'], function(){ - - //<![CDATA[ -- opFile<?= /* @escapeNotVerified */ $_rand ?> = { -+ opFile<?= /* @noEscape */ $_rand ?> = { - initializeFile: function(inputBox) { -- this.inputFile = inputBox.select('input[name="<?= /* @escapeNotVerified */ $_fileName ?>"]')[0]; -- this.inputFileAction = inputBox.select('input[name="<?= /* @escapeNotVerified */ $_fieldNameAction ?>"]')[0]; -- this.fileNameBox = inputBox.up('dd').select('.<?= /* @escapeNotVerified */ $_fileNamed ?>')[0]; -+ this.inputFile = inputBox.select('input[name="<?= /* @noEscape */ $_fileName ?>"]')[0]; -+ this.inputFileAction = inputBox.select('input[name="<?= /* @noEscape */ $_fieldNameAction ?>"]')[0]; -+ this.fileNameBox = inputBox.up('dd').select('.<?= /* @noEscape */ $_fileNamed ?>')[0]; - }, - - toggleFileChange: function(inputBox) { -@@ -62,42 +59,42 @@ require(['prototype'], function(){ - }); - </script> - --<div class="admin__field <?php if ($_option->getIsRequire()) echo ' required _required' ?>"> -+<div class="admin__field <?= $_option->getIsRequire() ? ' required _required' : '' ?>"> - <label class="admin__field-label label"> - <?= $block->escapeHtml($_option->getTitle()) ?> -- <?= /* @escapeNotVerified */ $block->getFormattedPrice() ?> -+ <?= /* @noEscape */ $block->getFormattedPrice() ?> - </label> - <div class="admin__field-control control"> -- <?php if ($_fileExists): ?> -+ <?php if ($_fileExists) :?> - <span class="<?= /* @noEscape */ $_fileNamed ?>"><?= $block->escapeHtml($_fileInfo->getTitle()) ?></span> -- <a href="javascript:void(0)" class="label" onclick="opFile<?= /* @escapeNotVerified */ $_rand ?>.toggleFileChange($(this).next('.input-box'))"> -- <?= /* @escapeNotVerified */ __('Change') ?> -+ <a href="javascript:void(0)" class="label" onclick="opFile<?= /* @noEscape */ $_rand ?>.toggleFileChange($(this).next('.input-box'))"> -+ <?= $block->escapeHtml(__('Change')) ?> - </a>  -- <?php if (!$_option->getIsRequire()): ?> -- <input type="checkbox" onclick="opFile<?= /* @escapeNotVerified */ $_rand ?>.toggleFileDelete($(this), $(this).next('.input-box'))" price="<?= /* @escapeNotVerified */ $block->getCurrencyPrice($_option->getPrice(true)) ?>"/> -- <span class="label"><?= /* @escapeNotVerified */ __('Delete') ?></span> -+ <?php if (!$_option->getIsRequire()) :?> -+ <input type="checkbox" onclick="opFile<?= /* @noEscape */ $_rand ?>.toggleFileDelete($(this), $(this).next('.input-box'))" price="<?= $block->escapeHtmlAttr($block->getCurrencyPrice($_option->getPrice(true))) ?>"/> -+ <span class="label"><?= $block->escapeHtml(__('Delete')) ?></span> - <?php endif; ?> - <?php endif; ?> - <div class="input-box" <?= $_fileExists ? 'style="display:none"' : '' ?>> - <!-- ToDo UI: add appropriate file class when z-index issue in ui dialog will be resolved --> -- <input type="file" name="<?= /* @noEscape */ $_fileName ?>" class="product-custom-option<?= $_option->getIsRequire() ? ' required-entry' : '' ?>" price="<?= /* @escapeNotVerified */ $block->getCurrencyPrice($_option->getPrice(true)) ?>" <?= $_fileExists ? 'disabled="disabled"' : '' ?>/> -- <input type="hidden" name="<?= /* @escapeNotVerified */ $_fieldNameAction ?>" value="<?= /* @escapeNotVerified */ $_fieldValueAction ?>" /> -+ <input type="file" name="<?= /* @noEscape */ $_fileName ?>" class="product-custom-option<?= $_option->getIsRequire() ? ' required-entry' : '' ?>" price="<?= $block->escapeHtmlAttr($block->getCurrencyPrice($_option->getPrice(true))) ?>" <?= $_fileExists ? 'disabled="disabled"' : '' ?>/> -+ <input type="hidden" name="<?= /* @noEscape */ $_fieldNameAction ?>" value="<?= /* @noEscape */ $_fieldValueAction ?>" /> - -- <?php if ($_option->getFileExtension()): ?> -+ <?php if ($_option->getFileExtension()) :?> - <div class="admin__field-note"> -- <span><?= /* @escapeNotVerified */ __('Compatible file extensions to upload') ?>: <strong><?= /* @escapeNotVerified */ $_option->getFileExtension() ?></strong></span> -+ <span><?= $block->escapeHtml(__('Compatible file extensions to upload')) ?>: <strong><?= $block->escapeHtml($_option->getFileExtension()) ?></strong></span> - </div> - <?php endif; ?> - -- <?php if ($_option->getImageSizeX() > 0): ?> -+ <?php if ($_option->getImageSizeX() > 0) :?> - <div class="admin__field-note"> -- <span><?= /* @escapeNotVerified */ __('Maximum image width') ?>: <strong><?= /* @escapeNotVerified */ $_option->getImageSizeX() ?> <?= /* @escapeNotVerified */ __('px.') ?></strong></span> -+ <span><?= $block->escapeHtml(__('Maximum image width')) ?>: <strong><?= (int)$_option->getImageSizeX() ?> <?= $block->escapeHtml(__('px.')) ?></strong></span> - </div> - <?php endif; ?> - -- <?php if ($_option->getImageSizeY() > 0): ?> -+ <?php if ($_option->getImageSizeY() > 0) :?> - <div class="admin__field-note"> -- <span><?= /* @escapeNotVerified */ __('Maximum image height') ?>: <strong><?= /* @escapeNotVerified */ $_option->getImageSizeY() ?> <?= /* @escapeNotVerified */ __('px.') ?></strong></span> -+ <span><?= $block->escapeHtml(__('Maximum image height')) ?>: <strong><?= (int)$_option->getImageSizeY() ?> <?= $block->escapeHtml(__('px.')) ?></strong></span> - </div> - <?php endif; ?> - </div> -diff --git a/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/composite/fieldset/options/type/select.phtml b/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/composite/fieldset/options/type/select.phtml -index af09bbe0acd..2218ce5d296 100644 ---- a/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/composite/fieldset/options/type/select.phtml -+++ b/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/composite/fieldset/options/type/select.phtml -@@ -3,21 +3,18 @@ - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -- --// @codingStandardsIgnoreFile -- - ?> - <?php /* @var $block \Magento\Catalog\Block\Product\View\Options\Type\Select */ ?> - <?php $_option = $block->getOption(); ?> --<div class="admin__field field<?php if ($_option->getIsRequire()) echo ' required _required' ?>"> -+<div class="admin__field field<?= $_option->getIsRequire() ? ' required _required' : '' ?>"> - <label class="label admin__field-label"> - <span><?= $block->escapeHtml($_option->getTitle()) ?></span> - </label> - <div class="control admin__field-control"> - <?= $block->getValuesHtml() ?> -- <?php if ($_option->getIsRequire()): ?> -- <?php if ($_option->getType() == \Magento\Catalog\Api\Data\ProductCustomOptionInterface::OPTION_TYPE_RADIO || $_option->getType() == \Magento\Catalog\Api\Data\ProductCustomOptionInterface::OPTION_TYPE_CHECKBOX): ?> -- <span id="options-<?= /* @escapeNotVerified */ $_option->getId() ?>-container"></span> -+ <?php if ($_option->getIsRequire()) :?> -+ <?php if ($_option->getType() == \Magento\Catalog\Api\Data\ProductCustomOptionInterface::OPTION_TYPE_RADIO || $_option->getType() == \Magento\Catalog\Api\Data\ProductCustomOptionInterface::OPTION_TYPE_CHECKBOX) :?> -+ <span id="options-<?= $block->escapeHtmlAttr($_option->getId()) ?>-container"></span> - <?php endif; ?> - <?php endif;?> - </div> -diff --git a/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/composite/fieldset/options/type/text.phtml b/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/composite/fieldset/options/type/text.phtml -index 11fba22ea81..d1a019911d5 100644 ---- a/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/composite/fieldset/options/type/text.phtml -+++ b/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/composite/fieldset/options/type/text.phtml -@@ -3,26 +3,33 @@ - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -- --// @codingStandardsIgnoreFile -- - ?> - <?php /* @var $block \Magento\Catalog\Block\Product\View\Options\Type\Text */ ?> - <?php $_option = $block->getOption(); ?> --<div class="field admin__field<?php if ($_option->getIsRequire()) echo ' required _required' ?>"> -+<div class="field admin__field<?= $_option->getIsRequire() ? ' required _required' : '' ?>"> - <label class="admin__field-label label"> - <?= $block->escapeHtml($_option->getTitle()) ?> -- <?= /* @escapeNotVerified */ $block->getFormattedPrice() ?> -+ <?= /* @noEscape */ $block->getFormattedPrice() ?> - </label> - <div class="control admin__field-control"> -- <?php if ($_option->getType() == \Magento\Catalog\Api\Data\ProductCustomOptionInterface::OPTION_TYPE_FIELD): ?> -- <input type="text" id="options_<?= /* @escapeNotVerified */ $_option->getId() ?>_text" class="input-text admin__control-text <?= $_option->getIsRequire() ? ' required-entry' : '' ?> <?= /* @escapeNotVerified */ $_option->getMaxCharacters() ? ' validate-length maximum-length-' . $_option->getMaxCharacters() : '' ?> product-custom-option" name="options[<?= /* @escapeNotVerified */ $_option->getId() ?>]" value="<?= $block->escapeHtml($block->getDefaultValue()) ?>" price="<?= /* @escapeNotVerified */ $block->getCurrencyPrice($_option->getPrice(true)) ?>" /> -- <?php elseif ($_option->getType() == \Magento\Catalog\Api\Data\ProductCustomOptionInterface::OPTION_TYPE_AREA): ?> -- <textarea id="options_<?= /* @escapeNotVerified */ $_option->getId() ?>_text" class="admin__control-textarea <?= $_option->getIsRequire() ? ' required-entry' : '' ?> <?= /* @escapeNotVerified */ $_option->getMaxCharacters() ? ' validate-length maximum-length-' . $_option->getMaxCharacters() : '' ?> product-custom-option" name="options[<?= /* @escapeNotVerified */ $_option->getId() ?>]" rows="5" cols="25" price="<?= /* @escapeNotVerified */ $block->getCurrencyPrice($_option->getPrice(true)) ?>"><?= $block->escapeHtml($block->getDefaultValue()) ?></textarea> -+ <?php if ($_option->getType() == \Magento\Catalog\Api\Data\ProductCustomOptionInterface::OPTION_TYPE_FIELD) :?> -+ <input type="text" -+ id="options_<?= $block->escapeHtmlAttr($_option->getId()) ?>_text" -+ class="input-text admin__control-text <?= $_option->getIsRequire() ? ' required-entry' : '' ?> <?= $_option->getMaxCharacters() ? ' validate-length maximum-length-' . (int) $_option->getMaxCharacters() : '' ?> product-custom-option" -+ name="options[<?= $block->escapeHtmlAttr($_option->getId()) ?>]" -+ value="<?= $block->escapeHtmlAttr($block->getDefaultValue()) ?>" -+ price="<?= $block->escapeHtmlAttr($block->getCurrencyPrice($_option->getPrice(true))) ?>" /> -+ <?php elseif ($_option->getType() == \Magento\Catalog\Api\Data\ProductCustomOptionInterface::OPTION_TYPE_AREA) :?> -+ <textarea id="options_<?= $block->escapeHtmlAttr($_option->getId()) ?>_text" -+ class="admin__control-textarea <?= $_option->getIsRequire() ? ' required-entry' : '' ?> <?= $_option->getMaxCharacters() ? ' validate-length maximum-length-' . (int) $_option->getMaxCharacters() : '' ?> product-custom-option" -+ name="options[<?= $block->escapeHtmlAttr($_option->getId()) ?>]" -+ rows="5" -+ cols="25" -+ price="<?= $block->escapeHtmlAttr($block->getCurrencyPrice($_option->getPrice(true))) ?>"><?= $block->escapeHtml($block->getDefaultValue()) ?></textarea> - <?php endif;?> - -- <?php if ($_option->getMaxCharacters()): ?> -- <p class="note"><?= /* @escapeNotVerified */ __('Maximum number of characters:') ?> <strong><?= /* @escapeNotVerified */ $_option->getMaxCharacters() ?></strong></p> -+ <?php if ($_option->getMaxCharacters()) :?> -+ <p class="note"><?= $block->escapeHtml(__('Maximum number of characters:')) ?> <strong><?= (int) $_option->getMaxCharacters() ?></strong></p> - <?php endif; ?> - </div> - </div> -diff --git a/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/composite/fieldset/qty.phtml b/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/composite/fieldset/qty.phtml -index 487c9b8e8f2..4726bdc0930 100644 ---- a/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/composite/fieldset/qty.phtml -+++ b/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/composite/fieldset/qty.phtml -@@ -3,9 +3,6 @@ - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -- --// @codingStandardsIgnoreFile -- - ?> - - <?php /* @var $block \Magento\Catalog\Block\Adminhtml\Product\Composite\Fieldset\Qty */ ?> -@@ -13,9 +10,9 @@ - <fieldset id="product_composite_configure_fields_qty" - class="fieldset product-composite-qty-block admin__fieldset <?= $block->getIsLastFieldset() ? 'last-fieldset' : '' ?>"> - <div class="field admin__field"> -- <label class="label admin__field-label"><span><?= /* @escapeNotVerified */ __('Quantity') ?></span></label> -+ <label class="label admin__field-label"><span><?= $block->escapeHtml(__('Quantity')) ?></span></label> - <div class="control admin__field-control"> -- <input id="product_composite_configure_input_qty" class="input-text admin__control-text qty" type="text" name="qty" value="<?= /* @escapeNotVerified */ $block->getQtyValue() * 1 ?>"> -+ <input id="product_composite_configure_input_qty" class="input-text admin__control-text qty" type="text" name="qty" value="<?= $block->getQtyValue() * 1 ?>"> - </div> - </div> - </fieldset> -diff --git a/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/edit.phtml b/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/edit.phtml -index 7c25c3686ea..66df098a194 100644 ---- a/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/edit.phtml -+++ b/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/edit.phtml -@@ -3,11 +3,10 @@ - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -- --// @codingStandardsIgnoreFile -- - ?> - <?php -+// phpcs:disable Magento2.Templates.ThisInTemplate.FoundThis -+ - /** - * @var $block \Magento\Catalog\Block\Adminhtml\Product\Edit - */ -@@ -17,11 +16,11 @@ - <div id="product-template-suggest-container" class="suggest-expandable"> - <div class="action-dropdown"> - <button type="button" class="action-toggle" data-mage-init='{"dropdown":{}}' data-toggle="dropdown"> -- <span><?= /* @escapeNotVerified */ $block->getAttributeSetName() ?></span> -+ <span><?= $block->escapeHtml($block->getAttributeSetName()) ?></span> - </button> - <ul class="dropdown-menu"> - <li><input type="text" id="product-template-suggest" class="search" -- placeholder="<?= /* @noEscape */ __('start typing to search template') ?>"/></li> -+ placeholder="<?= $block->escapeHtmlAttr(__('start typing to search template')) ?>"/></li> - </ul> - </div> - </div> -@@ -30,32 +29,32 @@ - <input type="checkbox" id="product-online-switcher" name="product-online-switcher" /> - <label class="switcher-label" - for="product-online-switcher" -- data-text-on="<?= /* @escapeNotVerified */ __('Product online') ?>" -- data-text-off="<?= /* @escapeNotVerified */ __('Product offline') ?>" -- title="<?= /* @escapeNotVerified */ __('Product online status') ?>"></label> -+ data-text-on="<?= $block->escapeHtmlAttr(__('Product online')) ?>" -+ data-text-off="<?= $block->escapeHtmlAttr(__('Product offline')) ?>" -+ title="<?= $block->escapeHtmlAttr(__('Product online status')) ?>"></label> - </div> - -- <?php if ($block->getProductId()): ?> -+ <?php if ($block->getProductId()) :?> - <?= $block->getDeleteButtonHtml() ?> - <?php endif; ?> -- <?php if ($block->getProductSetId()): ?> -+ <?php if ($block->getProductSetId()) :?> - <?= $block->getChangeAttributeSetButtonHtml() ?> - <?= $block->getSaveSplitButtonHtml() ?> - <?php endif; ?> - <?= $block->getBackButtonHtml() ?> - </div> - </div> --<?php if ($block->getUseContainer()): ?> --<form action="<?= /* @escapeNotVerified */ $block->getSaveUrl() ?>" method="post" enctype="multipart/form-data" -- data-form="edit-product" data-product-id="<?= /* @escapeNotVerified */ $block->getProduct()->getId() ?>"> -+<?php if ($block->getUseContainer()) :?> -+<form action="<?= $block->escapeUrl($block->getSaveUrl()) ?>" method="post" enctype="multipart/form-data" -+ data-form="edit-product" data-product-id="<?= $block->escapeHtmlAttr($block->getProduct()->getId()) ?>"> - <?php endif; ?> - <?= $block->getBlockHtml('formkey') ?> - <div data-role="tabs" id="product-edit-form-tabs"></div> <?php /* @TODO: remove id after elimination of setDestElementId('product-edit-form-tabs') */?> - <?= $block->getChildHtml('product-type-tabs') ?> -- <input type="hidden" id="product_type_id" value="<?= /* @escapeNotVerified */ $block->getProduct()->getTypeId() ?>"/> -- <input type="hidden" id="attribute_set_id" value="<?= /* @escapeNotVerified */ $block->getProduct()->getAttributeSetId() ?>"/> -+ <input type="hidden" id="product_type_id" value="<?= $block->escapeHtmlAttr($block->getProduct()->getTypeId()) ?>"/> -+ <input type="hidden" id="attribute_set_id" value="<?= $block->escapeHtmlAttr($block->getProduct()->getAttributeSetId()) ?>"/> - <button type="submit" class="hidden"></button> --<?php if ($block->getUseContainer()): ?> -+<?php if ($block->getUseContainer()) :?> - </form> - <?php endif; ?> - <script> -@@ -130,10 +129,10 @@ require([ - } - } - }); -- $form.mage('validation', {validationUrl: '<?= /* @escapeNotVerified */ $block->getValidationUrl() ?>'}); -+ $form.mage('validation', {validationUrl: '<?= $block->escapeJs($block->escapeUrl($block->getValidationUrl())) ?>'}); - -- var masks = <?= /* @escapeNotVerified */ $this->helper('Magento\Framework\Json\Helper\Data')->jsonEncode($block->getFieldsAutogenerationMasks()) ?>; -- var availablePlaceholders = <?= /* @escapeNotVerified */ $this->helper('Magento\Framework\Json\Helper\Data')->jsonEncode($block->getAttributesAllowedForAutogeneration()) ?>; -+ var masks = <?= /* @noEscape */ $this->helper(Magento\Framework\Json\Helper\Data::class)->jsonEncode($block->getFieldsAutogenerationMasks()) ?>; -+ var availablePlaceholders = <?= /* @noEscape */ $this->helper(Magento\Framework\Json\Helper\Data::class)->jsonEncode($block->getAttributesAllowedForAutogeneration()) ?>; - var Autogenerator = function(masks) { - this._masks = masks || {}; - this._fieldReverseIndex = this._buildReverseIndex(this._masks); -diff --git a/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/edit/action/attribute.phtml b/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/edit/action/attribute.phtml -index d1591d70945..056cf014f76 100644 ---- a/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/edit/action/attribute.phtml -+++ b/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/edit/action/attribute.phtml -@@ -4,18 +4,22 @@ - * See COPYING.txt for license details. - */ - --// @codingStandardsIgnoreFile -- -+/** @var $block Magento\Catalog\Block\Adminhtml\Product\Edit\Action\Attribute */ - ?> - --<form action="<?= /* @escapeNotVerified */ $block->getSaveUrl() ?>" method="post" id="attributes-edit-form" class="attributes-edit-form" enctype="multipart/form-data"> -+<form action="<?= $block->escapeUrl($block->getSaveUrl()) ?>" -+ method="post" -+ id="attributes-edit-form" -+ class="attributes-edit-form" -+ enctype="multipart/form-data"> - <?= $block->getBlockHtml('formkey') ?> - </form> --<script> --require(['jquery', "mage/mage"], function(jQuery){ -- -- jQuery('#attributes-edit-form').mage('form') -- .mage('validation', {validationUrl: '<?= /* @escapeNotVerified */ $block->getValidationUrl() ?>'}); -- --}); -+<script type="text/x-magento-init"> -+ { -+ "#attributes-edit-form": { -+ "Magento_Catalog/catalog/product/edit/attribute": { -+ "validationUrl": "<?= $block->escapeJs($block->escapeUrl($block->getValidationUrl())) ?>" -+ } -+ } -+ } - </script> -diff --git a/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/edit/action/inventory.phtml b/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/edit/action/inventory.phtml -index efc06d675c3..792af12494a 100644 ---- a/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/edit/action/inventory.phtml -+++ b/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/edit/action/inventory.phtml -@@ -4,7 +4,6 @@ - * See COPYING.txt for license details. - */ - --// @codingStandardsIgnoreFile - /** @var Magento\Catalog\Block\Adminhtml\Product\Edit\Tab\Inventory $block */ - ?> - <script> -@@ -30,329 +29,373 @@ - }); - </script> - -+<?php -+$defaultMinSaleQty = $block->getDefaultConfigValue('min_sale_qty'); -+if (!is_numeric($defaultMinSaleQty)) { -+ $defaultMinSaleQty = json_decode($defaultMinSaleQty, true); -+ $defaultMinSaleQty = (float) $defaultMinSaleQty[\Magento\Customer\Api\Data\GroupInterface::CUST_GROUP_ALL] ?? 1; -+} -+?> - <div class="fieldset-wrapper form-inline advanced-inventory-edit"> - <div class="fieldset-wrapper-title"> - <strong class="title"> -- <span><?= /* @escapeNotVerified */ __('Advanced Inventory') ?></span> -+ <span><?= $block->escapeHtml(__('Advanced Inventory')) ?></span> - </strong> - </div> - <div class="fieldset-wrapper-content"> - <fieldset class="fieldset" id="table_cataloginventory"> - <div class="field"> - <label class="label" for="inventory_manage_stock"> -- <span><?= /* @escapeNotVerified */ __('Manage Stock') ?></span> -+ <span><?= $block->escapeHtml(__('Manage Stock')) ?></span> - </label> - - <div class="control"> - <div class="fields-group-2"> - <div class="field"> -- <select id="inventory_manage_stock" name="<?= /* @escapeNotVerified */ $block->getFieldSuffix() ?>[manage_stock]" -+ <select id="inventory_manage_stock" name="<?= /* @noEscape */ $block->getFieldSuffix() ?>[manage_stock]" - class="select" disabled="disabled"> -- <option value="1"><?= /* @escapeNotVerified */ __('Yes') ?></option> -- <option -- value="0"<?php if ($block->getFieldValue('manage_stock') == 0): ?> selected="selected"<?php endif; ?>><?= /* @escapeNotVerified */ __('No') ?></option> -+ <option value="1"><?= $block->escapeHtml(__('Yes')) ?></option> -+ <option value="0" -+ <?php if ($block->getFieldValue('manage_stock') == 0) :?> -+ selected="selected" -+ <?php endif; ?>><?= $block->escapeHtml(__('No')) ?></option> - </select> - </div> - <div class="field choice"> -- <input name="<?= /* @escapeNotVerified */ $block->getFieldSuffix() ?>[use_config_manage_stock]" type="checkbox" -+ <input name="<?= /* @noEscape */ $block->getFieldSuffix() ?>[use_config_manage_stock]" type="checkbox" - id="inventory_use_config_manage_stock" data-role="toggle-editability" value="1" - checked="checked" disabled="disabled"/> - <label for="inventory_use_config_manage_stock" -- class="label"><span><?= /* @escapeNotVerified */ __('Use Config Settings') ?></span></label> -+ class="label"><span><?= $block->escapeHtml(__('Use Config Settings')) ?></span></label> - </div> - <div class="field choice"> - <input type="checkbox" id="inventory_manage_stock_checkbox" data-role="toggle-editability-all"/> - <label for="inventory_manage_stock_checkbox" -- class="label"><span><?= /* @escapeNotVerified */ __('Change') ?></span></label> -+ class="label"><span><?= $block->escapeHtml(__('Change')) ?></span></label> - </div> - </div> - </div> -- <div class="field-service" value-scope="<?= /* @escapeNotVerified */ __('[GLOBAL]') ?>"></div> -+ <div class="field-service" value-scope="<?= $block->escapeHtmlAttr(__('[GLOBAL]')) ?>"></div> - </div> - - <div class="field required"> - <label class="label" for="inventory_qty"> -- <span><?= /* @escapeNotVerified */ __('Qty') ?></span> -+ <span><?= $block->escapeHtml(__('Qty')) ?></span> - </label> - - <div class="control"> - <div class="fields-group-2"> - <div class="field"> - <input type="text" class="input-text required-entry validate-number" id="inventory_qty" -- name="<?= /* @escapeNotVerified */ $block->getFieldSuffix() ?>[qty]" -- value="<?= /* @escapeNotVerified */ $block->getDefaultConfigValue('qty') * 1 ?>" disabled="disabled"/> -+ name="<?= /* @noEscape */ $block->getFieldSuffix() ?>[qty]" -+ value="<?= $block->getDefaultConfigValue('qty') * 1 ?>" disabled="disabled"/> - </div> - <div class="field choice"> - <input type="checkbox" id="inventory_qty_checkbox" data-role="toggle-editability-all"/> - <label for="inventory_qty_checkbox" -- class="label"><span><?= /* @escapeNotVerified */ __('Change') ?></span></label> -+ class="label"><span><?= $block->escapeHtml(__('Change')) ?></span></label> - </div> - </div> - </div> -- <div class="field-service" value-scope="<?= /* @escapeNotVerified */ __('[GLOBAL]') ?>"></div> -+ <div class="field-service" value-scope="<?= $block->escapeHtmlAttr(__('[GLOBAL]')) ?>"></div> - </div> - - <div class="field with-addon"> - <label class="label" for="inventory_min_qty"> -- <span><?= /* @escapeNotVerified */ __('Out-of-Stock Threshold') ?></span> -+ <span><?= $block->escapeHtml(__('Out-of-Stock Threshold')) ?></span> - </label> - - <div class="control"> - <div class="fields-group-2"> - <div class="field"> - <input type="text" class="input-text validate-number" id="inventory_min_qty" -- name="<?= /* @escapeNotVerified */ $block->getFieldSuffix() ?>[min_qty]" -- value="<?= /* @escapeNotVerified */ $block->getDefaultConfigValue('min_qty') * 1 ?>" disabled="disabled"/> -+ name="<?= /* @noEscape */ $block->getFieldSuffix() ?>[min_qty]" -+ value="<?= $block->getDefaultConfigValue('min_qty') * 1 ?>" disabled="disabled"/> - </div> - <div class="field choice"> - <input type="checkbox" id="inventory_use_config_min_qty" -- name="<?= /* @escapeNotVerified */ $block->getFieldSuffix() ?>[use_config_min_qty]" value="1" -+ name="<?= /* @noEscape */ $block->getFieldSuffix() ?>[use_config_min_qty]" value="1" - data-role="toggle-editability" checked="checked" disabled="disabled"/> - <label for="inventory_use_config_min_qty" class="label"> -- <span><?= /* @escapeNotVerified */ __('Use Config Settings') ?></span> -+ <span><?= $block->escapeHtml(__('Use Config Settings')) ?></span> - </label> - </div> - <div class="field choice"> - <input type="checkbox" id="inventory_min_qty_checkbox" data-role="toggle-editability-all"/> - <label for="inventory_min_qty_checkbox" -- class="label"><span><?= /* @escapeNotVerified */ __('Change') ?></span></label> -+ class="label"><span><?= $block->escapeHtml(__('Change')) ?></span></label> - </div> - </div> - </div> -- <div class="field-service" value-scope="<?= /* @escapeNotVerified */ __('[GLOBAL]') ?>"></div> -+ <div class="field-service" value-scope="<?= $block->escapeHtmlAttr(__('[GLOBAL]')) ?>"></div> - </div> - - <div class="field"> - <label class="label" for="inventory_min_sale_qty"> -- <span><?= /* @escapeNotVerified */ __('Minimum Qty Allowed in Shopping Cart') ?></span> -+ <span><?= $block->escapeHtml(__('Minimum Qty Allowed in Shopping Cart')) ?></span> - </label> - - <div class="control"> - <div class="fields-group-2"> - <div class="field"> - <input type="text" class="input-text validate-number" id="inventory_min_sale_qty" -- name="<?= /* @escapeNotVerified */ $block->getFieldSuffix() ?>[min_sale_qty]" -- value="<?= /* @escapeNotVerified */ $block->getDefaultConfigValue('min_sale_qty') * 1 ?>" -+ name="<?= /* @noEscape */ $block->getFieldSuffix() ?>[min_sale_qty]" -+ value="<?= $defaultMinSaleQty * 1 ?>" - disabled="disabled"/> - </div> - <div class="field choice"> - <input type="checkbox" id="inventory_use_config_min_sale_qty" -- name="<?= /* @escapeNotVerified */ $block->getFieldSuffix() ?>[use_config_min_sale_qty]" value="1" data-role="toggle-editability" checked="checked" disabled="disabled"/> -+ name="<?= /* @noEscape */ $block->getFieldSuffix() ?>[use_config_min_sale_qty]" -+ value="1" -+ data-role="toggle-editability" -+ checked="checked" -+ disabled="disabled"/> - <label for="inventory_use_config_min_sale_qty" -- class="label"><span><?= /* @escapeNotVerified */ __('Use Config Settings') ?></span></label> -+ class="label"><span><?= $block->escapeHtml(__('Use Config Settings')) ?></span></label> - </div> - <div class="field choice"> - <input type="checkbox" id="inventory_min_sale_qty_checkbox" data-role="toggle-editability-all"/> - <label for="inventory_min_sale_qty_checkbox" -- class="label"><span><?= /* @escapeNotVerified */ __('Change') ?></span></label> -+ class="label"><span><?= $block->escapeHtml(__('Change')) ?></span></label> - </div> - </div> - </div> -- <div class="field-service" value-scope="<?= /* @escapeNotVerified */ __('[GLOBAL]') ?>"></div> -+ <div class="field-service" value-scope="<?= $block->escapeHtmlAttr(__('[GLOBAL]')) ?>"></div> - </div> - - <div class="field"> - <label class="label" for="inventory_max_sale_qty"> -- <span><?= /* @escapeNotVerified */ __('Maximum Qty Allowed in Shopping Cart') ?></span> -+ <span><?= $block->escapeHtml(__('Maximum Qty Allowed in Shopping Cart')) ?></span> - </label> - - <div class="control"> - <div class="fields-group-2"> - <div class="field"> - <input type="text" class="input-text validate-number" id="inventory_max_sale_qty" -- name="<?= /* @escapeNotVerified */ $block->getFieldSuffix() ?>[max_sale_qty]" -- value="<?= /* @escapeNotVerified */ $block->getDefaultConfigValue('max_sale_qty') * 1 ?>" -+ name="<?= /* @noEscape */ $block->getFieldSuffix() ?>[max_sale_qty]" -+ value="<?= $block->getDefaultConfigValue('max_sale_qty') * 1 ?>" - disabled="disabled"/> - </div> - <div class="field choice"> -- <input type="checkbox" id="inventory_use_config_max_sale_qty" -- name="<?= /* @escapeNotVerified */ $block->getFieldSuffix() ?>[use_config_max_sale_qty]" value="1" data-role="toggle-editability" checked="checked" disabled="disabled"/> -+ <input type="checkbox" -+ id="inventory_use_config_max_sale_qty" -+ name="<?= /* @noEscape */ $block->getFieldSuffix() ?>[use_config_max_sale_qty]" -+ value="1" -+ data-role="toggle-editability" -+ checked="checked" -+ disabled="disabled"/> - <label for="inventory_use_config_max_sale_qty" -- class="label"><span><?= /* @escapeNotVerified */ __('Use Config Settings') ?></span></label> -+ class="label"><span><?= $block->escapeHtml(__('Use Config Settings')) ?></span></label> - </div> - <div class="field choice"> - <input type="checkbox" id="inventory_max_sale_checkbox" data-role="toggle-editability-all"/> - <label for="inventory_max_sale_checkbox" -- class="label"><span><?= /* @escapeNotVerified */ __('Change') ?></span></label> -+ class="label"><span><?= $block->escapeHtml(__('Change')) ?></span></label> - </div> - </div> - </div> -- <div class="field-service" value-scope="<?= /* @escapeNotVerified */ __('[GLOBAL]') ?>"></div> -+ <div class="field-service" value-scope="<?= $block->escapeHtmlAttr(__('[GLOBAL]')) ?>"></div> - </div> - - <div class="field"> - <label class="label" for="inventory_is_qty_decimal"> -- <span><?= /* @escapeNotVerified */ __('Qty Uses Decimals') ?></span> -+ <span><?= $block->escapeHtml(__('Qty Uses Decimals')) ?></span> - </label> - - <div class="control"> - <div class="fields-group-2"> - <div class="field"> - <select id="inventory_is_qty_decimal" -- name="<?= /* @escapeNotVerified */ $block->getFieldSuffix() ?>[is_qty_decimal]" class="select" -+ name="<?= /* @noEscape */ $block->getFieldSuffix() ?>[is_qty_decimal]" -+ class="select" - disabled="disabled"> -- <option value="0"><?= /* @escapeNotVerified */ __('No') ?></option> -- <option -- value="1"<?php if ($block->getDefaultConfigValue('is_qty_decimal') == 1): ?> selected="selected"<?php endif; ?>><?= /* @escapeNotVerified */ __('Yes') ?></option> -+ <option value="0"><?= $block->escapeHtml(__('No')) ?></option> -+ <option value="1" -+ <?php if ($block->getDefaultConfigValue('is_qty_decimal') == 1) :?> -+ selected="selected" -+ <?php endif; ?>><?= $block->escapeHtml(__('Yes')) ?></option> - </select> - </div> - <div class="field choice"> - <input type="checkbox" id="inventory_is_qty_decimal_checkbox" data-role="toggle-editability-all"/> - <label for="inventory_is_qty_decimal_checkbox" -- class="label"><span><?= /* @escapeNotVerified */ __('Change') ?></span></label> -+ class="label"><span><?= $block->escapeHtml(__('Change')) ?></span></label> - </div> - </div> - </div> -- <div class="field-service" value-scope="<?= /* @escapeNotVerified */ __('[GLOBAL]') ?>"></div> -+ <div class="field-service" value-scope="<?= $block->escapeHtmlAttr(__('[GLOBAL]')) ?>"></div> - </div> - - <div class="field"> - <label class="label" for="inventory_backorders"> -- <span><?= /* @escapeNotVerified */ __('Backorders') ?></span> -+ <span><?= $block->escapeHtml(__('Backorders')) ?></span> - </label> - - <div class="control"> - <div class="fields-group-2"> - <div class="field"> -- <select id="inventory_backorders" name="<?= /* @escapeNotVerified */ $block->getFieldSuffix() ?>[backorders]" -- class="select" disabled="disabled"> -- <?php foreach ($block->getBackordersOption() as $option): ?> -+ <select id="inventory_backorders" -+ name="<?= /* @noEscape */ $block->getFieldSuffix() ?>[backorders]" -+ class="select" -+ disabled="disabled"> -+ <?php foreach ($block->getBackordersOption() as $option) :?> - <?php $_selected = ($option['value'] == $block->getDefaultConfigValue('backorders')) ? ' selected="selected"' : '' ?> - <option -- value="<?= /* @escapeNotVerified */ $option['value'] ?>"<?= /* @escapeNotVerified */ $_selected ?>><?= /* @escapeNotVerified */ $option['label'] ?></option> -+ value="<?= $block->escapeHtmlAttr($option['value']) ?>"<?= /* @noEscape */ $_selected ?>><?= $block->escapeHtml($option['label']) ?></option> - <?php endforeach; ?> - </select> - </div> - <div class="field choice"> - <input type="checkbox" id="inventory_use_config_backorders" -- name="<?= /* @escapeNotVerified */ $block->getFieldSuffix() ?>[use_config_backorders]" value="1" data-role="toggle-editability" checked="checked" disabled="disabled"/> -+ name="<?= /* @noEscape */ $block->getFieldSuffix() ?>[use_config_backorders]" -+ value="1" -+ data-role="toggle-editability" -+ checked="checked" -+ disabled="disabled"/> - <label for="inventory_use_config_backorders" -- class="label"><span><?= /* @escapeNotVerified */ __('Use Config Settings') ?></span></label> -+ class="label"><span><?= $block->escapeHtml(__('Use Config Settings')) ?></span></label> - </div> - <div class="field choice"> - <input type="checkbox" id="inventory_backorders_checkbox" data-role="toggle-editability-all"/> -- <label for="inventory_backorders_checkbox" class="label"><span><?= /* @escapeNotVerified */ __('Change') ?></span></label> -+ <label for="inventory_backorders_checkbox" -+ class="label"><span><?= $block->escapeHtml(__('Change')) ?></span></label> - </div> - </div> - - </div> -- <div class="field-service" value-scope="<?= /* @escapeNotVerified */ __('[GLOBAL]') ?>"></div> -+ <div class="field-service" value-scope="<?= $block->escapeHtmlAttr(__('[GLOBAL]')) ?>"></div> - </div> - - <div class="field"> - <label class="label" for="inventory_notify_stock_qty"> -- <span><?= /* @escapeNotVerified */ __('Notify for Quantity Below') ?></span> -+ <span><?= $block->escapeHtml(__('Notify for Quantity Below')) ?></span> - </label> - - <div class="control"> - <div class="fields-group-2"> - <div class="field"> - <input type="text" class="input-text validate-number" id="inventory_notify_stock_qty" -- name="<?= /* @escapeNotVerified */ $block->getFieldSuffix() ?>[notify_stock_qty]" -- value="<?= /* @escapeNotVerified */ $block->getDefaultConfigValue('notify_stock_qty') * 1 ?>" -+ name="<?= /* @noEscape */ $block->getFieldSuffix() ?>[notify_stock_qty]" -+ value="<?= $block->getDefaultConfigValue('notify_stock_qty') * 1 ?>" - disabled="disabled"/> - </div> - <div class="field choice"> -- <input type="checkbox" id="inventory_use_config_notify_stock_qty" -- name="<?= /* @escapeNotVerified */ $block->getFieldSuffix() ?>[use_config_notify_stock_qty]" value="1" data-role="toggle-editability" checked="checked" disabled="disabled"/> -+ <input type="checkbox" -+ id="inventory_use_config_notify_stock_qty" -+ name="<?= /* @noEscape */ $block->getFieldSuffix() ?>[use_config_notify_stock_qty]" -+ value="1" -+ data-role="toggle-editability" -+ checked="checked" -+ disabled="disabled"/> - <label for="inventory_use_config_notify_stock_qty" -- class="label"><span><?= /* @escapeNotVerified */ __('Use Config Settings') ?></span></label> -+ class="label"><span><?= $block->escapeHtml(__('Use Config Settings')) ?></span></label> - </div> - <div class="field choice"> - <input type="checkbox" id="inventory_notify_stock_qty_checkbox" data-role="toggle-editability-all"/> - <label for="inventory_notify_stock_qty_checkbox" -- class="label"><span><?= /* @escapeNotVerified */ __('Change') ?></span></label> -+ class="label"><span><?= $block->escapeHtml(__('Change')) ?></span></label> - </div> - </div> - </div> -- <div class="field-service" value-scope="<?= /* @escapeNotVerified */ __('[GLOBAL]') ?>"></div> -+ <div class="field-service" value-scope="<?= $block->escapeHtmlAttr(__('[GLOBAL]')) ?>"></div> - </div> - - <div class="field"> - <label class="label" for="inventory_enable_qty_increments"> -- <span><?= /* @escapeNotVerified */ __('Enable Qty Increments') ?></span> -+ <span><?= $block->escapeHtml(__('Enable Qty Increments')) ?></span> - </label> - - <div class="control"> - <div class="fields-group-2"> - <div class="field"> - <select id="inventory_enable_qty_increments" -- name="<?= /* @escapeNotVerified */ $block->getFieldSuffix() ?>[enable_qty_increments]" class="select" -+ name="<?= /* @noEscape */ $block->getFieldSuffix() ?>[enable_qty_increments]" -+ class="select" - disabled="disabled"> -- <option value="1"><?= /* @escapeNotVerified */ __('Yes') ?></option> -- <option -- value="0"<?php if ($block->getDefaultConfigValue('enable_qty_increments') == 0): ?> selected="selected"<?php endif; ?>><?= /* @escapeNotVerified */ __('No') ?></option> -+ <option value="1"><?= $block->escapeHtml(__('Yes')) ?></option> -+ <option value="0" -+ <?php if ($block->getDefaultConfigValue('enable_qty_increments') == 0) :?> -+ selected="selected" -+ <?php endif; ?>><?= $block->escapeHtml(__('No')) ?></option> - </select> - </div> - <div class="field choice"> - <input type="checkbox" id="inventory_use_config_enable_qty_increments" -- name="<?= /* @escapeNotVerified */ $block->getFieldSuffix() ?>[use_config_enable_qty_increments]" value="1" data-role="toggle-editability" checked="checked" disabled="disabled"/> -+ name="<?= /* @noEscape */ $block->getFieldSuffix() ?>[use_config_enable_qty_increments]" -+ value="1" -+ data-role="toggle-editability" -+ checked="checked" -+ disabled="disabled"/> - <label for="inventory_use_config_enable_qty_increments" -- class="label"><span><?= /* @escapeNotVerified */ __('Use Config Settings') ?></span></label> -+ class="label"><span><?= $block->escapeHtml(__('Use Config Settings')) ?></span></label> - </div> - <div class="field choice"> - <input type="checkbox" id="inventory_enable_qty_increments_checkbox" data-role="toggle-editability-all"/> - <label for="inventory_enable_qty_increments_checkbox" -- class="label"><span><?= /* @escapeNotVerified */ __('Change') ?></span></label> -+ class="label"><span><?= $block->escapeHtml(__('Change')) ?></span></label> - </div> - </div> - </div> -- <div class="field-service" value-scope="<?= /* @escapeNotVerified */ __('[GLOBAL]') ?>"></div> -+ <div class="field-service" value-scope="<?= $block->escapeHtmlAttr(__('[GLOBAL]')) ?>"></div> - </div> - - <div class="field"> - <label class="label" for="inventory_qty_increments"> -- <span><?= /* @escapeNotVerified */ __('Qty Increments') ?></span> -+ <span><?= $block->escapeHtml(__('Qty Increments')) ?></span> - </label> - - <div class="control"> - <div class="fields-group-2"> - <div class="field"> - <input type="text" class="input-text validate-number" id="inventory_qty_increments" -- name="<?= /* @escapeNotVerified */ $block->getFieldSuffix() ?>[qty_increments]" -- value="<?= /* @escapeNotVerified */ $block->getDefaultConfigValue('qty_increments') * 1 ?>" -+ name="<?= /* @noEscape */ $block->getFieldSuffix() ?>[qty_increments]" -+ value="<?= $block->getDefaultConfigValue('qty_increments') * 1 ?>" - disabled="disabled"/> - </div> - <div class="field choice"> -- <input type="checkbox" id="inventory_use_config_qty_increments" -- name="<?= /* @escapeNotVerified */ $block->getFieldSuffix() ?>[use_config_qty_increments]" value="1" data-role="toggle-editability" checked="checked" disabled="disabled"/> -+ <input type="checkbox" -+ id="inventory_use_config_qty_increments" -+ name="<?= /* @noEscape */ $block->getFieldSuffix() ?>[use_config_qty_increments]" -+ value="1" -+ data-role="toggle-editability" -+ checked="checked" -+ disabled="disabled"/> - <label for="inventory_use_config_qty_increments" -- class="label"><span><?= /* @escapeNotVerified */ __('Use Config Settings') ?></span></label> -+ class="label"><span><?= $block->escapeHtml(__('Use Config Settings')) ?></span></label> - </div> - <div class="field choice"> - <input type="checkbox" id="inventory_qty_increments_checkbox" data-role="toggle-editability-all"/> - <label for="inventory_qty_increments_checkbox" -- class="label"><span><?= /* @escapeNotVerified */ __('Change') ?></span></label> -+ class="label"><span><?= $block->escapeHtml(__('Change')) ?></span></label> - </div> - </div> - </div> -- <div class="field-service" value-scope="<?= /* @escapeNotVerified */ __('[GLOBAL]') ?>"></div> -+ <div class="field-service" value-scope="<?= $block->escapeHtmlAttr(__('[GLOBAL]')) ?>"></div> - </div> - - <div class="field"> - <label class="label" for="inventory_stock_availability"> -- <span><?= /* @escapeNotVerified */ __('Stock Availability') ?></span> -+ <span><?= $block->escapeHtml(__('Stock Availability')) ?></span> - </label> - - <div class="control"> - <div class="fields-group-2"> - <div class="field"> - <select id="inventory_stock_availability" -- name="<?= /* @escapeNotVerified */ $block->getFieldSuffix() ?>[is_in_stock]" class="select" -+ name="<?= /* @noEscape */ $block->getFieldSuffix() ?>[is_in_stock]" class="select" - disabled="disabled"> -- <option value="1"><?= /* @escapeNotVerified */ __('In Stock') ?></option> -- <option -- value="0"<?php if ($block->getDefaultConfigValue('is_in_stock') == 0): ?> selected<?php endif; ?>><?= /* @escapeNotVerified */ __('Out of Stock') ?></option> -+ <option value="1"><?= $block->escapeHtml(__('In Stock')) ?></option> -+ <option value="0"<?php if ($block->getDefaultConfigValue('is_in_stock') == 0) :?> selected<?php endif; ?>><?= $block->escapeHtml(__('Out of Stock')) ?></option> - </select> - </div> - <div class="field choice"> - <input type="checkbox" id="inventory_stock_availability_checkbox" data-role="toggle-editability-all"/> - <label for="inventory_stock_availability_checkbox" -- class="label"><span><?= /* @escapeNotVerified */ __('Change') ?></span></label> -+ class="label"><span><?= $block->escapeHtml(__('Change')) ?></span></label> - </div> - </div> - </div> -- <div class="field-service" value-scope="<?= /* @escapeNotVerified */ __('[GLOBAL]') ?>"></div> -+ <div class="field-service" value-scope="<?= $block->escapeHtmlAttr(__('[GLOBAL]')) ?>"></div> - </div> - </fieldset> - </div> -diff --git a/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/edit/action/websites.phtml b/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/edit/action/websites.phtml -index cd297a7bbf2..98b06050e0d 100644 ---- a/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/edit/action/websites.phtml -+++ b/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/edit/action/websites.phtml -@@ -4,29 +4,35 @@ - * See COPYING.txt for license details. - */ - --// @codingStandardsIgnoreFile -- -+/** @var $block Magento\Catalog\Block\Adminhtml\Product\Edit\Action\Attribute\Tab\Websites */ - ?> - - <div class="fieldset-wrapper" id="add-products-to-website-wrapper"> - <fieldset class="fieldset" id="grop_fields"> - <legend class="legend"> -- <span><?= /* @escapeNotVerified */ __('Add Product To Websites') ?></span> -+ <span><?= $block->escapeHtml(__('Add Product To Websites')) ?></span> - </legend> - <br> - <div class="store-scope"> - <div class="store-tree" id="add-products-to-website-content"> -- <?php foreach ($block->getWebsiteCollection() as $_website): ?> -+ <?php foreach ($block->getWebsiteCollection() as $_website) :?> - <div class="website-name"> -- <input name="add_website_ids[]" value="<?= /* @escapeNotVerified */ $_website->getId() ?>" <?php if ($block->getWebsitesReadonly()): ?>disabled="disabled"<?php endif;?> class="checkbox website-checkbox" id="add_product_website_<?= /* @escapeNotVerified */ $_website->getId() ?>" type="checkbox" /> -- <label for="add_product_website_<?= /* @escapeNotVerified */ $_website->getId() ?>"><?= $block->escapeHtml($_website->getName()) ?></label> -+ <input name="add_website_ids[]" -+ value="<?= $block->escapeHtmlAttr($_website->getId()) ?>" -+ <?php if ($block->getWebsitesReadonly()) :?> -+ disabled="disabled" -+ <?php endif;?> -+ class="checkbox website-checkbox" -+ id="add_product_website_<?= $block->escapeHtmlAttr($_website->getId()) ?>" -+ type="checkbox" /> -+ <label for="add_product_website_<?= $block->escapeHtmlAttr($_website->getId()) ?>"><?= $block->escapeHtml($_website->getName()) ?></label> - </div> -- <dl class="webiste-groups" id="add_product_website_<?= /* @escapeNotVerified */ $_website->getId() ?>_data"> -- <?php foreach ($block->getGroupCollection($_website) as $_group): ?> -+ <dl class="webiste-groups" id="add_product_website_<?= $block->escapeHtmlAttr($_website->getId()) ?>_data"> -+ <?php foreach ($block->getGroupCollection($_website) as $_group) :?> - <dt><?= $block->escapeHtml($_group->getName()) ?></dt> - <dd class="group-stores"> - <ul> -- <?php foreach ($block->getStoreCollection($_group) as $_store): ?> -+ <?php foreach ($block->getStoreCollection($_group) as $_store) :?> - <li> - <?= $block->escapeHtml($_store->getName()) ?> - </li> -@@ -44,27 +50,35 @@ - <div class="fieldset-wrapper" id="remove-products-to-website-wrapper"> - <fieldset class="fieldset" id="grop_fields"> - <legend class="legend"> -- <span><?= /* @escapeNotVerified */ __('Remove Product From Websites') ?></span> -+ <span><?= $block->escapeHtml(__('Remove Product From Websites')) ?></span> - </legend> - <br> - <div class="messages"> - <div class="message message-notice"> -- <div><?= /* @escapeNotVerified */ __('To hide an item in catalog or search results, set the status to "Disabled".') ?></div> -+ <div><?= $block->escapeHtml(__('To hide an item in catalog or search results, set the status to "Disabled".')) ?></div> - </div> - </div> - <div class="store-scope"> - <div class="store-tree" id="remove-products-to-website-content"> -- <?php foreach ($block->getWebsiteCollection() as $_website): ?> -+ <?php foreach ($block->getWebsiteCollection() as $_website) :?> - <div class="website-name"> -- <input name="remove_website_ids[]" value="<?= /* @escapeNotVerified */ $_website->getId() ?>" <?php if ($block->getWebsitesReadonly()): ?>disabled="disabled"<?php endif;?> class="checkbox website-checkbox" id="remove_product_website_<?= /* @escapeNotVerified */ $_website->getId() ?>" type="checkbox" /> -- <label for="remove_product_website_<?= /* @escapeNotVerified */ $_website->getId() ?>"><?= $block->escapeHtml($_website->getName()) ?></label> -+ <input name="remove_website_ids[]" -+ value="<?= $block->escapeHtmlAttr($_website->getId()) ?>" -+ <?php if ($block->getWebsitesReadonly()) :?> -+ disabled="disabled" -+ <?php endif;?> -+ class="checkbox website-checkbox" -+ id="remove_product_website_<?= $block->escapeHtmlAttr($_website->getId()) ?>" -+ type="checkbox" /> -+ <label for="remove_product_website_<?= $block->escapeHtmlAttr($_website->getId()) ?>"><?= $block->escapeHtml($_website->getName()) ?></label> - </div> -- <dl class="webiste-groups" id="remove_product_website_<?= /* @escapeNotVerified */ $_website->getId() ?>_data"> -- <?php foreach ($block->getGroupCollection($_website) as $_group): ?> -+ <dl class="webiste-groups" -+ id="remove_product_website_<?= $block->escapeHtmlAttr($_website->getId()) ?>_data"> -+ <?php foreach ($block->getGroupCollection($_website) as $_group) :?> - <dt><?= $block->escapeHtml($_group->getName()) ?></dt> - <dd class="group-stores"> - <ul> -- <?php foreach ($block->getStoreCollection($_group) as $_store): ?> -+ <?php foreach ($block->getStoreCollection($_group) as $_store) :?> - <li> - <?= $block->escapeHtml($_store->getName()) ?> - </li> -diff --git a/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/edit/attribute_set.phtml b/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/edit/attribute_set.phtml -index 6a5f6c46484..d073053e2f8 100644 ---- a/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/edit/attribute_set.phtml -+++ b/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/edit/attribute_set.phtml -@@ -4,9 +4,9 @@ - * See COPYING.txt for license details. - */ - --// @codingStandardsIgnoreFile -+// phpcs:disable Magento2.Templates.ThisInTemplate.FoundThis - -- /* @var $block \Magento\Catalog\Block\Adminhtml\Product\Edit\AttributeSet */ -+/* @var $block \Magento\Catalog\Block\Adminhtml\Product\Edit\AttributeSet */ - ?> - <script id="product-template-selector-template" type="text/x-magento-template"> - <% if (!data.term && data.items.length && !data.allShown()) { %> -@@ -14,7 +14,7 @@ - <% } %> - <ul data-mage-init='{"menu":[]}'> - <% _.each(data.items, function(value) { %> -- <li <%- data.optionData(value) %>><a href="#"><%- value.label %></a></li> -+ <li <%= data.optionData(value) %>><a href="#"><%- value.label %></a></li> - <% }); %> - </ul> - <% if (!data.term && data.items.length && !data.allShown()) { %> -@@ -32,7 +32,7 @@ - } - }); - $suggest -- .mage('suggest',<?= /* @escapeNotVerified */ $this->helper('Magento\Framework\Json\Helper\Data')->jsonEncode($block->getSelectorOptions()) ?>) -+ .mage('suggest',<?= /* @noEscape */ $this->helper(Magento\Framework\Json\Helper\Data::class)->jsonEncode($block->getSelectorOptions()) ?>) - .on('suggestselect', function (e, ui) { - if (ui.item.id) { - $('[data-form=edit-product]').trigger('changeAttributeSet', ui.item); -diff --git a/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/edit/category/new/form.phtml b/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/edit/category/new/form.phtml -index 84c32578402..f12a99e6c78 100644 ---- a/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/edit/category/new/form.phtml -+++ b/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/edit/category/new/form.phtml -@@ -5,7 +5,7 @@ - */ - /* @var $block \Magento\Catalog\Block\Adminhtml\Product\Edit\NewCategory */ - ?> --<div id="<?= /* @escapeNotVerified */ $block->getNameInLayout() ?>" style="display:none"> -+<div id="<?= $block->escapeHtmlAttr($block->getNameInLayout()) ?>" style="display:none"> - <?= $block->getFormHtml() ?> - <?= $block->getAfterElementHtml() ?> - </div> -diff --git a/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/edit/options.phtml b/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/edit/options.phtml -index 2570a5d7126..ad38d250a33 100644 ---- a/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/edit/options.phtml -+++ b/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/edit/options.phtml -@@ -3,16 +3,13 @@ - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -- --// @codingStandardsIgnoreFile -- - ?> - <?php /** @var $block \Magento\Catalog\Block\Adminhtml\Product\Edit\Tab\Options */ ?> - - <div class="fieldset-wrapper" id="product-custom-options-wrapper" data-block="product-custom-options"> - <div class="fieldset-wrapper-title"> - <strong class="title"> -- <span><?= /* @escapeNotVerified */ __('Custom Options') ?></span> -+ <span><?= $block->escapeHtml(__('Custom Options')) ?></span> - </strong> - </div> - <div class="fieldset-wrapper-content" id="product-custom-options-content" data-role="product-custom-options-content"> -@@ -20,7 +17,7 @@ - <div class="messages"> - <div class="message message-error" id="dynamic-price-warning" style="display: none;"> - <div class="message-inner"> -- <div class="message-content"><?= /* @escapeNotVerified */ __('We can\'t save custom-defined options for bundles with dynamic pricing.') ?></div> -+ <div class="message-content"><?= $block->escapeHtml(__('We can\'t save custom-defined options for bundles with dynamic pricing.')) ?></div> - </div> - </div> - </div> -diff --git a/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/edit/options/option.phtml b/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/edit/options/option.phtml -index d2bca5ce173..713366e73ab 100644 ---- a/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/edit/options/option.phtml -+++ b/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/edit/options/option.phtml -@@ -4,8 +4,7 @@ - * See COPYING.txt for license details. - */ - --// @codingStandardsIgnoreFile -- -+// phpcs:disable Magento2.Templates.ThisInTemplate.FoundThis - ?> - <?php /** @var $block \Magento\Catalog\Block\Adminhtml\Product\Edit\Tab\Options\Option */ ?> - <?= $block->getTemplatesHtml() ?> -@@ -19,30 +18,52 @@ - <span id="option_<%- data.id %>_header_title"><%- data.title %></span> - </strong> - <div class="actions"> -- <button type="button" title="<?= /* @escapeNotVerified */ __('Delete Custom Option') ?>" class="action-delete" id="<?= /* @escapeNotVerified */ $block->getFieldId() ?>_<%- data.id %>_delete"> -- <span><?= /* @escapeNotVerified */ __('Delete Custom Option') ?></span> -+ <button type="button" -+ title="<?= $block->escapeHtmlAttr(__('Delete Custom Option')) ?>" -+ class="action-delete" -+ id="<?= /* @noEscape */ $block->getFieldId() ?>_<%- data.id %>_delete"> -+ <span><?= $block->escapeHtml(__('Delete Custom Option')) ?></span> - </button> - </div> -- <div id="<?= /* @escapeNotVerified */ $block->getFieldId() ?>_<%- data.id %>_move" data-role="draggable-handle" class="draggable-handle" -- title="<?= /* @escapeNotVerified */ __('Sort Custom Options') ?>"></div> -+ <div id="<?= /* @noEscape */ $block->getFieldId() ?>_<%- data.id %>_move" -+ data-role="draggable-handle" -+ class="draggable-handle" -+ title="<?= $block->escapeHtmlAttr(__('Sort Custom Options')) ?>"></div> - </div> - <div class="fieldset-wrapper-content in collapse" id="<%- data.id %>-content"> - <fieldset class="fieldset"> -- <fieldset class="fieldset-alt" id="<?= /* @escapeNotVerified */ $block->getFieldId() ?>_<%- data.id %>"> -- <input id="<?= /* @escapeNotVerified */ $block->getFieldId() ?>_<%- data.id %>_is_delete" name="<?= /* @escapeNotVerified */ $block->getFieldName() ?>[<%- data.id %>][is_delete]" type="hidden" value=""/> -- <input id="<?= /* @escapeNotVerified */ $block->getFieldId() ?>_<%- data.id %>_previous_type" name="<?= /* @escapeNotVerified */ $block->getFieldName() ?>[<%- data.id %>][previous_type]" type="hidden" value="<%- data.type %>"/> -- <input id="<?= /* @escapeNotVerified */ $block->getFieldId() ?>_<%- data.id %>_previous_group" name="<?= /* @escapeNotVerified */ $block->getFieldName() ?>[<%- data.id %>][previous_group]" type="hidden" value=""/> -- <input id="<?= /* @escapeNotVerified */ $block->getFieldId() ?>_<%- data.id %>_id" name="<?= /* @escapeNotVerified */ $block->getFieldName() ?>[<%- data.id %>][id]" type="hidden" value="<%- data.id %>"/> -- <input id="<?= /* @escapeNotVerified */ $block->getFieldId() ?>_<%- data.id %>_option_id" name="<?= /* @escapeNotVerified */ $block->getFieldName() ?>[<%- data.id %>][option_id]" type="hidden" value="<%- data.option_id %>"/> -- <input name="<?= /* @escapeNotVerified */ $block->getFieldName() ?>[<%- data.id %>][sort_order]" type="hidden" value="<%- data.sort_order %>"/> -+ <fieldset class="fieldset-alt" id="<?= /* @noEscape */ $block->getFieldId() ?>_<%- data.id %>"> -+ <input id="<?= /* @noEscape */ $block->getFieldId() ?>_<%- data.id %>_is_delete" -+ name="<?= /* @noEscape */ $block->getFieldName() ?>[<%- data.id %>][is_delete]" -+ type="hidden" -+ value=""/> -+ <input id="<?= /* @noEscape */ $block->getFieldId() ?>_<%- data.id %>_previous_type" -+ name="<?= /* @noEscape */ $block->getFieldName() ?>[<%- data.id %>][previous_type]" -+ type="hidden" -+ value="<%- data.type %>"/> -+ <input id="<?= /* @noEscape */ $block->getFieldId() ?>_<%- data.id %>_previous_group" -+ name="<?= /* @noEscape */ $block->getFieldName() ?>[<%- data.id %>][previous_group]" -+ type="hidden" -+ value=""/> -+ <input id="<?= /* @noEscape */ $block->getFieldId() ?>_<%- data.id %>_id" -+ name="<?= /* @noEscape */ $block->getFieldName() ?>[<%- data.id %>][id]" -+ type="hidden" -+ value="<%- data.id %>"/> -+ <input id="<?= /* @noEscape */ $block->getFieldId() ?>_<%- data.id %>_option_id" -+ name="<?= /* @noEscape */ $block->getFieldName() ?>[<%- data.id %>][option_id]" -+ type="hidden" -+ value="<%- data.option_id %>"/> -+ <input name="<?= /* @noEscape */ $block->getFieldName() ?>[<%- data.id %>][sort_order]" -+ type="hidden" -+ value="<%- data.sort_order %>"/> - - <div class="field field-option-title required"> -- <label class="label" for="<?= /* @escapeNotVerified */ $block->getFieldId() ?>_<%- data.id %>_title"> -- <?= /* @escapeNotVerified */ __('Option Title') ?> -+ <label class="label" for="<?= /* @noEscape */ $block->getFieldId() ?>_<%- data.id %>_title"> -+ <?= $block->escapeHtml(__('Option Title')) ?> - </label> - <div class="control"> -- <input id="<?= /* @escapeNotVerified */ $block->getFieldId() ?>_<%- data.id %>_title" -- name="<?= /* @escapeNotVerified */ $block->getFieldName() ?>[<%- data.id %>][title]" -+ <input id="<?= /* @noEscape */ $block->getFieldId() ?>_<%- data.id %>_title" -+ name="<?= /* @noEscape */ $block->getFieldName() ?>[<%- data.id %>][title]" - class="required-entry input-text" - type="text" - value="<%- data.title %>" -@@ -54,8 +75,8 @@ - </div> - - <div class="field field-option-input-type required"> -- <label class="label" for="<?= /* @escapeNotVerified */ $block->getFieldId() ?>_<%- data.id %>_title"> -- <?= /* @escapeNotVerified */ __('Input Type') ?> -+ <label class="label" for="<?= /* @noEscape */ $block->getFieldId() ?>_<%- data.id %>_title"> -+ <?= $block->escapeHtml(__('Input Type')) ?> - </label> - <div class="control opt-type"> - <?= $block->getTypeSelectHtml() ?> -@@ -64,9 +85,12 @@ - - <div class="field field-option-req"> - <div class="control"> -- <input id="<?= /* @escapeNotVerified */ $block->getFieldId() ?>_<%- data.id %>_required" class="is-required" type="checkbox" checked="checked"/> -+ <input id="<?= /* @noEscape */ $block->getFieldId() ?>_<%- data.id %>_required" -+ class="is-required" -+ type="checkbox" -+ checked="checked"/> - <label for="field-option-req"> -- <?= /* @escapeNotVerified */ __('Required') ?> -+ <?= $block->escapeHtml(__('Required')) ?> - </label> - <span style="display:none"><?= $block->getRequireSelectHtml() ?></span> - </div> -@@ -78,7 +102,7 @@ - </script> - - <div id="import-container" style="display: none;"></div> --<?php if (!$block->isReadonly()): ?> -+<?php if (!$block->isReadonly()) :?> - <div><input type="hidden" name="affect_product_custom_options" value="1"/></div> - <?php endif; ?> - <script> -@@ -89,21 +113,21 @@ require([ - - jQuery(function ($) { - var fieldSet = $('[data-block=product-custom-options]'); -- fieldSet.customOptions(<?php /* @escapeNotVerified */ echo $this->helper('Magento\Framework\Json\Helper\Data')->jsonEncode( -+ fieldSet.customOptions(<?= /* @noEscape */ $this->helper(Magento\Framework\Json\Helper\Data::class)->jsonEncode( - [ - 'fieldId' => $block->getFieldId(), -- 'productGridUrl' => $block->getProductGridUrl(), -+ 'productGridUrl' => $block->escapeUrl($block->getProductGridUrl()), - 'formKey' => $block->getFormKey(), -- 'customOptionsUrl' => $block->getCustomOptionsUrl(), -- 'isReadonly' => $block->isReadonly(), -- 'itemCount' => $block->getItemCount(), -- 'currentProductId' => $block->getCurrentProductId(), -+ 'customOptionsUrl' => $block->escapeUrl($block->getCustomOptionsUrl()), -+ 'isReadonly' => (bool) $block->isReadonly(), -+ 'itemCount' => (int) $block->getItemCount(), -+ 'currentProductId' => (int) $block->getCurrentProductId(), - ] - )?>); - //adding data to templates - <?php /** @var $_value \Magento\Framework\DataObject */ ?> -- <?php foreach ($block->getOptionValues() as $_value): ?> -- fieldSet.customOptions('addOption', <?= /* @escapeNotVerified */ $_value->toJson() ?>); -+ <?php foreach ($block->getOptionValues() as $_value) :?> -+ fieldSet.customOptions('addOption', <?= /* @noEscape */ $_value->toJson() ?>); - <?php endforeach; ?> - }); - -diff --git a/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/edit/options/type/date.phtml b/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/edit/options/type/date.phtml -index 07ce6e5d862..2063609bf05 100644 ---- a/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/edit/options/type/date.phtml -+++ b/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/edit/options/type/date.phtml -@@ -4,8 +4,6 @@ - * See COPYING.txt for license details. - */ - --// @codingStandardsIgnoreFile -- - ?> - <?php /** @var $block \Magento\Catalog\Block\Adminhtml\Product\Edit\Tab\Options\Type\Date */ ?> - <script id="custom-option-date-type-template" type="text/x-magento-template"> -@@ -14,10 +12,10 @@ - <thead> - <tr class="headings"> - <?php if ($block->getCanReadPrice() !== false) : ?> -- <th><?= /* @escapeNotVerified */ __('Price') ?></th> -- <th><?= /* @escapeNotVerified */ __('Price Type') ?></th> -+ <th><?= $block->escapeHtml(__('Price')) ?></th> -+ <th><?= $block->escapeHtml(__('Price Type')) ?></th> - <?php endif; ?> -- <th><?= /* @escapeNotVerified */ __('SKU') ?></th> -+ <th><?= $block->escapeHtml(__('SKU')) ?></th> - </tr> - </thead> - <tr> -diff --git a/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/edit/options/type/file.phtml b/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/edit/options/type/file.phtml -index 693c98fc02c..c0e61c5de99 100644 ---- a/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/edit/options/type/file.phtml -+++ b/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/edit/options/type/file.phtml -@@ -4,8 +4,6 @@ - * See COPYING.txt for license details. - */ - --// @codingStandardsIgnoreFile -- - ?> - <?php /** @var $block \Magento\Catalog\Block\Adminhtml\Product\Edit\Tab\Options\Type\File */ ?> - <script id="custom-option-file-type-template" type="text/x-magento-template"> -@@ -14,27 +12,27 @@ - <thead> - <tr> - <?php if ($block->getCanReadPrice() !== false) : ?> -- <th><?= /* @escapeNotVerified */ __('Price') ?></th> -- <th><?= /* @escapeNotVerified */ __('Price Type') ?></th> -+ <th><?= $block->escapeHtml(__('Price')) ?></th> -+ <th><?= $block->escapeHtml(__('Price Type')) ?></th> - <?php endif; ?> -- <th><?= /* @escapeNotVerified */ __('SKU') ?></th> -- <th><?= /* @escapeNotVerified */ __('Compatible File Extensions') ?></th> -- <th><?= /* @escapeNotVerified */ __('Maximum Image Size') ?></th> -+ <th><?= $block->escapeHtml(__('SKU')) ?></th> -+ <th><?= $block->escapeHtml(__('Compatible File Extensions')) ?></th> -+ <th><?= $block->escapeHtml(__('Maximum Image Size')) ?></th> - </tr> - </thead> - <tr> - <?php if ($block->getCanReadPrice() !== false) : ?> -- <td class="opt-price"> -- <input name="product[options][<%- data.option_id %>][price]" data-store-label="<%- data.price %>" -- class="input-text validate-zero-or-greater" type="text" value="<%- data.price %>" -- <?php if ($block->getCanEditPrice() === false) : ?> -- disabled="disabled" -- <?php endif; ?>> -- </td> -- <td class="opt-price-type"><?= $block->getPriceTypeSelectHtml('data-attr="price-type"') ?><%- data.checkboxScopePrice %></td> -+ <td class="opt-price"> -+ <input name="product[options][<%- data.option_id %>][price]" data-store-label="<%- data.price %>" -+ class="input-text validate-zero-or-greater" type="text" value="<%- data.price %>" -+ <?php if ($block->getCanEditPrice() === false) : ?> -+ disabled="disabled" -+ <?php endif; ?>> -+ </td> -+ <td class="opt-price-type"><?= $block->getPriceTypeSelectHtml('data-attr="price-type"') ?><%- data.checkboxScopePrice %></td> - <?php else : ?> -- <input name="product[options][<%- data.option_id %>][price]" type="hidden"> -- <input id="product_option_<%- data.option_id %>_price_type" name="product[options][<%- data.option_id %>][price_type]" type="hidden"> -+ <input name="product[options][<%- data.option_id %>][price]" type="hidden"> -+ <input id="product_option_<%- data.option_id %>_price_type" name="product[options][<%- data.option_id %>][price_type]" type="hidden"> - <?php endif; ?> - <td> - <input name="product[options][<%- data.option_id %>][sku]" class="input-text" type="text" value="<%- data.sku %>"> -@@ -42,10 +40,15 @@ - <td> - <input name="product[options][<%- data.option_id %>][file_extension]" class="input-text" type="text" value="<%- data.file_extension %>"> - </td> -- <td class="col-file"><?php /* @escapeNotVerified */ echo __('%1 <span>x</span> %2 <span>px.</span>', -- '<input class="input-text" type="text" name="product[options][<%- data.option_id %>][image_size_x]" value="<%- data.image_size_x %>">', -- '<input class="input-text" type="text" name="product[options][<%- data.option_id %>][image_size_y]" value="<%- data.image_size_y %>">') ?> -- <div class="note"><?= /* @escapeNotVerified */ __('Please leave blank if it is not an image.') ?></div> -+ <td class="col-file"><?= $block->escapeHtml( -+ __( -+ '%1 <span>x</span> %2 <span>px.</span>', -+ '<input class="input-text" type="text" name="product[options][<%- data.option_id %>][image_size_x]" value="<%- data.image_size_x %>">', -+ '<input class="input-text" type="text" name="product[options][<%- data.option_id %>][image_size_y]" value="<%- data.image_size_y %>">' -+ ), -+ ['span', 'input'] -+ ) ?> -+ <div class="note"><?= $block->escapeHtml(__('Please leave blank if it is not an image.')) ?></div> - </td> - </tr> - </table> -diff --git a/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/edit/options/type/select.phtml b/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/edit/options/type/select.phtml -index e8c398228a4..c7ff03a08d9 100644 ---- a/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/edit/options/type/select.phtml -+++ b/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/edit/options/type/select.phtml -@@ -4,8 +4,6 @@ - * See COPYING.txt for license details. - */ - --// @codingStandardsIgnoreFile -- - ?> - <?php /** @var $block \Magento\Catalog\Block\Adminhtml\Product\Edit\Tab\Options\Type\Select */ ?> - <script id="custom-option-select-type-template" type="text/x-magento-template"> -@@ -14,12 +12,12 @@ - <thead> - <tr> - <th class="col-draggable"> </th> -- <th class="col-name required"><?= /* @escapeNotVerified */ __('Title') ?><span class="required">*</span></th> -+ <th class="col-name required"><?= $block->escapeHtml(__('Title')) ?><span class="required">*</span></th> - <?php if ($block->getCanReadPrice() !== false) : ?> -- <th class="col-price"><?= /* @escapeNotVerified */ __('Price') ?></th> -- <th class="col-price-type"><?= /* @escapeNotVerified */ __('Price Type') ?></th> -+ <th class="col-price"><?= $block->escapeHtml(__('Price')) ?></th> -+ <th class="col-price-type"><?= $block->escapeHtml(__('Price Type')) ?></th> - <?php endif; ?> -- <th class="col-sku"><?= /* @escapeNotVerified */ __('SKU') ?></th> -+ <th class="col-sku"><?= $block->escapeHtml(__('SKU')) ?></th> - <th class="col-actions"> </th> - </tr> - </thead> -@@ -38,7 +36,7 @@ - <tr id="product_option_<%- data.id %>_select_<%- data.select_id %>"> - <td class="col-draggable"> - <div data-role="draggable-handle" class="draggable-handle" -- title="<?= /* @escapeNotVerified */ __('Sort Custom Option') ?>"></div> -+ title="<?= $block->escapeHtmlAttr(__('Sort Custom Option')) ?>"></div> - <input name="product[options][<%- data.id %>][values][<%- data.select_id %>][sort_order]" type="hidden" value="<%- data.sort_order %>"> - </td> - <td class="col-name select-opt-title"> -@@ -58,7 +56,7 @@ - <?php endif; ?>> - </td> - <td class="col-price-type select-opt-price-type"> -- <?= /* @escapeNotVerified */ $block->getPriceTypeSelectHtml('data-attr="price-type" <% if (typeof data.scopePriceDisabled != "undefined" && data.scopePriceDisabled != null) { %> disabled="disabled" <% } %>') ?><%- data.checkboxScopePrice %> -+ <?= /* @noEscape */ $block->getPriceTypeSelectHtml('data-attr="price-type" <% if (typeof data.scopePriceDisabled != "undefined" && data.scopePriceDisabled != null) { %> disabled="disabled" <% } %>') ?><%- data.checkboxScopePrice %> - </td> - <?php else : ?> - <input id="product_option_<%- data.id %>_select_<%- data.select_id %>_price" name="product[options][<%- data.id %>][values][<%- data.select_id %>][price]" type="hidden"> -diff --git a/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/edit/options/type/text.phtml b/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/edit/options/type/text.phtml -index c9d7190589f..89da5d633ef 100644 ---- a/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/edit/options/type/text.phtml -+++ b/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/edit/options/type/text.phtml -@@ -4,8 +4,6 @@ - * See COPYING.txt for license details. - */ - --// @codingStandardsIgnoreFile -- - ?> - <?php /** @var $block \Magento\Catalog\Block\Adminhtml\Product\Edit\Tab\Options\Type\Text */ ?> - <script id="custom-option-text-type-template" type="text/x-magento-template"> -@@ -14,11 +12,11 @@ - <thead> - <tr> - <?php if ($block->getCanReadPrice() !== false) : ?> -- <th class="type-price"><?= /* @escapeNotVerified */ __('Price') ?></th> -- <th class="type-type"><?= /* @escapeNotVerified */ __('Price Type') ?></th> -+ <th class="type-price"><?= $block->escapeHtml(__('Price')) ?></th> -+ <th class="type-type"><?= $block->escapeHtml(__('Price Type')) ?></th> - <?php endif; ?> -- <th class="type-sku"><?= /* @escapeNotVerified */ __('SKU') ?></th> -- <th class="type-last last"><?= /* @escapeNotVerified */ __('Max Characters') ?></th> -+ <th class="type-sku"><?= $block->escapeHtml(__('SKU')) ?></th> -+ <th class="type-last last"><?= $block->escapeHtml(__('Max Characters')) ?></th> - </tr> - </thead> - <tr> -diff --git a/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/edit/price/tier.phtml b/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/edit/price/tier.phtml -index 57715744823..e66a18c677c 100644 ---- a/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/edit/price/tier.phtml -+++ b/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/edit/price/tier.phtml -@@ -4,7 +4,7 @@ - * See COPYING.txt for license details. - */ - --// @codingStandardsIgnoreFile -+// phpcs:disable Magento2.Templates.ThisInTemplate.FoundThis - - /* @var $block \Magento\Catalog\Block\Adminhtml\Product\Edit\Tab\Price\Tier */ - $element = $block->getElement(); -@@ -20,28 +20,28 @@ $element = $block->getElement(); - - <?php $_showWebsite = $block->isShowWebsiteColumn(); ?> - <?php $_showWebsite = $block->isMultiWebsites(); ?> --<div class="field" id="attribute-<?= /* @escapeNotVerified */ $_htmlId ?>-container" data-attribute-code="<?= /* @escapeNotVerified */ $_htmlId ?>" -+<div class="field" id="attribute-<?= /* @noEscape */ $_htmlId ?>-container" data-attribute-code="<?= /* @noEscape */ $_htmlId ?>" - data-apply-to="<?= $block->escapeHtml( -- $this->helper('Magento\Framework\Json\Helper\Data')->jsonEncode( -+ $this->helper(Magento\Framework\Json\Helper\Data::class)->jsonEncode( - $element->hasEntityAttribute() ? $element->getEntityAttribute()->getApplyTo() : [] - ) - )?>"> -- <label class="label"><span><?= /* @escapeNotVerified */ $block->getElement()->getLabel() ?></span></label> -+ <label class="label"><span><?= $block->escapeHtml($block->getElement()->getLabel()) ?></span></label> - <div class="control"> - <table class="admin__control-table tiers_table" id="tiers_table"> - <thead> - <tr> -- <th class="col-websites" <?php if (!$_showWebsite): ?>style="display:none"<?php endif; ?>><?= /* @escapeNotVerified */ __('Web Site') ?></th> -- <th class="col-customer-group"><?= /* @escapeNotVerified */ __('Customer Group') ?></th> -- <th class="col-qty required"><?= /* @escapeNotVerified */ __('Quantity') ?></th> -- <th class="col-price required"><?= /* @escapeNotVerified */ $block->getPriceColumnHeader(__('Item Price')) ?></th> -- <th class="col-delete"><?= /* @escapeNotVerified */ __('Action') ?></th> -+ <th class="col-websites" <?php if (!$_showWebsite) :?>style="display:none"<?php endif; ?>><?= $block->escapeHtml(__('Web Site')) ?></th> -+ <th class="col-customer-group"><?= $block->escapeHtml(__('Customer Group')) ?></th> -+ <th class="col-qty required"><?= $block->escapeHtml(__('Quantity')) ?></th> -+ <th class="col-price required"><?= $block->escapeHtml($block->getPriceColumnHeader(__('Item Price'))) ?></th> -+ <th class="col-delete"><?= $block->escapeHtml(__('Action')) ?></th> - </tr> - </thead> -- <tbody id="<?= /* @escapeNotVerified */ $_htmlId ?>_container"></tbody> -+ <tbody id="<?= /* @noEscape */ $_htmlId ?>_container"></tbody> - <tfoot> - <tr> -- <td colspan="<?php if (!$_showWebsite): ?>4<?php else: ?>5<?php endif; ?>" class="col-actions-add"><?= $block->getAddButtonHtml() ?></td> -+ <td colspan="<?php if (!$_showWebsite) :?>4<?php else :?>5<?php endif; ?>" class="col-actions-add"><?= $block->getAddButtonHtml() ?></td> - </tr> - </tfoot> - </table> -@@ -55,39 +55,39 @@ require([ - - //<![CDATA[ - var tierPriceRowTemplate = '<tr>' -- + '<td class="col-websites"<?php if (!$_showWebsite): ?> style="display:none"<?php endif; ?>>' -- + '<select class="<?= /* @escapeNotVerified */ $_htmlClass ?> required-entry" name="<?= /* @escapeNotVerified */ $_htmlName ?>[<%- data.index %>][website_id]" id="tier_price_row_<%- data.index %>_website">' -- <?php foreach ($block->getWebsites() as $_websiteId => $_info): ?> -- + '<option value="<?= /* @escapeNotVerified */ $_websiteId ?>"><?= $block->escapeJs($_info['name']) ?><?php if (!empty($_info['currency'])): ?> [<?= $block->escapeHtml($_info['currency']) ?>]<?php endif; ?></option>' -+ + '<td class="col-websites"<?php if (!$_showWebsite) :?> style="display:none"<?php endif; ?>>' -+ + '<select class="<?= $block->escapeHtmlAttr($_htmlClass) ?> required-entry" name="<?= /* @noEscape */ $_htmlName ?>[<%- data.index %>][website_id]" id="tier_price_row_<%- data.index %>_website">' -+ <?php foreach ($block->getWebsites() as $_websiteId => $_info) :?> -+ + '<option value="<?= $block->escapeHtmlAttr($_websiteId) ?>"><?= $block->escapeHtml($_info['name']) ?><?php if (!empty($_info['currency'])) :?> [<?= $block->escapeHtml($_info['currency']) ?>]<?php endif; ?></option>' - <?php endforeach ?> - + '</select></td>' -- + '<td class="col-customer-group"><select class="<?= /* @escapeNotVerified */ $_htmlClass ?> custgroup required-entry" name="<?= /* @escapeNotVerified */ $_htmlName ?>[<%- data.index %>][cust_group]" id="tier_price_row_<%- data.index %>_cust_group">' -- <?php foreach ($block->getCustomerGroups() as $_groupId => $_groupName): ?> -- + '<option value="<?= /* @escapeNotVerified */ $_groupId ?>"><?= $block->escapeJs($_groupName) ?></option>' -+ + '<td class="col-customer-group"><select class="<?= $block->escapeHtmlAttr($_htmlClass) ?> custgroup required-entry" name="<?= /* @noEscape */ $_htmlName ?>[<%- data.index %>][cust_group]" id="tier_price_row_<%- data.index %>_cust_group">' -+ <?php foreach ($block->getCustomerGroups() as $_groupId => $_groupName) :?> -+ + '<option value="<?= $block->escapeHtmlAttr($_groupId) ?>"><?= $block->escapeHtml($_groupName) ?></option>' - <?php endforeach ?> - + '</select></td>' - + '<td class="col-qty">' -- + '<input class="<?= /* @escapeNotVerified */ $_htmlClass ?> qty required-entry validate-greater-than-zero" type="text" name="<?= /* @escapeNotVerified */ $_htmlName ?>[<%- data.index %>][price_qty]" value="<%- data.qty %>" id="tier_price_row_<%- data.index %>_qty" />' -- + '<span><?= /* @escapeNotVerified */ __("and above") ?></span>' -+ + '<input class="<?= $block->escapeHtmlAttr($_htmlClass) ?> qty required-entry validate-greater-than-zero" type="text" name="<?= /* @noEscape */ $_htmlName ?>[<%- data.index %>][price_qty]" value="<%- data.qty %>" id="tier_price_row_<%- data.index %>_qty" />' -+ + '<span><?= $block->escapeHtml(__("and above")) ?></span>' - + '</td>' -- + '<td class="col-price"><input class="<?= /* @escapeNotVerified */ $_htmlClass ?> required-entry <?= /* @escapeNotVerified */ $_priceValueValidation ?>" type="text" name="<?= /* @escapeNotVerified */ $_htmlName ?>[<%- data.index %>][price]" value="<%- data.price %>" id="tier_price_row_<%- data.index %>_price" /></td>' -- + '<td class="col-delete"><input type="hidden" name="<?= /* @escapeNotVerified */ $_htmlName ?>[<%- data.index %>][delete]" class="delete" value="" id="tier_price_row_<%- data.index %>_delete" />' -- + '<button title="<?= /* @escapeNotVerified */ $block->escapeHtml(__('Delete Tier')) ?>" type="button" class="action- scalable delete icon-btn delete-product-option" id="tier_price_row_<%- data.index %>_delete_button" onclick="return tierPriceControl.deleteItem(event);">' -- + '<span><?= /* @escapeNotVerified */ __("Delete") ?></span></button></td>' -+ + '<td class="col-price"><input class="<?= $block->escapeHtmlAttr($_htmlClass) ?> required-entry <?= $block->escapeHtmlAttr($_priceValueValidation) ?>" type="text" name="<?= /* @noEscape */ $_htmlName ?>[<%- data.index %>][price]" value="<%- data.price %>" id="tier_price_row_<%- data.index %>_price" /></td>' -+ + '<td class="col-delete"><input type="hidden" name="<?= /* @noEscape */ $_htmlName ?>[<%- data.index %>][delete]" class="delete" value="" id="tier_price_row_<%- data.index %>_delete" />' -+ + '<button title="<?= $block->escapeHtml(__('Delete Tier')) ?>" type="button" class="action- scalable delete icon-btn delete-product-option" id="tier_price_row_<%- data.index %>_delete_button" onclick="return tierPriceControl.deleteItem(event);">' -+ + '<span><?= $block->escapeHtml(__("Delete")) ?></span></button></td>' - + '</tr>'; - - var tierPriceControl = { - template: mageTemplate(tierPriceRowTemplate), - itemsCount: 0, - addItem : function () { -- <?php if ($_readonly): ?> -+ <?php if ($_readonly) :?> - if (arguments.length < 4) { - return; - } - <?php endif; ?> - var data = { -- website_id: '<?= /* @escapeNotVerified */ $block->getDefaultWebsite() ?>', -- group: '<?= /* @escapeNotVerified */ $block->getDefaultCustomerGroup() ?>', -+ website_id: '<?= (int) $block->getDefaultWebsite() ?>', -+ group: '<?= (int) $block->getDefaultCustomerGroup() ?>', - qty: '', - price: '', - readOnly: false, -@@ -104,7 +104,7 @@ var tierPriceControl = { - data.readOnly = arguments[4]; - } - -- Element.insert($('<?= /* @escapeNotVerified */ $_htmlId ?>_container'), { -+ Element.insert($('<?= $block->escapeJs($_htmlId) ?>_container'), { - bottom : this.template({ - data: data - }) -@@ -113,7 +113,7 @@ var tierPriceControl = { - $('tier_price_row_' + data.index + '_cust_group').value = data.group; - $('tier_price_row_' + data.index + '_website').value = data.website_id; - -- <?php if ($block->isShowWebsiteColumn() && !$block->isAllowChangeWebsite()):?> -+ <?php if ($block->isShowWebsiteColumn() && !$block->isAllowChangeWebsite()) :?> - var wss = $('tier_price_row_' + data.index + '_website'); - var txt = wss.options[wss.selectedIndex].text; - -@@ -128,11 +128,11 @@ var tierPriceControl = { - $('tier_price_row_'+data.index+'_delete_button').hide(); - } - -- <?php if ($_readonly): ?> -- $('<?= /* @escapeNotVerified */ $_htmlId ?>_container').select('input', 'select').each(this.disableElement); -- $('<?= /* @escapeNotVerified */ $_htmlId ?>_container').up('table').select('button').each(this.disableElement); -- <?php else: ?> -- $('<?= /* @escapeNotVerified */ $_htmlId ?>_container').select('input', 'select').each(function(el){ Event.observe(el, 'change', el.setHasChanges.bind(el)); }); -+ <?php if ($_readonly) :?> -+ $('<?= $block->escapeJs($_htmlId) ?>_container').select('input', 'select').each(this.disableElement); -+ $('<?= $block->escapeJs($_htmlId) ?>_container').up('table').select('button').each(this.disableElement); -+ <?php else :?> -+ $('<?= $block->escapeJs($_htmlId) ?>_container').select('input', 'select').each(function(el){ Event.observe(el, 'change', el.setHasChanges.bind(el)); }); - <?php endif; ?> - }, - disableElement: function(el) { -@@ -150,11 +150,11 @@ var tierPriceControl = { - return false; - } - }; --<?php foreach ($block->getValues() as $_item): ?> --tierPriceControl.addItem('<?= /* @escapeNotVerified */ $_item['website_id'] ?>', '<?= /* @escapeNotVerified */ $_item['cust_group'] ?>', '<?= /* @escapeNotVerified */ $_item['price_qty']*1 ?>', '<?= /* @escapeNotVerified */ $_item['price'] ?>', <?= (int)!empty($_item['readonly']) ?>); -+<?php foreach ($block->getValues() as $_item) :?> -+tierPriceControl.addItem('<?= $block->escapeJs($_item['website_id']) ?>', '<?= $block->escapeJs($_item['cust_group']) ?>', '<?= $_item['price_qty']*1 ?>', '<?= $block->escapeJs($_item['price']) ?>', <?= (int)!empty($_item['readonly']) ?>); - <?php endforeach; ?> --<?php if ($_readonly): ?> --$('<?= /* @escapeNotVerified */ $_htmlId ?>_container').up('table').select('button') -+<?php if ($_readonly) :?> -+$('<?= $block->escapeJs($_htmlId) ?>_container').up('table').select('button') - .each(tierPriceControl.disableElement); - <?php endif; ?> - -diff --git a/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/edit/serializer.phtml b/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/edit/serializer.phtml -index 44fdb75cdac..0c1da98c7d8 100644 ---- a/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/edit/serializer.phtml -+++ b/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/edit/serializer.phtml -@@ -4,9 +4,12 @@ - * See COPYING.txt for license details. - */ - --// @codingStandardsIgnoreFile -- -+/** @var $block \Magento\Catalog\Block\Adminhtml\Product\Edit\Tab\Ajax\Serializer */ - ?> - -+// phpcs:disable Magento2.Security.InsecureFunction.DiscouragedWithAlternative - <?php $_id = 'id_' . md5(microtime()) ?> --<input type="hidden" name="<?= /* @escapeNotVerified */ $block->getInputElementName() ?>" value="" id="<?= /* @escapeNotVerified */ $_id ?>" /> -+<input type="hidden" -+ name="<?= $block->escapeHtmlAttr($block->getInputElementName()) ?>" -+ value="" -+ id="<?= /* @noEscape */ $_id ?>" /> -diff --git a/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/edit/websites.phtml b/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/edit/websites.phtml -index 8f7f20f32d9..0193d7764cb 100644 ---- a/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/edit/websites.phtml -+++ b/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/edit/websites.phtml -@@ -4,16 +4,15 @@ - * See COPYING.txt for license details. - */ - --// @codingStandardsIgnoreFile -- -+/** @var $block \Magento\Catalog\Block\Adminhtml\Product\Edit\Tab\Websites */ - ?> - <fieldset id="grop_fields" class="fieldset"> -- <legend class="legend"><span><?= /* @escapeNotVerified */ __('Product In Websites') ?></span></legend> -+ <legend class="legend"><span><?= $block->escapeHtml(__('Product In Websites')) ?></span></legend> - <br> -- <?php if ($block->getProductId()): ?> -+ <?php if ($block->getProductId()) :?> - <div class="messages"> - <div class="message message-notice"> -- <?= /* @escapeNotVerified */ __('To hide an item in catalog or search results, set the status to "Disabled".') ?> -+ <?= $block->escapeHtml(__('To hide an item in catalog or search results, set the status to "Disabled".')) ?> - </div> - </div> - <?php endif; ?> -@@ -21,22 +20,36 @@ - <?= $block->getHintHtml() ?> - <div class="store-tree"> - <?php $_websites = $block->getWebsiteCollection() ?> -- <?php foreach ($_websites as $_website): ?> -+ <?php foreach ($_websites as $_website) :?> - <div class="website-name"> -- <input name="product[website_ids][]" value="<?= /* @escapeNotVerified */ $_website->getId() ?>" <?php if ($block->isReadonly()): ?> disabled="disabled"<?php endif;?> class="checkbox website-checkbox" id="product_website_<?= /* @escapeNotVerified */ $_website->getId() ?>" type="checkbox"<?php if ($block->hasWebsite($_website->getId()) || !$block->getProductId() && count($_websites) === 1): ?> checked="checked"<?php endif; ?> /> -- <label for="product_website_<?= /* @escapeNotVerified */ $_website->getId() ?>"><?= $block->escapeHtml($_website->getName()) ?></label> -+ <input name="product[website_ids][]" -+ value="<?= (int) $_website->getId() ?>" -+ <?php if ($block->isReadonly()) :?> -+ disabled="disabled" -+ <?php endif;?> -+ class="checkbox website-checkbox" -+ id="product_website_<?= (int) $_website->getId() ?>" -+ type="checkbox" -+ <?php if ($block->hasWebsite($_website->getId()) || !$block->getProductId() && count($_websites) === 1) :?> -+ checked="checked" -+ <?php endif; ?> -+ /> -+ <label for="product_website_<?= (int) $_website->getId() ?>"><?= $block->escapeHtml($_website->getName()) ?></label> - </div> -- <dl class="webiste-groups" id="product_website_<?= /* @escapeNotVerified */ $_website->getId() ?>_data"> -- <?php foreach ($block->getGroupCollection($_website) as $_group): ?> -+ <dl class="webiste-groups" id="product_website_<?= (int) $_website->getId() ?>_data"> -+ <?php foreach ($block->getGroupCollection($_website) as $_group) :?> - <dt><?= $block->escapeHtml($_group->getName()) ?></dt> - <dd> - <ul> -- <?php foreach ($block->getStoreCollection($_group) as $_store): ?> -+ <?php foreach ($block->getStoreCollection($_group) as $_store) :?> - <li> - <?= $block->escapeHtml($_store->getName()) ?> -- <?php if ($block->getWebsites() && !$block->hasWebsite($_website->getId())): ?> -- <span class="website-<?= /* @escapeNotVerified */ $_website->getId() ?>-select" style="display:none"> -- <?= __('(Copy data from: %1)', $block->getChooseFromStoreHtml($_store)) ?> -+ <?php if ($block->getWebsites() && !$block->hasWebsite($_website->getId())) :?> -+ <span class="website-<?= (int) $_website->getId() ?>-select" style="display:none"> -+ <?= $block->escapeHtml( -+ __('(Copy data from: %1)', $block->getChooseFromStoreHtml($_store)), -+ ['select', 'option', 'optgroup'] -+ ) ?> - </span> - <?php endif; ?> - </li> -diff --git a/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/helper/gallery.phtml b/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/helper/gallery.phtml -index 574c9ee81af..ca77aa28ea6 100644 ---- a/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/helper/gallery.phtml -+++ b/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/helper/gallery.phtml -@@ -4,7 +4,7 @@ - * See COPYING.txt for license details. - */ - --// @codingStandardsIgnoreFile -+// phpcs:disable Magento2.Templates.ThisInTemplate.FoundThis - - /** @var $block \Magento\Catalog\Block\Adminhtml\Product\Helper\Form\Gallery\Content */ - $elementName = $block->getElement()->getName() . '[images]'; -@@ -16,61 +16,60 @@ $formName = $block->getFormName(); - data-parent-component="<?= $block->escapeHtml($block->getData('config/parentComponent')) ?>" - data-images="<?= $block->escapeHtml($block->getImagesJson()) ?>" - data-types="<?= $block->escapeHtml( -- $this->helper('Magento\Framework\Json\Helper\Data')->jsonEncode($block->getImageTypes()) -+ $this->helper(Magento\Framework\Json\Helper\Data::class)->jsonEncode($block->getImageTypes()) - ) ?>" -- > -+> - <?php if (!$block->getElement()->getReadonly()) {?> - <div class="image image-placeholder"> - <?= $block->getUploaderHtml() ?> - <div class="product-image-wrapper"> - <p class="image-placeholder-text"> -- <?= /* @escapeNotVerified */ __('Browse to find or drag image here') ?> -+ <?= $block->escapeHtml(__('Browse to find or drag image here')) ?> - </p> - </div> - </div> - <?php } ?> - <?php foreach ($block->getImageTypes() as $typeData) { -- ?> -- <input name="<?= $block->escapeHtml($typeData['name']) ?>" -- data-form-part="<?= /* @escapeNotVerified */ $formName ?>" -- class="image-<?= $block->escapeHtml($typeData['code']) ?>" -+ ?> -+ <input name="<?= $block->escapeHtmlAttr($typeData['name']) ?>" -+ data-form-part="<?= $block->escapeHtmlAttr($formName) ?>" -+ class="image-<?= $block->escapeHtmlAttr($typeData['code']) ?>" - type="hidden" -- value="<?= $block->escapeHtml($typeData['value']) ?>"/> -- <?php -- -+ value="<?= $block->escapeHtmlAttr($typeData['value']) ?>"/> -+ <?php - } ?> - - <script id="<?= $block->getHtmlId() ?>-template" type="text/x-magento-template"> - <div class="image item<% if (data.disabled == 1) { %> hidden-for-front<% } %>" - data-role="image"> - <input type="hidden" -- name="<?= /* @escapeNotVerified */ $elementName ?>[<%- data.file_id %>][position]" -+ name="<?= $block->escapeHtmlAttr($elementName) ?>[<%- data.file_id %>][position]" - value="<%- data.position %>" -- data-form-part="<?= /* @escapeNotVerified */ $formName ?>" -+ data-form-part="<?= $block->escapeHtmlAttr($formName) ?>" - class="position"/> - <input type="hidden" -- name="<?= /* @escapeNotVerified */ $elementName ?>[<%- data.file_id %>][file]" -- data-form-part="<?= /* @escapeNotVerified */ $formName ?>" -+ name="<?= $block->escapeHtmlAttr($elementName) ?>[<%- data.file_id %>][file]" -+ data-form-part="<?= $block->escapeHtmlAttr($formName) ?>" - value="<%- data.file %>"/> - <input type="hidden" -- name="<?= /* @escapeNotVerified */ $elementName ?>[<%- data.file_id %>][value_id]" -- data-form-part="<?= /* @escapeNotVerified */ $formName ?>" -+ name="<?= $block->escapeHtmlAttr($elementName) ?>[<%- data.file_id %>][value_id]" -+ data-form-part="<?= $block->escapeHtmlAttr($formName) ?>" - value="<%- data.value_id %>"/> - <input type="hidden" -- name="<?= /* @escapeNotVerified */ $elementName ?>[<%- data.file_id %>][label]" -- data-form-part="<?= /* @escapeNotVerified */ $formName ?>" -+ name="<?= $block->escapeHtmlAttr($elementName) ?>[<%- data.file_id %>][label]" -+ data-form-part="<?= $block->escapeHtmlAttr($formName) ?>" - value="<%- data.label %>"/> - <input type="hidden" -- name="<?= /* @escapeNotVerified */ $elementName ?>[<%- data.file_id %>][disabled]" -- data-form-part="<?= /* @escapeNotVerified */ $formName ?>" -+ name="<?= $block->escapeHtmlAttr($elementName) ?>[<%- data.file_id %>][disabled]" -+ data-form-part="<?= $block->escapeHtmlAttr(formName) ?>" - value="<%- data.disabled %>"/> - <input type="hidden" -- name="<?= /* @escapeNotVerified */ $elementName ?>[<%- data.file_id %>][media_type]" -- data-form-part="<?= /* @escapeNotVerified */ $formName ?>" -+ name="<?= $block->escapeHtmlAttr($elementName) ?>[<%- data.file_id %>][media_type]" -+ data-form-part="<?= $block->escapeHtmlAttr($formName) ?>" - value="image"/> - <input type="hidden" -- name="<?= /* @escapeNotVerified */ $elementName ?>[<%- data.file_id %>][removed]" -- data-form-part="<?= /* @escapeNotVerified */ $formName ?>" -+ name="<?= $block->escapeHtmlAttr($elementName) ?>[<%- data.file_id %>][removed]" -+ data-form-part="<?= $block->escapeHtmlAttr($formName) ?>" - value="" - class="is-removed"/> - -@@ -84,21 +83,21 @@ $formName = $block->getFormName(); - <button type="button" - class="action-remove" - data-role="delete-button" -- title="<?= /* @escapeNotVerified */ __('Delete image') ?>"> -+ title="<?= $block->escapeHtmlAttr(__('Delete image')) ?>"> - <span> -- <?= /* @escapeNotVerified */ __('Delete image') ?> -+ <?= $block->escapeHtml(__('Delete image')) ?> - </span> - </button> - <div class="draggable-handle"></div> - </div> -- <div class="image-fade"><span><?= /* @escapeNotVerified */ __('Hidden') ?></span></div> -+ <div class="image-fade"><span><?= $block->escapeHtml(__('Hidden')) ?></span></div> - </div> - - - <div class="item-description"> - <div class="item-title" data-role="img-title"><%- data.label %></div> - <div class="item-size"> -- <span data-role="image-dimens"></span>, <span data-role="image-size"><%- data.sizeLabel %></span> -+ <span data-role="image-dimens"></span>, <span data-role="image-size"><%- data.sizeLabel %></span> - </div> - </div> - -@@ -106,12 +105,9 @@ $formName = $block->getFormName(); - <?php - foreach ($block->getImageTypes() as $typeData) { - ?> -- <li data-role-code="<?php /* @escapeNotVerified */ echo $block->escapeHtml( -- $typeData['code'] -- ) ?>" class="item-role item-role-<?php /* @escapeNotVerified */ echo $block->escapeHtml( -- $typeData['code'] -- ) ?>"> -- <?= /* @escapeNotVerified */ $block->escapeHtml($typeData['label']) ?> -+ <li data-role-code="<?= $block->escapeHtmlAttr($typeData['code']) ?>" -+ class="item-role item-role-<?= $block->escapeHtmlAttr($typeData['code']) ?>"> -+ <?= $block->escapeHtml($typeData['label']) ?> - </li> - <?php - } -@@ -121,98 +117,94 @@ $formName = $block->getFormName(); - </script> - - <script data-role="img-dialog-container-tmpl" type="text/x-magento-template"> -- <div class="image-panel" data-role="dialog"> -- </div> -+ <div class="image-panel" data-role="dialog"> -+ </div> - </script> - - <script data-role="img-dialog-tmpl" type="text/x-magento-template"> -- <div class="image-panel-preview"> -- <img src="<%- data.url %>" alt="<%- data.label %>" /> -- </div> -- <div class="image-panel-controls"> -- <strong class="image-name"><%- data.label %></strong> -+ <div class="image-panel-preview"> -+ <img src="<%- data.url %>" alt="<%- data.label %>" /> -+ </div> -+ <div class="image-panel-controls"> -+ <strong class="image-name"><%- data.label %></strong> - -- <fieldset class="admin__fieldset fieldset-image-panel"> -- <div class="admin__field field-image-description"> -- <label class="admin__field-label" for="image-description"> -- <span><?= /* @escapeNotVerified */ __('Alt Text') ?></span> -- </label> -+ <fieldset class="admin__fieldset fieldset-image-panel"> -+ <div class="admin__field field-image-description"> -+ <label class="admin__field-label" for="image-description"> -+ <span><?= $block->escapeHtml(__('Alt Text')) ?></span> -+ </label> - -- <div class="admin__field-control"> -+ <div class="admin__field-control"> - <textarea data-role="image-description" - rows="3" - class="admin__control-textarea" -- name="<?php /* @escapeNotVerified */ -- echo $elementName -- ?>[<%- data.file_id %>][label]"><%- data.label %></textarea> -- </div> -- </div> -+ name="<?= $block->escapeHtmlAttr($elementName) ?>[<%- data.file_id %>][label]"><%- data.label %></textarea> -+ </div> -+ </div> - -- <div class="admin__field field-image-role"> -- <label class="admin__field-label"> -- <span><?= /* @escapeNotVerified */ __('Role') ?></span> -- </label> -- <div class="admin__field-control"> -- <ul class="multiselect-alt"> -- <?php -- foreach ($block->getMediaAttributes() as $attribute) : -- ?> -- <li class="item"> -- <label> -- <input class="image-type" -- data-role="type-selector" -- data-form-part="<?= /* @escapeNotVerified */ $formName ?>" -- type="checkbox" -- value="<?php /* @escapeNotVerified */ echo $block->escapeHtml( -- $attribute->getAttributeCode() -- ) ?>" -- /> -- <?php /* @escapeNotVerified */ echo $block->escapeHtml( -- $attribute->getFrontendLabel() -- ) ?> -- </label> -- </li> -+ <div class="admin__field field-image-role"> -+ <label class="admin__field-label"> -+ <span><?= $block->escapeHtml(__('Role')) ?></span> -+ </label> -+ <div class="admin__field-control"> -+ <ul class="multiselect-alt"> -+ <?php -+ foreach ($block->getMediaAttributes() as $attribute) : -+ ?> -+ <li class="item"> -+ <label> -+ <input class="image-type" -+ data-role="type-selector" -+ data-form-part="<?= $block->escapeHtmlAttr($formName) ?>" -+ type="checkbox" -+ value="<?= $block->escapeHtmlAttr($attribute->getAttributeCode()) ?>" -+ /> -+ <?= $block->escapeHtml( -+ $attribute->getFrontendLabel() -+ ) ?> -+ </label> -+ </li> - <?php - endforeach; -- ?> -- </ul> -- </div> -+ ?> -+ </ul> - </div> -+ </div> - -- <div class="admin__field admin__field-inline field-image-size" data-role="size"> -- <label class="admin__field-label"> -- <span><?= /* @escapeNotVerified */ __('Image Size') ?></span> -- </label> -- <div class="admin__field-value" data-message="<?= /* @escapeNotVerified */ __('{size}') ?>"></div> -- </div> -+ <div class="admin__field admin__field-inline field-image-size" data-role="size"> -+ <label class="admin__field-label"> -+ <span><?= $block->escapeHtml(__('Image Size')) ?></span> -+ </label> -+ <div class="admin__field-value" data-message="<?= $block->escapeHtmlAttr(__('{size}')) ?>"></div> -+ </div> - -- <div class="admin__field admin__field-inline field-image-resolution" data-role="resolution"> -- <label class="admin__field-label"> -- <span><?= /* @escapeNotVerified */ __('Image Resolution') ?></span> -- </label> -- <div class="admin__field-value" data-message="<?= /* @escapeNotVerified */ __('{width}^{height} px') ?>"></div> -- </div> -+ <div class="admin__field admin__field-inline field-image-resolution" data-role="resolution"> -+ <label class="admin__field-label"> -+ <span><?= $block->escapeHtml(__('Image Resolution')) ?></span> -+ </label> -+ <div class="admin__field-value" data-message="<?= $block->escapeHtmlAttr(__('{width}^{height} px')) ?>"></div> -+ </div> - -- <div class="admin__field field-image-hide"> -- <div class="admin__field-control"> -- <div class="admin__field admin__field-option"> -- <input type="checkbox" -- id="hide-from-product-page" -- data-role="visibility-trigger" -- data-form-part="<?= /* @escapeNotVerified */ $formName ?>" -- value="1" -- class="admin__control-checkbox" -- name="<?= /* @escapeNotVerified */ $elementName ?>[<%- data.file_id %>][disabled]" -- <% if (data.disabled == 1) { %>checked="checked"<% } %> /> -- -- <label for="hide-from-product-page" class="admin__field-label"> -- <?= /* @escapeNotVerified */ __('Hide from Product Page') ?> -- </label> -- </div> -+ <div class="admin__field field-image-hide"> -+ <div class="admin__field-control"> -+ <div class="admin__field admin__field-option"> -+ <input type="checkbox" -+ id="hide-from-product-page" -+ data-role="visibility-trigger" -+ data-form-part="<?= $block->escapeHtmlAttr($formName) ?>" -+ value="1" -+ class="admin__control-checkbox" -+ name="<?= $block->escapeHtmlAttr($elementName) ?>[<%- data.file_id %>][disabled]" -+ <% if (data.disabled == 1) { %>checked="checked"<% } %> /> -+ -+ <label for="hide-from-product-page" class="admin__field-label"> -+ <?= $block->escapeHtml(__('Hide from Product Page')) ?> -+ </label> - </div> - </div> -- </fieldset> -- </div> -+ </div> -+ </fieldset> -+ </div> - </script> - <?= $block->getChildHtml('new-video') ?> - </div> -diff --git a/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/js.phtml b/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/js.phtml -index 4134392c0f5..0a13aee5930 100644 ---- a/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/js.phtml -+++ b/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/js.phtml -@@ -4,7 +4,7 @@ - * See COPYING.txt for license details. - */ - --// @codingStandardsIgnoreFile -+// phpcs:disable Magento2.Templates.ThisInTemplate.FoundThis - - /** @var \Magento\Catalog\Block\Adminhtml\Product\Edit\Js $block */ - ?> -@@ -30,8 +30,8 @@ function registerTaxRecalcs() { - Event.observe($('tax_class_id'), 'change', recalculateTax); - } - --var priceFormat = <?= /* @escapeNotVerified */ $this->helper('Magento\Tax\Helper\Data')->getPriceFormat($block->getStore()) ?>; --var taxRates = <?= /* @escapeNotVerified */ $block->getAllRatesByProductClassJson() ?>; -+var priceFormat = <?= /* @noEscape */ $this->helper(Magento\Tax\Helper\Data::class)->getPriceFormat($block->getStore()) ?>; -+var taxRates = <?= /* @noEscape */ $block->getAllRatesByProductClassJson() ?>; - - function recalculateTax() { - if (typeof dynamicTaxes == 'undefined') { -@@ -75,10 +75,10 @@ function bindActiveProductTab(event, ui) { - jQuery(document).on('tabsactivate', bindActiveProductTab); - - // bind active tab --<?php if ($tabsBlock = $block->getLayout()->getBlock('product_tabs')): ?> -+<?php if ($tabsBlock = $block->getLayout()->getBlock('product_tabs')) :?> - jQuery(function () { -- if (jQuery('#<?= /* @escapeNotVerified */ $tabsBlock->getId() ?>').length && jQuery('#<?= /* @escapeNotVerified */ $tabsBlock->getId() ?>').is(':mage-tabs')) { -- var activeAnchor = jQuery('#<?= /* @escapeNotVerified */ $tabsBlock->getId() ?>').tabs('activeAnchor'); -+ if (jQuery('#<?= $block->escapeJs($tabsBlock->getId()) ?>').length && jQuery('#<?= $block->escapeJs($tabsBlock->getId()) ?>').is(':mage-tabs')) { -+ var activeAnchor = jQuery('#<?= $block->escapeJs($tabsBlock->getId()) ?>').tabs('activeAnchor'); - if (activeAnchor && $('store_switcher')) { - $('store_switcher').switchParams = 'active_tab/' + activeAnchor.prop('name') + '/'; - } -diff --git a/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/tab/alert.phtml b/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/tab/alert.phtml -index 5b07121de49..7c3bee3d4d2 100644 ---- a/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/tab/alert.phtml -+++ b/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/tab/alert.phtml -@@ -4,8 +4,6 @@ - * See COPYING.txt for license details. - */ - --// @codingStandardsIgnoreFile -- - ?> - <?php - /** -@@ -14,7 +12,7 @@ - ?> - <div id="alert_messages_block"><?= $block->getMessageHtml() ?></div> - <div> -- <h4 class="icon-head head-edit-form"><?= /* @escapeNotVerified */ __('Product Alerts') ?></h4> -+ <h4 class="icon-head head-edit-form"><?= $block->escapeHtml(__('Product Alerts')) ?></h4> - </div> - <div class="clear"></div> - <?= $block->getAccordionHtml() ?> -diff --git a/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/tab/inventory.phtml b/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/tab/inventory.phtml -index 2c62bbf8db3..5028d3c1e83 100644 ---- a/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/tab/inventory.phtml -+++ b/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/tab/inventory.phtml -@@ -4,382 +4,448 @@ - * See COPYING.txt for license details. - */ - --// @codingStandardsIgnoreFile -- - /** @var $block \Magento\Catalog\Block\Adminhtml\Product\Edit\Tab\Inventory */ - ?> --<?php if ($block->isReadonly()): ?> --<?php $_readonly = ' disabled="disabled" '; ?> --<?php else: ?> --<?php $_readonly = ''; ?> -+<?php if ($block->isReadonly()) :?> -+ <?php $_readonly = ' disabled="disabled" '; ?> -+<?php else :?> -+ <?php $_readonly = ''; ?> - <?php endif; ?> - <fieldset class="fieldset form-inline"> --<legend class="legend"><span><?= /* @escapeNotVerified */ __('Advanced Inventory') ?></span></legend> --<br> --<div id="table_cataloginventory"> --<div class="field"> -- <label class="label" for="inventory_manage_stock"> -- <span><?= /* @escapeNotVerified */ __('Manage Stock') ?></span> -- </label> -- <div class="control"> -- <select id="inventory_manage_stock" name="<?= /* @escapeNotVerified */ $block->getFieldSuffix() ?>[stock_data][manage_stock]" <?= /* @escapeNotVerified */ $_readonly ?>> -- <option value="1"><?= /* @escapeNotVerified */ __('Yes') ?></option> -- <option value="0"<?php if ($block->getFieldValue('manage_stock') == 0): ?> selected="selected"<?php endif; ?>><?= /* @escapeNotVerified */ __('No') ?></option> -- </select> -- <input type="hidden" id="inventory_manage_stock_default" value="<?= /* @escapeNotVerified */ $block->getDefaultConfigValue('manage_stock') ?>"> -- <?php $_checked = ($block->getFieldValue('use_config_manage_stock') || $block->isNew()) ? 'checked="checked"' : '' ?> -- <input type="checkbox" id="inventory_use_config_manage_stock" name="<?= /* @escapeNotVerified */ $block->getFieldSuffix() ?>[stock_data][use_config_manage_stock]" value="1" <?= /* @escapeNotVerified */ $_checked ?> onclick="toggleValueElements(this, this.parentNode);" <?= /* @escapeNotVerified */ $_readonly ?>> -- <label for="inventory_use_config_manage_stock"><?= /* @escapeNotVerified */ __('Use Config Settings') ?></label> -- <?php if (!$block->isReadonly()): ?> -- <script> --require(['prototype'], function(){ --toggleValueElements($('inventory_use_config_manage_stock'), $('inventory_use_config_manage_stock').parentNode); --}); --</script> -- <?php endif; ?> -- </div> -- <?php if (!$block->isSingleStoreMode()): ?> -- <div class="field-service"><?= /* @escapeNotVerified */ __('[GLOBAL]') ?></div> -- <?php endif; ?> --</div> -+ <legend class="legend"><span><?= $block->escapeHtml(__('Advanced Inventory')) ?></span></legend> -+ <br> -+ <div id="table_cataloginventory"> -+ <div class="field"> -+ <label class="label" for="inventory_manage_stock"> -+ <span><?= $block->escapeHtml(__('Manage Stock')) ?></span> -+ </label> -+ <div class="control"> -+ <select id="inventory_manage_stock" -+ name="<?= /* @noEscape */ $block->getFieldSuffix() ?>[stock_data][manage_stock]" <?= /* @noEscape */ $_readonly ?>> -+ <option value="1"><?= $block->escapeHtml(__('Yes')) ?></option> -+ <option value="0"<?php if ($block->getFieldValue('manage_stock') == 0) :?> selected="selected"<?php endif; ?>><?= $block->escapeHtml(__('No')) ?></option> -+ </select> -+ <input type="hidden" -+ id="inventory_manage_stock_default" -+ value="<?= $block->escapeHtmlAttr($block->getDefaultConfigValue('manage_stock')) ?>"> -+ <?php $_checked = ($block->getFieldValue('use_config_manage_stock') || $block->isNew()) ? 'checked="checked"' : '' ?> -+ <input type="checkbox" -+ id="inventory_use_config_manage_stock" -+ name="<?= /* @noEscape */ $block->getFieldSuffix() ?>[stock_data][use_config_manage_stock]" -+ value="1" <?= /* @noEscape */ $_checked ?> -+ onclick="toggleValueElements(this, this.parentNode);" <?= /* @noEscape */ $_readonly ?>> -+ <label for="inventory_use_config_manage_stock"><?= $block->escapeHtml(__('Use Config Settings')) ?></label> -+ <?php if (!$block->isReadonly()) :?> -+ <script> -+ require(['prototype'], function(){ -+ toggleValueElements($('inventory_use_config_manage_stock'), $('inventory_use_config_manage_stock').parentNode); -+ }); -+ </script> -+ <?php endif; ?> -+ </div> -+ <?php if (!$block->isSingleStoreMode()) :?> -+ <div class="field-service"><?= $block->escapeHtml(__('[GLOBAL]')) ?></div> -+ <?php endif; ?> -+ </div> - --<?php if (!$block->getProduct()->isComposite()): ?> --<div class="field"> -- <label class="label" for="inventory_qty"> -- <span><?= /* @escapeNotVerified */ __('Qty') ?></span> -- </label> -- <div class="control"> -- <?php if (!$_readonly): ?> -- <input type="hidden" id="original_inventory_qty" name="<?= /* @escapeNotVerified */ $block->getFieldSuffix() ?>[stock_data][original_inventory_qty]" value="<?= /* @escapeNotVerified */ $block->getFieldValue('qty') * 1 ?>"> -- <?php endif;?> -- <input type="text" class="input-text validate-number" id="inventory_qty" name="<?= /* @escapeNotVerified */ $block->getFieldSuffix() ?>[stock_data][qty]" value="<?= /* @escapeNotVerified */ $block->getFieldValue('qty') * 1 ?>" <?= /* @escapeNotVerified */ $_readonly ?>> -- </div> -- <?php if (!$block->isSingleStoreMode()): ?> -- <div class="field-service"><?= /* @escapeNotVerified */ __('[GLOBAL]') ?></div> -- <?php endif; ?> --</div> -+ <?php if (!$block->getProduct()->isComposite()) :?> -+ <div class="field"> -+ <label class="label" for="inventory_qty"> -+ <span><?= $block->escapeHtml(__('Qty')) ?></span> -+ </label> -+ <div class="control"> -+ <?php if (!$_readonly) :?> -+ <input type="hidden" -+ id="original_inventory_qty" -+ name="<?= /* @noEscape */ $block->getFieldSuffix() ?>[stock_data][original_inventory_qty]" -+ value="<?= $block->getFieldValue('qty') * 1 ?>"> -+ <?php endif;?> -+ <input type="text" -+ class="input-text validate-number" -+ id="inventory_qty" -+ name="<?= /* @noEscape */ $block->getFieldSuffix() ?>[stock_data][qty]" -+ value="<?= $block->getFieldValue('qty') * 1 ?>" <?= /* @noEscape */ $_readonly ?>> -+ </div> -+ <?php if (!$block->isSingleStoreMode()) :?> -+ <div class="field-service"><?= $block->escapeHtml(__('[GLOBAL]')) ?></div> -+ <?php endif; ?> -+ </div> - --<div class="field"> -- <label class="label" for="inventory_min_qty"> -- <span><?= /* @escapeNotVerified */ __('Out-of-Stock Threshold') ?></span> -- </label> -- <div class="control"> -- <input type="text" class="input-text validate-number" id="inventory_min_qty" name="<?= /* @escapeNotVerified */ $block->getFieldSuffix() ?>[stock_data][min_qty]" value="<?= /* @escapeNotVerified */ $block->getFieldValue('min_qty') * 1 ?>" <?= /* @escapeNotVerified */ $_readonly ?>> -+ <div class="field"> -+ <label class="label" for="inventory_min_qty"> -+ <span><?= $block->escapeHtml(__('Out-of-Stock Threshold')) ?></span> -+ </label> -+ <div class="control"> -+ <input type="text" -+ class="input-text validate-number" -+ id="inventory_min_qty" -+ name="<?= /* @noEscape */ $block->getFieldSuffix() ?>[stock_data][min_qty]" -+ value="<?= $block->getFieldValue('min_qty') * 1 ?>" <?= /* @noEscape */ $_readonly ?>> - -- <div class="control-inner-wrap"> -- <?php $_checked = ($block->getFieldValue('use_config_min_qty') || $block->isNew()) ? 'checked="checked"' : '' ?> -- <input type="checkbox" id="inventory_use_config_min_qty" name="<?= /* @escapeNotVerified */ $block->getFieldSuffix() ?>[stock_data][use_config_min_qty]" value="1" <?= /* @escapeNotVerified */ $_checked ?> onclick="toggleValueElements(this, this.parentNode);" <?= /* @escapeNotVerified */ $_readonly ?>> -- <label for="inventory_use_config_min_qty"><?= /* @escapeNotVerified */ __('Use Config Settings') ?></label> -- </div> -+ <div class="control-inner-wrap"> -+ <?php $_checked = ($block->getFieldValue('use_config_min_qty') || $block->isNew()) ? 'checked="checked"' : '' ?> -+ <input type="checkbox" -+ id="inventory_use_config_min_qty" -+ name="<?= /* @noEscape */ $block->getFieldSuffix() ?>[stock_data][use_config_min_qty]" -+ value="1" <?= /* @noEscape */ $_checked ?> -+ onclick="toggleValueElements(this, this.parentNode);" <?= /* @noEscape */ $_readonly ?>> -+ <label for="inventory_use_config_min_qty"><?= $block->escapeHtml(__('Use Config Settings')) ?></label> -+ </div> - -- <?php if (!$block->isReadonly()): ?> -- <script> --require(["prototype"], function(){ --toggleValueElements($('inventory_use_config_min_qty'), $('inventory_use_config_min_qty').parentNode); --}); --</script> -- <?php endif; ?> -- </div> -- <?php if (!$block->isSingleStoreMode()): ?> -- <div class="field-service"><?= /* @escapeNotVerified */ __('[GLOBAL]') ?></div> -- <?php endif; ?> --</div> -+ <?php if (!$block->isReadonly()) :?> -+ <script> -+ require(["prototype"], function(){ -+ toggleValueElements($('inventory_use_config_min_qty'), $('inventory_use_config_min_qty').parentNode); -+ }); -+ </script> -+ <?php endif; ?> -+ </div> -+ <?php if (!$block->isSingleStoreMode()) :?> -+ <div class="field-service"><?= $block->escapeHtml(__('[GLOBAL]')) ?></div> -+ <?php endif; ?> -+ </div> - --<div class="field"> -- <label class="label" for="inventory_min_sale_qty"> -- <span><?= /* @escapeNotVerified */ __('Minimum Qty Allowed in Shopping Cart') ?></span> -- </label> -- <div class="control"> -- <input type="text" class="input-text validate-number" id="inventory_min_sale_qty" -- name="<?= /* @escapeNotVerified */ $block->getFieldSuffix() ?>[stock_data][min_sale_qty]" -- value="<?= /* @escapeNotVerified */ $block->getFieldValue('min_sale_qty') * 1 ?>" <?= /* @escapeNotVerified */ $_readonly ?>> -- <div class="control-inner-wrap"> -- <?php $_checked = ($block->getFieldValue('use_config_min_sale_qty') || $block->isNew()) ? 'checked="checked"' : '' ?> -- <input type="checkbox" id="inventory_use_config_min_sale_qty" name="<?= /* @escapeNotVerified */ $block->getFieldSuffix() ?>[stock_data][use_config_min_sale_qty]" value="1" <?= /* @escapeNotVerified */ $_checked ?> onclick="toggleValueElements(this, this.parentNode);" class="checkbox" <?= /* @escapeNotVerified */ $_readonly ?>> -- <label for="inventory_use_config_min_sale_qty"><?= /* @escapeNotVerified */ __('Use Config Settings') ?></label> -- </div> -- <?php if (!$block->isReadonly()): ?> -- <script> --require(['prototype'], function(){ --toggleValueElements($('inventory_use_config_min_sale_qty'), $('inventory_use_config_min_sale_qty').parentNode); --}); --</script> -- <?php endif; ?> -- </div> -- <?php if (!$block->isSingleStoreMode()): ?> -- <div class="field-service"><?= /* @escapeNotVerified */ __('[GLOBAL]') ?></div> -- <?php endif; ?> --</div> -+ <div class="field"> -+ <label class="label" for="inventory_min_sale_qty"> -+ <span><?= $block->escapeHtml(__('Minimum Qty Allowed in Shopping Cart')) ?></span> -+ </label> -+ <div class="control"> -+ <input type="text" class="input-text validate-number" id="inventory_min_sale_qty" -+ name="<?= /* @noEscape */ $block->getFieldSuffix() ?>[stock_data][min_sale_qty]" -+ value="<?= $block->getFieldValue('min_sale_qty') * 1 ?>" <?= /* @noEscape */ $_readonly ?>> -+ <div class="control-inner-wrap"> -+ <?php $_checked = ($block->getFieldValue('use_config_min_sale_qty') || $block->isNew()) ? 'checked="checked"' : '' ?> -+ <input type="checkbox" -+ id="inventory_use_config_min_sale_qty" -+ name="<?= /* @noEscape */ $block->getFieldSuffix() ?>[stock_data][use_config_min_sale_qty]" -+ value="1" <?= /* @noEscape */ $_checked ?> -+ onclick="toggleValueElements(this, this.parentNode);" -+ class="checkbox" <?= /* @noEscape */ $_readonly ?>> -+ <label for="inventory_use_config_min_sale_qty"><?= $block->escapeHtml(__('Use Config Settings')) ?></label> -+ </div> -+ <?php if (!$block->isReadonly()) :?> -+ <script> -+ require(['prototype'], function(){ -+ toggleValueElements($('inventory_use_config_min_sale_qty'), $('inventory_use_config_min_sale_qty').parentNode); -+ }); -+ </script> -+ <?php endif; ?> -+ </div> -+ <?php if (!$block->isSingleStoreMode()) :?> -+ <div class="field-service"><?= $block->escapeHtml(__('[GLOBAL]')) ?></div> -+ <?php endif; ?> -+ </div> - --<div class="field"> -- <label class="label" for="inventory_max_sale_qty"> -- <span><?= /* @escapeNotVerified */ __('Maximum Qty Allowed in Shopping Cart') ?></span> -- </label> -- <div class="control"> -- <input type="text" class="input-text validate-number" id="inventory_max_sale_qty" name="<?= /* @escapeNotVerified */ $block->getFieldSuffix() ?>[stock_data][max_sale_qty]" value="<?= /* @escapeNotVerified */ $block->getFieldValue('max_sale_qty') * 1 ?>" <?= /* @escapeNotVerified */ $_readonly ?>> -- <?php $_checked = ($block->getFieldValue('use_config_max_sale_qty') || $block->isNew()) ? 'checked="checked"' : '' ?> -- <div class="control-inner-wrap"> -- <input type="checkbox" id="inventory_use_config_max_sale_qty" name="<?= /* @escapeNotVerified */ $block->getFieldSuffix() ?>[stock_data][use_config_max_sale_qty]" value="1" <?= /* @escapeNotVerified */ $_checked ?> onclick="toggleValueElements(this, this.parentNode);" class="checkbox" <?= /* @escapeNotVerified */ $_readonly ?>> -- <label for="inventory_use_config_max_sale_qty"><?= /* @escapeNotVerified */ __('Use Config Settings') ?></label> -- </div> -- <?php if (!$block->isReadonly()): ?> -- <script> --require(['prototype'], function(){ --toggleValueElements($('inventory_use_config_max_sale_qty'), $('inventory_use_config_max_sale_qty').parentNode); --}); --</script> -- <?php endif; ?> -- </div> -- <?php if (!$block->isSingleStoreMode()): ?> -- <div class="field-service"><?= /* @escapeNotVerified */ __('[GLOBAL]') ?></div> -- <?php endif; ?> --</div> -+ <div class="field"> -+ <label class="label" for="inventory_max_sale_qty"> -+ <span><?= $block->escapeHtml(__('Maximum Qty Allowed in Shopping Cart')) ?></span> -+ </label> -+ <div class="control"> -+ <input type="text" -+ class="input-text validate-number" -+ id="inventory_max_sale_qty" -+ name="<?= /* @noEscape */ $block->getFieldSuffix() ?>[stock_data][max_sale_qty]" -+ value="<?= $block->getFieldValue('max_sale_qty') * 1 ?>" <?= /* @noEscape */ $_readonly ?>> -+ <?php $_checked = ($block->getFieldValue('use_config_max_sale_qty') || $block->isNew()) ? 'checked="checked"' : '' ?> -+ <div class="control-inner-wrap"> -+ <input type="checkbox" -+ id="inventory_use_config_max_sale_qty" -+ name="<?= /* @noEscape */ $block->getFieldSuffix() ?>[stock_data][use_config_max_sale_qty]" -+ value="1" <?= /* @noEscape */ $_checked ?> -+ onclick="toggleValueElements(this, this.parentNode);" -+ class="checkbox" <?= /* @noEscape */ $_readonly ?>> -+ <label for="inventory_use_config_max_sale_qty"><?= $block->escapeHtml(__('Use Config Settings')) ?></label> -+ </div> -+ <?php if (!$block->isReadonly()) :?> -+ <script> -+ require(['prototype'], function(){ -+ toggleValueElements($('inventory_use_config_max_sale_qty'), $('inventory_use_config_max_sale_qty').parentNode); -+ }); -+ </script> -+ <?php endif; ?> -+ </div> -+ <?php if (!$block->isSingleStoreMode()) :?> -+ <div class="field-service"><?= $block->escapeHtml(__('[GLOBAL]')) ?></div> -+ <?php endif; ?> -+ </div> - -- <?php if ($block->canUseQtyDecimals()): ?> -- <div class="field"> -- <label class="label" for="inventory_is_qty_decimal"> -- <span><?= /* @escapeNotVerified */ __('Qty Uses Decimals') ?></span> -- </label> -- <div class="control"> -- <select id="inventory_is_qty_decimal" name="<?= /* @escapeNotVerified */ $block->getFieldSuffix() ?>[stock_data][is_qty_decimal]" <?= /* @escapeNotVerified */ $_readonly ?>> -- <option value="0"><?= /* @escapeNotVerified */ __('No') ?></option> -- <option value="1"<?php if ($block->getFieldValue('is_qty_decimal') == 1): ?> selected="selected"<?php endif; ?>><?= /* @escapeNotVerified */ __('Yes') ?></option> -- </select> -- </div> -- <?php if (!$block->isSingleStoreMode()): ?> -- <div class="field-service"><?= /* @escapeNotVerified */ __('[GLOBAL]') ?></div> -- <?php endif; ?> -- </div> -+ <?php if ($block->canUseQtyDecimals()) :?> -+ <div class="field"> -+ <label class="label" for="inventory_is_qty_decimal"> -+ <span><?= $block->escapeHtml(__('Qty Uses Decimals')) ?></span> -+ </label> -+ <div class="control"> -+ <select id="inventory_is_qty_decimal" -+ name="<?= /* @noEscape */ $block->getFieldSuffix() ?>[stock_data][is_qty_decimal]" <?= /* @noEscape */ $_readonly ?>> -+ <option value="0"><?= $block->escapeHtml(__('No')) ?></option> -+ <option value="1"<?php if ($block->getFieldValue('is_qty_decimal') == 1) :?> selected="selected"<?php endif; ?>><?= $block->escapeHtml(__('Yes')) ?></option> -+ </select> -+ </div> -+ <?php if (!$block->isSingleStoreMode()) :?> -+ <div class="field-service"><?= $block->escapeHtml(__('[GLOBAL]')) ?></div> -+ <?php endif; ?> -+ </div> - -- <?php if (!$block->isVirtual()) : ?> -- <div class="field"> -- <label class="label" for="inventory_is_decimal_divided"> -- <span><?= /* @escapeNotVerified */ __('Allow Multiple Boxes for Shipping') ?></span> -- </label> -- <div class="control"> -- <select id="inventory_is_decimal_divided" name="<?= /* @escapeNotVerified */ $block->getFieldSuffix() ?>[stock_data][is_decimal_divided]" <?= /* @escapeNotVerified */ $_readonly ?>> -- <option value="0"><?= /* @escapeNotVerified */ __('No') ?></option> -- <option value="1"<?php if ($block->getFieldValue('is_decimal_divided') == 1): ?> selected="selected"<?php endif; ?>><?= /* @escapeNotVerified */ __('Yes') ?></option> -- </select> -- </div> -- <?php if (!$block->isSingleStoreMode()): ?> -- <div class="field-service"><?= /* @escapeNotVerified */ __('[GLOBAL]') ?></div> -+ <?php if (!$block->isVirtual()) :?> -+ <div class="field"> -+ <label class="label" for="inventory_is_decimal_divided"> -+ <span><?= $block->escapeHtml(__('Allow Multiple Boxes for Shipping')) ?></span> -+ </label> -+ <div class="control"> -+ <select id="inventory_is_decimal_divided" -+ name="<?= /* @noEscape */ $block->getFieldSuffix() ?>[stock_data][is_decimal_divided]" <?= /* @noEscape */ $_readonly ?>> -+ <option value="0"><?= $block->escapeHtml(__('No')) ?></option> -+ <option value="1"<?php if ($block->getFieldValue('is_decimal_divided') == 1) :?> -+ selected="selected"<?php endif; ?>><?= $block->escapeHtml(__('Yes')) ?></option> -+ </select> -+ </div> -+ <?php if (!$block->isSingleStoreMode()) :?> -+ <div class="field-service"><?= $block->escapeHtml(__('[GLOBAL]')) ?></div> -+ <?php endif; ?> -+ </div> -+ <?php endif; ?> - <?php endif; ?> -- </div> -- <?php endif; ?> -- <?php endif; ?> - --<div class="field"> -- <label class="label" for="inventory_backorders"> -- <span><?= /* @escapeNotVerified */ __('Backorders') ?></span> -- </label> -- <div class="control"> -- <select id="inventory_backorders" name="<?= /* @escapeNotVerified */ $block->getFieldSuffix() ?>[stock_data][backorders]" <?= /* @escapeNotVerified */ $_readonly ?>> -- <?php foreach ($block->getBackordersOption() as $option): ?> -- <?php $_selected = ($option['value'] == $block->getFieldValue('backorders')) ? 'selected="selected"' : '' ?> -- <option value="<?= /* @escapeNotVerified */ $option['value'] ?>" <?= /* @escapeNotVerified */ $_selected ?>><?= /* @escapeNotVerified */ $option['label'] ?></option> -- <?php endforeach; ?> -- </select> -+ <div class="field"> -+ <label class="label" for="inventory_backorders"> -+ <span><?= $block->escapeHtml(__('Backorders')) ?></span> -+ </label> -+ <div class="control"> -+ <select id="inventory_backorders" -+ name="<?= /* @noEscape */ $block->getFieldSuffix() ?>[stock_data][backorders]" <?= /* @noEscape */ $_readonly ?>> -+ <?php foreach ($block->getBackordersOption() as $option) :?> -+ <?php $_selected = ($option['value'] == $block->getFieldValue('backorders')) ? 'selected="selected"' : '' ?> -+ <option value="<?= $block->escapeHtmlAttr($option['value']) ?>" <?= /* @noEscape */ $_selected ?>><?= $block->escapeHtml($option['label']) ?></option> -+ <?php endforeach; ?> -+ </select> - -- <div class="control-inner-wrap"> -- <?php $_checked = ($block->getFieldValue('use_config_backorders') || $block->isNew()) ? 'checked="checked"' : '' ?> -- <input type="checkbox" id="inventory_use_config_backorders" name="<?= /* @escapeNotVerified */ $block->getFieldSuffix() ?>[stock_data][use_config_backorders]" value="1" <?= /* @escapeNotVerified */ $_checked ?> onclick="toggleValueElements(this, this.parentNode);" <?= /* @escapeNotVerified */ $_readonly ?>> -- <label for="inventory_use_config_backorders"><?= /* @escapeNotVerified */ __('Use Config Settings') ?></label> -- </div> -- <?php if (!$block->isReadonly()): ?> -- <script> --require(['prototype'], function(){ --toggleValueElements($('inventory_use_config_backorders'), $('inventory_use_config_backorders').parentNode); --}); --</script> -- <?php endif; ?> -- </div> -- <?php if (!$block->isSingleStoreMode()): ?> -- <div class="field-service"><?= /* @escapeNotVerified */ __('[GLOBAL]') ?></div> -- <?php endif; ?> --</div> -+ <div class="control-inner-wrap"> -+ <?php $_checked = ($block->getFieldValue('use_config_backorders') || $block->isNew()) ? 'checked="checked"' : '' ?> -+ <input type="checkbox" -+ id="inventory_use_config_backorders" -+ name="<?= /* @noEscape */ $block->getFieldSuffix() ?>[stock_data][use_config_backorders]" -+ value="1" <?= /* @noEscape */ $_checked ?> -+ onclick="toggleValueElements(this, this.parentNode);" <?= /* @noEscape */ $_readonly ?>> -+ <label for="inventory_use_config_backorders"><?= $block->escapeHtml(__('Use Config Settings')) ?></label> -+ </div> -+ <?php if (!$block->isReadonly()) :?> -+ <script> -+ require(['prototype'], function(){ -+ toggleValueElements($('inventory_use_config_backorders'), $('inventory_use_config_backorders').parentNode); -+ }); -+ </script> -+ <?php endif; ?> -+ </div> -+ <?php if (!$block->isSingleStoreMode()) :?> -+ <div class="field-service"><?= $block->escapeHtml(__('[GLOBAL]')) ?></div> -+ <?php endif; ?> -+ </div> - --<div class="field"> -- <label class="label" for="inventory_notify_stock_qty"> -- <span><?= /* @escapeNotVerified */ __('Notify for Quantity Below') ?></span> -- </label> -- <div class="control"> -- <input type="text" class="input-text validate-number" id="inventory_notify_stock_qty" name="<?= /* @escapeNotVerified */ $block->getFieldSuffix() ?>[stock_data][notify_stock_qty]" value="<?= /* @escapeNotVerified */ $block->getFieldValue('notify_stock_qty') * 1 ?>" <?= /* @escapeNotVerified */ $_readonly ?>> -+ <div class="field"> -+ <label class="label" for="inventory_notify_stock_qty"> -+ <span><?= $block->escapeHtml(__('Notify for Quantity Below')) ?></span> -+ </label> -+ <div class="control"> -+ <input type="text" -+ class="input-text validate-number" -+ id="inventory_notify_stock_qty" -+ name="<?= /* @noEscape */ $block->getFieldSuffix() ?>[stock_data][notify_stock_qty]" -+ value="<?= $block->getFieldValue('notify_stock_qty') * 1 ?>" <?= /* @noEscape */ $_readonly ?>> - -- <div class="control-inner-wrap"> -- <?php $_checked = ($block->getFieldValue('use_config_notify_stock_qty') || $block->isNew()) ? 'checked="checked"' : '' ?> -- <input type="checkbox" id="inventory_use_config_notify_stock_qty" name="<?= /* @escapeNotVerified */ $block->getFieldSuffix() ?>[stock_data][use_config_notify_stock_qty]" value="1" <?= /* @escapeNotVerified */ $_checked ?> onclick="toggleValueElements(this, this.parentNode);" <?= /* @escapeNotVerified */ $_readonly ?>> -- <label for="inventory_use_config_notify_stock_qty"><?= /* @escapeNotVerified */ __('Use Config Settings') ?></label> -- </div> -- <?php if (!$block->isReadonly()): ?> -- <script> --require(['prototype'], function(){ --toggleValueElements($('inventory_use_config_notify_stock_qty'), $('inventory_use_config_notify_stock_qty').parentNode); --}); --</script> -- <?php endif; ?> -- </div> -- <?php if (!$block->isSingleStoreMode()): ?> -- <div class="field-service"><?= /* @escapeNotVerified */ __('[GLOBAL]') ?></div> -- <?php endif; ?> --</div> -+ <div class="control-inner-wrap"> -+ <?php $_checked = ($block->getFieldValue('use_config_notify_stock_qty') || $block->isNew()) ? 'checked="checked"' : '' ?> -+ <input type="checkbox" -+ id="inventory_use_config_notify_stock_qty" -+ name="<?= /* @noEscape */ $block->getFieldSuffix() ?>[stock_data][use_config_notify_stock_qty]" -+ value="1" <?= /* @noEscape */ $_checked ?> -+ onclick="toggleValueElements(this, this.parentNode);" <?= /* @noEscape */ $_readonly ?>> -+ <label for="inventory_use_config_notify_stock_qty"><?= $block->escapeHtml(__('Use Config Settings')) ?></label> -+ </div> -+ <?php if (!$block->isReadonly()) :?> -+ <script> -+ require(['prototype'], function(){ -+ toggleValueElements($('inventory_use_config_notify_stock_qty'), $('inventory_use_config_notify_stock_qty').parentNode); -+ }); -+ </script> -+ <?php endif; ?> -+ </div> -+ <?php if (!$block->isSingleStoreMode()) :?> -+ <div class="field-service"><?= $block->escapeHtml(__('[GLOBAL]')) ?></div> -+ <?php endif; ?> -+ </div> - -- <?php endif; ?> --<div class="field"> -- <label class="label" for="inventory_enable_qty_increments"> -- <span><?= /* @escapeNotVerified */ __('Enable Qty Increments') ?></span> -- </label> -- <div class="control"> -- <?php $qtyIncrementsEnabled = $block->getFieldValue('enable_qty_increments'); ?> -- <select id="inventory_enable_qty_increments" name="<?= /* @escapeNotVerified */ $block->getFieldSuffix() ?>[stock_data][enable_qty_increments]" <?= /* @escapeNotVerified */ $_readonly ?>> -- <option value="1"<?php if ($qtyIncrementsEnabled): ?> selected="selected"<?php endif; ?>><?= /* @escapeNotVerified */ __('Yes') ?></option> -- <option value="0"<?php if (!$qtyIncrementsEnabled): ?> selected="selected"<?php endif; ?>><?= /* @escapeNotVerified */ __('No') ?></option> -- </select> -- <input type="hidden" id="inventory_enable_qty_increments_default" value="<?= /* @escapeNotVerified */ $block->getDefaultConfigValue('enable_qty_increments') ?>"> -+ <?php endif; ?> -+ <div class="field"> -+ <label class="label" for="inventory_enable_qty_increments"> -+ <span><?= $block->escapeHtml(__('Enable Qty Increments')) ?></span> -+ </label> -+ <div class="control"> -+ <?php $qtyIncrementsEnabled = $block->getFieldValue('enable_qty_increments'); ?> -+ <select id="inventory_enable_qty_increments" -+ name="<?= /* @noEscape */ $block->getFieldSuffix() ?>[stock_data][enable_qty_increments]" <?= /* @noEscape */ $_readonly ?>> -+ <option value="1"<?php if ($qtyIncrementsEnabled) :?> selected="selected"<?php endif; ?>><?= $block->escapeHtml(__('Yes')) ?></option> -+ <option value="0"<?php if (!$qtyIncrementsEnabled) :?> selected="selected"<?php endif; ?>><?= $block->escapeHtml(__('No')) ?></option> -+ </select> -+ <input type="hidden" -+ id="inventory_enable_qty_increments_default" -+ value="<?= $block->escapeHtmlAttr($block->getDefaultConfigValue('enable_qty_increments')) ?>"> - -- <div class="control-inner-wrap"> -- <?php $_checked = ($block->getFieldValue('use_config_enable_qty_inc') || $block->isNew()) ? 'checked="checked"' : '' ?> -- <input type="checkbox" id="inventory_use_config_enable_qty_increments" name="<?= /* @escapeNotVerified */ $block->getFieldSuffix() ?>[stock_data][use_config_enable_qty_increments]" value="1" <?= /* @escapeNotVerified */ $_checked ?> onclick="toggleValueElements(this, this.parentNode);" <?= /* @escapeNotVerified */ $_readonly ?>> -- <label for="inventory_use_config_enable_qty_increments"><?= /* @escapeNotVerified */ __('Use Config Settings') ?></label> -+ <div class="control-inner-wrap"> -+ <?php $_checked = ($block->getFieldValue('use_config_enable_qty_inc') || $block->isNew()) ? 'checked="checked"' : '' ?> -+ <input type="checkbox" -+ id="inventory_use_config_enable_qty_increments" -+ name="<?= /* @noEscape */ $block->getFieldSuffix() ?>[stock_data][use_config_enable_qty_increments]" -+ value="1" <?= /* @noEscape */ $_checked ?> -+ onclick="toggleValueElements(this, this.parentNode);" <?= /* @noEscape */ $_readonly ?>> -+ <label for="inventory_use_config_enable_qty_increments"><?= $block->escapeHtml(__('Use Config Settings')) ?></label> -+ </div> -+ <?php if (!$block->isReadonly()) :?> -+ <script> -+ require(['prototype'], function(){ -+ toggleValueElements($('inventory_use_config_enable_qty_increments'), $('inventory_use_config_enable_qty_increments').parentNode); -+ }); -+ </script> -+ <?php endif; ?> -+ </div> -+ <?php if (!$block->isSingleStoreMode()) :?> -+ <div class="field-service"><?= $block->escapeHtml(__('[GLOBAL]')) ?></div> -+ <?php endif; ?> - </div> -- <?php if (!$block->isReadonly()): ?> -- <script> --require(['prototype'], function(){ --toggleValueElements($('inventory_use_config_enable_qty_increments'), $('inventory_use_config_enable_qty_increments').parentNode); --}); --</script> -- <?php endif; ?> -- </div> -- <?php if (!$block->isSingleStoreMode()): ?> -- <div class="field-service"><?= /* @escapeNotVerified */ __('[GLOBAL]') ?></div> -- <?php endif; ?> --</div> - --<div class="field"> -- <label class="label" for="inventory_qty_increments"> -- <span><?= /* @escapeNotVerified */ __('Qty Increments') ?></span> -- </label> -- <div class="control"> -- <input type="text" class="input-text validate-digits" id="inventory_qty_increments" name="<?= /* @escapeNotVerified */ $block->getFieldSuffix() ?>[stock_data][qty_increments]" value="<?= /* @escapeNotVerified */ $block->getFieldValue('qty_increments') * 1 ?>" <?= /* @escapeNotVerified */ $_readonly ?>> -- <div class="control-inner-wrap"> -- <?php $_checked = ($block->getFieldValue('use_config_qty_increments') || $block->isNew()) ? 'checked="checked"' : '' ?> -- <input type="checkbox" id="inventory_use_config_qty_increments" name="<?= /* @escapeNotVerified */ $block->getFieldSuffix() ?>[stock_data][use_config_qty_increments]" value="1" <?= /* @escapeNotVerified */ $_checked ?> onclick="toggleValueElements(this, this.parentNode);" <?= /* @escapeNotVerified */ $_readonly ?>> -- <label for="inventory_use_config_qty_increments"><?= /* @escapeNotVerified */ __('Use Config Settings') ?></label> -+ <div class="field"> -+ <label class="label" for="inventory_qty_increments"> -+ <span><?= $block->escapeHtml(__('Qty Increments')) ?></span> -+ </label> -+ <div class="control"> -+ <input type="text" -+ class="input-text validate-digits" -+ id="inventory_qty_increments" -+ name="<?= /* @noEscape */ $block->getFieldSuffix() ?>[stock_data][qty_increments]" -+ value="<?= $block->getFieldValue('qty_increments') * 1 ?>" <?= /* @noEscape */ $_readonly ?>> -+ <div class="control-inner-wrap"> -+ <?php $_checked = ($block->getFieldValue('use_config_qty_increments') || $block->isNew()) ? 'checked="checked"' : '' ?> -+ <input type="checkbox" -+ id="inventory_use_config_qty_increments" -+ name="<?= /* @noEscape */ $block->getFieldSuffix() ?>[stock_data][use_config_qty_increments]" -+ value="1" <?= /* @noEscape */ $_checked ?> -+ onclick="toggleValueElements(this, this.parentNode);" <?= /* @noEscape */ $_readonly ?>> -+ <label for="inventory_use_config_qty_increments"><?= $block->escapeHtml(__('Use Config Settings')) ?></label> -+ </div> -+ <?php if (!$block->isReadonly()) :?> -+ <script> -+ require(['prototype'], function(){ -+ toggleValueElements($('inventory_use_config_qty_increments'), $('inventory_use_config_qty_increments').parentNode); -+ }); -+ </script> -+ <?php endif; ?> -+ </div> -+ <?php if (!$block->isSingleStoreMode()) :?> -+ <div class="field-service"><?= $block->escapeHtml(__('[GLOBAL]')) ?></div> -+ <?php endif; ?> - </div> -- <?php if (!$block->isReadonly()): ?> -- <script> --require(['prototype'], function(){ --toggleValueElements($('inventory_use_config_qty_increments'), $('inventory_use_config_qty_increments').parentNode); --}); --</script> -- <?php endif; ?> -- </div> -- <?php if (!$block->isSingleStoreMode()): ?> -- <div class="field-service"><?= /* @escapeNotVerified */ __('[GLOBAL]') ?></div> -- <?php endif; ?> --</div> - --<div class="field"> -- <label class="label" for="inventory_stock_availability"> -- <span><?= /* @escapeNotVerified */ __('Stock Availability') ?></span> -- </label> -- <div class="control"> -- <select id="inventory_stock_availability" name="<?= /* @escapeNotVerified */ $block->getFieldSuffix() ?>[stock_data][is_in_stock]" <?= /* @escapeNotVerified */ $_readonly ?>> -- <?php foreach ($block->getStockOption() as $option): ?> -- <?php $_selected = ($block->getFieldValue('is_in_stock') !== null && $option['value'] == $block->getFieldValue('is_in_stock')) ? 'selected="selected"' : '' ?> -- <option value="<?= /* @escapeNotVerified */ $option['value'] ?>" <?= /* @escapeNotVerified */ $_selected ?>><?= /* @escapeNotVerified */ $option['label'] ?></option> -- <?php endforeach; ?> -- </select> -+ <div class="field"> -+ <label class="label" for="inventory_stock_availability"> -+ <span><?= $block->escapeHtml(__('Stock Availability')) ?></span> -+ </label> -+ <div class="control"> -+ <select id="inventory_stock_availability" -+ name="<?= /* @noEscape */ $block->getFieldSuffix() ?>[stock_data][is_in_stock]" <?= /* @noEscape */ $_readonly ?>> -+ <?php foreach ($block->getStockOption() as $option) :?> -+ <?php $_selected = ($block->getFieldValue('is_in_stock') !== null && $option['value'] == $block->getFieldValue('is_in_stock')) ? 'selected="selected"' : '' ?> -+ <option value="<?= $block->escapeHtmlAttr($option['value']) ?>" <?= /* @noEscape */ $_selected ?>><?= $block->escapeHtml($option['label']) ?></option> -+ <?php endforeach; ?> -+ </select> -+ </div> -+ <?php if (!$block->isSingleStoreMode()) :?> -+ <div class="field-service"><?= $block->escapeHtml(__('[GLOBAL]')) ?></div> -+ <?php endif; ?> -+ </div> - </div> -- <?php if (!$block->isSingleStoreMode()): ?> -- <div class="field-service"><?= /* @escapeNotVerified */ __('[GLOBAL]') ?></div> -- <?php endif; ?> --</div> --</div> - </fieldset> - - <script> --require(["jquery","prototype"], function(jQuery){ -+ require(["jquery","prototype"], function(jQuery){ - -- //<![CDATA[ -- function changeManageStockOption() -- { -- var manageStock = $('inventory_use_config_manage_stock').checked -+ //<![CDATA[ -+ function changeManageStockOption() -+ { -+ var manageStock = $('inventory_use_config_manage_stock').checked - ? $('inventory_manage_stock_default').value - : $('inventory_manage_stock').value; -- var catalogInventoryNotManageStockFields = { -- inventory_min_sale_qty: true, -- inventory_max_sale_qty: true, -- inventory_enable_qty_increments : true, -- inventory_qty_increments: true -- }; -- -- $$('#table_cataloginventory > div').each(function(el) { -- if (el == $('inventory_manage_stock').up(1)) { -- return; -- } -+ var catalogInventoryNotManageStockFields = { -+ inventory_min_sale_qty: true, -+ inventory_max_sale_qty: true, -+ inventory_enable_qty_increments : true, -+ inventory_qty_increments: true -+ }; - -- for (field in catalogInventoryNotManageStockFields) { -- if ($(field) && ($(field).up(1) == el)) { -+ $$('#table_cataloginventory > div').each(function(el) { -+ if (el == $('inventory_manage_stock').up(1)) { - return; - } -- } - -- el[manageStock == 1 ? 'show' : 'hide'](); -- }); -+ for (field in catalogInventoryNotManageStockFields) { -+ if ($(field) && ($(field).up(1) == el)) { -+ return; -+ } -+ } - -- return true; -- } -+ el[manageStock == 1 ? 'show' : 'hide'](); -+ }); - -- function applyEnableQtyIncrements() { -- var enableQtyIncrements = $('inventory_use_config_enable_qty_increments').checked -- ? $('inventory_enable_qty_increments_default').value -- : $('inventory_enable_qty_increments').value; -+ return true; -+ } - -- $('inventory_qty_increments').up('.field')[enableQtyIncrements == 1 ? 'show' : 'hide'](); -- } -+ function applyEnableQtyIncrements() { -+ var enableQtyIncrements = $('inventory_use_config_enable_qty_increments').checked -+ ? $('inventory_enable_qty_increments_default').value -+ : $('inventory_enable_qty_increments').value; - -- function applyEnableDecimalDivided() { -- <?php if (!$block->isVirtual()) : ?> -- $('inventory_is_decimal_divided').up('.field').hide(); -- <?php endif; ?> -- $('inventory_qty_increments').removeClassName('validate-digits').removeClassName('validate-number'); -- $('inventory_min_sale_qty').removeClassName('validate-digits').removeClassName('validate-number'); -- if ($('inventory_is_qty_decimal').value == 1) { -- <?php if (!$block->isVirtual()) : ?> -- $('inventory_is_decimal_divided').up('.field').show(); -- <?php endif; ?> -- $('inventory_qty_increments').addClassName('validate-number'); -- $('inventory_min_sale_qty').addClassName('validate-number'); -- } else { -- $('inventory_qty_increments').addClassName('validate-digits'); -- $('inventory_min_sale_qty').addClassName('validate-digits'); -+ $('inventory_qty_increments').up('.field')[enableQtyIncrements == 1 ? 'show' : 'hide'](); - } -- } - -- Event.observe(window, 'load', function() { -- if ($('inventory_manage_stock') && $('inventory_use_config_manage_stock')) { -- Event.observe($('inventory_manage_stock'), 'change', changeManageStockOption); -- Event.observe($('inventory_use_config_manage_stock'), 'change', changeManageStockOption); -- changeManageStockOption(); -+ function applyEnableDecimalDivided() { -+ <?php if (!$block->isVirtual()) :?> -+ $('inventory_is_decimal_divided').up('.field').hide(); -+ <?php endif; ?> -+ $('inventory_qty_increments').removeClassName('validate-digits').removeClassName('validate-number'); -+ $('inventory_min_sale_qty').removeClassName('validate-digits').removeClassName('validate-number'); -+ if ($('inventory_is_qty_decimal').value == 1) { -+ <?php if (!$block->isVirtual()) :?> -+ $('inventory_is_decimal_divided').up('.field').show(); -+ <?php endif; ?> -+ $('inventory_qty_increments').addClassName('validate-number'); -+ $('inventory_min_sale_qty').addClassName('validate-number'); -+ } else { -+ $('inventory_qty_increments').addClassName('validate-digits'); -+ $('inventory_min_sale_qty').addClassName('validate-digits'); -+ } - } -- if ($('inventory_enable_qty_increments') && $('inventory_use_config_enable_qty_increments')) { -- //Delegation is used because of events, which are not firing while the input is disabled -- jQuery('#inventory_enable_qty_increments').parent() -+ -+ Event.observe(window, 'load', function() { -+ if ($('inventory_manage_stock') && $('inventory_use_config_manage_stock')) { -+ Event.observe($('inventory_manage_stock'), 'change', changeManageStockOption); -+ Event.observe($('inventory_use_config_manage_stock'), 'change', changeManageStockOption); -+ changeManageStockOption(); -+ } -+ if ($('inventory_enable_qty_increments') && $('inventory_use_config_enable_qty_increments')) { -+ //Delegation is used because of events, which are not firing while the input is disabled -+ jQuery('#inventory_enable_qty_increments').parent() - .on('change', '#inventory_enable_qty_increments', applyEnableQtyIncrements); -- Event.observe($('inventory_use_config_enable_qty_increments'), 'change', applyEnableQtyIncrements); -- applyEnableQtyIncrements(); -- } -- if ($('inventory_is_qty_decimal') && $('inventory_qty_increments') && $('inventory_min_sale_qty')) { -- Event.observe($('inventory_is_qty_decimal'), 'change', applyEnableDecimalDivided); -- applyEnableDecimalDivided(); -- } -- }); -+ Event.observe($('inventory_use_config_enable_qty_increments'), 'change', applyEnableQtyIncrements); -+ applyEnableQtyIncrements(); -+ } -+ if ($('inventory_is_qty_decimal') && $('inventory_qty_increments') && $('inventory_min_sale_qty')) { -+ Event.observe($('inventory_is_qty_decimal'), 'change', applyEnableDecimalDivided); -+ applyEnableDecimalDivided(); -+ } -+ }); - -- window.applyEnableDecimalDivided = applyEnableDecimalDivided; -- window.applyEnableQtyIncrements = applyEnableQtyIncrements; -- window.changeManageStockOption = changeManageStockOption; -- //]]> -+ window.applyEnableDecimalDivided = applyEnableDecimalDivided; -+ window.applyEnableQtyIncrements = applyEnableQtyIncrements; -+ window.changeManageStockOption = changeManageStockOption; -+ //]]> - --}); -+ }); - </script> -diff --git a/app/code/Magento/Catalog/view/adminhtml/templates/product/edit/attribute/search.phtml b/app/code/Magento/Catalog/view/adminhtml/templates/product/edit/attribute/search.phtml -index 04ccfb5aee8..17fb517b325 100644 ---- a/app/code/Magento/Catalog/view/adminhtml/templates/product/edit/attribute/search.phtml -+++ b/app/code/Magento/Catalog/view/adminhtml/templates/product/edit/attribute/search.phtml -@@ -4,24 +4,24 @@ - * See COPYING.txt for license details. - */ - --// @codingStandardsIgnoreFile -+// phpcs:disable Magento2.Templates.ThisInTemplate.FoundThis - - /** @var $block \Magento\Catalog\Block\Adminhtml\Product\Edit\Tab\Attributes\Search */ - ?> - <div id="product-attribute-search-container" class="suggest-expandable attribute-selector"> - <div class="action-dropdown"> - <button type="button" class="action-toggle action-choose" data-mage-init='{"dropdown":{}}' data-toggle="dropdown"> -- <span><?= /* @escapeNotVerified */ __('Add Attribute') ?></span> -+ <span><?= $block->escapeHtml(__('Add Attribute')) ?></span> - </button> - <div class="dropdown-menu"> - <input data-role="product-attribute-search" -- data-group="<?= $block->escapeHtml($block->getGroupCode()) ?>" -+ data-group="<?= $block->escapeHtmlAttr($block->getGroupCode()) ?>" - class="search" type="text" -- placeholder="<?= /* @noEscape */ __('start typing to search attribute') ?>" /> -+ placeholder="<?= $block->escapeHtmlAttr(__('start typing to search attribute')) ?>" /> - </div> - </div> - --<script data-template-for="product-attribute-search-<?= /* @escapeNotVerified */ $block->getGroupId() ?>" type="text/x-magento-template"> -+<script data-template-for="product-attribute-search-<?= $block->escapeHtmlAttr($block->getGroupId()) ?>" type="text/x-magento-template"> - <ul data-mage-init='{"menu":[]}'> - <% if (data.items.length) { %> - <% _.each(data.items, function(value){ %> -@@ -29,7 +29,7 @@ - <% }); %> - <% } else { %><span class="mage-suggest-no-records"><%- data.noRecordsText %></span><% } %> - </ul> -- <div class="actions"><?= /* @escapeNotVerified */ $block->getAttributeCreate() ?></div> -+ <div class="actions"><?= $block->escapeHtml($block->getAttributeCreate()) ?></div> - </script> - - <script> -@@ -51,13 +51,13 @@ - }); - }); - -- $suggest.mage('suggest', <?= /* @escapeNotVerified */ $this->helper('Magento\Framework\Json\Helper\Data')->jsonEncode($block->getSelectorOptions()) ?>) -+ $suggest.mage('suggest', <?= /* @noEscape */ $this->helper(Magento\Framework\Json\Helper\Data::class)->jsonEncode($block->getSelectorOptions()) ?>) - .on('suggestselect', function (e, ui) { - $(this).val(''); - var templateId = $('#attribute_set_id').val(); - if (ui.item.id) { - $.ajax({ -- url: '<?= /* @escapeNotVerified */ $block->getAddAttributeUrl() ?>', -+ url: '<?= $block->escapeJs($block->escapeUrl($block->getAddAttributeUrl())) ?>', - type: 'POST', - dataType: 'json', - data: {attribute_id: ui.item.id, template_id: templateId, group: $(this).data('group')}, -diff --git a/app/code/Magento/Catalog/view/adminhtml/templates/product/edit/tabs.phtml b/app/code/Magento/Catalog/view/adminhtml/templates/product/edit/tabs.phtml -index 6a62f01f97b..360694fceb2 100644 ---- a/app/code/Magento/Catalog/view/adminhtml/templates/product/edit/tabs.phtml -+++ b/app/code/Magento/Catalog/view/adminhtml/templates/product/edit/tabs.phtml -@@ -4,40 +4,38 @@ - * See COPYING.txt for license details. - */ - --// @codingStandardsIgnoreFile -- - /** @var $block \Magento\Catalog\Block\Adminhtml\Product\Edit\Tabs */ - ?> --<?php if (!empty($tabs)): ?> -+<?php if (!empty($tabs)) :?> - <?php $tabGroups = [ - \Magento\Catalog\Block\Adminhtml\Product\Edit\Tabs::BASIC_TAB_GROUP_CODE, - \Magento\Catalog\Block\Adminhtml\Product\Edit\Tabs::ADVANCED_TAB_GROUP_CODE, - ];?> - -- <div id="<?= /* @escapeNotVerified */ $block->getId() ?>" -+ <div id="<?= $block->escapeHtmlAttr($block->getId()) ?>" - data-mage-init='{"tabs":{ -- "active": "<?= /* @escapeNotVerified */ $block->getActiveTabId() ?>", -- "destination": "#<?= /* @escapeNotVerified */ $block->getDestElementId() ?>", -- "shadowTabs": "<?= /* @escapeNotVerified */ $block->getAllShadowTabs() ?>", -- "tabsBlockPrefix": "<?= /* @escapeNotVerified */ $block->getId() ?>_", -+ "active": "<?= $block->escapeHtmlAttr($block->getActiveTabId()) ?>", -+ "destination": "#<?= $block->escapeHtmlAttr($block->getDestElementId()) ?>", -+ "shadowTabs": "<?= /* @noEscape */ $block->getAllShadowTabs() ?>", -+ "tabsBlockPrefix": "<?= $block->escapeHtmlAttr($block->getId()) ?>_", - "tabIdArgument": "active_tab", -- "tabPanelClass": "<?= /* @escapeNotVerified */ $block->getPanelsClass() ?>", -- "excludedPanel": "<?= /* @escapeNotVerified */ $block->getExcludedPanel() ?>", -+ "tabPanelClass": "<?= $block->escapeHtmlAttr($block->getPanelsClass()) ?>", -+ "excludedPanel": "<?= $block->escapeHtmlAttr($block->getExcludedPanel()) ?>", - "groups": "ul.tabs" - }}'> -- <?php foreach ($tabGroups as $tabGroupCode): ?> -+ <?php foreach ($tabGroups as $tabGroupCode) :?> - <?php - $tabGroupId = $block->getId() . '-' . $tabGroupCode; - $isBasic = $tabGroupCode == \Magento\Catalog\Block\Adminhtml\Product\Edit\Tabs::BASIC_TAB_GROUP_CODE; - $activeCollapsible = $block->isAdvancedTabGroupActive() ? true : false; - ?> - -- <div class="admin__page-nav <?php if (!$isBasic): ?> <?= '_collapsed' ?> <?php endif;?>" -+ <div class="admin__page-nav <?php if (!$isBasic) :?> <?= '_collapsed' ?> <?php endif;?>" - data-role="container" -- id="<?= /* @escapeNotVerified */ $tabGroupId ?>" -- <?php if (!$isBasic): ?> -+ id="<?= $block->escapeHtmlAttr($tabGroupId) ?>" -+ <?php if (!$isBasic) :?> - data-mage-init='{"collapsible":{ -- "active": "<?= /* @escapeNotVerified */ $activeCollapsible ?>", -+ "active": "<?= /* @noEscape */ $activeCollapsible ?>", - "openedState": "_show", - "closedState": "_hide", - "animate": 200, -@@ -45,44 +43,45 @@ - }}' - <?php endif;?>> - -- <div class="admin__page-nav-title-wrap" <?= /* @escapeNotVerified */ $block->getUiId('title') ?> data-role="title"> -- <div class="admin__page-nav-title <?php if (!$isBasic): ?> <?= '_collapsible' ?><?php endif;?>" -+ <div class="admin__page-nav-title-wrap" <?= /* @noEscape */ $block->getUiId('title') ?> data-role="title"> -+ <div class="admin__page-nav-title <?php if (!$isBasic) :?> <?= '_collapsible' ?><?php endif;?>" - data-role="trigger"> - <strong> -- <?= /* @escapeNotVerified */ $isBasic ? __('Basic Settings') : __('Advanced Settings') ?> -+ <?= $block->escapeHtml($isBasic ? __('Basic Settings') : __('Advanced Settings')) ?> - </strong> - <span data-role="title-messages" class="admin__page-nav-title-messages"></span> - </div> - </div> - -- <ul <?= /* @escapeNotVerified */ $block->getUiId('tab', $tabGroupId) ?> class="tabs admin__page-nav-items" data-role="content"> -- <?php foreach ($tabs as $_tab): ?> -+ <ul <?= /* @noEscape */ $block->getUiId('tab', $tabGroupId) ?> class="tabs admin__page-nav-items" data-role="content"> -+ <?php foreach ($tabs as $_tab) :?> - <?php /** @var $_tab \Magento\Backend\Block\Widget\Tab\TabInterface */ ?> - <?php if (!$block->canShowTab($_tab) || $_tab->getParentTab() - || ($_tab->getGroupCode() && $_tab->getGroupCode() != $tabGroupCode) -- || (!$_tab->getGroupCode() && $isBasic)): continue; endif;?> -+ || (!$_tab->getGroupCode() && $isBasic)) : continue; -+ endif;?> - <?php $_tabClass = 'tab-item-link ' . $block->getTabClass($_tab) . ' ' . (preg_match('/\s?ajax\s?/', $_tab->getClass()) ? 'notloaded' : '') ?> - <?php $_tabType = (!preg_match('/\s?ajax\s?/', $_tabClass) && $block->getTabUrl($_tab) != '#') ? 'link' : '' ?> - <?php $_tabHref = $block->getTabUrl($_tab) == '#' ? '#' . $block->getTabId($_tab) . '_content' : $block->getTabUrl($_tab) ?> -- <li class="admin__page-nav-item <?php if ($block->getTabIsHidden($_tab)): ?> <?= "no-display" ?> <?php endif; ?> " <?= /* @escapeNotVerified */ $block->getUiId('tab', 'item', $_tab->getId()) ?>> -- <a href="<?= /* @escapeNotVerified */ $_tabHref ?>" id="<?= /* @escapeNotVerified */ $block->getTabId($_tab) ?>" -- name="<?= /* @escapeNotVerified */ $block->getTabId($_tab, false) ?>" -- title="<?= /* @escapeNotVerified */ $block->getTabTitle($_tab) ?>" -- class="admin__page-nav-link <?= /* @escapeNotVerified */ $_tabClass ?>" -- data-tab-type="<?= /* @escapeNotVerified */ $_tabType ?>" <?= /* @escapeNotVerified */ $block->getUiId('tab', 'link', $_tab->getId()) ?> -+ <li class="admin__page-nav-item <?php if ($block->getTabIsHidden($_tab)) :?> <?= "no-display" ?> <?php endif; ?> " <?= /* @noEscape */ $block->getUiId('tab', 'item', $_tab->getId()) ?>> -+ <a href="<?= $block->escapeUrl($_tabHref) ?>" id="<?= $block->escapeHtmlAttr($block->getTabId($_tab)) ?>" -+ name="<?= $block->escapeHtmlAttr($block->getTabId($_tab, false)) ?>" -+ title="<?= $block->escapeHtmlAttr($block->getTabTitle($_tab)) ?>" -+ class="admin__page-nav-link <?= $block->escapeHtmlAttr($_tabClass) ?>" -+ data-tab-type="<?= /* @noEscape */ $_tabType ?>" <?= /* @noEscape */ $block->getUiId('tab', 'link', $_tab->getId()) ?> - > - <span><?= $block->escapeHtml($block->getTabLabel($_tab)) ?></span> - <span class="admin__page-nav-item-messages" data-role="item-messages"> - <span class="admin__page-nav-item-message _changed"> - <span class="admin__page-nav-item-message-icon"></span> - <span class="admin__page-nav-item-message-tooltip"> -- <?= /* @escapeNotVerified */ __('Changes have been made to this section that have not been saved.') ?> -+ <?= $block->escapeHtml(__('Changes have been made to this section that have not been saved.')) ?> - </span> - </span> - <span class="admin__page-nav-item-message _error"> - <span class="admin__page-nav-item-message-icon"></span> - <span class="admin__page-nav-item-message-tooltip"> -- <?= /* @escapeNotVerified */ __('This tab contains invalid data. Please resolve this before saving.') ?> -+ <?= $block->escapeHtml(__('This tab contains invalid data. Please resolve this before saving.')) ?> - </span> - </span> - <span class="admin__page-nav-item-message-loader"> -@@ -93,11 +92,11 @@ - </span> - </span> - </a> -- <div id="<?= /* @escapeNotVerified */ $block->getTabId($_tab) ?>_content" class="no-display" -- data-tab-panel="<?= /* @escapeNotVerified */ $_tab->getTabId() ?>" -- <?= /* @escapeNotVerified */ $block->getUiId('tab', 'content', $_tab->getId()) ?> -+ <div id="<?= $block->escapeHtmlAttr($block->getTabId($_tab)) ?>_content" class="no-display" -+ data-tab-panel="<?= $block->escapeHtmlAttr($_tab->getTabId()) ?>" -+ <?= /* @noEscape */ $block->getUiId('tab', 'content', $_tab->getId()) ?> - > -- <?= /* @escapeNotVerified */ $block->getTabContent($_tab) ?> -+ <?= /* @noEscape */ $block->getTabContent($_tab) ?> - <?= /* @noEscape */ $block->getAccordion($_tab) ?> - </div> - </li> -diff --git a/app/code/Magento/Catalog/view/adminhtml/templates/product/edit/tabs/child_tab.phtml b/app/code/Magento/Catalog/view/adminhtml/templates/product/edit/tabs/child_tab.phtml -index 842ed17375f..c4dc1ddc0b0 100644 ---- a/app/code/Magento/Catalog/view/adminhtml/templates/product/edit/tabs/child_tab.phtml -+++ b/app/code/Magento/Catalog/view/adminhtml/templates/product/edit/tabs/child_tab.phtml -@@ -4,13 +4,11 @@ - * See COPYING.txt for license details. - */ - --// @codingStandardsIgnoreFile -- - /** @var $block \Magento\Catalog\Block\Adminhtml\Product\Edit\Tab\ChildTab */ - ?> - <div class="fieldset-wrapper admin__collapsible-block-wrapper" data-tab="<?= /* @noEscape */ $block->getTabId() ?>" - id="<?= /* @noEscape */ $block->getTabId() ?>-wrapper" data-mage-init='{"collapsible":{ -- "active": <?= /* @noEscape */ $block->isTabOpened() ? 'true' : 'false' ?>, -+ "active": <?= $block->isTabOpened() ? 'true' : 'false' ?>, - "openedState": "_show", - "closedState": "_hide", - "animate": 200, -diff --git a/app/code/Magento/Catalog/view/adminhtml/templates/product/grid/massaction_extended.phtml b/app/code/Magento/Catalog/view/adminhtml/templates/product/grid/massaction_extended.phtml -index 8df3e32b0a2..c814298d1db 100644 ---- a/app/code/Magento/Catalog/view/adminhtml/templates/product/grid/massaction_extended.phtml -+++ b/app/code/Magento/Catalog/view/adminhtml/templates/product/grid/massaction_extended.phtml -@@ -4,11 +4,10 @@ - * See COPYING.txt for license details. - */ - --// @codingStandardsIgnoreFile -- -+/** @var $block \Magento\Catalog\Block\Adminhtml\Product\Grid */ - ?> - <div id="<?= $block->getHtmlId() ?>" class="admin__grid-massaction"> -- <?php if ($block->getHideFormElement() !== true):?> -+ <?php if ($block->getHideFormElement() !== true) :?> - <form action="" id="<?= $block->getHtmlId() ?>-form" method="post"> - <?php endif ?> - <div class="admin__grid-massaction-form"> -@@ -16,43 +15,43 @@ - <select - id="<?= $block->getHtmlId() ?>-select" - class="local-validation admin__control-select"> -- <option class="admin__control-select-placeholder" value="" selected><?= /* @escapeNotVerified */ __('Actions') ?></option> -- <?php foreach ($block->getItems() as $_item): ?> -- <option value="<?= /* @escapeNotVerified */ $_item->getId() ?>"<?= ($_item->getSelected() ? ' selected="selected"' : '') ?>><?= /* @escapeNotVerified */ $_item->getLabel() ?></option> -+ <option class="admin__control-select-placeholder" value="" selected><?= $block->escapeHtml(__('Actions')) ?></option> -+ <?php foreach ($block->getItems() as $_item) :?> -+ <option value="<?= $block->escapeHtmlAttr($_item->getId()) ?>"<?= ($_item->getSelected() ? ' selected="selected"' : '') ?>><?= $block->escapeHtml($_item->getLabel()) ?></option> - <?php endforeach; ?> - </select> - <span class="outer-span" id="<?= $block->getHtmlId() ?>-form-hiddens"></span> - <span class="outer-span" id="<?= $block->getHtmlId() ?>-form-additional"></span> - <?= $block->getApplyButtonHtml() ?> - </div> -- <?php if ($block->getHideFormElement() !== true):?> -+ <?php if ($block->getHideFormElement() !== true) :?> - </form> - <?php endif ?> - <div class="no-display"> -- <?php foreach ($block->getItems() as $_item): ?> -- <div id="<?= $block->getHtmlId() ?>-item-<?= /* @escapeNotVerified */ $_item->getId() ?>-block"> -+ <?php foreach ($block->getItems() as $_item) :?> -+ <div id="<?= $block->getHtmlId() ?>-item-<?= $block->escapeHtmlAttr($_item->getId()) ?>-block"> - <?= $_item->getAdditionalActionBlockHtml() ?> - </div> - <?php endforeach; ?> - </div> - - <div class="mass-select-wrap"> -- <select id="<?= $block->getHtmlId() ?>-mass-select" data-menu="grid-mass-select"> -- <optgroup label="<?= /* @escapeNotVerified */ __('Mass Actions') ?>"> -+ <select id="<?= $block->escapeHtmlAttr($block->getHtmlId()) ?>-mass-select" data-menu="grid-mass-select"> -+ <optgroup label="<?= $block->escapeHtmlAttr(__('Mass Actions')) ?>"> - <option disabled selected></option> -- <?php if ($block->getUseSelectAll()):?> -+ <?php if ($block->getUseSelectAll()) :?> - <option value="selectAll"> -- <?= /* @escapeNotVerified */ __('Select All') ?> -+ <?= $block->escapeHtml(__('Select All')) ?> - </option> - <option value="unselectAll"> -- <?= /* @escapeNotVerified */ __('Unselect All') ?> -+ <?= $block->escapeHtml(__('Unselect All')) ?> - </option> - <?php endif; ?> - <option value="selectVisible"> -- <?= /* @escapeNotVerified */ __('Select Visible') ?> -+ <?= $block->escapeHtml(__('Select Visible')) ?> - </option> - <option value="unselectVisible"> -- <?= /* @escapeNotVerified */ __('Unselect Visible') ?> -+ <?= $block->escapeHtml(__('Unselect Visible')) ?> - </option> - </optgroup> - </select> -@@ -65,19 +64,19 @@ - $('#<?= $block->getHtmlId() ?>-mass-select').change(function () { - var massAction = $('option:selected', this).val(); - switch (massAction) { -- <?php if ($block->getUseSelectAll()):?> -+ <?php if ($block->getUseSelectAll()) :?> - case 'selectAll': -- return <?= /* @escapeNotVerified */ $block->getJsObjectName() ?>.selectAll(); -+ return <?= $block->escapeJs($block->getJsObjectName()) ?>.selectAll(); - break - case 'unselectAll': -- return <?= /* @escapeNotVerified */ $block->getJsObjectName() ?>.unselectAll(); -+ return <?= $block->escapeJs($block->getJsObjectName()) ?>.unselectAll(); - break - <?php endif; ?> - case 'selectVisible': -- return <?= /* @escapeNotVerified */ $block->getJsObjectName() ?>.selectVisible(); -+ return <?= $block->escapeJs($block->getJsObjectName()) ?>.selectVisible(); - break - case 'unselectVisible': -- return <?= /* @escapeNotVerified */ $block->getJsObjectName() ?>.unselectVisible(); -+ return <?= $block->escapeJs($block->getJsObjectName()) ?>.unselectVisible(); - break - } - this.blur(); -@@ -85,8 +84,8 @@ - - }); - -- <?php if (!$block->getParentBlock()->canDisplayContainer()): ?> -- <?= /* @escapeNotVerified */ $block->getJsObjectName() ?>.setGridIds('<?= /* @escapeNotVerified */ $block->getGridIdsJson() ?>'); -+ <?php if (!$block->getParentBlock()->canDisplayContainer()) :?> -+ <?= $block->escapeJs($block->getJsObjectName()) ?>.setGridIds('<?= /* @noEscape */ $block->getGridIdsJson() ?>'); - <?php endif; ?> - </script> - </div> -diff --git a/app/code/Magento/Catalog/view/adminhtml/templates/rss/grid/link.phtml b/app/code/Magento/Catalog/view/adminhtml/templates/rss/grid/link.phtml -index fb450d19312..668dc4a28a6 100644 ---- a/app/code/Magento/Catalog/view/adminhtml/templates/rss/grid/link.phtml -+++ b/app/code/Magento/Catalog/view/adminhtml/templates/rss/grid/link.phtml -@@ -4,10 +4,8 @@ - * See COPYING.txt for license details. - */ - --// @codingStandardsIgnoreFile -- - /** @var $block \Magento\Catalog\Block\Adminhtml\Rss\Grid\Link */ - ?> --<?php if ($block->isRssAllowed() && $block->getLink()): ?> --<a href="<?= /* @escapeNotVerified */ $block->getLink() ?>" class="link-feed"><?= /* @escapeNotVerified */ $block->getLabel() ?></a> -+<?php if ($block->isRssAllowed() && $block->getLink()) :?> -+<a href="<?= $block->escapeUrl($block->getLink()) ?>" class="link-feed"><?= $block->escapeHtml($block->getLabel()) ?></a> - <?php endif; ?> -diff --git a/app/code/Magento/Catalog/view/adminhtml/ui_component/category_form.xml b/app/code/Magento/Catalog/view/adminhtml/ui_component/category_form.xml -index dafea71f872..90d6e0b4840 100644 ---- a/app/code/Magento/Catalog/view/adminhtml/ui_component/category_form.xml -+++ b/app/code/Magento/Catalog/view/adminhtml/ui_component/category_form.xml -@@ -77,6 +77,13 @@ - <dataType>text</dataType> - </settings> - </field> -+ <field name="level" formElement="hidden"> -+ <argument name="data" xsi:type="array"> -+ <item name="config" xsi:type="array"> -+ <item name="source" xsi:type="string">category</item> -+ </item> -+ </argument> -+ </field> - <field name="store_id" formElement="hidden"> - <argument name="data" xsi:type="array"> - <item name="config" xsi:type="array"> -@@ -331,7 +338,6 @@ - <item name="type" xsi:type="string">group</item> - <item name="config" xsi:type="array"> - <item name="breakLine" xsi:type="boolean">true</item> -- <item name="required" xsi:type="boolean">true</item> - </item> - </argument> - <field name="filter_price_range" formElement="input"> -@@ -341,6 +347,9 @@ - </item> - </argument> - <settings> -+ <validation> -+ <rule name="required-entry" xsi:type="boolean">true</rule> -+ </validation> - <additionalClasses> - <class name="admin__field-small">true</class> - </additionalClasses> -diff --git a/app/code/Magento/Catalog/view/adminhtml/ui_component/design_config_form.xml b/app/code/Magento/Catalog/view/adminhtml/ui_component/design_config_form.xml -index 1e608239297..cb0beb67c27 100644 ---- a/app/code/Magento/Catalog/view/adminhtml/ui_component/design_config_form.xml -+++ b/app/code/Magento/Catalog/view/adminhtml/ui_component/design_config_form.xml -@@ -18,7 +18,7 @@ - <level>2</level> - <label translate="true">Base</label> - </settings> -- <field name="watermark_image_image" formElement="fileUploader"> -+ <field name="watermark_image_image" formElement="imageUploader"> - <settings> - <notice translate="true">Allowed file types: jpeg, gif, png.</notice> - <label translate="true">Image</label> -@@ -78,7 +78,7 @@ - <level>2</level> - <label translate="true">Thumbnail</label> - </settings> -- <field name="watermark_thumbnail_image" formElement="fileUploader"> -+ <field name="watermark_thumbnail_image" formElement="imageUploader"> - <settings> - <label translate="true">Image</label> - <componentType>imageUploader</componentType> -@@ -137,7 +137,7 @@ - <level>2</level> - <label translate="true">Small</label> - </settings> -- <field name="watermark_small_image_image" formElement="fileUploader"> -+ <field name="watermark_small_image_image" formElement="imageUploader"> - <settings> - <label translate="true">Image</label> - <componentType>imageUploader</componentType> -diff --git a/app/code/Magento/Catalog/view/adminhtml/ui_component/product_listing.xml b/app/code/Magento/Catalog/view/adminhtml/ui_component/product_listing.xml -index 65090fa3ac4..d2d6f098125 100644 ---- a/app/code/Magento/Catalog/view/adminhtml/ui_component/product_listing.xml -+++ b/app/code/Magento/Catalog/view/adminhtml/ui_component/product_listing.xml -@@ -132,6 +132,7 @@ - <settings> - <addField>true</addField> - <filter>text</filter> -+ <bodyTmpl>ui/grid/cells/html</bodyTmpl> - <label translate="true">Name</label> - </settings> - </column> -@@ -154,6 +155,7 @@ - <column name="sku" sortOrder="60"> - <settings> - <filter>text</filter> -+ <bodyTmpl>ui/grid/cells/html</bodyTmpl> - <label translate="true">SKU</label> - </settings> - </column> -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 75ee3019cf4..41f7a874c26 100644 ---- a/app/code/Magento/Catalog/view/adminhtml/web/catalog/category/edit.js -+++ b/app/code/Magento/Catalog/view/adminhtml/web/catalog/category/edit.js -@@ -82,7 +82,7 @@ define([ - return function (config, element) { - config = config || {}; - jQuery(element).on('click', function () { -- categorySubmit(config.url, config.ajax); -+ categorySubmit(); - }); - }; - }); -diff --git a/app/code/Magento/Catalog/view/adminhtml/web/catalog/category/form.js b/app/code/Magento/Catalog/view/adminhtml/web/catalog/category/form.js -index 0a04358e411..76aaddf55ac 100644 ---- a/app/code/Magento/Catalog/view/adminhtml/web/catalog/category/form.js -+++ b/app/code/Magento/Catalog/view/adminhtml/web/catalog/category/form.js -@@ -15,6 +15,7 @@ define([ - categoryIdSelector: 'input[name="id"]', - categoryPathSelector: 'input[name="path"]', - categoryParentSelector: 'input[name="parent"]', -+ categoryLevelSelector: 'input[name="level"]', - refreshUrl: config.refreshUrl - }, - -@@ -47,6 +48,7 @@ define([ - $(this.options.categoryIdSelector).val(data.id).change(); - $(this.options.categoryPathSelector).val(data.path).change(); - $(this.options.categoryParentSelector).val(data.parentId).change(); -+ $(this.options.categoryLevelSelector).val(data.level).change(); - } - } - }; -diff --git a/app/code/Magento/Catalog/view/adminhtml/web/catalog/product/composite/configure.js b/app/code/Magento/Catalog/view/adminhtml/web/catalog/product/composite/configure.js -index 6903a17bcdc..1ac2a4ffada 100644 ---- a/app/code/Magento/Catalog/view/adminhtml/web/catalog/product/composite/configure.js -+++ b/app/code/Magento/Catalog/view/adminhtml/web/catalog/product/composite/configure.js -@@ -91,8 +91,8 @@ define([ - - /** - * Add product list types as scope and their urls -- * expamle: addListType('product_to_add', {urlFetch: 'http://magento...'}) -- * expamle: addListType('wishlist', {urlSubmit: 'http://magento...'}) -+ * example: addListType('product_to_add', {urlFetch: 'http://magento...'}) -+ * example: addListType('wishlist', {urlSubmit: 'http://magento...'}) - * - * @param type types as scope - * @param urls obj can be -@@ -112,7 +112,7 @@ define([ - /** - * Adds complex list type - that is used to submit several list types at once - * Only urlSubmit is possible for this list type -- * expamle: addComplexListType(['wishlist', 'product_list'], 'http://magento...') -+ * example: addComplexListType(['wishlist', 'product_list'], 'http://magento...') - * - * @param type types as scope - * @param urls obj can be -@@ -469,26 +469,6 @@ define([ - } - }, - -- /** -- * toggles Selects states (for IE) except those to be shown in popup -- */ -- /*_toggleSelectsExceptBlock: function(flag) { -- if(Prototype.Browser.IE){ -- if (this.blockForm) { -- var states = new Array; -- var selects = this.blockForm.getElementsByTagName("select"); -- for(var i=0; i<selects.length; i++){ -- states[i] = selects[i].style.visibility -- } -- } -- if (this.blockForm) { -- for(i=0; i<selects.length; i++){ -- selects[i].style.visibility = states[i] -- } -- } -- } -- },*/ -- - /** - * Close configuration window - */ -diff --git a/app/code/Magento/Catalog/view/adminhtml/web/catalog/product/edit/attribute.js b/app/code/Magento/Catalog/view/adminhtml/web/catalog/product/edit/attribute.js -new file mode 100644 -index 00000000000..e1923dc46d6 ---- /dev/null -+++ b/app/code/Magento/Catalog/view/adminhtml/web/catalog/product/edit/attribute.js -@@ -0,0 +1,18 @@ -+/** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+ -+define([ -+ 'jquery', -+ 'mage/mage', -+ 'validation' -+], function ($) { -+ 'use strict'; -+ -+ return function (config, element) { -+ $(element).mage('form').validation({ -+ validationUrl: config.validationUrl -+ }); -+ }; -+}); -diff --git a/app/code/Magento/Catalog/view/adminhtml/web/js/category-checkbox-tree.js b/app/code/Magento/Catalog/view/adminhtml/web/js/category-checkbox-tree.js -index bc44128663c..0ec404a769f 100644 ---- a/app/code/Magento/Catalog/view/adminhtml/web/js/category-checkbox-tree.js -+++ b/app/code/Magento/Catalog/view/adminhtml/web/js/category-checkbox-tree.js -@@ -33,7 +33,6 @@ define([ - data = {}, - parameters = {}, - root = {}, -- len = 0, - key = ''; - - /** -@@ -160,15 +159,15 @@ define([ - * @returns {void} - */ - categoryLoader.buildCategoryTree = function (parent, nodeConfig) { -- var j = 0; -+ var i = 0; - - if (!nodeConfig) { - return null; - } - - if (parent && nodeConfig && nodeConfig.length) { -- for (j = 0; j < nodeConfig.length; j++) { -- categoryLoader.processCategoryTree(parent, nodeConfig, j); -+ for (i; i < nodeConfig.length; i++) { -+ categoryLoader.processCategoryTree(parent, nodeConfig, i); - } - } - }; -@@ -180,14 +179,15 @@ define([ - * @returns {Object} - */ - categoryLoader.buildHashChildren = function (hash, node) { -- var j = 0; -+ var i = 0, -+ len; - - if (node.childNodes.length > 0 || node.loaded === false && node.loading === false) { - hash.children = []; - -- for (j = 0, len = node.childNodes.length; j < len; j++) { -+ for (i, len = node.childNodes.length; i < len; i++) { - hash.children = hash.children ? hash.children : []; -- hash.children.push(this.buildHash(node.childNodes[j])); -+ hash.children.push(this.buildHash(node.childNodes[i])); - } - } - -@@ -225,6 +225,7 @@ define([ - - categoryLoader.on('beforeload', function (treeLoader, node) { - treeLoader.baseParams.id = node.attributes.id; -+ treeLoader.baseParams.selected = options.jsFormObject.updateElement.value; - }); - - categoryLoader.on('load', function () { -diff --git a/app/code/Magento/Catalog/view/adminhtml/web/js/category-tree.js b/app/code/Magento/Catalog/view/adminhtml/web/js/category-tree.js -index 9c66ce577a2..b71549c314d 100644 ---- a/app/code/Magento/Catalog/view/adminhtml/web/js/category-tree.js -+++ b/app/code/Magento/Catalog/view/adminhtml/web/js/category-tree.js -@@ -37,8 +37,8 @@ define([ - ajax: { - url: options.url, - type: 'POST', -- success: $.proxy(function (node) { -- return this._convertData(node[0]); -+ success: $.proxy(function (nodes) { -+ return this._convertDataNodes(nodes); - }, this), - - /** -@@ -77,6 +77,21 @@ define([ - } - }, - -+ /** -+ * @param {Array} nodes -+ * @returns {Array} -+ * @private -+ */ -+ _convertDataNodes: function (nodes) { -+ var nodesData = []; -+ -+ nodes.forEach(function (node) { -+ nodesData.push(this._convertData(node)); -+ }, this); -+ -+ return nodesData; -+ }, -+ - /** - * @param {Object} node - * @return {*} -diff --git a/app/code/Magento/Catalog/view/adminhtml/web/js/edit-tree.js b/app/code/Magento/Catalog/view/adminhtml/web/js/edit-tree.js -index 3544767b8d7..7c947195f33 100644 ---- a/app/code/Magento/Catalog/view/adminhtml/web/js/edit-tree.js -+++ b/app/code/Magento/Catalog/view/adminhtml/web/js/edit-tree.js -@@ -18,7 +18,10 @@ require([ - /** - * Delete some category - * This routine get categoryId explicitly, so even if currently selected tree node is out of sync -- * with this form, we surely delete same category in the tree and at backend -+ * with this form, we surely delete same category in the tree and at backend. -+ * -+ * @deprecated -+ * @see deleteConfirm - */ - function categoryDelete(url) { - confirm({ -diff --git a/app/code/Magento/Catalog/view/adminhtml/web/js/form/element/action-delete.js b/app/code/Magento/Catalog/view/adminhtml/web/js/form/element/action-delete.js -index 97f978de47b..f829c66c401 100644 ---- a/app/code/Magento/Catalog/view/adminhtml/web/js/form/element/action-delete.js -+++ b/app/code/Magento/Catalog/view/adminhtml/web/js/form/element/action-delete.js -@@ -5,10 +5,10 @@ - define([ - 'underscore', - 'Magento_Ui/js/form/element/abstract' --], function (_, Acstract) { -+], function (_, Abstract) { - 'use strict'; - -- return Acstract.extend({ -+ return Abstract.extend({ - defaults: { - prefixName: '', - prefixElementName: '', -diff --git a/app/code/Magento/Catalog/view/adminhtml/web/js/form/element/input.js b/app/code/Magento/Catalog/view/adminhtml/web/js/form/element/input.js -index 2f6703cc92e..4bbdea066b7 100644 ---- a/app/code/Magento/Catalog/view/adminhtml/web/js/form/element/input.js -+++ b/app/code/Magento/Catalog/view/adminhtml/web/js/form/element/input.js -@@ -5,10 +5,10 @@ - define([ - 'underscore', - 'Magento_Ui/js/form/element/abstract' --], function (_, Acstract) { -+], function (_, Abstract) { - 'use strict'; - -- return Acstract.extend({ -+ return Abstract.extend({ - defaults: { - prefixName: '', - prefixElementName: '', -diff --git a/app/code/Magento/Catalog/view/adminhtml/web/js/options.js b/app/code/Magento/Catalog/view/adminhtml/web/js/options.js -index 6ea00591576..7adc0dcfdf4 100644 ---- a/app/code/Magento/Catalog/view/adminhtml/web/js/options.js -+++ b/app/code/Magento/Catalog/view/adminhtml/web/js/options.js -@@ -20,7 +20,6 @@ define([ - - return function (config) { - var optionPanel = jQuery('#manage-options-panel'), -- optionsValues = [], - editForm = jQuery('#edit_form'), - attributeOption = { - table: $('attribute-options-table'), -@@ -145,7 +144,9 @@ define([ - - return optionDefaultInputType; - } -- }; -+ }, -+ tableBody = jQuery(), -+ activePanelClass = 'selected-type-options'; - - if ($('add_new_option_button')) { - Event.observe('add_new_option_button', 'click', attributeOption.add.bind(attributeOption, {}, true)); -@@ -180,30 +181,32 @@ define([ - }); - }); - } -- editForm.on('submit', function () { -- optionPanel.find('input') -- .each(function () { -- if (this.disabled) { -- return; -+ editForm.on('beforeSubmit', function () { -+ var optionContainer = optionPanel.find('table tbody'), -+ optionsValues; -+ -+ if (optionPanel.hasClass(activePanelClass)) { -+ optionsValues = jQuery.map( -+ optionContainer.find('tr'), -+ function (row) { -+ return jQuery(row).find('input, select, textarea').serialize(); - } -- -- if (this.type === 'checkbox' || this.type === 'radio') { -- if (this.checked) { -- optionsValues.push(this.name + '=' + jQuery(this).val()); -- } -- } else { -- optionsValues.push(this.name + '=' + jQuery(this).val()); -- } -- }); -- jQuery('<input>') -- .attr({ -- type: 'hidden', -- name: 'serialized_options' -- }) -- .val(JSON.stringify(optionsValues)) -- .prependTo(editForm); -- optionPanel.find('table') -- .replaceWith(jQuery('<div>').text(jQuery.mage.__('Sending attribute values as package.'))); -+ ); -+ jQuery('<input>') -+ .attr({ -+ type: 'hidden', -+ name: 'serialized_options' -+ }) -+ .val(JSON.stringify(optionsValues)) -+ .prependTo(editForm); -+ } -+ tableBody = optionContainer.detach(); -+ }); -+ editForm.on('afterValidate.error highlight.validate', function () { -+ if (optionPanel.hasClass(activePanelClass)) { -+ optionPanel.find('table').append(tableBody); -+ jQuery('input[name="serialized_options"]').remove(); -+ } - }); - window.attributeOption = attributeOption; - window.optionDefaultInputType = attributeOption.getOptionInputType(); -diff --git a/app/code/Magento/Catalog/view/base/templates/js/components.phtml b/app/code/Magento/Catalog/view/base/templates/js/components.phtml -index bad5acc209b..5902a9f25cc 100644 ---- a/app/code/Magento/Catalog/view/base/templates/js/components.phtml -+++ b/app/code/Magento/Catalog/view/base/templates/js/components.phtml -@@ -3,8 +3,5 @@ - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -- --// @codingStandardsIgnoreFile -- - ?> - <?= $block->getChildHtml() ?> -diff --git a/app/code/Magento/Catalog/view/base/templates/product/composite/fieldset/options/view/checkable.phtml b/app/code/Magento/Catalog/view/base/templates/product/composite/fieldset/options/view/checkable.phtml -new file mode 100644 -index 00000000000..dbc064665d3 ---- /dev/null -+++ b/app/code/Magento/Catalog/view/base/templates/product/composite/fieldset/options/view/checkable.phtml -@@ -0,0 +1,81 @@ -+<?php -+/** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+ -+use Magento\Catalog\Model\Product\Option; -+ -+/** -+ * @var $block \Magento\Catalog\Block\Product\View\Options\Type\Select\Checkable -+ */ -+$option = $block->getOption(); -+if ($option) : ?> -+ <?php -+ $configValue = $block->getPreconfiguredValue($option); -+ $optionType = $option->getType(); -+ $arraySign = $optionType === Option::OPTION_TYPE_CHECKBOX ? '[]' : ''; -+ $count = 1; -+ ?> -+ -+<div class="options-list nested" id="options-<?= $block->escapeHtmlAttr($option->getId()) ?>-list"> -+ <?php if ($optionType === Option::OPTION_TYPE_RADIO && !$option->getIsRequire()) :?> -+ <div class="field choice admin__field admin__field-option"> -+ <input type="radio" -+ id="options_<?= $block->escapeHtmlAttr($option->getId()) ?>" -+ class="radio admin__control-radio product-custom-option" -+ name="options[<?= $block->escapeHtmlAttr($option->getId()) ?>]" -+ data-selector="options[<?= $block->escapeHtmlAttr($option->getId()) ?>]" -+ onclick="<?= $block->getSkipJsReloadPrice() ? '' : 'opConfig.reloadPrice()' ?>" -+ value="" -+ checked="checked" -+ /> -+ <label class="label admin__field-label" for="options_<?= $block->escapeHtmlAttr($option->getId()) ?>"> -+ <span> -+ <?= $block->escapeHtml(__('None')) ?> -+ </span> -+ </label> -+ </div> -+<?php endif; ?> -+ -+ <?php foreach ($option->getValues() as $value) : ?> -+ <?php -+ $checked = ''; -+ $count++; -+ if ($arraySign) { -+ $checked = is_array($configValue) && in_array($value->getOptionTypeId(), $configValue) ? 'checked' : ''; -+ } else { -+ $checked = $configValue == $value->getOptionTypeId() ? 'checked' : ''; -+ } -+ $dataSelector = 'options[' . $option->getId() . ']'; -+ if ($arraySign) { -+ $dataSelector .= '[' . $value->getOptionTypeId() . ']'; -+ } -+ ?> -+ -+ <div class="field choice admin__field admin__field-option <?= /* @noEscape */ $option->getIsRequire() ? 'required': '' ?>"> -+ <input type="<?= $block->escapeHtmlAttr($optionType) ?>" -+ class="<?= $optionType === Option::OPTION_TYPE_RADIO -+ ? 'radio admin__control-radio' -+ : 'checkbox admin__control-checkbox' ?> <?= $option->getIsRequire() -+ ? 'required': '' ?> -+ product-custom-option -+ <?= $block->getSkipJsReloadPrice() ? '' : 'opConfig.reloadPrice()' ?>" -+ name="options[<?= $block->escapeHtmlAttr($option->getId()) ?>]<?= /* @noEscape */ $arraySign ?>" -+ id="options_<?= $block->escapeHtmlAttr($option->getId() . '_' . $count) ?>" -+ value="<?= $block->escapeHtmlAttr($value->getOptionTypeId()) ?>" -+ <?= $block->escapeHtml($checked) ?> -+ data-selector="<?= $block->escapeHtmlAttr($dataSelector) ?>" -+ price="<?= $block->escapeHtmlAttr($block->getCurrencyByStore($value)) ?>" -+ /> -+ <label class="label admin__field-label" -+ for="options_<?= $block->escapeHtmlAttr($option->getId() . '_' . $count) ?>"> -+ <span> -+ <?= $block->escapeHtml($value->getTitle()) ?> -+ </span> -+ <?= /* @noEscape */ $block->formatPrice($value) ?> -+ </label> -+ </div> -+ <?php endforeach; ?> -+ </div> -+<?php endif; ?> -diff --git a/app/code/Magento/Catalog/view/base/templates/product/price/amount/default.phtml b/app/code/Magento/Catalog/view/base/templates/product/price/amount/default.phtml -index ce1561e382e..b2c2acb7419 100644 ---- a/app/code/Magento/Catalog/view/base/templates/product/price/amount/default.phtml -+++ b/app/code/Magento/Catalog/view/base/templates/product/price/amount/default.phtml -@@ -3,29 +3,26 @@ - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -- --// @codingStandardsIgnoreFile -- - ?> - --<?php /** @var \Magento\Framework\Pricing\Render\Amount $block */ ?> -+<?php /** @var $block \Magento\Framework\Pricing\Render\Amount */ ?> - --<span class="price-container <?= /* @escapeNotVerified */ $block->getAdjustmentCssClasses() ?>" -+<span class="price-container <?= $block->escapeHtmlAttr($block->getAdjustmentCssClasses()) ?>" - <?= $block->getSchema() ? ' itemprop="offers" itemscope itemtype="http://schema.org/Offer"' : '' ?>> -- <?php if ($block->getDisplayLabel()): ?> -- <span class="price-label"><?= /* @escapeNotVerified */ $block->getDisplayLabel() ?></span> -+ <?php if ($block->getDisplayLabel()) :?> -+ <span class="price-label"><?= $block->escapeHtml($block->getDisplayLabel()) ?></span> - <?php endif; ?> -- <span <?php if ($block->getPriceId()): ?> id="<?= /* @escapeNotVerified */ $block->getPriceId() ?>"<?php endif;?> -- <?= ($block->getPriceDisplayLabel()) ? 'data-label="' . $block->getPriceDisplayLabel() . $block->getPriceDisplayInclExclTaxes() . '"' : '' ?> -- data-price-amount="<?= /* @escapeNotVerified */ $block->getDisplayValue() ?>" -- data-price-type="<?= /* @escapeNotVerified */ $block->getPriceType() ?>" -- class="price-wrapper <?= /* @escapeNotVerified */ $block->getPriceWrapperCss() ?>" -- ><?= /* @escapeNotVerified */ $block->formatCurrency($block->getDisplayValue(), (bool)$block->getIncludeContainer()) ?></span> -- <?php if ($block->hasAdjustmentsHtml()): ?> -+ <span <?php if ($block->getPriceId()) :?> id="<?= $block->escapeHtmlAttr($block->getPriceId()) ?>"<?php endif;?> -+ <?= ($block->getPriceDisplayLabel()) ? 'data-label="' . $block->escapeHtmlAttr($block->getPriceDisplayLabel() . $block->getPriceDisplayInclExclTaxes()) . '"' : '' ?> -+ data-price-amount="<?= $block->escapeHtmlAttr($block->getDisplayValue()) ?>" -+ data-price-type="<?= $block->escapeHtmlAttr($block->getPriceType()) ?>" -+ class="price-wrapper <?= $block->escapeHtmlAttr($block->getPriceWrapperCss()) ?>" -+ ><?= $block->escapeHtml($block->formatCurrency($block->getDisplayValue(), (bool)$block->getIncludeContainer()), ['span']) ?></span> -+ <?php if ($block->hasAdjustmentsHtml()) :?> - <?= $block->getAdjustmentsHtml() ?> - <?php endif; ?> -- <?php if ($block->getSchema()): ?> -- <meta itemprop="price" content="<?= /* @escapeNotVerified */ $block->getDisplayValue() ?>" /> -- <meta itemprop="priceCurrency" content="<?= /* @escapeNotVerified */ $block->getDisplayCurrencyCode() ?>" /> -+ <?php if ($block->getSchema()) :?> -+ <meta itemprop="price" content="<?= $block->escapeHtmlAttr($block->getDisplayValue()) ?>" /> -+ <meta itemprop="priceCurrency" content="<?= $block->escapeHtmlAttr($block->getDisplayCurrencyCode()) ?>" /> - <?php endif; ?> - </span> -diff --git a/app/code/Magento/Catalog/view/base/templates/product/price/default.phtml b/app/code/Magento/Catalog/view/base/templates/product/price/default.phtml -index b414f02a3d6..7005e65bcca 100644 ---- a/app/code/Magento/Catalog/view/base/templates/product/price/default.phtml -+++ b/app/code/Magento/Catalog/view/base/templates/product/price/default.phtml -@@ -3,9 +3,6 @@ - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -- --// @codingStandardsIgnoreFile -- - ?> - - <?php -@@ -15,7 +12,7 @@ - /** @var \Magento\Framework\Pricing\Price\PriceInterface $priceModel */ - $priceModel = $block->getPriceType('regular_price'); - --/* @escapeNotVerified */ echo $block->renderAmount($priceModel->getAmount(), [ -+/* @noEscape */ echo $block->renderAmount($priceModel->getAmount(), [ - 'price_id' => $block->getPriceId('product-price-'), - 'include_container' => true - ]); -diff --git a/app/code/Magento/Catalog/view/base/templates/product/price/final_price.phtml b/app/code/Magento/Catalog/view/base/templates/product/price/final_price.phtml -index 6e281bdef7a..e56804a06de 100644 ---- a/app/code/Magento/Catalog/view/base/templates/product/price/final_price.phtml -+++ b/app/code/Magento/Catalog/view/base/templates/product/price/final_price.phtml -@@ -3,9 +3,6 @@ - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -- --// @codingStandardsIgnoreFile -- - ?> - - <?php -@@ -21,9 +18,9 @@ $finalPriceModel = $block->getPriceType('final_price'); - $idSuffix = $block->getIdSuffix() ? $block->getIdSuffix() : ''; - $schema = ($block->getZone() == 'item_view') ? true : false; - ?> --<?php if ($block->hasSpecialPrice()): ?> -+<?php if ($block->hasSpecialPrice()) :?> - <span class="special-price"> -- <?php /* @escapeNotVerified */ echo $block->renderAmount($finalPriceModel->getAmount(), [ -+ <?= /* @noEscape */ $block->renderAmount($finalPriceModel->getAmount(), [ - 'display_label' => __('Special Price'), - 'price_id' => $block->getPriceId('product-price-' . $idSuffix), - 'price_type' => 'finalPrice', -@@ -32,7 +29,7 @@ $schema = ($block->getZone() == 'item_view') ? true : false; - ]); ?> - </span> - <span class="old-price"> -- <?php /* @escapeNotVerified */ echo $block->renderAmount($priceModel->getAmount(), [ -+ <?= /* @noEscape */ $block->renderAmount($priceModel->getAmount(), [ - 'display_label' => __('Regular Price'), - 'price_id' => $block->getPriceId('old-price-' . $idSuffix), - 'price_type' => 'oldPrice', -@@ -40,8 +37,8 @@ $schema = ($block->getZone() == 'item_view') ? true : false; - 'skip_adjustments' => true - ]); ?> - </span> --<?php else: ?> -- <?php /* @escapeNotVerified */ echo $block->renderAmount($finalPriceModel->getAmount(), [ -+<?php else :?> -+ <?= /* @noEscape */ $block->renderAmount($finalPriceModel->getAmount(), [ - 'price_id' => $block->getPriceId('product-price-' . $idSuffix), - 'price_type' => 'finalPrice', - 'include_container' => true, -@@ -49,14 +46,14 @@ $schema = ($block->getZone() == 'item_view') ? true : false; - ]); ?> - <?php endif; ?> - --<?php if ($block->showMinimalPrice()): ?> -- <?php if ($block->getUseLinkForAsLowAs()):?> -- <a href="<?= /* @escapeNotVerified */ $block->getSaleableItem()->getProductUrl() ?>" class="minimal-price-link"> -- <?= /* @escapeNotVerified */ $block->renderAmountMinimal() ?> -+<?php if ($block->showMinimalPrice()) :?> -+ <?php if ($block->getUseLinkForAsLowAs()) :?> -+ <a href="<?= $block->escapeUrl($block->getSaleableItem()->getProductUrl()) ?>" class="minimal-price-link"> -+ <?= /* @noEscape */ $block->renderAmountMinimal() ?> - </a> -- <?php else:?> -+ <?php else :?> - <span class="minimal-price-link"> -- <?= /* @escapeNotVerified */ $block->renderAmountMinimal() ?> -+ <?= /* @noEscape */ $block->renderAmountMinimal() ?> - </span> - <?php endif?> - <?php endif; ?> -diff --git a/app/code/Magento/Catalog/view/base/templates/product/price/tier_prices.phtml b/app/code/Magento/Catalog/view/base/templates/product/price/tier_prices.phtml -index f5cffb99d75..5949b54268a 100644 ---- a/app/code/Magento/Catalog/view/base/templates/product/price/tier_prices.phtml -+++ b/app/code/Magento/Catalog/view/base/templates/product/price/tier_prices.phtml -@@ -3,12 +3,12 @@ - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -- --// @codingStandardsIgnoreFile -- - ?> - - <?php -+// phpcs:disable Magento2.Templates.ThisInTemplate -+// phpcs:disable Generic.WhiteSpace.ScopeIndent -+ - /** @var \Magento\Catalog\Pricing\Render\PriceBox $block */ - - /** @var \Magento\Catalog\Pricing\Price\TierPrice $tierPriceModel */ -@@ -18,17 +18,17 @@ $msrpShowOnGesture = $block->getPriceType('msrp_price')->isShowPriceOnGesture(); - $product = $block->getSaleableItem(); - ?> - <?php if (count($tierPrices)) : ?> -- <ul class="<?= /* @escapeNotVerified */ ($block->hasListClass() ? $block->getListClass() : 'prices-tier items') ?>"> -- <?php foreach ($tierPrices as $index => $price) : ?> -- <li class="item"> -- <?php -+ <ul class="<?= $block->escapeHtmlAttr(($block->hasListClass() ? $block->getListClass() : 'prices-tier items')) ?>"> -+ <?php foreach ($tierPrices as $index => $price) : ?> -+ <li class="item"> -+ <?php - $productId = $product->getId(); - $isSaleable = $product->isSaleable(); - $popupId = 'msrp-popup-' . $productId . $block->getRandomString(20); -- if ($msrpShowOnGesture && $price['price']->getValue() < $product->getMsrp()): -+ if ($msrpShowOnGesture && $price['price']->getValue() < $product->getMsrp()) : - $addToCartUrl = ''; - if ($isSaleable) { -- $addToCartUrl = $this->helper('\Magento\Checkout\Helper\Cart') -+ $addToCartUrl = $this->helper(\Magento\Checkout\Helper\Cart::class) - ->getAddUrl($product, ['qty' => $price['price_qty']]); - } - $tierPriceData = [ -@@ -54,13 +54,13 @@ $product = $block->getSaleableItem(); - if ($block->getCanDisplayQty($product)) { - $tierPriceData['qty'] = $price['price_qty']; - } -- ?> -- <?= /* @escapeNotVerified */ __('Buy %1 for: ', $price['price_qty']) ?> -- <a href="javascript:void(0);" -- id="<?= /* @escapeNotVerified */ ($popupId) ?>" -- data-tier-price="<?= $block->escapeHtml($block->jsonEncode($tierPriceData)) ?>"> -- <?= /* @escapeNotVerified */ __('Click for price') ?></a> -- <?php else: -+ ?> -+ <?= $block->escapeHtml(__('Buy %1 for: ', $price['price_qty'])) ?> -+ <a href="javascript:void(0);" -+ id="<?= $block->escapeHtmlAttr($popupId) ?>" -+ data-tier-price="<?= $block->escapeHtml($block->jsonEncode($tierPriceData)) ?>"> -+ <?= $block->escapeHtml(__('Click for price')) ?></a> -+ <?php else : - $priceAmountBlock = $block->renderAmount( - $price['price'], - [ -@@ -70,22 +70,22 @@ $product = $block->getSaleableItem(); - 'zone' => \Magento\Framework\Pricing\Render::ZONE_ITEM_OPTION - ] - ); -- ?> -- <?php /* @escapeNotVerified */ echo ($block->getShowDetailedPrice() !== false) -- ? __( -- 'Buy %1 for %2 each and <strong class="benefit">save<span class="percent tier-%3"> %4</span>%</strong>', -- $price['price_qty'], -- $priceAmountBlock, -- $index, -- $tierPriceModel->getSavePercent($price['price']) -- ) -- : __('Buy %1 for %2 each', $price['price_qty'], $priceAmountBlock); -- ?> -- <?php endif; ?> -- </li> -- <?php endforeach; ?> -+ ?> -+ <?= /* @noEscape */ ($block->getShowDetailedPrice() !== false) -+ ? __( -+ 'Buy %1 for %2 each and <strong class="benefit">save<span class="percent tier-%3"> %4</span>%</strong>', -+ $price['price_qty'], -+ $priceAmountBlock, -+ $index, -+ $block->formatPercent($price['percentage_value'] ?? $tierPriceModel->getSavePercent($price['price'])) -+ ) -+ : __('Buy %1 for %2 each', $price['price_qty'], $priceAmountBlock); -+ ?> -+ <?php endif; ?> -+ </li> -+ <?php endforeach; ?> - </ul> -- <?php if ($msrpShowOnGesture):?> -+ <?php if ($msrpShowOnGesture) :?> - <script type="text/x-magento-init"> - { - ".product-info-main": { -@@ -95,9 +95,9 @@ $product = $block->getSaleableItem(); - "inputQty": "#qty", - "attr": "[data-tier-price]", - "productForm": "#product_addtocart_form", -- "productId": "<?= /* @escapeNotVerified */ $productId ?>", -+ "productId": "<?= (int) $productId ?>", - "productIdInput": "input[type=hidden][name=product]", -- "isSaleable": "<?= /* @escapeNotVerified */ $isSaleable ?>" -+ "isSaleable": "<?= (bool) $isSaleable ?>" - } - } - } -diff --git a/app/code/Magento/Catalog/view/base/web/js/price-utils.js b/app/code/Magento/Catalog/view/base/web/js/price-utils.js -index e2ea42f7d5f..7b83d12cc98 100644 ---- a/app/code/Magento/Catalog/view/base/web/js/price-utils.js -+++ b/app/code/Magento/Catalog/view/base/web/js/price-utils.js -@@ -60,7 +60,7 @@ define([ - pattern = pattern.indexOf('{sign}') < 0 ? s + pattern : pattern.replace('{sign}', s); - - // we're avoiding the usage of to fixed, and using round instead with the e representation to address -- // numbers like 1.005 = 1.01. Using ToFixed to only provide trailig zeroes in case we have a whole number -+ // numbers like 1.005 = 1.01. Using ToFixed to only provide trailing zeroes in case we have a whole number - i = parseInt( - amount = Number(Math.round(Math.abs(+amount || 0) + 'e+' + precision) + ('e-' + precision)), - 10 -diff --git a/app/code/Magento/Catalog/view/frontend/layout/catalog_product_view.xml b/app/code/Magento/Catalog/view/frontend/layout/catalog_product_view.xml -index 3630fddb326..13e2d998f6c 100644 ---- a/app/code/Magento/Catalog/view/frontend/layout/catalog_product_view.xml -+++ b/app/code/Magento/Catalog/view/frontend/layout/catalog_product_view.xml -@@ -91,7 +91,11 @@ - <container name="product.info.social" label="Product social links container" htmlTag="div" htmlClass="product-social-links"> - <block class="Magento\Catalog\Block\Product\View" name="product.info.addto" as="addto" template="Magento_Catalog::product/view/addto.phtml"> - <block class="Magento\Catalog\Block\Product\View\AddTo\Compare" name="view.addto.compare" after="view.addto.wishlist" -- template="Magento_Catalog::product/view/addto/compare.phtml" /> -+ template="Magento_Catalog::product/view/addto/compare.phtml" > -+ <arguments> -+ <argument name="addToCompareViewModel" xsi:type="object">Magento\Catalog\ViewModel\Product\Checker\AddToCompareAvailability</argument> -+ </arguments> -+ </block> - </block> - <block class="Magento\Catalog\Block\Product\View" name="product.info.mailto" template="Magento_Catalog::product/view/mailto.phtml"/> - </container> -@@ -121,7 +125,12 @@ - </arguments> - </block> - </container> -- <block class="Magento\Catalog\Block\Product\View\Gallery" name="product.info.media.image" template="Magento_Catalog::product/view/gallery.phtml"/> -+ <block class="Magento\Catalog\Block\Product\View\Gallery" name="product.info.media.image" template="Magento_Catalog::product/view/gallery.phtml"> -+ <arguments> -+ <argument name="gallery_options" xsi:type="object">Magento\Catalog\Block\Product\View\GalleryOptions</argument> -+ <argument name="imageHelper" xsi:type="object">Magento\Catalog\Helper\Image</argument> -+ </arguments> -+ </block> - <container name="skip_gallery_after.wrapper" htmlTag="div" htmlClass="action-skip-wrapper"> - <block class="Magento\Framework\View\Element\Template" after="product.info.media.image" name="skip_gallery_after" template="Magento_Theme::html/skip.phtml"> - <arguments> -@@ -136,7 +145,7 @@ - </arguments> - </block> - </container> -- <block class="Magento\Catalog\Block\Product\View\Description" name="product.info.details" template="Magento_Catalog::product/view/details.phtml" after="product.info.media"> -+ <block class="Magento\Catalog\Block\Product\View\Details" name="product.info.details" template="Magento_Catalog::product/view/details.phtml" after="product.info.media"> - <block class="Magento\Catalog\Block\Product\View\Description" name="product.info.description" as="description" template="Magento_Catalog::product/view/attribute.phtml" group="detailed_info"> - <arguments> - <argument name="at_call" xsi:type="string">getDescription</argument> -@@ -144,11 +153,13 @@ - <argument name="css_class" xsi:type="string">description</argument> - <argument name="at_label" xsi:type="string">none</argument> - <argument name="title" translate="true" xsi:type="string">Details</argument> -+ <argument name="sort_order" xsi:type="string">10</argument> - </arguments> - </block> - <block class="Magento\Catalog\Block\Product\View\Attributes" name="product.attributes" as="additional" template="Magento_Catalog::product/view/attributes.phtml" group="detailed_info"> - <arguments> - <argument translate="true" name="title" xsi:type="string">More Information</argument> -+ <argument name="sort_order" xsi:type="string">20</argument> - </arguments> - </block> - </block> -diff --git a/app/code/Magento/Catalog/view/frontend/templates/category/cms.phtml b/app/code/Magento/Catalog/view/frontend/templates/category/cms.phtml -index 3619ce94031..b50095e91d9 100644 ---- a/app/code/Magento/Catalog/view/frontend/templates/category/cms.phtml -+++ b/app/code/Magento/Catalog/view/frontend/templates/category/cms.phtml -@@ -3,9 +3,6 @@ - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -- --// @codingStandardsIgnoreFile -- - ?> - <?php - /** -@@ -14,7 +11,7 @@ - * @var $block \Magento\Catalog\Block\Category\View - */ - ?> --<?php if ($block->isContentMode() || $block->isMixedMode()): ?> -+<?php if ($block->isContentMode() || $block->isMixedMode()) :?> - <div class="category-cms"> - <?= $block->getCmsBlockHtml() ?> - </div> -diff --git a/app/code/Magento/Catalog/view/frontend/templates/category/description.phtml b/app/code/Magento/Catalog/view/frontend/templates/category/description.phtml -index 0efce1014f9..2f5b852575c 100644 ---- a/app/code/Magento/Catalog/view/frontend/templates/category/description.phtml -+++ b/app/code/Magento/Catalog/view/frontend/templates/category/description.phtml -@@ -3,19 +3,22 @@ - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -- --// @codingStandardsIgnoreFile -- - ?> - <?php -+// phpcs:disable Magento2.Templates.ThisInTemplate.FoundThis -+ - /** - * Category view template - * - * @var $block \Magento\Catalog\Block\Category\View - */ - ?> --<?php if ($_description = $block->getCurrentCategory()->getDescription()): ?> -+<?php if ($_description = $block->getCurrentCategory()->getDescription()) :?> - <div class="category-description"> -- <?= /* @escapeNotVerified */ $this->helper('Magento\Catalog\Helper\Output')->categoryAttribute($block->getCurrentCategory(), $_description, 'description') ?> -+ <?= /* @noEscape */ $this->helper(Magento\Catalog\Helper\Output::class)->categoryAttribute( -+ $block->getCurrentCategory(), -+ $_description, -+ 'description' -+ ) ?> - </div> - <?php endif; ?> -diff --git a/app/code/Magento/Catalog/view/frontend/templates/category/image.phtml b/app/code/Magento/Catalog/view/frontend/templates/category/image.phtml -index edff2147ad1..02593d3b541 100644 ---- a/app/code/Magento/Catalog/view/frontend/templates/category/image.phtml -+++ b/app/code/Magento/Catalog/view/frontend/templates/category/image.phtml -@@ -3,9 +3,6 @@ - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -- --// @codingStandardsIgnoreFile -- - ?> - <?php - /** -@@ -13,14 +10,24 @@ - * - * @var $block \Magento\Catalog\Block\Category\View - */ -+ -+// phpcs:disable Magento2.Templates.ThisInTemplate.FoundThis -+// phpcs:disable Generic.WhiteSpace.ScopeIndent.IncorrectExact -+// phpcs:disable Magento2.Security.LanguageConstruct.DirectOutput - ?> - <?php -- $_helper = $this->helper('Magento\Catalog\Helper\Output'); -+ $_helper = $this->helper(Magento\Catalog\Helper\Output::class); - $_category = $block->getCurrentCategory(); - $_imgHtml = ''; - if ($_imgUrl = $_category->getImageUrl()) { -- $_imgHtml = '<div class="category-image"><img src="' . $_imgUrl . '" alt="' . $block->escapeHtml($_category->getName()) . '" title="' . $block->escapeHtml($_category->getName()) . '" class="image" /></div>'; -+ $_imgHtml = '<div class="category-image"><img src="' -+ . $block->escapeUrl($_imgUrl) -+ . '" alt="' -+ . $block->escapeHtmlAttr($_category->getName()) -+ . '" title="' -+ . $block->escapeHtmlAttr($_category->getName()) -+ . '" class="image" /></div>'; - $_imgHtml = $_helper->categoryAttribute($_category, $_imgHtml, 'image'); -- /* @escapeNotVerified */ echo $_imgHtml; -+ /* @noEscape */ echo $_imgHtml; - } - ?> -diff --git a/app/code/Magento/Catalog/view/frontend/templates/category/products.phtml b/app/code/Magento/Catalog/view/frontend/templates/category/products.phtml -index c521cf03ad1..80a9ae0a03e 100644 ---- a/app/code/Magento/Catalog/view/frontend/templates/category/products.phtml -+++ b/app/code/Magento/Catalog/view/frontend/templates/category/products.phtml -@@ -3,9 +3,6 @@ - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -- --// @codingStandardsIgnoreFile -- - ?> - <?php - /** -@@ -14,6 +11,6 @@ - * @var $block \Magento\Catalog\Block\Category\View - */ - ?> --<?php if (!$block->isContentMode() || $block->isMixedMode()): ?> -+<?php if (!$block->isContentMode() || $block->isMixedMode()) :?> - <?= $block->getProductListHtml() ?> - <?php endif; ?> -diff --git a/app/code/Magento/Catalog/view/frontend/templates/category/rss.phtml b/app/code/Magento/Catalog/view/frontend/templates/category/rss.phtml -index 774aa8d839e..65ee7ea789e 100644 ---- a/app/code/Magento/Catalog/view/frontend/templates/category/rss.phtml -+++ b/app/code/Magento/Catalog/view/frontend/templates/category/rss.phtml -@@ -4,9 +4,9 @@ - * See COPYING.txt for license details. - */ - --// @codingStandardsIgnoreFile -- -+/** @var $block \Magento\Catalog\Block\Category\Rss\Link */ - ?> --<?php if ($block->isRssAllowed() && $block->getLink() && $block->isTopCategory()): ?> -- <a href="<?= /* @escapeNotVerified */ $block->getLink() ?>" class="action link rss"><span><?= /* @escapeNotVerified */ $block->getLabel() ?></span></a> -+<?php if ($block->isRssAllowed() && $block->getLink() && $block->isTopCategory()) :?> -+ <a href="<?= $block->escapeUrl($block->getLink()) ?>" -+ class="action link rss"><span><?= $block->escapeHtml($block->getLabel()) ?></span></a> - <?php endif; ?> -diff --git a/app/code/Magento/Catalog/view/frontend/templates/category/widget/link/link_block.phtml b/app/code/Magento/Catalog/view/frontend/templates/category/widget/link/link_block.phtml -index 2b0098be654..15fdd30c2d9 100644 ---- a/app/code/Magento/Catalog/view/frontend/templates/category/widget/link/link_block.phtml -+++ b/app/code/Magento/Catalog/view/frontend/templates/category/widget/link/link_block.phtml -@@ -3,9 +3,7 @@ - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -- --// @codingStandardsIgnoreFile - ?> - <div class="widget block block-category-link"> -- <a <?= /* @escapeNotVerified */ $block->getLinkAttributes() ?>><span><?= $block->escapeHtml($block->getLabel()) ?></span></a> -+ <a <?= /* @noEscape */ $block->getLinkAttributes() ?>><span><?= $block->escapeHtml($block->getLabel()) ?></span></a> - </div> -diff --git a/app/code/Magento/Catalog/view/frontend/templates/category/widget/link/link_href.phtml b/app/code/Magento/Catalog/view/frontend/templates/category/widget/link/link_href.phtml -index 91ab70b0376..18ffee4b5f7 100644 ---- a/app/code/Magento/Catalog/view/frontend/templates/category/widget/link/link_href.phtml -+++ b/app/code/Magento/Catalog/view/frontend/templates/category/widget/link/link_href.phtml -@@ -4,7 +4,6 @@ - * See COPYING.txt for license details. - */ - --// @codingStandardsIgnoreFile - /** @var Magento\Catalog\Block\Widget\Link $block */ - ?> - <?= $block->escapeHtml($block->getHref()) ?> -diff --git a/app/code/Magento/Catalog/view/frontend/templates/category/widget/link/link_inline.phtml b/app/code/Magento/Catalog/view/frontend/templates/category/widget/link/link_inline.phtml -index f53c1c1ed90..8f3b2ae6137 100644 ---- a/app/code/Magento/Catalog/view/frontend/templates/category/widget/link/link_inline.phtml -+++ b/app/code/Magento/Catalog/view/frontend/templates/category/widget/link/link_inline.phtml -@@ -3,9 +3,7 @@ - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -- --// @codingStandardsIgnoreFile - ?> - <span class="widget block block-category-link-inline"> -- <a <?= /* @escapeNotVerified */ $block->getLinkAttributes() ?>><span><?= $block->escapeHtml($block->getLabel()) ?></span></a> -+ <a <?= /* @noEscape */ $block->getLinkAttributes() ?>><span><?= $block->escapeHtml($block->getLabel()) ?></span></a> - </span> -diff --git a/app/code/Magento/Catalog/view/frontend/templates/frontend_storage_manager.phtml b/app/code/Magento/Catalog/view/frontend/templates/frontend_storage_manager.phtml -index 4c103b40ba2..52bec7858a9 100644 ---- a/app/code/Magento/Catalog/view/frontend/templates/frontend_storage_manager.phtml -+++ b/app/code/Magento/Catalog/view/frontend/templates/frontend_storage_manager.phtml -@@ -3,7 +3,8 @@ - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ --// @codingStandardsIgnoreFile -+ -+/** @var $block Magento\Catalog\Block\FrontendStorageManager */ - ?> - <script type="text/x-magento-init"> - { -@@ -12,9 +13,8 @@ - "components": { - "storage-manager": { - "component": "Magento_Catalog/js/storage-manager", -- "appendTo": "<?= /* @escapeNotVerified */ $block->getParentComponentName() ?>", -- "storagesConfiguration" : -- <?= /* @escapeNotVerified */ $block->getConfigurationJson() ?> -+ "appendTo": "<?= $block->escapeJs($block->getParentComponentName()) ?>", -+ "storagesConfiguration" : <?= /* @noEscape */ $block->getConfigurationJson() ?> - } - } - } -diff --git a/app/code/Magento/Catalog/view/frontend/templates/messages/addCompareSuccessMessage.phtml b/app/code/Magento/Catalog/view/frontend/templates/messages/addCompareSuccessMessage.phtml -index 5f44c42e17c..f5dca566abf 100644 ---- a/app/code/Magento/Catalog/view/frontend/templates/messages/addCompareSuccessMessage.phtml -+++ b/app/code/Magento/Catalog/view/frontend/templates/messages/addCompareSuccessMessage.phtml -@@ -3,12 +3,14 @@ - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ --// @codingStandardsIgnoreFile -+ - /** @var \Magento\Framework\View\Element\Template $block */ - ?> --<?= $block->escapeHtml(__( -- 'You added product %1 to the <a href="%2">comparison list</a>.', -- $block->getData('product_name'), -- $block->getData('compare_list_url')), -+<?= $block->escapeHtml( -+ __( -+ 'You added product %1 to the <a href="%2">comparison list</a>.', -+ $block->getData('product_name'), -+ $block->getData('compare_list_url') -+ ), - ['a'] - ); -diff --git a/app/code/Magento/Catalog/view/frontend/templates/navigation/left.phtml b/app/code/Magento/Catalog/view/frontend/templates/navigation/left.phtml -index 01820361744..6d5ddb95ab1 100644 ---- a/app/code/Magento/Catalog/view/frontend/templates/navigation/left.phtml -+++ b/app/code/Magento/Catalog/view/frontend/templates/navigation/left.phtml -@@ -4,8 +4,6 @@ - * See COPYING.txt for license details. - */ - --// @codingStandardsIgnoreFile -- - /** - * Category left navigation - * -@@ -15,25 +13,29 @@ - <?php if (!$block->getCategory()) { - return; - } ?> --<?php $_categories = $block->getCurrentChildCategories(); ?> -+<?php $_categories = $block->getCurrentChildCategories() ;?> - <?php $_count = is_array($_categories) ? count($_categories) : $_categories->count(); ?> --<?php if ($_count): ?> -+<?php if ($_count) :?> - <div class="block filter"> - <div class="title"> -- <strong><?= /* @escapeNotVerified */ __('Shop By') ?></strong> -+ <strong><?= $block->escapeHtml(__('Shop By')) ?></strong> - </div> - <div class="content"> -- <strong class="subtitle"><?= /* @escapeNotVerified */ __('Shopping Options') ?></strong> -+ <strong class="subtitle"><?= $block->escapeHtml(__('Shopping Options')) ?></strong> - <dl class="options" id="narrow-by-list2"> -- <dt><?= /* @escapeNotVerified */ __('Category') ?></dt> -+ <dt><?= $block->escapeHtml(__('Category')) ?></dt> - <dd> - <ol class="items"> - <?php /** @var \Magento\Catalog\Model\Category $_category */ ?> -- <?php foreach ($_categories as $_category): ?> -- <?php if ($_category->getIsActive()): ?> -+ <?php foreach ($_categories as $_category) :?> -+ <?php if ($_category->getIsActive()) :?> - <li class="item"> -- <a href="<?= /* @escapeNotVerified */ $block->getCategoryUrl($_category) ?>"<?php if ($block->isCategoryActive($_category)): ?> class="current"<?php endif; ?>><?= $block->escapeHtml($_category->getName()) ?></a> -- <span class="count"><?= /* @escapeNotVerified */ $_category->getProductCount() ?></span> -+ <a href="<?= $block->escapeUrl($block->getCategoryUrl($_category)) ?>" -+ <?php if ($block->isCategoryActive($_category)) :?> -+ class="current" -+ <?php endif; ?> -+ ><?= $block->escapeHtml($_category->getName()) ?></a> -+ <span class="count"><?= $block->escapeHtml($_category->getProductCount()) ?></span> - </li> - <?php endif; ?> - <?php endforeach ?> -diff --git a/app/code/Magento/Catalog/view/frontend/templates/product/compare/link.phtml b/app/code/Magento/Catalog/view/frontend/templates/product/compare/link.phtml -index b8595aae9d9..05a5649135e 100644 ---- a/app/code/Magento/Catalog/view/frontend/templates/product/compare/link.phtml -+++ b/app/code/Magento/Catalog/view/frontend/templates/product/compare/link.phtml -@@ -4,16 +4,16 @@ - * See COPYING.txt for license details. - */ - --// @codingStandardsIgnoreFile -+/** @var $block Magento\Framework\View\Element\Template */ - ?> - <li class="item link compare" data-bind="scope: 'compareProducts'" data-role="compare-products-link"> -- <a class="action compare no-display" title="<?= /* @escapeNotVerified */ __('Compare Products') ?>" -+ <a class="action compare no-display" title="<?= $block->escapeHtmlAttr(__('Compare Products')) ?>" - data-bind="attr: {'href': compareProducts().listUrl}, css: {'no-display': !compareProducts().count}" - > -- <?= /* @escapeNotVerified */ __('Compare Products') ?> -+ <?= $block->escapeHtml(__('Compare Products')) ?> - <span class="counter qty" data-bind="text: compareProducts().countCaption"></span> - </a> - </li> - <script type="text/x-magento-init"> --{"[data-role=compare-products-link]": {"Magento_Ui/js/core/app": <?= /* @escapeNotVerified */ $block->getJsLayout() ?>}} -+{"[data-role=compare-products-link]": {"Magento_Ui/js/core/app": <?= /* @noEscape */ $block->getJsLayout() ?>}} - </script> -diff --git a/app/code/Magento/Catalog/view/frontend/templates/product/compare/list.phtml b/app/code/Magento/Catalog/view/frontend/templates/product/compare/list.phtml -index 949d365e789..55772388d44 100644 ---- a/app/code/Magento/Catalog/view/frontend/templates/product/compare/list.phtml -+++ b/app/code/Magento/Catalog/view/frontend/templates/product/compare/list.phtml -@@ -4,14 +4,16 @@ - * See COPYING.txt for license details. - */ - --// @codingStandardsIgnoreFile -+// phpcs:disable Magento2.Templates.ThisInTemplate.FoundThis -+// phpcs:disable PSR2.ControlStructures.SwitchDeclaration -+// phpcs:disable Generic.WhiteSpace.ScopeIndent - - /* @var $block \Magento\Catalog\Block\Product\Compare\ListCompare */ - ?> - <?php $total = $block->getItems()->getSize() ?> --<?php if ($total): ?> -- <a href="#" class="action print hidden-print" title="<?= /* @escapeNotVerified */ __('Print This Page') ?>"> -- <span><?= /* @escapeNotVerified */ __('Print This Page') ?></span> -+<?php if ($total) :?> -+ <a href="#" class="action print hidden-print" title="<?= $block->escapeHtmlAttr(__('Print This Page')) ?>"> -+ <span><?= $block->escapeHtml(__('Print This Page')) ?></span> - </a> - <div class="table-wrapper comparison"> - <table class="data table table-comparison" id="product-comparison" -@@ -21,19 +23,19 @@ - "selectors":{ - "productAddToCartSelector":"button.action.tocart"} - }}'> -- <caption class="table-caption"><?= /* @escapeNotVerified */ __('Compare Products') ?></caption> -+ <caption class="table-caption"><?= $block->escapeHtml(__('Compare Products')) ?></caption> - <thead> - <tr> - <?php $index = 0 ?> -- <?php foreach ($block->getItems() as $item): ?> -- <?php if ($index++ == 0): ?> -- <th scope="row" class="cell label remove"><span><?= /* @escapeNotVerified */ __('Remove Product') ?></span></th> -+ <?php foreach ($block->getItems() as $item) :?> -+ <?php if ($index++ == 0) :?> -+ <th scope="row" class="cell label remove"><span><?= $block->escapeHtml(__('Remove Product')) ?></span></th> - <?php endif; ?> - <td class="cell remove product hidden-print"> -- <?php $compareHelper = $this->helper('Magento\Catalog\Helper\Product\Compare');?> -- <a href="#" data-post='<?= /* @escapeNotVerified */ $compareHelper->getPostDataRemove($item) ?>' -- class="action delete" title="<?= /* @escapeNotVerified */ __('Remove Product') ?>"> -- <span><?= /* @escapeNotVerified */ __('Remove Product') ?></span> -+ <?php $compareHelper = $this->helper(Magento\Catalog\Helper\Product\Compare::class);?> -+ <a href="#" data-post='<?= /* @noEscape */ $compareHelper->getPostDataRemove($item) ?>' -+ class="action delete" title="<?= $block->escapeHtmlAttr(__('Remove Product')) ?>"> -+ <span><?= $block->escapeHtml(__('Remove Product')) ?></span> - </a> - </td> - <?php endforeach; ?> -@@ -42,44 +44,54 @@ - <tbody> - <tr> - <?php $index = 0; ?> -- <?php $helper = $this->helper('Magento\Catalog\Helper\Output'); ?> -+ <?php $helper = $this->helper(Magento\Catalog\Helper\Output::class); ?> - <?php /** @var $item \Magento\Catalog\Model\Product */ ?> -- <?php foreach ($block->getItems() as $item): ?> -- <?php if ($index++ == 0): ?> -- <th scope="row" class="cell label product"><span><?= /* @escapeNotVerified */ __('Product') ?></span></th> -+ <?php foreach ($block->getItems() as $item) :?> -+ <?php if ($index++ == 0) :?> -+ <th scope="row" class="cell label product"> -+ <span><?= $block->escapeHtml(__('Product')) ?></span> -+ </th> - <?php endif; ?> -- <td data-th="<?= $block->escapeHtml(__('Product')) ?>" class="cell product info"> -- <a class="product-item-photo" href="<?= /* @escapeNotVerified */ $block->getProductUrl($item) ?>" title="<?= /* @escapeNotVerified */ $block->stripTags($item->getName(), null, true) ?>"> -+ <td data-th="<?= $block->escapeHtmlAttr(__('Product')) ?>" class="cell product info"> -+ <a class="product-item-photo" -+ href="<?= $block->escapeUrl($block->getProductUrl($item)) ?>" -+ title="<?= /* @noEscape */ $block->stripTags($item->getName(), null, true) ?>"> - <?= $block->getImage($item, 'product_comparison_list')->toHtml() ?> - </a> - <strong class="product-item-name"> -- <a href="<?= /* @escapeNotVerified */ $block->getProductUrl($item) ?>" title="<?= /* @escapeNotVerified */ $block->stripTags($item->getName(), null, true) ?>"> -- <?= /* @escapeNotVerified */ $helper->productAttribute($item, $item->getName(), 'name') ?> -+ <a href="<?= $block->escapeUrl($block->getProductUrl($item)) ?>" -+ title="<?= /* @noEscape */ $block->stripTags($item->getName(), null, true) ?>"> -+ <?= /* @noEscape */ $helper->productAttribute($item, $item->getName(), 'name') ?> - </a> - </strong> - <?= $block->getReviewsSummaryHtml($item, 'short') ?> -- <?= /* @escapeNotVerified */ $block->getProductPrice($item, '-compare-list-top') ?> -+ <?= /* @noEscape */ $block->getProductPrice($item, '-compare-list-top') ?> - <div class="product-item-actions hidden-print"> - <div class="actions-primary"> -- <?php if ($item->isSaleable()): ?> -- <form data-role="tocart-form" action="<?= /* @escapeNotVerified */ $this->helper('Magento\Catalog\Helper\Product\Compare')->getAddToCartUrl($item) ?>" method="post"> -+ <?php if ($item->isSaleable()) :?> -+ <form data-role="tocart-form" -+ action="<?= $block->escapeUrl($this->helper(Magento\Catalog\Helper\Product\Compare::class)->getAddToCartUrl($item)) ?>" -+ method="post"> - <?= $block->getBlockHtml('formkey') ?> - <button type="submit" class="action tocart primary"> -- <span><?= /* @escapeNotVerified */ __('Add to Cart') ?></span> -+ <span><?= $block->escapeHtml(__('Add to Cart')) ?></span> - </button> - </form> -- <?php else: ?> -- <?php if ($item->getIsSalable()): ?> -- <div class="stock available"><span><?= /* @escapeNotVerified */ __('In stock') ?></span></div> -- <?php else: ?> -- <div class="stock unavailable"><span><?= /* @escapeNotVerified */ __('Out of stock') ?></span></div> -+ <?php else :?> -+ <?php if ($item->getIsSalable()) :?> -+ <div class="stock available"><span><?= $block->escapeHtml(__('In stock')) ?></span></div> -+ <?php else :?> -+ <div class="stock unavailable"><span><?= $block->escapeHtml(__('Out of stock')) ?></span></div> - <?php endif; ?> - <?php endif; ?> - </div> -- <?php if ($this->helper('Magento\Wishlist\Helper\Data')->isAllow()) : ?> -+ <?php if ($this->helper(Magento\Wishlist\Helper\Data::class)->isAllow()) :?> - <div class="secondary-addto-links actions-secondary" data-role="add-to-links"> -- <a href="#" data-post='<?= /* @escapeNotVerified */ $block->getAddToWishlistParams($item) ?>' class="action towishlist" data-action="add-to-wishlist"> -- <span><?= /* @escapeNotVerified */ __('Add to Wish List') ?></span> -+ <a href="#" -+ data-post='<?= /* @noEscape */ $block->getAddToWishlistParams($item) ?>' -+ class="action towishlist" -+ data-action="add-to-wishlist"> -+ <span><?= $block->escapeHtml(__('Add to Wish List')) ?></span> - </a> - </div> - <?php endif; ?> -@@ -89,12 +101,12 @@ - </tr> - </tbody> - <tbody> -- <?php foreach ($block->getAttributes() as $attribute): ?> -+ <?php foreach ($block->getAttributes() as $attribute) :?> - <?php $index = 0; ?> -- <?php if ($block->hasAttributeValueForProducts($attribute)): ?> -+ <?php if ($block->hasAttributeValueForProducts($attribute)) :?> - <tr> -- <?php foreach ($block->getItems() as $item): ?> -- <?php if ($index++ == 0): ?> -+ <?php foreach ($block->getItems() as $item) :?> -+ <?php if ($index++ == 0) :?> - <th scope="row" class="cell label"> - <span class="attribute label"> - <?= $block->escapeHtml($attribute->getStoreLabel() ? $attribute->getStoreLabel() : __($attribute->getFrontendLabel())) ?> -@@ -105,19 +117,21 @@ - <div class="attribute value"> - <?php switch ($attribute->getAttributeCode()) { - case "price": ?> -- <?php -- /* @escapeNotVerified */ echo $block->getProductPrice( -- $item, -- '-compare-list-' . $attribute->getCode() -- ) -+ <?= -+ /* @noEscape */ $block->getProductPrice( -+ $item, -+ '-compare-list-' . $attribute->getCode() -+ ) - ?> - <?php break; - case "small_image": ?> - <?php $block->getImage($item, 'product_small_image')->toHtml(); ?> - <?php break; -- default: ?> -- <?= /* @escapeNotVerified */ $helper->productAttribute($item, $block->getProductAttributeValue($item, $attribute), $attribute->getAttributeCode()) ?> -- <?php break; -+ default :?> -+ <?php if (is_string($block->getProductAttributeValue($item, $attribute))) :?> -+ <?= /* @noEscape */ $helper->productAttribute($item, $block->getProductAttributeValue($item, $attribute), $attribute->getAttributeCode()) ?> -+ <?php endif; ?> -+ <?php break; - } ?> - </div> - </td> -@@ -128,7 +142,7 @@ - </tbody> - </table> - </div> -- <?php if (!$block->isRedirectToCartEnabled()) : ?> -+ <?php if (!$block->isRedirectToCartEnabled()) :?> - <script type="text/x-magento-init"> - { - "[data-role=tocart-form]": { -@@ -137,6 +151,6 @@ - } - </script> - <?php endif; ?> --<?php else: ?> -- <div class="message info empty"><div><?= /* @escapeNotVerified */ __('You have no items to compare.') ?></div></div> -+<?php else :?> -+ <div class="message info empty"><div><?= $block->escapeHtml(__('You have no items to compare.')) ?></div></div> - <?php endif; ?> -diff --git a/app/code/Magento/Catalog/view/frontend/templates/product/compare/sidebar.phtml b/app/code/Magento/Catalog/view/frontend/templates/product/compare/sidebar.phtml -index 8daa3424544..809ddc5c617 100644 ---- a/app/code/Magento/Catalog/view/frontend/templates/product/compare/sidebar.phtml -+++ b/app/code/Magento/Catalog/view/frontend/templates/product/compare/sidebar.phtml -@@ -4,12 +4,13 @@ - * See COPYING.txt for license details. - */ - --// @codingStandardsIgnoreFile -+// phpcs:disable Magento2.Templates.ThisInTemplate.FoundThis -+ - /* @var $block \Magento\Framework\View\Element\Template */ - ?> - <div class="block block-compare" data-bind="scope: 'compareProducts'" data-role="compare-products-sidebar"> - <div class="block-title"> -- <strong id="block-compare-heading" role="heading" aria-level="2"><?= /* @escapeNotVerified */ __('Compare Products') ?></strong> -+ <strong id="block-compare-heading" role="heading" aria-level="2"><?= $block->escapeHtml(__('Compare Products')) ?></strong> - <span class="counter qty no-display" data-bind="text: compareProducts().countCaption, css: {'no-display': !compareProducts().count}"></span> - </div> - <!-- ko if: compareProducts().count --> -@@ -20,29 +21,32 @@ - <strong class="product-item-name"> - <a data-bind="attr: {href: product_url}, html: name" class="product-item-link"></a> - </strong> -- <a href="#" data-bind="attr: {'data-post': remove_url}" title="<?= /* @escapeNotVerified */ __('Remove This Item') ?>" class="action delete"> -- <span><?= /* @escapeNotVerified */ __('Remove This Item') ?></span> -+ <a href="#" -+ data-bind="attr: {'data-post': remove_url}" -+ title="<?= $block->escapeHtmlAttr(__('Remove This Item')) ?>" -+ class="action delete"> -+ <span><?= $block->escapeHtml(__('Remove This Item')) ?></span> - </a> - </li> - </ol> - <div class="actions-toolbar"> - <div class="primary"> -- <a data-bind="attr: {'href': compareProducts().listUrl}" class="action compare primary"><span><?= /* @escapeNotVerified */ __('Compare') ?></span></a> -+ <a data-bind="attr: {'href': compareProducts().listUrl}" class="action compare primary"><span><?= $block->escapeHtml(__('Compare')) ?></span></a> - </div> - <div class="secondary"> - <a id="compare-clear-all" href="#" class="action clear" data-post="<?=$block->escapeHtml( -- $this->helper('Magento\Catalog\Helper\Product\Compare')->getPostDataClearList() -+ $this->helper(Magento\Catalog\Helper\Product\Compare::class)->getPostDataClearList() - ) ?>"> -- <span><?= /* @escapeNotVerified */ __('Clear All') ?></span> -+ <span><?= $block->escapeHtml(__('Clear All')) ?></span> - </a> - </div> - </div> - </div> - <!-- /ko --> - <!-- ko ifnot: compareProducts().count --> -- <div class="empty"><?= /* @escapeNotVerified */ __('You have no items to compare.') ?></div> -+ <div class="empty"><?= $block->escapeHtml(__('You have no items to compare.')) ?></div> - <!-- /ko --> - </div> - <script type="text/x-magento-init"> --{"[data-role=compare-products-sidebar]": {"Magento_Ui/js/core/app": <?= /* @escapeNotVerified */ $block->getJsLayout() ?>}} -+{"[data-role=compare-products-sidebar]": {"Magento_Ui/js/core/app": <?= /* @noEscape */ $block->getJsLayout() ?>}} - </script> -diff --git a/app/code/Magento/Catalog/view/frontend/templates/product/gallery.phtml b/app/code/Magento/Catalog/view/frontend/templates/product/gallery.phtml -index c7abb0525b3..e9551793c86 100644 ---- a/app/code/Magento/Catalog/view/frontend/templates/product/gallery.phtml -+++ b/app/code/Magento/Catalog/view/frontend/templates/product/gallery.phtml -@@ -4,39 +4,45 @@ - * See COPYING.txt for license details. - */ - --// @codingStandardsIgnoreFile -- - /** @var \Magento\Catalog\Block\Product\Gallery $block */ - ?> - <?php $_width = $block->getImageWidth(); ?> --<div class="product-image-popup" style="width:<?= /* @escapeNotVerified */ $_width ?>px;"> -- <div class="buttons-set"><a href="#" class="button" role="close-window"><span><?= /* @escapeNotVerified */ __('Close Window') ?></span></a></div> -- <?php if ($block->getPreviousImageUrl() || $block->getNextImageUrl()): ?> -+<div class="product-image-popup" style="width:<?= /* @noEscape */ $_width ?>px;"> -+ <div class="buttons-set"><a href="#" class="button" role="close-window"><span><?= $block->escapeHtml(__('Close Window')) ?></span></a></div> -+ <?php if ($block->getPreviousImageUrl() || $block->getNextImageUrl()) :?> - <div class="nav"> -- <?php if ($_prevUrl = $block->getPreviousImageUrl()): ?> -- <a href="<?= /* @escapeNotVerified */ $_prevUrl ?>" class="prev">« <?= /* @escapeNotVerified */ __('Prev') ?></a> -- <?php endif; ?> -- <?php if ($_nextUrl = $block->getNextImageUrl()): ?> -- <a href="<?= /* @escapeNotVerified */ $_nextUrl ?>" class="next"><?= /* @escapeNotVerified */ __('Next') ?> »</a> -- <?php endif; ?> -+ <?php if ($_prevUrl = $block->getPreviousImageUrl()) :?> -+ <a href="<?= $block->escapeUrl($_prevUrl) ?>" class="prev">« <?= $block->escapeHtml(__('Prev')) ?></a> -+ <?php endif; ?> -+ <?php if ($_nextUrl = $block->getNextImageUrl()) :?> -+ <a href="<?= $block->escapeUrl($_nextUrl) ?>" class="next"><?= $block->escapeHtml(__('Next')) ?> »</a> -+ <?php endif; ?> - </div> - <?php endif; ?> -- <?php if ($_imageTitle = $block->escapeHtml($block->getCurrentImage()->getLabel())): ?> -- <h1 class="image-label"><?= /* @escapeNotVerified */ $_imageTitle ?></h1> -+ <?php if ($_imageTitle = $block->escapeHtml($block->getCurrentImage()->getLabel())) :?> -+ <h1 class="image-label"><?= /* @noEscape */ $_imageTitle ?></h1> - <?php endif; ?> - <?php -- $imageUrl = $block->getImageUrl(); -+ $imageUrl = $block->getImageUrl(); - ?> -- <img src="<?= /* @escapeNotVerified */ $imageUrl ?>"<?php if ($_width): ?> width="<?= /* @escapeNotVerified */ $_width ?>"<?php endif; ?> alt="<?= $block->escapeHtml($block->getCurrentImage()->getLabel()) ?>" title="<?= $block->escapeHtml($block->getCurrentImage()->getLabel()) ?>" id="product-gallery-image" class="image" data-mage-init='{"catalogGallery":{}}'/> -- <div class="buttons-set"><a href="#" class="button" role="close-window"><span><?= /* @escapeNotVerified */ __('Close Window') ?></span></a></div> -- <?php if ($block->getPreviousImageUrl() || $block->getNextImageUrl()): ?> -+ <img src="<?= $block->escapeUrl($imageUrl) ?>" -+ <?php if ($_width) :?> -+ width="<?= /* @noEscape */ $_width ?>" -+ <?php endif; ?> -+ alt="<?= $block->escapeHtmlAttr($block->getCurrentImage()->getLabel()) ?>" -+ title="<?= $block->escapeHtmlAttr($block->getCurrentImage()->getLabel()) ?>" -+ id="product-gallery-image" -+ class="image" -+ data-mage-init='{"catalogGallery":{}}'/> -+ <div class="buttons-set"><a href="#" class="button" role="close-window"><span><?= /* @noEscape */ __('Close Window') ?></span></a></div> -+ <?php if ($block->getPreviousImageUrl() || $block->getNextImageUrl()) :?> - <div class="nav"> -- <?php if ($_prevUrl = $block->getPreviousImageUrl()): ?> -- <a href="<?= /* @escapeNotVerified */ $_prevUrl ?>" class="prev">« <?= /* @escapeNotVerified */ __('Prev') ?></a> -- <?php endif; ?> -- <?php if ($_nextUrl = $block->getNextImageUrl()): ?> -- <a href="<?= /* @escapeNotVerified */ $_nextUrl ?>" class="next"><?= /* @escapeNotVerified */ __('Next') ?> »</a> -- <?php endif; ?> -+ <?php if ($_prevUrl = $block->getPreviousImageUrl()) :?> -+ <a href="<?= $block->escapeUrl($_prevUrl) ?>" class="prev">« <?= $block->escapeHtml(__('Prev')) ?></a> -+ <?php endif; ?> -+ <?php if ($_nextUrl = $block->getNextImageUrl()) :?> -+ <a href="<?= $block->escapeUrl($_nextUrl) ?>" class="next"><?= $block->escapeHtml(__('Next')) ?> »</a> -+ <?php endif; ?> - </div> - <?php endif; ?> - </div> -diff --git a/app/code/Magento/Catalog/view/frontend/templates/product/image.phtml b/app/code/Magento/Catalog/view/frontend/templates/product/image.phtml -index 94b829eb921..5a1b102ff63 100644 ---- a/app/code/Magento/Catalog/view/frontend/templates/product/image.phtml -+++ b/app/code/Magento/Catalog/view/frontend/templates/product/image.phtml -@@ -7,8 +7,8 @@ - <?php /** @var $block \Magento\Catalog\Block\Product\Image */ ?> - - <img class="photo image" -- <?= /* @escapeNotVerified */ $block->getCustomAttributes() ?> -- src="<?= /* @escapeNotVerified */ $block->getImageUrl() ?>" -- width="<?= /* @escapeNotVerified */ $block->getWidth() ?>" -- height="<?= /* @escapeNotVerified */ $block->getHeight() ?>" -- alt="<?= /* @escapeNotVerified */ $block->stripTags($block->getLabel(), null, true) ?>" /> -+ <?= $block->escapeHtml($block->getCustomAttributes()) ?> -+ src="<?= $block->escapeUrl($block->getImageUrl()) ?>" -+ width="<?= $block->escapeHtmlAttr($block->getWidth()) ?>" -+ height="<?= $block->escapeHtmlAttr($block->getHeight()) ?>" -+ alt="<?= /* @noEscape */ $block->stripTags($block->getLabel(), null, true) ?>" /> -diff --git a/app/code/Magento/Catalog/view/frontend/templates/product/image_with_borders.phtml b/app/code/Magento/Catalog/view/frontend/templates/product/image_with_borders.phtml -index 74a0b2d7cf1..33f7620f1a1 100644 ---- a/app/code/Magento/Catalog/view/frontend/templates/product/image_with_borders.phtml -+++ b/app/code/Magento/Catalog/view/frontend/templates/product/image_with_borders.phtml -@@ -7,13 +7,13 @@ - <?php /** @var $block \Magento\Catalog\Block\Product\Image */ ?> - - <span class="product-image-container" -- style="width:<?= /* @escapeNotVerified */ $block->getWidth() ?>px;"> -+ style="width:<?= $block->escapeHtmlAttr($block->getWidth()) ?>px;"> - <span class="product-image-wrapper" -- style="padding-bottom: <?= /* @escapeNotVerified */ ($block->getRatio() * 100) ?>%;"> -- <img class="product-image-photo" -- <?= /* @escapeNotVerified */ $block->getCustomAttributes() ?> -- src="<?= /* @escapeNotVerified */ $block->getImageUrl() ?>" -- max-width="<?= /* @escapeNotVerified */ $block->getWidth() ?>" -- max-height="<?= /* @escapeNotVerified */ $block->getHeight() ?>" -- alt="<?= /* @escapeNotVerified */ $block->stripTags($block->getLabel(), null, true) ?>"/></span> -+ style="padding-bottom: <?= ($block->getRatio() * 100) ?>%;"> -+ <img class="<?= $block->escapeHtmlAttr($block->getClass()) ?>" -+ <?= $block->escapeHtmlAttr($block->getCustomAttributes()) ?> -+ src="<?= $block->escapeUrl($block->getImageUrl()) ?>" -+ max-width="<?= $block->escapeHtmlAttr($block->getWidth()) ?>" -+ max-height="<?= $block->escapeHtmlAttr($block->getHeight()) ?>" -+ alt="<?= /* @noEscape */ $block->stripTags($block->getLabel(), null, true) ?>"/></span> - </span> -diff --git a/app/code/Magento/Catalog/view/frontend/templates/product/list.phtml b/app/code/Magento/Catalog/view/frontend/templates/product/list.phtml -index f7799b30436..ce44884a575 100644 ---- a/app/code/Magento/Catalog/view/frontend/templates/product/list.phtml -+++ b/app/code/Magento/Catalog/view/frontend/templates/product/list.phtml -@@ -5,10 +5,10 @@ - */ - use Magento\Framework\App\Action\Action; - --// @codingStandardsIgnoreFile -- - ?> - <?php -+// phpcs:disable Magento2.Templates.ThisInTemplate.FoundThis -+ - /** - * Product list template - * -@@ -17,11 +17,11 @@ use Magento\Framework\App\Action\Action; - ?> - <?php - $_productCollection = $block->getLoadedProductCollection(); --$_helper = $this->helper('Magento\Catalog\Helper\Output'); -+$_helper = $this->helper(Magento\Catalog\Helper\Output::class); - ?> --<?php if (!$_productCollection->count()): ?> -- <div class="message info empty"><div><?= /* @escapeNotVerified */ __('We can\'t find products matching the selection.') ?></div></div> --<?php else: ?> -+<?php if (!$_productCollection->count()) :?> -+ <div class="message info empty"><div><?= $block->escapeHtml(__('We can\'t find products matching the selection.')) ?></div></div> -+<?php else :?> - <?= $block->getToolbarHtml() ?> - <?= $block->getAdditionalHtml() ?> - <?php -@@ -41,12 +41,12 @@ $_helper = $this->helper('Magento\Catalog\Helper\Output'); - */ - $pos = $block->getPositioned(); - ?> -- <div class="products wrapper <?= /* @escapeNotVerified */ $viewMode ?> products-<?= /* @escapeNotVerified */ $viewMode ?>"> -+ <div class="products wrapper <?= /* @noEscape */ $viewMode ?> products-<?= /* @noEscape */ $viewMode ?>"> - <ol class="products list items product-items"> - <?php /** @var $_product \Magento\Catalog\Model\Product */ ?> -- <?php foreach ($_productCollection as $_product): ?> -+ <?php foreach ($_productCollection as $_product) :?> - <li class="item product product-item"> -- <div class="product-item-info" data-container="product-grid"> -+ <div class="product-item-info" data-container="product-<?= /* @noEscape */ $viewMode ?>"> - <?php - $productImage = $block->getImage($_product, $imageDisplayArea); - if ($pos != null) { -@@ -55,7 +55,9 @@ $_helper = $this->helper('Magento\Catalog\Helper\Output'); - } - ?> - <?php // Product Image ?> -- <a href="<?= /* @escapeNotVerified */ $_product->getProductUrl() ?>" class="product photo product-item-photo" tabindex="-1"> -+ <a href="<?= $block->escapeUrl($_product->getProductUrl()) ?>" -+ class="product photo product-item-photo" -+ tabindex="-1"> - <?= $productImage->toHtml() ?> - </a> - <div class="product details product-item-details"> -@@ -64,48 +66,55 @@ $_helper = $this->helper('Magento\Catalog\Helper\Output'); - ?> - <strong class="product name product-item-name"> - <a class="product-item-link" -- href="<?= /* @escapeNotVerified */ $_product->getProductUrl() ?>"> -- <?= /* @escapeNotVerified */ $_helper->productAttribute($_product, $_product->getName(), 'name') ?> -+ href="<?= $block->escapeUrl($_product->getProductUrl()) ?>"> -+ <?= /* @noEscape */ $_helper->productAttribute($_product, $_product->getName(), 'name') ?> - </a> - </strong> - <?= $block->getReviewsSummaryHtml($_product, $templateType) ?> -- <?= /* @escapeNotVerified */ $block->getProductPrice($_product) ?> -+ <?= /* @noEscape */ $block->getProductPrice($_product) ?> - <?= $block->getProductDetailsHtml($_product) ?> - - <div class="product-item-inner"> -- <div class="product actions product-item-actions"<?= strpos($pos, $viewMode . '-actions') ? $position : '' ?>> -- <div class="actions-primary"<?= strpos($pos, $viewMode . '-primary') ? $position : '' ?>> -- <?php if ($_product->isSaleable()): ?> -+ <div class="product actions product-item-actions"<?= strpos($pos, $viewMode . '-actions') ? $block->escapeHtmlAttr($position) : '' ?>> -+ <div class="actions-primary"<?= strpos($pos, $viewMode . '-primary') ? $block->escapeHtmlAttr($position) : '' ?>> -+ <?php if ($_product->isSaleable()) :?> - <?php $postParams = $block->getAddToCartPostParams($_product); ?> -- <form data-role="tocart-form" data-product-sku="<?= $block->escapeHtml($_product->getSku()) ?>" action="<?= /* @NoEscape */ $postParams['action'] ?>" method="post"> -- <input type="hidden" name="product" value="<?= /* @escapeNotVerified */ $postParams['data']['product'] ?>"> -- <input type="hidden" name="<?= /* @escapeNotVerified */ Action::PARAM_NAME_URL_ENCODED ?>" value="<?= /* @escapeNotVerified */ $postParams['data'][Action::PARAM_NAME_URL_ENCODED] ?>"> -+ <form data-role="tocart-form" -+ data-product-sku="<?= $block->escapeHtml($_product->getSku()) ?>" -+ action="<?= $block->escapeUrl($postParams['action']) ?>" -+ method="post"> -+ <input type="hidden" -+ name="product" -+ value="<?= /* @noEscape */ $postParams['data']['product'] ?>"> -+ <input type="hidden" name="<?= /* @noEscape */ Action::PARAM_NAME_URL_ENCODED ?>" -+ value="<?= /* @noEscape */ $postParams['data'][Action::PARAM_NAME_URL_ENCODED] ?>"> - <?= $block->getBlockHtml('formkey') ?> - <button type="submit" -- title="<?= $block->escapeHtml(__('Add to Cart')) ?>" -+ title="<?= $block->escapeHtmlAttr(__('Add to Cart')) ?>" - class="action tocart primary"> -- <span><?= /* @escapeNotVerified */ __('Add to Cart') ?></span> -+ <span><?= $block->escapeHtml(__('Add to Cart')) ?></span> - </button> - </form> -- <?php else: ?> -- <?php if ($_product->isAvailable()): ?> -- <div class="stock available"><span><?= /* @escapeNotVerified */ __('In stock') ?></span></div> -- <?php else: ?> -- <div class="stock unavailable"><span><?= /* @escapeNotVerified */ __('Out of stock') ?></span></div> -+ <?php else :?> -+ <?php if ($_product->isAvailable()) :?> -+ <div class="stock available"><span><?= $block->escapeHtml(__('In stock')) ?></span></div> -+ <?php else :?> -+ <div class="stock unavailable"><span><?= $block->escapeHtml(__('Out of stock')) ?></span></div> - <?php endif; ?> - <?php endif; ?> - </div> -- <div data-role="add-to-links" class="actions-secondary"<?= strpos($pos, $viewMode . '-secondary') ? $position : '' ?>> -- <?php if ($addToBlock = $block->getChildBlock('addto')): ?> -+ <div data-role="add-to-links" class="actions-secondary"<?= strpos($pos, $viewMode . '-secondary') ? $block->escapeHtmlAttr($position) : '' ?>> -+ <?php if ($addToBlock = $block->getChildBlock('addto')) :?> - <?= $addToBlock->setProduct($_product)->getChildHtml() ?> - <?php endif; ?> - </div> - </div> -- <?php if ($showDescription):?> -+ <?php if ($showDescription) :?> - <div class="product description product-item-description"> -- <?= /* @escapeNotVerified */ $_helper->productAttribute($_product, $_product->getShortDescription(), 'short_description') ?> -- <a href="<?= /* @escapeNotVerified */ $_product->getProductUrl() ?>" title="<?= /* @escapeNotVerified */ $_productNameStripped ?>" -- class="action more"><?= /* @escapeNotVerified */ __('Learn More') ?></a> -+ <?= /* @noEscape */ $_helper->productAttribute($_product, $_product->getShortDescription(), 'short_description') ?> -+ <a href="<?= $block->escapeUrl($_product->getProductUrl()) ?>" -+ title="<?= /* @noEscape */ $_productNameStripped ?>" -+ class="action more"><?= $block->escapeHtml(__('Learn More')) ?></a> - </div> - <?php endif; ?> - </div> -@@ -116,12 +125,12 @@ $_helper = $this->helper('Magento\Catalog\Helper\Output'); - </ol> - </div> - <?= $block->getToolbarHtml() ?> -- <?php if (!$block->isRedirectToCartEnabled()) : ?> -+ <?php if (!$block->isRedirectToCartEnabled()) :?> - <script type="text/x-magento-init"> - { - "[data-role=tocart-form], .form.map.checkout": { - "catalogAddToCart": { -- "product_sku": "<?= /* @NoEscape */ $_product->getSku() ?>" -+ "product_sku": "<?= $block->escapeJs($_product->getSku()) ?>" - } - } - } -diff --git a/app/code/Magento/Catalog/view/frontend/templates/product/list/addto/compare.phtml b/app/code/Magento/Catalog/view/frontend/templates/product/list/addto/compare.phtml -index 8798170e8c0..c23ee021ca3 100644 ---- a/app/code/Magento/Catalog/view/frontend/templates/product/list/addto/compare.phtml -+++ b/app/code/Magento/Catalog/view/frontend/templates/product/list/addto/compare.phtml -@@ -4,14 +4,13 @@ - * See COPYING.txt for license details. - */ - --// @codingStandardsIgnoreFile - /** @var $block Magento\Catalog\Block\Product\ProductList\Item\AddTo\Compare */ - ?> - <a href="#" - class="action tocompare" - title="<?= $block->escapeHtml(__('Add to Compare')) ?>" - aria-label="<?= $block->escapeHtml(__('Add to Compare')) ?>" -- data-post='<?= /* @escapeNotVerified */ $block->getCompareHelper()->getPostDataParams($block->getProduct()) ?>' -+ data-post='<?= /* @noEscape */ $block->getCompareHelper()->getPostDataParams($block->getProduct()) ?>' - role="button"> -- <span><?= /* @escapeNotVerified */ __('Add to Compare') ?></span> -+ <span><?= $block->escapeHtml(__('Add to Compare')) ?></span> - </a> -diff --git a/app/code/Magento/Catalog/view/frontend/templates/product/list/items.phtml b/app/code/Magento/Catalog/view/frontend/templates/product/list/items.phtml -index f4344023460..91e261900ae 100644 ---- a/app/code/Magento/Catalog/view/frontend/templates/product/list/items.phtml -+++ b/app/code/Magento/Catalog/view/frontend/templates/product/list/items.phtml -@@ -4,7 +4,8 @@ - * See COPYING.txt for license details. - */ - --// @codingStandardsIgnoreFile -+// phpcs:disable Magento2.Templates.ThisInTemplate.FoundThis -+// phpcs:disable Generic.WhiteSpace.ScopeIndent.Incorrect - - /* @var $block \Magento\Catalog\Block\Product\AbstractProduct */ - ?> -@@ -29,7 +30,7 @@ switch ($type = $block->getType()) { - $templateType = null; - $description = false; - } -- break; -+ break; - - case 'related': - /** @var \Magento\Catalog\Block\Product\ProductList\Related $block */ -@@ -49,7 +50,7 @@ switch ($type = $block->getType()) { - $templateType = null; - $description = false; - } -- break; -+ break; - - case 'upsell-rule': - if ($exist = $block->hasItems()) { -@@ -68,7 +69,7 @@ switch ($type = $block->getType()) { - $description = false; - $canItemsAddToCart = false; - } -- break; -+ break; - - case 'upsell': - /** @var \Magento\Catalog\Block\Product\ProductList\Upsell $block */ -@@ -88,7 +89,7 @@ switch ($type = $block->getType()) { - $description = false; - $canItemsAddToCart = false; - } -- break; -+ break; - - case 'crosssell-rule': - /** @var \Magento\Catalog\Block\Product\ProductList\Crosssell $block */ -@@ -106,7 +107,7 @@ switch ($type = $block->getType()) { - $description = false; - $canItemsAddToCart = false; - } -- break; -+ break; - - case 'crosssell': - /** @var \Magento\Catalog\Block\Product\ProductList\Crosssell $block */ -@@ -124,7 +125,7 @@ switch ($type = $block->getType()) { - $description = false; - $canItemsAddToCart = false; - } -- break; -+ break; - - case 'new': - if ($exist = $block->getProductCollection()) { -@@ -144,117 +145,117 @@ switch ($type = $block->getType()) { - $description = ($mode == 'list') ? true : false; - $canItemsAddToCart = false; - } -- break; -+ break; - - default: - $exist = null; - } - ?> - --<?php if ($exist):?> -+<?php if ($exist) :?> - -- <?php if ($type == 'related' || $type == 'upsell'): ?> -- <?php if ($type == 'related'): ?> -- <div class="block <?= /* @escapeNotVerified */ $class ?>" data-mage-init='{"relatedProducts":{"relatedCheckbox":".related.checkbox"}}' data-limit="<?= /* @escapeNotVerified */ $limit ?>" data-shuffle="<?= /* @escapeNotVerified */ $shuffle ?>"> -- <?php else: ?> -- <div class="block <?= /* @escapeNotVerified */ $class ?>" data-mage-init='{"upsellProducts":{}}' data-limit="<?= /* @escapeNotVerified */ $limit ?>" data-shuffle="<?= /* @escapeNotVerified */ $shuffle ?>"> -+<?php if ($type == 'related' || $type == 'upsell') :?> -+<?php if ($type == 'related') :?> -+<div class="block <?= $block->escapeHtmlAttr($class) ?>" data-mage-init='{"relatedProducts":{"relatedCheckbox":".related.checkbox"}}' data-limit="<?= $block->escapeHtmlAttr($limit) ?>" data-shuffle="<?= /* @noEscape */ $shuffle ?>"> -+ <?php else :?> -+ <div class="block <?= $block->escapeHtmlAttr($class) ?>" data-mage-init='{"upsellProducts":{}}' data-limit="<?= $block->escapeHtmlAttr($limit) ?>" data-shuffle="<?= /* @noEscape */ $shuffle ?>"> - <?php endif; ?> -- <?php else: ?> -- <div class="block <?= /* @escapeNotVerified */ $class ?>"> -- <?php endif; ?> -- <div class="block-title title"> -- <strong id="block-<?= /* @escapeNotVerified */ $class ?>-heading" role="heading" aria-level="2"><?= /* @escapeNotVerified */ $title ?></strong> -- </div> -- <div class="block-content content" aria-labelledby="block-<?= /* @escapeNotVerified */ $class ?>-heading"> -- <?php if ($type == 'related' && $canItemsAddToCart): ?> -- <div class="block-actions"> -- <?= /* @escapeNotVerified */ __('Check items to add to the cart or') ?> -- <button type="button" class="action select" role="select-all"><span><?= /* @escapeNotVerified */ __('select all') ?></span></button> -- </div> -- <?php endif; ?> -- <div class="products wrapper grid products-grid products-<?= /* @escapeNotVerified */ $type ?>"> -- <ol class="products list items product-items"> -- <?php foreach ($items as $_item): ?> -- <?php $available = ''; ?> -- <?php if (!$_item->isComposite() && $_item->isSaleable() && $type == 'related'): ?> -- <?php if (!$_item->getRequiredOptions()): ?> -- <?php $available = 'related-available'; ?> -- <?php endif; ?> -- <?php endif; ?> -- <?php if ($type == 'related' || $type == 'upsell'): ?> -- <li class="item product product-item" style="display: none;"> -- <?php else: ?> -- <li class="item product product-item"> -+ <?php else :?> -+ <div class="block <?= $block->escapeHtmlAttr($class) ?>"> -+ <?php endif; ?> -+ <div class="block-title title"> -+ <strong id="block-<?= $block->escapeHtmlAttr($class) ?>-heading" role="heading" aria-level="2"><?= $block->escapeHtml($title) ?></strong> -+ </div> -+ <div class="block-content content" aria-labelledby="block-<?= $block->escapeHtmlAttr($class) ?>-heading"> -+ <?php if ($type == 'related' && $canItemsAddToCart) :?> -+ <div class="block-actions"> -+ <?= $block->escapeHtml(__('Check items to add to the cart or')) ?> -+ <button type="button" class="action select" role="button"><span><?= $block->escapeHtml(__('select all')) ?></span></button> -+ </div> - <?php endif; ?> -- <div class="product-item-info <?= /* @escapeNotVerified */ $available ?>"> -- <?= /* @escapeNotVerified */ '<!-- ' . $image . '-->' ?> -- <a href="<?= /* @escapeNotVerified */ $block->getProductUrl($_item) ?>" class="product photo product-item-photo"> -- <?= $block->getImage($_item, $image)->toHtml() ?> -- </a> -- <div class="product details product-item-details"> -- <strong class="product name product-item-name"><a class="product-item-link" title="<?= $block->escapeHtml($_item->getName()) ?>" href="<?= /* @escapeNotVerified */ $block->getProductUrl($_item) ?>"> -- <?= $block->escapeHtml($_item->getName()) ?></a> -- </strong> -- -- <?= /* @escapeNotVerified */ $block->getProductPrice($_item) ?> -- -- <?php if ($templateType): ?> -- <?= $block->getReviewsSummaryHtml($_item, $templateType) ?> -- <?php endif; ?> -- -- <?php if ($canItemsAddToCart && !$_item->isComposite() && $_item->isSaleable() && $type == 'related'): ?> -- <?php if (!$_item->getRequiredOptions()): ?> -- <div class="field choice related"> -- <input type="checkbox" class="checkbox related" id="related-checkbox<?= /* @escapeNotVerified */ $_item->getId() ?>" name="related_products[]" value="<?= /* @escapeNotVerified */ $_item->getId() ?>" /> -- <label class="label" for="related-checkbox<?= /* @escapeNotVerified */ $_item->getId() ?>"><span><?= /* @escapeNotVerified */ __('Add to Cart') ?></span></label> -- </div> -+ <div class="products wrapper grid products-grid products-<?= $block->escapeHtmlAttr($type) ?>"> -+ <ol class="products list items product-items"> -+ <?php foreach ($items as $_item) :?> -+ <?php $available = ''; ?> -+ <?php if (!$_item->isComposite() && $_item->isSaleable() && $type == 'related') :?> -+ <?php if (!$_item->getRequiredOptions()) :?> -+ <?php $available = 'related-available'; ?> - <?php endif; ?> - <?php endif; ?> -+ <?php if ($type == 'related' || $type == 'upsell') :?> -+ <li class="item product product-item" style="display: none;"> -+ <?php else :?> -+ <li class="item product product-item"> -+ <?php endif; ?> -+ <div class="product-item-info <?= /* @noEscape */ $available ?>"> -+ <?= /* @noEscape */ '<!-- ' . $image . '-->' ?> -+ <a href="<?= $block->escapeUrl($block->getProductUrl($_item)) ?>" class="product photo product-item-photo"> -+ <?= $block->getImage($_item, $image)->toHtml() ?> -+ </a> -+ <div class="product details product-item-details"> -+ <strong class="product name product-item-name"><a class="product-item-link" title="<?= $block->escapeHtml($_item->getName()) ?>" href="<?= $block->escapeUrl($block->getProductUrl($_item)) ?>"> -+ <?= $block->escapeHtml($_item->getName()) ?></a> -+ </strong> -+ -+ <?= /* @noEscape */ $block->getProductPrice($_item) ?> -+ -+ <?php if ($templateType) :?> -+ <?= $block->getReviewsSummaryHtml($_item, $templateType) ?> -+ <?php endif; ?> - -- <?php if ($showAddTo || $showCart): ?> -- <div class="product actions product-item-actions"> -- <?php if ($showCart): ?> -- <div class="actions-primary"> -- <?php if ($_item->isSaleable()): ?> -- <?php if ($_item->getTypeInstance()->hasRequiredOptions($_item)): ?> -- <button class="action tocart primary" data-mage-init='{"redirectUrl": {"url": "<?= /* @escapeNotVerified */ $block->getAddToCartUrl($_item) ?>"}}' type="button" title="<?= /* @escapeNotVerified */ __('Add to Cart') ?>"> -- <span><?= /* @escapeNotVerified */ __('Add to Cart') ?></span> -- </button> -- <?php else: ?> -- <?php $postDataHelper = $this->helper('Magento\Framework\Data\Helper\PostHelper'); -- $postData = $postDataHelper->getPostData($block->getAddToCartUrl($_item), ['product' => $_item->getEntityId()]) -- ?> -- <button class="action tocart primary" -- data-post='<?= /* @escapeNotVerified */ $postData ?>' -- type="button" title="<?= /* @escapeNotVerified */ __('Add to Cart') ?>"> -- <span><?= /* @escapeNotVerified */ __('Add to Cart') ?></span> -- </button> -- <?php endif; ?> -- <?php else: ?> -- <?php if ($_item->getIsSalable()): ?> -- <div class="stock available"><span><?= /* @escapeNotVerified */ __('In stock') ?></span></div> -- <?php else: ?> -- <div class="stock unavailable"><span><?= /* @escapeNotVerified */ __('Out of stock') ?></span></div> -- <?php endif; ?> -- <?php endif; ?> -- </div> -+ <?php if ($canItemsAddToCart && !$_item->isComposite() && $_item->isSaleable() && $type == 'related') :?> -+ <?php if (!$_item->getRequiredOptions()) :?> -+ <div class="field choice related"> -+ <input type="checkbox" class="checkbox related" id="related-checkbox<?= $block->escapeHtmlAttr($_item->getId()) ?>" name="related_products[]" value="<?= $block->escapeHtmlAttr($_item->getId()) ?>" /> -+ <label class="label" for="related-checkbox<?= $block->escapeHtmlAttr($_item->getId()) ?>"><span><?= $block->escapeHtml(__('Add to Cart')) ?></span></label> -+ </div> -+ <?php endif; ?> - <?php endif; ?> - -- <?php if ($showAddTo): ?> -- <div class="secondary-addto-links actions-secondary" data-role="add-to-links"> -- <?php if ($addToBlock = $block->getChildBlock('addto')): ?> -- <?= $addToBlock->setProduct($_item)->getChildHtml() ?> -+ <?php if ($showAddTo || $showCart) :?> -+ <div class="product actions product-item-actions"> -+ <?php if ($showCart) :?> -+ <div class="actions-primary"> -+ <?php if ($_item->isSaleable()) :?> -+ <?php if ($_item->getTypeInstance()->hasRequiredOptions($_item)) :?> -+ <button class="action tocart primary" data-mage-init='{"redirectUrl": {"url": "<?= $block->escapeUrl($block->getAddToCartUrl($_item)) ?>"}}' type="button" title="<?= $block->escapeHtmlAttr(__('Add to Cart')) ?>"> -+ <span><?= $block->escapeHtml(__('Add to Cart')) ?></span> -+ </button> -+ <?php else :?> -+ <?php $postDataHelper = $this->helper(Magento\Framework\Data\Helper\PostHelper::class); -+ $postData = $postDataHelper->getPostData($block->escapeUrl($block->getAddToCartUrl($_item)), ['product' => $_item->getEntityId()]) -+ ?> -+ <button class="action tocart primary" -+ data-post='<?= /* @noEscape */ $postData ?>' -+ type="button" title="<?= $block->escapeHtmlAttr(__('Add to Cart')) ?>"> -+ <span><?= $block->escapeHtml(__('Add to Cart')) ?></span> -+ </button> -+ <?php endif; ?> -+ <?php else :?> -+ <?php if ($_item->getIsSalable()) :?> -+ <div class="stock available"><span><?= $block->escapeHtml(__('In stock')) ?></span></div> -+ <?php else :?> -+ <div class="stock unavailable"><span><?= $block->escapeHtml(__('Out of stock')) ?></span></div> -+ <?php endif; ?> -+ <?php endif; ?> -+ </div> -+ <?php endif; ?> -+ -+ <?php if ($showAddTo) :?> -+ <div class="secondary-addto-links actions-secondary" data-role="add-to-links"> -+ <?php if ($addToBlock = $block->getChildBlock('addto')) :?> -+ <?= $addToBlock->setProduct($_item)->getChildHtml() ?> -+ <?php endif; ?> -+ </div> - <?php endif; ?> - </div> - <?php endif; ?> - </div> -- <?php endif; ?> -- </div> -- </div> -- </li> -- <?php endforeach ?> -- </ol> -+ </div> -+ </li> -+ <?php endforeach ?> -+ </ol> -+ </div> -+ </div> - </div> -- </div> --</div> --<?php endif;?> -+ <?php endif;?> -diff --git a/app/code/Magento/Catalog/view/frontend/templates/product/list/toolbar.phtml b/app/code/Magento/Catalog/view/frontend/templates/product/list/toolbar.phtml -index 02a6e999ad5..b2ae8b9f7ab 100644 ---- a/app/code/Magento/Catalog/view/frontend/templates/product/list/toolbar.phtml -+++ b/app/code/Magento/Catalog/view/frontend/templates/product/list/toolbar.phtml -@@ -3,9 +3,6 @@ - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -- --// @codingStandardsIgnoreFile -- - ?> - <?php - /** -@@ -13,11 +10,13 @@ - * - * @var $block \Magento\Catalog\Block\Product\ProductList\Toolbar - */ --use Magento\Catalog\Model\Product\ProductList\Toolbar; -+ -+// phpcs:disable Magento2.Security.IncludeFile.FoundIncludeFile -+// phpcs:disable PSR2.Methods.FunctionCallSignature.SpaceBeforeOpenBracket - ?> --<?php if ($block->getCollection()->getSize()): ?> -- <div class="toolbar toolbar-products" data-mage-init='<?= /* @escapeNotVerified */ $block->getWidgetOptionsJson() ?>'> -- <?php if ($block->isExpanded()): ?> -+<?php if ($block->getCollection()->getSize()) :?> -+ <div class="toolbar toolbar-products" data-mage-init='<?= /* @noEscape */ $block->getWidgetOptionsJson() ?>'> -+ <?php if ($block->isExpanded()) :?> - <?php include ($block->getTemplateFile('Magento_Catalog::product/list/toolbar/viewmode.phtml')) ?> - <?php endif; ?> - -@@ -27,7 +26,7 @@ use Magento\Catalog\Model\Product\ProductList\Toolbar; - - <?php include ($block->getTemplateFile('Magento_Catalog::product/list/toolbar/limiter.phtml')) ?> - -- <?php if ($block->isExpanded()): ?> -+ <?php if ($block->isExpanded()) :?> - <?php include ($block->getTemplateFile('Magento_Catalog::product/list/toolbar/sorter.phtml')) ?> - <?php endif; ?> - </div> -diff --git a/app/code/Magento/Catalog/view/frontend/templates/product/list/toolbar/amount.phtml b/app/code/Magento/Catalog/view/frontend/templates/product/list/toolbar/amount.phtml -index b4ff1afa1c6..a8f504d6a4f 100644 ---- a/app/code/Magento/Catalog/view/frontend/templates/product/list/toolbar/amount.phtml -+++ b/app/code/Magento/Catalog/view/frontend/templates/product/list/toolbar/amount.phtml -@@ -3,9 +3,6 @@ - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -- --// @codingStandardsIgnoreFile -- - ?> - <?php - /** -@@ -13,19 +10,27 @@ - * - * @var $block \Magento\Catalog\Block\Product\ProductList\Toolbar - */ --use Magento\Catalog\Model\Product\ProductList\Toolbar; - ?> - <p class="toolbar-amount" id="toolbar-amount"> -- <?php if ($block->getLastPageNum() > 1): ?> -- <?php /* @escapeNotVerified */ echo __('Items %1-%2 of %3', -- '<span class="toolbar-number">' . $block->getFirstNum() . '</span>', -- '<span class="toolbar-number">' . $block->getLastNum() . '</span>', -- '<span class="toolbar-number">' . $block->getTotalNum() . '</span>') ?> -- <?php elseif ($block->getTotalNum() == 1): ?> -- <?php /* @escapeNotVerified */ echo __('%1 Item', -- '<span class="toolbar-number">' . $block->getTotalNum() . '</span>') ?> -- <?php else: ?> -- <?php /* @escapeNotVerified */ echo __('%1 Items', -- '<span class="toolbar-number">' . $block->getTotalNum() . '</span>') ?> -+ <?php if ($block->getLastPageNum() > 1) :?> -+ <?= $block->escapeHtml( -+ __( -+ 'Items %1-%2 of %3', -+ '<span class="toolbar-number">' . $block->getFirstNum() . '</span>', -+ '<span class="toolbar-number">' . $block->getLastNum() . '</span>', -+ '<span class="toolbar-number">' . $block->getTotalNum() . '</span>' -+ ), -+ ['span'] -+ ) ?> -+ <?php elseif ($block->getTotalNum() == 1) :?> -+ <?= $block->escapeHtml( -+ __('%1 Item', '<span class="toolbar-number">' . $block->getTotalNum() . '</span>'), -+ ['span'] -+ ) ?> -+ <?php else :?> -+ <?= $block->escapeHtml( -+ __('%1 Items', '<span class="toolbar-number">' . $block->getTotalNum() . '</span>'), -+ ['span'] -+ ) ?> - <?php endif; ?> - </p> -diff --git a/app/code/Magento/Catalog/view/frontend/templates/product/list/toolbar/limiter.phtml b/app/code/Magento/Catalog/view/frontend/templates/product/list/toolbar/limiter.phtml -index ec4541bde5c..4ded219748c 100644 ---- a/app/code/Magento/Catalog/view/frontend/templates/product/list/toolbar/limiter.phtml -+++ b/app/code/Magento/Catalog/view/frontend/templates/product/list/toolbar/limiter.phtml -@@ -3,9 +3,6 @@ - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -- --// @codingStandardsIgnoreFile -- - ?> - <?php - /** -@@ -13,21 +10,22 @@ - * - * @var $block \Magento\Catalog\Block\Product\ProductList\Toolbar - */ --use Magento\Catalog\Model\Product\ProductList\Toolbar; - ?> - <div class="field limiter"> - <label class="label" for="limiter"> -- <span><?= /* @escapeNotVerified */ __('Show') ?></span> -+ <span><?= $block->escapeHtml(__('Show')) ?></span> - </label> - <div class="control"> - <select id="limiter" data-role="limiter" class="limiter-options"> -- <?php foreach ($block->getAvailableLimit() as $_key => $_limit): ?> -- <option value="<?= /* @escapeNotVerified */ $_key ?>"<?php if ($block->isLimitCurrent($_key)): ?> -- selected="selected"<?php endif ?>> -- <?= /* @escapeNotVerified */ $_limit ?> -+ <?php foreach ($block->getAvailableLimit() as $_key => $_limit) :?> -+ <option value="<?= $block->escapeHtmlAttr($_key) ?>" -+ <?php if ($block->isLimitCurrent($_key)) :?> -+ selected="selected" -+ <?php endif ?>> -+ <?= $block->escapeHtml($_limit) ?> - </option> - <?php endforeach; ?> - </select> - </div> -- <span class="limiter-text"><?= /* @escapeNotVerified */ __('per page') ?></span> -+ <span class="limiter-text"><?= $block->escapeHtml(__('per page')) ?></span> - </div> -diff --git a/app/code/Magento/Catalog/view/frontend/templates/product/list/toolbar/sorter.phtml b/app/code/Magento/Catalog/view/frontend/templates/product/list/toolbar/sorter.phtml -index 92514c5b8ea..58dde199998 100644 ---- a/app/code/Magento/Catalog/view/frontend/templates/product/list/toolbar/sorter.phtml -+++ b/app/code/Magento/Catalog/view/frontend/templates/product/list/toolbar/sorter.phtml -@@ -3,9 +3,6 @@ - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -- --// @codingStandardsIgnoreFile -- - ?> - <?php - /** -@@ -13,14 +10,13 @@ - * - * @var $block \Magento\Catalog\Block\Product\ProductList\Toolbar - */ --use Magento\Catalog\Model\Product\ProductList\Toolbar; - ?> - <div class="toolbar-sorter sorter"> -- <label class="sorter-label" for="sorter"><?= /* @escapeNotVerified */ __('Sort By') ?></label> -+ <label class="sorter-label" for="sorter"><?= $block->escapeHtml(__('Sort By')) ?></label> - <select id="sorter" data-role="sorter" class="sorter-options"> -- <?php foreach ($block->getAvailableOrders() as $_key => $_order): ?> -- <option value="<?= /* @escapeNotVerified */ $_key ?>" -- <?php if ($block->isOrderCurrent($_key)): ?> -+ <?php foreach ($block->getAvailableOrders() as $_key => $_order) :?> -+ <option value="<?= $block->escapeHtmlAttr($_key) ?>" -+ <?php if ($block->isOrderCurrent($_key)) :?> - selected="selected" - <?php endif; ?> - > -@@ -28,13 +24,21 @@ use Magento\Catalog\Model\Product\ProductList\Toolbar; - </option> - <?php endforeach; ?> - </select> -- <?php if ($block->getCurrentDirection() == 'desc'): ?> -- <a title="<?= /* @escapeNotVerified */ __('Set Ascending Direction') ?>" href="#" class="action sorter-action sort-desc" data-role="direction-switcher" data-value="asc"> -- <span><?= /* @escapeNotVerified */ __('Set Ascending Direction') ?></span> -+ <?php if ($block->getCurrentDirection() == 'desc') :?> -+ <a title="<?= $block->escapeHtmlAttr(__('Set Ascending Direction')) ?>" -+ href="#" -+ class="action sorter-action sort-desc" -+ data-role="direction-switcher" -+ data-value="asc"> -+ <span><?= $block->escapeHtml(__('Set Ascending Direction')) ?></span> - </a> -- <?php else: ?> -- <a title="<?= /* @escapeNotVerified */ __('Set Descending Direction') ?>" href="#" class="action sorter-action sort-asc" data-role="direction-switcher" data-value="desc"> -- <span><?= /* @escapeNotVerified */ __('Set Descending Direction') ?></span> -+ <?php else :?> -+ <a title="<?= $block->escapeHtmlAttr(__('Set Descending Direction')) ?>" -+ href="#" -+ class="action sorter-action sort-asc" -+ data-role="direction-switcher" -+ data-value="desc"> -+ <span><?= $block->escapeHtml(__('Set Descending Direction')) ?></span> - </a> - <?php endif; ?> - </div> -diff --git a/app/code/Magento/Catalog/view/frontend/templates/product/list/toolbar/viewmode.phtml b/app/code/Magento/Catalog/view/frontend/templates/product/list/toolbar/viewmode.phtml -index 366dfba71b0..955897f315d 100644 ---- a/app/code/Magento/Catalog/view/frontend/templates/product/list/toolbar/viewmode.phtml -+++ b/app/code/Magento/Catalog/view/frontend/templates/product/list/toolbar/viewmode.phtml -@@ -3,9 +3,6 @@ - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -- --// @codingStandardsIgnoreFile -- - ?> - <?php - /** -@@ -13,32 +10,31 @@ - * - * @var $block \Magento\Catalog\Block\Product\ProductList\Toolbar - */ --use Magento\Catalog\Model\Product\ProductList\Toolbar; - ?> --<?php if ($block->isEnabledViewSwitcher()): ?> --<div class="modes"> -- <?php $_modes = $block->getModes(); ?> -- <?php if ($_modes && count($_modes) > 1): ?> -- <strong class="modes-label" id="modes-label"><?= /* @escapeNotVerified */ __('View as') ?></strong> -- <?php foreach ($block->getModes() as $_code => $_label): ?> -- <?php if ($block->isModeActive($_code)): ?> -- <strong title="<?= /* @escapeNotVerified */ $_label ?>" -- class="modes-mode active mode-<?= /* @escapeNotVerified */ strtolower($_code) ?>" -- data-value="<?= /* @escapeNotVerified */ strtolower($_code) ?>"> -- <span><?= /* @escapeNotVerified */ $_label ?></span> -- </strong> -- <?php else: ?> -- <a class="modes-mode mode-<?= /* @escapeNotVerified */ strtolower($_code) ?>" -- title="<?= /* @escapeNotVerified */ $_label ?>" -- href="#" -- data-role="mode-switcher" -- data-value="<?= /* @escapeNotVerified */ strtolower($_code) ?>" -- id="mode-<?= /* @escapeNotVerified */ strtolower($_code) ?>" -- aria-labelledby="modes-label mode-<?= /* @escapeNotVerified */ strtolower($_code) ?>"> -- <span><?= /* @escapeNotVerified */ $_label ?></span> -- </a> -- <?php endif; ?> -- <?php endforeach; ?> -- <?php endif; ?> --</div> -+<?php if ($block->isEnabledViewSwitcher()) :?> -+ <div class="modes"> -+ <?php $_modes = $block->getModes(); ?> -+ <?php if ($_modes && count($_modes) > 1) :?> -+ <strong class="modes-label" id="modes-label"><?= $block->escapeHtml(__('View as')) ?></strong> -+ <?php foreach ($block->getModes() as $_code => $_label) :?> -+ <?php if ($block->isModeActive($_code)) :?> -+ <strong title="<?= $block->escapeHtmlAttr($_label) ?>" -+ class="modes-mode active mode-<?= $block->escapeHtmlAttr(strtolower($_code)) ?>" -+ data-value="<?= $block->escapeHtmlAttr(strtolower($_code)) ?>"> -+ <span><?= $block->escapeHtml($_label) ?></span> -+ </strong> -+ <?php else :?> -+ <a class="modes-mode mode-<?= $block->escapeHtmlAttr(strtolower($_code)) ?>" -+ title="<?= $block->escapeHtmlAttr($_label) ?>" -+ href="#" -+ data-role="mode-switcher" -+ data-value="<?= $block->escapeHtmlAttr(strtolower($_code)) ?>" -+ id="mode-<?= $block->escapeHtmlAttr(strtolower($_code)) ?>" -+ aria-labelledby="modes-label mode-<?= $block->escapeHtmlAttr(strtolower($_code)) ?>"> -+ <span><?= $block->escapeHtml($_label) ?></span> -+ </a> -+ <?php endif; ?> -+ <?php endforeach; ?> -+ <?php endif; ?> -+ </div> - <?php endif; ?> -diff --git a/app/code/Magento/Catalog/view/frontend/templates/product/listing.phtml b/app/code/Magento/Catalog/view/frontend/templates/product/listing.phtml -index f2d5e40cca4..b776fd4f7e1 100644 ---- a/app/code/Magento/Catalog/view/frontend/templates/product/listing.phtml -+++ b/app/code/Magento/Catalog/view/frontend/templates/product/listing.phtml -@@ -3,28 +3,29 @@ - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -- --// @codingStandardsIgnoreFile -- - ?> - <?php -+// phpcs:disable Magento2.Templates.ThisInTemplate.FoundThis -+// phpcs:disable Magento2.Files.LineLength.MaxExceeded -+// phpcs:disable Magento2.Security.LanguageConstruct.DirectOutput -+ - /** - * Product list template - * -- * @see \Magento\Catalog\Block\Product\ListProduct -+ * @var $block \Magento\Catalog\Block\Product\ListProduct - */ - ?> - <?php - $start = microtime(true); - $_productCollection = $block->getLoadedProductCollection(); --$_helper = $this->helper('Magento\Catalog\Helper\Output'); -+$_helper = $this->helper(Magento\Catalog\Helper\Output::class); - ?> --<?php if (!$_productCollection->count()): ?> --<p class="message note"><?= /* @escapeNotVerified */ __('We can\'t find products matching the selection.') ?></p> --<?php else: ?> --<?= $block->getToolbarHtml() ?> --<?= $block->getAdditionalHtml() ?> --<?php -+<?php if (!$_productCollection->count()) :?> -+ <p class="message note"><?= $block->escapeHtml(__('We can\'t find products matching the selection.')) ?></p> -+<?php else :?> -+ <?= $block->getToolbarHtml() ?> -+ <?= $block->getAdditionalHtml() ?> -+ <?php - if ($block->getMode() == 'grid') { - $viewMode = 'grid'; - $image = 'category_page_grid'; -@@ -36,65 +37,65 @@ $_helper = $this->helper('Magento\Catalog\Helper\Output'); - $showDescription = true; - $templateType = \Magento\Catalog\Block\Product\ReviewRendererInterface::FULL_VIEW; - } --?> --<div class="products wrapper <?= /* @escapeNotVerified */ $viewMode ?>"> -- <ol class="products list items"> -- <?php foreach ($_productCollection as $_product): ?> -- <li class="item product"> -- <div class="product"> -- <?php // Product Image ?> -- <a href="<?= /* @escapeNotVerified */ $_product->getProductUrl() ?>" class="product photo"> -- <?= $block->getImage($_product, $image)->toHtml() ?> -- </a> -- <div class="product details"> -- <?php -+ ?> -+ <div class="products wrapper <?= /* @noEscape */ $viewMode ?>"> -+ <ol class="products list items"> -+ <?php foreach ($_productCollection as $_product) :?> -+ <li class="item product"> -+ <div class="product"> -+ <?php // Product Image ?> -+ <a href="<?= $block->escapeUrl($_product->getProductUrl()) ?>" class="product photo"> -+ <?= $block->getImage($_product, $image)->toHtml() ?> -+ </a> -+ <div class="product details"> -+ <?php - -- $info = []; -- $info['name'] = '<strong class="product name">' -- . ' <a href="' . $_product->getProductUrl() . '" title="' -- . $block->stripTags($_product->getName(), null, true) . '">' -- . $_helper->productAttribute($_product, $_product->getName(), 'name') -- . '</a></strong>'; -- $info['price'] = $block->getProductPrice($_product); -- $info['review'] = $block->getReviewsSummaryHtml($_product, $templateType); -+ $info = []; -+ $info['name'] = '<strong class="product name">' -+ . ' <a href="' . $block->escapeUrl($_product->getProductUrl()) . '" title="' -+ . $block->stripTags($_product->getName(), null, true) . '">' -+ . $_helper->productAttribute($_product, $_product->getName(), 'name') -+ . '</a></strong>'; -+ $info['price'] = $block->getProductPrice($_product); -+ $info['review'] = $block->getReviewsSummaryHtml($_product, $templateType); - -- if ($_product->isSaleable()) { -- $info['button'] = '<button type="button" title="' . __('Add to Cart') . '" class="action tocart"' -- . ' data-mage-init=\'{ "redirectUrl": { "event": "click", url: "' . $block->getAddToCartUrl($_product) . '"} }\'>' -- . '<span>' . __('Add to Cart') . '</span></button>'; -- } else { -- $info['button'] = $_product->getIsSalable() ? '<div class="stock available"><span>' . __('In stock') . '</span></div>' : -- '<div class="stock unavailable"><span>' . __('Out of stock') . '</span></div>'; -- } -+ if ($_product->isSaleable()) { -+ $info['button'] = '<button type="button" title="' . $block->escapeHtmlAttr(__('Add to Cart')) . '" class="action tocart"' -+ . ' data-mage-init=\'{ "redirectUrl": { "event": "click", url: "' . $block->escapeUrl($block->getAddToCartUrl($_product)) . '"} }\'>' -+ . '<span>' . $block->escapeHtml(__('Add to Cart')) . '</span></button>'; -+ } else { -+ $info['button'] = $_product->getIsSalable() ? '<div class="stock available"><span>' . $block->escapeHtml(__('In stock')) . '</span></div>' : -+ '<div class="stock unavailable"><span>' . $block->escapeHtml(__('Out of stock')) . '</span></div>'; -+ } - -- $info['links'] = '<div class="product links" data-role="add-to-links">' -- . '<a href="#" data-post=\'' . $this->helper('Magento\Wishlist\Helper\Data')->getAddParams($_product) . '\' class="action towishlist" data-action="add-to-wishlist">' -- . '<span>' . __('Add to Wish List') . '</span></a>' -- . '<a href="' . $block->getAddToCompareUrl($_product) . '" class="action tocompare">' -- . '<span>' . __('Add to Compare') . '</span></a></div>'; -- $info['actions'] = '<div class="product action">' . $info['button'] . $info['links'] . '</div>'; -+ $info['links'] = '<div class="product links" data-role="add-to-links">' -+ . '<a href="#" data-post=\'' . $this->helper(Magento\Wishlist\Helper\Data::class)->getAddParams($_product) . '\' class="action towishlist" data-action="add-to-wishlist">' -+ . '<span>' . $block->escapeHtml(__('Add to Wish List')) . '</span></a>' -+ . '<a href="' . $block->escapeUrl($block->getAddToCompareUrl($_product)) . '" class="action tocompare">' -+ . '<span>' . $block->escapeHtml(__('Add to Compare')) . '</span></a></div>'; -+ $info['actions'] = '<div class="product action">' . $info['button'] . $info['links'] . '</div>'; - -- if ($showDescription) { -- $info['description'] = '<div class="product description">' -- . $_helper->productAttribute($_product, $_product->getShortDescription(), 'short_description') -- . ' <a href="' . $_product->getProductUrl() . '" class="action more">' -- . __('Learn More') . '</a></div>'; -- } else { -- $info['description'] = ''; -- } -+ if ($showDescription) { -+ $info['description'] = '<div class="product description">' -+ . $_helper->productAttribute($_product, $_product->getShortDescription(), 'short_description') -+ . ' <a href="' . $block->escapeUrl($_product->getProductUrl()) . '" class="action more">' -+ . $block->escapeHtml(__('Learn More')) . '</a></div>'; -+ } else { -+ $info['description'] = ''; -+ } - -- $details = $block->getInfoOrder() ?: ['name','price','review','description','actions']; -- foreach ($details as $detail) { -- /* @escapeNotVerified */ echo $info[$detail]; -- } -- ?> -+ $details = $block->getInfoOrder() ?: ['name','price','review','description','actions']; -+ foreach ($details as $detail) { -+ /* @noEscape */ echo $info[$detail]; -+ } -+ ?> - -+ </div> - </div> -- </div> -- </li> -- <?php endforeach; ?> -- </ol> --</div> --<?= $block->getToolbarHtml() ?> -+ </li> -+ <?php endforeach; ?> -+ </ol> -+ </div> -+ <?= $block->getToolbarHtml() ?> - <?php endif; ?> --<?= /* @escapeNotVerified */ $time_taken = microtime(true) - $start ?> -+<?= $time_taken = microtime(true) - $start ?> -diff --git a/app/code/Magento/Catalog/view/frontend/templates/product/view/additional.phtml b/app/code/Magento/Catalog/view/frontend/templates/product/view/additional.phtml -index 2d89e24cc7a..316fdb06592 100644 ---- a/app/code/Magento/Catalog/view/frontend/templates/product/view/additional.phtml -+++ b/app/code/Magento/Catalog/view/frontend/templates/product/view/additional.phtml -@@ -4,9 +4,8 @@ - * See COPYING.txt for license details. - */ - --// @codingStandardsIgnoreFile -- -+/** @var $block \Magento\Catalog\Block\Product\View\Additional */ - ?> --<?php foreach ($block->getChildHtmlList() as $_html): ?> -- <?= /* @escapeNotVerified */ $_html ?> -+<?php foreach ($block->getChildHtmlList() as $_html) :?> -+ <?= /* @noEscape */ $_html ?> - <?php endforeach; ?> -diff --git a/app/code/Magento/Catalog/view/frontend/templates/product/view/addto.phtml b/app/code/Magento/Catalog/view/frontend/templates/product/view/addto.phtml -index 0893cfab0bb..19241757645 100644 ---- a/app/code/Magento/Catalog/view/frontend/templates/product/view/addto.phtml -+++ b/app/code/Magento/Catalog/view/frontend/templates/product/view/addto.phtml -@@ -4,8 +4,6 @@ - * See COPYING.txt for license details. - */ - --// @codingStandardsIgnoreFile -- - /** @var $block \Magento\Catalog\Block\Product\View*/ - ?> - <div class="product-addto-links" data-role="add-to-links"> -diff --git a/app/code/Magento/Catalog/view/frontend/templates/product/view/addto/compare.phtml b/app/code/Magento/Catalog/view/frontend/templates/product/view/addto/compare.phtml -index adf0f44d0c8..9183e65181c 100644 ---- a/app/code/Magento/Catalog/view/frontend/templates/product/view/addto/compare.phtml -+++ b/app/code/Magento/Catalog/view/frontend/templates/product/view/addto/compare.phtml -@@ -4,11 +4,13 @@ - * See COPYING.txt for license details. - */ - --// @codingStandardsIgnoreFile -- - /** @var $block \Magento\Catalog\Block\Product\View\Addto\Compare */ - ?> - --<a href="#" data-post='<?= /* @escapeNotVerified */ $block->getPostDataParams() ?>' -+<?php $viewModel = $block->getData('addToCompareViewModel'); ?> -+<?php if ($viewModel->isAvailableForCompare($block->getProduct())) :?> -+<a href="#" data-post='<?= /* @noEscape */ $block->getPostDataParams() ?>' - data-role="add-to-links" -- class="action tocompare"><span><?= /* @escapeNotVerified */ __('Add to Compare') ?></span></a> -+ class="action tocompare"><span><?= $block->escapeHtml(__('Add to Compare')) ?></span></a> -+<?php endif; ?> -+ -diff --git a/app/code/Magento/Catalog/view/frontend/templates/product/view/addtocart.phtml b/app/code/Magento/Catalog/view/frontend/templates/product/view/addtocart.phtml -index 9c18a18ff58..f15824595f0 100644 ---- a/app/code/Magento/Catalog/view/frontend/templates/product/view/addtocart.phtml -+++ b/app/code/Magento/Catalog/view/frontend/templates/product/view/addtocart.phtml -@@ -4,24 +4,23 @@ - * See COPYING.txt for license details. - */ - --// @codingStandardsIgnoreFile -- - /** @var $block \Magento\Catalog\Block\Product\View */ - ?> - <?php $_product = $block->getProduct(); ?> - <?php $buttonTitle = __('Add to Cart'); ?> --<?php if ($_product->isSaleable()): ?> -+<?php if ($_product->isSaleable()) :?> - <div class="box-tocart"> - <div class="fieldset"> -- <?php if ($block->shouldRenderQuantity()): ?> -+ <?php if ($block->shouldRenderQuantity()) :?> - <div class="field qty"> -- <label class="label" for="qty"><span><?= /* @escapeNotVerified */ __('Qty') ?></span></label> -+ <label class="label" for="qty"><span><?= $block->escapeHtml(__('Qty')) ?></span></label> - <div class="control"> - <input type="number" - name="qty" - id="qty" -- value="<?= /* @escapeNotVerified */ $block->getProductDefaultQty() * 1 ?>" -- title="<?= /* @escapeNotVerified */ __('Qty') ?>" -+ min="0" -+ value="<?= $block->getProductDefaultQty() * 1 ?>" -+ title="<?= $block->escapeHtmlAttr(__('Qty')) ?>" - class="input-text qty" - data-validate="<?= $block->escapeHtml(json_encode($block->getQuantityValidators())) ?>" - /> -@@ -30,10 +29,10 @@ - <?php endif; ?> - <div class="actions"> - <button type="submit" -- title="<?= /* @escapeNotVerified */ $buttonTitle ?>" -+ title="<?= $block->escapeHtmlAttr($buttonTitle) ?>" - class="action primary tocart" -- id="product-addtocart-button"> -- <span><?= /* @escapeNotVerified */ $buttonTitle ?></span> -+ id="product-addtocart-button" disabled> -+ <span><?= $block->escapeHtml($buttonTitle) ?></span> - </button> - <?= $block->getChildHtml('', true) ?> - </div> -diff --git a/app/code/Magento/Catalog/view/frontend/templates/product/view/attribute.phtml b/app/code/Magento/Catalog/view/frontend/templates/product/view/attribute.phtml -index 86f97cf6f6a..2e022a5df14 100644 ---- a/app/code/Magento/Catalog/view/frontend/templates/product/view/attribute.phtml -+++ b/app/code/Magento/Catalog/view/frontend/templates/product/view/attribute.phtml -@@ -4,16 +4,16 @@ - * See COPYING.txt for license details. - */ - --// @codingStandardsIgnoreFile -+// phpcs:disable Magento2.Templates.ThisInTemplate.FoundThis - - /** - * Product view template - * -- * @see \Magento\Catalog\Block\Product\View\Description -+ * @var $block \Magento\Catalog\Block\Product\View\Description - */ - ?> - <?php --$_helper = $this->helper('Magento\Catalog\Helper\Output'); -+$_helper = $this->helper(Magento\Catalog\Helper\Output::class); - $_product = $block->getProduct(); - $_call = $block->getAtCall(); - $_code = $block->getAtCode(); -@@ -32,15 +32,19 @@ if ($_attributeLabel && $_attributeLabel == 'default') { - $_attributeLabel = $_product->getResource()->getAttribute($_code)->getStoreLabel(); - } - if ($_attributeType && $_attributeType == 'text') { -- $_attributeValue = ($_helper->productAttribute($_product, $_product->$_call(), $_code)) ? $_product->getAttributeText($_code) : ''; -+ $_attributeValue = ($_helper->productAttribute($_product, $_product->$_call(), $_code)) -+ ? $_product->getAttributeText($_code) -+ : ''; - } else { - $_attributeValue = $_helper->productAttribute($_product, $_product->$_call(), $_code); - } - ?> - --<?php if ($_attributeValue): ?> --<div class="product attribute <?= /* @escapeNotVerified */ $_className ?>"> -- <?php if ($renderLabel): ?><strong class="type"><?= /* @escapeNotVerified */ $_attributeLabel ?></strong><?php endif; ?> -- <div class="value" <?= /* @escapeNotVerified */ $_attributeAddAttribute ?>><?= /* @escapeNotVerified */ $_attributeValue ?></div> -+<?php if ($_attributeValue) :?> -+<div class="product attribute <?= $block->escapeHtmlAttr($_className) ?>"> -+ <?php if ($renderLabel) :?> -+ <strong class="type"><?= $block->escapeHtml($_attributeLabel) ?></strong> -+ <?php endif; ?> -+ <div class="value" <?= /* @noEscape */ $_attributeAddAttribute ?>><?= /* @noEscape */ $_attributeValue ?></div> - </div> - <?php endif; ?> -diff --git a/app/code/Magento/Catalog/view/frontend/templates/product/view/attributes.phtml b/app/code/Magento/Catalog/view/frontend/templates/product/view/attributes.phtml -index c930d2195a0..a4f0fb3efab 100644 ---- a/app/code/Magento/Catalog/view/frontend/templates/product/view/attributes.phtml -+++ b/app/code/Magento/Catalog/view/frontend/templates/product/view/attributes.phtml -@@ -4,7 +4,7 @@ - * See COPYING.txt for license details. - */ - --// @codingStandardsIgnoreFile -+// phpcs:disable Magento2.Templates.ThisInTemplate.FoundThis - - /** - * Product additional attributes template -@@ -13,18 +13,18 @@ - */ - ?> - <?php -- $_helper = $this->helper('Magento\Catalog\Helper\Output'); -+ $_helper = $this->helper(Magento\Catalog\Helper\Output::class); - $_product = $block->getProduct(); - ?> --<?php if ($_additional = $block->getAdditionalData()): ?> -+<?php if ($_additional = $block->getAdditionalData()) :?> - <div class="additional-attributes-wrapper table-wrapper"> - <table class="data table additional-attributes" id="product-attribute-specs-table"> -- <caption class="table-caption"><?= /* @escapeNotVerified */ __('More Information') ?></caption> -+ <caption class="table-caption"><?= $block->escapeHtml(__('More Information')) ?></caption> - <tbody> -- <?php foreach ($_additional as $_data): ?> -+ <?php foreach ($_additional as $_data) :?> - <tr> -- <th class="col label" scope="row"><?= $block->escapeHtml(__($_data['label'])) ?></th> -- <td class="col data" data-th="<?= $block->escapeHtml(__($_data['label'])) ?>"><?= /* @escapeNotVerified */ $_helper->productAttribute($_product, $_data['value'], $_data['code']) ?></td> -+ <th class="col label" scope="row"><?= $block->escapeHtml($_data['label']) ?></th> -+ <td class="col data" data-th="<?= $block->escapeHtmlAttr($_data['label']) ?>"><?= /* @noEscape */ $_helper->productAttribute($_product, $_data['value'], $_data['code']) ?></td> - </tr> - <?php endforeach; ?> - </tbody> -diff --git a/app/code/Magento/Catalog/view/frontend/templates/product/view/counter.phtml b/app/code/Magento/Catalog/view/frontend/templates/product/view/counter.phtml -index 4414214f99a..a4aa675b2c3 100644 ---- a/app/code/Magento/Catalog/view/frontend/templates/product/view/counter.phtml -+++ b/app/code/Magento/Catalog/view/frontend/templates/product/view/counter.phtml -@@ -13,7 +13,7 @@ - { - "*": { - "Magento_Catalog/js/product/view/provider": { -- "data": <?= /* @escapeNotVerified */ $block->getCurrentProductData() ?> -+ "data": <?= /* @noEscape */ $block->getCurrentProductData() ?> - } - } - } -diff --git a/app/code/Magento/Catalog/view/frontend/templates/product/view/description.phtml b/app/code/Magento/Catalog/view/frontend/templates/product/view/description.phtml -index b5cdd1a2a31..c08c4d771b3 100644 ---- a/app/code/Magento/Catalog/view/frontend/templates/product/view/description.phtml -+++ b/app/code/Magento/Catalog/view/frontend/templates/product/view/description.phtml -@@ -4,7 +4,7 @@ - * See COPYING.txt for license details. - */ - --// @codingStandardsIgnoreFile -+// phpcs:disable Magento2.Templates.ThisInTemplate.FoundThis - - /** - * Product description template -@@ -12,4 +12,8 @@ - * @var $block \Magento\Catalog\Block\Product\View\Description - */ - ?> --<?= /* @escapeNotVerified */ $this->helper('Magento\Catalog\Helper\Output')->productAttribute($block->getProduct(), $block->getProduct()->getDescription(), 'description') ?> -+<?= /* @noEscape */ $this->helper(Magento\Catalog\Helper\Output::class)->productAttribute( -+ $block->getProduct(), -+ $block->getProduct()->getDescription(), -+ 'description' -+) ?> -diff --git a/app/code/Magento/Catalog/view/frontend/templates/product/view/details.phtml b/app/code/Magento/Catalog/view/frontend/templates/product/view/details.phtml -index b1af46b8055..f4de22f98b0 100644 ---- a/app/code/Magento/Catalog/view/frontend/templates/product/view/details.phtml -+++ b/app/code/Magento/Catalog/view/frontend/templates/product/view/details.phtml -@@ -4,35 +4,34 @@ - * See COPYING.txt for license details. - */ - --// @codingStandardsIgnoreFile -- -+/** @var \Magento\Catalog\Block\Product\View\Details $block */ - ?> --<?php if ($detailedInfoGroup = $block->getGroupChildNames('detailed_info', 'getChildHtml')):?> -+<?php if ($detailedInfoGroup = $block->getGroupSortedChildNames('detailed_info', 'getChildHtml')) :?> - <div class="product info detailed"> - <?php $layout = $block->getLayout(); ?> - <div class="product data items" data-mage-init='{"tabs":{"openedState":"active"}}'> -- <?php foreach ($detailedInfoGroup as $name):?> -+ <?php foreach ($detailedInfoGroup as $name) :?> - <?php -- $html = $layout->renderElement($name); -- if (!trim($html)) { -- continue; -- } -- $alias = $layout->getElementAlias($name); -- $label = $block->getChildData($alias, 'title'); -+ $html = $layout->renderElement($name); -+ if (!trim($html)) { -+ continue; -+ } -+ $alias = $layout->getElementAlias($name); -+ $label = $block->getChildData($alias, 'title'); - ?> - <div class="data item title" -- aria-labeledby="tab-label-<?= /* @escapeNotVerified */ $alias ?>-title" -- data-role="collapsible" id="tab-label-<?= /* @escapeNotVerified */ $alias ?>"> -+ data-role="collapsible" id="tab-label-<?= $block->escapeHtmlAttr($alias) ?>"> - <a class="data switch" - tabindex="-1" -- data-toggle="switch" -- href="#<?= /* @escapeNotVerified */ $alias ?>" -- id="tab-label-<?= /* @escapeNotVerified */ $alias ?>-title"> -- <?= /* @escapeNotVerified */ $label ?> -+ data-toggle="trigger" -+ href="#<?= $block->escapeUrl($alias) ?>" -+ id="tab-label-<?= $block->escapeHtmlAttr($alias) ?>-title"> -+ <?= /* @noEscape */ $label ?> - </a> - </div> -- <div class="data item content" id="<?= /* @escapeNotVerified */ $alias ?>" data-role="content"> -- <?= /* @escapeNotVerified */ $html ?> -+ <div class="data item content" -+ aria-labelledby="tab-label-<?= $block->escapeHtmlAttr($alias) ?>-title" id="<?= $block->escapeHtmlAttr($alias) ?>" data-role="content"> -+ <?= /* @noEscape */ $html ?> - </div> - <?php endforeach;?> - </div> -diff --git a/app/code/Magento/Catalog/view/frontend/templates/product/view/form.phtml b/app/code/Magento/Catalog/view/frontend/templates/product/view/form.phtml -index 9c5cce78655..8d298aec9f1 100644 ---- a/app/code/Magento/Catalog/view/frontend/templates/product/view/form.phtml -+++ b/app/code/Magento/Catalog/view/frontend/templates/product/view/form.phtml -@@ -4,7 +4,7 @@ - * See COPYING.txt for license details. - */ - --// @codingStandardsIgnoreFile -+// phpcs:disable Magento2.Templates.ThisInTemplate.FoundThis - - /** - * Product view template -@@ -12,28 +12,28 @@ - * @var $block \Magento\Catalog\Block\Product\View - */ - ?> --<?php $_helper = $this->helper('Magento\Catalog\Helper\Output'); ?> -+<?php $_helper = $this->helper(Magento\Catalog\Helper\Output::class); ?> - <?php $_product = $block->getProduct(); ?> - - <div class="product-add-form"> - <form data-product-sku="<?= $block->escapeHtml($_product->getSku()) ?>" -- action="<?= /* @NoEscape */ $block->getSubmitUrl($_product) ?>" method="post" -- id="product_addtocart_form"<?php if ($_product->getOptions()): ?> enctype="multipart/form-data"<?php endif; ?>> -- <input type="hidden" name="product" value="<?= /* @escapeNotVerified */ $_product->getId() ?>" /> -+ action="<?= $block->escapeUrl($block->getSubmitUrl($_product)) ?>" method="post" -+ id="product_addtocart_form"<?php if ($_product->getOptions()) :?> enctype="multipart/form-data"<?php endif; ?>> -+ <input type="hidden" name="product" value="<?= (int)$_product->getId() ?>" /> - <input type="hidden" name="selected_configurable_option" value="" /> - <input type="hidden" name="related_product" id="related-products-field" value="" /> -- <input type="hidden" name="item" value="<?= /* @noEscape */ $block->getRequest()->getParam('id') ?>" /> -+ <input type="hidden" name="item" value="<?= $block->escapeHtmlAttr($block->getRequest()->getParam('id')) ?>" /> - <?= $block->getBlockHtml('formkey') ?> - <?= $block->getChildHtml('form_top') ?> -- <?php if (!$block->hasOptions()):?> -+ <?php if (!$block->hasOptions()) :?> - <?= $block->getChildHtml('product_info_form_content') ?> -- <?php else:?> -- <?php if ($_product->isSaleable() && $block->getOptionsContainer() == 'container1'):?> -+ <?php else :?> -+ <?php if ($_product->isSaleable() && $block->getOptionsContainer() == 'container1') :?> - <?= $block->getChildChildHtml('options_container') ?> - <?php endif;?> - <?php endif; ?> - -- <?php if ($_product->isSaleable() && $block->hasOptions() && $block->getOptionsContainer() == 'container2'):?> -+ <?php if ($_product->isSaleable() && $block->hasOptions() && $block->getOptionsContainer() == 'container2') :?> - <?= $block->getChildChildHtml('options_container') ?> - <?php endif;?> - <?= $block->getChildHtml('form_bottom') ?> -@@ -52,6 +52,6 @@ - return !$(elem).find('.price-from').length; - }); - -- priceBoxes.priceBox({'priceConfig': <?= /* @escapeNotVerified */ $block->getJsonConfig() ?>}); -+ priceBoxes.priceBox({'priceConfig': <?= /* @noEscape */ $block->getJsonConfig() ?>}); - }); - </script> -diff --git a/app/code/Magento/Catalog/view/frontend/templates/product/view/gallery.phtml b/app/code/Magento/Catalog/view/frontend/templates/product/view/gallery.phtml -index 1bfa30478df..4b33864aef4 100644 ---- a/app/code/Magento/Catalog/view/frontend/templates/product/view/gallery.phtml -+++ b/app/code/Magento/Catalog/view/frontend/templates/product/view/gallery.phtml -@@ -4,86 +4,48 @@ - * See COPYING.txt for license details. - */ - --// @codingStandardsIgnoreFile -- - /** - * Product media data template - * - * @var $block \Magento\Catalog\Block\Product\View\Gallery - */ - ?> -+ -+<?php -+$images = $block->getGalleryImages()->getItems(); -+$mainImage = current(array_filter($images, function ($img) use ($block) { -+ return $block->isMainImage($img); -+})); -+ -+if (!empty($images) && empty($mainImage)) { -+ $mainImage = $block->getGalleryImages()->getFirstItem(); -+} -+ -+$helper = $block->getData('imageHelper'); -+$mainImageData = $mainImage ? -+ $mainImage->getData('medium_image_url') : -+ $helper->getDefaultPlaceholderUrl('image'); -+ -+?> -+ - <div class="gallery-placeholder _block-content-loading" data-gallery-role="gallery-placeholder"> -- <div data-role="loader" class="loading-mask"> -- <div class="loader"> -- <img src="<?= /* @escapeNotVerified */ $block->getViewFileUrl('images/loader-1.gif') ?>" -- alt="<?= /* @escapeNotVerified */ __('Loading...') ?>"> -- </div> -- </div> -+ <img -+ alt="main product photo" -+ class="gallery-placeholder__image" -+ src="<?= /* @noEscape */ $mainImageData ?>" -+ /> - </div> --<!--Fix for jumping content. Loader must be the same size as gallery.--> --<script> -- var config = { -- "width": <?= /* @escapeNotVerified */ $block->getImageAttribute('product_page_image_medium', 'width') ?>, -- "thumbheight": <?php /* @escapeNotVerified */ echo $block->getImageAttribute('product_page_image_small', 'height') -- ?: $block->getImageAttribute('product_page_image_small', 'width'); ?>, -- "navtype": "<?= /* @escapeNotVerified */ $block->getVar("gallery/navtype") ?>", -- "height": <?= /* @escapeNotVerified */ $block->getImageAttribute('product_page_image_medium', 'height') ?> -- }, -- thumbBarHeight = 0, -- loader = document.querySelectorAll('[data-gallery-role="gallery-placeholder"] [data-role="loader"]')[0]; - -- if (config.navtype === 'horizontal') { -- thumbBarHeight = config.thumbheight; -- } -- -- loader.style.paddingBottom = ( config.height / config.width * 100) + "%"; --</script> - <script type="text/x-magento-init"> - { - "[data-gallery-role=gallery-placeholder]": { - "mage/gallery/gallery": { - "mixins":["magnifier/magnify"], -- "magnifierOpts": <?= /* @escapeNotVerified */ $block->getMagnifier() ?>, -- "data": <?= /* @escapeNotVerified */ $block->getGalleryImagesJson() ?>, -- "options": { -- "nav": "<?= /* @escapeNotVerified */ $block->getVar("gallery/nav") ?>", -- "loop": <?= /* @escapeNotVerified */ $block->getVar("gallery/loop") ? 'true' : 'false' ?>, -- "keyboard": <?= /* @escapeNotVerified */ $block->getVar("gallery/keyboard") ? 'true' : 'false' ?>, -- "arrows": <?= /* @escapeNotVerified */ $block->getVar("gallery/arrows") ? 'true' : 'false' ?>, -- "allowfullscreen": <?= /* @escapeNotVerified */ $block->getVar("gallery/allowfullscreen") ? 'true' : 'false' ?>, -- "showCaption": <?= /* @escapeNotVerified */ $block->getVar("gallery/caption") ? 'true' : 'false' ?>, -- "width": "<?= /* @escapeNotVerified */ $block->getImageAttribute('product_page_image_medium', 'width') ?>", -- "thumbwidth": "<?= /* @escapeNotVerified */ $block->getImageAttribute('product_page_image_small', 'width') ?>", -- <?php if ($block->getImageAttribute('product_page_image_small', 'height') || $block->getImageAttribute('product_page_image_small', 'width')): ?> -- "thumbheight": <?php /* @escapeNotVerified */ echo $block->getImageAttribute('product_page_image_small', 'height') -- ?: $block->getImageAttribute('product_page_image_small', 'width'); ?>, -- <?php endif; ?> -- <?php if ($block->getImageAttribute('product_page_image_medium', 'height') || $block->getImageAttribute('product_page_image_medium', 'width')): ?> -- "height": <?php /* @escapeNotVerified */ echo $block->getImageAttribute('product_page_image_medium', 'height') -- ?: $block->getImageAttribute('product_page_image_medium', 'width'); ?>, -- <?php endif; ?> -- <?php if ($block->getVar("gallery/transition/duration")): ?> -- "transitionduration": <?= /* @escapeNotVerified */ $block->getVar("gallery/transition/duration") ?>, -- <?php endif; ?> -- "transition": "<?= /* @escapeNotVerified */ $block->getVar("gallery/transition/effect") ?>", -- "navarrows": <?= /* @escapeNotVerified */ $block->getVar("gallery/navarrows") ? 'true' : 'false' ?>, -- "navtype": "<?= /* @escapeNotVerified */ $block->getVar("gallery/navtype") ?>", -- "navdir": "<?= /* @escapeNotVerified */ $block->getVar("gallery/navdir") ?>" -- }, -- "fullscreen": { -- "nav": "<?= /* @escapeNotVerified */ $block->getVar("gallery/fullscreen/nav") ?>", -- "loop": <?= /* @escapeNotVerified */ $block->getVar("gallery/fullscreen/loop") ? 'true' : 'false' ?>, -- "navdir": "<?= /* @escapeNotVerified */ $block->getVar("gallery/fullscreen/navdir") ?>", -- "navarrows": <?= /* @escapeNotVerified */ $block->getVar("gallery/fullscreen/navarrows") ? 'true' : 'false' ?>, -- "navtype": "<?= /* @escapeNotVerified */ $block->getVar("gallery/fullscreen/navtype") ?>", -- "arrows": <?= /* @escapeNotVerified */ $block->getVar("gallery/fullscreen/arrows") ? 'true' : 'false' ?>, -- "showCaption": <?= /* @escapeNotVerified */ $block->getVar("gallery/fullscreen/caption") ? 'true' : 'false' ?>, -- <?php if ($block->getVar("gallery/fullscreen/transition/duration")): ?> -- "transitionduration": <?= /* @escapeNotVerified */ $block->getVar("gallery/fullscreen/transition/duration") ?>, -- <?php endif; ?> -- "transition": "<?= /* @escapeNotVerified */ $block->getVar("gallery/fullscreen/transition/effect") ?>" -- }, -- "breakpoints": <?= /* @escapeNotVerified */ $block->getBreakpoints() ?> -+ "magnifierOpts": <?= /* @noEscape */ $block->getMagnifier() ?>, -+ "data": <?= /* @noEscape */ $block->getGalleryImagesJson() ?>, -+ "options": <?= /* @noEscape */ $block->getGalleryOptions()->getOptionsJson() ?>, -+ "fullscreen": <?= /* @noEscape */ $block->getGalleryOptions()->getFSOptionsJson() ?>, -+ "breakpoints": <?= /* @noEscape */ $block->getBreakpoints() ?> - } - } - } -diff --git a/app/code/Magento/Catalog/view/frontend/templates/product/view/mailto.phtml b/app/code/Magento/Catalog/view/frontend/templates/product/view/mailto.phtml -index d52b594eded..f57c9b68ddb 100644 ---- a/app/code/Magento/Catalog/view/frontend/templates/product/view/mailto.phtml -+++ b/app/code/Magento/Catalog/view/frontend/templates/product/view/mailto.phtml -@@ -4,11 +4,10 @@ - * See COPYING.txt for license details. - */ - --// @codingStandardsIgnoreFile -- -+// phpcs:disable Magento2.Templates.ThisInTemplate.FoundThis - ?> - <?php $_product = $block->getProduct() ?> --<?php if ($block->canEmailToFriend()): ?> -- <a href="<?= /* @escapeNotVerified */ $this->helper('Magento\Catalog\Helper\Product')->getEmailToFriendUrl($_product) ?>" -- class="action mailto friend"><span><?= /* @escapeNotVerified */ __('Email') ?></span></a> -+<?php if ($block->canEmailToFriend()) :?> -+ <a href="<?= $block->escapeUrl($this->helper(Magento\Catalog\Helper\Product::class)->getEmailToFriendUrl($_product)) ?>" -+ class="action mailto friend"><span><?= $block->escapeHtml(__('Email')) ?></span></a> - <?php endif; ?> -diff --git a/app/code/Magento/Catalog/view/frontend/templates/product/view/opengraph/currency.phtml b/app/code/Magento/Catalog/view/frontend/templates/product/view/opengraph/currency.phtml -index 87655797f40..7f14b71a60c 100644 ---- a/app/code/Magento/Catalog/view/frontend/templates/product/view/opengraph/currency.phtml -+++ b/app/code/Magento/Catalog/view/frontend/templates/product/view/opengraph/currency.phtml -@@ -4,8 +4,7 @@ - * See COPYING.txt for license details. - */ - --// @codingStandardsIgnoreFile -- - /** @var $block \Magento\Directory\Block\Currency */ - ?> --<meta property="product:price:currency" content="<?= /* @escapeNotVerified */ $block->stripTags($block->getCurrentCurrencyCode()) ?>"/> -+<meta property="product:price:currency" -+ content="<?= /* @noEscape */ $block->stripTags($block->getCurrentCurrencyCode()) ?>"/> -diff --git a/app/code/Magento/Catalog/view/frontend/templates/product/view/opengraph/general.phtml b/app/code/Magento/Catalog/view/frontend/templates/product/view/opengraph/general.phtml -index a2b91a5eeb9..eb2bde647f9 100644 ---- a/app/code/Magento/Catalog/view/frontend/templates/product/view/opengraph/general.phtml -+++ b/app/code/Magento/Catalog/view/frontend/templates/product/view/opengraph/general.phtml -@@ -4,17 +4,18 @@ - * See COPYING.txt for license details. - */ - --// @codingStandardsIgnoreFile -- - /** @var $block \Magento\Catalog\Block\Product\View */ - ?> - - <meta property="og:type" content="product" /> --<meta property="og:title" content="<?= $block->escapeHtmlAttr($block->stripTags($block->getProduct()->getName())) ?>" /> --<meta property="og:image" content="<?= $block->escapeUrl($block->getImage($block->getProduct(), 'product_base_image')->getImageUrl()) ?>" /> --<meta property="og:description" content="<?= $block->escapeHtmlAttr($block->stripTags($block->getProduct()->getShortDescription())) ?>" /> -+<meta property="og:title" -+ content="<?= /* @noEscape */ $block->stripTags($block->getProduct()->getName()) ?>" /> -+<meta property="og:image" -+ content="<?= $block->escapeUrl($block->getImage($block->getProduct(), 'product_base_image')->getImageUrl()) ?>" /> -+<meta property="og:description" -+ content="<?= /* @noEscape */ $block->stripTags($block->getProduct()->getShortDescription()) ?>" /> - <meta property="og:url" content="<?= $block->escapeUrl($block->getProduct()->getProductUrl()) ?>" /> --<?php if ($priceAmount = $block->getProduct()->getFinalPrice()):?> -- <meta property="product:price:amount" content="<?= /* @escapeNotVerified */ $priceAmount ?>"/> -+<?php if ($priceAmount = $block->getProduct()->getPriceInfo()->getPrice(\Magento\Catalog\Pricing\Price\FinalPrice::PRICE_CODE)->getAmount()) :?> -+ <meta property="product:price:amount" content="<?= $block->escapeHtmlAttr($priceAmount) ?>"/> - <?= $block->getChildHtml('meta.currency') ?> - <?php endif;?> -diff --git a/app/code/Magento/Catalog/view/frontend/templates/product/view/options.phtml b/app/code/Magento/Catalog/view/frontend/templates/product/view/options.phtml -index 3ebfa768609..d9a0c845b9f 100644 ---- a/app/code/Magento/Catalog/view/frontend/templates/product/view/options.phtml -+++ b/app/code/Magento/Catalog/view/frontend/templates/product/view/options.phtml -@@ -4,26 +4,24 @@ - * See COPYING.txt for license details. - */ - --// @codingStandardsIgnoreFile -- - /* @var $block \Magento\Catalog\Block\Product\View\Options */ - ?> - - <?php $_options = $block->decorateArray($block->getOptions()) ?> - <?php $_productId = $block->getProduct()->getId() ?> --<?php if (count($_options)):?> -+<?php if (count($_options)) :?> - <script type="text/x-magento-init"> - { - "#product_addtocart_form": { - "priceOptions": { -- "optionConfig": <?= /* @escapeNotVerified */ $block->getJsonConfig() ?>, -+ "optionConfig": <?= /* @noEscape */ $block->getJsonConfig() ?>, - "controlContainer": ".field", - "priceHolderSelector": "[data-product-id='<?= $block->escapeHtml($_productId) ?>'][data-role=priceBox]" - } - } - } - </script> -- <?php foreach ($_options as $_option): ?> -+ <?php foreach ($_options as $_option) :?> - <?= $block->getOptionHtml($_option) ?> - <?php endforeach; ?> - <?php endif; ?> -diff --git a/app/code/Magento/Catalog/view/frontend/templates/product/view/options/type/date.phtml b/app/code/Magento/Catalog/view/frontend/templates/product/view/options/type/date.phtml -index 66895fa1eab..b7cd64277fe 100644 ---- a/app/code/Magento/Catalog/view/frontend/templates/product/view/options/type/date.phtml -+++ b/app/code/Magento/Catalog/view/frontend/templates/product/view/options/type/date.phtml -@@ -3,46 +3,43 @@ - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -- --// @codingStandardsIgnoreFile -- - ?> - <?php /* @var $block \Magento\Catalog\Block\Product\View\Options\Type\Date */ ?> - <?php $_option = $block->getOption() ?> --<?php $_optionId = $_option->getId() ?> -+<?php $_optionId = $block->escapeHtmlAttr($_option->getId()) ?> - <?php $class = ($_option->getIsRequire()) ? ' required' : ''; ?> --<div class="field date<?= /* @escapeNotVerified */ $class ?>" -+<div class="field date<?= /* @noEscape */ $class ?>" - data-mage-init='{"priceOptionDate":{"fromSelector":"#product_addtocart_form"}}'> -- <fieldset class="fieldset fieldset-product-options-inner<?= /* @escapeNotVerified */ $class ?>"> -+ <fieldset class="fieldset fieldset-product-options-inner<?= /* @noEscape */ $class ?>"> - <legend class="legend"> - <span><?= $block->escapeHtml($_option->getTitle()) ?></span> -- <?= /* @escapeNotVerified */ $block->getFormattedPrice() ?> -+ <?= /* @noEscape */ $block->getFormattedPrice() ?> - </legend> - <div class="control"> - <?php if ($_option->getType() == \Magento\Catalog\Api\Data\ProductCustomOptionInterface::OPTION_TYPE_DATE_TIME -- || $_option->getType() == \Magento\Catalog\Api\Data\ProductCustomOptionInterface::OPTION_TYPE_DATE): ?> -+ || $_option->getType() == \Magento\Catalog\Api\Data\ProductCustomOptionInterface::OPTION_TYPE_DATE) :?> - - <?= $block->getDateHtml() ?> - - <?php endif; ?> - - <?php if ($_option->getType() == \Magento\Catalog\Api\Data\ProductCustomOptionInterface::OPTION_TYPE_DATE_TIME -- || $_option->getType() == \Magento\Catalog\Api\Data\ProductCustomOptionInterface::OPTION_TYPE_TIME): ?> -+ || $_option->getType() == \Magento\Catalog\Api\Data\ProductCustomOptionInterface::OPTION_TYPE_TIME) :?> - <?= $block->getTimeHtml() ?> - <?php endif; ?> - -- <?php if ($_option->getIsRequire()): ?> -+ <?php if ($_option->getIsRequire()) :?> - <input type="hidden" -- name="validate_datetime_<?= /* @escapeNotVerified */ $_optionId ?>" -- class="validate-datetime-<?= /* @escapeNotVerified */ $_optionId ?>" -+ name="validate_datetime_<?= /* @noEscape */ $_optionId ?>" -+ class="validate-datetime-<?= /* @noEscape */ $_optionId ?>" - value="" -- data-validate="{'validate-required-datetime':<?= /* @escapeNotVerified */ $_optionId ?>}"/> -- <?php else: ?> -+ data-validate="{'validate-required-datetime':<?= /* @noEscape */ $_optionId ?>}"/> -+ <?php else :?> - <input type="hidden" -- name="validate_datetime_<?= /* @escapeNotVerified */ $_optionId ?>" -- class="validate-datetime-<?= /* @escapeNotVerified */ $_optionId ?>" -+ name="validate_datetime_<?= /* @noEscape */ $_optionId ?>" -+ class="validate-datetime-<?= /* @noEscape */ $_optionId ?>" - value="" -- data-validate="{'validate-optional-datetime':<?= /* @escapeNotVerified */ $_optionId ?>}"/> -+ data-validate="{'validate-optional-datetime':<?= /* @noEscape */ $_optionId ?>}"/> - <?php endif; ?> - <script type="text/x-magento-init"> - { -diff --git a/app/code/Magento/Catalog/view/frontend/templates/product/view/options/type/default.phtml b/app/code/Magento/Catalog/view/frontend/templates/product/view/options/type/default.phtml -index 2006bf6e9f4..c25dab8b70a 100644 ---- a/app/code/Magento/Catalog/view/frontend/templates/product/view/options/type/default.phtml -+++ b/app/code/Magento/Catalog/view/frontend/templates/product/view/options/type/default.phtml -@@ -3,9 +3,6 @@ - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -- --// @codingStandardsIgnoreFile -- - ?> - <?php $_option = $block->getOption() ?> - <div class="field"> -diff --git a/app/code/Magento/Catalog/view/frontend/templates/product/view/options/type/file.phtml b/app/code/Magento/Catalog/view/frontend/templates/product/view/options/type/file.phtml -index adb729c6d86..e83e55ad2a0 100644 ---- a/app/code/Magento/Catalog/view/frontend/templates/product/view/options/type/file.phtml -+++ b/app/code/Magento/Catalog/view/frontend/templates/product/view/options/type/file.phtml -@@ -3,65 +3,62 @@ - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -- --// @codingStandardsIgnoreFile -- - ?> - <?php /* @var $block \Magento\Catalog\Block\Product\View\Options\Type\File */ ?> - <?php $_option = $block->getOption(); ?> - <?php $_fileInfo = $block->getFileInfo(); ?> - <?php $_fileExists = $_fileInfo->hasData(); ?> --<?php $_fileName = 'options_' . $_option->getId() . '_file'; ?> -+<?php $_fileName = 'options_' . $block->escapeHtmlAttr($_option->getId()) . '_file'; ?> - <?php $_fieldNameAction = $_fileName . '_action'; ?> - <?php $_fieldValueAction = $_fileExists ? 'save_old' : 'save_new'; ?> - <?php $_fileNamed = $_fileName . '_name'; ?> - <?php $class = ($_option->getIsRequire()) ? ' required' : ''; ?> - --<div class="field file<?= /* @escapeNotVerified */ $class ?>"> -+<div class="field file<?= /* @noEscape */ $class ?>"> - <label class="label" for="<?= /* @noEscape */ $_fileName ?>" id="<?= /* @noEscape */ $_fileName ?>-label"> - <span><?= $block->escapeHtml($_option->getTitle()) ?></span> -- <?= /* @escapeNotVerified */ $block->getFormattedPrice() ?> -+ <?= /* @noEscape */ $block->getFormattedPrice() ?> - </label> -- <?php if ($_fileExists): ?> -+ <?php if ($_fileExists) :?> - <div class="control"> - <span class="<?= /* @noEscape */ $_fileNamed ?>"><?= $block->escapeHtml($_fileInfo->getTitle()) ?></span> - <a href="javascript:void(0)" class="label" id="change-<?= /* @noEscape */ $_fileName ?>" > -- <?= /* @escapeNotVerified */ __('Change') ?> -+ <?= $block->escapeHtml(__('Change')) ?> - </a> -- <?php if (!$_option->getIsRequire()): ?> -- <input type="checkbox" id="delete-<?= /* @escapeNotVerified */ $_fileName ?>" /> -- <span class="label"><?= /* @escapeNotVerified */ __('Delete') ?></span> -+ <?php if (!$_option->getIsRequire()) :?> -+ <input type="checkbox" id="delete-<?= /* @noEscape */ $_fileName ?>" /> -+ <span class="label"><?= $block->escapeHtml(__('Delete')) ?></span> - <?php endif; ?> - </div> - <?php endif; ?> -- <div class="control" id="input-box-<?= /* @escapeNotVerified */ $_fileName ?>" -+ <div class="control" id="input-box-<?= /* @noEscape */ $_fileName ?>" - data-mage-init='{"priceOptionFile":{ - "fileName":"<?= /* @noEscape */ $_fileName ?>", - "fileNamed":"<?= /* @noEscape */ $_fileNamed ?>", -- "fieldNameAction":"<?= /* @escapeNotVerified */ $_fieldNameAction ?>", -- "changeFileSelector":"#change-<?= /* @escapeNotVerified */ $_fileName ?>", -- "deleteFileSelector":"#delete-<?= /* @escapeNotVerified */ $_fileName ?>"} -+ "fieldNameAction":"<?= /* @noEscape */ $_fieldNameAction ?>", -+ "changeFileSelector":"#change-<?= /* @noEscape */ $_fileName ?>", -+ "deleteFileSelector":"#delete-<?= /* @noEscape */ $_fileName ?>"} - }' - <?= $_fileExists ? 'style="display:none"' : '' ?>> - <input type="file" -- name="<?= /* @escapeNotVerified */ $_fileName ?>" -- id="<?= /* @escapeNotVerified */ $_fileName ?>" -+ name="<?= /* @noEscape */ $_fileName ?>" -+ id="<?= /* @noEscape */ $_fileName ?>" - class="product-custom-option<?= $_option->getIsRequire() ? ' required' : '' ?>" -- <?= $_fileExists ? 'disabled="disabled"' : '' ?> /> -- <input type="hidden" name="<?= /* @escapeNotVerified */ $_fieldNameAction ?>" value="<?= /* @escapeNotVerified */ $_fieldValueAction ?>" /> -- <?php if ($_option->getFileExtension()): ?> -+ <?= $_fileExists ? 'disabled="disabled"' : '' ?> /> -+ <input type="hidden" name="<?= /* @noEscape */ $_fieldNameAction ?>" value="<?= /* @noEscape */ $_fieldValueAction ?>" /> -+ <?php if ($_option->getFileExtension()) :?> - <p class="note"> -- <?= /* @escapeNotVerified */ __('Compatible file extensions to upload') ?>: <strong><?= /* @escapeNotVerified */ $_option->getFileExtension() ?></strong> -+ <?= $block->escapeHtml(__('Compatible file extensions to upload')) ?>: <strong><?= $block->escapeHtml($_option->getFileExtension()) ?></strong> - </p> - <?php endif; ?> -- <?php if ($_option->getImageSizeX() > 0): ?> -+ <?php if ($_option->getImageSizeX() > 0) :?> - <p class="note"> -- <?= /* @escapeNotVerified */ __('Maximum image width') ?>: <strong><?= /* @escapeNotVerified */ $_option->getImageSizeX() ?> <?= /* @escapeNotVerified */ __('px.') ?></strong> -+ <?= $block->escapeHtml(__('Maximum image width')) ?>: <strong><?= (int)$_option->getImageSizeX() ?> <?= $block->escapeHtml(__('px.')) ?></strong> - </p> - <?php endif; ?> -- <?php if ($_option->getImageSizeY() > 0): ?> -+ <?php if ($_option->getImageSizeY() > 0) :?> - <p class="note"> -- <?= /* @escapeNotVerified */ __('Maximum image height') ?>: <strong><?= /* @escapeNotVerified */ $_option->getImageSizeY() ?> <?= /* @escapeNotVerified */ __('px.') ?></strong> -+ <?= $block->escapeHtml(__('Maximum image height')) ?>: <strong><?= (int)$_option->getImageSizeY() ?> <?= $block->escapeHtml(__('px.')) ?></strong> - </p> - <?php endif; ?> - </div> -diff --git a/app/code/Magento/Catalog/view/frontend/templates/product/view/options/type/select.phtml b/app/code/Magento/Catalog/view/frontend/templates/product/view/options/type/select.phtml -index 980b78f917c..c4c1d24423b 100644 ---- a/app/code/Magento/Catalog/view/frontend/templates/product/view/options/type/select.phtml -+++ b/app/code/Magento/Catalog/view/frontend/templates/product/view/options/type/select.phtml -@@ -3,9 +3,6 @@ - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -- --// @codingStandardsIgnoreFile -- - ?> - - <?php /* @var $block \Magento\Catalog\Block\Product\View\Options\Type\Select */ ?> -@@ -13,15 +10,15 @@ - $_option = $block->getOption(); - $class = ($_option->getIsRequire()) ? ' required' : ''; - ?> --<div class="field<?= /* @escapeNotVerified */ $class ?>"> -- <label class="label" for="select_<?= /* @escapeNotVerified */ $_option->getId() ?>"> -+<div class="field<?= /* @noEscape */ $class ?>"> -+ <label class="label" for="select_<?= $block->escapeHtmlAttr($_option->getId()) ?>"> - <span><?= $block->escapeHtml($_option->getTitle()) ?></span> - </label> - <div class="control"> - <?= $block->getValuesHtml() ?> -- <?php if ($_option->getIsRequire()): ?> -- <?php if ($_option->getType() == \Magento\Catalog\Api\Data\ProductCustomOptionInterface::OPTION_TYPE_RADIO || $_option->getType() == \Magento\Catalog\Api\Data\ProductCustomOptionInterface::OPTION_TYPE_CHECKBOX): ?> -- <span id="options-<?= /* @escapeNotVerified */ $_option->getId() ?>-container"></span> -+ <?php if ($_option->getIsRequire()) :?> -+ <?php if ($_option->getType() == \Magento\Catalog\Api\Data\ProductCustomOptionInterface::OPTION_TYPE_RADIO || $_option->getType() == \Magento\Catalog\Api\Data\ProductCustomOptionInterface::OPTION_TYPE_CHECKBOX) :?> -+ <span id="options-<?= $block->escapeHtmlAttr($_option->getId()) ?>-container"></span> - <?php endif; ?> - <?php endif;?> - </div> -diff --git a/app/code/Magento/Catalog/view/frontend/templates/product/view/options/type/text.phtml b/app/code/Magento/Catalog/view/frontend/templates/product/view/options/type/text.phtml -index a04e366a43a..dd4c000d1f3 100644 ---- a/app/code/Magento/Catalog/view/frontend/templates/product/view/options/type/text.phtml -+++ b/app/code/Magento/Catalog/view/frontend/templates/product/view/options/type/text.phtml -@@ -3,9 +3,6 @@ - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -- --// @codingStandardsIgnoreFile -- - ?> - <?php /* @var $block \Magento\Catalog\Block\Product\View\Options\Type\Text */ ?> - <?php -@@ -15,14 +12,14 @@ $class = ($_option->getIsRequire()) ? ' required' : ''; - - <div class="field<?php if ($_option->getType() == \Magento\Catalog\Api\Data\ProductCustomOptionInterface::OPTION_TYPE_AREA) { - echo ' textarea'; --} ?><?= /* @escapeNotVerified */ $class ?>"> -- <label class="label" for="options_<?= /* @escapeNotVerified */ $_option->getId() ?>_text"> -+} ?><?= /* @noEscape */ $class ?>"> -+ <label class="label" for="options_<?= $block->escapeHtmlAttr($_option->getId()) ?>_text"> - <span><?= $block->escapeHtml($_option->getTitle()) ?></span> -- <?= /* @escapeNotVerified */ $block->getFormattedPrice() ?> -+ <?= /* @noEscape */ $block->getFormattedPrice() ?> - </label> - - <div class="control"> -- <?php if ($_option->getType() == \Magento\Catalog\Api\Data\ProductCustomOptionInterface::OPTION_TYPE_FIELD): ?> -+ <?php if ($_option->getType() == \Magento\Catalog\Api\Data\ProductCustomOptionInterface::OPTION_TYPE_FIELD) :?> - <?php $_textValidate = null; - if ($_option->getIsRequire()) { - $_textValidate['required'] = true; -@@ -33,15 +30,15 @@ $class = ($_option->getIsRequire()) ? ' required' : ''; - $_textValidate['validate-no-utf8mb4-characters'] = true; - ?> - <input type="text" -- id="options_<?= /* @escapeNotVerified */ $_option->getId() ?>_text" -+ id="options_<?= $block->escapeHtmlAttr($_option->getId()) ?>_text" - class="input-text product-custom-option" -- <?php if (!empty($_textValidate)) {?> -- data-validate="<?= $block->escapeHtml(json_encode($_textValidate)) ?>" -- <?php } ?> -- name="options[<?= /* @escapeNotVerified */ $_option->getId() ?>]" -- data-selector="options[<?= /* @escapeNotVerified */ $_option->getId() ?>]" -+ <?php if (!empty($_textValidate)) {?> -+ data-validate="<?= $block->escapeHtml(json_encode($_textValidate)) ?>" -+ <?php } ?> -+ name="options[<?= $block->escapeHtmlAttr($_option->getId()) ?>]" -+ data-selector="options[<?= $block->escapeHtmlAttr($_option->getId()) ?>]" - value="<?= $block->escapeHtml($block->getDefaultValue()) ?>"/> -- <?php elseif ($_option->getType() == \Magento\Catalog\Api\Data\ProductCustomOptionInterface::OPTION_TYPE_AREA): ?> -+ <?php elseif ($_option->getType() == \Magento\Catalog\Api\Data\ProductCustomOptionInterface::OPTION_TYPE_AREA) :?> - <?php $_textAreaValidate = null; - if ($_option->getIsRequire()) { - $_textAreaValidate['required'] = true; -@@ -51,31 +48,31 @@ $class = ($_option->getIsRequire()) ? ' required' : ''; - } - $_textAreaValidate['validate-no-utf8mb4-characters'] = true; - ?> -- <textarea id="options_<?= /* @escapeNotVerified */ $_option->getId() ?>_text" -+ <textarea id="options_<?= $block->escapeHtmlAttr($_option->getId()) ?>_text" - class="product-custom-option" - <?php if (!empty($_textAreaValidate)) {?> - data-validate="<?= $block->escapeHtml(json_encode($_textAreaValidate)) ?>" - <?php } ?> -- name="options[<?= /* @escapeNotVerified */ $_option->getId() ?>]" -- data-selector="options[<?= /* @escapeNotVerified */ $_option->getId() ?>]" -+ name="options[<?= $block->escapeHtmlAttr($_option->getId()) ?>]" -+ data-selector="options[<?= $block->escapeHtmlAttr($_option->getId()) ?>]" - rows="5" - cols="25"><?= $block->escapeHtml($block->getDefaultValue()) ?></textarea> - <?php endif; ?> -- <?php if ($_option->getMaxCharacters()): ?> -- <p class="note note_<?= /* @escapeNotVerified */ $_option->getId() ?>"> -- <?= /* @escapeNotVerified */ __('Maximum %1 characters', $_option->getMaxCharacters()) ?> -+ <?php if ($_option->getMaxCharacters()) :?> -+ <p class="note note_<?= $block->escapeHtmlAttr($_option->getId()) ?>"> -+ <?= $block->escapeHtml(__('Maximum %1 characters', $_option->getMaxCharacters())) ?> - <span class="character-counter no-display"></span> - </p> - <?php endif; ?> - </div> -- <?php if ($_option->getMaxCharacters()): ?> -+ <?php if ($_option->getMaxCharacters()) :?> - <script type="text/x-magento-init"> - { -- "[data-selector='options[<?= /* @escapeNotVerified */ $_option->getId() ?>]']": { -+ "[data-selector='options[<?= $block->escapeJs($_option->getId()) ?>]']": { - "Magento_Catalog/js/product/remaining-characters": { -- "maxLength": "<?= /* @escapeNotVerified */ $_option->getMaxCharacters() ?>", -- "noteSelector": ".note_<?= /* @escapeNotVerified */ $_option->getId() ?>", -- "counterSelector": ".note_<?= /* @escapeNotVerified */ $_option->getId() ?> .character-counter" -+ "maxLength": "<?= (int)$_option->getMaxCharacters() ?>", -+ "noteSelector": ".note_<?= $block->escapeJs($_option->getId()) ?>", -+ "counterSelector": ".note_<?= $block->escapeJs($_option->getId()) ?> .character-counter" - } - } - } -diff --git a/app/code/Magento/Catalog/view/frontend/templates/product/view/options/wrapper.phtml b/app/code/Magento/Catalog/view/frontend/templates/product/view/options/wrapper.phtml -index ca6960a215a..88ee45bafe7 100644 ---- a/app/code/Magento/Catalog/view/frontend/templates/product/view/options/wrapper.phtml -+++ b/app/code/Magento/Catalog/view/frontend/templates/product/view/options/wrapper.phtml -@@ -3,14 +3,16 @@ - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -+ -+/** @var $block Magento\Catalog\Block\Product\View */ - ?> - <?php - $required = ''; - if ($block->hasRequiredOptions()) { -- $required = ' data-hasrequired="' . __('* Required Fields') . '"'; -+ $required = ' data-hasrequired="' . $block->escapeHtmlAttr(__('* Required Fields')) . '"'; - } - ?> --<div class="product-options-wrapper" id="product-options-wrapper"<?= /* @escapeNotVerified */ $required ?>> -+<div class="product-options-wrapper" id="product-options-wrapper"<?= /* @noEscape */ $required ?>> - <div class="fieldset" tabindex="0"> - <?= $block->getChildHtml('', true) ?> - </div> -diff --git a/app/code/Magento/Catalog/view/frontend/templates/product/view/price_clone.phtml b/app/code/Magento/Catalog/view/frontend/templates/product/view/price_clone.phtml -index e8c0b32fd76..979bab167c3 100644 ---- a/app/code/Magento/Catalog/view/frontend/templates/product/view/price_clone.phtml -+++ b/app/code/Magento/Catalog/view/frontend/templates/product/view/price_clone.phtml -@@ -3,9 +3,6 @@ - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -- --// @codingStandardsIgnoreFile -- - ?> - <?php /** @var \Magento\Catalog\Block\Product\AbstractProduct $block */ ?> - <?php $_product = $block->getProduct() ?> -diff --git a/app/code/Magento/Catalog/view/frontend/templates/product/view/review.phtml b/app/code/Magento/Catalog/view/frontend/templates/product/view/review.phtml -index 5575d00df74..52506734366 100644 ---- a/app/code/Magento/Catalog/view/frontend/templates/product/view/review.phtml -+++ b/app/code/Magento/Catalog/view/frontend/templates/product/view/review.phtml -@@ -3,9 +3,6 @@ - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -- --// @codingStandardsIgnoreFile -- - ?> - <?php /** @var $block \Magento\Catalog\Block\Product\AbstractProduct */ ?> - <?= $block->getReviewsSummaryHtml($block->getProduct(), false, true) ?> -diff --git a/app/code/Magento/Catalog/view/frontend/templates/product/view/type/default.phtml b/app/code/Magento/Catalog/view/frontend/templates/product/view/type/default.phtml -index 7e522b4f883..30edb2df037 100644 ---- a/app/code/Magento/Catalog/view/frontend/templates/product/view/type/default.phtml -+++ b/app/code/Magento/Catalog/view/frontend/templates/product/view/type/default.phtml -@@ -3,21 +3,18 @@ - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -- --// @codingStandardsIgnoreFile -- - ?> - <?php /* @var $block \Magento\Catalog\Block\Product\View\AbstractView */?> - <?php $_product = $block->getProduct() ?> - --<?php if ($block->displayProductStockStatus()): ?> -- <?php if ($_product->isAvailable()): ?> -- <div class="stock available" title="<?= /* @escapeNotVerified */ __('Availability') ?>"> -- <span><?= /* @escapeNotVerified */ __('In stock') ?></span> -+<?php if ($block->displayProductStockStatus()) :?> -+ <?php if ($_product->isAvailable()) :?> -+ <div class="stock available" title="<?= $block->escapeHtmlAttr(__('Availability')) ?>"> -+ <span><?= $block->escapeHtml(__('In stock')) ?></span> - </div> -- <?php else: ?> -- <div class="stock unavailable" title="<?= /* @escapeNotVerified */ __('Availability') ?>"> -- <span><?= /* @escapeNotVerified */ __('Out of stock') ?></span> -+ <?php else :?> -+ <div class="stock unavailable" title="<?= $block->escapeHtmlAttr(__('Availability')) ?>"> -+ <span><?= $block->escapeHtml(__('Out of stock')) ?></span> - </div> - <?php endif; ?> - <?php endif; ?> -diff --git a/app/code/Magento/Catalog/view/frontend/templates/product/widget/compared/grid.phtml b/app/code/Magento/Catalog/view/frontend/templates/product/widget/compared/grid.phtml -index e0550cc7d44..a2187b685ca 100644 ---- a/app/code/Magento/Catalog/view/frontend/templates/product/widget/compared/grid.phtml -+++ b/app/code/Magento/Catalog/view/frontend/templates/product/widget/compared/grid.phtml -@@ -1,18 +1,16 @@ - <?php - /** -- * Copyright © Magento, Inc. All rights reserved. -+ * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -- -- //@codingStandardsIgnoreFile - ?> - <?php --/** -- * @var $block \Magento\Ui\Block\Wrapper -- */ -+// phpcs:disable Magento2.PHP.ShortEchoSyntax.ShortEchoTag -+ -+/** @var $block \Magento\Ui\Block\Wrapper */ - ?> - --<?php /* @escapeNotVerified */ echo $block->renderApp( -+<?php /* @noEscape */ echo $block->renderApp( - [ - 'widget_columns' => [ - 'displayMode' => 'grid' -diff --git a/app/code/Magento/Catalog/view/frontend/templates/product/widget/compared/list.phtml b/app/code/Magento/Catalog/view/frontend/templates/product/widget/compared/list.phtml -index 3a4f81d946b..5c5f6493c31 100644 ---- a/app/code/Magento/Catalog/view/frontend/templates/product/widget/compared/list.phtml -+++ b/app/code/Magento/Catalog/view/frontend/templates/product/widget/compared/list.phtml -@@ -3,16 +3,14 @@ - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -- -- //@codingStandardsIgnoreFile - ?> - <?php --/** -- * @var $block \Magento\Ui\Block\Wrapper -- */ -+// phpcs:disable Magento2.PHP.ShortEchoSyntax.ShortEchoTag -+ -+/** @var $block \Magento\Ui\Block\Wrapper */ - ?> - --<?php /* @escapeNotVerified */ echo $block->renderApp( -+<?php /* @noEscape */ echo $block->renderApp( - [ - 'widget_columns' => [ - 'displayMode' => 'list' -diff --git a/app/code/Magento/Catalog/view/frontend/templates/product/widget/compared/sidebar.phtml b/app/code/Magento/Catalog/view/frontend/templates/product/widget/compared/sidebar.phtml -index 2d2c91aadd4..8db3cc80368 100644 ---- a/app/code/Magento/Catalog/view/frontend/templates/product/widget/compared/sidebar.phtml -+++ b/app/code/Magento/Catalog/view/frontend/templates/product/widget/compared/sidebar.phtml -@@ -3,16 +3,14 @@ - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -- -- //@codingStandardsIgnoreFile - ?> - <?php --/** -- * @var $block \Magento\Ui\Block\Wrapper -- */ -+// phpcs:disable Magento2.PHP.ShortEchoSyntax.ShortEchoTag -+ -+/** @var $block \Magento\Ui\Block\Wrapper */ - ?> - --<?php /* @escapeNotVerified */ echo $block->renderApp( -+<?php /* @noEscape */ echo $block->renderApp( - [ - 'listing' => [ - 'displayMode' => 'grid' -diff --git a/app/code/Magento/Catalog/view/frontend/templates/product/widget/link/link_block.phtml b/app/code/Magento/Catalog/view/frontend/templates/product/widget/link/link_block.phtml -index 2ec671b8de3..69f0319134e 100644 ---- a/app/code/Magento/Catalog/view/frontend/templates/product/widget/link/link_block.phtml -+++ b/app/code/Magento/Catalog/view/frontend/templates/product/widget/link/link_block.phtml -@@ -3,9 +3,7 @@ - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -- --// @codingStandardsIgnoreFile - ?> - <div class="widget block block-product-link"> -- <a <?= /* @escapeNotVerified */ $block->getLinkAttributes() ?>><span><?= $block->escapeHtml($block->getLabel()) ?></span></a> -+ <a <?= /* @noEscape */ $block->getLinkAttributes() ?>><span><?= $block->escapeHtml($block->getLabel()) ?></span></a> - </div> -diff --git a/app/code/Magento/Catalog/view/frontend/templates/product/widget/link/link_inline.phtml b/app/code/Magento/Catalog/view/frontend/templates/product/widget/link/link_inline.phtml -index 373eda11174..8d9f6500894 100644 ---- a/app/code/Magento/Catalog/view/frontend/templates/product/widget/link/link_inline.phtml -+++ b/app/code/Magento/Catalog/view/frontend/templates/product/widget/link/link_inline.phtml -@@ -3,9 +3,7 @@ - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -- --// @codingStandardsIgnoreFile - ?> - <span class="widget block block-product-link-inline"> -- <a <?= /* @escapeNotVerified */ $block->getLinkAttributes() ?>><span><?= $block->escapeHtml($block->getLabel()) ?></span></a> -+ <a <?= /* @noEscape */ $block->getLinkAttributes() ?>><span><?= $block->escapeHtml($block->getLabel()) ?></span></a> - </span> -diff --git a/app/code/Magento/Catalog/view/frontend/templates/product/widget/new/column/new_default_list.phtml b/app/code/Magento/Catalog/view/frontend/templates/product/widget/new/column/new_default_list.phtml -index a4ae3ba9079..53a0682311b 100644 ---- a/app/code/Magento/Catalog/view/frontend/templates/product/widget/new/column/new_default_list.phtml -+++ b/app/code/Magento/Catalog/view/frontend/templates/product/widget/new/column/new_default_list.phtml -@@ -4,60 +4,61 @@ - * See COPYING.txt for license details. - */ - --// @codingStandardsIgnoreFile -- -+// phpcs:disable Magento2.Templates.ThisInTemplate.FoundThis -+// phpcs:disable Magento2.Files.LineLength.MaxExceeded - ?> --<?php if (($_products = $block->getProductCollection()) && $_products->getSize()): ?> -+<?php if (($_products = $block->getProductCollection()) && $_products->getSize()) :?> - <div class="block widget block-new-products-list"> - <div class="block-title"> -- <strong><?= /* @escapeNotVerified */ __('New Products') ?></strong> -+ <strong><?= $block->escapeHtml(__('New Products')) ?></strong> - </div> - <div class="block-content"> - <?php $suffix = $block->getNameInLayout(); ?> -- <ol class="product-items" id="widget-new-products-<?= /* @escapeNotVerified */ $suffix ?>"> -- <?php foreach ($_products->getItems() as $_product): ?> -+ <ol class="product-items" id="widget-new-products-<?= $block->escapeHtmlAttr($suffix) ?>"> -+ <?php foreach ($_products->getItems() as $_product) :?> - <li class="product-item"> - <div class="product-item-info"> -- <a class="product-item-photo" href="<?= /* @escapeNotVerified */ $_product->getProductUrl() ?>" -- title="<?= /* @escapeNotVerified */ $block->stripTags($_product->getName(), null, true) ?>"> -+ <a class="product-item-photo" href="<?= $block->escapeUrl($_product->getProductUrl()) ?>" -+ title="<?= /* @noEscape */ $block->stripTags($_product->getName(), null, true) ?>"> - <?= $block->getImage($_product, 'side_column_widget_product_thumbnail')->toHtml() ?> - </a> - <div class="product-item-details"> - <strong class="product-item-name"> -- <a href="<?= /* @escapeNotVerified */ $_product->getProductUrl() ?>" -- title="<?= /* @escapeNotVerified */ $block->stripTags($_product->getName(), null, true) ?>)" class="product-item-link"> -- <?= /* @escapeNotVerified */ $this->helper('Magento\Catalog\Helper\Output')->productAttribute($_product, $_product->getName(), 'name') ?> -+ <a href="<?= $block->escapeUrl($_product->getProductUrl()) ?>" -+ title="<?= /* @noEscape */ $block->stripTags($_product->getName(), null, true) ?>)" -+ class="product-item-link"> -+ <?= /* @noEscape */ $this->helper(Magento\Catalog\Helper\Output::class)->productAttribute($_product, $_product->getName(), 'name') ?> - </a> - </strong> -- <?= /* @escapeNotVerified */ $block->getProductPriceHtml($_product, '-widget-new-' . $suffix) ?> -+ <?= $block->getProductPriceHtml($_product, '-widget-new-' . $suffix) ?> - <div class="product-item-actions"> - <div class="actions-primary"> -- <?php if ($_product->isSaleable()): ?> -- <?php if ($_product->getTypeInstance()->hasRequiredOptions($_product)): ?> -- <button type="button" title="<?= /* @escapeNotVerified */ __('Add to Cart') ?>" -+ <?php if ($_product->isSaleable()) :?> -+ <?php if (!$_product->getTypeInstance()->isPossibleBuyFromList($_product)) :?> -+ <button type="button" title="<?= $block->escapeHtmlAttr(__('Add to Cart')) ?>" - class="action tocart primary" -- data-mage-init='{"redirectUrl":{"url":"<?= /* @escapeNotVerified */ $block->getAddToCartUrl($_product) ?>"}}'> -- <span><?= /* @escapeNotVerified */ __('Add to Cart') ?></span> -+ data-mage-init='{"redirectUrl":{"url":"<?= $block->escapeUrl($block->getAddToCartUrl($_product)) ?>"}}'> -+ <span><?= $block->escapeHtml(__('Add to Cart')) ?></span> - </button> -- <?php else: ?> -+ <?php else :?> - <?php -- $postDataHelper = $this->helper('Magento\Framework\Data\Helper\PostHelper'); -- $postData = $postDataHelper->getPostData($block->getAddToCartUrl($_product), ['product' => $_product->getEntityId()]); -- ?> -- <button type="button" title="<?= /* @escapeNotVerified */ __('Add to Cart') ?>" -+ $postDataHelper = $this->helper(Magento\Framework\Data\Helper\PostHelper::class); -+ $postData = $postDataHelper->getPostData($block->escapeUrl($block->getAddToCartUrl($_product)), ['product' => $_product->getEntityId()]); -+ ?> -+ <button type="button" title="<?= $block->escapeHtmlAttr(__('Add to Cart')) ?>" - class="action tocart primary" -- data-post='<?= /* @escapeNotVerified */ $postData ?>'> -- <span><?= /* @escapeNotVerified */ __('Add to Cart') ?></span> -+ data-post='<?= /* @noEscape */ $postData ?>'> -+ <span><?= $block->escapeHtml(__('Add to Cart')) ?></span> - </button> - <?php endif; ?> -- <?php else: ?> -- <?php if ($_product->getIsSalable()): ?> -- <div class="stock available" title="<?= /* @escapeNotVerified */ __('Availability') ?>"> -- <span><?= /* @escapeNotVerified */ __('In stock') ?></span> -+ <?php else :?> -+ <?php if ($_product->getIsSalable()) :?> -+ <div class="stock available" title="<?= $block->escapeHtmlAttr(__('Availability')) ?>"> -+ <span><?= $block->escapeHtml(__('In stock')) ?></span> - </div> -- <?php else: ?> -- <div class="stock unavailable" title="<?= /* @escapeNotVerified */ __('Availability') ?>"> -- <span><?= /* @escapeNotVerified */ __('Out of stock') ?></span> -+ <?php else :?> -+ <div class="stock unavailable" title="<?= $block->escapeHtmlAttr(__('Availability')) ?>"> -+ <span><?= $block->escapeHtml(__('Out of stock')) ?></span> - </div> - <?php endif; ?> - <?php endif; ?> -diff --git a/app/code/Magento/Catalog/view/frontend/templates/product/widget/new/column/new_images_list.phtml b/app/code/Magento/Catalog/view/frontend/templates/product/widget/new/column/new_images_list.phtml -index 2c40f9f7d63..8a776adc950 100644 ---- a/app/code/Magento/Catalog/view/frontend/templates/product/widget/new/column/new_images_list.phtml -+++ b/app/code/Magento/Catalog/view/frontend/templates/product/widget/new/column/new_images_list.phtml -@@ -3,22 +3,20 @@ - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -- --// @codingStandardsIgnoreFile -- - ?> --<?php if (($_products = $block->getProductCollection()) && $_products->getSize()): ?> -+<?php if (($_products = $block->getProductCollection()) && $_products->getSize()) :?> - <div class="block widget block-new-products-images"> - <div class="block-title"> -- <strong><?= /* @escapeNotVerified */ __('New Products') ?></strong> -+ <strong><?= $block->escapeHtml(__('New Products')) ?></strong> - </div> - <div class="block-content"> - <?php $suffix = $block->getNameInLayout(); ?> -- <ol id="widget-new-products-<?= /* @escapeNotVerified */ $suffix ?>" class="product-items product-items-images"> -- <?php foreach ($_products->getItems() as $_product): ?> -+ <ol id="widget-new-products-<?= $block->escapeHtmlAttr($suffix) ?>" -+ class="product-items product-items-images"> -+ <?php foreach ($_products->getItems() as $_product) :?> - <li class="product-item"> -- <a class="product-item-photo" href="<?= /* @escapeNotVerified */ $_product->getProductUrl() ?>" -- title="<?= /* @escapeNotVerified */ $block->stripTags($_product->getName(), null, true) ?>"> -+ <a class="product-item-photo" href="<?= $block->escapeUrl($_product->getProductUrl()) ?>" -+ title="<?= /* @noEscape */ $block->stripTags($_product->getName(), null, true) ?>"> - <?php /* new_products_images_only_widget */ ?> - <?= $block->getImage($_product, 'new_products_images_only_widget')->toHtml() ?> - </a> -diff --git a/app/code/Magento/Catalog/view/frontend/templates/product/widget/new/column/new_names_list.phtml b/app/code/Magento/Catalog/view/frontend/templates/product/widget/new/column/new_names_list.phtml -index c0fb12df911..371d4df7c02 100644 ---- a/app/code/Magento/Catalog/view/frontend/templates/product/widget/new/column/new_names_list.phtml -+++ b/app/code/Magento/Catalog/view/frontend/templates/product/widget/new/column/new_names_list.phtml -@@ -4,24 +4,26 @@ - * See COPYING.txt for license details. - */ - --// @codingStandardsIgnoreFile -- -+// phpcs:disable Magento2.Templates.ThisInTemplate.FoundThis - ?> --<?php if (($_products = $block->getProductCollection()) && $_products->getSize()): ?> -+<?php if (($_products = $block->getProductCollection()) && $_products->getSize()) :?> - <div class="block widget block-new-products-names"> - <div class="block-title"> -- <strong><?= /* @escapeNotVerified */ __('New Products') ?></strong> -+ <strong><?= $block->escapeHtml(__('New Products')) ?></strong> - </div> - <div class="block-content"> - <?php $suffix = $block->getNameInLayout(); ?> -- <ol id="widget-new-products-<?= /* @escapeNotVerified */ $suffix ?>" class="product-items product-items-names"> -- <?php foreach ($_products->getItems() as $_product): ?> -+ <ol id="widget-new-products-<?= $block->escapeHtmlAttr($suffix) ?>" -+ class="product-items product-items-names"> -+ <?php foreach ($_products->getItems() as $_product) :?> - <li class="product-item"> - <strong class="product-item-name"> -- <a href="<?= /* @escapeNotVerified */ $_product->getProductUrl() ?>" -- title="<?= /* @escapeNotVerified */ $block->stripTags($_product->getName(), null, true) ?>)" -+ <a href="<?= $block->escapeUrl($_product->getProductUrl()) ?>" -+ title="<?= /* @noEscape */ $block->stripTags($_product->getName(), null, true) ?>)" - class="product-item-link"> -- <?= /* @escapeNotVerified */ $this->helper('Magento\Catalog\Helper\Output')->productAttribute($_product, $_product->getName(), 'name') ?> -+ <?= /* @noEscape */ $this->helper( -+ Magento\Catalog\Helper\Output::class -+ )->productAttribute($_product, $_product->getName(), 'name') ?> - </a> - </strong> - </li> -diff --git a/app/code/Magento/Catalog/view/frontend/templates/product/widget/new/content/new_grid.phtml b/app/code/Magento/Catalog/view/frontend/templates/product/widget/new/content/new_grid.phtml -index 11bbfea1ac8..5108c488aec 100644 ---- a/app/code/Magento/Catalog/view/frontend/templates/product/widget/new/content/new_grid.phtml -+++ b/app/code/Magento/Catalog/view/frontend/templates/product/widget/new/content/new_grid.phtml -@@ -3,9 +3,6 @@ - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -- --// @codingStandardsIgnoreFile -- - ?> - <?php - /** -@@ -13,6 +10,10 @@ - * - * @var $block \Magento\Catalog\Block\Product\Widget\NewWidget - */ -+ -+// phpcs:disable Magento2.Files.LineLength.MaxExceeded -+// phpcs:disable Magento2.Templates.ThisInTemplate.FoundThis -+ - if ($exist = ($block->getProductCollection() && $block->getProductCollection()->getSize())) { - $type = 'widget-new-grid'; - -@@ -30,84 +31,93 @@ if ($exist = ($block->getProductCollection() && $block->getProductCollection()-> - } - ?> - --<?php if ($exist):?> -- <div class="block widget block-new-products <?= /* @escapeNotVerified */ $mode ?>"> -+<?php if ($exist) :?> -+ <div class="block widget block-new-products <?= /* @noEscape */ $mode ?>"> - <div class="block-title"> -- <strong role="heading" aria-level="2"><?= /* @escapeNotVerified */ $title ?></strong> -+ <strong role="heading" aria-level="2"><?= $block->escapeHtml($title) ?></strong> - </div> - <div class="block-content"> -- <?= /* @escapeNotVerified */ '<!-- ' . $image . '-->' ?> -- <div class="products-<?= /* @escapeNotVerified */ $mode ?> <?= /* @escapeNotVerified */ $mode ?>"> -- <ol class="product-items <?= /* @escapeNotVerified */ $type ?>"> -- <?php foreach ($items as $_item): ?> -+ <?= /* @noEscape */ '<!-- ' . $image . '-->' ?> -+ <div class="products-<?= /* @noEscape */ $mode ?> <?= /* @noEscape */ $mode ?>"> -+ <ol class="product-items <?= /* @noEscape */ $type ?>"> -+ <?php foreach ($items as $_item) :?> - <li class="product-item"> - <div class="product-item-info"> -- <a href="<?= /* @escapeNotVerified */ $block->getProductUrl($_item) ?>" class="product-item-photo"> -+ <a href="<?= $block->escapeUrl($block->getProductUrl($_item)) ?>" -+ class="product-item-photo"> - <?= $block->getImage($_item, $image)->toHtml() ?> - </a> - <div class="product-item-details"> - <strong class="product-item-name"> - <a title="<?= $block->escapeHtml($_item->getName()) ?>" -- href="<?= /* @escapeNotVerified */ $block->getProductUrl($_item) ?>" -+ href="<?= $block->escapeUrl($block->getProductUrl($_item)) ?>" - class="product-item-link"> - <?= $block->escapeHtml($_item->getName()) ?> - </a> - </strong> -- <?php -- echo $block->getProductPriceHtml($_item, $type); -- ?> -+ <?= $block->getProductPriceHtml($_item, $type); ?> - -- <?php if ($templateType): ?> -+ <?php if ($templateType) :?> - <?= $block->getReviewsSummaryHtml($_item, $templateType) ?> - <?php endif; ?> - -- <?php if ($showWishlist || $showCompare || $showCart): ?> -+ <?php if ($showWishlist || $showCompare || $showCart) :?> - <div class="product-item-actions"> -- <?php if ($showCart): ?> -+ <?php if ($showCart) :?> - <div class="actions-primary"> -- <?php if ($_item->isSaleable()): ?> -- <?php if ($_item->getTypeInstance()->hasRequiredOptions($_item)): ?> -+ <?php if ($_item->isSaleable()) :?> -+ <?php if (!$_item->getTypeInstance()->isPossibleBuyFromList($_item)) :?> - <button class="action tocart primary" -- data-mage-init='{"redirectUrl":{"url":"<?= /* @escapeNotVerified */ $block->getAddToCartUrl($_item) ?>"}}' -- type="button" title="<?= /* @escapeNotVerified */ __('Add to Cart') ?>"> -- <span><?= /* @escapeNotVerified */ __('Add to Cart') ?></span> -+ data-mage-init='{"redirectUrl":{"url":"<?= $block->escapeUrl($block->getAddToCartUrl($_item)) ?>"}}' -+ type="button" -+ title="<?= $block->escapeHtmlAttr(__('Add to Cart')) ?>"> -+ <span><?= $block->escapeHtml(__('Add to Cart')) ?></span> - </button> -- <?php else: ?> -+ <?php else :?> - <?php -- $postDataHelper = $this->helper('Magento\Framework\Data\Helper\PostHelper'); -- $postData = $postDataHelper->getPostData($block->getAddToCartUrl($_item), ['product' => $_item->getEntityId()]) -+ $postDataHelper = $this->helper(Magento\Framework\Data\Helper\PostHelper::class); -+ $postData = $postDataHelper->getPostData( -+ $block->escapeUrl($block->getAddToCartUrl($_item)), -+ ['product' => (int) $_item->getEntityId()] -+ ) - ?> - <button class="action tocart primary" -- data-post='<?= /* @escapeNotVerified */ $postData ?>' -- type="button" title="<?= /* @escapeNotVerified */ __('Add to Cart') ?>"> -- <span><?= /* @escapeNotVerified */ __('Add to Cart') ?></span> -+ data-post='<?= /* @noEscape */ $postData ?>' -+ type="button" -+ title="<?= $block->escapeHtmlAttr(__('Add to Cart')) ?>"> -+ <span><?= $block->escapeHtml(__('Add to Cart')) ?></span> - </button> - <?php endif; ?> -- <?php else: ?> -- <?php if ($_item->getIsSalable()): ?> -- <div class="stock available"><span><?= /* @escapeNotVerified */ __('In stock') ?></span></div> -- <?php else: ?> -- <div class="stock unavailable"><span><?= /* @escapeNotVerified */ __('Out of stock') ?></span></div> -+ <?php else :?> -+ <?php if ($_item->getIsSalable()) :?> -+ <div class="stock available"> -+ <span><?= $block->escapeHtml(__('In stock')) ?></span> -+ </div> -+ <?php else :?> -+ <div class="stock unavailable"> -+ <span><?= $block->escapeHtml(__('Out of stock')) ?></span> -+ </div> - <?php endif; ?> - <?php endif; ?> - </div> - <?php endif; ?> -- <?php if ($showWishlist || $showCompare): ?> -+ <?php if ($showWishlist || $showCompare) :?> - <div class="actions-secondary" data-role="add-to-links"> -- <?php if ($this->helper('Magento\Wishlist\Helper\Data')->isAllow() && $showWishlist): ?> -+ <?php if ($this->helper(Magento\Wishlist\Helper\Data::class)->isAllow() && $showWishlist) :?> - <a href="#" -- data-post='<?= /* @escapeNotVerified */ $block->getAddToWishlistParams($_item) ?>' -- class="action towishlist" data-action="add-to-wishlist" -- title="<?= /* @escapeNotVerified */ __('Add to Wish List') ?>"> -- <span><?= /* @escapeNotVerified */ __('Add to Wish List') ?></span> -+ data-post='<?= /* @noEscape */ $block->getAddToWishlistParams($_item) ?>' -+ class="action towishlist" -+ data-action="add-to-wishlist" -+ title="<?= $block->escapeHtmlAttr(__('Add to Wish List')) ?>"> -+ <span><?= $block->escapeHtml(__('Add to Wish List')) ?></span> - </a> - <?php endif; ?> -- <?php if ($block->getAddToCompareUrl() && $showCompare): ?> -- <?php $compareHelper = $this->helper('Magento\Catalog\Helper\Product\Compare');?> -+ <?php if ($block->getAddToCompareUrl() && $showCompare) :?> -+ <?php $compareHelper = $this->helper(Magento\Catalog\Helper\Product\Compare::class);?> - <a href="#" class="action tocompare" -- data-post='<?= /* @escapeNotVerified */ $compareHelper->getPostDataParams($_item) ?>' -- title="<?= /* @escapeNotVerified */ __('Add to Compare') ?>"> -- <span><?= /* @escapeNotVerified */ __('Add to Compare') ?></span> -+ data-post='<?= /* @noEscape */ $compareHelper->getPostDataParams($_item) ?>' -+ title="<?= $block->escapeHtmlAttr(__('Add to Compare')) ?>"> -+ <span><?= $block->escapeHtml(__('Add to Compare')) ?></span> - </a> - <?php endif; ?> - </div> -diff --git a/app/code/Magento/Catalog/view/frontend/templates/product/widget/new/content/new_list.phtml b/app/code/Magento/Catalog/view/frontend/templates/product/widget/new/content/new_list.phtml -index 615cd13fb6d..378cd49493a 100644 ---- a/app/code/Magento/Catalog/view/frontend/templates/product/widget/new/content/new_list.phtml -+++ b/app/code/Magento/Catalog/view/frontend/templates/product/widget/new/content/new_list.phtml -@@ -3,11 +3,12 @@ - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -- --// @codingStandardsIgnoreFile -- - ?> - <?php -+ -+// phpcs:disable Magento2.Templates.ThisInTemplate.FoundThis -+// phpcs:disable Magento2.Files.LineLength.MaxExceeded -+ - /** - * Template for displaying new products widget - * -@@ -21,7 +22,8 @@ if ($exist = ($block->getProductCollection() && $block->getProductCollection()-> - $image = 'new_products_content_widget_list'; - $title = __('New Products'); - $items = $block->getProductCollection()->getItems(); -- $_helper = $this->helper('Magento\Catalog\Helper\Output'); -+ /** @var Magento\Catalog\Helper\Output $_helper */ -+ $_helper = $this->helper(Magento\Catalog\Helper\Output::class); - - $showWishlist = true; - $showCompare = true; -@@ -31,94 +33,102 @@ if ($exist = ($block->getProductCollection() && $block->getProductCollection()-> - } - ?> - --<?php if ($exist):?> -- <div class="block widget block-new-products <?= /* @escapeNotVerified */ $mode ?>"> -+<?php if ($exist) :?> -+ <div class="block widget block-new-products <?= /* @noEscape */ $mode ?>"> - <div class="block-title"> -- <strong role="heading" aria-level="2"><?= /* @escapeNotVerified */ $title ?></strong> -+ <strong role="heading" aria-level="2"><?= $block->escapeHtml($title) ?></strong> - </div> - <div class="block-content"> -- <?= /* @escapeNotVerified */ '<!-- ' . $image . '-->' ?> -- <div class="products-<?= /* @escapeNotVerified */ $mode ?> <?= /* @escapeNotVerified */ $mode ?>"> -- <ol class="product-items <?= /* @escapeNotVerified */ $type ?>"> -- <?php foreach ($items as $_item): ?> -+ <?= /* @noEscape */ '<!-- ' . $image . '-->' ?> -+ <div class="products-<?= /* @noEscape */ $mode ?> <?= /* @noEscape */ $mode ?>"> -+ <ol class="product-items <?= /* @noEscape */ $type ?>"> -+ <?php foreach ($items as $_item) :?> - <li class="product-item"> - <div class="product-item-info"> -- <a href="<?= /* @escapeNotVerified */ $block->getProductUrl($_item) ?>" class="product-item-photo"> -+ <a href="<?= $block->escapeUrl($block->getProductUrl($_item)) ?>" -+ class="product-item-photo"> - <?= $block->getImage($_item, $image)->toHtml() ?> - </a> - <div class="product-item-details"> - <strong class="product-item-name"> -- <a title="<?= $block->escapeHtml($_item->getName()) ?>" -- href="<?= /* @escapeNotVerified */ $block->getProductUrl($_item) ?>" -+ <a title="<?= $block->escapeHtmlAttr($_item->getName()) ?>" -+ href="<?= $block->escapeUrl($block->getProductUrl($_item)) ?>" - class="product-item-link"> - <?= $block->escapeHtml($_item->getName()) ?> - </a> - </strong> - <?= $block->getProductPriceHtml($_item, $type) ?> - -- <?php if ($templateType): ?> -+ <?php if ($templateType) :?> - <?= $block->getReviewsSummaryHtml($_item, $templateType) ?> - <?php endif; ?> - -- <?php if ($showWishlist || $showCompare || $showCart): ?> -+ <?php if ($showWishlist || $showCompare || $showCart) :?> - <div class="product-item-actions"> -- <?php if ($showCart): ?> -+ <?php if ($showCart) :?> - <div class="actions-primary"> -- <?php if ($_item->isSaleable()): ?> -- <?php if ($_item->getTypeInstance()->hasRequiredOptions($_item)): ?> -+ <?php if ($_item->isSaleable()) :?> -+ <?php if (!$_item->getTypeInstance()->isPossibleBuyFromList($_item) -+ ) :?> - <button class="action tocart primary" -- data-mage-init='{"redirectUrl":{"url":"<?= /* @escapeNotVerified */ $block->getAddToCartUrl($_item) ?>"}}' -- type="button" title="<?= /* @escapeNotVerified */ __('Add to Cart') ?>"> -- <span><?= /* @escapeNotVerified */ __('Add to Cart') ?></span> -+ data-mage-init='{"redirectUrl":{"url":"<?= $block->escapeUrl($block->getAddToCartUrl($_item)) ?>"}}' -+ type="button" -+ title="<?= $block->escapeHtmlAttr(__('Add to Cart')) ?>"> -+ <span><?= $block->escapeHtml(__('Add to Cart')) ?></span> - </button> -- <?php else: ?> -+ <?php else :?> - <?php -- $postDataHelper = $this->helper('Magento\Framework\Data\Helper\PostHelper'); -+ $postDataHelper = $this->helper(Magento\Framework\Data\Helper\PostHelper::class); - $postData = $postDataHelper->getPostData($block->getAddToCartUrl($_item), ['product' => $_item->getEntityId()]) - ?> - <button class="action tocart primary" -- data-post='<?= /* @escapeNotVerified */ $postData ?>' -- type="button" title="<?= /* @escapeNotVerified */ __('Add to Cart') ?>"> -- <span><?= /* @escapeNotVerified */ __('Add to Cart') ?></span> -+ data-post='<?= /* @noEscape */ $postData ?>' -+ type="button" title="<?= $block->escapeHtmlAttr(__('Add to Cart')) ?>"> -+ <span><?= $block->escapeHtml(__('Add to Cart')) ?></span> - </button> - <?php endif; ?> -- <?php else: ?> -- <?php if ($_item->getIsSalable()): ?> -- <div class="stock available"><span><?= /* @escapeNotVerified */ __('In stock') ?></span></div> -- <?php else: ?> -- <div class="stock unavailable"><span><?= /* @escapeNotVerified */ __('Out of stock') ?></span></div> -+ <?php else :?> -+ <?php if ($_item->getIsSalable()) :?> -+ <div class="stock available"><span><?= $block->escapeHtml(__('In stock')) ?></span></div> -+ <?php else :?> -+ <div class="stock unavailable"><span><?= $block->escapeHtml(__('Out of stock')) ?></span></div> - <?php endif; ?> - <?php endif; ?> - </div> - <?php endif; ?> -- <?php if ($showWishlist || $showCompare): ?> -+ <?php if ($showWishlist || $showCompare) :?> - <div class="actions-secondary" data-role="add-to-links"> -- <?php if ($this->helper('Magento\Wishlist\Helper\Data')->isAllow() && $showWishlist): ?> -+ <?php if ($this->helper(Magento\Wishlist\Helper\Data::class)->isAllow() && $showWishlist) :?> - <a href="#" -- data-post='<?= /* @escapeNotVerified */ $block->getAddToWishlistParams($_item) ?>' -+ data-post='<?= /* @noEscape */ $block->getAddToWishlistParams($_item) ?>' - class="action towishlist" data-action="add-to-wishlist" -- title="<?= /* @escapeNotVerified */ __('Add to Wish List') ?>"> -- <span><?= /* @escapeNotVerified */ __('Add to Wish List') ?></span> -+ title="<?= $block->escapeHtmlAttr(__('Add to Wish List')) ?>"> -+ <span><?= $block->escapeHtml(__('Add to Wish List')) ?></span> - </a> - <?php endif; ?> -- <?php if ($block->getAddToCompareUrl() && $showCompare): ?> -- <?php $compareHelper = $this->helper('Magento\Catalog\Helper\Product\Compare'); ?> -+ <?php if ($block->getAddToCompareUrl() && $showCompare) :?> -+ <?php $compareHelper = $this->helper(Magento\Catalog\Helper\Product\Compare::class); ?> - <a href="#" class="action tocompare" -- title="<?= /* @escapeNotVerified */ __('Add to Compare') ?>" -- data-post='<?= /* @escapeNotVerified */ $compareHelper->getPostDataParams($_item) ?>'> -- <span><?= /* @escapeNotVerified */ __('Add to Compare') ?></span> -+ title="<?= $block->escapeHtmlAttr(__('Add to Compare')) ?>" -+ data-post='<?= /* @noEscape */ $compareHelper->getPostDataParams($_item) ?>' -+ > -+ <span><?= $block->escapeHtml(__('Add to Compare')) ?></span> - </a> - <?php endif; ?> - </div> - <?php endif; ?> - </div> - <?php endif; ?> -- <?php if ($description):?> -+ <?php if ($description) :?> - <div class="product-item-description"> -- <?= /* @escapeNotVerified */ $_helper->productAttribute($_item, $_item->getShortDescription(), 'short_description') ?> -- <a title="<?= $block->escapeHtml($_item->getName()) ?>" -- href="<?= /* @escapeNotVerified */ $block->getProductUrl($_item) ?>" -- class="action more"><?= /* @escapeNotVerified */ __('Learn More') ?></a> -+ <?= /* @noEscape */ $_helper->productAttribute( -+ $_item, -+ $_item->getShortDescription(), -+ 'short_description' -+ ) ?> -+ <a title="<?= $block->escapeHtmlAttr($_item->getName()) ?>" -+ href="<?= $block->escapeUrl($block->getProductUrl($_item))?>" -+ class="action more"><?= $block->escapeHtml(__('Learn More')) ?></a> - </div> - <?php endif; ?> - </div> -diff --git a/app/code/Magento/Catalog/view/frontend/templates/product/widget/viewed/grid.phtml b/app/code/Magento/Catalog/view/frontend/templates/product/widget/viewed/grid.phtml -index 578630a11e9..d4db174dbe5 100644 ---- a/app/code/Magento/Catalog/view/frontend/templates/product/widget/viewed/grid.phtml -+++ b/app/code/Magento/Catalog/view/frontend/templates/product/widget/viewed/grid.phtml -@@ -5,12 +5,13 @@ - */ - ?> - <?php --/** -- * @var $block \Magento\Ui\Block\Wrapper -- */ -+// phpcs:disable Magento2.Security.LanguageConstruct.DirectOutput -+// phpcs:disable Magento2.PHP.ShortEchoSyntax.ShortEchoTag -+ -+/** @var $block \Magento\Ui\Block\Wrapper */ - ?> - --<?= /* @escapeNotVerified */ $block->renderApp([ -+<?php /* @noEscape */ echo $block->renderApp([ - 'widget_columns' => [ - 'displayMode' => 'grid' - ], -diff --git a/app/code/Magento/Catalog/view/frontend/templates/product/widget/viewed/list.phtml b/app/code/Magento/Catalog/view/frontend/templates/product/widget/viewed/list.phtml -index 3770c330ad7..e03ac9ca692 100644 ---- a/app/code/Magento/Catalog/view/frontend/templates/product/widget/viewed/list.phtml -+++ b/app/code/Magento/Catalog/view/frontend/templates/product/widget/viewed/list.phtml -@@ -5,12 +5,13 @@ - */ - ?> - <?php --/** -- * @var $block \Magento\Ui\Block\Wrapper -- */ -+// phpcs:disable Magento2.Security.LanguageConstruct.DirectOutput -+// phpcs:disable Magento2.PHP.ShortEchoSyntax.ShortEchoTag -+ -+/** @var $block \Magento\Ui\Block\Wrapper */ - ?> - --<?= /* @escapeNotVerified */ $block->renderApp([ -+<?php /* @noEscape */ echo $block->renderApp([ - 'widget_columns' => [ - 'displayMode' => 'list' - ], -diff --git a/app/code/Magento/Catalog/view/frontend/templates/product/widget/viewed/sidebar.phtml b/app/code/Magento/Catalog/view/frontend/templates/product/widget/viewed/sidebar.phtml -index 1c4ad3105a2..a42b46a5238 100644 ---- a/app/code/Magento/Catalog/view/frontend/templates/product/widget/viewed/sidebar.phtml -+++ b/app/code/Magento/Catalog/view/frontend/templates/product/widget/viewed/sidebar.phtml -@@ -3,16 +3,14 @@ - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -- -- //@codingStandardsIgnoreFile - ?> - <?php --/** -- * @var $block \Magento\Ui\Block\Wrapper -- */ -+// phpcs:disable Magento2.PHP.ShortEchoSyntax.ShortEchoTag -+ -+/** @var $block \Magento\Ui\Block\Wrapper */ - ?> - --<?php /* @escapeNotVerified */ echo $block->renderApp( -+<?php /* @noEscape */ echo $block->renderApp( - [ - 'listing' => [ - 'displayMode' => 'grid' -diff --git a/app/code/Magento/Catalog/view/frontend/web/js/catalog-add-to-cart.js b/app/code/Magento/Catalog/view/frontend/web/js/catalog-add-to-cart.js -index 83e91d3c3d4..bcb7c668657 100644 ---- a/app/code/Magento/Catalog/view/frontend/web/js/catalog-add-to-cart.js -+++ b/app/code/Magento/Catalog/view/frontend/web/js/catalog-add-to-cart.js -@@ -6,8 +6,10 @@ - define([ - 'jquery', - 'mage/translate', -+ 'underscore', -+ 'Magento_Catalog/js/product/view/product-ids-resolver', - 'jquery/ui' --], function ($, $t) { -+], function ($, $t, _, idsResolver) { - 'use strict'; - - $.widget('mage.catalogAddToCart', { -@@ -76,17 +78,18 @@ define([ - /** - * Handler for the form 'submit' event - * -- * @param {Object} form -+ * @param {jQuery} form - */ - submitForm: function (form) { - this.ajaxSubmit(form); - }, - - /** -- * @param {String} form -+ * @param {jQuery} form - */ - ajaxSubmit: function (form) { - var self = this, -+ productIds = idsResolver(form), - formData; - - $(self.options.minicartSelector).trigger('contentLoading'); -@@ -115,6 +118,7 @@ define([ - - $(document).trigger('ajax:addToCart', { - 'sku': form.data().productSku, -+ 'productIds': productIds, - 'form': form, - 'response': res - }); -@@ -159,6 +163,23 @@ define([ - .html(res.product.statusText); - } - self.enableAddToCartButton(form); -+ }, -+ -+ /** @inheritdoc */ -+ error: function (res) { -+ $(document).trigger('ajax:addToCart:error', { -+ 'sku': form.data().productSku, -+ 'productIds': productIds, -+ 'form': form, -+ 'response': res -+ }); -+ }, -+ -+ /** @inheritdoc */ -+ complete: function (res) { -+ if (res.state() === 'rejected') { -+ location.reload(); -+ } - } - }); - }, -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 88be03a04e7..b8b6ff65be2 100644 ---- a/app/code/Magento/Catalog/view/frontend/web/js/product/list/toolbar.js -+++ b/app/code/Magento/Catalog/view/frontend/web/js/product/list/toolbar.js -@@ -27,7 +27,9 @@ define([ - directionDefault: 'asc', - orderDefault: 'position', - limitDefault: '9', -- url: '' -+ url: '', -+ formKey: '', -+ post: false - }, - - /** @inheritdoc */ -@@ -89,7 +91,7 @@ define([ - baseUrl = urlPaths[0], - urlParams = urlPaths[1] ? urlPaths[1].split('&') : [], - paramData = {}, -- parameters, i; -+ parameters, i, form, params, key, input, formKey; - - for (i = 0; i < urlParams.length; i++) { - parameters = urlParams[i].split('='); -@@ -99,12 +101,38 @@ define([ - } - paramData[paramName] = paramValue; - -- if (paramValue == defaultValue) { //eslint-disable-line eqeqeq -- delete paramData[paramName]; -- } -- paramData = $.param(paramData); -+ if (this.options.post) { -+ form = document.createElement('form'); -+ params = [this.options.mode, this.options.direction, this.options.order, this.options.limit]; -+ -+ for (key in paramData) { -+ if (params.indexOf(key) !== -1) { //eslint-disable-line max-depth -+ input = document.createElement('input'); -+ input.name = key; -+ input.value = paramData[key]; -+ form.appendChild(input); -+ delete paramData[key]; -+ } -+ } -+ formKey = document.createElement('input'); -+ formKey.name = 'form_key'; -+ formKey.value = this.options.formKey; -+ form.appendChild(formKey); -+ -+ paramData = $.param(paramData); -+ baseUrl += paramData.length ? '?' + paramData : ''; - -- location.href = baseUrl + (paramData.length ? '?' + paramData : ''); -+ form.action = baseUrl; -+ form.method = 'POST'; -+ document.body.appendChild(form); -+ form.submit(); -+ } else { -+ if (paramValue == defaultValue) { //eslint-disable-line eqeqeq -+ delete paramData[paramName]; -+ } -+ paramData = $.param(paramData); -+ location.href = baseUrl + (paramData.length ? '?' + paramData : ''); -+ } - } - }); - -diff --git a/app/code/Magento/Catalog/view/frontend/web/js/product/provider.js b/app/code/Magento/Catalog/view/frontend/web/js/product/provider.js -index c53b2fa6e2a..b29ebe7d57d 100644 ---- a/app/code/Magento/Catalog/view/frontend/web/js/product/provider.js -+++ b/app/code/Magento/Catalog/view/frontend/web/js/product/provider.js -@@ -5,11 +5,13 @@ - - define([ - 'underscore', -+ 'jquery', - 'mageUtils', - 'uiElement', - 'Magento_Catalog/js/product/storage/storage-service', -- 'Magento_Customer/js/customer-data' --], function (_, utils, Element, storage, customerData) { -+ 'Magento_Customer/js/customer-data', -+ 'Magento_Catalog/js/product/view/product-ids-resolver' -+], function (_, $, utils, Element, storage, customerData, productResolver) { - 'use strict'; - - return Element.extend({ -@@ -135,11 +137,16 @@ define([ - */ - filterIds: function (ids) { - var _ids = {}, -- currentTime = new Date().getTime() / 1000; -+ currentTime = new Date().getTime() / 1000, -+ currentProductIds = productResolver($('#product_addtocart_form')); - - _.each(ids, function (id) { -- if (currentTime - id['added_at'] < ~~this.idsStorage.lifetime) { -+ if ( -+ currentTime - id['added_at'] < ~~this.idsStorage.lifetime && -+ !_.contains(currentProductIds, id['product_id']) -+ ) { - _ids[id['product_id']] = id; -+ - } - }, this); - -diff --git a/app/code/Magento/Catalog/view/frontend/web/js/product/storage/data-storage.js b/app/code/Magento/Catalog/view/frontend/web/js/product/storage/data-storage.js -index ab566a70a75..3dc9f3e8445 100644 ---- a/app/code/Magento/Catalog/view/frontend/web/js/product/storage/data-storage.js -+++ b/app/code/Magento/Catalog/view/frontend/web/js/product/storage/data-storage.js -@@ -34,6 +34,21 @@ define([ - }; - } - -+ /** -+ * Set data to localStorage with support check. -+ * -+ * @param {String} namespace -+ * @param {Object} data -+ */ -+ function setLocalStorageItem(namespace, data) { -+ try { -+ window.localStorage.setItem(namespace, JSON.stringify(data)); -+ } catch (e) { -+ console.warn('localStorage is unavailable - skipping local caching of product data'); -+ console.error(e); -+ } -+ } -+ - return { - - /** -@@ -118,7 +133,7 @@ define([ - if (_.isEmpty(data)) { - this.localStorage.removeAll(); - } else { -- this.localStorage.set(data); -+ setLocalStorageItem(this.namespace, data); - } - }, - -diff --git a/app/code/Magento/Catalog/view/frontend/web/js/product/storage/ids-storage.js b/app/code/Magento/Catalog/view/frontend/web/js/product/storage/ids-storage.js -index 7eafbad8299..ec07c19a2c1 100644 ---- a/app/code/Magento/Catalog/view/frontend/web/js/product/storage/ids-storage.js -+++ b/app/code/Magento/Catalog/view/frontend/web/js/product/storage/ids-storage.js -@@ -11,6 +11,21 @@ define([ - ], function ($, _, ko, utils) { - 'use strict'; - -+ /** -+ * Set data to localStorage with support check. -+ * -+ * @param {String} namespace -+ * @param {Object} data -+ */ -+ function setLocalStorageItem(namespace, data) { -+ try { -+ window.localStorage.setItem(namespace, JSON.stringify(data)); -+ } catch (e) { -+ console.warn('localStorage is unavailable - skipping local caching of product data'); -+ console.error(e); -+ } -+ } -+ - return { - - /** -@@ -94,11 +109,7 @@ define([ - * Initializes handler to "data" property update - */ - internalDataHandler: function (data) { -- var localStorage = this.localStorage.get(); -- -- if (!utils.compare(data, localStorage).equal) { -- this.localStorage.set(data); -- } -+ setLocalStorageItem(this.namespace, data); - }, - - /** -diff --git a/app/code/Magento/Catalog/view/frontend/web/js/product/storage/storage-service.js b/app/code/Magento/Catalog/view/frontend/web/js/product/storage/storage-service.js -index e571baa23e4..b35ab867bb0 100644 ---- a/app/code/Magento/Catalog/view/frontend/web/js/product/storage/storage-service.js -+++ b/app/code/Magento/Catalog/view/frontend/web/js/product/storage/storage-service.js -@@ -47,7 +47,7 @@ define([ - * @param {*} data - */ - add: function (data) { -- if (!_.isEmpty(data) && !utils.compare(data, this.data()).equal) { -+ if (!_.isEmpty(data)) { - this.data(_.extend(utils.copy(this.data()), data)); - } - }, -diff --git a/app/code/Magento/Catalog/view/frontend/web/js/product/view/product-ids-resolver.js b/app/code/Magento/Catalog/view/frontend/web/js/product/view/product-ids-resolver.js -new file mode 100644 -index 00000000000..f13e8f84a1b ---- /dev/null -+++ b/app/code/Magento/Catalog/view/frontend/web/js/product/view/product-ids-resolver.js -@@ -0,0 +1,29 @@ -+/** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+define([ -+ 'underscore', -+ 'Magento_Catalog/js/product/view/product-ids' -+], function (_, productIds) { -+ 'use strict'; -+ -+ /** -+ * Returns id's of products in form. -+ * -+ * @param {jQuery} $form -+ * @return {Array} -+ */ -+ return function ($form) { -+ var idSet = productIds(), -+ product = _.findWhere($form.serializeArray(), { -+ name: 'product' -+ }); -+ -+ if (!_.isUndefined(product)) { -+ idSet.push(product.value); -+ } -+ -+ return _.uniq(idSet); -+ }; -+}); -diff --git a/app/code/Magento/Catalog/view/frontend/web/js/product/view/product-ids.js b/app/code/Magento/Catalog/view/frontend/web/js/product/view/product-ids.js -new file mode 100644 -index 00000000000..2198b7b8e48 ---- /dev/null -+++ b/app/code/Magento/Catalog/view/frontend/web/js/product/view/product-ids.js -@@ -0,0 +1,12 @@ -+/** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+ -+define([ -+ 'ko' -+], function (ko) { -+ 'use strict'; -+ -+ return ko.observableArray([]); -+}); -diff --git a/app/code/Magento/Catalog/view/frontend/web/js/related-products.js b/app/code/Magento/Catalog/view/frontend/web/js/related-products.js -index 0c37f9ff4f0..66df48c28bf 100644 ---- a/app/code/Magento/Catalog/view/frontend/web/js/related-products.js -+++ b/app/code/Magento/Catalog/view/frontend/web/js/related-products.js -@@ -17,7 +17,7 @@ define([ - relatedProductsField: '#related-products-field', // Hidden input field that stores related products. - selectAllMessage: $.mage.__('select all'), - unselectAllMessage: $.mage.__('unselect all'), -- selectAllLink: '[role="select-all"]', -+ selectAllLink: '[role="button"]', - elementsSelector: '.item.product' - }, - -diff --git a/app/code/Magento/Catalog/view/frontend/web/js/validate-product.js b/app/code/Magento/Catalog/view/frontend/web/js/validate-product.js -index c0637cb672d..755e777a01f 100644 ---- a/app/code/Magento/Catalog/view/frontend/web/js/validate-product.js -+++ b/app/code/Magento/Catalog/view/frontend/web/js/validate-product.js -@@ -13,7 +13,8 @@ define([ - $.widget('mage.productValidate', { - options: { - bindSubmit: false, -- radioCheckboxClosest: '.nested' -+ radioCheckboxClosest: '.nested', -+ addToCartButtonSelector: '.action.tocart' - }, - - /** -@@ -41,6 +42,7 @@ define([ - return false; - } - }); -+ $(this.options.addToCartButtonSelector).attr('disabled', false); - } - }); - -diff --git a/app/code/Magento/CatalogAnalytics/README.md b/app/code/Magento/CatalogAnalytics/README.md -index df125446117..f93b223c342 100644 ---- a/app/code/Magento/CatalogAnalytics/README.md -+++ b/app/code/Magento/CatalogAnalytics/README.md -@@ -1,3 +1,3 @@ - # Magento_CatalogAnalytics module - --The Magento_CatalogAnalytics module configures data definitions for a data collection related to the Catalog module entities to be used in [Advanced Reporting](http://devdocs.magento.com/guides/v2.2/advanced-reporting/modules.html). -+The Magento_CatalogAnalytics module configures data definitions for a data collection related to the Catalog module entities to be used in [Advanced Reporting](https://devdocs.magento.com/guides/v2.2/advanced-reporting/modules.html). -diff --git a/app/code/Magento/CatalogAnalytics/composer.json b/app/code/Magento/CatalogAnalytics/composer.json -index 5c97261d483..805be8a1776 100644 ---- a/app/code/Magento/CatalogAnalytics/composer.json -+++ b/app/code/Magento/CatalogAnalytics/composer.json -@@ -4,7 +4,8 @@ - "require": { - "php": "~7.1.3||~7.2.0", - "magento/framework": "*", -- "magento/module-catalog": "*" -+ "magento/module-catalog": "*", -+ "magento/module-analytics": "*" - }, - "type": "magento2-module", - "license": [ -diff --git a/app/code/Magento/CatalogGraphQl/Model/AttributesJoiner.php b/app/code/Magento/CatalogGraphQl/Model/AttributesJoiner.php -index ebd8671de02..d57154c4299 100644 ---- a/app/code/Magento/CatalogGraphQl/Model/AttributesJoiner.php -+++ b/app/code/Magento/CatalogGraphQl/Model/AttributesJoiner.php -@@ -15,6 +15,11 @@ use Magento\Eav\Model\Entity\Collection\AbstractCollection; - */ - class AttributesJoiner - { -+ /** -+ * @var array -+ */ -+ private $queryFields = []; -+ - /** - * Join fields attached to field node to collection's select. - * -@@ -24,17 +29,33 @@ class AttributesJoiner - */ - public function join(FieldNode $fieldNode, AbstractCollection $collection) : void - { -- $query = $fieldNode->selectionSet->selections; -- -- /** @var FieldNode $field */ -- foreach ($query as $field) { -- if ($field->kind === 'InlineFragment') { -- continue; -+ foreach ($this->getQueryFields($fieldNode) as $field) { -+ if (!$collection->isAttributeAdded($field)) { -+ $collection->addAttributeToSelect($field); - } -+ } -+ } - -- if (!$collection->isAttributeAdded($field->name->value)) { -- $collection->addAttributeToSelect($field->name->value); -+ /** -+ * Get an array of queried fields. -+ * -+ * @param FieldNode $fieldNode -+ * @return string[] -+ */ -+ public function getQueryFields(FieldNode $fieldNode) -+ { -+ if (!isset($this->queryFields[$fieldNode->name->value])) { -+ $this->queryFields[$fieldNode->name->value] = []; -+ $query = $fieldNode->selectionSet->selections; -+ /** @var FieldNode $field */ -+ foreach ($query as $field) { -+ if ($field->kind === 'InlineFragment') { -+ continue; -+ } -+ $this->queryFields[$fieldNode->name->value][] = $field->name->value; - } - } -+ -+ return $this->queryFields[$fieldNode->name->value]; - } - } -diff --git a/app/code/Magento/CatalogGraphQl/Model/Category/DepthCalculator.php b/app/code/Magento/CatalogGraphQl/Model/Category/DepthCalculator.php -index dbe58a9c77c..b5d02511da4 100644 ---- a/app/code/Magento/CatalogGraphQl/Model/Category/DepthCalculator.php -+++ b/app/code/Magento/CatalogGraphQl/Model/Category/DepthCalculator.php -@@ -26,7 +26,7 @@ class DepthCalculator - $depth = count($selections) ? 1 : 0; - $childrenDepth = [0]; - foreach ($selections as $node) { -- if ($node->kind === 'InlineFragment') { -+ if ($node->kind === 'InlineFragment' || null !== $node->alias) { - continue; - } - -diff --git a/app/code/Magento/CatalogGraphQl/Model/Category/Hydrator.php b/app/code/Magento/CatalogGraphQl/Model/Category/Hydrator.php -index da4ec37c51d..d2c1fc8f7be 100644 ---- a/app/code/Magento/CatalogGraphQl/Model/Category/Hydrator.php -+++ b/app/code/Magento/CatalogGraphQl/Model/Category/Hydrator.php -@@ -8,6 +8,7 @@ declare(strict_types=1); - namespace Magento\CatalogGraphQl\Model\Category; - - use Magento\Catalog\Api\Data\CategoryInterface; -+use Magento\Catalog\Model\Category; - use Magento\CatalogGraphQl\Model\Resolver\Products\DataProvider\CustomAttributesFlattener; - use Magento\Framework\Reflection\DataObjectProcessor; - -@@ -41,16 +42,21 @@ class Hydrator - /** - * Hydrate and flatten category object to flat array - * -- * @param CategoryInterface $category -+ * @param Category $category -+ * @param bool $basicFieldsOnly Set to false to avoid expensive hydration, used for performance optimization - * @return array - */ -- public function hydrateCategory(CategoryInterface $category) : array -+ public function hydrateCategory(Category $category, $basicFieldsOnly = false) : array - { -- $categoryData = $this->dataObjectProcessor->buildOutputDataArray($category, CategoryInterface::class); -+ if ($basicFieldsOnly) { -+ $categoryData = $category->getData(); -+ } else { -+ $categoryData = $this->dataObjectProcessor->buildOutputDataArray($category, CategoryInterface::class); -+ } - $categoryData['id'] = $category->getId(); -- $categoryData['product_count'] = $category->getProductCount(); - $categoryData['children'] = []; - $categoryData['available_sort_by'] = $category->getAvailableSortBy(); -+ $categoryData['model'] = $category; - return $this->flattener->flatten($categoryData); - } - } -diff --git a/app/code/Magento/CatalogGraphQl/Model/Category/LevelCalculator.php b/app/code/Magento/CatalogGraphQl/Model/Category/LevelCalculator.php -index 0401e1c4233..f587be245c9 100644 ---- a/app/code/Magento/CatalogGraphQl/Model/Category/LevelCalculator.php -+++ b/app/code/Magento/CatalogGraphQl/Model/Category/LevelCalculator.php -@@ -15,6 +15,16 @@ use Magento\Catalog\Model\ResourceModel\Category; - */ - class LevelCalculator - { -+ /** -+ * @var ResourceConnection -+ */ -+ private $resourceConnection; -+ -+ /** -+ * @var Category -+ */ -+ private $resourceCategory; -+ - /** - * @param ResourceConnection $resourceConnection - * @param Category $resourceCategory -@@ -39,6 +49,7 @@ class LevelCalculator - $select = $connection->select() - ->from($this->resourceConnection->getTableName('catalog_category_entity'), 'level') - ->where($this->resourceCategory->getLinkField() . " = ?", $rootCategoryId); -+ - return (int) $connection->fetchOne($select); - } - } -diff --git a/app/code/Magento/CatalogGraphQl/Model/LayerFilterItemTypeResolverComposite.php b/app/code/Magento/CatalogGraphQl/Model/LayerFilterItemTypeResolverComposite.php -index 20e55901175..02594ecfaf8 100644 ---- a/app/code/Magento/CatalogGraphQl/Model/LayerFilterItemTypeResolverComposite.php -+++ b/app/code/Magento/CatalogGraphQl/Model/LayerFilterItemTypeResolverComposite.php -@@ -31,7 +31,7 @@ class LayerFilterItemTypeResolverComposite implements TypeResolverInterface - } - - /** -- * {@inheritdoc} -+ * @inheritdoc - */ - public function resolveType(array $data) : string - { -@@ -42,10 +42,6 @@ class LayerFilterItemTypeResolverComposite implements TypeResolverInterface - return $resolvedType; - } - } -- if (empty($resolvedType)) { -- throw new GraphQlInputException( -- __('Concrete type for %1 not implemented', ['ProductLinksInterface']) -- ); -- } -+ throw new GraphQlInputException(__('Cannot resolve layered filter type')); - } - } -diff --git a/app/code/Magento/CatalogGraphQl/Model/Product/Option/DateType.php b/app/code/Magento/CatalogGraphQl/Model/Product/Option/DateType.php -new file mode 100644 -index 00000000000..e1106a3f696 ---- /dev/null -+++ b/app/code/Magento/CatalogGraphQl/Model/Product/Option/DateType.php -@@ -0,0 +1,67 @@ -+<?php -+/** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+declare(strict_types=1); -+ -+namespace Magento\CatalogGraphQl\Model\Product\Option; -+ -+use Magento\Catalog\Model\Product\Option\Type\Date as ProductDateOptionType; -+use Magento\Framework\Exception\LocalizedException; -+use Magento\Framework\Stdlib\DateTime; -+ -+/** -+ * @inheritdoc -+ */ -+class DateType extends ProductDateOptionType -+{ -+ /** -+ * Make valid string as a value of date option type for GraphQl queries -+ * -+ * @param array $values All product option values, i.e. array (option_id => mixed, option_id => mixed...) -+ * @return ProductDateOptionType -+ */ -+ public function validateUserValue($values) -+ { -+ if ($this->_dateExists() || $this->_timeExists()) { -+ return parent::validateUserValue($this->formatValues($values)); -+ } -+ -+ return $this; -+ } -+ -+ /** -+ * Format date value from string to date array -+ * -+ * @param [] $values -+ * @return [] -+ * @throws LocalizedException -+ */ -+ private function formatValues($values) -+ { -+ if (isset($values[$this->getOption()->getId()])) { -+ $value = $values[$this->getOption()->getId()]; -+ $dateTime = \DateTime::createFromFormat(DateTime::DATETIME_PHP_FORMAT, $value); -+ $values[$this->getOption()->getId()] = [ -+ 'date' => $value, -+ 'year' => $dateTime->format('Y'), -+ 'month' => $dateTime->format('m'), -+ 'day' => $dateTime->format('d'), -+ 'hour' => $dateTime->format('H'), -+ 'minute' => $dateTime->format('i'), -+ 'day_part' => $dateTime->format('a'), -+ ]; -+ } -+ -+ return $values; -+ } -+ -+ /** -+ * @inheritdoc -+ */ -+ public function useCalendar() -+ { -+ return false; -+ } -+} -diff --git a/app/code/Magento/CatalogGraphQl/Model/ProductDataProvider.php b/app/code/Magento/CatalogGraphQl/Model/ProductDataProvider.php -new file mode 100644 -index 00000000000..0d38490407e ---- /dev/null -+++ b/app/code/Magento/CatalogGraphQl/Model/ProductDataProvider.php -@@ -0,0 +1,45 @@ -+<?php -+/** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+declare(strict_types=1); -+ -+namespace Magento\CatalogGraphQl\Model; -+ -+use Magento\Catalog\Api\ProductRepositoryInterface; -+ -+/** -+ * Product data provider -+ * -+ * TODO: will be replaces on deferred mechanism -+ */ -+class ProductDataProvider -+{ -+ /** -+ * @var ProductRepositoryInterface -+ */ -+ private $productRepository; -+ -+ /** -+ * @param ProductRepositoryInterface $productRepository -+ */ -+ public function __construct(ProductRepositoryInterface $productRepository) -+ { -+ $this->productRepository = $productRepository; -+ } -+ -+ /** -+ * Get product data by id -+ * -+ * @param int $productId -+ * @return array -+ */ -+ public function getProductDataById(int $productId): array -+ { -+ $product = $this->productRepository->getById($productId); -+ $productData = $product->toArray(); -+ $productData['model'] = $product; -+ return $productData; -+ } -+} -diff --git a/app/code/Magento/CatalogGraphQl/Model/ProductLinkTypeResolverComposite.php b/app/code/Magento/CatalogGraphQl/Model/ProductLinkTypeResolverComposite.php -index 937e3921758..c1bf5c0b7bb 100644 ---- a/app/code/Magento/CatalogGraphQl/Model/ProductLinkTypeResolverComposite.php -+++ b/app/code/Magento/CatalogGraphQl/Model/ProductLinkTypeResolverComposite.php -@@ -11,7 +11,7 @@ use Magento\Framework\GraphQl\Exception\GraphQlInputException; - use Magento\Framework\GraphQl\Query\Resolver\TypeResolverInterface; - - /** -- * {@inheritdoc} -+ * @inheritdoc - */ - class ProductLinkTypeResolverComposite implements TypeResolverInterface - { -@@ -29,8 +29,7 @@ class ProductLinkTypeResolverComposite implements TypeResolverInterface - } - - /** -- * {@inheritdoc} -- * @throws GraphQlInputException -+ * @inheritdoc - */ - public function resolveType(array $data) : string - { -@@ -48,11 +47,6 @@ class ProductLinkTypeResolverComposite implements TypeResolverInterface - return $resolvedType; - } - } -- -- if (!$resolvedType) { -- throw new GraphQlInputException( -- __('Concrete type for %1 not implemented', ['ProductLinksInterface']) -- ); -- } -+ throw new GraphQlInputException(__('Cannot resolve type')); - } - } -diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/Categories.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/Categories.php -index 378e7cb4c36..cb392a7b229 100644 ---- a/app/code/Magento/CatalogGraphQl/Model/Resolver/Categories.php -+++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/Categories.php -@@ -7,6 +7,7 @@ declare(strict_types=1); - - namespace Magento\CatalogGraphQl\Model\Resolver; - -+use Magento\Framework\Exception\LocalizedException; - use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; - use Magento\Catalog\Api\Data\CategoryInterface; - use Magento\Catalog\Model\ResourceModel\Category\Collection; -@@ -15,9 +16,8 @@ use Magento\CatalogGraphQl\Model\AttributesJoiner; - use Magento\CatalogGraphQl\Model\Resolver\Products\DataProvider\CustomAttributesFlattener; - use Magento\Framework\GraphQl\Config\Element\Field; - use Magento\Framework\GraphQl\Query\ResolverInterface; --use Magento\Framework\GraphQl\Query\Resolver\Value; - use Magento\Framework\GraphQl\Query\Resolver\ValueFactory; --use Magento\Framework\Reflection\DataObjectProcessor; -+use Magento\CatalogGraphQl\Model\Category\Hydrator as CategoryHydrator; - - /** - * Resolver for category objects the product is assigned to. -@@ -36,11 +36,6 @@ class Categories implements ResolverInterface - */ - private $categoryIds = []; - -- /** -- * @var DataObjectProcessor -- */ -- private $dataObjectProcessor; -- - /** - * @var AttributesJoiner - */ -@@ -57,33 +52,42 @@ class Categories implements ResolverInterface - private $valueFactory; - - /** -- * Category constructor. -+ * @var CategoryHydrator -+ */ -+ private $categoryHydrator; -+ -+ /** - * @param CollectionFactory $collectionFactory -- * @param DataObjectProcessor $dataObjectProcessor - * @param AttributesJoiner $attributesJoiner - * @param CustomAttributesFlattener $customAttributesFlattener - * @param ValueFactory $valueFactory -+ * @param CategoryHydrator $categoryHydrator - */ - public function __construct( - CollectionFactory $collectionFactory, -- DataObjectProcessor $dataObjectProcessor, - AttributesJoiner $attributesJoiner, - CustomAttributesFlattener $customAttributesFlattener, -- ValueFactory $valueFactory -+ ValueFactory $valueFactory, -+ CategoryHydrator $categoryHydrator - ) { - $this->collection = $collectionFactory->create(); -- $this->dataObjectProcessor = $dataObjectProcessor; - $this->attributesJoiner = $attributesJoiner; - $this->customAttributesFlattener = $customAttributesFlattener; - $this->valueFactory = $valueFactory; -+ $this->categoryHydrator = $categoryHydrator; - } - - /** -- * {@inheritdoc} -+ * @inheritdoc -+ * - * @SuppressWarnings(PHPMD.UnusedFormalParameter) - */ -- public function resolve(Field $field, $context, ResolveInfo $info, array $value = null, array $args = null) : Value -+ public function resolve(Field $field, $context, ResolveInfo $info, array $value = null, array $args = null) - { -+ if (!isset($value['model'])) { -+ throw new LocalizedException(__('"model" value should be specified')); -+ } -+ - /** @var \Magento\Catalog\Model\Product $product */ - $product = $value['model']; - $categoryIds = $product->getCategoryIds(); -@@ -97,19 +101,24 @@ class Categories implements ResolverInterface - } - - if (!$this->collection->isLoaded()) { -- $that->attributesJoiner->join($info->fieldASTs[0], $this->collection); -+ $that->attributesJoiner->join($info->fieldNodes[0], $this->collection); - $this->collection->addIdFilter($this->categoryIds); - } - /** @var CategoryInterface | \Magento\Catalog\Model\Category $item */ - foreach ($this->collection as $item) { - if (in_array($item->getId(), $categoryIds)) { -- $categories[$item->getId()] = $this->dataObjectProcessor->buildOutputDataArray( -- $item, -- CategoryInterface::class -- ); -- $categories[$item->getId()] = $this->customAttributesFlattener -- ->flatten($categories[$item->getId()]); -- $categories[$item->getId()]['product_count'] = $item->getProductCount(); -+ // Try to extract all requested fields from the loaded collection data -+ $categories[$item->getId()] = $this->categoryHydrator->hydrateCategory($item, true); -+ $categories[$item->getId()]['model'] = $item; -+ $requestedFields = $that->attributesJoiner->getQueryFields($info->fieldNodes[0]); -+ $extractedFields = array_keys($categories[$item->getId()]); -+ $foundFields = array_intersect($requestedFields, $extractedFields); -+ if (count($requestedFields) === count($foundFields)) { -+ continue; -+ } -+ -+ // If not all requested fields were extracted from the collection, start more complex extraction -+ $categories[$item->getId()] = $this->categoryHydrator->hydrateCategory($item); - } - } - -diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/Category/Breadcrumbs.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/Category/Breadcrumbs.php -index d49c7f5e567..b93c7e27915 100644 ---- a/app/code/Magento/CatalogGraphQl/Model/Resolver/Category/Breadcrumbs.php -+++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/Category/Breadcrumbs.php -@@ -7,12 +7,11 @@ declare(strict_types=1); - - namespace Magento\CatalogGraphQl\Model\Resolver\Category; - --use \Magento\CatalogGraphQl\Model\Resolver\Category\DataProvider\Breadcrumbs as BreadcrumbsDataProvider; -+use Magento\CatalogGraphQl\Model\Resolver\Category\DataProvider\Breadcrumbs as BreadcrumbsDataProvider; -+use Magento\Framework\Exception\LocalizedException; - use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; - use Magento\Framework\GraphQl\Config\Element\Field; - use Magento\Framework\GraphQl\Query\ResolverInterface; --use Magento\Framework\GraphQl\Query\Resolver\Value; --use Magento\Framework\GraphQl\Query\Resolver\ValueFactory; - - /** - * Retrieves breadcrumbs -@@ -24,39 +23,25 @@ class Breadcrumbs implements ResolverInterface - */ - private $breadcrumbsDataProvider; - -- /** -- * @var ValueFactory -- */ -- private $valueFactory; -- - /** - * @param BreadcrumbsDataProvider $breadcrumbsDataProvider -- * @param ValueFactory $valueFactory - */ - public function __construct( -- BreadcrumbsDataProvider $breadcrumbsDataProvider, -- ValueFactory $valueFactory -+ BreadcrumbsDataProvider $breadcrumbsDataProvider - ) { - $this->breadcrumbsDataProvider = $breadcrumbsDataProvider; -- $this->valueFactory = $valueFactory; - } - - /** - * @inheritdoc - */ -- public function resolve(Field $field, $context, ResolveInfo $info, array $value = null, array $args = null): Value -+ public function resolve(Field $field, $context, ResolveInfo $info, array $value = null, array $args = null) - { - if (!isset($value['path'])) { -- $result = function () { -- return null; -- }; -- return $this->valueFactory->create($result); -+ throw new LocalizedException(__('"path" value should be specified')); - } - -- $result = function () use ($value) { -- $breadcrumbsData = $this->breadcrumbsDataProvider->getData($value['path']); -- return count($breadcrumbsData) ? $breadcrumbsData : null; -- }; -- return $this->valueFactory->create($result); -+ $breadcrumbsData = $this->breadcrumbsDataProvider->getData($value['path']); -+ return count($breadcrumbsData) ? $breadcrumbsData : null; - } - } -diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/Category/CategoriesIdentity.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/Category/CategoriesIdentity.php -new file mode 100644 -index 00000000000..aba2d7b198d ---- /dev/null -+++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/Category/CategoriesIdentity.php -@@ -0,0 +1,33 @@ -+<?php -+/** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+declare(strict_types=1); -+ -+namespace Magento\CatalogGraphQl\Model\Resolver\Category; -+ -+use Magento\Framework\GraphQl\Query\Resolver\IdentityInterface; -+ -+/** -+ * Identity for multiple resolved categories -+ */ -+class CategoriesIdentity implements IdentityInterface -+{ -+ /** -+ * Get category IDs from resolved data -+ * -+ * @param array $resolvedData -+ * @return string[] -+ */ -+ public function getIdentities(array $resolvedData): array -+ { -+ $ids = []; -+ if (!empty($resolvedData)) { -+ foreach ($resolvedData as $category) { -+ $ids[] = $category['id']; -+ } -+ } -+ return $ids; -+ } -+} -diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/Category/CategoryHtmlAttribute.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/Category/CategoryHtmlAttribute.php -new file mode 100644 -index 00000000000..7ccb46c3a29 ---- /dev/null -+++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/Category/CategoryHtmlAttribute.php -@@ -0,0 +1,57 @@ -+<?php -+/** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+declare(strict_types=1); -+ -+namespace Magento\CatalogGraphQl\Model\Resolver\Category; -+ -+use Magento\Catalog\Model\Category; -+use Magento\Framework\Exception\LocalizedException; -+use Magento\Framework\GraphQl\Config\Element\Field; -+use Magento\Framework\GraphQl\Query\ResolverInterface; -+use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; -+use Magento\Catalog\Helper\Output as OutputHelper; -+ -+/** -+ * Resolve rendered content for category attributes where HTML content is allowed -+ */ -+class CategoryHtmlAttribute implements ResolverInterface -+{ -+ /** -+ * @var OutputHelper -+ */ -+ private $outputHelper; -+ -+ /** -+ * @param OutputHelper $outputHelper -+ */ -+ public function __construct( -+ OutputHelper $outputHelper -+ ) { -+ $this->outputHelper = $outputHelper; -+ } -+ -+ /** -+ * @inheritdoc -+ */ -+ public function resolve( -+ Field $field, -+ $context, -+ ResolveInfo $info, -+ array $value = null, -+ array $args = null -+ ) { -+ if (!isset($value['model'])) { -+ throw new LocalizedException(__('"model" value should be specified')); -+ } -+ -+ /* @var $category Category */ -+ $category = $value['model']; -+ $fieldName = $field->getName(); -+ $renderedValue = $this->outputHelper->categoryAttribute($category, $category->getData($fieldName), $fieldName); -+ -+ return $renderedValue; -+ } -+} -diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/Category/CategoryTreeIdentity.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/Category/CategoryTreeIdentity.php -new file mode 100644 -index 00000000000..e4970f08b3e ---- /dev/null -+++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/Category/CategoryTreeIdentity.php -@@ -0,0 +1,27 @@ -+<?php -+/** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+declare(strict_types=1); -+ -+namespace Magento\CatalogGraphQl\Model\Resolver\Category; -+ -+use Magento\Framework\GraphQl\Query\Resolver\IdentityInterface; -+ -+/** -+ * Identity for resolved category -+ */ -+class CategoryTreeIdentity implements IdentityInterface -+{ -+ /** -+ * Get category ID from resolved data -+ * -+ * @param array $resolvedData -+ * @return string[] -+ */ -+ public function getIdentities(array $resolvedData): array -+ { -+ return empty($resolvedData['id']) ? [] : [$resolvedData['id']]; -+ } -+} -diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/Category/CheckCategoryIsActive.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/Category/CheckCategoryIsActive.php -new file mode 100644 -index 00000000000..16f816a9671 ---- /dev/null -+++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/Category/CheckCategoryIsActive.php -@@ -0,0 +1,67 @@ -+<?php -+/** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+declare(strict_types=1); -+ -+namespace Magento\CatalogGraphQl\Model\Resolver\Category; -+ -+use Magento\Catalog\Api\Data\CategoryInterface; -+use Magento\Framework\GraphQl\Exception\GraphQlNoSuchEntityException; -+use Magento\Framework\EntityManager\MetadataPool; -+use Magento\Catalog\Model\ResourceModel\Category\CollectionFactory; -+ -+/** -+ * Check if category is active. -+ */ -+class CheckCategoryIsActive -+{ -+ /** -+ * @var CollectionFactory -+ */ -+ private $collectionFactory; -+ -+ /** -+ * @var MetadataPool -+ */ -+ private $metadata; -+ -+ /** -+ * @param CollectionFactory $collectionFactory -+ * @param MetadataPool $metadata -+ */ -+ public function __construct( -+ CollectionFactory $collectionFactory, -+ MetadataPool $metadata -+ ) { -+ $this->collectionFactory = $collectionFactory; -+ $this->metadata = $metadata; -+ } -+ -+ /** -+ * Check if category is active. -+ * -+ * @param int $categoryId -+ * @throws GraphQlNoSuchEntityException -+ */ -+ public function execute(int $categoryId): void -+ { -+ $collection = $this->collectionFactory->create(); -+ $collection->addAttributeToFilter(CategoryInterface::KEY_IS_ACTIVE, ['eq' => 1]) -+ ->getSelect() -+ ->where( -+ $collection->getSelect() -+ ->getConnection() -+ ->quoteIdentifier( -+ 'e.' . -+ $this->metadata->getMetadata(CategoryInterface::class)->getIdentifierField() -+ ) . ' = ?', -+ $categoryId -+ ); -+ -+ if ($collection->count() === 0) { -+ throw new GraphQlNoSuchEntityException(__('Category doesn\'t exist')); -+ } -+ } -+} -diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/Category/Products.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/Category/Products.php -index 406b4173e68..e0580213dde 100644 ---- a/app/code/Magento/CatalogGraphQl/Model/Resolver/Category/Products.php -+++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/Category/Products.php -@@ -9,8 +9,6 @@ namespace Magento\CatalogGraphQl\Model\Resolver\Category; - - use Magento\Catalog\Api\ProductRepositoryInterface; - use Magento\Framework\GraphQl\Config\Element\Field; --use Magento\Framework\GraphQl\Query\Resolver\Value; --use Magento\Framework\GraphQl\Query\Resolver\ValueFactory; - use Magento\Framework\GraphQl\Query\ResolverInterface; - use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; - use Magento\Framework\GraphQl\Query\Resolver\Argument\SearchCriteria\Builder; -@@ -22,38 +20,38 @@ use Magento\Framework\GraphQl\Exception\GraphQlInputException; - */ - class Products implements ResolverInterface - { -- /** @var \Magento\Catalog\Api\ProductRepositoryInterface */ -+ /** -+ * @var \Magento\Catalog\Api\ProductRepositoryInterface -+ */ - private $productRepository; - -- /** @var Builder */ -+ /** -+ * @var Builder -+ */ - private $searchCriteriaBuilder; - -- /** @var Filter */ -+ /** -+ * @var Filter -+ */ - private $filterQuery; - -- /** @var ValueFactory */ -- private $valueFactory; -- - /** - * @param ProductRepositoryInterface $productRepository - * @param Builder $searchCriteriaBuilder - * @param Filter $filterQuery -- * @param ValueFactory $valueFactory - */ - public function __construct( - ProductRepositoryInterface $productRepository, - Builder $searchCriteriaBuilder, -- Filter $filterQuery, -- ValueFactory $valueFactory -+ Filter $filterQuery - ) { - $this->productRepository = $productRepository; - $this->searchCriteriaBuilder = $searchCriteriaBuilder; - $this->filterQuery = $filterQuery; -- $this->valueFactory = $valueFactory; - } - - /** -- * {@inheritdoc} -+ * @inheritdoc - */ - public function resolve( - Field $field, -@@ -61,13 +59,19 @@ class Products implements ResolverInterface - ResolveInfo $info, - array $value = null, - array $args = null -- ): Value { -+ ) { - $args['filter'] = [ - 'category_id' => [ - 'eq' => $value['id'] - ] - ]; - $searchCriteria = $this->searchCriteriaBuilder->build($field->getName(), $args); -+ if ($args['currentPage'] < 1) { -+ throw new GraphQlInputException(__('currentPage value must be greater than 0.')); -+ } -+ if ($args['pageSize'] < 1) { -+ throw new GraphQlInputException(__('pageSize value must be greater than 0.')); -+ } - $searchCriteria->setCurrentPage($args['currentPage']); - $searchCriteria->setPageSize($args['pageSize']); - $searchResult = $this->filterQuery->getResult($searchCriteria, $info); -@@ -94,14 +98,10 @@ class Products implements ResolverInterface - 'items' => $searchResult->getProductsSearchResult(), - 'page_info' => [ - 'page_size' => $searchCriteria->getPageSize(), -- 'current_page' => $currentPage -+ 'current_page' => $currentPage, -+ 'total_pages' => $maxPages - ] - ]; -- -- $result = function () use ($data) { -- return $data; -- }; -- -- return $this->valueFactory->create($result); -+ return $data; - } - } -diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/Category/ProductsCount.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/Category/ProductsCount.php -new file mode 100644 -index 00000000000..397fd12b7e7 ---- /dev/null -+++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/Category/ProductsCount.php -@@ -0,0 +1,70 @@ -+<?php -+/** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+declare(strict_types=1); -+ -+namespace Magento\CatalogGraphQl\Model\Resolver\Category; -+ -+use Magento\Catalog\Model\Category; -+use Magento\Catalog\Model\Product\Visibility; -+use Magento\CatalogGraphQl\Model\Resolver\Products\DataProvider\Product\CollectionProcessor\StockProcessor; -+use Magento\Framework\Api\SearchCriteriaInterface; -+use Magento\Framework\GraphQl\Exception\GraphQlInputException; -+use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; -+use Magento\Framework\GraphQl\Config\Element\Field; -+use Magento\Framework\GraphQl\Query\ResolverInterface; -+ -+/** -+ * Retrieves products count for a category -+ */ -+class ProductsCount implements ResolverInterface -+{ -+ /** -+ * @var Visibility -+ */ -+ private $catalogProductVisibility; -+ -+ /** -+ * @var StockProcessor -+ */ -+ private $stockProcessor; -+ -+ /** -+ * @var SearchCriteriaInterface -+ */ -+ private $searchCriteria; -+ -+ /** -+ * @param Visibility $catalogProductVisibility -+ * @param SearchCriteriaInterface $searchCriteria -+ * @param StockProcessor $stockProcessor -+ */ -+ public function __construct( -+ Visibility $catalogProductVisibility, -+ SearchCriteriaInterface $searchCriteria, -+ StockProcessor $stockProcessor -+ ) { -+ $this->catalogProductVisibility = $catalogProductVisibility; -+ $this->searchCriteria = $searchCriteria; -+ $this->stockProcessor = $stockProcessor; -+ } -+ -+ /** -+ * @inheritdoc -+ */ -+ public function resolve(Field $field, $context, ResolveInfo $info, array $value = null, array $args = null) -+ { -+ if (!isset($value['model'])) { -+ throw new GraphQlInputException(__('"model" value should be specified')); -+ } -+ /** @var Category $category */ -+ $category = $value['model']; -+ $productsCollection = $category->getProductCollection(); -+ $productsCollection->setVisibility($this->catalogProductVisibility->getVisibleInSiteIds()); -+ $productsCollection = $this->stockProcessor->process($productsCollection, $this->searchCriteria, []); -+ -+ return $productsCollection->getSize(); -+ } -+} -diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/Category/SortFields.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/Category/SortFields.php -index ca68b299101..cb5553bb037 100644 ---- a/app/code/Magento/CatalogGraphQl/Model/Resolver/Category/SortFields.php -+++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/Category/SortFields.php -@@ -9,8 +9,6 @@ namespace Magento\CatalogGraphQl\Model\Resolver\Category; - - use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; - use Magento\Framework\GraphQl\Config\Element\Field; --use Magento\Framework\GraphQl\Query\Resolver\Value; --use Magento\Framework\GraphQl\Query\Resolver\ValueFactory; - use Magento\Framework\GraphQl\Query\ResolverInterface; - - /** -@@ -18,11 +16,6 @@ use Magento\Framework\GraphQl\Query\ResolverInterface; - */ - class SortFields implements ResolverInterface - { -- /** -- * @var ValueFactory -- */ -- private $valueFactory; -- - /** - * @var \Magento\Catalog\Model\Config - */ -@@ -39,27 +32,24 @@ class SortFields implements ResolverInterface - private $sortbyAttributeSource; - - /** -- * @param ValueFactory $valueFactory - * @param \Magento\Catalog\Model\Config $catalogConfig - * @param \Magento\Store\Model\StoreManagerInterface $storeManager - * @oaram \Magento\Catalog\Model\Category\Attribute\Source\Sortby $sortbyAttributeSource - */ - public function __construct( -- ValueFactory $valueFactory, - \Magento\Catalog\Model\Config $catalogConfig, - \Magento\Store\Model\StoreManagerInterface $storeManager, - \Magento\Catalog\Model\Category\Attribute\Source\Sortby $sortbyAttributeSource - ) { -- $this->valueFactory = $valueFactory; - $this->catalogConfig = $catalogConfig; - $this->storeManager = $storeManager; - $this->sortbyAttributeSource = $sortbyAttributeSource; - } - - /** -- * {@inheritDoc} -+ * @inheritdoc - */ -- public function resolve(Field $field, $context, ResolveInfo $info, array $value = null, array $args = null) : Value -+ public function resolve(Field $field, $context, ResolveInfo $info, array $value = null, array $args = null) - { - $sortFieldsOptions = $this->sortbyAttributeSource->getAllOptions(); - array_walk( -@@ -73,10 +63,6 @@ class SortFields implements ResolverInterface - 'options' => $sortFieldsOptions, - ]; - -- $result = function () use ($data) { -- return $data; -- }; -- -- return $this->valueFactory->create($result); -+ return $data; - } - } -diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/CategoryTree.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/CategoryTree.php -index f631e5ff61d..89d3805383e 100644 ---- a/app/code/Magento/CatalogGraphQl/Model/Resolver/CategoryTree.php -+++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/CategoryTree.php -@@ -7,12 +7,14 @@ declare(strict_types=1); - - namespace Magento\CatalogGraphQl\Model\Resolver; - -+use Magento\Catalog\Model\Category; -+use Magento\CatalogGraphQl\Model\Resolver\Category\CheckCategoryIsActive; -+use Magento\CatalogGraphQl\Model\Resolver\Products\DataProvider\ExtractDataFromCategoryTree; - use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; - use Magento\Framework\GraphQl\Config\Element\Field; - use Magento\Framework\GraphQl\Exception\GraphQlInputException; -+use Magento\Framework\GraphQl\Exception\GraphQlNoSuchEntityException; - use Magento\Framework\GraphQl\Query\ResolverInterface; --use Magento\Framework\GraphQl\Query\Resolver\Value; --use Magento\Framework\GraphQl\Query\Resolver\ValueFactory; - - /** - * Category tree field resolver, used for GraphQL request processing. -@@ -25,60 +27,71 @@ class CategoryTree implements ResolverInterface - const CATEGORY_INTERFACE = 'CategoryInterface'; - - /** -- * @var Products\DataProvider\CategoryTree -+ * @var \Magento\CatalogGraphQl\Model\Resolver\Products\DataProvider\CategoryTree - */ - private $categoryTree; - - /** -- * @var ValueFactory -+ * @var ExtractDataFromCategoryTree - */ -- private $valueFactory; -+ private $extractDataFromCategoryTree; - - /** -- * @param Products\DataProvider\CategoryTree $categoryTree -- * @param ValueFactory $valueFactory -+ * @var CheckCategoryIsActive -+ */ -+ private $checkCategoryIsActive; -+ -+ /** -+ * @param \Magento\CatalogGraphQl\Model\Resolver\Products\DataProvider\CategoryTree $categoryTree -+ * @param ExtractDataFromCategoryTree $extractDataFromCategoryTree -+ * @param CheckCategoryIsActive $checkCategoryIsActive - */ - public function __construct( - \Magento\CatalogGraphQl\Model\Resolver\Products\DataProvider\CategoryTree $categoryTree, -- ValueFactory $valueFactory -+ ExtractDataFromCategoryTree $extractDataFromCategoryTree, -+ CheckCategoryIsActive $checkCategoryIsActive - ) { - $this->categoryTree = $categoryTree; -- $this->valueFactory = $valueFactory; -+ $this->extractDataFromCategoryTree = $extractDataFromCategoryTree; -+ $this->checkCategoryIsActive = $checkCategoryIsActive; - } - - /** -- * Assert that filters from search criteria are valid and retrieve root category id -+ * Get category id - * - * @param array $args - * @return int - * @throws GraphQlInputException - */ -- private function assertFiltersAreValidAndGetCategoryRootIds(array $args) : int -+ private function getCategoryId(array $args) : int - { - if (!isset($args['id'])) { - throw new GraphQlInputException(__('"id for category should be specified')); - } - -- return (int) $args['id']; -+ return (int)$args['id']; - } - - /** -- * {@inheritdoc} -+ * @inheritdoc - */ -- public function resolve(Field $field, $context, ResolveInfo $info, array $value = null, array $args = null) : Value -+ public function resolve(Field $field, $context, ResolveInfo $info, array $value = null, array $args = null) - { -- return $this->valueFactory->create(function () use ($value, $args, $field, $info) { -- if (isset($value[$field->getName()])) { -- return $value[$field->getName()]; -- } -+ if (isset($value[$field->getName()])) { -+ return $value[$field->getName()]; -+ } -+ -+ $rootCategoryId = $this->getCategoryId($args); -+ if ($rootCategoryId !== Category::TREE_ROOT_ID) { -+ $this->checkCategoryIsActive->execute($rootCategoryId); -+ } -+ $categoriesTree = $this->categoryTree->getTree($info, $rootCategoryId); -+ -+ if (empty($categoriesTree) || ($categoriesTree->count() == 0)) { -+ throw new GraphQlNoSuchEntityException(__('Category doesn\'t exist')); -+ } - -- $rootCategoryId = $this->assertFiltersAreValidAndGetCategoryRootIds($args); -- $categoriesTree = $this->categoryTree->getTree($info, $rootCategoryId); -- if (!empty($categoriesTree)) { -- return current($categoriesTree); -- } else { -- return null; -- } -- }); -+ $result = $this->extractDataFromCategoryTree->execute($categoriesTree); -+ return current($result); - } - } -diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/LayerFilters.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/LayerFilters.php -new file mode 100644 -index 00000000000..0ec7e12e42d ---- /dev/null -+++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/LayerFilters.php -@@ -0,0 +1,49 @@ -+<?php -+/** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+declare(strict_types=1); -+ -+namespace Magento\CatalogGraphQl\Model\Resolver; -+ -+use Magento\Framework\GraphQl\Config\Element\Field; -+use Magento\Framework\GraphQl\Query\ResolverInterface; -+use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; -+ -+/** -+ * Layered navigation filters resolver, used for GraphQL request processing. -+ */ -+class LayerFilters implements ResolverInterface -+{ -+ /** -+ * @var Layer\DataProvider\Filters -+ */ -+ private $filtersDataProvider; -+ -+ /** -+ * @param \Magento\CatalogGraphQl\Model\Resolver\Layer\DataProvider\Filters $filtersDataProvider -+ */ -+ public function __construct( -+ \Magento\CatalogGraphQl\Model\Resolver\Layer\DataProvider\Filters $filtersDataProvider -+ ) { -+ $this->filtersDataProvider = $filtersDataProvider; -+ } -+ -+ /** -+ * @inheritdoc -+ */ -+ public function resolve( -+ Field $field, -+ $context, -+ ResolveInfo $info, -+ array $value = null, -+ array $args = null -+ ) { -+ if (!isset($value['layer_type'])) { -+ return null; -+ } -+ -+ return $this->filtersDataProvider->getData($value['layer_type']); -+ } -+} -diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/Product.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/Product.php -index 8bf3335bbb9..86137990cc5 100644 ---- a/app/code/Magento/CatalogGraphQl/Model/Resolver/Product.php -+++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/Product.php -@@ -7,17 +7,16 @@ declare(strict_types=1); - - namespace Magento\CatalogGraphQl\Model\Resolver; - --use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; -+use Magento\CatalogGraphQl\Model\Resolver\Product\ProductFieldsSelector; - use Magento\CatalogGraphQl\Model\Resolver\Products\DataProvider\Deferred\Product as ProductDataProvider; - use Magento\Framework\GraphQl\Config\Element\Field; - use Magento\Framework\GraphQl\Exception\GraphQlInputException; --use Magento\Framework\GraphQl\Query\FieldTranslator; --use Magento\Framework\GraphQl\Query\Resolver\Value; - use Magento\Framework\GraphQl\Query\Resolver\ValueFactory; - use Magento\Framework\GraphQl\Query\ResolverInterface; -+use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; - - /** -- * {@inheritdoc} -+ * @inheritdoc - */ - class Product implements ResolverInterface - { -@@ -32,35 +31,35 @@ class Product implements ResolverInterface - private $valueFactory; - - /** -- * @var FieldTranslator -+ * @var ProductFieldsSelector - */ -- private $fieldTranslator; -+ private $productFieldsSelector; - - /** - * @param ProductDataProvider $productDataProvider - * @param ValueFactory $valueFactory -- * @param FieldTranslator $fieldTranslator -+ * @param ProductFieldsSelector $productFieldsSelector - */ - public function __construct( - ProductDataProvider $productDataProvider, - ValueFactory $valueFactory, -- FieldTranslator $fieldTranslator -+ ProductFieldsSelector $productFieldsSelector - ) { - $this->productDataProvider = $productDataProvider; - $this->valueFactory = $valueFactory; -- $this->fieldTranslator = $fieldTranslator; -+ $this->productFieldsSelector = $productFieldsSelector; - } - - /** -- * {@inheritDoc} -+ * @inheritdoc - */ -- public function resolve(Field $field, $context, ResolveInfo $info, array $value = null, array $args = null) : Value -+ public function resolve(Field $field, $context, ResolveInfo $info, array $value = null, array $args = null) - { - if (!isset($value['sku'])) { - throw new GraphQlInputException(__('No child sku found for product link.')); - } - $this->productDataProvider->addProductSku($value['sku']); -- $fields = $this->getProductFields($info); -+ $fields = $this->productFieldsSelector->getProductFieldsFromInfo($info); - $this->productDataProvider->addEavAttributes($fields); - - $result = function () use ($value) { -@@ -86,34 +85,4 @@ class Product implements ResolverInterface - - return $this->valueFactory->create($result); - } -- -- /** -- * Return field names for all requested product fields. -- * -- * @param ResolveInfo $info -- * @return string[] -- */ -- private function getProductFields(ResolveInfo $info) : array -- { -- $fieldNames = []; -- foreach ($info->fieldNodes as $node) { -- if ($node->name->value !== 'product') { -- continue; -- } -- foreach ($node->selectionSet->selections as $selectionNode) { -- if ($selectionNode->kind === 'InlineFragment') { -- foreach ($selectionNode->selectionSet->selections as $inlineSelection) { -- if ($inlineSelection->kind === 'InlineFragment') { -- continue; -- } -- $fieldNames[] = $this->fieldTranslator->translate($inlineSelection->name->value); -- } -- continue; -- } -- $fieldNames[] = $this->fieldTranslator->translate($selectionNode->name->value); -- } -- } -- -- return $fieldNames; -- } - } -diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/CanonicalUrl.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/CanonicalUrl.php -index d2675848c2d..9047eaee4b5 100644 ---- a/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/CanonicalUrl.php -+++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/CanonicalUrl.php -@@ -8,9 +8,8 @@ declare(strict_types=1); - namespace Magento\CatalogGraphQl\Model\Resolver\Product; - - use Magento\Catalog\Model\Product; -+use Magento\Framework\Exception\LocalizedException; - use Magento\Framework\GraphQl\Config\Element\Field; --use Magento\Framework\GraphQl\Query\Resolver\Value; --use Magento\Framework\GraphQl\Query\Resolver\ValueFactory; - use Magento\Framework\GraphQl\Query\ResolverInterface; - use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; - -@@ -20,21 +19,7 @@ use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; - class CanonicalUrl implements ResolverInterface - { - /** -- * @var ValueFactory -- */ -- private $valueFactory; -- -- /** -- * @param ValueFactory $valueFactory -- */ -- public function __construct( -- ValueFactory $valueFactory -- ) { -- $this->valueFactory = $valueFactory; -- } -- -- /** -- * {@inheritdoc} -+ * @inheritdoc - */ - public function resolve( - Field $field, -@@ -42,21 +27,15 @@ class CanonicalUrl implements ResolverInterface - ResolveInfo $info, - array $value = null, - array $args = null -- ): Value { -+ ) { - if (!isset($value['model'])) { -- $result = function () { -- return null; -- }; -- return $this->valueFactory->create($result); -+ throw new LocalizedException(__('"model" value should be specified')); - } - - /* @var $product Product */ - $product = $value['model']; - $url = $product->getUrlModel()->getUrl($product, ['_ignore_category' => true]); -- $result = function () use ($url) { -- return $url; -- }; -- -- return $this->valueFactory->create($result); -+ -+ return $url; - } - } -diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/EntityIdToId.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/EntityIdToId.php -index 4c101f68eb4..ada3caad5f9 100644 ---- a/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/EntityIdToId.php -+++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/EntityIdToId.php -@@ -7,19 +7,18 @@ declare(strict_types=1); - - namespace Magento\CatalogGraphQl\Model\Resolver\Product; - -+use Magento\Framework\Exception\LocalizedException; - use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; - use Magento\Catalog\Api\Data\ProductInterface; - use Magento\Catalog\Model\Product; - use Magento\Framework\EntityManager\MetadataPool; - use Magento\Framework\GraphQl\Config\Element\Field; --use Magento\Framework\GraphQl\Query\Resolver\Value; --use Magento\Framework\GraphQl\Query\Resolver\ValueFactory; - use Magento\Framework\GraphQl\Query\ResolverInterface; - - /** -- * Fixed the id related data in the product data -+ * @inheritdoc - * -- * {@inheritdoc} -+ * Fixed the id related data in the product data - */ - class EntityIdToId implements ResolverInterface - { -@@ -28,23 +27,16 @@ class EntityIdToId implements ResolverInterface - */ - private $metadataPool; - -- /** -- * @var ValueFactory -- */ -- private $valueFactory; -- - /** - * @param MetadataPool $metadataPool -- * @param ValueFactory $valueFactory - */ -- public function __construct(MetadataPool $metadataPool, ValueFactory $valueFactory) -+ public function __construct(MetadataPool $metadataPool) - { - $this->metadataPool = $metadataPool; -- $this->valueFactory = $valueFactory; - } - - /** -- * {@inheritDoc} -+ * @inheritdoc - */ - public function resolve( - Field $field, -@@ -52,12 +44,9 @@ class EntityIdToId implements ResolverInterface - ResolveInfo $info, - array $value = null, - array $args = null -- ): Value { -+ ) { - if (!isset($value['model'])) { -- $result = function () { -- return null; -- }; -- return $this->valueFactory->create($result); -+ throw new LocalizedException(__('"model" value should be specified')); - } - - /** @var Product $product */ -@@ -67,10 +56,6 @@ class EntityIdToId implements ResolverInterface - $this->metadataPool->getMetadata(ProductInterface::class)->getLinkField() - ); - -- $result = function () use ($productId) { -- return $productId; -- }; -- -- return $this->valueFactory->create($result); -+ return $productId; - } - } -diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/Identity.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/Identity.php -new file mode 100644 -index 00000000000..198b1c112dc ---- /dev/null -+++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/Identity.php -@@ -0,0 +1,33 @@ -+<?php -+/** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+declare(strict_types=1); -+ -+namespace Magento\CatalogGraphQl\Model\Resolver\Product; -+ -+use Magento\Framework\GraphQl\Query\Resolver\IdentityInterface; -+ -+/** -+ * Identity for resolved products -+ */ -+class Identity implements IdentityInterface -+{ -+ /** -+ * Get product ids for cache tag -+ * -+ * @param array $resolvedData -+ * @return string[] -+ */ -+ public function getIdentities(array $resolvedData): array -+ { -+ $ids = []; -+ $items = $resolvedData['items'] ?? []; -+ foreach ($items as $item) { -+ $ids[] = $item['entity_id']; -+ } -+ -+ return $ids; -+ } -+} -diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/MediaGalleryEntries.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/MediaGalleryEntries.php -index ac028eef1fb..c8f167da583 100644 ---- a/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/MediaGalleryEntries.php -+++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/MediaGalleryEntries.php -@@ -7,35 +7,32 @@ declare(strict_types=1); - - namespace Magento\CatalogGraphQl\Model\Resolver\Product; - -+use Magento\Framework\Exception\LocalizedException; -+use Magento\Framework\GraphQl\Query\Resolver\ContextInterface; - use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; - use Magento\Catalog\Model\Product; - use Magento\Framework\GraphQl\Config\Element\Field; --use Magento\Framework\GraphQl\Query\Resolver\Value; --use Magento\Framework\GraphQl\Query\Resolver\ValueFactory; - use Magento\Framework\GraphQl\Query\ResolverInterface; - - /** -+ * @inheritdoc -+ * - * Format a product's media gallery information to conform to GraphQL schema representation - */ - class MediaGalleryEntries implements ResolverInterface - { - /** -- * @var ValueFactory -- */ -- private $valueFactory; -- -- /** -- * @param ValueFactory $valueFactory -- */ -- public function __construct(ValueFactory $valueFactory) -- { -- $this->valueFactory = $valueFactory; -- } -- -- /** -+ * @inheritdoc -+ * - * Format product's media gallery entry data to conform to GraphQL schema - * -- * {@inheritdoc} -+ * @param \Magento\Framework\GraphQl\Config\Element\Field $field -+ * @param ContextInterface $context -+ * @param ResolveInfo $info -+ * @param array|null $value -+ * @param array|null $args -+ * @throws \Exception -+ * @return array - */ - public function resolve( - Field $field, -@@ -43,12 +40,9 @@ class MediaGalleryEntries implements ResolverInterface - ResolveInfo $info, - array $value = null, - array $args = null -- ): Value { -+ ) { - if (!isset($value['model'])) { -- $result = function () { -- return null; -- }; -- return $this->valueFactory->create($result); -+ throw new LocalizedException(__('"model" value should be specified')); - } - - /** @var Product $product */ -@@ -64,11 +58,6 @@ class MediaGalleryEntries implements ResolverInterface - } - } - } -- -- $result = function () use ($mediaGalleryEntries) { -- return $mediaGalleryEntries; -- }; -- -- return $this->valueFactory->create($result); -+ return $mediaGalleryEntries; - } - } -diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/NewFromTo.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/NewFromTo.php -index 34fb58b97b1..12016282a30 100644 ---- a/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/NewFromTo.php -+++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/NewFromTo.php -@@ -7,35 +7,31 @@ declare(strict_types=1); - - namespace Magento\CatalogGraphQl\Model\Resolver\Product; - -+use Magento\Framework\Exception\LocalizedException; - use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; - use Magento\Catalog\Model\Product; - use Magento\Framework\GraphQl\Config\Element\Field; --use Magento\Framework\GraphQl\Query\Resolver\Value; --use Magento\Framework\GraphQl\Query\Resolver\ValueFactory; - use Magento\Framework\GraphQl\Query\ResolverInterface; - - /** -+ * @inheritdoc -+ * - * Format the new from and to typo of legacy fields news_from_date and news_to_date - */ - class NewFromTo implements ResolverInterface - { - /** -- * @var ValueFactory -- */ -- private $valueFactory; -- -- /** -- * @param ValueFactory $valueFactory -- */ -- public function __construct(ValueFactory $valueFactory) -- { -- $this->valueFactory = $valueFactory; -- } -- -- /** -+ * @inheritdoc -+ * - * Transfer data from legacy news_from_date and news_to_date to new names corespondent fields - * -- * {@inheritdoc} -+ * @param \Magento\Framework\GraphQl\Config\Element\Field $field -+ * @param \Magento\Framework\GraphQl\Query\Resolver\ContextInterface $context -+ * @param ResolveInfo $info -+ * @param array|null $value -+ * @param array|null $args -+ * @throws \Exception -+ * @return null|array - */ - public function resolve( - Field $field, -@@ -43,12 +39,9 @@ class NewFromTo implements ResolverInterface - ResolveInfo $info, - array $value = null, - array $args = null -- ): Value { -+ ) { - if (!isset($value['model'])) { -- $result = function () { -- return null; -- }; -- return $this->valueFactory->create($result); -+ throw new LocalizedException(__('"model" value should be specified')); - } - - /** @var Product $product */ -@@ -60,10 +53,6 @@ class NewFromTo implements ResolverInterface - $data = $product->getData($attributeName); - } - -- $result = function () use ($data) { -- return $data; -- }; -- -- return $this->valueFactory->create($result); -+ return $data; - } - } -diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/Options.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/Options.php -index 8e06877452f..76602288039 100644 ---- a/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/Options.php -+++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/Options.php -@@ -7,12 +7,12 @@ declare(strict_types=1); - - namespace Magento\CatalogGraphQl\Model\Resolver\Product; - -+use Magento\Framework\Exception\LocalizedException; -+use Magento\Framework\GraphQl\Query\Resolver\ContextInterface; - use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; - use Magento\Catalog\Model\Product; - use Magento\Catalog\Model\Product\Option; - use Magento\Framework\GraphQl\Config\Element\Field; --use Magento\Framework\GraphQl\Query\Resolver\Value; --use Magento\Framework\GraphQl\Query\Resolver\ValueFactory; - use Magento\Framework\GraphQl\Query\ResolverInterface; - - /** -@@ -21,22 +21,17 @@ use Magento\Framework\GraphQl\Query\ResolverInterface; - class Options implements ResolverInterface - { - /** -- * @var ValueFactory -- */ -- private $valueFactory; -- -- /** -- * @param ValueFactory $valueFactory -- */ -- public function __construct(ValueFactory $valueFactory) -- { -- $this->valueFactory = $valueFactory; -- } -- -- /** -+ * @inheritdoc -+ * - * Format product's option data to conform to GraphQL schema - * -- * {@inheritdoc} -+ * @param \Magento\Framework\GraphQl\Config\Element\Field $field -+ * @param ContextInterface $context -+ * @param ResolveInfo $info -+ * @param array|null $value -+ * @param array|null $args -+ * @throws \Exception -+ * @return null|array - */ - public function resolve( - Field $field, -@@ -44,12 +39,9 @@ class Options implements ResolverInterface - ResolveInfo $info, - array $value = null, - array $args = null -- ): Value { -+ ) { - if (!isset($value['model'])) { -- $result = function () { -- return null; -- }; -- return $this->valueFactory->create($result); -+ throw new LocalizedException(__('"model" value should be specified')); - } - - /** @var Product $product */ -@@ -80,10 +72,6 @@ class Options implements ResolverInterface - } - } - -- $result = function () use ($options) { -- return $options; -- }; -- -- return $this->valueFactory->create($result); -+ return $options; - } - } -diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/Price.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/Price.php -index 29b693abc26..55d930101fb 100644 ---- a/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/Price.php -+++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/Price.php -@@ -7,13 +7,13 @@ declare(strict_types=1); - - namespace Magento\CatalogGraphQl\Model\Resolver\Product; - -+use Magento\Framework\Exception\LocalizedException; -+use Magento\Framework\GraphQl\Query\Resolver\ContextInterface; - use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; - use Magento\Catalog\Model\Product; - use Magento\Catalog\Pricing\Price\FinalPrice; - use Magento\Catalog\Pricing\Price\RegularPrice; - use Magento\Framework\GraphQl\Config\Element\Field; --use Magento\Framework\GraphQl\Query\Resolver\Value; --use Magento\Framework\GraphQl\Query\Resolver\ValueFactory; - use Magento\Framework\GraphQl\Query\ResolverInterface; - use Magento\Framework\Pricing\Adjustment\AdjustmentInterface; - use Magento\Framework\Pricing\Amount\AmountInterface; -@@ -35,30 +35,30 @@ class Price implements ResolverInterface - */ - private $priceInfoFactory; - -- /** -- * @var ValueFactory -- */ -- private $valueFactory; -- - /** - * @param StoreManagerInterface $storeManager - * @param PriceInfoFactory $priceInfoFactory -- * @param ValueFactory $valueFactory - */ - public function __construct( - StoreManagerInterface $storeManager, -- PriceInfoFactory $priceInfoFactory, -- ValueFactory $valueFactory -+ PriceInfoFactory $priceInfoFactory - ) { - $this->storeManager = $storeManager; - $this->priceInfoFactory = $priceInfoFactory; -- $this->valueFactory = $valueFactory; - } - - /** -+ * @inheritdoc -+ * - * Format product's tier price data to conform to GraphQL schema - * -- * {@inheritdoc} -+ * @param \Magento\Framework\GraphQl\Config\Element\Field $field -+ * @param ContextInterface $context -+ * @param ResolveInfo $info -+ * @param array|null $value -+ * @param array|null $args -+ * @throws \Exception -+ * @return array - */ - public function resolve( - Field $field, -@@ -66,12 +66,9 @@ class Price implements ResolverInterface - ResolveInfo $info, - array $value = null, - array $args = null -- ): Value { -+ ) { - if (!isset($value['model'])) { -- $result = function () { -- return null; -- }; -- return $this->valueFactory->create($result); -+ throw new LocalizedException(__('"model" value should be specified')); - } - - /** @var Product $product */ -@@ -90,11 +87,7 @@ class Price implements ResolverInterface - 'maximalPrice' => $this->createAdjustmentsArray($priceInfo->getAdjustments(), $maximalPriceAmount) - ]; - -- $result = function () use ($prices) { -- return $prices; -- }; -- -- return $this->valueFactory->create($result); -+ return $prices; - } - - /** -diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/ProductComplexTextAttribute.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/ProductComplexTextAttribute.php -new file mode 100644 -index 00000000000..2573e92e564 ---- /dev/null -+++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/ProductComplexTextAttribute.php -@@ -0,0 +1,57 @@ -+<?php -+/** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+declare(strict_types=1); -+ -+namespace Magento\CatalogGraphQl\Model\Resolver\Product; -+ -+use Magento\Framework\Exception\LocalizedException; -+use Magento\Framework\GraphQl\Config\Element\Field; -+use Magento\Framework\GraphQl\Query\ResolverInterface; -+use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; -+use Magento\Catalog\Model\Product; -+use Magento\Catalog\Helper\Output as OutputHelper; -+ -+/** -+ * Resolve rendered content for attributes where HTML content is allowed -+ */ -+class ProductComplexTextAttribute implements ResolverInterface -+{ -+ /** -+ * @var OutputHelper -+ */ -+ private $outputHelper; -+ -+ /** -+ * @param OutputHelper $outputHelper -+ */ -+ public function __construct( -+ OutputHelper $outputHelper -+ ) { -+ $this->outputHelper = $outputHelper; -+ } -+ -+ /** -+ * @inheritdoc -+ */ -+ public function resolve( -+ Field $field, -+ $context, -+ ResolveInfo $info, -+ array $value = null, -+ array $args = null -+ ): array { -+ if (!isset($value['model'])) { -+ throw new LocalizedException(__('"model" value should be specified')); -+ } -+ -+ /* @var $product Product */ -+ $product = $value['model']; -+ $fieldName = $field->getName(); -+ $renderedValue = $this->outputHelper->productAttribute($product, $product->getData($fieldName), $fieldName); -+ -+ return ['html' => $renderedValue ?? '']; -+ } -+} -diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/ProductFieldsSelector.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/ProductFieldsSelector.php -new file mode 100644 -index 00000000000..9ddad4e6451 ---- /dev/null -+++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/ProductFieldsSelector.php -@@ -0,0 +1,61 @@ -+<?php -+/** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+declare(strict_types=1); -+ -+namespace Magento\CatalogGraphQl\Model\Resolver\Product; -+ -+use Magento\Framework\GraphQl\Query\FieldTranslator; -+use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; -+ -+/** -+ * Select Product Fields From Resolve Info -+ */ -+class ProductFieldsSelector -+{ -+ /** -+ * @var FieldTranslator -+ */ -+ private $fieldTranslator; -+ -+ /** -+ * @param FieldTranslator $fieldTranslator -+ */ -+ public function __construct(FieldTranslator $fieldTranslator) -+ { -+ $this->fieldTranslator = $fieldTranslator; -+ } -+ -+ /** -+ * Return field names for all requested product fields. -+ * -+ * @param ResolveInfo $info -+ * @param string $productNodeName -+ * @return string[] -+ */ -+ public function getProductFieldsFromInfo(ResolveInfo $info, string $productNodeName = 'product') : array -+ { -+ $fieldNames = []; -+ foreach ($info->fieldNodes as $node) { -+ if ($node->name->value !== $productNodeName) { -+ continue; -+ } -+ foreach ($node->selectionSet->selections as $selectionNode) { -+ if ($selectionNode->kind === 'InlineFragment') { -+ foreach ($selectionNode->selectionSet->selections as $inlineSelection) { -+ if ($inlineSelection->kind === 'InlineFragment') { -+ continue; -+ } -+ $fieldNames[] = $this->fieldTranslator->translate($inlineSelection->name->value); -+ } -+ continue; -+ } -+ $fieldNames[] = $this->fieldTranslator->translate($selectionNode->name->value); -+ } -+ } -+ -+ return $fieldNames; -+ } -+} -diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/ProductImage.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/ProductImage.php -new file mode 100644 -index 00000000000..d1566162472 ---- /dev/null -+++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/ProductImage.php -@@ -0,0 +1,44 @@ -+<?php -+/** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+declare(strict_types=1); -+ -+namespace Magento\CatalogGraphQl\Model\Resolver\Product; -+ -+use Magento\Catalog\Model\Product; -+use Magento\Framework\Exception\LocalizedException; -+use Magento\Framework\GraphQl\Config\Element\Field; -+use Magento\Framework\GraphQl\Query\ResolverInterface; -+use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; -+ -+/** -+ * Returns product's image data -+ */ -+class ProductImage implements ResolverInterface -+{ -+ /** -+ * @inheritdoc -+ */ -+ public function resolve( -+ Field $field, -+ $context, -+ ResolveInfo $info, -+ array $value = null, -+ array $args = null -+ ): array { -+ if (!isset($value['model'])) { -+ throw new LocalizedException(__('"model" value should be specified')); -+ } -+ -+ /** @var Product $product */ -+ $product = $value['model']; -+ $imageType = $field->getName(); -+ -+ return [ -+ 'model' => $product, -+ 'image_type' => $imageType, -+ ]; -+ } -+} -diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/ProductImage/Label.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/ProductImage/Label.php -new file mode 100644 -index 00000000000..f971e357426 ---- /dev/null -+++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/ProductImage/Label.php -@@ -0,0 +1,96 @@ -+<?php -+/** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+declare(strict_types=1); -+ -+namespace Magento\CatalogGraphQl\Model\Resolver\Product\ProductImage; -+ -+use Magento\Catalog\Model\Product; -+use Magento\Catalog\Model\ResourceModel\Product as ProductResourceModel; -+use Magento\Framework\Exception\LocalizedException; -+use Magento\Framework\GraphQl\Config\Element\Field; -+use Magento\Framework\GraphQl\Query\ResolverInterface; -+use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; -+use Magento\Store\Model\StoreManagerInterface; -+ -+/** -+ * Returns product's image label -+ */ -+class Label implements ResolverInterface -+{ -+ /** -+ * @var ProductResourceModel -+ */ -+ private $productResource; -+ -+ /** -+ * @var StoreManagerInterface -+ */ -+ private $storeManager; -+ -+ /** -+ * @param ProductResourceModel $productResource -+ * @param StoreManagerInterface $storeManager -+ */ -+ public function __construct( -+ ProductResourceModel $productResource, -+ StoreManagerInterface $storeManager -+ ) { -+ $this->productResource = $productResource; -+ $this->storeManager = $storeManager; -+ } -+ -+ /** -+ * @inheritdoc -+ */ -+ public function resolve( -+ Field $field, -+ $context, -+ ResolveInfo $info, -+ array $value = null, -+ array $args = null -+ ) { -+ if (!isset($value['image_type'])) { -+ throw new LocalizedException(__('"image_type" value should be specified')); -+ } -+ -+ if (!isset($value['model'])) { -+ throw new LocalizedException(__('"model" value should be specified')); -+ } -+ -+ /** @var Product $product */ -+ $product = $value['model']; -+ $imageType = $value['image_type']; -+ $imagePath = $product->getData($imageType); -+ $productId = (int)$product->getEntityId(); -+ -+ // null if image is not set -+ if (null === $imagePath) { -+ return $this->getAttributeValue($productId, 'name'); -+ } -+ -+ $imageLabel = $this->getAttributeValue($productId, $imageType . '_label'); -+ if (null === $imageLabel) { -+ $imageLabel = $this->getAttributeValue($productId, 'name'); -+ } -+ -+ return $imageLabel; -+ } -+ -+ /** -+ * Get attribute value -+ * -+ * @param int $productId -+ * @param string $attributeCode -+ * @return null|string Null if attribute value is not exists -+ */ -+ private function getAttributeValue(int $productId, string $attributeCode): ?string -+ { -+ $storeId = $this->storeManager->getStore()->getId(); -+ -+ $value = $this->productResource->getAttributeRawValue($productId, $attributeCode, $storeId); -+ return is_array($value) && empty($value) ? null : $value; -+ } -+} -diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/ProductImage/Url.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/ProductImage/Url.php -new file mode 100644 -index 00000000000..23a8c2d15c0 ---- /dev/null -+++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/ProductImage/Url.php -@@ -0,0 +1,89 @@ -+<?php -+/** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+declare(strict_types=1); -+ -+namespace Magento\CatalogGraphQl\Model\Resolver\Product\ProductImage; -+ -+use Magento\Catalog\Model\Product; -+use Magento\Catalog\Model\Product\ImageFactory; -+use Magento\CatalogGraphQl\Model\Resolver\Products\DataProvider\Image\Placeholder as PlaceholderProvider; -+use Magento\Framework\Exception\LocalizedException; -+use Magento\Framework\GraphQl\Config\Element\Field; -+use Magento\Framework\GraphQl\Query\ResolverInterface; -+use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; -+ -+/** -+ * Returns product's image url -+ */ -+class Url implements ResolverInterface -+{ -+ /** -+ * @var ImageFactory -+ */ -+ private $productImageFactory; -+ /** -+ * @var PlaceholderProvider -+ */ -+ private $placeholderProvider; -+ -+ /** -+ * @param ImageFactory $productImageFactory -+ * @param PlaceholderProvider $placeholderProvider -+ */ -+ public function __construct( -+ ImageFactory $productImageFactory, -+ PlaceholderProvider $placeholderProvider -+ ) { -+ $this->productImageFactory = $productImageFactory; -+ $this->placeholderProvider = $placeholderProvider; -+ } -+ -+ /** -+ * @inheritdoc -+ */ -+ public function resolve( -+ Field $field, -+ $context, -+ ResolveInfo $info, -+ array $value = null, -+ array $args = null -+ ) { -+ if (!isset($value['image_type'])) { -+ throw new LocalizedException(__('"image_type" value should be specified')); -+ } -+ -+ if (!isset($value['model'])) { -+ throw new LocalizedException(__('"model" value should be specified')); -+ } -+ -+ /** @var Product $product */ -+ $product = $value['model']; -+ $imagePath = $product->getData($value['image_type']); -+ -+ return $this->getImageUrl($value['image_type'], $imagePath); -+ } -+ -+ /** -+ * Get image URL -+ * -+ * @param string $imageType -+ * @param string|null $imagePath -+ * @return string -+ * @throws \Exception -+ */ -+ private function getImageUrl(string $imageType, ?string $imagePath): string -+ { -+ $image = $this->productImageFactory->create(); -+ $image->setDestinationSubdir($imageType) -+ ->setBaseFile($imagePath); -+ -+ if ($image->isBaseFilePlaceholder()) { -+ return $this->placeholderProvider->getPlaceholder($imageType); -+ } -+ -+ return $image->getUrl(); -+ } -+} -diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/ProductLinks.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/ProductLinks.php -index 181371f16d3..4d1b11a74b9 100644 ---- a/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/ProductLinks.php -+++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/ProductLinks.php -@@ -7,18 +7,18 @@ declare(strict_types=1); - - namespace Magento\CatalogGraphQl\Model\Resolver\Product; - -+use Magento\Framework\Exception\LocalizedException; -+use Magento\Framework\GraphQl\Query\Resolver\ContextInterface; - use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; - use Magento\Catalog\Model\Product; - use Magento\Catalog\Model\ProductLink\Link; - use Magento\Framework\GraphQl\Config\Element\Field; --use Magento\Framework\GraphQl\Query\Resolver\Value; --use Magento\Framework\GraphQl\Query\Resolver\ValueFactory; - use Magento\Framework\GraphQl\Query\ResolverInterface; - - /** -- * Format the product links information to conform to GraphQL schema representation -+ * @inheritdoc - * -- * {@inheritdoc} -+ * Format the product links information to conform to GraphQL schema representation - */ - class ProductLinks implements ResolverInterface - { -@@ -28,22 +28,17 @@ class ProductLinks implements ResolverInterface - private $linkTypes = ['related', 'upsell', 'crosssell']; - - /** -- * @var ValueFactory -- */ -- private $valueFactory; -- -- /** -- * @param ValueFactory $valueFactory -- */ -- public function __construct(ValueFactory $valueFactory) -- { -- $this->valueFactory = $valueFactory; -- } -- -- /** -+ * @inheritdoc -+ * - * Format product links data to conform to GraphQL schema - * -- * {@inheritdoc} -+ * @param \Magento\Framework\GraphQl\Config\Element\Field $field -+ * @param ContextInterface $context -+ * @param ResolveInfo $info -+ * @param array|null $value -+ * @param array|null $args -+ * @throws \Exception -+ * @return null|array - */ - public function resolve( - Field $field, -@@ -51,12 +46,9 @@ class ProductLinks implements ResolverInterface - ResolveInfo $info, - array $value = null, - array $args = null -- ): Value { -+ ) { - if (!isset($value['model'])) { -- $result = function () { -- return null; -- }; -- return $this->valueFactory->create($result); -+ throw new LocalizedException(__('"model" value should be specified')); - } - - /** @var Product $product */ -@@ -73,10 +65,6 @@ class ProductLinks implements ResolverInterface - } - } - -- $result = function () use ($links) { -- return $links; -- }; -- -- return $this->valueFactory->create($result); -+ return $links; - } - } -diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/TierPrices.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/TierPrices.php -index 0b7811d4f74..726ef91c568 100644 ---- a/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/TierPrices.php -+++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/TierPrices.php -@@ -7,38 +7,33 @@ declare(strict_types=1); - - namespace Magento\CatalogGraphQl\Model\Resolver\Product; - -+use Magento\Framework\Exception\LocalizedException; -+use Magento\Framework\GraphQl\Query\Resolver\ContextInterface; - use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; - use Magento\Catalog\Model\Product; - use Magento\Catalog\Model\Product\TierPrice; - use Magento\Framework\GraphQl\Config\Element\Field; --use Magento\Framework\GraphQl\Query\Resolver\Value; --use Magento\Framework\GraphQl\Query\Resolver\ValueFactory; - use Magento\Framework\GraphQl\Query\ResolverInterface; - - /** -- * Format a product's tier price information to conform to GraphQL schema representation -+ * @inheritdoc - * -- * {@inheritdoc} -+ * Format a product's tier price information to conform to GraphQL schema representation - */ - class TierPrices implements ResolverInterface - { - /** -- * @var ValueFactory -- */ -- private $valueFactory; -- -- /** -- * @param ValueFactory $valueFactory -- */ -- public function __construct(ValueFactory $valueFactory) -- { -- $this->valueFactory = $valueFactory; -- } -- -- /** -+ * @inheritdoc -+ * - * Format product's tier price data to conform to GraphQL schema - * -- * {@inheritdoc} -+ * @param \Magento\Framework\GraphQl\Config\Element\Field $field -+ * @param ContextInterface $context -+ * @param ResolveInfo $info -+ * @param array|null $value -+ * @param array|null $args -+ * @throws \Exception -+ * @return null|array - */ - public function resolve( - Field $field, -@@ -46,12 +41,9 @@ class TierPrices implements ResolverInterface - ResolveInfo $info, - array $value = null, - array $args = null -- ): Value { -+ ) { - if (!isset($value['model'])) { -- $result = function () { -- return null; -- }; -- return $this->valueFactory->create($result); -+ throw new LocalizedException(__('"model" value should be specified')); - } - - /** @var Product $product */ -@@ -66,10 +58,6 @@ class TierPrices implements ResolverInterface - } - } - -- $result = function () use ($tierPrices) { -- return $tierPrices; -- }; -- -- return $this->valueFactory->create($result); -+ return $tierPrices; - } - } -diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/Websites.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/Websites.php -index 867f1fa0d0b..070c564713a 100644 ---- a/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/Websites.php -+++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/Websites.php -@@ -7,10 +7,9 @@ declare(strict_types=1); - - namespace Magento\CatalogGraphQl\Model\Resolver\Product; - -+use Magento\Framework\Exception\LocalizedException; - use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; --use Magento\Catalog\Model\Product; - use Magento\Framework\GraphQl\Config\Element\Field; --use Magento\Framework\GraphQl\Query\Resolver\Value; - use Magento\Framework\GraphQl\Query\Resolver\ValueFactory; - use Magento\Framework\GraphQl\Query\ResolverInterface; - use Magento\CatalogGraphQl\Model\Resolver\Product\Websites\Collection; -@@ -43,15 +42,12 @@ class Websites implements ResolverInterface - } - - /** -- * {@inheritDoc} -+ * @inheritdoc - */ -- public function resolve(Field $field, $context, ResolveInfo $info, array $value = null, array $args = null) : Value -+ public function resolve(Field $field, $context, ResolveInfo $info, array $value = null, array $args = null) - { - if (!isset($value['entity_id'])) { -- $result = function () { -- return null; -- }; -- return $this->valueFactory->create($result); -+ throw new LocalizedException(__('"model" value should be specified')); - } - $this->productWebsitesCollection->addIdFilters((int)$value['entity_id']); - $result = function () use ($value) { -diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/Products.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/Products.php -index cc791ce780c..a75a9d2cf50 100644 ---- a/app/code/Magento/CatalogGraphQl/Model/Resolver/Products.php -+++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/Products.php -@@ -14,8 +14,6 @@ use Magento\Framework\GraphQl\Config\Element\Field; - use Magento\Framework\GraphQl\Exception\GraphQlInputException; - use Magento\Framework\GraphQl\Query\Resolver\Argument\SearchCriteria\Builder; - use Magento\Framework\GraphQl\Query\Resolver\Argument\SearchCriteria\SearchFilter; --use Magento\Framework\GraphQl\Query\Resolver\Value; --use Magento\Framework\GraphQl\Query\Resolver\ValueFactory; - use Magento\Framework\GraphQl\Query\ResolverInterface; - use Magento\Catalog\Model\Layer\Resolver; - -@@ -44,40 +42,26 @@ class Products implements ResolverInterface - */ - private $searchFilter; - -- /** -- * @var ValueFactory -- */ -- private $valueFactory; -- -- /** -- * @var Layer\DataProvider\Filters -- */ -- private $filtersDataProvider; -- - /** - * @param Builder $searchCriteriaBuilder - * @param Search $searchQuery - * @param Filter $filterQuery -- * @param ValueFactory $valueFactory -+ * @param SearchFilter $searchFilter - */ - public function __construct( - Builder $searchCriteriaBuilder, - Search $searchQuery, - Filter $filterQuery, -- SearchFilter $searchFilter, -- ValueFactory $valueFactory, -- \Magento\CatalogGraphQl\Model\Resolver\Layer\DataProvider\Filters $filtersDataProvider -+ SearchFilter $searchFilter - ) { - $this->searchCriteriaBuilder = $searchCriteriaBuilder; - $this->searchQuery = $searchQuery; - $this->filterQuery = $filterQuery; - $this->searchFilter = $searchFilter; -- $this->valueFactory = $valueFactory; -- $this->filtersDataProvider = $filtersDataProvider; - } - - /** -- * {@inheritdoc} -+ * @inheritdoc - */ - public function resolve( - Field $field, -@@ -85,8 +69,14 @@ class Products implements ResolverInterface - ResolveInfo $info, - array $value = null, - array $args = null -- ) : Value { -+ ) { - $searchCriteria = $this->searchCriteriaBuilder->build($field->getName(), $args); -+ if ($args['currentPage'] < 1) { -+ throw new GraphQlInputException(__('currentPage value must be greater than 0.')); -+ } -+ if ($args['pageSize'] < 1) { -+ throw new GraphQlInputException(__('pageSize value must be greater than 0.')); -+ } - $searchCriteria->setCurrentPage($args['currentPage']); - $searchCriteria->setPageSize($args['pageSize']); - if (!isset($args['search']) && !isset($args['filter'])) { -@@ -110,10 +100,10 @@ class Products implements ResolverInterface - - $currentPage = $searchCriteria->getCurrentPage(); - if ($searchCriteria->getCurrentPage() > $maxPages && $searchResult->getTotalCount() > 0) { -- $currentPage = new GraphQlInputException( -+ throw new GraphQlInputException( - __( -- 'currentPage value %1 specified is greater than the number of pages available.', -- [$maxPages] -+ 'currentPage value %1 specified is greater than the %2 page(s) available.', -+ [$currentPage, $maxPages] - ) - ); - } -@@ -123,15 +113,12 @@ class Products implements ResolverInterface - 'items' => $searchResult->getProductsSearchResult(), - 'page_info' => [ - 'page_size' => $searchCriteria->getPageSize(), -- 'current_page' => $currentPage -+ 'current_page' => $currentPage, -+ 'total_pages' => $maxPages - ], -- 'filters' => $this->filtersDataProvider->getData($layerType) -+ 'layer_type' => $layerType - ]; - -- $result = function () use ($data) { -- return $data; -- }; -- -- return $this->valueFactory->create($result); -+ return $data; - } - } -diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/DataProvider/CategoryTree.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/DataProvider/CategoryTree.php -index d2fc174fb1e..fc5a563c82b 100644 ---- a/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/DataProvider/CategoryTree.php -+++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/DataProvider/CategoryTree.php -@@ -9,7 +9,6 @@ namespace Magento\CatalogGraphQl\Model\Resolver\Products\DataProvider; - - use GraphQL\Language\AST\FieldNode; - use Magento\CatalogGraphQl\Model\Category\DepthCalculator; --use Magento\CatalogGraphQl\Model\Category\Hydrator; - use Magento\CatalogGraphQl\Model\Category\LevelCalculator; - use Magento\Framework\EntityManager\MetadataPool; - use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; -@@ -17,6 +16,7 @@ use Magento\Catalog\Api\Data\CategoryInterface; - use Magento\Catalog\Model\ResourceModel\Category\Collection; - use Magento\Catalog\Model\ResourceModel\Category\CollectionFactory; - use Magento\CatalogGraphQl\Model\AttributesJoiner; -+use Magento\Catalog\Model\Category; - - /** - * Category tree data provider -@@ -53,81 +53,75 @@ class CategoryTree - */ - private $metadata; - -- /** -- * @var Hydrator -- */ -- private $hydrator; -- - /** - * @param CollectionFactory $collectionFactory - * @param AttributesJoiner $attributesJoiner - * @param DepthCalculator $depthCalculator - * @param LevelCalculator $levelCalculator - * @param MetadataPool $metadata -- * @param Hydrator $hydrator - */ - public function __construct( - CollectionFactory $collectionFactory, - AttributesJoiner $attributesJoiner, - DepthCalculator $depthCalculator, - LevelCalculator $levelCalculator, -- MetadataPool $metadata, -- Hydrator $hydrator -+ MetadataPool $metadata - ) { - $this->collectionFactory = $collectionFactory; - $this->attributesJoiner = $attributesJoiner; - $this->depthCalculator = $depthCalculator; - $this->levelCalculator = $levelCalculator; - $this->metadata = $metadata; -- $this->hydrator = $hydrator; - } - - /** -+ * Returns categories tree starting from parent $rootCategoryId -+ * - * @param ResolveInfo $resolveInfo - * @param int $rootCategoryId -- * @return array -+ * @return \Iterator - */ -- public function getTree(ResolveInfo $resolveInfo, int $rootCategoryId) : array -+ public function getTree(ResolveInfo $resolveInfo, int $rootCategoryId): \Iterator - { -- $categoryQuery = $resolveInfo->fieldASTs[0]; -+ $categoryQuery = $resolveInfo->fieldNodes[0]; - $collection = $this->collectionFactory->create(); - $this->joinAttributesRecursively($collection, $categoryQuery); - $depth = $this->depthCalculator->calculate($categoryQuery); - $level = $this->levelCalculator->calculate($rootCategoryId); -+ -+ // If root category is being filter, we've to remove first slash -+ if ($rootCategoryId == Category::TREE_ROOT_ID) { -+ $regExpPathFilter = sprintf('.*%s/[/0-9]*$', $rootCategoryId); -+ } else { -+ $regExpPathFilter = sprintf('.*/%s/[/0-9]*$', $rootCategoryId); -+ } -+ - //Search for desired part of category tree -- $collection->addPathFilter(sprintf('.*/%s/[/0-9]*$', $rootCategoryId)); -+ $collection->addPathFilter($regExpPathFilter); -+ - $collection->addFieldToFilter('level', ['gt' => $level]); - $collection->addFieldToFilter('level', ['lteq' => $level + $depth - self::DEPTH_OFFSET]); -+ $collection->addAttributeToFilter('is_active', 1, "left"); - $collection->setOrder('level'); -+ $collection->setOrder( -+ 'position', -+ $collection::SORT_ORDER_DESC -+ ); - $collection->getSelect()->orWhere( -- $this->metadata->getMetadata(CategoryInterface::class)->getIdentifierField() . ' = ?', -+ $collection->getSelect() -+ ->getConnection() -+ ->quoteIdentifier( -+ 'e.' . $this->metadata->getMetadata(CategoryInterface::class)->getIdentifierField() -+ ) . ' = ?', - $rootCategoryId - ); -- return $this->processTree($collection->getIterator()); -- } -- -- /** -- * @param \Iterator $iterator -- * @return array -- */ -- private function processTree(\Iterator $iterator) : array -- { -- $tree = []; -- while ($iterator->valid()) { -- /** @var CategoryInterface $category */ -- $category = $iterator->current(); -- $iterator->next(); -- $nextCategory = $iterator->current(); -- $tree[$category->getId()] = $this->hydrator->hydrateCategory($category); -- if ($nextCategory && (int) $nextCategory->getLevel() !== (int) $category->getLevel()) { -- $tree[$category->getId()]['children'] = $this->processTree($iterator); -- } -- } - -- return $tree; -+ return $collection->getIterator(); - } - - /** -+ * Join attributes recursively -+ * - * @param Collection $collection - * @param FieldNode $fieldNode - * @return void -diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/DataProvider/ExtractDataFromCategoryTree.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/DataProvider/ExtractDataFromCategoryTree.php -new file mode 100644 -index 00000000000..3525ccbb6a2 ---- /dev/null -+++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/DataProvider/ExtractDataFromCategoryTree.php -@@ -0,0 +1,111 @@ -+<?php -+/** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+declare(strict_types=1); -+ -+namespace Magento\CatalogGraphQl\Model\Resolver\Products\DataProvider; -+ -+use Magento\CatalogGraphQl\Model\Category\Hydrator; -+use Magento\Catalog\Api\Data\CategoryInterface; -+ -+/** -+ * Extract data from category tree -+ */ -+class ExtractDataFromCategoryTree -+{ -+ /** -+ * @var Hydrator -+ */ -+ private $categoryHydrator; -+ -+ /** -+ * @var CategoryInterface -+ */ -+ private $iteratingCategory; -+ -+ /** -+ * @var int -+ */ -+ private $startCategoryFetchLevel = 1; -+ -+ /** -+ * @param Hydrator $categoryHydrator -+ */ -+ public function __construct( -+ Hydrator $categoryHydrator -+ ) { -+ $this->categoryHydrator = $categoryHydrator; -+ } -+ -+ /** -+ * Extract data from category tree -+ * -+ * @param \Iterator $iterator -+ * @return array -+ */ -+ public function execute(\Iterator $iterator): array -+ { -+ $tree = []; -+ while ($iterator->valid()) { -+ /** @var CategoryInterface $category */ -+ $category = $iterator->current(); -+ $iterator->next(); -+ $pathElements = explode("/", $category->getPath()); -+ if (empty($tree)) { -+ $this->startCategoryFetchLevel = count($pathElements) - 1; -+ } -+ $this->iteratingCategory = $category; -+ $currentLevelTree = $this->explodePathToArray($pathElements, $this->startCategoryFetchLevel); -+ if (empty($tree)) { -+ $tree = $currentLevelTree; -+ } -+ $tree = $this->mergeCategoriesTrees($currentLevelTree, $tree); -+ } -+ return $tree; -+ } -+ -+ /** -+ * Merge together complex categories trees -+ * -+ * @param array $tree1 -+ * @param array $tree2 -+ * @return array -+ */ -+ private function mergeCategoriesTrees(array &$tree1, array &$tree2): array -+ { -+ $mergedTree = $tree1; -+ foreach ($tree2 as $currentKey => &$value) { -+ if (is_array($value) && isset($mergedTree[$currentKey]) && is_array($mergedTree[$currentKey])) { -+ $mergedTree[$currentKey] = $this->mergeCategoriesTrees($mergedTree[$currentKey], $value); -+ } else { -+ $mergedTree[$currentKey] = $value; -+ } -+ } -+ return $mergedTree; -+ } -+ -+ /** -+ * Recursive method to generate tree for one category path -+ * -+ * @param array $pathElements -+ * @param int $index -+ * @return array -+ */ -+ private function explodePathToArray(array $pathElements, int $index): array -+ { -+ $tree = []; -+ $tree[$pathElements[$index]]['id'] = $pathElements[$index]; -+ if ($index === count($pathElements) - 1) { -+ $tree[$pathElements[$index]] = $this->categoryHydrator->hydrateCategory($this->iteratingCategory); -+ $tree[$pathElements[$index]]['model'] = $this->iteratingCategory; -+ } -+ $currentIndex = $index; -+ $index++; -+ if (isset($pathElements[$index])) { -+ $tree[$pathElements[$currentIndex]]['children'] = $this->explodePathToArray($pathElements, $index); -+ } -+ return $tree; -+ } -+} -diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/DataProvider/Image/Placeholder.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/DataProvider/Image/Placeholder.php -new file mode 100644 -index 00000000000..f5cf2a9ef82 ---- /dev/null -+++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/DataProvider/Image/Placeholder.php -@@ -0,0 +1,59 @@ -+<?php -+/** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+declare(strict_types=1); -+ -+namespace Magento\CatalogGraphQl\Model\Resolver\Products\DataProvider\Image; -+ -+use Magento\Catalog\Model\View\Asset\PlaceholderFactory; -+use Magento\Framework\View\Asset\Repository as AssetRepository; -+ -+/** -+ * Image Placeholder provider -+ */ -+class Placeholder -+{ -+ /** -+ * @var PlaceholderFactory -+ */ -+ private $placeholderFactory; -+ -+ /** -+ * @var AssetRepository -+ */ -+ private $assetRepository; -+ -+ /** -+ * @param PlaceholderFactory $placeholderFactory -+ * @param AssetRepository $assetRepository -+ */ -+ public function __construct( -+ PlaceholderFactory $placeholderFactory, -+ AssetRepository $assetRepository -+ ) { -+ $this->placeholderFactory = $placeholderFactory; -+ $this->assetRepository = $assetRepository; -+ } -+ -+ /** -+ * Get placeholder -+ * -+ * @param string $imageType -+ * @return string -+ */ -+ public function getPlaceholder(string $imageType): string -+ { -+ $imageAsset = $this->placeholderFactory->create(['type' => $imageType]); -+ -+ // check if placeholder defined in config -+ if ($imageAsset->getFilePath()) { -+ return $imageAsset->getUrl(); -+ } -+ -+ return $this->assetRepository->getUrl( -+ "Magento_Catalog::images/product/placeholder/{$imageType}.jpg" -+ ); -+ } -+} -diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/DataProvider/Image/Placeholder/Theme.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/DataProvider/Image/Placeholder/Theme.php -new file mode 100644 -index 00000000000..dc48c5ef693 ---- /dev/null -+++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/DataProvider/Image/Placeholder/Theme.php -@@ -0,0 +1,71 @@ -+<?php -+/** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+declare(strict_types=1); -+ -+namespace Magento\CatalogGraphQl\Model\Resolver\Products\DataProvider\Image\Placeholder; -+ -+use Magento\Framework\App\Config\ScopeConfigInterface; -+use Magento\Framework\View\Design\Theme\ThemeProviderInterface; -+use Magento\Store\Model\StoreManagerInterface; -+ -+/** -+ * Theme provider -+ */ -+class Theme -+{ -+ /** -+ * @var ScopeConfigInterface -+ */ -+ private $scopeConfig; -+ -+ /** -+ * @var StoreManagerInterface -+ */ -+ private $storeManager; -+ -+ /** -+ * @var ThemeProviderInterface -+ */ -+ private $themeProvider; -+ -+ /** -+ * @param ScopeConfigInterface $scopeConfig -+ * @param StoreManagerInterface $storeManager -+ * @param ThemeProviderInterface $themeProvider -+ */ -+ public function __construct( -+ ScopeConfigInterface $scopeConfig, -+ StoreManagerInterface $storeManager, -+ ThemeProviderInterface $themeProvider -+ ) { -+ $this->scopeConfig = $scopeConfig; -+ $this->storeManager = $storeManager; -+ $this->themeProvider = $themeProvider; -+ } -+ -+ /** -+ * Get theme model -+ * -+ * @return array -+ * @throws \Magento\Framework\Exception\NoSuchEntityException -+ */ -+ public function getThemeData(): array -+ { -+ $themeId = $this->scopeConfig->getValue( -+ \Magento\Framework\View\DesignInterface::XML_PATH_THEME_ID, -+ \Magento\Store\Model\ScopeInterface::SCOPE_STORE, -+ $this->storeManager->getStore()->getId() -+ ); -+ -+ /** @var $theme \Magento\Framework\View\Design\ThemeInterface */ -+ $theme = $this->themeProvider->getThemeById($themeId); -+ -+ $data = $theme->getData(); -+ $data['themeModel'] = $theme; -+ -+ return $data; -+ } -+} -diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/DataProvider/Product.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/DataProvider/Product.php -index 2c73d7a0791..7f1fd719422 100644 ---- a/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/DataProvider/Product.php -+++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/DataProvider/Product.php -@@ -86,8 +86,12 @@ class Product - $collection->load(); - - // Methods that perform extra fetches post-load -- $collection->addMediaGalleryData(); -- $collection->addOptionsToResult(); -+ if (in_array('media_gallery_entries', $attributes)) { -+ $collection->addMediaGalleryData(); -+ } -+ if (in_array('options', $attributes)) { -+ $collection->addOptionsToResult(); -+ } - - $searchResult = $this->searchResultsFactory->create(); - $searchResult->setSearchCriteria($searchCriteria); -diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/Query/Search.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/Query/Search.php -index c4da59fd2ce..bc40c664425 100644 ---- a/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/Query/Search.php -+++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/Query/Search.php -@@ -12,7 +12,6 @@ use Magento\Framework\Api\Search\SearchCriteriaInterface; - use Magento\CatalogGraphQl\Model\Resolver\Products\SearchCriteria\Helper\Filter as FilterHelper; - use Magento\CatalogGraphQl\Model\Resolver\Products\SearchResult; - use Magento\CatalogGraphQl\Model\Resolver\Products\SearchResultFactory; --use Magento\Framework\EntityManager\EntityManager; - use Magento\Search\Api\SearchInterface; - - /** -@@ -45,31 +44,42 @@ class Search - */ - private $metadataPool; - -+ /** -+ * @var \Magento\Search\Model\Search\PageSizeProvider -+ */ -+ private $pageSizeProvider; -+ - /** - * @param SearchInterface $search - * @param FilterHelper $filterHelper - * @param Filter $filterQuery - * @param SearchResultFactory $searchResultFactory -+ * @param \Magento\Framework\EntityManager\MetadataPool $metadataPool -+ * @param \Magento\Search\Model\Search\PageSizeProvider $pageSize - */ - public function __construct( - SearchInterface $search, - FilterHelper $filterHelper, - Filter $filterQuery, - SearchResultFactory $searchResultFactory, -- \Magento\Framework\EntityManager\MetadataPool $metadataPool -+ \Magento\Framework\EntityManager\MetadataPool $metadataPool, -+ \Magento\Search\Model\Search\PageSizeProvider $pageSize - ) { - $this->search = $search; - $this->filterHelper = $filterHelper; - $this->filterQuery = $filterQuery; - $this->searchResultFactory = $searchResultFactory; - $this->metadataPool = $metadataPool; -+ $this->pageSizeProvider = $pageSize; - } - - /** - * Return results of full text catalog search of given term, and will return filtered results if filter is specified - * - * @param SearchCriteriaInterface $searchCriteria -+ * @param ResolveInfo $info - * @return SearchResult -+ * @throws \Exception - */ - public function getResult(SearchCriteriaInterface $searchCriteria, ResolveInfo $info) : SearchResult - { -@@ -79,7 +89,8 @@ class Search - $realPageSize = $searchCriteria->getPageSize(); - $realCurrentPage = $searchCriteria->getCurrentPage(); - // Current page must be set to 0 and page size to max for search to grab all ID's as temporary workaround -- $searchCriteria->setPageSize(PHP_INT_MAX); -+ $pageSize = $this->pageSizeProvider->getMaxPageSize(); -+ $searchCriteria->setPageSize($pageSize); - $searchCriteria->setCurrentPage(0); - $itemsResults = $this->search->search($searchCriteria); - -@@ -133,7 +144,7 @@ class Search - $offset = $length * ($searchCriteria->getCurrentPage() - 1); - - if ($searchCriteria->getPageSize()) { -- $maxPages = ceil($searchResult->getTotalCount() / $searchCriteria->getPageSize()) - 1; -+ $maxPages = ceil($searchResult->getTotalCount() / $searchCriteria->getPageSize()); - } else { - $maxPages = 0; - } -diff --git a/app/code/Magento/CatalogGraphQl/Model/Search/Adapter/Mysql/Query/Builder/Match.php b/app/code/Magento/CatalogGraphQl/Model/Search/Adapter/Mysql/Query/Builder/Match.php -new file mode 100644 -index 00000000000..4490cf031e5 ---- /dev/null -+++ b/app/code/Magento/CatalogGraphQl/Model/Search/Adapter/Mysql/Query/Builder/Match.php -@@ -0,0 +1,78 @@ -+<?php -+/** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+declare(strict_types=1); -+ -+namespace Magento\CatalogGraphQl\Model\Search\Adapter\Mysql\Query\Builder; -+ -+use Magento\Framework\DB\Helper\Mysql\Fulltext; -+use Magento\Framework\Search\Adapter\Mysql\Field\ResolverInterface; -+use Magento\Framework\Search\Adapter\Mysql\Query\Builder\Match as BuilderMatch; -+use Magento\Framework\Search\Adapter\Preprocessor\PreprocessorInterface; -+use Magento\Framework\Search\Request\Query\BoolExpression; -+use Magento\Search\Helper\Data; -+ -+/** -+ * @inheritdoc -+ */ -+class Match extends BuilderMatch -+{ -+ /** -+ * @var Data -+ */ -+ private $searchHelper; -+ -+ /** -+ * @param ResolverInterface $resolver -+ * @param Fulltext $fulltextHelper -+ * @param Data $searchHelper -+ * @param string $fulltextSearchMode -+ * @param PreprocessorInterface[] $preprocessors -+ */ -+ public function __construct( -+ ResolverInterface $resolver, -+ Fulltext $fulltextHelper, -+ Data $searchHelper, -+ $fulltextSearchMode = Fulltext::FULLTEXT_MODE_BOOLEAN, -+ array $preprocessors = [] -+ ) { -+ parent::__construct($resolver, $fulltextHelper, $fulltextSearchMode, $preprocessors); -+ $this->searchHelper = $searchHelper; -+ } -+ -+ /** -+ * @inheritdoc -+ */ -+ protected function prepareQuery($queryValue, $conditionType) -+ { -+ $replaceSymbols = str_split(self::SPECIAL_CHARACTERS, 1); -+ $queryValue = str_replace($replaceSymbols, ' ', $queryValue); -+ foreach ($this->preprocessors as $preprocessor) { -+ $queryValue = $preprocessor->process($queryValue); -+ } -+ -+ $stringPrefix = ''; -+ if ($conditionType === BoolExpression::QUERY_CONDITION_MUST) { -+ $stringPrefix = '+'; -+ } elseif ($conditionType === BoolExpression::QUERY_CONDITION_NOT) { -+ $stringPrefix = '-'; -+ } -+ -+ $queryValues = explode(' ', $queryValue); -+ -+ foreach ($queryValues as $queryKey => $queryValue) { -+ if (empty($queryValue)) { -+ unset($queryValues[$queryKey]); -+ } else { -+ $stringSuffix = $this->searchHelper->getMinQueryLength() > strlen($queryValue) ? '' : '*'; -+ $queryValues[$queryKey] = $stringPrefix . $queryValue . $stringSuffix; -+ } -+ } -+ -+ $queryValue = implode(' ', $queryValues); -+ -+ return $queryValue; -+ } -+} -diff --git a/app/code/Magento/CatalogGraphQl/Test/Unit/Model/Resolver/Product/CanonicalUrlTest.php b/app/code/Magento/CatalogGraphQl/Test/Unit/Model/Resolver/Product/CanonicalUrlTest.php -deleted file mode 100644 -index ae01c67eb52..00000000000 ---- a/app/code/Magento/CatalogGraphQl/Test/Unit/Model/Resolver/Product/CanonicalUrlTest.php -+++ /dev/null -@@ -1,92 +0,0 @@ --<?php --/** -- * Copyright © Magento, Inc. All rights reserved. -- * See COPYING.txt for license details. -- */ --declare(strict_types=1); -- --namespace Magento\CatalogGraphQl\Test\Unit\Model\Resolver\Product; -- --use Magento\CatalogGraphQl\Model\Resolver\Product\CanonicalUrl; --use Magento\CatalogUrlRewrite\Model\ProductUrlPathGenerator; --use Magento\Framework\GraphQl\Query\Resolver\Value; --use Magento\Framework\GraphQl\Query\Resolver\ValueFactory; --use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; --use Magento\Store\Model\StoreManagerInterface; --use PHPUnit\Framework\TestCase; -- --class CanonicalUrlTest extends TestCase --{ -- /** -- * @var ObjectManager -- */ -- private $objectManager; -- -- /** -- * @var CanonicalUrl -- */ -- private $subject; -- -- /** -- * @var \PHPUnit_Framework_MockObject_MockObject -- */ -- private $mockValueFactory; -- -- /** -- * @var \PHPUnit_Framework_MockObject_MockObject -- */ -- private $mockStoreManager; -- -- public function testReturnsNullWhenNoProductAvailable() -- { -- $mockField = $this->getMockBuilder(\Magento\Framework\GraphQl\Config\Element\Field::class) -- ->disableOriginalConstructor() -- ->getMock(); -- $mockInfo = $this->getMockBuilder(\Magento\Framework\GraphQl\Schema\Type\ResolveInfo::class) -- ->disableOriginalConstructor() -- ->getMock(); -- -- $this->mockValueFactory->method('create')->with( -- $this->callback( -- function ($param) { -- return $param() === null; -- } -- ) -- ); -- -- $this->subject->resolve($mockField, '', $mockInfo, [], []); -- } -- -- protected function setUp() -- { -- parent::setUp(); -- $this->objectManager = new ObjectManager($this); -- $this->mockStoreManager = $this->getMockBuilder(StoreManagerInterface::class)->getMock(); -- $this->mockValueFactory = $this->getMockBuilder(ValueFactory::class) -- ->disableOriginalConstructor() -- ->getMock(); -- -- $this->mockValueFactory->method('create')->willReturn( -- $this->objectManager->getObject( -- Value::class, -- ['callback' => function () { -- return ''; -- }] -- ) -- ); -- -- $mockProductUrlPathGenerator = $this->getMockBuilder(ProductUrlPathGenerator::class) -- ->disableOriginalConstructor() -- ->getMock(); -- $mockProductUrlPathGenerator->method('getUrlPathWithSuffix')->willReturn('product_url.html'); -- -- $this->subject = $this->objectManager->getObject( -- CanonicalUrl::class, -- [ -- 'valueFactory' => $this->mockValueFactory, -- 'storeManager' => $this->mockStoreManager, -- 'productUrlPathGenerator' => $mockProductUrlPathGenerator -- ] -- ); -- } --} -diff --git a/app/code/Magento/CatalogGraphQl/composer.json b/app/code/Magento/CatalogGraphQl/composer.json -index eb86ac63441..950b496263f 100644 ---- a/app/code/Magento/CatalogGraphQl/composer.json -+++ b/app/code/Magento/CatalogGraphQl/composer.json -@@ -14,6 +14,7 @@ - }, - "suggest": { - "magento/module-graph-ql": "*", -+ "magento/module-graph-ql-cache": "*", - "magento/module-store-graph-ql": "*" - }, - "license": [ -diff --git a/app/code/Magento/CatalogGraphQl/etc/graphql/di.xml b/app/code/Magento/CatalogGraphQl/etc/graphql/di.xml -index 68a292ede6b..2292004f3cf 100644 ---- a/app/code/Magento/CatalogGraphQl/etc/graphql/di.xml -+++ b/app/code/Magento/CatalogGraphQl/etc/graphql/di.xml -@@ -6,6 +6,7 @@ - */ - --> - <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd"> -+ <preference for="Magento\Catalog\Model\Product\Option\Type\Date" type="Magento\CatalogGraphQl\Model\Product\Option\DateType" /> - <type name="Magento\CatalogGraphQl\Model\ProductInterfaceTypeResolverComposite"> - <arguments> - <argument name="productTypeNameResolvers" xsi:type="array"> -@@ -37,11 +38,15 @@ - </item> - <item name="customizable_options" xsi:type="array"> - <item name="field" xsi:type="string">CustomizableFieldOption</item> -+ <item name="date" xsi:type="string">CustomizableDateOption</item> - <item name="date_time" xsi:type="string">CustomizableDateOption</item> -+ <item name="time" xsi:type="string">CustomizableDateOption</item> - <item name="file" xsi:type="string">CustomizableFileOption</item> - <item name="area" xsi:type="string">CustomizableAreaOption</item> - <item name="drop_down" xsi:type="string">CustomizableDropDownOption</item> -+ <item name="multiple" xsi:type="string">CustomizableMultipleOption</item> - <item name="radio" xsi:type="string">CustomizableRadioOption</item> -+ <item name="checkbox" xsi:type="string">CustomizableCheckboxOption</item> - </item> - </argument> - </arguments> -@@ -73,4 +78,21 @@ - </argument> - </arguments> - </virtualType> -+ <preference for="Magento\Framework\Search\Adapter\Mysql\Query\Builder\Match" -+ type="Magento\CatalogGraphQl\Model\Search\Adapter\Mysql\Query\Builder\Match" /> -+ <type name="Magento\StoreGraphQl\Model\Resolver\Store\StoreConfigDataProvider"> -+ <arguments> -+ <argument name="extendedConfigData" xsi:type="array"> -+ <item name="product_url_suffix" xsi:type="string">catalog/seo/product_url_suffix</item> -+ <item name="category_url_suffix" xsi:type="string">catalog/seo/category_url_suffix</item> -+ <item name="title_separator" xsi:type="string">catalog/seo/title_separator</item> -+ <item name="list_mode" xsi:type="string">catalog/frontend/list_mode</item> -+ <item name="grid_per_page_values" xsi:type="string">catalog/frontend/grid_per_page_values</item> -+ <item name="list_per_page_values" xsi:type="string">catalog/frontend/list_per_page_values</item> -+ <item name="grid_per_page" xsi:type="string">catalog/frontend/grid_per_page</item> -+ <item name="list_per_page" xsi:type="string">catalog/frontend/list_per_page</item> -+ <item name="catalog_default_sort_by" xsi:type="string">catalog/frontend/default_sort_by</item> -+ </argument> -+ </arguments> -+ </type> - </config> -diff --git a/app/code/Magento/CatalogGraphQl/etc/schema.graphqls b/app/code/Magento/CatalogGraphQl/etc/schema.graphqls -index 9235ec271a3..f4d0990b170 100644 ---- a/app/code/Magento/CatalogGraphQl/etc/schema.graphqls -+++ b/app/code/Magento/CatalogGraphQl/etc/schema.graphqls -@@ -9,195 +9,22 @@ type Query { - currentPage: Int = 1 @doc(description: "Specifies which page of results to return. The default value is 1."), - sort: ProductSortInput @doc(description: "Specifies which attribute to sort on, and whether to return the results in ascending or descending order.") - ): Products -- @resolver(class: "Magento\\CatalogGraphQl\\Model\\Resolver\\Products") @doc(description: "The products query searches for products that match the criteria specified in the search and filter attributes") -+ @resolver(class: "Magento\\CatalogGraphQl\\Model\\Resolver\\Products") @doc(description: "The products query searches for products that match the criteria specified in the search and filter attributes.") @cache(cacheTag: "cat_p", cacheIdentity: "Magento\\CatalogGraphQl\\Model\\Resolver\\Product\\Identity") - category ( -- id: Int @doc(description: "Id of the category") -+ id: Int @doc(description: "Id of the category.") - ): CategoryTree -- @resolver(class: "Magento\\CatalogGraphQl\\Model\\Resolver\\CategoryTree") --} -- --enum CurrencyEnum @doc(description: "The list of available currency codes") { -- AFN -- ALL -- AZN -- DZD -- AOA -- ARS -- AMD -- AWG -- AUD -- BSD -- BHD -- BDT -- BBD -- BYR -- BZD -- BMD -- BTN -- BOB -- BAM -- BWP -- BRL -- GBP -- BND -- BGN -- BUK -- BIF -- KHR -- CAD -- CVE -- CZK -- KYD -- GQE -- CLP -- CNY -- COP -- KMF -- CDF -- CRC -- HRK -- CUP -- DKK -- DJF -- DOP -- XCD -- EGP -- SVC -- ERN -- EEK -- ETB -- EUR -- FKP -- FJD -- GMD -- GEK -- GEL -- GHS -- GIP -- GTQ -- GNF -- GYD -- HTG -- HNL -- HKD -- HUF -- ISK -- INR -- IDR -- IRR -- IQD -- ILS -- JMD -- JPY -- JOD -- KZT -- KES -- KWD -- KGS -- LAK -- LVL -- LBP -- LSL -- LRD -- LYD -- LTL -- MOP -- MKD -- MGA -- MWK -- MYR -- MVR -- LSM -- MRO -- MUR -- MXN -- MDL -- MNT -- MAD -- MZN -- MMK -- NAD -- NPR -- ANG -- YTL -- NZD -- NIC -- NGN -- KPW -- NOK -- OMR -- PKR -- PAB -- PGK -- PYG -- PEN -- PHP -- PLN -- QAR -- RHD -- RON -- RUB -- RWF -- SHP -- STD -- SAR -- RSD -- SCR -- SLL -- SGD -- SKK -- SBD -- SOS -- ZAR -- KRW -- LKR -- SDG -- SRD -- SZL -- SEK -- CHF -- SYP -- TWD -- TJS -- TZS -- THB -- TOP -- TTD -- TND -- TMM -- USD -- UGX -- UAH -- AED -- UYU -- UZS -- VUV -- VEB -- VEF -- VND -- CHE -- CHW -- XOF -- WST -- YER -- ZMK -- ZWD -- TRY -- AZM -- ROL -- TRL -- XPF -+ @resolver(class: "Magento\\CatalogGraphQl\\Model\\Resolver\\CategoryTree") @doc(description: "The category query searches for categories that match the criteria specified in the search and filter attributes.") @cache(cacheTag: "cat_c", cacheIdentity: "Magento\\CatalogGraphQl\\Model\\Resolver\\Category\\CategoryTreeIdentity") - } - - type Price @doc(description: "The Price object defines the price of a product as well as any tax-related adjustments.") { -- amount: Money @doc(description: "The price of a product plus a three-letter currency code") -- adjustments: [PriceAdjustment] @doc(description: "An array that provides information about tax, weee, or weee_tax adjustments") -+ amount: Money @doc(description: "The price of a product plus a three-letter currency code.") -+ adjustments: [PriceAdjustment] @doc(description: "An array that provides information about tax, weee, or weee_tax adjustments.") - } - - type PriceAdjustment @doc(description: "The PricedAdjustment object defines the amount of money to apply as an adjustment, the type of adjustment to apply, and whether the item is included or excluded from the adjustment.") { -- amount: Money @doc(description: "The amount of the price adjustment and its currency code") -- code: PriceAdjustmentCodesEnum @doc(description: "Indicates whether the adjustment involves tax, weee, or weee_tax") -- description: PriceAdjustmentDescriptionEnum @doc(description: "Indicates whether the entity described by the code attribute is included or excluded from the adjustment") -+ amount: Money @doc(description: "The amount of the price adjustment and its currency code.") -+ code: PriceAdjustmentCodesEnum @doc(description: "Indicates whether the adjustment involves tax, weee, or weee_tax.") -+ description: PriceAdjustmentDescriptionEnum @doc(description: "Indicates whether the entity described by the code attribute is included or excluded from the adjustment.") - } - - enum PriceAdjustmentCodesEnum @doc(description: "Note: This enumeration contains values defined in modules other than the Catalog module.") { -@@ -214,11 +41,6 @@ enum PriceTypeEnum @doc(description: "This enumeration the price type.") { - DYNAMIC - } - --type Money @doc(description: "A Money object defines a monetary value, including a numeric value and a currency code.") { -- value: Float @doc(description: "A number expressing a monetary value") -- currency: CurrencyEnum @doc(description: "A three-letter currency code, such as USD or EUR") --} -- - type ProductPrices @doc(description: "The ProductPrices object contains the regular price of an item, as well as its minimum and maximum prices. Only composite products, which include bundle, configurable, and grouped products, can contain a minimum and maximum price.") { - minimalPrice: Price @doc(description: "The lowest possible final price for all the options defined within a composite product. If you are specifying a price range, this would be the from value.") - maximalPrice: Price @doc(description: "The highest possible final price for all the options defined within a composite product. If you are specifying a price range, this would be the to value.") -@@ -229,313 +51,341 @@ type ProductLinks implements ProductLinksInterface @doc(description: "ProductLin - } - - interface ProductLinksInterface @typeResolver(class: "Magento\\CatalogGraphQl\\Model\\ProductLinkTypeResolverComposite") @doc(description:"ProductLinks contains information about linked products, including the link type and product type of each item.") { -- sku: String @doc(description: "The identifier of the linked product") -- link_type: String @doc(description: "One of related, associated, upsell, or crosssell") -- linked_product_sku: String @doc(description: "The SKU of the linked product") -- linked_product_type: String @doc(description: "The type of linked product (simple, virtual, bundle, downloadable, grouped, configurable)") -- position: Int @doc(description: "The position within the list of product links") -+ sku: String @doc(description: "The identifier of the linked product.") -+ link_type: String @doc(description: "One of related, associated, upsell, or crosssell.") -+ linked_product_sku: String @doc(description: "The SKU of the linked product.") -+ linked_product_type: String @doc(description: "The type of linked product (simple, virtual, bundle, downloadable, grouped, configurable).") -+ position: Int @doc(description: "The position within the list of product links.") - } - - type ProductTierPrices @doc(description: "The ProductTierPrices object defines a tier price, which is a quantity discount offered to a specific customer group.") { -- customer_group_id: String @doc(description: "The ID of the customer group") -- qty: Float @doc(description: "The number of items that must be purchased to qualify for tier pricing") -- value: Float @doc(description: "The price of the fixed price item") -- percentage_value: Float @doc(description: "The percentage discount of the item") -- website_id: Float @doc(description: "The ID assigned to the website") -+ customer_group_id: String @doc(description: "The ID of the customer group.") -+ qty: Float @doc(description: "The number of items that must be purchased to qualify for tier pricing.") -+ value: Float @doc(description: "The price of the fixed price item.") -+ percentage_value: Float @doc(description: "The percentage discount of the item.") -+ website_id: Float @doc(description: "The ID assigned to the website.") - } - - interface ProductInterface @typeResolver(class: "Magento\\CatalogGraphQl\\Model\\ProductInterfaceTypeResolverComposite") @doc(description: "The ProductInterface contains attributes that are common to all types of products. Note that descriptions may not be available for custom and EAV attributes.") { -- id: Int @doc(description: "The ID number assigned to the product") @resolver(class: "Magento\\CatalogGraphQl\\Model\\Resolver\\Product\\EntityIdToId") -+ id: Int @doc(description: "The ID number assigned to the product.") @resolver(class: "Magento\\CatalogGraphQl\\Model\\Resolver\\Product\\EntityIdToId") - name: String @doc(description: "The product name. Customers use this name to identify the product.") -- sku: String @doc(description: "A number or code assigned to a product to identify the product, options, price, and manufacturer") -- description: String @doc(description: "Detailed information about the product. The value can include simple HTML tags.") -- short_description: String @doc(description: "A short description of the product. Its use depends on the theme.") -- special_price: Float @doc(description: "The discounted price of the product") -- special_from_date: String @doc(description: "The beginning date that a product has a special price") -- special_to_date: String @doc(description: "The end date that a product has a special price") -- attribute_set_id: Int @doc(description: "The attribute set assigned to the product") -- meta_title: String @doc(description: "A string that is displayed in the title bar and tab of the browser and in search results lists") -- meta_keyword: String @doc(description: "A comma-separated list of keywords that are visible only to search engines") -- meta_description: String @doc(description: "A brief overview of the product for search results listings, maximum 255 characters") -- image: String @doc(description: "The relative path to the main image on the product page") -- small_image: String @doc(description: "The relative path to the small image, which is used on catalog pages") -- thumbnail: String @doc(description: "The relative path to the product's thumbnail image") -- new_from_date: String @doc(description: "The beginning date for new product listings, and determines if the product is featured as a new product") @resolver(class: "Magento\\CatalogGraphQl\\Model\\Resolver\\Product\\NewFromTo") -- new_to_date: String @doc(description: "The end date for new product listings") @resolver(class: "Magento\\CatalogGraphQl\\Model\\Resolver\\Product\\NewFromTo") -- tier_price: Float @doc(description: "The price when tier pricing is in effect and the items purchased threshold has been reached") -- options_container: String @doc(description: "If the product has multiple options, determines where they appear on the product page") -- image_label: String @doc(description: "The label assigned to a product image") -- small_image_label: String @doc(description: "The label assigned to a product's small image") -- thumbnail_label: String @doc(description: "The label assigned to a product's thumbnail image") -- created_at: String @doc(description: "Timestamp indicating when the product was created") -- updated_at: String @doc(description: "Timestamp indicating when the product was updated") -- country_of_manufacture: String @doc(description: "The product's country of origin") -- type_id: String @doc(description: "One of simple, virtual, bundle, downloadable, grouped, or configurable") -- websites: [Website] @doc(description: "An array of websites in which the product is available") @resolver(class: "Magento\\CatalogGraphQl\\Model\\Resolver\\Product\\Websites") -- product_links: [ProductLinksInterface] @doc(description: "An array of ProductLinks objects") @resolver(class: "Magento\\CatalogGraphQl\\Model\\Resolver\\Product\\ProductLinks") -- media_gallery_entries: [MediaGalleryEntry] @doc(description: "An array of MediaGalleryEntry objects") @resolver(class: "Magento\\CatalogGraphQl\\Model\\Resolver\\Product\\MediaGalleryEntries") -- tier_prices: [ProductTierPrices] @doc(description: "An array of ProductTierPrices objects") @resolver(class: "Magento\\CatalogGraphQl\\Model\\Resolver\\Product\\TierPrices") -- price: ProductPrices @doc(description: "A ProductPrices object, indicating the price of an item") @resolver(class: "Magento\\CatalogGraphQl\\Model\\Resolver\\Product\\Price") -- gift_message_available: String @doc(description: "Indicates whether a gift message is available") -- manufacturer: Int @doc(description: "A number representing the product's manufacturer") -- categories: [CategoryInterface] @doc(description: "The categories assigned to a product") @resolver(class: "Magento\\CatalogGraphQl\\Model\\Resolver\\Categories") -- canonical_url: String @doc(description: "Canonical URL") @resolver(class: "Magento\\CatalogGraphQl\\Model\\Resolver\\Product\\CanonicalUrl") --} -- --interface PhysicalProductInterface @typeResolver(class: "Magento\\CatalogGraphQl\\Model\\ProductInterfaceTypeResolverComposite") @doc(description: "PhysicalProductInterface contains attributes specific to tangible products") { -- weight: Float @doc(description: "The weight of the item, in units defined by the store") --} -- --type CustomizableAreaOption implements CustomizableOptionInterface @doc(description: "CustomizableAreaOption contains information about a text area that is defined as part of a customizable option") { -- value: CustomizableAreaValue @doc(description: "An object that defines a text area") -- product_sku: String @doc(description: "The Stock Keeping Unit of the base product") --} -- --type CustomizableAreaValue @doc(description: "CustomizableAreaValue defines the price and sku of a product whose page contains a customized text area") { -- price: Float @doc(description: "The price assigned to this option") -- price_type: PriceTypeEnum @doc(description: "FIXED, PERCENT, or DYNAMIC") -- sku: String @doc(description: "The Stock Keeping Unit for this option") -- max_characters: Int @doc(description: "The maximum number of characters that can be entered for this customizable option") --} -- --type CategoryTree implements CategoryInterface @doc(description: "Category Tree implementation") { -- children: [CategoryTree] @doc(description: "Child categories tree") @resolve(class: "Magento\\CatalogGraphQl\\Model\\Resolver\\CategoryTree") --} -- --type CustomizableDateOption implements CustomizableOptionInterface @doc(description: "CustomizableDateOption contains information about a date picker that is defined as part of a customizable option") { -+ sku: String @doc(description: "A number or code assigned to a product to identify the product, options, price, and manufacturer.") -+ description: ComplexTextValue @doc(description: "Detailed information about the product. The value can include simple HTML tags.") @resolver(class: "\\Magento\\CatalogGraphQl\\Model\\Resolver\\Product\\ProductComplexTextAttribute") -+ short_description: ComplexTextValue @doc(description: "A short description of the product. Its use depends on the theme.") @resolver(class: "\\Magento\\CatalogGraphQl\\Model\\Resolver\\Product\\ProductComplexTextAttribute") -+ special_price: Float @doc(description: "The discounted price of the product.") -+ special_from_date: String @doc(description: "The beginning date that a product has a special price.") -+ special_to_date: String @doc(description: "The end date that a product has a special price.") -+ attribute_set_id: Int @doc(description: "The attribute set assigned to the product.") -+ meta_title: String @doc(description: "A string that is displayed in the title bar and tab of the browser and in search results lists.") -+ meta_keyword: String @doc(description: "A comma-separated list of keywords that are visible only to search engines.") -+ meta_description: String @doc(description: "A brief overview of the product for search results listings, maximum 255 characters.") -+ image: ProductImage @doc(description: "The relative path to the main image on the product page.") @resolver(class: "Magento\\CatalogGraphQl\\Model\\Resolver\\Product\\ProductImage") -+ small_image: ProductImage @doc(description: "The relative path to the small image, which is used on catalog pages.") @resolver(class: "Magento\\CatalogGraphQl\\Model\\Resolver\\Product\\ProductImage") -+ thumbnail: ProductImage @doc(description: "The relative path to the product's thumbnail image.") @resolver(class: "Magento\\CatalogGraphQl\\Model\\Resolver\\Product\\ProductImage") -+ new_from_date: String @doc(description: "The beginning date for new product listings, and determines if the product is featured as a new product.") @resolver(class: "Magento\\CatalogGraphQl\\Model\\Resolver\\Product\\NewFromTo") -+ new_to_date: String @doc(description: "The end date for new product listings.") @resolver(class: "Magento\\CatalogGraphQl\\Model\\Resolver\\Product\\NewFromTo") -+ tier_price: Float @doc(description: "The price when tier pricing is in effect and the items purchased threshold has been reached.") -+ options_container: String @doc(description: "If the product has multiple options, determines where they appear on the product page.") -+ created_at: String @doc(description: "Timestamp indicating when the product was created.") -+ updated_at: String @doc(description: "Timestamp indicating when the product was updated.") -+ country_of_manufacture: String @doc(description: "The product's country of origin.") -+ type_id: String @doc(description: "One of simple, virtual, bundle, downloadable, grouped, or configurable.") -+ websites: [Website] @doc(description: "An array of websites in which the product is available.") @resolver(class: "Magento\\CatalogGraphQl\\Model\\Resolver\\Product\\Websites") -+ product_links: [ProductLinksInterface] @doc(description: "An array of ProductLinks objects.") @resolver(class: "Magento\\CatalogGraphQl\\Model\\Resolver\\Product\\ProductLinks") -+ media_gallery_entries: [MediaGalleryEntry] @doc(description: "An array of MediaGalleryEntry objects.") @resolver(class: "Magento\\CatalogGraphQl\\Model\\Resolver\\Product\\MediaGalleryEntries") -+ tier_prices: [ProductTierPrices] @doc(description: "An array of ProductTierPrices objects.") @resolver(class: "Magento\\CatalogGraphQl\\Model\\Resolver\\Product\\TierPrices") -+ price: ProductPrices @doc(description: "A ProductPrices object, indicating the price of an item.") @resolver(class: "Magento\\CatalogGraphQl\\Model\\Resolver\\Product\\Price") -+ gift_message_available: String @doc(description: "Indicates whether a gift message is available.") -+ manufacturer: Int @doc(description: "A number representing the product's manufacturer.") -+ categories: [CategoryInterface] @doc(description: "The categories assigned to a product.") @cache(cacheTag: "cat_c", cacheIdentity: "Magento\\CatalogGraphQl\\Model\\Resolver\\Category\\CategoriesIdentity") @resolver(class: "Magento\\CatalogGraphQl\\Model\\Resolver\\Categories") -+ canonical_url: String @doc(description: "Canonical URL.") @resolver(class: "Magento\\CatalogGraphQl\\Model\\Resolver\\Product\\CanonicalUrl") -+} -+ -+interface PhysicalProductInterface @typeResolver(class: "Magento\\CatalogGraphQl\\Model\\ProductInterfaceTypeResolverComposite") @doc(description: "PhysicalProductInterface contains attributes specific to tangible products.") { -+ weight: Float @doc(description: "The weight of the item, in units defined by the store.") -+} -+ -+type CustomizableAreaOption implements CustomizableOptionInterface @doc(description: "CustomizableAreaOption contains information about a text area that is defined as part of a customizable option.") { -+ value: CustomizableAreaValue @doc(description: "An object that defines a text area.") -+ product_sku: String @doc(description: "The Stock Keeping Unit of the base product.") -+} -+ -+type CustomizableAreaValue @doc(description: "CustomizableAreaValue defines the price and sku of a product whose page contains a customized text area.") { -+ price: Float @doc(description: "The price assigned to this option.") -+ price_type: PriceTypeEnum @doc(description: "FIXED, PERCENT, or DYNAMIC.") -+ sku: String @doc(description: "The Stock Keeping Unit for this option.") -+ max_characters: Int @doc(description: "The maximum number of characters that can be entered for this customizable option.") -+} -+ -+type CategoryTree implements CategoryInterface @doc(description: "Category Tree implementation.") { -+ children: [CategoryTree] @doc(description: "Child categories tree.") @resolver(class: "Magento\\CatalogGraphQl\\Model\\Resolver\\CategoryTree") -+} -+ -+type CustomizableDateOption implements CustomizableOptionInterface @doc(description: "CustomizableDateOption contains information about a date picker that is defined as part of a customizable option.") { - value: CustomizableDateValue @doc(description: "An object that defines a date field in a customizable option.") -- product_sku: String @doc(description: "The Stock Keeping Unit of the base product") -+ product_sku: String @doc(description: "The Stock Keeping Unit of the base product.") -+} -+ -+type CustomizableDateValue @doc(description: "CustomizableDateValue defines the price and sku of a product whose page contains a customized date picker.") { -+ price: Float @doc(description: "The price assigned to this option.") -+ price_type: PriceTypeEnum @doc(description: "FIXED, PERCENT, or DYNAMIC.") -+ sku: String @doc(description: "The Stock Keeping Unit for this option.") -+} -+ -+type CustomizableDropDownOption implements CustomizableOptionInterface @doc(description: "CustomizableDropDownOption contains information about a drop down menu that is defined as part of a customizable option.") { -+ value: [CustomizableDropDownValue] @doc(description: "An array that defines the set of options for a drop down menu.") -+} -+ -+type CustomizableDropDownValue @doc(description: "CustomizableDropDownValue defines the price and sku of a product whose page contains a customized drop down menu.") { -+ option_type_id: Int @doc(description: "The ID assigned to the value.") -+ price: Float @doc(description: "The price assigned to this option.") -+ price_type: PriceTypeEnum @doc(description: "FIXED, PERCENT, or DYNAMIC.") -+ sku: String @doc(description: "The Stock Keeping Unit for this option.") -+ title: String @doc(description: "The display name for this option.") -+ sort_order: Int @doc(description: "The order in which the option is displayed.") - } - --type CustomizableDateValue @doc(description: "CustomizableDateValue defines the price and sku of a product whose page contains a customized date picker") { -- price: Float @doc(description: "The price assigned to this option") -- price_type: PriceTypeEnum @doc(description: "FIXED, PERCENT, or DYNAMIC") -- sku: String @doc(description: "The Stock Keeping Unit for this option") -+type CustomizableMultipleOption implements CustomizableOptionInterface @doc(description: "CustomizableMultipleOption contains information about a multiselect that is defined as part of a customizable option.") { -+ value: [CustomizableMultipleValue] @doc(description: "An array that defines the set of options for a multiselect.") - } - --type CustomizableDropDownOption implements CustomizableOptionInterface @doc(description: "CustomizableDropDownOption contains information about a drop down menu that is defined as part of a customizable option") { -- value: [CustomizableDropDownValue] @doc(description: "An array that defines the set of options for a drop down menu") -+type CustomizableMultipleValue @doc(description: "CustomizableMultipleValue defines the price and sku of a product whose page contains a customized multiselect.") { -+ option_type_id: Int @doc(description: "The ID assigned to the value.") -+ price: Float @doc(description: "The price assigned to this option.") -+ price_type: PriceTypeEnum @doc(description: "FIXED, PERCENT, or DYNAMIC.") -+ sku: String @doc(description: "The Stock Keeping Unit for this option.") -+ title: String @doc(description: "The display name for this option.") -+ sort_order: Int @doc(description: "The order in which the option is displayed.") - } - --type CustomizableDropDownValue @doc(description: "CustomizableDropDownValue defines the price and sku of a product whose page contains a customized drop down menu") { -- option_type_id: Int @doc(description: "The ID assigned to the value") -- price: Float @doc(description: "The price assigned to this option") -- price_type: PriceTypeEnum @doc(description: "FIXED, PERCENT, or DYNAMIC") -- sku: String @doc(description: "The Stock Keeping Unit for this option") -- title: String @doc(description: "The display name for this option") -- sort_order: Int @doc(description: "The order in which the option is displayed") -+type CustomizableFieldOption implements CustomizableOptionInterface @doc(description: "CustomizableFieldOption contains information about a text field that is defined as part of a customizable option.") { -+ value: CustomizableFieldValue @doc(description: "An object that defines a text field.") -+ product_sku: String @doc(description: "The Stock Keeping Unit of the base product.") - } - --type CustomizableFieldOption implements CustomizableOptionInterface @doc(description: "CustomizableFieldOption contains information about a text field that is defined as part of a customizable option") { -- value: CustomizableFieldValue @doc(description: "An object that defines a text field") -- product_sku: String @doc(description: "The Stock Keeping Unit of the base product") -+type CustomizableFieldValue @doc(description: "CustomizableFieldValue defines the price and sku of a product whose page contains a customized text field.") { -+ price: Float @doc(description: "The price of the custom value.") -+ price_type: PriceTypeEnum @doc(description: "FIXED, PERCENT, or DYNAMIC.") -+ sku: String @doc(description: "The Stock Keeping Unit for this option.") -+ max_characters: Int @doc(description: "The maximum number of characters that can be entered for this customizable option.") - } - --type CustomizableFieldValue @doc(description: "CustomizableFieldValue defines the price and sku of a product whose page contains a customized text field") { -- price: Float @doc(description: "The price of the custom value") -- price_type: PriceTypeEnum @doc(description: "FIXED, PERCENT, or DYNAMIC") -- sku: String @doc(description: "The Stock Keeping Unit for this option") -- max_characters: Int @doc(description: "The maximum number of characters that can be entered for this customizable option") -+type CustomizableFileOption implements CustomizableOptionInterface @doc(description: "CustomizableFileOption contains information about a file picker that is defined as part of a customizable option.") { -+ value: CustomizableFileValue @doc(description: "An object that defines a file value.") -+ product_sku: String @doc(description: "The Stock Keeping Unit of the base product.") - } - --type CustomizableFileOption implements CustomizableOptionInterface @doc(description: "CustomizableFileOption contains information about a file picker that is defined as part of a customizable option") { -- value: CustomizableFileValue @doc(description: "An object that defines a file value") -- product_sku: String @doc(description: "The Stock Keeping Unit of the base product") -+type CustomizableFileValue @doc(description: "CustomizableFileValue defines the price and sku of a product whose page contains a customized file picker.") { -+ price: Float @doc(description: "The price assigned to this option.") -+ price_type: PriceTypeEnum @doc(description: "FIXED, PERCENT, or DYNAMIC.") -+ sku: String @doc(description: "The Stock Keeping Unit for this option.") -+ file_extension: String @doc(description: "The file extension to accept.") -+ image_size_x: Int @doc(description: "The maximum width of an image.") -+ image_size_y: Int @doc(description: "The maximum height of an image.") - } - --type CustomizableFileValue @doc(description: "CustomizableFileValue defines the price and sku of a product whose page contains a customized file picker") { -- price: Float @doc(description: "The price assigned to this option") -- price_type: PriceTypeEnum @doc(description: "FIXED, PERCENT, or DYNAMIC") -- sku: String @doc(description: "The Stock Keeping Unit for this option") -- file_extension: String @doc(description: "The file extension to accept") -- image_size_x: Int @doc(description: "The maximum width of an image") -- image_size_y: Int @doc(description: "The maximum height of an image") -+type ProductImage @doc(description: "Product image information. Contains image relative path, URL and label.") { -+ url: String @resolver(class: "Magento\\CatalogGraphQl\\Model\\Resolver\\Product\\ProductImage\\Url") -+ label: String @resolver(class: "Magento\\CatalogGraphQl\\Model\\Resolver\\Product\\ProductImage\\Label") - } - - interface CustomizableOptionInterface @typeResolver(class: "Magento\\CatalogGraphQl\\Model\\CustomizableOptionTypeResolver") @doc(description: "The CustomizableOptionInterface contains basic information about a customizable option. It can be implemented by several types of configurable options.") { -- title: String @doc(description: "The display name for this option") -- required: Boolean @doc(description: "Indicates whether the option is required") -- sort_order: Int @doc(description: "The order in which the option is displayed") -+ title: String @doc(description: "The display name for this option.") -+ required: Boolean @doc(description: "Indicates whether the option is required.") -+ sort_order: Int @doc(description: "The order in which the option is displayed.") -+ option_id: Int @doc(description: "Option ID.") - } - - interface CustomizableProductInterface @typeResolver(class: "Magento\\CatalogGraphQl\\Model\\ProductInterfaceTypeResolverComposite") @doc(description: "CustomizableProductInterface contains information about customizable product options.") { -- options: [CustomizableOptionInterface] @doc(description: "An array of options for a customizable product") @resolver(class: "Magento\\CatalogGraphQl\\Model\\Resolver\\Product\\Options") --} -- --interface CategoryInterface @typeResolver(class: "Magento\\CatalogGraphQl\\Model\\CategoryInterfaceTypeResolver") @doc(description: "CategoryInterface contains the full set of attributes that can be returned in a category search") { -- id: Int @doc(description: "An ID that uniquely identifies the category") -- description: String @doc(description: "An optional description of the category") -- name: String @doc(description: "The display name of the category") -- path: String @doc(description: "Category Path") -- path_in_store: String @doc(description: "Category path in store") -- url_key: String @doc(description: "The url key assigned to the category") -- url_path: String @doc(description: "The url path assigned to the category") -- position: Int @doc(description: "The position of the category relative to other categories at the same level in tree") -- level: Int @doc(description: "Indicates the depth of the category within the tree") -- created_at: String @doc(description: "Timestamp indicating when the category was created") -- updated_at: String @doc(description: "Timestamp indicating when the category was updated") -- product_count: Int @doc(description: "The number of products in the category") -- default_sort_by: String @doc(description: "The attribute to use for sorting") -+ options: [CustomizableOptionInterface] @doc(description: "An array of options for a customizable product.") @resolver(class: "Magento\\CatalogGraphQl\\Model\\Resolver\\Product\\Options") -+} -+ -+interface CategoryInterface @typeResolver(class: "Magento\\CatalogGraphQl\\Model\\CategoryInterfaceTypeResolver") @doc(description: "CategoryInterface contains the full set of attributes that can be returned in a category search.") { -+ id: Int @doc(description: "An ID that uniquely identifies the category.") -+ description: String @doc(description: "An optional description of the category.") @resolver(class: "\\Magento\\CatalogGraphQl\\Model\\Resolver\\Category\\CategoryHtmlAttribute") -+ name: String @doc(description: "The display name of the category.") -+ path: String @doc(description: "Category Path.") -+ path_in_store: String @doc(description: "Category path in store.") -+ url_key: String @doc(description: "The url key assigned to the category.") -+ url_path: String @doc(description: "The url path assigned to the category.") -+ position: Int @doc(description: "The position of the category relative to other categories at the same level in tree.") -+ level: Int @doc(description: "Indicates the depth of the category within the tree.") -+ created_at: String @doc(description: "Timestamp indicating when the category was created.") -+ updated_at: String @doc(description: "Timestamp indicating when the category was updated.") -+ product_count: Int @doc(description: "The number of products in the category.") @resolver(class: "Magento\\CatalogGraphQl\\Model\\Resolver\\Category\\ProductsCount") -+ default_sort_by: String @doc(description: "The attribute to use for sorting.") - products( - pageSize: Int = 20 @doc(description: "Specifies the maximum number of results to return at once. This attribute is optional."), - currentPage: Int = 1 @doc(description: "Specifies which page of results to return. The default value is 1."), - sort: ProductSortInput @doc(description: "Specifies which attribute to sort on, and whether to return the results in ascending or descending order.") -- ): CategoryProducts @doc(description: "The list of products assigned to the category") @resolver(class: "Magento\\CatalogGraphQl\\Model\\Resolver\\Category\\Products") -- breadcrumbs: [Breadcrumb] @doc(description: "Breadcrumbs, parent categories info") @resolver(class: "Magento\\CatalogGraphQl\\Model\\Resolver\\Category\\Breadcrumbs") -+ ): CategoryProducts @doc(description: "The list of products assigned to the category.") @cache(cacheTag: "cat_p", cacheIdentity: "Magento\\CatalogGraphQl\\Model\\Resolver\\Product\\Identity") @resolver(class: "Magento\\CatalogGraphQl\\Model\\Resolver\\Category\\Products") -+ breadcrumbs: [Breadcrumb] @doc(description: "Breadcrumbs, parent categories info.") @resolver(class: "Magento\\CatalogGraphQl\\Model\\Resolver\\Category\\Breadcrumbs") -+} -+ -+type Breadcrumb @doc(description: "Breadcrumb item."){ -+ category_id: Int @doc(description: "Category ID.") -+ category_name: String @doc(description: "Category name.") -+ category_level: Int @doc(description: "Category level.") -+ category_url_key: String @doc(description: "Category URL key.") - } - --type Breadcrumb @doc(description: "Breadcrumb item"){ -- category_id: Int @doc(description: "Category ID") -- category_name: String @doc(description: "Category name") -- category_level: Int @doc(description: "Category level") -- category_url_key: String @doc(description: "Category URL key") -+type CustomizableRadioOption implements CustomizableOptionInterface @doc(description: "CustomizableRadioOption contains information about a set of radio buttons that are defined as part of a customizable option.") { -+ value: [CustomizableRadioValue] @doc(description: "An array that defines a set of radio buttons.") - } - --type CustomizableRadioOption implements CustomizableOptionInterface @doc(description: "CustomizableRadioOption contains information about a set of radio buttons that are defined as part of a customizable option") { -- value: [CustomizableRadioValue] @doc(description: "An array that defines a set of radio buttons") -+type CustomizableRadioValue @doc(description: "CustomizableRadioValue defines the price and sku of a product whose page contains a customized set of radio buttons.") { -+ option_type_id: Int @doc(description: "The ID assigned to the value.") -+ price: Float @doc(description: "The price assigned to this option.") -+ price_type: PriceTypeEnum @doc(description: "FIXED, PERCENT, or DYNAMIC.") -+ sku: String @doc(description: "The Stock Keeping Unit for this option.") -+ title: String @doc(description: "The display name for this option.") -+ sort_order: Int @doc(description: "The order in which the radio button is displayed.") - } - --type CustomizableRadioValue @doc(description: "CustomizableRadioValue defines the price and sku of a product whose page contains a customized set of radio buttons") { -- option_type_id: Int @doc(description: "The ID assigned to the value") -- price: Float @doc(description: "The price assigned to this option") -- price_type: PriceTypeEnum @doc(description: "FIXED, PERCENT, or DYNAMIC") -- sku: String @doc(description: "The Stock Keeping Unit for this option") -- title: String @doc(description: "The display name for this option") -- sort_order: Int @doc(description: "The order in which the radio button is displayed") -+type CustomizableCheckboxOption implements CustomizableOptionInterface @doc(description: "CustomizableCheckbbixOption contains information about a set of checkbox values that are defined as part of a customizable option.") { -+ value: [CustomizableCheckboxValue] @doc(description: "An array that defines a set of checkbox values.") - } - --type VirtualProduct implements ProductInterface, CustomizableProductInterface @doc(description: "A virtual product is non-tangible product that does not require shipping and is not kept in inventory") { -+type CustomizableCheckboxValue @doc(description: "CustomizableCheckboxValue defines the price and sku of a product whose page contains a customized set of checkbox values.") { -+ option_type_id: Int @doc(description: "The ID assigned to the value.") -+ price: Float @doc(description: "The price assigned to this option.") -+ price_type: PriceTypeEnum @doc(description: "FIXED, PERCENT, or DYNAMIC.") -+ sku: String @doc(description: "The Stock Keeping Unit for this option.") -+ title: String @doc(description: "The display name for this option.") -+ sort_order: Int @doc(description: "The order in which the checkbox value is displayed.") - } - --type SimpleProduct implements ProductInterface, PhysicalProductInterface, CustomizableProductInterface @doc(description: "A simple product is tangible and are usually sold as single units or in fixed quantities") -+type VirtualProduct implements ProductInterface, CustomizableProductInterface @doc(description: "A virtual product is non-tangible product that does not require shipping and is not kept in inventory.") { -+} -+ -+type SimpleProduct implements ProductInterface, PhysicalProductInterface, CustomizableProductInterface @doc(description: "A simple product is tangible and are usually sold as single units or in fixed quantities.") - { - } - --type Products @doc(description: "The Products object is the top-level object returned in a product search") { -- items: [ProductInterface] @doc(description: "An array of products that match the specified search criteria") -- page_info: SearchResultPageInfo @doc(description: "An object that includes the page_info and currentPage values specified in the query") -- total_count: Int @doc(description: "The number of products returned") -- filters: [LayerFilter] @doc(description: "Layered navigation filters array") -- sort_fields: SortFields @doc(description: "An object that includes the default sort field and all available sort fields") @resolver(class: "Magento\\CatalogGraphQl\\Model\\Resolver\\Category\\SortFields") -+type Products @doc(description: "The Products object is the top-level object returned in a product search.") { -+ items: [ProductInterface] @doc(description: "An array of products that match the specified search criteria.") -+ page_info: SearchResultPageInfo @doc(description: "An object that includes the page_info and currentPage values specified in the query.") -+ total_count: Int @doc(description: "The number of products returned.") -+ filters: [LayerFilter] @resolver(class: "Magento\\CatalogGraphQl\\Model\\Resolver\\LayerFilters") @doc(description: "Layered navigation filters array.") -+ sort_fields: SortFields @doc(description: "An object that includes the default sort field and all available sort fields.") @resolver(class: "Magento\\CatalogGraphQl\\Model\\Resolver\\Category\\SortFields") - } - --type CategoryProducts @doc(description: "The category products object returned in the Category query") { -- items: [ProductInterface] @doc(description: "An array of products that are assigned to the category") -- page_info: SearchResultPageInfo @doc(description: "An object that includes the page_info and currentPage values specified in the query") -- total_count: Int @doc(description: "The number of products returned") -+type CategoryProducts @doc(description: "The category products object returned in the Category query.") { -+ items: [ProductInterface] @doc(description: "An array of products that are assigned to the category.") -+ page_info: SearchResultPageInfo @doc(description: "An object that includes the page_info and currentPage values specified in the query.") -+ total_count: Int @doc(description: "The number of products returned.") - } - - input ProductFilterInput @doc(description: "ProductFilterInput defines the filters to be used in the search. A filter contains at least one attribute, a comparison operator, and the value that is being searched for.") { - name: FilterTypeInput @doc(description: "The product name. Customers use this name to identify the product.") -- sku: FilterTypeInput @doc(description: "A number or code assigned to a product to identify the product, options, price, and manufacturer") -+ sku: FilterTypeInput @doc(description: "A number or code assigned to a product to identify the product, options, price, and manufacturer.") - description: FilterTypeInput @doc(description: "Detailed information about the product. The value can include simple HTML tags.") - short_description: FilterTypeInput @doc(description: "A short description of the product. Its use depends on the theme.") -- price: FilterTypeInput @doc(description: "The price of an item") -- special_price: FilterTypeInput @doc(description: "The discounted price of the product") -- special_from_date: FilterTypeInput @doc(description: "The beginning date that a product has a special price") -- special_to_date: FilterTypeInput @doc(description: "The end date that a product has a special price") -- weight: FilterTypeInput @doc(description: "The weight of the item, in units defined by the store") -- manufacturer: FilterTypeInput @doc(description: "A number representing the product's manufacturer") -- meta_title: FilterTypeInput @doc(description: "A string that is displayed in the title bar and tab of the browser and in search results lists") -- meta_keyword: FilterTypeInput @doc(description: "A comma-separated list of keywords that are visible only to search engines") -- meta_description: FilterTypeInput @doc(description: "A brief overview of the product for search results listings, maximum 255 characters") -- image: FilterTypeInput @doc(description: "The relative path to the main image on the product page") -- small_image: FilterTypeInput @doc(description: "The relative path to the small image, which is used on catalog pages") -- thumbnail: FilterTypeInput @doc(description: "The relative path to the product's thumbnail image") -- tier_price: FilterTypeInput @doc(description: "The price when tier pricing is in effect and the items purchased threshold has been reached") -- news_from_date: FilterTypeInput @doc(description: "The beginning date for new product listings, and determines if the product is featured as a new product") -- news_to_date: FilterTypeInput @doc(description: "The end date for new product listings") -- custom_layout_update: FilterTypeInput @doc(description: "XML code that is applied as a layout update to the product page") -+ price: FilterTypeInput @doc(description: "The price of an item.") -+ special_price: FilterTypeInput @doc(description: "The discounted price of the product. Do not include the currency code.") -+ special_from_date: FilterTypeInput @doc(description: "The beginning date that a product has a special price.") -+ special_to_date: FilterTypeInput @doc(description: "The end date that a product has a special price.") -+ weight: FilterTypeInput @doc(description: "The weight of the item, in units defined by the store.") -+ manufacturer: FilterTypeInput @doc(description: "A number representing the product's manufacturer.") -+ meta_title: FilterTypeInput @doc(description: "A string that is displayed in the title bar and tab of the browser and in search results lists.") -+ meta_keyword: FilterTypeInput @doc(description: "A comma-separated list of keywords that are visible only to search engines.") -+ meta_description: FilterTypeInput @doc(description: "A brief overview of the product for search results listings, maximum 255 characters.") -+ image: FilterTypeInput @doc(description: "The relative path to the main image on the product page.") -+ small_image: FilterTypeInput @doc(description: "The relative path to the small image, which is used on catalog pages.") -+ thumbnail: FilterTypeInput @doc(description: "The relative path to the product's thumbnail image.") -+ tier_price: FilterTypeInput @doc(description: "The price when tier pricing is in effect and the items purchased threshold has been reached.") -+ news_from_date: FilterTypeInput @doc(description: "The beginning date for new product listings, and determines if the product is featured as a new product.") -+ news_to_date: FilterTypeInput @doc(description: "The end date for new product listings.") -+ custom_layout_update: FilterTypeInput @doc(description: "XML code that is applied as a layout update to the product page.") - min_price: FilterTypeInput @doc(description:"The numeric minimal price of the product. Do not include the currency code.") - max_price: FilterTypeInput @doc(description:"The numeric maximal price of the product. Do not include the currency code.") -- special_price: FilterTypeInput @doc(description:"The numeric special price of the product. Do not include the currency code.") -- category_id: FilterTypeInput @doc(description: "Category ID the product belongs to") -- options_container: FilterTypeInput @doc(description: "If the product has multiple options, determines where they appear on the product page") -- required_options: FilterTypeInput @doc(description: "Indicates whether the product has required options") -- has_options: FilterTypeInput @doc(description: "Indicates whether additional attributes have been created for the product") -- image_label: FilterTypeInput @doc(description: "The label assigned to a product image") -- small_image_label: FilterTypeInput @doc(description: "The label assigned to a product's small image") -- thumbnail_label: FilterTypeInput @doc(description: "The label assigned to a product's thumbnail image") -- created_at: FilterTypeInput @doc(description: "Timestamp indicating when the product was created") -- updated_at: FilterTypeInput @doc(description: "Timestamp indicating when the product was updated") -- country_of_manufacture: FilterTypeInput @doc(description: "The product's country of origin") -- custom_layout: FilterTypeInput @doc(description: "The name of a custom layout") -- gift_message_available: FilterTypeInput @doc(description: "Indicates whether a gift message is available") -- or: ProductFilterInput @doc(description: "The keyword required to perform a logical OR comparison") --} -- --type ProductMediaGalleryEntriesContent @doc(description: "ProductMediaGalleryEntriesContent contains an image in base64 format and basic information about the image") { -- base64_encoded_data: String @doc(description: "The image in base64 format") -- type: String @doc(description: "The MIME type of the file, such as image/png") -- name: String @doc(description: "The file name of the image") --} -- --type ProductMediaGalleryEntriesVideoContent @doc(description: "ProductMediaGalleryEntriesVideoContent contains a link to a video file and basic information about the video") { -- media_type: String @doc(description: "Must be external-video") -- video_provider: String @doc(description: "Describes the video source") -- video_url: String @doc(description: "The URL to the video") -- video_title: String @doc(description: "The title of the video") -- video_description: String @doc(description: "A description of the video") -- video_metadata: String @doc(description: "Optional data about the video") --} -- --input ProductSortInput @doc(description: "ProductSortInput specifies the attribute to use for sorting search results and indicates whether the results are sorted in ascending or descending order") { -+ category_id: FilterTypeInput @doc(description: "Category ID the product belongs to.") -+ options_container: FilterTypeInput @doc(description: "If the product has multiple options, determines where they appear on the product page.") -+ required_options: FilterTypeInput @doc(description: "Indicates whether the product has required options.") -+ has_options: FilterTypeInput @doc(description: "Indicates whether additional attributes have been created for the product.") -+ image_label: FilterTypeInput @doc(description: "The label assigned to a product image.") -+ small_image_label: FilterTypeInput @doc(description: "The label assigned to a product's small image.") -+ thumbnail_label: FilterTypeInput @doc(description: "The label assigned to a product's thumbnail image.") -+ created_at: FilterTypeInput @doc(description: "Timestamp indicating when the product was created.") -+ updated_at: FilterTypeInput @doc(description: "Timestamp indicating when the product was updated.") -+ country_of_manufacture: FilterTypeInput @doc(description: "The product's country of origin.") -+ custom_layout: FilterTypeInput @doc(description: "The name of a custom layout.") -+ gift_message_available: FilterTypeInput @doc(description: "Indicates whether a gift message is available.") -+ or: ProductFilterInput @doc(description: "The keyword required to perform a logical OR comparison.") -+} -+ -+type ProductMediaGalleryEntriesContent @doc(description: "ProductMediaGalleryEntriesContent contains an image in base64 format and basic information about the image.") { -+ base64_encoded_data: String @doc(description: "The image in base64 format.") -+ type: String @doc(description: "The MIME type of the file, such as image/png.") -+ name: String @doc(description: "The file name of the image.") -+} -+ -+type ProductMediaGalleryEntriesVideoContent @doc(description: "ProductMediaGalleryEntriesVideoContent contains a link to a video file and basic information about the video.") { -+ media_type: String @doc(description: "Must be external-video.") -+ video_provider: String @doc(description: "Describes the video source.") -+ video_url: String @doc(description: "The URL to the video.") -+ video_title: String @doc(description: "The title of the video.") -+ video_description: String @doc(description: "A description of the video.") -+ video_metadata: String @doc(description: "Optional data about the video.") -+} -+ -+input ProductSortInput @doc(description: "ProductSortInput specifies the attribute to use for sorting search results and indicates whether the results are sorted in ascending or descending order.") { - name: SortEnum @doc(description: "The product name. Customers use this name to identify the product.") -- sku: SortEnum @doc(description: "A number or code assigned to a product to identify the product, options, price, and manufacturer") -+ sku: SortEnum @doc(description: "A number or code assigned to a product to identify the product, options, price, and manufacturer.") - description: SortEnum @doc(description: "Detailed information about the product. The value can include simple HTML tags.") - short_description: SortEnum @doc(description: "A short description of the product. Its use depends on the theme.") -- price: SortEnum @doc(description: "The price of the item") -- special_price: SortEnum @doc(description: "The discounted price of the product") -- special_from_date: SortEnum @doc(description: "The beginning date that a product has a special price") -- special_to_date: SortEnum @doc(description: "The end date that a product has a special price") -- weight: SortEnum @doc(description: "The weight of the item, in units defined by the store") -- manufacturer: SortEnum @doc(description: "A number representing the product's manufacturer") -- meta_title: SortEnum @doc(description: "A string that is displayed in the title bar and tab of the browser and in search results lists") -- meta_keyword: SortEnum @doc(description: "A comma-separated list of keywords that are visible only to search engines") -- meta_description: SortEnum @doc(description: "A brief overview of the product for search results listings, maximum 255 characters") -- image: SortEnum @doc(description: "The relative path to the main image on the product page") -- small_image: SortEnum @doc(description: "The relative path to the small image, which is used on catalog pages") -- thumbnail: SortEnum @doc(description: "The relative path to the product's thumbnail image") -- tier_price: SortEnum @doc(description: "The price when tier pricing is in effect and the items purchased threshold has been reached") -- news_from_date: SortEnum @doc(description: "The beginning date for new product listings, and determines if the product is featured as a new product") -- news_to_date: SortEnum @doc(description: "The end date for new product listings") -- custom_layout_update: SortEnum @doc(description: "XML code that is applied as a layout update to the product page") -- options_container: SortEnum @doc(description: "If the product has multiple options, determines where they appear on the product page") -- required_options: SortEnum @doc(description: "Indicates whether the product has required options") -- has_options: SortEnum @doc(description: "Indicates whether additional attributes have been created for the product") -- image_label: SortEnum @doc(description: "The label assigned to a product image") -- small_image_label: SortEnum @doc(description: "The label assigned to a product's small image") -- thumbnail_label: SortEnum @doc(description: "The label assigned to a product's thumbnail image") -- created_at: SortEnum @doc(description: "Timestamp indicating when the product was created") -- updated_at: SortEnum @doc(description: "Timestamp indicating when the product was updated") -- country_of_manufacture: SortEnum @doc(description: "The product's country of origin") -- custom_layout: SortEnum @doc(description: "The name of a custom layout") -- gift_message_available: SortEnum @doc(description: "Indicates whether a gift message is available") --} -- --type MediaGalleryEntry @doc(description: "MediaGalleryEntry defines characteristics about images and videos associated with a specific product") { -- id: Int @doc(description: "The identifier assigned to the object") -- media_type: String @doc(description: "image or video") -- label: String @doc(description: "The alt text displayed on the UI when the user points to the image") -- position: Int @doc(description: "The media item's position after it has been sorted") -- disabled: Boolean @doc(description: "Whether the image is hidden from vie") -- types: [String] @doc(description: "Array of image types. It can have the following values: image, small_image, thumbnail") -- file: String @doc(description: "The path of the image on the server") -- content: ProductMediaGalleryEntriesContent @doc(description: "Contains a ProductMediaGalleryEntriesContent object") -- video_content: ProductMediaGalleryEntriesVideoContent @doc(description: "Contains a ProductMediaGalleryEntriesVideoContent object") -+ price: SortEnum @doc(description: "The price of the item.") -+ special_price: SortEnum @doc(description: "The discounted price of the product.") -+ special_from_date: SortEnum @doc(description: "The beginning date that a product has a special price.") -+ special_to_date: SortEnum @doc(description: "The end date that a product has a special price.") -+ weight: SortEnum @doc(description: "The weight of the item, in units defined by the store.") -+ manufacturer: SortEnum @doc(description: "A number representing the product's manufacturer.") -+ meta_title: SortEnum @doc(description: "A string that is displayed in the title bar and tab of the browser and in search results lists.") -+ meta_keyword: SortEnum @doc(description: "A comma-separated list of keywords that are visible only to search engines.") -+ meta_description: SortEnum @doc(description: "A brief overview of the product for search results listings, maximum 255 characters.") -+ image: SortEnum @doc(description: "The relative path to the main image on the product page.") -+ small_image: SortEnum @doc(description: "The relative path to the small image, which is used on catalog pages.") -+ thumbnail: SortEnum @doc(description: "The relative path to the product's thumbnail image.") -+ tier_price: SortEnum @doc(description: "The price when tier pricing is in effect and the items purchased threshold has been reached.") -+ news_from_date: SortEnum @doc(description: "The beginning date for new product listings, and determines if the product is featured as a new product.") -+ news_to_date: SortEnum @doc(description: "The end date for new product listings.") -+ custom_layout_update: SortEnum @doc(description: "XML code that is applied as a layout update to the product page.") -+ options_container: SortEnum @doc(description: "If the product has multiple options, determines where they appear on the product page.") -+ required_options: SortEnum @doc(description: "Indicates whether the product has required options.") -+ has_options: SortEnum @doc(description: "Indicates whether additional attributes have been created for the product.") -+ image_label: SortEnum @doc(description: "The label assigned to a product image.") -+ small_image_label: SortEnum @doc(description: "The label assigned to a product's small image.") -+ thumbnail_label: SortEnum @doc(description: "The label assigned to a product's thumbnail image.") -+ created_at: SortEnum @doc(description: "Timestamp indicating when the product was created.") -+ updated_at: SortEnum @doc(description: "Timestamp indicating when the product was updated.") -+ country_of_manufacture: SortEnum @doc(description: "The product's country of origin.") -+ custom_layout: SortEnum @doc(description: "The name of a custom layout.") -+ gift_message_available: SortEnum @doc(description: "Indicates whether a gift message is available.") -+} -+ -+type MediaGalleryEntry @doc(description: "MediaGalleryEntry defines characteristics about images and videos associated with a specific product.") { -+ id: Int @doc(description: "The identifier assigned to the object.") -+ media_type: String @doc(description: "image or video.") -+ label: String @doc(description: "The alt text displayed on the UI when the user points to the image.") -+ position: Int @doc(description: "The media item's position after it has been sorted.") -+ disabled: Boolean @doc(description: "Whether the image is hidden from vie.") -+ types: [String] @doc(description: "Array of image types. It can have the following values: image, small_image, thumbnail.") -+ file: String @doc(description: "The path of the image on the server.") -+ content: ProductMediaGalleryEntriesContent @doc(description: "Contains a ProductMediaGalleryEntriesContent object.") -+ video_content: ProductMediaGalleryEntriesVideoContent @doc(description: "Contains a ProductMediaGalleryEntriesVideoContent object.") - } - - type LayerFilter { -- name: String @doc(description: "Layered navigation filter name") -- request_var: String @doc(description: "Request variable name for filter query") -- filter_items_count: Int @doc(description: "Count of filter items in filter group") -- filter_items: [LayerFilterItemInterface] @doc(description: "Array of filter items") -+ name: String @doc(description: "Layered navigation filter name.") -+ request_var: String @doc(description: "Request variable name for filter query.") -+ filter_items_count: Int @doc(description: "Count of filter items in filter group.") -+ filter_items: [LayerFilterItemInterface] @doc(description: "Array of filter items.") - } - - interface LayerFilterItemInterface @typeResolver(class: "Magento\\CatalogGraphQl\\Model\\LayerFilterItemTypeResolverComposite") { -- label: String @doc(description: "Filter label") -- value_string: String @doc(description: "Value for filter request variable to be used in query") -- items_count: Int @doc(description: "Count of items by filter") -+ label: String @doc(description: "Filter label.") -+ value_string: String @doc(description: "Value for filter request variable to be used in query.") -+ items_count: Int @doc(description: "Count of items by filter.") - } - - type LayerFilterItem implements LayerFilterItemInterface { -@@ -543,11 +393,23 @@ type LayerFilterItem implements LayerFilterItemInterface { - } - - type SortField { -- value: String @doc(description: "Attribute code of sort field") -- label: String @doc(description: "Label of sort field") -+ value: String @doc(description: "Attribute code of sort field.") -+ label: String @doc(description: "Label of sort field.") -+} -+ -+type SortFields @doc(description: "SortFields contains a default value for sort fields and all available sort fields.") { -+ default: String @doc(description: "Default value of sort fields.") -+ options: [SortField] @doc(description: "Available sort fields.") - } - --type SortFields @doc(description: "SortFields contains a default value for sort fields and all available sort fields") { -- default: String @doc(description: "Default value of sort fields") -- options: [SortField] @doc(description: "Available sort fields") -+type StoreConfig @doc(description: "The type contains information about a store config.") { -+ product_url_suffix : String @doc(description: "Product URL Suffix.") -+ category_url_suffix : String @doc(description: "Category URL Suffix.") -+ title_separator : String @doc(description: "Page Title Separator.") -+ list_mode : String @doc(description: "List Mode.") -+ grid_per_page_values : String @doc(description: "Products per Page on Grid Allowed Values.") -+ list_per_page_values : String @doc(description: "Products per Page on List Allowed Values.") -+ grid_per_page : Int @doc(description: "Products per Page on Grid Default Value.") -+ list_per_page : Int @doc(description: "Products per Page on List Default Value.") -+ catalog_default_sort_by : String @doc(description: "Default Sort By.") - } -diff --git a/app/code/Magento/CatalogImportExport/Model/Export/Product.php b/app/code/Magento/CatalogImportExport/Model/Export/Product.php -index 23aa8d65ddb..428c61c7fec 100644 ---- a/app/code/Magento/CatalogImportExport/Model/Export/Product.php -+++ b/app/code/Magento/CatalogImportExport/Model/Export/Product.php -@@ -20,6 +20,7 @@ use Magento\Catalog\Model\Product as ProductEntity; - * @SuppressWarnings(PHPMD.TooManyFields) - * @SuppressWarnings(PHPMD.ExcessiveClassComplexity) - * @SuppressWarnings(PHPMD.CouplingBetweenObjects) -+ * @SuppressWarnings(PHPMD.ExcessiveParameterList) - * @since 100.0.2 - */ - class Product extends \Magento\ImportExport\Model\Export\Entity\AbstractEntity -@@ -349,12 +350,14 @@ class Product extends \Magento\ImportExport\Model\Export\Entity\AbstractEntity - private $productEntityLinkField; - - /** -+ * Product constructor. -+ * - * @param \Magento\Framework\Stdlib\DateTime\TimezoneInterface $localeDate - * @param \Magento\Eav\Model\Config $config - * @param \Magento\Framework\App\ResourceConnection $resource - * @param \Magento\Store\Model\StoreManagerInterface $storeManager - * @param \Psr\Log\LoggerInterface $logger -- * @param \Magento\Catalog\Model\ResourceModel\Product\Collection $collection -+ * @param \Magento\Catalog\Model\ResourceModel\Product\CollectionFactory $collectionFactory - * @param \Magento\ImportExport\Model\Export\ConfigInterface $exportConfig - * @param \Magento\Catalog\Model\ResourceModel\ProductFactory $productFactory - * @param \Magento\Eav\Model\ResourceModel\Entity\Attribute\Set\CollectionFactory $attrSetColFactory -@@ -363,10 +366,10 @@ class Product extends \Magento\ImportExport\Model\Export\Entity\AbstractEntity - * @param \Magento\Catalog\Model\ResourceModel\Product\Option\CollectionFactory $optionColFactory - * @param \Magento\Catalog\Model\ResourceModel\Product\Attribute\CollectionFactory $attributeColFactory - * @param Product\Type\Factory $_typeFactory -- * @param \Magento\Catalog\Model\Product\LinkTypeProvider $linkTypeProvider -- * @param \Magento\CatalogImportExport\Model\Export\RowCustomizerInterface $rowCustomizer -+ * @param ProductEntity\LinkTypeProvider $linkTypeProvider -+ * @param RowCustomizerInterface $rowCustomizer - * @param array $dateAttrCodes -- * @SuppressWarnings(PHPMD.ExcessiveParameterList) -+ * @throws \Magento\Framework\Exception\LocalizedException - */ - public function __construct( - \Magento\Framework\Stdlib\DateTime\TimezoneInterface $localeDate, -@@ -441,8 +444,11 @@ class Product extends \Magento\ImportExport\Model\Export\Entity\AbstractEntity - if ($pathSize > 1) { - $path = []; - for ($i = 1; $i < $pathSize; $i++) { -- $name = $collection->getItemById($structure[$i])->getName(); -- $path[] = $this->quoteCategoryDelimiter($name); -+ $childCategory = $collection->getItemById($structure[$i]); -+ if ($childCategory) { -+ $name = $childCategory->getName(); -+ $path[] = $this->quoteCategoryDelimiter($name); -+ } - } - $this->_rootCategories[$category->getId()] = array_shift($path); - if ($pathSize > 2) { -@@ -520,10 +526,13 @@ class Product extends \Magento\ImportExport\Model\Export\Entity\AbstractEntity - if (empty($productIds)) { - return []; - } -+ -+ $productEntityJoinField = $this->getProductEntityLinkField(); -+ - $select = $this->_connection->select()->from( - ['mgvte' => $this->_resourceModel->getTableName('catalog_product_entity_media_gallery_value_to_entity')], - [ -- "mgvte.{$this->getProductEntityLinkField()}", -+ "mgvte.$productEntityJoinField", - 'mgvte.value_id' - ] - )->joinLeft( -@@ -535,7 +544,7 @@ class Product extends \Magento\ImportExport\Model\Export\Entity\AbstractEntity - ] - )->joinLeft( - ['mgv' => $this->_resourceModel->getTableName('catalog_product_entity_media_gallery_value')], -- '(mg.value_id = mgv.value_id)', -+ "(mg.value_id = mgv.value_id) and (mgvte.$productEntityJoinField = mgv.$productEntityJoinField)", - [ - 'mgv.label', - 'mgv.position', -@@ -543,14 +552,14 @@ class Product extends \Magento\ImportExport\Model\Export\Entity\AbstractEntity - 'mgv.store_id', - ] - )->where( -- "mgvte.{$this->getProductEntityLinkField()} IN (?)", -+ "mgvte.$productEntityJoinField IN (?)", - $productIds - ); - - $rowMediaGallery = []; - $stmt = $this->_connection->query($select); - while ($mediaRow = $stmt->fetch()) { -- $rowMediaGallery[$mediaRow[$this->getProductEntityLinkField()]][] = [ -+ $rowMediaGallery[$mediaRow[$productEntityJoinField]][] = [ - '_media_attribute_id' => $mediaRow['attribute_id'], - '_media_image' => $mediaRow['filename'], - '_media_label' => $mediaRow['label'], -@@ -667,8 +676,8 @@ class Product extends \Magento\ImportExport\Model\Export\Entity\AbstractEntity - /** - * Update data row with information about categories. Return true, if data row was updated - * -- * @param array &$dataRow -- * @param array &$rowCategories -+ * @param array $dataRow -+ * @param array $rowCategories - * @param int $productId - * @return bool - */ -@@ -692,7 +701,9 @@ class Product extends \Magento\ImportExport\Model\Export\Entity\AbstractEntity - } - - /** -- * {@inheritdoc} -+ * Get header columns -+ * -+ * @return string[] - */ - public function _getHeaderColumns() - { -@@ -751,7 +762,10 @@ class Product extends \Magento\ImportExport\Model\Export\Entity\AbstractEntity - } - - /** -- * {@inheritdoc} -+ * Get entity collection -+ * -+ * @param bool $resetCollection -+ * @return \Magento\Framework\Data\Collection\AbstractDb - */ - protected function _getEntityCollection($resetCollection = false) - { -@@ -796,7 +810,7 @@ class Product extends \Magento\ImportExport\Model\Export\Entity\AbstractEntity - // Maximal Products limit - $maxProductsLimit = 5000; - -- $this->_itemsPerPage = intval( -+ $this->_itemsPerPage = (int)( - ($memoryLimit * $memoryUsagePercent - memory_get_usage(true)) / $memoryPerProduct - ); - if ($this->_itemsPerPage < $minProductsLimit) { -@@ -829,6 +843,7 @@ class Product extends \Magento\ImportExport\Model\Export\Entity\AbstractEntity - public function export() - { - //Execution time may be very long -+ // phpcs:ignore Magento2.Functions.DiscouragedFunction - set_time_limit(0); - - $writer = $this->getWriter(); -@@ -858,7 +873,10 @@ class Product extends \Magento\ImportExport\Model\Export\Entity\AbstractEntity - } - - /** -- * {@inheritdoc} -+ * Apply filter to collection and add not skipped attributes to select. -+ * -+ * @param \Magento\Eav\Model\Entity\Collection\AbstractCollection $collection -+ * @return \Magento\Eav\Model\Entity\Collection\AbstractCollection - * @since 100.2.0 - */ - protected function _prepareEntityCollection(\Magento\Eav\Model\Entity\Collection\AbstractCollection $collection) -@@ -920,8 +938,7 @@ class Product extends \Magento\ImportExport\Model\Export\Entity\AbstractEntity - } - - /** -- * Load products' data from the collection -- * and filter it (if needed). -+ * Load products' data from the collection and filter it (if needed). - * - * @return array Keys are product IDs, values arrays with keys as store IDs - * and values as store-specific versions of Product entity. -@@ -929,15 +946,17 @@ class Product extends \Magento\ImportExport\Model\Export\Entity\AbstractEntity - protected function loadCollection(): array - { - $data = []; -- - $collection = $this->_getEntityCollection(); - foreach (array_keys($this->_storeIdToCode) as $storeId) { -+ $collection->setOrder('entity_id', 'asc'); -+ $this->_prepareEntityCollection($collection); - $collection->setStoreId($storeId); -+ $collection->load(); - foreach ($collection as $itemId => $item) { - $data[$itemId][$storeId] = $item; - } -+ $collection->clear(); - } -- $collection->clear(); - - return $data; - } -@@ -948,6 +967,7 @@ class Product extends \Magento\ImportExport\Model\Export\Entity\AbstractEntity - * @return array - * @SuppressWarnings(PHPMD.CyclomaticComplexity) - * @SuppressWarnings(PHPMD.NPathComplexity) -+ * phpcs:disable Generic.Metrics.NestingLevel - */ - protected function collectRawData() - { -@@ -1042,6 +1062,7 @@ class Product extends \Magento\ImportExport\Model\Export\Entity\AbstractEntity - - return $data; - } -+ //phpcs:enable Generic.Metrics.NestingLevel - - /** - * Wrap values with double quotes if "Fields Enclosure" option is enabled -@@ -1063,6 +1084,8 @@ class Product extends \Magento\ImportExport\Model\Export\Entity\AbstractEntity - } - - /** -+ * Collect multi raw data from -+ * - * @return array - */ - protected function collectMultirawData() -@@ -1104,6 +1127,8 @@ class Product extends \Magento\ImportExport\Model\Export\Entity\AbstractEntity - } - - /** -+ * Check the current data has multiselect value -+ * - * @param \Magento\Catalog\Model\Product $item - * @param int $storeId - * @return bool -@@ -1116,6 +1141,8 @@ class Product extends \Magento\ImportExport\Model\Export\Entity\AbstractEntity - } - - /** -+ * Collect multiselect values based on value -+ * - * @param \Magento\Catalog\Model\Product $item - * @param string $attrCode - * @param int $storeId -@@ -1140,6 +1167,8 @@ class Product extends \Magento\ImportExport\Model\Export\Entity\AbstractEntity - } - - /** -+ * Check attribute is valid. -+ * - * @param string $code - * @param mixed $value - * @return bool -@@ -1155,10 +1184,16 @@ class Product extends \Magento\ImportExport\Model\Export\Entity\AbstractEntity - $isValid = false; - } - -+ if (is_array($value)) { -+ $isValid = false; -+ } -+ - return $isValid; - } - - /** -+ * Append multi row data -+ * - * @param array $dataRow - * @param array $multiRawData - * @return array -@@ -1268,11 +1303,23 @@ class Product extends \Magento\ImportExport\Model\Export\Entity\AbstractEntity - } - - if (!empty($multiRawData['customOptionsData'][$productLinkId][$storeId])) { -+ $shouldBeMerged = true; - $customOptionsRows = $multiRawData['customOptionsData'][$productLinkId][$storeId]; -- $multiRawData['customOptionsData'][$productLinkId][$storeId] = []; -- $customOptions = implode(ImportProduct::PSEUDO_MULTI_LINE_SEPARATOR, $customOptionsRows); - -- $dataRow = array_merge($dataRow, ['custom_options' => $customOptions]); -+ if ($storeId != Store::DEFAULT_STORE_ID -+ && !empty($multiRawData['customOptionsData'][$productLinkId][Store::DEFAULT_STORE_ID]) -+ ) { -+ $defaultCustomOptions = $multiRawData['customOptionsData'][$productLinkId][Store::DEFAULT_STORE_ID]; -+ if (!array_diff($defaultCustomOptions, $customOptionsRows)) { -+ $shouldBeMerged = false; -+ } -+ } -+ -+ if ($shouldBeMerged) { -+ $multiRawData['customOptionsData'][$productLinkId][$storeId] = []; -+ $customOptions = implode(ImportProduct::PSEUDO_MULTI_LINE_SEPARATOR, $customOptionsRows); -+ $dataRow = array_merge($dataRow, ['custom_options' => $customOptions]); -+ } - } - - if (empty($dataRow)) { -@@ -1288,6 +1335,8 @@ class Product extends \Magento\ImportExport\Model\Export\Entity\AbstractEntity - } - - /** -+ * Add multi row data to export -+ * - * @deprecated 100.1.0 - * @param array $dataRow - * @param array $multiRawData -@@ -1335,6 +1384,8 @@ class Product extends \Magento\ImportExport\Model\Export\Entity\AbstractEntity - } - - /** -+ * Convert option row to cell string -+ * - * @param array $option - * @return string - */ -@@ -1364,6 +1415,7 @@ class Product extends \Magento\ImportExport\Model\Export\Entity\AbstractEntity - protected function getCustomOptionsData($productIds) - { - $customOptionsData = []; -+ $defaultOptionsData = []; - - foreach (array_keys($this->_storeIdToCode) as $storeId) { - $options = $this->_optionColFactory->create(); -@@ -1376,38 +1428,42 @@ class Product extends \Magento\ImportExport\Model\Export\Entity\AbstractEntity - ->addValuesToResult($storeId); - - foreach ($options as $option) { -+ $optionData = $option->toArray(); - $row = []; - $productId = $option['product_id']; - $row['name'] = $option['title']; - $row['type'] = $option['type']; -- if (Store::DEFAULT_STORE_ID === $storeId) { -- $row['required'] = $option['is_require']; -- $row['price'] = $option['price']; -- $row['price_type'] = ($option['price_type'] === 'percent') ? 'percent' : 'fixed'; -- $row['sku'] = $option['sku']; -- if ($option['max_characters']) { -- $row['max_characters'] = $option['max_characters']; -- } - -- foreach (['file_extension', 'image_size_x', 'image_size_y'] as $fileOptionKey) { -- if (!isset($option[$fileOptionKey])) { -- continue; -- } -- -- $row[$fileOptionKey] = $option[$fileOptionKey]; -+ $row['required'] = $this->getOptionValue('is_require', $defaultOptionsData, $optionData); -+ $row['price'] = $this->getOptionValue('price', $defaultOptionsData, $optionData); -+ $row['sku'] = $this->getOptionValue('sku', $defaultOptionsData, $optionData); -+ if (array_key_exists('max_characters', $optionData) -+ || array_key_exists('max_characters', $defaultOptionsData) -+ ) { -+ $row['max_characters'] = $this->getOptionValue('max_characters', $defaultOptionsData, $optionData); -+ } -+ foreach (['file_extension', 'image_size_x', 'image_size_y'] as $fileOptionKey) { -+ if (isset($option[$fileOptionKey]) || isset($defaultOptionsData[$fileOptionKey])) { -+ $row[$fileOptionKey] = $this->getOptionValue($fileOptionKey, $defaultOptionsData, $optionData); - } - } -+ $percentType = $this->getOptionValue('price_type', $defaultOptionsData, $optionData); -+ $row['price_type'] = ($percentType === 'percent') ? 'percent' : 'fixed'; -+ -+ if (Store::DEFAULT_STORE_ID === $storeId) { -+ $optionId = $option['option_id']; -+ $defaultOptionsData[$optionId] = $option->toArray(); -+ } -+ - $values = $option->getValues(); - - if ($values) { - foreach ($values as $value) { - $row['option_title'] = $value['title']; -- if (Store::DEFAULT_STORE_ID === $storeId) { -- $row['option_title'] = $value['title']; -- $row['price'] = $value['price']; -- $row['price_type'] = ($value['price_type'] === 'percent') ? 'percent' : 'fixed'; -- $row['sku'] = $value['sku']; -- } -+ $row['option_title'] = $value['title']; -+ $row['price'] = $value['price']; -+ $row['price_type'] = ($value['price_type'] === 'percent') ? 'percent' : 'fixed'; -+ $row['sku'] = $value['sku']; - $customOptionsData[$productId][$storeId][] = $this->optionRowToCellString($row); - } - } else { -@@ -1421,6 +1477,31 @@ class Product extends \Magento\ImportExport\Model\Export\Entity\AbstractEntity - return $customOptionsData; - } - -+ /** -+ * Get value for custom option according to store or default value -+ * -+ * @param string $optionName -+ * @param array $defaultOptionsData -+ * @param array $optionData -+ * @return mixed -+ */ -+ private function getOptionValue($optionName, $defaultOptionsData, $optionData) -+ { -+ $optionId = $optionData['option_id']; -+ -+ if (array_key_exists($optionName, $optionData) && $optionData[$optionName] !== null) { -+ return $optionData[$optionName]; -+ } -+ -+ if (array_key_exists($optionId, $defaultOptionsData) -+ && array_key_exists($optionName, $defaultOptionsData[$optionId]) -+ ) { -+ return $defaultOptionsData[$optionId][$optionName]; -+ } -+ -+ return null; -+ } -+ - /** - * Clean up already loaded attribute collection. - * -diff --git a/app/code/Magento/CatalogImportExport/Model/Import/Product.php b/app/code/Magento/CatalogImportExport/Model/Import/Product.php -index 6c3bef2a52b..5c083d421f0 100644 ---- a/app/code/Magento/CatalogImportExport/Model/Import/Product.php -+++ b/app/code/Magento/CatalogImportExport/Model/Import/Product.php -@@ -3,18 +3,22 @@ - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -+ - namespace Magento\CatalogImportExport\Model\Import; - -+use Magento\Catalog\Api\ProductRepositoryInterface; - use Magento\Catalog\Model\Config as CatalogConfig; - use Magento\Catalog\Model\Product\Visibility; --use Magento\CatalogImportExport\Model\Import\Product\MediaGalleryProcessor; -+use Magento\Catalog\Model\ResourceModel\Product\Link; - use Magento\CatalogImportExport\Model\Import\Product\ImageTypeProcessor; -+use Magento\CatalogImportExport\Model\Import\Product\MediaGalleryProcessor; - use Magento\CatalogImportExport\Model\Import\Product\RowValidatorInterface as ValidatorInterface; --use Magento\CatalogInventory\Api\Data\StockItemInterface; - use Magento\CatalogImportExport\Model\StockItemImporterInterface; -+use Magento\CatalogInventory\Api\Data\StockItemInterface; - use Magento\Framework\App\Filesystem\DirectoryList; - use Magento\Framework\App\ObjectManager; - use Magento\Framework\Exception\LocalizedException; -+use Magento\Framework\Exception\NoSuchEntityException; - use Magento\Framework\Filesystem; - use Magento\Framework\Intl\DateTimeFactory; - use Magento\Framework\Model\ResourceModel\Db\ObjectRelationProcessor; -@@ -34,6 +38,7 @@ use Magento\Store\Model\Store; - * @SuppressWarnings(PHPMD.TooManyFields) - * @SuppressWarnings(PHPMD.ExcessiveClassComplexity) - * @SuppressWarnings(PHPMD.CouplingBetweenObjects) -+ * @SuppressWarnings(PHPMD.ExcessivePublicCount) - * @since 100.0.2 - */ - class Product extends \Magento\ImportExport\Model\Import\Entity\AbstractEntity -@@ -295,7 +300,8 @@ class Product extends \Magento\ImportExport\Model\Import\Entity\AbstractEntity - ValidatorInterface::ERROR_MEDIA_URL_NOT_ACCESSIBLE => 'Imported resource (image) could not be downloaded from external resource due to timeout or access permissions', - ValidatorInterface::ERROR_INVALID_WEIGHT => 'Product weight is invalid', - ValidatorInterface::ERROR_DUPLICATE_URL_KEY => 'Url key: \'%s\' was already generated for an item with the SKU: \'%s\'. You need to specify the unique URL key manually', -- ValidatorInterface::ERROR_DUPLICATE_MULTISELECT_VALUES => "Value for multiselect attribute %s contains duplicated values", -+ ValidatorInterface::ERROR_DUPLICATE_MULTISELECT_VALUES => 'Value for multiselect attribute %s contains duplicated values', -+ 'invalidNewToDateValue' => 'Make sure new_to_date is later than or the same as new_from_date', - ]; - //@codingStandardsIgnoreEnd - -@@ -732,6 +738,11 @@ class Product extends \Magento\ImportExport\Model\Import\Entity\AbstractEntity - */ - private $dateTimeFactory; - -+ /** -+ * @var ProductRepositoryInterface -+ */ -+ private $productRepository; -+ - /** - * @param \Magento\Framework\Json\Helper\Data $jsonHelper - * @param \Magento\ImportExport\Helper\Data $importExportData -@@ -776,7 +787,11 @@ class Product extends \Magento\ImportExport\Model\Import\Entity\AbstractEntity - * @param MediaGalleryProcessor $mediaProcessor - * @param StockItemImporterInterface|null $stockItemImporter - * @param DateTimeFactory $dateTimeFactory -+ * @param ProductRepositoryInterface|null $productRepository -+ * @throws LocalizedException -+ * @throws \Magento\Framework\Exception\FileSystemException - * @SuppressWarnings(PHPMD.ExcessiveParameterList) -+ * @SuppressWarnings(PHPMD.ExcessiveMethodLength) - */ - public function __construct( - \Magento\Framework\Json\Helper\Data $jsonHelper, -@@ -821,7 +836,8 @@ class Product extends \Magento\ImportExport\Model\Import\Entity\AbstractEntity - ImageTypeProcessor $imageTypeProcessor = null, - MediaGalleryProcessor $mediaProcessor = null, - StockItemImporterInterface $stockItemImporter = null, -- DateTimeFactory $dateTimeFactory = null -+ DateTimeFactory $dateTimeFactory = null, -+ ProductRepositoryInterface $productRepository = null - ) { - $this->_eventManager = $eventManager; - $this->stockRegistry = $stockRegistry; -@@ -875,6 +891,8 @@ class Product extends \Magento\ImportExport\Model\Import\Entity\AbstractEntity - ->initImagesArrayKeys(); - $this->validator->init($this); - $this->dateTimeFactory = $dateTimeFactory ?? ObjectManager::getInstance()->get(DateTimeFactory::class); -+ $this->productRepository = $productRepository ?? ObjectManager::getInstance() -+ ->get(ProductRepositoryInterface::class); - } - - /** -@@ -890,7 +908,7 @@ class Product extends \Magento\ImportExport\Model\Import\Entity\AbstractEntity - { - if (!$this->validator->isAttributeValid($attrCode, $attrParams, $rowData)) { - foreach ($this->validator->getMessages() as $message) { -- $this->addRowError($message, $rowNum, $attrCode); -+ $this->skipRow($rowNum, $message, ProcessingError::ERROR_LEVEL_NOT_CRITICAL, $attrCode); - } - return false; - } -@@ -898,8 +916,8 @@ class Product extends \Magento\ImportExport\Model\Import\Entity\AbstractEntity - } - - /** -- * - * Multiple value separator getter. -+ * - * @return string - */ - public function getMultipleValueSeparator() -@@ -949,6 +967,8 @@ class Product extends \Magento\ImportExport\Model\Import\Entity\AbstractEntity - } - - /** -+ * Retrieve product type by name. -+ * - * @param string $name - * @return Product\Type\AbstractType - */ -@@ -981,10 +1001,12 @@ class Product extends \Magento\ImportExport\Model\Import\Entity\AbstractEntity - */ - public function deleteProductsForReplacement() - { -- $this->setParameters(array_merge( -- $this->getParameters(), -- ['behavior' => Import::BEHAVIOR_DELETE] -- )); -+ $this->setParameters( -+ array_merge( -+ $this->getParameters(), -+ ['behavior' => Import::BEHAVIOR_DELETE] -+ ) -+ ); - $this->_deleteProducts(); - - return $this; -@@ -1073,10 +1095,12 @@ class Product extends \Magento\ImportExport\Model\Import\Entity\AbstractEntity - $this->deleteProductsForReplacement(); - $this->_oldSku = $this->skuProcessor->reloadOldSkus()->getOldSkus(); - $this->_validatedRows = null; -- $this->setParameters(array_merge( -- $this->getParameters(), -- ['behavior' => Import::BEHAVIOR_APPEND] -- )); -+ $this->setParameters( -+ array_merge( -+ $this->getParameters(), -+ ['behavior' => Import::BEHAVIOR_APPEND] -+ ) -+ ); - $this->_saveProductsData(); - - return $this; -@@ -1189,8 +1213,10 @@ class Product extends \Magento\ImportExport\Model\Import\Entity\AbstractEntity - } - - /** -- * Set valid attribute set and product type to rows with all scopes -- * to ensure that existing products doesn't changed. -+ * Set valid attribute set and product type to rows. -+ * -+ * Set valid attribute set and product type to rows with all -+ * scopes to ensure that existing products doesn't changed. - * - * @param array $rowData - * @return array -@@ -1220,22 +1246,21 @@ class Product extends \Magento\ImportExport\Model\Import\Entity\AbstractEntity - - /** - * Gather and save information about product links. -+ * - * Must be called after ALL products saving done. - * - * @return $this -- * @SuppressWarnings(PHPMD.CyclomaticComplexity) -- * @SuppressWarnings(PHPMD.NPathComplexity) -- * @SuppressWarnings(PHPMD.ExcessiveMethodLength) - */ - protected function _saveLinks() - { -+ /** @var Link $resource */ - $resource = $this->_linkFactory->create(); - $mainTable = $resource->getMainTable(); - $positionAttrId = []; - $nextLinkId = $this->_resourceHelper->getNextAutoincrement($mainTable); - - // pre-load 'position' attributes ID for each link type once -- foreach ($this->_linkNameToId as $linkName => $linkId) { -+ foreach ($this->_linkNameToId as $linkId) { - $select = $this->_connection->select()->from( - $resource->getTable('catalog_product_link_attribute'), - ['id' => 'product_link_attribute_id'] -@@ -1246,110 +1271,7 @@ class Product extends \Magento\ImportExport\Model\Import\Entity\AbstractEntity - $positionAttrId[$linkId] = $this->_connection->fetchOne($select, $bind); - } - while ($bunch = $this->_dataSourceModel->getNextBunch()) { -- $productIds = []; -- $linkRows = []; -- $positionRows = []; -- -- foreach ($bunch as $rowNum => $rowData) { -- if (!$this->isRowAllowedToImport($rowData, $rowNum)) { -- continue; -- } -- -- $sku = $rowData[self::COL_SKU]; -- -- $productId = $this->skuProcessor->getNewSku($sku)[$this->getProductEntityLinkField()]; -- $productLinkKeys = []; -- $select = $this->_connection->select()->from( -- $resource->getTable('catalog_product_link'), -- ['id' => 'link_id', 'linked_id' => 'linked_product_id', 'link_type_id' => 'link_type_id'] -- )->where( -- 'product_id = :product_id' -- ); -- $bind = [':product_id' => $productId]; -- foreach ($this->_connection->fetchAll($select, $bind) as $linkData) { -- $linkKey = "{$productId}-{$linkData['linked_id']}-{$linkData['link_type_id']}"; -- $productLinkKeys[$linkKey] = $linkData['id']; -- } -- foreach ($this->_linkNameToId as $linkName => $linkId) { -- $productIds[] = $productId; -- if (isset($rowData[$linkName . 'sku'])) { -- $linkSkus = explode($this->getMultipleValueSeparator(), $rowData[$linkName . 'sku']); -- $linkPositions = !empty($rowData[$linkName . 'position']) -- ? explode($this->getMultipleValueSeparator(), $rowData[$linkName . 'position']) -- : []; -- foreach ($linkSkus as $linkedKey => $linkedSku) { -- $linkedSku = trim($linkedSku); -- if (($this->skuProcessor->getNewSku($linkedSku) !== null || $this->isSkuExist($linkedSku)) -- && strcasecmp($linkedSku, $sku) !== 0 -- ) { -- $newSku = $this->skuProcessor->getNewSku($linkedSku); -- if (!empty($newSku)) { -- $linkedId = $newSku['entity_id']; -- } else { -- $linkedId = $this->getExistingSku($linkedSku)['entity_id']; -- } -- -- if ($linkedId == null) { -- // Import file links to a SKU which is skipped for some reason, -- // which leads to a "NULL" -- // link causing fatal errors. -- $this->_logger->critical( -- new \Exception( -- sprintf( -- 'WARNING: Orphaned link skipped: From SKU %s (ID %d) to SKU %s, ' . -- 'Link type id: %d', -- $sku, -- $productId, -- $linkedSku, -- $linkId -- ) -- ) -- ); -- continue; -- } -- -- $linkKey = "{$productId}-{$linkedId}-{$linkId}"; -- if (empty($productLinkKeys[$linkKey])) { -- $productLinkKeys[$linkKey] = $nextLinkId; -- } -- if (!isset($linkRows[$linkKey])) { -- $linkRows[$linkKey] = [ -- 'link_id' => $productLinkKeys[$linkKey], -- 'product_id' => $productId, -- 'linked_product_id' => $linkedId, -- 'link_type_id' => $linkId, -- ]; -- } -- if (!empty($linkPositions[$linkedKey])) { -- $positionRows[] = [ -- 'link_id' => $productLinkKeys[$linkKey], -- 'product_link_attribute_id' => $positionAttrId[$linkId], -- 'value' => $linkPositions[$linkedKey], -- ]; -- } -- $nextLinkId++; -- } -- } -- } -- } -- } -- if (Import::BEHAVIOR_APPEND != $this->getBehavior() && $productIds) { -- $this->_connection->delete( -- $mainTable, -- $this->_connection->quoteInto('product_id IN (?)', array_unique($productIds)) -- ); -- } -- if ($linkRows) { -- $this->_connection->insertOnDuplicate($mainTable, $linkRows, ['link_id']); -- } -- if ($positionRows) { -- // process linked product positions -- $this->_connection->insertOnDuplicate( -- $resource->getAttributeTypeTable('int'), -- $positionRows, -- ['value'] -- ); -- } -+ $this->processLinkBunches($bunch, $resource, $nextLinkId, $positionAttrId); - } - return $this; - } -@@ -1406,7 +1328,7 @@ class Product extends \Magento\ImportExport\Model\Import\Entity\AbstractEntity - $delProductId[] = $productId; - - foreach (array_keys($categories) as $categoryId) { -- $categoriesIn[] = ['product_id' => $productId, 'category_id' => $categoryId, 'position' => 1]; -+ $categoriesIn[] = ['product_id' => $productId, 'category_id' => $categoryId, 'position' => 0]; - } - } - if (Import::BEHAVIOR_APPEND != $this->getBehavior()) { -@@ -1468,6 +1390,7 @@ class Product extends \Magento\ImportExport\Model\Import\Entity\AbstractEntity - - /** - * Return additional data, needed to select. -+ * - * @return array - */ - private function getOldSkuFieldsForSelect() -@@ -1477,6 +1400,7 @@ class Product extends \Magento\ImportExport\Model\Import\Entity\AbstractEntity - - /** - * Adds newly created products to _oldSku -+ * - * @param array $newProducts - * @return void - */ -@@ -1514,6 +1438,7 @@ class Product extends \Magento\ImportExport\Model\Import\Entity\AbstractEntity - - /** - * Init media gallery resources -+ * - * @return void - * @since 100.0.4 - * @deprecated -@@ -1544,6 +1469,8 @@ class Product extends \Magento\ImportExport\Model\Import\Entity\AbstractEntity - } - - /** -+ * Retrieve image from row. -+ * - * @param array $rowData - * @return array - */ -@@ -1582,6 +1509,7 @@ class Product extends \Magento\ImportExport\Model\Import\Entity\AbstractEntity - * @SuppressWarnings(PHPMD.ExcessiveMethodLength) - * @SuppressWarnings(PHPMD.UnusedLocalVariable) - * @throws LocalizedException -+ * phpcs:disable Generic.Metrics.NestingLevel - */ - protected function _saveProducts() - { -@@ -1599,6 +1527,7 @@ class Product extends \Magento\ImportExport\Model\Import\Entity\AbstractEntity - $tierPrices = []; - $mediaGallery = []; - $labelsForUpdate = []; -+ $imagesForChangeVisibility = []; - $uploadedImages = []; - $previousType = null; - $prevAttributeSet = null; -@@ -1617,14 +1546,25 @@ class Product extends \Magento\ImportExport\Model\Import\Entity\AbstractEntity - } - $rowScope = $this->getRowScope($rowData); - -- $rowData[self::URL_KEY] = $this->getUrlKey($rowData); -+ $urlKey = $this->getUrlKey($rowData); -+ if (!empty($rowData[self::URL_KEY])) { -+ // If url_key column and its value were in the CSV file -+ $rowData[self::URL_KEY] = $urlKey; -+ } elseif ($this->isNeedToChangeUrlKey($rowData)) { -+ // If url_key column was empty or even not declared in the CSV file but by the rules it is need to -+ // be setteed. In case when url_key is generating from name column we have to ensure that the bunch -+ // of products will pass for the event with url_key column. -+ $bunch[$rowNum][self::URL_KEY] = $rowData[self::URL_KEY] = $urlKey; -+ } - - $rowSku = $rowData[self::COL_SKU]; - - if (null === $rowSku) { - $this->getErrorAggregator()->addRowToSkip($rowNum); - continue; -- } elseif (self::SCOPE_STORE == $rowScope) { -+ } -+ -+ if (self::SCOPE_STORE == $rowScope) { - // set necessary data from SCOPE_DEFAULT row - $rowData[self::COL_TYPE] = $this->skuProcessor->getNewSku($rowSku)['type_id']; - $rowData['attribute_set_id'] = $this->skuProcessor->getNewSku($rowSku)['attr_set_id']; -@@ -1687,6 +1627,14 @@ class Product extends \Magento\ImportExport\Model\Import\Entity\AbstractEntity - $websiteId = $this->storeResolver->getWebsiteCodeToId($websiteCode); - $this->websitesCache[$rowSku][$websiteId] = true; - } -+ } else { -+ $product = $this->retrieveProductBySku($rowSku); -+ if ($product) { -+ $websiteIds = $product->getWebsiteIds(); -+ foreach ($websiteIds as $websiteId) { -+ $this->websitesCache[$rowSku][$websiteId] = true; -+ } -+ } - } - - // 3. Categories phase -@@ -1718,21 +1666,24 @@ class Product extends \Magento\ImportExport\Model\Import\Entity\AbstractEntity - } - - // 5. Media gallery phase -- $disabledImages = []; - list($rowImages, $rowLabels) = $this->getImagesFromRow($rowData); - $storeId = !empty($rowData[self::COL_STORE]) - ? $this->getStoreIdByCode($rowData[self::COL_STORE]) - : Store::DEFAULT_STORE_ID; -- if (isset($rowData['_media_is_disabled']) && strlen(trim($rowData['_media_is_disabled']))) { -- $disabledImages = array_flip( -- explode($this->getMultipleValueSeparator(), $rowData['_media_is_disabled']) -- ); -+ $imageHiddenStates = $this->getImagesHiddenStates($rowData); -+ foreach (array_keys($imageHiddenStates) as $image) { -+ if (array_key_exists($rowSku, $existingImages) -+ && array_key_exists($image, $existingImages[$rowSku]) -+ ) { -+ $rowImages[self::COL_MEDIA_IMAGE][] = $image; -+ $uploadedImages[$image] = $image; -+ } -+ - if (empty($rowImages)) { -- foreach (array_keys($disabledImages) as $disabledImage) { -- $rowImages[self::COL_MEDIA_IMAGE][] = $disabledImage; -- } -+ $rowImages[self::COL_MEDIA_IMAGE][] = $image; - } - } -+ - $rowData[self::COL_MEDIA_IMAGE] = []; - - /* -@@ -1748,6 +1699,7 @@ class Product extends \Magento\ImportExport\Model\Import\Entity\AbstractEntity - if ($uploadedFile) { - $uploadedImages[$columnImage] = $uploadedFile; - } else { -+ unset($rowData[$column]); - $this->addRowError( - ValidatorInterface::ERROR_MEDIA_URL_NOT_ACCESSIBLE, - $rowNum, -@@ -1764,31 +1716,44 @@ class Product extends \Magento\ImportExport\Model\Import\Entity\AbstractEntity - $rowData[$column] = $uploadedFile; - } - -- if ($uploadedFile && !isset($mediaGallery[$storeId][$rowSku][$uploadedFile])) { -- if (isset($existingImages[$rowSku][$uploadedFile])) { -- if (isset($rowLabels[$column][$columnImageKey]) -- && $rowLabels[$column][$columnImageKey] != -- $existingImages[$rowSku][$uploadedFile]['label'] -- ) { -- $labelsForUpdate[] = [ -- 'label' => $rowLabels[$column][$columnImageKey], -- 'imageData' => $existingImages[$rowSku][$uploadedFile] -- ]; -- } -- } else { -- if ($column == self::COL_MEDIA_IMAGE) { -- $rowData[$column][] = $uploadedFile; -- } -- $mediaGallery[$storeId][$rowSku][$uploadedFile] = [ -- 'attribute_id' => $this->getMediaGalleryAttributeId(), -- 'label' => isset($rowLabels[$column][$columnImageKey]) -- ? $rowLabels[$column][$columnImageKey] -- : '', -- 'position' => ++$position, -- 'disabled' => isset($disabledImages[$columnImage]) ? '1' : '0', -- 'value' => $uploadedFile, -+ if (!$uploadedFile || isset($mediaGallery[$storeId][$rowSku][$uploadedFile])) { -+ continue; -+ } -+ -+ if (isset($existingImages[$rowSku][$uploadedFile])) { -+ $currentFileData = $existingImages[$rowSku][$uploadedFile]; -+ if (isset($rowLabels[$column][$columnImageKey]) -+ && $rowLabels[$column][$columnImageKey] != -+ $currentFileData['label'] -+ ) { -+ $labelsForUpdate[] = [ -+ 'label' => $rowLabels[$column][$columnImageKey], -+ 'imageData' => $currentFileData - ]; - } -+ -+ if (array_key_exists($uploadedFile, $imageHiddenStates) -+ && $currentFileData['disabled'] != $imageHiddenStates[$uploadedFile] -+ ) { -+ $imagesForChangeVisibility[] = [ -+ 'disabled' => $imageHiddenStates[$uploadedFile], -+ 'imageData' => $currentFileData -+ ]; -+ } -+ } else { -+ if ($column == self::COL_MEDIA_IMAGE) { -+ $rowData[$column][] = $uploadedFile; -+ } -+ $mediaGallery[$storeId][$rowSku][$uploadedFile] = [ -+ 'attribute_id' => $this->getMediaGalleryAttributeId(), -+ 'label' => isset($rowLabels[$column][$columnImageKey]) -+ ? $rowLabels[$column][$columnImageKey] -+ : '', -+ 'position' => ++$position, -+ 'disabled' => isset($imageHiddenStates[$columnImage]) -+ ? $imageHiddenStates[$columnImage] : '0', -+ 'value' => $uploadedFile, -+ ]; - } - } - } -@@ -1905,6 +1870,8 @@ class Product extends \Magento\ImportExport\Model\Import\Entity\AbstractEntity - $mediaGallery - )->_saveProductAttributes( - $attributes -+ )->updateMediaGalleryVisibility( -+ $imagesForChangeVisibility - )->updateMediaGalleryLabels( - $labelsForUpdate - ); -@@ -1919,6 +1886,34 @@ class Product extends \Magento\ImportExport\Model\Import\Entity\AbstractEntity - } - - /** -+ * Prepare array with image states (visible or hidden from product page) -+ * -+ * @param array $rowData -+ * @return array -+ */ -+ private function getImagesHiddenStates($rowData) -+ { -+ $statesArray = []; -+ $mappingArray = [ -+ '_media_is_disabled' => '1' -+ ]; -+ -+ foreach ($mappingArray as $key => $value) { -+ if (isset($rowData[$key]) && strlen(trim($rowData[$key]))) { -+ $items = explode($this->getMultipleValueSeparator(), $rowData[$key]); -+ -+ foreach ($items as $item) { -+ $statesArray[$item] = $value; -+ } -+ } -+ } -+ -+ return $statesArray; -+ } -+ -+ /** -+ * Resolve valid category ids from provided row data. -+ * - * @param array $rowData - * @return array - */ -@@ -1941,11 +1936,18 @@ class Product extends \Magento\ImportExport\Model\Import\Entity\AbstractEntity - . ' ' . $error['exception']->getMessage() - ); - } -+ } else { -+ $product = $this->retrieveProductBySku($rowData['sku']); -+ if ($product) { -+ $categoryIds = $product->getCategoryIds(); -+ } - } - return $categoryIds; - } - - /** -+ * Get product websites. -+ * - * @param string $productSku - * @return array - */ -@@ -1955,6 +1957,8 @@ class Product extends \Magento\ImportExport\Model\Import\Entity\AbstractEntity - } - - /** -+ * Retrieve product categories. -+ * - * @param string $productSku - * @return array - */ -@@ -1964,6 +1968,8 @@ class Product extends \Magento\ImportExport\Model\Import\Entity\AbstractEntity - } - - /** -+ * Get store id by code. -+ * - * @param string $storeCode - * @return array|int|null|string - */ -@@ -2055,6 +2061,8 @@ class Product extends \Magento\ImportExport\Model\Import\Entity\AbstractEntity - } - - /** -+ * Retrieve uploader. -+ * - * @return Uploader - * @throws \Magento\Framework\Exception\LocalizedException - */ -@@ -2065,6 +2073,7 @@ class Product extends \Magento\ImportExport\Model\Import\Entity\AbstractEntity - - /** - * Uploading files into the "catalog/product" media folder. -+ * - * Return a new file name if the same file is already exists. - * - * @param string $fileName -@@ -2259,7 +2268,7 @@ class Product extends \Magento\ImportExport\Model\Import\Entity\AbstractEntity - * Returns array of new products data with SKU as key. All SKU keys are in lowercase for avoiding creation of - * new products with the same SKU in different letter cases. - * -- * @var string $sku -+ * @param string $sku - * @return array - */ - public function getNewSku($sku = null) -@@ -2323,6 +2332,7 @@ class Product extends \Magento\ImportExport\Model\Import\Entity\AbstractEntity - * @SuppressWarnings(PHPMD.CyclomaticComplexity) - * @SuppressWarnings(PHPMD.NPathComplexity) - * @SuppressWarnings(PHPMD.ExcessiveMethodLength) -+ * @throws \Zend_Validate_Exception - */ - public function validateRow(array $rowData, $rowNum) - { -@@ -2338,32 +2348,35 @@ class Product extends \Magento\ImportExport\Model\Import\Entity\AbstractEntity - // BEHAVIOR_DELETE and BEHAVIOR_REPLACE use specific validation logic - if (Import::BEHAVIOR_REPLACE == $this->getBehavior()) { - if (self::SCOPE_DEFAULT == $rowScope && !$this->isSkuExist($sku)) { -- $this->addRowError(ValidatorInterface::ERROR_SKU_NOT_FOUND_FOR_DELETE, $rowNum); -+ $this->skipRow($rowNum, ValidatorInterface::ERROR_SKU_NOT_FOUND_FOR_DELETE); - return false; - } - } - if (Import::BEHAVIOR_DELETE == $this->getBehavior()) { - if (self::SCOPE_DEFAULT == $rowScope && !$this->isSkuExist($sku)) { -- $this->addRowError(ValidatorInterface::ERROR_SKU_NOT_FOUND_FOR_DELETE, $rowNum); -+ $this->skipRow($rowNum, ValidatorInterface::ERROR_SKU_NOT_FOUND_FOR_DELETE); - return false; - } - return true; - } - -+ // if product doesn't exist, need to throw critical error else all errors should be not critical. -+ $errorLevel = $this->getValidationErrorLevel($sku); -+ - if (!$this->validator->isValid($rowData)) { - foreach ($this->validator->getMessages() as $message) { -- $this->addRowError($message, $rowNum, $this->validator->getInvalidAttribute()); -+ $this->skipRow($rowNum, $message, $errorLevel, $this->validator->getInvalidAttribute()); - } - } - - if (null === $sku) { -- $this->addRowError(ValidatorInterface::ERROR_SKU_IS_EMPTY, $rowNum); -+ $this->skipRow($rowNum, ValidatorInterface::ERROR_SKU_IS_EMPTY, $errorLevel); - } elseif (false === $sku) { -- $this->addRowError(ValidatorInterface::ERROR_ROW_IS_ORPHAN, $rowNum); -+ $this->skipRow($rowNum, ValidatorInterface::ERROR_ROW_IS_ORPHAN, $errorLevel); - } elseif (self::SCOPE_STORE == $rowScope - && !$this->storeResolver->getStoreCodeToId($rowData[self::COL_STORE]) - ) { -- $this->addRowError(ValidatorInterface::ERROR_INVALID_STORE, $rowNum); -+ $this->skipRow($rowNum, ValidatorInterface::ERROR_INVALID_STORE, $errorLevel); - } - - // SKU is specified, row is SCOPE_DEFAULT, new product block begins -@@ -2378,16 +2391,15 @@ class Product extends \Magento\ImportExport\Model\Import\Entity\AbstractEntity - $this->prepareNewSkuData($sku) - ); - } else { -- $this->addRowError(ValidatorInterface::ERROR_TYPE_UNSUPPORTED, $rowNum); -+ $this->skipRow($rowNum, ValidatorInterface::ERROR_TYPE_UNSUPPORTED, $errorLevel); - } - } else { - // validate new product type and attribute set -- if (!isset($rowData[self::COL_TYPE]) || !isset($this->_productTypeModels[$rowData[self::COL_TYPE]])) { -- $this->addRowError(ValidatorInterface::ERROR_INVALID_TYPE, $rowNum); -- } elseif (!isset($rowData[self::COL_ATTR_SET]) -- || !isset($this->_attrSetNameToId[$rowData[self::COL_ATTR_SET]]) -+ if (!isset($rowData[self::COL_TYPE], $this->_productTypeModels[$rowData[self::COL_TYPE]])) { -+ $this->skipRow($rowNum, ValidatorInterface::ERROR_INVALID_TYPE, $errorLevel); -+ } elseif (!isset($rowData[self::COL_ATTR_SET], $this->_attrSetNameToId[$rowData[self::COL_ATTR_SET]]) - ) { -- $this->addRowError(ValidatorInterface::ERROR_INVALID_ATTR_SET, $rowNum); -+ $this->skipRow($rowNum, ValidatorInterface::ERROR_INVALID_ATTR_SET, $errorLevel); - } elseif ($this->skuProcessor->getNewSku($sku) === null) { - $this->skuProcessor->addNewSku( - $sku, -@@ -2443,24 +2455,50 @@ class Product extends \Magento\ImportExport\Model\Import\Entity\AbstractEntity - ValidatorInterface::ERROR_DUPLICATE_URL_KEY, - $rowNum, - $rowData[self::COL_NAME], -- $message -- ); -+ $message, -+ $errorLevel -+ ) -+ ->getErrorAggregator() -+ ->addRowToSkip($rowNum); - } - } - } -+ -+ if (!empty($rowData['new_from_date']) && !empty($rowData['new_to_date']) -+ ) { -+ $newFromTimestamp = strtotime($this->dateTime->formatDate($rowData['new_from_date'], false)); -+ $newToTimestamp = strtotime($this->dateTime->formatDate($rowData['new_to_date'], false)); -+ if ($newFromTimestamp > $newToTimestamp) { -+ $this->skipRow( -+ $rowNum, -+ 'invalidNewToDateValue', -+ $errorLevel, -+ $rowData['new_to_date'] -+ ); -+ } -+ } -+ - return !$this->getErrorAggregator()->isRowInvalid($rowNum); - } - - /** -+ * Check if need to validate url key. -+ * - * @param array $rowData - * @return bool - */ - private function isNeedToValidateUrlKey($rowData) - { -+ if (!empty($rowData[self::COL_SKU]) && empty($rowData[self::URL_KEY]) -+ && $this->getBehavior() === Import::BEHAVIOR_APPEND -+ && $this->isSkuExist($rowData[self::COL_SKU])) { -+ return false; -+ } -+ - return (!empty($rowData[self::URL_KEY]) || !empty($rowData[self::COL_NAME])) - && (empty($rowData[self::COL_VISIBILITY]) -- || $rowData[self::COL_VISIBILITY] -- !== (string)Visibility::getOptionArray()[Visibility::VISIBILITY_NOT_VISIBLE]); -+ || $rowData[self::COL_VISIBILITY] -+ !== (string)Visibility::getOptionArray()[Visibility::VISIBILITY_NOT_VISIBLE]); - } - - /** -@@ -2600,9 +2638,12 @@ class Product extends \Magento\ImportExport\Model\Import\Entity\AbstractEntity - return explode($delimiter, $values); - } - if (preg_match_all('~"((?:[^"]|"")*)"~', $values, $matches)) { -- return $values = array_map(function ($value) { -- return str_replace('""', '"', $value); -- }, $matches[1]); -+ return $values = array_map( -+ function ($value) { -+ return str_replace('""', '"', $value); -+ }, -+ $matches[1] -+ ); - } - return [$values]; - } -@@ -2763,17 +2804,22 @@ class Product extends \Magento\ImportExport\Model\Import\Entity\AbstractEntity - } - - /** -+ * Retrieve url key from provided row data. -+ * - * @param array $rowData - * @return string -+ * - * @since 100.0.3 - */ - protected function getUrlKey($rowData) - { - if (!empty($rowData[self::URL_KEY])) { -- return strtolower($rowData[self::URL_KEY]); -+ $urlKey = (string) $rowData[self::URL_KEY]; -+ return trim(strtolower($urlKey)); - } - -- if (!empty($rowData[self::COL_NAME])) { -+ if (!empty($rowData[self::COL_NAME]) -+ && (array_key_exists(self::URL_KEY, $rowData) || !$this->isSkuExist($rowData[self::COL_SKU]))) { - return $this->productUrl->formatUrlKey($rowData[self::COL_NAME]); - } - -@@ -2781,7 +2827,10 @@ class Product extends \Magento\ImportExport\Model\Import\Entity\AbstractEntity - } - - /** -+ * Retrieve resource. -+ * - * @return Proxy\Product\ResourceModel -+ * - * @since 100.0.3 - */ - protected function getResource() -@@ -2792,6 +2841,26 @@ class Product extends \Magento\ImportExport\Model\Import\Entity\AbstractEntity - return $this->_resource; - } - -+ /** -+ * Whether a url key is needed to be change. -+ * -+ * @param array $rowData -+ * @return bool -+ */ -+ private function isNeedToChangeUrlKey(array $rowData): bool -+ { -+ $urlKey = $this->getUrlKey($rowData); -+ $productExists = $this->isSkuExist($rowData[self::COL_SKU]); -+ $markedToEraseUrlKey = isset($rowData[self::URL_KEY]); -+ // The product isn't new and the url key index wasn't marked for change. -+ if (!$urlKey && $productExists && !$markedToEraseUrlKey) { -+ // Seems there is no need to change the url key -+ return false; -+ } -+ -+ return true; -+ } -+ - /** - * Get product entity link field - * -@@ -2835,6 +2904,21 @@ class Product extends \Magento\ImportExport\Model\Import\Entity\AbstractEntity - } - } - -+ /** -+ * Update 'disabled' field for media gallery entity -+ * -+ * @param array $images -+ * @return $this -+ */ -+ private function updateMediaGalleryVisibility(array $images) -+ { -+ if (!empty($images)) { -+ $this->mediaProcessor->updateMediaGalleryVisibility($images); -+ } -+ -+ return $this; -+ } -+ - /** - * Parse values from multiple attributes fields - * -@@ -2897,9 +2981,7 @@ class Product extends \Magento\ImportExport\Model\Import\Entity\AbstractEntity - - if ($this->stockConfiguration->isQty($this->skuProcessor->getNewSku($sku)['type_id'])) { - $stockItemDo->setData($row); -- $row['is_in_stock'] = isset($row['is_in_stock']) && $stockItemDo->getBackorders() -- ? $row['is_in_stock'] -- : $this->stockStateProvider->verifyStock($stockItemDo); -+ $row['is_in_stock'] = $row['is_in_stock'] ?? $this->stockStateProvider->verifyStock($stockItemDo); - if ($this->stockStateProvider->verifyNotification($stockItemDo)) { - $date = $this->dateTimeFactory->create('now', new \DateTimeZone('UTC')); - $row['low_stock_date'] = $date->format(DateTime::DATETIME_PHP_FORMAT); -@@ -2911,4 +2993,217 @@ class Product extends \Magento\ImportExport\Model\Import\Entity\AbstractEntity - - return $row; - } -+ -+ /** -+ * Retrieve product by sku. -+ * -+ * @param string $sku -+ * @return \Magento\Catalog\Api\Data\ProductInterface|null -+ */ -+ private function retrieveProductBySku($sku) -+ { -+ try { -+ $product = $this->productRepository->get($sku); -+ } catch (NoSuchEntityException $e) { -+ return null; -+ } -+ return $product; -+ } -+ -+ /** -+ * Add row as skipped -+ * -+ * @param int $rowNum -+ * @param string $errorCode Error code or simply column name -+ * @param string $errorLevel error level -+ * @param string|null $colName optional column name -+ * @return $this -+ */ -+ private function skipRow( -+ $rowNum, -+ string $errorCode, -+ string $errorLevel = ProcessingError::ERROR_LEVEL_NOT_CRITICAL, -+ $colName = null -+ ): self { -+ $this->addRowError($errorCode, $rowNum, $colName, null, $errorLevel); -+ $this->getErrorAggregator() -+ ->addRowToSkip($rowNum); -+ return $this; -+ } -+ -+ /** -+ * Returns errorLevel for validation -+ * -+ * @param string $sku -+ * @return string -+ */ -+ private function getValidationErrorLevel($sku): string -+ { -+ return (!$this->isSkuExist($sku) && Import::BEHAVIOR_REPLACE !== $this->getBehavior()) -+ ? ProcessingError::ERROR_LEVEL_CRITICAL -+ : ProcessingError::ERROR_LEVEL_NOT_CRITICAL; -+ } -+ -+ /** -+ * Processes link bunches -+ * -+ * @param array $bunch -+ * @param Link $resource -+ * @param int $nextLinkId -+ * @param array $positionAttrId -+ * @return void -+ */ -+ private function processLinkBunches( -+ array $bunch, -+ Link $resource, -+ int $nextLinkId, -+ array $positionAttrId -+ ): void { -+ $productIds = []; -+ $linkRows = []; -+ $positionRows = []; -+ -+ $bunch = array_filter($bunch, [$this, 'isRowAllowedToImport'], ARRAY_FILTER_USE_BOTH); -+ foreach ($bunch as $rowData) { -+ $sku = $rowData[self::COL_SKU]; -+ $productId = $this->skuProcessor->getNewSku($sku)[$this->getProductEntityLinkField()]; -+ $productIds[] = $productId; -+ $productLinkKeys = $this->fetchProductLinks($resource, $productId); -+ $linkNameToId = array_filter( -+ $this->_linkNameToId, -+ function ($linkName) use ($rowData) { -+ return isset($rowData[$linkName . 'sku']); -+ }, -+ ARRAY_FILTER_USE_KEY -+ ); -+ foreach ($linkNameToId as $linkName => $linkId) { -+ $linkSkus = explode($this->getMultipleValueSeparator(), $rowData[$linkName . 'sku']); -+ $linkPositions = !empty($rowData[$linkName . 'position']) -+ ? explode($this->getMultipleValueSeparator(), $rowData[$linkName . 'position']) -+ : []; -+ -+ $linkSkus = array_filter( -+ $linkSkus, -+ function ($linkedSku) use ($sku) { -+ $linkedSku = trim($linkedSku); -+ return ($this->skuProcessor->getNewSku($linkedSku) !== null || $this->isSkuExist($linkedSku)) -+ && strcasecmp($linkedSku, $sku) !== 0; -+ } -+ ); -+ foreach ($linkSkus as $linkedKey => $linkedSku) { -+ $linkedId = $this->getProductLinkedId($linkedSku); -+ if ($linkedId == null) { -+ // Import file links to a SKU which is skipped for some reason, which leads to a "NULL" -+ // link causing fatal errors. -+ $formatStr = 'WARNING: Orphaned link skipped: From SKU %s (ID %d) to SKU %s, Link type id: %d'; -+ $exception = new \Exception(sprintf($formatStr, $sku, $productId, $linkedSku, $linkId)); -+ $this->_logger->critical($exception); -+ continue; -+ } -+ $linkKey = $this->composeLinkKey($productId, $linkedId, $linkId); -+ $productLinkKeys[$linkKey] = $productLinkKeys[$linkKey] ?? $nextLinkId; -+ -+ $linkRows[$linkKey] = $linkRows[$linkKey] ?? [ -+ 'link_id' => $productLinkKeys[$linkKey], -+ 'product_id' => $productId, -+ 'linked_product_id' => $linkedId, -+ 'link_type_id' => $linkId, -+ ]; -+ -+ if (!empty($linkPositions[$linkedKey])) { -+ $positionRows[] = [ -+ 'link_id' => $productLinkKeys[$linkKey], -+ 'product_link_attribute_id' => $positionAttrId[$linkId], -+ 'value' => $linkPositions[$linkedKey], -+ ]; -+ } -+ $nextLinkId++; -+ } -+ } -+ } -+ $this->saveLinksData($resource, $productIds, $linkRows, $positionRows); -+ } -+ -+ /** -+ * Fetches Product Links -+ * -+ * @param Link $resource -+ * @param int $productId -+ * @return array -+ */ -+ private function fetchProductLinks(Link $resource, int $productId) : array -+ { -+ $productLinkKeys = []; -+ $select = $this->_connection->select()->from( -+ $resource->getTable('catalog_product_link'), -+ ['id' => 'link_id', 'linked_id' => 'linked_product_id', 'link_type_id' => 'link_type_id'] -+ )->where( -+ 'product_id = :product_id' -+ ); -+ $bind = [':product_id' => $productId]; -+ foreach ($this->_connection->fetchAll($select, $bind) as $linkData) { -+ $linkKey = $this->composeLinkKey($productId, $linkData['linked_id'], $linkData['link_type_id']); -+ $productLinkKeys[$linkKey] = $linkData['id']; -+ } -+ -+ return $productLinkKeys; -+ } -+ -+ /** -+ * Gets the Id of the Sku -+ * -+ * @param string $linkedSku -+ * @return int|null -+ */ -+ private function getProductLinkedId(string $linkedSku) : ?int -+ { -+ $linkedSku = trim($linkedSku); -+ $newSku = $this->skuProcessor->getNewSku($linkedSku); -+ $linkedId = !empty($newSku) ? $newSku['entity_id'] : $this->getExistingSku($linkedSku)['entity_id']; -+ return $linkedId; -+ } -+ -+ /** -+ * Saves information about product links -+ * -+ * @param Link $resource -+ * @param array $productIds -+ * @param array $linkRows -+ * @param array $positionRows -+ * @throws LocalizedException -+ */ -+ private function saveLinksData(Link $resource, array $productIds, array $linkRows, array $positionRows) -+ { -+ $mainTable = $resource->getMainTable(); -+ if (Import::BEHAVIOR_APPEND != $this->getBehavior() && $productIds) { -+ $this->_connection->delete( -+ $mainTable, -+ $this->_connection->quoteInto('product_id IN (?)', array_unique($productIds)) -+ ); -+ } -+ if ($linkRows) { -+ $this->_connection->insertOnDuplicate($mainTable, $linkRows, ['link_id']); -+ } -+ if ($positionRows) { -+ // process linked product positions -+ $this->_connection->insertOnDuplicate( -+ $resource->getAttributeTypeTable('int'), -+ $positionRows, -+ ['value'] -+ ); -+ } -+ } -+ -+ /** -+ * Composes the link key -+ * -+ * @param int $productId -+ * @param int $linkedId -+ * @param int $linkTypeId -+ * @return string -+ */ -+ private function composeLinkKey(int $productId, int $linkedId, int $linkTypeId) : string -+ { -+ return "{$productId}-{$linkedId}-{$linkTypeId}"; -+ } - } -diff --git a/app/code/Magento/CatalogImportExport/Model/Import/Product/CategoryProcessor.php b/app/code/Magento/CatalogImportExport/Model/Import/Product/CategoryProcessor.php -index db5b07279ed..951989146e6 100644 ---- a/app/code/Magento/CatalogImportExport/Model/Import/Product/CategoryProcessor.php -+++ b/app/code/Magento/CatalogImportExport/Model/Import/Product/CategoryProcessor.php -@@ -66,6 +66,8 @@ class CategoryProcessor - } - - /** -+ * Initialize categories -+ * - * @return $this - */ - protected function initCategories() -@@ -104,7 +106,6 @@ class CategoryProcessor - * - * @param string $name - * @param int $parentId -- * - * @return int - */ - protected function createCategory($name, $parentId) -@@ -120,13 +121,8 @@ class CategoryProcessor - $category->setIsActive(true); - $category->setIncludeInMenu(true); - $category->setAttributeSetId($category->getDefaultAttributeSetId()); -- try { -- $category->save(); -- $this->categoriesCache[$category->getId()] = $category; -- } catch (\Exception $e) { -- $this->addFailedCategory($category, $e); -- } -- -+ $category->save(); -+ $this->categoriesCache[$category->getId()] = $category; - return $category->getId(); - } - -@@ -134,7 +130,6 @@ class CategoryProcessor - * Returns ID of category by string path creating nonexistent ones. - * - * @param string $categoryPath -- * - * @return int - */ - protected function upsertCategory($categoryPath) -@@ -165,7 +160,6 @@ class CategoryProcessor - * - * @param string $categoriesString - * @param string $categoriesSeparator -- * - * @return array - */ - public function upsertCategories($categoriesString, $categoriesSeparator) -@@ -234,7 +228,7 @@ class CategoryProcessor - */ - public function getCategoryById($categoryId) - { -- return isset($this->categoriesCache[$categoryId]) ? $this->categoriesCache[$categoryId] : null; -+ return $this->categoriesCache[$categoryId] ?? null; - } - - /** -diff --git a/app/code/Magento/CatalogImportExport/Model/Import/Product/MediaGalleryProcessor.php b/app/code/Magento/CatalogImportExport/Model/Import/Product/MediaGalleryProcessor.php -index ec7c6a11729..d43dc11a68f 100644 ---- a/app/code/Magento/CatalogImportExport/Model/Import/Product/MediaGalleryProcessor.php -+++ b/app/code/Magento/CatalogImportExport/Model/Import/Product/MediaGalleryProcessor.php -@@ -103,7 +103,7 @@ class MediaGalleryProcessor - /** - * Save product media gallery. - * -- * @param $mediaGalleryData -+ * @param array $mediaGalleryData - * @return void - */ - public function saveMediaGallery(array $mediaGalleryData) -@@ -152,14 +152,37 @@ class MediaGalleryProcessor - * @return void - */ - public function updateMediaGalleryLabels(array $labels) -+ { -+ $this->updateMediaGalleryField($labels, 'label'); -+ } -+ -+ /** -+ * Update 'disabled' field for media gallery entity -+ * -+ * @param array $images -+ * @return void -+ */ -+ public function updateMediaGalleryVisibility(array $images) -+ { -+ $this->updateMediaGalleryField($images, 'disabled'); -+ } -+ -+ /** -+ * Update value for requested field in media gallery entities -+ * -+ * @param array $data -+ * @param string $field -+ * @return void -+ */ -+ private function updateMediaGalleryField(array $data, $field) - { - $insertData = []; -- foreach ($labels as $label) { -- $imageData = $label['imageData']; -+ foreach ($data as $datum) { -+ $imageData = $datum['imageData']; - -- if ($imageData['label'] === null) { -+ if ($imageData[$field] === null) { - $insertData[] = [ -- 'label' => $label['label'], -+ $field => $datum[$field], - $this->getProductEntityLinkField() => $imageData[$this->getProductEntityLinkField()], - 'value_id' => $imageData['value_id'], - 'store_id' => Store::DEFAULT_STORE_ID, -@@ -168,7 +191,7 @@ class MediaGalleryProcessor - $this->connection->update( - $this->mediaGalleryValueTableName, - [ -- 'label' => $label['label'], -+ $field => $datum[$field], - ], - [ - $this->getProductEntityLinkField() . ' = ?' => $imageData[$this->getProductEntityLinkField()], -@@ -224,6 +247,7 @@ class MediaGalleryProcessor - ), - [ - 'label' => 'mgv.label', -+ 'disabled' => 'mgv.disabled', - ] - )->joinInner( - ['pe' => $this->productEntityTableName], -@@ -263,7 +287,7 @@ class MediaGalleryProcessor - /** - * Save media gallery data per store. - * -- * @param $storeId -+ * @param int $storeId - * @param array $mediaGalleryData - * @param array $newMediaValues - * @param array $valueToProductId -@@ -339,6 +363,8 @@ class MediaGalleryProcessor - } - - /** -+ * Get resource. -+ * - * @return \Magento\CatalogImportExport\Model\Import\Proxy\Product\ResourceModel - */ - private function getResource() -diff --git a/app/code/Magento/CatalogImportExport/Model/Import/Product/Option.php b/app/code/Magento/CatalogImportExport/Model/Import/Product/Option.php -index ae29fd2ef4b..7435c0bebfc 100644 ---- a/app/code/Magento/CatalogImportExport/Model/Import/Product/Option.php -+++ b/app/code/Magento/CatalogImportExport/Model/Import/Product/Option.php -@@ -333,6 +333,11 @@ class Option extends \Magento\ImportExport\Model\Import\Entity\AbstractEntity - */ - private $optionTypeTitles; - -+ /** -+ * @var array -+ */ -+ private $lastOptionTitle; -+ - /** - * @param \Magento\ImportExport\Model\ResourceModel\Import\Data $importData - * @param ResourceConnection $resource -@@ -426,7 +431,10 @@ class Option extends \Magento\ImportExport\Model\Import\Entity\AbstractEntity - ); - $this->_productEntity->addMessageTemplate( - self::ERROR_INVALID_TYPE, -- __('Value for \'type\' sub attribute in \'custom_options\' attribute contains incorrect value, acceptable values are: \'dropdown\', \'checkbox\'') -+ __( -+ 'Value for \'type\' sub attribute in \'custom_options\' attribute contains incorrect value, acceptable values are: %1', -+ '\''.implode('\', \'', array_keys($this->_specificTypes)).'\'' -+ ) - ); - $this->_productEntity->addMessageTemplate(self::ERROR_EMPTY_TITLE, __('Please enter a value for title.')); - $this->_productEntity->addMessageTemplate( -@@ -624,7 +632,7 @@ class Option extends \Magento\ImportExport\Model\Import\Entity\AbstractEntity - $this->_addRowsErrors(self::ERROR_AMBIGUOUS_NEW_NAMES, $errorRows); - return false; - } -- if ($this->getBehavior() == \Magento\ImportExport\Model\Import::BEHAVIOR_APPEND) { -+ if ($this->getBehavior() == Import::BEHAVIOR_APPEND) { - $errorRows = $this->_findOldOptionsWithTheSameTitles(); - if ($errorRows) { - $this->_addRowsErrors(self::ERROR_AMBIGUOUS_OLD_NAMES, $errorRows); -@@ -962,11 +970,10 @@ class Option extends \Magento\ImportExport\Model\Import\Entity\AbstractEntity - return false; - } - } -- return true; - } - } - -- return false; -+ return true; - } - - /** -@@ -1117,6 +1124,8 @@ class Option extends \Magento\ImportExport\Model\Import\Entity\AbstractEntity - } - - /** -+ * Process option row. -+ * - * @param string $name - * @param array $optionRow - * @return array -@@ -1198,6 +1207,7 @@ class Option extends \Magento\ImportExport\Model\Import\Entity\AbstractEntity - - /** - * Import data rows. -+ * - * Additional store view data (option titles) will be sought in store view specified import file rows - * - * @return boolean -@@ -1206,7 +1216,6 @@ class Option extends \Magento\ImportExport\Model\Import\Entity\AbstractEntity - protected function _importData() - { - $this->_initProductsSku(); -- - $nextOptionId = $this->_resourceHelper->getNextAutoincrement($this->_tables['catalog_product_option']); - $nextValueId = $this->_resourceHelper->getNextAutoincrement( - $this->_tables['catalog_product_option_type_value'] -@@ -1225,7 +1234,6 @@ class Option extends \Magento\ImportExport\Model\Import\Entity\AbstractEntity - $parentCount = []; - $childCount = []; - $optionsToRemove = []; -- - foreach ($bunch as $rowNumber => $rowData) { - if (isset($optionId, $valueId) && empty($rowData[PRODUCT::COL_STORE_VIEW_CODE])) { - $nextOptionId = $optionId; -@@ -1273,17 +1281,26 @@ class Option extends \Magento\ImportExport\Model\Import\Entity\AbstractEntity - $parentCount, - $childCount - ); -+ - $this->_collectOptionTitle($combinedData, $prevOptionId, $titles); -+ $this->checkOptionTitles( -+ $options, -+ $titles, -+ $combinedData, -+ $prevOptionId, -+ $optionId, -+ $products, -+ $prices -+ ); - } - } -- - $this->removeExistingOptions($products, $optionsToRemove); -- - $types = [ - 'values' => $typeValues, - 'prices' => $typePrices, - 'titles' => $typeTitles, - ]; -+ $this->setLastOptionTitle($titles); - //Save prepared custom options data. - $this->savePreparedCustomOptions( - $products, -@@ -1293,11 +1310,69 @@ class Option extends \Magento\ImportExport\Model\Import\Entity\AbstractEntity - $types - ); - } -- - return true; - } - - /** -+ * Check options titles. -+ * -+ * If products were split up between bunches, -+ * this function will add needed option for option titles -+ * -+ * @param array $options -+ * @param array $titles -+ * @param array $combinedData -+ * @param int $prevOptionId -+ * @param int $optionId -+ * @param array $products -+ * @param array $prices -+ * @return void -+ */ -+ private function checkOptionTitles( -+ array &$options, -+ array &$titles, -+ array $combinedData, -+ int &$prevOptionId, -+ int &$optionId, -+ array $products, -+ array $prices -+ ) : void { -+ $titlesCount = count($titles); -+ if ($titlesCount > 0 && count($options) !== $titlesCount) { -+ $combinedData[Product::COL_STORE_VIEW_CODE] = ''; -+ $optionId--; -+ $option = $this->_collectOptionMainData( -+ $combinedData, -+ $prevOptionId, -+ $optionId, -+ $products, -+ $prices -+ ); -+ if ($option) { -+ $options[] = $option; -+ } -+ } -+ } -+ -+ /** -+ * Setting last Custom Option Title -+ * to use it later in _collectOptionTitle -+ * to set correct title for default store view -+ * -+ * @param array $titles -+ */ -+ private function setLastOptionTitle(array &$titles) : void -+ { -+ if (count($titles) > 0) { -+ end($titles); -+ $key = key($titles); -+ $this->lastOptionTitle[$key] = $titles[$key]; -+ } -+ } -+ -+ /** -+ * Remove existing options. -+ * - * Remove all existing options if import behaviour is APPEND - * in other case remove options for products with empty "custom_options" row only. - * -@@ -1308,7 +1383,7 @@ class Option extends \Magento\ImportExport\Model\Import\Entity\AbstractEntity - */ - private function removeExistingOptions(array $products, array $optionsToRemove): void - { -- if ($this->getBehavior() != \Magento\ImportExport\Model\Import::BEHAVIOR_APPEND) { -+ if ($this->getBehavior() != Import::BEHAVIOR_APPEND) { - $this->_deleteEntities(array_keys($products)); - } elseif (!empty($optionsToRemove)) { - // Remove options for products with empty "custom_options" row -@@ -1447,8 +1522,12 @@ class Option extends \Magento\ImportExport\Model\Import\Entity\AbstractEntity - $defaultStoreId = Store::DEFAULT_STORE_ID; - if (!empty($rowData[self::COLUMN_TITLE])) { - if (!isset($titles[$prevOptionId][$defaultStoreId])) { -- // ensure default title is set -- $titles[$prevOptionId][$defaultStoreId] = $rowData[self::COLUMN_TITLE]; -+ if (isset($this->lastOptionTitle[$prevOptionId])) { -+ $titles[$prevOptionId] = $this->lastOptionTitle[$prevOptionId]; -+ unset($this->lastOptionTitle); -+ } else { -+ $titles[$prevOptionId][$defaultStoreId] = $rowData[self::COLUMN_TITLE]; -+ } - } - $titles[$prevOptionId][$this->_rowStoreId] = $rowData[self::COLUMN_TITLE]; - } -@@ -1547,6 +1626,8 @@ class Option extends \Magento\ImportExport\Model\Import\Entity\AbstractEntity - } - - /** -+ * Rarse required data. -+ * - * Parse required data from current row and store to class internal variables some data - * for underlying dependent rows - * -@@ -1715,7 +1796,8 @@ class Option extends \Magento\ImportExport\Model\Import\Entity\AbstractEntity - ]; - - $priceData = false; -- if (!empty($rowData[self::COLUMN_ROW_PRICE])) { -+ $customOptionRowPrice = $rowData[self::COLUMN_ROW_PRICE]; -+ if (!empty($customOptionRowPrice) || $customOptionRowPrice === '0') { - $priceData = [ - 'price' => (double)rtrim($rowData[self::COLUMN_ROW_PRICE], '%'), - 'price_type' => 'fixed', -@@ -2028,7 +2110,7 @@ class Option extends \Magento\ImportExport\Model\Import\Entity\AbstractEntity - array $types - ): void { - if ($this->_isReadyForSaving($options, $titles, $types['values'])) { -- if ($this->getBehavior() == \Magento\ImportExport\Model\Import::BEHAVIOR_APPEND) { -+ if ($this->getBehavior() == Import::BEHAVIOR_APPEND) { - $this->_compareOptionsWithExisting($options, $titles, $prices, $types['values']); - $this->restoreOriginalOptionTypeIds($types['values'], $types['prices'], $types['titles']); - } -diff --git a/app/code/Magento/CatalogImportExport/Model/Import/Product/SkuProcessor.php b/app/code/Magento/CatalogImportExport/Model/Import/Product/SkuProcessor.php -index addd1523f87..ea6049ba651 100644 ---- a/app/code/Magento/CatalogImportExport/Model/Import/Product/SkuProcessor.php -+++ b/app/code/Magento/CatalogImportExport/Model/Import/Product/SkuProcessor.php -@@ -142,7 +142,7 @@ class SkuProcessor - { - if ($sku !== null) { - $sku = strtolower($sku); -- return isset($this->newSkus[$sku]) ? $this->newSkus[$sku] : null; -+ return $this->newSkus[$sku] ?? null; - } - return $this->newSkus; - } -diff --git a/app/code/Magento/CatalogImportExport/Model/Import/Product/StoreResolver.php b/app/code/Magento/CatalogImportExport/Model/Import/Product/StoreResolver.php -index be5644e22b0..3dff0188a7d 100644 ---- a/app/code/Magento/CatalogImportExport/Model/Import/Product/StoreResolver.php -+++ b/app/code/Magento/CatalogImportExport/Model/Import/Product/StoreResolver.php -@@ -75,7 +75,7 @@ class StoreResolver - $this->_initWebsites(); - } - if ($code) { -- return isset($this->websiteCodeToId[$code]) ? $this->websiteCodeToId[$code] : null; -+ return $this->websiteCodeToId[$code] ?? null; - } - return $this->websiteCodeToId; - } -@@ -90,7 +90,7 @@ class StoreResolver - $this->_initWebsites(); - } - if ($code) { -- return isset($this->websiteCodeToStoreIds[$code]) ? $this->websiteCodeToStoreIds[$code] : null; -+ return $this->websiteCodeToStoreIds[$code] ?? null; - } - return $this->websiteCodeToStoreIds; - } -@@ -119,7 +119,7 @@ class StoreResolver - $this->_initStores(); - } - if ($code) { -- return isset($this->storeCodeToId[$code]) ? $this->storeCodeToId[$code] : null; -+ return $this->storeCodeToId[$code] ?? null; - } - return $this->storeCodeToId; - } -@@ -134,7 +134,7 @@ class StoreResolver - $this->_initStores(); - } - if ($code) { -- return isset($this->storeIdToWebsiteStoreIds[$code]) ? $this->storeIdToWebsiteStoreIds[$code] : null; -+ return $this->storeIdToWebsiteStoreIds[$code] ?? null; - } - return $this->storeIdToWebsiteStoreIds; - } -diff --git a/app/code/Magento/CatalogImportExport/Model/Import/Product/Type/AbstractType.php b/app/code/Magento/CatalogImportExport/Model/Import/Product/Type/AbstractType.php -index afd018f077d..3b6caef66ce 100644 ---- a/app/code/Magento/CatalogImportExport/Model/Import/Product/Type/AbstractType.php -+++ b/app/code/Magento/CatalogImportExport/Model/Import/Product/Type/AbstractType.php -@@ -195,6 +195,8 @@ abstract class AbstractType - } - - /** -+ * Initialize template for error message. -+ * - * @param array $templateCollection - * @return $this - */ -@@ -377,6 +379,8 @@ abstract class AbstractType - } - - /** -+ * Adding attribute option. -+ * - * In case we've dynamically added new attribute option during import we need to add it to our cache - * in order to keep it up to date. - * -@@ -508,8 +512,10 @@ abstract class AbstractType - } - - /** -- * Prepare attributes values for save: exclude non-existent, static or with empty values attributes; -- * set default values if needed -+ * Adding default attribute to product before save. -+ * -+ * Prepare attributes values for save: exclude non-existent, static or with empty values attributes, -+ * set default values if needed. - * - * @param array $rowData - * @param bool $withDefaultValue -@@ -537,9 +543,9 @@ abstract class AbstractType - } else { - $resultAttrs[$attrCode] = $rowData[$attrCode]; - } -- } elseif (array_key_exists($attrCode, $rowData)) { -+ } elseif (array_key_exists($attrCode, $rowData) && empty($rowData['_store'])) { - $resultAttrs[$attrCode] = $rowData[$attrCode]; -- } elseif ($withDefaultValue && null !== $attrParams['default_value']) { -+ } elseif ($withDefaultValue && null !== $attrParams['default_value'] && empty($rowData['_store'])) { - $resultAttrs[$attrCode] = $attrParams['default_value']; - } - } -@@ -611,7 +617,8 @@ abstract class AbstractType - } - - /** -- * Clean cached values -+ * Clean cached values. -+ * - * @since 100.2.0 - */ - public function __destruct() -diff --git a/app/code/Magento/CatalogImportExport/Model/Import/Product/Validator.php b/app/code/Magento/CatalogImportExport/Model/Import/Product/Validator.php -index 2aa21059918..4b7416f6ad9 100644 ---- a/app/code/Magento/CatalogImportExport/Model/Import/Product/Validator.php -+++ b/app/code/Magento/CatalogImportExport/Model/Import/Product/Validator.php -@@ -7,6 +7,7 @@ namespace Magento\CatalogImportExport\Model\Import\Product; - - use Magento\CatalogImportExport\Model\Import\Product; - use Magento\Framework\Validator\AbstractValidator; -+use Magento\Catalog\Model\Product\Attribute\Backend\Sku; - - /** - * Class Validator -@@ -60,6 +61,8 @@ class Validator extends AbstractValidator implements RowValidatorInterface - } - - /** -+ * Text validation -+ * - * @param mixed $attrCode - * @param string $type - * @return bool -@@ -69,6 +72,8 @@ class Validator extends AbstractValidator implements RowValidatorInterface - $val = $this->string->cleanString($this->_rowData[$attrCode]); - if ($type == 'text') { - $valid = $this->string->strlen($val) < Product::DB_MAX_TEXT_LENGTH; -+ } else if ($attrCode == Product::COL_SKU) { -+ $valid = $this->string->strlen($val) <= SKU::SKU_MAX_LENGTH; - } else { - $valid = $this->string->strlen($val) < Product::DB_MAX_VARCHAR_LENGTH; - } -@@ -105,6 +110,8 @@ class Validator extends AbstractValidator implements RowValidatorInterface - } - - /** -+ * Numeric validation -+ * - * @param mixed $attrCode - * @param string $type - * @return bool -@@ -132,6 +139,8 @@ class Validator extends AbstractValidator implements RowValidatorInterface - } - - /** -+ * Is required attribute valid -+ * - * @param string $attrCode - * @param array $attributeParams - * @param array $rowData -@@ -159,6 +168,8 @@ class Validator extends AbstractValidator implements RowValidatorInterface - } - - /** -+ * Is attribute valid -+ * - * @param string $attrCode - * @param array $attrParams - * @param array $rowData -@@ -255,6 +266,8 @@ class Validator extends AbstractValidator implements RowValidatorInterface - } - - /** -+ * Set invalid attribute -+ * - * @param string|null $attribute - * @return void - * @since 100.1.0 -@@ -265,6 +278,8 @@ class Validator extends AbstractValidator implements RowValidatorInterface - } - - /** -+ * Get invalid attribute -+ * - * @return string - * @since 100.1.0 - */ -@@ -274,6 +289,8 @@ class Validator extends AbstractValidator implements RowValidatorInterface - } - - /** -+ * Is valid attributes -+ * - * @return bool - * @SuppressWarnings(PHPMD.UnusedLocalVariable) - */ -@@ -300,7 +317,7 @@ class Validator extends AbstractValidator implements RowValidatorInterface - } - - /** -- * {@inheritdoc} -+ * @inheritdoc - */ - public function isValid($value) - { -@@ -331,6 +348,8 @@ class Validator extends AbstractValidator implements RowValidatorInterface - } - - /** -+ * Init -+ * - * @param \Magento\CatalogImportExport\Model\Import\Product $context - * @return $this - */ -diff --git a/app/code/Magento/CatalogImportExport/Model/Import/Uploader.php b/app/code/Magento/CatalogImportExport/Model/Import/Uploader.php -index e7ffe408cc7..3ac7f98818d 100644 ---- a/app/code/Magento/CatalogImportExport/Model/Import/Uploader.php -+++ b/app/code/Magento/CatalogImportExport/Model/Import/Uploader.php -@@ -101,7 +101,7 @@ class Uploader extends \Magento\MediaStorage\Model\File\Uploader - * @param \Magento\MediaStorage\Model\File\Validator\NotProtectedExtension $validator - * @param \Magento\Framework\Filesystem $filesystem - * @param \Magento\Framework\Filesystem\File\ReadFactory $readFactory -- * @param null $filePath -+ * @param string|null $filePath - * @throws \Magento\Framework\Exception\LocalizedException - */ - public function __construct( -@@ -113,15 +113,15 @@ class Uploader extends \Magento\MediaStorage\Model\File\Uploader - \Magento\Framework\Filesystem\File\ReadFactory $readFactory, - $filePath = null - ) { -- if ($filePath !== null) { -- $this->_setUploadFile($filePath); -- } - $this->_imageFactory = $imageFactory; - $this->_coreFileStorageDb = $coreFileStorageDb; - $this->_coreFileStorage = $coreFileStorage; - $this->_validator = $validator; - $this->_directory = $filesystem->getDirectoryWrite(DirectoryList::ROOT); - $this->_readFactory = $readFactory; -+ if ($filePath !== null) { -+ $this->_setUploadFile($filePath); -+ } - } - - /** -@@ -146,20 +146,24 @@ class Uploader extends \Magento\MediaStorage\Model\File\Uploader - * @param string $fileName - * @param bool $renameFileOff - * @return array -+ * @throws \Magento\Framework\Exception\LocalizedException - */ - public function move($fileName, $renameFileOff = false) - { - if ($renameFileOff) { - $this->setAllowRenameFiles(false); - } -+ -+ if ($this->getTmpDir()) { -+ $filePath = $this->getTmpDir() . '/'; -+ } else { -+ $filePath = ''; -+ } -+ - if (preg_match('/\bhttps?:\/\//i', $fileName, $matches)) { - $url = str_replace($matches[0], '', $fileName); -- -- if ($matches[0] === $this->httpScheme) { -- $read = $this->_readFactory->create($url, DriverPool::HTTP); -- } else { -- $read = $this->_readFactory->create($url, DriverPool::HTTPS); -- } -+ $driver = $matches[0] === $this->httpScheme ? DriverPool::HTTP : DriverPool::HTTPS; -+ $read = $this->_readFactory->create($url, $driver); - - //only use filename (for URI with query parameters) - $parsedUrlPath = parse_url($url, PHP_URL_PATH); -@@ -170,24 +174,19 @@ class Uploader extends \Magento\MediaStorage\Model\File\Uploader - } - } - -- if ($this->getTmpDir()) { -- $filePath = $this->getTmpDir() . '/'; -- } else { -- $filePath = ''; -+ $fileExtension = pathinfo($fileName, PATHINFO_EXTENSION); -+ if ($fileExtension && !$this->checkAllowedExtension($fileExtension)) { -+ throw new \Magento\Framework\Exception\LocalizedException(__('Disallowed file type.')); - } -+ - $fileName = preg_replace('/[^a-z0-9\._-]+/i', '', $fileName); -- $filePath = $this->_directory->getRelativePath($filePath . $fileName); -+ $relativePath = $this->_directory->getRelativePath($filePath . $fileName); - $this->_directory->writeFile( -- $filePath, -+ $relativePath, - $read->readAll() - ); - } - -- if ($this->getTmpDir()) { -- $filePath = $this->getTmpDir() . '/'; -- } else { -- $filePath = ''; -- } - $filePath = $this->_directory->getRelativePath($filePath . $fileName); - $this->_setUploadFile($filePath); - $destDir = $this->_directory->getAbsolutePath($this->getDestDir()); -@@ -353,7 +352,7 @@ class Uploader extends \Magento\MediaStorage\Model\File\Uploader - } - - /** -- * {@inheritdoc} -+ * @inheritdoc - */ - protected function chmod($file) - { -diff --git a/app/code/Magento/CatalogImportExport/Test/Mftf/ActionGroup/AdminExportActionGroup.xml b/app/code/Magento/CatalogImportExport/Test/Mftf/ActionGroup/AdminExportActionGroup.xml -new file mode 100644 -index 00000000000..65588daa96c ---- /dev/null -+++ b/app/code/Magento/CatalogImportExport/Test/Mftf/ActionGroup/AdminExportActionGroup.xml -@@ -0,0 +1,86 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> -+ -+ <!-- Export products using filtering by attribute --> -+ <actionGroup name="exportProductsFilterByAttribute"> -+ <arguments> -+ <argument name="attribute" type="string"/> -+ <argument name="attributeData" type="string"/> -+ </arguments> -+ <selectOption selector="{{AdminExportMainSection.entityType}}" userInput="Products" stepKey="selectProductsOption"/> -+ <waitForElementVisible selector="{{AdminExportMainSection.entityAttributes}}" stepKey="waitForElementVisible"/> -+ <scrollTo selector="{{AdminExportAttributeSection.chooseAttribute('attribute')}}" stepKey="scrollToAttribute" /> -+ <checkOption selector="{{AdminExportAttributeSection.chooseAttribute('attribute')}}" stepKey="selectAttribute"/> -+ <fillField selector="{{AdminExportAttributeSection.fillFilter('attribute')}}" userInput="{{attributeData}}" stepKey="setDataInField"/> -+ <waitForPageLoad stepKey="waitForUserInput"/> -+ <scrollTo selector="{{AdminExportAttributeSection.continueBtn}}" stepKey="scrollToContinue" /> -+ <click selector="{{AdminExportAttributeSection.continueBtn}}" stepKey="clickContinueButton"/> -+ <see selector="{{AdminMessagesSection.successMessage}}" userInput="Message is added to queue, wait to get your file soon" stepKey="seeSuccessMessage"/> -+ </actionGroup> -+ -+ <!-- Export products without filtering --> -+ <actionGroup name="exportAllProducts"> -+ <selectOption selector="{{AdminExportMainSection.entityType}}" userInput="Products" stepKey="selectProductsOption"/> -+ <waitForElementVisible selector="{{AdminExportMainSection.entityAttributes}}" stepKey="waitForElementVisible" time="5"/> -+ <scrollTo selector="{{AdminExportAttributeSection.continueBtn}}" stepKey="scrollToContinue"/> -+ <wait stepKey="waitForScroll" time="5"/> -+ <click selector="{{AdminExportAttributeSection.continueBtn}}" stepKey="clickContinueButton"/> -+ <wait stepKey="waitForClick" time="5"/> -+ <see selector="{{AdminMessagesSection.successMessage}}" userInput="Message is added to queue, wait to get your file soon" stepKey="seeSuccessMessage"/> -+ </actionGroup> -+ -+ <!-- Download first file in the grid --> -+ <actionGroup name="downloadFileByRowIndex"> -+ <arguments> -+ <argument name="rowIndex" type="string"/> -+ </arguments> -+ <reloadPage stepKey="refreshPage"/> -+ <waitForPageLoad stepKey="waitFormReload"/> -+ <click stepKey="clickSelectBtn" selector="{{AdminExportAttributeSection.selectByIndex(rowIndex)}}"/> -+ <click stepKey="clickOnDownload" selector="{{AdminExportAttributeSection.download(rowIndex)}}" after="clickSelectBtn"/> -+ </actionGroup> -+ -+ <!-- Delete exported file --> -+ <actionGroup name="deleteExportedFile"> -+ <arguments> -+ <argument name="rowIndex" type="string"/> -+ </arguments> -+ <amOnPage url="{{AdminExportIndexPage.url}}" stepKey="goToExportIndexPage"/> -+ <click stepKey="clickSelectBtn" selector="{{AdminExportAttributeSection.selectByIndex(rowIndex)}}"/> -+ <click stepKey="clickOnDelete" selector="{{AdminExportAttributeSection.delete(rowIndex)}}" after="clickSelectBtn"/> -+ <waitForElementVisible selector="{{AdminProductGridConfirmActionSection.title}}" stepKey="waitForConfirmModal"/> -+ <click selector="{{AdminProductGridConfirmActionSection.ok}}" stepKey="confirmDelete"/> -+ <waitForElementVisible selector="{{AdminDataGridTableSection.dataGridEmpty}}" stepKey="waitDataGridEmptyMessageAppears"/> -+ <see selector="{{AdminDataGridTableSection.dataGridEmpty}}" userInput="We couldn't find any records." stepKey="assertDataGridEmptyMessage"/> -+ </actionGroup> -+ -+ <actionGroup name="deleteAllExportedFiles"> -+ <amOnPage url="{{AdminExportIndexPage.url}}" stepKey="goToExportIndexPage"/> -+ <executeInSelenium -+ function=" -+ function ($webdriver) use ($I) { -+ $buttons = $webdriver->findElements(\Facebook\WebDriver\WebDriverBy::xpath('//tr[@data-repeat-index=\'0\']//button')); -+ while(!empty($buttons)) { -+ $buttons[0]->click(); -+ $I->waitForElementVisible('//tr[@data-repeat-index=\'0\']//a[text()=\'Delete\']', 10); -+ $deleteButton = $webdriver->findElement(\Facebook\WebDriver\WebDriverBy::xpath('//tr[@data-repeat-index=\'0\']//a[text()=\'Delete\']')); -+ $deleteButton->click(); -+ $I->waitForElementVisible('.modal-popup.confirm button.action-accept', 10); -+ $I->click('.modal-popup.confirm button.action-accept'); -+ $I->waitForPageLoad(60); -+ $buttons = $webdriver->findElements(\Facebook\WebDriver\WebDriverBy::xpath('//tr[@data-repeat-index=\'0\']//button')); -+ } -+ }" -+ stepKey="deleteAllExportedFilesOneByOne"/> -+ <waitForElementVisible selector="{{AdminDataGridTableSection.dataGridEmpty}}" stepKey="waitDataGridEmptyMessageAppears"/> -+ <see selector="{{AdminDataGridTableSection.dataGridEmpty}}" userInput="We couldn't find any records." stepKey="assertDataGridEmptyMessage"/> -+ </actionGroup> -+</actionGroups> -diff --git a/app/code/Magento/CatalogImportExport/Test/Mftf/Test/AdminExportBundleProductTest.xml b/app/code/Magento/CatalogImportExport/Test/Mftf/Test/AdminExportBundleProductTest.xml -new file mode 100644 -index 00000000000..281c8c0db30 ---- /dev/null -+++ b/app/code/Magento/CatalogImportExport/Test/Mftf/Test/AdminExportBundleProductTest.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="AdminExportBundleProductTest"> -+ <annotations> -+ <features value="CatalogImportExport"/> -+ <stories value="Export products"/> -+ <title value="Export Bundle Product"/> -+ <description value="Admin should be able to export Bundle Product"/> -+ <severity value="CRITICAL"/> -+ <testCaseId value="MC-14008"/> -+ <group value="catalog_import_export"/> -+ <group value="mtf_migrated"/> -+ <skip> -+ <issueId value="MC-15934"/> -+ </skip> -+ </annotations> -+ <before> -+ <!--Create bundle product with dynamic price with two simple products --> -+ <createData entity="SimpleProduct2" stepKey="firstSimpleProductForDynamic"/> -+ <createData entity="SimpleProduct2" stepKey="secondSimpleProductForDynamic"/> -+ <createData entity="ApiBundleProduct" stepKey="createDynamicBundleProduct"/> -+ <createData entity="DropDownBundleOption" stepKey="createFirstBundleOption"> -+ <requiredEntity createDataKey="createDynamicBundleProduct"/> -+ </createData> -+ <createData entity="ApiBundleLink" stepKey="firstLinkOptionToDynamicProduct"> -+ <requiredEntity createDataKey="createDynamicBundleProduct"/> -+ <requiredEntity createDataKey="createFirstBundleOption"/> -+ <requiredEntity createDataKey="firstSimpleProductForDynamic"/> -+ </createData> -+ <createData entity="ApiBundleLink" stepKey="secondLinkOptionToDynamicProduct"> -+ <requiredEntity createDataKey="createDynamicBundleProduct"/> -+ <requiredEntity createDataKey="createFirstBundleOption"/> -+ <requiredEntity createDataKey="secondSimpleProductForDynamic"/> -+ </createData> -+ -+ <!-- Create bundle product with fixed price with two simple products --> -+ <createData entity="SimpleProduct2" stepKey="firstSimpleProductForFixed"/> -+ <createData entity="SimpleProduct2" stepKey="secondSimpleProductForFixed"/> -+ <createData entity="ApiFixedBundleProduct" stepKey="createFixedBundleProduct"/> -+ <createData entity="DropDownBundleOption" stepKey="createSecondBundleOption"> -+ <requiredEntity createDataKey="createFixedBundleProduct"/> -+ </createData> -+ <createData entity="ApiBundleLink" stepKey="firstLinkOptionToFixedProduct"> -+ <requiredEntity createDataKey="createFixedBundleProduct"/> -+ <requiredEntity createDataKey="createSecondBundleOption"/> -+ <requiredEntity createDataKey="firstSimpleProductForFixed"/> -+ </createData> -+ <createData entity="ApiBundleLink" stepKey="secondLinkOptionToFixedProduct"> -+ <requiredEntity createDataKey="createFixedBundleProduct"/> -+ <requiredEntity createDataKey="createSecondBundleOption"/> -+ <requiredEntity createDataKey="secondSimpleProductForFixed"/> -+ </createData> -+ -+ <!-- Create bundle product with custom textarea attribute with two simple products --> -+ <createData entity="productAttributeWysiwyg" stepKey="createProductAttribute"/> -+ <createData entity="AddToDefaultSet" stepKey="addToDefaultAttributeSet"> -+ <requiredEntity createDataKey="createProductAttribute"/> -+ </createData> -+ <createData entity="ApiFixedBundleProduct" stepKey="createFixedBundleProductWithAttribute"> -+ <requiredEntity createDataKey="addToDefaultAttributeSet"/> -+ </createData> -+ <createData entity="SimpleProduct2" stepKey="firstSimpleProductForFixedWithAttribute"/> -+ <createData entity="SimpleProduct2" stepKey="secondSimpleProductForFixedWithAttribute"/> -+ <createData entity="DropDownBundleOption" stepKey="createBundleOptionWithAttribute"> -+ <requiredEntity createDataKey="createFixedBundleProductWithAttribute"/> -+ </createData> -+ <createData entity="ApiBundleLink" stepKey="firstLinkOptionToFixedProductWithAttribute"> -+ <requiredEntity createDataKey="createFixedBundleProductWithAttribute"/> -+ <requiredEntity createDataKey="createBundleOptionWithAttribute"/> -+ <requiredEntity createDataKey="firstSimpleProductForFixedWithAttribute"/> -+ </createData> -+ <createData entity="ApiBundleLink" stepKey="secondLinkOptionToFixedProductWithAttribute"> -+ <requiredEntity createDataKey="createFixedBundleProductWithAttribute"/> -+ <requiredEntity createDataKey="createBundleOptionWithAttribute"/> -+ <requiredEntity createDataKey="secondSimpleProductForFixedWithAttribute"/> -+ </createData> -+ -+ <!-- Run cron twice --> -+ <magentoCLI command="cron:run" stepKey="runCron1"/> -+ <magentoCLI command="cron:run" stepKey="runCron2"/> -+ -+ <!-- Login as admin --> -+ <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> -+ <actionGroup ref="deleteAllExportedFiles" stepKey="clearExportedFilesList"/> -+ </before> -+ <after> -+ <!-- Delete products creations --> -+ <deleteData createDataKey="createDynamicBundleProduct" stepKey="deleteDynamicBundleProduct"/> -+ <deleteData createDataKey="firstSimpleProductForDynamic" stepKey="deleteFirstSimpleProductForDynamic"/> -+ <deleteData createDataKey="secondSimpleProductForDynamic" stepKey="deleteSecondSimpleProductForDynamic"/> -+ <deleteData createDataKey="createFixedBundleProduct" stepKey="deleteFixedBundleProduct"/> -+ <deleteData createDataKey="firstSimpleProductForFixed" stepKey="deleteFirstSimpleProductForFixed"/> -+ <deleteData createDataKey="secondSimpleProductForFixed" stepKey="deleteSecondSimpleProductForFixed"/> -+ <deleteData createDataKey="createFixedBundleProductWithAttribute" stepKey="deleteFixedBundleProductWithAttribute"/> -+ <deleteData createDataKey="firstSimpleProductForFixedWithAttribute" stepKey="deleteFirstSimpleProductForFixedWithAttribute"/> -+ <deleteData createDataKey="secondSimpleProductForFixedWithAttribute" stepKey="deleteSecondSimpleProductForFixedWithAttribute"/> -+ <deleteData createDataKey="createProductAttribute" stepKey="deleteProductAttribute"/> -+ -+ <!-- Delete exported file --> -+ <actionGroup ref="deleteExportedFile" stepKey="deleteExportedFile"> -+ <argument name="rowIndex" value="0"/> -+ </actionGroup> -+ <!-- Log out --> -+ <actionGroup ref="logout" stepKey="logout"/> -+ </after> -+ -+ <!-- Go to export page --> -+ <amOnPage url="{{AdminExportIndexPage.url}}" stepKey="goToExportIndexPage"/> -+ -+ <!-- Export created below products --> -+ <actionGroup ref="exportAllProducts" stepKey="exportCreatedProducts"/> -+ -+ <!-- Run cron --> -+ <magentoCLI command="cron:run" stepKey="runCron3"/> -+ <magentoCLI command="cron:run" stepKey="runCron4"/> -+ -+ <!-- Download product --> -+ <actionGroup ref="downloadFileByRowIndex" stepKey="downloadCreatedProducts"> -+ <argument name="rowIndex" value="0"/> -+ </actionGroup> -+ </test> -+</tests> -diff --git a/app/code/Magento/CatalogImportExport/Test/Mftf/Test/AdminExportGroupedProductWithSpecialPriceTest.xml b/app/code/Magento/CatalogImportExport/Test/Mftf/Test/AdminExportGroupedProductWithSpecialPriceTest.xml -new file mode 100644 -index 00000000000..c47b7dc83af ---- /dev/null -+++ b/app/code/Magento/CatalogImportExport/Test/Mftf/Test/AdminExportGroupedProductWithSpecialPriceTest.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="AdminExportGroupedProductWithSpecialPriceTest"> -+ <annotations> -+ <features value="CatalogImportExport"/> -+ <stories value="Export products"/> -+ <title value="Export grouped product with special price"/> -+ <description value="Admin should be able to export grouped product with special price"/> -+ <severity value="CRITICAL"/> -+ <testCaseId value="MC-14009"/> -+ <group value="catalog_import_export"/> -+ <group value="mtf_migrated"/> -+ <skip> -+ <issueId value="MC-15934"/> -+ </skip> -+ </annotations> -+ <before> -+ <!-- Create first simple product and add special price --> -+ <createData entity="SimpleProduct2" stepKey="createFirstSimpleProduct"/> -+ <createData entity="specialProductPrice2" stepKey="specialPriceToFirstProduct"> -+ <requiredEntity createDataKey="createFirstSimpleProduct"/> -+ </createData> -+ -+ <!-- Create second simple product and add special price--> -+ <createData entity="SimpleProduct2" stepKey="createSecondSimpleProduct"/> -+ <createData entity="specialProductPrice2" stepKey="specialPriceToSecondProduct"> -+ <requiredEntity createDataKey="createSecondSimpleProduct"/> -+ </createData> -+ -+ <!-- Create category --> -+ <createData entity="_defaultCategory" stepKey="createCategory"/> -+ -+ <!-- Create group product with created below simple products --> -+ <createData entity="ApiGroupedProduct2" stepKey="createGroupedProduct"> -+ <requiredEntity createDataKey="createCategory"/> -+ </createData> -+ <createData entity="OneSimpleProductLink" stepKey="addFirstProduct"> -+ <requiredEntity createDataKey="createGroupedProduct"/> -+ <requiredEntity createDataKey="createFirstSimpleProduct"/> -+ </createData> -+ <updateData entity="OneMoreSimpleProductLink" createDataKey="addFirstProduct" stepKey="addSecondProduct"> -+ <requiredEntity createDataKey="createGroupedProduct"/> -+ <requiredEntity createDataKey="createSecondSimpleProduct"/> -+ </updateData> -+ -+ <!-- Run cron twice --> -+ <magentoCLI command="cron:run" stepKey="runCron1"/> -+ <magentoCLI command="cron:run" stepKey="runCron2"/> -+ -+ <!-- Login as admin --> -+ <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> -+ <actionGroup ref="deleteAllExportedFiles" stepKey="clearExportedFilesList"/> -+ </before> -+ <after> -+ <!-- Deleted created products --> -+ <deleteData createDataKey="createFirstSimpleProduct" stepKey="deleteFirstSimpleProduct"/> -+ <deleteData createDataKey="createSecondSimpleProduct" stepKey="deleteSecondSimpleProduct"/> -+ <deleteData createDataKey="createGroupedProduct" stepKey="deleteGroupedProduct"/> -+ -+ <!-- Delete category --> -+ <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> -+ -+ <!-- Delete exported file --> -+ <actionGroup ref="deleteExportedFile" stepKey="deleteExportedFile"> -+ <argument name="rowIndex" value="0"/> -+ </actionGroup> -+ <!-- Log out --> -+ <actionGroup ref="logout" stepKey="logout"/> -+ </after> -+ -+ <!-- Go to export page --> -+ <amOnPage url="{{AdminExportIndexPage.url}}" stepKey="goToExportIndexPage"/> -+ <waitForPageLoad stepKey="waitForExportIndexPageLoad"/> -+ -+ <!-- Export created below products --> -+ <actionGroup ref="exportAllProducts" stepKey="exportCreatedProducts"/> -+ -+ <!-- Run cron --> -+ <magentoCLI command="cron:run" stepKey="runCron3"/> -+ <magentoCLI command="cron:run" stepKey="runCron4"/> -+ -+ <!-- Download product --> -+ <actionGroup ref="downloadFileByRowIndex" stepKey="downloadCreatedProducts"> -+ <argument name="rowIndex" value="0"/> -+ </actionGroup> -+ </test> -+</tests> -diff --git a/app/code/Magento/CatalogImportExport/Test/Mftf/Test/AdminExportSimpleAndConfigurableProductsWithCustomOptionsTest.xml b/app/code/Magento/CatalogImportExport/Test/Mftf/Test/AdminExportSimpleAndConfigurableProductsWithCustomOptionsTest.xml -new file mode 100644 -index 00000000000..160abe61799 ---- /dev/null -+++ b/app/code/Magento/CatalogImportExport/Test/Mftf/Test/AdminExportSimpleAndConfigurableProductsWithCustomOptionsTest.xml -@@ -0,0 +1,122 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> -+ <test name="AdminExportSimpleAndConfigurableProductsWithCustomOptionsTest"> -+ <annotations> -+ <features value="CatalogImportExport"/> -+ <stories value="Export products"/> -+ <title value="Export Simple and Configurable products with custom options"/> -+ <description value="Admin should be able to export Simple and Configurable products with custom options"/> -+ <severity value="CRITICAL"/> -+ <testCaseId value="MC-14005"/> -+ <group value="catalog_import_export"/> -+ <group value="mtf_migrated"/> -+ <skip> -+ <issueId value="MC-15934"/> -+ </skip> -+ </annotations> -+ <before> -+ <!-- Create category --> -+ <createData entity="SimpleSubCategory" stepKey="createCategory"/> -+ -+ <!-- Create configurable product with two attributes --> -+ <createData entity="ApiConfigurableProduct" stepKey="createConfigProduct"> -+ <requiredEntity createDataKey="createCategory"/> -+ </createData> -+ <createData entity="productAttributeWithTwoOptions" stepKey="createConfigProductAttribute"/> -+ <createData entity="productAttributeOption1" stepKey="createConfigProductAttributeFirstOption"> -+ <requiredEntity createDataKey="createConfigProductAttribute"/> -+ </createData> -+ <createData entity="productAttributeOption2" stepKey="createConfigProductAttributeSecondOption"> -+ <requiredEntity createDataKey="createConfigProductAttribute"/> -+ </createData> -+ <createData entity="AddToDefaultSet" stepKey="createConfigAddToAttributeSet"> -+ <requiredEntity createDataKey="createConfigProductAttribute"/> -+ </createData> -+ <getData entity="ProductAttributeOptionGetter" index="1" stepKey="getConfigAttributeFirstOption"> -+ <requiredEntity createDataKey="createConfigProductAttribute"/> -+ </getData> -+ <getData entity="ProductAttributeOptionGetter" index="2" stepKey="getConfigAttributeSecondOption"> -+ <requiredEntity createDataKey="createConfigProductAttribute"/> -+ </getData> -+ -+ <!-- Add custom options to configurable product --> -+ <updateData createDataKey="createConfigProduct" entity="productWithOptions" stepKey="updateProductWithOptions"/> -+ -+ <!-- Create two simple product which will be the part of configurable product --> -+ <createData entity="ApiSimpleOne" stepKey="createConfigFirstChildProduct"> -+ <requiredEntity createDataKey="createConfigProductAttribute"/> -+ <requiredEntity createDataKey="getConfigAttributeFirstOption"/> -+ </createData> -+ <createData entity="ApiSimpleTwo" stepKey="createConfigSecondChildProduct"> -+ <requiredEntity createDataKey="createConfigProductAttribute"/> -+ <requiredEntity createDataKey="getConfigAttributeSecondOption"/> -+ </createData> -+ <createData entity="ConfigurableProductTwoOptions" stepKey="createConfigProductOption"> -+ <requiredEntity createDataKey="createConfigProduct"/> -+ <requiredEntity createDataKey="createConfigProductAttribute"/> -+ <requiredEntity createDataKey="getConfigAttributeFirstOption"/> -+ <requiredEntity createDataKey="getConfigAttributeSecondOption"/> -+ </createData> -+ -+ <!-- Add created below children products to configurable product --> -+ <createData entity="ConfigurableProductAddChild" stepKey="createConfigProductAddFirstChild"> -+ <requiredEntity createDataKey="createConfigProduct"/> -+ <requiredEntity createDataKey="createConfigFirstChildProduct"/> -+ </createData> -+ <createData entity="ConfigurableProductAddChild" stepKey="createConfigProductAddSecondChild"> -+ <requiredEntity createDataKey="createConfigProduct"/> -+ <requiredEntity createDataKey="createConfigSecondChildProduct"/> -+ </createData> -+ -+ <!-- Run cron twice --> -+ <magentoCLI command="cron:run" stepKey="runCron1"/> -+ <magentoCLI command="cron:run" stepKey="runCron2"/> -+ -+ <!-- Login as admin --> -+ <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> -+ <actionGroup ref="deleteAllExportedFiles" stepKey="clearExportedFilesList"/> -+ </before> -+ <after> -+ <!-- Delete configurable product creation --> -+ <deleteData createDataKey="createConfigProduct" stepKey="deleteConfigProduct"/> -+ <deleteData createDataKey="createConfigFirstChildProduct" stepKey="deleteConfigFirstChildProduct"/> -+ <deleteData createDataKey="createConfigSecondChildProduct" stepKey="deleteConfigSecondChildProduct"/> -+ <deleteData createDataKey="createConfigProductAttribute" stepKey="deleteConfigProductAttribute"/> -+ <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> -+ -+ <!-- Delete exported file --> -+ <actionGroup ref="deleteExportedFile" stepKey="deleteExportedFile"> -+ <argument name="rowIndex" value="0"/> -+ </actionGroup> -+ <!-- Log out --> -+ <actionGroup ref="logout" stepKey="logout"/> -+ </after> -+ -+ <!-- Go to export page --> -+ <amOnPage url="{{AdminExportIndexPage.url}}" stepKey="goToExportIndexPage"/> -+ <waitForPageLoad stepKey="waitForExportIndexPageLoad"/> -+ -+ <!-- Fill entity attributes data --> -+ <actionGroup ref="exportProductsFilterByAttribute" stepKey="exportProductBySku"> -+ <argument name="attribute" value="sku"/> -+ <argument name="attributeData" value="$$createConfigProduct.sku$$"/> -+ </actionGroup> -+ -+ <!-- Run cron --> -+ <magentoCLI command="cron:run" stepKey="runCron3"/> -+ <magentoCLI command="cron:run" stepKey="runCron4"/> -+ -+ <!-- Download product --> -+ <actionGroup ref="downloadFileByRowIndex" stepKey="downloadCreatedProducts"> -+ <argument name="rowIndex" value="0"/> -+ </actionGroup> -+ </test> -+</tests> -diff --git a/app/code/Magento/CatalogImportExport/Test/Mftf/Test/AdminExportSimpleProductAndConfigurableProductsWithAssignedImagesTest.xml b/app/code/Magento/CatalogImportExport/Test/Mftf/Test/AdminExportSimpleProductAndConfigurableProductsWithAssignedImagesTest.xml -new file mode 100644 -index 00000000000..3b8da4055ab ---- /dev/null -+++ b/app/code/Magento/CatalogImportExport/Test/Mftf/Test/AdminExportSimpleProductAndConfigurableProductsWithAssignedImagesTest.xml -@@ -0,0 +1,138 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> -+ <test name="AdminExportSimpleProductAndConfigurableProductsWithAssignedImagesTest"> -+ <annotations> -+ <features value="CatalogImportExport"/> -+ <stories value="Export products"/> -+ <title value="Export Simple product and Configurable products with assigned images"/> -+ <description value="Admin should be able to export Simple and Configurable products with assigned images"/> -+ <severity value="CRITICAL"/> -+ <testCaseId value="MC-14004"/> -+ <group value="catalog_import_export"/> -+ <group value="mtf_migrated"/> -+ <skip> -+ <issueId value="MC-17140"/> -+ </skip> -+ </annotations> -+ <before> -+ <!-- Create category --> -+ <createData entity="SimpleSubCategory" stepKey="createCategory"/> -+ -+ <!-- Create configurable product with two attributes --> -+ <createData entity="ApiConfigurableProduct" stepKey="createConfigProduct"> -+ <requiredEntity createDataKey="createCategory"/> -+ </createData> -+ <createData entity="productAttributeWithTwoOptions" stepKey="createConfigProductAttribute"/> -+ <createData entity="productAttributeOption1" stepKey="createConfigProductAttributeFirstOption"> -+ <requiredEntity createDataKey="createConfigProductAttribute"/> -+ </createData> -+ <createData entity="productAttributeOption2" stepKey="createConfigProductAttributeSecondOption"> -+ <requiredEntity createDataKey="createConfigProductAttribute"/> -+ </createData> -+ <createData entity="AddToDefaultSet" stepKey="createConfigAddToAttributeSet"> -+ <requiredEntity createDataKey="createConfigProductAttribute"/> -+ </createData> -+ <getData entity="ProductAttributeOptionGetter" index="1" stepKey="getConfigAttributeFirstOption"> -+ <requiredEntity createDataKey="createConfigProductAttribute"/> -+ </getData> -+ <getData entity="ProductAttributeOptionGetter" index="2" stepKey="getConfigAttributeSecondOption"> -+ <requiredEntity createDataKey="createConfigProductAttribute"/> -+ </getData> -+ -+ <!-- Create first simple product which will be the part of configurable product --> -+ <createData entity="ApiSimpleOne" stepKey="createConfigFirstChildProduct"> -+ <requiredEntity createDataKey="createConfigProductAttribute"/> -+ <requiredEntity createDataKey="getConfigAttributeFirstOption"/> -+ </createData> -+ -+ <!-- Add image to first simple product --> -+ <createData entity="ApiProductAttributeMediaGalleryEntryTestImage" stepKey="createConfigChildFirstProductImage"> -+ <requiredEntity createDataKey="createConfigFirstChildProduct"/> -+ </createData> -+ -+ <!-- Create second simple product which will be the part of configurable product --> -+ <createData entity="ApiSimpleTwo" stepKey="createConfigSecondChildProduct"> -+ <requiredEntity createDataKey="createConfigProductAttribute"/> -+ <requiredEntity createDataKey="getConfigAttributeSecondOption"/> -+ </createData> -+ -+ <!-- Add image to second simple product --> -+ <createData entity="ApiProductAttributeMediaGalleryEntryMagentoLogo" stepKey="createConfigSecondChildProductImage"> -+ <requiredEntity createDataKey="createConfigSecondChildProduct"/> -+ </createData> -+ -+ <!-- Add two options to configurable product --> -+ <createData entity="ConfigurableProductTwoOptions" stepKey="createConfigProductOption"> -+ <requiredEntity createDataKey="createConfigProduct"/> -+ <requiredEntity createDataKey="createConfigProductAttribute"/> -+ <requiredEntity createDataKey="getConfigAttributeFirstOption"/> -+ <requiredEntity createDataKey="getConfigAttributeSecondOption"/> -+ </createData> -+ -+ <!-- Add created below children products to configurable product --> -+ <createData entity="ConfigurableProductAddChild" stepKey="createConfigProductAddFirstChild"> -+ <requiredEntity createDataKey="createConfigProduct"/> -+ <requiredEntity createDataKey="createConfigFirstChildProduct"/> -+ </createData> -+ <createData entity="ConfigurableProductAddChild" stepKey="createConfigProductAddSecondChild"> -+ <requiredEntity createDataKey="createConfigProduct"/> -+ <requiredEntity createDataKey="createConfigSecondChildProduct"/> -+ </createData> -+ -+ <!-- Add image to configurable product --> -+ <createData entity="ApiProductAttributeMediaGalleryEntryTestImage" stepKey="createConfigProductImage"> -+ <requiredEntity createDataKey="createConfigProduct"/> -+ </createData> -+ -+ <!-- Run cron twice --> -+ <magentoCLI command="cron:run" stepKey="runCron1"/> -+ <magentoCLI command="cron:run" stepKey="runCron2"/> -+ -+ <!-- Login as admin --> -+ <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> -+ <actionGroup ref="deleteAllExportedFiles" stepKey="clearExportedFilesList"/> -+ </before> -+ <after> -+ <!-- Delete configurable product creation --> -+ <deleteData createDataKey="createConfigProduct" stepKey="deleteConfigProduct"/> -+ <deleteData createDataKey="createConfigFirstChildProduct" stepKey="deleteConfigFirstChildProduct"/> -+ <deleteData createDataKey="createConfigSecondChildProduct" stepKey="deleteConfigSecondChildProduct"/> -+ <deleteData createDataKey="createConfigProductAttribute" stepKey="deleteConfigProductAttribute"/> -+ <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> -+ -+ <!-- Delete exported file --> -+ <actionGroup ref="deleteExportedFile" stepKey="deleteExportedFile"> -+ <argument name="rowIndex" value="0"/> -+ </actionGroup> -+ <!-- Log out --> -+ <actionGroup ref="logout" stepKey="logout"/> -+ </after> -+ -+ <!-- Go to export page --> -+ <amOnPage url="{{AdminExportIndexPage.url}}" stepKey="goToExportIndexPage"/> -+ <waitForPageLoad stepKey="waitForExportIndexPageLoad"/> -+ -+ <!-- Fill entity attributes data --> -+ <actionGroup ref="exportProductsFilterByAttribute" stepKey="exportProductBySku"> -+ <argument name="attribute" value="sku"/> -+ <argument name="attributeData" value="$$createConfigProduct.sku$$"/> -+ </actionGroup> -+ -+ <!-- Run cron --> -+ <magentoCLI command="cron:run" stepKey="runCron3"/> -+ <magentoCLI command="cron:run" stepKey="runCron4"/> -+ -+ <!-- Download product --> -+ <actionGroup ref="downloadFileByRowIndex" stepKey="downloadCreatedProducts"> -+ <argument name="rowIndex" value="0"/> -+ </actionGroup> -+ </test> -+</tests> -diff --git a/app/code/Magento/CatalogImportExport/Test/Mftf/Test/AdminExportSimpleProductAssignedToMainWebsiteAndConfigurableProductAssignedToCustomWebsiteTest.xml b/app/code/Magento/CatalogImportExport/Test/Mftf/Test/AdminExportSimpleProductAssignedToMainWebsiteAndConfigurableProductAssignedToCustomWebsiteTest.xml -new file mode 100644 -index 00000000000..271b4621d1a ---- /dev/null -+++ b/app/code/Magento/CatalogImportExport/Test/Mftf/Test/AdminExportSimpleProductAssignedToMainWebsiteAndConfigurableProductAssignedToCustomWebsiteTest.xml -@@ -0,0 +1,117 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> -+ <test name="AdminExportSimpleProductAssignedToMainWebsiteAndConfigurableProductAssignedToCustomWebsiteTest"> -+ <annotations> -+ <features value="CatalogImportExport"/> -+ <stories value="Export products"/> -+ <title value="Export Simple Product assigned to Main Website and Configurable Product assigned to Custom Website"/> -+ <description value="Admin should be able to export Simple Product assigned to Main Website and Configurable Product assigned to Custom Website"/> -+ <severity value="CRITICAL"/> -+ <testCaseId value="MC-14006"/> -+ <group value="catalog_import_export"/> -+ <group value="mtf_migrated"/> -+ </annotations> -+ <before> -+ <!-- Create simple product --> -+ <createData entity="_defaultCategory" stepKey="createCategory"/> -+ <createData entity="_defaultProduct" stepKey="createSimpleProduct"> -+ <requiredEntity createDataKey="createCategory"/> -+ </createData> -+ -+ <!-- Create configurable product --> -+ <createData entity="ApiConfigurableProduct" stepKey="createConfigProduct"> -+ <requiredEntity createDataKey="createCategory"/> -+ </createData> -+ <createData entity="productAttributeWithTwoOptions" stepKey="createConfigProductAttribute"/> -+ <createData entity="productAttributeOption1" stepKey="createConfigProductAttributeFirstOption"> -+ <requiredEntity createDataKey="createConfigProductAttribute"/> -+ </createData> -+ <createData entity="productAttributeOption2" stepKey="createConfigProductAttributeSecondOption"> -+ <requiredEntity createDataKey="createConfigProductAttribute"/> -+ </createData> -+ <createData entity="AddToDefaultSet" stepKey="createConfigAddToAttributeSet"> -+ <requiredEntity createDataKey="createConfigProductAttribute"/> -+ </createData> -+ <getData entity="ProductAttributeOptionGetter" index="1" stepKey="getConfigAttributeFirstOption"> -+ <requiredEntity createDataKey="createConfigProductAttribute"/> -+ </getData> -+ <getData entity="ProductAttributeOptionGetter" index="2" stepKey="getConfigAttributeSecondOption"> -+ <requiredEntity createDataKey="createConfigProductAttribute"/> -+ </getData> -+ -+ <!-- Create two simple product which will be the part of configurable product --> -+ <createData entity="ApiSimpleOne" stepKey="createConfigFirstChildProduct"> -+ <requiredEntity createDataKey="createConfigProductAttribute"/> -+ <requiredEntity createDataKey="getConfigAttributeFirstOption"/> -+ </createData> -+ <createData entity="ApiSimpleTwo" stepKey="createConfigSecondChildProduct"> -+ <requiredEntity createDataKey="createConfigProductAttribute"/> -+ <requiredEntity createDataKey="getConfigAttributeSecondOption"/> -+ </createData> -+ <createData entity="ConfigurableProductTwoOptions" stepKey="createConfigProductOption"> -+ <requiredEntity createDataKey="createConfigProduct"/> -+ <requiredEntity createDataKey="createConfigProductAttribute"/> -+ <requiredEntity createDataKey="getConfigAttributeFirstOption"/> -+ <requiredEntity createDataKey="getConfigAttributeSecondOption"/> -+ </createData> -+ <createData entity="ConfigurableProductAddChild" stepKey="createConfigProductAddFirstChild"> -+ <requiredEntity createDataKey="createConfigProduct"/> -+ <requiredEntity createDataKey="createConfigFirstChildProduct"/> -+ </createData> -+ <createData entity="ConfigurableProductAddChild" stepKey="createConfigProductAddSecondChild"> -+ <requiredEntity createDataKey="createConfigProduct"/> -+ <requiredEntity createDataKey="createConfigSecondChildProduct"/> -+ </createData> -+ -+ <!-- Run cron twice --> -+ <magentoCLI command="cron:run" stepKey="runCron1"/> -+ <magentoCLI command="cron:run" stepKey="runCron2"/> -+ -+ <!-- Login as admin --> -+ <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> -+ <actionGroup ref="deleteAllExportedFiles" stepKey="clearExportedFilesList"/> -+ </before> -+ <after> -+ <!-- Delete simple product --> -+ <deleteData createDataKey="createSimpleProduct" stepKey="deleteSimpleProduct"/> -+ -+ <!-- Delete configurable product creation --> -+ <deleteData createDataKey="createConfigProduct" stepKey="deleteConfigProduct"/> -+ <deleteData createDataKey="createConfigFirstChildProduct" stepKey="deleteConfigFirstChildProduct"/> -+ <deleteData createDataKey="createConfigSecondChildProduct" stepKey="deleteConfigSecondChildProduct"/> -+ <deleteData createDataKey="createConfigProductAttribute" stepKey="deleteConfigProductAttribute"/> -+ <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> -+ -+ <!-- Delete exported file --> -+ <actionGroup ref="deleteExportedFile" stepKey="deleteExportedFile"> -+ <argument name="rowIndex" value="0"/> -+ </actionGroup> -+ <!-- Log out --> -+ <actionGroup ref="logout" stepKey="logout"/> -+ </after> -+ -+ <!-- Go to export page --> -+ <amOnPage url="{{AdminExportIndexPage.url}}" stepKey="goToExportIndexPage"/> -+ <waitForPageLoad stepKey="waitForExportIndexPageLoad"/> -+ -+ <!-- Export created below products --> -+ <actionGroup ref="exportAllProducts" stepKey="exportCreatedProducts"/> -+ -+ <!-- Run cron --> -+ <magentoCLI command="cron:run" stepKey="runCron3"/> -+ <magentoCLI command="cron:run" stepKey="runCron4"/> -+ -+ <!-- Download product --> -+ <actionGroup ref="downloadFileByRowIndex" stepKey="downloadCreatedProducts"> -+ <argument name="rowIndex" value="0"/> -+ </actionGroup> -+ </test> -+</tests> -diff --git a/app/code/Magento/CatalogImportExport/Test/Mftf/Test/AdminExportSimpleProductWithCustomAttributeTest.xml b/app/code/Magento/CatalogImportExport/Test/Mftf/Test/AdminExportSimpleProductWithCustomAttributeTest.xml -new file mode 100644 -index 00000000000..f958978a9ef ---- /dev/null -+++ b/app/code/Magento/CatalogImportExport/Test/Mftf/Test/AdminExportSimpleProductWithCustomAttributeTest.xml -@@ -0,0 +1,72 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> -+ <test name="AdminExportSimpleProductWithCustomAttributeTest"> -+ <annotations> -+ <features value="CatalogImportExport"/> -+ <stories value="Export products"/> -+ <title value="Export Simple Product with custom attribute"/> -+ <description value="Admin should be able to export Simple Product with custom attribute"/> -+ <severity value="CRITICAL"/> -+ <testCaseId value="MC-14007"/> -+ <group value="catalog_import_export"/> -+ <group value="mtf_migrated"/> -+ <skip> -+ <issueId value="MC-15934"/> -+ </skip> -+ </annotations> -+ <before> -+ <!-- Create simple product with custom attribute set --> -+ <createData entity="_defaultCategory" stepKey="createCategory"/> -+ <createData entity="CatalogAttributeSet" stepKey="createAttributeSet"/> -+ <createData entity="SimpleProductWithCustomAttributeSet" stepKey="createSimpleProductWithCustomAttributeSet"> -+ <requiredEntity createDataKey="createCategory"/> -+ <requiredEntity createDataKey="createAttributeSet"/> -+ </createData> -+ -+ <!-- Run cron twice --> -+ <magentoCLI command="cron:run" stepKey="runCron1"/> -+ <magentoCLI command="cron:run" stepKey="runCron2"/> -+ -+ <!-- Login as admin --> -+ <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> -+ <actionGroup ref="deleteAllExportedFiles" stepKey="clearExportedFilesList"/> -+ </before> -+ <after> -+ <!-- Delete product creations --> -+ <deleteData createDataKey="createSimpleProductWithCustomAttributeSet" stepKey="deleteSimpleProductWithCustomAttributeSet"/> -+ <deleteData createDataKey="createAttributeSet" stepKey="deleteAttributeSet"/> -+ <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> -+ -+ <!-- Delete exported file --> -+ <actionGroup ref="deleteExportedFile" stepKey="deleteExportedFile"> -+ <argument name="rowIndex" value="0"/> -+ </actionGroup> -+ <!-- Log out --> -+ <actionGroup ref="logout" stepKey="logout"/> -+ </after> -+ -+ <!-- Go to export page --> -+ <amOnPage url="{{AdminExportIndexPage.url}}" stepKey="goToExportIndexPage"/> -+ <waitForPageLoad stepKey="waitForExportIndexPageLoad"/> -+ -+ <!-- Export created below products --> -+ <actionGroup ref="exportAllProducts" stepKey="exportCreatedProducts"/> -+ -+ <!-- Run cron --> -+ <magentoCLI command="cron:run" stepKey="runCron3"/> -+ <magentoCLI command="cron:run" stepKey="runCron4"/> -+ -+ <!-- Download product --> -+ <actionGroup ref="downloadFileByRowIndex" stepKey="downloadCreatedProducts"> -+ <argument name="rowIndex" value="0"/> -+ </actionGroup> -+ </test> -+</tests> -diff --git a/app/code/Magento/CatalogImportExport/Test/Unit/Model/Import/Product/CategoryProcessorTest.php b/app/code/Magento/CatalogImportExport/Test/Unit/Model/Import/Product/CategoryProcessorTest.php -index 69f4a465e33..919f0cfda7c 100644 ---- a/app/code/Magento/CatalogImportExport/Test/Unit/Model/Import/Product/CategoryProcessorTest.php -+++ b/app/code/Magento/CatalogImportExport/Test/Unit/Model/Import/Product/CategoryProcessorTest.php -@@ -34,41 +34,49 @@ class CategoryProcessorTest extends \PHPUnit\Framework\TestCase - */ - protected $product; - -+ /** -+ * @var \Magento\Catalog\Model\Category -+ */ -+ private $childCategory; -+ -+ /** -+ * \Magento\Catalog\Model\Category -+ */ -+ private $parentCategory; -+ - protected function setUp() - { - $this->objectManager = new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this); - $this->objectManagerHelper = new ObjectManagerHelper($this); - -- $childCategory = $this->getMockBuilder(\Magento\Catalog\Model\Category::class) -+ $this->childCategory = $this->getMockBuilder(\Magento\Catalog\Model\Category::class) - ->disableOriginalConstructor() - ->getMock(); -- $childCategory->method('getId')->will($this->returnValue(self::CHILD_CATEGORY_ID)); -- $childCategory->method('getName')->will($this->returnValue(self::CHILD_CATEGORY_NAME)); -- $childCategory->method('getPath')->will($this->returnValue( -+ $this->childCategory->method('getId')->will($this->returnValue(self::CHILD_CATEGORY_ID)); -+ $this->childCategory->method('getName')->will($this->returnValue(self::CHILD_CATEGORY_NAME)); -+ $this->childCategory->method('getPath')->will($this->returnValue( - self::PARENT_CATEGORY_ID . CategoryProcessor::DELIMITER_CATEGORY - . self::CHILD_CATEGORY_ID - )); - -- $childCategory->method('save')->willThrowException(new \Exception()); -- -- $parentCategory = $this->getMockBuilder(\Magento\Catalog\Model\Category::class) -+ $this->parentCategory = $this->getMockBuilder(\Magento\Catalog\Model\Category::class) - ->disableOriginalConstructor() - ->getMock(); -- $parentCategory->method('getId')->will($this->returnValue(self::PARENT_CATEGORY_ID)); -- $parentCategory->method('getName')->will($this->returnValue('Parent')); -- $parentCategory->method('getPath')->will($this->returnValue(self::PARENT_CATEGORY_ID)); -+ $this->parentCategory->method('getId')->will($this->returnValue(self::PARENT_CATEGORY_ID)); -+ $this->parentCategory->method('getName')->will($this->returnValue('Parent')); -+ $this->parentCategory->method('getPath')->will($this->returnValue(self::PARENT_CATEGORY_ID)); - - $categoryCollection = - $this->objectManagerHelper->getCollectionMock( - \Magento\Catalog\Model\ResourceModel\Category\Collection::class, - [ -- self::PARENT_CATEGORY_ID => $parentCategory, -- self::CHILD_CATEGORY_ID => $childCategory, -+ self::PARENT_CATEGORY_ID => $this->parentCategory, -+ self::CHILD_CATEGORY_ID => $this->childCategory, - ] - ); - $map = [ -- [self::PARENT_CATEGORY_ID, $parentCategory], -- [self::CHILD_CATEGORY_ID, $childCategory], -+ [self::PARENT_CATEGORY_ID, $this->parentCategory], -+ [self::CHILD_CATEGORY_ID, $this->childCategory], - ]; - $categoryCollection->expects($this->any()) - ->method('getItemById') -@@ -91,7 +99,7 @@ class CategoryProcessorTest extends \PHPUnit\Framework\TestCase - - $categoryFactory = $this->createPartialMock(\Magento\Catalog\Model\CategoryFactory::class, ['create']); - -- $categoryFactory->method('create')->will($this->returnValue($childCategory)); -+ $categoryFactory->method('create')->will($this->returnValue($this->childCategory)); - - $this->categoryProcessor = - new \Magento\CatalogImportExport\Model\Import\Product\CategoryProcessor( -@@ -110,11 +118,13 @@ class CategoryProcessorTest extends \PHPUnit\Framework\TestCase - /** - * Tests case when newly created category save throws exception. - */ -- public function testCreateCategoryException() -+ public function testUpsertCategoriesWithAlreadyExistsException() - { -- $method = new \ReflectionMethod(CategoryProcessor::class, 'createCategory'); -- $method->setAccessible(true); -- $method->invoke($this->categoryProcessor, self::CHILD_CATEGORY_NAME, self::PARENT_CATEGORY_ID); -+ $exception = new \Magento\Framework\Exception\AlreadyExistsException(); -+ $categoriesSeparator = '/'; -+ $categoryName = 'Exception Category'; -+ $this->childCategory->method('save')->willThrowException($exception); -+ $this->categoryProcessor->upsertCategories($categoryName, $categoriesSeparator); - $this->assertNotEmpty($this->categoryProcessor->getFailedCategories()); - } - -diff --git a/app/code/Magento/CatalogImportExport/Test/Unit/Model/Import/Product/Type/OptionTest.php b/app/code/Magento/CatalogImportExport/Test/Unit/Model/Import/Product/Type/OptionTest.php -index 98e434f2174..f0a52a67e00 100644 ---- a/app/code/Magento/CatalogImportExport/Test/Unit/Model/Import/Product/Type/OptionTest.php -+++ b/app/code/Magento/CatalogImportExport/Test/Unit/Model/Import/Product/Type/OptionTest.php -@@ -702,7 +702,7 @@ class OptionTest extends \Magento\ImportExport\Test\Unit\Model\Import\AbstractIm - { - $rowData = include __DIR__ . '/_files/row_data_no_custom_option.php'; - $this->_bypassModelMethodGetMultiRowFormat($rowData); -- $this->assertFalse($this->modelMock->validateRow($rowData, 0)); -+ $this->assertTrue($this->modelMock->validateRow($rowData, 0)); - } - - /** -diff --git a/app/code/Magento/CatalogImportExport/Test/Unit/Model/Import/ProductTest.php b/app/code/Magento/CatalogImportExport/Test/Unit/Model/Import/ProductTest.php -index a562916b3b4..f85d33edb5d 100644 ---- a/app/code/Magento/CatalogImportExport/Test/Unit/Model/Import/ProductTest.php -+++ b/app/code/Magento/CatalogImportExport/Test/Unit/Model/Import/ProductTest.php -@@ -3,11 +3,14 @@ - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -+ - namespace Magento\CatalogImportExport\Test\Unit\Model\Import; - -+use Magento\CatalogImportExport\Model\Import\Product; - use Magento\CatalogImportExport\Model\Import\Product\ImageTypeProcessor; - use Magento\Framework\App\Filesystem\DirectoryList; - use Magento\ImportExport\Model\Import; -+use PHPUnit\Framework\MockObject\MockObject; - - /** - * Class ProductTest -@@ -26,126 +29,126 @@ class ProductTest extends \Magento\ImportExport\Test\Unit\Model\Import\AbstractI - - const ENTITY_ID = 13; - -- /** @var \Magento\Framework\DB\Adapter\AdapterInterface|\PHPUnit_Framework_MockObject_MockObject */ -+ /** @var \Magento\Framework\DB\Adapter\AdapterInterface| MockObject */ - protected $_connection; - -- /** @var \Magento\Framework\Json\Helper\Data|\PHPUnit_Framework_MockObject_MockObject */ -+ /** @var \Magento\Framework\Json\Helper\Data| MockObject */ - protected $jsonHelper; - -- /** @var \Magento\ImportExport\Model\ResourceModel\Import\Data|\PHPUnit_Framework_MockObject_MockObject */ -+ /** @var \Magento\ImportExport\Model\ResourceModel\Import\Data| MockObject */ - protected $_dataSourceModel; - -- /** @var \Magento\Framework\App\ResourceConnection|\PHPUnit_Framework_MockObject_MockObject */ -+ /** @var \Magento\Framework\App\ResourceConnection| MockObject */ - protected $resource; - -- /** @var \Magento\ImportExport\Model\ResourceModel\Helper|\PHPUnit_Framework_MockObject_MockObject */ -+ /** @var \Magento\ImportExport\Model\ResourceModel\Helper| MockObject */ - protected $_resourceHelper; - -- /** @var \Magento\Framework\Stdlib\StringUtils|\PHPUnit_Framework_MockObject_MockObject */ -+ /** @var \Magento\Framework\Stdlib\StringUtils|MockObject */ - protected $string; - -- /** @var \Magento\Framework\Event\ManagerInterface|\PHPUnit_Framework_MockObject_MockObject */ -+ /** @var \Magento\Framework\Event\ManagerInterface|MockObject */ - protected $_eventManager; - -- /** @var \Magento\CatalogInventory\Api\StockRegistryInterface|\PHPUnit_Framework_MockObject_MockObject */ -+ /** @var \Magento\CatalogInventory\Api\StockRegistryInterface|MockObject */ - protected $stockRegistry; - -- /** @var \Magento\CatalogImportExport\Model\Import\Product\OptionFactory|\PHPUnit_Framework_MockObject_MockObject */ -+ /** @var \Magento\CatalogImportExport\Model\Import\Product\OptionFactory|MockObject */ - protected $optionFactory; - -- /** @var \Magento\CatalogInventory\Api\StockConfigurationInterface|\PHPUnit_Framework_MockObject_MockObject */ -+ /** @var \Magento\CatalogInventory\Api\StockConfigurationInterface|MockObject */ - protected $stockConfiguration; - -- /** @var \Magento\CatalogInventory\Model\Spi\StockStateProviderInterface|\PHPUnit_Framework_MockObject_MockObject */ -+ /** @var \Magento\CatalogInventory\Model\Spi\StockStateProviderInterface|MockObject */ - protected $stockStateProvider; - -- /** @var \Magento\CatalogImportExport\Model\Import\Product\Option|\PHPUnit_Framework_MockObject_MockObject */ -+ /** @var \Magento\CatalogImportExport\Model\Import\Product\Option|MockObject */ - protected $optionEntity; - -- /** @var \Magento\Framework\Stdlib\DateTime|\PHPUnit_Framework_MockObject_MockObject */ -+ /** @var \Magento\Framework\Stdlib\DateTime|MockObject */ - protected $dateTime; - - /** @var array */ - protected $data; - -- /** @var \Magento\ImportExport\Helper\Data|\PHPUnit_Framework_MockObject_MockObject */ -+ /** @var \Magento\ImportExport\Helper\Data|MockObject */ - protected $importExportData; - -- /** @var \Magento\ImportExport\Model\ResourceModel\Import\Data|\PHPUnit_Framework_MockObject_MockObject */ -+ /** @var \Magento\ImportExport\Model\ResourceModel\Import\Data|MockObject */ - protected $importData; - -- /** @var \Magento\Eav\Model\Config|\PHPUnit_Framework_MockObject_MockObject */ -+ /** @var \Magento\Eav\Model\Config|MockObject */ - protected $config; - -- /** @var \Magento\ImportExport\Model\ResourceModel\Helper|\PHPUnit_Framework_MockObject_MockObject */ -+ /** @var \Magento\ImportExport\Model\ResourceModel\Helper|MockObject */ - protected $resourceHelper; - -- /** @var \Magento\Catalog\Helper\Data|\PHPUnit_Framework_MockObject_MockObject */ -+ /** @var \Magento\Catalog\Helper\Data|MockObject */ - protected $_catalogData; - -- /** @var \Magento\ImportExport\Model\Import\Config|\PHPUnit_Framework_MockObject_MockObject */ -+ /** @var \Magento\ImportExport\Model\Import\Config|MockObject */ - protected $_importConfig; - -- /** @var \PHPUnit_Framework_MockObject_MockObject */ -+ /** @var MockObject */ - protected $_resourceFactory; - - // @codingStandardsIgnoreStart -- /** @var \Magento\Eav\Model\ResourceModel\Entity\Attribute\Set\CollectionFactory|\PHPUnit_Framework_MockObject_MockObject */ -+ /** @var \Magento\Eav\Model\ResourceModel\Entity\Attribute\Set\CollectionFactory|MockObject */ - protected $_setColFactory; - -- /** @var \Magento\CatalogImportExport\Model\Import\Product\Type\Factory|\PHPUnit_Framework_MockObject_MockObject */ -+ /** @var \Magento\CatalogImportExport\Model\Import\Product\Type\Factory|MockObject */ - protected $_productTypeFactory; - -- /** @var \Magento\Catalog\Model\ResourceModel\Product\LinkFactory|\PHPUnit_Framework_MockObject_MockObject */ -+ /** @var \Magento\Catalog\Model\ResourceModel\Product\LinkFactory|MockObject */ - protected $_linkFactory; - -- /** @var \Magento\CatalogImportExport\Model\Import\Proxy\ProductFactory|\PHPUnit_Framework_MockObject_MockObject */ -+ /** @var \Magento\CatalogImportExport\Model\Import\Proxy\ProductFactory|MockObject */ - protected $_proxyProdFactory; - -- /** @var \Magento\CatalogImportExport\Model\Import\UploaderFactory|\PHPUnit_Framework_MockObject_MockObject */ -+ /** @var \Magento\CatalogImportExport\Model\Import\UploaderFactory|MockObject */ - protected $_uploaderFactory; - -- /** @var \Magento\Framework\Filesystem|\PHPUnit_Framework_MockObject_MockObject */ -+ /** @var \Magento\Framework\Filesystem|MockObject */ - protected $_filesystem; - -- /** @var \Magento\Framework\Filesystem\Directory\WriteInterface|\PHPUnit_Framework_MockObject_MockObject */ -+ /** @var \Magento\Framework\Filesystem\Directory\WriteInterface|MockObject */ - protected $_mediaDirectory; - -- /** @var \Magento\CatalogInventory\Model\ResourceModel\Stock\ItemFactory|\PHPUnit_Framework_MockObject_MockObject */ -+ /** @var \Magento\CatalogInventory\Model\ResourceModel\Stock\ItemFactory|MockObject */ - protected $_stockResItemFac; - -- /** @var \Magento\Framework\Stdlib\DateTime\TimezoneInterface|\PHPUnit_Framework_MockObject_MockObject */ -+ /** @var \Magento\Framework\Stdlib\DateTime\TimezoneInterface|MockObject */ - protected $_localeDate; - -- /** @var \Magento\Framework\Indexer\IndexerRegistry|\PHPUnit_Framework_MockObject_MockObject */ -+ /** @var \Magento\Framework\Indexer\IndexerRegistry|MockObject */ - protected $indexerRegistry; - -- /** @var \Psr\Log\LoggerInterface|\PHPUnit_Framework_MockObject_MockObject */ -+ /** @var \Psr\Log\LoggerInterface|MockObject */ - protected $_logger; - -- /** @var \Magento\CatalogImportExport\Model\Import\Product\StoreResolver|\PHPUnit_Framework_MockObject_MockObject */ -+ /** @var \Magento\CatalogImportExport\Model\Import\Product\StoreResolver|MockObject */ - protected $storeResolver; - -- /** @var \Magento\CatalogImportExport\Model\Import\Product\SkuProcessor|\PHPUnit_Framework_MockObject_MockObject */ -+ /** @var \Magento\CatalogImportExport\Model\Import\Product\SkuProcessor|MockObject */ - protected $skuProcessor; - -- /** @var \Magento\CatalogImportExport\Model\Import\Product\CategoryProcessor|\PHPUnit_Framework_MockObject_MockObject */ -+ /** @var \Magento\CatalogImportExport\Model\Import\Product\CategoryProcessor|MockObject */ - protected $categoryProcessor; - -- /** @var \Magento\CatalogImportExport\Model\Import\Product\Validator|\PHPUnit_Framework_MockObject_MockObject */ -+ /** @var \Magento\CatalogImportExport\Model\Import\Product\Validator|MockObject */ - protected $validator; - -- /** @var \Magento\Framework\Model\ResourceModel\Db\ObjectRelationProcessor|\PHPUnit_Framework_MockObject_MockObject */ -+ /** @var \Magento\Framework\Model\ResourceModel\Db\ObjectRelationProcessor|MockObject */ - protected $objectRelationProcessor; - -- /** @var \Magento\Framework\Model\ResourceModel\Db\TransactionManagerInterface|\PHPUnit_Framework_MockObject_MockObject */ -+ /** @var \Magento\Framework\Model\ResourceModel\Db\TransactionManagerInterface|MockObject */ - protected $transactionManager; - -- /** @var \Magento\CatalogImportExport\Model\Import\Product\TaxClassProcessor|\PHPUnit_Framework_MockObject_MockObject */ -+ /** @var \Magento\CatalogImportExport\Model\Import\Product\TaxClassProcessor|MockObject */ - // @codingStandardsIgnoreEnd - protected $taxClassProcessor; - -- /** @var \Magento\CatalogImportExport\Model\Import\Product */ -+ /** @var Product */ - protected $importProduct; - - /** -@@ -153,13 +156,13 @@ class ProductTest extends \Magento\ImportExport\Test\Unit\Model\Import\AbstractI - */ - protected $errorAggregator; - -- /** @var \Magento\Framework\App\Config\ScopeConfigInterface|\PHPUnit_Framework_MockObject_MockObject*/ -+ /** @var \Magento\Framework\App\Config\ScopeConfigInterface|MockObject */ - protected $scopeConfig; - -- /** @var \Magento\Catalog\Model\Product\Url|\PHPUnit_Framework_MockObject_MockObject*/ -+ /** @var \Magento\Catalog\Model\Product\Url|MockObject */ - protected $productUrl; - -- /** @var ImageTypeProcessor|\PHPUnit_Framework_MockObject_MockObject */ -+ /** @var ImageTypeProcessor|MockObject */ - protected $imageTypeProcessor; - - /** -@@ -343,7 +346,7 @@ class ProductTest extends \Magento\ImportExport\Test\Unit\Model\Import\AbstractI - $objectManager = new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this); - - $this->importProduct = $objectManager->getObject( -- \Magento\CatalogImportExport\Model\Import\Product::class, -+ Product::class, - [ - 'jsonHelper' => $this->jsonHelper, - 'importExportData' => $this->importExportData, -@@ -385,7 +388,7 @@ class ProductTest extends \Magento\ImportExport\Test\Unit\Model\Import\AbstractI - 'imageTypeProcessor' => $this->imageTypeProcessor - ] - ); -- $reflection = new \ReflectionClass(\Magento\CatalogImportExport\Model\Import\Product::class); -+ $reflection = new \ReflectionClass(Product::class); - $reflectionProperty = $reflection->getProperty('metadataPool'); - $reflectionProperty->setAccessible(true); - $reflectionProperty->setValue($this->importProduct, $metadataPoolMock); -@@ -627,7 +630,7 @@ class ProductTest extends \Magento\ImportExport\Test\Unit\Model\Import\AbstractI - - public function testDeleteProductsForReplacement() - { -- $importProduct = $this->getMockBuilder(\Magento\CatalogImportExport\Model\Import\Product::class) -+ $importProduct = $this->getMockBuilder(Product::class) - ->disableOriginalConstructor() - ->setMethods([ - 'setParameters', '_deleteProducts' -@@ -693,7 +696,7 @@ class ProductTest extends \Magento\ImportExport\Test\Unit\Model\Import\AbstractI - */ - public function testValidateRow($rowScope, $oldSku, $expectedResult, $behaviour = Import::BEHAVIOR_DELETE) - { -- $importProduct = $this->getMockBuilder(\Magento\CatalogImportExport\Model\Import\Product::class) -+ $importProduct = $this->getMockBuilder(Product::class) - ->disableOriginalConstructor() - ->setMethods(['getBehavior', 'getRowScope', 'getErrorAggregator']) - ->getMock(); -@@ -705,7 +708,7 @@ class ProductTest extends \Magento\ImportExport\Test\Unit\Model\Import\AbstractI - ->method('getErrorAggregator') - ->willReturn($this->getErrorAggregatorObject()); - $importProduct->expects($this->once())->method('getRowScope')->willReturn($rowScope); -- $skuKey = \Magento\CatalogImportExport\Model\Import\Product::COL_SKU; -+ $skuKey = Product::COL_SKU; - $rowData = [ - $skuKey => 'sku', - ]; -@@ -717,18 +720,22 @@ class ProductTest extends \Magento\ImportExport\Test\Unit\Model\Import\AbstractI - - public function testValidateRowDeleteBehaviourAddRowErrorCall() - { -- $importProduct = $this->getMockBuilder(\Magento\CatalogImportExport\Model\Import\Product::class) -+ $importProduct = $this->getMockBuilder(Product::class) - ->disableOriginalConstructor() -- ->setMethods(['getBehavior', 'getRowScope', 'addRowError']) -+ ->setMethods(['getBehavior', 'getRowScope', 'addRowError', 'getErrorAggregator']) - ->getMock(); - - $importProduct->expects($this->exactly(2))->method('getBehavior') - ->willReturn(\Magento\ImportExport\Model\Import::BEHAVIOR_DELETE); - $importProduct->expects($this->once())->method('getRowScope') -- ->willReturn(\Magento\CatalogImportExport\Model\Import\Product::SCOPE_DEFAULT); -+ ->willReturn(Product::SCOPE_DEFAULT); - $importProduct->expects($this->once())->method('addRowError'); -+ $importProduct->method('getErrorAggregator') -+ ->willReturn( -+ $this->getErrorAggregatorObject(['addRowToSkip']) -+ ); - $rowData = [ -- \Magento\CatalogImportExport\Model\Import\Product::COL_SKU => 'sku', -+ Product::COL_SKU => 'sku', - ]; - - $importProduct->validateRow($rowData, 0); -@@ -739,7 +746,7 @@ class ProductTest extends \Magento\ImportExport\Test\Unit\Model\Import\AbstractI - $messages = ['validator message']; - $this->validator->expects($this->once())->method('getMessages')->willReturn($messages); - $rowData = [ -- \Magento\CatalogImportExport\Model\Import\Product::COL_SKU => 'sku', -+ Product::COL_SKU => 'sku', - ]; - $rowNum = 0; - $this->importProduct->validateRow($rowData, $rowNum); -@@ -841,7 +848,7 @@ class ProductTest extends \Magento\ImportExport\Test\Unit\Model\Import\AbstractI - return [ - [ - '$storeCode' => null, -- '$expectedResult' => \Magento\CatalogImportExport\Model\Import\Product::SCOPE_DEFAULT, -+ '$expectedResult' => Product::SCOPE_DEFAULT, - ], - [ - '$storeCode' => 'value', -@@ -856,17 +863,17 @@ class ProductTest extends \Magento\ImportExport\Test\Unit\Model\Import\AbstractI - public function testValidateRowCheckSpecifiedSku($sku, $expectedError) - { - $importProduct = $this->createModelMockWithErrorAggregator( -- [ 'addRowError', 'getOptionEntity', 'getRowScope'], -+ ['addRowError', 'getOptionEntity', 'getRowScope'], - ['isRowInvalid' => true] - ); - - $rowNum = 0; - $rowData = [ -- \Magento\CatalogImportExport\Model\Import\Product::COL_SKU => $sku, -- \Magento\CatalogImportExport\Model\Import\Product::COL_STORE => '', -+ Product::COL_SKU => $sku, -+ Product::COL_STORE => '', - ]; - -- $this->storeResolver->expects($this->any())->method('getStoreCodeToId')->willReturn(null); -+ $this->storeResolver->method('getStoreCodeToId')->willReturn(null); - $this->setPropertyValue($importProduct, 'storeResolver', $this->storeResolver); - $this->setPropertyValue($importProduct, 'skuProcessor', $this->skuProcessor); - -@@ -875,7 +882,7 @@ class ProductTest extends \Magento\ImportExport\Test\Unit\Model\Import\AbstractI - $importProduct - ->expects($this->once()) - ->method('getRowScope') -- ->willReturn(\Magento\CatalogImportExport\Model\Import\Product::SCOPE_STORE); -+ ->willReturn(Product::SCOPE_STORE); - $importProduct->expects($this->at(1))->method('addRowError')->with($expectedError, $rowNum)->willReturn(null); - - $importProduct->validateRow($rowData, $rowNum); -@@ -889,7 +896,7 @@ class ProductTest extends \Magento\ImportExport\Test\Unit\Model\Import\AbstractI - $errorAggregator->method('isRowInvalid')->willReturn(true); - $this->setPropertyValue($this->importProduct, '_processedEntitiesCount', $count); - $this->setPropertyValue($this->importProduct, 'errorAggregator', $errorAggregator); -- $rowData = [\Magento\CatalogImportExport\Model\Import\Product::COL_SKU => false]; -+ $rowData = [Product::COL_SKU => false]; - //suppress validator - $this->_setValidatorMockInImportProduct($this->importProduct); - $this->importProduct->validateRow($rowData, $rowNum); -@@ -899,14 +906,14 @@ class ProductTest extends \Magento\ImportExport\Test\Unit\Model\Import\AbstractI - public function testValidateRowValidateExistingProductTypeAddNewSku() - { - $importProduct = $this->createModelMockWithErrorAggregator( -- [ 'addRowError', 'getOptionEntity'], -+ ['addRowError', 'getOptionEntity'], - ['isRowInvalid' => true] - ); - - $sku = 'sku'; - $rowNum = 0; - $rowData = [ -- \Magento\CatalogImportExport\Model\Import\Product::COL_SKU => $sku, -+ Product::COL_SKU => $sku, - ]; - $oldSku = [ - $sku => [ -@@ -929,7 +936,7 @@ class ProductTest extends \Magento\ImportExport\Test\Unit\Model\Import\AbstractI - $this->setPropertyValue($importProduct, '_oldSku', $oldSku); - - $expectedData = [ -- 'entity_id' => $oldSku[$sku]['entity_id'], //entity_id_val -+ 'entity_id' => $oldSku[$sku]['entity_id'], //entity_id_val - 'type_id' => $oldSku[$sku]['type_id'],// type_id_val - 'attr_set_id' => $oldSku[$sku]['attr_set_id'], //attr_set_id_val - 'attr_set_code' => $_attrSetIdToName[$oldSku[$sku]['attr_set_id']],//attr_set_id_val -@@ -947,7 +954,7 @@ class ProductTest extends \Magento\ImportExport\Test\Unit\Model\Import\AbstractI - $sku = 'sku'; - $rowNum = 0; - $rowData = [ -- \Magento\CatalogImportExport\Model\Import\Product::COL_SKU => $sku, -+ Product::COL_SKU => $sku, - ]; - $oldSku = [ - $sku => [ -@@ -972,6 +979,11 @@ class ProductTest extends \Magento\ImportExport\Test\Unit\Model\Import\AbstractI - - /** - * @dataProvider validateRowValidateNewProductTypeAddRowErrorCallDataProvider -+ * @param string $colType -+ * @param string $productTypeModelsColType -+ * @param string $colAttrSet -+ * @param string $attrSetNameToIdColAttrSet -+ * @param string $error - */ - public function testValidateRowValidateNewProductTypeAddRowErrorCall( - $colType, -@@ -983,15 +995,15 @@ class ProductTest extends \Magento\ImportExport\Test\Unit\Model\Import\AbstractI - $sku = 'sku'; - $rowNum = 0; - $rowData = [ -- \Magento\CatalogImportExport\Model\Import\Product::COL_SKU => $sku, -- \Magento\CatalogImportExport\Model\Import\Product::COL_TYPE => $colType, -- \Magento\CatalogImportExport\Model\Import\Product::COL_ATTR_SET => $colAttrSet, -+ Product::COL_SKU => $sku, -+ Product::COL_TYPE => $colType, -+ Product::COL_ATTR_SET => $colAttrSet, - ]; - $_attrSetNameToId = [ -- $rowData[\Magento\CatalogImportExport\Model\Import\Product::COL_ATTR_SET] => $attrSetNameToIdColAttrSet, -+ $rowData[Product::COL_ATTR_SET] => $attrSetNameToIdColAttrSet, - ]; - $_productTypeModels = [ -- $rowData[\Magento\CatalogImportExport\Model\Import\Product::COL_TYPE] => $productTypeModelsColType, -+ $rowData[Product::COL_TYPE] => $productTypeModelsColType, - ]; - $oldSku = [ - $sku => null, -@@ -1019,29 +1031,25 @@ class ProductTest extends \Magento\ImportExport\Test\Unit\Model\Import\AbstractI - $sku = 'sku'; - $rowNum = 0; - $rowData = [ -- \Magento\CatalogImportExport\Model\Import\Product::COL_SKU => $sku, -- \Magento\CatalogImportExport\Model\Import\Product::COL_TYPE => 'value', -- \Magento\CatalogImportExport\Model\Import\Product::COL_ATTR_SET => 'value', -+ Product::COL_SKU => $sku, -+ Product::COL_TYPE => 'value', -+ Product::COL_ATTR_SET => 'value', - ]; - $_productTypeModels = [ -- $rowData[\Magento\CatalogImportExport\Model\Import\Product::COL_TYPE] => 'value', -+ $rowData[Product::COL_TYPE] => 'value', - ]; - $oldSku = [ - $sku => null, - ]; - $_attrSetNameToId = [ -- $rowData[\Magento\CatalogImportExport\Model\Import\Product::COL_ATTR_SET] => 'attr_set_code_val' -+ $rowData[Product::COL_ATTR_SET] => 'attr_set_code_val' - ]; - $expectedData = [ - 'entity_id' => null, -- 'type_id' => $rowData[\Magento\CatalogImportExport\Model\Import\Product::COL_TYPE],//value -+ 'type_id' => $rowData[Product::COL_TYPE],//value - //attr_set_id_val -- 'attr_set_id' => $_attrSetNameToId[ -- $rowData[ -- \Magento\CatalogImportExport\Model\Import\Product::COL_ATTR_SET -- ] -- ], -- 'attr_set_code' => $rowData[\Magento\CatalogImportExport\Model\Import\Product::COL_ATTR_SET],//value -+ 'attr_set_id' => $_attrSetNameToId[$rowData[Product::COL_ATTR_SET]], -+ 'attr_set_code' => $rowData[Product::COL_ATTR_SET],//value - 'row_id' => null - ]; - $importProduct = $this->createModelMockWithErrorAggregator( -@@ -1077,8 +1085,8 @@ class ProductTest extends \Magento\ImportExport\Test\Unit\Model\Import\AbstractI - $sku = 'sku'; - $rowNum = 0; - $rowData = [ -- \Magento\CatalogImportExport\Model\Import\Product::COL_SKU => $sku, -- \Magento\CatalogImportExport\Model\Import\Product::COL_ATTR_SET => 'col_attr_set_val', -+ Product::COL_SKU => $sku, -+ Product::COL_ATTR_SET => 'col_attr_set_val', - ]; - $expectedAttrSetCode = 'new_attr_set_code'; - $newSku = [ -@@ -1086,8 +1094,8 @@ class ProductTest extends \Magento\ImportExport\Test\Unit\Model\Import\AbstractI - 'type_id' => 'new_type_id_val', - ]; - $expectedRowData = [ -- \Magento\CatalogImportExport\Model\Import\Product::COL_SKU => $sku, -- \Magento\CatalogImportExport\Model\Import\Product::COL_ATTR_SET => $newSku['attr_set_code'], -+ Product::COL_SKU => $sku, -+ Product::COL_ATTR_SET => $newSku['attr_set_code'], - ]; - $oldSku = [ - $sku => [ -@@ -1121,8 +1129,8 @@ class ProductTest extends \Magento\ImportExport\Test\Unit\Model\Import\AbstractI - $sku = 'sku'; - $rowNum = 0; - $rowData = [ -- \Magento\CatalogImportExport\Model\Import\Product::COL_SKU => $sku, -- \Magento\CatalogImportExport\Model\Import\Product::COL_ATTR_SET => 'col_attr_set_val', -+ Product::COL_SKU => $sku, -+ Product::COL_ATTR_SET => 'col_attr_set_val', - ]; - $oldSku = [ - $sku => [ -@@ -1374,7 +1382,7 @@ class ProductTest extends \Magento\ImportExport\Test\Unit\Model\Import\AbstractI - { - return [ - [ -- '$rowScope' => \Magento\CatalogImportExport\Model\Import\Product::SCOPE_DEFAULT, -+ '$rowScope' => Product::SCOPE_DEFAULT, - '$oldSku' => null, - '$expectedResult' => false, - ], -@@ -1389,12 +1397,12 @@ class ProductTest extends \Magento\ImportExport\Test\Unit\Model\Import\AbstractI - '$expectedResult' => true, - ], - [ -- '$rowScope' => \Magento\CatalogImportExport\Model\Import\Product::SCOPE_DEFAULT, -+ '$rowScope' => Product::SCOPE_DEFAULT, - '$oldSku' => true, - '$expectedResult' => true, - ], - [ -- '$rowScope' => \Magento\CatalogImportExport\Model\Import\Product::SCOPE_DEFAULT, -+ '$rowScope' => Product::SCOPE_DEFAULT, - '$oldSku' => null, - '$expectedResult' => false, - '$behaviour' => Import::BEHAVIOR_REPLACE -@@ -1415,7 +1423,7 @@ class ProductTest extends \Magento\ImportExport\Test\Unit\Model\Import\AbstractI - '$rowData' => [ - 'code' => str_repeat( - 'a', -- \Magento\CatalogImportExport\Model\Import\Product::DB_MAX_VARCHAR_LENGTH - 1 -+ Product::DB_MAX_VARCHAR_LENGTH - 1 - ), - ], - ], -@@ -1468,7 +1476,7 @@ class ProductTest extends \Magento\ImportExport\Test\Unit\Model\Import\AbstractI - '$rowData' => [ - 'code' => str_repeat( - 'a', -- \Magento\CatalogImportExport\Model\Import\Product::DB_MAX_TEXT_LENGTH - 1 -+ Product::DB_MAX_TEXT_LENGTH - 1 - ), - ], - ], -@@ -1488,7 +1496,7 @@ class ProductTest extends \Magento\ImportExport\Test\Unit\Model\Import\AbstractI - '$rowData' => [ - 'code' => str_repeat( - 'a', -- \Magento\CatalogImportExport\Model\Import\Product::DB_MAX_VARCHAR_LENGTH + 1 -+ Product::DB_MAX_VARCHAR_LENGTH + 1 - ), - ], - ], -@@ -1541,7 +1549,7 @@ class ProductTest extends \Magento\ImportExport\Test\Unit\Model\Import\AbstractI - '$rowData' => [ - 'code' => str_repeat( - 'a', -- \Magento\CatalogImportExport\Model\Import\Product::DB_MAX_TEXT_LENGTH + 1 -+ Product::DB_MAX_TEXT_LENGTH + 1 - ), - ], - ], -@@ -1553,8 +1561,8 @@ class ProductTest extends \Magento\ImportExport\Test\Unit\Model\Import\AbstractI - */ - public function getRowScopeDataProvider() - { -- $colSku = \Magento\CatalogImportExport\Model\Import\Product::COL_SKU; -- $colStore = \Magento\CatalogImportExport\Model\Import\Product::COL_STORE; -+ $colSku = Product::COL_SKU; -+ $colStore = Product::COL_STORE; - - return [ - [ -@@ -1562,21 +1570,21 @@ class ProductTest extends \Magento\ImportExport\Test\Unit\Model\Import\AbstractI - $colSku => null, - $colStore => 'store', - ], -- '$expectedResult' => \Magento\CatalogImportExport\Model\Import\Product::SCOPE_STORE -+ '$expectedResult' => Product::SCOPE_STORE - ], - [ - '$rowData' => [ - $colSku => 'sku', - $colStore => null, - ], -- '$expectedResult' => \Magento\CatalogImportExport\Model\Import\Product::SCOPE_DEFAULT -+ '$expectedResult' => Product::SCOPE_DEFAULT - ], - [ - '$rowData' => [ - $colSku => 'sku', - $colStore => 'store', - ], -- '$expectedResult' => \Magento\CatalogImportExport\Model\Import\Product::SCOPE_STORE -+ '$expectedResult' => Product::SCOPE_STORE - ], - ]; - } -@@ -1653,9 +1661,9 @@ class ProductTest extends \Magento\ImportExport\Test\Unit\Model\Import\AbstractI - * - * @see _rewriteGetOptionEntityInImportProduct() - * @see _setValidatorMockInImportProduct() -- * @param \Magento\CatalogImportExport\Model\Import\Product -+ * @param Product - * Param should go with rewritten getOptionEntity method. -- * @return \Magento\CatalogImportExport\Model\Import\Product\Option|\PHPUnit_Framework_MockObject_MockObject -+ * @return \Magento\CatalogImportExport\Model\Import\Product\Option|MockObject - */ - private function _suppressValidateRowOptionValidatorInvalidRows($importProduct) - { -@@ -1671,8 +1679,8 @@ class ProductTest extends \Magento\ImportExport\Test\Unit\Model\Import\AbstractI - * Used in group of validateRow method's tests. - * Set validator mock in importProduct, return true for isValid method. - * -- * @param \Magento\CatalogImportExport\Model\Import\Product -- * @return \Magento\CatalogImportExport\Model\Import\Product\Validator|\PHPUnit_Framework_MockObject_MockObject -+ * @param Product -+ * @return \Magento\CatalogImportExport\Model\Import\Product\Validator|MockObject - */ - private function _setValidatorMockInImportProduct($importProduct) - { -@@ -1686,9 +1694,9 @@ class ProductTest extends \Magento\ImportExport\Test\Unit\Model\Import\AbstractI - * Used in group of validateRow method's tests. - * Make getOptionEntity return option mock. - * -- * @param \Magento\CatalogImportExport\Model\Import\Product -+ * @param Product - * Param should go with rewritten getOptionEntity method. -- * @return \Magento\CatalogImportExport\Model\Import\Product\Option|\PHPUnit_Framework_MockObject_MockObject -+ * @return \Magento\CatalogImportExport\Model\Import\Product\Option|MockObject - */ - private function _rewriteGetOptionEntityInImportProduct($importProduct) - { -@@ -1703,12 +1711,12 @@ class ProductTest extends \Magento\ImportExport\Test\Unit\Model\Import\AbstractI - /** - * @param array $methods - * @param array $errorAggregatorMethods -- * @return \PHPUnit_Framework_MockObject_MockObject -+ * @return MockObject - */ - protected function createModelMockWithErrorAggregator(array $methods = [], array $errorAggregatorMethods = []) - { - $methods[] = 'getErrorAggregator'; -- $importProduct = $this->getMockBuilder(\Magento\CatalogImportExport\Model\Import\Product::class) -+ $importProduct = $this->getMockBuilder(Product::class) - ->disableOriginalConstructor() - ->setMethods($methods) - ->getMock(); -diff --git a/app/code/Magento/CatalogImportExport/Test/Unit/Model/Import/UploaderTest.php b/app/code/Magento/CatalogImportExport/Test/Unit/Model/Import/UploaderTest.php -index 262593377aa..f734596de01 100644 ---- a/app/code/Magento/CatalogImportExport/Test/Unit/Model/Import/UploaderTest.php -+++ b/app/code/Magento/CatalogImportExport/Test/Unit/Model/Import/UploaderTest.php -@@ -93,14 +93,14 @@ class UploaderTest extends \PHPUnit\Framework\TestCase - $this->filesystem, - $this->readFactory, - ]) -- ->setMethods(['_setUploadFile', 'save', 'getTmpDir']) -+ ->setMethods(['_setUploadFile', 'save', 'getTmpDir', 'checkAllowedExtension']) - ->getMock(); - } - - /** - * @dataProvider moveFileUrlDataProvider - */ -- public function testMoveFileUrl($fileUrl, $expectedHost, $expectedFileName) -+ public function testMoveFileUrl($fileUrl, $expectedHost, $expectedFileName, $checkAllowedExtension) - { - $destDir = 'var/dest/dir'; - $expectedRelativeFilePath = $expectedFileName; -@@ -128,6 +128,9 @@ class UploaderTest extends \PHPUnit\Framework\TestCase - $this->uploader->expects($this->once())->method('_setUploadFile')->will($this->returnSelf()); - $this->uploader->expects($this->once())->method('save')->with($destDir . '/' . $expectedFileName) - ->willReturn(['name' => $expectedFileName, 'path' => 'absPath']); -+ $this->uploader->expects($this->exactly($checkAllowedExtension)) -+ ->method('checkAllowedExtension') -+ ->willReturn(true); - - $this->uploader->setDestDir($destDir); - $result = $this->uploader->move($fileUrl); -@@ -224,31 +227,37 @@ class UploaderTest extends \PHPUnit\Framework\TestCase - '$fileUrl' => 'http://test_uploader_file', - '$expectedHost' => 'test_uploader_file', - '$expectedFileName' => 'test_uploader_file', -+ '$checkAllowedExtension' => 0 - ], - [ - '$fileUrl' => 'https://!:^&`;file', - '$expectedHost' => '!:^&`;file', - '$expectedFileName' => 'file', -+ '$checkAllowedExtension' => 0 - ], - [ - '$fileUrl' => 'https://www.google.com/image.jpg', - '$expectedHost' => 'www.google.com/image.jpg', - '$expectedFileName' => 'image.jpg', -+ '$checkAllowedExtension' => 1 - ], - [ - '$fileUrl' => 'https://www.google.com/image.jpg?param=1', - '$expectedHost' => 'www.google.com/image.jpg?param=1', - '$expectedFileName' => 'image.jpg', -+ '$checkAllowedExtension' => 1 - ], - [ - '$fileUrl' => 'https://www.google.com/image.jpg?param=1¶m=2', - '$expectedHost' => 'www.google.com/image.jpg?param=1¶m=2', - '$expectedFileName' => 'image.jpg', -+ '$checkAllowedExtension' => 1 - ], - [ - '$fileUrl' => 'http://www.google.com/image.jpg?param=1¶m=2', - '$expectedHost' => 'www.google.com/image.jpg?param=1¶m=2', - '$expectedFileName' => 'image.jpg', -+ '$checkAllowedExtension' => 1 - ], - ]; - } -diff --git a/app/code/Magento/CatalogInventory/Api/Data/StockCollectionInterface.php b/app/code/Magento/CatalogInventory/Api/Data/StockCollectionInterface.php -index c2521f77ca2..9ae22e5e1a3 100644 ---- a/app/code/Magento/CatalogInventory/Api/Data/StockCollectionInterface.php -+++ b/app/code/Magento/CatalogInventory/Api/Data/StockCollectionInterface.php -@@ -15,6 +15,10 @@ use Magento\Framework\Api\SearchResultsInterface; - * Interface StockCollectionInterface - * @api - * @since 100.0.2 -+ * -+ * @deprecated 2.3.0 Replaced with Multi Source Inventory -+ * @link https://devdocs.magento.com/guides/v2.3/inventory/index.html -+ * @link https://devdocs.magento.com/guides/v2.3/inventory/catalog-inventory-replacements.html - */ - interface StockCollectionInterface extends SearchResultsInterface - { -diff --git a/app/code/Magento/CatalogInventory/Api/Data/StockInterface.php b/app/code/Magento/CatalogInventory/Api/Data/StockInterface.php -index 53e95921ea9..087fae6e656 100644 ---- a/app/code/Magento/CatalogInventory/Api/Data/StockInterface.php -+++ b/app/code/Magento/CatalogInventory/Api/Data/StockInterface.php -@@ -11,6 +11,10 @@ use Magento\Framework\Api\ExtensibleDataInterface; - * Interface Stock - * @api - * @since 100.0.2 -+ * -+ * @deprecated 2.3.0 Replaced with Multi Source Inventory -+ * @link https://devdocs.magento.com/guides/v2.3/inventory/index.html -+ * @link https://devdocs.magento.com/guides/v2.3/inventory/catalog-inventory-replacements.html - */ - interface StockInterface extends ExtensibleDataInterface - { -diff --git a/app/code/Magento/CatalogInventory/Api/Data/StockItemCollectionInterface.php b/app/code/Magento/CatalogInventory/Api/Data/StockItemCollectionInterface.php -index 038174c8e52..59a1f58b74c 100644 ---- a/app/code/Magento/CatalogInventory/Api/Data/StockItemCollectionInterface.php -+++ b/app/code/Magento/CatalogInventory/Api/Data/StockItemCollectionInterface.php -@@ -15,6 +15,10 @@ use Magento\Framework\Api\SearchResultsInterface; - * Interface StockItemCollectionInterface - * @api - * @since 100.0.2 -+ * -+ * @deprecated 2.3.0 Replaced with Multi Source Inventory -+ * @link https://devdocs.magento.com/guides/v2.3/inventory/index.html -+ * @link https://devdocs.magento.com/guides/v2.3/inventory/catalog-inventory-replacements.html - */ - interface StockItemCollectionInterface extends SearchResultsInterface - { -diff --git a/app/code/Magento/CatalogInventory/Api/Data/StockItemInterface.php b/app/code/Magento/CatalogInventory/Api/Data/StockItemInterface.php -index b876615468b..e2e8a744c4b 100644 ---- a/app/code/Magento/CatalogInventory/Api/Data/StockItemInterface.php -+++ b/app/code/Magento/CatalogInventory/Api/Data/StockItemInterface.php -@@ -11,6 +11,10 @@ use Magento\Framework\Api\ExtensibleDataInterface; - * Interface StockItem - * @api - * @since 100.0.2 -+ * -+ * @deprecated 2.3.0 Replaced with Multi Source Inventory -+ * @link https://devdocs.magento.com/guides/v2.3/inventory/index.html -+ * @link https://devdocs.magento.com/guides/v2.3/inventory/catalog-inventory-replacements.html - */ - interface StockItemInterface extends ExtensibleDataInterface - { -diff --git a/app/code/Magento/CatalogInventory/Api/Data/StockStatusCollectionInterface.php b/app/code/Magento/CatalogInventory/Api/Data/StockStatusCollectionInterface.php -index 70a2c29ff9a..1cc045745a0 100644 ---- a/app/code/Magento/CatalogInventory/Api/Data/StockStatusCollectionInterface.php -+++ b/app/code/Magento/CatalogInventory/Api/Data/StockStatusCollectionInterface.php -@@ -11,6 +11,10 @@ use Magento\Framework\Api\SearchResultsInterface; - * Stock Status collection interface - * @api - * @since 100.0.2 -+ * -+ * @deprecated 2.3.0 Replaced with Multi Source Inventory -+ * @link https://devdocs.magento.com/guides/v2.3/inventory/index.html -+ * @link https://devdocs.magento.com/guides/v2.3/inventory/catalog-inventory-replacements.html - */ - interface StockStatusCollectionInterface extends SearchResultsInterface - { -diff --git a/app/code/Magento/CatalogInventory/Api/Data/StockStatusInterface.php b/app/code/Magento/CatalogInventory/Api/Data/StockStatusInterface.php -index c9ae6a96a36..66b639fb088 100644 ---- a/app/code/Magento/CatalogInventory/Api/Data/StockStatusInterface.php -+++ b/app/code/Magento/CatalogInventory/Api/Data/StockStatusInterface.php -@@ -11,6 +11,10 @@ use Magento\Framework\Api\ExtensibleDataInterface; - * Interface StockStatusInterface - * @api - * @since 100.0.2 -+ * -+ * @deprecated 2.3.0 Replaced with Multi Source Inventory -+ * @link https://devdocs.magento.com/guides/v2.3/inventory/index.html -+ * @link https://devdocs.magento.com/guides/v2.3/inventory/catalog-inventory-replacements.html - */ - interface StockStatusInterface extends ExtensibleDataInterface - { -diff --git a/app/code/Magento/CatalogInventory/Api/RegisterProductSaleInterface.php b/app/code/Magento/CatalogInventory/Api/RegisterProductSaleInterface.php -index 9122fb00386..6fd1e746697 100644 ---- a/app/code/Magento/CatalogInventory/Api/RegisterProductSaleInterface.php -+++ b/app/code/Magento/CatalogInventory/Api/RegisterProductSaleInterface.php -@@ -12,6 +12,10 @@ use Magento\Framework\Exception\LocalizedException; - - /** - * @api -+ * -+ * @deprecated 2.3.0 Replaced with Multi Source Inventory -+ * @link https://devdocs.magento.com/guides/v2.3/inventory/index.html -+ * @link https://devdocs.magento.com/guides/v2.3/inventory/catalog-inventory-replacements.html - */ - interface RegisterProductSaleInterface - { -diff --git a/app/code/Magento/CatalogInventory/Api/RevertProductSaleInterface.php b/app/code/Magento/CatalogInventory/Api/RevertProductSaleInterface.php -index ed496f3882f..552e30da892 100644 ---- a/app/code/Magento/CatalogInventory/Api/RevertProductSaleInterface.php -+++ b/app/code/Magento/CatalogInventory/Api/RevertProductSaleInterface.php -@@ -9,6 +9,10 @@ namespace Magento\CatalogInventory\Api; - - /** - * @api -+ * -+ * @deprecated 2.3.0 Replaced with Multi Source Inventory -+ * @link https://devdocs.magento.com/guides/v2.3/inventory/index.html -+ * @link https://devdocs.magento.com/guides/v2.3/inventory/catalog-inventory-replacements.html - */ - interface RevertProductSaleInterface - { -diff --git a/app/code/Magento/CatalogInventory/Api/StockConfigurationInterface.php b/app/code/Magento/CatalogInventory/Api/StockConfigurationInterface.php -index a23d5030b82..5019e86b7af 100644 ---- a/app/code/Magento/CatalogInventory/Api/StockConfigurationInterface.php -+++ b/app/code/Magento/CatalogInventory/Api/StockConfigurationInterface.php -@@ -9,6 +9,10 @@ namespace Magento\CatalogInventory\Api; - * Interface StockConfigurationInterface - * @api - * @since 100.0.2 -+ * -+ * @deprecated 2.3.0 Replaced with Multi Source Inventory -+ * @link https://devdocs.magento.com/guides/v2.3/inventory/index.html -+ * @link https://devdocs.magento.com/guides/v2.3/inventory/catalog-inventory-replacements.html - */ - interface StockConfigurationInterface - { -diff --git a/app/code/Magento/CatalogInventory/Api/StockCriteriaInterface.php b/app/code/Magento/CatalogInventory/Api/StockCriteriaInterface.php -index 969af6481cb..eb6fb2e812f 100644 ---- a/app/code/Magento/CatalogInventory/Api/StockCriteriaInterface.php -+++ b/app/code/Magento/CatalogInventory/Api/StockCriteriaInterface.php -@@ -9,6 +9,10 @@ namespace Magento\CatalogInventory\Api; - * Interface StockCriteriaInterface - * @api - * @since 100.0.2 -+ * -+ * @deprecated 2.3.0 Replaced with Multi Source Inventory -+ * @link https://devdocs.magento.com/guides/v2.3/inventory/index.html -+ * @link https://devdocs.magento.com/guides/v2.3/inventory/catalog-inventory-replacements.html - */ - interface StockCriteriaInterface extends \Magento\Framework\Api\CriteriaInterface - { -diff --git a/app/code/Magento/CatalogInventory/Api/StockIndexInterface.php b/app/code/Magento/CatalogInventory/Api/StockIndexInterface.php -index 1521b34c715..18bab6571c2 100644 ---- a/app/code/Magento/CatalogInventory/Api/StockIndexInterface.php -+++ b/app/code/Magento/CatalogInventory/Api/StockIndexInterface.php -@@ -9,6 +9,10 @@ namespace Magento\CatalogInventory\Api; - * Interface StockIndexInterface - * @api - * @since 100.0.2 -+ * -+ * @deprecated 2.3.0 Replaced with Multi Source Inventory -+ * @link https://devdocs.magento.com/guides/v2.3/inventory/index.html -+ * @link https://devdocs.magento.com/guides/v2.3/inventory/catalog-inventory-replacements.html - */ - interface StockIndexInterface - { -diff --git a/app/code/Magento/CatalogInventory/Api/StockItemCriteriaInterface.php b/app/code/Magento/CatalogInventory/Api/StockItemCriteriaInterface.php -index a2fc7801b1d..1d2cabbb48a 100644 ---- a/app/code/Magento/CatalogInventory/Api/StockItemCriteriaInterface.php -+++ b/app/code/Magento/CatalogInventory/Api/StockItemCriteriaInterface.php -@@ -9,6 +9,10 @@ namespace Magento\CatalogInventory\Api; - * Interface StockItemCriteriaInterface - * @api - * @since 100.0.2 -+ * -+ * @deprecated 2.3.0 Replaced with Multi Source Inventory -+ * @link https://devdocs.magento.com/guides/v2.3/inventory/index.html -+ * @link https://devdocs.magento.com/guides/v2.3/inventory/catalog-inventory-replacements.html - */ - interface StockItemCriteriaInterface extends \Magento\Framework\Api\CriteriaInterface - { -diff --git a/app/code/Magento/CatalogInventory/Api/StockItemRepositoryInterface.php b/app/code/Magento/CatalogInventory/Api/StockItemRepositoryInterface.php -index 2732048d144..eecf6cbe076 100644 ---- a/app/code/Magento/CatalogInventory/Api/StockItemRepositoryInterface.php -+++ b/app/code/Magento/CatalogInventory/Api/StockItemRepositoryInterface.php -@@ -9,6 +9,10 @@ namespace Magento\CatalogInventory\Api; - * Interface StockItemRepository - * @api - * @since 100.0.2 -+ * -+ * @deprecated 2.3.0 Replaced with Multi Source Inventory -+ * @link https://devdocs.magento.com/guides/v2.3/inventory/index.html -+ * @link https://devdocs.magento.com/guides/v2.3/inventory/catalog-inventory-replacements.html - */ - interface StockItemRepositoryInterface - { -diff --git a/app/code/Magento/CatalogInventory/Api/StockManagementInterface.php b/app/code/Magento/CatalogInventory/Api/StockManagementInterface.php -index ddb84abf3d1..8796953e32f 100644 ---- a/app/code/Magento/CatalogInventory/Api/StockManagementInterface.php -+++ b/app/code/Magento/CatalogInventory/Api/StockManagementInterface.php -@@ -9,6 +9,10 @@ namespace Magento\CatalogInventory\Api; - * Interface StockManagementInterface - * @api - * @since 100.0.2 -+ * -+ * @deprecated 2.3.0 Replaced with Multi Source Inventory -+ * @link https://devdocs.magento.com/guides/v2.3/inventory/index.html -+ * @link https://devdocs.magento.com/guides/v2.3/inventory/catalog-inventory-replacements.html - */ - interface StockManagementInterface - { -diff --git a/app/code/Magento/CatalogInventory/Api/StockRegistryInterface.php b/app/code/Magento/CatalogInventory/Api/StockRegistryInterface.php -index 2d011e24f41..5478f90fb7d 100644 ---- a/app/code/Magento/CatalogInventory/Api/StockRegistryInterface.php -+++ b/app/code/Magento/CatalogInventory/Api/StockRegistryInterface.php -@@ -9,6 +9,10 @@ namespace Magento\CatalogInventory\Api; - * Interface StockRegistryInterface - * @api - * @since 100.0.2 -+ * -+ * @deprecated 2.3.0 Replaced with Multi Source Inventory -+ * @link https://devdocs.magento.com/guides/v2.3/inventory/index.html -+ * @link https://devdocs.magento.com/guides/v2.3/inventory/catalog-inventory-replacements.html - */ - interface StockRegistryInterface - { -diff --git a/app/code/Magento/CatalogInventory/Api/StockRepositoryInterface.php b/app/code/Magento/CatalogInventory/Api/StockRepositoryInterface.php -index 80a7e79289c..3cfdf455063 100644 ---- a/app/code/Magento/CatalogInventory/Api/StockRepositoryInterface.php -+++ b/app/code/Magento/CatalogInventory/Api/StockRepositoryInterface.php -@@ -9,6 +9,10 @@ namespace Magento\CatalogInventory\Api; - * Interface StockRepositoryInterface - * @api - * @since 100.0.2 -+ * -+ * @deprecated 2.3.0 Replaced with Multi Source Inventory -+ * @link https://devdocs.magento.com/guides/v2.3/inventory/index.html -+ * @link https://devdocs.magento.com/guides/v2.3/inventory/catalog-inventory-replacements.html - */ - interface StockRepositoryInterface - { -diff --git a/app/code/Magento/CatalogInventory/Api/StockStateInterface.php b/app/code/Magento/CatalogInventory/Api/StockStateInterface.php -index 8a1f7da5158..8be7f5be79f 100644 ---- a/app/code/Magento/CatalogInventory/Api/StockStateInterface.php -+++ b/app/code/Magento/CatalogInventory/Api/StockStateInterface.php -@@ -9,6 +9,10 @@ namespace Magento\CatalogInventory\Api; - * Interface StockStateInterface - * @api - * @since 100.0.2 -+ * -+ * @deprecated 2.3.0 Replaced with Multi Source Inventory -+ * @link https://devdocs.magento.com/guides/v2.3/inventory/index.html -+ * @link https://devdocs.magento.com/guides/v2.3/inventory/catalog-inventory-replacements.html - */ - interface StockStateInterface - { -diff --git a/app/code/Magento/CatalogInventory/Api/StockStatusCriteriaInterface.php b/app/code/Magento/CatalogInventory/Api/StockStatusCriteriaInterface.php -index e504e6355a1..99ad7005d9d 100644 ---- a/app/code/Magento/CatalogInventory/Api/StockStatusCriteriaInterface.php -+++ b/app/code/Magento/CatalogInventory/Api/StockStatusCriteriaInterface.php -@@ -9,6 +9,10 @@ namespace Magento\CatalogInventory\Api; - * Interface StockStatusCriteriaInterface - * @api - * @since 100.0.2 -+ * -+ * @deprecated 2.3.0 Replaced with Multi Source Inventory -+ * @link https://devdocs.magento.com/guides/v2.3/inventory/index.html -+ * @link https://devdocs.magento.com/guides/v2.3/inventory/catalog-inventory-replacements.html - */ - interface StockStatusCriteriaInterface extends \Magento\Framework\Api\CriteriaInterface - { -diff --git a/app/code/Magento/CatalogInventory/Api/StockStatusRepositoryInterface.php b/app/code/Magento/CatalogInventory/Api/StockStatusRepositoryInterface.php -index 94d4998c1e3..d29171f557f 100644 ---- a/app/code/Magento/CatalogInventory/Api/StockStatusRepositoryInterface.php -+++ b/app/code/Magento/CatalogInventory/Api/StockStatusRepositoryInterface.php -@@ -9,6 +9,10 @@ namespace Magento\CatalogInventory\Api; - * Interface StockStatusRepositoryInterface - * @api - * @since 100.0.2 -+ * -+ * @deprecated 2.3.0 Replaced with Multi Source Inventory -+ * @link https://devdocs.magento.com/guides/v2.3/inventory/index.html -+ * @link https://devdocs.magento.com/guides/v2.3/inventory/catalog-inventory-replacements.html - */ - interface StockStatusRepositoryInterface - { -diff --git a/app/code/Magento/CatalogInventory/Block/Adminhtml/Form/Field/Customergroup.php b/app/code/Magento/CatalogInventory/Block/Adminhtml/Form/Field/Customergroup.php -index dc992378d12..f349e94235a 100644 ---- a/app/code/Magento/CatalogInventory/Block/Adminhtml/Form/Field/Customergroup.php -+++ b/app/code/Magento/CatalogInventory/Block/Adminhtml/Form/Field/Customergroup.php -@@ -84,7 +84,7 @@ class Customergroup extends \Magento\Framework\View\Element\Html\Select - $this->_customerGroups[$notLoggedInGroup->getId()] = $notLoggedInGroup->getCode(); - } - if ($groupId !== null) { -- return isset($this->_customerGroups[$groupId]) ? $this->_customerGroups[$groupId] : null; -+ return $this->_customerGroups[$groupId] ?? null; - } - return $this->_customerGroups; - } -diff --git a/app/code/Magento/CatalogInventory/Block/Adminhtml/Form/Field/Minsaleqty.php b/app/code/Magento/CatalogInventory/Block/Adminhtml/Form/Field/Minsaleqty.php -index 15dfbd122e9..d3c165bbde1 100644 ---- a/app/code/Magento/CatalogInventory/Block/Adminhtml/Form/Field/Minsaleqty.php -+++ b/app/code/Magento/CatalogInventory/Block/Adminhtml/Form/Field/Minsaleqty.php -@@ -10,6 +10,10 @@ namespace Magento\CatalogInventory\Block\Adminhtml\Form\Field; - * - * @api - * @since 100.0.2 -+ * -+ * @deprecated 2.3.0 Replaced with Multi Source Inventory -+ * @link https://devdocs.magento.com/guides/v2.3/inventory/index.html -+ * @link https://devdocs.magento.com/guides/v2.3/inventory/catalog-inventory-replacements.html - */ - class Minsaleqty extends \Magento\Config\Block\System\Config\Form\Field\FieldArray\AbstractFieldArray - { -diff --git a/app/code/Magento/CatalogInventory/Block/Adminhtml/Form/Field/Stock.php b/app/code/Magento/CatalogInventory/Block/Adminhtml/Form/Field/Stock.php -index 2c41e179892..5378801b6c2 100644 ---- a/app/code/Magento/CatalogInventory/Block/Adminhtml/Form/Field/Stock.php -+++ b/app/code/Magento/CatalogInventory/Block/Adminhtml/Form/Field/Stock.php -@@ -14,6 +14,10 @@ use Magento\Framework\Data\Form; - /** - * @api - * @since 100.0.2 -+ * -+ * @deprecated 2.3.0 Replaced with Multi Source Inventory -+ * @link https://devdocs.magento.com/guides/v2.3/inventory/index.html -+ * @link https://devdocs.magento.com/guides/v2.3/inventory/catalog-inventory-replacements.html - */ - class Stock extends \Magento\Framework\Data\Form\Element\Select - { -diff --git a/app/code/Magento/CatalogInventory/Block/Qtyincrements.php b/app/code/Magento/CatalogInventory/Block/Qtyincrements.php -index adaa762f327..a12b72cd0a9 100644 ---- a/app/code/Magento/CatalogInventory/Block/Qtyincrements.php -+++ b/app/code/Magento/CatalogInventory/Block/Qtyincrements.php -@@ -14,6 +14,10 @@ use Magento\Framework\View\Element\Template; - * - * @api - * @since 100.0.2 -+ * -+ * @deprecated 2.3.0 Replaced with Multi Source Inventory -+ * @link https://devdocs.magento.com/guides/v2.3/inventory/index.html -+ * @link https://devdocs.magento.com/guides/v2.3/inventory/catalog-inventory-replacements.html - */ - class Qtyincrements extends Template implements IdentityInterface - { -diff --git a/app/code/Magento/CatalogInventory/Block/Stockqty/AbstractStockqty.php b/app/code/Magento/CatalogInventory/Block/Stockqty/AbstractStockqty.php -index 568fa600ec5..4c8f356519e 100644 ---- a/app/code/Magento/CatalogInventory/Block/Stockqty/AbstractStockqty.php -+++ b/app/code/Magento/CatalogInventory/Block/Stockqty/AbstractStockqty.php -@@ -131,7 +131,9 @@ abstract class AbstractStockqty extends \Magento\Framework\View\Element\Template - */ - public function isMsgVisible() - { -- return $this->getStockQty() > 0 && $this->getStockQtyLeft() <= $this->getThresholdQty(); -+ return $this->getStockQty() > 0 -+ && $this->getStockQtyLeft() > 0 -+ && $this->getStockQtyLeft() <= $this->getThresholdQty(); - } - - /** -diff --git a/app/code/Magento/CatalogInventory/Block/Stockqty/DefaultStockqty.php b/app/code/Magento/CatalogInventory/Block/Stockqty/DefaultStockqty.php -index c315338be0d..5a3a3ca6ee9 100644 ---- a/app/code/Magento/CatalogInventory/Block/Stockqty/DefaultStockqty.php -+++ b/app/code/Magento/CatalogInventory/Block/Stockqty/DefaultStockqty.php -@@ -11,6 +11,10 @@ namespace Magento\CatalogInventory\Block\Stockqty; - * - * @api - * @since 100.0.2 -+ * -+ * @deprecated 2.3.0 Replaced with Multi Source Inventory -+ * @link https://devdocs.magento.com/guides/v2.3/inventory/index.html -+ * @link https://devdocs.magento.com/guides/v2.3/inventory/catalog-inventory-replacements.html - */ - class DefaultStockqty extends AbstractStockqty implements \Magento\Framework\DataObject\IdentityInterface - { -diff --git a/app/code/Magento/CatalogInventory/Helper/Minsaleqty.php b/app/code/Magento/CatalogInventory/Helper/Minsaleqty.php -index b4f5d8b670f..96bf5bd9653 100644 ---- a/app/code/Magento/CatalogInventory/Helper/Minsaleqty.php -+++ b/app/code/Magento/CatalogInventory/Helper/Minsaleqty.php -@@ -95,7 +95,7 @@ class Minsaleqty - } - return $this->serializer->serialize($data); - } else { -- return ''; -+ return $value; - } - } - -diff --git a/app/code/Magento/CatalogInventory/Helper/Stock.php b/app/code/Magento/CatalogInventory/Helper/Stock.php -index 494d440eeed..798ac4074c1 100644 ---- a/app/code/Magento/CatalogInventory/Helper/Stock.php -+++ b/app/code/Magento/CatalogInventory/Helper/Stock.php -@@ -18,6 +18,10 @@ use Magento\Store\Model\StoreManagerInterface; - * Class Stock - * @SuppressWarnings(PHPMD.CouplingBetweenObjects) - * @api -+ * -+ * @deprecated 2.3.0 Replaced with Multi Source Inventory -+ * @link https://devdocs.magento.com/guides/v2.3/inventory/index.html -+ * @link https://devdocs.magento.com/guides/v2.3/inventory/catalog-inventory-replacements.html - */ - class Stock - { -diff --git a/app/code/Magento/CatalogInventory/Model/AddStockStatusToCollection.php b/app/code/Magento/CatalogInventory/Model/AddStockStatusToCollection.php -index 2c52b49c1a0..0a02d4eb6a9 100644 ---- a/app/code/Magento/CatalogInventory/Model/AddStockStatusToCollection.php -+++ b/app/code/Magento/CatalogInventory/Model/AddStockStatusToCollection.php -@@ -7,6 +7,8 @@ - namespace Magento\CatalogInventory\Model; - - use Magento\Catalog\Model\ResourceModel\Product\Collection; -+use Magento\Framework\Search\EngineResolverInterface; -+use Magento\Search\Model\EngineResolver; - - /** - * Catalog inventory module plugin -@@ -17,18 +19,27 @@ class AddStockStatusToCollection - * @var \Magento\CatalogInventory\Helper\Stock - */ - protected $stockHelper; -- -+ -+ /** -+ * @var EngineResolverInterface -+ */ -+ private $engineResolver; -+ - /** -- * @param \Magento\CatalogInventory\Model\Configuration $configuration - * @param \Magento\CatalogInventory\Helper\Stock $stockHelper -+ * @param EngineResolverInterface $engineResolver - */ - public function __construct( -- \Magento\CatalogInventory\Helper\Stock $stockHelper -+ \Magento\CatalogInventory\Helper\Stock $stockHelper, -+ EngineResolverInterface $engineResolver - ) { - $this->stockHelper = $stockHelper; -+ $this->engineResolver = $engineResolver; - } - - /** -+ * Add stock filter to collection. -+ * - * @param Collection $productCollection - * @param bool $printQuery - * @param bool $logQuery -@@ -36,7 +47,9 @@ class AddStockStatusToCollection - */ - public function beforeLoad(Collection $productCollection, $printQuery = false, $logQuery = false) - { -- $this->stockHelper->addIsInStockFilterToCollection($productCollection); -+ if ($this->engineResolver->getCurrentSearchEngine() === EngineResolver::CATALOG_SEARCH_MYSQL_ENGINE) { -+ $this->stockHelper->addIsInStockFilterToCollection($productCollection); -+ } - return [$printQuery, $logQuery]; - } - } -diff --git a/app/code/Magento/CatalogInventory/Model/Adminhtml/Stock/Item.php b/app/code/Magento/CatalogInventory/Model/Adminhtml/Stock/Item.php -index 36721db8748..145b0d1454a 100644 ---- a/app/code/Magento/CatalogInventory/Model/Adminhtml/Stock/Item.php -+++ b/app/code/Magento/CatalogInventory/Model/Adminhtml/Stock/Item.php -@@ -20,6 +20,10 @@ use Magento\Catalog\Model\Product; - * @SuppressWarnings(PHPMD.CouplingBetweenObjects) - * @api - * @since 100.0.2 -+ * -+ * @deprecated 2.3.0 Replaced with Multi Source Inventory -+ * @link https://devdocs.magento.com/guides/v2.3/inventory/index.html -+ * @link https://devdocs.magento.com/guides/v2.3/inventory/catalog-inventory-replacements.html - */ - class Item extends \Magento\CatalogInventory\Model\Stock\Item implements IdentityInterface - { -diff --git a/app/code/Magento/CatalogInventory/Model/Configuration.php b/app/code/Magento/CatalogInventory/Model/Configuration.php -index 2f0415b40dc..8b0849c8874 100644 ---- a/app/code/Magento/CatalogInventory/Model/Configuration.php -+++ b/app/code/Magento/CatalogInventory/Model/Configuration.php -@@ -9,6 +9,7 @@ use Magento\CatalogInventory\Api\StockConfigurationInterface; - use Magento\CatalogInventory\Helper\Minsaleqty as MinsaleqtyHelper; - use Magento\Framework\App\Config\ScopeConfigInterface; - use Magento\Catalog\Model\ProductTypes\ConfigInterface; -+use Magento\Store\Model\ScopeInterface; - use Magento\Store\Model\StoreManagerInterface; - - /** -@@ -131,6 +132,8 @@ class Configuration implements StockConfigurationInterface - protected $storeManager; - - /** -+ * Configuration constructor. -+ * - * @param ConfigInterface $config - * @param ScopeConfigInterface $scopeConfig - * @param MinsaleqtyHelper $minsaleqtyHelper -@@ -149,7 +152,7 @@ class Configuration implements StockConfigurationInterface - } - - /** -- * {@inheritdoc} -+ * @inheritdoc - */ - public function getDefaultScopeId() - { -@@ -159,6 +162,8 @@ class Configuration implements StockConfigurationInterface - } - - /** -+ * Is Qty Type Ids -+ * - * @param int|null $filter - * @return array - */ -@@ -182,6 +187,8 @@ class Configuration implements StockConfigurationInterface - } - - /** -+ * Is Qty -+ * - * @param int $productTypeId - * @return bool - */ -@@ -201,12 +208,14 @@ class Configuration implements StockConfigurationInterface - { - return $this->scopeConfig->isSetFlag( - self::XML_PATH_CAN_SUBTRACT, -- \Magento\Store\Model\ScopeInterface::SCOPE_STORE, -+ ScopeInterface::SCOPE_STORE, - $store - ); - } - - /** -+ * Get Min Qty -+ * - * @param null|string|bool|int|\Magento\Store\Model\Store $store - * @return float - */ -@@ -214,12 +223,14 @@ class Configuration implements StockConfigurationInterface - { - return (float)$this->scopeConfig->getValue( - self::XML_PATH_MIN_QTY, -- \Magento\Store\Model\ScopeInterface::SCOPE_STORE, -+ ScopeInterface::SCOPE_STORE, - $store - ); - } - - /** -+ * Get Min Sale Qty -+ * - * @param null|string|bool|int|\Magento\Store\Model\Store $store - * @param int $customerGroupId - * @return float -@@ -230,6 +241,8 @@ class Configuration implements StockConfigurationInterface - } - - /** -+ * Get Max Sale Qty -+ * - * @param null|string|bool|int|\Magento\Store\Model\Store $store - * @return float|null - */ -@@ -237,12 +250,14 @@ class Configuration implements StockConfigurationInterface - { - return (float)$this->scopeConfig->getValue( - self::XML_PATH_MAX_SALE_QTY, -- \Magento\Store\Model\ScopeInterface::SCOPE_STORE, -+ ScopeInterface::SCOPE_STORE, - $store - ); - } - - /** -+ * Get Notify Stock Qty -+ * - * @param null|string|bool|int|\Magento\Store\Model\Store $store - * @return float - */ -@@ -250,7 +265,7 @@ class Configuration implements StockConfigurationInterface - { - return (float) $this->scopeConfig->getValue( - self::XML_PATH_NOTIFY_STOCK_QTY, -- \Magento\Store\Model\ScopeInterface::SCOPE_STORE, -+ ScopeInterface::SCOPE_STORE, - $store - ); - } -@@ -264,14 +279,16 @@ class Configuration implements StockConfigurationInterface - */ - public function getEnableQtyIncrements($store = null) - { -- return (bool) $this->scopeConfig->getValue( -+ return $this->scopeConfig->isSetFlag( - self::XML_PATH_ENABLE_QTY_INCREMENTS, -- \Magento\Store\Model\ScopeInterface::SCOPE_STORE, -+ ScopeInterface::SCOPE_STORE, - $store - ); - } - - /** -+ * Get Qty Increments -+ * - * @param null|string|bool|int|\Magento\Store\Model\Store $store - * @return float - */ -@@ -279,7 +296,7 @@ class Configuration implements StockConfigurationInterface - { - return (float)$this->scopeConfig->getValue( - self::XML_PATH_QTY_INCREMENTS, -- \Magento\Store\Model\ScopeInterface::SCOPE_STORE, -+ ScopeInterface::SCOPE_STORE, - $store - ); - } -@@ -294,7 +311,7 @@ class Configuration implements StockConfigurationInterface - { - return (int) $this->scopeConfig->getValue( - self::XML_PATH_BACKORDERS, -- \Magento\Store\Model\ScopeInterface::SCOPE_STORE, -+ ScopeInterface::SCOPE_STORE, - $store - ); - } -@@ -309,7 +326,7 @@ class Configuration implements StockConfigurationInterface - { - return (int) $this->scopeConfig->isSetFlag( - self::XML_PATH_MANAGE_STOCK, -- \Magento\Store\Model\ScopeInterface::SCOPE_STORE, -+ ScopeInterface::SCOPE_STORE, - $store - ); - } -@@ -325,7 +342,7 @@ class Configuration implements StockConfigurationInterface - { - return $this->scopeConfig->isSetFlag( - self::XML_PATH_CAN_BACK_IN_STOCK, -- \Magento\Store\Model\ScopeInterface::SCOPE_STORE, -+ ScopeInterface::SCOPE_STORE, - $store - ); - } -@@ -340,7 +357,7 @@ class Configuration implements StockConfigurationInterface - { - return $this->scopeConfig->isSetFlag( - self::XML_PATH_SHOW_OUT_OF_STOCK, -- \Magento\Store\Model\ScopeInterface::SCOPE_STORE, -+ ScopeInterface::SCOPE_STORE, - $store - ); - } -@@ -355,14 +372,13 @@ class Configuration implements StockConfigurationInterface - { - return $this->scopeConfig->isSetFlag( - self::XML_PATH_ITEM_AUTO_RETURN, -- \Magento\Store\Model\ScopeInterface::SCOPE_STORE, -+ ScopeInterface::SCOPE_STORE, - $store - ); - } - - /** -- * Get 'Display product stock status' option value -- * Shows if it is necessary to show product stock status ('in stock'/'out of stock') -+ * Display product stock status. Shows if it is necessary to show product stock status in stock/out of stock. - * - * @param null|string|bool|int|\Magento\Store\Model\Store $store - * @return bool -@@ -371,12 +387,14 @@ class Configuration implements StockConfigurationInterface - { - return $this->scopeConfig->isSetFlag( - self::XML_PATH_DISPLAY_PRODUCT_STOCK_STATUS, -- \Magento\Store\Model\ScopeInterface::SCOPE_STORE, -+ ScopeInterface::SCOPE_STORE, - $store - ); - } - - /** -+ * Get Default Config Value -+ * - * @param string $field - * @param null|string|bool|int|\Magento\Store\Model\Store $store - * @return string|null -@@ -385,12 +403,14 @@ class Configuration implements StockConfigurationInterface - { - return $this->scopeConfig->getValue( - self::XML_PATH_ITEM . $field, -- \Magento\Store\Model\ScopeInterface::SCOPE_STORE, -+ ScopeInterface::SCOPE_STORE, - $store - ); - } - - /** -+ * Get Stock Threshold Qty -+ * - * @param null|string|bool|int|\Magento\Store\Model\Store $store - * @return string|null - */ -@@ -398,7 +418,7 @@ class Configuration implements StockConfigurationInterface - { - return $this->scopeConfig->getValue( - self::XML_PATH_STOCK_THRESHOLD_QTY, -- \Magento\Store\Model\ScopeInterface::SCOPE_STORE, -+ ScopeInterface::SCOPE_STORE, - $store - ); - } -diff --git a/app/code/Magento/CatalogInventory/Model/Indexer/ProductPriceIndexFilter.php b/app/code/Magento/CatalogInventory/Model/Indexer/ProductPriceIndexFilter.php -index 5e7210c1b74..f9a49d4f8d1 100644 ---- a/app/code/Magento/CatalogInventory/Model/Indexer/ProductPriceIndexFilter.php -+++ b/app/code/Magento/CatalogInventory/Model/Indexer/ProductPriceIndexFilter.php -@@ -9,11 +9,11 @@ namespace Magento\CatalogInventory\Model\Indexer; - - use Magento\CatalogInventory\Api\StockConfigurationInterface; - use Magento\CatalogInventory\Model\ResourceModel\Stock\Item; --use Magento\CatalogInventory\Model\Stock; - use Magento\Catalog\Model\ResourceModel\Product\Indexer\Price\PriceModifierInterface; - use Magento\Catalog\Model\ResourceModel\Product\Indexer\Price\IndexTableStructure; - use Magento\Framework\App\ResourceConnection; - use Magento\Framework\App\ObjectManager; -+use Magento\Framework\DB\Query\Generator; - - /** - * Class for filter product price index. -@@ -40,22 +40,38 @@ class ProductPriceIndexFilter implements PriceModifierInterface - */ - private $connectionName; - -+ /** -+ * @var Generator -+ */ -+ private $batchQueryGenerator; -+ -+ /** -+ * @var int -+ */ -+ private $batchSize; -+ - /** - * @param StockConfigurationInterface $stockConfiguration - * @param Item $stockItem - * @param ResourceConnection $resourceConnection - * @param string $connectionName -+ * @param Generator $batchQueryGenerator -+ * @param int $batchSize - */ - public function __construct( - StockConfigurationInterface $stockConfiguration, - Item $stockItem, - ResourceConnection $resourceConnection = null, -- $connectionName = 'indexer' -+ $connectionName = 'indexer', -+ Generator $batchQueryGenerator = null, -+ $batchSize = 100 - ) { - $this->stockConfiguration = $stockConfiguration; - $this->stockItem = $stockItem; - $this->resourceConnection = $resourceConnection ?: ObjectManager::getInstance()->get(ResourceConnection::class); - $this->connectionName = $connectionName; -+ $this->batchQueryGenerator = $batchQueryGenerator ?: ObjectManager::getInstance()->get(Generator::class); -+ $this->batchSize = $batchSize; - } - - /** -@@ -76,32 +92,37 @@ class ProductPriceIndexFilter implements PriceModifierInterface - - $connection = $this->resourceConnection->getConnection($this->connectionName); - $select = $connection->select(); -+ - $select->from( -- ['price_index' => $priceTable->getTableName()], -- [] -- ); -- $select->joinInner( - ['stock_item' => $this->stockItem->getMainTable()], -- 'stock_item.product_id = price_index.' . $priceTable->getEntityField() -- . ' AND stock_item.stock_id = ' . Stock::DEFAULT_STOCK_ID, -- [] -+ ['stock_item.product_id', 'MAX(stock_item.is_in_stock) as max_is_in_stock'] - ); -+ - if ($this->stockConfiguration->getManageStock()) { -- $stockStatus = $connection->getCheckSql( -- 'use_config_manage_stock = 0 AND manage_stock = 0', -- Stock::STOCK_IN_STOCK, -- 'is_in_stock' -- ); -+ $select->where('stock_item.use_config_manage_stock = 1 OR stock_item.manage_stock = 1'); - } else { -- $stockStatus = $connection->getCheckSql( -- 'use_config_manage_stock = 0 AND manage_stock = 1', -- 'is_in_stock', -- Stock::STOCK_IN_STOCK -- ); -+ $select->where('stock_item.use_config_manage_stock = 0 AND stock_item.manage_stock = 1'); - } -- $select->where($stockStatus . ' = ?', Stock::STOCK_OUT_OF_STOCK); - -- $query = $select->deleteFromSelect('price_index'); -- $connection->query($query); -+ $select->group('stock_item.product_id'); -+ $select->having('max_is_in_stock = 0'); -+ -+ $batchSelectIterator = $this->batchQueryGenerator->generate( -+ 'product_id', -+ $select, -+ $this->batchSize, -+ \Magento\Framework\DB\Query\BatchIteratorInterface::UNIQUE_FIELD_ITERATOR -+ ); -+ -+ foreach ($batchSelectIterator as $select) { -+ $productIds = null; -+ foreach ($connection->query($select)->fetchAll() as $row) { -+ $productIds[] = $row['product_id']; -+ } -+ if ($productIds !== null) { -+ $where = [$priceTable->getEntityField() .' IN (?)' => $productIds]; -+ $connection->delete($priceTable->getTableName(), $where); -+ } -+ } - } - } -diff --git a/app/code/Magento/CatalogInventory/Model/Indexer/Stock/Action/Full.php b/app/code/Magento/CatalogInventory/Model/Indexer/Stock/Action/Full.php -index bc10d38173b..43a5aabee97 100644 ---- a/app/code/Magento/CatalogInventory/Model/Indexer/Stock/Action/Full.php -+++ b/app/code/Magento/CatalogInventory/Model/Indexer/Stock/Action/Full.php -@@ -6,12 +6,19 @@ - * See COPYING.txt for license details. - */ - -+declare(strict_types=1); -+ - namespace Magento\CatalogInventory\Model\Indexer\Stock\Action; - -+use Magento\Catalog\Api\Data\ProductInterface; - use Magento\Catalog\Model\ResourceModel\Indexer\ActiveTableSwitcher; -+use Magento\CatalogInventory\Model\Indexer\Stock\BatchSizeManagement; -+use Magento\CatalogInventory\Model\ResourceModel\Indexer\Stock\DefaultStock; - use Magento\Framework\App\ResourceConnection; - use Magento\CatalogInventory\Model\ResourceModel\Indexer\StockFactory; - use Magento\Catalog\Model\Product\Type as ProductType; -+use Magento\Framework\DB\Query\BatchIteratorInterface; -+use Magento\Framework\DB\Query\Generator as QueryGenerator; - use Magento\Framework\Indexer\CacheContext; - use Magento\Framework\Event\ManagerInterface as EventManager; - use Magento\Framework\EntityManager\MetadataPool; -@@ -25,7 +32,6 @@ use Magento\CatalogInventory\Model\ResourceModel\Indexer\Stock\StockInterface; - /** - * Class Full reindex action - * -- * @package Magento\CatalogInventory\Model\Indexer\Stock\Action - * @SuppressWarnings(PHPMD.CouplingBetweenObjects) - */ - class Full extends AbstractAction -@@ -60,6 +66,11 @@ class Full extends AbstractAction - */ - private $activeTableSwitcher; - -+ /** -+ * @var QueryGenerator|null -+ */ -+ private $batchQueryGenerator; -+ - /** - * @param ResourceConnection $resource - * @param StockFactory $indexerFactory -@@ -71,7 +82,7 @@ class Full extends AbstractAction - * @param BatchProviderInterface|null $batchProvider - * @param array $batchRowsCount - * @param ActiveTableSwitcher|null $activeTableSwitcher -- * -+ * @param QueryGenerator|null $batchQueryGenerator - * @SuppressWarnings(PHPMD.ExcessiveParameterList) - */ - public function __construct( -@@ -84,7 +95,8 @@ class Full extends AbstractAction - BatchSizeManagementInterface $batchSizeManagement = null, - BatchProviderInterface $batchProvider = null, - array $batchRowsCount = [], -- ActiveTableSwitcher $activeTableSwitcher = null -+ ActiveTableSwitcher $activeTableSwitcher = null, -+ QueryGenerator $batchQueryGenerator = null - ) { - parent::__construct( - $resource, -@@ -97,11 +109,12 @@ class Full extends AbstractAction - $this->metadataPool = $metadataPool ?: ObjectManager::getInstance()->get(MetadataPool::class); - $this->batchProvider = $batchProvider ?: ObjectManager::getInstance()->get(BatchProviderInterface::class); - $this->batchSizeManagement = $batchSizeManagement ?: ObjectManager::getInstance()->get( -- \Magento\CatalogInventory\Model\Indexer\Stock\BatchSizeManagement::class -+ BatchSizeManagement::class - ); - $this->batchRowsCount = $batchRowsCount; - $this->activeTableSwitcher = $activeTableSwitcher ?: ObjectManager::getInstance() - ->get(ActiveTableSwitcher::class); -+ $this->batchQueryGenerator = $batchQueryGenerator ?: ObjectManager::getInstance()->get(QueryGenerator::class); - } - - /** -@@ -109,22 +122,20 @@ class Full extends AbstractAction - * - * @param null|array $ids - * @throws LocalizedException -- * - * @return void -- * - * @SuppressWarnings(PHPMD.UnusedFormalParameter) - */ -- public function execute($ids = null) -+ public function execute($ids = null): void - { - try { - $this->useIdxTable(false); - $this->cleanIndexersTables($this->_getTypeIndexers()); - -- $entityMetadata = $this->metadataPool->getMetadata(\Magento\Catalog\Api\Data\ProductInterface::class); -+ $entityMetadata = $this->metadataPool->getMetadata(ProductInterface::class); - - $columns = array_keys($this->_getConnection()->describeTable($this->_getIdxTable())); - -- /** @var \Magento\CatalogInventory\Model\ResourceModel\Indexer\Stock\DefaultStock $indexer */ -+ /** @var DefaultStock $indexer */ - foreach ($this->_getTypeIndexers() as $indexer) { - $indexer->setActionType(self::ACTION_TYPE); - $connection = $indexer->getConnection(); -@@ -135,22 +146,21 @@ class Full extends AbstractAction - : $this->batchRowsCount['default']; - - $this->batchSizeManagement->ensureBatchSize($connection, $batchRowCount); -- $batches = $this->batchProvider->getBatches( -- $connection, -- $entityMetadata->getEntityTable(), -+ -+ $select = $connection->select(); -+ $select->distinct(true); -+ $select->from(['e' => $entityMetadata->getEntityTable()], $entityMetadata->getIdentifierField()); -+ -+ $batchQueries = $this->batchQueryGenerator->generate( - $entityMetadata->getIdentifierField(), -- $batchRowCount -+ $select, -+ $batchRowCount, -+ BatchIteratorInterface::NON_UNIQUE_FIELD_ITERATOR - ); - -- foreach ($batches as $batch) { -+ foreach ($batchQueries as $query) { - $this->clearTemporaryIndexTable(); -- // Get entity ids from batch -- $select = $connection->select(); -- $select->distinct(true); -- $select->from(['e' => $entityMetadata->getEntityTable()], $entityMetadata->getIdentifierField()); -- $select->where('type_id = ?', $indexer->getTypeId()); -- -- $entityIds = $this->batchProvider->getBatchIds($connection, $select, $batch); -+ $entityIds = $connection->fetchCol($query); - if (!empty($entityIds)) { - $indexer->reindexEntity($entityIds); - $select = $connection->select()->from($this->_getIdxTable(), $columns); -@@ -167,12 +177,13 @@ class Full extends AbstractAction - - /** - * Delete all records from index table -+ * - * Used to clean table before re-indexation - * - * @param array $indexers - * @return void - */ -- private function cleanIndexersTables(array $indexers) -+ private function cleanIndexersTables(array $indexers): void - { - $tables = array_map( - function (StockInterface $indexer) { -diff --git a/app/code/Magento/CatalogInventory/Model/Indexer/Stock/CacheCleaner.php b/app/code/Magento/CatalogInventory/Model/Indexer/Stock/CacheCleaner.php -index a32faa4640a..b3fa07479a7 100644 ---- a/app/code/Magento/CatalogInventory/Model/Indexer/Stock/CacheCleaner.php -+++ b/app/code/Magento/CatalogInventory/Model/Indexer/Stock/CacheCleaner.php -@@ -10,8 +10,10 @@ namespace Magento\CatalogInventory\Model\Indexer\Stock; - - use Magento\CatalogInventory\Api\StockConfigurationInterface; - use Magento\Framework\App\ResourceConnection; -+use Magento\Framework\App\ObjectManager; - use Magento\Framework\DB\Adapter\AdapterInterface; - use Magento\Framework\Event\ManagerInterface; -+use Magento\Framework\EntityManager\MetadataPool; - use Magento\Framework\Indexer\CacheContext; - use Magento\CatalogInventory\Model\Stock; - use Magento\Catalog\Model\Product; -@@ -46,25 +48,35 @@ class CacheCleaner - */ - private $connection; - -+ /** -+ * @var MetadataPool -+ */ -+ private $metadataPool; -+ - /** - * @param ResourceConnection $resource - * @param StockConfigurationInterface $stockConfiguration - * @param CacheContext $cacheContext - * @param ManagerInterface $eventManager -+ * @param MetadataPool|null $metadataPool - */ - public function __construct( - ResourceConnection $resource, - StockConfigurationInterface $stockConfiguration, - CacheContext $cacheContext, -- ManagerInterface $eventManager -+ ManagerInterface $eventManager, -+ MetadataPool $metadataPool = null - ) { - $this->resource = $resource; - $this->stockConfiguration = $stockConfiguration; - $this->cacheContext = $cacheContext; - $this->eventManager = $eventManager; -+ $this->metadataPool = $metadataPool ?: ObjectManager::getInstance()->get(MetadataPool::class); - } - - /** -+ * Clean cache by product ids. -+ * - * @param array $productIds - * @param callable $reindex - * @return void -@@ -76,22 +88,37 @@ class CacheCleaner - $productStatusesAfter = $this->getProductStockStatuses($productIds); - $productIds = $this->getProductIdsForCacheClean($productStatusesBefore, $productStatusesAfter); - if ($productIds) { -- $this->cacheContext->registerEntities(Product::CACHE_TAG, $productIds); -+ $this->cacheContext->registerEntities(Product::CACHE_TAG, array_unique($productIds)); - $this->eventManager->dispatch('clean_cache_by_tags', ['object' => $this->cacheContext]); - } - } - - /** -+ * Get current stock statuses for product ids. -+ * - * @param array $productIds - * @return array - */ - private function getProductStockStatuses(array $productIds) - { -+ $linkField = $this->metadataPool->getMetadata(\Magento\Catalog\Api\Data\ProductInterface::class) -+ ->getLinkField(); - $select = $this->getConnection()->select() - ->from( -- $this->resource->getTableName('cataloginventory_stock_status'), -+ ['css' => $this->resource->getTableName('cataloginventory_stock_status')], - ['product_id', 'stock_status', 'qty'] -- )->where('product_id IN (?)', $productIds) -+ ) -+ ->joinLeft( -+ ['cpr' => $this->resource->getTableName('catalog_product_relation')], -+ 'css.product_id = cpr.child_id', -+ [] -+ ) -+ ->joinLeft( -+ ['cpe' => $this->resource->getTableName('catalog_product_entity')], -+ 'cpr.parent_id = cpe.' . $linkField, -+ ['parent_id' => 'cpe.entity_id'] -+ ) -+ ->where('product_id IN (?)', $productIds) - ->where('stock_id = ?', Stock::DEFAULT_STOCK_ID) - ->where('website_id = ?', $this->stockConfiguration->getDefaultScopeId()); - -@@ -125,6 +152,9 @@ class CacheCleaner - if ($statusBefore['stock_status'] !== $statusAfter['stock_status'] - || ($stockThresholdQty && $statusAfter['qty'] <= $stockThresholdQty)) { - $productIds[] = $productId; -+ if (isset($statusAfter['parent_id'])) { -+ $productIds[] = $statusAfter['parent_id']; -+ } - } - } - -@@ -132,6 +162,8 @@ class CacheCleaner - } - - /** -+ * Get database connection. -+ * - * @return AdapterInterface - */ - private function getConnection() -diff --git a/app/code/Magento/CatalogInventory/Model/Plugin/FilterCustomAttribute.php b/app/code/Magento/CatalogInventory/Model/Plugin/FilterCustomAttribute.php -index ff0b5a42bfd..edbb7f50771 100644 ---- a/app/code/Magento/CatalogInventory/Model/Plugin/FilterCustomAttribute.php -+++ b/app/code/Magento/CatalogInventory/Model/Plugin/FilterCustomAttribute.php -@@ -8,39 +8,40 @@ - namespace Magento\CatalogInventory\Model\Plugin; - - use Magento\Catalog\Model\Product\Attribute\Repository; -+use Magento\Catalog\Model\FilterProductCustomAttribute as Filter; - - class FilterCustomAttribute - { - /** -- * @var array -+ * @var Filter - */ -- private $blackList; -+ private $filter; - - /** -- * @param array $blackList -+ * @param Filter $filter -+ * @internal param Filter $customAttribute - */ -- public function __construct(array $blackList = []) -+ public function __construct(Filter $filter) - { -- $this->blackList = $blackList; -+ $this->filter = $filter; - } - - /** -- * Delete custom attribute -+ * Remove attributes from black list - * - * @param Repository $repository - * @param array $attributes -- * @return \Magento\Eav\Model\AttributeRepository -+ * @return \Magento\Framework\Api\MetadataObjectInterface[] - * - * @SuppressWarnings(PHPMD.UnusedFormalParameter) - */ -- public function afterGetCustomAttributesMetadata(Repository $repository, array $attributes) -+ public function afterGetCustomAttributesMetadata(Repository $repository, array $attributes): array - { -- foreach ($attributes as $key => $attribute) { -- if (in_array($attribute->getAttributeCode(), $this->blackList)) { -- unset($attributes[$key]); -- } -+ $return = []; -+ foreach ($attributes as $attribute) { -+ $return[$attribute->getAttributeCode()] = $attribute; - } - -- return $attributes; -+ return $this->filter->execute($return); - } - } -diff --git a/app/code/Magento/CatalogInventory/Model/Plugin/Layer.php b/app/code/Magento/CatalogInventory/Model/Plugin/Layer.php -index b8e8e47bb1f..168e947b8fa 100644 ---- a/app/code/Magento/CatalogInventory/Model/Plugin/Layer.php -+++ b/app/code/Magento/CatalogInventory/Model/Plugin/Layer.php -@@ -3,8 +3,15 @@ - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -+ - namespace Magento\CatalogInventory\Model\Plugin; - -+use Magento\Framework\Search\EngineResolverInterface; -+use Magento\Search\Model\EngineResolver; -+ -+/** -+ * Catalog inventory plugin for layer. -+ */ - class Layer - { - /** -@@ -21,16 +28,24 @@ class Layer - */ - protected $scopeConfig; - -+ /** -+ * @var EngineResolverInterface -+ */ -+ private $engineResolver; -+ - /** - * @param \Magento\CatalogInventory\Helper\Stock $stockHelper - * @param \Magento\Framework\App\Config\ScopeConfigInterface $scopeConfig -+ * @param EngineResolverInterface $engineResolver - */ - public function __construct( - \Magento\CatalogInventory\Helper\Stock $stockHelper, -- \Magento\Framework\App\Config\ScopeConfigInterface $scopeConfig -+ \Magento\Framework\App\Config\ScopeConfigInterface $scopeConfig, -+ EngineResolverInterface $engineResolver - ) { - $this->stockHelper = $stockHelper; - $this->scopeConfig = $scopeConfig; -+ $this->engineResolver = $engineResolver; - } - - /** -@@ -46,12 +61,22 @@ class Layer - \Magento\Catalog\Model\Layer $subject, - \Magento\Catalog\Model\ResourceModel\Collection\AbstractCollection $collection - ) { -- if ($this->_isEnabledShowOutOfStock()) { -+ if (!$this->isCurrentEngineMysql() || $this->_isEnabledShowOutOfStock()) { - return; - } - $this->stockHelper->addIsInStockFilterToCollection($collection); - } - -+ /** -+ * Check if current engine is MYSQL. -+ * -+ * @return bool -+ */ -+ private function isCurrentEngineMysql() -+ { -+ return $this->engineResolver->getCurrentSearchEngine() === EngineResolver::CATALOG_SEARCH_MYSQL_ENGINE; -+ } -+ - /** - * Get config value for 'display out of stock' option - * -diff --git a/app/code/Magento/CatalogInventory/Model/Quote/Item/QuantityValidator.php b/app/code/Magento/CatalogInventory/Model/Quote/Item/QuantityValidator.php -index 0cc77bb7caf..502d9532e8a 100644 ---- a/app/code/Magento/CatalogInventory/Model/Quote/Item/QuantityValidator.php -+++ b/app/code/Magento/CatalogInventory/Model/Quote/Item/QuantityValidator.php -@@ -17,10 +17,18 @@ use Magento\CatalogInventory\Model\Quote\Item\QuantityValidator\Initializer\Stoc - use Magento\CatalogInventory\Model\Stock; - use Magento\Framework\Event\Observer; - use Magento\Framework\Exception\LocalizedException; -+use Magento\Quote\Model\Quote\Item; - - /** -+ * Quote item quantity validator. -+ * - * @api - * @since 100.0.2 -+ * @SuppressWarnings(PHPMD.CouplingBetweenObjects) -+ * -+ * @deprecated 2.3.0 Replaced with Multi Source Inventory -+ * @link https://devdocs.magento.com/guides/v2.3/inventory/index.html -+ * @link https://devdocs.magento.com/guides/v2.3/inventory/catalog-inventory-replacements.html - */ - class QuantityValidator - { -@@ -67,8 +75,7 @@ class QuantityValidator - * Add error information to Quote Item - * - * @param \Magento\Framework\DataObject $result -- * @param \Magento\Quote\Model\Quote\Item $quoteItem -- * @param bool $removeError -+ * @param Item $quoteItem - * @return void - */ - private function addErrorInfoToQuote($result, $quoteItem) -@@ -100,7 +107,7 @@ class QuantityValidator - */ - public function validate(Observer $observer) - { -- /* @var $quoteItem \Magento\Quote\Model\Quote\Item */ -+ /* @var $quoteItem Item */ - $quoteItem = $observer->getEvent()->getItem(); - if (!$quoteItem || - !$quoteItem->getProductId() || -@@ -175,35 +182,11 @@ class QuantityValidator - $qty = $product->getTypeInstance()->prepareQuoteItemQty($qty, $product); - $quoteItem->setData('qty', $qty); - if ($stockStatus) { -- $result = $this->stockState->checkQtyIncrements( -- $product->getId(), -- $qty, -- $product->getStore()->getWebsiteId() -- ); -- if ($result->getHasError()) { -- $quoteItem->addErrorInfo( -- 'cataloginventory', -- Data::ERROR_QTY_INCREMENTS, -- $result->getMessage() -- ); -- -- $quoteItem->getQuote()->addErrorInfo( -- $result->getQuoteMessageIndex(), -- 'cataloginventory', -- Data::ERROR_QTY_INCREMENTS, -- $result->getQuoteMessage() -- ); -- } else { -- // Delete error from item and its quote, if it was set due to qty problems -- $this->_removeErrorsFromQuoteAndItem( -- $quoteItem, -- Data::ERROR_QTY_INCREMENTS -- ); -- } -+ $this->checkOptionsQtyIncrements($quoteItem, $options); - } -+ - // variable to keep track if we have previously encountered an error in one of the options - $removeError = true; -- - foreach ($options as $option) { - $result = $option->getStockStateResult(); - if ($result->getHasError()) { -@@ -228,10 +211,47 @@ class QuantityValidator - } - } - -+ /** -+ * Verifies product options quantity increments. -+ * -+ * @param Item $quoteItem -+ * @param array $options -+ * @return void -+ */ -+ private function checkOptionsQtyIncrements(Item $quoteItem, array $options): void -+ { -+ $removeErrors = true; -+ foreach ($options as $option) { -+ $result = $this->stockState->checkQtyIncrements( -+ $option->getProduct()->getId(), -+ $quoteItem->getData('qty'), -+ $option->getProduct()->getStore()->getWebsiteId() -+ ); -+ if ($result->getHasError()) { -+ $quoteItem->getQuote()->addErrorInfo( -+ $result->getQuoteMessageIndex(), -+ 'cataloginventory', -+ Data::ERROR_QTY_INCREMENTS, -+ $result->getQuoteMessage() -+ ); -+ -+ $removeErrors = false; -+ } -+ } -+ -+ if ($removeErrors) { -+ // Delete error from item and its quote, if it was set due to qty problems -+ $this->_removeErrorsFromQuoteAndItem( -+ $quoteItem, -+ Data::ERROR_QTY_INCREMENTS -+ ); -+ } -+ } -+ - /** - * Removes error statuses from quote and item, set by this observer - * -- * @param \Magento\Quote\Model\Quote\Item $item -+ * @param Item $item - * @param int $code - * @return void - */ -diff --git a/app/code/Magento/CatalogInventory/Model/Quote/Item/QuantityValidator/Initializer/Option.php b/app/code/Magento/CatalogInventory/Model/Quote/Item/QuantityValidator/Initializer/Option.php -index b99e43d52f4..a48b9ae5f08 100644 ---- a/app/code/Magento/CatalogInventory/Model/Quote/Item/QuantityValidator/Initializer/Option.php -+++ b/app/code/Magento/CatalogInventory/Model/Quote/Item/QuantityValidator/Initializer/Option.php -@@ -9,6 +9,9 @@ use Magento\CatalogInventory\Api\StockRegistryInterface; - use Magento\CatalogInventory\Api\StockStateInterface; - use Magento\CatalogInventory\Model\Quote\Item\QuantityValidator\QuoteItemQtyList; - -+/** -+ * Quote item option initializer. -+ */ - class Option - { - /** -@@ -67,10 +70,6 @@ class Option - * define that stock item is child for composite product - */ - $stockItem->setIsChildItem(true); -- /** -- * don't check qty increments value for option product -- */ -- $stockItem->setSuppressCheckQtyIncrements(true); - - return $stockItem; - } -@@ -121,7 +120,7 @@ class Option - /** - * if option's qty was updates we also need to update quote item qty - */ -- $quoteItem->setData('qty', intval($qty)); -+ $quoteItem->setData('qty', (int) $qty); - } - if ($result->getMessage() !== null) { - $option->setMessage($result->getMessage()); -diff --git a/app/code/Magento/CatalogInventory/Model/Quote/Item/QuantityValidator/Initializer/StockItem.php b/app/code/Magento/CatalogInventory/Model/Quote/Item/QuantityValidator/Initializer/StockItem.php -index 6fb0a949941..7a46780f2d7 100644 ---- a/app/code/Magento/CatalogInventory/Model/Quote/Item/QuantityValidator/Initializer/StockItem.php -+++ b/app/code/Magento/CatalogInventory/Model/Quote/Item/QuantityValidator/Initializer/StockItem.php -@@ -7,8 +7,15 @@ namespace Magento\CatalogInventory\Model\Quote\Item\QuantityValidator\Initialize - - use Magento\Catalog\Model\ProductTypes\ConfigInterface; - use Magento\CatalogInventory\Api\StockStateInterface; -+use Magento\CatalogInventory\Api\Data\StockItemInterface; - use Magento\CatalogInventory\Model\Quote\Item\QuantityValidator\QuoteItemQtyList; -+use Magento\CatalogInventory\Model\Spi\StockStateProviderInterface; -+use Magento\Framework\App\ObjectManager; -+use Magento\Quote\Model\Quote\Item; - -+/** -+ * Class StockItem initializes stock item and populates it with data -+ */ - class StockItem - { - /** -@@ -26,26 +33,35 @@ class StockItem - */ - protected $stockState; - -+ /** -+ * @var StockStateProviderInterface -+ */ -+ private $stockStateProvider; -+ - /** - * @param ConfigInterface $typeConfig - * @param QuoteItemQtyList $quoteItemQtyList - * @param StockStateInterface $stockState -+ * @param StockStateProviderInterface|null $stockStateProvider - */ - public function __construct( - ConfigInterface $typeConfig, - QuoteItemQtyList $quoteItemQtyList, -- StockStateInterface $stockState -+ StockStateInterface $stockState, -+ StockStateProviderInterface $stockStateProvider = null - ) { - $this->quoteItemQtyList = $quoteItemQtyList; - $this->typeConfig = $typeConfig; - $this->stockState = $stockState; -+ $this->stockStateProvider = $stockStateProvider ?: ObjectManager::getInstance() -+ ->get(StockStateProviderInterface::class); - } - - /** - * Initialize stock item - * -- * @param \Magento\CatalogInventory\Api\Data\StockItemInterface $stockItem -- * @param \Magento\Quote\Model\Quote\Item $quoteItem -+ * @param StockItemInterface $stockItem -+ * @param Item $quoteItem - * @param int $qty - * - * @return \Magento\Framework\DataObject -@@ -54,11 +70,14 @@ class StockItem - * @SuppressWarnings(PHPMD.NPathComplexity) - */ - public function initialize( -- \Magento\CatalogInventory\Api\Data\StockItemInterface $stockItem, -- \Magento\Quote\Model\Quote\Item $quoteItem, -+ StockItemInterface $stockItem, -+ Item $quoteItem, - $qty - ) { - $product = $quoteItem->getProduct(); -+ $quoteItemId = $quoteItem->getId(); -+ $quoteId = $quoteItem->getQuoteId(); -+ $productId = $product->getId(); - /** - * When we work with subitem - */ -@@ -68,14 +87,14 @@ class StockItem - * we are using 0 because original qty was processed - */ - $qtyForCheck = $this->quoteItemQtyList -- ->getQty($product->getId(), $quoteItem->getId(), $quoteItem->getQuoteId(), 0); -+ ->getQty($productId, $quoteItemId, $quoteId, 0); - } else { - $increaseQty = $quoteItem->getQtyToAdd() ? $quoteItem->getQtyToAdd() : $qty; - $rowQty = $qty; - $qtyForCheck = $this->quoteItemQtyList->getQty( -- $product->getId(), -- $quoteItem->getId(), -- $quoteItem->getQuoteId(), -+ $productId, -+ $quoteItemId, -+ $quoteId, - $increaseQty - ); - } -@@ -90,14 +109,20 @@ class StockItem - - $stockItem->setProductName($product->getName()); - -+ /** @var \Magento\Framework\DataObject $result */ - $result = $this->stockState->checkQuoteItemQty( -- $product->getId(), -+ $productId, - $rowQty, - $qtyForCheck, - $qty, - $product->getStore()->getWebsiteId() - ); - -+ /* We need to ensure that any possible plugin will not erase the data */ -+ $backOrdersQty = $this->stockStateProvider->checkQuoteItemQty($stockItem, $rowQty, $qtyForCheck, $qty) -+ ->getItemBackorders(); -+ $result->setItemBackorders($backOrdersQty); -+ - if ($stockItem->hasIsChildItem()) { - $stockItem->unsIsChildItem(); - } -diff --git a/app/code/Magento/CatalogInventory/Model/ResourceModel/Indexer/Stock/DefaultStock.php b/app/code/Magento/CatalogInventory/Model/ResourceModel/Indexer/Stock/DefaultStock.php -index 366cb1c3902..c5644060c68 100644 ---- a/app/code/Magento/CatalogInventory/Model/ResourceModel/Indexer/Stock/DefaultStock.php -+++ b/app/code/Magento/CatalogInventory/Model/ResourceModel/Indexer/Stock/DefaultStock.php -@@ -18,6 +18,10 @@ use Magento\Catalog\Model\Product\Attribute\Source\Status as ProductStatus; - * @SuppressWarnings(PHPMD.CouplingBetweenObjects) - * @api - * @since 100.0.2 -+ * -+ * @deprecated 2.3.0 Replaced with Multi Source Inventory -+ * @link https://devdocs.magento.com/guides/v2.3/inventory/index.html -+ * @link https://devdocs.magento.com/guides/v2.3/inventory/catalog-inventory-replacements.html - */ - class DefaultStock extends AbstractIndexer implements StockInterface - { -@@ -288,6 +292,7 @@ class DefaultStock extends AbstractIndexer implements StockInterface - */ - protected function _updateIndex($entityIds) - { -+ $this->deleteOldRecords($entityIds); - $connection = $this->getConnection(); - $select = $this->_getStockStatusSelect($entityIds, true); - $select = $this->getQueryProcessorComposite()->processQuery($select, $entityIds, true); -@@ -310,7 +315,6 @@ class DefaultStock extends AbstractIndexer implements StockInterface - } - } - -- $this->deleteOldRecords($entityIds); - $this->_updateIndexTable($data); - - return $this; -@@ -318,6 +322,7 @@ class DefaultStock extends AbstractIndexer implements StockInterface - - /** - * Delete records by their ids from index table -+ * - * Used to clean table before re-indexation - * - * @param array $ids -@@ -362,6 +367,8 @@ class DefaultStock extends AbstractIndexer implements StockInterface - } - - /** -+ * Get status expression -+ * - * @param AdapterInterface $connection - * @param bool $isAggregate - * @return mixed -@@ -387,6 +394,8 @@ class DefaultStock extends AbstractIndexer implements StockInterface - } - - /** -+ * Get stock configuration -+ * - * @return StockConfigurationInterface - * - * @deprecated 100.1.0 -@@ -402,6 +411,8 @@ class DefaultStock extends AbstractIndexer implements StockInterface - } - - /** -+ * Get query processor composite -+ * - * @return QueryProcessorComposite - */ - private function getQueryProcessorComposite() -diff --git a/app/code/Magento/CatalogInventory/Model/ResourceModel/Indexer/Stock/QueryProcessorInterface.php b/app/code/Magento/CatalogInventory/Model/ResourceModel/Indexer/Stock/QueryProcessorInterface.php -index abe473bd968..115002b2376 100644 ---- a/app/code/Magento/CatalogInventory/Model/ResourceModel/Indexer/Stock/QueryProcessorInterface.php -+++ b/app/code/Magento/CatalogInventory/Model/ResourceModel/Indexer/Stock/QueryProcessorInterface.php -@@ -11,6 +11,10 @@ use Magento\Framework\DB\Select; - /** - * @api - * @since 100.1.0 -+ * -+ * @deprecated 2.3.0 Replaced with Multi Source Inventory -+ * @link https://devdocs.magento.com/guides/v2.3/inventory/index.html -+ * @link https://devdocs.magento.com/guides/v2.3/inventory/catalog-inventory-replacements.html - */ - interface QueryProcessorInterface - { -diff --git a/app/code/Magento/CatalogInventory/Model/ResourceModel/Indexer/Stock/StockInterface.php b/app/code/Magento/CatalogInventory/Model/ResourceModel/Indexer/Stock/StockInterface.php -index 5ced55edf20..24ed4963728 100644 ---- a/app/code/Magento/CatalogInventory/Model/ResourceModel/Indexer/Stock/StockInterface.php -+++ b/app/code/Magento/CatalogInventory/Model/ResourceModel/Indexer/Stock/StockInterface.php -@@ -9,6 +9,10 @@ namespace Magento\CatalogInventory\Model\ResourceModel\Indexer\Stock; - * CatalogInventory Stock Indexer Interface - * @api - * @since 100.0.2 -+ * -+ * @deprecated 2.3.0 Replaced with Multi Source Inventory -+ * @link https://devdocs.magento.com/guides/v2.3/inventory/index.html -+ * @link https://devdocs.magento.com/guides/v2.3/inventory/catalog-inventory-replacements.html - */ - interface StockInterface - { -diff --git a/app/code/Magento/CatalogInventory/Model/ResourceModel/Indexer/StockFactory.php b/app/code/Magento/CatalogInventory/Model/ResourceModel/Indexer/StockFactory.php -index f9738484766..0ee162e429f 100644 ---- a/app/code/Magento/CatalogInventory/Model/ResourceModel/Indexer/StockFactory.php -+++ b/app/code/Magento/CatalogInventory/Model/ResourceModel/Indexer/StockFactory.php -@@ -12,6 +12,10 @@ namespace Magento\CatalogInventory\Model\ResourceModel\Indexer; - /** - * @api - * @since 100.0.2 -+ * -+ * @deprecated 2.3.0 Replaced with Multi Source Inventory -+ * @link https://devdocs.magento.com/guides/v2.3/inventory/index.html -+ * @link https://devdocs.magento.com/guides/v2.3/inventory/catalog-inventory-replacements.html - */ - class StockFactory - { -diff --git a/app/code/Magento/CatalogInventory/Model/ResourceModel/Stock/Item.php b/app/code/Magento/CatalogInventory/Model/ResourceModel/Stock/Item.php -index ce8930ad4f7..edccad60231 100644 ---- a/app/code/Magento/CatalogInventory/Model/ResourceModel/Stock/Item.php -+++ b/app/code/Magento/CatalogInventory/Model/ResourceModel/Stock/Item.php -@@ -263,6 +263,12 @@ class Item extends \Magento\Framework\Model\ResourceModel\Db\AbstractDb - $connection->update($this->getMainTable(), $value, $where); - } - -+ /** -+ * Get Manage Stock Expression -+ * -+ * @param string $tableAlias -+ * @return \Zend_Db_Expr -+ */ - public function getManageStockExpr(string $tableAlias = ''): \Zend_Db_Expr - { - if ($tableAlias) { -@@ -277,6 +283,12 @@ class Item extends \Magento\Framework\Model\ResourceModel\Db\AbstractDb - return $manageStock; - } - -+ /** -+ * Get Backorders Expression -+ * -+ * @param string $tableAlias -+ * @return \Zend_Db_Expr -+ */ - public function getBackordersExpr(string $tableAlias = ''): \Zend_Db_Expr - { - if ($tableAlias) { -@@ -291,6 +303,12 @@ class Item extends \Magento\Framework\Model\ResourceModel\Db\AbstractDb - return $itemBackorders; - } - -+ /** -+ * Get Minimum Sale Quantity Expression -+ * -+ * @param string $tableAlias -+ * @return \Zend_Db_Expr -+ */ - public function getMinSaleQtyExpr(string $tableAlias = ''): \Zend_Db_Expr - { - if ($tableAlias) { -diff --git a/app/code/Magento/CatalogInventory/Model/ResourceModel/Stock/Status.php b/app/code/Magento/CatalogInventory/Model/ResourceModel/Stock/Status.php -index bc5fda4939a..402ce5f2f61 100644 ---- a/app/code/Magento/CatalogInventory/Model/ResourceModel/Stock/Status.php -+++ b/app/code/Magento/CatalogInventory/Model/ResourceModel/Stock/Status.php -@@ -13,6 +13,10 @@ use Magento\Framework\App\ObjectManager; - * CatalogInventory Stock Status per website Resource Model - * @SuppressWarnings(PHPMD.CouplingBetweenObjects) - * @api -+ * -+ * @deprecated 2.3.0 Replaced with Multi Source Inventory -+ * @link https://devdocs.magento.com/guides/v2.3/inventory/index.html -+ * @link https://devdocs.magento.com/guides/v2.3/inventory/catalog-inventory-replacements.html - */ - class Status extends \Magento\Framework\Model\ResourceModel\Db\AbstractDb - { -diff --git a/app/code/Magento/CatalogInventory/Model/Source/Backorders.php b/app/code/Magento/CatalogInventory/Model/Source/Backorders.php -index 1d200d27a44..0bffb9a9888 100644 ---- a/app/code/Magento/CatalogInventory/Model/Source/Backorders.php -+++ b/app/code/Magento/CatalogInventory/Model/Source/Backorders.php -@@ -9,6 +9,10 @@ namespace Magento\CatalogInventory\Model\Source; - * Back orders source class - * @api - * @since 100.0.2 -+ * -+ * @deprecated 2.3.0 Replaced with Multi Source Inventory -+ * @link https://devdocs.magento.com/guides/v2.3/inventory/index.html -+ * @link https://devdocs.magento.com/guides/v2.3/inventory/catalog-inventory-replacements.html - */ - class Backorders implements \Magento\Framework\Option\ArrayInterface - { -diff --git a/app/code/Magento/CatalogInventory/Model/Source/Stock.php b/app/code/Magento/CatalogInventory/Model/Source/Stock.php -index 9ed891d1dcc..7d44ab782de 100644 ---- a/app/code/Magento/CatalogInventory/Model/Source/Stock.php -+++ b/app/code/Magento/CatalogInventory/Model/Source/Stock.php -@@ -11,6 +11,10 @@ use Magento\Eav\Model\Entity\Attribute\Source\AbstractSource; - * CatalogInventory Stock source model - * @api - * @since 100.0.2 -+ * -+ * @deprecated 2.3.0 Replaced with Multi Source Inventory -+ * @link https://devdocs.magento.com/guides/v2.3/inventory/index.html -+ * @link https://devdocs.magento.com/guides/v2.3/inventory/catalog-inventory-replacements.html - */ - class Stock extends AbstractSource - { -diff --git a/app/code/Magento/CatalogInventory/Model/Spi/StockRegistryProviderInterface.php b/app/code/Magento/CatalogInventory/Model/Spi/StockRegistryProviderInterface.php -index f711268bc79..0fa4b919c40 100644 ---- a/app/code/Magento/CatalogInventory/Model/Spi/StockRegistryProviderInterface.php -+++ b/app/code/Magento/CatalogInventory/Model/Spi/StockRegistryProviderInterface.php -@@ -7,16 +7,24 @@ namespace Magento\CatalogInventory\Model\Spi; - - /** - * Interface StockRegistryProviderInterface -+ * -+ * @deprecated 2.3.0 Replaced with Multi Source Inventory -+ * @link https://devdocs.magento.com/guides/v2.3/inventory/index.html -+ * @link https://devdocs.magento.com/guides/v2.3/inventory/catalog-inventory-replacements.html - */ - interface StockRegistryProviderInterface - { - /** -+ * Get stock. -+ * - * @param int $scopeId - * @return \Magento\CatalogInventory\Api\Data\StockInterface - */ - public function getStock($scopeId); - - /** -+ * Get stock item. -+ * - * @param int $productId - * @param int $scopeId - * @return \Magento\CatalogInventory\Api\Data\StockItemInterface -@@ -24,6 +32,8 @@ interface StockRegistryProviderInterface - public function getStockItem($productId, $scopeId); - - /** -+ * Get stock status. -+ * - * @param int $productId - * @param int $scopeId - * @return \Magento\CatalogInventory\Api\Data\StockStatusInterface -diff --git a/app/code/Magento/CatalogInventory/Model/Spi/StockStateProviderInterface.php b/app/code/Magento/CatalogInventory/Model/Spi/StockStateProviderInterface.php -index 89fb54e7e49..30f703b5b92 100644 ---- a/app/code/Magento/CatalogInventory/Model/Spi/StockStateProviderInterface.php -+++ b/app/code/Magento/CatalogInventory/Model/Spi/StockStateProviderInterface.php -@@ -9,22 +9,32 @@ use Magento\CatalogInventory\Api\Data\StockItemInterface; - - /** - * Interface StockStateProviderInterface -+ * -+ * @deprecated 2.3.0 Replaced with Multi Source Inventory -+ * @link https://devdocs.magento.com/guides/v2.3/inventory/index.html -+ * @link https://devdocs.magento.com/guides/v2.3/inventory/catalog-inventory-replacements.html - */ - interface StockStateProviderInterface - { - /** -+ * Verify stock. -+ * - * @param StockItemInterface $stockItem - * @return bool - */ - public function verifyStock(StockItemInterface $stockItem); - - /** -+ * Verify notification. -+ * - * @param StockItemInterface $stockItem - * @return bool - */ - public function verifyNotification(StockItemInterface $stockItem); - - /** -+ * Validate quote qty. -+ * - * @param StockItemInterface $stockItem - * @param int|float $itemQty - * @param int|float $qtyToCheck -@@ -44,8 +54,9 @@ interface StockStateProviderInterface - public function checkQty(StockItemInterface $stockItem, $qty); - - /** -- * Returns suggested qty that satisfies qty increments and minQty/maxQty/minSaleQty/maxSaleQty conditions -- * or original qty if such value does not exist -+ * Returns suggested qty or original qty if such value does not exist. -+ * -+ * Suggested qty satisfies qty increments and minQty/maxQty/minSaleQty/maxSaleQty conditions. - * - * @param StockItemInterface $stockItem - * @param int|float $qty -@@ -54,6 +65,8 @@ interface StockStateProviderInterface - public function suggestQty(StockItemInterface $stockItem, $qty); - - /** -+ * Check qty increments. -+ * - * @param StockItemInterface $stockItem - * @param int|float $qty - * @return \Magento\Framework\DataObject -diff --git a/app/code/Magento/CatalogInventory/Model/Stock/Item.php b/app/code/Magento/CatalogInventory/Model/Stock/Item.php -index bcab2c622a5..553ea7393c9 100644 ---- a/app/code/Magento/CatalogInventory/Model/Stock/Item.php -+++ b/app/code/Magento/CatalogInventory/Model/Stock/Item.php -@@ -206,7 +206,7 @@ class Item extends AbstractExtensibleModel implements StockItemInterface - */ - public function getQty() - { -- return null === $this->_getData(static::QTY) ? null : floatval($this->_getData(static::QTY)); -+ return null === $this->_getData(static::QTY) ? null : (float)$this->_getData(static::QTY); - } - - /** -diff --git a/app/code/Magento/CatalogInventory/Model/Stock/Status.php b/app/code/Magento/CatalogInventory/Model/Stock/Status.php -index 899056d8f08..4941d5d333b 100644 ---- a/app/code/Magento/CatalogInventory/Model/Stock/Status.php -+++ b/app/code/Magento/CatalogInventory/Model/Stock/Status.php -@@ -72,6 +72,8 @@ class Status extends AbstractExtensibleModel implements StockStatusInterface - //@codeCoverageIgnoreStart - - /** -+ * Retrieve product ID -+ * - * @return int - */ - public function getProductId() -@@ -80,6 +82,8 @@ class Status extends AbstractExtensibleModel implements StockStatusInterface - } - - /** -+ * Retrieve website ID -+ * - * @return int - */ - public function getWebsiteId() -@@ -88,6 +92,8 @@ class Status extends AbstractExtensibleModel implements StockStatusInterface - } - - /** -+ * Retrieve stock ID -+ * - * @return int - */ - public function getStockId() -@@ -96,6 +102,8 @@ class Status extends AbstractExtensibleModel implements StockStatusInterface - } - - /** -+ * Retrieve qty -+ * - * @return int - */ - public function getQty() -@@ -104,16 +112,20 @@ class Status extends AbstractExtensibleModel implements StockStatusInterface - } - - /** -+ * Retrieve stock status -+ * - * @return int - */ -- public function getStockStatus() -+ public function getStockStatus(): int - { -- return $this->getData(self::KEY_STOCK_STATUS); -+ return (int)$this->getData(self::KEY_STOCK_STATUS); - } - - //@codeCoverageIgnoreEnd - - /** -+ * Retrieve stock item -+ * - * @return StockItemInterface - */ - public function getStockItem() -@@ -124,6 +136,8 @@ class Status extends AbstractExtensibleModel implements StockStatusInterface - //@codeCoverageIgnoreStart - - /** -+ * Set product ID -+ * - * @param int $productId - * @return $this - */ -@@ -133,6 +147,8 @@ class Status extends AbstractExtensibleModel implements StockStatusInterface - } - - /** -+ * Set web website ID -+ * - * @param int $websiteId - * @return $this - */ -@@ -142,6 +158,8 @@ class Status extends AbstractExtensibleModel implements StockStatusInterface - } - - /** -+ * Set stock ID -+ * - * @param int $stockId - * @return $this - */ -@@ -151,6 +169,8 @@ class Status extends AbstractExtensibleModel implements StockStatusInterface - } - - /** -+ * Set qty -+ * - * @param int $qty - * @return $this - */ -@@ -160,6 +180,8 @@ class Status extends AbstractExtensibleModel implements StockStatusInterface - } - - /** -+ * Set stock status -+ * - * @param int $stockStatus - * @return $this - */ -@@ -169,7 +191,7 @@ class Status extends AbstractExtensibleModel implements StockStatusInterface - } - - /** -- * {@inheritdoc} -+ * Retrieve existing extension attributes object or create a new one. - * - * @return \Magento\CatalogInventory\Api\Data\StockStatusExtensionInterface|null - */ -@@ -179,7 +201,7 @@ class Status extends AbstractExtensibleModel implements StockStatusInterface - } - - /** -- * {@inheritdoc} -+ * Set an extension attributes object. - * - * @param \Magento\CatalogInventory\Api\Data\StockStatusExtensionInterface $extensionAttributes - * @return $this -diff --git a/app/code/Magento/CatalogInventory/Model/StockManagement.php b/app/code/Magento/CatalogInventory/Model/StockManagement.php -index b3939f2e514..5d7d099dc01 100644 ---- a/app/code/Magento/CatalogInventory/Model/StockManagement.php -+++ b/app/code/Magento/CatalogInventory/Model/StockManagement.php -@@ -85,6 +85,7 @@ class StockManagement implements StockManagementInterface, RegisterProductSaleIn - - /** - * Subtract product qtys from stock. -+ * - * Return array of items that require full save. - * - * @param string[] $items -@@ -141,17 +142,25 @@ class StockManagement implements StockManagementInterface, RegisterProductSaleIn - } - - /** -- * @param string[] $items -- * @param int $websiteId -- * @return bool -+ * @inheritdoc - */ - public function revertProductsSale($items, $websiteId = null) - { - //if (!$websiteId) { - $websiteId = $this->stockConfiguration->getDefaultScopeId(); - //} -- $this->qtyCounter->correctItemsQty($items, $websiteId, '+'); -- return true; -+ $revertItems = []; -+ foreach ($items as $productId => $qty) { -+ $stockItem = $this->stockRegistryProvider->getStockItem($productId, $websiteId); -+ $canSubtractQty = $stockItem->getItemId() && $this->canSubtractQty($stockItem); -+ if (!$canSubtractQty || !$this->stockConfiguration->isQty($stockItem->getTypeId())) { -+ continue; -+ } -+ $revertItems[$productId] = $qty; -+ } -+ $this->qtyCounter->correctItemsQty($revertItems, $websiteId, '+'); -+ -+ return $revertItems; - } - - /** -@@ -195,6 +204,8 @@ class StockManagement implements StockManagementInterface, RegisterProductSaleIn - } - - /** -+ * Get stock resource. -+ * - * @return ResourceStock - */ - protected function getResource() -diff --git a/app/code/Magento/CatalogInventory/Model/StockRegistryStorage.php b/app/code/Magento/CatalogInventory/Model/StockRegistryStorage.php -index 0a54adfe91c..8238c1e8f6b 100644 ---- a/app/code/Magento/CatalogInventory/Model/StockRegistryStorage.php -+++ b/app/code/Magento/CatalogInventory/Model/StockRegistryStorage.php -@@ -68,7 +68,7 @@ class StockRegistryStorage - */ - public function getStockItem($productId, $scopeId) - { -- return isset($this->stockItems[$productId][$scopeId]) ? $this->stockItems[$productId][$scopeId] : null; -+ return $this->stockItems[$productId][$scopeId] ?? null; - } - - /** -@@ -103,7 +103,7 @@ class StockRegistryStorage - */ - public function getStockStatus($productId, $scopeId) - { -- return isset($this->stockStatuses[$productId][$scopeId]) ? $this->stockStatuses[$productId][$scopeId] : null; -+ return $this->stockStatuses[$productId][$scopeId] ?? null; - } - - /** -diff --git a/app/code/Magento/CatalogInventory/Model/StockStateProvider.php b/app/code/Magento/CatalogInventory/Model/StockStateProvider.php -index fb6fc3be613..6851b05aa56 100644 ---- a/app/code/Magento/CatalogInventory/Model/StockStateProvider.php -+++ b/app/code/Magento/CatalogInventory/Model/StockStateProvider.php -@@ -9,9 +9,9 @@ namespace Magento\CatalogInventory\Model; - use Magento\Catalog\Model\ProductFactory; - use Magento\CatalogInventory\Api\Data\StockItemInterface; - use Magento\CatalogInventory\Model\Spi\StockStateProviderInterface; -+use Magento\Framework\DataObject\Factory as ObjectFactory; - use Magento\Framework\Locale\FormatInterface; - use Magento\Framework\Math\Division as MathDivision; --use Magento\Framework\DataObject\Factory as ObjectFactory; - - /** - * Interface StockStateProvider -@@ -65,6 +65,8 @@ class StockStateProvider implements StockStateProviderInterface - } - - /** -+ * Validate stock -+ * - * @param StockItemInterface $stockItem - * @return bool - */ -@@ -82,6 +84,8 @@ class StockStateProvider implements StockStateProviderInterface - } - - /** -+ * Verify notification -+ * - * @param StockItemInterface $stockItem - * @return bool - */ -@@ -91,6 +95,8 @@ class StockStateProvider implements StockStateProviderInterface - } - - /** -+ * Validate quote qty -+ * - * @param StockItemInterface $stockItem - * @param int|float $qty - * @param int|float $summaryQty -@@ -113,14 +119,12 @@ class StockStateProvider implements StockStateProviderInterface - $result->setItemIsQtyDecimal($stockItem->getIsQtyDecimal()); - if (!$stockItem->getIsQtyDecimal()) { - $result->setHasQtyOptionUpdate(true); -- $qty = intval($qty); -+ $qty = (int) $qty ?: 1; - /** - * Adding stock data to quote item - */ - $result->setItemQty($qty); -- $qty = $this->getNumber($qty); -- $origQty = intval($origQty); -- $result->setOrigQty($origQty); -+ $result->setOrigQty((int)$this->getNumber($origQty) ?: 1); - } - - if ($stockItem->getMinSaleQty() && $qty < $stockItem->getMinSaleQty()) { -@@ -254,6 +258,8 @@ class StockStateProvider implements StockStateProviderInterface - } - - /** -+ * Returns suggested qty -+ * - * Returns suggested qty that satisfies qty increments and minQty/maxQty/minSaleQty/maxSaleQty conditions - * or original qty if such value does not exist - * -@@ -294,6 +300,8 @@ class StockStateProvider implements StockStateProviderInterface - } - - /** -+ * Check Qty Increments -+ * - * @param StockItemInterface $stockItem - * @param float|int $qty - * @return \Magento\Framework\DataObject -@@ -369,6 +377,8 @@ class StockStateProvider implements StockStateProviderInterface - } - - /** -+ * Get numeric qty -+ * - * @param string|float|int|null $qty - * @return float|null - */ -diff --git a/app/code/Magento/CatalogInventory/Observer/CancelOrderItemObserver.php b/app/code/Magento/CatalogInventory/Observer/CancelOrderItemObserver.php -index 1e99794d68a..098e254d785 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 @@ class CancelOrderItemObserver implements ObserverInterface - $item = $observer->getEvent()->getItem(); - $children = $item->getChildrenItems(); - $qty = $item->getQtyOrdered() - max($item->getQtyShipped(), $item->getQtyInvoiced()) - $item->getQtyCanceled(); -- if ($item->getId() && $item->getProductId() && empty($children) && $qty) { -+ if ($item->getId() && $item->getProductId() && empty($children) && $qty && $this->configuration -+ ->getCanBackInStock()) { - $this->stockManagement->backItemQty($item->getProductId(), $qty, $item->getStore()->getWebsiteId()); - } - $this->priceIndexer->reindexRow($item->getProductId()); -diff --git a/app/code/Magento/CatalogInventory/Observer/ParentItemProcessorInterface.php b/app/code/Magento/CatalogInventory/Observer/ParentItemProcessorInterface.php -new file mode 100644 -index 00000000000..dd5689a396e ---- /dev/null -+++ b/app/code/Magento/CatalogInventory/Observer/ParentItemProcessorInterface.php -@@ -0,0 +1,24 @@ -+<?php -+/** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+declare(strict_types=1); -+ -+namespace Magento\CatalogInventory\Observer; -+ -+use Magento\Catalog\Api\Data\ProductInterface as Product; -+ -+/** -+ * Interface for processing parent items of complex product types -+ */ -+interface ParentItemProcessorInterface -+{ -+ /** -+ * Process stock for parent items -+ * -+ * @param Product $product -+ * @return void -+ */ -+ public function process(Product $product); -+} -diff --git a/app/code/Magento/CatalogInventory/Observer/ProcessInventoryDataObserver.php b/app/code/Magento/CatalogInventory/Observer/ProcessInventoryDataObserver.php -index b831af53d4a..6aad119694a 100644 ---- a/app/code/Magento/CatalogInventory/Observer/ProcessInventoryDataObserver.php -+++ b/app/code/Magento/CatalogInventory/Observer/ProcessInventoryDataObserver.php -@@ -12,8 +12,7 @@ use Magento\Framework\Event\ObserverInterface; - use Magento\Framework\Event\Observer as EventObserver; - - /** -- * This observer prepares stock data for saving by combining stock data from the stock data property -- * and quantity_and_stock_status attribute and setting it to the single point represented by stock data property. -+ * Prepares stock data for saving - * - * @deprecated 100.2.0 Stock data should be processed using the module API - * @see StockItemInterface when you want to change the stock data -@@ -74,7 +73,6 @@ class ProcessInventoryDataObserver implements ObserverInterface - $this->setStockDataToProduct($product, $stockItem, $quantityAndStockStatus); - } - } -- $product->unsetData('quantity_and_stock_status'); - } - - /** -diff --git a/app/code/Magento/CatalogInventory/Observer/RevertQuoteInventoryObserver.php b/app/code/Magento/CatalogInventory/Observer/RevertQuoteInventoryObserver.php -index 93a50cc9a7a..ab21f32b3f6 100644 ---- a/app/code/Magento/CatalogInventory/Observer/RevertQuoteInventoryObserver.php -+++ b/app/code/Magento/CatalogInventory/Observer/RevertQuoteInventoryObserver.php -@@ -64,8 +64,8 @@ class RevertQuoteInventoryObserver implements ObserverInterface - { - $quote = $observer->getEvent()->getQuote(); - $items = $this->productQty->getProductQty($quote->getAllItems()); -- $this->stockManagement->revertProductsSale($items, $quote->getStore()->getWebsiteId()); -- $productIds = array_keys($items); -+ $revertedItems = $this->stockManagement->revertProductsSale($items, $quote->getStore()->getWebsiteId()); -+ $productIds = array_keys($revertedItems); - if (!empty($productIds)) { - $this->stockIndexerProcessor->reindexList($productIds); - $this->priceIndexer->reindexList($productIds); -diff --git a/app/code/Magento/CatalogInventory/Observer/SaveInventoryDataObserver.php b/app/code/Magento/CatalogInventory/Observer/SaveInventoryDataObserver.php -index 03ba58d3f49..dd67140fa0c 100644 ---- a/app/code/Magento/CatalogInventory/Observer/SaveInventoryDataObserver.php -+++ b/app/code/Magento/CatalogInventory/Observer/SaveInventoryDataObserver.php -@@ -13,6 +13,8 @@ use Magento\CatalogInventory\Api\StockConfigurationInterface; - use Magento\CatalogInventory\Api\StockRegistryInterface; - use Magento\CatalogInventory\Model\StockItemValidator; - use Magento\Framework\Event\Observer as EventObserver; -+use Magento\Framework\Exception\LocalizedException; -+use Magento\Framework\Exception\NoSuchEntityException; - - /** - * Saves stock data from a product to the Stock Item -@@ -39,6 +41,11 @@ class SaveInventoryDataObserver implements ObserverInterface - */ - private $stockItemValidator; - -+ /** -+ * @var ParentItemProcessorInterface[] -+ */ -+ private $parentItemProcessorPool; -+ - /** - * @var array - */ -@@ -77,15 +84,18 @@ class SaveInventoryDataObserver implements ObserverInterface - * @param StockConfigurationInterface $stockConfiguration - * @param StockRegistryInterface $stockRegistry - * @param StockItemValidator $stockItemValidator -+ * @param ParentItemProcessorInterface[] $parentItemProcessorPool - */ - public function __construct( - StockConfigurationInterface $stockConfiguration, - StockRegistryInterface $stockRegistry, -- StockItemValidator $stockItemValidator = null -+ StockItemValidator $stockItemValidator = null, -+ array $parentItemProcessorPool = [] - ) { - $this->stockConfiguration = $stockConfiguration; - $this->stockRegistry = $stockRegistry; - $this->stockItemValidator = $stockItemValidator ?: ObjectManager::getInstance()->get(StockItemValidator::class); -+ $this->parentItemProcessorPool = $parentItemProcessorPool; - } - - /** -@@ -96,10 +106,15 @@ class SaveInventoryDataObserver implements ObserverInterface - * - * @param EventObserver $observer - * @return void -+ * @throws LocalizedException -+ * @throws NoSuchEntityException - */ - public function execute(EventObserver $observer) - { -+ /** @var Product $product */ - $product = $observer->getEvent()->getProduct(); -+ -+ /** @var Item $stockItem */ - $stockItem = $this->getStockItemToBeUpdated($product); - - if ($product->getStockData() !== null) { -@@ -108,6 +123,7 @@ class SaveInventoryDataObserver implements ObserverInterface - } - $this->stockItemValidator->validate($product, $stockItem); - $this->stockRegistry->updateStockItemBySku($product->getSku(), $stockItem); -+ $this->processParents($product); - } - - /** -@@ -156,4 +172,17 @@ class SaveInventoryDataObserver implements ObserverInterface - } - return $stockData; - } -+ -+ /** -+ * Process stock data for parent products -+ * -+ * @param Product $product -+ * @return void -+ */ -+ private function processParents(Product $product) -+ { -+ foreach ($this->parentItemProcessorPool as $processor) { -+ $processor->process($product); -+ } -+ } - } -diff --git a/app/code/Magento/CatalogInventory/Plugin/MassUpdateProductAttribute.php b/app/code/Magento/CatalogInventory/Plugin/MassUpdateProductAttribute.php -new file mode 100644 -index 00000000000..334d2b22edb ---- /dev/null -+++ b/app/code/Magento/CatalogInventory/Plugin/MassUpdateProductAttribute.php -@@ -0,0 +1,159 @@ -+<?php -+/** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+namespace Magento\CatalogInventory\Plugin; -+ -+use Magento\Catalog\Controller\Adminhtml\Product\Action\Attribute\Save; -+use Magento\CatalogInventory\Api\Data\StockItemInterface; -+ -+/** -+ * MassUpdate product attribute. -+ * @SuppressWarnings(PHPMD.CouplingBetweenObjects) -+ */ -+class MassUpdateProductAttribute -+{ -+ /** -+ * @var \Magento\CatalogInventory\Model\Indexer\Stock\Processor -+ */ -+ private $stockIndexerProcessor; -+ -+ /** -+ * @var \Magento\Framework\Api\DataObjectHelper -+ */ -+ private $dataObjectHelper; -+ -+ /** -+ * @var \Magento\CatalogInventory\Api\StockRegistryInterface -+ */ -+ private $stockRegistry; -+ -+ /** -+ * @var \Magento\CatalogInventory\Api\StockItemRepositoryInterface -+ */ -+ private $stockItemRepository; -+ -+ /** -+ * @var \Magento\CatalogInventory\Api\StockConfigurationInterface -+ */ -+ private $stockConfiguration; -+ -+ /** -+ * @var \Magento\Catalog\Helper\Product\Edit\Action\Attribute -+ */ -+ private $attributeHelper; -+ -+ /** -+ * @var \Magento\Framework\Message\ManagerInterface -+ */ -+ private $messageManager; -+ -+ /** -+ * @param \Magento\CatalogInventory\Model\Indexer\Stock\Processor $stockIndexerProcessor -+ * @param \Magento\Framework\Api\DataObjectHelper $dataObjectHelper -+ * @param \Magento\CatalogInventory\Api\StockRegistryInterface $stockRegistry -+ * @param \Magento\CatalogInventory\Api\StockItemRepositoryInterface $stockItemRepository -+ * @param \Magento\CatalogInventory\Api\StockConfigurationInterface $stockConfiguration -+ * @param \Magento\Catalog\Helper\Product\Edit\Action\Attribute $attributeHelper -+ * @param \Magento\Framework\Message\ManagerInterface $messageManager -+ * @SuppressWarnings(PHPMD.ExcessiveParameterList) -+ */ -+ public function __construct( -+ \Magento\CatalogInventory\Model\Indexer\Stock\Processor $stockIndexerProcessor, -+ \Magento\Framework\Api\DataObjectHelper $dataObjectHelper, -+ \Magento\CatalogInventory\Api\StockRegistryInterface $stockRegistry, -+ \Magento\CatalogInventory\Api\StockItemRepositoryInterface $stockItemRepository, -+ \Magento\CatalogInventory\Api\StockConfigurationInterface $stockConfiguration, -+ \Magento\Catalog\Helper\Product\Edit\Action\Attribute $attributeHelper, -+ \Magento\Framework\Message\ManagerInterface $messageManager -+ ) { -+ $this->stockIndexerProcessor = $stockIndexerProcessor; -+ $this->dataObjectHelper = $dataObjectHelper; -+ $this->stockRegistry = $stockRegistry; -+ $this->stockItemRepository = $stockItemRepository; -+ $this->stockConfiguration = $stockConfiguration; -+ $this->attributeHelper = $attributeHelper; -+ $this->messageManager = $messageManager; -+ } -+ -+ /** -+ * Around execute plugin -+ * -+ * @param Save $subject -+ * @param callable $proceed -+ * -+ * @return \Magento\Framework\Controller\ResultInterface -+ */ -+ public function aroundExecute(Save $subject, callable $proceed) -+ { -+ try { -+ /** @var \Magento\Framework\App\RequestInterface $request */ -+ $request = $subject->getRequest(); -+ $inventoryData = $request->getParam('inventory', []); -+ $inventoryData = $this->addConfigSettings($inventoryData); -+ -+ $storeId = $this->attributeHelper->getSelectedStoreId(); -+ $websiteId = $this->attributeHelper->getStoreWebsiteId($storeId); -+ $productIds = $this->attributeHelper->getProductIds(); -+ -+ if (!empty($inventoryData)) { -+ $this->updateInventoryInProducts($productIds, $websiteId, $inventoryData); -+ } -+ -+ return $proceed(); -+ } catch (\Magento\Framework\Exception\LocalizedException $e) { -+ $this->messageManager->addErrorMessage($e->getMessage()); -+ return $proceed(); -+ } catch (\Exception $e) { -+ $this->messageManager->addExceptionMessage( -+ $e, -+ __('Something went wrong while updating the product(s) attributes.') -+ ); -+ return $proceed(); -+ } -+ } -+ -+ /** -+ * Add config settings -+ * -+ * @param array $inventoryData -+ * -+ * @return array -+ */ -+ private function addConfigSettings($inventoryData) -+ { -+ $options = $this->stockConfiguration->getConfigItemOptions(); -+ foreach ($options as $option) { -+ $useConfig = 'use_config_' . $option; -+ if (isset($inventoryData[$option]) && !isset($inventoryData[$useConfig])) { -+ $inventoryData[$useConfig] = 0; -+ } -+ } -+ return $inventoryData; -+ } -+ -+ /** -+ * Update inventory in products -+ * -+ * @param array $productIds -+ * @param int $websiteId -+ * @param array $inventoryData -+ * -+ * @return void -+ */ -+ private function updateInventoryInProducts($productIds, $websiteId, $inventoryData): void -+ { -+ foreach ($productIds as $productId) { -+ $stockItemDo = $this->stockRegistry->getStockItem($productId, $websiteId); -+ if (!$stockItemDo->getProductId()) { -+ $inventoryData['product_id'] = $productId; -+ } -+ $stockItemId = $stockItemDo->getId(); -+ $this->dataObjectHelper->populateWithArray($stockItemDo, $inventoryData, StockItemInterface::class); -+ $stockItemDo->setItemId($stockItemId); -+ $this->stockItemRepository->save($stockItemDo); -+ } -+ $this->stockIndexerProcessor->reindexList($productIds); -+ } -+} -diff --git a/app/code/Magento/CatalogInventory/Test/Mftf/ActionGroup/DisplayOutOfStockProductActionGroup.xml b/app/code/Magento/CatalogInventory/Test/Mftf/ActionGroup/DisplayOutOfStockProductActionGroup.xml -index 1bec4cc99c0..2850b8d0692 100644 ---- a/app/code/Magento/CatalogInventory/Test/Mftf/ActionGroup/DisplayOutOfStockProductActionGroup.xml -+++ b/app/code/Magento/CatalogInventory/Test/Mftf/ActionGroup/DisplayOutOfStockProductActionGroup.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"> - <actionGroup name="displayOutOfStockProduct"> - <amOnPage url="{{InventoryConfigurationPage.url}}" stepKey="navigateToInventoryConfigurationPage"/> - <waitForPageLoad stepKey="waitForConfigPageToLoad"/> -@@ -21,6 +21,7 @@ - <actionGroup name="noDisplayOutOfStockProduct"> - <amOnPage url="{{InventoryConfigurationPage.url}}" stepKey="navigateToInventoryConfigurationPage"/> - <waitForPageLoad stepKey="waitForConfigPageToLoad"/> -+ <conditionalClick stepKey="expandProductStockOptions" selector="{{InventoryConfigSection.ProductStockOptionsTab}}" dependentSelector="{{InventoryConfigSection.CheckIfProductStockOptionsTabExpanded}}" visible="true" /> - <uncheckOption selector="{{InventoryConfigSection.DisplayOutOfStockSystemValue}}" stepKey="uncheckUseSystemValue"/> - <waitForElementVisible selector="{{InventoryConfigSection.DisplayOutOfStockDropdown}}" stepKey="waitForSwitcherDropdown" /> - <selectOption selector="{{InventoryConfigSection.DisplayOutOfStockDropdown}}" userInput="No" stepKey="switchToNo" /> -diff --git a/app/code/Magento/CatalogInventory/Test/Mftf/Data/CatalogInventoryConfigData.xml b/app/code/Magento/CatalogInventory/Test/Mftf/Data/CatalogInventoryConfigData.xml -new file mode 100644 -index 00000000000..e14c36446fc ---- /dev/null -+++ b/app/code/Magento/CatalogInventory/Test/Mftf/Data/CatalogInventoryConfigData.xml -@@ -0,0 +1,23 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<entities xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataProfileSchema.xsd"> -+ <entity name="StockOptionsDisplayOutOfStockProductsEnable"> -+ <data key="path">cataloginventory/options/show_out_of_stock</data> -+ <data key="scope_id">0</data> -+ <data key="label">Yes</data> -+ <data key="value">1</data> -+ </entity> -+ <entity name="StockOptionsDisplayOutOfStockProductsDisable"> -+ <data key="path">cataloginventory/options/show_out_of_stock</data> -+ <data key="scope_id">0</data> -+ <data key="label">No</data> -+ <data key="value">0</data> -+ </entity> -+</entities> -diff --git a/app/code/Magento/CatalogInventory/Test/Mftf/Data/CatalogInventryConfigData.xml b/app/code/Magento/CatalogInventory/Test/Mftf/Data/CatalogInventryConfigData.xml -new file mode 100644 -index 00000000000..3a49b821ead ---- /dev/null -+++ b/app/code/Magento/CatalogInventory/Test/Mftf/Data/CatalogInventryConfigData.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="EnableCatalogInventoryConfigData"> -+ <!--Default Value --> -+ <data key="path">cataloginventory/options/can_subtract</data> -+ <data key="scope_id">0</data> -+ <data key="label">Yes</data> -+ <data key="value">1</data> -+ </entity> -+ <entity name="DisableCatalogInventoryConfigData"> -+ <data key="path">cataloginventory/options/can_subtract</data> -+ <data key="scope_id">0</data> -+ <data key="label">No</data> -+ <data key="value">0</data> -+ </entity> -+</entities> -\ No newline at end of file -diff --git a/app/code/Magento/CatalogInventory/Test/Mftf/Page/InventoryConfigurationPage.xml b/app/code/Magento/CatalogInventory/Test/Mftf/Page/InventoryConfigurationPage.xml -index 95e873a3b16..ba8a3c300b2 100644 ---- a/app/code/Magento/CatalogInventory/Test/Mftf/Page/InventoryConfigurationPage.xml -+++ b/app/code/Magento/CatalogInventory/Test/Mftf/Page/InventoryConfigurationPage.xml -@@ -5,7 +5,7 @@ - * 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"> -+<pages xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/PageObject.xsd"> - <page name="InventoryConfigurationPage" url="admin/system_config/edit/section/cataloginventory/" area="admin" module="Magento_Config"> - <section name="InventorySection"/> - </page> -diff --git a/app/code/Magento/CatalogInventory/Test/Mftf/Section/InventorySection.xml b/app/code/Magento/CatalogInventory/Test/Mftf/Section/InventorySection.xml -index 55fbc84ead9..929f43467b9 100644 ---- a/app/code/Magento/CatalogInventory/Test/Mftf/Section/InventorySection.xml -+++ b/app/code/Magento/CatalogInventory/Test/Mftf/Section/InventorySection.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="InventoryConfigSection"> - <element name="ProductStockOptionsTab" type="button" selector="#cataloginventory_options-head"/> - <element name="CheckIfProductStockOptionsTabExpanded" type="button" selector="#cataloginventory_options-head:not(.open)"/> -diff --git a/app/code/Magento/CatalogInventory/Test/Mftf/Test/AssociatedProductToConfigurableOutOfStockTest.xml b/app/code/Magento/CatalogInventory/Test/Mftf/Test/AssociatedProductToConfigurableOutOfStockTest.xml -new file mode 100644 -index 00000000000..8458fcf3b94 ---- /dev/null -+++ b/app/code/Magento/CatalogInventory/Test/Mftf/Test/AssociatedProductToConfigurableOutOfStockTest.xml -@@ -0,0 +1,146 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> -+ <test name="AssociatedProductToConfigurableOutOfStockTest"> -+ <annotations> -+ <features value="CatalogInventory"/> -+ <stories value="Add/remove images and videos for all product types and category"/> -+ <title value="Out of stock associated products to configurable are not full page cache cleaned "/> -+ <description value="After last configurable product was ordered it becomes out of stock"/> -+ <severity value="MAJOR"/> -+ <testCaseId value="MAGETWO-94135"/> -+ <group value="CatalogInventory"/> -+ </annotations> -+ -+ <before> -+ <createData entity="SimpleSubCategory" stepKey="simplecategory"/> -+ -+ <!-- Create configurable product with two options --> -+ <createData entity="ApiConfigurableProduct" stepKey="createConfigProduct"> -+ <requiredEntity createDataKey="simplecategory"/> -+ </createData> -+ -+ <createData entity="productAttributeWithTwoOptions" stepKey="createConfigProductAttribute"/> -+ <createData entity="productAttributeOption1" stepKey="createConfigProductAttributeOption1"> -+ <requiredEntity createDataKey="createConfigProductAttribute"/> -+ </createData> -+ <createData entity="productAttributeOption2" stepKey="createConfigProductAttributeOption2"> -+ <requiredEntity createDataKey="createConfigProductAttribute"/> -+ </createData> -+ -+ <createData entity="AddToDefaultSet" stepKey="createConfigAddToAttributeSet"> -+ <requiredEntity createDataKey="createConfigProductAttribute"/> -+ </createData> -+ -+ <getData entity="ProductAttributeOptionGetter" index="1" stepKey="getConfigAttributeOption1"> -+ <requiredEntity createDataKey="createConfigProductAttribute"/> -+ </getData> -+ <getData entity="ProductAttributeOptionGetter" index="2" stepKey="getConfigAttributeOption2"> -+ <requiredEntity createDataKey="createConfigProductAttribute"/> -+ </getData> -+ -+ <!-- Create child product with single quantity --> -+ <createData entity="ApiSimpleSingleQty" stepKey="createConfigChildProduct1"> -+ <requiredEntity createDataKey="createConfigProductAttribute"/> -+ <requiredEntity createDataKey="getConfigAttributeOption1"/> -+ </createData> -+ -+ <createData entity="ApiSimpleOne" stepKey="createConfigChildProduct2"> -+ <requiredEntity createDataKey="createConfigProductAttribute"/> -+ <requiredEntity createDataKey="getConfigAttributeOption2"/> -+ </createData> -+ -+ <createData entity="ConfigurableProductTwoOptions" stepKey="createConfigProductOption"> -+ <requiredEntity createDataKey="createConfigProduct"/> -+ <requiredEntity createDataKey="createConfigProductAttribute"/> -+ <requiredEntity createDataKey="getConfigAttributeOption1"/> -+ <requiredEntity createDataKey="getConfigAttributeOption2"/> -+ </createData> -+ -+ <createData entity="ConfigurableProductAddChild" stepKey="createConfigProductAddChild1"> -+ <requiredEntity createDataKey="createConfigProduct"/> -+ <requiredEntity createDataKey="createConfigChildProduct1"/> -+ </createData> -+ <createData entity="ConfigurableProductAddChild" stepKey="createConfigProductAddChild2"> -+ <requiredEntity createDataKey="createConfigProduct"/> -+ <requiredEntity createDataKey="createConfigChildProduct2"/> -+ </createData> -+ -+ <!-- Create customer --> -+ <createData entity="Simple_US_Customer" stepKey="createSimpleUsCustomer"> -+ <field key="group_id">1</field> -+ </createData> -+ </before> -+ -+ <after> -+ <deleteData createDataKey="createConfigProduct" stepKey="deleteConfigProduct"/> -+ <deleteData createDataKey="simplecategory" stepKey="deleteSimpleCategory"/> -+ <deleteData createDataKey="createSimpleUsCustomer" stepKey="deleteCustomer"/> -+ <deleteData createDataKey="createConfigChildProduct1" stepKey="deleteConfigChildProduct1"/> -+ <deleteData createDataKey="createConfigChildProduct2" stepKey="deleteConfigChildProduct2"/> -+ <deleteData createDataKey="createConfigProductAttribute" stepKey="deleteConfigProductAttribute"/> -+ </after> -+ -+ <!-- Login as a customer --> -+ <actionGroup ref="LoginToStorefrontActionGroup" stepKey="signUpNewUser"> -+ <argument name="Customer" value="$$createSimpleUsCustomer$$"/> -+ </actionGroup> -+ <amOnPage url="/{{ApiConfigurableProduct.urlKey}}2.html" stepKey="goToConfigProductPage"/> -+ <!-- Go to configurable product page --> -+ <waitForPageLoad stepKey="waitForProductPageLoad"/> -+ -+ <!-- Order product with single quantity --> -+ <selectOption userInput="$$createConfigProductAttributeOption1.option[store_labels][1][label]$$" selector="{{StorefrontProductInfoMainSection.optionByAttributeId($$createConfigProductAttribute.attribute_id$$)}}" stepKey="configProductFillOption" /> -+ <click stepKey="addSimpleProductToCart" selector="{{StorefrontProductActionSection.addToCart}}"/> -+ <waitForElementVisible selector="{{StorefrontCategoryMainSection.SuccessMsg}}" time="30" stepKey="waitForProductAdded"/> -+ <amOnPage url="{{CheckoutCartPage.url}}" stepKey="goToShoppingCartPage"/> -+ <actionGroup ref="GoToCheckoutFromMinicartActionGroup" stepKey="goToCheckoutFromMinicart"/> -+ <waitForElement selector="{{CheckoutShippingMethodsSection.next}}" time="30" stepKey="waitForNextButton"/> -+ <click selector="{{CheckoutShippingSection.next}}" stepKey="clickNext"/> -+ <!-- Checkout select Check/Money Order payment --> -+ <actionGroup ref="CheckoutSelectCheckMoneyOrderPaymentActionGroup" stepKey="selectCheckMoneyPayment"/> -+ <click selector="{{CheckoutPaymentSection.placeOrder}}" stepKey="clickPlaceOrder"/> -+ <waitForPageLoad stepKey="waitForOrderSuccessPage1"/> -+ <grabTextFrom selector="{{CheckoutSuccessMainSection.orderNumber22}}" stepKey="grabOrderNumber"/> -+ <actionGroup ref="StorefrontSignOutActionGroup" stepKey="StorefrontSignOutActionGroup"/> -+ <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin1"/> -+ -+ <amOnPage url="{{AdminOrdersPage.url}}" stepKey="onOrdersPage"/> -+ <waitForLoadingMaskToDisappear stepKey="waitForLoadingMask3"/> -+ <!-- Reset admin order filter --> -+ <actionGroup ref="AdminOrdersGridClearFiltersActionGroup" stepKey="clearOrderFilters"/> -+ <waitForLoadingMaskToDisappear stepKey="waitForLoadingMask5"/> -+ <fillField selector="{{AdminOrdersGridSection.search}}" userInput="{$grabOrderNumber}" stepKey="searchOrderNum"/> -+ <click selector="{{AdminOrdersGridSection.submitSearch}}" stepKey="submitSearch"/> -+ <waitForLoadingMaskToDisappear stepKey="waitForLoadingMask4"/> -+ -+ <click selector="{{AdminOrdersGridSection.firstRow}}" stepKey="clickOrderRow"/> -+ <waitForPageLoad stepKey="waitForOrderPageToLoad"/> -+ <click selector="{{AdminOrderDetailsMainActionsSection.invoice}}" stepKey="clickInvoice"/> -+ <waitForPageLoad stepKey="waitForNewInvoicePageToLoad"/> -+ <click selector="{{AdminInvoiceMainActionsSection.submitInvoice}}" stepKey="clickSubmitInvoice"/> -+ <waitForPageLoad stepKey="waitForNewInvoiceToBeCreated"/> -+ <see selector="{{AdminOrderDetailsMessagesSection.successMessage}}" userInput="The invoice has been created." stepKey="seeSuccessMessage"/> -+ <click selector="{{AdminOrderDetailsMainActionsSection.ship}}" stepKey="clickShip"/> -+ <waitForLoadingMaskToDisappear stepKey="waitForShipLoadingMask"/> -+ <click selector="{{AdminShipmentMainActionsSection.submitShipment}}" stepKey="submitShipment"/> -+ <waitForPageLoad stepKey="waitShipmentCreated"/> -+ <actionGroup ref="logout" stepKey="logout"/> -+ <magentoCLI stepKey="runCron" command="cron:run --group='index'"/> -+ -+ <!-- Wait till cron job runs for schedule updates --> -+ <wait time="60" stepKey="waitForUpdateStarts"/> -+ -+ <!-- Assert that product with single quantity is not available for order --> -+ <amOnPage url="/{{ApiConfigurableProduct.urlKey}}2.html" stepKey="goToConfigProductPage2"/> -+ <waitForPageLoad stepKey="waitForProductPageLoad2"/> -+ <dontSee userInput="$$createConfigProductAttributeOption1.option[store_labels][1][label]$$" selector="{{StorefrontProductInfoMainSection.optionByAttributeId($$createConfigProductAttribute.attribute_id$$)}}" stepKey="assertOptionNotAvailable" /> -+ </test> -+</tests> -diff --git a/app/code/Magento/CatalogInventory/Test/Unit/Helper/MinsaleqtyTest.php b/app/code/Magento/CatalogInventory/Test/Unit/Helper/MinsaleqtyTest.php -index f008ed7d9d6..051e002b80c 100644 ---- a/app/code/Magento/CatalogInventory/Test/Unit/Helper/MinsaleqtyTest.php -+++ b/app/code/Magento/CatalogInventory/Test/Unit/Helper/MinsaleqtyTest.php -@@ -216,7 +216,7 @@ class MinsaleqtyTest extends \PHPUnit\Framework\TestCase - public function makeStorableArrayFieldValueDataProvider() - { - return [ -- 'invalid bool' => [false, ''], -+ 'invalid bool' => [false, false], - 'invalid empty string' => ['', ''], - 'valid numeric' => ['22', '22'], - 'valid empty array' => [[], '[]', 1], -@@ -240,7 +240,8 @@ class MinsaleqtyTest extends \PHPUnit\Framework\TestCase - '[1]', - 1, - [0 => 1.0] -- ] -+ ], -+ 'json value' => ['{"32000":2,"0":1}', '{"32000":2,"0":1}'], - ]; - } - } -diff --git a/app/code/Magento/CatalogInventory/Test/Unit/Model/AddStockStatusToCollectionTest.php b/app/code/Magento/CatalogInventory/Test/Unit/Model/AddStockStatusToCollectionTest.php -index af35666ced3..906df547327 100644 ---- a/app/code/Magento/CatalogInventory/Test/Unit/Model/AddStockStatusToCollectionTest.php -+++ b/app/code/Magento/CatalogInventory/Test/Unit/Model/AddStockStatusToCollectionTest.php -@@ -6,6 +6,7 @@ - namespace Magento\CatalogInventory\Test\Unit\Model; - - use Magento\CatalogInventory\Model\AddStockStatusToCollection; -+use Magento\Framework\Search\EngineResolverInterface; - - class AddStockStatusToCollectionTest extends \PHPUnit\Framework\TestCase - { -@@ -19,13 +20,24 @@ class AddStockStatusToCollectionTest extends \PHPUnit\Framework\TestCase - */ - protected $stockHelper; - -+ /** -+ * @var EngineResolverInterface|\PHPUnit_Framework_MockObject_MockObject -+ */ -+ private $engineResolver; -+ - protected function setUp() - { - $this->stockHelper = $this->createMock(\Magento\CatalogInventory\Helper\Stock::class); -+ $this->engineResolver = $this->getMockBuilder(EngineResolverInterface::class) -+ ->disableOriginalConstructor() -+ ->setMethods(['getCurrentSearchEngine']) -+ ->getMockForAbstractClass(); -+ - $this->plugin = (new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this))->getObject( - \Magento\CatalogInventory\Model\AddStockStatusToCollection::class, - [ - 'stockHelper' => $this->stockHelper, -+ 'engineResolver' => $this->engineResolver - ] - ); - } -@@ -36,6 +48,10 @@ class AddStockStatusToCollectionTest extends \PHPUnit\Framework\TestCase - ->disableOriginalConstructor() - ->getMock(); - -+ $this->engineResolver->expects($this->any()) -+ ->method('getCurrentSearchEngine') -+ ->willReturn('mysql'); -+ - $this->stockHelper->expects($this->once()) - ->method('addIsInStockFilterToCollection') - ->with($productCollection) -diff --git a/app/code/Magento/CatalogInventory/Test/Unit/Model/ConfigurationTest.php b/app/code/Magento/CatalogInventory/Test/Unit/Model/ConfigurationTest.php -index d2779b79b30..cefc4ada7d2 100644 ---- a/app/code/Magento/CatalogInventory/Test/Unit/Model/ConfigurationTest.php -+++ b/app/code/Magento/CatalogInventory/Test/Unit/Model/ConfigurationTest.php -@@ -144,7 +144,7 @@ class ConfigurationTest extends \PHPUnit\Framework\TestCase - $store = 1; - - $this->scopeConfigMock->expects($this->once()) -- ->method('getValue') -+ ->method('isSetFlag') - ->with( - Configuration::XML_PATH_ENABLE_QTY_INCREMENTS, - \Magento\Store\Model\ScopeInterface::SCOPE_STORE, -diff --git a/app/code/Magento/CatalogInventory/Test/Unit/Model/Indexer/Stock/CacheCleanerTest.php b/app/code/Magento/CatalogInventory/Test/Unit/Model/Indexer/Stock/CacheCleanerTest.php -index 5e4249685f8..755e54a919b 100644 ---- a/app/code/Magento/CatalogInventory/Test/Unit/Model/Indexer/Stock/CacheCleanerTest.php -+++ b/app/code/Magento/CatalogInventory/Test/Unit/Model/Indexer/Stock/CacheCleanerTest.php -@@ -12,6 +12,7 @@ use Magento\Framework\App\ResourceConnection; - use Magento\Framework\DB\Adapter\AdapterInterface; - use Magento\Framework\DB\Select; - use Magento\Framework\Event\ManagerInterface; -+use Magento\Framework\EntityManager\MetadataPool; - use Magento\Framework\Indexer\CacheContext; - use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; - use Magento\Catalog\Model\Product; -@@ -43,6 +44,11 @@ class CacheCleanerTest extends \PHPUnit\Framework\TestCase - */ - private $cacheContextMock; - -+ /** -+ * @var MetadataPool |\PHPUnit_Framework_MockObject_MockObject -+ */ -+ private $metadataPoolMock; -+ - /** - * @var StockConfigurationInterface|\PHPUnit_Framework_MockObject_MockObject - */ -@@ -61,6 +67,8 @@ class CacheCleanerTest extends \PHPUnit\Framework\TestCase - ->setMethods(['getStockThresholdQty'])->getMockForAbstractClass(); - $this->cacheContextMock = $this->getMockBuilder(CacheContext::class)->disableOriginalConstructor()->getMock(); - $this->eventManagerMock = $this->getMockBuilder(ManagerInterface::class)->getMock(); -+ $this->metadataPoolMock = $this->getMockBuilder(MetadataPool::class) -+ ->setMethods(['getMetadata', 'getLinkField'])->disableOriginalConstructor()->getMock(); - $this->selectMock = $this->getMockBuilder(Select::class)->disableOriginalConstructor()->getMock(); - - $this->resourceMock->expects($this->any()) -@@ -73,7 +81,8 @@ class CacheCleanerTest extends \PHPUnit\Framework\TestCase - 'resource' => $this->resourceMock, - 'stockConfiguration' => $this->stockConfigurationMock, - 'cacheContext' => $this->cacheContextMock, -- 'eventManager' => $this->eventManagerMock -+ 'eventManager' => $this->eventManagerMock, -+ 'metadataPool' => $this->metadataPoolMock - ] - ); - } -@@ -90,6 +99,7 @@ class CacheCleanerTest extends \PHPUnit\Framework\TestCase - $productId = 123; - $this->selectMock->expects($this->any())->method('from')->willReturnSelf(); - $this->selectMock->expects($this->any())->method('where')->willReturnSelf(); -+ $this->selectMock->expects($this->any())->method('joinLeft')->willReturnSelf(); - $this->connectionMock->expects($this->exactly(2))->method('select')->willReturn($this->selectMock); - $this->connectionMock->expects($this->exactly(2))->method('fetchAll')->willReturnOnConsecutiveCalls( - [ -@@ -105,7 +115,10 @@ class CacheCleanerTest extends \PHPUnit\Framework\TestCase - ->with(Product::CACHE_TAG, [$productId]); - $this->eventManagerMock->expects($this->once())->method('dispatch') - ->with('clean_cache_by_tags', ['object' => $this->cacheContextMock]); -- -+ $this->metadataPoolMock->expects($this->exactly(2))->method('getMetadata') -+ ->willReturnSelf(); -+ $this->metadataPoolMock->expects($this->exactly(2))->method('getLinkField') -+ ->willReturn('row_id'); - $callback = function () { - }; - $this->unit->clean([], $callback); -@@ -136,6 +149,7 @@ class CacheCleanerTest extends \PHPUnit\Framework\TestCase - $productId = 123; - $this->selectMock->expects($this->any())->method('from')->willReturnSelf(); - $this->selectMock->expects($this->any())->method('where')->willReturnSelf(); -+ $this->selectMock->expects($this->any())->method('joinLeft')->willReturnSelf(); - $this->connectionMock->expects($this->exactly(2))->method('select')->willReturn($this->selectMock); - $this->connectionMock->expects($this->exactly(2))->method('fetchAll')->willReturnOnConsecutiveCalls( - [ -@@ -149,6 +163,10 @@ class CacheCleanerTest extends \PHPUnit\Framework\TestCase - ->willReturn($stockThresholdQty); - $this->cacheContextMock->expects($this->never())->method('registerEntities'); - $this->eventManagerMock->expects($this->never())->method('dispatch'); -+ $this->metadataPoolMock->expects($this->exactly(2))->method('getMetadata') -+ ->willReturnSelf(); -+ $this->metadataPoolMock->expects($this->exactly(2))->method('getLinkField') -+ ->willReturn('row_id'); - - $callback = function () { - }; -diff --git a/app/code/Magento/CatalogInventory/Test/Unit/Model/Plugin/LayerTest.php b/app/code/Magento/CatalogInventory/Test/Unit/Model/Plugin/LayerTest.php -index 287459bd8cb..b64563a3517 100644 ---- a/app/code/Magento/CatalogInventory/Test/Unit/Model/Plugin/LayerTest.php -+++ b/app/code/Magento/CatalogInventory/Test/Unit/Model/Plugin/LayerTest.php -@@ -5,6 +5,8 @@ - */ - namespace Magento\CatalogInventory\Test\Unit\Model\Plugin; - -+use Magento\Framework\Search\EngineResolverInterface; -+ - class LayerTest extends \PHPUnit\Framework\TestCase - { - /** -@@ -22,14 +24,24 @@ class LayerTest extends \PHPUnit\Framework\TestCase - */ - protected $_stockHelperMock; - -+ /** -+ * @var EngineResolverInterface|\PHPUnit_Framework_MockObject_MockObject -+ */ -+ private $engineResolver; -+ - protected function setUp() - { - $this->_scopeConfigMock = $this->createMock(\Magento\Framework\App\Config\ScopeConfigInterface::class); - $this->_stockHelperMock = $this->createMock(\Magento\CatalogInventory\Helper\Stock::class); -+ $this->engineResolver = $this->getMockBuilder(EngineResolverInterface::class) -+ ->disableOriginalConstructor() -+ ->setMethods(['getCurrentSearchEngine']) -+ ->getMockForAbstractClass(); - - $this->_model = new \Magento\CatalogInventory\Model\Plugin\Layer( - $this->_stockHelperMock, -- $this->_scopeConfigMock -+ $this->_scopeConfigMock, -+ $this->engineResolver - ); - } - -@@ -38,6 +50,10 @@ class LayerTest extends \PHPUnit\Framework\TestCase - */ - public function testAddStockStatusDisabledShow() - { -+ $this->engineResolver->expects($this->any()) -+ ->method('getCurrentSearchEngine') -+ ->willReturn('mysql'); -+ - $this->_scopeConfigMock->expects( - $this->once() - )->method( -@@ -60,6 +76,10 @@ class LayerTest extends \PHPUnit\Framework\TestCase - */ - public function testAddStockStatusEnabledShow() - { -+ $this->engineResolver->expects($this->any()) -+ ->method('getCurrentSearchEngine') -+ ->willReturn('mysql'); -+ - $this->_scopeConfigMock->expects( - $this->once() - )->method( -diff --git a/app/code/Magento/CatalogInventory/Test/Unit/Model/Quote/Item/QuantityValidator/Initializer/OptionTest.php b/app/code/Magento/CatalogInventory/Test/Unit/Model/Quote/Item/QuantityValidator/Initializer/OptionTest.php -index eb32c30ab4f..87233b4048b 100644 ---- a/app/code/Magento/CatalogInventory/Test/Unit/Model/Quote/Item/QuantityValidator/Initializer/OptionTest.php -+++ b/app/code/Magento/CatalogInventory/Test/Unit/Model/Quote/Item/QuantityValidator/Initializer/OptionTest.php -@@ -151,7 +151,6 @@ class OptionTest extends \PHPUnit\Framework\TestCase - $this->optionMock->expects($this->any())->method('getProduct')->will($this->returnValue($this->productMock)); - - $this->stockItemMock->expects($this->once())->method('setIsChildItem')->with(true); -- $this->stockItemMock->expects($this->once())->method('setSuppressCheckQtyIncrements')->with(true); - $this->stockItemMock->expects($this->once())->method('getItemId')->will($this->returnValue(true)); - - $this->stockRegistry -@@ -222,7 +221,6 @@ class OptionTest extends \PHPUnit\Framework\TestCase - $this->optionMock->expects($this->any())->method('getProduct')->will($this->returnValue($this->productMock)); - - $this->stockItemMock->expects($this->once())->method('setIsChildItem')->with(true); -- $this->stockItemMock->expects($this->once())->method('setSuppressCheckQtyIncrements')->with(true); - $this->stockItemMock->expects($this->once())->method('getItemId')->will($this->returnValue(true)); - - $this->stockRegistry -diff --git a/app/code/Magento/CatalogInventory/Test/Unit/Model/Quote/Item/QuantityValidator/Initializer/QuantityValidatorTest.php b/app/code/Magento/CatalogInventory/Test/Unit/Model/Quote/Item/QuantityValidator/Initializer/QuantityValidatorTest.php -index 7e2bad0b963..633505a5609 100644 ---- a/app/code/Magento/CatalogInventory/Test/Unit/Model/Quote/Item/QuantityValidator/Initializer/QuantityValidatorTest.php -+++ b/app/code/Magento/CatalogInventory/Test/Unit/Model/Quote/Item/QuantityValidator/Initializer/QuantityValidatorTest.php -@@ -278,11 +278,13 @@ class QuantityValidatorTest extends \PHPUnit\Framework\TestCase - { - $optionMock = $this->getMockBuilder(OptionItem::class) - ->disableOriginalConstructor() -- ->setMethods(['setHasError', 'getStockStateResult']) -+ ->setMethods(['setHasError', 'getStockStateResult', 'getProduct']) - ->getMock(); - $optionMock->expects($this->once()) - ->method('getStockStateResult') - ->willReturn($this->resultMock); -+ $optionMock->method('getProduct') -+ ->willReturn($this->productMock); - $this->stockRegistryMock->expects($this->at(0)) - ->method('getStockItem') - ->willReturn($this->stockItemMock); -@@ -319,7 +321,7 @@ class QuantityValidatorTest extends \PHPUnit\Framework\TestCase - { - $optionMock = $this->getMockBuilder(OptionItem::class) - ->disableOriginalConstructor() -- ->setMethods(['setHasError', 'getStockStateResult']) -+ ->setMethods(['setHasError', 'getStockStateResult', 'getProduct']) - ->getMock(); - $this->stockRegistryMock->expects($this->at(0)) - ->method('getStockItem') -@@ -330,6 +332,8 @@ class QuantityValidatorTest extends \PHPUnit\Framework\TestCase - $optionMock->expects($this->once()) - ->method('getStockStateResult') - ->willReturn($this->resultMock); -+ $optionMock->method('getProduct') -+ ->willReturn($this->productMock); - $options = [$optionMock]; - $this->createInitialStub(1); - $this->setUpStubForQuantity(1, true); -@@ -360,7 +364,7 @@ class QuantityValidatorTest extends \PHPUnit\Framework\TestCase - { - $optionMock = $this->getMockBuilder(OptionItem::class) - ->disableOriginalConstructor() -- ->setMethods(['setHasError', 'getStockStateResult']) -+ ->setMethods(['setHasError', 'getStockStateResult', 'getProduct']) - ->getMock(); - $quoteItem = $this->getMockBuilder(Item::class) - ->disableOriginalConstructor() -@@ -369,6 +373,8 @@ class QuantityValidatorTest extends \PHPUnit\Framework\TestCase - $optionMock->expects($this->once()) - ->method('getStockStateResult') - ->willReturn($this->resultMock); -+ $optionMock->method('getProduct') -+ ->willReturn($this->productMock); - $this->stockRegistryMock->expects($this->at(0)) - ->method('getStockItem') - ->willReturn($this->stockItemMock); -diff --git a/app/code/Magento/CatalogInventory/Test/Unit/Model/Quote/Item/QuantityValidator/Initializer/StockItemTest.php b/app/code/Magento/CatalogInventory/Test/Unit/Model/Quote/Item/QuantityValidator/Initializer/StockItemTest.php -index 8c9a1aa7715..01dab7fce33 100644 ---- a/app/code/Magento/CatalogInventory/Test/Unit/Model/Quote/Item/QuantityValidator/Initializer/StockItemTest.php -+++ b/app/code/Magento/CatalogInventory/Test/Unit/Model/Quote/Item/QuantityValidator/Initializer/StockItemTest.php -@@ -8,6 +8,7 @@ namespace Magento\CatalogInventory\Test\Unit\Model\Quote\Item\QuantityValidator\ - use Magento\CatalogInventory\Model\Quote\Item\QuantityValidator\QuoteItemQtyList; - - /** -+ * Class StockItemTest - * @SuppressWarnings(PHPMD.CouplingBetweenObjects) - */ - class StockItemTest extends \PHPUnit\Framework\TestCase -@@ -28,10 +29,18 @@ class StockItemTest extends \PHPUnit\Framework\TestCase - protected $typeConfig; - - /** -- * @var \PHPUnit_Framework_MockObject_MockObject -+ * @var \Magento\CatalogInventory\Api\StockStateInterface\PHPUnit_Framework_MockObject_MockObject - */ - protected $stockStateMock; - -+ /** -+ * @var \Magento\CatalogInventory\Model\StockStateProviderInterface| \PHPUnit_Framework_MockObject_MockObject -+ */ -+ private $stockStateProviderMock; -+ -+ /** -+ * @inheritdoc -+ */ - protected function setUp() - { - $this->quoteItemQtyList = $this -@@ -48,17 +57,25 @@ class StockItemTest extends \PHPUnit\Framework\TestCase - $this->stockStateMock = $this->getMockBuilder(\Magento\CatalogInventory\Api\StockStateInterface::class) - ->disableOriginalConstructor() - ->getMock(); -+ -+ $this->stockStateProviderMock = $this -+ ->getMockBuilder(\Magento\CatalogInventory\Model\StockStateProvider::class) -+ ->disableOriginalConstructor() -+ ->getMock(); -+ - $this->model = $objectManagerHelper->getObject( - \Magento\CatalogInventory\Model\Quote\Item\QuantityValidator\Initializer\StockItem::class, - [ - 'quoteItemQtyList' => $this->quoteItemQtyList, - 'typeConfig' => $this->typeConfig, -- 'stockState' => $this->stockStateMock -+ 'stockState' => $this->stockStateMock, -+ 'stockStateProvider' => $this->stockStateProviderMock - ] - ); - } - - /** -+ * Test initialize with Subitem - * @SuppressWarnings(PHPMD.ExcessiveMethodLength) - */ - public function testInitializeWithSubitem() -@@ -141,6 +158,10 @@ class StockItemTest extends \PHPUnit\Framework\TestCase - ->method('checkQuoteItemQty') - ->withAnyParameters() - ->will($this->returnValue($result)); -+ $this->stockStateProviderMock->expects($this->once()) -+ ->method('checkQuoteItemQty') -+ ->withAnyParameters() -+ ->will($this->returnValue($result)); - $product->expects($this->once()) - ->method('getCustomOption') - ->with('product_type') -@@ -177,13 +198,16 @@ class StockItemTest extends \PHPUnit\Framework\TestCase - $quoteItem->expects($this->once())->method('setUseOldQty')->with('item')->will($this->returnSelf()); - $result->expects($this->exactly(2))->method('getMessage')->will($this->returnValue('message')); - $quoteItem->expects($this->once())->method('setMessage')->with('message')->will($this->returnSelf()); -- $result->expects($this->exactly(2))->method('getItemBackorders')->will($this->returnValue('backorders')); -+ $result->expects($this->exactly(3))->method('getItemBackorders')->will($this->returnValue('backorders')); - $quoteItem->expects($this->once())->method('setBackorders')->with('backorders')->will($this->returnSelf()); - $quoteItem->expects($this->once())->method('setStockStateResult')->with($result)->will($this->returnSelf()); - - $this->model->initialize($stockItem, $quoteItem, $qty); - } - -+ /** -+ * Test initialize without Subitem -+ */ - public function testInitializeWithoutSubitem() - { - $qty = 3; -@@ -234,6 +258,10 @@ class StockItemTest extends \PHPUnit\Framework\TestCase - ->with($productId, 'quote_item_id', 'quote_id', $qty) - ->will($this->returnValue('summary_qty')); - $this->stockStateMock->expects($this->once()) -+ ->method('checkQuoteItemQty') -+ ->withAnyParameters() -+ ->will($this->returnValue($result)); -+ $this->stockStateProviderMock->expects($this->once()) - ->method('checkQuoteItemQty') - ->withAnyParameters() - ->will($this->returnValue($result)); -@@ -256,7 +284,7 @@ class StockItemTest extends \PHPUnit\Framework\TestCase - $result->expects($this->once())->method('getHasQtyOptionUpdate')->will($this->returnValue(false)); - $result->expects($this->once())->method('getItemUseOldQty')->will($this->returnValue(null)); - $result->expects($this->once())->method('getMessage')->will($this->returnValue(null)); -- $result->expects($this->once())->method('getItemBackorders')->will($this->returnValue(null)); -+ $result->expects($this->exactly(2))->method('getItemBackorders')->will($this->returnValue(null)); - - $this->model->initialize($stockItem, $quoteItem, $qty); - } -diff --git a/app/code/Magento/CatalogInventory/Ui/DataProvider/Product/AddQuantityAndStockStatusFieldToCollection.php b/app/code/Magento/CatalogInventory/Ui/DataProvider/Product/AddQuantityAndStockStatusFieldToCollection.php -new file mode 100644 -index 00000000000..d66a783c672 ---- /dev/null -+++ b/app/code/Magento/CatalogInventory/Ui/DataProvider/Product/AddQuantityAndStockStatusFieldToCollection.php -@@ -0,0 +1,32 @@ -+<?php -+/** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+declare(strict_types=1); -+ -+namespace Magento\CatalogInventory\Ui\DataProvider\Product; -+ -+use Magento\Framework\Data\Collection; -+use Magento\Ui\DataProvider\AddFieldToCollectionInterface; -+ -+/** -+ * Add quantity_and_stock_status field to collection -+ */ -+class AddQuantityAndStockStatusFieldToCollection implements AddFieldToCollectionInterface -+{ -+ /** -+ * @inheritdoc -+ */ -+ public function addField(Collection $collection, $field, $alias = null) -+ { -+ $collection->joinField( -+ 'quantity_and_stock_status', -+ 'cataloginventory_stock_item', -+ 'is_in_stock', -+ 'product_id=entity_id', -+ '{{table}}.stock_id=1', -+ 'left' -+ ); -+ } -+} -diff --git a/app/code/Magento/CatalogInventory/composer.json b/app/code/Magento/CatalogInventory/composer.json -index 8b55b6f3279..eb6239ea87e 100644 ---- a/app/code/Magento/CatalogInventory/composer.json -+++ b/app/code/Magento/CatalogInventory/composer.json -@@ -8,6 +8,7 @@ - "php": "~7.1.3||~7.2.0", - "magento/framework": "*", - "magento/module-catalog": "*", -+ "magento/module-search": "*", - "magento/module-config": "*", - "magento/module-customer": "*", - "magento/module-eav": "*", -@@ -27,5 +28,6 @@ - "psr-4": { - "Magento\\CatalogInventory\\": "" - } -- } -+ }, -+ "abandoned": "magento/inventory-composer-metapackage" - } -diff --git a/app/code/Magento/CatalogInventory/etc/adminhtml/di.xml b/app/code/Magento/CatalogInventory/etc/adminhtml/di.xml -index 3397ef25918..28035de29bc 100644 ---- a/app/code/Magento/CatalogInventory/etc/adminhtml/di.xml -+++ b/app/code/Magento/CatalogInventory/etc/adminhtml/di.xml -@@ -23,6 +23,7 @@ - <arguments> - <argument name="addFieldStrategies" xsi:type="array"> - <item name="qty" xsi:type="object">Magento\CatalogInventory\Ui\DataProvider\Product\AddQuantityFieldToCollection</item> -+ <item name="quantity_and_stock_status" xsi:type="object">Magento\CatalogInventory\Ui\DataProvider\Product\AddQuantityAndStockStatusFieldToCollection</item> - </argument> - <argument name="addFilterStrategies" xsi:type="array"> - <item name="qty" xsi:type="object">Magento\CatalogInventory\Ui\DataProvider\Product\AddQuantityFilterToCollection</item> -diff --git a/app/code/Magento/CatalogInventory/etc/adminhtml/system.xml b/app/code/Magento/CatalogInventory/etc/adminhtml/system.xml -index b9332575c96..08ed0a8f494 100644 ---- a/app/code/Magento/CatalogInventory/etc/adminhtml/system.xml -+++ b/app/code/Magento/CatalogInventory/etc/adminhtml/system.xml -@@ -41,13 +41,13 @@ - <![CDATA[Please note that these settings apply to individual items in the cart, not to the entire cart.]]> - </comment> - <label>Product Stock Options</label> -- <field id="manage_stock" translate="label" type="select" sortOrder="1" showInDefault="1" showInWebsite="0" showInStore="0" canRestore="1"> -+ <field id="manage_stock" translate="label comment" type="select" sortOrder="1" showInDefault="1" showInWebsite="0" showInStore="0" canRestore="1"> - <label>Manage Stock</label> - <source_model>Magento\Config\Model\Config\Source\Yesno</source_model> - <backend_model>Magento\CatalogInventory\Model\Config\Backend\Managestock</backend_model> - <comment>Changing can take some time due to processing whole catalog.</comment> - </field> -- <field id="backorders" translate="label" type="select" sortOrder="3" showInDefault="1" showInWebsite="0" showInStore="0" canRestore="1"> -+ <field id="backorders" translate="label comment" type="select" sortOrder="3" showInDefault="1" showInWebsite="0" showInStore="0" canRestore="1"> - <label>Backorders</label> - <source_model>Magento\CatalogInventory\Model\Source\Backorders</source_model> - <backend_model>Magento\CatalogInventory\Model\Config\Backend\Backorders</backend_model> -diff --git a/app/code/Magento/CatalogInventory/etc/db_schema.xml b/app/code/Magento/CatalogInventory/etc/db_schema.xml -index 8a6ae8d2d93..5ac7fedc5aa 100644 ---- a/app/code/Magento/CatalogInventory/etc/db_schema.xml -+++ b/app/code/Magento/CatalogInventory/etc/db_schema.xml -@@ -13,10 +13,10 @@ - <column xsi:type="smallint" name="website_id" padding="5" unsigned="true" nullable="false" identity="false" - comment="Website Id"/> - <column xsi:type="varchar" name="stock_name" nullable="true" length="255" comment="Stock Name"/> -- <constraint xsi:type="primary" name="PRIMARY"> -+ <constraint xsi:type="primary" referenceId="PRIMARY"> - <column name="stock_id"/> - </constraint> -- <index name="CATALOGINVENTORY_STOCK_WEBSITE_ID" indexType="btree"> -+ <index referenceId="CATALOGINVENTORY_STOCK_WEBSITE_ID" indexType="btree"> - <column name="website_id"/> - </index> - </table> -@@ -71,23 +71,23 @@ - identity="false" default="0" comment="Is Divided into Multiple Boxes for Shipping"/> - <column xsi:type="smallint" name="website_id" padding="5" unsigned="true" nullable="false" identity="false" - default="0" comment="Website ID"/> -- <constraint xsi:type="primary" name="PRIMARY"> -+ <constraint xsi:type="primary" referenceId="PRIMARY"> - <column name="item_id"/> - </constraint> -- <constraint xsi:type="foreign" name="CATINV_STOCK_ITEM_PRD_ID_CAT_PRD_ENTT_ENTT_ID" -+ <constraint xsi:type="foreign" referenceId="CATINV_STOCK_ITEM_PRD_ID_CAT_PRD_ENTT_ENTT_ID" - table="cataloginventory_stock_item" column="product_id" referenceTable="catalog_product_entity" - referenceColumn="entity_id" onDelete="CASCADE"/> -- <constraint xsi:type="foreign" name="CATINV_STOCK_ITEM_STOCK_ID_CATINV_STOCK_STOCK_ID" -+ <constraint xsi:type="foreign" referenceId="CATINV_STOCK_ITEM_STOCK_ID_CATINV_STOCK_STOCK_ID" - table="cataloginventory_stock_item" column="stock_id" referenceTable="cataloginventory_stock" - referenceColumn="stock_id" onDelete="CASCADE"/> -- <constraint xsi:type="unique" name="CATALOGINVENTORY_STOCK_ITEM_PRODUCT_ID_STOCK_ID"> -+ <constraint xsi:type="unique" referenceId="CATALOGINVENTORY_STOCK_ITEM_PRODUCT_ID_STOCK_ID"> - <column name="product_id"/> - <column name="stock_id"/> - </constraint> -- <index name="CATALOGINVENTORY_STOCK_ITEM_WEBSITE_ID" indexType="btree"> -+ <index referenceId="CATALOGINVENTORY_STOCK_ITEM_WEBSITE_ID" indexType="btree"> - <column name="website_id"/> - </index> -- <index name="CATALOGINVENTORY_STOCK_ITEM_STOCK_ID" indexType="btree"> -+ <index referenceId="CATALOGINVENTORY_STOCK_ITEM_STOCK_ID" indexType="btree"> - <column name="stock_id"/> - </index> - </table> -@@ -103,18 +103,18 @@ - comment="Qty"/> - <column xsi:type="smallint" name="stock_status" padding="5" unsigned="true" nullable="false" identity="false" - comment="Stock Status"/> -- <constraint xsi:type="primary" name="PRIMARY"> -+ <constraint xsi:type="primary" referenceId="PRIMARY"> - <column name="product_id"/> - <column name="website_id"/> - <column name="stock_id"/> - </constraint> -- <index name="CATALOGINVENTORY_STOCK_STATUS_STOCK_ID" indexType="btree"> -+ <index referenceId="CATALOGINVENTORY_STOCK_STATUS_STOCK_ID" indexType="btree"> - <column name="stock_id"/> - </index> -- <index name="CATALOGINVENTORY_STOCK_STATUS_WEBSITE_ID" indexType="btree"> -+ <index referenceId="CATALOGINVENTORY_STOCK_STATUS_WEBSITE_ID" indexType="btree"> - <column name="website_id"/> - </index> -- <index name="CATALOGINVENTORY_STOCK_STATUS_STOCK_STATUS" indexType="btree"> -+ <index referenceId="CATALOGINVENTORY_STOCK_STATUS_STOCK_STATUS" indexType="btree"> - <column name="stock_status"/> - </index> - </table> -@@ -130,15 +130,15 @@ - comment="Qty"/> - <column xsi:type="smallint" name="stock_status" padding="5" unsigned="true" nullable="false" identity="false" - comment="Stock Status"/> -- <constraint xsi:type="primary" name="PRIMARY"> -+ <constraint xsi:type="primary" referenceId="PRIMARY"> - <column name="product_id"/> - <column name="website_id"/> - <column name="stock_id"/> - </constraint> -- <index name="CATALOGINVENTORY_STOCK_STATUS_IDX_STOCK_ID" indexType="btree"> -+ <index referenceId="CATALOGINVENTORY_STOCK_STATUS_IDX_STOCK_ID" indexType="btree"> - <column name="stock_id"/> - </index> -- <index name="CATALOGINVENTORY_STOCK_STATUS_IDX_WEBSITE_ID" indexType="btree"> -+ <index referenceId="CATALOGINVENTORY_STOCK_STATUS_IDX_WEBSITE_ID" indexType="btree"> - <column name="website_id"/> - </index> - </table> -@@ -154,15 +154,15 @@ - comment="Qty"/> - <column xsi:type="smallint" name="stock_status" padding="5" unsigned="true" nullable="false" identity="false" - comment="Stock Status"/> -- <constraint xsi:type="primary" name="PRIMARY"> -+ <constraint xsi:type="primary" referenceId="PRIMARY"> - <column name="product_id"/> - <column name="website_id"/> - <column name="stock_id"/> - </constraint> -- <index name="CATALOGINVENTORY_STOCK_STATUS_TMP_STOCK_ID" indexType="hash"> -+ <index referenceId="CATALOGINVENTORY_STOCK_STATUS_TMP_STOCK_ID" indexType="hash"> - <column name="stock_id"/> - </index> -- <index name="CATALOGINVENTORY_STOCK_STATUS_TMP_WEBSITE_ID" indexType="hash"> -+ <index referenceId="CATALOGINVENTORY_STOCK_STATUS_TMP_WEBSITE_ID" indexType="hash"> - <column name="website_id"/> - </index> - </table> -@@ -178,18 +178,18 @@ - comment="Qty"/> - <column xsi:type="smallint" name="stock_status" padding="5" unsigned="true" nullable="false" identity="false" - comment="Stock Status"/> -- <constraint xsi:type="primary" name="PRIMARY"> -+ <constraint xsi:type="primary" referenceId="PRIMARY"> - <column name="product_id"/> - <column name="website_id"/> - <column name="stock_id"/> - </constraint> -- <index name="CATALOGINVENTORY_STOCK_STATUS_STOCK_ID" indexType="btree"> -+ <index referenceId="CATALOGINVENTORY_STOCK_STATUS_STOCK_ID" indexType="btree"> - <column name="stock_id"/> - </index> -- <index name="CATALOGINVENTORY_STOCK_STATUS_WEBSITE_ID" indexType="btree"> -+ <index referenceId="CATALOGINVENTORY_STOCK_STATUS_WEBSITE_ID" indexType="btree"> - <column name="website_id"/> - </index> -- <index name="CATALOGINVENTORY_STOCK_STATUS_STOCK_STATUS" indexType="btree"> -+ <index referenceId="CATALOGINVENTORY_STOCK_STATUS_STOCK_STATUS" indexType="btree"> - <column name="stock_status"/> - </index> - </table> -diff --git a/app/code/Magento/CatalogInventory/etc/di.xml b/app/code/Magento/CatalogInventory/etc/di.xml -index 912ab48d288..e7d79c593b8 100644 ---- a/app/code/Magento/CatalogInventory/etc/di.xml -+++ b/app/code/Magento/CatalogInventory/etc/di.xml -@@ -35,7 +35,7 @@ - <type name="Magento\Catalog\Model\Product\Attribute\Repository"> - <plugin name="filterCustomAttribute" type="Magento\CatalogInventory\Model\Plugin\FilterCustomAttribute" /> - </type> -- <type name="Magento\CatalogInventory\Model\Plugin\FilterCustomAttribute"> -+ <type name="Magento\Catalog\Model\FilterProductCustomAttribute"> - <arguments> - <argument name="blackList" xsi:type="array"> - <item name="quantity_and_stock_status" xsi:type="string">quantity_and_stock_status</item> -@@ -44,7 +44,7 @@ - </type> - <type name="Magento\CatalogInventory\Observer\UpdateItemsStockUponConfigChangeObserver"> - <arguments> -- <argument name="resourceStock" xsi:type="object">Magento\CatalogInventory\Model\ResourceModel\Stock\Proxy</argument> -+ <argument name="resourceStockItem" xsi:type="object">Magento\CatalogInventory\Model\ResourceModel\Stock\Item\Proxy</argument> - </arguments> - </type> - <type name="Magento\Catalog\Model\Layer"> -@@ -111,7 +111,7 @@ - <argument name="batchSizeManagement" xsi:type="object">Magento\CatalogInventory\Model\Indexer\Stock\BatchSizeManagement</argument> - </arguments> - </type> -- <type name="\Magento\Framework\Data\CollectionModifier"> -+ <type name="Magento\Framework\Data\CollectionModifier"> - <arguments> - <argument name="conditions" xsi:type="array"> - <item name="stockStatusCondition" xsi:type="object">Magento\CatalogInventory\Model\ProductCollectionStockCondition</item> -@@ -135,4 +135,7 @@ - <type name="Magento\CatalogInventory\Model\ResourceModel\Stock\Item"> - <plugin name="priceIndexUpdater" type="Magento\CatalogInventory\Model\Plugin\PriceIndexUpdater" /> - </type> -+ <type name="Magento\Catalog\Controller\Adminhtml\Product\Action\Attribute\Save"> -+ <plugin name="massAction" type="Magento\CatalogInventory\Plugin\MassUpdateProductAttribute" /> -+ </type> - </config> -diff --git a/app/code/Magento/CatalogInventory/view/adminhtml/ui_component/product_form.xml b/app/code/Magento/CatalogInventory/view/adminhtml/ui_component/product_form.xml -index f8571b6e332..0a7f0fdc32d 100644 ---- a/app/code/Magento/CatalogInventory/view/adminhtml/ui_component/product_form.xml -+++ b/app/code/Magento/CatalogInventory/view/adminhtml/ui_component/product_form.xml -@@ -288,7 +288,7 @@ - <settings> - <scopeLabel>[GLOBAL]</scopeLabel> - <validation> -- <rule name="validate-number" xsi:type="boolean">true</rule> -+ <rule name="validate-greater-than-zero" xsi:type="boolean">true</rule> - </validation> - <label translate="true">Maximum Qty Allowed in Shopping Cart</label> - <dataScope>max_sale_qty</dataScope> -diff --git a/app/code/Magento/CatalogInventory/view/frontend/templates/qtyincrements.phtml b/app/code/Magento/CatalogInventory/view/frontend/templates/qtyincrements.phtml -index 8e0dbf1278e..8b63d29be81 100644 ---- a/app/code/Magento/CatalogInventory/view/frontend/templates/qtyincrements.phtml -+++ b/app/code/Magento/CatalogInventory/view/frontend/templates/qtyincrements.phtml -@@ -4,14 +4,12 @@ - * See COPYING.txt for license details. - */ - --// @codingStandardsIgnoreFile -- - /** - * @var $block \Magento\CatalogInventory\Block\Qtyincrements - */ - ?> - <?php if ($block->getProductQtyIncrements()) : ?> - <div class="product pricing"> -- <?= /* @escapeNotVerified */ __('%1 is available to buy in increments of %2', $block->getProductName(), $block->getProductQtyIncrements()) ?> -+ <?= $block->escapeHtml(__('%1 is available to buy in increments of %2', $block->getProductName(), $block->getProductQtyIncrements())) ?> - </div> - <?php endif ?> -diff --git a/app/code/Magento/CatalogInventory/view/frontend/templates/stockqty/composite.phtml b/app/code/Magento/CatalogInventory/view/frontend/templates/stockqty/composite.phtml -index 481ed1297a8..de667d19fad 100644 ---- a/app/code/Magento/CatalogInventory/view/frontend/templates/stockqty/composite.phtml -+++ b/app/code/Magento/CatalogInventory/view/frontend/templates/stockqty/composite.phtml -@@ -4,30 +4,28 @@ - * See COPYING.txt for license details. - */ - --// @codingStandardsIgnoreFile -- - /** - * @var $block \Magento\CatalogInventory\Block\Stockqty\Composite - */ - ?> --<?php if ($block->isMsgVisible()): ?> -+<?php if ($block->isMsgVisible()) : ?> - <div class="availability only"> - <a href="#" -- data-mage-init='{"toggleAdvanced": {"selectorsToggleClass": "active", "baseToggleClass": "expanded", "toggleContainers": "#<?= /* @escapeNotVerified */ $block->getDetailsPlaceholderId() ?>"}}' -- id="<?= /* @escapeNotVerified */ $block->getPlaceholderId() ?>" -- title="<?= /* @escapeNotVerified */ __('Only %1 left', ($block->getStockQtyLeft())) ?>" -+ data-mage-init='{"toggleAdvanced": {"selectorsToggleClass": "active", "baseToggleClass": "expanded", "toggleContainers": "#<?= $block->escapeHtmlAttr($block->getDetailsPlaceholderId()) ?>"}}' -+ id="<?= $block->escapeHtmlAttr($block->getPlaceholderId()) ?>" -+ title="<?= $block->escapeHtmlAttr(__('Only %1 left', ($block->getStockQtyLeft()))) ?>" - class="action show"> -- <?= /* @escapeNotVerified */ __('Only %1 left', "<strong>{$block->getStockQtyLeft()}</strong>") ?> -+ <?= /* @noEscape */ __('Only %1 left', "<strong>{$block->escapeHtml($block->getStockQtyLeft())}</strong>") ?> - </a> - </div> -- <div class="availability only detailed" id="<?= /* @escapeNotVerified */ $block->getDetailsPlaceholderId() ?>"> -+ <div class="availability only detailed" id="<?= $block->escapeHtmlAttr($block->getDetailsPlaceholderId()) ?>"> - <div class="table-wrapper"> - <table class="data table"> -- <caption class="table-caption"><?= /* @escapeNotVerified */ __('Product availability') ?></caption> -+ <caption class="table-caption"><?= $block->escapeHtml(__('Product availability')) ?></caption> - <thead> - <tr> -- <th class="col item" scope="col"><?= /* @escapeNotVerified */ __('Product Name') ?></th> -- <th class="col qty" scope="col"><?= /* @escapeNotVerified */ __('Qty') ?></th> -+ <th class="col item" scope="col"><?= $block->escapeHtml(__('Product Name')) ?></th> -+ <th class="col qty" scope="col"><?= $block->escapeHtml(__('Qty')) ?></th> - </tr> - </thead> - <tbody> -@@ -35,8 +33,8 @@ - <?php $childProductStockQty = $block->getProductStockQty($childProduct); ?> - <?php if ($childProductStockQty > 0) : ?> - <tr> -- <td data-th="<?= $block->escapeHtml(__('Product Name')) ?>" class="col item"><?= /* @escapeNotVerified */ $childProduct->getName() ?></td> -- <td data-th="<?= $block->escapeHtml(__('Qty')) ?>" class="col qty"><?= /* @escapeNotVerified */ $childProductStockQty ?></td> -+ <td data-th="<?= $block->escapeHtmlAttr(__('Product Name')) ?>" class="col item"><?= $block->escapeHtml($childProduct->getName()) ?></td> -+ <td data-th="<?= $block->escapeHtmlAttr(__('Qty')) ?>" class="col qty"><?= $block->escapeHtml($childProductStockQty) ?></td> - </tr> - <?php endif ?> - <?php endforeach ?> -diff --git a/app/code/Magento/CatalogInventory/view/frontend/templates/stockqty/default.phtml b/app/code/Magento/CatalogInventory/view/frontend/templates/stockqty/default.phtml -index 43fb697de26..c32cb9dd6ec 100644 ---- a/app/code/Magento/CatalogInventory/view/frontend/templates/stockqty/default.phtml -+++ b/app/code/Magento/CatalogInventory/view/frontend/templates/stockqty/default.phtml -@@ -4,14 +4,12 @@ - * See COPYING.txt for license details. - */ - --// @codingStandardsIgnoreFile -- - /** - * @var $block \Magento\CatalogInventory\Block\Stockqty\DefaultStockqty - */ - ?> --<?php if ($block->isMsgVisible()): ?> -- <div class="availability only" title="<?= /* @escapeNotVerified */ __('Only %1 left', ($block->getStockQtyLeft())) ?>"> -- <?= /* @escapeNotVerified */ __('Only %1 left', "<strong>{$block->getStockQtyLeft()}</strong>") ?> -+<?php if ($block->isMsgVisible()) : ?> -+ <div class="availability only" title="<?= $block->escapeHtmlAttr(__('Only %1 left', ($block->getStockQtyLeft()))) ?>"> -+ <?= /* @noEscape */ __('Only %1 left', "<strong>{$block->escapeHtml($block->getStockQtyLeft())}</strong>") ?> - </div> - <?php endif ?> -diff --git a/app/code/Magento/CatalogInventoryGraphQl/Model/Resolver/OnlyXLeftInStockResolver.php b/app/code/Magento/CatalogInventoryGraphQl/Model/Resolver/OnlyXLeftInStockResolver.php -new file mode 100644 -index 00000000000..9e10f0b4485 ---- /dev/null -+++ b/app/code/Magento/CatalogInventoryGraphQl/Model/Resolver/OnlyXLeftInStockResolver.php -@@ -0,0 +1,100 @@ -+<?php -+/** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+declare(strict_types=1); -+ -+namespace Magento\CatalogInventoryGraphQl\Model\Resolver; -+ -+use Magento\Catalog\Api\Data\ProductInterface; -+use Magento\CatalogInventory\Api\StockRegistryInterface; -+use Magento\CatalogInventory\Model\Configuration; -+use Magento\Framework\App\Config\ScopeConfigInterface; -+use Magento\Framework\Exception\LocalizedException; -+use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; -+use Magento\Framework\GraphQl\Config\Element\Field; -+use Magento\Framework\GraphQl\Query\ResolverInterface; -+use Magento\Store\Model\ScopeInterface; -+ -+/** -+ * @inheritdoc -+ */ -+class OnlyXLeftInStockResolver implements ResolverInterface -+{ -+ /** -+ * @var ScopeConfigInterface -+ */ -+ private $scopeConfig; -+ -+ /** -+ * @var StockRegistryInterface -+ */ -+ private $stockRegistry; -+ -+ /** -+ * @param ScopeConfigInterface $scopeConfig -+ * @param StockRegistryInterface $stockRegistry -+ */ -+ public function __construct( -+ ScopeConfigInterface $scopeConfig, -+ StockRegistryInterface $stockRegistry -+ ) { -+ $this->scopeConfig = $scopeConfig; -+ $this->stockRegistry = $stockRegistry; -+ } -+ -+ /** -+ * @inheritdoc -+ */ -+ public function resolve(Field $field, $context, ResolveInfo $info, array $value = null, array $args = null) -+ { -+ if (!array_key_exists('model', $value) || !$value['model'] instanceof ProductInterface) { -+ throw new LocalizedException(__('"model" value should be specified')); -+ } -+ -+ /* @var $product ProductInterface */ -+ $product = $value['model']; -+ $onlyXLeftQty = $this->getOnlyXLeftQty($product); -+ -+ return $onlyXLeftQty; -+ } -+ -+ /** -+ * Get product qty left when "Catalog > Inventory > Stock Options > Only X left Threshold" is greater than 0 -+ * -+ * @param ProductInterface $product -+ * -+ * @return null|float -+ */ -+ private function getOnlyXLeftQty(ProductInterface $product): ?float -+ { -+ $thresholdQty = (float)$this->scopeConfig->getValue( -+ Configuration::XML_PATH_STOCK_THRESHOLD_QTY, -+ ScopeInterface::SCOPE_STORE -+ ); -+ if ($thresholdQty === 0) { -+ return null; -+ } -+ -+ $stockItem = $this->stockRegistry->getStockItem($product->getId()); -+ -+ $stockCurrentQty = $this->stockRegistry->getStockStatus( -+ $product->getId(), -+ $product->getStore()->getWebsiteId() -+ )->getQty(); -+ -+ $stockLeft = $stockCurrentQty - $stockItem->getMinQty(); -+ -+ $thresholdQty = (float)$this->scopeConfig->getValue( -+ Configuration::XML_PATH_STOCK_THRESHOLD_QTY, -+ ScopeInterface::SCOPE_STORE -+ ); -+ -+ if ($stockCurrentQty > 0 && $stockLeft <= $thresholdQty) { -+ return (float)$stockLeft; -+ } -+ -+ return null; -+ } -+} -diff --git a/app/code/Magento/CatalogInventoryGraphQl/Model/Resolver/StockStatusProvider.php b/app/code/Magento/CatalogInventoryGraphQl/Model/Resolver/StockStatusProvider.php -new file mode 100644 -index 00000000000..354e053efa9 ---- /dev/null -+++ b/app/code/Magento/CatalogInventoryGraphQl/Model/Resolver/StockStatusProvider.php -@@ -0,0 +1,53 @@ -+<?php -+/** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+declare(strict_types=1); -+ -+namespace Magento\CatalogInventoryGraphQl\Model\Resolver; -+ -+use Magento\Catalog\Api\Data\ProductInterface; -+use Magento\CatalogInventory\Api\Data\StockStatusInterface; -+use Magento\CatalogInventory\Api\StockStatusRepositoryInterface; -+use Magento\Framework\Exception\LocalizedException; -+use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; -+use Magento\Framework\GraphQl\Config\Element\Field; -+use Magento\Framework\GraphQl\Query\ResolverInterface; -+ -+/** -+ * @inheritdoc -+ */ -+class StockStatusProvider implements ResolverInterface -+{ -+ /** -+ * @var StockStatusRepositoryInterface -+ */ -+ private $stockStatusRepository; -+ -+ /** -+ * @param StockStatusRepositoryInterface $stockStatusRepository -+ */ -+ public function __construct(StockStatusRepositoryInterface $stockStatusRepository) -+ { -+ $this->stockStatusRepository = $stockStatusRepository; -+ } -+ -+ /** -+ * @inheritdoc -+ */ -+ public function resolve(Field $field, $context, ResolveInfo $info, array $value = null, array $args = null) -+ { -+ if (!array_key_exists('model', $value) || !$value['model'] instanceof ProductInterface) { -+ throw new LocalizedException(__('"model" value should be specified')); -+ } -+ -+ /* @var $product ProductInterface */ -+ $product = $value['model']; -+ -+ $stockStatus = $this->stockStatusRepository->get($product->getId()); -+ $productStockStatus = (int)$stockStatus->getStockStatus(); -+ -+ return $productStockStatus === StockStatusInterface::STATUS_IN_STOCK ? 'IN_STOCK' : 'OUT_OF_STOCK'; -+ } -+} -diff --git a/app/code/Magento/CatalogInventoryGraphQl/README.md b/app/code/Magento/CatalogInventoryGraphQl/README.md -new file mode 100644 -index 00000000000..ef4302e8da3 ---- /dev/null -+++ b/app/code/Magento/CatalogInventoryGraphQl/README.md -@@ -0,0 +1,4 @@ -+# CatalogInventoryGraphQl -+ -+**CatalogInventoryGraphQl** provides type information for the GraphQl module -+to generate inventory stock fields for product information endpoints. -diff --git a/app/code/Magento/CatalogInventoryGraphQl/composer.json b/app/code/Magento/CatalogInventoryGraphQl/composer.json -new file mode 100644 -index 00000000000..5e85c3ae12f ---- /dev/null -+++ b/app/code/Magento/CatalogInventoryGraphQl/composer.json -@@ -0,0 +1,24 @@ -+{ -+ "name": "magento/module-catalog-inventory-graph-ql", -+ "description": "N/A", -+ "type": "magento2-module", -+ "require": { -+ "php": "~7.1.3||~7.2.0", -+ "magento/framework": "*", -+ "magento/module-store": "*", -+ "magento/module-catalog": "*", -+ "magento/module-catalog-inventory": "*" -+ }, -+ "license": [ -+ "OSL-3.0", -+ "AFL-3.0" -+ ], -+ "autoload": { -+ "files": [ -+ "registration.php" -+ ], -+ "psr-4": { -+ "Magento\\CatalogInventoryGraphQl\\": "" -+ } -+ } -+} -diff --git a/app/code/Magento/CatalogInventoryGraphQl/etc/module.xml b/app/code/Magento/CatalogInventoryGraphQl/etc/module.xml -new file mode 100644 -index 00000000000..776e4e16533 ---- /dev/null -+++ b/app/code/Magento/CatalogInventoryGraphQl/etc/module.xml -@@ -0,0 +1,16 @@ -+<?xml version="1.0"?> -+<!-- -+/** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:Module/etc/module.xsd"> -+ <module name="Magento_CatalogInventoryGraphQl"> -+ <sequence> -+ <module name="Magento_Store"/> -+ <module name="Magento_Catalog"/> -+ <module name="Magento_CatalogInventory"/> -+ </sequence> -+ </module> -+</config> -diff --git a/app/code/Magento/CatalogInventoryGraphQl/etc/schema.graphqls b/app/code/Magento/CatalogInventoryGraphQl/etc/schema.graphqls -new file mode 100644 -index 00000000000..6a6c7e75dbd ---- /dev/null -+++ b/app/code/Magento/CatalogInventoryGraphQl/etc/schema.graphqls -@@ -0,0 +1,12 @@ -+# Copyright © Magento, Inc. All rights reserved. -+# See COPYING.txt for license details. -+ -+interface ProductInterface { -+ only_x_left_in_stock: Float @doc(description: "Product stock only x left count") @resolver(class: "Magento\\CatalogInventoryGraphQl\\Model\\Resolver\\OnlyXLeftInStockResolver") -+ stock_status: ProductStockStatus @doc(description: "Stock status of the product") @resolver(class: "Magento\\CatalogInventoryGraphQl\\Model\\Resolver\\StockStatusProvider") -+} -+ -+enum ProductStockStatus @doc(description: "This enumeration states whether a product stock status is in stock or out of stock") { -+ IN_STOCK -+ OUT_OF_STOCK -+} -diff --git a/app/code/Magento/CatalogInventoryGraphQl/registration.php b/app/code/Magento/CatalogInventoryGraphQl/registration.php -new file mode 100644 -index 00000000000..6ef9d3b1f90 ---- /dev/null -+++ b/app/code/Magento/CatalogInventoryGraphQl/registration.php -@@ -0,0 +1,10 @@ -+<?php -+/** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+declare(strict_types=1); -+ -+use Magento\Framework\Component\ComponentRegistrar; -+ -+ComponentRegistrar::register(ComponentRegistrar::MODULE, 'Magento_CatalogInventoryGraphQl', __DIR__); -diff --git a/app/code/Magento/CatalogRule/Block/Adminhtml/Edit/DeleteButton.php b/app/code/Magento/CatalogRule/Block/Adminhtml/Edit/DeleteButton.php -index 6390822b58f..184cb641929 100644 ---- a/app/code/Magento/CatalogRule/Block/Adminhtml/Edit/DeleteButton.php -+++ b/app/code/Magento/CatalogRule/Block/Adminhtml/Edit/DeleteButton.php -@@ -25,7 +25,7 @@ class DeleteButton extends GenericButton implements ButtonProviderInterface - 'class' => 'delete', - 'on_click' => 'deleteConfirm(\'' . __( - 'Are you sure you want to do this?' -- ) . '\', \'' . $this->urlBuilder->getUrl('*/*/delete', ['id' => $ruleId]) . '\')', -+ ) . '\', \'' . $this->urlBuilder->getUrl('*/*/delete', ['id' => $ruleId]) . '\', {data: {}})', - 'sort_order' => 20, - ]; - } -diff --git a/app/code/Magento/CatalogRule/Controller/Adminhtml/Promo/Catalog/Delete.php b/app/code/Magento/CatalogRule/Controller/Adminhtml/Promo/Catalog/Delete.php -index 3500506d8d6..998d45b839c 100644 ---- a/app/code/Magento/CatalogRule/Controller/Adminhtml/Promo/Catalog/Delete.php -+++ b/app/code/Magento/CatalogRule/Controller/Adminhtml/Promo/Catalog/Delete.php -@@ -6,9 +6,10 @@ - */ - namespace Magento\CatalogRule\Controller\Adminhtml\Promo\Catalog; - -+use Magento\Framework\App\Action\HttpPostActionInterface; - use Magento\Framework\Exception\LocalizedException; - --class Delete extends \Magento\CatalogRule\Controller\Adminhtml\Promo\Catalog -+class Delete extends \Magento\CatalogRule\Controller\Adminhtml\Promo\Catalog implements HttpPostActionInterface - { - /** - * @return void -diff --git a/app/code/Magento/CatalogRule/Controller/Adminhtml/Promo/Catalog/Edit.php b/app/code/Magento/CatalogRule/Controller/Adminhtml/Promo/Catalog/Edit.php -index 945c28b2088..2c2abcef8b2 100644 ---- a/app/code/Magento/CatalogRule/Controller/Adminhtml/Promo/Catalog/Edit.php -+++ b/app/code/Magento/CatalogRule/Controller/Adminhtml/Promo/Catalog/Edit.php -@@ -6,7 +6,9 @@ - */ - namespace Magento\CatalogRule\Controller\Adminhtml\Promo\Catalog; - --class Edit extends \Magento\CatalogRule\Controller\Adminhtml\Promo\Catalog -+use Magento\Framework\App\Action\HttpGetActionInterface as HttpGetActionInterface; -+ -+class Edit extends \Magento\CatalogRule\Controller\Adminhtml\Promo\Catalog implements HttpGetActionInterface - { - /** - * @return void -diff --git a/app/code/Magento/CatalogRule/Controller/Adminhtml/Promo/Catalog/Index.php b/app/code/Magento/CatalogRule/Controller/Adminhtml/Promo/Catalog/Index.php -index 8c6a3881951..a9dcd1f6383 100644 ---- a/app/code/Magento/CatalogRule/Controller/Adminhtml/Promo/Catalog/Index.php -+++ b/app/code/Magento/CatalogRule/Controller/Adminhtml/Promo/Catalog/Index.php -@@ -6,7 +6,9 @@ - */ - namespace Magento\CatalogRule\Controller\Adminhtml\Promo\Catalog; - --class Index extends \Magento\CatalogRule\Controller\Adminhtml\Promo\Catalog -+use Magento\Framework\App\Action\HttpGetActionInterface as HttpGetActionInterface; -+ -+class Index extends \Magento\CatalogRule\Controller\Adminhtml\Promo\Catalog implements HttpGetActionInterface - { - /** - * @return void -diff --git a/app/code/Magento/CatalogRule/Controller/Adminhtml/Promo/Catalog/NewAction.php b/app/code/Magento/CatalogRule/Controller/Adminhtml/Promo/Catalog/NewAction.php -index c677c75c595..d86c56402d2 100644 ---- a/app/code/Magento/CatalogRule/Controller/Adminhtml/Promo/Catalog/NewAction.php -+++ b/app/code/Magento/CatalogRule/Controller/Adminhtml/Promo/Catalog/NewAction.php -@@ -6,7 +6,9 @@ - */ - namespace Magento\CatalogRule\Controller\Adminhtml\Promo\Catalog; - --class NewAction extends \Magento\CatalogRule\Controller\Adminhtml\Promo\Catalog -+use Magento\Framework\App\Action\HttpGetActionInterface as HttpGetActionInterface; -+ -+class NewAction extends \Magento\CatalogRule\Controller\Adminhtml\Promo\Catalog implements HttpGetActionInterface - { - /** - * @return void -diff --git a/app/code/Magento/CatalogRule/Controller/Adminhtml/Promo/Catalog/NewConditionHtml.php b/app/code/Magento/CatalogRule/Controller/Adminhtml/Promo/Catalog/NewConditionHtml.php -index b790d90d380..a845c104f94 100644 ---- a/app/code/Magento/CatalogRule/Controller/Adminhtml/Promo/Catalog/NewConditionHtml.php -+++ b/app/code/Magento/CatalogRule/Controller/Adminhtml/Promo/Catalog/NewConditionHtml.php -@@ -6,9 +6,12 @@ - */ - namespace Magento\CatalogRule\Controller\Adminhtml\Promo\Catalog; - -+use Magento\Framework\App\Action\HttpGetActionInterface; -+use Magento\Framework\App\Action\HttpPostActionInterface as HttpPostActionInterface; - use Magento\Rule\Model\Condition\AbstractCondition; -+use Magento\CatalogRule\Controller\Adminhtml\Promo\Catalog as CatalogAction; - --class NewConditionHtml extends \Magento\CatalogRule\Controller\Adminhtml\Promo\Catalog -+class NewConditionHtml extends CatalogAction implements HttpPostActionInterface, HttpGetActionInterface - { - /** - * @return void -diff --git a/app/code/Magento/CatalogRule/Controller/Adminhtml/Promo/Catalog/Save.php b/app/code/Magento/CatalogRule/Controller/Adminhtml/Promo/Catalog/Save.php -index f3046c58a38..0ff12faf54c 100644 ---- a/app/code/Magento/CatalogRule/Controller/Adminhtml/Promo/Catalog/Save.php -+++ b/app/code/Magento/CatalogRule/Controller/Adminhtml/Promo/Catalog/Save.php -@@ -6,6 +6,7 @@ - */ - namespace Magento\CatalogRule\Controller\Adminhtml\Promo\Catalog; - -+use Magento\Framework\App\Action\HttpPostActionInterface as HttpPostActionInterface; - use Magento\Backend\App\Action\Context; - use Magento\Framework\Exception\LocalizedException; - use Magento\Framework\Registry; -@@ -13,9 +14,11 @@ use Magento\Framework\Stdlib\DateTime\Filter\Date; - use Magento\Framework\App\Request\DataPersistorInterface; - - /** -+ * Save action for catalog rule -+ * - * @SuppressWarnings(PHPMD.CouplingBetweenObjects) - */ --class Save extends \Magento\CatalogRule\Controller\Adminhtml\Promo\Catalog -+class Save extends \Magento\CatalogRule\Controller\Adminhtml\Promo\Catalog implements HttpPostActionInterface - { - /** - * @var DataPersistorInterface -@@ -39,7 +42,9 @@ class Save extends \Magento\CatalogRule\Controller\Adminhtml\Promo\Catalog - } - - /** -- * @return void -+ * Execute save action from catalog rule -+ * -+ * @return \Magento\Framework\App\ResponseInterface|\Magento\Framework\Controller\ResultInterface|void - * @SuppressWarnings(PHPMD.CyclomaticComplexity) - */ - public function execute() -@@ -60,6 +65,17 @@ class Save extends \Magento\CatalogRule\Controller\Adminhtml\Promo\Catalog - ['request' => $this->getRequest()] - ); - $data = $this->getRequest()->getPostValue(); -+ -+ $filterValues = ['from_date' => $this->_dateFilter]; -+ if ($this->getRequest()->getParam('to_date')) { -+ $filterValues['to_date'] = $this->_dateFilter; -+ } -+ $inputFilter = new \Zend_Filter_Input( -+ $filterValues, -+ [], -+ $data -+ ); -+ $data = $inputFilter->getUnescaped(); - $id = $this->getRequest()->getParam('rule_id'); - if ($id) { - $model = $ruleRepository->get($id); -diff --git a/app/code/Magento/CatalogRule/Controller/Adminhtml/Promo/Widget/CategoriesJson.php b/app/code/Magento/CatalogRule/Controller/Adminhtml/Promo/Widget/CategoriesJson.php -index d049d74bd26..18a58c0f854 100644 ---- a/app/code/Magento/CatalogRule/Controller/Adminhtml/Promo/Widget/CategoriesJson.php -+++ b/app/code/Magento/CatalogRule/Controller/Adminhtml/Promo/Widget/CategoriesJson.php -@@ -1,6 +1,5 @@ - <?php - /** -- * - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -@@ -9,8 +8,12 @@ namespace Magento\CatalogRule\Controller\Adminhtml\Promo\Widget; - use Magento\Backend\App\Action\Context; - use Magento\Catalog\Model\Category; - use Magento\Framework\Registry; -+use Magento\Framework\App\Action\HttpPostActionInterface as HttpPostActionInterface; - --class CategoriesJson extends \Magento\CatalogRule\Controller\Adminhtml\Promo\Widget -+/** -+ * Categories json widget for catalog rule -+ */ -+class CategoriesJson extends \Magento\CatalogRule\Controller\Adminhtml\Promo\Widget implements HttpPostActionInterface - { - /** - * Core registry -@@ -77,10 +80,11 @@ class CategoriesJson extends \Magento\CatalogRule\Controller\Adminhtml\Promo\Wid - if (!($category = $this->_initCategory())) { - return; - } -+ $selected = $this->getRequest()->getPost('selected', ''); - $block = $this->_view->getLayout()->createBlock( - \Magento\Catalog\Block\Adminhtml\Category\Checkboxes\Tree::class - )->setCategoryIds( -- [$categoryId] -+ explode(',', $selected) - ); - $this->getResponse()->representJson( - $block->getTreeJson($category) -diff --git a/app/code/Magento/CatalogRule/Model/Indexer/AbstractIndexer.php b/app/code/Magento/CatalogRule/Model/Indexer/AbstractIndexer.php -index 5d93e6f2168..6b7c12dfdf4 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\Indexer\ActionInterface as IndexerActionInterface; - 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 @@ abstract class AbstractIndexer implements IndexerActionInterface, MviewActionInt - { - $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 @@ abstract class AbstractIndexer implements IndexerActionInterface, MviewActionInt - abstract protected function doExecuteRow($id); - - /** -- * @return \Magento\Framework\App\CacheInterface|mixed -+ * Get cache manager - * -+ * @return \Magento\Framework\App\CacheInterface|mixed - * @deprecated 100.0.7 - */ - private function getCacheManager() -diff --git a/app/code/Magento/CatalogRule/Model/Indexer/IndexBuilder.php b/app/code/Magento/CatalogRule/Model/Indexer/IndexBuilder.php -index 1f62200fc6b..e12eabba764 100644 ---- a/app/code/Magento/CatalogRule/Model/Indexer/IndexBuilder.php -+++ b/app/code/Magento/CatalogRule/Model/Indexer/IndexBuilder.php -@@ -7,6 +7,7 @@ - namespace Magento\CatalogRule\Model\Indexer; - - use Magento\Catalog\Model\Product; -+use Magento\CatalogRule\Model\ResourceModel\Rule\Collection as RuleCollection; - use Magento\CatalogRule\Model\ResourceModel\Rule\CollectionFactory as RuleCollectionFactory; - use Magento\CatalogRule\Model\Rule; - use Magento\Framework\App\ObjectManager; -@@ -15,6 +16,8 @@ use Magento\CatalogRule\Model\Indexer\IndexBuilder\ProductLoader; - use Magento\CatalogRule\Model\Indexer\IndexerTableSwapperInterface as TableSwapper; - - /** -+ * Catalog rule index builder -+ * - * @api - * @SuppressWarnings(PHPMD.CouplingBetweenObjects) - * @SuppressWarnings(PHPMD.ExcessiveClassComplexity) -@@ -270,14 +273,14 @@ class IndexBuilder - */ - protected function doReindexByIds($ids) - { -- $this->cleanByIds($ids); -+ $this->cleanProductIndex($ids); - - $products = $this->productLoader->getProducts($ids); -- foreach ($this->getActiveRules() as $rule) { -- foreach ($products as $product) { -- $this->applyRule($rule, $product); -- } -+ $activeRules = $this->getActiveRules(); -+ foreach ($products as $product) { -+ $this->applyRules($activeRules, $product); - } -+ $this->reindexRuleGroupWebsite->execute(); - } - - /** -@@ -322,6 +325,30 @@ class IndexBuilder - ); - } - -+ /** -+ * Clean product index -+ * -+ * @param array $productIds -+ * @return void -+ */ -+ private function cleanProductIndex(array $productIds): void -+ { -+ $where = ['product_id IN (?)' => $productIds]; -+ $this->connection->delete($this->getTable('catalogrule_product'), $where); -+ } -+ -+ /** -+ * Clean product price index -+ * -+ * @param array $productIds -+ * @return void -+ */ -+ private function cleanProductPriceIndex(array $productIds): void -+ { -+ $where = ['product_id IN (?)' => $productIds]; -+ $this->connection->delete($this->getTable('catalogrule_product_price'), $where); -+ } -+ - /** - * Clean by product ids - * -@@ -330,51 +357,35 @@ class IndexBuilder - */ - protected function cleanByIds($productIds) - { -- $query = $this->connection->deleteFromSelect( -- $this->connection -- ->select() -- ->from($this->resource->getTableName('catalogrule_product'), 'product_id') -- ->distinct() -- ->where('product_id IN (?)', $productIds), -- $this->resource->getTableName('catalogrule_product') -- ); -- $this->connection->query($query); -- -- $query = $this->connection->deleteFromSelect( -- $this->connection->select() -- ->from($this->resource->getTableName('catalogrule_product_price'), 'product_id') -- ->distinct() -- ->where('product_id IN (?)', $productIds), -- $this->resource->getTableName('catalogrule_product_price') -- ); -- $this->connection->query($query); -+ $this->cleanProductIndex($productIds); -+ $this->cleanProductPriceIndex($productIds); - } - - /** -+ * Assign product to rule -+ * - * @param Rule $rule - * @param Product $product -- * @return $this -- * @throws \Exception -- * @SuppressWarnings(PHPMD.NPathComplexity) -+ * @return void - */ -- protected function applyRule(Rule $rule, $product) -+ private function assignProductToRule(Rule $rule, Product $product): void - { -- $ruleId = $rule->getId(); -- $productEntityId = $product->getId(); -- $websiteIds = array_intersect($product->getWebsiteIds(), $rule->getWebsiteIds()); -- - if (!$rule->validate($product)) { -- return $this; -+ return; - } - -+ $ruleId = (int) $rule->getId(); -+ $productEntityId = (int) $product->getId(); -+ $ruleProductTable = $this->getTable('catalogrule_product'); - $this->connection->delete( -- $this->resource->getTableName('catalogrule_product'), -+ $ruleProductTable, - [ -- $this->connection->quoteInto('rule_id = ?', $ruleId), -- $this->connection->quoteInto('product_id = ?', $productEntityId) -+ 'rule_id = ?' => $ruleId, -+ 'product_id = ?' => $productEntityId, - ] - ); - -+ $websiteIds = array_intersect($product->getWebsiteIds(), $rule->getWebsiteIds()); - $customerGroupIds = $rule->getCustomerGroupIds(); - $fromTime = strtotime($rule->getFromDate()); - $toTime = strtotime($rule->getToDate()); -@@ -385,36 +396,44 @@ class IndexBuilder - $actionStop = $rule->getStopRulesProcessing(); - - $rows = []; -- try { -- foreach ($websiteIds as $websiteId) { -- foreach ($customerGroupIds as $customerGroupId) { -- $rows[] = [ -- 'rule_id' => $ruleId, -- 'from_time' => $fromTime, -- 'to_time' => $toTime, -- 'website_id' => $websiteId, -- 'customer_group_id' => $customerGroupId, -- 'product_id' => $productEntityId, -- 'action_operator' => $actionOperator, -- 'action_amount' => $actionAmount, -- 'action_stop' => $actionStop, -- 'sort_order' => $sortOrder, -- ]; -- -- if (count($rows) == $this->batchCount) { -- $this->connection->insertMultiple($this->getTable('catalogrule_product'), $rows); -- $rows = []; -- } -+ foreach ($websiteIds as $websiteId) { -+ foreach ($customerGroupIds as $customerGroupId) { -+ $rows[] = [ -+ 'rule_id' => $ruleId, -+ 'from_time' => $fromTime, -+ 'to_time' => $toTime, -+ 'website_id' => $websiteId, -+ 'customer_group_id' => $customerGroupId, -+ 'product_id' => $productEntityId, -+ 'action_operator' => $actionOperator, -+ 'action_amount' => $actionAmount, -+ 'action_stop' => $actionStop, -+ 'sort_order' => $sortOrder, -+ ]; -+ -+ if (count($rows) == $this->batchCount) { -+ $this->connection->insertMultiple($ruleProductTable, $rows); -+ $rows = []; - } - } -- -- if (!empty($rows)) { -- $this->connection->insertMultiple($this->resource->getTableName('catalogrule_product'), $rows); -- } -- } catch (\Exception $e) { -- throw $e; - } -+ if ($rows) { -+ $this->connection->insertMultiple($ruleProductTable, $rows); -+ } -+ } - -+ /** -+ * Apply rule -+ * -+ * @param Rule $rule -+ * @param Product $product -+ * @return $this -+ * @throws \Exception -+ * @SuppressWarnings(PHPMD.NPathComplexity) -+ */ -+ protected function applyRule(Rule $rule, $product) -+ { -+ $this->assignProductToRule($rule, $product); - $this->reindexRuleProductPrice->execute($this->batchCount, $product); - $this->reindexRuleGroupWebsite->execute(); - -@@ -422,6 +441,25 @@ class IndexBuilder - } - - /** -+ * Apply rules -+ * -+ * @param RuleCollection $ruleCollection -+ * @param Product $product -+ * @return void -+ */ -+ private function applyRules(RuleCollection $ruleCollection, Product $product): void -+ { -+ foreach ($ruleCollection as $rule) { -+ $this->assignProductToRule($rule, $product); -+ } -+ -+ $this->cleanProductPriceIndex([$product->getId()]); -+ $this->reindexRuleProductPrice->execute($this->batchCount, $product); -+ } -+ -+ /** -+ * Retrieve table name -+ * - * @param string $tableName - * @return string - */ -@@ -431,6 +469,8 @@ class IndexBuilder - } - - /** -+ * Update rule product data -+ * - * @param Rule $rule - * @return $this - * @deprecated 100.2.0 -@@ -456,6 +496,8 @@ class IndexBuilder - } - - /** -+ * Apply all rules -+ * - * @param Product|null $product - * @throws \Exception - * @return $this -@@ -495,8 +537,10 @@ class IndexBuilder - } - - /** -+ * Calculate rule product price -+ * - * @param array $ruleData -- * @param null $productData -+ * @param array $productData - * @return float - * @deprecated 100.2.0 - * @see ProductPriceCalculator::calculate -@@ -507,6 +551,8 @@ class IndexBuilder - } - - /** -+ * Get rule products statement -+ * - * @param int $websiteId - * @param Product|null $product - * @return \Zend_Db_Statement_Interface -@@ -520,6 +566,8 @@ class IndexBuilder - } - - /** -+ * Save rule product prices -+ * - * @param array $arrData - * @return $this - * @throws \Exception -@@ -535,7 +583,7 @@ class IndexBuilder - /** - * Get active rules - * -- * @return array -+ * @return RuleCollection - */ - protected function getActiveRules() - { -@@ -545,7 +593,7 @@ class IndexBuilder - /** - * Get active rules - * -- * @return array -+ * @return RuleCollection - */ - protected function getAllRules() - { -@@ -553,6 +601,8 @@ class IndexBuilder - } - - /** -+ * Get product -+ * - * @param int $productId - * @return Product - */ -@@ -565,6 +615,8 @@ class IndexBuilder - } - - /** -+ * Log critical exception -+ * - * @param \Exception $e - * @return void - */ -diff --git a/app/code/Magento/CatalogRule/Model/Indexer/RuleProductPricesPersistor.php b/app/code/Magento/CatalogRule/Model/Indexer/RuleProductPricesPersistor.php -index 0b1264a2162..25bcfb8f20e 100644 ---- a/app/code/Magento/CatalogRule/Model/Indexer/RuleProductPricesPersistor.php -+++ b/app/code/Magento/CatalogRule/Model/Indexer/RuleProductPricesPersistor.php -@@ -76,25 +76,19 @@ class RuleProductPricesPersistor - ); - } - -- $productIds = []; -- -- try { -- foreach ($priceData as $key => $data) { -- $productIds['product_id'] = $data['product_id']; -- $priceData[$key]['rule_date'] = $this->dateFormat->formatDate($data['rule_date'], false); -- $priceData[$key]['latest_start_date'] = $this->dateFormat->formatDate( -- $data['latest_start_date'], -- false -- ); -- $priceData[$key]['earliest_end_date'] = $this->dateFormat->formatDate( -- $data['earliest_end_date'], -- false -- ); -- } -- $connection->insertOnDuplicate($indexTable, $priceData); -- } catch (\Exception $e) { -- throw $e; -+ foreach ($priceData as $key => $data) { -+ $priceData[$key]['rule_date'] = $this->dateFormat->formatDate($data['rule_date'], false); -+ $priceData[$key]['latest_start_date'] = $this->dateFormat->formatDate( -+ $data['latest_start_date'], -+ false -+ ); -+ $priceData[$key]['earliest_end_date'] = $this->dateFormat->formatDate( -+ $data['earliest_end_date'], -+ false -+ ); - } -+ $connection->insertOnDuplicate($indexTable, $priceData); -+ - return true; - } - } -diff --git a/app/code/Magento/CatalogRule/Model/Rule/Condition/ConditionsToSearchCriteriaMapper.php b/app/code/Magento/CatalogRule/Model/Rule/Condition/ConditionsToSearchCriteriaMapper.php -index 6d343fe149d..fabe504fbe3 100644 ---- a/app/code/Magento/CatalogRule/Model/Rule/Condition/ConditionsToSearchCriteriaMapper.php -+++ b/app/code/Magento/CatalogRule/Model/Rule/Condition/ConditionsToSearchCriteriaMapper.php -@@ -71,6 +71,8 @@ class ConditionsToSearchCriteriaMapper - } - - /** -+ * Convert condition to filter group -+ * - * @param ConditionInterface $condition - * @return null|\Magento\Framework\Api\CombinedFilterGroup|\Magento\Framework\Api\Filter - * @throws InputException -@@ -89,6 +91,8 @@ class ConditionsToSearchCriteriaMapper - } - - /** -+ * Convert combined condition to filter group -+ * - * @param Combine $combinedCondition - * @return null|\Magento\Framework\Api\CombinedFilterGroup - * @throws InputException -@@ -121,6 +125,8 @@ class ConditionsToSearchCriteriaMapper - } - - /** -+ * Convert simple condition to filter group -+ * - * @param ConditionInterface $productCondition - * @return FilterGroup|Filter - * @throws InputException -@@ -139,6 +145,8 @@ class ConditionsToSearchCriteriaMapper - } - - /** -+ * Convert simple condition with array value to filter group -+ * - * @param ConditionInterface $productCondition - * @return FilterGroup - * @throws InputException -@@ -161,6 +169,8 @@ class ConditionsToSearchCriteriaMapper - } - - /** -+ * Get glue for multiple values by operator -+ * - * @param string $operator - * @return string - */ -@@ -211,6 +221,8 @@ class ConditionsToSearchCriteriaMapper - } - - /** -+ * Convert filters array into combined filter group -+ * - * @param array $filters - * @param string $combinationMode - * @return FilterGroup -@@ -227,6 +239,8 @@ class ConditionsToSearchCriteriaMapper - } - - /** -+ * Creating of filter object by filtering params -+ * - * @param string $field - * @param string $value - * @param string $conditionType -@@ -264,6 +278,7 @@ class ConditionsToSearchCriteriaMapper - '!{}' => 'nlike', // does not contains - '()' => 'in', // is one of - '!()' => 'nin', // is not one of -+ '<=>' => 'is_null' - ]; - - if (!array_key_exists($ruleOperator, $operatorsMap)) { -diff --git a/app/code/Magento/CatalogRule/Model/Rule/Condition/Product.php b/app/code/Magento/CatalogRule/Model/Rule/Condition/Product.php -index ab650c94a0f..0db178b2a0a 100644 ---- a/app/code/Magento/CatalogRule/Model/Rule/Condition/Product.php -+++ b/app/code/Magento/CatalogRule/Model/Rule/Condition/Product.php -@@ -4,12 +4,11 @@ - * See COPYING.txt for license details. - */ - --/** -- * Catalog Rule Product Condition data model -- */ - namespace Magento\CatalogRule\Model\Rule\Condition; - - /** -+ * Catalog Rule Product Condition data model -+ * - * @method string getAttribute() Returns attribute code - */ - class Product extends \Magento\Rule\Model\Condition\Product\AbstractProduct -@@ -29,6 +28,9 @@ class Product extends \Magento\Rule\Model\Condition\Product\AbstractProduct - - $oldAttrValue = $model->getData($attrCode); - if ($oldAttrValue === null) { -+ if ($this->getOperator() === '<=>') { -+ return true; -+ } - return false; - } - -diff --git a/app/code/Magento/CatalogRule/Observer/AddDirtyRulesNotice.php b/app/code/Magento/CatalogRule/Observer/AddDirtyRulesNotice.php -index 08c3d97b216..749ac3cf512 100644 ---- a/app/code/Magento/CatalogRule/Observer/AddDirtyRulesNotice.php -+++ b/app/code/Magento/CatalogRule/Observer/AddDirtyRulesNotice.php -@@ -37,7 +37,7 @@ class AddDirtyRulesNotice implements ObserverInterface - $dirtyRules = $observer->getData('dirty_rules'); - if (!empty($dirtyRules)) { - if ($dirtyRules->getState()) { -- $this->messageManager->addNotice($observer->getData('message')); -+ $this->messageManager->addNoticeMessage($observer->getData('message')); - } - } - } -diff --git a/app/code/Magento/CatalogRule/Observer/RulePricesStorage.php b/app/code/Magento/CatalogRule/Observer/RulePricesStorage.php -index 8a3a8217076..321780eca56 100644 ---- a/app/code/Magento/CatalogRule/Observer/RulePricesStorage.php -+++ b/app/code/Magento/CatalogRule/Observer/RulePricesStorage.php -@@ -22,7 +22,7 @@ class RulePricesStorage - */ - public function getRulePrice($id) - { -- return isset($this->rulePrices[$id]) ? $this->rulePrices[$id] : false; -+ return $this->rulePrices[$id] ?? false; - } - - /** -diff --git a/app/code/Magento/CatalogRule/Pricing/Price/CatalogRulePrice.php b/app/code/Magento/CatalogRule/Pricing/Price/CatalogRulePrice.php -index a45a548264a..c71b51317fd 100644 ---- a/app/code/Magento/CatalogRule/Pricing/Price/CatalogRulePrice.php -+++ b/app/code/Magento/CatalogRule/Pricing/Price/CatalogRulePrice.php -@@ -89,7 +89,7 @@ class CatalogRulePrice extends AbstractPrice implements BasePriceProviderInterfa - { - if (null === $this->value) { - if ($this->product->hasData(self::PRICE_CODE)) { -- $this->value = floatval($this->product->getData(self::PRICE_CODE)) ?: false; -+ $this->value = (float)$this->product->getData(self::PRICE_CODE) ?: false; - } else { - $this->value = $this->getRuleResource() - ->getRulePrice( -@@ -98,7 +98,7 @@ class CatalogRulePrice extends AbstractPrice implements BasePriceProviderInterfa - $this->customerSession->getCustomerGroupId(), - $this->product->getId() - ); -- $this->value = $this->value ? floatval($this->value) : false; -+ $this->value = $this->value ? (float)$this->value : false; - } - if ($this->value) { - $this->value = $this->priceCurrency->convertAndRound($this->value); -diff --git a/app/code/Magento/CatalogRule/Test/Mftf/ActionGroup/AdminAssertCustomerGroupOnCatalogPriceRuleFormActionGroup.xml b/app/code/Magento/CatalogRule/Test/Mftf/ActionGroup/AdminAssertCustomerGroupOnCatalogPriceRuleFormActionGroup.xml -new file mode 100644 -index 00000000000..ef498b95a5d ---- /dev/null -+++ b/app/code/Magento/CatalogRule/Test/Mftf/ActionGroup/AdminAssertCustomerGroupOnCatalogPriceRuleFormActionGroup.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="AdminAssertCustomerGroupOnCatalogPriceRuleForm"> -+ <arguments> -+ <argument name="customerGroupName" type="string"/> -+ </arguments> -+ <amOnPage url="{{AdminNewCatalogRulePage.url}}" stepKey="amOnCatalogPriceRuleCreatePage"/> -+ <waitForElementVisible selector="{{AdminNewCatalogPriceRule.customerGroups}}" stepKey="waitForElementVisible"/> -+ <see selector="{{AdminNewCatalogPriceRule.customerGroups}}" userInput="{{customerGroupName}}" stepKey="assertCustomerGroupPresentOnCatalogPriceRuleForm"/> -+ </actionGroup> -+</actionGroups> -diff --git a/app/code/Magento/CatalogRule/Test/Mftf/ActionGroup/AdminCreateNewCatalogPriceRuleActionGroup.xml b/app/code/Magento/CatalogRule/Test/Mftf/ActionGroup/AdminCreateNewCatalogPriceRuleActionGroup.xml -new file mode 100644 -index 00000000000..4599e325e39 ---- /dev/null -+++ b/app/code/Magento/CatalogRule/Test/Mftf/ActionGroup/AdminCreateNewCatalogPriceRuleActionGroup.xml -@@ -0,0 +1,30 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> -+ <actionGroup name="AdminCreateNewCatalogPriceRuleActionGroup"> -+ <arguments> -+ <argument name="catalogRule" defaultValue="_defaultCatalogRule"/> -+ <argument name="customerGroup" type="string"/> -+ </arguments> -+ <amOnPage url="{{AdminNewCatalogPriceRulePage.url}}" stepKey="openNewCatalogPriceRulePage"/> -+ <waitForPageLoad stepKey="waitForPageToLoad"/> -+ <fillField stepKey="fillName" selector="{{AdminNewCatalogPriceRule.ruleName}}" userInput="{{catalogRule.name}}"/> -+ <fillField stepKey="fillDescription" selector="{{AdminNewCatalogPriceRule.description}}" userInput="{{catalogRule.description}}"/> -+ <selectOption selector="{{AdminNewCatalogPriceRule.status}}" userInput="{{catalogRule.is_active}}" stepKey="selectStatus"/> -+ <selectOption stepKey="selectWebSite" selector="{{AdminNewCatalogPriceRule.websites}}" userInput="{{catalogRule.website_ids[0]}}"/> -+ <selectOption selector="{{AdminNewCatalogPriceRule.customerGroups}}" userInput="{{customerGroup}}" stepKey="selectCustomerGroup"/> -+ <scrollTo selector="{{AdminNewCatalogPriceRule.actionsTab}}" stepKey="scrollToActionTab"/> -+ <click stepKey="openActionDropdown" selector="{{AdminNewCatalogPriceRule.actionsTab}}"/> -+ <selectOption stepKey="discountType" selector="{{AdminNewCatalogPriceRuleActions.apply}}" userInput="{{catalogRule.simple_action}}"/> -+ <fillField stepKey="fillDiscountValue" selector="{{AdminNewCatalogPriceRuleActions.discountAmount}}" userInput="{{catalogRule.discount_amount}}"/> -+ <selectOption stepKey="discardSubsequentRules" selector="{{AdminNewCatalogPriceRuleActions.disregardRules}}" userInput="Yes"/> -+ <waitForPageLoad stepKey="waitForApplied"/> -+ </actionGroup> -+</actionGroups> -\ No newline at end of file -diff --git a/app/code/Magento/CatalogRule/Test/Mftf/ActionGroup/AdminDeleteCatalogRuleActionGroup.xml b/app/code/Magento/CatalogRule/Test/Mftf/ActionGroup/AdminDeleteCatalogRuleActionGroup.xml -new file mode 100644 -index 00000000000..ea2d6821917 ---- /dev/null -+++ b/app/code/Magento/CatalogRule/Test/Mftf/ActionGroup/AdminDeleteCatalogRuleActionGroup.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="AdminDeleteCatalogRuleActionGroup"> -+ <click selector="{{AdminNewCatalogPriceRule.delete}}" stepKey="clickOnDeleteButton"/> -+ <waitForElementVisible selector="{{AdminNewCatalogPriceRule.okButton}}" stepKey="waitForOkButtonToBeVisible"/> -+ <click selector="{{AdminNewCatalogPriceRule.okButton}}" stepKey="clickOnOkButton"/> -+ <waitForPageLoad stepKey="waitForPagetoLoad"/> -+ <see selector="{{AdminNewCatalogPriceRule.successMessage}}" userInput="You deleted the rule." stepKey="seeSuccessDeleteMessage"/> -+ </actionGroup> -+</actionGroups> -\ No newline at end of file -diff --git a/app/code/Magento/CatalogRule/Test/Mftf/ActionGroup/AdminOpenNewCatalogPriceRuleFormPageActionGroup.xml b/app/code/Magento/CatalogRule/Test/Mftf/ActionGroup/AdminOpenNewCatalogPriceRuleFormPageActionGroup.xml -new file mode 100644 -index 00000000000..8651a17cb96 ---- /dev/null -+++ b/app/code/Magento/CatalogRule/Test/Mftf/ActionGroup/AdminOpenNewCatalogPriceRuleFormPageActionGroup.xml -@@ -0,0 +1,14 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> -+ <actionGroup name="AdminOpenNewCatalogPriceRuleFormPageActionGroup"> -+ <amOnPage url="{{AdminNewCatalogRulePage.url}}" stepKey="openNewCatalogPriceRulePage" /> -+ <waitForPageLoad stepKey="waitForPageLoad" /> -+ </actionGroup> -+</actionGroups> -diff --git a/app/code/Magento/CatalogRule/Test/Mftf/ActionGroup/AdminSaveAndApplyRulesActionGroup.xml b/app/code/Magento/CatalogRule/Test/Mftf/ActionGroup/AdminSaveAndApplyRulesActionGroup.xml -new file mode 100644 -index 00000000000..2584b8b36b7 ---- /dev/null -+++ b/app/code/Magento/CatalogRule/Test/Mftf/ActionGroup/AdminSaveAndApplyRulesActionGroup.xml -@@ -0,0 +1,21 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> -+ <actionGroup name="AdminSaveAndApplyRulesActionGroup"> -+ <waitForPageLoad stepKey="waitForPageToLoad"/> -+ <scrollToTopOfPage stepKey="scrollToTop"/> -+ <click selector="{{AdminNewCatalogPriceRule.save}}" stepKey="saveTheCatalogRule"/> -+ <waitForPageLoad stepKey="waitForPageToLoad1"/> -+ <seeElement selector="{{AdminCatalogPriceRuleGrid.updateMessage}}" stepKey="seeMessageToUpdateTheCatalogRules"/> -+ <see selector="{{AdminNewCatalogPriceRule.successMessage}}" userInput="You saved the rule." stepKey="seeSuccessMessage"/> -+ <click stepKey="applyRules" selector="{{AdminCatalogPriceRuleGrid.applyRules}}"/> -+ <see stepKey="assertSuccess" selector="{{AdminCategoryMessagesSection.SuccessMessage}}" userInput="Updated rules applied."/> -+ </actionGroup> -+</actionGroups> -\ No newline at end of file -diff --git a/app/code/Magento/CatalogRule/Test/Mftf/ActionGroup/AdminSearchCatalogRuleInGridActionGroup.xml b/app/code/Magento/CatalogRule/Test/Mftf/ActionGroup/AdminSearchCatalogRuleInGridActionGroup.xml -new file mode 100644 -index 00000000000..c7d5d853642 ---- /dev/null -+++ b/app/code/Magento/CatalogRule/Test/Mftf/ActionGroup/AdminSearchCatalogRuleInGridActionGroup.xml -@@ -0,0 +1,23 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> -+ <actionGroup name="AdminSearchCatalogRuleInGridActionGroup"> -+ <arguments> -+ <argument name="catalogRuleName" type="string"/> -+ </arguments> -+ <amOnPage stepKey="goToPriceRulePage" url="{{CatalogRulePage.url}}"/> -+ <waitForPageLoad stepKey="waitForPriceRulePage"/> -+ <click selector="{{AdminCatalogPriceRuleGrid.resetFilter}}" stepKey="clickOnResetFilter"/> -+ <waitForPageLoad stepKey="waitForTheGridPageToLoad"/> -+ <fillField selector="{{AdminCatalogPriceRuleGrid.ruleFilter}}" userInput="{{catalogRuleName}}" stepKey="fillTheRuleFilter"/> -+ <click selector="{{AdminCatalogPriceRuleGrid.search}}" stepKey="clickOnSearchButton"/> -+ <waitForPageLoad stepKey="waitForTheSearchResult"/> -+ </actionGroup> -+</actionGroups> -\ No newline at end of file -diff --git a/app/code/Magento/CatalogRule/Test/Mftf/ActionGroup/AdminSelectCatalogRuleFromGridActionGroup.xml b/app/code/Magento/CatalogRule/Test/Mftf/ActionGroup/AdminSelectCatalogRuleFromGridActionGroup.xml -new file mode 100644 -index 00000000000..f0e927ea840 ---- /dev/null -+++ b/app/code/Magento/CatalogRule/Test/Mftf/ActionGroup/AdminSelectCatalogRuleFromGridActionGroup.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="AdminSelectCatalogRuleFromGridActionGroup"> -+ <arguments> -+ <argument name="catalogRuleName" type="string"/> -+ </arguments> -+ <click selector="{{AdminCatalogPriceRuleGrid.selectRowByRuleName(catalogRuleName)}}" stepKey="selectRow"/> -+ <waitForPageLoad stepKey="waitForPageToLoad"/> -+ </actionGroup> -+</actionGroups> -\ No newline at end of file -diff --git a/app/code/Magento/CatalogRule/Test/Mftf/ActionGroup/AssertCatalogPriceRuleFormActionGroup.xml b/app/code/Magento/CatalogRule/Test/Mftf/ActionGroup/AssertCatalogPriceRuleFormActionGroup.xml -new file mode 100644 -index 00000000000..330c2ad7e15 ---- /dev/null -+++ b/app/code/Magento/CatalogRule/Test/Mftf/ActionGroup/AssertCatalogPriceRuleFormActionGroup.xml -@@ -0,0 +1,29 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> -+ <actionGroup name="AssertCatalogPriceRuleFormActionGroup"> -+ <arguments> -+ <argument name="catalogRule" defaultValue="inactiveCatalogRule" /> -+ <argument name="status" type="string" defaultValue=""/> -+ <argument name="websites" type="string"/> -+ <argument name="customerGroup" type="string"/> -+ </arguments> -+ <seeInField stepKey="fillName" selector="{{AdminNewCatalogPriceRule.ruleName}}" userInput="{{catalogRule.name}}"/> -+ <seeInField stepKey="fillDescription" selector="{{AdminNewCatalogPriceRule.description}}" userInput="{{catalogRule.description}}"/> -+ <seeOptionIsSelected selector="{{AdminNewCatalogPriceRule.status}}" userInput="{{status}}" stepKey="selectStatus"/> -+ <see stepKey="seeWebSite" selector="{{AdminNewCatalogPriceRule.websites}}" userInput="{{websites}}"/> -+ <seeOptionIsSelected selector="{{AdminNewCatalogPriceRule.customerGroups}}" userInput="{{customerGroup}}" stepKey="selectCustomerGroup"/> -+ <scrollTo selector="{{AdminNewCatalogPriceRule.actionsTab}}" stepKey="scrollToActionTab"/> -+ <click stepKey="openActionDropdown" selector="{{AdminNewCatalogPriceRule.actionsTab}}"/> -+ <seeInField stepKey="discountType" selector="{{AdminNewCatalogPriceRuleActions.apply}}" userInput="{{catalogRule.simple_action}}"/> -+ <seeInField stepKey="fillDiscountValue" selector="{{AdminNewCatalogPriceRuleActions.discountAmount}}" userInput="{{catalogRule.discount_amount}}"/> -+ <waitForPageLoad stepKey="waitForPageToLoad"/> -+ </actionGroup> -+</actionGroups> -\ No newline at end of file -diff --git a/app/code/Magento/CatalogRule/Test/Mftf/ActionGroup/AssertCatalogRuleInGridActionGroup.xml b/app/code/Magento/CatalogRule/Test/Mftf/ActionGroup/AssertCatalogRuleInGridActionGroup.xml -new file mode 100644 -index 00000000000..8be41c3b07a ---- /dev/null -+++ b/app/code/Magento/CatalogRule/Test/Mftf/ActionGroup/AssertCatalogRuleInGridActionGroup.xml -@@ -0,0 +1,23 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> -+ <actionGroup name="AssertCatalogRuleInGridActionGroup"> -+ <arguments> -+ <argument name="catalogRuleName" type="string"/> -+ <argument name="status" type="string" defaultValue=""/> -+ <argument name="websites" type="string"/> -+ <argument name="catalogRuleId" type="string"/> -+ </arguments> -+ <see selector="{{AdminCatalogPriceRuleGrid.firstRow}}" userInput="{{catalogRuleId}}" stepKey="seeCatalogRuleId"/> -+ <see selector="{{AdminCatalogPriceRuleGrid.firstRow}}" userInput="{{catalogRuleName}}" stepKey="seeCatalogRuleName"/> -+ <see selector="{{AdminCatalogPriceRuleGrid.firstRow}}" userInput="{{status}}" stepKey="seeCatalogRuleStatus"/> -+ <see selector="{{AdminCatalogPriceRuleGrid.firstRow}}" userInput="{{websites}}" stepKey="seeCatalogRuleWebsite"/> -+ </actionGroup> -+</actionGroups> -\ No newline at end of file -diff --git a/app/code/Magento/CatalogRule/Test/Mftf/ActionGroup/AssertCustomerGroupNotOnCatalogPriceRuleFormActionGroup.xml b/app/code/Magento/CatalogRule/Test/Mftf/ActionGroup/AssertCustomerGroupNotOnCatalogPriceRuleFormActionGroup.xml -new file mode 100644 -index 00000000000..93a2a8a6109 ---- /dev/null -+++ b/app/code/Magento/CatalogRule/Test/Mftf/ActionGroup/AssertCustomerGroupNotOnCatalogPriceRuleFormActionGroup.xml -@@ -0,0 +1,20 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> -+ <actionGroup name="AssertCustomerGroupNotOnCatalogPriceRuleFormActionGroup"> -+ <arguments> -+ <argument name="customerGroup" type="entity" /> -+ </arguments> -+ <grabMultiple selector="{{AdminNewCatalogPriceRule.customerGroupsOptions}}" stepKey="customerGroups" /> -+ <assertNotContains stepKey="assertCustomerGroupNotInOptions"> -+ <actualResult type="variable">customerGroups</actualResult> -+ <expectedResult type="string">{{customerGroup.code}}</expectedResult> -+ </assertNotContains> -+ </actionGroup> -+</actionGroups> -diff --git a/app/code/Magento/CatalogRule/Test/Mftf/ActionGroup/CatalogPriceRuleActionGroup.xml b/app/code/Magento/CatalogRule/Test/Mftf/ActionGroup/CatalogPriceRuleActionGroup.xml -index f82974b7bc8..de0d2baee2d 100644 ---- a/app/code/Magento/CatalogRule/Test/Mftf/ActionGroup/CatalogPriceRuleActionGroup.xml -+++ b/app/code/Magento/CatalogRule/Test/Mftf/ActionGroup/CatalogPriceRuleActionGroup.xml -@@ -7,17 +7,17 @@ - --> - - <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"> - <!-- action group to create a new catalog price rule giving a catalogRule entity --> - <actionGroup name="newCatalogPriceRuleByUI"> - <arguments> - <argument name="catalogRule" defaultValue="_defaultCatalogRule"/> - </arguments> -+ - <!-- Go to the admin Catalog rule grid and add a new one --> - <amOnPage stepKey="goToPriceRulePage" url="{{CatalogRulePage.url}}"/> - <waitForPageLoad stepKey="waitForPriceRulePage"/> - <click stepKey="addNewRule" selector="{{AdminGridMainControls.add}}"/> -- <waitForPageLoad stepKey="waitForIndividualRulePage"/> - - <!-- Fill the form according the attributes of the entity --> - <fillField stepKey="fillName" selector="{{AdminNewCatalogPriceRule.ruleName}}" userInput="{{catalogRule.name}}"/> -@@ -28,8 +28,8 @@ - <click stepKey="clickToCalender" selector="{{AdminNewCatalogPriceRule.toDateButton}}"/> - <click stepKey="clickToToday" selector="{{AdminNewCatalogPriceRule.todayDate}}"/> - <click stepKey="openActionDropdown" selector="{{AdminNewCatalogPriceRule.actionsTab}}"/> -- <selectOption stepKey="discountType" selector="{{AdminNewCatalogPriceRuleActions.apply}}" userInput="{{catalogRule.simple_action}}"/> - <fillField stepKey="fillDiscountValue" selector="{{AdminNewCatalogPriceRuleActions.discountAmount}}" userInput="{{catalogRule.discount_amount}}"/> -+ <selectOption stepKey="discountType" selector="{{AdminNewCatalogPriceRuleActions.apply}}" userInput="{{catalogRule.simple_action}}"/> - <selectOption stepKey="discardSubsequentRules" selector="{{AdminNewCatalogPriceRuleActions.disregardRules}}" userInput="Yes"/> - - <!-- Scroll to top and either save or save and apply after the action group --> -@@ -37,6 +37,60 @@ - <waitForPageLoad stepKey="waitForApplied"/> - </actionGroup> - -+ <actionGroup name="createCatalogPriceRule"> -+ <arguments> -+ <argument name="catalogRule" defaultValue="_defaultCatalogRule"/> -+ </arguments> -+ -+ <click stepKey="addNewRule" selector="{{AdminGridMainControls.add}}"/> -+ <fillField selector="{{AdminNewCatalogPriceRule.ruleName}}" userInput="{{catalogRule.name}}" stepKey="fillName" /> -+ <fillField selector="{{AdminNewCatalogPriceRule.description}}" userInput="{{catalogRule.description}}" stepKey="fillDescription" /> -+ <selectOption selector="{{AdminNewCatalogPriceRule.websites}}" parameterArray="{{catalogRule.website_ids}}" stepKey="selectSite" /> -+ <click stepKey="openActionDropdown" selector="{{AdminNewCatalogPriceRule.actionsTab}}"/> -+ <fillField stepKey="fillDiscountValue" selector="{{AdminNewCatalogPriceRuleActions.discountAmount}}" userInput="{{catalogRule.discount_amount}}"/> -+ -+ <scrollToTopOfPage stepKey="scrollToTop"/> -+ <waitForPageLoad stepKey="waitForApplied"/> -+ </actionGroup> -+ -+ <actionGroup name="CreateCatalogPriceRuleViaTheUi"> -+ <arguments> -+ <argument name="catalogRule" defaultValue="_defaultCatalogRule"/> -+ <argument name="customerGroup" defaultValue="General" type="string"/> -+ <argument name="disregardRules" defaultValue="Yes" type="string"/> -+ </arguments> -+ -+ <fillField selector="{{AdminNewCatalogPriceRule.ruleName}}" userInput="{{catalogRule.name}}" stepKey="fillName1"/> -+ <fillField selector="{{AdminNewCatalogPriceRule.description}}" userInput="{{catalogRule.description}}" stepKey="fillDescription1"/> -+ <selectOption selector="{{AdminNewCatalogPriceRule.websites}}" userInput="{{catalogRule.website_ids[0]}}" stepKey="selectWebSite1"/> -+ <selectOption selector="{{AdminNewCatalogPriceRule.customerGroups}}" userInput="{{customerGroup}}" stepKey="selectCustomerGroup1"/> -+ <scrollTo selector="{{AdminNewCatalogPriceRule.actionsTab}}" stepKey="scrollToActionTab1"/> -+ <click selector="{{AdminNewCatalogPriceRule.actionsTab}}" stepKey="openActionDropdown1"/> -+ <selectOption selector="{{AdminNewCatalogPriceRuleActions.apply}}" userInput="{{catalogRule.simple_action}}" stepKey="discountType1"/> -+ <fillField selector="{{AdminNewCatalogPriceRuleActions.discountAmount}}" userInput="{{catalogRule.discount_amount}}" stepKey="fillDiscountValue1"/> -+ <selectOption selector="{{AdminNewCatalogPriceRuleActions.disregardRules}}" userInput="{{disregardRules}}" stepKey="discardSubsequentRules1"/> -+ <waitForPageLoad stepKey="waitForPageToLoad1"/> -+ <scrollToTopOfPage stepKey="scrollToTop1"/> -+ </actionGroup> -+ -+ <actionGroup name="CreateCatalogPriceRuleConditionWithAttribute"> -+ <arguments> -+ <argument name="attributeName" type="string"/> -+ <argument name="targetValue" type="string"/> -+ <argument name="targetSelectValue" type="string"/> -+ </arguments> -+ -+ <click selector="{{AdminNewCatalogPriceRule.conditionsTab}}" stepKey="openConditionsTab"/> -+ <waitForPageLoad stepKey="waitForConditionTabOpened"/> -+ <click selector="{{AdminNewCatalogPriceRuleConditions.newCondition}}" stepKey="addNewCondition"/> -+ <selectOption selector="{{AdminNewCatalogPriceRuleConditions.conditionSelect('1')}}" userInput="{{attributeName}}" stepKey="selectTypeCondition"/> -+ <waitForElement selector="{{AdminNewCatalogPriceRuleConditions.targetEllipsisValue('1', targetValue)}}" stepKey="waitForIsTarget"/> -+ <click selector="{{AdminNewCatalogPriceRuleConditions.targetEllipsisValue('1', 'is')}}" stepKey="clickOnIs"/> -+ <selectOption selector="{{AdminNewCatalogPriceRuleConditions.targetSelect('1')}}" userInput="{{targetSelectValue}}" stepKey="selectTargetCondition"/> -+ <click selector="{{AdminNewCatalogPriceRule.fromDateButton}}" stepKey="clickFromCalender"/> -+ <click selector="{{AdminNewCatalogPriceRule.todayDate}}" stepKey="clickFromToday"/> -+ </actionGroup> -+ - <!-- Apply all of the saved catalog price rules --> - <actionGroup name="applyCatalogPriceRules"> - <amOnPage stepKey="goToPriceRulePage" url="{{CatalogRulePage.url}}"/> -@@ -44,4 +98,63 @@ - <click stepKey="applyRules" selector="{{AdminCatalogPriceRuleGrid.applyRules}}"/> - <see stepKey="assertSuccess" selector="{{AdminCategoryMessagesSection.SuccessMessage}}" userInput="Updated rules applied."/> - </actionGroup> -+ -+ <!--Add Catalog Rule Condition With product SKU--> -+ <actionGroup name="newCatalogPriceRuleByUIWithConditionIsSKU" extends="newCatalogPriceRuleByUI"> -+ <arguments> -+ <argument name="productSku"/> -+ </arguments> -+ <click selector="{{AdminNewCatalogPriceRule.conditionsTab}}" after="discardSubsequentRules" stepKey="openConditionsTab"/> -+ <waitForPageLoad after="openConditionsTab" stepKey="waitForConditionTabOpened"/> -+ <click selector="{{AdminNewCatalogPriceRuleConditions.newCondition}}" after="waitForConditionTabOpened" stepKey="addNewCondition"/> -+ <selectOption selector="{{AdminNewCatalogPriceRuleConditions.conditionSelect('1')}}" userInput="Magento\CatalogRule\Model\Rule\Condition\Product|sku" after="addNewCondition" stepKey="selectTypeCondition"/> -+ <waitForPageLoad after="selectTypeCondition" stepKey="waitForConditionChosed"/> -+ <click selector="{{AdminNewCatalogPriceRuleConditions.targetEllipsis('1')}}" after="waitForConditionChosed" stepKey="clickEllipsis"/> -+ <fillField selector="{{AdminNewCatalogPriceRuleConditions.targetInput('1', '1')}}" userInput="{{productSku}}" after="clickEllipsis" stepKey="fillProductSku"/> -+ <click selector="{{AdminNewCatalogPriceRuleConditions.applyButton('1', '1')}}" after="fillProductSku" stepKey="clickApply"/> -+ </actionGroup> -+ -+ <!--Add Catalog Rule Condition With Category--> -+ <actionGroup name="newCatalogPriceRuleByUIWithConditionIsCategory" extends="newCatalogPriceRuleByUI"> -+ <arguments> -+ <argument name="categoryId"/> -+ </arguments> -+ <click selector="{{AdminNewCatalogPriceRule.conditionsTab}}" after="discardSubsequentRules" stepKey="openConditionsTab"/> -+ <waitForPageLoad after="openConditionsTab" stepKey="waitForConditionTabOpened"/> -+ <click selector="{{AdminNewCatalogPriceRuleConditions.newCondition}}" after="waitForConditionTabOpened" stepKey="addNewCondition"/> -+ <selectOption selector="{{AdminNewCatalogPriceRuleConditions.conditionSelect('1')}}" userInput="Magento\CatalogRule\Model\Rule\Condition\Product|category_ids" after="addNewCondition" stepKey="selectTypeCondition"/> -+ <waitForPageLoad after="selectTypeCondition" stepKey="waitForConditionChosed"/> -+ <click selector="{{AdminNewCatalogPriceRuleConditions.targetEllipsis('1')}}" after="waitForConditionChosed" stepKey="clickEllipsis"/> -+ <fillField selector="{{AdminNewCatalogPriceRuleConditions.targetInput('1', '1')}}" userInput="{{categoryId}}" after="clickEllipsis" stepKey="fillCategoryId"/> -+ <click selector="{{AdminNewCatalogPriceRuleConditions.applyButton('1', '1')}}" after="fillCategoryId" stepKey="clickApply"/> -+ </actionGroup> -+ -+ <actionGroup name="selectGeneralCustomerGroupActionGroup"> -+ <selectOption selector="{{AdminNewCatalogPriceRule.customerGroups}}" userInput="General" stepKey="selectCustomerGroup"/> -+ </actionGroup> -+ -+ <actionGroup name="selectNotLoggedInCustomerGroupActionGroup"> -+ <selectOption selector="{{AdminNewCatalogPriceRule.customerGroups}}" userInput="NOT LOGGED IN" stepKey="selectCustomerGroup"/> -+ </actionGroup> -+ -+ <!-- 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> -+ -+ <actionGroup name="RemoveCatalogPriceRule" extends="OpenCatalogPriceRule"> -+ <click selector="{{AdminMainActionsSection.delete}}" after="waitForPageLoad" stepKey="clickToDelete"/> -+ <waitForElementVisible selector="{{AdminConfirmationModalSection.ok}}" after="clickToDelete" stepKey="waitForElementVisible"/> -+ <click selector="{{AdminConfirmationModalSection.ok}}" after="waitForElementVisible" stepKey="clickToConfirm"/> -+ <waitForElementVisible selector="{{AdminMessagesSection.success}}" after="clickToConfirm" stepKey="waitForSuccessMessage"/> -+ <see selector="{{AdminMessagesSection.success}}" userInput="You deleted the rule." after="waitForSuccessMessage" stepKey="verifyRuleIsDeleted"/> -+ </actionGroup> - </actionGroups> -diff --git a/app/code/Magento/CatalogRule/Test/Mftf/ActionGroup/CatalogSelectCustomerGroupActionGroup.xml b/app/code/Magento/CatalogRule/Test/Mftf/ActionGroup/CatalogSelectCustomerGroupActionGroup.xml -new file mode 100644 -index 00000000000..da961abac30 ---- /dev/null -+++ b/app/code/Magento/CatalogRule/Test/Mftf/ActionGroup/CatalogSelectCustomerGroupActionGroup.xml -@@ -0,0 +1,17 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> -+ <actionGroup name="CatalogSelectCustomerGroupActionGroup"> -+ <arguments> -+ <argument name="customerGroupName" defaultValue="NOT LOGGED IN" type="string"/> -+ </arguments> -+ <selectOption selector="{{AdminNewCatalogPriceRule.customerGroups}}" userInput="{{customerGroupName}}" stepKey="selectCustomerGroup"/> -+ </actionGroup> -+</actionGroups> -diff --git a/app/code/Magento/CatalogRule/Test/Mftf/ActionGroup/CreateCatalogPriceRuleConditionWithAttributeAndOptionActionGroup.xml b/app/code/Magento/CatalogRule/Test/Mftf/ActionGroup/CreateCatalogPriceRuleConditionWithAttributeAndOptionActionGroup.xml -new file mode 100644 -index 00000000000..41323b8f3b4 ---- /dev/null -+++ b/app/code/Magento/CatalogRule/Test/Mftf/ActionGroup/CreateCatalogPriceRuleConditionWithAttributeAndOptionActionGroup.xml -@@ -0,0 +1,26 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> -+ <actionGroup name="CreateCatalogPriceRuleConditionWithAttributeAndOptionActionGroup"> -+ <arguments> -+ <argument name="attributeName" type="string"/> -+ <argument name="targetSelectValue" type="string"/> -+ <argument name="indexA" type="string"/> -+ <argument name="indexB" type="string"/> -+ </arguments> -+ <click selector="{{AdminNewCatalogPriceRule.conditionsTab}}" stepKey="openConditionsTab"/> -+ <waitForPageLoad stepKey="waitForConditionTabOpened"/> -+ <click selector="{{AdminNewCatalogPriceRuleConditions.newCondition}}" stepKey="addNewCondition"/> -+ <selectOption selector="{{AdminNewCatalogPriceRuleConditions.conditionSelect(indexA)}}" userInput="{{attributeName}}" stepKey="selectTypeCondition"/> -+ <waitForElement selector="{{AdminNewCatalogPriceRuleConditions.ellipsisValue(indexA)}}" stepKey="waitForTarget"/> -+ <click selector="{{AdminNewCatalogPriceRuleConditions.ellipsisValue(indexA)}}" stepKey="clickOnEllipsis"/> -+ <selectOption selector="{{AdminNewCatalogPriceRuleConditions.targetEllipsisSelect(indexA, indexB)}}" userInput="{{targetSelectValue}}" stepKey="selectOption"/> -+ </actionGroup> -+</actionGroups> -diff --git a/app/code/Magento/CatalogRule/Test/Mftf/ActionGroup/SaveAndApplyCatalogPriceRuleActionGroup.xml b/app/code/Magento/CatalogRule/Test/Mftf/ActionGroup/SaveAndApplyCatalogPriceRuleActionGroup.xml -new file mode 100644 -index 00000000000..bc75414e0de ---- /dev/null -+++ b/app/code/Magento/CatalogRule/Test/Mftf/ActionGroup/SaveAndApplyCatalogPriceRuleActionGroup.xml -@@ -0,0 +1,16 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> -+ <actionGroup name="SaveAndApplyCatalogPriceRuleActionGroup"> -+ <waitForElementVisible selector="{{AdminNewCatalogPriceRule.saveAndApply}}" stepKey="waitForSaveAndApplyButton"/> -+ <click selector="{{AdminNewCatalogPriceRule.saveAndApply}}" stepKey="saveAndApply"/> -+ <see selector="{{AdminCategoryMessagesSection.SuccessMessage}}" userInput="You saved the rule." stepKey="assertSuccess"/> -+ </actionGroup> -+</actionGroups> -\ No newline at end of file -diff --git a/app/code/Magento/CatalogRule/Test/Mftf/Data/AdminMenuData.xml b/app/code/Magento/CatalogRule/Test/Mftf/Data/AdminMenuData.xml -new file mode 100644 -index 00000000000..eb9cac1401c ---- /dev/null -+++ b/app/code/Magento/CatalogRule/Test/Mftf/Data/AdminMenuData.xml -@@ -0,0 +1,21 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<entities xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataProfileSchema.xsd"> -+ <entity name="AdminMenuMarketing"> -+ <data key="pageTitle">Marketing</data> -+ <data key="title">Marketing</data> -+ <data key="dataUiId">magento-backend-marketing</data> -+ </entity> -+ <entity name="AdminMenuMarketingPromotionsCatalogPriceRule"> -+ <data key="pageTitle">Catalog Price Rule</data> -+ <data key="title">Catalog Price Rule</data> -+ <data key="dataUiId">magento-catalogrule-promo-catalog</data> -+ </entity> -+</entities> -diff --git a/app/code/Magento/CatalogRule/Test/Mftf/Data/CatalogRuleData.xml b/app/code/Magento/CatalogRule/Test/Mftf/Data/CatalogRuleData.xml -index 3199a53026a..5c6ea970d3b 100644 ---- a/app/code/Magento/CatalogRule/Test/Mftf/Data/CatalogRuleData.xml -+++ b/app/code/Magento/CatalogRule/Test/Mftf/Data/CatalogRuleData.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="_defaultCatalogRule" type="catalogRule"> - <data key="name" unique="suffix">CatalogPriceRule</data> - <data key="description">Catalog Price Rule Description</data> -@@ -33,7 +33,7 @@ - <item>1</item> - </array> - <data key="simple_action">by_fixed</data> -- <data key="discount_amount">10</data> -+ <data key="discount_amount">12.3</data> - </entity> - - <entity name="CatalogRuleToPercent" type="catalogRule"> -@@ -61,6 +61,100 @@ - <item>1</item> - </array> - <data key="simple_action">to_fixed</data> -- <data key="discount_amount">100</data> -+ <data key="discount_amount">110.7</data> -+ </entity> -+ -+ <entity name="CatalogRuleByPercentWith96Amount" type="catalogRule"> -+ <data key="name" unique="suffix">CatalogPriceRule</data> -+ <data key="description">Catalog Price Rule Description</data> -+ <data key="is_active">1</data> -+ <array key="customer_group_ids"> -+ <item>0</item> -+ </array> -+ <array key="website_ids"> -+ <item>1</item> -+ </array> -+ <data key="simple_action">by_percent</data> -+ <data key="discount_amount">96</data> -+ </entity> -+ -+ <entity name="CatalogRuleWithAllCustomerGroups" type="catalogRule"> -+ <data key="name" unique="suffix">CatalogPriceRule</data> -+ <data key="description">Catalog Price Rule Description</data> -+ <data key="is_active">1</data> -+ <array key="customer_group_ids"> -+ <item>0</item> -+ <item>1</item> -+ <item>2</item> -+ <item>3</item> -+ </array> -+ <array key="website_ids"> -+ <item>1</item> -+ </array> -+ <data key="simple_action">by_percent</data> -+ <data key="discount_amount">10</data> -+ </entity> -+ -+ <entity name="InactiveCatalogRule" type="catalogRule"> -+ <data key="name" unique="suffix">InactiveCatalogRule</data> -+ <data key="description">Inactive Catalog Price Rule Description</data> -+ <data key="is_active">0</data> -+ <array key="customer_group_ids"> -+ <item>1</item> -+ </array> -+ <array key="website_ids"> -+ <item>1</item> -+ </array> -+ <data key="simple_action">by_fixed</data> -+ <data key="discount_amount">10</data> -+ </entity> -+ -+ <entity name="CatalogRuleWithoutDiscount" type="catalogRule"> -+ <data key="name" unique="suffix">CatalogPriceRuleWithoutDiscount</data> -+ <data key="description">Catalog Price Rule Description</data> -+ <data key="is_active">1</data> -+ <array key="customer_group_ids"> -+ <item>0</item> -+ </array> -+ <array key="website_ids"> -+ <item>1</item> -+ </array> -+ <data key="simple_action">by_percent</data> -+ <data key="discount_amount">0</data> -+ </entity> -+ -+ <entity name="ActiveCatalogPriceRuleWithConditions" type="catalogRule"> -+ <data key="name" unique="suffix">Active Catalog Rule with conditions </data> -+ <data key="description">Rule Description</data> -+ <data key="is_active">1</data> -+ <array key="customer_group_ids"> -+ <item>0</item> -+ <item>1</item> -+ <item>2</item> -+ <item>3</item> -+ </array> -+ <array key="website_ids"> -+ <item>1</item> -+ </array> -+ <data key="simple_action">by_percent</data> -+ <data key="discount_amount">10</data> -+ </entity> -+ -+ <!-- DO NOT USE IN OTHER TESTS AS IT WILL BREAK THE EXISTING TESTS --> -+ <entity name="DeleteActiveCatalogPriceRuleWithConditions" type="catalogRule"> -+ <data key="name" unique="suffix">Delete Active Catalog Rule with conditions </data> -+ <data key="description">Rule Description</data> -+ <data key="is_active">1</data> -+ <array key="customer_group_ids"> -+ <item>0</item> -+ <item>1</item> -+ <item>2</item> -+ <item>3</item> -+ </array> -+ <array key="website_ids"> -+ <item>1</item> -+ </array> -+ <data key="simple_action">by_percent</data> -+ <data key="discount_amount">10</data> - </entity> - </entities> -diff --git a/app/code/Magento/CatalogRule/Test/Mftf/Metadata/catalog-rule-meta.xml b/app/code/Magento/CatalogRule/Test/Mftf/Metadata/catalog-rule-meta.xml -index 6f5bd2decc6..0d89c7970b8 100644 ---- a/app/code/Magento/CatalogRule/Test/Mftf/Metadata/catalog-rule-meta.xml -+++ b/app/code/Magento/CatalogRule/Test/Mftf/Metadata/catalog-rule-meta.xml -@@ -8,7 +8,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"> -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataOperation.xsd"> - <operation name="createCatalogRule" dataType="catalogRule" - type="create" auth="adminFormKey" url="/catalog_rule/promo_catalog/save/" method="POST"> - <contentType>application/x-www-form-urlencoded</contentType> -diff --git a/app/code/Magento/CatalogRule/Test/Mftf/Page/AdminCatalogPriceRuleGridPage.xml b/app/code/Magento/CatalogRule/Test/Mftf/Page/AdminCatalogPriceRuleGridPage.xml -new file mode 100644 -index 00000000000..24e4b27604f ---- /dev/null -+++ b/app/code/Magento/CatalogRule/Test/Mftf/Page/AdminCatalogPriceRuleGridPage.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="AdminCatalogPriceRuleGridPage" url="catalog_rule/promo_catalog/" module="Magento_CatalogRule" area="admin"> -+ <section name="AdminCatalogPriceRuleGridSection"/> -+ </page> -+</pages> -diff --git a/app/code/Magento/CatalogRule/Test/Mftf/Page/AdminNewCatalogPriceRulePage.xml b/app/code/Magento/CatalogRule/Test/Mftf/Page/AdminNewCatalogPriceRulePage.xml -new file mode 100644 -index 00000000000..fded4f5e5f3 ---- /dev/null -+++ b/app/code/Magento/CatalogRule/Test/Mftf/Page/AdminNewCatalogPriceRulePage.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="AdminNewCatalogPriceRulePage" url="catalog_rule/promo_catalog/new/" module="Magento_CatalogRule" area="admin"> -+ <section name="AdminNewCatalogPriceRule"/> -+ </page> -+</pages> -diff --git a/app/code/Magento/CatalogRule/Test/Mftf/Page/AdminNewCatalogRulePage.xml b/app/code/Magento/CatalogRule/Test/Mftf/Page/AdminNewCatalogRulePage.xml -new file mode 100644 -index 00000000000..c5307bf4e22 ---- /dev/null -+++ b/app/code/Magento/CatalogRule/Test/Mftf/Page/AdminNewCatalogRulePage.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="AdminNewCatalogRulePage" url="catalog_rule/promo_catalog/new/" module="Magento_CatalogRule" area="admin"> -+ </page> -+</pages> -diff --git a/app/code/Magento/CatalogRule/Test/Mftf/Page/CatalogRulePage.xml b/app/code/Magento/CatalogRule/Test/Mftf/Page/CatalogRulePage.xml -index e080a252e78..511a9ac0615 100644 ---- a/app/code/Magento/CatalogRule/Test/Mftf/Page/CatalogRulePage.xml -+++ b/app/code/Magento/CatalogRule/Test/Mftf/Page/CatalogRulePage.xml -@@ -7,7 +7,7 @@ - --> - - <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="CatalogRulePage" url="catalog_rule/promo_catalog/" module="Magento_CatalogRule" area="admin"> - <section name="AdminSecondaryGridSection"/> - </page> -diff --git a/app/code/Magento/CatalogRule/Test/Mftf/Section/AdminCatalogPriceRuleGridSection.xml b/app/code/Magento/CatalogRule/Test/Mftf/Section/AdminCatalogPriceRuleGridSection.xml -new file mode 100644 -index 00000000000..21f1401b624 ---- /dev/null -+++ b/app/code/Magento/CatalogRule/Test/Mftf/Section/AdminCatalogPriceRuleGridSection.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="AdminCatalogPriceRuleGridSection"> -+ <element name="filterByRuleName" type="input" selector="#promo_catalog_grid_filter_name"/> -+ <element name="attribute" type="text" selector="//*[@id='promo_catalog_grid_table']//td[contains(text(), '{{attributeValue}}')]" parameterized="true"/> -+ <element name="applyRulesButton" type="button" selector="#apply_rules" timeout="30"/> -+ </section> -+</sections> -diff --git a/app/code/Magento/CatalogRule/Test/Mftf/Section/AdminCatalogPriceRuleStagingSection.xml b/app/code/Magento/CatalogRule/Test/Mftf/Section/AdminCatalogPriceRuleStagingSection.xml -index d212aac1c0d..bab9842caaa 100644 ---- a/app/code/Magento/CatalogRule/Test/Mftf/Section/AdminCatalogPriceRuleStagingSection.xml -+++ b/app/code/Magento/CatalogRule/Test/Mftf/Section/AdminCatalogPriceRuleStagingSection.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="AdminCatalogPriceRuleStagingSection"> - <element name="status" type="select" selector=".modal-component [data-index='is_active'] select"/> - </section> -diff --git a/app/code/Magento/CatalogRule/Test/Mftf/Section/AdminNewCatalogPriceRuleSection.xml b/app/code/Magento/CatalogRule/Test/Mftf/Section/AdminNewCatalogPriceRuleSection.xml -index 4773fd8224f..5cb69cb6d7f 100644 ---- a/app/code/Magento/CatalogRule/Test/Mftf/Section/AdminNewCatalogPriceRuleSection.xml -+++ b/app/code/Magento/CatalogRule/Test/Mftf/Section/AdminNewCatalogPriceRuleSection.xml -@@ -7,12 +7,14 @@ - --> - - <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="AdminNewCatalogPriceRule"> - <element name="saveAndApply" type="button" selector="#save_and_apply" timeout="30"/> - <element name="save" type="button" selector="#save" timeout="30"/> - <element name="saveAndContinue" type="button" selector="#save_and_continue" timeout="30"/> - <element name="delete" type="button" selector="#delete" timeout="30"/> -+ <element name="okButton" type="button" selector="//button[@class='action-primary action-accept']"/> -+ <element name="successMessage" type="text" selector="#messages"/> - - <element name="ruleName" type="input" selector="[name='name']"/> - <element name="description" type="textarea" selector="[name='description']"/> -@@ -37,7 +39,25 @@ - <element name="disregardRules" type="select" selector="[name='stop_rules_processing']"/> - </section> - -+ <section name="AdminNewCatalogPriceRuleConditions"> -+ <element name="newCondition" type="button" selector=".rule-param.rule-param-new-child"/> -+ <element name="conditionSelect" type="select" selector="select#conditions__{{var}}__new_child" parameterized="true"/> -+ <element name="targetEllipsis" type="button" selector="//li[{{var}}]//a[@class='label'][text() = '...']" parameterized="true"/> -+ <element name="targetEllipsisValue" type="button" selector="//ul[@id='conditions__{{var}}__children']//a[contains(text(), '{{var1}}')]" parameterized="true" timeout="30"/> -+ <element name="ellipsisValue" type="button" selector="//ul[@id='conditions__{{var}}__children']//a[contains(text(), '...')]" parameterized="true" timeout="30"/> -+ <element name="targetEllipsisSelect" type="select" selector="select#conditions__{{var1}}--{{var2}}__value" parameterized="true" timeout="30"/> -+ <element name="targetSelect" type="select" selector="//ul[@id='conditions__{{var}}__children']//select" parameterized="true" timeout="30"/> -+ <element name="targetInput" type="input" selector="input#conditions__{{var1}}--{{var2}}__value" parameterized="true"/> -+ <element name="applyButton" type="button" selector="#conditions__{{var1}}__children li:nth-of-type({{var2}}) a.rule-param-apply" parameterized="true"/> -+ </section> -+ - <section name="AdminCatalogPriceRuleGrid"> - <element name="applyRules" type="button" selector="#apply_rules" timeout="30"/> -+ <element name="updateMessage" type="text" selector="//div[@class='message message-notice notice']//div[contains(.,'We found updated rules that are not applied. Please click')]"/> -+ <element name="ruleFilter" type="input" selector="//td[@data-column='name']/input[@name='name']"/> -+ <element name="resetFilter" type="button" selector="//button[@title='Reset Filter']" timeout="30"/> -+ <element name="search" type="button" selector="//div[@id='promo_catalog_grid']//button[@title='Search']" timeout="30"/> -+ <element name="selectRowByRuleName" type="text" selector="//tr[@data-role='row']//td[contains(.,'{{ruleName}}')]" parameterized="true"/> -+ <element name="firstRow" type="text" selector="//tr[@data-role='row']"/> - </section> - </sections> -diff --git a/app/code/Magento/CatalogRule/Test/Mftf/Test/AdminApplyCatalogRuleByCategoryTest.xml b/app/code/Magento/CatalogRule/Test/Mftf/Test/AdminApplyCatalogRuleByCategoryTest.xml -new file mode 100644 -index 00000000000..875a7842f21 ---- /dev/null -+++ b/app/code/Magento/CatalogRule/Test/Mftf/Test/AdminApplyCatalogRuleByCategoryTest.xml -@@ -0,0 +1,96 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> -+ <test name="AdminApplyCatalogRuleByCategoryTest"> -+ <annotations> -+ <features value="CatalogRule"/> -+ <stories value="Apply catalog price rule"/> -+ <title value="Admin should be able to apply the catalog rule by category"/> -+ <description value="Admin should be able to apply the catalog rule by category"/> -+ <severity value="MAJOR"/> -+ <testCaseId value="MC-74"/> -+ <group value="CatalogRule"/> -+ <skip> -+ <issueId value="MC-5777"/> -+ </skip> -+ </annotations> -+ <before> -+ <createData entity="ApiCategory" stepKey="createCategoryOne"/> -+ <createData entity="ApiSimpleProduct" stepKey="createSimpleProductOne"> -+ <requiredEntity createDataKey="createCategoryOne"/> -+ </createData> -+ <createData entity="ApiCategory" stepKey="createCategoryTwo"/> -+ <createData entity="ApiSimpleProduct" stepKey="createSimpleProductTwo"> -+ <requiredEntity createDataKey="createCategoryTwo"/> -+ </createData> -+ -+ <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> -+ </before> -+ <after> -+ <deleteData createDataKey="createCategoryOne" stepKey="deleteCategoryOne"/> -+ <deleteData createDataKey="createSimpleProductOne" stepKey="deleteSimpleProductOne"/> -+ <deleteData createDataKey="createCategoryTwo" stepKey="deleteCategoryTwo"/> -+ <deleteData createDataKey="createSimpleProductTwo" stepKey="deleteSimpleProductTwo"/> -+ -+ <!-- Delete the catalog price rule --> -+ <amOnPage stepKey="goToPriceRulePage" url="{{CatalogRulePage.url}}"/> -+ <actionGroup stepKey="deletePriceRule" ref="deleteEntitySecondaryGrid"> -+ <argument name="name" value="{{_defaultCatalogRule.name}}"/> -+ <argument name="searchInput" value="{{AdminSecondaryGridSection.catalogRuleIdentifierSearch}}"/> -+ </actionGroup> -+ -+ <amOnPage url="{{AdminLogoutPage.url}}" stepKey="amOnLogoutPage"/> -+ </after> -+ -+ <!-- 1. Begin creating a new catalog price rule --> -+ <amOnPage url="{{CatalogRulePage.url}}" stepKey="goToPriceRulePage"/> -+ <waitForPageLoad stepKey="waitForPriceRulePage"/> -+ <click selector="{{AdminGridMainControls.add}}" stepKey="addNewRule"/> -+ <waitForPageLoad stepKey="waitForIndividualRulePage"/> -+ <fillField selector="{{AdminNewCatalogPriceRule.ruleName}}" userInput="{{_defaultCatalogRule.name}}" stepKey="fillName"/> -+ <fillField selector="{{AdminNewCatalogPriceRule.description}}" userInput="{{_defaultCatalogRule.description}}" stepKey="fillDescription"/> -+ <selectOption selector="{{AdminNewCatalogPriceRule.websites}}" userInput="{{_defaultCatalogRule.website_ids[0]}}" stepKey="selectSite"/> -+ <click selector="{{AdminNewCatalogPriceRule.fromDateButton}}" stepKey="clickFromCalender"/> -+ <click selector="{{AdminNewCatalogPriceRule.todayDate}}" stepKey="clickFromToday"/> -+ <click selector="{{AdminNewCatalogPriceRule.toDateButton}}" stepKey="clickToCalender"/> -+ <click selector="{{AdminNewCatalogPriceRule.todayDate}}" stepKey="clickToToday"/> -+ <click selector="{{AdminNewCatalogPriceRule.conditionsTab}}" stepKey="openConditions"/> -+ <click selector="{{AdminNewCatalogPriceRuleConditions.newCondition}}" stepKey="clickNewRule"/> -+ <selectOption selector="{{AdminNewCatalogPriceRuleConditions.conditionSelect('1')}}" userInput="Category" stepKey="selectCategory"/> -+ <waitForPageLoad stepKey="waitForEllipsis"/> -+ <click selector="{{AdminNewCatalogPriceRuleConditions.targetEllipsis('1')}}" stepKey="clickEllipsis"/> -+ <waitForPageLoad stepKey="waitForInput"/> -+ -+ <!-- 2. Fill condition of category = createCategoryOne --> -+ <fillField selector="{{AdminNewCatalogPriceRuleConditions.targetInput('1', '1')}}" userInput="$$createCategoryOne.id$$" stepKey="fillCategory"/> -+ <click selector="{{AdminNewCatalogPriceRuleConditions.applyButton('1', '1')}}" stepKey="clickApply"/> -+ <click selector="{{AdminNewCatalogPriceRule.actionsTab}}" stepKey="openActionDropdown"/> -+ <selectOption selector="{{AdminNewCatalogPriceRuleActions.apply}}" userInput="{{_defaultCatalogRule.simple_action}}" stepKey="discountType"/> -+ <fillField selector="{{AdminNewCatalogPriceRuleActions.discountAmount}}" userInput="50" stepKey="fillDiscountValue"/> -+ <selectOption selector="{{AdminNewCatalogPriceRuleActions.disregardRules}}" userInput="Yes" stepKey="discardSubsequentRules"/> -+ <scrollToTopOfPage stepKey="scrollToTop"/> -+ <waitForPageLoad stepKey="waitForApplied"/> -+ <actionGroup ref="selectNotLoggedInCustomerGroup" stepKey="selectNotLoggedInCustomerGroup"/> -+ -+ <!-- 3. Save and apply the new catalog price rule --> -+ <click selector="{{AdminNewCatalogPriceRule.saveAndApply}}" stepKey="saveAndApply"/> -+ <magentoCLI command="indexer:reindex" stepKey="reindex"/> -+ <magentoCLI command="cache:flush" stepKey="flushCache"/> -+ -+ <!-- 4. Verify the storefront --> -+ <amOnPage url="$$createCategoryOne.name$$.html" stepKey="goToCategoryOne"/> -+ <see selector="{{StorefrontCategoryProductSection.ProductInfoByNumber('1')}}" userInput="$$createSimpleProductOne.name$$" stepKey="seeProductOne"/> -+ <see selector="{{StorefrontCategoryProductSection.ProductInfoByNumber('1')}}" userInput="$61.50" stepKey="seeProductOnePrice"/> -+ <see selector="{{StorefrontCategoryProductSection.ProductInfoByNumber('1')}}" userInput="Regular Price $123.00" stepKey="seeProductOneRegularPrice"/> -+ <amOnPage url="$$createCategoryTwo.name$$.html" stepKey="goToCategoryTwo"/> -+ <see selector="{{StorefrontCategoryProductSection.ProductInfoByNumber('1')}}" userInput="$$createSimpleProductTwo.name$$" stepKey="seeProductTwo"/> -+ <see selector="{{StorefrontCategoryProductSection.ProductInfoByNumber('1')}}" userInput="$123.00" stepKey="seeProductTwoPrice"/> -+ <dontSee selector="{{StorefrontCategoryProductSection.ProductInfoByNumber('1')}}" userInput="$61.50" stepKey="dontSeeDiscount"/> -+ </test> -+</tests> -diff --git a/app/code/Magento/CatalogRule/Test/Mftf/Test/AdminCreateCatalogPriceRuleTest.xml b/app/code/Magento/CatalogRule/Test/Mftf/Test/AdminCreateCatalogPriceRuleTest.xml -index 70fd7459865..10db68e9053 100644 ---- a/app/code/Magento/CatalogRule/Test/Mftf/Test/AdminCreateCatalogPriceRuleTest.xml -+++ b/app/code/Magento/CatalogRule/Test/Mftf/Test/AdminCreateCatalogPriceRuleTest.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="AdminCreateCatalogPriceRuleByPercentTest"> - <annotations> - <features value="CatalogRule"/> -@@ -17,6 +17,9 @@ - <severity value="MAJOR"/> - <testCaseId value="MC-65"/> - <group value="CatalogRule"/> -+ <skip> -+ <issueId value="MC-5777"/> -+ </skip> - </annotations> - <before> - <!-- Create the simple product and category that it will be in --> -@@ -28,7 +31,7 @@ - <!-- log in and create the price rule --> - <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> - <actionGroup stepKey="createNewPriceRule" ref="newCatalogPriceRuleByUI"/> -- <actionGroup stepKey="selectLoggedInCustomers" ref="selectNotLoggedInCustomerGroup"/> -+ <actionGroup stepKey="selectNotLoggedInCustomerGroup" ref="selectNotLoggedInCustomerGroup"/> - <click stepKey="saveAndApply" selector="{{AdminNewCatalogPriceRule.saveAndApply}}"/> - <see stepKey="assertSuccess" selector="{{AdminCategoryMessagesSection.SuccessMessage}}" userInput="You saved the rule."/> - </before> -@@ -65,7 +68,7 @@ - <see stepKey="seeNewPriceInCart" selector="{{CheckoutCartSummarySection.subtotal}}" userInput="$110.70"/> - </test> - -- <test name="AdminCreateCatalogPriceRuleByFixedTest"> -+ <test name="AdminCreateCatalogPriceRuleByFixedTest" extends="AdminCreateCatalogPriceRuleByPercentTest"> - <annotations> - <features value="CatalogRule"/> - <stories value="Create catalog price rule"/> -@@ -74,57 +77,24 @@ - <severity value="MAJOR"/> - <testCaseId value="MC-93"/> - <group value="CatalogRule"/> -+ <skip> -+ <issueId value="MC-5777"/> -+ </skip> - </annotations> - <before> -- <!-- Create the simple product and category that it will be in --> -- <createData entity="ApiCategory" stepKey="createCategory"/> -- <createData entity="ApiSimpleProduct" stepKey="createProduct"> -- <requiredEntity createDataKey="createCategory"/> -- </createData> -- -- <!-- log in and create the price rule --> -- <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> - <actionGroup stepKey="createNewPriceRule" ref="newCatalogPriceRuleByUI"> - <argument name="catalogRule" value="CatalogRuleByFixed"/> - </actionGroup> -- <actionGroup stepKey="selectLoggedInCustomers" ref="selectNotLoggedInCustomerGroup"/> -- <click stepKey="saveAndApply" selector="{{AdminNewCatalogPriceRule.saveAndApply}}"/> -- <see stepKey="assertSuccess" selector="{{AdminCategoryMessagesSection.SuccessMessage}}" userInput="You saved the rule."/> - </before> - <after> -- <!-- delete the simple product and catalog price rule and logout --> -- <amOnPage stepKey="goToPriceRulePage" url="admin/catalog_rule/promo_catalog/"/> - <actionGroup stepKey="deletePriceRule" ref="deleteEntitySecondaryGrid"> - <argument name="name" value="{{CatalogRuleByFixed.name}}"/> - <argument name="searchInput" value="{{AdminSecondaryGridSection.catalogRuleIdentifierSearch}}"/> - </actionGroup> -- <amOnPage url="{{AdminLogoutPage.url}}" stepKey="amOnLogoutPage"/> -- <deleteData createDataKey="createProduct" stepKey="deleteSimpleProduct"/> -- <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> - </after> -- -- <!-- Go to category page and make sure that all of the prices are correct --> -- <amOnPage url="$$createCategory.name$$.html" stepKey="goToCategoryPage"/> -- <waitForPageLoad stepKey="waitForCategory"/> -- <see stepKey="seeOldPrice" selector="{{StorefrontCategoryProductSection.ProductOldPriceByNumber('1')}}" userInput="$$createProduct.price$$"/> -- <see stepKey="seeNewPrice" selector="{{StorefrontCategoryProductSection.ProductSpecialPriceByNumber('1')}}" userInput="$113.00"/> -- -- <!-- Go to the simple product page and check that the prices are correct --> -- <amOnPage stepKey="goToProductPage" url="$$createProduct.sku$$.html"/> -- <waitForPageLoad stepKey="waitForProductPage"/> -- <see stepKey="seeOldPriceTag" selector="{{StorefrontProductInfoMainSection.oldPriceTag}}" userInput="Regular Price"/> -- <see stepKey="seeOldPrice2" selector="{{StorefrontProductInfoMainSection.oldPriceAmount}}" userInput="$$createProduct.price$$"/> -- <see stepKey="seeNewPrice2" selector="{{StorefrontProductInfoMainSection.updatedPrice}}" userInput="$113.00"/> -- -- <!-- Add the product to cart and check that the price is correct there --> -- <click stepKey="addToCart" selector="{{StorefrontProductActionSection.addToCart}}"/> -- <waitForPageLoad stepKey="waitForAddedToCart"/> -- <amOnPage url="{{CheckoutCartPage.url}}" stepKey="goToCheckout"/> -- <waitForPageLoad stepKey="waitForCart"/> -- <see stepKey="seeNewPriceInCart" selector="{{CheckoutCartSummarySection.subtotal}}" userInput="$113.00"/> - </test> - -- <test name="AdminCreateCatalogPriceRuleToPercentTest"> -+ <test name="AdminCreateCatalogPriceRuleToPercentTest" extends="AdminCreateCatalogPriceRuleByPercentTest"> - <annotations> - <features value="CatalogRule"/> - <stories value="Create catalog price rule"/> -@@ -133,57 +103,24 @@ - <severity value="MAJOR"/> - <testCaseId value="MC-69"/> - <group value="CatalogRule"/> -+ <skip> -+ <issueId value="MC-5777"/> -+ </skip> - </annotations> - <before> -- <!-- Create the simple product and category that it will be in --> -- <createData entity="ApiCategory" stepKey="createCategory"/> -- <createData entity="ApiSimpleProduct" stepKey="createProduct"> -- <requiredEntity createDataKey="createCategory"/> -- </createData> -- -- <!-- log in and create the price rule --> -- <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> - <actionGroup stepKey="createNewPriceRule" ref="newCatalogPriceRuleByUI"> - <argument name="catalogRule" value="CatalogRuleToPercent"/> - </actionGroup> -- <actionGroup stepKey="selectLoggedInCustomers" ref="selectNotLoggedInCustomerGroup"/> -- <click stepKey="saveAndApply" selector="{{AdminNewCatalogPriceRule.saveAndApply}}"/> -- <see stepKey="assertSuccess" selector="{{AdminCategoryMessagesSection.SuccessMessage}}" userInput="You saved the rule."/> - </before> - <after> -- <!-- delete the simple product and catalog price rule and logout --> -- <amOnPage stepKey="goToPriceRulePage" url="admin/catalog_rule/promo_catalog/"/> - <actionGroup stepKey="deletePriceRule" ref="deleteEntitySecondaryGrid"> - <argument name="name" value="{{CatalogRuleToPercent.name}}"/> - <argument name="searchInput" value="{{AdminSecondaryGridSection.catalogRuleIdentifierSearch}}"/> - </actionGroup> -- <amOnPage url="{{AdminLogoutPage.url}}" stepKey="amOnLogoutPage"/> -- <deleteData createDataKey="createProduct" stepKey="deleteSimpleProduct"/> -- <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> - </after> -- -- <!-- Go to category page and make sure that all of the prices are correct --> -- <amOnPage url="$$createCategory.name$$.html" stepKey="goToCategoryPage"/> -- <waitForPageLoad stepKey="waitForCategory"/> -- <see stepKey="seeOldPrice" selector="{{StorefrontCategoryProductSection.ProductOldPriceByNumber('1')}}" userInput="$$createProduct.price$$"/> -- <see stepKey="seeNewPrice" selector="{{StorefrontCategoryProductSection.ProductSpecialPriceByNumber('1')}}" userInput="$110.70"/> -- -- <!-- Go to the simple product page and check that the prices are correct --> -- <amOnPage stepKey="goToProductPage" url="$$createProduct.sku$$.html"/> -- <waitForPageLoad stepKey="waitForProductPage"/> -- <see stepKey="seeOldPriceTag" selector="{{StorefrontProductInfoMainSection.oldPriceTag}}" userInput="Regular Price"/> -- <see stepKey="seeOldPrice2" selector="{{StorefrontProductInfoMainSection.oldPriceAmount}}" userInput="$$createProduct.price$$"/> -- <see stepKey="seeNewPrice2" selector="{{StorefrontProductInfoMainSection.updatedPrice}}" userInput="$110.70"/> -- -- <!-- Add the product to cart and check that the price is correct there --> -- <click stepKey="addToCart" selector="{{StorefrontProductActionSection.addToCart}}"/> -- <waitForPageLoad stepKey="waitForAddedToCart"/> -- <amOnPage url="{{CheckoutCartPage.url}}" stepKey="goToCheckout"/> -- <waitForPageLoad stepKey="waitForCart"/> -- <see stepKey="seeNewPriceInCart" selector="{{CheckoutCartSummarySection.subtotal}}" userInput="$110.70"/> - </test> - -- <test name="AdminCreateCatalogPriceRuleToFixedTest"> -+ <test name="AdminCreateCatalogPriceRuleToFixedTest" extends="AdminCreateCatalogPriceRuleByPercentTest"> - <annotations> - <features value="CatalogRule"/> - <stories value="Create catalog price rule"/> -@@ -192,53 +129,77 @@ - <severity value="MAJOR"/> - <testCaseId value="MC-60"/> - <group value="CatalogRule"/> -+ <skip> -+ <issueId value="MC-5777"/> -+ </skip> - </annotations> - <before> -- <!-- Create the simple product and category that it will be in --> -- <createData entity="ApiCategory" stepKey="createCategory"/> -- <createData entity="ApiSimpleProduct" stepKey="createProduct"> -- <requiredEntity createDataKey="createCategory"/> -- </createData> -- -- <!-- log in and create the price rule --> -- <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> - <actionGroup stepKey="createNewPriceRule" ref="newCatalogPriceRuleByUI"> - <argument name="catalogRule" value="CatalogRuleToFixed"/> - </actionGroup> -- <actionGroup stepKey="selectLoggedInCustomers" ref="selectNotLoggedInCustomerGroup"/> -- <click stepKey="saveAndApply" selector="{{AdminNewCatalogPriceRule.saveAndApply}}"/> -- <see stepKey="assertSuccess" selector="{{AdminCategoryMessagesSection.SuccessMessage}}" userInput="You saved the rule."/> - </before> - <after> -- <!-- delete the simple product and catalog price rule and logout --> -- <amOnPage stepKey="goToPriceRulePage" url="admin/catalog_rule/promo_catalog/"/> - <actionGroup stepKey="deletePriceRule" ref="deleteEntitySecondaryGrid"> - <argument name="name" value="{{CatalogRuleToFixed.name}}"/> - <argument name="searchInput" value="{{AdminSecondaryGridSection.catalogRuleIdentifierSearch}}"/> - </actionGroup> -- <amOnPage url="{{AdminLogoutPage.url}}" stepKey="amOnLogoutPage"/> -- <deleteData createDataKey="createProduct" stepKey="deleteSimpleProduct"/> -- <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> - </after> -+ </test> - -- <!-- Go to category page and make sure that all of the prices are correct --> -- <amOnPage url="$$createCategory.name$$.html" stepKey="goToCategoryPage"/> -- <waitForPageLoad stepKey="waitForCategory"/> -- <see stepKey="seeOldPrice" selector="{{StorefrontCategoryProductSection.ProductOldPriceByNumber('1')}}" userInput="$$createProduct.price$$"/> -- <see stepKey="seeNewPrice" selector="{{StorefrontCategoryProductSection.ProductSpecialPriceByNumber('1')}}" userInput="$100.00"/> -- -- <!-- Go to the simple product page and check that the prices are correct --> -- <amOnPage stepKey="goToProductPage" url="$$createProduct.sku$$.html"/> -- <waitForPageLoad stepKey="waitForProductPage"/> -- <see stepKey="seeOldPriceTag" selector="{{StorefrontProductInfoMainSection.oldPriceTag}}" userInput="Regular Price"/> -- <see stepKey="seeOldPrice2" selector="{{StorefrontProductInfoMainSection.oldPriceAmount}}" userInput="$$createProduct.price$$"/> -- <see stepKey="seeNewPrice2" selector="{{StorefrontProductInfoMainSection.updatedPrice}}" userInput="$100.00"/> -+ <test name="AdminCreateCatalogPriceRuleForCustomerGroupTest"> -+ <annotations> -+ <features value="CatalogRule"/> -+ <stories value="Apply catalog price rule"/> -+ <title value="Admin should be able to apply the catalog rule by customer group"/> -+ <description value="Admin should be able to apply the catalog rule by customer group"/> -+ <severity value="MAJOR"/> -+ <testCaseId value="MC-71"/> -+ <group value="CatalogRule"/> -+ <skip> -+ <issueId value="MC-5777"/> -+ </skip> -+ </annotations> -+ <before> -+ <!-- Create a simple product and a category--> -+ <createData entity="ApiCategory" stepKey="createCategory"/> -+ <createData entity="ApiSimpleProduct" stepKey="createProduct"> -+ <requiredEntity createDataKey="createCategory"/> -+ </createData> -+ <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> -+ </before> -+ <after> -+ <!-- Delete the simple product and category --> -+ <deleteData createDataKey="createProduct" stepKey="deleteProduct"/> -+ <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> -+ <!-- Delete the catalog rule --> -+ <amOnPage url="{{CatalogRulePage.url}}" stepKey="goToRulePage"/> -+ <waitForPageLoad stepKey="waitForRulePage"/> -+ <actionGroup ref="deleteEntitySecondaryGrid" stepKey="deletePriceRule"> -+ <argument name="name" value="{{_defaultCatalogRule.name}}"/> -+ <argument name="searchInput" value="{{AdminSecondaryGridSection.catalogRuleIdentifierSearch}}"/> -+ </actionGroup> -+ <amOnPage url="{{AdminLogoutPage.url}}" stepKey="amOnLogoutPage"/> -+ </after> - -- <!-- Add the product to cart and check that the price is correct there --> -- <click stepKey="addToCart" selector="{{StorefrontProductActionSection.addToCart}}"/> -- <waitForPageLoad stepKey="waitForAddedToCart"/> -- <amOnPage url="{{CheckoutCartPage.url}}" stepKey="goToCheckout"/> -- <waitForPageLoad stepKey="waitForCart"/> -- <see stepKey="seeNewPriceInCart" selector="{{CheckoutCartSummarySection.subtotal}}" userInput="$100.00"/> -+ <!-- Create a catalog rule for the NOT LOGGED IN customer group --> -+ <actionGroup ref="newCatalogPriceRuleByUI" stepKey="createNewPriceRule"/> -+ <actionGroup ref="selectNotLoggedInCustomerGroup" stepKey="selectNotLoggedInCustomerGroup"/> -+ <click selector="{{AdminNewCatalogPriceRule.saveAndApply}}" stepKey="saveAndApply"/> -+ <see selector="{{AdminCategoryMessagesSection.SuccessMessage}}" userInput="You saved the rule." stepKey="assertSuccess"/> -+ -+ <!-- As a NOT LOGGED IN user, go to the storefront category page and should see the discount --> -+ <amOnPage url="$$createCategory.name$$.html" stepKey="goToCategory1"/> -+ <see selector="{{StorefrontCategoryProductSection.ProductInfoByNumber('1')}}" userInput="$$createProduct.name$$" stepKey="seeProduct1"/> -+ <see selector="{{StorefrontCategoryProductSection.ProductInfoByNumber('1')}}" userInput="$110.70" stepKey="seeDiscountedPrice1"/> -+ -+ <!-- Create a user account --> -+ <actionGroup ref="SignUpNewUserFromStorefrontActionGroup" stepKey="createAnAccount"> -+ <argument name="Customer" value="CustomerEntityOne"/> -+ </actionGroup> -+ -+ <!-- As a logged in user, go to the storefront category page and should NOT see discount --> -+ <amOnPage url="$$createCategory.name$$.html" stepKey="goToCategory2"/> -+ <see selector="{{StorefrontCategoryProductSection.ProductInfoByNumber('1')}}" userInput="$$createProduct.name$$" stepKey="seeProduct2"/> -+ <see selector="{{StorefrontCategoryProductSection.ProductInfoByNumber('1')}}" userInput="$123.00" stepKey="seeDiscountedPrice2"/> - </test> - </tests> -diff --git a/app/code/Magento/CatalogRule/Test/Mftf/Test/AdminCreateInactiveCatalogPriceRuleTest.xml b/app/code/Magento/CatalogRule/Test/Mftf/Test/AdminCreateInactiveCatalogPriceRuleTest.xml -new file mode 100644 -index 00000000000..5223b18df4e ---- /dev/null -+++ b/app/code/Magento/CatalogRule/Test/Mftf/Test/AdminCreateInactiveCatalogPriceRuleTest.xml -@@ -0,0 +1,75 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> -+ <test name="AdminCreateInactiveCatalogPriceRuleTest"> -+ <annotations> -+ <stories value="Create Catalog Price Rule"/> -+ <title value="Create Inactive Catalog Price Rule"/> -+ <description value="Login as admin and create inactive catalog price Rule"/> -+ <testCaseId value="MC-13977"/> -+ <severity value="CRITICAL"/> -+ <group value="mtf_migrated"/> -+ </annotations> -+ -+ <before> -+ <actionGroup ref="LoginAsAdmin" stepKey="loginToAdminPanel"/> -+ </before> -+ <after> -+ <actionGroup ref="AdminSearchCatalogRuleInGridActionGroup" stepKey="searchCreatedCatalogRule"> -+ <argument name="catalogRuleName" value="{{InactiveCatalogRule.name}}"/> -+ </actionGroup> -+ <actionGroup ref="AdminSelectCatalogRuleFromGridActionGroup" stepKey="selectCreatedCatalogRule"> -+ <argument name="catalogRuleName" value="{{InactiveCatalogRule.name}}"/> -+ </actionGroup> -+ <actionGroup ref="AdminDeleteCatalogRuleActionGroup" stepKey="deleteTheCatalogRule"/> -+ <actionGroup ref="logout" stepKey="logout"/> -+ </after> -+ -+ <!-- Create Inactive Catalog Price Rule --> -+ <actionGroup ref="AdminCreateNewCatalogPriceRuleActionGroup" stepKey="createCatalogPriceRule"> -+ <argument name="catalogRule" value="InactiveCatalogRule"/> -+ <argument name="customerGroup" value="General"/> -+ </actionGroup> -+ -+ <!-- Save and Apply Rules --> -+ <actionGroup ref="AdminSaveAndApplyRulesActionGroup" stepKey="saveAndApplyRules"/> -+ -+ <!-- Search Catalog Rule in Grid --> -+ <actionGroup ref="AdminSearchCatalogRuleInGridActionGroup" stepKey="searchAndSelectCreatedCatalogRule"> -+ <argument name="catalogRuleName" value="{{InactiveCatalogRule.name}}"/> -+ </actionGroup> -+ -+ <!--Select Catalog Rule in Grid --> -+ <actionGroup ref="AdminSelectCatalogRuleFromGridActionGroup" stepKey="selectCreatedCatalogRule"> -+ <argument name="catalogRuleName" value="{{InactiveCatalogRule.name}}"/> -+ </actionGroup> -+ <grabFromCurrentUrl stepKey="catalogRuleId" regex="#\/([0-9]*)?\/$#"/> -+ -+ <!-- Assert catalog Price Rule Form --> -+ <actionGroup ref="AssertCatalogPriceRuleFormActionGroup" stepKey="assertCatalogRuleForm"> -+ <argument name="catalogRule" value="InactiveCatalogRule"/> -+ <argument name="status" value="Inactive"/> -+ <argument name="websites" value="Main Website"/> -+ <argument name="customerGroup" value="General"/> -+ </actionGroup> -+ -+ <!-- Search Catalog Rule in Grid --> -+ <actionGroup ref="AdminSearchCatalogRuleInGridActionGroup" stepKey="searchCreatedCatalogRule"> -+ <argument name="catalogRuleName" value="{{InactiveCatalogRule.name}}"/> -+ </actionGroup> -+ -+ <!-- Assert Catalog Rule In Grid --> -+ <actionGroup ref="AssertCatalogRuleInGridActionGroup" stepKey="assertCatalogRuleInGrid"> -+ <argument name="catalogRuleName" value="{{InactiveCatalogRule.name}}"/> -+ <argument name="status" value="Inactive"/> -+ <argument name="websites" value="Main Website"/> -+ <argument name="catalogRuleId" value="$catalogRuleId"/> -+ </actionGroup> -+ </test> -+</tests> -diff --git a/app/code/Magento/CatalogRule/Test/Mftf/Test/AdminDeleteCatalogPriceRuleEntityTest.xml b/app/code/Magento/CatalogRule/Test/Mftf/Test/AdminDeleteCatalogPriceRuleEntityTest.xml -new file mode 100644 -index 00000000000..06392764290 ---- /dev/null -+++ b/app/code/Magento/CatalogRule/Test/Mftf/Test/AdminDeleteCatalogPriceRuleEntityTest.xml -@@ -0,0 +1,218 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> -+ <test name="AdminDeleteCatalogPriceRuleEntityFromSimpleProductTest"> -+ <annotations> -+ <stories value="Delete Catalog Price Rule"/> -+ <title value="Delete Catalog Price Rule for Simple Product"/> -+ <description value="Assert that Catalog Price Rule is not applied for simple product."/> -+ <testCaseId value="MC-14073"/> -+ <severity value="CRITICAL"/> -+ <group value="CatalogRule"/> -+ <group value="mtf_migrated"/> -+ </annotations> -+ -+ <before> -+ <createData entity="Simple_US_Customer" stepKey="createCustomer1"/> -+ <createData entity="_defaultCategory" stepKey="createCategory1"/> -+ <createData entity="SimpleProduct" stepKey="createProduct1"> -+ <requiredEntity createDataKey="createCategory1"/> -+ </createData> -+ -+ <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin1"/> -+ -+ <amOnPage url="{{AdminNewCatalogPriceRulePage.url}}" stepKey="openNewCatalogPriceRulePage"/> -+ <waitForPageLoad stepKey="waitForPageToLoad1"/> -+ -+ <actionGroup ref="CreateCatalogPriceRuleViaTheUi" stepKey="createCatalogPriceRuleViaTheUi1"> -+ <argument name="catalogRule" value="DeleteActiveCatalogPriceRuleWithConditions"/> -+ <argument name="customerGroup" value="General"/> -+ <argument name="disregardRules" value="Yes"/> -+ </actionGroup> -+ -+ <click selector="{{AdminNewCatalogPriceRule.save}}" stepKey="saveTheCatalogRule"/> -+ <waitForPageLoad stepKey="waitForPageToLoad3"/> -+ <see selector="{{AdminNewCatalogPriceRule.successMessage}}" userInput="You saved the rule." stepKey="seeSuccessMessage"/> -+ </before> -+ <after> -+ <actionGroup ref="logout" stepKey="logoutOfAdmin1"/> -+ -+ <deleteData createDataKey="createCustomer1" stepKey="deleteCustomer1"/> -+ <deleteData createDataKey="createProduct1" stepKey="deleteSimpleProduct1"/> -+ <deleteData createDataKey="createCategory1" stepKey="deleteCategoryFirst1"/> -+ </after> -+ -+ <!-- Delete the simple product and catalog price rule --> -+ <amOnPage url="{{CatalogRulePage.url}}" stepKey="goToPriceRulePage1"/> -+ <waitForPageLoad stepKey="waitForPriceRulePage"/> -+ <actionGroup ref="deleteEntitySecondaryGrid" stepKey="deletePriceRule1"> -+ <argument name="name" value="{{DeleteActiveCatalogPriceRuleWithConditions.name}}"/> -+ <argument name="searchInput" value="{{AdminSecondaryGridSection.catalogRuleIdentifierSearch}}"/> -+ </actionGroup> -+ <waitForPageLoad time="30" stepKey="waitForPageLoad1"/> -+ -+ <!-- Assert that the Success message is present after the delete --> -+ <see selector="{{AdminMessagesSection.successMessage}}" userInput="You deleted the rule." stepKey="seeDeletedRuleMessage1"/> -+ -+ <!-- Reindex --> -+ <magentoCLI command="cache:flush" stepKey="flushCache1"/> -+ <magentoCLI command="indexer:reindex" stepKey="reindex1"/> -+ -+ <!-- Assert that the rule isn't present on the Category page --> -+ <amOnPage url="$$createCategory1.name$$.html" stepKey="goToStorefrontCategoryPage1"/> -+ <waitForPageLoad stepKey="waitForPageLoad3"/> -+ <dontSee selector="{{StorefrontCategoryProductSection.ProductCatalogRulePriceTitleByName($$createProduct1.name$$)}}" userInput="Regular Price" stepKey="dontSeeRegularPriceText1"/> -+ <dontSeeElement selector="{{StorefrontCategoryProductSection.ProductCatalogRuleSpecialPriceTitleByName($$createProduct1.name$$)}}" stepKey="dontSeeSpecialPrice1"/> -+ -+ <!-- Assert that the rule isn't present on the Product page --> -+ <amOnPage url="$$createProduct1.name$$.html" stepKey="goToStorefrontProductPage1"/> -+ <waitForPageLoad stepKey="waitForPageLoad4"/> -+ <dontSee selector="{{StorefrontProductInfoMainSection.oldPriceTag}}" userInput="Regular Price" stepKey="dontSeeRegularPRiceText2"/> -+ <see selector="{{StorefrontProductInfoMainSection.productPrice}}" userInput="$$createProduct1.price$$" stepKey="seeTrueProductPrice1"/> -+ -+ <!-- Assert that the rule isn't present in the Shopping Cart --> -+ <actionGroup ref="addToCartFromStorefrontProductPage" stepKey="addProductToShoppingCart1"> -+ <argument name="productName" value="$$createProduct1.name$$"/> -+ </actionGroup> -+ <click selector="{{StorefrontMinicartSection.showCart}}" stepKey="openMiniShoppingCart1"/> -+ <see selector="{{StorefrontMinicartSection.productPriceByName($$createProduct1.name$$)}}" userInput="$$createProduct1.price$$" stepKey="seeCorrectProductPrice1"/> -+ -+ <!-- Assert that the rule isn't present on the Checkout page --> -+ <click selector="{{StorefrontMiniCartSection.goToCheckout}}" stepKey="goToCheckout1"/> -+ <conditionalClick selector="{{CheckoutCartSummarySection.expandShoppingCartSummary}}" dependentSelector="{{CheckoutCartSummarySection.expandShoppingCartSummary}}" visible="true" stepKey="expandShoppingCartSummary1"/> -+ <see selector="{{CheckoutCartProductSection.ProductRegularPriceByName($$createProduct1.name$$)}}" userInput="$$createProduct1.price$$" stepKey="seeCorrectProductPriceOnCheckout1"/> -+ </test> -+ -+ <test name="AdminDeleteCatalogPriceRuleEntityFromConfigurableProductTest"> -+ <annotations> -+ <stories value="Delete Catalog Price Rule"/> -+ <title value="Delete Catalog Price Rule for Configurable Product"/> -+ <description value="Assert that Catalog Price Rule is not applied for configurable product"/> -+ <testCaseId value="MC-14074"/> -+ <severity value="CRITICAL"/> -+ <group value="CatalogRule"/> -+ <group value="mtf_migrated"/> -+ </annotations> -+ -+ <before> -+ <createData entity="Simple_US_Customer" stepKey="createCustomer1"/> -+ <createData entity="SimpleSubCategory" stepKey="createCategory1"/> -+ -+ <!-- Create the configurable product based on the data in the /data folder --> -+ <createData entity="ApiConfigurableProduct" stepKey="createConfigProduct1"> -+ <requiredEntity createDataKey="createCategory1"/> -+ </createData> -+ -+ <!-- Make the configurable product have two options, that are children of the default attribute set --> -+ <createData entity="productAttributeWithTwoOptions" stepKey="createConfigProductAttribute1"/> -+ <createData entity="productAttributeOption1" stepKey="createConfigProductAttributeOption1"> -+ <requiredEntity createDataKey="createConfigProductAttribute1"/> -+ </createData> -+ <createData entity="productAttributeOption2" stepKey="createConfigProductAttributeOption2"> -+ <requiredEntity createDataKey="createConfigProductAttribute1"/> -+ </createData> -+ <createData entity="AddToDefaultSet" stepKey="createConfigAddToAttributeSet"> -+ <requiredEntity createDataKey="createConfigProductAttribute1"/> -+ </createData> -+ <getData entity="ProductAttributeOptionGetter" index="1" stepKey="getConfigAttributeOption1"> -+ <requiredEntity createDataKey="createConfigProductAttribute1"/> -+ </getData> -+ <getData entity="ProductAttributeOptionGetter" index="2" stepKey="getConfigAttributeOption2"> -+ <requiredEntity createDataKey="createConfigProductAttribute1"/> -+ </getData> -+ -+ <!-- Create the 2 children that will be a part of the configurable product --> -+ <createData entity="ApiSimpleOne" stepKey="createConfigChildProduct1"> -+ <requiredEntity createDataKey="createConfigProductAttribute1"/> -+ <requiredEntity createDataKey="getConfigAttributeOption1"/> -+ </createData> -+ <createData entity="ApiSimpleTwo" stepKey="createConfigChildProduct2"> -+ <requiredEntity createDataKey="createConfigProductAttribute1"/> -+ <requiredEntity createDataKey="getConfigAttributeOption2"/> -+ </createData> -+ -+ <!-- Assign the two products to the configurable product --> -+ <createData entity="ConfigurableProductTwoOptions" stepKey="createConfigProductOption1"> -+ <requiredEntity createDataKey="createConfigProduct1"/> -+ <requiredEntity createDataKey="createConfigProductAttribute1"/> -+ <requiredEntity createDataKey="getConfigAttributeOption1"/> -+ <requiredEntity createDataKey="getConfigAttributeOption2"/> -+ </createData> -+ <createData entity="ConfigurableProductAddChild" stepKey="createConfigProductAddChild1"> -+ <requiredEntity createDataKey="createConfigProduct1"/> -+ <requiredEntity createDataKey="createConfigChildProduct1"/> -+ </createData> -+ <createData entity="ConfigurableProductAddChild" stepKey="createConfigProductAddChild2"> -+ <requiredEntity createDataKey="createConfigProduct1"/> -+ <requiredEntity createDataKey="createConfigChildProduct2"/> -+ </createData> -+ -+ <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin1"/> -+ -+ <amOnPage url="{{AdminNewCatalogPriceRulePage.url}}" stepKey="openNewCatalogPriceRulePage"/> -+ <waitForPageLoad stepKey="waitForPageToLoad1"/> -+ -+ <actionGroup ref="CreateCatalogPriceRuleViaTheUi" stepKey="createCatalogPriceRuleViaTheUi1"> -+ <argument name="catalogRule" value="DeleteActiveCatalogPriceRuleWithConditions"/> -+ <argument name="customerGroup" value="General"/> -+ <argument name="disregardRules" value="Yes"/> -+ </actionGroup> -+ -+ <click selector="{{AdminNewCatalogPriceRule.save}}" stepKey="saveTheCatalogRule"/> -+ <waitForPageLoad stepKey="waitForPageToLoad3"/> -+ <see selector="{{AdminNewCatalogPriceRule.successMessage}}" userInput="You saved the rule." stepKey="seeSuccessMessage"/> -+ </before> -+ <after> -+ <actionGroup ref="logout" stepKey="logoutOfAdmin1"/> -+ -+ <deleteData createDataKey="createCustomer1" stepKey="deleteCustomer"/> -+ <deleteData createDataKey="createCategory1" stepKey="deleteCategory1"/> -+ <deleteData createDataKey="createConfigProduct1" stepKey="deleteConfigProduct1"/> -+ <deleteData createDataKey="createConfigChildProduct1" stepKey="deleteConfigChildProduct1"/> -+ <deleteData createDataKey="createConfigChildProduct2" stepKey="deleteConfigChildProduct2"/> -+ <deleteData createDataKey="createConfigProductAttribute1" stepKey="deleteConfigProductAttribute1"/> -+ </after> -+ -+ <!-- Delete the simple product and catalog price rule --> -+ <amOnPage url="{{CatalogRulePage.url}}" stepKey="goToPriceRulePage1"/> -+ <waitForPageLoad stepKey="waitForPriceRulePage"/> -+ <actionGroup ref="deleteEntitySecondaryGrid" stepKey="deletePriceRule1"> -+ <argument name="name" value="{{DeleteActiveCatalogPriceRuleWithConditions.name}}"/> -+ <argument name="searchInput" value="{{AdminSecondaryGridSection.catalogRuleIdentifierSearch}}"/> -+ </actionGroup> -+ <waitForPageLoad time="30" stepKey="waitForPageLoad1"/> -+ <see selector="{{AdminMessagesSection.successMessage}}" userInput="You deleted the rule." stepKey="seeDeletedRuleMessage1"/> -+ -+ <!-- Reindex --> -+ <magentoCLI command="cache:flush" stepKey="flushCache1"/> -+ <magentoCLI command="indexer:reindex" stepKey="reindex1"/> -+ -+ <!-- Assert that the rule isn't present on the Category page --> -+ <amOnPage url="$$createCategory1.name$$.html" stepKey="goToStorefrontCategoryPage1"/> -+ <waitForPageLoad stepKey="waitForPageLoad2"/> -+ <see selector="{{StorefrontCategoryProductSection.ProductPriceByName($$createConfigProduct1.name$$)}}" userInput="$$createConfigChildProduct1.price$$" stepKey="seeRegularPriceText1"/> -+ -+ <!-- Assert that the rule isn't present on the Product page --> -+ <amOnPage url="{{StorefrontProductPage.url($$createConfigProduct1.custom_attributes[url_key]$$)}}" stepKey="goToStorefrontProductPage1"/> -+ <waitForPageLoad stepKey="waitForPageLoad3"/> -+ <dontSee selector="{{StorefrontProductInfoMainSection.oldPriceTag}}" userInput="Regular Price" stepKey="dontSeeRegularPriceText2"/> -+ <see selector="{{StorefrontProductInfoMainSection.productPrice}}" userInput="$$createConfigChildProduct1.price$$" stepKey="seeTrueProductPrice1"/> -+ -+ <!-- Assert that the rule isn't present in the Shopping Cart --> -+ <selectOption selector="{{StorefrontProductInfoMainSection.productAttributeOptionsSelectButton}}" userInput="option1" stepKey="selectOption1"/> -+ <click selector="{{StorefrontProductActionSection.addToCart}}" stepKey="addToCart1"/> -+ <waitForPageLoad time="30" stepKey="waitForPageLoad4"/> -+ <waitForElementVisible selector="{{StorefrontMessagesSection.success}}" stepKey="waitForSuccessMessage"/> -+ <see selector="{{StorefrontMessagesSection.success}}" userInput="You added $$createConfigProduct1.name$ to your shopping cart." stepKey="seeAddToCartSuccessMessage"/> -+ <click selector="{{StorefrontMinicartSection.showCart}}" stepKey="openMiniShoppingCart1"/> -+ <waitForPageLoad time="30" stepKey="waitForPageLoad5"/> -+ <see selector="{{StorefrontMinicartSection.productPriceByName($$createConfigProduct1.name$$)}}" userInput="$$createConfigProduct1.price$$" stepKey="seeCorrectProductPrice1"/> -+ </test> -+</tests> -diff --git a/app/code/Magento/CatalogRule/Test/Mftf/Test/AdminDeleteCatalogPriceRuleTest.xml b/app/code/Magento/CatalogRule/Test/Mftf/Test/AdminDeleteCatalogPriceRuleTest.xml -new file mode 100644 -index 00000000000..06f3682aedd ---- /dev/null -+++ b/app/code/Magento/CatalogRule/Test/Mftf/Test/AdminDeleteCatalogPriceRuleTest.xml -@@ -0,0 +1,101 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> -+ <test name="AdminDeleteCatalogPriceRuleTest"> -+ <annotations> -+ <features value="CatalogRule"/> -+ <stories value="Delete Catalog Price Rule"/> -+ <title value="Admin should be able to delete catalog price rule"/> -+ <description value="Admin should be able to delete catalog price rule"/> -+ <severity value="MAJOR"/> -+ <testCaseId value="MC-160"/> -+ <group value="CatalogRule"/> -+ <skip> -+ <issueId value="MC-5777"/> -+ </skip> -+ </annotations> -+ <before> -+ <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> -+ <createData entity="ApiCategory" stepKey="createCategory"/> -+ -+ <!-- Create a simple product --> -+ <createData entity="ApiSimpleProduct" stepKey="createSimpleProduct"> -+ <requiredEntity createDataKey="createCategory"/> -+ </createData> -+ -+ <!-- Create a configurable product --> -+ <actionGroup ref="createConfigurableProduct" stepKey="createConfigurableProduct"> -+ <argument name="product" value="_defaultProduct"/> -+ <argument name="category" value="$$createCategory$$"/> -+ </actionGroup> -+ </before> -+ <after> -+ <amOnPage url="{{AdminLogoutPage.url}}" stepKey="amOnLogoutPage"/> -+ <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> -+ <deleteData createDataKey="createSimpleProduct" stepKey="deleteSimpleProduct"/> -+ </after> -+ -+ <!-- Create a catalog price rule --> -+ <actionGroup ref="newCatalogPriceRuleByUI" stepKey="createNewPriceRule"/> -+ <actionGroup ref="selectNotLoggedInCustomerGroup" stepKey="selectNotLoggedInCustomerGroup"/> -+ <click selector="{{AdminNewCatalogPriceRule.saveAndApply}}" stepKey="saveAndApply"/> -+ <see selector="{{AdminCategoryMessagesSection.SuccessMessage}}" userInput="You saved the rule." stepKey="assertSuccess"/> -+ -+ <!-- Verify that category page shows the discount --> -+ <amOnPage url="$$createCategory.name$$.html" stepKey="goToCategoryPage1"/> -+ <see selector="{{StorefrontCategoryProductSection.ProductTitleByName(ApiSimpleProduct.name)}}" userInput="$$createSimpleProduct.name$$" stepKey="seeSimpleProduct1"/> -+ <see selector="{{StorefrontCategoryProductSection.ProductPriceByName(ApiSimpleProduct.name)}}" userInput="$110.70" stepKey="seeSimpleProductDiscount1"/> -+ <see selector="{{StorefrontCategoryProductSection.ProductTitleByName(_defaultProduct.name)}}" userInput="{{_defaultProduct.name}}" stepKey="seeConfigurableProduct1"/> -+ <see selector="{{StorefrontCategoryProductSection.ProductPriceByName(_defaultProduct.name)}}" userInput="$0.90" stepKey="seeConfigurableProductDiscount1"/> -+ -+ <!-- Verify that the simple product page shows the discount --> -+ <amOnPage url="$$createSimpleProduct.custom_attributes[url_key]$$.html" stepKey="goToSimpleProductPage1"/> -+ <see selector="{{StorefrontProductInfoMainSection.productName}}" userInput="$$createSimpleProduct.name$$" stepKey="seeCorrectName1"/> -+ <see selector="{{StorefrontProductInfoMainSection.productSku}}" userInput="$$createSimpleProduct.sku$$" stepKey="seeCorrectSku1"/> -+ <see selector="{{StorefrontProductInfoMainSection.productPrice}}" userInput="$110.70" stepKey="seeCorrectPrice1"/> -+ -+ <!-- Verify that the configurable product page the catalog price rule discount --> -+ <amOnPage url="{{_defaultProduct.urlKey}}.html" stepKey="goToConfigurableProductPage1"/> -+ <see selector="{{StorefrontProductInfoMainSection.productName}}" userInput="{{_defaultProduct.name}}" stepKey="seeCorrectName2"/> -+ <see selector="{{StorefrontProductInfoMainSection.productSku}}" userInput="{{_defaultProduct.sku}}" stepKey="seeCorrectSku2"/> -+ <see selector="{{StorefrontProductInfoMainSection.productPrice}}" userInput="$0.90" stepKey="seeCorrectPrice2"/> -+ -+ <!-- Delete the rule --> -+ <amOnPage url="{{CatalogRulePage.url}}" stepKey="goToPriceRulePage"/> -+ <actionGroup ref="deleteEntitySecondaryGrid" stepKey="deletePriceRule"> -+ <argument name="name" value="{{_defaultCatalogRule.name}}"/> -+ <argument name="searchInput" value="{{AdminSecondaryGridSection.catalogRuleIdentifierSearch}}"/> -+ </actionGroup> -+ -+ <!-- Apply and flush the cache --> -+ <click selector="{{AdminCatalogPriceRuleGrid.applyRules}}" stepKey="clickApplyRules"/> -+ <magentoCLI command="indexer:reindex" stepKey="reindex"/> -+ <magentoCLI command="cache:flush" stepKey="flushCache"/> -+ -+ <!-- Verify that category page shows the original prices --> -+ <amOnPage url="$$createCategory.name$$.html" stepKey="goToCategoryPage2"/> -+ <see selector="{{StorefrontCategoryProductSection.ProductTitleByName(ApiSimpleProduct.name)}}" userInput="$$createSimpleProduct.name$$" stepKey="seeSimpleProduct2"/> -+ <see selector="{{StorefrontCategoryProductSection.ProductPriceByName(ApiSimpleProduct.name)}}" userInput="$123.00" stepKey="seeSimpleProductDiscount2"/> -+ <see selector="{{StorefrontCategoryProductSection.ProductTitleByName(_defaultProduct.name)}}" userInput="{{_defaultProduct.name}}" stepKey="seeConfigurableProduct2"/> -+ <see selector="{{StorefrontCategoryProductSection.ProductPriceByName(_defaultProduct.name)}}" userInput="$1.00" stepKey="seeConfigurableProductDiscount2"/> -+ -+ <!-- Verify that the simple product page shows the original price --> -+ <amOnPage url="$$createSimpleProduct.custom_attributes[url_key]$$.html" stepKey="goToSimpleProductPage2"/> -+ <see selector="{{StorefrontProductInfoMainSection.productName}}" userInput="$$createSimpleProduct.name$$" stepKey="seeCorrectName3"/> -+ <see selector="{{StorefrontProductInfoMainSection.productSku}}" userInput="$$createSimpleProduct.sku$$" stepKey="seeCorrectSku3"/> -+ <see selector="{{StorefrontProductInfoMainSection.productPrice}}" userInput="$123.00" stepKey="seeCorrectPrice3"/> -+ -+ <!-- Verify that the configurable product page shows the original price --> -+ <amOnPage url="{{_defaultProduct.urlKey}}.html" stepKey="goToConfigurableProductPage2"/> -+ <see selector="{{StorefrontProductInfoMainSection.productName}}" userInput="{{_defaultProduct.name}}" stepKey="seeCorrectName4"/> -+ <see selector="{{StorefrontProductInfoMainSection.productSku}}" userInput="{{_defaultProduct.sku}}" stepKey="seeCorrectSku4"/> -+ <see selector="{{StorefrontProductInfoMainSection.productPrice}}" userInput="$1.00" stepKey="seeCorrectPrice4"/> -+ </test> -+</tests> -diff --git a/app/code/Magento/CatalogRule/Test/Mftf/Test/AdminEnableAttributeIsUndefinedCatalogPriceRuleTest.xml b/app/code/Magento/CatalogRule/Test/Mftf/Test/AdminEnableAttributeIsUndefinedCatalogPriceRuleTest.xml -new file mode 100644 -index 00000000000..0ff7e0cd852 ---- /dev/null -+++ b/app/code/Magento/CatalogRule/Test/Mftf/Test/AdminEnableAttributeIsUndefinedCatalogPriceRuleTest.xml -@@ -0,0 +1,137 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> -+ <test name="AdminEnableAttributeIsUndefinedCatalogPriceRuleTest"> -+ <annotations> -+ <features value="CatalogRule"/> -+ <stories value="Catalog price rule"/> -+ <title value="Enable 'is undefined' condition to Scope Catalog Price rules by custom product attribute"/> -+ <description value="Enable 'is undefined' condition to Scope Catalog Price rules by custom product attribute"/> -+ <severity value="CRITICAL"/> -+ <testCaseId value="MC-13654"/> -+ <useCaseId value="MC-10971"/> -+ <group value="CatalogRule"/> -+ </annotations> -+ <before> -+ <actionGroup ref="LoginAsAdmin" stepKey="login"/> -+ -+ <createData entity="ApiCategory" stepKey="createFirstCategory"/> -+ <createData entity="ApiSimpleProduct" stepKey="createFirstProduct"> -+ <requiredEntity createDataKey="createFirstCategory"/> -+ </createData> -+ <createData entity="SimpleProduct" stepKey="createSecondProduct"> -+ <requiredEntity createDataKey="createFirstCategory"/> -+ </createData> -+ <createData entity="productYesNoAttribute" stepKey="createProductAttribute"/> -+ <createData entity="AddToDefaultSet" stepKey="addToAttributeSetHandle"> -+ <requiredEntity createDataKey="createProductAttribute"/> -+ </createData> -+ -+ <createData entity="SimpleSubCategory" stepKey="createSecondCategory"/> -+ <createData entity="SimpleProduct3" stepKey="createThirdProduct"> -+ <requiredEntity createDataKey="createSecondCategory"/> -+ </createData> -+ <createData entity="SimpleProduct4" stepKey="createForthProduct"> -+ <requiredEntity createDataKey="createSecondCategory"/> -+ </createData> -+ <createData entity="productDropDownAttribute" stepKey="createSecondProductAttribute"> -+ <field key="scope">website</field> -+ </createData> -+ </before> -+ <after> -+ -+ <!--Delete created data--> -+ <amOnPage url="{{CatalogRulePage.url}}" stepKey="goToCatalogPriceRulePage"/> -+ <actionGroup ref="deleteEntitySecondaryGrid" stepKey="deletePriceRule"> -+ <argument name="name" value="{{CatalogRuleWithAllCustomerGroups.name}}"/> -+ <argument name="searchInput" value="{{AdminSecondaryGridSection.catalogRuleIdentifierSearch}}"/> -+ </actionGroup> -+ <click stepKey="resetFilters" selector="{{AdminSecondaryGridSection.resetFilters}}"/> -+ <deleteData createDataKey="createFirstProduct" stepKey="deleteFirstProduct"/> -+ <deleteData createDataKey="createSecondProduct" stepKey="deleteSecondProduct"/> -+ <deleteData createDataKey="createFirstCategory" stepKey="deleteFirstCategory"/> -+ <deleteData createDataKey="createThirdProduct" stepKey="deleteThirdProduct"/> -+ <deleteData createDataKey="createForthProduct" stepKey="deleteForthProduct"/> -+ <deleteData createDataKey="createSecondCategory" stepKey="deleteSecondCategory"/> -+ <deleteData createDataKey="createSecondProductAttribute" stepKey="deleteSecondProductAttribute"/> -+ -+ <actionGroup ref="logout" stepKey="logout"/> -+ </after> -+ -+ <!--Create catalog price rule--> -+ <amOnPage url="{{CatalogRulePage.url}}" stepKey="goToPriceRulePage"/> -+ <waitForPageLoad stepKey="waitForPriceRulePage"/> -+ <actionGroup ref="createCatalogPriceRule" stepKey="createCatalogPriceRule"> -+ <argument name="catalogRule" value="CatalogRuleWithAllCustomerGroups"/> -+ </actionGroup> -+ <actionGroup ref="selectNotLoggedInCustomerGroupActionGroup" stepKey="selectCustomerGroup"/> -+ <actionGroup ref="CreateCatalogPriceRuleConditionWithAttribute" stepKey="createCatalogPriceRuleCondition"> -+ <argument name="attributeName" value="$$createProductAttribute.attribute[frontend_labels][0][label]$$"/> -+ <argument name="targetValue" value="is"/> -+ <argument name="targetSelectValue" value="is undefined"/> -+ </actionGroup> -+ <click selector="{{AdminNewCatalogPriceRule.saveAndApply}}" stepKey="clickSaveAndApplyRules"/> -+ <magentoCLI command="indexer:reindex" stepKey="reindex"/> -+ <magentoCLI command="cache:flush" stepKey="flushCache"/> -+ -+ <!--Check Catalog Price Rule for first product--> -+ <amOnPage url="{{StorefrontProductPage.url($$createFirstProduct.custom_attributes[url_key]$$)}}" stepKey="navigateToFirstProductPage"/> -+ <waitForPageLoad stepKey="waitForFirstProductPageLoad"/> -+ <grabTextFrom selector="{{StorefrontProductInfoMainSection.updatedPrice}}" stepKey="grabFirstProductUpdatedPrice"/> -+ <assertEquals expected='$110.70' expectedType="string" actual="($grabFirstProductUpdatedPrice)" stepKey="assertFirstProductUpdatedPrice"/> -+ -+ <!--Check Catalog Price Rule for second product--> -+ <amOnPage url="{{StorefrontProductPage.url($$createSecondProduct.custom_attributes[url_key]$$)}}" stepKey="navigateToSecondProductPage"/> -+ <waitForPageLoad stepKey="waitForSecondProductPageLoad"/> -+ <grabTextFrom selector="{{StorefrontProductInfoMainSection.updatedPrice}}" stepKey="grabSecondProductUpdatedPrice"/> -+ <assertEquals expected='$110.70' expectedType="string" actual="($grabFirstProductUpdatedPrice)" stepKey="assertSecondProductUpdatedPrice"/> -+ -+ <!--Delete previous attribute and Catalog Price Rule--> -+ <deleteData createDataKey="createProductAttribute" stepKey="deleteProductAttribute"/> -+ <amOnPage url="{{CatalogRulePage.url}}" stepKey="goToCatalogPriceRulePage"/> -+ <actionGroup ref="deleteEntitySecondaryGrid" stepKey="deletePriceRule"> -+ <argument name="name" value="{{CatalogRuleWithAllCustomerGroups.name}}"/> -+ <argument name="searchInput" value="{{AdminSecondaryGridSection.catalogRuleIdentifierSearch}}"/> -+ </actionGroup> -+ -+ <!--Add new attribute to Default set--> -+ <createData entity="AddToDefaultSet" stepKey="addToAttributeSetHandle1"> -+ <requiredEntity createDataKey="createSecondProductAttribute"/> -+ </createData> -+ -+ <!--Create new Catalog Price Rule--> -+ <amOnPage url="{{CatalogRulePage.url}}" stepKey="goToPriceRulePage1"/> -+ <waitForPageLoad stepKey="waitForPriceRulePage1"/> -+ <actionGroup ref="createCatalogPriceRule" stepKey="createCatalogPriceRule1"> -+ <argument name="catalogRule" value="CatalogRuleWithAllCustomerGroups"/> -+ </actionGroup> -+ <actionGroup ref="selectNotLoggedInCustomerGroupActionGroup" stepKey="selectCustomerGroup1"/> -+ <actionGroup ref="CreateCatalogPriceRuleConditionWithAttribute" stepKey="createCatalogPriceRuleCondition1"> -+ <argument name="attributeName" value="$$createSecondProductAttribute.attribute[frontend_labels][0][label]$$"/> -+ <argument name="targetValue" value="is"/> -+ <argument name="targetSelectValue" value="is undefined"/> -+ </actionGroup> -+ <click selector="{{AdminNewCatalogPriceRule.saveAndApply}}" stepKey="clickSaveAndApplyRules1"/> -+ <magentoCLI command="indexer:reindex" stepKey="reindex1"/> -+ <magentoCLI command="cache:flush" stepKey="flushCache1"/> -+ -+ <!--Check Catalog Price Rule for third product--> -+ <amOnPage url="{{StorefrontProductPage.url($$createThirdProduct.custom_attributes[url_key]$$)}}" stepKey="navigateToThirdProductPage"/> -+ <waitForPageLoad stepKey="waitForThirdProductPageLoad"/> -+ <grabTextFrom selector="{{StorefrontProductInfoMainSection.updatedPrice}}" stepKey="grabThirdProductUpdatedPrice"/> -+ <assertEquals expected='$110.70' expectedType="string" actual="($grabThirdProductUpdatedPrice)" stepKey="assertThirdProductUpdatedPrice"/> -+ -+ <!--Check Catalog Price Rule for forth product--> -+ <amOnPage url="{{StorefrontProductPage.url($$createForthProduct.custom_attributes[url_key]$$)}}" stepKey="navigateToForthProductPage"/> -+ <waitForPageLoad stepKey="waitForForthProductPageLoad"/> -+ <grabTextFrom selector="{{StorefrontProductInfoMainSection.updatedPrice}}" stepKey="grabForthProductUpdatedPrice"/> -+ <assertEquals expected='$110.70' expectedType="string" actual="($grabForthProductUpdatedPrice)" stepKey="assertForthProductUpdatedPrice"/> -+ </test> -+</tests> -diff --git a/app/code/Magento/CatalogRule/Test/Mftf/Test/AdminMarketingCatalogPriceRuleNavigateMenuTest.xml b/app/code/Magento/CatalogRule/Test/Mftf/Test/AdminMarketingCatalogPriceRuleNavigateMenuTest.xml -new file mode 100644 -index 00000000000..0fe35419aaf ---- /dev/null -+++ b/app/code/Magento/CatalogRule/Test/Mftf/Test/AdminMarketingCatalogPriceRuleNavigateMenuTest.xml -@@ -0,0 +1,36 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> -+ <test name="AdminMarketingCatalogPriceRuleNavigateMenuTest"> -+ <annotations> -+ <features value="CatalogRule"/> -+ <stories value="Menu Navigation"/> -+ <title value="Admin marketing catalog price rule navigate menu test"/> -+ <description value="Admin should be able to navigate to Marketing > Catalog Price Rule"/> -+ <severity value="CRITICAL"/> -+ <testCaseId value="MC-14134"/> -+ <group value="menu"/> -+ <group value="mtf_migrated"/> -+ </annotations> -+ <before> -+ <actionGroup ref="LoginAsAdmin" stepKey="LoginAsAdmin"/> -+ </before> -+ <after> -+ <actionGroup ref="logout" stepKey="logout"/> -+ </after> -+ <actionGroup ref="AdminNavigateMenuActionGroup" stepKey="navigateToCatalogPriceRulePage"> -+ <argument name="menuUiId" value="{{AdminMenuMarketing.dataUiId}}"/> -+ <argument name="submenuUiId" value="{{AdminMenuMarketingPromotionsCatalogPriceRule.dataUiId}}"/> -+ </actionGroup> -+ <actionGroup ref="AdminAssertPageTitleActionGroup" stepKey="seePageTitle"> -+ <argument name="title" value="{{AdminMenuMarketingPromotionsCatalogPriceRule.pageTitle}}"/> -+ </actionGroup> -+ </test> -+</tests> -diff --git a/app/code/Magento/CatalogRule/Test/Mftf/Test/ApplyCatalogRuleForSimpleAndConfigurableProductTest.xml b/app/code/Magento/CatalogRule/Test/Mftf/Test/ApplyCatalogRuleForSimpleAndConfigurableProductTest.xml -new file mode 100644 -index 00000000000..b9e7bfb4d41 ---- /dev/null -+++ b/app/code/Magento/CatalogRule/Test/Mftf/Test/ApplyCatalogRuleForSimpleAndConfigurableProductTest.xml -@@ -0,0 +1,182 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> -+ <test name="ApplyCatalogPriceRuleForSimpleProductAndConfigurableProductTest"> -+ <annotations> -+ <features value="CatalogRule"/> -+ <stories value="Apply catalog price rule"/> -+ <title value="Admin should be able to apply the catalog price rule for simple product and configurable product"/> -+ <description value="Admin should be able to apply the catalog price rule for simple product and configurable product"/> -+ <severity value="CRITICAL"/> -+ <testCaseId value="MC-14770"/> -+ <group value="CatalogRule"/> -+ <group value="mtf_migrated"/> -+ <skip> -+ <issueId value="MC-17140"/> -+ </skip> -+ </annotations> -+ <before> -+ <!-- Login as Admin --> -+ <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> -+ -+ <!-- Create Simple Product --> -+ <createData entity="ApiCategory" stepKey="createCategory"/> -+ <createData entity="_defaultProduct" stepKey="createSimpleProduct"> -+ <requiredEntity createDataKey="createCategory"/> -+ <field key="price">56.78</field> -+ </createData> -+ -+ <!-- Create the configurable product based on the data in the /data folder --> -+ <createData entity="ApiConfigurableProduct" stepKey="createConfigProduct"> -+ <requiredEntity createDataKey="createCategory"/> -+ </createData> -+ -+ <!-- Make the configurable product have two options, that are children of the default attribute set --> -+ <createData entity="productAttributeWithTwoOptions" stepKey="createConfigProductAttribute"/> -+ <createData entity="productAttributeOption1" stepKey="createConfigProductAttributeOption1"> -+ <requiredEntity createDataKey="createConfigProductAttribute"/> -+ </createData> -+ <createData entity="productAttributeOption2" stepKey="createConfigProductAttributeOption2"> -+ <requiredEntity createDataKey="createConfigProductAttribute"/> -+ </createData> -+ <createData entity="AddToDefaultSet" stepKey="createConfigAddToAttributeSet"> -+ <requiredEntity createDataKey="createConfigProductAttribute"/> -+ </createData> -+ <getData entity="ProductAttributeOptionGetter" index="1" stepKey="getConfigAttributeOption1"> -+ <requiredEntity createDataKey="createConfigProductAttribute"/> -+ </getData> -+ <getData entity="ProductAttributeOptionGetter" index="2" stepKey="getConfigAttributeOption2"> -+ <requiredEntity createDataKey="createConfigProductAttribute"/> -+ </getData> -+ -+ <!-- Create the 2 children that will be a part of the configurable product --> -+ <createData entity="ApiSimpleOne" stepKey="createConfigChildProduct1"> -+ <requiredEntity createDataKey="createConfigProductAttribute"/> -+ <requiredEntity createDataKey="getConfigAttributeOption1"/> -+ </createData> -+ <createData entity="ApiSimpleTwo" stepKey="createConfigChildProduct2"> -+ <requiredEntity createDataKey="createConfigProductAttribute"/> -+ <requiredEntity createDataKey="getConfigAttributeOption2"/> -+ </createData> -+ -+ <!-- Assign the two products to the configurable product --> -+ <createData entity="ConfigurableProductTwoOptions" stepKey="createConfigProductOption"> -+ <requiredEntity createDataKey="createConfigProduct"/> -+ <requiredEntity createDataKey="createConfigProductAttribute"/> -+ <requiredEntity createDataKey="getConfigAttributeOption1"/> -+ <requiredEntity createDataKey="getConfigAttributeOption2"/> -+ </createData> -+ <createData entity="ConfigurableProductAddChild" stepKey="createConfigProductAddChild1"> -+ <requiredEntity createDataKey="createConfigProduct"/> -+ <requiredEntity createDataKey="createConfigChildProduct1"/> -+ </createData> -+ <createData entity="ConfigurableProductAddChild" stepKey="createConfigProductAddChild2"> -+ <requiredEntity createDataKey="createConfigProduct"/> -+ <requiredEntity createDataKey="createConfigChildProduct2"/> -+ </createData> -+ </before> -+ <after> -+ <!-- Delete the catalog price rule --> -+ <amOnPage stepKey="goToPriceRulePage" url="{{CatalogRulePage.url}}"/> -+ <actionGroup stepKey="deletePriceRule" ref="deleteEntitySecondaryGrid"> -+ <argument name="name" value="{{_defaultCatalogRule.name}}"/> -+ <argument name="searchInput" value="{{AdminSecondaryGridSection.catalogRuleIdentifierSearch}}"/> -+ </actionGroup> -+ -+ <!-- Logout --> -+ <actionGroup ref="logout" stepKey="logout"/> -+ -+ <!-- Delete products and category --> -+ <deleteData createDataKey="createSimpleProduct" stepKey="deleteSimpleProduct"/> -+ <deleteData createDataKey="createConfigProduct" stepKey="deleteConfigProduct"/> -+ <deleteData createDataKey="createConfigChildProduct1" stepKey="deleteConfigChildProduct1"/> -+ <deleteData createDataKey="createConfigChildProduct2" stepKey="deleteConfigChildProduct2"/> -+ <deleteData createDataKey="createConfigProductAttribute" stepKey="deleteConfigProductAttribute"/> -+ <deleteData createDataKey="createCategory" stepKey="deleteApiCategory"/> -+ </after> -+ <!-- Begin creating a new catalog price rule --> -+ <actionGroup ref="newCatalogPriceRuleByUIWithConditionIsCategory" stepKey="newCatalogPriceRuleByUIWithConditionIsCategory"> -+ <argument name ="categoryId" value="$createCategory.id$"/> -+ </actionGroup> -+ -+ <!-- Select not logged in customer group --> -+ <actionGroup ref="selectNotLoggedInCustomerGroupActionGroup" stepKey="selectNotLoggedInCustomerGroup"/> -+ -+ <!-- Save and apply the new catalog price rule --> -+ <actionGroup ref="SaveAndApplyCatalogPriceRuleActionGroup" stepKey="saveAndApplyCatalogPriceRule"/> -+ -+ <magentoCLI command="indexer:reindex" stepKey="reindex"/> -+ <magentoCLI command="cache:flush" stepKey="flushCache"/> -+ -+ <!-- Navigate to category on store front --> -+ <amOnPage url="{{StorefrontProductPage.url($createCategory.name$)}}" stepKey="goToCategoryPage"/> -+ -+ <!-- Check simple product name on store front category page --> -+ <actionGroup ref="AssertProductDetailsOnStorefrontActionGroup" stepKey="storefrontProduct1Name"> -+ <argument name="productInfo" value="$createSimpleProduct.name$"/> -+ <argument name="productNumber" value="2"/> -+ </actionGroup> -+ -+ <!-- Check simple product price on store front category page --> -+ <actionGroup ref="AssertProductDetailsOnStorefrontActionGroup" stepKey="storefrontProduct1Price"> -+ <argument name="productInfo" value="$51.10"/> -+ <argument name="productNumber" value="2"/> -+ </actionGroup> -+ -+ <!-- Check simple product regular price on store front category page --> -+ <actionGroup ref="AssertProductDetailsOnStorefrontActionGroup" stepKey="storefrontProduct1RegularPrice"> -+ <argument name="productInfo" value="$56.78"/> -+ <argument name="productNumber" value="2"/> -+ </actionGroup> -+ -+ <!-- Check configurable product name on store front category page --> -+ <actionGroup ref="AssertProductDetailsOnStorefrontActionGroup" stepKey="storefrontProduct2Name"> -+ <argument name="productInfo" value="$createConfigProduct.name$"/> -+ <argument name="productNumber" value="1"/> -+ </actionGroup> -+ -+ <!-- Check configurable product price on store front category page --> -+ <actionGroup ref="AssertProductDetailsOnStorefrontActionGroup" stepKey="storefrontProduct2Price"> -+ <argument name="productInfo" value="$110.70"/> -+ <argument name="productNumber" value="1"/> -+ </actionGroup> -+ -+ <!-- Navigate to simple product on store front --> -+ <amOnPage url="{{StorefrontProductPage.url($createSimpleProduct.name$)}}" stepKey="goToProductPage1"/> -+ -+ <!-- Assert regular and special price after selecting ProductOptionValueDropdown1 --> -+ <actionGroup ref="AssertStorefrontProductPricesActionGroup" stepKey="assertStorefrontProductPrices"> -+ <argument name="productPrice" value="$56.78"/> -+ <argument name="productFinalPrice" value="$51.10"/> -+ </actionGroup> -+ -+ <!-- Add simple product to cart --> -+ <actionGroup ref="StorefrontAddProductToCartWithQtyActionGroup" stepKey="cartAddSimpleProduct1ToCart"> -+ <argument name="productQty" value="1"/> -+ </actionGroup> -+ -+ <!-- Open configurable product 1 select option 1 attribute --> -+ <amOnPage url="{{StorefrontProductPage.url($$createConfigProduct.custom_attributes[url_key]$$)}}" stepKey="amOnConfigurableProductPage"/> -+ <selectOption selector="{{StorefrontProductInfoMainSection.productOptionSelect('$$createConfigProductAttribute.attribute[frontend_labels][0][label]$$')}}" userInput="$$createConfigProductAttributeOption1.option[store_labels][0][label]$$" stepKey="selectOption"/> -+ <actionGroup ref="AssertStorefrontProductPricesActionGroup" stepKey="assertStorefrontProductPrices2"> -+ <argument name="productPrice" value="$123.00"/> -+ <argument name="productFinalPrice" value="$110.70"/> -+ </actionGroup> -+ -+ <!-- Assert regular and special price after selecting ProductOptionValueDropdown1 --> -+ <actionGroup ref="StorefrontAddProductToCartWithQtyActionGroup" stepKey="cartAddConfigProduct1ToCart"> -+ <argument name="productQty" value="1"/> -+ </actionGroup> -+ -+ <!-- Assert sub total on mini shopping cart --> -+ <actionGroup ref="AssertSubTotalOnStorefrontMiniCartActionGroup" stepKey="assertSubTotalOnStorefrontMiniCart"> -+ <argument name="subTotal" value="$161.80"/> -+ </actionGroup> -+ </test> -+</tests> -diff --git a/app/code/Magento/CatalogRule/Test/Mftf/Test/ApplyCatalogRuleForSimpleProductAndFixedMethodTest.xml b/app/code/Magento/CatalogRule/Test/Mftf/Test/ApplyCatalogRuleForSimpleProductAndFixedMethodTest.xml -new file mode 100644 -index 00000000000..3405d0c4e77 ---- /dev/null -+++ b/app/code/Magento/CatalogRule/Test/Mftf/Test/ApplyCatalogRuleForSimpleProductAndFixedMethodTest.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="ApplyCatalogRuleForSimpleProductAndFixedMethodTest"> -+ <annotations> -+ <features value="CatalogRule"/> -+ <stories value="Apply catalog price rule"/> -+ <title value="Admin should be able to apply the catalog price rule for simple product with custom options"/> -+ <description value="Admin should be able to apply the catalog price rule for simple product with custom options"/> -+ <severity value="CRITICAL"/> -+ <testCaseId value="MC-14771"/> -+ <group value="CatalogRule"/> -+ <group value="mtf_migrated"/> -+ <skip> -+ <issueId value="MC-17140"/> -+ </skip> -+ </annotations> -+ <before> -+ <!-- Login as Admin --> -+ <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> -+ -+ <!-- Create category --> -+ <createData entity="_defaultCategory" stepKey="createCategory"/> -+ -+ <!-- Create Simple Product --> -+ <createData entity="_defaultProduct" stepKey="createProduct1"> -+ <requiredEntity createDataKey="createCategory"/> -+ <field key="price">56.78</field> -+ </createData> -+ -+ <!-- Update all products to have custom options --> -+ <updateData createDataKey="createProduct1" entity="productWithFixedOptions" stepKey="updateProductWithOptions1"/> -+ </before> -+ <after> -+ <!-- Delete products and category --> -+ <deleteData createDataKey="createProduct1" stepKey="deleteProduct1"/> -+ <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> -+ -+ <!-- Delete the catalog price rule --> -+ <amOnPage stepKey="goToPriceRulePage" url="{{CatalogRulePage.url}}"/> -+ <actionGroup stepKey="deletePriceRule" ref="deleteEntitySecondaryGrid"> -+ <argument name="name" value="{{CatalogRuleByFixed.name}}"/> -+ <argument name="searchInput" value="{{AdminSecondaryGridSection.catalogRuleIdentifierSearch}}"/> -+ </actionGroup> -+ -+ <!-- Logout --> -+ <actionGroup ref="logout" stepKey="logout"/> -+ </after> -+ <!-- 1. Begin creating a new catalog price rule --> -+ <actionGroup ref="newCatalogPriceRuleByUIWithConditionIsCategory" stepKey="newCatalogPriceRuleByUIWithConditionIsCategory"> -+ <argument name ="categoryId" value="$createCategory.id$"/> -+ <argument name ="catalogRule" value="CatalogRuleByFixed"/> -+ </actionGroup> -+ -+ <!-- Select not logged in customer group --> -+ <actionGroup ref="selectNotLoggedInCustomerGroupActionGroup" stepKey="selectNotLoggedInCustomerGroup"/> -+ -+ <!-- Save and apply the new catalog price rule --> -+ <actionGroup ref="SaveAndApplyCatalogPriceRuleActionGroup" stepKey="saveAndApplyCatalogPriceRule"/> -+ <magentoCLI command="indexer:reindex" stepKey="reindex"/> -+ <magentoCLI command="cache:flush" stepKey="flushCache"/> -+ -+ <!-- Navigate to category on store front --> -+ <amOnPage url="{{StorefrontProductPage.url($createCategory.name$)}}" stepKey="goToCategoryPage"/> -+ -+ <!-- Check product 1 name on store front category page --> -+ <actionGroup ref="AssertProductDetailsOnStorefrontActionGroup" stepKey="storefrontProduct1Name"> -+ <argument name="productInfo" value="$createProduct1.name$"/> -+ <argument name="productNumber" value="1"/> -+ </actionGroup> -+ -+ <!-- Check product 1 price on store front category page --> -+ <actionGroup ref="AssertProductDetailsOnStorefrontActionGroup" stepKey="storefrontProduct1Price"> -+ <argument name="productInfo" value="$44.48"/> -+ <argument name="productNumber" value="1"/> -+ </actionGroup> -+ -+ <!-- Check product 1 regular price on store front category page --> -+ <actionGroup ref="AssertProductDetailsOnStorefrontActionGroup" stepKey="storefrontProduct1RegularPrice"> -+ <argument name="productInfo" value="$56.78"/> -+ <argument name="productNumber" value="1"/> -+ </actionGroup> -+ -+ <!-- Navigate to product 1 on store front --> -+ <amOnPage url="{{StorefrontProductPage.url($createProduct1.name$)}}" stepKey="goToProductPage1"/> -+ -+ <!-- Assert regular and special price after selecting ProductOptionValueDropdown1 --> -+ <actionGroup ref="StorefrontSelectCustomOptionRadioAndAssertPricesActionGroup" stepKey="storefrontSelectCustomOptionAndAssertPrices1"> -+ <argument name="customOption" value="ProductOptionRadioButton2"/> -+ <argument name="customOptionValue" value="ProductOptionValueRadioButtons1"/> -+ <argument name="productPrice" value="$156.77"/> -+ <argument name="productFinalPrice" value="$144.47"/> -+ </actionGroup> -+ -+ <!-- Add product 1 to cart --> -+ <actionGroup ref="StorefrontAddProductToCartWithQtyActionGroup" stepKey="cartAddSimpleProduct1ToCart"> -+ <argument name="productQty" value="1"/> -+ </actionGroup> -+ -+ <!-- Assert sub total on mini shopping cart --> -+ <actionGroup ref="AssertSubTotalOnStorefrontMiniCartActionGroup" stepKey="assertSubTotalOnStorefrontMiniCart"> -+ <argument name="subTotal" value="$144.47"/> -+ </actionGroup> -+ </test> -+</tests> -diff --git a/app/code/Magento/CatalogRule/Test/Mftf/Test/ApplyCatalogRuleForSimpleProductForNewCustomerGroupTest.xml b/app/code/Magento/CatalogRule/Test/Mftf/Test/ApplyCatalogRuleForSimpleProductForNewCustomerGroupTest.xml -new file mode 100644 -index 00000000000..c3bcde92bd1 ---- /dev/null -+++ b/app/code/Magento/CatalogRule/Test/Mftf/Test/ApplyCatalogRuleForSimpleProductForNewCustomerGroupTest.xml -@@ -0,0 +1,162 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> -+ <test name="ApplyCatalogRuleForSimpleProductForNewCustomerGroupTest"> -+ <annotations> -+ <features value="CatalogRule"/> -+ <stories value="Apply catalog price rule"/> -+ <title value="Admin should be able to apply the catalog price rule for simple product for new customer group"/> -+ <description value="Admin should be able to apply the catalog price rule for simple product for new customer group"/> -+ <severity value="CRITICAL"/> -+ <testCaseId value="MC-14772"/> -+ <group value="CatalogRule"/> -+ <group value="mtf_migrated"/> -+ <skip> -+ <issueId value="MC-17140"/> -+ </skip> -+ </annotations> -+ <before> -+ <!-- Login as Admin --> -+ <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> -+ -+ <!-- Create new customer group --> -+ <createData entity="CustomCustomerGroup" stepKey="customerGroup" /> -+ -+ <!--Creating customer--> -+ <createData entity="Simple_US_Customer" stepKey="createCustomer" > -+ <field key="group_id">$customerGroup.id$</field> -+ </createData> -+ -+ <!-- Create category --> -+ <createData entity="_defaultCategory" stepKey="createCategory"/> -+ -+ <!-- Create Simple Product --> -+ <createData entity="_defaultProduct" stepKey="createSimpleProduct"> -+ <requiredEntity createDataKey="createCategory"/> -+ <field key="price">56.78</field> -+ </createData> -+ </before> -+ <after> -+ <!-- Delete products and category --> -+ <deleteData createDataKey="createSimpleProduct" stepKey="deleteSimpleProduct"/> -+ <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> -+ -+ <!-- Delete customer --> -+ <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> -+ -+ <!-- Delete customer group --> -+ <deleteData createDataKey="customerGroup" stepKey="deleteCustomerGroup"/> -+ -+ <!-- Delete the catalog price rule --> -+ <amOnPage stepKey="goToPriceRulePage" url="{{CatalogRulePage.url}}"/> -+ <actionGroup stepKey="deletePriceRule" ref="deleteEntitySecondaryGrid"> -+ <argument name="name" value="{{CatalogRuleByFixed.name}}"/> -+ <argument name="searchInput" value="{{AdminSecondaryGridSection.catalogRuleIdentifierSearch}}"/> -+ </actionGroup> -+ -+ <!-- Logout --> -+ <actionGroup ref="logout" stepKey="logout"/> -+ </after> -+ <!-- 1. Begin creating a new catalog price rule --> -+ <actionGroup ref="newCatalogPriceRuleByUIWithConditionIsCategory" stepKey="newCatalogPriceRuleByUIWithConditionIsCategory"> -+ <argument name ="categoryId" value="$createCategory.id$"/> -+ <argument name="catalogRule" value="CatalogRuleByFixed"/> -+ </actionGroup> -+ -+ <!-- Select custom customer group --> -+ <actionGroup ref="CatalogSelectCustomerGroupActionGroup" stepKey="selectCustomCustomerGroup"> -+ <argument name="customerGroupName" value="$customerGroup.code$"/> -+ </actionGroup> -+ -+ <!-- Save and apply the new catalog price rule --> -+ <actionGroup ref="SaveAndApplyCatalogPriceRuleActionGroup" stepKey="saveAndApplyCatalogPriceRule"/> -+ -+ <magentoCLI command="indexer:reindex" stepKey="reindex"/> -+ -+ <!-- Navigate to category on store front --> -+ <amOnPage url="{{StorefrontProductPage.url($createCategory.name$)}}" stepKey="goToCategoryPage"/> -+ -+ <!-- Check product 1 name on store front category page --> -+ <actionGroup ref="AssertProductDetailsOnStorefrontActionGroup" stepKey="storefrontProductName"> -+ <argument name="productInfo" value="$createSimpleProduct.name$"/> -+ <argument name="productNumber" value="1"/> -+ </actionGroup> -+ -+ <!-- Check product 1 has no discounts applied on store front category page --> -+ <actionGroup ref="AssertDontSeeProductDetailsOnStorefrontActionGroup" stepKey="storefrontProductRegularPrice"> -+ <argument name="productInfo" value="$44.48"/> -+ <argument name="productNumber" value="1"/> -+ </actionGroup> -+ -+ <!-- Check product 1 price on store front category page --> -+ <actionGroup ref="AssertProductDetailsOnStorefrontActionGroup" stepKey="storefrontProductPrice"> -+ <argument name="productInfo" value="$56.78"/> -+ <argument name="productNumber" value="1"/> -+ </actionGroup> -+ -+ <!-- Navigate to product 1 on store front --> -+ <amOnPage url="{{StorefrontProductPage.url($createSimpleProduct.name$)}}" stepKey="goToProductPage"/> -+ -+ <!-- Add product 1 to cart --> -+ <actionGroup ref="StorefrontAddProductToCartWithQtyActionGroup" stepKey="cartAddSimpleProductToCart"> -+ <argument name="productQty" value="1"/> -+ </actionGroup> -+ -+ <!-- Assert sub total on mini shopping cart --> -+ <actionGroup ref="AssertSubTotalOnStorefrontMiniCartActionGroup" stepKey="assertSubTotalOnStorefrontMiniCart"> -+ <argument name="subTotal" value="$56.78"/> -+ </actionGroup> -+ -+ <!--Login to storefront as previously created customer--> -+ <actionGroup ref="LoginToStorefrontActionGroup" stepKey="loginAsCustomer"> -+ <argument name="Customer" value="$$createCustomer$$"/> -+ </actionGroup> -+ -+ <!-- Navigate to category on store front as customer--> -+ <amOnPage url="{{StorefrontProductPage.url($createCategory.name$)}}" stepKey="goToCategoryPageAsCustomer"/> -+ -+ <!-- Check simple product name on store front category page --> -+ <actionGroup ref="AssertProductDetailsOnStorefrontActionGroup" stepKey="storefrontProductNameAsCustomer"> -+ <argument name="productInfo" value="$createSimpleProduct.name$"/> -+ <argument name="productNumber" value="1"/> -+ </actionGroup> -+ -+ <!-- Check simple product price on store front category page as customer --> -+ <actionGroup ref="AssertProductDetailsOnStorefrontActionGroup" stepKey="storefrontProductPriceAsCustomer"> -+ <argument name="productInfo" value="$44.48"/> -+ <argument name="productNumber" value="1"/> -+ </actionGroup> -+ -+ <!-- Check simple product regular price on store front category page as customer --> -+ <actionGroup ref="AssertProductDetailsOnStorefrontActionGroup" stepKey="storefrontProductRegularPriceAsCustomer"> -+ <argument name="productInfo" value="$56.78"/> -+ <argument name="productNumber" value="1"/> -+ </actionGroup> -+ -+ <!-- Navigate to simple product on store front as customer --> -+ <amOnPage url="{{StorefrontProductPage.url($createSimpleProduct.name$)}}" stepKey="goToProductPage1AsCustomer"/> -+ -+ <!-- Assert regular and special price as customer--> -+ <actionGroup ref="AssertStorefrontProductPricesActionGroup" stepKey="assertStorefrontProductPrices"> -+ <argument name="productPrice" value="$56.78"/> -+ <argument name="productFinalPrice" value="$44.48"/> -+ </actionGroup> -+ -+ <!-- Add simple product to cart as customer --> -+ <actionGroup ref="StorefrontAddProductToCartWithQtyActionGroup" stepKey="cartAddSimpleProductToCartAsCustomer"> -+ <argument name="productQty" value="1"/> -+ </actionGroup> -+ -+ <!-- Assert sub total on mini shopping cart as customer --> -+ <actionGroup ref="AssertSubTotalOnStorefrontMiniCartActionGroup" stepKey="assertSubTotalOnStorefrontMiniCartAsCustomer"> -+ <argument name="subTotal" value="$88.96"/> -+ </actionGroup> -+ -+ </test> -+</tests> -diff --git a/app/code/Magento/CatalogRule/Test/Mftf/Test/ApplyCatalogRuleForSimpleProductWithCustomOptionsTest.xml b/app/code/Magento/CatalogRule/Test/Mftf/Test/ApplyCatalogRuleForSimpleProductWithCustomOptionsTest.xml -new file mode 100644 -index 00000000000..055eacaeb2b ---- /dev/null -+++ b/app/code/Magento/CatalogRule/Test/Mftf/Test/ApplyCatalogRuleForSimpleProductWithCustomOptionsTest.xml -@@ -0,0 +1,201 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> -+ <test name="ApplyCatalogRuleForSimpleProductWithCustomOptionsTest"> -+ <annotations> -+ <features value="CatalogRule"/> -+ <stories value="Apply catalog price rule"/> -+ <title value="Admin should be able to apply the catalog price rule for simple product with custom options"/> -+ <description value="Admin should be able to apply the catalog price rule for simple product with custom options"/> -+ <severity value="CRITICAL"/> -+ <testCaseId value="MC-14769"/> -+ <group value="CatalogRule"/> -+ <group value="mtf_migrated"/> -+ <skip> -+ <issueId value="MC-16684"/> -+ </skip> -+ </annotations> -+ <before> -+ <!-- Login as Admin --> -+ <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> -+ -+ <!-- Create category --> -+ <createData entity="_defaultCategory" stepKey="createCategory"/> -+ -+ <!-- Create Simple Product 1 --> -+ <createData entity="_defaultProduct" stepKey="createProduct1"> -+ <requiredEntity createDataKey="createCategory"/> -+ <field key="price">56.78</field> -+ </createData> -+ -+ <!-- Create Simple Product 2 --> -+ <createData entity="_defaultProduct" stepKey="createProduct2"> -+ <requiredEntity createDataKey="createCategory"/> -+ <field key="price">56.78</field> -+ </createData> -+ -+ <!-- Create Simple Product 3 --> -+ <createData entity="_defaultProduct" stepKey="createProduct3"> -+ <requiredEntity createDataKey="createCategory"/> -+ <field key="price">56.78</field> -+ </createData> -+ -+ <!-- Update all products to have custom options --> -+ <updateData createDataKey="createProduct1" entity="productWithCustomOptions" stepKey="updateProductWithOptions1"/> -+ <updateData createDataKey="createProduct2" entity="productWithCustomOptions" stepKey="updateProductWithOptions2"/> -+ <updateData createDataKey="createProduct3" entity="productWithCustomOptions" stepKey="updateProductWithOptions3"/> -+ </before> -+ <after> -+ <!-- Delete products and category --> -+ <deleteData createDataKey="createProduct1" stepKey="deleteProduct1"/> -+ <deleteData createDataKey="createProduct2" stepKey="deleteProduct2"/> -+ <deleteData createDataKey="createProduct3" stepKey="deleteProduct3"/> -+ <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> -+ -+ <!-- Delete the catalog price rule --> -+ <amOnPage stepKey="goToPriceRulePage" url="{{CatalogRulePage.url}}"/> -+ <actionGroup stepKey="deletePriceRule" ref="deleteEntitySecondaryGrid"> -+ <argument name="name" value="{{_defaultCatalogRule.name}}"/> -+ <argument name="searchInput" value="{{AdminSecondaryGridSection.catalogRuleIdentifierSearch}}"/> -+ </actionGroup> -+ -+ <!-- Logout --> -+ <actionGroup ref="logout" stepKey="logout"/> -+ </after> -+ <!-- 1. Begin creating a new catalog price rule --> -+ <actionGroup ref="newCatalogPriceRuleByUIWithConditionIsCategory" stepKey="newCatalogPriceRuleByUIWithConditionIsCategory"> -+ <argument name ="categoryId" value="$createCategory.id$"/> -+ </actionGroup> -+ -+ <!-- Select not logged in customer group --> -+ <actionGroup ref="selectNotLoggedInCustomerGroupActionGroup" stepKey="selectNotLoggedInCustomerGroup"/> -+ -+ <!-- Save and apply the new catalog price rule --> -+ <actionGroup ref="SaveAndApplyCatalogPriceRuleActionGroup" stepKey="saveAndApplyCatalogPriceRule"/> -+ <magentoCLI command="indexer:reindex" stepKey="reindex"/> -+ <magentoCLI command="cache:flush" stepKey="flushCache"/> -+ -+ <!-- Navigate to category on store front --> -+ <amOnPage url="{{StorefrontProductPage.url($createCategory.name$)}}" stepKey="goToCategoryPage"/> -+ -+ <!-- Check product 1 name on store front category page --> -+ <actionGroup ref="AssertProductDetailsOnStorefrontActionGroup" stepKey="storefrontProduct1Name"> -+ <argument name="productInfo" value="$createProduct1.name$"/> -+ <argument name="productNumber" value="3"/> -+ </actionGroup> -+ -+ <!-- Check product 1 price on store front category page --> -+ <actionGroup ref="AssertProductDetailsOnStorefrontActionGroup" stepKey="storefrontProduct1Price"> -+ <argument name="productInfo" value="$51.10"/> -+ <argument name="productNumber" value="3"/> -+ </actionGroup> -+ -+ <!-- Check product 1 regular price on store front category page --> -+ <actionGroup ref="AssertProductDetailsOnStorefrontActionGroup" stepKey="storefrontProduct1RegularPrice"> -+ <argument name="productInfo" value="$56.78"/> -+ <argument name="productNumber" value="3"/> -+ </actionGroup> -+ -+ <!-- Check product 2 name on store front category page --> -+ <actionGroup ref="AssertProductDetailsOnStorefrontActionGroup" stepKey="storefrontProduct2Name"> -+ <argument name="productInfo" value="$createProduct2.name$"/> -+ <argument name="productNumber" value="2"/> -+ </actionGroup> -+ -+ <!-- Check product 2 price on store front category page --> -+ <actionGroup ref="AssertProductDetailsOnStorefrontActionGroup" stepKey="storefrontProduct2Price"> -+ <argument name="productInfo" value="$51.10"/> -+ <argument name="productNumber" value="2"/> -+ </actionGroup> -+ -+ <!-- Check product 2 price on store front category page --> -+ <actionGroup ref="AssertProductDetailsOnStorefrontActionGroup" stepKey="storefrontProduct2RegularPrice"> -+ <argument name="productInfo" value="$56.78"/> -+ <argument name="productNumber" value="2"/> -+ </actionGroup> -+ -+ <!-- Check product 3 name on store front category page --> -+ <actionGroup ref="AssertProductDetailsOnStorefrontActionGroup" stepKey="storefrontProduct3Name"> -+ <argument name="productInfo" value="$createProduct3.name$"/> -+ <argument name="productNumber" value="1"/> -+ </actionGroup> -+ -+ <!-- Check product 3 price on store front category page --> -+ <actionGroup ref="AssertProductDetailsOnStorefrontActionGroup" stepKey="storefrontProduct3Price"> -+ <argument name="productInfo" value="$51.10"/> -+ <argument name="productNumber" value="1"/> -+ </actionGroup> -+ -+ <!-- Check product 3 regular price on store front category page --> -+ <actionGroup ref="AssertProductDetailsOnStorefrontActionGroup" stepKey="storefrontProduct3RegularPrice"> -+ <argument name="productInfo" value="$56.78"/> -+ <argument name="productNumber" value="1"/> -+ </actionGroup> -+ -+ <!-- Navigate to product 1 on store front --> -+ <amOnPage url="{{StorefrontProductPage.url($createProduct1.name$)}}" stepKey="goToProductPage1"/> -+ -+ <!-- Assert regular and special price after selecting ProductOptionValueDropdown1 --> -+ <actionGroup ref="StorefrontSelectCustomOptionDropDownAndAssertPricesActionGroup" stepKey="storefrontSelectCustomOptionAndAssertPrices1"> -+ <argument name="customOption" value="{{ProductOptionValueDropdown1.title}} +$0.01"/> -+ <argument name="productPrice" value="$56.79"/> -+ <argument name="productFinalPrice" value="$51.11"/> -+ </actionGroup> -+ -+ <!-- Add product 1 to cart --> -+ <actionGroup ref="StorefrontAddProductToCartWithQtyActionGroup" stepKey="cartAddSimpleProduct1ToCart"> -+ <argument name="productQty" value="1"/> -+ </actionGroup> -+ -+ <!-- Navigate to product 2 on store front --> -+ <amOnPage url="{{StorefrontProductPage.url($createProduct1.name$)}}" stepKey="goToProductPage2"/> -+ -+ <!-- Assert regular and special price after selecting ProductOptionValueDropdown3 --> -+ <actionGroup ref="StorefrontSelectCustomOptionDropDownAndAssertPricesActionGroup" stepKey="storefrontSelectCustomOptionAndAssertPrices2"> -+ <argument name="customOption" value="{{ProductOptionValueDropdown3.title}} +$5.11"/> -+ <argument name="productPrice" value="$62.46"/> -+ <argument name="productFinalPrice" value="$56.21"/> -+ </actionGroup> -+ -+ <!-- Add product 2 to cart --> -+ <actionGroup ref="StorefrontAddProductToCartWithQtyActionGroup" stepKey="cartAddSimpleProduct2ToCart"> -+ <argument name="productQty" value="1"/> -+ </actionGroup> -+ -+ <!-- Navigate to product 3 on store front --> -+ <amOnPage url="{{StorefrontProductPage.url($createProduct3.name$)}}" stepKey="goToProductPage3"/> -+ -+ <!-- Add product 3 to cart with no custom option --> -+ <actionGroup ref="StorefrontAddProductToCartWithQtyActionGroup" stepKey="cartAddSimpleProduct3ToCart"> -+ <argument name="productQty" value="1"/> -+ </actionGroup> -+ -+ <!-- Assert sub total on mini shopping cart --> -+ <actionGroup ref="AssertSubTotalOnStorefrontMiniCartActionGroup" stepKey="assertSubTotalOnStorefrontMiniCart"> -+ <argument name="subTotal" value="$158.42"/> -+ </actionGroup> -+ -+ <!-- Navigate to checkout shipping page --> -+ <amOnPage stepKey="navigateToShippingPage" url="{{CheckoutShippingPage.url}}"/> -+ <waitForPageLoad stepKey="waitFoCheckoutShippingPageLoad"/> -+ -+ <!-- Fill Shipping information --> -+ <actionGroup ref="GuestCheckoutFillingShippingSectionActionGroup" stepKey="fillOrderShippingInfo"> -+ <argument name="customerVar" value="Simple_US_Customer"/> -+ <argument name="customerAddressVar" value="US_Address_TX"/> -+ </actionGroup> -+ -+ <!-- Verify order summary on payment page --> -+ <actionGroup ref="VerifyCheckoutPaymentOrderSummaryActionGroup" stepKey="verifyCheckoutPaymentOrderSummaryActionGroup"> -+ <argument name="orderSummarySubTotal" value="$158.42"/> -+ <argument name="orderSummaryShippingTotal" value="$15.00"/> -+ <argument name="orderSummaryTotal" value="$173.42"/> -+ </actionGroup> -+ </test> -+</tests> -diff --git a/app/code/Magento/CatalogRule/Test/Mftf/Test/CatalogPriceRuleAndCustomerGroupMembershipArePersistedUnderLongTermCookieTest.xml b/app/code/Magento/CatalogRule/Test/Mftf/Test/CatalogPriceRuleAndCustomerGroupMembershipArePersistedUnderLongTermCookieTest.xml -new file mode 100644 -index 00000000000..e3eac52a8d4 ---- /dev/null -+++ b/app/code/Magento/CatalogRule/Test/Mftf/Test/CatalogPriceRuleAndCustomerGroupMembershipArePersistedUnderLongTermCookieTest.xml -@@ -0,0 +1,88 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> -+ <test name="CatalogPriceRuleAndCustomerGroupMembershipArePersistedUnderLongTermCookieTest"> -+ <annotations> -+ <features value="Persistent"/> -+ <stories value="Check the price"/> -+ <title value="Verify that Catalog Price Rule and Customer Group Membership are persisted under long-term cookie"/> -+ <description value="Verify that Catalog Price Rule and Customer Group Membership are persisted under long-term cookie"/> -+ <severity value="CRITICAL"/> -+ <testCaseId value="MAGETWO-69455"/> -+ <group value="persistent"/> -+ <skip> -+ <issueId value="MC-5777"/> -+ </skip> -+ </annotations> -+ <before> -+ <createData entity="PersistentConfigEnabled" stepKey="enablePersistent"/> -+ <createData entity="PersistentLogoutClearDisable" stepKey="persistentLogoutClearDisable"/> -+ <createData entity="_defaultCategory" stepKey="createCategory"/> -+ <createData entity="_defaultProduct" stepKey="createProduct"> -+ <requiredEntity createDataKey="createCategory"/> -+ <field key="price">50</field> -+ </createData> -+ <createData entity="Simple_US_Customer" stepKey="createCustomer"> -+ <field key="group_id">1</field> -+ </createData> -+ -+ <actionGroup ref="LoginAsAdmin" stepKey="login"/> -+ <!--Create Catalog Rule--> -+ <actionGroup ref="newCatalogPriceRuleByUIWithConditionIsCategory" stepKey="createCatalogPriceRule"> -+ <argument name="catalogRule" value="_defaultCatalogRule"/> -+ <argument name="categoryId" value="$$createCategory.id$$"/> -+ </actionGroup> -+ <actionGroup ref="selectGeneralCustomerGroupActionGroup" stepKey="selectCustomerGroup"/> -+ <click selector="{{AdminNewCatalogPriceRule.saveAndApply}}" stepKey="clickSaveAndApplyRules"/> -+ <see selector="{{AdminCategoryMessagesSection.SuccessMessage}}" userInput="You saved the rule." stepKey="assertSuccess"/> -+ <magentoCLI command="indexer:reindex" stepKey="reindex"/> -+ <magentoCLI command="cache:flush" stepKey="flushCache"/> -+ </before> -+ <after> -+ <!-- Delete the rule --> -+ <amOnPage url="{{CatalogRulePage.url}}" stepKey="goToCatalogPriceRulePage"/> -+ <actionGroup ref="deleteEntitySecondaryGrid" stepKey="deletePriceRule"> -+ <argument name="name" value="{{_defaultCatalogRule.name}}"/> -+ <argument name="searchInput" value="{{AdminSecondaryGridSection.catalogRuleIdentifierSearch}}"/> -+ </actionGroup> -+ <actionGroup ref="logout" stepKey="logout"/> -+ <createData entity="PersistentConfigDefault" stepKey="setDefaultPersistentState"/> -+ <createData entity="PersistentLogoutClearEnabled" stepKey="persistentLogoutClearEnabled"/> -+ <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> -+ <deleteData createDataKey="createProduct" stepKey="deleteProduct"/> -+ <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> -+ </after> -+ -+ <!--Go to category and check price--> -+ <amOnPage url="{{StorefrontCategoryPage.url($$createCategory.name$$)}}" stepKey="onStorefrontCategoryPage"/> -+ <see selector="{{StorefrontCategoryProductSection.ProductPriceByNumber('1')}}" userInput="$$createProduct.price$$" stepKey="checkPriceSimpleProduct"/> -+ -+ <!--Login to storefront from customer and check price--> -+ <actionGroup ref="LoginToStorefrontActionGroup" stepKey="logInFromCustomer"> -+ <argument name="Customer" value="$$createCustomer$$"/> -+ </actionGroup> -+ <amOnPage url="{{StorefrontCategoryPage.url($$createCategory.name$$)}}" stepKey="onStorefrontCategoryPage2"/> -+ <see userInput="Welcome, $$createCustomer.firstname$$ $$createCustomer.lastname$$!" selector="{{StorefrontPanelHeaderSection.WelcomeMessage}}" stepKey="homeCheckWelcome"/> -+ <see selector="{{StorefrontCategoryProductSection.ProductSpecialPriceByNumber('1')}}" userInput="45.00" stepKey="checkPriceSimpleProduct2"/> -+ -+ <!--Click *Sign Out* and check the price of the Simple Product--> -+ <actionGroup ref="StorefrontSignOutActionGroup" stepKey="storefrontSignOut"/> -+ <amOnPage url="{{StorefrontCategoryPage.url($$createCategory.name$$)}}" stepKey="onStorefrontCategoryPage3"/> -+ <see userInput="Welcome, $$createCustomer.firstname$$ $$createCustomer.lastname$$!" selector="{{StorefrontPanelHeaderSection.WelcomeMessage}}" stepKey="homeCheckWelcome2"/> -+ <seeElement selector="{{StorefrontPanelHeaderSection.notYouLink}}" stepKey="checkLinkNotYoy"/> -+ <see selector="{{StorefrontCategoryProductSection.ProductSpecialPriceByNumber('1')}}" userInput="45.00" stepKey="checkPriceSimpleProduct3"/> -+ -+ <!--Click the *Not you?* link and check the price for Simple Product--> -+ <click selector="{{StorefrontPanelHeaderSection.notYouLink}}" stepKey="clickNext"/> -+ <amOnPage url="{{StorefrontCategoryPage.url($$createCategory.name$$)}}" stepKey="onStorefrontCategoryPage4"/> -+ <see userInput="Default welcome msg!" selector="{{StorefrontPanelHeaderSection.WelcomeMessage}}" stepKey="homeCheckWelcome3"/> -+ <see selector="{{StorefrontCategoryProductSection.ProductPriceByNumber('1')}}" userInput="$$createProduct.price$$" stepKey="checkPriceSimpleProduct4"/> -+ </test> -+</tests> -diff --git a/app/code/Magento/CatalogRule/Test/Mftf/Test/DeleteCustomerGroupTest.xml b/app/code/Magento/CatalogRule/Test/Mftf/Test/DeleteCustomerGroupTest.xml -new file mode 100644 -index 00000000000..75223fcfc4c ---- /dev/null -+++ b/app/code/Magento/CatalogRule/Test/Mftf/Test/DeleteCustomerGroupTest.xml -@@ -0,0 +1,17 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> -+ <test name="DeleteCustomerGroupTest"> -+ <actionGroup ref="AdminOpenNewCatalogPriceRuleFormPageActionGroup" stepKey="openNewCatalogPriceRuleForm" /> -+ <actionGroup ref="AssertCustomerGroupNotOnCatalogPriceRuleFormActionGroup" stepKey="assertCustomerGroupNotOnCatalogPriceRuleForm"> -+ <argument name="customerGroup" value="$$customerGroup$$" /> -+ </actionGroup> -+ </test> -+</tests> -diff --git a/app/code/Magento/CatalogRule/Test/Mftf/Test/StorefrontInactiveCatalogRuleTest.xml b/app/code/Magento/CatalogRule/Test/Mftf/Test/StorefrontInactiveCatalogRuleTest.xml -index 64f19cb3ca8..22fcf6870c1 100644 ---- a/app/code/Magento/CatalogRule/Test/Mftf/Test/StorefrontInactiveCatalogRuleTest.xml -+++ b/app/code/Magento/CatalogRule/Test/Mftf/Test/StorefrontInactiveCatalogRuleTest.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="StorefrontInactiveCatalogRuleTest"> - <annotations> - <features value="CatalogRule"/> -@@ -17,6 +17,9 @@ - <severity value="CRITICAL"/> - <testCaseId value="MC-79"/> - <group value="CatalogRule"/> -+ <skip> -+ <issueId value="MC-5777"/> -+ </skip> - </annotations> - <before> - <actionGroup ref="LoginAsAdmin" stepKey="login"/> -@@ -54,6 +57,7 @@ - <!-- Verify price is not discounted in the cart --> - <click selector="{{StorefrontProductActionSection.addToCart}}" stepKey="addToCart"/> - <waitForPageLoad stepKey="waitForCart"/> -+ <waitForElementVisible selector="{{StorefrontMessagesSection.success}}" stepKey="waitForSuccessMessage"/> - <amOnPage url="{{CheckoutCartPage.url}}" stepKey="goToCheckout"/> - <waitForPageLoad stepKey="waitForCheckout"/> - <see selector="{{CheckoutCartSummarySection.subtotal}}" userInput="$$createProduct.price$$" stepKey="seePrice3"/> -diff --git a/app/code/Magento/CatalogRule/Test/Unit/Block/Adminhtml/Edit/DeleteButtonTest.php b/app/code/Magento/CatalogRule/Test/Unit/Block/Adminhtml/Edit/DeleteButtonTest.php -index 6178d51644f..f969b342f82 100644 ---- a/app/code/Magento/CatalogRule/Test/Unit/Block/Adminhtml/Edit/DeleteButtonTest.php -+++ b/app/code/Magento/CatalogRule/Test/Unit/Block/Adminhtml/Edit/DeleteButtonTest.php -@@ -24,13 +24,16 @@ class DeleteButtonTest extends \PHPUnit\Framework\TestCase - */ - protected $registryMock; - -+ /** -+ * @inheritDoc -+ */ - protected function setUp() - { - $this->urlBuilderMock = $this->createMock(\Magento\Framework\UrlInterface::class); - $this->registryMock = $this->createMock(\Magento\Framework\Registry::class); - $contextMock = $this->createMock(\Magento\Backend\Block\Widget\Context::class); - -- $contextMock->expects($this->once())->method('getUrlBuilder')->willReturn($this->urlBuilderMock); -+ $contextMock->expects($this->any())->method('getUrlBuilder')->willReturn($this->urlBuilderMock); - - $this->model = new \Magento\CatalogRule\Block\Adminhtml\Edit\DeleteButton( - $contextMock, -@@ -38,33 +41,9 @@ class DeleteButtonTest extends \PHPUnit\Framework\TestCase - ); - } - -- public function testGetButtonData() -- { -- $ruleId = 42; -- $deleteUrl = 'http://magento.com/rule/delete/' . $ruleId; -- $ruleMock = new \Magento\Framework\DataObject(['id' => $ruleId]); -- -- $this->registryMock->expects($this->once()) -- ->method('registry') -- ->with(RegistryConstants::CURRENT_CATALOG_RULE_ID) -- ->willReturn($ruleMock); -- $this->urlBuilderMock->expects($this->once()) -- ->method('getUrl') -- ->with('*/*/delete', ['id' => $ruleId]) -- ->willReturn($deleteUrl); -- -- $data = [ -- 'label' => __('Delete Rule'), -- 'class' => 'delete', -- 'on_click' => 'deleteConfirm(\'' . __( -- 'Are you sure you want to do this?' -- ) . '\', \'' . $deleteUrl . '\')', -- 'sort_order' => 20, -- ]; -- -- $this->assertEquals($data, $this->model->getButtonData()); -- } -- -+ /** -+ * Test empty response without a present rule. -+ */ - public function testGetButtonDataWithoutRule() - { - $this->assertEquals([], $this->model->getButtonData()); -diff --git a/app/code/Magento/CatalogRule/Test/Unit/Model/Indexer/IndexBuilderTest.php b/app/code/Magento/CatalogRule/Test/Unit/Model/Indexer/IndexBuilderTest.php -index 521e4e1d598..920dcb8e1ed 100644 ---- a/app/code/Magento/CatalogRule/Test/Unit/Model/Indexer/IndexBuilderTest.php -+++ b/app/code/Magento/CatalogRule/Test/Unit/Model/Indexer/IndexBuilderTest.php -@@ -144,14 +144,12 @@ class IndexBuilderTest extends \PHPUnit\Framework\TestCase - ); - $this->ruleCollectionFactory = $this->createPartialMock( - \Magento\CatalogRule\Model\ResourceModel\Rule\CollectionFactory::class, -- ['create', 'addFieldToFilter'] -+ ['create'] - ); - $this->backend = $this->createMock(\Magento\Eav\Model\Entity\Attribute\Backend\AbstractBackend::class); - $this->select = $this->createMock(\Magento\Framework\DB\Select::class); - $this->metadataPool = $this->createMock(\Magento\Framework\EntityManager\MetadataPool::class); -- $metadata = $this->getMockBuilder(\Magento\Framework\EntityManager\EntityMetadata::class) -- ->disableOriginalConstructor() -- ->getMock(); -+ $metadata = $this->createMock(\Magento\Framework\EntityManager\EntityMetadata::class); - $this->metadataPool->expects($this->any())->method('getMetadata')->willReturn($metadata); - $this->connection = $this->createMock(\Magento\Framework\DB\Adapter\AdapterInterface::class); - $this->db = $this->createMock(\Zend_Db_Statement_Interface::class); -@@ -181,10 +179,16 @@ class IndexBuilderTest extends \PHPUnit\Framework\TestCase - $this->rules->expects($this->any())->method('getWebsiteIds')->will($this->returnValue([1])); - $this->rules->expects($this->any())->method('getCustomerGroupIds')->will($this->returnValue([1])); - -- $this->ruleCollectionFactory->expects($this->any())->method('create')->will($this->returnSelf()); -- $this->ruleCollectionFactory->expects($this->any())->method('addFieldToFilter')->will( -- $this->returnValue([$this->rules]) -- ); -+ $ruleCollection = $this->createMock(\Magento\CatalogRule\Model\ResourceModel\Rule\Collection::class); -+ $this->ruleCollectionFactory->expects($this->once()) -+ ->method('create') -+ ->willReturn($ruleCollection); -+ $ruleCollection->expects($this->once()) -+ ->method('addFieldToFilter') -+ ->willReturnSelf(); -+ $ruleIterator = new \ArrayIterator([$this->rules]); -+ $ruleCollection->method('getIterator') -+ ->willReturn($ruleIterator); - - $this->product->expects($this->any())->method('load')->will($this->returnSelf()); - $this->product->expects($this->any())->method('getId')->will($this->returnValue(1)); -@@ -213,19 +217,20 @@ class IndexBuilderTest extends \PHPUnit\Framework\TestCase - ] - ); - -- $this->reindexRuleProductPrice = -- $this->getMockBuilder(\Magento\CatalogRule\Model\Indexer\ReindexRuleProductPrice::class) -- ->disableOriginalConstructor() -- ->getMock(); -- $this->reindexRuleGroupWebsite = -- $this->getMockBuilder(\Magento\CatalogRule\Model\Indexer\ReindexRuleGroupWebsite::class) -- ->disableOriginalConstructor() -- ->getMock(); -- $this->setProperties($this->indexBuilder, [ -- 'metadataPool' => $this->metadataPool, -- 'reindexRuleProductPrice' => $this->reindexRuleProductPrice, -- 'reindexRuleGroupWebsite' => $this->reindexRuleGroupWebsite -- ]); -+ $this->reindexRuleProductPrice = $this->createMock( -+ \Magento\CatalogRule\Model\Indexer\ReindexRuleProductPrice::class -+ ); -+ $this->reindexRuleGroupWebsite = $this->createMock( -+ \Magento\CatalogRule\Model\Indexer\ReindexRuleGroupWebsite::class -+ ); -+ $this->setProperties( -+ $this->indexBuilder, -+ [ -+ 'metadataPool' => $this->metadataPool, -+ 'reindexRuleProductPrice' => $this->reindexRuleProductPrice, -+ 'reindexRuleGroupWebsite' => $this->reindexRuleGroupWebsite, -+ ] -+ ); - } - - /** -diff --git a/app/code/Magento/CatalogRule/Test/Unit/Observer/AddDirtyRulesNoticeTest.php b/app/code/Magento/CatalogRule/Test/Unit/Observer/AddDirtyRulesNoticeTest.php -index b052ccddbf6..25bae43a930 100644 ---- a/app/code/Magento/CatalogRule/Test/Unit/Observer/AddDirtyRulesNoticeTest.php -+++ b/app/code/Magento/CatalogRule/Test/Unit/Observer/AddDirtyRulesNoticeTest.php -@@ -49,7 +49,7 @@ class AddDirtyRulesNoticeTest extends \PHPUnit\Framework\TestCase - $eventObserverMock->expects($this->at(0))->method('getData')->with('dirty_rules')->willReturn($flagMock); - $flagMock->expects($this->once())->method('getState')->willReturn(1); - $eventObserverMock->expects($this->at(1))->method('getData')->with('message')->willReturn($message); -- $this->messageManagerMock->expects($this->once())->method('addNotice')->with($message); -+ $this->messageManagerMock->expects($this->once())->method('addNoticeMessage')->with($message); - $this->observer->execute($eventObserverMock); - } - } -diff --git a/app/code/Magento/CatalogRule/etc/db_schema.xml b/app/code/Magento/CatalogRule/etc/db_schema.xml -index f4c40a6930c..59082e93b04 100644 ---- a/app/code/Magento/CatalogRule/etc/db_schema.xml -+++ b/app/code/Magento/CatalogRule/etc/db_schema.xml -@@ -9,7 +9,7 @@ - xsi:noNamespaceSchemaLocation="urn:magento:framework:Setup/Declaration/Schema/etc/schema.xsd"> - <table name="catalogrule" resource="default" engine="innodb" comment="CatalogRule"> - <column xsi:type="int" name="rule_id" padding="10" unsigned="true" nullable="false" identity="true" -- comment="Entity Id"/> -+ comment="Entity ID"/> - <column xsi:type="varchar" name="name" nullable="true" length="255" comment="Name"/> - <column xsi:type="text" name="description" nullable="true" comment="Description"/> - <column xsi:type="date" name="from_date" comment="From"/> -@@ -23,12 +23,12 @@ - <column xsi:type="int" name="sort_order" padding="10" unsigned="true" nullable="false" identity="false" - default="0" comment="Sort Order"/> - <column xsi:type="varchar" name="simple_action" nullable="true" length="32" comment="Simple Action"/> -- <column xsi:type="decimal" name="discount_amount" scale="4" precision="12" unsigned="false" nullable="false" -+ <column xsi:type="decimal" name="discount_amount" scale="6" precision="20" unsigned="false" nullable="false" - default="0" comment="Discount Amount"/> -- <constraint xsi:type="primary" name="PRIMARY"> -+ <constraint xsi:type="primary" referenceId="PRIMARY"> - <column name="rule_id"/> - </constraint> -- <index name="CATALOGRULE_IS_ACTIVE_SORT_ORDER_TO_DATE_FROM_DATE" indexType="btree"> -+ <index referenceId="CATALOGRULE_IS_ACTIVE_SORT_ORDER_TO_DATE_FROM_DATE" indexType="btree"> - <column name="is_active"/> - <column name="sort_order"/> - <column name="to_date"/> -@@ -39,7 +39,7 @@ - <column xsi:type="int" name="rule_product_id" padding="10" unsigned="true" nullable="false" identity="true" - comment="Rule Product Id"/> - <column xsi:type="int" name="rule_id" padding="10" unsigned="true" nullable="false" identity="false" default="0" -- comment="Rule Id"/> -+ comment="Rule ID"/> - <column xsi:type="int" name="from_time" padding="10" unsigned="true" nullable="false" identity="false" - default="0" comment="From Time"/> - <column xsi:type="int" name="to_time" padding="10" unsigned="true" nullable="false" identity="false" default="0" -@@ -49,7 +49,7 @@ - default="0" comment="Product Id"/> - <column xsi:type="varchar" name="action_operator" nullable="true" length="10" default="to_fixed" - comment="Action Operator"/> -- <column xsi:type="decimal" name="action_amount" scale="4" precision="12" unsigned="false" nullable="false" -+ <column xsi:type="decimal" name="action_amount" scale="6" precision="20" unsigned="false" nullable="false" - default="0" comment="Action Amount"/> - <column xsi:type="smallint" name="action_stop" padding="6" unsigned="false" nullable="false" identity="false" - default="0" comment="Action Stop"/> -@@ -57,10 +57,10 @@ - default="0" comment="Sort Order"/> - <column xsi:type="smallint" name="website_id" padding="5" unsigned="true" nullable="false" identity="false" - comment="Website Id"/> -- <constraint xsi:type="primary" name="PRIMARY"> -+ <constraint xsi:type="primary" referenceId="PRIMARY"> - <column name="rule_product_id"/> - </constraint> -- <constraint xsi:type="unique" name="UNQ_EAA51B56FF092A0DCB795D1CEF812B7B"> -+ <constraint xsi:type="unique" referenceId="UNQ_EAA51B56FF092A0DCB795D1CEF812B7B"> - <column name="rule_id"/> - <column name="from_time"/> - <column name="to_time"/> -@@ -69,19 +69,19 @@ - <column name="product_id"/> - <column name="sort_order"/> - </constraint> -- <index name="CATALOGRULE_PRODUCT_CUSTOMER_GROUP_ID" indexType="btree"> -+ <index referenceId="CATALOGRULE_PRODUCT_CUSTOMER_GROUP_ID" indexType="btree"> - <column name="customer_group_id"/> - </index> -- <index name="CATALOGRULE_PRODUCT_WEBSITE_ID" indexType="btree"> -+ <index referenceId="CATALOGRULE_PRODUCT_WEBSITE_ID" indexType="btree"> - <column name="website_id"/> - </index> -- <index name="CATALOGRULE_PRODUCT_FROM_TIME" indexType="btree"> -+ <index referenceId="CATALOGRULE_PRODUCT_FROM_TIME" indexType="btree"> - <column name="from_time"/> - </index> -- <index name="CATALOGRULE_PRODUCT_TO_TIME" indexType="btree"> -+ <index referenceId="CATALOGRULE_PRODUCT_TO_TIME" indexType="btree"> - <column name="to_time"/> - </index> -- <index name="CATALOGRULE_PRODUCT_PRODUCT_ID" indexType="btree"> -+ <index referenceId="CATALOGRULE_PRODUCT_PRODUCT_ID" indexType="btree"> - <column name="product_id"/> - </index> - </table> -@@ -92,84 +92,86 @@ - <column xsi:type="int" name="customer_group_id" padding="11" unsigned="false" nullable="true" identity="false"/> - <column xsi:type="int" name="product_id" padding="10" unsigned="true" nullable="false" identity="false" - default="0" comment="Product Id"/> -- <column xsi:type="decimal" name="rule_price" scale="4" precision="12" unsigned="false" nullable="false" -+ <column xsi:type="decimal" name="rule_price" scale="6" precision="20" unsigned="false" nullable="false" - default="0" comment="Rule Price"/> - <column xsi:type="smallint" name="website_id" padding="5" unsigned="true" nullable="false" identity="false" - comment="Website Id"/> - <column xsi:type="date" name="latest_start_date" comment="Latest StartDate"/> - <column xsi:type="date" name="earliest_end_date" comment="Earliest EndDate"/> -- <constraint xsi:type="primary" name="PRIMARY"> -+ <constraint xsi:type="primary" referenceId="PRIMARY"> - <column name="rule_product_price_id"/> - </constraint> -- <constraint xsi:type="unique" name="CATRULE_PRD_PRICE_RULE_DATE_WS_ID_CSTR_GROUP_ID_PRD_ID"> -+ <constraint xsi:type="unique" referenceId="CATRULE_PRD_PRICE_RULE_DATE_WS_ID_CSTR_GROUP_ID_PRD_ID"> - <column name="rule_date"/> - <column name="website_id"/> - <column name="customer_group_id"/> - <column name="product_id"/> - </constraint> -- <index name="CATALOGRULE_PRODUCT_PRICE_CUSTOMER_GROUP_ID" indexType="btree"> -+ <index referenceId="CATALOGRULE_PRODUCT_PRICE_CUSTOMER_GROUP_ID" indexType="btree"> - <column name="customer_group_id"/> - </index> -- <index name="CATALOGRULE_PRODUCT_PRICE_WEBSITE_ID" indexType="btree"> -+ <index referenceId="CATALOGRULE_PRODUCT_PRICE_WEBSITE_ID" indexType="btree"> - <column name="website_id"/> - </index> -- <index name="CATALOGRULE_PRODUCT_PRICE_PRODUCT_ID" indexType="btree"> -+ <index referenceId="CATALOGRULE_PRODUCT_PRICE_PRODUCT_ID" indexType="btree"> - <column name="product_id"/> - </index> - </table> - <table name="catalogrule_group_website" resource="default" engine="innodb" comment="CatalogRule Group Website"> - <column xsi:type="int" name="rule_id" padding="10" unsigned="true" nullable="false" identity="false" default="0" -- comment="Rule Id"/> -+ comment="Rule ID"/> - <column xsi:type="int" name="customer_group_id" padding="10" unsigned="true" nullable="false" identity="false" - default="0" comment="Customer Group Id"/> - <column xsi:type="smallint" name="website_id" padding="5" unsigned="true" nullable="false" identity="false" - default="0" comment="Website Id"/> -- <constraint xsi:type="primary" name="PRIMARY"> -+ <constraint xsi:type="primary" referenceId="PRIMARY"> - <column name="rule_id"/> - <column name="customer_group_id"/> - <column name="website_id"/> - </constraint> -- <index name="CATALOGRULE_GROUP_WEBSITE_CUSTOMER_GROUP_ID" indexType="btree"> -+ <index referenceId="CATALOGRULE_GROUP_WEBSITE_CUSTOMER_GROUP_ID" indexType="btree"> - <column name="customer_group_id"/> - </index> -- <index name="CATALOGRULE_GROUP_WEBSITE_WEBSITE_ID" indexType="btree"> -+ <index referenceId="CATALOGRULE_GROUP_WEBSITE_WEBSITE_ID" indexType="btree"> - <column name="website_id"/> - </index> - </table> - <table name="catalogrule_website" resource="default" engine="innodb" comment="Catalog Rules To Websites Relations"> -- <column xsi:type="int" name="rule_id" padding="10" unsigned="true" nullable="false" identity="false"/> -+ <column xsi:type="int" name="rule_id" padding="10" unsigned="true" nullable="false" identity="false" -+ comment="Rule ID"/> - <column xsi:type="smallint" name="website_id" padding="5" unsigned="true" nullable="false" identity="false" - comment="Website Id"/> -- <constraint xsi:type="primary" name="PRIMARY"> -+ <constraint xsi:type="primary" referenceId="PRIMARY"> - <column name="rule_id"/> - <column name="website_id"/> - </constraint> -- <constraint xsi:type="foreign" name="CATALOGRULE_WEBSITE_RULE_ID_CATALOGRULE_RULE_ID" -+ <constraint xsi:type="foreign" referenceId="CATALOGRULE_WEBSITE_RULE_ID_CATALOGRULE_RULE_ID" - table="catalogrule_website" column="rule_id" referenceTable="catalogrule" referenceColumn="rule_id" - onDelete="CASCADE"/> -- <constraint xsi:type="foreign" name="CATALOGRULE_WEBSITE_WEBSITE_ID_STORE_WEBSITE_WEBSITE_ID" -+ <constraint xsi:type="foreign" referenceId="CATALOGRULE_WEBSITE_WEBSITE_ID_STORE_WEBSITE_WEBSITE_ID" - table="catalogrule_website" column="website_id" referenceTable="store_website" - referenceColumn="website_id" onDelete="CASCADE"/> -- <index name="CATALOGRULE_WEBSITE_WEBSITE_ID" indexType="btree"> -+ <index referenceId="CATALOGRULE_WEBSITE_WEBSITE_ID" indexType="btree"> - <column name="website_id"/> - </index> - </table> - <table name="catalogrule_customer_group" resource="default" engine="innodb" - comment="Catalog Rules To Customer Groups Relations"> -- <column xsi:type="int" name="rule_id" padding="10" unsigned="true" nullable="false" identity="false"/> -+ <column xsi:type="int" name="rule_id" padding="10" unsigned="true" nullable="false" identity="false" -+ comment="Rule ID"/> - <column xsi:type="int" name="customer_group_id" padding="10" unsigned="true" nullable="false" identity="false" - comment="Customer Group Id"/> -- <constraint xsi:type="primary" name="PRIMARY"> -+ <constraint xsi:type="primary" referenceId="PRIMARY"> - <column name="rule_id"/> - <column name="customer_group_id"/> - </constraint> -- <constraint xsi:type="foreign" name="CATALOGRULE_CUSTOMER_GROUP_RULE_ID_CATALOGRULE_RULE_ID" -+ <constraint xsi:type="foreign" referenceId="CATALOGRULE_CUSTOMER_GROUP_RULE_ID_CATALOGRULE_RULE_ID" - table="catalogrule_customer_group" column="rule_id" referenceTable="catalogrule" - referenceColumn="rule_id" onDelete="CASCADE"/> -- <constraint xsi:type="foreign" name="CATRULE_CSTR_GROUP_CSTR_GROUP_ID_CSTR_GROUP_CSTR_GROUP_ID" -+ <constraint xsi:type="foreign" referenceId="CATRULE_CSTR_GROUP_CSTR_GROUP_ID_CSTR_GROUP_CSTR_GROUP_ID" - table="catalogrule_customer_group" column="customer_group_id" referenceTable="customer_group" - referenceColumn="customer_group_id" onDelete="CASCADE"/> -- <index name="CATALOGRULE_CUSTOMER_GROUP_CUSTOMER_GROUP_ID" indexType="btree"> -+ <index referenceId="CATALOGRULE_CUSTOMER_GROUP_CUSTOMER_GROUP_ID" indexType="btree"> - <column name="customer_group_id"/> - </index> - </table> -@@ -177,7 +179,7 @@ - <column xsi:type="int" name="rule_product_id" padding="10" unsigned="true" nullable="false" identity="true" - comment="Rule Product Id"/> - <column xsi:type="int" name="rule_id" padding="10" unsigned="true" nullable="false" identity="false" default="0" -- comment="Rule Id"/> -+ comment="Rule ID"/> - <column xsi:type="int" name="from_time" padding="10" unsigned="true" nullable="false" identity="false" - default="0" comment="From Time"/> - <column xsi:type="int" name="to_time" padding="10" unsigned="true" nullable="false" identity="false" default="0" -@@ -187,7 +189,7 @@ - default="0" comment="Product Id"/> - <column xsi:type="varchar" name="action_operator" nullable="true" default="to_fixed" length="10" - comment="Action Operator"/> -- <column xsi:type="decimal" name="action_amount" scale="4" precision="12" unsigned="false" nullable="false" -+ <column xsi:type="decimal" name="action_amount" scale="6" precision="20" unsigned="false" nullable="false" - default="0" comment="Action Amount"/> - <column xsi:type="smallint" name="action_stop" padding="6" unsigned="false" nullable="false" identity="false" - default="0" comment="Action Stop"/> -@@ -195,10 +197,10 @@ - default="0" comment="Sort Order"/> - <column xsi:type="smallint" name="website_id" padding="5" unsigned="true" nullable="false" identity="false" - comment="Website Id"/> -- <constraint xsi:type="primary" name="PRIMARY"> -+ <constraint xsi:type="primary" referenceId="PRIMARY"> - <column name="rule_product_id"/> - </constraint> -- <constraint xsi:type="unique" name="UNQ_EAA51B56FF092A0DCB795D1CEF812B7B"> -+ <constraint xsi:type="unique" referenceId="UNQ_EAA51B56FF092A0DCB795D1CEF812B7B"> - <column name="rule_id"/> - <column name="from_time"/> - <column name="to_time"/> -@@ -207,19 +209,19 @@ - <column name="product_id"/> - <column name="sort_order"/> - </constraint> -- <index name="CATALOGRULE_PRODUCT_CUSTOMER_GROUP_ID" indexType="btree"> -+ <index referenceId="CATALOGRULE_PRODUCT_CUSTOMER_GROUP_ID" indexType="btree"> - <column name="customer_group_id"/> - </index> -- <index name="CATALOGRULE_PRODUCT_WEBSITE_ID" indexType="btree"> -+ <index referenceId="CATALOGRULE_PRODUCT_WEBSITE_ID" indexType="btree"> - <column name="website_id"/> - </index> -- <index name="CATALOGRULE_PRODUCT_FROM_TIME" indexType="btree"> -+ <index referenceId="CATALOGRULE_PRODUCT_FROM_TIME" indexType="btree"> - <column name="from_time"/> - </index> -- <index name="CATALOGRULE_PRODUCT_TO_TIME" indexType="btree"> -+ <index referenceId="CATALOGRULE_PRODUCT_TO_TIME" indexType="btree"> - <column name="to_time"/> - </index> -- <index name="CATALOGRULE_PRODUCT_PRODUCT_ID" indexType="btree"> -+ <index referenceId="CATALOGRULE_PRODUCT_PRODUCT_ID" indexType="btree"> - <column name="product_id"/> - </index> - </table> -@@ -231,48 +233,48 @@ - <column xsi:type="int" name="customer_group_id" padding="11" unsigned="false" nullable="true" identity="false"/> - <column xsi:type="int" name="product_id" padding="10" unsigned="true" nullable="false" identity="false" - default="0" comment="Product Id"/> -- <column xsi:type="decimal" name="rule_price" scale="4" precision="12" unsigned="false" nullable="false" -+ <column xsi:type="decimal" name="rule_price" scale="6" precision="20" unsigned="false" nullable="false" - default="0" comment="Rule Price"/> - <column xsi:type="smallint" name="website_id" padding="5" unsigned="true" nullable="false" identity="false" - comment="Website Id"/> - <column xsi:type="date" name="latest_start_date" comment="Latest StartDate"/> - <column xsi:type="date" name="earliest_end_date" comment="Earliest EndDate"/> -- <constraint xsi:type="primary" name="PRIMARY"> -+ <constraint xsi:type="primary" referenceId="PRIMARY"> - <column name="rule_product_price_id"/> - </constraint> -- <constraint xsi:type="unique" name="CATRULE_PRD_PRICE_RULE_DATE_WS_ID_CSTR_GROUP_ID_PRD_ID"> -+ <constraint xsi:type="unique" referenceId="CATRULE_PRD_PRICE_RULE_DATE_WS_ID_CSTR_GROUP_ID_PRD_ID"> - <column name="rule_date"/> - <column name="website_id"/> - <column name="customer_group_id"/> - <column name="product_id"/> - </constraint> -- <index name="CATALOGRULE_PRODUCT_PRICE_CUSTOMER_GROUP_ID" indexType="btree"> -+ <index referenceId="CATALOGRULE_PRODUCT_PRICE_CUSTOMER_GROUP_ID" indexType="btree"> - <column name="customer_group_id"/> - </index> -- <index name="CATALOGRULE_PRODUCT_PRICE_WEBSITE_ID" indexType="btree"> -+ <index referenceId="CATALOGRULE_PRODUCT_PRICE_WEBSITE_ID" indexType="btree"> - <column name="website_id"/> - </index> -- <index name="CATALOGRULE_PRODUCT_PRICE_PRODUCT_ID" indexType="btree"> -+ <index referenceId="CATALOGRULE_PRODUCT_PRICE_PRODUCT_ID" indexType="btree"> - <column name="product_id"/> - </index> - </table> - <table name="catalogrule_group_website_replica" resource="default" engine="innodb" - comment="CatalogRule Group Website"> - <column xsi:type="int" name="rule_id" padding="10" unsigned="true" nullable="false" identity="false" default="0" -- comment="Rule Id"/> -+ comment="Rule ID"/> - <column xsi:type="int" name="customer_group_id" padding="10" unsigned="true" nullable="false" identity="false" - default="0" comment="Customer Group Id"/> - <column xsi:type="smallint" name="website_id" padding="5" unsigned="true" nullable="false" identity="false" - default="0" comment="Website Id"/> -- <constraint xsi:type="primary" name="PRIMARY"> -+ <constraint xsi:type="primary" referenceId="PRIMARY"> - <column name="rule_id"/> - <column name="customer_group_id"/> - <column name="website_id"/> - </constraint> -- <index name="CATALOGRULE_GROUP_WEBSITE_CUSTOMER_GROUP_ID" indexType="btree"> -+ <index referenceId="CATALOGRULE_GROUP_WEBSITE_CUSTOMER_GROUP_ID" indexType="btree"> - <column name="customer_group_id"/> - </index> -- <index name="CATALOGRULE_GROUP_WEBSITE_WEBSITE_ID" indexType="btree"> -+ <index referenceId="CATALOGRULE_GROUP_WEBSITE_WEBSITE_ID" indexType="btree"> - <column name="website_id"/> - </index> - </table> -diff --git a/app/code/Magento/CatalogRule/etc/mview.xml b/app/code/Magento/CatalogRule/etc/mview.xml -index 35efe33461a..9e5a1c866a8 100644 ---- a/app/code/Magento/CatalogRule/etc/mview.xml -+++ b/app/code/Magento/CatalogRule/etc/mview.xml -@@ -16,7 +16,6 @@ - <table name="catalog_product_entity" entity_column="entity_id" /> - <table name="catalog_product_entity_datetime" entity_column="entity_id" /> - <table name="catalog_product_entity_decimal" entity_column="entity_id" /> -- <table name="catalog_product_entity_gallery" entity_column="entity_id" /> - <table name="catalog_product_entity_int" entity_column="entity_id" /> - <table name="catalog_product_entity_text" entity_column="entity_id" /> - <table name="catalog_product_entity_tier_price" entity_column="entity_id" /> -diff --git a/app/code/Magento/CatalogRule/view/adminhtml/layout/catalog_rule_promo_catalog_block.xml b/app/code/Magento/CatalogRule/view/adminhtml/layout/catalog_rule_promo_catalog_block.xml -index 99d64ed7a63..f38f6e0fcd8 100644 ---- a/app/code/Magento/CatalogRule/view/adminhtml/layout/catalog_rule_promo_catalog_block.xml -+++ b/app/code/Magento/CatalogRule/view/adminhtml/layout/catalog_rule_promo_catalog_block.xml -@@ -11,7 +11,7 @@ - <block class="Magento\Backend\Block\Widget\Grid" name="promo.catalog.grid" as="grid"> - <arguments> - <argument name="id" xsi:type="string">promo_catalog_grid</argument> -- <argument name="dataSource" xsi:type="object">Magento\CatalogRule\Model\ResourceModel\Grid\Collection</argument> -+ <argument name="dataSource" xsi:type="object" shared="false">Magento\CatalogRule\Model\ResourceModel\Grid\Collection</argument> - <argument name="default_sort" xsi:type="string">name</argument> - <argument name="default_dir" xsi:type="string">ASC</argument> - <argument name="save_parameters_in_session" xsi:type="string">1</argument> -diff --git a/app/code/Magento/CatalogRule/view/adminhtml/templates/promo/fieldset.phtml b/app/code/Magento/CatalogRule/view/adminhtml/templates/promo/fieldset.phtml -index 1798b111242..1c3eedf43b2 100644 ---- a/app/code/Magento/CatalogRule/view/adminhtml/templates/promo/fieldset.phtml -+++ b/app/code/Magento/CatalogRule/view/adminhtml/templates/promo/fieldset.phtml -@@ -4,17 +4,15 @@ - * See COPYING.txt for license details. - */ - --// @codingStandardsIgnoreFile -- - /**@var \Magento\Backend\Block\Widget\Form\Renderer\Fieldset $block */ - ?> - <?php $_element = $block->getElement() ?> - <?php $_jsObjectName = $block->getFieldSetId() != null ? $block->getFieldSetId() : $_element->getHtmlId() ?> - <div class="rule-tree"> -- <fieldset id="<?= /* @escapeNotVerified */ $_jsObjectName ?>" <?= /* @escapeNotVerified */ $_element->serialize(['class']) ?> class="fieldset"> -- <legend class="legend"><span><?= /* @escapeNotVerified */ $_element->getLegend() ?></span></legend> -+ <fieldset id="<?= $block->escapeHtmlAttr($_jsObjectName) ?>" <?= /* @noEscape */ $_element->serialize(['class']) ?> class="fieldset"> -+ <legend class="legend"><span><?= $block->escapeHtml($_element->getLegend()) ?></span></legend> - <br> -- <?php if ($_element->getComment()): ?> -+ <?php if ($_element->getComment()) : ?> - <div class="messages"> - <div class="message message-notice"><?= $block->escapeHtml($_element->getComment()) ?></div> - </div> -@@ -30,9 +28,9 @@ require([ - "prototype" - ], function(VarienRulesForm){ - --window.<?= /* @escapeNotVerified */ $_jsObjectName ?> = new VarienRulesForm('<?= /* @escapeNotVerified */ $_jsObjectName ?>', '<?= /* @escapeNotVerified */ $block->getNewChildUrl() ?>'); --<?php if ($_element->getReadonly()): ?> -- <?= $_element->getHtmlId() ?>.setReadonly(true); -+window.<?= /* @noEscape */ $_jsObjectName ?> = new VarienRulesForm('<?= /* @noEscape */ $_jsObjectName ?>', '<?= /* @noEscape */ $block->getNewChildUrl() ?>'); -+<?php if ($_element->getReadonly()) : ?> -+ <?= /* @noEscape */ $_element->getHtmlId() ?>.setReadonly(true); - <?php endif; ?> - - }); -diff --git a/app/code/Magento/CatalogRule/view/adminhtml/templates/promo/form.phtml b/app/code/Magento/CatalogRule/view/adminhtml/templates/promo/form.phtml -index 1572c8f0d4e..5aaa22305a1 100644 ---- a/app/code/Magento/CatalogRule/view/adminhtml/templates/promo/form.phtml -+++ b/app/code/Magento/CatalogRule/view/adminhtml/templates/promo/form.phtml -@@ -3,9 +3,6 @@ - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -- --// @codingStandardsIgnoreFile -- - ?> - <div class="entry-edit rule-tree"> - <?= $block->getFormHtml() ?> -diff --git a/app/code/Magento/CatalogRule/view/adminhtml/ui_component/catalog_rule_form.xml b/app/code/Magento/CatalogRule/view/adminhtml/ui_component/catalog_rule_form.xml -index fb34a3ac4bb..8174434a06c 100644 ---- a/app/code/Magento/CatalogRule/view/adminhtml/ui_component/catalog_rule_form.xml -+++ b/app/code/Magento/CatalogRule/view/adminhtml/ui_component/catalog_rule_form.xml -@@ -136,7 +136,7 @@ - </validation> - <dataType>number</dataType> - <tooltip> -- <link>http://docs.magento.com/m2/ce/user_guide/configuration/scope.html</link> -+ <link>https://docs.magento.com/m2/ce/user_guide/configuration/scope.html</link> - <description>What is this?</description> - </tooltip> - <label translate="true">Websites</label> -diff --git a/app/code/Magento/CatalogRuleConfigurable/Test/Mftf/ActionGroup/StorefrontAssertCatalogPriceRuleAppliedToProductOptionActionGroup.xml b/app/code/Magento/CatalogRuleConfigurable/Test/Mftf/ActionGroup/StorefrontAssertCatalogPriceRuleAppliedToProductOptionActionGroup.xml -new file mode 100644 -index 00000000000..4345d575a33 ---- /dev/null -+++ b/app/code/Magento/CatalogRuleConfigurable/Test/Mftf/ActionGroup/StorefrontAssertCatalogPriceRuleAppliedToProductOptionActionGroup.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="StorefrontAssertCatalogPriceRuleAppliedToProductOptionActionGroup"> -+ <arguments> -+ <argument name="option" type="string"/> -+ <argument name="expectedPrice" type="string"/> -+ </arguments> -+ <selectOption userInput="{{option}}" selector="{{StorefrontProductInfoMainSection.productAttributeOptionsSelectButton}}" stepKey="selectOption"/> -+ <see userInput="{{expectedPrice}}" selector="{{StorefrontProductInfoMainSection.productPrice}}" stepKey="seeProductPrice"/> -+ </actionGroup> -+</actionGroups> -diff --git a/app/code/Magento/CatalogRuleConfigurable/Test/Mftf/ActionGroup/StorefrontAssertUpdatedProductPriceInStorefrontProductPageActionGroup.xml b/app/code/Magento/CatalogRuleConfigurable/Test/Mftf/ActionGroup/StorefrontAssertUpdatedProductPriceInStorefrontProductPageActionGroup.xml -new file mode 100644 -index 00000000000..56480583af7 ---- /dev/null -+++ b/app/code/Magento/CatalogRuleConfigurable/Test/Mftf/ActionGroup/StorefrontAssertUpdatedProductPriceInStorefrontProductPageActionGroup.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="StorefrontAssertUpdatedProductPriceInStorefrontProductPageActionGroup"> -+ <arguments> -+ <argument name="productName" type="string"/> -+ <argument name="expectedPrice" type="string"/> -+ </arguments> -+ <seeInTitle userInput="{{productName}}" stepKey="assertProductNameTitle"/> -+ <see userInput="{{productName}}" selector="{{StorefrontProductInfoMainSection.productName}}" stepKey="assertProductName"/> -+ <see userInput="{{expectedPrice}}" selector="{{StorefrontProductInfoMainSection.productPrice}}" stepKey="assertProductPrice"/> -+ </actionGroup> -+</actionGroups> -diff --git a/app/code/Magento/CatalogRuleConfigurable/Test/Mftf/Test/AdminApplyCatalogRuleForConfigurableProductWithAssignedSimpleProductsTest.xml b/app/code/Magento/CatalogRuleConfigurable/Test/Mftf/Test/AdminApplyCatalogRuleForConfigurableProductWithAssignedSimpleProductsTest.xml -new file mode 100644 -index 00000000000..3e700b5bcfb ---- /dev/null -+++ b/app/code/Magento/CatalogRuleConfigurable/Test/Mftf/Test/AdminApplyCatalogRuleForConfigurableProductWithAssignedSimpleProductsTest.xml -@@ -0,0 +1,268 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> -+ <test name="AdminApplyCatalogRuleForConfigurableProductWithAssignedSimpleProductsTest"> -+ <annotations> -+ <features value="CatalogRuleConfigurable"/> -+ <stories value="Apply catalog price rule"/> -+ <title value="Apply catalog rule for configurable product with assigned simple products"/> -+ <description value="Admin should be able to apply catalog rule for configurable product with assigned simple products"/> -+ <severity value="CRITICAL"/> -+ <testCaseId value="MC-14063"/> -+ <group value="catalogRuleConfigurable"/> -+ <group value="mtf_migrated"/> -+ <skip> -+ <issueId value="MC-17140"/> -+ </skip> -+ </annotations> -+ <before> -+ <!-- Create category for first configurable product --> -+ <createData entity="SimpleSubCategory" stepKey="firstSimpleCategory"/> -+ -+ <!-- Create first configurable product with two options --> -+ <createData entity="ApiConfigurableProduct" stepKey="createFirstConfigProduct"> -+ <requiredEntity createDataKey="firstSimpleCategory"/> -+ </createData> -+ -+ <createData entity="productAttributeWithTwoOptions" stepKey="createFirstConfigProductAttribute"/> -+ <createData entity="productAttributeOption1" stepKey="createFirstConfigProductAttributeFirstOption"> -+ <requiredEntity createDataKey="createFirstConfigProductAttribute"/> -+ </createData> -+ <createData entity="productAttributeOption2" stepKey="createFirstConfigProductAttributeSecondOption"> -+ <requiredEntity createDataKey="createFirstConfigProductAttribute"/> -+ </createData> -+ -+ <createData entity="AddToDefaultSet" stepKey="addFirstProductToAttributeSet"> -+ <requiredEntity createDataKey="createFirstConfigProductAttribute"/> -+ </createData> -+ -+ <getData entity="ProductAttributeOptionGetter" index="1" stepKey="getFirstConfigAttributeFirstOption"> -+ <requiredEntity createDataKey="createFirstConfigProductAttribute"/> -+ </getData> -+ <getData entity="ProductAttributeOptionGetter" index="2" stepKey="getFirstConfigAttributeSecondOption"> -+ <requiredEntity createDataKey="createFirstConfigProductAttribute"/> -+ </getData> -+ -+ <!-- Create two child products for first configurable product --> -+ <createData entity="ApiSimpleOne" stepKey="createFirstConfigFirstChildProduct"> -+ <requiredEntity createDataKey="createFirstConfigProductAttribute"/> -+ <requiredEntity createDataKey="getFirstConfigAttributeFirstOption"/> -+ </createData> -+ -+ <createData entity="ApiSimpleOne" stepKey="createFirstConfigSecondChildProduct"> -+ <requiredEntity createDataKey="createFirstConfigProductAttribute"/> -+ <requiredEntity createDataKey="getFirstConfigAttributeSecondOption"/> -+ </createData> -+ -+ <createData entity="ConfigurableProductTwoOptions" stepKey="createFirstConfigProductOption"> -+ <requiredEntity createDataKey="createFirstConfigProduct"/> -+ <requiredEntity createDataKey="createFirstConfigProductAttribute"/> -+ <requiredEntity createDataKey="getFirstConfigAttributeFirstOption"/> -+ <requiredEntity createDataKey="getFirstConfigAttributeSecondOption"/> -+ </createData> -+ -+ <createData entity="ConfigurableProductAddChild" stepKey="createFirstConfigProductAddFirstChild"> -+ <requiredEntity createDataKey="createFirstConfigProduct"/> -+ <requiredEntity createDataKey="createFirstConfigFirstChildProduct"/> -+ </createData> -+ <createData entity="ConfigurableProductAddChild" stepKey="createFirstConfigProductAddSecondChild"> -+ <requiredEntity createDataKey="createFirstConfigProduct"/> -+ <requiredEntity createDataKey="createFirstConfigSecondChildProduct"/> -+ </createData> -+ -+ <!-- Add customizable options to first product --> -+ <updateData createDataKey="createFirstConfigProduct" entity="productWithOptionRadiobutton" stepKey="updateFirstProductWithOption"/> -+ -+ <!-- Create category for second configurable product --> -+ <createData entity="SimpleSubCategory" stepKey="secondSimpleCategory"/> -+ -+ <!-- Create second configurable product with two options --> -+ <createData entity="ApiConfigurableProduct" stepKey="createSecondConfigProduct"> -+ <requiredEntity createDataKey="secondSimpleCategory"/> -+ </createData> -+ -+ <createData entity="productAttributeWithTwoOptions" stepKey="createSecondConfigProductAttribute"/> -+ <createData entity="productAttributeOption1" stepKey="createSecondConfigProductAttributeFirstOption"> -+ <requiredEntity createDataKey="createSecondConfigProductAttribute"/> -+ </createData> -+ <createData entity="productAttributeOption2" stepKey="createSecondConfigProductAttributeSecondOption"> -+ <requiredEntity createDataKey="createSecondConfigProductAttribute"/> -+ </createData> -+ -+ <createData entity="AddToDefaultSet" stepKey="addSecondProductToAttributeSet"> -+ <requiredEntity createDataKey="createSecondConfigProductAttribute"/> -+ </createData> -+ -+ <getData entity="ProductAttributeOptionGetter" index="1" stepKey="getSecondConfigAttributeFirstOption"> -+ <requiredEntity createDataKey="createSecondConfigProductAttribute"/> -+ </getData> -+ <getData entity="ProductAttributeOptionGetter" index="2" stepKey="getSecondConfigAttributeSecondOption"> -+ <requiredEntity createDataKey="createSecondConfigProductAttribute"/> -+ </getData> -+ -+ <!-- Create two child products for second configurable product --> -+ <createData entity="ApiSimpleOne" stepKey="createSecondConfigFirstChildProduct"> -+ <requiredEntity createDataKey="createSecondConfigProductAttribute"/> -+ <requiredEntity createDataKey="getSecondConfigAttributeFirstOption"/> -+ </createData> -+ -+ <createData entity="ApiSimpleOne" stepKey="createSecondConfigSecondChildProduct"> -+ <requiredEntity createDataKey="createSecondConfigProductAttribute"/> -+ <requiredEntity createDataKey="getSecondConfigAttributeSecondOption"/> -+ </createData> -+ -+ <createData entity="ConfigurableProductTwoOptions" stepKey="createSecondConfigProductOption"> -+ <requiredEntity createDataKey="createSecondConfigProduct"/> -+ <requiredEntity createDataKey="createSecondConfigProductAttribute"/> -+ <requiredEntity createDataKey="getSecondConfigAttributeFirstOption"/> -+ <requiredEntity createDataKey="getSecondConfigAttributeSecondOption"/> -+ </createData> -+ -+ <createData entity="ConfigurableProductAddChild" stepKey="createSecondConfigProductAddFirstChild"> -+ <requiredEntity createDataKey="createSecondConfigProduct"/> -+ <requiredEntity createDataKey="createSecondConfigFirstChildProduct"/> -+ </createData> -+ <createData entity="ConfigurableProductAddChild" stepKey="createSecondConfigProductAddSecondChild"> -+ <requiredEntity createDataKey="createSecondConfigProduct"/> -+ <requiredEntity createDataKey="createSecondConfigSecondChildProduct"/> -+ </createData> -+ -+ <!-- Add customizable options to second product --> -+ <updateData createDataKey="createSecondConfigProduct" entity="productWithOptionRadiobutton" stepKey="updateSecondProductWithOption"/> -+ -+ <!--Create customer group --> -+ <createData entity="CustomCustomerGroup" stepKey="customerGroup"/> -+ -+ <!-- Create Customer --> -+ <createData entity="SimpleUsCustomerWithNewCustomerGroup" stepKey="createCustomer"> -+ <requiredEntity createDataKey="customerGroup" /> -+ </createData> -+ -+ <!-- Login as Admin --> -+ <actionGroup ref="LoginAsAdmin" stepKey="loginToAdminPanel"/> -+ </before> -+ <after> -+ <!-- Customer log out --> -+ <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="customerLogout"/> -+ -+ <!-- Delete created data --> -+ <deleteData createDataKey="createFirstConfigProduct" stepKey="deleteFirstConfigProduct"/> -+ <deleteData createDataKey="createFirstConfigFirstChildProduct" stepKey="deleteFirstConfigFirstChildProduct"/> -+ <deleteData createDataKey="createFirstConfigSecondChildProduct" stepKey="deleteFirstConfigSecondChildProduct"/> -+ <deleteData createDataKey="createFirstConfigProductAttribute" stepKey="deleteFirstConfigProductAttribute"/> -+ <deleteData createDataKey="firstSimpleCategory" stepKey="deleteFirstSimpleCategory"/> -+ -+ <deleteData createDataKey="createSecondConfigProduct" stepKey="deleteSecondConfigProduct"/> -+ <deleteData createDataKey="createSecondConfigFirstChildProduct" stepKey="deleteSecondConfigFirstChildProduct"/> -+ <deleteData createDataKey="createSecondConfigSecondChildProduct" stepKey="deleteSecondConfigSecondChildProduct"/> -+ <deleteData createDataKey="createSecondConfigProductAttribute" stepKey="deleteSecondConfigProductAttribute"/> -+ <deleteData createDataKey="secondSimpleCategory" stepKey="deleteSimpleCategory"/> -+ -+ <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> -+ <deleteData createDataKey="customerGroup" stepKey="deleteCustomerGroup"/> -+ -+ <!-- Delete created price rules --> -+ <amOnPage url="{{CatalogRulePage.url}}" stepKey="goToPriceRulePage"/> -+ <actionGroup ref="deleteEntitySecondaryGrid" stepKey="deletePriceRuleForFirstOption"> -+ <argument name="name" value="{{CatalogRuleToFixed.name}}"/> -+ <argument name="searchInput" value="{{AdminSecondaryGridSection.catalogRuleIdentifierSearch}}"/> -+ </actionGroup> -+ -+ <!-- Admin log out --> -+ <actionGroup ref="logout" stepKey="logout"/> -+ </after> -+ <!-- Create price rule --> -+ <actionGroup ref="newCatalogPriceRuleByUI" stepKey="createPriceRule"> -+ <argument name="catalogRule" value="CatalogRuleToFixed"/> -+ </actionGroup> -+ <actionGroup ref="CatalogSelectCustomerGroupActionGroup" stepKey="addCustomerGroup"> -+ <argument name="customerGroupName" value="$$customerGroup.code$$"/> -+ </actionGroup> -+ -+ <!-- Save price rule --> -+ <click selector="{{AdminNewCatalogPriceRule.saveAndApply}}" stepKey="clickSaveAndApplyRule"/> -+ <see selector="{{AdminCategoryMessagesSection.SuccessMessage}}" userInput="You saved the rule." stepKey="assertSuccessMessage"/> -+ -+ <!-- Run full reindex and clear caches --> -+ <magentoCLI command="indexer:reindex" stepKey="reindex"/> -+ <magentoCLI command="cache:flush" stepKey="flushCache"/> -+ -+ <!-- Login to storefront from customer --> -+ <actionGroup ref="LoginToStorefrontActionGroup" stepKey="logInFromCustomer"> -+ <argument name="Customer" value="$$createCustomer$$"/> -+ </actionGroup> -+ -+ <!-- Assert first product in category --> -+ <amOnPage url="$$firstSimpleCategory.name$$.html" stepKey="goToFirstCategoryPageStorefront"/> -+ <waitForPageLoad stepKey="waitForFirstCategoryPageLoad"/> -+ <actionGroup ref="StorefrontCheckCategoryConfigurableProductWithUpdatedPriceActionGroup" stepKey="checkFirstProductPriceInCategory"> -+ <argument name="productName" value="$$createFirstConfigProduct.name$$"/> -+ <argument name="expectedPrice" value="{{CatalogRuleToFixed.discount_amount}}"/> -+ </actionGroup> -+ -+ <!-- Assert second product in category --> -+ <amOnPage url="$$secondSimpleCategory.name$$.html" stepKey="goToSecondCategoryPageStorefront"/> -+ <waitForPageLoad stepKey="waitForSecondCategoryPageLoad"/> -+ <actionGroup ref="StorefrontCheckCategoryConfigurableProductWithUpdatedPriceActionGroup" stepKey="checkSecondProductPriceInCategory"> -+ <argument name="productName" value="$$createSecondConfigProduct.name$$"/> -+ <argument name="expectedPrice" value="{{CatalogRuleToFixed.discount_amount}}"/> -+ </actionGroup> -+ -+ <!-- Assert first product in storefront product page --> -+ <amOnPage url="$$createFirstConfigProduct.custom_attributes[url_key]$$.html" stepKey="amOnFirstProductPage"/> -+ <waitForPageLoad stepKey="waitForFirstProductPageLoad"/> -+ <actionGroup ref="StorefrontAssertUpdatedProductPriceInStorefrontProductPageActionGroup" stepKey="checkFirstProductPriceInStorefrontProductPage"> -+ <argument name="productName" value="$$createFirstConfigProduct.name$$"/> -+ <argument name="expectedPrice" value="{{CatalogRuleToFixed.discount_amount}}"/> -+ </actionGroup> -+ -+ <!-- Add first product with selected options to the cart --> -+ <actionGroup ref="StorefrontAddProductWithSelectedConfigurableAndCustomOptionsToCartActionGroup" stepKey="addFirstProductWithSelectedOptionToCart1"> -+ <argument name="product" value="$$createFirstConfigProduct$$"/> -+ <argument name="option" value="$$createFirstConfigProductAttributeFirstOption.option[store_labels][1][label]$$"/> -+ <argument name="customizableOption" value="{{ProductOptionValueRadioButtons1.title}}"/> -+ </actionGroup> -+ <grabTextFrom selector="{{StorefrontProductInfoMainSection.updatedPrice}}" stepKey="grabForthProductUpdatedPrice1"/> -+ -+ <actionGroup ref="StorefrontAddProductWithSelectedConfigurableAndCustomOptionsToCartActionGroup" stepKey="addFirstProductWithSelectedOptionToCart2"> -+ <argument name="product" value="$$createFirstConfigProduct$$"/> -+ <argument name="option" value="$$createFirstConfigProductAttributeSecondOption.option[store_labels][1][label]$$"/> -+ <argument name="customizableOption" value="{{ProductOptionValueRadioButtons3.title}}"/> -+ </actionGroup> -+ -+ <!-- Assert second product in storefront product page --> -+ <amOnPage url="$$createSecondConfigProduct.custom_attributes[url_key]$$.html" stepKey="amOnSecondProductPage"/> -+ <waitForPageLoad stepKey="waitForSecondProductPageLoad"/> -+ <actionGroup ref="StorefrontAssertUpdatedProductPriceInStorefrontProductPageActionGroup" stepKey="checkSecondProductPriceInStorefrontProductPage"> -+ <argument name="productName" value="$$createSecondConfigProduct.name$$"/> -+ <argument name="expectedPrice" value="{{CatalogRuleToFixed.discount_amount}}"/> -+ </actionGroup> -+ -+ <!-- Add second product with selected options to the cart --> -+ <actionGroup ref="StorefrontAddProductWithSelectedConfigurableAndCustomOptionsToCartActionGroup" stepKey="addSecondProductWithSelectedOptionToCart1"> -+ <argument name="product" value="$$createSecondConfigProduct$$"/> -+ <argument name="option" value="$$createSecondConfigProductAttributeFirstOption.option[store_labels][1][label]$$"/> -+ <argument name="customizableOption" value="{{ProductOptionValueRadioButtons1.title}}"/> -+ </actionGroup> -+ -+ <actionGroup ref="StorefrontAddProductWithSelectedConfigurableAndCustomOptionsToCartActionGroup" stepKey="addSecondProductWithSelectedOptionToCart2"> -+ <argument name="product" value="$$createSecondConfigProduct$$"/> -+ <argument name="option" value="$$createSecondConfigProductAttributeSecondOption.option[store_labels][1][label]$$"/> -+ <argument name="customizableOption" value="{{ProductOptionValueRadioButtons3.title}}"/> -+ </actionGroup> -+ -+ <!--Assert products prices in the cart --> -+ <amOnPage url="/checkout/cart/" stepKey="amOnShoppingCartPage"/> -+ <waitForPageLoad stepKey="waitForShoppingCartPageLoad"/> -+ <see userInput="$210.69" selector="{{CheckoutCartProductSection.ProductPriceByOption($$createFirstConfigProductAttributeFirstOption.option[store_labels][1][label]$$)}}" stepKey="assertFirstProductPriceForFirstProductOption"/> -+ <see userInput="$120.70" selector="{{CheckoutCartProductSection.ProductPriceByOption($$createFirstConfigProductAttributeSecondOption.option[store_labels][1][label]$$)}}" stepKey="assertFirstProductPriceForSecondProductOption"/> -+ <see userInput="$210.69" selector="{{CheckoutCartProductSection.ProductPriceByOption($$createSecondConfigProductAttributeFirstOption.option[store_labels][1][label]$$)}}" stepKey="assertSecondProductPriceForFirstProductOption"/> -+ <see userInput="$120.70" selector="{{CheckoutCartProductSection.ProductPriceByOption($$createSecondConfigProductAttributeSecondOption.option[store_labels][1][label]$$)}}" stepKey="assertSecondProductPriceForSecondProductOption"/> -+ </test> -+</tests> -diff --git a/app/code/Magento/CatalogRuleConfigurable/Test/Mftf/Test/AdminApplyCatalogRuleForConfigurableProductWithOptionsTest.xml b/app/code/Magento/CatalogRuleConfigurable/Test/Mftf/Test/AdminApplyCatalogRuleForConfigurableProductWithOptionsTest.xml -new file mode 100644 -index 00000000000..e53e51c626a ---- /dev/null -+++ b/app/code/Magento/CatalogRuleConfigurable/Test/Mftf/Test/AdminApplyCatalogRuleForConfigurableProductWithOptionsTest.xml -@@ -0,0 +1,221 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> -+ <test name="AdminApplyCatalogRuleForConfigurableProductWithOptionsTest"> -+ <annotations> -+ <features value="CatalogRuleConfigurable"/> -+ <stories value="Apply catalog price rule"/> -+ <title value="Apply catalog price rule for configurable product with options"/> -+ <description value="Admin should be able to apply the catalog rule for configurable product with options"/> -+ <severity value="CRITICAL"/> -+ <testCaseId value="MC-14062"/> -+ <group value="catalogRuleConfigurable"/> -+ <group value="mtf_migrated"/> -+ <skip> -+ <issueId value="MC-17140"/> -+ </skip> -+ </annotations> -+ <before> -+ <!-- Create category --> -+ <createData entity="SimpleSubCategory" stepKey="simpleCategory"/> -+ -+ <!-- Create configurable product with three options --> -+ <createData entity="ApiConfigurableProduct" stepKey="createConfigProduct"> -+ <requiredEntity createDataKey="simpleCategory"/> -+ </createData> -+ -+ <createData entity="productAttributeWithTwoOptions" stepKey="createConfigProductAttribute"/> -+ <createData entity="productAttributeOption1" stepKey="createConfigProductAttributeFirstOption"> -+ <requiredEntity createDataKey="createConfigProductAttribute"/> -+ </createData> -+ <createData entity="productAttributeOption2" stepKey="createConfigProductAttributeSecondOption"> -+ <requiredEntity createDataKey="createConfigProductAttribute"/> -+ </createData> -+ <createData entity="productAttributeOption3" stepKey="createConfigProductAttributeThirdOption"> -+ <requiredEntity createDataKey="createConfigProductAttribute"/> -+ </createData> -+ -+ <createData entity="AddToDefaultSet" stepKey="createConfigAddToAttributeSet"> -+ <requiredEntity createDataKey="createConfigProductAttribute"/> -+ </createData> -+ -+ <getData entity="ProductAttributeOptionGetter" index="1" stepKey="getConfigAttributeFirstOption"> -+ <requiredEntity createDataKey="createConfigProductAttribute"/> -+ </getData> -+ <getData entity="ProductAttributeOptionGetter" index="2" stepKey="getConfigAttributeSecondOption"> -+ <requiredEntity createDataKey="createConfigProductAttribute"/> -+ </getData> -+ <getData entity="ProductAttributeOptionGetter" index="3" stepKey="getConfigAttributeThirdOption"> -+ <requiredEntity createDataKey="createConfigProductAttribute"/> -+ </getData> -+ -+ <!-- Create three child products --> -+ <createData entity="ApiSimpleOne" stepKey="createConfigFirstChildProduct"> -+ <requiredEntity createDataKey="createConfigProductAttribute"/> -+ <requiredEntity createDataKey="getConfigAttributeFirstOption"/> -+ </createData> -+ -+ <createData entity="ApiSimpleOne" stepKey="createConfigSecondChildProduct"> -+ <requiredEntity createDataKey="createConfigProductAttribute"/> -+ <requiredEntity createDataKey="getConfigAttributeSecondOption"/> -+ </createData> -+ -+ <createData entity="ApiSimpleOne" stepKey="createConfigThirdChildProduct"> -+ <requiredEntity createDataKey="createConfigProductAttribute"/> -+ <requiredEntity createDataKey="getConfigAttributeThirdOption"/> -+ </createData> -+ -+ <createData entity="ConfigurableProductTwoOptions" stepKey="createConfigProductOption"> -+ <requiredEntity createDataKey="createConfigProduct"/> -+ <requiredEntity createDataKey="createConfigProductAttribute"/> -+ <requiredEntity createDataKey="getConfigAttributeFirstOption"/> -+ <requiredEntity createDataKey="getConfigAttributeSecondOption"/> -+ <requiredEntity createDataKey="getConfigAttributeThirdOption"/> -+ </createData> -+ -+ <createData entity="ConfigurableProductAddChild" stepKey="createConfigProductAddFirstChild"> -+ <requiredEntity createDataKey="createConfigProduct"/> -+ <requiredEntity createDataKey="createConfigFirstChildProduct"/> -+ </createData> -+ <createData entity="ConfigurableProductAddChild" stepKey="createConfigProductAddSecondChild"> -+ <requiredEntity createDataKey="createConfigProduct"/> -+ <requiredEntity createDataKey="createConfigSecondChildProduct"/> -+ </createData> -+ <createData entity="ConfigurableProductAddChild" stepKey="createConfigProductAddThirdChild"> -+ <requiredEntity createDataKey="createConfigProduct"/> -+ <requiredEntity createDataKey="createConfigThirdChildProduct"/> -+ </createData> -+ -+ <!-- Login as Admin --> -+ <actionGroup ref="LoginAsAdmin" stepKey="loginToAdminPanel"/> -+ </before> -+ <after> -+ <!-- Delete created data --> -+ <deleteData createDataKey="createConfigProduct" stepKey="deleteConfigProduct"/> -+ <deleteData createDataKey="createConfigFirstChildProduct" stepKey="deleteFirstSimpleProduct"/> -+ <deleteData createDataKey="createConfigSecondChildProduct" stepKey="deleteSecondSimpleProduct"/> -+ <deleteData createDataKey="createConfigThirdChildProduct" stepKey="deleteThirdSimpleProduct"/> -+ <deleteData createDataKey="createConfigProductAttribute" stepKey="deleteConfigProductAttribute"/> -+ <deleteData createDataKey="simpleCategory" stepKey="deleteCategory"/> -+ -+ <!-- Delete created price rules --> -+ <amOnPage url="{{CatalogRulePage.url}}" stepKey="goToPriceRulePage"/> -+ <actionGroup ref="deleteEntitySecondaryGrid" stepKey="deletePriceRuleForFirstOption"> -+ <argument name="name" value="{{CatalogRuleToFixed.name}}"/> -+ <argument name="searchInput" value="{{AdminSecondaryGridSection.catalogRuleIdentifierSearch}}"/> -+ </actionGroup> -+ -+ <actionGroup ref="deleteEntitySecondaryGrid" stepKey="deletePriceRuleForSecondOption"> -+ <argument name="name" value="{{_defaultCatalogRule.name}}"/> -+ <argument name="searchInput" value="{{AdminSecondaryGridSection.catalogRuleIdentifierSearch}}"/> -+ </actionGroup> -+ -+ <actionGroup ref="deleteEntitySecondaryGrid" stepKey="deletePriceRuleForThirdOption"> -+ <argument name="name" value="{{CatalogRuleWithoutDiscount.name}}"/> -+ <argument name="searchInput" value="{{AdminSecondaryGridSection.catalogRuleIdentifierSearch}}"/> -+ </actionGroup> -+ -+ <!-- Log out --> -+ <actionGroup ref="logout" stepKey="logout"/> -+ </after> -+ -+ <!-- Create price rule for first configurable product option --> -+ <actionGroup ref="newCatalogPriceRuleByUI" stepKey="createFirstPriceRule"> -+ <argument name="catalogRule" value="CatalogRuleToFixed"/> -+ </actionGroup> -+ <actionGroup ref="selectNotLoggedInCustomerGroup" stepKey="selectNotLoggedInCustomerGroupForFirstPriceRule"/> -+ <actionGroup ref="CreateCatalogPriceRuleConditionWithAttributeAndOptionActionGroup" stepKey="createFirstCatalogPriceRuleCondition"> -+ <argument name="attributeName" value="$$createConfigProductAttribute.attribute[frontend_labels][0][label]$$"/> -+ <argument name="targetSelectValue" value="$$createConfigProductAttributeFirstOption.option[store_labels][1][label]$$"/> -+ <argument name="indexA" value="1"/> -+ <argument name="indexB" value="1"/> -+ </actionGroup> -+ <click selector="{{AdminNewCatalogPriceRule.saveAndApply}}" stepKey="saveAndApplyFirstPriceRule"/> -+ <see selector="{{AdminCategoryMessagesSection.SuccessMessage}}" userInput="You saved the rule." stepKey="assertSuccessMessageForFirstPriceRule"/> -+ -+ <!-- Create price rule for second configurable product option --> -+ <actionGroup ref="newCatalogPriceRuleByUI" stepKey="createSecondPriceRule"> -+ <argument name="catalogRule" value="_defaultCatalogRule"/> -+ </actionGroup> -+ <actionGroup ref="selectNotLoggedInCustomerGroup" stepKey="selectNotLoggedInCustomerGroupForSecondPriceRule"/> -+ <actionGroup ref="CreateCatalogPriceRuleConditionWithAttributeAndOptionActionGroup" stepKey="createSecondCatalogPriceRuleCondition"> -+ <argument name="attributeName" value="$$createConfigProductAttribute.attribute[frontend_labels][0][label]$$"/> -+ <argument name="targetSelectValue" value="$$createConfigProductAttributeSecondOption.option[store_labels][1][label]$$"/> -+ <argument name="indexA" value="1"/> -+ <argument name="indexB" value="1"/> -+ </actionGroup> -+ <click selector="{{AdminNewCatalogPriceRule.saveAndApply}}" stepKey="saveAndApplySecondPriceRule"/> -+ <see selector="{{AdminCategoryMessagesSection.SuccessMessage}}" userInput="You saved the rule." stepKey="assertSuccessMessageForSecondPriceRule"/> -+ -+ <!-- Create price rule for third configurable product option --> -+ <actionGroup ref="newCatalogPriceRuleByUI" stepKey="createThirdPriceRule"> -+ <argument name="catalogRule" value="CatalogRuleWithoutDiscount"/> -+ </actionGroup> -+ <actionGroup ref="selectNotLoggedInCustomerGroup" stepKey="selectNotLoggedInCustomerGroupForThirdPriceRule"/> -+ <actionGroup ref="CreateCatalogPriceRuleConditionWithAttributeAndOptionActionGroup" stepKey="createThirdCatalogPriceRuleCondition"> -+ <argument name="attributeName" value="$$createConfigProductAttribute.attribute[frontend_labels][0][label]$$"/> -+ <argument name="targetSelectValue" value="$$createConfigProductAttributeThirdOption.option[store_labels][1][label]$$"/> -+ <argument name="indexA" value="1"/> -+ <argument name="indexB" value="1"/> -+ </actionGroup> -+ <click selector="{{AdminNewCatalogPriceRule.saveAndApply}}" stepKey="saveAndApplyThirdPriceRule"/> -+ <see selector="{{AdminCategoryMessagesSection.SuccessMessage}}" userInput="You saved the rule." stepKey="assertSuccessMessageForThirdPriceRule"/> -+ -+ <!-- Run full reindex and clear caches --> -+ <magentoCLI command="indexer:reindex" stepKey="reindex"/> -+ <magentoCLI command="cache:flush" stepKey="flushCache"/> -+ -+ <!-- Assert product in storefront product page --> -+ <amOnPage url="$$createConfigProduct.custom_attributes[url_key]$$.html" stepKey="amOnProductPage"/> -+ <waitForPageLoad stepKey="waitForProductPageLoad"/> -+ <actionGroup ref="StorefrontAssertUpdatedProductPriceInStorefrontProductPageActionGroup" stepKey="assertUpdatedProductPriceInStorefrontProductPage"> -+ <argument name="productName" value="$$createConfigProduct.name$$"/> -+ <argument name="expectedPrice" value="{{CatalogRuleToFixed.discount_amount}}"/> -+ </actionGroup> -+ -+ <!-- Assert product options price in storefront product page --> -+ <actionGroup ref="StorefrontAssertCatalogPriceRuleAppliedToProductOptionActionGroup" stepKey="assertCatalogPriceRuleAppliedToFirstProductOption"> -+ <argument name="option" value="$$createConfigProductAttributeFirstOption.option[store_labels][1][label]$$"/> -+ <argument name="expectedPrice" value="{{CatalogRuleToFixed.discount_amount}}"/> -+ </actionGroup> -+ -+ <actionGroup ref="StorefrontAssertCatalogPriceRuleAppliedToProductOptionActionGroup" stepKey="assertCatalogPriceRuleAppliedToSecondProductOption"> -+ <argument name="option" value="$$createConfigProductAttributeSecondOption.option[store_labels][1][label]$$"/> -+ <argument name="expectedPrice" value="$110.70"/> -+ </actionGroup> -+ -+ <actionGroup ref="StorefrontAssertCatalogPriceRuleAppliedToProductOptionActionGroup" stepKey="assertCatalogPriceRuleAppliedToThirdProductOption"> -+ <argument name="option" value="$$createConfigProductAttributeThirdOption.option[store_labels][1][label]$$"/> -+ <argument name="expectedPrice" value="{{ApiConfigurableProduct.price}}"/> -+ </actionGroup> -+ -+ <!-- Add product with selected option to the cart --> -+ <actionGroup ref="StorefrontAddProductWithSelectedConfigurableOptionToCartActionGroup" stepKey="addProductWithSelectedFirstOptionToCart"> -+ <argument name="product" value="$$createConfigProduct$$"/> -+ <argument name="option" value="$$createConfigProductAttributeFirstOption.option[store_labels][1][label]$$"/> -+ </actionGroup> -+ -+ <actionGroup ref="StorefrontAddProductWithSelectedConfigurableOptionToCartActionGroup" stepKey="addProductWithSelectedSecondOptionToCart"> -+ <argument name="product" value="$$createConfigProduct$$"/> -+ <argument name="option" value="$$createConfigProductAttributeSecondOption.option[store_labels][1][label]$$"/> -+ </actionGroup> -+ -+ <actionGroup ref="StorefrontAddProductWithSelectedConfigurableOptionToCartActionGroup" stepKey="addProductWithSelectedThirdOptionToCart"> -+ <argument name="product" value="$$createConfigProduct$$"/> -+ <argument name="option" value="$$createConfigProductAttributeThirdOption.option[store_labels][1][label]$$"/> -+ </actionGroup> -+ -+ <!--Assert product price in the cart --> -+ <amOnPage url="/checkout/cart/" stepKey="amOnShoppingCartPage"/> -+ <waitForPageLoad stepKey="waitForShoppingCartPageLoad"/> -+ <see userInput="{{CatalogRuleToFixed.discount_amount}}" selector="{{CheckoutCartProductSection.ProductPriceByOption($$createConfigProductAttributeFirstOption.option[store_labels][1][label]$$)}}" stepKey="assertProductPriceForFirstProductOption"/> -+ <see userInput="$110.70" selector="{{CheckoutCartProductSection.ProductPriceByOption($$createConfigProductAttributeSecondOption.option[store_labels][1][label]$$)}}" stepKey="assertProductPriceForSecondProductOption"/> -+ <see userInput="{{ApiConfigurableProduct.price}}" selector="{{CheckoutCartProductSection.ProductPriceByOption($$createConfigProductAttributeThirdOption.option[store_labels][1][label]$$)}}" stepKey="assertProductPriceForThirdProductOption"/> -+ </test> -+</tests> -diff --git a/app/code/Magento/CatalogSearch/Block/Advanced/Form.php b/app/code/Magento/CatalogSearch/Block/Advanced/Form.php -index 4d1957991d1..681b7ecfb02 100644 ---- a/app/code/Magento/CatalogSearch/Block/Advanced/Form.php -+++ b/app/code/Magento/CatalogSearch/Block/Advanced/Form.php -@@ -4,11 +4,6 @@ - * See COPYING.txt for license details. - */ - --/** -- * Advanced search form -- * -- * @author Magento Core Team <core@magentocommerce.com> -- */ - namespace Magento\CatalogSearch\Block\Advanced; - - use Magento\CatalogSearch\Model\Advanced; -@@ -21,6 +16,8 @@ use Magento\Framework\View\Element\Template; - use Magento\Framework\View\Element\Template\Context; - - /** -+ * Advanced search form -+ * - * @api - * @since 100.0.2 - */ -@@ -58,7 +55,7 @@ class Form extends Template - } - - /** -- * @return AbstractBlock -+ * @inheritdoc - */ - public function _prepareLayout() - { -@@ -286,6 +283,8 @@ class Form extends Template - } - - /** -+ * Get select block. -+ * - * @return BlockInterface - */ - protected function _getSelectBlock() -@@ -299,6 +298,8 @@ class Form extends Template - } - - /** -+ * Get date block. -+ * - * @return BlockInterface|mixed - */ - protected function _getDateBlock() -diff --git a/app/code/Magento/CatalogSearch/Block/Advanced/Result.php b/app/code/Magento/CatalogSearch/Block/Advanced/Result.php -index 7c8e65b2491..9f25990594a 100644 ---- a/app/code/Magento/CatalogSearch/Block/Advanced/Result.php -+++ b/app/code/Magento/CatalogSearch/Block/Advanced/Result.php -@@ -63,7 +63,7 @@ class Result extends Template - } - - /** -- * @return AbstractBlock -+ * @inheritdoc - */ - protected function _prepareLayout() - { -@@ -125,6 +125,8 @@ class Result extends Template - } - - /** -+ * Initialize list collection. -+ * - * @return void - */ - public function setListCollection() -@@ -133,6 +135,8 @@ class Result extends Template - } - - /** -+ * Get product collection. -+ * - * @return Collection - */ - protected function _getProductCollection() -@@ -141,6 +145,8 @@ class Result extends Template - } - - /** -+ * Set search model. -+ * - * @return Advanced - */ - public function getSearchModel() -@@ -149,6 +155,8 @@ class Result extends Template - } - - /** -+ * Get result count. -+ * - * @return mixed - */ - public function getResultCount() -@@ -161,6 +169,8 @@ class Result extends Template - } - - /** -+ * Get product list HTML. -+ * - * @return string - */ - public function getProductListHtml() -@@ -169,6 +179,8 @@ class Result extends Template - } - - /** -+ * Get form URL. -+ * - * @return string - */ - public function getFormUrl() -@@ -182,6 +194,8 @@ class Result extends Template - } - - /** -+ * Get search criteria. -+ * - * @return array - */ - public function getSearchCriterias() -diff --git a/app/code/Magento/CatalogSearch/Block/Plugin/FrontTabPlugin.php b/app/code/Magento/CatalogSearch/Block/Plugin/FrontTabPlugin.php -index 94cb9c3c8fc..85ad66013cf 100644 ---- a/app/code/Magento/CatalogSearch/Block/Plugin/FrontTabPlugin.php -+++ b/app/code/Magento/CatalogSearch/Block/Plugin/FrontTabPlugin.php -@@ -11,7 +11,7 @@ use Magento\Framework\Data\Form; - use Magento\Framework\Data\Form\Element\Fieldset; - - /** -- * Plugin for Magento\Catalog\Block\Adminhtml\Product\Attribute\Edit\Tab\Front -+ * Add Search Weight field to the product attribute add/edit tab - */ - class FrontTabPlugin - { -diff --git a/app/code/Magento/CatalogSearch/Block/SearchTermsLog.php b/app/code/Magento/CatalogSearch/Block/SearchTermsLog.php -index 0be43ce6ff1..005c7860cfe 100644 ---- a/app/code/Magento/CatalogSearch/Block/SearchTermsLog.php -+++ b/app/code/Magento/CatalogSearch/Block/SearchTermsLog.php -@@ -9,7 +9,7 @@ use Magento\Framework\App\ResponseInterface; - use Magento\Framework\View\Element\Block\ArgumentInterface; - - /** -- * Class for logging search terms on cached pages -+ * Provider of the information on whether the page is cacheable, so that AJAX-based logging of terms can be triggered - */ - class SearchTermsLog implements ArgumentInterface - { -diff --git a/app/code/Magento/CatalogSearch/Controller/Advanced/Index.php b/app/code/Magento/CatalogSearch/Controller/Advanced/Index.php -index b20e36016d2..4942b36743c 100644 ---- a/app/code/Magento/CatalogSearch/Controller/Advanced/Index.php -+++ b/app/code/Magento/CatalogSearch/Controller/Advanced/Index.php -@@ -6,12 +6,17 @@ - */ - namespace Magento\CatalogSearch\Controller\Advanced; - -+use Magento\Framework\App\Action\HttpGetActionInterface; -+use Magento\Framework\App\Action\HttpPostActionInterface; - use Magento\Framework\Controller\ResultFactory; - --class Index extends \Magento\Framework\App\Action\Action -+/** -+ * Advanced search controller. -+ */ -+class Index extends \Magento\Framework\App\Action\Action implements HttpGetActionInterface, HttpPostActionInterface - { - /** -- * @return \Magento\Framework\Controller\ResultInterface -+ * @inheritdoc - */ - public function execute() - { -diff --git a/app/code/Magento/CatalogSearch/Controller/Advanced/Result.php b/app/code/Magento/CatalogSearch/Controller/Advanced/Result.php -index d6a30f4f314..384132415a1 100644 ---- a/app/code/Magento/CatalogSearch/Controller/Advanced/Result.php -+++ b/app/code/Magento/CatalogSearch/Controller/Advanced/Result.php -@@ -6,12 +6,22 @@ - */ - namespace Magento\CatalogSearch\Controller\Advanced; - -+use Magento\Framework\App\Action\HttpGetActionInterface; - use Magento\CatalogSearch\Model\Advanced as ModelAdvanced; - use Magento\Framework\App\Action\Context; -+use Magento\Framework\App\Action\HttpPostActionInterface; - use Magento\Framework\UrlFactory; - --class Result extends \Magento\Framework\App\Action\Action -+/** -+ * Advanced search result. -+ */ -+class Result extends \Magento\Framework\App\Action\Action implements HttpGetActionInterface, HttpPostActionInterface - { -+ /** -+ * No results default handle. -+ */ -+ const DEFAULT_NO_RESULT_HANDLE = 'catalogsearch_advanced_result_noresults'; -+ - /** - * Url factory - * -@@ -44,13 +54,16 @@ class Result extends \Magento\Framework\App\Action\Action - } - - /** -- * @return \Magento\Framework\Controller\Result\Redirect -+ * @inheritdoc - */ - public function execute() - { - try { - $this->_catalogSearchAdvanced->addFilters($this->getRequest()->getQueryValue()); -- $this->_view->loadLayout(); -+ $this->_view->getPage()->initLayout(); -+ $handles = $this->_view->getLayout()->getUpdate()->getHandles(); -+ $handles[] = static::DEFAULT_NO_RESULT_HANDLE; -+ $this->_view->loadLayout($handles); - $this->_view->renderLayout(); - } catch (\Magento\Framework\Exception\LocalizedException $e) { - $this->messageManager->addError($e->getMessage()); -diff --git a/app/code/Magento/CatalogSearch/Controller/Result/Index.php b/app/code/Magento/CatalogSearch/Controller/Result/Index.php -index 22958b64d44..975c6ba1e7e 100644 ---- a/app/code/Magento/CatalogSearch/Controller/Result/Index.php -+++ b/app/code/Magento/CatalogSearch/Controller/Result/Index.php -@@ -6,15 +6,25 @@ - */ - namespace Magento\CatalogSearch\Controller\Result; - -+use Magento\Framework\App\Action\HttpGetActionInterface; - use Magento\Catalog\Model\Layer\Resolver; - use Magento\Catalog\Model\Session; - use Magento\Framework\App\Action\Context; -+use Magento\Framework\App\Action\HttpPostActionInterface; - use Magento\Store\Model\StoreManagerInterface; - use Magento\Search\Model\QueryFactory; - use Magento\Search\Model\PopularSearchTerms; - --class Index extends \Magento\Framework\App\Action\Action -+/** -+ * Search result. -+ */ -+class Index extends \Magento\Framework\App\Action\Action implements HttpGetActionInterface, HttpPostActionInterface - { -+ /** -+ * No results default handle. -+ */ -+ const DEFAULT_NO_RESULT_HANDLE = 'catalogsearch_result_index_noresults'; -+ - /** - * Catalog session - * -@@ -85,12 +95,19 @@ class Index extends \Magento\Framework\App\Action\Action - $getAdditionalRequestParameters = $this->getRequest()->getParams(); - unset($getAdditionalRequestParameters[QueryFactory::QUERY_VAR_NAME]); - -+ $handles = null; -+ if ($query->getNumResults() == 0) { -+ $this->_view->getPage()->initLayout(); -+ $handles = $this->_view->getLayout()->getUpdate()->getHandles(); -+ $handles[] = static::DEFAULT_NO_RESULT_HANDLE; -+ } -+ - if (empty($getAdditionalRequestParameters) && - $this->_objectManager->get(PopularSearchTerms::class)->isCacheable($queryText, $storeId) - ) { -- $this->getCacheableResult($catalogSearchHelper, $query); -+ $this->getCacheableResult($catalogSearchHelper, $query, $handles); - } else { -- $this->getNotCacheableResult($catalogSearchHelper, $query); -+ $this->getNotCacheableResult($catalogSearchHelper, $query, $handles); - } - } else { - $this->getResponse()->setRedirect($this->_redirect->getRedirectUrl()); -@@ -102,9 +119,10 @@ class Index extends \Magento\Framework\App\Action\Action - * - * @param \Magento\CatalogSearch\Helper\Data $catalogSearchHelper - * @param \Magento\Search\Model\Query $query -+ * @param array $handles - * @return void - */ -- private function getCacheableResult($catalogSearchHelper, $query) -+ private function getCacheableResult($catalogSearchHelper, $query, $handles) - { - if (!$catalogSearchHelper->isMinQueryLength()) { - $redirect = $query->getRedirect(); -@@ -116,7 +134,7 @@ class Index extends \Magento\Framework\App\Action\Action - - $catalogSearchHelper->checkNotes(); - -- $this->_view->loadLayout(); -+ $this->_view->loadLayout($handles); - $this->_view->renderLayout(); - } - -@@ -125,11 +143,12 @@ class Index extends \Magento\Framework\App\Action\Action - * - * @param \Magento\CatalogSearch\Helper\Data $catalogSearchHelper - * @param \Magento\Search\Model\Query $query -+ * @param array $handles - * @return void - * - * @throws \Magento\Framework\Exception\LocalizedException - */ -- private function getNotCacheableResult($catalogSearchHelper, $query) -+ private function getNotCacheableResult($catalogSearchHelper, $query, $handles) - { - if ($catalogSearchHelper->isMinQueryLength()) { - $query->setId(0)->setIsActive(1)->setIsProcessed(1); -@@ -144,7 +163,7 @@ class Index extends \Magento\Framework\App\Action\Action - - $catalogSearchHelper->checkNotes(); - -- $this->_view->loadLayout(); -+ $this->_view->loadLayout($handles); - $this->getResponse()->setNoCacheHeaders(); - $this->_view->renderLayout(); - } -diff --git a/app/code/Magento/CatalogSearch/Model/Adapter/Aggregation/AggregationResolver.php b/app/code/Magento/CatalogSearch/Model/Adapter/Aggregation/AggregationResolver.php -index 8ca0c0eeddf..8a484d4cc79 100644 ---- a/app/code/Magento/CatalogSearch/Model/Adapter/Aggregation/AggregationResolver.php -+++ b/app/code/Magento/CatalogSearch/Model/Adapter/Aggregation/AggregationResolver.php -@@ -14,6 +14,9 @@ use Magento\Framework\Search\Request\Config; - use Magento\Framework\Search\RequestInterface; - use Magento\Catalog\Model\ResourceModel\Product\Attribute\Collection as AttributeCollection; - -+/** -+ * Aggregation resolver. -+ */ - class AggregationResolver implements AggregationResolverInterface - { - /** -@@ -75,7 +78,7 @@ class AggregationResolver implements AggregationResolverInterface - } - - /** -- * {@inheritdoc} -+ * @inheritdoc - */ - public function resolve(RequestInterface $request, array $documentIds) - { -diff --git a/app/code/Magento/CatalogSearch/Model/Adapter/Aggregation/Checker/Query/AdvancedSearch.php b/app/code/Magento/CatalogSearch/Model/Adapter/Aggregation/Checker/Query/AdvancedSearch.php -index 8f1f3fde142..cea42347dc3 100644 ---- a/app/code/Magento/CatalogSearch/Model/Adapter/Aggregation/Checker/Query/AdvancedSearch.php -+++ b/app/code/Magento/CatalogSearch/Model/Adapter/Aggregation/Checker/Query/AdvancedSearch.php -@@ -29,7 +29,7 @@ class AdvancedSearch implements RequestCheckerInterface - } - - /** -- * {@inheritdoc} -+ * @inheritdoc - */ - public function isApplicable(RequestInterface $request) - { -diff --git a/app/code/Magento/CatalogSearch/Model/Adapter/Aggregation/Checker/Query/CatalogView.php b/app/code/Magento/CatalogSearch/Model/Adapter/Aggregation/Checker/Query/CatalogView.php -index 01e95c4676a..42da02f8ca4 100644 ---- a/app/code/Magento/CatalogSearch/Model/Adapter/Aggregation/Checker/Query/CatalogView.php -+++ b/app/code/Magento/CatalogSearch/Model/Adapter/Aggregation/Checker/Query/CatalogView.php -@@ -51,7 +51,7 @@ class CatalogView implements RequestCheckerInterface - } - - /** -- * {@inheritdoc} -+ * @inheritdoc - */ - public function isApplicable(RequestInterface $request) - { -diff --git a/app/code/Magento/CatalogSearch/Model/Adapter/Aggregation/RequestCheckerComposite.php b/app/code/Magento/CatalogSearch/Model/Adapter/Aggregation/RequestCheckerComposite.php -index 9e4f93b4598..75910e17207 100644 ---- a/app/code/Magento/CatalogSearch/Model/Adapter/Aggregation/RequestCheckerComposite.php -+++ b/app/code/Magento/CatalogSearch/Model/Adapter/Aggregation/RequestCheckerComposite.php -@@ -9,6 +9,9 @@ use Magento\Framework\Search\RequestInterface; - use Magento\Catalog\Api\CategoryRepositoryInterface; - use Magento\Store\Model\StoreManagerInterface; - -+/** -+ * Composite request checker. -+ */ - class RequestCheckerComposite implements RequestCheckerInterface - { - /** -@@ -53,7 +56,7 @@ class RequestCheckerComposite implements RequestCheckerInterface - } - - /** -- * {@inheritdoc} -+ * @inheritdoc - */ - public function isApplicable(RequestInterface $request) - { -diff --git a/app/code/Magento/CatalogSearch/Model/Adapter/Mysql/Aggregation/DataProvider.php b/app/code/Magento/CatalogSearch/Model/Adapter/Mysql/Aggregation/DataProvider.php -index 64a776e7354..66f5ad7a719 100644 ---- a/app/code/Magento/CatalogSearch/Model/Adapter/Mysql/Aggregation/DataProvider.php -+++ b/app/code/Magento/CatalogSearch/Model/Adapter/Mysql/Aggregation/DataProvider.php -@@ -16,7 +16,14 @@ use Magento\Framework\DB\Ddl\Table; - use Magento\Framework\DB\Select; - use Magento\Framework\Search\Adapter\Mysql\Aggregation\DataProviderInterface; - use Magento\Framework\Search\Request\BucketInterface; -+use Magento\Framework\Event\Manager; - -+/** -+ * Data Provider for catalog search. -+ * -+ * @deprecated -+ * @see \Magento\ElasticSearch -+ */ - class DataProvider implements DataProviderInterface - { - /** -@@ -39,12 +46,18 @@ class DataProvider implements DataProviderInterface - */ - private $selectBuilderForAttribute; - -+ /** -+ * @var Manager -+ */ -+ private $eventManager; -+ - /** - * @param Config $eavConfig - * @param ResourceConnection $resource - * @param ScopeResolverInterface $scopeResolver - * @param null $customerSession @deprecated - * @param SelectBuilderForAttribute|null $selectBuilderForAttribute -+ * @param Manager|null $eventManager - * - * @SuppressWarnings(PHPMD.UnusedFormalParameter) - */ -@@ -53,17 +66,19 @@ class DataProvider implements DataProviderInterface - ResourceConnection $resource, - ScopeResolverInterface $scopeResolver, - $customerSession, -- SelectBuilderForAttribute $selectBuilderForAttribute = null -+ SelectBuilderForAttribute $selectBuilderForAttribute = null, -+ Manager $eventManager = null - ) { - $this->eavConfig = $eavConfig; - $this->connection = $resource->getConnection(); - $this->scopeResolver = $scopeResolver; - $this->selectBuilderForAttribute = $selectBuilderForAttribute - ?: ObjectManager::getInstance()->get(SelectBuilderForAttribute::class); -+ $this->eventManager = $eventManager ?: ObjectManager::getInstance()->get(Manager::class); - } - - /** -- * {@inheritdoc} -+ * @inheritdoc - */ - public function getDataSet( - BucketInterface $bucket, -@@ -79,13 +94,17 @@ class DataProvider implements DataProviderInterface - 'main_table.entity_id = entities.entity_id', - [] - ); -+ $this->eventManager->dispatch( -+ 'catalogsearch_query_add_filter_after', -+ ['bucket' => $bucket, 'select' => $select] -+ ); - $select = $this->selectBuilderForAttribute->build($select, $attribute, $currentScope); - - return $select; - } - - /** -- * {@inheritdoc} -+ * @inheritdoc - */ - public function execute(Select $select) - { -@@ -93,6 +112,8 @@ class DataProvider implements DataProviderInterface - } - - /** -+ * Get select. -+ * - * @return Select - */ - private function getSelect() -diff --git a/app/code/Magento/CatalogSearch/Model/Adapter/Mysql/Aggregation/DataProvider/QueryBuilder.php b/app/code/Magento/CatalogSearch/Model/Adapter/Mysql/Aggregation/DataProvider/QueryBuilder.php -index 7ebf71f4244..26837448f2d 100644 ---- a/app/code/Magento/CatalogSearch/Model/Adapter/Mysql/Aggregation/DataProvider/QueryBuilder.php -+++ b/app/code/Magento/CatalogSearch/Model/Adapter/Mysql/Aggregation/DataProvider/QueryBuilder.php -@@ -22,6 +22,9 @@ use Magento\Store\Model\Indexer\WebsiteDimensionProvider; - /** - * Attribute query builder - * -+ * @deprecated -+ * @see \Magento\ElasticSearch -+ * - * @SuppressWarnings(PHPMD.CouplingBetweenObjects) - */ - class QueryBuilder -diff --git a/app/code/Magento/CatalogSearch/Model/Adapter/Mysql/Aggregation/DataProvider/SelectBuilderForAttribute.php b/app/code/Magento/CatalogSearch/Model/Adapter/Mysql/Aggregation/DataProvider/SelectBuilderForAttribute.php -index be0a9833913..00012a78d10 100644 ---- a/app/code/Magento/CatalogSearch/Model/Adapter/Mysql/Aggregation/DataProvider/SelectBuilderForAttribute.php -+++ b/app/code/Magento/CatalogSearch/Model/Adapter/Mysql/Aggregation/DataProvider/SelectBuilderForAttribute.php -@@ -21,6 +21,9 @@ use Magento\Store\Model\Store; - - /** - * Build select for attribute. -+ * -+ * @deprecated -+ * @see \Magento\ElasticSearch - */ - class SelectBuilderForAttribute - { -@@ -71,6 +74,8 @@ class SelectBuilderForAttribute - } - - /** -+ * Build select for attribute search -+ * - * @param Select $select - * @param AbstractAttribute $attribute - * @param int $currentScope -@@ -98,7 +103,7 @@ class SelectBuilderForAttribute - $subSelect = $select; - $subSelect->from(['main_table' => $table], ['main_table.entity_id', 'main_table.value']) - ->distinct() -- ->where('main_table.attribute_id = ?', $attribute->getAttributeId()) -+ ->where('main_table.attribute_id = ?', (int) $attribute->getAttributeId()) - ->where('main_table.store_id = ? ', $currentScopeId); - if ($this->isAddStockFilter()) { - $subSelect = $this->applyStockConditionToSelect->execute($subSelect); -@@ -113,6 +118,8 @@ class SelectBuilderForAttribute - } - - /** -+ * Is add stock filter -+ * - * @return bool - */ - private function isAddStockFilter() -diff --git a/app/code/Magento/CatalogSearch/Model/Adapter/Mysql/Aggregation/DataProvider/SelectBuilderForAttribute/ApplyStockConditionToSelect.php b/app/code/Magento/CatalogSearch/Model/Adapter/Mysql/Aggregation/DataProvider/SelectBuilderForAttribute/ApplyStockConditionToSelect.php -index 83f9c6f9c30..be572793f1e 100644 ---- a/app/code/Magento/CatalogSearch/Model/Adapter/Mysql/Aggregation/DataProvider/SelectBuilderForAttribute/ApplyStockConditionToSelect.php -+++ b/app/code/Magento/CatalogSearch/Model/Adapter/Mysql/Aggregation/DataProvider/SelectBuilderForAttribute/ApplyStockConditionToSelect.php -@@ -13,6 +13,9 @@ use Magento\Framework\DB\Select; - - /** - * Join stock table with stock condition to select. -+ * -+ * @deprecated -+ * @see \Magento\ElasticSearch - */ - class ApplyStockConditionToSelect - { -diff --git a/app/code/Magento/CatalogSearch/Model/Adapter/Mysql/BaseSelectStrategy/BaseSelectAttributesSearchStrategy.php b/app/code/Magento/CatalogSearch/Model/Adapter/Mysql/BaseSelectStrategy/BaseSelectAttributesSearchStrategy.php -index 87fc896f509..27a784f8609 100644 ---- a/app/code/Magento/CatalogSearch/Model/Adapter/Mysql/BaseSelectStrategy/BaseSelectAttributesSearchStrategy.php -+++ b/app/code/Magento/CatalogSearch/Model/Adapter/Mysql/BaseSelectStrategy/BaseSelectAttributesSearchStrategy.php -@@ -18,6 +18,9 @@ use Magento\CatalogSearch\Model\Search\SelectContainer\SelectContainer; - * - * The main idea of this strategy is using eav index table as main table for query - * in case when search request requires search by attributes -+ * -+ * @deprecated -+ * @see \Magento\ElasticSearch - */ - class BaseSelectAttributesSearchStrategy implements BaseSelectStrategyInterface - { -diff --git a/app/code/Magento/CatalogSearch/Model/Adapter/Mysql/BaseSelectStrategy/BaseSelectFullTextSearchStrategy.php b/app/code/Magento/CatalogSearch/Model/Adapter/Mysql/BaseSelectStrategy/BaseSelectFullTextSearchStrategy.php -index 0732df7bd9f..bff878122c8 100644 ---- a/app/code/Magento/CatalogSearch/Model/Adapter/Mysql/BaseSelectStrategy/BaseSelectFullTextSearchStrategy.php -+++ b/app/code/Magento/CatalogSearch/Model/Adapter/Mysql/BaseSelectStrategy/BaseSelectFullTextSearchStrategy.php -@@ -17,6 +17,9 @@ use Magento\Framework\App\ResourceConnection; - * - * The main idea of this strategy is using fulltext search index table as main table for query - * in case when search request does not requires any search by attributes -+ * -+ * @deprecated -+ * @see \Magento\ElasticSearch - */ - class BaseSelectFullTextSearchStrategy implements BaseSelectStrategyInterface - { -diff --git a/app/code/Magento/CatalogSearch/Model/Adapter/Mysql/Dynamic/DataProvider.php b/app/code/Magento/CatalogSearch/Model/Adapter/Mysql/Dynamic/DataProvider.php -index d2e653ac9b9..eb4761adf83 100644 ---- a/app/code/Magento/CatalogSearch/Model/Adapter/Mysql/Dynamic/DataProvider.php -+++ b/app/code/Magento/CatalogSearch/Model/Adapter/Mysql/Dynamic/DataProvider.php -@@ -24,6 +24,8 @@ use \Magento\Framework\Search\Request\IndexScopeResolverInterface; - - /** - * @SuppressWarnings(PHPMD.CouplingBetweenObjects) -+ * @deprecated -+ * @see \Magento\ElasticSearch - */ - class DataProvider implements DataProviderInterface - { -diff --git a/app/code/Magento/CatalogSearch/Model/Adapter/Mysql/Field/Resolver.php b/app/code/Magento/CatalogSearch/Model/Adapter/Mysql/Field/Resolver.php -index a187ccc8f96..c24acf4610e 100644 ---- a/app/code/Magento/CatalogSearch/Model/Adapter/Mysql/Field/Resolver.php -+++ b/app/code/Magento/CatalogSearch/Model/Adapter/Mysql/Field/Resolver.php -@@ -10,6 +10,10 @@ use Magento\Framework\Search\Adapter\Mysql\Field\FieldFactory; - use Magento\Framework\Search\Adapter\Mysql\Field\FieldInterface; - use Magento\Framework\Search\Adapter\Mysql\Field\ResolverInterface; - -+/** -+ * @deprecated -+ * @see \Magento\ElasticSearch -+ */ - class Resolver implements ResolverInterface - { - /** -diff --git a/app/code/Magento/CatalogSearch/Model/Adapter/Mysql/Filter/AliasResolver.php b/app/code/Magento/CatalogSearch/Model/Adapter/Mysql/Filter/AliasResolver.php -index 547122125b1..bf431396cc0 100644 ---- a/app/code/Magento/CatalogSearch/Model/Adapter/Mysql/Filter/AliasResolver.php -+++ b/app/code/Magento/CatalogSearch/Model/Adapter/Mysql/Filter/AliasResolver.php -@@ -12,6 +12,8 @@ use Magento\CatalogSearch\Model\Search\RequestGenerator; - * Purpose of class is to resolve table alias for Search Request filter - * @api - * @since 100.1.6 -+ * @deprecated -+ * @see \Magento\ElasticSearch - */ - class AliasResolver - { -diff --git a/app/code/Magento/CatalogSearch/Model/Adapter/Mysql/Filter/Preprocessor.php b/app/code/Magento/CatalogSearch/Model/Adapter/Mysql/Filter/Preprocessor.php -index 35addc3fafd..c758e773f43 100644 ---- a/app/code/Magento/CatalogSearch/Model/Adapter/Mysql/Filter/Preprocessor.php -+++ b/app/code/Magento/CatalogSearch/Model/Adapter/Mysql/Filter/Preprocessor.php -@@ -8,7 +8,9 @@ namespace Magento\CatalogSearch\Model\Adapter\Mysql\Filter; - use Magento\Catalog\Api\Data\ProductInterface; - use Magento\Catalog\Model\Product; - use Magento\Catalog\Model\ResourceModel\Eav\Attribute; -+use Magento\CatalogSearch\Model\Search\FilterMapper\VisibilityFilter; - use Magento\CatalogSearch\Model\Search\TableMapper; -+use Magento\Customer\Model\Session; - use Magento\Eav\Model\Config; - use Magento\Framework\App\Config\ScopeConfigInterface; - use Magento\Framework\App\ObjectManager; -@@ -20,11 +22,14 @@ use Magento\Framework\Search\Adapter\Mysql\ConditionManager; - use Magento\Framework\Search\Adapter\Mysql\Filter\PreprocessorInterface; - use Magento\Framework\Search\Request\FilterInterface; - use Magento\Store\Model\Store; --use Magento\Customer\Model\Session; --use Magento\CatalogSearch\Model\Search\FilterMapper\VisibilityFilter; - - /** -+ * ElasticSearch search filter pre-processor. -+ * -+ * @SuppressWarnings(PHPMD.CookieAndSessionMisuse) - * @SuppressWarnings(PHPMD.CouplingBetweenObjects) -+ * @deprecated -+ * @see \Magento\ElasticSearch - */ - class Preprocessor implements PreprocessorInterface - { -@@ -126,7 +131,7 @@ class Preprocessor implements PreprocessorInterface - } - - /** -- * {@inheritdoc} -+ * @inheritdoc - */ - public function process(FilterInterface $filter, $isNegation, $query) - { -@@ -134,10 +139,13 @@ class Preprocessor implements PreprocessorInterface - } - - /** -+ * Process query with field. -+ * - * @param FilterInterface $filter - * @param bool $isNegation - * @param string $query - * @return string -+ * @throws \Magento\Framework\Exception\LocalizedException - */ - private function processQueryWithField(FilterInterface $filter, $isNegation, $query) - { -@@ -168,7 +176,7 @@ class Preprocessor implements PreprocessorInterface - } elseif ($filter->getField() === VisibilityFilter::VISIBILITY_FILTER_FIELD) { - return ''; - } elseif ($filter->getType() === FilterInterface::TYPE_TERM && -- in_array($attribute->getFrontendInput(), ['select', 'multiselect'], true) -+ in_array($attribute->getFrontendInput(), ['select', 'multiselect', 'boolean'], true) - ) { - $resultQuery = $this->processTermSelect($filter, $isNegation); - } elseif ($filter->getType() === FilterInterface::TYPE_RANGE && -@@ -202,19 +210,23 @@ class Preprocessor implements PreprocessorInterface - ->where('main_table.store_id = ?', Store::DEFAULT_STORE_ID) - ->having($query); - -- $resultQuery = 'search_index.entity_id IN ( -- select entity_id from ' . $this->conditionManager->wrapBrackets($select) . ' as filter -- )'; -+ $resultQuery = 'search_index.entity_id IN (' -+ . 'select entity_id from ' -+ . $this->conditionManager->wrapBrackets($select) -+ . ' as filter)'; - } - - return $resultQuery; - } - - /** -+ * Process range numeric. -+ * - * @param FilterInterface $filter - * @param string $query - * @param Attribute $attribute - * @return string -+ * @throws \Exception - */ - private function processRangeNumeric(FilterInterface $filter, $query, $attribute) - { -@@ -236,14 +248,17 @@ class Preprocessor implements PreprocessorInterface - ->where('main_table.store_id = ?', $currentStoreId) - ->having($query); - -- $resultQuery = 'search_index.entity_id IN ( -- select entity_id from ' . $this->conditionManager->wrapBrackets($select) . ' as filter -- )'; -+ $resultQuery = 'search_index.entity_id IN (' -+ . 'select entity_id from ' -+ . $this->conditionManager->wrapBrackets($select) -+ . ' as filter)'; - - return $resultQuery; - } - - /** -+ * Process term select. -+ * - * @param FilterInterface $filter - * @param bool $isNegation - * @return string -diff --git a/app/code/Magento/CatalogSearch/Model/Adapter/Mysql/Plugin/Aggregation/Category/DataProvider.php b/app/code/Magento/CatalogSearch/Model/Adapter/Mysql/Plugin/Aggregation/Category/DataProvider.php -index 94432bbfe4a..a5650cac733 100644 ---- a/app/code/Magento/CatalogSearch/Model/Adapter/Mysql/Plugin/Aggregation/Category/DataProvider.php -+++ b/app/code/Magento/CatalogSearch/Model/Adapter/Mysql/Plugin/Aggregation/Category/DataProvider.php -@@ -18,6 +18,8 @@ use Magento\Catalog\Model\Indexer\Category\Product\AbstractAction; - - /** - * @SuppressWarnings(PHPMD.CouplingBetweenObjects) -+ * @deprecated -+ * @see \Magento\ElasticSearch - */ - class DataProvider - { -diff --git a/app/code/Magento/CatalogSearch/Model/Adapter/Options.php b/app/code/Magento/CatalogSearch/Model/Adapter/Options.php -index 425e6c00416..f2e1c3c4c42 100644 ---- a/app/code/Magento/CatalogSearch/Model/Adapter/Options.php -+++ b/app/code/Magento/CatalogSearch/Model/Adapter/Options.php -@@ -10,6 +10,8 @@ use Magento\Framework\Search\Adapter\OptionsInterface; - use Magento\Store\Model\ScopeInterface; - - /** -+ * Catalog search config. -+ * - * @api - * @since 100.0.2 - */ -@@ -33,7 +35,7 @@ class Options implements OptionsInterface - } - - /** -- * {@inheritdoc} -+ * @inheritdoc - */ - public function get() - { -diff --git a/app/code/Magento/CatalogSearch/Model/Adminhtml/System/Config/Backend/Engine.php b/app/code/Magento/CatalogSearch/Model/Adminhtml/System/Config/Backend/Engine.php -index 2964f836ab9..1f11d265033 100644 ---- a/app/code/Magento/CatalogSearch/Model/Adminhtml/System/Config/Backend/Engine.php -+++ b/app/code/Magento/CatalogSearch/Model/Adminhtml/System/Config/Backend/Engine.php -@@ -6,7 +6,8 @@ - namespace Magento\CatalogSearch\Model\Adminhtml\System\Config\Backend; - - /** -- * @author Magento Core Team <core@magentocommerce.com> -+ * Backend model for catalog search engine system config -+ * - * @api - * @since 100.0.2 - */ -@@ -43,6 +44,7 @@ class Engine extends \Magento\Framework\App\Config\Value - - /** - * After save call -+ * - * Invalidate catalog search index if engine was changed - * - * @return $this -diff --git a/app/code/Magento/CatalogSearch/Model/Advanced.php b/app/code/Magento/CatalogSearch/Model/Advanced.php -index 28f67a7829e..5b96a8c21cb 100644 ---- a/app/code/Magento/CatalogSearch/Model/Advanced.php -+++ b/app/code/Magento/CatalogSearch/Model/Advanced.php -@@ -6,6 +6,8 @@ - namespace Magento\CatalogSearch\Model; - - use Magento\Catalog\Model\Config; -+use Magento\CatalogSearch\Model\Advanced\ProductCollectionPrepareStrategyProvider; -+use Magento\CatalogSearch\Model\Search\ItemCollectionProviderInterface; - use Magento\Catalog\Model\Product\Visibility; - use Magento\Catalog\Model\ProductFactory; - use Magento\Catalog\Model\ResourceModel\Eav\Attribute; -@@ -19,9 +21,11 @@ use Magento\Framework\Model\Context; - use Magento\Framework\Exception\LocalizedException; - use Magento\Framework\Registry; - use Magento\Store\Model\StoreManagerInterface; -+use Magento\Framework\App\ObjectManager; - - /** - * Catalog advanced search model -+ * - * @method int getEntityTypeId() - * @method \Magento\CatalogSearch\Model\Advanced setEntityTypeId(int $value) - * @method int getAttributeSetId() -@@ -63,6 +67,7 @@ class Advanced extends \Magento\Framework\Model\AbstractModel - /** - * Initialize dependencies - * -+ * @deprecated - * @var Config - */ - protected $_catalogConfig; -@@ -105,10 +110,22 @@ class Advanced extends \Magento\Framework\Model\AbstractModel - /** - * Advanced Collection Factory - * -+ * @deprecated -+ * @see $collectionProvider - * @var ProductCollectionFactory - */ - protected $productCollectionFactory; - -+ /** -+ * @var ItemCollectionProviderInterface -+ */ -+ private $collectionProvider; -+ -+ /** -+ * @var ProductCollectionPrepareStrategyProvider|null -+ */ -+ private $productCollectionPrepareStrategyProvider; -+ - /** - * Construct - * -@@ -123,7 +140,8 @@ class Advanced extends \Magento\Framework\Model\AbstractModel - * @param ProductCollectionFactory $productCollectionFactory - * @param AdvancedFactory $advancedFactory - * @param array $data -- * -+ * @param ItemCollectionProviderInterface|null $collectionProvider -+ * @param ProductCollectionPrepareStrategyProvider|null $productCollectionPrepareStrategyProvider - * @SuppressWarnings(PHPMD.ExcessiveParameterList) - */ - public function __construct( -@@ -137,7 +155,9 @@ class Advanced extends \Magento\Framework\Model\AbstractModel - StoreManagerInterface $storeManager, - ProductCollectionFactory $productCollectionFactory, - AdvancedFactory $advancedFactory, -- array $data = [] -+ array $data = [], -+ ItemCollectionProviderInterface $collectionProvider = null, -+ ProductCollectionPrepareStrategyProvider $productCollectionPrepareStrategyProvider = null - ) { - $this->_attributeCollectionFactory = $attributeCollectionFactory; - $this->_catalogProductVisibility = $catalogProductVisibility; -@@ -146,11 +166,14 @@ class Advanced extends \Magento\Framework\Model\AbstractModel - $this->_productFactory = $productFactory; - $this->_storeManager = $storeManager; - $this->productCollectionFactory = $productCollectionFactory; -+ $this->collectionProvider = $collectionProvider; -+ $this->productCollectionPrepareStrategyProvider = $productCollectionPrepareStrategyProvider -+ ?: ObjectManager::getInstance()->get(ProductCollectionPrepareStrategyProvider::class); - parent::__construct( - $context, - $registry, - $advancedFactory->create(), -- $this->productCollectionFactory->create(), -+ $this->resolveProductCollection(), - $data - ); - } -@@ -265,7 +288,7 @@ class Advanced extends \Magento\Framework\Model\AbstractModel - public function getProductCollection() - { - if ($this->_productCollection === null) { -- $collection = $this->productCollectionFactory->create(); -+ $collection = $this->resolveProductCollection(); - $this->prepareProductCollection($collection); - if (!$collection) { - return $collection; -@@ -276,6 +299,18 @@ class Advanced extends \Magento\Framework\Model\AbstractModel - return $this->_productCollection; - } - -+ /** -+ * Resolve product collection. -+ * -+ * @return \Magento\Catalog\Model\ResourceModel\Product\Collection|\Magento\Framework\Data\Collection -+ */ -+ private function resolveProductCollection() -+ { -+ return (null === $this->collectionProvider) -+ ? $this->productCollectionFactory->create() -+ : $this->collectionProvider->getCollection(); -+ } -+ - /** - * Prepare product collection - * -@@ -284,18 +319,14 @@ class Advanced extends \Magento\Framework\Model\AbstractModel - */ - public function prepareProductCollection($collection) - { -- $collection -- ->addAttributeToSelect($this->_catalogConfig->getProductAttributes()) -- ->setStore($this->_storeManager->getStore()) -- ->addMinimalPrice() -- ->addTaxPercents() -- ->addStoreFilter() -- ->setVisibility($this->_catalogProductVisibility->getVisibleInSearchIds()); -+ $this->productCollectionPrepareStrategyProvider->getStrategy()->prepare($collection); - - return $this; - } - - /** -+ * Add search criteria. -+ * - * @param EntityAttribute $attribute - * @param mixed $value - * @return void -diff --git a/app/code/Magento/CatalogSearch/Model/Advanced/ProductCollectionPrepareStrategy.php b/app/code/Magento/CatalogSearch/Model/Advanced/ProductCollectionPrepareStrategy.php -new file mode 100644 -index 00000000000..e62de9c689f ---- /dev/null -+++ b/app/code/Magento/CatalogSearch/Model/Advanced/ProductCollectionPrepareStrategy.php -@@ -0,0 +1,61 @@ -+<?php -+/** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+namespace Magento\CatalogSearch\Model\Advanced; -+ -+use Magento\Catalog\Model\ResourceModel\Product\Collection; -+use Magento\Catalog\Model\Config; -+use Magento\Catalog\Model\Product\Visibility; -+use Magento\Store\Model\StoreManagerInterface; -+ -+/** -+ * Strategy interface for preparing product collection. -+ */ -+class ProductCollectionPrepareStrategy implements ProductCollectionPrepareStrategyInterface -+{ -+ /** -+ * @var Config -+ */ -+ private $catalogConfig; -+ -+ /** -+ * @var StoreManagerInterface -+ */ -+ private $storeManager; -+ -+ /** -+ * @var Visibility -+ */ -+ private $catalogProductVisibility; -+ -+ /** -+ * @param Config $catalogConfig -+ * @param StoreManagerInterface $storeManager -+ * @param Visibility $catalogProductVisibility -+ */ -+ public function __construct( -+ Config $catalogConfig, -+ StoreManagerInterface $storeManager, -+ Visibility $catalogProductVisibility -+ ) { -+ $this->catalogConfig = $catalogConfig; -+ $this->storeManager = $storeManager; -+ $this->catalogProductVisibility = $catalogProductVisibility; -+ } -+ -+ /** -+ * @inheritdoc -+ */ -+ public function prepare(Collection $collection) -+ { -+ $collection -+ ->addAttributeToSelect($this->catalogConfig->getProductAttributes()) -+ ->setStore($this->storeManager->getStore()) -+ ->addMinimalPrice() -+ ->addTaxPercents() -+ ->addStoreFilter() -+ ->setVisibility($this->catalogProductVisibility->getVisibleInSearchIds()); -+ } -+} -diff --git a/app/code/Magento/CatalogSearch/Model/Advanced/ProductCollectionPrepareStrategyInterface.php b/app/code/Magento/CatalogSearch/Model/Advanced/ProductCollectionPrepareStrategyInterface.php -new file mode 100644 -index 00000000000..23719a6713a ---- /dev/null -+++ b/app/code/Magento/CatalogSearch/Model/Advanced/ProductCollectionPrepareStrategyInterface.php -@@ -0,0 +1,22 @@ -+<?php -+/** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+namespace Magento\CatalogSearch\Model\Advanced; -+ -+use Magento\Catalog\Model\ResourceModel\Product\Collection; -+ -+/** -+ * Strategy interface for preparing product collection. -+ */ -+interface ProductCollectionPrepareStrategyInterface -+{ -+ /** -+ * Prepare product collection. -+ * -+ * @param Collection $collection -+ * @return void -+ */ -+ public function prepare(Collection $collection); -+} -diff --git a/app/code/Magento/CatalogSearch/Model/Advanced/ProductCollectionPrepareStrategyProvider.php b/app/code/Magento/CatalogSearch/Model/Advanced/ProductCollectionPrepareStrategyProvider.php -new file mode 100644 -index 00000000000..6e963ea1aa8 ---- /dev/null -+++ b/app/code/Magento/CatalogSearch/Model/Advanced/ProductCollectionPrepareStrategyProvider.php -@@ -0,0 +1,49 @@ -+<?php -+/** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+namespace Magento\CatalogSearch\Model\Advanced; -+ -+use Magento\Framework\Search\EngineResolverInterface; -+ -+/** -+ * Strategy provider for preparing product collection. -+ */ -+class ProductCollectionPrepareStrategyProvider -+{ -+ /** -+ * @var EngineResolverInterface -+ */ -+ private $engineResolver; -+ -+ /** -+ * @var array -+ */ -+ private $strategies; -+ -+ /** -+ * @param EngineResolverInterface $engineResolver -+ * @param array $strategies -+ */ -+ public function __construct( -+ EngineResolverInterface $engineResolver, -+ array $strategies -+ ) { -+ $this->engineResolver = $engineResolver; -+ $this->strategies = $strategies; -+ } -+ -+ /** -+ * Get strategy provider for product collection prepare process. -+ * -+ * @return ProductCollectionPrepareStrategyInterface -+ */ -+ public function getStrategy(): ProductCollectionPrepareStrategyInterface -+ { -+ if (!isset($this->strategies[$this->engineResolver->getCurrentSearchEngine()])) { -+ throw new \DomainException('Undefined strategy ' . $this->engineResolver->getCurrentSearchEngine()); -+ } -+ return $this->strategies[$this->engineResolver->getCurrentSearchEngine()]; -+ } -+} -diff --git a/app/code/Magento/CatalogSearch/Model/Advanced/Request/Builder.php b/app/code/Magento/CatalogSearch/Model/Advanced/Request/Builder.php -index 429c29fd3d2..9e7737123b2 100644 ---- a/app/code/Magento/CatalogSearch/Model/Advanced/Request/Builder.php -+++ b/app/code/Magento/CatalogSearch/Model/Advanced/Request/Builder.php -@@ -8,12 +8,16 @@ namespace Magento\CatalogSearch\Model\Advanced\Request; - use Magento\Framework\Search\Request\Builder as RequestBuilder; - - /** -+ * Catalog search advanced request builder. -+ * - * @api - * @since 100.0.2 - */ - class Builder extends RequestBuilder - { - /** -+ * Bind value to query. -+ * - * @param string $attributeCode - * @param array|string $attributeValue - * @return void -diff --git a/app/code/Magento/CatalogSearch/Model/Attribute/SearchWeight.php b/app/code/Magento/CatalogSearch/Model/Attribute/SearchWeight.php -index d6110f4b3b2..fa42cadb8a2 100644 ---- a/app/code/Magento/CatalogSearch/Model/Attribute/SearchWeight.php -+++ b/app/code/Magento/CatalogSearch/Model/Attribute/SearchWeight.php -@@ -7,8 +7,9 @@ - namespace Magento\CatalogSearch\Model\Attribute; - - /** -- * This plugin is responsible for processing of search_weight property of a product attribute, -- * which is used to boost matches by specific attributes. -+ * This plugin is responsible for processing of search_weight property of a product attribute. -+ * -+ * 'search_weight' is used to boost matches by specific attributes. - * - * This is part of search accuracy customization functionality. - */ -diff --git a/app/code/Magento/CatalogSearch/Model/Autocomplete/DataProvider.php b/app/code/Magento/CatalogSearch/Model/Autocomplete/DataProvider.php -index c1c9997bc83..f014c6d1331 100644 ---- a/app/code/Magento/CatalogSearch/Model/Autocomplete/DataProvider.php -+++ b/app/code/Magento/CatalogSearch/Model/Autocomplete/DataProvider.php -@@ -13,6 +13,9 @@ use Magento\Search\Model\Autocomplete\ItemFactory; - use Magento\Framework\App\Config\ScopeConfigInterface as ScopeConfig; - use Magento\Store\Model\ScopeInterface; - -+/** -+ * Catalog search auto-complete data provider. -+ */ - class DataProvider implements DataProviderInterface - { - /** -@@ -44,6 +47,7 @@ class DataProvider implements DataProviderInterface - /** - * @param QueryFactory $queryFactory - * @param ItemFactory $itemFactory -+ * @param ScopeConfig $scopeConfig - */ - public function __construct( - QueryFactory $queryFactory, -@@ -60,7 +64,7 @@ class DataProvider implements DataProviderInterface - } - - /** -- * {@inheritdoc} -+ * @inheritdoc - */ - public function getItems() - { -diff --git a/app/code/Magento/CatalogSearch/Model/Fulltext.php b/app/code/Magento/CatalogSearch/Model/Fulltext.php -index 1792fd21fb8..398d6e9dd18 100644 ---- a/app/code/Magento/CatalogSearch/Model/Fulltext.php -+++ b/app/code/Magento/CatalogSearch/Model/Fulltext.php -@@ -21,6 +21,9 @@ use Magento\Search\Model\QueryFactory; - * @method \Magento\CatalogSearch\Model\Fulltext setStoreId(int $value) - * @method string getDataIndex() - * @method \Magento\CatalogSearch\Model\Fulltext setDataIndex(string $value) -+ * -+ * @deprecated -+ * @see \Magento\ElasticSearch - */ - class Fulltext extends \Magento\Framework\Model\AbstractModel - { -diff --git a/app/code/Magento/CatalogSearch/Model/Indexer/Fulltext.php b/app/code/Magento/CatalogSearch/Model/Indexer/Fulltext.php -index 37208ed896e..21d8b7297da 100644 ---- a/app/code/Magento/CatalogSearch/Model/Indexer/Fulltext.php -+++ b/app/code/Magento/CatalogSearch/Model/Indexer/Fulltext.php -@@ -117,7 +117,8 @@ class Fulltext implements - } - - /** -- * {@inheritdoc} -+ * @inheritdoc -+ * - * @throws \InvalidArgumentException - */ - public function executeByDimensions(array $dimensions, \Traversable $entityIds = null) -@@ -145,8 +146,10 @@ class Fulltext implements - $productIds = array_unique( - array_merge($entityIds, $this->fulltextResource->getRelationsByChild($entityIds)) - ); -- $saveHandler->deleteIndex($dimensions, new \ArrayIterator($productIds)); -- $saveHandler->saveIndex($dimensions, $this->fullAction->rebuildStoreIndex($storeId, $productIds)); -+ if ($saveHandler->isAvailable($dimensions)) { -+ $saveHandler->deleteIndex($dimensions, new \ArrayIterator($productIds)); -+ $saveHandler->saveIndex($dimensions, $this->fullAction->rebuildStoreIndex($storeId, $productIds)); -+ } - } - } - -diff --git a/app/code/Magento/CatalogSearch/Model/Indexer/Fulltext/Action/DataProvider.php b/app/code/Magento/CatalogSearch/Model/Indexer/Fulltext/Action/DataProvider.php -index a8d46911193..cd2529a8fd7 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\Framework\DB\Select; - use Magento\Store\Model\Store; - - /** -+ * Catalog search full test search data provider. -+ * - * @SuppressWarnings(PHPMD.CouplingBetweenObjects) - * @SuppressWarnings(PHPMD.TooManyFields) - * @api -@@ -568,14 +570,13 @@ class DataProvider - } - } - 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; -- } else { -- $index[$attributeId] = [$entityId => $value]; -+ if (!isset($index[$attributeId])) { -+ $index[$attributeId] = []; - } -+ $index[$attributeId][$entityId] = $value; - } - } - } -@@ -600,16 +601,16 @@ class DataProvider - * Retrieve attribute source value for search - * - * @param int $attributeId -- * @param mixed $valueId -+ * @param mixed $valueIds - * @param int $storeId - * @return string - */ -- private function getAttributeValue($attributeId, $valueId, $storeId) -+ private function getAttributeValue($attributeId, $valueIds, $storeId) - { - $attribute = $this->getSearchableAttribute($attributeId); -- $value = $this->engine->processAttributeValue($attribute, $valueId); -+ $value = $this->engine->processAttributeValue($attribute, $valueIds); - if (false !== $value) { -- $optionValue = $this->getAttributeOptionValue($attributeId, $valueId, $storeId); -+ $optionValue = $this->getAttributeOptionValue($attributeId, $valueIds, $storeId); - if (null === $optionValue) { - $value = $this->filterAttributeValue($value); - } else { -@@ -624,13 +625,15 @@ class DataProvider - * 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); -@@ -641,15 +644,22 @@ class DataProvider - $attribute->setStoreId($storeId); - $options = $attribute->getSource()->toOptionArray(); - $this->attributeOptions[$optionKey] = array_column($options, 'label', 'value'); -- $this->attributeOptions[$optionKey] = array_map(function ($value) { -- return $this->filterAttributeValue($value); -- }, $this->attributeOptions[$optionKey]); -+ $this->attributeOptions[$optionKey] = array_map( -+ function ($value) { -+ return $this->filterAttributeValue($value); -+ }, -+ $this->attributeOptions[$optionKey] -+ ); - } else { - $this->attributeOptions[$optionKey] = null; - } - } -- -- return $this->attributeOptions[$optionKey][$valueId] ?? null; -+ foreach ($attributeValueIds as $attrValueId) { -+ if (isset($this->attributeOptions[$optionKey][$attrValueId])) { -+ $attributeOptionValue .= $this->attributeOptions[$optionKey][$attrValueId] . ' '; -+ } -+ } -+ return empty($attributeOptionValue) ? null : trim($attributeOptionValue); - } - - /** -diff --git a/app/code/Magento/CatalogSearch/Model/Indexer/Fulltext/Action/Full.php b/app/code/Magento/CatalogSearch/Model/Indexer/Fulltext/Action/Full.php -index 8a18c1bfcc5..f56a4fe4d1b 100644 ---- a/app/code/Magento/CatalogSearch/Model/Indexer/Fulltext/Action/Full.php -+++ b/app/code/Magento/CatalogSearch/Model/Indexer/Fulltext/Action/Full.php -@@ -397,7 +397,7 @@ class Full - } - $products = $this->dataProvider - ->getSearchableProducts($storeId, $staticFields, $productIds, $lastProductId, $this->batchSize); -- }; -+ } - } - - /** -diff --git a/app/code/Magento/CatalogSearch/Model/Indexer/Fulltext/Action/IndexIterator.php b/app/code/Magento/CatalogSearch/Model/Indexer/Fulltext/Action/IndexIterator.php -index abd65c71cad..a2c39deff18 100644 ---- a/app/code/Magento/CatalogSearch/Model/Indexer/Fulltext/Action/IndexIterator.php -+++ b/app/code/Magento/CatalogSearch/Model/Indexer/Fulltext/Action/IndexIterator.php -@@ -14,6 +14,9 @@ namespace Magento\CatalogSearch\Model\Indexer\Fulltext\Action; - * @see \Magento\CatalogSearch\Model\Indexer\Fulltext\Action\Full - * @api - * @since 100.0.3 -+ * -+ * @deprecated -+ * @see \Magento\ElasticSearch - */ - class IndexIterator implements \Iterator - { -diff --git a/app/code/Magento/CatalogSearch/Model/Indexer/Fulltext/Plugin/Attribute.php b/app/code/Magento/CatalogSearch/Model/Indexer/Fulltext/Plugin/Attribute.php -index 83ad7acca84..86dccf8cfe5 100644 ---- a/app/code/Magento/CatalogSearch/Model/Indexer/Fulltext/Plugin/Attribute.php -+++ b/app/code/Magento/CatalogSearch/Model/Indexer/Fulltext/Plugin/Attribute.php -@@ -7,6 +7,9 @@ namespace Magento\CatalogSearch\Model\Indexer\Fulltext\Plugin; - - use Magento\CatalogSearch\Model\Indexer\Fulltext; - -+/** -+ * Catalog search indexer plugin for catalog attribute. -+ */ - class Attribute extends AbstractPlugin - { - /** -diff --git a/app/code/Magento/CatalogSearch/Model/Indexer/Fulltext/Plugin/Category.php b/app/code/Magento/CatalogSearch/Model/Indexer/Fulltext/Plugin/Category.php -index e9c0e06ac38..1218e3da9a7 100644 ---- a/app/code/Magento/CatalogSearch/Model/Indexer/Fulltext/Plugin/Category.php -+++ b/app/code/Magento/CatalogSearch/Model/Indexer/Fulltext/Plugin/Category.php -@@ -10,8 +10,7 @@ use Magento\Catalog\Model\ResourceModel\Category as ResourceCategory; - use Magento\Framework\Model\AbstractModel; - - /** -- * Class Category -- * @package Magento\CatalogSearch\Model\Indexer\Fulltext\Plugin -+ * Catalog search indexer plugin for catalog category. - */ - class Category extends AbstractPlugin - { -@@ -30,6 +29,8 @@ class Category extends AbstractPlugin - } - - /** -+ * Reindex catalog search. -+ * - * @param ResourceCategory $resourceCategory - * @param \Closure $proceed - * @param AbstractModel $category -diff --git a/app/code/Magento/CatalogSearch/Model/Indexer/Fulltext/Plugin/Product.php b/app/code/Magento/CatalogSearch/Model/Indexer/Fulltext/Plugin/Product.php -index 949033bb338..e250d112393 100644 ---- a/app/code/Magento/CatalogSearch/Model/Indexer/Fulltext/Plugin/Product.php -+++ b/app/code/Magento/CatalogSearch/Model/Indexer/Fulltext/Plugin/Product.php -@@ -9,15 +9,19 @@ namespace Magento\CatalogSearch\Model\Indexer\Fulltext\Plugin; - use Magento\Catalog\Model\ResourceModel\Product as ResourceProduct; - use Magento\Framework\Model\AbstractModel; - -+/** -+ * Catalog search indexer plugin for catalog product. -+ */ - class Product extends AbstractPlugin - { - /** -- * Reindex on product save -+ * Reindex on product save. - * - * @param ResourceProduct $productResource - * @param \Closure $proceed - * @param AbstractModel $product - * @return ResourceProduct -+ * @throws \Exception - */ - public function aroundSave(ResourceProduct $productResource, \Closure $proceed, AbstractModel $product) - { -@@ -31,6 +35,7 @@ class Product extends AbstractPlugin - * @param \Closure $proceed - * @param AbstractModel $product - * @return ResourceProduct -+ * @throws \Exception - */ - public function aroundDelete(ResourceProduct $productResource, \Closure $proceed, AbstractModel $product) - { -@@ -38,6 +43,8 @@ class Product extends AbstractPlugin - } - - /** -+ * Reindex catalog search. -+ * - * @param ResourceProduct $productResource - * @param \Closure $proceed - * @param AbstractModel $product -diff --git a/app/code/Magento/CatalogSearch/Model/Indexer/Fulltext/Store.php b/app/code/Magento/CatalogSearch/Model/Indexer/Fulltext/Store.php -index e971f59cf10..23ab52012f2 100644 ---- a/app/code/Magento/CatalogSearch/Model/Indexer/Fulltext/Store.php -+++ b/app/code/Magento/CatalogSearch/Model/Indexer/Fulltext/Store.php -@@ -11,6 +11,9 @@ use Magento\Framework\Search\Request\DimensionFactory; - use Magento\Framework\Indexer\ConfigInterface; - use Magento\Framework\Event\ObserverInterface; - -+/** -+ * Catalog search indexer plugin for store. -+ */ - class Store implements ObserverInterface - { - /** -@@ -44,6 +47,8 @@ class Store implements ObserverInterface - } - - /** -+ * Reindex catalog search. -+ * - * @param \Magento\Store\Model\Store $store - * @return void - */ -@@ -59,6 +64,8 @@ class Store implements ObserverInterface - } - - /** -+ * Reindex catalog search on store modification. -+ * - * @param \Magento\Framework\Event\Observer $observer - * @return void - */ -diff --git a/app/code/Magento/CatalogSearch/Model/Indexer/IndexStructure.php b/app/code/Magento/CatalogSearch/Model/Indexer/IndexStructure.php -index c46f062c1e6..0d226acdc3d 100644 ---- a/app/code/Magento/CatalogSearch/Model/Indexer/IndexStructure.php -+++ b/app/code/Magento/CatalogSearch/Model/Indexer/IndexStructure.php -@@ -15,6 +15,8 @@ use Magento\Framework\Indexer\ScopeResolver\IndexScopeResolver; - use Magento\Framework\Search\Request\IndexScopeResolverInterface; - - /** -+ * Catalog search index structure. -+ * - * @api - * @since 100.0.2 - */ -@@ -43,9 +45,7 @@ class IndexStructure implements IndexStructureInterface - } - - /** -- * @param string $index -- * @param Dimension[] $dimensions -- * @return void -+ * @inheritdoc - */ - public function delete($index, array $dimensions = []) - { -@@ -56,11 +56,7 @@ class IndexStructure implements IndexStructureInterface - } - - /** -- * @param string $index -- * @param array $fields -- * @param array $dimensions -- * @SuppressWarnings(PHPMD.UnusedFormalParameter) -- * @return void -+ * @inheritdoc - */ - public function create($index, array $fields, array $dimensions = []) - { -@@ -68,6 +64,8 @@ class IndexStructure implements IndexStructureInterface - } - - /** -+ * Create fulltext index table. -+ * - * @param string $tableName - * @throws \Zend_Db_Exception - * @return void -diff --git a/app/code/Magento/CatalogSearch/Model/Indexer/IndexStructureFactory.php b/app/code/Magento/CatalogSearch/Model/Indexer/IndexStructureFactory.php -index d8b3c19ddb9..d54d6c939cc 100644 ---- a/app/code/Magento/CatalogSearch/Model/Indexer/IndexStructureFactory.php -+++ b/app/code/Magento/CatalogSearch/Model/Indexer/IndexStructureFactory.php -@@ -10,6 +10,8 @@ use Magento\Framework\ObjectManagerInterface; - use Magento\Framework\Search\EngineResolverInterface; - - /** -+ * Index structure factory -+ * - * @api - * @since 100.1.0 - */ -diff --git a/app/code/Magento/CatalogSearch/Model/Indexer/IndexStructureProxy.php b/app/code/Magento/CatalogSearch/Model/Indexer/IndexStructureProxy.php -index 0fb8af51445..c62d2a03356 100644 ---- a/app/code/Magento/CatalogSearch/Model/Indexer/IndexStructureProxy.php -+++ b/app/code/Magento/CatalogSearch/Model/Indexer/IndexStructureProxy.php -@@ -7,6 +7,9 @@ namespace Magento\CatalogSearch\Model\Indexer; - - use Magento\Framework\Indexer\IndexStructureInterface; - -+/** -+ * Catalog search index structure proxy. -+ */ - class IndexStructureProxy implements IndexStructureInterface - { - /** -@@ -29,7 +32,7 @@ class IndexStructureProxy implements IndexStructureInterface - } - - /** -- * {@inheritdoc} -+ * @inheritdoc - */ - public function delete( - $index, -@@ -39,7 +42,7 @@ class IndexStructureProxy implements IndexStructureInterface - } - - /** -- * {@inheritdoc} -+ * @inheritdoc - */ - public function create( - $index, -diff --git a/app/code/Magento/CatalogSearch/Model/Indexer/IndexSwitcherInterface.php b/app/code/Magento/CatalogSearch/Model/Indexer/IndexSwitcherInterface.php -index bdb663a0b61..f45ef11a863 100644 ---- a/app/code/Magento/CatalogSearch/Model/Indexer/IndexSwitcherInterface.php -+++ b/app/code/Magento/CatalogSearch/Model/Indexer/IndexSwitcherInterface.php -@@ -6,7 +6,8 @@ - namespace Magento\CatalogSearch\Model\Indexer; - - /** -- * Provides a functionality to replace main index with its temporary representation -+ * Provides a functionality to replace main index with its temporary representation. -+ * - * @api - * @since 100.2.0 - */ -diff --git a/app/code/Magento/CatalogSearch/Model/Indexer/IndexSwitcherProxy.php b/app/code/Magento/CatalogSearch/Model/Indexer/IndexSwitcherProxy.php -index e5c801cf0b7..e4a20cc188f 100644 ---- a/app/code/Magento/CatalogSearch/Model/Indexer/IndexSwitcherProxy.php -+++ b/app/code/Magento/CatalogSearch/Model/Indexer/IndexSwitcherProxy.php -@@ -51,7 +51,7 @@ class IndexSwitcherProxy implements IndexSwitcherInterface - } - - /** -- * {@inheritDoc} -+ * @inheritDoc - * - * As index switcher is an optional part of the search SPI, it may be not defined by a search engine. - * It is especially reasonable for search engines with pre-defined indexes declaration (like Sphinx) -diff --git a/app/code/Magento/CatalogSearch/Model/Indexer/IndexerHandler.php b/app/code/Magento/CatalogSearch/Model/Indexer/IndexerHandler.php -index 931d7571a90..9f105bd3ea4 100644 ---- a/app/code/Magento/CatalogSearch/Model/Indexer/IndexerHandler.php -+++ b/app/code/Magento/CatalogSearch/Model/Indexer/IndexerHandler.php -@@ -14,8 +14,12 @@ use Magento\Framework\Search\Request\IndexScopeResolverInterface; - use Magento\Framework\Indexer\SaveHandler\Batch; - - /** -+ * Catalog search indexer handler. -+ * - * @api - * @since 100.0.2 -+ * @deprecated -+ * @see \Magento\ElasticSearch - */ - class IndexerHandler implements IndexerInterface - { -@@ -90,7 +94,7 @@ class IndexerHandler implements IndexerInterface - } - - /** -- * {@inheritdoc} -+ * @inheritdoc - */ - public function saveIndex($dimensions, \Traversable $documents) - { -@@ -100,7 +104,7 @@ class IndexerHandler implements IndexerInterface - } - - /** -- * {@inheritdoc} -+ * @inheritdoc - */ - public function deleteIndex($dimensions, \Traversable $documents) - { -@@ -111,7 +115,7 @@ class IndexerHandler implements IndexerInterface - } - - /** -- * {@inheritdoc} -+ * @inheritdoc - */ - public function cleanIndex($dimensions) - { -@@ -120,14 +124,20 @@ class IndexerHandler implements IndexerInterface - } - - /** -- * {@inheritdoc} -+ * @inheritdoc - */ -- public function isAvailable() -+ public function isAvailable($dimensions = []) - { -- return true; -+ if (empty($dimensions)) { -+ return true; -+ } -+ -+ return $this->resource->getConnection()->isTableExists($this->getTableName($dimensions)); - } - - /** -+ * Returns table name. -+ * - * @param Dimension[] $dimensions - * @return string - */ -@@ -137,6 +147,8 @@ class IndexerHandler implements IndexerInterface - } - - /** -+ * Returns index name. -+ * - * @return string - */ - private function getIndexName() -@@ -145,6 +157,8 @@ class IndexerHandler implements IndexerInterface - } - - /** -+ * Add documents to storage. -+ * - * @param array $documents - * @param Dimension[] $dimensions - * @return void -@@ -163,6 +177,8 @@ class IndexerHandler implements IndexerInterface - } - - /** -+ * Searchable filter preparation. -+ * - * @param array $documents - * @return array - */ -@@ -183,6 +199,8 @@ class IndexerHandler implements IndexerInterface - } - - /** -+ * Prepare fields. -+ * - * @return void - */ - private function prepareFields() -diff --git a/app/code/Magento/CatalogSearch/Model/Indexer/IndexerHandlerFactory.php b/app/code/Magento/CatalogSearch/Model/Indexer/IndexerHandlerFactory.php -index b9b44df6f40..841ee8708f2 100644 ---- a/app/code/Magento/CatalogSearch/Model/Indexer/IndexerHandlerFactory.php -+++ b/app/code/Magento/CatalogSearch/Model/Indexer/IndexerHandlerFactory.php -@@ -10,6 +10,8 @@ use Magento\Framework\ObjectManagerInterface; - use Magento\Framework\Search\EngineResolverInterface; - - /** -+ * Indexer handler factory. -+ * - * @api - * @since 100.0.2 - */ -diff --git a/app/code/Magento/CatalogSearch/Model/Indexer/Mview/Action.php b/app/code/Magento/CatalogSearch/Model/Indexer/Mview/Action.php -index 47a8681a73c..b6639f76045 100644 ---- a/app/code/Magento/CatalogSearch/Model/Indexer/Mview/Action.php -+++ b/app/code/Magento/CatalogSearch/Model/Indexer/Mview/Action.php -@@ -9,6 +9,9 @@ use Magento\CatalogSearch\Model\Indexer\Fulltext; - use Magento\Framework\Mview\ActionInterface; - use Magento\Framework\Indexer\IndexerInterfaceFactory; - -+/** -+ * Catalog search materialized view index action. -+ */ - class Action implements ActionInterface - { - /** -diff --git a/app/code/Magento/CatalogSearch/Model/Indexer/ProductFieldset.php b/app/code/Magento/CatalogSearch/Model/Indexer/ProductFieldset.php -index 343dc50d604..6db063bde7d 100644 ---- a/app/code/Magento/CatalogSearch/Model/Indexer/ProductFieldset.php -+++ b/app/code/Magento/CatalogSearch/Model/Indexer/ProductFieldset.php -@@ -13,6 +13,8 @@ use Magento\Eav\Model\Entity\Attribute; - /** - * @api - * @since 100.0.2 -+ * @deprecated -+ * @see \Magento\ElasticSearch - */ - class ProductFieldset implements \Magento\Framework\Indexer\FieldsetInterface - { -diff --git a/app/code/Magento/CatalogSearch/Model/Indexer/Scope/IndexSwitcher.php b/app/code/Magento/CatalogSearch/Model/Indexer/Scope/IndexSwitcher.php -index 86649cd1093..ed2b1be5c70 100644 ---- a/app/code/Magento/CatalogSearch/Model/Indexer/Scope/IndexSwitcher.php -+++ b/app/code/Magento/CatalogSearch/Model/Indexer/Scope/IndexSwitcher.php -@@ -11,6 +11,9 @@ use Magento\Framework\Search\Request\IndexScopeResolverInterface; - - /** - * Provides a functionality to replace main index with its temporary representation -+ * -+ * @deprecated -+ * @see \Magento\ElasticSearch - */ - class IndexSwitcher implements IndexSwitcherInterface - { -diff --git a/app/code/Magento/CatalogSearch/Model/Indexer/Scope/IndexTableNotExistException.php b/app/code/Magento/CatalogSearch/Model/Indexer/Scope/IndexTableNotExistException.php -index a386b74084a..b01f3c50d50 100644 ---- a/app/code/Magento/CatalogSearch/Model/Indexer/Scope/IndexTableNotExistException.php -+++ b/app/code/Magento/CatalogSearch/Model/Indexer/Scope/IndexTableNotExistException.php -@@ -14,6 +14,8 @@ use Magento\Framework\Exception\LocalizedException; - * - * @api - * @since 100.2.0 -+ * @deprecated -+ * @see \Magento\ElasticSearch - */ - class IndexTableNotExistException extends LocalizedException - { -diff --git a/app/code/Magento/CatalogSearch/Model/Indexer/Scope/ScopeProxy.php b/app/code/Magento/CatalogSearch/Model/Indexer/Scope/ScopeProxy.php -index 9166ddbef60..fcecb36e7d5 100644 ---- a/app/code/Magento/CatalogSearch/Model/Indexer/Scope/ScopeProxy.php -+++ b/app/code/Magento/CatalogSearch/Model/Indexer/Scope/ScopeProxy.php -@@ -9,8 +9,7 @@ namespace Magento\CatalogSearch\Model\Indexer\Scope; - use Magento\Framework\Search\Request\Dimension; - - /** -- * Implementation of IndexScopeResolverInterface which resolves index scope dynamically -- * depending on current scope state -+ * Implementation of IndexScopeResolverInterface which resolves index scope dynamically depending on current scope state - */ - class ScopeProxy implements \Magento\Framework\Search\Request\IndexScopeResolverInterface - { -@@ -64,9 +63,7 @@ class ScopeProxy implements \Magento\Framework\Search\Request\IndexScopeResolver - } - - /** -- * @param string $index -- * @param Dimension[] $dimensions -- * @return string -+ * @inheritdoc - */ - public function resolve($index, array $dimensions) - { -diff --git a/app/code/Magento/CatalogSearch/Model/Indexer/Scope/State.php b/app/code/Magento/CatalogSearch/Model/Indexer/Scope/State.php -index 5f9a3e30599..a12ae8e69c3 100644 ---- a/app/code/Magento/CatalogSearch/Model/Indexer/Scope/State.php -+++ b/app/code/Magento/CatalogSearch/Model/Indexer/Scope/State.php -@@ -31,6 +31,7 @@ class State - - /** - * Set the state to use temporary Index -+ * - * @return void - */ - public function useTemporaryIndex() -@@ -40,6 +41,7 @@ class State - - /** - * Set the state to use regular Index -+ * - * @return void - */ - public function useRegularIndex() -@@ -48,6 +50,8 @@ class State - } - - /** -+ * Get state. -+ * - * @return string - */ - public function getState() -diff --git a/app/code/Magento/CatalogSearch/Model/Indexer/Scope/TemporaryResolver.php b/app/code/Magento/CatalogSearch/Model/Indexer/Scope/TemporaryResolver.php -index c52a0a05866..796559d1f70 100644 ---- a/app/code/Magento/CatalogSearch/Model/Indexer/Scope/TemporaryResolver.php -+++ b/app/code/Magento/CatalogSearch/Model/Indexer/Scope/TemporaryResolver.php -@@ -11,6 +11,9 @@ use Magento\Framework\Search\Request\Dimension; - - /** - * Resolves name of a temporary table for indexation -+ * -+ * @deprecated -+ * @see \Magento\ElasticSearch - */ - class TemporaryResolver implements \Magento\Framework\Search\Request\IndexScopeResolverInterface - { -diff --git a/app/code/Magento/CatalogSearch/Model/Indexer/Scope/UnknownStateException.php b/app/code/Magento/CatalogSearch/Model/Indexer/Scope/UnknownStateException.php -index b44a2d22054..8722cd52b61 100644 ---- a/app/code/Magento/CatalogSearch/Model/Indexer/Scope/UnknownStateException.php -+++ b/app/code/Magento/CatalogSearch/Model/Indexer/Scope/UnknownStateException.php -@@ -13,6 +13,8 @@ use Magento\Framework\Exception\LocalizedException; - * - * @api - * @since 100.2.0 -+ * @deprecated -+ * @see \Magento\ElasticSearch - */ - class UnknownStateException extends LocalizedException - { -diff --git a/app/code/Magento/CatalogSearch/Model/Layer/Category/ItemCollectionProvider.php b/app/code/Magento/CatalogSearch/Model/Layer/Category/ItemCollectionProvider.php -index 4ce286bf159..c24665f4808 100644 ---- a/app/code/Magento/CatalogSearch/Model/Layer/Category/ItemCollectionProvider.php -+++ b/app/code/Magento/CatalogSearch/Model/Layer/Category/ItemCollectionProvider.php -@@ -9,6 +9,9 @@ namespace Magento\CatalogSearch\Model\Layer\Category; - use Magento\Catalog\Model\Layer\ItemCollectionProviderInterface; - use Magento\Catalog\Model\ResourceModel\Product\CollectionFactory; - -+/** -+ * Catalog search category layer collection provider. -+ */ - class ItemCollectionProvider implements ItemCollectionProviderInterface - { - /** -@@ -25,8 +28,7 @@ class ItemCollectionProvider implements ItemCollectionProviderInterface - } - - /** -- * @param \Magento\Catalog\Model\Category $category -- * @return \Magento\Catalog\Model\ResourceModel\Product\Collection -+ * @inheritdoc - */ - public function getCollection(\Magento\Catalog\Model\Category $category) - { -diff --git a/app/code/Magento/CatalogSearch/Model/Layer/Filter/Attribute.php b/app/code/Magento/CatalogSearch/Model/Layer/Filter/Attribute.php -index 7aac6e98fc0..794d0ac9715 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 @@ class Attribute extends AbstractFilter - 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 @@ class Attribute extends AbstractFilter - * @param array $option - * @param boolean $isAttributeFilterable - * @param array $optionsFacetedData -- * @param int $productSize - * @return void - */ -- private function buildOptionData($option, $isAttributeFilterable, $optionsFacetedData, $productSize) -+ private function buildOptionData($option, $isAttributeFilterable, $optionsFacetedData) - { - $value = $this->getOptionValue($option); - if ($value === false) { - return; - } - $count = $this->getOptionCount($value, $optionsFacetedData); -- if ($isAttributeFilterable && (!$this->isOptionReducesResults($count, $productSize) || $count === 0)) { -+ if ($isAttributeFilterable && $count === 0) { - return; - } - -@@ -156,4 +153,12 @@ class Attribute extends AbstractFilter - ? (int)$optionsFacetedData[$value]['count'] - : 0; - } -+ -+ /** -+ * @inheritdoc -+ */ -+ protected function isOptionReducesResults($optionCount, $totalSize) -+ { -+ return $optionCount <= $totalSize; -+ } - } -diff --git a/app/code/Magento/CatalogSearch/Model/Layer/Filter/Category.php b/app/code/Magento/CatalogSearch/Model/Layer/Filter/Category.php -index 7c15514f211..0998cf7a9b3 100644 ---- a/app/code/Magento/CatalogSearch/Model/Layer/Filter/Category.php -+++ b/app/code/Magento/CatalogSearch/Model/Layer/Filter/Category.php -@@ -24,14 +24,16 @@ class Category extends AbstractFilter - private $dataProvider; - - /** -+ * Category constructor. -+ * - * @param \Magento\Catalog\Model\Layer\Filter\ItemFactory $filterItemFactory - * @param \Magento\Store\Model\StoreManagerInterface $storeManager - * @param \Magento\Catalog\Model\Layer $layer - * @param \Magento\Catalog\Model\Layer\Filter\Item\DataBuilder $itemDataBuilder -- * @param \Magento\Catalog\Model\CategoryFactory $categoryFactory - * @param \Magento\Framework\Escaper $escaper -- * @param CategoryManagerFactory $categoryManager -+ * @param \Magento\Catalog\Model\Layer\Filter\DataProvider\CategoryFactory $categoryDataProviderFactory - * @param array $data -+ * @throws \Magento\Framework\Exception\LocalizedException - */ - public function __construct( - \Magento\Catalog\Model\Layer\Filter\ItemFactory $filterItemFactory, -diff --git a/app/code/Magento/CatalogSearch/Model/Layer/Filter/Decimal.php b/app/code/Magento/CatalogSearch/Model/Layer/Filter/Decimal.php -index e61a886a41d..e9fb1070fed 100644 ---- a/app/code/Magento/CatalogSearch/Model/Layer/Filter/Decimal.php -+++ b/app/code/Magento/CatalogSearch/Model/Layer/Filter/Decimal.php -@@ -111,12 +111,9 @@ class Decimal extends AbstractFilter - $from = ''; - } - if ($to == '*') { -- $to = ''; -+ $to = null; - } -- $label = $this->renderRangeLabel( -- empty($from) ? 0 : $from, -- empty($to) ? 0 : $to -- ); -+ $label = $this->renderRangeLabel(empty($from) ? 0 : $from, $to); - $value = $from . '-' . $to; - - $data[] = [ -@@ -141,7 +138,7 @@ class Decimal extends AbstractFilter - protected function renderRangeLabel($fromPrice, $toPrice) - { - $formattedFromPrice = $this->priceCurrency->format($fromPrice); -- if ($toPrice === '') { -+ if ($toPrice === null) { - return __('%1 and above', $formattedFromPrice); - } else { - if ($fromPrice != $toPrice) { -diff --git a/app/code/Magento/CatalogSearch/Model/Layer/Filter/Price.php b/app/code/Magento/CatalogSearch/Model/Layer/Filter/Price.php -index 108f1b9f4fd..a19f53469ae 100644 ---- a/app/code/Magento/CatalogSearch/Model/Layer/Filter/Price.php -+++ b/app/code/Magento/CatalogSearch/Model/Layer/Filter/Price.php -@@ -86,6 +86,8 @@ class Price extends AbstractFilter - } - - /** -+ * Get resource model. -+ * - * @return \Magento\Catalog\Model\ResourceModel\Layer\Filter\Price - */ - public function getResource() -@@ -223,6 +225,8 @@ class Price extends AbstractFilter - } - - /** -+ * Get 'to' part of the filter. -+ * - * @param float $from - * @return float - */ -@@ -237,6 +241,8 @@ class Price extends AbstractFilter - } - - /** -+ * Get 'from' part of the filter. -+ * - * @param float $from - * @return float - */ -@@ -251,6 +257,8 @@ class Price extends AbstractFilter - } - - /** -+ * Prepare filter data. -+ * - * @param string $key - * @param int $count - * @return array -diff --git a/app/code/Magento/CatalogSearch/Model/Layer/Search/Plugin/CollectionFilter.php b/app/code/Magento/CatalogSearch/Model/Layer/Search/Plugin/CollectionFilter.php -index 4ffd8ff4ba5..eb901498c4e 100644 ---- a/app/code/Magento/CatalogSearch/Model/Layer/Search/Plugin/CollectionFilter.php -+++ b/app/code/Magento/CatalogSearch/Model/Layer/Search/Plugin/CollectionFilter.php -@@ -9,6 +9,9 @@ namespace Magento\CatalogSearch\Model\Layer\Search\Plugin; - use Magento\Catalog\Model\Category; - use Magento\Search\Model\QueryFactory; - -+/** -+ * Catalog search plugin for search collection filter in layered navigation. -+ */ - class CollectionFilter - { - /** -diff --git a/app/code/Magento/CatalogSearch/Model/Layer/Search/StateKey.php b/app/code/Magento/CatalogSearch/Model/Layer/Search/StateKey.php -index 4f14b7daba1..98caccea2ae 100644 ---- a/app/code/Magento/CatalogSearch/Model/Layer/Search/StateKey.php -+++ b/app/code/Magento/CatalogSearch/Model/Layer/Search/StateKey.php -@@ -9,6 +9,9 @@ namespace Magento\CatalogSearch\Model\Layer\Search; - - use Magento\Catalog\Model\Layer\StateKeyInterface; - -+/** -+ * Catalog search state key for layered navigation. -+ */ - class StateKey extends \Magento\Catalog\Model\Layer\Category\StateKey implements StateKeyInterface - { - /** -@@ -31,8 +34,7 @@ class StateKey extends \Magento\Catalog\Model\Layer\Category\StateKey implements - } - - /** -- * @param \Magento\Catalog\Model\Category $category -- * @return string|void -+ * @inheritdoc - */ - public function toString($category) - { -diff --git a/app/code/Magento/CatalogSearch/Model/Price/Interval.php b/app/code/Magento/CatalogSearch/Model/Price/Interval.php -index db1d550c372..ea2d24aeadf 100644 ---- a/app/code/Magento/CatalogSearch/Model/Price/Interval.php -+++ b/app/code/Magento/CatalogSearch/Model/Price/Interval.php -@@ -7,6 +7,9 @@ namespace Magento\CatalogSearch\Model\Price; - - use Magento\Framework\Search\Dynamic\IntervalInterface; - -+/** -+ * Catalog search price interval. -+ */ - class Interval implements IntervalInterface - { - /** -@@ -23,7 +26,7 @@ class Interval implements IntervalInterface - } - - /** -- * {@inheritdoc} -+ * @inheritdoc - */ - public function load($limit, $offset = null, $lower = null, $upper = null) - { -@@ -32,7 +35,7 @@ class Interval implements IntervalInterface - } - - /** -- * {@inheritdoc} -+ * @inheritdoc - */ - public function loadPrevious($data, $index, $lower = null) - { -@@ -41,7 +44,7 @@ class Interval implements IntervalInterface - } - - /** -- * {@inheritdoc} -+ * @inheritdoc - */ - public function loadNext($data, $rightIndex, $upper = null) - { -@@ -50,6 +53,8 @@ class Interval implements IntervalInterface - } - - /** -+ * Convert to float values. -+ * - * @param array $prices - * @return array - */ -diff --git a/app/code/Magento/CatalogSearch/Model/ResourceModel/Advanced.php b/app/code/Magento/CatalogSearch/Model/ResourceModel/Advanced.php -index 8a5a6372bb2..05254a50aad 100644 ---- a/app/code/Magento/CatalogSearch/Model/ResourceModel/Advanced.php -+++ b/app/code/Magento/CatalogSearch/Model/ResourceModel/Advanced.php -@@ -8,7 +8,6 @@ namespace Magento\CatalogSearch\Model\ResourceModel; - /** - * Advanced Catalog Search resource model - * -- * @author Magento Core Team <core@magentocommerce.com> - * @api - * @since 100.0.2 - */ -diff --git a/app/code/Magento/CatalogSearch/Model/ResourceModel/Advanced/Collection.php b/app/code/Magento/CatalogSearch/Model/ResourceModel/Advanced/Collection.php -index 57896ba5a79..9bd9a895a2a 100644 ---- a/app/code/Magento/CatalogSearch/Model/ResourceModel/Advanced/Collection.php -+++ b/app/code/Magento/CatalogSearch/Model/ResourceModel/Advanced/Collection.php -@@ -3,23 +3,36 @@ - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -+ - namespace Magento\CatalogSearch\Model\ResourceModel\Advanced; - - use Magento\Catalog\Model\Product; -+use Magento\CatalogSearch\Model\ResourceModel\Fulltext\Collection\SearchCriteriaResolverInterface; -+use Magento\CatalogSearch\Model\ResourceModel\Fulltext\Collection\SearchResultApplierInterface; -+use Magento\Framework\Search\EngineResolverInterface; -+use Magento\Search\Model\EngineResolver; -+use Magento\CatalogSearch\Model\ResourceModel\Fulltext\Collection\TotalRecordsResolverInterface; -+use Magento\CatalogSearch\Model\ResourceModel\Fulltext\Collection\TotalRecordsResolverFactory; - use Magento\Framework\Api\FilterBuilder; -+use Magento\Framework\DB\Select; - use Magento\Framework\Api\Search\SearchCriteriaBuilder; - use Magento\Framework\Api\Search\SearchResultFactory; - use Magento\Framework\EntityManager\MetadataPool; - use Magento\Framework\Exception\LocalizedException; --use Magento\Framework\Search\Adapter\Mysql\TemporaryStorage; - use Magento\Framework\Search\Request\EmptyRequestDataException; - use Magento\Framework\Search\Request\NonExistingRequestNameException; - use Magento\Catalog\Model\ResourceModel\Product\Collection\ProductLimitationFactory; -+use Magento\CatalogSearch\Model\ResourceModel\Fulltext\Collection\SearchCriteriaResolverFactory; -+use Magento\CatalogSearch\Model\ResourceModel\Fulltext\Collection\SearchResultApplierFactory; -+use Magento\Framework\App\ObjectManager; -+use Magento\Framework\Api\Search\SearchResultInterface; - - /** -- * Collection Advanced -+ * Advanced search collection -+ * -+ * This collection should be refactored to not have dependencies on MySQL-specific implementation. - * -- * @author Magento Core Team <core@magentocommerce.com> -+ * @SuppressWarnings(PHPMD.CookieAndSessionMisuse) - * @SuppressWarnings(PHPMD.CouplingBetweenObjects) - * @api - * @since 100.0.2 -@@ -39,6 +52,7 @@ class Collection extends \Magento\Catalog\Model\ResourceModel\Product\Collection - - /** - * @var \Magento\Framework\Search\Adapter\Mysql\TemporaryStorageFactory -+ * @deprecated There must be no dependencies on specific adapter in generic search implementation - */ - private $temporaryStorageFactory; - -@@ -57,6 +71,41 @@ class Collection extends \Magento\Catalog\Model\ResourceModel\Product\Collection - */ - private $filterBuilder; - -+ /** -+ * @var \Magento\Framework\Api\Search\SearchResultInterface -+ */ -+ private $searchResult; -+ -+ /** -+ * @var string -+ */ -+ private $searchRequestName; -+ -+ /** -+ * @var SearchCriteriaResolverFactory -+ */ -+ private $searchCriteriaResolverFactory; -+ -+ /** -+ * @var SearchResultApplierFactory -+ */ -+ private $searchResultApplierFactory; -+ -+ /** -+ * @var TotalRecordsResolverFactory -+ */ -+ private $totalRecordsResolverFactory; -+ -+ /** -+ * @var EngineResolverInterface -+ */ -+ private $engineResolver; -+ -+ /** -+ * @var array -+ */ -+ private $searchOrders; -+ - /** - * Collection constructor - * -@@ -70,7 +119,7 @@ class Collection extends \Magento\Catalog\Model\ResourceModel\Product\Collection - * @param \Magento\Catalog\Model\ResourceModel\Helper $resourceHelper - * @param \Magento\Framework\Validator\UniversalFactory $universalFactory - * @param \Magento\Store\Model\StoreManagerInterface $storeManager -- * @param \Magento\Framework\Module\Manager $moduleManager -+ * @param \Magento\Framework\Module\ModuleManagerInterface $moduleManager - * @param \Magento\Catalog\Model\Indexer\Product\Flat\State $catalogProductFlatState - * @param \Magento\Framework\App\Config\ScopeConfigInterface $scopeConfig - * @param Product\OptionFactory $productOptionFactory -@@ -86,7 +135,11 @@ class Collection extends \Magento\Catalog\Model\ResourceModel\Product\Collection - * @param SearchResultFactory|null $searchResultFactory - * @param ProductLimitationFactory|null $productLimitationFactory - * @param MetadataPool|null $metadataPool -- * -+ * @param string $searchRequestName -+ * @param SearchCriteriaResolverFactory|null $searchCriteriaResolverFactory -+ * @param SearchResultApplierFactory|null $searchResultApplierFactory -+ * @param TotalRecordsResolverFactory|null $totalRecordsResolverFactory -+ * @param EngineResolverInterface|null $engineResolver - * @SuppressWarnings(PHPMD.ExcessiveParameterList) - */ - public function __construct( -@@ -100,7 +153,7 @@ class Collection extends \Magento\Catalog\Model\ResourceModel\Product\Collection - \Magento\Catalog\Model\ResourceModel\Helper $resourceHelper, - \Magento\Framework\Validator\UniversalFactory $universalFactory, - \Magento\Store\Model\StoreManagerInterface $storeManager, -- \Magento\Framework\Module\Manager $moduleManager, -+ \Magento\Framework\Module\ModuleManagerInterface $moduleManager, - \Magento\Catalog\Model\Indexer\Product\Flat\State $catalogProductFlatState, - \Magento\Framework\App\Config\ScopeConfigInterface $scopeConfig, - \Magento\Catalog\Model\Product\OptionFactory $productOptionFactory, -@@ -115,15 +168,29 @@ class Collection extends \Magento\Catalog\Model\ResourceModel\Product\Collection - \Magento\Framework\DB\Adapter\AdapterInterface $connection = null, - SearchResultFactory $searchResultFactory = null, - ProductLimitationFactory $productLimitationFactory = null, -- MetadataPool $metadataPool = null -+ MetadataPool $metadataPool = null, -+ $searchRequestName = 'advanced_search_container', -+ SearchCriteriaResolverFactory $searchCriteriaResolverFactory = null, -+ SearchResultApplierFactory $searchResultApplierFactory = null, -+ TotalRecordsResolverFactory $totalRecordsResolverFactory = null, -+ EngineResolverInterface $engineResolver = null - ) { - $this->requestBuilder = $requestBuilder; - $this->searchEngine = $searchEngine; - $this->temporaryStorageFactory = $temporaryStorageFactory; -+ $this->searchRequestName = $searchRequestName; - if ($searchResultFactory === null) { - $this->searchResultFactory = \Magento\Framework\App\ObjectManager::getInstance() - ->get(\Magento\Framework\Api\Search\SearchResultFactory::class); - } -+ $this->searchCriteriaResolverFactory = $searchCriteriaResolverFactory ?: ObjectManager::getInstance() -+ ->get(SearchCriteriaResolverFactory::class); -+ $this->searchResultApplierFactory = $searchResultApplierFactory ?: ObjectManager::getInstance() -+ ->get(SearchResultApplierFactory::class); -+ $this->totalRecordsResolverFactory = $totalRecordsResolverFactory ?: ObjectManager::getInstance() -+ ->get(TotalRecordsResolverFactory::class); -+ $this->engineResolver = $engineResolver ?: ObjectManager::getInstance() -+ ->get(EngineResolverInterface::class); - parent::__construct( - $entityFactory, - $logger, -@@ -165,11 +232,89 @@ class Collection extends \Magento\Catalog\Model\ResourceModel\Product\Collection - return $this; - } - -+ /** -+ * @inheritdoc -+ */ -+ public function setOrder($attribute, $dir = Select::SQL_DESC) -+ { -+ $this->setSearchOrder($attribute, $dir); -+ if ($this->isCurrentEngineMysql()) { -+ parent::setOrder($attribute, $dir); -+ } -+ -+ return $this; -+ } -+ -+ /** -+ * @inheritdoc -+ */ -+ public function addCategoryFilter(\Magento\Catalog\Model\Category $category) -+ { -+ /** -+ * This changes need in backward compatible reasons for support dynamic improved algorithm -+ * for price aggregation process. -+ */ -+ if ($this->isCurrentEngineMysql()) { -+ parent::addCategoryFilter($category); -+ } else { -+ $this->addFieldToFilter('category_ids', $category->getId()); -+ $this->_productLimitationPrice(); -+ } -+ -+ return $this; -+ } -+ -+ /** -+ * @inheritdoc -+ */ -+ public function setVisibility($visibility) -+ { -+ /** -+ * This changes need in backward compatible reasons for support dynamic improved algorithm -+ * for price aggregation process. -+ */ -+ if ($this->isCurrentEngineMysql()) { -+ parent::setVisibility($visibility); -+ } else { -+ $this->addFieldToFilter('visibility', $visibility); -+ } -+ -+ return $this; -+ } -+ -+ /** -+ * Set sort order for search query. -+ * -+ * @param string $field -+ * @param string $direction -+ * @return void -+ */ -+ private function setSearchOrder($field, $direction) -+ { -+ $field = (string)$this->_getMappedField($field); -+ $direction = strtoupper($direction) == self::SORT_ORDER_ASC ? self::SORT_ORDER_ASC : self::SORT_ORDER_DESC; -+ -+ $this->searchOrders[$field] = $direction; -+ } -+ -+ /** -+ * Check if current engine is MYSQL. -+ * -+ * @return bool -+ */ -+ private function isCurrentEngineMysql() -+ { -+ return $this->engineResolver->getCurrentSearchEngine() === EngineResolver::CATALOG_SEARCH_MYSQL_ENGINE; -+ } -+ - /** - * @inheritdoc - */ - protected function _renderFiltersBefore() - { -+ if ($this->isLoaded()) { -+ return; -+ } - if ($this->filters) { - foreach ($this->filters as $attributes) { - foreach ($attributes as $attributeCode => $attributeValue) { -@@ -177,34 +322,79 @@ class Collection extends \Magento\Catalog\Model\ResourceModel\Product\Collection - $this->addAttributeToSearch($attributeCode, $attributeValue); - } - } -- $searchCriteria = $this->getSearchCriteriaBuilder()->create(); -- $searchCriteria->setRequestName('advanced_search_container'); -+ $searchCriteria = $this->getSearchCriteriaResolver()->resolve(); - try { -- $searchResult = $this->getSearch()->search($searchCriteria); -+ $this->searchResult = $this->getSearch()->search($searchCriteria); -+ $this->_totalRecords = $this->getTotalRecordsResolver($this->searchResult)->resolve(); - } catch (EmptyRequestDataException $e) { - /** @var \Magento\Framework\Api\Search\SearchResultInterface $searchResult */ -- $searchResult = $this->searchResultFactory->create()->setItems([]); -+ $this->searchResult = $this->searchResultFactory->create()->setItems([]); - } catch (NonExistingRequestNameException $e) { - $this->_logger->error($e->getMessage()); - throw new LocalizedException( - __('An error occurred. For details, see the error log.') - ); - } -- $temporaryStorage = $this->temporaryStorageFactory->create(); -- $table = $temporaryStorage->storeApiDocuments($searchResult->getItems()); -- -- $this->getSelect()->joinInner( -- [ -- 'search_result' => $table->getName(), -- ], -- 'e.entity_id = search_result.' . TemporaryStorage::FIELD_ENTITY_ID, -- [] -- ); -+ $this->getSearchResultApplier($this->searchResult)->apply(); - } - parent::_renderFiltersBefore(); - } - - /** -+ * Get total records resolver. -+ * -+ * @param SearchResultInterface $searchResult -+ * @return TotalRecordsResolverInterface -+ */ -+ private function getTotalRecordsResolver(SearchResultInterface $searchResult): TotalRecordsResolverInterface -+ { -+ return $this->totalRecordsResolverFactory->create( -+ [ -+ 'searchResult' => $searchResult, -+ ] -+ ); -+ } -+ -+ /** -+ * Get search criteria resolver. -+ * -+ * @return SearchCriteriaResolverInterface -+ */ -+ private function getSearchCriteriaResolver(): SearchCriteriaResolverInterface -+ { -+ return $this->searchCriteriaResolverFactory->create( -+ [ -+ 'builder' => $this->getSearchCriteriaBuilder(), -+ 'collection' => $this, -+ 'searchRequestName' => $this->searchRequestName, -+ 'currentPage' => $this->_curPage, -+ 'size' => $this->getPageSize(), -+ 'orders' => $this->searchOrders, -+ ] -+ ); -+ } -+ -+ /** -+ * Get search result applier. -+ * -+ * @param SearchResultInterface $searchResult -+ * @return SearchResultApplierInterface -+ */ -+ private function getSearchResultApplier(SearchResultInterface $searchResult): SearchResultApplierInterface -+ { -+ return $this->searchResultApplierFactory->create( -+ [ -+ 'collection' => $this, -+ 'searchResult' => $searchResult, -+ /** This variable sets by serOrder method, but doesn't have a getter method. */ -+ 'orders' => $this->_orders -+ ] -+ ); -+ } -+ -+ /** -+ * Get attribute code. -+ * - * @param string $attributeCode - * @return string - */ -@@ -264,6 +454,8 @@ class Collection extends \Magento\Catalog\Model\ResourceModel\Product\Collection - } - - /** -+ * Get search. -+ * - * @return \Magento\Search\Api\SearchInterface - */ - private function getSearch() -@@ -276,6 +468,8 @@ class Collection extends \Magento\Catalog\Model\ResourceModel\Product\Collection - } - - /** -+ * Get search criteria builder. -+ * - * @return SearchCriteriaBuilder - */ - private function getSearchCriteriaBuilder() -@@ -288,6 +482,8 @@ class Collection extends \Magento\Catalog\Model\ResourceModel\Product\Collection - } - - /** -+ * Get filter builder. -+ * - * @return FilterBuilder - */ - private function getFilterBuilder() -diff --git a/app/code/Magento/CatalogSearch/Model/ResourceModel/Engine.php b/app/code/Magento/CatalogSearch/Model/ResourceModel/Engine.php -index fac8c4d2a47..93ae2c94e21 100644 ---- a/app/code/Magento/CatalogSearch/Model/ResourceModel/Engine.php -+++ b/app/code/Magento/CatalogSearch/Model/ResourceModel/Engine.php -@@ -7,6 +7,9 @@ namespace Magento\CatalogSearch\Model\ResourceModel; - - /** - * CatalogSearch Fulltext Index Engine resource model -+ * -+ * @deprecated -+ * @see \Magento\ElasticSearch - */ - class Engine implements EngineInterface - { -@@ -107,7 +110,9 @@ class Engine implements EngineInterface - && in_array($attribute->getFrontendInput(), ['text', 'textarea']) - ) { - $result = $value; -- } elseif ($this->isTermFilterableAttribute($attribute)) { -+ } elseif ($this->isTermFilterableAttribute($attribute) -+ || ($attribute->getIsSearchable() && in_array($attribute->getFrontendInput(), ['select', 'multiselect'])) -+ ) { - $result = ''; - } - -@@ -116,6 +121,7 @@ class Engine implements EngineInterface - - /** - * Prepare index array as a string glued by separator -+ * - * Support 2 level array gluing - * - * @param array $index -diff --git a/app/code/Magento/CatalogSearch/Model/ResourceModel/EngineInterface.php b/app/code/Magento/CatalogSearch/Model/ResourceModel/EngineInterface.php -index 0182b09bcac..4b9db55105e 100644 ---- a/app/code/Magento/CatalogSearch/Model/ResourceModel/EngineInterface.php -+++ b/app/code/Magento/CatalogSearch/Model/ResourceModel/EngineInterface.php -@@ -3,13 +3,11 @@ - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -- --/** -- * CatalogSearch Index Engine Interface -- */ - namespace Magento\CatalogSearch\Model\ResourceModel; - - /** -+ * CatalogSearch Index Engine Interface -+ * - * @api - * @since 100.0.2 - */ -diff --git a/app/code/Magento/CatalogSearch/Model/ResourceModel/EngineProvider.php b/app/code/Magento/CatalogSearch/Model/ResourceModel/EngineProvider.php -index aa883aeb842..d1259159606 100644 ---- a/app/code/Magento/CatalogSearch/Model/ResourceModel/EngineProvider.php -+++ b/app/code/Magento/CatalogSearch/Model/ResourceModel/EngineProvider.php -@@ -4,15 +4,13 @@ - * See COPYING.txt for license details. - */ - --/** -- * Catalog Search engine provider -- */ - namespace Magento\CatalogSearch\Model\ResourceModel; - --use Magento\CatalogSearch\Model\ResourceModel\EngineInterface; - use Magento\Framework\Search\EngineResolverInterface; - - /** -+ * Catalog Search engine provider -+ * - * @api - * @since 100.0.2 - */ -diff --git a/app/code/Magento/CatalogSearch/Model/ResourceModel/Fulltext/Collection.php b/app/code/Magento/CatalogSearch/Model/ResourceModel/Fulltext/Collection.php -index 0408957e511..7e4b4f764f6 100644 ---- a/app/code/Magento/CatalogSearch/Model/ResourceModel/Fulltext/Collection.php -+++ b/app/code/Magento/CatalogSearch/Model/ResourceModel/Fulltext/Collection.php -@@ -3,13 +3,22 @@ - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -+ - namespace Magento\CatalogSearch\Model\ResourceModel\Fulltext; - --use Magento\CatalogSearch\Model\Search\RequestGenerator; -+use Magento\CatalogSearch\Model\ResourceModel\Fulltext\Collection\TotalRecordsResolverInterface; -+use Magento\CatalogSearch\Model\ResourceModel\Fulltext\Collection\TotalRecordsResolverFactory; -+use Magento\CatalogSearch\Model\ResourceModel\Fulltext\Collection\SearchCriteriaResolverInterface; -+use Magento\CatalogSearch\Model\ResourceModel\Fulltext\Collection\SearchCriteriaResolverFactory; -+use Magento\CatalogSearch\Model\ResourceModel\Fulltext\Collection\SearchResultApplierFactory; -+use Magento\CatalogSearch\Model\ResourceModel\Fulltext\Collection\SearchResultApplierInterface; -+use Magento\Framework\Search\EngineResolverInterface; -+use Magento\Framework\Data\Collection\Db\SizeResolverInterfaceFactory; - use Magento\Framework\DB\Select; -+use Magento\Framework\Api\Search\SearchResultInterface; -+use Magento\CatalogSearch\Model\Search\RequestGenerator; - use Magento\Framework\EntityManager\MetadataPool; - use Magento\Framework\Exception\StateException; --use Magento\Framework\Search\Adapter\Mysql\TemporaryStorage; - use Magento\Framework\Search\Response\QueryResponse; - use Magento\Framework\Search\Request\EmptyRequestDataException; - use Magento\Framework\Search\Request\NonExistingRequestNameException; -@@ -17,13 +26,18 @@ use Magento\Framework\Api\Search\SearchResultFactory; - use Magento\Framework\Exception\LocalizedException; - use Magento\Framework\App\ObjectManager; - use Magento\Catalog\Model\ResourceModel\Product\Collection\ProductLimitationFactory; -+use Magento\Search\Model\EngineResolver; - - /** - * Fulltext Collection -- * @SuppressWarnings(PHPMD.CouplingBetweenObjects) -+ * -+ * This collection should be refactored to not have dependencies on MySQL-specific implementation. - * - * @api - * @since 100.0.2 -+ * @SuppressWarnings(PHPMD.CouplingBetweenObjects) -+ * @SuppressWarnings(PHPMD.TooManyFields) -+ * @SuppressWarnings(PHPMD.CookieAndSessionMisuse) - */ - class Collection extends \Magento\Catalog\Model\ResourceModel\Product\Collection - { -@@ -58,11 +72,6 @@ class Collection extends \Magento\Catalog\Model\ResourceModel\Product\Collection - */ - private $queryText; - -- /** -- * @var string|null -- */ -- private $relevanceOrderDirection = null; -- - /** - * @var string - */ -@@ -70,6 +79,7 @@ class Collection extends \Magento\Catalog\Model\ResourceModel\Product\Collection - - /** - * @var \Magento\Framework\Search\Adapter\Mysql\TemporaryStorageFactory -+ * @deprecated There must be no dependencies on specific adapter in generic search implementation - */ - private $temporaryStorageFactory; - -@@ -98,6 +108,31 @@ class Collection extends \Magento\Catalog\Model\ResourceModel\Product\Collection - */ - private $filterBuilder; - -+ /** -+ * @var SearchCriteriaResolverFactory -+ */ -+ private $searchCriteriaResolverFactory; -+ -+ /** -+ * @var SearchResultApplierFactory -+ */ -+ private $searchResultApplierFactory; -+ -+ /** -+ * @var TotalRecordsResolverFactory -+ */ -+ private $totalRecordsResolverFactory; -+ -+ /** -+ * @var EngineResolverInterface -+ */ -+ private $engineResolver; -+ -+ /** -+ * @var array -+ */ -+ private $searchOrders; -+ - /** - * Collection constructor - * -@@ -111,7 +146,7 @@ class Collection extends \Magento\Catalog\Model\ResourceModel\Product\Collection - * @param \Magento\Catalog\Model\ResourceModel\Helper $resourceHelper - * @param \Magento\Framework\Validator\UniversalFactory $universalFactory - * @param \Magento\Store\Model\StoreManagerInterface $storeManager -- * @param \Magento\Framework\Module\Manager $moduleManager -+ * @param \Magento\Framework\Module\ModuleManagerInterface $moduleManager - * @param \Magento\Catalog\Model\Indexer\Product\Flat\State $catalogProductFlatState - * @param \Magento\Framework\App\Config\ScopeConfigInterface $scopeConfig - * @param \Magento\Catalog\Model\Product\OptionFactory $productOptionFactory -@@ -129,8 +164,15 @@ class Collection extends \Magento\Catalog\Model\ResourceModel\Product\Collection - * @param SearchResultFactory|null $searchResultFactory - * @param ProductLimitationFactory|null $productLimitationFactory - * @param MetadataPool|null $metadataPool -- * -+ * @param \Magento\Search\Api\SearchInterface|null $search -+ * @param \Magento\Framework\Api\Search\SearchCriteriaBuilder|null $searchCriteriaBuilder -+ * @param \Magento\Framework\Api\FilterBuilder|null $filterBuilder -+ * @param SearchCriteriaResolverFactory|null $searchCriteriaResolverFactory -+ * @param SearchResultApplierFactory|null $searchResultApplierFactory -+ * @param TotalRecordsResolverFactory|null $totalRecordsResolverFactory -+ * @param EngineResolverInterface|null $engineResolver - * @SuppressWarnings(PHPMD.ExcessiveParameterList) -+ * @SuppressWarnings(PHPMD.NPathComplexity) - */ - public function __construct( - \Magento\Framework\Data\Collection\EntityFactory $entityFactory, -@@ -143,7 +185,7 @@ class Collection extends \Magento\Catalog\Model\ResourceModel\Product\Collection - \Magento\Catalog\Model\ResourceModel\Helper $resourceHelper, - \Magento\Framework\Validator\UniversalFactory $universalFactory, - \Magento\Store\Model\StoreManagerInterface $storeManager, -- \Magento\Framework\Module\Manager $moduleManager, -+ \Magento\Framework\Module\ModuleManagerInterface $moduleManager, - \Magento\Catalog\Model\Indexer\Product\Flat\State $catalogProductFlatState, - \Magento\Framework\App\Config\ScopeConfigInterface $scopeConfig, - \Magento\Catalog\Model\Product\OptionFactory $productOptionFactory, -@@ -160,7 +202,14 @@ class Collection extends \Magento\Catalog\Model\ResourceModel\Product\Collection - $searchRequestName = 'catalog_view_container', - SearchResultFactory $searchResultFactory = null, - ProductLimitationFactory $productLimitationFactory = null, -- MetadataPool $metadataPool = null -+ MetadataPool $metadataPool = null, -+ \Magento\Search\Api\SearchInterface $search = null, -+ \Magento\Framework\Api\Search\SearchCriteriaBuilder $searchCriteriaBuilder = null, -+ \Magento\Framework\Api\FilterBuilder $filterBuilder = null, -+ SearchCriteriaResolverFactory $searchCriteriaResolverFactory = null, -+ SearchResultApplierFactory $searchResultApplierFactory = null, -+ TotalRecordsResolverFactory $totalRecordsResolverFactory = null, -+ EngineResolverInterface $engineResolver = null - ) { - $this->queryFactory = $catalogSearchData; - if ($searchResultFactory === null) { -@@ -195,9 +244,24 @@ class Collection extends \Magento\Catalog\Model\ResourceModel\Product\Collection - $this->searchEngine = $searchEngine; - $this->temporaryStorageFactory = $temporaryStorageFactory; - $this->searchRequestName = $searchRequestName; -+ $this->search = $search ?: ObjectManager::getInstance()->get(\Magento\Search\Api\SearchInterface::class); -+ $this->searchCriteriaBuilder = $searchCriteriaBuilder ?: ObjectManager::getInstance() -+ ->get(\Magento\Framework\Api\Search\SearchCriteriaBuilder::class); -+ $this->filterBuilder = $filterBuilder ?: ObjectManager::getInstance() -+ ->get(\Magento\Framework\Api\FilterBuilder::class); -+ $this->searchCriteriaResolverFactory = $searchCriteriaResolverFactory ?: ObjectManager::getInstance() -+ ->get(SearchCriteriaResolverFactory::class); -+ $this->searchResultApplierFactory = $searchResultApplierFactory ?: ObjectManager::getInstance() -+ ->get(SearchResultApplierFactory::class); -+ $this->totalRecordsResolverFactory = $totalRecordsResolverFactory ?: ObjectManager::getInstance() -+ ->get(TotalRecordsResolverFactory::class); -+ $this->engineResolver = $engineResolver ?: ObjectManager::getInstance() -+ ->get(EngineResolverInterface::class); - } - - /** -+ * Get search. -+ * - * @deprecated 100.1.0 - * @return \Magento\Search\Api\SearchInterface - */ -@@ -210,6 +274,8 @@ class Collection extends \Magento\Catalog\Model\ResourceModel\Product\Collection - } - - /** -+ * Test search. -+ * - * @deprecated 100.1.0 - * @param \Magento\Search\Api\SearchInterface $object - * @return void -@@ -221,6 +287,8 @@ class Collection extends \Magento\Catalog\Model\ResourceModel\Product\Collection - } - - /** -+ * Set search criteria builder. -+ * - * @deprecated 100.1.0 - * @return \Magento\Framework\Api\Search\SearchCriteriaBuilder - */ -@@ -234,6 +302,8 @@ class Collection extends \Magento\Catalog\Model\ResourceModel\Product\Collection - } - - /** -+ * Set search criteria builder. -+ * - * @deprecated 100.1.0 - * @param \Magento\Framework\Api\Search\SearchCriteriaBuilder $object - * @return void -@@ -245,6 +315,8 @@ class Collection extends \Magento\Catalog\Model\ResourceModel\Product\Collection - } - - /** -+ * Get filter builder. -+ * - * @deprecated 100.1.0 - * @return \Magento\Framework\Api\FilterBuilder - */ -@@ -257,6 +329,8 @@ class Collection extends \Magento\Catalog\Model\ResourceModel\Product\Collection - } - - /** -+ * Set filter builder. -+ * - * @deprecated 100.1.0 - * @param \Magento\Framework\Api\FilterBuilder $object - * @return void -@@ -271,7 +345,7 @@ class Collection extends \Magento\Catalog\Model\ResourceModel\Product\Collection - * Apply attribute filter to facet collection - * - * @param string $field -- * @param null $condition -+ * @param mixed|null $condition - * @return $this - */ - public function addFieldToFilter($field, $condition = null) -@@ -316,32 +390,32 @@ class Collection extends \Magento\Catalog\Model\ResourceModel\Product\Collection - /** - * @inheritdoc - */ -- protected function _renderFiltersBefore() -+ public function setOrder($attribute, $dir = Select::SQL_DESC) - { -- $this->getSearchCriteriaBuilder(); -- $this->getFilterBuilder(); -- $this->getSearch(); -- -- if ($this->queryText) { -- $this->filterBuilder->setField('search_term'); -- $this->filterBuilder->setValue($this->queryText); -- $this->searchCriteriaBuilder->addFilter($this->filterBuilder->create()); -+ $this->setSearchOrder($attribute, $dir); -+ if ($this->isCurrentEngineMysql()) { -+ parent::setOrder($attribute, $dir); - } - -- $priceRangeCalculation = $this->_scopeConfig->getValue( -- \Magento\Catalog\Model\Layer\Filter\Dynamic\AlgorithmFactory::XML_PATH_RANGE_CALCULATION, -- \Magento\Store\Model\ScopeInterface::SCOPE_STORE -- ); -- if ($priceRangeCalculation) { -- $this->filterBuilder->setField('price_dynamic_algorithm'); -- $this->filterBuilder->setValue($priceRangeCalculation); -- $this->searchCriteriaBuilder->addFilter($this->filterBuilder->create()); -+ return $this; -+ } -+ -+ /** -+ * @inheritdoc -+ */ -+ protected function _renderFiltersBefore() -+ { -+ if ($this->isLoaded()) { -+ return; - } - -- $searchCriteria = $this->searchCriteriaBuilder->create(); -- $searchCriteria->setRequestName($this->searchRequestName); -+ $this->prepareSearchTermFilter(); -+ $this->preparePriceAggregation(); -+ -+ $searchCriteria = $this->getSearchCriteriaResolver()->resolve(); - try { - $this->searchResult = $this->getSearch()->search($searchCriteria); -+ $this->_totalRecords = $this->getTotalRecordsResolver($this->searchResult)->resolve(); - } catch (EmptyRequestDataException $e) { - /** @var \Magento\Framework\Api\Search\SearchResultInterface $searchResult */ - $this->searchResult = $this->searchResultFactory->create()->setItems([]); -@@ -350,23 +424,85 @@ class Collection extends \Magento\Catalog\Model\ResourceModel\Product\Collection - throw new LocalizedException(__('An error occurred. For details, see the error log.')); - } - -- $temporaryStorage = $this->temporaryStorageFactory->create(); -- $table = $temporaryStorage->storeApiDocuments($this->searchResult->getItems()); -+ $this->getSearchResultApplier($this->searchResult)->apply(); -+ parent::_renderFiltersBefore(); -+ } -+ -+ /** -+ * Set sort order for search query. -+ * -+ * @param string $field -+ * @param string $direction -+ * @return void -+ */ -+ private function setSearchOrder($field, $direction) -+ { -+ $field = (string)$this->_getMappedField($field); -+ $direction = strtoupper($direction) == self::SORT_ORDER_ASC ? self::SORT_ORDER_ASC : self::SORT_ORDER_DESC; -+ -+ $this->searchOrders[$field] = $direction; -+ } - -- $this->getSelect()->joinInner( -+ /** -+ * Check if current engine is MYSQL. -+ * -+ * @return bool -+ */ -+ private function isCurrentEngineMysql() -+ { -+ return $this->engineResolver->getCurrentSearchEngine() === EngineResolver::CATALOG_SEARCH_MYSQL_ENGINE; -+ } -+ -+ /** -+ * Get total records resolver. -+ * -+ * @param SearchResultInterface $searchResult -+ * @return TotalRecordsResolverInterface -+ */ -+ private function getTotalRecordsResolver(SearchResultInterface $searchResult): TotalRecordsResolverInterface -+ { -+ return $this->totalRecordsResolverFactory->create( - [ -- 'search_result' => $table->getName(), -- ], -- 'e.entity_id = search_result.' . TemporaryStorage::FIELD_ENTITY_ID, -- [] -+ 'searchResult' => $searchResult, -+ ] - ); -+ } - -- if ($this->relevanceOrderDirection) { -- $this->getSelect()->order( -- 'search_result.'. TemporaryStorage::FIELD_SCORE . ' ' . $this->relevanceOrderDirection -- ); -- } -- return parent::_renderFiltersBefore(); -+ /** -+ * Get search criteria resolver. -+ * -+ * @return SearchCriteriaResolverInterface -+ */ -+ private function getSearchCriteriaResolver(): SearchCriteriaResolverInterface -+ { -+ return $this->searchCriteriaResolverFactory->create( -+ [ -+ 'builder' => $this->getSearchCriteriaBuilder(), -+ 'collection' => $this, -+ 'searchRequestName' => $this->searchRequestName, -+ 'currentPage' => $this->_curPage, -+ 'size' => $this->getPageSize(), -+ 'orders' => $this->searchOrders, -+ ] -+ ); -+ } -+ -+ /** -+ * Get search result applier. -+ * -+ * @param SearchResultInterface $searchResult -+ * @return SearchResultApplierInterface -+ */ -+ private function getSearchResultApplier(SearchResultInterface $searchResult): SearchResultApplierInterface -+ { -+ return $this->searchResultApplierFactory->create( -+ [ -+ 'collection' => $this, -+ 'searchResult' => $searchResult, -+ /** This variable sets by serOrder method, but doesn't have a getter method. */ -+ 'orders' => $this->_orders, -+ ] -+ ); - } - - /** -@@ -384,6 +520,8 @@ class Collection extends \Magento\Catalog\Model\ResourceModel\Product\Collection - } - - /** -+ * Render filters. -+ * - * @return $this - */ - protected function _renderFilters() -@@ -392,24 +530,6 @@ class Collection extends \Magento\Catalog\Model\ResourceModel\Product\Collection - return parent::_renderFilters(); - } - -- /** -- * Set Order field -- * -- * @param string $attribute -- * @param string $dir -- * @return $this -- */ -- public function setOrder($attribute, $dir = Select::SQL_DESC) -- { -- if ($attribute === 'relevance') { -- $this->relevanceOrderDirection = $dir; -- } else { -- parent::setOrder($attribute, $dir); -- } -- -- return $this; -- } -- - /** - * Stub method for compatibility with other search engines - * -@@ -456,7 +576,17 @@ class Collection extends \Magento\Catalog\Model\ResourceModel\Product\Collection - public function addCategoryFilter(\Magento\Catalog\Model\Category $category) - { - $this->addFieldToFilter('category_ids', $category->getId()); -- return parent::addCategoryFilter($category); -+ /** -+ * This changes need in backward compatible reasons for support dynamic improved algorithm -+ * for price aggregation process. -+ */ -+ if ($this->isCurrentEngineMysql()) { -+ parent::addCategoryFilter($category); -+ } else { -+ $this->_productLimitationPrice(); -+ } -+ -+ return $this; - } - - /** -@@ -468,6 +598,46 @@ class Collection extends \Magento\Catalog\Model\ResourceModel\Product\Collection - public function setVisibility($visibility) - { - $this->addFieldToFilter('visibility', $visibility); -- return parent::setVisibility($visibility); -+ /** -+ * This changes need in backward compatible reasons for support dynamic improved algorithm -+ * for price aggregation process. -+ */ -+ if ($this->isCurrentEngineMysql()) { -+ parent::setVisibility($visibility); -+ } -+ -+ return $this; -+ } -+ -+ /** -+ * Prepare search term filter for text query. -+ * -+ * @return void -+ */ -+ private function prepareSearchTermFilter(): void -+ { -+ if ($this->queryText) { -+ $this->filterBuilder->setField('search_term'); -+ $this->filterBuilder->setValue($this->queryText); -+ $this->searchCriteriaBuilder->addFilter($this->filterBuilder->create()); -+ } -+ } -+ -+ /** -+ * Prepare price aggregation algorithm. -+ * -+ * @return void -+ */ -+ private function preparePriceAggregation(): void -+ { -+ $priceRangeCalculation = $this->_scopeConfig->getValue( -+ \Magento\Catalog\Model\Layer\Filter\Dynamic\AlgorithmFactory::XML_PATH_RANGE_CALCULATION, -+ \Magento\Store\Model\ScopeInterface::SCOPE_STORE -+ ); -+ if ($priceRangeCalculation) { -+ $this->filterBuilder->setField('price_dynamic_algorithm'); -+ $this->filterBuilder->setValue($priceRangeCalculation); -+ $this->searchCriteriaBuilder->addFilter($this->filterBuilder->create()); -+ } - } - } -diff --git a/app/code/Magento/CatalogSearch/Model/ResourceModel/Fulltext/Collection/SearchCriteriaResolver.php b/app/code/Magento/CatalogSearch/Model/ResourceModel/Fulltext/Collection/SearchCriteriaResolver.php -new file mode 100644 -index 00000000000..632e1ab9270 ---- /dev/null -+++ b/app/code/Magento/CatalogSearch/Model/ResourceModel/Fulltext/Collection/SearchCriteriaResolver.php -@@ -0,0 +1,51 @@ -+<?php -+/** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+ -+namespace Magento\CatalogSearch\Model\ResourceModel\Fulltext\Collection; -+ -+use Magento\Framework\Data\Collection; -+use Magento\Framework\Api\Search\SearchCriteriaBuilder; -+use Magento\Framework\Api\Search\SearchCriteria; -+ -+/** -+ * Resolve specific attributes for search criteria. -+ */ -+class SearchCriteriaResolver implements SearchCriteriaResolverInterface -+{ -+ /** -+ * @var SearchCriteriaBuilder -+ */ -+ private $builder; -+ -+ /** -+ * @var string -+ */ -+ private $searchRequestName; -+ -+ /** -+ * SearchCriteriaResolver constructor. -+ * @param SearchCriteriaBuilder $builder -+ * @param string $searchRequestName -+ */ -+ public function __construct( -+ SearchCriteriaBuilder $builder, -+ string $searchRequestName -+ ) { -+ $this->builder = $builder; -+ $this->searchRequestName = $searchRequestName; -+ } -+ -+ /** -+ * @inheritdoc -+ */ -+ public function resolve() : SearchCriteria -+ { -+ $searchCriteria = $this->builder->create(); -+ $searchCriteria->setRequestName($this->searchRequestName); -+ -+ return $searchCriteria; -+ } -+} -diff --git a/app/code/Magento/CatalogSearch/Model/ResourceModel/Fulltext/Collection/SearchCriteriaResolverInterface.php b/app/code/Magento/CatalogSearch/Model/ResourceModel/Fulltext/Collection/SearchCriteriaResolverInterface.php -new file mode 100644 -index 00000000000..047fa7f71e4 ---- /dev/null -+++ b/app/code/Magento/CatalogSearch/Model/ResourceModel/Fulltext/Collection/SearchCriteriaResolverInterface.php -@@ -0,0 +1,22 @@ -+<?php -+/** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+ -+namespace Magento\CatalogSearch\Model\ResourceModel\Fulltext\Collection; -+ -+use Magento\Framework\Api\Search\SearchCriteria; -+ -+/** -+ * Resolve specific attributes for search criteria. -+ */ -+interface SearchCriteriaResolverInterface -+{ -+ /** -+ * Resolve specific attribute. -+ * -+ * @return SearchCriteria -+ */ -+ public function resolve(): SearchCriteria; -+} -diff --git a/app/code/Magento/CatalogSearch/Model/ResourceModel/Fulltext/Collection/SearchResultApplier.php b/app/code/Magento/CatalogSearch/Model/ResourceModel/Fulltext/Collection/SearchResultApplier.php -new file mode 100644 -index 00000000000..20c237646e5 ---- /dev/null -+++ b/app/code/Magento/CatalogSearch/Model/ResourceModel/Fulltext/Collection/SearchResultApplier.php -@@ -0,0 +1,79 @@ -+<?php -+/** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+ -+namespace Magento\CatalogSearch\Model\ResourceModel\Fulltext\Collection; -+ -+use Magento\Framework\Data\Collection; -+use Magento\Framework\Search\Adapter\Mysql\TemporaryStorage; -+use Magento\Framework\Search\Adapter\Mysql\TemporaryStorageFactory; -+use Magento\Framework\Api\Search\SearchResultInterface; -+ -+/** -+ * Resolve specific attributes for search criteria. -+ */ -+class SearchResultApplier implements SearchResultApplierInterface -+{ -+ /** -+ * @var Collection -+ */ -+ private $collection; -+ -+ /** -+ * @var SearchResultInterface -+ */ -+ private $searchResult; -+ -+ /** -+ * @var TemporaryStorageFactory -+ */ -+ private $temporaryStorageFactory; -+ -+ /** -+ * @var array -+ */ -+ private $orders; -+ -+ /** -+ * @param Collection $collection -+ * @param SearchResultInterface $searchResult -+ * @param TemporaryStorageFactory $temporaryStorageFactory -+ * @param array $orders -+ */ -+ public function __construct( -+ Collection $collection, -+ SearchResultInterface $searchResult, -+ TemporaryStorageFactory $temporaryStorageFactory, -+ array $orders -+ ) { -+ $this->collection = $collection; -+ $this->searchResult = $searchResult; -+ $this->temporaryStorageFactory = $temporaryStorageFactory; -+ $this->orders = $orders; -+ } -+ -+ /** -+ * @inheritdoc -+ */ -+ public function apply() -+ { -+ $temporaryStorage = $this->temporaryStorageFactory->create(); -+ $table = $temporaryStorage->storeApiDocuments($this->searchResult->getItems()); -+ -+ $this->collection->getSelect()->joinInner( -+ [ -+ 'search_result' => $table->getName(), -+ ], -+ 'e.entity_id = search_result.' . TemporaryStorage::FIELD_ENTITY_ID, -+ [] -+ ); -+ -+ if (isset($this->orders['relevance'])) { -+ $this->collection->getSelect()->order( -+ 'search_result.' . TemporaryStorage::FIELD_SCORE . ' ' . $this->orders['relevance'] -+ ); -+ } -+ } -+} -diff --git a/app/code/Magento/CatalogSearch/Model/ResourceModel/Fulltext/Collection/SearchResultApplierInterface.php b/app/code/Magento/CatalogSearch/Model/ResourceModel/Fulltext/Collection/SearchResultApplierInterface.php -new file mode 100644 -index 00000000000..1b3e2a6bbac ---- /dev/null -+++ b/app/code/Magento/CatalogSearch/Model/ResourceModel/Fulltext/Collection/SearchResultApplierInterface.php -@@ -0,0 +1,20 @@ -+<?php -+/** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+ -+namespace Magento\CatalogSearch\Model\ResourceModel\Fulltext\Collection; -+ -+/** -+ * Resolve specific attributes for search criteria. -+ */ -+interface SearchResultApplierInterface -+{ -+ /** -+ * Apply search results to collection. -+ * -+ * @return void -+ */ -+ public function apply(); -+} -diff --git a/app/code/Magento/CatalogSearch/Model/ResourceModel/Fulltext/Collection/TotalRecordsResolver.php b/app/code/Magento/CatalogSearch/Model/ResourceModel/Fulltext/Collection/TotalRecordsResolver.php -new file mode 100644 -index 00000000000..12b6f813139 ---- /dev/null -+++ b/app/code/Magento/CatalogSearch/Model/ResourceModel/Fulltext/Collection/TotalRecordsResolver.php -@@ -0,0 +1,23 @@ -+<?php -+/** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+ -+namespace Magento\CatalogSearch\Model\ResourceModel\Fulltext\Collection; -+ -+/** -+ * Resolve total records count. -+ * -+ * For Mysql search engine we can't resolve total record count before full load of collection. -+ */ -+class TotalRecordsResolver implements TotalRecordsResolverInterface -+{ -+ /** -+ * @inheritdoc -+ */ -+ public function resolve(): ?int -+ { -+ return null; -+ } -+} -diff --git a/app/code/Magento/CatalogSearch/Model/ResourceModel/Fulltext/Collection/TotalRecordsResolverInterface.php b/app/code/Magento/CatalogSearch/Model/ResourceModel/Fulltext/Collection/TotalRecordsResolverInterface.php -new file mode 100644 -index 00000000000..190450f9606 ---- /dev/null -+++ b/app/code/Magento/CatalogSearch/Model/ResourceModel/Fulltext/Collection/TotalRecordsResolverInterface.php -@@ -0,0 +1,20 @@ -+<?php -+/** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+ -+namespace Magento\CatalogSearch\Model\ResourceModel\Fulltext\Collection; -+ -+/** -+ * Resolve total records count. -+ */ -+interface TotalRecordsResolverInterface -+{ -+ /** -+ * Resolve total records. -+ * -+ * @return int -+ */ -+ public function resolve(): ?int; -+} -diff --git a/app/code/Magento/CatalogSearch/Model/ResourceModel/Search/Collection.php b/app/code/Magento/CatalogSearch/Model/ResourceModel/Search/Collection.php -index b958de91314..6cdcc7c55a2 100644 ---- a/app/code/Magento/CatalogSearch/Model/ResourceModel/Search/Collection.php -+++ b/app/code/Magento/CatalogSearch/Model/ResourceModel/Search/Collection.php -@@ -10,6 +10,7 @@ namespace Magento\CatalogSearch\Model\ResourceModel\Search; - * Search collection - * - * @SuppressWarnings(PHPMD.CouplingBetweenObjects) -+ * @SuppressWarnings(PHPMD.CookieAndSessionMisuse) - * @api - * @since 100.0.2 - */ -@@ -49,7 +50,7 @@ class Collection extends \Magento\Catalog\Model\ResourceModel\Product\Collection - * @param \Magento\Catalog\Model\ResourceModel\Helper $resourceHelper - * @param \Magento\Framework\Validator\UniversalFactory $universalFactory - * @param \Magento\Store\Model\StoreManagerInterface $storeManager -- * @param \Magento\Framework\Module\Manager $moduleManager -+ * @param \Magento\Framework\Module\ModuleManagerInterface $moduleManager - * @param \Magento\Catalog\Model\Indexer\Product\Flat\State $catalogProductFlatState - * @param \Magento\Framework\App\Config\ScopeConfigInterface $scopeConfig - * @param \Magento\Catalog\Model\Product\OptionFactory $productOptionFactory -@@ -60,7 +61,6 @@ class Collection extends \Magento\Catalog\Model\ResourceModel\Product\Collection - * @param \Magento\Customer\Api\GroupManagementInterface $groupManagement - * @param \Magento\Catalog\Model\ResourceModel\Product\Attribute\CollectionFactory $attributeCollectionFactory - * @param \Magento\Framework\DB\Adapter\AdapterInterface $connection -- * - * @SuppressWarnings(PHPMD.ExcessiveParameterList) - */ - public function __construct( -@@ -74,7 +74,7 @@ class Collection extends \Magento\Catalog\Model\ResourceModel\Product\Collection - \Magento\Catalog\Model\ResourceModel\Helper $resourceHelper, - \Magento\Framework\Validator\UniversalFactory $universalFactory, - \Magento\Store\Model\StoreManagerInterface $storeManager, -- \Magento\Framework\Module\Manager $moduleManager, -+ \Magento\Framework\Module\ModuleManagerInterface $moduleManager, - \Magento\Catalog\Model\Indexer\Product\Flat\State $catalogProductFlatState, - \Magento\Framework\App\Config\ScopeConfigInterface $scopeConfig, - \Magento\Catalog\Model\Product\OptionFactory $productOptionFactory, -@@ -269,6 +269,7 @@ class Collection extends \Magento\Catalog\Model\ResourceModel\Product\Collection - - $sql = $this->_getSearchInOptionSql($query); - if ($sql) { -+ // phpcs:ignore Magento2.SQL.RawQuery - $selects[] = "SELECT * FROM ({$sql}) AS inoptionsql"; // inherent unions may be inside - } - -diff --git a/app/code/Magento/CatalogSearch/Model/ResourceModel/Setup/PropertyMapper.php b/app/code/Magento/CatalogSearch/Model/ResourceModel/Setup/PropertyMapper.php -new file mode 100644 -index 00000000000..f72e1536f47 ---- /dev/null -+++ b/app/code/Magento/CatalogSearch/Model/ResourceModel/Setup/PropertyMapper.php -@@ -0,0 +1,34 @@ -+<?php -+declare(strict_types=1); -+ -+/** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+ -+namespace Magento\CatalogSearch\Model\ResourceModel\Setup; -+ -+use Magento\Eav\Model\Entity\Setup\PropertyMapperAbstract; -+ -+/** -+ * Class PropertyMapper -+ * -+ * @package Magento\CatalogSearch\Model\ResourceModel\Setup -+ */ -+class PropertyMapper extends PropertyMapperAbstract -+{ -+ /** -+ * Map input attribute properties to storage representation -+ * -+ * @param array $input -+ * @param int $entityTypeId -+ * @return array -+ * @SuppressWarnings(PHPMD.UnusedFormalParameter) -+ */ -+ public function map(array $input, $entityTypeId): array -+ { -+ return [ -+ 'search_weight' => $this->_getValue($input, 'search_weight', 1), -+ ]; -+ } -+} -diff --git a/app/code/Magento/CatalogSearch/Model/Search/BaseSelectStrategy/BaseSelectStrategyInterface.php b/app/code/Magento/CatalogSearch/Model/Search/BaseSelectStrategy/BaseSelectStrategyInterface.php -index c6495015fee..2d8dfb92224 100644 ---- a/app/code/Magento/CatalogSearch/Model/Search/BaseSelectStrategy/BaseSelectStrategyInterface.php -+++ b/app/code/Magento/CatalogSearch/Model/Search/BaseSelectStrategy/BaseSelectStrategyInterface.php -@@ -8,8 +8,10 @@ namespace Magento\CatalogSearch\Model\Search\BaseSelectStrategy; - use Magento\CatalogSearch\Model\Search\SelectContainer\SelectContainer; - - /** -- * Interface BaseSelectStrategyInterface - * This interface represents strategy that will be used to create base select for search request -+ * -+ * @deprecated -+ * @see \Magento\ElasticSearch - */ - interface BaseSelectStrategyInterface - { -diff --git a/app/code/Magento/CatalogSearch/Model/Search/BaseSelectStrategy/StrategyMapper.php b/app/code/Magento/CatalogSearch/Model/Search/BaseSelectStrategy/StrategyMapper.php -index 65759685c2b..e554d3c774a 100644 ---- a/app/code/Magento/CatalogSearch/Model/Search/BaseSelectStrategy/StrategyMapper.php -+++ b/app/code/Magento/CatalogSearch/Model/Search/BaseSelectStrategy/StrategyMapper.php -@@ -10,8 +10,10 @@ use Magento\CatalogSearch\Model\Adapter\Mysql\BaseSelectStrategy\BaseSelectFullT - use Magento\CatalogSearch\Model\Adapter\Mysql\BaseSelectStrategy\BaseSelectAttributesSearchStrategy; - - /** -- * Class StrategyMapper - * This class is responsible for deciding which BaseSelectStrategyInterface should be used for passed SelectContainer -+ * -+ * @deprecated -+ * @see \Magento\ElasticSearch - */ - class StrategyMapper - { -diff --git a/app/code/Magento/CatalogSearch/Model/Search/Catalog.php b/app/code/Magento/CatalogSearch/Model/Search/Catalog.php -index 4572336d761..31ac889b19e 100644 ---- a/app/code/Magento/CatalogSearch/Model/Search/Catalog.php -+++ b/app/code/Magento/CatalogSearch/Model/Search/Catalog.php -@@ -9,8 +9,6 @@ use Magento\Search\Model\QueryFactory; - - /** - * Search model for backend search -- * -- * @deprecated 100.2.0 - */ - class Catalog extends \Magento\Framework\DataObject - { -diff --git a/app/code/Magento/CatalogSearch/Model/Search/CustomAttributeFilterCheck.php b/app/code/Magento/CatalogSearch/Model/Search/CustomAttributeFilterCheck.php -index 98bf1e5984a..657c8540d7c 100644 ---- a/app/code/Magento/CatalogSearch/Model/Search/CustomAttributeFilterCheck.php -+++ b/app/code/Magento/CatalogSearch/Model/Search/CustomAttributeFilterCheck.php -@@ -10,8 +10,10 @@ use Magento\Eav\Model\Config as EavConfig; - use Magento\Catalog\Model\Product; - - /** -- * Class CustomAttributeFilterSelector - * Checks if FilterInterface is by custom attribute -+ * -+ * @deprecated -+ * @see \Magento\ElasticSearch - */ - class CustomAttributeFilterCheck - { -@@ -42,7 +44,7 @@ class CustomAttributeFilterCheck - - return $attribute - && $filter->getType() === FilterInterface::TYPE_TERM -- && in_array($attribute->getFrontendInput(), ['select', 'multiselect'], true); -+ && in_array($attribute->getFrontendInput(), ['select', 'multiselect', 'boolean'], true); - } - - /** -diff --git a/app/code/Magento/CatalogSearch/Model/Search/FilterMapper/CustomAttributeFilter.php b/app/code/Magento/CatalogSearch/Model/Search/FilterMapper/CustomAttributeFilter.php -index fc93b86f5da..8c796f87706 100644 ---- a/app/code/Magento/CatalogSearch/Model/Search/FilterMapper/CustomAttributeFilter.php -+++ b/app/code/Magento/CatalogSearch/Model/Search/FilterMapper/CustomAttributeFilter.php -@@ -16,8 +16,10 @@ use Magento\CatalogSearch\Model\Adapter\Mysql\Filter\AliasResolver; - use Magento\Catalog\Model\Product; - - /** -- * Class CustomAttributeFilter -- * Applies filters by custom attributes to base select -+ * Applies filters by custom attributes to base select. -+ * -+ * @deprecated -+ * @see \Magento\ElasticSearch - */ - class CustomAttributeFilter - { -@@ -71,13 +73,13 @@ class CustomAttributeFilter - * Applies filters by custom attributes to base select - * - * @param Select $select -- * @param FilterInterface[] ...$filters -+ * @param FilterInterface[] $filters - * @return Select - * @throws \Magento\Framework\Exception\LocalizedException - * @throws \InvalidArgumentException - * @throws \DomainException - */ -- public function apply(Select $select, FilterInterface ... $filters) -+ public function apply(Select $select, FilterInterface ...$filters) - { - $select = clone $select; - $mainTableAlias = $this->extractTableAliasFromSelect($select); -@@ -141,7 +143,6 @@ class CustomAttributeFilter - { - return [ - sprintf('`%s`.`entity_id` = `%s`.`entity_id`', $mainTable, $joinTable), -- sprintf('`%s`.`source_id` = `%s`.`source_id`', $mainTable, $joinTable), - $this->conditionManager->generateCondition( - sprintf('%s.attribute_id', $joinTable), - '=', -diff --git a/app/code/Magento/CatalogSearch/Model/Search/FilterMapper/DimensionsProcessor.php b/app/code/Magento/CatalogSearch/Model/Search/FilterMapper/DimensionsProcessor.php -index 67f427d4be4..3d2b9eed037 100644 ---- a/app/code/Magento/CatalogSearch/Model/Search/FilterMapper/DimensionsProcessor.php -+++ b/app/code/Magento/CatalogSearch/Model/Search/FilterMapper/DimensionsProcessor.php -@@ -16,6 +16,9 @@ use Magento\CatalogSearch\Model\Search\SelectContainer\SelectContainer; - /** - * Class DimensionsProcessor - * Adds dimension conditions to select query -+ * -+ * @deprecated -+ * @see \Magento\ElasticSearch - */ - class DimensionsProcessor - { -diff --git a/app/code/Magento/CatalogSearch/Model/Search/FilterMapper/ExclusionStrategy.php b/app/code/Magento/CatalogSearch/Model/Search/FilterMapper/ExclusionStrategy.php -index 512dd69aad9..c382569338e 100644 ---- a/app/code/Magento/CatalogSearch/Model/Search/FilterMapper/ExclusionStrategy.php -+++ b/app/code/Magento/CatalogSearch/Model/Search/FilterMapper/ExclusionStrategy.php -@@ -21,6 +21,9 @@ use Magento\Store\Model\Indexer\WebsiteDimensionProvider; - /** - * Strategy which processes exclusions from general rules - * -+ * @deprecated -+ * @see \Magento\ElasticSearch -+ * - * @SuppressWarnings(PHPMD.CouplingBetweenObjects) - */ - class ExclusionStrategy implements FilterStrategyInterface -diff --git a/app/code/Magento/CatalogSearch/Model/Search/FilterMapper/FilterContext.php b/app/code/Magento/CatalogSearch/Model/Search/FilterMapper/FilterContext.php -index 0c823331633..67ed66da2a0 100644 ---- a/app/code/Magento/CatalogSearch/Model/Search/FilterMapper/FilterContext.php -+++ b/app/code/Magento/CatalogSearch/Model/Search/FilterMapper/FilterContext.php -@@ -13,7 +13,10 @@ use Magento\Eav\Model\Entity\Attribute\AbstractAttribute; - /** - * FilterContext represents a Context of the Strategy pattern - * Its responsibility is to choose appropriate strategy to apply passed filter to the Select -+ * - * @SuppressWarnings(PHPMD.CouplingBetweenObjects) -+ * @deprecated -+ * @see \Magento\ElasticSearch - */ - class FilterContext implements FilterStrategyInterface - { -diff --git a/app/code/Magento/CatalogSearch/Model/Search/FilterMapper/FilterMapper.php b/app/code/Magento/CatalogSearch/Model/Search/FilterMapper/FilterMapper.php -index 27128327554..7136fad5b19 100644 ---- a/app/code/Magento/CatalogSearch/Model/Search/FilterMapper/FilterMapper.php -+++ b/app/code/Magento/CatalogSearch/Model/Search/FilterMapper/FilterMapper.php -@@ -13,6 +13,9 @@ use Magento\CatalogInventory\Model\Stock; - /** - * Class FilterMapper - * This class applies filters to Select based on SelectContainer configuration -+ * -+ * @deprecated -+ * @see \Magento\ElasticSearch - */ - class FilterMapper - { -diff --git a/app/code/Magento/CatalogSearch/Model/Search/FilterMapper/FilterStrategyInterface.php b/app/code/Magento/CatalogSearch/Model/Search/FilterMapper/FilterStrategyInterface.php -index b59f7eeec97..a61c691c0d5 100644 ---- a/app/code/Magento/CatalogSearch/Model/Search/FilterMapper/FilterStrategyInterface.php -+++ b/app/code/Magento/CatalogSearch/Model/Search/FilterMapper/FilterStrategyInterface.php -@@ -10,6 +10,8 @@ namespace Magento\CatalogSearch\Model\Search\FilterMapper; - * FilterStrategyInterface provides the interface to work with strategies - * @api - * @since 100.1.6 -+ * @deprecated -+ * @see \Magento\ElasticSearch - */ - interface FilterStrategyInterface - { -diff --git a/app/code/Magento/CatalogSearch/Model/Search/FilterMapper/StaticAttributeStrategy.php b/app/code/Magento/CatalogSearch/Model/Search/FilterMapper/StaticAttributeStrategy.php -index 8544b463dbb..3986cc617f0 100644 ---- a/app/code/Magento/CatalogSearch/Model/Search/FilterMapper/StaticAttributeStrategy.php -+++ b/app/code/Magento/CatalogSearch/Model/Search/FilterMapper/StaticAttributeStrategy.php -@@ -12,6 +12,9 @@ use Magento\Framework\DB\Select; - - /** - * This strategy handles static attributes -+ * -+ * @deprecated -+ * @see \Magento\ElasticSearch - */ - class StaticAttributeStrategy implements FilterStrategyInterface - { -diff --git a/app/code/Magento/CatalogSearch/Model/Search/FilterMapper/StockStatusFilter.php b/app/code/Magento/CatalogSearch/Model/Search/FilterMapper/StockStatusFilter.php -index 1cd9d1a3dd7..0e3ba0d4e66 100644 ---- a/app/code/Magento/CatalogSearch/Model/Search/FilterMapper/StockStatusFilter.php -+++ b/app/code/Magento/CatalogSearch/Model/Search/FilterMapper/StockStatusFilter.php -@@ -15,6 +15,9 @@ use Magento\CatalogInventory\Api\StockRegistryInterface; - /** - * Class StockStatusFilter - * Adds filter by stock status to base select -+ * -+ * @deprecated -+ * @see \Magento\ElasticSearch - */ - class StockStatusFilter - { -diff --git a/app/code/Magento/CatalogSearch/Model/Search/FilterMapper/TermDropdownStrategy.php b/app/code/Magento/CatalogSearch/Model/Search/FilterMapper/TermDropdownStrategy.php -index 6b5dce5fde4..9d7e31ee3b6 100644 ---- a/app/code/Magento/CatalogSearch/Model/Search/FilterMapper/TermDropdownStrategy.php -+++ b/app/code/Magento/CatalogSearch/Model/Search/FilterMapper/TermDropdownStrategy.php -@@ -15,6 +15,9 @@ use Magento\Framework\App\ObjectManager; - * This strategy handles attributes which comply with two criteria: - * - The filter for dropdown or multi-select attribute - * - The filter is Term filter -+ * -+ * @deprecated -+ * @see \Magento\ElasticSearch - */ - class TermDropdownStrategy implements FilterStrategyInterface - { -diff --git a/app/code/Magento/CatalogSearch/Model/Search/FilterMapper/TermDropdownStrategy/ApplyStockConditionToSelect.php b/app/code/Magento/CatalogSearch/Model/Search/FilterMapper/TermDropdownStrategy/ApplyStockConditionToSelect.php -index dee8b09a051..c28bc3485cf 100644 ---- a/app/code/Magento/CatalogSearch/Model/Search/FilterMapper/TermDropdownStrategy/ApplyStockConditionToSelect.php -+++ b/app/code/Magento/CatalogSearch/Model/Search/FilterMapper/TermDropdownStrategy/ApplyStockConditionToSelect.php -@@ -13,6 +13,9 @@ use Magento\CatalogInventory\Model\Stock\Status; - - /** - * Apply stock condition to select. -+ * -+ * @deprecated -+ * @see \Magento\ElasticSearch - */ - class ApplyStockConditionToSelect - { -diff --git a/app/code/Magento/CatalogSearch/Model/Search/FilterMapper/TermDropdownStrategy/SelectBuilder.php b/app/code/Magento/CatalogSearch/Model/Search/FilterMapper/TermDropdownStrategy/SelectBuilder.php -index fccbfa364d8..007647db39b 100644 ---- a/app/code/Magento/CatalogSearch/Model/Search/FilterMapper/TermDropdownStrategy/SelectBuilder.php -+++ b/app/code/Magento/CatalogSearch/Model/Search/FilterMapper/TermDropdownStrategy/SelectBuilder.php -@@ -16,6 +16,9 @@ use Magento\Store\Model\StoreManagerInterface; - - /** - * Add joins to select. -+ * -+ * @deprecated -+ * @see \Magento\ElasticSearch - */ - class SelectBuilder - { -diff --git a/app/code/Magento/CatalogSearch/Model/Search/FilterMapper/VisibilityFilter.php b/app/code/Magento/CatalogSearch/Model/Search/FilterMapper/VisibilityFilter.php -index a615d76bef4..690ef9115ed 100644 ---- a/app/code/Magento/CatalogSearch/Model/Search/FilterMapper/VisibilityFilter.php -+++ b/app/code/Magento/CatalogSearch/Model/Search/FilterMapper/VisibilityFilter.php -@@ -16,6 +16,9 @@ use Magento\Eav\Model\Config as EavConfig; - /** - * Class VisibilityFilter - * Applies filter by visibility to base select -+ * -+ * @deprecated -+ * @see \Magento\ElasticSearch - */ - class VisibilityFilter - { -diff --git a/app/code/Magento/CatalogSearch/Model/Search/FiltersExtractor.php b/app/code/Magento/CatalogSearch/Model/Search/FiltersExtractor.php -index 53c27eb66f6..55c85829799 100644 ---- a/app/code/Magento/CatalogSearch/Model/Search/FiltersExtractor.php -+++ b/app/code/Magento/CatalogSearch/Model/Search/FiltersExtractor.php -@@ -12,6 +12,9 @@ use Magento\Framework\Search\Request\Filter\BoolExpression; - /** - * Class FiltersExtractor - * Extracts filters from QueryInterface -+ * -+ * @deprecated -+ * @see \Magento\ElasticSearch - */ - class FiltersExtractor - { -diff --git a/app/code/Magento/CatalogSearch/Model/Search/IndexBuilder.php b/app/code/Magento/CatalogSearch/Model/Search/IndexBuilder.php -index 890a0d40001..906220db28d 100644 ---- a/app/code/Magento/CatalogSearch/Model/Search/IndexBuilder.php -+++ b/app/code/Magento/CatalogSearch/Model/Search/IndexBuilder.php -@@ -26,6 +26,8 @@ use Magento\CatalogSearch\Model\Search\FilterMapper\FilterMapper; - /** - * Build base Query for Index - * @SuppressWarnings(PHPMD.CouplingBetweenObjects) -+ * @deprecated -+ * @see \Magento\ElasticSearch - */ - class IndexBuilder implements IndexBuilderInterface - { -diff --git a/app/code/Magento/CatalogSearch/Model/Search/ItemCollectionProvider.php b/app/code/Magento/CatalogSearch/Model/Search/ItemCollectionProvider.php -new file mode 100644 -index 00000000000..f621bcbf918 ---- /dev/null -+++ b/app/code/Magento/CatalogSearch/Model/Search/ItemCollectionProvider.php -@@ -0,0 +1,50 @@ -+<?php -+/** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+ -+namespace Magento\CatalogSearch\Model\Search; -+ -+use Magento\Framework\Search\EngineResolverInterface; -+use Magento\Framework\Data\Collection; -+ -+/** -+ * Search collection provider. -+ */ -+class ItemCollectionProvider implements ItemCollectionProviderInterface -+{ -+ /** -+ * @var EngineResolverInterface -+ */ -+ private $engineResolver; -+ -+ /** -+ * @var array -+ */ -+ private $factories; -+ -+ /** -+ * ItemCollectionProvider constructor. -+ * @param EngineResolverInterface $engineResolver -+ * @param array $factories -+ */ -+ public function __construct( -+ EngineResolverInterface $engineResolver, -+ array $factories -+ ) { -+ $this->engineResolver = $engineResolver; -+ $this->factories = $factories; -+ } -+ -+ /** -+ * @inheritdoc -+ */ -+ public function getCollection(): Collection -+ { -+ if (!isset($this->factories[$this->engineResolver->getCurrentSearchEngine()])) { -+ throw new \DomainException('Undefined factory ' . $this->engineResolver->getCurrentSearchEngine()); -+ } -+ return $this->factories[$this->engineResolver->getCurrentSearchEngine()]->create(); -+ } -+} -diff --git a/app/code/Magento/CatalogSearch/Model/Search/ItemCollectionProviderInterface.php b/app/code/Magento/CatalogSearch/Model/Search/ItemCollectionProviderInterface.php -new file mode 100644 -index 00000000000..db02d5ac5f5 ---- /dev/null -+++ b/app/code/Magento/CatalogSearch/Model/Search/ItemCollectionProviderInterface.php -@@ -0,0 +1,23 @@ -+<?php -+/** -+ * -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+ -+namespace Magento\CatalogSearch\Model\Search; -+ -+use Magento\Framework\Data\Collection; -+ -+/** -+ * Search collection provider. -+ */ -+interface ItemCollectionProviderInterface -+{ -+ /** -+ * Get collection. -+ * -+ * @return Collection -+ */ -+ public function getCollection() : Collection; -+} -diff --git a/app/code/Magento/CatalogSearch/Model/Search/QueryChecker/FullTextSearchCheck.php b/app/code/Magento/CatalogSearch/Model/Search/QueryChecker/FullTextSearchCheck.php -index f6c2ac0a4f5..a47ea543752 100644 ---- a/app/code/Magento/CatalogSearch/Model/Search/QueryChecker/FullTextSearchCheck.php -+++ b/app/code/Magento/CatalogSearch/Model/Search/QueryChecker/FullTextSearchCheck.php -@@ -12,6 +12,9 @@ use Magento\Framework\Search\Request\Query\Filter; - - /** - * Class is responsible for checking if fulltext search is required for search query -+ * -+ * @deprecated -+ * @see \Magento\ElasticSearch - */ - class FullTextSearchCheck - { -@@ -22,7 +25,7 @@ class FullTextSearchCheck - * to join catalog_eav_attribute table to search query or not - * - * In case when the $query does not requires full text search -- * - we can skipp joining catalog_eav_attribute table because it becomes excessive -+ * - we can skip joining catalog_eav_attribute table because it becomes excessive - * - * @param QueryInterface $query - * @return bool -@@ -34,6 +37,8 @@ class FullTextSearchCheck - } - - /** -+ * Process query -+ * - * @param QueryInterface $query - * @return bool - * @throws \InvalidArgumentException -@@ -59,6 +64,8 @@ class FullTextSearchCheck - } - - /** -+ * Process boolean query -+ * - * @param BoolExpression $query - * @return bool - * @throws \InvalidArgumentException -@@ -87,6 +94,8 @@ class FullTextSearchCheck - } - - /** -+ * Process filter query -+ * - * @param Filter $query - * @return bool - * @throws \InvalidArgumentException -diff --git a/app/code/Magento/CatalogSearch/Model/Search/ReaderPlugin.php b/app/code/Magento/CatalogSearch/Model/Search/ReaderPlugin.php -index 8ddc8408959..916e03f4714 100644 ---- a/app/code/Magento/CatalogSearch/Model/Search/ReaderPlugin.php -+++ b/app/code/Magento/CatalogSearch/Model/Search/ReaderPlugin.php -@@ -5,6 +5,10 @@ - */ - namespace Magento\CatalogSearch\Model\Search; - -+/** -+ * @deprecated -+ * @see \Magento\ElasticSearch -+ */ - class ReaderPlugin - { - /** -diff --git a/app/code/Magento/CatalogSearch/Model/Search/RequestGenerator.php b/app/code/Magento/CatalogSearch/Model/Search/RequestGenerator.php -index 6f6a5eed642..8f8ba39ebd3 100644 ---- a/app/code/Magento/CatalogSearch/Model/Search/RequestGenerator.php -+++ b/app/code/Magento/CatalogSearch/Model/Search/RequestGenerator.php -@@ -14,6 +14,8 @@ use Magento\Framework\Search\Request\FilterInterface; - use Magento\Framework\Search\Request\QueryInterface; - - /** -+ * Catalog search request generator. -+ * - * @api - * @since 100.0.2 - */ -@@ -101,7 +103,7 @@ class RequestGenerator - } - } - /** @var $attribute Attribute */ -- if (!$attribute->getIsSearchable() || in_array($attribute->getAttributeCode(), ['price', 'sku'], true)) { -+ if (!$attribute->getIsSearchable() || in_array($attribute->getAttributeCode(), ['price'], true)) { - // Some fields have their own specific handlers - continue; - } -diff --git a/app/code/Magento/CatalogSearch/Model/Search/RequestGenerator/Decimal.php b/app/code/Magento/CatalogSearch/Model/Search/RequestGenerator/Decimal.php -index 3fb3021ff9d..b3d39a48fe9 100644 ---- a/app/code/Magento/CatalogSearch/Model/Search/RequestGenerator/Decimal.php -+++ b/app/code/Magento/CatalogSearch/Model/Search/RequestGenerator/Decimal.php -@@ -10,10 +10,13 @@ use Magento\Catalog\Model\ResourceModel\Eav\Attribute; - use Magento\Framework\Search\Request\BucketInterface; - use Magento\Framework\Search\Request\FilterInterface; - -+/** -+ * Catalog search range request generator. -+ */ - class Decimal implements GeneratorInterface - { - /** -- * {@inheritdoc} -+ * @inheritdoc - */ - public function getFilterData(Attribute $attribute, $filterName) - { -@@ -27,7 +30,7 @@ class Decimal implements GeneratorInterface - } - - /** -- * {@inheritdoc} -+ * @inheritdoc - */ - public function getAggregationData(Attribute $attribute, $bucketName) - { -diff --git a/app/code/Magento/CatalogSearch/Model/Search/RequestGenerator/General.php b/app/code/Magento/CatalogSearch/Model/Search/RequestGenerator/General.php -index f2965bb9f98..63b09de7f08 100644 ---- a/app/code/Magento/CatalogSearch/Model/Search/RequestGenerator/General.php -+++ b/app/code/Magento/CatalogSearch/Model/Search/RequestGenerator/General.php -@@ -10,10 +10,13 @@ use Magento\Catalog\Model\ResourceModel\Eav\Attribute; - use Magento\Framework\Search\Request\BucketInterface; - use Magento\Framework\Search\Request\FilterInterface; - -+/** -+ * Catalog search request generator. -+ */ - class General implements GeneratorInterface - { - /** -- * {@inheritdoc} -+ * @inheritdoc - */ - public function getFilterData(Attribute $attribute, $filterName) - { -@@ -26,7 +29,7 @@ class General implements GeneratorInterface - } - - /** -- * {@inheritdoc} -+ * @inheritdoc - */ - public function getAggregationData(Attribute $attribute, $bucketName) - { -diff --git a/app/code/Magento/CatalogSearch/Model/Search/RequestGenerator/GeneratorInterface.php b/app/code/Magento/CatalogSearch/Model/Search/RequestGenerator/GeneratorInterface.php -index a7379eaa0bd..22f829063fb 100644 ---- a/app/code/Magento/CatalogSearch/Model/Search/RequestGenerator/GeneratorInterface.php -+++ b/app/code/Magento/CatalogSearch/Model/Search/RequestGenerator/GeneratorInterface.php -@@ -9,13 +9,16 @@ namespace Magento\CatalogSearch\Model\Search\RequestGenerator; - use Magento\Catalog\Model\ResourceModel\Eav\Attribute; - - /** -+ * Catalog search reguest generator interface. -+ * - * @api - * @since 100.1.6 - */ - interface GeneratorInterface - { - /** -- * Get filter data for specific attribute -+ * Get filter data for specific attribute. -+ * - * @param Attribute $attribute - * @param string $filterName - * @return array -@@ -24,7 +27,8 @@ interface GeneratorInterface - public function getFilterData(Attribute $attribute, $filterName); - - /** -- * Get aggregation data for specific attribute -+ * Get aggregation data for specific attribute. -+ * - * @param Attribute $attribute - * @param string $bucketName - * @return array -diff --git a/app/code/Magento/CatalogSearch/Model/Search/RequestGenerator/GeneratorResolver.php b/app/code/Magento/CatalogSearch/Model/Search/RequestGenerator/GeneratorResolver.php -index d2841e5cdf3..68ca546b819 100644 ---- a/app/code/Magento/CatalogSearch/Model/Search/RequestGenerator/GeneratorResolver.php -+++ b/app/code/Magento/CatalogSearch/Model/Search/RequestGenerator/GeneratorResolver.php -@@ -9,6 +9,8 @@ namespace Magento\CatalogSearch\Model\Search\RequestGenerator; - /** - * @api - * @since 100.1.6 -+ * @deprecated -+ * @see \Magento\ElasticSearch - */ - class GeneratorResolver - { -diff --git a/app/code/Magento/CatalogSearch/Model/Search/SelectContainer/SelectContainer.php b/app/code/Magento/CatalogSearch/Model/Search/SelectContainer/SelectContainer.php -index 5305ed276bf..f0eade4bfbc 100644 ---- a/app/code/Magento/CatalogSearch/Model/Search/SelectContainer/SelectContainer.php -+++ b/app/code/Magento/CatalogSearch/Model/Search/SelectContainer/SelectContainer.php -@@ -10,8 +10,10 @@ use Magento\Framework\DB\Select; - use Magento\Framework\Search\Request\FilterInterface; - - /** -- * Class SelectContainer - * This class is a container for all data that is required for creating select query by search request -+ * -+ * @deprecated -+ * @see \Magento\ElasticSearch - */ - class SelectContainer - { -diff --git a/app/code/Magento/CatalogSearch/Model/Search/SelectContainer/SelectContainerBuilder.php b/app/code/Magento/CatalogSearch/Model/Search/SelectContainer/SelectContainerBuilder.php -index a18ef8e91d7..d5b7be8bf01 100644 ---- a/app/code/Magento/CatalogSearch/Model/Search/SelectContainer/SelectContainerBuilder.php -+++ b/app/code/Magento/CatalogSearch/Model/Search/SelectContainer/SelectContainerBuilder.php -@@ -18,6 +18,8 @@ use Magento\CatalogSearch\Model\Search\FilterMapper\VisibilityFilter; - * Class SelectContainerBuilder - * Class is responsible for SelectContainer creation and filling it with all required data - * @SuppressWarnings(PHPMD.CouplingBetweenObjects) -+ * @deprecated -+ * @see \Magento\ElasticSearch - */ - class SelectContainerBuilder - { -diff --git a/app/code/Magento/CatalogSearch/Model/Search/TableMapper.php b/app/code/Magento/CatalogSearch/Model/Search/TableMapper.php -index eab79c22309..6b18c4307f5 100644 ---- a/app/code/Magento/CatalogSearch/Model/Search/TableMapper.php -+++ b/app/code/Magento/CatalogSearch/Model/Search/TableMapper.php -@@ -25,6 +25,8 @@ use Magento\Store\Model\StoreManagerInterface; - * @SuppressWarnings(PHPMD.CouplingBetweenObjects) - * @api - * @since 100.0.2 -+ * @deprecated -+ * @see \Magento\ElasticSearch - */ - class TableMapper - { -diff --git a/app/code/Magento/CatalogSearch/Plugin/EnableEavIndexer.php b/app/code/Magento/CatalogSearch/Plugin/EnableEavIndexer.php -index c624f9d1c2e..956a1b2360f 100644 ---- a/app/code/Magento/CatalogSearch/Plugin/EnableEavIndexer.php -+++ b/app/code/Magento/CatalogSearch/Plugin/EnableEavIndexer.php -@@ -9,6 +9,9 @@ namespace Magento\CatalogSearch\Plugin; - - /** - * Enable Product EAV indexer in configuration for MySQL search engine -+ * -+ * @deprecated -+ * @see \Magento\ElasticSearch - */ - class EnableEavIndexer - { -diff --git a/app/code/Magento/CatalogSearch/Setup/Patch/Data/MySQLSearchDeprecationNotification.php b/app/code/Magento/CatalogSearch/Setup/Patch/Data/MySQLSearchDeprecationNotification.php -new file mode 100644 -index 00000000000..8fa9f56d784 ---- /dev/null -+++ b/app/code/Magento/CatalogSearch/Setup/Patch/Data/MySQLSearchDeprecationNotification.php -@@ -0,0 +1,70 @@ -+<?php -+/** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+declare(strict_types=1); -+ -+namespace Magento\CatalogSearch\Setup\Patch\Data; -+ -+/** -+ * Implementation of the notification about MySQL search being deprecated. -+ * -+ * @deprecated -+ * @see \Magento\ElasticSearch -+ */ -+class MySQLSearchDeprecationNotification implements \Magento\Framework\Setup\Patch\DataPatchInterface -+{ -+ /** -+ * @var \Magento\Framework\Search\EngineResolverInterface -+ */ -+ private $searchEngineResolver; -+ -+ /** -+ * @var \Magento\Framework\Notification\NotifierInterface -+ */ -+ private $notifier; -+ -+ /** -+ * @param \Magento\Framework\Search\EngineResolverInterface $searchEngineResolver -+ * @param \Magento\Framework\Notification\NotifierInterface $notifier -+ */ -+ public function __construct( -+ \Magento\Framework\Search\EngineResolverInterface $searchEngineResolver, -+ \Magento\Framework\Notification\NotifierInterface $notifier -+ ) { -+ $this->searchEngineResolver = $searchEngineResolver; -+ $this->notifier = $notifier; -+ } -+ -+ /** -+ * @inheritdoc -+ */ -+ public function apply() -+ { -+ if ($this->searchEngineResolver->getCurrentSearchEngine() === 'mysql') { -+ $message = <<<MESSAGE -+Catalog Search is currently configured to use the MySQL engine, which has been deprecated. Consider migrating to one of -+the Elasticsearch engines now to ensure there are no service interruptions during your next upgrade. -+MESSAGE; -+ -+ $this->notifier->addNotice(__('Deprecation Notice'), __($message)); -+ } -+ } -+ -+ /** -+ * @inheritdoc -+ */ -+ public function getAliases() -+ { -+ return []; -+ } -+ -+ /** -+ * @inheritdoc -+ */ -+ public static function getDependencies() -+ { -+ return []; -+ } -+} -diff --git a/app/code/Magento/CatalogSearch/Setup/Patch/Data/SetInitialSearchWeightForAttributes.php b/app/code/Magento/CatalogSearch/Setup/Patch/Data/SetInitialSearchWeightForAttributes.php -index beff1c66d4f..7f6dbe033e3 100644 ---- a/app/code/Magento/CatalogSearch/Setup/Patch/Data/SetInitialSearchWeightForAttributes.php -+++ b/app/code/Magento/CatalogSearch/Setup/Patch/Data/SetInitialSearchWeightForAttributes.php -@@ -13,8 +13,8 @@ use Magento\Framework\Indexer\IndexerInterfaceFactory; - use Magento\Catalog\Api\ProductAttributeRepositoryInterface; - - /** -- * Class SetInitialSearchWeightForAttributes -- * @package Magento\CatalogSearch\Setup\Patch -+ * @deprecated -+ * @see \Magento\ElasticSearch - */ - class SetInitialSearchWeightForAttributes implements DataPatchInterface, PatchVersionInterface - { -diff --git a/app/code/Magento/CatalogSearch/Test/Mftf/ActionGroup/AdminCatalogSearchTermActionGroup.xml b/app/code/Magento/CatalogSearch/Test/Mftf/ActionGroup/AdminCatalogSearchTermActionGroup.xml -new file mode 100644 -index 00000000000..33ffa4fe1b2 ---- /dev/null -+++ b/app/code/Magento/CatalogSearch/Test/Mftf/ActionGroup/AdminCatalogSearchTermActionGroup.xml -@@ -0,0 +1,59 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> -+ <actionGroup name="AssertSearchTermSaveSuccessMessage"> -+ <arguments> -+ <argument name="searchQuery" type="string"/> -+ <argument name="storeValue" type="string"/> -+ <argument name="redirectUrl" type="string"/> -+ <argument name="displayInSuggestedTerm" type="string"/> -+ </arguments> -+ <amOnPage url="{{AdminCatalogSearchTermIndexPage.url}}" stepKey="openAdminCatalogSearchTermIndexPage"/> -+ <waitForPageLoad stepKey="waitForAdminCatalogSearchTermIndexPageLoad"/> -+ <click selector="{{AdminCatalogSearchTermIndexSection.addNewSearchTermButton}}" stepKey="clickAddNewSearchTermButton"/> -+ <waitForPageLoad stepKey="waitForAdminCatalogSearchTermNewPageLoad"/> -+ <fillField selector="{{AdminCatalogSearchTermNewSection.searchQuery}}" userInput="{{searchQuery}}" stepKey="fillSearchQueryTextBox"/> -+ <selectOption selector="{{AdminCatalogSearchTermNewSection.store}}" userInput="{{storeValue}}" stepKey="selectStoreValue"/> -+ <fillField selector="{{AdminCatalogSearchTermNewSection.redirectUrl}}" userInput="{{redirectUrl}}" stepKey="fillRedirectUrl"/> -+ <selectOption selector="{{AdminCatalogSearchTermNewSection.displayInSuggestedTerm}}" userInput="{{displayInSuggestedTerm}}" stepKey="selectDisplayInSuggestedTerm"/> -+ <click selector="{{AdminCatalogSearchTermNewSection.saveSearchButton}}" stepKey="clickSaveSearchButton"/> -+ <see selector="{{AdminCatalogSearchTermMessagesSection.successMessage}}" userInput="You saved the search term." stepKey="seeSaveSuccessMessage"/> -+ </actionGroup> -+ <actionGroup name="AssertSearchTermSuccessDeleteMessage"> -+ <arguments> -+ <argument name="searchQuery" type="string"/> -+ </arguments> -+ <amOnPage url="{{AdminCatalogSearchTermIndexPage.url}}" stepKey="openCatalogSearchIndexPage"/> -+ <waitForPageLoad stepKey="waitForAdminCatalogSearchTermIndexPageLoad"/> -+ <click selector="{{AdminCatalogSearchTermIndexSection.resetFilterButton}}" stepKey="clickOnResetButton"/> -+ <waitForPageLoad stepKey="waitForPageToLoad"/> -+ <fillField selector="{{AdminCatalogSearchTermIndexSection.searchQuery}}" userInput="{{searchQuery}}" stepKey="fillSearchQuery"/> -+ <click selector="{{AdminCatalogSearchTermIndexSection.searchButton}}" stepKey="clickSearchButton"/> -+ <waitForPageLoad stepKey="waitForSearchResultLoad"/> -+ <click selector="{{AdminCatalogSearchTermIndexSection.nthRow('1')}}" stepKey="checkFirstRow"/> -+ <selectOption selector="{{AdminCatalogSearchTermIndexSection.massActions}}" userInput="delete" stepKey="selectDeleteOption"/> -+ <click selector="{{AdminCatalogSearchTermIndexSection.submit}}" stepKey="clickSubmitButton"/> -+ <click selector="{{AdminCatalogSearchTermIndexSection.okButton}}" stepKey="clickOkButton"/> -+ <see selector="{{AdminCatalogSearchTermMessagesSection.successMessage}}" userInput="Total of 1 record(s) were deleted." stepKey="seeSuccessMessage"/> -+ </actionGroup> -+ <actionGroup name="AssertSearchTermNotInGrid"> -+ <arguments> -+ <argument name="searchQuery" type="string"/> -+ </arguments> -+ <amOnPage url="{{AdminCatalogSearchTermIndexPage.url}}" stepKey="openCatalogSearchIndexPage"/> -+ <waitForPageLoad stepKey="waitForAdminCatalogSearchTermIndexPageLoad"/> -+ <click selector="{{AdminCatalogSearchTermIndexSection.resetFilterButton}}" stepKey="clickOnResetButton"/> -+ <waitForPageLoad stepKey="waitForPageToLoad"/> -+ <fillField selector="{{AdminCatalogSearchTermIndexSection.searchQuery}}" userInput="{{searchQuery}}" stepKey="fillSearchQuery"/> -+ <click selector="{{AdminCatalogSearchTermIndexSection.searchButton}}" stepKey="clickSearchButton"/> -+ <waitForPageLoad stepKey="waitForSearchResultToLoad"/> -+ <see selector="{{AdminCatalogSearchTermIndexSection.emptyRecords}}" userInput="We couldn't find any records." stepKey="seeEmptyRecordMessage"/> -+ </actionGroup> -+</actionGroups> -\ No newline at end of file -diff --git a/app/code/Magento/CatalogSearch/Test/Mftf/ActionGroup/AdminSetMinimalQueryLengthActionGroup.xml b/app/code/Magento/CatalogSearch/Test/Mftf/ActionGroup/AdminSetMinimalQueryLengthActionGroup.xml -new file mode 100644 -index 00000000000..b9ef37cb4ef ---- /dev/null -+++ b/app/code/Magento/CatalogSearch/Test/Mftf/ActionGroup/AdminSetMinimalQueryLengthActionGroup.xml -@@ -0,0 +1,29 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> -+ <actionGroup name="SetMinimalQueryLengthActionGroup"> -+ <arguments> -+ <argument name="minLength" type="string" defaultValue="1"/> -+ </arguments> -+ <amOnPage url="{{AdminCatalogSearchConfigurationPage.url}}" stepKey="navigateToConfigurationPage"/> -+ <waitForPageLoad stepKey="wait1"/> -+ <scrollTo selector="{{AdminCatalogSearchConfigurationSection.catalogSearchTab}}" stepKey="scrollToCatalogSearchTab"/> -+ <conditionalClick selector="{{AdminCatalogSearchConfigurationSection.catalogSearchTab}}" dependentSelector="{{AdminCatalogSearchConfigurationSection.minQueryLength}}" visible="false" stepKey="expandCatalogSearchTab"/> -+ <waitForElementVisible selector="{{AdminCatalogSearchConfigurationSection.minQueryLength}}" stepKey="waitTabToCollapse"/> -+ <see userInput="{{MinMaxQueryLength.Hint}}" selector="{{AdminCatalogSearchConfigurationSection.minQueryLengthHint}}" stepKey="seeHint1"/> -+ <see userInput="{{MinMaxQueryLength.Hint}}" selector="{{AdminCatalogSearchConfigurationSection.maxQueryLengthHint}}" stepKey="seeHint2"/> -+ <uncheckOption selector="{{AdminCatalogSearchConfigurationSection.minQueryLengthInherit}}" stepKey="uncheckSystemValue"/> -+ <fillField selector="{{AdminCatalogSearchConfigurationSection.minQueryLength}}" userInput="{{minLength}}" stepKey="setMinQueryLength"/> -+ <click selector="{{AdminCatalogSearchConfigurationSection.catalogSearchTab}}" stepKey="collapseTab"/> -+ <click selector="{{ContentManagementSection.Save}}" stepKey="saveConfig"/> -+ <waitForPageLoad stepKey="waitForConfigSaved"/> -+ <see userInput="You saved the configuration." stepKey="seeSuccessMessage"/> -+ </actionGroup> -+</actionGroups> -diff --git a/app/code/Magento/CatalogSearch/Test/Mftf/ActionGroup/StorefrontCatalogSearchActionGroup.xml b/app/code/Magento/CatalogSearch/Test/Mftf/ActionGroup/StorefrontCatalogSearchActionGroup.xml -index c4bb5ff4b6d..067d76821d6 100644 ---- a/app/code/Magento/CatalogSearch/Test/Mftf/ActionGroup/StorefrontCatalogSearchActionGroup.xml -+++ b/app/code/Magento/CatalogSearch/Test/Mftf/ActionGroup/StorefrontCatalogSearchActionGroup.xml -@@ -7,19 +7,70 @@ - --> - - <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"> - <!-- Quick search the phrase and check if the result page contains correct information --> - <actionGroup name="StorefrontCheckQuickSearchActionGroup"> - <arguments> -- <argument name="phrase"/> -+ <argument name="phrase" /> - </arguments> -- <fillField userInput="{{phrase}}" selector="{{StorefrontQuickSearchSection.searchPhrase}}" stepKey="fillQuickSearch"/> -- <click selector="{{StorefrontQuickSearchSection.searchButton}}" stepKey="clickQuickSearchButton" /> -+ <submitForm selector="{{StorefrontQuickSearchSection.searchMiniForm}}" parameterArray="['q' => {{phrase}}]" stepKey="fillQuickSearch" /> - <seeInCurrentUrl url="{{StorefrontCatalogSearchPage.url}}" stepKey="checkUrl"/> -+ <dontSeeInCurrentUrl url="form_key=" stepKey="checkUrlFormKey"/> - <seeInTitle userInput="Search results for: '{{phrase}}'" stepKey="assertQuickSearchTitle"/> - <see userInput="Search results for: '{{phrase}}'" selector="{{StorefrontCatalogSearchMainSection.SearchTitle}}" stepKey="assertQuickSearchName"/> - </actionGroup> - -+ <!-- Quick search the phrase and check if the result page contains correct information, usable with type="string" --> -+ <actionGroup name="StorefrontCheckQuickSearchStringActionGroup"> -+ <arguments> -+ <argument name="phrase" type="string"/> -+ </arguments> -+ <fillField stepKey="fillInput" selector="{{StorefrontQuickSearchResultsSection.searchTextBox}}" userInput="{{phrase}}"/> -+ <submitForm selector="{{StorefrontQuickSearchResultsSection.searchTextBox}}" parameterArray="[]" stepKey="submitQuickSearch" /> -+ <seeInCurrentUrl url="{{StorefrontCatalogSearchPage.url}}" stepKey="checkUrl"/> -+ <dontSeeInCurrentUrl url="form_key=" stepKey="checkUrlFormKey"/> -+ <seeInTitle userInput="Search results for: '{{phrase}}'" stepKey="assertQuickSearchTitle"/> -+ <see userInput="Search results for: '{{phrase}}'" selector="{{StorefrontCatalogSearchMainSection.SearchTitle}}" stepKey="assertQuickSearchName"/> -+ </actionGroup> -+ -+ <!-- Opens product from QuickSearch and performs assertions--> -+ <actionGroup name="StorefrontOpenProductFromQuickSearch"> -+ <arguments> -+ <argument name="productName" type="string"/> -+ <argument name="productUrlKey" type="string"/> -+ </arguments> -+ <click stepKey="openProduct" selector="{{StorefrontQuickSearchResultsSection.productByName(productName)}}"/> -+ <waitForPageLoad stepKey="waitForProductLoad"/> -+ <seeInCurrentUrl url="{{productUrlKey}}" stepKey="checkUrl"/> -+ <see stepKey="checkName" selector="{{StorefrontProductInfoMainSection.productName}}" userInput="{{productName}}"/> -+ </actionGroup> -+ -+ <!-- Adds product from Quicksearch page and perform assertions--> -+ <actionGroup name="StorefrontAddToCartFromQuickSearch"> -+ <arguments> -+ <argument name="productName" type="string"/> -+ </arguments> -+ <moveMouseOver stepKey="hoverOverProduct" selector="{{StorefrontQuickSearchResultsSection.productByIndex('1')}}"/> -+ <click selector="{{StorefrontQuickSearchResultsSection.productByName(productName)}} {{StorefrontQuickSearchResultsSection.addToCartBtn}}" stepKey="addToCart"/> -+ <waitForElementVisible selector="{{StorefrontQuickSearchResultsSection.messageSection}}" time="30" stepKey="waitForProductAdded"/> -+ <see selector="{{StorefrontQuickSearchResultsSection.messageSection}}" userInput="You added {{productName}} to your shopping cart." stepKey="seeAddedToCartMessage"/> -+ </actionGroup> -+ -+ <actionGroup name="StorefrontQuickSearchCheckProductNameInGrid"> -+ <arguments> -+ <argument name="productName" type="string"/> -+ <argument name="index" type="string"/> -+ </arguments> -+ <see selector="{{StorefrontQuickSearchResultsSection.productByIndex(index)}}" userInput="{{productName}}" stepKey="seeProductName"/> -+ </actionGroup> -+ -+ <actionGroup name="StorefrontQuickSearchCheckProductNameNotInGrid"> -+ <arguments> -+ <argument name="productName" type="string"/> -+ </arguments> -+ <dontSee selector="{{StorefrontQuickSearchResultsSection.allResults}}" userInput="{{productName}}" stepKey="dontSeeProductName"/> -+ </actionGroup> -+ - <!-- Open advanced search page --> - <actionGroup name="StorefrontOpenAdvancedSearchActionGroup"> - <click selector="{{StorefrontFooterSection.AdvancedSearch}}" stepKey="clickAdvancedSearchLink" /> -@@ -48,7 +99,7 @@ - <!-- Go to store's advanced catalog search page --> - <actionGroup name="GoToStoreViewAdvancedCatalogSearchActionGroup"> - <amOnPage url="{{StorefrontCatalogSearchAdvancedFormPage.url}}" stepKey="GoToStoreViewAdvancedCatalogSearchActionGroup"/> -- <waitForPageLoad stepKey="waitForPageLoad"/> -+ <waitForPageLoad time="90" stepKey="waitForPageLoad"/> - </actionGroup> - - <!-- Storefront advanced catalog search by product name --> -@@ -116,4 +167,9 @@ - <click selector="{{StorefrontCatalogSearchAdvancedFormSection.SubmitButton}}" stepKey="clickSubmit"/> - <waitForPageLoad stepKey="waitForPageLoad"/> - </actionGroup> -+ -+ <!-- Asserts that search results do not contain any results--> -+ <actionGroup name="StorefrontCheckSearchIsEmpty"> -+ <see stepKey="checkEmpty" selector="{{StorefrontQuickSearchResultsSection.messageSection}}" userInput="Your search returned no results"/> -+ </actionGroup> - </actionGroups> -\ No newline at end of file -diff --git a/app/code/Magento/CatalogSearch/Test/Mftf/ActionGroup/StorefrontCatalogSearchTermActionGroup.xml b/app/code/Magento/CatalogSearch/Test/Mftf/ActionGroup/StorefrontCatalogSearchTermActionGroup.xml -new file mode 100644 -index 00000000000..83e4ac50a74 ---- /dev/null -+++ b/app/code/Magento/CatalogSearch/Test/Mftf/ActionGroup/StorefrontCatalogSearchTermActionGroup.xml -@@ -0,0 +1,37 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> -+ <!--Verify AssertSearchTermNotOnFrontend--> -+ <actionGroup name="AssertSearchTermNotOnFrontend"> -+ <arguments> -+ <argument name="searchQuery" type="string"/> -+ <argument name="url_key" type="string"/> -+ </arguments> -+ <amOnPage url="{{StorefrontProductPage.url('url_key')}}" stepKey="goToMagentoStorefrontPage"/> -+ <waitForPageLoad stepKey="waitForStoreFrontProductPageLoad"/> -+ <fillField selector="{{StorefrontQuickSearchResultsSection.searchTextBox}}" userInput="{{searchQuery}}" stepKey="fillSearchQuery"/> -+ <waitForPageLoad stepKey="waitForSearchTextBox"/> -+ <click selector="{{StorefrontQuickSearchResultsSection.searchTextBoxButton}}" stepKey="clickSearchTextBoxButton"/> -+ <waitForPageLoad stepKey="waitForSearch"/> -+ <see selector="{{StorefrontMessagesSection.noticeMessage}}" userInput="Your search returned no results." stepKey="seeAssertSearchTermNotOnFrontendNoticeMessage"/> -+ </actionGroup> -+ -+ <actionGroup name="AssertSearchTermOnFrontend"> -+ <arguments> -+ <argument name="searchQuery" type="string"/> -+ <argument name="redirectUrl" type="string"/> -+ </arguments> -+ <fillField selector="{{StorefrontQuickSearchResultsSection.searchTextBox}}" userInput="{{searchQuery}}" stepKey="fillSearchQuery"/> -+ <waitForPageLoad stepKey="waitForFillField"/> -+ <click selector="{{StorefrontQuickSearchResultsSection.searchTextBoxButton}}" stepKey="clickSearchTextBoxButton"/> -+ <waitForPageLoad stepKey="waitForSearch"/> -+ <seeInCurrentUrl url="{{redirectUrl}}" stepKey="checkUrl"/> -+ </actionGroup> -+</actionGroups> -\ No newline at end of file -diff --git a/app/code/Magento/CatalogSearch/Test/Mftf/Data/AdminMenuData.xml b/app/code/Magento/CatalogSearch/Test/Mftf/Data/AdminMenuData.xml -new file mode 100644 -index 00000000000..df1c3db6e56 ---- /dev/null -+++ b/app/code/Magento/CatalogSearch/Test/Mftf/Data/AdminMenuData.xml -@@ -0,0 +1,21 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<entities xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataProfileSchema.xsd"> -+ <entity name="AdminMenuMarketingSEOAndSearchSearchTerms"> -+ <data key="pageTitle">Search Terms</data> -+ <data key="title">Search Terms</data> -+ <data key="dataUiId">magento-search-search-terms</data> -+ </entity> -+ <entity name="AdminMenuReportsMarketingSearchTerms"> -+ <data key="pageTitle">Search Terms Report</data> -+ <data key="title">Search Terms</data> -+ <data key="dataUiId">magento-search-report-search-term</data> -+ </entity> -+</entities> -diff --git a/app/code/Magento/CatalogSearch/Test/Mftf/Data/CatalogSearchData.xml b/app/code/Magento/CatalogSearch/Test/Mftf/Data/CatalogSearchData.xml -new file mode 100644 -index 00000000000..68684560791 ---- /dev/null -+++ b/app/code/Magento/CatalogSearch/Test/Mftf/Data/CatalogSearchData.xml -@@ -0,0 +1,27 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<entities xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataProfileSchema.xsd"> -+ <entity name="SetMinQueryLengthToDefault" type="catalog_search_config_def"> -+ <requiredEntity type="enable">DefaultMinQueryLength</requiredEntity> -+ </entity> -+ <entity name="UncheckMinQueryLengthAndSet" type="catalog_search_config_query_length"> -+ <requiredEntity type="number">SetMinQueryLengthToOne</requiredEntity> -+ </entity> -+ <entity name="DefaultMinQueryLength" type="enable"> -+ <data key="inherit">true</data> -+ </entity> -+ <entity name="DefaultMinQueryLengthDisable" type="enable"> -+ <data key="inherit">0</data> -+ </entity> -+ <entity name="SetMinQueryLengthToOne" type="number"> -+ <data key="value">1</data> -+ </entity> -+ -+</entities> -\ No newline at end of file -diff --git a/app/code/Magento/CatalogSearch/Test/Mftf/Data/ConstData.xml b/app/code/Magento/CatalogSearch/Test/Mftf/Data/ConstData.xml -index 08fc1ce00e5..52fd61301c3 100644 ---- a/app/code/Magento/CatalogSearch/Test/Mftf/Data/ConstData.xml -+++ b/app/code/Magento/CatalogSearch/Test/Mftf/Data/ConstData.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"> - <!-- @TODO: Get rid off this workaround and its usages after MQE-498 is implemented --> - <entity name="CONST" type="CONST"> - <data key="apiSimpleProduct">Api Simple Product</data> -diff --git a/app/code/Magento/CatalogSearch/Test/Mftf/Data/MinMaxQueryLengthHintsData.xml b/app/code/Magento/CatalogSearch/Test/Mftf/Data/MinMaxQueryLengthHintsData.xml -new file mode 100644 -index 00000000000..6fb254afea3 ---- /dev/null -+++ b/app/code/Magento/CatalogSearch/Test/Mftf/Data/MinMaxQueryLengthHintsData.xml -@@ -0,0 +1,14 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<entities xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataProfileSchema.xsd"> -+ <entity name="MinMaxQueryLength" type="constant"> -+ <data key="Hint">This value must be compatible with the corresponding setting in the configured search engine</data> -+ </entity> -+</entities> -diff --git a/app/code/Magento/CatalogSearch/Test/Mftf/Data/SearchTermData.xml b/app/code/Magento/CatalogSearch/Test/Mftf/Data/SearchTermData.xml -new file mode 100644 -index 00000000000..995b860d107 ---- /dev/null -+++ b/app/code/Magento/CatalogSearch/Test/Mftf/Data/SearchTermData.xml -@@ -0,0 +1,17 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<entities xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataProfileSchema.xsd"> -+ <entity name="SimpleTerm" type="searchTerm"> -+ <data key="search_query" unique="suffix">Query text</data> -+ <data key="store_id">Default Store View</data> -+ <data key="redirect" unique="suffix">http://example.com/</data> -+ <data key="display_in_terms">No</data> -+ </entity> -+</entities> -\ No newline at end of file -diff --git a/app/code/Magento/CatalogSearch/Test/Mftf/Metadata/catalog_search-meta.xml b/app/code/Magento/CatalogSearch/Test/Mftf/Metadata/catalog_search-meta.xml -new file mode 100644 -index 00000000000..7405377249a ---- /dev/null -+++ b/app/code/Magento/CatalogSearch/Test/Mftf/Metadata/catalog_search-meta.xml -@@ -0,0 +1,32 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+<operations xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataOperation.xsd"> -+ <operation name="CatalogSearchConfigDefault" dataType="catalog_search_config_def" type="create" auth="adminFormKey" url="/admin/system_config/save/section/catalog/" method="POST"> -+ <object key="groups" dataType="catalog_search_config_def"> -+ <object key="search" dataType="catalog_search_config_def"> -+ <object key="fields" dataType="catalog_search_config_def"> -+ <object key="min_query_length" dataType="enable"> -+ <field key="inherit">boolean</field> -+ </object> -+ </object> -+ </object> -+ </object> -+ </operation> -+ <operation name="CatalogSearchConfigQueryLength" dataType="catalog_search_config_query_length" type="create" auth="adminFormKey" url="/admin/system_config/save/section/catalog/" method="POST"> -+ <object key="groups" dataType="catalog_search_config_query_length"> -+ <object key="search" dataType="catalog_search_config_query_length"> -+ <object key="fields" dataType="catalog_search_config_query_length"> -+ <object key="min_query_length" dataType="number"> -+ <field key="value">integer</field> -+ </object> -+ </object> -+ </object> -+ </object> -+ </operation> -+</operations> -diff --git a/app/code/Magento/CatalogSearch/Test/Mftf/Page/AdminCatalogSearchTermIndexPage.xml b/app/code/Magento/CatalogSearch/Test/Mftf/Page/AdminCatalogSearchTermIndexPage.xml -new file mode 100644 -index 00000000000..bbafff8ad77 ---- /dev/null -+++ b/app/code/Magento/CatalogSearch/Test/Mftf/Page/AdminCatalogSearchTermIndexPage.xml -@@ -0,0 +1,14 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<pages xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/PageObject.xsd"> -+ <page name="AdminCatalogSearchTermIndexPage" url="/search/term/index/" area="admin" module="Magento_CatalogSearch"> -+ <section name="AdminCatalogSearchTermIndexSection"/> -+ </page> -+</pages> -\ No newline at end of file -diff --git a/app/code/Magento/CatalogSearch/Test/Mftf/Page/AdminCatalogSearchTermNewPage.xml b/app/code/Magento/CatalogSearch/Test/Mftf/Page/AdminCatalogSearchTermNewPage.xml -new file mode 100644 -index 00000000000..de749147174 ---- /dev/null -+++ b/app/code/Magento/CatalogSearch/Test/Mftf/Page/AdminCatalogSearchTermNewPage.xml -@@ -0,0 +1,14 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<pages xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/PageObject.xsd"> -+ <page name="AdminCatalogSearchTermNewPage" url="/search/term/new/" area="admin" module="Magento_CatalogSearch"> -+ <section name="AdminCatalogSearchTermNewSection"/> -+ </page> -+</pages> -\ No newline at end of file -diff --git a/app/code/Magento/CatalogSearch/Test/Mftf/Page/StorefrontCatalogSearchAdvancedFormPage.xml b/app/code/Magento/CatalogSearch/Test/Mftf/Page/StorefrontCatalogSearchAdvancedFormPage.xml -index c52d816f0f3..28515c8186a 100644 ---- a/app/code/Magento/CatalogSearch/Test/Mftf/Page/StorefrontCatalogSearchAdvancedFormPage.xml -+++ b/app/code/Magento/CatalogSearch/Test/Mftf/Page/StorefrontCatalogSearchAdvancedFormPage.xml -@@ -7,7 +7,7 @@ - --> - - <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="StorefrontCatalogSearchAdvancedFormPage" url="/catalogsearch/advanced/" area="storefront" module="Magento_CatalogSearch"> - <section name="StorefrontCatalogSearchAdvancedFormSection" /> - <section name="StorefrontQuickSearchSection" /> -diff --git a/app/code/Magento/CatalogSearch/Test/Mftf/Page/StorefrontCatalogSearchAdvancedResultPage.xml b/app/code/Magento/CatalogSearch/Test/Mftf/Page/StorefrontCatalogSearchAdvancedResultPage.xml -index 422ccc652b7..0584f5e3380 100644 ---- a/app/code/Magento/CatalogSearch/Test/Mftf/Page/StorefrontCatalogSearchAdvancedResultPage.xml -+++ b/app/code/Magento/CatalogSearch/Test/Mftf/Page/StorefrontCatalogSearchAdvancedResultPage.xml -@@ -7,7 +7,7 @@ - --> - - <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="StorefrontCatalogSearchAdvancedResultPage" url="/catalogsearch/advanced/result" area="storefront" module="Magento_CatalogSearch"> - <section name="StorefrontCatalogSearchAdvancedResultMainSection" /> - <section name="StorefrontQuickSearchSection" /> -diff --git a/app/code/Magento/CatalogSearch/Test/Mftf/Page/StorefrontCatalogSearchPage.xml b/app/code/Magento/CatalogSearch/Test/Mftf/Page/StorefrontCatalogSearchPage.xml -index 6141aa96226..0700adb6d30 100644 ---- a/app/code/Magento/CatalogSearch/Test/Mftf/Page/StorefrontCatalogSearchPage.xml -+++ b/app/code/Magento/CatalogSearch/Test/Mftf/Page/StorefrontCatalogSearchPage.xml -@@ -7,7 +7,7 @@ - --> - - <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="StorefrontCatalogSearchPage" url="/catalogsearch/result/" area="storefront" module="Magento_CatalogSearch"> - <section name="StorefrontCatalogSearchMainSection" /> - <section name="StorefrontQuickSearchSection" /> -diff --git a/app/code/Magento/CatalogSearch/Test/Mftf/Section/AdminCatalogSearchTermIndexSection.xml b/app/code/Magento/CatalogSearch/Test/Mftf/Section/AdminCatalogSearchTermIndexSection.xml -new file mode 100644 -index 00000000000..aa0145b9f96 ---- /dev/null -+++ b/app/code/Magento/CatalogSearch/Test/Mftf/Section/AdminCatalogSearchTermIndexSection.xml -@@ -0,0 +1,25 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> -+ <section name="AdminCatalogSearchTermIndexSection"> -+ <element name="addNewSearchTermButton" type="button" selector="//div[@class='page-actions-buttons']/button[@id='add']" timeout="30"/> -+ <element name="resetFilterButton" type="button" selector="//button[@class='action-default scalable action-reset action-tertiary']" timeout="30"/> -+ <element name="searchButton" type="button" selector="//button[@class='action-default scalable action-secondary']" timeout="30"/> -+ <element name="massActions" type="text" selector="//div[@class='admin__grid-massaction-form']//select[@id='search_term_grid_massaction-select']"/> -+ <element name="submit" type="button" selector="//button[@class='action-default scalable']/span" timeout="30"/> -+ <element name="searchQuery" type="text" selector="//tr[@class='data-grid-filters']//td/input[@name='search_query']"/> -+ <element name="nthRow" type="checkbox" selector="//tbody/tr['{{rowNum}}']//input[@name='search']" parameterized="true"/> -+ <element name="searchTermRowCheckboxBySearchQuery" type="checkbox" selector="//*[normalize-space()='{{var1}}']/preceding-sibling::td//input[@name='search']" parameterized="true" timeout="30"/> -+ <element name="okButton" type="button" selector="//button[@class='action-primary action-accept']/span" timeout="30"/> -+ <element name="emptyRecords" type="text" selector="//tr[@class='data-grid-tr-no-data even']/td[@class='empty-text']"/> -+ <element name="gridRow" type="text" selector="//tr[@data-role='row']"/> -+ <element name="numberOfSearchTermResults" type="text" selector="//tr[@data-role='row']/td[@data-column='num_results']"/> -+ </section> -+</sections> -\ No newline at end of file -diff --git a/app/code/Magento/CatalogSearch/Test/Mftf/Section/AdminCatalogSearchTermMessagesSection.xml b/app/code/Magento/CatalogSearch/Test/Mftf/Section/AdminCatalogSearchTermMessagesSection.xml -new file mode 100644 -index 00000000000..5d19198a1b9 ---- /dev/null -+++ b/app/code/Magento/CatalogSearch/Test/Mftf/Section/AdminCatalogSearchTermMessagesSection.xml -@@ -0,0 +1,14 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> -+ <section name="AdminCatalogSearchTermMessagesSection"> -+ <element name="successMessage" type="text" selector="//div[@class='message message-success success']/div[@data-ui-id='messages-message-success']"/> -+ </section> -+</sections> -\ No newline at end of file -diff --git a/app/code/Magento/CatalogSearch/Test/Mftf/Section/AdminCatalogSearchTermNewSection.xml b/app/code/Magento/CatalogSearch/Test/Mftf/Section/AdminCatalogSearchTermNewSection.xml -new file mode 100644 -index 00000000000..a07e2a9b8a5 ---- /dev/null -+++ b/app/code/Magento/CatalogSearch/Test/Mftf/Section/AdminCatalogSearchTermNewSection.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="AdminCatalogSearchTermNewSection"> -+ <element name="searchQuery" type="text" selector="#query_text"/> -+ <element name="store" type="text" selector="#store_id"/> -+ <element name="numberOfResults" type="button" selector="#num_results"/> -+ <element name="numberOfUses" type="button" selector="#popularity"/> -+ <element name="redirectUrl" type="text" selector="//div[@class='admin__field-control control']/input[@id='redirect']"/> -+ <element name="displayInSuggestedTerm" type="select" selector="//select[@name='display_in_terms']"/> -+ <element name="saveSearchButton" type="button" selector="//button[@id='save']/span[@class='ui-button-text']" timeout="30"/> -+ </section> -+</sections> -\ No newline at end of file -diff --git a/app/code/Magento/CatalogSearch/Test/Mftf/Section/CatalogSearchAdminConfigSection.xml b/app/code/Magento/CatalogSearch/Test/Mftf/Section/CatalogSearchAdminConfigSection.xml -new file mode 100644 -index 00000000000..605bcabb8a8 ---- /dev/null -+++ b/app/code/Magento/CatalogSearch/Test/Mftf/Section/CatalogSearchAdminConfigSection.xml -@@ -0,0 +1,17 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> -+ <section name="AdminCatalogSearchConfigurationSection"> -+ <element name="minQueryLength" type="input" selector="#catalog_search_min_query_length"/> -+ <element name="minQueryLengthInherit" type="checkbox" selector="#catalog_search_min_query_length_inherit"/> -+ <element name="minQueryLengthHint" type="text" selector="#row_catalog_search_min_query_length .value span"/> -+ <element name="maxQueryLengthHint" type="text" selector="#row_catalog_search_max_query_length .value span"/> -+ </section> -+</sections> -\ No newline at end of file -diff --git a/app/code/Magento/CatalogSearch/Test/Mftf/Section/StorefrontCatalogSearchAdvancedFormSection.xml b/app/code/Magento/CatalogSearch/Test/Mftf/Section/StorefrontCatalogSearchAdvancedFormSection.xml -index d7c63ca1af2..68890255300 100644 ---- a/app/code/Magento/CatalogSearch/Test/Mftf/Section/StorefrontCatalogSearchAdvancedFormSection.xml -+++ b/app/code/Magento/CatalogSearch/Test/Mftf/Section/StorefrontCatalogSearchAdvancedFormSection.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="StorefrontCatalogSearchAdvancedFormSection"> - <element name="SearchTitle" type="text" selector=".page-title span"/> - <element name="ProductName" type="input" selector="#name"/> -diff --git a/app/code/Magento/CatalogSearch/Test/Mftf/Section/StorefrontCatalogSearchAdvancedResultMainSection.xml b/app/code/Magento/CatalogSearch/Test/Mftf/Section/StorefrontCatalogSearchAdvancedResultMainSection.xml -index d0634754eee..6b28b4f36c6 100644 ---- a/app/code/Magento/CatalogSearch/Test/Mftf/Section/StorefrontCatalogSearchAdvancedResultMainSection.xml -+++ b/app/code/Magento/CatalogSearch/Test/Mftf/Section/StorefrontCatalogSearchAdvancedResultMainSection.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="StorefrontCatalogSearchAdvancedResultMainSection"> - <element name="SearchTitle" type="text" selector=".page-title span"/> - <element name="ProductItemInfo" type="button" selector=".product-item-info"/> -diff --git a/app/code/Magento/CatalogSearch/Test/Mftf/Section/StorefrontCatalogSearchMainSection.xml b/app/code/Magento/CatalogSearch/Test/Mftf/Section/StorefrontCatalogSearchMainSection.xml -index 165629f1cea..667f08fea65 100644 ---- a/app/code/Magento/CatalogSearch/Test/Mftf/Section/StorefrontCatalogSearchMainSection.xml -+++ b/app/code/Magento/CatalogSearch/Test/Mftf/Section/StorefrontCatalogSearchMainSection.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="StorefrontCatalogSearchMainSection"> - <element name="SearchTitle" type="text" selector=".page-title span"/> - <element name="ProductItemInfo" type="button" selector=".product-item-info"/> -@@ -15,5 +15,6 @@ - <element name="SuccessMsg" type="button" selector="div.message-success"/> - <element name="productCount" type="text" selector="#toolbar-amount"/> - <element name="message" type="text" selector="div.message div"/> -+ <element name="searchResults" type="block" selector="#maincontent .column.main"/> - </section> - </sections> -diff --git a/app/code/Magento/CatalogSearch/Test/Mftf/Section/StorefrontFooterSection.xml b/app/code/Magento/CatalogSearch/Test/Mftf/Section/StorefrontFooterSection.xml -index dae21aeefc1..dbecf55a010 100644 ---- a/app/code/Magento/CatalogSearch/Test/Mftf/Section/StorefrontFooterSection.xml -+++ b/app/code/Magento/CatalogSearch/Test/Mftf/Section/StorefrontFooterSection.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="StorefrontFooterSection"> - <element name="AdvancedSearch" type="button" selector="//footer//ul//li//a[text()='Advanced Search']"/> - </section> -diff --git a/app/code/Magento/CatalogSearch/Test/Mftf/Test/AdminCreateSearchTermEntityTest.xml b/app/code/Magento/CatalogSearch/Test/Mftf/Test/AdminCreateSearchTermEntityTest.xml -new file mode 100644 -index 00000000000..2b425f34f8a ---- /dev/null -+++ b/app/code/Magento/CatalogSearch/Test/Mftf/Test/AdminCreateSearchTermEntityTest.xml -@@ -0,0 +1,59 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> -+ <test name="AdminCreateSearchTermEntityTest"> -+ <annotations> -+ <stories value="Search terms"/> -+ <title value="Create search term test"/> -+ <description value="Admin should be able to create search term"/> -+ <testCaseId value="MC-13989"/> -+ <severity value="CRITICAL"/> -+ <group value="CatalogSearch"/> -+ <group value="mtf_migrated"/> -+ </annotations> -+ <before> -+ <!-- Login as admin --> -+ <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> -+ -+ <!-- Create simple product --> -+ <createData entity="SimpleProduct2" stepKey="createSimpleProduct"/> -+ </before> -+ <after> -+ <!-- Delete created search term --> -+ <actionGroup ref="AssertSearchTermSuccessDeleteMessage" stepKey="deleteSearchTerm"> -+ <argument name="searchQuery" value="$$createSimpleProduct.sku$$"/> -+ </actionGroup> -+ -+ <!-- Delete created product --> -+ <deleteData createDataKey="createSimpleProduct" stepKey="deleteSimpleProduct"/> -+ -+ <!-- Log out --> -+ <actionGroup ref="logout" stepKey="logout"/> -+ </after> -+ -+ <!-- Go to the search terms page and create new search term --> -+ <actionGroup ref="AssertSearchTermSaveSuccessMessage" stepKey="createNewSearchTerm"> -+ <argument name="searchQuery" value="$$createSimpleProduct.sku$$"/> -+ <argument name="storeValue" value="{{SimpleTerm.store_id}}"/> -+ <argument name="redirectUrl" value="{{SimpleTerm.redirect}}"/> -+ <argument name="displayInSuggestedTerm" value="{{SimpleTerm.display_in_terms}}"/> -+ </actionGroup> -+ -+ <!-- Go to storefront --> -+ <amOnPage url="{{StorefrontHomePage.url}}" stepKey="amOnStorefrontPage"/> -+ <waitForPageLoad stepKey="waitForPageLoad"/> -+ -+ <!-- Assert created search term on storefront --> -+ <actionGroup ref="AssertSearchTermOnFrontend" stepKey="assertCreatedSearchTermOnFrontend"> -+ <argument name="searchQuery" value="$$createSimpleProduct.sku$$"/> -+ <argument name="redirectUrl" value="{{SimpleTerm.redirect}}"/> -+ </actionGroup> -+ </test> -+</tests> -diff --git a/app/code/Magento/CatalogSearch/Test/Mftf/Test/AdminDeleteSearchTermTest.xml b/app/code/Magento/CatalogSearch/Test/Mftf/Test/AdminDeleteSearchTermTest.xml -new file mode 100644 -index 00000000000..c72ed424ef3 ---- /dev/null -+++ b/app/code/Magento/CatalogSearch/Test/Mftf/Test/AdminDeleteSearchTermTest.xml -@@ -0,0 +1,59 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> -+ <test name="AdminDeleteSearchTermTest"> -+ <annotations> -+ <stories value="Search terms"/> -+ <title value="Delete Search Term and Verify Storefront"/> -+ <description value="Test log in to SearchTerm and DeleteSearchTerm"/> -+ <testCaseId value="MC-13988"/> -+ <severity value="CRITICAL"/> -+ <group value="CatalogSearch"/> -+ <group value="mtf_migrated"/> -+ </annotations> -+ -+ <before> -+ <actionGroup ref = "LoginAsAdmin" stepKey="loginAsAdmin"/> -+ <createData entity="SimpleSubCategory" stepKey="initialCategoryEntity"/> -+ <createData entity="SimpleProduct" stepKey="simpleProduct"> -+ <requiredEntity createDataKey="initialCategoryEntity"/> -+ </createData> -+ </before> -+ <after> -+ <deleteData stepKey="deleteSimpleSubCategory" createDataKey="initialCategoryEntity"/> -+ <deleteData stepKey="deleteSimpleProduct" createDataKey="simpleProduct"/> -+ <actionGroup ref="logout" stepKey="logout"/> -+ </after> -+ -+ <!--Add new search term--> -+ <actionGroup ref="AssertSearchTermSaveSuccessMessage" stepKey="addNewSearchTerm"> -+ <argument name="searchQuery" value="{{SimpleTerm.search_query}}"/> -+ <argument name="storeValue" value="{{SimpleTerm.store_id}}"/> -+ <argument name="redirectUrl" value="{{SimpleTerm.redirect}}"/> -+ <argument name="displayInSuggestedTerm" value="{{SimpleTerm.display_in_terms}}"/> -+ </actionGroup> -+ -+ <!--Search and delete search term and AssertSearchTermSuccessDeleteMessage--> -+ <actionGroup ref="AssertSearchTermSuccessDeleteMessage" stepKey="deleteSearchTerm"> -+ <argument name="searchQuery" value="{{SimpleTerm.search_query}}"/> -+ </actionGroup> -+ -+ <!--Verify deleted search term in grid and AssertSearchTermNotInGrid--> -+ <actionGroup ref="AssertSearchTermNotInGrid" stepKey="verifyDeletedSearchTermNotInGrid"> -+ <argument name="searchQuery" value="{{SimpleTerm.search_query}}"/> -+ </actionGroup> -+ -+ <!--Go to storefront and Verify AssertSearchTermNotOnFrontend--> -+ <actionGroup ref="AssertSearchTermNotOnFrontend" stepKey="verifySearchTermNotOnFrontend"> -+ <argument name="searchQuery" value="{{SimpleTerm.search_query}}"/> -+ <argument name="url_key" value="$$simpleProduct.custom_attributes[url_key]$$"/> -+ </actionGroup> -+ </test> -+</tests> -\ No newline at end of file -diff --git a/app/code/Magento/CatalogSearch/Test/Mftf/Test/AdminMarketingSearchTermsNavigateMenuTest.xml b/app/code/Magento/CatalogSearch/Test/Mftf/Test/AdminMarketingSearchTermsNavigateMenuTest.xml -new file mode 100644 -index 00000000000..bc255020d98 ---- /dev/null -+++ b/app/code/Magento/CatalogSearch/Test/Mftf/Test/AdminMarketingSearchTermsNavigateMenuTest.xml -@@ -0,0 +1,36 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> -+ <test name="AdminMarketingSearchTermsNavigateMenuTest"> -+ <annotations> -+ <features value="CatalogSearch"/> -+ <stories value="Menu Navigation"/> -+ <title value="Admin marketing search terms navigate menu test"/> -+ <description value="Admin should be able to navigate to Marketing > Search Terms"/> -+ <severity value="CRITICAL"/> -+ <testCaseId value="MC-14135"/> -+ <group value="menu"/> -+ <group value="mtf_migrated"/> -+ </annotations> -+ <before> -+ <actionGroup ref="LoginAsAdmin" stepKey="LoginAsAdmin"/> -+ </before> -+ <after> -+ <actionGroup ref="logout" stepKey="logout"/> -+ </after> -+ <actionGroup ref="AdminNavigateMenuActionGroup" stepKey="navigateToMarketingSearchTermsPage"> -+ <argument name="menuUiId" value="{{AdminMenuMarketing.dataUiId}}"/> -+ <argument name="submenuUiId" value="{{AdminMenuMarketingSEOAndSearchSearchTerms.dataUiId}}"/> -+ </actionGroup> -+ <actionGroup ref="AdminAssertPageTitleActionGroup" stepKey="seePageTitle"> -+ <argument name="title" value="{{AdminMenuMarketingSEOAndSearchSearchTerms.pageTitle}}"/> -+ </actionGroup> -+ </test> -+</tests> -diff --git a/app/code/Magento/CatalogSearch/Test/Mftf/Test/AdminReportsSearchTermsNavigateMenuTest.xml b/app/code/Magento/CatalogSearch/Test/Mftf/Test/AdminReportsSearchTermsNavigateMenuTest.xml -new file mode 100644 -index 00000000000..85cf0e3ba90 ---- /dev/null -+++ b/app/code/Magento/CatalogSearch/Test/Mftf/Test/AdminReportsSearchTermsNavigateMenuTest.xml -@@ -0,0 +1,36 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> -+ <test name="AdminReportsSearchTermsNavigateMenuTest"> -+ <annotations> -+ <features value="CatalogSearch"/> -+ <stories value="Menu Navigation"/> -+ <title value="Admin reports search terms navigate menu test"/> -+ <description value="Admin should be able to navigate to Reports > Search Terms"/> -+ <severity value="CRITICAL"/> -+ <testCaseId value="MC-14136"/> -+ <group value="menu"/> -+ <group value="mtf_migrated"/> -+ </annotations> -+ <before> -+ <actionGroup ref="LoginAsAdmin" stepKey="LoginAsAdmin"/> -+ </before> -+ <after> -+ <actionGroup ref="logout" stepKey="logout"/> -+ </after> -+ <actionGroup ref="AdminNavigateMenuActionGroup" stepKey="navigateToReportSearchTermsPage"> -+ <argument name="menuUiId" value="{{AdminMenuReports.dataUiId}}"/> -+ <argument name="submenuUiId" value="{{AdminMenuReportsMarketingSearchTerms.dataUiId}}"/> -+ </actionGroup> -+ <actionGroup ref="AdminAssertPageTitleActionGroup" stepKey="seePageTitle"> -+ <argument name="title" value="{{AdminMenuReportsMarketingSearchTerms.pageTitle}}"/> -+ </actionGroup> -+ </test> -+</tests> -diff --git a/app/code/Magento/CatalogSearch/Test/Mftf/Test/AdvanceCatalogSearchSimpleProductTest.xml b/app/code/Magento/CatalogSearch/Test/Mftf/Test/AdvanceCatalogSearchSimpleProductTest.xml -index f33f3db14b6..13665100f79 100644 ---- a/app/code/Magento/CatalogSearch/Test/Mftf/Test/AdvanceCatalogSearchSimpleProductTest.xml -+++ b/app/code/Magento/CatalogSearch/Test/Mftf/Test/AdvanceCatalogSearchSimpleProductTest.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="AdvanceCatalogSearchSimpleProductByNameTest"> - <annotations> - <features value="CatalogSearch"/> -diff --git a/app/code/Magento/CatalogSearch/Test/Mftf/Test/EndToEndB2CGuestUserTest.xml b/app/code/Magento/CatalogSearch/Test/Mftf/Test/EndToEndB2CGuestUserTest.xml -index b19c00eaf32..99f3fc00a74 100644 ---- a/app/code/Magento/CatalogSearch/Test/Mftf/Test/EndToEndB2CGuestUserTest.xml -+++ b/app/code/Magento/CatalogSearch/Test/Mftf/Test/EndToEndB2CGuestUserTest.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="EndToEndB2CGuestUserTest"> - <!-- Step 2: User searches for product --> - <comment userInput="Start of searching products" stepKey="startOfSearchingProducts" after="endOfBrowsingCatalog"/> -diff --git a/app/code/Magento/CatalogSearch/Test/Mftf/Test/EndToEndB2CLoggedInUserTest.xml b/app/code/Magento/CatalogSearch/Test/Mftf/Test/EndToEndB2CLoggedInUserTest.xml -index 5669a788105..367cb6a6e21 100644 ---- a/app/code/Magento/CatalogSearch/Test/Mftf/Test/EndToEndB2CLoggedInUserTest.xml -+++ b/app/code/Magento/CatalogSearch/Test/Mftf/Test/EndToEndB2CLoggedInUserTest.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="EndToEndB2CLoggedInUserTest"> - <!-- Step 2: User searches for product --> - <comment userInput="Start of searching products" stepKey="startOfSearchingProducts" after="endOfBrowsingCatalog"/> -diff --git a/app/code/Magento/CatalogSearch/Test/Mftf/Test/MinimalQueryLengthForCatalogSearchTest.xml b/app/code/Magento/CatalogSearch/Test/Mftf/Test/MinimalQueryLengthForCatalogSearchTest.xml -new file mode 100644 -index 00000000000..b6417e12a6d ---- /dev/null -+++ b/app/code/Magento/CatalogSearch/Test/Mftf/Test/MinimalQueryLengthForCatalogSearchTest.xml -@@ -0,0 +1,44 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> -+ <test name="MinimalQueryLengthForCatalogSearchTest"> -+ <annotations> -+ <features value="CatalogSearch"/> -+ <stories value="Catalog search"/> -+ <title value="Minimal query length for catalog search"/> -+ <description value="Minimal query length for catalog search"/> -+ <severity value="AVERAGE"/> -+ <testCaseId value="MC-6325"/> -+ <useCaseId value="MAGETWO-58764"/> -+ <group value="CatalogSearch"/> -+ </annotations> -+ <before> -+ <createData entity="ApiCategory" stepKey="createCategory"/> -+ <createData entity="ApiSimpleProduct" stepKey="createProduct"> -+ <requiredEntity createDataKey="createCategory"/> -+ </createData> -+ <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> -+ </before> -+ <after> -+ <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> -+ <deleteData createDataKey="createProduct" stepKey="deleteProduct"/> -+ <createData entity="SetMinQueryLengthToDefault" stepKey="setMinimumQueryLengthToDefault"/> -+ <actionGroup ref="logout" stepKey="logout"/> -+ </after> -+ <actionGroup ref="SetMinimalQueryLengthActionGroup" stepKey="setMinQueryLength"/> -+ <comment userInput="Go to Storefront and search for product" stepKey="searchProdUsingMinQueryLength"/> -+ <amOnPage url="{{StorefrontHomePage.url}}" stepKey="goToHomePage"/> -+ <fillField selector="{{StorefrontQuickSearchResultsSection.searchTextBox}}" userInput="s" stepKey="fillAttribute"/> -+ <waitForPageLoad stepKey="waitForSearchTextBox"/> -+ <click selector="{{StorefrontQuickSearchResultsSection.searchTextBoxButton}}" stepKey="clickSearchTextBoxButton"/> -+ <waitForPageLoad stepKey="waitForSearch"/> -+ <see selector="{{StorefrontCategoryMainSection.productName}}" userInput="$$createProduct.name$$" stepKey="seeProductNameInCategoryPage"/> -+ </test> -+</tests> -diff --git a/app/code/Magento/CatalogSearch/Test/Mftf/Test/SearchEntityResultsTest.xml b/app/code/Magento/CatalogSearch/Test/Mftf/Test/SearchEntityResultsTest.xml -new file mode 100644 -index 00000000000..19db201e91f ---- /dev/null -+++ b/app/code/Magento/CatalogSearch/Test/Mftf/Test/SearchEntityResultsTest.xml -@@ -0,0 +1,629 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> -+ <test name="QuickSearchProductBySku"> -+ <annotations> -+ <stories value="Search Product on Storefront"/> -+ <title value="User should be able to use Quick Search to find products"/> -+ <description value="Use Quick Search to find a product"/> -+ <severity value="MAJOR"/> -+ <testCaseId value="MC-14783"/> -+ <group value="CatalogSearch"/> -+ <group value="mtf_migrated"/> -+ </annotations> -+ <before> -+ <createData entity="_defaultCategory" stepKey="createCategory"/> -+ <createData entity="_defaultProduct" stepKey="createSimpleProduct"> -+ <requiredEntity createDataKey="createCategory"/> -+ </createData> -+ </before> -+ <after> -+ <deleteData stepKey="deleteProduct" createDataKey="createSimpleProduct"/> -+ <deleteData stepKey="deleteCategory" createDataKey="createCategory"/> -+ </after> -+ <amOnPage url="{{StorefrontHomePage.url}}" stepKey="goToFrontPage"/> -+ <actionGroup ref="StorefrontCheckQuickSearchStringActionGroup" stepKey="searchStorefront"> -+ <argument name="phrase" value="$createSimpleProduct.sku$"/> -+ </actionGroup> -+ <actionGroup ref="StorefrontOpenProductFromQuickSearch" stepKey="openAndCheckProduct"> -+ <argument name="productName" value="$createSimpleProduct.name$"/> -+ <argument name="productUrlKey" value="$createSimpleProduct.custom_attributes[url_key]$"/> -+ </actionGroup> -+ </test> -+ <test name="QuickSearchProductByName" extends="QuickSearchProductBySku"> -+ <annotations> -+ <stories value="Search Product on Storefront"/> -+ <title value="User should be able to use Quick Search to find products via Name"/> -+ <description value="Use Quick Search to find a product"/> -+ <severity value="MAJOR"/> -+ <testCaseId value="MC-14791"/> -+ <group value="CatalogSearch"/> -+ <group value="mtf_migrated"/> -+ </annotations> -+ <!-- Overwrite search to use name --> -+ <actionGroup ref="StorefrontCheckQuickSearchStringActionGroup" stepKey="searchStorefront"> -+ <argument name="phrase" value="$createSimpleProduct.name$"/> -+ </actionGroup> -+ </test> -+ <test name="QuickSearchProductByNameWithSpecialChars" extends="QuickSearchProductBySku"> -+ <annotations> -+ <stories value="Search Product on Storefront"/> -+ <title value="Quick Search can find products with names that contain special characters"/> -+ <description value="Use Quick Search to find a product by name"/> -+ <severity value="MAJOR"/> -+ <testCaseId value="MC-14792"/> -+ <group value="CatalogSearch"/> -+ <group value="mtf_migrated"/> -+ </annotations> -+ <before> -+ <createData entity="productWithSpecialCharacters" stepKey="createSimpleProduct"> -+ <requiredEntity createDataKey="createCategory"/> -+ </createData> -+ </before> -+ <!-- Overwrite search to use name --> -+ <actionGroup ref="StorefrontCheckQuickSearchStringActionGroup" stepKey="searchStorefront"> -+ <argument name="phrase" value="$createSimpleProduct.name$"/> -+ </actionGroup> -+ </test> -+ <test name="QuickSearchEmptyResults"> -+ <annotations> -+ <stories value="Search Product on Storefront"/> -+ <title value="User should not get search results on query that doesn't return anything"/> -+ <description value="Use invalid query to return no products"/> -+ <severity value="MAJOR"/> -+ <testCaseId value="MC-14793"/> -+ <group value="CatalogSearch"/> -+ <group value="mtf_migrated"/> -+ </annotations> -+ <before> -+ <createData entity="_defaultCategory" stepKey="createCategory"/> -+ <createData entity="_defaultProduct" stepKey="createSimpleProduct"> -+ <requiredEntity createDataKey="createCategory"/> -+ </createData> -+ </before> -+ <after> -+ <deleteData stepKey="deleteProduct" createDataKey="createSimpleProduct"/> -+ <deleteData stepKey="deleteCategory" createDataKey="createCategory"/> -+ </after> -+ <amOnPage url="{{StorefrontHomePage.url}}" stepKey="goToFrontPage"/> -+ <actionGroup ref="StorefrontCheckQuickSearchStringActionGroup" stepKey="searchStorefront"> -+ <argument name="phrase" value="ThisShouldn'tReturnAnything"/> -+ </actionGroup> -+ <actionGroup ref="StorefrontCheckSearchIsEmpty" stepKey="checkEmpty"/> -+ </test> -+ <test name="QuickSearchWithTwoCharsEmptyResults" extends="QuickSearchEmptyResults"> -+ <annotations> -+ <stories value="Search Product on Storefront"/> -+ <title value="User should not get search results on query that only contains two characters"/> -+ <description value="Use of 2 character query to return no products"/> -+ <severity value="MAJOR"/> -+ <testCaseId value="MC-14794"/> -+ <group value="CatalogSearch"/> -+ <group value="mtf_migrated"/> -+ <skip> -+ <issueId value="MC-15827"/> -+ </skip> -+ </annotations> -+ <executeJS function="var s = '$createSimpleProduct.name$'; var ret=s.substring(0,2); return ret;" stepKey="getFirstTwoLetters" before="searchStorefront"/> -+ <actionGroup ref="StorefrontCheckQuickSearchStringActionGroup" stepKey="searchStorefront"> -+ <argument name="phrase" value="{$getFirstTwoLetters}"/> -+ </actionGroup> -+ </test> -+ <test name="QuickSearchProductByNameWithThreeLetters" extends="QuickSearchProductBySku"> -+ <annotations> -+ <stories value="Search Product on Storefront"/> -+ <title value="User should be able to use Quick Search to find products by their first three letters"/> -+ <description value="Use Quick Search to find a product using only first three letters"/> -+ <severity value="MAJOR"/> -+ <testCaseId value="MC-15034"/> -+ <group value="CatalogSearch"/> -+ <group value="mtf_migrated"/> -+ </annotations> -+ <executeJS function="var s = '$createSimpleProduct.name$'; var ret=s.substring(0,3); return ret;" stepKey="getFirstThreeLetters" before="searchStorefront"/> -+ <actionGroup ref="StorefrontCheckQuickSearchStringActionGroup" stepKey="searchStorefront"> -+ <argument name="phrase" value="{$getFirstThreeLetters}"/> -+ </actionGroup> -+ </test> -+ <test name="QuickSearchProductBy128CharQuery" extends="QuickSearchProductBySku"> -+ <annotations> -+ <stories value="Search Product on Storefront"/> -+ <title value="User should be able to use Quick Search product with long names, using first 128 letters"/> -+ <description value="Use Quick Search to find a product with name of 130 length with query of only 128"/> -+ <severity value="MAJOR"/> -+ <testCaseId value="MC-14795"/> -+ <group value="CatalogSearch"/> -+ <group value="mtf_migrated"/> -+ </annotations> -+ <before> -+ <createData entity="productWith130CharName" stepKey="createSimpleProduct"> -+ <requiredEntity createDataKey="createCategory"/> -+ </createData> -+ </before> -+ <executeJS function="var s = '$createSimpleProduct.name$'; var ret=s.substring(0,128); return ret;" stepKey="get128Letters" before="searchStorefront"/> -+ <actionGroup ref="StorefrontCheckQuickSearchStringActionGroup" stepKey="searchStorefront"> -+ <argument name="phrase" value="{$get128Letters}"/> -+ </actionGroup> -+ </test> -+ -+ <test name="QuickSearchTwoProductsWithSameWeight"> -+ <annotations> -+ <stories value="Search Product on Storefront"/> -+ <title value="Quick Search should sort products with the same weight appropriately"/> -+ <description value="Use Quick Search to find a two products with the same weight"/> -+ <severity value="MAJOR"/> -+ <testCaseId value="MC-14796"/> -+ <group value="CatalogSearch"/> -+ <group value="mtf_migrated"/> -+ </annotations> -+ <before> -+ <createData entity="_defaultCategory" stepKey="createCategory"/> -+ <createData entity="productAlphabeticalA" stepKey="product1"> -+ <requiredEntity createDataKey="createCategory"/> -+ </createData> -+ <createData entity="productAlphabeticalB" stepKey="product2"> -+ <requiredEntity createDataKey="createCategory"/> -+ </createData> -+ <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin1"/> -+ -+ -+ <!-- Create and Assign Attribute to product1--> -+ <actionGroup ref="goToProductPageViaID" stepKey="goToProduct1"> -+ <argument name="productId" value="$product1.id$"/> -+ </actionGroup> -+ <actionGroup ref="AdminCreateAttributeWithSearchWeight" stepKey="createProduct1Attribute"> -+ <argument name="attributeType" value="Text Field"/> -+ <argument name="attributeName" value="$product1.name$"/> -+ <argument name="attributeSetName" value="$product1.name$"/> -+ <argument name="weight" value="1"/> -+ <argument name="defaultValue" value="{{_defaultProduct.name}}"/> -+ </actionGroup> -+ <actionGroup ref="AdminProductPageSelectAttributeSet" stepKey="selectAttributeSet1"> -+ <argument name="attributeSetName" value="$product1.name$"/> -+ </actionGroup> -+ <!--fill in default--> -+ <actionGroup ref="saveProductForm" stepKey="saveProduct1a"/> -+ <actionGroup ref="AdminProductPageFillTextAttributeValueByName" stepKey="fillDefault1"> -+ <argument name="attributeName" value="$product1.name$"/> -+ <argument name="value" value="{{_defaultProduct.name}}"/> -+ </actionGroup> -+ <actionGroup ref="saveProductForm" stepKey="saveProduct1b"/> -+ <!-- Create and Assign Attribute to product2--> -+ <actionGroup ref="goToProductPageViaID" stepKey="goToProduct2"> -+ <argument name="productId" value="$product2.id$"/> -+ </actionGroup> -+ <actionGroup ref="AdminCreateAttributeWithSearchWeight" stepKey="createProduct2Attribute"> -+ <argument name="attributeType" value="Text Field"/> -+ <argument name="attributeName" value="$product2.name$"/> -+ <argument name="attributeSetName" value="$product2.name$"/> -+ <argument name="weight" value="1"/> -+ <argument name="defaultValue" value="{{_defaultProduct.name}}"/> -+ </actionGroup> -+ <actionGroup ref="AdminProductPageSelectAttributeSet" stepKey="selectAttributeSet2"> -+ <argument name="attributeSetName" value="$product2.name$"/> -+ </actionGroup> -+ <actionGroup ref="saveProductForm" stepKey="saveProduct2a"/> -+ <!--fill in default--> -+ <actionGroup ref="AdminProductPageFillTextAttributeValueByName" stepKey="fillDefault2"> -+ <argument name="attributeName" value="$product2.name$"/> -+ <argument name="value" value="{{_defaultProduct.name}}"/> -+ </actionGroup> -+ <actionGroup ref="saveProductForm" stepKey="saveProduct2b"/> -+ </before> -+ <after> -+ <deleteData stepKey="deleteProduct1" createDataKey="product1"/> -+ <deleteData stepKey="deleteProduct2" createDataKey="product2"/> -+ <deleteData stepKey="deleteCategory" createDataKey="createCategory"/> -+ </after> -+ <amOnPage url="{{StorefrontHomePage.url}}" stepKey="goToFrontPage"/> -+ <actionGroup ref="StorefrontCheckQuickSearchStringActionGroup" stepKey="searchStorefront"> -+ <argument name="phrase" value="{{_defaultProduct.name}}"/> -+ </actionGroup> -+ <actionGroup ref="StorefrontQuickSearchCheckProductNameInGrid" stepKey="assertProduct1Position"> -+ <argument name="productName" value="$product1.name$"/> -+ <argument name="index" value="2"/> -+ </actionGroup> -+ <actionGroup ref="StorefrontQuickSearchCheckProductNameInGrid" stepKey="assertProduct2Position"> -+ <argument name="productName" value="$product2.name$"/> -+ <argument name="index" value="1"/> -+ </actionGroup> -+ </test> -+ <test name="QuickSearchTwoProductsWithDifferentWeight" extends="QuickSearchTwoProductsWithSameWeight"> -+ <annotations> -+ <stories value="Search Product on Storefront"/> -+ <title value="Quick Search should sort products with the different weight appropriately"/> -+ <description value="Use Quick Search to find a two products with the different weight"/> -+ <severity value="MAJOR"/> -+ <testCaseId value="MC-14797"/> -+ <group value="CatalogSearch"/> -+ <group value="mtf_migrated"/> -+ </annotations> -+ <before> -+ <actionGroup ref="AdminCreateAttributeWithSearchWeight" stepKey="createProduct1Attribute"> -+ <argument name="attributeType" value="Text Field"/> -+ <argument name="attributeName" value="$product1.name$"/> -+ <argument name="attributeSetName" value="$product1.name$"/> -+ <argument name="weight" value="5"/> -+ <argument name="defaultValue" value="{{_defaultProduct.name}}"/> -+ </actionGroup> -+ </before> -+ <actionGroup ref="StorefrontQuickSearchCheckProductNameInGrid" stepKey="assertProduct1Position"> -+ <argument name="productName" value="$product1.name$"/> -+ <argument name="index" value="1"/> -+ </actionGroup> -+ <actionGroup ref="StorefrontQuickSearchCheckProductNameInGrid" stepKey="assertProduct2Position"> -+ <argument name="productName" value="$product2.name$"/> -+ <argument name="index" value="2"/> -+ </actionGroup> -+ </test> -+ -+ <test name="QuickSearchAndAddToCart"> -+ <annotations> -+ <stories value="Search Product on Storefront"/> -+ <title value="User should be able to use Quick Search to find a simple product and add it to cart"/> -+ <description value="Use Quick Search to find simple Product and Add to Cart"/> -+ <severity value="MAJOR"/> -+ <testCaseId value="MC-14784"/> -+ <group value="CatalogSearch"/> -+ <group value="mtf_migrated"/> -+ </annotations> -+ <before> -+ <createData entity="_defaultCategory" stepKey="createCategory"/> -+ <createData entity="_defaultProduct" stepKey="createSimpleProduct"> -+ <requiredEntity createDataKey="createCategory"/> -+ </createData> -+ </before> -+ <after> -+ <deleteData stepKey="deleteProduct" createDataKey="createSimpleProduct"/> -+ <deleteData stepKey="deleteCategory" createDataKey="createCategory"/> -+ </after> -+ <amOnPage url="{{StorefrontHomePage.url}}" stepKey="goToFrontPage"/> -+ <actionGroup ref="StorefrontCheckQuickSearchStringActionGroup" stepKey="searchStorefront"> -+ <argument name="phrase" value="$createSimpleProduct.name$"/> -+ </actionGroup> -+ <actionGroup ref="StorefrontAddToCartFromQuickSearch" stepKey="addProductToCart"> -+ <argument name="productName" value="$createSimpleProduct.name$"/> -+ </actionGroup> -+ </test> -+ <test name="QuickSearchAndAddToCartVirtual"> -+ <annotations> -+ <stories value="Search Product on Storefront"/> -+ <title value="User should be able to use Quick Search to find a virtual product and add it to cart"/> -+ <description value="Use Quick Search to find virtual Product and Add to Cart"/> -+ <severity value="MAJOR"/> -+ <testCaseId value="MC-14785"/> -+ <group value="CatalogSearch"/> -+ <group value="mtf_migrated"/> -+ </annotations> -+ <before> -+ <createData entity="_defaultCategory" stepKey="createCategory"/> -+ <createData entity="VirtualProduct" stepKey="createVirtualProduct"> -+ <requiredEntity createDataKey="createCategory"/> -+ </createData> -+ </before> -+ <after> -+ <deleteData stepKey="deleteProduct" createDataKey="createVirtualProduct"/> -+ <deleteData stepKey="deleteCategory" createDataKey="createCategory"/> -+ </after> -+ <amOnPage url="{{StorefrontHomePage.url}}" stepKey="goToFrontPage"/> -+ <actionGroup ref="StorefrontCheckQuickSearchStringActionGroup" stepKey="searchStorefront"> -+ <argument name="phrase" value="$createVirtualProduct.name$"/> -+ </actionGroup> -+ <actionGroup ref="StorefrontAddToCartFromQuickSearch" stepKey="addProductToCart"> -+ <argument name="productName" value="$createVirtualProduct.name$"/> -+ </actionGroup> -+ </test> -+ <test name="QuickSearchAndAddToCartConfigurable"> -+ <annotations> -+ <stories value="Search Product on Storefront"/> -+ <title value="User should be able to use Quick Search to find a configurable product and add it to cart"/> -+ <description value="Use Quick Search to find configurable Product and Add to Cart"/> -+ <severity value="MAJOR"/> -+ <testCaseId value="MC-14786"/> -+ <group value="CatalogSearch"/> -+ <group value="mtf_migrated"/> -+ </annotations> -+ <before> -+ <createData entity="_defaultCategory" stepKey="createCategory"/> -+ <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin1"/> -+ <actionGroup ref="createConfigurableProduct" stepKey="createProduct"> -+ <argument name="product" value="_defaultProduct"/> -+ <argument name="category" value="$$createCategory$$"/> -+ </actionGroup> -+ </before> -+ <after> -+ <deleteData stepKey="deleteCategory" createDataKey="createCategory"/> -+ <actionGroup ref="deleteProductBySku" stepKey="deleteProduct"> -+ <argument name="sku" value="{{_defaultProduct.sku}}"/> -+ </actionGroup> -+ </after> -+ <amOnPage url="{{StorefrontHomePage.url}}" stepKey="goToFrontPage"/> -+ <actionGroup ref="StorefrontCheckQuickSearchStringActionGroup" stepKey="searchStorefront"> -+ <argument name="phrase" value="{{_defaultProduct.name}}"/> -+ </actionGroup> -+ <actionGroup ref="StorefrontOpenProductFromQuickSearch" stepKey="openAndCheckProduct"> -+ <argument name="productName" value="{{_defaultProduct.name}}"/> -+ <argument name="productUrlKey" value="{{_defaultProduct.urlKey}}"/> -+ </actionGroup> -+ <actionGroup ref="SelectSingleAttributeAndAddToCart" stepKey="addProductToCart"> -+ <argument name="productName" value="{{_defaultProduct.name}}"/> -+ <argument name="attributeCode" value="{{colorProductAttribute.default_label}}"/> -+ <argument name="optionName" value="{{colorProductAttribute1.name}}"/> -+ </actionGroup> -+ </test> -+ <test name="QuickSearchAndAddToCartDownloadable"> -+ <annotations> -+ <stories value="Search Product on Storefront"/> -+ <title value="User should be able to use Quick Search to find a downloadable product and add it to cart"/> -+ <description value="Use Quick Search to find downloadable Product and Add to Cart"/> -+ <severity value="MAJOR"/> -+ <testCaseId value="MC-14787"/> -+ <group value="CatalogSearch"/> -+ <group value="mtf_migrated"/> -+ </annotations> -+ <before> -+ <createData entity="_defaultCategory" stepKey="createCategory"/> -+ <createData entity="DownloadableProductWithOneLink" stepKey="createProduct"> -+ <requiredEntity createDataKey="createCategory"/> -+ </createData> -+ <createData entity="downloadableLink1" stepKey="addDownloadableLink1"> -+ <requiredEntity createDataKey="createProduct"/> -+ </createData> -+ </before> -+ <after> -+ <deleteData stepKey="deleteProduct" createDataKey="createProduct"/> -+ <deleteData stepKey="deleteCategory" createDataKey="createCategory"/> -+ </after> -+ <amOnPage url="{{StorefrontHomePage.url}}" stepKey="goToFrontPage"/> -+ <actionGroup ref="StorefrontCheckQuickSearchStringActionGroup" stepKey="searchStorefront"> -+ <argument name="phrase" value="$createProduct.name$"/> -+ </actionGroup> -+ <actionGroup ref="StorefrontAddToCartFromQuickSearch" stepKey="addProductToCart"> -+ <argument name="productName" value="$createProduct.name$"/> -+ </actionGroup> -+ </test> -+ <test name="QuickSearchAndAddToCartGrouped"> -+ <annotations> -+ <stories value="Search Product on Storefront"/> -+ <title value="User should be able to use Quick Search to find a grouped product and add it to cart"/> -+ <description value="Use Quick Search to find grouped Product and Add to Cart"/> -+ <severity value="MAJOR"/> -+ <testCaseId value="MC-14788"/> -+ <group value="CatalogSearch"/> -+ <group value="mtf_migrated"/> -+ </annotations> -+ <before> -+ <createData entity="ApiProductWithDescription" stepKey="simple1"/> -+ <createData entity="ApiGroupedProduct" stepKey="createProduct"/> -+ <createData entity="OneSimpleProductLink" stepKey="addProductOne"> -+ <requiredEntity createDataKey="createProduct"/> -+ <requiredEntity createDataKey="simple1"/> -+ </createData> -+ </before> -+ <after> -+ <deleteData stepKey="deleteProduct" createDataKey="createProduct"/> -+ </after> -+ <amOnPage url="{{StorefrontHomePage.url}}" stepKey="goToFrontPage"/> -+ <actionGroup ref="StorefrontCheckQuickSearchStringActionGroup" stepKey="searchStorefront"> -+ <argument name="phrase" value="$createProduct.name$"/> -+ </actionGroup> -+ <actionGroup ref="StorefrontAddToCartFromQuickSearch" stepKey="addProductToCart"> -+ <argument name="productName" value="$createProduct.name$"/> -+ </actionGroup> -+ </test> -+ <test name="QuickSearchAndAddToCartBundleDynamic"> -+ <annotations> -+ <stories value="Search Product on Storefront"/> -+ <title value="User should be able to use Quick Search to find a Bundle Dynamic product and add it to cart"/> -+ <description value="Use Quick Search to find Bundle Dynamic Product and Add to Cart"/> -+ <severity value="MAJOR"/> -+ <testCaseId value="MC-14789"/> -+ <group value="CatalogSearch"/> -+ <group value="mtf_migrated"/> -+ </annotations> -+ <before> -+ <createData entity="_defaultCategory" stepKey="createCategory"/> -+ <createData entity="_defaultProduct" stepKey="createProduct"> -+ <requiredEntity createDataKey="createCategory"/> -+ </createData> -+ -+ <!--Create dynamic product--> -+ <createData entity="ApiBundleProductPriceViewRange" stepKey="createBundleProduct"> -+ <requiredEntity createDataKey="createCategory"/> -+ </createData> -+ <createData entity="DropDownBundleOption" stepKey="bundleOption"> -+ <requiredEntity createDataKey="createBundleProduct"/> -+ </createData> -+ <createData entity="ApiBundleLink" stepKey="createBundleLink1"> -+ <requiredEntity createDataKey="createBundleProduct"/> -+ <requiredEntity createDataKey="bundleOption"/> -+ <requiredEntity createDataKey="createProduct"/> -+ <field key="qty">10</field> -+ </createData> -+ <!--Finish bundle creation--> -+ <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> -+ <amOnPage url="{{AdminProductEditPage.url($$createBundleProduct.id$$)}}" stepKey="goToProductEditPage"/> -+ <actionGroup ref="saveProductForm" stepKey="saveProduct"/> -+ </before> -+ <after> -+ <deleteData stepKey="deleteBundleProduct" createDataKey="createBundleProduct"/> -+ <deleteData stepKey="deleteProduct" createDataKey="createProduct"/> -+ <deleteData stepKey="deleteCategory" createDataKey="createCategory"/> -+ </after> -+ <amOnPage url="{{StorefrontHomePage.url}}" stepKey="goToFrontPage"/> -+ <actionGroup ref="StorefrontCheckQuickSearchStringActionGroup" stepKey="searchStorefront"> -+ <argument name="phrase" value="$createBundleProduct.name$"/> -+ </actionGroup> -+ <actionGroup ref="StorefrontOpenProductFromQuickSearch" stepKey="openAndCheckProduct"> -+ <argument name="productName" value="$createBundleProduct.name$"/> -+ <argument name="productUrlKey" value="$createBundleProduct.custom_attributes[url_key]$"/> -+ </actionGroup> -+ <actionGroup ref="StorefrontAddBundleProductFromProductToCartActionGroup" stepKey="addProductToCart"> -+ <argument name="productName" value="$createBundleProduct.name$"/> -+ </actionGroup> -+ </test> -+ <test name="QuickSearchAndAddToCartBundleFixed"> -+ <annotations> -+ <stories value="Search Product on Storefront"/> -+ <title value="User should be able to use Quick Search to find a Bundle Fixed product and add it to cart"/> -+ <description value="Use Quick Search to find Bundle Fixed Product and Add to Cart"/> -+ <severity value="MAJOR"/> -+ <testCaseId value="MC-14790"/> -+ <group value="CatalogSearch"/> -+ <group value="mtf_migrated"/> -+ </annotations> -+ <before> -+ <createData entity="_defaultCategory" stepKey="createCategory"/> -+ <createData entity="_defaultProduct" stepKey="createProduct"> -+ <requiredEntity createDataKey="createCategory"/> -+ </createData> -+ -+ <!--Create fixed product--> -+ <!--Create 2 simple products--> -+ <createData entity="SimpleProduct2" stepKey="simpleProduct1"/> -+ <createData entity="SimpleProduct2" stepKey="simpleProduct2"/> -+ <!-- Create the bundle product based --> -+ <createData entity="ApiFixedBundleProduct" stepKey="createBundleProduct"/> -+ <createData entity="MultipleSelectOption" stepKey="createBundleOption1_1"> -+ <requiredEntity createDataKey="createBundleProduct"/> -+ <field key="required">false</field> -+ </createData> -+ <createData entity="CheckboxOption" stepKey="createBundleOption1_2"> -+ <requiredEntity createDataKey="createBundleProduct"/> -+ </createData> -+ <createData entity="ApiBundleLink" stepKey="linkOptionToProduct"> -+ <requiredEntity createDataKey="createBundleProduct"/> -+ <requiredEntity createDataKey="createBundleOption1_1"/> -+ <requiredEntity createDataKey="simpleProduct1"/> -+ </createData> -+ <createData entity="ApiBundleLink" stepKey="linkOptionToProduct2"> -+ <requiredEntity createDataKey="createBundleProduct"/> -+ <requiredEntity createDataKey="createBundleOption1_1"/> -+ <requiredEntity createDataKey="simpleProduct2"/> -+ </createData> -+ -+ <!--Finish bundle creation--> -+ <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> -+ <amOnPage url="{{AdminProductEditPage.url($$createBundleProduct.id$$)}}" stepKey="goToProductEditPage"/> -+ <actionGroup ref="saveProductForm" stepKey="saveProduct"/> -+ </before> -+ <after> -+ <deleteData stepKey="deleteBundleProduct" createDataKey="createBundleProduct"/> -+ <deleteData stepKey="deleteProduct" createDataKey="createProduct"/> -+ <deleteData stepKey="deleteCategory" createDataKey="createCategory"/> -+ </after> -+ <comment userInput="$simpleProduct1.name$" stepKey="asdf"/> -+ <amOnPage url="{{StorefrontHomePage.url}}" stepKey="goToFrontPage"/> -+ <actionGroup ref="StorefrontCheckQuickSearchStringActionGroup" stepKey="searchStorefront"> -+ <argument name="phrase" value="$createBundleProduct.name$"/> -+ </actionGroup> -+ <actionGroup ref="StorefrontOpenProductFromQuickSearch" stepKey="openAndCheckProduct"> -+ <argument name="productName" value="$createBundleProduct.name$"/> -+ <argument name="productUrlKey" value="$createBundleProduct.custom_attributes[url_key]$"/> -+ </actionGroup> -+ <actionGroup ref="StorefrontAddBundleProductFromProductToCartWithMultiOption" stepKey="addProductToCart"> -+ <argument name="productName" value="$createBundleProduct.name$"/> -+ <argument name="optionName" value="$createBundleOption1_1.name$"/> -+ <argument name="value" value="$simpleProduct1.name$"/> -+ </actionGroup> -+ </test> -+ -+ <test name="QuickSearchConfigurableChildren"> -+ <annotations> -+ <stories value="Search Product on Storefront"/> -+ <title value="User should be able to use Quick Search to a configurable product's child products"/> -+ <description value="Use Quick Search to find a configurable product with enabled/disable children"/> -+ <severity value="MAJOR"/> -+ <testCaseId value="MC-14798"/> -+ <group value="CatalogSearch"/> -+ <group value="mtf_migrated"/> -+ <skip> -+ <issueId value="MC-15101"/> -+ </skip> -+ </annotations> -+ <before> -+ <!-- Create the category --> -+ <createData entity="ApiCategory" stepKey="createCategory"/> -+ -+ <!-- Create blank AttributeSet--> -+ <createData entity="CatalogAttributeSet" stepKey="attributeSet"/> -+ -+ <!-- Create an attribute with two options to be used in the first child product --> -+ <createData entity="hiddenDropdownAttributeWithOptions" stepKey="createConfigProductAttribute"/> -+ <createData entity="productAttributeOption1" stepKey="createConfigProductAttributeOption1"> -+ <requiredEntity createDataKey="createConfigProductAttribute"/> -+ </createData> -+ -+ <!-- Assign attribute to set --> -+ <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> -+ <actionGroup ref="goToAttributeGridPage" stepKey="goToPage"/> -+ <actionGroup ref="goToAttributeSetByName" stepKey="goToSet"> -+ <argument name="name" value="$attributeSet.attribute_set_name$"/> -+ </actionGroup> -+ <actionGroup ref="AssignAttributeToGroup" stepKey="assignToAttributeSetAndGroup"> -+ <argument name="group" value="Product Details"/> -+ <argument name="attribute" value="$createConfigProductAttribute.attribute_code$"/> -+ </actionGroup> -+ <actionGroup ref="SaveAttributeSet" stepKey="savePage"/> -+ -+ <!-- Get the first option of the attribute we created --> -+ <getData entity="ProductAttributeOptionGetter" index="1" stepKey="getConfigAttributeOption1"> -+ <requiredEntity createDataKey="createConfigProductAttribute"/> -+ </getData> -+ -+ <!-- Create a simple product,give it the attributeSet and attribute with the first option --> -+ <createData entity="ApiSimpleOneHidden" stepKey="createConfigChildProduct1"> -+ <field key="attribute_set_id">$attributeSet.attribute_set_id$</field> -+ <requiredEntity createDataKey="createConfigProductAttribute"/> -+ <requiredEntity createDataKey="getConfigAttributeOption1"/> -+ </createData> -+ <updateData entity="ApiSimpleProductUpdateDescription" stepKey="updateSimpleProduct1" createDataKey="createConfigChildProduct1"/> -+ -+ <!-- Create the configurable product, give it the attributeSet and add it to the category --> -+ <createData entity="ApiConfigurableProduct" stepKey="createConfigProduct"> -+ <field key="attribute_set_id">$attributeSet.attribute_set_id$</field> -+ <requiredEntity createDataKey="createCategory"/> -+ </createData> -+ <!-- Create the configurable product --> -+ <createData entity="ConfigurableProductOneOption" stepKey="createConfigProductOption"> -+ <requiredEntity createDataKey="createConfigProduct"/> -+ <requiredEntity createDataKey="createConfigProductAttribute"/> -+ <requiredEntity createDataKey="getConfigAttributeOption1"/> -+ </createData> -+ <!-- Add the first simple product to the configurable product --> -+ <createData entity="ConfigurableProductAddChild" stepKey="createConfigProductAddChild1"> -+ <requiredEntity createDataKey="createConfigProduct"/> -+ <requiredEntity createDataKey="createConfigChildProduct1"/> -+ </createData> -+ </before> -+ <amOnPage url="{{StorefrontHomePage.url}}" stepKey="goToFrontPage"/> -+ <actionGroup ref="StorefrontCheckQuickSearchStringActionGroup" stepKey="searchStorefront"> -+ <argument name="phrase" value="$createConfigProduct.name$"/> -+ </actionGroup> -+ <actionGroup ref="StorefrontQuickSearchCheckProductNameInGrid" stepKey="seeProductInGrid"> -+ <argument name="productName" value="$createConfigProduct.name$"/> -+ <argument name="index" value="1"/> -+ </actionGroup> -+ -+ <!-- Disable Child Product --> -+ <actionGroup ref="goToProductPageViaID" stepKey="goToChildProduct"> -+ <argument name="productId" value="$createConfigChildProduct1.id$"/> -+ </actionGroup> -+ <actionGroup ref="toggleProductEnabled" stepKey="disableProduct"/> -+ <actionGroup ref="saveProductForm" stepKey="saveProduct"/> -+ -+ <amOnPage url="{{StorefrontHomePage.url}}" stepKey="goToFrontPageAgain"/> -+ <actionGroup ref="StorefrontCheckQuickSearchStringActionGroup" stepKey="searchStorefrontAgain"> -+ <argument name="phrase" value="$createConfigProduct.name$"/> -+ </actionGroup> -+ <actionGroup ref="StorefrontQuickSearchCheckProductNameNotInGrid" stepKey="dontSeeProductAnymore"> -+ <argument name="productName" value="$createConfigProduct.name$"/> -+ </actionGroup> -+ </test> -+</tests> -diff --git a/app/code/Magento/CatalogSearch/Test/Mftf/Test/StorefrontUpdateSearchTermEntityTest.xml b/app/code/Magento/CatalogSearch/Test/Mftf/Test/StorefrontUpdateSearchTermEntityTest.xml -new file mode 100644 -index 00000000000..3ebb09f3c9c ---- /dev/null -+++ b/app/code/Magento/CatalogSearch/Test/Mftf/Test/StorefrontUpdateSearchTermEntityTest.xml -@@ -0,0 +1,72 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> -+ <test name="StorefrontUpdateSearchTermEntityTest"> -+ <annotations> -+ <stories value="Storefront Search"/> -+ <title value="Update Storefront Search Results"/> -+ <description value="You should see the updated Search Term on the Storefront via the Admin."/> -+ <testCaseId value="MC-13987"/> -+ <severity value="CRITICAL"/> -+ <group value="search"/> -+ <group value="mtf_migrated"/> -+ </annotations> -+ -+ <before> -+ <createData entity="_defaultCategory" stepKey="createCategory1"/> -+ <createData entity="SimpleProduct" stepKey="createProduct1"> -+ <requiredEntity createDataKey="createCategory1"/> -+ </createData> -+ -+ <amOnPage url="{{StorefrontHomePage.url}}" stepKey="amOnStorefrontPage1"/> -+ <waitForPageLoad stepKey="waitForPageLoad1"/> -+ </before> -+ <after> -+ <actionGroup ref="logout" stepKey="logoutOfAdmin1"/> -+ -+ <deleteData createDataKey="createProduct1" stepKey="deleteSimpleProduct1"/> -+ <deleteData createDataKey="createCategory1" stepKey="deleteCategory1"/> -+ </after> -+ -+ <actionGroup ref="StorefrontCheckQuickSearchStringActionGroup" stepKey="quickSearchByProductName1"> -+ <argument name="phrase" value="$$createProduct1.name$$"/> -+ </actionGroup> -+ -+ <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin1"/> -+ -+ <amOnPage url="{{AdminCatalogSearchTermIndexPage.url}}" stepKey="openAdminCatalogSearchTermIndexPage1"/> -+ <waitForPageLoad stepKey="waitForPageLoad1"/> -+ -+ <actionGroup ref="searchTermFilterBySearchQuery" stepKey="filterByFirstSearchQuery1"> -+ <argument name="searchQuery" value="$$createProduct1.name$$"/> -+ </actionGroup> -+ -+ <click selector="{{AdminGridRow.editByValue($$createProduct1.name$$)}}" stepKey="clickOnSearchResult1"/> -+ <waitForPageLoad stepKey="waitForPageLoad2"/> -+ -+ <actionGroup ref="AdminFillAllSearchTermFieldsActionGroup" stepKey="searchForSearchTerm1"> -+ <argument name="searchTerm" value="UpdatedSearchTermData1"/> -+ </actionGroup> -+ -+ <amOnPage url="{{AdminCatalogSearchTermIndexPage.url}}" stepKey="openAdminCatalogSearchTermIndexPage2"/> -+ <waitForPageLoad stepKey="waitForPageLoad3"/> -+ -+ <actionGroup ref="searchTermFilterBySearchQuery" stepKey="filterByFirstSearchQuery2"> -+ <argument name="searchQuery" value="{{UpdatedSearchTermData1.query_text}}"/> -+ </actionGroup> -+ -+ <amOnPage url="{{StorefrontHomePage.url}}" stepKey="amOnStorefrontPage2"/> -+ <waitForPageLoad stepKey="waitForPageLoad4"/> -+ -+ <actionGroup ref="StorefrontCheckQuickSearchStringActionGroup" stepKey="quickSearchByProductName2"> -+ <argument name="phrase" value="{{UpdatedSearchTermData1.query_text}}"/> -+ </actionGroup> -+ </test> -+</tests> -diff --git a/app/code/Magento/CatalogSearch/Test/Unit/Controller/Advanced/ResultTest.php b/app/code/Magento/CatalogSearch/Test/Unit/Controller/Advanced/ResultTest.php -index 891f008979e..8d1dbbd6dd6 100644 ---- a/app/code/Magento/CatalogSearch/Test/Unit/Controller/Advanced/ResultTest.php -+++ b/app/code/Magento/CatalogSearch/Test/Unit/Controller/Advanced/ResultTest.php -@@ -10,12 +10,27 @@ namespace Magento\CatalogSearch\Test\Unit\Controller\Advanced; - */ - class ResultTest extends \PHPUnit\Framework\TestCase - { -+ /** -+ * Test result action filters set before load layout scenario -+ * -+ * @return void -+ */ - public function testResultActionFiltersSetBeforeLoadLayout() - { - $filters = null; - $expectedQuery = 'filtersData'; - -- $view = $this->createPartialMock(\Magento\Framework\App\View::class, ['loadLayout', 'renderLayout']); -+ $view = $this->createPartialMock( -+ \Magento\Framework\App\View::class, -+ ['loadLayout', 'renderLayout', 'getPage', 'getLayout'] -+ ); -+ $update = $this->createPartialMock(\Magento\Framework\View\Model\Layout\Merge::class, ['getHandles']); -+ $update->expects($this->once())->method('getHandles')->will($this->returnValue([])); -+ $layout = $this->createPartialMock(\Magento\Framework\View\Result\Layout::class, ['getUpdate']); -+ $layout->expects($this->once())->method('getUpdate')->will($this->returnValue($update)); -+ $view->expects($this->once())->method('getLayout')->will($this->returnValue($layout)); -+ $page = $this->createPartialMock(\Magento\Framework\View\Result\Page::class, ['initLayout']); -+ $view->expects($this->once())->method('getPage')->will($this->returnValue($page)); - $view->expects($this->once())->method('loadLayout')->will( - $this->returnCallback( - function () use (&$filters, $expectedQuery) { -@@ -53,6 +68,11 @@ class ResultTest extends \PHPUnit\Framework\TestCase - $instance->execute(); - } - -+ /** -+ * Test url set on exception scenario -+ * -+ * @return void -+ */ - public function testUrlSetOnException() - { - $redirectResultMock = $this->createMock(\Magento\Framework\Controller\Result\Redirect::class); -@@ -131,11 +151,62 @@ class ResultTest extends \PHPUnit\Framework\TestCase - /** @var \Magento\CatalogSearch\Controller\Advanced\Result $instance */ - $instance = $objectManager->getObject( - \Magento\CatalogSearch\Controller\Advanced\Result::class, -- ['context' => $contextMock, -- 'catalogSearchAdvanced' => $catalogSearchAdvanced, -- 'urlFactory' => $urlFactoryMock -+ [ -+ 'context' => $contextMock, -+ 'catalogSearchAdvanced' => $catalogSearchAdvanced, -+ 'urlFactory' => $urlFactoryMock - ] - ); - $this->assertEquals($redirectResultMock, $instance->execute()); - } -+ -+ /** -+ * Test no result handle scenario -+ * -+ * @return void -+ */ -+ public function testNoResultsHandle() -+ { -+ $expectedQuery = 'notExistTerm'; -+ -+ $update = $this->createPartialMock(\Magento\Framework\View\Model\Layout\Merge::class, ['getHandles']); -+ $update->expects($this->once())->method('getHandles')->will($this->returnValue([])); -+ -+ $layout = $this->createPartialMock(\Magento\Framework\View\Result\Layout::class, ['getUpdate']); -+ $layout->expects($this->once())->method('getUpdate')->will($this->returnValue($update)); -+ -+ $page = $this->createPartialMock(\Magento\Framework\View\Result\Page::class, ['initLayout']); -+ -+ $view = $this->createPartialMock( -+ \Magento\Framework\App\View::class, -+ ['loadLayout', 'renderLayout', 'getPage', 'getLayout'] -+ ); -+ -+ $view->expects($this->once())->method('loadLayout') -+ ->with([\Magento\CatalogSearch\Controller\Advanced\Result::DEFAULT_NO_RESULT_HANDLE]); -+ -+ $view->expects($this->once())->method('getPage')->will($this->returnValue($page)); -+ $view->expects($this->once())->method('getLayout')->will($this->returnValue($layout)); -+ -+ $request = $this->createPartialMock(\Magento\Framework\App\Console\Request::class, ['getQueryValue']); -+ $request->expects($this->once())->method('getQueryValue')->will($this->returnValue($expectedQuery)); -+ -+ $catalogSearchAdvanced = $this->createPartialMock( -+ \Magento\CatalogSearch\Model\Advanced::class, -+ ['addFilters', '__wakeup', 'getProductCollection'] -+ ); -+ -+ $objectManager = new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this); -+ $context = $objectManager->getObject( -+ \Magento\Framework\App\Action\Context::class, -+ ['view' => $view, 'request' => $request] -+ ); -+ -+ /** @var \Magento\CatalogSearch\Controller\Advanced\Result $instance */ -+ $instance = $objectManager->getObject( -+ \Magento\CatalogSearch\Controller\Advanced\Result::class, -+ ['context' => $context, 'catalogSearchAdvanced' => $catalogSearchAdvanced] -+ ); -+ $instance->execute(); -+ } - } -diff --git a/app/code/Magento/CatalogSearch/Test/Unit/Model/Adapter/Mysql/Aggregation/DataProvider/QueryBuilderTest.php b/app/code/Magento/CatalogSearch/Test/Unit/Model/Adapter/Mysql/Aggregation/DataProvider/QueryBuilderTest.php -index 72379c3819d..949554506d5 100644 ---- a/app/code/Magento/CatalogSearch/Test/Unit/Model/Adapter/Mysql/Aggregation/DataProvider/QueryBuilderTest.php -+++ b/app/code/Magento/CatalogSearch/Test/Unit/Model/Adapter/Mysql/Aggregation/DataProvider/QueryBuilderTest.php -@@ -20,6 +20,8 @@ use Magento\Store\Model\Store; - * Test for Magento\CatalogSearch\Model\Adapter\Mysql\Aggregation\DataProvider\QueryBuilder. - * - * @SuppressWarnings(PHPMD.CouplingBetweenObjects) -+ * @deprecated -+ * @see \Magento\ElasticSearch - */ - class QueryBuilderTest extends \PHPUnit\Framework\TestCase - { -diff --git a/app/code/Magento/CatalogSearch/Test/Unit/Model/Adapter/Mysql/Aggregation/DataProviderTest.php b/app/code/Magento/CatalogSearch/Test/Unit/Model/Adapter/Mysql/Aggregation/DataProviderTest.php -index 8eeceba8209..e3cc3e1d183 100644 ---- a/app/code/Magento/CatalogSearch/Test/Unit/Model/Adapter/Mysql/Aggregation/DataProviderTest.php -+++ b/app/code/Magento/CatalogSearch/Test/Unit/Model/Adapter/Mysql/Aggregation/DataProviderTest.php -@@ -20,9 +20,13 @@ use Magento\Framework\Search\Request\Dimension; - use Magento\Eav\Model\Entity\Attribute; - use Magento\Catalog\Model\Product; - use Magento\Framework\DB\Ddl\Table; -+use Magento\Framework\Event\Manager; - - /** - * @SuppressWarnings(PHPMD.CouplingBetweenObjects) -+ * -+ * @deprecated -+ * @see \Magento\ElasticSearch - */ - class DataProviderTest extends \PHPUnit\Framework\TestCase - { -@@ -61,6 +65,11 @@ class DataProviderTest extends \PHPUnit\Framework\TestCase - */ - private $selectBuilderForAttribute; - -+ /** -+ * @var Manager|\PHPUnit_Framework_MockObject_MockObject -+ */ -+ private $eventManager; -+ - protected function setUp() - { - $this->eavConfigMock = $this->createMock(Config::class); -@@ -70,12 +79,14 @@ class DataProviderTest extends \PHPUnit\Framework\TestCase - $this->adapterMock = $this->createMock(AdapterInterface::class); - $this->resourceConnectionMock->expects($this->once())->method('getConnection')->willReturn($this->adapterMock); - $this->selectBuilderForAttribute = $this->createMock(SelectBuilderForAttribute::class); -+ $this->eventManager = $this->createMock(Manager::class); - $this->model = new DataProvider( - $this->eavConfigMock, - $this->resourceConnectionMock, - $this->scopeResolverMock, - $this->sessionMock, -- $this->selectBuilderForAttribute -+ $this->selectBuilderForAttribute, -+ $this->eventManager - ); - } - -@@ -99,6 +110,7 @@ class DataProviderTest extends \PHPUnit\Framework\TestCase - - $selectMock = $this->createMock(Select::class); - $this->adapterMock->expects($this->atLeastOnce())->method('select')->willReturn($selectMock); -+ $this->eventManager->expects($this->once())->method('dispatch')->willReturn($selectMock); - $tableMock = $this->createMock(Table::class); - - $this->model->getDataSet($bucketMock, ['scope' => $dimensionMock], $tableMock); -@@ -126,6 +138,7 @@ class DataProviderTest extends \PHPUnit\Framework\TestCase - $selectMock = $this->createMock(Select::class); - $this->selectBuilderForAttribute->expects($this->once())->method('build')->willReturn($selectMock); - $this->adapterMock->expects($this->atLeastOnce())->method('select')->willReturn($selectMock); -+ $this->eventManager->expects($this->once())->method('dispatch')->willReturn($selectMock); - $tableMock = $this->createMock(Table::class); - $this->model->getDataSet($bucketMock, ['scope' => $dimensionMock], $tableMock); - } -diff --git a/app/code/Magento/CatalogSearch/Test/Unit/Model/Adapter/Mysql/Dynamic/DataProviderTest.php b/app/code/Magento/CatalogSearch/Test/Unit/Model/Adapter/Mysql/Dynamic/DataProviderTest.php -index 1186dd6936c..b064eec3338 100644 ---- a/app/code/Magento/CatalogSearch/Test/Unit/Model/Adapter/Mysql/Dynamic/DataProviderTest.php -+++ b/app/code/Magento/CatalogSearch/Test/Unit/Model/Adapter/Mysql/Dynamic/DataProviderTest.php -@@ -19,8 +19,9 @@ use Magento\Framework\Search\Dynamic\EntityStorage; - use Magento\Store\Model\StoreManager; - - /** -- * Class DataProviderTest - * @SuppressWarnings(PHPMD.CouplingBetweenObjects) -+ * @deprecated -+ * @see \Magento\ElasticSearch - */ - class DataProviderTest extends \PHPUnit\Framework\TestCase - { -diff --git a/app/code/Magento/CatalogSearch/Test/Unit/Model/Adapter/Mysql/Field/ResolverTest.php b/app/code/Magento/CatalogSearch/Test/Unit/Model/Adapter/Mysql/Field/ResolverTest.php -index ca19e5e995c..1e609cdeac2 100644 ---- a/app/code/Magento/CatalogSearch/Test/Unit/Model/Adapter/Mysql/Field/ResolverTest.php -+++ b/app/code/Magento/CatalogSearch/Test/Unit/Model/Adapter/Mysql/Field/ResolverTest.php -@@ -9,6 +9,9 @@ use Magento\Framework\Search\Adapter\Mysql\Field\FieldInterface; - - /** - * Unit tests for Magento\CatalogSearch\Model\Adapter\Mysql\Field\Resolver class. -+ * -+ * @deprecated -+ * @see \Magento\ElasticSearch - */ - class ResolverTest extends \PHPUnit\Framework\TestCase - { -diff --git a/app/code/Magento/CatalogSearch/Test/Unit/Model/Adapter/Mysql/Filter/AliasResolverTest.php b/app/code/Magento/CatalogSearch/Test/Unit/Model/Adapter/Mysql/Filter/AliasResolverTest.php -index 697fab65079..1690d9f3936 100644 ---- a/app/code/Magento/CatalogSearch/Test/Unit/Model/Adapter/Mysql/Filter/AliasResolverTest.php -+++ b/app/code/Magento/CatalogSearch/Test/Unit/Model/Adapter/Mysql/Filter/AliasResolverTest.php -@@ -9,6 +9,10 @@ namespace Magento\CatalogSearch\Test\Unit\Model\Adapter\Mysql\Filter; - use Magento\CatalogSearch\Model\Search\RequestGenerator; - use Magento\Framework\TestFramework\Unit\Helper\ObjectManager as ObjectManagerHelper; - -+/** -+ * @deprecated -+ * @see \Magento\ElasticSearch -+ */ - class AliasResolverTest extends \PHPUnit\Framework\TestCase - { - /** -diff --git a/app/code/Magento/CatalogSearch/Test/Unit/Model/Adapter/Mysql/Filter/PreprocessorTest.php b/app/code/Magento/CatalogSearch/Test/Unit/Model/Adapter/Mysql/Filter/PreprocessorTest.php -index 01108358da2..7e3de7534e8 100644 ---- a/app/code/Magento/CatalogSearch/Test/Unit/Model/Adapter/Mysql/Filter/PreprocessorTest.php -+++ b/app/code/Magento/CatalogSearch/Test/Unit/Model/Adapter/Mysql/Filter/PreprocessorTest.php -@@ -15,6 +15,8 @@ use PHPUnit_Framework_MockObject_MockObject as MockObject; - - /** - * @SuppressWarnings(PHPMD.CouplingBetweenObjects) -+ * @deprecated -+ * @see \Magento\ElasticSearch - */ - class PreprocessorTest extends \PHPUnit\Framework\TestCase - { -diff --git a/app/code/Magento/CatalogSearch/Test/Unit/Model/AdvancedTest.php b/app/code/Magento/CatalogSearch/Test/Unit/Model/AdvancedTest.php -index fc5915bb3cd..a4f62ce83a1 100644 ---- a/app/code/Magento/CatalogSearch/Test/Unit/Model/AdvancedTest.php -+++ b/app/code/Magento/CatalogSearch/Test/Unit/Model/AdvancedTest.php -@@ -250,6 +250,7 @@ class AdvancedTest extends \PHPUnit\Framework\TestCase - 'productCollectionFactory' => $productCollectionFactory, - 'storeManager' => $this->storeManager, - 'currencyFactory' => $currencyFactory, -+ 'collectionProvider' => null - ] - ); - $instance->addFilters($values); -diff --git a/app/code/Magento/CatalogSearch/Test/Unit/Model/Indexer/FulltextTest.php b/app/code/Magento/CatalogSearch/Test/Unit/Model/Indexer/FulltextTest.php -index d7129b9c224..f70c61cdbaf 100644 ---- a/app/code/Magento/CatalogSearch/Test/Unit/Model/Indexer/FulltextTest.php -+++ b/app/code/Magento/CatalogSearch/Test/Unit/Model/Indexer/FulltextTest.php -@@ -116,6 +116,7 @@ class FulltextTest extends \PHPUnit\Framework\TestCase - ->willReturn($ids); - $this->saveHandler->expects($this->exactly(count($stores)))->method('deleteIndex'); - $this->saveHandler->expects($this->exactly(2))->method('saveIndex'); -+ $this->saveHandler->expects($this->exactly(2))->method('isAvailable')->willReturn(true); - $consecutiveStoreRebuildArguments = array_map( - function ($store) use ($ids) { - return [$store, $ids]; -@@ -186,6 +187,7 @@ class FulltextTest extends \PHPUnit\Framework\TestCase - ->willReturn($ids); - $this->saveHandler->expects($this->exactly(count($stores)))->method('deleteIndex'); - $this->saveHandler->expects($this->exactly(2))->method('saveIndex'); -+ $this->saveHandler->expects($this->exactly(2))->method('isAvailable')->willReturn(true); - $this->fullAction->expects($this->exactly(2)) - ->method('rebuildStoreIndex') - ->willReturn(new \ArrayObject([$indexData, $indexData])); -@@ -204,6 +206,7 @@ class FulltextTest extends \PHPUnit\Framework\TestCase - ->willReturn([$id]); - $this->saveHandler->expects($this->exactly(count($stores)))->method('deleteIndex'); - $this->saveHandler->expects($this->exactly(2))->method('saveIndex'); -+ $this->saveHandler->expects($this->exactly(2))->method('isAvailable')->willReturn(true); - $this->fullAction->expects($this->exactly(2)) - ->method('rebuildStoreIndex') - ->willReturn(new \ArrayObject([$indexData, $indexData])); -diff --git a/app/code/Magento/CatalogSearch/Test/Unit/Model/Layer/Filter/AttributeTest.php b/app/code/Magento/CatalogSearch/Test/Unit/Model/Layer/Filter/AttributeTest.php -index abc0fdd1069..69e2c33d02d 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 @@ class AttributeTest extends \PHPUnit\Framework\TestCase - ->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 @@ class AttributeTest extends \PHPUnit\Framework\TestCase - $this->fulltextCollection->expects($this->once()) - ->method('getFacetedData') - ->willReturn($facetedData); -- $this->fulltextCollection->expects($this->once()) -- ->method('getSize') -- ->will($this->returnValue(50)); - - $this->itemDataBuilder->expects($this->once()) - ->method('addItemData') -diff --git a/app/code/Magento/CatalogSearch/Test/Unit/Model/ResourceModel/Advanced/CollectionTest.php b/app/code/Magento/CatalogSearch/Test/Unit/Model/ResourceModel/Advanced/CollectionTest.php -index 21d67bdf53c..683070c2862 100644 ---- a/app/code/Magento/CatalogSearch/Test/Unit/Model/ResourceModel/Advanced/CollectionTest.php -+++ b/app/code/Magento/CatalogSearch/Test/Unit/Model/ResourceModel/Advanced/CollectionTest.php -@@ -7,12 +7,20 @@ namespace Magento\CatalogSearch\Test\Unit\Model\ResourceModel\Advanced; - - use Magento\Catalog\Model\Product; - use Magento\Catalog\Model\ResourceModel\Product\Collection\ProductLimitationFactory; -+use Magento\CatalogSearch\Model\ResourceModel\Fulltext\Collection\SearchCriteriaResolverFactory; -+use Magento\CatalogSearch\Model\ResourceModel\Fulltext\Collection\SearchCriteriaResolverInterface; -+use Magento\CatalogSearch\Model\ResourceModel\Fulltext\Collection\SearchResultApplierInterface; -+use Magento\CatalogSearch\Model\ResourceModel\Fulltext\Collection\TotalRecordsResolverInterface; - use Magento\CatalogSearch\Test\Unit\Model\ResourceModel\BaseCollection; -+use Magento\CatalogSearch\Model\ResourceModel\Fulltext\Collection\SearchResultApplierFactory; -+use Magento\CatalogSearch\Model\ResourceModel\Fulltext\Collection\TotalRecordsResolverFactory; - - /** - * Tests Magento\CatalogSearch\Model\ResourceModel\Advanced\Collection - * - * @SuppressWarnings(PHPMD.CouplingBetweenObjects) -+ * @deprecated -+ * @see \Magento\ElasticSearch - */ - class CollectionTest extends BaseCollection - { -@@ -77,6 +85,42 @@ class CollectionTest extends BaseCollection - $productLimitationFactoryMock->method('create') - ->willReturn($productLimitationMock); - -+ $searchCriteriaResolver = $this->getMockBuilder(SearchCriteriaResolverInterface::class) -+ ->disableOriginalConstructor() -+ ->setMethods(['resolve']) -+ ->getMockForAbstractClass(); -+ $searchCriteriaResolverFactory = $this->getMockBuilder(SearchCriteriaResolverFactory::class) -+ ->disableOriginalConstructor() -+ ->setMethods(['create']) -+ ->getMock(); -+ $searchCriteriaResolverFactory->expects($this->any()) -+ ->method('create') -+ ->willReturn($searchCriteriaResolver); -+ -+ $searchResultApplier = $this->getMockBuilder(SearchResultApplierInterface::class) -+ ->disableOriginalConstructor() -+ ->setMethods(['apply']) -+ ->getMockForAbstractClass(); -+ $searchResultApplierFactory = $this->getMockBuilder(SearchResultApplierFactory::class) -+ ->disableOriginalConstructor() -+ ->setMethods(['create']) -+ ->getMock(); -+ $searchResultApplierFactory->expects($this->any()) -+ ->method('create') -+ ->willReturn($searchResultApplier); -+ -+ $totalRecordsResolver = $this->getMockBuilder(TotalRecordsResolverInterface::class) -+ ->disableOriginalConstructor() -+ ->setMethods(['resolve']) -+ ->getMockForAbstractClass(); -+ $totalRecordsResolverFactory = $this->getMockBuilder(TotalRecordsResolverFactory::class) -+ ->disableOriginalConstructor() -+ ->setMethods(['create']) -+ ->getMock(); -+ $totalRecordsResolverFactory->expects($this->any()) -+ ->method('create') -+ ->willReturn($totalRecordsResolver); -+ - $this->advancedCollection = $this->objectManager->getObject( - \Magento\CatalogSearch\Model\ResourceModel\Advanced\Collection::class, - [ -@@ -88,6 +132,10 @@ class CollectionTest extends BaseCollection - 'temporaryStorageFactory' => $this->temporaryStorageFactory, - 'search' => $this->search, - 'productLimitationFactory' => $productLimitationFactoryMock, -+ 'collectionProvider' => null, -+ 'searchCriteriaResolverFactory' => $searchCriteriaResolverFactory, -+ 'searchResultApplierFactory' => $searchResultApplierFactory, -+ 'totalRecordsResolverFactory' => $totalRecordsResolverFactory - ] - ); - } -@@ -115,18 +163,8 @@ class CollectionTest extends BaseCollection - ->willReturn($this->filterBuilder); - - $filter = $this->createMock(\Magento\Framework\Api\Filter::class); -- $this->filterBuilder->expects($this->once())->method('create')->willReturn($filter); -- -- $criteria = $this->createMock(\Magento\Framework\Api\Search\SearchCriteria::class); -- $this->criteriaBuilder->expects($this->once())->method('create')->willReturn($criteria); -- $criteria->expects($this->once()) -- ->method('setRequestName') -- ->with('advanced_search_container'); -- -- $tempTable = $this->createMock(\Magento\Framework\DB\Ddl\Table::class); -- $temporaryStorage = $this->createMock(\Magento\Framework\Search\Adapter\Mysql\TemporaryStorage::class); -- $temporaryStorage->expects($this->once())->method('storeApiDocuments')->willReturn($tempTable); -- $this->temporaryStorageFactory->expects($this->once())->method('create')->willReturn($temporaryStorage); -+ $this->filterBuilder->expects($this->any())->method('create')->willReturn($filter); -+ - $searchResult = $this->createMock(\Magento\Framework\Api\Search\SearchResultInterface::class); - $this->search->expects($this->once())->method('search')->willReturn($searchResult); - -diff --git a/app/code/Magento/CatalogSearch/Test/Unit/Model/ResourceModel/BaseCollection.php b/app/code/Magento/CatalogSearch/Test/Unit/Model/ResourceModel/BaseCollection.php -index e99d75c25f5..9ea103e23d2 100644 ---- a/app/code/Magento/CatalogSearch/Test/Unit/Model/ResourceModel/BaseCollection.php -+++ b/app/code/Magento/CatalogSearch/Test/Unit/Model/ResourceModel/BaseCollection.php -@@ -9,6 +9,9 @@ namespace Magento\CatalogSearch\Test\Unit\Model\ResourceModel; - * Base class for Collection tests. - * - * Contains helper methods to get commonly used mocks used for collection tests. -+ * -+ * @deprecated -+ * @see \Magento\ElasticSearch - **/ - class BaseCollection extends \PHPUnit\Framework\TestCase - { -diff --git a/app/code/Magento/CatalogSearch/Test/Unit/Model/ResourceModel/Fulltext/CollectionTest.php b/app/code/Magento/CatalogSearch/Test/Unit/Model/ResourceModel/Fulltext/CollectionTest.php -index a3b1d2fd0f2..9170b81dc31 100644 ---- a/app/code/Magento/CatalogSearch/Test/Unit/Model/ResourceModel/Fulltext/CollectionTest.php -+++ b/app/code/Magento/CatalogSearch/Test/Unit/Model/ResourceModel/Fulltext/CollectionTest.php -@@ -5,6 +5,12 @@ - */ - namespace Magento\CatalogSearch\Test\Unit\Model\ResourceModel\Fulltext; - -+use Magento\CatalogSearch\Model\ResourceModel\Fulltext\Collection\SearchCriteriaResolverFactory; -+use Magento\CatalogSearch\Model\ResourceModel\Fulltext\Collection\SearchCriteriaResolverInterface; -+use Magento\CatalogSearch\Model\ResourceModel\Fulltext\Collection\SearchResultApplierFactory; -+use Magento\CatalogSearch\Model\ResourceModel\Fulltext\Collection\TotalRecordsResolverFactory; -+use Magento\CatalogSearch\Model\ResourceModel\Fulltext\Collection\SearchResultApplierInterface; -+use Magento\CatalogSearch\Model\ResourceModel\Fulltext\Collection\TotalRecordsResolverInterface; - use Magento\CatalogSearch\Test\Unit\Model\ResourceModel\BaseCollection; - use Magento\Framework\Search\Adapter\Mysql\TemporaryStorageFactory; - use PHPUnit_Framework_MockObject_MockObject as MockObject; -@@ -97,6 +103,41 @@ class CollectionTest extends BaseCollection - $temporaryStorageFactory->expects($this->any()) - ->method('create') - ->willReturn($this->temporaryStorage); -+ $searchCriteriaResolver = $this->getMockBuilder(SearchCriteriaResolverInterface::class) -+ ->disableOriginalConstructor() -+ ->setMethods(['resolve']) -+ ->getMockForAbstractClass(); -+ $searchCriteriaResolverFactory = $this->getMockBuilder(SearchCriteriaResolverFactory::class) -+ ->disableOriginalConstructor() -+ ->setMethods(['create']) -+ ->getMock(); -+ $searchCriteriaResolverFactory->expects($this->any()) -+ ->method('create') -+ ->willReturn($searchCriteriaResolver); -+ -+ $searchResultApplier = $this->getMockBuilder(SearchResultApplierInterface::class) -+ ->disableOriginalConstructor() -+ ->setMethods(['apply']) -+ ->getMockForAbstractClass(); -+ $searchResultApplierFactory = $this->getMockBuilder(SearchResultApplierFactory::class) -+ ->disableOriginalConstructor() -+ ->setMethods(['create']) -+ ->getMock(); -+ $searchResultApplierFactory->expects($this->any()) -+ ->method('create') -+ ->willReturn($searchResultApplier); -+ -+ $totalRecordsResolver = $this->getMockBuilder(TotalRecordsResolverInterface::class) -+ ->disableOriginalConstructor() -+ ->setMethods(['resolve']) -+ ->getMockForAbstractClass(); -+ $totalRecordsResolverFactory = $this->getMockBuilder(TotalRecordsResolverFactory::class) -+ ->disableOriginalConstructor() -+ ->setMethods(['create']) -+ ->getMock(); -+ $totalRecordsResolverFactory->expects($this->any()) -+ ->method('create') -+ ->willReturn($totalRecordsResolver); - - $this->model = $this->objectManager->getObject( - \Magento\CatalogSearch\Model\ResourceModel\Fulltext\Collection::class, -@@ -106,6 +147,9 @@ class CollectionTest extends BaseCollection - 'scopeConfig' => $this->scopeConfig, - 'temporaryStorageFactory' => $temporaryStorageFactory, - 'productLimitationFactory' => $productLimitationFactoryMock, -+ 'searchCriteriaResolverFactory' => $searchCriteriaResolverFactory, -+ 'searchResultApplierFactory' => $searchResultApplierFactory, -+ 'totalRecordsResolverFactory' => $totalRecordsResolverFactory, - ] - ); - -@@ -124,37 +168,10 @@ class CollectionTest extends BaseCollection - $reflectionProperty->setValue(null); - } - -- /** -- * @expectedException \Exception -- * @expectedExceptionCode 333 -- * @expectedExceptionMessage setRequestName -- */ -- public function testGetFacetedDataWithException() -- { -- $criteria = $this->createMock(\Magento\Framework\Api\Search\SearchCriteria::class); -- $this->criteriaBuilder->expects($this->once())->method('create')->willReturn($criteria); -- $criteria->expects($this->once()) -- ->method('setRequestName') -- ->withConsecutive(['catalog_view_container']) -- ->willThrowException(new \Exception('setRequestName', 333)); -- $this->model->getFacetedData('field'); -- } -- - public function testGetFacetedDataWithEmptyAggregations() - { -- $criteria = $this->createMock(\Magento\Framework\Api\Search\SearchCriteria::class); -- $this->criteriaBuilder->expects($this->once())->method('create')->willReturn($criteria); -- $criteria->expects($this->once()) -- ->method('setRequestName') -- ->withConsecutive(['catalog_view_container']); - $searchResult = $this->getMockBuilder(\Magento\Framework\Api\Search\SearchResultInterface::class) - ->getMockForAbstractClass(); -- $table = $this->getMockBuilder(\Magento\Framework\DB\Ddl\Table::class) -- ->setMethods(['getName']) -- ->getMock(); -- $this->temporaryStorage->expects($this->once()) -- ->method('storeApiDocuments') -- ->willReturn($table); - $this->search->expects($this->once()) - ->method('search') - ->willReturn($searchResult); -diff --git a/app/code/Magento/CatalogSearch/Test/Unit/Model/ResourceModel/Setup/PropertyMapperTest.php b/app/code/Magento/CatalogSearch/Test/Unit/Model/ResourceModel/Setup/PropertyMapperTest.php -new file mode 100644 -index 00000000000..5c917b360f1 ---- /dev/null -+++ b/app/code/Magento/CatalogSearch/Test/Unit/Model/ResourceModel/Setup/PropertyMapperTest.php -@@ -0,0 +1,65 @@ -+<?php -+declare(strict_types=1); -+ -+/** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+ -+namespace Magento\CatalogSearch\Test\Unit\Model\ResourceModel\Setup; -+ -+use Magento\CatalogSearch\Model\ResourceModel\Setup\PropertyMapper; -+use PHPUnit\Framework\TestCase; -+ -+/** -+ * Class PropertyMapperTest -+ * -+ * @package Magento\CatalogSearch\Test\Unit\Model\ResourceModel\Setup -+ */ -+class PropertyMapperTest extends TestCase -+{ -+ /** -+ * @var PropertyMapper -+ */ -+ private $propertyMapper; -+ -+ /** -+ * @return void -+ */ -+ protected function setUp(): void -+ { -+ $this->propertyMapper = new PropertyMapper(); -+ } -+ -+ /** -+ * @return array -+ */ -+ public function caseProvider(): array -+ { -+ return [ -+ [ -+ ['search_weight' => 9, 'something_other' => '3'], -+ ['search_weight' => 9] -+ ], -+ [ -+ ['something' => 3], -+ ['search_weight' => 1] -+ ] -+ ]; -+ } -+ -+ /** -+ * @dataProvider caseProvider -+ * -+ * @test -+ * -+ * @param array $input -+ * @param array $result -+ * @return void -+ */ -+ public function testMapCorrectlyMapsValue(array $input, array $result): void -+ { -+ //Second parameter doesn't matter as it is not used -+ $this->assertSame($result, $this->propertyMapper->map($input, 4)); -+ } -+} -diff --git a/app/code/Magento/CatalogSearch/Test/Unit/Model/Search/BaseSelectStrategy/StrategyMapperTest.php b/app/code/Magento/CatalogSearch/Test/Unit/Model/Search/BaseSelectStrategy/StrategyMapperTest.php -index 5fa5b0333c6..b168e728117 100644 ---- a/app/code/Magento/CatalogSearch/Test/Unit/Model/Search/BaseSelectStrategy/StrategyMapperTest.php -+++ b/app/code/Magento/CatalogSearch/Test/Unit/Model/Search/BaseSelectStrategy/StrategyMapperTest.php -@@ -11,6 +11,10 @@ use Magento\CatalogSearch\Model\Adapter\Mysql\BaseSelectStrategy\BaseSelectFullT - use \Magento\CatalogSearch\Model\Search\BaseSelectStrategy\StrategyMapper; - use \Magento\CatalogSearch\Model\Search\SelectContainer\SelectContainer; - -+/** -+ * @deprecated -+ * @see \Magento\ElasticSearch -+ */ - class StrategyMapperTest extends \PHPUnit\Framework\TestCase - { - /** @var BaseSelectAttributesSearchStrategy|\PHPUnit_Framework_MockObject_MockObject */ -diff --git a/app/code/Magento/CatalogSearch/Test/Unit/Model/Search/FilterMapper/ExclusionStrategyTest.php b/app/code/Magento/CatalogSearch/Test/Unit/Model/Search/FilterMapper/ExclusionStrategyTest.php -index 09591532f9f..e693807760e 100644 ---- a/app/code/Magento/CatalogSearch/Test/Unit/Model/Search/FilterMapper/ExclusionStrategyTest.php -+++ b/app/code/Magento/CatalogSearch/Test/Unit/Model/Search/FilterMapper/ExclusionStrategyTest.php -@@ -17,6 +17,8 @@ use Magento\Store\Api\Data\WebsiteInterface; - - /** - * @SuppressWarnings(PHPMD.CouplingBetweenObjects) -+ * @deprecated -+ * @see \Magento\ElasticSearch - */ - class ExclusionStrategyTest extends \PHPUnit\Framework\TestCase - { -diff --git a/app/code/Magento/CatalogSearch/Test/Unit/Model/Search/FilterMapper/FilterContextTest.php b/app/code/Magento/CatalogSearch/Test/Unit/Model/Search/FilterMapper/FilterContextTest.php -index e16f23ca09d..12ce0d63ac6 100644 ---- a/app/code/Magento/CatalogSearch/Test/Unit/Model/Search/FilterMapper/FilterContextTest.php -+++ b/app/code/Magento/CatalogSearch/Test/Unit/Model/Search/FilterMapper/FilterContextTest.php -@@ -16,6 +16,10 @@ use Magento\Eav\Model\Entity\Attribute\AbstractAttribute; - use Magento\Framework\Search\Request\FilterInterface; - use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; - -+/** -+ * @deprecated -+ * @see \Magento\ElasticSearch -+ */ - class FilterContextTest extends \PHPUnit\Framework\TestCase - { - /** -diff --git a/app/code/Magento/CatalogSearch/Test/Unit/Model/Search/FilterMapper/TermDropdownStrategyTest.php b/app/code/Magento/CatalogSearch/Test/Unit/Model/Search/FilterMapper/TermDropdownStrategyTest.php -index 8771c92039f..e064f46655d 100644 ---- a/app/code/Magento/CatalogSearch/Test/Unit/Model/Search/FilterMapper/TermDropdownStrategyTest.php -+++ b/app/code/Magento/CatalogSearch/Test/Unit/Model/Search/FilterMapper/TermDropdownStrategyTest.php -@@ -18,6 +18,9 @@ use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; - /** - * Class TermDropdownStrategyTest. - * Unit test for \Magento\CatalogSearch\Model\Search\FilterMapper\TermDropdownStrategy. -+ * -+ * @deprecated -+ * @see \Magento\ElasticSearch - */ - class TermDropdownStrategyTest extends \PHPUnit\Framework\TestCase - { -diff --git a/app/code/Magento/CatalogSearch/Test/Unit/Model/Search/IndexBuilderTest.php b/app/code/Magento/CatalogSearch/Test/Unit/Model/Search/IndexBuilderTest.php -index da7cfa1ea98..b066c118ef7 100644 ---- a/app/code/Magento/CatalogSearch/Test/Unit/Model/Search/IndexBuilderTest.php -+++ b/app/code/Magento/CatalogSearch/Test/Unit/Model/Search/IndexBuilderTest.php -@@ -27,6 +27,8 @@ use Magento\Framework\DB\Select; - * Test for \Magento\CatalogSearch\Model\Search\IndexBuilder - * - * @SuppressWarnings(PHPMD.CouplingBetweenObjects) -+ * @deprecated -+ * @see \Magento\ElasticSearch - */ - class IndexBuilderTest extends \PHPUnit\Framework\TestCase - { -diff --git a/app/code/Magento/CatalogSearch/Test/Unit/Model/Search/RequestGeneratorTest.php b/app/code/Magento/CatalogSearch/Test/Unit/Model/Search/RequestGeneratorTest.php -index b52c9cfd674..a8c654652a3 100644 ---- a/app/code/Magento/CatalogSearch/Test/Unit/Model/Search/RequestGeneratorTest.php -+++ b/app/code/Magento/CatalogSearch/Test/Unit/Model/Search/RequestGeneratorTest.php -@@ -9,6 +9,9 @@ use Magento\Catalog\Model\ResourceModel\Product\Attribute\CollectionFactory; - use Magento\CatalogSearch\Model\Search\RequestGenerator\GeneratorResolver; - use Magento\CatalogSearch\Model\Search\RequestGenerator\GeneratorInterface; - -+/** -+ * Test for \Magento\CatalogSearch\Model\Search\RequestGenerator -+ */ - class RequestGeneratorTest extends \PHPUnit\Framework\TestCase - { - /** @var \Magento\Framework\TestFramework\Unit\Helper\ObjectManager */ -@@ -61,7 +64,7 @@ class RequestGeneratorTest extends \PHPUnit\Framework\TestCase - return [ - [ - [ -- 'quick_search_container' => ['queries' => 0, 'filters' => 0, 'aggregations' => 0], -+ 'quick_search_container' => ['queries' => 1, 'filters' => 0, 'aggregations' => 0], - 'advanced_search_container' => ['queries' => 0, 'filters' => 0, 'aggregations' => 0], - 'catalog_view_container' => ['queries' => 0, 'filters' => 0, 'aggregations' => 0] - ], -diff --git a/app/code/Magento/CatalogSearch/Test/Unit/Model/Search/TableMapperTest.php b/app/code/Magento/CatalogSearch/Test/Unit/Model/Search/TableMapperTest.php -index cfddc07bcee..db467552615 100644 ---- a/app/code/Magento/CatalogSearch/Test/Unit/Model/Search/TableMapperTest.php -+++ b/app/code/Magento/CatalogSearch/Test/Unit/Model/Search/TableMapperTest.php -@@ -18,7 +18,10 @@ use Magento\Framework\Search\Request\Filter\Term; - - /** - * Test for \Magento\CatalogSearch\Model\Search\TableMapper -+ * - * @SuppressWarnings(PHPMD.CouplingBetweenObjects) -+ * @deprecated -+ * @see \Magento\ElasticSearch - */ - class TableMapperTest extends \PHPUnit\Framework\TestCase - { -diff --git a/app/code/Magento/CatalogSearch/Test/Unit/Plugin/EnableEavIndexerTest.php b/app/code/Magento/CatalogSearch/Test/Unit/Plugin/EnableEavIndexerTest.php -index 0eac2e3309a..b20fdde7c52 100644 ---- a/app/code/Magento/CatalogSearch/Test/Unit/Plugin/EnableEavIndexerTest.php -+++ b/app/code/Magento/CatalogSearch/Test/Unit/Plugin/EnableEavIndexerTest.php -@@ -9,6 +9,10 @@ namespace Magento\CatalogSearch\Test\Unit\Plugin; - - use Magento\Framework\TestFramework\Unit\Helper\ObjectManager as ObjectManagerHelper; - -+/** -+ * @deprecated -+ * @see \Magento\ElasticSearch -+ */ - class EnableEavIndexerTest extends \PHPUnit\Framework\TestCase - { - /** -diff --git a/app/code/Magento/CatalogSearch/Ui/DataProvider/Product/AddFulltextFilterToCollection.php b/app/code/Magento/CatalogSearch/Ui/DataProvider/Product/AddFulltextFilterToCollection.php -index 1b05152903a..f312178e0bf 100644 ---- a/app/code/Magento/CatalogSearch/Ui/DataProvider/Product/AddFulltextFilterToCollection.php -+++ b/app/code/Magento/CatalogSearch/Ui/DataProvider/Product/AddFulltextFilterToCollection.php -@@ -5,8 +5,8 @@ - */ - namespace Magento\CatalogSearch\Ui\DataProvider\Product; - --use Magento\Framework\Data\Collection; - use Magento\CatalogSearch\Model\ResourceModel\Search\Collection as SearchCollection; -+use Magento\Framework\Data\Collection; - use Magento\Ui\DataProvider\AddFilterToCollectionInterface; - - /** -@@ -30,14 +30,14 @@ class AddFulltextFilterToCollection implements AddFilterToCollectionInterface - } - - /** -- * {@inheritdoc} -+ * @inheritdoc - * - * @SuppressWarnings(PHPMD.UnusedFormalParameter) - */ - public function addFilter(Collection $collection, $field, $condition = null) - { - /** @var $collection \Magento\Catalog\Model\ResourceModel\Product\Collection */ -- if (isset($condition['fulltext']) && !empty($condition['fulltext'])) { -+ if (isset($condition['fulltext']) && (string)$condition['fulltext'] !== '') { - $this->searchCollection->addBackendSearchFilter($condition['fulltext']); - $productIds = $this->searchCollection->load()->getAllIds(); - $collection->addIdFilter($productIds); -diff --git a/app/code/Magento/CatalogSearch/composer.json b/app/code/Magento/CatalogSearch/composer.json -index 1000e349b6f..7bcb91e9454 100644 ---- a/app/code/Magento/CatalogSearch/composer.json -+++ b/app/code/Magento/CatalogSearch/composer.json -@@ -1,6 +1,6 @@ - { - "name": "magento/module-catalog-search", -- "description": "N/A", -+ "description": "Catalog search", - "config": { - "sort-packages": true - }, -diff --git a/app/code/Magento/CatalogSearch/etc/adminhtml/system.xml b/app/code/Magento/CatalogSearch/etc/adminhtml/system.xml -index 39235511eae..c358062b88a 100644 ---- a/app/code/Magento/CatalogSearch/etc/adminhtml/system.xml -+++ b/app/code/Magento/CatalogSearch/etc/adminhtml/system.xml -@@ -19,15 +19,17 @@ - <field id="engine" canRestore="1"> - <backend_model>Magento\CatalogSearch\Model\Adminhtml\System\Config\Backend\Engine</backend_model> - </field> -- <field id="min_query_length" translate="label" type="text" sortOrder="5" showInDefault="1" showInWebsite="1" showInStore="1" canRestore="1"> -+ <field id="min_query_length" translate="label comment" type="text" sortOrder="5" showInDefault="1" showInWebsite="1" showInStore="1" canRestore="1"> - <label>Minimal Query Length</label> - <validate>validate-digits</validate> -+ <comment>This value must be compatible with the corresponding setting in the configured search engine. Be aware: a low query length limit may cause the performance impact.</comment> - </field> -- <field id="max_query_length" translate="label" type="text" sortOrder="10" showInDefault="1" showInWebsite="1" showInStore="1" canRestore="1"> -+ <field id="max_query_length" translate="label comment" type="text" sortOrder="10" showInDefault="1" showInWebsite="1" showInStore="1" canRestore="1"> - <label>Maximum Query Length</label> - <validate>validate-digits</validate> -+ <comment>This value must be compatible with the corresponding setting in the configured search engine.</comment> - </field> -- <field id="max_count_cacheable_search_terms" translate="label" type="text" sortOrder="10" showInDefault="1" showInWebsite="1" showInStore="1" canRestore="1"> -+ <field id="max_count_cacheable_search_terms" translate="label comment" type="text" sortOrder="10" showInDefault="1" showInWebsite="1" showInStore="1" canRestore="1"> - <label>Number of top search results to cache</label> - <comment>Number of popular search terms to be cached for faster response. Use “0” to cache all results after a term is searched for the second time.</comment> - <validate>validate-digits</validate> -diff --git a/app/code/Magento/CatalogSearch/etc/config.xml b/app/code/Magento/CatalogSearch/etc/config.xml -index 66b79226c9f..7ea15c6caa5 100644 ---- a/app/code/Magento/CatalogSearch/etc/config.xml -+++ b/app/code/Magento/CatalogSearch/etc/config.xml -@@ -13,7 +13,7 @@ - </seo> - <search> - <engine>mysql</engine> -- <min_query_length>1</min_query_length> -+ <min_query_length>3</min_query_length> - <max_query_length>128</max_query_length> - <max_count_cacheable_search_terms>100</max_count_cacheable_search_terms> - <autocomplete_limit>8</autocomplete_limit> -diff --git a/app/code/Magento/CatalogSearch/etc/di.xml b/app/code/Magento/CatalogSearch/etc/di.xml -index cc07384d4c5..7359bd6b454 100644 ---- a/app/code/Magento/CatalogSearch/etc/di.xml -+++ b/app/code/Magento/CatalogSearch/etc/di.xml -@@ -14,6 +14,10 @@ - <preference for="Magento\CatalogSearch\Model\Search\FilterMapper\FilterStrategyInterface" type="Magento\CatalogSearch\Model\Search\FilterMapper\FilterContext"/> - <preference for="Magento\CatalogSearch\Model\Indexer\IndexSwitcherInterface" type="Magento\CatalogSearch\Model\Indexer\IndexSwitcherProxy"/> - <preference for="Magento\CatalogSearch\Model\Adapter\Aggregation\RequestCheckerInterface" type="Magento\CatalogSearch\Model\Adapter\Aggregation\RequestCheckerComposite"/> -+ <preference for="Magento\CatalogSearch\Model\ResourceModel\Fulltext\Collection\SearchCriteriaResolverInterface" type="Magento\CatalogSearch\Model\ResourceModel\Fulltext\Collection\SearchCriteriaResolver"/> -+ <preference for="Magento\CatalogSearch\Model\ResourceModel\Fulltext\Collection\SearchResultApplierInterface" type="Magento\CatalogSearch\Model\ResourceModel\Fulltext\Collection\SearchResultApplier"/> -+ <preference for="Magento\CatalogSearch\Model\ResourceModel\Fulltext\Collection\TotalRecordsResolverInterface" type="Magento\CatalogSearch\Model\ResourceModel\Fulltext\Collection\TotalRecordsResolver"/> -+ <preference for="Magento\CatalogSearch\Model\Search\ItemCollectionProviderInterface" type="Magento\CatalogSearch\Model\Search\ItemCollectionProvider"/> - <type name="Magento\CatalogSearch\Model\Indexer\IndexerHandlerFactory"> - <arguments> - <argument name="handlers" xsi:type="array"> -@@ -176,8 +180,13 @@ - <argument name="collectionFactory" xsi:type="object">Magento\CatalogSearch\Model\ResourceModel\Fulltext\SearchCollectionFactory</argument> - </arguments> - </virtualType> -- <virtualType name="Magento\CatalogSearch\Model\ResourceModel\Advanced\CollectionFactory" -- type="Magento\Catalog\Model\ResourceModel\Product\CollectionFactory"> -+ -+ <virtualType name="Magento\CatalogSearch\Model\Advanced\ItemCollectionProvider" type="Magento\Catalog\Model\Layer\Search\ItemCollectionProvider"> -+ <arguments> -+ <argument name="collectionFactory" xsi:type="object">Magento\CatalogSearch\Model\ResourceModel\Advanced\CollectionFactory</argument> -+ </arguments> -+ </virtualType> -+ <virtualType name="Magento\CatalogSearch\Model\ResourceModel\Advanced\CollectionFactory" type="Magento\Catalog\Model\ResourceModel\Product\CollectionFactory"> - <arguments> - <argument name="instanceName" xsi:type="string">Magento\CatalogSearch\Model\ResourceModel\Advanced\Collection</argument> - </arguments> -@@ -185,6 +194,21 @@ - <type name="Magento\CatalogSearch\Model\Advanced"> - <arguments> - <argument name="productCollectionFactory" xsi:type="object">Magento\CatalogSearch\Model\ResourceModel\Advanced\CollectionFactory</argument> -+ <argument name="collectionProvider" xsi:type="object">Magento\CatalogSearch\Model\Search\ItemCollectionProviderInterface</argument> -+ </arguments> -+ </type> -+ <type name="Magento\CatalogSearch\Model\Search\ItemCollectionProvider"> -+ <arguments> -+ <argument name="factories" xsi:type="array"> -+ <item name="mysql" xsi:type="object">Magento\CatalogSearch\Model\ResourceModel\Advanced\CollectionFactory</item> -+ </argument> -+ </arguments> -+ </type> -+ <type name="Magento\CatalogSearch\Model\Advanced\ProductCollectionPrepareStrategyProvider"> -+ <arguments> -+ <argument name="strategies" xsi:type="array"> -+ <item name="mysql" xsi:type="object">Magento\CatalogSearch\Model\Advanced\ProductCollectionPrepareStrategy</item> -+ </argument> - </arguments> - </type> - <virtualType name="Magento\CatalogSearch\Model\Layer\Category\Context" type="Magento\Catalog\Model\Layer\Category\Context"> -@@ -340,4 +364,11 @@ - <type name="Magento\Config\Model\Config"> - <plugin name="config_enable_eav_indexer" type="Magento\CatalogSearch\Plugin\EnableEavIndexer" /> - </type> -+ <type name="Magento\Eav\Model\Entity\Setup\PropertyMapper\Composite"> -+ <arguments> -+ <argument name="propertyMappers" xsi:type="array"> -+ <item name="catalog_search" xsi:type="string">Magento\CatalogSearch\Model\ResourceModel\Setup\PropertyMapper</item> -+ </argument> -+ </arguments> -+ </type> - </config> -diff --git a/app/code/Magento/CatalogSearch/etc/search_request.xml b/app/code/Magento/CatalogSearch/etc/search_request.xml -index d7bfb2e6b4a..6f9eb6e2066 100644 ---- a/app/code/Magento/CatalogSearch/etc/search_request.xml -+++ b/app/code/Magento/CatalogSearch/etc/search_request.xml -@@ -19,7 +19,6 @@ - <queryReference clause="must" ref="visibility"/> - </query> - <query xsi:type="matchQuery" value="$search_term$" name="search"> -- <match field="sku"/> - <match field="*"/> - </query> - <query xsi:type="filteredQuery" name="category"> -diff --git a/app/code/Magento/CatalogSearch/i18n/en_US.csv b/app/code/Magento/CatalogSearch/i18n/en_US.csv -index 9121520774c..f25d1a589d4 100644 ---- a/app/code/Magento/CatalogSearch/i18n/en_US.csv -+++ b/app/code/Magento/CatalogSearch/i18n/en_US.csv -@@ -37,3 +37,6 @@ name,name - "Minimal Query Length","Minimal Query Length" - "Maximum Query Length","Maximum Query Length" - "Rebuild Catalog product fulltext search index","Rebuild Catalog product fulltext search index" -+"Please enter a valid price range.","Please enter a valid price range." -+"This value must be compatible with the corresponding setting in the configured search engine. Be aware: a low query length limit may cause the performance impact.","This value must be compatible with the corresponding setting in the configured search engine. Be aware: a low query length limit may cause the performance impact." -+"This value must be compatible with the corresponding setting in the configured search engine.","This value must be compatible with the corresponding setting in the configured search engine." -\ No newline at end of file -diff --git a/app/code/Magento/CatalogSearch/view/frontend/templates/advanced/form.phtml b/app/code/Magento/CatalogSearch/view/frontend/templates/advanced/form.phtml -index 53a30102287..3712f221233 100644 ---- a/app/code/Magento/CatalogSearch/view/frontend/templates/advanced/form.phtml -+++ b/app/code/Magento/CatalogSearch/view/frontend/templates/advanced/form.phtml -@@ -4,8 +4,7 @@ - * See COPYING.txt for license details. - */ - --// @codingStandardsIgnoreFile -- -+// phpcs:disable Magento2.Templates.ThisInTemplate.FoundThis - ?> - <?php - /** -@@ -14,108 +13,120 @@ - * @var $block \Magento\CatalogSearch\Block\Advanced\Form - */ - ?> --<?php $maxQueryLength = $this->helper('Magento\CatalogSearch\Helper\Data')->getMaxQueryLength();?> --<form class="form search advanced" action="<?= /* @escapeNotVerified */ $block->getSearchPostUrl() ?>" method="get" id="form-validate"> -+<?php $maxQueryLength = $this->helper(\Magento\CatalogSearch\Helper\Data::class)->getMaxQueryLength();?> -+<form class="form search advanced" action="<?= $block->escapeUrl($block->getSearchPostUrl()) ?>" method="get" id="form-validate"> - <fieldset class="fieldset"> -- <legend class="legend"><span><?= /* @escapeNotVerified */ __('Search Settings') ?></span></legend><br /> -- <?php foreach ($block->getSearchableAttributes() as $_attribute): ?> -- <?php $_code = $_attribute->getAttributeCode() ?> -- <div class="field <?= /* @escapeNotVerified */ $_code ?>"> -- <label class="label" for="<?= /* @escapeNotVerified */ $_code ?>"> -+ <legend class="legend"><span><?= $block->escapeHtml(__('Search Settings')) ?></span></legend><br /> -+ <?php foreach ($block->getSearchableAttributes() as $_attribute) : ?> -+ <?php $_code = $_attribute->getAttributeCode() ?> -+ <div class="field <?= $block->escapeHtmlAttr($_code) ?>"> -+ <label class="label" for="<?= $block->escapeHtmlAttr($_code) ?>"> - <span><?= $block->escapeHtml(__($block->getAttributeLabel($_attribute))) ?></span> - </label> - <div class="control"> -- <?php switch ($block->getAttributeInputType($_attribute)): -- case 'number': ?> -+ <?php -+ switch ($block->getAttributeInputType($_attribute)) : -+ case 'number': -+ ?> - <div class="range fields group group-2"> - <div class="field no-label"> - <div class="control"> - <input type="text" -- name="<?= /* @escapeNotVerified */ $_code ?>[from]" -+ name="<?= $block->escapeHtmlAttr($_code) ?>[from]" - value="<?= $block->escapeHtml($block->getAttributeValue($_attribute, 'from')) ?>" -- id="<?= /* @escapeNotVerified */ $_code ?>" -+ id="<?= $block->escapeHtmlAttr($_code) ?>" - title="<?= $block->escapeHtml($block->getAttributeLabel($_attribute)) ?>" - class="input-text" -- maxlength="<?= /* @escapeNotVerified */ $maxQueryLength ?>" -- data-validate="{number:true, 'less-than-equals-to':'#<?= /* @escapeNotVerified */ $_code ?>_to'}" /> -+ maxlength="<?= $block->escapeHtmlAttr($maxQueryLength) ?>" -+ data-validate="{number:true, 'less-than-equals-to':'#<?= $block->escapeHtmlAttr($_code) ?>_to'}" /> - </div> - </div> - <div class="field no-label"> - <div class="control"> - <input type="text" -- name="<?= /* @escapeNotVerified */ $_code ?>[to]" -+ name="<?= $block->escapeHtmlAttr($_code) ?>[to]" - value="<?= $block->escapeHtml($block->getAttributeValue($_attribute, 'to')) ?>" -- id="<?= /* @escapeNotVerified */ $_code ?>_to" -+ id="<?= $block->escapeHtmlAttr($_code) ?>_to" - title="<?= $block->escapeHtml($block->getAttributeLabel($_attribute)) ?>" - class="input-text" -- maxlength="<?= /* @escapeNotVerified */ $maxQueryLength ?>" -- data-validate="{number:true, 'greater-than-equals-to':'#<?= /* @escapeNotVerified */ $_code ?>'}" /> -+ maxlength="<?= $block->escapeHtmlAttr($maxQueryLength) ?>" -+ data-validate="{number:true, 'greater-than-equals-to':'#<?= $block->escapeHtmlAttr($_code) ?>'}" /> - </div> - </div> - </div> -- <?php break; -- case 'price': ?> -+ <?php -+ break; -+ case 'price': -+ ?> - <div class="range price fields group group-2"> - <div class="field no-label"> - <div class="control"> -- <input name="<?= /* @escapeNotVerified */ $_code ?>[from]" -+ <input name="<?= $block->escapeHtmlAttr($_code) ?>[from]" - value="<?= $block->escapeHtml($block->getAttributeValue($_attribute, 'from')) ?>" -- id="<?= /* @escapeNotVerified */ $_code ?>" -+ id="<?= $block->escapeHtmlAttr($_code) ?>" - title="<?= $block->escapeHtml($block->getAttributeLabel($_attribute)) ?>" - class="input-text" - type="text" -- maxlength="<?= /* @escapeNotVerified */ $maxQueryLength ?>" -- data-validate="{number:true, 'less-than-equals-to':'#<?= /* @escapeNotVerified */ $_code ?>_to'}" /> -+ maxlength="<?= $block->escapeHtmlAttr($maxQueryLength) ?>" -+ data-validate="{number:true, 'less-than-equals-to':'#<?= $block->escapeHtmlAttr($_code) ?>_to'}" /> - </div> - </div> - <div class="field with-addon no-label"> - <div class="control"> - <div class="addon"> -- <input name="<?= /* @escapeNotVerified */ $_code ?>[to]" -+ <input name="<?= $block->escapeHtmlAttr($_code) ?>[to]" - value="<?= $block->escapeHtml($block->getAttributeValue($_attribute, 'to')) ?>" -- id="<?= /* @escapeNotVerified */ $_code ?>_to" -+ id="<?= $block->escapeHtmlAttr($_code) ?>_to" - title="<?= $block->escapeHtml($block->getAttributeLabel($_attribute)) ?>" - class="input-text" - type="text" -- maxlength="<?= /* @escapeNotVerified */ $maxQueryLength ?>" -- data-validate="{number:true, 'greater-than-equals-to':'#<?= /* @escapeNotVerified */ $_code ?>'}" /> -+ maxlength="<?= $block->escapeHtmlAttr($maxQueryLength) ?>" -+ data-validate="{number:true, 'greater-than-equals-to':'#<?= $block->escapeHtmlAttr($_code) ?>'}" /> - <label class="addafter" -- for="<?= /* @escapeNotVerified */ $_code ?>_to"> -- <?= /* @escapeNotVerified */ $block->getCurrency($_attribute) ?> -+ for="<?= $block->escapeHtmlAttr($_code) ?>_to"> -+ <?= $block->escapeHtml($block->getCurrency($_attribute)) ?> - </label> - </div> - </div> - </div> - </div> -- <?php break; -- case 'select': ?> -- <?= /* @escapeNotVerified */ $block->getAttributeSelectElement($_attribute) ?> -- <?php break; -- case 'yesno': ?> -- <?= /* @escapeNotVerified */ $block->getAttributeYesNoElement($_attribute) ?> -- <?php break; -- case 'date': ?> -+ <?php -+ break; -+ case 'select': -+ ?> -+ <?= /* @noEscape */ $block->getAttributeSelectElement($_attribute) ?> -+ <?php -+ break; -+ case 'yesno': -+ ?> -+ <?= /* @noEscape */ $block->getAttributeYesNoElement($_attribute) ?> -+ <?php -+ break; -+ case 'date': -+ ?> - <div class="range dates fields group group-2"> - <div class="field date no-label"> - <div class="control"> -- <?= /* @escapeNotVerified */ $block->getDateInput($_attribute, 'from') ?> -+ <?= /* @noEscape */ $block->getDateInput($_attribute, 'from') ?> - </div> - </div> - <div class="field date no-label"> - <div class="control"> -- <?= /* @escapeNotVerified */ $block->getDateInput($_attribute, 'to') ?> -+ <?= /* @noEscape */ $block->getDateInput($_attribute, 'to') ?> - </div> - </div> - </div> -- <?php break; -- default: ?> -+ <?php -+ break; -+ default: -+ ?> - <input type="text" -- name="<?= /* @escapeNotVerified */ $_code ?>" -- id="<?= /* @escapeNotVerified */ $_code ?>" -+ name="<?= $block->escapeHtmlAttr($_code) ?>" -+ id="<?= $block->escapeHtmlAttr($_code) ?>" - value="<?= $block->escapeHtml($block->getAttributeValue($_attribute)) ?>" - title="<?= $block->escapeHtml($block->getAttributeLabel($_attribute)) ?>" -- class="input-text <?= /* @escapeNotVerified */ $block->getAttributeValidationClass($_attribute) ?>" -- maxlength="<?= /* @escapeNotVerified */ $maxQueryLength ?>" /> -+ class="input-text <?= $block->escapeHtmlAttr($block->getAttributeValidationClass($_attribute)) ?>" -+ maxlength="<?= $block->escapeHtmlAttr($maxQueryLength) ?>" /> - <?php endswitch; ?> - </div> - </div> -@@ -126,7 +137,7 @@ - <button type="submit" - class="action search primary" - title="<?= $block->escapeHtml(__('Search')) ?>"> -- <span><?= /* @escapeNotVerified */ __('Search') ?></span> -+ <span><?= $block->escapeHtml(__('Search')) ?></span> - </button> - </div> - </div> -@@ -147,8 +158,8 @@ require([ - } - }, - messages: { -- 'price[to]': {'greater-than-equals-to': 'Please enter a valid price range.'}, -- 'price[from]': {'less-than-equals-to': 'Please enter a valid price range.'} -+ 'price[to]': {'greater-than-equals-to': '<?= $block->escapeJs(__('Please enter a valid price range.')) ?>'}, -+ 'price[from]': {'less-than-equals-to': '<?= $block->escapeJs(__('Please enter a valid price range.')) ?>'} - } - }); - }); -diff --git a/app/code/Magento/CatalogSearch/view/frontend/templates/advanced/link.phtml b/app/code/Magento/CatalogSearch/view/frontend/templates/advanced/link.phtml -index 09098b1ccd0..2e183291da7 100644 ---- a/app/code/Magento/CatalogSearch/view/frontend/templates/advanced/link.phtml -+++ b/app/code/Magento/CatalogSearch/view/frontend/templates/advanced/link.phtml -@@ -4,13 +4,13 @@ - * See COPYING.txt for license details. - */ - --// @codingStandardsIgnoreFile -+// phpcs:disable Magento2.Templates.ThisInTemplate.FoundThis - - /** @var \Magento\CatalogSearch\Helper\Data $helper */ --$helper = $this->helper('Magento\CatalogSearch\Helper\Data'); -+$helper = $this->helper(\Magento\CatalogSearch\Helper\Data::class); - ?> - <div class="nested"> -- <a class="action advanced" href="<?= /* @escapeNotVerified */ $helper->getAdvancedSearchUrl() ?>" data-action="advanced-search"> -- <?= /* @escapeNotVerified */ __('Advanced Search') ?> -+ <a class="action advanced" href="<?= $block->escapeUrl($helper->getAdvancedSearchUrl()) ?>" data-action="advanced-search"> -+ <?= $block->escapeHtml(__('Advanced Search')) ?> - </a> - </div> -diff --git a/app/code/Magento/CatalogSearch/view/frontend/templates/advanced/result.phtml b/app/code/Magento/CatalogSearch/view/frontend/templates/advanced/result.phtml -index 83808df5b95..e21b031d695 100644 ---- a/app/code/Magento/CatalogSearch/view/frontend/templates/advanced/result.phtml -+++ b/app/code/Magento/CatalogSearch/view/frontend/templates/advanced/result.phtml -@@ -3,52 +3,51 @@ - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -- --// @codingStandardsIgnoreFile -- - ?> - <?php - /** - * @var $block \Magento\CatalogSearch\Block\Advanced\Result - */ -+/** this changes need for valid apply filters and configuration before search process is started */ -+$productList = $block->getProductListHtml(); - ?> --<?php if ($results = $block->getResultCount()): ?> -+<?php if ($results = $block->getResultCount()) : ?> - <div class="search found"> - <?php if ($results == 1) : ?> -- <?= /* @escapeNotVerified */ __('<strong>%1 item</strong> were found using the following search criteria', $results) ?> -- <?php else: ?> -- <?= /* @escapeNotVerified */ __('<strong>%1 items</strong> were found using the following search criteria', $results) ?> -+ <?= /* @noEscape */ __('<strong>%1 item</strong> were found using the following search criteria', $results) ?> -+ <?php else : ?> -+ <?= /* @noEscape */ __('<strong>%1 items</strong> were found using the following search criteria', $results) ?> - <?php endif; ?> - </div> --<?php else: ?> -+<?php else : ?> - <div role="alert" class="message error"> - <div> -- <?= /* @escapeNotVerified */ __('We can\'t find any items matching these search criteria.') ?> <a href="<?= /* @escapeNotVerified */ $block->getFormUrl() ?>"><?= /* @escapeNotVerified */ __('Modify your search.') ?></a> -+ <?= $block->escapeHtml(__('We can\'t find any items matching these search criteria.')) ?> <a href="<?= $block->escapeUrl($block->getFormUrl()) ?>"><?= $block->escapeHtml(__('Modify your search.')) ?></a> - </div> - </div> - <?php endif; ?> - - <?php $searchCriterias = $block->getSearchCriterias(); ?> - <div class="search summary"> -- <?php foreach (['left', 'right'] as $side): ?> -- <?php if (@$searchCriterias[$side]): ?> -+ <?php foreach (['left', 'right'] as $side) : ?> -+ <?php if (!empty($searchCriterias[$side])) : ?> - <ul class="items"> -- <?php foreach ($searchCriterias[$side] as $criteria): ?> -+ <?php foreach ($searchCriterias[$side] as $criteria) : ?> - <li class="item"><strong><?= $block->escapeHtml(__($criteria['name'])) ?>:</strong> <?= $block->escapeHtml($criteria['value']) ?></li> - <?php endforeach; ?> - </ul> - <?php endif; ?> - <?php endforeach; ?> - </div> --<?php if ($block->getResultCount()): ?> -+<?php if ($block->getResultCount()) : ?> - <div class="message notice"> - <div> -- <?= /* @escapeNotVerified */ __("Don't see what you're looking for?") ?> -- <a href="<?= /* @escapeNotVerified */ $block->getFormUrl() ?>"><?= /* @escapeNotVerified */ __('Modify your search.') ?></a> -+ <?= $block->escapeHtml(__("Don't see what you're looking for?")) ?> -+ <a href="<?= $block->escapeUrl($block->getFormUrl()) ?>"><?= $block->escapeHtml(__('Modify your search.')) ?></a> - </div> - </div> - <?php endif; ?> --<?php if ($block->getResultCount()): ?> -- <div class="search results"><?= $block->getProductListHtml() ?></div> -+<?php if ($block->getResultCount()) : ?> -+ <div class="search results"><?= /* @noEscape */ $productList ?></div> - <?php endif; ?> - <?php $block->getSearchCriterias(); ?> -diff --git a/app/code/Magento/CatalogSearch/view/frontend/templates/result.phtml b/app/code/Magento/CatalogSearch/view/frontend/templates/result.phtml -index 2757ae3b5f7..c63e6ff4abe 100644 ---- a/app/code/Magento/CatalogSearch/view/frontend/templates/result.phtml -+++ b/app/code/Magento/CatalogSearch/view/frontend/templates/result.phtml -@@ -3,34 +3,31 @@ - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -- --// @codingStandardsIgnoreFile -- - ?> --<?php if ($block->getResultCount()): ?> --<?= $block->getChildHtml('tagged_product_list_rss_link') ?> -+<?php if ($block->getResultCount()) : ?> -+ <?= /* @noEscape */ $block->getChildHtml('tagged_product_list_rss_link') ?> - <div class="search results"> -- <?php if ($messages = $block->getNoteMessages()):?> -+ <?php if ($messages = $block->getNoteMessages()) : ?> - <div class="message notice"> - <div> -- <?php foreach ($messages as $message):?> -- <?= /* @escapeNotVerified */ $message ?><br /> -- <?php endforeach;?> -+ <?php foreach ($messages as $message) : ?> -+ <?= /* @noEscape */ $message ?><br /> -+ <?php endforeach; ?> - </div> - </div> - <?php endif; ?> - <?= $block->getProductListHtml() ?> - </div> --<?php else: ?> -+<?php else : ?> - - <div class="message notice"> - <div> -- <?= /* @escapeNotVerified */ ($block->getNoResultText()) ? $block->getNoResultText() : __('Your search returned no results.') ?> -- <?= $block->getAdditionalHtml() ?> -- <?php if ($messages = $block->getNoteMessages()):?> -- <?php foreach ($messages as $message):?> -- <br /><?= /* @escapeNotVerified */ $message ?> -- <?php endforeach;?> -+ <?= $block->escapeHtml($block->getNoResultText() ? $block->getNoResultText() : __('Your search returned no results.')) ?> -+ <?= /* @noEscape */ $block->getAdditionalHtml() ?> -+ <?php if ($messages = $block->getNoteMessages()) : ?> -+ <?php foreach ($messages as $message) : ?> -+ <br /><?= /* @noEscape */ $message ?> -+ <?php endforeach; ?> - <?php endif; ?> - </div> - </div> -diff --git a/app/code/Magento/CatalogSearch/view/frontend/templates/search_terms_log.phtml b/app/code/Magento/CatalogSearch/view/frontend/templates/search_terms_log.phtml -index 61609bdf66b..38ef11933a4 100644 ---- a/app/code/Magento/CatalogSearch/view/frontend/templates/search_terms_log.phtml -+++ b/app/code/Magento/CatalogSearch/view/frontend/templates/search_terms_log.phtml -@@ -3,14 +3,13 @@ - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ --// @codingStandardsIgnoreFile - ?> --<?php if ($block->getSearchTermsLog()->isPageCacheable()): ?> -+<?php if ($block->getSearchTermsLog()->isPageCacheable()) : ?> - <script type="text/x-magento-init"> - { - "*": { - "Magento_CatalogSearch/js/search-terms-log": { -- "url": "<?= /* @escapeNotVerified */ $block->getUrl('catalogsearch/searchTermsLog/save') ?>" -+ "url": "<?= $block->escapeUrl($block->getUrl('catalogsearch/searchTermsLog/save')) ?>" - } - } - } -diff --git a/app/code/Magento/CatalogUrlRewrite/Model/Category/ChildrenUrlRewriteGenerator.php b/app/code/Magento/CatalogUrlRewrite/Model/Category/ChildrenUrlRewriteGenerator.php -index 6aa33f37cd3..d18034220cf 100644 ---- a/app/code/Magento/CatalogUrlRewrite/Model/Category/ChildrenUrlRewriteGenerator.php -+++ b/app/code/Magento/CatalogUrlRewrite/Model/Category/ChildrenUrlRewriteGenerator.php -@@ -10,7 +10,11 @@ use Magento\CatalogUrlRewrite\Model\CategoryUrlRewriteGeneratorFactory; - use Magento\CatalogUrlRewrite\Model\CategoryUrlRewriteGenerator; - use Magento\UrlRewrite\Model\MergeDataProviderFactory; - use Magento\Framework\App\ObjectManager; -+use Magento\Catalog\Api\CategoryRepositoryInterface; - -+/** -+ * Model for generate url rewrites for children categories -+ */ - class ChildrenUrlRewriteGenerator - { - /** -@@ -28,15 +32,22 @@ class ChildrenUrlRewriteGenerator - */ - private $mergeDataProviderPrototype; - -+ /** -+ * @var CategoryRepositoryInterface -+ */ -+ private $categoryRepository; -+ - /** - * @param \Magento\CatalogUrlRewrite\Model\Category\ChildrenCategoriesProvider $childrenCategoriesProvider - * @param \Magento\CatalogUrlRewrite\Model\CategoryUrlRewriteGeneratorFactory $categoryUrlRewriteGeneratorFactory - * @param \Magento\UrlRewrite\Model\MergeDataProviderFactory|null $mergeDataProviderFactory -+ * @param CategoryRepositoryInterface|null $categoryRepository - */ - public function __construct( - ChildrenCategoriesProvider $childrenCategoriesProvider, - CategoryUrlRewriteGeneratorFactory $categoryUrlRewriteGeneratorFactory, -- MergeDataProviderFactory $mergeDataProviderFactory = null -+ MergeDataProviderFactory $mergeDataProviderFactory = null, -+ CategoryRepositoryInterface $categoryRepository = null - ) { - $this->childrenCategoriesProvider = $childrenCategoriesProvider; - $this->categoryUrlRewriteGeneratorFactory = $categoryUrlRewriteGeneratorFactory; -@@ -44,6 +55,8 @@ class ChildrenUrlRewriteGenerator - $mergeDataProviderFactory = ObjectManager::getInstance()->get(MergeDataProviderFactory::class); - } - $this->mergeDataProviderPrototype = $mergeDataProviderFactory->create(); -+ $this->categoryRepository = $categoryRepository -+ ?: ObjectManager::getInstance()->get(CategoryRepositoryInterface::class); - } - - /** -@@ -53,18 +66,23 @@ class ChildrenUrlRewriteGenerator - * @param \Magento\Catalog\Model\Category $category - * @param int|null $rootCategoryId - * @return \Magento\UrlRewrite\Service\V1\Data\UrlRewrite[] -+ * @throws \Magento\Framework\Exception\NoSuchEntityException - */ - public function generate($storeId, Category $category, $rootCategoryId = null) - { - $mergeDataProvider = clone $this->mergeDataProviderPrototype; -- foreach ($this->childrenCategoriesProvider->getChildren($category, true) as $childCategory) { -- $childCategory->setStoreId($storeId); -- $childCategory->setData('save_rewrites_history', $category->getData('save_rewrites_history')); -- /** @var CategoryUrlRewriteGenerator $categoryUrlRewriteGenerator */ -- $categoryUrlRewriteGenerator = $this->categoryUrlRewriteGeneratorFactory->create(); -- $mergeDataProvider->merge( -- $categoryUrlRewriteGenerator->generate($childCategory, false, $rootCategoryId) -- ); -+ $childrenIds = $this->childrenCategoriesProvider->getChildrenIds($category, true); -+ if ($childrenIds) { -+ foreach ($childrenIds as $childId) { -+ /** @var Category $childCategory */ -+ $childCategory = $this->categoryRepository->get($childId, $storeId); -+ $childCategory->setData('save_rewrites_history', $category->getData('save_rewrites_history')); -+ /** @var CategoryUrlRewriteGenerator $categoryUrlRewriteGenerator */ -+ $categoryUrlRewriteGenerator = $this->categoryUrlRewriteGeneratorFactory->create(); -+ $mergeDataProvider->merge( -+ $categoryUrlRewriteGenerator->generate($childCategory, false, $rootCategoryId) -+ ); -+ } - } - - return $mergeDataProvider->getData(); -diff --git a/app/code/Magento/CatalogUrlRewrite/Model/Category/Plugin/Category/Move.php b/app/code/Magento/CatalogUrlRewrite/Model/Category/Plugin/Category/Move.php -index 17d12ba563e..f3984bf7d62 100644 ---- a/app/code/Magento/CatalogUrlRewrite/Model/Category/Plugin/Category/Move.php -+++ b/app/code/Magento/CatalogUrlRewrite/Model/Category/Plugin/Category/Move.php -@@ -6,9 +6,14 @@ - namespace Magento\CatalogUrlRewrite\Model\Category\Plugin\Category; - - use Magento\Catalog\Model\Category; -+use Magento\Catalog\Model\CategoryFactory; - use Magento\CatalogUrlRewrite\Model\CategoryUrlPathGenerator; - use Magento\CatalogUrlRewrite\Model\Category\ChildrenCategoriesProvider; -+use Magento\Store\Model\Store; - -+/** -+ * Perform url updating for children categories. -+ */ - class Move - { - /** -@@ -21,16 +26,24 @@ class Move - */ - private $childrenCategoriesProvider; - -+ /** -+ * @var CategoryFactory -+ */ -+ private $categoryFactory; -+ - /** - * @param CategoryUrlPathGenerator $categoryUrlPathGenerator - * @param ChildrenCategoriesProvider $childrenCategoriesProvider -+ * @param CategoryFactory $categoryFactory - */ - public function __construct( - CategoryUrlPathGenerator $categoryUrlPathGenerator, -- ChildrenCategoriesProvider $childrenCategoriesProvider -+ ChildrenCategoriesProvider $childrenCategoriesProvider, -+ CategoryFactory $categoryFactory - ) { - $this->categoryUrlPathGenerator = $categoryUrlPathGenerator; - $this->childrenCategoriesProvider = $childrenCategoriesProvider; -+ $this->categoryFactory = $categoryFactory; - } - - /** -@@ -51,20 +64,57 @@ class Move - Category $newParent, - $afterCategoryId - ) { -- $category->setUrlPath($this->categoryUrlPathGenerator->getUrlPath($category)); -- $category->getResource()->saveAttribute($category, 'url_path'); -- $this->updateUrlPathForChildren($category); -+ $categoryStoreId = $category->getStoreId(); -+ foreach ($category->getStoreIds() as $storeId) { -+ $category->setStoreId($storeId); -+ if (!$this->isGlobalScope($storeId)) { -+ $this->updateCategoryUrlKeyForStore($category); -+ $category->unsUrlPath(); -+ $category->setUrlPath($this->categoryUrlPathGenerator->getUrlPath($category)); -+ $category->getResource()->saveAttribute($category, 'url_path'); -+ $this->updateUrlPathForChildren($category); -+ } -+ } -+ $category->setStoreId($categoryStoreId); - - return $result; - } - - /** -+ * Set category url_key according to current category store id. -+ * -+ * @param Category $category -+ * @return void -+ */ -+ private function updateCategoryUrlKeyForStore(Category $category) -+ { -+ $item = $this->categoryFactory->create(); -+ $item->setStoreId($category->getStoreId()); -+ $item->load($category->getId()); -+ $category->setUrlKey($item->getUrlKey()); -+ } -+ -+ /** -+ * Check is global scope. -+ * -+ * @param int|null $storeId -+ * @return bool -+ */ -+ private function isGlobalScope($storeId) -+ { -+ return null === $storeId || $storeId == Store::DEFAULT_STORE_ID; -+ } -+ -+ /** -+ * Updates url_path for child categories. -+ * - * @param Category $category - * @return void - */ -- protected function updateUrlPathForChildren($category) -+ private function updateUrlPathForChildren($category) - { - foreach ($this->childrenCategoriesProvider->getChildren($category, true) as $childCategory) { -+ $childCategory->setStoreId($category->getStoreId()); - $childCategory->unsUrlPath(); - $childCategory->setUrlPath($this->categoryUrlPathGenerator->getUrlPath($childCategory)); - $childCategory->getResource()->saveAttribute($childCategory, 'url_path'); -diff --git a/app/code/Magento/CatalogUrlRewrite/Model/Category/Plugin/Storage.php b/app/code/Magento/CatalogUrlRewrite/Model/Category/Plugin/Storage.php -index 572152e84b2..00bf88675e7 100644 ---- a/app/code/Magento/CatalogUrlRewrite/Model/Category/Plugin/Storage.php -+++ b/app/code/Magento/CatalogUrlRewrite/Model/Category/Plugin/Storage.php -@@ -11,6 +11,9 @@ use Magento\UrlRewrite\Model\UrlFinderInterface; - use Magento\UrlRewrite\Service\V1\Data\UrlRewrite; - use Magento\CatalogUrlRewrite\Model\ResourceModel\Category\Product; - -+/** -+ * Storage Plugin -+ */ - class Storage - { - /** -@@ -36,6 +39,8 @@ class Storage - } - - /** -+ * Save product/category urlRewrite association -+ * - * @param \Magento\UrlRewrite\Model\StorageInterface $object - * @param \Magento\UrlRewrite\Service\V1\Data\UrlRewrite[] $result - * @param \Magento\UrlRewrite\Service\V1\Data\UrlRewrite[] $urls -@@ -53,13 +58,15 @@ class Storage - 'product_id' => $record->getEntityId(), - ]; - } -- if ($toSave) { -+ if (count($toSave) > 0) { - $this->productResource->saveMultiple($toSave); - } - return $result; - } - - /** -+ * Remove product/category urlRewrite association -+ * - * @param \Magento\UrlRewrite\Model\StorageInterface $object - * @param array $data - * @return void -@@ -71,6 +78,8 @@ class Storage - } - - /** -+ * Filter urls -+ * - * @param \Magento\UrlRewrite\Service\V1\Data\UrlRewrite[] $urls - * @return \Magento\UrlRewrite\Service\V1\Data\UrlRewrite[] - */ -@@ -96,6 +105,8 @@ class Storage - } - - /** -+ * Check if url is correct -+ * - * @param UrlRewrite $url - * @return bool - */ -diff --git a/app/code/Magento/CatalogUrlRewrite/Model/CategoryUrlPathGenerator.php b/app/code/Magento/CatalogUrlRewrite/Model/CategoryUrlPathGenerator.php -index ee20b0e934b..cba9218ce7c 100644 ---- a/app/code/Magento/CatalogUrlRewrite/Model/CategoryUrlPathGenerator.php -+++ b/app/code/Magento/CatalogUrlRewrite/Model/CategoryUrlPathGenerator.php -@@ -8,6 +8,9 @@ namespace Magento\CatalogUrlRewrite\Model; - use Magento\Catalog\Api\CategoryRepositoryInterface; - use Magento\Catalog\Model\Category; - -+/** -+ * Class for generation category url_path -+ */ - class CategoryUrlPathGenerator - { - /** -@@ -61,9 +64,11 @@ class CategoryUrlPathGenerator - * Build category URL path - * - * @param \Magento\Catalog\Api\Data\CategoryInterface|\Magento\Framework\Model\AbstractModel $category -+ * @param null|\Magento\Catalog\Api\Data\CategoryInterface|\Magento\Framework\Model\AbstractModel $parentCategory - * @return string -+ * @throws \Magento\Framework\Exception\NoSuchEntityException - */ -- public function getUrlPath($category) -+ public function getUrlPath($category, $parentCategory = null) - { - if (in_array($category->getParentId(), [Category::ROOT_CATEGORY_ID, Category::TREE_ROOT_ID])) { - return ''; -@@ -77,15 +82,17 @@ class CategoryUrlPathGenerator - return $category->getUrlPath(); - } - if ($this->isNeedToGenerateUrlPathForParent($category)) { -- $parentPath = $this->getUrlPath( -- $this->categoryRepository->get($category->getParentId(), $category->getStoreId()) -- ); -+ $parentCategory = $parentCategory === null ? -+ $this->categoryRepository->get($category->getParentId(), $category->getStoreId()) : $parentCategory; -+ $parentPath = $this->getUrlPath($parentCategory); - $path = $parentPath === '' ? $path : $parentPath . '/' . $path; - } - return $path; - } - - /** -+ * Define whether we should generate URL path for parent -+ * - * @param \Magento\Catalog\Model\Category $category - * @return bool - */ -diff --git a/app/code/Magento/CatalogUrlRewrite/Model/CategoryUrlRewriteGenerator.php b/app/code/Magento/CatalogUrlRewrite/Model/CategoryUrlRewriteGenerator.php -index b2da0ab39f3..a86604672e2 100644 ---- a/app/code/Magento/CatalogUrlRewrite/Model/CategoryUrlRewriteGenerator.php -+++ b/app/code/Magento/CatalogUrlRewrite/Model/CategoryUrlRewriteGenerator.php -@@ -15,6 +15,9 @@ use Magento\Catalog\Api\CategoryRepositoryInterface; - use Magento\Framework\App\ObjectManager; - use Magento\UrlRewrite\Model\MergeDataProviderFactory; - -+/** -+ * Generate list of urls. -+ */ - class CategoryUrlRewriteGenerator - { - /** Entity type code */ -@@ -84,6 +87,8 @@ class CategoryUrlRewriteGenerator - } - - /** -+ * Generate list of urls. -+ * - * @param \Magento\Catalog\Model\Category $category - * @param bool $overrideStoreUrls - * @param int|null $rootCategoryId -@@ -119,6 +124,7 @@ class CategoryUrlRewriteGenerator - $mergeDataProvider = clone $this->mergeDataProviderPrototype; - $categoryId = $category->getId(); - foreach ($category->getStoreIds() as $storeId) { -+ $category->setStoreId($storeId); - if (!$this->isGlobalScope($storeId) - && $this->isOverrideUrlsForStore($storeId, $categoryId, $overrideStoreUrls) - ) { -@@ -131,6 +137,8 @@ class CategoryUrlRewriteGenerator - } - - /** -+ * Checks if urls should be overridden for store. -+ * - * @param int $storeId - * @param int $categoryId - * @param bool $overrideStoreUrls -diff --git a/app/code/Magento/CatalogUrlRewrite/Model/ObjectRegistry.php b/app/code/Magento/CatalogUrlRewrite/Model/ObjectRegistry.php -index 019959f9c1f..a048c216139 100644 ---- a/app/code/Magento/CatalogUrlRewrite/Model/ObjectRegistry.php -+++ b/app/code/Magento/CatalogUrlRewrite/Model/ObjectRegistry.php -@@ -5,6 +5,9 @@ - */ - namespace Magento\CatalogUrlRewrite\Model; - -+/** -+ * Class ObjectRegistry -+ */ - class ObjectRegistry - { - /** -@@ -26,15 +29,19 @@ class ObjectRegistry - } - - /** -+ * Get Entity -+ * - * @param int $entityId - * @return \Magento\Framework\DataObject|null - */ - public function get($entityId) - { -- return isset($this->entitiesMap[$entityId]) ? $this->entitiesMap[$entityId] : null; -+ return $this->entitiesMap[$entityId] ?? null; - } - - /** -+ * List Entities -+ * - * @return \Magento\Framework\DataObject[] - */ - public function getList() -diff --git a/app/code/Magento/CatalogUrlRewrite/Model/Product/AnchorUrlRewriteGenerator.php b/app/code/Magento/CatalogUrlRewrite/Model/Product/AnchorUrlRewriteGenerator.php -index a7cc894c9a0..4a191b54dea 100644 ---- a/app/code/Magento/CatalogUrlRewrite/Model/Product/AnchorUrlRewriteGenerator.php -+++ b/app/code/Magento/CatalogUrlRewrite/Model/Product/AnchorUrlRewriteGenerator.php -@@ -13,7 +13,12 @@ use Magento\CatalogUrlRewrite\Model\ProductUrlRewriteGenerator; - use Magento\Framework\Exception\NoSuchEntityException; - use Magento\UrlRewrite\Service\V1\Data\UrlRewrite; - use Magento\UrlRewrite\Service\V1\Data\UrlRewriteFactory; -+use Magento\Framework\App\Config\ScopeConfigInterface; -+use Magento\Framework\App\ObjectManager; - -+/** -+ * Generate url rewrites for anchor categories -+ */ - class AnchorUrlRewriteGenerator - { - /** -@@ -52,7 +57,7 @@ class AnchorUrlRewriteGenerator - * @param int $storeId - * @param Product $product - * @param ObjectRegistry $productCategories -- * @return UrlRewrite[] -+ * @return UrlRewrite[]|array - */ - public function generate($storeId, Product $product, ObjectRegistry $productCategories) - { -diff --git a/app/code/Magento/CatalogUrlRewrite/Model/Product/CategoriesUrlRewriteGenerator.php b/app/code/Magento/CatalogUrlRewrite/Model/Product/CategoriesUrlRewriteGenerator.php -index 9e787e74ae0..a7e4b511ecd 100644 ---- a/app/code/Magento/CatalogUrlRewrite/Model/Product/CategoriesUrlRewriteGenerator.php -+++ b/app/code/Magento/CatalogUrlRewrite/Model/Product/CategoriesUrlRewriteGenerator.php -@@ -11,7 +11,12 @@ use Magento\CatalogUrlRewrite\Model\ProductUrlPathGenerator; - use Magento\CatalogUrlRewrite\Model\ProductUrlRewriteGenerator; - use Magento\UrlRewrite\Service\V1\Data\UrlRewrite; - use Magento\UrlRewrite\Service\V1\Data\UrlRewriteFactory; -+use Magento\Framework\App\Config\ScopeConfigInterface; -+use Magento\Framework\App\ObjectManager; - -+/** -+ * Generate url rewrites for categories -+ */ - class CategoriesUrlRewriteGenerator - { - /** -@@ -28,8 +33,10 @@ class CategoriesUrlRewriteGenerator - * @param ProductUrlPathGenerator $productUrlPathGenerator - * @param UrlRewriteFactory $urlRewriteFactory - */ -- public function __construct(ProductUrlPathGenerator $productUrlPathGenerator, UrlRewriteFactory $urlRewriteFactory) -- { -+ public function __construct( -+ ProductUrlPathGenerator $productUrlPathGenerator, -+ UrlRewriteFactory $urlRewriteFactory -+ ) { - $this->productUrlPathGenerator = $productUrlPathGenerator; - $this->urlRewriteFactory = $urlRewriteFactory; - } -diff --git a/app/code/Magento/CatalogUrlRewrite/Model/ProductScopeRewriteGenerator.php b/app/code/Magento/CatalogUrlRewrite/Model/ProductScopeRewriteGenerator.php -index 6b838f83d31..9d26184e2c2 100644 ---- a/app/code/Magento/CatalogUrlRewrite/Model/ProductScopeRewriteGenerator.php -+++ b/app/code/Magento/CatalogUrlRewrite/Model/ProductScopeRewriteGenerator.php -@@ -13,6 +13,7 @@ use Magento\CatalogUrlRewrite\Model\Product\CanonicalUrlRewriteGenerator; - use Magento\CatalogUrlRewrite\Model\Product\CategoriesUrlRewriteGenerator; - use Magento\CatalogUrlRewrite\Model\Product\CurrentUrlRewritesRegenerator; - use Magento\CatalogUrlRewrite\Service\V1\StoreViewService; -+use Magento\Framework\App\Config\ScopeConfigInterface; - use Magento\Framework\App\ObjectManager; - use Magento\Store\Model\Store; - use Magento\Store\Model\StoreManagerInterface; -@@ -34,6 +35,11 @@ class ProductScopeRewriteGenerator - */ - private $storeManager; - -+ /** -+ * @var ScopeConfigInterface -+ */ -+ private $config; -+ - /** - * @var ObjectRegistryFactory - */ -@@ -79,6 +85,8 @@ class ProductScopeRewriteGenerator - * @param AnchorUrlRewriteGenerator $anchorUrlRewriteGenerator - * @param \Magento\UrlRewrite\Model\MergeDataProviderFactory|null $mergeDataProviderFactory - * @param CategoryRepositoryInterface|null $categoryRepository -+ * @param ScopeConfigInterface|null $config -+ * @SuppressWarnings(PHPMD.ExcessiveParameterList) - */ - public function __construct( - StoreViewService $storeViewService, -@@ -89,7 +97,8 @@ class ProductScopeRewriteGenerator - CurrentUrlRewritesRegenerator $currentUrlRewritesRegenerator, - AnchorUrlRewriteGenerator $anchorUrlRewriteGenerator, - MergeDataProviderFactory $mergeDataProviderFactory = null, -- CategoryRepositoryInterface $categoryRepository = null -+ CategoryRepositoryInterface $categoryRepository = null, -+ ScopeConfigInterface $config = null - ) { - $this->storeViewService = $storeViewService; - $this->storeManager = $storeManager; -@@ -104,6 +113,7 @@ class ProductScopeRewriteGenerator - $this->mergeDataProviderPrototype = $mergeDataProviderFactory->create(); - $this->categoryRepository = $categoryRepository ?: - ObjectManager::getInstance()->get(CategoryRepositoryInterface::class); -+ $this->config = $config ?: ObjectManager::getInstance()->get(ScopeConfigInterface::class); - } - - /** -@@ -173,9 +183,13 @@ class ProductScopeRewriteGenerator - $mergeDataProvider->merge( - $this->canonicalUrlRewriteGenerator->generate($storeId, $product) - ); -- $mergeDataProvider->merge( -- $this->categoriesUrlRewriteGenerator->generate($storeId, $product, $productCategories) -- ); -+ -+ if ($this->isCategoryRewritesEnabled()) { -+ $mergeDataProvider->merge( -+ $this->categoriesUrlRewriteGenerator->generate($storeId, $product, $productCategories) -+ ); -+ } -+ - $mergeDataProvider->merge( - $this->currentUrlRewritesRegenerator->generate( - $storeId, -@@ -184,9 +198,13 @@ class ProductScopeRewriteGenerator - $rootCategoryId - ) - ); -- $mergeDataProvider->merge( -- $this->anchorUrlRewriteGenerator->generate($storeId, $product, $productCategories) -- ); -+ -+ if ($this->isCategoryRewritesEnabled()) { -+ $mergeDataProvider->merge( -+ $this->anchorUrlRewriteGenerator->generate($storeId, $product, $productCategories) -+ ); -+ } -+ - $mergeDataProvider->merge( - $this->currentUrlRewritesRegenerator->generateAnchor( - $storeId, -@@ -195,6 +213,7 @@ class ProductScopeRewriteGenerator - $rootCategoryId - ) - ); -+ - return $mergeDataProvider->getData(); - } - -@@ -216,10 +235,12 @@ class ProductScopeRewriteGenerator - } - - /** -+ * Check if URL key has been changed -+ * - * Checks if URL key has been changed for provided category and returns reloaded category, - * in other case - returns provided category. - * -- * @param $storeId -+ * @param int $storeId - * @param Category $category - * @return Category - */ -@@ -236,4 +257,14 @@ class ProductScopeRewriteGenerator - } - return $this->categoryRepository->get($category->getEntityId(), $storeId); - } -+ -+ /** -+ * Check config value of generate_category_product_rewrites -+ * -+ * @return bool -+ */ -+ private function isCategoryRewritesEnabled() -+ { -+ return (bool)$this->config->getValue('catalog/seo/generate_category_product_rewrites'); -+ } - } -diff --git a/app/code/Magento/CatalogUrlRewrite/Model/ProductUrlPathGenerator.php b/app/code/Magento/CatalogUrlRewrite/Model/ProductUrlPathGenerator.php -index 2e192d895c6..ac3a5092bb3 100644 ---- a/app/code/Magento/CatalogUrlRewrite/Model/ProductUrlPathGenerator.php -+++ b/app/code/Magento/CatalogUrlRewrite/Model/ProductUrlPathGenerator.php -@@ -3,10 +3,21 @@ - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -+ - namespace Magento\CatalogUrlRewrite\Model; - - use Magento\Store\Model\Store; -+use Magento\Catalog\Api\ProductRepositoryInterface; -+use Magento\Catalog\Model\Category; -+use Magento\Catalog\Model\Product; -+use Magento\CatalogUrlRewrite\Model\CategoryUrlPathGenerator; -+use Magento\Framework\App\Config\ScopeConfigInterface; -+use Magento\Store\Model\ScopeInterface; -+use Magento\Store\Model\StoreManagerInterface; - -+/** -+ * Class ProductUrlPathGenerator -+ */ - class ProductUrlPathGenerator - { - const XML_PATH_PRODUCT_URL_SUFFIX = 'catalog/seo/product_url_suffix'; -@@ -19,36 +30,36 @@ class ProductUrlPathGenerator - protected $productUrlSuffix = []; - - /** -- * @var \Magento\Store\Model\StoreManagerInterface -+ * @var StoreManagerInterface - */ - protected $storeManager; - - /** -- * @var \Magento\Framework\App\Config\ScopeConfigInterface -+ * @var ScopeConfigInterface - */ - protected $scopeConfig; - - /** -- * @var \Magento\CatalogUrlRewrite\Model\CategoryUrlPathGenerator -+ * @var CategoryUrlPathGenerator - */ - protected $categoryUrlPathGenerator; - - /** -- * @var \Magento\Catalog\Api\ProductRepositoryInterface -+ * @var ProductRepositoryInterface - */ - protected $productRepository; - - /** -- * @param \Magento\Store\Model\StoreManagerInterface $storeManager -- * @param \Magento\Framework\App\Config\ScopeConfigInterface $scopeConfig -+ * @param StoreManagerInterface $storeManager -+ * @param ScopeConfigInterface $scopeConfig - * @param CategoryUrlPathGenerator $categoryUrlPathGenerator -- * @param \Magento\Catalog\Api\ProductRepositoryInterface $productRepository -+ * @param ProductRepositoryInterface $productRepository - */ - public function __construct( -- \Magento\Store\Model\StoreManagerInterface $storeManager, -- \Magento\Framework\App\Config\ScopeConfigInterface $scopeConfig, -- \Magento\CatalogUrlRewrite\Model\CategoryUrlPathGenerator $categoryUrlPathGenerator, -- \Magento\Catalog\Api\ProductRepositoryInterface $productRepository -+ StoreManagerInterface $storeManager, -+ ScopeConfigInterface $scopeConfig, -+ CategoryUrlPathGenerator $categoryUrlPathGenerator, -+ ProductRepositoryInterface $productRepository - ) { - $this->storeManager = $storeManager; - $this->scopeConfig = $scopeConfig; -@@ -59,8 +70,8 @@ class ProductUrlPathGenerator - /** - * Retrieve Product Url path (with category if exists) - * -- * @param \Magento\Catalog\Model\Product $product -- * @param \Magento\Catalog\Model\Category $category -+ * @param Product $product -+ * @param Category $category - * - * @return string - */ -@@ -80,10 +91,10 @@ class ProductUrlPathGenerator - /** - * Prepare URL Key with stored product data (fallback for "Use Default Value" logic) - * -- * @param \Magento\Catalog\Model\Product $product -+ * @param Product $product - * @return string - */ -- protected function prepareProductDefaultUrlKey(\Magento\Catalog\Model\Product $product) -+ protected function prepareProductDefaultUrlKey(Product $product) - { - $storedProduct = $this->productRepository->getById($product->getId()); - $storedUrlKey = $storedProduct->getUrlKey(); -@@ -93,9 +104,9 @@ class ProductUrlPathGenerator - /** - * Retrieve Product Url path with suffix - * -- * @param \Magento\Catalog\Model\Product $product -+ * @param Product $product - * @param int $storeId -- * @param \Magento\Catalog\Model\Category $category -+ * @param Category $category - * @return string - */ - public function getUrlPathWithSuffix($product, $storeId, $category = null) -@@ -106,8 +117,8 @@ class ProductUrlPathGenerator - /** - * Get canonical product url path - * -- * @param \Magento\Catalog\Model\Product $product -- * @param \Magento\Catalog\Model\Category|null $category -+ * @param Product $product -+ * @param Category|null $category - * @return string - */ - public function getCanonicalUrlPath($product, $category = null) -@@ -119,24 +130,27 @@ class ProductUrlPathGenerator - /** - * Generate product url key based on url_key entered by merchant or product name - * -- * @param \Magento\Catalog\Model\Product $product -- * @return string -+ * @param Product $product -+ * @return string|null - */ - public function getUrlKey($product) - { -- return $product->getUrlKey() === false ? false : $this->prepareProductUrlKey($product); -+ $generatedProductUrlKey = $this->prepareProductUrlKey($product); -+ return ($product->getUrlKey() === false || empty($generatedProductUrlKey)) ? null : $generatedProductUrlKey; - } - - /** - * Prepare url key for product - * -- * @param \Magento\Catalog\Model\Product $product -+ * @param Product $product - * @return string - */ -- protected function prepareProductUrlKey(\Magento\Catalog\Model\Product $product) -+ protected function prepareProductUrlKey(Product $product) - { -- $urlKey = $product->getUrlKey(); -- return $product->formatUrlKey($urlKey === '' || $urlKey === null ? $product->getName() : $urlKey); -+ $urlKey = (string)$product->getUrlKey(); -+ $urlKey = trim(strtolower($urlKey)); -+ -+ return $urlKey ?: $product->formatUrlKey($product->getName()); - } - - /** -@@ -154,7 +168,7 @@ class ProductUrlPathGenerator - if (!isset($this->productUrlSuffix[$storeId])) { - $this->productUrlSuffix[$storeId] = $this->scopeConfig->getValue( - self::XML_PATH_PRODUCT_URL_SUFFIX, -- \Magento\Store\Model\ScopeInterface::SCOPE_STORE, -+ ScopeInterface::SCOPE_STORE, - $storeId - ); - } -diff --git a/app/code/Magento/CatalogUrlRewrite/Model/Products/AdaptUrlRewritesToVisibilityAttribute.php b/app/code/Magento/CatalogUrlRewrite/Model/Products/AdaptUrlRewritesToVisibilityAttribute.php -new file mode 100644 -index 00000000000..f5851cf1e11 ---- /dev/null -+++ b/app/code/Magento/CatalogUrlRewrite/Model/Products/AdaptUrlRewritesToVisibilityAttribute.php -@@ -0,0 +1,127 @@ -+<?php -+/** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+declare(strict_types=1); -+ -+namespace Magento\CatalogUrlRewrite\Model\Products; -+ -+use Magento\Catalog\Api\Data\ProductInterface; -+use Magento\Catalog\Model\Product; -+use Magento\Catalog\Model\Product\Visibility; -+use Magento\Catalog\Model\ResourceModel\Product\CollectionFactory; -+use Magento\CatalogUrlRewrite\Model\ProductUrlPathGenerator; -+use Magento\CatalogUrlRewrite\Model\ProductUrlRewriteGenerator; -+use Magento\UrlRewrite\Model\Exception\UrlAlreadyExistsException; -+use Magento\UrlRewrite\Model\UrlPersistInterface; -+use Magento\UrlRewrite\Service\V1\Data\UrlRewrite; -+ -+/** -+ * Save/Delete UrlRewrites by Product ID's and visibility -+ */ -+class AdaptUrlRewritesToVisibilityAttribute -+{ -+ /** -+ * @var CollectionFactory -+ */ -+ private $productCollectionFactory; -+ -+ /** -+ * @var ProductUrlRewriteGenerator -+ */ -+ private $urlRewriteGenerator; -+ -+ /** -+ * @var UrlPersistInterface -+ */ -+ private $urlPersist; -+ -+ /** -+ * @var ProductUrlPathGenerator -+ */ -+ private $urlPathGenerator; -+ -+ /** -+ * @param CollectionFactory $collectionFactory -+ * @param ProductUrlRewriteGenerator $urlRewriteGenerator -+ * @param UrlPersistInterface $urlPersist -+ * @param ProductUrlPathGenerator|null $urlPathGenerator -+ */ -+ public function __construct( -+ CollectionFactory $collectionFactory, -+ ProductUrlRewriteGenerator $urlRewriteGenerator, -+ UrlPersistInterface $urlPersist, -+ ProductUrlPathGenerator $urlPathGenerator -+ ) { -+ $this->productCollectionFactory = $collectionFactory; -+ $this->urlRewriteGenerator = $urlRewriteGenerator; -+ $this->urlPersist = $urlPersist; -+ $this->urlPathGenerator = $urlPathGenerator; -+ } -+ -+ /** -+ * Process Url Rewrites according to the products visibility attribute -+ * -+ * @param array $productIds -+ * @param int $visibility -+ * @throws UrlAlreadyExistsException -+ */ -+ public function execute(array $productIds, int $visibility): void -+ { -+ $products = $this->getProductsByIds($productIds); -+ -+ /** @var Product $product */ -+ foreach ($products as $product) { -+ if ($visibility == Visibility::VISIBILITY_NOT_VISIBLE) { -+ $this->urlPersist->deleteByData( -+ [ -+ UrlRewrite::ENTITY_ID => $product->getId(), -+ UrlRewrite::ENTITY_TYPE => ProductUrlRewriteGenerator::ENTITY_TYPE, -+ ] -+ ); -+ } elseif ($visibility !== Visibility::VISIBILITY_NOT_VISIBLE) { -+ $product->setVisibility($visibility); -+ $productUrlPath = $this->urlPathGenerator->getUrlPath($product); -+ $productUrlRewrite = $this->urlRewriteGenerator->generate($product); -+ $product->unsUrlPath(); -+ $product->setUrlPath($productUrlPath); -+ -+ try { -+ $this->urlPersist->replace($productUrlRewrite); -+ } catch (UrlAlreadyExistsException $e) { -+ throw new UrlAlreadyExistsException( -+ __( -+ 'Can not change the visibility of the product with SKU equals "%1". ' -+ . 'URL key "%2" for specified store already exists.', -+ $product->getSku(), -+ $product->getUrlKey() -+ ), -+ $e, -+ $e->getCode(), -+ $e->getUrls() -+ ); -+ } -+ } -+ } -+ } -+ -+ /** -+ * Get Product Models by Id's -+ * -+ * @param array $productIds -+ * @return array -+ */ -+ private function getProductsByIds(array $productIds): array -+ { -+ $productCollection = $this->productCollectionFactory->create(); -+ $productCollection->addAttributeToSelect(ProductInterface::VISIBILITY); -+ $productCollection->addAttributeToSelect('url_key'); -+ $productCollection->addFieldToFilter( -+ 'entity_id', -+ ['in' => array_unique($productIds)] -+ ); -+ -+ return $productCollection->getItems(); -+ } -+} -diff --git a/app/code/Magento/CatalogUrlRewrite/Model/ResourceModel/Category/Product.php b/app/code/Magento/CatalogUrlRewrite/Model/ResourceModel/Category/Product.php -index 685a9d28287..a475e3d5f4b 100644 ---- a/app/code/Magento/CatalogUrlRewrite/Model/ResourceModel/Category/Product.php -+++ b/app/code/Magento/CatalogUrlRewrite/Model/ResourceModel/Category/Product.php -@@ -3,11 +3,17 @@ - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -+ -+declare(strict_types=1); -+ - namespace Magento\CatalogUrlRewrite\Model\ResourceModel\Category; - - use Magento\Framework\Model\ResourceModel\Db\AbstractDb; - use Magento\UrlRewrite\Model\Storage\DbStorage; - -+/** -+ * Product Resource Class -+ */ - class Product extends AbstractDb - { - /** -@@ -38,13 +44,15 @@ class Product extends AbstractDb - } - - /** -+ * Save multiple data -+ * - * @param array $insertData - * @return int - */ - public function saveMultiple(array $insertData) - { - $connection = $this->getConnection(); -- if (sizeof($insertData) <= self::CHUNK_SIZE) { -+ if (count($insertData) <= self::CHUNK_SIZE) { - return $connection->insertMultiple($this->getTable(self::TABLE_NAME), $insertData); - } - $data = array_chunk($insertData, self::CHUNK_SIZE); -@@ -70,7 +78,10 @@ class Product extends AbstractDb - } - - /** -- * Removes multiple entities from url_rewrite table using entities from catalog_url_rewrite_product_category -+ * Removes multiple data by filter -+ * -+ * Removes multiple entities from url_rewrite table -+ * using entities from catalog_url_rewrite_product_category - * Example: $filter = ['category_id' => [1, 2, 3], 'product_id' => [1, 2, 3]] - * - * @param array $filter -@@ -78,10 +89,7 @@ class Product extends AbstractDb - */ - public function removeMultipleByProductCategory(array $filter) - { -- return $this->getConnection()->delete( -- $this->getTable(self::TABLE_NAME), -- ['url_rewrite_id in (?)' => $this->prepareSelect($filter)] -- ); -+ return $this->getConnection()->deleteFromSelect($this->prepareSelect($filter), self::TABLE_NAME); - } - - /** -@@ -93,10 +101,13 @@ class Product extends AbstractDb - private function prepareSelect($data) - { - $select = $this->getConnection()->select(); -- $select->from($this->getTable(DbStorage::TABLE_NAME), 'url_rewrite_id'); -- -+ $select->from(DbStorage::TABLE_NAME); -+ $select->join( -+ self::TABLE_NAME, -+ DbStorage::TABLE_NAME . '.url_rewrite_id = ' . self::TABLE_NAME . '.url_rewrite_id' -+ ); - foreach ($data as $column => $value) { -- $select->where($this->getConnection()->quoteIdentifier($column) . ' IN (?)', $value); -+ $select->where(DbStorage::TABLE_NAME . '.' . $column . ' IN (?)', $value); - } - return $select; - } -diff --git a/app/code/Magento/CatalogUrlRewrite/Model/Storage/DbStorage.php b/app/code/Magento/CatalogUrlRewrite/Model/Storage/DbStorage.php -index f0351467e5f..4bbecbfae6b 100644 ---- a/app/code/Magento/CatalogUrlRewrite/Model/Storage/DbStorage.php -+++ b/app/code/Magento/CatalogUrlRewrite/Model/Storage/DbStorage.php -@@ -3,32 +3,40 @@ - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -+declare(strict_types=1); -+ - namespace Magento\CatalogUrlRewrite\Model\Storage; - - use Magento\CatalogUrlRewrite\Model\ResourceModel\Category\Product; - use Magento\UrlRewrite\Model\Storage\DbStorage as BaseDbStorage; - use Magento\UrlRewrite\Service\V1\Data\UrlRewrite; - -+/** -+ * Class DbStorage -+ */ - class DbStorage extends BaseDbStorage - { - /** -- * {@inheritDoc} -+ * @inheritDoc - */ - protected function prepareSelect(array $data) - { - $metadata = []; -- if (array_key_exists(UrlRewrite::METADATA, $data)) { -+ if (isset($data[UrlRewrite::METADATA])) { - $metadata = $data[UrlRewrite::METADATA]; - unset($data[UrlRewrite::METADATA]); - } - - $select = $this->connection->select(); -- $select->from([ -- 'url_rewrite' => $this->resource->getTableName(self::TABLE_NAME) -- ]); -+ $select->from( -+ [ -+ 'url_rewrite' => $this->resource->getTableName(self::TABLE_NAME) -+ ] -+ ); - $select->joinLeft( - ['relation' => $this->resource->getTableName(Product::TABLE_NAME)], -- 'url_rewrite.url_rewrite_id = relation.url_rewrite_id' -+ 'url_rewrite.url_rewrite_id = relation.url_rewrite_id', -+ ['relation.category_id', 'relation.product_id'] - ); - - foreach ($data as $column => $value) { -diff --git a/app/code/Magento/CatalogUrlRewrite/Model/Storage/DynamicStorage.php b/app/code/Magento/CatalogUrlRewrite/Model/Storage/DynamicStorage.php -new file mode 100644 -index 00000000000..edca633fb14 ---- /dev/null -+++ b/app/code/Magento/CatalogUrlRewrite/Model/Storage/DynamicStorage.php -@@ -0,0 +1,259 @@ -+<?php -+/** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+declare(strict_types=1); -+ -+namespace Magento\CatalogUrlRewrite\Model\Storage; -+ -+use Magento\Catalog\Model\ResourceModel\ProductFactory; -+use Magento\CatalogUrlRewrite\Model\CategoryUrlPathGenerator; -+use Magento\Framework\Api\DataObjectHelper; -+use Magento\Framework\App\Config\ScopeConfigInterface; -+use Magento\Framework\App\ResourceConnection; -+use Magento\CatalogUrlRewrite\Model\ResourceModel\Category\Product; -+use Magento\Store\Model\ScopeInterface; -+use Magento\UrlRewrite\Model\OptionProvider; -+use Magento\UrlRewrite\Model\Storage\DbStorage as BaseDbStorage; -+use Magento\UrlRewrite\Service\V1\Data\UrlRewrite; -+use Magento\UrlRewrite\Service\V1\Data\UrlRewriteFactory; -+use Psr\Log\LoggerInterface; -+ -+/** -+ * Class DbStorage -+ */ -+class DynamicStorage extends BaseDbStorage -+{ -+ /** -+ * @var ScopeConfigInterface -+ */ -+ private $config; -+ -+ /** -+ * @var ProductFactory -+ */ -+ private $productFactory; -+ -+ /** -+ * @param UrlRewriteFactory $urlRewriteFactory -+ * @param DataObjectHelper $dataObjectHelper -+ * @param ResourceConnection $resource -+ * @param ScopeConfigInterface $config -+ * @param ProductFactory $productFactory -+ * @param LoggerInterface|null $logger -+ */ -+ public function __construct( -+ UrlRewriteFactory $urlRewriteFactory, -+ DataObjectHelper $dataObjectHelper, -+ ResourceConnection $resource, -+ ScopeConfigInterface $config, -+ ProductFactory $productFactory, -+ LoggerInterface $logger = null -+ ) { -+ parent::__construct($urlRewriteFactory, $dataObjectHelper, $resource, $logger); -+ $this->config = $config; -+ $this->productFactory = $productFactory; -+ } -+ -+ /** -+ * @inheritDoc -+ */ -+ protected function prepareSelect(array $data) -+ { -+ $metadata = []; -+ if (isset($data[UrlRewrite::METADATA])) { -+ $metadata = $data[UrlRewrite::METADATA]; -+ unset($data[UrlRewrite::METADATA]); -+ } -+ $select = $this->connection->select(); -+ $select->from( -+ [ -+ 'url_rewrite' => $this->resource->getTableName(self::TABLE_NAME) -+ ] -+ ); -+ $select->joinLeft( -+ ['relation' => $this->resource->getTableName(Product::TABLE_NAME)], -+ 'url_rewrite.url_rewrite_id = relation.url_rewrite_id' -+ ); -+ foreach ($data as $column => $value) { -+ $select->where('url_rewrite.' . $column . ' IN (?)', $value); -+ } -+ if (empty($metadata['category_id'])) { -+ $select->where('relation.category_id IS NULL'); -+ } else { -+ $select->where( -+ 'relation.category_id = ?', -+ $metadata['category_id'] -+ ); -+ } -+ return $select; -+ } -+ -+ /** -+ * @inheritdoc -+ */ -+ protected function doFindOneByData(array $data) -+ { -+ if (isset($data[UrlRewrite::REQUEST_PATH]) -+ && isset($data[UrlRewrite::STORE_ID]) -+ && is_string($data[UrlRewrite::REQUEST_PATH]) -+ ) { -+ return $this->findProductRewriteByRequestPath($data); -+ } -+ -+ $filterResults = $this->findProductRewritesByFilter($data); -+ if (!empty($filterResults)) { -+ return reset($filterResults); -+ } else { -+ return null; -+ } -+ } -+ -+ /** -+ * @inheritdoc -+ */ -+ protected function doFindAllByData(array $data) -+ { -+ $rewrites = parent::doFindAllByData($data); -+ -+ $remainingProducts = []; -+ if (isset($data[UrlRewrite::ENTITY_ID]) && is_array($data[UrlRewrite::ENTITY_ID])) { -+ $remainingProducts = array_fill_keys($data[UrlRewrite::ENTITY_ID], 1); -+ foreach ($rewrites as $rewrite) { -+ $id = $rewrite[UrlRewrite::ENTITY_ID]; -+ if (isset($remainingProducts[$id])) { -+ unset($remainingProducts[$id]); -+ } -+ } -+ } -+ -+ if (!empty($remainingProducts)) { -+ $data[UrlRewrite::ENTITY_ID] = array_keys($remainingProducts); -+ $rewrites = array_merge($rewrites, $this->findProductRewritesByFilter($data)); -+ } -+ -+ return $rewrites; -+ } -+ -+ /** -+ * Get category urlSuffix from config -+ * -+ * @param int $storeId -+ * @return string -+ */ -+ private function getCategoryUrlSuffix($storeId = null): string -+ { -+ return $this->config->getValue( -+ CategoryUrlPathGenerator::XML_PATH_CATEGORY_URL_SUFFIX, -+ ScopeInterface::SCOPE_STORE, -+ $storeId -+ ); -+ } -+ -+ /** -+ * Find product rewrite by request data -+ * -+ * @param array $data -+ * @return array|null -+ */ -+ private function findProductRewriteByRequestPath(array $data): ?array -+ { -+ $requestPath = $data[UrlRewrite::REQUEST_PATH] ?? null; -+ -+ $productUrl = $this->getBaseName($requestPath); -+ $data[UrlRewrite::REQUEST_PATH] = [$productUrl]; -+ -+ $productFromDb = $this->connection->fetchRow($this->prepareSelect($data)); -+ if ($productFromDb === false) { -+ return null; -+ } -+ $categorySuffix = $this->getCategoryUrlSuffix($data[UrlRewrite::STORE_ID]); -+ $productResource = $this->productFactory->create(); -+ $categoryPath = str_replace('/' . $productUrl, '', $requestPath); -+ if ($productFromDb[UrlRewrite::REDIRECT_TYPE]) { -+ $productUrl = $productFromDb[UrlRewrite::TARGET_PATH]; -+ } -+ if ($categoryPath) { -+ $data[UrlRewrite::REQUEST_PATH] = [$categoryPath . $categorySuffix]; -+ unset($data[UrlRewrite::IS_AUTOGENERATED]); -+ $categoryFromDb = $this->connection->fetchRow($this->prepareSelect($data)); -+ -+ if ($categoryFromDb[UrlRewrite::REDIRECT_TYPE]) { -+ $productFromDb[UrlRewrite::REDIRECT_TYPE] = OptionProvider::PERMANENT; -+ $categoryPath = str_replace($categorySuffix, '', $categoryFromDb[UrlRewrite::TARGET_PATH]); -+ } -+ -+ if ($categoryFromDb === false -+ || !$productResource->canBeShowInCategory( -+ $productFromDb[UrlRewrite::ENTITY_ID], -+ $categoryFromDb[UrlRewrite::ENTITY_ID] -+ ) -+ ) { -+ return null; -+ } -+ -+ $productFromDb[UrlRewrite::TARGET_PATH] = $productFromDb[UrlRewrite::TARGET_PATH] -+ . '/category/' . $categoryFromDb[UrlRewrite::ENTITY_ID]; -+ } -+ -+ if ($productFromDb[UrlRewrite::REDIRECT_TYPE]) { -+ $productFromDb[UrlRewrite::TARGET_PATH] = $categoryPath . '/' . $productUrl; -+ } -+ -+ $productFromDb[UrlRewrite::REQUEST_PATH] = $requestPath; -+ -+ return $productFromDb; -+ } -+ -+ /** -+ * Find product rewrites by filter array -+ * -+ * @param array $data -+ * @return array -+ */ -+ private function findProductRewritesByFilter(array $data): array -+ { -+ if (empty($data[UrlRewrite::ENTITY_TYPE])) { -+ return []; -+ } -+ $rewrites = []; -+ $metadata = $data[UrlRewrite::METADATA] ?? []; -+ if (isset($data[UrlRewrite::METADATA])) { -+ unset($data[UrlRewrite::METADATA]); -+ } -+ $productsFromDb = $this->connection->fetchAll($this->prepareSelect($data)); -+ -+ if (!empty($metadata['category_id'])) { -+ $categoryId = $metadata['category_id']; -+ $data[UrlRewrite::ENTITY_ID] = $categoryId; -+ $data[UrlRewrite::ENTITY_TYPE] = 'category'; -+ $categoryFromDb = $this->connection->fetchRow($this->prepareSelect($data)); -+ foreach ($productsFromDb as $productFromDb) { -+ $productUrl = $this->getBaseName($productFromDb[UrlRewrite::REQUEST_PATH]); -+ $productFromDb[UrlRewrite::REQUEST_PATH] = str_replace( -+ $this->getCategoryUrlSuffix($data[UrlRewrite::STORE_ID]), -+ '', -+ $categoryFromDb[UrlRewrite::REQUEST_PATH] -+ ) -+ . '/' . $productUrl; -+ $rewrites[] = $productFromDb; -+ } -+ } else { -+ $rewrites = $productsFromDb; -+ } -+ -+ return $rewrites; -+ } -+ -+ /** -+ * Return base name for path -+ * -+ * @param string|null $string -+ * @return string -+ */ -+ private function getBaseName($string): string -+ { -+ return preg_replace('|.*?([^/]+)$|', '\1', $string, 1); -+ } -+} -diff --git a/app/code/Magento/CatalogUrlRewrite/Model/TableCleaner.php b/app/code/Magento/CatalogUrlRewrite/Model/TableCleaner.php -new file mode 100644 -index 00000000000..070836380ac ---- /dev/null -+++ b/app/code/Magento/CatalogUrlRewrite/Model/TableCleaner.php -@@ -0,0 +1,82 @@ -+<?php -+/** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+declare(strict_types=1); -+ -+namespace Magento\CatalogUrlRewrite\Model; -+ -+use Magento\Framework\App\Cache\TypeListInterface; -+use Magento\Framework\App\Config\ScopeConfigInterface; -+use Magento\Framework\App\Config\Value as ConfigValue; -+use Magento\Framework\Data\Collection\AbstractDb; -+use Magento\Framework\Exception\LocalizedException; -+use Magento\Framework\Model\Context; -+use Magento\Framework\Model\ResourceModel\AbstractResource; -+use Magento\Framework\Registry; -+use Magento\UrlRewrite\Model\ResourceModel\UrlRewrite; -+ -+/** -+ * Table Cleaner in case of switching generate_category_product_rewrites off -+ */ -+class TableCleaner extends ConfigValue -+{ -+ const AUTO_GENERATED_ROW_FLAG = 1; -+ const URL_REWRITE_GENERATION_OFF_FLAG = 0; -+ -+ /** -+ * @var UrlRewrite -+ */ -+ private $urlRewrite; -+ -+ /** -+ * @param UrlRewrite $urlRewrite -+ * @param Context $context -+ * @param Registry $registry -+ * @param ScopeConfigInterface $config -+ * @param TypeListInterface $cacheTypeList -+ * @param AbstractResource|null $resource -+ * @param AbstractDb|null $resourceCollection -+ * @param array $data -+ */ -+ public function __construct( -+ UrlRewrite $urlRewrite, -+ Context $context, -+ Registry $registry, -+ ScopeConfigInterface $config, -+ TypeListInterface $cacheTypeList, -+ AbstractResource $resource = null, -+ AbstractDb $resourceCollection = null, -+ array $data = [] -+ ) { -+ parent::__construct($context, $registry, $config, $cacheTypeList, $resource, $resourceCollection, $data); -+ $this->urlRewrite = $urlRewrite; -+ } -+ -+ /** -+ * @inheritDoc -+ * @return ConfigValue -+ * @throws LocalizedException -+ */ -+ public function afterSave() -+ { -+ if ($this->getValue() == self::URL_REWRITE_GENERATION_OFF_FLAG) { -+ $this->clearOldData(); -+ } -+ return parent::afterSave(); -+ } -+ -+ /** -+ * Clear urlrewrites for products in categories -+ */ -+ private function clearOldData(): void -+ { -+ $tableName = $this->urlRewrite->getMainTable(); -+ $conditions = [ -+ 'metadata LIKE ?' => '{"category_id"%', -+ 'is_autogenerated = ?' => self::AUTO_GENERATED_ROW_FLAG -+ ]; -+ $this->urlRewrite->getConnection()->delete($tableName, $conditions); -+ } -+} -diff --git a/app/code/Magento/CatalogUrlRewrite/Observer/AfterImportDataObserver.php b/app/code/Magento/CatalogUrlRewrite/Observer/AfterImportDataObserver.php -index 022a78be001..33c0cafc8f0 100644 ---- a/app/code/Magento/CatalogUrlRewrite/Observer/AfterImportDataObserver.php -+++ b/app/code/Magento/CatalogUrlRewrite/Observer/AfterImportDataObserver.php -@@ -6,22 +6,36 @@ - namespace Magento\CatalogUrlRewrite\Observer; - - use Magento\Catalog\Model\Category; -+use Magento\Catalog\Model\Product; - use Magento\Catalog\Model\Product\Visibility; -+use Magento\Catalog\Model\ProductFactory; - use Magento\Catalog\Model\ResourceModel\Category\Collection as CategoryCollection; - use Magento\Catalog\Model\ResourceModel\Category\CollectionFactory as CategoryCollectionFactory; - use Magento\CatalogImportExport\Model\Import\Product as ImportProduct; -+use Magento\CatalogUrlRewrite\Model\ObjectRegistry; -+use Magento\CatalogUrlRewrite\Model\ObjectRegistryFactory; -+use Magento\CatalogUrlRewrite\Model\ProductUrlPathGenerator; - use Magento\CatalogUrlRewrite\Model\ProductUrlRewriteGenerator; -+use Magento\CatalogUrlRewrite\Service\V1\StoreViewService; -+use Magento\Framework\App\Config\ScopeConfigInterface; - use Magento\Framework\App\ObjectManager; -+use Magento\Framework\DataObject; - use Magento\Framework\Event\Observer; - use Magento\Framework\Event\ObserverInterface; -+use Magento\Framework\Exception\LocalizedException; -+use Magento\Framework\Exception\NoSuchEntityException; - use Magento\ImportExport\Model\Import as ImportExport; - use Magento\Store\Model\Store; -+use Magento\Store\Model\StoreManagerInterface; -+use Magento\UrlRewrite\Model\Exception\UrlAlreadyExistsException; -+use Magento\UrlRewrite\Model\MergeDataProvider; - use Magento\UrlRewrite\Model\MergeDataProviderFactory; - use Magento\UrlRewrite\Model\OptionProvider; - use Magento\UrlRewrite\Model\UrlFinderInterface; - use Magento\UrlRewrite\Model\UrlPersistInterface; - use Magento\UrlRewrite\Service\V1\Data\UrlRewrite; - use Magento\UrlRewrite\Service\V1\Data\UrlRewriteFactory; -+use RuntimeException; - - /** - * Class AfterImportDataObserver -@@ -37,12 +51,12 @@ class AfterImportDataObserver implements ObserverInterface - const URL_KEY_ATTRIBUTE_CODE = 'url_key'; - - /** -- * @var \Magento\CatalogUrlRewrite\Service\V1\StoreViewService -+ * @var StoreViewService - */ - protected $storeViewService; - - /** -- * @var \Magento\Catalog\Model\Product -+ * @var Product - */ - protected $product; - -@@ -57,42 +71,42 @@ class AfterImportDataObserver implements ObserverInterface - protected $products = []; - - /** -- * @var \Magento\CatalogUrlRewrite\Model\ObjectRegistryFactory -+ * @var ObjectRegistryFactory - */ - protected $objectRegistryFactory; - - /** -- * @var \Magento\CatalogUrlRewrite\Model\ObjectRegistry -+ * @var ObjectRegistry - */ - protected $productCategories; - - /** -- * @var \Magento\UrlRewrite\Model\UrlFinderInterface -+ * @var UrlFinderInterface - */ - protected $urlFinder; - - /** -- * @var \Magento\Store\Model\StoreManagerInterface -+ * @var StoreManagerInterface - */ - protected $storeManager; - - /** -- * @var \Magento\UrlRewrite\Model\UrlPersistInterface -+ * @var UrlPersistInterface - */ - protected $urlPersist; - - /** -- * @var \Magento\UrlRewrite\Service\V1\Data\UrlRewriteFactory -+ * @var UrlRewriteFactory - */ - protected $urlRewriteFactory; - - /** -- * @var \Magento\CatalogImportExport\Model\Import\Product -+ * @var ImportProduct - */ - protected $import; - - /** -- * @var \Magento\Catalog\Model\ProductFactory -+ * @var ProductFactory - */ - protected $catalogProductFactory; - -@@ -102,7 +116,7 @@ class AfterImportDataObserver implements ObserverInterface - protected $acceptableCategories; - - /** -- * @var \Magento\CatalogUrlRewrite\Model\ProductUrlPathGenerator -+ * @var ProductUrlPathGenerator - */ - protected $productUrlPathGenerator; - -@@ -135,10 +149,11 @@ class AfterImportDataObserver implements ObserverInterface - 'url_path', - 'name', - 'visibility', -+ 'save_rewrites_history' - ]; - - /** -- * @var \Magento\UrlRewrite\Model\MergeDataProvider -+ * @var MergeDataProvider - */ - private $mergeDataProviderPrototype; - -@@ -157,29 +172,37 @@ class AfterImportDataObserver implements ObserverInterface - private $categoriesCache = []; - - /** -- * @param \Magento\Catalog\Model\ProductFactory $catalogProductFactory -- * @param \Magento\CatalogUrlRewrite\Model\ObjectRegistryFactory $objectRegistryFactory -- * @param \Magento\CatalogUrlRewrite\Model\ProductUrlPathGenerator $productUrlPathGenerator -- * @param \Magento\CatalogUrlRewrite\Service\V1\StoreViewService $storeViewService -- * @param \Magento\Store\Model\StoreManagerInterface $storeManager -+ * @var ScopeConfigInterface|null -+ */ -+ private $scopeConfig; -+ -+ /** -+ * @param ProductFactory $catalogProductFactory -+ * @param ObjectRegistryFactory $objectRegistryFactory -+ * @param ProductUrlPathGenerator $productUrlPathGenerator -+ * @param StoreViewService $storeViewService -+ * @param StoreManagerInterface $storeManager - * @param UrlPersistInterface $urlPersist - * @param UrlRewriteFactory $urlRewriteFactory - * @param UrlFinderInterface $urlFinder -- * @param \Magento\UrlRewrite\Model\MergeDataProviderFactory|null $mergeDataProviderFactory -+ * @param MergeDataProviderFactory|null $mergeDataProviderFactory - * @param CategoryCollectionFactory|null $categoryCollectionFactory -+ * @param ScopeConfigInterface|null $scopeConfig -+ * @throws RuntimeException - * @SuppressWarnings(PHPMD.ExcessiveParameterList) - */ - public function __construct( -- \Magento\Catalog\Model\ProductFactory $catalogProductFactory, -- \Magento\CatalogUrlRewrite\Model\ObjectRegistryFactory $objectRegistryFactory, -- \Magento\CatalogUrlRewrite\Model\ProductUrlPathGenerator $productUrlPathGenerator, -- \Magento\CatalogUrlRewrite\Service\V1\StoreViewService $storeViewService, -- \Magento\Store\Model\StoreManagerInterface $storeManager, -+ ProductFactory $catalogProductFactory, -+ ObjectRegistryFactory $objectRegistryFactory, -+ ProductUrlPathGenerator $productUrlPathGenerator, -+ StoreViewService $storeViewService, -+ StoreManagerInterface $storeManager, - UrlPersistInterface $urlPersist, - UrlRewriteFactory $urlRewriteFactory, - UrlFinderInterface $urlFinder, - MergeDataProviderFactory $mergeDataProviderFactory = null, -- CategoryCollectionFactory $categoryCollectionFactory = null -+ CategoryCollectionFactory $categoryCollectionFactory = null, -+ ScopeConfigInterface $scopeConfig = null - ) { - $this->urlPersist = $urlPersist; - $this->catalogProductFactory = $catalogProductFactory; -@@ -195,15 +218,17 @@ class AfterImportDataObserver implements ObserverInterface - $this->mergeDataProviderPrototype = $mergeDataProviderFactory->create(); - $this->categoryCollectionFactory = $categoryCollectionFactory ?: - ObjectManager::getInstance()->get(CategoryCollectionFactory::class); -+ $this->scopeConfig = $scopeConfig ?: -+ ObjectManager::getInstance()->get(ScopeConfigInterface::class); - } - - /** -- * Action after data import. -- * Save new url rewrites and remove old if exist. -+ * Action after data import. Save new url rewrites and remove old if exist. - * - * @param Observer $observer -- * - * @return void -+ * @throws LocalizedException -+ * @throws UrlAlreadyExistsException - */ - public function execute(Observer $observer) - { -@@ -223,18 +248,15 @@ class AfterImportDataObserver implements ObserverInterface - * Create product model from imported data for URL rewrite purposes. - * - * @param array $rowData -- * -- * @return ImportExport -+ * @return AfterImportDataObserver|null -+ * @throws LocalizedException - * @SuppressWarnings(PHPMD.CyclomaticComplexity) - */ - protected function _populateForUrlGeneration($rowData) - { - $newSku = $this->import->getNewSku($rowData[ImportProduct::COL_SKU]); -- if (empty($newSku) || !isset($newSku['entity_id'])) { -- return null; -- } -- if ($this->import->getRowScope($rowData) == ImportProduct::SCOPE_STORE -- && empty($rowData[self::URL_KEY_ATTRIBUTE_CODE])) { -+ $oldSku = $this->import->getOldSku(); -+ if (!$this->isNeedToPopulateForUrlGeneration($rowData, $newSku, $oldSku)) { - return null; - } - $rowData['entity_id'] = $newSku['entity_id']; -@@ -261,23 +283,47 @@ class AfterImportDataObserver implements ObserverInterface - if ($this->isGlobalScope($product->getStoreId())) { - $this->populateGlobalProduct($product); - } else { -+ $this->storesCache[$product->getStoreId()] = true; - $this->addProductToImport($product, $product->getStoreId()); - } - return $this; - } - - /** -- * @param \Magento\Catalog\Model\Product $product -+ * Check is need to populate data for url generation -+ * -+ * @param array $rowData -+ * @param array $newSku -+ * @param array $oldSku -+ * @return bool -+ */ -+ private function isNeedToPopulateForUrlGeneration($rowData, $newSku, $oldSku): bool -+ { -+ if ((empty($newSku) || !isset($newSku['entity_id'])) -+ || ($this->import->getRowScope($rowData) == ImportProduct::SCOPE_STORE -+ && empty($rowData[self::URL_KEY_ATTRIBUTE_CODE])) -+ || (array_key_exists($rowData[ImportProduct::COL_SKU], $oldSku) -+ && !isset($rowData[self::URL_KEY_ATTRIBUTE_CODE]) -+ && $this->import->getBehavior() === ImportExport::BEHAVIOR_APPEND)) { -+ return false; -+ } -+ return true; -+ } -+ -+ /** -+ * Add store id to product data. -+ * -+ * @param Product $product - * @param array $rowData - * @return void - */ -- protected function setStoreToProduct(\Magento\Catalog\Model\Product $product, array $rowData) -+ protected function setStoreToProduct(Product $product, array $rowData) - { - if (!empty($rowData[ImportProduct::COL_STORE]) - && ($storeId = $this->import->getStoreIdByCode($rowData[ImportProduct::COL_STORE])) - ) { - $product->setStoreId($storeId); -- } elseif (!$product->hasData(\Magento\Catalog\Model\Product::STORE_ID)) { -+ } elseif (!$product->hasData(Product::STORE_ID)) { - $product->setStoreId(Store::DEFAULT_STORE_ID); - } - } -@@ -285,7 +331,7 @@ class AfterImportDataObserver implements ObserverInterface - /** - * Add product to import - * -- * @param \Magento\Catalog\Model\Product $product -+ * @param Product $product - * @param string $storeId - * @return $this - */ -@@ -304,7 +350,7 @@ class AfterImportDataObserver implements ObserverInterface - /** - * Populate global product - * -- * @param \Magento\Catalog\Model\Product $product -+ * @param Product $product - * @return $this - */ - protected function populateGlobalProduct($product) -@@ -323,13 +369,16 @@ class AfterImportDataObserver implements ObserverInterface - /** - * Generate product url rewrites - * -- * @return \Magento\UrlRewrite\Service\V1\Data\UrlRewrite[] -+ * @return UrlRewrite[] -+ * @throws LocalizedException - */ - protected function generateUrls() - { - $mergeDataProvider = clone $this->mergeDataProviderPrototype; - $mergeDataProvider->merge($this->canonicalUrlRewriteGenerate()); -- $mergeDataProvider->merge($this->categoriesUrlRewriteGenerate()); -+ if ($this->isCategoryRewritesEnabled()) { -+ $mergeDataProvider->merge($this->categoriesUrlRewriteGenerate()); -+ } - $mergeDataProvider->merge($this->currentUrlRewritesRegenerate()); - $this->productCategories = null; - -@@ -378,6 +427,7 @@ class AfterImportDataObserver implements ObserverInterface - * Generate list based on categories. - * - * @return UrlRewrite[] -+ * @throws LocalizedException - */ - protected function categoriesUrlRewriteGenerate() - { -@@ -436,6 +486,8 @@ class AfterImportDataObserver implements ObserverInterface - } - - /** -+ * Generate url-rewrite for outogenerated url-rewirte. -+ * - * @param UrlRewrite $url - * @param Category $category - * @return array -@@ -470,6 +522,8 @@ class AfterImportDataObserver implements ObserverInterface - } - - /** -+ * Generate url-rewrite for custom url-rewirte. -+ * - * @param UrlRewrite $url - * @param Category $category - * @return array -@@ -503,6 +557,8 @@ class AfterImportDataObserver implements ObserverInterface - } - - /** -+ * Retrieve category from url metadata. -+ * - * @param UrlRewrite $url - * @return Category|null|bool - */ -@@ -517,9 +573,12 @@ class AfterImportDataObserver implements ObserverInterface - } - - /** -- * @param \Magento\Catalog\Model\Category $category -+ * Check, category suited for url-rewrite generation. -+ * -+ * @param Category $category - * @param int $storeId - * @return bool -+ * @throws NoSuchEntityException - */ - protected function isCategoryProperForGenerating($category, $storeId) - { -@@ -528,7 +587,7 @@ class AfterImportDataObserver implements ObserverInterface - return $this->acceptableCategories[$storeId][$category->getId()]; - } - $acceptable = false; -- if ($category->getParentId() != \Magento\Catalog\Model\Category::TREE_ROOT_ID) { -+ if ($category->getParentId() != Category::TREE_ROOT_ID) { - list(, $rootCategoryId) = $category->getParentIds(); - $acceptable = ($rootCategoryId == $this->storeManager->getStore($storeId)->getRootCategoryId()); - } -@@ -544,7 +603,8 @@ class AfterImportDataObserver implements ObserverInterface - * - * @param int $categoryId - * @param int $storeId -- * @return Category|\Magento\Framework\DataObject -+ * @return Category|DataObject -+ * @throws LocalizedException - */ - private function getCategoryById($categoryId, $storeId) - { -@@ -561,4 +621,14 @@ class AfterImportDataObserver implements ObserverInterface - - return $this->categoriesCache[$categoryId][$storeId]; - } -+ -+ /** -+ * Check config value of generate_category_product_rewrites -+ * -+ * @return bool -+ */ -+ private function isCategoryRewritesEnabled() -+ { -+ return (bool)$this->scopeConfig->getValue('catalog/seo/generate_category_product_rewrites'); -+ } - } -diff --git a/app/code/Magento/CatalogUrlRewrite/Observer/CategoryProcessUrlRewriteMovingObserver.php b/app/code/Magento/CatalogUrlRewrite/Observer/CategoryProcessUrlRewriteMovingObserver.php -index 0afdf774c7e..244aaf4d5cd 100644 ---- a/app/code/Magento/CatalogUrlRewrite/Observer/CategoryProcessUrlRewriteMovingObserver.php -+++ b/app/code/Magento/CatalogUrlRewrite/Observer/CategoryProcessUrlRewriteMovingObserver.php -@@ -74,8 +74,8 @@ class CategoryProcessUrlRewriteMovingObserver implements ObserverInterface - UrlRewriteBunchReplacer $urlRewriteBunchReplacer, - DatabaseMapPool $databaseMapPool, - $dataUrlRewriteClassNames = [ -- DataCategoryUrlRewriteDatabaseMap::class, -- DataProductUrlRewriteDatabaseMap::class -+ DataCategoryUrlRewriteDatabaseMap::class, -+ DataProductUrlRewriteDatabaseMap::class - ] - ) { - $this->categoryUrlRewriteGenerator = $categoryUrlRewriteGenerator; -@@ -88,6 +88,8 @@ class CategoryProcessUrlRewriteMovingObserver implements ObserverInterface - } - - /** -+ * Execute observer functional -+ * - * @param \Magento\Framework\Event\Observer $observer - * @return void - */ -@@ -105,8 +107,12 @@ class CategoryProcessUrlRewriteMovingObserver implements ObserverInterface - $categoryUrlRewriteResult = $this->categoryUrlRewriteGenerator->generate($category, true); - $this->urlRewriteHandler->deleteCategoryRewritesForChildren($category); - $this->urlRewriteBunchReplacer->doBunchReplace($categoryUrlRewriteResult); -- $productUrlRewriteResult = $this->urlRewriteHandler->generateProductUrlRewrites($category); -- $this->urlRewriteBunchReplacer->doBunchReplace($productUrlRewriteResult); -+ -+ if ($this->isCategoryRewritesEnabled()) { -+ $productUrlRewriteResult = $this->urlRewriteHandler->generateProductUrlRewrites($category); -+ $this->urlRewriteBunchReplacer->doBunchReplace($productUrlRewriteResult); -+ } -+ - //frees memory for maps that are self-initialized in multiple classes that were called by the generators - $this->resetUrlRewritesDataMaps($category); - } -@@ -124,4 +130,14 @@ class CategoryProcessUrlRewriteMovingObserver implements ObserverInterface - $this->databaseMapPool->resetMap($className, $category->getEntityId()); - } - } -+ -+ /** -+ * Check config value of generate_category_product_rewrites -+ * -+ * @return bool -+ */ -+ private function isCategoryRewritesEnabled() -+ { -+ return (bool)$this->scopeConfig->getValue('catalog/seo/generate_category_product_rewrites'); -+ } - } -diff --git a/app/code/Magento/CatalogUrlRewrite/Observer/CategoryProcessUrlRewriteSavingObserver.php b/app/code/Magento/CatalogUrlRewrite/Observer/CategoryProcessUrlRewriteSavingObserver.php -index 5d7e323e8b2..62ce54cd671 100644 ---- a/app/code/Magento/CatalogUrlRewrite/Observer/CategoryProcessUrlRewriteSavingObserver.php -+++ b/app/code/Magento/CatalogUrlRewrite/Observer/CategoryProcessUrlRewriteSavingObserver.php -@@ -12,10 +12,12 @@ 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\Framework\App\Config\ScopeConfigInterface; - use Magento\Framework\Event\ObserverInterface; - use Magento\Store\Model\ResourceModel\Group\CollectionFactory; - use Magento\Store\Model\ResourceModel\Group\Collection as StoreGroupCollection; - use Magento\Framework\App\ObjectManager; -+use Magento\Store\Model\ScopeInterface; - - /** - * Generates Category Url Rewrites after save and Products Url Rewrites assigned to the category that's being saved -@@ -52,11 +54,17 @@ class CategoryProcessUrlRewriteSavingObserver implements ObserverInterface - */ - private $storeGroupFactory; - -+ /** -+ * @var ScopeConfigInterface -+ */ -+ private $scopeConfig; -+ - /** - * @param CategoryUrlRewriteGenerator $categoryUrlRewriteGenerator - * @param UrlRewriteHandler $urlRewriteHandler - * @param UrlRewriteBunchReplacer $urlRewriteBunchReplacer - * @param DatabaseMapPool $databaseMapPool -+ * @param ScopeConfigInterface $scopeConfig - * @param string[] $dataUrlRewriteClassNames - * @param CollectionFactory|null $storeGroupFactory - */ -@@ -65,6 +73,7 @@ class CategoryProcessUrlRewriteSavingObserver implements ObserverInterface - UrlRewriteHandler $urlRewriteHandler, - UrlRewriteBunchReplacer $urlRewriteBunchReplacer, - DatabaseMapPool $databaseMapPool, -+ ScopeConfigInterface $scopeConfig, - $dataUrlRewriteClassNames = [ - DataCategoryUrlRewriteDatabaseMap::class, - DataProductUrlRewriteDatabaseMap::class -@@ -78,6 +87,7 @@ class CategoryProcessUrlRewriteSavingObserver implements ObserverInterface - $this->dataUrlRewriteClassNames = $dataUrlRewriteClassNames; - $this->storeGroupFactory = $storeGroupFactory - ?: ObjectManager::getInstance()->get(CollectionFactory::class); -+ $this->scopeConfig = $scopeConfig; - } - - /** -@@ -100,16 +110,21 @@ class CategoryProcessUrlRewriteSavingObserver implements ObserverInterface - } - - $mapsGenerated = false; -- if ($category->dataHasChangedFor('url_key') -- || $category->dataHasChangedFor('is_anchor') -- || !empty($category->getChangedProductIds()) -- ) { -+ if ($this->isCategoryHasChanged($category)) { - if ($category->dataHasChangedFor('url_key')) { - $categoryUrlRewriteResult = $this->categoryUrlRewriteGenerator->generate($category); - $this->urlRewriteBunchReplacer->doBunchReplace($categoryUrlRewriteResult); - } -- $productUrlRewriteResult = $this->urlRewriteHandler->generateProductUrlRewrites($category); -- $this->urlRewriteBunchReplacer->doBunchReplace($productUrlRewriteResult); -+ if ($this->isCategoryRewritesEnabled()) { -+ if ($this->isChangedOnlyProduct($category)) { -+ $productUrlRewriteResult = -+ $this->urlRewriteHandler->updateProductUrlRewritesForChangedProduct($category); -+ $this->urlRewriteBunchReplacer->doBunchReplace($productUrlRewriteResult); -+ } else { -+ $productUrlRewriteResult = $this->urlRewriteHandler->generateProductUrlRewrites($category); -+ $this->urlRewriteBunchReplacer->doBunchReplace($productUrlRewriteResult); -+ } -+ } - $mapsGenerated = true; - } - -@@ -119,6 +134,38 @@ class CategoryProcessUrlRewriteSavingObserver implements ObserverInterface - } - } - -+ /** -+ * 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 -@@ -154,4 +201,14 @@ class CategoryProcessUrlRewriteSavingObserver implements ObserverInterface - $this->databaseMapPool->resetMap($className, $category->getEntityId()); - } - } -+ -+ /** -+ * Check config value of generate_category_product_rewrites -+ * -+ * @return bool -+ */ -+ private function isCategoryRewritesEnabled() -+ { -+ return (bool)$this->scopeConfig->getValue('catalog/seo/generate_category_product_rewrites'); -+ } - } -diff --git a/app/code/Magento/CatalogUrlRewrite/Observer/CategoryUrlPathAutogeneratorObserver.php b/app/code/Magento/CatalogUrlRewrite/Observer/CategoryUrlPathAutogeneratorObserver.php -index eb54f0427c1..713dd6ac0c7 100644 ---- a/app/code/Magento/CatalogUrlRewrite/Observer/CategoryUrlPathAutogeneratorObserver.php -+++ b/app/code/Magento/CatalogUrlRewrite/Observer/CategoryUrlPathAutogeneratorObserver.php -@@ -8,11 +8,15 @@ namespace Magento\CatalogUrlRewrite\Observer; - use Magento\Catalog\Model\Category; - use Magento\CatalogUrlRewrite\Model\CategoryUrlPathGenerator; - use Magento\CatalogUrlRewrite\Service\V1\StoreViewService; -+use Magento\Catalog\Api\CategoryRepositoryInterface; - use Magento\Framework\Event\Observer; - use Magento\CatalogUrlRewrite\Model\Category\ChildrenCategoriesProvider; - use Magento\Framework\Event\ObserverInterface; - use Magento\Store\Model\Store; - -+/** -+ * Class for set or update url path. -+ */ - class CategoryUrlPathAutogeneratorObserver implements ObserverInterface - { - /** -@@ -30,22 +34,32 @@ class CategoryUrlPathAutogeneratorObserver implements ObserverInterface - */ - protected $storeViewService; - -+ /** -+ * @var CategoryRepositoryInterface -+ */ -+ private $categoryRepository; -+ - /** - * @param CategoryUrlPathGenerator $categoryUrlPathGenerator - * @param ChildrenCategoriesProvider $childrenCategoriesProvider - * @param \Magento\CatalogUrlRewrite\Service\V1\StoreViewService $storeViewService -+ * @param CategoryRepositoryInterface $categoryRepository - */ - public function __construct( - CategoryUrlPathGenerator $categoryUrlPathGenerator, - ChildrenCategoriesProvider $childrenCategoriesProvider, -- StoreViewService $storeViewService -+ StoreViewService $storeViewService, -+ CategoryRepositoryInterface $categoryRepository - ) { - $this->categoryUrlPathGenerator = $categoryUrlPathGenerator; - $this->childrenCategoriesProvider = $childrenCategoriesProvider; - $this->storeViewService = $storeViewService; -+ $this->categoryRepository = $categoryRepository; - } - - /** -+ * Method for update/set url path. -+ * - * @param \Magento\Framework\Event\Observer $observer - * @return void - * @throws \Magento\Framework\Exception\LocalizedException -@@ -57,45 +71,69 @@ class CategoryUrlPathAutogeneratorObserver implements ObserverInterface - $useDefaultAttribute = !$category->isObjectNew() && !empty($category->getData('use_default')['url_key']); - if ($category->getUrlKey() !== false && !$useDefaultAttribute) { - $resultUrlKey = $this->categoryUrlPathGenerator->getUrlKey($category); -- if (empty($resultUrlKey)) { -- throw new \Magento\Framework\Exception\LocalizedException(__('Invalid URL key')); -- } -- $category->setUrlKey($resultUrlKey) -- ->setUrlPath($this->categoryUrlPathGenerator->getUrlPath($category)); -- if (!$category->isObjectNew()) { -- $category->getResource()->saveAttribute($category, 'url_path'); -- if ($category->dataHasChangedFor('url_path')) { -- $this->updateUrlPathForChildren($category); -- } -+ $this->updateUrlKey($category, $resultUrlKey); -+ } else if ($useDefaultAttribute) { -+ $resultUrlKey = $category->formatUrlKey($category->getOrigData('name')); -+ $this->updateUrlKey($category, $resultUrlKey); -+ $category->setUrlKey(null)->setUrlPath(null); -+ } -+ } -+ -+ /** -+ * Update Url Key -+ * -+ * @param Category $category -+ * @param string $urlKey -+ * @throws \Magento\Framework\Exception\LocalizedException -+ * @throws \Magento\Framework\Exception\NoSuchEntityException -+ */ -+ private function updateUrlKey($category, $urlKey) -+ { -+ if (empty($urlKey)) { -+ throw new \Magento\Framework\Exception\LocalizedException(__('Invalid URL key')); -+ } -+ $category->setUrlKey($urlKey) -+ ->setUrlPath($this->categoryUrlPathGenerator->getUrlPath($category)); -+ if (!$category->isObjectNew()) { -+ $category->getResource()->saveAttribute($category, 'url_path'); -+ if ($category->dataHasChangedFor('url_path')) { -+ $this->updateUrlPathForChildren($category); - } - } - } - - /** -+ * Update url path for children category. -+ * - * @param Category $category - * @return void - */ - protected function updateUrlPathForChildren(Category $category) - { -- $children = $this->childrenCategoriesProvider->getChildren($category, true); -- - if ($this->isGlobalScope($category->getStoreId())) { -- foreach ($children as $child) { -+ $childrenIds = $this->childrenCategoriesProvider->getChildrenIds($category, true); -+ foreach ($childrenIds as $childId) { - foreach ($category->getStoreIds() as $storeId) { - if ($this->storeViewService->doesEntityHaveOverriddenUrlPathForStore( - $storeId, -- $child->getId(), -+ $childId, - Category::ENTITY - )) { -- $child->setStoreId($storeId); -+ $child = $this->categoryRepository->get($childId, $storeId); - $this->updateUrlPathForCategory($child); - } - } - } - } else { -+ $children = $this->childrenCategoriesProvider->getChildren($category, true); - foreach ($children as $child) { -+ /** @var Category $child */ - $child->setStoreId($category->getStoreId()); -- $this->updateUrlPathForCategory($child); -+ if ($child->getParentId() === $category->getId()) { -+ $this->updateUrlPathForCategory($child, $category); -+ } else { -+ $this->updateUrlPathForCategory($child); -+ } - } - } - } -@@ -112,13 +150,17 @@ class CategoryUrlPathAutogeneratorObserver implements ObserverInterface - } - - /** -+ * Update url path for category. -+ * - * @param Category $category -+ * @param Category|null $parentCategory - * @return void -+ * @throws \Magento\Framework\Exception\NoSuchEntityException - */ -- protected function updateUrlPathForCategory(Category $category) -+ protected function updateUrlPathForCategory(Category $category, Category $parentCategory = null) - { - $category->unsUrlPath(); -- $category->setUrlPath($this->categoryUrlPathGenerator->getUrlPath($category)); -+ $category->setUrlPath($this->categoryUrlPathGenerator->getUrlPath($category, $parentCategory)); - $category->getResource()->saveAttribute($category, 'url_path'); - } - } -diff --git a/app/code/Magento/CatalogUrlRewrite/Observer/ProcessUrlRewriteOnChangeProductVisibilityObserver.php b/app/code/Magento/CatalogUrlRewrite/Observer/ProcessUrlRewriteOnChangeProductVisibilityObserver.php -new file mode 100644 -index 00000000000..2337bb3646b ---- /dev/null -+++ b/app/code/Magento/CatalogUrlRewrite/Observer/ProcessUrlRewriteOnChangeProductVisibilityObserver.php -@@ -0,0 +1,54 @@ -+<?php -+/** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+declare(strict_types=1); -+ -+namespace Magento\CatalogUrlRewrite\Observer; -+ -+use Magento\Catalog\Api\Data\ProductInterface; -+use Magento\CatalogUrlRewrite\Model\Products\AdaptUrlRewritesToVisibilityAttribute; -+use Magento\Framework\Event\Observer; -+use Magento\Framework\Event\ObserverInterface; -+use Magento\UrlRewrite\Model\Exception\UrlAlreadyExistsException; -+ -+/** -+ * Consider URL rewrites on change product visibility via mass action -+ */ -+class ProcessUrlRewriteOnChangeProductVisibilityObserver implements ObserverInterface -+{ -+ /** -+ * @var AdaptUrlRewritesToVisibilityAttribute -+ */ -+ private $adaptUrlRewritesToVisibility; -+ -+ /** -+ * @param AdaptUrlRewritesToVisibilityAttribute $adaptUrlRewritesToVisibility -+ */ -+ public function __construct(AdaptUrlRewritesToVisibilityAttribute $adaptUrlRewritesToVisibility) -+ { -+ $this->adaptUrlRewritesToVisibility = $adaptUrlRewritesToVisibility; -+ } -+ -+ /** -+ * Generate urls for UrlRewrites and save it in storage -+ * -+ * @param Observer $observer -+ * @return void -+ * @throws UrlAlreadyExistsException -+ */ -+ public function execute(Observer $observer) -+ { -+ $event = $observer->getEvent(); -+ $attrData = $event->getAttributesData(); -+ $productIds = $event->getProductIds(); -+ $visibility = $attrData[ProductInterface::VISIBILITY] ?? 0; -+ -+ if (!$visibility || !$productIds) { -+ return; -+ } -+ -+ $this->adaptUrlRewritesToVisibility->execute($productIds, (int)$visibility); -+ } -+} -diff --git a/app/code/Magento/CatalogUrlRewrite/Observer/ProductProcessUrlRewriteSavingObserver.php b/app/code/Magento/CatalogUrlRewrite/Observer/ProductProcessUrlRewriteSavingObserver.php -index c4d67f447e2..6eda8dd0b61 100644 ---- a/app/code/Magento/CatalogUrlRewrite/Observer/ProductProcessUrlRewriteSavingObserver.php -+++ b/app/code/Magento/CatalogUrlRewrite/Observer/ProductProcessUrlRewriteSavingObserver.php -@@ -6,11 +6,15 @@ - namespace Magento\CatalogUrlRewrite\Observer; - - use Magento\Catalog\Model\Product; -+use Magento\CatalogUrlRewrite\Model\ProductUrlPathGenerator; - use Magento\CatalogUrlRewrite\Model\ProductUrlRewriteGenerator; -+use Magento\Framework\App\ObjectManager; - use Magento\UrlRewrite\Model\UrlPersistInterface; --use Magento\UrlRewrite\Service\V1\Data\UrlRewrite; - use Magento\Framework\Event\ObserverInterface; - -+/** -+ * Class ProductProcessUrlRewriteSavingObserver -+ */ - class ProductProcessUrlRewriteSavingObserver implements ObserverInterface - { - /** -@@ -23,22 +27,33 @@ class ProductProcessUrlRewriteSavingObserver implements ObserverInterface - */ - private $urlPersist; - -+ /** -+ * @var ProductUrlPathGenerator -+ */ -+ private $productUrlPathGenerator; -+ - /** - * @param ProductUrlRewriteGenerator $productUrlRewriteGenerator - * @param UrlPersistInterface $urlPersist -+ * @param ProductUrlPathGenerator|null $productUrlPathGenerator - */ - public function __construct( - ProductUrlRewriteGenerator $productUrlRewriteGenerator, -- UrlPersistInterface $urlPersist -+ UrlPersistInterface $urlPersist, -+ ProductUrlPathGenerator $productUrlPathGenerator = null - ) { - $this->productUrlRewriteGenerator = $productUrlRewriteGenerator; - $this->urlPersist = $urlPersist; -+ $this->productUrlPathGenerator = $productUrlPathGenerator ?: ObjectManager::getInstance() -+ ->get(ProductUrlPathGenerator::class); - } - - /** - * Generate urls for UrlRewrite and save it in storage -+ * - * @param \Magento\Framework\Event\Observer $observer - * @return void -+ * @throws \Magento\UrlRewrite\Model\Exception\UrlAlreadyExistsException - */ - public function execute(\Magento\Framework\Event\Observer $observer) - { -@@ -51,6 +66,8 @@ class ProductProcessUrlRewriteSavingObserver implements ObserverInterface - || $product->dataHasChangedFor('visibility') - ) { - if ($product->isVisibleInSiteVisibility()) { -+ $product->unsUrlPath(); -+ $product->setUrlPath($this->productUrlPathGenerator->getUrlPath($product)); - $this->urlPersist->replace($this->productUrlRewriteGenerator->generate($product)); - } - } -diff --git a/app/code/Magento/CatalogUrlRewrite/Observer/ProductToWebsiteChangeObserver.php b/app/code/Magento/CatalogUrlRewrite/Observer/ProductToWebsiteChangeObserver.php -index fc2056e83ec..44b47faf3d4 100644 ---- a/app/code/Magento/CatalogUrlRewrite/Observer/ProductToWebsiteChangeObserver.php -+++ b/app/code/Magento/CatalogUrlRewrite/Observer/ProductToWebsiteChangeObserver.php -@@ -13,7 +13,12 @@ use Magento\Framework\Event\ObserverInterface; - use Magento\Store\Model\Store; - use Magento\UrlRewrite\Model\UrlPersistInterface; - use Magento\UrlRewrite\Service\V1\Data\UrlRewrite; -+use Magento\Store\Api\StoreWebsiteRelationInterface; -+use Magento\Framework\App\ObjectManager; - -+/** -+ * Observer to assign the products to website -+ */ - class ProductToWebsiteChangeObserver implements ObserverInterface - { - /** -@@ -36,22 +41,31 @@ class ProductToWebsiteChangeObserver implements ObserverInterface - */ - protected $request; - -+ /** -+ * @var StoreWebsiteRelationInterface -+ */ -+ private $storeWebsiteRelation; -+ - /** - * @param ProductUrlRewriteGenerator $productUrlRewriteGenerator - * @param UrlPersistInterface $urlPersist - * @param ProductRepositoryInterface $productRepository - * @param RequestInterface $request -+ * @param StoreWebsiteRelationInterface $storeWebsiteRelation - */ - public function __construct( - ProductUrlRewriteGenerator $productUrlRewriteGenerator, - UrlPersistInterface $urlPersist, - ProductRepositoryInterface $productRepository, -- RequestInterface $request -+ RequestInterface $request, -+ StoreWebsiteRelationInterface $storeWebsiteRelation = null - ) { - $this->productUrlRewriteGenerator = $productUrlRewriteGenerator; - $this->urlPersist = $urlPersist; - $this->productRepository = $productRepository; - $this->request = $request; -+ $this->storeWebsiteRelation = $storeWebsiteRelation ?: -+ ObjectManager::getInstance()->get(StoreWebsiteRelationInterface::class); - } - - /** -@@ -69,12 +83,21 @@ class ProductToWebsiteChangeObserver implements ObserverInterface - $this->request->getParam('store_id', Store::DEFAULT_STORE_ID) - ); - -- $this->urlPersist->deleteByData([ -- UrlRewrite::ENTITY_ID => $product->getId(), -- UrlRewrite::ENTITY_TYPE => ProductUrlRewriteGenerator::ENTITY_TYPE, -- ]); -- if ($product->getVisibility() != Visibility::VISIBILITY_NOT_VISIBLE) { -- $this->urlPersist->replace($this->productUrlRewriteGenerator->generate($product)); -+ if (!empty($this->productUrlRewriteGenerator->generate($product))) { -+ if ($this->request->getParam('remove_website_ids')) { -+ foreach ($this->request->getParam('remove_website_ids') as $webId) { -+ foreach ($this->storeWebsiteRelation->getStoreByWebsiteId($webId) as $storeId) { -+ $this->urlPersist->deleteByData([ -+ UrlRewrite::ENTITY_ID => $product->getId(), -+ UrlRewrite::ENTITY_TYPE => ProductUrlRewriteGenerator::ENTITY_TYPE, -+ UrlRewrite::STORE_ID => $storeId -+ ]); -+ } -+ } -+ } -+ if ($product->getVisibility() != Visibility::VISIBILITY_NOT_VISIBLE) { -+ $this->urlPersist->replace($this->productUrlRewriteGenerator->generate($product)); -+ } - } - } - } -diff --git a/app/code/Magento/CatalogUrlRewrite/Observer/ProductUrlKeyAutogeneratorObserver.php b/app/code/Magento/CatalogUrlRewrite/Observer/ProductUrlKeyAutogeneratorObserver.php -index b201ae31b68..28afff56c01 100644 ---- a/app/code/Magento/CatalogUrlRewrite/Observer/ProductUrlKeyAutogeneratorObserver.php -+++ b/app/code/Magento/CatalogUrlRewrite/Observer/ProductUrlKeyAutogeneratorObserver.php -@@ -33,6 +33,9 @@ class ProductUrlKeyAutogeneratorObserver implements ObserverInterface - { - /** @var Product $product */ - $product = $observer->getEvent()->getProduct(); -- $product->setUrlKey($this->productUrlPathGenerator->getUrlKey($product)); -+ $urlKey = $this->productUrlPathGenerator->getUrlKey($product); -+ if (null !== $urlKey) { -+ $product->setUrlKey($urlKey); -+ } - } - } -diff --git a/app/code/Magento/CatalogUrlRewrite/Observer/UrlRewriteHandler.php b/app/code/Magento/CatalogUrlRewrite/Observer/UrlRewriteHandler.php -index c4ec0bb3a74..083b39d621f 100644 ---- a/app/code/Magento/CatalogUrlRewrite/Observer/UrlRewriteHandler.php -+++ b/app/code/Magento/CatalogUrlRewrite/Observer/UrlRewriteHandler.php -@@ -16,6 +16,7 @@ use Magento\CatalogUrlRewrite\Model\CategoryProductUrlPathGenerator; - use Magento\CatalogUrlRewrite\Model\CategoryUrlRewriteGenerator; - use Magento\CatalogUrlRewrite\Model\ProductScopeRewriteGenerator; - use Magento\CatalogUrlRewrite\Model\ProductUrlRewriteGenerator; -+use Magento\Framework\App\Config\ScopeConfigInterface; - use Magento\Framework\App\ObjectManager; - use Magento\Framework\Serialize\Serializer\Json; - use Magento\UrlRewrite\Model\MergeDataProvider; -@@ -24,6 +25,8 @@ use Magento\UrlRewrite\Model\UrlPersistInterface; - use Magento\UrlRewrite\Service\V1\Data\UrlRewrite; - - /** -+ * Class for management url rewrites. -+ * - * @SuppressWarnings(PHPMD.CouplingBetweenObjects) - */ - class UrlRewriteHandler -@@ -78,6 +81,11 @@ class UrlRewriteHandler - */ - private $productScopeRewriteGenerator; - -+ /** -+ * @var ScopeConfigInterface -+ */ -+ private $scopeConfig; -+ - /** - * @param ChildrenCategoriesProvider $childrenCategoriesProvider - * @param CategoryUrlRewriteGenerator $categoryUrlRewriteGenerator -@@ -88,6 +96,8 @@ class UrlRewriteHandler - * @param MergeDataProviderFactory|null $mergeDataProviderFactory - * @param Json|null $serializer - * @param ProductScopeRewriteGenerator|null $productScopeRewriteGenerator -+ * @param ScopeConfigInterface|null $scopeConfig -+ * @SuppressWarnings(PHPMD.ExcessiveParameterList) - */ - public function __construct( - ChildrenCategoriesProvider $childrenCategoriesProvider, -@@ -98,7 +108,8 @@ class UrlRewriteHandler - CategoryProductUrlPathGenerator $categoryBasedProductRewriteGenerator, - MergeDataProviderFactory $mergeDataProviderFactory = null, - Json $serializer = null, -- ProductScopeRewriteGenerator $productScopeRewriteGenerator = null -+ ProductScopeRewriteGenerator $productScopeRewriteGenerator = null, -+ ScopeConfigInterface $scopeConfig = null - ) { - $this->childrenCategoriesProvider = $childrenCategoriesProvider; - $this->categoryUrlRewriteGenerator = $categoryUrlRewriteGenerator; -@@ -113,6 +124,7 @@ class UrlRewriteHandler - $this->serializer = $serializer ?: $objectManager->get(Json::class); - $this->productScopeRewriteGenerator = $productScopeRewriteGenerator - ?: $objectManager->get(ProductScopeRewriteGenerator::class); -+ $this->scopeConfig = $scopeConfig ?? $objectManager->get(ScopeConfigInterface::class); - } - - /** -@@ -131,14 +143,19 @@ class UrlRewriteHandler - if ($category->getChangedProductIds()) { - $this->generateChangedProductUrls($mergeDataProvider, $category, $storeId, $saveRewriteHistory); - } else { -- $mergeDataProvider->merge( -- $this->getCategoryProductsUrlRewrites( -- $category, -- $storeId, -- $saveRewriteHistory, -- $category->getEntityId() -- ) -- ); -+ $categoryStoreIds = $this->getCategoryStoreIds($category); -+ -+ foreach ($categoryStoreIds as $categoryStoreId) { -+ $this->isSkippedProduct[$category->getEntityId()] = []; -+ $mergeDataProvider->merge( -+ $this->getCategoryProductsUrlRewrites( -+ $category, -+ $categoryStoreId, -+ $saveRewriteHistory, -+ $category->getEntityId() -+ ) -+ ); -+ } - } - - foreach ($this->childrenCategoriesProvider->getChildren($category, true) as $childCategory) { -@@ -156,6 +173,30 @@ class UrlRewriteHandler - } - - /** -+ * 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 +225,8 @@ class UrlRewriteHandler - } - - /** -+ * Get category products url rewrites. -+ * - * @param Category $category - * @param int $storeId - * @param bool $saveRewriteHistory -@@ -197,12 +240,13 @@ class UrlRewriteHandler - $rootCategoryId = null - ) { - $mergeDataProvider = clone $this->mergeDataProviderPrototype; -+ $generateProductRewrite = (bool)$this->scopeConfig->getValue('catalog/seo/generate_category_product_rewrites'); - - /** @var Collection $productCollection */ - $productCollection = $this->productCollectionFactory->create(); - - $productCollection->addCategoriesFilter(['eq' => [$category->getEntityId()]]) -- ->setStoreId($storeId) -+ ->addStoreFilter($storeId) - ->addAttributeToSelect('name') - ->addAttributeToSelect('visibility') - ->addAttributeToSelect('url_key') -@@ -217,6 +261,7 @@ class UrlRewriteHandler - $this->isSkippedProduct[$category->getEntityId()][] = $product->getId(); - $product->setStoreId($storeId); - $product->setData('save_rewrites_history', $saveRewriteHistory); -+ $product->setData('generate_rewrites', $generateProductRewrite); - $mergeDataProvider->merge( - $this->categoryBasedProductRewriteGenerator->generate($product, $rootCategoryId) - ); -@@ -252,7 +297,7 @@ class UrlRewriteHandler - /* @var Collection $collection */ - $collection = $this->productCollectionFactory->create() - ->setStoreId($categoryStoreId) -- ->addIdFilter($category->getAffectedProductIds()) -+ ->addIdFilter($category->getChangedProductIds()) - ->addAttributeToSelect('visibility') - ->addAttributeToSelect('name') - ->addAttributeToSelect('url_key') -diff --git a/app/code/Magento/CatalogUrlRewrite/Plugin/DynamicCategoryRewrites.php b/app/code/Magento/CatalogUrlRewrite/Plugin/DynamicCategoryRewrites.php -new file mode 100644 -index 00000000000..139452116f5 ---- /dev/null -+++ b/app/code/Magento/CatalogUrlRewrite/Plugin/DynamicCategoryRewrites.php -@@ -0,0 +1,96 @@ -+<?php -+/** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+declare(strict_types=1); -+ -+namespace Magento\CatalogUrlRewrite\Plugin; -+ -+use Magento\Framework\App\Config\ScopeConfigInterface; -+use Magento\UrlRewrite\Service\V1\Data\UrlRewrite; -+use Magento\CatalogUrlRewrite\Model\Storage\DynamicStorage; -+use Magento\CatalogUrlRewrite\Model\Storage\DbStorage; -+ -+/** -+ * Class DbStorage -+ */ -+class DynamicCategoryRewrites -+{ -+ /** -+ * @var ScopeConfigInterface -+ */ -+ private $config; -+ -+ /** -+ * @var DynamicStorage -+ */ -+ private $dynamicStorage; -+ -+ /** -+ * @param ScopeConfigInterface|null $config -+ * @param DynamicStorage $dynamicStorage -+ */ -+ public function __construct( -+ ScopeConfigInterface $config, -+ DynamicStorage $dynamicStorage -+ ) { -+ $this->config = $config; -+ $this->dynamicStorage = $dynamicStorage; -+ } -+ -+ /** -+ * Check config value of generate_category_product_rewrites -+ * -+ * @return bool -+ */ -+ private function isCategoryRewritesEnabled(): bool -+ { -+ return (bool)$this->config->getValue('catalog/seo/generate_category_product_rewrites'); -+ } -+ -+ /** -+ * Execute proxy -+ * -+ * @param callable $proceed -+ * @param array $data -+ * @param string $functionName -+ * @return mixed -+ */ -+ private function proxy(callable $proceed, array $data, string $functionName) -+ { -+ if ($this->isCategoryRewritesEnabled()) { -+ return $proceed($data); -+ } -+ -+ return $this->dynamicStorage->$functionName($data); -+ } -+ -+ /** -+ * Find rewrite by specific data -+ * -+ * @SuppressWarnings(PHPMD.UnusedFormalParameter) -+ * @param DbStorage $subject -+ * @param callable $proceed -+ * @param array $data -+ * @return UrlRewrite|null -+ */ -+ public function aroundFindOneByData(DbStorage $subject, callable $proceed, array $data) -+ { -+ return $this->proxy($proceed, $data, 'findOneByData'); -+ } -+ -+ /** -+ * Find rewrites by specific data -+ * -+ * @SuppressWarnings(PHPMD.UnusedFormalParameter) -+ * @param DbStorage $subject -+ * @param callable $proceed -+ * @param array $data -+ * @return UrlRewrite[] -+ */ -+ public function aroundFindAllByData(DbStorage $subject, callable $proceed, array $data) -+ { -+ return $this->proxy($proceed, $data, 'findAllByData'); -+ } -+} -diff --git a/app/code/Magento/CatalogUrlRewrite/Plugin/Webapi/Controller/Rest/InputParamsResolver.php b/app/code/Magento/CatalogUrlRewrite/Plugin/Webapi/Controller/Rest/InputParamsResolver.php -new file mode 100644 -index 00000000000..4e8e3840693 ---- /dev/null -+++ b/app/code/Magento/CatalogUrlRewrite/Plugin/Webapi/Controller/Rest/InputParamsResolver.php -@@ -0,0 +1,88 @@ -+<?php -+/** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+ -+declare(strict_types=1); -+ -+namespace Magento\CatalogUrlRewrite\Plugin\Webapi\Controller\Rest; -+ -+use Magento\Catalog\Api\ProductRepositoryInterface; -+use Magento\Framework\Webapi\Rest\Request as RestRequest; -+ -+/** -+ * Plugin for InputParamsResolver -+ * -+ * Used to modify product data with save_rewrites_history flag -+ */ -+class InputParamsResolver -+{ -+ /** -+ * @var RestRequest -+ */ -+ private $request; -+ -+ /** -+ * @param RestRequest $request -+ */ -+ public function __construct(RestRequest $request) -+ { -+ $this->request = $request; -+ } -+ -+ /** -+ * Add 'save_rewrites_history' param to the product data -+ * -+ * @see \Magento\CatalogUrlRewrite\Plugin\Catalog\Controller\Adminhtml\Product\Initialization\Helper -+ * @param \Magento\Webapi\Controller\Rest\InputParamsResolver $subject -+ * @param array $result -+ * @return array -+ */ -+ public function afterResolve(\Magento\Webapi\Controller\Rest\InputParamsResolver $subject, array $result): array -+ { -+ $route = $subject->getRoute(); -+ $serviceMethodName = $route->getServiceMethod(); -+ $serviceClassName = $route->getServiceClass(); -+ $requestBodyParams = $this->request->getBodyParams(); -+ -+ if ($this->isProductSaveCalled($serviceClassName, $serviceMethodName) -+ && $this->isCustomAttributesExists($requestBodyParams)) { -+ foreach ($requestBodyParams['product']['custom_attributes'] as $attribute) { -+ if ($attribute['attribute_code'] === 'save_rewrites_history') { -+ foreach ($result as $resultItem) { -+ if ($resultItem instanceof \Magento\Catalog\Model\Product) { -+ $resultItem->setData('save_rewrites_history', (bool)$attribute['value']); -+ break 2; -+ } -+ } -+ break; -+ } -+ } -+ } -+ return $result; -+ } -+ -+ /** -+ * Check that product save method called -+ * -+ * @param string $serviceClassName -+ * @param string $serviceMethodName -+ * @return bool -+ */ -+ private function isProductSaveCalled(string $serviceClassName, string $serviceMethodName): bool -+ { -+ return $serviceClassName === ProductRepositoryInterface::class && $serviceMethodName === 'save'; -+ } -+ -+ /** -+ * Check is any custom options exists in product data -+ * -+ * @param array $requestBodyParams -+ * @return bool -+ */ -+ private function isCustomAttributesExists(array $requestBodyParams): bool -+ { -+ return !empty($requestBodyParams['product']['custom_attributes']); -+ } -+} -diff --git a/app/code/Magento/CatalogUrlRewrite/Test/Mftf/Test/AdminUrlForProductRewrittenCorrectlyTest.xml b/app/code/Magento/CatalogUrlRewrite/Test/Mftf/Test/AdminUrlForProductRewrittenCorrectlyTest.xml -new file mode 100644 -index 00000000000..54232676765 ---- /dev/null -+++ b/app/code/Magento/CatalogUrlRewrite/Test/Mftf/Test/AdminUrlForProductRewrittenCorrectlyTest.xml -@@ -0,0 +1,83 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> -+ <test name="AdminUrlForProductRewrittenCorrectlyTest"> -+ <annotations> -+ <features value="CatalogUrlRewrite"/> -+ <stories value="Url rewrites"/> -+ <title value="Check that URL for product rewritten correctly"/> -+ <description value="Check that URL for product rewritten correctly"/> -+ <severity value="MAJOR"/> -+ <testCaseId value="MAGETWO-97224"/> -+ <useCaseId value="MAGETWO-64191"/> -+ <group value="CatalogUrlRewrite"/> -+ </annotations> -+ -+ <before> -+ <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> -+ -+ <!--Create product--> -+ <createData entity="_defaultCategory" stepKey="category"/> -+ <createData entity="ApiSimpleProduct" stepKey="createProduct"> -+ <requiredEntity createDataKey="category"/> -+ </createData> -+ </before> -+ <after> -+ <!--Delete created data--> -+ <deleteData createDataKey="createProduct" stepKey="deleteSimpleProduct"/> -+ <deleteData createDataKey="category" stepKey="deleteCategory"/> -+ -+ <actionGroup ref="logout" stepKey="logout"/> -+ </after> -+ -+ <!--Open Created product--> -+ <amOnPage url="{{AdminProductEditPage.url($$createProduct.id$$)}}" stepKey="amOnEditPage"/> -+ <waitForPageLoad stepKey="waitForEditPage"/> -+ -+ <!--Switch to Default Store view--> -+ <actionGroup ref="SwitchToTheNewStoreView" stepKey="selectSecondStoreView"> -+ <argument name="storeViewName" value="Default Store View"/> -+ </actionGroup> -+ <waitForPageLoad stepKey="waitForStoreViewLoad"/> -+ -+ <!--Set use default url--> -+ <click selector="{{AdminProductSEOSection.sectionHeader}}" stepKey="clickOnSearchEngineOptimization"/> -+ <waitForElementVisible selector="{{AdminProductSEOSection.useDefaultUrl}}" stepKey="waitForUseDefaultUrlCheckbox"/> -+ <click selector="{{AdminProductSEOSection.useDefaultUrl}}" stepKey="clickUseDefaultUrlCheckbox"/> -+ <fillField selector="{{AdminProductSEOSection.urlKeyInput}}" userInput="$$createProduct.sku$$-new" stepKey="changeUrlKey"/> -+ <actionGroup ref="saveProductForm" stepKey="saveProduct"/> -+ -+ <!--Select product and go toUpdate Attribute page--> -+ <amOnPage url="{{ProductCatalogPage.url}}" stepKey="GoToCatalogPageChangingView"/> -+ <waitForPageLoad stepKey="WaitForPageToLoadFullyChangingView"/> -+ <actionGroup ref="filterProductGridByName" stepKey="filterBundleProductOptionsDownToName"> -+ <argument name="product" value="ApiSimpleProduct"/> -+ </actionGroup> -+ <click selector="{{AdminProductFiltersSection.allCheckbox}}" stepKey="ClickOnSelectAllCheckBoxChangingView"/> -+ <click selector="{{AdminProductGridSection.bulkActionDropdown}}" stepKey="clickActionDropdown"/> -+ <click selector="{{AdminProductGridSection.bulkActionOption('Update attributes')}}" stepKey="clickBulkUpdate"/> -+ <waitForPageLoad stepKey="waitForUpdateAttributesPageLoad"/> -+ <seeInCurrentUrl url="{{ProductAttributesEditPage.url}}" stepKey="seeInUrlAttributeUpdatePage"/> -+ <click selector="{{AdminUpdateAttributesWebsiteSection.website}}" stepKey="clickWebsiteTab"/> -+ <waitForAjaxLoad stepKey="waitForLoadWebSiteTab"/> -+ <click selector="{{AdminUpdateAttributesWebsiteSection.addProductToWebsite}}" stepKey="checkAddProductToWebsiteCheckbox"/> -+ <click selector="{{AdminUpdateAttributesSection.saveButton}}" stepKey="clickSave"/> -+ <see selector="{{AdminProductMessagesSection.successMessage}}" userInput="Message is added to queue" stepKey="seeSaveSuccess"/> -+ -+ <!-- Run cron twice --> -+ <magentoCLI command="cron:run" stepKey="runCron1"/> -+ <magentoCLI command="cron:run" stepKey="runCron2"/> -+ <reloadPage stepKey="refreshPage"/> -+ <waitForPageLoad stepKey="waitFormToReload1"/> -+ -+ <!--Got to Store front product page and check url--> -+ <amOnPage url="{{StorefrontProductPage.url($$createProduct.sku$$-new)}}" stepKey="navigateToSimpleProductPage"/> -+ <seeInCurrentUrl url="{{StorefrontProductPage.url($$createProduct.sku$$-new)}}" stepKey="seeProductNewUrl"/> -+ </test> -+</tests> -diff --git a/app/code/Magento/CatalogUrlRewrite/Test/Mftf/Test/RewriteStoreLevelUrlKeyOfChildCategoryTest.xml b/app/code/Magento/CatalogUrlRewrite/Test/Mftf/Test/RewriteStoreLevelUrlKeyOfChildCategoryTest.xml -new file mode 100644 -index 00000000000..67870c51140 ---- /dev/null -+++ b/app/code/Magento/CatalogUrlRewrite/Test/Mftf/Test/RewriteStoreLevelUrlKeyOfChildCategoryTest.xml -@@ -0,0 +1,67 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> -+ <test name="RewriteStoreLevelUrlKeyOfChildCategoryTest"> -+ <annotations> -+ <title value="Rewriting Store-level URL key of child category"/> -+ <stories value="MAGETWO-91649: #13513: Magento ignore store-level url_key of child category in URL rewrite process for global scope"/> -+ <description value="Rewriting Store-level URL key of child category"/> -+ <features value="CatalogUrlRewrite"/> -+ <severity value="MAJOR"/> -+ <testCaseId value="MAGETWO-94934"/> -+ <group value="CatalogUrlRewrite"/> -+ </annotations> -+ -+ <before> -+ <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> -+ <actionGroup ref="AdminCreateStoreViewActionGroup" stepKey="createStoreView" /> -+ -+ <createData entity="_defaultCategory" stepKey="defaultCategory"/> -+ <createData entity="SubCategoryWithParent" stepKey="subCategory"> -+ <requiredEntity createDataKey="defaultCategory"/> -+ </createData> -+ </before> -+ -+ <actionGroup ref="navigateToCreatedCategory" stepKey="navigateToCreatedSubCategory"> -+ <argument name="Category" value="$$subCategory$$"/> -+ </actionGroup> -+ -+ <actionGroup ref="AdminSwitchStoreViewActionGroup" stepKey="AdminSwitchStoreViewForSubCategory"/> -+ -+ <actionGroup ref="ChangeSeoUrlKeyForSubCategory" stepKey="changeSeoUrlKeyForSubCategory"> -+ <argument name="value" value="bags-second"/> -+ </actionGroup> -+ -+ <actionGroup ref="navigateToCreatedCategory" stepKey="navigateToCreatedDefaultCategory"> -+ <argument name="Category" value="$$defaultCategory$$"/> -+ </actionGroup> -+ -+ <actionGroup ref="ChangeSeoUrlKey" stepKey="changeSeoUrlKeyForDefaultCategory"> -+ <argument name="value" value="gear-global"/> -+ </actionGroup> -+ -+ <amOnPage url="{{StorefrontHomePage.url}}" stepKey="goToStorefrontPage"/> -+ <waitForPageLoad stepKey="waitForPageLoad"/> -+ -+ <actionGroup ref="StorefrontSwitchStoreViewActionGroup" stepKey="storefrontSwitchStoreView"/> -+ -+ <actionGroup ref="GoToSubCategoryPage" stepKey="goToSubCategoryPage"> -+ <argument name="parentCategory" value="$$defaultCategory$$"/> -+ <argument name="subCategory" value="$$subCategory$$"/> -+ <argument name="urlPath" value="gear-global/bags-second"/> -+ </actionGroup> -+ -+ <after> -+ <actionGroup ref="AdminDeleteStoreViewActionGroup" stepKey="deleteStoreView"/> -+ <actionGroup ref="logout" stepKey="logout"/> -+ -+ <deleteData createDataKey="subCategory" stepKey="deleteSubCategory"/> -+ <deleteData createDataKey="defaultCategory" stepKey="deleteNewRootCategory"/> -+ </after> -+ </test> -+</tests> -diff --git a/app/code/Magento/CatalogUrlRewrite/Test/Unit/Model/Category/ChildrenUrlRewriteGeneratorTest.php b/app/code/Magento/CatalogUrlRewrite/Test/Unit/Model/Category/ChildrenUrlRewriteGeneratorTest.php -index 3f641256b12..f8422c7c05f 100644 ---- a/app/code/Magento/CatalogUrlRewrite/Test/Unit/Model/Category/ChildrenUrlRewriteGeneratorTest.php -+++ b/app/code/Magento/CatalogUrlRewrite/Test/Unit/Model/Category/ChildrenUrlRewriteGeneratorTest.php -@@ -31,6 +31,9 @@ class ChildrenUrlRewriteGeneratorTest extends \PHPUnit\Framework\TestCase - /** @var \PHPUnit_Framework_MockObject_MockObject */ - private $serializerMock; - -+ /** @var \PHPUnit_Framework_MockObject_MockObject */ -+ private $categoryRepository; -+ - protected function setUp() - { - $this->serializerMock = $this->getMockBuilder(Json::class) -@@ -47,6 +50,9 @@ class ChildrenUrlRewriteGeneratorTest extends \PHPUnit\Framework\TestCase - $this->categoryUrlRewriteGenerator = $this->getMockBuilder( - \Magento\CatalogUrlRewrite\Model\CategoryUrlRewriteGenerator::class - )->disableOriginalConstructor()->getMock(); -+ $this->categoryRepository = $this->getMockBuilder( -+ \Magento\Catalog\Model\CategoryRepository::class -+ )->disableOriginalConstructor()->getMock(); - $mergeDataProviderFactory = $this->createPartialMock( - \Magento\UrlRewrite\Model\MergeDataProviderFactory::class, - ['create'] -@@ -59,14 +65,15 @@ class ChildrenUrlRewriteGeneratorTest extends \PHPUnit\Framework\TestCase - [ - 'childrenCategoriesProvider' => $this->childrenCategoriesProvider, - 'categoryUrlRewriteGeneratorFactory' => $this->categoryUrlRewriteGeneratorFactory, -- 'mergeDataProviderFactory' => $mergeDataProviderFactory -+ 'mergeDataProviderFactory' => $mergeDataProviderFactory, -+ 'categoryRepository' => $this->categoryRepository - ] - ); - } - - public function testNoChildrenCategories() - { -- $this->childrenCategoriesProvider->expects($this->once())->method('getChildren')->with($this->category, true) -+ $this->childrenCategoriesProvider->expects($this->once())->method('getChildrenIds')->with($this->category, true) - ->will($this->returnValue([])); - - $this->assertEquals([], $this->childrenUrlRewriteGenerator->generate('store_id', $this->category)); -@@ -76,14 +83,16 @@ class ChildrenUrlRewriteGeneratorTest extends \PHPUnit\Framework\TestCase - { - $storeId = 'store_id'; - $saveRewritesHistory = 'flag'; -+ $childId = 2; - - $childCategory = $this->getMockBuilder(\Magento\Catalog\Model\Category::class) - ->disableOriginalConstructor()->getMock(); -- $childCategory->expects($this->once())->method('setStoreId')->with($storeId); - $childCategory->expects($this->once())->method('setData') - ->with('save_rewrites_history', $saveRewritesHistory); -- $this->childrenCategoriesProvider->expects($this->once())->method('getChildren')->with($this->category, true) -- ->will($this->returnValue([$childCategory])); -+ $this->childrenCategoriesProvider->expects($this->once())->method('getChildrenIds')->with($this->category, true) -+ ->will($this->returnValue([$childId])); -+ $this->categoryRepository->expects($this->once())->method('get') -+ ->with($childId, $storeId)->willReturn($childCategory); - $this->category->expects($this->any())->method('getData')->with('save_rewrites_history') - ->will($this->returnValue($saveRewritesHistory)); - $this->categoryUrlRewriteGeneratorFactory->expects($this->once())->method('create') -diff --git a/app/code/Magento/CatalogUrlRewrite/Test/Unit/Model/Category/CurrentUrlRewritesRegeneratorTest.php b/app/code/Magento/CatalogUrlRewrite/Test/Unit/Model/Category/CurrentUrlRewritesRegeneratorTest.php -index fbc620a6d74..294cf856290 100644 ---- a/app/code/Magento/CatalogUrlRewrite/Test/Unit/Model/Category/CurrentUrlRewritesRegeneratorTest.php -+++ b/app/code/Magento/CatalogUrlRewrite/Test/Unit/Model/Category/CurrentUrlRewritesRegeneratorTest.php -@@ -252,7 +252,7 @@ class CurrentUrlRewritesRegeneratorTest extends \PHPUnit\Framework\TestCase - ->disableOriginalConstructor()->getMock(); - foreach ($urlRewrite as $key => $value) { - $url->expects($this->any()) -- ->method('get' . str_replace(' ', '', ucwords(str_replace('_', ' ', $key)))) -+ ->method('get' . str_replace('_', '', ucwords($key, '_'))) - ->will($this->returnValue($value)); - } - $rewrites[] = $url; -diff --git a/app/code/Magento/CatalogUrlRewrite/Test/Unit/Model/Category/Plugin/Category/MoveTest.php b/app/code/Magento/CatalogUrlRewrite/Test/Unit/Model/Category/Plugin/Category/MoveTest.php -index f91a55c11b9..85e88370271 100644 ---- a/app/code/Magento/CatalogUrlRewrite/Test/Unit/Model/Category/Plugin/Category/MoveTest.php -+++ b/app/code/Magento/CatalogUrlRewrite/Test/Unit/Model/Category/Plugin/Category/MoveTest.php -@@ -5,6 +5,7 @@ - */ - namespace Magento\CatalogUrlRewrite\Test\Unit\Model\Category\Plugin\Category; - -+use Magento\Catalog\Model\CategoryFactory; - use Magento\CatalogUrlRewrite\Model\Category\Plugin\Category\Move as CategoryMovePlugin; - use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; - use Magento\CatalogUrlRewrite\Model\CategoryUrlPathGenerator; -@@ -39,6 +40,11 @@ class MoveTest extends \PHPUnit\Framework\TestCase - */ - private $categoryMock; - -+ /** -+ * @var CategoryFactory|\PHPUnit_Framework_MockObject_MockObject -+ */ -+ private $categoryFactory; -+ - /** - * @var CategoryMovePlugin - */ -@@ -55,28 +61,44 @@ class MoveTest extends \PHPUnit\Framework\TestCase - ->disableOriginalConstructor() - ->setMethods(['getChildren']) - ->getMock(); -+ $this->categoryFactory = $this->getMockBuilder(CategoryFactory::class) -+ ->disableOriginalConstructor() -+ ->getMock(); - $this->subjectMock = $this->getMockBuilder(CategoryResourceModel::class) - ->disableOriginalConstructor() - ->getMock(); - $this->categoryMock = $this->getMockBuilder(Category::class) - ->disableOriginalConstructor() -- ->setMethods(['getResource', 'setUrlPath']) -+ ->setMethods(['getResource', 'setUrlPath', 'getStoreIds', 'getStoreId', 'setStoreId']) - ->getMock(); - $this->plugin = $this->objectManager->getObject( - CategoryMovePlugin::class, - [ - 'categoryUrlPathGenerator' => $this->categoryUrlPathGeneratorMock, -- 'childrenCategoriesProvider' => $this->childrenCategoriesProviderMock -+ 'childrenCategoriesProvider' => $this->childrenCategoriesProviderMock, -+ 'categoryFactory' => $this->categoryFactory - ] - ); - } - -+ /** -+ * Tests url updating for children categories. -+ */ - public function testAfterChangeParent() - { - $urlPath = 'test/path'; -- $this->categoryMock->expects($this->once()) -- ->method('getResource') -+ $storeIds = [1]; -+ $originalCategory = $this->getMockBuilder(Category::class) -+ ->disableOriginalConstructor() -+ ->getMock(); -+ $this->categoryFactory->method('create') -+ ->willReturn($originalCategory); -+ -+ $this->categoryMock->method('getResource') - ->willReturn($this->subjectMock); -+ $this->categoryMock->expects($this->once()) -+ ->method('getStoreIds') -+ ->willReturn($storeIds); - $this->childrenCategoriesProviderMock->expects($this->once()) - ->method('getChildren') - ->with($this->categoryMock, true) -@@ -85,9 +107,6 @@ class MoveTest extends \PHPUnit\Framework\TestCase - ->method('getUrlPath') - ->with($this->categoryMock) - ->willReturn($urlPath); -- $this->categoryMock->expects($this->once()) -- ->method('getResource') -- ->willReturn($this->subjectMock); - $this->categoryMock->expects($this->once()) - ->method('setUrlPath') - ->with($urlPath); -diff --git a/app/code/Magento/CatalogUrlRewrite/Test/Unit/Model/Product/CurrentUrlRewritesRegeneratorTest.php b/app/code/Magento/CatalogUrlRewrite/Test/Unit/Model/Product/CurrentUrlRewritesRegeneratorTest.php -index 4855478b848..c431743fc0b 100644 ---- a/app/code/Magento/CatalogUrlRewrite/Test/Unit/Model/Product/CurrentUrlRewritesRegeneratorTest.php -+++ b/app/code/Magento/CatalogUrlRewrite/Test/Unit/Model/Product/CurrentUrlRewritesRegeneratorTest.php -@@ -294,7 +294,7 @@ class CurrentUrlRewritesRegeneratorTest extends \PHPUnit\Framework\TestCase - ->disableOriginalConstructor()->getMock(); - foreach ($urlRewrite as $key => $value) { - $url->expects($this->any()) -- ->method('get' . str_replace(' ', '', ucwords(str_replace('_', ' ', $key)))) -+ ->method('get' . str_replace('_', '', ucwords($key, '_'))) - ->will($this->returnValue($value)); - } - $rewrites[] = $url; -diff --git a/app/code/Magento/CatalogUrlRewrite/Test/Unit/Model/ProductScopeRewriteGeneratorTest.php b/app/code/Magento/CatalogUrlRewrite/Test/Unit/Model/ProductScopeRewriteGeneratorTest.php -index 06be01445df..d4e0978dda6 100644 ---- a/app/code/Magento/CatalogUrlRewrite/Test/Unit/Model/ProductScopeRewriteGeneratorTest.php -+++ b/app/code/Magento/CatalogUrlRewrite/Test/Unit/Model/ProductScopeRewriteGeneratorTest.php -@@ -50,6 +50,9 @@ class ProductScopeRewriteGeneratorTest extends \PHPUnit\Framework\TestCase - /** @var \Magento\Catalog\Model\Category|\PHPUnit_Framework_MockObject_MockObject */ - private $categoryMock; - -+ /** @var \Magento\Framework\App\Config\ScopeConfigInterface|\PHPUnit_Framework_MockObject_MockObject */ -+ private $configMock; -+ - public function setUp() - { - $this->serializer = $this->createMock(\Magento\Framework\Serialize\Serializer\Json::class); -@@ -96,6 +99,7 @@ class ProductScopeRewriteGeneratorTest extends \PHPUnit\Framework\TestCase - ); - $this->mergeDataProvider = new \Magento\UrlRewrite\Model\MergeDataProvider(); - $mergeDataProviderFactory->expects($this->once())->method('create')->willReturn($this->mergeDataProvider); -+ $this->configMock = $this->getMockBuilder(\Magento\Framework\App\Config\ScopeConfigInterface::class)->getMock(); - - $this->productScopeGenerator = (new ObjectManager($this))->getObject( - \Magento\CatalogUrlRewrite\Model\ProductScopeRewriteGenerator::class, -@@ -107,7 +111,8 @@ class ProductScopeRewriteGeneratorTest extends \PHPUnit\Framework\TestCase - 'objectRegistryFactory' => $this->objectRegistryFactory, - 'storeViewService' => $this->storeViewService, - 'storeManager' => $this->storeManager, -- 'mergeDataProviderFactory' => $mergeDataProviderFactory -+ 'mergeDataProviderFactory' => $mergeDataProviderFactory, -+ 'config' => $this->configMock - ] - ); - $this->categoryMock = $this->getMockBuilder(Category::class)->disableOriginalConstructor()->getMock(); -@@ -115,6 +120,9 @@ class ProductScopeRewriteGeneratorTest extends \PHPUnit\Framework\TestCase - - public function testGenerationForGlobalScope() - { -+ $this->configMock->expects($this->any())->method('getValue') -+ ->with('catalog/seo/generate_category_product_rewrites') -+ ->willReturn('1'); - $product = $this->createMock(\Magento\Catalog\Model\Product::class); - $product->expects($this->any())->method('getStoreId')->will($this->returnValue(null)); - $product->expects($this->any())->method('getStoreIds')->will($this->returnValue([1])); -diff --git a/app/code/Magento/CatalogUrlRewrite/Test/Unit/Model/ProductUrlPathGeneratorTest.php b/app/code/Magento/CatalogUrlRewrite/Test/Unit/Model/ProductUrlPathGeneratorTest.php -index b32b0216b9b..5076577447a 100644 ---- a/app/code/Magento/CatalogUrlRewrite/Test/Unit/Model/ProductUrlPathGeneratorTest.php -+++ b/app/code/Magento/CatalogUrlRewrite/Test/Unit/Model/ProductUrlPathGeneratorTest.php -@@ -3,12 +3,17 @@ - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -+declare(strict_types=1); -+ - namespace Magento\CatalogUrlRewrite\Test\Unit\Model; - - use Magento\CatalogUrlRewrite\Model\ProductUrlPathGenerator; - use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; - use Magento\Store\Model\ScopeInterface; - -+/** -+ * Class ProductUrlPathGeneratorTest -+ */ - class ProductUrlPathGeneratorTest extends \PHPUnit\Framework\TestCase - { - /** @var \Magento\CatalogUrlRewrite\Model\ProductUrlPathGenerator */ -@@ -32,7 +37,10 @@ class ProductUrlPathGeneratorTest extends \PHPUnit\Framework\TestCase - /** @var \Magento\Catalog\Model\Category|\PHPUnit_Framework_MockObject_MockObject */ - protected $category; - -- protected function setUp() -+ /** -+ * @inheritdoc -+ */ -+ protected function setUp(): void - { - $this->category = $this->createMock(\Magento\Catalog\Model\Category::class); - $productMethods = [ -@@ -69,13 +77,14 @@ class ProductUrlPathGeneratorTest extends \PHPUnit\Framework\TestCase - /** - * @return array - */ -- public function getUrlPathDataProvider() -+ public function getUrlPathDataProvider(): array - { - return [ -- 'path based on url key' => ['url-key', null, 'url-key'], -- 'path based on product name 1' => ['', 'product-name', 'product-name'], -- 'path based on product name 2' => [null, 'product-name', 'product-name'], -- 'path based on product name 3' => [false, 'product-name', 'product-name'] -+ 'path based on url key uppercase' => ['Url-Key', null, 0, 'url-key'], -+ 'path based on url key' => ['url-key', null, 0, 'url-key'], -+ 'path based on product name 1' => ['', 'product-name', 1, 'product-name'], -+ 'path based on product name 2' => [null, 'product-name', 1, 'product-name'], -+ 'path based on product name 3' => [false, 'product-name', 1, 'product-name'] - ]; - } - -@@ -83,15 +92,18 @@ class ProductUrlPathGeneratorTest extends \PHPUnit\Framework\TestCase - * @dataProvider getUrlPathDataProvider - * @param string|null|bool $urlKey - * @param string|null|bool $productName -+ * @param int $formatterCalled - * @param string $result -+ * @return void - */ -- public function testGetUrlPath($urlKey, $productName, $result) -+ public function testGetUrlPath($urlKey, $productName, $formatterCalled, $result): void - { - $this->product->expects($this->once())->method('getData')->with('url_path') - ->will($this->returnValue(null)); - $this->product->expects($this->any())->method('getUrlKey')->will($this->returnValue($urlKey)); - $this->product->expects($this->any())->method('getName')->will($this->returnValue($productName)); -- $this->product->expects($this->once())->method('formatUrlKey')->will($this->returnArgument(0)); -+ $this->product->expects($this->exactly($formatterCalled)) -+ ->method('formatUrlKey')->will($this->returnArgument(0)); - - $this->assertEquals($result, $this->productUrlPathGenerator->getUrlPath($this->product, null)); - } -@@ -99,22 +111,23 @@ class ProductUrlPathGeneratorTest extends \PHPUnit\Framework\TestCase - /** - * @param string|bool $productUrlKey - * @param string|bool $expectedUrlKey -+ * @return void - * @dataProvider getUrlKeyDataProvider - */ -- public function testGetUrlKey($productUrlKey, $expectedUrlKey) -+ public function testGetUrlKey($productUrlKey, $expectedUrlKey): void - { - $this->product->expects($this->any())->method('getUrlKey')->will($this->returnValue($productUrlKey)); - $this->product->expects($this->any())->method('formatUrlKey')->will($this->returnValue($productUrlKey)); -- $this->assertEquals($expectedUrlKey, $this->productUrlPathGenerator->getUrlKey($this->product)); -+ $this->assertSame($expectedUrlKey, $this->productUrlPathGenerator->getUrlKey($this->product)); - } - - /** - * @return array - */ -- public function getUrlKeyDataProvider() -+ public function getUrlKeyDataProvider(): array - { - return [ -- 'URL Key use default' => [false, false], -+ 'URL Key use default' => [false, null], - 'URL Key empty' => ['product-url', 'product-url'], - ]; - } -@@ -123,9 +136,10 @@ class ProductUrlPathGeneratorTest extends \PHPUnit\Framework\TestCase - * @param string|null|bool $storedUrlKey - * @param string|null|bool $productName - * @param string $expectedUrlKey -+ * @return void - * @dataProvider getUrlPathDefaultUrlKeyDataProvider - */ -- public function testGetUrlPathDefaultUrlKey($storedUrlKey, $productName, $expectedUrlKey) -+ public function testGetUrlPathDefaultUrlKey($storedUrlKey, $productName, $expectedUrlKey): void - { - $this->product->expects($this->once())->method('getData')->with('url_path') - ->will($this->returnValue(null)); -@@ -138,7 +152,7 @@ class ProductUrlPathGeneratorTest extends \PHPUnit\Framework\TestCase - /** - * @return array - */ -- public function getUrlPathDefaultUrlKeyDataProvider() -+ public function getUrlPathDefaultUrlKeyDataProvider(): array - { - return [ - ['default-store-view-url-key', null, 'default-store-view-url-key'], -@@ -146,7 +160,10 @@ class ProductUrlPathGeneratorTest extends \PHPUnit\Framework\TestCase - ]; - } - -- public function testGetUrlPathWithCategory() -+ /** -+ * @return void -+ */ -+ public function testGetUrlPathWithCategory(): void - { - $this->product->expects($this->once())->method('getData')->with('url_path') - ->will($this->returnValue('product-path')); -@@ -159,7 +176,10 @@ class ProductUrlPathGeneratorTest extends \PHPUnit\Framework\TestCase - ); - } - -- public function testGetUrlPathWithSuffix() -+ /** -+ * @return void -+ */ -+ public function testGetUrlPathWithSuffix(): void - { - $storeId = 1; - $this->product->expects($this->once())->method('getData')->with('url_path') -@@ -177,7 +197,10 @@ class ProductUrlPathGeneratorTest extends \PHPUnit\Framework\TestCase - ); - } - -- public function testGetUrlPathWithSuffixAndCategoryAndStore() -+ /** -+ * @return void -+ */ -+ public function testGetUrlPathWithSuffixAndCategoryAndStore(): void - { - $storeId = 1; - $this->product->expects($this->once())->method('getData')->with('url_path') -@@ -195,7 +218,10 @@ class ProductUrlPathGeneratorTest extends \PHPUnit\Framework\TestCase - ); - } - -- public function testGetCanonicalUrlPath() -+ /** -+ * @return void -+ */ -+ public function testGetCanonicalUrlPath(): void - { - $this->product->expects($this->once())->method('getId')->will($this->returnValue(1)); - -@@ -205,7 +231,10 @@ class ProductUrlPathGeneratorTest extends \PHPUnit\Framework\TestCase - ); - } - -- public function testGetCanonicalUrlPathWithCategory() -+ /** -+ * @return void -+ */ -+ public function testGetCanonicalUrlPathWithCategory(): void - { - $this->product->expects($this->once())->method('getId')->will($this->returnValue(1)); - $this->category->expects($this->once())->method('getId')->will($this->returnValue(1)); -diff --git a/app/code/Magento/CatalogUrlRewrite/Test/Unit/Observer/AfterImportDataObserverTest.php b/app/code/Magento/CatalogUrlRewrite/Test/Unit/Observer/AfterImportDataObserverTest.php -index fd9ab10537f..3984d949332 100644 ---- a/app/code/Magento/CatalogUrlRewrite/Test/Unit/Observer/AfterImportDataObserverTest.php -+++ b/app/code/Magento/CatalogUrlRewrite/Test/Unit/Observer/AfterImportDataObserverTest.php -@@ -694,7 +694,7 @@ class AfterImportDataObserverTest extends \PHPUnit\Framework\TestCase - ->disableOriginalConstructor()->getMock(); - foreach ($urlRewrite as $key => $value) { - $url->expects($this->any()) -- ->method('get' . str_replace(' ', '', ucwords(str_replace('_', ' ', $key)))) -+ ->method('get' . str_replace('_', '', ucwords($key, '_'))) - ->will($this->returnValue($value)); - } - $rewrites[] = $url; -diff --git a/app/code/Magento/CatalogUrlRewrite/Test/Unit/Observer/CategoryProcessUrlRewriteMovingObserverTest.php b/app/code/Magento/CatalogUrlRewrite/Test/Unit/Observer/CategoryProcessUrlRewriteMovingObserverTest.php -new file mode 100644 -index 00000000000..57162b44f98 ---- /dev/null -+++ b/app/code/Magento/CatalogUrlRewrite/Test/Unit/Observer/CategoryProcessUrlRewriteMovingObserverTest.php -@@ -0,0 +1,148 @@ -+<?php -+/** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+declare(strict_types=1); -+ -+namespace Magento\CatalogUrlRewrite\Test\Unit\Observer; -+ -+use Magento\Catalog\Model\Category; -+use Magento\CatalogUrlRewrite\Block\UrlKeyRenderer; -+use Magento\CatalogUrlRewrite\Model\CategoryUrlRewriteGenerator; -+use Magento\CatalogUrlRewrite\Model\Map\DatabaseMapPool; -+use Magento\CatalogUrlRewrite\Model\Map\DataCategoryUrlRewriteDatabaseMap; -+use Magento\CatalogUrlRewrite\Model\Map\DataProductUrlRewriteDatabaseMap; -+use Magento\CatalogUrlRewrite\Model\UrlRewriteBunchReplacer; -+use Magento\CatalogUrlRewrite\Observer\CategoryProcessUrlRewriteMovingObserver; -+use Magento\CatalogUrlRewrite\Observer\UrlRewriteHandler; -+use Magento\Framework\App\Config\ScopeConfigInterface; -+use Magento\Framework\Event; -+use Magento\Framework\Event\Observer; -+use Magento\UrlRewrite\Model\UrlPersistInterface; -+ -+/** -+ * Class CategoryProcessUrlRewriteMovingObserverTest -+ * -+ * @SuppressWarnings(PHPMD.CouplingBetweenObjects) -+ */ -+class CategoryProcessUrlRewriteMovingObserverTest extends \PHPUnit\Framework\TestCase -+{ -+ /** -+ * @var CategoryProcessUrlRewriteMovingObserver -+ */ -+ private $observer; -+ -+ /** -+ * @var CategoryUrlRewriteGenerator|\PHPUnit_Framework_MockObject_MockObject -+ */ -+ private $categoryUrlRewriteGeneratorMock; -+ -+ /** -+ * @var UrlPersistInterface|\PHPUnit_Framework_MockObject_MockObject -+ */ -+ private $urlPersistMock; -+ -+ /** -+ * @var ScopeConfigInterface|\PHPUnit_Framework_MockObject_MockObject -+ */ -+ private $scopeConfigMock; -+ -+ /** -+ * @var UrlRewriteHandler|\PHPUnit_Framework_MockObject_MockObject -+ */ -+ private $urlRewriteHandlerMock; -+ -+ /** -+ * @var DatabaseMapPool|\PHPUnit_Framework_MockObject_MockObject -+ */ -+ private $databaseMapPoolMock; -+ -+ /** -+ * Set Up -+ */ -+ protected function setUp() -+ { -+ $this->categoryUrlRewriteGeneratorMock = $this->createMock(CategoryUrlRewriteGenerator::class); -+ $this->urlPersistMock = $this->createMock(UrlPersistInterface::class); -+ $this->scopeConfigMock = $this->createMock(ScopeConfigInterface::class); -+ $this->urlRewriteHandlerMock = $this->createMock(UrlRewriteHandler::class); -+ /** @var UrlRewriteBunchReplacer|\PHPUnit_Framework_MockObject_MockObject $urlRewriteMock */ -+ $urlRewriteMock = $this->createMock(UrlRewriteBunchReplacer::class); -+ $this->databaseMapPoolMock = $this->createMock(DatabaseMapPool::class); -+ -+ $this->observer = new CategoryProcessUrlRewriteMovingObserver( -+ $this->categoryUrlRewriteGeneratorMock, -+ $this->urlPersistMock, -+ $this->scopeConfigMock, -+ $this->urlRewriteHandlerMock, -+ $urlRewriteMock, -+ $this->databaseMapPoolMock, -+ [ -+ DataCategoryUrlRewriteDatabaseMap::class, -+ DataProductUrlRewriteDatabaseMap::class -+ ] -+ ); -+ } -+ -+ /** -+ * Test category process rewrite url by changing the parent -+ * -+ * @return void -+ */ -+ public function testCategoryProcessUrlRewriteAfterMovingWithChangedParentId() -+ { -+ /** @var Observer|\PHPUnit_Framework_MockObject_MockObject $observerMock */ -+ $observerMock = $this->createMock(Observer::class); -+ $eventMock = $this->getMockBuilder(Event::class) -+ ->disableOriginalConstructor() -+ ->setMethods(['getCategory']) -+ ->getMock(); -+ $categoryMock = $this->createPartialMock( -+ Category::class, -+ [ -+ 'dataHasChangedFor', -+ 'getEntityId', -+ 'getStoreId', -+ 'setData' -+ ] -+ ); -+ -+ $categoryMock->expects($this->once())->method('dataHasChangedFor')->with('parent_id') -+ ->willReturn(true); -+ $eventMock->expects($this->once())->method('getCategory')->willReturn($categoryMock); -+ $observerMock->expects($this->once())->method('getEvent')->willReturn($eventMock); -+ $this->scopeConfigMock->expects($this->once())->method('isSetFlag') -+ ->with(UrlKeyRenderer::XML_PATH_SEO_SAVE_HISTORY)->willReturn(true); -+ $this->scopeConfigMock->method('getValue')->willReturn(true); -+ $this->categoryUrlRewriteGeneratorMock->expects($this->once())->method('generate') -+ ->with($categoryMock, true)->willReturn(['category-url-rewrite']); -+ $this->urlRewriteHandlerMock->expects($this->once())->method('generateProductUrlRewrites') -+ ->with($categoryMock)->willReturn(['product-url-rewrite']); -+ $this->databaseMapPoolMock->expects($this->exactly(2))->method('resetMap')->willReturnSelf(); -+ -+ $this->observer->execute($observerMock); -+ } -+ -+ /** -+ * Test category process rewrite url without changing the parent -+ * -+ * @return void -+ */ -+ public function testCategoryProcessUrlRewriteAfterMovingWithinNotChangedParent() -+ { -+ /** @var Observer|\PHPUnit_Framework_MockObject_MockObject $observerMock */ -+ $observerMock = $this->createMock(Observer::class); -+ $eventMock = $this->getMockBuilder(Event::class) -+ ->disableOriginalConstructor() -+ ->setMethods(['getCategory']) -+ ->getMock(); -+ $categoryMock = $this->createPartialMock(Category::class, ['dataHasChangedFor']); -+ $observerMock->expects($this->once())->method('getEvent')->willReturn($eventMock); -+ $eventMock->expects($this->once())->method('getCategory')->willReturn($categoryMock); -+ $categoryMock->expects($this->once())->method('dataHasChangedFor')->with('parent_id') -+ ->willReturn(false); -+ -+ $this->observer->execute($observerMock); -+ } -+} -diff --git a/app/code/Magento/CatalogUrlRewrite/Test/Unit/Observer/CategoryProcessUrlRewriteSavingObserverTest.php b/app/code/Magento/CatalogUrlRewrite/Test/Unit/Observer/CategoryProcessUrlRewriteSavingObserverTest.php -index afdb5488875..cdea1347581 100644 ---- a/app/code/Magento/CatalogUrlRewrite/Test/Unit/Observer/CategoryProcessUrlRewriteSavingObserverTest.php -+++ b/app/code/Magento/CatalogUrlRewrite/Test/Unit/Observer/CategoryProcessUrlRewriteSavingObserverTest.php -@@ -12,6 +12,7 @@ use Magento\CatalogUrlRewrite\Observer\CategoryProcessUrlRewriteSavingObserver; - use Magento\CatalogUrlRewrite\Model\UrlRewriteBunchReplacer; - use Magento\CatalogUrlRewrite\Observer\UrlRewriteHandler; - use Magento\CatalogUrlRewrite\Model\Map\DatabaseMapPool; -+use Magento\Framework\App\Config\ScopeConfigInterface as ScopeConfigInterfaceAlias; - use Magento\Store\Model\ResourceModel\Group\CollectionFactory; - use Magento\Framework\TestFramework\Unit\Helper\ObjectManager as ObjectManagerHelper; - use Magento\Catalog\Model\Category; -@@ -61,6 +62,11 @@ class CategoryProcessUrlRewriteSavingObserverTest extends \PHPUnit\Framework\Tes - */ - private $storeGroupFactory; - -+ /** -+ * @var \PHPUnit\Framework\MockObject\MockObject -+ */ -+ private $scopeConfigMock; -+ - /** - * {@inheritDoc} - */ -@@ -70,12 +76,16 @@ class CategoryProcessUrlRewriteSavingObserverTest extends \PHPUnit\Framework\Tes - \Magento\Framework\Event\Observer::class, - ['getEvent', 'getData'] - ); -- $this->category = $this->createPartialMock(Category::class, [ -- 'hasData', -- 'getParentId', -- 'dataHasChangedFor', -- 'getChangedProductIds', -- ]); -+ $this->category = $this->createPartialMock( -+ Category::class, -+ [ -+ 'hasData', -+ 'getParentId', -+ 'getStoreId', -+ 'dataHasChangedFor', -+ 'getChangedProductIds', -+ ] -+ ); - $this->observer->expects($this->any()) - ->method('getEvent') - ->willReturnSelf(); -@@ -100,6 +110,11 @@ class CategoryProcessUrlRewriteSavingObserverTest extends \PHPUnit\Framework\Tes - ->setMethods(['create']) - ->disableOriginalConstructor() - ->getMock(); -+ $this->scopeConfigMock = $this->getMockBuilder(ScopeConfigInterfaceAlias::class) -+ ->setMethods(['getValue']) -+ ->disableOriginalConstructor() -+ ->getMockForAbstractClass(); -+ $this->scopeConfigMock->method('getValue')->willReturn(true); - - $this->categoryProcessUrlRewriteSavingObserver = (new ObjectManagerHelper($this))->getObject( - CategoryProcessUrlRewriteSavingObserver::class, -@@ -109,6 +124,7 @@ class CategoryProcessUrlRewriteSavingObserverTest extends \PHPUnit\Framework\Tes - 'urlRewriteBunchReplacer' => $this->urlRewriteBunchReplacerMock, - 'databaseMapPool' => $this->databaseMapPoolMock, - 'storeGroupFactory' => $this->storeGroupFactory, -+ 'scopeConfig' => $this->scopeConfigMock - ] - ); - } -@@ -200,6 +216,7 @@ class CategoryProcessUrlRewriteSavingObserverTest extends \PHPUnit\Framework\Tes - $this->category->expects($this->any()) - ->method('getChangedProductIds') - ->willReturn([]); -+ $this->category->method('getStoreId')->willReturn(1); - - $result1 = ['test']; - $this->categoryUrlRewriteGeneratorMock->expects($this->once()) -diff --git a/app/code/Magento/CatalogUrlRewrite/Test/Unit/Observer/ProductUrlKeyAutogeneratorObserverTest.php b/app/code/Magento/CatalogUrlRewrite/Test/Unit/Observer/ProductUrlKeyAutogeneratorObserverTest.php -new file mode 100644 -index 00000000000..b9628caff84 ---- /dev/null -+++ b/app/code/Magento/CatalogUrlRewrite/Test/Unit/Observer/ProductUrlKeyAutogeneratorObserverTest.php -@@ -0,0 +1,99 @@ -+<?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\Framework\TestFramework\Unit\Helper\ObjectManager as ObjectManagerHelper; -+use \Magento\CatalogUrlRewrite\Model\ProductUrlPathGenerator; -+ -+/** -+ * Unit tests for \Magento\CatalogUrlRewrite\Observer\ProductUrlKeyAutogeneratorObserver class -+ */ -+class ProductUrlKeyAutogeneratorObserverTest extends \PHPUnit\Framework\TestCase -+{ -+ /** -+ * @var \Magento\CatalogUrlRewrite\Model\ProductUrlPathGenerator|\PHPUnit_Framework_MockObject_MockObject -+ */ -+ private $productUrlPathGenerator; -+ -+ /** @var \Magento\CatalogUrlRewrite\Observer\ProductUrlKeyAutogeneratorObserver */ -+ private $productUrlKeyAutogeneratorObserver; -+ -+ /** -+ * @inheritdoc -+ */ -+ protected function setUp(): void -+ { -+ $this->productUrlPathGenerator = $this->getMockBuilder(ProductUrlPathGenerator::class) -+ ->disableOriginalConstructor() -+ ->setMethods(['getUrlKey']) -+ ->getMock(); -+ -+ $this->productUrlKeyAutogeneratorObserver = (new ObjectManagerHelper($this))->getObject( -+ \Magento\CatalogUrlRewrite\Observer\ProductUrlKeyAutogeneratorObserver::class, -+ [ -+ 'productUrlPathGenerator' => $this->productUrlPathGenerator -+ ] -+ ); -+ } -+ -+ /** -+ * @return void -+ */ -+ public function testExecuteWithUrlKey(): void -+ { -+ $urlKey = 'product_url_key'; -+ -+ $product = $this->getMockBuilder(\Magento\Catalog\Model\Product::class) -+ ->disableOriginalConstructor() -+ ->setMethods(['setUrlKey']) -+ ->getMock(); -+ $product->expects($this->atLeastOnce())->method('setUrlKey')->with($urlKey); -+ $event = $this->getMockBuilder(\Magento\Framework\Event::class) -+ ->disableOriginalConstructor() -+ ->setMethods(['getProduct']) -+ ->getMock(); -+ $event->expects($this->atLeastOnce())->method('getProduct')->willReturn($product); -+ /** @var \Magento\Framework\Event\Observer|\PHPUnit_Framework_MockObject_MockObject $observer */ -+ $observer = $this->getMockBuilder(\Magento\Framework\Event\Observer::class) -+ ->disableOriginalConstructor() -+ ->setMethods(['getEvent']) -+ ->getMock(); -+ $observer->expects($this->atLeastOnce())->method('getEvent')->willReturn($event); -+ $this->productUrlPathGenerator->expects($this->atLeastOnce())->method('getUrlKey')->with($product) -+ ->willReturn($urlKey); -+ -+ $this->productUrlKeyAutogeneratorObserver->execute($observer); -+ } -+ -+ /** -+ * @return void -+ */ -+ public function testExecuteWithEmptyUrlKey(): void -+ { -+ $product = $this->getMockBuilder(\Magento\Catalog\Model\Product::class) -+ ->disableOriginalConstructor() -+ ->setMethods(['setUrlKey']) -+ ->getMock(); -+ $product->expects($this->never())->method('setUrlKey'); -+ $event = $this->getMockBuilder(\Magento\Framework\Event::class) -+ ->disableOriginalConstructor() -+ ->setMethods(['getProduct']) -+ ->getMock(); -+ $event->expects($this->atLeastOnce())->method('getProduct')->willReturn($product); -+ /** @var \Magento\Framework\Event\Observer|\PHPUnit_Framework_MockObject_MockObject $observer */ -+ $observer = $this->getMockBuilder(\Magento\Framework\Event\Observer::class) -+ ->disableOriginalConstructor() -+ ->setMethods(['getEvent']) -+ ->getMock(); -+ $observer->expects($this->atLeastOnce())->method('getEvent')->willReturn($event); -+ $this->productUrlPathGenerator->expects($this->atLeastOnce())->method('getUrlKey')->with($product) -+ ->willReturn(null); -+ -+ $this->productUrlKeyAutogeneratorObserver->execute($observer); -+ } -+} -diff --git a/app/code/Magento/CatalogUrlRewrite/Test/Unit/Observer/UrlRewriteHandlerTest.php b/app/code/Magento/CatalogUrlRewrite/Test/Unit/Observer/UrlRewriteHandlerTest.php -index b18597a42bf..06a89a9dd5e 100644 ---- a/app/code/Magento/CatalogUrlRewrite/Test/Unit/Observer/UrlRewriteHandlerTest.php -+++ b/app/code/Magento/CatalogUrlRewrite/Test/Unit/Observer/UrlRewriteHandlerTest.php -@@ -138,8 +138,14 @@ class UrlRewriteHandlerTest extends \PHPUnit\Framework\TestCase - ->willReturn(1); - $category->expects($this->any()) - ->method('getData') -- ->with('save_rewrites_history') -- ->willReturn(true); -+ ->withConsecutive( -+ [$this->equalTo('save_rewrites_history')], -+ [$this->equalTo('initial_setup_flag')] -+ ) -+ ->willReturnOnConsecutiveCalls( -+ true, -+ null -+ ); - - /* @var \Magento\Catalog\Model\Category|\PHPUnit_Framework_MockObject_MockObject $childCategory1 */ - $childCategory1 = $this->getMockBuilder(\Magento\Catalog\Model\Category::class) -@@ -175,6 +181,7 @@ class UrlRewriteHandlerTest extends \PHPUnit\Framework\TestCase - ->method('addIdFilter') - ->willReturnSelf(); - $productCollection->expects($this->any())->method('setStoreId')->willReturnSelf(); -+ $productCollection->expects($this->any())->method('addStoreFilter')->willReturnSelf(); - $productCollection->expects($this->any())->method('addAttributeToSelect')->willReturnSelf(); - $iterator = new \ArrayIterator([]); - $productCollection->expects($this->any())->method('getIterator')->will($this->returnValue($iterator)); -diff --git a/app/code/Magento/CatalogUrlRewrite/Test/Unit/Plugin/Webapi/Controller/Rest/InputParamsResolverTest.php b/app/code/Magento/CatalogUrlRewrite/Test/Unit/Plugin/Webapi/Controller/Rest/InputParamsResolverTest.php -new file mode 100644 -index 00000000000..8e705b2c09f ---- /dev/null -+++ b/app/code/Magento/CatalogUrlRewrite/Test/Unit/Plugin/Webapi/Controller/Rest/InputParamsResolverTest.php -@@ -0,0 +1,116 @@ -+<?php -+/** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+ -+declare(strict_types=1); -+ -+namespace Magento\CatalogUrlRewrite\Test\Unit\Plugin\Webapi\Controller\Rest; -+ -+use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; -+use Magento\Webapi\Controller\Rest\InputParamsResolver; -+use Magento\CatalogUrlRewrite\Plugin\Webapi\Controller\Rest\InputParamsResolver as InputParamsResolverPlugin; -+use Magento\Framework\Webapi\Rest\Request as RestRequest; -+use Magento\Catalog\Model\Product; -+use Magento\Webapi\Controller\Rest\Router\Route; -+use Magento\Catalog\Api\ProductRepositoryInterface; -+ -+/** -+ * Unit test for InputParamsResolver plugin -+ */ -+class InputParamsResolverTest extends \PHPUnit\Framework\TestCase -+{ -+ /** -+ * @var string -+ */ -+ private $saveRewritesHistory; -+ -+ /** -+ * @var array -+ */ -+ private $requestBodyParams; -+ -+ /** -+ * @var array -+ */ -+ private $result; -+ -+ /** -+ * @var ObjectManager -+ */ -+ private $objectManager; -+ -+ /** -+ * @var InputParamsResolver|\PHPUnit_Framework_MockObject_MockObject -+ */ -+ private $subject; -+ -+ /** -+ * @var RestRequest|\PHPUnit_Framework_MockObject_MockObject -+ */ -+ private $request; -+ -+ /** -+ * @var Product|\PHPUnit_Framework_MockObject_MockObject -+ */ -+ private $product; -+ -+ /** -+ * @var Route|\PHPUnit_Framework_MockObject_MockObject -+ */ -+ private $route; -+ -+ /** -+ * @var InputParamsResolverPlugin -+ */ -+ private $plugin; -+ -+ /** -+ * @inheritdoc -+ */ -+ protected function setUp() -+ { -+ $this->saveRewritesHistory = 'save_rewrites_history'; -+ $this->requestBodyParams = [ -+ 'product' => [ -+ 'sku' => 'test', -+ 'custom_attributes' => [ -+ ['attribute_code' => $this->saveRewritesHistory, 'value' => 1] -+ ] -+ ] -+ ]; -+ -+ $this->route = $this->createPartialMock(Route::class, ['getServiceMethod', 'getServiceClass']); -+ $this->request = $this->createPartialMock(RestRequest::class, ['getBodyParams']); -+ $this->request->expects($this->any())->method('getBodyParams')->willReturn($this->requestBodyParams); -+ $this->subject = $this->createPartialMock(InputParamsResolver::class, ['getRoute']); -+ $this->subject->expects($this->any())->method('getRoute')->willReturn($this->route); -+ $this->product = $this->createPartialMock(Product::class, ['setData']); -+ -+ $this->result = [false, $this->product, 'test']; -+ -+ $this->objectManager = new ObjectManager($this); -+ $this->plugin = $this->objectManager->getObject( -+ InputParamsResolverPlugin::class, -+ [ -+ 'request' => $this->request -+ ] -+ ); -+ } -+ -+ public function testAfterResolve() -+ { -+ $this->route->expects($this->once()) -+ ->method('getServiceClass') -+ ->willReturn(ProductRepositoryInterface::class); -+ $this->route->expects($this->once()) -+ ->method('getServiceMethod') -+ ->willReturn('save'); -+ $this->product->expects($this->once()) -+ ->method('setData') -+ ->with($this->saveRewritesHistory, true); -+ -+ $this->plugin->afterResolve($this->subject, $this->result); -+ } -+} -diff --git a/app/code/Magento/CatalogUrlRewrite/composer.json b/app/code/Magento/CatalogUrlRewrite/composer.json -index e373d8c8c17..b4ceff96b50 100644 ---- a/app/code/Magento/CatalogUrlRewrite/composer.json -+++ b/app/code/Magento/CatalogUrlRewrite/composer.json -@@ -16,6 +16,9 @@ - "magento/module-ui": "*", - "magento/module-url-rewrite": "*" - }, -+ "suggest": { -+ "magento/module-webapi": "*" -+ }, - "type": "magento2-module", - "license": [ - "OSL-3.0", -diff --git a/app/code/Magento/CatalogUrlRewrite/etc/adminhtml/system.xml b/app/code/Magento/CatalogUrlRewrite/etc/adminhtml/system.xml -index 4aa2e7f40c7..1e1f4e86fa3 100644 ---- a/app/code/Magento/CatalogUrlRewrite/etc/adminhtml/system.xml -+++ b/app/code/Magento/CatalogUrlRewrite/etc/adminhtml/system.xml -@@ -28,6 +28,15 @@ - <label>Create Permanent Redirect for URLs if URL Key Changed</label> - <source_model>Magento\Config\Model\Config\Source\Yesno</source_model> - </field> -+ <field id="generate_category_product_rewrites" translate="label" type="select" sortOrder="6" showInDefault="1" showInWebsite="0" showInStore="0" canRestore="1"> -+ <label>Generate "category/product" URL Rewrites</label> -+ <backend_model>Magento\CatalogUrlRewrite\Model\TableCleaner</backend_model> -+ <source_model>Magento\Config\Model\Config\Source\Yesno</source_model> -+ <comment> -+ <![CDATA[<strong style="color:red">Warning!</strong> Turning this option off will result in permanent removal of category/product URL rewrites without an ability to restore them.]]> -+ </comment> -+ <frontend_class>generate_category_product_rewrites</frontend_class> -+ </field> - </group> - </section> - </system> -diff --git a/app/code/Magento/CatalogUrlRewrite/etc/config.xml b/app/code/Magento/CatalogUrlRewrite/etc/config.xml -new file mode 100644 -index 00000000000..d05c0b4b7aa ---- /dev/null -+++ b/app/code/Magento/CatalogUrlRewrite/etc/config.xml -@@ -0,0 +1,16 @@ -+<?xml version="1.0"?> -+<!-- -+/** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Store:etc/config.xsd"> -+ <default> -+ <catalog> -+ <seo> -+ <generate_category_product_rewrites>1</generate_category_product_rewrites> -+ </seo> -+ </catalog> -+ </default> -+</config> -diff --git a/app/code/Magento/CatalogUrlRewrite/etc/db_schema.xml b/app/code/Magento/CatalogUrlRewrite/etc/db_schema.xml -index 174173fa201..c8da5b59cf5 100644 ---- a/app/code/Magento/CatalogUrlRewrite/etc/db_schema.xml -+++ b/app/code/Magento/CatalogUrlRewrite/etc/db_schema.xml -@@ -15,16 +15,16 @@ - comment="category_id"/> - <column xsi:type="int" name="product_id" padding="10" unsigned="true" nullable="false" identity="false" - comment="product_id"/> -- <constraint xsi:type="foreign" name="CAT_URL_REWRITE_PRD_CTGR_PRD_ID_CAT_PRD_ENTT_ENTT_ID" -+ <constraint xsi:type="foreign" referenceId="CAT_URL_REWRITE_PRD_CTGR_PRD_ID_CAT_PRD_ENTT_ENTT_ID" - table="catalog_url_rewrite_product_category" column="product_id" - referenceTable="catalog_product_entity" referenceColumn="entity_id" onDelete="CASCADE"/> -- <constraint xsi:type="foreign" name="FK_BB79E64705D7F17FE181F23144528FC8" -+ <constraint xsi:type="foreign" referenceId="FK_BB79E64705D7F17FE181F23144528FC8" - table="catalog_url_rewrite_product_category" column="url_rewrite_id" referenceTable="url_rewrite" - referenceColumn="url_rewrite_id" onDelete="CASCADE"/> -- <constraint xsi:type="foreign" name="CAT_URL_REWRITE_PRD_CTGR_CTGR_ID_CAT_CTGR_ENTT_ENTT_ID" -+ <constraint xsi:type="foreign" referenceId="CAT_URL_REWRITE_PRD_CTGR_CTGR_ID_CAT_CTGR_ENTT_ENTT_ID" - table="catalog_url_rewrite_product_category" column="category_id" - referenceTable="catalog_category_entity" referenceColumn="entity_id" onDelete="CASCADE"/> -- <index name="CATALOG_URL_REWRITE_PRODUCT_CATEGORY_CATEGORY_ID_PRODUCT_ID" indexType="btree"> -+ <index referenceId="CATALOG_URL_REWRITE_PRODUCT_CATEGORY_CATEGORY_ID_PRODUCT_ID" indexType="btree"> - <column name="category_id"/> - <column name="product_id"/> - </index> -diff --git a/app/code/Magento/CatalogUrlRewrite/etc/di.xml b/app/code/Magento/CatalogUrlRewrite/etc/di.xml -index f6426677e8c..e6fbcaefd07 100644 ---- a/app/code/Magento/CatalogUrlRewrite/etc/di.xml -+++ b/app/code/Magento/CatalogUrlRewrite/etc/di.xml -@@ -24,6 +24,9 @@ - <type name="Magento\UrlRewrite\Model\StorageInterface"> - <plugin name="storage_plugin" type="Magento\CatalogUrlRewrite\Model\Category\Plugin\Storage"/> - </type> -+ <type name="Magento\CatalogUrlRewrite\Model\Storage\DbStorage"> -+ <plugin name="dynamic_storage_plugin" type="Magento\CatalogUrlRewrite\Plugin\DynamicCategoryRewrites"/> -+ </type> - <type name="Magento\CatalogUrlRewrite\Model\Map\UrlRewriteFinder"> - <arguments> - <argument name="urlRewriteClassNames" xsi:type="array"> -@@ -32,4 +35,14 @@ - </argument> - </arguments> - </type> -+ <type name="Magento\UrlRewrite\Model\CompositeUrlFinder"> -+ <arguments> -+ <argument name="children" xsi:type="array"> -+ <item name="catalog" xsi:type="array"> -+ <item name="class" xsi:type="string">Magento\CatalogUrlRewrite\Model\Storage\DbStorage</item> -+ <item name="sortOrder" xsi:type="number">20</item> -+ </item> -+ </argument> -+ </arguments> -+ </type> - </config> -diff --git a/app/code/Magento/CatalogUrlRewrite/etc/events.xml b/app/code/Magento/CatalogUrlRewrite/etc/events.xml -index cc558fe81f1..728442acf7a 100644 ---- a/app/code/Magento/CatalogUrlRewrite/etc/events.xml -+++ b/app/code/Magento/CatalogUrlRewrite/etc/events.xml -@@ -27,6 +27,9 @@ - <event name="catalog_product_save_after"> - <observer name="process_url_rewrite_saving" instance="Magento\CatalogUrlRewrite\Observer\ProductProcessUrlRewriteSavingObserver"/> - </event> -+ <event name="catalog_product_attribute_update_before"> -+ <observer name="process_url_rewrite_on_change_product_visibility" instance="Magento\CatalogUrlRewrite\Observer\ProcessUrlRewriteOnChangeProductVisibilityObserver"/> -+ </event> - <event name="catalog_category_save_before"> - <observer name="category_url_path_autogeneration" instance="Magento\CatalogUrlRewrite\Observer\CategoryUrlPathAutogeneratorObserver"/> - </event> -diff --git a/app/code/Magento/CatalogUrlRewrite/etc/frontend/di.xml b/app/code/Magento/CatalogUrlRewrite/etc/frontend/di.xml -new file mode 100644 -index 00000000000..0c7ab27e51d ---- /dev/null -+++ b/app/code/Magento/CatalogUrlRewrite/etc/frontend/di.xml -@@ -0,0 +1,10 @@ -+<?xml version="1.0"?> -+<!-- -+/** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd"> -+ -+</config> -diff --git a/app/code/Magento/CatalogUrlRewrite/etc/webapi_rest/di.xml b/app/code/Magento/CatalogUrlRewrite/etc/webapi_rest/di.xml -new file mode 100644 -index 00000000000..9c5186a5ec0 ---- /dev/null -+++ b/app/code/Magento/CatalogUrlRewrite/etc/webapi_rest/di.xml -@@ -0,0 +1,12 @@ -+<?xml version="1.0"?> -+<!-- -+/** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd"> -+ <type name="Magento\Webapi\Controller\Rest\InputParamsResolver"> -+ <plugin name="product_save_rewrites_history_rest_plugin" type="Magento\CatalogUrlRewrite\Plugin\Webapi\Controller\Rest\InputParamsResolver" sortOrder="1" disabled="false" /> -+ </type> -+</config> -diff --git a/app/code/Magento/CatalogUrlRewrite/i18n/en_US.csv b/app/code/Magento/CatalogUrlRewrite/i18n/en_US.csv -index 2dce6b233cb..b3335dc3523 100644 ---- a/app/code/Magento/CatalogUrlRewrite/i18n/en_US.csv -+++ b/app/code/Magento/CatalogUrlRewrite/i18n/en_US.csv -@@ -5,3 +5,4 @@ - "Product URL Suffix","Product URL Suffix" - "Use Categories Path for Product URLs","Use Categories Path for Product URLs" - "Create Permanent Redirect for URLs if URL Key Changed","Create Permanent Redirect for URLs if URL Key Changed" -+"Generate "category/product" URL Rewrites","Generate "category/product" URL Rewrites" -\ No newline at end of file -diff --git a/app/code/Magento/CatalogUrlRewrite/view/adminhtml/layout/adminhtml_system_config_edit.xml b/app/code/Magento/CatalogUrlRewrite/view/adminhtml/layout/adminhtml_system_config_edit.xml -new file mode 100644 -index 00000000000..c4766138843 ---- /dev/null -+++ b/app/code/Magento/CatalogUrlRewrite/view/adminhtml/layout/adminhtml_system_config_edit.xml -@@ -0,0 +1,14 @@ -+<?xml version="1.0"?> -+<!-- -+/** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+<page xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:View/Layout/etc/page_configuration.xsd"> -+ <body> -+ <referenceContainer name="js"> -+ <block name="js.confirm_remove_old_urls" template="Magento_CatalogUrlRewrite::confirm.phtml"/> -+ </referenceContainer> -+ </body> -+</page> -diff --git a/app/code/Magento/CatalogUrlRewrite/view/adminhtml/templates/confirm.phtml b/app/code/Magento/CatalogUrlRewrite/view/adminhtml/templates/confirm.phtml -new file mode 100644 -index 00000000000..800ecfd8a6e ---- /dev/null -+++ b/app/code/Magento/CatalogUrlRewrite/view/adminhtml/templates/confirm.phtml -@@ -0,0 +1,30 @@ -+<?php -+/** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+?> -+<script> -+ require([ -+ "jquery", -+ "Magento_Ui/js/modal/confirm", -+ "mage/translate", -+ ], function(jQuery, confirmation, $t) { -+ //confirmation for removing category/product URL rewrites -+ jQuery('select.generate_category_product_rewrites').on('change', function () { -+ if (this.value == 0) { -+ confirmation({ -+ title: $t('Turn off "category/products" URL rewrites?'), -+ content: $t('Turning off automatic generation of "category/products" URL rewrites will result in permanent removal of all the currently existing “category/product” type URL rewrites without an ability to restore them back. ' + -+ 'This may potentially cause unresolved “category/product” type URL conflicts which you have to resolve by updating URL key manually.'), -+ actions: { -+ cancel: function () { -+ jQuery('select.generate_category_product_rewrites').val(1); -+ return false; -+ }, -+ } -+ }) -+ } -+ }); -+ }); -+</script> -diff --git a/app/code/Magento/CatalogUrlRewriteGraphQl/etc/module.xml b/app/code/Magento/CatalogUrlRewriteGraphQl/etc/module.xml -index be4bb9fcd70..e6e0f6cf721 100644 ---- a/app/code/Magento/CatalogUrlRewriteGraphQl/etc/module.xml -+++ b/app/code/Magento/CatalogUrlRewriteGraphQl/etc/module.xml -@@ -6,5 +6,10 @@ - */ - --> - <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:Module/etc/module.xsd"> -- <module name="Magento_CatalogUrlRewriteGraphQl" /> -+ <module name="Magento_CatalogUrlRewriteGraphQl" > -+ <sequence> -+ <module name="Magento_UrlRewriteGraphQl"/> -+ <module name="Magento_CatalogGraphQl"/> -+ </sequence> -+ </module> - </config> -diff --git a/app/code/Magento/CatalogUrlRewriteGraphQl/etc/schema.graphqls b/app/code/Magento/CatalogUrlRewriteGraphQl/etc/schema.graphqls -index b96cfcb03d4..89108e578d6 100644 ---- a/app/code/Magento/CatalogUrlRewriteGraphQl/etc/schema.graphqls -+++ b/app/code/Magento/CatalogUrlRewriteGraphQl/etc/schema.graphqls -@@ -3,17 +3,18 @@ - - interface ProductInterface { - url_key: String @doc(description: "The part of the URL that identifies the product") -- url_path: String @doc(description: "The part of the URL that precedes the url_key") -+ url_path: String @deprecated(reason: "Use product's `canonical_url` or url rewrites instead") -+ url_rewrites: [UrlRewrite] @doc(description: "URL rewrites list") @resolver(class: "Magento\\UrlRewriteGraphQl\\Model\\Resolver\\UrlRewrite") - } - - input ProductFilterInput { - url_key: FilterTypeInput @doc(description: "The part of the URL that identifies the product") -- url_path: FilterTypeInput @doc(description: "The part of the URL that precedes the url_key") -+ url_path: FilterTypeInput @deprecated(reason: "Use product's `canonical_url` or url rewrites instead") - } - - input ProductSortInput { - url_key: SortEnum @doc(description: "The part of the URL that identifies the product") -- url_path: SortEnum @doc(description: "The part of the URL that precedes the url_key") -+ url_path: SortEnum @deprecated(reason: "Use product's `canonical_url` or url rewrites instead") - } - - enum UrlRewriteEntityTypeEnum @doc(description: "This enumeration defines the entity type.") { -diff --git a/app/code/Magento/CatalogWidget/Block/Product/ProductsList.php b/app/code/Magento/CatalogWidget/Block/Product/ProductsList.php -index 9a55f981b76..9e47830debf 100644 ---- a/app/code/Magento/CatalogWidget/Block/Product/ProductsList.php -+++ b/app/code/Magento/CatalogWidget/Block/Product/ProductsList.php -@@ -6,16 +6,21 @@ - - namespace Magento\CatalogWidget\Block\Product; - -+use Magento\Catalog\Model\Product; - use Magento\Framework\App\ObjectManager; -+use Magento\Framework\App\ActionInterface; - use Magento\Framework\DataObject\IdentityInterface; - use Magento\Framework\Pricing\PriceCurrencyInterface; - use Magento\Framework\Serialize\Serializer\Json; -+use Magento\Framework\View\LayoutFactory; - use Magento\Widget\Block\BlockInterface; -+use Magento\Framework\Url\EncoderInterface; - - /** - * Catalog Products List widget block -- * Class ProductsList -+ * - * @SuppressWarnings(PHPMD.CouplingBetweenObjects) -+ * @SuppressWarnings(PHPMD.ExcessiveParameterList) - */ - class ProductsList extends \Magento\Catalog\Block\Product\AbstractProduct implements BlockInterface, IdentityInterface - { -@@ -94,6 +99,21 @@ class ProductsList extends \Magento\Catalog\Block\Product\AbstractProduct implem - */ - private $json; - -+ /** -+ * @var LayoutFactory -+ */ -+ private $layoutFactory; -+ -+ /** -+ * @var \Magento\Framework\Url\EncoderInterface|null -+ */ -+ private $urlEncoder; -+ -+ /** -+ * @var \Magento\Framework\View\Element\RendererList -+ */ -+ private $rendererListBlock; -+ - /** - * @param \Magento\Catalog\Block\Product\Context $context - * @param \Magento\Catalog\Model\ResourceModel\Product\CollectionFactory $productCollectionFactory -@@ -104,6 +124,10 @@ class ProductsList extends \Magento\Catalog\Block\Product\AbstractProduct implem - * @param \Magento\Widget\Helper\Conditions $conditionsHelper - * @param array $data - * @param Json|null $json -+ * @param LayoutFactory|null $layoutFactory -+ * @param \Magento\Framework\Url\EncoderInterface|null $urlEncoder -+ * -+ * @SuppressWarnings(PHPMD.ExcessiveParameterList) - */ - public function __construct( - \Magento\Catalog\Block\Product\Context $context, -@@ -114,7 +138,9 @@ class ProductsList extends \Magento\Catalog\Block\Product\AbstractProduct implem - \Magento\CatalogWidget\Model\Rule $rule, - \Magento\Widget\Helper\Conditions $conditionsHelper, - array $data = [], -- Json $json = null -+ Json $json = null, -+ LayoutFactory $layoutFactory = null, -+ EncoderInterface $urlEncoder = null - ) { - $this->productCollectionFactory = $productCollectionFactory; - $this->catalogProductVisibility = $catalogProductVisibility; -@@ -123,6 +149,8 @@ class ProductsList extends \Magento\Catalog\Block\Product\AbstractProduct implem - $this->rule = $rule; - $this->conditionsHelper = $conditionsHelper; - $this->json = $json ?: ObjectManager::getInstance()->get(Json::class); -+ $this->layoutFactory = $layoutFactory ?: ObjectManager::getInstance()->get(LayoutFactory::class); -+ $this->urlEncoder = $urlEncoder ?: ObjectManager::getInstance()->get(EncoderInterface::class); - parent::__construct( - $context, - $data -@@ -130,7 +158,9 @@ class ProductsList extends \Magento\Catalog\Block\Product\AbstractProduct implem - } - - /** -- * {@inheritdoc} -+ * Internal constructor, that is called from real constructor -+ * -+ * @return void - */ - protected function _construct() - { -@@ -151,6 +181,7 @@ class ProductsList extends \Magento\Catalog\Block\Product\AbstractProduct implem - * Get key pieces for caching block content - * - * @return array -+ * @SuppressWarnings(PHPMD.RequestAwareBlockMethod) - */ - public function getCacheKeyInfo() - { -@@ -164,8 +195,9 @@ class ProductsList extends \Magento\Catalog\Block\Product\AbstractProduct implem - $this->_storeManager->getStore()->getId(), - $this->_design->getDesignTheme()->getId(), - $this->httpContext->getValue(\Magento\Customer\Model\Context::CONTEXT_GROUP), -- intval($this->getRequest()->getParam($this->getData('page_var_name'), 1)), -+ (int) $this->getRequest()->getParam($this->getData('page_var_name'), 1), - $this->getProductsPerPage(), -+ $this->getProductsCount(), - $conditions, - $this->json->serialize($this->getRequest()->getParams()), - $this->getTemplate(), -@@ -174,7 +206,7 @@ class ProductsList extends \Magento\Catalog\Block\Product\AbstractProduct implem - } - - /** -- * {@inheritdoc} -+ * @inheritdoc - * @SuppressWarnings(PHPMD.NPathComplexity) - */ - public function getProductPriceHtml( -@@ -196,22 +228,62 @@ class ProductsList extends \Magento\Catalog\Block\Product\AbstractProduct implem - ? $arguments['display_minimal_price'] - : true; - -- /** @var \Magento\Framework\Pricing\Render $priceRender */ -+ /** @var \Magento\Framework\Pricing\Render $priceRender */ - $priceRender = $this->getLayout()->getBlock('product.price.render.default'); -- -- $price = ''; -- if ($priceRender) { -- $price = $priceRender->render( -- \Magento\Catalog\Pricing\Price\FinalPrice::PRICE_CODE, -- $product, -- $arguments -+ if (!$priceRender) { -+ $priceRender = $this->getLayout()->createBlock( -+ \Magento\Framework\Pricing\Render::class, -+ 'product.price.render.default', -+ ['data' => ['price_render_handle' => 'catalog_product_prices']] - ); - } -+ -+ $price = $priceRender->render( -+ \Magento\Catalog\Pricing\Price\FinalPrice::PRICE_CODE, -+ $product, -+ $arguments -+ ); -+ - return $price; - } - - /** -- * {@inheritdoc} -+ * @inheritdoc -+ */ -+ protected function getDetailsRendererList() -+ { -+ if (empty($this->rendererListBlock)) { -+ /** @var $layout \Magento\Framework\View\LayoutInterface */ -+ $layout = $this->layoutFactory->create(['cacheable' => false]); -+ $layout->getUpdate()->addHandle('catalog_widget_product_list')->load(); -+ $layout->generateXml(); -+ $layout->generateElements(); -+ -+ $this->rendererListBlock = $layout->getBlock('category.product.type.widget.details.renderers'); -+ } -+ return $this->rendererListBlock; -+ } -+ -+ /** -+ * Get post parameters. -+ * -+ * @param Product $product -+ * @return array -+ */ -+ public function getAddToCartPostParams(Product $product) -+ { -+ $url = $this->getAddToCartUrl($product); -+ return [ -+ 'action' => $url, -+ 'data' => [ -+ 'product' => $product->getEntityId(), -+ ActionInterface::PARAM_NAME_URL_ENCODED => $this->urlEncoder->encode($url), -+ ] -+ ]; -+ } -+ -+ /** -+ * @inheritdoc - */ - protected function _beforeToHtml() - { -@@ -223,15 +295,22 @@ class ProductsList extends \Magento\Catalog\Block\Product\AbstractProduct implem - * Prepare and return product collection - * - * @return \Magento\Catalog\Model\ResourceModel\Product\Collection -+ * @SuppressWarnings(PHPMD.RequestAwareBlockMethod) - */ - public function createCollection() - { - /** @var $collection \Magento\Catalog\Model\ResourceModel\Product\Collection */ - $collection = $this->productCollectionFactory->create(); -+ -+ if ($this->getData('store_id') !== null) { -+ $collection->setStoreId($this->getData('store_id')); -+ } -+ - $collection->setVisibility($this->catalogProductVisibility->getVisibleInCatalogIds()); - - $collection = $this->_addProductAttributesAndPrices($collection) - ->addStoreFilter() -+ ->addAttributeToSort('created_at', 'desc') - ->setPageSize($this->getPageSize()) - ->setCurPage($this->getRequest()->getParam($this->getData('page_var_name'), 1)); - -@@ -249,6 +328,8 @@ class ProductsList extends \Magento\Catalog\Block\Product\AbstractProduct implem - } - - /** -+ * Get conditions -+ * - * @return \Magento\Rule\Model\Condition\Combine - */ - protected function getConditions() -@@ -338,7 +419,7 @@ class ProductsList extends \Magento\Catalog\Block\Product\AbstractProduct implem - if (!$this->pager) { - $this->pager = $this->getLayout()->createBlock( - \Magento\Catalog\Block\Product\Widget\Html\Pager::class, -- 'widget.products.list.pager' -+ $this->getWidgetPagerBlockName() - ); - - $this->pager->setUseContainer(true) -@@ -386,8 +467,9 @@ class ProductsList extends \Magento\Catalog\Block\Product\AbstractProduct implem - } - - /** -- * @return PriceCurrencyInterface -+ * Get currency of product - * -+ * @return PriceCurrencyInterface - * @deprecated 100.2.0 - */ - private function getPriceCurrency() -@@ -398,4 +480,37 @@ class ProductsList extends \Magento\Catalog\Block\Product\AbstractProduct implem - } - return $this->priceCurrency; - } -+ -+ /** -+ * @inheritdoc -+ */ -+ public function getAddToCartUrl($product, $additional = []) -+ { -+ $requestingPageUrl = $this->getRequest()->getParam('requesting_page_url'); -+ -+ if (!empty($requestingPageUrl)) { -+ $additional['useUencPlaceholder'] = true; -+ $url = parent::getAddToCartUrl($product, $additional); -+ return str_replace('%25uenc%25', $this->urlEncoder->encode($requestingPageUrl), $url); -+ } -+ -+ return parent::getAddToCartUrl($product, $additional); -+ } -+ -+ /** -+ * Get widget block name -+ * -+ * @return string -+ */ -+ private function getWidgetPagerBlockName() -+ { -+ $pageName = $this->getData('page_var_name'); -+ $pagerBlockName = 'widget.products.list.pager'; -+ -+ if (!$pageName) { -+ return $pagerBlockName; -+ } -+ -+ return $pagerBlockName . '.' . $pageName; -+ } - } -diff --git a/app/code/Magento/CatalogWidget/Model/Rule/Condition/Product.php b/app/code/Magento/CatalogWidget/Model/Rule/Condition/Product.php -index 29adc1816d3..e5fb20a58ae 100644 ---- a/app/code/Magento/CatalogWidget/Model/Rule/Condition/Product.php -+++ b/app/code/Magento/CatalogWidget/Model/Rule/Condition/Product.php -@@ -9,6 +9,7 @@ - */ - namespace Magento\CatalogWidget\Model\Rule\Condition; - -+use Magento\Catalog\Api\Data\ProductInterface; - use Magento\Catalog\Model\ProductCategoryList; - - /** -@@ -77,17 +78,22 @@ class Product extends \Magento\Rule\Model\Condition\Product\AbstractProduct - } - - /** -- * {@inheritdoc} -+ * @inheritdoc - */ - public function loadAttributeOptions() - { - $productAttributes = $this->_productResource->loadAllAttributes()->getAttributesByCode(); -+ $productAttributes = array_filter( -+ $productAttributes, -+ function ($attribute) { -+ return $attribute->getFrontendLabel() && -+ $attribute->getFrontendInput() !== 'text' && -+ $attribute->getAttributeCode() !== ProductInterface::STATUS; -+ } -+ ); - - $attributes = []; - foreach ($productAttributes as $attribute) { -- if (!$attribute->getFrontendLabel() || $attribute->getFrontendInput() == 'text') { -- continue; -- } - $attributes[$attribute->getAttributeCode()] = $attribute->getFrontendLabel(); - } - -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 00000000000..1f54ff40283 ---- /dev/null -+++ b/app/code/Magento/CatalogWidget/Test/Mftf/ActionGroup/AdminCreateBlockWithWidgetActionGroup.xml -@@ -0,0 +1,49 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> -+ <actionGroup name="AdminCreateBlockWithWidget"> -+ <arguments> -+ <argument name="addCondition" type="string"/> -+ <argument name="isCondition" type="string"/> -+ <argument name="fieldCondition" type="string"/> -+ </arguments> -+ -+ <click stepKey="clickShowHideButton" selector="{{BlockWYSIWYGSection.ShowHideBtn}}"/> -+ <waitForElementVisible stepKey="waitForInsertWidgetButton" selector="{{CatalogWidgetSection.insertWidgetButton}}"/> -+ -+ <selectOption stepKey="selectAllStoreView" userInput="All Store Views" selector="{{CatalogWidgetSection.storeViewOption}}"/> -+ <fillField selector="{{BlockContentSection.TextArea}}" userInput="" stepKey="makeContentFieldEmpty"/> -+ -+ <click selector="{{CatalogWidgetSection.insertWidgetButton}}" stepKey="clickInsertWidgetButton"/> -+ <waitForElementVisible stepKey="waitForInsertWidgetFrame" selector="{{InsertWidgetSection.widgetTypeDropDown}}" time="10"/> -+ -+ <selectOption selector="{{InsertWidgetSection.widgetTypeDropDown}}" userInput="Catalog Products List" stepKey="selectCatalogProductListOption"/> -+ <waitForElementVisible stepKey="waitForConditionsElementBecomeAvailable" selector="{{InsertWidgetSection.conditionsAddButton}}"/> -+ -+ <click selector="{{InsertWidgetSection.conditionsAddButton}}" stepKey="clickToAddCondition"/> -+ <waitForElementVisible stepKey="waitForSelectBoxOpened" selector="{{InsertWidgetSection.conditionsSelectBox}}"/> -+ -+ <selectOption selector="{{InsertWidgetSection.conditionsSelectBox}}" userInput="{{addCondition}}" stepKey="selectConditionsSelectBox"/> -+ <waitForElementVisible stepKey="seeConditionsAdded" selector="{{InsertWidgetSection.addCondition('1')}}"/> -+ -+ <click selector="{{InsertWidgetSection.conditionIs}}" stepKey="clickToConditionIs"/> -+ <selectOption selector="{{InsertWidgetSection.conditionOperator('1')}}" stepKey="selectOperatorGreaterThan" userInput="{{isCondition}}"/> -+ -+ <click selector="{{InsertWidgetSection.addCondition('1')}}" stepKey="clickAddConditionItem"/> -+ <waitForElementVisible stepKey="waitForConditionFieldOpened" selector="{{InsertWidgetSection.conditionField('1')}}"/> -+ -+ <fillField selector="{{InsertWidgetSection.conditionField('1')}}" stepKey="setOperator" userInput="{{fieldCondition}}"/> -+ <click selector="{{WidgetSection.InsertWidget}}" stepKey="clickInsertWidget"/> -+ -+ <waitForElementVisible stepKey="waitForInsertWidgetSaved" selector="{{InsertWidgetSection.save}}"/> -+ <click stepKey="clickSaveButton" selector="{{InsertWidgetSection.save}}"/> -+ <see userInput="You saved the block." stepKey="seeSavedBlockMsgOnForm"/> -+ </actionGroup> -+</actionGroups> -diff --git a/app/code/Magento/CatalogWidget/Test/Mftf/Section/CatalogWidgetSection.xml b/app/code/Magento/CatalogWidget/Test/Mftf/Section/CatalogWidgetSection.xml -new file mode 100644 -index 00000000000..855d325c985 ---- /dev/null -+++ b/app/code/Magento/CatalogWidget/Test/Mftf/Section/CatalogWidgetSection.xml -@@ -0,0 +1,26 @@ -+<?xml version="1.0" encoding="utf-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> -+ <section name="CatalogWidgetSection"> -+ <element name="insertWidgetButton" type="button" selector=".scalable.action-add-widget.plugin"/> -+ <element name="storeViewOption" type="button" selector="//*[@name='store_id']"/> -+ </section> -+ -+ <section name="InsertWidgetSection"> -+ <element name="widgetTypeDropDown" type="select" selector="#select_widget_type"/> -+ <element name="conditionsAddButton" type="button" selector=".rule-param.rule-param-new-child"/> -+ <element name="conditionsSelectBox" type="button" selector="#conditions__1__new_child"/> -+ <element name="addCondition" type="button" selector="//*[@id='conditions__1--{{arg1}}__value']/../preceding-sibling::a" parameterized="true"/> -+ <element name="conditionField" type="button" selector="#conditions__1--{{arg2}}__value" parameterized="true"/> -+ <element name="save" type="button" selector="#save-button"/> -+ <element name="conditionIs" type="button" selector="//*[@id='conditions__1--1__attribute']/following-sibling::span[1]"/> -+ <element name="conditionOperator" type="button" selector="#conditions__1--{{arg3}}__operator" parameterized="true"/> -+ <element name="checkElementStorefrontByPrice" type="button" selector="//*[@class='product-items widget-product-grid']//*[contains(text(),'${{arg4}}.00')]" parameterized="true"/> -+ <element name="checkElementStorefrontByName" type="button" selector="//*[@class='product-items widget-product-grid']//*[@class='product-item'][{{productPosition}}]//a[contains(text(), '{{productName}}')]" parameterized="true"/> -+ </section> -+</sections> -diff --git a/app/code/Magento/CatalogWidget/Test/Mftf/Section/ProductListWidgetSection.xml b/app/code/Magento/CatalogWidget/Test/Mftf/Section/ProductListWidgetSection.xml -new file mode 100644 -index 00000000000..03bef8ffa3b ---- /dev/null -+++ b/app/code/Magento/CatalogWidget/Test/Mftf/Section/ProductListWidgetSection.xml -@@ -0,0 +1,15 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> -+ <section name="ProductListWidgetSection"> -+ <element name="AddToCartByName" type="button" selector="//*[contains(@class,'product-item-info')][descendant::a[contains(text(), '{{arg1}}')]]//button[contains(@class,'tocart')]" parameterized="true"/> -+ <element name="AddToCompareByName" type="button" selector="//*[contains(@class,'product-item-info')][descendant::a[contains(text(), '{{arg1}}')]]//button[contains(@class,'tocompare')]" parameterized="true"/> -+ </section> -+</sections> -diff --git a/app/code/Magento/CatalogWidget/Test/Mftf/Test/CatalogProductListWidgetOperatorsTest.xml b/app/code/Magento/CatalogWidget/Test/Mftf/Test/CatalogProductListWidgetOperatorsTest.xml -new file mode 100644 -index 00000000000..32bea8b604c ---- /dev/null -+++ b/app/code/Magento/CatalogWidget/Test/Mftf/Test/CatalogProductListWidgetOperatorsTest.xml -@@ -0,0 +1,157 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> -+ <test name="CatalogProductListWidgetOperatorsTest"> -+ <annotations> -+ <features value="CatalogWidget"/> -+ <stories value="MAGETWO-91609: Problems with operator more/less in the 'catalog Products List' widget"/> -+ <title value="Checking operator more/less in the 'catalog Products List' widget"/> -+ <description value="Check 'less than', 'equals or greater than', 'equals or less than' operators"/> -+ <severity value="MAJOR"/> -+ <testCaseId value="MAGETWO-94479"/> -+ <group value="CatalogWidget"/> -+ <group value="WYSIWYGDisabled"/> -+ </annotations> -+ -+ <before> -+ <createData entity="SimpleSubCategory" stepKey="simplecategory"/> -+ <createData entity="SimpleProduct" stepKey="createFirstProduct"> -+ <requiredEntity createDataKey="simplecategory"/> -+ <field key="price">10</field> -+ </createData> -+ <createData entity="SimpleProduct" stepKey="createSecondProduct"> -+ <requiredEntity createDataKey="simplecategory"/> -+ <field key="price">50</field> -+ </createData> -+ <createData entity="SimpleProduct" stepKey="createThirdProduct"> -+ <requiredEntity createDataKey="simplecategory"/> -+ <field key="price">100</field> -+ </createData> -+ -+ <createData entity="_defaultBlock" stepKey="createPreReqBlock"/> -+ <!--User log in on back-end as admin--> -+ <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> -+ <actionGroup ref="EnabledWYSIWYG" stepKey="enableWYSIWYG"/> -+ </before> -+ -+ <!--Open block with widget.--> -+ <actionGroup ref="navigateToCreatedCMSBlockPage" stepKey="navigateToCreatedCMSBlockPage1"> -+ <argument name="CMSBlockPage" value="$$createPreReqBlock$$"/> -+ </actionGroup> -+ -+ <actionGroup ref="AdminCreateBlockWithWidget" stepKey="adminCreateBlockWithWidget"> -+ <argument name="addCondition" value="Price"/> -+ <argument name="isCondition" value="greater than"/> -+ <argument name="fieldCondition" value="20"/> -+ </actionGroup> -+ -+ <!--Go to Catalog > Categories (choose category where created products)--> -+ <amOnPage url="{{AdminCategoryPage.url}}" stepKey="onCategoryIndexPage"/> -+ <waitForPageLoad stepKey="waitForCategoryPageLoadAddProducts" after="onCategoryIndexPage"/> -+ <click selector="{{AdminCategorySidebarTreeSection.expandAll}}" stepKey="clickExpandAll" after="waitForCategoryPageLoadAddProducts"/> -+ <click selector="{{AdminCategorySidebarTreeSection.categoryInTree(SimpleSubCategory.name)}}" stepKey="clickCategoryLink"/> -+ <waitForPageLoad stepKey="waitForCategoryPageLoad"/> -+ -+ <!--Content > Add CMS Block: name saved block--> -+ <waitForElementVisible selector="{{AdminCategoryContentSection.sectionHeader}}" stepKey="waitForContentSection"/> -+ <conditionalClick selector="{{AdminCategoryContentSection.sectionHeader}}" dependentSelector="{{AdminCategoryContentSection.uploadButton}}" visible="false" stepKey="openContentSection"/> -+ <waitForPageLoad stepKey="waitForContentLoad"/> -+ -+ <selectOption selector="{{AdminCategoryContentSection.AddCMSBlock}}" stepKey="selectSavedBlock" userInput="{{_defaultBlock.title}}"/> -+ -+ <!--Display Settings > Display Mode: Static block only--> -+ <waitForElementVisible selector="{{AdminCategoryDisplaySettingsSection.settingsHeader}}" stepKey="waitForDisplaySettingsSection"/> -+ <conditionalClick selector="{{AdminCategoryDisplaySettingsSection.settingsHeader}}" dependentSelector="{{AdminCategoryDisplaySettingsSection.displayMode}}" visible="false" stepKey="openDisplaySettingsSection"/> -+ <waitForPageLoad stepKey="waitForDisplaySettingsLoad"/> -+ <selectOption stepKey="selectStaticBlockOnlyOption" userInput="Static block only" selector="{{AdminCategoryDisplaySettingsSection.displayMode}}"/> -+ <click selector="{{AdminCategoryMainActionsSection.SaveButton}}" stepKey="saveCategoryWithProducts"/> -+ <waitForPageLoad stepKey="waitForCategorySaved"/> -+ <see userInput="You saved the category." stepKey="seeSuccessMessage"/> -+ -+ <!--Go to Storefront > category--> -+ <amOnPage url="$$simplecategory.name$$.html" stepKey="goToStorefrontCategoryPage"/> -+ <waitForPageLoad stepKey="waitForStorefrontPageLoaded"/> -+ -+ <!--Check operators Greater than--> -+ <dontSeeElement selector="{{InsertWidgetSection.checkElementStorefrontByPrice('10')}}" stepKey="dontSeeElementByPrice20"/> -+ <seeElement selector="{{InsertWidgetSection.checkElementStorefrontByPrice('50')}}" stepKey="seeElementByPrice50"/> -+ <seeElement selector="{{InsertWidgetSection.checkElementStorefrontByPrice('100')}}" stepKey="seeElementByPrice100"/> -+ -+ <!--Open block with widget.--> -+ <actionGroup ref="navigateToCreatedCMSBlockPage" stepKey="navigateToCreatedCMSBlockPage2"> -+ <argument name="CMSBlockPage" value="$$createPreReqBlock$$"/> -+ </actionGroup> -+ -+ <actionGroup ref="AdminCreateBlockWithWidget" stepKey="adminCreateBlockWithWidgetLessThan"> -+ <argument name="addCondition" value="Price"/> -+ <argument name="isCondition" value="less than"/> -+ <argument name="fieldCondition" value="20"/> -+ </actionGroup> -+ -+ <!--Go to Storefront > category--> -+ <amOnPage url="$$simplecategory.name$$.html" stepKey="goToStorefrontCategoryPage2"/> -+ <waitForPageLoad stepKey="waitForStorefrontPageLoaded2"/> -+ -+ <!--Check operators Greater than--> -+ <seeElement selector="{{InsertWidgetSection.checkElementStorefrontByPrice('10')}}" stepKey="seeElementByPrice20"/> -+ <dontSeeElement selector="{{InsertWidgetSection.checkElementStorefrontByPrice('50')}}" stepKey="dontSeeElementByPrice50"/> -+ <dontSeeElement selector="{{InsertWidgetSection.checkElementStorefrontByPrice('100')}}" stepKey="dontSeeElementByPrice100"/> -+ -+ <!--Open block with widget.--> -+ <actionGroup ref="navigateToCreatedCMSBlockPage" stepKey="navigateToCreatedCMSBlockPage3"> -+ <argument name="CMSBlockPage" value="$$createPreReqBlock$$"/> -+ </actionGroup> -+ -+ <actionGroup ref="AdminCreateBlockWithWidget" stepKey="adminCreateBlockWithWidgetEqualsOrGreaterThan"> -+ <argument name="addCondition" value="Price"/> -+ <argument name="isCondition" value="equals or greater than"/> -+ <argument name="fieldCondition" value="50"/> -+ </actionGroup> -+ -+ <!--Go to Storefront > category--> -+ <amOnPage url="$$simplecategory.name$$.html" stepKey="goToStorefrontCategoryPage3"/> -+ <waitForPageLoad stepKey="waitForStorefrontPageLoaded3"/> -+ -+ <!--Check operators Greater than--> -+ <dontSeeElement selector="{{InsertWidgetSection.checkElementStorefrontByPrice('10')}}" stepKey="dontSeeElementByPrice20s"/> -+ <seeElement selector="{{InsertWidgetSection.checkElementStorefrontByPrice('50')}}" stepKey="seeElementByPrice50s"/> -+ <seeElement selector="{{InsertWidgetSection.checkElementStorefrontByPrice('100')}}" stepKey="seeElementByPrice100s"/> -+ -+ <!--Open block with widget.--> -+ <actionGroup ref="navigateToCreatedCMSBlockPage" stepKey="navigateToCreatedCMSBlockPage4"> -+ <argument name="CMSBlockPage" value="$$createPreReqBlock$$"/> -+ </actionGroup> -+ -+ <actionGroup ref="AdminCreateBlockWithWidget" stepKey="adminCreateBlockWithWidgetEqualsOrLessThan"> -+ <argument name="addCondition" value="Price"/> -+ <argument name="isCondition" value="equals or less than"/> -+ <argument name="fieldCondition" value="50"/> -+ </actionGroup> -+ -+ <!--Go to Storefront > category--> -+ <amOnPage url="$$simplecategory.name$$.html" stepKey="goToStorefrontCategoryPage4"/> -+ <waitForPageLoad stepKey="waitForStorefrontPageLoaded4"/> -+ -+ <!--Check operators Greater than--> -+ <seeElement selector="{{InsertWidgetSection.checkElementStorefrontByPrice('10')}}" stepKey="seeElementByPrice20s"/> -+ <seeElement selector="{{InsertWidgetSection.checkElementStorefrontByPrice('50')}}" stepKey="seeElementByPrice50t"/> -+ <dontSeeElement selector="{{InsertWidgetSection.checkElementStorefrontByPrice('100')}}" stepKey="dontSeeElementByPrice100s"/> -+ -+ <after> -+ <actionGroup ref="DisabledWYSIWYG" stepKey="disableWYSIWYG"/> -+ <deleteData createDataKey="createPreReqBlock" stepKey="deletePreReqBlock" /> -+ <deleteData createDataKey="simplecategory" stepKey="deleteSimpleCategory"/> -+ <deleteData createDataKey="createFirstProduct" stepKey="deleteFirstProduct"/> -+ <deleteData createDataKey="createSecondProduct" stepKey="deleteSecondProduct"/> -+ <deleteData createDataKey="createThirdProduct" stepKey="deleteThirdProduct"/> -+ <actionGroup ref="logout" stepKey="logout"/> -+ </after> -+ </test> -+</tests> -diff --git a/app/code/Magento/CatalogWidget/Test/Mftf/Test/CatalogProductListWidgetOrderTest.xml b/app/code/Magento/CatalogWidget/Test/Mftf/Test/CatalogProductListWidgetOrderTest.xml -new file mode 100644 -index 00000000000..11586207c4d ---- /dev/null -+++ b/app/code/Magento/CatalogWidget/Test/Mftf/Test/CatalogProductListWidgetOrderTest.xml -@@ -0,0 +1,88 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> -+ <test name="CatalogProductListWidgetOrderTest"> -+ <annotations> -+ <features value="CatalogWidget"/> -+ <stories value="MC-5905: Wrong sorting on Products component"/> -+ <title value="Checking order of products in the 'catalog Products List' widget"/> -+ <description value="Check that products are ordered with recently added products first"/> -+ <severity value="MAJOR"/> -+ <testCaseId value="MC-13794"/> -+ <group value="CatalogWidget"/> -+ <group value="WYSIWYGDisabled"/> -+ <skip> -+ <issueId value="MC-13923"/> -+ </skip> -+ </annotations> -+ <before> -+ <createData entity="SimpleSubCategory" stepKey="simplecategory"/> -+ <createData entity="SimpleProduct" stepKey="createFirstProduct"> -+ <requiredEntity createDataKey="simplecategory"/> -+ <field key="price">10</field> -+ </createData> -+ <createData entity="SimpleProduct" stepKey="createSecondProduct"> -+ <requiredEntity createDataKey="simplecategory"/> -+ <field key="price">20</field> -+ </createData> -+ <createData entity="SimpleProduct" stepKey="createThirdProduct"> -+ <requiredEntity createDataKey="simplecategory"/> -+ <field key="price">30</field> -+ </createData> -+ <createData entity="_defaultCmsPage" stepKey="createPreReqPage"/> -+ <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> -+ <actionGroup ref="EnabledWYSIWYG" stepKey="enableWYSIWYG"/> -+ </before> -+ <!--Open created cms page--> -+ <comment userInput="Open created cms page" stepKey="commentOpenCreatedCmsPage"/> -+ <actionGroup ref="navigateToCreatedCMSPage" stepKey="navigateToCreatedCMSPage1"> -+ <argument name="CMSPage" value="$$createPreReqPage$$"/> -+ </actionGroup> -+ <!--Add widget to cms page--> -+ <comment userInput="Add widget to cms page" stepKey="commentAddWidgetToCmsPage"/> -+ <click selector="{{TinyMCESection.InsertWidgetIcon}}" stepKey="clickInsertWidgetIcon" /> -+ <waitForPageLoad stepKey="waitForPageLoad1" /> -+ <selectOption selector="{{WidgetSection.WidgetType}}" userInput="Catalog Products List" stepKey="selectCatalogProductsList" /> -+ <waitForLoadingMaskToDisappear stepKey="waitForLoadingMaskDisappear1" /> -+ <click selector="{{WidgetSection.AddParam}}" stepKey="clickAddParamBtn" /> -+ <waitForElementVisible selector="{{WidgetSection.ConditionsDropdown}}" stepKey="waitForDropdownVisible"/> -+ <selectOption selector="{{WidgetSection.ConditionsDropdown}}" userInput="Category" stepKey="selectCategoryCondition" /> -+ <waitForLoadingMaskToDisappear stepKey="waitForLoadingMaskDisappear2" /> -+ <click selector="{{WidgetSection.RuleParam}}" stepKey="clickRuleParam" /> -+ <waitForElementVisible selector="{{WidgetSection.Chooser}}" stepKey="waitForElement" /> -+ <click selector="{{WidgetSection.Chooser}}" stepKey="clickChooser" /> -+ <waitForLoadingMaskToDisappear stepKey="waitForLoadingMaskDisappear3" /> -+ <click selector="{{WidgetSection.PreCreateCategory('$$simplecategory.name$$')}}" stepKey="selectCategory" /> -+ <click selector="{{WidgetSection.InsertWidget}}" stepKey="clickInsertWidget" /> -+ <waitForPageLoad stepKey="waitForPageLoad2" /> -+ <!--Save cms page and go to Storefront--> -+ <comment userInput="Save cms page and go to Storefront" stepKey="commentSaveCmsPageAndGoToStorefront"/> -+ <click selector="{{CmsNewPagePageActionsSection.expandSplitButton}}" stepKey="expandButtonMenu"/> -+ <waitForElementVisible selector="{{CmsNewPagePageActionsSection.splitButtonMenu}}" stepKey="waitForSplitButtonMenuVisible"/> -+ <click selector="{{CmsNewPagePageActionsSection.savePage}}" stepKey="clickSavePage"/> -+ <see userInput="You saved the page." stepKey="seeSuccessMessage"/> -+ <amOnPage url="$$createPreReqPage.identifier$$" stepKey="amOnPageTestPage"/> -+ <waitForPageLoad stepKey="waitForPageLoad3" /> -+ <!--Check order of products: recently added first--> -+ <comment userInput="Check order of products: recently added first" stepKey="commentCheckOrderOfProductsRecentlyAddedFirst"/> -+ <seeElement selector="{{InsertWidgetSection.checkElementStorefrontByName('1','$$createThirdProduct.name$$')}}" stepKey="seeElementByName1"/> -+ <seeElement selector="{{InsertWidgetSection.checkElementStorefrontByName('2','$$createSecondProduct.name$$')}}" stepKey="seeElementByName2"/> -+ <seeElement selector="{{InsertWidgetSection.checkElementStorefrontByName('3','$$createFirstProduct.name$$')}}" stepKey="seeElementByName3"/> -+ <after> -+ <actionGroup ref="DisabledWYSIWYG" stepKey="disableWYSIWYG"/> -+ <deleteData createDataKey="createPreReqPage" stepKey="deletePreReqPage" /> -+ <deleteData createDataKey="simplecategory" stepKey="deleteSimpleCategory"/> -+ <deleteData createDataKey="createFirstProduct" stepKey="deleteFirstProduct"/> -+ <deleteData createDataKey="createSecondProduct" stepKey="deleteSecondProduct"/> -+ <deleteData createDataKey="createThirdProduct" stepKey="deleteThirdProduct"/> -+ <actionGroup ref="logout" stepKey="logout"/> -+ </after> -+ </test> -+</tests> -\ No newline at end of file -diff --git a/app/code/Magento/CatalogWidget/Test/Unit/Block/Product/ProductsListTest.php b/app/code/Magento/CatalogWidget/Test/Unit/Block/Product/ProductsListTest.php -index 5de8b9d9632..a7897537957 100644 ---- a/app/code/Magento/CatalogWidget/Test/Unit/Block/Product/ProductsListTest.php -+++ b/app/code/Magento/CatalogWidget/Test/Unit/Block/Product/ProductsListTest.php -@@ -167,6 +167,7 @@ class ProductsListTest extends \PHPUnit\Framework\TestCase - 'context_group', - 1, - 5, -+ 10, - 'some_serialized_conditions', - json_encode('request_params'), - 'test_template', -@@ -274,6 +275,7 @@ class ProductsListTest extends \PHPUnit\Framework\TestCase - 'addAttributeToSelect', - 'addUrlRewrite', - 'addStoreFilter', -+ 'addAttributeToSort', - 'setPageSize', - 'setCurPage', - 'distinct' -@@ -288,6 +290,7 @@ class ProductsListTest extends \PHPUnit\Framework\TestCase - $collection->expects($this->once())->method('addAttributeToSelect')->willReturnSelf(); - $collection->expects($this->once())->method('addUrlRewrite')->willReturnSelf(); - $collection->expects($this->once())->method('addStoreFilter')->willReturnSelf(); -+ $collection->expects($this->once())->method('addAttributeToSort')->with('created_at', 'desc')->willReturnSelf(); - $collection->expects($this->once())->method('setPageSize')->with($expectedPageSize)->willReturnSelf(); - $collection->expects($this->once())->method('setCurPage')->willReturnSelf(); - $collection->expects($this->once())->method('distinct')->willReturnSelf(); -diff --git a/app/code/Magento/CatalogWidget/view/adminhtml/templates/product/widget/conditions.phtml b/app/code/Magento/CatalogWidget/view/adminhtml/templates/product/widget/conditions.phtml -index d40cc3dc6d9..0e21f9e42c9 100644 ---- a/app/code/Magento/CatalogWidget/view/adminhtml/templates/product/widget/conditions.phtml -+++ b/app/code/Magento/CatalogWidget/view/adminhtml/templates/product/widget/conditions.phtml -@@ -4,8 +4,6 @@ - * See COPYING.txt for license details. - */ - --// @codingStandardsIgnoreFile -- - /** @var \Magento\CatalogWidget\Block\Product\Widget\Conditions $block */ - - $element = $block->getElement(); -diff --git a/app/code/Magento/CatalogWidget/view/frontend/layout/catalog_widget_product_list.xml b/app/code/Magento/CatalogWidget/view/frontend/layout/catalog_widget_product_list.xml -new file mode 100644 -index 00000000000..db44d8b62dc ---- /dev/null -+++ b/app/code/Magento/CatalogWidget/view/frontend/layout/catalog_widget_product_list.xml -@@ -0,0 +1,17 @@ -+<!-- -+ ~ Copyright © Magento, Inc. All rights reserved. -+ ~ See COPYING.txt for license details. -+ --> -+ -+<!-- -+ ~ Copyright © Magento, Inc. All rights reserved. -+ ~ See COPYING.txt for license details. -+ --> -+ -+<page xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:View/Layout/etc/page_configuration.xsd"> -+ <body> -+ <block class="Magento\Framework\View\Element\RendererList" name="category.product.type.widget.details.renderers"> -+ <block class="Magento\Framework\View\Element\Template" name="category.product.type.details.renderers.default" as="default"/> -+ </block> -+ </body> -+</page> -\ No newline at end of file -diff --git a/app/code/Magento/CatalogWidget/view/frontend/templates/product/widget/content/grid.phtml b/app/code/Magento/CatalogWidget/view/frontend/templates/product/widget/content/grid.phtml -index f2273f7d44f..881d8b28dfa 100644 ---- a/app/code/Magento/CatalogWidget/view/frontend/templates/product/widget/content/grid.phtml -+++ b/app/code/Magento/CatalogWidget/view/frontend/templates/product/widget/content/grid.phtml -@@ -3,13 +3,12 @@ - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -- --// @codingStandardsIgnoreFile -+use Magento\Framework\App\Action\Action; - - /** @var \Magento\CatalogWidget\Block\Product\ProductsList $block */ - ?> --<?php if ($exist = ($block->getProductCollection() && $block->getProductCollection()->getSize())): ?> --<?php -+<?php if ($exist = ($block->getProductCollection() && $block->getProductCollection()->getSize())) : ?> -+ <?php - $type = 'widget-product-grid'; - - $mode = 'grid'; -@@ -20,21 +19,21 @@ - $showWishlist = true; - $showCompare = true; - $showCart = true; -- $templateType = \Magento\Catalog\Block\Product\ReviewRendererInterface::DEFAULT_VIEW; -+ $templateType = \Magento\Catalog\Block\Product\ReviewRendererInterface::SHORT_VIEW; - $description = false; --?> -+ ?> - <div class="block widget block-products-list <?= /* @noEscape */ $mode ?>"> -- <?php if ($block->getTitle()): ?> -- <div class="block-title"> -- <strong><?= $block->escapeHtml(__($block->getTitle())) ?></strong> -- </div> -+ <?php if ($block->getTitle()) : ?> -+ <div class="block-title"> -+ <strong><?= $block->escapeHtml(__($block->getTitle())) ?></strong> -+ </div> - <?php endif ?> - <div class="block-content"> - <?= /* @noEscape */ '<!-- ' . $image . '-->' ?> - <div class="products-<?= /* @noEscape */ $mode ?> <?= /* @noEscape */ $mode ?>"> - <ol class="product-items <?= /* @noEscape */ $type ?>"> - <?php $iterator = 1; ?> -- <?php foreach ($items as $_item): ?> -+ <?php foreach ($items as $_item) : ?> - <?= /* @noEscape */ ($iterator++ == 1) ? '<li class="product-item">' : '</li><li class="product-item">' ?> - <div class="product-item-info"> - <a href="<?= $block->escapeUrl($block->getProductUrl($_item)) ?>" class="product-item-photo"> -@@ -48,57 +47,57 @@ - <?= $block->escapeHtml($_item->getName()) ?> - </a> - </strong> -- <?php -- echo $block->getProductPriceHtml($_item, $type); -- ?> -- -- <?php if ($templateType): ?> -+ <?php if ($templateType) : ?> - <?= $block->getReviewsSummaryHtml($_item, $templateType) ?> - <?php endif; ?> - -- <?php if ($showWishlist || $showCompare || $showCart): ?> -- <div class="product-item-actions"> -- <?php if ($showCart): ?> -- <div class="actions-primary"> -- <?php if ($_item->isSaleable()): ?> -- <?php if ($_item->getTypeInstance()->hasRequiredOptions($_item)): ?> -- <button class="action tocart primary" data-mage-init='{"redirectUrl":{"url":"<?= $block->escapeUrl($block->getAddToCartUrl($_item)) ?>"}}' type="button" title="<?= $block->escapeHtmlAttr(__('Add to Cart')) ?>"> -- <span><?= $block->escapeHtml(__('Add to Cart')) ?></span> -- </button> -- <?php else: ?> -- <?php -- $postDataHelper = $this->helper('Magento\Framework\Data\Helper\PostHelper'); -- $postData = $postDataHelper->getPostData($block->getAddToCartUrl($_item), ['product' => $_item->getEntityId()]) -- ?> -- <button class="action tocart primary" data-post='<?= /* @noEscape */ $postData ?>' type="button" title="<?= $block->escapeHtmlAttr(__('Add to Cart')) ?>"> -- <span><?= $block->escapeHtml(__('Add to Cart')) ?></span> -- </button> -+ <?= $block->getProductPriceHtml($_item, $type) ?> -+ -+ <?= $block->getProductDetailsHtml($_item) ?> -+ -+ <?php if ($showWishlist || $showCompare || $showCart) : ?> -+ <div class="product-item-inner"> -+ <div class="product-item-actions"> -+ <?php if ($showCart) : ?> -+ <div class="actions-primary"> -+ <?php if ($_item->isSaleable()) : ?> -+ <?php $postParams = $block->getAddToCartPostParams($_item); ?> -+ <form data-role="tocart-form" data-product-sku="<?= $block->escapeHtml($_item->getSku()) ?>" action="<?= $block->escapeUrl($postParams['action']) ?>" method="post"> -+ <input type="hidden" name="product" value="<?= $block->escapeHtmlAttr($postParams['data']['product']) ?>"> -+ <input type="hidden" name="<?= /* @noEscape */ Action::PARAM_NAME_URL_ENCODED ?>" value="<?= /* @noEscape */ $postParams['data'][Action::PARAM_NAME_URL_ENCODED] ?>"> -+ <?= $block->getBlockHtml('formkey') ?> -+ <button type="submit" -+ title="<?= $block->escapeHtml(__('Add to Cart')) ?>" -+ class="action tocart primary"> -+ <span><?= $block->escapeHtml(__('Add to Cart')) ?></span> -+ </button> -+ </form> -+ <?php else : ?> -+ <?php if ($_item->getIsSalable()) : ?> -+ <div class="stock available"><span><?= $block->escapeHtml(__('In stock')) ?></span></div> -+ <?php else : ?> -+ <div class="stock unavailable"><span><?= $block->escapeHtml(__('Out of stock')) ?></span></div> -+ <?php endif; ?> -+ <?php endif; ?> -+ </div> -+ <?php endif; ?> -+ <?php if ($showWishlist || $showCompare) : ?> -+ <div class="actions-secondary" data-role="add-to-links"> -+ <?php if ($this->helper(\Magento\Wishlist\Helper\Data::class)->isAllow() && $showWishlist) : ?> -+ <a href="#" -+ data-post='<?= /* @noEscape */ $block->getAddToWishlistParams($_item) ?>' class="action towishlist" data-action="add-to-wishlist" title="<?= $block->escapeHtmlAttr(__('Add to Wish List')) ?>"> -+ <span><?= $block->escapeHtml(__('Add to Wish List')) ?></span> -+ </a> - <?php endif; ?> -- <?php else: ?> -- <?php if ($_item->getIsSalable()): ?> -- <div class="stock available"><span><?= $block->escapeHtml(__('In stock')) ?></span></div> -- <?php else: ?> -- <div class="stock unavailable"><span><?= $block->escapeHtml(__('Out of stock')) ?></span></div> -+ <?php if ($block->getAddToCompareUrl() && $showCompare) : ?> -+ <?php $compareHelper = $this->helper(\Magento\Catalog\Helper\Product\Compare::class);?> -+ <a href="#" class="action tocompare" data-post='<?= /* @noEscape */ $compareHelper->getPostDataParams($_item) ?>' title="<?= $block->escapeHtmlAttr(__('Add to Compare')) ?>"> -+ <span><?= $block->escapeHtml(__('Add to Compare')) ?></span> -+ </a> - <?php endif; ?> -- <?php endif; ?> -- </div> -- <?php endif; ?> -- <?php if ($showWishlist || $showCompare): ?> -- <div class="actions-secondary" data-role="add-to-links"> -- <?php if ($this->helper('Magento\Wishlist\Helper\Data')->isAllow() && $showWishlist): ?> -- <a href="#" -- data-post='<?= /* @noEscape */ $block->getAddToWishlistParams($_item) ?>' class="action towishlist" data-action="add-to-wishlist" title="<?= $block->escapeHtmlAttr(__('Add to Wish List')) ?>"> -- <span><?= $block->escapeHtml(__('Add to Wish List')) ?></span> -- </a> -- <?php endif; ?> -- <?php if ($block->getAddToCompareUrl() && $showCompare): ?> -- <?php $compareHelper = $this->helper('Magento\Catalog\Helper\Product\Compare');?> -- <a href="#" class="action tocompare" data-post='<?= /* @noEscape */ $compareHelper->getPostDataParams($_item) ?>' title="<?= $block->escapeHtmlAttr(__('Add to Compare')) ?>"> -- <span><?= $block->escapeHtml(__('Add to Compare')) ?></span> -- </a> -- <?php endif; ?> -- </div> -- <?php endif; ?> -+ </div> -+ <?php endif; ?> -+ </div> - </div> - <?php endif; ?> - </div> -diff --git a/app/code/Magento/Checkout/Block/Cart/Item/Renderer.php b/app/code/Magento/Checkout/Block/Cart/Item/Renderer.php -index a43f074d8df..4941bf8451b 100644 ---- a/app/code/Magento/Checkout/Block/Cart/Item/Renderer.php -+++ b/app/code/Magento/Checkout/Block/Cart/Item/Renderer.php -@@ -85,7 +85,7 @@ class Renderer extends \Magento\Framework\View\Element\Template implements - protected $priceCurrency; - - /** -- * @var \Magento\Framework\Module\Manager -+ * @var \Magento\Framework\Module\ModuleManagerInterface - */ - public $moduleManager; - -@@ -105,7 +105,7 @@ class Renderer extends \Magento\Framework\View\Element\Template implements - * @param \Magento\Framework\Url\Helper\Data $urlHelper - * @param \Magento\Framework\Message\ManagerInterface $messageManager - * @param PriceCurrencyInterface $priceCurrency -- * @param \Magento\Framework\Module\Manager $moduleManager -+ * @param \Magento\Framework\Module\ModuleManagerInterface $moduleManager - * @param InterpretationStrategyInterface $messageInterpretationStrategy - * @param array $data - * @param ItemResolverInterface|null $itemResolver -@@ -120,7 +120,7 @@ class Renderer extends \Magento\Framework\View\Element\Template implements - \Magento\Framework\Url\Helper\Data $urlHelper, - \Magento\Framework\Message\ManagerInterface $messageManager, - PriceCurrencyInterface $priceCurrency, -- \Magento\Framework\Module\Manager $moduleManager, -+ \Magento\Framework\Module\ModuleManagerInterface $moduleManager, - InterpretationStrategyInterface $messageInterpretationStrategy, - array $data = [], - ItemResolverInterface $itemResolver = null -@@ -185,6 +185,8 @@ class Renderer extends \Magento\Framework\View\Element\Template implements - } - - /** -+ * Override product url. -+ * - * @param string $productUrl - * @return $this - * @codeCoverageIgnore -@@ -313,11 +315,7 @@ class Renderer extends \Magento\Framework\View\Element\Template implements - } - - /** -- * Retrieve item messages -- * Return array with keys -- * -- * text => the message text -- * type => type of a message -+ * Retrieve item messages, return array with keys, text => the message text, type => type of a message - * - * @return array - */ -@@ -472,6 +470,8 @@ class Renderer extends \Magento\Framework\View\Element\Template implements - } - - /** -+ * Get price renderer. -+ * - * @return \Magento\Framework\Pricing\Render - * @codeCoverageIgnore - */ -diff --git a/app/code/Magento/Checkout/Block/Cart/Link.php b/app/code/Magento/Checkout/Block/Cart/Link.php -index abc1ce4e310..6ea51375211 100644 ---- a/app/code/Magento/Checkout/Block/Cart/Link.php -+++ b/app/code/Magento/Checkout/Block/Cart/Link.php -@@ -13,7 +13,7 @@ namespace Magento\Checkout\Block\Cart; - class Link extends \Magento\Framework\View\Element\Html\Link - { - /** -- * @var \Magento\Framework\Module\Manager -+ * @var \Magento\Framework\Module\ModuleManagerInterface - */ - protected $_moduleManager; - -@@ -24,14 +24,14 @@ class Link extends \Magento\Framework\View\Element\Html\Link - - /** - * @param \Magento\Framework\View\Element\Template\Context $context -- * @param \Magento\Framework\Module\Manager $moduleManager -+ * @param \Magento\Framework\Module\ModuleManagerInterface $moduleManager - * @param \Magento\Checkout\Helper\Cart $cartHelper - * @param array $data - * @codeCoverageIgnore - */ - public function __construct( - \Magento\Framework\View\Element\Template\Context $context, -- \Magento\Framework\Module\Manager $moduleManager, -+ \Magento\Framework\Module\ModuleManagerInterface $moduleManager, - \Magento\Checkout\Helper\Cart $cartHelper, - array $data = [] - ) { -@@ -41,6 +41,8 @@ class Link extends \Magento\Framework\View\Element\Html\Link - } - - /** -+ * Get label. -+ * - * @return string - * @codeCoverageIgnore - */ -@@ -50,6 +52,8 @@ class Link extends \Magento\Framework\View\Element\Html\Link - } - - /** -+ * Get href. -+ * - * @return string - * @codeCoverageIgnore - */ -diff --git a/app/code/Magento/Checkout/Block/Cart/Sidebar.php b/app/code/Magento/Checkout/Block/Cart/Sidebar.php -index 92ba6bf2bbb..c5e309df3ca 100644 ---- a/app/code/Magento/Checkout/Block/Cart/Sidebar.php -+++ b/app/code/Magento/Checkout/Block/Cart/Sidebar.php -@@ -82,11 +82,14 @@ class Sidebar extends AbstractCart - 'baseUrl' => $this->getBaseUrl(), - 'minicartMaxItemsVisible' => $this->getMiniCartMaxItemsCount(), - 'websiteId' => $this->_storeManager->getStore()->getWebsiteId(), -- 'maxItemsToDisplay' => $this->getMaxItemsToDisplay() -+ 'maxItemsToDisplay' => $this->getMaxItemsToDisplay(), -+ 'storeId' => $this->_storeManager->getStore()->getId() - ]; - } - - /** -+ * Get serialized config -+ * - * @return string - * @since 100.2.0 - */ -@@ -96,6 +99,8 @@ class Sidebar extends AbstractCart - } - - /** -+ * Get image html template -+ * - * @return string - */ - public function getImageHtmlTemplate() -@@ -130,6 +135,7 @@ class Sidebar extends AbstractCart - * - * @return string - * @codeCoverageIgnore -+ * @SuppressWarnings(PHPMD.RequestAwareBlockMethod) - */ - public function getUpdateItemQtyUrl() - { -@@ -141,6 +147,7 @@ class Sidebar extends AbstractCart - * - * @return string - * @codeCoverageIgnore -+ * @SuppressWarnings(PHPMD.RequestAwareBlockMethod) - */ - public function getRemoveItemUrl() - { -@@ -210,6 +217,7 @@ class Sidebar extends AbstractCart - - /** - * Returns maximum cart items to display -+ * - * This setting regulates how many items will be displayed in minicart - * - * @return int -diff --git a/app/code/Magento/Checkout/Block/Cart/Totals.php b/app/code/Magento/Checkout/Block/Cart/Totals.php -index 375c564f290..131e5b157c7 100644 ---- a/app/code/Magento/Checkout/Block/Cart/Totals.php -+++ b/app/code/Magento/Checkout/Block/Cart/Totals.php -@@ -3,12 +3,16 @@ - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -+ - namespace Magento\Checkout\Block\Cart; - - use Magento\Framework\View\Element\BlockInterface; - use Magento\Checkout\Block\Checkout\LayoutProcessorInterface; -+use Magento\Sales\Model\ConfigInterface; - - /** -+ * Totals cart block. -+ * - * @api - */ - class Totals extends \Magento\Checkout\Block\Cart\AbstractCart -@@ -42,7 +46,7 @@ class Totals extends \Magento\Checkout\Block\Cart\AbstractCart - * @param \Magento\Framework\View\Element\Template\Context $context - * @param \Magento\Customer\Model\Session $customerSession - * @param \Magento\Checkout\Model\Session $checkoutSession -- * @param \Magento\Sales\Model\Config $salesConfig -+ * @param ConfigInterface $salesConfig - * @param array $layoutProcessors - * @param array $data - * @codeCoverageIgnore -@@ -51,7 +55,7 @@ class Totals extends \Magento\Checkout\Block\Cart\AbstractCart - \Magento\Framework\View\Element\Template\Context $context, - \Magento\Customer\Model\Session $customerSession, - \Magento\Checkout\Model\Session $checkoutSession, -- \Magento\Sales\Model\Config $salesConfig, -+ ConfigInterface $salesConfig, - array $layoutProcessors = [], - array $data = [] - ) { -@@ -62,6 +66,8 @@ class Totals extends \Magento\Checkout\Block\Cart\AbstractCart - } - - /** -+ * Retrieve encoded js layout. -+ * - * @return string - */ - public function getJsLayout() -@@ -74,6 +80,8 @@ class Totals extends \Magento\Checkout\Block\Cart\AbstractCart - } - - /** -+ * Retrieve totals from cache. -+ * - * @return array - */ - public function getTotals() -@@ -85,6 +93,8 @@ class Totals extends \Magento\Checkout\Block\Cart\AbstractCart - } - - /** -+ * Set totals to cache. -+ * - * @param array $value - * @return $this - * @codeCoverageIgnore -@@ -96,6 +106,8 @@ class Totals extends \Magento\Checkout\Block\Cart\AbstractCart - } - - /** -+ * Create totals block and set totals. -+ * - * @param string $code - * @return BlockInterface - */ -@@ -121,6 +133,8 @@ class Totals extends \Magento\Checkout\Block\Cart\AbstractCart - } - - /** -+ * Get totals html. -+ * - * @param mixed $total - * @param int|null $area - * @param int $colspan -@@ -177,7 +191,7 @@ class Totals extends \Magento\Checkout\Block\Cart\AbstractCart - } - - /** -- * Get formated in base currency base grand total value -+ * Get formatted in base currency base grand total value - * - * @return string - */ -diff --git a/app/code/Magento/Checkout/Block/Checkout/AttributeMerger.php b/app/code/Magento/Checkout/Block/Checkout/AttributeMerger.php -index de996bed024..5dedf2c7e7e 100644 ---- a/app/code/Magento/Checkout/Block/Checkout/AttributeMerger.php -+++ b/app/code/Magento/Checkout/Block/Checkout/AttributeMerger.php -@@ -6,10 +6,18 @@ - namespace Magento\Checkout\Block\Checkout; - - use Magento\Customer\Api\CustomerRepositoryInterface as CustomerRepository; -+use Magento\Customer\Api\Data\CustomerInterface; - use Magento\Customer\Helper\Address as AddressHelper; - use Magento\Customer\Model\Session; - use Magento\Directory\Helper\Data as DirectoryHelper; -+use Magento\Framework\Exception\LocalizedException; -+use Magento\Framework\Exception\NoSuchEntityException; - -+/** -+ * Fields attribute merger. -+ * -+ * @SuppressWarnings(PHPMD.CookieAndSessionMisuse) -+ */ - class AttributeMerger - { - /** -@@ -46,6 +54,7 @@ class AttributeMerger - 'alpha' => 'validate-alpha', - 'numeric' => 'validate-number', - 'alphanumeric' => 'validate-alphanum', -+ 'alphanum-with-spaces' => 'validate-alphanum-with-spaces', - 'url' => 'validate-url', - 'email' => 'email2', - 'length' => 'validate-length', -@@ -67,7 +76,7 @@ class AttributeMerger - private $customerRepository; - - /** -- * @var \Magento\Customer\Api\Data\CustomerInterface -+ * @var CustomerInterface - */ - private $customer; - -@@ -269,6 +278,7 @@ class AttributeMerger - for ($lineIndex = 0; $lineIndex < (int)$attributeConfig['size']; $lineIndex++) { - $isFirstLine = $lineIndex === 0; - $line = [ -+ 'label' => __("%1: Line %2", $attributeConfig['label'], $lineIndex + 1), - 'component' => 'Magento_Ui/js/form/element/abstract', - 'config' => [ - // customScope is used to group elements within a single form e.g. they can be validated separately -@@ -309,10 +319,14 @@ class AttributeMerger - } - - /** -+ * Returns default attribute value. -+ * - * @param string $attributeCode -+ * @throws NoSuchEntityException -+ * @throws LocalizedException - * @return null|string - */ -- protected function getDefaultValue($attributeCode) -+ protected function getDefaultValue($attributeCode): ?string - { - if ($attributeCode === 'country_id') { - return $this->directoryHelper->getDefaultCountry(); -@@ -346,9 +360,13 @@ class AttributeMerger - } - - /** -- * @return \Magento\Customer\Api\Data\CustomerInterface|null -+ * Returns logged customer. -+ * -+ * @throws NoSuchEntityException -+ * @throws LocalizedException -+ * @return CustomerInterface|null - */ -- protected function getCustomer() -+ protected function getCustomer(): ?CustomerInterface - { - if (!$this->customer) { - if ($this->customerSession->isLoggedIn()) { -diff --git a/app/code/Magento/Checkout/Block/Checkout/LayoutProcessor.php b/app/code/Magento/Checkout/Block/Checkout/LayoutProcessor.php -index f47e514948d..557f1433524 100644 ---- a/app/code/Magento/Checkout/Block/Checkout/LayoutProcessor.php -+++ b/app/code/Magento/Checkout/Block/Checkout/LayoutProcessor.php -@@ -7,7 +7,7 @@ namespace Magento\Checkout\Block\Checkout; - - use Magento\Checkout\Helper\Data; - use Magento\Framework\App\ObjectManager; --use Magento\Store\Api\StoreResolverInterface; -+use Magento\Store\Model\StoreManagerInterface; - - /** - * Class LayoutProcessor -@@ -40,9 +40,9 @@ class LayoutProcessor implements \Magento\Checkout\Block\Checkout\LayoutProcesso - private $checkoutDataHelper; - - /** -- * @var StoreResolverInterface -+ * @var StoreManagerInterface - */ -- private $storeResolver; -+ private $storeManager; - - /** - * @var \Magento\Shipping\Model\Config -@@ -53,30 +53,36 @@ class LayoutProcessor implements \Magento\Checkout\Block\Checkout\LayoutProcesso - * @param \Magento\Customer\Model\AttributeMetadataDataProvider $attributeMetadataDataProvider - * @param \Magento\Ui\Component\Form\AttributeMapper $attributeMapper - * @param AttributeMerger $merger -+ * @param \Magento\Customer\Model\Options|null $options -+ * @param Data|null $checkoutDataHelper -+ * @param \Magento\Shipping\Model\Config|null $shippingConfig -+ * @param StoreManagerInterface|null $storeManager - */ - public function __construct( - \Magento\Customer\Model\AttributeMetadataDataProvider $attributeMetadataDataProvider, - \Magento\Ui\Component\Form\AttributeMapper $attributeMapper, -- AttributeMerger $merger -+ AttributeMerger $merger, -+ \Magento\Customer\Model\Options $options = null, -+ Data $checkoutDataHelper = null, -+ \Magento\Shipping\Model\Config $shippingConfig = null, -+ StoreManagerInterface $storeManager = null - ) { - $this->attributeMetadataDataProvider = $attributeMetadataDataProvider; - $this->attributeMapper = $attributeMapper; - $this->merger = $merger; -+ $this->options = $options ?: \Magento\Framework\App\ObjectManager::getInstance() -+ ->get(\Magento\Customer\Model\Options::class); -+ $this->checkoutDataHelper = $checkoutDataHelper ?: \Magento\Framework\App\ObjectManager::getInstance() -+ ->get(Data::class); -+ $this->shippingConfig = $shippingConfig ?: \Magento\Framework\App\ObjectManager::getInstance() -+ ->get(\Magento\Shipping\Model\Config::class); -+ $this->storeManager = $storeManager ?: \Magento\Framework\App\ObjectManager::getInstance() -+ ->get(StoreManagerInterface::class); - } - - /** -- * @deprecated 100.0.11 -- * @return \Magento\Customer\Model\Options -- */ -- private function getOptions() -- { -- if (!is_object($this->options)) { -- $this->options = ObjectManager::getInstance()->get(\Magento\Customer\Model\Options::class); -- } -- return $this->options; -- } -- -- /** -+ * Get address attributes. -+ * - * @return array - */ - private function getAddressAttributes() -@@ -116,6 +122,7 @@ class LayoutProcessor implements \Magento\Checkout\Block\Checkout\LayoutProcesso - if (!in_array($code, $codes)) { - continue; - } -+ // phpcs:ignore Magento2.Functions.DiscouragedFunction - $options = call_user_func($attributesToConvert[$code]); - if (!is_array($options)) { - continue; -@@ -143,8 +150,8 @@ class LayoutProcessor implements \Magento\Checkout\Block\Checkout\LayoutProcesso - public function process($jsLayout) - { - $attributesToConvert = [ -- 'prefix' => [$this->getOptions(), 'getNamePrefixOptions'], -- 'suffix' => [$this->getOptions(), 'getNameSuffixOptions'], -+ 'prefix' => [$this->options, 'getNamePrefixOptions'], -+ 'suffix' => [$this->options, 'getNameSuffixOptions'], - ]; - - $elements = $this->getAddressAttributes(); -@@ -192,8 +199,8 @@ class LayoutProcessor implements \Magento\Checkout\Block\Checkout\LayoutProcesso - */ - private function processShippingChildrenComponents($shippingRatesLayout) - { -- $activeCarriers = $this->getShippingConfig()->getActiveCarriers( -- $this->getStoreResolver()->getCurrentStoreId() -+ $activeCarriers = $this->shippingConfig->getActiveCarriers( -+ $this->storeManager->getStore()->getId() - ); - foreach (array_keys($shippingRatesLayout) as $carrierName) { - $carrierKey = str_replace('-rates-validation', '', $carrierName); -@@ -206,6 +213,7 @@ class LayoutProcessor implements \Magento\Checkout\Block\Checkout\LayoutProcesso - - /** - * Appends billing address form component to payment layout -+ * - * @param array $paymentLayout - * @param array $elements - * @return array -@@ -221,7 +229,7 @@ class LayoutProcessor implements \Magento\Checkout\Block\Checkout\LayoutProcesso - } - - // The if billing address should be displayed on Payment method or page -- if ($this->getCheckoutDataHelper()->isDisplayBillingOnPaymentMethodAvailable()) { -+ if ($this->checkoutDataHelper->isDisplayBillingOnPaymentMethodAvailable()) { - $paymentLayout['payments-list']['children'] = - array_merge_recursive( - $paymentLayout['payments-list']['children'], -@@ -280,8 +288,14 @@ class LayoutProcessor implements \Magento\Checkout\Block\Checkout\LayoutProcesso - 'provider' => 'checkoutProvider', - 'deps' => 'checkoutProvider', - 'dataScopePrefix' => 'billingAddress' . $paymentCode, -+ 'billingAddressListProvider' => '${$.name}.billingAddressList', - 'sortOrder' => 1, - 'children' => [ -+ 'billingAddressList' => [ -+ 'component' => 'Magento_Checkout/js/view/billing-address/list', -+ 'displayArea' => 'billing-address-list', -+ 'template' => 'Magento_Checkout/billing-address/list' -+ ], - 'form-fields' => [ - 'component' => 'uiComponent', - 'displayArea' => 'additional-fieldsets', -@@ -340,49 +354,4 @@ class LayoutProcessor implements \Magento\Checkout\Block\Checkout\LayoutProcesso - ], - ]; - } -- -- /** -- * Get checkout data helper instance -- * -- * @return Data -- * @deprecated 100.1.4 -- */ -- private function getCheckoutDataHelper() -- { -- if (!$this->checkoutDataHelper) { -- $this->checkoutDataHelper = ObjectManager::getInstance()->get(Data::class); -- } -- -- return $this->checkoutDataHelper; -- } -- -- /** -- * Retrieve Shipping Configuration. -- * -- * @return \Magento\Shipping\Model\Config -- * @deprecated 100.2.0 -- */ -- private function getShippingConfig() -- { -- if (!$this->shippingConfig) { -- $this->shippingConfig = ObjectManager::getInstance()->get(\Magento\Shipping\Model\Config::class); -- } -- -- return $this->shippingConfig; -- } -- -- /** -- * Get store resolver. -- * -- * @return StoreResolverInterface -- * @deprecated 100.2.0 -- */ -- private function getStoreResolver() -- { -- if (!$this->storeResolver) { -- $this->storeResolver = ObjectManager::getInstance()->get(StoreResolverInterface::class); -- } -- -- return $this->storeResolver; -- } - } -diff --git a/app/code/Magento/Checkout/Block/Link.php b/app/code/Magento/Checkout/Block/Link.php -index cb2fb3309b8..3d0740181f4 100644 ---- a/app/code/Magento/Checkout/Block/Link.php -+++ b/app/code/Magento/Checkout/Block/Link.php -@@ -13,7 +13,7 @@ namespace Magento\Checkout\Block; - class Link extends \Magento\Framework\View\Element\Html\Link - { - /** -- * @var \Magento\Framework\Module\Manager -+ * @var \Magento\Framework\Module\ModuleManagerInterface - */ - protected $_moduleManager; - -@@ -24,14 +24,14 @@ class Link extends \Magento\Framework\View\Element\Html\Link - - /** - * @param \Magento\Framework\View\Element\Template\Context $context -- * @param \Magento\Framework\Module\Manager $moduleManager -+ * @param \Magento\Framework\Module\ModuleManagerInterface $moduleManager - * @param \Magento\Checkout\Helper\Data $checkoutHelper - * @param array $data - * @codeCoverageIgnore - */ - public function __construct( - \Magento\Framework\View\Element\Template\Context $context, -- \Magento\Framework\Module\Manager $moduleManager, -+ \Magento\Framework\Module\ModuleManagerInterface $moduleManager, - \Magento\Checkout\Helper\Data $checkoutHelper, - array $data = [] - ) { -@@ -41,6 +41,8 @@ class Link extends \Magento\Framework\View\Element\Html\Link - } - - /** -+ * Get href. -+ * - * @return string - * @codeCoverageIgnore - */ -diff --git a/app/code/Magento/Checkout/Block/Onepage.php b/app/code/Magento/Checkout/Block/Onepage.php -index ca6b045ddbb..e01d5835b4c 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 @@ class Onepage extends \Magento\Framework\View\Element\Template - \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 @@ class Onepage extends \Magento\Framework\View\Element\Template - $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 @@ class Onepage extends \Magento\Framework\View\Element\Template - $this->jsLayout = $processor->process($this->jsLayout); - } - -- return json_encode($this->jsLayout, JSON_HEX_TAG); -+ return $this->serializer->serialize($this->jsLayout); - } - - /** -@@ -115,11 +117,13 @@ class Onepage extends \Magento\Framework\View\Element\Template - } - - /** -+ * Retrieve serialized checkout config. -+ * - * @return bool|string - * @since 100.2.0 - */ - public function getSerializedCheckoutConfig() - { -- return json_encode($this->getCheckoutConfig(), JSON_HEX_TAG); -+ return $this->serializer->serialize($this->getCheckoutConfig()); - } - } -diff --git a/app/code/Magento/Checkout/Block/QuoteShortcutButtons.php b/app/code/Magento/Checkout/Block/QuoteShortcutButtons.php -index 6b3774f7e38..3b2f1604fae 100644 ---- a/app/code/Magento/Checkout/Block/QuoteShortcutButtons.php -+++ b/app/code/Magento/Checkout/Block/QuoteShortcutButtons.php -@@ -8,6 +8,8 @@ namespace Magento\Checkout\Block; - use Magento\Framework\View\Element\Template; - - /** -+ * Displays buttons on shopping cart page -+ * - * @api - */ - class QuoteShortcutButtons extends \Magento\Catalog\Block\ShortcutButtons -@@ -45,7 +47,8 @@ class QuoteShortcutButtons extends \Magento\Catalog\Block\ShortcutButtons - 'container' => $this, - 'is_catalog_product' => $this->_isCatalogProduct, - 'or_position' => $this->_orPosition, -- 'checkout_session' => $this->_checkoutSession -+ 'checkout_session' => $this->_checkoutSession, -+ 'is_shopping_cart' => true - ] - ); - return $this; -diff --git a/app/code/Magento/Checkout/Controller/Account/DelegateCreate.php b/app/code/Magento/Checkout/Controller/Account/DelegateCreate.php -index 6c4c8b053e2..e4f909f9a81 100644 ---- a/app/code/Magento/Checkout/Controller/Account/DelegateCreate.php -+++ b/app/code/Magento/Checkout/Controller/Account/DelegateCreate.php -@@ -7,6 +7,7 @@ declare(strict_types=1); - - namespace Magento\Checkout\Controller\Account; - -+use Magento\Framework\App\Action\HttpGetActionInterface as HttpGetActionInterface; - use Magento\Framework\App\Action\Action; - use Magento\Framework\App\Action\Context; - use Magento\Checkout\Model\Session; -@@ -15,7 +16,7 @@ use Magento\Sales\Api\OrderCustomerDelegateInterface; - /** - * Redirect guest customer for registration. - */ --class DelegateCreate extends Action -+class DelegateCreate extends Action implements HttpGetActionInterface - { - /** - * @var OrderCustomerDelegateInterface -diff --git a/app/code/Magento/Checkout/Controller/Cart.php b/app/code/Magento/Checkout/Controller/Cart.php -index f244fe310af..d7b09c17ee0 100644 ---- a/app/code/Magento/Checkout/Controller/Cart.php -+++ b/app/code/Magento/Checkout/Controller/Cart.php -@@ -106,8 +106,7 @@ abstract class Cart extends \Magento\Framework\App\Action\Action implements View - /** - * Get resolved back url - * -- * @param null $defaultUrl -- * -+ * @param string|null $defaultUrl - * @return mixed|null|string - */ - protected function getBackUrl($defaultUrl = null) -@@ -118,12 +117,7 @@ abstract class Cart extends \Magento\Framework\App\Action\Action implements View - return $returnUrl; - } - -- $shouldRedirectToCart = $this->_scopeConfig->getValue( -- 'checkout/cart/redirect_to_cart', -- \Magento\Store\Model\ScopeInterface::SCOPE_STORE -- ); -- -- if ($shouldRedirectToCart || $this->getRequest()->getParam('in_cart')) { -+ if ($this->shouldRedirectToCart() || $this->getRequest()->getParam('in_cart')) { - if ($this->getRequest()->getActionName() == 'add' && !$this->getRequest()->getParam('in_cart')) { - $this->_checkoutSession->setContinueShoppingUrl($this->_redirect->getRefererUrl()); - } -@@ -132,4 +126,17 @@ abstract class Cart extends \Magento\Framework\App\Action\Action implements View - - return $defaultUrl; - } -+ -+ /** -+ * Is redirect should be performed after the product was added to cart. -+ * -+ * @return bool -+ */ -+ private function shouldRedirectToCart() -+ { -+ return $this->_scopeConfig->isSetFlag( -+ 'checkout/cart/redirect_to_cart', -+ \Magento\Store\Model\ScopeInterface::SCOPE_STORE -+ ); -+ } - } -diff --git a/app/code/Magento/Checkout/Controller/Cart/Add.php b/app/code/Magento/Checkout/Controller/Cart/Add.php -index 92dd8dd8f25..739f71caeb8 100644 ---- a/app/code/Magento/Checkout/Controller/Cart/Add.php -+++ b/app/code/Magento/Checkout/Controller/Cart/Add.php -@@ -6,14 +6,17 @@ - */ - namespace Magento\Checkout\Controller\Cart; - -+use Magento\Framework\App\Action\HttpPostActionInterface as HttpPostActionInterface; - use Magento\Catalog\Api\ProductRepositoryInterface; - use Magento\Checkout\Model\Cart as CustomerCart; - use Magento\Framework\Exception\NoSuchEntityException; - - /** -+ * Controller for processing add to cart action. -+ * - * @SuppressWarnings(PHPMD.CouplingBetweenObjects) - */ --class Add extends \Magento\Checkout\Controller\Cart -+class Add extends \Magento\Checkout\Controller\Cart implements HttpPostActionInterface - { - /** - * @var ProductRepositoryInterface -@@ -80,6 +83,9 @@ class Add extends \Magento\Checkout\Controller\Cart - public function execute() - { - if (!$this->_formKeyValidator->validate($this->getRequest())) { -+ $this->messageManager->addErrorMessage( -+ __('Your session has expired') -+ ); - return $this->resultRedirectFactory->create()->setPath('*/*/'); - } - -@@ -122,11 +128,21 @@ class Add extends \Magento\Checkout\Controller\Cart - - if (!$this->_checkoutSession->getNoCartRedirect(true)) { - if (!$this->cart->getQuote()->getHasError()) { -- $message = __( -- 'You added %1 to your shopping cart.', -- $product->getName() -- ); -- $this->messageManager->addSuccessMessage($message); -+ if ($this->shouldRedirectToCart()) { -+ $message = __( -+ 'You added %1 to your shopping cart.', -+ $product->getName() -+ ); -+ $this->messageManager->addSuccessMessage($message); -+ } else { -+ $this->messageManager->addComplexSuccessMessage( -+ 'addCartSuccessMessage', -+ [ -+ 'product_name' => $product->getName(), -+ 'cart_url' => $this->getCartUrl(), -+ ] -+ ); -+ } - } - return $this->goBack(null, $product); - } -@@ -147,8 +163,7 @@ class Add extends \Magento\Checkout\Controller\Cart - $url = $this->_checkoutSession->getRedirectUrl(true); - - if (!$url) { -- $cartUrl = $this->_objectManager->get(\Magento\Checkout\Helper\Cart::class)->getCartUrl(); -- $url = $this->_redirect->getRedirectUrl($cartUrl); -+ $url = $this->_redirect->getRedirectUrl($this->getCartUrl()); - } - - return $this->goBack($url); -@@ -191,4 +206,27 @@ class Add extends \Magento\Checkout\Controller\Cart - $this->_objectManager->get(\Magento\Framework\Json\Helper\Data::class)->jsonEncode($result) - ); - } -+ -+ /** -+ * Returns cart url -+ * -+ * @return string -+ */ -+ private function getCartUrl() -+ { -+ return $this->_url->getUrl('checkout/cart', ['_secure' => true]); -+ } -+ -+ /** -+ * Is redirect should be performed after the product was added to cart. -+ * -+ * @return bool -+ */ -+ private function shouldRedirectToCart() -+ { -+ return $this->_scopeConfig->isSetFlag( -+ 'checkout/cart/redirect_to_cart', -+ \Magento\Store\Model\ScopeInterface::SCOPE_STORE -+ ); -+ } - } -diff --git a/app/code/Magento/Checkout/Controller/Cart/Addgroup.php b/app/code/Magento/Checkout/Controller/Cart/Addgroup.php -index c205f3c1607..7eb93620312 100644 ---- a/app/code/Magento/Checkout/Controller/Cart/Addgroup.php -+++ b/app/code/Magento/Checkout/Controller/Cart/Addgroup.php -@@ -4,17 +4,21 @@ - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -+ - namespace Magento\Checkout\Controller\Cart; - - use Magento\Checkout\Model\Cart as CustomerCart; -+use Magento\Framework\App\Action\HttpPostActionInterface; - use Magento\Framework\Escaper; - use Magento\Framework\App\ObjectManager; - use Magento\Sales\Model\Order\Item; - - /** -+ * Add grouped items controller. -+ * - * @SuppressWarnings(PHPMD.CouplingBetweenObjects) - */ --class Addgroup extends \Magento\Checkout\Controller\Cart -+class Addgroup extends \Magento\Checkout\Controller\Cart implements HttpPostActionInterface - { - /** - * @var Escaper -@@ -44,6 +48,8 @@ class Addgroup extends \Magento\Checkout\Controller\Cart - } - - /** -+ * Add items in group. -+ * - * @return \Magento\Framework\Controller\Result\Redirect - */ - public function execute() -@@ -74,6 +80,8 @@ class Addgroup extends \Magento\Checkout\Controller\Cart - } - } - $this->cart->save(); -+ } else { -+ $this->messageManager->addErrorMessage(__('Please select at least one product to add to cart')); - } - return $this->_goBack(); - } -diff --git a/app/code/Magento/Checkout/Controller/Cart/Configure.php b/app/code/Magento/Checkout/Controller/Cart/Configure.php -index 19b2d2db345..aa4ae755d79 100644 ---- a/app/code/Magento/Checkout/Controller/Cart/Configure.php -+++ b/app/code/Magento/Checkout/Controller/Cart/Configure.php -@@ -7,13 +7,14 @@ - - namespace Magento\Checkout\Controller\Cart; - -+use Magento\Framework\App\Action\HttpGetActionInterface as HttpGetActionInterface; - use Magento\Framework; - use Magento\Framework\Controller\ResultFactory; - - /** - * @SuppressWarnings(PHPMD.CouplingBetweenObjects) - */ --class Configure extends \Magento\Checkout\Controller\Cart -+class Configure extends \Magento\Checkout\Controller\Cart implements HttpGetActionInterface - { - /** - * @var \Magento\Framework\View\Result\PageFactory -diff --git a/app/code/Magento/Checkout/Controller/Cart/CouponPost.php b/app/code/Magento/Checkout/Controller/Cart/CouponPost.php -index 5f683351811..fdfe817f354 100644 ---- a/app/code/Magento/Checkout/Controller/Cart/CouponPost.php -+++ b/app/code/Magento/Checkout/Controller/Cart/CouponPost.php -@@ -5,10 +5,12 @@ - */ - namespace Magento\Checkout\Controller\Cart; - -+use Magento\Framework\App\Action\HttpPostActionInterface as HttpPostActionInterface; -+ - /** - * @SuppressWarnings(PHPMD.CouplingBetweenObjects) - */ --class CouponPost extends \Magento\Checkout\Controller\Cart -+class CouponPost extends \Magento\Checkout\Controller\Cart implements HttpPostActionInterface - { - /** - * Sales quote repository -diff --git a/app/code/Magento/Checkout/Controller/Cart/Delete.php b/app/code/Magento/Checkout/Controller/Cart/Delete.php -index 4a6174e83fd..e9c15bd7f8c 100644 ---- a/app/code/Magento/Checkout/Controller/Cart/Delete.php -+++ b/app/code/Magento/Checkout/Controller/Cart/Delete.php -@@ -1,12 +1,18 @@ - <?php - /** -- * - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ - namespace Magento\Checkout\Controller\Cart; - --class Delete extends \Magento\Checkout\Controller\Cart -+use Magento\Framework\App\Action\HttpPostActionInterface as HttpPostActionInterface; -+ -+/** -+ * Action Delete. -+ * -+ * Deletes item from cart. -+ */ -+class Delete extends \Magento\Checkout\Controller\Cart implements HttpPostActionInterface - { - /** - * Delete shopping cart item action -@@ -22,7 +28,12 @@ class Delete extends \Magento\Checkout\Controller\Cart - $id = (int)$this->getRequest()->getParam('id'); - if ($id) { - try { -- $this->cart->removeItem($id)->save(); -+ $this->cart->removeItem($id); -+ // We should set Totals to be recollected once more because of Cart model as usually is loading -+ // before action executing and in case when triggerRecollect setted as true recollecting will -+ // executed and the flag will be true already. -+ $this->cart->getQuote()->setTotalsCollectedFlag(false); -+ $this->cart->save(); - } catch (\Exception $e) { - $this->messageManager->addErrorMessage(__('We can\'t remove the item.')); - $this->_objectManager->get(\Psr\Log\LoggerInterface::class)->critical($e); -diff --git a/app/code/Magento/Checkout/Controller/Cart/Index.php b/app/code/Magento/Checkout/Controller/Cart/Index.php -index 3fb582d35e2..182ab677777 100644 ---- a/app/code/Magento/Checkout/Controller/Cart/Index.php -+++ b/app/code/Magento/Checkout/Controller/Cart/Index.php -@@ -7,7 +7,9 @@ - - namespace Magento\Checkout\Controller\Cart; - --class Index extends \Magento\Checkout\Controller\Cart -+use Magento\Framework\App\Action\HttpGetActionInterface as HttpGetActionInterface; -+ -+class Index extends \Magento\Checkout\Controller\Cart implements HttpGetActionInterface - { - /** - * @var \Magento\Framework\View\Result\PageFactory -diff --git a/app/code/Magento/Checkout/Controller/Cart/UpdateItemOptions.php b/app/code/Magento/Checkout/Controller/Cart/UpdateItemOptions.php -index 59bd6489bf9..40ce2252581 100644 ---- a/app/code/Magento/Checkout/Controller/Cart/UpdateItemOptions.php -+++ b/app/code/Magento/Checkout/Controller/Cart/UpdateItemOptions.php -@@ -7,7 +7,9 @@ - - namespace Magento\Checkout\Controller\Cart; - --class UpdateItemOptions extends \Magento\Checkout\Controller\Cart -+use Magento\Framework\App\Action\HttpPostActionInterface as HttpPostActionInterface; -+ -+class UpdateItemOptions extends \Magento\Checkout\Controller\Cart implements HttpPostActionInterface - { - /** - * Update product configuration for a cart item -diff --git a/app/code/Magento/Checkout/Controller/Cart/UpdatePost.php b/app/code/Magento/Checkout/Controller/Cart/UpdatePost.php -index 05ce48c9b46..bfc408d920a 100644 ---- a/app/code/Magento/Checkout/Controller/Cart/UpdatePost.php -+++ b/app/code/Magento/Checkout/Controller/Cart/UpdatePost.php -@@ -1,14 +1,18 @@ - <?php - /** -- * - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ - namespace Magento\Checkout\Controller\Cart; - - use Magento\Checkout\Model\Cart\RequestQuantityProcessor; -+use Magento\Framework\App\Action\HttpPostActionInterface; -+use Magento\Framework\App\Action\HttpGetActionInterface; - --class UpdatePost extends \Magento\Checkout\Controller\Cart -+/** -+ * Post update shopping cart. -+ */ -+class UpdatePost extends \Magento\Checkout\Controller\Cart implements HttpGetActionInterface, HttpPostActionInterface - { - /** - * @var RequestQuantityProcessor -diff --git a/app/code/Magento/Checkout/Controller/Index/Index.php b/app/code/Magento/Checkout/Controller/Index/Index.php -index 785c1f1473b..5acfab435b5 100644 ---- a/app/code/Magento/Checkout/Controller/Index/Index.php -+++ b/app/code/Magento/Checkout/Controller/Index/Index.php -@@ -9,7 +9,9 @@ declare(strict_types=1); - - namespace Magento\Checkout\Controller\Index; - --class Index extends \Magento\Checkout\Controller\Onepage -+use Magento\Framework\App\Action\HttpGetActionInterface as HttpGetActionInterface; -+ -+class Index extends \Magento\Checkout\Controller\Onepage implements HttpGetActionInterface - { - /** - * Checkout page -diff --git a/app/code/Magento/Checkout/Controller/Onepage/SaveOrder.php b/app/code/Magento/Checkout/Controller/Onepage/SaveOrder.php -index 12b725a8f6d..66b05413620 100644 ---- a/app/code/Magento/Checkout/Controller/Onepage/SaveOrder.php -+++ b/app/code/Magento/Checkout/Controller/Onepage/SaveOrder.php -@@ -6,10 +6,14 @@ - - namespace Magento\Checkout\Controller\Onepage; - -+use Magento\Framework\App\Action\HttpPostActionInterface; - use Magento\Framework\DataObject; - use Magento\Framework\Exception\PaymentException; - --class SaveOrder extends \Magento\Checkout\Controller\Onepage -+/** -+ * One Page Checkout saveOrder action -+ */ -+class SaveOrder extends \Magento\Checkout\Controller\Onepage implements HttpPostActionInterface - { - /** - * Create order action -@@ -31,7 +35,7 @@ class SaveOrder extends \Magento\Checkout\Controller\Onepage - $result = new DataObject(); - try { - $agreementsValidator = $this->_objectManager->get( -- \Magento\CheckoutAgreements\Model\AgreementsValidator::class -+ \Magento\Checkout\Api\AgreementsValidatorInterface::class - ); - if (!$agreementsValidator->isValid(array_keys($this->getRequest()->getPost('agreement', [])))) { - $result->setData('success', false); -diff --git a/app/code/Magento/Checkout/Controller/Onepage/Success.php b/app/code/Magento/Checkout/Controller/Onepage/Success.php -index ae9be42a89c..a657b23cca4 100644 ---- a/app/code/Magento/Checkout/Controller/Onepage/Success.php -+++ b/app/code/Magento/Checkout/Controller/Onepage/Success.php -@@ -1,12 +1,16 @@ - <?php - /** -- * - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ - namespace Magento\Checkout\Controller\Onepage; - --class Success extends \Magento\Checkout\Controller\Onepage -+use Magento\Framework\App\Action\HttpGetActionInterface as HttpGetActionInterface; -+ -+/** -+ * Onepage checkout success controller class -+ */ -+class Success extends \Magento\Checkout\Controller\Onepage implements HttpGetActionInterface - { - /** - * Order success action -@@ -24,7 +28,10 @@ class Success extends \Magento\Checkout\Controller\Onepage - $resultPage = $this->resultPageFactory->create(); - $this->_eventManager->dispatch( - 'checkout_onepage_controller_success_action', -- ['order_ids' => [$session->getLastOrderId()]] -+ [ -+ 'order_ids' => [$session->getLastOrderId()], -+ 'order' => $session->getLastRealOrder() -+ ] - ); - return $resultPage; - } -diff --git a/app/code/Magento/Checkout/Controller/Sidebar/RemoveItem.php b/app/code/Magento/Checkout/Controller/Sidebar/RemoveItem.php -index c84aec336a5..f589e702de9 100644 ---- a/app/code/Magento/Checkout/Controller/Sidebar/RemoveItem.php -+++ b/app/code/Magento/Checkout/Controller/Sidebar/RemoveItem.php -@@ -5,7 +5,9 @@ - */ - namespace Magento\Checkout\Controller\Sidebar; - --class RemoveItem extends \Magento\Framework\App\Action\Action -+use Magento\Framework\App\Action\HttpPostActionInterface as HttpPostActionInterface; -+ -+class RemoveItem extends \Magento\Framework\App\Action\Action implements HttpPostActionInterface - { - /** - * @var \Magento\Checkout\Model\Sidebar -diff --git a/app/code/Magento/Checkout/CustomerData/Cart.php b/app/code/Magento/Checkout/CustomerData/Cart.php -index ddb077462ef..169be4cc62f 100644 ---- a/app/code/Magento/Checkout/CustomerData/Cart.php -+++ b/app/code/Magento/Checkout/CustomerData/Cart.php -@@ -10,6 +10,8 @@ use Magento\Customer\CustomerData\SectionSourceInterface; - - /** - * Cart source -+ * -+ * @SuppressWarnings(PHPMD.CookieAndSessionMisuse) - */ - class Cart extends \Magento\Framework\DataObject implements SectionSourceInterface - { -@@ -82,7 +84,7 @@ class Cart extends \Magento\Framework\DataObject implements SectionSourceInterfa - } - - /** -- * {@inheritdoc} -+ * @inheritdoc - */ - public function getSectionData() - { -@@ -98,7 +100,8 @@ class Cart extends \Magento\Framework\DataObject implements SectionSourceInterfa - 'items' => $this->getRecentItems(), - 'extra_actions' => $this->layout->createBlock(\Magento\Catalog\Block\ShortcutButtons::class)->toHtml(), - 'isGuestCheckoutAllowed' => $this->isGuestCheckoutAllowed(), -- 'website_id' => $this->getQuote()->getStore()->getWebsiteId() -+ 'website_id' => $this->getQuote()->getStore()->getWebsiteId(), -+ 'storeId' => $this->getQuote()->getStore()->getStoreId() - ]; - } - -@@ -158,11 +161,10 @@ class Cart extends \Magento\Framework\DataObject implements SectionSourceInterfa - : $item->getProduct(); - - $products = $this->catalogUrl->getRewriteByProductStore([$product->getId() => $item->getStoreId()]); -- if (!isset($products[$product->getId()])) { -- continue; -+ if (isset($products[$product->getId()])) { -+ $urlDataObject = new \Magento\Framework\DataObject($products[$product->getId()]); -+ $item->getProduct()->setUrlDataObject($urlDataObject); - } -- $urlDataObject = new \Magento\Framework\DataObject($products[$product->getId()]); -- $item->getProduct()->setUrlDataObject($urlDataObject); - } - $items[] = $this->itemPoolInterface->getItemData($item); - } -diff --git a/app/code/Magento/Checkout/Helper/Data.php b/app/code/Magento/Checkout/Helper/Data.php -index 636d4aaca21..40bdf93d161 100644 ---- a/app/code/Magento/Checkout/Helper/Data.php -+++ b/app/code/Magento/Checkout/Helper/Data.php -@@ -59,6 +59,8 @@ class Data extends \Magento\Framework\App\Helper\AbstractHelper - private $paymentFailures; - - /** -+ * Data constructor. -+ * - * @param \Magento\Framework\App\Helper\Context $context - * @param \Magento\Store\Model\StoreManagerInterface $storeManager - * @param \Magento\Checkout\Model\Session $checkoutSession -@@ -113,6 +115,8 @@ class Data extends \Magento\Framework\App\Helper\AbstractHelper - } - - /** -+ * Format Price -+ * - * @param float $price - * @return string - */ -@@ -127,6 +131,8 @@ class Data extends \Magento\Framework\App\Helper\AbstractHelper - } - - /** -+ * Convert Price -+ * - * @param float $price - * @param bool $format - * @return float -@@ -145,9 +151,9 @@ class Data extends \Magento\Framework\App\Helper\AbstractHelper - */ - public function canOnepageCheckout() - { -- return (bool)$this->scopeConfig->getValue( -+ return $this->scopeConfig->isSetFlag( - 'checkout/options/onepage_checkout_enabled', -- \Magento\Store\Model\ScopeInterface::SCOPE_STORE -+ ScopeInterface::SCOPE_STORE - ); - } - -@@ -164,7 +170,7 @@ class Data extends \Magento\Framework\App\Helper\AbstractHelper - } - $qty = $item->getQty() ? $item->getQty() : ($item->getQtyOrdered() ? $item->getQtyOrdered() : 1); - $taxAmount = $item->getTaxAmount() + $item->getDiscountTaxCompensation(); -- $price = floatval($qty) ? ($item->getRowTotal() + $taxAmount) / $qty : 0; -+ $price = (float)$qty ? ($item->getRowTotal() + $taxAmount) / $qty : 0; - return $this->priceCurrency->round($price); - } - -@@ -184,6 +190,8 @@ class Data extends \Magento\Framework\App\Helper\AbstractHelper - } - - /** -+ * Get Base Price Incl Tax -+ * - * @param AbstractItem $item - * @return float - */ -@@ -191,11 +199,13 @@ class Data extends \Magento\Framework\App\Helper\AbstractHelper - { - $qty = $item->getQty() ? $item->getQty() : ($item->getQtyOrdered() ? $item->getQtyOrdered() : 1); - $taxAmount = $item->getBaseTaxAmount() + $item->getBaseDiscountTaxCompensation(); -- $price = floatval($qty) ? ($item->getBaseRowTotal() + $taxAmount) / $qty : 0; -+ $price = (float)$qty ? ($item->getBaseRowTotal() + $taxAmount) / $qty : 0; - return $this->priceCurrency->round($price); - } - - /** -+ * Get Base Subtotal Incl Tax -+ * - * @param AbstractItem $item - * @return float - */ -@@ -217,13 +227,15 @@ class Data extends \Magento\Framework\App\Helper\AbstractHelper - \Magento\Quote\Model\Quote $checkout, - string $message, - string $checkoutType = 'onepage' -- ): \Magento\Checkout\Helper\Data { -+ ): Data { - $this->paymentFailures->handle((int)$checkout->getId(), $message, $checkoutType); - - return $this; - } - - /** -+ * Get Emails -+ * - * @param string $configPath - * @param null|string|bool|int|Store $storeId - * @return array|false -@@ -232,7 +244,7 @@ class Data extends \Magento\Framework\App\Helper\AbstractHelper - { - $data = $this->scopeConfig->getValue( - $configPath, -- \Magento\Store\Model\ScopeInterface::SCOPE_STORE, -+ ScopeInterface::SCOPE_STORE, - $storeId - ); - if (!empty($data)) { -@@ -242,8 +254,7 @@ class Data extends \Magento\Framework\App\Helper\AbstractHelper - } - - /** -- * Check is allowed Guest Checkout -- * Use config settings and observer -+ * Check is allowed Guest Checkout. Use config settings and observer - * - * @param \Magento\Quote\Model\Quote $quote - * @param int|Store $store -@@ -256,7 +267,7 @@ class Data extends \Magento\Framework\App\Helper\AbstractHelper - } - $guestCheckout = $this->scopeConfig->isSetFlag( - self::XML_PATH_GUEST_CHECKOUT, -- \Magento\Store\Model\ScopeInterface::SCOPE_STORE, -+ ScopeInterface::SCOPE_STORE, - $store - ); - -@@ -295,13 +306,13 @@ class Data extends \Magento\Framework\App\Helper\AbstractHelper - { - return $this->scopeConfig->isSetFlag( - self::XML_PATH_CUSTOMER_MUST_BE_LOGGED, -- \Magento\Store\Model\ScopeInterface::SCOPE_STORE -+ ScopeInterface::SCOPE_STORE - ); - } - - /** -- * Checks if display billing address on payment method is available, otherwise -- * billing address should be display on payment page -+ * If display billing address on payment method is available, otherwise should be display on payment page -+ * - * @return bool - */ - public function isDisplayBillingOnPaymentMethodAvailable() -diff --git a/app/code/Magento/Checkout/Model/Cart.php b/app/code/Magento/Checkout/Model/Cart.php -index a18cba3f67c..cec99909dc9 100644 ---- a/app/code/Magento/Checkout/Model/Cart.php -+++ b/app/code/Magento/Checkout/Model/Cart.php -@@ -16,6 +16,7 @@ use Magento\Framework\Exception\NoSuchEntityException; - * Shopping cart model - * - * @api -+ * @SuppressWarnings(PHPMD.CookieAndSessionMisuse) - * @SuppressWarnings(PHPMD.CouplingBetweenObjects) - * @deprecated 100.1.0 Use \Magento\Quote\Model\Quote instead - * @see \Magento\Quote\Api\Data\CartInterface -@@ -365,21 +366,15 @@ class Cart extends DataObject implements CartInterface - public function addProduct($productInfo, $requestInfo = null) - { - $product = $this->_getProduct($productInfo); -- $request = $this->_getProductRequest($requestInfo); - $productId = $product->getId(); - - if ($productId) { -- $stockItem = $this->stockRegistry->getStockItem($productId, $product->getStore()->getWebsiteId()); -- $minimumQty = $stockItem->getMinSaleQty(); -- //If product quantity is not specified in request and there is set minimal qty for it -- if ($minimumQty -- && $minimumQty > 0 -- && !$request->getQty() -- ) { -- $request->setQty($minimumQty); -- } -- -+ $request = $this->getQtyRequest($product, $requestInfo); - try { -+ $this->_eventManager->dispatch( -+ 'checkout_cart_product_add_before', -+ ['info' => $requestInfo, 'product' => $product] -+ ); - $result = $this->getQuote()->addProduct($product, $request); - } catch (\Magento\Framework\Exception\LocalizedException $e) { - $this->_checkoutSession->setUseNotice(false); -@@ -434,8 +429,9 @@ class Cart extends DataObject implements CartInterface - } - $product = $this->_getProduct($productId); - if ($product->getId() && $product->isVisibleInCatalog()) { -+ $request = $this->getQtyRequest($product); - try { -- $this->getQuote()->addProduct($product); -+ $this->getQuote()->addProduct($product, $request); - } catch (\Exception $e) { - $allAdded = false; - } -@@ -615,6 +611,8 @@ class Cart extends DataObject implements CartInterface - } - - /** -+ * Get product ids. -+ * - * @return int[] - */ - public function getProductIds() -@@ -756,4 +754,27 @@ class Cart extends DataObject implements CartInterface - } - return $this->requestInfoFilter; - } -+ -+ /** -+ * Get request quantity -+ * -+ * @param Product $product -+ * @param \Magento\Framework\DataObject|int|array $request -+ * @return int|DataObject -+ */ -+ private function getQtyRequest($product, $request = 0) -+ { -+ $request = $this->_getProductRequest($request); -+ $stockItem = $this->stockRegistry->getStockItem($product->getId(), $product->getStore()->getWebsiteId()); -+ $minimumQty = $stockItem->getMinSaleQty(); -+ //If product quantity is not specified in request and there is set minimal qty for it -+ if ($minimumQty -+ && $minimumQty > 0 -+ && !$request->getQty() -+ ) { -+ $request->setQty($minimumQty); -+ } -+ -+ return $request; -+ } - } -diff --git a/app/code/Magento/Checkout/Model/DefaultConfigProvider.php b/app/code/Magento/Checkout/Model/DefaultConfigProvider.php -index 16f13511001..470d4a3aca5 100644 ---- a/app/code/Magento/Checkout/Model/DefaultConfigProvider.php -+++ b/app/code/Magento/Checkout/Model/DefaultConfigProvider.php -@@ -10,9 +10,12 @@ use Magento\Checkout\Helper\Data as CheckoutHelper; - use Magento\Checkout\Model\Session as CheckoutSession; - use Magento\Customer\Api\AddressMetadataInterface; - use Magento\Customer\Api\CustomerRepositoryInterface as CustomerRepository; -+use Magento\Customer\Model\Address\CustomerAddressDataProvider; - use Magento\Customer\Model\Context as CustomerContext; - use Magento\Customer\Model\Session as CustomerSession; - use Magento\Customer\Model\Url as CustomerUrlManager; -+use Magento\Eav\Api\AttributeOptionManagementInterface; -+use Magento\Framework\Api\CustomAttributesDataInterface; - use Magento\Framework\App\Config\ScopeConfigInterface; - use Magento\Framework\App\Http\Context as HttpContext; - use Magento\Framework\App\ObjectManager; -@@ -21,16 +24,26 @@ use Magento\Framework\Locale\FormatInterface as LocaleFormat; - use Magento\Framework\UrlInterface; - use Magento\Quote\Api\CartItemRepositoryInterface as QuoteItemRepository; - use Magento\Quote\Api\CartTotalRepositoryInterface; -+use Magento\Quote\Api\Data\AddressInterface; - use Magento\Quote\Api\ShippingMethodManagementInterface as ShippingMethodManager; - use Magento\Quote\Model\QuoteIdMaskFactory; - use Magento\Store\Model\ScopeInterface; -+use Magento\Ui\Component\Form\Element\Multiline; - - /** -+ * Default Config Provider -+ * - * @SuppressWarnings(PHPMD.CouplingBetweenObjects) - * @SuppressWarnings(PHPMD.TooManyFields) -+ * @SuppressWarnings(PHPMD.CookieAndSessionMisuse) - */ - class DefaultConfigProvider implements ConfigProviderInterface - { -+ /** -+ * @var AttributeOptionManagementInterface -+ */ -+ private $attributeOptionManager; -+ - /** - * @var CheckoutHelper - */ -@@ -166,6 +179,11 @@ class DefaultConfigProvider implements ConfigProviderInterface - */ - private $addressMetadata; - -+ /** -+ * @var CustomerAddressDataProvider -+ */ -+ private $customerAddressData; -+ - /** - * @param CheckoutHelper $checkoutHelper - * @param Session $checkoutSession -@@ -194,6 +212,8 @@ class DefaultConfigProvider implements ConfigProviderInterface - * @param \Magento\Quote\Api\PaymentMethodManagementInterface $paymentMethodManagement - * @param UrlInterface $urlBuilder - * @param AddressMetadataInterface $addressMetadata -+ * @param AttributeOptionManagementInterface $attributeOptionManager -+ * @param CustomerAddressDataProvider|null $customerAddressData - * @codeCoverageIgnore - * @SuppressWarnings(PHPMD.ExcessiveParameterList) - */ -@@ -224,7 +244,9 @@ class DefaultConfigProvider implements ConfigProviderInterface - \Magento\Store\Model\StoreManagerInterface $storeManager, - \Magento\Quote\Api\PaymentMethodManagementInterface $paymentMethodManagement, - UrlInterface $urlBuilder, -- AddressMetadataInterface $addressMetadata = null -+ AddressMetadataInterface $addressMetadata = null, -+ AttributeOptionManagementInterface $attributeOptionManager = null, -+ CustomerAddressDataProvider $customerAddressData = null - ) { - $this->checkoutHelper = $checkoutHelper; - $this->checkoutSession = $checkoutSession; -@@ -253,20 +275,41 @@ class DefaultConfigProvider implements ConfigProviderInterface - $this->paymentMethodManagement = $paymentMethodManagement; - $this->urlBuilder = $urlBuilder; - $this->addressMetadata = $addressMetadata ?: ObjectManager::getInstance()->get(AddressMetadataInterface::class); -+ $this->attributeOptionManager = $attributeOptionManager ?? -+ ObjectManager::getInstance()->get(AttributeOptionManagementInterface::class); -+ $this->customerAddressData = $customerAddressData ?: -+ ObjectManager::getInstance()->get(CustomerAddressDataProvider::class); - } - - /** -- * {@inheritdoc} -+ * Return configuration array -+ * -+ * @return array|mixed -+ * @throws \Magento\Framework\Exception\NoSuchEntityException -+ * @throws \Magento\Framework\Exception\LocalizedException - */ - public function getConfig() - { -- $quoteId = $this->checkoutSession->getQuote()->getId(); -+ $quote = $this->checkoutSession->getQuote(); -+ $quoteId = $quote->getId(); -+ $email = $quote->getShippingAddress()->getEmail(); -+ $quoteItemData = $this->getQuoteItemData(); - $output['formKey'] = $this->formKey->getFormKey(); - $output['customerData'] = $this->getCustomerData(); - $output['quoteData'] = $this->getQuoteData(); -- $output['quoteItemData'] = $this->getQuoteItemData(); -+ $output['quoteItemData'] = $quoteItemData; -+ $output['quoteMessages'] = $this->getQuoteItemsMessages($quoteItemData); - $output['isCustomerLoggedIn'] = $this->isCustomerLoggedIn(); - $output['selectedShippingMethod'] = $this->getSelectedShippingMethod(); -+ if ($email && !$this->isCustomerLoggedIn()) { -+ $shippingAddressFromData = $this->getAddressFromData($quote->getShippingAddress()); -+ $billingAddressFromData = $this->getAddressFromData($quote->getBillingAddress()); -+ $output['shippingAddressFromData'] = $shippingAddressFromData; -+ if ($shippingAddressFromData != $billingAddressFromData) { -+ $output['billingAddressFromData'] = $billingAddressFromData; -+ } -+ $output['validatedEmailValue'] = $email; -+ } - $output['storeCode'] = $this->getStoreCode(); - $output['isGuestCheckoutAllowed'] = $this->isGuestCheckoutAllowed(); - $output['isCustomerLoginRequired'] = $this->isCustomerLoginRequired(); -@@ -278,14 +321,15 @@ class DefaultConfigProvider implements ConfigProviderInterface - $output['staticBaseUrl'] = $this->getStaticBaseUrl(); - $output['priceFormat'] = $this->localeFormat->getPriceFormat( - null, -- $this->checkoutSession->getQuote()->getQuoteCurrencyCode() -+ $quote->getQuoteCurrencyCode() - ); - $output['basePriceFormat'] = $this->localeFormat->getPriceFormat( - null, -- $this->checkoutSession->getQuote()->getBaseCurrencyCode() -+ $quote->getBaseCurrencyCode() - ); - $output['postCodes'] = $this->postCodesConfig->getPostCodes(); - $output['imageData'] = $this->imageProvider->getImages($quoteId); -+ - $output['totalsData'] = $this->getTotalsData(); - $output['shippingPolicy'] = [ - 'isEnabled' => $this->scopeConfig->isSetFlag( -@@ -326,57 +370,18 @@ class DefaultConfigProvider implements ConfigProviderInterface - * - * @return array - */ -- private function getCustomerData() -+ private function getCustomerData(): array - { - $customerData = []; - if ($this->isCustomerLoggedIn()) { -+ /** @var \Magento\Customer\Api\Data\CustomerInterface $customer */ - $customer = $this->customerRepository->getById($this->customerSession->getCustomerId()); - $customerData = $customer->__toArray(); -- foreach ($customer->getAddresses() as $key => $address) { -- $customerData['addresses'][$key]['inline'] = $this->getCustomerAddressInline($address); -- if ($address->getCustomAttributes()) { -- $customerData['addresses'][$key]['custom_attributes'] = $this->filterNotVisibleAttributes( -- $customerData['addresses'][$key]['custom_attributes'] -- ); -- } -- } -+ $customerData['addresses'] = $this->customerAddressData->getAddressDataByCustomer($customer); - } - return $customerData; - } - -- /** -- * Filter not visible on storefront custom attributes. -- * -- * @param array $attributes -- * @return array -- */ -- private function filterNotVisibleAttributes(array $attributes) -- { -- $attributesMetadata = $this->addressMetadata->getAllAttributesMetadata(); -- foreach ($attributesMetadata as $attributeMetadata) { -- if (!$attributeMetadata->isVisible()) { -- unset($attributes[$attributeMetadata->getAttributeCode()]); -- } -- } -- -- return $attributes; -- } -- -- /** -- * Set additional customer address data -- * -- * @param \Magento\Customer\Api\Data\AddressInterface $address -- * @return string -- */ -- private function getCustomerAddressInline($address) -- { -- $builtOutputAddressData = $this->addressMapper->toFlatArray($address); -- return $this->addressConfig -- ->getFormatByCode(\Magento\Customer\Model\Address\Config::DEFAULT_ADDRESS_FORMAT) -- ->getRenderer() -- ->renderArray($builtOutputAddressData); -- } -- - /** - * Retrieve quote data - * -@@ -420,6 +425,7 @@ class DefaultConfigProvider implements ConfigProviderInterface - $quoteItem->getProduct(), - 'product_thumbnail_image' - )->getUrl(); -+ $quoteItemData[$index]['message'] = $quoteItem->getMessage(); - } - } - return $quoteItemData; -@@ -513,6 +519,38 @@ class DefaultConfigProvider implements ConfigProviderInterface - return $shippingMethodData; - } - -+ /** -+ * Create address data appropriate to fill checkout address form -+ * -+ * @param AddressInterface $address -+ * @return array -+ * @throws \Magento\Framework\Exception\LocalizedException -+ */ -+ private function getAddressFromData(AddressInterface $address) -+ { -+ $addressData = []; -+ $attributesMetadata = $this->addressMetadata->getAllAttributesMetadata(); -+ foreach ($attributesMetadata as $attributeMetadata) { -+ if (!$attributeMetadata->isVisible()) { -+ continue; -+ } -+ $attributeCode = $attributeMetadata->getAttributeCode(); -+ $attributeData = $address->getData($attributeCode); -+ if ($attributeData) { -+ if ($attributeMetadata->getFrontendInput() === Multiline::NAME) { -+ $attributeData = \is_array($attributeData) ? $attributeData : explode("\n", $attributeData); -+ $attributeData = (object)$attributeData; -+ } -+ if ($attributeMetadata->isUserDefined()) { -+ $addressData[CustomAttributesDataInterface::CUSTOM_ATTRIBUTES][$attributeCode] = $attributeData; -+ continue; -+ } -+ $addressData[$attributeCode] = $attributeData; -+ } -+ } -+ return $addressData; -+ } -+ - /** - * Retrieve store code - * -@@ -581,6 +619,7 @@ class DefaultConfigProvider implements ConfigProviderInterface - - /** - * Return quote totals data -+ * - * @return array - */ - private function getTotalsData() -@@ -612,6 +651,7 @@ class DefaultConfigProvider implements ConfigProviderInterface - - /** - * Returns active carriers codes -+ * - * @return array - */ - private function getActiveCarriers() -@@ -625,6 +665,7 @@ class DefaultConfigProvider implements ConfigProviderInterface - - /** - * Returns origin country code -+ * - * @return string - */ - private function getOriginCountryCode() -@@ -638,7 +679,9 @@ class DefaultConfigProvider implements ConfigProviderInterface - - /** - * Returns array of payment methods -- * @return array -+ * -+ * @return array $paymentMethods -+ * @throws \Magento\Framework\Exception\NoSuchEntityException - */ - private function getPaymentMethods() - { -@@ -654,4 +697,22 @@ class DefaultConfigProvider implements ConfigProviderInterface - } - return $paymentMethods; - } -+ -+ /** -+ * Get notification messages for the quote items -+ * -+ * @param array $quoteItemData -+ * @return array -+ */ -+ private function getQuoteItemsMessages(array $quoteItemData): array -+ { -+ $quoteItemsMessages = []; -+ if ($quoteItemData) { -+ foreach ($quoteItemData as $item) { -+ $quoteItemsMessages[$item['item_id']] = $item['message']; -+ } -+ } -+ -+ return $quoteItemsMessages; -+ } - } -diff --git a/app/code/Magento/Checkout/Model/GuestPaymentInformationManagement.php b/app/code/Magento/Checkout/Model/GuestPaymentInformationManagement.php -index 333226b7d21..da29482f012 100644 ---- a/app/code/Magento/Checkout/Model/GuestPaymentInformationManagement.php -+++ b/app/code/Magento/Checkout/Model/GuestPaymentInformationManagement.php -@@ -14,6 +14,8 @@ use Magento\Framework\Exception\CouldNotSaveException; - use Magento\Quote\Model\Quote; - - /** -+ * Guest payment information management model. -+ * - * @SuppressWarnings(PHPMD.CouplingBetweenObjects) - */ - class GuestPaymentInformationManagement implements \Magento\Checkout\Api\GuestPaymentInformationManagementInterface -@@ -66,7 +68,7 @@ class GuestPaymentInformationManagement implements \Magento\Checkout\Api\GuestPa - * @param \Magento\Checkout\Api\PaymentInformationManagementInterface $paymentInformationManagement - * @param \Magento\Quote\Model\QuoteIdMaskFactory $quoteIdMaskFactory - * @param CartRepositoryInterface $cartRepository -- * @param ResourceConnection|null -+ * @param ResourceConnection $connectionPool - * @codeCoverageIgnore - */ - public function __construct( -@@ -88,7 +90,7 @@ class GuestPaymentInformationManagement implements \Magento\Checkout\Api\GuestPa - } - - /** -- * {@inheritDoc} -+ * @inheritdoc - */ - public function savePaymentInformationAndPlaceOrder( - $cartId, -@@ -129,7 +131,7 @@ class GuestPaymentInformationManagement implements \Magento\Checkout\Api\GuestPa - } - - /** -- * {@inheritDoc} -+ * @inheritdoc - */ - public function savePaymentInformation( - $cartId, -@@ -156,7 +158,7 @@ class GuestPaymentInformationManagement implements \Magento\Checkout\Api\GuestPa - } - - /** -- * {@inheritDoc} -+ * @inheritdoc - */ - public function getPaymentInformation($cartId) - { -@@ -190,9 +192,8 @@ class GuestPaymentInformationManagement implements \Magento\Checkout\Api\GuestPa - { - $shippingAddress = $quote->getShippingAddress(); - if ($shippingAddress && $shippingAddress->getShippingMethod()) { -- $shippingDataArray = explode('_', $shippingAddress->getShippingMethod()); -- $shippingCarrier = array_shift($shippingDataArray); -- $shippingAddress->setLimitCarrier($shippingCarrier); -+ $shippingRate = $shippingAddress->getShippingRateByCode($shippingAddress->getShippingMethod()); -+ $shippingAddress->setLimitCarrier($shippingRate->getCarrier()); - } - } - } -diff --git a/app/code/Magento/Checkout/Model/Layout/AbstractTotalsProcessor.php b/app/code/Magento/Checkout/Model/Layout/AbstractTotalsProcessor.php -index 12a8838a7e9..ca577ed714a 100644 ---- a/app/code/Magento/Checkout/Model/Layout/AbstractTotalsProcessor.php -+++ b/app/code/Magento/Checkout/Model/Layout/AbstractTotalsProcessor.php -@@ -3,9 +3,11 @@ - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -+ - namespace Magento\Checkout\Model\Layout; - - use Magento\Framework\App\Config\ScopeConfigInterface; -+use Magento\Store\Model\ScopeInterface; - - /** - * Abstract totals processor. -@@ -13,6 +15,7 @@ use Magento\Framework\App\Config\ScopeConfigInterface; - * Can be used to process totals information that will be rendered during checkout. - * Abstract class provides sorting routing to sort total information based on configuration settings. - * -+ * phpcs:disable Magento2.Classes.AbstractApi - * @api - */ - abstract class AbstractTotalsProcessor -@@ -35,12 +38,14 @@ abstract class AbstractTotalsProcessor - } - - /** -+ * Sort total information based on configuration settings. -+ * - * @param array $totals - * @return array - */ - public function sortTotals($totals) - { -- $configData = $this->scopeConfig->getValue('sales/totals_sort'); -+ $configData = $this->scopeConfig->getValue('sales/totals_sort', ScopeInterface::SCOPE_STORES); - foreach ($totals as $code => &$total) { - //convert JS naming style to config naming style - $code = str_replace('-', '_', $code); -diff --git a/app/code/Magento/Checkout/Model/PaymentInformationManagement.php b/app/code/Magento/Checkout/Model/PaymentInformationManagement.php -index 164109177d4..2eced5c6422 100644 ---- a/app/code/Magento/Checkout/Model/PaymentInformationManagement.php -+++ b/app/code/Magento/Checkout/Model/PaymentInformationManagement.php -@@ -9,6 +9,8 @@ namespace Magento\Checkout\Model; - use Magento\Framework\Exception\CouldNotSaveException; - - /** -+ * Payment information management -+ * - * @SuppressWarnings(PHPMD.CouplingBetweenObjects) - */ - class PaymentInformationManagement implements \Magento\Checkout\Api\PaymentInformationManagementInterface -@@ -72,7 +74,7 @@ class PaymentInformationManagement implements \Magento\Checkout\Api\PaymentInfor - } - - /** -- * {@inheritDoc} -+ * @inheritdoc - */ - public function savePaymentInformationAndPlaceOrder( - $cartId, -@@ -98,7 +100,7 @@ class PaymentInformationManagement implements \Magento\Checkout\Api\PaymentInfor - } - - /** -- * {@inheritDoc} -+ * @inheritdoc - */ - public function savePaymentInformation( - $cartId, -@@ -110,14 +112,21 @@ class PaymentInformationManagement implements \Magento\Checkout\Api\PaymentInfor - $quoteRepository = $this->getCartRepository(); - /** @var \Magento\Quote\Model\Quote $quote */ - $quote = $quoteRepository->getActive($cartId); -+ $customerId = $quote->getBillingAddress() -+ ->getCustomerId(); -+ if (!$billingAddress->getCustomerId() && $customerId) { -+ //It's necessary to verify the price rules with the customer data -+ $billingAddress->setCustomerId($customerId); -+ } - $quote->removeAddress($quote->getBillingAddress()->getId()); - $quote->setBillingAddress($billingAddress); - $quote->setDataChanges(true); - $shippingAddress = $quote->getShippingAddress(); - if ($shippingAddress && $shippingAddress->getShippingMethod()) { -- $shippingDataArray = explode('_', $shippingAddress->getShippingMethod()); -- $shippingCarrier = array_shift($shippingDataArray); -- $shippingAddress->setLimitCarrier($shippingCarrier); -+ $shippingRate = $shippingAddress->getShippingRateByCode($shippingAddress->getShippingMethod()); -+ $shippingAddress->setLimitCarrier( -+ $shippingRate ? $shippingRate->getCarrier() : $shippingAddress->getShippingMethod() -+ ); - } - } - $this->paymentMethodManagement->set($cartId, $paymentMethod); -@@ -125,7 +134,7 @@ class PaymentInformationManagement implements \Magento\Checkout\Api\PaymentInfor - } - - /** -- * {@inheritDoc} -+ * @inheritdoc - */ - public function getPaymentInformation($cartId) - { -diff --git a/app/code/Magento/Checkout/Model/ShippingInformationManagement.php b/app/code/Magento/Checkout/Model/ShippingInformationManagement.php -index 381ee2b9015..499e02d681f 100644 ---- a/app/code/Magento/Checkout/Model/ShippingInformationManagement.php -+++ b/app/code/Magento/Checkout/Model/ShippingInformationManagement.php -@@ -19,6 +19,9 @@ use Magento\Quote\Model\ShippingFactory; - use Magento\Framework\App\ObjectManager; - - /** -+ * Class ShippingInformationManagement -+ * -+ * @package Magento\Checkout\Model - * @SuppressWarnings(PHPMD.CouplingBetweenObjects) - */ - class ShippingInformationManagement implements \Magento\Checkout\Api\ShippingInformationManagementInterface -@@ -99,8 +102,8 @@ class ShippingInformationManagement implements \Magento\Checkout\Api\ShippingInf - * @param \Magento\Customer\Api\AddressRepositoryInterface $addressRepository - * @param \Magento\Framework\App\Config\ScopeConfigInterface $scopeConfig - * @param \Magento\Quote\Model\Quote\TotalsCollector $totalsCollector -- * @param CartExtensionFactory|null $cartExtensionFactory, -- * @param ShippingAssignmentFactory|null $shippingAssignmentFactory, -+ * @param CartExtensionFactory|null $cartExtensionFactory -+ * @param ShippingAssignmentFactory|null $shippingAssignmentFactory - * @param ShippingFactory|null $shippingFactory - * @SuppressWarnings(PHPMD.ExcessiveParameterList) - */ -@@ -136,7 +139,14 @@ class ShippingInformationManagement implements \Magento\Checkout\Api\ShippingInf - } - - /** -- * {@inheritDoc} -+ * Save address information. -+ * -+ * @param int $cartId -+ * @param \Magento\Checkout\Api\Data\ShippingInformationInterface $addressInformation -+ * @return \Magento\Checkout\Api\Data\PaymentDetailsInterface -+ * @throws InputException -+ * @throws NoSuchEntityException -+ * @throws StateException - */ - public function saveAddressInformation( - $cartId, -@@ -151,6 +161,10 @@ class ShippingInformationManagement implements \Magento\Checkout\Api\ShippingInf - $address->setCustomerAddressId(null); - } - -+ if ($billingAddress && !$billingAddress->getCustomerAddressId()) { -+ $billingAddress->setCustomerAddressId(null); -+ } -+ - if (!$address->getCountryId()) { - throw new StateException(__('The shipping address is missing. Set the address and try again.')); - } -@@ -177,7 +191,9 @@ class ShippingInformationManagement implements \Magento\Checkout\Api\ShippingInf - - $shippingAddress = $quote->getShippingAddress(); - -- if (!$shippingAddress->getShippingRateByCode($shippingAddress->getShippingMethod())) { -+ if (!$quote->getIsVirtual() -+ && !$shippingAddress->getShippingRateByCode($shippingAddress->getShippingMethod()) -+ ) { - throw new NoSuchEntityException( - __('Carrier with such method not found: %1, %2', $carrierCode, $methodCode) - ); -@@ -208,6 +224,8 @@ class ShippingInformationManagement implements \Magento\Checkout\Api\ShippingInf - } - - /** -+ * 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 d926e33d541..6bc7965ff5e 100644 ---- a/app/code/Magento/Checkout/Observer/SalesQuoteSaveAfterObserver.php -+++ b/app/code/Magento/Checkout/Observer/SalesQuoteSaveAfterObserver.php -@@ -7,6 +7,9 @@ namespace Magento\Checkout\Observer; - - use Magento\Framework\Event\ObserverInterface; - -+/** -+ * Class SalesQuoteSaveAfterObserver -+ */ - class SalesQuoteSaveAfterObserver implements ObserverInterface - { - /** -@@ -24,15 +27,18 @@ class SalesQuoteSaveAfterObserver implements ObserverInterface - } - - /** -+ * Assign quote to session -+ * - * @param \Magento\Framework\Event\Observer $observer - * @return void - */ - public function execute(\Magento\Framework\Event\Observer $observer) - { -+ /* @var \Magento\Quote\Model\Quote $quote */ - $quote = $observer->getEvent()->getQuote(); -- /* @var $quote \Magento\Quote\Model\Quote */ -+ - if ($quote->getIsCheckoutCart()) { -- $this->checkoutSession->getQuoteId($quote->getId()); -+ $this->checkoutSession->setQuoteId($quote->getId()); - } - } - } -diff --git a/app/code/Magento/Checkout/Plugin/Model/Quote/ResetQuoteAddresses.php b/app/code/Magento/Checkout/Plugin/Model/Quote/ResetQuoteAddresses.php -index 3791b28917e..e7776b3dcbe 100644 ---- a/app/code/Magento/Checkout/Plugin/Model/Quote/ResetQuoteAddresses.php -+++ b/app/code/Magento/Checkout/Plugin/Model/Quote/ResetQuoteAddresses.php -@@ -15,10 +15,11 @@ use Magento\Quote\Model\Quote; - class ResetQuoteAddresses - { - /** -+ * Clears the quote addresses when all the items are removed from the cart -+ * - * @param Quote $quote - * @param Quote $result - * @param mixed $itemId -- * - * @return Quote - * @SuppressWarnings(PHPMD.UnusedFormalParameter) - */ -@@ -28,6 +29,10 @@ class ResetQuoteAddresses - foreach ($result->getAllAddresses() as $address) { - $result->removeAddress($address->getId()); - } -+ $extensionAttributes = $result->getExtensionAttributes(); -+ if (!$result->isVirtual() && $extensionAttributes && $extensionAttributes->getShippingAssignments()) { -+ $extensionAttributes->setShippingAssignments([]); -+ } - } - - return $result; -diff --git a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/AdminCheckoutActionGroup.xml b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/AdminCheckoutActionGroup.xml -new file mode 100644 -index 00000000000..a29564b2457 ---- /dev/null -+++ b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/AdminCheckoutActionGroup.xml -@@ -0,0 +1,18 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> -+ <!-- Checkout select Check/Money billing method --> -+ <actionGroup name="AdminCheckoutSelectCheckMoneyOrderBillingMethodActionGroup"> -+ <waitForLoadingMaskToDisappear stepKey="waitForLoadingMask"/> -+ <waitForPageLoad stepKey="waitForPageLoad"/> -+ <conditionalClick selector="{{AdminCheckoutPaymentSection.checkBillingMethodByName('Check / Money order')}}" dependentSelector="{{AdminCheckoutPaymentSection.checkBillingMethodByName('Check / Money order')}}" visible="true" stepKey="selectCheckmoBillingMethod"/> -+ <waitForLoadingMaskToDisappear stepKey="waitForLoadingMaskAfterBillingMethodSelection"/> -+ </actionGroup> -+</actionGroups> -diff --git a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/AssertAdminEmailValidationMessageOnCheckoutActionGroup.xml b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/AssertAdminEmailValidationMessageOnCheckoutActionGroup.xml -new file mode 100644 -index 00000000000..858dbfc5e76 ---- /dev/null -+++ b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/AssertAdminEmailValidationMessageOnCheckoutActionGroup.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="AssertAdminEmailValidationMessageOnCheckoutActionGroup"> -+ <arguments> -+ <argument name="message" type="string" defaultValue="Please enter a valid email address (Ex: johndoe@domain.com)."/> -+ </arguments> -+ <waitForElementVisible selector="{{AdminOrderFormAccountSection.emailErrorMessage}}" stepKey="waitForFormValidation"/> -+ <see selector="{{AdminOrderFormAccountSection.emailErrorMessage}}" userInput="{{message}}" stepKey="seeTheErrorMessageIsDisplayed"/> -+ </actionGroup> -+</actionGroups> -diff --git a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/AssertMiniShoppingCartSubTotalActionGroup.xml b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/AssertMiniShoppingCartSubTotalActionGroup.xml -new file mode 100644 -index 00000000000..8c5c6f41fff ---- /dev/null -+++ b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/AssertMiniShoppingCartSubTotalActionGroup.xml -@@ -0,0 +1,25 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> -+ <actionGroup name="AssertMiniShoppingCartSubTotalActionGroup"> -+ <arguments> -+ <argument name="dataQuote" type="entity" /> -+ </arguments> -+ <waitForPageLoad stepKey="waitForPageLoad" time="120"/> -+ <grabTextFrom selector="{{StorefrontMinicartSection.miniCartSubtotalField}}" stepKey="grabMiniCartTotal" /> -+ <assertContains stepKey="assertMiniCartTotal"> -+ <actualResult type="variable">$grabMiniCartTotal</actualResult> -+ <expectedResult type="string">{{dataQuote.subtotal}}</expectedResult> -+ </assertContains> -+ <assertContains stepKey="assertMiniCartCurrency"> -+ <actualResult type="variable">$grabMiniCartTotal</actualResult> -+ <expectedResult type="string">{{dataQuote.currency}}</expectedResult> -+ </assertContains> -+ </actionGroup> -+</actionGroups> -diff --git a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/AssertShoppingCartIsEmptyActionGroup.xml b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/AssertShoppingCartIsEmptyActionGroup.xml -new file mode 100644 -index 00000000000..d71832c63cb ---- /dev/null -+++ b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/AssertShoppingCartIsEmptyActionGroup.xml -@@ -0,0 +1,15 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> -+ <actionGroup name="AssertShoppingCartIsEmptyActionGroup"> -+ <amOnPage url="{{CheckoutCartPage.url}}" stepKey="amOnPageShoppingCart"/> -+ <waitForPageLoad stepKey="waitForCheckoutPageLoad"/> -+ <see userInput="You have no items in your shopping cart." stepKey="seeNoItemsInShoppingCart"/> -+ </actionGroup> -+</actionGroups> -diff --git a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/AssertStorefronElementVisibleActionGroup.xml b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/AssertStorefronElementVisibleActionGroup.xml -new file mode 100644 -index 00000000000..b99c4b7f5bf ---- /dev/null -+++ b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/AssertStorefronElementVisibleActionGroup.xml -@@ -0,0 +1,17 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> -+ <actionGroup name="AssertStorefrontElementVisibleActionGroup"> -+ <arguments> -+ <argument name="selector" type="string"/> -+ <argument name="userInput" type="string"/> -+ </arguments> -+ <see selector="{{selector}}" userInput="{{userInput}}" stepKey="assertElement"/> -+ </actionGroup> -+</actionGroups> -diff --git a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/AssertStorefrontCheckoutCartItemsActionGroup.xml b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/AssertStorefrontCheckoutCartItemsActionGroup.xml -new file mode 100644 -index 00000000000..77a8bef9baf ---- /dev/null -+++ b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/AssertStorefrontCheckoutCartItemsActionGroup.xml -@@ -0,0 +1,23 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> -+ <actionGroup name="AssertStorefrontCheckoutCartItemsActionGroup"> -+ <arguments> -+ <argument name="productName" type="string"/> -+ <argument name="productSku" type="string"/> -+ <argument name="productPrice" type="string"/> -+ <argument name="subtotal" type="string"/> -+ <argument name="qty" type="string"/> -+ </arguments> -+ <see selector="{{CheckoutCartProductSection.productName}}" userInput="{{productName}}" stepKey="seeProductNameInCheckoutSummary"/> -+ <see selector="{{CheckoutCartProductSection.checkoutCartProductPrice}}" userInput="{{productPrice}}" stepKey="seeProductPriceInCart"/> -+ <see selector="{{CheckoutCartProductSection.checkoutCartSubtotal}}" userInput="{{subtotal}}" stepKey="seeSubtotalPrice"/> -+ <seeInField selector="{{CheckoutCartProductSection.qtyByContains(productSku)}}" userInput="{{qty}}" stepKey="seeProductQuantity"/> -+ </actionGroup> -+</actionGroups> -diff --git a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/AssertStorefrontElementInvisibleActionGroup.xml b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/AssertStorefrontElementInvisibleActionGroup.xml -new file mode 100644 -index 00000000000..69fe27ff074 ---- /dev/null -+++ b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/AssertStorefrontElementInvisibleActionGroup.xml -@@ -0,0 +1,17 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> -+ <actionGroup name="AssertStorefrontElementInvisibleActionGroup"> -+ <arguments> -+ <argument name="selector" type="string"/> -+ <argument name="userInput" type="string"/> -+ </arguments> -+ <dontSee selector="{{selector}}" userInput="{{userInput}}" stepKey="dontSeeElement"/> -+ </actionGroup> -+</actionGroups> -diff --git a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/AssertStorefrontEmailNoteMessageOnCheckoutActionGroup.xml b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/AssertStorefrontEmailNoteMessageOnCheckoutActionGroup.xml -new file mode 100644 -index 00000000000..c4fc753e737 ---- /dev/null -+++ b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/AssertStorefrontEmailNoteMessageOnCheckoutActionGroup.xml -@@ -0,0 +1,18 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> -+ <actionGroup name="AssertStorefrontEmailNoteMessageOnCheckoutActionGroup"> -+ <arguments> -+ <argument name="message" type="string" defaultValue="You can create an account after checkout." /> -+ </arguments> -+ <waitForElementVisible selector="{{StorefrontCheckoutCheckoutCustomerLoginSection.emailNoteMessage}}" stepKey="waitForFormValidation"/> -+ <see selector="{{StorefrontCheckoutCheckoutCustomerLoginSection.emailNoteMessage}}" userInput="{{message}}" stepKey="seeTheNoteMessageIsDisplayed"/> -+ </actionGroup> -+</actionGroups> -diff --git a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/AssertStorefrontEmailTooltipContentOnCheckoutActionGroup.xml b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/AssertStorefrontEmailTooltipContentOnCheckoutActionGroup.xml -new file mode 100644 -index 00000000000..f9c6771262c ---- /dev/null -+++ b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/AssertStorefrontEmailTooltipContentOnCheckoutActionGroup.xml -@@ -0,0 +1,19 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> -+ <actionGroup name="AssertStorefrontEmailTooltipContentOnCheckoutActionGroup"> -+ <arguments> -+ <argument name="content" type="string" defaultValue="We'll send your order confirmation here." /> -+ </arguments> -+ <waitForElementVisible selector="{{StorefrontCheckoutCheckoutCustomerLoginSection.emailTooltipButton}}" stepKey="waitForTooltipButtonVisible" /> -+ <click selector="{{StorefrontCheckoutCheckoutCustomerLoginSection.emailTooltipButton}}" stepKey="clickEmailTooltipButton" /> -+ <see selector="{{StorefrontCheckoutCheckoutCustomerLoginSection.emailTooltipContent}}" userInput="{{content}}" stepKey="seeEmailTooltipContent" /> -+ </actionGroup> -+</actionGroups> -diff --git a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/AssertStorefrontEmailValidationMessageOnCheckoutActionGroup.xml b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/AssertStorefrontEmailValidationMessageOnCheckoutActionGroup.xml -new file mode 100644 -index 00000000000..14b96ed46ce ---- /dev/null -+++ b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/AssertStorefrontEmailValidationMessageOnCheckoutActionGroup.xml -@@ -0,0 +1,18 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> -+ <actionGroup name="AssertStorefrontEmailValidationMessageOnCheckoutActionGroup"> -+ <arguments> -+ <argument name="message" type="string" defaultValue="Please enter a valid email address (Ex: johndoe@domain.com)." /> -+ </arguments> -+ <waitForElementVisible selector="{{StorefrontCheckoutCheckoutCustomerLoginSection.emailErrorMessage}}" stepKey="waitForFormValidation"/> -+ <see selector="{{StorefrontCheckoutCheckoutCustomerLoginSection.emailErrorMessage}}" userInput="{{message}}" stepKey="seeTheErrorMessageIsDisplayed"/> -+ </actionGroup> -+</actionGroups> -diff --git a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/AssertStorefrontMiniCartItemsActionGroup.xml b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/AssertStorefrontMiniCartItemsActionGroup.xml -new file mode 100644 -index 00000000000..258d8ed9021 ---- /dev/null -+++ b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/AssertStorefrontMiniCartItemsActionGroup.xml -@@ -0,0 +1,24 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> -+ <actionGroup name="AssertStorefrontMiniCartItemsActionGroup"> -+ <arguments> -+ <argument name="productName" type="string"/> -+ <argument name="productPrice" type="string"/> -+ <argument name="cartSubtotal" type="string"/> -+ <argument name="qty" type="string"/> -+ </arguments> -+ <see selector="{{StorefrontMinicartSection.miniCartItemsText}}" userInput="{{productName}}" stepKey="seeProductNameInMiniCart"/> -+ <see selector="{{StorefrontMinicartSection.miniCartItemsText}}" userInput="{{productPrice}}" stepKey="seeProductPriceInMiniCart"/> -+ <seeElement selector="{{StorefrontMinicartSection.goToCheckout}}" stepKey="seeCheckOutButtonInMiniCart"/> -+ <seeElement selector="{{StorefrontMinicartSection.productQuantity(productName, qty)}}" stepKey="seeProductQuantity1"/> -+ <seeElement selector="{{StorefrontMinicartSection.productImage}}" stepKey="seeProductImage"/> -+ <see selector="{{StorefrontMinicartSection.productSubTotal}}" userInput="{{cartSubtotal}}" stepKey="seeSubTotal"/> -+ </actionGroup> -+</actionGroups> -diff --git a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/AssertStorefrontSeeElementActionGroup.xml b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/AssertStorefrontSeeElementActionGroup.xml -new file mode 100644 -index 00000000000..02f00a8cd31 ---- /dev/null -+++ b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/AssertStorefrontSeeElementActionGroup.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="AssertStorefrontSeeElementActionGroup"> -+ <arguments> -+ <argument name="selector" type="string"/> -+ </arguments> -+ <waitForPageLoad stepKey="waitForPageToLoad"/> -+ <scrollTo selector="{{selector}}" stepKey="scrollToElement"/> -+ <seeElement selector="{{selector}}" stepKey="assertElement"/> -+ </actionGroup> -+</actionGroups> -diff --git a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/AssertStorefrontShoppingCartSummaryItemsActionGroup.xml b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/AssertStorefrontShoppingCartSummaryItemsActionGroup.xml -new file mode 100644 -index 00000000000..9b963273b04 ---- /dev/null -+++ b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/AssertStorefrontShoppingCartSummaryItemsActionGroup.xml -@@ -0,0 +1,23 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> -+ <actionGroup name="AssertStorefrontShoppingCartSummaryItemsActionGroup"> -+ <arguments> -+ <argument name="subtotal" type="string"/> -+ <argument name="total" type="string"/> -+ </arguments> -+ <seeInCurrentUrl url="{{CheckoutCartPage.url}}" stepKey="assertUrl"/> -+ <waitForElementVisible selector="{{CheckoutCartSummarySection.subtotal}}" stepKey="waitForSubtotalVisible"/> -+ <see selector="{{CheckoutCartSummarySection.subtotal}}" userInput="{{subtotal}}" stepKey="assertSubtotal"/> -+ <waitForElementVisible selector="{{CheckoutCartSummarySection.total}}" stepKey="waitForTotalVisible"/> -+ <waitForElementVisible selector="{{CheckoutCartSummarySection.totalAmount(total)}}" stepKey="waitForTotalAmountVisible"/> -+ <see selector="{{CheckoutCartSummarySection.total}}" userInput="{{total}}" stepKey="assertTotal"/> -+ <seeElement selector="{{CheckoutCartSummarySection.proceedToCheckout}}" stepKey="seeProceedToCheckoutButton"/> -+ </actionGroup> -+</actionGroups> -diff --git a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/AssertStorefrontShoppingCartSummaryWithShippingActionGroup.xml b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/AssertStorefrontShoppingCartSummaryWithShippingActionGroup.xml -new file mode 100644 -index 00000000000..7d8406adc90 ---- /dev/null -+++ b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/AssertStorefrontShoppingCartSummaryWithShippingActionGroup.xml -@@ -0,0 +1,17 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> -+ <actionGroup name="AssertStorefrontShoppingCartSummaryWithShippingActionGroup" extends="AssertStorefrontShoppingCartSummaryItemsActionGroup"> -+ <arguments> -+ <argument name="shipping" type="string"/> -+ </arguments> -+ <waitForElementVisible selector="{{CheckoutCartSummarySection.shipping}}" stepKey="waitForElementToBeVisible" after="assertSubtotal"/> -+ <waitForText userInput="{{shipping}}" selector="{{CheckoutCartSummarySection.shipping}}" time="30" stepKey="assertShipping" after="waitForElementToBeVisible"/> -+ </actionGroup> -+</actionGroups> -diff --git a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/CheckoutActionGroup.xml b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/CheckoutActionGroup.xml -index 4c9ef93c2c4..2e4b742ece8 100644 ---- a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/CheckoutActionGroup.xml -+++ b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/CheckoutActionGroup.xml -@@ -7,17 +7,61 @@ - --> - - <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"> -+ <!-- Checkout select Flat Rate shipping method --> -+ <actionGroup name="CheckoutSelectFlatRateShippingMethodActionGroup"> -+ <conditionalClick selector="{{CheckoutShippingMethodsSection.checkShippingMethodByName('Flat Rate')}}" dependentSelector="{{CheckoutShippingMethodsSection.checkShippingMethodByName('Flat Rate')}}" visible="true" stepKey="selectFlatRateShippingMethod"/> -+ <waitForLoadingMaskToDisappear stepKey="waitForLoadingMaskForNextButton"/> -+ </actionGroup> -+ - <!-- Go to checkout from minicart --> - <actionGroup name="GoToCheckoutFromMinicartActionGroup"> -- <wait stepKey="wait" time="10" /> -+ <waitForElementNotVisible selector="{{StorefrontMinicartSection.emptyCart}}" stepKey="waitUpdateQuantity" /> -+ <wait time="5" stepKey="waitMinicartRendering"/> - <click selector="{{StorefrontMinicartSection.showCart}}" stepKey="clickCart"/> - <click selector="{{StorefrontMinicartSection.goToCheckout}}" stepKey="goToCheckout"/> -- <waitForPageLoad stepKey="waitForPageLoad"/> -+ </actionGroup> -+ -+ <!-- Go to checkout from cart --> -+ <actionGroup name="GoToCheckoutFromCartActionGroup"> -+ <waitForElementNotVisible selector="{{StorefrontMinicartSection.emptyCart}}" stepKey="waitUpdateQuantity" /> -+ <click selector="{{StorefrontMinicartSection.showCart}}" stepKey="clickCart"/> -+ <waitForLoadingMaskToDisappear stepKey="waitForLoadingMask"/> -+ <seeInCurrentUrl url="{{CheckoutCartPage.url}}" stepKey="assertCheckoutCartUrl"/> -+ <click selector="{{CheckoutCartSummarySection.proceedToCheckout}}" stepKey="goToCheckout"/> - </actionGroup> - - <!-- Guest checkout filling shipping section --> - <actionGroup name="GuestCheckoutFillingShippingSectionActionGroup"> -+ <arguments> -+ <argument name="customerVar" defaultValue="CustomerEntityOne"/> -+ <argument name="customerAddressVar" defaultValue="CustomerAddressSimple"/> -+ <!--First available shipping method will be selected if value is not passed for shippingMethod--> -+ <argument name="shippingMethod" defaultValue="" type="string"/> -+ </arguments> -+ <fillField selector="{{CheckoutShippingSection.email}}" userInput="{{customerVar.email}}" stepKey="enterEmail"/> -+ <fillField selector="{{CheckoutShippingSection.firstName}}" userInput="{{customerVar.firstname}}" stepKey="enterFirstName"/> -+ <fillField selector="{{CheckoutShippingSection.lastName}}" userInput="{{customerVar.lastname}}" stepKey="enterLastName"/> -+ <fillField selector="{{CheckoutShippingSection.street}}" userInput="{{customerAddressVar.street[0]}}" stepKey="enterStreet"/> -+ <fillField selector="{{CheckoutShippingSection.city}}" userInput="{{customerAddressVar.city}}" stepKey="enterCity"/> -+ <selectOption selector="{{CheckoutShippingSection.region}}" userInput="{{customerAddressVar.state}}" stepKey="selectRegion"/> -+ <fillField selector="{{CheckoutShippingSection.postcode}}" userInput="{{customerAddressVar.postcode}}" stepKey="enterPostcode"/> -+ <fillField selector="{{CheckoutShippingSection.telephone}}" userInput="{{customerAddressVar.telephone}}" stepKey="enterTelephone"/> -+ <waitForLoadingMaskToDisappear stepKey="waitForLoadingMask"/> -+ <click selector="{{CheckoutShippingMethodsSection.checkShippingMethodByName('shippingMethod')}}" stepKey="selectShippingMethod"/> -+ <waitForElement selector="{{CheckoutShippingSection.next}}" time="30" stepKey="waitForNextButton"/> -+ <click selector="{{CheckoutShippingSection.next}}" stepKey="clickNext"/> -+ <waitForElement selector="{{CheckoutPaymentSection.paymentSectionTitle}}" time="30" stepKey="waitForPaymentSectionLoaded"/> -+ <seeInCurrentUrl url="{{CheckoutPage.url}}/#payment" stepKey="assertCheckoutPaymentUrl"/> -+ </actionGroup> -+ -+ <actionGroup name="GuestCheckoutFillShippingNoWaitForPaymentActionGroup" extends="GuestCheckoutFillingShippingSectionActionGroup"> -+ <remove keyForRemoval="waitForPaymentSectionLoaded"/> -+ <remove keyForRemoval="assertCheckoutPaymentUrl"/> -+ </actionGroup> -+ -+ <!-- Guest checkout filling shipping section without region --> -+ <actionGroup name="GuestCheckoutFillingShippingSectionWithoutRegionActionGroup"> - <arguments> - <argument name="customerVar"/> - <argument name="customerAddressVar"/> -@@ -27,8 +71,8 @@ - <fillField selector="{{CheckoutShippingSection.lastName}}" userInput="{{customerVar.lastname}}" stepKey="enterLastName"/> - <fillField selector="{{CheckoutShippingSection.street}}" userInput="{{customerAddressVar.street[0]}}" stepKey="enterStreet"/> - <fillField selector="{{CheckoutShippingSection.city}}" userInput="{{customerAddressVar.city}}" stepKey="enterCity"/> -- <selectOption selector="{{CheckoutShippingSection.region}}" userInput="{{customerAddressVar.state}}" stepKey="selectRegion"/> - <fillField selector="{{CheckoutShippingSection.postcode}}" userInput="{{customerAddressVar.postcode}}" stepKey="enterPostcode"/> -+ <selectOption selector="{{CheckoutShippingSection.country}}" userInput="{{customerAddressVar.country_id}}" stepKey="enterCountry"/> - <fillField selector="{{CheckoutShippingSection.telephone}}" userInput="{{customerAddressVar.telephone}}" stepKey="enterTelephone"/> - <waitForLoadingMaskToDisappear stepKey="waitForLoadingMask"/> - <click selector="{{CheckoutShippingSection.firstShippingMethod}}" stepKey="selectFirstShippingMethod"/> -@@ -38,8 +82,59 @@ - <seeInCurrentUrl url="{{CheckoutPage.url}}/#payment" stepKey="assertCheckoutPaymentUrl"/> - </actionGroup> - -+ <!-- Guest checkout filling shipping section with unavailable payments--> -+ <actionGroup name="GuestCheckoutFillingShippingSectionUnavailablePaymentActionGroup"> -+ <arguments> -+ <argument name="customerVar"/> -+ <argument name="customerAddressVar"/> -+ </arguments> -+ <fillField selector="{{CheckoutShippingSection.email}}" userInput="{{customerVar.email}}" stepKey="enterEmail"/> -+ <fillField selector="{{CheckoutShippingSection.firstName}}" userInput="{{customerVar.firstname}}" stepKey="enterFirstName"/> -+ <fillField selector="{{CheckoutShippingSection.lastName}}" userInput="{{customerVar.lastname}}" stepKey="enterLastName"/> -+ <fillField selector="{{CheckoutShippingSection.street}}" userInput="{{customerAddressVar.street[0]}}" stepKey="enterStreet"/> -+ <fillField selector="{{CheckoutShippingSection.city}}" userInput="{{customerAddressVar.city}}" stepKey="enterCity"/> -+ <selectOption selector="{{CheckoutShippingSection.region}}" userInput="{{customerAddressVar.state}}" stepKey="selectRegion"/> -+ <fillField selector="{{CheckoutShippingSection.postcode}}" userInput="{{customerAddressVar.postcode}}" stepKey="enterPostcode"/> -+ <fillField selector="{{CheckoutShippingSection.telephone}}" userInput="{{customerAddressVar.telephone}}" stepKey="enterTelephone"/> -+ <waitForLoadingMaskToDisappear stepKey="waitForLoadingMask"/> -+ <click selector="{{CheckoutShippingSection.firstShippingMethod}}" stepKey="selectFirstShippingMethod"/> -+ <waitForElement selector="{{CheckoutShippingSection.next}}" time="30" stepKey="waitForNextButton"/> -+ <click selector="{{CheckoutShippingSection.next}}" stepKey="clickNext"/> -+ <seeInCurrentUrl url="{{CheckoutPage.url}}/#payment" stepKey="assertCheckoutPaymentUrl"/> -+ <waitForElementVisible selector="{{CheckoutPaymentSection.noQuotes}}" stepKey="waitMessage"/> -+ <see userInput="No Payment method available." stepKey="checkMessage"/> -+ </actionGroup> -+ -+ <actionGroup name="GuestCheckoutWithSpecificCountryOptionForPaymentMethodActionGroup" extends="GuestCheckoutFillingShippingSectionUnavailablePaymentActionGroup"> -+ <arguments> -+ <argument name="paymentMethod" type="string"/> -+ </arguments> -+ <remove keyForRemoval="checkMessage"/> -+ <dontSee selector="{{CheckoutPaymentSection.paymentMethodByName(paymentMethod)}}" stepKey="paymentMethodDoesNotAvailable"/> -+ </actionGroup> - <!-- Logged in user checkout filling shipping section --> - <actionGroup name="LoggedInUserCheckoutFillingShippingSectionActionGroup"> -+ <arguments> -+ <argument name="customerVar" defaultValue="CustomerEntityOne"/> -+ <argument name="customerAddressVar" defaultValue="CustomerAddressSimple"/> -+ </arguments> -+ <fillField selector="{{CheckoutShippingSection.firstName}}" userInput="{{customerVar.firstname}}" stepKey="enterFirstName"/> -+ <fillField selector="{{CheckoutShippingSection.lastName}}" userInput="{{customerVar.lastname}}" stepKey="enterLastName"/> -+ <fillField selector="{{CheckoutShippingSection.street}}" userInput="{{customerAddressVar.street[0]}}" stepKey="enterStreet"/> -+ <fillField selector="{{CheckoutShippingSection.city}}" userInput="{{customerAddressVar.city}}" stepKey="enterCity"/> -+ <selectOption selector="{{CheckoutShippingSection.region}}" userInput="{{customerAddressVar.state}}" stepKey="selectRegion"/> -+ <fillField selector="{{CheckoutShippingSection.postcode}}" userInput="{{customerAddressVar.postcode}}" stepKey="enterPostcode"/> -+ <fillField selector="{{CheckoutShippingSection.telephone}}" userInput="{{customerAddressVar.telephone}}" stepKey="enterTelephone"/> -+ <waitForLoadingMaskToDisappear stepKey="waitForLoadingMask"/> -+ <click selector="{{CheckoutShippingSection.firstShippingMethod}}" stepKey="selectFirstShippingMethod"/> -+ <waitForElement selector="{{CheckoutShippingSection.next}}" time="30" stepKey="waitForNextButton"/> -+ <click selector="{{CheckoutShippingSection.next}}" stepKey="clickNext"/> -+ <waitForElement selector="{{CheckoutPaymentSection.paymentSectionTitle}}" time="30" stepKey="waitForPaymentSectionLoaded"/> -+ <seeInCurrentUrl url="{{CheckoutPage.url}}/#payment" stepKey="assertCheckoutPaymentUrl"/> -+ </actionGroup> -+ -+ <!-- Logged in user checkout filling shipping section --> -+ <actionGroup name="LoggedInUserCheckoutAddNewShippingSectionWithoutRegionActionGroup"> - <arguments> - <argument name="customerVar"/> - <argument name="customerAddressVar"/> -@@ -48,9 +143,10 @@ - <fillField selector="{{CheckoutShippingSection.lastName}}" userInput="{{customerVar.lastname}}" stepKey="enterLastName"/> - <fillField selector="{{CheckoutShippingSection.street}}" userInput="{{customerAddressVar.street[0]}}" stepKey="enterStreet"/> - <fillField selector="{{CheckoutShippingSection.city}}" userInput="{{customerAddressVar.city}}" stepKey="enterCity"/> -- <selectOption selector="{{CheckoutShippingSection.region}}" userInput="{{customerAddressVar.state}}" stepKey="selectRegion"/> - <fillField selector="{{CheckoutShippingSection.postcode}}" userInput="{{customerAddressVar.postcode}}" stepKey="enterPostcode"/> -+ <selectOption selector="{{CheckoutShippingSection.country}}" userInput="{{customerAddressVar.country_id}}" stepKey="enterCountry"/> - <fillField selector="{{CheckoutShippingSection.telephone}}" userInput="{{customerAddressVar.telephone}}" stepKey="enterTelephone"/> -+ <click selector="{{CheckoutShippingSection.saveAddress}}" stepKey="clickSaveAddress"/> - <waitForLoadingMaskToDisappear stepKey="waitForLoadingMask"/> - <click selector="{{CheckoutShippingSection.firstShippingMethod}}" stepKey="selectFirstShippingMethod"/> - <waitForElement selector="{{CheckoutShippingSection.next}}" time="30" stepKey="waitForNextButton"/> -@@ -59,6 +155,24 @@ - <seeInCurrentUrl url="{{CheckoutPage.url}}/#payment" stepKey="assertCheckoutPaymentUrl"/> - </actionGroup> - -+ <!-- Place order with logged the user --> -+ <actionGroup name="PlaceOrderWithLoggedUserActionGroup"> -+ <arguments> -+ <!--First available shipping method will be selected if value is not passed for shippingMethod--> -+ <argument name="shippingMethod" defaultValue="" type="string"/> -+ </arguments> -+ <waitForElementVisible selector="{{CheckoutCartSummarySection.proceedToCheckout}}" stepKey="waitProceedToCheckout"/> -+ <click selector="{{CheckoutCartSummarySection.proceedToCheckout}}" stepKey="clickProceedToCheckout"/> -+ <click selector="{{CheckoutShippingMethodsSection.checkShippingMethodByName('shippingMethod')}}" stepKey="selectShippingMethod"/> -+ <waitForElement selector="{{CheckoutShippingSection.next}}" stepKey="waitForNextButton"/> -+ <click selector="{{CheckoutShippingSection.next}}" stepKey="clickNext"/> -+ <waitForElement selector="{{CheckoutPaymentSection.paymentSectionTitle}}" stepKey="waitForPaymentSectionLoaded"/> -+ <seeInCurrentUrl url="{{CheckoutPage.url}}/#payment" stepKey="assertCheckoutPaymentUrl"/> -+ <waitForElementVisible selector="{{CheckoutPaymentSection.placeOrder}}" stepKey="waitForPlaceOrderButton"/> -+ <click selector="{{CheckoutPaymentSection.placeOrder}}" stepKey="clickPlaceOrder"/> -+ <see selector="{{CheckoutSuccessMainSection.successTitle}}" userInput="Thank you for your purchase!" stepKey="waitForLoadSuccessPage"/> -+ </actionGroup> -+ - <!-- Check product in checkout cart items --> - <actionGroup name="CheckProductInCheckoutCartItemsActionGroup"> - <arguments> -@@ -74,18 +188,26 @@ - <!-- Check order summary in checkout --> - <actionGroup name="CheckOrderSummaryInCheckoutActionGroup"> - <arguments> -- <argument name="subtotal"/> -- <argument name="shippingTotal"/> -- <argument name="shippingMethod"/> -- <argument name="total"/> -+ <argument name="subtotal" type="string"/> -+ <argument name="shippingTotal" type="string"/> -+ <argument name="shippingMethod" type="string"/> -+ <argument name="total" type="string"/> - </arguments> - <waitForElement selector="{{CheckoutPaymentSection.paymentSectionTitle}}" time="30" stepKey="waitForPaymentSectionLoaded"/> -- <see userInput="${{subtotal}}" selector="{{CheckoutPaymentSection.orderSummarySubtotal}}" stepKey="assertSubtotal"/> -- <see userInput="${{shippingTotal}}" selector="{{CheckoutPaymentSection.orderSummaryShippingTotal}}" stepKey="assertShipping"/> -+ <see userInput="{{subtotal}}" selector="{{CheckoutPaymentSection.orderSummarySubtotal}}" stepKey="assertSubtotal"/> -+ <see userInput="{{shippingTotal}}" selector="{{CheckoutPaymentSection.orderSummaryShippingTotal}}" stepKey="assertShipping"/> - <see userInput="{{shippingMethod}}" selector="{{CheckoutPaymentSection.orderSummaryShippingMethod}}" stepKey="assertShippingMethod"/> -- <see userInput="${{total}}" selector="{{CheckoutPaymentSection.orderSummaryTotal}}" stepKey="assertTotal"/> -+ <see userInput="{{total}}" selector="{{CheckoutPaymentSection.orderSummaryTotal}}" stepKey="assertTotal"/> - </actionGroup> - -+ <actionGroup name="CheckTotalsSortOrderInSummarySection"> -+ <arguments> -+ <argument name="elementName" type="string"/> -+ <argument name="positionNumber" type="string"/> -+ </arguments> -+ <see userInput="{{elementName}}" selector="{{CheckoutCartSummarySection.elementPosition(positionNumber)}}" stepKey="assertElementPosition"/> -+ </actionGroup> -+ - <!-- Check ship to information in checkout --> - <actionGroup name="CheckShipToInformationInCheckoutActionGroup"> - <arguments> -@@ -113,8 +235,25 @@ - - <!-- Checkout select Check/Money Order payment --> - <actionGroup name="CheckoutSelectCheckMoneyOrderPaymentActionGroup"> -- <waitForElement selector="{{CheckoutPaymentSection.paymentSectionTitle}}" time="30" stepKey="waitForPaymentSectionLoaded"/> -- <conditionalClick selector="{{CheckoutPaymentSection.checkMoneyOrderPayment}}" dependentSelector="{{CheckoutPaymentSection.billingAddress}}" visible="false" stepKey="clickCheckMoneyOrderPayment" /> -+ <waitForLoadingMaskToDisappear stepKey="waitForLoadingMask"/> -+ <waitForPageLoad stepKey="waitForPageLoad"/> -+ <conditionalClick selector="{{StorefrontCheckoutPaymentMethodSection.checkPaymentMethodByName('Check / Money order')}}" dependentSelector="{{StorefrontCheckoutPaymentMethodSection.checkPaymentMethodByName('Check / Money order')}}" visible="true" stepKey="selectCheckmoPaymentMethod"/> -+ <waitForLoadingMaskToDisappear stepKey="waitForLoadingMaskAfterPaymentMethodSelection"/> -+ </actionGroup> -+ -+ <!-- Check selected shipping address information on shipping information step --> -+ <actionGroup name="CheckSelectedShippingAddressInCheckoutActionGroup"> -+ <arguments> -+ <argument name="customerVar"/> -+ <argument name="customerAddressVar"/> -+ </arguments> -+ <waitForElement selector="{{CheckoutShippingSection.shippingTab}}" time="30" stepKey="waitForShippingSectionLoaded"/> -+ <see stepKey="VerifyFirstNameInSelectedAddress" selector="{{CheckoutShippingSection.selectedShippingAddress}}" userInput="{{customerVar.firstname}}" /> -+ <see stepKey="VerifyLastNameInSelectedAddress" selector="{{CheckoutShippingSection.selectedShippingAddress}}" userInput="{{customerVar.lastname}}" /> -+ <see stepKey="VerifyStreetInSelectedAddress" selector="{{CheckoutShippingSection.selectedShippingAddress}}" userInput="{{customerAddressVar.street[0]}}" /> -+ <see stepKey="VerifyCityInSelectedAddress" selector="{{CheckoutShippingSection.selectedShippingAddress}}" userInput="{{customerAddressVar.city}}" /> -+ <see stepKey="VerifyZipInSelectedAddress" selector="{{CheckoutShippingSection.selectedShippingAddress}}" userInput="{{customerAddressVar.postcode}}" /> -+ <see stepKey="VerifyPhoneInSelectedAddress" selector="{{CheckoutShippingSection.selectedShippingAddress}}" userInput="{{customerAddressVar.telephone}}" /> - </actionGroup> - - <!-- Check billing address in checkout --> -@@ -133,16 +272,55 @@ - <see userInput="{{customerAddressVar.telephone}}" selector="{{CheckoutPaymentSection.billingAddress}}" stepKey="assertBillingAddressTelephone"/> - </actionGroup> - -+ <!-- Check billing address in checkout with billing address on payment page --> -+ <actionGroup name="CheckBillingAddressInCheckoutWithBillingAddressOnPaymentPageActionGroup"> -+ <arguments> -+ <argument name="customerVar"/> -+ <argument name="customerAddressVar"/> -+ </arguments> -+ <waitForElement selector="{{CheckoutPaymentSection.paymentSectionTitle}}" time="30" stepKey="waitForPaymentSectionLoaded"/> -+ <see userInput="{{customerVar.firstName}}" selector="{{CheckoutPaymentWithDisplayBillingAddressOnPaymentPageSection.billingAddressDetails}}" stepKey="assertBillingAddressDetailsFirstName"/> -+ <see userInput="{{customerVar.lastName}}" selector="{{CheckoutPaymentWithDisplayBillingAddressOnPaymentPageSection.billingAddressDetails}}" stepKey="assertBillingAddressDetailsLastName"/> -+ <see userInput="{{customerAddressVar.street[0]}}" selector="{{CheckoutPaymentWithDisplayBillingAddressOnPaymentPageSection.billingAddressDetails}}" stepKey="assertBillingAddressDetailsStreet"/> -+ <see userInput="{{customerAddressVar.city}}" selector="{{CheckoutPaymentWithDisplayBillingAddressOnPaymentPageSection.billingAddressDetails}}" stepKey="assertBillingAddressDetailsCity"/> -+ <see userInput="{{customerAddressVar.state}}" selector="{{CheckoutPaymentWithDisplayBillingAddressOnPaymentPageSection.billingAddressDetails}}" stepKey="assertBillingAddressDetailsState"/> -+ <see userInput="{{customerAddressVar.postcode}}" selector="{{CheckoutPaymentWithDisplayBillingAddressOnPaymentPageSection.billingAddressDetails}}" stepKey="assertBillingAddressDetailsPostcode"/> -+ <see userInput="{{customerAddressVar.telephone}}" selector="{{CheckoutPaymentWithDisplayBillingAddressOnPaymentPageSection.billingAddressDetails}}" stepKey="assertBillingAddressDetailsTelephone"/> -+ </actionGroup> -+ - <!-- Checkout place order --> - <actionGroup name="CheckoutPlaceOrderActionGroup"> - <arguments> - <argument name="orderNumberMessage"/> - <argument name="emailYouMessage"/> - </arguments> -- <waitForElement selector="{{CheckoutPaymentSection.placeOrder}}" time="30" stepKey="waitForPlaceOrderButton"/> -+ <waitForElementVisible selector="{{CheckoutPaymentSection.placeOrder}}" time="30" stepKey="waitForPlaceOrderButton"/> - <click selector="{{CheckoutPaymentSection.placeOrder}}" stepKey="clickPlaceOrder"/> - <see selector="{{CheckoutSuccessMainSection.success}}" userInput="{{orderNumberMessage}}" stepKey="seeOrderNumber"/> - <see selector="{{CheckoutSuccessMainSection.success}}" userInput="{{emailYouMessage}}" stepKey="seeEmailYou"/> - </actionGroup> - --</actionGroups> -\ No newline at end of file -+ <!--Verify country options in checkout top destination section--> -+ <actionGroup name="VerifyTopDestinationsCountry"> -+ <arguments> -+ <argument name="country" type="string"/> -+ <argument name="placeNumber"/> -+ </arguments> -+ <conditionalClick selector="{{CheckoutCartSummarySection.shippingHeading}}" dependentSelector="{{CheckoutCartSummarySection.country}}" visible="false" stepKey="openShippingDetails"/> -+ <see selector="{{CheckoutCartSummarySection.countryParameterized('placeNumber')}}" userInput="{{country}}" stepKey="seeCountry"/> -+ </actionGroup> -+ -+ <actionGroup name="StorefrontSignOutActionGroup"> -+ <click selector="{{StoreFrontSignOutSection.customerAccount}}" stepKey="clickCustomerButton"/> -+ <click selector="{{StoreFrontSignOutSection.signOut}}" stepKey="clickToSignOut"/> -+ <waitForPageLoad stepKey="waitForPageLoad"/> -+ <see userInput="You are signed out" stepKey="signOut"/> -+ </actionGroup> -+ -+ <!--Click Place Order button--> -+ <actionGroup name="ClickPlaceOrderActionGroup"> -+ <waitForElement selector="{{CheckoutPaymentSection.placeOrder}}" time="30" stepKey="waitForPlaceOrderButton"/> -+ <click selector="{{CheckoutPaymentSection.placeOrder}}" stepKey="clickPlaceOrder"/> -+ <see selector="{{CheckoutSuccessMainSection.successTitle}}" userInput="Thank you for your purchase!" stepKey="waitForLoadSuccessPage"/> -+ </actionGroup> -+</actionGroups> -diff --git a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/ClearShippingAddressActionGroup.xml b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/ClearShippingAddressActionGroup.xml -new file mode 100644 -index 00000000000..0e6994e8fea ---- /dev/null -+++ b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/ClearShippingAddressActionGroup.xml -@@ -0,0 +1,21 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details.z -+ */ -+--> -+ -+<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> -+ <actionGroup name="ClearShippingAddressActionGroup"> -+ <clearField selector="{{CheckoutShippingSection.firstName}}" stepKey="clearFieldFirstName"/> -+ <clearField selector="{{CheckoutShippingSection.company}}" stepKey="clearFieldCompany"/> -+ <clearField selector="{{CheckoutShippingSection.street}}" stepKey="clearFieldStreetAddress"/> -+ <clearField selector="{{CheckoutShippingSection.city}}" stepKey="clearFieldCityName"/> -+ <selectOption selector="{{CheckoutShippingSection.region}}" userInput="" stepKey="clearFieldRegion"/> -+ <clearField selector="{{CheckoutShippingSection.postcode}}" stepKey="clearFieldZip"/> -+ <selectOption selector="{{CheckoutShippingSection.country}}" userInput="" stepKey="clearFieldCounty"/> -+ <clearField selector="{{CheckoutShippingSection.telephone}}" stepKey="clearFieldPhoneNumber"/> -+ </actionGroup> -+</actionGroups> -diff --git a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/CustomerCheckoutFillNewShippingAddressActionGroup.xml b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/CustomerCheckoutFillNewShippingAddressActionGroup.xml -new file mode 100644 -index 00000000000..6a3d8810f42 ---- /dev/null -+++ b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/CustomerCheckoutFillNewShippingAddressActionGroup.xml -@@ -0,0 +1,23 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> -+ <actionGroup name="CustomerCheckoutFillNewShippingAddressActionGroup"> -+ <arguments> -+ <argument name="address" type="entity"/> -+ </arguments> -+ <selectOption selector="{{CheckoutShippingSection.country}}" userInput="{{address.country}}" stepKey="selectCounty"/> -+ <fillField selector="{{CheckoutShippingSection.street}}" userInput="{{address.street}}" stepKey="fillStreet"/> -+ <fillField selector="{{CheckoutShippingSection.city}}" userInput="{{address.city}}" stepKey="fillCity"/> -+ <selectOption selector="{{CheckoutShippingSection.region}}" userInput="{{address.state}}" stepKey="selectRegion"/> -+ <fillField selector="{{CheckoutShippingSection.postcode}}" userInput="{{address.postcode}}" stepKey="fillZipCode"/> -+ <fillField selector="{{CheckoutShippingSection.telephone}}" userInput="{{address.telephone}}" stepKey="fillPhone"/> -+ <fillField selector="{{CheckoutShippingSection.company}}" userInput="{{address.company}}" stepKey="fillCompany"/> -+ </actionGroup> -+</actionGroups> -diff --git a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/DeleteProductFromShoppingCartActionGroup.xml b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/DeleteProductFromShoppingCartActionGroup.xml -new file mode 100644 -index 00000000000..86261e85d8d ---- /dev/null -+++ b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/DeleteProductFromShoppingCartActionGroup.xml -@@ -0,0 +1,19 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details.z -+ */ -+--> -+ -+<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> -+ <actionGroup name="DeleteProductFromShoppingCartActionGroup"> -+ <arguments> -+ <argument name="productName" type="string"/> -+ </arguments> -+ <click selector="{{CheckoutCartProductSection.removeProductByName(productName)}}" stepKey="deleteProductFromCheckoutCart"/> -+ <waitForPageLoad stepKey="waitForPageLoad"/> -+ <see userInput="You have no items in your shopping cart." stepKey="seeNoItemsInShoppingCart"/> -+ </actionGroup> -+</actionGroups> -diff --git a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/FillNewShippingAddressModalActionGroup.xml b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/FillNewShippingAddressModalActionGroup.xml -new file mode 100644 -index 00000000000..7035855cc0e ---- /dev/null -+++ b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/FillNewShippingAddressModalActionGroup.xml -@@ -0,0 +1,18 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> -+ <actionGroup name="FillNewShippingAddressModalActionGroup" extends="FillShippingAddressOneStreetActionGroup"> -+ <arguments> -+ <argument name="address"/> -+ </arguments> -+ <selectOption stepKey="selectRegion" selector="{{CheckoutShippingSection.region}}" -+ userInput="{{address.state}}" after="fillCityName"/> -+ </actionGroup> -+</actionGroups> -diff --git a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/FillShippingAddressOneStreetActionGroup.xml b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/FillShippingAddressOneStreetActionGroup.xml -new file mode 100644 -index 00000000000..cbc6c5320b0 ---- /dev/null -+++ b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/FillShippingAddressOneStreetActionGroup.xml -@@ -0,0 +1,24 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> -+ <actionGroup name="FillShippingAddressOneStreetActionGroup"> -+ <arguments> -+ <argument name="address" type="entity"/> -+ </arguments> -+ <fillField stepKey="fillFirstName" selector="{{CheckoutShippingSection.firstName}}" userInput="{{address.firstname}}"/> -+ <fillField stepKey="fillLastName" selector="{{CheckoutShippingSection.lastName}}" userInput="{{address.lastname}}"/> -+ <fillField stepKey="fillCompany" selector="{{CheckoutShippingSection.company}}" userInput="{{address.company}}"/> -+ <fillField stepKey="fillPhoneNumber" selector="{{CheckoutShippingSection.telephone}}" userInput="{{address.telephone}}"/> -+ <fillField stepKey="fillStreetAddress" selector="{{CheckoutShippingSection.street}}" userInput="{{address.street[0]}}"/> -+ <fillField stepKey="fillCityName" selector="{{CheckoutShippingSection.city}}" userInput="{{address.city}}"/> -+ <selectOption stepKey="selectCounty" selector="{{CheckoutShippingSection.country}}" userInput="{{address.country_id}}"/> -+ <fillField stepKey="fillZip" selector="{{CheckoutShippingSection.postcode}}" userInput="{{address.postcode}}"/> -+ </actionGroup> -+</actionGroups> -diff --git a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/FillShippingZipFormActionGroup.xml b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/FillShippingZipFormActionGroup.xml -new file mode 100644 -index 00000000000..f12bf4344ab ---- /dev/null -+++ b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/FillShippingZipFormActionGroup.xml -@@ -0,0 +1,23 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> -+ <actionGroup name="FillShippingZipForm"> -+ <arguments> -+ <argument name="address"/> -+ </arguments> -+ <conditionalClick stepKey="openShippingDetails" selector="{{CheckoutCartSummarySection.shippingHeading}}" dependentSelector="{{CheckoutCartSummarySection.country}}" visible="false"/> -+ <waitForLoadingMaskToDisappear stepKey="waitForLoadingMask"/> -+ <waitForElementVisible selector="{{CheckoutCartSummarySection.country}}" time="30" stepKey="waitForCountryFieldAppears"/> -+ <selectOption stepKey="selectCountry" selector="{{CheckoutCartSummarySection.country}}" userInput="{{address.country}}"/> -+ <selectOption stepKey="selectStateProvince" selector="{{CheckoutCartSummarySection.stateProvince}}" userInput="{{address.state}}"/> -+ <fillField stepKey="fillPostCode" selector="{{CheckoutCartSummarySection.postcode}}" userInput="{{address.postcode}}"/> -+ <waitForPageLoad stepKey="waitForFormUpdate"/> -+ </actionGroup> -+</actionGroups> -diff --git a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/GuestCheckoutFillNewBillingAddressActionGroup.xml b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/GuestCheckoutFillNewBillingAddressActionGroup.xml -index e7a6e219d28..59e997eccec 100644 ---- a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/GuestCheckoutFillNewBillingAddressActionGroup.xml -+++ b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/GuestCheckoutFillNewBillingAddressActionGroup.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"> - <!-- Guest checkout filling billing section --> - <actionGroup name="GuestCheckoutFillNewBillingAddressActionGroup"> - <arguments> -@@ -44,5 +44,32 @@ - <selectOption stepKey="selectCounty" selector="{{classPrefix}} {{CheckoutShippingSection.country}}" userInput="{{Address.country_id}}"/> - <waitForPageLoad stepKey="waitForFormUpdate2"/> - </actionGroup> -+ <!--Filling address without second address field and without state field--> -+ <actionGroup name="LoggedInCheckoutWithOneAddressFieldWithoutStateField" extends="LoggedInCheckoutFillNewBillingAddressActionGroup"> -+ <remove keyForRemoval="fillStreetAddress2"/> -+ <remove keyForRemoval="selectState"/> -+ </actionGroup> - --</actionGroups> -\ No newline at end of file -+ <actionGroup name="clearCheckoutAddressPopupFieldsActionGroup"> -+ <arguments> -+ <argument name="classPrefix" type="string" defaultValue=""/> -+ </arguments> -+ <clearField selector="{{classPrefix}} {{CheckoutShippingSection.firstName}}" stepKey="clearFieldFirstName"/> -+ <clearField selector="{{classPrefix}} {{CheckoutShippingSection.lastName}}" stepKey="clearFieldLastName"/> -+ <clearField selector="{{classPrefix}} {{CheckoutShippingSection.company}}" stepKey="clearFieldCompany"/> -+ <clearField selector="{{classPrefix}} {{CheckoutShippingSection.street}}" stepKey="clearFieldStreetAddress1"/> -+ <clearField selector="{{classPrefix}} {{CheckoutShippingSection.street2}}" stepKey="clearFieldStreetAddress2"/> -+ <clearField selector="{{classPrefix}} {{CheckoutShippingSection.city}}" stepKey="clearFieldCityName"/> -+ <selectOption selector="{{classPrefix}} {{CheckoutShippingSection.region}}" userInput="" stepKey="clearFieldRegion"/> -+ <clearField selector="{{classPrefix}} {{CheckoutShippingSection.postcode}}" stepKey="clearFieldZip"/> -+ <selectOption selector="{{classPrefix}} {{CheckoutShippingSection.country}}" userInput="" stepKey="clearFieldCounty"/> -+ <clearField selector="{{classPrefix}} {{CheckoutShippingSection.telephone}}" stepKey="clearFieldPhoneNumber"/> -+ </actionGroup> -+ <actionGroup name="GuestCheckoutSelectPaymentAndFillNewBillingAddressActionGroup" extends="GuestCheckoutFillNewBillingAddressActionGroup"> -+ <arguments> -+ <argument name="paymentMethod" type="string"/> -+ </arguments> -+ <waitForElement selector="{{CheckoutPaymentSection.paymentSectionTitle}}" time="30" after="waitForLoading3" stepKey="waitForPaymentSectionLoaded"/> -+ <conditionalClick selector="{{CheckoutPaymentSection.paymentMethodByName(paymentMethod)}}" dependentSelector="{{CheckoutPaymentSection.billingAddress}}" visible="false" before="enterFirstName" stepKey="clickCheckMoneyOrderPayment"/> -+ </actionGroup> -+</actionGroups> -diff --git a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/GuestCheckoutFillNewShippingAddressActionGroup.xml b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/GuestCheckoutFillNewShippingAddressActionGroup.xml -new file mode 100644 -index 00000000000..6ea2ccb08d8 ---- /dev/null -+++ b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/GuestCheckoutFillNewShippingAddressActionGroup.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="GuestCheckoutFillNewShippingAddressActionGroup"> -+ <arguments> -+ <argument name="customer" type="entity"/> -+ <argument name="address" type="entity"/> -+ </arguments> -+ <fillField selector="{{CheckoutShippingSection.email}}" userInput="{{customer.email}}" stepKey="fillEmailField"/> -+ <fillField selector="{{CheckoutShippingSection.firstName}}" userInput="{{customer.firstName}}" stepKey="fillFirstName"/> -+ <fillField selector="{{CheckoutShippingSection.lastName}}" userInput="{{customer.lastName}}" stepKey="fillLastName"/> -+ <fillField selector="{{CheckoutShippingSection.street}}" userInput="{{address.street}}" stepKey="fillStreet"/> -+ <fillField selector="{{CheckoutShippingSection.city}}" userInput="{{address.city}}" stepKey="fillCity"/> -+ <selectOption selector="{{CheckoutShippingSection.region}}" userInput="{{address.state}}" stepKey="selectRegion"/> -+ <fillField selector="{{CheckoutShippingSection.postcode}}" userInput="{{address.postcode}}" stepKey="fillZipCode"/> -+ <fillField selector="{{CheckoutShippingSection.telephone}}" userInput="{{address.telephone}}" stepKey="fillPhone"/> -+ </actionGroup> -+</actionGroups> -diff --git a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/IdentityOfDefaultBillingAndShippingAddressActionGroup.xml b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/IdentityOfDefaultBillingAndShippingAddressActionGroup.xml -new file mode 100644 -index 00000000000..15c157a9826 ---- /dev/null -+++ b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/IdentityOfDefaultBillingAndShippingAddressActionGroup.xml -@@ -0,0 +1,21 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> -+ <!--Assert That Shipping And Billing Address are the same--> -+ <actionGroup name="AssertThatShippingAndBillingAddressTheSame"> -+ <!--Get shipping and billing addresses--> -+ <grabTextFrom selector="{{ShipmentFormSection.shippingAddress}}" stepKey="shippingAddr"/> -+ <grabTextFrom selector="{{ShipmentFormSection.billingAddress}}" stepKey="billingAddr"/> -+ <!--Make sure that shipping and billing addresses are different--> -+ <see userInput="Shipping Address" stepKey="seeShippingAddress"/> -+ <see userInput="Billing Address" stepKey="seeBillingAddress"/> -+ <assertEquals stepKey="assert" actual="$billingAddr" expected="$shippingAddr"/> -+ </actionGroup> -+</actionGroups> -diff --git a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/LoginAsCustomerOnCheckoutPageActionGroup.xml b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/LoginAsCustomerOnCheckoutPageActionGroup.xml -new file mode 100644 -index 00000000000..06a49b856b5 ---- /dev/null -+++ b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/LoginAsCustomerOnCheckoutPageActionGroup.xml -@@ -0,0 +1,27 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> -+ <actionGroup name="LoginAsCustomerOnCheckoutPageActionGroup"> -+ <arguments> -+ <argument name="customer" type="entity"/> -+ </arguments> -+ <waitForPageLoad stepKey="waitForCheckoutShippingSectionToLoad"/> -+ <fillField selector="{{CheckoutShippingSection.email}}" userInput="{{customer.email}}" stepKey="fillEmailField"/> -+ <waitForLoadingMaskToDisappear stepKey="waitForLoadingMaskToDisappear"/> -+ <waitForElementVisible selector="{{CheckoutShippingSection.password}}" stepKey="waitForElementVisible"/> -+ <fillField selector="{{CheckoutShippingSection.password}}" userInput="{{customer.password}}" stepKey="fillPasswordField"/> -+ <waitForLoadingMaskToDisappear stepKey="waitForLoadingMaskToDisappear2"/> -+ <waitForElementVisible selector="{{CheckoutShippingSection.loginButton}}" stepKey="waitForLoginButtonVisible"/> -+ <doubleClick selector="{{CheckoutShippingSection.loginButton}}" stepKey="clickLoginBtn"/> -+ <waitForLoadingMaskToDisappear stepKey="waitForLoadingMaskToDisappear3"/> -+ <waitForPageLoad stepKey="waitToBeLoggedIn"/> -+ <waitForElementNotVisible selector="{{CheckoutShippingSection.email}}" stepKey="waitForEmailInvisible" time ="60"/> -+ </actionGroup> -+</actionGroups> -diff --git a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/LoginAsCustomerUsingSignInLinkActionGroup.xml b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/LoginAsCustomerUsingSignInLinkActionGroup.xml -new file mode 100644 -index 00000000000..35e8e368ae3 ---- /dev/null -+++ b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/LoginAsCustomerUsingSignInLinkActionGroup.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="LoginAsCustomerUsingSignInLinkActionGroup"> -+ <arguments> -+ <argument name="customer" type="entity"/> -+ </arguments> -+ <click selector="{{StorefrontCustomerSignInLinkSection.signInLink}}" stepKey="clickOnCustomizeAndAddToCartButton"/> -+ <fillField selector="{{StorefrontCustomerSignInLinkSection.email}}" userInput="{{customer.email}}" stepKey="fillEmail"/> -+ <fillField selector="{{StorefrontCustomerSignInLinkSection.password}}" userInput="{{customer.password}}" stepKey="fillPassword"/> -+ <click selector="{{StorefrontCustomerSignInLinkSection.signInBtn}}" stepKey="clickSignInBtn"/> -+ </actionGroup> -+</actionGroups> -diff --git a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/OpenStoreFrontCheckoutShippingPageActionGroup.xml b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/OpenStoreFrontCheckoutShippingPageActionGroup.xml -new file mode 100644 -index 00000000000..cea9d968b58 ---- /dev/null -+++ b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/OpenStoreFrontCheckoutShippingPageActionGroup.xml -@@ -0,0 +1,14 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> -+ <actionGroup name="OpenStoreFrontCheckoutShippingPageActionGroup"> -+ <amOnPage url="{{CheckoutShippingPage.url}}" stepKey="amOnCheckoutShippingPage"/> -+ <waitForPageLoad stepKey="waitForCheckoutShippingPageLoad"/> -+ </actionGroup> -+</actionGroups> -diff --git a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontAddBundleProductToTheCartActionGroup.xml b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontAddBundleProductToTheCartActionGroup.xml -new file mode 100644 -index 00000000000..2c64eda38a4 ---- /dev/null -+++ b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontAddBundleProductToTheCartActionGroup.xml -@@ -0,0 +1,24 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> -+ <actionGroup name="StorefrontAddBundleProductToTheCartActionGroup"> -+ <arguments> -+ <argument name="productName" type="string"/> -+ <argument name="quantity" type="string"/> -+ </arguments> -+ <click selector="{{StorefrontBundleProductActionSection.customizeAndAddToCartButton}}" stepKey="clickOnCustomizeAndAddToCartButton"/> -+ <waitForPageLoad stepKey="waitForPageLoad"/> -+ <click selector="{{StorefrontBundleProductActionSection.dropdownSelectOption}}" stepKey="clickOnSelectOption"/> -+ <click selector="{{StorefrontBundleProductActionSection.dropdownProductSelection(productName)}}" stepKey="selectProduct"/> -+ <clearField selector="{{StorefrontBundleProductActionSection.quantityField}}" stepKey="clearTheQuantityField"/> -+ <fillField selector="{{StorefrontBundleProductActionSection.quantityField}}" userInput="{{quantity}}" stepKey="fillTheProductQuantity"/> -+ <click selector="{{StorefrontBundleProductActionSection.addToCartButton}}" stepKey="clickOnAddToButton"/> -+ <waitForPageLoad stepKey="waitForPageLoad2"/> -+ </actionGroup> -+</actionGroups> -diff --git a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontAddProductToCartFromCategoryActionGroup.xml b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontAddProductToCartFromCategoryActionGroup.xml -new file mode 100644 -index 00000000000..de08fe7427c ---- /dev/null -+++ b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontAddProductToCartFromCategoryActionGroup.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="StorefrontAddProductToCartFromCategoryActionGroup"> -+ <arguments> -+ <argument name="productName" type="string"/> -+ </arguments> -+ <scrollTo selector="{{StorefrontCategoryProductSection.ProductInfoByName(productName)}}" stepKey="scroll"/> -+ <moveMouseOver selector="{{StorefrontCategoryProductSection.ProductInfoByName(productName)}}" stepKey="moveMouseOverProduct" /> -+ <click selector="{{StorefrontCategoryProductSection.ProductAddToCartByName(productName)}}" stepKey="clickAddToCart" /> -+ <waitForAjaxLoad stepKey="waitForAjax"/> -+ </actionGroup> -+</actionGroups> -+ -+ -diff --git a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontAddProductWithSelectedConfigurableAndCustomOptionsToCartActionGroup.xml b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontAddProductWithSelectedConfigurableAndCustomOptionsToCartActionGroup.xml -new file mode 100644 -index 00000000000..8e13748d64e ---- /dev/null -+++ b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontAddProductWithSelectedConfigurableAndCustomOptionsToCartActionGroup.xml -@@ -0,0 +1,24 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> -+ <!-- Add product with selected configurable option and customizable option to cart from the product page --> -+ <actionGroup name="StorefrontAddProductWithSelectedConfigurableAndCustomOptionsToCartActionGroup"> -+ <arguments> -+ <argument name="product" type="entity"/> -+ <argument name="option" type="string"/> -+ <argument name="customizableOption" type="string"/> -+ </arguments> -+ <selectOption userInput="{{option}}" selector="{{StorefrontProductInfoMainSection.productAttributeOptionsSelectButton}}" stepKey="selectOption"/> -+ <click selector="{{StorefrontProductInfoMainSection.selectCustomOptionByName(customizableOption)}}" stepKey="selectCustomOption"/> -+ <click selector="{{StorefrontProductInfoMainSection.AddToCart}}" stepKey="clickAddToCart"/> -+ <waitForElementVisible selector="{{StorefrontCategoryMainSection.SuccessMsg}}" stepKey="waitForSuccessMessage"/> -+ <see selector="{{StorefrontProductPageSection.messagesBlock}}" userInput="You added {{product.name}} to your shopping cart." stepKey="assertSuccessMessage"/> -+ </actionGroup> -+</actionGroups> -diff --git a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontAddProductWithSelectedOptionToCartActionGroup.xml b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontAddProductWithSelectedOptionToCartActionGroup.xml -new file mode 100644 -index 00000000000..5d844523d44 ---- /dev/null -+++ b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontAddProductWithSelectedOptionToCartActionGroup.xml -@@ -0,0 +1,22 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> -+ <!-- Add product with selected option to Cart from the product page --> -+ <actionGroup name="StorefrontAddProductWithSelectedConfigurableOptionToCartActionGroup"> -+ <arguments> -+ <argument name="product" type="entity"/> -+ <argument name="option" type="string"/> -+ </arguments> -+ <selectOption userInput="{{option}}" selector="{{StorefrontProductInfoMainSection.productAttributeOptionsSelectButton}}" stepKey="selectOption"/> -+ <click selector="{{StorefrontProductInfoMainSection.AddToCart}}" stepKey="clickAddToCart"/> -+ <waitForElementVisible selector="{{StorefrontCategoryMainSection.SuccessMsg}}" stepKey="waitForSuccessMessage"/> -+ <see selector="{{StorefrontProductPageSection.messagesBlock}}" userInput="You added {{product.name}} to your shopping cart." stepKey="assertSuccessMessage"/> -+ </actionGroup> -+</actionGroups> -diff --git a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontAddSimpleProductToCartActionGroup.xml b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontAddSimpleProductToCartActionGroup.xml -new file mode 100644 -index 00000000000..7bfc87cd8d6 ---- /dev/null -+++ b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontAddSimpleProductToCartActionGroup.xml -@@ -0,0 +1,22 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> -+ <!-- Add Product to Cart from the category page and check message --> -+ <actionGroup name="StorefrontAddSimpleProductToCartActionGroup"> -+ <arguments> -+ <argument name="product" type="entity"/> -+ </arguments> -+ <moveMouseOver selector="{{StorefrontCategoryProductSection.ProductInfoByName(product.name)}}" stepKey="moveMouseOverProduct" /> -+ <click selector="{{StorefrontCategoryProductSection.ProductAddToCartByName(product.name)}}" stepKey="clickAddToCart" /> -+ <waitForElementVisible selector="{{StorefrontCategoryMainSection.SuccessMsg}}" stepKey="waitForSuccessMessage" /> -+ <see selector="{{StorefrontCategoryMainSection.SuccessMsg}}" userInput="You added {{product.name}} to your shopping cart." stepKey="assertSuccessMessage"/> -+ </actionGroup> -+</actionGroups> -+ -diff --git a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontAddToTheCartActionGroup.xml b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontAddToTheCartActionGroup.xml -new file mode 100644 -index 00000000000..1603758016e ---- /dev/null -+++ b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontAddToTheCartActionGroup.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="StorefrontAddToTheCartActionGroup"> -+ <waitForPageLoad stepKey="waitForPageLoad"/> -+ <scrollTo selector="{{StorefrontProductActionSection.addToCart}}" stepKey="scrollToAddToCartButton"/> -+ <click selector="{{StorefrontProductActionSection.addToCart}}" stepKey="addToCart"/> -+ <waitForPageLoad stepKey="waitForPageToLoad"/> -+ <waitForElementVisible selector="{{StorefrontMessagesSection.success}}" stepKey="waitForSuccessMessage"/> -+ </actionGroup> -+</actionGroups> -diff --git a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontAssertCartEstimateShippingAndTaxActionGroup.xml b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontAssertCartEstimateShippingAndTaxActionGroup.xml -new file mode 100644 -index 00000000000..0d6f34098c0 ---- /dev/null -+++ b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontAssertCartEstimateShippingAndTaxActionGroup.xml -@@ -0,0 +1,20 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> -+ <!-- Assert Estimate Shipping and Tax Data in Cart --> -+ <actionGroup name="StorefrontAssertCartEstimateShippingAndTaxActionGroup"> -+ <arguments> -+ <argument name="customerData" defaultValue="Simple_US_CA_Customer_For_Shipment"/> -+ </arguments> -+ <seeInField selector="{{CheckoutCartSummarySection.country}}" userInput="{{customerData.country}}" stepKey="assertCountryFieldInCartEstimateShippingAndTaxSection"/> -+ <seeInField selector="{{CheckoutCartSummarySection.stateProvinceInput}}" userInput="{{customerData.region}}" stepKey="assertStateProvinceInCartEstimateShippingAndTaxSection"/> -+ <seeInField selector="{{CheckoutCartSummarySection.postcode}}" userInput="{{customerData.postcode}}" stepKey="assertZipPostalCodeInCartEstimateShippingAndTaxSection"/> -+ </actionGroup> -+</actionGroups> -diff --git a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontAssertCartShippingMethodSelectedActionGroup.xml b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontAssertCartShippingMethodSelectedActionGroup.xml -new file mode 100644 -index 00000000000..4061f97821c ---- /dev/null -+++ b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontAssertCartShippingMethodSelectedActionGroup.xml -@@ -0,0 +1,19 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> -+ <!-- Assert Shipping Method Is Checked on Cart --> -+ <actionGroup name="StorefrontAssertCartShippingMethodSelectedActionGroup"> -+ <arguments> -+ <argument name="carrierCode" type="string"/> -+ <argument name="methodCode" type="string"/> -+ </arguments> -+ <seeCheckboxIsChecked selector="{{CheckoutCartSummarySection.shippingMethodElementId(carrierCode, methodCode)}}" stepKey="assertShippingMethodIsChecked"/> -+ </actionGroup> -+</actionGroups> -diff --git a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontAssertCheckoutEstimateShippingInformationActionGroup.xml b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontAssertCheckoutEstimateShippingInformationActionGroup.xml -new file mode 100644 -index 00000000000..82d7e12105b ---- /dev/null -+++ b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontAssertCheckoutEstimateShippingInformationActionGroup.xml -@@ -0,0 +1,20 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> -+ <!-- Assert Estimate Shipping and Tax Data on Checkout --> -+ <actionGroup name="StorefrontAssertCheckoutEstimateShippingInformationActionGroup"> -+ <arguments> -+ <argument name="customerData" defaultValue="Simple_US_CA_Customer_For_Shipment"/> -+ </arguments> -+ <seeInField selector="{{CheckoutShippingGuestInfoSection.country}}" userInput="{{customerData.country}}" stepKey="assertCountryField"/> -+ <seeInField selector="{{CheckoutShippingGuestInfoSection.region}}" userInput="{{customerData.region}}" stepKey="assertStateProvinceField"/> -+ <seeInField selector="{{CheckoutShippingGuestInfoSection.postcode}}" userInput="{{customerData.postcode}}" stepKey="assertZipPostalCodeField"/> -+ </actionGroup> -+</actionGroups> -diff --git a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontAssertCheckoutShippingMethodSelectedActionGroup.xml b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontAssertCheckoutShippingMethodSelectedActionGroup.xml -new file mode 100644 -index 00000000000..33f2852f1f0 ---- /dev/null -+++ b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontAssertCheckoutShippingMethodSelectedActionGroup.xml -@@ -0,0 +1,18 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> -+ <!-- Assert Shipping Method by Name Is Checked on Checkout --> -+ <actionGroup name="StorefrontAssertCheckoutShippingMethodSelectedActionGroup"> -+ <arguments> -+ <argument name="shippingMethod"/> -+ </arguments> -+ <seeCheckboxIsChecked selector="{{CheckoutShippingMethodsSection.checkShippingMethodByName('shippingMethod')}}" stepKey="assertShippingMethodByNameIsChecked"/> -+ </actionGroup> -+</actionGroups> -diff --git a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontAssertGuestShippingInfoActionGroup.xml b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontAssertGuestShippingInfoActionGroup.xml -new file mode 100644 -index 00000000000..02c362bf340 ---- /dev/null -+++ b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontAssertGuestShippingInfoActionGroup.xml -@@ -0,0 +1,28 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> -+ <!-- Assert guest shipping info on checkout --> -+ <actionGroup name="StorefrontAssertGuestShippingInfoActionGroup"> -+ <arguments> -+ <argument name="customerData" defaultValue="Simple_UK_Customer_For_Shipment"/> -+ </arguments> -+ <seeInField selector="{{CheckoutShippingGuestInfoSection.email}}" userInput="{{customerData.email}}" stepKey="assertEmailAddress"/> -+ <seeInField selector="{{CheckoutShippingGuestInfoSection.firstName}}" userInput="{{customerData.firstName}}" stepKey="assertFirstName"/> -+ <seeInField selector="{{CheckoutShippingGuestInfoSection.lastName}}" userInput="{{customerData.lastName}}" stepKey="assertLastName"/> -+ <seeInField selector="{{CheckoutShippingGuestInfoSection.company}}" userInput="{{customerData.company}}" stepKey="assertCompany"/> -+ <seeInField selector="{{CheckoutShippingGuestInfoSection.street}}" userInput="{{customerData.streetFirstLine}}" stepKey="assertAddressFirstLine"/> -+ <seeInField selector="{{CheckoutShippingGuestInfoSection.street2}}" userInput="{{customerData.streetSecondLine}}" stepKey="assertAddressSecondLine"/> -+ <seeInField selector="{{CheckoutShippingGuestInfoSection.city}}" userInput="{{customerData.city}}" stepKey="assertCity"/> -+ <seeInField selector="{{CheckoutShippingGuestInfoSection.country}}" userInput="{{customerData.country}}" stepKey="assertCountry"/> -+ <seeInField selector="{{CheckoutShippingGuestInfoSection.regionInput}}" userInput="{{customerData.region}}" stepKey="assertStateProvince"/> -+ <seeInField selector="{{CheckoutShippingGuestInfoSection.postcode}}" userInput="{{customerData.postcode}}" stepKey="assertZipPostalCode"/> -+ <seeInField selector="{{CheckoutShippingGuestInfoSection.telephone}}" userInput="{{customerData.telephone}}" stepKey="assertPhoneNumber"/> -+ </actionGroup> -+</actionGroups> -diff --git a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontAssertMiniCartItemCountActionGroup.xml b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontAssertMiniCartItemCountActionGroup.xml -new file mode 100644 -index 00000000000..03caa558e42 ---- /dev/null -+++ b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontAssertMiniCartItemCountActionGroup.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="StorefrontAssertMiniCartItemCountActionGroup"> -+ <arguments> -+ <argument name="productCount" type="string"/> -+ <argument name="productCountText" type="string"/> -+ </arguments> -+ <see selector="{{StorefrontMinicartSection.productCount}}" userInput="{{productCount}}" stepKey="seeProductCountInCart"/> -+ <see selector="{{StorefrontMinicartSection.visibleItemsCountText}}" userInput="{{productCountText}}" stepKey="seeNumberOfItemDisplayInMiniCart"/> -+ </actionGroup> -+</actionGroups> -diff --git a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontAssertProductDetailsInOrderSummaryActionGroup.xml b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontAssertProductDetailsInOrderSummaryActionGroup.xml -new file mode 100644 -index 00000000000..59159ea2d0e ---- /dev/null -+++ b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontAssertProductDetailsInOrderSummaryActionGroup.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="StorefrontAssertProductDetailsInOrderSummaryActionGroup"> -+ <arguments> -+ <argument name="productName" type="string"/> -+ <argument name="qty" type="string"/> -+ <argument name="price" type="string"/> -+ </arguments> -+ <see selector="{{CheckoutOrderSummarySection.productItemName}}" userInput="{{productName}}" stepKey="seeProductName"/> -+ <see selector="{{CheckoutOrderSummarySection.productItemQty}}" userInput="{{qty}}" stepKey="seeProductQty"/> -+ <see selector="{{CheckoutOrderSummarySection.productItemPrice}}" userInput="{{price}}" stepKey="seeProductPrice"/> -+ </actionGroup> -+</actionGroups> -diff --git a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontAssertShippingAddressPageDisplayActionGroup.xml b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontAssertShippingAddressPageDisplayActionGroup.xml -new file mode 100644 -index 00000000000..cd3d18e2aa0 ---- /dev/null -+++ b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontAssertShippingAddressPageDisplayActionGroup.xml -@@ -0,0 +1,15 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> -+ <actionGroup name="StorefrontAssertShippingAddressPageDisplayActionGroup"> -+ <seeInCurrentUrl url="checkout/#shipping" stepKey="seeCurrentUrl"/> -+ <seeElement selector="{{CheckoutShippingSection.shippingTab}}" stepKey="seeShippingTab"/> -+ <seeElement selector="{{CheckoutShippingGuestInfoSection.shippingBlock}}" stepKey="seeShippingBlock"/> -+ </actionGroup> -+</actionGroups> -diff --git a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontAssertShippingMethodOptionPresentInCartActionGroup.xml b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontAssertShippingMethodOptionPresentInCartActionGroup.xml -new file mode 100644 -index 00000000000..6e033268934 ---- /dev/null -+++ b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontAssertShippingMethodOptionPresentInCartActionGroup.xml -@@ -0,0 +1,20 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> -+ <!-- Assert shipping method name and price are present in cart --> -+ <actionGroup name="StorefrontAssertShippingMethodOptionPresentInCartActionGroup"> -+ <arguments> -+ <argument name="methodName" type="string"/> -+ <argument name="price" type="string"/> -+ </arguments> -+ <see selector="{{CheckoutCartSummarySection.methodName}}" userInput="{{methodName}}" stepKey="seeShippingName"/> -+ <see selector="{{CheckoutCartSummarySection.shippingPrice}}" userInput="{{price}}" stepKey="seeShippingPrice"/> -+ </actionGroup> -+</actionGroups> -diff --git a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontAssertShippingMethodPresentInCartActionGroup.xml b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontAssertShippingMethodPresentInCartActionGroup.xml -new file mode 100644 -index 00000000000..3d8530ae837 ---- /dev/null -+++ b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontAssertShippingMethodPresentInCartActionGroup.xml -@@ -0,0 +1,18 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> -+ <!-- Assert shipping method is present in cart --> -+ <actionGroup name="StorefrontAssertShippingMethodPresentInCartActionGroup"> -+ <arguments> -+ <argument name="shippingMethod" type="string"/> -+ </arguments> -+ <see selector="{{CheckoutCartSummarySection.shippingMethodLabel}}" userInput="{{shippingMethod}}" stepKey="assertShippingMethodIsPresentInCart"/> -+ </actionGroup> -+</actionGroups> -diff --git a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontAttachOptionFileActionGroup.xml b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontAttachOptionFileActionGroup.xml -new file mode 100644 -index 00000000000..364f05abd38 ---- /dev/null -+++ b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontAttachOptionFileActionGroup.xml -@@ -0,0 +1,17 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> -+ <actionGroup name="StorefrontAttachOptionFileActionGroup"> -+ <arguments> -+ <argument name="optionTitle" defaultValue="ProductOptionFile"/> -+ <argument name="file" defaultValue="MagentoLogo.file" /> -+ </arguments> -+ <attachFile selector="{{StorefrontProductInfoMainSection.addLinkFileUploadFile(optionTitle.title)}}" userInput="{{file}}" stepKey="attachFile"/> -+ </actionGroup> -+</actionGroups> -diff --git a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontCartEstimateShippingAndTaxActionGroup.xml b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontCartEstimateShippingAndTaxActionGroup.xml -new file mode 100644 -index 00000000000..4176859f99f ---- /dev/null -+++ b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontCartEstimateShippingAndTaxActionGroup.xml -@@ -0,0 +1,25 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> -+ <!-- Fill Estimate Shipping and Tax fields --> -+ <actionGroup name="StorefrontCartEstimateShippingAndTaxActionGroup"> -+ <arguments> -+ <argument name="estimateAddress" defaultValue="EstimateAddressCalifornia"/> -+ </arguments> -+ <conditionalClick selector="{{CheckoutCartSummarySection.estimateShippingAndTax}}" dependentSelector="{{CheckoutCartSummarySection.country}}" visible="false" stepKey="clickOnEstimateShippingAndTax"/> -+ <waitForElementVisible selector="{{CheckoutCartSummarySection.country}}" stepKey="waitForCountrySelectorIsVisible"/> -+ <selectOption selector="{{CheckoutCartSummarySection.country}}" userInput="{{estimateAddress.country}}" stepKey="selectCountry"/> -+ <waitForLoadingMaskToDisappear stepKey="waitForCountryLoadingMaskDisappear"/> -+ <selectOption selector="{{CheckoutCartSummarySection.stateProvince}}" userInput="{{estimateAddress.state}}" stepKey="selectStateProvince"/> -+ <waitForLoadingMaskToDisappear stepKey="waitForStateLoadingMaskDisappear"/> -+ <fillField selector="{{CheckoutCartSummarySection.postcode}}" userInput="{{estimateAddress.zipCode}}" stepKey="fillZipPostalCodeField"/> -+ <waitForLoadingMaskToDisappear stepKey="waitForZipLoadingMaskDisappear"/> -+ </actionGroup> -+</actionGroups> -diff --git a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontCheckCartDiscountAndSummaryActionGroup.xml b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontCheckCartDiscountAndSummaryActionGroup.xml -new file mode 100644 -index 00000000000..6c541db6def ---- /dev/null -+++ b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontCheckCartDiscountAndSummaryActionGroup.xml -@@ -0,0 +1,23 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> -+ <!-- Open the Minicart and check Summary --> -+ <actionGroup name="StorefrontCheckCartDiscountAndSummaryActionGroup"> -+ <arguments> -+ <argument name="total" type="string"/> -+ <argument name="discount" type="string"/> -+ </arguments> -+ <waitForPageLoad stepKey="waitForPageLoad"/> -+ <waitForLoadingMaskToDisappear stepKey="waitForPrices"/> -+ <see selector="{{CheckoutCartSummarySection.discountAmount}}" userInput="-${{discount}}" stepKey="assertDiscount"/> -+ <wait time="10" stepKey="waitForTotalPrice"/> -+ <see selector="{{CheckoutCartSummarySection.total}}" userInput="${{total}}" stepKey="assertTotal"/> -+ </actionGroup> -+</actionGroups> -diff --git a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontCheckThatCartIsEmptyActionGroup.xml b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontCheckThatCartIsEmptyActionGroup.xml -new file mode 100644 -index 00000000000..e5fbb9065f9 ---- /dev/null -+++ b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontCheckThatCartIsEmptyActionGroup.xml -@@ -0,0 +1,15 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> -+ <actionGroup name="StorefrontCheckThatCartIsEmptyActionGroup"> -+ <click selector="{{StorefrontMinicartSection.showCart}}" stepKey="clickOnMiniCart"/> -+ <see selector="{{StorefrontMinicartSection.messageEmptyCart}}" userInput="You have no items in your shopping cart." stepKey="seeNoItemsInShoppingCart"/> -+ </actionGroup> -+</actionGroups> -diff --git a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontCheckoutAndAssertOrderSummaryDisplayActionGroup.xml b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontCheckoutAndAssertOrderSummaryDisplayActionGroup.xml -new file mode 100644 -index 00000000000..0a29cba11d4 ---- /dev/null -+++ b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontCheckoutAndAssertOrderSummaryDisplayActionGroup.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="StorefrontCheckoutAndAssertOrderSummaryDisplayActionGroup"> -+ <arguments> -+ <argument name="itemsText" type="string"/> -+ </arguments> -+ <click selector="{{StorefrontMinicartSection.goToCheckout}}" stepKey="clickOnCheckOutButtonInMiniCart"/> -+ <waitForPageLoad stepKey="waitForPageToLoad"/> -+ <see selector="{{CheckoutShippingGuestInfoSection.itemInCart}}" userInput="{{itemsText}}" stepKey="seeOrderSummaryText"/> -+ <seeElement selector="{{CheckoutOrderSummarySection.miniCartTab}}" stepKey="clickOnOrderSummaryTab"/> -+ <waitForPageLoad stepKey="waitForPageToLoad1"/> -+ </actionGroup> -+</actionGroups> -diff --git a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontCheckoutCheckOutOfStockProductActionGroup.xml b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontCheckoutCheckOutOfStockProductActionGroup.xml -new file mode 100644 -index 00000000000..92ce4f1f360 ---- /dev/null -+++ b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontCheckoutCheckOutOfStockProductActionGroup.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="StorefrontCheckoutCheckOutOfStockProductActionGroup"> -+ <arguments> -+ <argument name="product" type="entity"/> -+ </arguments> -+ <see selector="{{CheckoutCartMessageSection.errorMessage}}" userInput="1 product requires your attention." stepKey="seeErrorMessage"/> -+ <see selector="{{CheckoutCartProductSection.productName}}" userInput="{{product.name}}" stepKey="seeProductName"/> -+ <see selector="{{CheckoutCartProductSection.messageErrorItem}}" userInput="Availability: Out of stock" stepKey="seeProductOutOfStock"/> -+ </actionGroup> -+</actionGroups> -diff --git a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontCheckoutCheckProductActionGroup.xml b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontCheckoutCheckProductActionGroup.xml -new file mode 100644 -index 00000000000..a9946118692 ---- /dev/null -+++ b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontCheckoutCheckProductActionGroup.xml -@@ -0,0 +1,21 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> -+ <actionGroup name="StorefrontCheckoutCheckProductActionGroup"> -+ <arguments> -+ <argument name="product" type="entity"/> -+ <argument name="cartItem" type="entity"/> -+ </arguments> -+ -+ <see selector="{{CheckoutCartProductSection.productName}}" userInput="{{product.name}}" stepKey="seeProductName"/> -+ <grabValueFrom selector="{{CheckoutCartProductSection.ProductQuantityByName(product.name)}}" stepKey="grabProductQty"/> -+ <assertEquals expected="{{cartItem.qty}}" actual="$grabProductQty" stepKey="assertQtyShoppingCart"/> -+ </actionGroup> -+</actionGroups> -diff --git a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontClickOnMiniCartActionGroup.xml b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontClickOnMiniCartActionGroup.xml -new file mode 100644 -index 00000000000..09e991c2164 ---- /dev/null -+++ b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontClickOnMiniCartActionGroup.xml -@@ -0,0 +1,16 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> -+ <actionGroup name="StorefrontClickOnMiniCartActionGroup"> -+ <scrollToTopOfPage stepKey="scrollToTheTopOfThePage"/> -+ <waitForElementVisible selector="{{StorefrontMinicartSection.showCart}}" stepKey="waitForElementToBeVisible"/> -+ <click selector="{{StorefrontMinicartSection.showCart}}" stepKey="clickOnMiniCart"/> -+ <waitForPageLoad stepKey="waitForPageToLoad"/> -+ </actionGroup> -+</actionGroups> -diff --git a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontEnterProductQuantityAndAddToTheCartActionGroup.xml b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontEnterProductQuantityAndAddToTheCartActionGroup.xml -new file mode 100644 -index 00000000000..c76cb8c0f1f ---- /dev/null -+++ b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontEnterProductQuantityAndAddToTheCartActionGroup.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="StorefrontEnterProductQuantityAndAddToTheCartActionGroup"> -+ <arguments> -+ <argument name="quantity" type="string"/> -+ </arguments> -+ <clearField selector="{{StorefrontBundleProductActionSection.quantityField}}" stepKey="clearTheQuantityField"/> -+ <fillField selector="{{StorefrontBundleProductActionSection.quantityField}}" userInput="{{quantity}}" stepKey="fillTheProductQuantity"/> -+ <click selector="{{StorefrontBundleProductActionSection.addToCartButton}}" stepKey="clickOnAddToButton"/> -+ <waitForPageLoad stepKey="waitForPageLoad2"/> -+ </actionGroup> -+</actionGroups> -diff --git a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontFillEmailFieldOnCheckoutActionGroup.xml b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontFillEmailFieldOnCheckoutActionGroup.xml -new file mode 100644 -index 00000000000..fcac780a367 ---- /dev/null -+++ b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontFillEmailFieldOnCheckoutActionGroup.xml -@@ -0,0 +1,19 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> -+ <actionGroup name="StorefrontFillEmailFieldOnCheckoutActionGroup"> -+ <arguments> -+ <argument name="email" type="string" /> -+ </arguments> -+ <fillField selector="{{StorefrontCheckoutCheckoutCustomerLoginSection.email}}" userInput="{{email}}" stepKey="fillCustomerEmailField"/> -+ <doubleClick selector="{{StorefrontCheckoutCheckoutCustomerLoginSection.emailTooltipButton}}" stepKey="clickToMoveFocusFromEmailInput" /> -+ <waitForPageLoad stepKey="waitForPageLoad" /> -+ </actionGroup> -+</actionGroups> -diff --git a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontFillGuestShippingInfoActionGroup.xml b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontFillGuestShippingInfoActionGroup.xml -new file mode 100644 -index 00000000000..e7669d62c79 ---- /dev/null -+++ b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontFillGuestShippingInfoActionGroup.xml -@@ -0,0 +1,25 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> -+ <!-- Fill data in checkout shipping section --> -+ <actionGroup name="StorefrontFillGuestShippingInfoActionGroup"> -+ <arguments> -+ <argument name="customerData" defaultValue="Simple_UK_Customer_For_Shipment"/> -+ </arguments> -+ <fillField selector="{{CheckoutShippingGuestInfoSection.email}}" userInput="{{customerData.email}}" stepKey="fillEmailAddressField"/> -+ <fillField selector="{{CheckoutShippingGuestInfoSection.firstName}}" userInput="{{customerData.firstName}}" stepKey="fillFirstNameField"/> -+ <fillField selector="{{CheckoutShippingGuestInfoSection.lastName}}" userInput="{{customerData.lastName}}" stepKey="fillLastNameField"/> -+ <fillField selector="{{CheckoutShippingGuestInfoSection.company}}" userInput="{{customerData.company}}" stepKey="fillCompanyField"/> -+ <fillField selector="{{CheckoutShippingGuestInfoSection.street}}" userInput="{{customerData.streetFirstLine}}" stepKey="fillStreetAddressFirstLineField"/> -+ <fillField selector="{{CheckoutShippingGuestInfoSection.street2}}" userInput="{{customerData.streetSecondLine}}" stepKey="fillStreetAddressSecondLineField"/> -+ <fillField selector="{{CheckoutShippingGuestInfoSection.city}}" userInput="{{customerData.city}}" stepKey="fillCityField"/> -+ <fillField selector="{{CheckoutShippingGuestInfoSection.telephone}}" userInput="{{customerData.telephone}}" stepKey="fillPhoneNumberField"/> -+ </actionGroup> -+</actionGroups> -diff --git a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontFillOptionFieldInputActionGroup.xml b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontFillOptionFieldInputActionGroup.xml -new file mode 100644 -index 00000000000..001cdae5a47 ---- /dev/null -+++ b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontFillOptionFieldInputActionGroup.xml -@@ -0,0 +1,17 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> -+ <actionGroup name="StorefrontFillOptionFieldInputActionGroup"> -+ <arguments> -+ <argument name="optionTitle" defaultValue="ProductOptionField"/> -+ <argument name="fieldInput" type="string" /> -+ </arguments> -+ <fillField selector="{{StorefrontProductInfoMainSection.productOptionFieldInput(optionTitle.title)}}" userInput="{{fieldInput}}" stepKey="fillOptionField"/> -+ </actionGroup> -+</actionGroups> -diff --git a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontFillOptionTextAreaActionGroup.xml b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontFillOptionTextAreaActionGroup.xml -new file mode 100644 -index 00000000000..1e3162bac3b ---- /dev/null -+++ b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontFillOptionTextAreaActionGroup.xml -@@ -0,0 +1,17 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> -+ <actionGroup name="StorefrontFillOptionTextAreaActionGroup"> -+ <arguments> -+ <argument name="optionTitle" defaultValue="ProductOptionArea"/> -+ <argument name="fieldInput" type="string"/> -+ </arguments> -+ <fillField selector="{{StorefrontProductInfoMainSection.productOptionAreaInput(optionTitle.title)}}" userInput="{{fieldInput}}" stepKey="fillOptionTextArea"/> -+ </actionGroup> -+</actionGroups> -diff --git a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontMiniCartActionGroup.xml b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontMiniCartActionGroup.xml -index b2402d94723..28e109e2387 100644 ---- a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontMiniCartActionGroup.xml -+++ b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontMiniCartActionGroup.xml -@@ -6,11 +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="clickViewAndEditCartFromMiniCart"> -+ <scrollTo selector="{{StorefrontMinicartSection.showCart}}" stepKey="scrollToMiniCart"/> - <conditionalClick selector="{{StorefrontMinicartSection.showCart}}" dependentSelector="{{StorefrontMinicartSection.miniCartOpened}}" visible="false" stepKey="openMiniCart"/> - <waitForElementVisible selector="{{StorefrontMinicartSection.viewAndEditCart}}" stepKey="waitForViewAndEditCartVisible"/> - <click selector="{{StorefrontMinicartSection.viewAndEditCart}}" stepKey="viewAndEditCart"/> -+ <waitForPageLoad stepKey="waitForPageToLoad"/> - <seeInCurrentUrl url="checkout/cart" stepKey="seeInCurrentUrl"/> - </actionGroup> - <actionGroup name="assertOneProductNameInMiniCart"> -@@ -21,4 +23,25 @@ - <waitForElementVisible selector="{{StorefrontMinicartSection.viewAndEditCart}}" stepKey="waitForViewAndEditCartVisible"/> - <see selector="{{StorefrontMinicartSection.miniCartItemsText}}" userInput="{{productName}}" stepKey="seeInMiniCart"/> - </actionGroup> -+ -+ <!--Remove an item from the cart using minicart--> -+ <actionGroup name="removeProductFromMiniCart"> -+ <arguments> -+ <argument name="productName" type="string"/> -+ </arguments> -+ <conditionalClick selector="{{StorefrontMinicartSection.showCart}}" dependentSelector="{{StorefrontMinicartSection.miniCartOpened}}" visible="false" stepKey="openMiniCart"/> -+ <waitForElementVisible selector="{{StorefrontMinicartSection.viewAndEditCart}}" stepKey="waitForMiniCartOpen"/> -+ <click selector="{{StorefrontMinicartSection.deleteMiniCartItemByName(productName)}}" stepKey="clickDelete"/> -+ <waitForElementVisible selector="{{StoreFrontRemoveItemModalSection.message}}" stepKey="waitForConfirmationModal"/> -+ <see selector="{{StoreFrontRemoveItemModalSection.message}}" userInput="Are you sure you would like to remove this item from the shopping cart?" stepKey="seeDeleteConfirmationMessage"/> -+ <click selector="{{StoreFrontRemoveItemModalSection.ok}}" stepKey="confirmDelete"/> -+ <waitForPageLoad stepKey="waitForDeleteToFinish"/> -+ </actionGroup> -+ -+ <!--Check that the minicart is empty--> -+ <actionGroup name="assertMiniCartEmpty"> -+ <dontSeeElement selector="{{StorefrontMinicartSection.productCount}}" stepKey="dontSeeMinicartProductCount"/> -+ <click selector="{{StorefrontMinicartSection.showCart}}" stepKey="expandMinicart"/> -+ <see selector="{{StorefrontMinicartSection.minicartContent}}" userInput="You have no items in your shopping cart." stepKey="seeEmptyCartMessage"/> -+ </actionGroup> - </actionGroups> -diff --git a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontOpenCheckoutPageActionGroup.xml b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontOpenCheckoutPageActionGroup.xml -new file mode 100644 -index 00000000000..b18d476c02c ---- /dev/null -+++ b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontOpenCheckoutPageActionGroup.xml -@@ -0,0 +1,14 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> -+ <actionGroup name="StorefrontOpenCheckoutPageActionGroup"> -+ <amOnPage url="{{CheckoutPage.url}}" stepKey="openCheckoutPage" /> -+ <waitForPageLoad stepKey="waitForCheckoutPageLoaded" /> -+ </actionGroup> -+</actionGroups> -diff --git a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontOpenMiniCartActionGroup.xml b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontOpenMiniCartActionGroup.xml -new file mode 100644 -index 00000000000..7afcb070f3f ---- /dev/null -+++ b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontOpenMiniCartActionGroup.xml -@@ -0,0 +1,15 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> -+ <actionGroup name="StorefrontOpenMiniCartActionGroup"> -+ <waitForElementVisible selector="{{StorefrontMinicartSection.showCart}}" stepKey="waitForElementToBeVisible"/> -+ <click selector="{{StorefrontMinicartSection.showCart}}" stepKey="clickOnMiniCart"/> -+ <waitForPageLoad stepKey="waitForPageToLoad"/> -+ </actionGroup> -+</actionGroups> -diff --git a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontProductCartActionGroup.xml b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontProductCartActionGroup.xml -index 3842b3962c1..fb3f2958361 100644 ---- a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontProductCartActionGroup.xml -+++ b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontProductCartActionGroup.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"> - <!-- Add Product to Cart from the category page and check message and product count in Minicart --> - <actionGroup name="StorefrontAddCategoryProductToCartActionGroup"> - <arguments> -@@ -16,8 +16,9 @@ - </arguments> - <moveMouseOver selector="{{StorefrontCategoryProductSection.ProductInfoByName(product.name)}}" stepKey="moveMouseOverProduct" /> - <click selector="{{StorefrontCategoryProductSection.ProductAddToCartByName(product.name)}}" stepKey="clickAddToCart" /> -- <!-- @TODO: Use general message selector after MQE-694 is fixed --> -- <waitForElement selector="{{StorefrontMessagesSection.messageProductAddedToCart(product.name)}}" time="30" stepKey="assertMessage"/> -+ <waitForElementVisible selector="{{StorefrontCategoryMainSection.SuccessMsg}}" stepKey="waitForSuccessMessage" /> -+ <see selector="{{StorefrontCategoryMainSection.SuccessMsg}}" userInput="You added {{product.name}} to your shopping cart." stepKey="assertSuccessMessage"/> -+ <seeLink stepKey="assertLinkToShoppingCart" url="{{_ENV.MAGENTO_BASE_URL}}checkout/cart/" userInput="shopping cart" /> - <waitForText userInput="{{productCount}}" selector="{{StorefrontMinicartSection.productCount}}" time="30" stepKey="assertProductCount"/> - </actionGroup> - -@@ -30,8 +31,9 @@ - </arguments> - <moveMouseOver selector="{{StorefrontCategoryProductSection.ProductInfoByName(product.name)}}" stepKey="moveMouseOverProduct" /> - <click selector="{{StorefrontCategoryProductSection.ProductAddToCartByName(product.name)}}" stepKey="clickAddToCart" /> -- <!-- @TODO: Use general message selector after MQE-694 is fixed --> -- <waitForElement selector="{{StorefrontMessagesSection.messageProductAddedToCart(product.name)}}" time="30" stepKey="assertMessage"/> -+ <waitForElementVisible selector="{{StorefrontCategoryMainSection.SuccessMsg}}" stepKey="waitForSuccessMessage" /> -+ <see selector="{{StorefrontCategoryMainSection.SuccessMsg}}" userInput="You added {{product.name}} to your shopping cart." stepKey="assertSuccessMessage"/> -+ <seeLink stepKey="assertLinkToShoppingCart" url="{{_ENV.MAGENTO_BASE_URL}}checkout/cart/" userInput="shopping cart" /> - <waitForText userInput="{{checkQuantity}}" selector="{{StorefrontMinicartSection.productCount}}" time="30" stepKey="assertProductCount"/> - <conditionalClick selector="{{StorefrontMinicartSection.showCart}}" dependentSelector="{{StorefrontMinicartSection.miniCartOpened}}" visible="false" stepKey="openMiniCart"/> - <waitForElementVisible selector="{{StorefrontMinicartSection.viewAndEditCart}}" stepKey="waitForViewAndEditCartVisible"/> -@@ -46,11 +48,11 @@ - <argument name="productCount" type="string"/> - </arguments> - <click selector="{{StorefrontProductInfoMainSection.AddToCart}}" stepKey="clickAddToCart" /> -- <!-- @TODO: Use general message selector after MQE-694 is fixed --> -- <waitForElement selector="{{StorefrontMessagesSection.messageProductAddedToCart(product.name)}}" time="30" stepKey="assertMessage"/> -+ <waitForElementVisible selector="{{StorefrontCategoryMainSection.SuccessMsg}}" stepKey="waitForSuccessMessage" /> -+ <see selector="{{StorefrontProductPageSection.messagesBlock}}" userInput="You added {{product.name}} to your shopping cart." stepKey="assertSuccessMessage"/> -+ <seeLink stepKey="assertLinkToShoppingCart" url="{{_ENV.MAGENTO_BASE_URL}}checkout/cart/" userInput="shopping cart" /> - <waitForText userInput="{{productCount}}" selector="{{StorefrontMinicartSection.productCount}}" time="30" stepKey="assertProductCount"/> - </actionGroup> -- - <!-- Open the Minicart and check Simple Product --> - <actionGroup name="StorefrontOpenMinicartAndCheckSimpleProductActionGroup"> - <arguments> -@@ -75,17 +77,20 @@ - <!-- Check the Cart --> - <actionGroup name="StorefrontCheckCartActionGroup"> - <arguments> -- <argument name="subtotal"/> -- <argument name="shipping"/> -- <argument name="shippingMethod"/> -- <argument name="total"/> -+ <argument name="subtotal" type="string"/> -+ <argument name="shipping" type="string"/> -+ <argument name="shippingMethod" type="string"/> -+ <argument name="total" type="string"/> - </arguments> - <seeInCurrentUrl url="{{CheckoutCartPage.url}}" stepKey="assertUrl"/> -- <waitForText userInput="${{total}}" selector="{{CheckoutCartSummarySection.total}}" time="30" stepKey="waitForTotal"/> -- <see userInput="${{subtotal}}" selector="{{CheckoutCartSummarySection.subtotal}}" stepKey="assertSubtotal"/> -- <see userInput="${{shipping}}" selector="{{CheckoutCartSummarySection.shipping}}" stepKey="assertShipping"/> -+ <waitForPageLoad stepKey="waitForCartPage"/> -+ <conditionalClick selector="{{CheckoutCartSummarySection.shippingHeading}}" dependentSelector="{{CheckoutCartSummarySection.shippingMethodForm}}" visible="false" stepKey="openEstimateShippingSection"/> -+ <waitForElementVisible selector="{{CheckoutCartSummarySection.flatRateShippingMethod}}" stepKey="waitForShippingSection"/> -+ <checkOption selector="{{CheckoutCartSummarySection.flatRateShippingMethod}}" stepKey="selectShippingMethod"/> -+ <see userInput="{{subtotal}}" selector="{{CheckoutCartSummarySection.subtotal}}" stepKey="assertSubtotal"/> - <see userInput="({{shippingMethod}})" selector="{{CheckoutCartSummarySection.shippingMethod}}" stepKey="assertShippingMethod"/> -- <see userInput="${{total}}" selector="{{CheckoutCartSummarySection.total}}" stepKey="assertTotal"/> -+ <waitForText userInput="{{shipping}}" selector="{{CheckoutCartSummarySection.shipping}}" time="45" stepKey="assertShipping"/> -+ <see userInput="{{total}}" selector="{{CheckoutCartSummarySection.total}}" stepKey="assertTotal"/> - </actionGroup> - - <!-- Open the Cart from Minicart--> -@@ -106,4 +111,4 @@ - <fillField stepKey="fillZip" selector="{{CheckoutCartSummarySection.postcode}}" userInput="{{taxCode.zip}}"/> - <waitForPageLoad stepKey="waitForFormUpdate"/> - </actionGroup> --</actionGroups> -\ No newline at end of file -+</actionGroups> -diff --git a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontSelectOptionCheckBoxActionGroup.xml b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontSelectOptionCheckBoxActionGroup.xml -new file mode 100644 -index 00000000000..731ac03ca41 ---- /dev/null -+++ b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontSelectOptionCheckBoxActionGroup.xml -@@ -0,0 +1,16 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> -+ <actionGroup name="StorefrontSelectOptionCheckBoxActionGroup"> -+ <arguments> -+ <argument name="optionTitle" defaultValue="ProductOptionValueCheckbox"/> -+ </arguments> -+ <checkOption selector="{{StorefrontProductInfoMainSection.customOptionCheckBox(optionTitle.title)}}" stepKey="SelectOptionRadioButton"/> -+ </actionGroup> -+</actionGroups> -diff --git a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontSelectOptionDateActionGroup.xml b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontSelectOptionDateActionGroup.xml -new file mode 100644 -index 00000000000..37f2afd2a00 ---- /dev/null -+++ b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontSelectOptionDateActionGroup.xml -@@ -0,0 +1,21 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> -+ <actionGroup name="StorefrontSelectOptionDateActionGroup"> -+ <arguments> -+ <argument name="optionTitle" defaultValue="ProductOptionDate"/> -+ <argument name="month" type="string"/> -+ <argument name="day" type="string"/> -+ <argument name="year" type="string"/> -+ </arguments> -+ <selectOption selector="{{StorefrontProductInfoMainSection.customOptionMonth(optionTitle.title)}}" userInput="{{month}}" stepKey="selectMonthForOptionDate"/> -+ <selectOption selector="{{StorefrontProductInfoMainSection.customOptionDay(optionTitle.title)}}" userInput="{{day}}" stepKey="selectDayForOptionDate"/> -+ <selectOption selector="{{StorefrontProductInfoMainSection.customOptionYear(optionTitle.title)}}" userInput="{{year}}" stepKey="selectYearForOptionDate"/> -+ </actionGroup> -+</actionGroups> -diff --git a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontSelectOptionDateTimeActionGroup.xml b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontSelectOptionDateTimeActionGroup.xml -new file mode 100644 -index 00000000000..f2bac08a381 ---- /dev/null -+++ b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontSelectOptionDateTimeActionGroup.xml -@@ -0,0 +1,27 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> -+ <actionGroup name="StorefrontSelectOptionDateTimeActionGroup"> -+ <arguments> -+ <argument name="optionTitle" defaultValue="ProductOptionDateTime"/> -+ <argument name="month" type="string"/> -+ <argument name="day" type="string"/> -+ <argument name="year" type="string"/> -+ <argument name="hour" type="string"/> -+ <argument name="minute" type="string"/> -+ <argument name="dayPart" type="string"/> -+ </arguments> -+ <selectOption selector="{{StorefrontProductInfoMainSection.customOptionMonth(optionTitle.title)}}" userInput="{{month}}" stepKey="selectMonthForOptionDate"/> -+ <selectOption selector="{{StorefrontProductInfoMainSection.customOptionDay(optionTitle.title)}}" userInput="{{day}}" stepKey="selectDayForOptionDate"/> -+ <selectOption selector="{{StorefrontProductInfoMainSection.customOptionYear(optionTitle.title)}}" userInput="{{year}}" stepKey="selectYearForOptionDate"/> -+ <selectOption selector="{{StorefrontProductInfoMainSection.customOptionHour(optionTitle.title)}}" userInput="{{hour}}" stepKey="selectHourrForOptionDateTime"/> -+ <selectOption selector="{{StorefrontProductInfoMainSection.customOptionMinute(optionTitle.title)}}" userInput="{{minute}}" stepKey="selectMinuteForOptionDateTime"/> -+ <selectOption selector="{{StorefrontProductInfoMainSection.customOptionDayPart(optionTitle.title)}}" userInput="{{dayPart}}" stepKey="selectDayPartForOptionDateTime"/> -+ </actionGroup> -+</actionGroups> -diff --git a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontSelectOptionDropDownActionGroup.xml b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontSelectOptionDropDownActionGroup.xml -new file mode 100644 -index 00000000000..1f8cedae4ef ---- /dev/null -+++ b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontSelectOptionDropDownActionGroup.xml -@@ -0,0 +1,17 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> -+ <actionGroup name="StorefrontSelectOptionDropDownActionGroup"> -+ <arguments> -+ <argument name="optionTitle" defaultValue="ProductOptionDropDown"/> -+ <argument name="option" defaultValue="ProductOptionValueDropdown2.title" /> -+ </arguments> -+ <selectOption selector="{{StorefrontProductInfoMainSection.productOptionSelect(optionTitle.title)}}" userInput="{{option}}" stepKey="fillOptionDropDown"/> -+ </actionGroup> -+</actionGroups> -diff --git a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontSelectOptionMultiSelectActionGroup.xml b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontSelectOptionMultiSelectActionGroup.xml -new file mode 100644 -index 00000000000..ebf14b3f27d ---- /dev/null -+++ b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontSelectOptionMultiSelectActionGroup.xml -@@ -0,0 +1,17 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> -+ <actionGroup name="StorefrontSelectOptionMultiSelectActionGroup"> -+ <arguments> -+ <argument name="optionTitle" defaultValue="ProductOptionMultiSelect"/> -+ <argument name="option" defaultValue="ProductOptionValueMultiSelect2.title"/> -+ </arguments> -+ <selectOption selector="{{StorefrontProductInfoMainSection.productOptionSelect(optionTitle.title)}}" userInput="{{option}}" stepKey="selectOption"/> -+ </actionGroup> -+</actionGroups> -diff --git a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontSelectOptionRadioButtonActionGroup.xml b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontSelectOptionRadioButtonActionGroup.xml -new file mode 100644 -index 00000000000..844c9ede802 ---- /dev/null -+++ b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontSelectOptionRadioButtonActionGroup.xml -@@ -0,0 +1,17 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> -+ <actionGroup name="StorefrontSelectOptionRadioButtonActionGroup"> -+ <arguments> -+ <argument name="optionTitle" defaultValue="ProductOptionRadiobutton"/> -+ <argument name="optionPrice" defaultValue="ProductOptionValueRadioButtons2"/> -+ </arguments> -+ <checkOption selector="{{StorefrontProductInfoMainSection.productOptionRadioButtonsCheckbox(optionTitle.title, optionPrice.price)}}" stepKey="SelectOptionCheckBox"/> -+ </actionGroup> -+</actionGroups> -diff --git a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontSelectOptionTimeActionGroup.xml b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontSelectOptionTimeActionGroup.xml -new file mode 100644 -index 00000000000..bfa75eae702 ---- /dev/null -+++ b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontSelectOptionTimeActionGroup.xml -@@ -0,0 +1,21 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> -+ <actionGroup name="StorefrontSelectOptionTimeActionGroup"> -+ <arguments> -+ <argument name="optionTitle" defaultValue="ProductOptionTime"/> -+ <argument name="hour" type="string"/> -+ <argument name="minute" type="string"/> -+ <argument name="dayPart" type="string"/> -+ </arguments> -+ <selectOption selector="{{StorefrontProductInfoMainSection.customOptionHour(optionTitle.title)}}" userInput="{{hour}}" stepKey="selectHourrForOptionDateTime"/> -+ <selectOption selector="{{StorefrontProductInfoMainSection.customOptionMinute(optionTitle.title)}}" userInput="{{minute}}" stepKey="selectMinuteForOptionDateTime"/> -+ <selectOption selector="{{StorefrontProductInfoMainSection.customOptionDayPart(optionTitle.title)}}" userInput="{{dayPart}}" stepKey="selectDayPartForOptionDateTime"/> -+ </actionGroup> -+</actionGroups> -diff --git a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontShippmentFromActionGroup.xml b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontShippmentFromActionGroup.xml -new file mode 100644 -index 00000000000..d3d96cb9c74 ---- /dev/null -+++ b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontShippmentFromActionGroup.xml -@@ -0,0 +1,29 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> -+ <!--Fill shipment form for free shipping--> -+ <actionGroup name="ShipmentFormFreeShippingActionGroup"> -+ <fillField selector="{{CheckoutShippingSection.email}}" userInput="{{CustomerEntityOne.email}}" stepKey="setCustomerEmail"/> -+ <fillField selector="{{CheckoutShippingSection.firstName}}" userInput="{{CustomerEntityOne.firstname}}" stepKey="SetCustomerFirstName"/> -+ <fillField selector="{{CheckoutShippingSection.lastName}}" userInput="{{CustomerEntityOne.lastname}}" stepKey="SetCustomerLastName"/> -+ <fillField selector="{{CheckoutShippingSection.street}}" userInput="{{CustomerAddressSimple.street}}" stepKey="SetCustomerStreetAddress"/> -+ <fillField selector="{{CheckoutShippingSection.city}}" userInput="{{CustomerAddressSimple.city}}" stepKey="SetCustomerCity"/> -+ <fillField selector="{{CheckoutShippingSection.postcode}}" userInput="{{CustomerAddressSimple.postcode}}" stepKey="SetCustomerZipCode"/> -+ <fillField selector="{{CheckoutShippingSection.telephone}}" userInput="{{CustomerAddressSimple.telephone}}" stepKey="SetCustomerPhoneNumber"/> -+ <click selector="{{CheckoutShippingSection.region}}" stepKey="clickToSetState"/> -+ <click selector="{{CheckoutShippingSection.state}}" stepKey="clickToChooseState"/> -+ <see userInput="$0.00 Free Free Shipping" selector="{{CheckoutShippingMethodsSection.shippingMethodRowByName('Free Shipping')}}" stepKey="seeShippingMethod" after="clickToChooseState"/> -+ <click selector="{{CheckoutShippingMethodsSection.checkShippingMethodByName('Free Shipping')}}" stepKey="selectFlatShippingMethod" after="seeShippingMethod"/> -+ <waitForLoadingMaskToDisappear stepKey="waitForLoadingMask" after="selectFlatShippingMethod"/> -+ <click selector="{{CheckoutShippingSection.next}}" stepKey="clickToSaveShippingInfo"/> -+ <waitForPageLoad time="5" stepKey="waitForReviewAndPaymentsPageIsLoaded"/> -+ <seeInCurrentUrl url="payment" stepKey="reviewAndPaymentIsShown"/> -+ </actionGroup> -+</actionGroups> -diff --git a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontUpdateProductQtyMiniShoppingCartActionGroup.xml b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontUpdateProductQtyMiniShoppingCartActionGroup.xml -new file mode 100644 -index 00000000000..ee8b761a452 ---- /dev/null -+++ b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontUpdateProductQtyMiniShoppingCartActionGroup.xml -@@ -0,0 +1,28 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> -+ <actionGroup name="StorefrontUpdateProductQtyMiniShoppingCartActionGroup"> -+ <arguments> -+ <argument name="product" type="entity" /> -+ <argument name="quote" type="entity" /> -+ </arguments> -+ -+ <click selector="{{StorefrontMinicartSection.showCart}}" stepKey="goToMiniShoppingCart"/> -+ -+ <!-- Clearing QTY field --> -+ <doubleClick selector="{{StorefrontMinicartSection.itemQuantityBySku(product.sku)}}" stepKey="doubleClickOnQtyInput" /> -+ <pressKey selector="{{StorefrontMinicartSection.itemQuantityBySku(product.sku)}}" parameterArray="[\WebDriverKeys::DELETE]" stepKey="clearQty"/> -+ <!-- Clearing QTY field --> -+ -+ <fillField selector="{{StorefrontMinicartSection.itemQuantityBySku(product.sku)}}" userInput="{{quote.qty}}" stepKey="changeQty"/> -+ <click selector="{{StorefrontMinicartSection.itemQuantityUpdateBySku(product.sku)}}" stepKey="clickUpdateButton"/> -+ <waitForPageLoad stepKey="waitForProductQtyUpdate" /> -+ -+ </actionGroup> -+</actionGroups> -diff --git a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/VerifyCheckoutPaymentOrderSummaryActionGroup.xml b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/VerifyCheckoutPaymentOrderSummaryActionGroup.xml -new file mode 100644 -index 00000000000..7937092965f ---- /dev/null -+++ b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/VerifyCheckoutPaymentOrderSummaryActionGroup.xml -@@ -0,0 +1,22 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> -+ <!--Fill shipment form for free shipping--> -+ <actionGroup name="VerifyCheckoutPaymentOrderSummaryActionGroup"> -+ <arguments> -+ <argument name="orderSummarySubTotal" type="string"/> -+ <argument name="orderSummaryShippingTotal" type="string"/> -+ <argument name="orderSummaryTotal" type="string"/> -+ </arguments> -+ <see selector="{{CheckoutPaymentSection.orderSummarySubtotal}}" userInput="{{orderSummarySubTotal}}" stepKey="seeCorrectSubtotal"/> -+ <see selector="{{CheckoutPaymentSection.orderSummaryShippingTotal}}" userInput="{{orderSummaryShippingTotal}}" stepKey="seeCorrectShipping"/> -+ <see selector="{{CheckoutPaymentSection.orderSummaryTotal}}" userInput="{{orderSummaryTotal}}" stepKey="seeCorrectOrderTotal"/> -+ </actionGroup> -+</actionGroups> -diff --git a/app/code/Magento/Checkout/Test/Mftf/Data/ConfigData.xml b/app/code/Magento/Checkout/Test/Mftf/Data/ConfigData.xml -new file mode 100644 -index 00000000000..cd9cd59d20b ---- /dev/null -+++ b/app/code/Magento/Checkout/Test/Mftf/Data/ConfigData.xml -@@ -0,0 +1,91 @@ -+<?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"> -+ <!-- Free shipping --> -+ <entity name="EnableFreeShippingConfigData"> -+ <data key="path">carriers/freeshipping/active</data> -+ <data key="scope">carriers</data> -+ <data key="scope_id">1</data> -+ <data key="label">Yes</data> -+ <data key="value">1</data> -+ </entity> -+ <entity name="EnableFreeShippingToSpecificCountriesConfigData"> -+ <data key="path">carriers/freeshipping/sallowspecific</data> -+ <data key="scope">carriers</data> -+ <data key="scope_id">1</data> -+ <data key="label">Specific Countries</data> -+ <data key="value">1</data> -+ </entity> -+ <entity name="EnableFreeShippingToAfghanistanConfigData"> -+ <data key="path">carriers/freeshipping/specificcountry</data> -+ <data key="scope">carriers</data> -+ <data key="scope_id">1</data> -+ <data key="label">Afghanistan</data> -+ <data key="value">AF</data> -+ </entity> -+ <entity name="EnableFreeShippingToAllAllowedCountriesConfigData"> -+ <data key="path">carriers/freeshipping/sallowspecific</data> -+ <data key="scope">carriers</data> -+ <data key="scope_id">1</data> -+ <data key="label">All Allowed Countries</data> -+ <data key="value">0</data> -+ </entity> -+ <entity name="DisableFreeShippingConfigData"> -+ <data key="path">carriers/freeshipping/active</data> -+ <data key="scope">carriers</data> -+ <data key="scope_id">1</data> -+ <data key="label">No</data> -+ <data key="value">0</data> -+ </entity> -+ -+ <!-- Flat Rate shipping --> -+ <entity name="EnableFlatRateConfigData"> -+ <data key="path">carriers/flatrate/active</data> -+ <data key="scope">carriers</data> -+ <data key="scope_id">1</data> -+ <data key="label">Yes</data> -+ <data key="value">1</data> -+ </entity> -+ <entity name="EnableFlatRateDefaultPriceConfigData"> -+ <data key="path">carriers/flatrate/price</data> -+ <data key="scope">carriers</data> -+ <data key="scope_id">1</data> -+ <data key="label">Yes</data> -+ <data key="value">5.00</data> -+ </entity> -+ <entity name="EnableFlatRateToSpecificCountriesConfigData"> -+ <data key="path">carriers/flatrate/sallowspecific</data> -+ <data key="scope">carriers</data> -+ <data key="scope_id">1</data> -+ <data key="label">Specific Countries</data> -+ <data key="value">1</data> -+ </entity> -+ <entity name="EnableFlatRateToAfghanistanConfigData"> -+ <data key="path">carriers/flatrate/specificcountry</data> -+ <data key="scope">carriers</data> -+ <data key="scope_id">1</data> -+ <data key="label">Afghanistan</data> -+ <data key="value">AF</data> -+ </entity> -+ <entity name="EnableFlatRateToAllAllowedCountriesConfigData"> -+ <data key="path">carriers/flatrate/sallowspecific</data> -+ <data key="scope">carriers</data> -+ <data key="scope_id">1</data> -+ <data key="label">All Allowed Countries</data> -+ <data key="value">0</data> -+ </entity> -+ <entity name="DisableFlatRateConfigData"> -+ <data key="path">carriers/flatrate/active</data> -+ <data key="scope">carriers</data> -+ <data key="scope_id">1</data> -+ <data key="label">No</data> -+ <data key="value">0</data> -+ </entity> -+</entities> -diff --git a/app/code/Magento/Checkout/Test/Mftf/Data/ConstData.xml b/app/code/Magento/Checkout/Test/Mftf/Data/ConstData.xml -index 1703d7255e1..f946d04fc9f 100644 ---- a/app/code/Magento/Checkout/Test/Mftf/Data/ConstData.xml -+++ b/app/code/Magento/Checkout/Test/Mftf/Data/ConstData.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"> - <!-- @TODO: Get rid off this workaround and its usages after MQE-498 is implemented --> - <entity name="CONST" type="CONST"> - <data key="successGuestCheckoutOrderNumberMessage">Your order # is:</data> -diff --git a/app/code/Magento/Checkout/Test/Mftf/Data/CountryData.xml b/app/code/Magento/Checkout/Test/Mftf/Data/CountryData.xml -new file mode 100644 -index 00000000000..7fc349bf9f0 ---- /dev/null -+++ b/app/code/Magento/Checkout/Test/Mftf/Data/CountryData.xml -@@ -0,0 +1,38 @@ -+<?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="Countries" type="countryArray"> -+ <array key="country"> -+ <item>Bahamas</item> -+ </array> -+ </entity> -+ <entity name="DefaultCountriesWithRequiredRegions" type="countryArray"> -+ <array key="country"> -+ <item>Australia</item> -+ <item>Brazil</item> -+ <item>Canada</item> -+ <item>Croatia</item> -+ <item>Estonia</item> -+ <item>India</item> -+ <item>Latvia</item> -+ <item>Lithuania</item> -+ <item>Romania</item> -+ <item>Spain</item> -+ <item>Switzerland</item> -+ <item>United States</item> -+ <item>Australia</item> -+ </array> -+ </entity> -+ <entity name="CustomCountryWithRequiredRegion" type="countryArray"> -+ <array key="country"> -+ <item>United Kingdom</item> -+ </array> -+ </entity> -+</entities> -diff --git a/app/code/Magento/Checkout/Test/Mftf/Data/EstimateAndTaxData.xml b/app/code/Magento/Checkout/Test/Mftf/Data/EstimateAndTaxData.xml -new file mode 100644 -index 00000000000..36dea5a521a ---- /dev/null -+++ b/app/code/Magento/Checkout/Test/Mftf/Data/EstimateAndTaxData.xml -@@ -0,0 +1,16 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<entities xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataProfileSchema.xsd"> -+ <entity name="EstimateAddressCalifornia"> -+ <data key="country">United States</data> -+ <data key="state">California</data> -+ <data key="zipCode">90240</data> -+ </entity> -+</entities> -diff --git a/app/code/Magento/Checkout/Test/Mftf/Data/QuoteData.xml b/app/code/Magento/Checkout/Test/Mftf/Data/QuoteData.xml -index 51fb264a16a..7c4f814e2b9 100644 ---- a/app/code/Magento/Checkout/Test/Mftf/Data/QuoteData.xml -+++ b/app/code/Magento/Checkout/Test/Mftf/Data/QuoteData.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"> - <!-- @TODO: Get rid off this workaround and its usages after MQE-498 is implemented --> - <entity name="E2EB2CQuote" type="Quote"> - <data key="subtotal">480.00</data> -@@ -15,4 +15,43 @@ - <data key="total">495.00</data> - <data key="shippingMethod">Flat Rate - Fixed</data> - </entity> -+ <entity name="simpleOrderQty2" type="Quote"> -+ <data key="price">560.00</data> -+ <data key="qty">2</data> -+ <data key="subtotal">1,120.00</data> -+ <data key="shipping">10.00</data> -+ <data key="total">1,130.00</data> -+ <data key="shippingMethod">Flat Rate - Fixed</data> -+ <data key="currency">$</data> -+ </entity> -+ <entity name="quoteQty3Price123" type="Quote"> -+ <data key="price">123.00</data> -+ <data key="qty">3</data> -+ <data key="subtotal">369.00</data> -+ <data key="currency">$</data> -+ </entity> -+ <entity name="quoteQty11Subtotal1320" type="Quote"> -+ <data key="price">100.00</data> -+ <data key="customOptionsPrice">20</data> -+ <data key="qty">11</data> -+ <data key="subtotal">1,320.00</data> -+ <data key="currency">$</data> -+ </entity> -+ <entity name="quoteQty2Price123" type="Quote"> -+ <data key="price">123.00</data> -+ <data key="qty">2</data> -+ <data key="shipping">10.00</data> -+ <data key="subtotal">246.00</data> -+ <data key="total">256.00</data> -+ <data key="currency">$</data> -+ </entity> -+ <entity name="quoteQty2Subtotal266" type="Quote"> -+ <data key="qty">2</data> -+ <data key="customOptionsPrice">20</data> -+ <data key="price">143</data> -+ <data key="subtotal">266.00</data> -+ <data key="shipping">10.00</data> -+ <data key="total">276.00</data> -+ <data key="currency">$</data> -+ </entity> - </entities> -diff --git a/app/code/Magento/Checkout/Test/Mftf/Page/CheckoutCartPage.xml b/app/code/Magento/Checkout/Test/Mftf/Page/CheckoutCartPage.xml -index 433d397ee81..a77b07a129d 100644 ---- a/app/code/Magento/Checkout/Test/Mftf/Page/CheckoutCartPage.xml -+++ b/app/code/Magento/Checkout/Test/Mftf/Page/CheckoutCartPage.xml -@@ -7,9 +7,11 @@ - --> - - <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="CheckoutCartPage" url="/checkout/cart" module="Magento_Checkout" area="storefront"> - <section name="CheckoutCartProductSection"/> - <section name="CheckoutCartSummarySection"/> -+ <section name="CheckoutCartCrossSellSection"/> -+ <section name="CheckoutCartMessageSection"/> - </page> - </pages> -diff --git a/app/code/Magento/Checkout/Test/Mftf/Page/CheckoutPage.xml b/app/code/Magento/Checkout/Test/Mftf/Page/CheckoutPage.xml -index aa11d42275a..d3fa045e465 100644 ---- a/app/code/Magento/Checkout/Test/Mftf/Page/CheckoutPage.xml -+++ b/app/code/Magento/Checkout/Test/Mftf/Page/CheckoutPage.xml -@@ -7,7 +7,7 @@ - --> - - <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="CheckoutPage" url="/checkout" area="storefront" module="Magento_Checkout"> - <section name="CheckoutShippingSection"/> - <section name="CheckoutShippingMethodsSection"/> -diff --git a/app/code/Magento/Checkout/Test/Mftf/Page/CheckoutShippingPage.xml b/app/code/Magento/Checkout/Test/Mftf/Page/CheckoutShippingPage.xml -index 59c3b20aca6..c8641f7d8fb 100644 ---- a/app/code/Magento/Checkout/Test/Mftf/Page/CheckoutShippingPage.xml -+++ b/app/code/Magento/Checkout/Test/Mftf/Page/CheckoutShippingPage.xml -@@ -7,9 +7,10 @@ - --> - - <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="CheckoutShippingPage" url="/checkout/#shipping" module="Checkout" area="storefront"> - <section name="CheckoutShippingGuestInfoSection"/> - <section name="CheckoutShippingSection"/> -+ <section name="StorefrontCheckoutAddressPopupSection"/> - </page> - </pages> -diff --git a/app/code/Magento/Checkout/Test/Mftf/Page/CheckoutSuccessPage.xml b/app/code/Magento/Checkout/Test/Mftf/Page/CheckoutSuccessPage.xml -index 1c3293267e2..ebca0651b45 100644 ---- a/app/code/Magento/Checkout/Test/Mftf/Page/CheckoutSuccessPage.xml -+++ b/app/code/Magento/Checkout/Test/Mftf/Page/CheckoutSuccessPage.xml -@@ -7,7 +7,7 @@ - --> - - <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="CheckoutSuccessPage" url="/checkout/onepage/success/" area="storefront" module="Magento_Checkout"> - <section name="CheckoutSuccessMainSection"/> - <section name="CheckoutSuccessRegisterSection"/> -diff --git a/app/code/Magento/Checkout/Test/Mftf/Page/GuestCheckoutReviewAndPaymentsPage.xml b/app/code/Magento/Checkout/Test/Mftf/Page/GuestCheckoutReviewAndPaymentsPage.xml -index cac9c934cd6..90f8a914b4f 100644 ---- a/app/code/Magento/Checkout/Test/Mftf/Page/GuestCheckoutReviewAndPaymentsPage.xml -+++ b/app/code/Magento/Checkout/Test/Mftf/Page/GuestCheckoutReviewAndPaymentsPage.xml -@@ -6,7 +6,7 @@ - */ - --> - --<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"> -+<pages xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/PageObject.xsd"> - <page name="GuestCheckoutReviewAndPaymentsPage" url="/checkout/#payment" area="storefront" module="Magento_Checkout"> - <section name="CheckoutPaymentSection"/> - </page> -diff --git a/app/code/Magento/Checkout/Test/Mftf/Section/AdminCheckoutPaymentSection.xml b/app/code/Magento/Checkout/Test/Mftf/Section/AdminCheckoutPaymentSection.xml -new file mode 100644 -index 00000000000..2b5dd512bc4 ---- /dev/null -+++ b/app/code/Magento/Checkout/Test/Mftf/Section/AdminCheckoutPaymentSection.xml -@@ -0,0 +1,14 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> -+ <section name="AdminCheckoutPaymentSection"> -+ <element name="checkBillingMethodByName" type="radio" selector="//div[@id='order-billing_method']//dl[@class='admin__payment-methods']//dt//label[contains(., '{{methodName}}')]/..//input" parameterized="true"/> -+ </section> -+</sections> -diff --git a/app/code/Magento/Checkout/Test/Mftf/Section/AdminDataGridHeaderSection.xml b/app/code/Magento/Checkout/Test/Mftf/Section/AdminDataGridHeaderSection.xml -index 56062f96152..3e1f902f6c3 100644 ---- a/app/code/Magento/Checkout/Test/Mftf/Section/AdminDataGridHeaderSection.xml -+++ b/app/code/Magento/Checkout/Test/Mftf/Section/AdminDataGridHeaderSection.xml -@@ -7,7 +7,7 @@ - --> - - <sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -- xsi:noNamespaceSchemaLocation="../../../../../../vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Page/etc/SectionObject.xsd"> -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> - <section name="AdminDataGridHeaderSection"> - <element name="attributeCodeFilterInput" type="input" selector=".admin__data-grid-filters input[name='attribute_code']"/> - </section> -diff --git a/app/code/Magento/Checkout/Test/Mftf/Section/CheckoutCartCrossSellSection.xml b/app/code/Magento/Checkout/Test/Mftf/Section/CheckoutCartCrossSellSection.xml -new file mode 100644 -index 00000000000..aa23f336477 ---- /dev/null -+++ b/app/code/Magento/Checkout/Test/Mftf/Section/CheckoutCartCrossSellSection.xml -@@ -0,0 +1,17 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> -+ <section name="CheckoutCartCrossSellSection"> -+ <element name="title" type="text" selector=".block.crosssell .block-title"/> -+ <element name="products" type="block" selector=".block.crosssell .block-content"/> -+ <element name="productRowByName" type="block" selector="//li[@class='item product product-item'and .//a[@title='{{name}}']]" parameterized="true"/> -+ <element name="addToCart" type="block" selector="//button[@title='Add to Cart']"/> -+ </section> -+</sections> -\ No newline at end of file -diff --git a/app/code/Magento/Checkout/Test/Mftf/Section/CheckoutCartMessageSection.xml b/app/code/Magento/Checkout/Test/Mftf/Section/CheckoutCartMessageSection.xml -new file mode 100644 -index 00000000000..cf15cdf15cf ---- /dev/null -+++ b/app/code/Magento/Checkout/Test/Mftf/Section/CheckoutCartMessageSection.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="CheckoutCartMessageSection"> -+ <element name="successMessage" type="text" selector=".message.message-success.success>div" /> -+ <element name="errorMessage" type="text" selector=".message-error.error.message>div" /> -+ <element name="emptyCartMessage" type="text" selector=".cart-empty>p"/> -+ </section> -+</sections> -diff --git a/app/code/Magento/Checkout/Test/Mftf/Section/CheckoutCartProductSection.xml b/app/code/Magento/Checkout/Test/Mftf/Section/CheckoutCartProductSection.xml -index b3e3f082319..2024d249418 100644 ---- a/app/code/Magento/Checkout/Test/Mftf/Section/CheckoutCartProductSection.xml -+++ b/app/code/Magento/Checkout/Test/Mftf/Section/CheckoutCartProductSection.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="CheckoutCartProductSection"> - <element name="ProductLinkByName" type="button" - selector="//main//table[@id='shopping-cart-table']//tbody//tr//strong[contains(@class, 'product-item-name')]//a[contains(text(), '{{var1}}')]" -@@ -15,6 +15,9 @@ - <element name="ProductPriceByName" type="text" - selector="//main//table[@id='shopping-cart-table']//tbody//tr[..//strong[contains(@class, 'product-item-name')]//a/text()='{{var1}}'][1]//td[contains(@class, 'price')]//span[@class='price']" - parameterized="true"/> -+ <element name="ProductRegularPriceByName" type="text" -+ selector="//div[descendant::*[contains(text(), '{{var1}}')]]//*[contains(@class, 'subtotal')]" -+ parameterized="true"/> - <element name="ProductImageByName" type="text" - selector="//main//table[@id='shopping-cart-table']//tbody//tr//img[contains(@class, 'product-image-photo') and @alt='{{var1}}']" - parameterized="true"/> -@@ -24,12 +27,24 @@ - <element name="ProductOptionByNameAndAttribute" type="input" - selector="//main//table[@id='shopping-cart-table']//tbody//tr[.//strong[contains(@class, 'product-item-name')]//a[contains(text(), '{{var1}}')]]//dl[@class='item-options']//dt[.='{{var2}}']/following-sibling::dd[1]" - parameterized="true"/> -+ <element name="ProductPriceByOption" type="text" selector="//*[contains(@class, 'item-options')]/dd[normalize-space(.)='{{var1}}']/ancestor::tr//td[contains(@class, 'price')]//span[@class='price']" 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="removeProductByName" type="text" selector="//*[contains(text(), '{{productName}}')]/ancestor::tbody//a[@class='action action-delete']" parameterized="true" timeout="30"/> -+ <element name="productName" type="text" selector="//tbody[@class='cart item']//strong[@class='product-item-name']"/> - <element name="nthItemOption" type="block" selector=".item:nth-of-type({{numElement}}) .item-options" parameterized="true"/> - <element name="nthEditButton" type="block" selector=".item:nth-of-type({{numElement}}) .action-edit" parameterized="true"/> - <element name="nthBundleOptionName" type="text" selector=".product-item-details .item-options:nth-of-type({{numOption}}) dt" parameterized="true"/> - <element name="productSubtotalByName" type="input" selector="//main//table[@id='shopping-cart-table']//tbody//tr[..//strong[contains(@class, 'product-item-name')]//a/text()='{{var1}}'][1]//td[contains(@class, 'subtotal')]//span[@class='price']" parameterized="true"/> - <element name="updateShoppingCartButton" type="button" selector="#form-validate button[type='submit'].update" timeout="30"/> -+ <element name="qty" type="input" selector="//input[@data-cart-item-id='{{var}}'][@title='Qty']" parameterized="true"/> -+ <element name="qtyByContains" type="text" selector="//input[contains(@data-cart-item-id, '{{sku}}')][@title='Qty']" parameterized="true"/> -+ <element name="messageErrorItem" type="text" selector="#sku-stock-failed-"/> -+ <element name="messageErrorNeedChooseOptions" type="text" selector="//*[contains(@class, 'error')]/div"/> -+ <element name="productOptionsLink" type="text" selector="a.action.configure"/> -+ <element name="productOptionLabel" type="text" selector="//dl[@class='item-options']"/> -+ <element name="checkoutCartProductPrice" type="text" selector="//td[@class='col price']//span[@class='price']"/> -+ <element name="checkoutCartSubtotal" type="text" selector="//td[@class='col subtotal']//span[@class='price']"/> -+ <element name="emptyCart" selector=".cart-empty" type="text"/> - </section> - </sections> -diff --git a/app/code/Magento/Checkout/Test/Mftf/Section/CheckoutCartSummarySection.xml b/app/code/Magento/Checkout/Test/Mftf/Section/CheckoutCartSummarySection.xml -index 01b483c8ecf..e6637bec765 100644 ---- a/app/code/Magento/Checkout/Test/Mftf/Section/CheckoutCartSummarySection.xml -+++ b/app/code/Magento/Checkout/Test/Mftf/Section/CheckoutCartSummarySection.xml -@@ -7,19 +7,30 @@ - --> - - <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="CheckoutCartSummarySection"> -+ <element name="expandShoppingCartSummary" type="button" selector="//*[contains(@class, 'items-in-cart')][not(contains(@class, 'active'))]"/> -+ <element name="elementPosition" type="text" selector=".data.table.totals > tbody tr:nth-of-type({{value}}) > th" parameterized="true"/> - <element name="subtotal" type="text" selector="//*[@id='cart-totals']//tr[@class='totals sub']//td//span[@class='price']"/> -+ <element name="shippingMethodForm" type="text" selector="#co-shipping-method-form"/> - <element name="shippingMethod" type="text" selector="//*[@id='cart-totals']//tr[@class='totals shipping excl']//th//span[@class='value']"/> - <element name="shipping" type="text" selector="//*[@id='cart-totals']//tr[@class='totals shipping excl']//td//span[@class='price']"/> -+ <element name="shippingAmount" type="text" selector="//*[@id='cart-totals']//tr[@class='totals shipping excl']//td//span[@class='price' and contains(text(), '{{amount}}')]" parameterized="true"/> - <element name="total" type="text" selector="//*[@id='cart-totals']//tr[@class='grand totals']//td//span[@class='price']"/> -+ <element name="totalAmount" type="text" selector="//*[@id='cart-totals']//tr[@class='grand totals']//td//span[@class='price' and contains(text(), '{{amount}}')]" parameterized="true"/> - <element name="proceedToCheckout" type="button" selector=".action.primary.checkout span" timeout="30"/> - <element name="discountAmount" type="text" selector="td[data-th='Discount']"/> -- <element name="shippingHeading" type="button" selector="#block-shipping-heading"/> -+ <element name="shippingHeading" type="button" selector="#block-shipping-heading" timeout="60"/> - <element name="postcode" type="input" selector="input[name='postcode']" timeout="10"/> - <element name="stateProvince" type="select" selector="select[name='region_id']" timeout="10"/> -+ <element name="stateProvinceInput" type="input" selector="input[name='region']"/> - <element name="country" type="select" selector="select[name='country_id']" timeout="10"/> -+ <element name="countryParameterized" type="select" selector="select[name='country_id'] > option:nth-child({{var}})" timeout="10" parameterized="true"/> - <element name="estimateShippingAndTax" type="text" selector="#block-shipping-heading" timeout="5"/> -- <element name="flatRateShippingMethod" type="radio" selector="#s_method_flatrate_flatrate" timeout="30"/> -+ <element name="flatRateShippingMethod" type="input" selector="#s_method_flatrate_flatrate" timeout="30"/> -+ <element name="shippingMethodLabel" type="text" selector="#co-shipping-method-form dl dt span"/> -+ <element name="methodName" type="text" selector="#co-shipping-method-form label"/> -+ <element name="shippingPrice" type="text" selector="#co-shipping-method-form span .price"/> -+ <element name="shippingMethodElementId" type="radio" selector="#s_method_{{carrierCode}}_{{methodCode}}" parameterized="true"/> - </section> - </sections> -diff --git a/app/code/Magento/Checkout/Test/Mftf/Section/CheckoutHeaderSection.xml b/app/code/Magento/Checkout/Test/Mftf/Section/CheckoutHeaderSection.xml -index ca42eff89cf..babbe51746d 100644 ---- a/app/code/Magento/Checkout/Test/Mftf/Section/CheckoutHeaderSection.xml -+++ b/app/code/Magento/Checkout/Test/Mftf/Section/CheckoutHeaderSection.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="CheckoutHeaderSection"> - <element name="shippingMethodStep" type="text" selector=".opc-progress-bar-item:nth-of-type(1)"/> - <element name="reviewAndPaymentsStep" type="text" selector=".opc-progress-bar-item:nth-of-type(2)"/> -diff --git a/app/code/Magento/Checkout/Test/Mftf/Section/CheckoutOrderSummarySection.xml b/app/code/Magento/Checkout/Test/Mftf/Section/CheckoutOrderSummarySection.xml -index cb079d2f036..d3ad2aed969 100644 ---- a/app/code/Magento/Checkout/Test/Mftf/Section/CheckoutOrderSummarySection.xml -+++ b/app/code/Magento/Checkout/Test/Mftf/Section/CheckoutOrderSummarySection.xml -@@ -7,11 +7,17 @@ - --> - - <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="CheckoutOrderSummarySection"> - <element name="miniCartTab" type="button" selector=".title[role='tab']"/> - <element name="productItemName" type="text" selector=".product-item-name"/> - <element name="productItemQty" type="text" selector=".value"/> - <element name="productItemPrice" type="text" selector=".price"/> -+ <element name="orderNumber" type="text" selector="//div[@class='checkout-success']//a"/> -+ <element name="shippingAddress" type="textarea" selector="//*[@class='box box-address-shipping']//address"/> -+ <element name="billingAddress" type="textarea" selector="//*[@class='box box-address-billing']//address"/> -+ <element name="additionalAddress" type="text" selector=".block.block-addresses-list"/> -+ <element name="miniCartTabClosed" type="button" selector=".title[aria-expanded='false']" timeout="30"/> -+ <element name="itemsQtyInCart" type="text" selector=".items-in-cart > .title > strong > span"/> - </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 a12852ea1ef..e1c2a3b1550 100644 ---- a/app/code/Magento/Checkout/Test/Mftf/Section/CheckoutPaymentSection.xml -+++ b/app/code/Magento/Checkout/Test/Mftf/Section/CheckoutPaymentSection.xml -@@ -7,27 +7,30 @@ - --> - - <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="CheckoutPaymentSection"> - <element name="isPaymentSection" type="text" selector="//*[@class='opc-progress-bar']/li[contains(@class, '_active') and span[contains(.,'Review & Payments')]]"/> - <element name="availablePaymentSolutions" type="text" selector="#checkout-payment-method-load>div>div>div:nth-child(2)>div.payment-method-title.field.choice"/> - <element name="notAvailablePaymentSolutions" type="text" selector="#checkout-payment-method-load>div>div>div.payment-method._active>div.payment-method-title.field.choice"/> - <element name="billingNewAddressForm" type="text" selector="[data-form='billing-new-address']"/> -+ <element name="billingAddressNotSameCheckbox" type="checkbox" selector="#billing-address-same-as-shipping-checkmo"/> -+ <element name="editAddress" type="button" selector="button.action.action-edit-address"/> - <element name="placeOrderDisabled" type="button" selector="#checkout-payment-method-load button.disabled"/> - <element name="update" type="button" selector=".payment-method-billing-address .action.action-update"/> - <element name="guestFirstName" type="input" selector=".billing-address-form input[name*='firstname']"/> - <element name="guestLastName" type="input" selector=".billing-address-form input[name*='lastname']"/> - <element name="guestStreet" type="input" selector=".billing-address-form input[name*='street[0]']"/> - <element name="guestCity" type="input" selector=".billing-address-form input[name*='city']"/> -+ <element name="guestCountry" type="select" selector=".billing-address-form select[name*='country_id']"/> - <element name="guestRegion" type="select" selector=".billing-address-form select[name*='region_id']"/> - <element name="guestPostcode" type="input" selector=".billing-address-form input[name*='postcode']"/> - <element name="guestTelephone" type="input" selector=".billing-address-form input[name*='telephone']"/> -- <element name="billingAddress" type="text" selector="div.billing-address-details"/> -+ <element name="billingAddress" type="text" selector=".payment-method._active div.billing-address-details"/> - <element name="cartItems" type="text" selector="ol.minicart-items"/> - <element name="cartItemsArea" type="button" selector="div.block.items-in-cart"/> -- <element name="cartItemsAreaActive" type="textarea" selector="div.block.items-in-cart.active"/> -+ <element name="cartItemsAreaActive" type="textarea" selector="div.block.items-in-cart.active" timeout="30"/> - <element name="checkMoneyOrderPayment" type="radio" selector="input#checkmo.radio" timeout="30"/> -- <element name="placeOrder" type="button" selector="button.action.primary.checkout" timeout="30"/> -+ <element name="placeOrder" type="button" selector=".payment-method._active button.action.primary.checkout" timeout="30"/> - <element name="paymentSectionTitle" type="text" selector="//*[@id='checkout-payment-method-load']//div[text()='Payment Method']" /> - <element name="orderSummarySubtotal" type="text" selector="//tr[@class='totals sub']//span[@class='price']" /> - <element name="orderSummaryShippingTotal" type="text" selector="//tr[@class='totals shipping excl']//span[@class='price']" /> -@@ -36,15 +39,25 @@ - <element name="ProductItemByName" type="text" selector="//div[@class='product-item-details']//strong[@class='product-item-name'][text()='{{var1}}']" parameterized="true" /> - <element name="ProductOptionsByProductItemName" type="text" selector="//div[@class='product-item-details']//strong[@class='product-item-name'][text()='{{var1}}']//ancestor::div[@class='product-item-details']//div[@class='product options']" parameterized="true" /> - <element name="ProductOptionsActiveByProductItemName" type="text" selector="//div[@class='product-item-details']//strong[@class='product-item-name'][text()='{{var1}}']//ancestor::div[@class='product-item-details']//div[@class='product options active']" parameterized="true" /> -+ <element name="ProductOptionLinkActiveByProductItemName" type="text" selector="//div[@class='product-item-details']//strong[@class='product-item-name'][text()='{{var1}}']//ancestor::div[@class='product-item-details']//div[@class='product options active']//a[text() = '{{var2}}']" parameterized="true" /> - <element name="shipToInformation" type="text" selector="//div[@class='ship-to']//div[@class='shipping-information-content']" /> - <element name="shippingMethodInformation" type="text" selector="//div[@class='ship-via']//div[@class='shipping-information-content']" /> - <element name="paymentMethodTitle" type="text" selector=".payment-method-title span" /> -- <element name="productOptionsByProductItemPrice" type="text" selector="//div[@class='product-item-inner']//div[@class='subtotal']//span[@class='price'][contains(.,'{{var1}}')]//ancestor::div[@class='product-item-details']//div[@class='product options']" parameterized="true"/> -- <element name="productOptionsActiveByProductItemPrice" type="text" selector="//div[@class='subtotal']//span[@class='price'][contains(.,'{{var1}}')]//ancestor::div[@class='product-item-details']//div[@class='product options active']" parameterized="true"/> -+ <element name="productOptionsByProductItemPrice" type="text" selector="//div[@class='product-item-inner']//div[@class='subtotal']//span[@class='price'][contains(.,'{{price}}')]//ancestor::div[@class='product-item-details']//div[@class='product options']" parameterized="true"/> -+ <element name="productOptionsActiveByProductItemPrice" type="text" selector="//div[@class='subtotal']//span[@class='price'][contains(.,'{{price}}')]//ancestor::div[@class='product-item-details']//div[@class='product options active']" parameterized="true"/> -+ <element name="productItemPriceByName" type="text" selector="//div[@class='product-item-details'][contains(., '{{ProductName}}')]//span[@class='price']" parameterized="true"/> - - <element name="tax" type="text" selector="[data-th='Tax'] span" timeout="30"/> - <element name="taxPercentage" type="text" selector=".totals-tax-details .mark"/> - <element name="orderSummaryTotalIncluding" type="text" selector="//tr[@class='grand totals incl']//span[@class='price']" /> - <element name="orderSummaryTotalExcluding" type="text" selector="//tr[@class='grand totals excl']//span[@class='price']" /> -+ <element name="shippingAndBillingAddressSame" type="input" selector="#billing-address-same-as-shipping-braintree_cc_vault"/> -+ <element name="addressAction" type="button" selector="//span[text()='{{action}}']" parameterized="true"/> -+ <element name="addressBook" type="button" selector="//a[text()='Address Book']"/> -+ <element name="noQuotes" type="text" selector=".no-quotes-block"/> -+ <element name="paymentMethodByName" type="text" selector="//*[@id='checkout-payment-method-load']//*[contains(@class, 'payment-group')]//label[normalize-space(.)='{{var1}}']" parameterized="true"/> -+ <element name="billingAddressSelectShared" type="select" selector=".checkout-billing-address select[name='billing_address_id']"/> -+ <element name="discount" type="block" selector="tr.totals.discount"/> -+ <element name="discountPrice" type="text" selector=".discount .price"/> - </section> - </sections> -diff --git a/app/code/Magento/Checkout/Test/Mftf/Section/CheckoutPaymentWithDisplayBillingAddressOnPaymentPageSection.xml b/app/code/Magento/Checkout/Test/Mftf/Section/CheckoutPaymentWithDisplayBillingAddressOnPaymentPageSection.xml -new file mode 100644 -index 00000000000..42decd8d432 ---- /dev/null -+++ b/app/code/Magento/Checkout/Test/Mftf/Section/CheckoutPaymentWithDisplayBillingAddressOnPaymentPageSection.xml -@@ -0,0 +1,14 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> -+ <section name="CheckoutPaymentWithDisplayBillingAddressOnPaymentPageSection"> -+ <element name="billingAddressDetails" type="text" selector="div.billing-address-details"/> -+ </section> -+</sections> -diff --git a/app/code/Magento/Checkout/Test/Mftf/Section/CheckoutShippingGuestInfoSection.xml b/app/code/Magento/Checkout/Test/Mftf/Section/CheckoutShippingGuestInfoSection.xml -index ca13af52c1e..e9beacc3f85 100644 ---- a/app/code/Magento/Checkout/Test/Mftf/Section/CheckoutShippingGuestInfoSection.xml -+++ b/app/code/Magento/Checkout/Test/Mftf/Section/CheckoutShippingGuestInfoSection.xml -@@ -7,17 +7,26 @@ - --> - - <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="CheckoutShippingGuestInfoSection"> -- <element name="email" type="input" selector="#customer-email"/> -+ <element name="email" type="input" selector="#checkout-customer-email"/> - <element name="firstName" type="input" selector="input[name=firstname]"/> - <element name="lastName" type="input" selector="input[name=lastname]"/> -+ <element name="company" type="input" selector="input[name=company]"/> - <element name="street" type="input" selector="input[name='street[0]']"/> -+ <element name="street2" type="input" selector="input[name='street[1]']"/> - <element name="city" type="input" selector="input[name=city]"/> - <element name="region" type="select" selector="select[name=region_id]"/> -+ <element name="regionInput" type="input" selector="input[name=region]"/> - <element name="postcode" type="input" selector="input[name=postcode]"/> -+ <element name="country" type="select" selector="select[name=country_id]"/> - <element name="telephone" type="input" selector="input[name=telephone]"/> - <element name="next" type="button" selector="button.button.action.continue.primary" timeout="30"/> - <element name="firstShippingMethod" type="radio" selector=".row:nth-of-type(1) .col-method .radio"/> -+ <element name="shippingBlock" type="text" selector="#checkout-step-shipping"/> -+ -+ <!--Order Summary--> -+ <element name="itemInCart" type="button" selector="//div[@class='title']"/> -+ <element name="productName" type="text" selector="//strong[@class='product-item-name']"/> - </section> - </sections> -diff --git a/app/code/Magento/Checkout/Test/Mftf/Section/CheckoutShippingMethodsSection.xml b/app/code/Magento/Checkout/Test/Mftf/Section/CheckoutShippingMethodsSection.xml -index 2d07cdd9bd0..2f49e4f422a 100644 ---- a/app/code/Magento/Checkout/Test/Mftf/Section/CheckoutShippingMethodsSection.xml -+++ b/app/code/Magento/Checkout/Test/Mftf/Section/CheckoutShippingMethodsSection.xml -@@ -7,14 +7,18 @@ - --> - - <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="CheckoutShippingMethodsSection"> - <element name="next" type="button" selector="button.button.action.continue.primary" timeout="30"/> - <element name="firstShippingMethod" type="radio" selector="//*[@id='checkout-shipping-method-load']//input[@class='radio']"/> - <element name="shippingMethodRow" type="text" selector=".form.methods-shipping table tbody tr"/> - <element name="checkShippingMethodByName" type="radio" selector="//div[@id='checkout-shipping-method-load']//td[contains(., '{{var1}}')]/..//input" parameterized="true"/> -+ <element name="shippingMethodFlatRate" type="radio" selector="#checkout-shipping-method-load input[value='flatrate_flatrate']"/> -+ <element name="shippingMethodFreeShipping" type="radio" selector="#checkout-shipping-method-load input[value='freeshipping_freeshipping']" timeout="60"/> - <element name="shippingMethodRowByName" type="text" selector="//div[@id='checkout-shipping-method-load']//td[contains(., '{{var1}}')]/.." parameterized="true"/> -- <element name="shipHereButton" type="button" selector="//button[contains(@class, 'action-select-shipping-item')]/parent::div/following-sibling::div/button[contains(@class, 'action-select-shipping-item')]"/> -+ <element name="shipHereButton" type="button" selector="//div/following-sibling::div/button[contains(@class, 'action-select-shipping-item')]"/> - <element name="shippingMethodLoader" type="button" selector="//div[contains(@class, 'checkout-shipping-method')]/following-sibling::div[contains(@class, 'loading-mask')]"/> -+ <element name="freeShippingShippingMethod" type="input" selector="#s_method_freeshipping_freeshipping" timeout="30"/> -+ <element name="noQuotesMsg" type="text" selector="#checkout-step-shipping_method div"/> - </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 136658cc591..f90ec0aad46 100644 ---- a/app/code/Magento/Checkout/Test/Mftf/Section/CheckoutShippingSection.xml -+++ b/app/code/Magento/Checkout/Test/Mftf/Section/CheckoutShippingSection.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="CheckoutShippingSection"> - <element name="isShippingStep" type="text" selector="//*[@class='opc-progress-bar']/li[contains(@class, '_active') and span[contains(.,'Shipping')]]"/> - <element name="shippingTab" type="text" selector="//li[contains(@class,'opc-progress-bar-item')]//*[text()='Shipping']" timeout="30"/> -@@ -15,7 +15,8 @@ - <element name="editAddressButton" type="button" selector=".action-edit-address" timeout="30"/> - <element name="addressDropdown" type="select" selector="[name=billing_address_id]"/> - <element name="newAddressButton" type="button" selector=".action-show-popup" timeout="30"/> -- <element name="email" type="input" selector="#customer-email"/> -+ <element name="email" type="input" selector="input[id*=customer-email]"/> -+ <element name="password" type="input" selector="#customer-password"/> - <element name="firstName" type="input" selector="input[name=firstname]"/> - <element name="lastName" type="input" selector="input[name=lastname]"/> - <element name="company" type="input" selector="input[name=company]"/> -@@ -27,10 +28,18 @@ - <element name="country" type="select" selector="select[name=country_id]"/> - <element name="telephone" type="input" selector="input[name=telephone]"/> - <element name="saveAddress" type="button" selector=".action-save-address"/> -+ <element name="cancelChangeAddress" type="button" selector=".action-hide-popup"/> - <element name="updateAddress" type="button" selector=".action-update"/> - <element name="next" type="button" selector="button.button.action.continue.primary" timeout="30"/> - <element name="firstShippingMethod" type="radio" selector="//*[@id='checkout-shipping-method-load']//input[@class='radio']"/> - <element name="defaultShipping" type="button" selector=".billing-address-details"/> -+ <element name="state" type="button" selector="//*[text()='Alabama']"/> - <element name="stateInput" type="input" selector="input[name=region]"/> -+ <element name="regionOptions" type="select" selector="select[name=region_id] option"/> -+ <element name="editActiveAddress" type="button" selector="//div[@class='shipping-address-item selected-item']//span[text()='Edit']" timeout="30"/> -+ <element name="loginButton" type="button" selector=".action.login" timeout="30"/> -+ <element name="shipHereButton" type="button" selector="//div[text()='{{street}}']/button[@class='action action-select-shipping-item']" parameterized="true" timeout="30"/> -+ <element name="textFieldAttrRequireMessage" type="text" selector="//input[@name='custom_attributes[{{attribute}}]']/ancestor::div[contains(@class, 'control')]/div/span" parameterized="true" timeout="30"/> -+ <element name="textFieldAttribute" type="input" selector="[name*='custom_attributes[{{attribute}}]']" parameterized="true" timeout="30"/> - </section> - </sections> -diff --git a/app/code/Magento/Checkout/Test/Mftf/Section/CheckoutSuccessMainSection.xml b/app/code/Magento/Checkout/Test/Mftf/Section/CheckoutSuccessMainSection.xml -index 8a55015f9d2..81e63baddf5 100644 ---- a/app/code/Magento/Checkout/Test/Mftf/Section/CheckoutSuccessMainSection.xml -+++ b/app/code/Magento/Checkout/Test/Mftf/Section/CheckoutSuccessMainSection.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="CheckoutSuccessMainSection"> - <element name="successTitle" type="text" selector=".page-title"/> - <element name="success" type="text" selector="div.checkout-success"/> -@@ -16,6 +16,7 @@ - <element name="orderLink" type="text" selector="a[href*=order_id].order-number" timeout="30"/> - <element name="orderNumberText" type="text" selector=".checkout-success > p:nth-child(1)"/> - <element name="continueShoppingButton" type="button" selector=".action.primary.continue" timeout="30"/> -+ <element name="createAnAccount" type="button" selector="[data-bind*="i18n: 'Create an Account'"]" timeout="30"/> - <element name="printLink" type="button" selector=".print" timeout="30"/> - </section> - </sections> -diff --git a/app/code/Magento/Checkout/Test/Mftf/Section/CheckoutSuccessRegisterSection.xml b/app/code/Magento/Checkout/Test/Mftf/Section/CheckoutSuccessRegisterSection.xml -index 271ccec4505..baee6cc7177 100644 ---- a/app/code/Magento/Checkout/Test/Mftf/Section/CheckoutSuccessRegisterSection.xml -+++ b/app/code/Magento/Checkout/Test/Mftf/Section/CheckoutSuccessRegisterSection.xml -@@ -7,10 +7,11 @@ - --> - - <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="CheckoutSuccessRegisterSection"> - <element name="registerMessage" type="text" selector="#registration p:nth-child(1)"/> - <element name="customerEmail" type="text" selector="#registration p:nth-child(2)"/> -- <element name="createAccountButton" type="button" selector="#registration form input[type='submit']" timeout="30"/> -+ <element name="createAccountButton" type="button" selector="[data-bind*="i18n: 'Create an Account'"]" timeout="30"/> -+ <element name="orderNumber" type="text" selector="//p[text()='Your order # is: ']//span"/> - </section> - </sections> -diff --git a/app/code/Magento/Checkout/Test/Mftf/Section/IdentityOfDefaultBillingAndShippingAddressSection.xml b/app/code/Magento/Checkout/Test/Mftf/Section/IdentityOfDefaultBillingAndShippingAddressSection.xml -new file mode 100644 -index 00000000000..2039128ac2d ---- /dev/null -+++ b/app/code/Magento/Checkout/Test/Mftf/Section/IdentityOfDefaultBillingAndShippingAddressSection.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="ShipmentFormSection"> -+ <element name="shippingAddress" type="textarea" selector="//*[@class='box box-billing-address']//address"/> -+ <element name="billingAddress" type="textarea" selector="//*[@class='box box-shipping-address']//address"/> -+ </section> -+</sections> -diff --git a/app/code/Magento/Checkout/Test/Mftf/Section/StoreFrontRemoveItemModalSection.xml b/app/code/Magento/Checkout/Test/Mftf/Section/StoreFrontRemoveItemModalSection.xml -index 4e2a08e94bd..e8001af6f03 100644 ---- a/app/code/Magento/Checkout/Test/Mftf/Section/StoreFrontRemoveItemModalSection.xml -+++ b/app/code/Magento/Checkout/Test/Mftf/Section/StoreFrontRemoveItemModalSection.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="StoreFrontRemoveItemModalSection"> - <element name="message" type="text" selector="aside.confirm div.modal-content"/> - <element name="ok" type="button" selector="aside.confirm .modal-footer .action-primary"/> -diff --git a/app/code/Magento/Checkout/Test/Mftf/Section/StorefrontCategoryProductSection.xml b/app/code/Magento/Checkout/Test/Mftf/Section/StorefrontCategoryProductSection.xml -index 0edbb21bc6f..0427938a02d 100644 ---- a/app/code/Magento/Checkout/Test/Mftf/Section/StorefrontCategoryProductSection.xml -+++ b/app/code/Magento/Checkout/Test/Mftf/Section/StorefrontCategoryProductSection.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="StorefrontCategoryProductSection"> - <element name="ProductAddToCartByNumber" type="button" selector="//main//li[{{var1}}]//button[contains(@class, 'tocart')]" parameterized="true"/> - <element name="ProductAddToCartByName" type="button" selector="//main//li[.//a[contains(text(), '{{var1}}')]]//button[contains(@class, 'tocart')]" parameterized="true"/> -diff --git a/app/code/Magento/Checkout/Test/Mftf/Section/StorefrontCheckoutAddressPopupSection.xml b/app/code/Magento/Checkout/Test/Mftf/Section/StorefrontCheckoutAddressPopupSection.xml -new file mode 100644 -index 00000000000..6a27915768d ---- /dev/null -+++ b/app/code/Magento/Checkout/Test/Mftf/Section/StorefrontCheckoutAddressPopupSection.xml -@@ -0,0 +1,15 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> -+ <section name="StorefrontCheckoutAddressPopupSection"> -+ <element name="newAddressModalPopup" type="block" selector=".modal-popup.modal-slide._inner-scroll"/> -+ <element name="closeAddressModalPopup" type="button" selector=".action-hide-popup"/> -+ </section> -+</sections> -diff --git a/app/code/Magento/Checkout/Test/Mftf/Section/StorefrontCheckoutCheckoutCustomerLoginSection.xml b/app/code/Magento/Checkout/Test/Mftf/Section/StorefrontCheckoutCheckoutCustomerLoginSection.xml -new file mode 100644 -index 00000000000..9772fa1993a ---- /dev/null -+++ b/app/code/Magento/Checkout/Test/Mftf/Section/StorefrontCheckoutCheckoutCustomerLoginSection.xml -@@ -0,0 +1,22 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> -+ <section name="StorefrontCheckoutCheckoutCustomerLoginSection"> -+ <element name="email" type="input" selector="form[data-role='email-with-possible-login'] input[name='username']" /> -+ <element name="emailNoteMessage" type="text" selector="//form[@data-role='email-with-possible-login']//div[input[@name='username']]//*[contains(@class, 'note')]" /> -+ <element name="emailErrorMessage" type="text" selector="//form[@data-role='email-with-possible-login']//div[input[@name='username']]//*[@id='checkout-customer-email-error']" /> -+ <element name="emailTooltipButton" type="button" selector="//form[@data-role='email-with-possible-login']//div[input[@name='username']]//*[contains(@class, 'action-help')]" /> -+ <element name="emailTooltipContent" type="text" selector="//form[@data-role='email-with-possible-login']//div[input[@name='username']]//*[contains(@class, 'field-tooltip-content')]" /> -+ <element name="password" type="input" selector="form[data-role='email-with-possible-login'] input[name='password']" /> -+ <element name="passwordNoteMessage" type="text" selector="//form[@data-role='email-with-possible-login']//div[input[@name='password']]//*[contains(@class, 'note')]" /> -+ <element name="submit" type="button" selector="form[data-role='email-with-possible-login'] button[type='submit']" /> -+ <element name="forgotPassword" type="button" selector="form[data-role='email-with-possible-login'] a.remind" /> -+ </section> -+</sections> -diff --git a/app/code/Magento/Checkout/Test/Mftf/Section/StorefrontCheckoutPaymentMethodSection.xml b/app/code/Magento/Checkout/Test/Mftf/Section/StorefrontCheckoutPaymentMethodSection.xml -new file mode 100644 -index 00000000000..40214b9c11f ---- /dev/null -+++ b/app/code/Magento/Checkout/Test/Mftf/Section/StorefrontCheckoutPaymentMethodSection.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="StorefrontCheckoutPaymentMethodSection"> -+ <element name="billingAddress" type="text" selector=".checkout-billing-address"/> -+ <element name="checkPaymentMethodByName" type="radio" selector="//div[@id='checkout-payment-method-load']//div[@class='payment-method']//label//span[contains(., '{{methodName}}')]/../..//input" parameterized="true"/> -+ <element name="billingAddressSameAsShipping" type="checkbox" selector=".payment-method._active [name='billing-address-same-as-shipping']"/> -+ <element name="billingAddressSameAsShippingShared" type="checkbox" selector="#billing-address-same-as-shipping-shared"/> -+ <element name="paymentOnAccount" type="radio" selector="#companycredit"/> -+ <element name="paymentOnAccountLabel" type="text" selector="//span[text()='Payment on Account']"/> -+ <element name="purchaseOrderNumber" type="input" selector="#po_number"/> -+ </section> -+</sections> -diff --git a/app/code/Magento/Checkout/Test/Mftf/Section/StorefrontMessagesSection.xml b/app/code/Magento/Checkout/Test/Mftf/Section/StorefrontMessagesSection.xml -index 4341d99c3fb..8bd6a59f563 100644 ---- a/app/code/Magento/Checkout/Test/Mftf/Section/StorefrontMessagesSection.xml -+++ b/app/code/Magento/Checkout/Test/Mftf/Section/StorefrontMessagesSection.xml -@@ -7,11 +7,11 @@ - --> - - <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="StorefrontMessagesSection"> - <!-- @TODO: Use general message selector after MQE-694 is fixed --> - <element name="messageProductAddedToCart" type="text" -- selector="//main//div[contains(@class, 'messages')]//div[contains(@class, 'message')]/div[contains(text(), 'You added {{var1}} to your shopping cart.')]" -+ selector="//main//div[contains(@class, 'messages')]//div[contains(@class, 'message')]/div[contains(text(), 'You added {{var1}} to your') and a[contains(., 'shopping cart')]]" - parameterized="true" - /> - </section> -diff --git a/app/code/Magento/Checkout/Test/Mftf/Section/StorefrontMiniCartSection.xml b/app/code/Magento/Checkout/Test/Mftf/Section/StorefrontMiniCartSection.xml -index 2c556f25f20..621cd1d3a7b 100644 ---- a/app/code/Magento/Checkout/Test/Mftf/Section/StorefrontMiniCartSection.xml -+++ b/app/code/Magento/Checkout/Test/Mftf/Section/StorefrontMiniCartSection.xml -@@ -7,23 +7,36 @@ - --> - - <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="StorefrontMinicartSection"> - <element name="productCount" type="text" selector="//header//div[contains(@class, 'minicart-wrapper')]//a[contains(@class, 'showcart')]//span[@class='counter-number']"/> - <element name="productLinkByName" type="button" selector="//header//ol[@id='mini-cart']//div[@class='product-item-details']//a[contains(text(), '{{var1}}')]" parameterized="true"/> - <element name="productPriceByName" type="text" selector="//header//ol[@id='mini-cart']//div[@class='product-item-details'][.//a[contains(text(), '{{var1}}')]]//span[@class='price']" parameterized="true"/> - <element name="productImageByName" type="text" selector="//header//ol[@id='mini-cart']//span[@class='product-image-container']//img[@alt='{{var1}}']" parameterized="true"/> -+ <element name="productName" type="text" selector=".product-item-name"/> - <element name="productOptionsDetailsByName" type="button" selector="//header//ol[@id='mini-cart']//div[@class='product-item-details'][.//a[contains(text(), '{{var1}}')]]//span[.='See Details']" parameterized="true"/> - <element name="productOptionByNameAndAttribute" type="text" selector="//header//ol[@id='mini-cart']//div[@class='product-item-details'][.//a[contains(text(), '{{var1}}')]]//dt[@class='label' and .='{{var2}}']/following-sibling::dd[@class='values']//span" parameterized="true"/> -- <element name="showCart" type="button" selector="a.showcart"/> -+ <element name="showCart" type="button" selector="a.showcart" timeout="60"/> - <element name="quantity" type="button" selector="span.counter-number"/> - <element name="miniCartOpened" type="button" selector="a.showcart.active"/> - <element name="goToCheckout" type="button" selector="#top-cart-btn-checkout" timeout="30"/> - <element name="viewAndEditCart" type="button" selector=".action.viewcart" timeout="30"/> - <element name="miniCartItemsText" type="text" selector=".minicart-items"/> - <element name="deleteMiniCartItem" type="button" selector=".action.delete" timeout="30"/> -+ <element name="deleteMiniCartItemByName" type="button" selector="//ol[@id='mini-cart']//div[contains(., '{{var}}')]//a[contains(@class, 'delete')]" parameterized="true"/> - <element name="miniCartSubtotalField" type="text" selector=".block-minicart .amount span.price"/> -+ <element name="itemQuantityBySku" type="input" selector="#minicart-content-wrapper input[data-cart-item-id='{{productSku}}']" parameterized="true"/> -+ <element name="itemQuantityUpdateBySku" type="button" selector="//div[@id='minicart-content-wrapper']//input[@data-cart-item-id='{{productSku}}']/../button[contains(@class, 'update-cart-item')]" parameterized="true"/> - <element name="itemQuantity" type="input" selector="//a[text()='{{productName}}']/../..//input[contains(@class,'cart-item-qty')]" parameterized="true"/> - <element name="itemQuantityUpdate" type="button" selector="//a[text()='{{productName}}']/../..//span[text()='Update']" parameterized="true"/> -+ <element name="itemDiscount" type="text" selector="//tr[@class='totals']//td[@class='amount']/span"/> -+ <element name="subtotal" type="text" selector="//tr[@class='totals sub']//td[@class='amount']/span"/> -+ <element name="emptyCart" type="text" selector=".counter.qty.empty"/> -+ <element name="minicartContent" type="block" selector="#minicart-content-wrapper"/> -+ <element name="messageEmptyCart" type="text" selector="//*[@id='minicart-content-wrapper']//*[contains(@class,'subtitle empty')]"/> -+ <element name="visibleItemsCountText" type="text" selector="//div[@class='items-total']"/> -+ <element name="productQuantity" type="input" selector="//*[@id='mini-cart']//a[contains(text(),'{{productName}}')]/../..//div[@class='details-qty qty']//input[@data-item-qty='{{qty}}']" parameterized="true"/> -+ <element name="productImage" type="text" selector="//ol[@id='mini-cart']//img[@class='product-image-photo']"/> -+ <element name="productSubTotal" type="text" selector="//div[@class='subtotal']//span/span[@class='price']"/> - </section> - </sections> -diff --git a/app/code/Magento/Checkout/Test/Mftf/Section/StorefrontProductCartGiftOptionSection.xml b/app/code/Magento/Checkout/Test/Mftf/Section/StorefrontProductCartGiftOptionSection.xml -new file mode 100644 -index 00000000000..c1bee4af4c2 ---- /dev/null -+++ b/app/code/Magento/Checkout/Test/Mftf/Section/StorefrontProductCartGiftOptionSection.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="StorefrontProductCartGiftOptionSection"> -+ <element name="giftOptions" type="button" selector=".action.action-gift"/> -+ <element name="fieldTo" type="input" selector=".gift-options-content .field-to input"/> -+ <element name="fieldFrom" type="input" selector=".gift-options-content .field-from input"/> -+ <element name="message" type="textarea" selector="#gift-message-whole-message"/> -+ <element name="update" type="button" selector=".action-update"/> -+ <element name="cancel" type="button" selector=".action-cancel"/> -+ </section> -+</sections> -diff --git a/app/code/Magento/Checkout/Test/Mftf/Section/StorefrontProductCompareMainSection.xml b/app/code/Magento/Checkout/Test/Mftf/Section/StorefrontProductCompareMainSection.xml -index 823260be42f..200a742e58f 100644 ---- a/app/code/Magento/Checkout/Test/Mftf/Section/StorefrontProductCompareMainSection.xml -+++ b/app/code/Magento/Checkout/Test/Mftf/Section/StorefrontProductCompareMainSection.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="StorefrontProductCompareMainSection"> - <element name="ProductAddToCartByName" type="button" selector="//*[@id='product-comparison']//td[.//strong[@class='product-item-name']/a[contains(text(), '{{var1}}')]]//button[contains(@class, 'tocart')]" parameterized="true"/> - </section> -diff --git a/app/code/Magento/Checkout/Test/Mftf/Section/StorefrontProductInfoMainSection.xml b/app/code/Magento/Checkout/Test/Mftf/Section/StorefrontProductInfoMainSection.xml -index 1ff5d2c8744..95929401620 100644 ---- a/app/code/Magento/Checkout/Test/Mftf/Section/StorefrontProductInfoMainSection.xml -+++ b/app/code/Magento/Checkout/Test/Mftf/Section/StorefrontProductInfoMainSection.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="StorefrontProductInfoMainSection"> - <element name="AddToCart" type="button" selector="#product-addtocart-button"/> - </section> -diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/AddressStateFieldForUKCustomerRemainOptionAfterRefreshTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/AddressStateFieldForUKCustomerRemainOptionAfterRefreshTest.xml -index 66146137d01..52a69307550 100644 ---- a/app/code/Magento/Checkout/Test/Mftf/Test/AddressStateFieldForUKCustomerRemainOptionAfterRefreshTest.xml -+++ b/app/code/Magento/Checkout/Test/Mftf/Test/AddressStateFieldForUKCustomerRemainOptionAfterRefreshTest.xml -@@ -7,17 +7,19 @@ - --> - - <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="AddressStateFieldForUKCustomerRemainOptionAfterRefreshTest"> - <annotations> - <features value="Checkout"/> -- <title value="Guest Checkout"/> -+ <stories value="Guest checkout"/> -+ <title value="Address State Field For UK Customers Remain Option even After Browser Refresh"/> - <description value="Address State Field For UK Customers Remain Option even After Browser Refresh"/> - <severity value="MAJOR"/> - <testCaseId value="MAGETWO-93329"/> - <group value="checkout"/> -- <!-- Skipped because of MAGETWO-93726 --> -- <group value="skip"/> -+ <skip> -+ <issueId value="MAGETWO-93726"/> -+ </skip> - </annotations> - <before> - <createData entity="_defaultCategory" stepKey="createCategory"/> -diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/AddressStateFieldShouldNotAcceptJustIntegerValuesTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/AddressStateFieldShouldNotAcceptJustIntegerValuesTest.xml -index add1a1b1cf9..f3e31d23f71 100644 ---- a/app/code/Magento/Checkout/Test/Mftf/Test/AddressStateFieldShouldNotAcceptJustIntegerValuesTest.xml -+++ b/app/code/Magento/Checkout/Test/Mftf/Test/AddressStateFieldShouldNotAcceptJustIntegerValuesTest.xml -@@ -7,15 +7,15 @@ - --> - - <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="AddressStateFieldShouldNotAcceptJustIntegerValuesTest"> - <annotations> - <features value="Checkout"/> - <stories value="MAGETWO-91465"/> -- <title value="Guest Checkout"/> -+ <title value="Guest Checkout - address State field should not allow just integer values"/> - <description value="Address State field should not allow just integer values"/> - <severity value="MAJOR"/> -- <testCaseId value="MAGETWO-93203"/> -+ <testCaseId value="MC-6223"/> - <group value="checkout"/> - </annotations> - <before> -diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/AdminCheckConfigsChangesIsNotAffectedStartedCheckoutProcessTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/AdminCheckConfigsChangesIsNotAffectedStartedCheckoutProcessTest.xml -new file mode 100644 -index 00000000000..31a9d011a91 ---- /dev/null -+++ b/app/code/Magento/Checkout/Test/Mftf/Test/AdminCheckConfigsChangesIsNotAffectedStartedCheckoutProcessTest.xml -@@ -0,0 +1,102 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> -+ <test name="AdminCheckConfigsChangesAreNotAffectedStartedCheckoutProcessTest"> -+ <annotations> -+ <features value="Checkout"/> -+ <stories value="Changes in configs are not affecting checkout process"/> -+ <title value="Admin check configs changes are not affected started checkout process test"/> -+ <description value="Changes in admin for shipping rates are not affecting checkout process that has been started"/> -+ <severity value="CRITICAL"/> -+ <testCaseId value="MC-12599"/> -+ <group value="checkout"/> -+ </annotations> -+ <before> -+ <!-- Create simple product --> -+ <createData entity="SimpleProduct2" stepKey="createProduct"/> -+ -+ <!-- Enable free shipping method --> -+ <magentoCLI command="config:set {{EnableFreeShippingConfigData.path}} {{EnableFreeShippingConfigData.value}}" stepKey="enableFreeShipping"/> -+ -+ <!-- Disable flat rate method --> -+ <magentoCLI command="config:set {{DisableFlatRateConfigData.path}} {{DisableFlatRateConfigData.value}}" stepKey="disableFlatRate"/> -+ </before> -+ <after> -+ <!-- Roll back configuration --> -+ <magentoCLI command="config:set {{EnableFlatRateConfigData.path}} {{EnableFlatRateConfigData.value}}" stepKey="enableFlatRate"/> -+ <magentoCLI command="config:set {{DisableFreeShippingConfigData.path}} {{DisableFreeShippingConfigData.value}}" stepKey="disableFreeShipping"/> -+ -+ <!-- Delete simple product --> -+ <deleteData createDataKey="createProduct" stepKey="deleteProduct"/> -+ -+ <!-- Log out --> -+ <actionGroup ref="logout" stepKey="logout"/> -+ </after> -+ -+ <!-- Add product to cart --> -+ <actionGroup ref="OpenStoreFrontProductPageActionGroup" stepKey="openProductPage"> -+ <argument name="productUrlKey" value="$$createProduct.custom_attributes[url_key]$$"/> -+ </actionGroup> -+ <actionGroup ref="StorefrontAddProductToCartActionGroup" stepKey="addProductToCart"> -+ <argument name="product" value="$$createProduct$$"/> -+ <argument name="productCount" value="1"/> -+ </actionGroup> -+ -+ <!-- Proceed to Checkout from mini shopping cart --> -+ <actionGroup ref="GoToCheckoutFromMinicartActionGroup" stepKey="goToCheckout"/> -+ -+ <!-- Fill all required fields --> -+ <actionGroup ref="GuestCheckoutFillNewShippingAddressActionGroup" stepKey="fillNewShippingAddress"> -+ <argument name="customer" value="Simple_Customer_Without_Address" /> -+ <argument name="address" value="US_Address_TX"/> -+ </actionGroup> -+ -+ <!-- Assert Free Shipping checkbox --> -+ <seeCheckboxIsChecked selector="{{CheckoutShippingMethodsSection.shippingMethodFreeShipping}}" stepKey="freeShippingMethodCheckboxIsChecked"/> -+ -+ <!-- Click Next button --> -+ <click selector="{{CheckoutShippingGuestInfoSection.next}}" stepKey="clickNext"/> -+ <waitForPageLoad stepKey="waitForShipmentPageLoad"/> -+ -+ <!-- Payment step is opened --> -+ <waitForElement selector="{{CheckoutPaymentSection.paymentSectionTitle}}" stepKey="waitForPaymentSectionLoaded"/> -+ -+ <!-- Order Summary block contains information about shipping --> -+ <actionGroup ref="CheckShippingMethodInCheckoutActionGroup" stepKey="guestCheckoutCheckShippingMethod"> -+ <argument name="shippingMethod" value="freeTitleDefault.value"/> -+ </actionGroup> -+ -+ <!-- Open new browser's window and login as Admin --> -+ <openNewTab stepKey="openNewTab"/> -+ <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> -+ -+ <!-- Go to Store > Configuration > Sales > Shipping Methods --> -+ <actionGroup ref="AdminOpenShippingMethodsConfigPageActionGroup" stepKey="openShippingMethodConfigPage"/> -+ -+ <!-- Enable "Flat Rate" --> -+ <actionGroup ref="AdminChangeFlatRateShippingMethodStatusActionGroup" stepKey="enableFlatRateShippingStatus"/> -+ -+ <!-- Flush cache --> -+ <magentoCLI command="cache:flush" stepKey="cacheFlush"/> -+ -+ <!-- Back to the Checkout and refresh the page --> -+ <switchToPreviousTab stepKey="switchToPreviousTab"/> -+ <reloadPage stepKey="refreshPage"/> -+ <waitForPageLoad stepKey="waitPageReload"/> -+ -+ <!-- Payment step is opened after refreshing --> -+ <waitForElement selector="{{CheckoutPaymentSection.paymentSectionTitle}}" stepKey="waitForPaymentSection"/> -+ -+ <!-- Order Summary block contains information about free shipping --> -+ <actionGroup ref="CheckShippingMethodInCheckoutActionGroup" stepKey="guestCheckoutCheckFreeShippingMethod"> -+ <argument name="shippingMethod" value="freeTitleDefault.value"/> -+ </actionGroup> -+ </test> -+</tests> -diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/CheckCheckoutSuccessPageTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/CheckCheckoutSuccessPageTest.xml -index d7182222835..9714b76a056 100644 ---- a/app/code/Magento/Checkout/Test/Mftf/Test/CheckCheckoutSuccessPageTest.xml -+++ b/app/code/Magento/Checkout/Test/Mftf/Test/CheckCheckoutSuccessPageTest.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="CheckCheckoutSuccessPageAsRegisterCustomer"> - <annotations> - <features value="Checkout"/> -@@ -15,7 +15,7 @@ - <title value="Customer Checkout"/> - <description value="To be sure that other elements of Success page are shown for placed order as registered Customer."/> - <severity value="CRITICAL"/> -- <testCaseId value="MAGETWO-60345"/> -+ <testCaseId value="MC-16488"/> - <group value="checkout"/> - </annotations> - -@@ -55,8 +55,8 @@ - <waitForLoadingMaskToDisappear stepKey="waitForLoadingMask2"/> - <waitForElement selector="{{CheckoutShippingMethodsSection.next}}" time="30" stepKey="waitForNextButton"/> - <click selector="{{CheckoutShippingMethodsSection.next}}" stepKey="clickNext"/> -- <waitForElement selector="{{CheckoutPaymentSection.paymentSectionTitle}}" time="30" stepKey="waitForPaymentSectionLoadedTest3"/> -- -+ <!-- Checkout select Check/Money Order payment --> -+ <actionGroup ref="CheckoutSelectCheckMoneyOrderPaymentActionGroup" stepKey="selectCheckMoneyPayment"/> - <!--Click Place Order button--> - <click selector="{{CheckoutPaymentSection.placeOrder}}" stepKey="clickPlaceOrder"/> - <see selector="{{CheckoutSuccessMainSection.successTitle}}" userInput="Thank you for your purchase!" stepKey="seeSuccessTitle"/> -@@ -81,7 +81,8 @@ - <waitForLoadingMaskToDisappear stepKey="waitForLoadingMask3"/> - <waitForElement selector="{{CheckoutShippingMethodsSection.next}}" time="30" stepKey="waitForNextButton2"/> - <click selector="{{CheckoutShippingMethodsSection.next}}" stepKey="clickNext2"/> -- <waitForElement selector="{{CheckoutPaymentSection.paymentSectionTitle}}" time="30" stepKey="waitForPaymentSectionLoadedTest4"/> -+ <!-- Checkout select Check/Money Order payment --> -+ <actionGroup ref="CheckoutSelectCheckMoneyOrderPaymentActionGroup" stepKey="selectCheckMoneyPayment2"/> - - <!--Click Place Order button--> - <click selector="{{CheckoutPaymentSection.placeOrder}}" stepKey="clickPlaceOrder2"/> -@@ -105,7 +106,9 @@ - <waitForLoadingMaskToDisappear stepKey="waitForLoadingMask4"/> - <waitForElement selector="{{CheckoutShippingMethodsSection.next}}" time="30" stepKey="waitForNextButton3"/> - <click selector="{{CheckoutShippingMethodsSection.next}}" stepKey="clickNext3"/> -- <waitForElement selector="{{CheckoutPaymentSection.paymentSectionTitle}}" time="30" stepKey="waitForPaymentSectionLoadedTest5"/> -+ -+ <!-- Checkout select Check/Money Order payment --> -+ <actionGroup ref="CheckoutSelectCheckMoneyOrderPaymentActionGroup" stepKey="selectCheckMoneyPayment3"/> - - <!--Click Place Order button--> - <click selector="{{CheckoutPaymentSection.placeOrder}}" stepKey="clickPlaceOrder3"/> -@@ -132,10 +135,10 @@ - <annotations> - <features value="Checkout"/> - <stories value="Success page elements are presented for placed order as Guest"/> -- <title value="Customer Checkout"/> -- <description value="To be sure that other elements of Success page are presented for placed order as Guest."/> -+ <title value="Guest Checkout - elements of success page are presented for placed order as guest"/> -+ <description value="To be sure that other elements of Success page are presented for placed order as Guest"/> - <severity value="CRITICAL"/> -- <testCaseId value="MAGETWO-60346"/> -+ <testCaseId value="MC-16490"/> - <group value="checkout"/> - </annotations> - -@@ -165,6 +168,9 @@ - <argument name="customerAddressVar" value="CustomerAddressSimple" /> - </actionGroup> - -+ <!-- Checkout select Check/Money Order payment --> -+ <actionGroup ref="CheckoutSelectCheckMoneyOrderPaymentActionGroup" stepKey="selectCheckMoneyPayment"/> -+ - <!--Click Place Order button--> - <click selector="{{CheckoutPaymentSection.placeOrder}}" stepKey="clickPlaceOrder"/> - <see selector="{{CheckoutSuccessMainSection.successTitle}}" userInput="Thank you for your purchase!" stepKey="waitForLoadSuccessPage"/> -@@ -198,6 +204,9 @@ - <argument name="customerAddressVar" value="CustomerAddressSimple" /> - </actionGroup> - -+ <!-- Checkout select Check/Money Order payment --> -+ <actionGroup ref="CheckoutSelectCheckMoneyOrderPaymentActionGroup" stepKey="selectCheckMoneyPayment2"/> -+ - <!--Click Place Order button--> - <click selector="{{CheckoutPaymentSection.placeOrder}}" stepKey="clickPlaceOrder2"/> - <see selector="{{CheckoutSuccessMainSection.successTitle}}" userInput="Thank you for your purchase!" stepKey="waitForLoadSuccessPage2"/> -diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/CheckNotVisibleProductInMinicartTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/CheckNotVisibleProductInMinicartTest.xml -new file mode 100644 -index 00000000000..4b4ca1935fd ---- /dev/null -+++ b/app/code/Magento/Checkout/Test/Mftf/Test/CheckNotVisibleProductInMinicartTest.xml -@@ -0,0 +1,70 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> -+ <test name="CheckNotVisibleProductInMinicartTest"> -+ <annotations> -+ <features value="Checkout"/> -+ <stories value="MAGETWO-96422: Hidden Products are absent in Storefront Mini-Cart" /> -+ <title value="Not visible individually product in mini-shopping cart."/> -+ <description value="To be sure that product in mini-shopping cart remains visible after admin makes it not visible individually"/> -+ <severity value="MAJOR"/> -+ <group value="checkout"/> -+ </annotations> -+ -+ <!--Create simple product1 and simple product2--> -+ <createData entity="SimpleTwo" stepKey="createSimpleProduct1"/> -+ <createData entity="SimpleTwo" stepKey="createSimpleProduct2"/> -+ -+ <!--Go to simple product1 page--> -+ <amOnPage url="$$createSimpleProduct1.custom_attributes[url_key]$$.html" stepKey="navigateToSimpleProductPage1"/> -+ <waitForPageLoad stepKey="waitForCatalogPageLoad"/> -+ -+ <!--Add simple product1 to Shopping Cart--> -+ <actionGroup ref="addToCartFromStorefrontProductPage" stepKey="addToCartFromStorefrontProductPage1"> -+ <argument name="productName" value="$$createSimpleProduct1.name$$"/> -+ </actionGroup> -+ -+ <!--Check simple product1 in minicart--> -+ <comment userInput="Check simple product 1 in minicart" stepKey="commentCheckSimpleProduct1InMinicart" after="addToCartFromStorefrontProductPage1"/> -+ <actionGroup ref="assertOneProductNameInMiniCart" stepKey="assertProduct1NameInMiniCart"> -+ <argument name="productName" value="$$createSimpleProduct1.name$$"/> -+ </actionGroup> -+ -+ <!--Make simple product1 not visible individually--> -+ <updateData entity="SetProductVisibilityHidden" createDataKey="createSimpleProduct1" stepKey="updateSimpleProduct1"> -+ <requiredEntity createDataKey="createSimpleProduct1"/> -+ </updateData> -+ -+ <!--Go to simple product2 page--> -+ <amOnPage url="$$createSimpleProduct2.custom_attributes[url_key]$$.html" stepKey="navigateToSimpleProductPage2"/> -+ <waitForPageLoad stepKey="waitForCatalogPageLoad2"/> -+ -+ <!--Add simple product2 to Shopping Cart for updating cart items--> -+ <actionGroup ref="addToCartFromStorefrontProductPage" stepKey="addToCartFromStorefrontProductPage2"> -+ <argument name="productName" value="$$createSimpleProduct2.name$$"/> -+ </actionGroup> -+ -+ <!--Check simple product1 in minicart--> -+ <comment userInput="Check hidden simple product 1 in minicart" stepKey="commentCheckHiddenSimpleProduct1InMinicart" after="addToCartFromStorefrontProductPage2"/> -+ <actionGroup ref="assertOneProductNameInMiniCart" stepKey="assertHiddenProduct1NameInMiniCart"> -+ <argument name="productName" value="$$createSimpleProduct1.name$$"/> -+ </actionGroup> -+ -+ <!--Check simple product2 in minicart--> -+ <comment userInput="Check hidden simple product 2 in minicart" stepKey="commentCheckSimpleProduct2InMinicart" after="addToCartFromStorefrontProductPage2"/> -+ <actionGroup ref="assertOneProductNameInMiniCart" stepKey="assertProduct2NameInMiniCart"> -+ <argument name="productName" value="$$createSimpleProduct2.name$$"/> -+ </actionGroup> -+ -+ <!--Delete simple product1 and simple product2--> -+ <deleteData createDataKey="createSimpleProduct1" stepKey="deleteProduct1"/> -+ <deleteData createDataKey="createSimpleProduct2" stepKey="deleteProduct2"/> -+ </test> -+</tests> -diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/CheckoutSpecificDestinationsTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/CheckoutSpecificDestinationsTest.xml -new file mode 100644 -index 00000000000..f3807388399 ---- /dev/null -+++ b/app/code/Magento/Checkout/Test/Mftf/Test/CheckoutSpecificDestinationsTest.xml -@@ -0,0 +1,86 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> -+ <test name="CheckoutSpecificDestinationsTest"> -+ <annotations> -+ <title value="Check that top destinations can be removed after a selection was previously saved"/> -+ <stories value="MAGETWO-91511: Top destinations cannot be removed after a selection was previously saved"/> -+ <description value="Check that top destinations can be removed after a selection was previously saved"/> -+ <features value="Checkout"/> -+ <severity value="AVERAGE"/> -+ <testCaseId value="MAGETWO-94195"/> -+ <group value="Checkout"/> -+ </annotations> -+ -+ <before> -+ <createData entity="_defaultCategory" stepKey="defaultCategory"/> -+ <createData entity="_defaultProduct" stepKey="simpleProduct"> -+ <requiredEntity createDataKey="defaultCategory"/> -+ </createData> -+ -+ <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> -+ </before> -+ -+ <!--Go to configuration general page--> -+ <actionGroup ref="NavigateToConfigurationGeneralPage" stepKey="navigateToConfigurationGeneralPage"/> -+ -+ <!--Open country options section--> -+ <conditionalClick selector="{{CountryOptionsSection.countryOptions}}" dependentSelector="{{CountryOptionsSection.countryOptionsOpen}}" visible="false" stepKey="clickOnStoreInformation"/> -+ -+ <!--Select top destinations country--> -+ <actionGroup ref="SelectTopDestinationsCountry" stepKey="selectTopDestinationsCountry"> -+ <argument name="countries" value="Countries"/> -+ </actionGroup> -+ -+ <!--Go to product page--> -+ <amOnPage url="{{StorefrontProductPage.url($$simpleProduct.name$$)}}" stepKey="amOnStorefrontProductPage"/> -+ <waitForPageLoad stepKey="waitForProductPageLoad"/> -+ -+ <!--Add product to cart--> -+ <actionGroup ref="StorefrontAddToCartCustomOptionsProductPageActionGroup" stepKey="addToCart"> -+ <argument name="productName" value="$$simpleProduct.name$$"/> -+ </actionGroup> -+ -+ <!--Go to shopping cart--> -+ <amOnPage url="{{CheckoutCartPage.url}}" stepKey="amOnPageShoppingCart"/> -+ -+ <!--Verify country options in checkout top destination section--> -+ <actionGroup ref="VerifyTopDestinationsCountry" stepKey="verifyTopDestinationsCountry"> -+ <argument name="country" value="Bahamas"/> -+ <argument name="placeNumber" value="2"/> -+ </actionGroup> -+ -+ <!--Go to configuration general page--> -+ <actionGroup ref="NavigateToConfigurationGeneralPage" stepKey="navigateToConfigurationGeneralPage2"/> -+ -+ <!--Open country options section--> -+ <conditionalClick selector="{{CountryOptionsSection.countryOptions}}" dependentSelector="{{CountryOptionsSection.countryOptionsOpen}}" visible="false" stepKey="clickOnStoreInformation2"/> -+ -+ <!--Deselect top destinations country--> -+ <actionGroup ref="UnSelectTopDestinationsCountry" stepKey="unSelectTopDestinationsCountry"> -+ <argument name="countries" value="Countries"/> -+ </actionGroup> -+ -+ <!--Go to shopping cart--> -+ <amOnPage url="{{CheckoutCartPage.url}}" stepKey="amOnPageShoppingCart2"/> -+ -+ <!--Verify country options is shown by default--> -+ <actionGroup ref="VerifyTopDestinationsCountry" stepKey="verifyTopDestinationsCountry2"> -+ <argument name="country" value="Afghanistan"/> -+ <argument name="placeNumber" value="2"/> -+ </actionGroup> -+ -+ <after> -+ <actionGroup ref="logout" stepKey="logout"/> -+ -+ <deleteData createDataKey="simpleProduct" stepKey="deleteProduct"/> -+ <deleteData createDataKey="defaultCategory" stepKey="deleteCategory"/> -+ </after> -+ </test> -+</tests> -diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/ConfiguringInstantPurchaseFunctionalityTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/ConfiguringInstantPurchaseFunctionalityTest.xml -new file mode 100644 -index 00000000000..0897e20f1b1 ---- /dev/null -+++ b/app/code/Magento/Checkout/Test/Mftf/Test/ConfiguringInstantPurchaseFunctionalityTest.xml -@@ -0,0 +1,255 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> -+ <test name="ConfiguringInstantPurchaseFunctionalityTest"> -+ <annotations> -+ <features value="One Page Checkout"/> -+ <stories value="Configuring instant purchase"/> -+ <title value="Configuring instant purchase functionality test"/> -+ <description value="Configuring instant purchase"/> -+ <severity value="CRITICAL"/> -+ <testCaseId value="MC-6436"/> -+ <group value="checkout"/> -+ <skip> -+ <issueId value="MQE-1576"/> -+ </skip> -+ </annotations> -+ <before> -+ <!-- Configure Braintree Vault-enabled payment method --> -+ <createData entity="BraintreeConfig" stepKey="braintreeConfigurationData"/> -+ <createData entity="CustomBraintreeConfigurationData" stepKey="enableBraintree"/> -+ -+ <!-- Create customer --> -+ <createData entity="Simple_US_Customer" stepKey="createCustomer"/> -+ -+ <!-- Create product --> -+ <createData entity="SimpleProduct2" stepKey="createProduct"/> -+ -+ <!-- Login as admin --> -+ <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> -+ </before> -+ <after> -+ <!-- Set configs to default --> -+ <createData entity="DefaultBraintreeConfig" stepKey="defaultBraintreeConfig"/> -+ <createData entity="RollBackCustomBraintreeConfigurationData" stepKey="rollBackCustomBraintreeConfigurationData"/> -+ -+ <!-- Delete product --> -+ <deleteData createDataKey="createProduct" stepKey="deleteProduct"/> -+ -+ <!-- Delete customer --> -+ <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> -+ -+ <!-- Admin logout --> -+ <actionGroup ref="logout" stepKey="adminLogout"/> -+ </after> -+ -+ <!-- Create store views --> -+ <actionGroup ref="AdminCreateStoreViewActionGroup" stepKey="createFirstStoreView"> -+ <argument name="customStore" value="storeViewData1"/> -+ </actionGroup> -+ <actionGroup ref="AdminCreateStoreViewActionGroup" stepKey="createSecondStoreView"> -+ <argument name="customStore" value="storeViewData2"/> -+ </actionGroup> -+ -+ <!-- Login to Frontend --> -+ <actionGroup ref="LoginToStorefrontActionGroup" stepKey="loginAsCustomer"> -+ <argument name="Customer" value="$$createCustomer$$"/> -+ </actionGroup> -+ -+ <!-- Add product to cart --> -+ <actionGroup ref="AddSimpleProductToCart" stepKey="addProductToCart"> -+ <argument name="product" value="$$createProduct$$"/> -+ </actionGroup> -+ <actionGroup ref="GoToCheckoutFromMinicartActionGroup" stepKey="goToCheckoutFromMinicart"/> -+ -+ <!-- Customer placed order with payment method save --> -+ <waitForElement selector="{{CheckoutShippingMethodsSection.next}}" stepKey="waitForNextButton"/> -+ <click selector="{{CheckoutShippingMethodsSection.next}}" stepKey="clickNext"/> -+ <waitForElement selector="{{CheckoutPaymentSection.paymentSectionTitle}}" stepKey="waitForPaymentSectionLoaded"/> -+ -+ <!-- Fill Braintree cart data --> -+ <click selector="{{BraintreeConfigurationPaymentSection.creditCart}}" stepKey="selectBraintreePaymentMethod"/> -+ <waitForPageLoad stepKey="waitForBraintreeFormLoad"/> -+ <scrollTo selector="{{BraintreeConfigurationPaymentSection.creditCart}}" stepKey="scrollToCreditCardSection"/> -+ <actionGroup ref="StorefrontFillCartDataActionGroup" stepKey="fillCartData"/> -+ <waitForPageLoad stepKey="waitForFillCartData"/> -+ -+ <!-- Place order --> -+ <click selector="{{BraintreeConfigurationPaymentSection.paymentMethodContainer}}{{CheckoutPaymentSection.placeOrder}}" stepKey="checkoutPlaceOrder"/> -+ <waitForPageLoad stepKey="waitForPageLoad"/> -+ <see selector="{{CheckoutSuccessMainSection.successTitle}}" userInput="Thank you for your purchase!" stepKey="waitForLoadSuccessPage"/> -+ -+ <!-- Go to Configuration > Sales --> -+ <actionGroup ref="AdminOpenInstantPurchaseConfigPageActionGroup" stepKey="openInstantPurchaseConfigPage"/> -+ -+ <!-- Enable Instant Purchase --> -+ <actionGroup ref="AdminChangeInstantPurchaseStatusActionGroup" stepKey="enableInstantPurchase"/> -+ -+ <!-- Switch to specific store view --> -+ <actionGroup ref="AdminSwitchStoreViewActionGroup" stepKey="switchToSpecificStoreView"> -+ <argument name="storeView" value="storeViewData1.name"/> -+ </actionGroup> -+ -+ <!-- Change button text on a single store view --> -+ <actionGroup ref="AdminChangeInstantPurchaseButtonTextActionGroup" stepKey="changeInstantPurchaseButtonText"> -+ <argument name="buttonText" value="Quick Buy"/> -+ </actionGroup> -+ -+ <!-- Verify changes on the front-end by opening a simple product as a logged in customer with saved card and address on given store view: -+ 1. Go to Storefront page --> -+ <actionGroup ref="StorefrontOpenHomePageActionGroup" stepKey="openStorefrontPage"/> -+ -+ <!-- 2. Switch store view --> -+ <actionGroup ref="StorefrontSwitchStoreViewActionGroup" stepKey="SwitchStoreView"> -+ <argument name="storeView" value="storeViewData1"/> -+ </actionGroup> -+ -+ <!-- 3. Assert customer is logged with saved card with address --> -+ <actionGroup ref="OpenStorefrontCustomerStoredPaymentMethodsPageActionGroup" stepKey="openStorefrontCustomerStoredPaymentMethodsPage"/> -+ <actionGroup ref="AssertStorefrontCustomerSavedCardActionGroup" stepKey="assertCustomerPaymentMethod"/> -+ <actionGroup ref="StorefrontCustomerGoToSidebarMenu" stepKey="goToAddressBook"> -+ <argument name="menu" value="Address Book"/> -+ </actionGroup> -+ <see selector="{{CheckoutOrderSummarySection.shippingAddress}}" userInput="{{US_Address_TX.street[0]}} {{US_Address_TX.city}}, {{US_Address_TX.state}}, {{US_Address_TX.postcode}}" stepKey="checkShippingAddress"/> -+ -+ <!-- Open product page --> -+ <actionGroup ref="StorefrontOpenProductPageActionGroup" stepKey="openProductPage"> -+ <argument name="productUrl" value="$$createProduct.custom_attributes[url_key]$$"/> -+ </actionGroup> -+ -+ <!-- Quick Buy button shows up. Clicking it opens review popup --> -+ <actionGroup ref="AssertStorefrontElementVisibleActionGroup" stepKey="seeQuickBuyButton"> -+ <argument name="selector" value="{{StorefrontInstantPurchaseSection.instantPurchaseButton}}"/> -+ <argument name="userInput" value="Quick Buy"/> -+ </actionGroup> -+ <click selector="{{StorefrontInstantPurchaseSection.instantPurchaseButton}}" stepKey="clickQuickBuyButton"/> -+ <waitForElementVisible selector="{{StorefrontInstantPurchasePopupSection.modalTitle}}" stepKey="waitForPopUpTitleVisible"/> -+ <see selector="{{StorefrontInstantPurchasePopupSection.modalTitle}}" userInput="Instant Purchase Confirmation" stepKey="seeReviewPopUp"/> -+ <click selector="{{StorefrontInstantPurchasePopupSection.cancel}}" stepKey="closeModalPopup"/> -+ <waitForPageLoad stepKey="waitForClosing"/> -+ -+ <!-- Verify changes on the front-end by opening a simple product as a logged in customer with saved card and address on a store view for which description was not changed -+ 1. New customer session should be started to verify changes --> -+ <actionGroup ref="StorefrontSignOutActionGroup" stepKey="customerLogout"/> -+ <actionGroup ref="LoginToStorefrontActionGroup" stepKey="loginCustomer"> -+ <argument name="Customer" value="$$createCustomer$$"/> -+ </actionGroup> -+ -+ <!-- 2. Switch store view which description was not changed --> -+ <actionGroup ref="StorefrontSwitchStoreViewActionGroup" stepKey="SwitchToSecondStoreView"> -+ <argument name="storeView" value="storeViewData2"/> -+ </actionGroup> -+ -+ <!-- 3. Assert customer is logged with saved card and address --> -+ <actionGroup ref="OpenStorefrontCustomerStoredPaymentMethodsPageActionGroup" stepKey="openStorefrontCustomerPaymentMethodsPage"/> -+ <actionGroup ref="AssertStorefrontCustomerSavedCardActionGroup" stepKey="assertPaymentMethod"/> -+ <actionGroup ref="StorefrontCustomerGoToSidebarMenu" stepKey="openAddressBook"> -+ <argument name="menu" value="Address Book"/> -+ </actionGroup> -+ <see selector="{{CheckoutOrderSummarySection.shippingAddress}}" userInput="{{US_Address_TX.street[0]}} {{US_Address_TX.city}}, {{US_Address_TX.state}}, {{US_Address_TX.postcode}}" stepKey="seeShippingAddress"/> -+ -+ <!-- Open product page --> -+ <actionGroup ref="StorefrontOpenProductPageActionGroup" stepKey="openCreatedProductPage"> -+ <argument name="productUrl" value="$$createProduct.custom_attributes[url_key]$$"/> -+ </actionGroup> -+ -+ <!-- Instant Purchase button shows up. Clicking it opens review popup. --> -+ <actionGroup ref="AssertStorefrontElementVisibleActionGroup" stepKey="seeInstantPurchaseButton"> -+ <argument name="selector" value="{{StorefrontInstantPurchaseSection.instantPurchaseButton}}"/> -+ <argument name="userInput" value="Instant Purchase"/> -+ </actionGroup> -+ <click selector="{{StorefrontInstantPurchaseSection.instantPurchaseButton}}" stepKey="clickInstantPurchaseButton"/> -+ <waitForElementVisible selector="{{StorefrontInstantPurchasePopupSection.modalTitle}}" stepKey="waitForPopUpVisible"/> -+ <see selector="{{StorefrontInstantPurchasePopupSection.modalTitle}}" userInput="Instant Purchase Confirmation" stepKey="seeReviewPopUpTitle"/> -+ <click selector="{{StorefrontInstantPurchasePopupSection.cancel}}" stepKey="closeModalPopUp"/> -+ <waitForPageLoad stepKey="waitForModalClosing"/> -+ -+ <actionGroup ref="StorefrontSignOutActionGroup" stepKey="createdCustomerLogout"/> -+ -+ <!-- Return to configuration and disable Instant Purchase in a specific store view --> -+ <actionGroup ref="AdminOpenInstantPurchaseConfigPageActionGroup" stepKey="openInstantPurchasePage"/> -+ <scrollToTopOfPage stepKey="scrollToTop"/> -+ <actionGroup ref="AdminSwitchStoreViewActionGroup" stepKey="switchToFirstSpecificStoreView"> -+ <argument name="storeView" value="storeViewData1.name"/> -+ </actionGroup> -+ <actionGroup ref="AdminChangeInstantPurchaseStatusActionGroup" stepKey="disableInstantPurchase"> -+ <argument name="status" value="0"/> -+ </actionGroup> -+ -+ <!-- Verify changes on the front-end by opening a simple product as a logged in customer with saved card and address in the specific store view -+ 1. New customer session should be started to verify changes --> -+ <actionGroup ref="LoginToStorefrontActionGroup" stepKey="loginAsCreatedCustomer"> -+ <argument name="Customer" value="$$createCustomer$$"/> -+ </actionGroup> -+ -+ <!-- 2. Switch store view --> -+ <actionGroup ref="StorefrontSwitchStoreViewActionGroup" stepKey="switchStoreView"> -+ <argument name="storeView" value="storeViewData1"/> -+ </actionGroup> -+ -+ <!-- 3. Assert customer is logged with saved card and address --> -+ <actionGroup ref="OpenStorefrontCustomerStoredPaymentMethodsPageActionGroup" stepKey="openCustomerPaymentMethodsPage"/> -+ <actionGroup ref="AssertStorefrontCustomerSavedCardActionGroup" stepKey="assertCartPaymentMethod"/> -+ <actionGroup ref="StorefrontCustomerGoToSidebarMenu" stepKey="goToAddressBookPage"> -+ <argument name="menu" value="Address Book"/> -+ </actionGroup> -+ <see selector="{{CheckoutOrderSummarySection.shippingAddress}}" userInput="{{US_Address_TX.street[0]}} {{US_Address_TX.city}}, {{US_Address_TX.state}}, {{US_Address_TX.postcode}}" stepKey="assertCustomerShippingAddress"/> -+ -+ <!-- Open product page --> -+ <actionGroup ref="StorefrontOpenProductPageActionGroup" stepKey="openProductIndexPage"> -+ <argument name="productUrl" value="$$createProduct.custom_attributes[url_key]$$"/> -+ </actionGroup> -+ -+ <!-- Quick Buy button is not visible --> -+ <actionGroup ref="AssertStorefrontElementInvisibleActionGroup" stepKey="dontSeeQuickBuyButton"> -+ <argument name="selector" value="{{StorefrontInstantPurchaseSection.instantPurchaseButton}}"/> -+ <argument name="userInput" value="Quick Buy"/> -+ </actionGroup> -+ -+ <!-- Verify changes on the front-end by opening a simple product as a logged in customer with saved card and address in the other store view -+ 1. New customer session should be started to verify changes --> -+ <actionGroup ref="StorefrontSignOutActionGroup" stepKey="logoutCustomer"/> -+ <actionGroup ref="LoginToStorefrontActionGroup" stepKey="customerLoginToStorefront"> -+ <argument name="Customer" value="$$createCustomer$$"/> -+ </actionGroup> -+ -+ <!-- 2. Switch store view --> -+ <actionGroup ref="StorefrontSwitchStoreViewActionGroup" stepKey="switchToSecondStoreView"> -+ <argument name="storeView" value="storeViewData2"/> -+ </actionGroup> -+ -+ <!-- 3. Assert customer is logged with saved card and address --> -+ <actionGroup ref="OpenStorefrontCustomerStoredPaymentMethodsPageActionGroup" stepKey="goToStorefrontCustomerPaymentMethodsPage"/> -+ <actionGroup ref="AssertStorefrontCustomerSavedCardActionGroup" stepKey="assertCardPaymentMethod"/> -+ <actionGroup ref="StorefrontCustomerGoToSidebarMenu" stepKey="openAddressBookPage"> -+ <argument name="menu" value="Address Book"/> -+ </actionGroup> -+ <see selector="{{CheckoutOrderSummarySection.shippingAddress}}" userInput="{{US_Address_TX.street[0]}} {{US_Address_TX.city}}, {{US_Address_TX.state}}, {{US_Address_TX.postcode}}" stepKey="seeCustomerShippingAddress"/> -+ -+ <!-- Open product page --> -+ <actionGroup ref="StorefrontOpenProductPageActionGroup" stepKey="goToProductPage"> -+ <argument name="productUrl" value="$$createProduct.custom_attributes[url_key]$$"/> -+ </actionGroup> -+ -+ <!-- Instant Purchase button is still visible --> -+ <actionGroup ref="AssertStorefrontElementVisibleActionGroup" stepKey="seeInstantPurchaseBtn"> -+ <argument name="selector" value="{{StorefrontInstantPurchaseSection.instantPurchaseButton}}"/> -+ <argument name="userInput" value="Instant Purchase"/> -+ </actionGroup> -+ -+ <!-- Delete store views --> -+ <actionGroup ref="AdminDeleteStoreViewActionGroup" stepKey="deleteFirstStoreView"> -+ <argument name="customStore" value="storeViewData1"/> -+ </actionGroup> -+ <actionGroup ref="AdminDeleteStoreViewActionGroup" stepKey="deleteSecondStoreView"> -+ <argument name="customStore" value="storeViewData2"/> -+ </actionGroup> -+ </test> -+</tests> -diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/DefaultBillingAddressShouldBeCheckedOnPaymentPageTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/DefaultBillingAddressShouldBeCheckedOnPaymentPageTest.xml -new file mode 100644 -index 00000000000..166f5022d5a ---- /dev/null -+++ b/app/code/Magento/Checkout/Test/Mftf/Test/DefaultBillingAddressShouldBeCheckedOnPaymentPageTest.xml -@@ -0,0 +1,63 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> -+ <test name="DefaultBillingAddressShouldBeCheckedOnPaymentPageTest"> -+ <annotations> -+ <features value="Checkout"/> -+ <stories value="Checkout via the Storefront"/> -+ <title value="The default billing address should be used on checkout"/> -+ <description value="Default billing address should be preselected on payments page on checkout if it exist"/> -+ <severity value="CRITICAL"/> -+ <testCaseId value="MAGETWO-98892"/> -+ <useCaseId value="MAGETWO-70996"/> -+ <group value="checkout"/> -+ </annotations> -+ <before> -+ <createData entity="_defaultCategory" stepKey="createCategory"/> -+ <createData entity="SimpleProduct" stepKey="createProduct"> -+ <requiredEntity createDataKey="createCategory"/> -+ </createData> -+ <createData entity="Simple_US_Customer" stepKey="createCustomer"/> -+ <!--Go to Storefront as Customer--> -+ <actionGroup ref="LoginToStorefrontActionGroup" stepKey="customerLogin"> -+ <argument name="Customer" value="$$createCustomer$$" /> -+ </actionGroup> -+ </before> -+ <after> -+ <deleteData createDataKey="createProduct" stepKey="deleteProduct"/> -+ <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> -+ <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> -+ <!--Logout from customer account--> -+ <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="logoutCustomer"/> -+ </after> -+ <!-- Add simple product to cart and go to checkout--> -+ <actionGroup ref="AddSimpleProductToCart" stepKey="addProductToCart"> -+ <argument name="product" value="$$createProduct$$"/> -+ </actionGroup> -+ <actionGroup ref="GoToCheckoutFromMinicartActionGroup" stepKey="goToCheckoutFromMinicart"/> -+ <!-- Click "+ New Address" and Fill new address--> -+ <click selector="{{CheckoutShippingSection.newAddressButton}}" stepKey="addAddress"/> -+ <actionGroup ref="LoggedInCheckoutWithOneAddressFieldWithoutStateField" stepKey="changeAddress"> -+ <argument name="Address" value="UK_Not_Default_Address"/> -+ <argument name="classPrefix" value="._show"/> -+ </actionGroup> -+ <!--Click "Save Addresses" --> -+ <click selector="{{CheckoutShippingSection.saveAddress}}" stepKey="saveAddress"/> -+ <waitForPageLoad stepKey="waitForAddressSaved"/> -+ <dontSeeElement selector="{{StorefrontCheckoutAddressPopupSection.newAddressModalPopup}}" stepKey="dontSeeModalPopup"/> -+ <!--Select Shipping Rate "Flat Rate" and click "Next" button--> -+ <actionGroup ref="CheckoutSelectFlatRateShippingMethodActionGroup" stepKey="selectFlatRateShipping"/> -+ <waitForLoadingMaskToDisappear stepKey="waitForLoadingMask2"/> -+ <click selector="{{CheckoutShippingMethodsSection.next}}" stepKey="clickNext"/> -+ <!--Verify that "My billing and shipping address are the same" is unchecked and billing address is preselected--> -+ <dontSeeCheckboxIsChecked selector="{{CheckoutPaymentSection.billingAddressNotSameCheckbox}}" stepKey="shippingAndBillingAddressIsSameUnchecked"/> -+ <see selector="{{CheckoutPaymentSection.billingAddress}}" userInput="{{US_Address_TX.street[0]}}" stepKey="assertBillingAddress"/> -+ </test> -+</tests> -diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/DeleteBundleDynamicProductFromShoppingCartTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/DeleteBundleDynamicProductFromShoppingCartTest.xml -new file mode 100644 -index 00000000000..5ad4c764026 ---- /dev/null -+++ b/app/code/Magento/Checkout/Test/Mftf/Test/DeleteBundleDynamicProductFromShoppingCartTest.xml -@@ -0,0 +1,69 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> -+ <test name="DeleteBundleDynamicProductFromShoppingCartTest"> -+ <annotations> -+ <features value="Checkout"/> -+ <stories value="Delete Products from Shopping Cart"/> -+ <title value="Delete bundle dynamic product from shopping cart test"/> -+ <description value="Delete bundle dynamic product from shopping cart"/> -+ <severity value="CRITICAL"/> -+ <testCaseId value="MC-14689"/> -+ <group value="checkout"/> -+ <group value="mtf_migrated"/> -+ </annotations> -+ <before> -+ <!-- Create category and simple product --> -+ <createData entity="SimpleSubCategory" stepKey="createCategory"/> -+ <createData entity="_defaultProduct" stepKey="createSimpleProduct"> -+ <requiredEntity createDataKey="createCategory"/> -+ </createData> -+ -+ <!-- Create bundle product --> -+ <createData entity="ApiBundleProductPriceViewRange" stepKey="createBundleDynamicProduct"> -+ <requiredEntity createDataKey="createCategory"/> -+ </createData> -+ <createData entity="DropDownBundleOption" stepKey="bundleOption"> -+ <requiredEntity createDataKey="createBundleDynamicProduct"/> -+ </createData> -+ <createData entity="ApiBundleLink" stepKey="createNewBundleLink"> -+ <requiredEntity createDataKey="createBundleDynamicProduct"/> -+ <requiredEntity createDataKey="bundleOption"/> -+ <requiredEntity createDataKey="createSimpleProduct"/> -+ </createData> -+ <magentoCLI command="indexer:reindex" stepKey="reindex"/> -+ </before> -+ <after> -+ <!-- Delete category --> -+ <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> -+ -+ <!-- Delete bundle product data --> -+ <deleteData createDataKey="createSimpleProduct" stepKey="deleteSimpleProduct"/> -+ <deleteData createDataKey="createBundleDynamicProduct" stepKey="deleteBundleProduct"/> -+ </after> -+ -+ <!-- Go to bundle product page --> -+ <amOnPage url="{{StorefrontProductPage.url($$createBundleDynamicProduct.custom_attributes[url_key]$$)}}" stepKey="navigateToProductPage"/> -+ <waitForPageLoad stepKey="waitForPageLoad"/> -+ -+ <!-- Add product to the cart --> -+ <click selector="{{StorefrontBundleProductActionSection.customizeAndAddToCartButton}}" stepKey="clickCustomizeAndAddToCart"/> -+ <actionGroup ref="addToCartFromStorefrontProductPage" stepKey="addProductToCart"> -+ <argument name="productName" value="$$createBundleDynamicProduct.name$$"/> -+ </actionGroup> -+ -+ <!-- Remove product from cart --> -+ <amOnPage url="{{CheckoutCartPage.url}}" stepKey="goToCart"/> -+ <waitForPageLoad stepKey="waitForCartPageLoad"/> -+ <actionGroup ref="DeleteProductFromShoppingCartActionGroup" stepKey="deleteProduct"> -+ <argument name="productName" value="$$createBundleDynamicProduct.name$$"/> -+ </actionGroup> -+ </test> -+</tests> -diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/DeleteBundleFixedProductFromShoppingCartTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/DeleteBundleFixedProductFromShoppingCartTest.xml -new file mode 100644 -index 00000000000..d1008c58319 ---- /dev/null -+++ b/app/code/Magento/Checkout/Test/Mftf/Test/DeleteBundleFixedProductFromShoppingCartTest.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="DeleteBundleFixedProductFromShoppingCartTest"> -+ <annotations> -+ <features value="Checkout"/> -+ <stories value="Delete Products from Shopping Cart"/> -+ <title value="Delete bundle fixed product from shopping cart test"/> -+ <description value="Delete bundle fixed product from shopping cart"/> -+ <severity value="CRITICAL"/> -+ <testCaseId value="MC-14690"/> -+ <group value="checkout"/> -+ <group value="mtf_migrated"/> -+ </annotations> -+ <before> -+ <!-- Create simple product --> -+ <createData entity="SimpleProduct2" stepKey="createSimpleProduct"/> -+ -+ <!-- Create bundle product --> -+ <createData entity="ApiFixedBundleProduct" stepKey="createFixedBundleProduct"/> -+ <createData entity="DropDownBundleOption" stepKey="createBundleOption"> -+ <requiredEntity createDataKey="createFixedBundleProduct"/> -+ </createData> -+ <createData entity="ApiBundleLink" stepKey="addLinkOptionToBundleProduct"> -+ <requiredEntity createDataKey="createFixedBundleProduct"/> -+ <requiredEntity createDataKey="createBundleOption"/> -+ <requiredEntity createDataKey="createSimpleProduct"/> -+ </createData> -+ <magentoCLI command="indexer:reindex" stepKey="reindex"/> -+ </before> -+ <after> -+ <!-- Delete bundle product data --> -+ <deleteData createDataKey="createSimpleProduct" stepKey="deleteSimpleProduct"/> -+ <deleteData createDataKey="createFixedBundleProduct" stepKey="deleteFixedBundleProduct"/> -+ </after> -+ -+ <!-- Go to bundle product page --> -+ <amOnPage url="{{StorefrontProductPage.url($$createFixedBundleProduct.custom_attributes[url_key]$$)}}" stepKey="navigateToProductPage"/> -+ <waitForPageLoad stepKey="waitForPageLoad"/> -+ -+ <!-- Add product to the cart --> -+ <click selector="{{StorefrontBundleProductActionSection.customizeAndAddToCartButton}}" stepKey="clickCustomizeAndAddToCart"/> -+ <actionGroup ref="addToCartFromStorefrontProductPage" stepKey="addProductToCart"> -+ <argument name="productName" value="$$createFixedBundleProduct.name$$"/> -+ </actionGroup> -+ -+ <!-- Remove product from cart --> -+ <amOnPage url="{{CheckoutCartPage.url}}" stepKey="goToCart"/> -+ <waitForPageLoad stepKey="waitForCartPageLoad"/> -+ <actionGroup ref="DeleteProductFromShoppingCartActionGroup" stepKey="deleteProduct"> -+ <argument name="productName" value="$$createFixedBundleProduct.name$$"/> -+ </actionGroup> -+ </test> -+</tests> -diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/DeleteConfigurableProductFromShoppingCartTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/DeleteConfigurableProductFromShoppingCartTest.xml -new file mode 100644 -index 00000000000..891f647e3d5 ---- /dev/null -+++ b/app/code/Magento/Checkout/Test/Mftf/Test/DeleteConfigurableProductFromShoppingCartTest.xml -@@ -0,0 +1,80 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> -+ <test name="DeleteConfigurableProductFromShoppingCartTest"> -+ <annotations> -+ <features value="Checkout"/> -+ <stories value="Delete Products from Shopping Cart"/> -+ <title value="Delete configurable product from shopping cart test"/> -+ <description value="Delete configurable product from shopping cart"/> -+ <severity value="CRITICAL"/> -+ <testCaseId value="MC-14692"/> -+ <group value="checkout"/> -+ <group value="mtf_migrated"/> -+ </annotations> -+ <before> -+ <!-- Create category --> -+ <createData entity="_defaultCategory" stepKey="createCategory"/> -+ -+ <!-- Create configurable product --> -+ <createData entity="ApiConfigurableProduct" stepKey="createConfigProduct"> -+ <requiredEntity createDataKey="createCategory"/> -+ </createData> -+ <createData entity="productAttributeWithTwoOptions" stepKey="createConfigProductAttribute"/> -+ <createData entity="productAttributeOption1" stepKey="createConfigProductAttributeOption"> -+ <requiredEntity createDataKey="createConfigProductAttribute"/> -+ </createData> -+ <createData entity="AddToDefaultSet" stepKey="createConfigAddToAttributeSet"> -+ <requiredEntity createDataKey="createConfigProductAttribute"/> -+ </createData> -+ <getData entity="ProductAttributeOptionGetter" index="1" stepKey="getConfigAttributeOption"> -+ <requiredEntity createDataKey="createConfigProductAttribute"/> -+ </getData> -+ <createData entity="ApiSimpleOne" stepKey="createConfigChildProduct"> -+ <requiredEntity createDataKey="createConfigProductAttribute"/> -+ <requiredEntity createDataKey="getConfigAttributeOption"/> -+ <requiredEntity createDataKey="createCategory"/> -+ </createData> -+ <createData entity="ConfigurableProductTwoOptions" stepKey="createConfigProductOption"> -+ <requiredEntity createDataKey="createConfigProduct"/> -+ <requiredEntity createDataKey="createConfigProductAttribute"/> -+ <requiredEntity createDataKey="getConfigAttributeOption"/> -+ </createData> -+ <createData entity="ConfigurableProductAddChild" stepKey="createConfigProductAddChild"> -+ <requiredEntity createDataKey="createConfigProduct"/> -+ <requiredEntity createDataKey="createConfigChildProduct"/> -+ </createData> -+ </before> -+ <after> -+ <!-- Delete configurable product data --> -+ <deleteData createDataKey="createConfigChildProduct" stepKey="deleteConfigChildProduct"/> -+ <deleteData createDataKey="createConfigProduct" stepKey="deleteConfigProduct"/> -+ <deleteData createDataKey="createConfigProductAttribute" stepKey="deleteConfigProductAttribute"/> -+ -+ <!-- Delete category --> -+ <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> -+ </after> -+ -+ <!-- Add configurable product to the cart --> -+ <actionGroup ref="StorefrontAddConfigurableProductToTheCartActionGroup" stepKey="addConfigurableProductToCart"> -+ <argument name="urlKey" value="$$createConfigProduct.custom_attributes[url_key]$$" /> -+ <argument name="productAttribute" value="$$createConfigProductAttribute.default_value$$"/> -+ <argument name="productOption" value="$$getConfigAttributeOption.value$$"/> -+ <argument name="qty" value="1"/> -+ </actionGroup> -+ -+ <!-- Remove product from cart --> -+ <amOnPage url="{{CheckoutCartPage.url}}" stepKey="goToCart"/> -+ <waitForPageLoad stepKey="waitForCartPageLoad"/> -+ <actionGroup ref="DeleteProductFromShoppingCartActionGroup" stepKey="deleteProduct"> -+ <argument name="productName" value="$$createConfigProduct.name$$"/> -+ </actionGroup> -+ </test> -+</tests> -diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/DeleteDownloadableProductFromShoppingCartTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/DeleteDownloadableProductFromShoppingCartTest.xml -new file mode 100644 -index 00000000000..1a51e1e02fe ---- /dev/null -+++ b/app/code/Magento/Checkout/Test/Mftf/Test/DeleteDownloadableProductFromShoppingCartTest.xml -@@ -0,0 +1,51 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> -+ <test name="DeleteDownloadableProductFromShoppingCartTest"> -+ <annotations> -+ <features value="Checkout"/> -+ <stories value="Delete Products from Shopping Cart"/> -+ <title value="Delete downloadable product from shopping cart test"/> -+ <description value="Delete downloadable product from shopping cart"/> -+ <severity value="CRITICAL"/> -+ <testCaseId value="MC-14693"/> -+ <group value="checkout"/> -+ <group value="mtf_migrated"/> -+ </annotations> -+ <before> -+ <!-- Create downloadable product --> -+ <createData entity="ApiDownloadableProduct" stepKey="createDownloadableProduct"/> -+ <createData entity="ApiDownloadableLink" stepKey="addDownloadableLink"> -+ <requiredEntity createDataKey="createDownloadableProduct"/> -+ </createData> -+ <createData entity="ApiDownloadableLink" stepKey="addDownloadableLink1"> -+ <requiredEntity createDataKey="createDownloadableProduct"/> -+ </createData> -+ </before> -+ <after> -+ <!-- Delete downloadable product --> -+ <deleteData createDataKey="createDownloadableProduct" stepKey="deleteDownloadableProduct"/> -+ </after> -+ -+ <!-- Add downloadable product to the cart --> -+ <amOnPage url="{{StorefrontProductPage.url($$createDownloadableProduct.custom_attributes[url_key]$$)}}" stepKey="navigateToDownloadableProductPage"/> -+ <waitForPageLoad stepKey="waitForDownloadableProductPageLoad"/> -+ <actionGroup ref="addToCartFromStorefrontProductPage" stepKey="addToCartDownloadableProductFromStorefrontProductPage"> -+ <argument name="productName" value="$$createDownloadableProduct.name$$"/> -+ </actionGroup> -+ -+ <!-- Remove product from cart --> -+ <amOnPage url="{{CheckoutCartPage.url}}" stepKey="goToCart"/> -+ <waitForPageLoad stepKey="waitForCartPageLoad"/> -+ <actionGroup ref="DeleteProductFromShoppingCartActionGroup" stepKey="deleteProduct"> -+ <argument name="productName" value="$$createDownloadableProduct.name$$"/> -+ </actionGroup> -+ </test> -+</tests> -diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/DeleteGroupedProductFromShoppingCartTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/DeleteGroupedProductFromShoppingCartTest.xml -new file mode 100644 -index 00000000000..eb8e753ea0b ---- /dev/null -+++ b/app/code/Magento/Checkout/Test/Mftf/Test/DeleteGroupedProductFromShoppingCartTest.xml -@@ -0,0 +1,69 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> -+ <test name="DeleteGroupedProductFromShoppingCartTest"> -+ <annotations> -+ <features value="Checkout"/> -+ <stories value="Delete Products from Shopping Cart"/> -+ <title value="Delete grouped product from shopping cart test"/> -+ <description value="Delete grouped product from shopping cart"/> -+ <severity value="CRITICAL"/> -+ <testCaseId value="MC-14694"/> -+ <group value="checkout"/> -+ <group value="mtf_migrated"/> -+ </annotations> -+ <before> -+ <!-- Create grouped product with three simple products --> -+ <createData entity="SimpleProduct2" stepKey="createFirstSimpleProduct"/> -+ <createData entity="SimpleProduct2" stepKey="createSecondSimpleProduct"/> -+ <createData entity="SimpleProduct2" stepKey="createThirdSimpleProduct"/> -+ <createData entity="ApiGroupedProduct" stepKey="createGroupedProduct"/> -+ <createData entity="OneSimpleProductLink" stepKey="addFirstProductToLink"> -+ <requiredEntity createDataKey="createGroupedProduct"/> -+ <requiredEntity createDataKey="createFirstSimpleProduct"/> -+ </createData> -+ <updateData entity="OneMoreSimpleProductLink" createDataKey="addFirstProductToLink" stepKey="addSecondProductTwo"> -+ <requiredEntity createDataKey="createGroupedProduct"/> -+ <requiredEntity createDataKey="createSecondSimpleProduct"/> -+ </updateData> -+ <updateData entity="OneMoreSimpleProductLink" createDataKey="addFirstProductToLink" stepKey="addThirdProductThree"> -+ <requiredEntity createDataKey="createGroupedProduct"/> -+ <requiredEntity createDataKey="createThirdSimpleProduct"/> -+ </updateData> -+ </before> -+ <after> -+ <!-- Delete grouped product data --> -+ <deleteData createDataKey="createFirstSimpleProduct" stepKey="deleteFirstProduct"/> -+ <deleteData createDataKey="createSecondSimpleProduct" stepKey="deleteSecondProduct"/> -+ <deleteData createDataKey="createThirdSimpleProduct" stepKey="deleteThirdProduct"/> -+ <deleteData createDataKey="createGroupedProduct" stepKey="deleteGroupedProduct"/> -+ </after> -+ -+ <!-- Add grouped product to the cart --> -+ <actionGroup ref="StorefrontAddThreeGroupedProductToTheCartActionGroup" stepKey="addGropedProductsToTheCart"> -+ <argument name="urlKey" value="$$createGroupedProduct.custom_attributes[url_key]$$"/> -+ <argument name="product1" value="$$createFirstSimpleProduct.name$$"/> -+ <argument name="product2" value="$$createSecondSimpleProduct.name$$"/> -+ <argument name="product3" value="$$createThirdSimpleProduct.name$$"/> -+ <argument name="qty1" value="1"/> -+ <argument name="qty2" value="1"/> -+ <argument name="qty3" value="1"/> -+ </actionGroup> -+ -+ <!-- Remove products from cart --> -+ <amOnPage url="{{CheckoutCartPage.url}}" stepKey="goToCart"/> -+ <waitForPageLoad stepKey="waitForCartPageLoad"/> -+ <click selector="{{CheckoutCartProductSection.removeProductByName($$createFirstSimpleProduct.name$$)}}" stepKey="deleteFirstProductFromCheckoutCart"/> -+ <click selector="{{CheckoutCartProductSection.removeProductByName($$createSecondSimpleProduct.name$$)}}" stepKey="deleteSecondProductFromCheckoutCart"/> -+ <click selector="{{CheckoutCartProductSection.removeProductByName($$createThirdSimpleProduct.name$$)}}" stepKey="deleteThirdProductFromCheckoutCart"/> -+ <waitForPageLoad stepKey="waitForPageLoad"/> -+ <see userInput="You have no items in your shopping cart." stepKey="seeNoItemsInShoppingCart"/> -+ </test> -+</tests> -diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/DeleteVirtualProductFromShoppingCartTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/DeleteVirtualProductFromShoppingCartTest.xml -new file mode 100644 -index 00000000000..97dcdd52127 ---- /dev/null -+++ b/app/code/Magento/Checkout/Test/Mftf/Test/DeleteVirtualProductFromShoppingCartTest.xml -@@ -0,0 +1,47 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> -+ <test name="DeleteVirtualProductFromShoppingCartTest"> -+ <annotations> -+ <features value="Checkout"/> -+ <stories value="Delete Products from Shopping Cart"/> -+ <title value="Delete virtual product from shopping cart test"/> -+ <description value="Delete virtual product from shopping cart"/> -+ <severity value="CRITICAL"/> -+ <testCaseId value="MC-14691"/> -+ <group value="checkout"/> -+ <group value="mtf_migrated"/> -+ </annotations> -+ <before> -+ <!-- Create virtual product --> -+ <createData entity="VirtualProduct" stepKey="createVirtualProduct"> -+ <field key="price">50</field> -+ </createData> -+ </before> -+ <after> -+ <!-- Delete virtual product --> -+ <deleteData createDataKey="createVirtualProduct" stepKey="deleteVirtualProduct"/> -+ </after> -+ -+ <!-- Add virtual product to the cart --> -+ <amOnPage url="{{StorefrontProductPage.url($$createVirtualProduct.custom_attributes[url_key]$$)}}" stepKey="navigateToProductPage"/> -+ <waitForPageLoad stepKey="waitForProductPageLoad"/> -+ <actionGroup ref="addToCartFromStorefrontProductPage" stepKey="addToCartVirtualProductFromStorefrontProductPage"> -+ <argument name="productName" value="$$createVirtualProduct.name$$"/> -+ </actionGroup> -+ -+ <!-- Remove product from cart --> -+ <amOnPage url="{{CheckoutCartPage.url}}" stepKey="goToCart"/> -+ <waitForPageLoad stepKey="waitForCartPageLoad"/> -+ <actionGroup ref="DeleteProductFromShoppingCartActionGroup" stepKey="deleteProduct"> -+ <argument name="productName" value="$$createVirtualProduct.name$$"/> -+ </actionGroup> -+ </test> -+</tests> -diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/EditShippingAddressOnePageCheckoutTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/EditShippingAddressOnePageCheckoutTest.xml -new file mode 100644 -index 00000000000..20015f76e08 ---- /dev/null -+++ b/app/code/Magento/Checkout/Test/Mftf/Test/EditShippingAddressOnePageCheckoutTest.xml -@@ -0,0 +1,80 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> -+ <test name="EditShippingAddressOnePageCheckoutTest"> -+ <annotations> -+ <features value="Checkout"/> -+ <stories value="Edit Shipping Address"/> -+ <title value="Edit Shipping Address on Checkout Page."/> -+ <description value="Edit Shipping Address on Checkout Page."/> -+ <severity value="MAJOR"/> -+ <testCaseId value="MC-14680"/> -+ <group value="shoppingCart"/> -+ <group value="mtf_migrated"/> -+ </annotations> -+ <before> -+ <createData entity="_defaultCategory" stepKey="createCategory"/> -+ <createData entity="simpleProductDefault" stepKey="createProduct"> -+ <requiredEntity createDataKey="createCategory"/> -+ </createData> -+ <createData entity="Simple_US_Customer_NY" stepKey="createCustomer"/> -+ </before> -+ <after> -+ <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> -+ <deleteData createDataKey="createProduct" stepKey="deleteProduct"/> -+ <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> -+ </after> -+ -+ <!-- Go to Frontend as Customer --> -+ <actionGroup ref="LoginToStorefrontActionGroup" stepKey="customerLogin"> -+ <argument name="Customer" value="$$createCustomer$$" /> -+ </actionGroup> -+ -+ <!-- Add product to cart --> -+ <amOnPage url="{{StorefrontCategoryPage.url($$createCategory.name$$)}}" stepKey="onCategoryPage"/> -+ <actionGroup ref="StorefrontAddSimpleProductToCartActionGroup" stepKey="addProductToCart"> -+ <argument name="product" value="$$createProduct$$"/> -+ </actionGroup> -+ -+ <!-- Go to checkout page --> -+ <actionGroup ref="GoToCheckoutFromMinicartActionGroup" stepKey="customerGoToCheckoutFromMinicart" /> -+ -+ <!-- *New Address* button on 1st checkout step --> -+ <click selector="{{CheckoutShippingSection.newAddressButton}}" stepKey="addNewAddress"/> -+ -+ <!--Fill in required fields and click *Save address* button--> -+ <actionGroup ref="FillShippingAddressOneStreetActionGroup" stepKey="changeAddress"> -+ <argument name="address" value="UK_Not_Default_Address"/> -+ </actionGroup> -+ <click selector="{{CheckoutShippingSection.saveAddress}}" stepKey="saveNewAddress"/> -+ -+ <!--Select Shipping Rate--> -+ <scrollTo selector="{{CheckoutShippingMethodsSection.next}}" stepKey="scrollToShippingRate"/> -+ <click selector="{{CheckoutShippingMethodsSection.shippingMethodFlatRate}}" stepKey="selectShippingMethod"/> -+ -+ <!-- Click *Edit* button for the new address --> -+ <click selector="{{CheckoutShippingSection.editActiveAddress}}" stepKey="editNewAddress"/> -+ -+ <!--Remove values from required fields and click *Cancel* button --> -+ <actionGroup ref="ClearShippingAddressActionGroup" stepKey="clearRequiredFields"/> -+ <click selector="{{CheckoutShippingSection.cancelChangeAddress}}" stepKey="cancelEditAddress"/> -+ -+ <!-- Go to *Next* --> -+ <scrollTo selector="{{CheckoutShippingMethodsSection.next}}" stepKey="scrollToButtonNext"/> -+ <click selector="{{CheckoutShippingMethodsSection.next}}" stepKey="goNext"/> -+ -+ <!-- Select payment solution --> -+ <checkOption selector="{{CheckoutPaymentSection.billingAddressNotSameCheckbox}}" stepKey="selectPaymentSolution" /> -+ -+ <!--Refresh Page and Place Order--> -+ <reloadPage stepKey="reloadPage"/> -+ <actionGroup ref="ClickPlaceOrderActionGroup" stepKey="placeOrder"/> -+ </test> -+ </tests> -diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/EndToEndB2CGuestUserTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/EndToEndB2CGuestUserTest.xml -index e386698092a..a4864d612a4 100644 ---- a/app/code/Magento/Checkout/Test/Mftf/Test/EndToEndB2CGuestUserTest.xml -+++ b/app/code/Magento/Checkout/Test/Mftf/Test/EndToEndB2CGuestUserTest.xml -@@ -7,8 +7,13 @@ - --> - - <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="EndToEndB2CGuestUserTest"> -+ <annotations> -+ <skip> -+ <issueId value="MC-16684"/> -+ </skip> -+ </annotations> - <!-- Step 3: User adds products to cart --> - <comment userInput="Start of adding products to cart" stepKey="startOfAddingProductsToCart" after="endOfBrowsingCatalog"/> - <!-- Add Simple Product 1 to cart --> -@@ -96,14 +101,10 @@ - <comment userInput="Check cart information" stepKey="commentCheckCartInformation" after="cartMinicartAssertSimpleProduct2PageImageNotDefault" /> - <actionGroup ref="StorefrontOpenCartFromMinicartActionGroup" stepKey="cartOpenCart" after="commentCheckCartInformation"/> - <actionGroup ref="StorefrontCheckCartActionGroup" stepKey="cartAssertCart" after="cartOpenCart"> -- <!-- @TODO: Change to scalar value after MQE-498 is implemented --> -- <argument name="subtotal" value="E2EB2CQuote.subtotal"/> -- <!-- @TODO: Change to scalar value after MQE-498 is implemented --> -- <argument name="shipping" value="E2EB2CQuote.shipping"/> -- <!-- @TODO: Change to scalar value after MQE-498 is implemented --> -- <argument name="shippingMethod" value="E2EB2CQuote.shippingMethod"/> -- <!-- @TODO: Change to scalar value after MQE-498 is implemented --> -- <argument name="total" value="E2EB2CQuote.total"/> -+ <argument name="subtotal" value="480.00"/> -+ <argument name="shipping" value="15.00"/> -+ <argument name="shippingMethod" value="Flat Rate - Fixed"/> -+ <argument name="total" value="495.00"/> - </actionGroup> - - <!-- Check simple product 1 in cart --> -@@ -157,14 +158,10 @@ - <!-- Check order summary in checkout --> - <comment userInput="Check order summary in checkout" stepKey="commentCheckOrderSummaryInCheckout" after="guestCheckoutFillingShippingSection" /> - <actionGroup ref="CheckOrderSummaryInCheckoutActionGroup" stepKey="guestCheckoutCheckOrderSummary" after="commentCheckOrderSummaryInCheckout"> -- <!-- @TODO: Change to scalar value after MQE-498 is implemented --> -- <argument name="subtotal" value="E2EB2CQuote.subtotal"/> -- <!-- @TODO: Change to scalar value after MQE-498 is implemented --> -- <argument name="shippingTotal" value="E2EB2CQuote.shipping"/> -- <!-- @TODO: Change to scalar value after MQE-498 is implemented --> -- <argument name="shippingMethod" value="E2EB2CQuote.shippingMethod"/> -- <!-- @TODO: Change to scalar value after MQE-498 is implemented --> -- <argument name="total" value="E2EB2CQuote.total"/> -+ <argument name="subtotal" value="480.00"/> -+ <argument name="shippingTotal" value="15.00"/> -+ <argument name="shippingMethod" value="Flat Rate - Fixed"/> -+ <argument name="total" value="495.00"/> - </actionGroup> - - <!-- Check ship to information in checkout --> -diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/EndToEndB2CLoggedInUserTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/EndToEndB2CLoggedInUserTest.xml -index 6effeec6851..a4784a5cdc2 100644 ---- a/app/code/Magento/Checkout/Test/Mftf/Test/EndToEndB2CLoggedInUserTest.xml -+++ b/app/code/Magento/Checkout/Test/Mftf/Test/EndToEndB2CLoggedInUserTest.xml -@@ -7,8 +7,13 @@ - --> - - <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="EndToEndB2CLoggedInUserTest"> -+ <annotations> -+ <skip> -+ <issueId value="MC-16684"/> -+ </skip> -+ </annotations> - <!-- Step 3: User adds products to cart --> - <comment userInput="Start of adding products to cart" stepKey="startOfAddingProductsToCart" after="endOfBrowsingCatalog"/> - <!-- Add Simple Product 1 to cart --> -@@ -96,14 +101,10 @@ - <comment userInput="Check cart information" stepKey="commentCheckCartInformation" after="cartMinicartAssertSimpleProduct2PageImageNotDefault" /> - <actionGroup ref="StorefrontOpenCartFromMinicartActionGroup" stepKey="cartOpenCart" after="commentCheckCartInformation"/> - <actionGroup ref="StorefrontCheckCartActionGroup" stepKey="cartAssertCart" after="cartOpenCart"> -- <!-- @TODO: Change to scalar value after MQE-498 is implemented --> -- <argument name="subtotal" value="E2EB2CQuote.subtotal"/> -- <!-- @TODO: Change to scalar value after MQE-498 is implemented --> -- <argument name="shipping" value="E2EB2CQuote.shipping"/> -- <!-- @TODO: Change to scalar value after MQE-498 is implemented --> -- <argument name="shippingMethod" value="E2EB2CQuote.shippingMethod"/> -- <!-- @TODO: Change to scalar value after MQE-498 is implemented --> -- <argument name="total" value="E2EB2CQuote.total"/> -+ <argument name="subtotal" value="480.00"/> -+ <argument name="shipping" value="15.00"/> -+ <argument name="shippingMethod" value="Flat Rate - Fixed"/> -+ <argument name="total" value="495.00"/> - </actionGroup> - - <!-- Check simple product 1 in cart --> -@@ -157,14 +158,10 @@ - <!-- Check order summary in checkout --> - <comment userInput="Check order summary in checkout" stepKey="commentCheckOrderSummaryInCheckout" after="checkoutFillingShippingSection" /> - <actionGroup ref="CheckOrderSummaryInCheckoutActionGroup" stepKey="checkoutCheckOrderSummary" after="commentCheckOrderSummaryInCheckout"> -- <!-- @TODO: Change to scalar value after MQE-498 is implemented --> -- <argument name="subtotal" value="E2EB2CQuote.subtotal"/> -- <!-- @TODO: Change to scalar value after MQE-498 is implemented --> -- <argument name="shippingTotal" value="E2EB2CQuote.shipping"/> -- <!-- @TODO: Change to scalar value after MQE-498 is implemented --> -- <argument name="shippingMethod" value="E2EB2CQuote.shippingMethod"/> -- <!-- @TODO: Change to scalar value after MQE-498 is implemented --> -- <argument name="total" value="E2EB2CQuote.total"/> -+ <argument name="subtotal" value="480.00"/> -+ <argument name="shippingTotal" value="15.00"/> -+ <argument name="shippingMethod" value="Flat Rate - Fixed"/> -+ <argument name="total" value="495.00"/> - </actionGroup> - - <!-- Check ship to information in checkout --> -diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/IdentityOfDefaultBillingAndShippingAddressTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/IdentityOfDefaultBillingAndShippingAddressTest.xml -new file mode 100644 -index 00000000000..89028e146c3 ---- /dev/null -+++ b/app/code/Magento/Checkout/Test/Mftf/Test/IdentityOfDefaultBillingAndShippingAddressTest.xml -@@ -0,0 +1,72 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> -+ <test name="IdentityOfDefaultBillingAndShippingAddressTest"> -+ <annotations> -+ <features value="Customer"/> -+ <title value="Checking assignment of default billing address after placing an orde"/> -+ <description value="In 'Address book' field 'Default Billing Address' should be the same as 'Default Shipping Address'"/> -+ <severity value="MAJOR"/> -+ <testCaseId value="MAGETWO-94108"/> -+ <stories value="MAGETWO-62891: New address is not marked as 'Default Billing'"/> -+ <group value="customer"/> -+ </annotations> -+ -+ <before> -+ <!--Create product--> -+ <createData stepKey="category" entity="SimpleSubCategory"/> -+ <createData stepKey="product" entity="SimpleProduct"> -+ <requiredEntity createDataKey="category"/> -+ </createData> -+ </before> -+ -+ <!--Go to Storefront--> -+ <amOnPage url="" stepKey="DoToStorefront"/> -+ -+ <!-- Fill out form for a new user with address --> -+ <actionGroup ref="SignUpNewUserFromStorefrontActionGroup" stepKey="signUpNewUser"> -+ <argument name="Customer" value="Simple_US_Customer_NY"/> -+ </actionGroup> -+ -+ <!-- Add simple product to cart --> -+ <actionGroup ref="AddSimpleProductToCart" stepKey="addProductToCart1"> -+ <argument name="product" value="$$product$$"/> -+ </actionGroup> -+ -+ <!--Proceed to shipment--> -+ <amOnPage url="{{CheckoutPage.url}}/" stepKey="goToCheckout"/> -+ <waitForPageLoad stepKey="waitForShippingSection"/> -+ -+ <!--Fill shipment form--> -+ <actionGroup ref="LoggedInUserCheckoutFillingShippingSectionActionGroup" stepKey="checkoutFillingShippingSection" > -+ <argument name="customerVar" value="Simple_US_Customer_NY" /> -+ <argument name="customerAddressVar" value="US_Address_NY" /> -+ </actionGroup> -+ -+ <!--Fill cart data--> -+ <actionGroup ref="CheckoutSelectCheckMoneyOrderPaymentActionGroup" stepKey="selectCheckMoneyOrderPayment" /> -+ -+ <actionGroup ref="CheckoutPlaceOrderActionGroup" stepKey="placeorder"> -+ <argument name="orderNumberMessage" value="CONST.successCheckoutOrderNumberMessage" /> -+ <argument name="emailYouMessage" value="CONST.successCheckoutEmailYouMessage" /> -+ </actionGroup> -+ <!--Go To My Account--> -+ <amOnPage stepKey="goToMyAccountPage" url="/customer/account/"/> -+ -+ <!--Assert That Shipping And Billing Address are the same--> -+ <actionGroup ref="AssertThatShippingAndBillingAddressTheSame" stepKey="assertThatShippingAndBillingAddressTheSame"/> -+ -+ <after> -+ <!--Delete created Product--> -+ <deleteData stepKey="deleteProduct" createDataKey="product"/> -+ <deleteData stepKey="deleteCategory" createDataKey="category"/> -+ </after> -+ </test> -+</tests> -diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/NoErrorCartCheckoutForProductsDeletedFromMiniCartTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/NoErrorCartCheckoutForProductsDeletedFromMiniCartTest.xml -index efa8b4ca751..1f3d9db5ca5 100644 ---- a/app/code/Magento/Checkout/Test/Mftf/Test/NoErrorCartCheckoutForProductsDeletedFromMiniCartTest.xml -+++ b/app/code/Magento/Checkout/Test/Mftf/Test/NoErrorCartCheckoutForProductsDeletedFromMiniCartTest.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="NoErrorCartCheckoutForProductsDeletedFromMiniCartTest"> - <annotations> - <features value="Checkout"/> -diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/OnePageCheckoutAsCustomerUsingDefaultAddressTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/OnePageCheckoutAsCustomerUsingDefaultAddressTest.xml -new file mode 100644 -index 00000000000..aa3665a81bb ---- /dev/null -+++ b/app/code/Magento/Checkout/Test/Mftf/Test/OnePageCheckoutAsCustomerUsingDefaultAddressTest.xml -@@ -0,0 +1,106 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> -+ <test name="OnePageCheckoutAsCustomerUsingDefaultAddressTest"> -+ <annotations> -+ <features value="OnePageCheckout"/> -+ <stories value="OnePageCheckout within Offline Payment Methods"/> -+ <title value="OnePageCheckout as customer using default address test"/> -+ <description value="Checkout as customer using default address"/> -+ <severity value="CRITICAL"/> -+ <testCaseId value="MC-14741"/> -+ <group value="checkout"/> -+ <group value="mtf_migrated"/> -+ </annotations> -+ <before> -+ <!-- Create Simple Product --> -+ <createData entity="SimpleProduct2" stepKey="createSimpleProduct"> -+ <field key="price">560</field> -+ </createData> -+ -+ <!-- Create customer --> -+ <createData entity="Simple_US_Customer_Multiple_Addresses" stepKey="createCustomer"/> -+ </before> -+ <after> -+ <!-- Admin log out --> -+ <actionGroup ref="logout" stepKey="logout"/> -+ -+ <!-- Customer log out --> -+ <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="customerLogout"/> -+ -+ <!-- Delete created product --> -+ <deleteData createDataKey="createSimpleProduct" stepKey="deleteSimpleProduct"/> -+ -+ <!-- Delete customer --> -+ <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> -+ </after> -+ -+ <!-- Add Simple Product to cart --> -+ <amOnPage url="{{StorefrontProductPage.url($$createSimpleProduct.custom_attributes[url_key]$$)}}" stepKey="navigateToSimpleProductPage"/> -+ <waitForPageLoad stepKey="waitForSimpleProductPageLoad"/> -+ <actionGroup ref="addToCartFromStorefrontProductPage" stepKey="addToCartFromStorefrontProductPage"> -+ <argument name="productName" value="$$createSimpleProduct.name$$"/> -+ </actionGroup> -+ -+ <!-- Go to shopping cart --> -+ <actionGroup ref="clickViewAndEditCartFromMiniCart" stepKey="goToShoppingCartFromMinicart"/> -+ <actionGroup ref="FillShippingZipForm" stepKey="fillShippingZipForm"> -+ <argument name="address" value="US_Address_CA"/> -+ </actionGroup> -+ <click selector="{{CheckoutCartSummarySection.proceedToCheckout}}" stepKey="clickProceedToCheckout"/> -+ <waitForPageLoad stepKey="waitForProceedToCheckout"/> -+ -+ -+ <!-- Login as customer on checkout page --> -+ <actionGroup ref="LoginAsCustomerOnCheckoutPageActionGroup" stepKey="customerLogin"> -+ <argument name="customer" value="$createCustomer$"/> -+ </actionGroup> -+ -+ <!-- Fill customer address data --> -+ <waitForElementVisible selector="{{CheckoutShippingSection.shipHereButton(UK_Not_Default_Address.street[0])}}" stepKey="waitForShipHereVisible"/> -+ <!-- Change address --> -+ <click selector="{{CheckoutShippingSection.shipHereButton(UK_Not_Default_Address.street[0])}}" stepKey="clickShipHere"/> -+ -+ <!-- Click next button to open payment section --> -+ <click selector="{{CheckoutShippingGuestInfoSection.next}}" stepKey="clickNext"/> -+ <waitForPageLoad stepKey="waitForShipmentPageLoad"/> -+ -+ <!-- Select payment solution --> -+ <checkOption selector="{{CheckoutPaymentSection.billingAddressNotSameCheckbox}}" stepKey="selectPaymentSolution" /> -+ -+ <!-- Check order summary in checkout --> -+ <waitForElement selector="{{CheckoutPaymentSection.paymentSectionTitle}}" stepKey="waitForPaymentSectionLoaded"/> -+ <click selector="{{CheckoutPaymentSection.placeOrder}}" stepKey="clickPlaceOrderButton"/> -+ <seeElement selector="{{CheckoutSuccessMainSection.success}}" stepKey="orderIsSuccessfullyPlaced"/> -+ <grabTextFrom selector="{{CheckoutSuccessMainSection.orderNumber22}}" stepKey="grabOrderNumber"/> -+ -+ <!-- Login as admin --> -+ <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> -+ -+ <!-- Open created order in backend --> -+ <amOnPage url="{{AdminOrdersPage.url}}" stepKey="goToOrders"/> -+ <waitForPageLoad stepKey="waitForOrdersPageLoad"/> -+ <actionGroup ref="OpenOrderById" stepKey="filterOrderGridById"> -+ <argument name="orderId" value="$grabOrderNumber"/> -+ </actionGroup> -+ -+ <!-- Assert order total --> -+ <scrollTo selector="{{AdminOrderTotalSection.grandTotal}}" stepKey="scrollToOrderTotalSection"/> -+ <see selector="{{AdminOrderTotalSection.grandTotal}}" userInput="$565.00" stepKey="checkOrderTotalInBackend"/> -+ -+ <!-- Assert order addresses --> -+ <see selector="{{AdminShipmentAddressInformationSection.billingAddress}}" userInput="{{UK_Not_Default_Address.street[0]}}" stepKey="seeBillingAddressStreet"/> -+ <see selector="{{AdminShipmentAddressInformationSection.billingAddress}}" userInput="{{UK_Not_Default_Address.city}}" stepKey="seeBillingAddressCity"/> -+ <see selector="{{AdminShipmentAddressInformationSection.billingAddress}}" userInput="{{UK_Not_Default_Address.postcode}}" stepKey="seeBillingAddressPostcode"/> -+ <see selector="{{AdminShipmentAddressInformationSection.shippingAddress}}" userInput="{{UK_Not_Default_Address.street[0]}}" stepKey="seeShippingAddressStreet"/> -+ <see selector="{{AdminShipmentAddressInformationSection.shippingAddress}}" userInput="{{UK_Not_Default_Address.city}}" stepKey="seeShippingAddressCity"/> -+ <see selector="{{AdminShipmentAddressInformationSection.shippingAddress}}" userInput="{{UK_Not_Default_Address.postcode}}" stepKey="seeShippingAddressPostcode"/> -+ </test> -+</tests> -diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/OnePageCheckoutAsCustomerUsingNewAddressTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/OnePageCheckoutAsCustomerUsingNewAddressTest.xml -new file mode 100644 -index 00000000000..bafad6f28a6 ---- /dev/null -+++ b/app/code/Magento/Checkout/Test/Mftf/Test/OnePageCheckoutAsCustomerUsingNewAddressTest.xml -@@ -0,0 +1,119 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> -+ <test name="OnePageCheckoutAsCustomerUsingNewAddressTest"> -+ <annotations> -+ <features value="OnePageCheckout"/> -+ <stories value="OnePageCheckout within Offline Payment Methods"/> -+ <title value="OnePageCheckout as customer using new address test"/> -+ <description value="Checkout as customer using new address"/> -+ <severity value="CRITICAL"/> -+ <testCaseId value="MC-14740"/> -+ <group value="checkout"/> -+ <group value="mtf_migrated"/> -+ </annotations> -+ <before> -+ <!-- Create Simple Product --> -+ <createData entity="SimpleProduct2" stepKey="createSimpleProduct"> -+ <field key="price">560</field> -+ </createData> -+ -+ <!-- Create customer --> -+ <createData entity="Simple_US_Customer_NY" stepKey="createCustomer"/> -+ </before> -+ <after> -+ <!-- Admin log out --> -+ <actionGroup ref="logout" stepKey="logout"/> -+ -+ <!-- Customer log out --> -+ <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="customerLogout"/> -+ -+ <!-- Delete created product --> -+ <deleteData createDataKey="createSimpleProduct" stepKey="deleteSimpleProduct"/> -+ -+ <!-- Delete customer --> -+ <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> -+ </after> -+ -+ <!-- Add Simple Product to cart --> -+ <amOnPage url="{{StorefrontProductPage.url($$createSimpleProduct.custom_attributes[url_key]$$)}}" stepKey="navigateToSimpleProductPage"/> -+ <waitForPageLoad stepKey="waitForSimpleProductPageLoad"/> -+ <actionGroup ref="addToCartFromStorefrontProductPage" stepKey="addToCartFromStorefrontProductPage"> -+ <argument name="productName" value="$$createSimpleProduct.name$$"/> -+ </actionGroup> -+ -+ <!-- Go to shopping cart --> -+ <actionGroup ref="clickViewAndEditCartFromMiniCart" stepKey="goToShoppingCartFromMinicart"/> -+ <actionGroup ref="FillShippingZipForm" stepKey="fillShippingZipForm"> -+ <argument name="address" value="US_Address_CA"/> -+ </actionGroup> -+ <click selector="{{CheckoutCartSummarySection.proceedToCheckout}}" stepKey="clickProceedToCheckout"/> -+ <waitForPageLoad stepKey="waitForProceedToCheckout"/> -+ -+ <!-- Login using Sign In link from checkout page --> -+ <actionGroup ref="LoginAsCustomerUsingSignInLinkActionGroup" stepKey="customerLogin"> -+ <argument name="customer" value="$$createCustomer$$"/> -+ </actionGroup> -+ -+ <!-- Add new address --> -+ <click selector="{{CheckoutShippingSection.newAddressButton}}" stepKey="addNewAddress"/> -+ -+ <!-- Fill in required fields and save --> -+ <actionGroup ref="FillShippingAddressOneStreetActionGroup" stepKey="changeAddress"> -+ <argument name="address" value="UK_Not_Default_Address"/> -+ </actionGroup> -+ <click selector="{{CheckoutShippingSection.saveAddress}}" stepKey="saveNewAddress"/> -+ <waitForPageLoad stepKey="waitForAddressSaving"/> -+ -+ <!-- Click next button to open payment section --> -+ <click selector="{{CheckoutShippingSection.next}}" stepKey="clickNext"/> -+ <waitForPageLoad stepKey="waitForShipmentPageLoad"/> -+ -+ <!-- Change the address --> -+ <uncheckOption selector="{{CheckoutPaymentSection.billingAddressNotSameCheckbox}}" stepKey="selectPaymentSolution"/> -+ <click selector="{{CheckoutPaymentSection.editAddress}}" stepKey="editAddress"/> -+ <waitForElementVisible selector="{{CheckoutShippingSection.addressDropdown}}" stepKey="waitForAddressDropDownToBeVisible"/> -+ <selectOption selector="{{CheckoutShippingSection.addressDropdown}}" userInput="New Address" stepKey="addAddress"/> -+ <waitForPageLoad stepKey="waitForNewAddressForm"/> -+ <actionGroup ref="LoggedInCheckoutFillNewBillingAddressActionGroup" stepKey="changeBillingAddress"> -+ <argument name="Address" value="US_Address_NY"/> -+ </actionGroup> -+ <click selector="{{CheckoutShippingSection.updateAddress}}" stepKey="saveAddress"/> -+ <waitForPageLoad stepKey="waitForAddressSaved"/> -+ -+ <!-- Place order --> -+ <click selector="{{CheckoutPaymentSection.placeOrder}}" stepKey="clickPlaceOrder"/> -+ <waitForPageLoad stepKey="waitForCheckoutPaymentSectionPageLoad"/> -+ <seeElement selector="{{CheckoutSuccessMainSection.success}}" stepKey="orderIsSuccessfullyPlaced"/> -+ <grabTextFrom selector="{{CheckoutSuccessMainSection.orderNumber22}}" stepKey="grabOrderNumber"/> -+ -+ <!-- Login as admin --> -+ <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> -+ -+ <!-- Open created order in backend --> -+ <amOnPage url="{{AdminOrdersPage.url}}" stepKey="goToOrders"/> -+ <waitForPageLoad stepKey="waitForOrdersPageLoad"/> -+ <actionGroup ref="OpenOrderById" stepKey="filterOrderGridById"> -+ <argument name="orderId" value="$grabOrderNumber"/> -+ </actionGroup> -+ -+ <!-- Assert order total --> -+ <scrollTo selector="{{AdminOrderTotalSection.grandTotal}}" stepKey="scrollToOrderTotalSection"/> -+ <see selector="{{AdminOrderTotalSection.grandTotal}}" userInput="$565.00" stepKey="checkOrderTotalInBackend"/> -+ -+ <!-- Assert order addresses --> -+ <see selector="{{AdminShipmentAddressInformationSection.billingAddress}}" userInput="{{US_Address_NY.street[0]}}" stepKey="seeBillingAddressStreet"/> -+ <see selector="{{AdminShipmentAddressInformationSection.billingAddress}}" userInput="{{US_Address_NY.city}}" stepKey="seeBillingAddressCity"/> -+ <see selector="{{AdminShipmentAddressInformationSection.billingAddress}}" userInput="{{US_Address_NY.postcode}}" stepKey="seeBillingAddressPostcode"/> -+ <see selector="{{AdminShipmentAddressInformationSection.shippingAddress}}" userInput="{{UK_Not_Default_Address.street[0]}}" stepKey="seeShippingAddressStreet"/> -+ <see selector="{{AdminShipmentAddressInformationSection.shippingAddress}}" userInput="{{UK_Not_Default_Address.city}}" stepKey="seeShippingAddressCity"/> -+ <see selector="{{AdminShipmentAddressInformationSection.shippingAddress}}" userInput="{{UK_Not_Default_Address.postcode}}" stepKey="seeShippingAddressPostcode"/> -+ </test> -+</tests> -diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/OnePageCheckoutAsCustomerUsingNonDefaultAddressTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/OnePageCheckoutAsCustomerUsingNonDefaultAddressTest.xml -new file mode 100644 -index 00000000000..2c341a5c4c1 ---- /dev/null -+++ b/app/code/Magento/Checkout/Test/Mftf/Test/OnePageCheckoutAsCustomerUsingNonDefaultAddressTest.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="OnePageCheckoutAsCustomerUsingNonDefaultAddressTest"> -+ <annotations> -+ <features value="OnePageCheckout"/> -+ <stories value="OnePageCheckout within Offline Payment Methods"/> -+ <title value="OnePageCheckout as customer using non default address test"/> -+ <description value="Checkout as customer using non default address"/> -+ <severity value="CRITICAL"/> -+ <testCaseId value="MC-14739"/> -+ <group value="checkout"/> -+ <group value="mtf_migrated"/> -+ </annotations> -+ <before> -+ <!-- Create Simple Product --> -+ <createData entity="SimpleProduct2" stepKey="createSimpleProduct"> -+ <field key="price">560</field> -+ </createData> -+ -+ <!-- Create customer --> -+ <createData entity="Customer_US_UK_DE" stepKey="createCustomer"/> -+ </before> -+ <after> -+ <!-- Admin log out --> -+ <actionGroup ref="logout" stepKey="logout"/> -+ -+ <!-- Customer Log out --> -+ <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="customerLogout"/> -+ -+ <!-- Delete created product --> -+ <deleteData createDataKey="createSimpleProduct" stepKey="deleteSimpleProduct"/> -+ -+ <!-- Delete customer --> -+ <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> -+ </after> -+ -+ <!-- Add Simple Product to cart --> -+ <amOnPage url="{{StorefrontProductPage.url($$createSimpleProduct.custom_attributes[url_key]$$)}}" stepKey="navigateToSimpleProductPage"/> -+ <waitForPageLoad stepKey="waitForSimpleProductPageLoad"/> -+ <actionGroup ref="addToCartFromStorefrontProductPage" stepKey="addToCartFromStorefrontProductPage"> -+ <argument name="productName" value="$$createSimpleProduct.name$$"/> -+ </actionGroup> -+ -+ <!-- Go to shopping cart --> -+ <actionGroup ref="clickViewAndEditCartFromMiniCart" stepKey="goToShoppingCartFromMinicart"/> -+ <actionGroup ref="FillShippingZipForm" stepKey="fillShippingZipForm"> -+ <argument name="address" value="US_Address_CA"/> -+ </actionGroup> -+ <click selector="{{CheckoutCartSummarySection.proceedToCheckout}}" stepKey="clickProceedToCheckout"/> -+ <waitForPageLoad stepKey="waitForProceedToCheckout"/> -+ -+ <!-- Login as customer on checkout page --> -+ <actionGroup ref="LoginAsCustomerOnCheckoutPageActionGroup" stepKey="customerLogin"> -+ <argument name="customer" value="$$createCustomer$$"/> -+ </actionGroup> -+ <click selector="{{CheckoutShippingSection.shipHereButton(DE_Address_Berlin_Not_Default_Address.street[0])}}" stepKey="clickShipHere"/> -+ -+ <!-- Click next button to open payment section --> -+ <click selector="{{CheckoutShippingGuestInfoSection.next}}" stepKey="clickNext"/> -+ <waitForPageLoad stepKey="waitForShipmentPageLoad"/> -+ <uncheckOption selector="{{CheckoutPaymentSection.billingAddressNotSameCheckbox}}" stepKey="selectPaymentSolution"/> -+ -+ <!-- Change the address --> -+ <click selector="{{CheckoutPaymentSection.editAddress}}" stepKey="editAddress"/> -+ <waitForElementVisible selector="{{CheckoutShippingSection.addressDropdown}}" stepKey="waitForDropDownToBeVisible"/> -+ <selectOption selector="{{CheckoutShippingSection.addressDropdown}}" userInput="{{UK_Not_Default_Address.street[0]}}" stepKey="addAddress"/> -+ -+ <!-- Check order summary in checkout --> -+ <click selector="{{CheckoutShippingSection.updateAddress}}" stepKey="clickToUpdate"/> -+ <waitForPageLoad stepKey="waitForPageLoad"/> -+ -+ <!-- Place order --> -+ <click selector="{{CheckoutPaymentSection.placeOrder}}" stepKey="clickPlaceOrder"/> -+ <waitForPageLoad stepKey="waitForCheckoutPaymentSectionPageLoad"/> -+ <seeElement selector="{{CheckoutSuccessMainSection.success}}" stepKey="orderIsSuccessfullyPlaced"/> -+ <grabTextFrom selector="{{CheckoutSuccessMainSection.orderNumber22}}" stepKey="grabOrderNumber"/> -+ -+ <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> -+ -+ <!-- Open created order in backend --> -+ <amOnPage url="{{AdminOrdersPage.url}}" stepKey="goToOrders"/> -+ <waitForPageLoad stepKey="waitForOrdersPageLoad"/> -+ <actionGroup ref="OpenOrderById" stepKey="filterOrderGridById"> -+ <argument name="orderId" value="$grabOrderNumber"/> -+ </actionGroup> -+ -+ <!-- Assert order total --> -+ <scrollTo selector="{{AdminOrderTotalSection.grandTotal}}" stepKey="scrollToOrderTotalSection"/> -+ <see selector="{{AdminOrderTotalSection.grandTotal}}" userInput="$565.00" stepKey="checkOrderTotalInBackend"/> -+ -+ <!-- Assert order addresses --> -+ <see selector="{{AdminShipmentAddressInformationSection.billingAddress}}" userInput="{{UK_Not_Default_Address.street[0]}}" stepKey="seeBillingAddressStreet"/> -+ <see selector="{{AdminShipmentAddressInformationSection.billingAddress}}" userInput="{{UK_Not_Default_Address.city}}" stepKey="seeBillingAddressCity"/> -+ <see selector="{{AdminShipmentAddressInformationSection.billingAddress}}" userInput="{{UK_Not_Default_Address.postcode}}" stepKey="seeBillingAddressPostcode"/> -+ <see selector="{{AdminShipmentAddressInformationSection.shippingAddress}}" userInput="{{DE_Address_Berlin_Not_Default_Address.street[0]}}" stepKey="seeShippingAddressStreet"/> -+ <see selector="{{AdminShipmentAddressInformationSection.shippingAddress}}" userInput="{{DE_Address_Berlin_Not_Default_Address.city}}" stepKey="seeShippingAddressCity"/> -+ <see selector="{{AdminShipmentAddressInformationSection.shippingAddress}}" userInput="{{DE_Address_Berlin_Not_Default_Address.postcode}}" stepKey="seeShippingAddressPostcode"/> -+ </test> -+</tests> -diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/OnePageCheckoutUsingSignInLinkTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/OnePageCheckoutUsingSignInLinkTest.xml -new file mode 100644 -index 00000000000..990459d7c81 ---- /dev/null -+++ b/app/code/Magento/Checkout/Test/Mftf/Test/OnePageCheckoutUsingSignInLinkTest.xml -@@ -0,0 +1,93 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> -+ <test name="OnePageCheckoutUsingSignInLinkTest"> -+ <annotations> -+ <features value="OnePageCheckout"/> -+ <stories value="OnePageCheckout within Offline Payment Methods"/> -+ <title value="OnePageCheckout using sign in link test"/> -+ <description value="Checkout using 'Sign In' link"/> -+ <severity value="CRITICAL"/> -+ <testCaseId value="MC-14738"/> -+ <group value="checkout"/> -+ <group value="mtf_migrated"/> -+ </annotations> -+ <before> -+ <!-- Create Simple Product --> -+ <createData entity="SimpleProduct2" stepKey="createSimpleProduct"> -+ <field key="price">560</field> -+ </createData> -+ -+ <!-- Create customer --> -+ <createData entity="Simple_US_Customer_With_Different_Billing_Shipping_Addresses" stepKey="createCustomer"/> -+ </before> -+ <after> -+ <!-- Admin log out --> -+ <actionGroup ref="logout" stepKey="logout"/> -+ -+ <!-- Customer Log out --> -+ <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="customerLogout"/> -+ -+ <!-- Delete created product --> -+ <deleteData createDataKey="createSimpleProduct" stepKey="deleteSimpleProduct"/> -+ -+ <!-- Delete customer --> -+ <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> -+ </after> -+ -+ <!-- Add Simple Product to cart --> -+ <amOnPage url="{{StorefrontProductPage.url($$createSimpleProduct.custom_attributes[url_key]$$)}}" stepKey="navigateToSimpleProductPage"/> -+ <waitForPageLoad stepKey="waitForSimpleProductPageLoad"/> -+ <actionGroup ref="addToCartFromStorefrontProductPage" stepKey="addToCartFromStorefrontProductPage"> -+ <argument name="productName" value="$$createSimpleProduct.name$$"/> -+ </actionGroup> -+ -+ <!-- Go to shopping cart --> -+ <actionGroup ref="clickViewAndEditCartFromMiniCart" stepKey="goToShoppingCartFromMinicart"/> -+ <actionGroup ref="FillShippingZipForm" stepKey="fillShippingZipForm"> -+ <argument name="address" value="US_Address_CA"/> -+ </actionGroup> -+ <click selector="{{CheckoutCartSummarySection.proceedToCheckout}}" stepKey="clickProceedToCheckout"/> -+ <waitForPageLoad stepKey="waitForProceedToCheckout"/> -+ -+ <!-- Login using Sign In link from checkout page --> -+ <actionGroup ref="LoginAsCustomerUsingSignInLinkActionGroup" stepKey="customerLogin"> -+ <argument name="customer" value="$$createCustomer$$"/> -+ </actionGroup> -+ -+ <!-- Click next button to open payment section --> -+ <click selector="{{CheckoutShippingGuestInfoSection.next}}" stepKey="clickNext"/> -+ <waitForPageLoad stepKey="waitForShipmentPageLoad"/> -+ -+ <!-- Select payment solution --> -+ <checkOption selector="{{CheckoutPaymentSection.billingAddressNotSameCheckbox}}" stepKey="selectPaymentSolution" /> -+ -+ <!-- Check order summary in checkout --> -+ <waitForElement selector="{{CheckoutPaymentSection.paymentSectionTitle}}" stepKey="waitForPaymentSectionLoaded"/> -+ <click selector="{{CheckoutPaymentSection.placeOrder}}" stepKey="clickPlaceOrderButton"/> -+ <seeElement selector="{{CheckoutSuccessMainSection.success}}" stepKey="orderIsSuccessfullyPlaced"/> -+ <grabTextFrom selector="{{CheckoutSuccessMainSection.orderNumber22}}" stepKey="grabOrderNumber"/> -+ -+ <!-- Login as admin --> -+ <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> -+ -+ <!-- Open created order in backend --> -+ <amOnPage url="{{AdminOrdersPage.url}}" stepKey="goToOrders"/> -+ <waitForPageLoad stepKey="waitForOrdersPageLoad"/> -+ <actionGroup ref="OpenOrderById" stepKey="filterOrderGridById"> -+ <argument name="orderId" value="$grabOrderNumber"/> -+ </actionGroup> -+ -+ <!-- Assert that shipping and billing address are the same --> -+ <grabTextFrom selector="{{AdminShipmentAddressInformationSection.shippingAddress}}" stepKey="shippingAddress"/> -+ <grabTextFrom selector="{{AdminShipmentAddressInformationSection.billingAddress}}" stepKey="billingAddress"/> -+ <assertEquals stepKey="assertAddress" actual="$billingAddress" expected="$shippingAddress"/> -+ </test> -+</tests> -diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/OnePageCheckoutWithAllProductTypesTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/OnePageCheckoutWithAllProductTypesTest.xml -new file mode 100644 -index 00000000000..3ec73aec580 ---- /dev/null -+++ b/app/code/Magento/Checkout/Test/Mftf/Test/OnePageCheckoutWithAllProductTypesTest.xml -@@ -0,0 +1,223 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> -+ <test name="OnePageCheckoutWithAllProductTypesTest"> -+ <annotations> -+ <features value="OnePageCheckout"/> -+ <stories value="OnePageCheckout within Offline Payment Methods"/> -+ <title value="OnePageCheckout with all product types test"/> -+ <description value="Checkout with all product types"/> -+ <severity value="CRITICAL"/> -+ <testCaseId value="MC-14742"/> -+ <group value="checkout"/> -+ <group value="mtf_migrated"/> -+ </annotations> -+ <before> -+ <!-- Create category --> -+ <createData entity="_defaultCategory" stepKey="createCategory"/> -+ -+ <!-- Create Configurable Product --> -+ <createData entity="ApiConfigurableProduct" stepKey="createConfigProduct"> -+ <requiredEntity createDataKey="createCategory"/> -+ </createData> -+ <createData entity="productAttributeWithTwoOptions" stepKey="createConfigProductAttribute"/> -+ <createData entity="productAttributeOption1" stepKey="createConfigProductAttributeOption"> -+ <requiredEntity createDataKey="createConfigProductAttribute"/> -+ </createData> -+ <createData entity="AddToDefaultSet" stepKey="createConfigAddToAttributeSet"> -+ <requiredEntity createDataKey="createConfigProductAttribute"/> -+ </createData> -+ <getData entity="ProductAttributeOptionGetter" index="1" stepKey="getConfigAttributeOption"> -+ <requiredEntity createDataKey="createConfigProductAttribute"/> -+ </getData> -+ <createData entity="ApiSimpleOne" stepKey="createConfigChildProduct"> -+ <requiredEntity createDataKey="createConfigProductAttribute"/> -+ <requiredEntity createDataKey="getConfigAttributeOption"/> -+ <requiredEntity createDataKey="createCategory"/> -+ </createData> -+ <createData entity="ConfigurableProductTwoOptions" stepKey="createConfigProductOption"> -+ <requiredEntity createDataKey="createConfigProduct"/> -+ <requiredEntity createDataKey="createConfigProductAttribute"/> -+ <requiredEntity createDataKey="getConfigAttributeOption"/> -+ </createData> -+ <createData entity="ConfigurableProductAddChild" stepKey="createConfigProductAddChild"> -+ <requiredEntity createDataKey="createConfigProduct"/> -+ <requiredEntity createDataKey="createConfigChildProduct"/> -+ </createData> -+ -+ <!--Create Bundle Product --> -+ <createData entity="SimpleProduct2" stepKey="createSimpleProductForBundleProduct"/> -+ <createData entity="ApiFixedBundleProduct" stepKey="createFixedBundleProduct"/> -+ <createData entity="DropDownBundleOption" stepKey="createBundleOption"> -+ <requiredEntity createDataKey="createFixedBundleProduct"/> -+ </createData> -+ <createData entity="ApiBundleLink" stepKey="firstLinkOptionToFixedProduct"> -+ <requiredEntity createDataKey="createFixedBundleProduct"/> -+ <requiredEntity createDataKey="createBundleOption"/> -+ <requiredEntity createDataKey="createSimpleProductForBundleProduct"/> -+ </createData> -+ -+ <!-- Create Virtual Product --> -+ <createData entity="VirtualProduct" stepKey="createVirtualProduct"/> -+ -+ <!--Create Downloadable Product --> -+ <createData entity="ApiDownloadableProduct" stepKey="createDownloadableProduct"/> -+ <createData entity="ApiDownloadableLink" stepKey="addDownloadableLink"> -+ <requiredEntity createDataKey="createDownloadableProduct"/> -+ </createData> -+ <createData entity="ApiDownloadableLink" stepKey="addDownloadableLink1"> -+ <requiredEntity createDataKey="createDownloadableProduct"/> -+ </createData> -+ -+ <!-- Create Grouped Product --> -+ <createData entity="SimpleProduct2" stepKey="createSimpleProduct"/> -+ <createData entity="ApiGroupedProduct" stepKey="createGroupedProduct"/> -+ <createData entity="OneSimpleProductLink" stepKey="addProductOne"> -+ <requiredEntity createDataKey="createGroupedProduct"/> -+ <requiredEntity createDataKey="createSimpleProduct"/> -+ </createData> -+ -+ <!-- Create customer --> -+ <createData entity="Simple_Customer_Without_Address" stepKey="createCustomer"/> -+ -+ <magentoCLI command="indexer:reindex" stepKey="reindex"/> -+ </before> -+ <after> -+ <!-- Delete category --> -+ <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> -+ -+ <!-- Delete all created products --> -+ <deleteData createDataKey="createConfigChildProduct" stepKey="deleteConfigChildProduct"/> -+ <deleteData createDataKey="createConfigProduct" stepKey="deleteConfigProduct"/> -+ <deleteData createDataKey="createConfigProductAttribute" stepKey="deleteConfigProductAttribute"/> -+ <deleteData createDataKey="createSimpleProductForBundleProduct" stepKey="deleteSimpleProductForBundleProduct"/> -+ <deleteData createDataKey="createFixedBundleProduct" stepKey="deleteFixedBundleProduct"/> -+ <deleteData createDataKey="createVirtualProduct" stepKey="deleteVirtualProduct"/> -+ <deleteData createDataKey="createDownloadableProduct" stepKey="deleteDownloadableProduct"/> -+ <deleteData createDataKey="createSimpleProduct" stepKey="deleteSimpleProduct"/> -+ <deleteData createDataKey="createGroupedProduct" stepKey="deleteGroupedProduct"/> -+ -+ <!-- Delete customer --> -+ <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> -+ -+ <!-- Logout customer --> -+ <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="customerLogoutStorefront"/> -+ </after> -+ -+ <!-- Add Simple Product to cart --> -+ <amOnPage url="{{StorefrontProductPage.url($$createSimpleProduct.custom_attributes[url_key]$$)}}" stepKey="navigateToSimpleProductPage"/> -+ <waitForPageLoad stepKey="waitForSimpleProductPageLoad"/> -+ <actionGroup ref="addToCartFromStorefrontProductPage" stepKey="addToCartFromStorefrontProductPage"> -+ <argument name="productName" value="$$createSimpleProduct.name$$"/> -+ </actionGroup> -+ -+ <!-- Add Configurable Product to cart --> -+ <actionGroup ref="StorefrontAddConfigurableProductToTheCartActionGroup" stepKey="addConfigurableProductToCart"> -+ <argument name="urlKey" value="$$createConfigProduct.custom_attributes[url_key]$$" /> -+ <argument name="productAttribute" value="$$createConfigProductAttribute.default_value$$"/> -+ <argument name="productOption" value="$$getConfigAttributeOption.value$$"/> -+ <argument name="qty" value="1"/> -+ </actionGroup> -+ -+ <!-- Add Virtual Product to cart --> -+ <amOnPage url="{{StorefrontProductPage.url($$createVirtualProduct.custom_attributes[url_key]$$)}}" stepKey="navigateToVirtualProductPage"/> -+ <waitForPageLoad stepKey="waitForVirtualProductPageLoad"/> -+ <actionGroup ref="addToCartFromStorefrontProductPage" stepKey="addToCartVirtualProductFromStorefrontProductPage"> -+ <argument name="productName" value="$$createVirtualProduct.name$$"/> -+ </actionGroup> -+ -+ <!-- Add Downloadable Product to cart --> -+ <amOnPage url="{{StorefrontProductPage.url($$createDownloadableProduct.custom_attributes[url_key]$$)}}" stepKey="navigateToDownloadableProductPage"/> -+ <waitForPageLoad stepKey="waitForDownloadableProductPageLoad"/> -+ <actionGroup ref="addToCartFromStorefrontProductPage" stepKey="addToCartDownloadableProductFromStorefrontProductPage"> -+ <argument name="productName" value="$$createDownloadableProduct.name$$"/> -+ </actionGroup> -+ -+ <!-- Add Grouped Product to cart --> -+ <amOnPage url="{{StorefrontProductPage.url($$createGroupedProduct.custom_attributes[url_key]$$)}}" stepKey="navigateToGroupedProductPage"/> -+ <waitForPageLoad stepKey="waitForGroupedProductPageLoad"/> -+ <fillField selector="{{StorefrontProductPageSection.qtyInput}}" userInput="1" stepKey="fillFieldQtyInput"/> -+ <actionGroup ref="addToCartFromStorefrontProductPage" stepKey="addToCartGroupedProductFromStorefrontProductPage"> -+ <argument name="productName" value="$$createGroupedProduct.name$$"/> -+ </actionGroup> -+ -+ <!-- Add Bundle Product to cart --> -+ <amOnPage url="{{StorefrontProductPage.url($$createFixedBundleProduct.custom_attributes[url_key]$$)}}" stepKey="navigateToBundleProductPage"/> -+ <waitForPageLoad stepKey="waitForFixedBundleProductPageLoad"/> -+ <click selector="{{StorefrontBundleProductActionSection.customizeAndAddToCartButton}}" stepKey="clickCustomizeAndAddToCart"/> -+ <actionGroup ref="addToCartFromStorefrontProductPage" stepKey="addToCartFixedBundleProductFromStorefrontProductPage"> -+ <argument name="productName" value="$$createFixedBundleProduct.name$$"/> -+ </actionGroup> -+ -+ <!--Go to shopping cart--> -+ <actionGroup ref="clickViewAndEditCartFromMiniCart" stepKey="goToShoppingCartFromMinicart"/> -+ <actionGroup ref="FillShippingZipForm" stepKey="fillShippingZipForm"> -+ <argument name="address" value="US_Address_CA"/> -+ </actionGroup> -+ -+ <click selector="{{CheckoutCartSummarySection.proceedToCheckout}}" stepKey="clickProceedToCheckout"/> -+ <waitForPageLoad stepKey="waitForProceedToCheckout"/> -+ <actionGroup ref="FillCustomerSignInPopupFormActionGroup" stepKey="fillCustomerSignInPopupForm"> -+ <argument name="customer" value="$$createCustomer$$"/> -+ </actionGroup> -+ -+ <amOnPage url="{{CheckoutShippingPage.url}}" stepKey="navigateToShippingPage"/> -+ <waitForPageLoad stepKey="waitForShippingPageLoad"/> -+ -+ <!-- Fill customer address data --> -+ <fillField selector="{{CheckoutShippingGuestInfoSection.company}}" userInput="{{CustomerAddressSimple.company}}" stepKey="fillCompany"/> -+ <fillField selector="{{CheckoutShippingGuestInfoSection.street}}" userInput="{{CustomerAddressSimple.street}}" stepKey="fillStreet"/> -+ <fillField selector="{{CheckoutShippingGuestInfoSection.city}}" userInput="{{CustomerAddressSimple.city}}" stepKey="fillCity" /> -+ <selectOption selector="{{CheckoutShippingGuestInfoSection.region}}" userInput="{{CustomerAddressSimple.state}}" stepKey="selectRegion"/> -+ <fillField selector="{{CheckoutShippingGuestInfoSection.postcode}}" userInput="{{CustomerAddressSimple.postcode}}" stepKey="fillZipCode" /> -+ <fillField selector="{{CheckoutShippingGuestInfoSection.telephone}}" userInput="{{CustomerAddressSimple.telephone}}" stepKey="fillPhone" /> -+ -+ <!-- Click next button to open payment section --> -+ <click selector="{{CheckoutShippingGuestInfoSection.next}}" stepKey="clickNextBtn"/> -+ <waitForPageLoad stepKey="waitForShipmentPageLoad"/> -+ -+ <!-- Check order summary in checkout --> -+ <waitForElement selector="{{CheckoutPaymentSection.paymentSectionTitle}}" stepKey="waitForPaymentSectionLoaded"/> -+ <click selector="{{CheckoutPaymentSection.placeOrder}}" stepKey="clickPlaceOrderButton"/> -+ <seeElement selector="{{CheckoutSuccessMainSection.success}}" stepKey="orderIsSuccessfullyPlaced" /> -+ <grabTextFrom selector="{{CheckoutSuccessMainSection.orderNumber22}}" stepKey="grabOrderNumber"/> -+ -+ <!-- Login as admin --> -+ <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> -+ -+ <!-- Open created order --> -+ <amOnPage url="{{AdminOrdersPage.url}}" stepKey="goToOrders"/> -+ <waitForPageLoad stepKey="waitForOrderPageLoad"/> -+ <actionGroup ref="OpenOrderById" stepKey="filterOrderGridById"> -+ <argument name="orderId" value="$grabOrderNumber"/> -+ </actionGroup> -+ -+ <!-- Assert that addresses on order page the same --> -+ <grabTextFrom selector="{{AdminShipmentAddressInformationSection.shippingAddress}}" stepKey="shippingAddressOrderPage"/> -+ <grabTextFrom selector="{{AdminShipmentAddressInformationSection.billingAddress}}" stepKey="billingAddressOrderPage"/> -+ <assertEquals actual="$billingAddressOrderPage" expected="$shippingAddressOrderPage" stepKey="assertAddressOrderPage"/> -+ -+ <!-- Assert order total --> -+ <amOnPage url="{{StorefrontCustomerDashboardPage.url}}" stepKey="navigateToCustomerDashboardPage"/> -+ <waitForPageLoad stepKey="waitForCustomerDashboardPageLoad"/> -+ <see selector="{{StorefrontCustomerRecentOrdersSection.orderTotal}}" userInput="$613.23" stepKey="checkOrderTotalInStorefront"/> -+ -+ <!-- Go to Address Book --> -+ <actionGroup ref="StorefrontCustomerGoToSidebarMenu" stepKey="goToAddressBook"> -+ <argument name="menu" value="Address Book"/> -+ </actionGroup> -+ -+ <!-- Asserts that addresses in address book equal to addresses in order --> -+ <grabTextFrom selector="{{CheckoutOrderSummarySection.shippingAddress}}" stepKey="shippingAddress"/> -+ <grabTextFrom selector="{{CheckoutOrderSummarySection.billingAddress}}" stepKey="billingAddress"/> -+ <assertEquals actual="$shippingAddress" expected="$shippingAddressOrderPage" stepKey="assertShippingAddress"/> -+ <assertEquals actual="$billingAddress" expected="$billingAddressOrderPage" stepKey="assertBillingAddress"/> -+ </test> -+</tests> -diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/ShoppingCartAndMiniShoppingCartPerCustomerTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/ShoppingCartAndMiniShoppingCartPerCustomerTest.xml -new file mode 100644 -index 00000000000..84cdb8abd93 ---- /dev/null -+++ b/app/code/Magento/Checkout/Test/Mftf/Test/ShoppingCartAndMiniShoppingCartPerCustomerTest.xml -@@ -0,0 +1,185 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> -+ <test name="ShoppingCartAndMiniShoppingCartPerCustomerTest"> -+ <annotations> -+ <features value="Checkout"/> -+ <stories value="Shopping Cart and Mini Shopping Cart per Customer"/> -+ <title value="Shopping cart and mini shopping cart per customer test"/> -+ <description value="Shopping cart and mini shopping cart per customer with enabled cached"/> -+ <severity value="CRITICAL"/> -+ <testCaseId value="MC-14730"/> -+ <group value="checkout"/> -+ </annotations> -+ <before> -+ <!-- Flush cache --> -+ <magentoCLI command="cache:flush" stepKey="clearCache"/> -+ -+ <!-- Create two customers --> -+ <createData entity="Simple_US_Customer" stepKey="createFirstCustomer"/> -+ <createData entity="Simple_US_CA_Customer" stepKey="createSecondCustomer"/> -+ -+ <!-- Create products --> -+ <createData entity="_defaultCategory" stepKey="createCategory"/> -+ -+ <!-- Create simple product --> -+ <createData entity="_defaultProduct" stepKey="createSimpleProduct"> -+ <requiredEntity createDataKey="createCategory"/> -+ </createData> -+ -+ <!-- Create simple product with custom options --> -+ <createData entity="_defaultProduct" stepKey="createSimpleProductWithCustomOptions"> -+ <requiredEntity createDataKey="createCategory"/> -+ </createData> -+ <updateData createDataKey="createSimpleProductWithCustomOptions" entity="productWithDropdownAndFieldOptions" stepKey="updateProductWithCustomOption"/> -+ </before> -+ <after> -+ <!-- Delete products --> -+ <deleteData createDataKey="createSimpleProduct" stepKey="deleteSimpleProduct"/> -+ <deleteData createDataKey="createSimpleProductWithCustomOptions" stepKey="deleteSimpleProductWithCustomOptions"/> -+ -+ <!-- Delete category --> -+ <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> -+ -+ <!-- Delete customers --> -+ <deleteData createDataKey="createFirstCustomer" stepKey="deleteFirstCustomer"/> -+ <deleteData createDataKey="createSecondCustomer" stepKey="deleteSecondCustomer"/> -+ </after> -+ -+ <!-- Login as first customer --> -+ <actionGroup ref="LoginToStorefrontActionGroup" stepKey="loginToStorefrontAccountAsFirstCustomer"> -+ <argument name="Customer" value="$$createFirstCustomer$$"/> -+ </actionGroup> -+ -+ <!-- Assert cart is empty --> -+ <actionGroup ref="AssertShoppingCartIsEmptyActionGroup" stepKey="seeEmptyShoppingCartForFirstCustomer"/> -+ -+ <!-- Go to first product page --> -+ <amOnPage url="{{StorefrontProductPage.url($$createSimpleProduct.custom_attributes[url_key]$$)}}" stepKey="goToFirstProductPage"/> -+ <waitForPageLoad stepKey="waitForFirstProductPageLoad"/> -+ -+ <!-- Add the product to the shopping cart --> -+ <actionGroup ref="addToCartFromStorefrontProductPage" stepKey="addFirstProductToCart"> -+ <argument name="productName" value="$$createSimpleProduct.name$$"/> -+ </actionGroup> -+ -+ <!-- Go to the second product page --> -+ <amOnPage url="{{StorefrontProductPage.url($$createSimpleProductWithCustomOptions.custom_attributes[url_key]$$)}}" stepKey="goToSecondProductPage"/> -+ <waitForPageLoad stepKey="waitForSecondProductPageLoad"/> -+ -+ <!-- Fill the custom options values --> -+ <actionGroup ref="StorefrontSelectOptionDropDownActionGroup" stepKey="selectFirstOption"> -+ <argument name="optionTitle" value="ProductOptionValueDropdown"/> -+ <argument name="option" value="ProductOptionValueWithSkuDropdown1.title"/> -+ </actionGroup> -+ <fillField selector="{{StorefrontProductInfoMainSection.productOptionFieldInput(ProductOptionField.title)}}" userInput="OptionField" stepKey="fillProductOptionInputField"/> -+ -+ <!-- Add the product to the shopping cart --> -+ <actionGroup ref="StorefrontAddToCartCustomOptionsProductPageActionGroup" stepKey="addSecondProductToCart"> -+ <argument name="productName" value="$$createSimpleProductWithCustomOptions.name$$"/> -+ </actionGroup> -+ -+ <!-- Logout first customer --> -+ <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="firstCustomerLogout"/> -+ -+ <!-- Login as second customer --> -+ <actionGroup ref="LoginToStorefrontActionGroup" stepKey="loginToStorefrontAccountAsSecondCustomer"> -+ <argument name="Customer" value="$$createSecondCustomer$$"/> -+ </actionGroup> -+ -+ <!-- Assert cart is empty --> -+ <actionGroup ref="AssertShoppingCartIsEmptyActionGroup" stepKey="seeEmptyShoppingCartForSecondCustomer"/> -+ -+ <!-- Go to first product page --> -+ <amOnPage url="{{StorefrontProductPage.url($$createSimpleProduct.custom_attributes[url_key]$$)}}" stepKey="goToProductPage"/> -+ <waitForPageLoad stepKey="waitForProductPage"/> -+ -+ <!-- Add the product to the shopping cart --> -+ <actionGroup ref="AddProductWithQtyToCartFromStorefrontProductPage" stepKey="addProductToCart"> -+ <argument name="productName" value="$$createSimpleProduct.name$$"/> -+ <argument name="productQty" value="{{quoteQty2Price123.qty}}"/> -+ </actionGroup> -+ -+ <!-- Logout second customer --> -+ <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="secondCustomerLogout"/> -+ -+ <!-- Login as first customer --> -+ <actionGroup ref="LoginToStorefrontActionGroup" stepKey="loginAsFirstCustomer"> -+ <argument name="Customer" value="$$createFirstCustomer$$"/> -+ </actionGroup> -+ -+ <amOnPage url="{{CheckoutCartPage.url}}" stepKey="amOnPageShoppingCart"/> -+ <waitForPageLoad stepKey="waitForCheckoutPageLoad"/> -+ -+ <!-- Assert first products present in shopping cart --> -+ <actionGroup ref="StorefrontCheckCartSimpleProductActionGroup" stepKey="checkFirstProductInCart"> -+ <argument name="product" value="$$createSimpleProduct$$"/> -+ <argument name="productQuantity" value="ApiSimpleSingleQty.quantity"/> -+ </actionGroup> -+ -+ <!-- Assert second products present in shopping cart --> -+ <seeElement selector="{{CheckoutCartProductSection.ProductLinkByName($$createSimpleProductWithCustomOptions.name$$)}}" stepKey="assertProductName"/> -+ <see selector="{{CheckoutCartProductSection.ProductPriceByName($$createSimpleProductWithCustomOptions.name$$)}}" userInput="{{quoteQty2Subtotal266.currency}}{{quoteQty2Subtotal266.price}}" stepKey="assertProductPrice"/> -+ <see selector="{{CheckoutCartProductSection.ProductOptionByNameAndAttribute($$createSimpleProductWithCustomOptions.name$$, ProductOptionField.title)}}" userInput="OptionField" stepKey="seeFieldOption"/> -+ <see selector="{{CheckoutCartProductSection.ProductOptionByNameAndAttribute($$createSimpleProductWithCustomOptions.name$$, ProductOptionValueDropdown.title)}}" userInput="{{ProductOptionValueWithSkuDropdown1.title}}" stepKey="seeDropDownOption"/> -+ -+ <!-- Assert subtotal and grand total --> -+ <see selector="{{StorefrontProductPageSection.subTotal}}" userInput="{{quoteQty2Subtotal266.currency}}{{quoteQty2Subtotal266.subtotal}}" stepKey="seeFirstCustomerSubTotal"/> -+ <see selector="{{StorefrontProductPageSection.orderTotal}}" userInput="{{quoteQty2Subtotal266.currency}}{{quoteQty2Subtotal266.total}}" stepKey="seeFirstCustomerOrderTotal"/> -+ -+ <!-- Assert products in mini cart for first customer --> -+ <amOnPage url="{{StorefrontHomePage.url}}" stepKey="goToStoreFrontHomePage"/> -+ <waitForPageLoad stepKey="waitForHomePageLoad"/> -+ <actionGroup ref="assertOneProductNameInMiniCart" stepKey="assertFirstProductInMiniCart"> -+ <argument name="productName" value="$$createSimpleProduct.name$$"/> -+ </actionGroup> -+ <actionGroup ref="assertOneProductNameInMiniCart" stepKey="assertSecondProductInMiniCart"> -+ <argument name="productName" value="$$createSimpleProductWithCustomOptions.name$$"/> -+ </actionGroup> -+ <actionGroup ref="AssertMiniShoppingCartSubTotalActionGroup" stepKey="assertMiniCartSubTotal"> -+ <argument name="dataQuote" value="quoteQty2Subtotal266"/> -+ </actionGroup> -+ -+ <!-- Logout first customer --> -+ <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="logoutFirstCustomer"/> -+ -+ <!-- Login as second customer --> -+ <actionGroup ref="LoginToStorefrontActionGroup" stepKey="loginAsSecondCustomer"> -+ <argument name="Customer" value="$$createSecondCustomer$$"/> -+ </actionGroup> -+ -+ <!-- Assert first products present in shopping cart --> -+ <amOnPage url="{{CheckoutCartPage.url}}" stepKey="amOnShoppingCartPage"/> -+ <waitForPageLoad stepKey="waitForShoppingCartPageLoad"/> -+ <actionGroup ref="StorefrontCheckCartSimpleProductActionGroup" stepKey="checkProductInCart"> -+ <argument name="product" value="$$createSimpleProduct$$"/> -+ <argument name="productQuantity" value="quoteQty2Price123.qty"/> -+ </actionGroup> -+ -+ <!-- Assert subtotal and grand total --> -+ <see selector="{{StorefrontProductPageSection.subTotal}}" userInput="{{quoteQty2Price123.currency}}{{quoteQty2Price123.subtotal}}" stepKey="seeSecondCustomerSubTotal"/> -+ <see selector="{{StorefrontProductPageSection.orderTotal}}" userInput="{{quoteQty2Price123.currency}}{{quoteQty2Price123.total}}" stepKey="seeSecondCustomerOrderTotal"/> -+ -+ <!-- Assert product in mini cart --> -+ <amOnPage url="{{StorefrontHomePage.url}}" stepKey="goToHomePage"/> -+ <waitForPageLoad stepKey="waitForHomePageToLoad"/> -+ <click selector="{{StorefrontMinicartSection.showCart}}" stepKey="clickOnMiniCart"/> -+ <waitForPageLoad stepKey="waitForPageToLoad"/> -+ <actionGroup ref="AssertStorefrontMiniCartItemsActionGroup" stepKey="assertProductInMiniCart"> -+ <argument name="productName" value="$$createSimpleProduct.name$$"/> -+ <argument name="productPrice" value="$$createSimpleProduct.price$$"/> -+ <argument name="cartSubtotal" value="{{quoteQty2Price123.currency}}{{quoteQty2Price123.subtotal}}"/> -+ <argument name="qty" value="{{quoteQty2Price123.qty}}"/> -+ </actionGroup> -+ -+ <!-- Logout second customer --> -+ <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="logoutSecondCustomer"/> -+ </test> -+</tests> -diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StoreFrontAddProductWithAllTypesOfCustomOptionToTheShoppingCartWithoutAnySelectedOptionTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StoreFrontAddProductWithAllTypesOfCustomOptionToTheShoppingCartWithoutAnySelectedOptionTest.xml -new file mode 100644 -index 00000000000..d108dc3657a ---- /dev/null -+++ b/app/code/Magento/Checkout/Test/Mftf/Test/StoreFrontAddProductWithAllTypesOfCustomOptionToTheShoppingCartWithoutAnySelectedOptionTest.xml -@@ -0,0 +1,84 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> -+ <test name="StoreFrontAddProductWithAllTypesOfCustomOptionToTheShoppingCartWithoutAnySelectedOptionTest"> -+ <annotations> -+ <stories value="Shopping Cart"/> -+ <title value="Add simple product with all types of custom options to cart without selecting any options"/> -+ <description value="Add simple product with all types of custom options to cart without selecting any options"/> -+ <testCaseId value="MC-14725"/> -+ <severity value="CRITICAL"/> -+ <group value="mtf_migrated"/> -+ </annotations> -+ -+ <before> -+ <createData entity="_defaultCategory" stepKey="createCategory"/> -+ <createData entity="_defaultProduct" stepKey="createProduct"> -+ <requiredEntity createDataKey="createCategory"/> -+ </createData> -+ <updateData createDataKey="createProduct" entity="productWithOptions" stepKey="updateProductWithCustomOptions"/> -+ <magentoCLI command="indexer:reindex" stepKey="reindex"/> -+ </before> -+ <after> -+ <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> -+ <deleteData createDataKey="createProduct" stepKey="deleteProduct"/> -+ </after> -+ -+ <!-- Open Product page in StoreFront --> -+ <actionGroup ref="AssertProductNameAndSkuInStorefrontProductPageByCustomAttributeUrlKey" stepKey="openProduct1PageAndVerifyProduct"> -+ <argument name="product" value="$$createProduct$$"/> -+ </actionGroup> -+ -+ <!--Click on Add To Cart button--> -+ <click selector="{{StorefrontProductActionSection.addToCart}}" stepKey="addToCart"/> -+ <waitForPageLoad stepKey="waitForPageToLoad"/> -+ -+ <!--Assert all types of product options field displayed Required message --> -+ <actionGroup ref="AssertStorefrontSeeElementActionGroup" stepKey="assertRequiredProductOptionField"> -+ <argument name="selector" value="{{StorefrontProductInfoMainSection.requiredCustomField(ProductOptionField.title)}}"/> -+ </actionGroup> -+ -+ <actionGroup ref="AssertStorefrontSeeElementActionGroup" stepKey="assertRequiredProductOptionDropDown"> -+ <argument name="selector" value="{{StorefrontProductInfoMainSection.requiredCustomField(ProductOptionDropDown.title)}}"/> -+ </actionGroup> -+ -+ <actionGroup ref="AssertStorefrontSeeElementActionGroup" stepKey="assertRequiredProductOptionRadioButton"> -+ <argument name="selector" value="{{StorefrontProductInfoMainSection.requiredCustomField(ProductOptionRadiobutton.title)}}"/> -+ </actionGroup> -+ -+ <actionGroup ref="AssertStorefrontSeeElementActionGroup" stepKey="assertRequiredProductOptionCheckBox"> -+ <argument name="selector" value="{{StorefrontProductInfoMainSection.requiredCustomField(ProductOptionCheckbox.title)}}"/> -+ </actionGroup> -+ -+ <actionGroup ref="AssertStorefrontSeeElementActionGroup" stepKey="assertRequiredProductOptionMultiSelect"> -+ <argument name="selector" value="{{StorefrontProductInfoMainSection.requiredCustomField(ProductOptionMultiSelect.title)}}"/> -+ </actionGroup> -+ -+ <actionGroup ref="AssertStorefrontSeeElementActionGroup" stepKey="assertRequiredProductFileOption"> -+ <argument name="selector" value="{{StorefrontProductInfoMainSection.requiredCustomFile(ProductOptionFile.title)}}"/> -+ </actionGroup> -+ -+ <actionGroup ref="AssertStorefrontSeeElementActionGroup" stepKey="assertRequiredProductDateOption"> -+ <argument name="selector" value="{{StorefrontProductInfoMainSection.requiredCustomDate(ProductOptionDate.title)}}"/> -+ </actionGroup> -+ -+ <actionGroup ref="AssertStorefrontSeeElementActionGroup" stepKey="assertRequiredProductDateAndTimeOption"> -+ <argument name="selector" value="{{StorefrontProductInfoMainSection.requiredCustomDate(ProductOptionDateTime.title)}}"/> -+ </actionGroup> -+ -+ <actionGroup ref="AssertStorefrontSeeElementActionGroup" stepKey="assertRequiredProducTimeOption"> -+ <argument name="selector" value="{{StorefrontProductInfoMainSection.requiredCustomDate(ProductOptionTime.title)}}"/> -+ </actionGroup> -+ -+ <!-- Verify mini cart is empty --> -+ <actionGroup ref="AssertStorefrontSeeElementActionGroup" stepKey="assertEmptryCart"> -+ <argument name="selector" value="{{StorefrontMiniCartSection.emptyMiniCart}}"/> -+ </actionGroup> -+ </test> -+</tests> -diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StoreFrontCheckCustomerInfoCreatedByGuestTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StoreFrontCheckCustomerInfoCreatedByGuestTest.xml -new file mode 100644 -index 00000000000..693c05684f2 ---- /dev/null -+++ b/app/code/Magento/Checkout/Test/Mftf/Test/StoreFrontCheckCustomerInfoCreatedByGuestTest.xml -@@ -0,0 +1,62 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> -+ <test name="StoreFrontCheckCustomerInfoCreatedByGuestTest"> -+ <annotations> -+ <features value="Checkout"/> -+ <stories value="Check customer information created by guest"/> -+ <title value="Check Customer Information Created By Guest"/> -+ <description value="Check customer information after placing the order as the guest who created an account"/> -+ <severity value="MAJOR"/> -+ <testCaseId value="MAGETWO-95932"/> -+ <useCaseId value="MAGETWO-95820"/> -+ <group value="checkout"/> -+ </annotations> -+ -+ <before> -+ <createData entity="_defaultCategory" stepKey="category"/> -+ <createData entity="_defaultProduct" stepKey="product"> -+ <requiredEntity createDataKey="category"/> -+ </createData> -+ <magentoCLI command="cache:flush" stepKey="flushCache"/> -+ </before> -+ -+ <after> -+ <deleteData createDataKey="product" stepKey="deleteProduct" /> -+ <deleteData createDataKey="category" stepKey="deleteCategory" /> -+ <actionGroup ref="logout" stepKey="logoutFromAdmin"/> -+ </after> -+ -+ <amOnPage url="$$product.name$$.html" stepKey="navigateToProductPage"/> -+ <waitForPageLoad stepKey="waitForProductPage"/> -+ <actionGroup ref="addToCartFromStorefrontProductPage" stepKey="addToCartFromStorefrontProductPage"> -+ <argument name="productName" value="$$product.name$$"/> -+ </actionGroup> -+ <actionGroup ref="GoToCheckoutFromMinicartActionGroup" stepKey="goToCheckoutFromMinicart"/> -+ <actionGroup ref="GuestCheckoutFillingShippingSectionActionGroup" stepKey="guestCheckoutFillingShippingSection"> -+ <argument name="customerVar" value="CustomerEntityOne"/> -+ <argument name="customerAddressVar" value="CustomerAddressSimple"/> -+ </actionGroup> -+ <actionGroup ref="CheckoutPlaceOrderActionGroup" stepKey="placeOrder"> -+ <argument name="orderNumberMessage" value="CONST.successGuestCheckoutOrderNumberMessage"/> -+ <argument name="emailYouMessage" value="CONST.successCheckoutEmailYouMessage" /> -+ </actionGroup> -+ <grabTextFrom selector="{{CheckoutSuccessRegisterSection.orderNumber}}" stepKey="grabOrderNumber"/> -+ <click selector="{{CheckoutSuccessRegisterSection.createAccountButton}}" stepKey="clickCreateAccountButton"/> -+ <fillField selector="{{StorefrontCustomerCreateFormSection.passwordField}}" userInput="{{CustomerEntityOne.password}}" stepKey="TypePassword"/> -+ <fillField selector="{{StorefrontCustomerCreateFormSection.confirmPasswordField}}" userInput="{{CustomerEntityOne.password}}" stepKey="TypeConfirmationPassword"/> -+ <click selector="{{StorefrontCustomerCreateFormSection.createAccountButton}}" stepKey="clickOnCreateAccount"/> -+ <see userInput="Thank you for registering" stepKey="verifyAccountCreated"/> -+ <actionGroup ref="LoginAsAdmin" stepKey="loginToAdmin"/> -+ <amOnPage url="{{AdminOrderPage.url({$grabOrderNumber})}}" stepKey="navigateToOrderPage"/> -+ <waitForPageLoad stepKey="waitForCreatedOrderPage"/> -+ <see stepKey="seeCustomerName" userInput="{{CustomerEntityOne.firstname}}" selector="{{AdminShipmentOrderInformationSection.customerName}}"/> -+ </test> -+</tests> -diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StoreFrontFreeShippingRecalculationAfterCouponCodeAddedTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StoreFrontFreeShippingRecalculationAfterCouponCodeAddedTest.xml -new file mode 100644 -index 00000000000..330a026bb94 ---- /dev/null -+++ b/app/code/Magento/Checkout/Test/Mftf/Test/StoreFrontFreeShippingRecalculationAfterCouponCodeAddedTest.xml -@@ -0,0 +1,97 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> -+ <test name="StoreFrontFreeShippingRecalculationAfterCouponCodeAddedTest"> -+ <annotations> -+ <title value="Checkout Free Shipping Recalculation after Coupon Code Added"/> -+ <stories value="Checkout Free Shipping Recalculation after Coupon Code Added"/> -+ <description value="User should be able to do checkout free shipping recalculation after adding coupon code"/> -+ <features value="Checkout"/> -+ <severity value="MAJOR"/> -+ <testCaseId value="MAGETWO-96537"/> -+ <useCaseId value="MAGETWO-96431"/> -+ <group value="Checkout"/> -+ </annotations> -+ -+ <before> -+ <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> -+ <createData entity="Simple_US_Customer" stepKey="createSimpleUsCustomer"> -+ <field key="group_id">1</field> -+ </createData> -+ <createData entity="_defaultCategory" stepKey="defaultCategory"/> -+ <createData entity="_defaultProduct" stepKey="simpleProduct"> -+ <field key="price">90</field> -+ <requiredEntity createDataKey="defaultCategory"/> -+ </createData> -+ <!--It is default for FlatRate--> -+ <createData entity="FlatRateShippingMethodConfig" stepKey="enableFlatRate"/> -+ <createData entity="FreeShippingMethodsSettingConfig" stepKey="freeShippingMethodsSettingConfig"/> -+ <createData entity="MinimumOrderAmount90" stepKey="minimumOrderAmount90"/> -+ <magentoCLI command="cache:flush" stepKey="flushCache1"/> -+ <actionGroup ref="AdminCreateCartPriceRuleWithCouponCode" stepKey="createCartPriceRule"> -+ <argument name="ruleName" value="CatPriceRule"/> -+ <argument name="couponCode" value="CatPriceRule.coupon_code"/> -+ </actionGroup> -+ <actionGroup ref="LoginToStorefrontActionGroup" stepKey="loginToStoreFront"> -+ <argument name="Customer" value="$$createSimpleUsCustomer$$"/> -+ </actionGroup> -+ <amOnPage url="$$simpleProduct.name$$.html" stepKey="navigateToProductPage"/> -+ <waitForPageLoad stepKey="waitForProductPage"/> -+ </before> -+ <after> -+ <deleteData createDataKey="simpleProduct" stepKey="deleteProduct"/> -+ <deleteData createDataKey="defaultCategory" stepKey="deleteCategory"/> -+ <createData entity="DefaultShippingMethodsConfig" stepKey="defaultShippingMethodsConfig"/> -+ <createData entity="DefaultMinimumOrderAmount" stepKey="defaultMinimumOrderAmount"/> -+ <deleteData createDataKey="createSimpleUsCustomer" stepKey="deleteCustomer"/> -+ <magentoCLI command="cache:flush" stepKey="flushCache2"/> -+ <actionGroup ref="DeleteCartPriceRuleByName" stepKey="deleteCartPriceRule"> -+ <argument name="ruleName" value="{{CatPriceRule.name}}"/> -+ </actionGroup> -+ <actionGroup ref="logout" stepKey="logout"/> -+ </after> -+ -+ <actionGroup ref="ApplyCartRuleOnStorefrontActionGroup" stepKey="applyCartRule"> -+ <argument name="product" value="$$simpleProduct$$"/> -+ <argument name="couponCode" value="{{CatPriceRule.coupon_code}}"/> -+ </actionGroup> -+ <actionGroup ref="GoToCheckoutFromMinicartActionGroup" stepKey="goToCheckoutFromMinicart1"/> -+ <waitForPageLoad stepKey="waitForpageLoad1"/> -+ <dontSee selector="{{CheckoutShippingMethodsSection.shippingMethodRowByName('Free')}}" stepKey="dontSeeFreeShipping"/> -+ <amOnPage url="{{CheckoutCartPage.url}}" stepKey="goToShoppingCartPage"/> -+ <waitForPageLoad stepKey="waitForShoppingCartPage"/> -+ <conditionalClick selector="{{DiscountSection.DiscountTab}}" dependentSelector="{{DiscountSection.CouponInput}}" visible="false" stepKey="clickIfDiscountTabClosed1"/> -+ <waitForPageLoad stepKey="waitForCouponTabOpen1"/> -+ <click selector="{{DiscountSection.CancelCoupon}}" stepKey="cancelCoupon"/> -+ <waitForPageLoad stepKey="waitForCancel"/> -+ <see userInput='You canceled the coupon code.' stepKey="seeCancellationMessage"/> -+ <actionGroup ref="GoToCheckoutFromMinicartActionGroup" stepKey="goToCheckoutFromMinicart2"/> -+ <waitForPageLoad stepKey="waitForShippingMethods"/> -+ <click stepKey="chooseFreeShipping" selector="{{CheckoutShippingMethodsSection.shippingMethodRowByName('Free')}}"/> -+ <click selector="{{CheckoutShippingMethodsSection.next}}" stepKey="clickNext1"/> -+ <waitForPageLoad stepKey="waitForReviewAndPayments1"/> -+ <conditionalClick selector="{{DiscountSection.DiscountTab}}" dependentSelector="{{DiscountSection.CouponInput}}" visible="false" stepKey="clickIfDiscountTabClosed2"/> -+ <waitForPageLoad stepKey="waitForCouponTabOpen2"/> -+ <fillField selector="{{DiscountSection.DiscountInput}}" userInput="{{CatPriceRule.coupon_code}}" stepKey="fillCouponCode"/> -+ <click selector="{{DiscountSection.ApplyCodeBtn}}" stepKey="applyCode"/> -+ <waitForPageLoad stepKey="waitForPageLoad"/> -+ <see userInput="Your coupon was successfully applied." stepKey="seeSuccessMessage"/> -+ <click selector="{{CheckoutPaymentSection.placeOrder}}" stepKey="clickPlaceOrder1"/> -+ <waitForPageLoad stepKey="waitForError"/> -+ <see stepKey="seeShippingMethodError" userInput="The shipping method is missing. Select the shipping method and try again."/> -+ <amOnPage stepKey="navigateToShippingPage" url="{{CheckoutShippingPage.url}}"/> -+ <waitForPageLoad stepKey="waitForShippingPageLoad"/> -+ <click stepKey="chooseFlatRateShipping" selector="{{CheckoutShippingMethodsSection.shippingMethodRowByName('Flat Rate')}}"/> -+ <click selector="{{CheckoutShippingMethodsSection.next}}" stepKey="clickNext2"/> -+ <waitForPageLoad stepKey="waitForReviewAndPayments2"/> -+ <click selector="{{CheckoutPaymentSection.placeOrder}}" stepKey="clickPlaceOrder2"/> -+ <waitForPageLoad stepKey="waitForSuccessfullyPlacedOrder"/> -+ <see stepKey="seeSuccessMessageForPlacedOrder" userInput="Thank you for your purchase!"/> -+ </test> -+</tests> -diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StoreFrontUpdateShoppingCartWhileUpdateMinicartTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StoreFrontUpdateShoppingCartWhileUpdateMinicartTest.xml -new file mode 100644 -index 00000000000..fb80b4880a6 ---- /dev/null -+++ b/app/code/Magento/Checkout/Test/Mftf/Test/StoreFrontUpdateShoppingCartWhileUpdateMinicartTest.xml -@@ -0,0 +1,56 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> -+ <test name="StoreFrontUpdateShoppingCartWhileUpdateMinicartTest"> -+ <annotations> -+ <stories value="Shopping Cart"/> -+ <title value="Check updating shopping cart while updating items from minicart"/> -+ <description value="Check updating shopping cart while updating items from minicart"/> -+ <severity value="AVERAGE"/> -+ <testCaseId value="MAGETWO-97280"/> -+ <useCaseId value="MAGETWO-71344"/> -+ <group value="checkout"/> -+ </annotations> -+ -+ <before> -+ <!--Create product--> -+ <createData entity="SimpleProduct2" stepKey="createProduct"/> -+ </before> -+ -+ <after> -+ <!--Delete created data--> -+ <deleteData createDataKey="createProduct" stepKey="deleteProduct" /> -+ </after> -+ -+ <!--Add product to cart--> -+ <amOnPage url="$$createProduct.name$$.html" stepKey="navigateToProductPage"/> -+ <waitForPageLoad stepKey="waitForProductPage"/> -+ <actionGroup ref="addToCartFromStorefrontProductPage" stepKey="addToCartFromStorefrontProductPage"> -+ <argument name="productName" value="$$createProduct.name$$"/> -+ </actionGroup> -+ -+ <!--Go to Shopping cart and check Qty--> -+ <actionGroup ref="clickViewAndEditCartFromMiniCart" stepKey="goToShoppingCart"/> -+ <grabValueFrom selector="{{CheckoutCartProductSection.ProductQuantityByName($$createProduct.name$$)}}" stepKey="grabQtyShoppingCart"/> -+ <assertEquals expected="1" actual="$grabQtyShoppingCart" stepKey="assertQtyShoppingCart"/> -+ -+ <!--Open minicart and change Qty--> -+ <click selector="{{StorefrontMinicartSection.showCart}}" stepKey="openMiniCart"/> -+ <waitForElementVisible selector="{{StorefrontMinicartSection.quantity}}" stepKey="waitForElementQty"/> -+ <pressKey selector="{{StorefrontMinicartSection.itemQuantity($$createProduct.name$$)}}" parameterArray="[\Facebook\WebDriver\WebDriverKeys::BACKSPACE]" stepKey="deleteFiled"/> -+ <fillField selector="{{StorefrontMinicartSection.itemQuantity($$createProduct.name$$)}}" userInput="5" stepKey="changeQty"/> -+ <click selector="{{StorefrontMinicartSection.itemQuantityUpdate($$createProduct.name$$)}}" stepKey="updateQty"/> -+ <waitForAjaxLoad stepKey="waitForAjaxLoad"/> -+ -+ <!--Check Qty in shopping cart after updating--> -+ <grabValueFrom selector="{{CheckoutCartProductSection.ProductQuantityByName($$createProduct.name$$)}}" stepKey="grabQtyShoppingCart1"/> -+ <assertEquals expected="5" actual="$grabQtyShoppingCart1" stepKey="assertQtyShoppingCart1"/> -+ </test> -+</tests> -diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontAddBundleDynamicProductToShoppingCartTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontAddBundleDynamicProductToShoppingCartTest.xml -new file mode 100644 -index 00000000000..f63cf38d15f ---- /dev/null -+++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontAddBundleDynamicProductToShoppingCartTest.xml -@@ -0,0 +1,126 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> -+ <test name="StorefrontAddBundleDynamicProductToShoppingCartTest"> -+ <annotations> -+ <stories value="Shopping Cart"/> -+ <title value="Add bundle dynamic product to the cart"/> -+ <description value="Add bundle dynamic product to the cart"/> -+ <testCaseId value="MC-14715"/> -+ <severity value="CRITICAL"/> -+ <group value="mtf_migrated"/> -+ <skip> -+ <issueId value="MC-16684"/> -+ </skip> -+ </annotations> -+ -+ <before> -+ <magentoCLI command="config:set {{EnableFlatRateConfigData.path}} {{EnableFlatRateConfigData.value}}" stepKey="enableFlatRate"/> -+ <magentoCLI command="config:set {{EnableFlatRateDefaultPriceConfigData.path}} {{EnableFlatRateDefaultPriceConfigData.value}}" stepKey="enableFlatRateDefaultPrice"/> -+ <createData entity="SimpleSubCategory" stepKey="createSubCategory"/> -+ <!--Create simple product--> -+ <createData entity="SimpleProduct2" stepKey="simpleProduct1"> -+ <field key="price">10.00</field> -+ </createData> -+ <createData entity="SimpleProduct2" stepKey="simpleProduct2"> -+ <field key="price">50.00</field> -+ </createData> -+ <!--Create Bundle product--> -+ <createData entity="BundleProductPriceViewRange" stepKey="createBundleProduct"> -+ <requiredEntity createDataKey="createSubCategory"/> -+ </createData> -+ <createData entity="DropDownBundleOption" stepKey="createBundleOption1_1"> -+ <requiredEntity createDataKey="createBundleProduct"/> -+ <field key="required">True</field> -+ </createData> -+ <createData entity="ApiBundleLink" stepKey="linkOptionToProduct"> -+ <requiredEntity createDataKey="createBundleProduct"/> -+ <requiredEntity createDataKey="createBundleOption1_1"/> -+ <requiredEntity createDataKey="simpleProduct1"/> -+ </createData> -+ <createData entity="ApiBundleLink" stepKey="linkOptionToProduct2"> -+ <requiredEntity createDataKey="createBundleProduct"/> -+ <requiredEntity createDataKey="createBundleOption1_1"/> -+ <requiredEntity createDataKey="simpleProduct2"/> -+ </createData> -+ <magentoCLI command="indexer:reindex" stepKey="reindex"/> -+ <magentoCLI command="cache:flush" stepKey="flushCache"/> -+ </before> -+ <after> -+ <deleteData createDataKey="simpleProduct1" stepKey="deleteProduct1"/> -+ <deleteData createDataKey="simpleProduct2" stepKey="deleteProduct2"/> -+ <deleteData createDataKey="createBundleProduct" stepKey="deleteBundleProduct"/> -+ <deleteData createDataKey="createSubCategory" stepKey="deleteCategory"/> -+ </after> -+ -+ <!--Open Product page in StoreFront --> -+ <actionGroup ref="AssertProductNameAndSkuInStorefrontProductPageByCustomAttributeUrlKey" stepKey="openProductPageAndVerifyProduct"> -+ <argument name="product" value="$$createBundleProduct$$"/> -+ </actionGroup> -+ -+ <!--Assert Product Price Range --> -+ <actionGroup ref="AssertStorefrontElementVisibleActionGroup" stepKey="seePriceRangeIsVisible"> -+ <argument name="selector" value="{{StorefrontProductInfoMainSection.priceFrom}}"/> -+ <argument name="userInput" value="From $10.00"/> -+ </actionGroup> -+ <actionGroup ref="AssertStorefrontElementVisibleActionGroup" stepKey="seePriceRangeIsVisible1"> -+ <argument name="selector" value="{{StorefrontProductInfoMainSection.priceTo}}"/> -+ <argument name="userInput" value="To $50.00"/> -+ </actionGroup> -+ -+ <!-- Click on customize And Add To Cart Button --> -+ <actionGroup ref="StorefrontSelectCustomizeAndAddToTheCartButtonActionGroup" stepKey="clickOnCustomizeAndAddtoCartButton"/> -+ -+ <!-- Select Product, Quantity and add to the cart --> -+ <click selector="{{StorefrontBundleProductActionSection.dropdownSelectOption}}" stepKey="clickOnSelectOption"/> -+ <click selector="{{StorefrontBundleProductActionSection.dropdownProductSelection($$simpleProduct2.name$$)}}" stepKey="selectProduct"/> -+ <actionGroup ref="StorefrontEnterProductQuantityAndAddToTheCartActionGroup" stepKey="enterProductQuantityAndAddToTheCart"> -+ <argument name="quantity" value="2"/> -+ </actionGroup> -+ -+ <!-- Select Mini Cart and select 'View And Edit Cart' --> -+ <actionGroup ref="clickViewAndEditCartFromMiniCart" stepKey="selectViewAndEditCart"/> -+ -+ <!--Assert Shopping Cart Summary--> -+ <actionGroup ref="AssertStorefrontShoppingCartSummaryWithShippingActionGroup" stepKey="AssertCartSummary" > -+ <argument name="subtotal" value="$100.00"/> -+ <argument name="shipping" value="10.00"/> -+ <argument name="total" value="110.00"/> -+ </actionGroup> -+ -+ <!--Assert Product items in cart --> -+ <actionGroup ref="AssertStorefrontCheckoutCartItemsActionGroup" stepKey="assertSimpleProduct1ItemsInCheckOutCart"> -+ <argument name="productName" value="$$createBundleProduct.name$$"/> -+ <argument name="productSku" value="$$createBundleProduct.sku$$"/> -+ <argument name="productPrice" value="$50.00"/> -+ <argument name="subtotal" value="$100.00" /> -+ <argument name="qty" value="2"/> -+ </actionGroup> -+ -+ <!-- Assert Product Option In CheckOut Cart --> -+ <actionGroup ref="AssertStorefrontElementVisibleActionGroup" stepKey="seeProductOptionTitleInCart"> -+ <argument name="selector" value="{{CheckoutCartProductSection.productOptionLabel}}"/> -+ <argument name="userInput" value="$$createBundleOption1_1.title$$"/> -+ </actionGroup> -+ <actionGroup ref="AssertStorefrontElementVisibleActionGroup" stepKey="seeProductOptionInCart"> -+ <argument name="selector" value="{{CheckoutCartProductSection.productOptionLabel}}"/> -+ <argument name="userInput" value="1 x $$simpleProduct2.name$$ $50.00"/> -+ </actionGroup> -+ -+ <!-- Assert Product in Mini Cart --> -+ <click selector="{{StorefrontMinicartSection.showCart}}" stepKey="clickOnMiniCart"/> -+ <waitForPageLoad stepKey="waitForPageToLoad1"/> -+ <actionGroup ref="AssertStorefrontMiniCartItemsActionGroup" stepKey="assertSimpleProduct3MiniCart"> -+ <argument name="productName" value="$$createBundleProduct.name$$"/> -+ <argument name="productPrice" value="$50.00"/> -+ <argument name="cartSubtotal" value="$100.00" /> -+ <argument name="qty" value="2"/> -+ </actionGroup> -+ </test> -+</tests> -diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontAddBundleDynamicProductToShoppingCartWithDisableMiniCartSidebarTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontAddBundleDynamicProductToShoppingCartWithDisableMiniCartSidebarTest.xml -new file mode 100644 -index 00000000000..dcffd5cee24 ---- /dev/null -+++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontAddBundleDynamicProductToShoppingCartWithDisableMiniCartSidebarTest.xml -@@ -0,0 +1,131 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> -+ <test name="StorefrontAddBundleDynamicProductToShoppingCartWithDisableMiniCartSidebarTest"> -+ <annotations> -+ <stories value="Shopping Cart"/> -+ <title value="Create Grouped product and verify mini cart action with disabled and enable Mini Cart Sidebar"/> -+ <description value="Create Grouped product and verify mini cart action with disabled and enable Mini Cart Sidebar"/> -+ <testCaseId value="MC-14719"/> -+ <severity value="CRITICAL"/> -+ <group value="mtf_migrated"/> -+ <skip> -+ <issueId value="MC-16684"/> -+ </skip> -+ </annotations> -+ -+ <before> -+ <magentoCLI stepKey="disableShoppingCartSidebar" command="config:set checkout/sidebar/display 0"/> -+ <magentoCLI command="config:set {{EnableFlatRateConfigData.path}} {{EnableFlatRateConfigData.value}}" stepKey="enableFlatRate"/> -+ <magentoCLI command="config:set {{EnableFlatRateDefaultPriceConfigData.path}} {{EnableFlatRateDefaultPriceConfigData.value}}" stepKey="enableFlatRatePrice"/> -+ <createData entity="SimpleSubCategory" stepKey="createSubCategory"/> -+ -+ <!--Create simple products--> -+ <createData entity="SimpleProduct2" stepKey="simpleProduct1"> -+ <field key="price">10.00</field> -+ </createData> -+ <createData entity="SimpleProduct2" stepKey="simpleProduct2"> -+ <field key="price">50.00</field> -+ </createData> -+ -+ <!--Create Bundle product--> -+ <createData entity="BundleProductPriceViewRange" stepKey="createBundleProduct"> -+ <requiredEntity createDataKey="createSubCategory"/> -+ </createData> -+ <createData entity="DropDownBundleOption" stepKey="createBundleOption1_1"> -+ <requiredEntity createDataKey="createBundleProduct"/> -+ <field key="required">True</field> -+ </createData> -+ <createData entity="ApiBundleLink" stepKey="linkOptionToProduct"> -+ <requiredEntity createDataKey="createBundleProduct"/> -+ <requiredEntity createDataKey="createBundleOption1_1"/> -+ <requiredEntity createDataKey="simpleProduct1"/> -+ </createData> -+ <createData entity="ApiBundleLink" stepKey="linkOptionToProduct2"> -+ <requiredEntity createDataKey="createBundleProduct"/> -+ <requiredEntity createDataKey="createBundleOption1_1"/> -+ <requiredEntity createDataKey="simpleProduct2"/> -+ </createData> -+ <magentoCLI command="indexer:reindex" stepKey="reindex"/> -+ <magentoCLI command="cache:flush" stepKey="flushCache"/> -+ </before> -+ <after> -+ <deleteData createDataKey="simpleProduct1" stepKey="deleteProduct1"/> -+ <deleteData createDataKey="simpleProduct2" stepKey="deleteProduct2"/> -+ <deleteData createDataKey="createBundleProduct" stepKey="deleteBundleProduct"/> -+ <deleteData createDataKey="createSubCategory" stepKey="deleteCategory"/> -+ <magentoCLI stepKey="disableShoppingCartSidebar" command="config:set checkout/sidebar/display 1"/> -+ </after> -+ -+ <!--Open Product page in StoreFront and assert product and price range --> -+ <actionGroup ref="AssertProductNameAndSkuInStorefrontProductPageByCustomAttributeUrlKey" stepKey="openProductPageAndVerifyProduct"> -+ <argument name="product" value="$$createBundleProduct$$"/> -+ </actionGroup> -+ <actionGroup ref="AssertStorefrontElementVisibleActionGroup" stepKey="seePriceRangeIsVisible"> -+ <argument name="selector" value="{{StorefrontProductInfoMainSection.priceFrom}}"/> -+ <argument name="userInput" value="From $10.00"/> -+ </actionGroup> -+ <actionGroup ref="AssertStorefrontElementVisibleActionGroup" stepKey="seePriceRangeIsVisible1"> -+ <argument name="selector" value="{{StorefrontProductInfoMainSection.priceTo}}"/> -+ <argument name="userInput" value="To $50.00"/> -+ </actionGroup> -+ -+ <!-- Add Bundle dynamic product to the cart --> -+ <actionGroup ref="StorefrontAddBundleProductToTheCartActionGroup" stepKey="addBundleDynamicProductToTheCart"> -+ <argument name="productName" value="$$simpleProduct2.name$$"/> -+ <argument name="quantity" value="2"/> -+ </actionGroup> -+ -+ <!-- Select Mini Cart, verify it doen't open --> -+ <actionGroup ref="StorefrontClickOnMiniCartActionGroup" stepKey="clickOnMiniCart"/> -+ <dontSeeElement selector="{{StorefrontMinicartSection.viewAndEditCart}}" stepKey="dontSeeViewAndEditCart"/> -+ -+ <!--Assert Product items in cart --> -+ <actionGroup ref="AssertStorefrontCheckoutCartItemsActionGroup" stepKey="assertSimpleProduct1ItemsInCheckOutCart"> -+ <argument name="productName" value="$$createBundleProduct.name$$"/> -+ <argument name="productSku" value="$$createBundleProduct.sku$$"/> -+ <argument name="productPrice" value="$50.00"/> -+ <argument name="subtotal" value="$100.00" /> -+ <argument name="qty" value="2"/> -+ </actionGroup> -+ -+ <!-- Assert Grouped product with option is displayed in cart --> -+ <actionGroup ref="AssertStorefrontElementVisibleActionGroup" stepKey="seeProductOptionTitle"> -+ <argument name="selector" value="{{CheckoutCartProductSection.productOptionLabel}}"/> -+ <argument name="userInput" value="$$createBundleOption1_1.title$$"/> -+ </actionGroup> -+ <actionGroup ref="AssertStorefrontElementVisibleActionGroup" stepKey="seeProductOptionInCart"> -+ <argument name="selector" value="{{CheckoutCartProductSection.productOptionLabel}}"/> -+ <argument name="userInput" value="1 x $$simpleProduct2.name$$ $50.00"/> -+ </actionGroup> -+ -+ <!--Assert Shopping Cart Summary--> -+ <actionGroup ref="AssertStorefrontShoppingCartSummaryWithShippingActionGroup" stepKey="AssertCartSummary" > -+ <argument name="subtotal" value="$100.00"/> -+ <argument name="shipping" value="10.00"/> -+ <argument name="total" value="110.00"/> -+ </actionGroup> -+ -+ <!--Enabled Shopping Cart Sidebar --> -+ <magentoCLI stepKey="enableShoppingCartSidebar" command="config:set checkout/sidebar/display 1"/> -+ <magentoCLI command="cache:flush" stepKey="flushCache"/> -+ <reloadPage stepKey="reloadThePage"/> -+ -+ <!--Click on mini cart--> -+ <actionGroup ref="StorefrontClickOnMiniCartActionGroup" stepKey="clickOnMiniCart1"/> -+ -+ <!--Assert mini cart can open and check mini cart items --> -+ <actionGroup ref="AssertStorefrontMiniCartItemsActionGroup" stepKey="assertSimpleProductInMiniCart1"> -+ <argument name="productName" value="$$createBundleProduct.name$$"/> -+ <argument name="productPrice" value="$50.00"/> -+ <argument name="cartSubtotal" value="$100.00" /> -+ <argument name="qty" value="2"/> -+ </actionGroup> -+ </test> -+</tests> -diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontAddConfigurableProductToShoppingCartTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontAddConfigurableProductToShoppingCartTest.xml -new file mode 100644 -index 00000000000..90d7643a126 ---- /dev/null -+++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontAddConfigurableProductToShoppingCartTest.xml -@@ -0,0 +1,166 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> -+ <test name="StorefrontAddConfigurableProductToShoppingCartTest"> -+ <annotations> -+ <stories value="Shopping Cart"/> -+ <title value="Create configurable product with three options and add configurable product to the cart"/> -+ <description value="Create configurable product with three options and add configurable product to the cart"/> -+ <testCaseId value="MC-14716"/> -+ <severity value="CRITICAL"/> -+ <group value="mtf_migrated"/> -+ <skip> -+ <issueId value="MC-16684"/> -+ </skip> -+ </annotations> -+ -+ <before> -+ <magentoCLI command="config:set {{EnableFlatRateConfigData.path}} {{EnableFlatRateConfigData.value}}" stepKey="enableFlatRate"/> -+ <magentoCLI command="config:set {{EnableFlatRateDefaultPriceConfigData.path}} {{EnableFlatRateDefaultPriceConfigData.value}}" stepKey="enableFlatRatePrice"/> -+ <!-- Create Default Category --> -+ <createData entity="_defaultCategory" stepKey="createCategory"/> -+ -+ <!-- Create an attribute with three options to be used in the first child product --> -+ <createData entity="productAttributeWithTwoOptions" stepKey="createConfigProductAttribute"/> -+ <createData entity="productAttributeOption1" stepKey="createConfigProductAttributeOption1"> -+ <requiredEntity createDataKey="createConfigProductAttribute"/> -+ </createData> -+ <createData entity="productAttributeOption2" stepKey="createConfigProductAttributeOption2"> -+ <requiredEntity createDataKey="createConfigProductAttribute"/> -+ </createData> -+ <createData entity="productAttributeOption3" stepKey="createConfigProductAttributeOption3"> -+ <requiredEntity createDataKey="createConfigProductAttribute"/> -+ </createData> -+ -+ <!-- Add the attribute just created to default attribute set --> -+ <createData entity="AddToDefaultSet" stepKey="createConfigAddToAttributeSet"> -+ <requiredEntity createDataKey="createConfigProductAttribute"/> -+ </createData> -+ -+ <!-- Get the first option of the attribute created --> -+ <getData entity="ProductAttributeOptionGetter" index="1" stepKey="getConfigAttributeOption1"> -+ <requiredEntity createDataKey="createConfigProductAttribute"/> -+ </getData> -+ -+ <!-- Get the second option of the attribute created --> -+ <getData entity="ProductAttributeOptionGetter" index="2" stepKey="getConfigAttributeOption2"> -+ <requiredEntity createDataKey="createConfigProductAttribute"/> -+ </getData> -+ -+ <!-- Get the third option of the attribute created --> -+ <getData entity="ProductAttributeOptionGetter" index="3" stepKey="getConfigAttributeOption3"> -+ <requiredEntity createDataKey="createConfigProductAttribute"/> -+ </getData> -+ -+ <!-- Create Configurable product --> -+ <createData entity="BaseConfigurableProduct" stepKey="createConfigProduct"> -+ <requiredEntity createDataKey="createCategory"/> -+ </createData> -+ -+ <!-- Create a simple product and give it the attribute with the first option --> -+ <createData entity="ApiSimpleOne" stepKey="createConfigChildProduct1"> -+ <requiredEntity createDataKey="createConfigProductAttribute"/> -+ <requiredEntity createDataKey="getConfigAttributeOption1"/> -+ <field key="price">10.00</field> -+ </createData> -+ -+ <!--Create a simple product and give it the attribute with the second option --> -+ <createData entity="ApiSimpleTwo" stepKey="createConfigChildProduct2"> -+ <requiredEntity createDataKey="createConfigProductAttribute"/> -+ <requiredEntity createDataKey="getConfigAttributeOption2"/> -+ <field key="price">20.00</field> -+ </createData> -+ -+ <!--Create a simple product and give it the attribute with the Third option --> -+ <createData entity="ApiSimpleTwo" stepKey="createConfigChildProduct3"> -+ <requiredEntity createDataKey="createConfigProductAttribute"/> -+ <requiredEntity createDataKey="getConfigAttributeOption3"/> -+ <field key="price">30.00</field> -+ </createData> -+ -+ <!-- Create the configurable product --> -+ <createData entity="ConfigurableProductThreeOptions" stepKey="createConfigProductOption"> -+ <requiredEntity createDataKey="createConfigProduct"/> -+ <requiredEntity createDataKey="createConfigProductAttribute"/> -+ <requiredEntity createDataKey="getConfigAttributeOption1"/> -+ <requiredEntity createDataKey="getConfigAttributeOption2"/> -+ <requiredEntity createDataKey="getConfigAttributeOption3"/> -+ </createData> -+ -+ <!-- Add the first simple product to the configurable product --> -+ <createData entity="ConfigurableProductAddChild" stepKey="createConfigProductAddChild1"> -+ <requiredEntity createDataKey="createConfigProduct"/> -+ <requiredEntity createDataKey="createConfigChildProduct1"/> -+ </createData> -+ -+ <!-- Add the second simple product to the configurable product --> -+ <createData entity="ConfigurableProductAddChild" stepKey="createConfigProductAddChild2"> -+ <requiredEntity createDataKey="createConfigProduct"/> -+ <requiredEntity createDataKey="createConfigChildProduct2"/> -+ </createData> -+ -+ <!-- Add the third simple product to the configurable product --> -+ <createData entity="ConfigurableProductAddChild" stepKey="createConfigProductAddChild3"> -+ <requiredEntity createDataKey="createConfigProduct"/> -+ <requiredEntity createDataKey="createConfigChildProduct3"/> -+ </createData> -+ <magentoCLI command="indexer:reindex" stepKey="reindex"/> -+ <magentoCLI command="cache:flush" stepKey="flushCache"/> -+ </before> -+ <after> -+ <deleteData createDataKey="createConfigChildProduct1" stepKey="deleteSimpleProduct1"/> -+ <deleteData createDataKey="createConfigChildProduct2" stepKey="deleteSimpleProduct2"/> -+ <deleteData createDataKey="createConfigChildProduct3" stepKey="deleteSimpleProduct3"/> -+ <deleteData createDataKey="createConfigProduct" stepKey="deleteProduct"/> -+ <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> -+ <deleteData createDataKey="createConfigProductAttribute" stepKey="deleteProductAttribute"/> -+ </after> -+ -+ <!-- Add Configurable Product to the cart --> -+ <actionGroup ref="StorefrontAddConfigurableProductToTheCartActionGroup" stepKey="addConfigurableProductToCart"> -+ <argument name="urlKey" value="$$createConfigProduct.custom_attributes[url_key]$$" /> -+ <argument name="productAttribute" value="$$createConfigProductAttribute.default_value$$"/> -+ <argument name="productOption" value="$$getConfigAttributeOption2.label$$"/> -+ <argument name="qty" value="2"/> -+ </actionGroup> -+ -+ <!-- Select Mini Cart and select 'View And Edit Cart' --> -+ <actionGroup ref="clickViewAndEditCartFromMiniCart" stepKey="selectViewAndEditCart"/> -+ -+ <!--Assert Shopping Cart Summary --> -+ <actionGroup ref="AssertStorefrontShoppingCartSummaryWithShippingActionGroup" stepKey="AssertCartSummary" > -+ <argument name="subtotal" value="$40.00"/> -+ <argument name="shipping" value="10.00"/> -+ <argument name="total" value="50.00"/> -+ </actionGroup> -+ -+ <!--Assert Product Details In Checkout cart --> -+ <actionGroup ref="AssertStorefrontCheckoutCartItemsActionGroup" stepKey="assertProductItemInCheckOutCart"> -+ <argument name="productName" value="$$createConfigProduct.name$$"/> -+ <argument name="productSku" value="$$createConfigChildProduct2.sku$$"/> -+ <argument name="productPrice" value="$$createConfigChildProduct2.price$$"/> -+ <argument name="subtotal" value="$40.00" /> -+ <argument name="qty" value="2"/> -+ </actionGroup> -+ <actionGroup ref="AssertStorefrontElementVisibleActionGroup" stepKey="seeProductOptionInCart"> -+ <argument name="selector" value="{{CheckoutCartProductSection.productOptionLabel}}"/> -+ <argument name="userInput" value="$$getConfigAttributeOption2.label$$"/> -+ </actionGroup> -+ -+ <!-- Assert product details in Mini Cart --> -+ <click selector="{{StorefrontMinicartSection.showCart}}" stepKey="clickOnMiniCart"/> -+ <waitForPageLoad stepKey="waitForPageToLoad"/> -+ <actionGroup ref="AssertStorefrontMiniCartItemsActionGroup" stepKey="assertMiniCart"> -+ <argument name="productName" value="$$createConfigProduct.name$$"/> -+ <argument name="productPrice" value="$$createConfigChildProduct2.price$$"/> -+ <argument name="cartSubtotal" value="$40.00" /> -+ <argument name="qty" value="2"/> -+ </actionGroup> -+ </test> -+</tests> -diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontAddDownloadableProductToShoppingCartTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontAddDownloadableProductToShoppingCartTest.xml -new file mode 100644 -index 00000000000..5a46dbc90e2 ---- /dev/null -+++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontAddDownloadableProductToShoppingCartTest.xml -@@ -0,0 +1,87 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> -+ <test name="StorefrontAddDownloadableProductToShoppingCartTest"> -+ <annotations> -+ <stories value="Shopping Cart"/> -+ <title value="Create Downloadable product with two links and add to the shopping cart"/> -+ <description value="Create Downloadable product with two links and add to the shopping cart"/> -+ <testCaseId value="MC-14717"/> -+ <severity value="CRITICAL"/> -+ <group value="mtf_migrated"/> -+ </annotations> -+ -+ <before> -+ <magentoCLI command="config:set {{EnableFlatRateConfigData.path}} {{EnableFlatRateConfigData.value}}" stepKey="enableFlatRate"/> -+ <magentoCLI command="config:set {{EnableFlatRateDefaultPriceConfigData.path}} {{EnableFlatRateDefaultPriceConfigData.value}}" stepKey="enableFlatRateDefaultPrice"/> -+ <createData entity="ApiDownloadableProduct" stepKey="createDownloadableProduct"/> -+ <createData entity="downloadableLink1" stepKey="addDownloadableLink1"> -+ <requiredEntity createDataKey="createDownloadableProduct"/> -+ </createData> -+ <createData entity="downloadableLink2" stepKey="addDownloadableLink2"> -+ <requiredEntity createDataKey="createDownloadableProduct"/> -+ </createData> -+ <magentoCLI command="indexer:reindex" stepKey="reindex"/> -+ <magentoCLI command="cache:flush" stepKey="flushCache"/> -+ </before> -+ <after> -+ <deleteData createDataKey="createDownloadableProduct" stepKey="deleteProduct"/> -+ <actionGroup ref="logout" stepKey="logout"/> -+ </after> -+ -+ <!-- Open Downloadable Product page --> -+ <amOnPage url="{{StorefrontProductPage.url($$createDownloadableProduct.custom_attributes[url_key]$$)}}" stepKey="OpenStoreFrontProductPage"/> -+ <waitForPageLoad stepKey="waitForPageToLoad"/> -+ -+ <!-- Add Downloadable product to the cart --> -+ <actionGroup ref="StorefrontAddToCartCustomOptionsProductPageActionGroup" stepKey="addToTheCart"> -+ <argument name="productName" value="$$createDownloadableProduct.name$$" /> -+ </actionGroup> -+ -+ <!-- Select Mini Cart and select 'View And Edit Cart' --> -+ <actionGroup ref="clickViewAndEditCartFromMiniCart" stepKey="selectViewAndEditCart"/> -+ -+ <!--Assert Shopping Cart Summary--> -+ <actionGroup ref="AssertStorefrontShoppingCartSummaryItemsActionGroup" stepKey="AssertCartSummary" > -+ <argument name="subtotal" value="$123.00"/> -+ <argument name="total" value="123.00"/> -+ </actionGroup> -+ -+ <!--Assert Product Details In Checkout cart --> -+ <actionGroup ref="AssertStorefrontCheckoutCartItemsActionGroup" stepKey="assertProductItemInCheckOutCart"> -+ <argument name="productName" value="$$createDownloadableProduct.name$$"/> -+ <argument name="productSku" value="$$createDownloadableProduct.sku$$"/> -+ <argument name="productPrice" value="$123.00"/> -+ <argument name="subtotal" value="$123.00" /> -+ <argument name="qty" value="1"/> -+ </actionGroup> -+ <actionGroup ref="AssertStorefrontElementVisibleActionGroup" stepKey="seeLinksInCart"> -+ <argument name="selector" value="{{CheckoutCartProductSection.productOptionLabel}}"/> -+ <argument name="userInput" value="Links"/> -+ </actionGroup> -+ <actionGroup ref="AssertStorefrontElementVisibleActionGroup" stepKey="seeProductOptionLink1"> -+ <argument name="selector" value="{{CheckoutCartProductSection.productOptionLabel}}"/> -+ <argument name="userInput" value="{{downloadableLink1.title}}"/> -+ </actionGroup> -+ <actionGroup ref="AssertStorefrontElementVisibleActionGroup" stepKey="seeProductOptionLink2"> -+ <argument name="selector" value="{{CheckoutCartProductSection.productOptionLabel}}"/> -+ <argument name="userInput" value="{{downloadableLink2.title}}"/> -+ </actionGroup> -+ -+ <!-- Assert product details in Mini Cart --> -+ <click selector="{{StorefrontMinicartSection.showCart}}" stepKey="clickOnMiniCart"/> -+ <waitForPageLoad stepKey="waitForPageToLoad1"/> -+ <actionGroup ref="AssertStorefrontMiniCartItemsActionGroup" stepKey="assertMiniCart"> -+ <argument name="productName" value="$$createDownloadableProduct.name$$"/> -+ <argument name="productPrice" value="$123.00"/> -+ <argument name="cartSubtotal" value="123.00" /> -+ <argument name="qty" value="1"/> -+ </actionGroup> -+ </test> -+</tests> -diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontAddGroupedProductToShoppingCartTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontAddGroupedProductToShoppingCartTest.xml -new file mode 100644 -index 00000000000..247fab27143 ---- /dev/null -+++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontAddGroupedProductToShoppingCartTest.xml -@@ -0,0 +1,132 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> -+ <test name="StorefrontAddGroupedProductToShoppingCartTest"> -+ <annotations> -+ <stories value="Shopping Cart"/> -+ <title value="Create grouped product with three simple product and Add grouped product to the cart"/> -+ <description value="Create grouped product with three simple product and Add grouped product to the cart"/> -+ <testCaseId value="MC-14718"/> -+ <severity value="CRITICAL"/> -+ <group value="mtf_migrated"/> -+ <skip> -+ <issueId value="MC-16684"/> -+ </skip> -+ </annotations> -+ -+ <before> -+ <magentoCLI command="config:set {{EnableFlatRateConfigData.path}} {{EnableFlatRateConfigData.value}}" stepKey="enableFlatRate"/> -+ <magentoCLI command="config:set {{EnableFlatRateDefaultPriceConfigData.path}} {{EnableFlatRateDefaultPriceConfigData.value}}" stepKey="enableFlatRateDefaultPrice"/> -+ <!--Create Grouped product with three simple product --> -+ <createData entity="ApiProductWithDescription" stepKey="simple1" before="simple2"> -+ <field key="price">100.00</field> -+ </createData> -+ <createData entity="ApiProductWithDescription" stepKey="simple2" before="simple3"> -+ <field key="price">200.00</field> -+ </createData> -+ <createData entity="ApiProductWithDescription" stepKey="simple3" before="product"> -+ <field key="price">300.00</field> -+ </createData> -+ <createData entity="ApiGroupedProduct" stepKey="product"/> -+ <createData entity="OneSimpleProductLink" stepKey="addProductOne"> -+ <requiredEntity createDataKey="product"/> -+ <requiredEntity createDataKey="simple1"/> -+ </createData> -+ <updateData entity="OneMoreSimpleProductLink" createDataKey="addProductOne" stepKey="addProductTwo"> -+ <requiredEntity createDataKey="product"/> -+ <requiredEntity createDataKey="simple2"/> -+ </updateData> -+ <updateData entity="OneMoreSimpleProductLink" createDataKey="addProductOne" stepKey="addProductThree"> -+ <requiredEntity createDataKey="product"/> -+ <requiredEntity createDataKey="simple3"/> -+ </updateData> -+ <magentoCLI command="cache:flush" stepKey="flushCache"/> -+ </before> -+ <after> -+ <deleteData createDataKey="simple1" stepKey="deleteProduct1"/> -+ <deleteData createDataKey="simple2" stepKey="deleteProduct2"/> -+ <deleteData createDataKey="simple3" stepKey="deleteProduct3"/> -+ <deleteData createDataKey="product" stepKey="deleteGroupProduct"/> -+ </after> -+ -+ <!-- Fill Quantity and add Product to the cart --> -+ <actionGroup ref="StorefrontAddThreeGroupedProductToTheCartActionGroup" stepKey="addGropedProductsToTheCart"> -+ <argument name="urlKey" value="$$product.custom_attributes[url_key]$$"/> -+ <argument name="product1" value="$$simple1.name$$"/> -+ <argument name="product2" value="$$simple2.name$$"/> -+ <argument name="product3" value="$$simple3.name$$"/> -+ <argument name="qty1" value="1"/> -+ <argument name="qty2" value="2"/> -+ <argument name="qty3" value="3"/> -+ </actionGroup> -+ -+ <!-- Select Mini Cart and select 'View And Edit Cart' --> -+ <actionGroup ref="clickViewAndEditCartFromMiniCart" stepKey="selectViewAndEditCart"/> -+ -+ <!--Assert Product1 items in cart --> -+ <actionGroup ref="AssertStorefrontCheckoutCartItemsActionGroup" stepKey="assertSimpleProduct1ItemsInCheckOutCart"> -+ <argument name="productName" value="$$simple1.name$$"/> -+ <argument name="productSku" value="$$simple1.sku$$"/> -+ <argument name="productPrice" value="$100.00"/> -+ <argument name="subtotal" value="$100.00" /> -+ <argument name="qty" value="1"/> -+ </actionGroup> -+ -+ <!--Assert Product2 items in cart --> -+ <actionGroup ref="AssertStorefrontCheckoutCartItemsActionGroup" stepKey="assertSimpleProduct2ItemsInCheckOutCart"> -+ <argument name="productName" value="$$simple2.name$$"/> -+ <argument name="productSku" value="$$simple2.sku$$"/> -+ <argument name="productPrice" value="$200.00"/> -+ <argument name="subtotal" value="$400.00" /> -+ <argument name="qty" value="2"/> -+ </actionGroup> -+ -+ <!--Assert Product3 items in cart --> -+ <actionGroup ref="AssertStorefrontCheckoutCartItemsActionGroup" stepKey="assertSimpleProduct3ItemsInCheckOutCart"> -+ <argument name="productName" value="$$simple3.name$$"/> -+ <argument name="productSku" value="$$simple3.sku$$"/> -+ <argument name="productPrice" value="$300.00"/> -+ <argument name="subtotal" value="$900.00" /> -+ <argument name="qty" value="3"/> -+ </actionGroup> -+ -+ <!--Assert Shopping Cart Summary--> -+ <actionGroup ref="AssertStorefrontShoppingCartSummaryWithShippingActionGroup" stepKey="AssertCartSummary" > -+ <argument name="subtotal" value="$1,400.00"/> -+ <argument name="shipping" value="30.00"/> -+ <argument name="total" value="1,430.00"/> -+ </actionGroup> -+ -+ <!-- Assert product1 details in Mini Cart --> -+ <click selector="{{StorefrontMinicartSection.showCart}}" stepKey="clickOnMiniCart"/> -+ <waitForPageLoad stepKey="waitForPageToLoad1"/> -+ <actionGroup ref="AssertStorefrontMiniCartItemsActionGroup" stepKey="assertSimpleProduct3MiniCart"> -+ <argument name="productName" value="$$simple3.name$$"/> -+ <argument name="productPrice" value="$300.00"/> -+ <argument name="cartSubtotal" value="$1,400.00" /> -+ <argument name="qty" value="3"/> -+ </actionGroup> -+ -+ <!-- Assert product2 details in Mini Cart --> -+ <actionGroup ref="AssertStorefrontMiniCartItemsActionGroup" stepKey="assertSimpleProduct2MiniCart"> -+ <argument name="productName" value="$$simple2.name$$"/> -+ <argument name="productPrice" value="$200.00"/> -+ <argument name="cartSubtotal" value="$1,400.00" /> -+ <argument name="qty" value="2"/> -+ </actionGroup> -+ -+ <!-- Assert product3 details in Mini Cart --> -+ <actionGroup ref="AssertStorefrontMiniCartItemsActionGroup" stepKey="assertSimpleProduct1MiniCart"> -+ <argument name="productName" value="$$simple1.name$$"/> -+ <argument name="productPrice" value="$100.00"/> -+ <argument name="cartSubtotal" value="$1,400.00" /> -+ <argument name="qty" value="1"/> -+ </actionGroup> -+ </test> -+</tests> -diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontAddOneBundleMultiSelectOptionToTheShoppingCartTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontAddOneBundleMultiSelectOptionToTheShoppingCartTest.xml -new file mode 100644 -index 00000000000..73fa380f4bc ---- /dev/null -+++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontAddOneBundleMultiSelectOptionToTheShoppingCartTest.xml -@@ -0,0 +1,124 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> -+ <test name="StorefrontAddOneBundleMultiSelectOptionToTheShoppingCartTest"> -+ <annotations> -+ <stories value="Shopping Cart"/> -+ <title value="Select one multi select option of a bundle product and add to the shopping cart"/> -+ <description value="Select one multi select option of a bundle product and add to the shopping cart "/> -+ <testCaseId value="MC-14727"/> -+ <severity value="CRITICAL"/> -+ <group value="mtf_migrated"/> -+ <skip> -+ <issueId value="MC-16684"/> -+ </skip> -+ </annotations> -+ -+ <before> -+ <magentoCLI command="config:set {{EnableFlatRateConfigData.path}} {{EnableFlatRateConfigData.value}}" stepKey="enableFlatRate"/> -+ <magentoCLI command="config:set {{EnableFlatRateDefaultPriceConfigData.path}} {{EnableFlatRateDefaultPriceConfigData.value}}" stepKey="enableFlatRateDefaultPrice"/> -+ <createData entity="SimpleSubCategory" stepKey="createSubCategory"/> -+ <!--Create simple product--> -+ <createData entity="SimpleProduct2" stepKey="simpleProduct1"> -+ <field key="price">10.00</field> -+ </createData> -+ <createData entity="SimpleProduct2" stepKey="simpleProduct2"> -+ <field key="price">50.00</field> -+ </createData> -+ <!--Create Bundle product with Multi Select option--> -+ <createData entity="BundleProductPriceViewRange" stepKey="createBundleProduct"> -+ <requiredEntity createDataKey="createSubCategory"/> -+ </createData> -+ <createData entity="MultipleSelectOption" stepKey="createBundleOption1_1"> -+ <requiredEntity createDataKey="createBundleProduct"/> -+ <field key="required">True</field> -+ </createData> -+ <createData entity="ApiBundleLink" stepKey="linkOptionToProduct"> -+ <requiredEntity createDataKey="createBundleProduct"/> -+ <requiredEntity createDataKey="createBundleOption1_1"/> -+ <requiredEntity createDataKey="simpleProduct1"/> -+ </createData> -+ <createData entity="ApiBundleLink" stepKey="linkOptionToProduct2"> -+ <requiredEntity createDataKey="createBundleProduct"/> -+ <requiredEntity createDataKey="createBundleOption1_1"/> -+ <requiredEntity createDataKey="simpleProduct2"/> -+ </createData> -+ <magentoCLI command="indexer:reindex" stepKey="reindex"/> -+ <magentoCLI command="cache:flush" stepKey="flushCache"/> -+ </before> -+ <after> -+ <deleteData createDataKey="simpleProduct1" stepKey="deleteProduct1"/> -+ <deleteData createDataKey="simpleProduct2" stepKey="deleteProduct2"/> -+ <deleteData createDataKey="createBundleProduct" stepKey="deleteBundleProduct"/> -+ <deleteData createDataKey="createSubCategory" stepKey="deleteCategory"/> -+ </after> -+ -+ <!--Open Product page in StoreFront and assert product details --> -+ <actionGroup ref="AssertProductNameAndSkuInStorefrontProductPageByCustomAttributeUrlKey" stepKey="openProductPageAndVerifyProduct"> -+ <argument name="product" value="$$createBundleProduct$$"/> -+ </actionGroup> -+ <actionGroup ref="AssertStorefrontElementVisibleActionGroup" stepKey="seePriceRangeIsVisible"> -+ <argument name="selector" value="{{StorefrontProductInfoMainSection.priceFrom}}"/> -+ <argument name="userInput" value="From $10.00"/> -+ </actionGroup> -+ <actionGroup ref="AssertStorefrontElementVisibleActionGroup" stepKey="seePriceRangeIsVisible1"> -+ <argument name="selector" value="{{StorefrontProductInfoMainSection.priceTo}}"/> -+ <argument name="userInput" value="To $60.00"/> -+ </actionGroup> -+ -+ <!-- Click on customize And Add To Cart Button --> -+ <actionGroup ref="StorefrontSelectCustomizeAndAddToTheCartButtonActionGroup" stepKey="clickOnCustomizeAndAddtoCartButton"/> -+ -+ <!-- Select One Product Option from MultiSelect option --> -+ <selectOption selector="{{StorefrontBundledSection.multiSelectOption}}" userInput="$$simpleProduct2.name$$ +$50.00" stepKey="selectOption2Product"/> -+ -+ <!--Enter product Quantity and add to the cart --> -+ <actionGroup ref="StorefrontEnterProductQuantityAndAddToTheCartActionGroup" stepKey="enterProductQuantityAndAddToTheCart"> -+ <argument name="quantity" value="1"/> -+ </actionGroup> -+ -+ <!-- Select Mini Cart and select 'View And Edit Cart' --> -+ <actionGroup ref="clickViewAndEditCartFromMiniCart" stepKey="selectViewAndEditCart"/> -+ -+ <!--Assert Shopping Cart Summary--> -+ <actionGroup ref="AssertStorefrontShoppingCartSummaryWithShippingActionGroup" stepKey="AssertCartSummary" > -+ <argument name="subtotal" value="$50.00"/> -+ <argument name="shipping" value="5.00"/> -+ <argument name="total" value="55.00"/> -+ </actionGroup> -+ -+ <!--Assert Product items in cart --> -+ <actionGroup ref="AssertStorefrontCheckoutCartItemsActionGroup" stepKey="assertSimpleProduct1ItemsInCheckOutCart"> -+ <argument name="productName" value="$$createBundleProduct.name$$"/> -+ <argument name="productSku" value="$$createBundleProduct.sku$$"/> -+ <argument name="productPrice" value="$50.00"/> -+ <argument name="subtotal" value="$50.00" /> -+ <argument name="qty" value="1"/> -+ </actionGroup> -+ -+ <!-- Assert Grouped product options is displayed in cart --> -+ <actionGroup ref="AssertStorefrontElementVisibleActionGroup" stepKey="seeProductOptionTitle"> -+ <argument name="selector" value="{{CheckoutCartProductSection.productOptionLabel}}"/> -+ <argument name="userInput" value="$$createBundleOption1_1.title$$"/> -+ </actionGroup> -+ <actionGroup ref="AssertStorefrontElementVisibleActionGroup" stepKey="seeProductOptionInCart"> -+ <argument name="selector" value="{{CheckoutCartProductSection.productOptionLabel}}"/> -+ <argument name="userInput" value="1 x $$simpleProduct2.name$$ $50.00"/> -+ </actionGroup> -+ -+ <!-- Assert Product in Mini Cart --> -+ <actionGroup ref="StorefrontClickOnMiniCartActionGroup" stepKey="clickOnMiniCart"/> -+ <actionGroup ref="AssertStorefrontMiniCartItemsActionGroup" stepKey="assertSimpleProductInMiniCart"> -+ <argument name="productName" value="$$createBundleProduct.name$$"/> -+ <argument name="productPrice" value="$50.00"/> -+ <argument name="cartSubtotal" value="$50.00" /> -+ <argument name="qty" value="1"/> -+ </actionGroup> -+ </test> -+</tests> -diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontAddProductWithAllTypesOfCustomOptionToTheShoppingCartTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontAddProductWithAllTypesOfCustomOptionToTheShoppingCartTest.xml -new file mode 100644 -index 00000000000..611c53dba0d ---- /dev/null -+++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontAddProductWithAllTypesOfCustomOptionToTheShoppingCartTest.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="StorefrontAddProductWithAllTypesOfCustomOptionToTheShoppingCartTest"> -+ <annotations> -+ <stories value="Shopping Cart"/> -+ <title value="Create a simple products with all types of oprtions and add it to the cart"/> -+ <description value="Create a simple products with all types of oprtions and add it to the cart"/> -+ <testCaseId value="MC-14726"/> -+ <severity value="CRITICAL"/> -+ <group value="mtf_migrated"/> -+ </annotations> -+ -+ <before> -+ <createData entity="_defaultCategory" stepKey="createCategory"/> -+ <createData entity="_defaultProduct" stepKey="createProduct"> -+ <requiredEntity createDataKey="createCategory"/> -+ </createData> -+ <updateData createDataKey="createProduct" entity="productWithOptions" stepKey="updateProductWithCustomOptions"/> -+ <magentoCLI command="indexer:reindex" stepKey="reindex"/> -+ </before> -+ <after> -+ <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> -+ <deleteData createDataKey="createProduct" stepKey="deleteProduct"/> -+ </after> -+ <!-- Open Product page in StoreFront --> -+ <actionGroup ref="AssertProductNameAndSkuInStorefrontProductPageByCustomAttributeUrlKey" stepKey="openProduct1PageAndVerifyProduct"> -+ <argument name="product" value="$$createProduct$$"/> -+ </actionGroup> -+ -+ <!-- Fill Option Input field --> -+ <actionGroup ref="StorefrontFillOptionFieldInputActionGroup" stepKey="fillTheOptionFieldInput"> -+ <argument name="fieldInput" value="1"/> -+ </actionGroup> -+ -+ <!-- Fill Option Text Area --> -+ <actionGroup ref="StorefrontFillOptionTextAreaActionGroup" stepKey="fillTheOptionTextAreaInput"> -+ <argument name="fieldInput" value="1"/> -+ </actionGroup> -+ -+ <!-- Attach file option--> -+ <actionGroup ref="StorefrontAttachOptionFileActionGroup" stepKey="selectAndAttachFile"/> -+ -+ <!--Select Option From DropDown option --> -+ <actionGroup ref="StorefrontSelectOptionDropDownActionGroup" stepKey="selectDropDownOption"/> -+ <scrollTo selector="{{StorefrontProductInfoMainSection.customOptionLabel(ProductOptionMultiSelect.title)}}" stepKey="scrollToMultiSelect"/> -+ -+ <!-- Select CheckBox From CheckBox option --> -+ <actionGroup ref="StorefrontSelectOptionCheckBoxActionGroup" stepKey="selectCheckBoxOption"/> -+ -+ <!-- Select RadioButton From Radio Button option --> -+ <actionGroup ref="StorefrontSelectOptionRadioButtonActionGroup" stepKey="selectRadioButtonOption"/> -+ -+ <!-- Select option From MultiSelect option --> -+ <actionGroup ref="StorefrontSelectOptionMultiSelectActionGroup" stepKey="selectOptionFromMultiSelect"/> -+ -+ <!-- Generate Date --> -+ <generateDate date="Now" format="Y" stepKey="year"/> -+ -+ <!-- Select Month,Day and Year From Date option --> -+ <actionGroup ref="StorefrontSelectOptionDateActionGroup" stepKey="fillOptionDate"> -+ <argument name="month" value="11"/> -+ <argument name="day" value="10"/> -+ <argument name="year" value="$year"/> -+ </actionGroup> -+ -+ <!-- Select Month, Day, Year, Hour,Minute and DayPart from DateTime option --> -+ <actionGroup ref="StorefrontSelectOptionDateTimeActionGroup" stepKey="fillOptionDateAndTime"> -+ <argument name="month" value="11"/> -+ <argument name="day" value="10"/> -+ <argument name="year" value="$year"/> -+ <argument name="hour" value="10"/> -+ <argument name="minute" value="20"/> -+ <argument name="dayPart" value="AM"/> -+ </actionGroup> -+ -+ <!-- Select Hour,Minute and DayPart from Time option --> -+ <actionGroup ref="StorefrontSelectOptionTimeActionGroup" stepKey="fillOptionTime"> -+ <argument name="hour" value="10"/> -+ <argument name="minute" value="20"/> -+ <argument name="dayPart" value="AM"/> -+ </actionGroup> -+ -+ <!-- Add product to the cart --> -+ <fillField selector="{{StorefrontProductInfoMainSection.qty}}" userInput="2" stepKey="fillQuantity"/> -+ <actionGroup ref="StorefrontAddProductToCartActionGroup" stepKey="addProductToTheCart"> -+ <argument name="product" value="$$createProduct$$"/> -+ <argument name="productCount" value="2"/> -+ </actionGroup> -+ -+ <!-- Open Mini Cart --> -+ <actionGroup ref="StorefrontOpenMiniCartActionGroup" stepKey="openMiniCart"/> -+ -+ <!-- Assert Product Count in Mini Cart --> -+ <actionGroup ref="StorefrontAssertMiniCartItemCountActionGroup" stepKey="assertProductCountAndTextInMiniCart"> -+ <argument name="productCount" value="2"/> -+ <argument name="productCountText" value="1 Item in Cart"/> -+ </actionGroup> -+ -+ <!--Assert Product Items in Mini cart--> -+ <actionGroup ref="AssertStorefrontMiniCartItemsActionGroup" stepKey="assertSimpleProduct3MiniCart"> -+ <argument name="productName" value="$$createProduct.name$$"/> -+ <argument name="productPrice" value="$1,642.58"/> -+ <argument name="cartSubtotal" value="$3,285.16" /> -+ <argument name="qty" value="2"/> -+ </actionGroup> -+ </test> -+</tests> -\ No newline at end of file -diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontAddTwoBundleMultiSelectOptionsToTheShoppingCartTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontAddTwoBundleMultiSelectOptionsToTheShoppingCartTest.xml -new file mode 100644 -index 00000000000..e2ff7f2c98d ---- /dev/null -+++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontAddTwoBundleMultiSelectOptionsToTheShoppingCartTest.xml -@@ -0,0 +1,127 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> -+ <test name="StorefrontAddTwoBundleMultiSelectOptionsToTheShoppingCartTest"> -+ <annotations> -+ <stories value="Shopping Cart"/> -+ <title value="Add two products to the cart from multi select options of a bundle product"/> -+ <description value="Add two products to the cart from multi select options of a bundle product."/> -+ <testCaseId value="MC-14728"/> -+ <severity value="CRITICAL"/> -+ <group value="mtf_migrated"/> -+ <skip> -+ <issueId value="MC-16684"/> -+ </skip> -+ </annotations> -+ -+ <before> -+ <magentoCLI command="config:set {{EnableFlatRateConfigData.path}} {{EnableFlatRateConfigData.value}}" stepKey="enableFlatRate"/> -+ <magentoCLI command="config:set {{EnableFlatRateDefaultPriceConfigData.path}} {{EnableFlatRateDefaultPriceConfigData.value}}" stepKey="enableFlatRateDefaultPrice"/> -+ <createData entity="SimpleSubCategory" stepKey="createSubCategory"/> -+ <!--Create simple product--> -+ <createData entity="SimpleProduct2" stepKey="simpleProduct1"> -+ <field key="price">10.00</field> -+ </createData> -+ <createData entity="SimpleProduct2" stepKey="simpleProduct2"> -+ <field key="price">50.00</field> -+ </createData> -+ <!--Create Bundle product with multi select option--> -+ <createData entity="BundleProductPriceViewRange" stepKey="createBundleProduct"> -+ <requiredEntity createDataKey="createSubCategory"/> -+ </createData> -+ <createData entity="MultipleSelectOption" stepKey="createBundleOption1_1"> -+ <requiredEntity createDataKey="createBundleProduct"/> -+ <field key="required">True</field> -+ </createData> -+ <createData entity="ApiBundleLink" stepKey="linkOptionToProduct"> -+ <requiredEntity createDataKey="createBundleProduct"/> -+ <requiredEntity createDataKey="createBundleOption1_1"/> -+ <requiredEntity createDataKey="simpleProduct1"/> -+ </createData> -+ <createData entity="ApiBundleLink" stepKey="linkOptionToProduct2"> -+ <requiredEntity createDataKey="createBundleProduct"/> -+ <requiredEntity createDataKey="createBundleOption1_1"/> -+ <requiredEntity createDataKey="simpleProduct2"/> -+ </createData> -+ <magentoCLI command="indexer:reindex" stepKey="reindex"/> -+ <magentoCLI command="cache:flush" stepKey="flushCache"/> -+ </before> -+ <after> -+ <deleteData createDataKey="simpleProduct1" stepKey="deleteProduct1"/> -+ <deleteData createDataKey="simpleProduct2" stepKey="deleteProduct2"/> -+ <deleteData createDataKey="createBundleProduct" stepKey="deleteBundleProduct"/> -+ <deleteData createDataKey="createSubCategory" stepKey="deleteCategory"/> -+ </after> -+ -+ <!--Open Product page in StoreFront --> -+ <actionGroup ref="AssertProductNameAndSkuInStorefrontProductPageByCustomAttributeUrlKey" stepKey="openProductPageAndVerifyProduct"> -+ <argument name="product" value="$$createBundleProduct$$"/> -+ </actionGroup> -+ <actionGroup ref="AssertStorefrontElementVisibleActionGroup" stepKey="seePriceRangeIsVisible"> -+ <argument name="selector" value="{{StorefrontProductInfoMainSection.priceFrom}}"/> -+ <argument name="userInput" value="From $10.00"/> -+ </actionGroup> -+ <actionGroup ref="AssertStorefrontElementVisibleActionGroup" stepKey="seePriceRangeIsVisible1"> -+ <argument name="selector" value="{{StorefrontProductInfoMainSection.priceTo}}"/> -+ <argument name="userInput" value="To $60.00"/> -+ </actionGroup> -+ -+ <!-- Click on customize And Add To Cart Button --> -+ <actionGroup ref="StorefrontSelectCustomizeAndAddToTheCartButtonActionGroup" stepKey="clickOnCustomizeAndAddtoCartButton"/> -+ -+ <!-- Select Two Products, enter the quantity and add product to the cart --> -+ <selectOption selector="{{StorefrontBundledSection.multiSelectOption}}" parameterArray="[$$simpleProduct1.name$$ +$10.00, $$simpleProduct2.name$$ +$50.00]" stepKey="selectOptions"/> -+ <actionGroup ref="StorefrontEnterProductQuantityAndAddToTheCartActionGroup" stepKey="enterProductQuantityAndAddToTheCart"> -+ <argument name="quantity" value="1"/> -+ </actionGroup> -+ -+ <!-- Select Mini Cart and select 'View And Edit Cart' --> -+ <actionGroup ref="clickViewAndEditCartFromMiniCart" stepKey="selectViewAndEditCart"/> -+ -+ <!--Assert Shopping Cart Summary--> -+ <actionGroup ref="AssertStorefrontShoppingCartSummaryWithShippingActionGroup" stepKey="AssertCartSummary" > -+ <argument name="subtotal" value="$60.00"/> -+ <argument name="shipping" value="5.00"/> -+ <argument name="total" value="65.00"/> -+ </actionGroup> -+ -+ <!--Assert Product items in cart --> -+ <actionGroup ref="AssertStorefrontCheckoutCartItemsActionGroup" stepKey="assertSimpleProduct1ItemsInCheckOutCart"> -+ <argument name="productName" value="$$createBundleProduct.name$$"/> -+ <argument name="productSku" value="$$createBundleProduct.sku$$"/> -+ <argument name="productPrice" value="$60.00"/> -+ <argument name="subtotal" value="$60.00" /> -+ <argument name="qty" value="1"/> -+ </actionGroup> -+ -+ <!-- Assert Grouped product options is displayed in cart --> -+ <actionGroup ref="AssertStorefrontElementVisibleActionGroup" stepKey="seeProductOptionTitle"> -+ <argument name="selector" value="{{CheckoutCartProductSection.productOptionLabel}}"/> -+ <argument name="userInput" value="$$createBundleOption1_1.title$$"/> -+ </actionGroup> -+ <actionGroup ref="AssertStorefrontElementVisibleActionGroup" stepKey="seeProductOption1InCart"> -+ <argument name="selector" value="{{CheckoutCartProductSection.productOptionLabel}}"/> -+ <argument name="userInput" value="1 x $$simpleProduct1.name$$ $10.00"/> -+ </actionGroup> -+ <actionGroup ref="AssertStorefrontElementVisibleActionGroup" stepKey="seeProductOption2InCart"> -+ <argument name="selector" value="{{CheckoutCartProductSection.productOptionLabel}}"/> -+ <argument name="userInput" value="1 x $$simpleProduct2.name$$ $50.00"/> -+ </actionGroup> -+ -+ <!-- Assert Product in Mini Cart --> -+ <click selector="{{StorefrontMinicartSection.showCart}}" stepKey="clickOnMiniCart"/> -+ <waitForPageLoad stepKey="waitForPageToLoad1"/> -+ <actionGroup ref="AssertStorefrontMiniCartItemsActionGroup" stepKey="assertSimpleProduct3MiniCart"> -+ <argument name="productName" value="$$createBundleProduct.name$$"/> -+ <argument name="productPrice" value="$60.00"/> -+ <argument name="cartSubtotal" value="$60.00" /> -+ <argument name="qty" value="1"/> -+ </actionGroup> -+ </test> -+</tests> -diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontApplyPromoCodeDuringCheckoutTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontApplyPromoCodeDuringCheckoutTest.xml -new file mode 100644 -index 00000000000..bdfdfceab53 ---- /dev/null -+++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontApplyPromoCodeDuringCheckoutTest.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="StorefrontApplyPromoCodeDuringCheckoutTest"> -+ <annotations> -+ <features value="OnePageCheckout"/> -+ <stories value="OnePageCheckout with Promo Code"/> -+ <title value="Storefront apply promo code during checkout test"/> -+ <description value="Apply promo code during checkout for physical product"/> -+ <severity value="CRITICAL"/> -+ <testCaseId value="MC-13212"/> -+ <group value="checkout"/> -+ </annotations> -+ <before> -+ <!-- Create simple product --> -+ <createData entity="SimpleProduct2" stepKey="createProduct"> -+ <field key="price">10.00</field> -+ </createData> -+ -+ <!-- Create cart price rule --> -+ <createData entity="ActiveSalesRuleForNotLoggedIn" stepKey="createCartPriceRule"/> -+ <createData entity="SimpleSalesRuleCoupon" stepKey="createCouponForCartPriceRule"> -+ <requiredEntity createDataKey="createCartPriceRule"/> -+ </createData> -+ </before> -+ <after> -+ <!-- Delete simple product --> -+ <deleteData createDataKey="createProduct" stepKey="deleteProduct"/> -+ -+ <!-- Delete sales rule --> -+ <deleteData createDataKey="createCartPriceRule" stepKey="deleteCartPriceRule"/> -+ -+ <!-- Admin log out --> -+ <actionGroup ref="logout" stepKey="logout"/> -+ </after> -+ -+ <!-- Go to Storefront as Guest and add simple product to cart --> -+ <actionGroup ref="AddSimpleProductToCart" stepKey="addProductToCart"> -+ <argument name="product" value="$$createProduct$$"/> -+ </actionGroup> -+ -+ <!-- Go to Checkout --> -+ <actionGroup ref="GoToCheckoutFromMinicartActionGroup" stepKey="goToCheckoutFromMinicart"/> -+ -+ <!-- Fill all required fields with valid data and select Flat Rate, price = 5, shipping --> -+ <actionGroup ref="GuestCheckoutFillingShippingSectionActionGroup" stepKey="guestCheckoutFillingShipping"> -+ <argument name="shippingMethod" value="Flat Rate"/> -+ </actionGroup> -+ -+ <!-- Click Apply Discount Code: section is expanded. Input promo code, apply and see success message --> -+ <actionGroup ref="StorefrontApplyDiscountCodeActionGroup" stepKey="applyCoupon"> -+ <argument name="discountCode" value="$$createCouponForCartPriceRule.code$$"/> -+ </actionGroup> -+ -+ <!-- Apply button is disappeared --> -+ <dontSeeElement selector="{{DiscountSection.ApplyCodeBtn}}" stepKey="dontSeeApplyButton"/> -+ -+ <!-- Cancel coupon button is appeared --> -+ <seeElement selector="{{DiscountSection.CancelCouponBtn}}" stepKey="seeCancelCouponButton"/> -+ -+ <!-- Order summary contains information about applied code --> -+ <seeElement selector="{{CheckoutPaymentSection.discount}}" stepKey="seeDiscountCouponInSummaryBlock"/> -+ <see selector="{{CheckoutPaymentSection.discountPrice}}" userInput="-$5.00" stepKey="seeDiscountPrice"/> -+ -+ <!-- Select payment solution --> -+ <actionGroup ref="CheckoutSelectCheckMoneyOrderPaymentActionGroup" stepKey="clickCheckMoneyOrderPayment"/> -+ -+ <!-- Place Order: order is successfully placed --> -+ <actionGroup ref="ClickPlaceOrderActionGroup" stepKey="clickPlaceOrder"/> -+ <grabTextFrom selector="{{CheckoutSuccessMainSection.orderNumber}}" stepKey="grabOrderNumber"/> -+ -+ <!-- Login as admin --> -+ <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> -+ -+ <!-- Verify total on order page --> -+ <actionGroup ref="filterOrderGridById" stepKey="filterOrderById"> -+ <argument name="orderId" value="$grabOrderNumber"/> -+ </actionGroup> -+ <click selector="{{AdminOrdersGridSection.firstRow}}" stepKey="clickOrderRow"/> -+ <waitForPageLoad stepKey="waitForAdminOrderPageLoad"/> -+ <scrollTo selector="{{AdminOrderTotalSection.grandTotal}}" stepKey="scrollToOrderTotalSection"/> -+ <see selector="{{AdminOrderTotalSection.grandTotal}}" userInput="$$createProduct.price$$" stepKey="checkTotal"/> -+ </test> -+</tests> -diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCheckCartAndSummaryBlockItemDisplayWithDefaultDisplayLimitationTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCheckCartAndSummaryBlockItemDisplayWithDefaultDisplayLimitationTest.xml -new file mode 100644 -index 00000000000..beb2d40f94c ---- /dev/null -+++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCheckCartAndSummaryBlockItemDisplayWithDefaultDisplayLimitationTest.xml -@@ -0,0 +1,343 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> -+ <test name="StorefrontCheckCartAndSummaryBlockItemDisplayWithDefaultDisplayLimitationTest"> -+ <annotations> -+ <stories value="Shopping Cart"/> -+ <title value="Add 10 items to the cart and check cart and summary block display with default display limit"/> -+ <description value="Add 10 items to the cart and check cart and summary block display with default display limit"/> -+ <testCaseId value="MC-14722"/> -+ <severity value="CRITICAL"/> -+ <group value="mtf_migrated"/> -+ </annotations> -+ -+ <before> -+ <magentoCLI command="config:set {{EnableFlatRateConfigData.path}} {{EnableFlatRateConfigData.value}}" stepKey="enableFlatRate"/> -+ <magentoCLI command="config:set {{EnableFlatRateDefaultPriceConfigData.path}} {{EnableFlatRateDefaultPriceConfigData.value}}" stepKey="enableFlatRateDefaultPrice"/> -+ <!--Create simple product--> -+ <createData entity="SimpleProduct2" stepKey="simpleProduct1"> -+ <field key="price">10.00</field> -+ </createData> -+ <createData entity="SimpleProduct2" stepKey="simpleProduct2"> -+ <field key="price">20.00</field> -+ </createData> -+ <createData entity="SimpleProduct2" stepKey="simpleProduct3"> -+ <field key="price">30.00</field> -+ </createData> -+ <createData entity="SimpleProduct2" stepKey="simpleProduct4"> -+ <field key="price">40.00</field> -+ </createData> -+ <createData entity="SimpleProduct2" stepKey="simpleProduct5"> -+ <field key="price">50.00</field> -+ </createData> -+ <createData entity="SimpleProduct2" stepKey="simpleProduct6"> -+ <field key="price">60.00</field> -+ </createData> -+ <createData entity="SimpleProduct2" stepKey="simpleProduct7"> -+ <field key="price">70.00</field> -+ </createData> -+ <createData entity="SimpleProduct2" stepKey="simpleProduct8"> -+ <field key="price">80.00</field> -+ </createData> -+ <createData entity="SimpleProduct2" stepKey="simpleProduct9"> -+ <field key="price">90.00</field> -+ </createData> -+ <createData entity="SimpleProduct2" stepKey="simpleProduct10"> -+ <field key="price">100.00</field> -+ </createData> -+ <magentoCLI command="cache:flush" stepKey="flushCache"/> -+ </before> -+ <after> -+ <deleteData createDataKey="simpleProduct1" stepKey="deleteProduct1"/> -+ <deleteData createDataKey="simpleProduct2" stepKey="deleteProduct2"/> -+ <deleteData createDataKey="simpleProduct3" stepKey="deleteProduct3"/> -+ <deleteData createDataKey="simpleProduct4" stepKey="deleteProduct4"/> -+ <deleteData createDataKey="simpleProduct5" stepKey="deleteProduct5"/> -+ <deleteData createDataKey="simpleProduct6" stepKey="deleteProduct6"/> -+ <deleteData createDataKey="simpleProduct7" stepKey="deleteProduct7"/> -+ <deleteData createDataKey="simpleProduct8" stepKey="deleteProduct8"/> -+ <deleteData createDataKey="simpleProduct9" stepKey="deleteProduct9"/> -+ <deleteData createDataKey="simpleProduct10" stepKey="deleteProduct10"/> -+ </after> -+ -+ <!-- Open Product1 page in StoreFront --> -+ <actionGroup ref="AssertProductNameAndSkuInStorefrontProductPageByCustomAttributeUrlKey" stepKey="openProduct1PageAndVerifyProduct"> -+ <argument name="product" value="$$simpleProduct1$$"/> -+ </actionGroup> -+ -+ <!-- Add Product1 to the cart --> -+ <actionGroup ref="addToCartFromStorefrontProductPage" stepKey="addProduct1ToTheCart"> -+ <argument name="productName" value="$$simpleProduct1.name$$"/> -+ </actionGroup> -+ -+ <!-- Open Product2 page in StoreFront --> -+ <actionGroup ref="AssertProductNameAndSkuInStorefrontProductPageByCustomAttributeUrlKey" stepKey="openProduct2PageAndVerifyProduct"> -+ <argument name="product" value="$$simpleProduct2$$"/> -+ </actionGroup> -+ -+ <!-- Add Product2 to the cart --> -+ <actionGroup ref="addToCartFromStorefrontProductPage" stepKey="addProduct2ToTheCart"> -+ <argument name="productName" value="$$simpleProduct2.name$$"/> -+ </actionGroup> -+ -+ <!-- Open Product3 page in StoreFront --> -+ <actionGroup ref="AssertProductNameAndSkuInStorefrontProductPageByCustomAttributeUrlKey" stepKey="openProduct3PageAndVerifyProduct"> -+ <argument name="product" value="$$simpleProduct3$$"/> -+ </actionGroup> -+ -+ <!-- Add Product3 to the cart --> -+ <actionGroup ref="addToCartFromStorefrontProductPage" stepKey="addProduct3ToTheCart"> -+ <argument name="productName" value="$$simpleProduct3.name$$"/> -+ </actionGroup> -+ -+ <!-- Open Product4 page in StoreFront --> -+ <actionGroup ref="AssertProductNameAndSkuInStorefrontProductPageByCustomAttributeUrlKey" stepKey="openProduct4PageAndVerifyProduct"> -+ <argument name="product" value="$$simpleProduct4$$"/> -+ </actionGroup> -+ -+ <!-- Add Product4 to the cart --> -+ <actionGroup ref="addToCartFromStorefrontProductPage" stepKey="addProduct4ToTheCart"> -+ <argument name="productName" value="$$simpleProduct4.name$$"/> -+ </actionGroup> -+ -+ <!-- Open Product5 page in StoreFront --> -+ <actionGroup ref="AssertProductNameAndSkuInStorefrontProductPageByCustomAttributeUrlKey" stepKey="openProduct5PageAndVerifyProduct"> -+ <argument name="product" value="$$simpleProduct5$$"/> -+ </actionGroup> -+ -+ <!-- Add Product5 to the cart --> -+ <actionGroup ref="addToCartFromStorefrontProductPage" stepKey="addProduct5ToTheCart"> -+ <argument name="productName" value="$$simpleProduct5.name$$"/> -+ </actionGroup> -+ -+ <!-- Open Product6 page in StoreFront --> -+ <actionGroup ref="AssertProductNameAndSkuInStorefrontProductPageByCustomAttributeUrlKey" stepKey="openProduct6PageAndVerifyProduct"> -+ <argument name="product" value="$$simpleProduct6$$"/> -+ </actionGroup> -+ -+ <!-- Add Product6 to the cart --> -+ <actionGroup ref="addToCartFromStorefrontProductPage" stepKey="addProduct6ToTheCart"> -+ <argument name="productName" value="$$simpleProduct6.name$$"/> -+ </actionGroup> -+ -+ <!-- Open Product7 page in StoreFront --> -+ <actionGroup ref="AssertProductNameAndSkuInStorefrontProductPageByCustomAttributeUrlKey" stepKey="openProduct7PageAndVerifyProduct"> -+ <argument name="product" value="$$simpleProduct7$$"/> -+ </actionGroup> -+ -+ <!-- Add Product7 to the cart --> -+ <actionGroup ref="addToCartFromStorefrontProductPage" stepKey="addProduct7ToTheCart"> -+ <argument name="productName" value="$$simpleProduct7.name$$"/> -+ </actionGroup> -+ -+ <!-- Open Product8 page in StoreFront --> -+ <actionGroup ref="AssertProductNameAndSkuInStorefrontProductPageByCustomAttributeUrlKey" stepKey="openProduct8PageAndVerifyProduct"> -+ <argument name="product" value="$$simpleProduct8$$"/> -+ </actionGroup> -+ -+ <!-- Add Product8 to the cart --> -+ <actionGroup ref="addToCartFromStorefrontProductPage" stepKey="addProduct8ToTheCart"> -+ <argument name="productName" value="$$simpleProduct8.name$$"/> -+ </actionGroup> -+ -+ <!-- Open Product9 page in StoreFront --> -+ <actionGroup ref="AssertProductNameAndSkuInStorefrontProductPageByCustomAttributeUrlKey" stepKey="openProduct9PageAndVerifyProduct"> -+ <argument name="product" value="$$simpleProduct9$$"/> -+ </actionGroup> -+ -+ <!-- Add Product9 to the cart --> -+ <actionGroup ref="addToCartFromStorefrontProductPage" stepKey="addProduct9ToTheCart"> -+ <argument name="productName" value="$$simpleProduct9.name$$"/> -+ </actionGroup> -+ -+ <!-- Open Product10 page in StoreFront --> -+ <actionGroup ref="AssertProductNameAndSkuInStorefrontProductPageByCustomAttributeUrlKey" stepKey="openProductPage10AndVerifyProduct"> -+ <argument name="product" value="$$simpleProduct10$$"/> -+ </actionGroup> -+ -+ <!-- Add Product10 to the cart --> -+ <actionGroup ref="addToCartFromStorefrontProductPage" stepKey="addProduct10ToTheCart"> -+ <argument name="productName" value="$$simpleProduct10.name$$"/> -+ </actionGroup> -+ -+ <!-- Open Mini Cart --> -+ <actionGroup ref="StorefrontOpenMiniCartActionGroup" stepKey="openMiniCart"/> -+ -+ <!-- Assert Product Count in Mini Cart --> -+ <actionGroup ref="StorefrontAssertMiniCartItemCountActionGroup" stepKey="assertProductCountAndTextInMiniCart"> -+ <argument name="productCount" value="10"/> -+ <argument name="productCountText" value="10 Items in Cart"/> -+ </actionGroup> -+ -+ <!-- Assert Product1 in Mini Cart --> -+ <actionGroup ref="AssertStorefrontMiniCartItemsActionGroup" stepKey="assertSimpleProduct11MiniCart"> -+ <argument name="productName" value="$$simpleProduct1.name$$"/> -+ <argument name="productPrice" value="$10.00"/> -+ <argument name="cartSubtotal" value="$550.00" /> -+ <argument name="qty" value="1"/> -+ </actionGroup> -+ -+ <!-- Assert Product2 in Mini Cart --> -+ <actionGroup ref="AssertStorefrontMiniCartItemsActionGroup" stepKey="assertSimpleProduct2MiniCart"> -+ <argument name="productName" value="$$simpleProduct2.name$$"/> -+ <argument name="productPrice" value="$20.00"/> -+ <argument name="cartSubtotal" value="$550.00" /> -+ <argument name="qty" value="1"/> -+ </actionGroup> -+ -+ <!-- Assert Product3 in Mini Cart --> -+ <actionGroup ref="AssertStorefrontMiniCartItemsActionGroup" stepKey="assertSimpleProduct3MiniCart"> -+ <argument name="productName" value="$$simpleProduct3.name$$"/> -+ <argument name="productPrice" value="$30.00"/> -+ <argument name="cartSubtotal" value="$550.00" /> -+ <argument name="qty" value="1"/> -+ </actionGroup> -+ -+ <!-- Assert Product4 in Mini Cart --> -+ <actionGroup ref="AssertStorefrontMiniCartItemsActionGroup" stepKey="assertSimpleProduct4MiniCart"> -+ <argument name="productName" value="$$simpleProduct4.name$$"/> -+ <argument name="productPrice" value="$40.00"/> -+ <argument name="cartSubtotal" value="$550.00" /> -+ <argument name="qty" value="1"/> -+ </actionGroup> -+ -+ <!-- Assert Product5 in Mini Cart --> -+ <actionGroup ref="AssertStorefrontMiniCartItemsActionGroup" stepKey="assertSimpleProduct5MiniCart"> -+ <argument name="productName" value="$$simpleProduct5.name$$"/> -+ <argument name="productPrice" value="$50.00"/> -+ <argument name="cartSubtotal" value="$550.00" /> -+ <argument name="qty" value="1"/> -+ </actionGroup> -+ -+ <!-- Assert Product6 in Mini Cart --> -+ <actionGroup ref="AssertStorefrontMiniCartItemsActionGroup" stepKey="assertSimpleProduct6MiniCart"> -+ <argument name="productName" value="$$simpleProduct6.name$$"/> -+ <argument name="productPrice" value="$60.00"/> -+ <argument name="cartSubtotal" value="$550.00" /> -+ <argument name="qty" value="1"/> -+ </actionGroup> -+ -+ <!-- Assert Product7 in Mini Cart --> -+ <actionGroup ref="AssertStorefrontMiniCartItemsActionGroup" stepKey="assertSimpleProduct7MiniCart"> -+ <argument name="productName" value="$$simpleProduct7.name$$"/> -+ <argument name="productPrice" value="$70.00"/> -+ <argument name="cartSubtotal" value="$550.00" /> -+ <argument name="qty" value="1"/> -+ </actionGroup> -+ -+ <!-- Assert Product8 in Mini Cart --> -+ <actionGroup ref="AssertStorefrontMiniCartItemsActionGroup" stepKey="assertSimpleProduct8MiniCart"> -+ <argument name="productName" value="$$simpleProduct8.name$$"/> -+ <argument name="productPrice" value="$80.00"/> -+ <argument name="cartSubtotal" value="$550.00" /> -+ <argument name="qty" value="1"/> -+ </actionGroup> -+ -+ <!-- Assert Product9 in Mini Cart --> -+ <actionGroup ref="AssertStorefrontMiniCartItemsActionGroup" stepKey="assertSimpleProduct9MiniCart"> -+ <argument name="productName" value="$$simpleProduct9.name$$"/> -+ <argument name="productPrice" value="$90.00"/> -+ <argument name="cartSubtotal" value="$550.00" /> -+ <argument name="qty" value="1"/> -+ </actionGroup> -+ -+ <!-- Assert Product10 in Mini Cart --> -+ <actionGroup ref="AssertStorefrontMiniCartItemsActionGroup" stepKey="assertSimpleProduct10MiniCart"> -+ <argument name="productName" value="$$simpleProduct10.name$$"/> -+ <argument name="productPrice" value="$100.00"/> -+ <argument name="cartSubtotal" value="$550.00" /> -+ <argument name="qty" value="1"/> -+ </actionGroup> -+ -+ <!-- Assert Order Summary --> -+ <actionGroup ref="StorefrontCheckoutAndAssertOrderSummaryDisplayActionGroup" stepKey="AssertOrderSummary"> -+ <argument name="itemsText" value="10 Items in Cart"/> -+ </actionGroup> -+ -+ <!-- Assert Shipping Page --> -+ <actionGroup ref="StorefrontAssertShippingAddressPageDisplayActionGroup" stepKey="assertShippingPageDisplay"/> -+ -+ <!--Click on order summary tab --> -+ <conditionalClick selector="{{CheckoutOrderSummarySection.miniCartTab}}" dependentSelector="{{CheckoutOrderSummarySection.miniCartTabClosed}}" visible="true" stepKey="clickOnOrderSummaryTab"/> -+ <waitForPageLoad stepKey="waitForPageToLoad"/> -+ -+ <!-- Assert Product1 displayed in Order Summary --> -+ <actionGroup ref="StorefrontAssertProductDetailsInOrderSummaryActionGroup" stepKey="assertProduct1InOrderSummary"> -+ <argument name="productName" value="$$simpleProduct1.name$$"/> -+ <argument name="qty" value="1"/> -+ <argument name="price" value="$10.00"/> -+ </actionGroup> -+ -+ <!-- Assert Product2 displayed in Order Summary --> -+ <actionGroup ref="StorefrontAssertProductDetailsInOrderSummaryActionGroup" stepKey="assertProduct2InOrderSummary"> -+ <argument name="productName" value="$$simpleProduct2.name$$"/> -+ <argument name="qty" value="1"/> -+ <argument name="price" value="$20.00"/> -+ </actionGroup> -+ -+ <!-- Assert Product3 displayed in Order Summary --> -+ <actionGroup ref="StorefrontAssertProductDetailsInOrderSummaryActionGroup" stepKey="assertProduct3InOrderSummary"> -+ <argument name="productName" value="$$simpleProduct3.name$$"/> -+ <argument name="qty" value="1"/> -+ <argument name="price" value="$30.00"/> -+ </actionGroup> -+ -+ <!-- Assert Product4 displayed in Order Summary --> -+ <actionGroup ref="StorefrontAssertProductDetailsInOrderSummaryActionGroup" stepKey="assertProduct4InOrderSummary"> -+ <argument name="productName" value="$$simpleProduct4.name$$"/> -+ <argument name="qty" value="1"/> -+ <argument name="price" value="$40.00"/> -+ </actionGroup> -+ -+ <!-- Assert Product5 displayed in Order Summary --> -+ <actionGroup ref="StorefrontAssertProductDetailsInOrderSummaryActionGroup" stepKey="assertProduct5InOrderSummary"> -+ <argument name="productName" value="$$simpleProduct5.name$$"/> -+ <argument name="qty" value="1"/> -+ <argument name="price" value="$50.00"/> -+ </actionGroup> -+ -+ <!-- Assert Product6 displayed in Order Summary --> -+ <actionGroup ref="StorefrontAssertProductDetailsInOrderSummaryActionGroup" stepKey="assertProduct6InOrderSummary"> -+ <argument name="productName" value="$$simpleProduct6.name$$"/> -+ <argument name="qty" value="1"/> -+ <argument name="price" value="$60.00"/> -+ </actionGroup> -+ -+ <!-- Assert Product7 displayed in Order Summary --> -+ <actionGroup ref="StorefrontAssertProductDetailsInOrderSummaryActionGroup" stepKey="assertProduct7InOrderSummary"> -+ <argument name="productName" value="$$simpleProduct7.name$$"/> -+ <argument name="qty" value="1"/> -+ <argument name="price" value="$70.00"/> -+ </actionGroup> -+ -+ <!-- Assert Product8 displayed in Order Summary --> -+ <actionGroup ref="StorefrontAssertProductDetailsInOrderSummaryActionGroup" stepKey="assertProduct8InOrderSummary"> -+ <argument name="productName" value="$$simpleProduct8.name$$"/> -+ <argument name="qty" value="1"/> -+ <argument name="price" value="$80.00"/> -+ </actionGroup> -+ -+ <!-- Assert Product9 displayed in Order Summary --> -+ <actionGroup ref="StorefrontAssertProductDetailsInOrderSummaryActionGroup" stepKey="assertProduct9InOrderSummary"> -+ <argument name="productName" value="$$simpleProduct9.name$$"/> -+ <argument name="qty" value="1"/> -+ <argument name="price" value="$90.00"/> -+ </actionGroup> -+ -+ <!-- Assert Product10 displayed in Order Summary --> -+ <actionGroup ref="StorefrontAssertProductDetailsInOrderSummaryActionGroup" stepKey="assertProduct10InOrderSummary"> -+ <argument name="productName" value="$$simpleProduct10.name$$"/> -+ <argument name="qty" value="1"/> -+ <argument name="price" value="$100.00"/> -+ </actionGroup> -+ -+ <!-- Verify View and Edit Cart is not displayed in Order Summary --> -+ <dontSeeElement selector="{{StorefrontMinicartSection.viewAndEditCart}}" stepKey="dontSeeViewAndEditLink"/> -+ </test> -+</tests> -diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCheckCartItemDisplayWhenMoreItemsAddedToTheCartThanDefaultDisplayLimitTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCheckCartItemDisplayWhenMoreItemsAddedToTheCartThanDefaultDisplayLimitTest.xml -new file mode 100644 -index 00000000000..09a5ce4c703 ---- /dev/null -+++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCheckCartItemDisplayWhenMoreItemsAddedToTheCartThanDefaultDisplayLimitTest.xml -@@ -0,0 +1,286 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> -+ <test name="StorefrontCheckCartItemDisplayWhenMoreItemsAddedToTheCartThanDefaultDisplayLimitTest"> -+ <annotations> -+ <stories value="Shopping Cart"/> -+ <title value="Add 11 Simple product to the cart and check cart display with default display limit"/> -+ <description value="Add 11 Simple product to the cart and check cart display with default display limit"/> -+ <testCaseId value="MC-14720"/> -+ <severity value="CRITICAL"/> -+ <group value="mtf_migrated"/> -+ <skip> -+ <issueId value="MC-17140"/> -+ </skip> -+ </annotations> -+ -+ <before> -+ <magentoCLI command="config:set {{EnableFlatRateConfigData.path}} {{EnableFlatRateConfigData.value}}" stepKey="enableFlatRate"/> -+ <magentoCLI command="config:set {{EnableFlatRateDefaultPriceConfigData.path}} {{EnableFlatRateDefaultPriceConfigData.value}}" stepKey="enableFlatRateDefaultPrice"/> -+ <!--Create simple product--> -+ <createData entity="SimpleProduct2" stepKey="simpleProduct1"> -+ <field key="price">10.00</field> -+ </createData> -+ <createData entity="SimpleProduct2" stepKey="simpleProduct2"> -+ <field key="price">20.00</field> -+ </createData> -+ <createData entity="SimpleProduct2" stepKey="simpleProduct3"> -+ <field key="price">30.00</field> -+ </createData> -+ <createData entity="SimpleProduct2" stepKey="simpleProduct4"> -+ <field key="price">40.00</field> -+ </createData> -+ <createData entity="SimpleProduct2" stepKey="simpleProduct5"> -+ <field key="price">50.00</field> -+ </createData> -+ <createData entity="SimpleProduct2" stepKey="simpleProduct6"> -+ <field key="price">60.00</field> -+ </createData> -+ <createData entity="SimpleProduct2" stepKey="simpleProduct7"> -+ <field key="price">70.00</field> -+ </createData> -+ <createData entity="SimpleProduct2" stepKey="simpleProduct8"> -+ <field key="price">80.00</field> -+ </createData> -+ <createData entity="SimpleProduct2" stepKey="simpleProduct9"> -+ <field key="price">90.00</field> -+ </createData> -+ <createData entity="SimpleProduct2" stepKey="simpleProduct10"> -+ <field key="price">100.00</field> -+ </createData> -+ <createData entity="SimpleProduct2" stepKey="simpleProduct11"> -+ <field key="price">110.00</field> -+ </createData> -+ <magentoCLI command="indexer:reindex" stepKey="reindex"/> -+ <magentoCLI command="cache:flush" stepKey="flushCache"/> -+ </before> -+ <after> -+ <deleteData createDataKey="simpleProduct1" stepKey="deleteProduct1"/> -+ <deleteData createDataKey="simpleProduct2" stepKey="deleteProduct2"/> -+ <deleteData createDataKey="simpleProduct3" stepKey="deleteProduct3"/> -+ <deleteData createDataKey="simpleProduct4" stepKey="deleteProduct4"/> -+ <deleteData createDataKey="simpleProduct5" stepKey="deleteProduct5"/> -+ <deleteData createDataKey="simpleProduct6" stepKey="deleteProduct6"/> -+ <deleteData createDataKey="simpleProduct7" stepKey="deleteProduct7"/> -+ <deleteData createDataKey="simpleProduct8" stepKey="deleteProduct8"/> -+ <deleteData createDataKey="simpleProduct9" stepKey="deleteProduct9"/> -+ <deleteData createDataKey="simpleProduct10" stepKey="deleteProduct10"/> -+ <deleteData createDataKey="simpleProduct11" stepKey="deleteProduct11"/> -+ </after> -+ -+ <!-- Open Product1 page in StoreFront --> -+ <actionGroup ref="AssertProductNameAndSkuInStorefrontProductPageByCustomAttributeUrlKey" stepKey="openProduct1PageAndVerifyProduct"> -+ <argument name="product" value="$$simpleProduct1$$"/> -+ </actionGroup> -+ -+ <!-- Add Product1 to the cart --> -+ <actionGroup ref="addToCartFromStorefrontProductPage" stepKey="addProduct1ToTheCart"> -+ <argument name="productName" value="$$simpleProduct1.name$$"/> -+ </actionGroup> -+ -+ <!-- Open Product2 page in StoreFront --> -+ <actionGroup ref="AssertProductNameAndSkuInStorefrontProductPageByCustomAttributeUrlKey" stepKey="openProduct2PageAndVerifyProduct"> -+ <argument name="product" value="$$simpleProduct2$$"/> -+ </actionGroup> -+ -+ <!-- Add Product2 to the cart --> -+ <actionGroup ref="addToCartFromStorefrontProductPage" stepKey="addProduct2ToTheCart"> -+ <argument name="productName" value="$$simpleProduct2.name$$"/> -+ </actionGroup> -+ -+ <!-- Open Product3 page in StoreFront --> -+ <actionGroup ref="AssertProductNameAndSkuInStorefrontProductPageByCustomAttributeUrlKey" stepKey="openProduct3PageAndVerifyProduct"> -+ <argument name="product" value="$$simpleProduct3$$"/> -+ </actionGroup> -+ -+ <!-- Add Product3 to the cart --> -+ <actionGroup ref="addToCartFromStorefrontProductPage" stepKey="addProduct3ToTheCart"> -+ <argument name="productName" value="$$simpleProduct3.name$$"/> -+ </actionGroup> -+ -+ <!-- Open Product4 page in StoreFront --> -+ <actionGroup ref="AssertProductNameAndSkuInStorefrontProductPageByCustomAttributeUrlKey" stepKey="openProduct4PageAndVerifyProduct"> -+ <argument name="product" value="$$simpleProduct4$$"/> -+ </actionGroup> -+ -+ <!-- Add Product4 to the cart --> -+ <actionGroup ref="addToCartFromStorefrontProductPage" stepKey="addProduct4ToTheCart"> -+ <argument name="productName" value="$$simpleProduct4.name$$"/> -+ </actionGroup> -+ -+ <!-- Open Product5 page in StoreFront --> -+ <actionGroup ref="AssertProductNameAndSkuInStorefrontProductPageByCustomAttributeUrlKey" stepKey="openProduct5PageAndVerifyProduct"> -+ <argument name="product" value="$$simpleProduct5$$"/> -+ </actionGroup> -+ -+ <!-- Add Product5 to the cart --> -+ <actionGroup ref="addToCartFromStorefrontProductPage" stepKey="addProduct5ToTheCart"> -+ <argument name="productName" value="$$simpleProduct5.name$$"/> -+ </actionGroup> -+ -+ <!-- Open Product6 page in StoreFront --> -+ <actionGroup ref="AssertProductNameAndSkuInStorefrontProductPageByCustomAttributeUrlKey" stepKey="openProduct6PageAndVerifyProduct"> -+ <argument name="product" value="$$simpleProduct6$$"/> -+ </actionGroup> -+ -+ <!-- Add Product6 to the cart --> -+ <actionGroup ref="addToCartFromStorefrontProductPage" stepKey="addProduct6ToTheCart"> -+ <argument name="productName" value="$$simpleProduct6.name$$"/> -+ </actionGroup> -+ -+ <!-- Open Product7 page in StoreFront --> -+ <actionGroup ref="AssertProductNameAndSkuInStorefrontProductPageByCustomAttributeUrlKey" stepKey="openProduct7PageAndVerifyProduct"> -+ <argument name="product" value="$$simpleProduct7$$"/> -+ </actionGroup> -+ -+ <!-- Add Product7 to the cart --> -+ <actionGroup ref="addToCartFromStorefrontProductPage" stepKey="addProduct7ToTheCart"> -+ <argument name="productName" value="$$simpleProduct7.name$$"/> -+ </actionGroup> -+ -+ <!-- Open Product8 page in StoreFront --> -+ <actionGroup ref="AssertProductNameAndSkuInStorefrontProductPageByCustomAttributeUrlKey" stepKey="openProduct8PageAndVerifyProduct"> -+ <argument name="product" value="$$simpleProduct8$$"/> -+ </actionGroup> -+ -+ <!-- Add Product8 to the cart --> -+ <actionGroup ref="addToCartFromStorefrontProductPage" stepKey="addProduct8ToTheCart"> -+ <argument name="productName" value="$$simpleProduct8.name$$"/> -+ </actionGroup> -+ -+ <!-- Open Product9 page in StoreFront --> -+ <actionGroup ref="AssertProductNameAndSkuInStorefrontProductPageByCustomAttributeUrlKey" stepKey="openProduct9PageAndVerifyProduct"> -+ <argument name="product" value="$$simpleProduct9$$"/> -+ </actionGroup> -+ -+ <!-- Add Product9 to the cart --> -+ <actionGroup ref="addToCartFromStorefrontProductPage" stepKey="addProduct9ToTheCart"> -+ <argument name="productName" value="$$simpleProduct9.name$$"/> -+ </actionGroup> -+ -+ <!-- Open Product10 page in StoreFront --> -+ <actionGroup ref="AssertProductNameAndSkuInStorefrontProductPageByCustomAttributeUrlKey" stepKey="openProductPage10AndVerifyProduct"> -+ <argument name="product" value="$$simpleProduct10$$"/> -+ </actionGroup> -+ -+ <!-- Add Product10 to the cart --> -+ <actionGroup ref="addToCartFromStorefrontProductPage" stepKey="addProduct10ToTheCart"> -+ <argument name="productName" value="$$simpleProduct10.name$$"/> -+ </actionGroup> -+ -+ <!-- Open Product11 page in StoreFront --> -+ <actionGroup ref="AssertProductNameAndSkuInStorefrontProductPageByCustomAttributeUrlKey" stepKey="openProductPage11AndVerifyProduct"> -+ <argument name="product" value="$$simpleProduct11$$"/> -+ </actionGroup> -+ -+ <!-- Add Product11 to the cart --> -+ <actionGroup ref="addToCartFromStorefrontProductPage" stepKey="addProduct11ToTheCart"> -+ <argument name="productName" value="$$simpleProduct11.name$$"/> -+ </actionGroup> -+ -+ -+ <!-- Assert Product details in Mini Cart --> -+ <actionGroup ref="StorefrontClickOnMiniCartActionGroup" stepKey="clickOnMiniCart"/> -+ <actionGroup ref="AssertStorefrontElementVisibleActionGroup" stepKey="seeProductCountInMiniCart"> -+ <argument name="selector" value="{{StorefrontMinicartSection.productCount}}"/> -+ <argument name="userInput" value="11"/> -+ </actionGroup> -+ <actionGroup ref="AssertStorefrontElementVisibleActionGroup" stepKey="seeTotalNumberOfItemDisplayed"> -+ <argument name="selector" value="{{StorefrontMinicartSection.visibleItemsCountText}}"/> -+ <argument name="userInput" value="10 of 11 Items in Cart"/> -+ </actionGroup> -+ <actionGroup ref="AssertStorefrontSeeElementActionGroup" stepKey="seeViewAndEditLink"> -+ <argument name="selector" value="{{StorefrontMinicartSection.viewAndEditCart}}"/> -+ </actionGroup> -+ -+ <!-- Assert Product1 in Mini Cart --> -+ <actionGroup ref="AssertStorefrontMiniCartItemsActionGroup" stepKey="assertSimpleProduct1InMiniCart"> -+ <argument name="productName" value="$$simpleProduct1.name$$"/> -+ <argument name="productPrice" value="$10.00"/> -+ <argument name="cartSubtotal" value="$660.00" /> -+ <argument name="qty" value="1"/> -+ </actionGroup> -+ -+ <!-- Assert Product2 in Mini Cart --> -+ <actionGroup ref="AssertStorefrontMiniCartItemsActionGroup" stepKey="assertSimpleProduct2InMiniCart"> -+ <argument name="productName" value="$$simpleProduct2.name$$"/> -+ <argument name="productPrice" value="$20.00"/> -+ <argument name="cartSubtotal" value="$660.00" /> -+ <argument name="qty" value="1"/> -+ </actionGroup> -+ -+ <!-- Assert Product3 in Mini Cart --> -+ <actionGroup ref="AssertStorefrontMiniCartItemsActionGroup" stepKey="assertSimpleProduct3MiniCart"> -+ <argument name="productName" value="$$simpleProduct3.name$$"/> -+ <argument name="productPrice" value="$30.00"/> -+ <argument name="cartSubtotal" value="$660.00" /> -+ <argument name="qty" value="1"/> -+ </actionGroup> -+ -+ <!-- Assert Product4 in Mini Cart --> -+ <actionGroup ref="AssertStorefrontMiniCartItemsActionGroup" stepKey="assertSimpleProduct4InMiniCart"> -+ <argument name="productName" value="$$simpleProduct4.name$$"/> -+ <argument name="productPrice" value="$40.00"/> -+ <argument name="cartSubtotal" value="$660.00" /> -+ <argument name="qty" value="1"/> -+ </actionGroup> -+ -+ <!-- Assert Product5 in Mini Cart --> -+ <actionGroup ref="AssertStorefrontMiniCartItemsActionGroup" stepKey="assertSimpleProduct5InMiniCart"> -+ <argument name="productName" value="$$simpleProduct5.name$$"/> -+ <argument name="productPrice" value="$50.00"/> -+ <argument name="cartSubtotal" value="$660.00" /> -+ <argument name="qty" value="1"/> -+ </actionGroup> -+ -+ <!-- Assert Product6 in Mini Cart --> -+ <actionGroup ref="AssertStorefrontMiniCartItemsActionGroup" stepKey="assertSimpleProduct6InMiniCart"> -+ <argument name="productName" value="$$simpleProduct6.name$$"/> -+ <argument name="productPrice" value="$60.00"/> -+ <argument name="cartSubtotal" value="$660.00" /> -+ <argument name="qty" value="1"/> -+ </actionGroup> -+ -+ <!-- Assert Product7 in Mini Cart --> -+ <actionGroup ref="AssertStorefrontMiniCartItemsActionGroup" stepKey="assertSimpleProduct7InMiniCart"> -+ <argument name="productName" value="$$simpleProduct7.name$$"/> -+ <argument name="productPrice" value="$70.00"/> -+ <argument name="cartSubtotal" value="$660.00" /> -+ <argument name="qty" value="1"/> -+ </actionGroup> -+ -+ <!-- Assert Product8 in Mini Cart --> -+ <actionGroup ref="AssertStorefrontMiniCartItemsActionGroup" stepKey="assertSimpleProduct8InMiniCart"> -+ <argument name="productName" value="$$simpleProduct8.name$$"/> -+ <argument name="productPrice" value="$80.00"/> -+ <argument name="cartSubtotal" value="$660.00" /> -+ <argument name="qty" value="1"/> -+ </actionGroup> -+ -+ <!-- Assert Product9 in Mini Cart --> -+ <actionGroup ref="AssertStorefrontMiniCartItemsActionGroup" stepKey="assertSimpleProduct9InMiniCart"> -+ <argument name="productName" value="$$simpleProduct9.name$$"/> -+ <argument name="productPrice" value="$90.00"/> -+ <argument name="cartSubtotal" value="$660.00" /> -+ <argument name="qty" value="1"/> -+ </actionGroup> -+ -+ <!-- Assert Product10 in Mini Cart --> -+ <actionGroup ref="AssertStorefrontMiniCartItemsActionGroup" stepKey="assertSimpleProduct10InMiniCart"> -+ <argument name="productName" value="$$simpleProduct10.name$$"/> -+ <argument name="productPrice" value="$100.00"/> -+ <argument name="cartSubtotal" value="$660.00" /> -+ <argument name="qty" value="1"/> -+ </actionGroup> -+ -+ <!-- Verify Product11 is not displayed in Mini Cart --> -+ <dontSee selector="{{StorefrontMinicartSection.miniCartItemsText}}" userInput="$$simpleProduct11.name$$" stepKey="dontSeeProduct11NameInMiniCart"/> -+ <dontSee selector="{{StorefrontMinicartSection.miniCartItemsText}}" userInput="110" stepKey="dontSeeProduct11PriceInMiniCart"/> -+ </test> -+</tests> -diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCheckCartItemDisplayWithDefaultDisplayLimitAndDefaultTotalQuantityTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCheckCartItemDisplayWithDefaultDisplayLimitAndDefaultTotalQuantityTest.xml -new file mode 100644 -index 00000000000..2339789bd85 ---- /dev/null -+++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCheckCartItemDisplayWithDefaultDisplayLimitAndDefaultTotalQuantityTest.xml -@@ -0,0 +1,256 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> -+ <test name="StorefrontCheckCartItemDisplayWithDefaultDisplayLimitAndDefaultTotalQuantityTest"> -+ <annotations> -+ <stories value="Shopping Cart"/> -+ <title value="Add 10 items to the cart and check cart display with default display limit"/> -+ <description value="Add 10 items to the cart and check cart display with default display limit"/> -+ <testCaseId value="MC-14721"/> -+ <severity value="CRITICAL"/> -+ <group value="mtf_migrated"/> -+ </annotations> -+ -+ <before> -+ <!--Create simple product--> -+ <createData entity="SimpleProduct2" stepKey="simpleProduct1"> -+ <field key="price">10.00</field> -+ </createData> -+ <createData entity="SimpleProduct2" stepKey="simpleProduct2"> -+ <field key="price">20.00</field> -+ </createData> -+ <createData entity="SimpleProduct2" stepKey="simpleProduct3"> -+ <field key="price">30.00</field> -+ </createData> -+ <createData entity="SimpleProduct2" stepKey="simpleProduct4"> -+ <field key="price">40.00</field> -+ </createData> -+ <createData entity="SimpleProduct2" stepKey="simpleProduct5"> -+ <field key="price">50.00</field> -+ </createData> -+ <createData entity="SimpleProduct2" stepKey="simpleProduct6"> -+ <field key="price">60.00</field> -+ </createData> -+ <createData entity="SimpleProduct2" stepKey="simpleProduct7"> -+ <field key="price">70.00</field> -+ </createData> -+ <createData entity="SimpleProduct2" stepKey="simpleProduct8"> -+ <field key="price">80.00</field> -+ </createData> -+ <createData entity="SimpleProduct2" stepKey="simpleProduct9"> -+ <field key="price">90.00</field> -+ </createData> -+ <createData entity="SimpleProduct2" stepKey="simpleProduct10"> -+ <field key="price">100.00</field> -+ </createData> -+ <magentoCLI command="cache:flush" stepKey="flushCache"/> -+ </before> -+ <after> -+ <deleteData createDataKey="simpleProduct1" stepKey="deleteProduct1"/> -+ <deleteData createDataKey="simpleProduct2" stepKey="deleteProduct2"/> -+ <deleteData createDataKey="simpleProduct3" stepKey="deleteProduct3"/> -+ <deleteData createDataKey="simpleProduct4" stepKey="deleteProduct4"/> -+ <deleteData createDataKey="simpleProduct5" stepKey="deleteProduct5"/> -+ <deleteData createDataKey="simpleProduct6" stepKey="deleteProduct6"/> -+ <deleteData createDataKey="simpleProduct7" stepKey="deleteProduct7"/> -+ <deleteData createDataKey="simpleProduct8" stepKey="deleteProduct8"/> -+ <deleteData createDataKey="simpleProduct9" stepKey="deleteProduct9"/> -+ <deleteData createDataKey="simpleProduct10" stepKey="deleteProduct10"/> -+ </after> -+ -+ <!-- Open Product1 page in StoreFront --> -+ <actionGroup ref="AssertProductNameAndSkuInStorefrontProductPageByCustomAttributeUrlKey" stepKey="openProduct1PageAndVerifyProduct"> -+ <argument name="product" value="$$simpleProduct1$$"/> -+ </actionGroup> -+ -+ <!-- Add Product1 to the cart --> -+ <actionGroup ref="addToCartFromStorefrontProductPage" stepKey="addProduct1ToTheCart"> -+ <argument name="productName" value="$$simpleProduct1.name$$"/> -+ </actionGroup> -+ -+ <!-- Open Product2 page in StoreFront --> -+ <actionGroup ref="AssertProductNameAndSkuInStorefrontProductPageByCustomAttributeUrlKey" stepKey="openProduct2PageAndVerifyProduct"> -+ <argument name="product" value="$$simpleProduct2$$"/> -+ </actionGroup> -+ -+ <!-- Add Product2 to the cart --> -+ <actionGroup ref="addToCartFromStorefrontProductPage" stepKey="addProduct2ToTheCart"> -+ <argument name="productName" value="$$simpleProduct2.name$$"/> -+ </actionGroup> -+ -+ <!-- Open Product3 page in StoreFront --> -+ <actionGroup ref="AssertProductNameAndSkuInStorefrontProductPageByCustomAttributeUrlKey" stepKey="openProduct3PageAndVerifyProduct"> -+ <argument name="product" value="$$simpleProduct3$$"/> -+ </actionGroup> -+ -+ <!-- Add Product3 to the cart --> -+ <actionGroup ref="addToCartFromStorefrontProductPage" stepKey="addProduct3ToTheCart"> -+ <argument name="productName" value="$$simpleProduct3.name$$"/> -+ </actionGroup> -+ -+ <!-- Open Product4 page in StoreFront --> -+ <actionGroup ref="AssertProductNameAndSkuInStorefrontProductPageByCustomAttributeUrlKey" stepKey="openProduct4PageAndVerifyProduct"> -+ <argument name="product" value="$$simpleProduct4$$"/> -+ </actionGroup> -+ -+ <!-- Add Product4 to the cart --> -+ <actionGroup ref="addToCartFromStorefrontProductPage" stepKey="addProduct4ToTheCart"> -+ <argument name="productName" value="$$simpleProduct4.name$$"/> -+ </actionGroup> -+ -+ <!-- Open Product5 page in StoreFront --> -+ <actionGroup ref="AssertProductNameAndSkuInStorefrontProductPageByCustomAttributeUrlKey" stepKey="openProduct5PageAndVerifyProduct"> -+ <argument name="product" value="$$simpleProduct5$$"/> -+ </actionGroup> -+ -+ <!-- Add Product5 to the cart --> -+ <actionGroup ref="addToCartFromStorefrontProductPage" stepKey="addProduct5ToTheCart"> -+ <argument name="productName" value="$$simpleProduct5.name$$"/> -+ </actionGroup> -+ -+ <!-- Open Product6 page in StoreFront --> -+ <actionGroup ref="AssertProductNameAndSkuInStorefrontProductPageByCustomAttributeUrlKey" stepKey="openProduct6PageAndVerifyProduct"> -+ <argument name="product" value="$$simpleProduct6$$"/> -+ </actionGroup> -+ -+ <!-- Add Product6 to the cart --> -+ <actionGroup ref="addToCartFromStorefrontProductPage" stepKey="addProduct6ToTheCart"> -+ <argument name="productName" value="$$simpleProduct6.name$$"/> -+ </actionGroup> -+ -+ <!-- Open Product7 page in StoreFront --> -+ <actionGroup ref="AssertProductNameAndSkuInStorefrontProductPageByCustomAttributeUrlKey" stepKey="openProduct7PageAndVerifyProduct"> -+ <argument name="product" value="$$simpleProduct7$$"/> -+ </actionGroup> -+ -+ <!-- Add Product7 to the cart --> -+ <actionGroup ref="addToCartFromStorefrontProductPage" stepKey="addProduct7ToTheCart"> -+ <argument name="productName" value="$$simpleProduct7.name$$"/> -+ </actionGroup> -+ -+ <!-- Open Product8 page in StoreFront --> -+ <actionGroup ref="AssertProductNameAndSkuInStorefrontProductPageByCustomAttributeUrlKey" stepKey="openProduct8PageAndVerifyProduct"> -+ <argument name="product" value="$$simpleProduct8$$"/> -+ </actionGroup> -+ -+ <!-- Add Product8 to the cart --> -+ <actionGroup ref="addToCartFromStorefrontProductPage" stepKey="addProduct8ToTheCart"> -+ <argument name="productName" value="$$simpleProduct8.name$$"/> -+ </actionGroup> -+ -+ <!-- Open Product9 page in StoreFront --> -+ <actionGroup ref="AssertProductNameAndSkuInStorefrontProductPageByCustomAttributeUrlKey" stepKey="openProduct9PageAndVerifyProduct"> -+ <argument name="product" value="$$simpleProduct9$$"/> -+ </actionGroup> -+ -+ <!-- Add Product9 to the cart --> -+ <actionGroup ref="addToCartFromStorefrontProductPage" stepKey="addProduct9ToTheCart"> -+ <argument name="productName" value="$$simpleProduct9.name$$"/> -+ </actionGroup> -+ -+ <!-- Open Product10 page in StoreFront --> -+ <actionGroup ref="AssertProductNameAndSkuInStorefrontProductPageByCustomAttributeUrlKey" stepKey="openProductPage10AndVerifyProduct"> -+ <argument name="product" value="$$simpleProduct10$$"/> -+ </actionGroup> -+ -+ <!-- Add Product10 to the cart --> -+ <actionGroup ref="addToCartFromStorefrontProductPage" stepKey="addProduct10ToTheCart"> -+ <argument name="productName" value="$$simpleProduct10.name$$"/> -+ </actionGroup> -+ -+ <!-- Open Mini Cart --> -+ <actionGroup ref="StorefrontOpenMiniCartActionGroup" stepKey="openMiniCart"/> -+ -+ <!-- Assert Product Count in Mini Cart --> -+ <actionGroup ref="StorefrontAssertMiniCartItemCountActionGroup" stepKey="assertProductCountAndTextInMiniCart"> -+ <argument name="productCount" value="10"/> -+ <argument name="productCountText" value="10 Items in Cart"/> -+ </actionGroup> -+ -+ <!-- Assert Product1 in Mini Cart --> -+ <actionGroup ref="AssertStorefrontMiniCartItemsActionGroup" stepKey="assertSimpleProduct11MiniCart"> -+ <argument name="productName" value="$$simpleProduct1.name$$"/> -+ <argument name="productPrice" value="$10.00"/> -+ <argument name="cartSubtotal" value="$550.00" /> -+ <argument name="qty" value="1"/> -+ </actionGroup> -+ -+ <!-- Assert Product2 in Mini Cart --> -+ <actionGroup ref="AssertStorefrontMiniCartItemsActionGroup" stepKey="assertSimpleProduct2MiniCart"> -+ <argument name="productName" value="$$simpleProduct2.name$$"/> -+ <argument name="productPrice" value="$20.00"/> -+ <argument name="cartSubtotal" value="$550.00" /> -+ <argument name="qty" value="1"/> -+ </actionGroup> -+ -+ <!-- Assert Product3 in Mini Cart --> -+ <actionGroup ref="AssertStorefrontMiniCartItemsActionGroup" stepKey="assertSimpleProduct3MiniCart"> -+ <argument name="productName" value="$$simpleProduct3.name$$"/> -+ <argument name="productPrice" value="$30.00"/> -+ <argument name="cartSubtotal" value="$550.00" /> -+ <argument name="qty" value="1"/> -+ </actionGroup> -+ -+ <!-- Assert Product4 in Mini Cart --> -+ <actionGroup ref="AssertStorefrontMiniCartItemsActionGroup" stepKey="assertSimpleProduct4MiniCart"> -+ <argument name="productName" value="$$simpleProduct4.name$$"/> -+ <argument name="productPrice" value="$40.00"/> -+ <argument name="cartSubtotal" value="$550.00" /> -+ <argument name="qty" value="1"/> -+ </actionGroup> -+ -+ <!-- Assert Product5 in Mini Cart --> -+ <actionGroup ref="AssertStorefrontMiniCartItemsActionGroup" stepKey="assertSimpleProduct5MiniCart"> -+ <argument name="productName" value="$$simpleProduct5.name$$"/> -+ <argument name="productPrice" value="$50.00"/> -+ <argument name="cartSubtotal" value="$550.00" /> -+ <argument name="qty" value="1"/> -+ </actionGroup> -+ -+ <!-- Assert Product6 in Mini Cart --> -+ <actionGroup ref="AssertStorefrontMiniCartItemsActionGroup" stepKey="assertSimpleProduct6MiniCart"> -+ <argument name="productName" value="$$simpleProduct6.name$$"/> -+ <argument name="productPrice" value="$60.00"/> -+ <argument name="cartSubtotal" value="$550.00" /> -+ <argument name="qty" value="1"/> -+ </actionGroup> -+ -+ <!-- Assert Product7 in Mini Cart --> -+ <actionGroup ref="AssertStorefrontMiniCartItemsActionGroup" stepKey="assertSimpleProduct7MiniCart"> -+ <argument name="productName" value="$$simpleProduct7.name$$"/> -+ <argument name="productPrice" value="$70.00"/> -+ <argument name="cartSubtotal" value="$550.00" /> -+ <argument name="qty" value="1"/> -+ </actionGroup> -+ -+ <!-- Assert Product8 in Mini Cart --> -+ <actionGroup ref="AssertStorefrontMiniCartItemsActionGroup" stepKey="assertSimpleProduct8MiniCart"> -+ <argument name="productName" value="$$simpleProduct8.name$$"/> -+ <argument name="productPrice" value="$80.00"/> -+ <argument name="cartSubtotal" value="$550.00" /> -+ <argument name="qty" value="1"/> -+ </actionGroup> -+ -+ <!-- Assert Product9 in Mini Cart --> -+ <actionGroup ref="AssertStorefrontMiniCartItemsActionGroup" stepKey="assertSimpleProduct9MiniCart"> -+ <argument name="productName" value="$$simpleProduct9.name$$"/> -+ <argument name="productPrice" value="$90.00"/> -+ <argument name="cartSubtotal" value="$550.00" /> -+ <argument name="qty" value="1"/> -+ </actionGroup> -+ -+ <!-- Assert Product10 in Mini Cart --> -+ <actionGroup ref="AssertStorefrontMiniCartItemsActionGroup" stepKey="assertSimpleProduct10MiniCart"> -+ <argument name="productName" value="$$simpleProduct10.name$$"/> -+ <argument name="productPrice" value="$100.00"/> -+ <argument name="cartSubtotal" value="$550.00" /> -+ <argument name="qty" value="1"/> -+ </actionGroup> -+ </test> -+</tests> -diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCheckSimpleProductCartItemDisplayWithDefaultLimitationTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCheckSimpleProductCartItemDisplayWithDefaultLimitationTest.xml -new file mode 100644 -index 00000000000..084c89312cc ---- /dev/null -+++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCheckSimpleProductCartItemDisplayWithDefaultLimitationTest.xml -@@ -0,0 +1,357 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> -+ <test name="StorefrontCheckSimpleProductCartItemDisplayWithDefaultLimitationTest"> -+ <annotations> -+ <stories value="Shopping Cart"/> -+ <title value="Add 10 SimpleProducts to the cart and check cart display with default display limit"/> -+ <description value="Add 10 SimpleProducts to the cart and check cart display with default display limit"/> -+ <testCaseId value="MC-14723"/> -+ <severity value="CRITICAL"/> -+ <group value="mtf_migrated"/> -+ </annotations> -+ -+ <before> -+ <magentoCLI command="config:set {{EnableFlatRateConfigData.path}} {{EnableFlatRateConfigData.value}}" stepKey="enableFlatRate"/> -+ <magentoCLI command="config:set {{EnableFlatRateDefaultPriceConfigData.path}} {{EnableFlatRateDefaultPriceConfigData.value}}" stepKey="enableFlatRateDefaultPrice"/> -+ <!--Create simple product--> -+ <createData entity="SimpleProduct2" stepKey="simpleProduct1"> -+ <field key="price">10.00</field> -+ </createData> -+ <createData entity="SimpleProduct2" stepKey="simpleProduct2"> -+ <field key="price">20.00</field> -+ </createData> -+ <createData entity="SimpleProduct2" stepKey="simpleProduct3"> -+ <field key="price">30.00</field> -+ </createData> -+ <createData entity="SimpleProduct2" stepKey="simpleProduct4"> -+ <field key="price">40.00</field> -+ </createData> -+ <createData entity="SimpleProduct2" stepKey="simpleProduct5"> -+ <field key="price">50.00</field> -+ </createData> -+ <createData entity="SimpleProduct2" stepKey="simpleProduct6"> -+ <field key="price">60.00</field> -+ </createData> -+ <createData entity="SimpleProduct2" stepKey="simpleProduct7"> -+ <field key="price">70.00</field> -+ </createData> -+ <createData entity="SimpleProduct2" stepKey="simpleProduct8"> -+ <field key="price">80.00</field> -+ </createData> -+ <createData entity="SimpleProduct2" stepKey="simpleProduct9"> -+ <field key="price">90.00</field> -+ </createData> -+ <createData entity="SimpleProduct2" stepKey="simpleProduct10"> -+ <field key="price">100.00</field> -+ </createData> -+ <createData entity="SimpleProduct2" stepKey="simpleProduct11"> -+ <field key="price">110.00</field> -+ </createData> -+ <magentoCLI command="cache:flush" stepKey="flushCache"/> -+ </before> -+ <after> -+ <deleteData createDataKey="simpleProduct1" stepKey="deleteProduct1"/> -+ <deleteData createDataKey="simpleProduct2" stepKey="deleteProduct2"/> -+ <deleteData createDataKey="simpleProduct3" stepKey="deleteProduct3"/> -+ <deleteData createDataKey="simpleProduct4" stepKey="deleteProduct4"/> -+ <deleteData createDataKey="simpleProduct5" stepKey="deleteProduct5"/> -+ <deleteData createDataKey="simpleProduct6" stepKey="deleteProduct6"/> -+ <deleteData createDataKey="simpleProduct7" stepKey="deleteProduct7"/> -+ <deleteData createDataKey="simpleProduct8" stepKey="deleteProduct8"/> -+ <deleteData createDataKey="simpleProduct9" stepKey="deleteProduct9"/> -+ <deleteData createDataKey="simpleProduct10" stepKey="deleteProduct10"/> -+ <deleteData createDataKey="simpleProduct11" stepKey="deleteProduct11"/> -+ </after> -+ -+ <!-- Open Product1 page in StoreFront --> -+ <actionGroup ref="AssertProductNameAndSkuInStorefrontProductPageByCustomAttributeUrlKey" stepKey="openProduct1PageAndVerifyProduct"> -+ <argument name="product" value="$$simpleProduct1$$"/> -+ </actionGroup> -+ -+ <!-- Add Product1 to the cart --> -+ <actionGroup ref="addToCartFromStorefrontProductPage" stepKey="addProduct1ToTheCart"> -+ <argument name="productName" value="$$simpleProduct1.name$$"/> -+ </actionGroup> -+ -+ <!-- Open Product2 page in StoreFront --> -+ <actionGroup ref="AssertProductNameAndSkuInStorefrontProductPageByCustomAttributeUrlKey" stepKey="openProduct2PageAndVerifyProduct"> -+ <argument name="product" value="$$simpleProduct2$$"/> -+ </actionGroup> -+ -+ <!-- Add Product2 to the cart --> -+ <actionGroup ref="addToCartFromStorefrontProductPage" stepKey="addProduct2ToTheCart"> -+ <argument name="productName" value="$$simpleProduct2.name$$"/> -+ </actionGroup> -+ -+ <!-- Open Product3 page in StoreFront --> -+ <actionGroup ref="AssertProductNameAndSkuInStorefrontProductPageByCustomAttributeUrlKey" stepKey="openProduct3PageAndVerifyProduct"> -+ <argument name="product" value="$$simpleProduct3$$"/> -+ </actionGroup> -+ -+ <!-- Add Product3 to the cart --> -+ <actionGroup ref="addToCartFromStorefrontProductPage" stepKey="addProduct3ToTheCart"> -+ <argument name="productName" value="$$simpleProduct3.name$$"/> -+ </actionGroup> -+ -+ <!-- Open Product4 page in StoreFront --> -+ <actionGroup ref="AssertProductNameAndSkuInStorefrontProductPageByCustomAttributeUrlKey" stepKey="openProduct4PageAndVerifyProduct"> -+ <argument name="product" value="$$simpleProduct4$$"/> -+ </actionGroup> -+ -+ <!-- Add Product4 to the cart --> -+ <actionGroup ref="addToCartFromStorefrontProductPage" stepKey="addProduct4ToTheCart"> -+ <argument name="productName" value="$$simpleProduct4.name$$"/> -+ </actionGroup> -+ -+ <!-- Open Product5 page in StoreFront --> -+ <actionGroup ref="AssertProductNameAndSkuInStorefrontProductPageByCustomAttributeUrlKey" stepKey="openProduct5PageAndVerifyProduct"> -+ <argument name="product" value="$$simpleProduct5$$"/> -+ </actionGroup> -+ -+ <!-- Add Product5 to the cart --> -+ <actionGroup ref="addToCartFromStorefrontProductPage" stepKey="addProduct5ToTheCart"> -+ <argument name="productName" value="$$simpleProduct5.name$$"/> -+ </actionGroup> -+ -+ <!-- Open Product6 page in StoreFront --> -+ <actionGroup ref="AssertProductNameAndSkuInStorefrontProductPageByCustomAttributeUrlKey" stepKey="openProduct6PageAndVerifyProduct"> -+ <argument name="product" value="$$simpleProduct6$$"/> -+ </actionGroup> -+ -+ <!-- Add Product6 to the cart --> -+ <actionGroup ref="addToCartFromStorefrontProductPage" stepKey="addProduct6ToTheCart"> -+ <argument name="productName" value="$$simpleProduct6.name$$"/> -+ </actionGroup> -+ -+ <!-- Open Product7 page in StoreFront --> -+ <actionGroup ref="AssertProductNameAndSkuInStorefrontProductPageByCustomAttributeUrlKey" stepKey="openProduct7PageAndVerifyProduct"> -+ <argument name="product" value="$$simpleProduct7$$"/> -+ </actionGroup> -+ -+ <!-- Add Product7 to the cart --> -+ <actionGroup ref="addToCartFromStorefrontProductPage" stepKey="addProduct7ToTheCart"> -+ <argument name="productName" value="$$simpleProduct7.name$$"/> -+ </actionGroup> -+ -+ <!-- Open Product8 page in StoreFront --> -+ <actionGroup ref="AssertProductNameAndSkuInStorefrontProductPageByCustomAttributeUrlKey" stepKey="openProduct8PageAndVerifyProduct"> -+ <argument name="product" value="$$simpleProduct8$$"/> -+ </actionGroup> -+ -+ <!-- Add Product8 to the cart --> -+ <actionGroup ref="addToCartFromStorefrontProductPage" stepKey="addProduct8ToTheCart"> -+ <argument name="productName" value="$$simpleProduct8.name$$"/> -+ </actionGroup> -+ -+ <!-- Open Product9 page in StoreFront --> -+ <actionGroup ref="AssertProductNameAndSkuInStorefrontProductPageByCustomAttributeUrlKey" stepKey="openProduct9PageAndVerifyProduct"> -+ <argument name="product" value="$$simpleProduct9$$"/> -+ </actionGroup> -+ -+ <!-- Add Product9 to the cart --> -+ <actionGroup ref="addToCartFromStorefrontProductPage" stepKey="addProduct9ToTheCart"> -+ <argument name="productName" value="$$simpleProduct9.name$$"/> -+ </actionGroup> -+ -+ <!-- Open Product10 page in StoreFront --> -+ <actionGroup ref="AssertProductNameAndSkuInStorefrontProductPageByCustomAttributeUrlKey" stepKey="openProductPage10AndVerifyProduct"> -+ <argument name="product" value="$$simpleProduct10$$"/> -+ </actionGroup> -+ -+ <!-- Add Product10 to the cart --> -+ <actionGroup ref="addToCartFromStorefrontProductPage" stepKey="addProduct10ToTheCart"> -+ <argument name="productName" value="$$simpleProduct10.name$$"/> -+ </actionGroup> -+ -+ <!-- Open Product11 page in StoreFront --> -+ <actionGroup ref="AssertProductNameAndSkuInStorefrontProductPageByCustomAttributeUrlKey" stepKey="openProductPage11AndVerifyProduct"> -+ <argument name="product" value="$$simpleProduct11$$"/> -+ </actionGroup> -+ -+ <!-- Add Product11 to the cart --> -+ <actionGroup ref="addToCartFromStorefrontProductPage" stepKey="addProduct11ToTheCart"> -+ <argument name="productName" value="$$simpleProduct11.name$$"/> -+ </actionGroup> -+ -+ <!-- Open Mini Cart --> -+ <actionGroup ref="StorefrontOpenMiniCartActionGroup" stepKey="openMiniCart"/> -+ -+ <!-- Assert Product Count in Mini Cart --> -+ <actionGroup ref="StorefrontAssertMiniCartItemCountActionGroup" stepKey="assertProductCountAndTextInMiniCart"> -+ <argument name="productCount" value="11"/> -+ <argument name="productCountText" value="10 of 11 Items in Cart"/> -+ </actionGroup> -+ -+ <!-- Assert Product1 in Mini Cart --> -+ <actionGroup ref="AssertStorefrontMiniCartItemsActionGroup" stepKey="assertSimpleProduct11MiniCart"> -+ <argument name="productName" value="$$simpleProduct1.name$$"/> -+ <argument name="productPrice" value="$10.00"/> -+ <argument name="cartSubtotal" value="$660.00" /> -+ <argument name="qty" value="1"/> -+ </actionGroup> -+ -+ <!-- Assert Product2 in Mini Cart --> -+ <actionGroup ref="AssertStorefrontMiniCartItemsActionGroup" stepKey="assertSimpleProduct2MiniCart"> -+ <argument name="productName" value="$$simpleProduct2.name$$"/> -+ <argument name="productPrice" value="$20.00"/> -+ <argument name="cartSubtotal" value="$660.00" /> -+ <argument name="qty" value="1"/> -+ </actionGroup> -+ -+ <!-- Assert Product3 in Mini Cart --> -+ <actionGroup ref="AssertStorefrontMiniCartItemsActionGroup" stepKey="assertSimpleProduct3MiniCart"> -+ <argument name="productName" value="$$simpleProduct3.name$$"/> -+ <argument name="productPrice" value="$30.00"/> -+ <argument name="cartSubtotal" value="$660.00" /> -+ <argument name="qty" value="1"/> -+ </actionGroup> -+ -+ <!-- Assert Product4 in Mini Cart --> -+ <actionGroup ref="AssertStorefrontMiniCartItemsActionGroup" stepKey="assertSimpleProduct4MiniCart"> -+ <argument name="productName" value="$$simpleProduct4.name$$"/> -+ <argument name="productPrice" value="$40.00"/> -+ <argument name="cartSubtotal" value="$660.00" /> -+ <argument name="qty" value="1"/> -+ </actionGroup> -+ -+ <!-- Assert Product5 in Mini Cart --> -+ <actionGroup ref="AssertStorefrontMiniCartItemsActionGroup" stepKey="assertSimpleProduct5MiniCart"> -+ <argument name="productName" value="$$simpleProduct5.name$$"/> -+ <argument name="productPrice" value="$50.00"/> -+ <argument name="cartSubtotal" value="$660.00" /> -+ <argument name="qty" value="1"/> -+ </actionGroup> -+ -+ <!-- Assert Product6 in Mini Cart --> -+ <actionGroup ref="AssertStorefrontMiniCartItemsActionGroup" stepKey="assertSimpleProduct6MiniCart"> -+ <argument name="productName" value="$$simpleProduct6.name$$"/> -+ <argument name="productPrice" value="$60.00"/> -+ <argument name="cartSubtotal" value="$660.00" /> -+ <argument name="qty" value="1"/> -+ </actionGroup> -+ -+ <!-- Assert Product7 in Mini Cart --> -+ <actionGroup ref="AssertStorefrontMiniCartItemsActionGroup" stepKey="assertSimpleProduct7MiniCart"> -+ <argument name="productName" value="$$simpleProduct7.name$$"/> -+ <argument name="productPrice" value="$70.00"/> -+ <argument name="cartSubtotal" value="$660.00" /> -+ <argument name="qty" value="1"/> -+ </actionGroup> -+ -+ <!-- Assert Product8 in Mini Cart --> -+ <actionGroup ref="AssertStorefrontMiniCartItemsActionGroup" stepKey="assertSimpleProduct8MiniCart"> -+ <argument name="productName" value="$$simpleProduct8.name$$"/> -+ <argument name="productPrice" value="$80.00"/> -+ <argument name="cartSubtotal" value="$660.00" /> -+ <argument name="qty" value="1"/> -+ </actionGroup> -+ -+ <!-- Assert Product9 in Mini Cart --> -+ <actionGroup ref="AssertStorefrontMiniCartItemsActionGroup" stepKey="assertSimpleProduct9MiniCart"> -+ <argument name="productName" value="$$simpleProduct9.name$$"/> -+ <argument name="productPrice" value="$90.00"/> -+ <argument name="cartSubtotal" value="$660.00" /> -+ <argument name="qty" value="1"/> -+ </actionGroup> -+ -+ <!-- Assert Product10 in Mini Cart --> -+ <actionGroup ref="AssertStorefrontMiniCartItemsActionGroup" stepKey="assertSimpleProduct10MiniCart"> -+ <argument name="productName" value="$$simpleProduct10.name$$"/> -+ <argument name="productPrice" value="$100.00"/> -+ <argument name="cartSubtotal" value="$660.00" /> -+ <argument name="qty" value="1"/> -+ </actionGroup> -+ -+ <!-- Assert Order Summary --> -+ <actionGroup ref="StorefrontCheckoutAndAssertOrderSummaryDisplayActionGroup" stepKey="AssertItemsCountInOrderSummary"> -+ <argument name="itemsText" value="10 of 11 Items in Cart"/> -+ </actionGroup> -+ <actionGroup ref="AssertStorefrontSeeElementActionGroup" stepKey="seeViewAndEditLinkInOrderSummary"> -+ <argument name="selector" value="{{StorefrontMinicartSection.viewAndEditCart}}"/> -+ </actionGroup> -+ -+ <!-- Click and open order summary tab--> -+ <conditionalClick selector="{{CheckoutOrderSummarySection.miniCartTab}}" dependentSelector="{{CheckoutOrderSummarySection.miniCartTabClosed}}" visible="true" stepKey="clickOnOrderSummaryTab"/> -+ <waitForPageLoad stepKey="waitForPageToLoad"/> -+ -+ <!-- Assert Shipping Page --> -+ <actionGroup ref="StorefrontAssertShippingAddressPageDisplayActionGroup" stepKey="assertShippingPageDisplay"/> -+ -+ <!-- Assert Product2 displayed in Order Summary --> -+ <actionGroup ref="StorefrontAssertProductDetailsInOrderSummaryActionGroup" stepKey="assertProduct2InOrderSummary"> -+ <argument name="productName" value="$$simpleProduct2.name$$"/> -+ <argument name="qty" value="1"/> -+ <argument name="price" value="$20.00"/> -+ </actionGroup> -+ -+ <!-- Assert Product3 displayed in Order Summary --> -+ <actionGroup ref="StorefrontAssertProductDetailsInOrderSummaryActionGroup" stepKey="assertProduct3InOrderSummary"> -+ <argument name="productName" value="$$simpleProduct3.name$$"/> -+ <argument name="qty" value="1"/> -+ <argument name="price" value="$30.00"/> -+ </actionGroup> -+ -+ <!-- Assert Product4 displayed in Order Summary --> -+ <actionGroup ref="StorefrontAssertProductDetailsInOrderSummaryActionGroup" stepKey="assertProduct4InOrderSummary"> -+ <argument name="productName" value="$$simpleProduct4.name$$"/> -+ <argument name="qty" value="1"/> -+ <argument name="price" value="$40.00"/> -+ </actionGroup> -+ -+ <!-- Assert Product5 displayed in Order Summary --> -+ <actionGroup ref="StorefrontAssertProductDetailsInOrderSummaryActionGroup" stepKey="assertProduct5InOrderSummary"> -+ <argument name="productName" value="$$simpleProduct5.name$$"/> -+ <argument name="qty" value="1"/> -+ <argument name="price" value="$50.00"/> -+ </actionGroup> -+ -+ <!-- Assert Product6 displayed in Order Summary --> -+ <actionGroup ref="StorefrontAssertProductDetailsInOrderSummaryActionGroup" stepKey="assertProduct6InOrderSummary"> -+ <argument name="productName" value="$$simpleProduct6.name$$"/> -+ <argument name="qty" value="1"/> -+ <argument name="price" value="$60.00"/> -+ </actionGroup> -+ -+ <!-- Assert Product7 displayed in Order Summary --> -+ <actionGroup ref="StorefrontAssertProductDetailsInOrderSummaryActionGroup" stepKey="assertProduct7InOrderSummary"> -+ <argument name="productName" value="$$simpleProduct7.name$$"/> -+ <argument name="qty" value="1"/> -+ <argument name="price" value="$70.00"/> -+ </actionGroup> -+ -+ <!-- Assert Product8 displayed in Order Summary --> -+ <actionGroup ref="StorefrontAssertProductDetailsInOrderSummaryActionGroup" stepKey="assertProduct8InOrderSummary"> -+ <argument name="productName" value="$$simpleProduct8.name$$"/> -+ <argument name="qty" value="1"/> -+ <argument name="price" value="$80.00"/> -+ </actionGroup> -+ -+ <!-- Assert Product9 displayed in Order Summary --> -+ <actionGroup ref="StorefrontAssertProductDetailsInOrderSummaryActionGroup" stepKey="assertProduct9InOrderSummary"> -+ <argument name="productName" value="$$simpleProduct9.name$$"/> -+ <argument name="qty" value="1"/> -+ <argument name="price" value="$90.00"/> -+ </actionGroup> -+ -+ <!-- Assert Product10 displayed in Order Summary --> -+ <actionGroup ref="StorefrontAssertProductDetailsInOrderSummaryActionGroup" stepKey="assertProduct10InOrderSummary"> -+ <argument name="productName" value="$$simpleProduct10.name$$"/> -+ <argument name="qty" value="1"/> -+ <argument name="price" value="$100.00"/> -+ </actionGroup> -+ -+ <!-- Assert Product11 displayed in Order Summary --> -+ <actionGroup ref="StorefrontAssertProductDetailsInOrderSummaryActionGroup" stepKey="assertProduct11InOrderSummary"> -+ <argument name="productName" value="$$simpleProduct11.name$$"/> -+ <argument name="qty" value="1"/> -+ <argument name="price" value="$110.00"/> -+ </actionGroup> -+ </test> -+</tests> -diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCheckVirtualProductCountDisplayWithCustomDisplayConfigurationTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCheckVirtualProductCountDisplayWithCustomDisplayConfigurationTest.xml -new file mode 100644 -index 00000000000..1f63565899f ---- /dev/null -+++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCheckVirtualProductCountDisplayWithCustomDisplayConfigurationTest.xml -@@ -0,0 +1,147 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> -+ <test name="StorefrontCheckVirtualProductCountDisplayWithCustomDisplayConfigurationTest"> -+ <annotations> -+ <stories value="Shopping Cart"/> -+ <title value="Verify virtual products count in mini cart and summary block with custom display configuration"/> -+ <description value="Verify virtual products count in mini cart and summary block with custom display configuration"/> -+ <testCaseId value="MC-14724"/> -+ <severity value="CRITICAL"/> -+ <group value="mtf_migrated"/> -+ </annotations> -+ -+ <before> -+ <!--Set Mini Cart and Summary Block Display --> -+ <magentoCLI stepKey="setMaxDisplayCountForMiniCart" command="config:set checkout/options/max_items_display_count 2"/> -+ <magentoCLI stepKey="setMaxDisplayCountForOrderSummary" command="config:set checkout/sidebar/max_items_display_count 3"/> -+ <!--Create simple product--> -+ <createData entity="VirtualProduct" stepKey="virtualProduct1"> -+ <field key="price">10.00</field> -+ </createData> -+ <createData entity="VirtualProduct" stepKey="virtualProduct2"> -+ <field key="price">20.00</field> -+ </createData> -+ <createData entity="VirtualProduct" stepKey="virtualProduct3"> -+ <field key="price">30.00</field> -+ </createData> -+ <createData entity="VirtualProduct" stepKey="virtualProduct4"> -+ <field key="price">40.00</field> -+ </createData> -+ <magentoCLI command="cache:flush" stepKey="flushCache"/> -+ </before> -+ <after> -+ <deleteData createDataKey="virtualProduct1" stepKey="deleteProduct1"/> -+ <deleteData createDataKey="virtualProduct2" stepKey="deleteProduct2"/> -+ <deleteData createDataKey="virtualProduct3" stepKey="deleteProduct3"/> -+ <deleteData createDataKey="virtualProduct4" stepKey="deleteProduct4"/> -+ <magentoCLI stepKey="setMaxDisplayCountForMiniCart" command="config:set checkout/options/max_items_display_count 10"/> -+ <magentoCLI stepKey="setMaxDisplayCountForOrderSummary" command="config:set checkout/sidebar/max_items_display_count 10"/> -+ </after> -+ -+ <!-- Open Product1 page in StoreFront --> -+ <actionGroup ref="AssertProductNameAndSkuInStorefrontProductPageByCustomAttributeUrlKey" stepKey="openProduct1PageAndVerifyProduct"> -+ <argument name="product" value="$$virtualProduct1$$"/> -+ </actionGroup> -+ -+ <!-- Add Product1 to the cart --> -+ <actionGroup ref="addToCartFromStorefrontProductPage" stepKey="addProduct1ToTheCart"> -+ <argument name="productName" value="$$virtualProduct1.name$$"/> -+ </actionGroup> -+ -+ <!-- Open Product2 page in StoreFront --> -+ <actionGroup ref="AssertProductNameAndSkuInStorefrontProductPageByCustomAttributeUrlKey" stepKey="openProduct2PageAndVerifyProduct"> -+ <argument name="product" value="$$virtualProduct2$$"/> -+ </actionGroup> -+ -+ <!-- Add Product2 to the cart --> -+ <actionGroup ref="addToCartFromStorefrontProductPage" stepKey="addProduct2ToTheCart"> -+ <argument name="productName" value="$$virtualProduct2.name$$"/> -+ </actionGroup> -+ -+ <!-- Open Product3 page in StoreFront --> -+ <actionGroup ref="AssertProductNameAndSkuInStorefrontProductPageByCustomAttributeUrlKey" stepKey="openProduct3PageAndVerifyProduct"> -+ <argument name="product" value="$$virtualProduct3$$"/> -+ </actionGroup> -+ -+ <!-- Add Product3 to the cart --> -+ <actionGroup ref="addToCartFromStorefrontProductPage" stepKey="addProduct3ToTheCart"> -+ <argument name="productName" value="$$virtualProduct3.name$$"/> -+ </actionGroup> -+ -+ <!-- Open Product4 page in StoreFront --> -+ <actionGroup ref="AssertProductNameAndSkuInStorefrontProductPageByCustomAttributeUrlKey" stepKey="openProduct4PageAndVerifyProduct"> -+ <argument name="product" value="$$virtualProduct4$$"/> -+ </actionGroup> -+ -+ <!-- Add Product4 to the cart --> -+ <actionGroup ref="addToCartFromStorefrontProductPage" stepKey="addProduct4ToTheCart"> -+ <argument name="productName" value="$$virtualProduct4.name$$"/> -+ </actionGroup> -+ -+ <!-- Open Mini Cart --> -+ <actionGroup ref="StorefrontOpenMiniCartActionGroup" stepKey="openMiniCart"/> -+ -+ <!-- Assert Product Count in Mini Cart --> -+ <actionGroup ref="StorefrontAssertMiniCartItemCountActionGroup" stepKey="assertProductCountAndTextInMiniCart"> -+ <argument name="productCount" value="4"/> -+ <argument name="productCountText" value="3 of 4 Items in Cart"/> -+ </actionGroup> -+ -+ <!-- Assert Product1 in Mini Cart --> -+ <actionGroup ref="AssertStorefrontMiniCartItemsActionGroup" stepKey="assertSimpleProduct11MiniCart"> -+ <argument name="productName" value="$$virtualProduct1.name$$"/> -+ <argument name="productPrice" value="$10.00"/> -+ <argument name="cartSubtotal" value="$100.00" /> -+ <argument name="qty" value="1"/> -+ </actionGroup> -+ -+ <!-- Assert Product2 in Mini Cart --> -+ <actionGroup ref="AssertStorefrontMiniCartItemsActionGroup" stepKey="assertSimpleProduct2MiniCart"> -+ <argument name="productName" value="$$virtualProduct2.name$$"/> -+ <argument name="productPrice" value="$20.00"/> -+ <argument name="cartSubtotal" value="$100.00" /> -+ <argument name="qty" value="1"/> -+ </actionGroup> -+ -+ <!-- Assert Product3 in Mini Cart --> -+ <actionGroup ref="AssertStorefrontMiniCartItemsActionGroup" stepKey="assertSimpleProduct3MiniCart"> -+ <argument name="productName" value="$$virtualProduct3.name$$"/> -+ <argument name="productPrice" value="$30.00"/> -+ <argument name="cartSubtotal" value="$100.00" /> -+ <argument name="qty" value="1"/> -+ </actionGroup> -+ -+ <!-- Assert Order Summary --> -+ <actionGroup ref="StorefrontCheckoutAndAssertOrderSummaryDisplayActionGroup" stepKey="AssertItemCountInOrderSummary"> -+ <argument name="itemsText" value="2 of 4 Items in Cart"/> -+ </actionGroup> -+ <actionGroup ref="AssertStorefrontSeeElementActionGroup" stepKey="seeViewAndEditLinkInOrderSummary"> -+ <argument name="selector" value="{{StorefrontMinicartSection.viewAndEditCart}}"/> -+ </actionGroup> -+ -+ <!-- Click and open order summary tab--> -+ <conditionalClick selector="{{CheckoutOrderSummarySection.miniCartTab}}" dependentSelector="{{CheckoutOrderSummarySection.miniCartTabClosed}}" visible="true" stepKey="clickOnOrderSummaryTab"/> -+ <waitForPageLoad stepKey="waitForPageToLoad"/> -+ -+ <!-- Assert Product3 displayed in Order Summary --> -+ <actionGroup ref="StorefrontAssertProductDetailsInOrderSummaryActionGroup" stepKey="assertProduct3InOrderSummary"> -+ <argument name="productName" value="$$virtualProduct3.name$$"/> -+ <argument name="qty" value="1"/> -+ <argument name="price" value="$30.00"/> -+ </actionGroup> -+ -+ <!-- Assert Product4 displayed in Order Summary --> -+ <actionGroup ref="StorefrontAssertProductDetailsInOrderSummaryActionGroup" stepKey="assertProduct4InOrderSummary"> -+ <argument name="productName" value="$$virtualProduct4.name$$"/> -+ <argument name="qty" value="1"/> -+ <argument name="price" value="$40.00"/> -+ </actionGroup> -+ </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 1efff9783b9..40b781df9b2 100644 ---- a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCustomerCheckoutTest.xml -+++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCustomerCheckoutTest.xml -@@ -12,10 +12,10 @@ - <annotations> - <features value="Checkout"/> - <stories value="Checkout via the Admin"/> -- <title value="Customer Checkout"/> -+ <title value="Customer Checkout via the Admin"/> - <description value="Should be able to place an order as a customer."/> - <severity value="CRITICAL"/> -- <testCaseId value="#"/> -+ <testCaseId value="MC-5922"/> - <group value="checkout"/> - </annotations> - <before> -@@ -26,10 +26,13 @@ - <createData entity="Simple_US_Customer" stepKey="simpleuscustomer"/> - </before> - <after> -- <amOnPage url="admin/admin/auth/logout/" stepKey="amOnLogoutPage"/> -+ <!--Clear filters--> -+ <amOnPage url="{{AdminCustomerPage.url}}" stepKey="navigateToCustomers"/> -+ <conditionalClick selector="{{AdminDataGridHeaderSection.clearFilters}}" dependentSelector="{{AdminDataGridHeaderSection.clearFilters}}" visible="true" stepKey="clearExistingCustomerFilters"/> -+ -+ <actionGroup ref="logout" stepKey="logout"/> - <deleteData createDataKey="simpleproduct1" stepKey="deleteProduct1"/> - <deleteData createDataKey="simplecategory" stepKey="deleteCategory"/> -- <deleteData createDataKey="simpleuscustomer" stepKey="deleteCustomer"/> - </after> - - <actionGroup ref="LoginToStorefrontActionGroup" stepKey="customerLogin"> -@@ -48,6 +51,8 @@ - <click stepKey="s35" selector="{{CheckoutShippingMethodsSection.firstShippingMethod}}"/> - <waitForElement stepKey="s36" selector="{{CheckoutShippingMethodsSection.next}}" time="30"/> - <click stepKey="s37" selector="{{CheckoutShippingMethodsSection.next}}" /> -+ <!-- Checkout select Check/Money Order payment --> -+ <actionGroup ref="CheckoutSelectCheckMoneyOrderPaymentActionGroup" stepKey="selectCheckMoneyPayment"/> - <waitForPageLoad stepKey="s39"/> - <waitForElement stepKey="s41" selector="{{CheckoutPaymentSection.placeOrder}}" time="30" /> - <see stepKey="s47" selector="{{CheckoutPaymentSection.billingAddress}}" userInput="{{US_Address_TX.street[0]}}" /> -@@ -67,6 +72,7 @@ - <click stepKey="s81" selector="{{AdminOrdersGridSection.submitSearch22}}" /> - <waitForPageLoad stepKey="s831"/> - <click stepKey="s84" selector="{{AdminOrdersGridSection.firstRow}}" /> -+ <waitForPageLoad stepKey="waitForOrderToLoad"/> - <see stepKey="s85" selector="{{AdminOrderDetailsInformationSection.orderStatus}}" userInput="Pending" /> - <see stepKey="s87" selector="{{AdminOrderDetailsInformationSection.accountInformation}}" userInput="Customer" /> - <see stepKey="s89" selector="{{AdminOrderDetailsInformationSection.accountInformation}}" userInput="$$simpleuscustomer.email$$" /> -@@ -76,6 +82,7 @@ - - <amOnPage stepKey="s96" url="{{AdminCustomerPage.url}}"/> - <waitForPageLoad stepKey="s97"/> -+ <waitForElementVisible selector="{{AdminCustomerFiltersSection.filtersButton}}" time="30" stepKey="waitFiltersButton"/> - <click stepKey="s98" selector="{{AdminCustomerFiltersSection.filtersButton}}"/> - <fillField stepKey="s99" selector="{{AdminCustomerFiltersSection.emailInput}}" userInput="$$simpleuscustomer.email$$"/> - <click stepKey="s100" selector="{{AdminCustomerFiltersSection.apply}}"/> -@@ -85,12 +92,13 @@ - </test> - <test name="StorefrontCustomerCheckoutTestWithMultipleAddressesAndTaxRates"> - <annotations> -+ <features value="Checkout"/> -+ <stories value="Customer checkout"/> - <title value="Customer Checkout with multiple addresses and tax rates"/> - <description value="Should be able to place an order as a customer with multiple addresses and tax rates."/> - <testCaseId value="MAGETWO-93109"/> -- <skip> -- <issueId value="MQE-1187" /> -- </skip> -+ <severity value="AVERAGE"/> -+ <group value="checkout"/> - </annotations> - <before> - <createData entity="SimpleSubCategory" stepKey="simplecategory"/> -@@ -162,7 +170,8 @@ - <click stepKey="selectFirstShippingMethod1" selector="{{CheckoutShippingMethodsSection.firstShippingMethod}}"/> - <waitForElement stepKey="waitForShippingMethodSelect1" selector="{{CheckoutShippingMethodsSection.next}}" time="30"/> - <click stepKey="clickNextOnShippingMethodLoad1" selector="{{CheckoutShippingMethodsSection.next}}" /> -- <waitForPageLoad stepKey="waitForPaymentLoad1"/> -+ <!-- Checkout select Check/Money Order payment --> -+ <actionGroup ref="CheckoutSelectCheckMoneyOrderPaymentActionGroup" stepKey="selectCheckMoneyPayment"/> - <waitForElement stepKey="waitForPlaceOrderButton1" selector="{{CheckoutPaymentSection.placeOrder}}" time="30" /> - <see stepKey="seeBillingAddressIsCorrect1" selector="{{CheckoutPaymentSection.billingAddress}}" userInput="{{US_Address_NY.street[0]}}" /> - <click stepKey="clickPlaceOrderButton1" selector="{{CheckoutPaymentSection.placeOrder}}" /> -@@ -178,16 +187,87 @@ - <see selector="{{StorefrontMinicartSection.quantity}}" userInput="1" stepKey="seeCartQuantity2"/> - <actionGroup ref="GoToCheckoutFromMinicartActionGroup" stepKey="goToCheckoutFromMinicart2" /> - -- <click stepKey="changeShippingAddress" selector="{{CheckoutShippingMethodsSection.shipHereButton}}"/> -- <waitForElementNotVisible stepKey="waitForShippingMethodLoaderNotVisible" selector="{{CheckoutShippingMethodsSection.shippingMethodLoader}}" time="30"/> -- <click stepKey="selectFirstShippingMethod2" selector="{{CheckoutShippingMethodsSection.firstShippingMethod}}"/> -- <waitForElement stepKey="waitForShippingMethodSelect2" selector="{{CheckoutShippingMethodsSection.next}}" time="30"/> -- <click stepKey="clickNextOnShippingMethodLoad2" selector="{{CheckoutShippingMethodsSection.next}}" /> -- <waitForPageLoad stepKey="waitForPaymentLoad2"/> -- <waitForElement stepKey="waitForPlaceOrderButton2" selector="{{CheckoutPaymentSection.placeOrder}}" time="30" /> -- <see stepKey="seeBillingAddressIsCorrect2" selector="{{CheckoutPaymentSection.billingAddress}}" userInput="{{UK_Not_Default_Address.street[0]}}" /> -- <click stepKey="clickPlaceOrderButton2" selector="{{CheckoutPaymentSection.placeOrder}}" /> -+ <click selector="{{CheckoutShippingMethodsSection.shipHereButton}}" stepKey="changeShippingAddress"/> -+ <waitForElementNotVisible selector="{{CheckoutShippingMethodsSection.shippingMethodLoader}}" time="30" stepKey="waitForShippingMethodLoaderNotVisible"/> -+ <waitForElementVisible selector="{{CheckoutShippingMethodsSection.firstShippingMethod}}" time="30" stepKey="waitForShippingMethodRadioToBeVisible"/> -+ <waitForPageLoad stepKey="waitForPageLoad23"/> -+ <click selector="{{CheckoutShippingMethodsSection.firstShippingMethod}}" stepKey="selectFirstShippingMethod2"/> -+ <waitForElement selector="{{CheckoutShippingMethodsSection.next}}" time="30" stepKey="waitForShippingMethodSelect2"/> -+ <click selector="{{CheckoutShippingMethodsSection.next}}" stepKey="clickNextOnShippingMethodLoad2"/> -+ <!-- Checkout select Check/Money Order payment --> -+ <actionGroup ref="CheckoutSelectCheckMoneyOrderPaymentActionGroup" stepKey="selectCheckMoneyPayment2"/> -+ <waitForElement selector="{{CheckoutPaymentSection.placeOrder}}" time="30" stepKey="waitForPlaceOrderButton2"/> -+ <see selector="{{CheckoutPaymentSection.billingAddress}}" userInput="{{US_Address_NY.street[0]}}" stepKey="seeBillingAddressIsCorrect2" /> -+ <click selector="{{CheckoutPaymentSection.placeOrder}}" stepKey="clickPlaceOrderButton2"/> - <waitForPageLoad stepKey="waitForOrderSuccessPage2"/> -- <see stepKey="seeSuccessMessage2" selector="{{CheckoutSuccessMainSection.success}}" userInput="Your order number is:" /> -+ <see selector="{{CheckoutSuccessMainSection.success}}" userInput="Your order number is:" stepKey="seeSuccessMessage2"/> -+ </test> -+ <test name="StorefrontCustomerCheckoutTestWithRestrictedCountriesForPayment"> -+ <annotations> -+ <features value="Checkout"/> -+ <stories value="Checkout flow if payment solutions are not available"/> -+ <title value="Checkout via Customer Checkout with restricted countries for payment"/> -+ <description value="Should be able to place an order as a Customer with restricted countries for payment."/> -+ <severity value="MAJOR"/> -+ <testCaseId value="MAGETWO-42653"/> -+ <group value="checkout"/> -+ </annotations> -+ <before> -+ <createData entity="_defaultCategory" stepKey="createCategory"/> -+ <createData entity="ApiSimpleProduct" stepKey="createProduct"> -+ <requiredEntity createDataKey="createCategory"/> -+ </createData> -+ <magentoCLI command="config:set checkout/options/display_billing_address_on 1" stepKey="setShowBillingAddressOnPaymentPage" /> -+ <magentoCLI command="config:set payment/checkmo/allowspecific 1" stepKey="allowSpecificValue" /> -+ <magentoCLI command="config:set payment/checkmo/specificcountry GB" stepKey="specificCountryValue" /> -+ <createData entity="Simple_US_Customer" stepKey="simpleuscustomer"/> -+ </before> -+ <after> -+ <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> -+ <deleteData createDataKey="createProduct" stepKey="deleteProduct"/> -+ <magentoCLI command="config:set payment/checkmo/allowspecific 0" stepKey="allowSpecificValue" /> -+ <magentoCLI command="config:set payment/checkmo/specificcountry ''" stepKey="specificCountryValue" /> -+ <magentoCLI command="config:set checkout/options/display_billing_address_on 0" stepKey="setDisplayBillingAddressOnPaymentMethod" /> -+ <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="logoutCustomer"/> -+ </after> -+ <!-- Login as Customer --> -+ <actionGroup ref="LoginToStorefrontActionGroup" stepKey="customerLogin"> -+ <argument name="Customer" value="$$simpleuscustomer$$" /> -+ </actionGroup> -+ -+ <!-- Add product to cart --> -+ <amOnPage url="{{StorefrontCategoryPage.url($$createCategory.name$$)}}" stepKey="onCategoryPage"/> -+ <waitForPageLoad stepKey="waitForPageLoad1"/> -+ <moveMouseOver selector="{{StorefrontCategoryMainSection.ProductItemInfo}}" stepKey="hoverProduct"/> -+ <click selector="{{StorefrontCategoryMainSection.AddToCartBtn}}" stepKey="addToCart"/> -+ <waitForElementVisible selector="{{StorefrontCategoryMainSection.SuccessMsg}}" time="30" stepKey="waitForProductAdded"/> -+ <see selector="{{StorefrontCategoryMainSection.SuccessMsg}}" userInput="You added $$createProduct.name$$ to your shopping cart." stepKey="seeAddedToCartMessage"/> -+ <see selector="{{StorefrontMinicartSection.quantity}}" userInput="1" stepKey="seeCartQuantity"/> -+ -+ <!-- Go to checkout page --> -+ <actionGroup ref="GoToCheckoutFromMinicartActionGroup" stepKey="customerGoToCheckoutFromMinicart" /> -+ -+ <!-- Select address --> -+ <click stepKey="selectAddress" selector="{{CheckoutShippingMethodsSection.firstShippingMethod}}"/> -+ <waitForElement stepKey="waitNextButton" selector="{{CheckoutShippingMethodsSection.next}}" time="30"/> -+ <click stepKey="clickNextButton" selector="{{CheckoutShippingMethodsSection.next}}" /> -+ <waitForPageLoad stepKey="waitBillingForm"/> -+ <seeInCurrentUrl url="{{CheckoutPage.url}}/#payment" stepKey="assertCheckoutPaymentUrl"/> -+ <dontSee selector="{{CheckoutPaymentSection.paymentMethodByName('Check / Money order')}}" stepKey="paymentMethodDoesNotAvailable"/> -+ -+ <!-- Fill UK Address and verify that payment available and checkout successful --> -+ <uncheckOption selector="{{StorefrontCheckoutPaymentMethodSection.billingAddressSameAsShippingShared}}" stepKey="uncheckBillingAddressSameAsShippingCheckCheckBox"/> -+ <selectOption selector="{{CheckoutPaymentSection.billingAddressSelectShared}}" userInput="New Address" stepKey="clickOnNewAddress"/> -+ <waitForPageLoad stepKey="waitNewAddressBillingForm"/> -+ <actionGroup ref="LoggedInCheckoutFillNewBillingAddressActionGroup" stepKey="changeAddress"> -+ <argument name="Address" value="updateCustomerUKAddress"/> -+ <argument name="classPrefix" value="[aria-hidden=false]"/> -+ </actionGroup> -+ <click selector="{{CheckoutPaymentSection.addressAction('Update')}}" stepKey="clickUpdateBillingAddressButton" /> -+ <actionGroup ref="CheckoutSelectCheckMoneyOrderPaymentActionGroup" stepKey="customerSelectCheckMoneyOrderPayment" /> -+ <actionGroup ref="CheckoutPlaceOrderActionGroup" stepKey="customerPlaceorder"> -+ <argument name="orderNumberMessage" value="CONST.successCheckoutOrderNumberMessage" /> -+ <argument name="emailYouMessage" value="CONST.successCheckoutEmailYouMessage" /> -+ </actionGroup> - </test> --</tests> -+</tests> -\ No newline at end of file -diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCustomerCheckoutWithoutRegionTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCustomerCheckoutWithoutRegionTest.xml -new file mode 100644 -index 00000000000..0cc0dcf38e3 ---- /dev/null -+++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCustomerCheckoutWithoutRegionTest.xml -@@ -0,0 +1,55 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> -+ <test name="StorefrontCustomerCheckoutWithoutRegionTest"> -+ <annotations> -+ <features value="Checkout"/> -+ <stories value="Checkout via the Admin"/> -+ <title value="Shipping address is not validated in checkout when proceeding step as logged in user with default shipping address"/> -+ <description value="Shouldn't be able to place an order as a customer without state if it's required."/> -+ <severity value="CRITICAL"/> -+ <testCaseId value="#"/> -+ <group value="checkout"/> -+ </annotations> -+ <before> -+ <createData entity="SimpleSubCategory" stepKey="createCategory"/> -+ <createData entity="SimpleProduct" stepKey="createProduct"> -+ <requiredEntity createDataKey="createCategory"/> -+ </createData> -+ <createData entity="Simple_GB_Customer" stepKey="createCustomer"/> -+ <actionGroup stepKey="loginToAdminPanel" ref="LoginAsAdmin"/> -+ <actionGroup ref="SelectCountriesWithRequiredRegion" stepKey="setCustomCountryWithRequiredRegion"> -+ <argument name="countries" value="CustomCountryWithRequiredRegion"/> -+ </actionGroup> -+ </before> -+ <after> -+ <actionGroup ref="SelectCountriesWithRequiredRegion" stepKey="setDefaultCountriesWithRequiredRegion"> -+ <argument name="countries" value="DefaultCountriesWithRequiredRegions"/> -+ </actionGroup> -+ <actionGroup ref="logout" stepKey="logout"/> -+ <deleteData createDataKey="createProduct" stepKey="deleteProduct"/> -+ <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> -+ <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> -+ </after> -+ -+ <actionGroup ref="LoginToStorefrontActionGroup" stepKey="customerLogin"> -+ <argument name="Customer" value="$$createCustomer$$" /> -+ </actionGroup> -+ -+ <actionGroup ref="AddSimpleProductToCart" stepKey="addProductToCart"> -+ <argument name="product" value="$$createProduct$$"/> -+ </actionGroup> -+ -+ <actionGroup ref="GoToCheckoutFromMinicartActionGroup" stepKey="navigateToCheckoutPage"/> -+ -+ <click selector="{{CheckoutShippingSection.next}}" stepKey="clickNextButton"/> -+ <see selector="{{StorefrontMessagesSection.error}}" userInput='Please specify a regionId in shipping address.' stepKey="seeErrorMessages"/> -+ </test> -+</tests> -diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCustomerLoginDuringCheckoutTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCustomerLoginDuringCheckoutTest.xml -new file mode 100644 -index 00000000000..3a0ba2302a6 ---- /dev/null -+++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCustomerLoginDuringCheckoutTest.xml -@@ -0,0 +1,74 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> -+ <test name="StorefrontCustomerLoginDuringCheckoutTest"> -+ <annotations> -+ <features value="OnePageCheckout"/> -+ <stories value="Customer Login during checkout"/> -+ <title value="Storefront customer login during checkout test"/> -+ <description value="Logging during checkout for customer without addresses in address book"/> -+ <severity value="CRITICAL"/> -+ <testCaseId value="MC-13097"/> -+ <group value="OnePageCheckout"/> -+ </annotations> -+ <before> -+ <!-- Create simple product --> -+ <createData entity="SimpleProduct2" stepKey="createProduct"/> -+ -+ <!-- Login as admin --> -+ <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> -+ </before> -+ <after> -+ <!-- Delete simple product --> -+ <deleteData createDataKey="createProduct" stepKey="deleteProduct"/> -+ -+ <!-- Customer log out --> -+ <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="customerLogout"/> -+ -+ <!-- Delete customer --> -+ <actionGroup ref="AdminDeleteCustomerActionGroup" stepKey="deleteCustomer"> -+ <argument name="customerEmail" value="CustomerEntityOne.email"/> -+ </actionGroup> -+ -+ <!-- Logout admin --> -+ <actionGroup ref="logout" stepKey="logout"/> -+ </after> -+ <!-- Go to Storefront as Guest and create new account --> -+ <actionGroup ref="SignUpNewUserFromStorefrontActionGroup" stepKey="createNewCustomerAccount"/> -+ -+ <!-- Sign Out --> -+ <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="customerLogout"/> -+ -+ <!-- Add simple product to cart as Guest --> -+ <amOnPage url="{{StorefrontProductPage.url($$createProduct.custom_attributes[url_key]$$)}}" stepKey="goToProductPage"/> -+ <waitForPageLoad stepKey="waitForPageLoad"/> -+ <actionGroup ref="StorefrontAddProductToCartActionGroup" stepKey="cartAddSimpleProductToCart"> -+ <argument name="product" value="$$createProduct$$"/> -+ <argument name="productCount" value="1"/> -+ </actionGroup> -+ -+ <!-- Go to Checkout page --> -+ <actionGroup ref="clickViewAndEditCartFromMiniCart" stepKey="goToShoppingCartFromMinicart"/> -+ <click selector="{{CheckoutCartSummarySection.proceedToCheckout}}" stepKey="clickProceedToCheckout"/> -+ <waitForPageLoad stepKey="waitForProceedToCheckout"/> -+ -+ <!-- Input in field email and password for newly created customer; click Login button --> -+ <actionGroup ref="LoginAsCustomerOnCheckoutPageActionGroup" stepKey="customerLogin"> -+ <argument name="customer" value="CustomerEntityOne"/> -+ </actionGroup> -+ -+ <!-- Block with email is disappeared --> -+ <dontSeeElement selector="{{CheckoutShippingSection.email}}" stepKey="dontSeeEmailBlock"/> -+ -+ <!-- Shipping form is pre-filed with first name and last name --> -+ <seeInField selector="{{CheckoutShippingSection.firstName}}" userInput="{{CustomerEntityOne.firstname}}" stepKey="seeCustomerFirstNameInField"/> -+ <seeInField selector="{{CheckoutShippingSection.lastName}}" userInput="{{CustomerEntityOne.lastname}}" stepKey="seeCustomerLastNameInField"/> -+ </test> -+</tests> -diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCustomerPlaceOrderWithNewAddressesThatWasEditedTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCustomerPlaceOrderWithNewAddressesThatWasEditedTest.xml -new file mode 100644 -index 00000000000..651c5bd8d43 ---- /dev/null -+++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCustomerPlaceOrderWithNewAddressesThatWasEditedTest.xml -@@ -0,0 +1,96 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> -+ <test name="StorefrontCustomerPlaceOrderWithNewAddressesThatWasEditedTest"> -+ <annotations> -+ <features value="Checkout"/> -+ <stories value="Checkout via the Storefront"/> -+ <title value="Customer can place order with new addresses that was edited during checkout with several conditions"/> -+ <description value="Customer can place order with new addresses."/> -+ <severity value="CRITICAL"/> -+ <testCaseId value="MAGETWO-67837"/> -+ <group value="checkout"/> -+ </annotations> -+ <before> -+ <createData entity="_defaultCategory" stepKey="createCategory"/> -+ <createData entity="SimpleProduct" stepKey="createProduct"> -+ <requiredEntity createDataKey="createCategory"/> -+ </createData> -+ <createData entity="Simple_US_Customer" stepKey="createCustomer"/> -+ </before> -+ <after> -+ <!--Logout from customer account--> -+ <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="logoutCustomer"/> -+ -+ <deleteData createDataKey="createProduct" stepKey="deleteProduct"/> -+ <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> -+ <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> -+ </after> -+ <!--Go to Storefront as Customer--> -+ <actionGroup ref="LoginToStorefrontActionGroup" stepKey="customerLogin"> -+ <argument name="Customer" value="$$createCustomer$$" /> -+ </actionGroup> -+ -+ <!-- Add simple product to cart and go to checkout--> -+ <actionGroup ref="AddSimpleProductToCart" stepKey="addProductToCart"> -+ <argument name="product" value="$$createProduct$$"/> -+ </actionGroup> -+ <actionGroup ref="GoToCheckoutFromMinicartActionGroup" stepKey="goToCheckoutFromMinicart" /> -+ -+ <!-- Click "+ New Address" and Fill new address--> -+ <click selector="{{CheckoutShippingSection.newAddressButton}}" stepKey="addAddress"/> -+ <actionGroup ref="LoggedInCheckoutWithOneAddressFieldWithoutStateField" stepKey="changeAddress"> -+ <argument name="Address" value="UK_Not_Default_Address"/> -+ <argument name="classPrefix" value="._show"/> -+ </actionGroup> -+ -+ <!--Click "Save Addresses" --> -+ <click selector="{{CheckoutShippingSection.saveAddress}}" stepKey="saveAddress"/> -+ <waitForPageLoad stepKey="waitForAddressSaved"/> -+ <dontSeeElement selector="{{StorefrontCheckoutAddressPopupSection.newAddressModalPopup}}" stepKey="dontSeeModalPopup"/> -+ -+ <!--Select Shipping Rate "Flat Rate"--> -+ <click selector="{{CheckoutShippingMethodsSection.checkShippingMethodByName('Flat Rate')}}" stepKey="selectFlatShippingMethod"/> -+ <waitForLoadingMaskToDisappear stepKey="waitForLoadingMask2"/> -+ -+ <click selector="{{CheckoutShippingSection.editActiveAddress}}" stepKey="editNewAddress"/> -+ <actionGroup ref="clearCheckoutAddressPopupFieldsActionGroup" stepKey="clearRequiredFields"> -+ <argument name="classPrefix" value="._show"/> -+ </actionGroup> -+ -+ <!--Close Popup and click next--> -+ <click selector="{{StorefrontCheckoutAddressPopupSection.closeAddressModalPopup}}" stepKey="closePopup"/> -+ <waitForElement selector="{{CheckoutShippingMethodsSection.next}}" stepKey="waitForNextButton"/> -+ <click selector="{{CheckoutShippingMethodsSection.next}}" stepKey="clickNext"/> -+ -+ <!--Refresh Page and Place Order--> -+ <actionGroup ref="CheckoutSelectCheckMoneyOrderPaymentActionGroup" stepKey="selectCheckMoneyPayment"/> -+ <reloadPage stepKey="reloadPage"/> -+ <waitForElement selector="{{CheckoutPaymentSection.placeOrder}}" stepKey="waitForPlaceOrderButton"/> -+ <click selector="{{CheckoutPaymentSection.placeOrder}}" stepKey="clickPlaceOrder"/> -+ <seeElement selector="{{CheckoutSuccessMainSection.success}}" stepKey="orderIsSuccessfullyPlaced"/> -+ <grabTextFrom selector="{{CheckoutSuccessMainSection.orderLink}}" stepKey="grabOrderNumber"/> -+ -+ <!--Verify New addresses in Customer's Address Book--> -+ <amOnPage url="{{StorefrontCustomerAddressesPage.url}}" stepKey="goToCustomerAddressBook"/> -+ <see userInput="{{UK_Not_Default_Address.street[0]}}" -+ selector="{{StorefrontCustomerAddressesSection.addressesList}}" stepKey="checkNewAddressesStreet"/> -+ <see userInput="{{UK_Not_Default_Address.city}}" -+ selector="{{StorefrontCustomerAddressesSection.addressesList}}" stepKey="checkNewAddressesCity"/> -+ <see userInput="{{UK_Not_Default_Address.postcode}}" -+ selector="{{StorefrontCustomerAddressesSection.addressesList}}" stepKey="checkNewAddressesPostcode"/> -+ <!--Order review page has address that was created during checkout--> -+ <amOnPage url="{{StorefrontCustomerOrderViewPage.url({$grabOrderNumber})}}" stepKey="goToOrderReviewPage"/> -+ <see userInput="{{UK_Not_Default_Address.street[0]}} {{UK_Not_Default_Address.city}}, {{UK_Not_Default_Address.postcode}}" -+ selector="{{StorefrontCustomerOrderViewSection.shippingAddress}}" stepKey="checkShippingAddress"/> -+ <see userInput="{{US_Address_TX_Default_Billing.street[0]}}" -+ selector="{{StorefrontCustomerOrderViewSection.billingAddress}}" stepKey="checkBillingAddress"/> -+ </test> -+</tests> -diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontDeleteBundleProductFromMiniShoppingCartTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontDeleteBundleProductFromMiniShoppingCartTest.xml -new file mode 100644 -index 00000000000..8f3ddbb27f6 ---- /dev/null -+++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontDeleteBundleProductFromMiniShoppingCartTest.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="StorefrontDeleteBundleProductFromMiniShoppingCartTest"> -+ <annotations> -+ <stories value="DeleteBundleProduct"/> -+ <title value="Storefront Delete Bundle Product From Mini Shopping Cart Test"/> -+ <description value="Test log in to Shopping Cart and Delete Bundle Product From Mini Shopping Cart Test"/> -+ <testCaseId value="MC-14682"/> -+ <severity value="CRITICAL"/> -+ <group value="Shopping Cart"/> -+ <group value="mtf_migrated"/> -+ </annotations> -+ -+ <before> -+ <actionGroup ref="LoginAsAdmin" stepKey="LoginAsAdmin"/> -+ <magentoCLI command="config:set {{EnableFlatRateConfigData.path}} {{EnableFlatRateConfigData.value}}" stepKey="enableFlatRate"/> -+ <magentoCLI command="config:set {{EnableFlatRateDefaultPriceConfigData.path}} {{EnableFlatRateDefaultPriceConfigData.value}}" stepKey="enableFlatRateDefaultPrice"/> -+ <createData entity="SimpleSubCategory" stepKey="createSubCategory"/> -+ <!--Create simple product--> -+ <createData entity="SimpleProduct2" stepKey="simpleProduct1"> -+ <field key="price">10.00</field> -+ </createData> -+ <!--Create Bundle product--> -+ <createData entity="BundleProductPriceViewRange" stepKey="createBundleProduct"> -+ <requiredEntity createDataKey="createSubCategory"/> -+ </createData> -+ <createData entity="DropDownBundleOption" stepKey="createBundleOption1_1"> -+ <requiredEntity createDataKey="createBundleProduct"/> -+ <field key="required">True</field> -+ </createData> -+ <createData entity="ApiBundleLink" stepKey="linkOptionToProduct"> -+ <requiredEntity createDataKey="createBundleProduct"/> -+ <requiredEntity createDataKey="createBundleOption1_1"/> -+ <requiredEntity createDataKey="simpleProduct1"/> -+ </createData> -+ <magentoCLI command="indexer:reindex" stepKey="reindex"/> -+ </before> -+ <after> -+ <deleteData createDataKey="simpleProduct1" stepKey="deleteProduct1"/> -+ <deleteData createDataKey="createBundleProduct" stepKey="deleteBundleProduct"/> -+ <deleteData createDataKey="createSubCategory" stepKey="deleteCategory"/> -+ <actionGroup ref="logout" stepKey="logout"/> -+ </after> -+ -+ <!--Open Product page in StoreFront --> -+ <actionGroup ref="AssertProductNameAndSkuInStorefrontProductPageByCustomAttributeUrlKey" stepKey="openProductPageAndVerifyProduct"> -+ <argument name="product" value="$$createBundleProduct$$"/> -+ </actionGroup> -+ -+ <!-- Click on customize And Add To Cart Button --> -+ <actionGroup ref="StorefrontSelectCustomizeAndAddToTheCartButtonActionGroup" stepKey="clickOnCustomizeAndAddtoCartButton"/> -+ -+ <!-- Select Product Quantity and add to the cart --> -+ <actionGroup ref="StorefrontEnterProductQuantityAndAddToTheCartActionGroup" stepKey="enterProductQuantityAndAddToTheCart"> -+ <argument name="quantity" value="1"/> -+ </actionGroup> -+ <scrollToTopOfPage stepKey="scrollToTop"/> -+ <waitForPageLoad stepKey="waitForMiniCartPanelToAppear"/> -+ -+ <!-- Assert Product in Mini Cart --> -+ <click selector="{{StorefrontMinicartSection.showCart}}" stepKey="clickOnMiniCart"/> -+ <waitForPageLoad stepKey="waitForPageToLoad1"/> -+ <actionGroup ref="AssertStorefrontMiniCartItemsActionGroup" stepKey="assertSimpleProduct3MiniCart"> -+ <argument name="productName" value="$$createBundleProduct.name$$"/> -+ <argument name="productPrice" value="$10.00"/> -+ <argument name="cartSubtotal" value="$10.00" /> -+ <argument name="qty" value="1"/> -+ </actionGroup> -+ -+ <!-- Select Mini Cart and select 'View And Edit Cart' --> -+ <actionGroup ref="assertOneProductNameInMiniCart" stepKey="seeProductInMiniCart"> -+ <argument name="productName" value="$$createBundleProduct.name$$"/> -+ </actionGroup> -+ -+ <!--Remove an item from the cart using minicart--> -+ <actionGroup ref="removeProductFromMiniCart" stepKey="removeProductFromMiniCart"> -+ <argument name="productName" value="$$createBundleProduct.name$$"/> -+ </actionGroup> -+ <reloadPage stepKey="reloadPage"/> -+ -+ <!--Check the minicart is empty and verify AssertProductAbsentInMiniShoppingCart--> -+ <actionGroup ref="assertMiniCartEmpty" stepKey="miniCartEnpty"/> -+ <dontSee selector="{{StorefrontMinicartSection.productLinkByName($$createBundleProduct.name$$)}}" stepKey="verifyAssertProductAbsentInMiniShoppingCart"/> -+ </test> -+</tests> -diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontDeleteConfigurableProductFromMiniShoppingCartTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontDeleteConfigurableProductFromMiniShoppingCartTest.xml -new file mode 100644 -index 00000000000..f6357bcf4ca ---- /dev/null -+++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontDeleteConfigurableProductFromMiniShoppingCartTest.xml -@@ -0,0 +1,100 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> -+ <test name="StorefrontDeleteConfigurableProductFromMiniShoppingCartTest"> -+ <annotations> -+ <stories value="DeleteConfigurableProduct"/> -+ <title value="Storefront Delete Configurable Product From Mini Shopping Cart Test"/> -+ <description value="Test log in to Shopping Cart and Delete Configurable Product From Mini Shopping Cart Test"/> -+ <testCaseId value="MC-14681"/> -+ <severity value="CRITICAL"/> -+ <group value="Shopping Cart"/> -+ <group value="mtf_migrated"/> -+ </annotations> -+ -+ <before> -+ <actionGroup ref="LoginAsAdmin" stepKey="LoginAsAdmin"/> -+ <!-- Create Default Category --> -+ <createData entity="_defaultCategory" stepKey="createCategory"/> -+ -+ <!-- Create an attribute with three options to be used in the first child product --> -+ <createData entity="productAttributeWithTwoOptions" stepKey="createConfigProductAttribute"/> -+ <createData entity="productAttributeOption1" stepKey="createConfigProductAttributeOption1"> -+ <requiredEntity createDataKey="createConfigProductAttribute"/> -+ </createData> -+ -+ <!-- Add the attribute just created to default attribute set --> -+ <createData entity="AddToDefaultSet" stepKey="createConfigAddToAttributeSet"> -+ <requiredEntity createDataKey="createConfigProductAttribute"/> -+ </createData> -+ -+ <!-- Get the first option of the attribute created --> -+ <getData entity="ProductAttributeOptionGetter" index="1" stepKey="getConfigAttributeOption1"> -+ <requiredEntity createDataKey="createConfigProductAttribute"/> -+ </getData> -+ -+ <!-- Create Configurable product --> -+ <createData entity="BaseConfigurableProduct" stepKey="createConfigProduct"> -+ <requiredEntity createDataKey="createCategory"/> -+ </createData> -+ -+ <!-- Create a simple product and give it the attribute with the first option --> -+ <createData entity="ApiSimpleOne" stepKey="createConfigChildProduct1"> -+ <requiredEntity createDataKey="createConfigProductAttribute"/> -+ <requiredEntity createDataKey="getConfigAttributeOption1"/> -+ <field key="price">10.00</field> -+ </createData> -+ -+ <!-- Create the configurable product --> -+ <createData entity="ConfigurableProductThreeOptions" stepKey="createConfigProductOption"> -+ <requiredEntity createDataKey="createConfigProduct"/> -+ <requiredEntity createDataKey="createConfigProductAttribute"/> -+ <requiredEntity createDataKey="getConfigAttributeOption1"/> -+ </createData> -+ -+ <!-- Add the first simple product to the configurable product --> -+ <createData entity="ConfigurableProductAddChild" stepKey="createConfigProductAddChild1"> -+ <requiredEntity createDataKey="createConfigProduct"/> -+ <requiredEntity createDataKey="createConfigChildProduct1"/> -+ </createData> -+ <magentoCLI command="indexer:reindex" stepKey="reindex"/> -+ <magentoCLI command="cache:flush" stepKey="flushCache"/> -+ </before> -+ <after> -+ <deleteData createDataKey="createConfigChildProduct1" stepKey="deleteSimpleProduct1"/> -+ <deleteData createDataKey="createConfigProduct" stepKey="deleteProduct"/> -+ <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> -+ <deleteData createDataKey="createConfigProductAttribute" stepKey="deleteProductAttribute"/> -+ <actionGroup ref="logout" stepKey="logout"/> -+ </after> -+ -+ <!-- Add Configurable Product to the cart --> -+ <actionGroup ref="StorefrontAddConfigurableProductToTheCartActionGroup" stepKey="addConfigurableProductToCart"> -+ <argument name="urlKey" value="$$createConfigProduct.custom_attributes[url_key]$$" /> -+ <argument name="productAttribute" value="$$createConfigProductAttribute.default_value$$"/> -+ <argument name="productOption" value="$$getConfigAttributeOption1.label$$"/> -+ <argument name="qty" value="1"/> -+ </actionGroup> -+ -+ <!-- Select Mini Cart and select 'View And Edit Cart' --> -+ <actionGroup ref="assertOneProductNameInMiniCart" stepKey="seeProductInMiniCart"> -+ <argument name="productName" value="$$createConfigProduct.name$$"/> -+ </actionGroup> -+ -+ <!--Remove an item from the cart using minicart--> -+ <actionGroup ref="removeProductFromMiniCart" stepKey="removeProductFromMiniCart"> -+ <argument name="productName" value="$$createConfigProduct.name$$"/> -+ </actionGroup> -+ <reloadPage stepKey="reloadPage"/> -+ -+ <!--Check the minicart is empty and verify AssertProductAbsentInMiniShoppingCart--> -+ <actionGroup ref="assertMiniCartEmpty" stepKey="miniCartEnpty"/> -+ <dontSee selector="{{StorefrontMinicartSection.productLinkByName($$createConfigProduct.name$$)}}" stepKey="verifyAssertProductAbsentInMiniShoppingCart"/> -+ </test> -+</tests> -\ No newline at end of file -diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontDeleteDownloadableProductFromMiniShoppingCartTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontDeleteDownloadableProductFromMiniShoppingCartTest.xml -new file mode 100644 -index 00000000000..6a3f6ab4f70 ---- /dev/null -+++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontDeleteDownloadableProductFromMiniShoppingCartTest.xml -@@ -0,0 +1,74 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> -+ <test name="StorefrontDeleteDownloadableProductFromMiniShoppingCartTest"> -+ <annotations> -+ <stories value="DeleteConfigurableProduct"/> -+ <title value="Storefront Delete Downloadable Product From Mini Shopping Cart Test"/> -+ <description value="Test log in to Shopping Cart and Delete Downloadable Product From Mini Shopping Cart Test"/> -+ <testCaseId value="MC-14683"/> -+ <severity value="CRITICAL"/> -+ <group value="Shopping Cart"/> -+ <group value="mtf_migrated"/> -+ </annotations> -+ -+ <before> -+ <actionGroup ref="LoginAsAdmin" stepKey="LoginAsAdmin"/> -+ <magentoCLI command="config:set {{EnableFlatRateConfigData.path}} {{EnableFlatRateConfigData.value}}" stepKey="enableFlatRate"/> -+ <magentoCLI command="config:set {{EnableFlatRateDefaultPriceConfigData.path}} {{EnableFlatRateDefaultPriceConfigData.value}}" stepKey="enableFlatRateDefaultPrice"/> -+ <createData entity="ApiDownloadableProduct" stepKey="createDownloadableProduct"/> -+ <createData entity="downloadableLink1" stepKey="addDownloadableLink1"> -+ <requiredEntity createDataKey="createDownloadableProduct"/> -+ </createData> -+ <magentoCLI command="indexer:reindex" stepKey="reindex"/> -+ <magentoCLI command="cache:flush" stepKey="flushCache"/> -+ </before> -+ <after> -+ <deleteData createDataKey="createDownloadableProduct" stepKey="deleteProduct"/> -+ <actionGroup ref="logout" stepKey="logout"/> -+ </after> -+ -+ <!-- Open Downloadable Product page --> -+ <amOnPage url="{{StorefrontProductPage.url($$createDownloadableProduct.custom_attributes[url_key]$$)}}" stepKey="OpenStoreFrontProductPage"/> -+ <waitForPageLoad stepKey="waitForPageToLoad"/> -+ -+ <!-- Add Downloadable product to the cart --> -+ <actionGroup ref="StorefrontAddToCartCustomOptionsProductPageActionGroup" stepKey="addToTheCart"> -+ <argument name="productName" value="$$createDownloadableProduct.name$$" /> -+ </actionGroup> -+ -+ <!-- Select Mini Cart and select 'View And Edit Cart' --> -+ <actionGroup ref="clickViewAndEditCartFromMiniCart" stepKey="selectViewAndEditCart"/> -+ -+ <!-- Assert product details in Mini Cart --> -+ <click selector="{{StorefrontMinicartSection.showCart}}" stepKey="clickOnMiniCart"/> -+ <waitForPageLoad stepKey="waitForPageToLoad1"/> -+ <actionGroup ref="AssertStorefrontMiniCartItemsActionGroup" stepKey="assertMiniCart"> -+ <argument name="productName" value="$$createDownloadableProduct.name$$"/> -+ <argument name="productPrice" value="$123.00"/> -+ <argument name="cartSubtotal" value="123.00" /> -+ <argument name="qty" value="1"/> -+ </actionGroup> -+ -+ <!-- Select Mini Cart and select 'View And Edit Cart' --> -+ <actionGroup ref="assertOneProductNameInMiniCart" stepKey="seeProductInMiniCart"> -+ <argument name="productName" value="$$createDownloadableProduct.name$$"/> -+ </actionGroup> -+ -+ <!--Remove an item from the cart using minicart--> -+ <actionGroup ref="removeProductFromMiniCart" stepKey="removeProductFromMiniCart"> -+ <argument name="productName" value="$$createDownloadableProduct.name$$"/> -+ </actionGroup> -+ <reloadPage stepKey="reloadPage"/> -+ -+ <!--Check the minicart is empty and verify AssertProductAbsentInMiniShoppingCart--> -+ <actionGroup ref="assertMiniCartEmpty" stepKey="miniCartEnpty"/> -+ <dontSee selector="{{StorefrontMinicartSection.productLinkByName($$createDownloadableProduct.name$$)}}" stepKey="verifyAssertProductAbsentInMiniShoppingCart"/> -+ </test> -+</tests> -diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontDeleteProductsWithCartItemsDisplayDefaultLimitationFromMiniShoppingCartTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontDeleteProductsWithCartItemsDisplayDefaultLimitationFromMiniShoppingCartTest.xml -new file mode 100644 -index 00000000000..cca5268564b ---- /dev/null -+++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontDeleteProductsWithCartItemsDisplayDefaultLimitationFromMiniShoppingCartTest.xml -@@ -0,0 +1,285 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> -+ <test name="StorefrontDeleteProductsWithCartItemsDisplayDefaultLimitationFromMiniShoppingCartTest"> -+ <annotations> -+ <stories value="DeleteProductsWithCartItemsDisplayDefaultLimitation"/> -+ <title value="Storefront Delete Products With Cart Items Display Default Limitation From Mini Shopping Cart Test"/> -+ <description value="Test log in to Shopping Cart and Delete Products With Cart Items Display Default Limitation From Mini Shopping Cart Test"/> -+ <testCaseId value="MC-14687"/> -+ <severity value="CRITICAL"/> -+ <group value="Shopping Cart"/> -+ <group value="mtf_migrated"/> -+ </annotations> -+ -+ <before> -+ <actionGroup ref="LoginAsAdmin" stepKey="LoginAsAdmin"/> -+ <!--Create 10 simple products--> -+ <createData entity="SimpleProduct2" stepKey="simpleProduct1"> -+ <field key="price">10.00</field> -+ </createData> -+ <createData entity="SimpleProduct2" stepKey="simpleProduct2"> -+ <field key="price">20.00</field> -+ </createData> -+ <createData entity="SimpleProduct2" stepKey="simpleProduct3"> -+ <field key="price">30.00</field> -+ </createData> -+ <createData entity="SimpleProduct2" stepKey="simpleProduct4"> -+ <field key="price">40.00</field> -+ </createData> -+ <createData entity="SimpleProduct2" stepKey="simpleProduct5"> -+ <field key="price">50.00</field> -+ </createData> -+ <createData entity="SimpleProduct2" stepKey="simpleProduct6"> -+ <field key="price">60.00</field> -+ </createData> -+ <createData entity="SimpleProduct2" stepKey="simpleProduct7"> -+ <field key="price">70.00</field> -+ </createData> -+ <createData entity="SimpleProduct2" stepKey="simpleProduct8"> -+ <field key="price">80.00</field> -+ </createData> -+ <createData entity="SimpleProduct2" stepKey="simpleProduct9"> -+ <field key="price">90.00</field> -+ </createData> -+ <createData entity="SimpleProduct2" stepKey="simpleProduct10"> -+ <field key="price">100.00</field> -+ </createData> -+ </before> -+ <after> -+ <deleteData createDataKey="simpleProduct1" stepKey="deleteProduct1"/> -+ <deleteData createDataKey="simpleProduct2" stepKey="deleteProduct2"/> -+ <deleteData createDataKey="simpleProduct3" stepKey="deleteProduct3"/> -+ <deleteData createDataKey="simpleProduct4" stepKey="deleteProduct4"/> -+ <deleteData createDataKey="simpleProduct5" stepKey="deleteProduct5"/> -+ <deleteData createDataKey="simpleProduct6" stepKey="deleteProduct6"/> -+ <deleteData createDataKey="simpleProduct7" stepKey="deleteProduct7"/> -+ <deleteData createDataKey="simpleProduct8" stepKey="deleteProduct8"/> -+ <deleteData createDataKey="simpleProduct9" stepKey="deleteProduct9"/> -+ <deleteData createDataKey="simpleProduct10" stepKey="deleteProduct10"/> -+ <actionGroup ref="logout" stepKey="logout"/> -+ </after> -+ -+ <!--Open Product1 page in StoreFront--> -+ <actionGroup ref="AssertProductNameAndSkuInStorefrontProductPageByCustomAttributeUrlKey" stepKey="openProduct1PageAndVerifyProduct"> -+ <argument name="product" value="$$simpleProduct1$$"/> -+ </actionGroup> -+ <!--Add Product1 to the cart--> -+ <actionGroup ref="addToCartFromStorefrontProductPage" stepKey="addProduct1ToTheCart"> -+ <argument name="productName" value="$$simpleProduct1.name$$"/> -+ </actionGroup> -+ -+ <!--Open Product2 page in StoreFront--> -+ <actionGroup ref="AssertProductNameAndSkuInStorefrontProductPageByCustomAttributeUrlKey" stepKey="openProduct2PageAndVerifyProduct"> -+ <argument name="product" value="$$simpleProduct2$$"/> -+ </actionGroup> -+ <!--Add Product2 to the cart--> -+ <actionGroup ref="addToCartFromStorefrontProductPage" stepKey="addProduct2ToTheCart"> -+ <argument name="productName" value="$$simpleProduct2.name$$"/> -+ </actionGroup> -+ -+ <!--Open Product3 page in StoreFront--> -+ <actionGroup ref="AssertProductNameAndSkuInStorefrontProductPageByCustomAttributeUrlKey" stepKey="openProduct3PageAndVerifyProduct"> -+ <argument name="product" value="$$simpleProduct3$$"/> -+ </actionGroup> -+ <!--Add Product3 to the cart--> -+ <actionGroup ref="addToCartFromStorefrontProductPage" stepKey="addProduct3ToTheCart"> -+ <argument name="productName" value="$$simpleProduct3.name$$"/> -+ </actionGroup> -+ -+ <!--Open Product4 page in StoreFront--> -+ <actionGroup ref="AssertProductNameAndSkuInStorefrontProductPageByCustomAttributeUrlKey" stepKey="openProduct4PageAndVerifyProduct"> -+ <argument name="product" value="$$simpleProduct4$$"/> -+ </actionGroup> -+ <!--Add Product4 to the cart--> -+ <actionGroup ref="addToCartFromStorefrontProductPage" stepKey="addProduct4ToTheCart"> -+ <argument name="productName" value="$$simpleProduct4.name$$"/> -+ </actionGroup> -+ -+ <!--Open Product5 page in StoreFront--> -+ <actionGroup ref="AssertProductNameAndSkuInStorefrontProductPageByCustomAttributeUrlKey" stepKey="openProduct5PageAndVerifyProduct"> -+ <argument name="product" value="$$simpleProduct5$$"/> -+ </actionGroup> -+ <!--Add Product5 to the cart--> -+ <actionGroup ref="addToCartFromStorefrontProductPage" stepKey="addProduct5ToTheCart"> -+ <argument name="productName" value="$$simpleProduct5.name$$"/> -+ </actionGroup> -+ -+ <!--Open Product6 page in StoreFront--> -+ <actionGroup ref="AssertProductNameAndSkuInStorefrontProductPageByCustomAttributeUrlKey" stepKey="openProduct6PageAndVerifyProduct"> -+ <argument name="product" value="$$simpleProduct6$$"/> -+ </actionGroup> -+ <!--Add Product6 to the cart--> -+ <actionGroup ref="addToCartFromStorefrontProductPage" stepKey="addProduct6ToTheCart"> -+ <argument name="productName" value="$$simpleProduct6.name$$"/> -+ </actionGroup> -+ -+ <!--Open Product7 page in StoreFront--> -+ <actionGroup ref="AssertProductNameAndSkuInStorefrontProductPageByCustomAttributeUrlKey" stepKey="openProduct7PageAndVerifyProduct"> -+ <argument name="product" value="$$simpleProduct7$$"/> -+ </actionGroup> -+ <!--Add Product7 to the cart--> -+ <actionGroup ref="addToCartFromStorefrontProductPage" stepKey="addProduct7ToTheCart"> -+ <argument name="productName" value="$$simpleProduct7.name$$"/> -+ </actionGroup> -+ -+ <!--Open Product8 page in StoreFront--> -+ <actionGroup ref="AssertProductNameAndSkuInStorefrontProductPageByCustomAttributeUrlKey" stepKey="openProduct8PageAndVerifyProduct"> -+ <argument name="product" value="$$simpleProduct8$$"/> -+ </actionGroup> -+ <!--Add Product8 to the cart--> -+ <actionGroup ref="addToCartFromStorefrontProductPage" stepKey="addProduct8ToTheCart"> -+ <argument name="productName" value="$$simpleProduct8.name$$"/> -+ </actionGroup> -+ -+ <!--Open Product9 page in StoreFront--> -+ <actionGroup ref="AssertProductNameAndSkuInStorefrontProductPageByCustomAttributeUrlKey" stepKey="openProduct9PageAndVerifyProduct"> -+ <argument name="product" value="$$simpleProduct9$$"/> -+ </actionGroup> -+ <!--Add Product9 to the cart--> -+ <actionGroup ref="addToCartFromStorefrontProductPage" stepKey="addProduct9ToTheCart"> -+ <argument name="productName" value="$$simpleProduct9.name$$"/> -+ </actionGroup> -+ -+ <!--Open Product10 page in StoreFront--> -+ <actionGroup ref="AssertProductNameAndSkuInStorefrontProductPageByCustomAttributeUrlKey" stepKey="openProductPage10AndVerifyProduct"> -+ <argument name="product" value="$$simpleProduct10$$"/> -+ </actionGroup> -+ <!--Add Product10 to the cart--> -+ <actionGroup ref="addToCartFromStorefrontProductPage" stepKey="addProduct10ToTheCart"> -+ <argument name="productName" value="$$simpleProduct10.name$$"/> -+ </actionGroup> -+ -+ <!--Open Mini Cart--> -+ <actionGroup ref="StorefrontOpenMiniCartActionGroup" stepKey="openMiniCart"/> -+ -+ <!--Assert Product Count in Mini Cart and verify AssertVisibleItemsQtyMessageInMiniShoppingCart--> -+ <actionGroup ref="StorefrontAssertMiniCartItemCountActionGroup" stepKey="assertProductCountAndTextInMiniCart"> -+ <argument name="productCount" value="10"/> -+ <argument name="productCountText" value="10 Items in Cart"/> -+ </actionGroup> -+ -+ <!--Assert Product1 in Mini Cart--> -+ <actionGroup ref="AssertStorefrontMiniCartItemsActionGroup" stepKey="assertSimpleProduct11MiniCart"> -+ <argument name="productName" value="$$simpleProduct1.name$$"/> -+ <argument name="productPrice" value="$10.00"/> -+ <argument name="cartSubtotal" value="$550.00" /> -+ <argument name="qty" value="1"/> -+ </actionGroup> -+ <!--Assert Product2 in Mini Cart--> -+ <actionGroup ref="AssertStorefrontMiniCartItemsActionGroup" stepKey="assertSimpleProduct2MiniCart"> -+ <argument name="productName" value="$$simpleProduct2.name$$"/> -+ <argument name="productPrice" value="$20.00"/> -+ <argument name="cartSubtotal" value="$550.00" /> -+ <argument name="qty" value="1"/> -+ </actionGroup> -+ <!--Assert Product3 in Mini Cart--> -+ <actionGroup ref="AssertStorefrontMiniCartItemsActionGroup" stepKey="assertSimpleProduct3MiniCart"> -+ <argument name="productName" value="$$simpleProduct3.name$$"/> -+ <argument name="productPrice" value="$30.00"/> -+ <argument name="cartSubtotal" value="$550.00" /> -+ <argument name="qty" value="1"/> -+ </actionGroup> -+ <!--Assert Product4 in Mini Cart--> -+ <actionGroup ref="AssertStorefrontMiniCartItemsActionGroup" stepKey="assertSimpleProduct4MiniCart"> -+ <argument name="productName" value="$$simpleProduct4.name$$"/> -+ <argument name="productPrice" value="$40.00"/> -+ <argument name="cartSubtotal" value="$550.00" /> -+ <argument name="qty" value="1"/> -+ </actionGroup> -+ <!--Assert Product5 in Mini Cart--> -+ <actionGroup ref="AssertStorefrontMiniCartItemsActionGroup" stepKey="assertSimpleProduct5MiniCart"> -+ <argument name="productName" value="$$simpleProduct5.name$$"/> -+ <argument name="productPrice" value="$50.00"/> -+ <argument name="cartSubtotal" value="$550.00" /> -+ <argument name="qty" value="1"/> -+ </actionGroup> -+ <!--Assert Product6 in Mini Cart--> -+ <actionGroup ref="AssertStorefrontMiniCartItemsActionGroup" stepKey="assertSimpleProduct6MiniCart"> -+ <argument name="productName" value="$$simpleProduct6.name$$"/> -+ <argument name="productPrice" value="$60.00"/> -+ <argument name="cartSubtotal" value="$550.00" /> -+ <argument name="qty" value="1"/> -+ </actionGroup> -+ <!--Assert Product7 in Mini Cart--> -+ <actionGroup ref="AssertStorefrontMiniCartItemsActionGroup" stepKey="assertSimpleProduct7MiniCart"> -+ <argument name="productName" value="$$simpleProduct7.name$$"/> -+ <argument name="productPrice" value="$70.00"/> -+ <argument name="cartSubtotal" value="$550.00" /> -+ <argument name="qty" value="1"/> -+ </actionGroup> -+ <!--Assert Product8 in Mini Cart--> -+ <actionGroup ref="AssertStorefrontMiniCartItemsActionGroup" stepKey="assertSimpleProduct8MiniCart"> -+ <argument name="productName" value="$$simpleProduct8.name$$"/> -+ <argument name="productPrice" value="$80.00"/> -+ <argument name="cartSubtotal" value="$550.00" /> -+ <argument name="qty" value="1"/> -+ </actionGroup> -+ <!--Assert Product9 in Mini Cart--> -+ <actionGroup ref="AssertStorefrontMiniCartItemsActionGroup" stepKey="assertSimpleProduct9MiniCart"> -+ <argument name="productName" value="$$simpleProduct9.name$$"/> -+ <argument name="productPrice" value="$90.00"/> -+ <argument name="cartSubtotal" value="$550.00" /> -+ <argument name="qty" value="1"/> -+ </actionGroup> -+ <!--Assert Product10 in Mini Cart--> -+ <actionGroup ref="AssertStorefrontMiniCartItemsActionGroup" stepKey="assertSimpleProduct10MiniCart"> -+ <argument name="productName" value="$$simpleProduct10.name$$"/> -+ <argument name="productPrice" value="$100.00"/> -+ <argument name="cartSubtotal" value="$550.00" /> -+ <argument name="qty" value="1"/> -+ </actionGroup> -+ -+ <!--Remove products from minicart--> -+ <actionGroup ref="removeProductFromMiniCart" stepKey="removeProduct1FromMiniCart"> -+ <argument name="productName" value="$$simpleProduct10.name$$"/> -+ </actionGroup> -+ <actionGroup ref="removeProductFromMiniCart" stepKey="removeProduct2FromMiniCart"> -+ <argument name="productName" value="$$simpleProduct9.name$$"/> -+ </actionGroup> -+ <actionGroup ref="removeProductFromMiniCart" stepKey="removeProduct3FromMiniCart"> -+ <argument name="productName" value="$$simpleProduct8.name$$"/> -+ </actionGroup> -+ <actionGroup ref="removeProductFromMiniCart" stepKey="removeProduct4FromMiniCart"> -+ <argument name="productName" value="$$simpleProduct7.name$$"/> -+ </actionGroup> -+ <actionGroup ref="removeProductFromMiniCart" stepKey="removeProduct5FromMiniCart"> -+ <argument name="productName" value="$$simpleProduct6.name$$"/> -+ </actionGroup> -+ <actionGroup ref="removeProductFromMiniCart" stepKey="removeProduct6FromMiniCart"> -+ <argument name="productName" value="$$simpleProduct5.name$$"/> -+ </actionGroup> -+ <actionGroup ref="removeProductFromMiniCart" stepKey="removeProduct7FromMiniCart"> -+ <argument name="productName" value="$$simpleProduct4.name$$"/> -+ </actionGroup> -+ <actionGroup ref="removeProductFromMiniCart" stepKey="removeProduct8FromMiniCart"> -+ <argument name="productName" value="$$simpleProduct3.name$$"/> -+ </actionGroup> -+ <actionGroup ref="removeProductFromMiniCart" stepKey="removeProduct9FromMiniCart"> -+ <argument name="productName" value="$$simpleProduct2.name$$"/> -+ </actionGroup> -+ <actionGroup ref="removeProductFromMiniCart" stepKey="removeProduct10FromMiniCart"> -+ <argument name="productName" value="$$simpleProduct1.name$$"/> -+ </actionGroup> -+ <reloadPage stepKey="reloadPage"/> -+ -+ <!--Check the minicart is empty and verify EmptyCartMessage and AssertProductAbsentInMiniShoppingCart--> -+ <actionGroup ref="assertMiniCartEmpty" stepKey="miniCartEnpty"/> -+ <dontSee selector="{{StorefrontMinicartSection.productLinkByName($$simpleProduct1.name$$)}}" stepKey="verifyAssertProduct1AbsentInMiniShoppingCart"/> -+ <dontSee selector="{{StorefrontMinicartSection.productLinkByName($$simpleProduct2.name$$)}}" stepKey="verifyAssertProduct2AbsentInMiniShoppingCart"/> -+ <dontSee selector="{{StorefrontMinicartSection.productLinkByName($$simpleProduct3.name$$)}}" stepKey="verifyAssertProduct3AbsentInMiniShoppingCart"/> -+ <dontSee selector="{{StorefrontMinicartSection.productLinkByName($$simpleProduct4.name$$)}}" stepKey="verifyAssertProduct4AbsentInMiniShoppingCart"/> -+ <dontSee selector="{{StorefrontMinicartSection.productLinkByName($$simpleProduct5.name$$)}}" stepKey="verifyAssertProduct5AbsentInMiniShoppingCart"/> -+ <dontSee selector="{{StorefrontMinicartSection.productLinkByName($$simpleProduct6.name$$)}}" stepKey="verifyAssertProduct6AbsentInMiniShoppingCart"/> -+ <dontSee selector="{{StorefrontMinicartSection.productLinkByName($$simpleProduct7.name$$)}}" stepKey="verifyAssertProduct7AbsentInMiniShoppingCart"/> -+ <dontSee selector="{{StorefrontMinicartSection.productLinkByName($$simpleProduct8.name$$)}}" stepKey="verifyAssertProduct8AbsentInMiniShoppingCart"/> -+ <dontSee selector="{{StorefrontMinicartSection.productLinkByName($$simpleProduct9.name$$)}}" stepKey="verifyAssertProduct9AbsentInMiniShoppingCart"/> -+ <dontSee selector="{{StorefrontMinicartSection.productLinkByName($$simpleProduct10.name$$)}}" stepKey="verifyAssertProduct10AbsentInMiniShoppingCart"/> -+ </test> -+</tests> -\ No newline at end of file -diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontDeleteSimpleAndVirtualProductFromMiniShoppingCartTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontDeleteSimpleAndVirtualProductFromMiniShoppingCartTest.xml -new file mode 100644 -index 00000000000..b8092ccdcdc ---- /dev/null -+++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontDeleteSimpleAndVirtualProductFromMiniShoppingCartTest.xml -@@ -0,0 +1,88 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> -+ <test name="StorefrontDeleteSimpleAndVirtualProductFromMiniShoppingCartTest"> -+ <annotations> -+ <stories value="DeleteSimpleAndVirtualProduct"/> -+ <title value="Storefront Delete Simple And Virtual Product From Mini Shopping Cart Test"/> -+ <description value="Test log in to Shopping Cart and Delete Simple And Virtual Product From Mini Shopping Cart Test"/> -+ <testCaseId value="MC-14685"/> -+ <severity value="CRITICAL"/> -+ <group value="Shopping Cart"/> -+ <group value="mtf_migrated"/> -+ </annotations> -+ -+ <before> -+ <actionGroup ref="LoginAsAdmin" stepKey="LoginAsAdmin"/> -+ <!--Create simple product--> -+ <createData entity="SimpleProduct2" stepKey="simpleProduct"> -+ <field key="price">10.00</field> -+ </createData> -+ <!--Create virtual product--> -+ <createData entity="VirtualProduct" stepKey="virtualProduct"> -+ <field key="price">20.00</field> -+ </createData> -+ </before> -+ <after> -+ <deleteData createDataKey="simpleProduct" stepKey="deleteSimpleProduct"/> -+ <deleteData createDataKey="virtualProduct" stepKey="deleteVirtualproduct"/> -+ <actionGroup ref="logout" stepKey="logout"/> -+ </after> -+ -+ <!-- Add Simple Product to the cart --> -+ <actionGroup ref="AddSimpleProductToCart" stepKey="addSimpleProductToCart"> -+ <argument name="product" value="$$simpleProduct$$"/> -+ </actionGroup> -+ <!-- Add virtual Product to the cart --> -+ <amOnPage url="{{StorefrontProductPage.url($$virtualProduct.name$$)}}" stepKey="amOnStorefrontVirtualProductPage"/> -+ <waitForPageLoad stepKey="waitForPageLoad"/> -+ <actionGroup ref="addToCartFromStorefrontProductPage" stepKey="addProduct1ToTheCart"> -+ <argument name="productName" value="$$virtualProduct.name$$"/> -+ </actionGroup> -+ -+ <!-- Assert Simple and Virtual products in mini cart --> -+ <click selector="{{StorefrontMinicartSection.showCart}}" stepKey="clickOnMiniCart"/> -+ <waitForPageLoad stepKey="waitForPageToLoad1"/> -+ <actionGroup ref="AssertStorefrontMiniCartItemsActionGroup" stepKey="assertSimpleProductInMiniCart"> -+ <argument name="productName" value="$$simpleProduct.name$$"/> -+ <argument name="productPrice" value="$10.00"/> -+ <argument name="cartSubtotal" value="$30.00" /> -+ <argument name="qty" value="1"/> -+ </actionGroup> -+ -+ <actionGroup ref="AssertStorefrontMiniCartItemsActionGroup" stepKey="assertVirtualProductInMiniCart"> -+ <argument name="productName" value="$$virtualProduct.name$$"/> -+ <argument name="productPrice" value="$20.00"/> -+ <argument name="cartSubtotal" value="$30.00" /> -+ <argument name="qty" value="1"/> -+ </actionGroup> -+ -+ <!-- Select mini Cart and verify Simple and Virtual products names in cart--> -+ <actionGroup ref="assertOneProductNameInMiniCart" stepKey="seeSimpleProductInMiniCart"> -+ <argument name="productName" value="$$simpleProduct.name$$"/> -+ </actionGroup> -+ <actionGroup ref="assertOneProductNameInMiniCart" stepKey="seeVirtualProductInMiniCart"> -+ <argument name="productName" value="$$virtualProduct.name$$"/> -+ </actionGroup> -+ -+ <!--Remove Simple and Virtual products from mini cart--> -+ <actionGroup ref="removeProductFromMiniCart" stepKey="removeProductFromMiniCart"> -+ <argument name="productName" value="$$simpleProduct.name$$"/> -+ </actionGroup> -+ <actionGroup ref="removeProductFromMiniCart" stepKey="removeVirtualProductFromMiniCart"> -+ <argument name="productName" value="$$virtualProduct.name$$"/> -+ </actionGroup> -+ <reloadPage stepKey="reloadPage"/> -+ -+ <!--Check the minicart is empty and verify EmptyCartMessage and AssertProductAbsentInMiniShoppingCart--> -+ <actionGroup ref="assertMiniCartEmpty" stepKey="miniCartEnpty"/> -+ <dontSee selector="{{StorefrontMinicartSection.productLinkByName($$simpleProduct.name$$)}}" stepKey="verifyAssertSimpleProductAbsentInMiniShoppingCart"/> -+ <dontSee selector="{{StorefrontMinicartSection.productLinkByName($$virtualProduct.name$$)}}" stepKey="verifyAssertVirtualProductAbsentInMiniShoppingCart"/> -+ </test> -+</tests> -\ No newline at end of file -diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontDeleteSimpleProductFromMiniShoppingCartTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontDeleteSimpleProductFromMiniShoppingCartTest.xml -new file mode 100644 -index 00000000000..05198060f5d ---- /dev/null -+++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontDeleteSimpleProductFromMiniShoppingCartTest.xml -@@ -0,0 +1,63 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> -+ <test name="StorefrontDeleteSimpleProductFromMiniShoppingCartTest"> -+ <annotations> -+ <stories value="DeleteSimpleProduct"/> -+ <title value="Storefront Delete Simple Product From Mini Shopping Cart Test"/> -+ <description value="Test log in to Shopping Cart and Delete Simple Product From Mini Shopping Cart Test"/> -+ <testCaseId value="MC-14686"/> -+ <severity value="CRITICAL"/> -+ <group value="Shopping Cart"/> -+ <group value="mtf_migrated"/> -+ </annotations> -+ -+ <before> -+ <actionGroup ref="LoginAsAdmin" stepKey="LoginAsAdmin"/> -+ <!--Create simple product--> -+ <createData entity="SimpleProduct2" stepKey="simpleProduct"> -+ <field key="price">10.00</field> -+ </createData> -+ </before> -+ <after> -+ <deleteData createDataKey="simpleProduct" stepKey="deleteProduct"/> -+ <actionGroup ref="logout" stepKey="logout"/> -+ </after> -+ -+ <!-- Add Simple Product to the cart --> -+ <actionGroup ref="AddSimpleProductToCart" stepKey="addProductToCart"> -+ <argument name="product" value="$$simpleProduct$$"/> -+ </actionGroup> -+ -+ <!-- Assert Product in Mini Cart --> -+ <click selector="{{StorefrontMinicartSection.showCart}}" stepKey="clickOnMiniCart"/> -+ <waitForPageLoad stepKey="waitForPageToLoad1"/> -+ <actionGroup ref="AssertStorefrontMiniCartItemsActionGroup" stepKey="assertSimpleProduct3MiniCart"> -+ <argument name="productName" value="$$simpleProduct.name$$"/> -+ <argument name="productPrice" value="$10.00"/> -+ <argument name="cartSubtotal" value="$10.00" /> -+ <argument name="qty" value="1"/> -+ </actionGroup> -+ -+ <!-- Select Mini Cart and select 'View And Edit Cart' --> -+ <actionGroup ref="assertOneProductNameInMiniCart" stepKey="seeProductInMiniCart"> -+ <argument name="productName" value="$$simpleProduct.name$$"/> -+ </actionGroup> -+ -+ <!--Remove an item from the cart using minicart--> -+ <actionGroup ref="removeProductFromMiniCart" stepKey="removeProductFromMiniCart"> -+ <argument name="productName" value="$$simpleProduct.name$$"/> -+ </actionGroup> -+ <reloadPage stepKey="reloadPage"/> -+ -+ <!--Check the minicart is empty and verify AssertProductAbsentInMiniShoppingCart--> -+ <actionGroup ref="assertMiniCartEmpty" stepKey="miniCartEnpty"/> -+ <dontSee selector="{{StorefrontMinicartSection.productLinkByName($$simpleProduct.name$$)}}" stepKey="verifyAssertProductAbsentInMiniShoppingCart"/> -+ </test> -+</tests> -\ No newline at end of file -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 00000000000..626f095604f ---- /dev/null -+++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontGuestCheckoutDataPersistTest.xml -@@ -0,0 +1,57 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> -+ <test name="StorefrontGuestCheckoutDataPersistTest"> -+ <annotations> -+ <features value="Checkout"/> -+ <stories value="MAGETWO-95068: Checkout data (shipping address etc) not persistant after cart update"/> -+ <title value="Check that checkout data persist after cart update"/> -+ <description value="Checkout data should be persist after updating cart"/> -+ <severity value="CRITICAL"/> -+ <testCaseId value="MAGETWO-96979"/> -+ <group value="checkout"/> -+ </annotations> -+ <before> -+ <createData entity="_defaultCategory" stepKey="createCategory"/> -+ <createData entity="_defaultProduct" stepKey="createProduct"> -+ <requiredEntity createDataKey="createCategory"/> -+ </createData> -+ </before> -+ <after> -+ <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> -+ <deleteData createDataKey="createProduct" stepKey="deleteProduct"/> -+ </after> -+ -+ <!-- Add simple product to cart --> -+ <actionGroup ref="AddSimpleProductToCart" stepKey="addProductToCart"> -+ <argument name="product" value="$$createProduct$$"/> -+ </actionGroup> -+ <!-- Navigate to checkout --> -+ <actionGroup ref="GoToCheckoutFromMinicartActionGroup" stepKey="goToCheckoutFromMinicart"/> -+ <!-- Fill shipping address --> -+ <actionGroup ref="GuestCheckoutFillingShippingSectionActionGroup" stepKey="guestCheckoutFillingShipping"> -+ <argument name="shippingMethod" value="Flat Rate"/> -+ </actionGroup> -+ <!-- Add simple product to cart --> -+ <actionGroup ref="AddSimpleProductToCart" stepKey="addProductToCart1"> -+ <argument name="product" value="$$createProduct$$"/> -+ </actionGroup> -+ <!-- Navigate to checkout --> -+ <actionGroup ref="GoToCheckoutFromMinicartActionGroup" stepKey="goToCheckoutFromMinicart1"/> -+ <seeInField selector="{{CheckoutShippingGuestInfoSection.email}}" userInput="{{CustomerEntityOne.email}}" stepKey="assertGuestEmail"/> -+ <seeInField selector="{{CheckoutShippingGuestInfoSection.firstName}}" userInput="{{CustomerEntityOne.firstname}}" stepKey="assertGuestFirstName"/> -+ <seeInField selector="{{CheckoutShippingGuestInfoSection.lastName}}" userInput="{{CustomerEntityOne.lastname}}" stepKey="assertGuestLastName"/> -+ <seeInField selector="{{CheckoutShippingGuestInfoSection.street}}" userInput="{{CustomerAddressSimple.street[0]}}" stepKey="assertGuestStreet"/> -+ <seeInField selector="{{CheckoutShippingGuestInfoSection.city}}" userInput="{{CustomerAddressSimple.city}}" stepKey="assertGuestCity"/> -+ <seeInField selector="{{CheckoutShippingGuestInfoSection.region}}" userInput="{{CustomerAddressSimple.state}}" stepKey="assertGuestRegion"/> -+ <seeInField selector="{{CheckoutShippingGuestInfoSection.postcode}}" userInput="{{CustomerAddressSimple.postcode}}" stepKey="assertGuestPostcode"/> -+ <seeInField selector="{{CheckoutShippingGuestInfoSection.telephone}}" userInput="{{CustomerAddressSimple.telephone}}" stepKey="assertGuestTelephone"/> -+ </test> -+</tests> -diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontGuestCheckoutForSpecificCountriesTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontGuestCheckoutForSpecificCountriesTest.xml -new file mode 100644 -index 00000000000..f6a332a529d ---- /dev/null -+++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontGuestCheckoutForSpecificCountriesTest.xml -@@ -0,0 +1,98 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> -+ <test name="StorefrontGuestCheckoutForSpecificCountriesTest"> -+ <annotations> -+ <features value="One Page Checkout"/> -+ <stories value="Checkout for Specific Countries"/> -+ <title value="Storefront guest checkout for specific countries test"/> -+ <description value="Checkout flow if shipping rates are not available"/> -+ <severity value="CRITICAL"/> -+ <testCaseId value="MC-13414"/> -+ <group value="checkout"/> -+ </annotations> -+ <before> -+ <!-- Create simple product --> -+ <createData entity="SimpleProduct2" stepKey="createProduct"/> -+ -+ <!-- Enable free shipping to specific country - Afghanistan --> -+ <magentoCLI command="config:set {{EnableFreeShippingConfigData.path}} {{EnableFreeShippingConfigData.value}}" stepKey="enableFreeShipping"/> -+ <magentoCLI command="config:set {{EnableFreeShippingToSpecificCountriesConfigData.path}} {{EnableFreeShippingToSpecificCountriesConfigData.value}}" stepKey="allowFreeShippingSpecificCountries"/> -+ <magentoCLI command="config:set {{EnableFreeShippingToAfghanistanConfigData.path}} {{EnableFreeShippingToAfghanistanConfigData.value}}" stepKey="enableFreeShippingToAfghanistan"/> -+ -+ <!-- Enable flat rate shipping to specific country - Afghanistan --> -+ <magentoCLI command="config:set {{EnableFlatRateConfigData.path}} {{EnableFlatRateConfigData.value}}" stepKey="enableFlatRate"/> -+ <magentoCLI command="config:set {{EnableFlatRateToSpecificCountriesConfigData.path}} {{EnableFlatRateToSpecificCountriesConfigData.value}}" stepKey="allowFlatRateSpecificCountries"/> -+ <magentoCLI command="config:set {{EnableFlatRateToAfghanistanConfigData.path}} {{EnableFlatRateToAfghanistanConfigData.value}}" stepKey="enableFlatRateToAfghanistan"/> -+ </before> -+ <after> -+ <!-- Rollback all configurations --> -+ <magentoCLI command="config:set {{DisableFreeShippingConfigData.path}} {{DisableFreeShippingConfigData.value}}" stepKey="disableFreeShipping"/> -+ <magentoCLI command="config:set {{EnableFreeShippingToAllAllowedCountriesConfigData.path}} {{EnableFreeShippingToAllAllowedCountriesConfigData.value}}" stepKey="allowFreeShippingToAllCountries"/> -+ <magentoCLI command="config:set {{EnableFlatRateToAllAllowedCountriesConfigData.path}} {{EnableFlatRateToAllAllowedCountriesConfigData.value}}" stepKey="allowFlatRateToAllCountries"/> -+ -+ <!-- Delete product --> -+ <deleteData createDataKey="createProduct" stepKey="deleteProduct"/> -+ </after> -+ -+ <!-- Add product to cart --> -+ <actionGroup ref="OpenStoreFrontProductPageActionGroup" stepKey="openProductPage"> -+ <argument name="productUrlKey" value="$$createProduct.custom_attributes[url_key]$$"/> -+ </actionGroup> -+ <actionGroup ref="StorefrontAddProductToCartActionGroup" stepKey="addProductToCart"> -+ <argument name="product" value="$$createProduct$$"/> -+ <argument name="productCount" value="1"/> -+ </actionGroup> -+ -+ <!-- Go to checkout page --> -+ <actionGroup ref="OpenStoreFrontCheckoutShippingPageActionGroup" stepKey="openCheckoutShippingPage"/> -+ -+ <!-- Assert shipping methods are unavailable --> -+ <actionGroup ref="AssertStoreFrontShippingMethodUnavailableActionGroup" stepKey="dontSeeFlatRateShippingMethod"> -+ <argument name="shippingMethodName" value="Flat Rate"/> -+ </actionGroup> -+ <actionGroup ref="AssertStoreFrontShippingMethodUnavailableActionGroup" stepKey="dontFreeShippingMethod"/> -+ -+ <!-- Assert no quotes message --> -+ <actionGroup ref="AssertStoreFrontNoQuotesMessageActionGroup" stepKey="assertNoQuotesMessage"/> -+ -+ <!-- Assert Next button --> -+ <dontSeeElement selector="{{CheckoutShippingMethodsSection.next}}" stepKey="dontSeeNextButton"/> -+ -+ <!-- Fill form with valid data for US > California --> -+ <selectOption selector="{{CheckoutShippingSection.country}}" userInput="{{US_Address_CA.country}}" stepKey="selectCountry"/> -+ <selectOption selector="{{CheckoutShippingSection.region}}" userInput="{{US_Address_CA.state}}" stepKey="selectState"/> -+ <fillField selector="{{CheckoutShippingSection.postcode}}" userInput="{{US_Address_CA.postcode}}" stepKey="fillPostcode"/> -+ -+ <!-- Assert shipping methods are unavailable for US > California --> -+ <actionGroup ref="AssertStoreFrontShippingMethodUnavailableActionGroup" stepKey="dontSeeFlatRateShippingMtd"> -+ <argument name="shippingMethodName" value="Flat Rate"/> -+ </actionGroup> -+ <actionGroup ref="AssertStoreFrontShippingMethodUnavailableActionGroup" stepKey="dontFreeShippingMtd"/> -+ -+ <!-- Assert no quotes message for US > California --> -+ <actionGroup ref="AssertStoreFrontNoQuotesMessageActionGroup" stepKey="assertNoQuotesMsg"/> -+ -+ <!-- Assert Next button for US > California --> -+ <dontSeeElement selector="{{CheckoutShippingMethodsSection.next}}" stepKey="dontSeeNextBtn"/> -+ -+ <!-- Fill form for specific country - Afghanistan --> -+ <selectOption selector="{{CheckoutShippingSection.country}}" userInput="Afghanistan" stepKey="selectSpecificCountry"/> -+ -+ <!-- Assert shipping methods are available --> -+ <actionGroup ref="AssertStoreFrontShippingMethodAvailableActionGroup" stepKey="seeFlatRateShippingMethod"/> -+ <actionGroup ref="AssertStoreFrontShippingMethodAvailableActionGroup" stepKey="seeFreeShippingMethod"> -+ <argument name="shippingMethodName" value="Free Shipping"/> -+ </actionGroup> -+ -+ <!-- Assert Next button is available --> -+ <seeElement selector="{{CheckoutShippingMethodsSection.next}}" stepKey="seeNextButton"/> -+ </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 9d88e42447c..a77341b8697 100644 ---- a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontGuestCheckoutTest.xml -+++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontGuestCheckoutTest.xml -@@ -7,15 +7,15 @@ - --> - - <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="StorefrontGuestCheckoutTest"> - <annotations> - <features value="Checkout"/> - <stories value="Checkout via Guest Checkout"/> -- <title value="Guest Checkout"/> -- <description value="Should be able to place an order as a Guest."/> -+ <title value="Guest Checkout - guest should be able to place an order"/> -+ <description value="Should be able to place an order as a Guest"/> - <severity value="CRITICAL"/> -- <testCaseId value="MAGETWO-72094"/> -+ <testCaseId value="MC-12825"/> - <group value="checkout"/> - </annotations> - <before> -@@ -57,15 +57,97 @@ - - <amOnPage url="{{AdminOrdersPage.url}}" stepKey="onOrdersPage"/> - <waitForLoadingMaskToDisappear stepKey="waitForLoadingMaskToDisappearOnOrdersPage"/> -+ <actionGroup ref="clearFiltersAdminDataGrid" stepKey="clearGridFilter"/> - <fillField selector="{{AdminOrdersGridSection.search}}" userInput="{$grabOrderNumber}" stepKey="fillOrderNum"/> - <click selector="{{AdminOrdersGridSection.submitSearch}}" stepKey="submitSearchOrderNum"/> - <waitForLoadingMaskToDisappear stepKey="waitForLoadingMaskToDisappearOnSearch"/> - <click selector="{{AdminOrdersGridSection.firstRow}}" stepKey="clickOrderRow"/> -+ <waitForPageLoad stepKey="waitForOrderPageToLoad"/> - <see selector="{{AdminOrderDetailsInformationSection.orderStatus}}" userInput="Pending" stepKey="seeAdminOrderStatus"/> -- <see selector="{{AdminOrderDetailsInformationSection.accountInformation}}" userInput="Guest" stepKey="seeAdminOrderGuest"/> -+ <see selector="{{AdminOrderDetailsInformationSection.accountInformation}}" userInput="{{CustomerEntityOne.fullname}}" stepKey="seeAdminOrderGuest"/> - <see selector="{{AdminOrderDetailsInformationSection.accountInformation}}" userInput="{{CustomerEntityOne.email}}" stepKey="seeAdminOrderEmail"/> - <see selector="{{AdminOrderDetailsInformationSection.billingAddress}}" userInput="{{CustomerAddressSimple.street[0]}}" stepKey="seeAdminOrderBillingAddress"/> - <see selector="{{AdminOrderDetailsInformationSection.shippingAddress}}" userInput="{{CustomerAddressSimple.street[0]}}" stepKey="seeAdminOrderShippingAddress"/> - <see selector="{{AdminOrderDetailsInformationSection.itemsOrdered}}" userInput="$$createProduct.name$$" stepKey="seeAdminOrderProduct"/> - </test> -+ <test name="StorefrontGuestCheckoutWithSidebarDisabledTest" extends="StorefrontGuestCheckoutTest"> -+ <annotations> -+ <features value="Checkout"/> -+ <stories value="Checkout via Guest Checkout"/> -+ <title value="Guest Checkout when Cart sidebar disabled"/> -+ <description value="Should be able to place an order as a Guest when Cart sidebar is disabled"/> -+ <severity value="CRITICAL"/> -+ <testCaseId value="MAGETWO-97001"/> -+ <group value="checkout"/> -+ <skip> -+ <issueId value="MC-17140"/> -+ </skip> -+ </annotations> -+ <before> -+ <magentoCLI stepKey="disableSidebar" command="config:set checkout/sidebar/display 0" /> -+ </before> -+ <after> -+ <magentoCLI stepKey="enableSidebar" command="config:set checkout/sidebar/display 1" /> -+ </after> -+ <remove keyForRemoval="guestGoToCheckoutFromMinicart" /> -+ <actionGroup ref="GoToCheckoutFromCartActionGroup" stepKey="guestGoToCheckoutFromCart" after="seeCartQuantity" /> -+ </test> -+ <test name="StorefrontGuestCheckoutTestWithRestrictedCountriesForPayment"> -+ <annotations> -+ <features value="Checkout"/> -+ <stories value="Checkout flow if payment solutions are not available"/> -+ <title value="Checkout via Guest Checkout with restricted countries for payment"/> -+ <description value="Should be able to place an order as a Guest with restricted countries for payment."/> -+ <severity value="CRITICAL"/> -+ <testCaseId value="MAGETWO-42653"/> -+ <group value="checkout"/> -+ </annotations> -+ <before> -+ <createData entity="_defaultCategory" stepKey="createCategory"/> -+ <createData entity="ApiSimpleProduct" stepKey="createProduct"> -+ <requiredEntity createDataKey="createCategory"/> -+ </createData> -+ <magentoCLI stepKey="allowSpecificValue" command="config:set payment/checkmo/allowspecific 1" /> -+ <magentoCLI stepKey="specificCountryValue" command="config:set payment/checkmo/specificcountry GB" /> -+ -+ </before> -+ <after> -+ <amOnPage url="admin/admin/auth/logout/" stepKey="amOnLogoutPage"/> -+ <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> -+ <deleteData createDataKey="createProduct" stepKey="deleteProduct"/> -+ <magentoCLI stepKey="allowSpecificValue" command="config:set payment/checkmo/allowspecific 0" /> -+ <magentoCLI stepKey="specificCountryValue" command="config:set payment/checkmo/specificcountry ''" /> -+ </after> -+ -+ <!-- Add product to cart --> -+ <amOnPage url="{{StorefrontCategoryPage.url($$createCategory.name$$)}}" stepKey="onCategoryPage"/> -+ <waitForPageLoad stepKey="waitForPageLoad1"/> -+ <moveMouseOver selector="{{StorefrontCategoryMainSection.ProductItemInfo}}" stepKey="hoverProduct"/> -+ <click selector="{{StorefrontCategoryMainSection.AddToCartBtn}}" stepKey="addToCart"/> -+ <waitForElementVisible selector="{{StorefrontCategoryMainSection.SuccessMsg}}" time="30" stepKey="waitForProductAdded"/> -+ <see selector="{{StorefrontCategoryMainSection.SuccessMsg}}" userInput="You added $$createProduct.name$$ to your shopping cart." stepKey="seeAddedToCartMessage"/> -+ <see selector="{{StorefrontMinicartSection.quantity}}" userInput="1" stepKey="seeCartQuantity"/> -+ -+ <!-- Go to checkout page --> -+ <actionGroup ref="GoToCheckoutFromMinicartActionGroup" stepKey="guestGoToCheckoutFromMinicart" /> -+ -+ <!-- Fill US Address and verify that no payment available --> -+ <actionGroup ref="GuestCheckoutWithSpecificCountryOptionForPaymentMethodActionGroup" stepKey="guestCheckoutFillingShippingSection"> -+ <argument name="customerVar" value="CustomerEntityOne" /> -+ <argument name="customerAddressVar" value="CustomerAddressSimple" /> -+ <argument name="paymentMethod" value="Check / Money order"/> -+ </actionGroup> -+ -+ <!-- Fill UK Address and verify that payment available and checkout successful --> -+ <click selector="{{CheckoutHeaderSection.shippingMethodStep}}" stepKey="goToShipping" /> -+ <actionGroup ref="GuestCheckoutFillingShippingSectionWithoutRegionActionGroup" stepKey="guestCheckoutFillingShippingSectionUK"> -+ <argument name="customerVar" value="CustomerEntityOne" /> -+ <argument name="customerAddressVar" value="UK_Not_Default_Address" /> -+ </actionGroup> -+ <actionGroup ref="CheckoutSelectCheckMoneyOrderPaymentActionGroup" stepKey="guestSelectCheckMoneyOrderPayment" /> -+ <actionGroup ref="CheckoutPlaceOrderActionGroup" stepKey="guestPlaceorder"> -+ <argument name="orderNumberMessage" value="CONST.successGuestCheckoutOrderNumberMessage" /> -+ <argument name="emailYouMessage" value="CONST.successCheckoutEmailYouMessage" /> -+ </actionGroup> -+ </test> - </tests> -diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontOnePageCheckoutDataWhenChangeQtyTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontOnePageCheckoutDataWhenChangeQtyTest.xml -new file mode 100644 -index 00000000000..913eb34b34d ---- /dev/null -+++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontOnePageCheckoutDataWhenChangeQtyTest.xml -@@ -0,0 +1,95 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> -+ <test name="StorefrontOnePageCheckoutDataWhenChangeQtyTest"> -+ <annotations> -+ <stories value="Checkout"/> -+ <title value="One page Checkout Customer data when changing Product Qty"/> -+ <description value="One page Checkout Customer data when changing Product Qty"/> -+ <severity value="MAJOR"/> -+ <testCaseId value="MAGETWO-96960"/> -+ <useCaseId value="MAGETWO-96850"/> -+ <group value="checkout"/> -+ </annotations> -+ <before> -+ <!--Create a product--> -+ <createData entity="SimpleProduct2" stepKey="createProduct"/> -+ </before> -+ <after> -+ <!--Delete created data--> -+ <deleteData createDataKey="createProduct" stepKey="deleteProduct"/> -+ </after> -+ -+ <!--Add product to cart and checkout--> -+ <amOnPage url="{{StorefrontProductPage.url($$createProduct.name$$)}}" stepKey="amOnSimpleProductPage"/> -+ <waitForPageLoad stepKey="waitForPageLoad"/> -+ <actionGroup ref="addToCartFromStorefrontProductPage" stepKey="addToCartFromStorefrontProductPage"> -+ <argument name="productName" value="$createProduct.name$$"/> -+ </actionGroup> -+ <actionGroup ref="GoToCheckoutFromMinicartActionGroup" stepKey="goToCheckoutFromMinicart"/> -+ <fillField selector="{{CheckoutShippingSection.email}}" userInput="{{CustomerEntityOne.email}}" stepKey="enterEmail"/> -+ <fillField selector="{{CheckoutShippingSection.firstName}}" userInput="{{CustomerEntityOne.firstname}}" stepKey="enterFirstName"/> -+ <fillField selector="{{CheckoutShippingSection.lastName}}" userInput="{{CustomerEntityOne.lastname}}" stepKey="enterLastName"/> -+ <fillField selector="{{CheckoutShippingSection.street}}" userInput="{{CustomerAddressSimple.street[0]}}" stepKey="enterStreet"/> -+ <fillField selector="{{CheckoutShippingSection.city}}" userInput="{{CustomerAddressSimple.city}}" stepKey="enterCity"/> -+ <selectOption selector="{{CheckoutShippingSection.region}}" userInput="{{CustomerAddressSimple.state}}" stepKey="selectRegion"/> -+ <fillField selector="{{CheckoutShippingSection.postcode}}" userInput="{{CustomerAddressSimple.postcode}}" stepKey="enterPostcode"/> -+ <fillField selector="{{CheckoutShippingSection.telephone}}" userInput="{{CustomerAddressSimple.telephone}}" stepKey="enterTelephone"/> -+ <waitForLoadingMaskToDisappear stepKey="waitForLoadingMask"/> -+ -+ <!--Grab customer data to check it--> -+ <grabValueFrom selector="{{CheckoutShippingSection.email}}" stepKey="grabEmail"/> -+ <grabValueFrom selector="{{CheckoutShippingSection.firstName}}" stepKey="grabFirstName"/> -+ <grabValueFrom selector="{{CheckoutShippingSection.lastName}}" stepKey="grabLastName"/> -+ <grabValueFrom selector="{{CheckoutShippingSection.street}}" stepKey="grabStreet"/> -+ <grabValueFrom selector="{{CheckoutShippingSection.city}}" stepKey="grabCity"/> -+ <grabTextFrom selector="{{CheckoutShippingSection.region}}" stepKey="grabRegion"/> -+ <grabValueFrom selector="{{CheckoutShippingSection.postcode}}" stepKey="grabPostcode"/> -+ <grabValueFrom selector="{{CheckoutShippingSection.telephone}}" stepKey="grabTelephone"/> -+ -+ <!--Select shipping method and finalize checkout--> -+ <click selector="{{CheckoutShippingSection.firstShippingMethod}}" stepKey="selectFirstShippingMethod"/> -+ <waitForElement selector="{{CheckoutShippingSection.next}}" time="30" stepKey="waitForNextButton"/> -+ <waitForPageLoad stepKey="waitForShippingMethodLoad"/> -+ <click selector="{{CheckoutShippingSection.next}}" stepKey="clickNext"/> -+ <waitForElement selector="{{CheckoutPaymentSection.paymentSectionTitle}}" time="30" stepKey="waitForPaymentSectionLoaded"/> -+ <seeInCurrentUrl url="{{CheckoutPage.url}}/#payment" stepKey="assertCheckoutPaymentUrl"/> -+ -+ <!--Go to cart page, update qty and proceed to checkout--> -+ <amOnPage url="{{CheckoutCartPage.url}}" stepKey="goToCartPage"/> -+ <waitForPageLoad stepKey="waitForCartPageLoad"/> -+ <see userInput="Shopping Cart" stepKey="seeCartPageIsOpened"/> -+ <fillField selector="{{CheckoutCartProductSection.qty($$createProduct.name$$)}}" userInput="2" stepKey="updateProductQty"/> -+ <click selector="{{CheckoutCartProductSection.updateShoppingCartButton}}" stepKey="clickUpdateShoppingCart"/> -+ <waitForAjaxLoad stepKey="waitForAjaxLoad"/> -+ <grabValueFrom selector="{{CheckoutCartProductSection.qty($$createProduct.name$$)}}" stepKey="grabQty"/> -+ <assertEquals expected="2" actual="$grabQty" stepKey="assertQty"/> -+ <click selector="{{CheckoutCartSummarySection.proceedToCheckout}}" stepKey="clickProceedToCheckout"/> -+ -+ <!--Check that form is filled with customer data--> -+ <grabValueFrom selector="{{CheckoutShippingSection.email}}" stepKey="grabEmail1"/> -+ <grabValueFrom selector="{{CheckoutShippingSection.firstName}}" stepKey="grabFirstName1"/> -+ <grabValueFrom selector="{{CheckoutShippingSection.lastName}}" stepKey="grabLastName1"/> -+ <grabValueFrom selector="{{CheckoutShippingSection.street}}" stepKey="grabStreet1"/> -+ <grabValueFrom selector="{{CheckoutShippingSection.city}}" stepKey="grabCity1"/> -+ <grabTextFrom selector="{{CheckoutShippingSection.region}}" stepKey="grabRegion1"/> -+ <grabValueFrom selector="{{CheckoutShippingSection.postcode}}" stepKey="grabPostcode1"/> -+ <grabValueFrom selector="{{CheckoutShippingSection.telephone}}" stepKey="grabTelephone1"/> -+ -+ <assertEquals expected="$grabEmail" actual="$grabEmail1" stepKey="assertEmail"/> -+ <assertEquals expected="$grabFirstName" actual="$grabFirstName1" stepKey="assertFirstName"/> -+ <assertEquals expected="$grabLastName" actual="$grabLastName1" stepKey="assertLastName"/> -+ <assertEquals expected="$grabStreet" actual="$grabStreet1" stepKey="assertStreet"/> -+ <assertEquals expected="$grabCity" actual="$grabCity1" stepKey="assertCity"/> -+ <assertEquals expected="$grabRegion" actual="$grabRegion1" stepKey="assertRegion"/> -+ <assertEquals expected="$grabPostcode" actual="$grabPostcode1" stepKey="assertPostcode"/> -+ <assertEquals expected="$grabTelephone" actual="$grabTelephone1" stepKey="assertTelephone"/> -+ </test> -+</tests> -diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontPersistentDataForGuestCustomerWithPhysicalQuoteTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontPersistentDataForGuestCustomerWithPhysicalQuoteTest.xml -new file mode 100644 -index 00000000000..20ff67a076e ---- /dev/null -+++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontPersistentDataForGuestCustomerWithPhysicalQuoteTest.xml -@@ -0,0 +1,90 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> -+ <test name="StorefrontPersistentDataForGuestCustomerWithPhysicalQuoteTest"> -+ <annotations> -+ <features value="Checkout"/> -+ <stories value="Checkout via Guest Checkout"/> -+ <title value="Persistent Data for Guest Customer with physical quote"/> -+ <description value="One can use Persistent Data for Guest Customer with physical quote"/> -+ <severity value="MAJOR"/> -+ <testCaseId value="MC-13479"/> -+ <group value="checkout"/> -+ </annotations> -+ <before> -+ <createData entity="SimpleProduct2" stepKey="createProduct"> -+ <field key="price">10</field> -+ </createData> -+ <createData entity="FreeShippinMethodConfig" stepKey="enableFreeShipping"/> -+ <amOnPage url="{{StorefrontHomePage.url}}" stepKey="goToStorefront"/> -+ <executeJS function="window.localStorage.clear();" stepKey="clearLocalStorage"/> -+ </before> -+ <after> -+ <deleteData createDataKey="createProduct" stepKey="deleteProduct"/> -+ <createData entity="FreeShippinMethodDefault" stepKey="disableFreeShipping"/> -+ </after> -+ <!-- 1. Add simple product to cart and go to checkout--> -+ <actionGroup ref="AddSimpleProductToCart" stepKey="addSimpleProductToCart"> -+ <argument name="product" value="$$createProduct$$"/> -+ </actionGroup> -+ <!-- 2. Go to Shopping Cart --> -+ <amOnPage url="{{CheckoutCartPage.url}}" stepKey="goToCheckoutCartIndexPage"/> -+ <!-- 3. Open "Estimate Shipping and Tax" section and input data --> -+ <actionGroup ref="StorefrontCartEstimateShippingAndTaxActionGroup" stepKey="fillEstimateShippingAndTaxSection"/> -+ <actionGroup ref="StorefrontAssertShippingMethodPresentInCartActionGroup" stepKey="assertShippingMethodFlatRateIsPresentInCart"> -+ <argument name="shippingMethod" value="Flat Rate"/> -+ </actionGroup> -+ <actionGroup ref="StorefrontAssertShippingMethodPresentInCartActionGroup" stepKey="assertShippingMethodFreeShippingIsPresentInCart"> -+ <argument name="shippingMethod" value="Free Shipping"/> -+ </actionGroup> -+ <!-- 4. Select Flat Rate as shipping --> -+ <checkOption selector="{{CheckoutCartSummarySection.flatRateShippingMethod}}" stepKey="selectFlatRateShippingMethod"/> -+ <waitForLoadingMaskToDisappear stepKey="waitForLoadingMaskToDisappearAfterFlatRateSelection"/> -+ <see selector="{{CheckoutCartSummarySection.total}}" userInput="15" stepKey="assertOrderTotalField"/> -+ <!-- 5. Refresh browser page (F5) --> -+ <reloadPage stepKey="reloadPage"/> -+ <waitForPageLoad stepKey="waitForPageLoad"/> -+ <actionGroup ref="StorefrontAssertCartEstimateShippingAndTaxActionGroup" stepKey="assertCartEstimateShippingAndTaxAfterPageReload"/> -+ <actionGroup ref="StorefrontAssertCartShippingMethodSelectedActionGroup" stepKey="assertFlatRateShippingMethodIsChecked"> -+ <argument name="carrierCode" value="flatrate"/> -+ <argument name="methodCode" value="flatrate"/> -+ </actionGroup> -+ <!-- 6. Go to Checkout --> -+ <click selector="{{CheckoutCartSummarySection.proceedToCheckout}}" stepKey="clickProceedToCheckout"/> -+ <actionGroup ref="StorefrontAssertCheckoutEstimateShippingInformationActionGroup" stepKey="assertCheckoutEstimateShippingInformationAfterGoingToCheckout"/> -+ <actionGroup ref="StorefrontAssertCheckoutShippingMethodSelectedActionGroup" stepKey="assertFlatRateShippingMethodIsCheckedAfterGoingToCheckout"> -+ <argument name="shippingMethod" value="Flat Rate"/> -+ </actionGroup> -+ <!-- 7. Change persisted data --> -+ <selectOption selector="{{CheckoutShippingGuestInfoSection.country}}" userInput="United Kingdom" stepKey="changeCountryField"/> -+ <fillField selector="{{CheckoutShippingGuestInfoSection.regionInput}}" userInput="" stepKey="changeStateProvinceField"/> -+ <fillField selector="{{CheckoutShippingGuestInfoSection.postcode}}" userInput="KW1 7NQ" stepKey="changeZipPostalCodeField"/> -+ <!-- 8. Change shipping rate, select Free Shipping --> -+ <checkOption selector="{{CheckoutShippingMethodsSection.checkShippingMethodByName('Free Shipping')}}" stepKey="checkFreeShippingAsShippingMethod"/> -+ <!-- 9. Fill other fields --> -+ <actionGroup ref="StorefrontFillGuestShippingInfoActionGroup" stepKey="fillOtherFieldsInCheckoutShippingSection"/> -+ <!-- 10. Refresh browser page(F5) --> -+ <reloadPage stepKey="reloadCheckoutPage"/> -+ <waitForPageLoad stepKey="waitForCheckoutPageLoad"/> -+ <actionGroup ref="StorefrontAssertGuestShippingInfoActionGroup" stepKey="assertGuestShippingPersistedInfoAfterReloadingCheckoutShippingPage"/> -+ <actionGroup ref="StorefrontAssertCheckoutShippingMethodSelectedActionGroup" stepKey="assertFreeShippingShippingMethodIsChecked"> -+ <argument name="shippingMethod" value="Free Shipping"/> -+ </actionGroup> -+ <!-- 11. Go back to the shopping cart --> -+ <amOnPage url="{{CheckoutCartPage.url}}" stepKey="goToCheckoutCartIndexPage1"/> -+ <actionGroup ref="StorefrontAssertCartEstimateShippingAndTaxActionGroup" stepKey="assertCartEstimateShippingAndTaxAfterGoingBackToShoppingCart"> -+ <argument name="customerData" value="Simple_UK_Customer_For_Shipment"/> -+ </actionGroup> -+ <actionGroup ref="StorefrontAssertCartShippingMethodSelectedActionGroup" stepKey="assertFreeShippingShippingMethodIsCheckedAfterGoingBackToShoppingCart"> -+ <argument name="carrierCode" value="freeshipping"/> -+ <argument name="methodCode" value="freeshipping"/> -+ </actionGroup> -+ </test> -+</tests> -diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontProductNameMinicartOnCheckoutPageDifferentStoreViewsTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontProductNameMinicartOnCheckoutPageDifferentStoreViewsTest.xml -new file mode 100644 -index 00000000000..5e7e76ae4f0 ---- /dev/null -+++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontProductNameMinicartOnCheckoutPageDifferentStoreViewsTest.xml -@@ -0,0 +1,90 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> -+ <test name="StorefrontProductNameMinicartOnCheckoutPageDifferentStoreViewsTest"> -+ <annotations> -+ <features value="Checkout"/> -+ <stories value="Minicart"/> -+ <title value="Checking Product name in Minicart and on Checkout page with different store views"/> -+ <description value="Checking Product name in Minicart and on Checkout page with different store views"/> -+ <severity value="MAJOR"/> -+ <testCaseId value="MAGETWO-96466"/> -+ <useCaseId value="MAGETWO-96421"/> -+ <group value="checkout"/> -+ </annotations> -+ <before> -+ <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> -+ -+ <!--Create a product--> -+ <createData entity="SimpleProduct2" stepKey="createProduct"/> -+ </before> -+ <after> -+ <!--Delete created data--> -+ <deleteData createDataKey="createProduct" stepKey="deleteProduct"/> -+ <actionGroup ref="AdminDeleteStoreViewActionGroup" stepKey="deleteStoreView"> -+ <argument name="customStore" value="customStore"/> -+ </actionGroup> -+ -+ <actionGroup ref="logout" stepKey="logout"/> -+ </after> -+ -+ <!-- Create store view --> -+ <actionGroup ref="AdminCreateStoreViewActionGroup" stepKey="createStoreView"> -+ <argument name="customStore" value="customStore"/> -+ </actionGroup> -+ -+ <!--Go to created product page--> -+ <amOnPage url="{{AdminProductEditPage.url($$createProduct.id$$)}}" stepKey="goToEditPage"/> -+ <waitForPageLoad stepKey="waitForProductPage"/> -+ -+ <!--Switch to second store view and change the product name--> -+ <actionGroup ref="SwitchToTheNewStoreView" stepKey="switchToCustomStoreView"> -+ <argument name="storeViewName" value="{{customStore.name}}"/> -+ </actionGroup> -+ <waitForPageLoad stepKey="waitForPageLoad"/> -+ <click selector="{{AdminProductFormSection.productNameUseDefault}}" stepKey="uncheckUseDefault"/> -+ <fillField selector="{{AdminProductFormSection.productName}}" userInput="$$createProduct.name$$-new" stepKey="fillProductName"/> -+ <click selector="{{AdminProductFormActionSection.saveButton}}" stepKey="saveProduct"/> -+ -+ <!--Add product to cart--> -+ <amOnPage url="{{StorefrontProductPage.url($$createProduct.name$$)}}" stepKey="amOnSimpleProductPage"/> -+ <waitForPageLoad stepKey="waitForProductPageLoad"/> -+ <actionGroup ref="addToCartFromStorefrontProductPage" stepKey="addToCartFromStorefrontProductPage"> -+ <argument name="productName" value="$createProduct.name$$"/> -+ </actionGroup> -+ -+ <!--Switch to second store view--> -+ <actionGroup ref="StorefrontSwitchStoreViewActionGroup" stepKey="switchStoreView"> -+ <argument name="storeView" value="customStore"/> -+ </actionGroup> -+ <waitForPageLoad stepKey="waitForStoreView"/> -+ -+ <!--Check product name in Minicart--> -+ <click selector="{{StorefrontMinicartSection.showCart}}" stepKey="clickCart"/> -+ <grabTextFrom selector="{{StorefrontMinicartSection.productName}}" stepKey="grabProductNameMinicart"/> -+ <assertContains expected="$$createProduct.name$$" actual="$grabProductNameMinicart" stepKey="assertProductNameMinicart"/> -+ <assertContains expectedType="string" expected="-new" actual="$grabProductNameMinicart" stepKey="assertProductNameMinicart1"/> -+ -+ <!--Check product name in Shopping Cart page--> -+ <click selector="{{StorefrontMinicartSection.viewAndEditCart}}" stepKey="clickViewAndEdit"/> -+ <waitForPageLoad stepKey="waitForShoppingCartPageLoad"/> -+ <grabTextFrom selector="{{CheckoutCartProductSection.productName}}" stepKey="grabProductNameCart"/> -+ <assertContains expected="$$createProduct.name$$" actual="$grabProductNameCart" stepKey="assertProductNameCart"/> -+ <assertContains expectedType="string" expected="-new" actual="$grabProductNameCart" stepKey="assertProductNameCart1"/> -+ -+ <!--Proceed to checkout and check product name in Order Summary area--> -+ <click selector="{{CheckoutCartSummarySection.proceedToCheckout}}" stepKey="proceedToCheckout"/> -+ <waitForPageLoad stepKey="waitForShippingPageLoad"/> -+ <click selector="{{CheckoutShippingGuestInfoSection.itemInCart}}" stepKey="clickItemInCart"/> -+ <grabTextFrom selector="{{CheckoutShippingGuestInfoSection.productName}}" stepKey="grabProductNameShipping"/> -+ <assertContains expected="$$createProduct.name$$" actual="$grabProductNameShipping" stepKey="assertProductNameShipping"/> -+ <assertContains expectedType="string" expected="-new" actual="$grabProductNameShipping" stepKey="assertProductNameShipping1"/> -+ </test> -+</tests> -diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontRefreshPageDuringGuestCheckoutTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontRefreshPageDuringGuestCheckoutTest.xml -new file mode 100644 -index 00000000000..1db460de449 ---- /dev/null -+++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontRefreshPageDuringGuestCheckoutTest.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="StorefrontRefreshPageDuringGuestCheckoutTest"> -+ <annotations> -+ <features value="Checkout"/> -+ <stories value="Guest checkout"/> -+ <title value="Storefront refresh page during guest checkout test"/> -+ <description value="Address is not lost for guest checkout if guest refresh page during checkout"/> -+ <severity value="CRITICAL"/> -+ <testCaseId value="MC-12084"/> -+ <group value="checkout"/> -+ </annotations> -+ <before> -+ <!-- Login as admin --> -+ <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> -+ -+ <!-- Create simple product --> -+ <createData entity="SimpleProduct2" stepKey="createProduct"/> -+ </before> -+ <after> -+ <!-- Delete simple product --> -+ <deleteData createDataKey="createProduct" stepKey="deleteProduct"/> -+ -+ <!-- Logout admin --> -+ <actionGroup ref="logout" stepKey="logoutAsAdmin"/> -+ </after> -+ <!-- Add simple product to cart as Guest --> -+ <amOnPage url="{{StorefrontProductPage.url($$createProduct.custom_attributes[url_key]$$)}}" stepKey="goToProductPage"/> -+ <waitForPageLoad stepKey="waitForPageLoad"/> -+ <actionGroup ref="StorefrontAddProductToCartActionGroup" stepKey="cartAddSimpleProductToCart"> -+ <argument name="product" value="$$createProduct$$"/> -+ <argument name="productCount" value="1"/> -+ </actionGroup> -+ -+ <!-- Go to Checkout page --> -+ <actionGroup ref="clickViewAndEditCartFromMiniCart" stepKey="goToShoppingCartFromMinicart"/> -+ <click selector="{{CheckoutCartSummarySection.proceedToCheckout}}" stepKey="clickProceedToCheckout"/> -+ <waitForPageLoad stepKey="waitForProceedToCheckout"/> -+ -+ <!-- Fill email field and addresses form and go next --> -+ <actionGroup ref="GuestCheckoutFillingShippingSectionActionGroup" stepKey="guestCheckoutFillingShipping"> -+ <argument name="shippingMethod" value="Flat Rate"/> -+ </actionGroup> -+ -+ <!-- Refresh page --> -+ <reloadPage stepKey="refreshPage"/> -+ -+ <!-- Click Place Order and assert order is placed --> -+ <actionGroup ref="ClickPlaceOrderActionGroup" stepKey="clickOnPlaceOrder"/> -+ <grabTextFrom selector="{{CheckoutSuccessMainSection.orderNumber}}" stepKey="grabOrderNumber"/> -+ -+ <!-- Order review page has address that was created during checkout --> -+ <amOnPage url="{{AdminOrderPage.url({$grabOrderNumber})}}" stepKey="navigateToOrderPage"/> -+ <waitForPageLoad stepKey="waitForCreatedOrderPage"/> -+ <see selector="{{AdminShipmentAddressInformationSection.shippingAddress}}" userInput="{{CustomerAddressSimple.street[0]}} {{CustomerAddressSimple.city}}, {{CustomerAddressSimple.state}}, {{CustomerAddressSimple.postcode}}" stepKey="checkShippingAddress"/> -+ </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 00000000000..b0e1dead1ff ---- /dev/null -+++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontShoppingCartCheckCustomerDefaultShippingAddressForVirtualQuoteTest.xml -@@ -0,0 +1,54 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> -+ <test name="StorefrontShoppingCartCheckCustomerDefaultShippingAddressForVirtualQuoteTest"> -+ <annotations> -+ <features value="Checkout"/> -+ <stories value="Estimator in Shopping cart must be pre-filled by Customer default shipping address for virtual quote"/> -+ <title value="Estimator in Shopping cart must be pre-filled by Customer default shipping address for virtual quote"/> -+ <description value="Estimator in Shopping cart must be pre-filled by Customer default shipping address for virtual quote"/> -+ <severity value="CRITICAL"/> -+ <testCaseId value="MAGETWO-46795"/> -+ <group value="checkout"/> -+ </annotations> -+ <before> -+ <createData entity="VirtualProduct" stepKey="createVirtualProduct"/> -+ <createData entity="Customer_With_Different_Default_Billing_Shipping_Addresses" stepKey="createCustomer"/> -+ </before> -+ <after> -+ <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="customerLogout"/> -+ <deleteData createDataKey="createVirtualProduct" stepKey="deleteVirtualProduct"/> -+ <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> -+ </after> -+ <!-- Steps --> -+ <!-- Step 1: Go to Storefront as Customer --> -+ <actionGroup ref="LoginToStorefrontActionGroup" stepKey="customerLogin"> -+ <argument name="Customer" value="$$createCustomer$$" /> -+ </actionGroup> -+ <!-- Step 2: Add virtual product to cart --> -+ <amOnPage url="{{StorefrontProductPage.url($$createVirtualProduct.custom_attributes[url_key]$)}}" stepKey="amOnPage"/> -+ <actionGroup ref="StorefrontAddProductToCartActionGroup" stepKey="addProductToCart"> -+ <argument name="product" value="$$createVirtualProduct$$"/> -+ <argument name="productCount" value="1"/> -+ </actionGroup> -+ <!-- Step 3: Go to Shopping Cart --> -+ <actionGroup ref="clickViewAndEditCartFromMiniCart" stepKey="goToShoppingcart"/> -+ <!-- Step 4: Open Estimate Tax section --> -+ <click selector="{{CheckoutCartSummarySection.estimateShippingAndTax}}" stepKey="openEstimateTaxSection"/> -+ <seeOptionIsSelected selector="{{CheckoutCartSummarySection.country}}" userInput="{{US_Address_CA.country}}" stepKey="checkCountry"/> -+ <seeOptionIsSelected selector="{{CheckoutCartSummarySection.stateProvince}}" userInput="{{US_Address_CA.state}}" stepKey="checkState"/> -+ <scrollTo selector="{{CheckoutCartSummarySection.postcode}}" stepKey="scrollToPostCodeField"/> -+ <grabValueFrom selector="{{CheckoutCartSummarySection.postcode}}" stepKey="grabTextPostCode"/> -+ <assertEquals message="Customer postcode is invalid" stepKey="checkCustomerPostcode"> -+ <expectedResult type="string">{{US_Address_CA.postcode}}</expectedResult> -+ <actualResult type="variable">grabTextPostCode</actualResult> -+ </assertEquals> -+ </test> -+</tests> -diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontUpdatePriceInShoppingCartAfterProductSaveTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontUpdatePriceInShoppingCartAfterProductSaveTest.xml -new file mode 100644 -index 00000000000..d27cb83a8ca ---- /dev/null -+++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontUpdatePriceInShoppingCartAfterProductSaveTest.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="StorefrontUpdatePriceInShoppingCartAfterProductSaveTest"> -+ <annotations> -+ <features value="Checkout"/> -+ <stories value="Checkout via the Storefront"/> -+ <title value="Update price in shopping cart after product save"/> -+ <description value="Price in shopping cart should be updated after product save with changed price"/> -+ <severity value="CRITICAL"/> -+ <testCaseId value="MAGETWO-58179"/> -+ <group value="checkout"/> -+ <skip> -+ <issueId value="MC-16684"/> -+ </skip> -+ </annotations> -+ <before> -+ <createData entity="SimpleProduct2" stepKey="createSimpleProduct"> -+ <field key="price">100</field> -+ </createData> -+ <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> -+ <actionGroup ref="SetCustomerDataLifetimeActionGroup" stepKey="setCustomerDataLifetime"> -+ <argument name="minutes" value="1"/> -+ </actionGroup> -+ </before> -+ <after> -+ <deleteData createDataKey="createSimpleProduct" stepKey="deleteProduct"/> -+ <actionGroup ref="SetCustomerDataLifetimeActionGroup" stepKey="setDefaultCustomerDataLifetime"/> -+ <magentoCLI command="indexer:reindex customer_grid" stepKey="reindexCustomerGrid"/> -+ <actionGroup ref="logout" stepKey="logout"/> -+ </after> -+ <!--Go to product page--> -+ <amOnPage url="{{StorefrontProductPage.url($$createSimpleProduct.custom_attributes[url_key]$$)}}" stepKey="navigateToSimpleProductPage"/> -+ -+ <!--Add Product to Shopping Cart--> -+ <actionGroup ref="addToCartFromStorefrontProductPage" stepKey="addToCartFromStorefrontProductPage"> -+ <argument name="productName" value="$$createSimpleProduct.name$$"/> -+ </actionGroup> -+ -+ <!--Go to Checkout--> -+ <actionGroup ref="GoToCheckoutFromMinicartActionGroup" stepKey="goToCheckoutFromMinicart"/> -+ <actionGroup ref="GuestCheckoutFillingShippingSectionActionGroup" stepKey="guestCheckoutFillingShipping"/> -+ -+ <!--Check price--> -+ <conditionalClick selector="{{CheckoutPaymentSection.cartItemsArea}}" dependentSelector="{{CheckoutPaymentSection.cartItemsAreaActive}}" visible="false" stepKey="openItemProductBlock"/> -+ <see userInput="$100.00" selector="{{CheckoutPaymentSection.orderSummarySubtotal}}" stepKey="checkSummarySubtotal"/> -+ <see userInput="$100.00" selector="{{CheckoutPaymentSection.productItemPriceByName($$createSimpleProduct.name$$)}}" stepKey="checkItemPrice"/> -+ -+ <!--Edit product price via admin panel--> -+ <openNewTab stepKey="openNewTab"/> -+ <amOnPage url="{{AdminProductEditPage.url($$createSimpleProduct.id$$)}}" stepKey="goToProductEditPage"/> -+ <fillField userInput="120" selector="{{AdminProductFormSection.productPrice}}" stepKey="setNewPrice"/> -+ <actionGroup ref="saveProductForm" stepKey="saveProduct"/> -+ <closeTab stepKey="closeTab"/> -+ -+ <!--Check price--> -+ <reloadPage stepKey="reloadPage"/> -+ <waitForPageLoad stepKey="waitForCheckoutPageReload"/> -+ <conditionalClick selector="{{CheckoutPaymentSection.cartItemsArea}}" dependentSelector="{{CheckoutPaymentSection.cartItemsAreaActive}}" visible="false" stepKey="openItemProductBlock1"/> -+ <see userInput="$120.00" selector="{{CheckoutPaymentSection.orderSummarySubtotal}}" stepKey="checkSummarySubtotal1"/> -+ <see userInput="$120.00" selector="{{CheckoutPaymentSection.productItemPriceByName($$createSimpleProduct.name$$)}}" stepKey="checkItemPrice1"/> -+ </test> -+</tests> -diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontUpdateShoppingCartSimpleProductQtyTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontUpdateShoppingCartSimpleProductQtyTest.xml -new file mode 100644 -index 00000000000..72f6cf95a6f ---- /dev/null -+++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontUpdateShoppingCartSimpleProductQtyTest.xml -@@ -0,0 +1,67 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> -+ <test name="StorefrontUpdateShoppingCartSimpleProductQtyTest"> -+ <annotations> -+ <features value="Checkout"/> -+ <stories value="Shopping cart"/> -+ <title value="Check updating shopping cart while updating items qty"/> -+ <description value="Check updating shopping cart while updating items qty"/> -+ <testCaseId value="MC-14731" /> -+ <severity value="AVERAGE"/> -+ <group value="shoppingCart"/> -+ <group value="mtf_migrated"/> -+ </annotations> -+ <before> -+ <createData entity="_defaultCategory" stepKey="createCategory"/> -+ <createData entity="SimpleProduct" stepKey="createProduct"> -+ <requiredEntity createDataKey="createCategory"/> -+ </createData> -+ -+ <!-- Add the newly created product to the shopping cart --> -+ <actionGroup ref="AddSimpleProductToCart" stepKey="addToCartFromStorefrontProductPage"> -+ <argument name="product" value="$$createProduct$$"/> -+ </actionGroup> -+ </before> -+ <after> -+ <deleteData createDataKey="createProduct" stepKey="deleteProduct"/> -+ <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> -+ </after> -+ -+ <!-- Go to the shopping cart --> -+ <amOnPage url="{{CheckoutCartPage.url}}" stepKey="amOnPageShoppingCart"/> -+ <waitForPageLoad stepKey="waitForCheckoutPageLoad1"/> -+ -+ <!-- Change the product QTY --> -+ <fillField selector="{{CheckoutCartProductSection.ProductQuantityByName($$createProduct.name$$)}}" userInput="{{quoteQty3Price123.qty}}" stepKey="changeCartQty"/> -+ <click selector="{{CheckoutCartProductSection.updateShoppingCartButton}}" stepKey="openShoppingCart"/> -+ <waitForPageLoad stepKey="waitForCheckoutPageLoad2"/> -+ -+ <!-- The price and QTY values should be updated for the product --> -+ <grabValueFrom selector="{{CheckoutCartProductSection.ProductQuantityByName($$createProduct.name$$)}}" stepKey="grabProductQtyInCart"/> -+ <see userInput="{{quoteQty3Price123.currency}}{{quoteQty3Price123.subtotal}}" selector="{{CheckoutCartProductSection.productSubtotalByName($$createProduct.name$$)}}" stepKey="assertProductPrice"/> -+ <assertEquals stepKey="assertProductQtyInCart"> -+ <actualResult type="variable">grabProductQtyInCart</actualResult> -+ <expectedResult type="string">{{quoteQty3Price123.qty}}</expectedResult> -+ </assertEquals> -+ -+ <!-- Subtotal should be updated --> -+ <see userInput="{{quoteQty3Price123.currency}}{{quoteQty3Price123.subtotal}}" selector="{{CheckoutCartSummarySection.subtotal}}" stepKey="assertCartSubtotal"/> -+ -+ <!-- Minicart product price and subtotal should be updated --> -+ <actionGroup ref="clickViewAndEditCartFromMiniCart" stepKey="openMinicart"/> -+ <grabValueFrom selector="{{StorefrontMinicartSection.itemQuantity($$createProduct.name$$)}}" stepKey="grabProductQtyInMinicart"/> -+ <assertEquals stepKey="assertProductQtyInMinicart"> -+ <actualResult type="variable">grabProductQtyInMinicart</actualResult> -+ <expectedResult type="string">{{quoteQty3Price123.qty}}</expectedResult> -+ </assertEquals> -+ <see userInput="{{quoteQty3Price123.currency}}{{quoteQty3Price123.subtotal}}" selector="{{StorefrontMinicartSection.subtotal}}" stepKey="assertMinicartSubtotal"/> -+ </test> -+</tests> -diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontUpdateShoppingCartSimpleWithCustomOptionsProductQtyTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontUpdateShoppingCartSimpleWithCustomOptionsProductQtyTest.xml -new file mode 100644 -index 00000000000..7a653f13c4e ---- /dev/null -+++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontUpdateShoppingCartSimpleWithCustomOptionsProductQtyTest.xml -@@ -0,0 +1,72 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> -+ <test name="StorefrontUpdateShoppingCartSimpleWithCustomOptionsProductQtyTest"> -+ <annotations> -+ <features value="Checkout"/> -+ <stories value="Shopping cart"/> -+ <title value="Check updating shopping cart while updating qty of items with custom options"/> -+ <description value="Check updating shopping cart while updating qty of items with custom options"/> -+ <testCaseId value="MC-14732" /> -+ <severity value="AVERAGE"/> -+ <group value="shoppingCart"/> -+ <group value="mtf_migrated"/> -+ </annotations> -+ <before> -+ <createData entity="_defaultCategory" stepKey="createCategory"/> -+ <createData entity="ApiSimpleProductWithCustomPrice" stepKey="createProduct"> -+ <requiredEntity createDataKey="createCategory"/> -+ </createData> -+ -+ <!-- Add two custom options to the product: field and textarea --> -+ <updateData createDataKey="createProduct" entity="ProductWithTextFieldAndAreaOptions" stepKey="updateProductWithOption"/> -+ -+ <!-- Go to the product page, fill the custom options values and add the product to the shopping cart --> -+ <amOnPage url="{{StorefrontProductPage.url($$createProduct.custom_attributes[url_key]$$)}}" stepKey="amOnProductPage"/> -+ <waitForPageLoad stepKey="waitForCatalogPageLoad"/> -+ <fillField userInput="OptionField" selector="{{StorefrontProductInfoMainSection.productOptionFieldInput(ProductOptionField.title)}}" stepKey="fillProductOptionInputField"/> -+ <fillField userInput="OptionArea" selector="{{StorefrontProductInfoMainSection.productOptionAreaInput(ProductOptionArea.title)}}" stepKey="fillProductOptionInputArea"/> -+ <actionGroup ref="StorefrontAddToCartCustomOptionsProductPageActionGroup" stepKey="addToCartFromStorefrontProductPage"> -+ <argument name="productName" value="$createProduct.name$"/> -+ </actionGroup> -+ </before> -+ <after> -+ <deleteData createDataKey="createProduct" stepKey="deleteProduct"/> -+ <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> -+ </after> -+ -+ <!-- Go to the shopping cart --> -+ <amOnPage url="{{CheckoutCartPage.url}}" stepKey="amOnPageShoppingCart"/> -+ <waitForPageLoad stepKey="waitForCheckoutPageLoad"/> -+ -+ <!-- Change the product QTY --> -+ <fillField selector="{{CheckoutCartProductSection.ProductQuantityByName($$createProduct.name$$)}}" userInput="{{quoteQty11Subtotal1320.qty}}" stepKey="changeCartQty"/> -+ <click selector="{{CheckoutCartProductSection.updateShoppingCartButton}}" stepKey="updateShoppingCart"/> -+ <waitForPageLoad stepKey="waitShoppingCartUpdated"/> -+ -+ <!-- The price and QTY values should be updated for the product --> -+ <grabValueFrom selector="{{CheckoutCartProductSection.ProductQuantityByName($$createProduct.name$$)}}" stepKey="grabProductQtyInCart"/> -+ <see userInput="{{quoteQty11Subtotal1320.currency}}{{quoteQty11Subtotal1320.subtotal}}" selector="{{CheckoutCartProductSection.productSubtotalByName($$createProduct.name$$)}}" stepKey="assertProductPrice"/> -+ <assertEquals stepKey="assertProductQtyInCart"> -+ <expectedResult type="string">{{quoteQty11Subtotal1320.qty}}</expectedResult> -+ <actualResult type="variable">grabProductQtyInCart</actualResult> -+ </assertEquals> -+ <see userInput="{{quoteQty11Subtotal1320.currency}}{{quoteQty11Subtotal1320.subtotal}}" selector="{{CheckoutCartSummarySection.subtotal}}" stepKey="assertSubtotal"/> -+ -+ <!-- Minicart product price and subtotal should be updated --> -+ <actionGroup ref="clickViewAndEditCartFromMiniCart" stepKey="openMinicart"/> -+ <grabValueFrom selector="{{StorefrontMinicartSection.itemQuantity($$createProduct.name$$)}}" stepKey="grabProductQtyInMinicart"/> -+ <assertEquals stepKey="assertProductQtyInMinicart"> -+ <expectedResult type="string">{{quoteQty11Subtotal1320.qty}}</expectedResult> -+ <actualResult type="variable">grabProductQtyInMinicart</actualResult> -+ </assertEquals> -+ <see userInput="{{quoteQty11Subtotal1320.currency}}{{quoteQty11Subtotal1320.subtotal}}" selector="{{StorefrontMinicartSection.subtotal}}" stepKey="assertMinicartSubtotal"/> -+ </test> -+</tests> -diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontValidateEmailOnCheckoutTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontValidateEmailOnCheckoutTest.xml -new file mode 100644 -index 00000000000..65f5dd365b2 ---- /dev/null -+++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontValidateEmailOnCheckoutTest.xml -@@ -0,0 +1,56 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> -+ <test name="StorefrontValidateEmailOnCheckoutTest"> -+ <annotations> -+ <features value="Checkout"/> -+ <stories value="Guest Checkout e-mail validation"/> -+ <title value="Guest e-mail address validation on Checkout process"/> -+ <description value="Guest should not be able to place an order when invalid e-mail address provided"/> -+ <stories value="Guest Checkout"/> -+ <testCaseId value="MC-14695"/> -+ <severity value="CRITICAL"/> -+ <group value="checkout"/> -+ <group value="shoppingCart"/> -+ <group value="mtf_migrated"/> -+ </annotations> -+ <before> -+ <createData entity="SimpleTwo" stepKey="simpleProduct"/> -+ </before> -+ -+ <after> -+ <deleteData createDataKey="simpleProduct" stepKey="deleteProduct"/> -+ </after> -+ -+ <actionGroup ref="StorefrontOpenProductPageActionGroup" stepKey="openProductStorefront"> -+ <argument name="productUrl" value="$$simpleProduct.custom_attributes[url_key]$$"/> -+ </actionGroup> -+ <actionGroup ref="StorefrontClickAddToCartOnProductPageActionGroup" stepKey="addToCartFromStorefrontProductPage"/> -+ -+ <actionGroup ref="StorefrontOpenCheckoutPageActionGroup" stepKey="openCheckoutPage"/> -+ <actionGroup ref="AssertStorefrontEmailTooltipContentOnCheckoutActionGroup" stepKey="assertEmailTooltipContent"/> -+ <actionGroup ref="AssertStorefrontEmailNoteMessageOnCheckoutActionGroup" stepKey="assertEmailNoteMessage"/> -+ -+ <actionGroup ref="StorefrontFillEmailFieldOnCheckoutActionGroup" stepKey="fillIncorrectEmailFirstAttempt"> -+ <argument name="email" value="John"/> -+ </actionGroup> -+ <actionGroup ref="AssertStorefrontEmailValidationMessageOnCheckoutActionGroup" stepKey="verifyValidationErrorMessageFirstAttempt"/> -+ -+ <actionGroup ref="StorefrontFillEmailFieldOnCheckoutActionGroup" stepKey="fillIncorrectEmailSecondAttempt"> -+ <argument name="email" value="johndoe#example.com"/> -+ </actionGroup> -+ <actionGroup ref="AssertStorefrontEmailValidationMessageOnCheckoutActionGroup" stepKey="verifyValidationErrorMessageSecondAttempt"/> -+ -+ <actionGroup ref="StorefrontFillEmailFieldOnCheckoutActionGroup" stepKey="fillIncorrectEmailThirdAttempt"> -+ <argument name="email" value="johndoe@example.c"/> -+ </actionGroup> -+ <actionGroup ref="AssertStorefrontEmailValidationMessageOnCheckoutActionGroup" stepKey="verifyValidationErrorMessageThirdAttempt"/> -+ </test> -+</tests> -diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/UpdateProductFromMiniShoppingCartEntityTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/UpdateProductFromMiniShoppingCartEntityTest.xml -new file mode 100644 -index 00000000000..7318f865a0d ---- /dev/null -+++ b/app/code/Magento/Checkout/Test/Mftf/Test/UpdateProductFromMiniShoppingCartEntityTest.xml -@@ -0,0 +1,47 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> -+ <test name="UpdateProductFromMiniShoppingCartEntityTest"> -+ <annotations> -+ <stories value="Shopping Cart"/> -+ <title value="Check updating product from mini shopping cart"/> -+ <description value="Update Product Qty on Mini Shopping Cart"/> -+ <severity value="MAJOR"/> -+ <testCaseId value="MC-15068"/> -+ <group value="shoppingCart"/> -+ <group value="mtf_migrated"/> -+ </annotations> -+ -+ <before> -+ <!--Create product according to dataset.--> -+ <createData entity="simpleProductWithoutCategory" stepKey="createProduct"/> -+ -+ <!--Add product to cart--> -+ <actionGroup ref="AddSimpleProductToCart" stepKey="addToCartFromStorefrontProductPage"> -+ <argument name="product" value="$$createProduct$$"/> -+ </actionGroup> -+ </before> -+ -+ <after> -+ <!--Delete created data--> -+ <deleteData createDataKey="createProduct" stepKey="deleteProduct" /> -+ </after> -+ -+ <actionGroup ref="StorefrontUpdateProductQtyMiniShoppingCartActionGroup" stepKey="updateProductQty"> -+ <argument name="product" value="$$createProduct$$" /> -+ <argument name="quote" value="simpleOrderQty2" /> -+ </actionGroup> -+ -+ <!-- Perform all assertions --> -+ <actionGroup ref="AssertMiniShoppingCartSubTotalActionGroup" stepKey="checkSummary"> -+ <argument name="dataQuote" value="simpleOrderQty2"/> -+ </actionGroup> -+ </test> -+</tests> -diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/ZeroSubtotalOrdersWithProcessingStatusTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/ZeroSubtotalOrdersWithProcessingStatusTest.xml -new file mode 100644 -index 00000000000..4b3e18fb318 ---- /dev/null -+++ b/app/code/Magento/Checkout/Test/Mftf/Test/ZeroSubtotalOrdersWithProcessingStatusTest.xml -@@ -0,0 +1,103 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> -+ <test name="ZeroSubtotalOrdersWithProcessingStatusTest"> -+ <annotations> -+ <features value="Checkout"/> -+ <stories value="MAGETWO-71375: Zero Subtotal Orders have incorrect status"/> -+ <title value="Checking status of Zero Subtotal Orders with 'Processing' New Order Status"/> -+ <description value="Created order should be in Processing status"/> -+ <severity value="MAJOR"/> -+ <testCaseId value="MAGETWO-94178"/> -+ <group value="checkout"/> -+ </annotations> -+ <before> -+ <createData entity="SimpleSubCategory" stepKey="simplecategory"/> -+ <createData entity="SimpleProduct" stepKey="simpleproduct"> -+ <requiredEntity createDataKey="simplecategory"/> -+ </createData> -+ <createData entity="PaymentMethodsSettingConfig" stepKey="paymentMethodsSettingConfig"/> -+ <createData entity="FreeShippingMethodsSettingConfig" stepKey="freeShippingMethodsSettingConfig"/> -+ <!--Go to Admin page--> -+ <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> -+ </before> -+ -+ <after> -+ <actionGroup ref="DeleteCartPriceRuleByName" stepKey="deleteSalesRule"> -+ <argument name="ruleName" value="{{ApiSalesRule.name}}"/> -+ </actionGroup> -+ <createData entity="DefaultShippingMethodsConfig" stepKey="defaultShippingMethodsConfig"/> -+ <createData entity="DisableFreeShippingConfig" stepKey="disableFreeShippingConfig"/> -+ <createData entity="DisablePaymentMethodsSettingConfig" stepKey="disablePaymentMethodsSettingConfig"/> -+ <actionGroup ref="logout" stepKey="logout"/> -+ <deleteData createDataKey="simpleproduct" stepKey="deleteProduct"/> -+ <deleteData createDataKey="simplecategory" stepKey="deleteCategory"/> -+ </after> -+ -+ <!--Open MARKETING > Cart Price Rules--> -+ <amOnPage url="{{AdminCartPriceRulesPage.url}}" stepKey="amOnCartPriceList"/> -+ <waitForPageLoad stepKey="waitForRulesPage"/> -+ -+ <!--Add New Rule--> -+ <click selector="{{AdminCartPriceRulesSection.addNewRuleButton}}" stepKey="clickAddNewRule"/> -+ <fillField selector="{{AdminCartPriceRulesFormSection.ruleName}}" userInput="{{ApiSalesRule.name}}" stepKey="fillRuleName"/> -+ <selectOption selector="{{AdminCartPriceRulesFormSection.websites}}" userInput="Main Website" stepKey="selectWebsite"/> -+ <actionGroup ref="selectNotLoggedInCustomerGroup" stepKey="chooseNotLoggedInCustomerGroup"/> -+ <generateDate date="-1 day" format="m/d/Y" stepKey="yesterdayDate"/> -+ <fillField selector="{{AdminCartPriceRulesFormSection.fromDate}}" userInput="{$yesterdayDate}" stepKey="fillFromDate"/> -+ <selectOption selector="{{AdminCartPriceRulesFormSection.coupon}}" userInput="Specific Coupon" stepKey="selectCouponType"/> -+ <fillField selector="{{AdminCartPriceRulesFormSection.couponCode}}" userInput="{{_defaultCoupon.code}}" stepKey="fillCouponCode"/> -+ <fillField selector="{{AdminCartPriceRulesFormSection.userPerCoupon}}" userInput="99" stepKey="fillUserPerCoupon"/> -+ <click selector="{{AdminCartPriceRulesFormSection.actionsHeader}}" stepKey="clickToExpandActions"/> -+ <selectOption selector="{{AdminCartPriceRulesFormSection.apply}}" userInput="Percent of product price discount" stepKey="selectActionType"/> -+ <fillField selector="{{AdminCartPriceRulesFormSection.discountAmount}}" userInput="100" stepKey="fillDiscountAmount"/> -+ <click selector="{{AdminCartPriceRulesFormSection.save}}" stepKey="clickSaveButton"/> -+ <see selector="{{AdminCartPriceRulesSection.messages}}" userInput="You saved the rule." stepKey="seeSuccessMessage"/> -+ -+ <!--Proceed to store front and place an order with free shipping using created coupon--> -+ <!--Add product to card--> -+ <actionGroup ref="AddSimpleProductToCart" stepKey="AddProductToCard"> -+ <argument name="product" value="$$simpleproduct$$"/> -+ </actionGroup> -+ -+ <!--Proceed to shipment--> -+ <click selector="{{StorefrontMinicartSection.showCart}}" stepKey="clickToOpenCard"/> -+ <click selector="{{StorefrontMinicartSection.goToCheckout}}" stepKey="clickToProceedToCheckout"/> -+ <waitForPageLoad stepKey="waitForTheFormIsOpened"/> -+ -+ <!--Fill shipping form--> -+ <actionGroup ref="ShipmentFormFreeShippingActionGroup" stepKey="shipmentFormFreeShippingActionGroup"/> -+ -+ <click selector="{{DiscountSection.DiscountTab}}" stepKey="clickToAddDiscount"/> -+ <fillField selector="{{DiscountSection.DiscountInput}}" userInput="{{_defaultCoupon.code}}" stepKey="TypeDiscountCode"/> -+ <click selector="{{DiscountSection.ApplyCodeBtn}}" stepKey="clickToApplyDiscount"/> -+ <waitForPageLoad stepKey="WaitForDiscountToBeAdded"/> -+ <see userInput="Your coupon was successfully applied." stepKey="verifyText"/> -+ -+ <waitForElement selector="{{CheckoutPaymentSection.placeOrder}}" time="30" stepKey="waitForPlaceOrderButton"/> -+ <click selector="{{CheckoutPaymentSection.placeOrder}}" stepKey="clickPlaceOrder"/> -+ <grabTextFrom selector="{{CheckoutSuccessMainSection.orderNumber}}" stepKey="grabOrderNumber"/> -+ -+ <!--Proceed to Admin panel > SALES > Orders. Created order should be in Processing status--> -+ <amOnPage url="/admin/sales/order/" stepKey="navigateToSalesOrderPage"/> -+ <waitForPageLoad stepKey="waitForSalesOrderPageLoaded"/> -+ -+ <!-- Open Order --> -+ <actionGroup ref="filterOrderGridById" stepKey="filterOrderGridById"> -+ <argument name="orderId" value="$grabOrderNumber"/> -+ </actionGroup> -+ <click selector="{{AdminOrdersGridSection.firstRow}}" stepKey="clickOrderRow"/> -+ <waitForPageLoad stepKey="waitForCreatedOrderPageOpened"/> -+ -+ <!--Verify that Created order is in Processing status--> -+ <see selector="{{AdminShipmentOrderInformationSection.orderStatus}}" userInput="Processing" stepKey="seeShipmentOrderStatus"/> -+ <actionGroup ref="AdminOrdersGridClearFiltersActionGroup" stepKey="clearOrderFilters"/> -+ </test> -+</tests> -diff --git a/app/code/Magento/Checkout/Test/Unit/Block/Cart/SidebarTest.php b/app/code/Magento/Checkout/Test/Unit/Block/Cart/SidebarTest.php -index 1c5224d007e..f69ced3b094 100644 ---- a/app/code/Magento/Checkout/Test/Unit/Block/Cart/SidebarTest.php -+++ b/app/code/Magento/Checkout/Test/Unit/Block/Cart/SidebarTest.php -@@ -144,7 +144,8 @@ class SidebarTest extends \PHPUnit\Framework\TestCase - 'baseUrl' => $baseUrl, - 'minicartMaxItemsVisible' => 3, - 'websiteId' => 100, -- 'maxItemsToDisplay' => 8 -+ 'maxItemsToDisplay' => 8, -+ 'storeId' => null - ]; - - $valueMap = [ -@@ -161,7 +162,7 @@ class SidebarTest extends \PHPUnit\Framework\TestCase - $this->urlBuilderMock->expects($this->exactly(4)) - ->method('getUrl') - ->willReturnMap($valueMap); -- $this->storeManagerMock->expects($this->exactly(2))->method('getStore')->willReturn($storeMock); -+ $this->storeManagerMock->expects($this->any())->method('getStore')->willReturn($storeMock); - $storeMock->expects($this->once())->method('getBaseUrl')->willReturn($baseUrl); - - $this->scopeConfigMock->expects($this->at(0)) -diff --git a/app/code/Magento/Checkout/Test/Unit/Block/Checkout/AttributeMergerTest.php b/app/code/Magento/Checkout/Test/Unit/Block/Checkout/AttributeMergerTest.php -new file mode 100644 -index 00000000000..23840da97bd ---- /dev/null -+++ b/app/code/Magento/Checkout/Test/Unit/Block/Checkout/AttributeMergerTest.php -@@ -0,0 +1,121 @@ -+<?php -+/** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+declare(strict_types=1); -+ -+namespace Magento\Checkout\Test\Unit\Block\Checkout; -+ -+use Magento\Customer\Api\CustomerRepositoryInterface as CustomerRepository; -+use Magento\Customer\Helper\Address as AddressHelper; -+use Magento\Customer\Model\Session as CustomerSession; -+use Magento\Directory\Helper\Data as DirectoryHelper; -+use Magento\Checkout\Block\Checkout\AttributeMerger; -+use PHPUnit\Framework\TestCase; -+ -+class AttributeMergerTest extends TestCase -+{ -+ /** -+ * @var CustomerRepository -+ */ -+ private $customerRepository; -+ -+ /** -+ * @var CustomerSession -+ */ -+ private $customerSession; -+ -+ /** -+ * @var AddressHelper -+ */ -+ private $addressHelper; -+ -+ /** -+ * @var DirectoryHelper -+ */ -+ private $directoryHelper; -+ -+ /** -+ * @var AttributeMerger -+ */ -+ private $attributeMerger; -+ -+ /** -+ * @inheritdoc -+ */ -+ protected function setUp() -+ { -+ -+ $this->customerRepository = $this->createMock(CustomerRepository::class); -+ $this->customerSession = $this->createMock(CustomerSession::class); -+ $this->addressHelper = $this->createMock(AddressHelper::class); -+ $this->directoryHelper = $this->createMock(DirectoryHelper::class); -+ -+ $this->attributeMerger = new AttributeMerger( -+ $this->addressHelper, -+ $this->customerSession, -+ $this->customerRepository, -+ $this->directoryHelper -+ ); -+ } -+ -+ /** -+ * Tests of element attributes merging. -+ * -+ * @param String $validationRule - validation rule. -+ * @param String $expectedValidation - expected mapped validation. -+ * @dataProvider validationRulesDataProvider -+ */ -+ public function testMerge(String $validationRule, String $expectedValidation): void -+ { -+ $elements = [ -+ 'field' => [ -+ 'visible' => true, -+ 'formElement' => 'input', -+ 'label' => __('City'), -+ 'value' => null, -+ 'sortOrder' => 1, -+ 'validation' => [ -+ 'input_validation' => $validationRule -+ ], -+ ] -+ ]; -+ -+ $actualResult = $this->attributeMerger->merge( -+ $elements, -+ 'provider', -+ 'dataScope', -+ ['field' => -+ [ -+ 'validation' => ['length' => true] -+ ] -+ ] -+ ); -+ -+ $expectedResult = [ -+ $expectedValidation => true, -+ 'length' => true -+ ]; -+ -+ self::assertEquals($expectedResult, $actualResult['field']['validation']); -+ } -+ -+ /** -+ * Provides possible validation types. -+ * -+ * @return array -+ */ -+ public function validationRulesDataProvider(): array -+ { -+ return [ -+ ['alpha', 'validate-alpha'], -+ ['numeric', 'validate-number'], -+ ['alphanumeric', 'validate-alphanum'], -+ ['alphanum-with-spaces', 'validate-alphanum-with-spaces'], -+ ['url', 'validate-url'], -+ ['email', 'email2'], -+ ['length', 'validate-length'] -+ ]; -+ } -+} -diff --git a/app/code/Magento/Checkout/Test/Unit/Block/Checkout/LayoutProcessorTest.php b/app/code/Magento/Checkout/Test/Unit/Block/Checkout/LayoutProcessorTest.php -index b3e55bb418d..31ca2a20330 100644 ---- a/app/code/Magento/Checkout/Test/Unit/Block/Checkout/LayoutProcessorTest.php -+++ b/app/code/Magento/Checkout/Test/Unit/Block/Checkout/LayoutProcessorTest.php -@@ -10,15 +10,12 @@ use Magento\Checkout\Block\Checkout\LayoutProcessor; - use Magento\Checkout\Helper\Data; - use Magento\Customer\Model\AttributeMetadataDataProvider; - use Magento\Customer\Model\Options; --use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; -+ - use Magento\Ui\Component\Form\AttributeMapper; - use PHPUnit_Framework_MockObject_MockObject as MockObject; - - /** -- * LayoutProcessorTest covers a list of variations for -- * checkout layout processor -- * -- * @SuppressWarnings(PHPMD.CouplingBetweenObjects) -+ * LayoutProcessorTest covers a list of variations for checkout layout processor - */ - class LayoutProcessorTest extends \PHPUnit\Framework\TestCase - { -@@ -50,12 +47,10 @@ class LayoutProcessorTest extends \PHPUnit\Framework\TestCase - /** - * @var MockObject - */ -- private $storeResolver; -+ private $storeManager; - - protected function setUp() - { -- $objectManager = new ObjectManager($this); -- - $this->attributeDataProvider = $this->getMockBuilder(AttributeMetadataDataProvider::class) - ->disableOriginalConstructor() - ->setMethods(['loadAttributesCollection']) -@@ -80,17 +75,21 @@ class LayoutProcessorTest extends \PHPUnit\Framework\TestCase - ->disableOriginalConstructor() - ->getMock(); - -+ $shippingConfig = $this->getMockBuilder(\Magento\Shipping\Model\Config::class) -+ ->disableOriginalConstructor() -+ ->getMock(); -+ -+ $this->storeManager = $this->createMock(\Magento\Store\Model\StoreManagerInterface::class); -+ - $this->layoutProcessor = new LayoutProcessor( - $this->attributeDataProvider, - $this->attributeMapper, -- $this->attributeMerger -+ $this->attributeMerger, -+ $options, -+ $this->dataHelper, -+ $shippingConfig, -+ $this->storeManager - ); -- -- $this->storeResolver = $this->createMock(\Magento\Store\Api\StoreResolverInterface::class); -- -- $objectManager->setBackwardCompatibleProperty($this->layoutProcessor, 'checkoutDataHelper', $this->dataHelper); -- $objectManager->setBackwardCompatibleProperty($this->layoutProcessor, 'options', $options); -- $objectManager->setBackwardCompatibleProperty($this->layoutProcessor, 'storeResolver', $this->storeResolver); - } - - /** -@@ -277,7 +276,7 @@ class LayoutProcessorTest extends \PHPUnit\Framework\TestCase - 'telephone' => [ - 'config' => [ - 'tooltip' => [ -- 'description' => __('For delivery questions.'), -+ 'description' => ('For delivery questions.'), - ], - ], - ], -diff --git a/app/code/Magento/Checkout/Test/Unit/Block/OnepageTest.php b/app/code/Magento/Checkout/Test/Unit/Block/OnepageTest.php -index 54f77c95148..b54339aa2c1 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 @@ class OnepageTest extends \PHPUnit\Framework\TestCase - \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 @@ class OnepageTest extends \PHPUnit\Framework\TestCase - $this->configProviderMock, - [$this->layoutProcessorMock], - [], -- $this->serializer -+ $this->serializerMock, -+ $this->serializerMock - ); - } - -@@ -93,6 +94,7 @@ class OnepageTest extends \PHPUnit\Framework\TestCase - $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 @@ class OnepageTest extends \PHPUnit\Framework\TestCase - { - $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/Controller/Cart/AddTest.php b/app/code/Magento/Checkout/Test/Unit/Controller/Cart/AddTest.php -new file mode 100644 -index 00000000000..7c0e542dd67 ---- /dev/null -+++ b/app/code/Magento/Checkout/Test/Unit/Controller/Cart/AddTest.php -@@ -0,0 +1,91 @@ -+<?php -+/** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+declare(strict_types=1); -+ -+namespace Magento\Checkout\Test\Unit\Controller\Cart; -+ -+use Magento\Framework\TestFramework\Unit\Helper\ObjectManager as ObjectManagerHelper; -+ -+class AddTest extends \PHPUnit\Framework\TestCase -+{ -+ /** -+ * @var ObjectManagerHelper -+ */ -+ private $objectManagerHelper; -+ -+ /** -+ * @var \Magento\Framework\Data\Form\FormKey\Validator|\PHPUnit_Framework_MockObject_MockObject -+ */ -+ private $formKeyValidator; -+ -+ /** -+ * @var \Magento\Framework\Controller\Result\RedirectFactory|\PHPUnit_Framework_MockObject_MockObject -+ */ -+ private $resultRedirectFactory; -+ -+ /** -+ * @var \Magento\Framework\App\RequestInterface|\PHPUnit_Framework_MockObject_MockObject -+ */ -+ private $request; -+ -+ /** -+ * @var \Magento\Framework\Message\ManagerInterface|\PHPUnit_Framework_MockObject_MockObject -+ */ -+ private $messageManager; -+ -+ /** -+ * @var \Magento\Checkout\Controller\Cart\Add|\PHPUnit_Framework_MockObject_MockObject -+ */ -+ private $cartAdd; -+ -+ /** -+ * Init mocks for tests. -+ * -+ * @return void -+ */ -+ public function setUp() -+ { -+ $this->formKeyValidator = $this->getMockBuilder(\Magento\Framework\Data\Form\FormKey\Validator::class) -+ ->disableOriginalConstructor()->getMock(); -+ $this->resultRedirectFactory = -+ $this->getMockBuilder(\Magento\Framework\Controller\Result\RedirectFactory::class) -+ ->disableOriginalConstructor()->getMock(); -+ $this->request = $this->getMockBuilder(\Magento\Framework\App\RequestInterface::class) -+ ->disableOriginalConstructor()->getmock(); -+ $this->messageManager = $this->getMockBuilder(\Magento\Framework\Message\ManagerInterface::class) -+ ->disableOriginalConstructor()->getMock(); -+ -+ $this->objectManagerHelper = new ObjectManagerHelper($this); -+ $this->cartAdd = $this->objectManagerHelper->getObject( -+ \Magento\Checkout\Controller\Cart\Add::class, -+ [ -+ '_formKeyValidator' => $this->formKeyValidator, -+ 'resultRedirectFactory' => $this->resultRedirectFactory, -+ '_request' => $this->request, -+ 'messageManager' => $this->messageManager -+ ] -+ ); -+ } -+ -+ /** -+ * Test for method execute. -+ * -+ * @return void -+ */ -+ public function testExecute() -+ { -+ $redirect = $this->getMockBuilder(\Magento\Framework\Controller\Result\Redirect::class) -+ ->disableOriginalConstructor() -+ ->getMock(); -+ $path = '*/*/'; -+ -+ $this->formKeyValidator->expects($this->once())->method('validate')->with($this->request)->willReturn(false); -+ $this->messageManager->expects($this->once())->method('addErrorMessage'); -+ $this->resultRedirectFactory->expects($this->once())->method('create')->willReturn($redirect); -+ $redirect->expects($this->once())->method('setPath')->with($path)->willReturnSelf(); -+ $this->assertEquals($redirect, $this->cartAdd->execute()); -+ } -+} -diff --git a/app/code/Magento/Checkout/Test/Unit/CustomerData/CartTest.php b/app/code/Magento/Checkout/Test/Unit/CustomerData/CartTest.php -index 75e181cbabd..e3e13cc5b1e 100644 ---- a/app/code/Magento/Checkout/Test/Unit/CustomerData/CartTest.php -+++ b/app/code/Magento/Checkout/Test/Unit/CustomerData/CartTest.php -@@ -113,7 +113,7 @@ class CartTest extends \PHPUnit\Framework\TestCase - - $storeMock = $this->createPartialMock(\Magento\Store\Model\System\Store::class, ['getWebsiteId']); - $storeMock->expects($this->once())->method('getWebsiteId')->willReturn($websiteId); -- $quoteMock->expects($this->once())->method('getStore')->willReturn($storeMock); -+ $quoteMock->expects($this->any())->method('getStore')->willReturn($storeMock); - - $productMock = $this->createPartialMock( - \Magento\Catalog\Model\Product::class, -@@ -162,6 +162,7 @@ class CartTest extends \PHPUnit\Framework\TestCase - 'isGuestCheckoutAllowed' => 1, - 'website_id' => $websiteId, - 'subtotalAmount' => 200, -+ 'storeId' => null - ]; - $this->assertEquals($expectedResult, $this->model->getSectionData()); - } -@@ -199,7 +200,7 @@ class CartTest extends \PHPUnit\Framework\TestCase - - $storeMock = $this->createPartialMock(\Magento\Store\Model\System\Store::class, ['getWebsiteId']); - $storeMock->expects($this->once())->method('getWebsiteId')->willReturn($websiteId); -- $quoteMock->expects($this->once())->method('getStore')->willReturn($storeMock); -+ $quoteMock->expects($this->any())->method('getStore')->willReturn($storeMock); - - $this->checkoutCartMock->expects($this->once())->method('getSummaryQty')->willReturn($summaryQty); - $this->checkoutHelperMock->expects($this->once()) -@@ -265,6 +266,7 @@ class CartTest extends \PHPUnit\Framework\TestCase - 'isGuestCheckoutAllowed' => 1, - 'website_id' => $websiteId, - 'subtotalAmount' => 200, -+ 'storeId' => null - ]; - $this->assertEquals($expectedResult, $this->model->getSectionData()); - } -diff --git a/app/code/Magento/Checkout/Test/Unit/Helper/DataTest.php b/app/code/Magento/Checkout/Test/Unit/Helper/DataTest.php -index 53132ffaa74..089ea15726c 100644 ---- a/app/code/Magento/Checkout/Test/Unit/Helper/DataTest.php -+++ b/app/code/Magento/Checkout/Test/Unit/Helper/DataTest.php -@@ -179,7 +179,7 @@ class DataTest extends \PHPUnit\Framework\TestCase - - public function testCanOnepageCheckout() - { -- $this->scopeConfig->expects($this->once())->method('getValue')->with( -+ $this->scopeConfig->expects($this->once())->method('isSetFlag')->with( - 'checkout/options/onepage_checkout_enabled', - 'store' - )->will($this->returnValue(true)); -diff --git a/app/code/Magento/Checkout/Test/Unit/Model/CartTest.php b/app/code/Magento/Checkout/Test/Unit/Model/CartTest.php -index 6bd0bdf258a..bc66324c298 100644 ---- a/app/code/Magento/Checkout/Test/Unit/Model/CartTest.php -+++ b/app/code/Magento/Checkout/Test/Unit/Model/CartTest.php -@@ -318,6 +318,12 @@ class CartTest extends \PHPUnit\Framework\TestCase - $this->productRepository->expects($this->any()) - ->method('getById') - ->will($this->returnValue($product)); -+ -+ $this->eventManagerMock->expects($this->at(0))->method('dispatch')->with( -+ 'checkout_cart_product_add_before', -+ ['info' => $requestInfo, 'product' => $product] -+ ); -+ - $this->quoteMock->expects($this->once()) - ->method('addProduct') - ->will($this->returnValue(1)); -@@ -325,7 +331,7 @@ class CartTest extends \PHPUnit\Framework\TestCase - ->method('getQuote') - ->will($this->returnValue($this->quoteMock)); - -- $this->eventManagerMock->expects($this->at(0))->method('dispatch')->with( -+ $this->eventManagerMock->expects($this->at(1))->method('dispatch')->with( - 'checkout_cart_product_add_after', - ['quote_item' => 1, 'product' => $product] - ); -@@ -363,6 +369,12 @@ class CartTest extends \PHPUnit\Framework\TestCase - $this->productRepository->expects($this->any()) - ->method('getById') - ->will($this->returnValue($product)); -+ -+ $this->eventManagerMock->expects($this->once())->method('dispatch')->with( -+ 'checkout_cart_product_add_before', -+ ['info' => 4, 'product' => $product] -+ ); -+ - $this->quoteMock->expects($this->once()) - ->method('addProduct') - ->will($this->returnValue('error')); -@@ -370,10 +382,6 @@ class CartTest extends \PHPUnit\Framework\TestCase - ->method('getQuote') - ->will($this->returnValue($this->quoteMock)); - -- $this->eventManagerMock->expects($this->never())->method('dispatch')->with( -- 'checkout_cart_product_add_after', -- ['quote_item' => 1, 'product' => $product] -- ); - $this->expectException(\Magento\Framework\Exception\LocalizedException::class); - $this->cart->addProduct(4, 4); - } -@@ -399,6 +407,11 @@ class CartTest extends \PHPUnit\Framework\TestCase - ->method('getById') - ->will($this->returnValue($product)); - -+ $this->eventManagerMock->expects($this->never())->method('dispatch')->with( -+ 'checkout_cart_product_add_before', -+ ['info' => 'bad', 'product' => $product] -+ ); -+ - $this->eventManagerMock->expects($this->never())->method('dispatch')->with( - 'checkout_cart_product_add_after', - ['quote_item' => 1, 'product' => $product] -diff --git a/app/code/Magento/Checkout/Test/Unit/Model/GuestPaymentInformationManagementTest.php b/app/code/Magento/Checkout/Test/Unit/Model/GuestPaymentInformationManagementTest.php -index 853ae0157e6..1de0ebce10f 100644 ---- a/app/code/Magento/Checkout/Test/Unit/Model/GuestPaymentInformationManagementTest.php -+++ b/app/code/Magento/Checkout/Test/Unit/Model/GuestPaymentInformationManagementTest.php -@@ -273,7 +273,7 @@ class GuestPaymentInformationManagementTest extends \PHPUnit\Framework\TestCase - */ - private function getMockForAssignBillingAddress( - int $cartId, -- \PHPUnit_Framework_MockObject_MockObject $billingAddressMock -+ \PHPUnit_Framework_MockObject_MockObject $billingAddressMock - ) : void { - $quoteIdMask = $this->createPartialMock(QuoteIdMask::class, ['getQuoteId', 'load']); - $this->quoteIdMaskFactoryMock->method('create') -@@ -287,9 +287,11 @@ class GuestPaymentInformationManagementTest extends \PHPUnit\Framework\TestCase - $billingAddressId = 1; - $quote = $this->createMock(Quote::class); - $quoteBillingAddress = $this->createMock(Address::class); -+ $shippingRate = $this->createPartialMock(\Magento\Quote\Model\Quote\Address\Rate::class, []); -+ $shippingRate->setCarrier('flatrate'); - $quoteShippingAddress = $this->createPartialMock( - Address::class, -- ['setLimitCarrier', 'getShippingMethod'] -+ ['setLimitCarrier', 'getShippingMethod', 'getShippingRateByCode'] - ); - $this->cartRepositoryMock->method('getActive') - ->with($cartId) -@@ -309,6 +311,9 @@ class GuestPaymentInformationManagementTest extends \PHPUnit\Framework\TestCase - $quote->expects($this->once()) - ->method('setBillingAddress') - ->with($billingAddressMock); -+ $quoteShippingAddress->expects($this->any()) -+ ->method('getShippingRateByCode') -+ ->willReturn($shippingRate); - $quote->expects($this->once()) - ->method('setDataChanges') - ->willReturnSelf(); -diff --git a/app/code/Magento/Checkout/Test/Unit/Model/Layout/DepersonalizePluginTest.php b/app/code/Magento/Checkout/Test/Unit/Model/Layout/DepersonalizePluginTest.php -index 350f9954208..3cc80e14fd0 100644 ---- a/app/code/Magento/Checkout/Test/Unit/Model/Layout/DepersonalizePluginTest.php -+++ b/app/code/Magento/Checkout/Test/Unit/Model/Layout/DepersonalizePluginTest.php -@@ -43,7 +43,7 @@ class DepersonalizePluginTest extends \PHPUnit\Framework\TestCase - ); - $this->checkoutSessionMock = $this->createPartialMock(\Magento\Checkout\Model\Session::class, ['clearStorage']); - $this->requestMock = $this->createMock(\Magento\Framework\App\Request\Http::class); -- $this->moduleManagerMock = $this->createMock(\Magento\Framework\Module\Manager::class); -+ $this->moduleManagerMock = $this->createMock(\Magento\Framework\Module\ModuleManagerInterface::class); - $this->cacheConfigMock = $this->createMock(\Magento\PageCache\Model\Config::class); - $this->depersonalizeCheckerMock = $this->createMock(\Magento\PageCache\Model\DepersonalizeChecker::class); - -diff --git a/app/code/Magento/Checkout/Test/Unit/Model/PaymentInformationManagementTest.php b/app/code/Magento/Checkout/Test/Unit/Model/PaymentInformationManagementTest.php -index 77c15fccfa8..df5c255398e 100644 ---- a/app/code/Magento/Checkout/Test/Unit/Model/PaymentInformationManagementTest.php -+++ b/app/code/Magento/Checkout/Test/Unit/Model/PaymentInformationManagementTest.php -@@ -163,6 +163,31 @@ class PaymentInformationManagementTest extends \PHPUnit\Framework\TestCase - $this->model->savePaymentInformationAndPlaceOrder($cartId, $paymentMock, $billingAddressMock); - } - -+ /** -+ * Test for save payment and place order with new billing address -+ * -+ * @return void -+ */ -+ public function testSavePaymentInformationAndPlaceOrderWithNewBillingAddress(): void -+ { -+ $cartId = 100; -+ $quoteBillingAddressId = 1; -+ $customerId = 1; -+ $quoteMock = $this->createMock(\Magento\Quote\Model\Quote::class); -+ $quoteBillingAddress = $this->createMock(\Magento\Quote\Model\Quote\Address::class); -+ $billingAddressMock = $this->createMock(\Magento\Quote\Api\Data\AddressInterface::class); -+ $paymentMock = $this->createMock(\Magento\Quote\Api\Data\PaymentInterface::class); -+ -+ $quoteBillingAddress->method('getCustomerId')->willReturn($customerId); -+ $quoteMock->method('getBillingAddress')->willReturn($quoteBillingAddress); -+ $quoteBillingAddress->method('getId')->willReturn($quoteBillingAddressId); -+ $this->cartRepositoryMock->method('getActive')->with($cartId)->willReturn($quoteMock); -+ -+ $this->paymentMethodManagementMock->expects($this->once())->method('set')->with($cartId, $paymentMock); -+ $billingAddressMock->expects($this->once())->method('setCustomerId')->with($customerId); -+ $this->assertTrue($this->model->savePaymentInformation($cartId, $paymentMock, $billingAddressMock)); -+ } -+ - /** - * @param int $cartId - * @param \PHPUnit_Framework_MockObject_MockObject $billingAddressMock -@@ -172,17 +197,21 @@ class PaymentInformationManagementTest extends \PHPUnit\Framework\TestCase - $billingAddressId = 1; - $quoteMock = $this->createMock(\Magento\Quote\Model\Quote::class); - $quoteBillingAddress = $this->createMock(\Magento\Quote\Model\Quote\Address::class); -+ $shippingRate = $this->createPartialMock(\Magento\Quote\Model\Quote\Address\Rate::class, []); -+ $shippingRate->setCarrier('flatrate'); - $quoteShippingAddress = $this->createPartialMock( - \Magento\Quote\Model\Quote\Address::class, -- ['setLimitCarrier', 'getShippingMethod'] -+ ['setLimitCarrier', 'getShippingMethod', 'getShippingRateByCode'] - ); - $this->cartRepositoryMock->expects($this->any())->method('getActive')->with($cartId)->willReturn($quoteMock); -- $quoteMock->expects($this->once())->method('getBillingAddress')->willReturn($quoteBillingAddress); -+ $quoteMock->method('getBillingAddress')->willReturn($quoteBillingAddress); - $quoteMock->expects($this->once())->method('getShippingAddress')->willReturn($quoteShippingAddress); - $quoteBillingAddress->expects($this->once())->method('getId')->willReturn($billingAddressId); -+ $quoteBillingAddress->expects($this->once())->method('getId')->willReturn($billingAddressId); - $quoteMock->expects($this->once())->method('removeAddress')->with($billingAddressId); - $quoteMock->expects($this->once())->method('setBillingAddress')->with($billingAddressMock); - $quoteMock->expects($this->once())->method('setDataChanges')->willReturnSelf(); -+ $quoteShippingAddress->expects($this->any())->method('getShippingRateByCode')->willReturn($shippingRate); - $quoteShippingAddress->expects($this->any())->method('getShippingMethod')->willReturn('flatrate_flatrate'); - $quoteShippingAddress->expects($this->once())->method('setLimitCarrier')->with('flatrate')->willReturnSelf(); - } -diff --git a/app/code/Magento/Checkout/Test/Unit/Model/SidebarTest.php b/app/code/Magento/Checkout/Test/Unit/Model/SidebarTest.php -index a196b10478c..ff7340f87f3 100644 ---- a/app/code/Magento/Checkout/Test/Unit/Model/SidebarTest.php -+++ b/app/code/Magento/Checkout/Test/Unit/Model/SidebarTest.php -@@ -98,7 +98,7 @@ class SidebarTest extends \PHPUnit\Framework\TestCase - - /** - * @expectedException \Magento\Framework\Exception\LocalizedException -- * @exceptedExceptionMessage The quote item isn't found. Verify the item and try again. -+ * @expectedExceptionMessage The quote item isn't found. Verify the item and try again. - */ - public function testCheckQuoteItemWithException() - { -diff --git a/app/code/Magento/Checkout/Test/Unit/Observer/SalesQuoteSaveAfterObserverTest.php b/app/code/Magento/Checkout/Test/Unit/Observer/SalesQuoteSaveAfterObserverTest.php -index 6070bb5d424..dabaf173d90 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 @@ class SalesQuoteSaveAfterObserverTest extends \PHPUnit\Framework\TestCase - - public function testSalesQuoteSaveAfter() - { -+ $quoteId = 7; - $observer = $this->createMock(\Magento\Framework\Event\Observer::class); - $observer->expects($this->once())->method('getEvent')->will( - $this->returnValue(new \Magento\Framework\DataObject( -- ['quote' => new \Magento\Framework\DataObject(['is_checkout_cart' => 1, 'id' => 7])] -+ ['quote' => new \Magento\Framework\DataObject(['is_checkout_cart' => 1, 'id' => $quoteId])] - )) - ); -- $this->checkoutSession->expects($this->once())->method('getQuoteId')->with(7); -+ $this->checkoutSession->expects($this->once())->method('setQuoteId')->with($quoteId); - - $this->object->execute($observer); - } -diff --git a/app/code/Magento/Checkout/etc/di.xml b/app/code/Magento/Checkout/etc/di.xml -index 71dfd12bb47..4ebd594a285 100644 ---- a/app/code/Magento/Checkout/etc/di.xml -+++ b/app/code/Magento/Checkout/etc/di.xml -@@ -49,7 +49,4 @@ - </argument> - </arguments> - </type> -- <type name="Magento\Quote\Model\Quote"> -- <plugin name="clear_addresses_after_product_delete" type="Magento\Checkout\Plugin\Model\Quote\ResetQuoteAddresses"/> -- </type> - </config> -diff --git a/app/code/Magento/Checkout/etc/frontend/di.xml b/app/code/Magento/Checkout/etc/frontend/di.xml -index f2aced01b39..8f35fe9f37a 100644 ---- a/app/code/Magento/Checkout/etc/frontend/di.xml -+++ b/app/code/Magento/Checkout/etc/frontend/di.xml -@@ -59,6 +59,7 @@ - <item name="totalsSortOrder" xsi:type="object">Magento\Checkout\Block\Checkout\TotalsProcessor</item> - <item name="directoryData" xsi:type="object">Magento\Checkout\Block\Checkout\DirectoryDataProcessor</item> - </argument> -+ <argument name="serializer" xsi:type="object">Magento\Framework\Serialize\Serializer\JsonHexTag</argument> - </arguments> - </type> - <type name="Magento\Checkout\Block\Cart\Totals"> -@@ -83,4 +84,19 @@ - </argument> - </arguments> - </type> -+ <type name="Magento\Framework\View\Element\Message\MessageConfigurationsPool"> -+ <arguments> -+ <argument name="configurationsMap" xsi:type="array"> -+ <item name="addCartSuccessMessage" xsi:type="array"> -+ <item name="renderer" xsi:type="const">\Magento\Framework\View\Element\Message\Renderer\BlockRenderer::CODE</item> -+ <item name="data" xsi:type="array"> -+ <item name="template" xsi:type="string">Magento_Checkout::messages/addCartSuccessMessage.phtml</item> -+ </item> -+ </item> -+ </argument> -+ </arguments> -+ </type> -+ <type name="Magento\Quote\Model\Quote"> -+ <plugin name="clear_addresses_after_product_delete" type="Magento\Checkout\Plugin\Model\Quote\ResetQuoteAddresses"/> -+ </type> - </config> -diff --git a/app/code/Magento/Checkout/etc/frontend/sections.xml b/app/code/Magento/Checkout/etc/frontend/sections.xml -index 35733a6119a..90c2878f501 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 a6ea2c13579..7f2f0b43903 100644 ---- a/app/code/Magento/Checkout/i18n/en_US.csv -+++ b/app/code/Magento/Checkout/i18n/en_US.csv -@@ -182,3 +182,4 @@ Payment,Payment - "Items in Cart","Items in Cart" - "Close","Close" - "Show Cross-sell Items in the Shopping Cart","Show Cross-sell Items in the Shopping Cart" -+"You added %1 to your <a href=""%2"">shopping cart</a>.","You added %1 to your <a href=""%2"">shopping cart</a>." -\ No newline at end of file -diff --git a/app/code/Magento/Checkout/view/adminhtml/email/failed_payment.html b/app/code/Magento/Checkout/view/adminhtml/email/failed_payment.html -index fb55f9b601d..03ad7d9e8d8 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 @@ - - <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 d4fadedf5d7..a305413bcf1 100644 ---- a/app/code/Magento/Checkout/view/frontend/layout/checkout_index_index.xml -+++ b/app/code/Magento/Checkout/view/frontend/layout/checkout_index_index.xml -@@ -105,7 +105,7 @@ - <item name="trigger" xsi:type="string">opc-new-shipping-address</item> - <item name="buttons" xsi:type="array"> - <item name="save" xsi:type="array"> -- <item name="text" xsi:type="string" translate="true">Save Address</item> -+ <item name="text" xsi:type="string" translate="true">Ship here</item> - <item name="class" xsi:type="string">action primary action-save-address</item> - </item> - <item name="cancel" xsi:type="array"> -@@ -404,6 +404,10 @@ - <item name="component" xsi:type="string">Magento_Checkout/js/view/summary/item/details/subtotal</item> - <item name="displayArea" xsi:type="string">after_details</item> - </item> -+ <item name="message" xsi:type="array"> -+ <item name="component" xsi:type="string">Magento_Checkout/js/view/summary/item/details/message</item> -+ <item name="displayArea" xsi:type="string">item_message</item> -+ </item> - </item> - </item> - </item> -diff --git a/app/code/Magento/Checkout/view/frontend/templates/button.phtml b/app/code/Magento/Checkout/view/frontend/templates/button.phtml -index c3edfe30f8b..b0087794ea8 100644 ---- a/app/code/Magento/Checkout/view/frontend/templates/button.phtml -+++ b/app/code/Magento/Checkout/view/frontend/templates/button.phtml -@@ -3,15 +3,12 @@ - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -- --// @codingStandardsIgnoreFile -- - ?> - <?php /** @var $block \Magento\Checkout\Block\Onepage\Success */ ?> - - <?php if ($block->getCanViewOrder() && $block->getCanPrintOrder()) :?> -- <a href="<?= /* @escapeNotVerified */ $block->getPrintUrl() ?>" target="_blank" class="print"> -- <?= /* @escapeNotVerified */ __('Print receipt') ?> -+ <a href="<?= $block->escapeUrl($block->getPrintUrl()) ?>" target="_blank" class="print"> -+ <?= $block->escapeHtml(__('Print receipt')) ?> - </a> - <?= $block->getChildHtml() ?> - <?php endif;?> -diff --git a/app/code/Magento/Checkout/view/frontend/templates/cart.phtml b/app/code/Magento/Checkout/view/frontend/templates/cart.phtml -index 929f053febf..e71ea8c6628 100644 ---- a/app/code/Magento/Checkout/view/frontend/templates/cart.phtml -+++ b/app/code/Magento/Checkout/view/frontend/templates/cart.phtml -@@ -12,7 +12,9 @@ - */ - - if ($block->getItemsCount()) { -+ // phpcs:ignore Magento2.Security.LanguageConstruct.DirectOutput - echo $block->getChildHtml('with-items'); - } else { -+ // phpcs:ignore Magento2.Security.LanguageConstruct.DirectOutput - echo $block->getChildHtml('no-items'); - } -diff --git a/app/code/Magento/Checkout/view/frontend/templates/cart/additional/info.phtml b/app/code/Magento/Checkout/view/frontend/templates/cart/additional/info.phtml -index cb92e62f1f0..b807a6db019 100644 ---- a/app/code/Magento/Checkout/view/frontend/templates/cart/additional/info.phtml -+++ b/app/code/Magento/Checkout/view/frontend/templates/cart/additional/info.phtml -@@ -4,8 +4,6 @@ - * See COPYING.txt for license details. - */ - --// @codingStandardsIgnoreFile -- - ?> - <?php - /** -@@ -16,6 +14,7 @@ - <?php - $name = $block->getNameInLayout(); - foreach ($block->getChildNames($name) as $childName) { -+ // phpcs:ignore Magento2.Security.LanguageConstruct.DirectOutput - echo $block->getChildBlock($childName)->setItem($block->getItem())->toHtml(); - } - ?> -diff --git a/app/code/Magento/Checkout/view/frontend/templates/cart/coupon.phtml b/app/code/Magento/Checkout/view/frontend/templates/cart/coupon.phtml -index 1d67b325e01..4522500d395 100644 ---- a/app/code/Magento/Checkout/view/frontend/templates/cart/coupon.phtml -+++ b/app/code/Magento/Checkout/view/frontend/templates/cart/coupon.phtml -@@ -4,16 +4,17 @@ - * See COPYING.txt for license details. - */ - --// @codingStandardsIgnoreFile -- - ?> --<div class="block discount" id="block-discount" data-mage-init='{"collapsible":{"openedState": "active", "saveState": false}}'> -+<div class="block discount" -+ id="block-discount" -+ data-mage-init='{"collapsible":{"openedState": "active", "saveState": false}}' -+> - <div class="title" data-role="title"> -- <strong id="block-discount-heading" role="heading" aria-level="2"><?= /* @escapeNotVerified */ __('Apply Discount Code') ?></strong> -+ <strong id="block-discount-heading" role="heading" aria-level="2"><?= $block->escapeHtml(__('Apply Discount Code')) ?></strong> - </div> - <div class="content" data-role="content" aria-labelledby="block-discount-heading"> - <form id="discount-coupon-form" -- action="<?= /* @escapeNotVerified */ $block->getUrl('checkout/cart/couponPost') ?>" -+ action="<?= $block->escapeUrl($block->getUrl('checkout/cart/couponPost')) ?>" - method="post" - data-mage-init='{"discountCode":{"couponCodeSelector": "#coupon_code", - "removeCouponSelector": "#remove-coupon", -@@ -22,21 +23,30 @@ - <div class="fieldset coupon<?= strlen($block->getCouponCode()) ? ' applied' : '' ?>"> - <input type="hidden" name="remove" id="remove-coupon" value="0" /> - <div class="field"> -- <label for="coupon_code" class="label"><span><?= /* @escapeNotVerified */ __('Enter discount code') ?></span></label> -+ <label for="coupon_code" class="label"><span><?= $block->escapeHtml(__('Enter discount code')) ?></span></label> - <div class="control"> -- <input type="text" class="input-text" id="coupon_code" name="coupon_code" value="<?= $block->escapeHtml($block->getCouponCode()) ?>" placeholder="<?= $block->escapeHtml(__('Enter discount code')) ?>" <?php if (strlen($block->getCouponCode())): ?> disabled="disabled" <?php endif; ?> /> -+ <input type="text" -+ class="input-text" -+ id="coupon_code" -+ name="coupon_code" -+ value="<?= $block->escapeHtmlAttr($block->getCouponCode()) ?>" -+ placeholder="<?= $block->escapeHtmlAttr(__('Enter discount code')) ?>" -+ <?php if (strlen($block->getCouponCode())) :?> -+ disabled="disabled" -+ <?php endif; ?> -+ /> - </div> - </div> - <div class="actions-toolbar"> -- <?php if (!strlen($block->getCouponCode())): ?> -+ <?php if (!strlen($block->getCouponCode())) :?> - <div class="primary"> -- <button class="action apply primary" type="button" value="<?= /* @escapeNotVerified */ __('Apply Discount') ?>"> -- <span><?= /* @escapeNotVerified */ __('Apply Discount') ?></span> -+ <button class="action apply primary" type="button" value="<?= $block->escapeHtmlAttr(__('Apply Discount')) ?>"> -+ <span><?= $block->escapeHtml(__('Apply Discount')) ?></span> - </button> - </div> -- <?php else: ?> -+ <?php else :?> - <div class="primary"> -- <button type="button" class="action cancel primary" value="<?= /* @escapeNotVerified */ __('Cancel Coupon') ?>"><span><?= /* @escapeNotVerified */ __('Cancel Coupon') ?></span></button> -+ <button type="button" class="action cancel primary" value="<?= $block->escapeHtmlAttr(__('Cancel Coupon')) ?>"><span><?= $block->escapeHtml(__('Cancel Coupon')) ?></span></button> - </div> - <?php endif; ?> - </div> -diff --git a/app/code/Magento/Checkout/view/frontend/templates/cart/form.phtml b/app/code/Magento/Checkout/view/frontend/templates/cart/form.phtml -index 71b1392d539..e1ab036c7d8 100644 ---- a/app/code/Magento/Checkout/view/frontend/templates/cart/form.phtml -+++ b/app/code/Magento/Checkout/view/frontend/templates/cart/form.phtml -@@ -4,51 +4,56 @@ - * See COPYING.txt for license details. - */ - --// @codingStandardsIgnoreFile -+// phpcs:disable Magento2.Templates.ThisInTemplate - - /** @var $block \Magento\Checkout\Block\Cart\Grid */ - ?> --<?php $mergedCells = ($this->helper('Magento\Tax\Helper\Data')->displayCartBothPrices() ? 2 : 1); ?> -+<?php $mergedCells = ($this->helper(Magento\Tax\Helper\Data::class)->displayCartBothPrices() ? 2 : 1); ?> - <?= $block->getChildHtml('form_before') ?> --<form action="<?= /* @escapeNotVerified */ $block->getUrl('checkout/cart/updatePost') ?>" -+<form action="<?= $block->escapeUrl($block->getUrl('checkout/cart/updatePost')) ?>" - method="post" - id="form-validate" - data-mage-init='{"Magento_Checkout/js/action/update-shopping-cart": -- {"validationURL" : "/checkout/cart/updateItemQty"} -+ {"validationURL" : "<?= $block->escapeUrl($block->getUrl('checkout/cart/updateItemQty')) ?>", -+ "updateCartActionContainer": "#update_cart_action_container"} - }' - class="form form-cart"> - <?= $block->getBlockHtml('formkey') ?> - <div class="cart table-wrapper<?= $mergedCells == 2 ? ' detailed' : '' ?>"> -- <?php if ($block->getPagerHtml()): ?> -- <div class="cart-products-toolbar cart-products-toolbar-top toolbar" data-attribute="cart-products-toolbar-top"><?= $block->getPagerHtml() ?></div> -+ <?php if ($block->getPagerHtml()) :?> -+ <div class="cart-products-toolbar cart-products-toolbar-top toolbar" -+ data-attribute="cart-products-toolbar-top"><?= $block->getPagerHtml() ?> -+ </div> - <?php endif ?> - <table id="shopping-cart-table" - class="cart items data table" -- data-mage-init='{"shoppingCart":{"emptyCartButton": "action.clear", -+ data-mage-init='{"shoppingCart":{"emptyCartButton": ".action.clear", - "updateCartActionContainer": "#update_cart_action_container"}}'> -- <caption role="heading" aria-level="2" class="table-caption"><?= /* @escapeNotVerified */ __('Shopping Cart Items') ?></caption> -+ <caption class="table-caption"><?= $block->escapeHtml(__('Shopping Cart Items')) ?></caption> - <thead> - <tr> -- <th class="col item" scope="col"><span><?= /* @escapeNotVerified */ __('Item') ?></span></th> -- <th class="col price" scope="col"><span><?= /* @escapeNotVerified */ __('Price') ?></span></th> -- <th class="col qty" scope="col"><span><?= /* @escapeNotVerified */ __('Qty') ?></span></th> -- <th class="col subtotal" scope="col"><span><?= /* @escapeNotVerified */ __('Subtotal') ?></span></th> -+ <th class="col item" scope="col"><span><?= $block->escapeHtml(__('Item')) ?></span></th> -+ <th class="col price" scope="col"><span><?= $block->escapeHtml(__('Price')) ?></span></th> -+ <th class="col qty" scope="col"><span><?= $block->escapeHtml(__('Qty')) ?></span></th> -+ <th class="col subtotal" scope="col"><span><?= $block->escapeHtml(__('Subtotal')) ?></span></th> - </tr> - </thead> -- <?php foreach ($block->getItems() as $_item): ?> -+ <?php foreach ($block->getItems() as $_item) :?> - <?= $block->getItemHtml($_item) ?> - <?php endforeach ?> - </table> -- <?php if ($block->getPagerHtml()): ?> -- <div class="cart-products-toolbar cart-products-toolbar-bottom toolbar" data-attribute="cart-products-toolbar-bottom"><?= $block->getPagerHtml() ?></div> -+ <?php if ($block->getPagerHtml()) :?> -+ <div class="cart-products-toolbar cart-products-toolbar-bottom toolbar" -+ data-attribute="cart-products-toolbar-bottom"><?= $block->getPagerHtml() ?> -+ </div> - <?php endif ?> - </div> - <div class="cart main actions"> -- <?php if ($block->getContinueShoppingUrl()): ?> -+ <?php if ($block->getContinueShoppingUrl()) :?> - <a class="action continue" - href="<?= $block->escapeUrl($block->getContinueShoppingUrl()) ?>" - title="<?= $block->escapeHtml(__('Continue Shopping')) ?>"> -- <span><?= /* @escapeNotVerified */ __('Continue Shopping') ?></span> -+ <span><?= $block->escapeHtml(__('Continue Shopping')) ?></span> - </a> - <?php endif; ?> - <button type="submit" -@@ -57,7 +62,7 @@ - value="empty_cart" - title="<?= $block->escapeHtml(__('Clear Shopping Cart')) ?>" - class="action clear" id="empty_cart_button"> -- <span><?= /* @escapeNotVerified */ __('Clear Shopping Cart') ?></span> -+ <span><?= $block->escapeHtml(__('Clear Shopping Cart')) ?></span> - </button> - <button type="submit" - name="update_cart_action" -@@ -65,7 +70,7 @@ - value="update_qty" - title="<?= $block->escapeHtml(__('Update Shopping Cart')) ?>" - class="action update"> -- <span><?= /* @escapeNotVerified */ __('Update Shopping Cart') ?></span> -+ <span><?= $block->escapeHtml(__('Update Shopping Cart')) ?></span> - </button> - <input type="hidden" value="" id="update_cart_action_container" data-cart-item-update=""/> - </div> -diff --git a/app/code/Magento/Checkout/view/frontend/templates/cart/item/configure/updatecart.phtml b/app/code/Magento/Checkout/view/frontend/templates/cart/item/configure/updatecart.phtml -index c1db2f7775c..3b09512eb50 100644 ---- a/app/code/Magento/Checkout/view/frontend/templates/cart/item/configure/updatecart.phtml -+++ b/app/code/Magento/Checkout/view/frontend/templates/cart/item/configure/updatecart.phtml -@@ -4,24 +4,23 @@ - * See COPYING.txt for license details. - */ - --// @codingStandardsIgnoreFile -- - /** @var $block \Magento\Catalog\Block\Product\View */ - ?> - <?php $_product = $block->getProduct(); ?> - <?php $buttonTitle = __('Update Cart'); ?> --<?php if ($_product->isSaleable()): ?> -+<?php if ($_product->isSaleable()) :?> - <div class="box-tocart update"> - <fieldset class="fieldset"> -- <?php if ($block->shouldRenderQuantity()): ?> -+ <?php if ($block->shouldRenderQuantity()) :?> - <div class="field qty"> -- <label class="label" for="qty"><span><?= /* @escapeNotVerified */ __('Qty') ?></span></label> -+ <label class="label" for="qty"><span><?= $block->escapeHtml(__('Qty')) ?></span></label> - <div class="control"> - <input type="number" - name="qty" - id="qty" -+ min="0" - value="" -- title="<?= /* @escapeNotVerified */ __('Qty') ?>" -+ title="<?= $block->escapeHtmlAttr(__('Qty')) ?>" - class="input-text qty" - data-validate="<?= $block->escapeHtml(json_encode($block->getQuantityValidators())) ?>"/> - </div> -@@ -29,10 +28,10 @@ - <?php endif; ?> - <div class="actions"> - <button type="submit" -- title="<?= /* @escapeNotVerified */ $buttonTitle ?>" -+ title="<?= $block->escapeHtmlAttr($buttonTitle) ?>" - class="action primary tocart" - id="product-updatecart-button"> -- <span><?= /* @escapeNotVerified */ $buttonTitle ?></span> -+ <span><?= $block->escapeHtml($buttonTitle) ?></span> - </button> - <?= $block->getChildHtml('', true) ?> - </div> -diff --git a/app/code/Magento/Checkout/view/frontend/templates/cart/item/default.phtml b/app/code/Magento/Checkout/view/frontend/templates/cart/item/default.phtml -index 0567c61f0db..77dde1eab48 100644 ---- a/app/code/Magento/Checkout/view/frontend/templates/cart/item/default.phtml -+++ b/app/code/Magento/Checkout/view/frontend/templates/cart/item/default.phtml -@@ -4,7 +4,8 @@ - * See COPYING.txt for license details. - */ - --// @codingStandardsIgnoreFile -+// phpcs:disable Magento2.Templates.ThisInTemplate -+// phpcs:disable Magento2.Files.LineLength.MaxExceeded - - /** @var $block \Magento\Checkout\Block\Cart\Item\Renderer */ - -@@ -12,108 +13,118 @@ $_item = $block->getItem(); - $product = $_item->getProduct(); - $isVisibleProduct = $product->isVisibleInSiteVisibility(); - /** @var \Magento\Msrp\Helper\Data $helper */ --$helper = $this->helper('Magento\Msrp\Helper\Data'); -+$helper = $this->helper(Magento\Msrp\Helper\Data::class); - $canApplyMsrp = $helper->isShowBeforeOrderConfirm($product) && $helper->isMinimalPriceLessMsrp($product); - ?> - <tbody class="cart item"> - <tr class="item-info"> - <td data-th="<?= $block->escapeHtml(__('Item')) ?>" class="col item"> -- <?php if ($block->hasProductUrl()):?> -- <a href="<?= /* @escapeNotVerified */ $block->getProductUrl() ?>" -+ <?php if ($block->hasProductUrl()) :?> -+ <a href="<?= $block->escapeUrl($block->getProductUrl()) ?>" - title="<?= $block->escapeHtml($block->getProductName()) ?>" - tabindex="-1" - class="product-item-photo"> -- <?php else:?> -+ <?php else :?> - <span class="product-item-photo"> - <?php endif;?> - <?= $block->getImage($block->getProductForThumbnail(), 'cart_page_product_thumbnail')->toHtml() ?> -- <?php if ($block->hasProductUrl()):?> -+ <?php if ($block->hasProductUrl()) :?> - </a> -- <?php else: ?> -+ <?php else :?> - </span> - <?php endif; ?> - <div class="product-item-details"> - <strong class="product-item-name"> -- <?php if ($block->hasProductUrl()):?> -- <a href="<?= /* @escapeNotVerified */ $block->getProductUrl() ?>"><?= $block->escapeHtml($block->getProductName()) ?></a> -- <?php else: ?> -+ <?php if ($block->hasProductUrl()) :?> -+ <a href="<?= $block->escapeUrl($block->getProductUrl()) ?>"><?= $block->escapeHtml($block->getProductName()) ?></a> -+ <?php else :?> - <?= $block->escapeHtml($block->getProductName()) ?> - <?php endif; ?> - </strong> -- <?php if ($_options = $block->getOptionList()):?> -+ <?php if ($_options = $block->getOptionList()) :?> - <dl class="item-options"> -- <?php foreach ($_options as $_option) : ?> -+ <?php foreach ($_options as $_option) :?> - <?php $_formatedOptionValue = $block->getFormatedOptionValue($_option) ?> - <dt><?= $block->escapeHtml($_option['label']) ?></dt> - <dd> -- <?php if (isset($_formatedOptionValue['full_view'])): ?> -- <?= /* @escapeNotVerified */ $_formatedOptionValue['full_view'] ?> -- <?php else: ?> -- <?= /* @escapeNotVerified */ $_formatedOptionValue['value'] ?> -+ <?php if (isset($_formatedOptionValue['full_view'])) :?> -+ <?= $block->escapeHtml($_formatedOptionValue['full_view']) ?> -+ <?php else :?> -+ <?= $block->escapeHtml($_formatedOptionValue['value'], ['span']) ?> - <?php endif; ?> - </dd> - <?php endforeach; ?> - </dl> - <?php endif;?> -- <?php if ($messages = $block->getMessages()): ?> -- <?php foreach ($messages as $message): ?> -- <div class="cart item message <?= /* @escapeNotVerified */ $message['type'] ?>"><div><?= $block->escapeHtml($message['text']) ?></div></div> -+ <?php if ($messages = $block->getMessages()) :?> -+ <?php foreach ($messages as $message) :?> -+ <div class= "cart item message <?= $block->escapeHtmlAttr($message['type']) ?>"> -+ <div><?= $block->escapeHtml($message['text']) ?></div> -+ </div> - <?php endforeach; ?> - <?php endif; ?> - <?php $addInfoBlock = $block->getProductAdditionalInformationBlock(); ?> -- <?php if ($addInfoBlock): ?> -+ <?php if ($addInfoBlock) :?> - <?= $addInfoBlock->setItem($_item)->toHtml() ?> - <?php endif;?> - </div> - </td> - -- <?php if ($canApplyMsrp): ?> -+ <?php if ($canApplyMsrp) :?> - <td class="col msrp" data-th="<?= $block->escapeHtml(__('Price')) ?>"> - <span class="pricing msrp"> -- <span class="msrp notice"><?= /* @escapeNotVerified */ __('See price before order confirmation.') ?></span> -+ <span class="msrp notice"><?= $block->escapeHtml(__('See price before order confirmation.')) ?></span> - <?php $helpLinkId = 'cart-msrp-help-' . $_item->getId(); ?> -- <a href="#" class="action help map" id="<?= /* @escapeNotVerified */ ($helpLinkId) ?>" data-mage-init='{"addToCart":{"helpLinkId": "#<?= /* @escapeNotVerified */ $helpLinkId ?>","productName": "<?= /* @escapeNotVerified */ $product->getName() ?>","showAddToCart": false}}'> -- <span><?= /* @escapeNotVerified */ __("What's this?") ?></span> -+ <a href="#" class="action help map" -+ id="<?= ($block->escapeHtmlAttr($helpLinkId)) ?>" -+ data-mage-init='{"addToCart":{ -+ "helpLinkId": "#<?= $block->escapeJs($block->escapeHtml($helpLinkId)) ?>", -+ "productName": "<?= $block->escapeJs($block->escapeHtml($product->getName())) ?>", -+ "showAddToCart": false -+ } -+ }' -+ > -+ <span><?= $block->escapeHtml(__("What's this?")) ?></span> - </a> - </span> - </td> -- <?php else: ?> -+ <?php else :?> - <td class="col price" data-th="<?= $block->escapeHtml(__('Price')) ?>"> - <?= $block->getUnitPriceHtml($_item) ?> - </td> - <?php endif; ?> - <td class="col qty" data-th="<?= $block->escapeHtml(__('Qty')) ?>"> - <div class="field qty"> -- <label class="label" for="cart-<?= /* @escapeNotVerified */ $_item->getId() ?>-qty"> -- <span><?= /* @escapeNotVerified */ __('Qty') ?></span> -- </label> - <div class="control qty"> -- <input id="cart-<?= /* @escapeNotVerified */ $_item->getId() ?>-qty" -- name="cart[<?= /* @escapeNotVerified */ $_item->getId() ?>][qty]" -- data-cart-item-id="<?= $block->escapeHtml($_item->getSku()) ?>" -- value="<?= /* @escapeNotVerified */ $block->getQty() ?>" -- type="number" -- size="4" -- title="<?= $block->escapeHtml(__('Qty')) ?>" -- class="input-text qty" -- data-validate="{required:true,'validate-greater-than-zero':true}" -- data-role="cart-item-qty"/> -+ <label for="cart-<?= $block->escapeHtmlAttr($_item->getId()) ?>-qty"> -+ <span class="label"><?= $block->escapeHtml(__('Qty')) ?></span> -+ <input id="cart-<?= $block->escapeHtmlAttr($_item->getId()) ?>-qty" -+ name="cart[<?= $block->escapeHtmlAttr($_item->getId()) ?>][qty]" -+ data-cart-item-id="<?= $block->escapeHtmlAttr($_item->getSku()) ?>" -+ value="<?= $block->escapeHtmlAttr($block->getQty()) ?>" -+ type="number" -+ size="4" -+ title="<?= $block->escapeHtmlAttr(__('Qty')) ?>" -+ class="input-text qty" -+ data-validate="{required:true,'validate-greater-than-zero':true}" -+ data-role="cart-item-qty"/> -+ </label> - </div> - </div> - </td> - - <td class="col subtotal" data-th="<?= $block->escapeHtml(__('Subtotal')) ?>"> -- <?php if ($canApplyMsrp): ?> -+ <?php if ($canApplyMsrp) :?> - <span class="cart msrp subtotal">--</span> -- <?php else: ?> -+ <?php else :?> - <?= $block->getRowTotalHtml($_item) ?> - <?php endif; ?> - </td> - </tr> - <tr class="item-actions"> -- <td colspan="100"> -+ <td colspan="4"> - <div class="actions-toolbar"> -- <?= /* @escapeNotVerified */ $block->getActions($_item) ?> -+ <?= /* @noEscape */ $block->getActions($_item) ?> - </div> - </td> - </tr> -diff --git a/app/code/Magento/Checkout/view/frontend/templates/cart/item/price/sidebar.phtml b/app/code/Magento/Checkout/view/frontend/templates/cart/item/price/sidebar.phtml -index d7a625695b4..1d30cfc8dc4 100644 ---- a/app/code/Magento/Checkout/view/frontend/templates/cart/item/price/sidebar.phtml -+++ b/app/code/Magento/Checkout/view/frontend/templates/cart/item/price/sidebar.phtml -@@ -4,12 +4,17 @@ - * See COPYING.txt for license details. - */ - --// @codingStandardsIgnoreFile -+// phpcs:disable Magento2.Templates.ThisInTemplate - - /** @var $block \Magento\Checkout\Block\Item\Price\Renderer */ - ?> - <?php $_item = $block->getItem() ?> - <div class="price-container"> -- <span class="price-label"><?= /* @escapeNotVerified */ __('Price') ?></span> -- <span class="price-wrapper"><?= /* @escapeNotVerified */ $this->helper('Magento\Checkout\Helper\Data')->formatPrice($_item->getCalculationPrice()) ?></span> -+ <span class="price-label"><?= $block->escapeHtml(__('Price')) ?></span> -+ <span class="price-wrapper"> -+ <?= $block->escapeHtml( -+ $this->helper(Magento\Checkout\Helper\Data::class)->formatPrice($_item->getCalculationPrice()), -+ ['span'] -+ ) ?> -+ </span> - </div> -diff --git a/app/code/Magento/Checkout/view/frontend/templates/cart/item/renderer/actions/edit.phtml b/app/code/Magento/Checkout/view/frontend/templates/cart/item/renderer/actions/edit.phtml -index 1876cf2edb7..357bbf27772 100644 ---- a/app/code/Magento/Checkout/view/frontend/templates/cart/item/renderer/actions/edit.phtml -+++ b/app/code/Magento/Checkout/view/frontend/templates/cart/item/renderer/actions/edit.phtml -@@ -4,16 +4,12 @@ - * See COPYING.txt for license details. - */ - --// @codingStandardsIgnoreFile -- - /** @var $block \Magento\Checkout\Block\Cart\Item\Renderer\Actions\Edit */ - ?> --<?php if ($block->isProductVisibleInSiteVisibility()): ?> -+<?php if ($block->isProductVisibleInSiteVisibility()) :?> - <a class="action action-edit" -- href="<?= /* @escapeNotVerified */ $block->getConfigureUrl() ?>" -- title="<?= $block->escapeHtml(__('Edit item parameters')) ?>"> -- <span> -- <?= /* @escapeNotVerified */ __('Edit') ?> -- </span> -- </a> -+ href="<?= $block->escapeUrl($block->getConfigureUrl()) ?>" -+ title="<?= $block->escapeHtmlAttr(__('Edit item parameters')) ?>"> -+ <span><?= $block->escapeHtml(__('Edit')) ?></span> -+ </a> - <?php endif ?> -diff --git a/app/code/Magento/Checkout/view/frontend/templates/cart/item/renderer/actions/remove.phtml b/app/code/Magento/Checkout/view/frontend/templates/cart/item/renderer/actions/remove.phtml -index 445721ca5d0..8b72b6c55e8 100644 ---- a/app/code/Magento/Checkout/view/frontend/templates/cart/item/renderer/actions/remove.phtml -+++ b/app/code/Magento/Checkout/view/frontend/templates/cart/item/renderer/actions/remove.phtml -@@ -4,15 +4,13 @@ - * See COPYING.txt for license details. - */ - --// @codingStandardsIgnoreFile -- - /** @var $block \Magento\Checkout\Block\Cart\Item\Renderer\Actions\Remove */ - ?> - <a href="#" - title="<?= $block->escapeHtml(__('Remove item')) ?>" - class="action action-delete" -- data-post='<?= /* @escapeNotVerified */ $block->getDeletePostJson() ?>'> -+ data-post='<?= /* @noEscape */ $block->getDeletePostJson() ?>'> - <span> -- <?= /* @escapeNotVerified */ __('Remove item') ?> -+ <?= $block->escapeHtml(__('Remove item')) ?> - </span> - </a> -diff --git a/app/code/Magento/Checkout/view/frontend/templates/cart/methods.phtml b/app/code/Magento/Checkout/view/frontend/templates/cart/methods.phtml -index d329e2e8c17..b045f4ec98f 100644 ---- a/app/code/Magento/Checkout/view/frontend/templates/cart/methods.phtml -+++ b/app/code/Magento/Checkout/view/frontend/templates/cart/methods.phtml -@@ -4,20 +4,18 @@ - * See COPYING.txt for license details. - */ - --// @codingStandardsIgnoreFile -- - ?> - <?php - /** @var $block \Magento\Checkout\Block\Cart */ - ?> --<?php if (!$block->hasError()): ?> --<?php $methods = $block->getMethods('methods') ?: $block->getMethods('top_methods') ?> --<ul class="checkout methods items checkout-methods-items"> --<?php foreach ($methods as $method): ?> -- <?php $methodHtml = $block->getMethodHtml($method); ?> -- <?php if (trim($methodHtml) !== ''): ?> -- <li class="item"><?= /* @escapeNotVerified */ $methodHtml ?></li> -- <?php endif; ?> --<?php endforeach; ?> --</ul> -+<?php if (!$block->hasError()) :?> -+ <?php $methods = $block->getMethods('methods') ?: $block->getMethods('top_methods') ?> -+ <ul class="checkout methods items checkout-methods-items"> -+ <?php foreach ($methods as $method) :?> -+ <?php $methodHtml = $block->getMethodHtml($method); ?> -+ <?php if (trim($methodHtml) !== '') :?> -+ <li class="item"><?= /* @noEscape */ $methodHtml ?></li> -+ <?php endif; ?> -+ <?php endforeach; ?> -+ </ul> - <?php endif; ?> -diff --git a/app/code/Magento/Checkout/view/frontend/templates/cart/minicart.phtml b/app/code/Magento/Checkout/view/frontend/templates/cart/minicart.phtml -index e6d0260cf23..28275e02239 100644 ---- a/app/code/Magento/Checkout/view/frontend/templates/cart/minicart.phtml -+++ b/app/code/Magento/Checkout/view/frontend/templates/cart/minicart.phtml -@@ -4,17 +4,15 @@ - * See COPYING.txt for license details. - */ - --// @codingStandardsIgnoreFile -- - /** @var $block \Magento\Checkout\Block\Cart\Sidebar */ - ?> - - <div data-block="minicart" class="minicart-wrapper"> -- <a class="action showcart" href="<?= /* @escapeNotVerified */ $block->getShoppingCartUrl() ?>" -+ <a class="action showcart" href="<?= $block->escapeUrl($block->getShoppingCartUrl()) ?>" - data-bind="scope: 'minicart_content'"> -- <span class="text"><?= /* @escapeNotVerified */ __('My Cart') ?></span> -+ <span class="text"><?= $block->escapeHtml(__('My Cart')) ?></span> - <span class="counter qty empty" -- data-bind="css: { empty: !!getCartParam('summary_count') == false }, blockLoader: isLoading"> -+ data-bind="css: { empty: !!getCartParam('summary_count') == false && !isLoading() }, blockLoader: isLoading"> - <span class="counter-number"><!-- ko text: getCartParam('summary_count') --><!-- /ko --></span> - <span class="counter-label"> - <!-- ko if: getCartParam('summary_count') --> -@@ -24,7 +22,7 @@ - </span> - </span> - </a> -- <?php if ($block->getIsNeedToDisplaySideBar()): ?> -+ <?php if ($block->getIsNeedToDisplaySideBar()) :?> - <div class="block block-minicart" - data-role="dropdownDialog" - data-mage-init='{"dropdownDialog":{ -@@ -41,17 +39,27 @@ - </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() ?>; -+ window.checkout = <?= /* @noEscape */ $block->getSerializedConfig() ?>; - </script> - <script type="text/x-magento-init"> - { - "[data-block='minicart']": { -- "Magento_Ui/js/core/app": <?= /* @escapeNotVerified */ $block->getJsLayout() ?> -+ "Magento_Ui/js/core/app": <?= /* @noEscape */ $block->getJsLayout() ?> - }, - "*": { -- "Magento_Ui/js/block-loader": "<?= /* @escapeNotVerified */ $block->getViewFileUrl('images/loader-1.gif') ?>" -+ "Magento_Ui/js/block-loader": "<?= $block->escapeJs( -+ $block->escapeUrl($block->getViewFileUrl('images/loader-1.gif')) -+ ) ?>" - } - } - </script> -diff --git a/app/code/Magento/Checkout/view/frontend/templates/cart/noItems.phtml b/app/code/Magento/Checkout/view/frontend/templates/cart/noItems.phtml -index 1c0c221a550..ac150b2aa98 100644 ---- a/app/code/Magento/Checkout/view/frontend/templates/cart/noItems.phtml -+++ b/app/code/Magento/Checkout/view/frontend/templates/cart/noItems.phtml -@@ -3,13 +3,26 @@ - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ --// @codingStandardsIgnoreFile -+ - /** @var $block \Magento\Checkout\Block\Cart */ - ?> - <div class="cart-empty"> - <?= $block->getChildHtml('checkout_cart_empty_widget') ?> -- <p><?= /* @escapeNotVerified */ __('You have no items in your shopping cart.') ?></p> -- <p><?php /* @escapeNotVerified */ echo __('Click <a href="%1">here</a> to continue shopping.', -- $block->escapeUrl($block->getContinueShoppingUrl())) ?></p> -+ <p><?= $block->escapeHtml(__('You have no items in your shopping cart.')) ?></p> -+ <p><?= $block->escapeHtml( -+ __( -+ 'Click <a href="%1">here</a> to continue shopping.', -+ $block->escapeUrl($block->getContinueShoppingUrl()) -+ ), -+ ['a'] -+ ) ?> -+ </p> - <?= $block->getChildHtml('shopping.cart.table.after') ?> - </div> -+<script type="text/x-magento-init"> -+{ -+ "*": { -+ "Magento_Checkout/js/empty-cart": {} -+ } -+} -+</script> -diff --git a/app/code/Magento/Checkout/view/frontend/templates/cart/shipping.phtml b/app/code/Magento/Checkout/view/frontend/templates/cart/shipping.phtml -index b5ddb8446ba..a44d37dccfd 100644 ---- a/app/code/Magento/Checkout/view/frontend/templates/cart/shipping.phtml -+++ b/app/code/Magento/Checkout/view/frontend/templates/cart/shipping.phtml -@@ -4,36 +4,47 @@ - * See COPYING.txt for license details. - */ - --// @codingStandardsIgnoreFile -- - ?> - <?php /** @var $block \Magento\Checkout\Block\Cart\Shipping */ ?> - --<div id="block-shipping" class="block shipping" data-mage-init='{"collapsible":{"openedState": "active", "saveState": true}}'> -+<div id="block-shipping" -+ class="block shipping" -+ data-mage-init='{"collapsible":{"openedState": "active", "saveState": true}}' -+> - <div class="title" data-role="title"> - <strong id="block-shipping-heading" role="heading" aria-level="2"> -- <?= /* @escapeNotVerified */ $block->getQuote()->isVirtual() ? __('Estimate Tax') : __('Estimate Shipping and Tax') ?> -+ <?= $block->getQuote()->isVirtual() -+ ? $block->escapeHtml(__('Estimate Tax')) -+ : $block->escapeHtml(__('Estimate Shipping and Tax')) -+ ?> - </strong> - </div> -- <div id="block-summary" data-bind="scope:'block-summary'" class="content" data-role="content" aria-labelledby="block-shipping-heading"> -+ <div id="block-summary" -+ data-bind="scope:'block-summary'" -+ class="content" -+ data-role="content" -+ aria-labelledby="block-shipping-heading" -+ > - <!-- ko template: getTemplate() --><!-- /ko --> - <script type="text/x-magento-init"> - { - "#block-summary": { -- "Magento_Ui/js/core/app": <?= /* @escapeNotVerified */ $block->getJsLayout() ?> -+ "Magento_Ui/js/core/app": <?= /* @noEscape */ $block->getJsLayout() ?> - } - } - </script> - <script> -- window.checkoutConfig = <?= /* @escapeNotVerified */ $block->getSerializedCheckoutConfig() ?>; -+ window.checkoutConfig = <?= /* @noEscape */ $block->getSerializedCheckoutConfig() ?>; - window.customerData = window.checkoutConfig.customerData; - window.isCustomerLoggedIn = window.checkoutConfig.isCustomerLoggedIn; - require([ - 'mage/url', - 'Magento_Ui/js/block-loader' - ], function(url, blockLoader) { -- blockLoader("<?= /* @escapeNotVerified */ $block->getViewFileUrl('images/loader-1.gif') ?>"); -- return url.setBaseUrl('<?= /* @escapeNotVerified */ $block->getBaseUrl() ?>'); -+ blockLoader( -+ "<?= $block->escapeJs($block->escapeUrl($block->getViewFileUrl('images/loader-1.gif'))) ?>" -+ ); -+ return url.setBaseUrl('<?= $block->escapeJs($block->escapeUrl($block->getBaseUrl())) ?>'); - }) - </script> - </div> -diff --git a/app/code/Magento/Checkout/view/frontend/templates/cart/totals.phtml b/app/code/Magento/Checkout/view/frontend/templates/cart/totals.phtml -index f39b70df984..784c4c39076 100644 ---- a/app/code/Magento/Checkout/view/frontend/templates/cart/totals.phtml -+++ b/app/code/Magento/Checkout/view/frontend/templates/cart/totals.phtml -@@ -15,7 +15,7 @@ - <script type="text/x-magento-init"> - { - "#cart-totals": { -- "Magento_Ui/js/core/app": <?= /* @escapeNotVerified */ $block->getJsLayout() ?> -+ "Magento_Ui/js/core/app": <?= /* @noEscape */ $block->getJsLayout() ?> - } - } - </script> -diff --git a/app/code/Magento/Checkout/view/frontend/templates/item/price/row.phtml b/app/code/Magento/Checkout/view/frontend/templates/item/price/row.phtml -index 37a945b238d..533d75b6ae8 100644 ---- a/app/code/Magento/Checkout/view/frontend/templates/item/price/row.phtml -+++ b/app/code/Magento/Checkout/view/frontend/templates/item/price/row.phtml -@@ -4,7 +4,7 @@ - * See COPYING.txt for license details. - */ - --// @codingStandardsIgnoreFile -+// phpcs:disable Magento2.Templates.ThisInTemplate - - /** @var $block \Magento\Checkout\Block\Item\Price\Renderer */ - -@@ -12,6 +12,9 @@ $_item = $block->getItem(); - ?> - <span class="price-excluding-tax" data-label="<?= $block->escapeHtml(__('Excl. Tax')) ?>"> - <span class="cart-price"> -- <?= /* @escapeNotVerified */ $this->helper('Magento\Checkout\Helper\Data')->formatPrice($_item->getRowTotal()) ?> -+ <?= $block->escapeHtml( -+ $this->helper(Magento\Checkout\Helper\Data::class)->formatPrice($_item->getRowTotal()), -+ ['span'] -+ ) ?> - </span> - </span> -diff --git a/app/code/Magento/Checkout/view/frontend/templates/item/price/unit.phtml b/app/code/Magento/Checkout/view/frontend/templates/item/price/unit.phtml -index 45a6ef48e36..fafbe9c7c96 100644 ---- a/app/code/Magento/Checkout/view/frontend/templates/item/price/unit.phtml -+++ b/app/code/Magento/Checkout/view/frontend/templates/item/price/unit.phtml -@@ -4,7 +4,7 @@ - * See COPYING.txt for license details. - */ - --// @codingStandardsIgnoreFile -+// phpcs:disable Magento2.Templates.ThisInTemplate - - /** @var $block \Magento\Checkout\Block\Item\Price\Renderer */ - -@@ -12,6 +12,9 @@ $_item = $block->getItem(); - ?> - <span class="price-including-tax" data-label="<?= $block->escapeHtml(__('Excl. Tax')) ?>"> - <span class="cart-price"> -- <?= /* @escapeNotVerified */ $this->helper('Magento\Checkout\Helper\Data')->formatPrice($_item->getCalculationPrice()) ?> -+ <?= $block->escapeHtml( -+ $this->helper(Magento\Checkout\Helper\Data::class)->formatPrice($_item->getCalculationPrice()), -+ ['span'] -+ ) ?> - </span> - </span> -diff --git a/app/code/Magento/Checkout/view/frontend/templates/js/components.phtml b/app/code/Magento/Checkout/view/frontend/templates/js/components.phtml -index bad5acc209b..6cf15f47701 100644 ---- a/app/code/Magento/Checkout/view/frontend/templates/js/components.phtml -+++ b/app/code/Magento/Checkout/view/frontend/templates/js/components.phtml -@@ -4,7 +4,5 @@ - * See COPYING.txt for license details. - */ - --// @codingStandardsIgnoreFile -- - ?> - <?= $block->getChildHtml() ?> -diff --git a/app/code/Magento/Checkout/view/frontend/templates/messages/addCartSuccessMessage.phtml b/app/code/Magento/Checkout/view/frontend/templates/messages/addCartSuccessMessage.phtml -new file mode 100644 -index 00000000000..a6686444d2e ---- /dev/null -+++ b/app/code/Magento/Checkout/view/frontend/templates/messages/addCartSuccessMessage.phtml -@@ -0,0 +1,14 @@ -+<?php -+/** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+ -+/** @var \Magento\Framework\View\Element\Template $block */ -+?> -+ -+<?= $block->escapeHtml(__( -+ 'You added %1 to your <a href="%2">shopping cart</a>.', -+ $block->getData('product_name'), -+ $block->getData('cart_url') -+), ['a']); -diff --git a/app/code/Magento/Checkout/view/frontend/templates/onepage.phtml b/app/code/Magento/Checkout/view/frontend/templates/onepage.phtml -index 47a56e8f333..55f7039f333 100644 ---- a/app/code/Magento/Checkout/view/frontend/templates/onepage.phtml -+++ b/app/code/Magento/Checkout/view/frontend/templates/onepage.phtml -@@ -4,13 +4,13 @@ - * See COPYING.txt for license details. - */ - --// @codingStandardsIgnoreFile -+/** @var $block \Magento\Checkout\Block\Onepage */ - ?> - <div id="checkout" data-bind="scope:'checkout'" class="checkout-container"> - <div id="checkout-loader" data-role="checkout-loader" class="loading-mask" data-mage-init='{"checkoutLoader": {}}'> - <div class="loader"> -- <img src="<?= /* @escapeNotVerified */ $block->getViewFileUrl('images/loader-1.gif') ?>" -- alt="<?= /* @escapeNotVerified */ __('Loading...') ?>" -+ <img src="<?= $block->escapeUrl($block->getViewFileUrl('images/loader-1.gif')) ?>" -+ alt="<?= $block->escapeHtmlAttr(__('Loading...')) ?>" - style="position: absolute;"> - </div> - </div> -@@ -18,12 +18,12 @@ - <script type="text/x-magento-init"> - { - "#checkout": { -- "Magento_Ui/js/core/app": <?= /* @escapeNotVerified */ $block->getJsLayout() ?> -+ "Magento_Ui/js/core/app": <?= /* @noEscape */ $block->getJsLayout() ?> - } - } - </script> - <script> -- window.checkoutConfig = <?= /* @escapeNotVerified */ $block->getSerializedCheckoutConfig() ?>; -+ window.checkoutConfig = <?= /* @noEscape */ $block->getSerializedCheckoutConfig() ?>; - // Create aliases for customer.js model from customer module - window.isCustomerLoggedIn = window.checkoutConfig.isCustomerLoggedIn; - window.customerData = window.checkoutConfig.customerData; -@@ -33,8 +33,8 @@ - 'mage/url', - 'Magento_Ui/js/block-loader' - ], function(url, blockLoader) { -- blockLoader("<?= /* @escapeNotVerified */ $block->getViewFileUrl('images/loader-1.gif') ?>"); -- return url.setBaseUrl('<?= /* @escapeNotVerified */ $block->getBaseUrl() ?>'); -+ blockLoader("<?= $block->escapeJs($block->escapeUrl($block->getViewFileUrl('images/loader-1.gif'))) ?>"); -+ return url.setBaseUrl('<?= $block->escapeJs($block->escapeUrl($block->getBaseUrl())) ?>'); - }) - </script> - </div> -diff --git a/app/code/Magento/Checkout/view/frontend/templates/onepage/failure.phtml b/app/code/Magento/Checkout/view/frontend/templates/onepage/failure.phtml -index 43791ef4967..ace2b046417 100644 ---- a/app/code/Magento/Checkout/view/frontend/templates/onepage/failure.phtml -+++ b/app/code/Magento/Checkout/view/frontend/templates/onepage/failure.phtml -@@ -4,9 +4,16 @@ - * See COPYING.txt for license details. - */ - --// @codingStandardsIgnoreFile -- -+/** @var $block \Magento\Checkout\Block\Onepage\Failure */ - ?> --<?php if ($block->getRealOrderId()) : ?><p><?= /* @escapeNotVerified */ __('Order #') . $block->getRealOrderId() ?></p><?php endif ?> --<?php if ($error = $block->getErrorMessage()) : ?><p><?= /* @escapeNotVerified */ $error ?></p><?php endif ?> --<p><?= /* @escapeNotVerified */ __('Click <a href="%1">here</a> to continue shopping.', $block->escapeUrl($block->getContinueShoppingUrl())) ?></p> -+<?php if ($block->getRealOrderId()) :?> -+ <p><?= $block->escapeHtml(__('Order #') . $block->getRealOrderId()) ?></p> -+<?php endif ?> -+<?php if ($error = $block->getErrorMessage()) :?> -+ <p><?= $block->escapeHtml($error) ?></p> -+<?php endif ?> -+<p><?= $block->escapeHtml( -+ __('Click <a href="%1">here</a> to continue shopping.', $block->escapeUrl($block->getContinueShoppingUrl())), -+ ['a'] -+) ?> -+</p> -diff --git a/app/code/Magento/Checkout/view/frontend/templates/onepage/link.phtml b/app/code/Magento/Checkout/view/frontend/templates/onepage/link.phtml -index 53a1fe87835..b667764ac7b 100644 ---- a/app/code/Magento/Checkout/view/frontend/templates/onepage/link.phtml -+++ b/app/code/Magento/Checkout/view/frontend/templates/onepage/link.phtml -@@ -4,15 +4,21 @@ - * See COPYING.txt for license details. - */ - --// @codingStandardsIgnoreFile -+/** @var $block \Magento\Checkout\Block\Onepage\Link */ - ?> --<?php if ($block->isPossibleOnepageCheckout()):?> -+<?php if ($block->isPossibleOnepageCheckout()) :?> - <button type="button" - data-role="proceed-to-checkout" -- title="<?= /* @escapeNotVerified */ __('Proceed to Checkout') ?>" -- data-mage-init='{"Magento_Checkout/js/proceed-to-checkout":{"checkoutUrl":"<?= /* @escapeNotVerified */ $block->getCheckoutUrl() ?>"}}' -+ title="<?= $block->escapeHtmlAttr(__('Proceed to Checkout')) ?>" -+ data-mage-init='{ -+ "Magento_Checkout/js/proceed-to-checkout":{ -+ "checkoutUrl":"<?= $block->escapeJs($block->escapeUrl($block->getCheckoutUrl())) ?>" -+ } -+ }' - class="action primary checkout<?= ($block->isDisabled()) ? ' disabled' : '' ?>" -- <?php if ($block->isDisabled()):?>disabled="disabled"<?php endif; ?>> -- <span><?= /* @escapeNotVerified */ __('Proceed to Checkout') ?></span> -+ <?php if ($block->isDisabled()) :?> -+ disabled="disabled" -+ <?php endif; ?>> -+ <span><?= $block->escapeHtml(__('Proceed to Checkout')) ?></span> - </button> - <?php endif?> -diff --git a/app/code/Magento/Checkout/view/frontend/templates/onepage/review/item.phtml b/app/code/Magento/Checkout/view/frontend/templates/onepage/review/item.phtml -index 2428cc01077..2a7ccc38e9d 100644 ---- a/app/code/Magento/Checkout/view/frontend/templates/onepage/review/item.phtml -+++ b/app/code/Magento/Checkout/view/frontend/templates/onepage/review/item.phtml -@@ -4,11 +4,12 @@ - * See COPYING.txt for license details. - */ - --// @codingStandardsIgnoreFile -+// phpcs:disable Magento2.Templates.ThisInTemplate - - /** @var $block Magento\Checkout\Block\Cart\Item\Renderer */ - - $_item = $block->getItem(); -+$taxDataHelper = $this->helper(Magento\Tax\Helper\Data::class); - ?> - <tbody class="cart item"> - <tr> -@@ -17,47 +18,53 @@ $_item = $block->getItem(); - <?= $block->getImage($block->getProductForThumbnail(), 'cart_page_product_thumbnail')->toHtml() ?> - </span> - <div class="product-item-details"> -- <strong class="product name product-item-name"><?= $block->escapeHtml($block->getProductName()) ?></strong> -- <?php if ($_options = $block->getOptionList()):?> -- <dl class="item-options"> -- <?php foreach ($_options as $_option) : ?> -- <?php $_formatedOptionValue = $block->getFormatedOptionValue($_option) ?> -- <dt><?= $block->escapeHtml($_option['label']) ?></dt> -- <dd> -- <?php if (isset($_formatedOptionValue['full_view'])): ?> -- <?= /* @escapeNotVerified */ $_formatedOptionValue['full_view'] ?> -- <?php else: ?> -- <?= /* @escapeNotVerified */ $_formatedOptionValue['value'] ?> -- <?php endif; ?> -- </dd> -- <?php endforeach; ?> -- </dl> -+ <strong class="product name product-item-name"> -+ <?= $block->escapeHtml($block->getProductName()) ?> -+ </strong> -+ <?php if ($_options = $block->getOptionList()) :?> -+ <dl class="item-options"> -+ <?php foreach ($_options as $_option) :?> -+ <?php $_formatedOptionValue = $block->getFormatedOptionValue($_option) ?> -+ <dt><?= $block->escapeHtml($_option['label']) ?></dt> -+ <dd> -+ <?php if (isset($_formatedOptionValue['full_view'])) :?> -+ <?= $block->escapeHtml($_formatedOptionValue['full_view']) ?> -+ <?php else :?> -+ <?= $block->escapeHtml($_formatedOptionValue['value']) ?> -+ <?php endif; ?> -+ </dd> -+ <?php endforeach; ?> -+ </dl> - <?php endif;?> -- <?php if ($addtInfoBlock = $block->getProductAdditionalInformationBlock()):?> -+ <?php if ($addtInfoBlock = $block->getProductAdditionalInformationBlock()) :?> - <?= $addtInfoBlock->setItem($_item)->toHtml() ?> - <?php endif;?> - </div> - </td> - <td class="col price" data-th="<?= $block->escapeHtml(__('Price')) ?>"> -- <?php if ($this->helper('Magento\Tax\Helper\Data')->displayCartPriceInclTax() || $this->helper('Magento\Tax\Helper\Data')->displayCartBothPrices()): ?> -+ <?php if ($taxDataHelper->displayCartPriceInclTax() || $taxDataHelper->displayCartBothPrices()) :?> - <span class="price-including-tax" data-label="<?= $block->escapeHtml(__('Incl. Tax')) ?>"> - <?= $block->getUnitPriceInclTaxHtml($_item) ?> - </span> - <?php endif; ?> -- <?php if ($this->helper('Magento\Tax\Helper\Data')->displayCartPriceExclTax() || $this->helper('Magento\Tax\Helper\Data')->displayCartBothPrices()): ?> -+ <?php if ($taxDataHelper->displayCartPriceExclTax() || $taxDataHelper->displayCartBothPrices()) :?> - <span class="price-excluding-tax" data-label="<?= $block->escapeHtml(__('Excl. Tax')) ?>"> - <?= $block->getUnitPriceExclTaxHtml($_item) ?> - </span> - <?php endif; ?> - </td> -- <td class="col qty" data-th="<?= $block->escapeHtml(__('Qty')) ?>"><span class="qty"><?= /* @escapeNotVerified */ $_item->getQty() ?></span></td> -+ <td class="col qty" -+ data-th="<?= $block->escapeHtml(__('Qty')) ?>" -+ > -+ <span class="qty"><?= $block->escapeHtml($_item->getQty()) ?></span> -+ </td> - <td class="col subtotal" data-th="<?= $block->escapeHtml(__('Subtotal')) ?>"> -- <?php if ($this->helper('Magento\Tax\Helper\Data')->displayCartPriceInclTax() || $this->helper('Magento\Tax\Helper\Data')->displayCartBothPrices()): ?> -+ <?php if ($taxDataHelper->displayCartPriceInclTax() || $taxDataHelper->displayCartBothPrices()) :?> - <span class="price-including-tax" data-label="<?= $block->escapeHtml(__('Incl. Tax')) ?>"> - <?= $block->getRowTotalInclTaxHtml($_item) ?> - </span> - <?php endif; ?> -- <?php if ($this->helper('Magento\Tax\Helper\Data')->displayCartPriceExclTax() || $this->helper('Magento\Tax\Helper\Data')->displayCartBothPrices()): ?> -+ <?php if ($taxDataHelper->displayCartPriceExclTax() || $taxDataHelper->displayCartBothPrices()) :?> - <span class="price-excluding-tax" data-label="<?= $block->escapeHtml(__('Excl. Tax')) ?>"> - <?= $block->getRowTotalExclTaxHtml($_item) ?> - </span> -diff --git a/app/code/Magento/Checkout/view/frontend/templates/onepage/review/item/price/row_excl_tax.phtml b/app/code/Magento/Checkout/view/frontend/templates/onepage/review/item/price/row_excl_tax.phtml -index 7ee3e416b9a..64eefd0dad0 100644 ---- a/app/code/Magento/Checkout/view/frontend/templates/onepage/review/item/price/row_excl_tax.phtml -+++ b/app/code/Magento/Checkout/view/frontend/templates/onepage/review/item/price/row_excl_tax.phtml -@@ -4,12 +4,15 @@ - * See COPYING.txt for license details. - */ - --// @codingStandardsIgnoreFile -+// phpcs:disable Magento2.Templates.ThisInTemplate - - /** @var $block \Magento\Checkout\Block\Item\Price\Renderer */ - - $_item = $block->getItem(); - ?> - <span class="cart-price"> -- <?= /* @escapeNotVerified */ $this->helper('Magento\Checkout\Helper\Data')->formatPrice($_item->getRowTotal()) ?> -+ <?= $block->escapeHtml( -+ $this->helper(Magento\Checkout\Helper\Data::class)->formatPrice($_item->getRowTotal()), -+ ['span'] -+ ) ?> - </span> -diff --git a/app/code/Magento/Checkout/view/frontend/templates/onepage/review/item/price/row_incl_tax.phtml b/app/code/Magento/Checkout/view/frontend/templates/onepage/review/item/price/row_incl_tax.phtml -index 2f364aafbbc..14deb07640e 100644 ---- a/app/code/Magento/Checkout/view/frontend/templates/onepage/review/item/price/row_incl_tax.phtml -+++ b/app/code/Magento/Checkout/view/frontend/templates/onepage/review/item/price/row_incl_tax.phtml -@@ -4,13 +4,16 @@ - * See COPYING.txt for license details. - */ - --// @codingStandardsIgnoreFile -+// phpcs:disable Magento2.Templates.ThisInTemplate - - /** @var $block \Magento\Checkout\Block\Item\Price\Renderer */ - - $_item = $block->getItem(); - ?> --<?php $_incl = $this->helper('Magento\Checkout\Helper\Data')->getSubtotalInclTax($_item); ?> -+<?php $_incl = $this->helper(Magento\Checkout\Helper\Data::class)->getSubtotalInclTax($_item); ?> - <span class="cart-price"> -- <?= /* @escapeNotVerified */ $this->helper('Magento\Checkout\Helper\Data')->formatPrice($_incl) ?> -+ <?= $block->escapeHtml( -+ $this->helper(Magento\Checkout\Helper\Data::class)->formatPrice($_incl), -+ ['span'] -+ ) ?> - </span> -diff --git a/app/code/Magento/Checkout/view/frontend/templates/onepage/review/item/price/unit_excl_tax.phtml b/app/code/Magento/Checkout/view/frontend/templates/onepage/review/item/price/unit_excl_tax.phtml -index a1ec004c2a8..a65a5a50ae1 100644 ---- a/app/code/Magento/Checkout/view/frontend/templates/onepage/review/item/price/unit_excl_tax.phtml -+++ b/app/code/Magento/Checkout/view/frontend/templates/onepage/review/item/price/unit_excl_tax.phtml -@@ -4,12 +4,15 @@ - * See COPYING.txt for license details. - */ - --// @codingStandardsIgnoreFile -+// phpcs:disable Magento2.Templates.ThisInTemplate.FoundThis - - /** @var $block \Magento\Checkout\Block\Item\Price\Renderer */ - - $_item = $block->getItem(); - ?> - <span class="cart-price"> -- <?= /* @escapeNotVerified */ $this->helper('Magento\Checkout\Helper\Data')->formatPrice($_item->getCalculationPrice()) ?> -+ <?= $block->escapeHtml( -+ $this->helper(Magento\Checkout\Helper\Data::class)->formatPrice($_item->getCalculationPrice()), -+ ['span'] -+ ) ?> - </span> -diff --git a/app/code/Magento/Checkout/view/frontend/templates/onepage/review/item/price/unit_incl_tax.phtml b/app/code/Magento/Checkout/view/frontend/templates/onepage/review/item/price/unit_incl_tax.phtml -index 0ed3c05ee6d..b623e1f1c3f 100644 ---- a/app/code/Magento/Checkout/view/frontend/templates/onepage/review/item/price/unit_incl_tax.phtml -+++ b/app/code/Magento/Checkout/view/frontend/templates/onepage/review/item/price/unit_incl_tax.phtml -@@ -4,13 +4,16 @@ - * See COPYING.txt for license details. - */ - --// @codingStandardsIgnoreFile -+// phpcs:disable Magento2.Templates.ThisInTemplate - - /** @var $block \Magento\Checkout\Block\Item\Price\Renderer */ - - $_item = $block->getItem(); - ?> --<?php $_incl = $this->helper('Magento\Checkout\Helper\Data')->getPriceInclTax($_item); ?> -+<?php $_incl = $this->helper(Magento\Checkout\Helper\Data::class)->getPriceInclTax($_item); ?> - <span class="cart-price"> -- <?= /* @escapeNotVerified */ $this->helper('Magento\Checkout\Helper\Data')->formatPrice($_incl) ?> -+ <?= $block->escapeHtml( -+ $this->helper(Magento\Checkout\Helper\Data::class)->formatPrice($_incl), -+ ['span'] -+ ) ?> - </span> -diff --git a/app/code/Magento/Checkout/view/frontend/templates/registration.phtml b/app/code/Magento/Checkout/view/frontend/templates/registration.phtml -index f239fbd47de..da36b4b61d6 100644 ---- a/app/code/Magento/Checkout/view/frontend/templates/registration.phtml -+++ b/app/code/Magento/Checkout/view/frontend/templates/registration.phtml -@@ -4,7 +4,7 @@ - * See COPYING.txt for license details. - */ - --// @codingStandardsIgnoreFile -+/** @var $block \Magento\Checkout\Block\Registration */ - ?> - <div id="registration" data-bind="scope:'registration'"> - <br /> -@@ -17,8 +17,9 @@ - "registration": { - "component": "Magento_Checkout/js/view/registration", - "config": { -- "registrationUrl": "<?= /* @escapeNotVerified */ $block->getCreateAccountUrl() ?>", -- "email": "<?= /* @escapeNotVerified */ $block->getEmailAddress() ?>" -+ "registrationUrl": -+ "<?= $block->escapeJs($block->escapeUrl($block->getCreateAccountUrl())) ?>", -+ "email": "<?= $block->escapeJs($block->getEmailAddress()) ?>" - }, - "children": { - "errors": { -diff --git a/app/code/Magento/Checkout/view/frontend/templates/shipping/price.phtml b/app/code/Magento/Checkout/view/frontend/templates/shipping/price.phtml -index 892b7926525..ba1a7a20376 100644 ---- a/app/code/Magento/Checkout/view/frontend/templates/shipping/price.phtml -+++ b/app/code/Magento/Checkout/view/frontend/templates/shipping/price.phtml -@@ -4,10 +4,8 @@ - * See COPYING.txt for license details. - */ - --// @codingStandardsIgnoreFile -- - ?> - <?php /** @var $block \Magento\Checkout\Block\Shipping\Price */ ?> - - <?php $shippingPrice = $block->getShippingPrice(); ?> --<?= /* @escapeNotVerified */ $shippingPrice ?> -+<?= $block->escapeHtml($shippingPrice) ?> -diff --git a/app/code/Magento/Checkout/view/frontend/templates/success.phtml b/app/code/Magento/Checkout/view/frontend/templates/success.phtml -index b3517eab8a5..828b4eb86c3 100644 ---- a/app/code/Magento/Checkout/view/frontend/templates/success.phtml -+++ b/app/code/Magento/Checkout/view/frontend/templates/success.phtml -@@ -4,25 +4,23 @@ - * See COPYING.txt for license details. - */ - --// @codingStandardsIgnoreFile -- - ?> - <?php /** @var $block \Magento\Checkout\Block\Onepage\Success */ ?> - <div class="checkout-success"> -- <?php if ($block->getOrderId()):?> -+ <?php if ($block->getOrderId()) :?> - <?php if ($block->getCanViewOrder()) :?> -- <p><?= __('Your order number is: %1.', sprintf('<a href="%s" class="order-number"><strong>%s</strong></a>', $block->escapeHtml($block->getViewOrderUrl()), $block->escapeHtml($block->getOrderId()))) ?></p> -+ <p><?= $block->escapeHtml(__('Your order number is: %1.', sprintf('<a href="%s" class="order-number"><strong>%s</strong></a>', $block->escapeUrl($block->getViewOrderUrl()), $block->getOrderId())), ['a', 'strong']) ?></p> - <?php else :?> -- <p><?= __('Your order # is: <span>%1</span>.', $block->escapeHtml($block->getOrderId())) ?></p> -+ <p><?= $block->escapeHtml(__('Your order # is: <span>%1</span>.', $block->getOrderId()), ['span']) ?></p> - <?php endif;?> -- <p><?= /* @escapeNotVerified */ __('We\'ll email you an order confirmation with details and tracking info.') ?></p> -+ <p><?= $block->escapeHtml(__('We\'ll email you an order confirmation with details and tracking info.')) ?></p> - <?php endif;?> - - <?= $block->getAdditionalInfoHtml() ?> - - <div class="actions-toolbar"> - <div class="primary"> -- <a class="action primary continue" href="<?= /* @escapeNotVerified */ $block->getContinueUrl() ?>"><span><?= /* @escapeNotVerified */ __('Continue Shopping') ?></span></a> -+ <a class="action primary continue" href="<?= $block->escapeUrl($block->getContinueUrl()) ?>"><span><?= $block->escapeHtml(__('Continue Shopping')) ?></span></a> - </div> - </div> - </div> -diff --git a/app/code/Magento/Checkout/view/frontend/templates/total/default.phtml b/app/code/Magento/Checkout/view/frontend/templates/total/default.phtml -index 2ea1cdd7f53..0d9da171c11 100644 ---- a/app/code/Magento/Checkout/view/frontend/templates/total/default.phtml -+++ b/app/code/Magento/Checkout/view/frontend/templates/total/default.phtml -@@ -4,18 +4,40 @@ - * See COPYING.txt for license details. - */ - --// @codingStandardsIgnoreFile -+// phpcs:disable Magento2.Templates.ThisInTemplate - -+/** @var $block \Magento\Checkout\Block\Total\DefaultTotal */ - ?> - <tr class="totals"> -- <th colspan="<?= /* @escapeNotVerified */ $block->getColspan() ?>" style="<?= /* @escapeNotVerified */ $block->getTotal()->getStyle() ?>" class="mark" scope="row"> -- <?php if ($block->getRenderingArea() == $block->getTotal()->getArea()): ?><strong><?php endif; ?> -- <?= $block->escapeHtml($block->getTotal()->getTitle()) ?> -- <?php if ($block->getRenderingArea() == $block->getTotal()->getArea()): ?></strong><?php endif; ?> -+ <th -+ colspan="<?= $block->escapeHtmlAttr($block->getColspan()) ?>" -+ style="<?= $block->escapeHtmlAttr($block->getTotal()->getStyle()) ?>" -+ class="mark" scope="row" -+ > -+ <?php if ($block->getRenderingArea() == $block->getTotal()->getArea()) :?> -+ <strong> -+ <?php endif; ?> -+ <?= $block->escapeHtml($block->getTotal()->getTitle()) ?> -+ <?php if ($block->getRenderingArea() == $block->getTotal()->getArea()) :?> -+ </strong> -+ <?php endif; ?> - </th> -- <td style="<?= /* @escapeNotVerified */ $block->getTotal()->getStyle() ?>" class="amount" data-th="<?= $block->escapeHtml($block->getTotal()->getTitle()) ?>"> -- <?php if ($block->getRenderingArea() == $block->getTotal()->getArea()): ?><strong><?php endif; ?> -- <span><?= /* @escapeNotVerified */ $this->helper('Magento\Checkout\Helper\Data')->formatPrice($block->getTotal()->getValue()) ?></span> -- <?php if ($block->getRenderingArea() == $block->getTotal()->getArea()): ?></strong><?php endif; ?> -+ <td -+ style="<?= $block->escapeHtmlAttr($block->getTotal()->getStyle()) ?>" -+ class="amount" -+ data-th="<?= $block->escapeHtmlAttr($block->getTotal()->getTitle()) ?>" -+ > -+ <?php if ($block->getRenderingArea() == $block->getTotal()->getArea()) :?> -+ <strong> -+ <?php endif; ?> -+ <span> -+ <?= $block->escapeHtml( -+ $this->helper(Magento\Checkout\Helper\Data::class)->formatPrice($block->getTotal()->getValue()), -+ ['span'] -+ ) ?> -+ </span> -+ <?php if ($block->getRenderingArea() == $block->getTotal()->getArea()) :?> -+ </strong> -+ <?php endif; ?> - </td> - </tr> -diff --git a/app/code/Magento/Checkout/view/frontend/web/js/action/create-billing-address.js b/app/code/Magento/Checkout/view/frontend/web/js/action/create-billing-address.js -index 7db0dc5ce74..c601bb8acf1 100644 ---- a/app/code/Magento/Checkout/view/frontend/web/js/action/create-billing-address.js -+++ b/app/code/Magento/Checkout/view/frontend/web/js/action/create-billing-address.js -@@ -12,6 +12,17 @@ define([ - 'use strict'; - - return function (addressData) { -- return addressConverter.formAddressDataToQuoteAddress(addressData); -+ var address = addressConverter.formAddressDataToQuoteAddress(addressData); -+ -+ /** -+ * Returns new customer billing address type. -+ * -+ * @returns {String} -+ */ -+ address.getType = function () { -+ return 'new-customer-billing-address'; -+ }; -+ -+ return address; - }; - }); -diff --git a/app/code/Magento/Checkout/view/frontend/web/js/action/set-payment-information-extended.js b/app/code/Magento/Checkout/view/frontend/web/js/action/set-payment-information-extended.js -new file mode 100644 -index 00000000000..4085da82f41 ---- /dev/null -+++ b/app/code/Magento/Checkout/view/frontend/web/js/action/set-payment-information-extended.js -@@ -0,0 +1,60 @@ -+/** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+ -+/** -+ * @api -+ */ -+define([ -+ 'Magento_Checkout/js/model/quote', -+ 'Magento_Checkout/js/model/url-builder', -+ 'mage/storage', -+ 'Magento_Checkout/js/model/error-processor', -+ 'Magento_Customer/js/model/customer', -+ 'Magento_Checkout/js/action/get-totals', -+ 'Magento_Checkout/js/model/full-screen-loader' -+], function (quote, urlBuilder, storage, errorProcessor, customer, getTotalsAction, fullScreenLoader) { -+ 'use strict'; -+ -+ return function (messageContainer, paymentData, skipBilling) { -+ var serviceUrl, -+ payload; -+ -+ skipBilling = skipBilling || false; -+ payload = { -+ cartId: quote.getQuoteId(), -+ paymentMethod: paymentData -+ }; -+ -+ /** -+ * Checkout for guest and registered customer. -+ */ -+ if (!customer.isLoggedIn()) { -+ serviceUrl = urlBuilder.createUrl('/guest-carts/:cartId/set-payment-information', { -+ cartId: quote.getQuoteId() -+ }); -+ payload.email = quote.guestEmail; -+ } else { -+ serviceUrl = urlBuilder.createUrl('/carts/mine/set-payment-information', {}); -+ } -+ -+ if (skipBilling === false) { -+ payload.billingAddress = quote.billingAddress(); -+ } -+ -+ fullScreenLoader.startLoader(); -+ -+ return storage.post( -+ serviceUrl, JSON.stringify(payload) -+ ).fail( -+ function (response) { -+ errorProcessor.process(response, messageContainer); -+ } -+ ).always( -+ function () { -+ fullScreenLoader.stopLoader(); -+ } -+ ); -+ }; -+}); -diff --git a/app/code/Magento/Checkout/view/frontend/web/js/action/set-payment-information.js b/app/code/Magento/Checkout/view/frontend/web/js/action/set-payment-information.js -index 997b60503a2..d5261c976a7 100644 ---- a/app/code/Magento/Checkout/view/frontend/web/js/action/set-payment-information.js -+++ b/app/code/Magento/Checkout/view/frontend/web/js/action/set-payment-information.js -@@ -7,54 +7,13 @@ - * @api - */ - define([ -- 'Magento_Checkout/js/model/quote', -- 'Magento_Checkout/js/model/url-builder', -- 'mage/storage', -- 'Magento_Checkout/js/model/error-processor', -- 'Magento_Customer/js/model/customer', -- 'Magento_Checkout/js/action/get-totals', -- 'Magento_Checkout/js/model/full-screen-loader' --], function (quote, urlBuilder, storage, errorProcessor, customer, getTotalsAction, fullScreenLoader) { -+ 'Magento_Checkout/js/action/set-payment-information-extended' -+ -+], function (setPaymentInformationExtended) { - 'use strict'; - - return function (messageContainer, paymentData) { -- var serviceUrl, -- payload; -- -- /** -- * Checkout for guest and registered customer. -- */ -- if (!customer.isLoggedIn()) { -- serviceUrl = urlBuilder.createUrl('/guest-carts/:cartId/set-payment-information', { -- cartId: quote.getQuoteId() -- }); -- payload = { -- cartId: quote.getQuoteId(), -- email: quote.guestEmail, -- paymentMethod: paymentData, -- billingAddress: quote.billingAddress() -- }; -- } else { -- serviceUrl = urlBuilder.createUrl('/carts/mine/set-payment-information', {}); -- payload = { -- cartId: quote.getQuoteId(), -- paymentMethod: paymentData, -- billingAddress: quote.billingAddress() -- }; -- } -- -- fullScreenLoader.startLoader(); - -- return storage.post( -- serviceUrl, JSON.stringify(payload) -- ).fail( -- function (response) { -- errorProcessor.process(response, messageContainer); -- } -- ).always( -- function () { -- fullScreenLoader.stopLoader(); -- } -- ); -+ return setPaymentInformationExtended(messageContainer, paymentData, false); - }; - }); -diff --git a/app/code/Magento/Checkout/view/frontend/web/js/action/update-shopping-cart.js b/app/code/Magento/Checkout/view/frontend/web/js/action/update-shopping-cart.js -index ce1527b3d72..1920bc4d7ac 100644 ---- a/app/code/Magento/Checkout/view/frontend/web/js/action/update-shopping-cart.js -+++ b/app/code/Magento/Checkout/view/frontend/web/js/action/update-shopping-cart.js -@@ -14,7 +14,8 @@ define([ - $.widget('mage.updateShoppingCart', { - options: { - validationURL: '', -- eventName: 'updateCartItemQty' -+ eventName: 'updateCartItemQty', -+ updateCartActionContainer: '' - }, - - /** @inheritdoc */ -@@ -31,7 +32,9 @@ define([ - * @return {Boolean} - */ - onSubmit: function (event) { -- if (!this.options.validationURL) { -+ var action = this.element.find(this.options.updateCartActionContainer).val(); -+ -+ if (!this.options.validationURL || action === 'empty_cart') { - return true; - } - -diff --git a/app/code/Magento/Checkout/view/frontend/web/js/checkout-data.js b/app/code/Magento/Checkout/view/frontend/web/js/checkout-data.js -index 22b37b2da0b..1858ce946fb 100644 ---- a/app/code/Magento/Checkout/view/frontend/web/js/checkout-data.js -+++ b/app/code/Magento/Checkout/view/frontend/web/js/checkout-data.js -@@ -10,7 +10,8 @@ - */ - define([ - 'jquery', -- 'Magento_Customer/js/customer-data' -+ 'Magento_Customer/js/customer-data', -+ 'jquery/jquery-storageapi' - ], function ($, storage) { - 'use strict'; - -@@ -23,6 +24,22 @@ define([ - storage.set(cacheKey, data); - }, - -+ /** -+ * @return {*} -+ */ -+ initData = function () { -+ return { -+ 'selectedShippingAddress': null, //Selected shipping address pulled from persistence storage -+ 'shippingAddressFromData': null, //Shipping address pulled from persistence storage -+ 'newCustomerShippingAddress': null, //Shipping address pulled from persistence storage for customer -+ 'selectedShippingRate': null, //Shipping rate pulled from persistence storage -+ 'selectedPaymentMethod': null, //Payment method pulled from persistence storage -+ 'selectedBillingAddress': null, //Selected billing address pulled from persistence storage -+ 'billingAddressFromData': null, //Billing address pulled from persistence storage -+ 'newCustomerBillingAddress': null //Billing address pulled from persistence storage for new customer -+ }; -+ }, -+ - /** - * @return {*} - */ -@@ -30,17 +47,12 @@ define([ - var data = storage.get(cacheKey)(); - - if ($.isEmptyObject(data)) { -- data = { -- 'selectedShippingAddress': null, //Selected shipping address pulled from persistence storage -- 'shippingAddressFromData': null, //Shipping address pulled from persistence storage -- 'newCustomerShippingAddress': null, //Shipping address pulled from persistence storage for customer -- 'selectedShippingRate': null, //Shipping rate pulled from persistence storage -- 'selectedPaymentMethod': null, //Payment method pulled from persistence storage -- 'selectedBillingAddress': null, //Selected billing address pulled from persistence storage -- 'billingAddressFromData': null, //Billing address pulled from persistence storage -- 'newCustomerBillingAddress': null //Billing address pulled from persistence storage for new customer -- }; -- saveData(data); -+ data = $.initNamespaceStorage('mage-cache-storage').localStorage.get(cacheKey); -+ -+ if ($.isEmptyObject(data)) { -+ data = initData(); -+ saveData(data); -+ } - } - - return data; -diff --git a/app/code/Magento/Checkout/view/frontend/web/js/empty-cart.js b/app/code/Magento/Checkout/view/frontend/web/js/empty-cart.js -new file mode 100644 -index 00000000000..4b30ad80752 ---- /dev/null -+++ b/app/code/Magento/Checkout/view/frontend/web/js/empty-cart.js -@@ -0,0 +1,16 @@ -+/** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+ -+define([ -+ 'Magento_Customer/js/customer-data' -+], function (customerData) { -+ 'use strict'; -+ -+ var cartData = customerData.get('cart'); -+ -+ if (cartData().items && cartData().items.length !== 0) { -+ customerData.reload(['cart'], false); -+ } -+}); -diff --git a/app/code/Magento/Checkout/view/frontend/web/js/model/address-converter.js b/app/code/Magento/Checkout/view/frontend/web/js/model/address-converter.js -index a1aacf6e803..9b20a782c38 100644 ---- a/app/code/Magento/Checkout/view/frontend/web/js/model/address-converter.js -+++ b/app/code/Magento/Checkout/view/frontend/web/js/model/address-converter.js -@@ -58,6 +58,16 @@ define([ - } - delete addressData['region_id']; - -+ if (addressData['custom_attributes']) { -+ addressData['custom_attributes'] = Object.entries(addressData['custom_attributes']) -+ .map(function (customAttribute) { -+ return { -+ 'attribute_code': customAttribute[0], -+ 'value': customAttribute[1] -+ }; -+ }); -+ } -+ - return address(addressData); - }, - -diff --git a/app/code/Magento/Checkout/view/frontend/web/js/model/cart/estimate-service.js b/app/code/Magento/Checkout/view/frontend/web/js/model/cart/estimate-service.js -index 76e3d911e7d..54e49613197 100644 ---- a/app/code/Magento/Checkout/view/frontend/web/js/model/cart/estimate-service.js -+++ b/app/code/Magento/Checkout/view/frontend/web/js/model/cart/estimate-service.js -@@ -14,55 +14,71 @@ define([ - 'use strict'; - - var rateProcessors = [], -- totalsProcessors = []; -+ totalsProcessors = [], - -- quote.shippingAddress.subscribe(function () { -- var type = quote.shippingAddress().getType(); -+ /** -+ * Estimate totals for shipping address and update shipping rates. -+ */ -+ estimateTotalsAndUpdateRates = function () { -+ var type = quote.shippingAddress().getType(); - -- if ( -- quote.isVirtual() || -- window.checkoutConfig.activeCarriers && window.checkoutConfig.activeCarriers.length === 0 -- ) { -- // update totals block when estimated address was set -- totalsProcessors['default'] = totalsDefaultProvider; -- totalsProcessors[type] ? -- totalsProcessors[type].estimateTotals(quote.shippingAddress()) : -- totalsProcessors['default'].estimateTotals(quote.shippingAddress()); -- } else { -- // check if user data not changed -> load rates from cache -- if (!cartCache.isChanged('address', quote.shippingAddress()) && -- !cartCache.isChanged('cartVersion', customerData.get('cart')()['data_id']) && -- cartCache.get('rates') -+ if ( -+ quote.isVirtual() || -+ window.checkoutConfig.activeCarriers && window.checkoutConfig.activeCarriers.length === 0 - ) { -- shippingService.setShippingRates(cartCache.get('rates')); -+ // update totals block when estimated address was set -+ totalsProcessors['default'] = totalsDefaultProvider; -+ totalsProcessors[type] ? -+ totalsProcessors[type].estimateTotals(quote.shippingAddress()) : -+ totalsProcessors['default'].estimateTotals(quote.shippingAddress()); -+ } else { -+ // check if user data not changed -> load rates from cache -+ if (!cartCache.isChanged('address', quote.shippingAddress()) && -+ !cartCache.isChanged('cartVersion', customerData.get('cart')()['data_id']) && -+ cartCache.get('rates') -+ ) { -+ shippingService.setShippingRates(cartCache.get('rates')); - -- return; -+ return; -+ } -+ -+ // update rates list when estimated address was set -+ rateProcessors['default'] = defaultProcessor; -+ rateProcessors[type] ? -+ rateProcessors[type].getRates(quote.shippingAddress()) : -+ rateProcessors['default'].getRates(quote.shippingAddress()); -+ -+ // save rates to cache after load -+ shippingService.getShippingRates().subscribe(function (rates) { -+ cartCache.set('rates', rates); -+ }); - } -+ }, - -- // update rates list when estimated address was set -- rateProcessors['default'] = defaultProcessor; -- rateProcessors[type] ? -- rateProcessors[type].getRates(quote.shippingAddress()) : -- rateProcessors['default'].getRates(quote.shippingAddress()); -+ /** -+ * Estimate totals for shipping address. -+ */ -+ estimateTotalsShipping = function () { -+ totalsDefaultProvider.estimateTotals(quote.shippingAddress()); -+ }, - -- // save rates to cache after load -- shippingService.getShippingRates().subscribe(function (rates) { -- cartCache.set('rates', rates); -- }); -- } -- }); -- quote.shippingMethod.subscribe(function () { -- totalsDefaultProvider.estimateTotals(quote.shippingAddress()); -- }); -- quote.billingAddress.subscribe(function () { -- var type = quote.billingAddress().getType(); -+ /** -+ * Estimate totals for billing address. -+ */ -+ estimateTotalsBilling = function () { -+ var type = quote.billingAddress().getType(); -+ -+ if (quote.isVirtual()) { -+ // update totals block when estimated address was set -+ totalsProcessors['default'] = totalsDefaultProvider; -+ totalsProcessors[type] ? -+ totalsProcessors[type].estimateTotals(quote.billingAddress()) : -+ totalsProcessors['default'].estimateTotals(quote.billingAddress()); -+ } -+ }; - -- if (quote.isVirtual()) { -- // update totals block when estimated address was set -- totalsProcessors['default'] = totalsDefaultProvider; -- totalsProcessors[type] ? -- totalsProcessors[type].estimateTotals(quote.billingAddress()) : -- totalsProcessors['default'].estimateTotals(quote.billingAddress()); -- } -- }); -+ quote.shippingAddress.subscribe(estimateTotalsAndUpdateRates); -+ quote.shippingMethod.subscribe(estimateTotalsShipping); -+ quote.billingAddress.subscribe(estimateTotalsBilling); -+ customerData.get('cart').subscribe(estimateTotalsShipping); - }); -diff --git a/app/code/Magento/Checkout/view/frontend/web/js/model/checkout-data-resolver.js b/app/code/Magento/Checkout/view/frontend/web/js/model/checkout-data-resolver.js -index 73f4df56790..bc0ab59b622 100644 ---- a/app/code/Magento/Checkout/view/frontend/web/js/model/checkout-data-resolver.js -+++ b/app/code/Magento/Checkout/view/frontend/web/js/model/checkout-data-resolver.js -@@ -60,14 +60,21 @@ define([ - this.resolveBillingAddress(); - } - } -- - }, - - /** - * Resolve shipping address. Used local storage - */ - resolveShippingAddress: function () { -- var newCustomerShippingAddress = checkoutData.getNewCustomerShippingAddress(); -+ var newCustomerShippingAddress; -+ -+ if (!checkoutData.getShippingAddressFromData() && -+ window.checkoutConfig.shippingAddressFromData -+ ) { -+ checkoutData.setShippingAddressFromData(window.checkoutConfig.shippingAddressFromData); -+ } -+ -+ newCustomerShippingAddress = checkoutData.getNewCustomerShippingAddress(); - - if (newCustomerShippingAddress) { - createShippingAddress(newCustomerShippingAddress); -@@ -196,15 +203,24 @@ define([ - * Resolve billing address. Used local storage - */ - resolveBillingAddress: function () { -- var selectedBillingAddress = checkoutData.getSelectedBillingAddress(), -- newCustomerBillingAddressData = checkoutData.getNewCustomerBillingAddress(); -+ var selectedBillingAddress, -+ newCustomerBillingAddressData; -+ -+ if (!checkoutData.getBillingAddressFromData() && -+ window.checkoutConfig.billingAddressFromData -+ ) { -+ checkoutData.setBillingAddressFromData(window.checkoutConfig.billingAddressFromData); -+ } -+ -+ selectedBillingAddress = checkoutData.getSelectedBillingAddress(); -+ newCustomerBillingAddressData = checkoutData.getNewCustomerBillingAddress(); - - if (selectedBillingAddress) { -- if (selectedBillingAddress == 'new-customer-address' && newCustomerBillingAddressData) { //eslint-disable-line -+ if (selectedBillingAddress === 'new-customer-billing-address' && newCustomerBillingAddressData) { - selectBillingAddress(createBillingAddress(newCustomerBillingAddressData)); - } else { - addressList.some(function (address) { -- if (selectedBillingAddress == address.getKey()) { //eslint-disable-line eqeqeq -+ if (selectedBillingAddress === address.getKey()) { - selectBillingAddress(address); - } - }); -@@ -218,16 +234,31 @@ define([ - * Apply resolved billing address to quote - */ - applyBillingAddress: function () { -- var shippingAddress; -+ var shippingAddress, -+ isBillingAddressInitialized; - - if (quote.billingAddress()) { - selectBillingAddress(quote.billingAddress()); - - return; - } -+ -+ if (quote.isVirtual() || !quote.billingAddress()) { -+ isBillingAddressInitialized = addressList.some(function (addrs) { -+ if (addrs.isDefaultBilling()) { -+ selectBillingAddress(addrs); -+ -+ return true; -+ } -+ -+ return false; -+ }); -+ } -+ - shippingAddress = quote.shippingAddress(); - -- if (shippingAddress && -+ if (!isBillingAddressInitialized && -+ shippingAddress && - shippingAddress.canUseForBilling() && - (shippingAddress.isDefaultShipping() || !quote.isVirtual()) - ) { -diff --git a/app/code/Magento/Checkout/view/frontend/web/js/model/error-processor.js b/app/code/Magento/Checkout/view/frontend/web/js/model/error-processor.js -index 848a7daf71e..bf1697650e7 100644 ---- a/app/code/Magento/Checkout/view/frontend/web/js/model/error-processor.js -+++ b/app/code/Magento/Checkout/view/frontend/web/js/model/error-processor.js -@@ -8,8 +8,9 @@ - */ - define([ - 'mage/url', -- 'Magento_Ui/js/model/messageList' --], function (url, globalMessageList) { -+ 'Magento_Ui/js/model/messageList', -+ 'mage/translate' -+], function (url, globalMessageList, $t) { - 'use strict'; - - return { -@@ -25,7 +26,13 @@ define([ - if (response.status == 401) { //eslint-disable-line eqeqeq - window.location.replace(url.build('customer/account/login/')); - } else { -- error = JSON.parse(response.responseText); -+ try { -+ error = JSON.parse(response.responseText); -+ } catch (exception) { -+ error = { -+ message: $t('Something went wrong with your request. Please try again later.') -+ }; -+ } - messageContainer.addErrorMessage(error); - } - } -diff --git a/app/code/Magento/Checkout/view/frontend/web/js/model/new-customer-address.js b/app/code/Magento/Checkout/view/frontend/web/js/model/new-customer-address.js -index a880bd423ab..4ef39421440 100644 ---- a/app/code/Magento/Checkout/view/frontend/web/js/model/new-customer-address.js -+++ b/app/code/Magento/Checkout/view/frontend/web/js/model/new-customer-address.js -@@ -22,6 +22,8 @@ define([ - - if (addressData.region && addressData.region['region_id']) { - regionId = addressData.region['region_id']; -+ } else if (!addressData['region_id']) { -+ regionId = undefined; - } else if ( - /* eslint-disable */ - addressData['country_id'] && addressData['country_id'] == window.checkoutConfig.defaultCountryId || -diff --git a/app/code/Magento/Checkout/view/frontend/web/js/model/payment/additional-validators.js b/app/code/Magento/Checkout/view/frontend/web/js/model/payment/additional-validators.js -index 1cb35a4cee2..1337e1affd3 100644 ---- a/app/code/Magento/Checkout/view/frontend/web/js/model/payment/additional-validators.js -+++ b/app/code/Magento/Checkout/view/frontend/web/js/model/payment/additional-validators.js -@@ -35,15 +35,17 @@ define([], function () { - * - * @returns {Boolean} - */ -- validate: function () { -+ validate: function (hideError) { - var validationResult = true; - -+ hideError = hideError || false; -+ - if (validators.length <= 0) { - return validationResult; - } - - validators.forEach(function (item) { -- if (item.validate() == false) { //eslint-disable-line eqeqeq -+ if (item.validate(hideError) == false) { //eslint-disable-line eqeqeq - validationResult = false; - - return false; -diff --git a/app/code/Magento/Checkout/view/frontend/web/js/model/place-order.js b/app/code/Magento/Checkout/view/frontend/web/js/model/place-order.js -index c3c5b9d68ce..c07878fcaea 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 a95471d90da..0a5334a42c7 100644 ---- a/app/code/Magento/Checkout/view/frontend/web/js/model/postcode-validator.js -+++ b/app/code/Magento/Checkout/view/frontend/web/js/model/postcode-validator.js -@@ -14,11 +14,13 @@ define([ - /** - * @param {*} postCode - * @param {*} countryId -+ * @param {Array} postCodesPatterns - * @return {Boolean} - */ -- validate: function (postCode, countryId) { -- var patterns = window.checkoutConfig.postCodes[countryId], -- pattern, regex; -+ validate: function (postCode, countryId, postCodesPatterns) { -+ var pattern, regex, -+ patterns = postCodesPatterns ? postCodesPatterns[countryId] : -+ window.checkoutConfig.postCodes[countryId]; - - this.validatedPostCodeExample = []; - -diff --git a/app/code/Magento/Checkout/view/frontend/web/js/model/quote.js b/app/code/Magento/Checkout/view/frontend/web/js/model/quote.js -index 2510d1aced3..3486a927366 100644 ---- a/app/code/Magento/Checkout/view/frontend/web/js/model/quote.js -+++ b/app/code/Magento/Checkout/view/frontend/web/js/model/quote.js -@@ -7,7 +7,8 @@ - */ - define([ - 'ko', -- 'underscore' -+ 'underscore', -+ 'domReady!' - ], function (ko, _) { - 'use strict'; - -diff --git a/app/code/Magento/Checkout/view/frontend/web/js/model/shipping-rates-validator.js b/app/code/Magento/Checkout/view/frontend/web/js/model/shipping-rates-validator.js -index d31c0dca381..8b07c02e4d3 100644 ---- a/app/code/Magento/Checkout/view/frontend/web/js/model/shipping-rates-validator.js -+++ b/app/code/Magento/Checkout/view/frontend/web/js/model/shipping-rates-validator.js -@@ -35,13 +35,14 @@ define([ - var checkoutConfig = window.checkoutConfig, - validators = [], - observedElements = [], -- postcodeElement = null, -+ postcodeElements = [], - postcodeElementName = 'postcode'; - - validators.push(defaultValidator); - - return { - validateAddressTimeout: 0, -+ validateZipCodeTimeout: 0, - validateDelay: 2000, - - /** -@@ -101,7 +102,7 @@ define([ - - if (element.index === postcodeElementName) { - this.bindHandler(element, delay); -- postcodeElement = element; -+ postcodeElements.push(element); - } - }, - -@@ -133,10 +134,20 @@ define([ - }); - } else { - element.on('value', function () { -+ clearTimeout(self.validateZipCodeTimeout); -+ self.validateZipCodeTimeout = setTimeout(function () { -+ if (element.index === postcodeElementName) { -+ self.postcodeValidation(element); -+ } else { -+ $.each(postcodeElements, function (index, elem) { -+ self.postcodeValidation(elem); -+ }); -+ } -+ }, delay); -+ - if (!formPopUpState.isVisible()) { - clearTimeout(self.validateAddressTimeout); - self.validateAddressTimeout = setTimeout(function () { -- self.postcodeValidation(); - self.validateFields(); - }, delay); - } -@@ -148,8 +159,8 @@ define([ - /** - * @return {*} - */ -- postcodeValidation: function () { -- var countryId = $('select[name="country_id"]').val(), -+ postcodeValidation: function (postcodeElement) { -+ var countryId = $('select[name="country_id"]:visible').val(), - validationResult, - warnMessage; - -@@ -178,8 +189,8 @@ define([ - */ - validateFields: function () { - var addressFlat = addressConverter.formDataProviderToFlatData( -- this.collectObservedData(), -- 'shippingAddress' -+ this.collectObservedData(), -+ 'shippingAddress' - ), - address; - -diff --git a/app/code/Magento/Checkout/view/frontend/web/js/region-updater.js b/app/code/Magento/Checkout/view/frontend/web/js/region-updater.js -index cf2a59cdba4..80481826260 100644 ---- a/app/code/Magento/Checkout/view/frontend/web/js/region-updater.js -+++ b/app/code/Magento/Checkout/view/frontend/web/js/region-updater.js -@@ -157,7 +157,7 @@ define([ - regionInput = $(this.options.regionInputId), - postcode = $(this.options.postcodeId), - label = regionList.parent().siblings('label'), -- requiredLabel = regionList.parents('div.field'); -+ container = regionList.parents('div.field'); - - this._clearError(); - this._checkRegionRequired(country); -@@ -181,15 +181,16 @@ define([ - - if (this.options.isRegionRequired) { - regionList.addClass('required-entry').removeAttr('disabled'); -- requiredLabel.addClass('required'); -+ container.addClass('required').show(); - } else { - regionList.removeClass('required-entry validate-select').removeAttr('data-validate'); -- requiredLabel.removeClass('required'); -+ container.removeClass('required'); - - if (!this.options.optionalRegionAllowed) { //eslint-disable-line max-depth -- regionList.attr('disabled', 'disabled'); -+ regionList.hide(); -+ container.hide(); - } else { -- regionList.removeAttr('disabled'); -+ regionList.show(); - } - } - -@@ -201,12 +202,13 @@ define([ - - if (this.options.isRegionRequired) { - regionInput.addClass('required-entry').removeAttr('disabled'); -- requiredLabel.addClass('required'); -+ container.addClass('required').show(); - } else { - if (!this.options.optionalRegionAllowed) { //eslint-disable-line max-depth - regionInput.attr('disabled', 'disabled'); -+ container.hide(); - } -- requiredLabel.removeClass('required'); -+ container.removeClass('required'); - regionInput.removeClass('required-entry'); - } - -diff --git a/app/code/Magento/Checkout/view/frontend/web/js/shopping-cart.js b/app/code/Magento/Checkout/view/frontend/web/js/shopping-cart.js -index 3ea49cd981d..eecfa65b189 100644 ---- a/app/code/Magento/Checkout/view/frontend/web/js/shopping-cart.js -+++ b/app/code/Magento/Checkout/view/frontend/web/js/shopping-cart.js -@@ -14,7 +14,11 @@ define([ - _create: function () { - var items, i, reload; - -- $(this.options.emptyCartButton).on('click', $.proxy(function () { -+ $(this.options.emptyCartButton).on('click', $.proxy(function (event) { -+ if (event.detail === 0) { -+ return; -+ } -+ - $(this.options.emptyCartButton).attr('name', 'update_cart_action_temp'); - $(this.options.updateCartActionContainer) - .attr('name', 'update_cart_action').attr('value', 'empty_cart'); -diff --git a/app/code/Magento/Checkout/view/frontend/web/js/sidebar.js b/app/code/Magento/Checkout/view/frontend/web/js/sidebar.js -index 3b5168453e1..e66c6600624 100644 ---- a/app/code/Magento/Checkout/view/frontend/web/js/sidebar.js -+++ b/app/code/Magento/Checkout/view/frontend/web/js/sidebar.js -@@ -25,6 +25,7 @@ define([ - } - }, - scrollHeight: 0, -+ shoppingCartUrl: window.checkout.shoppingCartUrl, - - /** - * Create sidebar. -@@ -226,7 +227,11 @@ define([ - var productData = this._getProductById(Number(elem.data('cart-item'))); - - if (!_.isUndefined(productData)) { -- $(document).trigger('ajax:updateCartItemQty', productData['product_sku']); -+ $(document).trigger('ajax:updateCartItemQty'); -+ -+ if (window.location.href === this.shoppingCartUrl) { -+ window.location.reload(false); -+ } - } - this._hideItemButton(elem); - }, -@@ -253,7 +258,9 @@ define([ - var productData = this._getProductById(Number(elem.data('cart-item'))); - - if (!_.isUndefined(productData)) { -- $(document).trigger('ajax:removeFromCart', productData['product_sku']); -+ $(document).trigger('ajax:removeFromCart', { -+ productIds: [productData['product_id']] -+ }); - } - }, - -diff --git a/app/code/Magento/Checkout/view/frontend/web/js/view/billing-address.js b/app/code/Magento/Checkout/view/frontend/web/js/view/billing-address.js -index 6b5d08c2641..a552aa01da0 100644 ---- a/app/code/Magento/Checkout/view/frontend/web/js/view/billing-address.js -+++ b/app/code/Magento/Checkout/view/frontend/web/js/view/billing-address.js -@@ -17,7 +17,8 @@ define([ - 'Magento_Customer/js/customer-data', - 'Magento_Checkout/js/action/set-billing-address', - 'Magento_Ui/js/model/messageList', -- 'mage/translate' -+ 'mage/translate', -+ 'Magento_Checkout/js/model/shipping-rates-validator' - ], - function ( - ko, -@@ -33,35 +34,29 @@ function ( - customerData, - setBillingAddressAction, - globalMessageList, -- $t -+ $t, -+ shippingRatesValidator - ) { - 'use strict'; - - var lastSelectedBillingAddress = null, -- newAddressOption = { -- /** -- * Get new address label -- * @returns {String} -- */ -- getAddressInline: function () { -- return $t('New Address'); -- }, -- customerAddressId: null -- }, - countryData = customerData.get('directory-data'), - addressOptions = addressList().filter(function (address) { -- return address.getType() == 'customer-address'; //eslint-disable-line eqeqeq -+ return address.getType() === 'customer-address'; - }); - -- addressOptions.push(newAddressOption); -- - return Component.extend({ - defaults: { -- template: 'Magento_Checkout/billing-address' -+ template: 'Magento_Checkout/billing-address', -+ actionsTemplate: 'Magento_Checkout/billing-address/actions', -+ formTemplate: 'Magento_Checkout/billing-address/form', -+ detailsTemplate: 'Magento_Checkout/billing-address/details', -+ links: { -+ isAddressFormVisible: '${$.billingAddressListProvider}:isNewAddressSelected' -+ } - }, - currentBillingAddress: quote.billingAddress, -- addressOptions: addressOptions, -- customerHasAddresses: addressOptions.length > 1, -+ customerHasAddresses: addressOptions.length > 0, - - /** - * Init component -@@ -71,6 +66,7 @@ function ( - quote.paymentMethod.subscribe(function () { - checkoutDataResolver.resolveBillingAddress(); - }, this); -+ shippingRatesValidator.initFields(this.get('name') + '.form-fields'); - }, - - /** -@@ -81,7 +77,7 @@ function ( - .observe({ - selectedAddress: null, - isAddressDetailsVisible: quote.billingAddress() != null, -- isAddressFormVisible: !customer.isLoggedIn() || addressOptions.length === 1, -+ isAddressFormVisible: !customer.isLoggedIn() || !addressOptions.length, - isAddressSameAsShipping: false, - saveInAddressBook: 1 - }); -@@ -144,7 +140,7 @@ function ( - updateAddress: function () { - var addressData, newBillingAddress; - -- if (this.selectedAddress() && this.selectedAddress() != newAddressOption) { //eslint-disable-line eqeqeq -+ if (this.selectedAddress() && !this.isAddressFormVisible()) { - selectBillingAddress(this.selectedAddress()); - checkoutData.setSelectedBillingAddress(this.selectedAddress().getKey()); - } else { -@@ -199,6 +195,13 @@ function ( - } - }, - -+ /** -+ * Manage cancel button visibility -+ */ -+ canUseCancelBillingAddress: ko.computed(function () { -+ return quote.billingAddress() || lastSelectedBillingAddress; -+ }), -+ - /** - * Restore billing address - */ -@@ -208,13 +211,6 @@ function ( - } - }, - -- /** -- * @param {Object} address -- */ -- onAddressChange: function (address) { -- this.isAddressFormVisible(address == newAddressOption); //eslint-disable-line eqeqeq -- }, -- - /** - * @param {Number} countryId - * @return {*} -diff --git a/app/code/Magento/Checkout/view/frontend/web/js/view/billing-address/list.js b/app/code/Magento/Checkout/view/frontend/web/js/view/billing-address/list.js -new file mode 100644 -index 00000000000..ca3a267c016 ---- /dev/null -+++ b/app/code/Magento/Checkout/view/frontend/web/js/view/billing-address/list.js -@@ -0,0 +1,77 @@ -+/** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+ -+define([ -+ 'uiComponent', -+ 'Magento_Customer/js/model/address-list', -+ 'mage/translate', -+ 'Magento_Customer/js/model/customer' -+], function (Component, addressList, $t, customer) { -+ 'use strict'; -+ -+ var newAddressOption = { -+ /** -+ * Get new address label -+ * @returns {String} -+ */ -+ getAddressInline: function () { -+ return $t('New Address'); -+ }, -+ customerAddressId: null -+ }, -+ addressOptions = addressList().filter(function (address) { -+ return address.getType() === 'customer-address'; -+ }); -+ -+ return Component.extend({ -+ defaults: { -+ template: 'Magento_Checkout/billing-address', -+ selectedAddress: null, -+ isNewAddressSelected: false, -+ addressOptions: addressOptions, -+ exports: { -+ selectedAddress: '${ $.parentName }:selectedAddress' -+ } -+ }, -+ -+ /** -+ * @returns {Object} Chainable. -+ */ -+ initConfig: function () { -+ this._super(); -+ this.addressOptions.push(newAddressOption); -+ -+ return this; -+ }, -+ -+ /** -+ * @return {exports.initObservable} -+ */ -+ initObservable: function () { -+ this._super() -+ .observe('selectedAddress isNewAddressSelected') -+ .observe({ -+ isNewAddressSelected: !customer.isLoggedIn() || !addressOptions.length -+ }); -+ -+ return this; -+ }, -+ -+ /** -+ * @param {Object} address -+ * @return {*} -+ */ -+ addressOptionsText: function (address) { -+ return address.getAddressInline(); -+ }, -+ -+ /** -+ * @param {Object} address -+ */ -+ onAddressChange: function (address) { -+ this.isNewAddressSelected(address === newAddressOption); -+ } -+ }); -+}); -diff --git a/app/code/Magento/Checkout/view/frontend/web/js/view/cart/shipping-estimation.js b/app/code/Magento/Checkout/view/frontend/web/js/view/cart/shipping-estimation.js -index 39b5ea0299a..a857d89a72b 100644 ---- a/app/code/Magento/Checkout/view/frontend/web/js/view/cart/shipping-estimation.js -+++ b/app/code/Magento/Checkout/view/frontend/web/js/view/cart/shipping-estimation.js -@@ -55,6 +55,12 @@ define( - checkoutDataResolver.resolveEstimationAddress(); - address = quote.isVirtual() ? quote.billingAddress() : quote.shippingAddress(); - -+ if (!address && quote.isVirtual()) { -+ address = addressConverter.formAddressDataToQuoteAddress( -+ checkoutData.getSelectedBillingAddress() -+ ); -+ } -+ - if (address) { - estimatedAddress = address.isEditable() ? - addressConverter.quoteAddressToFormAddressData(address) : -diff --git a/app/code/Magento/Checkout/view/frontend/web/js/view/form/element/email.js b/app/code/Magento/Checkout/view/frontend/web/js/view/form/element/email.js -index 4a25778e754..c0de643d3a2 100644 ---- a/app/code/Magento/Checkout/view/frontend/web/js/view/form/element/email.js -+++ b/app/code/Magento/Checkout/view/frontend/web/js/view/form/element/email.js -@@ -17,7 +17,16 @@ define([ - ], function ($, Component, ko, customer, checkEmailAvailability, loginAction, quote, checkoutData, fullScreenLoader) { - 'use strict'; - -- var validatedEmail = checkoutData.getValidatedEmailValue(); -+ var validatedEmail; -+ -+ if (!checkoutData.getValidatedEmailValue() && -+ window.checkoutConfig.validatedEmailValue -+ ) { -+ checkoutData.setInputFieldEmailValue(window.checkoutConfig.validatedEmailValue); -+ checkoutData.setValidatedEmailValue(window.checkoutConfig.validatedEmailValue); -+ } -+ -+ validatedEmail = checkoutData.getValidatedEmailValue(); - - if (validatedEmail && !customer.isLoggedIn()) { - quote.guestEmail = validatedEmail; -@@ -33,6 +42,9 @@ define([ - listens: { - email: 'emailHasChanged', - emailFocused: 'validateEmail' -+ }, -+ ignoreTmpls: { -+ email: true - } - }, - checkDelay: 2000, -@@ -168,7 +180,7 @@ define([ - }, - - /** -- * Resolves an initial sate of a login form. -+ * Resolves an initial state of a login form. - * - * @returns {Boolean} - initial visibility state. - */ -diff --git a/app/code/Magento/Checkout/view/frontend/web/js/view/minicart.js b/app/code/Magento/Checkout/view/frontend/web/js/view/minicart.js -index a2f8c8c56ff..5e29fa209a6 100644 ---- a/app/code/Magento/Checkout/view/frontend/web/js/view/minicart.js -+++ b/app/code/Magento/Checkout/view/frontend/web/js/view/minicart.js -@@ -81,6 +81,7 @@ define([ - maxItemsToDisplay: window.checkout.maxItemsToDisplay, - cart: {}, - -+ // jscs:disable requireCamelCaseOrUpperCaseIdentifiers - /** - * @override - */ -@@ -101,12 +102,16 @@ define([ - self.isLoading(true); - }); - -- if (cartData()['website_id'] !== window.checkout.websiteId) { -+ if (cartData().website_id !== window.checkout.websiteId || -+ cartData().store_id !== window.checkout.storeId -+ ) { - customerData.reload(['cart'], false); - } - - return this._super(); - }, -+ //jscs:enable requireCamelCaseOrUpperCaseIdentifiers -+ - isLoading: ko.observable(false), - initSidebar: initSidebar, - -diff --git a/app/code/Magento/Checkout/view/frontend/web/js/view/payment.js b/app/code/Magento/Checkout/view/frontend/web/js/view/payment.js -index c17e5e40d5c..e8994c61b72 100644 ---- a/app/code/Magento/Checkout/view/frontend/web/js/view/payment.js -+++ b/app/code/Magento/Checkout/view/frontend/web/js/view/payment.js -@@ -66,9 +66,21 @@ define([ - navigate: function () { - var self = this; - -- getPaymentInformation().done(function () { -- self.isVisible(true); -- }); -+ if (!self.hasShippingMethod()) { -+ this.isVisible(false); -+ stepNavigator.setHash('shipping'); -+ } else { -+ getPaymentInformation().done(function () { -+ self.isVisible(true); -+ }); -+ } -+ }, -+ -+ /** -+ * @return {Boolean} -+ */ -+ hasShippingMethod: function () { -+ return window.checkoutConfig.selectedShippingMethod !== null; - }, - - /** -diff --git a/app/code/Magento/Checkout/view/frontend/web/js/view/payment/default.js b/app/code/Magento/Checkout/view/frontend/web/js/view/payment/default.js -index 7b200860c4d..1b5463c0770 100644 ---- a/app/code/Magento/Checkout/view/frontend/web/js/view/payment/default.js -+++ b/app/code/Magento/Checkout/view/frontend/web/js/view/payment/default.js -@@ -133,15 +133,14 @@ define([ - event.preventDefault(); - } - -- if (this.validate() && additionalValidators.validate()) { -+ if (this.validate() && -+ additionalValidators.validate() && -+ this.isPlaceOrderActionAllowed() === true -+ ) { - this.isPlaceOrderActionAllowed(false); - - this.getPlaceOrderDeferredObject() -- .fail( -- function () { -- self.isPlaceOrderActionAllowed(true); -- } -- ).done( -+ .done( - function () { - self.afterPlaceOrder(); - -@@ -149,6 +148,10 @@ define([ - redirectOnSuccessAction.execute(); - } - } -+ ).always( -+ function () { -+ self.isPlaceOrderActionAllowed(true); -+ } - ); - - return true; -diff --git a/app/code/Magento/Checkout/view/frontend/web/js/view/progress-bar.js b/app/code/Magento/Checkout/view/frontend/web/js/view/progress-bar.js -index 683a18d0e4e..30ea9da1dd6 100644 ---- a/app/code/Magento/Checkout/view/frontend/web/js/view/progress-bar.js -+++ b/app/code/Magento/Checkout/view/frontend/web/js/view/progress-bar.js -@@ -23,11 +23,17 @@ define([ - - /** @inheritdoc */ - initialize: function () { -+ var stepsValue; -+ - this._super(); - window.addEventListener('hashchange', _.bind(stepNavigator.handleHash, stepNavigator)); - - if (!window.location.hash) { -- stepNavigator.setHash(stepNavigator.steps().sort(stepNavigator.sortItems)[0].code); -+ stepsValue = stepNavigator.steps(); -+ -+ if (stepsValue.length) { -+ stepNavigator.setHash(stepsValue.sort(stepNavigator.sortItems)[0].code); -+ } - } - - stepNavigator.handleHash(); -diff --git a/app/code/Magento/Checkout/view/frontend/web/js/view/shipping.js b/app/code/Magento/Checkout/view/frontend/web/js/view/shipping.js -index a7cb7f7e7de..c811d3a1e83 100644 ---- a/app/code/Magento/Checkout/view/frontend/web/js/view/shipping.js -+++ b/app/code/Magento/Checkout/view/frontend/web/js/view/shipping.js -@@ -247,6 +247,8 @@ define([ - */ - setShippingInformation: function () { - if (this.validateShippingInformation()) { -+ quote.billingAddress(null); -+ checkoutDataResolver.resolveBillingAddress(); - setShippingInformationAction().done( - function () { - stepNavigator.next(); -@@ -263,7 +265,11 @@ define([ - addressData, - loginFormSelector = 'form[data-role=email-with-possible-login]', - emailValidationResult = customer.isLoggedIn(), -- field; -+ field, -+ country = registry.get(this.parentName + '.shippingAddress.shipping-address-fieldset.country_id'), -+ countryIndexedOptions = country.indexedOptions, -+ option = countryIndexedOptions[quote.shippingAddress().countryId], -+ messageContainer = registry.get('checkout.errors').messageContainer; - - if (!quote.shippingMethod()) { - this.errorValidationMessage( -@@ -316,6 +322,16 @@ define([ - shippingAddress['save_in_address_book'] = 1; - } - selectShippingAddress(shippingAddress); -+ } else if (customer.isLoggedIn() && -+ option && -+ option['is_region_required'] && -+ !quote.shippingAddress().region -+ ) { -+ messageContainer.addErrorMessage({ -+ message: $t('Please specify a regionId in shipping address.') -+ }); -+ -+ return false; - } - - if (!emailValidationResult) { -diff --git a/app/code/Magento/Checkout/view/frontend/web/js/view/summary/item/details/message.js b/app/code/Magento/Checkout/view/frontend/web/js/view/summary/item/details/message.js -new file mode 100644 -index 00000000000..ed41fd26c47 ---- /dev/null -+++ b/app/code/Magento/Checkout/view/frontend/web/js/view/summary/item/details/message.js -@@ -0,0 +1,30 @@ -+/** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+ -+define(['uiComponent'], function (Component) { -+ 'use strict'; -+ -+ var quoteMessages = window.checkoutConfig.quoteMessages; -+ -+ return Component.extend({ -+ defaults: { -+ template: 'Magento_Checkout/summary/item/details/message' -+ }, -+ displayArea: 'item_message', -+ quoteMessages: quoteMessages, -+ -+ /** -+ * @param {Object} item -+ * @return {null} -+ */ -+ getMessage: function (item) { -+ if (this.quoteMessages[item['item_id']]) { -+ return this.quoteMessages[item['item_id']]; -+ } -+ -+ return null; -+ } -+ }); -+}); -diff --git a/app/code/Magento/Checkout/view/frontend/web/js/view/summary/shipping.js b/app/code/Magento/Checkout/view/frontend/web/js/view/summary/shipping.js -index 3fda2603392..10d49265e3b 100644 ---- a/app/code/Magento/Checkout/view/frontend/web/js/view/summary/shipping.js -+++ b/app/code/Magento/Checkout/view/frontend/web/js/view/summary/shipping.js -@@ -21,14 +21,21 @@ define([ - * @return {*} - */ - getShippingMethodTitle: function () { -- var shippingMethod; -+ var shippingMethod = '', -+ shippingMethodTitle = ''; - - if (!this.isCalculated()) { - return ''; - } - shippingMethod = quote.shippingMethod(); - -- return shippingMethod ? shippingMethod['carrier_title'] + ' - ' + shippingMethod['method_title'] : ''; -+ if (typeof shippingMethod['method_title'] !== 'undefined') { -+ shippingMethodTitle = ' - ' + shippingMethod['method_title']; -+ } -+ -+ return shippingMethod ? -+ shippingMethod['carrier_title'] + shippingMethodTitle : -+ shippingMethod['carrier_title']; - }, - - /** -diff --git a/app/code/Magento/Checkout/view/frontend/web/template/authentication.html b/app/code/Magento/Checkout/view/frontend/web/template/authentication.html -index 406a7d899b6..5b8dde81dd9 100644 ---- a/app/code/Magento/Checkout/view/frontend/web/template/authentication.html -+++ b/app/code/Magento/Checkout/view/frontend/web/template/authentication.html -@@ -31,15 +31,15 @@ - <div class="block block-customer-login" - data-bind="attr: {'data-label': $t('or')}"> - <div class="block-title"> -- <strong id="block-customer-login-heading" -- role="heading" -- aria-level="2" -- data-bind="i18n: 'Sign In'"></strong> -+ <strong id="block-customer-login-heading-checkout" -+ role="heading" -+ aria-level="2" -+ data-bind="i18n: 'Sign In'"></strong> - </div> - <!-- ko foreach: getRegion('messages') --> - <!-- ko template: getTemplate() --><!-- /ko --> - <!--/ko--> -- <div class="block-content" aria-labelledby="block-customer-login-heading"> -+ <div class="block-content" aria-labelledby="block-customer-login-heading-checkout"> - <form data-role="login" - data-bind="submit:login" - method="post"> -diff --git a/app/code/Magento/Checkout/view/frontend/web/template/billing-address.html b/app/code/Magento/Checkout/view/frontend/web/template/billing-address.html -index 5f735fbb4da..cabfcc9b3db 100644 ---- a/app/code/Magento/Checkout/view/frontend/web/template/billing-address.html -+++ b/app/code/Magento/Checkout/view/frontend/web/template/billing-address.html -@@ -5,28 +5,18 @@ - */ - --> - <div class="checkout-billing-address"> -- - <div class="billing-address-same-as-shipping-block field choice" data-bind="visible: canUseShippingAddress()"> - <input type="checkbox" name="billing-address-same-as-shipping" - data-bind="checked: isAddressSameAsShipping, click: useShippingAddress, attr: {id: 'billing-address-same-as-shipping-' + getCode($parent)}"/> - <label data-bind="attr: {for: 'billing-address-same-as-shipping-' + getCode($parent)}"><span - data-bind="i18n: 'My billing and shipping address are the same'"></span></label> - </div> -- -- <!-- ko template: 'Magento_Checkout/billing-address/details' --><!-- /ko --> -+ <render args="detailsTemplate"/> - <fieldset class="fieldset" data-bind="visible: !isAddressDetailsVisible()"> -- <!-- ko template: 'Magento_Checkout/billing-address/list' --><!-- /ko --> -- <!-- ko template: 'Magento_Checkout/billing-address/form' --><!-- /ko --> -- <div class="actions-toolbar"> -- <div class="primary"> -- <button class="action action-update" type="button" data-bind="click: updateAddress"> -- <span data-bind="i18n: 'Update'"></span> -- </button> -- <button class="action action-cancel" type="button" data-bind="click: cancelAddressEdit"> -- <span data-bind="i18n: 'Cancel'"></span> -- </button> -- </div> -+ <each args="getRegion('billing-address-list')" render="" /> -+ <div data-bind="fadeVisible: isAddressFormVisible"> -+ <render args="formTemplate"/> - </div> -+ <render args="actionsTemplate"/> - </fieldset> -- - </div> -diff --git a/app/code/Magento/Checkout/view/frontend/web/template/billing-address/actions.html b/app/code/Magento/Checkout/view/frontend/web/template/billing-address/actions.html -new file mode 100644 -index 00000000000..860f340d3f7 ---- /dev/null -+++ b/app/code/Magento/Checkout/view/frontend/web/template/billing-address/actions.html -@@ -0,0 +1,21 @@ -+<!-- -+/** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+<div class="actions-toolbar"> -+ <div class="primary"> -+ <button class="action action-update" -+ type="button" -+ click="updateAddress"> -+ <span translate="'Update'"/> -+ </button> -+ <button class="action action-cancel" -+ type="button" -+ click="cancelAddressEdit" -+ visible="canUseCancelBillingAddress()"> -+ <span translate="'Cancel'"/> -+ </button> -+ </div> -+</div> -diff --git a/app/code/Magento/Checkout/view/frontend/web/template/billing-address/details.html b/app/code/Magento/Checkout/view/frontend/web/template/billing-address/details.html -index fd994a4e8a9..ea521b3a8af 100644 ---- a/app/code/Magento/Checkout/view/frontend/web/template/billing-address/details.html -+++ b/app/code/Magento/Checkout/view/frontend/web/template/billing-address/details.html -@@ -4,28 +4,37 @@ - * See COPYING.txt for license details. - */ - --> --<div class="billing-address-details" data-bind="if: isAddressDetailsVisible() && currentBillingAddress()"> -- <!-- ko text: currentBillingAddress().prefix --><!-- /ko --> <!-- ko text: currentBillingAddress().firstname --><!-- /ko --> <!-- ko text: currentBillingAddress().middlename --><!-- /ko --> -- <!-- ko text: currentBillingAddress().lastname --><!-- /ko --> <!-- ko text: currentBillingAddress().suffix --><!-- /ko --><br/> -- <!-- ko text: _.values(currentBillingAddress().street).join(", ") --><!-- /ko --><br/> -- <!-- ko text: currentBillingAddress().city --><!-- /ko -->, <span data-bind="html: currentBillingAddress().region"></span> <!-- ko text: currentBillingAddress().postcode --><!-- /ko --><br/> -- <!-- ko text: getCountryName(currentBillingAddress().countryId) --><!-- /ko --><br/> -- <!-- ko if: (currentBillingAddress().telephone) --> -- <a data-bind="text: currentBillingAddress().telephone, attr: {'href': 'tel:' + currentBillingAddress().telephone}"></a> -- <!-- /ko --><br/> -- <!-- ko foreach: { data: currentBillingAddress().customAttributes, as: 'element' } --> -- <!-- ko foreach: { data: Object.keys(element), as: 'attribute' } --> -- <!-- ko if: (typeof element[attribute] === "object") --> -- <!-- ko text: element[attribute].value --><!-- /ko --> -- <!-- /ko --> -- <!-- ko if: (typeof element[attribute] === "string") --> -- <!-- ko text: element[attribute] --><!-- /ko --> -- <!-- /ko --><br/> -- <!-- /ko --> -- <!-- /ko --> -- <button type="button" -+<div if="isAddressDetailsVisible() && currentBillingAddress()" class="billing-address-details"> -+ <text args="currentBillingAddress().prefix"/> <text args="currentBillingAddress().firstname"/> <text args="currentBillingAddress().middlename"/> -+ <text args="currentBillingAddress().lastname"/> <text args="currentBillingAddress().suffix"/><br/> -+ <text args="_.values(currentBillingAddress().street).join(', ')"/><br/> -+ <text args="currentBillingAddress().city "/>, <span text="currentBillingAddress().region"></span> <text args="currentBillingAddress().postcode"/><br/> -+ <text args="getCountryName(currentBillingAddress().countryId)"/><br/> -+ <a if="currentBillingAddress().telephone" attr="'href': 'tel:' + currentBillingAddress().telephone" text="currentBillingAddress().telephone"></a><br/> -+ -+ <each args="data: currentBillingAddress().customAttributes, as: 'element'"> -+ <each args="data: Object.keys(element), as: 'attribute'"> -+ <if args="typeof element[attribute] === 'object'"> -+ <if args="element[attribute].label"> -+ <text args="element[attribute].label"/> -+ </if> -+ <ifnot args="element[attribute].label"> -+ <if args="element[attribute].value"> -+ <text args="element[attribute].value"/> -+ </if> -+ </ifnot> -+ </if> -+ <if args="typeof element[attribute] === 'string'"> -+ <text args="element[attribute]"/> -+ </if><br/> -+ </each> -+ </each> -+ -+ <button visible="!isAddressSameAsShipping()" -+ type="button" - class="action action-edit-address" -- data-bind="visible: !isAddressSameAsShipping(), click: editAddress"> -- <span data-bind="i18n: 'Edit'"></span> -+ click="editAddress"> -+ <span translate="'Edit'"></span> - </button> - </div> -+ -diff --git a/app/code/Magento/Checkout/view/frontend/web/template/billing-address/form.html b/app/code/Magento/Checkout/view/frontend/web/template/billing-address/form.html -index 54fe9a1f593..e29ed99d17b 100644 ---- a/app/code/Magento/Checkout/view/frontend/web/template/billing-address/form.html -+++ b/app/code/Magento/Checkout/view/frontend/web/template/billing-address/form.html -@@ -4,7 +4,7 @@ - * See COPYING.txt for license details. - */ - --> --<div class="billing-address-form" data-bind="fadeVisible: isAddressFormVisible"> -+<div class="billing-address-form"> - <!-- ko foreach: getRegion('before-fields') --> - <!-- ko template: getTemplate() --><!-- /ko --> - <!--/ko--> -diff --git a/app/code/Magento/Checkout/view/frontend/web/template/form/element/email.html b/app/code/Magento/Checkout/view/frontend/web/template/form/element/email.html -index 8d6142e07fc..6a784fa7a04 100644 ---- a/app/code/Magento/Checkout/view/frontend/web/template/form/element/email.html -+++ b/app/code/Magento/Checkout/view/frontend/web/template/form/element/email.html -@@ -14,7 +14,7 @@ - method="post"> - <fieldset id="customer-email-fieldset" class="fieldset" data-bind="blockLoader: isLoading"> - <div class="field required"> -- <label class="label" for="customer-email"> -+ <label class="label" for="checkout-customer-email"> - <span data-bind="i18n: 'Email Address'"></span> - </label> - <div class="control _with-tooltip"> -@@ -26,7 +26,7 @@ - mageInit: {'mage/trim-input':{}}" - name="username" - data-validate="{required:true, 'validate-email':true}" -- id="customer-email" /> -+ id="checkout-customer-email" /> - <!-- ko template: 'ui/form/element/helper/tooltip' --><!-- /ko --> - <span class="note" data-bind="fadeVisible: isPasswordVisible() == false"><!-- ko i18n: 'You can create an account after checkout.'--><!-- /ko --></span> - </div> -diff --git a/app/code/Magento/Checkout/view/frontend/web/template/minicart/content.html b/app/code/Magento/Checkout/view/frontend/web/template/minicart/content.html -index 2daca51a2f5..fb128a891ae 100644 ---- a/app/code/Magento/Checkout/view/frontend/web/template/minicart/content.html -+++ b/app/code/Magento/Checkout/view/frontend/web/template/minicart/content.html -@@ -97,7 +97,7 @@ - </div> - </div> - -- <div id="minicart-widgets" class="minicart-widgets"> -+ <div id="minicart-widgets" class="minicart-widgets" if="getRegion('promotion').length"> - <each args="getRegion('promotion')" render=""/> - </div> - </div> -diff --git a/app/code/Magento/Checkout/view/frontend/web/template/registration.html b/app/code/Magento/Checkout/view/frontend/web/template/registration.html -index ea94726e544..5cc0d189e7c 100644 ---- a/app/code/Magento/Checkout/view/frontend/web/template/registration.html -+++ b/app/code/Magento/Checkout/view/frontend/web/template/registration.html -@@ -11,8 +11,8 @@ - <!-- ko if: isFormVisible --> - <p data-bind="i18n: 'You can track your order status by creating an account.'"></p> - <p><span data-bind="i18n: 'Email Address'"></span>: <span data-bind="text: getEmailAddress()"></span></p> -- <form method="get" data-bind="attr: { action: getUrl() }"> -- <input type="submit" class="action primary" data-bind="value: $t('Create an Account')" /> -- </form> -+ <a class="action primary" data-bind="attr: { href: getUrl() }"> -+ <span data-bind="i18n: 'Create an Account'" /> -+ </a> - <!--/ko--> - </div> -diff --git a/app/code/Magento/Checkout/view/frontend/web/template/shipping-address/address-renderer/default.html b/app/code/Magento/Checkout/view/frontend/web/template/shipping-address/address-renderer/default.html -index 2e268461d1e..cf64c0140b9 100644 ---- a/app/code/Magento/Checkout/view/frontend/web/template/shipping-address/address-renderer/default.html -+++ b/app/code/Magento/Checkout/view/frontend/web/template/shipping-address/address-renderer/default.html -@@ -4,33 +4,40 @@ - * See COPYING.txt for license details. - */ - --> --<div class="shipping-address-item" data-bind="css: isSelected() ? 'selected-item' : 'not-selected-item'"> -- <!-- ko text: address().prefix --><!-- /ko --> <!-- ko text: address().firstname --><!-- /ko --> <!-- ko text: address().middlename --><!-- /ko --> -- <!-- ko text: address().lastname --><!-- /ko --> <!-- ko text: address().suffix --><!-- /ko --><br/> -- <!-- ko text: _.values(address().street).join(", ") --><!-- /ko --><br/> -- <!-- ko text: address().city --><!-- /ko -->, <span data-bind="html: address().region"></span> <!-- ko text: address().postcode --><!-- /ko --><br/> -- <!-- ko text: getCountryName(address().countryId) --><!-- /ko --><br/> -- <!-- ko if: (address().telephone) --> -- <a data-bind="text: address().telephone, attr: {'href': 'tel:' + address().telephone}"></a> -- <!-- /ko --><br/> -- <!-- ko foreach: { data: address().customAttributes, as: 'element' } --> -- <!-- ko foreach: { data: Object.keys(element), as: 'attribute' } --> -- <!-- ko if: (typeof element[attribute] === "object") --> -- <!-- ko text: element[attribute].value --><!-- /ko --> -- <!-- /ko --> -- <!-- ko if: (typeof element[attribute] === "string") --> -- <!-- ko text: element[attribute] --><!-- /ko --> -- <!-- /ko --><br/> -- <!-- /ko --> -- <!-- /ko --> -- <!-- ko if: (address().isEditable()) --> -- <button type="button" -+<div class="shipping-address-item" css="'selected-item' : isSelected() , 'not-selected-item':!isSelected()"> -+ <text args="address().prefix"/> <text args="address().firstname"/> <text args="address().middlename"/> -+ <text args="address().lastname"/> <text args="address().suffix"/><br/> -+ <text args="_.values(address().street).join(', ')"/><br/> -+ <text args="address().city "/>, <span text="address().region"></span> <text args="address().postcode"/><br/> -+ <text args="getCountryName(address().countryId)"/><br/> -+ <a if="address().telephone" attr="'href': 'tel:' + address().telephone" text="address().telephone"></a><br/> -+ -+ <each args="data: address().customAttributes, as: 'element'"> -+ <each args="data: Object.keys(element), as: 'attribute'"> -+ <if args="typeof element[attribute] === 'object'"> -+ <if args="element[attribute].label"> -+ <text args="element[attribute].label"/> -+ </if> -+ <ifnot args="element[attribute].label"> -+ <if args="element[attribute].value"> -+ <text args="element[attribute].value"/> -+ </if> -+ </ifnot> -+ </if> -+ <if args="typeof element[attribute] === 'string'"> -+ <text args="element[attribute]"/> -+ </if><br/> -+ </each> -+ </each> -+ -+ <button visible="address().isEditable()" type="button" - class="action edit-address-link" -- data-bind="click: editAddress, visible: address().isEditable()"> -- <span data-bind="i18n: 'Edit'"></span> -+ click="editAddress"> -+ <span translate="'Edit'"></span> - </button> -- <!-- /ko --> -- <button type="button" data-bind="click: selectAddress" class="action action-select-shipping-item"> -- <span data-bind="i18n: 'Ship Here'"></span> -+ <!-- ko if: (!isSelected()) --> -+ <button type="button" click="selectAddress" class="action action-select-shipping-item"> -+ <span translate="'Ship Here'"></span> - </button> -+ <!-- /ko --> - </div> -diff --git a/app/code/Magento/Checkout/view/frontend/web/template/shipping-information/address-renderer/default.html b/app/code/Magento/Checkout/view/frontend/web/template/shipping-information/address-renderer/default.html -index b66526f660a..541413955cb 100644 ---- a/app/code/Magento/Checkout/view/frontend/web/template/shipping-information/address-renderer/default.html -+++ b/app/code/Magento/Checkout/view/frontend/web/template/shipping-information/address-renderer/default.html -@@ -4,23 +4,29 @@ - * See COPYING.txt for license details. - */ - --> --<!-- ko if: (visible()) --> -- <!-- ko text: address().prefix --><!-- /ko --> <!-- ko text: address().firstname --><!-- /ko --> <!-- ko text: address().middlename --><!-- /ko --> -- <!-- ko text: address().lastname --><!-- /ko --> <!-- ko text: address().suffix --><!-- /ko --><br/> -- <!-- ko text: _.values(address().street).join(", ") --><!-- /ko --><br/> -- <!-- ko text: address().city --><!-- /ko -->, <span data-bind="html: address().region"></span> <!-- ko text: address().postcode --><!-- /ko --><br/> -- <!-- ko text: getCountryName(address().countryId) --><!-- /ko --><br/> -- <!-- ko if: (address().telephone) --> -- <a data-bind="text: address().telephone, attr: {'href': 'tel:' + address().telephone}"></a> -- <!-- /ko --><br/> -- <!-- ko foreach: { data: address().customAttributes, as: 'element' } --> -- <!-- ko foreach: { data: Object.keys(element), as: 'attribute' } --> -- <!-- ko if: (typeof element[attribute] === "object") --> -- <!-- ko text: element[attribute].value --><!-- /ko --> -- <!-- /ko --> -- <!-- ko if: (typeof element[attribute] === "string") --> -- <!-- ko text: element[attribute] --><!-- /ko --> -- <!-- /ko --><br/> -- <!-- /ko --> -- <!-- /ko --> --<!-- /ko --> -+<if args="visible()"> -+ <text args="address().prefix"/> <text args="address().firstname"/> <text args="address().middlename"/> -+ <text args="address().lastname"/> <text args="address().suffix"/><br/> -+ <text args="_.values(address().street).join(', ')"/><br/> -+ <text args="address().city "/>, <span text="address().region"></span> <text args="address().postcode"/><br/> -+ <text args="getCountryName(address().countryId)"/><br/> -+ <a if="address().telephone" attr="'href': 'tel:' + address().telephone" text="address().telephone"></a><br/> -+ -+ <each args="data: address().customAttributes, as: 'element'"> -+ <each args="data: Object.keys(element), as: 'attribute'"> -+ <if args="typeof element[attribute] === 'object'"> -+ <if args="element[attribute].label"> -+ <text args="element[attribute].label"/> -+ </if> -+ <ifnot args="element[attribute].label"> -+ <if args="element[attribute].value"> -+ <text args="element[attribute].value"/> -+ </if> -+ </ifnot> -+ </if> -+ <if args="typeof element[attribute] === 'string'"> -+ <text args="element[attribute]"/> -+ </if><br/> -+ </each> -+ </each> -+</if> -diff --git a/app/code/Magento/Checkout/view/frontend/web/template/shipping.html b/app/code/Magento/Checkout/view/frontend/web/template/shipping.html -index a1a5aa67a96..1fcfa4b3b13 100644 ---- a/app/code/Magento/Checkout/view/frontend/web/template/shipping.html -+++ b/app/code/Magento/Checkout/view/frontend/web/template/shipping.html -@@ -16,12 +16,14 @@ - - <!-- Address form pop up --> - <if args="!isFormInline"> -- <button type="button" -- class="action action-show-popup" -- click="showFormPopUp" -- visible="!isNewAddressAdded()"> -- <span translate="'New Address'" /> -- </button> -+ <div class="new-address-popup"> -+ <button type="button" -+ class="action action-show-popup" -+ click="showFormPopUp" -+ visible="!isNewAddressAdded()"> -+ <span translate="'New Address'" /> -+ </button> -+ </div> - <div id="opc-new-shipping-address" - visible="isFormPopUpVisible()" - render="shippingFormTemplate" /> -diff --git a/app/code/Magento/Checkout/view/frontend/web/template/summary/cart-items.html b/app/code/Magento/Checkout/view/frontend/web/template/summary/cart-items.html -index 34ec91aa43c..fc74a4691a2 100644 ---- a/app/code/Magento/Checkout/view/frontend/web/template/summary/cart-items.html -+++ b/app/code/Magento/Checkout/view/frontend/web/template/summary/cart-items.html -@@ -6,7 +6,7 @@ - --> - <div class="block items-in-cart" data-bind="mageInit: {'collapsible':{'openedState': 'active', 'active': isItemsBlockExpanded()}}"> - <div class="title" data-role="title"> -- <strong role="heading"> -+ <strong role="heading" aria-level="1"> - <translate args="maxCartItemsToDisplay" if="maxCartItemsToDisplay < getCartLineItemsCount()"/> - <translate args="'of'" if="maxCartItemsToDisplay < getCartLineItemsCount()"/> - <span data-bind="text: getCartLineItemsCount()"></span> -diff --git a/app/code/Magento/Checkout/view/frontend/web/template/summary/item/details.html b/app/code/Magento/Checkout/view/frontend/web/template/summary/item/details.html -index 730ceadbd91..2491ee12d26 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 -@@ -35,7 +35,7 @@ - <dd class="values" data-bind="html: full_view"></dd> - <!-- /ko --> - <!-- ko ifnot: ($data.full_view)--> -- <dd class="values" data-bind="text: value"></dd> -+ <dd class="values" data-bind="html: value"></dd> - <!-- /ko --> - <!-- /ko --> - </dl> -@@ -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 00000000000..ea8f58cccd5 ---- /dev/null -+++ b/app/code/Magento/Checkout/view/frontend/web/template/summary/item/details/message.html -@@ -0,0 +1,9 @@ -+<!-- -+/** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+<div class="cart item message notice" if="getMessage($parents[1])"> -+ <div data-bind="text: getMessage($parents[1])"></div> -+</div> -diff --git a/app/code/Magento/CheckoutAgreements/Controller/Adminhtml/Agreement/Delete.php b/app/code/Magento/CheckoutAgreements/Controller/Adminhtml/Agreement/Delete.php -index f7b178df996..d727107a86f 100644 ---- a/app/code/Magento/CheckoutAgreements/Controller/Adminhtml/Agreement/Delete.php -+++ b/app/code/Magento/CheckoutAgreements/Controller/Adminhtml/Agreement/Delete.php -@@ -12,8 +12,9 @@ use Magento\Backend\App\Action\Context; - use Magento\Framework\Registry; - use Magento\Framework\App\ObjectManager; - use Magento\Framework\Exception\LocalizedException; -+use Magento\Framework\App\Action\HttpPostActionInterface; - --class Delete extends Agreement -+class Delete extends Agreement implements HttpPostActionInterface - { - /** - * @var CheckoutAgreementsRepositoryInterface -diff --git a/app/code/Magento/CheckoutAgreements/Controller/Adminhtml/Agreement/Edit.php b/app/code/Magento/CheckoutAgreements/Controller/Adminhtml/Agreement/Edit.php -index 8bec3b581cd..1a82108a3da 100644 ---- a/app/code/Magento/CheckoutAgreements/Controller/Adminhtml/Agreement/Edit.php -+++ b/app/code/Magento/CheckoutAgreements/Controller/Adminhtml/Agreement/Edit.php -@@ -12,8 +12,9 @@ use Magento\Backend\App\Action\Context; - use Magento\Framework\Registry; - use Magento\Framework\App\ObjectManager; - use Magento\CheckoutAgreements\Block\Adminhtml\Agreement\Edit as BlockEdit; -+use Magento\Framework\App\Action\HttpGetActionInterface; - --class Edit extends Agreement -+class Edit extends Agreement implements HttpGetActionInterface - { - /** - * @var AgreementFactory -diff --git a/app/code/Magento/CheckoutAgreements/Controller/Adminhtml/Agreement/Index.php b/app/code/Magento/CheckoutAgreements/Controller/Adminhtml/Agreement/Index.php -index b1dfd8c304d..d32ee7a4b75 100644 ---- a/app/code/Magento/CheckoutAgreements/Controller/Adminhtml/Agreement/Index.php -+++ b/app/code/Magento/CheckoutAgreements/Controller/Adminhtml/Agreement/Index.php -@@ -6,7 +6,9 @@ - */ - namespace Magento\CheckoutAgreements\Controller\Adminhtml\Agreement; - --class Index extends \Magento\CheckoutAgreements\Controller\Adminhtml\Agreement -+use Magento\Framework\App\Action\HttpGetActionInterface as HttpGetActionInterface; -+ -+class Index extends \Magento\CheckoutAgreements\Controller\Adminhtml\Agreement implements HttpGetActionInterface - { - /** - * @return void -diff --git a/app/code/Magento/CheckoutAgreements/Controller/Adminhtml/Agreement/NewAction.php b/app/code/Magento/CheckoutAgreements/Controller/Adminhtml/Agreement/NewAction.php -index 4d482ebfc20..caa21c46823 100644 ---- a/app/code/Magento/CheckoutAgreements/Controller/Adminhtml/Agreement/NewAction.php -+++ b/app/code/Magento/CheckoutAgreements/Controller/Adminhtml/Agreement/NewAction.php -@@ -6,7 +6,9 @@ - */ - namespace Magento\CheckoutAgreements\Controller\Adminhtml\Agreement; - --class NewAction extends \Magento\CheckoutAgreements\Controller\Adminhtml\Agreement -+use Magento\Framework\App\Action\HttpGetActionInterface as HttpGetActionInterface; -+ -+class NewAction extends \Magento\CheckoutAgreements\Controller\Adminhtml\Agreement implements HttpGetActionInterface - { - /** - * @return void -diff --git a/app/code/Magento/CheckoutAgreements/Controller/Adminhtml/Agreement/Save.php b/app/code/Magento/CheckoutAgreements/Controller/Adminhtml/Agreement/Save.php -index 05a16d3dd42..4d4dd076ea1 100644 ---- a/app/code/Magento/CheckoutAgreements/Controller/Adminhtml/Agreement/Save.php -+++ b/app/code/Magento/CheckoutAgreements/Controller/Adminhtml/Agreement/Save.php -@@ -13,8 +13,9 @@ use Magento\Framework\Registry; - use Magento\Framework\App\ObjectManager; - use Magento\Framework\DataObject; - use Magento\Framework\Exception\LocalizedException; -+use Magento\Framework\App\Action\HttpPostActionInterface; - --class Save extends Agreement -+class Save extends Agreement implements HttpPostActionInterface - { - /** - * @var AgreementFactory -diff --git a/app/code/Magento/CheckoutAgreements/Model/AgreementsConfigProvider.php b/app/code/Magento/CheckoutAgreements/Model/AgreementsConfigProvider.php -index 1f1b5be9683..1217270d780 100644 ---- a/app/code/Magento/CheckoutAgreements/Model/AgreementsConfigProvider.php -+++ b/app/code/Magento/CheckoutAgreements/Model/AgreementsConfigProvider.php -@@ -67,17 +67,18 @@ class AgreementsConfigProvider implements ConfigProviderInterface - } - - /** -- * {@inheritdoc} -+ * @inheritdoc - */ - public function getConfig() - { - $agreements = []; - $agreements['checkoutAgreements'] = $this->getAgreementsConfig(); -+ - return $agreements; - } - - /** -- * Returns agreements config -+ * Returns agreements config. - * - * @return array - */ -@@ -99,7 +100,7 @@ class AgreementsConfigProvider implements ConfigProviderInterface - 'content' => $agreement->getIsHtml() - ? $agreement->getContent() - : nl2br($this->escaper->escapeHtml($agreement->getContent())), -- 'checkboxText' => $agreement->getCheckboxText(), -+ 'checkboxText' => $this->escaper->escapeHtml($agreement->getCheckboxText()), - 'mode' => $agreement->getMode(), - 'agreementId' => $agreement->getAgreementId() - ]; -diff --git a/app/code/Magento/CheckoutAgreements/Model/ResourceModel/Agreement/Grid/Collection.php b/app/code/Magento/CheckoutAgreements/Model/ResourceModel/Agreement/Grid/Collection.php -index 70794d24a64..bc055ca9f66 100644 ---- a/app/code/Magento/CheckoutAgreements/Model/ResourceModel/Agreement/Grid/Collection.php -+++ b/app/code/Magento/CheckoutAgreements/Model/ResourceModel/Agreement/Grid/Collection.php -@@ -14,7 +14,7 @@ class Collection extends \Magento\CheckoutAgreements\Model\ResourceModel\Agreeme - { - - /** -- * {@inheritdoc} -+ * @inheritdoc - */ - public function load($printQuery = false, $logQuery = false) - { -@@ -30,6 +30,8 @@ class Collection extends \Magento\CheckoutAgreements\Model\ResourceModel\Agreeme - } - - /** -+ * Add stores to result -+ * - * @return void - */ - private function addStoresToResult() -@@ -56,6 +58,8 @@ class Collection extends \Magento\CheckoutAgreements\Model\ResourceModel\Agreeme - } - - /** -+ * Get stores for agreements -+ * - * @return array - */ - private function getStoresForAgreements() -@@ -64,7 +68,7 @@ class Collection extends \Magento\CheckoutAgreements\Model\ResourceModel\Agreeme - - if (!empty($agreementId)) { - $select = $this->getConnection()->select()->from( -- ['agreement_store' => 'checkout_agreement_store'] -+ ['agreement_store' => $this->getResource()->getTable('checkout_agreement_store')] - )->where( - 'agreement_store.agreement_id IN (?)', - $agreementId -diff --git a/app/code/Magento/CheckoutAgreements/Test/Mftf/Data/AdminModuleData.xml b/app/code/Magento/CheckoutAgreements/Test/Mftf/Data/AdminModuleData.xml -new file mode 100644 -index 00000000000..d42c2c81394 ---- /dev/null -+++ b/app/code/Magento/CheckoutAgreements/Test/Mftf/Data/AdminModuleData.xml -@@ -0,0 +1,16 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<entities xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataProfileSchema.xsd"> -+ <entity name="AdminMenuStoresSettingsTermsAndConditions"> -+ <data key="pageTitle">Terms and Conditions</data> -+ <data key="title">Terms and Conditions</data> -+ <data key="dataUiId">magento-checkoutagreements-sales-checkoutagreement</data> -+ </entity> -+</entities> -diff --git a/app/code/Magento/CheckoutAgreements/Test/Mftf/Test/AdminStoresTermsAndConditionsNavigateMenuTest.xml b/app/code/Magento/CheckoutAgreements/Test/Mftf/Test/AdminStoresTermsAndConditionsNavigateMenuTest.xml -new file mode 100644 -index 00000000000..d2d4cb9138b ---- /dev/null -+++ b/app/code/Magento/CheckoutAgreements/Test/Mftf/Test/AdminStoresTermsAndConditionsNavigateMenuTest.xml -@@ -0,0 +1,36 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> -+ <test name="AdminStoresTermsAndConditionsNavigateMenuTest"> -+ <annotations> -+ <features value="CheckoutAgreements"/> -+ <stories value="Menu Navigation"/> -+ <title value="Admin stores terms and conditions navigate menu test"/> -+ <description value="Admin should be able to navigate to Stores > Terms and Conditions"/> -+ <severity value="CRITICAL"/> -+ <testCaseId value="MC-14148"/> -+ <group value="menu"/> -+ <group value="mtf_migrated"/> -+ </annotations> -+ <before> -+ <actionGroup ref="LoginAsAdmin" stepKey="LoginAsAdmin"/> -+ </before> -+ <after> -+ <actionGroup ref="logout" stepKey="logout"/> -+ </after> -+ <actionGroup ref="AdminNavigateMenuActionGroup" stepKey="navigateToStoresTermsAndConditionsPage"> -+ <argument name="menuUiId" value="{{AdminMenuStores.dataUiId}}"/> -+ <argument name="submenuUiId" value="{{AdminMenuStoresSettingsTermsAndConditions.dataUiId}}"/> -+ </actionGroup> -+ <actionGroup ref="AdminAssertPageTitleActionGroup" stepKey="seePageTitle"> -+ <argument name="title" value="{{AdminMenuStoresSettingsTermsAndConditions.pageTitle}}"/> -+ </actionGroup> -+ </test> -+</tests> -diff --git a/app/code/Magento/CheckoutAgreements/Test/Unit/Model/AgreementsConfigProviderTest.php b/app/code/Magento/CheckoutAgreements/Test/Unit/Model/AgreementsConfigProviderTest.php -index c59a3d2433e..c8309bacb0a 100644 ---- a/app/code/Magento/CheckoutAgreements/Test/Unit/Model/AgreementsConfigProviderTest.php -+++ b/app/code/Magento/CheckoutAgreements/Test/Unit/Model/AgreementsConfigProviderTest.php -@@ -8,6 +8,9 @@ namespace Magento\CheckoutAgreements\Test\Unit\Model; - use Magento\CheckoutAgreements\Model\AgreementsProvider; - use Magento\Store\Model\ScopeInterface; - -+/** -+ * Tests for AgreementsConfigProvider. -+ */ - class AgreementsConfigProviderTest extends \PHPUnit\Framework\TestCase - { - /** -@@ -35,6 +38,9 @@ class AgreementsConfigProviderTest extends \PHPUnit\Framework\TestCase - */ - private $agreementsFilterMock; - -+ /** -+ * @inheritdoc -+ */ - protected function setUp() - { - $this->scopeConfigMock = $this->createMock(\Magento\Framework\App\Config\ScopeConfigInterface::class); -@@ -59,10 +65,16 @@ class AgreementsConfigProviderTest extends \PHPUnit\Framework\TestCase - ); - } - -+ /** -+ * Test for getConfig if content is HTML. -+ * -+ * @return void -+ */ - public function testGetConfigIfContentIsHtml() - { - $content = 'content'; - $checkboxText = 'checkbox_text'; -+ $escapedCheckboxText = 'escaped_checkbox_text'; - $mode = \Magento\CheckoutAgreements\Model\AgreementModeOptions::MODE_AUTO; - $agreementId = 100; - $expectedResult = [ -@@ -71,12 +83,12 @@ class AgreementsConfigProviderTest extends \PHPUnit\Framework\TestCase - 'agreements' => [ - [ - 'content' => $content, -- 'checkboxText' => $checkboxText, -+ 'checkboxText' => $escapedCheckboxText, - 'mode' => $mode, -- 'agreementId' => $agreementId -- ] -- ] -- ] -+ 'agreementId' => $agreementId, -+ ], -+ ], -+ ], - ]; - - $this->scopeConfigMock->expects($this->once()) -@@ -94,6 +106,11 @@ class AgreementsConfigProviderTest extends \PHPUnit\Framework\TestCase - ->with($searchCriteriaMock) - ->willReturn([$agreement]); - -+ $this->escaperMock->expects($this->once()) -+ ->method('escapeHtml') -+ ->with($checkboxText) -+ ->willReturn($escapedCheckboxText); -+ - $agreement->expects($this->once())->method('getIsHtml')->willReturn(true); - $agreement->expects($this->once())->method('getContent')->willReturn($content); - $agreement->expects($this->once())->method('getCheckboxText')->willReturn($checkboxText); -@@ -103,11 +120,17 @@ class AgreementsConfigProviderTest extends \PHPUnit\Framework\TestCase - $this->assertEquals($expectedResult, $this->model->getConfig()); - } - -+ /** -+ * Test for getConfig if content is not HTML. -+ * -+ * @return void -+ */ - public function testGetConfigIfContentIsNotHtml() - { - $content = 'content'; - $escapedContent = 'escaped_content'; - $checkboxText = 'checkbox_text'; -+ $escapedCheckboxText = 'escaped_checkbox_text'; - $mode = \Magento\CheckoutAgreements\Model\AgreementModeOptions::MODE_AUTO; - $agreementId = 100; - $expectedResult = [ -@@ -116,12 +139,12 @@ class AgreementsConfigProviderTest extends \PHPUnit\Framework\TestCase - 'agreements' => [ - [ - 'content' => $escapedContent, -- 'checkboxText' => $checkboxText, -+ 'checkboxText' => $escapedCheckboxText, - 'mode' => $mode, -- 'agreementId' => $agreementId -- ] -- ] -- ] -+ 'agreementId' => $agreementId, -+ ], -+ ], -+ ], - ]; - - $this->scopeConfigMock->expects($this->once()) -@@ -139,8 +162,11 @@ class AgreementsConfigProviderTest extends \PHPUnit\Framework\TestCase - ->with($searchCriteriaMock) - ->willReturn([$agreement]); - -- $this->escaperMock->expects($this->once())->method('escapeHtml')->with($content)->willReturn($escapedContent); -- -+ $this->escaperMock->expects($this->at(0))->method('escapeHtml')->with($content)->willReturn($escapedContent); -+ $this->escaperMock->expects($this->at(1)) -+ ->method('escapeHtml') -+ ->with($checkboxText) -+ ->willReturn($escapedCheckboxText); - $agreement->expects($this->once())->method('getIsHtml')->willReturn(false); - $agreement->expects($this->once())->method('getContent')->willReturn($content); - $agreement->expects($this->once())->method('getCheckboxText')->willReturn($checkboxText); -diff --git a/app/code/Magento/CheckoutAgreements/etc/db_schema.xml b/app/code/Magento/CheckoutAgreements/etc/db_schema.xml -index 31b3111df98..09cd1c5b639 100644 ---- a/app/code/Magento/CheckoutAgreements/etc/db_schema.xml -+++ b/app/code/Magento/CheckoutAgreements/etc/db_schema.xml -@@ -20,7 +20,7 @@ - default="0" comment="Is Html"/> - <column xsi:type="smallint" name="mode" padding="6" unsigned="false" nullable="false" identity="false" - default="0" comment="Applied mode"/> -- <constraint xsi:type="primary" name="PRIMARY"> -+ <constraint xsi:type="primary" referenceId="PRIMARY"> - <column name="agreement_id"/> - </constraint> - </table> -@@ -29,14 +29,14 @@ - comment="Agreement Id"/> - <column xsi:type="smallint" name="store_id" padding="5" unsigned="true" nullable="false" identity="false" - comment="Store Id"/> -- <constraint xsi:type="primary" name="PRIMARY"> -+ <constraint xsi:type="primary" referenceId="PRIMARY"> - <column name="agreement_id"/> - <column name="store_id"/> - </constraint> -- <constraint xsi:type="foreign" name="CHKT_AGRT_STORE_AGRT_ID_CHKT_AGRT_AGRT_ID" table="checkout_agreement_store" -+ <constraint xsi:type="foreign" referenceId="CHKT_AGRT_STORE_AGRT_ID_CHKT_AGRT_AGRT_ID" table="checkout_agreement_store" - column="agreement_id" referenceTable="checkout_agreement" referenceColumn="agreement_id" - onDelete="CASCADE"/> -- <constraint xsi:type="foreign" name="CHECKOUT_AGREEMENT_STORE_STORE_ID_STORE_STORE_ID" -+ <constraint xsi:type="foreign" referenceId="CHECKOUT_AGREEMENT_STORE_STORE_ID_STORE_STORE_ID" - table="checkout_agreement_store" column="store_id" referenceTable="store" referenceColumn="store_id" - onDelete="CASCADE"/> - </table> -diff --git a/app/code/Magento/CheckoutAgreements/etc/di.xml b/app/code/Magento/CheckoutAgreements/etc/di.xml -index 081e3daa781..a8ff8f5941f 100644 ---- a/app/code/Magento/CheckoutAgreements/etc/di.xml -+++ b/app/code/Magento/CheckoutAgreements/etc/di.xml -@@ -23,7 +23,7 @@ - <type name="Magento\Checkout\Api\GuestPaymentInformationManagementInterface"> - <plugin name="validate-guest-agreements" type="Magento\CheckoutAgreements\Model\Checkout\Plugin\GuestValidation"/> - </type> -- <type name="\Magento\CheckoutAgreements\Model\CheckoutAgreementsList"> -+ <type name="Magento\CheckoutAgreements\Model\CheckoutAgreementsList"> - <arguments> - <argument name="collectionProcessor" xsi:type="object">Magento\CheckoutAgreements\Model\Api\SearchCriteria\CollectionProcessor</argument> - </arguments> -diff --git a/app/code/Magento/CheckoutAgreements/view/frontend/layout/multishipping_checkout_overview.xml b/app/code/Magento/CheckoutAgreements/view/frontend/layout/multishipping_checkout_overview.xml -index 3f742de0177..122160f1a10 100644 ---- a/app/code/Magento/CheckoutAgreements/view/frontend/layout/multishipping_checkout_overview.xml -+++ b/app/code/Magento/CheckoutAgreements/view/frontend/layout/multishipping_checkout_overview.xml -@@ -8,7 +8,7 @@ - <page xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:View/Layout/etc/page_configuration.xsd"> - <body> - <referenceBlock name="checkout_overview"> -- <block class="Magento\CheckoutAgreements\Block\Agreements" name="checkout.multishipping.agreements" as="agreements" template="Magento_CheckoutAgreements::multishipping_agreements.phtml"/> -+ <block class="Magento\CheckoutAgreements\Block\Agreements" name="checkout.multishipping.agreements" as="agreements" template="Magento_CheckoutAgreements::additional_agreements.phtml"/> - </referenceBlock> - </body> - </page> -diff --git a/app/code/Magento/CheckoutAgreements/view/frontend/templates/additional_agreements.phtml b/app/code/Magento/CheckoutAgreements/view/frontend/templates/additional_agreements.phtml -index 28a6e998d8d..9013a39f8e6 100644 ---- a/app/code/Magento/CheckoutAgreements/view/frontend/templates/additional_agreements.phtml -+++ b/app/code/Magento/CheckoutAgreements/view/frontend/templates/additional_agreements.phtml -@@ -4,8 +4,6 @@ - * See COPYING.txt for license details. - */ - --// @codingStandardsIgnoreFile -- - /** - * @var $block \Magento\CheckoutAgreements\Block\Agreements - */ -diff --git a/app/code/Magento/CheckoutAgreements/view/frontend/templates/agreements.phtml b/app/code/Magento/CheckoutAgreements/view/frontend/templates/agreements.phtml -index b0c6384bcc9..5cb256090c1 100644 ---- a/app/code/Magento/CheckoutAgreements/view/frontend/templates/agreements.phtml -+++ b/app/code/Magento/CheckoutAgreements/view/frontend/templates/agreements.phtml -@@ -4,7 +4,7 @@ - * See COPYING.txt for license details. - */ - --// @codingStandardsIgnoreFile -+// phpcs:disable Magento2.Files.LineLength - - ?> - <?php -@@ -17,30 +17,42 @@ - } ?> - <ol id="checkout-agreements" class="agreements checkout items"> - <?php /** @var \Magento\CheckoutAgreements\Api\Data\AgreementInterface $agreement */ ?> -- <?php foreach ($block->getAgreements() as $agreement): ?> -+ <?php foreach ($block->getAgreements() as $agreement) :?> - <li class="item"> -- <div class="checkout-agreement-item-content"<?= ($agreement->getContentHeight() ? ' style="height:' . $agreement->getContentHeight() . '"' : '') ?>> -- <?php if ($agreement->getIsHtml()):?> -- <?= /* @escapeNotVerified */ $agreement->getContent() ?> -- <?php else:?> -- <?= nl2br($block->escapeHtml($agreement->getContent())) ?> -+ <div class="checkout-agreement-item-content"<?= $block->escapeHtmlAttr($agreement->getContentHeight() ? ' style="height:' . $agreement->getContentHeight() . '"' : '') ?>> -+ <?php if ($agreement->getIsHtml()) :?> -+ <?= /* @noEscape */ $agreement->getContent() ?> -+ <?php else :?> -+ <?= $block->escapeHtml(nl2br($agreement->getContent())) ?> - <?php endif; ?> - </div> -- <form id="checkout-agreements-form-<?= /* @escapeNotVerified */ $agreement->getAgreementId() ?>" class="field choice agree required"> -- <?php if($agreement->getMode() == \Magento\CheckoutAgreements\Model\AgreementModeOptions::MODE_MANUAL): ?> -+ <form id="checkout-agreements-form-<?= (int) $agreement->getAgreementId() ?>" class="field choice agree required"> -+ <?php if ($agreement->getMode() == \Magento\CheckoutAgreements\Model\AgreementModeOptions::MODE_MANUAL) :?> - <input type="checkbox" -- id="agreement-<?= /* @escapeNotVerified */ $agreement->getAgreementId() ?>" -- name="agreement[<?= /* @escapeNotVerified */ $agreement->getAgreementId() ?>]" -+ id="agreement-<?= (int) $agreement->getAgreementId() ?>" -+ name="agreement[<?= (int) $agreement->getAgreementId() ?>]" - value="1" - title="<?= $block->escapeHtml($agreement->getCheckboxText()) ?>" - class="checkbox" - data-validate="{required:true}"/> -- <label class="label" for="agreement-<?= /* @escapeNotVerified */ $agreement->getAgreementId() ?>"> -- <span><?= $agreement->getIsHtml() ? $agreement->getCheckboxText() : $block->escapeHtml($agreement->getCheckboxText()) ?></span> -+ <label class="label" for="agreement-<?= (int) $agreement->getAgreementId() ?>"> -+ <span> -+ <?php if ($agreement->getIsHtml()) :?> -+ <?= /* @noEscape */ $agreement->getCheckboxText() ?> -+ <?php else :?> -+ <?= $block->escapeHtml($agreement->getCheckboxText()) ?> -+ <?php endif; ?> -+ </span> - </label> -- <?php elseif($agreement->getMode() == \Magento\CheckoutAgreements\Model\AgreementModeOptions::MODE_AUTO): ?> -- <div id="checkout-agreements-form-<?= /* @escapeNotVerified */ $agreement->getAgreementId() ?>" class="field choice agree"> -- <span><?= $agreement->getIsHtml() ? $agreement->getCheckboxText() : $block->escapeHtml($agreement->getCheckboxText()) ?></span> -+ <?php elseif ($agreement->getMode() == \Magento\CheckoutAgreements\Model\AgreementModeOptions::MODE_AUTO) :?> -+ <div id="checkout-agreements-form-<?= (int) $agreement->getAgreementId() ?>" class="field choice agree"> -+ <span> -+ <?php if ($agreement->getIsHtml()) :?> -+ <?= /* @noEscape */ $agreement->getCheckboxText() ?> -+ <?php else :?> -+ <?= $block->escapeHtml($agreement->getCheckboxText()) ?> -+ <?php endif; ?> -+ </span> - </div> - <?php endif; ?> - </form> -diff --git a/app/code/Magento/CheckoutAgreements/view/frontend/templates/multishipping_agreements.phtml b/app/code/Magento/CheckoutAgreements/view/frontend/templates/multishipping_agreements.phtml -index 3400770f5ce..fb2d5168d21 100644 ---- a/app/code/Magento/CheckoutAgreements/view/frontend/templates/multishipping_agreements.phtml -+++ b/app/code/Magento/CheckoutAgreements/view/frontend/templates/multishipping_agreements.phtml -@@ -4,7 +4,8 @@ - * See COPYING.txt for license details. - */ - --// @codingStandardsIgnoreFile -+// @deprecated -+// phpcs:disable Magento2.Files.LineLength - - ?> - <?php -@@ -17,31 +18,43 @@ - } ?> - <ol id="checkout-agreements" class="agreements checkout items"> - <?php /** @var \Magento\CheckoutAgreements\Api\Data\AgreementInterface $agreement */ ?> -- <?php foreach ($block->getAgreements() as $agreement): ?> -+ <?php foreach ($block->getAgreements() as $agreement) :?> - <li class="item"> -- <div class="checkout-agreement-item-content"<?= ($agreement->getContentHeight() ? ' style="height:' . $agreement->getContentHeight() . '"' : '') ?>> -- <?php if ($agreement->getIsHtml()):?> -- <?= /* @escapeNotVerified */ $agreement->getContent() ?> -- <?php else:?> -- <?= nl2br($block->escapeHtml($agreement->getContent())) ?> -+ <div class="checkout-agreement-item-content"<?= $block->escapeHtmlAttr($agreement->getContentHeight() ? ' style="height:' . $agreement->getContentHeight() . '"' : '') ?>> -+ <?php if ($agreement->getIsHtml()) :?> -+ <?= /* @noEscape */ $agreement->getContent() ?> -+ <?php else :?> -+ <?= $block->escapeHtml(nl2br($agreement->getContent())) ?> - <?php endif; ?> - </div> -- <?php if($agreement->getMode() == \Magento\CheckoutAgreements\Model\AgreementModeOptions::MODE_MANUAL): ?> -- <div id="checkout-agreements-form-<?= /* @escapeNotVerified */ $agreement->getAgreementId() ?>" class="field choice agree required"> -+ <?php if ($agreement->getMode() == \Magento\CheckoutAgreements\Model\AgreementModeOptions::MODE_MANUAL) :?> -+ <div id="checkout-agreements-form-<?= (int) $agreement->getAgreementId() ?>" class="field choice agree required"> - <input type="checkbox" -- id="agreement-<?= /* @escapeNotVerified */ $agreement->getAgreementId() ?>" -- name="agreement[<?= /* @escapeNotVerified */ $agreement->getAgreementId() ?>]" -+ id="agreement-<?= (int) $agreement->getAgreementId() ?>" -+ name="agreement[<?= (int) $agreement->getAgreementId() ?>]" - value="1" - title="<?= $block->escapeHtml($agreement->getCheckboxText()) ?>" - class="checkbox" - data-validate="{required:true}"/> -- <label class="label" for="agreement-<?= /* @escapeNotVerified */ $agreement->getAgreementId() ?>"> -- <span><?= $agreement->getIsHtml() ? $agreement->getCheckboxText() : $block->escapeHtml($agreement->getCheckboxText()) ?></span> -+ <label class="label" for="agreement-<?= (int) $agreement->getAgreementId() ?>"> -+ <span> -+ <?php if ($agreement->getIsHtml()) :?> -+ <?= /* @noEscape */ $agreement->getCheckboxText() ?> -+ <?php else :?> -+ <?= $block->escapeHtml($agreement->getCheckboxText()) ?> -+ <?php endif; ?> -+ </span> - </label> - </div> -- <?php elseif($agreement->getMode() == \Magento\CheckoutAgreements\Model\AgreementModeOptions::MODE_AUTO): ?> -- <div id="checkout-agreements-form-<?= /* @escapeNotVerified */ $agreement->getAgreementId() ?>" class="field choice agree"> -- <span><?= $agreement->getIsHtml() ? $agreement->getCheckboxText() : $block->escapeHtml($agreement->getCheckboxText()) ?></span> -+ <?php elseif ($agreement->getMode() == \Magento\CheckoutAgreements\Model\AgreementModeOptions::MODE_AUTO) :?> -+ <div id="checkout-agreements-form-<?= (int) $agreement->getAgreementId() ?>" class="field choice agree"> -+ <span> -+ <?php if ($agreement->getIsHtml()) :?> -+ <?= /* @noEscape */ $agreement->getCheckboxText() ?> -+ <?php else :?> -+ <?= $block->escapeHtml($agreement->getCheckboxText()) ?> -+ <?php endif; ?> -+ </span> - </div> - <?php endif; ?> - </li> -diff --git a/app/code/Magento/CheckoutAgreements/view/frontend/web/js/model/agreement-validator.js b/app/code/Magento/CheckoutAgreements/view/frontend/web/js/model/agreement-validator.js -index 157923323fd..cbd06b51fe1 100644 ---- a/app/code/Magento/CheckoutAgreements/view/frontend/web/js/model/agreement-validator.js -+++ b/app/code/Magento/CheckoutAgreements/view/frontend/web/js/model/agreement-validator.js -@@ -19,7 +19,7 @@ define([ - * - * @returns {Boolean} - */ -- validate: function () { -+ validate: function (hideError) { - var isValid = true; - - if (!agreementsConfig.isEnabled || $(agreementsInputPath).length === 0) { -@@ -28,7 +28,8 @@ define([ - - $(agreementsInputPath).each(function (index, element) { - if (!$.validator.validateSingleElement(element, { -- errorElement: 'div' -+ errorElement: 'div', -+ hideError: hideError || false - })) { - isValid = false; - } -diff --git a/app/code/Magento/CheckoutAgreements/view/frontend/web/template/checkout/checkout-agreements.html b/app/code/Magento/CheckoutAgreements/view/frontend/web/template/checkout/checkout-agreements.html -index a448537d64e..4b1a68624e5 100644 ---- a/app/code/Magento/CheckoutAgreements/view/frontend/web/template/checkout/checkout-agreements.html -+++ b/app/code/Magento/CheckoutAgreements/view/frontend/web/template/checkout/checkout-agreements.html -@@ -5,17 +5,17 @@ - */ - --> - <div data-role="checkout-agreements"> -- <div class="checkout-agreements" data-bind="visible: isVisible"> -+ <div class="checkout-agreements fieldset" data-bind="visible: isVisible"> - <!-- ko foreach: agreements --> - <!-- ko if: ($parent.isAgreementRequired($data)) --> -- <div class="checkout-agreement required"> -+ <div class="checkout-agreement field choice required"> - <input type="checkbox" class="required-entry" - data-bind="attr: { - 'id': $parent.getCheckboxId($parentContext, agreementId), - 'name': 'agreement[' + agreementId + ']', - 'value': agreementId - }"/> -- <label data-bind="attr: {'for': $parent.getCheckboxId($parentContext, agreementId)}"> -+ <label class="label" data-bind="attr: {'for': $parent.getCheckboxId($parentContext, agreementId)}"> - <button type="button" - class="action action-show" - data-bind="click: function(data, event) { return $parent.showContent(data, event) }" -diff --git a/app/code/Magento/CheckoutAgreementsGraphQl/Model/Resolver/CheckoutAgreements.php b/app/code/Magento/CheckoutAgreementsGraphQl/Model/Resolver/CheckoutAgreements.php -new file mode 100644 -index 00000000000..3daf88226d8 ---- /dev/null -+++ b/app/code/Magento/CheckoutAgreementsGraphQl/Model/Resolver/CheckoutAgreements.php -@@ -0,0 +1,91 @@ -+<?php -+/** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+declare(strict_types=1); -+ -+namespace Magento\CheckoutAgreementsGraphQl\Model\Resolver; -+ -+use Magento\CheckoutAgreements\Model\AgreementModeOptions; -+use Magento\CheckoutAgreements\Model\ResourceModel\Agreement\Collection; -+use Magento\Framework\GraphQl\Config\Element\Field; -+use Magento\Framework\GraphQl\Query\ResolverInterface; -+use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; -+use Magento\CheckoutAgreements\Api\Data\AgreementInterface; -+use Magento\CheckoutAgreements\Model\ResourceModel\Agreement\CollectionFactory; -+use Magento\Framework\App\Config\ScopeConfigInterface; -+use Magento\Store\Model\ScopeInterface; -+use Magento\Store\Model\StoreManagerInterface; -+ -+/** -+ * Checkout Agreements resolver, used for GraphQL request processing -+ */ -+class CheckoutAgreements implements ResolverInterface -+{ -+ /** -+ * @var CollectionFactory -+ */ -+ private $agreementCollectionFactory; -+ -+ /** -+ * @var StoreManagerInterface -+ */ -+ private $storeManager; -+ -+ /** -+ * @var ScopeConfigInterface -+ */ -+ private $scopeConfig; -+ -+ /** -+ * @param CollectionFactory $agreementCollectionFactory -+ * @param StoreManagerInterface $storeManager -+ * @param ScopeConfigInterface $scopeConfig -+ */ -+ public function __construct( -+ CollectionFactory $agreementCollectionFactory, -+ StoreManagerInterface $storeManager, -+ ScopeConfigInterface $scopeConfig -+ ) { -+ $this->agreementCollectionFactory = $agreementCollectionFactory; -+ $this->storeManager = $storeManager; -+ $this->scopeConfig = $scopeConfig; -+ } -+ -+ /** -+ * @inheritdoc -+ */ -+ public function resolve( -+ Field $field, -+ $context, -+ ResolveInfo $info, -+ array $value = null, -+ array $args = null -+ ) { -+ if (!$this->scopeConfig->isSetFlag('checkout/options/enable_agreements', ScopeInterface::SCOPE_STORE)) { -+ return []; -+ } -+ -+ /** @var Collection $agreementsCollection */ -+ $agreementsCollection = $this->agreementCollectionFactory->create(); -+ $agreementsCollection->addStoreFilter($this->storeManager->getStore()->getId()); -+ $agreementsCollection->addFieldToFilter(AgreementInterface::IS_ACTIVE, 1); -+ -+ $checkoutAgreementData = []; -+ /** @var AgreementInterface $checkoutAgreement */ -+ foreach ($agreementsCollection->getItems() as $checkoutAgreement) { -+ $checkoutAgreementData[] = [ -+ AgreementInterface::AGREEMENT_ID => $checkoutAgreement->getAgreementId(), -+ AgreementInterface::CONTENT => $checkoutAgreement->getContent(), -+ AgreementInterface::NAME => $checkoutAgreement->getName(), -+ AgreementInterface::CONTENT_HEIGHT => $checkoutAgreement->getContentHeight(), -+ AgreementInterface::CHECKBOX_TEXT => $checkoutAgreement->getCheckboxText(), -+ AgreementInterface::IS_HTML => $checkoutAgreement->getIsHtml(), -+ AgreementInterface::MODE => -+ AgreementModeOptions::MODE_AUTO === (int)$checkoutAgreement->getMode() ? 'AUTO' : 'MANUAL', -+ ]; -+ } -+ return $checkoutAgreementData; -+ } -+} -diff --git a/app/code/Magento/CheckoutAgreementsGraphQl/README.md b/app/code/Magento/CheckoutAgreementsGraphQl/README.md -new file mode 100644 -index 00000000000..3ef735e3937 ---- /dev/null -+++ b/app/code/Magento/CheckoutAgreementsGraphQl/README.md -@@ -0,0 +1,4 @@ -+# CheckoutAgreementsGraphQl -+ -+**CheckoutAgreementsGraphQl** provides type information for the GraphQl module -+to generate Checkout Agreements fields for Checkout Agreements information endpoints. -diff --git a/app/code/Magento/CheckoutAgreementsGraphQl/composer.json b/app/code/Magento/CheckoutAgreementsGraphQl/composer.json -new file mode 100644 -index 00000000000..5972d48b35e ---- /dev/null -+++ b/app/code/Magento/CheckoutAgreementsGraphQl/composer.json -@@ -0,0 +1,26 @@ -+{ -+ "name": "magento/module-checkout-agreements-graph-ql", -+ "description": "N/A", -+ "type": "magento2-module", -+ "require": { -+ "php": "~7.1.3||~7.2.0", -+ "magento/framework": "*", -+ "magento/module-store": "*", -+ "magento/module-checkout-agreements": "*" -+ }, -+ "suggest": { -+ "magento/module-graph-ql": "*" -+ }, -+ "license": [ -+ "OSL-3.0", -+ "AFL-3.0" -+ ], -+ "autoload": { -+ "files": [ -+ "registration.php" -+ ], -+ "psr-4": { -+ "Magento\\CheckoutAgreementsGraphQl\\": "" -+ } -+ } -+} -diff --git a/app/code/Magento/CheckoutAgreementsGraphQl/etc/module.xml b/app/code/Magento/CheckoutAgreementsGraphQl/etc/module.xml -new file mode 100644 -index 00000000000..d18e8ee17d0 ---- /dev/null -+++ b/app/code/Magento/CheckoutAgreementsGraphQl/etc/module.xml -@@ -0,0 +1,10 @@ -+<?xml version="1.0"?> -+<!-- -+/** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:Module/etc/module.xsd"> -+ <module name="Magento_CheckoutAgreementsGraphQl" /> -+</config> -diff --git a/app/code/Magento/CheckoutAgreementsGraphQl/etc/schema.graphqls b/app/code/Magento/CheckoutAgreementsGraphQl/etc/schema.graphqls -new file mode 100644 -index 00000000000..64ef9411dfc ---- /dev/null -+++ b/app/code/Magento/CheckoutAgreementsGraphQl/etc/schema.graphqls -@@ -0,0 +1,21 @@ -+# Copyright © Magento, Inc. All rights reserved. -+# See COPYING.txt for license details. -+ -+type Query { -+ checkoutAgreements: [CheckoutAgreement] @resolver(class: "Magento\\CheckoutAgreementsGraphQl\\Model\\Resolver\\CheckoutAgreements") @doc(description: "The Checkout Agreements information") -+} -+ -+type CheckoutAgreement @doc(description: "Defines all Checkout Agreement information") { -+ agreement_id: Int! @doc(description: "Checkout Agreement identifier") -+ name: String! @doc(description: "Checkout Agreement name") -+ content: String! @doc(description: "Checkout Agreement content") -+ content_height: String @doc(description: "Checkout Agreement content height") -+ checkbox_text: String! @doc(description: "Checkout Agreement checkbox text") -+ is_html: Boolean! @doc(description: "Is Checkout Agreement content in HTML format") -+ mode: CheckoutAgreementMode! -+} -+ -+enum CheckoutAgreementMode { -+ AUTO -+ MANUAL -+} -diff --git a/app/code/Magento/CheckoutAgreementsGraphQl/registration.php b/app/code/Magento/CheckoutAgreementsGraphQl/registration.php -new file mode 100644 -index 00000000000..b0b4839f33d ---- /dev/null -+++ b/app/code/Magento/CheckoutAgreementsGraphQl/registration.php -@@ -0,0 +1,10 @@ -+<?php -+/** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+declare(strict_types=1); -+ -+use Magento\Framework\Component\ComponentRegistrar; -+ -+ComponentRegistrar::register(ComponentRegistrar::MODULE, 'Magento_CheckoutAgreementsGraphQl', __DIR__); -diff --git a/app/code/Magento/Cms/Block/Adminhtml/Block/Edit/DeleteButton.php b/app/code/Magento/Cms/Block/Adminhtml/Block/Edit/DeleteButton.php -index a7410cac64d..51a9313fdef 100644 ---- a/app/code/Magento/Cms/Block/Adminhtml/Block/Edit/DeleteButton.php -+++ b/app/code/Magento/Cms/Block/Adminhtml/Block/Edit/DeleteButton.php -@@ -13,7 +13,7 @@ use Magento\Framework\View\Element\UiComponent\Control\ButtonProviderInterface; - class DeleteButton extends GenericButton implements ButtonProviderInterface - { - /** -- * @return array -+ * @inheritDoc - */ - public function getButtonData() - { -@@ -24,7 +24,7 @@ class DeleteButton extends GenericButton implements ButtonProviderInterface - 'class' => 'delete', - 'on_click' => 'deleteConfirm(\'' . __( - 'Are you sure you want to do this?' -- ) . '\', \'' . $this->getDeleteUrl() . '\')', -+ ) . '\', \'' . $this->getDeleteUrl() . '\', {"data": {}})', - 'sort_order' => 20, - ]; - } -@@ -32,6 +32,8 @@ class DeleteButton extends GenericButton implements ButtonProviderInterface - } - - /** -+ * URL to send delete requests to. -+ * - * @return string - */ - public function getDeleteUrl() -diff --git a/app/code/Magento/Cms/Block/Adminhtml/Page/Edit/DeleteButton.php b/app/code/Magento/Cms/Block/Adminhtml/Page/Edit/DeleteButton.php -index 1fc599e4c85..b434fd3f5d3 100644 ---- a/app/code/Magento/Cms/Block/Adminhtml/Page/Edit/DeleteButton.php -+++ b/app/code/Magento/Cms/Block/Adminhtml/Page/Edit/DeleteButton.php -@@ -13,7 +13,7 @@ use Magento\Framework\View\Element\UiComponent\Control\ButtonProviderInterface; - class DeleteButton extends GenericButton implements ButtonProviderInterface - { - /** -- * @return array -+ * @inheritDoc - */ - public function getButtonData() - { -@@ -24,7 +24,7 @@ class DeleteButton extends GenericButton implements ButtonProviderInterface - 'class' => 'delete', - 'on_click' => 'deleteConfirm(\'' . __( - 'Are you sure you want to do this?' -- ) . '\', \'' . $this->getDeleteUrl() . '\')', -+ ) . '\', \'' . $this->getDeleteUrl() . '\', {"data": {}})', - 'sort_order' => 20, - ]; - } -@@ -32,6 +32,8 @@ class DeleteButton extends GenericButton implements ButtonProviderInterface - } - - /** -+ * Url to send delete requests to. -+ * - * @return string - */ - public function getDeleteUrl() -diff --git a/app/code/Magento/Cms/Block/Adminhtml/Page/Grid/Renderer/Action/UrlBuilder.php b/app/code/Magento/Cms/Block/Adminhtml/Page/Grid/Renderer/Action/UrlBuilder.php -index 67ae137fb4d..08ba2c3fff3 100644 ---- a/app/code/Magento/Cms/Block/Adminhtml/Page/Grid/Renderer/Action/UrlBuilder.php -+++ b/app/code/Magento/Cms/Block/Adminhtml/Page/Grid/Renderer/Action/UrlBuilder.php -@@ -5,8 +5,9 @@ - */ - namespace Magento\Cms\Block\Adminhtml\Page\Grid\Renderer\Action; - --use Magento\Store\Api\StoreResolverInterface; -- -+/** -+ * Url builder class used to compose dynamic urls. -+ */ - class UrlBuilder - { - /** -@@ -32,15 +33,25 @@ class UrlBuilder - */ - public function getUrl($routePath, $scope, $store) - { -- $this->frontendUrlBuilder->setScope($scope); -- $href = $this->frontendUrlBuilder->getUrl( -- $routePath, -- [ -- '_current' => false, -- '_nosid' => true, -- '_query' => [StoreResolverInterface::PARAM_NAME => $store] -- ] -- ); -+ if ($scope) { -+ $this->frontendUrlBuilder->setScope($scope); -+ $href = $this->frontendUrlBuilder->getUrl( -+ $routePath, -+ [ -+ '_current' => false, -+ '_nosid' => true, -+ '_query' => [\Magento\Store\Model\StoreManagerInterface::PARAM_NAME => $store] -+ ] -+ ); -+ } else { -+ $href = $this->frontendUrlBuilder->getUrl( -+ $routePath, -+ [ -+ '_current' => false, -+ '_nosid' => true -+ ] -+ ); -+ } - - return $href; - } -diff --git a/app/code/Magento/Cms/Block/Block.php b/app/code/Magento/Cms/Block/Block.php -index d0d75ea6911..c611f4b1e9f 100644 ---- a/app/code/Magento/Cms/Block/Block.php -+++ b/app/code/Magento/Cms/Block/Block.php -@@ -84,4 +84,14 @@ class Block extends AbstractBlock implements \Magento\Framework\DataObject\Ident - { - return [\Magento\Cms\Model\Block::CACHE_TAG . '_' . $this->getBlockId()]; - } -+ -+ /** -+ * @inheritdoc -+ */ -+ public function getCacheKeyInfo() -+ { -+ $cacheKeyInfo = parent::getCacheKeyInfo(); -+ $cacheKeyInfo[] = $this->_storeManager->getStore()->getId(); -+ return $cacheKeyInfo; -+ } - } -diff --git a/app/code/Magento/Cms/Block/Widget/Block.php b/app/code/Magento/Cms/Block/Widget/Block.php -index d8bd483fae5..c665f2afc5d 100644 ---- a/app/code/Magento/Cms/Block/Widget/Block.php -+++ b/app/code/Magento/Cms/Block/Widget/Block.php -@@ -3,14 +3,22 @@ - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -+ -+declare(strict_types=1); -+ - namespace Magento\Cms\Block\Widget; - -+use Magento\Framework\DataObject\IdentityInterface; -+use Magento\Framework\Exception\NoSuchEntityException; -+use Magento\Cms\Model\Block as CmsBlock; -+use Magento\Widget\Block\BlockInterface; -+ - /** - * Cms Static Block Widget - * -- * @author Magento Core Team <core@magentocommerce.com> -+ * @author Magento Core Team <core@magentocommerce.com> - */ --class Block extends \Magento\Framework\View\Element\Template implements \Magento\Widget\Block\BlockInterface -+class Block extends \Magento\Framework\View\Element\Template implements BlockInterface, IdentityInterface - { - /** - * @var \Magento\Cms\Model\Template\FilterProvider -@@ -31,6 +39,11 @@ class Block extends \Magento\Framework\View\Element\Template implements \Magento - */ - protected $_blockFactory; - -+ /** -+ * @var CmsBlock -+ */ -+ private $block; -+ - /** - * @param \Magento\Framework\View\Element\Template\Context $context - * @param \Magento\Cms\Model\Template\FilterProvider $filterProvider -@@ -49,8 +62,9 @@ class Block extends \Magento\Framework\View\Element\Template implements \Magento - } - - /** -- * Prepare block text and determine whether block output enabled or not -- * Prevent blocks recursion if needed -+ * Prepare block text and determine whether block output enabled or not. -+ * -+ * Prevent blocks recursion if needed. - * - * @return $this - */ -@@ -65,19 +79,63 @@ class Block extends \Magento\Framework\View\Element\Template implements \Magento - } - self::$_widgetUsageMap[$blockHash] = true; - -- if ($blockId) { -- $storeId = $this->_storeManager->getStore()->getId(); -- /** @var \Magento\Cms\Model\Block $block */ -- $block = $this->_blockFactory->create(); -- $block->setStoreId($storeId)->load($blockId); -- if ($block->isActive()) { -+ $block = $this->getBlock(); -+ -+ if ($block && $block->isActive()) { -+ try { -+ $storeId = $this->getData('store_id') ?? $this->_storeManager->getStore()->getId(); - $this->setText( - $this->_filterProvider->getBlockFilter()->setStoreId($storeId)->filter($block->getContent()) - ); -+ } catch (NoSuchEntityException $e) { - } - } -- - unset(self::$_widgetUsageMap[$blockHash]); - return $this; - } -+ -+ /** -+ * Get identities of the Cms Block -+ * -+ * @return array -+ */ -+ public function getIdentities() -+ { -+ $block = $this->getBlock(); -+ -+ if ($block) { -+ return $block->getIdentities(); -+ } -+ -+ return []; -+ } -+ -+ /** -+ * Get block -+ * -+ * @return CmsBlock|null -+ */ -+ private function getBlock(): ?CmsBlock -+ { -+ if ($this->block) { -+ return $this->block; -+ } -+ -+ $blockId = $this->getData('block_id'); -+ -+ if ($blockId) { -+ try { -+ $storeId = $this->_storeManager->getStore()->getId(); -+ /** @var \Magento\Cms\Model\Block $block */ -+ $block = $this->_blockFactory->create(); -+ $block->setStoreId($storeId)->load($blockId); -+ $this->block = $block; -+ -+ return $block; -+ } catch (NoSuchEntityException $e) { -+ } -+ } -+ -+ return null; -+ } - } -diff --git a/app/code/Magento/Cms/Controller/Adminhtml/Block/Delete.php b/app/code/Magento/Cms/Controller/Adminhtml/Block/Delete.php -index 3aaf40e7d0a..4af6b684496 100644 ---- a/app/code/Magento/Cms/Controller/Adminhtml/Block/Delete.php -+++ b/app/code/Magento/Cms/Controller/Adminhtml/Block/Delete.php -@@ -6,7 +6,9 @@ - */ - namespace Magento\Cms\Controller\Adminhtml\Block; - --class Delete extends \Magento\Cms\Controller\Adminhtml\Block -+use Magento\Framework\App\Action\HttpPostActionInterface; -+ -+class Delete extends \Magento\Cms\Controller\Adminhtml\Block implements HttpPostActionInterface - { - /** - * Delete action -diff --git a/app/code/Magento/Cms/Controller/Adminhtml/Block/Edit.php b/app/code/Magento/Cms/Controller/Adminhtml/Block/Edit.php -index 87560890632..655b3eb5b91 100644 ---- a/app/code/Magento/Cms/Controller/Adminhtml/Block/Edit.php -+++ b/app/code/Magento/Cms/Controller/Adminhtml/Block/Edit.php -@@ -5,7 +5,12 @@ - */ - namespace Magento\Cms\Controller\Adminhtml\Block; - --class Edit extends \Magento\Cms\Controller\Adminhtml\Block -+use Magento\Framework\App\Action\HttpGetActionInterface; -+ -+/** -+ * Edit CMS block action. -+ */ -+class Edit extends \Magento\Cms\Controller\Adminhtml\Block implements HttpGetActionInterface - { - /** - * @var \Magento\Framework\View\Result\PageFactory -diff --git a/app/code/Magento/Cms/Controller/Adminhtml/Block/Index.php b/app/code/Magento/Cms/Controller/Adminhtml/Block/Index.php -index a4096e4d1a4..b7504a5c2b2 100644 ---- a/app/code/Magento/Cms/Controller/Adminhtml/Block/Index.php -+++ b/app/code/Magento/Cms/Controller/Adminhtml/Block/Index.php -@@ -1,12 +1,16 @@ - <?php - /** -- * - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ - namespace Magento\Cms\Controller\Adminhtml\Block; - --class Index extends \Magento\Cms\Controller\Adminhtml\Block -+use Magento\Framework\App\Action\HttpGetActionInterface; -+ -+/** -+ * Index action. -+ */ -+class Index extends \Magento\Cms\Controller\Adminhtml\Block implements HttpGetActionInterface - { - /** - * @var \Magento\Framework\View\Result\PageFactory -diff --git a/app/code/Magento/Cms/Controller/Adminhtml/Block/MassDelete.php b/app/code/Magento/Cms/Controller/Adminhtml/Block/MassDelete.php -index 92bc7ad71f5..ccdddc7c2b5 100644 ---- a/app/code/Magento/Cms/Controller/Adminhtml/Block/MassDelete.php -+++ b/app/code/Magento/Cms/Controller/Adminhtml/Block/MassDelete.php -@@ -6,6 +6,7 @@ - */ - namespace Magento\Cms\Controller\Adminhtml\Block; - -+use Magento\Framework\App\Action\HttpPostActionInterface; - use Magento\Framework\Controller\ResultFactory; - use Magento\Backend\App\Action\Context; - use Magento\Ui\Component\MassAction\Filter; -@@ -14,7 +15,7 @@ use Magento\Cms\Model\ResourceModel\Block\CollectionFactory; - /** - * Class MassDelete - */ --class MassDelete extends \Magento\Backend\App\Action -+class MassDelete extends \Magento\Backend\App\Action implements HttpPostActionInterface - { - /** - * Authorization level of a basic admin session -diff --git a/app/code/Magento/Cms/Controller/Adminhtml/Block/NewAction.php b/app/code/Magento/Cms/Controller/Adminhtml/Block/NewAction.php -index cfac00915c9..5983594f876 100644 ---- a/app/code/Magento/Cms/Controller/Adminhtml/Block/NewAction.php -+++ b/app/code/Magento/Cms/Controller/Adminhtml/Block/NewAction.php -@@ -1,12 +1,16 @@ - <?php - /** -- * - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ - namespace Magento\Cms\Controller\Adminhtml\Block; - --class NewAction extends \Magento\Cms\Controller\Adminhtml\Block -+use Magento\Framework\App\Action\HttpGetActionInterface; -+ -+/** -+ * Create CMS block action. -+ */ -+class NewAction extends \Magento\Cms\Controller\Adminhtml\Block implements HttpGetActionInterface - { - /** - * @var \Magento\Backend\Model\View\Result\ForwardFactory -diff --git a/app/code/Magento/Cms/Controller/Adminhtml/Block/Save.php b/app/code/Magento/Cms/Controller/Adminhtml/Block/Save.php -index 40974b7a4b5..0c6c6470398 100644 ---- a/app/code/Magento/Cms/Controller/Adminhtml/Block/Save.php -+++ b/app/code/Magento/Cms/Controller/Adminhtml/Block/Save.php -@@ -1,11 +1,11 @@ - <?php - /** -- * - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ - namespace Magento\Cms\Controller\Adminhtml\Block; - -+use Magento\Framework\App\Action\HttpPostActionInterface; - use Magento\Backend\App\Action\Context; - use Magento\Cms\Api\BlockRepositoryInterface; - use Magento\Cms\Model\Block; -@@ -14,7 +14,10 @@ use Magento\Framework\App\Request\DataPersistorInterface; - use Magento\Framework\Exception\LocalizedException; - use Magento\Framework\Registry; - --class Save extends \Magento\Cms\Controller\Adminhtml\Block -+/** -+ * Save CMS block action. -+ */ -+class Save extends \Magento\Cms\Controller\Adminhtml\Block implements HttpPostActionInterface - { - /** - * @var DataPersistorInterface -diff --git a/app/code/Magento/Cms/Controller/Adminhtml/Page/Delete.php b/app/code/Magento/Cms/Controller/Adminhtml/Page/Delete.php -index 16c99e9857c..15bb4093053 100644 ---- a/app/code/Magento/Cms/Controller/Adminhtml/Page/Delete.php -+++ b/app/code/Magento/Cms/Controller/Adminhtml/Page/Delete.php -@@ -1,12 +1,16 @@ - <?php - /** -- * - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ - namespace Magento\Cms\Controller\Adminhtml\Page; - --class Delete extends \Magento\Backend\App\Action -+use Magento\Framework\App\Action\HttpPostActionInterface; -+ -+/** -+ * Delete CMS page action. -+ */ -+class Delete extends \Magento\Backend\App\Action implements HttpPostActionInterface - { - /** - * Authorization level of a basic admin session -diff --git a/app/code/Magento/Cms/Controller/Adminhtml/Page/Edit.php b/app/code/Magento/Cms/Controller/Adminhtml/Page/Edit.php -index 6d51c28b6ac..f50fb2b19c0 100644 ---- a/app/code/Magento/Cms/Controller/Adminhtml/Page/Edit.php -+++ b/app/code/Magento/Cms/Controller/Adminhtml/Page/Edit.php -@@ -1,14 +1,17 @@ - <?php - /** -- * - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ - namespace Magento\Cms\Controller\Adminhtml\Page; - -+use Magento\Framework\App\Action\HttpGetActionInterface; - use Magento\Backend\App\Action; - --class Edit extends \Magento\Backend\App\Action -+/** -+ * Edit CMS page action. -+ */ -+class Edit extends \Magento\Backend\App\Action implements HttpGetActionInterface - { - /** - * Authorization level of a basic admin session -diff --git a/app/code/Magento/Cms/Controller/Adminhtml/Page/Index.php b/app/code/Magento/Cms/Controller/Adminhtml/Page/Index.php -index 75f9ad70dc4..d0ee1453eda 100644 ---- a/app/code/Magento/Cms/Controller/Adminhtml/Page/Index.php -+++ b/app/code/Magento/Cms/Controller/Adminhtml/Page/Index.php -@@ -1,15 +1,20 @@ - <?php - /** -- * - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ - namespace Magento\Cms\Controller\Adminhtml\Page; - -+use Magento\Framework\App\Action\HttpGetActionInterface; - use Magento\Backend\App\Action\Context; -+use Magento\Framework\App\ObjectManager; -+use Magento\Framework\App\Request\DataPersistorInterface; - use Magento\Framework\View\Result\PageFactory; - --class Index extends \Magento\Backend\App\Action -+/** -+ * Index action. -+ */ -+class Index extends \Magento\Backend\App\Action implements HttpGetActionInterface - { - /** - * Authorization level of a basic admin session -@@ -23,16 +28,24 @@ class Index extends \Magento\Backend\App\Action - */ - protected $resultPageFactory; - -+ /** -+ * @var DataPersistorInterface -+ */ -+ private $dataPersistor; -+ - /** - * @param Context $context - * @param PageFactory $resultPageFactory -+ * @param DataPersistorInterface $dataPersistor - */ - public function __construct( - Context $context, -- PageFactory $resultPageFactory -+ PageFactory $resultPageFactory, -+ DataPersistorInterface $dataPersistor = null - ) { - parent::__construct($context); - $this->resultPageFactory = $resultPageFactory; -+ $this->dataPersistor = $dataPersistor ?: ObjectManager::getInstance()->get(DataPersistorInterface::class); - } - - /** -@@ -49,8 +62,7 @@ class Index extends \Magento\Backend\App\Action - $resultPage->addBreadcrumb(__('Manage Pages'), __('Manage Pages')); - $resultPage->getConfig()->getTitle()->prepend(__('Pages')); - -- $dataPersistor = $this->_objectManager->get(\Magento\Framework\App\Request\DataPersistorInterface::class); -- $dataPersistor->clear('cms_page'); -+ $this->dataPersistor->clear('cms_page'); - - return $resultPage; - } -diff --git a/app/code/Magento/Cms/Controller/Adminhtml/Page/MassDelete.php b/app/code/Magento/Cms/Controller/Adminhtml/Page/MassDelete.php -index a1d32aa97a3..222849f9760 100644 ---- a/app/code/Magento/Cms/Controller/Adminhtml/Page/MassDelete.php -+++ b/app/code/Magento/Cms/Controller/Adminhtml/Page/MassDelete.php -@@ -5,6 +5,7 @@ - */ - namespace Magento\Cms\Controller\Adminhtml\Page; - -+use Magento\Framework\App\Action\HttpPostActionInterface; - use Magento\Framework\Controller\ResultFactory; - use Magento\Backend\App\Action\Context; - use Magento\Ui\Component\MassAction\Filter; -@@ -13,7 +14,7 @@ use Magento\Cms\Model\ResourceModel\Page\CollectionFactory; - /** - * Class MassDelete - */ --class MassDelete extends \Magento\Backend\App\Action -+class MassDelete extends \Magento\Backend\App\Action implements HttpPostActionInterface - { - /** - * Authorization level of a basic admin session -diff --git a/app/code/Magento/Cms/Controller/Adminhtml/Page/MassDisable.php b/app/code/Magento/Cms/Controller/Adminhtml/Page/MassDisable.php -index a85b8ecd5e5..0a8c667d4d7 100644 ---- a/app/code/Magento/Cms/Controller/Adminhtml/Page/MassDisable.php -+++ b/app/code/Magento/Cms/Controller/Adminhtml/Page/MassDisable.php -@@ -5,6 +5,7 @@ - */ - namespace Magento\Cms\Controller\Adminhtml\Page; - -+use Magento\Framework\App\Action\HttpPostActionInterface; - use Magento\Framework\Controller\ResultFactory; - use Magento\Backend\App\Action\Context; - use Magento\Ui\Component\MassAction\Filter; -@@ -13,7 +14,7 @@ use Magento\Cms\Model\ResourceModel\Page\CollectionFactory; - /** - * Class MassDisable - */ --class MassDisable extends \Magento\Backend\App\Action -+class MassDisable extends \Magento\Backend\App\Action implements HttpPostActionInterface - { - /** - * Authorization level of a basic admin session -diff --git a/app/code/Magento/Cms/Controller/Adminhtml/Page/MassEnable.php b/app/code/Magento/Cms/Controller/Adminhtml/Page/MassEnable.php -index 3f26769e4c9..e2cb8d984e0 100644 ---- a/app/code/Magento/Cms/Controller/Adminhtml/Page/MassEnable.php -+++ b/app/code/Magento/Cms/Controller/Adminhtml/Page/MassEnable.php -@@ -5,6 +5,7 @@ - */ - namespace Magento\Cms\Controller\Adminhtml\Page; - -+use Magento\Framework\App\Action\HttpPostActionInterface; - use Magento\Framework\Controller\ResultFactory; - use Magento\Backend\App\Action\Context; - use Magento\Ui\Component\MassAction\Filter; -@@ -13,7 +14,7 @@ use Magento\Cms\Model\ResourceModel\Page\CollectionFactory; - /** - * Class MassEnable - */ --class MassEnable extends \Magento\Backend\App\Action -+class MassEnable extends \Magento\Backend\App\Action implements HttpPostActionInterface - { - /** - * Authorization level of a basic admin session -diff --git a/app/code/Magento/Cms/Controller/Adminhtml/Page/NewAction.php b/app/code/Magento/Cms/Controller/Adminhtml/Page/NewAction.php -index 407657b05c1..6a4f4951cef 100644 ---- a/app/code/Magento/Cms/Controller/Adminhtml/Page/NewAction.php -+++ b/app/code/Magento/Cms/Controller/Adminhtml/Page/NewAction.php -@@ -6,7 +6,12 @@ - */ - namespace Magento\Cms\Controller\Adminhtml\Page; - --class NewAction extends \Magento\Backend\App\Action -+use Magento\Framework\App\Action\HttpGetActionInterface; -+ -+/** -+ * Create CMS page action. -+ */ -+class NewAction extends \Magento\Backend\App\Action implements HttpGetActionInterface - { - /** - * Authorization level of a basic admin session -diff --git a/app/code/Magento/Cms/Controller/Adminhtml/Page/Save.php b/app/code/Magento/Cms/Controller/Adminhtml/Page/Save.php -index ef4fda60c0f..37cb4575317 100644 ---- a/app/code/Magento/Cms/Controller/Adminhtml/Page/Save.php -+++ b/app/code/Magento/Cms/Controller/Adminhtml/Page/Save.php -@@ -1,17 +1,20 @@ - <?php - /** -- * - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ - namespace Magento\Cms\Controller\Adminhtml\Page; - -+use Magento\Framework\App\Action\HttpPostActionInterface; - use Magento\Backend\App\Action; - use Magento\Cms\Model\Page; - use Magento\Framework\App\Request\DataPersistorInterface; - use Magento\Framework\Exception\LocalizedException; - --class Save extends \Magento\Backend\App\Action -+/** -+ * Save CMS page action. -+ */ -+class Save extends \Magento\Backend\App\Action implements HttpPostActionInterface - { - /** - * Authorization level of a basic admin session -@@ -127,8 +130,8 @@ class Save extends \Magento\Backend\App\Action - /** - * Process result redirect - * -- * @param \Magento\Cms\Api\Data\PageInterface $model -- * @param \Magento\Backend\Model\View\Result\Redirect $resultRedirect -+ * @param \Magento\Cms\Api\Data\PageInterface $model -+ * @param \Magento\Backend\Model\View\Result\Redirect $resultRedirect - * @param array $data - * @return \Magento\Backend\Model\View\Result\Redirect - * @throws LocalizedException -diff --git a/app/code/Magento/Cms/Controller/Adminhtml/Page/Widget/Chooser.php b/app/code/Magento/Cms/Controller/Adminhtml/Page/Widget/Chooser.php -index 1c46ac6807e..340f4b24dd1 100644 ---- a/app/code/Magento/Cms/Controller/Adminhtml/Page/Widget/Chooser.php -+++ b/app/code/Magento/Cms/Controller/Adminhtml/Page/Widget/Chooser.php -@@ -1,14 +1,18 @@ - <?php - /** -- * - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ - namespace Magento\Cms\Controller\Adminhtml\Page\Widget; - -+use Magento\Framework\App\Action\HttpGetActionInterface; -+use Magento\Framework\App\Action\HttpPostActionInterface; - use Magento\Backend\App\Action; - --class Chooser extends \Magento\Backend\App\Action -+/** -+ * Chooser Source action. -+ */ -+class Chooser extends Action implements HttpPostActionInterface, HttpGetActionInterface - { - /** - * Authorization level of a basic admin session -diff --git a/app/code/Magento/Cms/Controller/Adminhtml/Wysiwyg/Images/DeleteFiles.php b/app/code/Magento/Cms/Controller/Adminhtml/Wysiwyg/Images/DeleteFiles.php -index 890c9bf5eae..6f57efad41e 100644 ---- a/app/code/Magento/Cms/Controller/Adminhtml/Wysiwyg/Images/DeleteFiles.php -+++ b/app/code/Magento/Cms/Controller/Adminhtml/Wysiwyg/Images/DeleteFiles.php -@@ -6,11 +6,12 @@ - namespace Magento\Cms\Controller\Adminhtml\Wysiwyg\Images; - - use Magento\Framework\App\Filesystem\DirectoryList; -+use Magento\Framework\App\Action\HttpPostActionInterface; - - /** - * Delete image files. - */ --class DeleteFiles extends \Magento\Cms\Controller\Adminhtml\Wysiwyg\Images -+class DeleteFiles extends \Magento\Cms\Controller\Adminhtml\Wysiwyg\Images implements HttpPostActionInterface - { - /** - * @var \Magento\Framework\Controller\Result\JsonFactory -@@ -79,7 +80,7 @@ class DeleteFiles extends \Magento\Cms\Controller\Adminhtml\Wysiwyg\Images - $filesystem = $this->_objectManager->get(\Magento\Framework\Filesystem::class); - $dir = $filesystem->getDirectoryRead(DirectoryList::MEDIA); - $filePath = $path . '/' . \Magento\Framework\File\Uploader::getCorrectFileName($file); -- if ($dir->isFile($dir->getRelativePath($filePath))) { -+ if ($dir->isFile($dir->getRelativePath($filePath)) && !preg_match('#.htaccess#', $file)) { - $this->getStorage()->deleteFile($filePath); - } - } -diff --git a/app/code/Magento/Cms/Controller/Adminhtml/Wysiwyg/Images/DeleteFolder.php b/app/code/Magento/Cms/Controller/Adminhtml/Wysiwyg/Images/DeleteFolder.php -index a1de11c3c46..5344472a79a 100644 ---- a/app/code/Magento/Cms/Controller/Adminhtml/Wysiwyg/Images/DeleteFolder.php -+++ b/app/code/Magento/Cms/Controller/Adminhtml/Wysiwyg/Images/DeleteFolder.php -@@ -6,12 +6,13 @@ - */ - namespace Magento\Cms\Controller\Adminhtml\Wysiwyg\Images; - -+use Magento\Framework\App\Action\HttpPostActionInterface; - use Magento\Framework\App\Filesystem\DirectoryList; - - /** - * Delete image folder. - */ --class DeleteFolder extends \Magento\Cms\Controller\Adminhtml\Wysiwyg\Images -+class DeleteFolder extends \Magento\Cms\Controller\Adminhtml\Wysiwyg\Images implements HttpPostActionInterface - { - /** - * @var \Magento\Framework\Controller\Result\JsonFactory -diff --git a/app/code/Magento/Cms/Controller/Adminhtml/Wysiwyg/Images/NewFolder.php b/app/code/Magento/Cms/Controller/Adminhtml/Wysiwyg/Images/NewFolder.php -index a7f49e8a431..82d200beb6d 100644 ---- a/app/code/Magento/Cms/Controller/Adminhtml/Wysiwyg/Images/NewFolder.php -+++ b/app/code/Magento/Cms/Controller/Adminhtml/Wysiwyg/Images/NewFolder.php -@@ -6,12 +6,13 @@ - */ - namespace Magento\Cms\Controller\Adminhtml\Wysiwyg\Images; - -+use Magento\Framework\App\Action\HttpPostActionInterface; - use Magento\Framework\App\Filesystem\DirectoryList; - - /** - * Creates new folder. - */ --class NewFolder extends \Magento\Cms\Controller\Adminhtml\Wysiwyg\Images -+class NewFolder extends \Magento\Cms\Controller\Adminhtml\Wysiwyg\Images implements HttpPostActionInterface - { - /** - * @var \Magento\Framework\Controller\Result\JsonFactory -diff --git a/app/code/Magento/Cms/Controller/Adminhtml/Wysiwyg/Images/Upload.php b/app/code/Magento/Cms/Controller/Adminhtml/Wysiwyg/Images/Upload.php -index 5c9aa2243bc..9bad371aa84 100644 ---- a/app/code/Magento/Cms/Controller/Adminhtml/Wysiwyg/Images/Upload.php -+++ b/app/code/Magento/Cms/Controller/Adminhtml/Wysiwyg/Images/Upload.php -@@ -4,14 +4,18 @@ - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -+ -+declare(strict_types=1); -+ - namespace Magento\Cms\Controller\Adminhtml\Wysiwyg\Images; - -+use Magento\Framework\App\Action\HttpPostActionInterface; - use Magento\Framework\App\Filesystem\DirectoryList; - - /** - * Upload image. - */ --class Upload extends \Magento\Cms\Controller\Adminhtml\Wysiwyg\Images -+class Upload extends \Magento\Cms\Controller\Adminhtml\Wysiwyg\Images implements HttpPostActionInterface - { - /** - * @var \Magento\Framework\Controller\Result\JsonFactory -@@ -57,13 +61,20 @@ class Upload extends \Magento\Cms\Controller\Adminhtml\Wysiwyg\Images - __('Directory %1 is not under storage root path.', $path) - ); - } -- $result = $this->getStorage()->uploadFile($path, $this->getRequest()->getParam('type')); -+ $uploaded = $this->getStorage()->uploadFile($path, $this->getRequest()->getParam('type')); -+ $response = [ -+ 'name' => $uploaded['name'], -+ 'type' => $uploaded['type'], -+ 'error' => $uploaded['error'], -+ 'size' => $uploaded['size'], -+ 'file' => $uploaded['file'] -+ ]; - } catch (\Exception $e) { -- $result = ['error' => $e->getMessage(), 'errorcode' => $e->getCode()]; -+ $response = ['error' => $e->getMessage(), 'errorcode' => $e->getCode()]; - } - /** @var \Magento\Framework\Controller\Result\Json $resultJson */ - $resultJson = $this->resultJsonFactory->create(); - -- return $resultJson->setData($result); -+ return $resultJson->setData($response); - } - } -diff --git a/app/code/Magento/Cms/Controller/Index/Index.php b/app/code/Magento/Cms/Controller/Index/Index.php -index c027bd1a2b7..59ed1a6248b 100644 ---- a/app/code/Magento/Cms/Controller/Index/Index.php -+++ b/app/code/Magento/Cms/Controller/Index/Index.php -@@ -5,6 +5,8 @@ - */ - namespace Magento\Cms\Controller\Index; - -+use Magento\Framework\App\Action\HttpPostActionInterface; -+use Magento\Framework\App\Action\HttpGetActionInterface; - use Magento\Framework\App\Action\Context; - use Magento\Framework\App\Config\ScopeConfigInterface; - use Magento\Framework\App\ObjectManager; -@@ -15,8 +17,12 @@ use Magento\Framework\Controller\Result\ForwardFactory; - use Magento\Framework\View\Result\Page as ResultPage; - use Magento\Cms\Helper\Page; - use Magento\Store\Model\ScopeInterface; -+use Magento\Framework\App\Action\Action; - --class Index extends \Magento\Framework\App\Action\Action -+/** -+ * Home page. Needs to be accessible by POST because of the store switching. -+ */ -+class Index extends Action implements HttpGetActionInterface, HttpPostActionInterface - { - /** - * @var ForwardFactory -diff --git a/app/code/Magento/Cms/Controller/Noroute/Index.php b/app/code/Magento/Cms/Controller/Noroute/Index.php -index db84ce9556d..b30beae73dc 100644 ---- a/app/code/Magento/Cms/Controller/Noroute/Index.php -+++ b/app/code/Magento/Cms/Controller/Noroute/Index.php -@@ -6,6 +6,9 @@ - */ - namespace Magento\Cms\Controller\Noroute; - -+/** -+ * @SuppressWarnings(PHPMD.AllPurposeAction) -+ */ - class Index extends \Magento\Framework\App\Action\Action - { - /** -diff --git a/app/code/Magento/Cms/Controller/Page/View.php b/app/code/Magento/Cms/Controller/Page/View.php -index ab02bc5e717..9d5785450ec 100644 ---- a/app/code/Magento/Cms/Controller/Page/View.php -+++ b/app/code/Magento/Cms/Controller/Page/View.php -@@ -6,7 +6,14 @@ - */ - namespace Magento\Cms\Controller\Page; - --class View extends \Magento\Framework\App\Action\Action -+use Magento\Framework\App\Action\HttpPostActionInterface; -+use Magento\Framework\App\Action\HttpGetActionInterface; -+use Magento\Framework\App\Action\Action; -+ -+/** -+ * Custom page for storefront. Needs to be accessible by POST because of the store switching. -+ */ -+class View extends Action implements HttpGetActionInterface, HttpPostActionInterface - { - /** - * @var \Magento\Framework\Controller\Result\ForwardFactory -diff --git a/app/code/Magento/Cms/Helper/Page.php b/app/code/Magento/Cms/Helper/Page.php -index abd260b260b..70e9437235a 100644 ---- a/app/code/Magento/Cms/Helper/Page.php -+++ b/app/code/Magento/Cms/Helper/Page.php -@@ -187,7 +187,7 @@ class Page extends \Magento\Framework\App\Helper\AbstractHelper - { - /** @var \Magento\Cms\Model\Page $page */ - $page = $this->_pageFactory->create(); -- if ($pageId !== null && $pageId !== $page->getId()) { -+ if ($pageId !== null) { - $page->setStoreId($this->_storeManager->getStore()->getId()); - $page->load($pageId); - } -diff --git a/app/code/Magento/Cms/Model/Block.php b/app/code/Magento/Cms/Model/Block.php -index e65675ceee9..0261ef46a49 100644 ---- a/app/code/Magento/Cms/Model/Block.php -+++ b/app/code/Magento/Cms/Model/Block.php -@@ -12,8 +12,8 @@ use Magento\Framework\Model\AbstractModel; - /** - * CMS block model - * -- * @method Block setStoreId(array $storeId) -- * @method array getStoreId() -+ * @method Block setStoreId(int $storeId) -+ * @method int getStoreId() - */ - class Block extends AbstractModel implements BlockInterface, IdentityInterface - { -@@ -41,6 +41,8 @@ class Block extends AbstractModel implements BlockInterface, IdentityInterface - protected $_eventPrefix = 'cms_block'; - - /** -+ * Construct. -+ * - * @return void - */ - protected function _construct() -diff --git a/app/code/Magento/Cms/Model/Page.php b/app/code/Magento/Cms/Model/Page.php -index d950f484cd1..8eefe26236b 100644 ---- a/app/code/Magento/Cms/Model/Page.php -+++ b/app/code/Magento/Cms/Model/Page.php -@@ -16,8 +16,8 @@ use Magento\Framework\Model\AbstractModel; - * Cms Page Model - * - * @api -- * @method Page setStoreId(array $storeId) -- * @method array getStoreId() -+ * @method Page setStoreId(int $storeId) -+ * @method int getStoreId() - * @SuppressWarnings(PHPMD.ExcessivePublicCount) - * @since 100.0.2 - */ -@@ -103,8 +103,7 @@ class Page extends AbstractModel implements PageInterface, IdentityInterface - } - - /** -- * Check if page identifier exist for specific store -- * return page id if page exists -+ * Check if page identifier exist for specific store return page id if page exists - * - * @param string $identifier - * @param int $storeId -@@ -116,8 +115,7 @@ class Page extends AbstractModel implements PageInterface, IdentityInterface - } - - /** -- * Prepare page's statuses. -- * Available event cms_page_get_available_statuses to customize statuses. -+ * Prepare page's statuses, available event cms_page_get_available_statuses to customize statuses. - * - * @return array - */ -@@ -538,7 +536,7 @@ class Page extends AbstractModel implements PageInterface, IdentityInterface - } - - /** -- * {@inheritdoc} -+ * @inheritdoc - * @since 101.0.0 - */ - public function beforeSave() -@@ -571,6 +569,8 @@ class Page extends AbstractModel implements PageInterface, IdentityInterface - } - - /** -+ * Returns scope config. -+ * - * @return ScopeConfigInterface - */ - private function getScopeConfig() -diff --git a/app/code/Magento/Cms/Model/Page/Source/PageLayout.php b/app/code/Magento/Cms/Model/Page/Source/PageLayout.php -index fb759348759..23a452c0fe5 100644 ---- a/app/code/Magento/Cms/Model/Page/Source/PageLayout.php -+++ b/app/code/Magento/Cms/Model/Page/Source/PageLayout.php -@@ -20,6 +20,7 @@ class PageLayout implements OptionSourceInterface - - /** - * @var array -+ * @deprecated since the cache is now handled by \Magento\Theme\Model\PageLayout\Config\Builder::$configFiles - */ - protected $options; - -@@ -34,16 +35,10 @@ class PageLayout implements OptionSourceInterface - } - - /** -- * Get options -- * -- * @return array -+ * @inheritdoc - */ - public function toOptionArray() - { -- if ($this->options !== null) { -- return $this->options; -- } -- - $configOptions = $this->pageLayoutBuilder->getPageLayoutsConfig()->getOptions(); - $options = []; - foreach ($configOptions as $key => $value) { -@@ -54,6 +49,6 @@ class PageLayout implements OptionSourceInterface - } - $this->options = $options; - -- return $this->options; -+ return $options; - } - } -diff --git a/app/code/Magento/Cms/Model/Plugin/Product.php b/app/code/Magento/Cms/Model/Plugin/Product.php -new file mode 100644 -index 00000000000..c8456d5cd6b ---- /dev/null -+++ b/app/code/Magento/Cms/Model/Plugin/Product.php -@@ -0,0 +1,50 @@ -+<?php -+/** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+declare(strict_types=1); -+ -+namespace Magento\Cms\Model\Plugin; -+ -+use Magento\Catalog\Model\Product as CatalogProduct; -+use Magento\Cms\Model\Page; -+ -+/** -+ * Cleaning no-route page cache for the product details page after enabling product that is not assigned to a category -+ */ -+class Product -+{ -+ /** -+ * @var Page -+ */ -+ private $page; -+ -+ /** -+ * @param Page $page -+ */ -+ public function __construct(Page $page) -+ { -+ $this->page = $page; -+ } -+ -+ /** -+ * After get identities -+ * -+ * @param CatalogProduct $product -+ * @param array $identities -+ * @return array -+ */ -+ public function afterGetIdentities(CatalogProduct $product, array $identities) -+ { -+ if ($product->getOrigData('status') > $product->getData('status')) { -+ if (empty($product->getCategoryIds())) { -+ $noRoutePage = $this->page->load(Page::NOROUTE_PAGE_ID); -+ $noRoutePageId = $noRoutePage->getId(); -+ $identities[] = Page::CACHE_TAG . '_' . $noRoutePageId; -+ } -+ } -+ -+ return array_unique($identities); -+ } -+} -diff --git a/app/code/Magento/Cms/Model/ResourceModel/Block.php b/app/code/Magento/Cms/Model/ResourceModel/Block.php -index 9aab54b02bc..30e81771375 100644 ---- a/app/code/Magento/Cms/Model/ResourceModel/Block.php -+++ b/app/code/Magento/Cms/Model/ResourceModel/Block.php -@@ -95,9 +95,11 @@ class Block extends AbstractDb - } - - /** -+ * Get block id. -+ * - * @param AbstractModel $object - * @param mixed $value -- * @param null $field -+ * @param string $field - * @return bool|int|string - * @throws LocalizedException - * @throws \Exception -@@ -183,10 +185,12 @@ class Block extends AbstractDb - $entityMetadata = $this->metadataPool->getMetadata(BlockInterface::class); - $linkField = $entityMetadata->getLinkField(); - -- if ($this->_storeManager->isSingleStoreMode()) { -- $stores = [Store::DEFAULT_STORE_ID]; -- } else { -- $stores = (array)$object->getData('store_id'); -+ $stores = (array)$object->getData('store_id'); -+ $isDefaultStore = $this->_storeManager->isSingleStoreMode() -+ || array_search(Store::DEFAULT_STORE_ID, $stores) !== false; -+ -+ if (!$isDefaultStore) { -+ $stores[] = Store::DEFAULT_STORE_ID; - } - - $select = $this->getConnection()->select() -@@ -196,8 +200,11 @@ class Block extends AbstractDb - 'cb.' . $linkField . ' = cbs.' . $linkField, - [] - ) -- ->where('cb.identifier = ?', $object->getData('identifier')) -- ->where('cbs.store_id IN (?)', $stores); -+ ->where('cb.identifier = ? ', $object->getData('identifier')); -+ -+ if (!$isDefaultStore) { -+ $select->where('cbs.store_id IN (?)', $stores); -+ } - - if ($object->getId()) { - $select->where('cb.' . $entityMetadata->getIdentifierField() . ' <> ?', $object->getId()); -@@ -236,6 +243,8 @@ class Block extends AbstractDb - } - - /** -+ * Save an object. -+ * - * @param AbstractModel $object - * @return $this - * @throws \Exception -diff --git a/app/code/Magento/Cms/Model/Wysiwyg/Config.php b/app/code/Magento/Cms/Model/Wysiwyg/Config.php -index 5db3933dd11..1da7b99c6d8 100644 ---- a/app/code/Magento/Cms/Model/Wysiwyg/Config.php -+++ b/app/code/Magento/Cms/Model/Wysiwyg/Config.php -@@ -89,8 +89,6 @@ class Config extends \Magento\Framework\DataObject implements ConfigInterface - - /** - * @var array -- * @deprecated -- * @see \Magento\Cms\Model\Wysiwyg\Gallery\DefaultConfigProvider - */ - protected $_windowSize; - -@@ -207,6 +205,12 @@ class Config extends \Magento\Framework\DataObject implements ConfigInterface - - if ($this->_authorization->isAllowed('Magento_Cms::media_gallery')) { - $this->configProvider->processGalleryConfig($config); -+ $config->addData( -+ [ -+ 'files_browser_window_width' => $this->_windowSize['width'], -+ 'files_browser_window_height' => $this->_windowSize['height'], -+ ] -+ ); - } - if ($config->getData('add_widgets')) { - $this->configProvider->processWidgetConfig($config); -diff --git a/app/code/Magento/Cms/Model/Wysiwyg/Images/Storage.php b/app/code/Magento/Cms/Model/Wysiwyg/Images/Storage.php -index dfd55f5346e..dfbbce99b65 100644 ---- a/app/code/Magento/Cms/Model/Wysiwyg/Images/Storage.php -+++ b/app/code/Magento/Cms/Model/Wysiwyg/Images/Storage.php -@@ -3,17 +3,24 @@ - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -+ -+declare(strict_types=1); -+ - namespace Magento\Cms\Model\Wysiwyg\Images; - - use Magento\Cms\Helper\Wysiwyg\Images; - use Magento\Framework\App\Filesystem\DirectoryList; - - /** -- * Wysiwyg Images model -+ * Wysiwyg Images model. -+ * -+ * Tightly connected with controllers responsible for managing files so it uses session and is (sort of) a part -+ * of the presentation layer. - * - * @SuppressWarnings(PHPMD.LongVariable) - * @SuppressWarnings(PHPMD.TooManyFields) - * @SuppressWarnings(PHPMD.CouplingBetweenObjects) -+ * @SuppressWarnings(PHPMD.CookieAndSessionMisuse) - * - * @api - * @since 100.0.2 -@@ -270,7 +277,8 @@ class Storage extends \Magento\Framework\DataObject - $collection = $this->getCollection($path) - ->setCollectDirs(true) - ->setCollectFiles(false) -- ->setCollectRecursively(false); -+ ->setCollectRecursively(false) -+ ->setOrder('basename', \Magento\Framework\Data\Collection\Filesystem::SORT_ORDER_ASC); - - $conditions = $this->getConditionsForExcludeDirs(); - -@@ -501,14 +509,6 @@ class Storage extends \Magento\Framework\DataObject - // create thumbnail - $this->resizeFile($targetPath . '/' . $uploader->getUploadedFileName(), true); - -- $result['cookie'] = [ -- 'name' => $this->getSession()->getName(), -- 'value' => $this->getSession()->getSessionId(), -- 'lifetime' => $this->getSession()->getCookieLifetime(), -- 'path' => $this->getSession()->getCookiePath(), -- 'domain' => $this->getSession()->getCookieDomain(), -- ]; -- - return $result; - } - -diff --git a/app/code/Magento/Cms/Test/Mftf/ActionGroup/AdminCMSPageMassActionSelectActionGroup.xml b/app/code/Magento/Cms/Test/Mftf/ActionGroup/AdminCMSPageMassActionSelectActionGroup.xml -new file mode 100644 -index 00000000000..2945538ed5f ---- /dev/null -+++ b/app/code/Magento/Cms/Test/Mftf/ActionGroup/AdminCMSPageMassActionSelectActionGroup.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="AdminCMSPageMassActionSelectActionGroup"> -+ <arguments> -+ <argument name="action" type="string" /> -+ </arguments> -+ <click selector="{{CmsPagesPageActionsSection.massActionsButton}}" stepKey="clickMassActionDropdown"/> -+ <click selector="{{CmsPagesPageActionsSection.massActionsOption(action)}}" stepKey="clickAction"/> -+ <waitForPageLoad stepKey="waitForPageToReload"/> -+ </actionGroup> -+</actionGroups> -diff --git a/app/code/Magento/Cms/Test/Mftf/ActionGroup/AdminOpenCMSPagesGridActionGroup.xml b/app/code/Magento/Cms/Test/Mftf/ActionGroup/AdminOpenCMSPagesGridActionGroup.xml -new file mode 100644 -index 00000000000..2439953cde0 ---- /dev/null -+++ b/app/code/Magento/Cms/Test/Mftf/ActionGroup/AdminOpenCMSPagesGridActionGroup.xml -@@ -0,0 +1,14 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> -+ <actionGroup name="AdminOpenCMSPagesGridActionGroup"> -+ <amOnPage url="{{CmsPagesPage.url}}" stepKey="navigateToCMSPagesGrid"/> -+ <waitForPageLoad stepKey="waitForPageLoad"/> -+ </actionGroup> -+</actionGroups> -diff --git a/app/code/Magento/Cms/Test/Mftf/ActionGroup/AdminOpenCmsBlockActionGroup.xml b/app/code/Magento/Cms/Test/Mftf/ActionGroup/AdminOpenCmsBlockActionGroup.xml -new file mode 100644 -index 00000000000..0f87ee90b7c ---- /dev/null -+++ b/app/code/Magento/Cms/Test/Mftf/ActionGroup/AdminOpenCmsBlockActionGroup.xml -@@ -0,0 +1,16 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> -+ <actionGroup name="AdminOpenCmsBlockActionGroup"> -+ <arguments> -+ <argument name="block_id" type="string"/> -+ </arguments> -+ <amOnPage url="{{AdminEditBlockPage.url(block_id)}}" stepKey="openEditCmsBlock"/> -+ </actionGroup> -+</actionGroups> -diff --git a/app/code/Magento/Cms/Test/Mftf/ActionGroup/AdminOpentCmsBlockActionGroup.xml b/app/code/Magento/Cms/Test/Mftf/ActionGroup/AdminOpentCmsBlockActionGroup.xml -new file mode 100644 -index 00000000000..0f87ee90b7c ---- /dev/null -+++ b/app/code/Magento/Cms/Test/Mftf/ActionGroup/AdminOpentCmsBlockActionGroup.xml -@@ -0,0 +1,16 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> -+ <actionGroup name="AdminOpenCmsBlockActionGroup"> -+ <arguments> -+ <argument name="block_id" type="string"/> -+ </arguments> -+ <amOnPage url="{{AdminEditBlockPage.url(block_id)}}" stepKey="openEditCmsBlock"/> -+ </actionGroup> -+</actionGroups> -diff --git a/app/code/Magento/Cms/Test/Mftf/ActionGroup/AdminSelectCMSPageInGridActionGroup.xml b/app/code/Magento/Cms/Test/Mftf/ActionGroup/AdminSelectCMSPageInGridActionGroup.xml -new file mode 100644 -index 00000000000..4de1157e318 ---- /dev/null -+++ b/app/code/Magento/Cms/Test/Mftf/ActionGroup/AdminSelectCMSPageInGridActionGroup.xml -@@ -0,0 +1,16 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> -+ <actionGroup name="AdminSelectCMSPageInGridActionGroup"> -+ <arguments> -+ <argument name="identifier" type="string"/> -+ </arguments> -+ <checkOption selector="{{CmsPagesPageActionsSection.pageRowCheckboxByIdentifier(identifier)}}" stepKey="selectCmsPageInGrid"/> -+ </actionGroup> -+</actionGroups> -diff --git a/app/code/Magento/Cms/Test/Mftf/ActionGroup/AssertCMSBlockContentActionGroup.xml b/app/code/Magento/Cms/Test/Mftf/ActionGroup/AssertCMSBlockContentActionGroup.xml -index 553d851707b..d2f81c1c24c 100644 ---- a/app/code/Magento/Cms/Test/Mftf/ActionGroup/AssertCMSBlockContentActionGroup.xml -+++ b/app/code/Magento/Cms/Test/Mftf/ActionGroup/AssertCMSBlockContentActionGroup.xml -@@ -6,7 +6,7 @@ - */ - --> - <actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -- xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Test/etc/actionGroupSchema.xsd"> -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> - <actionGroup name="AssertBlockContent"> - <grabValueFrom selector="{{BlockNewPageBasicFieldsSection.blockTitle}}" stepKey="grabTextFromTitle"/> - <assertEquals stepKey="assertTitle" message="pass"> -diff --git a/app/code/Magento/Cms/Test/Mftf/ActionGroup/AssertCMSPageContentActionGroup.xml b/app/code/Magento/Cms/Test/Mftf/ActionGroup/AssertCMSPageContentActionGroup.xml -index f286c9159c6..dde62373902 100644 ---- a/app/code/Magento/Cms/Test/Mftf/ActionGroup/AssertCMSPageContentActionGroup.xml -+++ b/app/code/Magento/Cms/Test/Mftf/ActionGroup/AssertCMSPageContentActionGroup.xml -@@ -6,7 +6,7 @@ - */ - --> - <actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -- xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Test/etc/actionGroupSchema.xsd"> -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> - <actionGroup name="AssertCMSPageContent"> - <grabValueFrom selector="{{CmsNewPagePageBasicFieldsSection.pageTitle}}" stepKey="grabTextFromTitle"/> - <assertEquals stepKey="assertTitle" message="pass"> -@@ -23,4 +23,14 @@ - <executeJS function="(el = document.querySelector('[name=\'identifier\']')) && el['se' + 'tAt' + 'tribute']('data-value', el.value.split('-')[0]);" stepKey="setAttribute" /> - <seeElement selector="{{CmsNewPagePageBasicFieldsSection.duplicatedURLKey(_duplicatedCMSPage.title)}}" stepKey="see"/> - </actionGroup> -+ <actionGroup name="AssertStoreFrontCMSPage"> -+ <arguments> -+ <argument name="cmsTitle" type="string"/> -+ <argument name="cmsContent" type="string"/> -+ <argument name="cmsContentHeading" type="string"/> -+ </arguments> -+ <see selector="{{StorefrontCMSPageSection.title}}" userInput="{{cmsTitle}}" stepKey="seeTitle"/> -+ <see selector="{{StorefrontCMSPageSection.mainTitle}}" userInput="{{cmsContentHeading}}" stepKey="seeContentHeading"/> -+ <see selector="{{StorefrontCMSPageSection.mainContent}}" userInput="{{cmsContent}}" stepKey="seeContent"/> -+ </actionGroup> - </actionGroups> -diff --git a/app/code/Magento/Cms/Test/Mftf/ActionGroup/AssertCMSPageInGridActionGroup.xml b/app/code/Magento/Cms/Test/Mftf/ActionGroup/AssertCMSPageInGridActionGroup.xml -new file mode 100644 -index 00000000000..84feb0a16c4 ---- /dev/null -+++ b/app/code/Magento/Cms/Test/Mftf/ActionGroup/AssertCMSPageInGridActionGroup.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="AssertCMSPageInGridActionGroup"> -+ <arguments> -+ <argument name="cmsPage" type="entity" /> -+ </arguments> -+ -+ <seeElement stepKey="seeElementByCmsPageIdentifier" selector="{{AdminDataGridTableSection.rowTemplateStrict(cmsPage.identifier)}}" /> -+ <see userInput="{{cmsPage.title}}" stepKey="seeCmsPageTitle" selector="{{AdminDataGridTableSection.rowTemplateStrict(cmsPage.identifier)}}" /> -+ </actionGroup> -+</actionGroups> -diff --git a/app/code/Magento/Cms/Test/Mftf/ActionGroup/AssertCMSPageNotFoundOnStorefrontActionGroup.xml b/app/code/Magento/Cms/Test/Mftf/ActionGroup/AssertCMSPageNotFoundOnStorefrontActionGroup.xml -new file mode 100644 -index 00000000000..b92413a3794 ---- /dev/null -+++ b/app/code/Magento/Cms/Test/Mftf/ActionGroup/AssertCMSPageNotFoundOnStorefrontActionGroup.xml -@@ -0,0 +1,13 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> -+ <actionGroup name="AssertCMSPageNotFoundOnStorefrontActionGroup"> -+ <see userInput="Whoops, our bad..." stepKey="seePageErrorNotFound"/> -+ </actionGroup> -+</actionGroups> -diff --git a/app/code/Magento/Cms/Test/Mftf/ActionGroup/AssignBlockToCMSPageActionGroup.xml b/app/code/Magento/Cms/Test/Mftf/ActionGroup/AssignBlockToCMSPageActionGroup.xml -index 3fa72c2d6b5..5720e79e95a 100644 ---- a/app/code/Magento/Cms/Test/Mftf/ActionGroup/AssignBlockToCMSPageActionGroup.xml -+++ b/app/code/Magento/Cms/Test/Mftf/ActionGroup/AssignBlockToCMSPageActionGroup.xml -@@ -6,7 +6,7 @@ - */ - --> - <actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -- xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Test/etc/actionGroupSchema.xsd"> -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> - <actionGroup name="AssignBlockToCMSPage"> - <arguments> - <argument name="Block" defaultValue=""/> -diff --git a/app/code/Magento/Cms/Test/Mftf/ActionGroup/CMSActionGroup.xml b/app/code/Magento/Cms/Test/Mftf/ActionGroup/CMSActionGroup.xml -index 06419356d8e..07e43347d9d 100644 ---- a/app/code/Magento/Cms/Test/Mftf/ActionGroup/CMSActionGroup.xml -+++ b/app/code/Magento/Cms/Test/Mftf/ActionGroup/CMSActionGroup.xml -@@ -6,7 +6,7 @@ - */ - --> - <actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -- xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Test/etc/actionGroupSchema.xsd"> -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> - <actionGroup name="navigateToCreatedCMSPage"> - <arguments> - <argument name="CMSPage" defaultValue=""/> -@@ -44,4 +44,75 @@ - <waitForPageLoad stepKey="waitForPageLoad3"/> - <waitForLoadingMaskToDisappear stepKey="waitForLoadingMaskOfStagingSection" /> - </actionGroup> -+ <actionGroup name="DeleteCMSBlockActionGroup"> -+ <amOnPage url="{{CmsBlocksPage.url}}" stepKey="navigateToCMSPagesGrid"/> -+ <waitForPageLoad stepKey="waitForPageLoad"/> -+ <click selector="{{CmsPagesPageActionsSection.select(_defaultBlock.title)}}" stepKey="ClickOnSelect"/> -+ <click selector="{{CmsPagesPageActionsSection.delete(_defaultBlock.title)}}" stepKey="ClickOnEdit"/> -+ <waitForPageLoad stepKey="waitForPageLoad3"/> -+ <click selector="{{CmsPagesPageActionsSection.deleteConfirm}}" stepKey="ClickToConfirm"/> -+ <waitForPageLoad stepKey="waitForPageLoad4"/> -+ <see userInput="You deleted the block." stepKey="VerifyBlockIsDeleted"/> -+ </actionGroup> -+ <actionGroup name="AddStoreViewToCmsPage" extends="navigateToCreatedCMSPage"> -+ <arguments> -+ <argument name="storeViewName" type="string"/> -+ </arguments> -+ <remove keyForRemoval="clickExpandContentTabForPage"/> -+ <remove keyForRemoval="waitForLoadingMaskOfStagingSection"/> -+ <click selector="{{CmsNewPagePiwSection.header}}" stepKey="clickPageInWebsites" after="waitForPageLoad3"/> -+ <waitForElementVisible selector="{{CmsNewPagePiwSection.selectStoreView(storeViewName)}}" stepKey="waitForStoreGridReload"/> -+ <clickWithLeftButton selector="{{CmsNewPagePiwSection.selectStoreView(storeViewName)}}" stepKey="clickStoreView"/> -+ <click selector="{{CmsNewPagePageActionsSection.expandSplitButton}}" stepKey="expandButtonMenu"/> -+ <waitForElementVisible selector="{{CmsNewPagePageActionsSection.splitButtonMenu}}" stepKey="waitForSplitButtonMenuVisible"/> -+ <click selector="{{CmsNewPagePageActionsSection.savePage}}" stepKey="clickSavePage"/> -+ <waitForPageLoad stepKey="waitForPageLoad"/> -+ <see userInput="You saved the page." stepKey="seeMessage"/> -+ </actionGroup> -+ <actionGroup name="saveAndCloseCMSBlockWithSplitButton"> -+ <waitForElementVisible selector="{{BlockNewPagePageActionsSection.expandSplitButton}}" stepKey="waitForExpandSplitButtonToBeVisible" /> -+ <click selector="{{BlockNewPagePageActionsSection.expandSplitButton}}" stepKey="expandSplitButton"/> -+ <click selector="{{BlockNewPagePageActionsSection.saveAndClose}}" stepKey="clickSaveBlock"/> -+ <waitForPageLoad stepKey="waitForPageLoadAfterClickingSave" /> -+ <see userInput="You saved the block." stepKey="assertSaveBlockSuccessMessage"/> -+ </actionGroup> -+ <actionGroup name="navigateToStorefrontForCreatedPage"> -+ <arguments> -+ <argument name="page" type="string"/> -+ </arguments> -+ <amOnPage url="{{page}}" stepKey="goToStorefront"/> -+ <waitForPageLoad stepKey="waitForPageLoad"/> -+ </actionGroup> -+ <actionGroup name="saveCMSBlock"> -+ <waitForElementVisible selector="{{CmsNewBlockBlockActionsSection.savePage}}" stepKey="waitForSaveButton"/> -+ <click selector="{{CmsNewBlockBlockActionsSection.savePage}}" stepKey="clickSaveButton"/> -+ <waitForPageLoad stepKey="waitForPageLoad"/> -+ <see userInput="You saved the block." stepKey="seeSuccessfulSaveMessage"/> -+ </actionGroup> -+ <actionGroup name="saveAndContinueEditCmsPage"> -+ <waitForElementVisible time="10" selector="{{CmsNewPagePageActionsSection.saveAndContinueEdit}}" stepKey="waitForSaveAndContinueVisibility"/> -+ <click selector="{{CmsNewPagePageActionsSection.saveAndContinueEdit}}" stepKey="clickSaveAndContinueEditCmsPage"/> -+ <waitForPageLoad stepKey="waitForCmsPageLoad"/> -+ <waitForElementVisible time="1" selector="{{CmsNewPagePageActionsSection.cmsPageTitle}}" stepKey="waitForCmsPageSaveButton"/> -+ <waitForLoadingMaskToDisappear stepKey="waitForLoadingMask"/> -+ </actionGroup> -+ <actionGroup name="saveCmsPage"> -+ <waitForElementVisible selector="{{CmsNewPagePageActionsSection.expandSplitButton}}" stepKey="waitForSplitButton"/> -+ <click selector="{{CmsNewPagePageActionsSection.expandSplitButton}}" stepKey="expandSplitButton"/> -+ <waitForElementVisible selector="{{CmsNewPagePageActionsSection.savePage}}" stepKey="waitForSaveCmsPage"/> -+ <click selector="{{CmsNewPagePageActionsSection.savePage}}" stepKey="clickSaveCmsPage"/> -+ <waitForElementVisible time="1" selector="{{CmsPagesPageActionsSection.addNewPageButton}}" stepKey="waitForCmsPageSaveButton"/> -+ <see userInput="You saved the page." selector="{{CmsPagesPageActionsSection.savePageSuccessMessage}}" stepKey="assertSavePageSuccessMessage"/> -+ </actionGroup> -+ <actionGroup name="setLayout"> -+ <arguments> -+ <argument name="designSection"/> -+ <argument name="layoutOption"/> -+ </arguments> -+ <waitForElementVisible selector="{{designSection.DesignTab}}" stepKey="waitForDesignTabVisible"/> -+ <conditionalClick selector="{{designSection.DesignTab}}" dependentSelector="{{designSection.LayoutDropdown}}" visible="false" stepKey="clickOnDesignTab"/> -+ <waitForPageLoad stepKey="waitForPageLoadDesignTab"/> -+ <waitForElementVisible selector="{{designSection.LayoutDropdown}}" stepKey="waitForLayoutDropDown" /> -+ <selectOption selector="{{designSection.LayoutDropdown}}" userInput="{{layoutOption}}" stepKey="selectLayout"/> -+ </actionGroup> - </actionGroups> -diff --git a/app/code/Magento/Cms/Test/Mftf/ActionGroup/ClearWidgetsFromCMSContentActionGroup.xml b/app/code/Magento/Cms/Test/Mftf/ActionGroup/ClearWidgetsFromCMSContentActionGroup.xml -new file mode 100644 -index 00000000000..2c45b9e140c ---- /dev/null -+++ b/app/code/Magento/Cms/Test/Mftf/ActionGroup/ClearWidgetsFromCMSContentActionGroup.xml -@@ -0,0 +1,33 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> -+ <actionGroup name="ClearWidgetsFromCMSContent"> -+ <amOnPage url="{{CmsPageEditPage.url('2')}}" stepKey="navigateToEditHomePagePage"/> -+ <waitForPageLoad stepKey="waitEditHomePagePageToLoad"/> -+ <click selector="{{CmsNewPagePageContentSection.header}}" stepKey="clickContentTab" /> -+ <waitForElementNotVisible selector="{{CmsWYSIWYGSection.CheckIfTabExpand}}" stepKey="waitForTabExpand"/> -+ <executeJS function="jQuery('[id=\'cms_page_form_content_ifr\']').attr('name', 'preview-iframe')" stepKey="setPreviewFrameName"/> -+ <switchToIFrame selector="preview-iframe" stepKey="switchToIframe"/> -+ <fillField selector="{{TinyMCESection.EditorContent}}" userInput="Hello TinyMCE4!" stepKey="clearWidgets"/> -+ <switchToIFrame stepKey="switchOutFromIframe"/> -+ <executeJS function="tinyMCE.activeEditor.setContent('Hello TinyMCE4!');" stepKey="executeJSFillContent1"/> -+ <click selector="{{InsertWidgetSection.save}}" stepKey="saveWidget"/> -+ <waitForPageLoad stepKey="waitSaveToBeApplied"/> -+ <see selector="{{AdminProductMessagesSection.successMessage}}" userInput="You saved the page." stepKey="seeSaveSuccess"/> -+ </actionGroup> -+ <actionGroup name="ClearWidgetsForCMSHomePageContentWYSIWYGDisabled"> -+ <amOnPage url="{{CmsPageEditPage.url('2')}}" stepKey="navigateToEditHomePagePage"/> -+ <waitForPageLoad stepKey="waitForCmsPageEditPage"/> -+ <conditionalClick selector="{{CmsNewPagePageActionsSection.contentSectionName}}" dependentSelector="{{CatalogWidgetSection.insertWidgetButton}}" visible="false" stepKey="clickShowHideEditorIfVisible"/> -+ <waitForElementVisible selector="{{CmsNewPagePageContentSection.content}}" stepKey="waitForContentField"/> -+ <fillField selector="{{CmsNewPagePageContentSection.content}}" userInput="CMS homepage content goes here." stepKey="resetCMSPageToDefaultContent"/> -+ <click selector="{{CmsNewPagePageActionsSection.saveAndContinueEdit}}" stepKey="clickSave"/> -+ <waitForPageLoad stepKey="waitForSettingsApply"/> -+ </actionGroup> -+</actionGroups> -diff --git a/app/code/Magento/Cms/Test/Mftf/ActionGroup/CreateNewPageWithAllValuesActionGroup.xml b/app/code/Magento/Cms/Test/Mftf/ActionGroup/CreateNewPageWithAllValuesActionGroup.xml -index 2225d3d34a6..a459c41ccb4 100644 ---- a/app/code/Magento/Cms/Test/Mftf/ActionGroup/CreateNewPageWithAllValuesActionGroup.xml -+++ b/app/code/Magento/Cms/Test/Mftf/ActionGroup/CreateNewPageWithAllValuesActionGroup.xml -@@ -6,7 +6,7 @@ - */ - --> - <actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -- xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Test/etc/actionGroupSchema.xsd"> -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> - <actionGroup name="CreateNewPageWithAllValues"> - <arguments> - <argument name="PageTitle" type="string"/> -@@ -28,4 +28,10 @@ - <click selector="{{CmsNewPageHierarchySection.header}}" stepKey="clickHierarchy"/> - <click selector="{{CmsNewPageHierarchySection.selectHierarchy(selectHierarchyOpt)}}" stepKey="clickPageCheckBoxes"/> - </actionGroup> -+ <actionGroup name="CreateNewPageWithAllValuesAndContent" extends="CreateNewPageWithAllValues"> -+ <arguments> -+ <argument name="pageContent" type="string"/> -+ </arguments> -+ <fillField selector="{{CmsNewPagePageContentSection.content}}" userInput="{{pageContent}}" stepKey="fillContentField" after="fillFieldContentHeading"/> -+ </actionGroup> - </actionGroups> -\ No newline at end of file -diff --git a/app/code/Magento/Cms/Test/Mftf/ActionGroup/CreateNewPageWithWidgetActionGroup.xml b/app/code/Magento/Cms/Test/Mftf/ActionGroup/CreateNewPageWithWidgetActionGroup.xml -new file mode 100644 -index 00000000000..a4b88c544de ---- /dev/null -+++ b/app/code/Magento/Cms/Test/Mftf/ActionGroup/CreateNewPageWithWidgetActionGroup.xml -@@ -0,0 +1,40 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> -+ <actionGroup name="CreateNewPageWithWidget"> -+ <arguments> -+ <argument name="pageTitle" type="string" defaultValue="{{defaultCmsPage.title}}"/> -+ <argument name="category" type="string"/> -+ <argument name="condition" type="string"/> -+ <argument name="widgetType" type="string"/> -+ </arguments> -+ <amOnPage url="{{CmsNewPagePage.url}}" stepKey="amOnCMSNewPage"/> -+ <waitForPageLoad stepKey="waitForPageLoad"/> -+ <fillField selector="{{CmsNewPagePageBasicFieldsSection.pageTitle}}" userInput="{{pageTitle}}" stepKey="fillFieldTitle"/> -+ <click selector="{{CmsNewPagePageContentSection.header}}" stepKey="clickExpandContent"/> -+ <click selector="{{CmsNewPagePageActionsSection.insertWidget}}" stepKey="clickToInsertWidget"/> -+ <waitForPageLoad stepKey="waitForPageLoad1"/> -+ <waitForElementVisible stepKey="waitForInsertWidgetTitle" selector="{{WidgetSection.InsertWidgetTitle}}"/> -+ <selectOption selector="{{WidgetSection.WidgetType}}" userInput="{{widgetType}}" stepKey="selectCatalogProductsList"/> -+ <waitForElementVisible selector="{{WidgetSection.AddParam}}" stepKey="waitForAddParam"/> -+ <scrollTo selector="{{WidgetSection.AddParam}}" stepKey="scrollToAddParamElement"/> -+ <click selector="{{WidgetSection.AddParam}}" stepKey="addParam"/> -+ <selectOption selector="{{WidgetSection.ConditionsDropdown}}" userInput="{{condition}}" stepKey="selectCategory"/> -+ <waitForElementVisible selector="{{WidgetSection.RuleParam}}" stepKey="waitForRuleParam"/> -+ <click selector="{{WidgetSection.RuleParam}}" stepKey="clickToAddRuleParam"/> -+ <click selector="{{WidgetSection.Chooser}}" stepKey="clickToSelectFromList"/> -+ <waitForPageLoad stepKey="waitForPageLoad2"/> -+ <click selector="{{WidgetSection.PreCreateCategory(category)}}" stepKey="selectPreCategory" /> -+ <click selector="{{WidgetSection.InsertWidget}}" stepKey="clickToSaveInsertedWidget"/> -+ <waitForPageLoad stepKey="waitForPageLoad3"/> -+ <click selector="{{CmsNewBlockBlockActionsSection.savePage}}" stepKey="saveCMSPage"/> -+ <waitForElementVisible selector="{{CmsPagesPageActionsSection.savePageSuccessMessage}}" stepKey="waitForSuccessMessageLoggedOut" time="5"/> -+ <see userInput="You saved the page." stepKey="seeSuccessMessage"/> -+ </actionGroup> -+</actionGroups> -diff --git a/app/code/Magento/Cms/Test/Mftf/ActionGroup/DeleteImageFromStorageActionGroup.xml b/app/code/Magento/Cms/Test/Mftf/ActionGroup/DeleteImageFromStorageActionGroup.xml -index cbd239cde80..6de6f27e106 100644 ---- a/app/code/Magento/Cms/Test/Mftf/ActionGroup/DeleteImageFromStorageActionGroup.xml -+++ b/app/code/Magento/Cms/Test/Mftf/ActionGroup/DeleteImageFromStorageActionGroup.xml -@@ -6,7 +6,7 @@ - */ - --> - <actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -- xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Test/etc/actionGroupSchema.xsd"> -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> - <actionGroup name="DeleteImageFromStorageActionGroup"> - <arguments> - <argument name="Image"/> -diff --git a/app/code/Magento/Cms/Test/Mftf/ActionGroup/DeletePageByUrlKeyActionGroup.xml b/app/code/Magento/Cms/Test/Mftf/ActionGroup/DeletePageByUrlKeyActionGroup.xml -index 690ad9881c7..2a2b2ff15d3 100644 ---- a/app/code/Magento/Cms/Test/Mftf/ActionGroup/DeletePageByUrlKeyActionGroup.xml -+++ b/app/code/Magento/Cms/Test/Mftf/ActionGroup/DeletePageByUrlKeyActionGroup.xml -@@ -6,7 +6,7 @@ - */ - --> - <actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -- xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Test/etc/actionGroupSchema.xsd"> -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> - <actionGroup name="DeletePageByUrlKeyActionGroup"> - <arguments> - <argument name="UrlKey" type="string"/> -diff --git a/app/code/Magento/Cms/Test/Mftf/ActionGroup/FillOutBlockContentActionGroup.xml b/app/code/Magento/Cms/Test/Mftf/ActionGroup/FillOutBlockContentActionGroup.xml -index ef7c925c3f8..3ffc999b41a 100644 ---- a/app/code/Magento/Cms/Test/Mftf/ActionGroup/FillOutBlockContentActionGroup.xml -+++ b/app/code/Magento/Cms/Test/Mftf/ActionGroup/FillOutBlockContentActionGroup.xml -@@ -6,7 +6,7 @@ - */ - --> - <actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -- xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Test/etc/actionGroupSchema.xsd"> -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> - <actionGroup name="FillOutBlockContent"> - <fillField selector="{{BlockNewPageBasicFieldsSection.blockTitle}}" userInput="{{_defaultBlock.title}}" stepKey="fillFieldTitle1"/> - <fillField selector="{{BlockNewPageBasicFieldsSection.identifier}}" userInput="{{_defaultBlock.identifier}}" stepKey="fillFieldIdentifier"/> -diff --git a/app/code/Magento/Cms/Test/Mftf/ActionGroup/FillOutCMSPageContentActionGroup.xml b/app/code/Magento/Cms/Test/Mftf/ActionGroup/FillOutCMSPageContentActionGroup.xml -index 5caeadcea28..e47ff472ccd 100644 ---- a/app/code/Magento/Cms/Test/Mftf/ActionGroup/FillOutCMSPageContentActionGroup.xml -+++ b/app/code/Magento/Cms/Test/Mftf/ActionGroup/FillOutCMSPageContentActionGroup.xml -@@ -6,7 +6,7 @@ - */ - --> - <actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -- xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Test/etc/actionGroupSchema.xsd"> -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> - <actionGroup name="FillOutCMSPageContent"> - <fillField selector="{{CmsNewPagePageBasicFieldsSection.pageTitle}}" userInput="{{_duplicatedCMSPage.title}}" stepKey="fillFieldTitle"/> - <click selector="{{CmsNewPagePageContentSection.header}}" stepKey="clickExpandContentTabForPage"/> -diff --git a/app/code/Magento/Cms/Test/Mftf/ActionGroup/NavigateToMediaFolderActionGroup.xml b/app/code/Magento/Cms/Test/Mftf/ActionGroup/NavigateToMediaFolderActionGroup.xml -index 031481d90d1..3c447f808e7 100644 ---- a/app/code/Magento/Cms/Test/Mftf/ActionGroup/NavigateToMediaFolderActionGroup.xml -+++ b/app/code/Magento/Cms/Test/Mftf/ActionGroup/NavigateToMediaFolderActionGroup.xml -@@ -6,7 +6,7 @@ - */ - --> - <actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -- xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Test/etc/actionGroupSchema.xsd"> -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> - <actionGroup name="NavigateToMediaFolderActionGroup"> - <arguments> - <argument name="FolderName" type="string"/> -diff --git a/app/code/Magento/Cms/Test/Mftf/ActionGroup/RestoreLayoutSettingActionGroup.xml b/app/code/Magento/Cms/Test/Mftf/ActionGroup/RestoreLayoutSettingActionGroup.xml -index 54c41647491..3016fba6cab 100644 ---- a/app/code/Magento/Cms/Test/Mftf/ActionGroup/RestoreLayoutSettingActionGroup.xml -+++ b/app/code/Magento/Cms/Test/Mftf/ActionGroup/RestoreLayoutSettingActionGroup.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"> - <actionGroup name="RestoreLayoutSetting"> - <waitForElementVisible selector="{{DefaultLayoutsSection.pageLayout}}" stepKey="waittForDefaultCMSLayout" after="expandDefaultLayouts" /> - <selectOption selector="{{DefaultLayoutsSection.pageLayout}}" userInput="1 column" stepKey="selectOneColumn" before="clickSaveConfig"/> -diff --git a/app/code/Magento/Cms/Test/Mftf/ActionGroup/SearchBlockOnGridPageActionGroup.xml b/app/code/Magento/Cms/Test/Mftf/ActionGroup/SearchBlockOnGridPageActionGroup.xml -index 8656f4e03a2..12205aeed8f 100644 ---- a/app/code/Magento/Cms/Test/Mftf/ActionGroup/SearchBlockOnGridPageActionGroup.xml -+++ b/app/code/Magento/Cms/Test/Mftf/ActionGroup/SearchBlockOnGridPageActionGroup.xml -@@ -6,7 +6,7 @@ - */ - --> - <actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -- xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Test/etc/actionGroupSchema.xsd"> -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> - <actionGroup name="searchBlockOnGridPage"> - <arguments> - <argument name="Block" defaultValue=""/> -@@ -16,4 +16,27 @@ - <waitForLoadingMaskToDisappear stepKey="waitForSecondIdSortDescendingToFinish2" /> - <waitForElementVisible selector="{{WidgetSection.BlockPage(Block.identifier)}}" stepKey="waitForBlockTitle" /> - </actionGroup> -+ <actionGroup name ="deleteBlock"> -+ <arguments> -+ <argument name="Block" defaultValue=""/> -+ </arguments> -+ <amOnPage url="{{CmsBlocksPage.url}}" stepKey="navigateToCMSBlocksGrid"/> -+ <waitForPageLoad stepKey="waitForPageLoad1"/> -+ <conditionalClick selector="{{BlockPageActionsSection.clearAll}}" dependentSelector="{{BlockPageActionsSection.activeFilters}}" stepKey="clickToResetFilter" visible="true"/> -+ <waitForPageLoad stepKey="waitForPageLoad2"/> -+ <click selector="{{BlockPageActionsSection.FilterBtn}}" stepKey="clickFilterBtn"/> -+ <fillField selector="{{BlockPageActionsSection.URLKey}}" userInput="{{Block.identifier}}" stepKey="fillBlockIdentifierInput"/> -+ <click selector="{{BlockPageActionsSection.ApplyFiltersBtn}}" stepKey="applyFilter"/> -+ <waitForLoadingMaskToDisappear stepKey="waitForGridToLoadResults" /> -+ <waitForElementVisible selector="{{BlockPageActionsSection.select(Block.identifier)}}" stepKey="waitForCMSPageGrid" /> -+ <click selector="{{BlockPageActionsSection.select(Block.identifier)}}" stepKey="clickSelect" /> -+ <waitForElementVisible selector="{{BlockPageActionsSection.edit(Block.identifier)}}" stepKey="waitForEditLink" /> -+ <click selector="{{BlockPageActionsSection.edit(Block.identifier)}}" stepKey="clickEdit" /> -+ <waitForLoadingMaskToDisappear stepKey="waitForPageToLoad" /> -+ <click selector="{{CmsBlockBlockActionSection.deleteBlock}}" stepKey="deleteBlock"/> -+ <waitForElementVisible selector="{{CmsBlockBlockActionSection.deleteConfirm}}" stepKey="waitForOkButtonToBeVisible"/> -+ <click selector="{{CmsBlockBlockActionSection.deleteConfirm}}" stepKey="clickOkButton"/> -+ <waitForPageLoad stepKey="waitForPageLoad3"/> -+ <see userInput="You deleted the block." stepKey="seeSuccessMessage"/> -+ </actionGroup> - </actionGroups> -diff --git a/app/code/Magento/Cms/Test/Mftf/ActionGroup/SelectImageFromMediaStorageActionGroup.xml b/app/code/Magento/Cms/Test/Mftf/ActionGroup/SelectImageFromMediaStorageActionGroup.xml -index 2daa1cbeca8..84704b18a40 100644 ---- a/app/code/Magento/Cms/Test/Mftf/ActionGroup/SelectImageFromMediaStorageActionGroup.xml -+++ b/app/code/Magento/Cms/Test/Mftf/ActionGroup/SelectImageFromMediaStorageActionGroup.xml -@@ -6,7 +6,7 @@ - */ - --> - <actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -- xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Test/etc/actionGroupSchema.xsd"> -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> - <actionGroup name="clickBrowseBtnOnUploadPopup"> - <click selector="{{MediaGallerySection.Browse}}" stepKey="clickBrowse" /> - <waitForPageLoad stepKey="waitForPageLoad1" /> -diff --git a/app/code/Magento/Cms/Test/Mftf/ActionGroup/StorefrontGoToCMSPageActionGroup.xml b/app/code/Magento/Cms/Test/Mftf/ActionGroup/StorefrontGoToCMSPageActionGroup.xml -new file mode 100644 -index 00000000000..0ec8db3f27c ---- /dev/null -+++ b/app/code/Magento/Cms/Test/Mftf/ActionGroup/StorefrontGoToCMSPageActionGroup.xml -@@ -0,0 +1,17 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> -+ <actionGroup name="StorefrontGoToCMSPageActionGroup"> -+ <arguments> -+ <argument name="identifier" type="string"/> -+ </arguments> -+ <amOnPage url="{{StorefrontHomePage.url}}{{identifier}}" stepKey="amOnCmsPageOnStorefront"/> -+ <waitForPageLoad stepKey="waitForPageLoadOnStorefront"/> -+ </actionGroup> -+</actionGroups> -\ No newline at end of file -diff --git a/app/code/Magento/Cms/Test/Mftf/ActionGroup/VerifyTinyMCEActionGroup.xml b/app/code/Magento/Cms/Test/Mftf/ActionGroup/VerifyTinyMCEActionGroup.xml -index 6c4a42d6440..ed19c291aad 100644 ---- a/app/code/Magento/Cms/Test/Mftf/ActionGroup/VerifyTinyMCEActionGroup.xml -+++ b/app/code/Magento/Cms/Test/Mftf/ActionGroup/VerifyTinyMCEActionGroup.xml -@@ -6,7 +6,7 @@ - */ - --> - <actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -- xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Test/etc/actionGroupSchema.xsd"> -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> - <actionGroup name="VerifyTinyMCEActionGroup"> - <seeElement selector="{{TinyMCESection.Style}}" stepKey="assertInfo2"/> - <seeElement selector="{{TinyMCESection.Bold}}" stepKey="assertInfo3"/> -diff --git a/app/code/Magento/Cms/Test/Mftf/Data/AdminMenuData.xml b/app/code/Magento/Cms/Test/Mftf/Data/AdminMenuData.xml -new file mode 100644 -index 00000000000..3e227df56c9 ---- /dev/null -+++ b/app/code/Magento/Cms/Test/Mftf/Data/AdminMenuData.xml -@@ -0,0 +1,26 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<entities xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataProfileSchema.xsd"> -+ <entity name="AdminMenuContent"> -+ <data key="pageTitle">Content</data> -+ <data key="title">Content</data> -+ <data key="dataUiId">magento-backend-content</data> -+ </entity> -+ <entity name="AdminMenuContentElementsPages"> -+ <data key="pageTitle">Pages</data> -+ <data key="title">Pages</data> -+ <data key="dataUiId">magento-cms-cms-page</data> -+ </entity> -+ <entity name="AdminMenuContentElementsBlocks"> -+ <data key="pageTitle">Blocks</data> -+ <data key="title">Blocks</data> -+ <data key="dataUiId">magento-cms-cms-block</data> -+ </entity> -+</entities> -diff --git a/app/code/Magento/Cms/Test/Mftf/Data/BlockData.xml b/app/code/Magento/Cms/Test/Mftf/Data/BlockData.xml -new file mode 100644 -index 00000000000..dea047ec435 ---- /dev/null -+++ b/app/code/Magento/Cms/Test/Mftf/Data/BlockData.xml -@@ -0,0 +1,18 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<entities xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataProfileSchema.xsd"> -+ <entity name="Sales25offBlock" type="block"> -+ <data key="title" unique="suffix">Sales25off</data> -+ <data key="identifier" unique="suffix">Sales25off</data> -+ <data key="store_id">All Store Views</data> -+ <data key="content">sales25off everything!</data> -+ <data key="is_active">0</data> -+ </entity> -+</entities> -diff --git a/app/code/Magento/Cms/Test/Mftf/Data/BlockPageData.xml b/app/code/Magento/Cms/Test/Mftf/Data/BlockPageData.xml -index 9e0db2ada4a..98cd9ae30b8 100644 ---- a/app/code/Magento/Cms/Test/Mftf/Data/BlockPageData.xml -+++ b/app/code/Magento/Cms/Test/Mftf/Data/BlockPageData.xml -@@ -7,9 +7,9 @@ - --> - - <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="_defaultBlock" type="block"> -- <data key="title">Default Block</data> -+ <data key="title" unique="suffix">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> -diff --git a/app/code/Magento/Cms/Test/Mftf/Data/CmsPageData.xml b/app/code/Magento/Cms/Test/Mftf/Data/CmsPageData.xml -index 84561d6cd96..5dc100573c3 100644 ---- a/app/code/Magento/Cms/Test/Mftf/Data/CmsPageData.xml -+++ b/app/code/Magento/Cms/Test/Mftf/Data/CmsPageData.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="_defaultCmsPage" type="cms_page"> - <data key="title">Test CMS Page</data> - <data key="content_heading">Test Content Heading</data> -@@ -20,6 +20,15 @@ - <data key="content">Sample page content. Yada yada yada.</data> - <data key="identifier" unique="suffix">test-page-</data> - </entity> -+ <entity name="customCmsPage" extends="_defaultCmsPage" type="cms_page"> -+ <data key="content">Test content data1</data> -+ <data key="identifier">url_key</data> -+ </entity> -+ <entity name="customCmsPage2" extends="_defaultCmsPage" type="cms_page"> -+ <data key="title">Test Second CMS Page</data> -+ <data key="content">Test content data2</data> -+ <data key="identifier">url_key</data> -+ </entity> - <entity name="_duplicatedCMSPage" type="cms_page"> - <data key="title">testpage</data> - <data key="content_heading">Test Content Heading</data> -@@ -50,6 +59,7 @@ - <data key="file_type">Upload File</data> - <data key="shareable">Yes</data> - <data key="file">magento-again.jpg</data> -+ <data key="fileName">magento-again</data> - <data key="value">magento-again.jpg</data> - <data key="content">Image content. Yeah.</data> - <data key="height">1000</data> -@@ -71,6 +81,7 @@ - <data key="file_type">Upload File</data> - <data key="shareable">Yes</data> - <data key="value">magento3.jpg</data> -+ <data key="file">magento3.jpg</data> - <data key="fileName">magento3</data> - <data key="extension">jpg</data> - <data key="content">Image content. Yeah.</data> -@@ -80,4 +91,19 @@ - <entity name="ImageFolder" type="uploadImage"> - <data key="name" unique="suffix">Test</data> - </entity> -+ <entity name="_longContentCmsPage" type="cms_page"> -+ <data key="title">Test CMS Page</data> -+ <data key="content_heading">Test Content Heading</data> -+ <data key="content">1<br/>2<br/>3<br/>4<br/>5<br/>6<br/>7<br/>8<br/>9<br/>10<br/>11<br/>12<br/>13<br/>14<br/>15<br/>16<br/>17<br/>18<br/>19<br/>20<br/>line21<br/>22<br/>23<br/>24<br/>25<br/>26<br/>line27<br/>2<br/>3<br/>4<br/>5</data> -+ <data key="identifier" unique="suffix">test-page-</data> -+ </entity> -+ <entity name="_emptyCmsPage" type="cms_page"> -+ <data key="title" unique="suffix">Test CMS Page</data> -+ <data key="identifier" unique="suffix">test-page-</data> -+ </entity> -+ <entity name="_emptyCmsBlock" type="block"> -+ <data key="title" unique="suffix">Test CMS Block</data> -+ <data key="identifier" unique="suffix" >block</data> -+ <data key="active">true</data> -+ </entity> - </entities> -diff --git a/app/code/Magento/Cms/Test/Mftf/Data/NewCMSPageData.xml b/app/code/Magento/Cms/Test/Mftf/Data/NewCMSPageData.xml -new file mode 100644 -index 00000000000..61dfb051d10 ---- /dev/null -+++ b/app/code/Magento/Cms/Test/Mftf/Data/NewCMSPageData.xml -@@ -0,0 +1,14 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<entities xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataProfileSchema.xsd"> -+ <entity name="defaultCmsPage" type="block"> -+ <data key="title" unique="suffix">CMSpage</data> -+ </entity> -+</entities> -diff --git a/app/code/Magento/Cms/Test/Mftf/Data/WysiwygConfigData.xml b/app/code/Magento/Cms/Test/Mftf/Data/WysiwygConfigData.xml -new file mode 100644 -index 00000000000..46a96895940 ---- /dev/null -+++ b/app/code/Magento/Cms/Test/Mftf/Data/WysiwygConfigData.xml -@@ -0,0 +1,31 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<entities xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataProfileSchema.xsd"> -+ <entity name="WysiwygEnabledByDefault"> -+ <data key="path">cms/wysiwyg/enabled</data> -+ <data key="scope_id">0</data> -+ <data key="value">enabled</data> -+ </entity> -+ <entity name="WysiwygDisabledByDefault"> -+ <data key="path">cms/wysiwyg/enabled</data> -+ <data key="scope_id">0</data> -+ <data key="value">hidden</data> -+ </entity> -+ <entity name="WysiwygTinyMCE3Enable"> -+ <data key="path">cms/wysiwyg/editor</data> -+ <data key="scope_id">0</data> -+ <data key="value">Magento_Tinymce3/tinymce3Adapter</data> -+ </entity> -+ <entity name="WysiwygTinyMCE4Enable"> -+ <data key="path">cms/wysiwyg/editor</data> -+ <data key="scope_id">0</data> -+ <data key="value">mage/adminhtml/wysiwyg/tiny_mce/tinymce4Adapter</data> -+ </entity> -+</entities> -diff --git a/app/code/Magento/Cms/Test/Mftf/Metadata/block-meta.xml b/app/code/Magento/Cms/Test/Mftf/Metadata/block-meta.xml -index d764275f7c4..c007c89b313 100644 ---- a/app/code/Magento/Cms/Test/Mftf/Metadata/block-meta.xml -+++ b/app/code/Magento/Cms/Test/Mftf/Metadata/block-meta.xml -@@ -6,7 +6,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"> -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataOperation.xsd"> - <operation name="CreateBlock" dataType="block" type="create" auth="adminOauth" url="/V1/cmsBlock" method="POST"> - <contentType>application/json</contentType> - <object key="block" dataType="block"> -diff --git a/app/code/Magento/Cms/Test/Mftf/Metadata/cms-meta.xml b/app/code/Magento/Cms/Test/Mftf/Metadata/cms-meta.xml -index 495ca2ee0c2..44a9d9452e8 100644 ---- a/app/code/Magento/Cms/Test/Mftf/Metadata/cms-meta.xml -+++ b/app/code/Magento/Cms/Test/Mftf/Metadata/cms-meta.xml -@@ -6,7 +6,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"> -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataOperation.xsd"> - <operation name="CreateCMSPage" dataType="cms_page" type="create" auth="adminOauth" url="/V1/cmsPage" method="POST"> - <contentType>application/json</contentType> - <object key="page" dataType="cms_page"> -diff --git a/app/code/Magento/Cms/Test/Mftf/Page/AdminCmsEditBlockPage.xml b/app/code/Magento/Cms/Test/Mftf/Page/AdminCmsEditBlockPage.xml -new file mode 100644 -index 00000000000..f7ee5f5aca9 ---- /dev/null -+++ b/app/code/Magento/Cms/Test/Mftf/Page/AdminCmsEditBlockPage.xml -@@ -0,0 +1,14 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<pages xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/PageObject.xsd"> -+ <page name="AdminEditBlockPage" url="cms/block/edit/block_id/{{id}}" area="admin" module="Magento_Cms" parameterized="true"> -+ <section name="AdminUpdateBlockSection"/> -+ </page> -+</pages> -diff --git a/app/code/Magento/Cms/Test/Mftf/Page/CmsBlocksPage.xml b/app/code/Magento/Cms/Test/Mftf/Page/CmsBlocksPage.xml -index 790c2feafca..1d9564fee86 100644 ---- a/app/code/Magento/Cms/Test/Mftf/Page/CmsBlocksPage.xml -+++ b/app/code/Magento/Cms/Test/Mftf/Page/CmsBlocksPage.xml -@@ -7,7 +7,7 @@ - --> - - <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="CmsBlocksPage" url="/cms/block/" area="admin" module="Magento_Cms"> - <section name="BlockPageActionsSection"/> - </page> -diff --git a/app/code/Magento/Cms/Test/Mftf/Page/CmsNewBlockPage.xml b/app/code/Magento/Cms/Test/Mftf/Page/CmsNewBlockPage.xml -index d607c1ccf39..0a2b7a7ed37 100644 ---- a/app/code/Magento/Cms/Test/Mftf/Page/CmsNewBlockPage.xml -+++ b/app/code/Magento/Cms/Test/Mftf/Page/CmsNewBlockPage.xml -@@ -7,7 +7,7 @@ - --> - - <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="CmsNewBlock" area="admin" url="/cms/block/new" module="Magento_Cms"> - <section name="CmsNewBlockBlockActionsSection"/> - <section name="CmsNewBlockBlockBasicFieldsSection"/> -diff --git a/app/code/Magento/Cms/Test/Mftf/Page/CmsNewPagePage.xml b/app/code/Magento/Cms/Test/Mftf/Page/CmsNewPagePage.xml -index b165d6c044c..c844dc55ea1 100644 ---- a/app/code/Magento/Cms/Test/Mftf/Page/CmsNewPagePage.xml -+++ b/app/code/Magento/Cms/Test/Mftf/Page/CmsNewPagePage.xml -@@ -7,7 +7,7 @@ - --> - - <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="CmsNewPagePage" url="/cms/page/new" area="admin" module="Magento_Cms"> - <section name="CmsNewPagePageActionsSection"/> - <section name="CmsNewPagePageBasicFieldsSection"/> -diff --git a/app/code/Magento/Cms/Test/Mftf/Page/CmsPageEditPage.xml b/app/code/Magento/Cms/Test/Mftf/Page/CmsPageEditPage.xml -new file mode 100644 -index 00000000000..73db6b61343 ---- /dev/null -+++ b/app/code/Magento/Cms/Test/Mftf/Page/CmsPageEditPage.xml -@@ -0,0 +1,16 @@ -+<?xml version="1.0" encoding="utf-8"?> -+<!-- -+/** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+ --> -+<pages xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/PageObject.xsd"> -+ <page name="CmsPageEditPage" area="admin" url="admin/cms_page/edit/page_id/{{var}}" parameterized="true" module="Magento_Cms"> -+ <section name="CmsNewPagePageActionsSection"/> -+ <section name="CmsNewPagePageBasicFieldsSection"/> -+ <section name="CmsNewPagePageContentSection"/> -+ <section name="CmsNewPagePageSeoSection"/> -+ </page> -+</pages> -diff --git a/app/code/Magento/Cms/Test/Mftf/Page/CmsPagesPage.xml b/app/code/Magento/Cms/Test/Mftf/Page/CmsPagesPage.xml -index 9dcb3d608d0..45ba6eb6cf0 100644 ---- a/app/code/Magento/Cms/Test/Mftf/Page/CmsPagesPage.xml -+++ b/app/code/Magento/Cms/Test/Mftf/Page/CmsPagesPage.xml -@@ -7,7 +7,7 @@ - --> - - <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="CmsPagesPage" url="/cms/page" area="admin" module="Magento_Cms"> - <section name="CmsPagesPageActionsSection"/> - </page> -diff --git a/app/code/Magento/Cms/Test/Mftf/Page/StorefrontHomePage.xml b/app/code/Magento/Cms/Test/Mftf/Page/StorefrontHomePage.xml -index 289d872aad8..07deacfaaef 100644 ---- a/app/code/Magento/Cms/Test/Mftf/Page/StorefrontHomePage.xml -+++ b/app/code/Magento/Cms/Test/Mftf/Page/StorefrontHomePage.xml -@@ -7,7 +7,7 @@ - --> - - <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="StorefrontHomePage" url="/" module="Magento_Cms" area="storefront"> - <section name="StorefrontHeaderSection"/> - </page> -diff --git a/app/code/Magento/Cms/Test/Mftf/Section/AdminBlockGridSection.xml b/app/code/Magento/Cms/Test/Mftf/Section/AdminBlockGridSection.xml -new file mode 100644 -index 00000000000..ab15570a01f ---- /dev/null -+++ b/app/code/Magento/Cms/Test/Mftf/Section/AdminBlockGridSection.xml -@@ -0,0 +1,18 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> -+ <section name="AdminBlockGridSection"> -+ <element name="search" type="input" selector="//input[@placeholder='Search by keyword']"/> -+ <element name="searchButton" type="button" selector="//div[@class='data-grid-search-control-wrap']//label[@class='data-grid-search-label']/following-sibling::button[@class='action-submit']"/> -+ <element name="checkbox" type="checkbox" selector="//label[@class='data-grid-checkbox-cell-inner']//input[@class='admin__control-checkbox']"/> -+ <element name="select" type="select" selector="//tr[@class='data-row']//button[@class='action-select']"/> -+ <element name="editInSelect" type="text" selector="//a[contains(text(), 'Edit')]"/> -+ </section> -+</sections> -diff --git a/app/code/Magento/Cms/Test/Mftf/Section/BlockPageActionsSection.xml b/app/code/Magento/Cms/Test/Mftf/Section/BlockPageActionsSection.xml -index 3fb56e0b179..d487517269c 100644 ---- a/app/code/Magento/Cms/Test/Mftf/Section/BlockPageActionsSection.xml -+++ b/app/code/Magento/Cms/Test/Mftf/Section/BlockPageActionsSection.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="BlockPageActionsSection"> - <element name="addNewBlock" type="button" selector="#add" timeout="30"/> - <element name="select" type="button" selector="//div[text()='{{var1}}']//parent::td//following-sibling::td//button[text()='Select']" parameterized="true"/> -diff --git a/app/code/Magento/Cms/Test/Mftf/Section/CmsNewBlockBlockActionsSection.xml b/app/code/Magento/Cms/Test/Mftf/Section/CmsNewBlockBlockActionsSection.xml -index 65ea1226772..2efa7f62fc4 100644 ---- a/app/code/Magento/Cms/Test/Mftf/Section/CmsNewBlockBlockActionsSection.xml -+++ b/app/code/Magento/Cms/Test/Mftf/Section/CmsNewBlockBlockActionsSection.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="CmsNewBlockBlockActionsSection"> - <element name="savePage" type="button" selector="#save-button" timeout="30"/> - </section> -@@ -17,6 +17,7 @@ - <element name="saveAndDuplicate" type="button" selector="#save_and_duplicate" timeout="10"/> - <element name="saveAndClose" type="button" selector="#save_and_close" timeout="10"/> - <element name="expandSplitButton" type="button" selector="//button[@data-ui-id='save-button-dropdown']" timeout="10"/> -+ <element name="back" type="button" selector="#back"/> - </section> - <section name="BlockWYSIWYGSection"> - <element name="ShowHideBtn" type="button" selector="#togglecms_block_form_content"/> -@@ -24,4 +25,8 @@ - <section name="BlockContentSection"> - <element name="TextArea" type="input" selector="#cms_block_form_content"/> - </section> -+ <section name="CmsBlockBlockActionSection"> -+ <element name="deleteBlock" type="button" selector="#delete" timeout="30"/> -+ <element name="deleteConfirm" type="button" selector=".action-primary.action-accept" timeout="60"/> -+ </section> - </sections> -diff --git a/app/code/Magento/Cms/Test/Mftf/Section/CmsNewBlockBlockBasicFieldsSection.xml b/app/code/Magento/Cms/Test/Mftf/Section/CmsNewBlockBlockBasicFieldsSection.xml -index 00b81686f71..79fc3bac0fb 100644 ---- a/app/code/Magento/Cms/Test/Mftf/Section/CmsNewBlockBlockBasicFieldsSection.xml -+++ b/app/code/Magento/Cms/Test/Mftf/Section/CmsNewBlockBlockBasicFieldsSection.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="CmsNewBlockBlockBasicFieldsSection"> - <element name="title" type="input" selector="input[name=title]"/> - <element name="identifier" type="input" selector="input[name=identifier]"/> -diff --git a/app/code/Magento/Cms/Test/Mftf/Section/CmsNewPageHierarchySection.xml b/app/code/Magento/Cms/Test/Mftf/Section/CmsNewPageHierarchySection.xml -index e2c4f48f4ff..a2e4aecf8db 100644 ---- a/app/code/Magento/Cms/Test/Mftf/Section/CmsNewPageHierarchySection.xml -+++ b/app/code/Magento/Cms/Test/Mftf/Section/CmsNewPageHierarchySection.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="CmsNewPageHierarchySection"> - <element name="header" type="button" selector="div[data-index=hierarchy]" timeout="30"/> - <element name="selectHierarchy" type="button" selector="//a/span[contains(text(),'{{var1}}')]" parameterized="true" timeout="30"/> -diff --git a/app/code/Magento/Cms/Test/Mftf/Section/CmsNewPagePageActionsSection.xml b/app/code/Magento/Cms/Test/Mftf/Section/CmsNewPagePageActionsSection.xml -index 810c482dffd..a340d0af1e7 100644 ---- a/app/code/Magento/Cms/Test/Mftf/Section/CmsNewPagePageActionsSection.xml -+++ b/app/code/Magento/Cms/Test/Mftf/Section/CmsNewPagePageActionsSection.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="CmsNewPagePageActionsSection"> - <element name="savePage" type="button" selector="#save_and_close" timeout="10"/> - <element name="reset" type="button" selector="#reset"/> -@@ -22,5 +22,6 @@ - <element name="content" type="input" selector="//textarea[@name='content']"/> - <element name="spinner" type="input" selector='//div[@data-component="cms_page_form.cms_page_form"]' /> - <element name="saveAndClose" type="button" selector="#save_and_close" timeout="10"/> -+ <element name="insertWidget" type="button" selector="//span[contains(text(),'Insert Widget...')]"/> - </section> - </sections> -diff --git a/app/code/Magento/Cms/Test/Mftf/Section/CmsNewPagePageBasicFieldsSection.xml b/app/code/Magento/Cms/Test/Mftf/Section/CmsNewPagePageBasicFieldsSection.xml -index 468dbecb20e..7288e5d455d 100644 ---- a/app/code/Magento/Cms/Test/Mftf/Section/CmsNewPagePageBasicFieldsSection.xml -+++ b/app/code/Magento/Cms/Test/Mftf/Section/CmsNewPagePageBasicFieldsSection.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="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');"/> - <element name="isActive" type="button" selector="//input[@name='is_active' and @value='{{var1}}']" parameterized="true"/> - <element name="duplicatedURLKey" type="input" selector="//input[contains(@data-value,'{{var1}}')]" parameterized="true"/> - </section> -diff --git a/app/code/Magento/Cms/Test/Mftf/Section/CmsNewPagePageContentSection.xml b/app/code/Magento/Cms/Test/Mftf/Section/CmsNewPagePageContentSection.xml -index 8ec0c8349c1..05a125b9cc6 100644 ---- a/app/code/Magento/Cms/Test/Mftf/Section/CmsNewPagePageContentSection.xml -+++ b/app/code/Magento/Cms/Test/Mftf/Section/CmsNewPagePageContentSection.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="CmsNewPagePageContentSection"> - <element name="header" type="button" selector="div[data-index=content]"/> - <element name="contentHeading" type="input" selector="input[name=content_heading]"/> -diff --git a/app/code/Magento/Cms/Test/Mftf/Section/CmsNewPagePageSeoSection.xml b/app/code/Magento/Cms/Test/Mftf/Section/CmsNewPagePageSeoSection.xml -index 0fe9c01d36f..dfd7386e09a 100644 ---- a/app/code/Magento/Cms/Test/Mftf/Section/CmsNewPagePageSeoSection.xml -+++ b/app/code/Magento/Cms/Test/Mftf/Section/CmsNewPagePageSeoSection.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="CmsNewPagePageSeoSection"> - <element name="header" type="button" selector="div[data-index=search_engine_optimisation]" timeout="30"/> - <element name="urlKey" type="input" selector="input[name=identifier]"/> -diff --git a/app/code/Magento/Cms/Test/Mftf/Section/CmsNewPagePiwSection.xml b/app/code/Magento/Cms/Test/Mftf/Section/CmsNewPagePiwSection.xml -index 456de55b491..bd487e3b2c0 100644 ---- a/app/code/Magento/Cms/Test/Mftf/Section/CmsNewPagePiwSection.xml -+++ b/app/code/Magento/Cms/Test/Mftf/Section/CmsNewPagePiwSection.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="CmsNewPagePiwSection"> - <element name="header" type="button" selector="div[data-index=websites]" timeout="30"/> - <element name="selectStoreView" type="select" selector="//option[contains(text(),'{{var1}}')]" parameterized="true"/> -diff --git a/app/code/Magento/Cms/Test/Mftf/Section/CmsPagesPageActionsSection.xml b/app/code/Magento/Cms/Test/Mftf/Section/CmsPagesPageActionsSection.xml -index 2f28aa46af6..494c98ca44e 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="filterButton" type="input" selector="//button[text()='Filters']"/> - <element name="URLKey" type="input" selector="//div[@class='admin__form-field-control']/input[@name='identifier']"/> -@@ -17,7 +17,7 @@ - <element name="addNewPageButton" type="button" selector="#add" timeout="30"/> - <element name="select" type="button" selector="//div[text()='{{var1}}']/parent::td//following-sibling::td[@class='data-grid-actions-cell']//button[text()='Select']" parameterized="true"/> - <element name="edit" type="button" selector="//div[text()='{{var1}}']/parent::td//following-sibling::td[@class='data-grid-actions-cell']//a[text()='Edit']" parameterized="true"/> -- <element name="preview" type="button" selector="//div[text()='{{var1}}']/parent::td//following-sibling::td[@class='data-grid-actions-cell']//a[text()='Preview']" parameterized="true"/> -+ <element name="preview" type="button" selector="//div[text()='{{var1}}']/parent::td//following-sibling::td[@class='data-grid-actions-cell']//a[text()='View']" parameterized="true"/> - <element name="clearAllButton" type="button" selector="//div[@class='admin__data-grid-header']//button[contains(text(), 'Clear all')]"/> - <element name="activeFilters" type="button" selector="//div[@class='admin__data-grid-header']//span[contains(text(), 'Active filters:')]" /> - <element name="spinner" type="input" selector='//div[@data-component="cms_page_listing.cms_page_listing.cms_page_columns"]'/> -@@ -27,5 +27,8 @@ - <element name="savePageSuccessMessage" type="text" selector=".message-success"/> - <element name="delete" type="button" selector="//div[text()='{{var1}}']/parent::td//following-sibling::td[@class='data-grid-actions-cell']//a[text()='Delete']" parameterized="true"/> - <element name="deleteConfirm" type="button" selector=".action-primary.action-accept" timeout="60"/> -+ <element name="pageRowCheckboxByIdentifier" type="checkbox" selector="//table[@data-role='grid']//td[count(../../..//th[./*[.='URL Key']]/preceding-sibling::th) + 1][./*[.='{{identifier}}']]/../td//input[@data-action='select-row']" parameterized="true" /> -+ <element name="massActionsButton" type="button" selector="//div[@class='admin__data-grid-header'][(not(ancestor::*[@class='sticky-header']) and not(contains(@style,'visibility: hidden'))) or (ancestor::*[@class='sticky-header' and not(contains(@style,'display: none'))])]//button[contains(@class, 'action-select')]" /> -+ <element name="massActionsOption" type="button" selector="//div[@class='admin__data-grid-header'][(not(ancestor::*[@class='sticky-header']) and not(contains(@style,'visibility: hidden'))) or (ancestor::*[@class='sticky-header' and not(contains(@style,'display: none'))])]//span[contains(@class, 'action-menu-item') and .= '{{action}}']" parameterized="true"/> - </section> - </sections> -diff --git a/app/code/Magento/Cms/Test/Mftf/Section/CustomVariableSection.xml b/app/code/Magento/Cms/Test/Mftf/Section/CustomVariableSection.xml -index 354c86cfc4b..1488134ea51 100644 ---- a/app/code/Magento/Cms/Test/Mftf/Section/CustomVariableSection.xml -+++ b/app/code/Magento/Cms/Test/Mftf/Section/CustomVariableSection.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="CustomVariableSection"> - <element name="GridCustomVariableCode" type="text" selector=".//*[@id='customVariablesGrid_table']/tbody//tr//td[contains(text(), '{{var1}}')]" parameterized="true"/> - <element name="variableCode" type="input" selector="#code"/> -diff --git a/app/code/Magento/Cms/Test/Mftf/Section/StorefrontBlockSection.xml b/app/code/Magento/Cms/Test/Mftf/Section/StorefrontBlockSection.xml -index fb4abe30b37..bd2f9e5a646 100644 ---- a/app/code/Magento/Cms/Test/Mftf/Section/StorefrontBlockSection.xml -+++ b/app/code/Magento/Cms/Test/Mftf/Section/StorefrontBlockSection.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="StorefrontBlockSection"> - <element name="mediaDescription" type="text" selector=".widget.block.block-static-block>p>img"/> - <element name="imageSource" type="text" selector="//img[contains(@src,'{{var1}}')]" parameterized="true"/> -diff --git a/app/code/Magento/Cms/Test/Mftf/Section/StorefrontCMSPageSection.xml b/app/code/Magento/Cms/Test/Mftf/Section/StorefrontCMSPageSection.xml -index d7c0a41464d..4ce8842c1ad 100644 ---- a/app/code/Magento/Cms/Test/Mftf/Section/StorefrontCMSPageSection.xml -+++ b/app/code/Magento/Cms/Test/Mftf/Section/StorefrontCMSPageSection.xml -@@ -7,10 +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="StorefrontCMSPageSection"> - <element name="mediaDescription" type="text" selector=".column.main>p>img"/> -- <element name="imageSource" type="text" selector="//img[contains(@src,'{{var1}}')]" parameterized="true"/> -+ <element name="imageSource" type="text" selector="//img[contains(@src,'{{imageName}}')]" parameterized="true"/> - <element name="mainTitle" type="text" selector="#maincontent .page-title"/> -+ <element name="mainContent" type="text" selector="#maincontent"/> -+ <element name="footerTop" type="text" selector="footer.page-footer"/> -+ <element name="title" type="text" selector="//div[@class='breadcrumbs']//ul/li[@class='item cms_page']"/> - </section> - </sections> -diff --git a/app/code/Magento/Cms/Test/Mftf/Section/StorefrontHeaderSection.xml b/app/code/Magento/Cms/Test/Mftf/Section/StorefrontHeaderSection.xml -deleted file mode 100644 -index 154bf33ac56..00000000000 ---- a/app/code/Magento/Cms/Test/Mftf/Section/StorefrontHeaderSection.xml -+++ /dev/null -@@ -1,13 +0,0 @@ --<?xml version="1.0" encoding="UTF-8"?> --<!-- -- /** -- * Copyright © Magento, Inc. All rights reserved. -- * See COPYING.txt for license details. -- */ ----> -- --<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -- xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Page/etc/SectionObject.xsd"> -- <section name="StorefrontHeaderSection"> -- </section> --</sections> -diff --git a/app/code/Magento/Cms/Test/Mftf/Section/TinyMCESection.xml b/app/code/Magento/Cms/Test/Mftf/Section/TinyMCESection.xml -index 0f13c3de47d..ff6167ffc10 100644 ---- a/app/code/Magento/Cms/Test/Mftf/Section/TinyMCESection.xml -+++ b/app/code/Magento/Cms/Test/Mftf/Section/TinyMCESection.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="TinyMCESection"> - <element name="checkIfContentTabOpen" type="button" selector="//span[text()='Content']/parent::strong/parent::*[@data-state-collapsible='closed']"/> - <element name="CheckIfTabExpand" type="button" selector="//div[@data-state-collapsible='closed']//span[text()='Content']"/> -@@ -31,18 +31,23 @@ - <element name="InsertImage" type="button" selector=".mce-i-image" /> - <element name="InsertTable" type="button" selector=".mce-i-table" /> - <element name="SpecialCharacter" type="button" selector=".mce-i-charmap" /> -+ <element name="WidgetButton" type="button" selector="span[class*='magento-widget mceNonEditable']"/> -+ <element name="EditorContent" type="input" selector="#tinymce"/> - </section> - <section name="MediaGallerySection"> - <element name="Browse" type="button" selector=".mce-i-browse"/> -+ <element name="browseForImage" type="button" selector="//*[@id='srcbrowser']"/> - <element name="BrowseUploadImage" type="file" selector=".fileupload" /> - <element name="image" type="text" selector="//small[text()='{{var1}}']" parameterized="true"/> - <element name="imageOrImageCopy" type="text" selector="//div[contains(@class,'media-gallery-modal')]//img[contains(@alt, '{{arg1}}.{{arg2}}')]|//img[contains(@alt,'{{arg1}}_') and contains(@alt,'.{{arg2}}')]" parameterized="true"/> - <element name="imageSelected" type="text" selector="//small[text()='{{var1}}']/parent::*[@class='filecnt selected']" parameterized="true"/> - <element name="ImageSource" type="input" selector=".mce-combobox.mce-abs-layout-item.mce-last.mce-has-open" /> - <element name="ImageDescription" type="input" selector=".mce-textbox.mce-abs-layout-item.mce-last" /> -+ <element name="ImageDescriptionTinyMCE3" type="input" selector="#alt" /> - <element name="Height" type="input" selector=".mce-textbox.mce-abs-layout-item.mce-first" /> - <element name="UploadImage" type="file" selector=".fileupload" /> - <element name="OkBtn" type="button" selector="//span[text()='Ok']"/> -+ <element name="insertBtn" type="button" selector="#insert"/> - <element name="InsertFile" type="text" selector="#insert_files"/> - <element name="CreateFolder" type="button" selector="#new_folder" /> - <element name="DeleteSelectedBtn" type="text" selector="#delete_files"/> -@@ -54,6 +59,7 @@ - <element name="WysiwygArrow" type="button" selector="#d3lzaXd5Zw-- > .jstree-icon" /> - <element name="checkIfWysiwygArrowExpand" type="button" selector="//li[@id='d3lzaXd5Zw--' and contains(@class,'jstree-closed')]" /> - <element name="confirmDelete" type="button" selector=".action-primary.action-accept" /> -+ <element name="imageBlockByName" type="block" selector="//div[@data-row='file'][contains(., '{{imageName}}')]" parameterized="true"/> - </section> - <section name="VariableSection"> - <element name="InsertWidget" type="button" selector="#insert_variable"/> -@@ -74,11 +80,12 @@ - </section> - <section name="WidgetSection"> - <element name="InsertWidgetTitle" type="text" selector="//h1[contains(text(),'Insert Widget')]"/> -+ <element name="DisplayType" type="select" selector="select[name='parameters[display_type]']"/> - <element name="SelectCategoryTitle" type="text" selector="//h1[contains(text(),'Select Category')]"/> - <element name="SelectProductTitle" type="text" selector="//h1[contains(text(),'Select Product')]"/> - <element name="SelectPageTitle" type="text" selector="//h1[contains(text(),'Select Page')]"/> - <element name="SelectBlockTitle" type="text" selector="//h1[contains(text(),'Select Block')]"/> -- <element name="InsertWidget" type="button" selector="#insert_button"/> -+ <element name="InsertWidget" type="button" selector="#insert_button" timeout="30"/> - <element name="InsertWidgetBtnDisabled" type="button" selector="//button[@id='insert_button' and contains(@class,'disabled')]"/> - <element name="InsertWidgetBtnEnabled" type="button" selector="//button[@id='insert_button' and not(contains(@class,'disabled'))]"/> - <element name="CancelBtnEnabled" type="button" selector="//button[@id='reset' and not(contains(@class,'disabled'))]"/> -@@ -94,6 +101,10 @@ - <element name="AddParam" type="button" selector=".rule-param-add"/> - <element name="ConditionsDropdown" type="select" selector="#conditions__1__new_child"/> - <element name="RuleParam" type="button" selector="//a[text()='...']"/> -+ <element name="RuleParam1" type="button" selector="(//span[@class='rule-param']//a)[{{var}}]" parameterized="true"/> -+ <element name="RuleParamSelect" type="select" selector="//ul[contains(@class,'rule-param-children')]/li[{{arg1}}]//*[contains(@class,'rule-param')][{{arg2}}]//select" parameterized="true"/> -+ <element name="RuleParamInput" type="input" selector="//ul[contains(@class,'rule-param-children')]/li[{{arg1}}]//*[contains(@class,'rule-param')][{{arg2}}]//input" parameterized="true"/> -+ <element name="RuleParamLabel" type="input" selector="//ul[contains(@class,'rule-param-children')]/li[{{arg1}}]//*[contains(@class,'rule-param')][{{arg2}}]//a" parameterized="true"/> - <element name="Chooser" type="button" selector="//img[@title='Open Chooser']"/> - <element name="PageSize" type="input" selector="input[name='parameters[page_size]']"/> - <element name="ProductAttribute" type="multiselect" selector="select[name='parameters[show_attributes][]']" /> -@@ -103,6 +114,8 @@ - <element name="CompareBtn" type="button" selector=".action.tocompare"/> - <element name="ClearCompare" type="button" selector="#compare-clear-all"/> - <element name="AcceptClear" type="button" selector=".action-primary.action-accept" /> -- -+ <element name="ChooserName" type="input" selector="input[name='chooser_name']" /> -+ <element name="SelectPageButton" type="button" selector="//button[@title='Select Page...']"/> -+ <element name="SelectPageFilterInput" type="input" selector="input.admin__control-text[name='{{filterName}}']" parameterized="true"/> - </section> - </sections> -diff --git a/app/code/Magento/Cms/Test/Mftf/Test/AdminAddImageToCMSPageTinyMCE3Test.xml b/app/code/Magento/Cms/Test/Mftf/Test/AdminAddImageToCMSPageTinyMCE3Test.xml -new file mode 100644 -index 00000000000..11bf03c1d5e ---- /dev/null -+++ b/app/code/Magento/Cms/Test/Mftf/Test/AdminAddImageToCMSPageTinyMCE3Test.xml -@@ -0,0 +1,70 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> -+ <test name="AdminAddImageToCMSPageTinyMCE3Test"> -+ <annotations> -+ <features value="Cms"/> -+ <stories value="Admin should be able to upload images with TinyMCE3 WYSIWYG"/> -+ <group value="Cms"/> -+ <title value="Verify that admin is able to upload image to a CMS Page with TinyMCE3 enabled"/> -+ <description value="Verify that admin is able to upload image to CMS Page with TinyMCE3 enabled"/> -+ <severity value="MAJOR"/> -+ <testCaseId value="MAGETWO-95725"/> -+ </annotations> -+ <before> -+ <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> -+ <magentoCLI command="config:set cms/wysiwyg/enabled enabled" stepKey="enableWYSIWYG"/> -+ <!-- Choose TinyMCE3 as the default WYSIWYG editor--> -+ <magentoCLI command="config:set cms/wysiwyg/editor Magento_Tinymce3/tinymce3Adapter" stepKey="enableTinyMCE3"/> -+ </before> -+ <after> -+ <!-- Switch WYSIWYG editor to TinyMCE4--> -+ <comment userInput="Reset editor as TinyMCE4" stepKey="chooseTinyMCE4AsEditor"/> -+ <magentoCLI command="config:set cms/wysiwyg/editor mage/adminhtml/wysiwyg/tiny_mce/tinymce4Adapter" stepKey="enableTinyMCE4"/> -+ <actionGroup ref="logout" stepKey="logout"/> -+ </after> -+ <amOnPage url="{{CmsNewPagePage.url}}" stepKey="navigateToPage2"/> -+ <waitForPageLoad stepKey="wait5"/> -+ <fillField selector="{{CmsNewPagePageBasicFieldsSection.pageTitle}}" userInput="{{_defaultCmsPage.title}}" stepKey="fillFieldTitle2"/> -+ <click selector="{{CmsNewPagePageContentSection.header}}" stepKey="clickContentTab2" /> -+ <waitForElementVisible selector="{{TinyMCESection.TinyMCE3}}" stepKey="waitForTinyMCE3"/> -+ <seeElement selector="{{TinyMCESection.TinyMCE3}}" stepKey="seeTinyMCE3" /> -+ <wait time="3" stepKey="waiting"/> -+ <comment userInput="Click Insert image button" stepKey="clickImageButton"/> -+ <click selector="{{TinyMCESection.InsertImageBtnTinyMCE3}}" stepKey="clickInsertImage" /> -+ <waitForPageLoad stepKey="waitForiFrameToLoad" /> -+ <!-- Switch to the Edit/Insert Image iFrame --> -+ <comment userInput="Switching to iFrame" stepKey="insertImageiFrame"/> -+ <executeJS function="document.querySelector('.clearlooks2 iframe').setAttribute('name', 'insert-image');" stepKey="makeIFrameInteractable"/> -+ <switchToIFrame selector="insert-image" stepKey="switchToIFrame"/> -+ <click selector="{{MediaGallerySection.browseForImage}}" stepKey="clickBrowse"/> -+ <switchToIFrame stepKey="switchOutOfIFrame"/> -+ <waitForPageLoad stepKey="waitForPageToLoad" /> -+ <actionGroup ref="CreateImageFolder" stepKey="CreateImageFolder"> -+ <argument name="ImageFolder" value="ImageFolder"/> -+ </actionGroup> -+ <actionGroup ref="attachImage" stepKey="attachImage1"> -+ <argument name="Image" value="ImageUpload"/> -+ </actionGroup> -+ <actionGroup ref="saveImage" stepKey="insertImage"/> -+ <!-- Switching back to the Edit/Insert Image iFrame--> -+ <comment userInput="switching back to iFrame" stepKey="switchBackToIFrame"/> -+ <executeJS function="document.querySelector('.clearlooks2 iframe').setAttribute('name', 'insert-image');" stepKey="makeIFrameInteractable2"/> -+ <switchToIFrame selector="insert-image" stepKey="switchToIFrame2"/> -+ <waitForElementVisible selector="{{MediaGallerySection.insertBtn}}" stepKey="waitForInsertBtnOnIFrame" /> -+ <fillField selector="{{MediaGallerySection.ImageDescriptionTinyMCE3}}" userInput="{{ImageUpload.content}}" stepKey="fillImageDescription" /> -+ <click selector="{{MediaGallerySection.insertBtn}}" stepKey="clickInsertBtn" /> -+ <waitForPageLoad stepKey="wait3"/> -+ <click selector="{{CmsNewPagePageActionsSection.expandSplitButton}}" stepKey="expandButtonMenu"/> -+ <waitForElementVisible selector="{{CmsNewPagePageActionsSection.splitButtonMenu}}" stepKey="waitForSplitButtonMenuVisible"/> -+ <click selector="{{CmsNewPagePageActionsSection.savePage}}" stepKey="clickSavePage"/> -+ <see userInput="You saved the page." stepKey="seeSuccessMessage"/> -+ </test> -+</tests> -\ No newline at end of file -diff --git a/app/code/Magento/Cms/Test/Mftf/Test/AdminAddImageToWYSIWYGBlockTest.xml b/app/code/Magento/Cms/Test/Mftf/Test/AdminAddImageToWYSIWYGBlockTest.xml -index 46aa4ac22e0..5baf75d43c5 100644 ---- a/app/code/Magento/Cms/Test/Mftf/Test/AdminAddImageToWYSIWYGBlockTest.xml -+++ b/app/code/Magento/Cms/Test/Mftf/Test/AdminAddImageToWYSIWYGBlockTest.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="AdminAddImageToWYSIWYGBlockTest"> - <annotations> - <features value="Cms"/> -@@ -17,7 +17,7 @@ - <severity value="CRITICAL"/> - <testCaseId value="MAGETWO-84376"/> - <skip> -- <issueId value="MQE-1187" /> -+ <issueId value="MC-17232"/> - </skip> - </annotations> - <before> -diff --git a/app/code/Magento/Cms/Test/Mftf/Test/AdminAddImageToWYSIWYGCMSTest.xml b/app/code/Magento/Cms/Test/Mftf/Test/AdminAddImageToWYSIWYGCMSTest.xml -index 995f52e42b3..e63a6be51bc 100644 ---- a/app/code/Magento/Cms/Test/Mftf/Test/AdminAddImageToWYSIWYGCMSTest.xml -+++ b/app/code/Magento/Cms/Test/Mftf/Test/AdminAddImageToWYSIWYGCMSTest.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="AdminAddImageToWYSIWYGCMSTest"> - <annotations> - <features value="Cms"/> -@@ -16,6 +16,9 @@ - <description value="Admin should be able to add image to WYSIWYG content of CMS Page"/> - <severity value="CRITICAL"/> - <testCaseId value="MAGETWO-85825"/> -+ <skip> -+ <issueId value="MC-17232"/> -+ </skip> - </annotations> - <before> - <createData entity="_defaultCmsPage" stepKey="createCMSPage" /> -diff --git a/app/code/Magento/Cms/Test/Mftf/Test/AdminAddVariableToWYSIWYGBlockTest.xml b/app/code/Magento/Cms/Test/Mftf/Test/AdminAddVariableToWYSIWYGBlockTest.xml -index d0d8edc6abc..ce34a8d09c3 100644 ---- a/app/code/Magento/Cms/Test/Mftf/Test/AdminAddVariableToWYSIWYGBlockTest.xml -+++ b/app/code/Magento/Cms/Test/Mftf/Test/AdminAddVariableToWYSIWYGBlockTest.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="AdminAddVariableToWYSIWYGBlockTest"> - <annotations> - <features value="Cms"/> -@@ -15,6 +15,7 @@ - <title value="Admin should be able to add variable to WYSIWYG content of Block"/> - <description value="You should be able to add variable to WYSIWYG content Block"/> - <testCaseId value="MAGETWO-84378"/> -+ <severity value="AVERAGE"/> - </annotations> - <before> - <createData entity="_defaultCmsPage" stepKey="createCMSPage" /> -@@ -48,9 +49,9 @@ - <waitForLoadingMaskToDisappear stepKey="waitForPageLoad3"/> - <!--see Insert Variable button disabled--> - <see selector="{{VariableSection.InsertVariableBtnDisabled}}" userInput="Insert Variable" stepKey="seeInsertWidgetDisabled" /> -- <!--see Cancel button enabed--> -+ <!--see Cancel button enabled--> - <see selector="{{VariableSection.CancelBtnEnabled}}" userInput="Cancel" stepKey="seeCancelBtnEnabled" /> -- <!--see 4 colums--> -+ <!--see 4 columns--> - <see selector="{{VariableSection.ColName('Select')}}" userInput="Select" stepKey="selectCol" /> - <see selector="{{VariableSection.ColName('Variable Name')}}" userInput="Variable Name" stepKey="variableCol" /> - <see selector="{{VariableSection.ColName('Type')}}" userInput="Type" stepKey="typeCol" /> -diff --git a/app/code/Magento/Cms/Test/Mftf/Test/AdminAddVariableToWYSIWYGCMSTest.xml b/app/code/Magento/Cms/Test/Mftf/Test/AdminAddVariableToWYSIWYGCMSTest.xml -index a7627b5492d..3b501859e60 100644 ---- a/app/code/Magento/Cms/Test/Mftf/Test/AdminAddVariableToWYSIWYGCMSTest.xml -+++ b/app/code/Magento/Cms/Test/Mftf/Test/AdminAddVariableToWYSIWYGCMSTest.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="AdminAddVariableToWYSIWYGCMSTest"> - <annotations> - <features value="Cms"/> -@@ -43,9 +43,9 @@ - <waitForText userInput="Insert Variable" stepKey="waitForSlideOutOpen"/> - <!--see Insert Variable button disabled--> - <see selector="{{VariableSection.InsertVariableBtnDisabled}}" userInput="Insert Variable" stepKey="seeInsertWidgetDisabled" /> -- <!--see Cancel button enabed--> -+ <!--see Cancel button enabled--> - <see selector="{{VariableSection.CancelBtnEnabled}}" userInput="Cancel" stepKey="seeCancelBtnEnabled" /> -- <!--see 4 colums--> -+ <!--see 4 columns--> - <see selector="{{VariableSection.ColName('Select')}}" userInput="Select" stepKey="selectCol" /> - <see selector="{{VariableSection.ColName('Variable Name')}}" userInput="Variable Name" stepKey="variableCol" /> - <see selector="{{VariableSection.ColName('Type')}}" userInput="Type" stepKey="typeCol" /> -diff --git a/app/code/Magento/Cms/Test/Mftf/Test/AdminAddWidgetToWYSIWYGBlockTest.xml b/app/code/Magento/Cms/Test/Mftf/Test/AdminAddWidgetToWYSIWYGBlockTest.xml -index 4d93980da9a..ad5e769c61b 100644 ---- a/app/code/Magento/Cms/Test/Mftf/Test/AdminAddWidgetToWYSIWYGBlockTest.xml -+++ b/app/code/Magento/Cms/Test/Mftf/Test/AdminAddWidgetToWYSIWYGBlockTest.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="AdminAddWidgetToWYSIWYGBlockTest"> - <annotations> - <features value="Cms"/> -diff --git a/app/code/Magento/Cms/Test/Mftf/Test/AdminAddWidgetToWYSIWYGWithCMSPageLinkTypeTest.xml b/app/code/Magento/Cms/Test/Mftf/Test/AdminAddWidgetToWYSIWYGWithCMSPageLinkTypeTest.xml -index 90caf89c6a0..1adb781a675 100644 ---- a/app/code/Magento/Cms/Test/Mftf/Test/AdminAddWidgetToWYSIWYGWithCMSPageLinkTypeTest.xml -+++ b/app/code/Magento/Cms/Test/Mftf/Test/AdminAddWidgetToWYSIWYGWithCMSPageLinkTypeTest.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="AdminAddWidgetToWYSIWYGWithCMSPageLinkTypeTest"> - <annotations> - <features value="Cms"/> -@@ -36,7 +36,7 @@ - <see userInput="Inserting a widget does not create a widget instance." stepKey="seeMessage" /> - <!--see Insert Widget button disabled--> - <see selector="{{WidgetSection.InsertWidgetBtnDisabled}}" userInput="Insert Widget" stepKey="seeInsertWidgetDisabled" /> -- <!--see Cancel button enabed--> -+ <!--see Cancel button enabled--> - <see selector="{{WidgetSection.CancelBtnEnabled}}" userInput="Cancel" stepKey="seeCancelBtnEnabled" /> - <!--Select "Widget Type"--> - <selectOption selector="{{WidgetSection.WidgetType}}" userInput="CMS Page Link" stepKey="selectCMSPageLink" /> -diff --git a/app/code/Magento/Cms/Test/Mftf/Test/AdminAddWidgetToWYSIWYGWithCMSStaticBlockTypeTest.xml b/app/code/Magento/Cms/Test/Mftf/Test/AdminAddWidgetToWYSIWYGWithCMSStaticBlockTypeTest.xml -index 89030034dde..f37038435e1 100644 ---- a/app/code/Magento/Cms/Test/Mftf/Test/AdminAddWidgetToWYSIWYGWithCMSStaticBlockTypeTest.xml -+++ b/app/code/Magento/Cms/Test/Mftf/Test/AdminAddWidgetToWYSIWYGWithCMSStaticBlockTypeTest.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="AdminAddWidgetToWYSIWYGWithCMSStaticBlockTypeTest"> - <annotations> - <features value="Cms"/> -diff --git a/app/code/Magento/Cms/Test/Mftf/Test/AdminAddWidgetToWYSIWYGWithCatalogCategoryLinkTypeTest.xml b/app/code/Magento/Cms/Test/Mftf/Test/AdminAddWidgetToWYSIWYGWithCatalogCategoryLinkTypeTest.xml -index 5993c7e2b82..552eae40739 100644 ---- a/app/code/Magento/Cms/Test/Mftf/Test/AdminAddWidgetToWYSIWYGWithCatalogCategoryLinkTypeTest.xml -+++ b/app/code/Magento/Cms/Test/Mftf/Test/AdminAddWidgetToWYSIWYGWithCatalogCategoryLinkTypeTest.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="AdminAddWidgetToWYSIWYGWithCatalogCategoryLinkTypeTest"> - <annotations> - <features value="Cms"/> -@@ -37,7 +37,7 @@ - <see userInput="Inserting a widget does not create a widget instance." stepKey="seeMessage" /> - <!--see Insert Widget button disabled--> - <see selector="{{WidgetSection.InsertWidgetBtnDisabled}}" userInput="Insert Widget" stepKey="seeInsertWidgetDisabled" /> -- <!--see Cancel button enabed--> -+ <!--see Cancel button enabled--> - <see selector="{{WidgetSection.CancelBtnEnabled}}" userInput="Cancel" stepKey="seeCancelBtnEnabled" /> - <!--Select "Widget Type"--> - <selectOption selector="{{WidgetSection.WidgetType}}" userInput="Catalog Category Link" stepKey="selectCatalogCategoryLink" /> -@@ -46,7 +46,9 @@ - <selectOption selector="{{WidgetSection.WidgetTemplate}}" userInput="Category Link Block Template" stepKey="selectTemplate" /> - <click selector="{{WidgetSection.BtnChooser}}" stepKey="clickSelectCategoryBtn" /> - <waitForLoadingMaskToDisappear stepKey="wait3"/> -- <click userInput="$$createPreReqCategory.name$$" stepKey="selectPreCreateCategory" /> -+ <click selector="{{AdminCategorySidebarTreeSection.expandRootCategory}}" stepKey="expandRootCategory" /> -+ <waitForElementVisible selector="{{WidgetSection.PreCreateCategory('$$createPreReqCategory.name$$')}}" stepKey="expandWait" /> -+ <click selector="{{WidgetSection.PreCreateCategory('$$createPreReqCategory.name$$')}}" stepKey="selectPreCreateCategory" /> - <waitForElementNotVisible selector="{{WidgetSection.SelectCategoryTitle}}" stepKey="waitForSlideoutCloses1" /> - <click selector="{{WidgetSection.InsertWidget}}" stepKey="clickInsertWidget" /> - <waitForElementNotVisible selector="{{WidgetSection.InsertWidgetTitle}}" stepKey="waitForSlideOutCloses2" /> -@@ -62,9 +64,19 @@ - <see userInput="Hello CMS Page!" stepKey="seeContent"/> - <!--see widget on Storefront--> - <see userInput="$$createPreReqCategory.name$$" stepKey="seeCategoryLink"/> -+ <magentoCLI command="config:set catalog/seo/generate_category_product_rewrites 0" stepKey="disableGenerateUrlRewrite"/> -+ <amOnPage url="{{_defaultCmsPage.identifier}}" stepKey="amOnPageTestPage2"/> -+ <waitForPageLoad stepKey="wait6" /> -+ <see userInput="Hello CMS Page!" stepKey="seeContent2"/> -+ <!--see widget on Storefront--> -+ <grabAttributeFrom selector=".widget a" userInput="href" stepKey="dataHref" /> -+ <assertRegExp expected="|$$createPreReqCategory.name$$.html|i" -+ expectedType="string" actual="$dataHref" actualType="variable" -+ stepKey="seeProductLinkInCategory"/> - <after> - <deleteData createDataKey="createPreReqCategory" stepKey="deletePreReqCatalog" /> - <actionGroup ref="DisabledWYSIWYG" stepKey="disableWYSIWYG"/> -+ <magentoCLI command="config:set catalog/seo/generate_category_product_rewrites 1" stepKey="enableGenerateUrlRewrite"/> - <actionGroup ref="logout" stepKey="logout"/> - </after> - </test> -diff --git a/app/code/Magento/Cms/Test/Mftf/Test/AdminAddWidgetToWYSIWYGWithCatalogProductLinkTypeTest.xml b/app/code/Magento/Cms/Test/Mftf/Test/AdminAddWidgetToWYSIWYGWithCatalogProductLinkTypeTest.xml -index 6d626b3a917..d75d422afa2 100644 ---- a/app/code/Magento/Cms/Test/Mftf/Test/AdminAddWidgetToWYSIWYGWithCatalogProductLinkTypeTest.xml -+++ b/app/code/Magento/Cms/Test/Mftf/Test/AdminAddWidgetToWYSIWYGWithCatalogProductLinkTypeTest.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="AdminAddWidgetToWYSIWYGWithCatalogProductLinkTypeTest"> - <annotations> - <features value="Cms"/> -@@ -41,7 +41,7 @@ - <waitForPageLoad stepKey="wait3"/> - <!--see Insert Widget button disabled--> - <see selector="{{WidgetSection.InsertWidgetBtnDisabled}}" userInput="Insert Widget" stepKey="seeInsertWidgetDisabled" /> -- <!--see Cancel button enabed--> -+ <!--see Cancel button enabled--> - <see selector="{{WidgetSection.CancelBtnEnabled}}" userInput="Cancel" stepKey="seeCancelBtnEnabled" /> - <!--Select "Widget Type"--> - <selectOption selector="{{WidgetSection.WidgetType}}" userInput="Catalog Product Link" stepKey="selectCatalogProductLink" /> -@@ -50,6 +50,8 @@ - <selectOption selector="{{WidgetSection.WidgetTemplate}}" userInput="Product Link Block Template" stepKey="selectTemplate" /> - <click selector="{{WidgetSection.BtnChooser}}" stepKey="clickSelectPageBtn" /> - <waitForLoadingMaskToDisappear stepKey="wait4"/> -+ <click selector="{{AdminCategorySidebarTreeSection.expandRootCategory}}" stepKey="expandRootCategory" /> -+ <waitForElementVisible selector="{{WidgetSection.PreCreateCategory('$$createPreReqCategory.name$$')}}" stepKey="expandWait" /> - <click selector="{{WidgetSection.PreCreateCategory('$$createPreReqCategory.name$$')}}" stepKey="selectPreCategory" /> - <waitForLoadingMaskToDisappear stepKey="waitLoadingMask" /> - <click selector="{{WidgetSection.PreCreateProduct('$$createPreReqProduct.name$$')}}" stepKey="selectPreProduct" /> -@@ -69,10 +71,19 @@ - <!--see widget on Storefront--> - <see userInput="Hello CMS Page!" stepKey="seeContent"/> - <see userInput="$$createPreReqProduct.name$$" stepKey="seeProductLink"/> -+ <magentoCLI command="config:set catalog/seo/generate_category_product_rewrites 0" stepKey="disableGenerateUrlRewrite"/> -+ <amOnPage url="{{_defaultCmsPage.identifier}}" stepKey="amOnPageTestPage2"/> -+ <waitForPageLoad stepKey="wait8" /> -+ <!--see widget on Storefront--> -+ <grabAttributeFrom selector=".widget a" userInput="href" stepKey="dataHref" /> -+ <assertRegExp expected="|$$createPreReqCategory.name$$/$$createPreReqProduct.name$$.html|i" -+ expectedType="string" actual="$dataHref" actualType="variable" -+ stepKey="seeProductLinkInCategory"/> - <after> - <deleteData createDataKey="createPreReqCategory" stepKey="deletePreReqCatalog" /> - <deleteData createDataKey="createPreReqProduct" stepKey="deletePreReqProduct" /> - <actionGroup ref="DisabledWYSIWYG" stepKey="disableWYSIWYG"/> -+ <magentoCLI command="config:set catalog/seo/generate_category_product_rewrites 1" stepKey="enableGenerateUrlRewrite"/> - <actionGroup ref="logout" stepKey="logout"/> - </after> - </test> -diff --git a/app/code/Magento/Cms/Test/Mftf/Test/AdminAddWidgetToWYSIWYGWithCatalogProductListTypeTest.xml b/app/code/Magento/Cms/Test/Mftf/Test/AdminAddWidgetToWYSIWYGWithCatalogProductListTypeTest.xml -index 69938147444..394d79bda1a 100644 ---- a/app/code/Magento/Cms/Test/Mftf/Test/AdminAddWidgetToWYSIWYGWithCatalogProductListTypeTest.xml -+++ b/app/code/Magento/Cms/Test/Mftf/Test/AdminAddWidgetToWYSIWYGWithCatalogProductListTypeTest.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="AdminAddWidgetToWYSIWYGWithCatalogProductListTypeTest"> - <annotations> - <features value="Cms"/> -@@ -42,7 +42,7 @@ - <see userInput="Inserting a widget does not create a widget instance." stepKey="seeMessage" /> - <!--see Insert Widget button disabled--> - <see selector="{{WidgetSection.InsertWidgetBtnDisabled}}" userInput="Insert Widget" stepKey="seeInsertWidgetDisabled" /> -- <!--see Cancel button enabed--> -+ <!--see Cancel button enabled--> - <see selector="{{WidgetSection.CancelBtnEnabled}}" userInput="Cancel" stepKey="seeCancelBtnEnabled" /> - <!--Select "Widget Type"--> - <selectOption selector="{{WidgetSection.WidgetType}}" userInput="Catalog Products List" stepKey="selectCatalogProductsList" /> -@@ -57,8 +57,29 @@ - <click selector="{{WidgetSection.RuleParam}}" stepKey="clickRuleParam" /> - <waitForElementVisible selector="{{WidgetSection.Chooser}}" stepKey="waitForElement" /> - <click selector="{{WidgetSection.Chooser}}" stepKey="clickChooser" /> -- <waitForLoadingMaskToDisappear stepKey="waitForLoadingMaskDisappear3" /> -+ <waitForLoadingMaskToDisappear stepKey="waitForLoadingMaskDisappear4" /> - <click selector="{{WidgetSection.PreCreateCategory('$$createPreReqCategory.name$$')}}" stepKey="selectPreCategory" /> -+ -+ <!-- Test that the "<" operand functions correctly --> -+ <click selector="{{WidgetSection.AddParam}}" stepKey="clickAddParamBtn2" /> -+ <waitForElementVisible selector="{{WidgetSection.ConditionsDropdown}}" stepKey="waitForDropdownVisible2"/> -+ <selectOption selector="{{WidgetSection.ConditionsDropdown}}" userInput="Price" stepKey="selectPriceCondition"/> -+ <waitForLoadingMaskToDisappear stepKey="waitForLoadingMaskDisappear3"/> -+ <click selector="{{WidgetSection.RuleParamLabel('2','1')}}" stepKey="clickOperatorLabel"/> -+ <selectOption selector="{{WidgetSection.RuleParamSelect('2','1')}}" userInput="<" stepKey="selectLessThanCondition"/> -+ <click selector="{{WidgetSection.RuleParam}}" stepKey="clickRuleParam2"/> -+ <fillField selector="{{WidgetSection.RuleParamInput('2','2')}}" userInput="125" stepKey="fillMaxPrice"/> -+ -+ <!-- Test that the ">" operand functions correctly --> -+ <click selector="{{WidgetSection.AddParam}}" stepKey="clickAddParamBtn3" /> -+ <waitForElementVisible selector="{{WidgetSection.ConditionsDropdown}}" stepKey="waitForDropdownVisible3"/> -+ <selectOption selector="{{WidgetSection.ConditionsDropdown}}" userInput="Price" stepKey="selectPriceCondition2"/> -+ <waitForLoadingMaskToDisappear stepKey="waitForLoadingMaskDisappear5"/> -+ <click selector="{{WidgetSection.RuleParamLabel('3','1')}}" stepKey="clickOperatorLabel2"/> -+ <selectOption selector="{{WidgetSection.RuleParamSelect('3','1')}}" userInput=">" stepKey="selectLessThanCondition2"/> -+ <click selector="{{WidgetSection.RuleParam}}" stepKey="clickRuleParam3"/> -+ <fillField selector="{{WidgetSection.RuleParamInput('3','2')}}" userInput="1" stepKey="fillMinPrice"/> -+ - <click selector="{{WidgetSection.InsertWidget}}" stepKey="clickInsertWidget" /> - <waitForPageLoad stepKey="wait6" /> - <scrollTo selector="{{CmsNewPagePageSeoSection.header}}" stepKey="scrollToSearchEngineTab" /> -diff --git a/app/code/Magento/Cms/Test/Mftf/Test/AdminAddWidgetToWYSIWYGWithRecentlyComparedProductsTypeTest.xml b/app/code/Magento/Cms/Test/Mftf/Test/AdminAddWidgetToWYSIWYGWithRecentlyComparedProductsTypeTest.xml -index 50441574d7a..862f51ea72f 100644 ---- a/app/code/Magento/Cms/Test/Mftf/Test/AdminAddWidgetToWYSIWYGWithRecentlyComparedProductsTypeTest.xml -+++ b/app/code/Magento/Cms/Test/Mftf/Test/AdminAddWidgetToWYSIWYGWithRecentlyComparedProductsTypeTest.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="AdminAddWidgetToWYSIWYGWithRecentlyComparedProductsTypeTest"> - <annotations> - <features value="Cms"/> -@@ -17,8 +17,6 @@ - <description value="Admin should be able to create a CMS page with widget type: Recently Compared Products"/> - <severity value="CRITICAL"/> - <testCaseId value="MAGETWO-83792"/> -- <!--Skip test due to MC-1284--> -- <group value="skip"/> - </annotations> - <!--Main test--> - <before> -@@ -43,7 +41,7 @@ - <waitForPageLoad stepKey="wait2"/> - <!--see Insert Widget button disabled--> - <see selector="{{WidgetSection.InsertWidgetBtnDisabled}}" userInput="Insert Widget" stepKey="seeInsertWidgetDisabled" /> -- <!--see Cancel button enabed--> -+ <!--see Cancel button enabled--> - <see selector="{{WidgetSection.CancelBtnEnabled}}" userInput="Cancel" stepKey="seeCancelBtnEnabled" /> - <!--Select "Widget Type"--> - <selectOption selector="{{WidgetSection.WidgetType}}" userInput="Recently Compared Products" stepKey="selectRecentlyComparedProducts" /> -diff --git a/app/code/Magento/Cms/Test/Mftf/Test/AdminAddWidgetToWYSIWYGWithRecentlyViewedProductsTypeTest.xml b/app/code/Magento/Cms/Test/Mftf/Test/AdminAddWidgetToWYSIWYGWithRecentlyViewedProductsTypeTest.xml -index 1574e6bd3b4..298aed917fc 100644 ---- a/app/code/Magento/Cms/Test/Mftf/Test/AdminAddWidgetToWYSIWYGWithRecentlyViewedProductsTypeTest.xml -+++ b/app/code/Magento/Cms/Test/Mftf/Test/AdminAddWidgetToWYSIWYGWithRecentlyViewedProductsTypeTest.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="AdminAddWidgetToWYSIWYGWithRecentlyViewedProductsTypeTest"> - <annotations> - <features value="Cms"/> -@@ -40,7 +40,7 @@ - <see userInput="Inserting a widget does not create a widget instance." stepKey="seeMessage" /> - <!--see Insert Widget button disabled--> - <see selector="{{WidgetSection.InsertWidgetBtnDisabled}}" userInput="Insert Widget" stepKey="seeInsertWidgetDisabled" /> -- <!--see Cancel button enabed--> -+ <!--see Cancel button enabled--> - <see selector="{{WidgetSection.CancelBtnEnabled}}" userInput="Cancel" stepKey="seeCancelBtnEnabled" /> - <!--Select "Widget Type"--> - <selectOption selector="{{WidgetSection.WidgetType}}" userInput="Recently Viewed Products" stepKey="selectRecentlyViewedProducts" /> -diff --git a/app/code/Magento/Cms/Test/Mftf/Test/AdminCmsPageMassActionTest.xml b/app/code/Magento/Cms/Test/Mftf/Test/AdminCmsPageMassActionTest.xml -new file mode 100644 -index 00000000000..7cc0719dcbe ---- /dev/null -+++ b/app/code/Magento/Cms/Test/Mftf/Test/AdminCmsPageMassActionTest.xml -@@ -0,0 +1,85 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> -+ <test name="AdminCmsPageMassActionTest"> -+ <annotations> -+ <features value="CmsPage"/> -+ <title value="Create two CMS Pages and perform mass disable action"/> -+ <description value="Admin should be able to perform mass actions to CMS pages"/> -+ <stories value="Admin Grid Mass Action" /> -+ <testCaseId value="MC-14659" /> -+ <severity value="CRITICAL"/> -+ <group value="backend"/> -+ <group value="CMSContent"/> -+ <group value="mtf_migrated"/> -+ </annotations> -+ <before> -+ <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> -+ <createData entity="_defaultCmsPage" stepKey="firstCMSPage" /> -+ <createData entity="_duplicatedCMSPage" stepKey="secondCMSPage" /> -+ </before> -+ <after> -+ <deleteData createDataKey="firstCMSPage" stepKey="deleteFirstCMSPage" /> -+ <deleteData createDataKey="secondCMSPage" stepKey="deleteSecondCMSPage" /> -+ <actionGroup ref="logout" stepKey="logout"/> -+ </after> -+ -+ <!--Go to Grid page--> -+ <actionGroup ref="AdminOpenCMSPagesGridActionGroup" stepKey="navigateToCMSPageGrid"/> -+ <actionGroup ref="AdminGridFilterResetActionGroup" stepKey="clearPossibleGridFilters"/> -+ -+ <!--Select pages in Grid--> -+ <actionGroup ref="AdminSelectCMSPageInGridActionGroup" stepKey="selectFirstCMSPage"> -+ <argument name="identifier" value="$$firstCMSPage.identifier$$"/> -+ </actionGroup> -+ <actionGroup ref="AdminSelectCMSPageInGridActionGroup" stepKey="selectSecondCMSPage"> -+ <argument name="identifier" value="$$secondCMSPage.identifier$$"/> -+ </actionGroup> -+ -+ <!-- Disable Pages--> -+ <actionGroup ref="AdminCMSPageMassActionSelectActionGroup" stepKey="disablePages"> -+ <argument name="action" value="Disable" /> -+ </actionGroup> -+ -+ <actionGroup ref="AssertMessageInAdminPanelActionGroup" stepKey="assertSuccessMessage"> -+ <argument name="message" value="A total of 2 record(s) have been disabled." /> -+ </actionGroup> -+ -+ <!--Verify pages in Grid--> -+ <actionGroup ref="AdminGridFilterResetActionGroup" stepKey="clearGridFilters"/> -+ <actionGroup ref="AdminGridFilterFillInputFieldActionGroup" stepKey="filterGridByFirstCmsPageIdentifier"> -+ <argument name="filterInputName" value="identifier" /> -+ <argument name="filterValue" value="$$firstCMSPage.identifier$$" /> -+ </actionGroup> -+ <actionGroup ref="AdminGridFilterApplyActionGroup" stepKey="applyFirstGridFilters"/> -+ <actionGroup ref="AssertCMSPageInGridActionGroup" stepKey="assertFirstCmsPageInGrid"> -+ <argument name="cmsPage" value="$$firstCMSPage$$" /> -+ </actionGroup> -+ -+ <actionGroup ref="AdminGridFilterFillInputFieldActionGroup" stepKey="filterGridBySecondCmsPageIdentifier"> -+ <argument name="filterInputName" value="identifier" /> -+ <argument name="filterValue" value="$$secondCMSPage.identifier$$" /> -+ </actionGroup> -+ <actionGroup ref="AdminGridFilterApplyActionGroup" stepKey="applySecondGridFilters"/> -+ <actionGroup ref="AssertCMSPageInGridActionGroup" stepKey="assertSecondCmsPageInGrid"> -+ <argument name="cmsPage" value="$$secondCMSPage$$" /> -+ </actionGroup> -+ <actionGroup ref="AdminGridFilterResetActionGroup" stepKey="clearGridFiltersToIsolateTest"/> -+ -+ <!--Verify pages are disabled on Storefront--> -+ <actionGroup ref="StorefrontGoToCMSPageActionGroup" stepKey="goToFirstCMSPageOnStorefront"> -+ <argument name="identifier" value="$$firstCMSPage.identifier$$"/> -+ </actionGroup> -+ <actionGroup ref="AssertCMSPageNotFoundOnStorefrontActionGroup" stepKey="seeNotFoundErrorForFirstPage"/> -+ <actionGroup ref="StorefrontGoToCMSPageActionGroup" stepKey="goToSecondCMSPageOnStorefront"> -+ <argument name="identifier" value="$$secondCMSPage.identifier$$"/> -+ </actionGroup> -+ <actionGroup ref="AssertCMSPageNotFoundOnStorefrontActionGroup" stepKey="seeNotFoundErrorForSecondPage"/> -+ </test> -+</tests> -diff --git a/app/code/Magento/Cms/Test/Mftf/Test/AdminContentBlocksNavigateMenuTest.xml b/app/code/Magento/Cms/Test/Mftf/Test/AdminContentBlocksNavigateMenuTest.xml -new file mode 100644 -index 00000000000..19f501d6aa2 ---- /dev/null -+++ b/app/code/Magento/Cms/Test/Mftf/Test/AdminContentBlocksNavigateMenuTest.xml -@@ -0,0 +1,36 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> -+ <test name="AdminContentBlocksNavigateMenuTest"> -+ <annotations> -+ <features value="Cms"/> -+ <stories value="Menu Navigation"/> -+ <title value="Admin content blocks navigate menu test"/> -+ <description value="Admin should be able to navigate to Content > Blocks"/> -+ <severity value="CRITICAL"/> -+ <testCaseId value="MC-14129"/> -+ <group value="menu"/> -+ <group value="mtf_migrated"/> -+ </annotations> -+ <before> -+ <actionGroup ref="LoginAsAdmin" stepKey="LoginAsAdmin"/> -+ </before> -+ <after> -+ <actionGroup ref="logout" stepKey="logout"/> -+ </after> -+ <actionGroup ref="AdminNavigateMenuActionGroup" stepKey="navigateToContentBlocksPage"> -+ <argument name="menuUiId" value="{{AdminMenuContent.dataUiId}}"/> -+ <argument name="submenuUiId" value="{{AdminMenuContentElementsBlocks.dataUiId}}"/> -+ </actionGroup> -+ <actionGroup ref="AdminAssertPageTitleActionGroup" stepKey="seePageTitle"> -+ <argument name="title" value="{{AdminMenuContentElementsBlocks.pageTitle}}"/> -+ </actionGroup> -+ </test> -+</tests> -diff --git a/app/code/Magento/Cms/Test/Mftf/Test/AdminContentPagesNavigateMenuTest.xml b/app/code/Magento/Cms/Test/Mftf/Test/AdminContentPagesNavigateMenuTest.xml -new file mode 100644 -index 00000000000..323a1de7b9a ---- /dev/null -+++ b/app/code/Magento/Cms/Test/Mftf/Test/AdminContentPagesNavigateMenuTest.xml -@@ -0,0 +1,36 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> -+ <test name="AdminContentPagesNavigateMenuTest"> -+ <annotations> -+ <features value="Cms"/> -+ <stories value="Menu Navigation"/> -+ <title value="Admin content pages navigate menu test"/> -+ <description value="Admin should be able to navigate to Content > Pages"/> -+ <severity value="CRITICAL"/> -+ <testCaseId value="MC-14128"/> -+ <group value="menu"/> -+ <group value="mtf_migrated"/> -+ </annotations> -+ <before> -+ <actionGroup ref="LoginAsAdmin" stepKey="LoginAsAdmin"/> -+ </before> -+ <after> -+ <actionGroup ref="logout" stepKey="logout"/> -+ </after> -+ <actionGroup ref="AdminNavigateMenuActionGroup" stepKey="navigateToContentPagesPage"> -+ <argument name="menuUiId" value="{{AdminMenuContent.dataUiId}}"/> -+ <argument name="submenuUiId" value="{{AdminMenuContentElementsPages.dataUiId}}"/> -+ </actionGroup> -+ <actionGroup ref="AdminAssertPageTitleActionGroup" stepKey="seePageTitle"> -+ <argument name="title" value="{{AdminMenuContentElementsPages.pageTitle}}"/> -+ </actionGroup> -+ </test> -+</tests> -diff --git a/app/code/Magento/Cms/Test/Mftf/Test/AdminCreateCmsBlockTest.xml b/app/code/Magento/Cms/Test/Mftf/Test/AdminCreateCmsBlockTest.xml -index 3b80204f5c3..7ab0d9209dd 100644 ---- a/app/code/Magento/Cms/Test/Mftf/Test/AdminCreateCmsBlockTest.xml -+++ b/app/code/Magento/Cms/Test/Mftf/Test/AdminCreateCmsBlockTest.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="AdminCreateDuplicatedCmsBlockTest"> - <annotations> - <features value="Cms"/> -@@ -23,6 +23,9 @@ - <actionGroup ref="DisabledWYSIWYG" stepKey="disableWYSIWYG"/> - </before> - <after> -+ <actionGroup ref="deleteBlock" stepKey="deleteCreatedBlock"> -+ <argument name="Block" value="_defaultBlock"/> -+ </actionGroup> - <actionGroup ref="logout" stepKey="logout"/> - </after> - <amOnPage url="{{CmsNewBlock.url}}" stepKey="amOnBlocksCreationForm"/> -diff --git a/app/code/Magento/Cms/Test/Mftf/Test/AdminCreateCmsPageTest.xml b/app/code/Magento/Cms/Test/Mftf/Test/AdminCreateCmsPageTest.xml -index 73e38fcdad5..fccc5b5980f 100644 ---- a/app/code/Magento/Cms/Test/Mftf/Test/AdminCreateCmsPageTest.xml -+++ b/app/code/Magento/Cms/Test/Mftf/Test/AdminCreateCmsPageTest.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="AdminCreateCmsPageTest"> - <annotations> - <features value="Cms"/> -diff --git a/app/code/Magento/Cms/Test/Mftf/Test/CheckStaticBlocksTest.xml b/app/code/Magento/Cms/Test/Mftf/Test/CheckStaticBlocksTest.xml -new file mode 100644 -index 00000000000..e6ab1c13060 ---- /dev/null -+++ b/app/code/Magento/Cms/Test/Mftf/Test/CheckStaticBlocksTest.xml -@@ -0,0 +1,74 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> -+ <test name="CheckStaticBlocksTest"> -+ <annotations> -+ <features value="Cms"/> -+ <stories value="MAGETWO-91559 - Static blocks with same ID appear in place of correct block"/> -+ <title value="Check static blocks: ID should be unique per Store View"/> -+ <description value="Check static blocks: ID should be unique per Store View"/> -+ <severity value="CRITICAL"/> -+ <testCaseId value="MAGETWO-94229"/> -+ <group value="Cms"/> -+ </annotations> -+ <before> -+ <magentoCLI command="config:set cms/wysiwyg/enabled disabled" stepKey="disableWYSIWYG"/> -+ <actionGroup ref="LoginAsAdmin" stepKey="LoginAsAdmin"/> -+ <actionGroup ref="AdminCreateWebsiteActionGroup" stepKey="AdminCreateWebsite"> -+ <argument name="newWebsiteName" value="secondWebsite"/> -+ <argument name="websiteCode" value="second_website"/> -+ </actionGroup> -+ <actionGroup ref="AdminCreateNewStoreGroupActionGroup" stepKey="AdminCreateStore"> -+ <argument name="website" value="secondWebsite"/> -+ <argument name="storeGroupName" value="{{customStoreGroup.name}}"/> -+ <argument name="storeGroupCode" value="{{customStoreGroup.code}}"/> -+ </actionGroup> -+ <actionGroup ref="AdminCreateStoreViewActionGroup" stepKey="AdminCreateStoreView"> -+ <argument name="StoreGroup" value="customStoreGroup"/> -+ <argument name="customStore" value="customStore"/> -+ </actionGroup> -+ </before> -+ -+ <!--Go to Cms blocks page--> -+ <amOnPage url="{{CmsBlocksPage.url}}" stepKey="navigateToCMSPagesGrid"/> -+ <waitForPageLoad stepKey="waitForPageLoad1"/> -+ <seeInCurrentUrl url="cms/block/" stepKey="VerifyPageIsOpened"/> -+ <!--Click to create new block--> -+ <click selector="{{BlockPageActionsSection.addNewBlock}}" stepKey="ClickToAddNewBlock"/> -+ <waitForPageLoad stepKey="waitForPageLoad2"/> -+ <seeInCurrentUrl url="cms/block/new" stepKey="VerifyNewBlockPageIsOpened"/> -+ <actionGroup ref="FillOutBlockContent" stepKey="FillOutBlockContent"/> -+ <click selector="{{BlockNewPagePageActionsSection.saveBlock}}" stepKey="ClickToSaveBlock"/> -+ <waitForPageLoad stepKey="waitForPageLoad3"/> -+ <see userInput="You saved the block." stepKey="VerifyBlockIsSaved"/> -+ <!--Click to go back and add new block--> -+ <click selector="{{BlockNewPagePageActionsSection.back}}" stepKey="ClickToGoBack"/> -+ <waitForPageLoad stepKey="waitForPageLoad4"/> -+ <click selector="{{BlockPageActionsSection.addNewBlock}}" stepKey="ClickToAddNewBlock1"/> -+ <waitForPageLoad stepKey="waitForPageLoad5"/> -+ <seeInCurrentUrl url="cms/block/new" stepKey="VerifyNewBlockPageIsOpened1"/> -+ <!--Add new BLock with the same data--> -+ <actionGroup ref="FillOutBlockContent" stepKey="FillOutBlockContent1"/> -+ <selectOption selector="{{BlockNewPageBasicFieldsSection.storeView}}" userInput="Default Store View" stepKey="selectDefaultStoreView" /> -+ <selectOption selector="{{BlockNewPageBasicFieldsSection.storeView}}" userInput="{{customStore.name}}" stepKey="selectSecondStoreView1" /> -+ <click selector="{{BlockNewPagePageActionsSection.saveBlock}}" stepKey="ClickToSaveBlock1"/> -+ <waitForPageLoad stepKey="waitForPageLoad6"/> -+ <!--Verify that corresponding message is displayed--> -+ <see userInput="A block identifier with the same properties already exists in the selected store." stepKey="VerifyBlockIsSaved1"/> -+ -+ <after> -+ <actionGroup ref="AdminDeleteWebsiteActionGroup" stepKey="DeleteWebsite"> -+ <argument name="websiteName" value="secondWebsite"/> -+ </actionGroup> -+ <actionGroup ref="DeleteCMSBlockActionGroup" stepKey="DeleteCMSBlockActionGroup"/> -+ <magentoCLI command="config:set cms/wysiwyg/enabled enabled" stepKey="enableWYSIWYG"/> -+ </after> -+ </test> -+</tests> -diff --git a/app/code/Magento/Cms/Test/Mftf/Test/StoreFrontMobileViewValidation.xml b/app/code/Magento/Cms/Test/Mftf/Test/StoreFrontMobileViewValidation.xml -new file mode 100644 -index 00000000000..6165def067e ---- /dev/null -+++ b/app/code/Magento/Cms/Test/Mftf/Test/StoreFrontMobileViewValidation.xml -@@ -0,0 +1,47 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> -+ <test name="StoreFrontMobileViewValidation"> -+ <annotations> -+ <features value="Cms"/> -+ <stories value="Mobile view page footer should stick to the bottom of page on Store front"/> -+ <title value="Mobile view page footer should stick to the bottom of page on Store front"/> -+ <description value="Mobile view page footer should stick to the bottom of page on Store front"/> -+ <severity value="MAJOR"/> -+ <testCaseId value="MAGETWO-94333"/> -+ <useCaseId value="MAGETWO-93978"/> -+ <group value="Cms"/> -+ </annotations> -+ <before> -+ <createData entity="_longContentCmsPage" stepKey="createPreReqCMSPage"/> -+ </before> -+ <after> -+ <deleteData createDataKey="createPreReqCMSPage" stepKey="deletePreReqCMSPage"/> -+ <resizeWindow width="1280" height="1024" stepKey="resizeWindowToDesktop"/> -+ </after> -+ <resizeWindow width="375" height="812" stepKey="resizeWindowToMobile"/> -+ <amOnPage url="$$createPreReqCMSPage.identifier$$" stepKey="amOnPageTestPage"/> -+ <waitForPageLoad stepKey="waitForPageLoad6" /> -+ <!--Verifying that Footer is not in visible area by default as the CMS page has lots of content which will occupy entire visible area--> -+ <executeJS function="return document.querySelector('{{StorefrontCMSPageSection.footerTop}}').getBoundingClientRect().top" stepKey="topOfFooter"/> -+ <assertGreaterThan stepKey="assertDefaultLoad"> -+ <actualResult type="variable">topOfFooter</actualResult> -+ <expectedResult type="string">812</expectedResult> -+ </assertGreaterThan> -+ <!--Verifying that even after scroll footer section is below the main content section--> -+ <scrollTo selector="{{StorefrontCMSPageSection.footerTop}}" stepKey="scrollToFooterSection"/> -+ <executeJS function="return document.querySelector('{{StorefrontCMSPageSection.footerTop}}').getBoundingClientRect().top" stepKey="topOfTheFooterAfterScroll"/> -+ <executeJS function="return document.querySelector('{{StorefrontCMSPageSection.mainContent}}').getBoundingClientRect().bottom" stepKey="bottomOfMainContent"/> -+ <assertGreaterThan stepKey="assertAfterScroll"> -+ <actualResult type="variable">topOfTheFooterAfterScroll</actualResult> -+ <expectedResult type="variable">bottomOfMainContent</expectedResult> -+ </assertGreaterThan> -+ </test> -+</tests> -diff --git a/app/code/Magento/Cms/Test/Mftf/Test/StoreViewLanguageCorrectSwitchTest.xml b/app/code/Magento/Cms/Test/Mftf/Test/StoreViewLanguageCorrectSwitchTest.xml -new file mode 100644 -index 00000000000..d4796b2ef7d ---- /dev/null -+++ b/app/code/Magento/Cms/Test/Mftf/Test/StoreViewLanguageCorrectSwitchTest.xml -@@ -0,0 +1,68 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> -+ <test name="StoreViewLanguageCorrectSwitchTest"> -+ <annotations> -+ <features value="Cms"/> -+ <stories value="Store view language"/> -+ <title value="Check that Store View(language) switches correct"/> -+ <description value="Check that Store View(language) switches correct"/> -+ <severity value="MAJOR"/> -+ <testCaseId value="MAGETWO-96388"/> -+ <useCaseId value="MAGETWO-57337"/> -+ <group value="Cms"/> -+ </annotations> -+ <before> -+ <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> -+ -+ <!-- Create Cms Pages --> -+ <createData entity="_newDefaultCmsPage" stepKey="createFirstCmsPage"/> -+ <createData entity="_newDefaultCmsPage" stepKey="createSecondCmsPage"/> -+ </before> -+ <after> -+ <deleteData createDataKey="createFirstCmsPage" stepKey="deleteFirstCmsPage"/> -+ <deleteData createDataKey="createSecondCmsPage" stepKey="deleteSecondCmsPage"/> -+ <actionGroup ref="AdminDeleteStoreViewActionGroup" stepKey="deleteStoreView"> -+ <argument name="customStore" value="NewStoreViewData"/> -+ </actionGroup> -+ <actionGroup ref="logout" stepKey="logout"/> -+ </after> -+ -+ <!-- Create StoreView --> -+ <actionGroup ref="AdminCreateStoreViewActionGroup" stepKey="createStoreView"> -+ <argument name="customStore" value="NewStoreViewData"/> -+ </actionGroup> -+ -+ <!-- Add StoreView To Cms Page--> -+ <actionGroup ref="AddStoreViewToCmsPage" stepKey="gotToCmsPage"> -+ <argument name="CMSPage" value="$$createSecondCmsPage$$"/> -+ <argument name="storeViewName" value="{{NewStoreViewData.name}}"/> -+ </actionGroup> -+ -+ <!-- Check that Cms Page is open --> -+ <amOnPage url="{{StorefrontHomePage.url}}/$$createFirstCmsPage.identifier$$" stepKey="gotToFirstCmsPage"/> -+ <see userInput="$$createFirstCmsPage.title$$" stepKey="seePageTitle"/> -+ -+ <!-- Switch StoreView and check that Cms Page is open --> -+ <actionGroup ref="StorefrontSwitchStoreViewActionGroup" stepKey="switchToCustomStoreView"> -+ <argument name="storeView" value="NewStoreViewData"/> -+ </actionGroup> -+ <amOnPage url="{{StorefrontHomePage.url}}/$$createSecondCmsPage.identifier$$" stepKey="gotToSecondCmsPage"/> -+ <see userInput="$$createSecondCmsPage.title$$" stepKey="seePageTitle1"/> -+ -+ <!--Open first Cms page on custom store view--> -+ <amOnPage url="{{StorefrontHomePage.url}}/$$createFirstCmsPage.identifier$$" stepKey="gotToFirstCmsPage1"/> -+ <see userInput="Whoops, our bad..." stepKey="seePageError"/> -+ -+ <!--Switch to default store view and check Cms page--> -+ <actionGroup ref="StorefrontSwitchDefaultStoreViewActionGroup" stepKey="switchToDefualtStoreView"/> -+ <see userInput="$$createFirstCmsPage.title$$" stepKey="seePageTitle2"/> -+ </test> -+</tests> -diff --git a/app/code/Magento/Cms/Test/Mftf/Test/VerifyTinyMCEv4IsNativeWYSIWYGOnBlockTest.xml b/app/code/Magento/Cms/Test/Mftf/Test/VerifyTinyMCEv4IsNativeWYSIWYGOnBlockTest.xml -index 3d45d4baae7..9ee2055aae6 100644 ---- a/app/code/Magento/Cms/Test/Mftf/Test/VerifyTinyMCEv4IsNativeWYSIWYGOnBlockTest.xml -+++ b/app/code/Magento/Cms/Test/Mftf/Test/VerifyTinyMCEv4IsNativeWYSIWYGOnBlockTest.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="VerifyTinyMCEv4IsNativeWYSIWYGOnBlockTest"> - <annotations> - <features value="Cms"/> -diff --git a/app/code/Magento/Cms/Test/Mftf/Test/VerifyTinyMCEv4IsNativeWYSIWYGOnCMSPageTest.xml b/app/code/Magento/Cms/Test/Mftf/Test/VerifyTinyMCEv4IsNativeWYSIWYGOnCMSPageTest.xml -index 2bfdc5f5037..caad1cabe78 100644 ---- a/app/code/Magento/Cms/Test/Mftf/Test/VerifyTinyMCEv4IsNativeWYSIWYGOnCMSPageTest.xml -+++ b/app/code/Magento/Cms/Test/Mftf/Test/VerifyTinyMCEv4IsNativeWYSIWYGOnCMSPageTest.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="VerifyTinyMCEv4IsNativeWYSIWYGOnCMSPageTest"> - <annotations> - <features value="Cms"/> -diff --git a/app/code/Magento/Cms/Test/Unit/Model/BlockTest.php b/app/code/Magento/Cms/Test/Unit/Model/BlockTest.php -new file mode 100644 -index 00000000000..448112b228a ---- /dev/null -+++ b/app/code/Magento/Cms/Test/Unit/Model/BlockTest.php -@@ -0,0 +1,337 @@ -+<?php -+/** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+declare(strict_types=1); -+ -+namespace Magento\Cms\Test\Unit\Model; -+ -+use Magento\Cms\Model\Block; -+use Magento\Cms\Model\ResourceModel\Block as BlockResource; -+use Magento\Framework\Event\ManagerInterface; -+use Magento\Framework\Exception\LocalizedException; -+use Magento\Framework\Model\Context; -+use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; -+ -+/** -+ * @covers \Magento\Cms\Model\Block -+ */ -+class BlockTest extends \PHPUnit\Framework\TestCase -+{ -+ /** -+ * Testable Object -+ * -+ * @var Block -+ */ -+ private $blockModel; -+ -+ /** -+ * Object Manager -+ * -+ * @var ObjectManager -+ */ -+ private $objectManager; -+ -+ /** -+ * @var ManagerInterface|\PHPUnit_Framework_MockObject_MockObject -+ */ -+ private $eventManagerMock; -+ -+ /** -+ * @var Context|\PHPUnit_Framework_MockObject_MockObject -+ */ -+ private $contextMock; -+ -+ /** -+ * @var BlockResource|\PHPUnit_Framework_MockObject_MockObject -+ */ -+ private $resourceMock; -+ -+ /** -+ * Set Up -+ * -+ * @return void -+ */ -+ protected function setUp() -+ { -+ $this->resourceMock = $this->createMock(BlockResource::class); -+ $this->eventManagerMock = $this->createMock(ManagerInterface::class); -+ $this->contextMock = $this->createMock(Context::class); -+ $this->contextMock->expects($this->any())->method('getEventDispatcher')->willReturn($this->eventManagerMock); -+ $this->objectManager = new ObjectManager($this); -+ $this->blockModel = $this->objectManager->getObject( -+ Block::class, -+ [ -+ 'context' => $this->contextMock, -+ 'resource' => $this->resourceMock, -+ ] -+ ); -+ } -+ -+ /** -+ * Test beforeSave method -+ * -+ * @return void -+ * -+ * @throws LocalizedException -+ */ -+ public function testBeforeSave() -+ { -+ $blockId = 7; -+ $this->blockModel->setData(Block::BLOCK_ID, $blockId); -+ $this->blockModel->setData(Block::CONTENT, 'test'); -+ $this->objectManager->setBackwardCompatibleProperty($this->blockModel, '_hasDataChanges', true); -+ $this->eventManagerMock->expects($this->atLeastOnce())->method('dispatch'); -+ $expected = $this->blockModel; -+ $actual = $this->blockModel->beforeSave(); -+ self::assertEquals($expected, $actual); -+ } -+ -+ /** -+ * Test beforeSave method -+ * -+ * @return void -+ * -+ * @throws LocalizedException -+ */ -+ public function testBeforeSaveWithException() -+ { -+ $blockId = 10; -+ $this->blockModel->setData(Block::BLOCK_ID, $blockId); -+ $this->blockModel->setData(Block::CONTENT, 'Test block_id="' . $blockId . '".'); -+ $this->objectManager->setBackwardCompatibleProperty($this->blockModel, '_hasDataChanges', false); -+ $this->eventManagerMock->expects($this->never())->method('dispatch'); -+ $this->expectException(LocalizedException::class); -+ $this->blockModel->beforeSave(); -+ } -+ -+ /** -+ * Test getIdentities method -+ * -+ * @return void -+ */ -+ public function testGetIdentities() -+ { -+ $result = $this->blockModel->getIdentities(); -+ self::assertInternalType('array', $result); -+ } -+ -+ /** -+ * Test getId method -+ * -+ * @return void -+ */ -+ public function testGetId() -+ { -+ $blockId = 12; -+ $this->blockModel->setData(Block::BLOCK_ID, $blockId); -+ $expected = $blockId; -+ $actual = $this->blockModel->getId(); -+ self::assertEquals($expected, $actual); -+ } -+ -+ /** -+ * Test getIdentifier method -+ * -+ * @return void -+ */ -+ public function testGetIdentifier() -+ { -+ $identifier = 'test01'; -+ $this->blockModel->setData(Block::IDENTIFIER, $identifier); -+ $expected = $identifier; -+ $actual = $this->blockModel->getIdentifier(); -+ self::assertEquals($expected, $actual); -+ } -+ -+ /** -+ * Test getTitle method -+ * -+ * @return void -+ */ -+ public function testGetTitle() -+ { -+ $title = 'test02'; -+ $this->blockModel->setData(Block::TITLE, $title); -+ $expected = $title; -+ $actual = $this->blockModel->getTitle(); -+ self::assertEquals($expected, $actual); -+ } -+ -+ /** -+ * Test getContent method -+ * -+ * @return void -+ */ -+ public function testGetContent() -+ { -+ $content = 'test03'; -+ $this->blockModel->setData(Block::CONTENT, $content); -+ $expected = $content; -+ $actual = $this->blockModel->getContent(); -+ self::assertEquals($expected, $actual); -+ } -+ -+ /** -+ * Test getCreationTime method -+ * -+ * @return void -+ */ -+ public function testGetCreationTime() -+ { -+ $creationTime = 'test04'; -+ $this->blockModel->setData(Block::CREATION_TIME, $creationTime); -+ $expected = $creationTime; -+ $actual = $this->blockModel->getCreationTime(); -+ self::assertEquals($expected, $actual); -+ } -+ -+ /** -+ * Test getUpdateTime method -+ * -+ * @return void -+ */ -+ public function testGetUpdateTime() -+ { -+ $updateTime = 'test05'; -+ $this->blockModel->setData(Block::UPDATE_TIME, $updateTime); -+ $expected = $updateTime; -+ $actual = $this->blockModel->getUpdateTime(); -+ self::assertEquals($expected, $actual); -+ } -+ -+ /** -+ * Test isActive method -+ * -+ * @return void -+ */ -+ public function testIsActive() -+ { -+ $isActive = true; -+ $this->blockModel->setData(Block::IS_ACTIVE, $isActive); -+ $result = $this->blockModel->isActive(); -+ self::assertTrue($result); -+ } -+ -+ /** -+ * Test setId method -+ * -+ * @return void -+ */ -+ public function testSetId() -+ { -+ $blockId = 15; -+ $this->blockModel->setId($blockId); -+ $expected = $blockId; -+ $actual = $this->blockModel->getData(Block::BLOCK_ID); -+ self::assertEquals($expected, $actual); -+ } -+ -+ /** -+ * Test setIdentifier method -+ * -+ * @return void -+ */ -+ public function testSetIdentifier() -+ { -+ $identifier = 'test06'; -+ $this->blockModel->setIdentifier($identifier); -+ $expected = $identifier; -+ $actual = $this->blockModel->getData(Block::IDENTIFIER); -+ self::assertEquals($expected, $actual); -+ } -+ -+ /** -+ * Test setTitle method -+ * -+ * @return void -+ */ -+ public function testSetTitle() -+ { -+ $title = 'test07'; -+ $this->blockModel->setTitle($title); -+ $expected = $title; -+ $actual = $this->blockModel->getData(Block::TITLE); -+ self::assertEquals($expected, $actual); -+ } -+ -+ /** -+ * Test setContent method -+ * -+ * @return void -+ */ -+ public function testSetContent() -+ { -+ $content = 'test08'; -+ $this->blockModel->setContent($content); -+ $expected = $content; -+ $actual = $this->blockModel->getData(Block::CONTENT); -+ self::assertEquals($expected, $actual); -+ } -+ -+ /** -+ * Test setCreationTime method -+ * -+ * @return void -+ */ -+ public function testSetCreationTime() -+ { -+ $creationTime = 'test09'; -+ $this->blockModel->setCreationTime($creationTime); -+ $expected = $creationTime; -+ $actual = $this->blockModel->getData(Block::CREATION_TIME); -+ self::assertEquals($expected, $actual); -+ } -+ -+ /** -+ * Test setUpdateTime method -+ * -+ * @return void -+ */ -+ public function testSetUpdateTime() -+ { -+ $updateTime = 'test10'; -+ $this->blockModel->setUpdateTime($updateTime); -+ $expected = $updateTime; -+ $actual = $this->blockModel->getData(Block::UPDATE_TIME); -+ self::assertEquals($expected, $actual); -+ } -+ -+ /** -+ * Test setIsActive method -+ * -+ * @return void -+ */ -+ public function testSetIsActive() -+ { -+ $this->blockModel->setIsActive(false); -+ $result = $this->blockModel->getData(Block::IS_ACTIVE); -+ self::assertFalse($result); -+ } -+ -+ /** -+ * Test getStores method -+ * -+ * @return void -+ */ -+ public function testGetStores() -+ { -+ $stores = [1, 4, 9]; -+ $this->blockModel->setData('stores', $stores); -+ $expected = $stores; -+ $actual = $this->blockModel->getStores(); -+ self::assertEquals($expected, $actual); -+ } -+ -+ /** -+ * Test getAvailableStatuses method -+ * -+ * @return void -+ */ -+ public function testGetAvailableStatuses() -+ { -+ $result = $this->blockModel->getAvailableStatuses(); -+ self::assertInternalType('array', $result); -+ } -+} -diff --git a/app/code/Magento/Cms/Test/Unit/Model/Plugin/ProductTest.php b/app/code/Magento/Cms/Test/Unit/Model/Plugin/ProductTest.php -new file mode 100644 -index 00000000000..3e8d8116236 ---- /dev/null -+++ b/app/code/Magento/Cms/Test/Unit/Model/Plugin/ProductTest.php -@@ -0,0 +1,93 @@ -+<?php -+/** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+declare(strict_types=1); -+ -+namespace Magento\Cms\Test\Unit\Model\Plugin; -+ -+use Magento\Catalog\Model\Product as CatalogProduct; -+use Magento\Cms\Model\Page; -+use Magento\Cms\Model\Plugin\Product; -+use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; -+use PHPUnit_Framework_MockObject_MockObject as MockObject; -+use PHPUnit\Framework\TestCase; -+ -+/** -+ * Product plugin test -+ */ -+class ProductTest extends TestCase -+{ -+ /** -+ * @var Product -+ */ -+ private $plugin; -+ -+ /** -+ * @var MockObject|CatalogProduct -+ */ -+ private $product; -+ -+ /** -+ * @var MockObject|Page -+ */ -+ private $page; -+ -+ /** -+ * @inheritdoc -+ */ -+ protected function setUp() -+ { -+ $objectManager = new ObjectManager($this); -+ -+ $this->product = $this->getMockBuilder(CatalogProduct::class) -+ ->disableOriginalConstructor() -+ ->setMethods(['getEntityId', 'getOrigData', 'getData', 'getCategoryIds']) -+ ->getMock(); -+ -+ $this->page = $this->getMockBuilder(Page::class) -+ ->disableOriginalConstructor() -+ ->setMethods(['getId', 'load']) -+ ->getMock(); -+ -+ $this->plugin = $objectManager->getObject( -+ Product::class, -+ [ -+ 'page' => $this->page -+ ] -+ ); -+ } -+ -+ public function testAfterGetIdentities() -+ { -+ $baseIdentities = [ -+ 'SomeCacheId', -+ 'AnotherCacheId', -+ ]; -+ $id = 12345; -+ $pageId = 1; -+ $expectedIdentities = [ -+ 'SomeCacheId', -+ 'AnotherCacheId', -+ Page::CACHE_TAG . '_' . $pageId, -+ ]; -+ -+ $this->product->method('getEntityId') -+ ->willReturn($id); -+ $this->product->method('getOrigData') -+ ->with('status') -+ ->willReturn(2); -+ $this->product->method('getData') -+ ->with('status') -+ ->willReturn(1); -+ $this->page->method('getId') -+ ->willReturn(1); -+ $this->page->method('load') -+ ->willReturnSelf(); -+ -+ $identities = $this->plugin->afterGetIdentities($this->product, $baseIdentities); -+ -+ $this->assertEquals($expectedIdentities, $identities); -+ } -+} -diff --git a/app/code/Magento/Cms/Test/Unit/Model/Wysiwyg/Images/StorageTest.php b/app/code/Magento/Cms/Test/Unit/Model/Wysiwyg/Images/StorageTest.php -index 2dc98bcefb9..7bec1e36014 100644 ---- a/app/code/Magento/Cms/Test/Unit/Model/Wysiwyg/Images/StorageTest.php -+++ b/app/code/Magento/Cms/Test/Unit/Model/Wysiwyg/Images/StorageTest.php -@@ -417,6 +417,10 @@ class StorageTest extends \PHPUnit\Framework\TestCase - ->method('setCollectRecursively') - ->with(false) - ->willReturnSelf(); -+ $storageCollectionMock->expects($this->once()) -+ ->method('setOrder') -+ ->with('basename', \Magento\Framework\Data\Collection\Filesystem::SORT_ORDER_ASC) -+ ->willReturnSelf(); - $storageCollectionMock->expects($this->once()) - ->method('getIterator') - ->willReturn(new \ArrayIterator($collectionArray)); -@@ -440,14 +444,7 @@ class StorageTest extends \PHPUnit\Framework\TestCase - $thumbnailDestination = $thumbnailTargetPath . '/' . $fileName; - $type = 'image'; - $result = [ -- 'result', -- 'cookie' => [ -- 'name' => 'session_name', -- 'value' => '1', -- 'lifetime' => '50', -- 'path' => 'cookie/path', -- 'domain' => 'cookie_domain', -- ], -+ 'result' - ]; - $uploader = $this->getMockBuilder(\Magento\MediaStorage\Model\File\Uploader::class) - ->disableOriginalConstructor() -@@ -507,17 +504,6 @@ class StorageTest extends \PHPUnit\Framework\TestCase - - $this->adapterFactoryMock->expects($this->atLeastOnce())->method('create')->willReturn($image); - -- $this->sessionMock->expects($this->atLeastOnce())->method('getName') -- ->willReturn($result['cookie']['name']); -- $this->sessionMock->expects($this->atLeastOnce())->method('getSessionId') -- ->willReturn($result['cookie']['value']); -- $this->sessionMock->expects($this->atLeastOnce())->method('getCookieLifetime') -- ->willReturn($result['cookie']['lifetime']); -- $this->sessionMock->expects($this->atLeastOnce())->method('getCookiePath') -- ->willReturn($result['cookie']['path']); -- $this->sessionMock->expects($this->atLeastOnce())->method('getCookieDomain') -- ->willReturn($result['cookie']['domain']); -- - $this->assertEquals($result, $this->imagesStorage->uploadFile($targetPath, $type)); - } - } -diff --git a/app/code/Magento/Cms/Test/Unit/Ui/Component/Listing/Column/BlockActionsTest.php b/app/code/Magento/Cms/Test/Unit/Ui/Component/Listing/Column/BlockActionsTest.php -index 3dcf6c4a3fc..3095abef7bb 100644 ---- a/app/code/Magento/Cms/Test/Unit/Ui/Component/Listing/Column/BlockActionsTest.php -+++ b/app/code/Magento/Cms/Test/Unit/Ui/Component/Listing/Column/BlockActionsTest.php -@@ -95,6 +95,7 @@ class BlockActionsTest extends \PHPUnit\Framework\TestCase - 'title' => __('Delete %1', $title), - 'message' => __('Are you sure you want to delete a %1 record?', $title) - ], -+ 'post' => true - ] - ], - ] -diff --git a/app/code/Magento/Cms/Test/Unit/Ui/Component/Listing/Column/PageActionsTest.php b/app/code/Magento/Cms/Test/Unit/Ui/Component/Listing/Column/PageActionsTest.php -index b0cc1bf061a..9b3165a2c55 100644 ---- a/app/code/Magento/Cms/Test/Unit/Ui/Component/Listing/Column/PageActionsTest.php -+++ b/app/code/Magento/Cms/Test/Unit/Ui/Component/Listing/Column/PageActionsTest.php -@@ -70,6 +70,7 @@ class PageActionsTest extends \PHPUnit\Framework\TestCase - 'title' => __('Delete %1', $title), - 'message' => __('Are you sure you want to delete a %1 record?', $title) - ], -+ 'post' => true - ] - ], - ] -diff --git a/app/code/Magento/Cms/Test/Unit/Ui/Component/Listing/DataProviderTest.php b/app/code/Magento/Cms/Test/Unit/Ui/Component/Listing/DataProviderTest.php -index 54e0e17ab7a..a624823d02c 100644 ---- a/app/code/Magento/Cms/Test/Unit/Ui/Component/Listing/DataProviderTest.php -+++ b/app/code/Magento/Cms/Test/Unit/Ui/Component/Listing/DataProviderTest.php -@@ -118,7 +118,8 @@ class DataProviderTest extends \PHPUnit\Framework\TestCase - 'config' => [ - 'editorConfig' => [ - 'enabled' => false -- ] -+ ], -+ 'componentType' => \Magento\Ui\Component\Container::NAME - ] - ] - ] -diff --git a/app/code/Magento/Cms/Ui/Component/DataProvider.php b/app/code/Magento/Cms/Ui/Component/DataProvider.php -index 5fc9c5a8960..b02dd6ba98e 100644 ---- a/app/code/Magento/Cms/Ui/Component/DataProvider.php -+++ b/app/code/Magento/Cms/Ui/Component/DataProvider.php -@@ -13,6 +13,9 @@ use Magento\Framework\App\RequestInterface; - use Magento\Framework\AuthorizationInterface; - use Magento\Framework\View\Element\UiComponent\DataProvider\Reporting; - -+/** -+ * DataProvider for cms ui. -+ */ - class DataProvider extends \Magento\Framework\View\Element\UiComponent\DataProvider\DataProvider - { - /** -@@ -67,6 +70,8 @@ class DataProvider extends \Magento\Framework\View\Element\UiComponent\DataProvi - } - - /** -+ * Get authorization info. -+ * - * @deprecated 101.0.7 - * @return AuthorizationInterface|mixed - */ -@@ -95,7 +100,8 @@ class DataProvider extends \Magento\Framework\View\Element\UiComponent\DataProvi - 'config' => [ - 'editorConfig' => [ - 'enabled' => false -- ] -+ ], -+ 'componentType' => \Magento\Ui\Component\Container::NAME - ] - ] - ] -diff --git a/app/code/Magento/Cms/Ui/Component/Listing/Column/BlockActions.php b/app/code/Magento/Cms/Ui/Component/Listing/Column/BlockActions.php -index 60b9f34d29a..f68ef35e534 100644 ---- a/app/code/Magento/Cms/Ui/Component/Listing/Column/BlockActions.php -+++ b/app/code/Magento/Cms/Ui/Component/Listing/Column/BlockActions.php -@@ -55,10 +55,7 @@ class BlockActions extends Column - } - - /** -- * Prepare Data Source -- * -- * @param array $dataSource -- * @return array -+ * @inheritDoc - */ - public function prepareDataSource(array $dataSource) - { -@@ -87,7 +84,8 @@ class BlockActions extends Column - 'confirm' => [ - 'title' => __('Delete %1', $title), - 'message' => __('Are you sure you want to delete a %1 record?', $title) -- ] -+ ], -+ 'post' => true - ] - ]; - } -@@ -99,6 +97,7 @@ class BlockActions extends Column - - /** - * Get instance of escaper -+ * - * @return Escaper - * @deprecated 101.0.7 - */ -diff --git a/app/code/Magento/Cms/Ui/Component/Listing/Column/PageActions.php b/app/code/Magento/Cms/Ui/Component/Listing/Column/PageActions.php -index ea6882e21c8..9c57aa050b0 100644 ---- a/app/code/Magento/Cms/Ui/Component/Listing/Column/PageActions.php -+++ b/app/code/Magento/Cms/Ui/Component/Listing/Column/PageActions.php -@@ -27,6 +27,11 @@ class PageActions extends Column - */ - protected $actionUrlBuilder; - -+ /** -+ * @var \Magento\Cms\ViewModel\Page\Grid\UrlBuilder -+ */ -+ private $scopeUrlBuilder; -+ - /** - * @var \Magento\Framework\UrlInterface - */ -@@ -50,6 +55,7 @@ class PageActions extends Column - * @param array $components - * @param array $data - * @param string $editUrl -+ * @param \Magento\Cms\ViewModel\Page\Grid\UrlBuilder|null $scopeUrlBuilder - */ - public function __construct( - ContextInterface $context, -@@ -58,19 +64,19 @@ class PageActions extends Column - UrlInterface $urlBuilder, - array $components = [], - array $data = [], -- $editUrl = self::CMS_URL_PATH_EDIT -+ $editUrl = self::CMS_URL_PATH_EDIT, -+ \Magento\Cms\ViewModel\Page\Grid\UrlBuilder $scopeUrlBuilder = null - ) { - $this->urlBuilder = $urlBuilder; - $this->actionUrlBuilder = $actionUrlBuilder; - $this->editUrl = $editUrl; - parent::__construct($context, $uiComponentFactory, $components, $data); -+ $this->scopeUrlBuilder = $scopeUrlBuilder ?: ObjectManager::getInstance() -+ ->get(\Magento\Cms\ViewModel\Page\Grid\UrlBuilder::class); - } - - /** -- * Prepare Data Source -- * -- * @param array $dataSource -- * @return array -+ * @inheritDoc - */ - public function prepareDataSource(array $dataSource) - { -@@ -89,12 +95,13 @@ class PageActions extends Column - 'confirm' => [ - 'title' => __('Delete %1', $title), - 'message' => __('Are you sure you want to delete a %1 record?', $title) -- ] -+ ], -+ 'post' => true - ]; - } - if (isset($item['identifier'])) { - $item[$name]['preview'] = [ -- 'href' => $this->actionUrlBuilder->getUrl( -+ 'href' => $this->scopeUrlBuilder->getUrl( - $item['identifier'], - isset($item['_first_store_id']) ? $item['_first_store_id'] : null, - isset($item['store_code']) ? $item['store_code'] : null -@@ -110,6 +117,7 @@ class PageActions extends Column - - /** - * Get instance of escaper -+ * - * @return Escaper - * @deprecated 101.0.7 - */ -diff --git a/app/code/Magento/Cms/ViewModel/Page/Grid/UrlBuilder.php b/app/code/Magento/Cms/ViewModel/Page/Grid/UrlBuilder.php -new file mode 100644 -index 00000000000..0faf62607f5 ---- /dev/null -+++ b/app/code/Magento/Cms/ViewModel/Page/Grid/UrlBuilder.php -@@ -0,0 +1,112 @@ -+<?php -+/** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+declare(strict_types=1); -+ -+namespace Magento\Cms\ViewModel\Page\Grid; -+ -+use Magento\Framework\Url\EncoderInterface; -+use Magento\Framework\App\ActionInterface; -+use Magento\Store\Model\StoreManagerInterface; -+ -+/** -+ * Url builder class used to compose dynamic urls. -+ */ -+class UrlBuilder -+{ -+ /** -+ * @var \Magento\Framework\UrlInterface -+ */ -+ private $frontendUrlBuilder; -+ -+ /** -+ * @var EncoderInterface -+ */ -+ private $urlEncoder; -+ -+ /** -+ * @var StoreManagerInterface -+ */ -+ private $storeManager; -+ -+ /** -+ * @param \Magento\Framework\UrlInterface $frontendUrlBuilder -+ * @param EncoderInterface $urlEncoder -+ * @param StoreManagerInterface $storeManager -+ */ -+ public function __construct( -+ \Magento\Framework\UrlInterface $frontendUrlBuilder, -+ EncoderInterface $urlEncoder, -+ StoreManagerInterface $storeManager -+ ) { -+ $this->frontendUrlBuilder = $frontendUrlBuilder; -+ $this->urlEncoder = $urlEncoder; -+ $this->storeManager = $storeManager; -+ } -+ -+ /** -+ * Get action url -+ * -+ * @param string $routePath -+ * @param string $scope -+ * @param string $store -+ * @return string -+ */ -+ public function getUrl($routePath, $scope, $store) -+ { -+ if ($scope) { -+ $this->frontendUrlBuilder->setScope($scope); -+ $targetUrl = $this->frontendUrlBuilder->getUrl( -+ $routePath, -+ [ -+ '_current' => false, -+ '_nosid' => true, -+ '_query' => [ -+ StoreManagerInterface::PARAM_NAME => $store -+ ] -+ ] -+ ); -+ $href = $this->frontendUrlBuilder->getUrl( -+ 'stores/store/switch', -+ [ -+ '_current' => false, -+ '_nosid' => true, -+ '_query' => $this->prepareRequestQuery($store, $targetUrl) -+ ] -+ ); -+ } else { -+ $href = $this->frontendUrlBuilder->getUrl( -+ $routePath, -+ [ -+ '_current' => false, -+ '_nosid' => true -+ ] -+ ); -+ } -+ -+ return $href; -+ } -+ -+ /** -+ * Prepare request query -+ * -+ * @param string $store -+ * @param string $href -+ * @return array -+ */ -+ private function prepareRequestQuery(string $store, string $href) : array -+ { -+ $storeView = $this->storeManager->getDefaultStoreView(); -+ $query = [ -+ StoreManagerInterface::PARAM_NAME => $store, -+ ActionInterface::PARAM_NAME_URL_ENCODED => $this->urlEncoder->encode($href) -+ ]; -+ if ($storeView->getCode() !== $store) { -+ $query['___from_store'] = $storeView->getCode(); -+ } -+ -+ return $query; -+ } -+} -diff --git a/app/code/Magento/Cms/etc/adminhtml/di.xml b/app/code/Magento/Cms/etc/adminhtml/di.xml -index 98a8ff6e9ec..363217af4cd 100644 ---- a/app/code/Magento/Cms/etc/adminhtml/di.xml -+++ b/app/code/Magento/Cms/etc/adminhtml/di.xml -@@ -12,6 +12,11 @@ - <argument name="frontendUrlBuilder" xsi:type="object">Magento\Framework\Url</argument> - </arguments> - </type> -+ <type name="Magento\Cms\ViewModel\Page\Grid\UrlBuilder"> -+ <arguments> -+ <argument name="frontendUrlBuilder" xsi:type="object">Magento\Framework\Url</argument> -+ </arguments> -+ </type> - <type name="Magento\Cms\Model\Wysiwyg\CompositeConfigProvider"> - <arguments> - <argument name="variablePluginConfigProvider" xsi:type="array"> -diff --git a/app/code/Magento/Cms/etc/db_schema.xml b/app/code/Magento/Cms/etc/db_schema.xml -index 2b825544f56..1e64c905bad 100644 ---- a/app/code/Magento/Cms/etc/db_schema.xml -+++ b/app/code/Magento/Cms/etc/db_schema.xml -@@ -9,7 +9,7 @@ - xsi:noNamespaceSchemaLocation="urn:magento:framework:Setup/Declaration/Schema/etc/schema.xsd"> - <table name="cms_block" resource="default" engine="innodb" comment="CMS Block Table"> - <column xsi:type="smallint" name="block_id" padding="6" unsigned="false" nullable="false" identity="true" -- comment="Entity Id"/> -+ comment="Entity ID"/> - <column xsi:type="varchar" name="title" nullable="false" length="255" comment="Block Title"/> - <column xsi:type="varchar" name="identifier" nullable="false" length="255" comment="Block String Identifier"/> - <column xsi:type="mediumtext" name="content" nullable="true" comment="Block Content"/> -@@ -19,10 +19,10 @@ - comment="Block Modification Time"/> - <column xsi:type="smallint" name="is_active" padding="6" unsigned="false" nullable="false" identity="false" - default="1" comment="Is Block Active"/> -- <constraint xsi:type="primary" name="PRIMARY"> -+ <constraint xsi:type="primary" referenceId="PRIMARY"> - <column name="block_id"/> - </constraint> -- <index name="CMS_BLOCK_TITLE_IDENTIFIER_CONTENT" indexType="fulltext"> -+ <index referenceId="CMS_BLOCK_TITLE_IDENTIFIER_CONTENT" indexType="fulltext"> - <column name="title"/> - <column name="identifier"/> - <column name="content"/> -@@ -32,21 +32,21 @@ - <column xsi:type="smallint" name="block_id" padding="6" unsigned="false" nullable="false" identity="false"/> - <column xsi:type="smallint" name="store_id" padding="5" unsigned="true" nullable="false" identity="false" - comment="Store ID"/> -- <constraint xsi:type="primary" name="PRIMARY"> -+ <constraint xsi:type="primary" referenceId="PRIMARY"> - <column name="block_id"/> - <column name="store_id"/> - </constraint> -- <constraint xsi:type="foreign" name="CMS_BLOCK_STORE_BLOCK_ID_CMS_BLOCK_BLOCK_ID" table="cms_block_store" -+ <constraint xsi:type="foreign" referenceId="CMS_BLOCK_STORE_BLOCK_ID_CMS_BLOCK_BLOCK_ID" table="cms_block_store" - column="block_id" referenceTable="cms_block" referenceColumn="block_id" onDelete="CASCADE"/> -- <constraint xsi:type="foreign" name="CMS_BLOCK_STORE_STORE_ID_STORE_STORE_ID" table="cms_block_store" -+ <constraint xsi:type="foreign" referenceId="CMS_BLOCK_STORE_STORE_ID_STORE_STORE_ID" table="cms_block_store" - column="store_id" referenceTable="store" referenceColumn="store_id" onDelete="CASCADE"/> -- <index name="CMS_BLOCK_STORE_STORE_ID" indexType="btree"> -+ <index referenceId="CMS_BLOCK_STORE_STORE_ID" indexType="btree"> - <column name="store_id"/> - </index> - </table> - <table name="cms_page" resource="default" engine="innodb" comment="CMS Page Table"> - <column xsi:type="smallint" name="page_id" padding="6" unsigned="false" nullable="false" identity="true" -- comment="Entity Id"/> -+ comment="Entity ID"/> - <column xsi:type="varchar" name="title" nullable="true" length="255" comment="Page Title"/> - <column xsi:type="varchar" name="page_layout" nullable="true" length="255" comment="Page Layout"/> - <column xsi:type="text" name="meta_keywords" nullable="true" comment="Page Meta Keywords"/> -@@ -71,13 +71,13 @@ - <column xsi:type="date" name="custom_theme_from" comment="Page Custom Theme Active From Date"/> - <column xsi:type="date" name="custom_theme_to" comment="Page Custom Theme Active To Date"/> - <column xsi:type="varchar" name="meta_title" nullable="true" length="255" comment="Page Meta Title"/> -- <constraint xsi:type="primary" name="PRIMARY"> -+ <constraint xsi:type="primary" referenceId="PRIMARY"> - <column name="page_id"/> - </constraint> -- <index name="CMS_PAGE_IDENTIFIER" indexType="btree"> -+ <index referenceId="CMS_PAGE_IDENTIFIER" indexType="btree"> - <column name="identifier"/> - </index> -- <index name="CMS_PAGE_TITLE_META_KEYWORDS_META_DESCRIPTION_IDENTIFIER_CONTENT" indexType="fulltext"> -+ <index referenceId="CMS_PAGE_TITLE_META_KEYWORDS_META_DESCRIPTION_IDENTIFIER_CONTENT" indexType="fulltext"> - <column name="title"/> - <column name="meta_keywords"/> - <column name="meta_description"/> -@@ -86,18 +86,19 @@ - </index> - </table> - <table name="cms_page_store" resource="default" engine="innodb" comment="CMS Page To Store Linkage Table"> -- <column xsi:type="smallint" name="page_id" padding="6" unsigned="false" nullable="false" identity="false"/> -+ <column xsi:type="smallint" name="page_id" padding="6" unsigned="false" nullable="false" identity="false" -+ comment="Entity ID"/> - <column xsi:type="smallint" name="store_id" padding="5" unsigned="true" nullable="false" identity="false" - comment="Store ID"/> -- <constraint xsi:type="primary" name="PRIMARY"> -+ <constraint xsi:type="primary" referenceId="PRIMARY"> - <column name="page_id"/> - <column name="store_id"/> - </constraint> -- <constraint xsi:type="foreign" name="CMS_PAGE_STORE_PAGE_ID_CMS_PAGE_PAGE_ID" table="cms_page_store" -+ <constraint xsi:type="foreign" referenceId="CMS_PAGE_STORE_PAGE_ID_CMS_PAGE_PAGE_ID" table="cms_page_store" - column="page_id" referenceTable="cms_page" referenceColumn="page_id" onDelete="CASCADE"/> -- <constraint xsi:type="foreign" name="CMS_PAGE_STORE_STORE_ID_STORE_STORE_ID" table="cms_page_store" -+ <constraint xsi:type="foreign" referenceId="CMS_PAGE_STORE_STORE_ID_STORE_STORE_ID" table="cms_page_store" - column="store_id" referenceTable="store" referenceColumn="store_id" onDelete="CASCADE"/> -- <index name="CMS_PAGE_STORE_STORE_ID" indexType="btree"> -+ <index referenceId="CMS_PAGE_STORE_STORE_ID" indexType="btree"> - <column name="store_id"/> - </index> - </table> -diff --git a/app/code/Magento/Cms/etc/di.xml b/app/code/Magento/Cms/etc/di.xml -index b6e13c63302..025d63115f4 100644 ---- a/app/code/Magento/Cms/etc/di.xml -+++ b/app/code/Magento/Cms/etc/di.xml -@@ -233,5 +233,7 @@ - </argument> - </arguments> - </type> -+ <type name="Magento\Catalog\Model\Product"> -+ <plugin name="cms" type="Magento\Cms\Model\Plugin\Product" sortOrder="100"/> -+ </type> - </config> -- -diff --git a/app/code/Magento/Cms/view/adminhtml/layout/cms_wysiwyg_images_index.xml b/app/code/Magento/Cms/view/adminhtml/layout/cms_wysiwyg_images_index.xml -index 1bc8828ef6c..6703b6c2771 100644 ---- a/app/code/Magento/Cms/view/adminhtml/layout/cms_wysiwyg_images_index.xml -+++ b/app/code/Magento/Cms/view/adminhtml/layout/cms_wysiwyg_images_index.xml -@@ -9,7 +9,11 @@ - <container name="root"> - <block class="Magento\Cms\Block\Adminhtml\Wysiwyg\Images\Content" name="wysiwyg_images.content" template="Magento_Cms::browser/content.phtml"> - <block class="Magento\Cms\Block\Adminhtml\Wysiwyg\Images\Tree" name="wysiwyg_images.tree" template="Magento_Cms::browser/tree.phtml"/> -- <block class="Magento\Cms\Block\Adminhtml\Wysiwyg\Images\Content\Uploader" name="wysiwyg_images.uploader" template="Magento_Cms::browser/content/uploader.phtml"/> -+ <block class="Magento\Cms\Block\Adminhtml\Wysiwyg\Images\Content\Uploader" name="wysiwyg_images.uploader" template="Magento_Cms::browser/content/uploader.phtml"> -+ <arguments> -+ <argument name="image_upload_config_data" xsi:type="object">Magento\Backend\Block\DataProviders\ImageUploadConfig</argument> -+ </arguments> -+ </block> - </block> - </container> - </layout> -diff --git a/app/code/Magento/Cms/view/adminhtml/templates/browser/content.phtml b/app/code/Magento/Cms/view/adminhtml/templates/browser/content.phtml -index ed376781dfa..5bba11cad79 100644 ---- a/app/code/Magento/Cms/view/adminhtml/templates/browser/content.phtml -+++ b/app/code/Magento/Cms/view/adminhtml/templates/browser/content.phtml -@@ -4,8 +4,6 @@ - * See COPYING.txt for license details. - */ - --// @codingStandardsIgnoreFile -- - /** @var $block \Magento\Cms\Block\Adminhtml\Wysiwyg\Images\Content */ - ?> - -diff --git a/app/code/Magento/Cms/view/adminhtml/templates/browser/content/files.phtml b/app/code/Magento/Cms/view/adminhtml/templates/browser/content/files.phtml -index da899918699..ff3d5f24dd5 100644 ---- a/app/code/Magento/Cms/view/adminhtml/templates/browser/content/files.phtml -+++ b/app/code/Magento/Cms/view/adminhtml/templates/browser/content/files.phtml -@@ -4,16 +4,14 @@ - * See COPYING.txt for license details. - */ - --// @codingStandardsIgnoreFile -- -- /** @var $block \Magento\Cms\Block\Adminhtml\Wysiwyg\Images\Content\Files */ -+/** @var $block \Magento\Cms\Block\Adminhtml\Wysiwyg\Images\Content\Files */ - - $_width = $block->getImagesWidth(); - $_height = $block->getImagesHeight(); - - ?> --<?php if ($block->getFilesCount() > 0): ?> -- <?php foreach ($block->getFiles() as $file): ?> -+<?php if ($block->getFilesCount() > 0) : ?> -+ <?php foreach ($block->getFiles() as $file) : ?> - <div - data-row="file" - class="filecnt" -@@ -21,17 +19,17 @@ $_height = $block->getImagesHeight(); - data-size="<?= $block->escapeHtmlAttr($file->getSize()) ?>" - data-mime-type="<?= $block->escapeHtmlAttr($file->getMimeType()) ?>" - > -- <p class="nm" style="height:<?= $block->escapeHtmlAttr($_height) ?>px;width:<?= $block->escapeHtmlAttr($_width) ?>px;"> -- <?php if ($block->getFileThumbUrl($file)):?> -+ <p class="nm" style="height:<?= $block->escapeHtmlAttr($_height) ?>px;"> -+ <?php if ($block->getFileThumbUrl($file)) : ?> - <img src="<?= $block->escapeHtmlAttr($block->getFileThumbUrl($file)) ?>" alt="<?= $block->escapeHtmlAttr($block->getFileName($file)) ?>"/> - <?php endif; ?> - </p> -- <?php if ($block->getFileWidth($file)): ?> -+ <?php if ($block->getFileWidth($file)) : ?> - <small><?= $block->escapeHtml($block->getFileWidth($file)) ?>x<?= $block->escapeHtml($block->getFileHeight($file)) ?> <?= $block->escapeHtml(__('px.')) ?></small><br/> - <?php endif; ?> - <small><?= $block->escapeHtml($block->getFileShortName($file)) ?></small> - </div> - <?php endforeach; ?> --<?php else: ?> -+<?php else : ?> - <div class="empty"><?= $block->escapeHtml(__('No files found')) ?></div> - <?php endif; ?> -diff --git a/app/code/Magento/Cms/view/adminhtml/templates/browser/content/uploader.phtml b/app/code/Magento/Cms/view/adminhtml/templates/browser/content/uploader.phtml -index 097235bc9fb..099efa0abdb 100644 ---- a/app/code/Magento/Cms/view/adminhtml/templates/browser/content/uploader.phtml -+++ b/app/code/Magento/Cms/view/adminhtml/templates/browser/content/uploader.phtml -@@ -4,12 +4,11 @@ - * See COPYING.txt for license details. - */ - --// @codingStandardsIgnoreFile -- - /** @var $block \Magento\Cms\Block\Adminhtml\Wysiwyg\Images\Content\Uploader */ - - $filters = $block->getConfig()->getFilters() ?? []; - $allowedExtensions = []; -+$blockHtmlId = $block->getHtmlId(); - - foreach ($filters as $media_type) { - $allowedExtensions = array_merge($allowedExtensions, array_map(function ($fileExt) { -@@ -17,15 +16,22 @@ foreach ($filters as $media_type) { - }, $media_type['files'])); - } - -+$resizeConfig = $block->getImageUploadConfigData()->getIsResizeEnabled() -+ ? "{action: 'resize', maxWidth: " -+ . $block->escapeHtml($block->getImageUploadMaxWidth()) -+ . ", maxHeight: " -+ . $block->escapeHtml($block->getImageUploadMaxHeight()) -+ . "}" -+ : "{action: 'resize'}"; - ?> - --<div id="<?= $block->getHtmlId() ?>" class="uploader"> -+<div id="<?= /* @noEscape */ $blockHtmlId ?>" class="uploader"> - <span class="fileinput-button form-buttons"> - <span><?= $block->escapeHtml(__('Upload Images')) ?></span> - <input class="fileupload" type="file" name="<?= $block->escapeHtmlAttr($block->getConfig()->getFileField()) ?>" data-url="<?= $block->escapeUrl($block->getConfig()->getUrl()) ?>" multiple> - </span> - <div class="clear"></div> -- <script type="text/x-magento-template" id="<?= $block->getHtmlId() ?>-template"> -+ <script type="text/x-magento-template" id="<?= /* @noEscape */ $blockHtmlId ?>-template"> - <div id="<%- data.id %>" class="file-row"> - <span class="file-info"><%- data.name %> (<%- data.size %>)</span> - <div class="progressbar-container"> -@@ -47,7 +53,7 @@ require([ - var maxFileSize = <?= $block->escapeJs($block->getFileSizeService()->getMaxFileSize()) ?>, - allowedExtensions = '<?= $block->escapeHtml(implode(' ', $allowedExtensions)) ?>'; - -- $('#<?= $block->getHtmlId() ?> .fileupload').fileupload({ -+ $('#<?= /* @noEscape */ $blockHtmlId ?> .fileupload').fileupload({ - dataType: 'json', - formData: { - isAjax: 'true', -@@ -57,9 +63,9 @@ require([ - acceptFileTypes: /(\.|\/)(gif|jpe?g|png)$/i, - allowedExtensions: allowedExtensions, - maxFileSize: maxFileSize, -- dropZone: $('#<?= $block->getHtmlId() ?>').closest('[role="dialog"]'), -+ dropZone: $('#<?= /* @noEscape */ $blockHtmlId ?>').closest('[role="dialog"]'), - add: function (e, data) { -- var progressTmpl = mageTemplate('#<?= $block->getHtmlId() ?>-template'), -+ var progressTmpl = mageTemplate('#<?= /* @noEscape */ $blockHtmlId ?>-template'), - fileSize, - tmpl, - validationResult; -@@ -103,7 +109,7 @@ require([ - } - }); - -- $(tmpl).data('image', data).appendTo('#<?= $block->getHtmlId() ?>'); -+ $(tmpl).data('image', data).appendTo('#<?= /* @noEscape */ $blockHtmlId ?>'); - - return true; - }); -@@ -140,16 +146,14 @@ require([ - } - }); - -- $('#<?= $block->getHtmlId() ?> .fileupload').fileupload('option', { -+ $('#<?= /* @noEscape */ $blockHtmlId ?> .fileupload').fileupload('option', { - process: [{ - action: 'load', - fileTypes: /^image\/(gif|jpeg|png)$/, - maxFileSize: <?= (int) $block->getFileSizeService()->getMaxFileSize() ?> * 10 -- }, { -- action: 'resize', -- maxWidth: <?= (float) \Magento\Framework\File\Uploader::MAX_IMAGE_WIDTH ?> , -- maxHeight: <?= (float) \Magento\Framework\File\Uploader::MAX_IMAGE_HEIGHT ?> -- }, { -+ }, -+ <?= /* @noEscape */ $resizeConfig ?>, -+ { - action: 'save' - }] - }); -diff --git a/app/code/Magento/Cms/view/adminhtml/templates/browser/tree.phtml b/app/code/Magento/Cms/view/adminhtml/templates/browser/tree.phtml -index 4f0ba000c1d..9603bb4f1a4 100644 ---- a/app/code/Magento/Cms/view/adminhtml/templates/browser/tree.phtml -+++ b/app/code/Magento/Cms/view/adminhtml/templates/browser/tree.phtml -@@ -4,8 +4,6 @@ - * See COPYING.txt for license details. - */ - --// @codingStandardsIgnoreFile -- - /** @var \Magento\Cms\Block\Adminhtml\Wysiwyg\Images\Tree $block */ - ?> - <div class="tree-panel" > -@@ -16,6 +14,6 @@ - <a onclick="jQuery('[data-role=tree]').jstree('open_all');"><?= $block->escapeHtml(__('Expand All')) ?></a> - </div> - </div> -- <div data-role="tree" data-mage-init='<?= $block->escapeHtml($this->helper('Magento\Framework\Json\Helper\Data')->jsonEncode($block->getTreeWidgetOptions())) ?>'> -+ <div data-role="tree" data-mage-init='<?= $block->escapeHtml($this->helper(\Magento\Framework\Json\Helper\Data::class)->jsonEncode($block->getTreeWidgetOptions())) ?>'> - </div> - </div> -diff --git a/app/code/Magento/Cms/view/adminhtml/templates/page/edit/form/renderer/content.phtml b/app/code/Magento/Cms/view/adminhtml/templates/page/edit/form/renderer/content.phtml -index c773750e5d6..54fe4c71fb9 100644 ---- a/app/code/Magento/Cms/view/adminhtml/templates/page/edit/form/renderer/content.phtml -+++ b/app/code/Magento/Cms/view/adminhtml/templates/page/edit/form/renderer/content.phtml -@@ -3,14 +3,11 @@ - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -- --// @codingStandardsIgnoreFile -- - ?> - <?php $_element = $block->getElement() ?> - --<?php if (!$_element->getNoDisplay()): ?> -+<?php if (!$_element->getNoDisplay()) : ?> - <div class="cms-manage-content-actions"> -- <?= trim($_element->getElementHtml()) ?> -+ <?= /* @noEscape */ trim($_element->getElementHtml()) ?> - </div> - <?php endif; ?> -diff --git a/app/code/Magento/Cms/view/adminhtml/ui_component/cms_block_form.xml b/app/code/Magento/Cms/view/adminhtml/ui_component/cms_block_form.xml -index 4b4a1a9bfe4..9218676a7f8 100644 ---- a/app/code/Magento/Cms/view/adminhtml/ui_component/cms_block_form.xml -+++ b/app/code/Magento/Cms/view/adminhtml/ui_component/cms_block_form.xml -@@ -123,6 +123,10 @@ - <rule name="required-entry" xsi:type="boolean">true</rule> - </validation> - <dataType>int</dataType> -+ <tooltip> -+ <link>https://docs.magento.com/m2/ce/user_guide/configuration/scope.html</link> -+ <description>What is this?</description> -+ </tooltip> - <label translate="true">Store View</label> - <dataScope>store_id</dataScope> - </settings> -diff --git a/app/code/Magento/Cms/view/adminhtml/ui_component/cms_block_listing.xml b/app/code/Magento/Cms/view/adminhtml/ui_component/cms_block_listing.xml -index 9f886f6f134..793fc7d26cb 100644 ---- a/app/code/Magento/Cms/view/adminhtml/ui_component/cms_block_listing.xml -+++ b/app/code/Magento/Cms/view/adminhtml/ui_component/cms_block_listing.xml -@@ -146,7 +146,6 @@ - <editor> - <validation> - <rule name="required-entry" xsi:type="boolean">true</rule> -- <rule name="validate-xml-identifier" xsi:type="boolean">true</rule> - </validation> - <editorType>text</editorType> - </editor> -diff --git a/app/code/Magento/Cms/view/adminhtml/ui_component/cms_page_form.xml b/app/code/Magento/Cms/view/adminhtml/ui_component/cms_page_form.xml -index 9781675a6d9..dd58d17cbf5 100644 ---- a/app/code/Magento/Cms/view/adminhtml/ui_component/cms_page_form.xml -+++ b/app/code/Magento/Cms/view/adminhtml/ui_component/cms_page_form.xml -@@ -207,6 +207,10 @@ - <rule name="required-entry" xsi:type="boolean">true</rule> - </validation> - <dataType>int</dataType> -+ <tooltip> -+ <link>https://docs.magento.com/m2/ce/user_guide/configuration/scope.html</link> -+ <description>What is this?</description> -+ </tooltip> - <label translate="true">Store View</label> - <dataScope>store_id</dataScope> - </settings> -@@ -285,6 +289,7 @@ - <settings> - <validation> - <rule name="validate-date" xsi:type="boolean">true</rule> -+ <rule name="validate-date-range" xsi:type="string">custom_theme_from</rule> - </validation> - <dataType>text</dataType> - <label translate="true">To</label> -diff --git a/app/code/Magento/Cms/view/frontend/templates/content.phtml b/app/code/Magento/Cms/view/frontend/templates/content.phtml -index d3baf144447..f83edeb9dee 100644 ---- a/app/code/Magento/Cms/view/frontend/templates/content.phtml -+++ b/app/code/Magento/Cms/view/frontend/templates/content.phtml -@@ -3,8 +3,5 @@ - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -- --// @codingStandardsIgnoreFile -- - ?> - <?= /* @noEscape */ $pageData->getPageContent() ?> -diff --git a/app/code/Magento/Cms/view/frontend/templates/meta.phtml b/app/code/Magento/Cms/view/frontend/templates/meta.phtml -index 3c03fa7d9ae..40820ba37e7 100644 ---- a/app/code/Magento/Cms/view/frontend/templates/meta.phtml -+++ b/app/code/Magento/Cms/view/frontend/templates/meta.phtml -@@ -3,13 +3,10 @@ - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -- --// @codingStandardsIgnoreFile -- - ?> --<?php if ($pageData->getPageMetaKeywords()): ?> -+<?php if ($pageData->getPageMetaKeywords()) : ?> - <meta name="keywords" content="<?= /* @noEscape */ $pageData->getPageMetaKeywords() ?>"/> - <?php endif; ?> --<?php if ($pageData->getPageMetaDescription()): ?> -+<?php if ($pageData->getPageMetaDescription()) : ?> - <meta name="description" content="<?= /* @noEscape */ $pageData->getPageMetaDescription() ?>"/> - <?php endif; ?> -diff --git a/app/code/Magento/CmsGraphQl/Model/Resolver/Block/Identity.php b/app/code/Magento/CmsGraphQl/Model/Resolver/Block/Identity.php -new file mode 100644 -index 00000000000..a40d23968c3 ---- /dev/null -+++ b/app/code/Magento/CmsGraphQl/Model/Resolver/Block/Identity.php -@@ -0,0 +1,37 @@ -+<?php -+/** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+declare(strict_types=1); -+ -+namespace Magento\CmsGraphQl\Model\Resolver\Block; -+ -+use Magento\Cms\Api\Data\BlockInterface; -+use Magento\Framework\GraphQl\Query\Resolver\IdentityInterface; -+ -+/** -+ * Identity for resolved CMS block -+ */ -+class Identity implements IdentityInterface -+{ -+ /** -+ * Get block identities from resolved data -+ * -+ * @param array $resolvedData -+ * @return string[] -+ */ -+ public function getIdentities(array $resolvedData): array -+ { -+ $ids = []; -+ $items = $resolvedData['items'] ?? []; -+ foreach ($items as $item) { -+ if (is_array($item) && !empty($item[BlockInterface::BLOCK_ID])) { -+ $ids[] = $item[BlockInterface::BLOCK_ID]; -+ $ids[] = $item[BlockInterface::IDENTIFIER]; -+ } -+ } -+ -+ return $ids; -+ } -+} -diff --git a/app/code/Magento/CmsGraphQl/Model/Resolver/Blocks.php b/app/code/Magento/CmsGraphQl/Model/Resolver/Blocks.php -new file mode 100644 -index 00000000000..e55db2a3fa4 ---- /dev/null -+++ b/app/code/Magento/CmsGraphQl/Model/Resolver/Blocks.php -@@ -0,0 +1,92 @@ -+<?php -+/** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+declare(strict_types=1); -+ -+namespace Magento\CmsGraphQl\Model\Resolver; -+ -+use Magento\CmsGraphQl\Model\Resolver\DataProvider\Block as BlockDataProvider; -+use Magento\Framework\Exception\NoSuchEntityException; -+use Magento\Framework\GraphQl\Config\Element\Field; -+use Magento\Framework\GraphQl\Exception\GraphQlInputException; -+use Magento\Framework\GraphQl\Exception\GraphQlNoSuchEntityException; -+use Magento\Framework\GraphQl\Query\ResolverInterface; -+use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; -+ -+/** -+ * CMS blocks field resolver, used for GraphQL request processing -+ */ -+class Blocks implements ResolverInterface -+{ -+ /** -+ * @var BlockDataProvider -+ */ -+ private $blockDataProvider; -+ -+ /** -+ * @param BlockDataProvider $blockDataProvider -+ */ -+ public function __construct( -+ BlockDataProvider $blockDataProvider -+ ) { -+ $this->blockDataProvider = $blockDataProvider; -+ } -+ -+ /** -+ * @inheritdoc -+ */ -+ public function resolve( -+ Field $field, -+ $context, -+ ResolveInfo $info, -+ array $value = null, -+ array $args = null -+ ) { -+ -+ $blockIdentifiers = $this->getBlockIdentifiers($args); -+ $blocksData = $this->getBlocksData($blockIdentifiers); -+ -+ $resultData = [ -+ 'items' => $blocksData, -+ ]; -+ return $resultData; -+ } -+ -+ /** -+ * Get block identifiers -+ * -+ * @param array $args -+ * @return string[] -+ * @throws GraphQlInputException -+ */ -+ private function getBlockIdentifiers(array $args): array -+ { -+ if (!isset($args['identifiers']) || !is_array($args['identifiers']) || count($args['identifiers']) === 0) { -+ throw new GraphQlInputException(__('"identifiers" of CMS blocks should be specified')); -+ } -+ -+ return $args['identifiers']; -+ } -+ -+ /** -+ * Get blocks data -+ * -+ * @param array $blockIdentifiers -+ * @return array -+ * @throws GraphQlNoSuchEntityException -+ */ -+ private function getBlocksData(array $blockIdentifiers): array -+ { -+ $blocksData = []; -+ foreach ($blockIdentifiers as $blockIdentifier) { -+ try { -+ $blocksData[$blockIdentifier] = $this->blockDataProvider->getData($blockIdentifier); -+ } catch (NoSuchEntityException $e) { -+ $blocksData[$blockIdentifier] = new GraphQlNoSuchEntityException(__($e->getMessage()), $e); -+ } -+ } -+ return $blocksData; -+ } -+} -diff --git a/app/code/Magento/CmsGraphQl/Model/Resolver/DataProvider/Block.php b/app/code/Magento/CmsGraphQl/Model/Resolver/DataProvider/Block.php -new file mode 100644 -index 00000000000..fa4944381b8 ---- /dev/null -+++ b/app/code/Magento/CmsGraphQl/Model/Resolver/DataProvider/Block.php -@@ -0,0 +1,69 @@ -+<?php -+/** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+declare(strict_types=1); -+ -+namespace Magento\CmsGraphQl\Model\Resolver\DataProvider; -+ -+use Magento\Cms\Api\BlockRepositoryInterface; -+use Magento\Cms\Api\Data\BlockInterface; -+use Magento\Framework\Exception\NoSuchEntityException; -+use Magento\Widget\Model\Template\FilterEmulate; -+ -+/** -+ * Cms block data provider -+ */ -+class Block -+{ -+ /** -+ * @var BlockRepositoryInterface -+ */ -+ private $blockRepository; -+ -+ /** -+ * @var FilterEmulate -+ */ -+ private $widgetFilter; -+ -+ /** -+ * @param BlockRepositoryInterface $blockRepository -+ * @param FilterEmulate $widgetFilter -+ */ -+ public function __construct( -+ BlockRepositoryInterface $blockRepository, -+ FilterEmulate $widgetFilter -+ ) { -+ $this->blockRepository = $blockRepository; -+ $this->widgetFilter = $widgetFilter; -+ } -+ -+ /** -+ * Get block data -+ * -+ * @param string $blockIdentifier -+ * @return array -+ * @throws NoSuchEntityException -+ */ -+ public function getData(string $blockIdentifier): array -+ { -+ $block = $this->blockRepository->getById($blockIdentifier); -+ -+ if (false === $block->isActive()) { -+ throw new NoSuchEntityException( -+ __('The CMS block with the "%1" ID doesn\'t exist.', $blockIdentifier) -+ ); -+ } -+ -+ $renderedContent = $this->widgetFilter->filter($block->getContent()); -+ -+ $blockData = [ -+ BlockInterface::BLOCK_ID => $block->getId(), -+ BlockInterface::IDENTIFIER => $block->getIdentifier(), -+ BlockInterface::TITLE => $block->getTitle(), -+ BlockInterface::CONTENT => $renderedContent, -+ ]; -+ return $blockData; -+ } -+} -diff --git a/app/code/Magento/CmsGraphQl/Model/Resolver/DataProvider/Page.php b/app/code/Magento/CmsGraphQl/Model/Resolver/DataProvider/Page.php -new file mode 100644 -index 00000000000..40825e70a99 ---- /dev/null -+++ b/app/code/Magento/CmsGraphQl/Model/Resolver/DataProvider/Page.php -@@ -0,0 +1,119 @@ -+<?php -+/** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+declare(strict_types=1); -+ -+namespace Magento\CmsGraphQl\Model\Resolver\DataProvider; -+ -+use Magento\Cms\Api\Data\PageInterface; -+use Magento\Cms\Api\GetPageByIdentifierInterface; -+use Magento\Cms\Api\PageRepositoryInterface; -+use Magento\Framework\Exception\NoSuchEntityException; -+use Magento\Store\Model\StoreManagerInterface; -+use Magento\Widget\Model\Template\FilterEmulate; -+ -+/** -+ * Cms page data provider -+ */ -+class Page -+{ -+ /** -+ * @var GetPageByIdentifierInterface -+ */ -+ private $pageByIdentifier; -+ -+ /** -+ * @var PageRepositoryInterface -+ */ -+ private $pageRepository; -+ -+ /** -+ * @var StoreManagerInterface -+ */ -+ private $storeManager; -+ -+ /** -+ * @var FilterEmulate -+ */ -+ private $widgetFilter; -+ -+ /** -+ * @param PageRepositoryInterface $pageRepository -+ * @param FilterEmulate $widgetFilter -+ * @param GetPageByIdentifierInterface $getPageByIdentifier -+ * @param StoreManagerInterface $storeManager -+ */ -+ public function __construct( -+ PageRepositoryInterface $pageRepository, -+ FilterEmulate $widgetFilter, -+ GetPageByIdentifierInterface $getPageByIdentifier, -+ StoreManagerInterface $storeManager -+ ) { -+ -+ $this->pageRepository = $pageRepository; -+ $this->widgetFilter = $widgetFilter; -+ $this->pageByIdentifier = $getPageByIdentifier; -+ $this->storeManager = $storeManager; -+ } -+ -+ /** -+ * Returns page data by page_id -+ * -+ * @param int $pageId -+ * @return array -+ * @throws NoSuchEntityException -+ */ -+ public function getDataByPageId(int $pageId): array -+ { -+ $page = $this->pageRepository->getById($pageId); -+ -+ return $this->convertPageData($page); -+ } -+ -+ /** -+ * Returns page data by page identifier -+ * -+ * @param string $pageIdentifier -+ * @return array -+ * @throws NoSuchEntityException -+ */ -+ public function getDataByPageIdentifier(string $pageIdentifier): array -+ { -+ $storeId = (int)$this->storeManager->getStore()->getId(); -+ $page = $this->pageByIdentifier->execute($pageIdentifier, $storeId); -+ -+ return $this->convertPageData($page); -+ } -+ -+ /** -+ * Convert page data -+ * -+ * @param PageInterface $page -+ * @return array -+ * @throws NoSuchEntityException -+ */ -+ private function convertPageData(PageInterface $page) -+ { -+ if (false === $page->isActive()) { -+ throw new NoSuchEntityException(); -+ } -+ -+ $renderedContent = $this->widgetFilter->filter($page->getContent()); -+ -+ $pageData = [ -+ 'url_key' => $page->getIdentifier(), -+ PageInterface::TITLE => $page->getTitle(), -+ PageInterface::CONTENT => $renderedContent, -+ PageInterface::CONTENT_HEADING => $page->getContentHeading(), -+ PageInterface::PAGE_LAYOUT => $page->getPageLayout(), -+ PageInterface::META_TITLE => $page->getMetaTitle(), -+ PageInterface::META_DESCRIPTION => $page->getMetaDescription(), -+ PageInterface::META_KEYWORDS => $page->getMetaKeywords(), -+ PageInterface::PAGE_ID => $page->getId(), -+ PageInterface::IDENTIFIER => $page->getIdentifier(), -+ ]; -+ return $pageData; -+ } -+} -diff --git a/app/code/Magento/CmsGraphQl/Model/Resolver/Page.php b/app/code/Magento/CmsGraphQl/Model/Resolver/Page.php -new file mode 100644 -index 00000000000..64891cfeaa8 ---- /dev/null -+++ b/app/code/Magento/CmsGraphQl/Model/Resolver/Page.php -@@ -0,0 +1,65 @@ -+<?php -+/** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+declare(strict_types=1); -+ -+namespace Magento\CmsGraphQl\Model\Resolver; -+ -+use Magento\CmsGraphQl\Model\Resolver\DataProvider\Page as PageDataProvider; -+use Magento\Framework\Exception\NoSuchEntityException; -+use Magento\Framework\GraphQl\Config\Element\Field; -+use Magento\Framework\GraphQl\Exception\GraphQlInputException; -+use Magento\Framework\GraphQl\Exception\GraphQlNoSuchEntityException; -+use Magento\Framework\GraphQl\Query\ResolverInterface; -+use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; -+ -+/** -+ * CMS page field resolver, used for GraphQL request processing -+ */ -+class Page implements ResolverInterface -+{ -+ /** -+ * @var PageDataProvider -+ */ -+ private $pageDataProvider; -+ -+ /** -+ * -+ * @param PageDataProvider $pageDataProvider -+ */ -+ public function __construct( -+ PageDataProvider $pageDataProvider -+ ) { -+ $this->pageDataProvider = $pageDataProvider; -+ } -+ -+ /** -+ * @inheritdoc -+ */ -+ public function resolve( -+ Field $field, -+ $context, -+ ResolveInfo $info, -+ array $value = null, -+ array $args = null -+ ) { -+ if (!isset($args['id']) && !isset($args['identifier'])) { -+ throw new GraphQlInputException(__('"Page id/identifier should be specified')); -+ } -+ -+ $pageData = []; -+ -+ try { -+ if (isset($args['id'])) { -+ $pageData = $this->pageDataProvider->getDataByPageId((int)$args['id']); -+ } elseif (isset($args['identifier'])) { -+ $pageData = $this->pageDataProvider->getDataByPageIdentifier((string)$args['identifier']); -+ } -+ } catch (NoSuchEntityException $e) { -+ throw new GraphQlNoSuchEntityException(__($e->getMessage()), $e); -+ } -+ return $pageData; -+ } -+} -diff --git a/app/code/Magento/CmsGraphQl/Model/Resolver/Page/Identity.php b/app/code/Magento/CmsGraphQl/Model/Resolver/Page/Identity.php -new file mode 100644 -index 00000000000..abc306451e3 ---- /dev/null -+++ b/app/code/Magento/CmsGraphQl/Model/Resolver/Page/Identity.php -@@ -0,0 +1,28 @@ -+<?php -+/** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+declare(strict_types=1); -+ -+namespace Magento\CmsGraphQl\Model\Resolver\Page; -+ -+use Magento\Cms\Api\Data\PageInterface; -+use Magento\Framework\GraphQl\Query\Resolver\IdentityInterface; -+ -+/** -+ * Identity for resolved CMS page -+ */ -+class Identity implements IdentityInterface -+{ -+ /** -+ * Get page ID from resolved data -+ * -+ * @param array $resolvedData -+ * @return string[] -+ */ -+ public function getIdentities(array $resolvedData): array -+ { -+ return empty($resolvedData[PageInterface::PAGE_ID]) ? [] : [$resolvedData[PageInterface::PAGE_ID]]; -+ } -+} -diff --git a/app/code/Magento/CmsGraphQl/README.md b/app/code/Magento/CmsGraphQl/README.md -new file mode 100644 -index 00000000000..970444837f1 ---- /dev/null -+++ b/app/code/Magento/CmsGraphQl/README.md -@@ -0,0 +1,4 @@ -+# CmsGraphQl -+ -+**CmsGraphQl** provides type information for the GraphQl module -+to generate CMS fields for cms information endpoints. -diff --git a/app/code/Magento/CmsGraphQl/composer.json b/app/code/Magento/CmsGraphQl/composer.json -new file mode 100644 -index 00000000000..e0e8481d59b ---- /dev/null -+++ b/app/code/Magento/CmsGraphQl/composer.json -@@ -0,0 +1,29 @@ -+{ -+ "name": "magento/module-cms-graph-ql", -+ "description": "N/A", -+ "type": "magento2-module", -+ "require": { -+ "php": "~7.1.3||~7.2.0", -+ "magento/framework": "*", -+ "magento/module-cms": "*", -+ "magento/module-store": "*", -+ "magento/module-widget": "*" -+ }, -+ "suggest": { -+ "magento/module-graph-ql": "*", -+ "magento/module-graph-ql-cache": "*", -+ "magento/module-store-graph-ql": "*" -+ }, -+ "license": [ -+ "OSL-3.0", -+ "AFL-3.0" -+ ], -+ "autoload": { -+ "files": [ -+ "registration.php" -+ ], -+ "psr-4": { -+ "Magento\\CmsGraphQl\\": "" -+ } -+ } -+} -diff --git a/app/code/Magento/CmsGraphQl/etc/graphql/di.xml b/app/code/Magento/CmsGraphQl/etc/graphql/di.xml -new file mode 100644 -index 00000000000..78c1071d8e0 ---- /dev/null -+++ b/app/code/Magento/CmsGraphQl/etc/graphql/di.xml -@@ -0,0 +1,21 @@ -+<?xml version="1.0"?> -+<!-- -+/** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd"> -+ <type name="Magento\StoreGraphQl\Model\Resolver\Store\StoreConfigDataProvider"> -+ <arguments> -+ <argument name="extendedConfigData" xsi:type="array"> -+ <item name="front" xsi:type="string">web/default/front</item> -+ <item name="cms_home_page" xsi:type="string">web/default/cms_home_page</item> -+ <item name="no_route" xsi:type="string">web/default/no_route</item> -+ <item name="cms_no_route" xsi:type="string">web/default/cms_no_route</item> -+ <item name="cms_no_cookies" xsi:type="string">web/default/cms_no_cookies</item> -+ <item name="show_cms_breadcrumbs" xsi:type="string">web/default/show_cms_breadcrumbs</item> -+ </argument> -+ </arguments> -+ </type> -+</config> -diff --git a/app/code/Magento/CmsGraphQl/etc/module.xml b/app/code/Magento/CmsGraphQl/etc/module.xml -new file mode 100644 -index 00000000000..4fca42430d1 ---- /dev/null -+++ b/app/code/Magento/CmsGraphQl/etc/module.xml -@@ -0,0 +1,14 @@ -+<?xml version="1.0"?> -+<!-- -+/** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:Module/etc/module.xsd"> -+ <module name="Magento_CmsGraphQl"> -+ <sequence> -+ <module name="Magento_GraphQl"/> -+ </sequence> -+ </module> -+</config> -diff --git a/app/code/Magento/CmsGraphQl/etc/schema.graphqls b/app/code/Magento/CmsGraphQl/etc/schema.graphqls -new file mode 100644 -index 00000000000..3558d853aa4 ---- /dev/null -+++ b/app/code/Magento/CmsGraphQl/etc/schema.graphqls -@@ -0,0 +1,43 @@ -+# Copyright © Magento, Inc. All rights reserved. -+# See COPYING.txt for license details. -+type StoreConfig @doc(description: "The type contains information about a store config") { -+ front : String @doc(description: "Default Web URL") -+ cms_home_page : String @doc(description: "CMS Home Page") -+ no_route : String @doc(description: "Default No-route URL") -+ cms_no_route : String @doc(description: "CMS No Route Page") -+ cms_no_cookies : String @doc(description: "CMS No Cookies Page") -+ show_cms_breadcrumbs : Int @doc(description: "Show Breadcrumbs for CMS Pages") -+} -+ -+ -+type Query { -+ cmsPage ( -+ id: Int @doc(description: "Id of the CMS page") @deprecated(reason: "The `id` is deprecated. Use `identifier` instead.") @doc(description: "The CMS page query returns information about a CMS page") -+ identifier: String @doc(description: "Identifier of the CMS page") -+ ): CmsPage @resolver(class: "Magento\\CmsGraphQl\\Model\\Resolver\\Page") @doc(description: "The CMS page query returns information about a CMS page") @cache(cacheTag: "cms_p", cacheIdentity: "Magento\\CmsGraphQl\\Model\\Resolver\\Page\\Identity") -+ cmsBlocks ( -+ identifiers: [String] @doc(description: "Identifiers of the CMS blocks") -+ ): CmsBlocks @resolver(class: "Magento\\CmsGraphQl\\Model\\Resolver\\Blocks") @doc(description: "The CMS block query returns information about CMS blocks") @cache(cacheTag: "cms_b", cacheIdentity: "Magento\\CmsGraphQl\\Model\\Resolver\\Block\\Identity") -+} -+ -+type CmsPage @doc(description: "CMS page defines all CMS page information") { -+ identifier: String @doc(description: "Identifier of the CMS page") -+ url_key: String @doc(description: "URL key of CMS page") -+ title: String @doc(description: "CMS page title") -+ content: String @doc(description: "CMS page content") -+ content_heading: String @doc(description: "CMS page content heading") -+ page_layout: String @doc(description: "CMS page content heading") -+ meta_title: String @doc(description: "CMS page meta title") -+ meta_description: String @doc(description: "CMS page meta description") -+ meta_keywords: String @doc(description: "CMS page meta keywords") -+} -+ -+type CmsBlocks @doc(description: "CMS blocks information") { -+ items: [CmsBlock] @doc(description: "An array of CMS blocks") -+} -+ -+type CmsBlock @doc(description: "CMS block defines all CMS block information") { -+ identifier: String @doc(description: "CMS block identifier") -+ title: String @doc(description: "CMS block title") -+ content: String @doc(description: "CMS block content") -+} -\ No newline at end of file -diff --git a/app/code/Magento/CmsGraphQl/registration.php b/app/code/Magento/CmsGraphQl/registration.php -new file mode 100644 -index 00000000000..9a3fabf6c95 ---- /dev/null -+++ b/app/code/Magento/CmsGraphQl/registration.php -@@ -0,0 +1,10 @@ -+<?php -+/** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+declare(strict_types=1); -+ -+use Magento\Framework\Component\ComponentRegistrar; -+ -+ComponentRegistrar::register(ComponentRegistrar::MODULE, 'Magento_CmsGraphQl', __DIR__); -diff --git a/app/code/Magento/Config/App/Config/Type/System.php b/app/code/Magento/Config/App/Config/Type/System.php -index 479b4f5c217..c63ccae8716 100644 ---- a/app/code/Magento/Config/App/Config/Type/System.php -+++ b/app/code/Magento/Config/App/Config/Type/System.php -@@ -3,27 +3,48 @@ - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -+ - namespace Magento\Config\App\Config\Type; - -+use Magento\Framework\App\Config\ConfigSourceInterface; - use Magento\Framework\App\Config\ConfigTypeInterface; -+use Magento\Framework\App\Config\Spi\PostProcessorInterface; -+use Magento\Framework\App\Config\Spi\PreProcessorInterface; - use Magento\Framework\App\ObjectManager; - use Magento\Config\App\Config\Type\System\Reader; -+use Magento\Framework\App\ScopeInterface; -+use Magento\Framework\Cache\FrontendInterface; -+use Magento\Framework\Cache\LockGuardedCacheLoader; -+use Magento\Framework\Lock\LockManagerInterface; -+use Magento\Framework\Serialize\SerializerInterface; -+use Magento\Store\Model\Config\Processor\Fallback; -+use Magento\Framework\Encryption\Encryptor; -+use Magento\Store\Model\ScopeInterface as StoreScope; - - /** - * System configuration type -+ * - * @api - * @since 100.1.2 -+ * @SuppressWarnings(PHPMD.CouplingBetweenObjects) -+ * @SuppressWarnings(PHPMD.UnusedPrivateMethod) - */ - class System implements ConfigTypeInterface - { -+ /** -+ * Config cache tag. -+ */ - const CACHE_TAG = 'config_scopes'; - -+ /** -+ * System config type. -+ */ - const CONFIG_TYPE = 'system'; - - /** -- * @var \Magento\Framework\App\Config\ConfigSourceInterface -+ * @var string - */ -- private $source; -+ private static $lockName = 'SYSTEM_CONFIG'; - - /** - * @var array -@@ -31,32 +52,17 @@ class System implements ConfigTypeInterface - private $data = []; - - /** -- * @var \Magento\Framework\App\Config\Spi\PostProcessorInterface -+ * @var PostProcessorInterface - */ - private $postProcessor; - - /** -- * @var \Magento\Framework\App\Config\Spi\PreProcessorInterface -- */ -- private $preProcessor; -- -- /** -- * @var \Magento\Framework\Cache\FrontendInterface -+ * @var FrontendInterface - */ - private $cache; - - /** -- * @var int -- */ -- private $cachingNestedLevel; -- -- /** -- * @var \Magento\Store\Model\Config\Processor\Fallback -- */ -- private $fallback; -- -- /** -- * @var \Magento\Framework\Serialize\SerializerInterface -+ * @var SerializerInterface - */ - private $serializer; - -@@ -79,42 +85,62 @@ class System implements ConfigTypeInterface - * - * @var array - */ -- private $availableDataScopes = null; -+ private $availableDataScopes; - - /** -- * @param \Magento\Framework\App\Config\ConfigSourceInterface $source -- * @param \Magento\Framework\App\Config\Spi\PostProcessorInterface $postProcessor -- * @param \Magento\Store\Model\Config\Processor\Fallback $fallback -- * @param \Magento\Framework\Cache\FrontendInterface $cache -- * @param \Magento\Framework\Serialize\SerializerInterface $serializer -- * @param \Magento\Framework\App\Config\Spi\PreProcessorInterface $preProcessor -+ * @var Encryptor -+ */ -+ private $encryptor; -+ -+ /** -+ * @var LockGuardedCacheLoader -+ */ -+ private $lockQuery; -+ -+ /** -+ * @param ConfigSourceInterface $source -+ * @param PostProcessorInterface $postProcessor -+ * @param Fallback $fallback -+ * @param FrontendInterface $cache -+ * @param SerializerInterface $serializer -+ * @param PreProcessorInterface $preProcessor - * @param int $cachingNestedLevel - * @param string $configType -- * @param Reader $reader -+ * @param Reader|null $reader -+ * @param Encryptor|null $encryptor -+ * @param LockManagerInterface|null $locker -+ * @param LockGuardedCacheLoader|null $lockQuery -+ * @SuppressWarnings(PHPMD.UnusedFormalParameter) -+ * @SuppressWarnings(PHPMD.ExcessiveParameterList) - */ - public function __construct( -- \Magento\Framework\App\Config\ConfigSourceInterface $source, -- \Magento\Framework\App\Config\Spi\PostProcessorInterface $postProcessor, -- \Magento\Store\Model\Config\Processor\Fallback $fallback, -- \Magento\Framework\Cache\FrontendInterface $cache, -- \Magento\Framework\Serialize\SerializerInterface $serializer, -- \Magento\Framework\App\Config\Spi\PreProcessorInterface $preProcessor, -+ ConfigSourceInterface $source, -+ PostProcessorInterface $postProcessor, -+ Fallback $fallback, -+ FrontendInterface $cache, -+ SerializerInterface $serializer, -+ PreProcessorInterface $preProcessor, - $cachingNestedLevel = 1, - $configType = self::CONFIG_TYPE, -- Reader $reader = null -+ Reader $reader = null, -+ Encryptor $encryptor = null, -+ LockManagerInterface $locker = null, -+ LockGuardedCacheLoader $lockQuery = null - ) { -- $this->source = $source; - $this->postProcessor = $postProcessor; -- $this->preProcessor = $preProcessor; - $this->cache = $cache; -- $this->cachingNestedLevel = $cachingNestedLevel; -- $this->fallback = $fallback; - $this->serializer = $serializer; - $this->configType = $configType; - $this->reader = $reader ?: ObjectManager::getInstance()->get(Reader::class); -+ $this->encryptor = $encryptor -+ ?: ObjectManager::getInstance()->get(Encryptor::class); -+ $this->lockQuery = $lockQuery -+ ?: ObjectManager::getInstance()->get(LockGuardedCacheLoader::class); - } - - /** -+ * Get configuration value by path -+ * - * System configuration is separated by scopes (default, websites, stores). Configuration of a scope is inherited - * from its parent scope (store inherits website). - * -@@ -136,68 +162,108 @@ class System implements ConfigTypeInterface - { - if ($path === '') { - $this->data = array_replace_recursive($this->loadAllData(), $this->data); -+ - return $this->data; - } -+ -+ return $this->getWithParts($path); -+ } -+ -+ /** -+ * Proceed with parts extraction from path. -+ * -+ * @param string $path -+ * @return array|int|string|boolean -+ */ -+ private function getWithParts($path) -+ { - $pathParts = explode('/', $path); -- if (count($pathParts) === 1 && $pathParts[0] !== 'default') { -+ -+ if (count($pathParts) === 1 && $pathParts[0] !== ScopeInterface::SCOPE_DEFAULT) { - if (!isset($this->data[$pathParts[0]])) { - $data = $this->readData(); - $this->data = array_replace_recursive($data, $this->data); - } -+ - return $this->data[$pathParts[0]]; - } -+ - $scopeType = array_shift($pathParts); -- if ($scopeType === 'default') { -+ -+ if ($scopeType === ScopeInterface::SCOPE_DEFAULT) { - if (!isset($this->data[$scopeType])) { - $this->data = array_replace_recursive($this->loadDefaultScopeData($scopeType), $this->data); - } -+ - return $this->getDataByPathParts($this->data[$scopeType], $pathParts); - } -+ - $scopeId = array_shift($pathParts); -+ - if (!isset($this->data[$scopeType][$scopeId])) { -- $this->data = array_replace_recursive($this->loadScopeData($scopeType, $scopeId), $this->data); -+ $scopeData = $this->loadScopeData($scopeType, $scopeId); -+ -+ if (!isset($this->data[$scopeType][$scopeId])) { -+ $this->data = array_replace_recursive($scopeData, $this->data); -+ } - } -+ - return isset($this->data[$scopeType][$scopeId]) - ? $this->getDataByPathParts($this->data[$scopeType][$scopeId], $pathParts) - : null; - } - - /** -- * Load configuration data for all scopes -+ * Load configuration data for all scopes. - * - * @return array - */ - private function loadAllData() - { -- $cachedData = $this->cache->load($this->configType); -- if ($cachedData === false) { -- $data = $this->readData(); -- } else { -- $data = $this->serializer->unserialize($cachedData); -- } -- return $data; -+ $loadAction = function () { -+ $cachedData = $this->cache->load($this->configType); -+ $data = false; -+ if ($cachedData !== false) { -+ $data = $this->serializer->unserialize($this->encryptor->decrypt($cachedData)); -+ } -+ return $data; -+ }; -+ -+ return $this->lockQuery->lockedLoadData( -+ self::$lockName, -+ $loadAction, -+ \Closure::fromCallable([$this, 'readData']), -+ \Closure::fromCallable([$this, 'cacheData']) -+ ); - } - - /** -- * Load configuration data for default scope -+ * Load configuration data for default scope. - * - * @param string $scopeType - * @return array - */ - private function loadDefaultScopeData($scopeType) - { -- $cachedData = $this->cache->load($this->configType . '_' . $scopeType); -- if ($cachedData === false) { -- $data = $this->readData(); -- $this->cacheData($data); -- } else { -- $data = [$scopeType => $this->serializer->unserialize($cachedData)]; -- } -- return $data; -+ $loadAction = function () use ($scopeType) { -+ $cachedData = $this->cache->load($this->configType . '_' . $scopeType); -+ $scopeData = false; -+ if ($cachedData !== false) { -+ $scopeData = [$scopeType => $this->serializer->unserialize($this->encryptor->decrypt($cachedData))]; -+ } -+ return $scopeData; -+ }; -+ -+ return $this->lockQuery->lockedLoadData( -+ self::$lockName, -+ $loadAction, -+ \Closure::fromCallable([$this, 'readData']), -+ \Closure::fromCallable([$this, 'cacheData']) -+ ); - } - - /** -- * Load configuration data for a specified scope -+ * Load configuration data for a specified scope. - * - * @param string $scopeType - * @param string $scopeId -@@ -205,27 +271,39 @@ class System implements ConfigTypeInterface - */ - private function loadScopeData($scopeType, $scopeId) - { -- $cachedData = $this->cache->load($this->configType . '_' . $scopeType . '_' . $scopeId); -- if ($cachedData === false) { -- if ($this->availableDataScopes === null) { -- $cachedScopeData = $this->cache->load($this->configType . '_scopes'); -- if ($cachedScopeData !== false) { -- $this->availableDataScopes = $this->serializer->unserialize($cachedScopeData); -+ $loadAction = function () use ($scopeType, $scopeId) { -+ $cachedData = $this->cache->load($this->configType . '_' . $scopeType . '_' . $scopeId); -+ $scopeData = false; -+ if ($cachedData === false) { -+ if ($this->availableDataScopes === null) { -+ $cachedScopeData = $this->cache->load($this->configType . '_scopes'); -+ if ($cachedScopeData !== false) { -+ $serializedCachedData = $this->encryptor->decrypt($cachedScopeData); -+ $this->availableDataScopes = $this->serializer->unserialize($serializedCachedData); -+ } - } -+ if (is_array($this->availableDataScopes) && !isset($this->availableDataScopes[$scopeType][$scopeId])) { -+ $scopeData = [$scopeType => [$scopeId => []]]; -+ } -+ } else { -+ $serializedCachedData = $this->encryptor->decrypt($cachedData); -+ $scopeData = [$scopeType => [$scopeId => $this->serializer->unserialize($serializedCachedData)]]; - } -- if (is_array($this->availableDataScopes) && !isset($this->availableDataScopes[$scopeType][$scopeId])) { -- return [$scopeType => [$scopeId => []]]; -- } -- $data = $this->readData(); -- $this->cacheData($data); -- } else { -- $data = [$scopeType => [$scopeId => $this->serializer->unserialize($cachedData)]]; -- } -- return $data; -+ -+ return $scopeData; -+ }; -+ -+ return $this->lockQuery->lockedLoadData( -+ self::$lockName, -+ $loadAction, -+ \Closure::fromCallable([$this, 'readData']), -+ \Closure::fromCallable([$this, 'cacheData']) -+ ); - } - - /** - * Cache configuration data. -+ * - * Caches data per scope to avoid reading data for all scopes on every request - * - * @param array $data -@@ -234,35 +312,35 @@ class System implements ConfigTypeInterface - private function cacheData(array $data) - { - $this->cache->save( -- $this->serializer->serialize($data), -+ $this->encryptor->encryptWithFastestAvailableAlgorithm($this->serializer->serialize($data)), - $this->configType, - [self::CACHE_TAG] - ); - $this->cache->save( -- $this->serializer->serialize($data['default']), -+ $this->encryptor->encryptWithFastestAvailableAlgorithm($this->serializer->serialize($data['default'])), - $this->configType . '_default', - [self::CACHE_TAG] - ); - $scopes = []; -- foreach (['websites', 'stores'] as $curScopeType) { -+ foreach ([StoreScope::SCOPE_WEBSITES, StoreScope::SCOPE_STORES] as $curScopeType) { - foreach ($data[$curScopeType] ?? [] as $curScopeId => $curScopeData) { - $scopes[$curScopeType][$curScopeId] = 1; - $this->cache->save( -- $this->serializer->serialize($curScopeData), -+ $this->encryptor->encryptWithFastestAvailableAlgorithm($this->serializer->serialize($curScopeData)), - $this->configType . '_' . $curScopeType . '_' . $curScopeId, - [self::CACHE_TAG] - ); - } - } - $this->cache->save( -- $this->serializer->serialize($scopes), -- $this->configType . "_scopes", -+ $this->encryptor->encryptWithFastestAvailableAlgorithm($this->serializer->serialize($scopes)), -+ $this->configType . '_scopes', - [self::CACHE_TAG] - ); - } - - /** -- * Walk nested hash map by keys from $pathParts -+ * Walk nested hash map by keys from $pathParts. - * - * @param array $data to walk in - * @param array $pathParts keys path -@@ -279,6 +357,7 @@ class System implements ConfigTypeInterface - return null; - } - } -+ - return $data; - } - -@@ -298,7 +377,7 @@ class System implements ConfigTypeInterface - } - - /** -- * Clean cache and global variables cache -+ * Clean cache and global variables cache. - * - * Next items cleared: - * - Internal property intended to store already loaded configuration data -@@ -310,6 +389,13 @@ class System implements ConfigTypeInterface - public function clean() - { - $this->data = []; -- $this->cache->clean(\Zend_Cache::CLEANING_MODE_MATCHING_TAG, [self::CACHE_TAG]); -+ $cleanAction = function () { -+ $this->cache->clean(\Zend_Cache::CLEANING_MODE_MATCHING_TAG, [self::CACHE_TAG]); -+ }; -+ -+ $this->lockQuery->lockedCleanData( -+ self::$lockName, -+ $cleanAction -+ ); - } - } -diff --git a/app/code/Magento/Config/Block/System/Config/Form.php b/app/code/Magento/Config/Block/System/Config/Form.php -index 81e39a83296..8378c058c19 100644 ---- a/app/code/Magento/Config/Block/System/Config/Form.php -+++ b/app/code/Magento/Config/Block/System/Config/Form.php -@@ -134,6 +134,7 @@ class Form extends \Magento\Backend\Block\Widget\Form\Generic - * @param \Magento\Config\Block\System\Config\Form\Fieldset\Factory $fieldsetFactory - * @param \Magento\Config\Block\System\Config\Form\Field\Factory $fieldFactory - * @param array $data -+ * @param SettingChecker|null $settingChecker - */ - public function __construct( - \Magento\Backend\Block\Template\Context $context, -@@ -143,13 +144,15 @@ class Form extends \Magento\Backend\Block\Widget\Form\Generic - \Magento\Config\Model\Config\Structure $configStructure, - \Magento\Config\Block\System\Config\Form\Fieldset\Factory $fieldsetFactory, - \Magento\Config\Block\System\Config\Form\Field\Factory $fieldFactory, -- array $data = [] -+ array $data = [], -+ SettingChecker $settingChecker = null - ) { - parent::__construct($context, $registry, $formFactory, $data); - $this->_configFactory = $configFactory; - $this->_configStructure = $configStructure; - $this->_fieldsetFactory = $fieldsetFactory; - $this->_fieldFactory = $fieldFactory; -+ $this->settingChecker = $settingChecker ?: ObjectManager::getInstance()->get(SettingChecker::class); - - $this->_scopeLabels = [ - self::SCOPE_DEFAULT => __('[GLOBAL]'), -@@ -158,18 +161,6 @@ class Form extends \Magento\Backend\Block\Widget\Form\Generic - ]; - } - -- /** -- * @deprecated 100.1.2 -- * @return SettingChecker -- */ -- private function getSettingChecker() -- { -- if ($this->settingChecker === null) { -- $this->settingChecker = ObjectManager::getInstance()->get(SettingChecker::class); -- } -- return $this->settingChecker; -- } -- - /** - * Initialize objects required to render config form - * -@@ -366,9 +357,8 @@ class Form extends \Magento\Backend\Block\Widget\Form\Generic - - $sharedClass = $this->_getSharedCssClass($field); - $requiresClass = $this->_getRequiresCssClass($field, $fieldPrefix); -+ $isReadOnly = $this->isReadOnly($field, $path); - -- $isReadOnly = $this->getElementVisibility()->isDisabled($field->getPath()) -- ?: $this->getSettingChecker()->isReadOnly($path, $this->getScope(), $this->getStringScopeCode()); - $formField = $fieldset->addField( - $elementId, - $field->getType(), -@@ -417,7 +407,7 @@ class Form extends \Magento\Backend\Block\Widget\Form\Generic - { - $data = $this->getAppConfigDataValue($path); - -- $placeholderValue = $this->getSettingChecker()->getPlaceholderValue( -+ $placeholderValue = $this->settingChecker->getPlaceholderValue( - $path, - $this->getScope(), - $this->getStringScopeCode() -@@ -434,6 +424,10 @@ class Form extends \Magento\Backend\Block\Widget\Form\Generic - $backendModel = $field->getBackendModel(); - // Backend models which implement ProcessorInterface are processed by ScopeConfigInterface - if (!$backendModel instanceof ProcessorInterface) { -+ if (array_key_exists($path, $this->_configData)) { -+ $data = $this->_configData[$path]; -+ } -+ - $backendModel->setPath($path) - ->setValue($data) - ->setWebsite($this->getWebsiteCode()) -@@ -541,7 +535,7 @@ class Form extends \Magento\Backend\Block\Widget\Form\Generic - } - - /** -- * @return \Magento\Backend\Block\Widget\Form|\Magento\Framework\View\Element\AbstractBlock -+ * @inheritdoc - */ - protected function _beforeToHtml() - { -@@ -718,6 +712,7 @@ class Form extends \Magento\Backend\Block\Widget\Form\Generic - * - * @TODO delete this methods when {^see above^} is done - * @return string -+ * @SuppressWarnings(PHPMD.RequestAwareBlockMethod) - */ - public function getSectionCode() - { -@@ -729,6 +724,7 @@ class Form extends \Magento\Backend\Block\Widget\Form\Generic - * - * @TODO delete this methods when {^see above^} is done - * @return string -+ * @SuppressWarnings(PHPMD.RequestAwareBlockMethod) - */ - public function getWebsiteCode() - { -@@ -740,6 +736,7 @@ class Form extends \Magento\Backend\Block\Widget\Form\Generic - * - * @TODO delete this methods when {^see above^} is done - * @return string -+ * @SuppressWarnings(PHPMD.RequestAwareBlockMethod) - */ - public function getStoreCode() - { -@@ -797,6 +794,26 @@ class Form extends \Magento\Backend\Block\Widget\Form\Generic - return $this->appConfig; - } - -+ /** -+ * Check Path is Readonly -+ * -+ * @param \Magento\Config\Model\Config\Structure\Element\Field $field -+ * @param string $path -+ * @return boolean -+ */ -+ private function isReadOnly(\Magento\Config\Model\Config\Structure\Element\Field $field, $path) -+ { -+ $isReadOnly = $this->settingChecker->isReadOnly( -+ $path, -+ ScopeConfigInterface::SCOPE_TYPE_DEFAULT -+ ); -+ if (!$isReadOnly) { -+ $isReadOnly = $this->getElementVisibility()->isDisabled($field->getPath()) -+ ?: $this->settingChecker->isReadOnly($path, $this->getScope(), $this->getStringScopeCode()); -+ } -+ return $isReadOnly; -+ } -+ - /** - * Retrieve deployment config data value by path - * -diff --git a/app/code/Magento/Config/Block/System/Config/Form/Field/Datetime.php b/app/code/Magento/Config/Block/System/Config/Form/Field/Datetime.php -index 63dbb2b80e3..16b18e02000 100644 ---- a/app/code/Magento/Config/Block/System/Config/Form/Field/Datetime.php -+++ b/app/code/Magento/Config/Block/System/Config/Form/Field/Datetime.php -@@ -33,6 +33,8 @@ class Datetime extends \Magento\Config\Block\System\Config\Form\Field - } - - /** -+ * Returns element html -+ * - * @param AbstractElement $element - * @return string - * @codeCoverageIgnore -@@ -40,7 +42,7 @@ class Datetime extends \Magento\Config\Block\System\Config\Form\Field - protected function _getElementHtml(AbstractElement $element) - { - return $this->dateTimeFormatter->formatObject( -- $this->_localeDate->date(intval($element->getValue())), -+ $this->_localeDate->date((int) $element->getValue()), - $this->_localeDate->getDateTimeFormat(\IntlDateFormatter::MEDIUM) - ); - } -diff --git a/app/code/Magento/Config/Block/System/Config/Form/Field/Notification.php b/app/code/Magento/Config/Block/System/Config/Form/Field/Notification.php -index 7f21bf4b92b..2e79cec7088 100644 ---- a/app/code/Magento/Config/Block/System/Config/Form/Field/Notification.php -+++ b/app/code/Magento/Config/Block/System/Config/Form/Field/Notification.php -@@ -10,6 +10,7 @@ use Magento\Framework\Stdlib\DateTime\DateTimeFormatterInterface; - - /** - * Backend system config datetime field renderer -+ * - * @api - * @since 100.0.2 - */ -@@ -35,6 +36,8 @@ class Notification extends \Magento\Config\Block\System\Config\Form\Field - } - - /** -+ * Returns element html -+ * - * @param AbstractElement $element - * @return string - */ -@@ -44,6 +47,6 @@ class Notification extends \Magento\Config\Block\System\Config\Form\Field - $format = $this->_localeDate->getDateTimeFormat( - \IntlDateFormatter::MEDIUM - ); -- return $this->dateTimeFormatter->formatObject($this->_localeDate->date(intval($element->getValue())), $format); -+ return $this->dateTimeFormatter->formatObject($this->_localeDate->date((int) $element->getValue()), $format); - } - } -diff --git a/app/code/Magento/Config/Console/Command/ConfigSet/DefaultProcessor.php b/app/code/Magento/Config/Console/Command/ConfigSet/DefaultProcessor.php -index d7d513bfad4..c622a48b7f2 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 @@ namespace Magento\Config\Console\Command\ConfigSet; - - 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 @@ class DefaultProcessor implements ConfigSetProcessorInterface - } - - try { -- /** @var Value $backendModel */ -- $backendModel = $this->preparedValueFactory->create($path, $value, $scope, $scopeCode); -- if ($backendModel instanceof Value) { -- $resourceModel = $backendModel->getResource(); -- $resourceModel->save($backendModel); -- } -+ $config = $this->configFactory->create(['data' => [ -+ 'scope' => $scope, -+ 'scope_code' => $scopeCode, -+ ]]); -+ $config->setDataByPath($path, $value); -+ $config->save(); - } catch (\Exception $exception) { - throw new CouldNotSaveException(__('%1', $exception->getMessage()), $exception); - } -diff --git a/app/code/Magento/Config/Console/Command/ConfigSetCommand.php b/app/code/Magento/Config/Console/Command/ConfigSetCommand.php -index cb79daddbf5..999d8e41af5 100644 ---- a/app/code/Magento/Config/Console/Command/ConfigSetCommand.php -+++ b/app/code/Magento/Config/Console/Command/ConfigSetCommand.php -@@ -114,13 +114,13 @@ class ConfigSetCommand extends Command - ), - new InputOption( - static::OPTION_LOCK_ENV, -- 'le', -+ 'e', - InputOption::VALUE_NONE, - 'Lock value which prevents modification in the Admin (will be saved in app/etc/env.php)' - ), - new InputOption( - static::OPTION_LOCK_CONFIG, -- 'lc', -+ 'c', - InputOption::VALUE_NONE, - 'Lock and share value with other installations, prevents modification in the Admin ' - . '(will be saved in app/etc/config.php)' -@@ -139,8 +139,10 @@ class ConfigSetCommand extends Command - /** - * Creates and run appropriate processor, depending on input options. - * -- * {@inheritdoc} -+ * @param InputInterface $input -+ * @param OutputInterface $output - * @since 100.2.0 -+ * @return int|null - */ - protected function execute(InputInterface $input, OutputInterface $output) - { -diff --git a/app/code/Magento/Config/Console/Command/ConfigShow/ValueProcessor.php b/app/code/Magento/Config/Console/Command/ConfigShow/ValueProcessor.php -index 582f8750808..aeb57010e49 100644 ---- a/app/code/Magento/Config/Console/Command/ConfigShow/ValueProcessor.php -+++ b/app/code/Magento/Config/Console/Command/ConfigShow/ValueProcessor.php -@@ -97,7 +97,7 @@ class ValueProcessor - $field = $configStructure->getElementByConfigPath($path); - - /** @var Value $backendModel */ -- $backendModel = $field && $field->hasBackendModel() -+ $backendModel = $field instanceof Field && $field->hasBackendModel() - ? $field->getBackendModel() - : $this->configValueFactory->create(); - -diff --git a/app/code/Magento/Config/Controller/Adminhtml/System/Config/Edit.php b/app/code/Magento/Config/Controller/Adminhtml/System/Config/Edit.php -index b65f6e9d4e4..12c6ecbab9f 100644 ---- a/app/code/Magento/Config/Controller/Adminhtml/System/Config/Edit.php -+++ b/app/code/Magento/Config/Controller/Adminhtml/System/Config/Edit.php -@@ -6,7 +6,9 @@ - */ - namespace Magento\Config\Controller\Adminhtml\System\Config; - --class Edit extends AbstractScopeConfig -+use Magento\Framework\App\Action\HttpGetActionInterface as HttpGetActionInterface; -+ -+class Edit extends AbstractScopeConfig implements HttpGetActionInterface - { - /** - * @var \Magento\Framework\View\Result\PageFactory -diff --git a/app/code/Magento/Config/Controller/Adminhtml/System/Config/Index.php b/app/code/Magento/Config/Controller/Adminhtml/System/Config/Index.php -index 66290a79261..03479085f3f 100644 ---- a/app/code/Magento/Config/Controller/Adminhtml/System/Config/Index.php -+++ b/app/code/Magento/Config/Controller/Adminhtml/System/Config/Index.php -@@ -6,7 +6,9 @@ - */ - namespace Magento\Config\Controller\Adminhtml\System\Config; - --class Index extends AbstractScopeConfig -+use Magento\Framework\App\Action\HttpGetActionInterface as HttpGetActionInterface; -+ -+class Index extends AbstractScopeConfig implements HttpGetActionInterface - { - /** - * @var \Magento\Backend\Model\View\Result\ForwardFactory -diff --git a/app/code/Magento/Config/Controller/Adminhtml/System/Config/Save.php b/app/code/Magento/Config/Controller/Adminhtml/System/Config/Save.php -index 290a43c9cd6..2d4b2003380 100644 ---- a/app/code/Magento/Config/Controller/Adminhtml/System/Config/Save.php -+++ b/app/code/Magento/Config/Controller/Adminhtml/System/Config/Save.php -@@ -5,6 +5,7 @@ - */ - namespace Magento\Config\Controller\Adminhtml\System\Config; - -+use Magento\Framework\App\Action\HttpPostActionInterface as HttpPostActionInterface; - use Magento\Config\Controller\Adminhtml\System\AbstractConfig; - - /** -@@ -13,7 +14,7 @@ use Magento\Config\Controller\Adminhtml\System\AbstractConfig; - * @author Magento Core Team <core@magentocommerce.com> - * @SuppressWarnings(PHPMD.CouplingBetweenObjects) - */ --class Save extends AbstractConfig -+class Save extends AbstractConfig implements HttpPostActionInterface - { - /** - * Backend Config Model Factory -diff --git a/app/code/Magento/Config/Controller/Adminhtml/System/Config/State.php b/app/code/Magento/Config/Controller/Adminhtml/System/Config/State.php -index 75716fa380b..5d74eb7ea4e 100644 ---- a/app/code/Magento/Config/Controller/Adminhtml/System/Config/State.php -+++ b/app/code/Magento/Config/Controller/Adminhtml/System/Config/State.php -@@ -6,7 +6,13 @@ - */ - namespace Magento\Config\Controller\Adminhtml\System\Config; - --class State extends AbstractScopeConfig -+use Magento\Framework\App\Action\HttpGetActionInterface; -+use Magento\Framework\App\Action\HttpPostActionInterface; -+ -+/** -+ * Save current state of open tabs. GET is allowed for legacy reasons. -+ */ -+class State extends AbstractScopeConfig implements HttpPostActionInterface, HttpGetActionInterface - { - /** - * @var \Magento\Framework\Controller\Result\RawFactory -diff --git a/app/code/Magento/Config/Model/Config.php b/app/code/Magento/Config/Model/Config.php -index c6e2412f7e5..356c6ca17da 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\Reader\Source\Deployed\SettingChecker; - use Magento\Config\Model\Config\Structure\Element\Group; - use Magento\Config\Model\Config\Structure\Element\Field; - use Magento\Framework\App\ObjectManager; -+use Magento\Framework\App\ScopeInterface; -+use Magento\Framework\App\ScopeResolverPool; -+use Magento\Store\Model\ScopeInterface as StoreScopeInterface; -+use Magento\Store\Model\ScopeTypeNormalizer; - - /** - * Backend config model -+ * - * Used to save configuration - * - * @author Magento Core Team <core@magentocommerce.com> - * @SuppressWarnings(PHPMD.CouplingBetweenObjects) - * @api - * @since 100.0.2 -+ * @method string getSection() -+ * @method void setSection(string $section) -+ * @method string getWebsite() -+ * @method void setWebsite(string $website) -+ * @method string getStore() -+ * @method void setStore(string $store) -+ * @method string getScope() -+ * @method void setScope(string $scope) -+ * @method int getScopeId() -+ * @method void setScopeId(int $scopeId) -+ * @method string getScopeCode() -+ * @method void setScopeCode(string $scopeCode) - */ - class Config extends \Magento\Framework\DataObject - { -@@ -87,6 +104,21 @@ class Config extends \Magento\Framework\DataObject - */ - private $settingChecker; - -+ /** -+ * @var ScopeResolverPool -+ */ -+ private $scopeResolverPool; -+ -+ /** -+ * @var ScopeTypeNormalizer -+ */ -+ private $scopeTypeNormalizer; -+ -+ /** -+ * @var \Magento\Framework\MessageQueue\PoisonPill\PoisonPillPutInterface -+ */ -+ private $pillPut; -+ - /** - * @param \Magento\Framework\App\Config\ReinitableConfigInterface $config - * @param \Magento\Framework\Event\ManagerInterface $eventManager -@@ -97,6 +129,10 @@ class Config extends \Magento\Framework\DataObject - * @param \Magento\Store\Model\StoreManagerInterface $storeManager - * @param Config\Reader\Source\Deployed\SettingChecker|null $settingChecker - * @param array $data -+ * @param ScopeResolverPool|null $scopeResolverPool -+ * @param ScopeTypeNormalizer|null $scopeTypeNormalizer -+ * @param \Magento\Framework\MessageQueue\PoisonPill\PoisonPillPutInterface|null $pillPut -+ * @SuppressWarnings(PHPMD.ExcessiveParameterList) - */ - public function __construct( - \Magento\Framework\App\Config\ReinitableConfigInterface $config, -@@ -107,7 +143,10 @@ class Config extends \Magento\Framework\DataObject - \Magento\Framework\App\Config\ValueFactory $configValueFactory, - \Magento\Store\Model\StoreManagerInterface $storeManager, - SettingChecker $settingChecker = null, -- array $data = [] -+ array $data = [], -+ ScopeResolverPool $scopeResolverPool = null, -+ ScopeTypeNormalizer $scopeTypeNormalizer = null, -+ \Magento\Framework\MessageQueue\PoisonPill\PoisonPillPutInterface $pillPut = null - ) { - parent::__construct($data); - $this->_eventManager = $eventManager; -@@ -117,11 +156,19 @@ class Config extends \Magento\Framework\DataObject - $this->_configLoader = $configLoader; - $this->_configValueFactory = $configValueFactory; - $this->_storeManager = $storeManager; -- $this->settingChecker = $settingChecker ?: ObjectManager::getInstance()->get(SettingChecker::class); -+ $this->settingChecker = $settingChecker -+ ?? ObjectManager::getInstance()->get(SettingChecker::class); -+ $this->scopeResolverPool = $scopeResolverPool -+ ?? ObjectManager::getInstance()->get(ScopeResolverPool::class); -+ $this->scopeTypeNormalizer = $scopeTypeNormalizer -+ ?? ObjectManager::getInstance()->get(ScopeTypeNormalizer::class); -+ $this->pillPut = $pillPut ?: \Magento\Framework\App\ObjectManager::getInstance() -+ ->get(\Magento\Framework\MessageQueue\PoisonPill\PoisonPillPutInterface::class); - } - - /** - * Save config section -+ * - * Require set: section, website, store and groups - * - * @throws \Exception -@@ -186,6 +233,8 @@ class Config extends \Magento\Framework\DataObject - throw $e; - } - -+ $this->pillPut->put(); -+ - return $this; - } - -@@ -237,13 +286,14 @@ class Config extends \Magento\Framework\DataObject - * Get field path - * - * @param Field $field -- * @param array &$oldConfig Need for compatibility with _processGroup() -- * @param array &$extraOldGroups Need for compatibility with _processGroup() -+ * @param string $fieldId Need for support of clone_field feature -+ * @param array $oldConfig Need for compatibility with _processGroup() -+ * @param array $extraOldGroups Need for compatibility with _processGroup() - * @return string - */ -- private function getFieldPath(Field $field, array &$oldConfig, array &$extraOldGroups): string -+ private function getFieldPath(Field $field, string $fieldId, array &$oldConfig, array &$extraOldGroups): string - { -- $path = $field->getGroupPath() . '/' . $field->getId(); -+ $path = $field->getGroupPath() . '/' . $fieldId; - - /** - * Look for custom defined field path -@@ -287,8 +337,8 @@ class Config extends \Magento\Framework\DataObject - * @param string $sectionId - * @param string $groupId - * @param array $groupData -- * @param array &$oldConfig -- * @param array &$extraOldGroups -+ * @param array $oldConfig -+ * @param array $extraOldGroups - * @return array - */ - private function getChangedPaths( -@@ -303,7 +353,7 @@ class Config extends \Magento\Framework\DataObject - if (isset($groupData['fields'])) { - foreach ($groupData['fields'] as $fieldId => $fieldData) { - $field = $this->getField($sectionId, $groupId, $fieldId); -- $path = $this->getFieldPath($field, $oldConfig, $extraOldGroups); -+ $path = $this->getFieldPath($field, $fieldId, $oldConfig, $extraOldGroups); - if ($this->isValueChanged($oldConfig, $path, $fieldData)) { - $changedPaths[] = $path; - } -@@ -334,8 +384,8 @@ class Config extends \Magento\Framework\DataObject - * @param array $groupData - * @param array $groups - * @param string $sectionPath -- * @param array &$extraOldGroups -- * @param array &$oldConfig -+ * @param array $extraOldGroups -+ * @param array $oldConfig - * @param \Magento\Framework\DB\Transaction $saveTransaction - * @param \Magento\Framework\DB\Transaction $deleteTransaction - * @return void -@@ -385,6 +435,11 @@ class Config extends \Magento\Framework\DataObject - if (!isset($fieldData['value'])) { - $fieldData['value'] = null; - } -+ -+ if ($field->getType() == 'multiline' && is_array($fieldData['value'])) { -+ $fieldData['value'] = trim(implode(PHP_EOL, $fieldData['value'])); -+ } -+ - $data = [ - 'field' => $fieldId, - 'groups' => $groups, -@@ -398,7 +453,7 @@ class Config extends \Magento\Framework\DataObject - $backendModel->addData($data); - $this->_checkSingleStoreMode($field, $backendModel); - -- $path = $this->getFieldPath($field, $extraOldGroups, $oldConfig); -+ $path = $this->getFieldPath($field, $fieldId, $extraOldGroups, $oldConfig); - $backendModel->setPath($path)->setValue($fieldData['value']); - - $inherit = !empty($fieldData['inherit']); -@@ -481,30 +536,37 @@ class Config extends \Magento\Framework\DataObject - if ($path === '') { - throw new \UnexpectedValueException('Path must not be empty'); - } -+ - $pathParts = explode('/', $path); - $keyDepth = count($pathParts); -- if ($keyDepth !== 3) { -+ if ($keyDepth < 3) { - throw new \UnexpectedValueException( -- "Allowed depth of configuration is 3 (<section>/<group>/<field>). Your configuration depth is " -- . $keyDepth . " for path '$path'" -+ 'Minimal depth of configuration is 3. Your configuration depth is ' . $keyDepth - ); - } -+ -+ $section = array_shift($pathParts); -+ $this->setData('section', $section); -+ - $data = [ -- 'section' => $pathParts[0], -- 'groups' => [ -- $pathParts[1] => [ -- 'fields' => [ -- $pathParts[2] => ['value' => $value], -- ], -- ], -+ 'fields' => [ -+ array_pop($pathParts) => ['value' => $value], - ], - ]; -- $this->addData($data); -+ while ($pathParts) { -+ $data = [ -+ 'groups' => [ -+ array_pop($pathParts) => $data, -+ ], -+ ]; -+ } -+ $groups = array_replace_recursive((array) $this->getData('groups'), $data['groups']); -+ $this->setData('groups', $groups); - } - - /** -- * Get scope name and scopeId -- * @todo refactor to scope resolver -+ * Set scope data -+ * - * @return void - */ - private function initScope() -@@ -512,31 +574,66 @@ class Config extends \Magento\Framework\DataObject - 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; - } - - /** -@@ -584,7 +681,7 @@ class Config extends \Magento\Framework\DataObject - * Get config data value - * - * @param string $path -- * @param null|bool &$inherit -+ * @param null|bool $inherit - * @param null|array $configData - * @return \Magento\Framework\Simplexml\Element - */ -diff --git a/app/code/Magento/Config/Model/Config/Backend/Admin/Usecustom.php b/app/code/Magento/Config/Model/Config/Backend/Admin/Usecustom.php -index 9a483de6a69..f5d568f2f36 100644 ---- a/app/code/Magento/Config/Model/Config/Backend/Admin/Usecustom.php -+++ b/app/code/Magento/Config/Model/Config/Backend/Admin/Usecustom.php -@@ -10,6 +10,8 @@ - namespace Magento\Config\Model\Config\Backend\Admin; - - /** -+ * Process custom admin url during configuration value save process. -+ * - * @api - * @since 100.0.2 - */ -@@ -56,8 +58,9 @@ class Usecustom extends \Magento\Framework\App\Config\Value - { - $value = $this->getValue(); - if ($value == 1) { -- $customUrl = $this->getData('groups/url/fields/custom/value'); -- if (empty($customUrl)) { -+ $customUrlField = $this->getData('groups/url/fields/custom/value'); -+ $customUrlConfig = $this->_config->getValue('admin/url/custom'); -+ if (empty($customUrlField) && empty($customUrlConfig)) { - throw new \Magento\Framework\Exception\LocalizedException(__('Please specify the admin custom URL.')); - } - } -diff --git a/app/code/Magento/Config/Model/Config/Backend/Currency/AbstractCurrency.php b/app/code/Magento/Config/Model/Config/Backend/Currency/AbstractCurrency.php -index b86b86ad3bb..25303093ace 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; - } - - /** -@@ -71,7 +74,7 @@ abstract class AbstractCurrency extends \Magento\Framework\App\Config\Value - $this->getScopeId() - ); - } -- return strval($value); -+ return (string)$value; - } - - /** -@@ -88,7 +91,7 @@ abstract class AbstractCurrency extends \Magento\Framework\App\Config\Value - $this->getScopeId() - ); - } -- return strval($value); -+ return (string)$value; - } - - /** -diff --git a/app/code/Magento/Config/Model/Config/Backend/Currency/Cron.php b/app/code/Magento/Config/Model/Config/Backend/Currency/Cron.php -index 3f80e01802b..f29fa0611ef 100644 ---- a/app/code/Magento/Config/Model/Config/Backend/Currency/Cron.php -+++ b/app/code/Magento/Config/Model/Config/Backend/Currency/Cron.php -@@ -10,6 +10,8 @@ - namespace Magento\Config\Model\Config\Backend\Currency; - - /** -+ * Cron job configuration for currency -+ * - * @api - * @since 100.0.2 - */ -@@ -47,6 +49,8 @@ class Cron extends \Magento\Framework\App\Config\Value - } - - /** -+ * After save handler -+ * - * @return $this - * @throws \Exception - */ -@@ -59,8 +63,8 @@ class Cron extends \Magento\Framework\App\Config\Value - $frequencyMonthly = \Magento\Cron\Model\Config\Source\Frequency::CRON_MONTHLY; - - $cronExprArray = [ -- intval($time[1]), # Minute -- intval($time[0]), # Hour -+ (int)$time[1], # Minute -+ (int)$time[0], # Hour - $frequency == $frequencyMonthly ? '1' : '*', # Day of the Month - '*', # Month of the Year - $frequency == $frequencyWeekly ? '1' : '*', # Day of the Week -diff --git a/app/code/Magento/Config/Model/Config/Backend/Encrypted.php b/app/code/Magento/Config/Model/Config/Backend/Encrypted.php -index 1a91e403a67..ea3b1d4c74a 100644 ---- a/app/code/Magento/Config/Model/Config/Backend/Encrypted.php -+++ b/app/code/Magento/Config/Model/Config/Backend/Encrypted.php -@@ -9,6 +9,8 @@ - namespace Magento\Config\Model\Config\Backend; - - /** -+ * Backend model for encrypted values. -+ * - * @api - * @since 100.0.2 - */ -@@ -48,9 +50,14 @@ class Encrypted extends \Magento\Framework\App\Config\Value implements - * Magic method called during class serialization - * - * @return string[] -+ * -+ * @SuppressWarnings(PHPMD.SerializationAware) -+ * @deprecated Do not use PHP serialization. - */ - public function __sleep() - { -+ trigger_error('Using PHP serialization is deprecated', E_USER_DEPRECATED); -+ - $properties = parent::__sleep(); - return array_diff($properties, ['_encryptor']); - } -@@ -59,9 +66,14 @@ class Encrypted extends \Magento\Framework\App\Config\Value implements - * Magic method called during class un-serialization - * - * @return void -+ * -+ * @SuppressWarnings(PHPMD.SerializationAware) -+ * @deprecated Do not use PHP serialization. - */ - public function __wakeup() - { -+ trigger_error('Using PHP serialization is deprecated', E_USER_DEPRECATED); -+ - parent::__wakeup(); - $this->_encryptor = \Magento\Framework\App\ObjectManager::getInstance()->get( - \Magento\Framework\Encryption\EncryptorInterface::class -diff --git a/app/code/Magento/Config/Model/Config/Backend/Log/Cron.php b/app/code/Magento/Config/Model/Config/Backend/Log/Cron.php -index 3c36baf6f31..cff6f54b3a7 100644 ---- a/app/code/Magento/Config/Model/Config/Backend/Log/Cron.php -+++ b/app/code/Magento/Config/Model/Config/Backend/Log/Cron.php -@@ -10,6 +10,8 @@ - namespace Magento\Config\Model\Config\Backend\Log; - - /** -+ * Cron logger configuration -+ * - * @api - * @since 100.0.2 - */ -@@ -73,8 +75,8 @@ class Cron extends \Magento\Framework\App\Config\Value - - if ($enabled) { - $cronExprArray = [ -- intval($time[1]), # Minute -- intval($time[0]), # Hour -+ (int)$time[1], # Minute -+ (int)$time[0], # Hour - $frequency == $frequencyMonthly ? '1' : '*', # Day of the Month - '*', # Month of the Year - $frequency == $frequencyWeekly ? '1' : '*', # Day of the Week -diff --git a/app/code/Magento/Config/Model/Config/Backend/Serialized.php b/app/code/Magento/Config/Model/Config/Backend/Serialized.php -index 3d5713357c3..6e0b6275db8 100644 ---- a/app/code/Magento/Config/Model/Config/Backend/Serialized.php -+++ b/app/code/Magento/Config/Model/Config/Backend/Serialized.php -@@ -9,6 +9,8 @@ use Magento\Framework\App\ObjectManager; - use Magento\Framework\Serialize\Serializer\Json; - - /** -+ * Serialized backend model -+ * - * @api - * @since 100.0.2 - */ -@@ -46,17 +48,32 @@ class Serialized extends \Magento\Framework\App\Config\Value - } - - /** -+ * Processing object after load data -+ * - * @return void - */ - protected function _afterLoad() - { - $value = $this->getValue(); - if (!is_array($value)) { -- $this->setValue(empty($value) ? false : $this->serializer->unserialize($value)); -+ try { -+ $this->setValue(empty($value) ? false : $this->serializer->unserialize($value)); -+ } catch (\Exception $e) { -+ $this->_logger->critical( -+ sprintf( -+ 'Failed to unserialize %s config value. The error is: %s', -+ $this->getPath(), -+ $e->getMessage() -+ ) -+ ); -+ $this->setValue(false); -+ } - } - } - - /** -+ * Processing object before save data -+ * - * @return $this - */ - public function beforeSave() -diff --git a/app/code/Magento/Config/Model/Config/Source/Locale/Currency.php b/app/code/Magento/Config/Model/Config/Source/Locale/Currency.php -index b3474674cf7..5beff0d043a 100644 ---- a/app/code/Magento/Config/Model/Config/Source/Locale/Currency.php -+++ b/app/code/Magento/Config/Model/Config/Source/Locale/Currency.php -@@ -4,12 +4,15 @@ - * See COPYING.txt for license details. - */ - --/** -- * Locale currency source -- */ - namespace Magento\Config\Model\Config\Source\Locale; - -+use Magento\Framework\App\Config\ScopeConfigInterface; -+use Magento\Framework\App\ObjectManager; -+use Magento\Framework\Locale\ListsInterface; -+ - /** -+ * Locale currency source. -+ * - * @api - * @since 100.0.2 - */ -@@ -21,27 +24,70 @@ class Currency implements \Magento\Framework\Option\ArrayInterface - protected $_options; - - /** -- * @var \Magento\Framework\Locale\ListsInterface -+ * @var ListsInterface - */ - protected $_localeLists; - - /** -- * @param \Magento\Framework\Locale\ListsInterface $localeLists -+ * @var ScopeConfigInterface - */ -- public function __construct(\Magento\Framework\Locale\ListsInterface $localeLists) -- { -+ private $config; -+ -+ /** -+ * @var array -+ */ -+ private $installedCurrencies; -+ -+ /** -+ * @param ListsInterface $localeLists -+ * @param ScopeConfigInterface $config -+ */ -+ public function __construct( -+ ListsInterface $localeLists, -+ ScopeConfigInterface $config = null -+ ) { - $this->_localeLists = $localeLists; -+ $this->config = $config ?: ObjectManager::getInstance()->get(ScopeConfigInterface::class); - } - - /** -- * @return array -+ * @inheritdoc - */ - public function toOptionArray() - { - if (!$this->_options) { - $this->_options = $this->_localeLists->getOptionCurrencies(); - } -- $options = $this->_options; -+ -+ $selected = array_flip($this->getInstalledCurrencies()); -+ -+ $options = array_filter( -+ $this->_options, -+ function ($option) use ($selected) { -+ return isset($selected[$option['value']]); -+ } -+ ); -+ - return $options; - } -+ -+ /** -+ * Retrieve Installed Currencies. -+ * -+ * @return array -+ */ -+ private function getInstalledCurrencies() -+ { -+ if (!$this->installedCurrencies) { -+ $this->installedCurrencies = explode( -+ ',', -+ $this->config->getValue( -+ 'system/currency/installed', -+ \Magento\Store\Model\ScopeInterface::SCOPE_STORE -+ ) -+ ); -+ } -+ -+ return $this->installedCurrencies; -+ } - } -diff --git a/app/code/Magento/Config/Model/Config/Structure.php b/app/code/Magento/Config/Model/Config/Structure.php -index 5c74220051b..a380dc82a7c 100644 ---- a/app/code/Magento/Config/Model/Config/Structure.php -+++ b/app/code/Magento/Config/Model/Config/Structure.php -@@ -337,7 +337,6 @@ class Structure implements \Magento\Config\Model\Config\Structure\SearchInterfac - /** - * Collects config paths and their structure paths from configuration files. - * Returns the map of config paths and their structure paths. -- * - * All paths are declared in module's system.xml. - * - * ```xml -@@ -394,7 +393,7 @@ class Structure implements \Magento\Config\Model\Config\Structure\SearchInterfac - - foreach ($elements as $element) { - if (isset($element['children'])) { -- $result = array_replace_recursive( -+ $result = array_merge_recursive( - $result, - $this->getFieldsRecursively($element['children']) - ); -diff --git a/app/code/Magento/Config/Model/Config/Structure/AbstractElement.php b/app/code/Magento/Config/Model/Config/Structure/AbstractElement.php -index 78025587c49..db815ec87ed 100644 ---- a/app/code/Magento/Config/Model/Config/Structure/AbstractElement.php -+++ b/app/code/Magento/Config/Model/Config/Structure/AbstractElement.php -@@ -11,6 +11,8 @@ use Magento\Store\Model\StoreManagerInterface; - use Magento\Framework\App\ObjectManager; - - /** -+ * Abstract element. -+ * phpcs:disable Magento2.Classes.AbstractApi - * @api - * @since 100.0.2 - */ -@@ -38,7 +40,7 @@ abstract class AbstractElement implements StructureElementInterface - protected $_storeManager; - - /** -- * @var \Magento\Framework\Module\Manager -+ * @var \Magento\Framework\Module\ModuleManagerInterface - */ - protected $moduleManager; - -@@ -48,11 +50,15 @@ abstract class AbstractElement implements StructureElementInterface - private $elementVisibility; - - /** -+ * Construct. -+ * - * @param \Magento\Store\Model\StoreManagerInterface $storeManager -- * @param \Magento\Framework\Module\Manager $moduleManager -+ * @param \Magento\Framework\Module\ModuleManagerInterface $moduleManager - */ -- public function __construct(StoreManagerInterface $storeManager, \Magento\Framework\Module\Manager $moduleManager) -- { -+ public function __construct( -+ StoreManagerInterface $storeManager, -+ \Magento\Framework\Module\ModuleManagerInterface $moduleManager -+ ) { - $this->_storeManager = $storeManager; - $this->moduleManager = $moduleManager; - } -diff --git a/app/code/Magento/Config/Model/Config/Structure/Element/AbstractComposite.php b/app/code/Magento/Config/Model/Config/Structure/Element/AbstractComposite.php -index 724772622c3..efb918226aa 100644 ---- a/app/code/Magento/Config/Model/Config/Structure/Element/AbstractComposite.php -+++ b/app/code/Magento/Config/Model/Config/Structure/Element/AbstractComposite.php -@@ -6,6 +6,9 @@ - namespace Magento\Config\Model\Config\Structure\Element; - - /** -+ * Abstract Composite. -+ * -+ * phpcs:disable Magento2.Classes.AbstractApi - * @api - * @since 100.0.2 - */ -@@ -20,12 +23,12 @@ abstract class AbstractComposite extends \Magento\Config\Model\Config\Structure\ - - /** - * @param \Magento\Store\Model\StoreManagerInterface $storeManager -- * @param \Magento\Framework\Module\Manager $moduleManager -+ * @param \Magento\Framework\Module\ModuleManagerInterface $moduleManager - * @param Iterator $childrenIterator - */ - public function __construct( - \Magento\Store\Model\StoreManagerInterface $storeManager, -- \Magento\Framework\Module\Manager $moduleManager, -+ \Magento\Framework\Module\ModuleManagerInterface $moduleManager, - Iterator $childrenIterator - ) { - parent::__construct($storeManager, $moduleManager); -diff --git a/app/code/Magento/Config/Model/Config/Structure/Element/Field.php b/app/code/Magento/Config/Model/Config/Structure/Element/Field.php -index 0a6a600b41d..6a8cc6e7674 100644 ---- a/app/code/Magento/Config/Model/Config/Structure/Element/Field.php -+++ b/app/code/Magento/Config/Model/Config/Structure/Element/Field.php -@@ -8,6 +8,8 @@ - namespace Magento\Config\Model\Config\Structure\Element; - - /** -+ * Element field. -+ * - * @api - * @since 100.0.2 - */ -@@ -54,7 +56,7 @@ class Field extends \Magento\Config\Model\Config\Structure\AbstractElement - - /** - * @param \Magento\Store\Model\StoreManagerInterface $storeManager -- * @param \Magento\Framework\Module\Manager $moduleManager -+ * @param \Magento\Framework\Module\ModuleManagerInterface $moduleManager - * @param \Magento\Config\Model\Config\BackendFactory $backendFactory - * @param \Magento\Config\Model\Config\SourceFactory $sourceFactory - * @param \Magento\Config\Model\Config\CommentFactory $commentFactory -@@ -63,7 +65,7 @@ class Field extends \Magento\Config\Model\Config\Structure\AbstractElement - */ - public function __construct( - \Magento\Store\Model\StoreManagerInterface $storeManager, -- \Magento\Framework\Module\Manager $moduleManager, -+ \Magento\Framework\Module\ModuleManagerInterface $moduleManager, - \Magento\Config\Model\Config\BackendFactory $backendFactory, - \Magento\Config\Model\Config\SourceFactory $sourceFactory, - \Magento\Config\Model\Config\CommentFactory $commentFactory, -@@ -243,6 +245,7 @@ class Field extends \Magento\Config\Model\Config\Structure\AbstractElement - */ - public function getGroupPath() - { -+ // phpcs:ignore Magento2.Functions.DiscouragedFunction - return dirname($this->getConfigPath() ?: $this->getPath()); - } - -diff --git a/app/code/Magento/Config/Model/Config/Structure/Element/Group.php b/app/code/Magento/Config/Model/Config/Structure/Element/Group.php -index 8003132d2b8..db479e8b795 100644 ---- a/app/code/Magento/Config/Model/Config/Structure/Element/Group.php -+++ b/app/code/Magento/Config/Model/Config/Structure/Element/Group.php -@@ -7,6 +7,8 @@ - namespace Magento\Config\Model\Config\Structure\Element; - - /** -+ * Group element. -+ * - * @api - * @since 100.0.2 - */ -@@ -27,14 +29,14 @@ class Group extends AbstractComposite - - /** - * @param \Magento\Store\Model\StoreManagerInterface $storeManager -- * @param \Magento\Framework\Module\Manager $moduleManager -+ * @param \Magento\Framework\Module\ModuleManagerInterface $moduleManager - * @param Iterator\Field $childrenIterator - * @param \Magento\Config\Model\Config\BackendClone\Factory $cloneModelFactory - * @param Dependency\Mapper $dependencyMapper - */ - public function __construct( - \Magento\Store\Model\StoreManagerInterface $storeManager, -- \Magento\Framework\Module\Manager $moduleManager, -+ \Magento\Framework\Module\ModuleManagerInterface $moduleManager, - \Magento\Config\Model\Config\Structure\Element\Iterator\Field $childrenIterator, - \Magento\Config\Model\Config\BackendClone\Factory $cloneModelFactory, - \Magento\Config\Model\Config\Structure\Element\Dependency\Mapper $dependencyMapper -diff --git a/app/code/Magento/Config/Model/Config/Structure/Element/Section.php b/app/code/Magento/Config/Model/Config/Structure/Element/Section.php -index c3d927a1d6d..134411fbd87 100644 ---- a/app/code/Magento/Config/Model/Config/Structure/Element/Section.php -+++ b/app/code/Magento/Config/Model/Config/Structure/Element/Section.php -@@ -6,6 +6,8 @@ - namespace Magento\Config\Model\Config\Structure\Element; - - /** -+ * Section -+ * - * @api - * @since 100.0.2 - */ -@@ -20,13 +22,13 @@ class Section extends AbstractComposite - - /** - * @param \Magento\Store\Model\StoreManagerInterface $storeManager -- * @param \Magento\Framework\Module\Manager $moduleManager -+ * @param \Magento\Framework\Module\ModuleManagerInterface $moduleManager - * @param Iterator $childrenIterator - * @param \Magento\Framework\AuthorizationInterface $authorization - */ - public function __construct( - \Magento\Store\Model\StoreManagerInterface $storeManager, -- \Magento\Framework\Module\Manager $moduleManager, -+ \Magento\Framework\Module\ModuleManagerInterface $moduleManager, - Iterator $childrenIterator, - \Magento\Framework\AuthorizationInterface $authorization - ) { -diff --git a/app/code/Magento/Config/Model/Config/Structure/Mapper/Sorting.php b/app/code/Magento/Config/Model/Config/Structure/Mapper/Sorting.php -index f6f3a0be187..19e1acc6170 100644 ---- a/app/code/Magento/Config/Model/Config/Structure/Mapper/Sorting.php -+++ b/app/code/Magento/Config/Model/Config/Structure/Mapper/Sorting.php -@@ -10,6 +10,8 @@ - namespace Magento\Config\Model\Config\Structure\Mapper; - - /** -+ * Sorting mapper -+ * - * @api - * @since 100.0.2 - */ -@@ -30,6 +32,8 @@ class Sorting extends \Magento\Config\Model\Config\Structure\AbstractMapper - } - - /** -+ * Process config -+ * - * @param array $data - * @return array - */ -@@ -55,17 +59,13 @@ class Sorting extends \Magento\Config\Model\Config\Structure\AbstractMapper - { - $sortIndexA = 0; - if ($this->_hasValue('sortOrder', $elementA)) { -- $sortIndexA = floatval($elementA['sortOrder']); -+ $sortIndexA = (float)$elementA['sortOrder']; - } - $sortIndexB = 0; - if ($this->_hasValue('sortOrder', $elementB)) { -- $sortIndexB = floatval($elementB['sortOrder']); -- } -- -- if ($sortIndexA == $sortIndexB) { -- return 0; -+ $sortIndexB = (float)$elementB['sortOrder']; - } - -- return $sortIndexA < $sortIndexB ? -1 : 1; -+ return $sortIndexA <=> $sortIndexB; - } - } -diff --git a/app/code/Magento/Config/Observer/Config/Backend/Admin/AfterCustomUrlChangedObserver.php b/app/code/Magento/Config/Observer/Config/Backend/Admin/AfterCustomUrlChangedObserver.php -index bf414890d0e..830b6376c94 100644 ---- a/app/code/Magento/Config/Observer/Config/Backend/Admin/AfterCustomUrlChangedObserver.php -+++ b/app/code/Magento/Config/Observer/Config/Backend/Admin/AfterCustomUrlChangedObserver.php -@@ -3,10 +3,17 @@ - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -+declare(strict_types=1); -+ - namespace Magento\Config\Observer\Config\Backend\Admin; - - use Magento\Framework\Event\ObserverInterface; - -+/** -+ * Class AfterCustomUrlChangedObserver redirects to new custom admin URL. -+ * -+ * @SuppressWarnings(PHPMD.CookieAndSessionMisuse) -+ */ - class AfterCustomUrlChangedObserver implements ObserverInterface - { - /** -@@ -56,7 +63,6 @@ class AfterCustomUrlChangedObserver implements ObserverInterface - * - * @param \Magento\Framework\Event\Observer $observer - * @return void -- * @SuppressWarnings(PHPMD.ExitExpression) - * @SuppressWarnings(PHPMD.UnusedFormalParameter) - */ - public function execute(\Magento\Framework\Event\Observer $observer) -@@ -68,6 +74,7 @@ class AfterCustomUrlChangedObserver implements ObserverInterface - $this->_authSession->destroy(); - $adminUrl = $this->_backendData->getHomePageUrl(); - $this->_response->setRedirect($adminUrl)->sendResponse(); -+ // phpcs:ignore Magento2.Security.LanguageConstruct.ExitUsage - exit(0); - } - } -diff --git a/app/code/Magento/Config/Setup/ConfigOptionsList.php b/app/code/Magento/Config/Setup/ConfigOptionsList.php -new file mode 100644 -index 00000000000..c410eeae615 ---- /dev/null -+++ b/app/code/Magento/Config/Setup/ConfigOptionsList.php -@@ -0,0 +1,135 @@ -+<?php -+/** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+declare(strict_types=1); -+ -+namespace Magento\Config\Setup; -+ -+use Magento\Framework\App\DeploymentConfig; -+use Magento\Framework\Config\Data\ConfigData; -+use Magento\Framework\Config\Data\ConfigDataFactory; -+use Magento\Framework\Config\File\ConfigFilePool; -+use Magento\Framework\Setup\ConfigOptionsListInterface; -+use Magento\Framework\Setup\Option\SelectConfigOption; -+ -+/** -+ * Deployment configuration options required for the Config module. -+ */ -+class ConfigOptionsList implements ConfigOptionsListInterface -+{ -+ /** -+ * Input key for the debug_logging option. -+ */ -+ const INPUT_KEY_DEBUG_LOGGING = 'enable-debug-logging'; -+ -+ /** -+ * Path to the debug_logging value in the deployment config. -+ */ -+ const CONFIG_PATH_DEBUG_LOGGING = 'dev/debug/debug_logging'; -+ -+ /** -+ * Input key for the syslog_logging option. -+ */ -+ const INPUT_KEY_SYSLOG_LOGGING = 'enable-syslog-logging'; -+ -+ /** -+ * Path to the syslog_logging value in the deployment config. -+ */ -+ const CONFIG_PATH_SYSLOG_LOGGING = 'dev/syslog/syslog_logging'; -+ -+ /** -+ * @var ConfigDataFactory -+ */ -+ private $configDataFactory; -+ -+ /** -+ * @param ConfigDataFactory $configDataFactory -+ */ -+ public function __construct(ConfigDataFactory $configDataFactory) -+ { -+ $this->configDataFactory = $configDataFactory; -+ } -+ -+ /** -+ * @inheritdoc -+ */ -+ public function getOptions() -+ { -+ return [ -+ new SelectConfigOption( -+ self::INPUT_KEY_DEBUG_LOGGING, -+ SelectConfigOption::FRONTEND_WIZARD_RADIO, -+ [true, false, 1, 0], -+ self::CONFIG_PATH_DEBUG_LOGGING, -+ 'Enable debug logging' -+ ), -+ new SelectConfigOption( -+ self::INPUT_KEY_SYSLOG_LOGGING, -+ SelectConfigOption::FRONTEND_WIZARD_RADIO, -+ [true, false, 1, 0], -+ self::CONFIG_PATH_SYSLOG_LOGGING, -+ 'Enable syslog logging' -+ ), -+ ]; -+ } -+ -+ /** -+ * @inheritdoc -+ */ -+ public function createConfig(array $options, DeploymentConfig $deploymentConfig) -+ { -+ $deploymentOption = [ -+ self::INPUT_KEY_DEBUG_LOGGING => self::CONFIG_PATH_DEBUG_LOGGING, -+ self::INPUT_KEY_SYSLOG_LOGGING => self::CONFIG_PATH_SYSLOG_LOGGING, -+ ]; -+ -+ $config = []; -+ foreach ($deploymentOption as $inputKey => $configPath) { -+ $configValue = $this->processBooleanConfigValue( -+ $inputKey, -+ $configPath, -+ $options -+ ); -+ if ($configValue) { -+ $config[] = $configValue; -+ } -+ } -+ -+ return $config; -+ } -+ -+ /** -+ * Provide config value from input. -+ * -+ * @param string $inputKey -+ * @param string $configPath -+ * @param array $options -+ * @return ConfigData|null -+ */ -+ private function processBooleanConfigValue(string $inputKey, string $configPath, array &$options): ?ConfigData -+ { -+ $configData = null; -+ if (isset($options[$inputKey])) { -+ $configData = $this->configDataFactory->create(ConfigFilePool::APP_ENV); -+ if ($options[$inputKey] === 'true' -+ || $options[$inputKey] === '1') { -+ $value = 1; -+ } else { -+ $value = 0; -+ } -+ $configData->set($configPath, $value); -+ } -+ -+ return $configData; -+ } -+ -+ /** -+ * @inheritdoc -+ */ -+ public function validate(array $options, DeploymentConfig $deploymentConfig) -+ { -+ return []; -+ } -+} -diff --git a/app/code/Magento/Config/Test/Mftf/ActionGroup/AdminConfigCreateNewAccountActionGroup.xml b/app/code/Magento/Config/Test/Mftf/ActionGroup/AdminConfigCreateNewAccountActionGroup.xml -new file mode 100644 -index 00000000000..084f4ce92f0 ---- /dev/null -+++ b/app/code/Magento/Config/Test/Mftf/ActionGroup/AdminConfigCreateNewAccountActionGroup.xml -@@ -0,0 +1,25 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> -+ <actionGroup name="SetGroupForValidVATIdIntraUnionActionGroup"> -+ <arguments> -+ <argument name="value" type="string"/> -+ </arguments> -+ <amOnPage url="{{AdminStoresCustomerConfigurationPage.url}}" stepKey="navigateToCustomerConfigurationPage" /> -+ <waitForPageLoad stepKey="waitForPageLoad"/> -+ <conditionalClick stepKey="expandCreateNewAccountOptionsTab" selector="{{AdminStoresCustomerConfigurationSection.createNewAccOpt}}" dependentSelector="{{AdminStoresCustomerConfigurationSection.enableAutoAssignCustomerGroup}}" visible="false"/> -+ <waitForElementVisible selector="{{AdminStoresCustomerConfigurationSection.createNewAccOpt}}" stepKey="waitForElementsAppeared"/> -+ <selectOption selector="{{AdminStoresCustomerConfigurationSection.groupForValidVATIdIntraUnion}}" userInput="{{value}}" stepKey="selectValue"/> -+ <click selector="{{AdminStoresCustomerConfigurationSection.createNewAccOpt}}" stepKey="collapseTab" /> -+ <click selector="{{ContentManagementSection.Save}}" stepKey="saveConfig"/> -+ <waitForPageLoad stepKey="waitForConfigSaved"/> -+ <see userInput="You saved the configuration." stepKey="seeSuccessMessage"/> -+ </actionGroup> -+</actionGroups> -diff --git a/app/code/Magento/Config/Test/Mftf/ActionGroup/AdminSaveConfigActionGroup.xml b/app/code/Magento/Config/Test/Mftf/ActionGroup/AdminSaveConfigActionGroup.xml -new file mode 100644 -index 00000000000..6ed0cfe95cb ---- /dev/null -+++ b/app/code/Magento/Config/Test/Mftf/ActionGroup/AdminSaveConfigActionGroup.xml -@@ -0,0 +1,16 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> -+ <actionGroup name="AdminSaveConfigActionGroup"> -+ <click selector="{{AdminConfigSection.saveButton}}" stepKey="clickSaveConfigBtn"/> -+ <waitForPageLoad stepKey="waitForPageLoad"/> -+ <see selector="{{AdminCategoryMessagesSection.SuccessMessage}}" userInput="You saved the configuration." stepKey="seeSuccessMessage"/> -+ </actionGroup> -+</actionGroups> -diff --git a/app/code/Magento/Config/Test/Mftf/ActionGroup/ConfigAdminAccountSharingActionGroup.xml b/app/code/Magento/Config/Test/Mftf/ActionGroup/ConfigAdminAccountSharingActionGroup.xml -index 51155423e62..00bda74c7b5 100644 ---- a/app/code/Magento/Config/Test/Mftf/ActionGroup/ConfigAdminAccountSharingActionGroup.xml -+++ b/app/code/Magento/Config/Test/Mftf/ActionGroup/ConfigAdminAccountSharingActionGroup.xml -@@ -7,16 +7,22 @@ - --> - - <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="ConfigAdminAccountSharingActionGroup"> -- <amOnPage url="{{_ENV.MAGENTO_BACKEND_NAME}}/admin/system_config/edit/section/admin/" stepKey="navigateToConfigurationPage" /> -- <waitForPageLoad stepKey="wait1"/> -- <conditionalClick stepKey="expandSecurityTab" selector="{{AdminSection.SecurityTab}}" dependentSelector="{{AdminSection.CheckIfTabExpand}}" visible="true" /> -- <waitForElementVisible selector="{{AdminSection.AdminAccountSharing}}" stepKey="waitForAdminAccountSharingDrpDown" /> -- <uncheckOption selector="{{AdminSection.EnableSystemValue}}" stepKey="uncheckUseSystemValue"/> -- <selectOption selector="{{AdminSection.AdminAccountSharing}}" userInput="Yes" stepKey="selectYes"/> -- <click selector="{{AdminSection.SecurityTab}}" stepKey="clollapseSecurityTab" /> -- <click selector="{{ContentManagementSection.Save}}" stepKey="saveConfig" /> --</actionGroup> -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> -+ <actionGroup name="ConfigAdminAccountSharingActionGroup"> -+ <amOnPage url="{{_ENV.MAGENTO_BACKEND_NAME}}/admin/system_config/edit/section/admin/" stepKey="navigateToConfigurationPage" /> -+ <waitForPageLoad stepKey="wait1"/> -+ <conditionalClick stepKey="expandSecurityTab" selector="{{AdminSection.SecurityTab}}" dependentSelector="{{AdminSection.CheckIfTabExpand}}" visible="true" /> -+ <waitForElementVisible selector="{{AdminSection.AdminAccountSharing}}" stepKey="waitForAdminAccountSharingDrpDown" /> -+ <uncheckOption selector="{{AdminSection.EnableSystemValue}}" stepKey="uncheckUseSystemValue"/> -+ <selectOption selector="{{AdminSection.AdminAccountSharing}}" userInput="Yes" stepKey="selectYes"/> -+ <click selector="{{AdminSection.SecurityTab}}" stepKey="clollapseSecurityTab" /> -+ <click selector="{{ContentManagementSection.Save}}" stepKey="saveConfig" /> -+ </actionGroup> -+ <actionGroup name="EnableAdminAccountSharingActionGroup"> -+ <createData stepKey="setConfig" entity="EnableAdminAccountSharing"/> -+ </actionGroup> -+ <actionGroup name="DisableAdminAccountSharingActionGroup"> -+ <createData stepKey="setConfig" entity="DisableAdminAccountSharing"/> -+ </actionGroup> - </actionGroups> - -diff --git a/app/code/Magento/Config/Test/Mftf/ActionGroup/ConfigAdminCatalogSearchActionGroup.xml b/app/code/Magento/Config/Test/Mftf/ActionGroup/ConfigAdminCatalogSearchActionGroup.xml -new file mode 100644 -index 00000000000..2109b1cdbb5 ---- /dev/null -+++ b/app/code/Magento/Config/Test/Mftf/ActionGroup/ConfigAdminCatalogSearchActionGroup.xml -@@ -0,0 +1,36 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> -+ <actionGroup name="ChooseElasticSearchAsSearchEngine"> -+ <amOnPage url="{{AdminCatalogSearchConfigurationPage.url}}" stepKey="configureSearchEngine"/> -+ <waitForPageLoad stepKey="waitForConfigPage"/> -+ <scrollTo selector="{{AdminCatalogSearchConfigurationSection.catalogSearchTab}}" stepKey="scrollToCatalogSearchTab"/> -+ <conditionalClick selector="{{AdminCatalogSearchConfigurationSection.catalogSearchTab}}" dependentSelector="{{AdminCatalogSearchConfigurationSection.checkIfCatalogSearchTabExpand}}" visible="true" stepKey="expandCatalogSearchTab"/> -+ <waitForElementVisible selector="{{AdminCatalogSearchConfigurationSection.searchEngine}}" stepKey="waitForDropdownToBeVisible"/> -+ <uncheckOption selector="{{AdminCatalogSearchConfigurationSection.searchEngineDefaultSystemValue}}" stepKey="uncheckUseSystemValue"/> -+ <selectOption selector="{{AdminCatalogSearchConfigurationSection.searchEngine}}" userInput="elasticsearch5" stepKey="chooseES5"/> -+ <!--<scrollTo selector="{{AdminCatalogSearchConfigurationSection.catalogSearchTab}}" stepKey="scrollToCatalogSearchTab2"/>--> -+ <!--<click selector="{{AdminCatalogSearchConfigurationSection.catalogSearchTab}}" stepKey="collapseCatalogSearchTab"/>--> -+ <click selector="{{ContentManagementSection.Save}}" stepKey="saveConfiguration"/> -+ <see selector="{{AdminMessagesSection.success}}" userInput="You saved the configuration." stepKey="seeConfigurationSuccessMessage"/> -+ </actionGroup> -+ <actionGroup name="ResetSearchEngineConfiguration"> -+ <amOnPage url="{{AdminCatalogSearchConfigurationPage.url}}" stepKey="resetSearchEngine"/> -+ <waitForPageLoad stepKey="waitForConfigPage2"/> -+ <scrollTo selector="{{AdminCatalogSearchConfigurationSection.catalogSearchTab}}" stepKey="scrollToCatalogSearchTab2"/> -+ <conditionalClick selector="{{AdminCatalogSearchConfigurationSection.catalogSearchTab}}" dependentSelector="{{AdminCatalogSearchConfigurationSection.checkIfCatalogSearchTabExpand}}" visible="true" stepKey="expandCatalogSearchTab2"/> -+ <waitForElementVisible selector="{{AdminCatalogSearchConfigurationSection.searchEngine}}" stepKey="waitForDropdownToBeVisible2"/> -+ <selectOption selector="{{AdminCatalogSearchConfigurationSection.searchEngine}}" userInput="mysql" stepKey="chooseMySQL"/> -+ <checkOption selector="{{AdminCatalogSearchConfigurationSection.searchEngineDefaultSystemValue}}" stepKey="checkUseSystemValue"/> -+ <click selector="{{ContentManagementSection.Save}}" stepKey="saveConfiguration2"/> -+ <see selector="{{AdminMessagesSection.success}}" userInput="You saved the configuration." stepKey="seeConfigurationSuccessMessage2"/> -+ </actionGroup> -+ -+</actionGroups> -diff --git a/app/code/Magento/Config/Test/Mftf/ActionGroup/ConfigSalesTaxClassActionGroup.xml b/app/code/Magento/Config/Test/Mftf/ActionGroup/ConfigSalesTaxClassActionGroup.xml -index 7bb2441a6a5..4e9319351a1 100644 ---- a/app/code/Magento/Config/Test/Mftf/ActionGroup/ConfigSalesTaxClassActionGroup.xml -+++ b/app/code/Magento/Config/Test/Mftf/ActionGroup/ConfigSalesTaxClassActionGroup.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"> - <actionGroup name="SetTaxClassForShipping"> - <amOnPage url="{{AdminSalesTaxClassPage.url}}" stepKey="navigateToSalesTaxPage"/> - <waitForPageLoad stepKey="waitForPageLoad"/> -@@ -28,4 +28,32 @@ - <click selector="{{SalesConfigSection.TaxClassesTab}}" stepKey="collapseTaxClassesTab"/> - <click selector="{{ContentManagementSection.Save}}" stepKey="saveConfiguration"/> - </actionGroup> -- </actionGroups> -\ No newline at end of file -+ <actionGroup name="SetTaxApplyOnSetting"> -+ <arguments> -+ <argument name="userInput" type="string"/> -+ </arguments> -+ <conditionalClick selector="{{AdminConfigureTaxSection.taxCalculationSettings}}" dependentSelector="{{AdminConfigureTaxSection.taxCalculationAlgorithm}}" visible="false" stepKey="openTaxCalcSettingsSection"/> -+ <scrollTo selector="{{AdminConfigureTaxSection.taxCalculationApplyTaxOnInherit}}" x="0" y="-80" stepKey="goToCheckbox"/> -+ <uncheckOption selector="{{AdminConfigureTaxSection.taxCalculationApplyTaxOnInherit}}" stepKey="enableApplyTaxOnSetting"/> -+ <selectOption selector="{{AdminConfigureTaxSection.taxCalculationApplyTaxOn}}" userInput="{{userInput}}" stepKey="setApplyTaxOn"/> -+ <scrollTo selector="{{SalesConfigSection.TaxClassesTab}}" stepKey="scrollToTop"/> -+ <click selector="{{AdminConfigureTaxSection.taxCalculationSettings}}" stepKey="collapseCalcSettingsTab"/> -+ <click selector="{{AdminConfigureTaxSection.save}}" stepKey="saveConfig"/> -+ <waitForPageLoad stepKey="waitForConfigSaved"/> -+ <see userInput="You saved the configuration." stepKey="seeSuccessMessage"/> -+ </actionGroup> -+ <actionGroup name="DisableTaxApplyOnOriginalPrice"> -+ <arguments> -+ <argument name="userInput" type="string"/> -+ </arguments> -+ <amOnPage url="{{AdminSalesTaxClassPage.url}}" stepKey="navigateToSalesTaxPage"/> -+ <waitForPageLoad stepKey="waitForPageLoad"/> -+ <conditionalClick selector="{{AdminConfigureTaxSection.taxCalculationSettings}}" dependentSelector="{{AdminConfigureTaxSection.taxCalculationAlgorithm}}" visible="false" stepKey="openTaxCalcSettingsSection"/> -+ <scrollTo selector="{{AdminConfigureTaxSection.taxCalculationApplyTaxOnInherit}}" x="0" y="-80" stepKey="goToCheckbox"/> -+ <selectOption selector="{{AdminConfigureTaxSection.taxCalculationApplyTaxOn}}" userInput="{{userInput}}" stepKey="setApplyTaxOff"/> -+ <checkOption selector="{{AdminConfigureTaxSection.taxCalculationApplyTaxOnInherit}}" stepKey="disableApplyTaxOnSetting"/> -+ <click selector="{{AdminConfigureTaxSection.save}}" stepKey="saveConfig"/> -+ <waitForPageLoad stepKey="waitForConfigSaved"/> -+ <see userInput="You saved the configuration." stepKey="seeSuccessMessage"/> -+ </actionGroup> -+</actionGroups> -diff --git a/app/code/Magento/Config/Test/Mftf/ActionGroup/ConfigWYSIWYGActionGroup.xml b/app/code/Magento/Config/Test/Mftf/ActionGroup/ConfigWYSIWYGActionGroup.xml -index 80351c9d975..eefaf5f3b53 100644 ---- a/app/code/Magento/Config/Test/Mftf/ActionGroup/ConfigWYSIWYGActionGroup.xml -+++ b/app/code/Magento/Config/Test/Mftf/ActionGroup/ConfigWYSIWYGActionGroup.xml -@@ -7,27 +7,23 @@ - --> - - <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="EnabledWYSIWYG"> -- <amOnPage url="admin/admin/system_config/edit/section/cms/" stepKey="navigateToConfigurationPage" /> -- <waitForPageLoad stepKey="wait1"/> -- <conditionalClick stepKey="expandWYSIWYGOptions" selector="{{ContentManagementSection.WYSIWYGOptions}}" dependentSelector="{{ContentManagementSection.CheckIfTabExpand}}" visible="true" /> -- <waitForElementVisible selector="{{ContentManagementSection.EnableWYSIWYG}}" stepKey="waitForEnableWYSIWYGDropdown1" /> -- <waitForElementVisible selector="{{ContentManagementSection.EnableSystemValue}}" stepKey="waitForUseSystemValueVisible"/> -- <uncheckOption selector="{{ContentManagementSection.EnableSystemValue}}" stepKey="uncheckUseSystemValue"/> -- <selectOption selector="{{ContentManagementSection.EnableWYSIWYG}}" userInput="Enabled by Default" stepKey="selectOption1"/> -+ <magentoCLI stepKey="enableWYSIWYG" command="config:set cms/wysiwyg/enabled enabled"/> -+ </actionGroup> -+ <actionGroup name="SwitchToTinyMCE3"> -+ <comment userInput="Choose TinyMCE3 as the default editor" stepKey="chooseTinyMCE3AsEditor"/> -+ <conditionalClick stepKey="expandWYSIWYGOptions1" selector="{{ContentManagementSection.WYSIWYGOptions}}" dependentSelector="{{ContentManagementSection.CheckIfTabExpand}}" visible="true" /> -+ <waitForElementVisible selector="{{ContentManagementSection.SwitcherSystemValue}}" stepKey="waitForCheckbox2" /> -+ <uncheckOption selector="{{ContentManagementSection.SwitcherSystemValue}}" stepKey="uncheckUseSystemValue2"/> -+ <waitForElementVisible selector="{{ContentManagementSection.Switcher}}" stepKey="waitForSwitcherDropdown2" /> -+ <selectOption selector="{{ContentManagementSection.Switcher}}" userInput="TinyMCE 3" stepKey="switchToVersion3" /> - <click selector="{{ContentManagementSection.WYSIWYGOptions}}" stepKey="collapseWYSIWYGOptions" /> - <click selector="{{ContentManagementSection.Save}}" stepKey="saveConfig" /> -+ <see selector="{{AdminMessagesSection.success}}" userInput="You saved the configuration." stepKey="seeConfigurationSuccessMessage"/> - </actionGroup> - <actionGroup name="DisabledWYSIWYG"> -- <amOnPage url="admin/admin/system_config/edit/section/cms/" stepKey="navigateToConfigurationPage" /> -- <waitForPageLoad stepKey="wait3"/> -- <conditionalClick stepKey="expandWYSIWYGOptions" selector="{{ContentManagementSection.WYSIWYGOptions}}" dependentSelector="{{ContentManagementSection.CheckIfTabExpand}}" visible="true" /> -- <waitForElementVisible selector="{{ContentManagementSection.EnableWYSIWYG}}" stepKey="waitForEnableWYSIWYGDropdown2" /> -- <uncheckOption selector="{{ContentManagementSection.EnableSystemValue}}" stepKey="uncheckUseSystemValue"/> -- <selectOption selector="{{ContentManagementSection.EnableWYSIWYG}}" userInput="Disabled Completely" stepKey="selectOption2"/> -- <click selector="{{ContentManagementSection.WYSIWYGOptions}}" stepKey="collapseWYSIWYGOptions" /> -- <click selector="{{ContentManagementSection.Save}}" stepKey="saveConfig" /> -+ <magentoCLI stepKey="disableWYSIWYG" command="config:set cms/wysiwyg/enabled disabled"/> - </actionGroup> - <actionGroup name="UseStaticURLForMediaContentInWYSIWYG"> - <arguments> -@@ -42,4 +38,15 @@ - <click selector="{{ContentManagementSection.Save}}" stepKey="saveConfig" /> - <waitForPageLoad stepKey="waitForPageLoad2" /> - </actionGroup> -+ <actionGroup name="EnabledWYSIWYGEditor"> -+ <amOnPage url="{{AdminContentManagementPage.url}}" stepKey="navigateToConfigurationPage"/> -+ <waitForPageLoad stepKey="waitForPageLoad"/> -+ <conditionalClick selector="{{ContentManagementSection.WYSIWYGOptions}}" dependentSelector="{{ContentManagementSection.EnableWYSIWYG}}" visible="false" stepKey="expandWYSIWYGOptionsTab"/> -+ <waitForElementVisible selector="{{ContentManagementSection.EnableWYSIWYG}}" stepKey="waitTabToExpand"/> -+ <uncheckOption selector="{{ContentManagementSection.EnableSystemValue}}" stepKey="enableEnableSystemValue"/> -+ <selectOption selector="{{ContentManagementSection.EnableWYSIWYG}}" userInput="Enabled by Default" stepKey="enableWYSIWYG"/> -+ <click selector="{{ContentManagementSection.WYSIWYGOptions}}" stepKey="collapseWYSIWYGOptionsTab"/> -+ <click selector="{{ContentManagementSection.Save}}" stepKey="clickSaveConfig" /> -+ <see stepKey="seeSuccess" selector="{{AdminCategoryMessagesSection.SuccessMessage}}" userInput="You saved the configuration."/> -+ </actionGroup> - </actionGroups> -diff --git a/app/code/Magento/Config/Test/Mftf/ActionGroup/ConfigWebUrlOptionsActionGroup.xml b/app/code/Magento/Config/Test/Mftf/ActionGroup/ConfigWebUrlOptionsActionGroup.xml -index 056b89624a2..f1e0ea6b7e1 100644 ---- a/app/code/Magento/Config/Test/Mftf/ActionGroup/ConfigWebUrlOptionsActionGroup.xml -+++ b/app/code/Magento/Config/Test/Mftf/ActionGroup/ConfigWebUrlOptionsActionGroup.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"> - <actionGroup name="EnableWebUrlOptions"> - <amOnPage url="{{WebConfigurationPage.url}}" stepKey="navigateToWebConfigurationPage"/> - <waitForPageLoad stepKey="waitForPageLoad"/> -diff --git a/app/code/Magento/Config/Test/Mftf/ActionGroup/GeneralConfigurationActionGroup.xml b/app/code/Magento/Config/Test/Mftf/ActionGroup/GeneralConfigurationActionGroup.xml -index c3c0430a3d5..14eca30d0f7 100644 ---- a/app/code/Magento/Config/Test/Mftf/ActionGroup/GeneralConfigurationActionGroup.xml -+++ b/app/code/Magento/Config/Test/Mftf/ActionGroup/GeneralConfigurationActionGroup.xml -@@ -7,11 +7,47 @@ - --> - - <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="NavigateToDefaultLayoutsSetting"> - <amOnPage url="{{WebConfigurationPage.url}}" stepKey="navigateToWebConfigurationPage"/> - <waitForPageLoad stepKey="waitForPageLoad"/> - <conditionalClick stepKey="expandDefaultLayouts" selector="{{WebSection.DefaultLayoutsTab}}" dependentSelector="{{WebSection.CheckIfTabExpand}}" visible="true" /> - <waitForElementVisible selector="{{DefaultLayoutsSection.categoryLayout}}" stepKey="waittForDefaultCategoryLayout" /> - </actionGroup> -+ -+ <actionGroup name="NavigateToConfigurationGeneralPage"> -+ <amOnPage url="{{AdminConfigGeneralPage.url}}" stepKey="navigateToConfigGeneralPage"/> -+ <waitForPageLoad stepKey="waitForConfigPageLoad"/> -+ </actionGroup> -+ -+ <actionGroup name="SelectTopDestinationsCountry"> -+ <arguments> -+ <argument name="countries" type="entity"/> -+ </arguments> -+ <selectOption selector="{{CountryOptionsSection.topDestinations}}" parameterArray="[{{countries.country}}]" stepKey="selectTopDestinationsCountry"/> -+ <click selector="#save" stepKey="saveConfig"/> -+ <waitForPageLoad stepKey="waitForSavingConfig"/> -+ </actionGroup> -+ -+ <actionGroup name="UnSelectTopDestinationsCountry"> -+ <arguments> -+ <argument name="countries" type="entity"/> -+ </arguments> -+ <unselectOption selector="{{CountryOptionsSection.topDestinations}}" parameterArray="[{{countries.country}}]" stepKey="unSelectTopDestinationsCountry"/> -+ <click selector="#save" stepKey="saveConfig"/> -+ <waitForPageLoad stepKey="waitForSavingConfig"/> -+ </actionGroup> -+ -+ <actionGroup name="SelectCountriesWithRequiredRegion"> -+ <arguments> -+ <argument name="countries" type="entity"/> -+ </arguments> -+ <amOnPage url="{{AdminConfigGeneralPage.url}}" stepKey="navigateToAdminConfigGeneralPage"/> -+ <conditionalClick selector="{{StateOptionsSection.stateOptions}}" dependentSelector="{{StateOptionsSection.countriesWithRequiredRegions}}" visible="false" stepKey="expandStateOptionsTab" /> -+ <waitForAjaxLoad stepKey="waitForAjax"/> -+ <scrollTo selector="{{StateOptionsSection.countriesWithRequiredRegions}}" stepKey="scrollToForm"/> -+ <selectOption selector="{{StateOptionsSection.countriesWithRequiredRegions}}" parameterArray="[{{countries.country}}]" stepKey="selectCountriesWithRequiredRegion"/> -+ <click selector="#save" stepKey="saveConfig"/> -+ <waitForPageLoad stepKey="waitForSavingConfig"/> -+ </actionGroup> - </actionGroups> -diff --git a/app/code/Magento/Config/Test/Mftf/ActionGroup/RestoreLayoutSettingActionGroup.xml b/app/code/Magento/Config/Test/Mftf/ActionGroup/RestoreLayoutSettingActionGroup.xml -index 670cd236be8..e9e899a68c3 100644 ---- a/app/code/Magento/Config/Test/Mftf/ActionGroup/RestoreLayoutSettingActionGroup.xml -+++ b/app/code/Magento/Config/Test/Mftf/ActionGroup/RestoreLayoutSettingActionGroup.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"> - <actionGroup name="RestoreLayoutSetting"> - <amOnPage url="{{WebConfigurationPage.url}}" stepKey="navigateToWebConfigurationPage"/> - <waitForPageLoad stepKey="waitForPageLoad"/> -diff --git a/app/code/Magento/Config/Test/Mftf/ActionGroup/SwitcherActionGroup.xml b/app/code/Magento/Config/Test/Mftf/ActionGroup/SwitcherActionGroup.xml -index 172ace8b18c..f29ee1a4072 100644 ---- a/app/code/Magento/Config/Test/Mftf/ActionGroup/SwitcherActionGroup.xml -+++ b/app/code/Magento/Config/Test/Mftf/ActionGroup/SwitcherActionGroup.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"> - <actionGroup name="SwitchToVersion4ActionGroup"> - <amOnPage url="{{ConfigurationStoresPage.url}}" stepKey="navigateToWYSIWYGConfigPage1"/> - <waitForPageLoad stepKey="waitForConfigPageToLoad"/> -diff --git a/app/code/Magento/Config/Test/Mftf/Data/AllowGuestCheckoutData.xml b/app/code/Magento/Config/Test/Mftf/Data/AllowGuestCheckoutData.xml -new file mode 100644 -index 00000000000..f89cdf1a87b ---- /dev/null -+++ b/app/code/Magento/Config/Test/Mftf/Data/AllowGuestCheckoutData.xml -@@ -0,0 +1,24 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<entities xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataProfileSchema.xsd"> -+ <entity name="EnableAllowGuestCheckout" type="allow_guest_checkout_config"> -+ <requiredEntity type="guest_checkout">AllowGuestCheckoutYes</requiredEntity> -+ </entity> -+ <entity name="AllowGuestCheckoutYes" type="guest_checkout"> -+ <data key="value">1</data> -+ </entity> -+ -+ <entity name="DisableAllowGuestCheckout" type="allow_guest_checkout_config"> -+ <requiredEntity type="guest_checkout">AllowGuestCheckoutNo</requiredEntity> -+ </entity> -+ <entity name="AllowGuestCheckoutNo" type="guest_checkout"> -+ <data key="value">0</data> -+ </entity> -+</entities> -diff --git a/app/code/Magento/Config/Test/Mftf/Data/CountryOptionConfigData.xml b/app/code/Magento/Config/Test/Mftf/Data/CountryOptionConfigData.xml -new file mode 100644 -index 00000000000..53ca46e7462 ---- /dev/null -+++ b/app/code/Magento/Config/Test/Mftf/Data/CountryOptionConfigData.xml -@@ -0,0 +1,24 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<entities xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataProfileSchema.xsd"> -+ <entity name="EnableAdminAccountAllowCountry" type="admin_account_country_options_config"> -+ <requiredEntity type="admin_account_country_options_value">AdminAccountAllowCountryUS</requiredEntity> -+ </entity> -+ <entity name="AdminAccountAllowCountryUS" type="admin_account_country_options_value"> -+ <data key="value">US</data> -+ </entity> -+ -+ <entity name="DisableAdminAccountAllowCountry" type="default_admin_account_country_options_config"> -+ <requiredEntity type="checkoutTotalFlagZero">DefaultAdminAccountAllowCountry</requiredEntity> -+ </entity> -+ <entity name="DefaultAdminAccountAllowCountry" type="checkoutTotalFlagZero"> -+ <data key="value">0</data> -+ </entity> -+</entities> -diff --git a/app/code/Magento/Config/Test/Mftf/Data/LocaleOptionsData.xml b/app/code/Magento/Config/Test/Mftf/Data/LocaleOptionsData.xml -new file mode 100644 -index 00000000000..5647283fae1 ---- /dev/null -+++ b/app/code/Magento/Config/Test/Mftf/Data/LocaleOptionsData.xml -@@ -0,0 +1,24 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<entities xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataProfileSchema.xsd"> -+ <entity name="SetLocaleOptions" type="locale_options_config"> -+ <requiredEntity type="code">setLocaleOptionsFrance</requiredEntity> -+ </entity> -+ <entity name="setLocaleOptionsFrance" type="code"> -+ <data key="value">fr_FR</data> -+ </entity> -+ -+ <entity name="DefaultLocaleOptions" type="locale_options_config"> -+ <requiredEntity type="code">setLocaleOptionsUSA</requiredEntity> -+ </entity> -+ <entity name="setLocaleOptionsUSA" type="code"> -+ <data key="value">en_US</data> -+ </entity> -+</entities> -diff --git a/app/code/Magento/Config/Test/Mftf/Data/SystemConfigData.xml b/app/code/Magento/Config/Test/Mftf/Data/SystemConfigData.xml -new file mode 100644 -index 00000000000..85188eb6e04 ---- /dev/null -+++ b/app/code/Magento/Config/Test/Mftf/Data/SystemConfigData.xml -@@ -0,0 +1,23 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<entities xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataProfileSchema.xsd"> -+ <entity name="AdminAccountSharingYes" type="admin_account_sharing_value"> -+ <data key="value">Yes</data> -+ </entity> -+ <entity name="AdminAccountSharingNo" type="admin_account_sharing_value"> -+ <data key="value">No</data> -+ </entity> -+ <entity name="EnableAdminAccountSharing" type="admin_account_sharing_config"> -+ <requiredEntity type="admin_account_sharing_value">AdminAccountSharingYes</requiredEntity> -+ </entity> -+ <entity name="DisableAdminAccountSharing" type="admin_account_sharing_config"> -+ <requiredEntity type="admin_account_sharing_value">AdminAccountSharingNo</requiredEntity> -+ </entity> -+</entities> -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 00000000000..eda0eb904be ---- /dev/null -+++ b/app/code/Magento/Config/Test/Mftf/Data/WebUrlOptionsConfigData.xml -@@ -0,0 +1,24 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<entities xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataProfileSchema.xsd"> -+ <entity name="DefaultWebUrlOptionsConfig" type="web_url_use_store"> -+ <requiredEntity type="url_use_store_value">DefaultConfigWebUrlOptions</requiredEntity> -+ </entity> -+ <entity name="DefaultConfigWebUrlOptions" type="url_use_store_value"> -+ <data key="value">0</data> -+ </entity> -+ -+ <entity name="EnableWebUrlOptionsConfig" type="web_url_use_store"> -+ <requiredEntity type="url_use_store_value">WebUrlOptionsYes</requiredEntity> -+ </entity> -+ <entity name="WebUrlOptionsYes" type="url_use_store_value"> -+ <data key="value">1</data> -+ </entity> -+</entities> -diff --git a/app/code/Magento/Config/Test/Mftf/Metadata/allow_guest_checkout-meta.xml b/app/code/Magento/Config/Test/Mftf/Metadata/allow_guest_checkout-meta.xml -new file mode 100644 -index 00000000000..052d9b65747 ---- /dev/null -+++ b/app/code/Magento/Config/Test/Mftf/Metadata/allow_guest_checkout-meta.xml -@@ -0,0 +1,21 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+<operations xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataOperation.xsd"> -+ <operation name="AllowGuestCheckoutConfig" dataType="allow_guest_checkout_config" type="create" auth="adminFormKey" url="/admin/system_config/save/section/checkout/" method="POST"> -+ <object key="groups" dataType="allow_guest_checkout_config"> -+ <object key="options" dataType="allow_guest_checkout_config"> -+ <object key="fields" dataType="allow_guest_checkout_config"> -+ <object key="guest_checkout" dataType="guest_checkout"> -+ <field key="value">string</field> -+ </object> -+ </object> -+ </object> -+ </object> -+ </operation> -+</operations> -diff --git a/app/code/Magento/Config/Test/Mftf/Metadata/locale_options_config-meta.xml b/app/code/Magento/Config/Test/Mftf/Metadata/locale_options_config-meta.xml -new file mode 100644 -index 00000000000..055a9896cd2 ---- /dev/null -+++ b/app/code/Magento/Config/Test/Mftf/Metadata/locale_options_config-meta.xml -@@ -0,0 +1,21 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+<operations xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataOperation.xsd"> -+ <operation name="GeneralLocaleOptionsConfig" dataType="locale_options_config" type="create" auth="adminFormKey" url="/admin/system_config/save/section/general/" method="POST"> -+ <object key="groups" dataType="locale_options_config"> -+ <object key="locale" dataType="locale_options_config"> -+ <object key="fields" dataType="locale_options_config"> -+ <object key="code" dataType="code"> -+ <field key="value">string</field> -+ </object> -+ </object> -+ </object> -+ </object> -+ </operation> -+</operations> -diff --git a/app/code/Magento/Config/Test/Mftf/Metadata/system_config-countries-meta.xml b/app/code/Magento/Config/Test/Mftf/Metadata/system_config-countries-meta.xml -new file mode 100644 -index 00000000000..bd16c225af5 ---- /dev/null -+++ b/app/code/Magento/Config/Test/Mftf/Metadata/system_config-countries-meta.xml -@@ -0,0 +1,35 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+<operations xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataOperation.xsd"> -+ <operation name="AdminAccountCountryOptionConfig" dataType="admin_account_country_options_config" type="create" auth="adminFormKey" url="/admin/system_config/save/section/general/" method="POST"> -+ <object key="groups" dataType="admin_account_country_options_config"> -+ <object key="country" dataType="admin_account_country_options_config"> -+ <object key="fields" dataType="admin_account_country_options_config"> -+ <object key="allow" dataType="admin_account_country_options_value"> -+ <field key="value">string</field> -+ </object> -+ </object> -+ </object> -+ </object> -+ </operation> -+ -+ <operation name="DefaultAdminAccountCountryOptionConfig" dataType="default_admin_account_country_options_config" type="create" auth="adminFormKey" url="/admin/system_config/save/section/general/" method="POST"> -+ <object key="groups" dataType="default_admin_account_country_options_config"> -+ <object key="country" dataType="default_admin_account_country_options_config"> -+ <object key="fields" dataType="default_admin_account_country_options_config"> -+ <object key="allow" dataType="default_admin_account_country_options_config"> -+ <object key="inherit" dataType="checkoutTotalFlagZero"> -+ <field key="value">string</field> -+ </object> -+ </object> -+ </object> -+ </object> -+ </object> -+ </operation> -+</operations> -diff --git a/app/code/Magento/Config/Test/Mftf/Metadata/system_config-meta.xml b/app/code/Magento/Config/Test/Mftf/Metadata/system_config-meta.xml -new file mode 100644 -index 00000000000..e7544c4e8ae ---- /dev/null -+++ b/app/code/Magento/Config/Test/Mftf/Metadata/system_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="AdminAccountSharingConfig" dataType="admin_account_sharing_config" type="create" auth="adminFormKey" url="/admin/system_config/save/section/admin/" method="POST"> -+ <object key="groups" dataType="admin_account_sharing_config"> -+ <object key="security" dataType="admin_account_sharing_config"> -+ <object key="fields" dataType="admin_account_sharing_config"> -+ <object key="admin_account_sharing" dataType="admin_account_sharing_value"> -+ <field key="value">string</field> -+ </object> -+ </object> -+ </object> -+ </object> -+ </operation> -+</operations> -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 00000000000..fc14ba7bbab ---- /dev/null -+++ b/app/code/Magento/Config/Test/Mftf/Metadata/web_url_options_config-meta.xml -@@ -0,0 +1,22 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+<operations xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataOperation.xsd"> -+ <operation name="WebUrlOptionsConfig" dataType="web_url_use_store" type="create" auth="adminFormKey" url="/admin/system_config/save/section/web/" -+ method="POST" successRegex="/messages-message-success/" returnRegex=""> -+ <object key="groups" dataType="web_url_use_store"> -+ <object key="url" dataType="web_url_use_store"> -+ <object key="fields" dataType="web_url_use_store"> -+ <object key="use_store" dataType="url_use_store_value"> -+ <field key="value">string</field> -+ </object> -+ </object> -+ </object> -+ </object> -+ </operation> -+</operations> -diff --git a/app/code/Magento/Config/Test/Mftf/Page/AdminCatalogSearchConfigurationPage.xml b/app/code/Magento/Config/Test/Mftf/Page/AdminCatalogSearchConfigurationPage.xml -new file mode 100644 -index 00000000000..d38035cd5e0 ---- /dev/null -+++ b/app/code/Magento/Config/Test/Mftf/Page/AdminCatalogSearchConfigurationPage.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="AdminCatalogSearchConfigurationPage" url="admin/system_config/edit/section/catalog/" area="admin" module="Magento_Config"> -+ <section name="AdminCatalogSearchConfigurationSection"/> -+ <section name="AdminCatalogSearchEngineConfigurationSection"/> -+ </page> -+</pages> -diff --git a/app/code/Magento/Config/Test/Mftf/Page/AdminConfigPage.xml b/app/code/Magento/Config/Test/Mftf/Page/AdminConfigPage.xml -index 8d5aaa5830b..7a62dfff832 100644 ---- a/app/code/Magento/Config/Test/Mftf/Page/AdminConfigPage.xml -+++ b/app/code/Magento/Config/Test/Mftf/Page/AdminConfigPage.xml -@@ -5,7 +5,7 @@ - * 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"> -+<pages xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/PageObject.xsd"> - <page name="AdminConfigPage" url="admin/system_config/" area="admin" module="Magento_Config"> - <section name="AdminConfigSection"/> - </page> -@@ -18,4 +18,7 @@ - <page name="AdminSalesTaxClassPage" url="admin/system_config/edit/section/tax/" area="admin" module="Magento_Config"> - <section name="SalesTaxClassSection"/> - </page> -+ <page name="AdminConfigGeneralPage" url="admin/system_config/edit/section/general/" area="admin" module="Magento_Config"> -+ <section name="GeneralSection"/> -+ </page> - </pages> -diff --git a/app/code/Magento/Config/Test/Mftf/Page/AdminSalesConfigPage.xml b/app/code/Magento/Config/Test/Mftf/Page/AdminSalesConfigPage.xml -index 1a99ff6533d..7897a181ff4 100644 ---- a/app/code/Magento/Config/Test/Mftf/Page/AdminSalesConfigPage.xml -+++ b/app/code/Magento/Config/Test/Mftf/Page/AdminSalesConfigPage.xml -@@ -5,7 +5,7 @@ - * 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"> -+<pages xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/PageObject.xsd"> - <page name="AdminSalesConfigPage" url="admin/system_config/edit/section/sales/{{var1}}" area="admin" parameterized="true" module="Magento_Config"> - <section name="AdminSalesConfigSection"/> - </page> -diff --git a/app/code/Magento/Config/Test/Mftf/Page/AdminStoresCustomerConfigurationPage.xml b/app/code/Magento/Config/Test/Mftf/Page/AdminStoresCustomerConfigurationPage.xml -new file mode 100644 -index 00000000000..6a4efb6b9e1 ---- /dev/null -+++ b/app/code/Magento/Config/Test/Mftf/Page/AdminStoresCustomerConfigurationPage.xml -@@ -0,0 +1,12 @@ -+<?xml version="1.0" encoding="utf-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+<pages xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/PageObject.xsd"> -+ <page name="AdminStoresCustomerConfigurationPage" url="admin/system_config/edit/section/customer/" area="admin" module="Magento_Config"> -+ <section name="AdminStoresCustomerConfigurationSection"/> -+ </page> -+</pages> -diff --git a/app/code/Magento/Config/Test/Mftf/Section/AdminCatalogSearchEngineConfigurationSection.xml b/app/code/Magento/Config/Test/Mftf/Section/AdminCatalogSearchEngineConfigurationSection.xml -new file mode 100644 -index 00000000000..570d831ce58 ---- /dev/null -+++ b/app/code/Magento/Config/Test/Mftf/Section/AdminCatalogSearchEngineConfigurationSection.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="AdminCatalogSearchEngineConfigurationSection"> -+ <element name="searchEngineOptimization" type="button" selector="#catalog_seo-head"/> -+ <element name="openedEngineOptimization" type="button" selector="#catalog_seo-head.open"/> -+ <element name="systemValueUseCategoriesPath" type="checkbox" selector="#catalog_seo_product_use_categories_inherit"/> -+ <element name="selectUseCategoriesPatForProductUrls" type="select" selector="#catalog_seo_product_use_categories"/> -+ </section> -+</sections> -\ No newline at end of file -diff --git a/app/code/Magento/Config/Test/Mftf/Section/AdminConfigSection.xml b/app/code/Magento/Config/Test/Mftf/Section/AdminConfigSection.xml -index a1b8c2f62f7..b5bfe9cc2ea 100644 ---- a/app/code/Magento/Config/Test/Mftf/Section/AdminConfigSection.xml -+++ b/app/code/Magento/Config/Test/Mftf/Section/AdminConfigSection.xml -@@ -5,19 +5,13 @@ - * 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="AdminConfigSection"> -- <element name="advancedReportingMenuItem" type="text" selector="//a[contains(concat(' ',normalize-space(@class),' '),'item-nav')]/span[text()='Advanced Reporting']"/> -- <element name="advancedReportingService" type="select" selector="#analytics_general_enabled"/> -- <element name="advancedReportingServiceLabel" type="text" selector="#row_analytics_general_enabled>td.label>label>span"/> -- <element name="advancedReportingServiceStatus" type="text" selector="#row_analytics_general_enabled>td.value>p>span"/> -- <element name="advancedReportingIndustry" type="select" selector="#analytics_general_vertical"/> -- <element name="advancedReportingIndustryLabel" type="text" selector=".config-vertical-label>label>span"/> -- <element name="advancedReportingHour" type="select" selector="#row_analytics_general_collection_time>td:nth-child(2)>select:nth-child(2)"/> -- <element name="advancedReportingMinute" type="select" selector="#row_analytics_general_collection_time>td:nth-child(2)>select:nth-child(3)"/> -- <element name="advancedReportingSeconds" type="select" selector="#row_analytics_general_collection_time>td:nth-child(2)>select:nth-child(4)"/> -- <element name="advancedReportingBlankIndustryError" type="text" selector=".message-error>div"/> -- <element name="advancedReportingSuccessMessage" type="text" selector=".message-success>div"/> - <element name="saveButton" type="button" selector="#save"/> -+ <element name="generalTab" type="text" selector="//div[@class='admin__page-nav-title title _collapsible']//strong[text()='General']"/> -+ <element name="generalTabClosed" type="text" selector="//div[@class='admin__page-nav-title title _collapsible' and @aria-expanded='false' or @aria-expanded='0']//strong[text()='General']"/> -+ <element name="generalTabOpened" type="text" selector="//div[@class='admin__page-nav-title title _collapsible' and @aria-expanded='true' or @aria-expanded='1']//strong[text()='General']"/> -+ <element name="defaultConfigButton" type="button" selector="#store-change-button" timeout="30"/> -+ <element name="defaultConfigDropdown" type="button" selector="//ul[@class='dropdown-menu']" timeout="30"/> - </section> - </sections> -\ No newline at end of file -diff --git a/app/code/Magento/Config/Test/Mftf/Section/AdminSalesConfigSection.xml b/app/code/Magento/Config/Test/Mftf/Section/AdminSalesConfigSection.xml -index 4897e8415c1..1c2e2603566 100644 ---- a/app/code/Magento/Config/Test/Mftf/Section/AdminSalesConfigSection.xml -+++ b/app/code/Magento/Config/Test/Mftf/Section/AdminSalesConfigSection.xml -@@ -5,9 +5,13 @@ - * 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="AdminSalesConfigSection"> - <element name="enableMAPUseSystemValue" type="checkbox" selector="#sales_msrp_enabled_inherit"/> - <element name="enableMAPSelect" type="select" selector="#sales_msrp_enabled"/> -+ <element name="giftOptions" type="select" selector="#sales_gift_options-head"/> -+ <element name="allowGiftReceipt" type="select" selector="#sales_gift_options_allow_gift_receipt"/> -+ <element name="allowPrintedCard" type="select" selector="#sales_gift_options_allow_printed_card"/> -+ <element name="go" type="select" selector="//a[@id='sales_gift_options-head']/ancestor::div[@class='entry-edit-head admin__collapsible-block']"/> - </section> - </sections> -\ No newline at end of file -diff --git a/app/code/Magento/Config/Test/Mftf/Section/AdminSection.xml b/app/code/Magento/Config/Test/Mftf/Section/AdminSection.xml -index 8278c6366b6..7b6c9f8ab3b 100644 ---- a/app/code/Magento/Config/Test/Mftf/Section/AdminSection.xml -+++ b/app/code/Magento/Config/Test/Mftf/Section/AdminSection.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="AdminSection"> - <element name="CheckIfTabExpand" type="button" selector="#admin_security-head:not(.open)"/> - <element name="SecurityTab" type="button" selector="#admin_security-head"/> -diff --git a/app/code/Magento/Config/Test/Mftf/Section/AdminStoresCustomerConfigurationSection.xml b/app/code/Magento/Config/Test/Mftf/Section/AdminStoresCustomerConfigurationSection.xml -new file mode 100644 -index 00000000000..823be383ce1 ---- /dev/null -+++ b/app/code/Magento/Config/Test/Mftf/Section/AdminStoresCustomerConfigurationSection.xml -@@ -0,0 +1,14 @@ -+<?xml version="1.0" encoding="utf-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> -+ <section name="AdminStoresCustomerConfigurationSection"> -+ <element name="createNewAccOpt" type="button" selector="#customer_create_account-head"/> -+ <element name="enableAutoAssignCustomerGroup" type="button" selector="#customer_create_account_auto_group_assign"/> -+ <element name="groupForValidVATIdIntraUnion" type="select" selector="#customer_create_account_viv_intra_union_group"/> -+ </section> -+</sections> -diff --git a/app/code/Magento/Config/Test/Mftf/Section/CatalogSearchAdminConfigSection.xml b/app/code/Magento/Config/Test/Mftf/Section/CatalogSearchAdminConfigSection.xml -new file mode 100644 -index 00000000000..e82ad4670f9 ---- /dev/null -+++ b/app/code/Magento/Config/Test/Mftf/Section/CatalogSearchAdminConfigSection.xml -@@ -0,0 +1,15 @@ -+<?xml version="1.0" encoding="utf-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> -+ <section name="AdminCatalogSearchConfigurationSection"> -+ <element name="catalogSearchTab" type="button" selector="#catalog_search-head"/> -+ <element name="checkIfCatalogSearchTabExpand" type="button" selector="#catalog_search-head:not(.open)"/> -+ <element name="searchEngineDefaultSystemValue" type="checkbox" selector="#catalog_search_engine_inherit"/> -+ <element name="searchEngine" type="select" selector="#catalog_search_engine"/> -+ </section> -+</sections> -\ No newline at end of file -diff --git a/app/code/Magento/Config/Test/Mftf/Section/CatalogSection.xml b/app/code/Magento/Config/Test/Mftf/Section/CatalogSection.xml -index 78b9d1f72f6..e024d9e5b2e 100644 ---- a/app/code/Magento/Config/Test/Mftf/Section/CatalogSection.xml -+++ b/app/code/Magento/Config/Test/Mftf/Section/CatalogSection.xml -@@ -7,9 +7,28 @@ - --> - - <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="CatalogSection"> - <element name="storefront" type="select" selector="#catalog_frontend-head"/> - <element name="CheckIfTabExpand" type="button" selector="#catalog_frontend-head:not(.open)"/> -+ <element name="price" type="button" selector="#catalog_price-head"/> -+ <element name="checkIfPriceExpand" type="button" selector="//a[@id='catalog_price-head' and @class='open']"/> -+ <element name="catalogPriceScope" type="select" selector="#catalog_price_scope"/> -+ <element name="catalogPriceScopeValue" type="select" selector="//select[@id='catalog_price_scope']/option[text()='{{args}}']" parameterized="true"/> -+ <element name="defaultProductPrice" type="input" selector="#catalog_price_default_product_price"/> -+ <element name="save" type="button" selector="#save"/> -+ <element name="flatCatalogCategoryCheckBox" type="checkbox" selector="#catalog_frontend_flat_catalog_category_inherit"/> -+ <element name="flatCatalogCategory" type="select" selector="#catalog_frontend_flat_catalog_category"/> -+ <element name="flatCatalogProduct" type="select" selector="#catalog_frontend_flat_catalog_product"/> -+ <element name="seo" type="button" selector="#catalog_seo-head"/> -+ <element name="CheckIfSeoTabExpand" type="button" selector="#catalog_seo-head:not(.open)"/> -+ <element name="GenerateUrlRewrites" type="select" selector="#catalog_seo_generate_category_product_rewrites"/> -+ <element name="successMessage" type="text" selector="#messages"/> -+ </section> -+ <section name="GenerateUrlRewritesConfirm"> -+ <element name="title" type="text" selector=".modal-popup.confirm h1.modal-title"/> -+ <element name="message" type="text" selector=".modal-popup.confirm div.modal-content"/> -+ <element name="cancel" type="button" selector=".modal-popup.confirm button.action-dismiss"/> -+ <element name="ok" type="button" selector=".modal-popup.confirm button.action-accept" timeout="60"/> - </section> - </sections> -diff --git a/app/code/Magento/Config/Test/Mftf/Section/GeneralSection.xml b/app/code/Magento/Config/Test/Mftf/Section/GeneralSection.xml -index 18e91dc017c..9bce5065317 100644 ---- a/app/code/Magento/Config/Test/Mftf/Section/GeneralSection.xml -+++ b/app/code/Magento/Config/Test/Mftf/Section/GeneralSection.xml -@@ -7,16 +7,17 @@ - --> - - <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="ContentManagementSection"> -- <element name="WYSIWYGOptions" type="button" selector="#cms_wysiwyg-head"/> -+ <element name="WYSIWYGOptions" type="button" selector="#cms_wysiwyg-head" timeout="60"/> - <element name="CheckIfTabExpand" type="button" selector="#cms_wysiwyg-head:not(.open)"/> - <element name="EnableSystemValue" type="button" selector="#cms_wysiwyg_enabled_inherit"/> - <element name="EnableWYSIWYG" type="button" selector="#cms_wysiwyg_enabled"/> -- <element name="SwitcherSystemValue" type="button" selector="#cms_wysiwyg_editor_inherit"/> -+ <element name="SwitcherSystemValue" type="button" selector="#cms_wysiwyg_editor_inherit" timeout="60"/> - <element name="Switcher" type="button" selector="#cms_wysiwyg_editor" /> - <element name="StaticURL" type="button" selector="#cms_wysiwyg_use_static_urls_in_catalog" /> -- <element name="Save" type="button" selector="#save"/> -+ <element name="Save" type="button" selector="#save" timeout="30"/> -+ <element name="StoreConfigurationPageSuccessMessage" type="text" selector="#messages [data-ui-id='messages-message-success']"/> - </section> - <section name="WebSection"> - <element name="DefaultLayoutsTab" type="button" selector="#web_default_layouts-head"/> -@@ -33,4 +34,14 @@ - <element name="addStoreCodeToUrl" type="select" selector="#web_url_use_store"/> - <element name="systemValueForStoreCode" type="checkbox" selector="#web_url_use_store_inherit"/> - </section> -+ <section name="CountryOptionsSection"> -+ <element name="countryOptions" type="button" selector="#general_country-head"/> -+ <element name="countryOptionsOpen" type="button" selector="#general_country-head.open"/> -+ <element name="topDestinations" type="select" selector="#general_country_destinations"/> -+ </section> -+ <section name="StateOptionsSection"> -+ <element name="stateOptions" type="button" selector="#general_region-head"/> -+ <element name="countriesWithRequiredRegions" type="select" selector="#general_region_state_required"/> -+ <element name="allowToChooseState" type="select" selector="general_region_display_all"/> -+ </section> - </sections> -diff --git a/app/code/Magento/Config/Test/Mftf/Section/SalesConfigSection.xml b/app/code/Magento/Config/Test/Mftf/Section/SalesConfigSection.xml -index f1520f5813e..fbe1fd77eaa 100644 ---- a/app/code/Magento/Config/Test/Mftf/Section/SalesConfigSection.xml -+++ b/app/code/Magento/Config/Test/Mftf/Section/SalesConfigSection.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="SalesConfigSection"> - <element name="TaxClassesTab" type="button" selector="#tax_classes-head"/> - <element name="CheckIfTaxClassesTabExpand" type="button" selector="#tax_classes-head:not(.open)"/> -diff --git a/app/code/Magento/Config/Test/Mftf/Section/StoreConfigSection.xml b/app/code/Magento/Config/Test/Mftf/Section/StoreConfigSection.xml -index 0ff3f3ca55d..52fc0018a94 100644 ---- a/app/code/Magento/Config/Test/Mftf/Section/StoreConfigSection.xml -+++ b/app/code/Magento/Config/Test/Mftf/Section/StoreConfigSection.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="StoreConfigSection"> - <element name="CheckIfTabExpand" type="button" selector="#general_store_information-head:not(.open)"/> - <element name="StoreInformation" type="button" selector="#general_store_information-head"/> -diff --git a/app/code/Magento/Config/Test/Mftf/Test/CheckingCountryDropDownWithOneAllowedCountryTest.xml b/app/code/Magento/Config/Test/Mftf/Test/CheckingCountryDropDownWithOneAllowedCountryTest.xml -new file mode 100644 -index 00000000000..b0a7ee07dda ---- /dev/null -+++ b/app/code/Magento/Config/Test/Mftf/Test/CheckingCountryDropDownWithOneAllowedCountryTest.xml -@@ -0,0 +1,45 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> -+ <test name="CheckingCountryDropDownWithOneAllowedCountryTest"> -+ <annotations> -+ <features value="Config"/> -+ <stories value="MAGETWO-96107: Additional blank option in country dropdown"/> -+ <title value="Checking country drop-down with one allowed country"/> -+ <description value="Check country drop-down with one allowed country"/> -+ <severity value="MAJOR"/> -+ <testCaseId value="MAGETWO-96133"/> -+ <group value="configuration"/> -+ </annotations> -+ <before> -+ <createData entity="EnableAdminAccountAllowCountry" stepKey="setAllowedCountries"/> -+ </before> -+ <after> -+ <createData entity="DisableAdminAccountAllowCountry" stepKey="setDefaultValueForAllowCountries"/> -+ <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> -+ <actionGroup ref="AdminDeleteCustomerActionGroup" stepKey="deleteCustomer"> -+ <argument name="customerEmail" value="CustomerEntityOne.email"/> -+ </actionGroup> -+ <actionGroup ref="AdminClearCustomersFiltersActionGroup" stepKey="clearFilters"/> -+ <waitForPageLoad stepKey="WaitForPageToLoad"/> -+ <actionGroup ref="logout" stepKey="logout"/> -+ </after> -+ <!--Flush Magento Cache--> -+ <magentoCLI stepKey="flushCache" command="cache:flush"/> -+ <!--Create a customer account from Storefront--> -+ <actionGroup ref="SignUpNewUserFromStorefrontActionGroup" stepKey="createAnAccount"> -+ <argument name="Customer" value="CustomerEntityOne"/> -+ </actionGroup> -+ <click selector="{{CheckoutPaymentSection.addressBook}}" stepKey="goToAddressBook"/> -+ <click selector="{{StorefrontCustomerAddressSection.country}}" stepKey="clickToExpandCountryDropDown"/> -+ <see selector="{{StorefrontCustomerAddressSection.country}}" userInput="United States" stepKey="seeSelectedCountry"/> -+ <dontSee selector="{{StorefrontCustomerAddressSection.country}}" userInput="Brazil" stepKey="canNotSeeSelectedCountry"/> -+ </test> -+</tests> -diff --git a/app/code/Magento/Config/Test/Mftf/Test/ConfigurationTest.xml b/app/code/Magento/Config/Test/Mftf/Test/ConfigurationTest.xml -index 0e1a6f4717c..66aacf706b0 100644 ---- a/app/code/Magento/Config/Test/Mftf/Test/ConfigurationTest.xml -+++ b/app/code/Magento/Config/Test/Mftf/Test/ConfigurationTest.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="VerifyAllowDynamicMediaURLsSettingIsRemoved"> - <annotations> - <features value="Backend"/> -diff --git a/app/code/Magento/Config/Test/Unit/App/Config/Type/SystemTest.php b/app/code/Magento/Config/Test/Unit/App/Config/Type/SystemTest.php -deleted file mode 100644 -index 40aa110382e..00000000000 ---- a/app/code/Magento/Config/Test/Unit/App/Config/Type/SystemTest.php -+++ /dev/null -@@ -1,181 +0,0 @@ --<?php --/** -- * Copyright © Magento, Inc. All rights reserved. -- * See COPYING.txt for license details. -- */ -- --namespace Magento\Config\Test\Unit\App\Config\Type; -- --use Magento\Config\App\Config\Type\System; --use Magento\Framework\App\Config\ConfigSourceInterface; --use Magento\Framework\App\Config\Spi\PostProcessorInterface; --use Magento\Framework\App\Config\Spi\PreProcessorInterface; --use Magento\Framework\Cache\FrontendInterface; --use Magento\Framework\Serialize\SerializerInterface; --use Magento\Store\Model\Config\Processor\Fallback; --use Magento\Config\App\Config\Type\System\Reader; -- --/** -- * Test how Class process source, cache them and retrieve value by path -- * @package Magento\Config\Test\Unit\App\Config\Type -- */ --class SystemTest extends \PHPUnit\Framework\TestCase --{ -- /** -- * @var ConfigSourceInterface|\PHPUnit_Framework_MockObject_MockObject -- */ -- private $source; -- -- /** -- * @var PostProcessorInterface|\PHPUnit_Framework_MockObject_MockObject -- */ -- private $postProcessor; -- -- /** -- * @var PreProcessorInterface|\PHPUnit_Framework_MockObject_MockObject -- */ -- private $preProcessor; -- -- /** -- * @var Fallback|\PHPUnit_Framework_MockObject_MockObject -- */ -- private $fallback; -- -- /** -- * @var FrontendInterface|\PHPUnit_Framework_MockObject_MockObject -- */ -- private $cache; -- -- /** -- * @var System -- */ -- private $configType; -- -- /** -- * @var SerializerInterface|\PHPUnit_Framework_MockObject_MockObject -- */ -- private $serializer; -- -- /** -- * @var Reader|\PHPUnit_Framework_MockObject_MockObject -- */ -- private $reader; -- -- public function setUp() -- { -- $this->source = $this->getMockBuilder(ConfigSourceInterface::class) -- ->getMockForAbstractClass(); -- $this->postProcessor = $this->getMockBuilder(PostProcessorInterface::class) -- ->getMockForAbstractClass(); -- $this->fallback = $this->getMockBuilder(Fallback::class) -- ->disableOriginalConstructor() -- ->getMock(); -- $this->cache = $this->getMockBuilder(FrontendInterface::class) -- ->getMockForAbstractClass(); -- $this->preProcessor = $this->getMockBuilder(PreProcessorInterface::class) -- ->getMockForAbstractClass(); -- $this->serializer = $this->getMockBuilder(SerializerInterface::class) -- ->getMock(); -- $this->reader = $this->getMockBuilder(Reader::class) -- ->disableOriginalConstructor() -- ->getMock(); -- -- $this->configType = new System( -- $this->source, -- $this->postProcessor, -- $this->fallback, -- $this->cache, -- $this->serializer, -- $this->preProcessor, -- 1, -- 'system', -- $this->reader -- ); -- } -- -- public function testGetCachedWithLoadDefaultScopeData() -- { -- $path = 'default/dev/unsecure/url'; -- $url = 'http://magento.test/'; -- $data = [ -- 'dev' => [ -- 'unsecure' => [ -- 'url' => $url -- ] -- ] -- ]; -- -- $this->cache->expects($this->any()) -- ->method('load') -- ->willReturnOnConsecutiveCalls('1', serialize($data)); -- $this->serializer->expects($this->once()) -- ->method('unserialize') -- ->willReturn($data); -- $this->assertEquals($url, $this->configType->get($path)); -- } -- -- public function testGetCachedWithLoadAllData() -- { -- $url = 'http://magento.test/'; -- $data = [ -- 'dev' => [ -- 'unsecure' => [ -- 'url' => $url -- ] -- ] -- ]; -- -- $this->cache->expects($this->any()) -- ->method('load') -- ->willReturnOnConsecutiveCalls('1', serialize($data)); -- $this->serializer->expects($this->once()) -- ->method('unserialize') -- ->willReturn($data); -- $this->assertEquals($data, $this->configType->get('')); -- } -- -- public function testGetNotCached() -- { -- $path = 'stores/default/dev/unsecure/url'; -- $url = 'http://magento.test/'; -- -- $dataToCache = [ -- 'unsecure' => [ -- 'url' => $url -- ] -- ]; -- $data = [ -- 'default' => [], -- 'websites' => [], -- 'stores' => [ -- 'default' => [ -- 'dev' => [ -- 'unsecure' => [ -- 'url' => $url -- ] -- ] -- ] -- ] -- ]; -- $this->cache->expects($this->any()) -- ->method('load') -- ->willReturnOnConsecutiveCalls(false, false); -- -- $this->serializer->expects($this->atLeastOnce()) -- ->method('serialize') -- ->willReturn(serialize($dataToCache)); -- $this->cache->expects($this->atLeastOnce()) -- ->method('save') -- ->willReturnSelf(); -- $this->reader->expects($this->once()) -- ->method('read') -- ->willReturn($data); -- $this->postProcessor->expects($this->once()) -- ->method('process') -- ->with($data) -- ->willReturn($data); -- -- $this->assertEquals($url, $this->configType->get($path)); -- $this->assertEquals($url, $this->configType->get($path)); -- } --} -diff --git a/app/code/Magento/Config/Test/Unit/Block/System/Config/Form/Field/FileTest.php b/app/code/Magento/Config/Test/Unit/Block/System/Config/Form/Field/FileTest.php -index de18d45d268..31215f1bdee 100644 ---- a/app/code/Magento/Config/Test/Unit/Block/System/Config/Form/Field/FileTest.php -+++ b/app/code/Magento/Config/Test/Unit/Block/System/Config/Form/Field/FileTest.php -@@ -40,7 +40,11 @@ class FileTest extends \PHPUnit\Framework\TestCase - - $this->file = $objectManager->getObject( - \Magento\Config\Block\System\Config\Form\Field\File::class, -- ['data' => $this->testData] -+ [ -+ '_escaper' => $objectManager->getObject(\Magento\Framework\Escaper::class), -+ 'data' => $this->testData, -+ -+ ] - ); - - $formMock = new \Magento\Framework\DataObject(); -diff --git a/app/code/Magento/Config/Test/Unit/Block/System/Config/Form/Field/ImageTest.php b/app/code/Magento/Config/Test/Unit/Block/System/Config/Form/Field/ImageTest.php -index 8a005a52ab6..b752f79f734 100644 ---- a/app/code/Magento/Config/Test/Unit/Block/System/Config/Form/Field/ImageTest.php -+++ b/app/code/Magento/Config/Test/Unit/Block/System/Config/Form/Field/ImageTest.php -@@ -34,6 +34,7 @@ class ImageTest extends \PHPUnit\Framework\TestCase - \Magento\Config\Block\System\Config\Form\Field\Image::class, - [ - 'urlBuilder' => $this->urlBuilderMock, -+ '_escaper' => $objectManager->getObject(\Magento\Framework\Escaper::class) - ] - ); - -diff --git a/app/code/Magento/Config/Test/Unit/Block/System/Config/Form/Field/Select/AllowspecificTest.php b/app/code/Magento/Config/Test/Unit/Block/System/Config/Form/Field/Select/AllowspecificTest.php -index f5c65e848b3..e7ba2e8aaa2 100644 ---- a/app/code/Magento/Config/Test/Unit/Block/System/Config/Form/Field/Select/AllowspecificTest.php -+++ b/app/code/Magento/Config/Test/Unit/Block/System/Config/Form/Field/Select/AllowspecificTest.php -@@ -21,7 +21,10 @@ class AllowspecificTest extends \PHPUnit\Framework\TestCase - { - $testHelper = new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this); - $this->_object = $testHelper->getObject( -- \Magento\Config\Block\System\Config\Form\Field\Select\Allowspecific::class -+ \Magento\Config\Block\System\Config\Form\Field\Select\Allowspecific::class, -+ [ -+ '_escaper' => $testHelper->getObject(\Magento\Framework\Escaper::class) -+ ] - ); - $this->_object->setData('html_id', 'spec_element'); - $this->_formMock = $this->createPartialMock( -diff --git a/app/code/Magento/Config/Test/Unit/Block/System/Config/FormTest.php b/app/code/Magento/Config/Test/Unit/Block/System/Config/FormTest.php -index 93650dd6265..4e260b0fb2b 100644 ---- a/app/code/Magento/Config/Test/Unit/Block/System/Config/FormTest.php -+++ b/app/code/Magento/Config/Test/Unit/Block/System/Config/FormTest.php -@@ -102,6 +102,9 @@ class FormTest extends \PHPUnit\Framework\TestCase - \Magento\Config\Block\System\Config\Form\Fieldset\Factory::class - ); - $this->_fieldFactoryMock = $this->createMock(\Magento\Config\Block\System\Config\Form\Field\Factory::class); -+ $settingCheckerMock = $this->getMockBuilder(SettingChecker::class) -+ ->disableOriginalConstructor() -+ ->getMock(); - $this->_coreConfigMock = $this->createMock(\Magento\Framework\App\Config\ScopeConfigInterface::class); - - $this->_backendConfigMock = $this->createMock(\Magento\Config\Model\Config::class); -@@ -153,6 +156,7 @@ class FormTest extends \PHPUnit\Framework\TestCase - 'fieldsetFactory' => $this->_fieldsetFactoryMock, - 'fieldFactory' => $this->_fieldFactoryMock, - 'context' => $context, -+ 'settingChecker' => $settingCheckerMock, - ]; - - $objectArguments = $helper->getConstructArguments(\Magento\Config\Block\System\Config\Form::class, $data); -@@ -532,7 +536,7 @@ class FormTest extends \PHPUnit\Framework\TestCase - - $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 984e0fe8426..35b2406b328 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 @@ namespace Magento\Config\Test\Unit\Console\Command\ConfigSet; - - 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 @@ class DefaultProcessorTest extends \PHPUnit\Framework\TestCase - ->disableOriginalConstructor() - ->setMethods(['getResource']) - ->getMock(); -- $this->preparedValueFactoryMock = $this->getMockBuilder(PreparedValueFactory::class) -- ->disableOriginalConstructor() -- ->getMock(); -+ $this->preparedValueFactoryMock = $this->createMock(PreparedValueFactory::class); -+ $this->configFactory = $this->createMock(ConfigFactory::class); - - $this->model = new DefaultProcessor( - $this->preparedValueFactoryMock, - $this->deploymentConfigMock, -- $this->configPathResolverMock -+ $this->configPathResolverMock, -+ $this->configFactory - ); - } - -@@ -98,15 +100,16 @@ class DefaultProcessorTest extends \PHPUnit\Framework\TestCase - { - $this->configMockForProcessTest($path, $scope, $scopeCode); - -- $this->preparedValueFactoryMock->expects($this->once()) -+ $config = $this->createMock(Config::class); -+ $this->configFactory->expects($this->once()) - ->method('create') -- ->willReturn($this->valueMock); -- $this->valueMock->expects($this->once()) -- ->method('getResource') -- ->willReturn($this->resourceModelMock); -- $this->resourceModelMock->expects($this->once()) -+ ->with(['data' => ['scope' => $scope, 'scope_code' => $scopeCode]]) -+ ->willReturn($config); -+ $config->expects($this->once()) -+ ->method('setDataByPath') -+ ->with($path, $value); -+ $config->expects($this->once()) - ->method('save') -- ->with($this->valueMock) - ->willReturnSelf(); - - $this->model->process($path, $value, $scope, $scopeCode); -@@ -124,28 +127,6 @@ class DefaultProcessorTest extends \PHPUnit\Framework\TestCase - ]; - } - -- public function testProcessWithWrongValueInstance() -- { -- $path = 'test/test/test'; -- $scope = ScopeConfigInterface::SCOPE_TYPE_DEFAULT; -- $scopeCode = null; -- $value = 'value'; -- $valueInterfaceMock = $this->getMockBuilder(ValueInterface::class) -- ->getMockForAbstractClass(); -- -- $this->configMockForProcessTest($path, $scope, $scopeCode); -- -- $this->preparedValueFactoryMock->expects($this->once()) -- ->method('create') -- ->willReturn($valueInterfaceMock); -- $this->valueMock->expects($this->never()) -- ->method('getResource'); -- $this->resourceModelMock->expects($this->never()) -- ->method('save'); -- -- $this->model->process($path, $value, $scope, $scopeCode); -- } -- - /** - * @param string $path - * @param string $scope -@@ -185,6 +166,9 @@ class DefaultProcessorTest extends \PHPUnit\Framework\TestCase - ->method('resolve') - ->willReturn('system/default/test/test/test'); - -+ $this->configFactory->expects($this->never()) -+ ->method('create'); -+ - $this->model->process($path, $value, ScopeConfigInterface::SCOPE_TYPE_DEFAULT, null); - } - } -diff --git a/app/code/Magento/Config/Test/Unit/Model/Config/Backend/SerializedTest.php b/app/code/Magento/Config/Test/Unit/Model/Config/Backend/SerializedTest.php -index bb1e0e02259..c2685e0a265 100644 ---- a/app/code/Magento/Config/Test/Unit/Model/Config/Backend/SerializedTest.php -+++ b/app/code/Magento/Config/Test/Unit/Model/Config/Backend/SerializedTest.php -@@ -9,7 +9,11 @@ use Magento\Config\Model\Config\Backend\Serialized; - use Magento\Framework\Model\Context; - use Magento\Framework\Serialize\Serializer\Json; - use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; -+use Psr\Log\LoggerInterface; - -+/** -+ * Class SerializedTest -+ */ - class SerializedTest extends \PHPUnit\Framework\TestCase - { - /** @var \Magento\Config\Model\Config\Backend\Serialized */ -@@ -18,14 +22,20 @@ class SerializedTest extends \PHPUnit\Framework\TestCase - /** @var Json|\PHPUnit_Framework_MockObject_MockObject */ - private $serializerMock; - -+ /** @var LoggerInterface|\PHPUnit_Framework_MockObject_MockObject */ -+ private $loggerMock; -+ - protected function setUp() - { - $objectManager = new ObjectManager($this); - $this->serializerMock = $this->createMock(Json::class); -+ $this->loggerMock = $this->createMock(LoggerInterface::class); - $contextMock = $this->createMock(Context::class); - $eventManagerMock = $this->createMock(\Magento\Framework\Event\ManagerInterface::class); - $contextMock->method('getEventDispatcher') - ->willReturn($eventManagerMock); -+ $contextMock->method('getLogger') -+ ->willReturn($this->loggerMock); - $this->serializedConfig = $objectManager->getObject( - Serialized::class, - [ -@@ -72,6 +82,20 @@ class SerializedTest extends \PHPUnit\Framework\TestCase - ]; - } - -+ public function testAfterLoadWithException() -+ { -+ $value = '{"key":'; -+ $expected = false; -+ $this->serializedConfig->setValue($value); -+ $this->serializerMock->expects($this->once()) -+ ->method('unserialize') -+ ->willThrowException(new \Exception()); -+ $this->loggerMock->expects($this->once()) -+ ->method('critical'); -+ $this->serializedConfig->afterLoad(); -+ $this->assertEquals($expected, $this->serializedConfig->getValue()); -+ } -+ - /** - * @param string $expected - * @param int|double|string|array|boolean|null $value -diff --git a/app/code/Magento/Config/Test/Unit/Model/Config/Structure/Element/AbstractCompositeTest.php b/app/code/Magento/Config/Test/Unit/Model/Config/Structure/Element/AbstractCompositeTest.php -index 57d6fa28a78..e448b628ef0 100644 ---- a/app/code/Magento/Config/Test/Unit/Model/Config/Structure/Element/AbstractCompositeTest.php -+++ b/app/code/Magento/Config/Test/Unit/Model/Config/Structure/Element/AbstractCompositeTest.php -@@ -8,6 +8,9 @@ namespace Magento\Config\Test\Unit\Model\Config\Structure\Element; - use Magento\Config\Model\Config\Structure\ElementVisibilityInterface; - use Magento\Framework\TestFramework\Unit\Helper\ObjectManager as ObjectManagerHelper; - -+/** -+ * Abstract composite test. -+ */ - class AbstractCompositeTest extends \PHPUnit\Framework\TestCase - { - /** -@@ -26,7 +29,7 @@ class AbstractCompositeTest extends \PHPUnit\Framework\TestCase - protected $_iteratorMock; - - /** -- * @var \Magento\Framework\Module\Manager | \PHPUnit_Framework_MockObject_MockObject -+ * @var \Magento\Framework\Module\ModuleManagerInterface | \PHPUnit_Framework_MockObject_MockObject - */ - protected $moduleManagerMock; - -@@ -53,7 +56,7 @@ class AbstractCompositeTest extends \PHPUnit\Framework\TestCase - ->getMockForAbstractClass(); - $this->_iteratorMock = $this->createMock(\Magento\Config\Model\Config\Structure\Element\Iterator::class); - $this->_storeManagerMock = $this->createMock(\Magento\Store\Model\StoreManager::class); -- $this->moduleManagerMock = $this->createMock(\Magento\Framework\Module\Manager::class); -+ $this->moduleManagerMock = $this->createMock(\Magento\Framework\Module\ModuleManagerInterface::class); - $this->_model = $this->getMockForAbstractClass( - \Magento\Config\Model\Config\Structure\Element\AbstractComposite::class, - [$this->_storeManagerMock, $this->moduleManagerMock, $this->_iteratorMock] -diff --git a/app/code/Magento/Config/Test/Unit/Model/Config/StructureTest.php b/app/code/Magento/Config/Test/Unit/Model/Config/StructureTest.php -index 6c059f4b69b..a17faf8f358 100644 ---- a/app/code/Magento/Config/Test/Unit/Model/Config/StructureTest.php -+++ b/app/code/Magento/Config/Test/Unit/Model/Config/StructureTest.php -@@ -418,6 +418,7 @@ class StructureTest extends \PHPUnit\Framework\TestCase - 'field_2' - ], - 'field_3' => [ -+ 'field_3', - 'field_3' - ], - 'field_3_1' => [ -diff --git a/app/code/Magento/Config/Test/Unit/Model/ConfigTest.php b/app/code/Magento/Config/Test/Unit/Model/ConfigTest.php -index d0568f48ded..66163e354cc 100644 ---- a/app/code/Magento/Config/Test/Unit/Model/ConfigTest.php -+++ b/app/code/Magento/Config/Test/Unit/Model/ConfigTest.php -@@ -5,6 +5,8 @@ - */ - namespace Magento\Config\Test\Unit\Model; - -+use PHPUnit\Framework\MockObject\MockObject; -+ - /** - * @SuppressWarnings(PHPMD.CouplingBetweenObjects) - */ -@@ -13,130 +15,158 @@ class ConfigTest extends \PHPUnit\Framework\TestCase - /** - * @var \Magento\Config\Model\Config - */ -- protected $_model; -+ private $model; -+ -+ /** -+ * @var \Magento\Framework\Event\ManagerInterface|MockObject -+ */ -+ private $eventManagerMock; -+ -+ /** -+ * @var \Magento\Config\Model\Config\Structure\Reader|MockObject -+ */ -+ private $structureReaderMock; -+ -+ /** -+ * @var \Magento\Framework\DB\TransactionFactory|MockObject -+ */ -+ private $transFactoryMock; - - /** -- * @var \PHPUnit_Framework_MockObject_MockObject -+ * @var \Magento\Framework\App\Config\ReinitableConfigInterface|MockObject - */ -- protected $_eventManagerMock; -+ private $appConfigMock; - - /** -- * @var \PHPUnit_Framework_MockObject_MockObject -+ * @var \Magento\Config\Model\Config\Loader|MockObject - */ -- protected $_structureReaderMock; -+ private $configLoaderMock; - - /** -- * @var \PHPUnit_Framework_MockObject_MockObject -+ * @var \Magento\Framework\App\Config\ValueFactory|MockObject - */ -- protected $_transFactoryMock; -+ private $dataFactoryMock; - - /** -- * @var \Magento\Framework\App\Config\ReinitableConfigInterface|\PHPUnit_Framework_MockObject_MockObject -+ * @var \Magento\Store\Model\StoreManagerInterface|MockObject - */ -- protected $_appConfigMock; -+ private $storeManager; - - /** -- * @var \PHPUnit_Framework_MockObject_MockObject -+ * @var \Magento\Config\Model\Config\Structure|MockObject - */ -- protected $_applicationMock; -+ private $configStructure; - - /** -- * @var \PHPUnit_Framework_MockObject_MockObject -+ * @var \Magento\Config\Model\Config\Reader\Source\Deployed\SettingChecker|MockObject - */ -- protected $_configLoaderMock; -+ private $settingsChecker; - - /** -- * @var \PHPUnit_Framework_MockObject_MockObject -+ * @var \Magento\Framework\App\ScopeResolverPool|MockObject - */ -- protected $_dataFactoryMock; -+ private $scopeResolverPool; - - /** -- * @var \Magento\Store\Model\StoreManagerInterface -+ * @var \Magento\Framework\App\ScopeResolverInterface|MockObject - */ -- protected $_storeManager; -+ private $scopeResolver; - - /** -- * @var \Magento\Config\Model\Config\Structure -+ * @var \Magento\Framework\App\ScopeInterface|MockObject - */ -- protected $_configStructure; -+ private $scope; - - /** -- * @var \PHPUnit_Framework_MockObject_MockObject -+ * @var \Magento\Store\Model\ScopeTypeNormalizer|MockObject - */ -- private $_settingsChecker; -+ private $scopeTypeNormalizer; - - protected function setUp() - { -- $this->_eventManagerMock = $this->createMock(\Magento\Framework\Event\ManagerInterface::class); -- $this->_structureReaderMock = $this->createPartialMock( -+ $this->eventManagerMock = $this->createMock(\Magento\Framework\Event\ManagerInterface::class); -+ $this->structureReaderMock = $this->createPartialMock( - \Magento\Config\Model\Config\Structure\Reader::class, - ['getConfiguration'] - ); -- $this->_configStructure = $this->createMock(\Magento\Config\Model\Config\Structure::class); -+ $this->configStructure = $this->createMock(\Magento\Config\Model\Config\Structure::class); - -- $this->_structureReaderMock->expects( -+ $this->structureReaderMock->expects( - $this->any() - )->method( - 'getConfiguration' - )->will( -- $this->returnValue($this->_configStructure) -+ $this->returnValue($this->configStructure) - ); - -- $this->_transFactoryMock = $this->createPartialMock( -+ $this->transFactoryMock = $this->createPartialMock( - \Magento\Framework\DB\TransactionFactory::class, - ['create', 'addObject'] - ); -- $this->_appConfigMock = $this->createMock(\Magento\Framework\App\Config\ReinitableConfigInterface::class); -- $this->_configLoaderMock = $this->createPartialMock( -+ $this->appConfigMock = $this->createMock(\Magento\Framework\App\Config\ReinitableConfigInterface::class); -+ $this->configLoaderMock = $this->createPartialMock( - \Magento\Config\Model\Config\Loader::class, - ['getConfigByPath'] - ); -- $this->_dataFactoryMock = $this->createMock(\Magento\Framework\App\Config\ValueFactory::class); -+ $this->dataFactoryMock = $this->createMock(\Magento\Framework\App\Config\ValueFactory::class); - -- $this->_storeManager = $this->getMockForAbstractClass(\Magento\Store\Model\StoreManagerInterface::class); -+ $this->storeManager = $this->createMock(\Magento\Store\Model\StoreManagerInterface::class); - -- $this->_settingsChecker = $this -+ $this->settingsChecker = $this - ->createMock(\Magento\Config\Model\Config\Reader\Source\Deployed\SettingChecker::class); - -- $this->_model = new \Magento\Config\Model\Config( -- $this->_appConfigMock, -- $this->_eventManagerMock, -- $this->_configStructure, -- $this->_transFactoryMock, -- $this->_configLoaderMock, -- $this->_dataFactoryMock, -- $this->_storeManager, -- $this->_settingsChecker -+ $this->scopeResolverPool = $this->createMock(\Magento\Framework\App\ScopeResolverPool::class); -+ $this->scopeResolver = $this->createMock(\Magento\Framework\App\ScopeResolverInterface::class); -+ $this->scopeResolverPool->method('get') -+ ->willReturn($this->scopeResolver); -+ $this->scope = $this->createMock(\Magento\Framework\App\ScopeInterface::class); -+ $this->scopeResolver->method('getScope') -+ ->willReturn($this->scope); -+ -+ $this->scopeTypeNormalizer = $this->createMock(\Magento\Store\Model\ScopeTypeNormalizer::class); -+ -+ $this->model = new \Magento\Config\Model\Config( -+ $this->appConfigMock, -+ $this->eventManagerMock, -+ $this->configStructure, -+ $this->transFactoryMock, -+ $this->configLoaderMock, -+ $this->dataFactoryMock, -+ $this->storeManager, -+ $this->settingsChecker, -+ [], -+ $this->scopeResolverPool, -+ $this->scopeTypeNormalizer - ); - } - - public function testSaveDoesNotDoAnythingIfGroupsAreNotPassed() - { -- $this->_configLoaderMock->expects($this->never())->method('getConfigByPath'); -- $this->_model->save(); -+ $this->configLoaderMock->expects($this->never())->method('getConfigByPath'); -+ $this->model->save(); - } - - public function testSaveEmptiesNonSetArguments() - { -- $this->_structureReaderMock->expects($this->never())->method('getConfiguration'); -- $this->assertNull($this->_model->getSection()); -- $this->assertNull($this->_model->getWebsite()); -- $this->assertNull($this->_model->getStore()); -- $this->_model->save(); -- $this->assertSame('', $this->_model->getSection()); -- $this->assertSame('', $this->_model->getWebsite()); -- $this->assertSame('', $this->_model->getStore()); -+ $this->structureReaderMock->expects($this->never())->method('getConfiguration'); -+ $this->assertNull($this->model->getSection()); -+ $this->assertNull($this->model->getWebsite()); -+ $this->assertNull($this->model->getStore()); -+ $this->model->save(); -+ $this->assertSame('', $this->model->getSection()); -+ $this->assertSame('', $this->model->getWebsite()); -+ $this->assertSame('', $this->model->getStore()); - } - - public function testSaveToCheckAdminSystemConfigChangedSectionEvent() - { - $transactionMock = $this->createMock(\Magento\Framework\DB\Transaction::class); - -- $this->_transFactoryMock->expects($this->any())->method('create')->will($this->returnValue($transactionMock)); -+ $this->transFactoryMock->expects($this->any())->method('create')->will($this->returnValue($transactionMock)); - -- $this->_configLoaderMock->expects($this->any())->method('getConfigByPath')->will($this->returnValue([])); -+ $this->configLoaderMock->expects($this->any())->method('getConfigByPath')->will($this->returnValue([])); - -- $this->_eventManagerMock->expects( -+ $this->eventManagerMock->expects( - $this->at(0) - )->method( - 'dispatch' -@@ -145,7 +175,7 @@ class ConfigTest extends \PHPUnit\Framework\TestCase - $this->arrayHasKey('website') - ); - -- $this->_eventManagerMock->expects( -+ $this->eventManagerMock->expects( - $this->at(0) - )->method( - 'dispatch' -@@ -154,20 +184,20 @@ class ConfigTest extends \PHPUnit\Framework\TestCase - $this->arrayHasKey('store') - ); - -- $this->_model->setGroups(['1' => ['data']]); -- $this->_model->save(); -+ $this->model->setGroups(['1' => ['data']]); -+ $this->model->save(); - } - - public function testDoNotSaveReadOnlyFields() - { - $transactionMock = $this->createMock(\Magento\Framework\DB\Transaction::class); -- $this->_transFactoryMock->expects($this->any())->method('create')->will($this->returnValue($transactionMock)); -+ $this->transFactoryMock->expects($this->any())->method('create')->will($this->returnValue($transactionMock)); - -- $this->_settingsChecker->expects($this->any())->method('isReadOnly')->will($this->returnValue(true)); -- $this->_configLoaderMock->expects($this->any())->method('getConfigByPath')->will($this->returnValue([])); -+ $this->settingsChecker->expects($this->any())->method('isReadOnly')->will($this->returnValue(true)); -+ $this->configLoaderMock->expects($this->any())->method('getConfigByPath')->will($this->returnValue([])); - -- $this->_model->setGroups(['1' => ['fields' => ['key' => ['data']]]]); -- $this->_model->setSection('section'); -+ $this->model->setGroups(['1' => ['fields' => ['key' => ['data']]]]); -+ $this->model->setSection('section'); - - $group = $this->createMock(\Magento\Config\Model\Config\Structure\Element\Group::class); - $group->method('getPath')->willReturn('section/1'); -@@ -176,15 +206,15 @@ class ConfigTest extends \PHPUnit\Framework\TestCase - $field->method('getGroupPath')->willReturn('section/1'); - $field->method('getId')->willReturn('key'); - -- $this->_configStructure->expects($this->at(0)) -+ $this->configStructure->expects($this->at(0)) - ->method('getElement') - ->with('section/1') - ->will($this->returnValue($group)); -- $this->_configStructure->expects($this->at(1)) -+ $this->configStructure->expects($this->at(1)) - ->method('getElement') - ->with('section/1') - ->will($this->returnValue($group)); -- $this->_configStructure->expects($this->at(2)) -+ $this->configStructure->expects($this->at(2)) - ->method('getElement') - ->with('section/1/key') - ->will($this->returnValue($field)); -@@ -193,28 +223,28 @@ class ConfigTest extends \PHPUnit\Framework\TestCase - \Magento\Framework\App\Config\Value::class, - ['addData'] - ); -- $this->_dataFactoryMock->expects($this->any())->method('create')->will($this->returnValue($backendModel)); -+ $this->dataFactoryMock->expects($this->any())->method('create')->will($this->returnValue($backendModel)); - -- $this->_transFactoryMock->expects($this->never())->method('addObject'); -+ $this->transFactoryMock->expects($this->never())->method('addObject'); - $backendModel->expects($this->never())->method('addData'); - -- $this->_model->save(); -+ $this->model->save(); - } - - public function testSaveToCheckScopeDataSet() - { - $transactionMock = $this->createMock(\Magento\Framework\DB\Transaction::class); -- $this->_transFactoryMock->expects($this->any())->method('create')->will($this->returnValue($transactionMock)); -+ $this->transFactoryMock->expects($this->any())->method('create')->will($this->returnValue($transactionMock)); - -- $this->_configLoaderMock->expects($this->any())->method('getConfigByPath')->will($this->returnValue([])); -+ $this->configLoaderMock->expects($this->any())->method('getConfigByPath')->will($this->returnValue([])); - -- $this->_eventManagerMock->expects($this->at(0)) -+ $this->eventManagerMock->expects($this->at(0)) - ->method('dispatch') - ->with( - $this->equalTo('admin_system_config_changed_section_section'), - $this->arrayHasKey('website') - ); -- $this->_eventManagerMock->expects($this->at(0)) -+ $this->eventManagerMock->expects($this->at(0)) - ->method('dispatch') - ->with( - $this->equalTo('admin_system_config_changed_section_section'), -@@ -228,36 +258,51 @@ class ConfigTest extends \PHPUnit\Framework\TestCase - $field->method('getGroupPath')->willReturn('section/1'); - $field->method('getId')->willReturn('key'); - -- $this->_configStructure->expects($this->at(0)) -+ $this->configStructure->expects($this->at(0)) - ->method('getElement') - ->with('section/1') - ->will($this->returnValue($group)); -- $this->_configStructure->expects($this->at(1)) -+ $this->configStructure->expects($this->at(1)) - ->method('getElement') - ->with('section/1') - ->will($this->returnValue($group)); -- $this->_configStructure->expects($this->at(2)) -+ $this->configStructure->expects($this->at(2)) - ->method('getElement') - ->with('section/1/key') - ->will($this->returnValue($field)); -- $this->_configStructure->expects($this->at(3)) -+ $this->configStructure->expects($this->at(3)) - ->method('getElement') - ->with('section/1') - ->will($this->returnValue($group)); -- $this->_configStructure->expects($this->at(4)) -+ $this->configStructure->expects($this->at(4)) - ->method('getElement') - ->with('section/1/key') - ->will($this->returnValue($field)); - -+ $this->scopeResolver->expects($this->atLeastOnce()) -+ ->method('getScope') -+ ->with('1') -+ ->willReturn($this->scope); -+ $this->scope->expects($this->atLeastOnce()) -+ ->method('getScopeType') -+ ->willReturn('website'); -+ $this->scope->expects($this->atLeastOnce()) -+ ->method('getId') -+ ->willReturn(1); -+ $this->scope->expects($this->atLeastOnce()) -+ ->method('getCode') -+ ->willReturn('website_code'); -+ $this->scopeTypeNormalizer->expects($this->atLeastOnce()) -+ ->method('normalize') -+ ->with('website') -+ ->willReturn('websites'); - $website = $this->createMock(\Magento\Store\Model\Website::class); -- $website->expects($this->any())->method('getCode')->will($this->returnValue('website_code')); -- $this->_storeManager->expects($this->any())->method('getWebsite')->will($this->returnValue($website)); -- $this->_storeManager->expects($this->any())->method('getWebsites')->will($this->returnValue([$website])); -- $this->_storeManager->expects($this->any())->method('isSingleStoreMode')->will($this->returnValue(true)); -+ $this->storeManager->expects($this->any())->method('getWebsites')->will($this->returnValue([$website])); -+ $this->storeManager->expects($this->any())->method('isSingleStoreMode')->will($this->returnValue(true)); - -- $this->_model->setWebsite('website'); -- $this->_model->setSection('section'); -- $this->_model->setGroups(['1' => ['fields' => ['key' => ['data']]]]); -+ $this->model->setWebsite('1'); -+ $this->model->setSection('section'); -+ $this->model->setGroups(['1' => ['fields' => ['key' => ['data']]]]); - - $backendModel = $this->createPartialMock( - \Magento\Framework\App\Config\Value::class, -@@ -270,7 +315,7 @@ class ConfigTest extends \PHPUnit\Framework\TestCase - 'groups' => [1 => ['fields' => ['key' => ['data']]]], - 'group_id' => null, - 'scope' => 'websites', -- 'scope_id' => 0, -+ 'scope_id' => 1, - 'scope_code' => 'website_code', - 'field_config' => null, - 'fieldset_data' => ['key' => null], -@@ -280,27 +325,64 @@ class ConfigTest extends \PHPUnit\Framework\TestCase - ->with('section/1/key') - ->will($this->returnValue($backendModel)); - -- $this->_dataFactoryMock->expects($this->any())->method('create')->will($this->returnValue($backendModel)); -+ $this->dataFactoryMock->expects($this->any())->method('create')->will($this->returnValue($backendModel)); -+ -+ $this->model->save(); -+ } - -- $this->_model->save(); -+ /** -+ * @param string $path -+ * @param string $value -+ * @param string $section -+ * @param array $groups -+ * @dataProvider setDataByPathDataProvider -+ */ -+ public function testSetDataByPath(string $path, string $value, string $section, array $groups) -+ { -+ $this->model->setDataByPath($path, $value); -+ $this->assertEquals($section, $this->model->getData('section')); -+ $this->assertEquals($groups, $this->model->getData('groups')); - } - -- public function testSetDataByPath() -+ /** -+ * @return array -+ */ -+ public function setDataByPathDataProvider(): array - { -- $value = 'value'; -- $path = '<section>/<group>/<field>'; -- $this->_model->setDataByPath($path, $value); -- $expected = [ -- 'section' => '<section>', -- 'groups' => [ -- '<group>' => [ -- 'fields' => [ -- '<field>' => ['value' => $value], -+ return [ -+ 'depth 3' => [ -+ 'a/b/c', -+ 'value1', -+ 'a', -+ [ -+ 'b' => [ -+ 'fields' => [ -+ 'c' => ['value' => 'value1'], -+ ], -+ ], -+ ], -+ ], -+ 'depth 5' => [ -+ 'a/b/c/d/e', -+ 'value1', -+ 'a', -+ [ -+ 'b' => [ -+ 'groups' => [ -+ 'c' => [ -+ 'groups' => [ -+ 'd' => [ -+ 'fields' => [ -+ 'e' => ['value' => 'value1'], -+ ], -+ ], -+ ], -+ ], -+ ], - ], - ], - ], - ]; -- $this->assertSame($expected, $this->_model->getData()); - } - - /** -@@ -309,34 +391,31 @@ class ConfigTest extends \PHPUnit\Framework\TestCase - */ - public function testSetDataByPathEmpty() - { -- $this->_model->setDataByPath('', 'value'); -+ $this->model->setDataByPath('', 'value'); - } - - /** - * @param string $path -- * @param string $expectedException -- * - * @dataProvider setDataByPathWrongDepthDataProvider - */ -- public function testSetDataByPathWrongDepth($path, $expectedException) -+ public function testSetDataByPathWrongDepth(string $path) - { -- $expectedException = 'Allowed depth of configuration is 3 (<section>/<group>/<field>). ' . $expectedException; -- $this->expectException('\UnexpectedValueException'); -+ $currentDepth = count(explode('/', $path)); -+ $expectedException = 'Minimal depth of configuration is 3. Your configuration depth is ' . $currentDepth; -+ $this->expectException(\UnexpectedValueException::class); - $this->expectExceptionMessage($expectedException); - $value = 'value'; -- $this->_model->setDataByPath($path, $value); -+ $this->model->setDataByPath($path, $value); - } - - /** - * @return array - */ -- public function setDataByPathWrongDepthDataProvider() -+ public function setDataByPathWrongDepthDataProvider(): array - { - return [ -- 'depth 2' => ['section/group', "Your configuration depth is 2 for path 'section/group'"], -- 'depth 1' => ['section', "Your configuration depth is 1 for path 'section'"], -- 'depth 4' => ['section/group/field/sub-field', "Your configuration depth is 4 for path" -- . " 'section/group/field/sub-field'", ], -+ 'depth 2' => ['section/group'], -+ 'depth 1' => ['section'], - ]; - } - } -diff --git a/app/code/Magento/Config/etc/adminhtml/di.xml b/app/code/Magento/Config/etc/adminhtml/di.xml -index 5e54f177776..189fbdf69a7 100644 ---- a/app/code/Magento/Config/etc/adminhtml/di.xml -+++ b/app/code/Magento/Config/etc/adminhtml/di.xml -@@ -6,7 +6,6 @@ - */ - --> - <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd"> -- <preference for="Magento\Config\Model\Config\Structure\SearchInterface" type="Magento\Config\Model\Config\Structure" /> - <preference for="Magento\Config\Model\Config\Backend\File\RequestData\RequestDataInterface" type="Magento\Config\Model\Config\Backend\File\RequestData" /> - <preference for="Magento\Config\Model\Config\Structure\ElementVisibilityInterface" type="Magento\Config\Model\Config\Structure\ElementVisibilityComposite" /> - <type name="Magento\Config\Model\Config\Structure\Element\Iterator\Tab" shared="false" /> -diff --git a/app/code/Magento/Config/etc/db_schema.xml b/app/code/Magento/Config/etc/db_schema.xml -index 3f55d582776..8aeac802fbd 100644 ---- a/app/code/Magento/Config/etc/db_schema.xml -+++ b/app/code/Magento/Config/etc/db_schema.xml -@@ -15,10 +15,10 @@ - default="0" comment="Config Scope Id"/> - <column xsi:type="varchar" name="path" nullable="false" length="255" default="general" comment="Config Path"/> - <column xsi:type="text" name="value" nullable="true" comment="Config Value"/> -- <constraint xsi:type="primary" name="PRIMARY"> -+ <constraint xsi:type="primary" referenceId="PRIMARY"> - <column name="config_id"/> - </constraint> -- <constraint xsi:type="unique" name="CORE_CONFIG_DATA_SCOPE_SCOPE_ID_PATH"> -+ <constraint xsi:type="unique" referenceId="CORE_CONFIG_DATA_SCOPE_SCOPE_ID_PATH"> - <column name="scope"/> - <column name="scope_id"/> - <column name="path"/> -diff --git a/app/code/Magento/Config/etc/di.xml b/app/code/Magento/Config/etc/di.xml -index a5dd18097fb..920cac382fc 100644 ---- a/app/code/Magento/Config/etc/di.xml -+++ b/app/code/Magento/Config/etc/di.xml -@@ -77,6 +77,11 @@ - </argument> - </arguments> - </type> -+ <type name="Magento\Framework\Lock\Backend\Cache"> -+ <arguments> -+ <argument name="cache" xsi:type="object">Magento\Framework\App\Cache\Type\Config</argument> -+ </arguments> -+ </type> - <type name="Magento\Config\App\Config\Type\System"> - <arguments> - <argument name="source" xsi:type="object">systemConfigSourceAggregatedProxy</argument> -@@ -85,8 +90,18 @@ - <argument name="preProcessor" xsi:type="object">Magento\Framework\App\Config\PreProcessorComposite</argument> - <argument name="serializer" xsi:type="object">Magento\Framework\Serialize\Serializer\Serialize</argument> - <argument name="reader" xsi:type="object">Magento\Config\App\Config\Type\System\Reader\Proxy</argument> -+ <argument name="lockQuery" xsi:type="object">systemConfigQueryLocker</argument> - </arguments> - </type> -+ -+ <virtualType name="systemConfigQueryLocker" type="Magento\Framework\Cache\LockGuardedCacheLoader"> -+ <arguments> -+ <argument name="locker" xsi:type="object">Magento\Framework\Lock\Backend\Cache</argument> -+ <argument name="lockTimeout" xsi:type="number">42000</argument> -+ <argument name="delayTimeout" xsi:type="number">100</argument> -+ </arguments> -+ </virtualType> -+ - <type name="Magento\Config\App\Config\Type\System\Reader"> - <arguments> - <argument name="source" xsi:type="object">systemConfigSourceAggregated</argument> -diff --git a/app/code/Magento/Config/view/adminhtml/templates/page/system/config/robots/reset.phtml b/app/code/Magento/Config/view/adminhtml/templates/page/system/config/robots/reset.phtml -index e8082dae94d..49a75d36fd8 100644 ---- a/app/code/Magento/Config/view/adminhtml/templates/page/system/config/robots/reset.phtml -+++ b/app/code/Magento/Config/view/adminhtml/templates/page/system/config/robots/reset.phtml -@@ -4,14 +4,12 @@ - * See COPYING.txt for license details. - */ - --// @codingStandardsIgnoreFile -- - /** - * @deprecated - * @var $block \Magento\Backend\Block\Page\System\Config\Robots\Reset - * @var $jsonHelper \Magento\Framework\Json\Helper\Data - */ --$jsonHelper = $this->helper('Magento\Framework\Json\Helper\Data'); -+$jsonHelper = $this->helper(\Magento\Framework\Json\Helper\Data::class); - ?> - - <script> -@@ -19,8 +17,8 @@ $jsonHelper = $this->helper('Magento\Framework\Json\Helper\Data'); - 'jquery' - ], function ($) { - window.resetRobotsToDefault = function(){ -- $('#design_search_engine_robots_custom_instructions').val(<?php -- /* @escapeNotVerified */ echo $jsonHelper->jsonEncode($block->getRobotsDefaultCustomInstructions()) -+ $('#design_search_engine_robots_custom_instructions').val(<?= -+ /* @noEscape */ $jsonHelper->jsonEncode($block->getRobotsDefaultCustomInstructions()) - ?>); - } - }); -diff --git a/app/code/Magento/Config/view/adminhtml/templates/system/config/edit.phtml b/app/code/Magento/Config/view/adminhtml/templates/system/config/edit.phtml -index a1e26b7805d..4b8ebb39beb 100644 ---- a/app/code/Magento/Config/view/adminhtml/templates/system/config/edit.phtml -+++ b/app/code/Magento/Config/view/adminhtml/templates/system/config/edit.phtml -@@ -3,9 +3,6 @@ - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -- --// @codingStandardsIgnoreFile -- - ?> - <?php - /** -@@ -22,7 +19,8 @@ - background-color: #DFF7FF!important; - } - </style> --<form action="<?= /* @escapeNotVerified */ $block->getSaveUrl() ?>" method="post" id="config-edit-form" enctype="multipart/form-data"> -+<form action="<?= $block->escapeUrl($block->getSaveUrl()) ?>" method="post" id="config-edit-form" -+ enctype="multipart/form-data"> - <?= $block->getBlockHtml('formkey') ?> - <div class="accordion"> - <?= $block->getChildHtml('form') ?> -@@ -32,6 +30,8 @@ - require([ - "jquery", - "uiRegistry", -+ "Magento_Ui/js/modal/confirm", -+ "mage/translate", - "mage/mage", - "prototype", - "mage/adminhtml/form", -@@ -159,7 +159,9 @@ require([ - var elId = $el.id; - $el.replace($el.outerHTML); - events.each(function(event) { -- Event.observe(Element.extend(document.getElementById(elId)), event.eventName, event.handler); -+ Event.observe( -+ Element.extend(document.getElementById(elId)), event.eventName, event.handler -+ ); - }); - } else { - el.stopObserving('change', adminSystemConfig.onchangeSharedElement); -@@ -296,7 +298,9 @@ require([ - if (!scopeElement || !scopeElement.checked) { - eventObj.element.enable(); - eventObj.requires.each(function(required) { -- adminSystemConfig.checkRequired.call(Element.extend(required), eventObj.element, eventObj.callback); -+ adminSystemConfig.checkRequired.call( -+ Element.extend(required), eventObj.element, eventObj.callback -+ ); - }.bind(this)); - } - }, -@@ -383,6 +387,6 @@ require([ - - registry.set('adminSystemConfig', adminSystemConfig); - -- adminSystemConfig.navigateToElement(<?php echo /* @noEscape */ $block->getConfigSearchParamsJson(); ?>); -+ adminSystemConfig.navigateToElement(<?= /* @noEscape */ $block->getConfigSearchParamsJson(); ?>); - }); - </script> -diff --git a/app/code/Magento/Config/view/adminhtml/templates/system/config/form/field/array.phtml b/app/code/Magento/Config/view/adminhtml/templates/system/config/form/field/array.phtml -index cf235d368b9..cf188bfeb68 100644 ---- a/app/code/Magento/Config/view/adminhtml/templates/system/config/form/field/array.phtml -+++ b/app/code/Magento/Config/view/adminhtml/templates/system/config/form/field/array.phtml -@@ -3,9 +3,6 @@ - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -- --// @codingStandardsIgnoreFile -- - ?> - - <?php -@@ -13,30 +10,30 @@ $_htmlId = $block->getHtmlId() ? $block->getHtmlId() : '_' . uniqid(); - $_colspan = $block->isAddAfter() ? 2 : 1; - ?> - --<div class="design_theme_ua_regexp" id="grid<?= /* @escapeNotVerified */ $_htmlId ?>"> -+<div class="design_theme_ua_regexp" id="grid<?= $block->escapeHtmlAttr($_htmlId) ?>"> - <div class="admin__control-table-wrapper"> -- <table class="admin__control-table" id="<?= /* @escapeNotVerified */ $block->getElement()->getId() ?>"> -+ <table class="admin__control-table" id="<?= $block->escapeHtmlAttr($block->getElement()->getId()) ?>"> - <thead> - <tr> -- <?php foreach ($block->getColumns() as $columnName => $column): ?> -- <th><?= /* @escapeNotVerified */ $column['label'] ?></th> -- <?php endforeach;?> -- <th class="col-actions" colspan="<?= /* @escapeNotVerified */ $_colspan ?>"><?= /* @escapeNotVerified */ __('Action') ?></th> -+ <?php foreach ($block->getColumns() as $columnName => $column) : ?> -+ <th><?= $block->escapeHtml($column['label']) ?></th> -+ <?php endforeach; ?> -+ <th class="col-actions" colspan="<?= (int)$_colspan ?>"><?= $block->escapeHtml(__('Action')) ?></th> - </tr> - </thead> - <tfoot> - <tr> - <td colspan="<?= count($block->getColumns())+$_colspan ?>" class="col-actions-add"> -- <button id="addToEndBtn<?= /* @escapeNotVerified */ $_htmlId ?>" class="action-add" title="<?= /* @escapeNotVerified */ __('Add') ?>" type="button"> -- <span><?= /* @escapeNotVerified */ $block->getAddButtonLabel() ?></span> -+ <button id="addToEndBtn<?= $block->escapeHtmlAttr($_htmlId) ?>" class="action-add" title="<?= $block->escapeHtmlAttr(__('Add')) ?>" type="button"> -+ <span><?= $block->escapeHtml($block->getAddButtonLabel()) ?></span> - </button> - </td> - </tr> - </tfoot> -- <tbody id="addRow<?= /* @escapeNotVerified */ $_htmlId ?>"></tbody> -+ <tbody id="addRow<?= $block->escapeHtmlAttr($_htmlId) ?>"></tbody> - </table> - </div> -- <input type="hidden" name="<?= /* @escapeNotVerified */ $block->getElement()->getName() ?>[__empty]" value="" /> -+ <input type="hidden" name="<?= $block->escapeHtmlAttr($block->getElement()->getName()) ?>[__empty]" value="" /> - - <script> - require([ -@@ -44,23 +41,28 @@ $_colspan = $block->isAddAfter() ? 2 : 1; - 'prototype' - ], function (mageTemplate) { - // create row creator -- window.arrayRow<?= /* @escapeNotVerified */ $_htmlId ?> = { -+ window.arrayRow<?= $block->escapeJs($_htmlId) ?> = { - - // define row prototypeJS template - template: mageTemplate( - '<tr id="<%- _id %>">' -- <?php foreach ($block->getColumns() as $columnName => $column): ?> -- + '<td>' -- + '<?= /* @escapeNotVerified */ $block->renderCellTemplate($columnName) ?>' -- + '<\/td>' -- <?php endforeach; ?> -- -- <?php if ($block->isAddAfter()): ?> -- + '<td><button class="action-add" type="button" id="addAfterBtn<%- _id %>"><span><?= /* @escapeNotVerified */ __('Add after') ?><\/span><\/button><\/td>' -- <?php endif; ?> -+ <?php foreach ($block->getColumns() as $columnName => $column) : ?> -+ + '<td>' -+ + '<?= $block->escapeJs($block->renderCellTemplate($columnName)) ?>' -+ + '<\/td>' -+ <?php endforeach; ?> - -- + '<td class="col-actions"><button onclick="arrayRow<?= /* @escapeNotVerified */ $_htmlId ?>.del(\'<%- _id %>\')" class="action-delete" type="button"><span><?= /* @escapeNotVerified */ __('Delete') ?><\/span><\/button><\/td>' -- +'<\/tr>' -+ <?php if ($block->isAddAfter()) : ?> -+ + '<td><button class="action-add" type="button" id="addAfterBtn<%- _id %>"><span>' -+ + '<?= $block->escapeJs($block->escapeHtml(__('Add after'))) ?>' -+ + '<\/span><\/button><\/td>' -+ <?php endif; ?> -+ -+ + '<td class="col-actions"><button ' -+ + 'onclick="arrayRow<?= $block->escapeJs($_htmlId) ?>.del(\'<%- _id %>\')" ' -+ + 'class="action-delete" type="button">' -+ + '<span><?= $block->escapeJs($block->escapeHtml(__('Delete'))) ?><\/span><\/button><\/td>' -+ + '<\/tr>' - ), - - add: function(rowData, insertAfterId) { -@@ -73,56 +75,61 @@ $_colspan = $block->isAddAfter() ? 2 : 1; - } else { - var d = new Date(); - templateValues = { -- <?php foreach ($block->getColumns() as $columnName => $column): ?> -- <?= /* @escapeNotVerified */ $columnName ?>: '', -- 'option_extra_attrs': {}, -- <?php endforeach; ?> -+ <?php foreach ($block->getColumns() as $columnName => $column) : ?> -+ <?= $block->escapeJs($columnName) ?>: '', -+ 'option_extra_attrs': {}, -+ <?php endforeach; ?> - _id: '_' + d.getTime() + '_' + d.getMilliseconds() - }; - } - - // Insert new row after specified row or at the bottom - if (insertAfterId) { -- Element.insert($(insertAfterId), {after: this.template(templateValues)}); -- } else { -- Element.insert($('addRow<?= /* @escapeNotVerified */ $_htmlId ?>'), {bottom: this.template(templateValues)}); -- } -+ Element.insert($(insertAfterId), {after: this.template(templateValues)}); -+ } else { -+ Element.insert($('addRow<?= $block->escapeJs($_htmlId) ?>'), {bottom: this.template(templateValues)}); -+ } - -- // Fill controls with data -- if (rowData) { -- var rowInputElementNames = Object.keys(rowData.column_values); -- for (var i = 0; i < rowInputElementNames.length; i++) { -- if ($(rowInputElementNames[i])) { -- $(rowInputElementNames[i]).setValue(rowData.column_values[rowInputElementNames[i]]); -+ // Fill controls with data -+ if (rowData) { -+ var rowInputElementNames = Object.keys(rowData.column_values); -+ for (var i = 0; i < rowInputElementNames.length; i++) { -+ if ($(rowInputElementNames[i])) { -+ $(rowInputElementNames[i]).setValue(rowData.column_values[rowInputElementNames[i]]); -+ } - } - } -- } - -- // Add event for {addAfterBtn} button -- <?php if ($block->isAddAfter()): ?> -- Event.observe('addAfterBtn' + templateValues._id, 'click', this.add.bind(this, false, templateValues._id)); -+ // Add event for {addAfterBtn} button -+ <?php if ($block->isAddAfter()) : ?> -+ Event.observe('addAfterBtn' + templateValues._id, 'click', this.add.bind(this, false, templateValues._id)); - <?php endif; ?> -- }, -+ }, - -- del: function(rowId) { -- $(rowId).remove(); -- } -+ del: function(rowId) { -+ $(rowId).remove(); -+ } - } - - // bind add action to "Add" button in last row -- Event.observe('addToEndBtn<?= /* @escapeNotVerified */ $_htmlId ?>', 'click', arrayRow<?= /* @escapeNotVerified */ $_htmlId ?>.add.bind(arrayRow<?= /* @escapeNotVerified */ $_htmlId ?>, false, false)); -+ Event.observe('addToEndBtn<?= $block->escapeJs($_htmlId) ?>', -+ 'click', -+ arrayRow<?= $block->escapeJs($_htmlId) ?>.add.bind( -+ arrayRow<?= $block->escapeJs($_htmlId) ?>, false, false -+ ) -+ ); - - // add existing rows - <?php - foreach ($block->getArrayRows() as $_rowId => $_row) { -- /* @escapeNotVerified */ echo "arrayRow{$_htmlId}.add(" . $_row->toJson() . ");\n"; -+ echo /** @noEscape */ "arrayRow{$block->escapeJs($_htmlId)}.add(" . /** @noEscape */ $_row->toJson() . ");\n"; - } - ?> - - // Toggle the grid availability, if element is disabled (depending on scope) -- <?php if ($block->getElement()->getDisabled()):?> -- toggleValueElements({checked: true}, $('grid<?= /* @escapeNotVerified */ $_htmlId ?>').parentNode); -- <?php endif;?> -+ <?php if ($block->getElement()->getDisabled()) : ?> -+ toggleValueElements({checked: true}, $('grid<?= $block->escapeJs($_htmlId) ?>').parentNode); -+ <?php endif; ?> - }); - </script> - </div> -diff --git a/app/code/Magento/Config/view/adminhtml/templates/system/config/js.phtml b/app/code/Magento/Config/view/adminhtml/templates/system/config/js.phtml -index b703641acad..29768778683 100644 ---- a/app/code/Magento/Config/view/adminhtml/templates/system/config/js.phtml -+++ b/app/code/Magento/Config/view/adminhtml/templates/system/config/js.phtml -@@ -3,9 +3,6 @@ - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -- --// @codingStandardsIgnoreFile -- - ?> - <script> - require([ -@@ -71,7 +68,7 @@ originModel.prototype = { - { - this.reload = false; - this.loader = new varienLoader(true); -- this.regionsUrl = "<?= /* @escapeNotVerified */ $block->getUrl('directory/json/countryRegion') ?>"; -+ this.regionsUrl = "<?= $block->escapeJs($block->escapeUrl($block->getUrl('directory/json/countryRegion'))) ?>"; - - this.bindCountryRegionRelation(); - }, -@@ -146,21 +143,32 @@ originModel.prototype = { - var value = this.regionElement.value; - var disabled = this.regionElement.disabled; - if (data.length) { -- var html = '<select name="'+this.regionElement.name+'" id="'+this.regionElement.id+'" class="required-entry select" title="'+this.regionElement.title+'"'+(disabled?" disabled":"")+'>'; -+ var select = document.createElement('select'); -+ select.setAttribute('name', this.regionElement.name); -+ select.setAttribute('title', this.regionElement.title); -+ select.setAttribute('id', this.regionElement.id); -+ select.setAttribute('class', 'required-entry select'); -+ if (disabled) { -+ select.setAttribute('disabled', ''); -+ } - for (var i in data) { - if (data[i].label) { -- html+= '<option value="'+data[i].value+'"'; -- if (this.regionElement.value && (this.regionElement.value == data[i].value || this.regionElement.value == data[i].label)) { -- html+= ' selected'; -+ var option = document.createElement('option'); -+ option.setAttribute('value', data[i].value); -+ option.innerText = data[i].label; -+ if (this.regionElement.value && -+ (this.regionElement.value == data[i].value || this.regionElement.value == data[i].label) -+ ) { -+ option.setAttribute('selected', ''); - } -- html+='>'+data[i].label+'<\/option>'; -+ select.add(option); - } - } -- html+= '<\/select>'; - - var parentNode = this.regionElement.parentNode; - var regionElementId = this.regionElement.id; -- parentNode.innerHTML = html; -+ parentNode.innerHTML = select.outerHTML; -+ - this.regionElement = $(regionElementId); - } else if (this.reload) { - this.clearRegionField(disabled); -@@ -168,10 +176,18 @@ originModel.prototype = { - } - }, - clearRegionField: function(disabled) { -- var html = '<input type="text" name="' + this.regionElement.name + '" id="' + this.regionElement.id + '" class="input-text" title="' + this.regionElement.title + '"' + (disabled ? " disabled" : "") + '>'; -+ var text = document.createElement('input'); -+ text.setAttribute('type', 'text'); -+ text.setAttribute('name', this.regionElement.name); -+ text.setAttribute('title', this.regionElement.title); -+ text.setAttribute('id', this.regionElement.id); -+ text.setAttribute('class', 'input-text'); -+ if (disabled) { -+ text.setAttribute('disabled', ''); -+ } - var parentNode = this.regionElement.parentNode; - var regionElementId = this.regionElement.id; -- parentNode.innerHTML = html; -+ parentNode.innerHTML = text.outerHTML; - this.regionElement = $(regionElementId); - } - } -diff --git a/app/code/Magento/Config/view/adminhtml/templates/system/config/switcher.phtml b/app/code/Magento/Config/view/adminhtml/templates/system/config/switcher.phtml -index 6677b800764..0d07051e666 100644 ---- a/app/code/Magento/Config/view/adminhtml/templates/system/config/switcher.phtml -+++ b/app/code/Magento/Config/view/adminhtml/templates/system/config/switcher.phtml -@@ -3,34 +3,38 @@ - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -- --// @codingStandardsIgnoreFile -- - ?> - <?php /* @var $block \Magento\Backend\Block\Template */ ?> - <div class="field field-store-switcher"> -- <label class="label" for="store_switcher"><?= /* @escapeNotVerified */ __('Current Configuration Scope:') ?></label> -+ <label class="label" for="store_switcher"><?= $block->escapeHtml(__('Current Configuration Scope:')) ?></label> - <div class="control"> -- <select id="store_switcher" class="system-config-store-switcher" onchange="location.href=this.options[this.selectedIndex].getAttribute('url')"> -- <?php foreach ($block->getStoreSelectOptions() as $_value => $_option): ?> -- <?php if (isset($_option['is_group'])): ?> -- <?php if ($_option['is_close']): ?> -- </optgroup> -- <?php else: ?> -- <optgroup label="<?= $block->escapeHtml($_option['label']) ?>" style="<?= /* @escapeNotVerified */ $_option['style'] ?>"> -+ <select id="store_switcher" class="system-config-store-switcher" -+ onchange="location.href=this.options[this.selectedIndex].getAttribute('url')"> -+ <?php foreach ($block->getStoreSelectOptions() as $_value => $_option) : ?> -+ <?php if (isset($_option['is_group'])) : ?> -+ <?php if ($_option['is_close']) : ?> -+ </optgroup> -+ <?php else : ?> -+ <optgroup label="<?= $block->escapeHtmlAttr($_option['label']) ?>" -+ style="<?= $block->escapeHtmlAttr($_option['style']) ?>"> -+ <?php endif; ?> -+ <?php continue ?> - <?php endif; ?> -- <?php continue ?> -- <?php endif; ?> -- <option value="<?= $block->escapeHtml($_value) ?>" url="<?= /* @escapeNotVerified */ $_option['url'] ?>" <?= $_option['selected'] ? 'selected="selected"' : '' ?> style="<?= /* @escapeNotVerified */ $_option['style'] ?>"> -- <?= $block->escapeHtml($_option['label']) ?> -- </option> -+ <option value="<?= $block->escapeHtmlAttr($_value) ?>" -+ url="<?= $block->escapeUrl($_option['url']) ?>" -+ <?= $_option['selected'] ? 'selected="selected"' : '' ?> -+ style="<?= $block->escapeHtmlAttr($_option['style']) ?>"> -+ <?= $block->escapeHtml($_option['label']) ?> -+ </option> - <?php endforeach ?> - </select> - </div> - <?= $block->getHintHtml() ?> -- <?php if ($block->getAuthorization()->isAllowed('Magento_Backend::store')): ?> -+ <?php if ($block->getAuthorization()->isAllowed('Magento_Backend::store')) : ?> - <div class="actions"> -- <a href="<?= /* @escapeNotVerified */ $block->getUrl('*/system_store') ?>"><?= /* @escapeNotVerified */ __('Stores') ?></a> -+ <a href="<?= $block->escapeUrl($block->getUrl('*/system_store')) ?>"> -+ <?= $block->escapeHtml(__('Stores')) ?> -+ </a> - </div> - <?php endif; ?> - </div> -diff --git a/app/code/Magento/Config/view/adminhtml/templates/system/config/tabs.phtml b/app/code/Magento/Config/view/adminhtml/templates/system/config/tabs.phtml -index d7000f28b5e..73641206a02 100644 ---- a/app/code/Magento/Config/view/adminhtml/templates/system/config/tabs.phtml -+++ b/app/code/Magento/Config/view/adminhtml/templates/system/config/tabs.phtml -@@ -4,52 +4,46 @@ - * See COPYING.txt for license details. - */ - --// @codingStandardsIgnoreFile -- - /** @var $block \Magento\Config\Block\System\Config\Tabs */ - ?> - --<?php if ($block->getTabs()): ?> -- <div id="<?= /* @escapeNotVerified */ $block->getId() ?>" class="config-nav"> -+<?php if ($block->getTabs()) : ?> -+ <div id="<?= $block->escapeHtmlAttr($block->getId()) ?>" class="config-nav"> - <?php - /** @var $_tab \Magento\Config\Model\Config\Structure\Element\Tab */ -- foreach ($block->getTabs() as $_tab): -- ?> -- -- <?php -- $activeCollapsible = false; -- foreach ($_tab->getChildren() as $_section) { -- if ($block->isSectionActive($_section)) { -- $activeCollapsible = true; -- } -+ foreach ($block->getTabs() as $_tab) : -+ $activeCollapsible = false; -+ foreach ($_tab->getChildren() as $_section) { -+ if ($block->isSectionActive($_section)) { -+ $activeCollapsible = true; - } -- ?> -+ } ?> - - <div class="config-nav-block admin__page-nav _collapsed -- <?php if ($_tab->getClass()): ?> -- <?= /* @escapeNotVerified */ $_tab->getClass() ?> -+ <?php if ($_tab->getClass()) : ?> -+ <?= $block->escapeHtmlAttr($_tab->getClass()) ?> - <?php endif ?>" -- data-mage-init='{"collapsible":{"active": "<?= /* @escapeNotVerified */ $activeCollapsible ?>", -+ data-mage-init='{"collapsible":{"active": "<?= $block->escapeHtmlAttr($activeCollapsible) ?>", - "openedState": "_show", - "closedState": "_hide", - "collapsible": true, - "animate": 200}}'> - <div class="admin__page-nav-title title _collapsible" data-role="title"> -- <strong><?= /* @escapeNotVerified */ $_tab->getLabel() ?></strong> -+ <strong><?= $block->escapeHtml($_tab->getLabel()) ?></strong> - </div> - - <ul class="admin__page-nav-items items" data-role="content"> - <?php $_iterator = 1; ?> - <?php - /** @var $_section \Magento\Config\Model\Config\Structure\Element\Section */ -- foreach ($_tab->getChildren() as $_section): ?> -+ foreach ($_tab->getChildren() as $_section) : ?> - <li class="admin__page-nav-item item -- <?= /* @escapeNotVerified */ $_section->getClass() ?> -- <?php if ($block->isSectionActive($_section)): ?> _active<?php endif ?> -+ <?= $block->escapeHtml($_section->getClass()) ?> -+ <?php if ($block->isSectionActive($_section)) : ?> _active<?php endif ?> - <?= $_tab->getChildren()->isLast($_section) ? ' _last' : '' ?>"> -- <a href="<?= /* @escapeNotVerified */ $block->getSectionUrl($_section) ?>" -+ <a href="<?= $block->escapeUrl($block->getSectionUrl($_section)) ?>" - class="admin__page-nav-link item-nav"> -- <span><?= /* @escapeNotVerified */ $_section->getLabel() ?></span> -+ <span><?= $block->escapeHtml($_section->getLabel()) ?></span> - </a> - </li> - <?php $_iterator++; ?> -@@ -57,8 +51,6 @@ - </ul> - - </div> -- <?php -- endforeach; -- ?> -+ <?php endforeach; ?> - </div> - <?php endif; ?> -diff --git a/app/code/Magento/ConfigurableImportExport/Model/Export/RowCustomizer.php b/app/code/Magento/ConfigurableImportExport/Model/Export/RowCustomizer.php -index 58462b873d8..7146108f61f 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 @@ class RowCustomizer implements RowCustomizerInterface - $productCollection->addAttributeToFilter('entity_id', ['in' => $productIds]) - ->addAttributeToFilter('type_id', ['eq' => ConfigurableProductType::TYPE_CODE]); - -+ // set global scope during export -+ $this->storeManager->setCurrentStore(Store::DEFAULT_STORE_ID); -+ - while ($product = $productCollection->fetchItem()) { - $productAttributesOptions = $product->getTypeInstance()->getConfigurableOptions($product); - $this->configurableData[$product->getId()] = []; -diff --git a/app/code/Magento/ConfigurableImportExport/Model/Import/Product/Type/Configurable.php b/app/code/Magento/ConfigurableImportExport/Model/Import/Product/Type/Configurable.php -index fdbd8560c26..2767e725cc9 100644 ---- a/app/code/Magento/ConfigurableImportExport/Model/Import/Product/Type/Configurable.php -+++ b/app/code/Magento/ConfigurableImportExport/Model/Import/Product/Type/Configurable.php -@@ -248,7 +248,7 @@ class Configurable extends \Magento\CatalogImportExport\Model\Import\Product\Typ - */ - protected function _addAttributeParams($attrSetName, array $attrParams, $attribute) - { -- // save super attributes for simplier and quicker search in future -+ // save super attributes for simpler and quicker search in future - if ('select' == $attrParams['type'] && 1 == $attrParams['is_global']) { - $this->_superAttributes[$attrParams['code']] = $attrParams; - } -@@ -596,7 +596,7 @@ class Configurable extends \Magento\CatalogImportExport\Model\Import\Product\Typ - $additionalRow['_super_attribute_position'] = $position; - $additionalRows[] = $additionalRow; - $additionalRow = []; -- $position += 1; -+ $position ++; - } - } else { - throw new LocalizedException( -@@ -937,7 +937,7 @@ class Configurable extends \Magento\CatalogImportExport\Model\Import\Product\Typ - } - foreach ($dataWithExtraVirtualRows as $option) { - if (isset($option['_super_products_sku'])) { -- if (in_array($option['_super_products_sku'], $skus)) { -+ if (in_array($option['_super_products_sku'], $skus, true)) { - $error = true; - $this->_entityModel->addRowError( - sprintf( -diff --git a/app/code/Magento/ConfigurableImportExport/Test/Unit/Model/Import/Product/Type/ConfigurableTest.php b/app/code/Magento/ConfigurableImportExport/Test/Unit/Model/Import/Product/Type/ConfigurableTest.php -index 4446f98cff5..8d75fd902e9 100644 ---- a/app/code/Magento/ConfigurableImportExport/Test/Unit/Model/Import/Product/Type/ConfigurableTest.php -+++ b/app/code/Magento/ConfigurableImportExport/Test/Unit/Model/Import/Product/Type/ConfigurableTest.php -@@ -616,6 +616,83 @@ class ConfigurableTest extends \Magento\ImportExport\Test\Unit\Model\Import\Abst - } - } - -+ public function testRowValidationForNumericalSkus() -+ { -+ // Set _attributes to avoid error in Magento\CatalogImportExport\Model\Import\Product\Type\AbstractType. -+ $this->setPropertyValue($this->configurable, '_attributes', [ -+ 'Default' => [], -+ ]); -+ // Avoiding errors about attributes not being super -+ $this->setPropertyValue( -+ $this->configurable, -+ '_superAttributes', -+ [ -+ 'testattr2' => [ -+ 'options' => [ -+ 'attr2val1' => 1, -+ 'attr2val2' => 2, -+ ] -+ ], -+ ] -+ ); -+ -+ $rowValidationDataProvider = $this->rowValidationDataProvider(); -+ -+ // Checking that variations with duplicate sku are invalid -+ $result = $this->configurable->isRowValid($rowValidationDataProvider['duplicateProduct'], 0); -+ $this->assertFalse($result); -+ -+ // Checking that variations with SKUs that are the same when interpreted as number, -+ // but different when interpreted as string are valid -+ $result = $this->configurable->isRowValid($rowValidationDataProvider['nonDuplicateProduct'], 0); -+ $this->assertTrue($result); -+ } -+ -+ /** -+ * @return array -+ */ -+ public function rowValidationDataProvider() -+ { -+ return [ -+ 'duplicateProduct' => [ -+ 'sku' => 'configurableNumericalSkuDuplicateVariation', -+ 'store_view_code' => null, -+ 'attribute_set_code' => 'Default', -+ 'product_type' => 'configurable', -+ 'name' => 'Configurable Product with duplicate numerical SKUs in variations', -+ 'product_websites' => 'website_1', -+ 'configurable_variation_labels' => 'testattr2=Select Configuration', -+ 'configurable_variations' => 'sku=1234.1,' -+ . 'testattr2=attr2val1,' -+ . 'display=1|sku=1234.1,' -+ . 'testattr2=attr2val1,' -+ . 'display=0', -+ '_store' => null, -+ '_attribute_set' => 'Default', -+ '_type' => 'configurable', -+ '_product_websites' => 'website_1', -+ ], -+ 'nonDuplicateProduct' => [ -+ 'sku' => 'configurableNumericalSkuNonDuplicateVariation', -+ 'store_view_code' => null, -+ 'attribute_set_code' => 'Default', -+ 'product_type' => 'configurable', -+ 'name' => 'Configurable Product with different numerical SKUs in variations', -+ 'product_websites' => 'website_1', -+ 'configurable_variation_labels' => 'testattr2=Select Configuration', -+ 'configurable_variations' => 'sku=1234.10,' -+ . 'testattr2=attr2val1,' -+ . 'display=1|sku=1234.1,' -+ . 'testattr2=attr2val2,' -+ . 'display=0', -+ '_store' => null, -+ '_attribute_set' => 'Default', -+ '_type' => 'configurable', -+ '_product_websites' => 'website_1', -+ ] -+ ]; -+ } -+ - /** - * Set object property value. - * -diff --git a/app/code/Magento/ConfigurableImportExport/composer.json b/app/code/Magento/ConfigurableImportExport/composer.json -index c1aab3e7a14..419a08e14b0 100644 ---- a/app/code/Magento/ConfigurableImportExport/composer.json -+++ b/app/code/Magento/ConfigurableImportExport/composer.json -@@ -11,7 +11,8 @@ - "magento/module-catalog-import-export": "*", - "magento/module-configurable-product": "*", - "magento/module-eav": "*", -- "magento/module-import-export": "*" -+ "magento/module-import-export": "*", -+ "magento/module-store": "*" - }, - "type": "magento2-module", - "license": [ -diff --git a/app/code/Magento/ConfigurableImportExport/etc/module.xml b/app/code/Magento/ConfigurableImportExport/etc/module.xml -index 7ff81f8d634..b59234ca0e7 100644 ---- a/app/code/Magento/ConfigurableImportExport/etc/module.xml -+++ b/app/code/Magento/ConfigurableImportExport/etc/module.xml -@@ -6,6 +6,9 @@ - */ - --> - <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:Module/etc/module.xsd"> -- <module name="Magento_ConfigurableImportExport" > -+ <module name="Magento_ConfigurableImportExport"> -+ <sequence> -+ <module name="Magento_ConfigurableProduct"/> -+ </sequence> - </module> - </config> -diff --git a/app/code/Magento/ConfigurableProduct/Block/Adminhtml/Product/Edit/Button/Save.php b/app/code/Magento/ConfigurableProduct/Block/Adminhtml/Product/Edit/Button/Save.php -index 8848fc78dad..f2de5e72211 100644 ---- a/app/code/Magento/ConfigurableProduct/Block/Adminhtml/Product/Edit/Button/Save.php -+++ b/app/code/Magento/ConfigurableProduct/Block/Adminhtml/Product/Edit/Button/Save.php -@@ -8,7 +8,6 @@ namespace Magento\ConfigurableProduct\Block\Adminhtml\Product\Edit\Button; - use Magento\Ui\Component\Control\Container; - use Magento\Catalog\Block\Adminhtml\Product\Edit\Button\Generic; - use Magento\ConfigurableProduct\Model\Product\Type\Configurable as ConfigurableType; --use Magento\Catalog\Model\Product\Type; - - /** - * Class Save -@@ -16,16 +15,7 @@ use Magento\Catalog\Model\Product\Type; - class Save extends Generic - { - /** -- * @var array -- */ -- private static $availableProductTypes = [ -- ConfigurableType::TYPE_CODE, -- Type::TYPE_SIMPLE, -- Type::TYPE_VIRTUAL -- ]; -- -- /** -- * {@inheritdoc} -+ * @inheritdoc - */ - public function getButtonData() - { -@@ -135,7 +125,8 @@ class Save extends Generic - } - - /** -- * Retrieve target for button -+ * Retrieve target for button. -+ * - * @return string - */ - protected function getSaveTarget() -@@ -148,7 +139,8 @@ class Save extends Generic - } - - /** -- * Retrieve action for button -+ * Retrieve action for button. -+ * - * @return string - */ - protected function getSaveAction() -@@ -161,10 +153,12 @@ class Save extends Generic - } - - /** -+ * Is configurable product. -+ * - * @return boolean - */ - protected function isConfigurableProduct() - { -- return in_array($this->getProduct()->getTypeId(), self::$availableProductTypes); -+ return !$this->getProduct()->isComposite() || $this->getProduct()->getTypeId() === ConfigurableType::TYPE_CODE; - } - } -diff --git a/app/code/Magento/ConfigurableProduct/Block/Cart/Item/Renderer/Configurable.php b/app/code/Magento/ConfigurableProduct/Block/Cart/Item/Renderer/Configurable.php -index 3b657dd1ab2..77110975401 100644 ---- a/app/code/Magento/ConfigurableProduct/Block/Cart/Item/Renderer/Configurable.php -+++ b/app/code/Magento/ConfigurableProduct/Block/Cart/Item/Renderer/Configurable.php -@@ -70,4 +70,14 @@ class Configurable extends Renderer implements IdentityInterface - } - return $identities; - } -+ -+ /** -+ * Get price for exact simple product added to cart -+ * -+ * @inheritdoc -+ */ -+ public function getProductPriceHtml(\Magento\Catalog\Model\Product $product) -+ { -+ return parent::getProductPriceHtml($this->getChildProduct()); -+ } - } -diff --git a/app/code/Magento/ConfigurableProduct/Block/Product/View/Type/Configurable.php b/app/code/Magento/ConfigurableProduct/Block/Product/View/Type/Configurable.php -index 2502b79921e..e07879e93a6 100644 ---- a/app/code/Magento/ConfigurableProduct/Block/Product/View/Type/Configurable.php -+++ b/app/code/Magento/ConfigurableProduct/Block/Product/View/Type/Configurable.php -@@ -15,6 +15,8 @@ use Magento\Framework\Locale\Format; - use Magento\Framework\Pricing\PriceCurrencyInterface; - - /** -+ * Confugurable product view type -+ * - * @api - * @SuppressWarnings(PHPMD.CouplingBetweenObjects) - * @api -@@ -276,6 +278,8 @@ class Configurable extends \Magento\Catalog\Block\Product\View\AbstractView - } - - /** -+ * Collect price options -+ * - * @return array - */ - protected function getOptionPrices() -@@ -314,6 +318,11 @@ class Configurable extends \Magento\Catalog\Block\Product\View\AbstractView - ), - ], - 'tierPrices' => $tierPrices, -+ 'msrpPrice' => [ -+ 'amount' => $this->localeFormat->getNumber( -+ $product->getMsrp() -+ ), -+ ], - ]; - } - return $prices; -diff --git a/app/code/Magento/ConfigurableProduct/Controller/Adminhtml/Product/AddAttribute.php b/app/code/Magento/ConfigurableProduct/Controller/Adminhtml/Product/AddAttribute.php -index 5991097e456..34f10b59f3a 100644 ---- a/app/code/Magento/ConfigurableProduct/Controller/Adminhtml/Product/AddAttribute.php -+++ b/app/code/Magento/ConfigurableProduct/Controller/Adminhtml/Product/AddAttribute.php -@@ -6,10 +6,11 @@ - */ - namespace Magento\ConfigurableProduct\Controller\Adminhtml\Product; - -+use Magento\Framework\App\Action\HttpGetActionInterface as HttpGetActionInterface; - use Magento\Backend\App\Action; - use Magento\Framework\Controller\ResultFactory; - --class AddAttribute extends Action -+class AddAttribute extends Action implements HttpGetActionInterface - { - /** - * Authorization level of a basic admin session -diff --git a/app/code/Magento/ConfigurableProduct/Controller/Adminhtml/Product/Attribute/CreateOptions.php b/app/code/Magento/ConfigurableProduct/Controller/Adminhtml/Product/Attribute/CreateOptions.php -index 6f5f106a8bb..8b4c8d29cd9 100644 ---- a/app/code/Magento/ConfigurableProduct/Controller/Adminhtml/Product/Attribute/CreateOptions.php -+++ b/app/code/Magento/ConfigurableProduct/Controller/Adminhtml/Product/Attribute/CreateOptions.php -@@ -4,12 +4,21 @@ - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -+declare(strict_types=1); -+ - namespace Magento\ConfigurableProduct\Controller\Adminhtml\Product\Attribute; - - use Magento\Backend\App\Action; -+use Magento\Catalog\Api\Data\ProductAttributeInterface; - use Magento\Catalog\Model\ResourceModel\Eav\AttributeFactory; -+use Magento\Framework\App\Action\HttpPostActionInterface as HttpPostActionInterface; -+use Magento\Framework\Exception\LocalizedException; -+use Magento\Framework\Json\Helper\Data; - --class CreateOptions extends Action -+/** -+ * Creates options for product attributes -+ */ -+class CreateOptions extends Action implements HttpPostActionInterface - { - /** - * Authorization level of a basic admin session -@@ -19,28 +28,33 @@ class CreateOptions extends Action - const ADMIN_RESOURCE = 'Magento_Catalog::products'; - - /** -- * @var \Magento\Framework\Json\Helper\Data -+ * @var Data - */ - protected $jsonHelper; - - /** -- * @var \Magento\Catalog\Model\ResourceModel\Eav\AttributeFactory -+ * @var AttributeFactory - */ - protected $attributeFactory; - -+ /** -+ * @var ProductAttributeInterface[] -+ */ -+ private $attributes; -+ - /** - * @param Action\Context $context -- * @param \Magento\Framework\Json\Helper\Data $jsonHelper -+ * @param Data $jsonHelper - * @param AttributeFactory $attributeFactory - */ - public function __construct( - Action\Context $context, -- \Magento\Framework\Json\Helper\Data $jsonHelper, -+ Data $jsonHelper, - AttributeFactory $attributeFactory - ) { -+ parent::__construct($context); - $this->jsonHelper = $jsonHelper; - $this->attributeFactory = $attributeFactory; -- parent::__construct($context); - } - - /** -@@ -50,7 +64,15 @@ class CreateOptions extends Action - */ - public function execute() - { -- $this->getResponse()->representJson($this->jsonHelper->jsonEncode($this->saveAttributeOptions())); -+ try { -+ $output = $this->saveAttributeOptions(); -+ } catch (LocalizedException $e) { -+ $output = [ -+ 'error' => true, -+ 'message' => $e->getMessage(), -+ ]; -+ } -+ $this->getResponse()->representJson($this->jsonHelper->jsonEncode($output)); - } - - /** -@@ -60,31 +82,103 @@ class CreateOptions extends Action - * @TODO Move this logic to configurable product type model - * when full set of operations for attribute options during - * product creation will be implemented: edit labels, remove, reorder. -- * Currently only addition of options to end and removal of just added option is supported. -+ * Currently only addition of options is supported. -+ * @throws LocalizedException - */ - protected function saveAttributeOptions() - { -- $options = (array)$this->getRequest()->getParam('options'); -+ $attributeIds = $this->getUpdatedAttributeIds(); - $savedOptions = []; -- foreach ($options as $option) { -- if (isset($option['label']) && isset($option['is_new'])) { -- $attribute = $this->attributeFactory->create(); -- $attribute->load($option['attribute_id']); -- $optionsBefore = $attribute->getSource()->getAllOptions(false); -- $attribute->setOption( -- [ -- 'value' => ['option_0' => [$option['label']]], -- 'order' => ['option_0' => count($optionsBefore) + 1], -- ] -- ); -- $attribute->save(); -- $attribute = $this->attributeFactory->create(); -- $attribute->load($option['attribute_id']); -- $optionsAfter = $attribute->getSource()->getAllOptions(false); -- $newOption = array_pop($optionsAfter); -- $savedOptions[$option['id']] = $newOption['value']; -+ foreach ($attributeIds as $attributeId => $newOptions) { -+ $attribute = $this->getAttribute($attributeId); -+ $this->checkUnique($attribute, $newOptions); -+ foreach ($newOptions as $newOption) { -+ $lastAddedOption = $this->saveOption($attribute, $newOption); -+ $savedOptions[$newOption['id']] = $lastAddedOption['value']; - } - } -+ - return $savedOptions; - } -+ -+ /** -+ * Checks unique values -+ * -+ * @param ProductAttributeInterface $attribute -+ * @param array $newOptions -+ * @return void -+ * @throws LocalizedException -+ */ -+ private function checkUnique(ProductAttributeInterface $attribute, array $newOptions) -+ { -+ $originalOptions = $attribute->getSource()->getAllOptions(false); -+ $allOptions = array_merge($originalOptions, $newOptions); -+ $optionValues = array_map( -+ function ($option) { -+ return $option['label'] ?? null; -+ }, -+ $allOptions -+ ); -+ -+ $uniqueValues = array_unique(array_filter($optionValues)); -+ $duplicates = array_diff_assoc($optionValues, $uniqueValues); -+ if ($duplicates) { -+ throw new LocalizedException(__('The value of attribute ""%1"" must be unique', $attribute->getName())); -+ } -+ } -+ -+ /** -+ * Loads the product attribute by the id -+ * -+ * @param int $attributeId -+ * @return ProductAttributeInterface -+ */ -+ private function getAttribute(int $attributeId) -+ { -+ if (!isset($this->attributes[$attributeId])) { -+ $attribute = $this->attributeFactory->create(); -+ $this->attributes[$attributeId] = $attribute->load($attributeId); -+ } -+ -+ return $this->attributes[$attributeId]; -+ } -+ -+ /** -+ * Retrieve updated attribute ids with new options -+ * -+ * @return array -+ */ -+ private function getUpdatedAttributeIds() -+ { -+ $options = (array)$this->getRequest()->getParam('options'); -+ $updatedAttributeIds = []; -+ foreach ($options as $option) { -+ if (isset($option['label'], $option['is_new'], $option['attribute_id'])) { -+ $updatedAttributeIds[$option['attribute_id']][] = $option; -+ } -+ } -+ -+ return $updatedAttributeIds; -+ } -+ -+ /** -+ * Saves the option -+ * -+ * @param ProductAttributeInterface $attribute -+ * @param array $newOption -+ * @return array -+ */ -+ private function saveOption(ProductAttributeInterface $attribute, array $newOption) -+ { -+ $optionsBefore = $attribute->getSource()->getAllOptions(false); -+ $attribute->setOption( -+ [ -+ 'value' => ['option_0' => [$newOption['label']]], -+ 'order' => ['option_0' => count($optionsBefore) + 1], -+ ] -+ ); -+ $attribute->save(); -+ $optionsAfter = $attribute->getSource()->getAllOptions(false); -+ return array_pop($optionsAfter); -+ } - } -diff --git a/app/code/Magento/ConfigurableProduct/Controller/Adminhtml/Product/Attribute/GetAttributes.php b/app/code/Magento/ConfigurableProduct/Controller/Adminhtml/Product/Attribute/GetAttributes.php -index b6b34073db6..9f5d5062b53 100644 ---- a/app/code/Magento/ConfigurableProduct/Controller/Adminhtml/Product/Attribute/GetAttributes.php -+++ b/app/code/Magento/ConfigurableProduct/Controller/Adminhtml/Product/Attribute/GetAttributes.php -@@ -6,10 +6,11 @@ - */ - namespace Magento\ConfigurableProduct\Controller\Adminhtml\Product\Attribute; - -+use Magento\Framework\App\Action\HttpGetActionInterface; - use Magento\Backend\App\Action; - use Magento\ConfigurableProduct\Model\AttributesListInterface; - --class GetAttributes extends Action -+class GetAttributes extends Action implements HttpGetActionInterface - { - /** - * Authorization level of a basic admin session -diff --git a/app/code/Magento/ConfigurableProduct/Controller/Adminhtml/Product/Initialization/Helper/Plugin/Configurable.php b/app/code/Magento/ConfigurableProduct/Controller/Adminhtml/Product/Initialization/Helper/Plugin/Configurable.php -index 5cd8b6a7d0b..b5940e36aa7 100644 ---- a/app/code/Magento/ConfigurableProduct/Controller/Adminhtml/Product/Initialization/Helper/Plugin/Configurable.php -+++ b/app/code/Magento/ConfigurableProduct/Controller/Adminhtml/Product/Initialization/Helper/Plugin/Configurable.php -@@ -158,7 +158,7 @@ class Configurable - $configurableMatrix = json_decode($configurableMatrix, true); - - foreach ($configurableMatrix as $item) { -- if ($item['newProduct']) { -+ if (isset($item['newProduct']) && $item['newProduct']) { - $result[$item['variationKey']] = $this->mapData($item); - - if (isset($item['qty'])) { -diff --git a/app/code/Magento/ConfigurableProduct/Controller/Adminhtml/Product/Wizard.php b/app/code/Magento/ConfigurableProduct/Controller/Adminhtml/Product/Wizard.php -index 8adfdea9610..104181aed4f 100644 ---- a/app/code/Magento/ConfigurableProduct/Controller/Adminhtml/Product/Wizard.php -+++ b/app/code/Magento/ConfigurableProduct/Controller/Adminhtml/Product/Wizard.php -@@ -5,6 +5,8 @@ - */ - namespace Magento\ConfigurableProduct\Controller\Adminhtml\Product; - -+use Magento\Framework\App\Action\HttpGetActionInterface; -+use Magento\Framework\App\Action\HttpPostActionInterface as HttpPostActionInterface; - use Magento\Backend\App\Action; - use Magento\Framework\Controller\ResultFactory; - use Magento\Catalog\Controller\Adminhtml\Product\Builder; -@@ -13,7 +15,7 @@ use Magento\Backend\App\Action\Context; - /** - * Class Wizard - */ --class Wizard extends Action -+class Wizard extends Action implements HttpPostActionInterface, HttpGetActionInterface - { - /** - * Authorization level of a basic admin session -diff --git a/app/code/Magento/ConfigurableProduct/Model/Inventory/ParentItemProcessor.php b/app/code/Magento/ConfigurableProduct/Model/Inventory/ParentItemProcessor.php -new file mode 100644 -index 00000000000..f1567f2b196 ---- /dev/null -+++ b/app/code/Magento/ConfigurableProduct/Model/Inventory/ParentItemProcessor.php -@@ -0,0 +1,129 @@ -+<?php -+/** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+declare(strict_types=1); -+ -+namespace Magento\ConfigurableProduct\Model\Inventory; -+ -+use Magento\ConfigurableProduct\Model\Product\Type\Configurable; -+use Magento\Catalog\Api\Data\ProductInterface as Product; -+use Magento\CatalogInventory\Api\StockItemCriteriaInterfaceFactory; -+use Magento\CatalogInventory\Api\StockItemRepositoryInterface; -+use Magento\CatalogInventory\Api\StockConfigurationInterface; -+use Magento\CatalogInventory\Api\Data\StockItemInterface; -+use Magento\CatalogInventory\Observer\ParentItemProcessorInterface; -+ -+/** -+ * Process parent stock item -+ */ -+class ParentItemProcessor implements ParentItemProcessorInterface -+{ -+ /** -+ * @var Configurable -+ */ -+ private $configurableType; -+ -+ /** -+ * @var StockItemCriteriaInterfaceFactory -+ */ -+ private $criteriaInterfaceFactory; -+ -+ /** -+ * @var StockItemRepositoryInterface -+ */ -+ private $stockItemRepository; -+ -+ /** -+ * @var StockConfigurationInterface -+ */ -+ private $stockConfiguration; -+ -+ /** -+ * @param Configurable $configurableType -+ * @param StockItemCriteriaInterfaceFactory $criteriaInterfaceFactory -+ * @param StockItemRepositoryInterface $stockItemRepository -+ * @param StockConfigurationInterface $stockConfiguration -+ */ -+ public function __construct( -+ Configurable $configurableType, -+ StockItemCriteriaInterfaceFactory $criteriaInterfaceFactory, -+ StockItemRepositoryInterface $stockItemRepository, -+ StockConfigurationInterface $stockConfiguration -+ ) { -+ $this->configurableType = $configurableType; -+ $this->criteriaInterfaceFactory = $criteriaInterfaceFactory; -+ $this->stockItemRepository = $stockItemRepository; -+ $this->stockConfiguration = $stockConfiguration; -+ } -+ -+ /** -+ * Process parent products -+ * -+ * @param Product $product -+ * @return void -+ */ -+ public function process(Product $product) -+ { -+ $parentIds = $this->configurableType->getParentIdsByChild($product->getId()); -+ foreach ($parentIds as $productId) { -+ $this->processStockForParent((int)$productId); -+ } -+ } -+ -+ /** -+ * Change stock item for parent product depending on children stock items -+ * -+ * @param int $productId -+ * @return void -+ */ -+ private function processStockForParent(int $productId) -+ { -+ $criteria = $this->criteriaInterfaceFactory->create(); -+ $criteria->setScopeFilter($this->stockConfiguration->getDefaultScopeId()); -+ -+ $criteria->setProductsFilter($productId); -+ $stockItemCollection = $this->stockItemRepository->getList($criteria); -+ $allItems = $stockItemCollection->getItems(); -+ if (empty($allItems)) { -+ return; -+ } -+ $parentStockItem = array_shift($allItems); -+ -+ $childrenIds = $this->configurableType->getChildrenIds($productId); -+ $criteria->setProductsFilter($childrenIds); -+ $stockItemCollection = $this->stockItemRepository->getList($criteria); -+ $allItems = $stockItemCollection->getItems(); -+ -+ $childrenIsInStock = false; -+ -+ foreach ($allItems as $childItem) { -+ if ($childItem->getIsInStock() === true) { -+ $childrenIsInStock = true; -+ break; -+ } -+ } -+ -+ if ($this->isNeedToUpdateParent($parentStockItem, $childrenIsInStock)) { -+ $parentStockItem->setIsInStock($childrenIsInStock); -+ $parentStockItem->setStockStatusChangedAuto(1); -+ $this->stockItemRepository->save($parentStockItem); -+ } -+ } -+ -+ /** -+ * Check is parent item should be updated -+ * -+ * @param StockItemInterface $parentStockItem -+ * @param bool $childrenIsInStock -+ * @return bool -+ */ -+ private function isNeedToUpdateParent( -+ StockItemInterface $parentStockItem, -+ bool $childrenIsInStock -+ ): bool { -+ return $parentStockItem->getIsInStock() !== $childrenIsInStock && -+ ($childrenIsInStock === false || $parentStockItem->getStockStatusChangedAuto()); -+ } -+} -diff --git a/app/code/Magento/ConfigurableProduct/Model/LinkManagement.php b/app/code/Magento/ConfigurableProduct/Model/LinkManagement.php -index 79c2dd812ac..890564fdb30 100644 ---- a/app/code/Magento/ConfigurableProduct/Model/LinkManagement.php -+++ b/app/code/Magento/ConfigurableProduct/Model/LinkManagement.php -@@ -1,6 +1,5 @@ - <?php - /** -- * - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -@@ -11,6 +10,11 @@ use Magento\Framework\Exception\InputException; - use Magento\Framework\Exception\NoSuchEntityException; - use Magento\Framework\Exception\StateException; - -+/** -+ * Configurable product link management. -+ * -+ * @SuppressWarnings(PHPMD.CouplingBetweenObjects) -+ */ - class LinkManagement implements \Magento\ConfigurableProduct\Api\LinkManagementInterface - { - /** -@@ -68,7 +72,7 @@ class LinkManagement implements \Magento\ConfigurableProduct\Api\LinkManagementI - } - - /** -- * {@inheritdoc} -+ * @inheritdoc - */ - public function getChildren($sku) - { -@@ -107,11 +111,15 @@ class LinkManagement implements \Magento\ConfigurableProduct\Api\LinkManagementI - } - - /** -- * {@inheritdoc} -+ * @inheritdoc -+ * @throws InputException -+ * @throws NoSuchEntityException -+ * @throws StateException -+ * @throws \Magento\Framework\Exception\CouldNotSaveException - */ - public function addChild($sku, $childSku) - { -- $product = $this->productRepository->get($sku); -+ $product = $this->productRepository->get($sku, true); - $child = $this->productRepository->get($childSku); - - $childrenIds = array_values($this->configurableType->getChildrenIds($product->getId())[0]); -@@ -124,7 +132,7 @@ class LinkManagement implements \Magento\ConfigurableProduct\Api\LinkManagementI - throw new StateException(__("The parent product doesn't have configurable product options.")); - } - -- $attributeIds = []; -+ $attributeData = []; - foreach ($configurableProductOptions as $configurableProductOption) { - $attributeCode = $configurableProductOption->getProductAttribute()->getAttributeCode(); - if (!$child->getData($attributeCode)) { -@@ -135,9 +143,11 @@ class LinkManagement implements \Magento\ConfigurableProduct\Api\LinkManagementI - ) - ); - } -- $attributeIds[] = $configurableProductOption->getAttributeId(); -+ $attributeData[$configurableProductOption->getAttributeId()] = [ -+ 'position' => $configurableProductOption->getPosition() -+ ]; - } -- $configurableOptionData = $this->getConfigurableAttributesData($attributeIds); -+ $configurableOptionData = $this->getConfigurableAttributesData($attributeData); - - /** @var \Magento\ConfigurableProduct\Helper\Product\Options\Factory $optionFactory */ - $optionFactory = $this->getOptionsFactory(); -@@ -150,7 +160,11 @@ class LinkManagement implements \Magento\ConfigurableProduct\Api\LinkManagementI - } - - /** -- * {@inheritdoc} -+ * @inheritdoc -+ * @throws InputException -+ * @throws NoSuchEntityException -+ * @throws StateException -+ * @throws \Magento\Framework\Exception\CouldNotSaveException - */ - public function removeChild($sku, $childSku) - { -@@ -199,16 +213,16 @@ class LinkManagement implements \Magento\ConfigurableProduct\Api\LinkManagementI - /** - * Get Configurable Attribute Data - * -- * @param int[] $attributeIds -+ * @param int[] $attributeData - * @return array - */ -- private function getConfigurableAttributesData($attributeIds) -+ private function getConfigurableAttributesData($attributeData) - { - $configurableAttributesData = []; - $attributeValues = []; - $attributes = $this->attributeFactory->create() - ->getCollection() -- ->addFieldToFilter('attribute_id', $attributeIds) -+ ->addFieldToFilter('attribute_id', array_keys($attributeData)) - ->getItems(); - foreach ($attributes as $attribute) { - foreach ($attribute->getOptions() as $option) { -@@ -225,6 +239,7 @@ class LinkManagement implements \Magento\ConfigurableProduct\Api\LinkManagementI - 'attribute_id' => $attribute->getId(), - 'code' => $attribute->getAttributeCode(), - 'label' => $attribute->getStoreLabel(), -+ 'position' => $attributeData[$attribute->getId()]['position'], - 'values' => $attributeValues, - ]; - } -diff --git a/app/code/Magento/ConfigurableProduct/Model/OptionRepository.php b/app/code/Magento/ConfigurableProduct/Model/OptionRepository.php -index b4db0a4db5f..368ecad1924 100644 ---- a/app/code/Magento/ConfigurableProduct/Model/OptionRepository.php -+++ b/app/code/Magento/ConfigurableProduct/Model/OptionRepository.php -@@ -22,6 +22,7 @@ use Magento\Framework\EntityManager\MetadataPool; - use Magento\Store\Model\Store; - - /** -+ * Repository for performing CRUD operations for a configurable product's options. - * @SuppressWarnings(PHPMD.CouplingBetweenObjects) - */ - class OptionRepository implements \Magento\ConfigurableProduct\Api\OptionRepositoryInterface -@@ -112,7 +113,7 @@ class OptionRepository implements \Magento\ConfigurableProduct\Api\OptionReposit - } - - /** -- * {@inheritdoc} -+ * @inheritdoc - */ - public function get($sku, $id) - { -@@ -131,7 +132,7 @@ class OptionRepository implements \Magento\ConfigurableProduct\Api\OptionReposit - } - - /** -- * {@inheritdoc} -+ * @inheritdoc - */ - public function getList($sku) - { -@@ -141,7 +142,7 @@ class OptionRepository implements \Magento\ConfigurableProduct\Api\OptionReposit - } - - /** -- * {@inheritdoc} -+ * @inheritdoc - */ - public function delete(OptionInterface $option) - { -@@ -167,7 +168,7 @@ class OptionRepository implements \Magento\ConfigurableProduct\Api\OptionReposit - } - - /** -- * {@inheritdoc} -+ * @inheritdoc - */ - public function deleteById($sku, $id) - { -@@ -184,7 +185,7 @@ class OptionRepository implements \Magento\ConfigurableProduct\Api\OptionReposit - } - - /** -- * {@inheritdoc} -+ * @inheritdoc - * @SuppressWarnings(PHPMD.CyclomaticComplexity) - */ - public function save($sku, OptionInterface $option) -@@ -213,6 +214,16 @@ class OptionRepository implements \Magento\ConfigurableProduct\Api\OptionReposit - throw new \InvalidArgumentException('Incompatible product type'); - } - $option->setProductId($product->getData($metadata->getLinkField())); -+ if (!empty($option->getProductId() && !empty($option->getAttributeId()))) { -+ $id = $this->optionResource->getIdByProductIdAndAttributeId( -+ $option, -+ $option->getProductId(), -+ $option->getAttributeId() -+ ); -+ if (!empty($id)) { -+ $option->setId($id); -+ } -+ } - } - - try { -@@ -296,6 +307,7 @@ class OptionRepository implements \Magento\ConfigurableProduct\Api\OptionReposit - - /** - * Get MetadataPool instance -+ * - * @return MetadataPool - */ - private function getMetadataPool() -diff --git a/app/code/Magento/ConfigurableProduct/Model/Plugin/Frontend/ProductIdentitiesExtender.php b/app/code/Magento/ConfigurableProduct/Model/Plugin/Frontend/ProductIdentitiesExtender.php -new file mode 100644 -index 00000000000..92b7ab0d88e ---- /dev/null -+++ b/app/code/Magento/ConfigurableProduct/Model/Plugin/Frontend/ProductIdentitiesExtender.php -@@ -0,0 +1,48 @@ -+<?php -+/** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+declare(strict_types=1); -+ -+namespace Magento\ConfigurableProduct\Model\Plugin\Frontend; -+ -+use Magento\ConfigurableProduct\Model\Product\Type\Configurable; -+use Magento\Catalog\Model\Product; -+ -+/** -+ * Extender of product identities for child of configurable products -+ */ -+class ProductIdentitiesExtender -+{ -+ /** -+ * @var Configurable -+ */ -+ private $configurableType; -+ -+ /** -+ * @param Configurable $configurableType -+ */ -+ public function __construct(Configurable $configurableType) -+ { -+ $this->configurableType = $configurableType; -+ } -+ -+ /** -+ * Add child identities to product identities -+ * -+ * @param Product $subject -+ * @param array $identities -+ * @return array -+ */ -+ public function afterGetIdentities(Product $subject, array $identities): array -+ { -+ foreach ($this->configurableType->getChildrenIds($subject->getId()) as $childIds) { -+ foreach ($childIds as $childId) { -+ $identities[] = Product::CACHE_TAG . '_' . $childId; -+ } -+ } -+ -+ return array_unique($identities); -+ } -+} -diff --git a/app/code/Magento/ConfigurableProduct/Model/Product/Configuration/Item/ItemProductResolver.php b/app/code/Magento/ConfigurableProduct/Model/Product/Configuration/Item/ItemProductResolver.php -index 6c33ecc138a..7de78b6612a 100644 ---- a/app/code/Magento/ConfigurableProduct/Model/Product/Configuration/Item/ItemProductResolver.php -+++ b/app/code/Magento/ConfigurableProduct/Model/Product/Configuration/Item/ItemProductResolver.php -@@ -13,16 +13,17 @@ use Magento\Catalog\Api\Data\ProductInterface; - use Magento\Catalog\Model\Product\Configuration\Item\ItemResolverInterface; - use Magento\Framework\App\Config\ScopeConfigInterface; - use Magento\Catalog\Model\Product; -+use Magento\Store\Model\ScopeInterface; - - /** -- * {@inheritdoc} -+ * Resolves the product from a configured item. - */ - class ItemProductResolver implements ItemResolverInterface - { - /** - * Path in config to the setting which defines if parent or child product should be used to generate a thumbnail. - */ -- const CONFIG_THUMBNAIL_SOURCE = 'checkout/cart/configurable_product_image'; -+ public const CONFIG_THUMBNAIL_SOURCE = 'checkout/cart/configurable_product_image'; - - /** - * @var ScopeConfigInterface -@@ -38,27 +39,21 @@ class ItemProductResolver implements ItemResolverInterface - } - - /** -- * {@inheritdoc} -+ * Get the final product from a configured item by product type and selection. -+ * -+ * @param ItemInterface $item -+ * @return ProductInterface - */ -- public function getFinalProduct(ItemInterface $item) : ProductInterface -+ public function getFinalProduct(ItemInterface $item): ProductInterface - { - /** - * Show parent product thumbnail if it must be always shown according to the related setting in system config - * or if child thumbnail is not available. - */ -- $parentProduct = $item->getProduct(); -- $finalProduct = $parentProduct; -+ $finalProduct = $item->getProduct(); - $childProduct = $this->getChildProduct($item); -- if ($childProduct !== $parentProduct) { -- $configValue = $this->scopeConfig->getValue( -- self::CONFIG_THUMBNAIL_SOURCE, -- \Magento\Store\Model\ScopeInterface::SCOPE_STORE -- ); -- $childThumb = $childProduct->getData('thumbnail'); -- $finalProduct = -- ($configValue == Thumbnail::OPTION_USE_PARENT_IMAGE) || (!$childThumb || $childThumb == 'no_selection') -- ? $parentProduct -- : $childProduct; -+ if ($childProduct !== null && $this->isUseChildProduct($childProduct)) { -+ $finalProduct = $childProduct; - } - return $finalProduct; - } -@@ -67,15 +62,30 @@ class ItemProductResolver implements ItemResolverInterface - * Get item configurable child product. - * - * @param ItemInterface $item -- * @return Product -+ * @return Product | null - */ -- private function getChildProduct(ItemInterface $item) : Product -+ private function getChildProduct(ItemInterface $item): ?Product - { -+ /** @var \Magento\Quote\Model\Quote\Item\Option $option */ - $option = $item->getOptionByCode('simple_product'); -- $product = $item->getProduct(); -- if ($option) { -- $product = $option->getProduct(); -- } -- return $product; -+ return $option ? $option->getProduct() : null; -+ } -+ -+ /** -+ * Is need to use child product -+ * -+ * @param Product $childProduct -+ * @return bool -+ */ -+ private function isUseChildProduct(Product $childProduct): bool -+ { -+ $configValue = $this->scopeConfig->getValue( -+ self::CONFIG_THUMBNAIL_SOURCE, -+ ScopeInterface::SCOPE_STORE -+ ); -+ $childThumb = $childProduct->getData('thumbnail'); -+ return $configValue !== Thumbnail::OPTION_USE_PARENT_IMAGE -+ && $childThumb !== null -+ && $childThumb !== 'no_selection'; - } - } -diff --git a/app/code/Magento/ConfigurableProduct/Model/Product/Type/Configurable.php b/app/code/Magento/ConfigurableProduct/Model/Product/Type/Configurable.php -index 19de63b7a97..a849d964eae 100644 ---- a/app/code/Magento/ConfigurableProduct/Model/Product/Type/Configurable.php -+++ b/app/code/Magento/ConfigurableProduct/Model/Product/Type/Configurable.php -@@ -24,6 +24,7 @@ use Magento\Framework\EntityManager\MetadataPool; - * @SuppressWarnings(PHPMD.TooManyFields) - * @SuppressWarnings(PHPMD.ExcessiveClassComplexity) - * @SuppressWarnings(PHPMD.CouplingBetweenObjects) -+ * @SuppressWarnings(PHPMD.CookieAndSessionMisuse) - * @api - * @since 100.0.2 - */ -@@ -453,6 +454,10 @@ class Configurable extends \Magento\Catalog\Model\Product\Type\AbstractType - ['group' => 'CONFIGURABLE', 'method' => __METHOD__] - ); - if (!$product->hasData($this->_configurableAttributes)) { -+ // for new product do not load configurable attributes -+ if (!$product->getId()) { -+ return []; -+ } - $configurableAttributes = $this->getConfigurableAttributeCollection($product); - $this->extensionAttributesJoinProcessor->process($configurableAttributes); - $configurableAttributes->orderByPosition()->load(); -@@ -1227,6 +1232,8 @@ class Configurable extends \Magento\Catalog\Model\Product\Type\AbstractType - /** - * Returns array of sub-products for specified configurable product - * -+ * $requiredAttributeIds - one dimensional array, if provided -+ * - * Result array contains all children for specified configurable product - * - * @param \Magento\Catalog\Model\Product $product -@@ -1240,9 +1247,12 @@ class Configurable extends \Magento\Catalog\Model\Product\Type\AbstractType - __METHOD__, - $product->getData($metadata->getLinkField()), - $product->getStoreId(), -- $this->getCustomerSession()->getCustomerGroupId(), -- $requiredAttributeIds -+ $this->getCustomerSession()->getCustomerGroupId() - ]; -+ if ($requiredAttributeIds !== null) { -+ sort($requiredAttributeIds); -+ $keyParts[] = implode('', $requiredAttributeIds); -+ } - $cacheKey = $this->getUsedProductsCacheKey($keyParts); - return $this->loadUsedProducts($product, $cacheKey); - } -@@ -1381,7 +1391,7 @@ class Configurable extends \Magento\Catalog\Model\Product\Type\AbstractType - */ - private function getUsedProductsCacheKey($keyParts) - { -- return md5(implode('_', $keyParts)); -+ return sha1(implode('_', $keyParts)); - } - - /** -@@ -1398,23 +1408,47 @@ class Configurable extends \Magento\Catalog\Model\Product\Type\AbstractType - $skipStockFilter = true - ) { - $collection = $this->getUsedProductCollection($product); -+ - if ($skipStockFilter) { - $collection->setFlag('has_stock_status_filter', true); - } -+ - $collection -- ->addAttributeToSelect($this->getCatalogConfig()->getProductAttributes()) -+ ->addAttributeToSelect($this->getAttributesForCollection($product)) - ->addFilterByRequiredOptions() - ->setStoreId($product->getStoreId()); - -- $requiredAttributes = ['name', 'price', 'weight', 'image', 'thumbnail', 'status', 'media_gallery']; -- foreach ($requiredAttributes as $attributeCode) { -- $collection->addAttributeToSelect($attributeCode); -- } -- foreach ($this->getUsedProductAttributes($product) as $usedProductAttribute) { -- $collection->addAttributeToSelect($usedProductAttribute->getAttributeCode()); -- } - $collection->addMediaGalleryData(); - $collection->addTierPriceData(); -+ - return $collection; - } -+ -+ /** -+ * @return array -+ */ -+ private function getAttributesForCollection(\Magento\Catalog\Model\Product $product) -+ { -+ $productAttributes = $this->getCatalogConfig()->getProductAttributes(); -+ -+ $requiredAttributes = [ -+ 'name', -+ 'price', -+ 'weight', -+ 'image', -+ 'thumbnail', -+ 'status', -+ 'visibility', -+ 'media_gallery' -+ ]; -+ -+ $usedAttributes = array_map( -+ function($attr) { -+ return $attr->getAttributeCode(); -+ }, -+ $this->getUsedProductAttributes($product) -+ ); -+ -+ return array_unique(array_merge($productAttributes, $requiredAttributes, $usedAttributes)); -+ } - } -diff --git a/app/code/Magento/ConfigurableProduct/Model/Product/Type/Configurable/Attribute.php b/app/code/Magento/ConfigurableProduct/Model/Product/Type/Configurable/Attribute.php -index 617297e545b..4ead9ffe0fe 100644 ---- a/app/code/Magento/ConfigurableProduct/Model/Product/Type/Configurable/Attribute.php -+++ b/app/code/Magento/ConfigurableProduct/Model/Product/Type/Configurable/Attribute.php -@@ -12,13 +12,15 @@ use Magento\Framework\Api\AttributeValueFactory; - use Magento\Framework\EntityManager\MetadataPool; - - /** -+ * Configurable product attribute model. -+ * - * @method Attribute setProductAttribute(\Magento\Eav\Model\Entity\Attribute\AbstractAttribute $value) - * @method \Magento\Eav\Model\Entity\Attribute\AbstractAttribute getProductAttribute() - */ - class Attribute extends \Magento\Framework\Model\AbstractExtensibleModel implements - \Magento\ConfigurableProduct\Api\Data\OptionInterface - { -- /**#@+ -+ /** - * Constants for field names - */ - const KEY_ATTRIBUTE_ID = 'attribute_id'; -@@ -27,9 +29,10 @@ class Attribute extends \Magento\Framework\Model\AbstractExtensibleModel impleme - const KEY_IS_USE_DEFAULT = 'is_use_default'; - const KEY_VALUES = 'values'; - const KEY_PRODUCT_ID = 'product_id'; -- /**#@-*/ - -- /**#@-*/ -+ /** -+ * @var MetadataPool|\Magento\Framework\EntityManager\MetadataPool -+ */ - private $metadataPool; - - /** -@@ -85,7 +88,7 @@ class Attribute extends \Magento\Framework\Model\AbstractExtensibleModel impleme - } - - /** -- * {@inheritdoc} -+ * @inheritdoc - */ - public function getLabel() - { -@@ -111,10 +114,10 @@ class Attribute extends \Magento\Framework\Model\AbstractExtensibleModel impleme - } - - /** -- * Load configurable attribute by product and product's attribute -+ * Load configurable attribute by product and product's attribute. - * - * @param \Magento\Catalog\Model\Product $product -- * @param \Magento\Catalog\Model\ResourceModel\Eav\Attribute $attribute -+ * @param \Magento\Catalog\Model\ResourceModel\Eav\Attribute $attribute - * @return void - */ - public function loadByProductAndAttribute($product, $attribute) -@@ -143,7 +146,7 @@ class Attribute extends \Magento\Framework\Model\AbstractExtensibleModel impleme - } - - /** -- * {@inheritdoc} -+ * @inheritdoc - * @codeCoverageIgnore - */ - public function getAttributeId() -@@ -152,7 +155,7 @@ class Attribute extends \Magento\Framework\Model\AbstractExtensibleModel impleme - } - - /** -- * {@inheritdoc} -+ * @inheritdoc - * @codeCoverageIgnore - */ - public function getPosition() -@@ -161,7 +164,7 @@ class Attribute extends \Magento\Framework\Model\AbstractExtensibleModel impleme - } - - /** -- * {@inheritdoc} -+ * @inheritdoc - * @codeCoverageIgnore - */ - public function getIsUseDefault() -@@ -170,7 +173,7 @@ class Attribute extends \Magento\Framework\Model\AbstractExtensibleModel impleme - } - - /** -- * {@inheritdoc} -+ * @inheritdoc - * @codeCoverageIgnore - */ - public function getValues() -@@ -181,8 +184,7 @@ class Attribute extends \Magento\Framework\Model\AbstractExtensibleModel impleme - //@codeCoverageIgnoreStart - - /** -- * @param string $attributeId -- * @return $this -+ * @inheritdoc - */ - public function setAttributeId($attributeId) - { -@@ -190,8 +192,7 @@ class Attribute extends \Magento\Framework\Model\AbstractExtensibleModel impleme - } - - /** -- * @param string $label -- * @return $this -+ * @inheritdoc - */ - public function setLabel($label) - { -@@ -199,8 +200,7 @@ class Attribute extends \Magento\Framework\Model\AbstractExtensibleModel impleme - } - - /** -- * @param int $position -- * @return $this -+ * @inheritdoc - */ - public function setPosition($position) - { -@@ -208,8 +208,7 @@ class Attribute extends \Magento\Framework\Model\AbstractExtensibleModel impleme - } - - /** -- * @param bool $isUseDefault -- * @return $this -+ * @inheritdoc - */ - public function setIsUseDefault($isUseDefault) - { -@@ -217,8 +216,7 @@ class Attribute extends \Magento\Framework\Model\AbstractExtensibleModel impleme - } - - /** -- * @param \Magento\ConfigurableProduct\Api\Data\OptionValueInterface[] $values -- * @return $this -+ * @inheritdoc - */ - public function setValues(array $values = null) - { -@@ -226,7 +224,7 @@ class Attribute extends \Magento\Framework\Model\AbstractExtensibleModel impleme - } - - /** -- * {@inheritdoc} -+ * @inheritdoc - * - * @return \Magento\ConfigurableProduct\Api\Data\OptionExtensionInterface|null - */ -@@ -236,7 +234,7 @@ class Attribute extends \Magento\Framework\Model\AbstractExtensibleModel impleme - } - - /** -- * {@inheritdoc} -+ * @inheritdoc - * - * @param \Magento\ConfigurableProduct\Api\Data\OptionExtensionInterface $extensionAttributes - * @return $this -@@ -248,7 +246,7 @@ class Attribute extends \Magento\Framework\Model\AbstractExtensibleModel impleme - } - - /** -- * {@inheritdoc} -+ * @inheritdoc - */ - public function getProductId() - { -@@ -256,7 +254,7 @@ class Attribute extends \Magento\Framework\Model\AbstractExtensibleModel impleme - } - - /** -- * {@inheritdoc} -+ * @inheritdoc - */ - public function setProductId($value) - { -@@ -267,9 +265,14 @@ class Attribute extends \Magento\Framework\Model\AbstractExtensibleModel impleme - - /** - * @inheritdoc -+ * -+ * @SuppressWarnings(PHPMD.SerializationAware) -+ * @deprecated Do not use PHP serialization. - */ - public function __sleep() - { -+ trigger_error('Using PHP serialization is deprecated', E_USER_DEPRECATED); -+ - return array_diff( - parent::__sleep(), - ['metadataPool'] -@@ -278,9 +281,14 @@ class Attribute extends \Magento\Framework\Model\AbstractExtensibleModel impleme - - /** - * @inheritdoc -+ * -+ * @SuppressWarnings(PHPMD.SerializationAware) -+ * @deprecated Do not use PHP serialization. - */ - public function __wakeup() - { -+ trigger_error('Using PHP serialization is deprecated', E_USER_DEPRECATED); -+ - parent::__wakeup(); - $objectManager = \Magento\Framework\App\ObjectManager::getInstance(); - $this->metadataPool = $objectManager->get(MetadataPool::class); -diff --git a/app/code/Magento/ConfigurableProduct/Model/Product/Type/Configurable/Price.php b/app/code/Magento/ConfigurableProduct/Model/Product/Type/Configurable/Price.php -index bee334596e9..f2bf3116af9 100644 ---- a/app/code/Magento/ConfigurableProduct/Model/Product/Type/Configurable/Price.php -+++ b/app/code/Magento/ConfigurableProduct/Model/Product/Type/Configurable/Price.php -@@ -7,14 +7,15 @@ - */ - namespace Magento\ConfigurableProduct\Model\Product\Type\Configurable; - -+use Magento\Catalog\Model\Product; -+ -+/** -+ * Class Price for configurable product -+ */ - class Price extends \Magento\Catalog\Model\Product\Type\Price - { - /** -- * Get product final price -- * -- * @param float $qty -- * @param \Magento\Catalog\Model\Product $product -- * @return float -+ * @inheritdoc - */ - public function getFinalPrice($qty, $product) - { -@@ -22,7 +23,10 @@ class Price extends \Magento\Catalog\Model\Product\Type\Price - return $product->getCalculatedFinalPrice(); - } - if ($product->getCustomOption('simple_product') && $product->getCustomOption('simple_product')->getProduct()) { -- $finalPrice = parent::getFinalPrice($qty, $product->getCustomOption('simple_product')->getProduct()); -+ /** @var Product $simpleProduct */ -+ $simpleProduct = $product->getCustomOption('simple_product')->getProduct(); -+ $simpleProduct->setCustomerGroupId($product->getCustomerGroupId()); -+ $finalPrice = parent::getFinalPrice($qty, $simpleProduct); - } else { - $priceInfo = $product->getPriceInfo(); - $finalPrice = $priceInfo->getPrice('final_price')->getAmount()->getValue(); -@@ -35,7 +39,7 @@ class Price extends \Magento\Catalog\Model\Product\Type\Price - } - - /** -- * {@inheritdoc} -+ * @inheritdoc - */ - public function getPrice($product) - { -@@ -48,6 +52,7 @@ class Price extends \Magento\Catalog\Model\Product\Type\Price - } - } - } -+ - return 0; - } - } -diff --git a/app/code/Magento/ConfigurableProduct/Model/Product/Type/Plugin.php b/app/code/Magento/ConfigurableProduct/Model/Product/Type/Plugin.php -index 16fff360632..e8b7299a03d 100644 ---- a/app/code/Magento/ConfigurableProduct/Model/Product/Type/Plugin.php -+++ b/app/code/Magento/ConfigurableProduct/Model/Product/Type/Plugin.php -@@ -6,19 +6,22 @@ - */ - namespace Magento\ConfigurableProduct\Model\Product\Type; - --use Magento\Framework\Module\Manager; -+use Magento\Framework\Module\ModuleManagerInterface; - -+/** -+ * Type plugin. -+ */ - class Plugin - { - /** -- * @var \Magento\Framework\Module\Manager -+ * @var \Magento\Framework\Module\ModuleManagerInterface - */ - protected $moduleManager; - - /** -- * @param Manager $moduleManager -+ * @param ModuleManagerInterface $moduleManager - */ -- public function __construct(Manager $moduleManager) -+ public function __construct(ModuleManagerInterface $moduleManager) - { - $this->moduleManager = $moduleManager; - } -diff --git a/app/code/Magento/ConfigurableProduct/Model/Product/VariationHandler.php b/app/code/Magento/ConfigurableProduct/Model/Product/VariationHandler.php -index 73e7f9053fa..09d25151926 100644 ---- a/app/code/Magento/ConfigurableProduct/Model/Product/VariationHandler.php -+++ b/app/code/Magento/ConfigurableProduct/Model/Product/VariationHandler.php -@@ -3,6 +3,7 @@ - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -+declare(strict_types=1); - - namespace Magento\ConfigurableProduct\Model\Product; - -@@ -201,10 +202,11 @@ class VariationHandler - - $keysFilter = ['item_id', 'product_id', 'stock_id', 'type_id', 'website_id']; - $postData['stock_data'] = array_diff_key((array)$parentProduct->getStockData(), array_flip($keysFilter)); -- if (!isset($postData['stock_data']['is_in_stock'])) { -- $stockStatus = $parentProduct->getQuantityAndStockStatus(); -+ $stockStatus = $parentProduct->getQuantityAndStockStatus(); -+ if (isset($stockStatus['is_in_stock'])) { - $postData['stock_data']['is_in_stock'] = $stockStatus['is_in_stock']; - } -+ - $postData = $this->processMediaGallery($product, $postData); - $postData['status'] = isset($postData['status']) - ? $postData['status'] -@@ -262,6 +264,8 @@ class VariationHandler - } - - /** -+ * Process media gallery for product -+ * - * @param \Magento\Catalog\Model\Product $product - * @param array $productData - * -diff --git a/app/code/Magento/ConfigurableProduct/Model/ResourceModel/Attribute/OptionSelectBuilder.php b/app/code/Magento/ConfigurableProduct/Model/ResourceModel/Attribute/OptionSelectBuilder.php -index 5d9eed0a188..8fbab8142fe 100644 ---- a/app/code/Magento/ConfigurableProduct/Model/ResourceModel/Attribute/OptionSelectBuilder.php -+++ b/app/code/Magento/ConfigurableProduct/Model/ResourceModel/Attribute/OptionSelectBuilder.php -@@ -40,7 +40,7 @@ class OptionSelectBuilder implements OptionSelectBuilderInterface - } - - /** -- * {@inheritdoc} -+ * @inheritdoc - */ - public function getSelect(AbstractAttribute $superAttribute, int $productId, ScopeInterface $scope) - { -@@ -91,7 +91,7 @@ class OptionSelectBuilder implements OptionSelectBuilderInterface - ] - ), - [] -- )->joinInner( -+ )->joinLeft( - ['attribute_option' => $this->attributeResource->getTable('eav_attribute_option')], - 'attribute_option.option_id = entity_value.value', - [] -diff --git a/app/code/Magento/ConfigurableProduct/Model/ResourceModel/Product/Indexer/Price/Configurable.php b/app/code/Magento/ConfigurableProduct/Model/ResourceModel/Product/Indexer/Price/Configurable.php -index 81e2e99bfe9..b7bbf7aa187 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\Indexer\Product\Price\TableMaintainer; - 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 @@ class Configurable implements DimensionalIndexerInterface - \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 @@ class Configurable implements DimensionalIndexerInterface - $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 @@ class Configurable implements DimensionalIndexerInterface - ['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 @@ class Configurable implements DimensionalIndexerInterface - /** - * 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 @@ class Configurable implements DimensionalIndexerInterface - { - return $this->resource->getTableName($tableName, $this->connectionName); - } -+ -+ /** -+ * Is flag Show Out Of Stock setted -+ * -+ * @return bool -+ */ -+ private function isConfigShowOutOfStock(): bool -+ { -+ return $this->scopeConfig->isSetFlag( -+ Configuration::XML_PATH_SHOW_OUT_OF_STOCK, -+ ScopeInterface::SCOPE_STORE -+ ); -+ } - } -diff --git a/app/code/Magento/ConfigurableProduct/Model/ResourceModel/Product/LinkedProductSelectBuilderComposite.php b/app/code/Magento/ConfigurableProduct/Model/ResourceModel/Product/LinkedProductSelectBuilderComposite.php -new file mode 100644 -index 00000000000..59a7b81e068 ---- /dev/null -+++ b/app/code/Magento/ConfigurableProduct/Model/ResourceModel/Product/LinkedProductSelectBuilderComposite.php -@@ -0,0 +1,45 @@ -+<?php -+/** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+declare(strict_types=1); -+ -+namespace Magento\ConfigurableProduct\Model\ResourceModel\Product; -+ -+use Magento\Catalog\Model\ResourceModel\Product\LinkedProductSelectBuilderInterface; -+ -+/** -+ * Used in Magento\ConfigurableProduct\Pricing\Price\LowestPriceOptionsProvider -+ * to provide queries to select configurable product option with lowest price -+ * -+ * @see app/code/Magento/ConfigurableProduct/etc/di.xml -+ */ -+class LinkedProductSelectBuilderComposite implements LinkedProductSelectBuilderInterface -+{ -+ /** -+ * @var LinkedProductSelectBuilderInterface[] -+ */ -+ private $linkedProductSelectBuilder; -+ -+ /** -+ * @param LinkedProductSelectBuilderInterface[] $linkedProductSelectBuilder -+ */ -+ public function __construct($linkedProductSelectBuilder) -+ { -+ $this->linkedProductSelectBuilder = $linkedProductSelectBuilder; -+ } -+ -+ /** -+ * {@inheritdoc} -+ */ -+ public function build($productId) -+ { -+ $selects = []; -+ foreach ($this->linkedProductSelectBuilder as $productSelectBuilder) { -+ $selects = array_merge($selects, $productSelectBuilder->build($productId)); -+ } -+ -+ return $selects; -+ } -+} -diff --git a/app/code/Magento/ConfigurableProduct/Model/ResourceModel/Product/Type/Configurable.php b/app/code/Magento/ConfigurableProduct/Model/ResourceModel/Product/Type/Configurable.php -index ccff85dd971..feffd22a0fb 100644 ---- a/app/code/Magento/ConfigurableProduct/Model/ResourceModel/Product/Type/Configurable.php -+++ b/app/code/Magento/ConfigurableProduct/Model/ResourceModel/Product/Type/Configurable.php -@@ -19,6 +19,9 @@ use Magento\Framework\App\ScopeResolverInterface; - use Magento\Framework\App\ObjectManager; - use Magento\Framework\DB\Adapter\AdapterInterface; - -+/** -+ * Configurable product resource model. -+ */ - class Configurable extends \Magento\Framework\Model\ResourceModel\Db\AbstractDb - { - /** -@@ -173,10 +176,13 @@ class Configurable extends \Magento\Framework\Model\ResourceModel\Db\AbstractDb - $parentId - ); - -- $childrenIds = [0 => []]; -- foreach ($this->getConnection()->fetchAll($select) as $row) { -- $childrenIds[0][$row['product_id']] = $row['product_id']; -- } -+ $childrenIds = [ -+ 0 => array_column( -+ $this->getConnection()->fetchAll($select), -+ 'product_id', -+ 'product_id' -+ ) -+ ]; - - return $childrenIds; - } -diff --git a/app/code/Magento/ConfigurableProduct/Model/ResourceModel/Product/Type/Configurable/Attribute.php b/app/code/Magento/ConfigurableProduct/Model/ResourceModel/Product/Type/Configurable/Attribute.php -index e93c44893bf..1b5ea4d020f 100644 ---- a/app/code/Magento/ConfigurableProduct/Model/ResourceModel/Product/Type/Configurable/Attribute.php -+++ b/app/code/Magento/ConfigurableProduct/Model/ResourceModel/Product/Type/Configurable/Attribute.php -@@ -1,7 +1,5 @@ - <?php - /** -- * Catalog super product attribute resource model -- * - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -@@ -11,6 +9,9 @@ use Magento\ConfigurableProduct\Model\Product\Type\Configurable\Attribute as Con - use Magento\Framework\DB\Adapter\AdapterInterface; - use Magento\Store\Model\Store; - -+/** -+ * Catalog super product attribute resource model. -+ */ - class Attribute extends \Magento\Framework\Model\ResourceModel\Db\AbstractDb - { - /** -@@ -52,7 +53,7 @@ class Attribute extends \Magento\Framework\Model\ResourceModel\Db\AbstractDb - } - - /** -- * Inititalize connection and define tables -+ * Initialize connection and define tables - * - * @return void - */ -@@ -189,8 +190,7 @@ class Attribute extends \Magento\Framework\Model\ResourceModel\Db\AbstractDb - } - - /** -- * @param \Magento\Framework\Model\AbstractModel $object -- * @return $this -+ * @inheritDoc - */ - protected function _afterLoad(\Magento\Framework\Model\AbstractModel $object) - { -diff --git a/app/code/Magento/ConfigurableProduct/Model/ResourceModel/Product/Type/Configurable/Attribute/Collection.php b/app/code/Magento/ConfigurableProduct/Model/ResourceModel/Product/Type/Configurable/Attribute/Collection.php -index 3c40d326be7..81cbbd06c52 100644 ---- a/app/code/Magento/ConfigurableProduct/Model/ResourceModel/Product/Type/Configurable/Attribute/Collection.php -+++ b/app/code/Magento/ConfigurableProduct/Model/ResourceModel/Product/Type/Configurable/Attribute/Collection.php -@@ -18,6 +18,8 @@ use Magento\Framework\EntityManager\MetadataPool; - use Magento\Catalog\Api\Data\ProductInterface; - - /** -+ * Collection of configurable product attributes. -+ * - * @api - * @SuppressWarnings(PHPMD.LongVariable) - * @SuppressWarnings(PHPMD.CouplingBetweenObjects) -@@ -302,6 +304,8 @@ class Collection extends \Magento\Framework\Model\ResourceModel\Db\Collection\Ab - } - - /** -+ * Load related options' data. -+ * - * @return void - */ - protected function loadOptions() -@@ -354,9 +358,14 @@ class Collection extends \Magento\Framework\Model\ResourceModel\Db\Collection\Ab - /** - * @inheritdoc - * @since 100.0.6 -+ * -+ * @SuppressWarnings(PHPMD.SerializationAware) -+ * @deprecated Do not use PHP serialization. - */ - public function __sleep() - { -+ trigger_error('Using PHP serialization is deprecated', E_USER_DEPRECATED); -+ - return array_diff( - parent::__sleep(), - [ -@@ -373,9 +382,14 @@ class Collection extends \Magento\Framework\Model\ResourceModel\Db\Collection\Ab - /** - * @inheritdoc - * @since 100.0.6 -+ * -+ * @SuppressWarnings(PHPMD.SerializationAware) -+ * @deprecated Do not use PHP serialization. - */ - public function __wakeup() - { -+ trigger_error('Using PHP serialization is deprecated', E_USER_DEPRECATED); -+ - parent::__wakeup(); - $objectManager = ObjectManager::getInstance(); - $this->_storeManager = $objectManager->get(\Magento\Store\Model\StoreManagerInterface::class); -diff --git a/app/code/Magento/ConfigurableProduct/Model/ResourceModel/Product/Type/Configurable/Product/Collection.php b/app/code/Magento/ConfigurableProduct/Model/ResourceModel/Product/Type/Configurable/Product/Collection.php -index 1460cc51681..b76954075bc 100644 ---- a/app/code/Magento/ConfigurableProduct/Model/ResourceModel/Product/Type/Configurable/Product/Collection.php -+++ b/app/code/Magento/ConfigurableProduct/Model/ResourceModel/Product/Type/Configurable/Product/Collection.php -@@ -12,6 +12,7 @@ namespace Magento\ConfigurableProduct\Model\ResourceModel\Product\Type\Configura - * - * @api - * @SuppressWarnings(PHPMD.CouplingBetweenObjects) -+ * @SuppressWarnings(PHPMD.CookieAndSessionMisuse) - * @since 100.0.2 - */ - class Collection extends \Magento\Catalog\Model\ResourceModel\Product\Collection -@@ -26,7 +27,7 @@ class Collection extends \Magento\Catalog\Model\ResourceModel\Product\Collection - /** - * @var \Magento\Catalog\Model\Product[] - */ -- private $products; -+ private $products = []; - - /** - * Assign link table name -@@ -41,6 +42,7 @@ class Collection extends \Magento\Catalog\Model\ResourceModel\Product\Collection - - /** - * Init select -+ * - * @return $this|\Magento\ConfigurableProduct\Model\ResourceModel\Product\Type\Configurable\Product\Collection - */ - protected function _initSelect() -@@ -73,9 +75,9 @@ class Collection extends \Magento\Catalog\Model\ResourceModel\Product\Collection - * - * @return $this - */ -- protected function _beforeLoad() -+ protected function _renderFilters() - { -- parent::_beforeLoad(); -+ parent::_renderFilters(); - $metadata = $this->getProductEntityMetadata(); - $parentIds = []; - foreach ($this->products as $product) { -@@ -88,8 +90,7 @@ class Collection extends \Magento\Catalog\Model\ResourceModel\Product\Collection - } - - /** -- * Retrieve is flat enabled flag -- * Return always false if magento run admin -+ * Retrieve is flat enabled flag. Return always false if magento run admin - * - * @return bool - */ -diff --git a/app/code/Magento/ConfigurableProduct/Plugin/Catalog/Model/Product/Pricing/Renderer/SalableResolver.php b/app/code/Magento/ConfigurableProduct/Plugin/Catalog/Model/Product/Pricing/Renderer/SalableResolver.php -index efddb278df3..c828c0929b4 100644 ---- a/app/code/Magento/ConfigurableProduct/Plugin/Catalog/Model/Product/Pricing/Renderer/SalableResolver.php -+++ b/app/code/Magento/ConfigurableProduct/Plugin/Catalog/Model/Product/Pricing/Renderer/SalableResolver.php -@@ -5,7 +5,7 @@ - */ - namespace Magento\ConfigurableProduct\Plugin\Catalog\Model\Product\Pricing\Renderer; - --use Magento\ConfigurableProduct\Pricing\Price\LowestPriceOptionsProviderInterface; -+use Magento\ConfigurableProduct\Model\Product\Type\Configurable as TypeConfigurable; - - /** - * A plugin for a salable resolver. -@@ -13,29 +13,25 @@ use Magento\ConfigurableProduct\Pricing\Price\LowestPriceOptionsProviderInterfac - class SalableResolver - { - /** -- * @var LowestPriceOptionsProviderInterface -+ * @var TypeConfigurable - */ -- private $lowestPriceOptionsProvider; -+ private $typeConfigurable; - - /** -- * @param LowestPriceOptionsProviderInterface $lowestPriceOptionsProvider -+ * @param TypeConfigurable $typeConfigurable - */ -- public function __construct( -- LowestPriceOptionsProviderInterface $lowestPriceOptionsProvider -- ) { -- $this->lowestPriceOptionsProvider = $lowestPriceOptionsProvider; -+ public function __construct(TypeConfigurable $typeConfigurable) -+ { -+ $this->typeConfigurable = $typeConfigurable; - } - - /** -- * Performs an additional check whether given configurable product has -- * at least one configuration in-stock. -+ * Performs an additional check whether given configurable product has at least one configuration in-stock. - * - * @param \Magento\Catalog\Model\Product\Pricing\Renderer\SalableResolver $subject - * @param bool $result - * @param \Magento\Framework\Pricing\SaleableInterface $salableItem -- * - * @return bool -- * - * @SuppressWarnings(PHPMD.UnusedFormalParameter) - */ - public function afterIsSalable( -@@ -43,10 +39,8 @@ class SalableResolver - $result, - \Magento\Framework\Pricing\SaleableInterface $salableItem - ) { -- if ($salableItem->getTypeId() == 'configurable' && $result) { -- if (!$this->lowestPriceOptionsProvider->getProducts($salableItem)) { -- $result = false; -- } -+ if ($salableItem->getTypeId() === TypeConfigurable::TYPE_CODE && $result) { -+ $result = $this->typeConfigurable->isSalable($salableItem); - } - - return $result; -diff --git a/app/code/Magento/ConfigurableProduct/Plugin/SalesRule/Model/Rule/Condition/Product.php b/app/code/Magento/ConfigurableProduct/Plugin/SalesRule/Model/Rule/Condition/Product.php -new file mode 100644 -index 00000000000..1ed4432347b ---- /dev/null -+++ b/app/code/Magento/ConfigurableProduct/Plugin/SalesRule/Model/Rule/Condition/Product.php -@@ -0,0 +1,69 @@ -+<?php -+/** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+declare(strict_types=1); -+ -+namespace Magento\ConfigurableProduct\Plugin\SalesRule\Model\Rule\Condition; -+ -+use Magento\ConfigurableProduct\Model\Product\Type\Configurable; -+ -+/** -+ * Class Product -+ * -+ * @package Magento\ConfigurableProduct\Plugin\SalesRule\Model\Rule\Condition -+ */ -+class Product -+{ -+ /** -+ * Prepare configurable product for validation. -+ * -+ * @param \Magento\SalesRule\Model\Rule\Condition\Product $subject -+ * @param \Magento\Framework\Model\AbstractModel $model -+ * @return array -+ */ -+ public function beforeValidate( -+ \Magento\SalesRule\Model\Rule\Condition\Product $subject, -+ \Magento\Framework\Model\AbstractModel $model -+ ) { -+ $product = $this->getProductToValidate($subject, $model); -+ if ($model->getProduct() !== $product) { -+ // We need to replace product only for validation and keep original product for all other cases. -+ $clone = clone $model; -+ $clone->setProduct($product); -+ $model = $clone; -+ } -+ -+ return [$model]; -+ } -+ -+ /** -+ * Select proper product for validation. -+ * -+ * @param \Magento\SalesRule\Model\Rule\Condition\Product $subject -+ * @param \Magento\Framework\Model\AbstractModel $model -+ * -+ * @return \Magento\Catalog\Api\Data\ProductInterface|\Magento\Catalog\Model\Product -+ */ -+ private function getProductToValidate( -+ \Magento\SalesRule\Model\Rule\Condition\Product $subject, -+ \Magento\Framework\Model\AbstractModel $model -+ ) { -+ /** @var \Magento\Catalog\Model\Product $product */ -+ $product = $model->getProduct(); -+ -+ $attrCode = $subject->getAttribute(); -+ -+ /* Check for attributes which are not available for configurable products */ -+ if ($product->getTypeId() == Configurable::TYPE_CODE && !$product->hasData($attrCode)) { -+ /** @var \Magento\Catalog\Model\AbstractModel $childProduct */ -+ $childProduct = current($model->getChildren())->getProduct(); -+ if ($childProduct->hasData($attrCode)) { -+ $product = $childProduct; -+ } -+ } -+ -+ return $product; -+ } -+} -diff --git a/app/code/Magento/ConfigurableProduct/Plugin/Tax/Model/Sales/Total/Quote/CommonTaxCollector.php b/app/code/Magento/ConfigurableProduct/Plugin/Tax/Model/Sales/Total/Quote/CommonTaxCollector.php -new file mode 100644 -index 00000000000..8bdde2aeb0c ---- /dev/null -+++ b/app/code/Magento/ConfigurableProduct/Plugin/Tax/Model/Sales/Total/Quote/CommonTaxCollector.php -@@ -0,0 +1,44 @@ -+<?php -+/** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+declare(strict_types=1); -+ -+namespace Magento\ConfigurableProduct\Plugin\Tax\Model\Sales\Total\Quote; -+ -+use Magento\ConfigurableProduct\Model\Product\Type\Configurable; -+use Magento\Quote\Model\Quote\Item\AbstractItem; -+use Magento\Tax\Api\Data\QuoteDetailsItemInterface; -+use Magento\Tax\Api\Data\QuoteDetailsItemInterfaceFactory; -+ -+/** -+ * Plugin for CommonTaxCollector to apply Tax Class ID from child item for configurable product -+ */ -+class CommonTaxCollector -+{ -+ /** -+ * Apply Tax Class ID from child item for configurable product -+ * -+ * @param \Magento\Tax\Model\Sales\Total\Quote\CommonTaxCollector $subject -+ * @param QuoteDetailsItemInterface $result -+ * @param QuoteDetailsItemInterfaceFactory $itemDataObjectFactory -+ * @param AbstractItem $item -+ * @return QuoteDetailsItemInterface -+ * -+ * @SuppressWarnings(PHPMD.UnusedFormalParameter) -+ */ -+ public function afterMapItem( -+ \Magento\Tax\Model\Sales\Total\Quote\CommonTaxCollector $subject, -+ QuoteDetailsItemInterface $result, -+ QuoteDetailsItemInterfaceFactory $itemDataObjectFactory, -+ AbstractItem $item -+ ) : QuoteDetailsItemInterface { -+ if ($item->getProduct()->getTypeId() === Configurable::TYPE_CODE && $item->getHasChildren()) { -+ $childItem = $item->getChildren()[0]; -+ $result->getTaxClassKey()->setValue($childItem->getProduct()->getTaxClassId()); -+ } -+ -+ return $result; -+ } -+} -diff --git a/app/code/Magento/ConfigurableProduct/Pricing/Render/TierPriceBox.php b/app/code/Magento/ConfigurableProduct/Pricing/Render/TierPriceBox.php -index 611523a60b0..447ba16d727 100644 ---- a/app/code/Magento/ConfigurableProduct/Pricing/Render/TierPriceBox.php -+++ b/app/code/Magento/ConfigurableProduct/Pricing/Render/TierPriceBox.php -@@ -5,6 +5,8 @@ - */ - namespace Magento\ConfigurableProduct\Pricing\Render; - -+use Magento\Catalog\Pricing\Price\TierPrice; -+ - /** - * Responsible for displaying tier price box on configurable product page. - * -@@ -17,9 +19,27 @@ class TierPriceBox extends FinalPriceBox - */ - public function toHtml() - { -- // Hide tier price block in case of MSRP. -- if (!$this->isMsrpPriceApplicable()) { -+ // Hide tier price block in case of MSRP or in case when no options with tier price. -+ if (!$this->isMsrpPriceApplicable() && $this->isTierPriceApplicable()) { - return parent::toHtml(); - } - } -+ -+ /** -+ * Check if at least one of simple products has tier price. -+ * -+ * @return bool -+ */ -+ private function isTierPriceApplicable() -+ { -+ $product = $this->getSaleableItem(); -+ foreach ($product->getTypeInstance()->getUsedProducts($product) as $simpleProduct) { -+ if ($simpleProduct->isSalable() && -+ !empty($simpleProduct->getPriceInfo()->getPrice(TierPrice::PRICE_CODE)->getTierPriceList()) -+ ) { -+ return true; -+ } -+ } -+ return false; -+ } - } -diff --git a/app/code/Magento/ConfigurableProduct/Setup/Patch/Data/InstallInitialConfigurableAttributes.php b/app/code/Magento/ConfigurableProduct/Setup/Patch/Data/InstallInitialConfigurableAttributes.php -index f69d8529fb8..c6b173453f5 100644 ---- a/app/code/Magento/ConfigurableProduct/Setup/Patch/Data/InstallInitialConfigurableAttributes.php -+++ b/app/code/Magento/ConfigurableProduct/Setup/Patch/Data/InstallInitialConfigurableAttributes.php -@@ -6,16 +6,16 @@ - - namespace Magento\ConfigurableProduct\Setup\Patch\Data; - -+use Magento\ConfigurableProduct\Model\Product\Type\Configurable; - use Magento\Eav\Setup\EavSetup; - use Magento\Eav\Setup\EavSetupFactory; --use Magento\Framework\App\ResourceConnection; - use Magento\Framework\Setup\ModuleDataSetupInterface; - use Magento\Framework\Setup\Patch\DataPatchInterface; - use Magento\Framework\Setup\Patch\PatchVersionInterface; --use Magento\ConfigurableProduct\Model\Product\Type\Configurable; - - /** - * Class InstallInitialConfigurableAttributes -+ * - * @package Magento\ConfigurableProduct\Setup\Patch - */ - class InstallInitialConfigurableAttributes implements DataPatchInterface, PatchVersionInterface -@@ -24,6 +24,7 @@ class InstallInitialConfigurableAttributes implements DataPatchInterface, PatchV - * @var ModuleDataSetupInterface - */ - private $moduleDataSetup; -+ - /** - * @var EavSetupFactory - */ -@@ -43,7 +44,7 @@ class InstallInitialConfigurableAttributes implements DataPatchInterface, PatchV - } - - /** -- * {@inheritdoc} -+ * @inheritdoc - */ - public function apply() - { -@@ -64,24 +65,27 @@ class InstallInitialConfigurableAttributes implements DataPatchInterface, PatchV - 'color' - ]; - foreach ($attributes as $attributeCode) { -- $relatedProductTypes = explode( -- ',', -- $eavSetup->getAttribute(\Magento\Catalog\Model\Product::ENTITY, $attributeCode, 'apply_to') -- ); -- if (!in_array(Configurable::TYPE_CODE, $relatedProductTypes)) { -- $relatedProductTypes[] = Configurable::TYPE_CODE; -- $eavSetup->updateAttribute( -- \Magento\Catalog\Model\Product::ENTITY, -- $attributeCode, -- 'apply_to', -- implode(',', $relatedProductTypes) -+ $attribute = $eavSetup->getAttribute(\Magento\Catalog\Model\Product::ENTITY, $attributeCode, 'apply_to'); -+ if ($attribute) { -+ $relatedProductTypes = explode( -+ ',', -+ $attribute - ); -+ if (!in_array(Configurable::TYPE_CODE, $relatedProductTypes)) { -+ $relatedProductTypes[] = Configurable::TYPE_CODE; -+ $eavSetup->updateAttribute( -+ \Magento\Catalog\Model\Product::ENTITY, -+ $attributeCode, -+ 'apply_to', -+ implode(',', $relatedProductTypes) -+ ); -+ } - } - } - } - - /** -- * {@inheritdoc} -+ * @inheritdoc - */ - public static function getDependencies() - { -@@ -89,7 +93,7 @@ class InstallInitialConfigurableAttributes implements DataPatchInterface, PatchV - } - - /** -- * {@inheritdoc} -+ * @inheritdoc - */ - public static function getVersion() - { -@@ -97,7 +101,7 @@ class InstallInitialConfigurableAttributes implements DataPatchInterface, PatchV - } - - /** -- * {@inheritdoc} -+ * @inheritdoc - */ - public function getAliases() - { -diff --git a/app/code/Magento/ConfigurableProduct/Setup/Patch/Data/UpdateManufacturerAttribute.php b/app/code/Magento/ConfigurableProduct/Setup/Patch/Data/UpdateManufacturerAttribute.php -index c9a2f7d373a..1e085e0fdb3 100644 ---- a/app/code/Magento/ConfigurableProduct/Setup/Patch/Data/UpdateManufacturerAttribute.php -+++ b/app/code/Magento/ConfigurableProduct/Setup/Patch/Data/UpdateManufacturerAttribute.php -@@ -15,8 +15,7 @@ use Magento\Framework\Setup\Patch\PatchVersionInterface; - use Magento\ConfigurableProduct\Model\Product\Type\Configurable; - - /** -- * Class UpdateManufacturerAttribute -- * @package Magento\ConfigurableProduct\Setup\Patch -+ * Update manufacturer attribute if it's presented in system. - */ - class UpdateManufacturerAttribute implements DataPatchInterface, PatchVersionInterface - { -@@ -31,7 +30,6 @@ class UpdateManufacturerAttribute implements DataPatchInterface, PatchVersionInt - private $eavSetupFactory; - - /** -- * UpdateTierPriceAttribute constructor. - * @param ModuleDataSetupInterface $moduleDataSetup - * @param EavSetupFactory $eavSetupFactory - */ -@@ -44,30 +42,37 @@ class UpdateManufacturerAttribute implements DataPatchInterface, PatchVersionInt - } - - /** -- * {@inheritdoc} -+ * @inheritdoc - */ - public function apply() - { - /** @var EavSetup $eavSetup */ - $eavSetup = $this->eavSetupFactory->create(['setup' => $this->moduleDataSetup]); -- $relatedProductTypes = explode( -- ',', -- $eavSetup->getAttribute(\Magento\Catalog\Model\Product::ENTITY, 'manufacturer', 'apply_to') -- ); - -- if (!in_array(Configurable::TYPE_CODE, $relatedProductTypes)) { -- $relatedProductTypes[] = Configurable::TYPE_CODE; -- $eavSetup->updateAttribute( -- \Magento\Catalog\Model\Product::ENTITY, -- 'manufacturer', -- 'apply_to', -- implode(',', $relatedProductTypes) -+ if ($manufacturer = $eavSetup->getAttribute( -+ \Magento\Catalog\Model\Product::ENTITY, -+ 'manufacturer', -+ 'apply_to' -+ )) { -+ $relatedProductTypes = explode( -+ ',', -+ $manufacturer - ); -+ -+ if (!in_array(Configurable::TYPE_CODE, $relatedProductTypes)) { -+ $relatedProductTypes[] = Configurable::TYPE_CODE; -+ $eavSetup->updateAttribute( -+ \Magento\Catalog\Model\Product::ENTITY, -+ 'manufacturer', -+ 'apply_to', -+ implode(',', $relatedProductTypes) -+ ); -+ } - } - } - - /** -- * {@inheritdoc} -+ * @inheritdoc - */ - public static function getDependencies() - { -@@ -77,7 +82,7 @@ class UpdateManufacturerAttribute implements DataPatchInterface, PatchVersionInt - } - - /** -- * {@inheritdoc}\ -+ * @inheritdoc - */ - public static function getVersion() - { -@@ -85,7 +90,7 @@ class UpdateManufacturerAttribute implements DataPatchInterface, PatchVersionInt - } - - /** -- * {@inheritdoc} -+ * @inheritdoc - */ - public function getAliases() - { -diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/ActionGroup/AdminAddOptionsToAttributeActionGroup.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/ActionGroup/AdminAddOptionsToAttributeActionGroup.xml -new file mode 100644 -index 00000000000..4328159d6e9 ---- /dev/null -+++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/ActionGroup/AdminAddOptionsToAttributeActionGroup.xml -@@ -0,0 +1,44 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> -+ <actionGroup name="addOptionsToAttributeActionGroup"> -+ <arguments> -+ <argument name="option1" defaultValue="colorProductAttribute2"/> -+ <argument name="option2" defaultValue="colorDefaultProductAttribute1"/> -+ <argument name="option3" defaultValue="colorProductAttribute3"/> -+ <argument name="option4" defaultValue="colorProductAttribute1"/> -+ <argument name="option5" defaultValue="colorDefaultProductAttribute2"/> -+ </arguments> -+ <!--Add option 1 to attribute--> -+ <click selector="{{AdminNewAttributePanel.addOption}}" stepKey="clickAddOption1"/> -+ <waitForElementVisible selector="{{AdminNewAttributePanel.isDefault('1')}}" time="30" stepKey="waitForOptionRow1" after="clickAddOption1"/> -+ <fillField selector="{{AdminNewAttributePanel.optionAdminValue('0')}}" userInput="{{option1.name}}" stepKey="fillAdminLabel1" after="waitForOptionRow1"/> -+ <!--Add option 2 to attribute--> -+ <click selector="{{AdminNewAttributePanel.addOption}}" stepKey="clickAddOption2" after="fillAdminLabel1"/> -+ <waitForElementVisible selector="{{AdminNewAttributePanel.isDefault('2')}}" time="30" stepKey="waitForOptionRow2" after="clickAddOption2"/> -+ <fillField selector="{{AdminNewAttributePanel.optionAdminValue('1')}}" userInput="{{option2.name}}" stepKey="fillAdminLabel2" after="waitForOptionRow2"/> -+ <!--Add option 3 to attribute--> -+ <click selector="{{AdminNewAttributePanel.addOption}}" stepKey="clickAddOption3" after="fillAdminLabel2"/> -+ <waitForElementVisible selector="{{AdminNewAttributePanel.isDefault('3')}}" time="30" stepKey="waitForOptionRow3" after="clickAddOption3"/> -+ <fillField selector="{{AdminNewAttributePanel.optionAdminValue('2')}}" userInput="{{option3.name}}" stepKey="fillAdminLabel3" after="waitForOptionRow3"/> -+ <!--Add option 4 to attribute--> -+ <click selector="{{AdminNewAttributePanel.addOption}}" stepKey="clickAddOption4" after="fillAdminLabel3"/> -+ <waitForElementVisible selector="{{AdminNewAttributePanel.isDefault('4')}}" time="30" stepKey="waitForOptionRow4" after="clickAddOption4"/> -+ <fillField selector="{{AdminNewAttributePanel.optionAdminValue('3')}}" userInput="{{option4.name}}" stepKey="fillAdminLabel4" after="waitForOptionRow4"/> -+ <!--Add option 5 to attribute--> -+ <click selector="{{AdminNewAttributePanel.addOption}}" stepKey="clickAddOption5" after="fillAdminLabel4"/> -+ <waitForElementVisible selector="{{AdminNewAttributePanel.isDefault('5')}}" time="30" stepKey="waitForOptionRow5" after="clickAddOption5"/> -+ <fillField selector="{{AdminNewAttributePanel.optionAdminValue('4')}}" userInput="{{option5.name}}" stepKey="fillAdminLabel5" after="waitForOptionRow5"/> -+ <!--Save attribute--> -+ <click selector="{{AdminNewAttributePanel.saveAttribute}}" stepKey="clickSaveAttribute" after="fillAdminLabel5"/> -+ <waitForPageLoad stepKey="waitForSavingAttribute"/> -+ <see userInput="You saved the product attribute." stepKey="seeSuccessMessage"/> -+ </actionGroup> -+</actionGroups> -diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/ActionGroup/AdminConfigurableProductActionGroup.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/ActionGroup/AdminConfigurableProductActionGroup.xml -index 63ef6cb99f8..5efc6e5601b 100644 ---- a/app/code/Magento/ConfigurableProduct/Test/Mftf/ActionGroup/AdminConfigurableProductActionGroup.xml -+++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/ActionGroup/AdminConfigurableProductActionGroup.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"> - <!--Filter the product grid and view expected products--> - <actionGroup name="viewConfigurableProductInAdminGrid"> - <arguments> -@@ -60,6 +60,7 @@ - <fillField userInput="{{product.price}}" selector="{{AdminProductFormSection.productPrice}}" stepKey="fillPrice"/> - <fillField userInput="{{product.quantity}}" selector="{{AdminProductFormSection.productQuantity}}" stepKey="fillQuantity"/> - <searchAndMultiSelectOption selector="{{AdminProductFormSection.categoriesDropdown}}" parameterArray="[{{category.name}}]" stepKey="fillCategory"/> -+ <selectOption userInput="{{product.visibility}}" selector="{{AdminProductFormSection.visibility}}" stepKey="fillVisibility"/> - <click selector="{{AdminProductSEOSection.sectionHeader}}" stepKey="openSeoSection"/> - <fillField userInput="{{product.urlKey}}" selector="{{AdminProductSEOSection.urlKeyInput}}" stepKey="fillUrlKey"/> - -@@ -103,4 +104,255 @@ - <seeElement selector="{{AdminProductMessagesSection.successMessage}}" stepKey="seeSaveProductMessage"/> - <seeInTitle userInput="{{product.name}}" stepKey="seeProductNameInTitle"/> - </actionGroup> -+ -+ <actionGroup name="generateConfigurationsByAttributeCode"> -+ <arguments> -+ <argument name="attributeCode" type="string" defaultValue="SomeString"/> -+ </arguments> -+ <click selector="{{AdminProductFormConfigurationsSection.createConfigurations}}" stepKey="clickCreateConfigurations"/> -+ <click selector="{{AdminCreateProductConfigurationsPanel.filters}}" stepKey="clickFilters"/> -+ <fillField selector="{{AdminCreateProductConfigurationsPanel.attributeCode}}" userInput="{{attributeCode}}" stepKey="fillFilterAttributeCodeField"/> -+ <click selector="{{AdminCreateProductConfigurationsPanel.applyFilters}}" stepKey="clickApplyFiltersButton"/> -+ <click selector="{{AdminCreateProductConfigurationsPanel.firstCheckbox}}" stepKey="clickOnFirstCheckbox"/> -+ <click selector="{{AdminCreateProductConfigurationsPanel.next}}" stepKey="clickOnNextButton1"/> -+ <click selector="{{AdminCreateProductConfigurationsPanel.selectAll}}" stepKey="clickOnSelectAll"/> -+ <click selector="{{AdminCreateProductConfigurationsPanel.next}}" stepKey="clickOnNextButton2"/> -+ <click selector="{{AdminCreateProductConfigurationsPanel.applySingleQuantityToEachSkus}}" stepKey="clickOnApplySingleQuantityToEachSku"/> -+ <fillField selector="{{AdminCreateProductConfigurationsPanel.quantity}}" userInput="99" stepKey="enterAttributeQuantity"/> -+ <click selector="{{AdminCreateProductConfigurationsPanel.next}}" stepKey="clickOnNextButton3"/> -+ <click selector="{{AdminCreateProductConfigurationsPanel.next}}" stepKey="clickOnNextButton4"/> -+ </actionGroup> -+ <actionGroup name="createOptionsForAttribute"> -+ <arguments> -+ <argument name="attributeName" type="string" defaultValue="{{productAttributeColor.default_label}}"/> -+ <argument name="firstOptionName" type="string" defaultValue="option1"/> -+ <argument name="secondOptionName" type="string" defaultValue="option2"/> -+ </arguments> -+ <click selector="{{AdminCreateProductConfigurationsPanel.filters}}" stepKey="clickOnFilters"/> -+ <fillField userInput="{{attributeName}}" selector="{{AdminCreateProductConfigurationsPanel.attributeCode}}" stepKey="fillFilterAttributeCodeField"/> -+ <click selector="{{AdminCreateProductConfigurationsPanel.applyFilters}}" stepKey="clickApplyFiltersButton"/> -+ <click selector="{{AdminCreateProductConfigurationsPanel.firstCheckbox}}" stepKey="clickOnFirstCheckbox"/> -+ <click selector="{{AdminCreateProductConfigurationsPanel.next}}" stepKey="clickOnNextButton"/> -+ <click selector="{{AdminCreateProductConfigurationsPanel.createNewValue}}" stepKey="clickOnCreateFirstNewValue"/> -+ <fillField userInput="{{firstOptionName}}" selector="{{AdminCreateProductConfigurationsPanel.attributeName}}" stepKey="fillFieldForNewFirstOption"/> -+ <click selector="{{AdminCreateProductConfigurationsPanel.saveAttribute}}" stepKey="clickOnSaveNewAttribute"/> -+ <click selector="{{AdminCreateProductConfigurationsPanel.createNewValue}}" stepKey="clickOnCreateSecondNewValue"/> -+ <fillField userInput="{{secondOptionName}}" selector="{{AdminCreateProductConfigurationsPanel.attributeName}}" stepKey="fillFieldForNewSecondOption"/> -+ <click selector="{{AdminCreateProductConfigurationsPanel.saveAttribute}}" stepKey="clickOnSaveAttribute"/> -+ <click selector="{{AdminCreateProductConfigurationsPanel.selectAll}}" stepKey="clickOnSelectAll"/> -+ </actionGroup> -+ -+ <actionGroup name="createConfigurationsForAttribute" extends="generateConfigurationsByAttributeCode"> -+ <arguments> -+ <argument name="attributeCode" type="string" defaultValue="SomeString"/> -+ </arguments> -+ <click selector="{{AdminProductFormActionSection.saveButton}}" stepKey="clickOnSaveButton2"/> -+ <click selector="{{AdminChooseAffectedAttributeSetPopup.confirm}}" stepKey="clickOnConfirmInPopup"/> -+ </actionGroup> -+ -+ <actionGroup name="createConfigurationsForAttributeWithImages" extends="generateConfigurationsByAttributeCode"> -+ <arguments> -+ <argument name="attributeCode" type="string" defaultValue="SomeString"/> -+ <argument name="image" defaultValue="ProductImage"/> -+ </arguments> -+ -+ <click selector="{{AdminCreateProductConfigurationsPanel.applySingleSetOfImages}}" stepKey="clickOnApplySingleImageSetToAllSku" after="enterAttributeQuantity"/> -+ <waitForElementVisible selector="{{AdminCreateProductConfigurationsPanel.imageUploadButton}}" stepKey="seeImageSectionIsReady" after="clickOnApplySingleImageSetToAllSku"/> -+ <attachFile selector="{{AdminCreateProductConfigurationsPanel.imageFileUpload}}" userInput="{{image.file}}" stepKey="uploadFile" after="seeImageSectionIsReady"/> -+ <waitForElementNotVisible selector="{{AdminCreateProductConfigurationsPanel.uploadProgressBar}}" stepKey="waitForUpload" after="uploadFile"/> -+ <waitForElementVisible selector="{{AdminCreateProductConfigurationsPanel.imageFile(image.fileName)}}" stepKey="waitForThumbnail" after="waitForUpload"/> -+ -+ <click selector="{{AdminProductFormActionSection.saveButton}}" stepKey="clickOnSaveButton2" after="clickOnNextButton4"/> -+ <click selector="{{AdminChooseAffectedAttributeSetPopup.confirm}}" stepKey="clickOnConfirmInPopup" after="clickOnSaveButton2"/> -+ </actionGroup> -+ -+ <actionGroup name="createConfigurationsForTwoAttribute" extends="generateConfigurationsByAttributeCode"> -+ <arguments> -+ <argument name="secondAttributeCode" type="string"/> -+ </arguments> -+ <remove keyForRemoval="clickOnSelectAll"/> -+ <remove keyForRemoval="clickFilters"/> -+ <remove keyForRemoval="fillFilterAttributeCodeField"/> -+ <remove keyForRemoval="clickApplyFiltersButton"/> -+ <remove keyForRemoval="clickOnFirstCheckbox"/> -+ -+ <click selector="{{AdminCreateProductConfigurationsPanel.attributeCheckbox(attributeCode)}}" stepKey="clickOnFirstAttributeCheckbox" after="clickCreateConfigurations"/> -+ <click selector="{{AdminCreateProductConfigurationsPanel.attributeCheckbox(secondAttributeCode)}}" stepKey="clickOnSecondAttributeCheckbox" after="clickOnFirstAttributeCheckbox"/> -+ <grabTextFrom selector="{{AdminCreateProductConfigurationsPanel.defaultLabel(attributeCode)}}" stepKey="grabFirstAttributeDefaultLabel" after="clickOnSecondAttributeCheckbox"/> -+ <grabTextFrom selector="{{AdminCreateProductConfigurationsPanel.defaultLabel(secondAttributeCode)}}" stepKey="grabSecondAttributeDefaultLabel" after="grabFirstAttributeDefaultLabel"/> -+ <click selector="{{AdminCreateProductConfigurationsPanel.selectAllByAttribute({$grabFirstAttributeDefaultLabel})}}" stepKey="clickOnSelectAllForFirstAttribute" after="clickOnNextButton1"/> -+ <click selector="{{AdminCreateProductConfigurationsPanel.selectAllByAttribute({$grabSecondAttributeDefaultLabel})}}" stepKey="clickOnSelectAllForSecondAttribute" after="clickOnSelectAllForFirstAttribute"/> -+ <click selector="{{AdminProductFormActionSection.saveButton}}" stepKey="clickOnSaveButton2"/> -+ <click selector="{{AdminChooseAffectedAttributeSetPopup.confirm}}" stepKey="clickOnConfirmInPopup"/> -+ </actionGroup> -+ -+ <actionGroup name="saveConfiguredProduct"> -+ <click selector="{{AdminProductFormActionSection.saveButton}}" stepKey="clickOnSaveButton2"/> -+ <click selector="{{AdminChooseAffectedAttributeSetPopup.confirm}}" stepKey="clickOnConfirmInPopup"/> -+ <seeElement selector="{{AdminProductMessagesSection.successMessage}}" stepKey="seeSaveProductMessage"/> -+ </actionGroup> -+ -+ <actionGroup name="addNewProductConfigurationAttribute"> -+ <arguments> -+ <argument name="attribute" type="entity"/> -+ <argument name="firstOption" type="entity"/> -+ <argument name="secondOption" type="entity"/> -+ </arguments> -+ <!-- Create new attribute --> -+ <click selector="{{AdminCreateProductConfigurationsPanel.createNewAttribute}}" stepKey="clickOnNewAttribute"/> -+ <waitForPageLoad stepKey="waitForIFrame"/> -+ <switchToIFrame selector="{{AdminNewAttributePanel.newAttributeIFrame}}" stepKey="switchToNewAttributeIFrame"/> -+ <fillField selector="{{AdminNewAttributePanel.defaultLabel}}" userInput="{{attribute.default_label}}" stepKey="fillDefaultLabel"/> -+ <click selector="{{AdminNewAttributePanel.saveAttribute}}" stepKey="clickOnNewAttributePanel"/> -+ <waitForPageLoad stepKey="waitForSaveAttribute"/> -+ <switchToIFrame stepKey="switchOutOfIFrame"/> -+ <waitForPageLoad stepKey="waitForFilters"/> -+ <!-- Find created below attribute and add option; save attribute --> -+ <click selector="{{AdminCreateProductConfigurationsPanel.filters}}" stepKey="clickOnFilters"/> -+ <fillField userInput="{{attribute.default_label}}" selector="{{AdminCreateProductConfigurationsPanel.attributeCode}}" stepKey="fillFilterAttributeCodeField"/> -+ <click selector="{{AdminCreateProductConfigurationsPanel.applyFilters}}" stepKey="clickApplyFiltersButton"/> -+ <click selector="{{AdminCreateProductConfigurationsPanel.firstCheckbox}}" stepKey="clickOnFirstCheckbox"/> -+ <click selector="{{AdminCreateProductConfigurationsPanel.next}}" stepKey="clickOnNextButton"/> -+ <click selector="{{AdminCreateProductConfigurationsPanel.createNewValue}}" stepKey="clickOnCreateFirstNewValue"/> -+ <fillField userInput="{{firstOption.name}}" selector="{{AdminCreateProductConfigurationsPanel.attributeName}}" stepKey="fillFieldForNewFirstOption"/> -+ <click selector="{{AdminCreateProductConfigurationsPanel.saveAttribute}}" stepKey="clickOnSaveNewAttribute"/> -+ <click selector="{{AdminCreateProductConfigurationsPanel.createNewValue}}" stepKey="clickOnCreateSecondNewValue"/> -+ <fillField userInput="{{secondOption.name}}" selector="{{AdminCreateProductConfigurationsPanel.attributeName}}" stepKey="fillFieldForNewSecondOption"/> -+ <click selector="{{AdminCreateProductConfigurationsPanel.saveAttribute}}" stepKey="clickOnSaveAttribute"/> -+ <click selector="{{AdminCreateProductConfigurationsPanel.selectAll}}" stepKey="clickOnSelectAll"/> -+ <click selector="{{AdminCreateProductConfigurationsPanel.next}}" stepKey="clickOnSecondNextButton"/> -+ <click selector="{{AdminCreateProductConfigurationsPanel.next}}" stepKey="clickOnThirdNextButton"/> -+ <click selector="{{AdminCreateProductConfigurationsPanel.next}}" stepKey="clickOnFourthNextButton"/> -+ </actionGroup> -+ <actionGroup name="selectCreatedAttributeAndCreateTwoOptions" extends="addNewProductConfigurationAttribute"> -+ <remove keyForRemoval="clickOnNewAttribute"/> -+ <remove keyForRemoval="waitForIFrame"/> -+ <remove keyForRemoval="switchToNewAttributeIFrame"/> -+ <remove keyForRemoval="fillDefaultLabel"/> -+ <remove keyForRemoval="clickOnNewAttributePanel"/> -+ <remove keyForRemoval="waitForSaveAttribute"/> -+ <remove keyForRemoval="switchOutOfIFrame"/> -+ <remove keyForRemoval="waitForFilters"/> -+ <fillField userInput="{{attribute.attribute_code}}" selector="{{AdminCreateProductConfigurationsPanel.attributeCode}}" stepKey="fillFilterAttributeCodeField"/> -+ <fillField userInput="{{firstOption.label}}" selector="{{AdminCreateProductConfigurationsPanel.attributeName}}" stepKey="fillFieldForNewFirstOption"/> -+ <fillField userInput="{{secondOption.label}}" selector="{{AdminCreateProductConfigurationsPanel.attributeName}}" stepKey="fillFieldForNewSecondOption"/> -+ <remove keyForRemoval="clickOnSelectAll"/> -+ <remove keyForRemoval="clickOnSecondNextButton"/> -+ <remove keyForRemoval="clickOnThirdNextButton"/> -+ <remove keyForRemoval="clickOnFourthNextButton"/> -+ </actionGroup> -+ <actionGroup name="changeProductConfigurationsInGrid"> -+ <arguments> -+ <argument name="firstOption" type="entity"/> -+ <argument name="secondOption" type="entity"/> -+ </arguments> -+ <fillField userInput="{{firstOption.name}}" selector="{{AdminProductFormConfigurationsSection.confProductNameCell(firstOption.name)}}" stepKey="fillFieldNameForFirstAttributeOption"/> -+ <fillField userInput="{{secondOption.name}}" selector="{{AdminProductFormConfigurationsSection.confProductNameCell(secondOption.name)}}" stepKey="fillFieldNameForSecondAttributeOption"/> -+ <fillField userInput="{{firstOption.sku}}" selector="{{AdminProductFormConfigurationsSection.confProductSkuCell(firstOption.name)}}" stepKey="fillFieldSkuForFirstAttributeOption"/> -+ <fillField userInput="{{secondOption.sku}}" selector="{{AdminProductFormConfigurationsSection.confProductSkuCell(secondOption.name)}}" stepKey="fillFieldSkuForSecondAttributeOption"/> -+ <fillField userInput="{{firstOption.price}}" selector="{{AdminProductFormConfigurationsSection.confProductPriceCell(firstOption.name)}}" stepKey="fillFieldPriceForFirstAttributeOption"/> -+ <fillField userInput="{{secondOption.price}}" selector="{{AdminProductFormConfigurationsSection.confProductPriceCell(secondOption.name)}}" stepKey="fillFieldPriceForSecondAttributeOption"/> -+ <fillField userInput="{{firstOption.quantity}}" selector="{{AdminProductFormConfigurationsSection.confProductQuantityCell(firstOption.name)}}" stepKey="fillFieldQuantityForFirstAttributeOption"/> -+ <fillField userInput="{{secondOption.quantity}}" selector="{{AdminProductFormConfigurationsSection.confProductQuantityCell(secondOption.name)}}" stepKey="fillFieldQuantityForSecondAttributeOption"/> -+ <fillField userInput="{{firstOption.weight}}" selector="{{AdminProductFormConfigurationsSection.confProductWeightCell(firstOption.name)}}" stepKey="fillFieldWeightForFirstAttributeOption"/> -+ <fillField userInput="{{secondOption.weight}}" selector="{{AdminProductFormConfigurationsSection.confProductWeightCell(secondOption.name)}}" stepKey="fillFieldWeightForSecondAttributeOption"/> -+ </actionGroup> -+ -+ <actionGroup name="changeProductConfigurationsInGridExceptSku" extends="changeProductConfigurationsInGrid"> -+ <remove keyForRemoval="fillFieldSkuForFirstAttributeOption"/> -+ <remove keyForRemoval="fillFieldSkuForSecondAttributeOption"/> -+ </actionGroup> -+ -+ <actionGroup name="addProductToConfigurationsGrid"> -+ <arguments> -+ <argument name="sku" type="string"/> -+ <argument name="name" type="string"/> -+ </arguments> -+ <click selector="{{AdminProductFormConfigurationsSection.actionsBtnByProductName(name)}}" stepKey="clickToExpandFirstActions"/> -+ <click selector="{{AdminProductFormConfigurationsSection.addProduct(name)}}" stepKey="clickChooseFirstDifferentProduct"/> -+ <switchToIFrame stepKey="switchOutOfIFrame"/> -+ <waitForPageLoad stepKey="waitForFilters"/> -+ <click selector="{{AdminCreateProductConfigurationsPanel.filters}}" stepKey="clickFilters"/> -+ <fillField selector="{{AdminProductGridFilterSection.skuFilter}}" userInput="{{sku}}" stepKey="fillProductSkuFilter"/> -+ <click selector="{{AdminProductGridFilterSection.applyFilters}}" stepKey="clickApplyFilters"/> -+ <click selector="{{AdminProductGridFilterSection.firstRowBySku(sku)}}" stepKey="clickOnFirstRow"/> -+ </actionGroup> -+ -+ <actionGroup name="addUniqueImageToConfigurableProductOption"> -+ <arguments> -+ <argument name="image" defaultValue="ProductImage"/> -+ <argument name="frontend_label" type="string"/> -+ <argument name="label" type="string"/> -+ </arguments> -+ <click selector="{{AdminCreateProductConfigurationsPanel.applyUniqueImagesToEachSkus}}" stepKey="clickOnApplyUniqueImagesToEachSku"/> -+ <selectOption userInput="{{frontend_label}}" selector="{{AdminCreateProductConfigurationsPanel.selectImagesButton}}" stepKey="selectOption"/> -+ <attachFile selector="{{AdminCreateProductConfigurationsPanel.uploadImagesButton(label)}}" userInput="{{image.file}}" stepKey="uploadFile"/> -+ <waitForElementNotVisible selector="{{AdminCreateProductConfigurationsPanel.uploadProgressBar}}" stepKey="waitForUpload"/> -+ <waitForElementVisible selector="{{AdminCreateProductConfigurationsPanel.imageFile(image.fileName)}}" stepKey="waitForThumbnail"/> -+ </actionGroup> -+ -+ <actionGroup name="addUniquePriceToConfigurableProductOption"> -+ <arguments> -+ <argument name="frontend_label" type="string"/> -+ <argument name="label" type="string"/> -+ <argument name="price" type="string"/> -+ </arguments> -+ <click selector="{{AdminCreateProductConfigurationsPanel.applyUniquePricesToEachSkus}}" stepKey="clickOnApplyUniquePricesToEachSku"/> -+ <selectOption userInput="{{frontend_label}}" selector="{{AdminCreateProductConfigurationsPanel.selectPriceButton}}" stepKey="selectOption"/> -+ <fillField selector="{{AdminCreateProductConfigurationsPanel.price(label)}}" userInput="{{price}}" stepKey="enterAttributeQuantity"/> -+ </actionGroup> -+ <actionGroup name="addUniqueQuantityToConfigurableProductOption"> -+ <arguments> -+ <argument name="frontend_label" type="string" defaultValue="{{productAttributeColor.default_label}}"/> -+ <argument name="label" type="string" defaultValue="option1"/> -+ <argument name="quantity" type="string" defaultValue="10"/> -+ </arguments> -+ <click selector="{{AdminCreateProductConfigurationsPanel.applyUniqueQuantityToEachSkus}}" stepKey="clickOnApplyUniqueQuantitiesToEachSku"/> -+ <selectOption selector="{{AdminCreateProductConfigurationsPanel.selectQuantity}}" userInput="{{frontend_label}}" stepKey="selectOption"/> -+ <fillField selector="{{AdminCreateProductConfigurationsPanel.applyUniqueQuantity(label)}}" userInput="{{quantity}}" stepKey="enterAttributeQuantity"/> -+ </actionGroup> -+ -+ <actionGroup name="saveConfigurableProductWithNewAttributeSet"> -+ <click selector="{{AdminProductFormActionSection.saveButton}}" stepKey="clickSaveConfigurableProduct"/> -+ <waitForElementVisible selector="{{AdminChooseAffectedAttributeSetPopup.confirm}}" time="30" stepKey="waitForAttributeSetConfirmation"/> -+ <click selector="{{AdminChooseAffectedAttributeSetPopup.addNewAttrSet}}" stepKey="clickAddNewAttributeSet"/> -+ <fillField selector="{{AdminChooseAffectedAttributeSetPopup.createNewAttrSetName}}" userInput="{{ProductAttributeFrontendLabel.label}}" stepKey="fillFieldNewAttrSetName"/> -+ <click selector="{{AdminChooseAffectedAttributeSetPopup.confirm}}" stepKey="clickConfirmAttributeSet"/> -+ <see selector="You saved the product" stepKey="seeConfigurableSaveConfirmation" after="clickConfirmAttributeSet"/> -+ </actionGroup> -+ -+ <actionGroup name="saveConfigurableProductAddToCurrentAttributeSet"> -+ <waitForElementVisible selector="{{AdminProductFormActionSection.saveButton}}" stepKey="waitForSaveBtnVisible"/> -+ <click selector="{{AdminProductFormActionSection.saveButton}}" stepKey="saveProductAgain"/> -+ <waitForElementVisible selector="{{AdminChooseAffectedAttributeSetPopup.confirm}}" stepKey="waitPopUpVisible"/> -+ <click selector="{{AdminChooseAffectedAttributeSetPopup.confirm}}" stepKey="clickOnConfirmPopup"/> -+ <seeElement selector="{{AdminMessagesSection.success}}" stepKey="seeSaveProductMessage"/> -+ </actionGroup> -+ -+ <actionGroup name="assertConfigurableProductOnAdminProductPage"> -+ <arguments> -+ <argument name="product" type="entity"/> -+ </arguments> -+ <seeInField userInput="{{ApiConfigurableProduct.name}}" selector="{{AdminProductFormSection.productName}}" stepKey="seeNameRequired"/> -+ <seeInField userInput="{{ApiConfigurableProduct.sku}}" selector="{{AdminProductFormSection.productSku}}" stepKey="seeSkuRequired"/> -+ <dontSeeInField userInput="{{ApiConfigurableProduct.price}}" selector="{{AdminProductFormSection.productPrice}}" stepKey="dontSeePriceRequired"/> -+ </actionGroup> -+ -+ <!--Click in Next Step and see Title--> -+ <actionGroup name="AdminConfigurableWizardMoveToNextStepActionGroup"> -+ <arguments> -+ <argument name="title" type="string"/> -+ </arguments> -+ <click selector="{{ConfigurableProductSection.nextButton}}" stepKey="clickNextButton"/> -+ <waitForPageLoad stepKey="waitForNextStepLoaded"/> -+ <see userInput="{{title}}" selector="{{AdminProductFormConfigurationsSection.stepsWizardTitle}}" stepKey="seeStepTitle"/> -+ </actionGroup> -+ <actionGroup name="AdminConfigurableProductDisableConfigurationsActionGroup"> -+ <arguments> -+ <argument name="productName" type="string" defaultValue="{{SimpleProduct.name}}"/> -+ </arguments> -+ <click selector="{{AdminProductFormConfigurationsSection.actionsBtnByProductName(productName)}}" stepKey="clickToExpandActionsSelect"/> -+ <click selector="{{AdminProductFormConfigurationsSection.disableProductBtn}}" stepKey="clickDisableChildProduct"/> -+ <see selector="{{AdminProductFormConfigurationsSection.confProductOptionStatusCell(productName)}}" userInput="Disabled" stepKey="seeConfigDisabled"/> -+ </actionGroup> - </actionGroups> -diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/ActionGroup/AdminCreateApiConfigurableProductActionGroup.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/ActionGroup/AdminCreateApiConfigurableProductActionGroup.xml -new file mode 100644 -index 00000000000..5feaab40a76 ---- /dev/null -+++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/ActionGroup/AdminCreateApiConfigurableProductActionGroup.xml -@@ -0,0 +1,78 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> -+ <actionGroup name="AdminCreateApiConfigurableProductActionGroup"> -+ <arguments> -+ <argument name="productName" defaultValue="{{ApiConfigurableProductWithOutCategory.name}}" type="string"/> -+ </arguments> -+ -+ <!-- Create the configurable product based on the data in the /data folder --> -+ <createData entity="ApiConfigurableProductWithOutCategory" stepKey="createConfigProduct"> -+ <field key="name">{{productName}}</field> -+ </createData> -+ -+ <!-- Create attribute with 2 options to be used in children products --> -+ <createData entity="productAttributeWithTwoOptions" stepKey="createConfigProductAttribute"/> -+ <createData entity="productAttributeOption1" stepKey="createConfigProductAttributeOption1"> -+ <requiredEntity createDataKey="createConfigProductAttribute"/> -+ </createData> -+ <createData entity="productAttributeOption2" stepKey="createConfigProductAttributeOption2"> -+ <requiredEntity createDataKey="createConfigProductAttribute"/> -+ </createData> -+ <createData entity="AddToDefaultSet" stepKey="addAttributeToAttributeSet"> -+ <requiredEntity createDataKey="createConfigProductAttribute"/> -+ </createData> -+ <getData entity="ProductAttributeOptionGetter" index="1" stepKey="getConfigAttributeOption1"> -+ <requiredEntity createDataKey="createConfigProductAttribute"/> -+ </getData> -+ <getData entity="ProductAttributeOptionGetter" index="2" stepKey="getConfigAttributeOption2"> -+ <requiredEntity createDataKey="createConfigProductAttribute"/> -+ </getData> -+ -+ <!-- Create the 2 children that will be a part of the configurable product --> -+ <createData entity="ApiSimpleOne" stepKey="createConfigChildProduct1"> -+ <requiredEntity createDataKey="createConfigProductAttribute"/> -+ <requiredEntity createDataKey="getConfigAttributeOption1"/> -+ </createData> -+ <createData entity="ApiSimpleTwo" stepKey="createConfigChildProduct2"> -+ <requiredEntity createDataKey="createConfigProductAttribute"/> -+ <requiredEntity createDataKey="getConfigAttributeOption2"/> -+ </createData> -+ -+ <!-- Assign the two products to the configurable product --> -+ <createData entity="ConfigurableProductTwoOptions" stepKey="createConfigProductOption"> -+ <requiredEntity createDataKey="createConfigProduct"/> -+ <requiredEntity createDataKey="createConfigProductAttribute"/> -+ <requiredEntity createDataKey="getConfigAttributeOption1"/> -+ <requiredEntity createDataKey="getConfigAttributeOption2"/> -+ </createData> -+ <createData entity="ConfigurableProductAddChild" stepKey="createConfigProductAddChild1"> -+ <requiredEntity createDataKey="createConfigProduct"/> -+ <requiredEntity createDataKey="createConfigChildProduct1"/> -+ </createData> -+ <createData entity="ConfigurableProductAddChild" stepKey="createConfigProductAddChild2"> -+ <requiredEntity createDataKey="createConfigProduct"/> -+ <requiredEntity createDataKey="createConfigChildProduct2"/> -+ </createData> -+ </actionGroup> -+ -+ <!-- Create the configurable product, children are not visible individually --> -+ <actionGroup name="AdminCreateApiConfigurableProductWithHiddenChildActionGroup" extends="AdminCreateApiConfigurableProductActionGroup"> -+ <!-- Create the 2 children that will be a part of the configurable product --> -+ <createData entity="ApiSimpleOneHidden" stepKey="createConfigChildProduct1"> -+ <requiredEntity createDataKey="createConfigProductAttribute"/> -+ <requiredEntity createDataKey="getConfigAttributeOption1"/> -+ </createData> -+ <createData entity="ApiSimpleTwoHidden" stepKey="createConfigChildProduct2"> -+ <requiredEntity createDataKey="createConfigProductAttribute"/> -+ <requiredEntity createDataKey="getConfigAttributeOption2"/> -+ </createData> -+ </actionGroup> -+</actionGroups> -diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/ActionGroup/AdminOrderConfigurableProductActionGroup.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/ActionGroup/AdminOrderConfigurableProductActionGroup.xml -new file mode 100644 -index 00000000000..1c8fdf26735 ---- /dev/null -+++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/ActionGroup/AdminOrderConfigurableProductActionGroup.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="AdminOrderConfigureConfigurableProduct"> -+ <arguments> -+ <argument name="optionName" type="string" defaultValue="option1"/> -+ <argument name="productQty" type="string" defaultValue="1"/> -+ </arguments> -+ <click selector="{{AdminOrderFormItemsOrderedSection.configureButtonBySku}}" stepKey="clickConfigure"/> -+ <waitForPageLoad stepKey="waitForConfigurePageLoad"/> -+ <selectOption selector="{{AdminOrderFormConfigureProductSection.selectOption}}" userInput="{{optionName}}" stepKey="selectOption"/> -+ <fillField selector="{{AdminOrderFormConfigureProductSection.quantity}}" userInput="{{productQty}}" stepKey="fillProductQty"/> -+ <click selector="{{AdminOrderFormConfigureProductSection.ok}}" stepKey="clickOk"/> -+ </actionGroup> -+</actionGroups> -diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/ActionGroup/ConfigurableProductAttributeNameDesignActionGroup.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/ActionGroup/ConfigurableProductAttributeNameDesignActionGroup.xml -index eec1fd6273e..c4ad02ee141 100644 ---- a/app/code/Magento/ConfigurableProduct/Test/Mftf/ActionGroup/ConfigurableProductAttributeNameDesignActionGroup.xml -+++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/ActionGroup/ConfigurableProductAttributeNameDesignActionGroup.xml -@@ -7,8 +7,7 @@ - --> - - <actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -- xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Test/etc/actionGroupSchema.xsd"> -- -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> - <actionGroup name="GotoCatalogProductsPage"> - - <!--Click on Catalog item--> -@@ -69,26 +68,31 @@ - - <!--Add option 1 to attribute--> - <click selector="{{NewProduct.addOptionButton}}" stepKey="clickAddOption1"/> -+ <waitForPageLoad stepKey="waitForOption1"/> - <fillField stepKey="fillInAdminFieldRed" selector="{{NewProduct.adminFieldRed}}" userInput="{{NewProductsData.adminFieldRed}}"/> - <fillField stepKey="fillInDefaultStoreViewFieldRed" selector="{{NewProduct.defaultStoreViewFieldRed}}" userInput="{{NewProductsData.defaultStoreViewFieldRed}}"/> - - <!--Add option 2 to attribute--> - <click selector="{{NewProduct.addOptionButton}}" stepKey="clickAddOption2"/> -+ <waitForPageLoad stepKey="waitForOption2"/> - <fillField stepKey="fillInAdminFieldBlue" selector="{{NewProduct.adminFieldBlue}}" userInput="{{NewProductsData.adminFieldBlue}}"/> - <fillField stepKey="fillInDefaultStoreViewFieldBlue" selector="{{NewProduct.defaultStoreViewFieldBlue}}" userInput="{{NewProductsData.defaultStoreViewFieldBlue}}"/> - - <!--Add option 3 to attribute--> - <click selector="{{NewProduct.addOptionButton}}" stepKey="clickAddOption3"/> -+ <waitForPageLoad stepKey="waitForOption3"/> - <fillField stepKey="fillInAdminFieldYellow" selector="{{NewProduct.adminFieldYellow}}" userInput="{{NewProductsData.adminFieldYellow}}"/> - <fillField stepKey="fillInDefaultStoreViewFieldYellow" selector="{{NewProduct.defaultStoreViewFieldYellow}}" userInput="{{NewProductsData.defaultStoreViewFieldYellow}}"/> - - <!--Add option 4 to attribute--> - <click selector="{{NewProduct.addOptionButton}}" stepKey="clickAddOption4"/> -+ <waitForPageLoad stepKey="waitForOption4"/> - <fillField stepKey="fillInAdminFieldGreen" selector="{{NewProduct.adminFieldGreen}}" userInput="{{NewProductsData.adminFieldGreen}}"/> - <fillField stepKey="fillInDefaultStoreViewFieldGreen" selector="{{NewProduct.defaultStoreViewFieldGreen}}" userInput="{{NewProductsData.defaultStoreViewFieldGreen}}"/> - - <!--Add option 5 to attribute--> - <click selector="{{NewProduct.addOptionButton}}" stepKey="clickAddOption5"/> -+ <waitForPageLoad stepKey="waitForOption5"/> - <fillField stepKey="fillInAdminFieldBlack" selector="{{NewProduct.adminFieldBlack}}" userInput="{{NewProductsData.adminFieldBlack}}"/> - <fillField stepKey="fillInDefaultStoreViewFieldBlack" selector="{{NewProduct.defaultStoreViewFieldBlack}}" userInput="{{NewProductsData.defaultStoreViewFieldBlack}}"/> - -@@ -135,6 +139,7 @@ - - <!--Click on Stores item--> - <click stepKey="clickOnStoresItem" selector="{{CatalogProductsSection.storesItem}}"/> -+ <waitForPageLoad stepKey="waitForNavigationPanel"/> - - <!--Click on Products item--> - <waitForElementVisible selector="{{CatalogProductsSection.storesProductItem}}" stepKey="waitForCatalogLoad"/> -@@ -162,5 +167,4 @@ - <waitForPageLoad stepKey="waitForAllFilterReset"/> - - </actionGroup> -- - </actionGroups> -diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/ActionGroup/ConfigurableProductCheckoutActionGroup.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/ActionGroup/ConfigurableProductCheckoutActionGroup.xml -index f272fa8ea73..f88ed5e1b02 100644 ---- a/app/code/Magento/ConfigurableProduct/Test/Mftf/ActionGroup/ConfigurableProductCheckoutActionGroup.xml -+++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/ActionGroup/ConfigurableProductCheckoutActionGroup.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"> - <!-- Check configurable product in checkout cart items --> - <actionGroup name="CheckConfigurableProductInCheckoutCartItemsActionGroup"> - <arguments> -diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/ActionGroup/StorefrontAddConfigurableProductToTheCartActionGroup.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/ActionGroup/StorefrontAddConfigurableProductToTheCartActionGroup.xml -new file mode 100644 -index 00000000000..380ffb1d0c7 ---- /dev/null -+++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/ActionGroup/StorefrontAddConfigurableProductToTheCartActionGroup.xml -@@ -0,0 +1,27 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> -+ <actionGroup name="StorefrontAddConfigurableProductToTheCartActionGroup"> -+ <arguments> -+ <argument name="urlKey" type="string"/> -+ <argument name="productAttribute" type="string"/> -+ <argument name="productOption" type="string" /> -+ <argument name="qty" type="string"/> -+ </arguments> -+ <amOnPage url="{{urlKey}}.html" stepKey="goToStorefrontPage"/> -+ <waitForPageLoad stepKey="waitForProductFrontPageToLoad"/> -+ <selectOption selector="{{StorefrontProductInfoMainSection.productOptionSelect(productAttribute)}}" userInput="{{productOption}}" stepKey="selectOption1"/> -+ <fillField selector="{{StorefrontProductPageSection.qtyInput}}" userInput="{{qty}}" stepKey="fillProductQuantity"/> -+ <click selector="{{StorefrontProductActionSection.addToCart}}" stepKey="clickOnAddToCartButton"/> -+ <waitForPageLoad stepKey="waitForProductToAddInCart"/> -+ <waitForElementVisible selector="{{StorefrontMessagesSection.success}}" stepKey="waitForSuccessMessage"/> -+ <seeElement selector="{{StorefrontProductPageSection.successMsg}}" stepKey="seeSuccessSaveMessage"/> -+ </actionGroup> -+</actionGroups> -\ No newline at end of file -diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/ActionGroup/StorefrontCategoryActionGroup.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/ActionGroup/StorefrontCategoryActionGroup.xml -index 9c160d72acc..e2759fc0fd2 100644 ---- a/app/code/Magento/ConfigurableProduct/Test/Mftf/ActionGroup/StorefrontCategoryActionGroup.xml -+++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/ActionGroup/StorefrontCategoryActionGroup.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"> - <!-- Check configurable product on the category page --> - <actionGroup name="StorefrontCheckCategoryConfigurableProduct"> - <arguments> -@@ -21,4 +21,15 @@ - <!-- @TODO: MAGETWO-80272 Move to Magento_Checkout --> - <seeElement selector="{{StorefrontCategoryProductSection.ProductAddToCartByName(product.name)}}" stepKey="AssertAddToCart" /> - </actionGroup> -+ -+ <!-- Check configurable product out of stock on the category page --> -+ <actionGroup name="StorefrontCheckCategoryOutOfStockConfigurableProduct"> -+ <arguments> -+ <argument name="product" type="entity"/> -+ </arguments> -+ <seeElement selector="{{StorefrontCategoryProductSection.ProductTitleByName(product.name)}}" stepKey="assertProductName"/> -+ <moveMouseOver selector="{{StorefrontCategoryProductSection.ProductInfoByName(product.name)}}" stepKey="moveMouseOverProduct" /> -+ <seeElement selector="{{StorefrontCategoryProductSection.ProductStockUnavailable}}" stepKey="AssertOutOfStock"/> -+ <dontSeeElement selector="{{StorefrontCategoryProductSection.ProductAddToCartByName(product.name)}}" stepKey="AssertAddToCart" /> -+ </actionGroup> - </actionGroups> -\ No newline at end of file -diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/ActionGroup/StorefrontCheckCategoryConfigurableProductWithUpdatedPriceActionGroup.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/ActionGroup/StorefrontCheckCategoryConfigurableProductWithUpdatedPriceActionGroup.xml -new file mode 100644 -index 00000000000..5d5e37ecce4 ---- /dev/null -+++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/ActionGroup/StorefrontCheckCategoryConfigurableProductWithUpdatedPriceActionGroup.xml -@@ -0,0 +1,21 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> -+ <actionGroup name="StorefrontCheckCategoryConfigurableProductWithUpdatedPriceActionGroup"> -+ <arguments> -+ <argument name="productName" type="string"/> -+ <argument name="expectedPrice" type="string"/> -+ </arguments> -+ <seeElement selector="{{StorefrontCategoryProductSection.ProductTitleByName(productName)}}" stepKey="assertProductName"/> -+ <see userInput="{{expectedPrice}}" selector="{{StorefrontCategoryProductSection.ProductPriceByName(productName)}}" stepKey="AssertProductPrice"/> -+ <moveMouseOver selector="{{StorefrontCategoryProductSection.ProductInfoByName(productName)}}" stepKey="moveMouseOverProduct"/> -+ <seeElement selector="{{StorefrontCategoryProductSection.ProductAddToCartByName(productName)}}" stepKey="AssertAddToCart"/> -+ </actionGroup> -+</actionGroups> -diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/ActionGroup/StorefrontCompareActionGroup.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/ActionGroup/StorefrontCompareActionGroup.xml -index 62e03b62151..a0c82ae356e 100644 ---- a/app/code/Magento/ConfigurableProduct/Test/Mftf/ActionGroup/StorefrontCompareActionGroup.xml -+++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/ActionGroup/StorefrontCompareActionGroup.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"> - <!-- Check the configurable product in comparison page --> - <actionGroup name="StorefrontCheckCompareConfigurableProductActionGroup"> - <arguments> -diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/ActionGroup/StorefrontProductActionGroup.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/ActionGroup/StorefrontProductActionGroup.xml -index 968f8c490af..2c3e5716d6a 100644 ---- a/app/code/Magento/ConfigurableProduct/Test/Mftf/ActionGroup/StorefrontProductActionGroup.xml -+++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/ActionGroup/StorefrontProductActionGroup.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"> - <!-- Check the configurable product on the product page --> - <actionGroup name="StorefrontCheckConfigurableProduct"> - <arguments> -@@ -24,4 +24,83 @@ - <see userInput="{{product.custom_attributes[description]}}" selector="{{StorefrontProductInfoMainSection.productDescription}}" stepKey="assertProductDescription"/> - <see userInput="{{product.custom_attributes[short_description]}}" selector="{{StorefrontProductInfoMainSection.productShortDescription}}" stepKey="assertProductShortDescription"/> - </actionGroup> --</actionGroups> -\ No newline at end of file -+ -+ <!-- Check Storefront Configurable Product Option --> -+ <actionGroup name="VerifyOptionInProductStorefront"> -+ <arguments> -+ <argument name="attributeCode" type="string"/> -+ <argument name="optionName" type="string"/> -+ </arguments> -+ <seeElement selector="{{StorefrontProductInfoMainSection.attributeOptionByAttributeID(attributeCode, optionName)}}" stepKey="verifyOptionExists"/> -+ </actionGroup> -+ -+ <!-- Adds Single Option Configurable Product to cart--> -+ <actionGroup name="SelectSingleAttributeAndAddToCart"> -+ <arguments> -+ <argument name="productName" type="string"/> -+ <argument name="attributeCode" type="string"/> -+ <argument name="optionName" type="string"/> -+ </arguments> -+ <selectOption selector="{{StorefrontProductInfoMainSection.attributeSelectByAttributeID(attributeCode)}}" userInput="{{optionName}}" stepKey="selectAttribute"/> -+ <click stepKey="addProduct" selector="{{StorefrontProductActionSection.addToCart}}"/> -+ <waitForElementVisible selector="{{StorefrontQuickSearchResultsSection.messageSection}}" time="30" stepKey="waitForProductAdded"/> -+ <see selector="{{StorefrontQuickSearchResultsSection.messageSection}}" userInput="You added {{productName}} to your shopping cart." stepKey="seeAddedToCartMessage"/> -+ </actionGroup> -+ -+ <!-- Verify configurable product options in storefront product view --> -+ <actionGroup name="storefrontCheckConfigurableProductOptions"> -+ <arguments> -+ <argument name="product" type="entity"/> -+ <argument name="firstOption" type="entity"/> -+ <argument name="secondOption" type="entity"/> -+ </arguments> -+ <selectOption userInput="{{firstOption.name}}" selector="{{StorefrontProductInfoMainSection.productAttributeOptionsSelectButton}}" stepKey="selectOption1"/> -+ <see userInput="{{product.name}}" selector="{{StorefrontProductInfoMainSection.productName}}" stepKey="seeConfigurableProductName"/> -+ <see userInput="{{firstOption.price}}" selector="{{StorefrontProductInfoMainSection.productPrice}}" stepKey="assertProductPricePresent"/> -+ <see userInput="{{product.sku}}" selector="{{StorefrontProductInfoMainSection.productSku}}" stepKey="seeConfigurableProductSku"/> -+ <see userInput="IN STOCK" selector="{{StorefrontProductInfoMainSection.productStockStatus}}" stepKey="assertInStock"/> -+ <see userInput="{{colorProductAttribute.default_label}}" selector="{{StorefrontProductInfoMainSection.productAttributeTitle1}}" stepKey="seeColorAttributeName"/> -+ <dontSee userInput="As low as" selector="{{StorefrontProductInfoMainSection.productPriceLabel}}" stepKey="dontSeeProductPriceLabel1"/> -+ <selectOption userInput="{{secondOption.name}}" selector="{{StorefrontProductInfoMainSection.productAttributeOptionsSelectButton}}" stepKey="selectOption2"/> -+ <dontSee userInput="As low as" selector="{{StorefrontProductInfoMainSection.productPriceLabel}}" stepKey="dontSeeProductPriceLabel2"/> -+ <see userInput="{{secondOption.price}}" selector="{{StorefrontProductInfoMainSection.productPrice}}" stepKey="seeProductPrice2"/> -+ </actionGroup> -+ -+ <!-- Assert option image in storefront product page --> -+ <actionGroup name="assertOptionImageInStorefrontProductPage"> -+ <arguments> -+ <argument name="product" type="entity"/> -+ <argument name="label" type="string"/> -+ <argument name="image" defaultValue="MagentoLogo"/> -+ </arguments> -+ <seeInCurrentUrl url="/{{product.urlKey}}.html" stepKey="checkUrl"/> -+ <waitForPageLoad stepKey="waitForPageLoad"/> -+ <selectOption userInput="{{label}}" selector="{{StorefrontProductInfoMainSection.productAttributeOptionsSelectButton}}" stepKey="selectOption1"/> -+ <seeElement selector="{{StorefrontProductMediaSection.imageFile(image.filename)}}" stepKey="seeFirstImage"/> -+ </actionGroup> -+ -+ <!-- Assert option image and price in storefront product page --> -+ <actionGroup name="AssertOptionImageAndPriceInStorefrontProductActionGroup"> -+ <arguments> -+ <argument name="label" type="string"/> -+ <argument name="image" type="string"/> -+ <argument name="price" type="string"/> -+ </arguments> -+ <selectOption userInput="{{label}}" selector="{{StorefrontProductInfoMainSection.productAttributeOptionsSelectButton}}" stepKey="selectOption"/> -+ <seeElement selector="{{StorefrontProductMediaSection.imageFile(image)}}" stepKey="seeImage"/> -+ <see userInput="{{price}}" selector="{{StorefrontProductInfoMainSection.price}}" stepKey="seeProductPrice"/> -+ </actionGroup> -+ -+ <!-- Assert configurable product with special price in storefront product page --> -+ <actionGroup name="assertConfigurableProductWithSpecialPriceOnStorefrontProductPage"> -+ <arguments> -+ <argument name="option" type="string"/> -+ <argument name="price" type="string"/> -+ <argument name="specialPrice" defaultValue="specialProductPrice"/> -+ </arguments> -+ <selectOption userInput="{{option}}" selector="{{StorefrontProductInfoMainSection.productAttributeOptionsSelectButton}}" stepKey="selectOptionWithSpecialPrice"/> -+ <see userInput="{{specialProductPrice.price}}" selector="{{StorefrontProductInfoMainSection.productSpecialPrice}}" stepKey="seeSpecialProductPrice"/> -+ <see userInput="Regular Price" selector="{{StorefrontProductInfoMainSection.specialProductText}}" stepKey="seeText"/> -+ <see userInput="{{price}}" selector="{{StorefrontProductInfoMainSection.oldProductPrice}}" stepKey="seeOldProductPrice"/> -+ </actionGroup> -+</actionGroups> -diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/ActionGroup/StorefrontProductAttributeActionGroup.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/ActionGroup/StorefrontProductAttributeActionGroup.xml -new file mode 100644 -index 00000000000..77808273815 ---- /dev/null -+++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/ActionGroup/StorefrontProductAttributeActionGroup.xml -@@ -0,0 +1,26 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> -+ <!-- Check configurable product attribute options on the category page --> -+ <actionGroup name="SelectStorefrontSideBarAttributeOption"> -+ <arguments> -+ <argument name="categoryName" type="string"/> -+ <argument name="attributeDefaultLabel" type="string"/> -+ </arguments> -+ <amOnPage url="{{categoryName}}" stepKey="openCategoryStoreFrontPage"/> -+ <waitForPageLoad stepKey="waitForCategoryPageToLoad"/> -+ <seeElement selector="{{StorefrontHeaderSection.NavigationCategoryByName(categoryName)}}" stepKey="seeCategoryInFrontPage"/> -+ <click selector="{{StorefrontHeaderSection.NavigationCategoryByName(categoryName)}}" stepKey="clickOnCategory"/> -+ <waitForPageLoad stepKey="waitForCategoryPageToLoad1"/> -+ <seeElement selector="{{StorefrontCategorySidebarSection.filterOptionsTitle(attributeDefaultLabel)}}" stepKey="seeAttributeOptionsTitle"/> -+ <click selector="{{StorefrontCategorySidebarSection.filterOptionsTitle(attributeDefaultLabel)}}" stepKey="clickAttributeOptions"/> -+ <waitForPageLoad stepKey="waitForPageToLoad"/> -+ </actionGroup> -+</actionGroups> -diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/ActionGroup/StorefrontProductCartActionGroup.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/ActionGroup/StorefrontProductCartActionGroup.xml -index cc88a2c6147..e07b97c63a9 100644 ---- a/app/code/Magento/ConfigurableProduct/Test/Mftf/ActionGroup/StorefrontProductCartActionGroup.xml -+++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/ActionGroup/StorefrontProductCartActionGroup.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"> - <!-- Check Configurable Product in the Cart --> - <actionGroup name="StorefrontCheckCartConfigurableProductActionGroup"> - <arguments> -diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/ActionGroup/VerifyProductTypeOrderActionGroup.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/ActionGroup/VerifyProductTypeOrderActionGroup.xml -new file mode 100644 -index 00000000000..f3b07862360 ---- /dev/null -+++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/ActionGroup/VerifyProductTypeOrderActionGroup.xml -@@ -0,0 +1,14 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> -+ <actionGroup name="VerifyProductTypeOrder"> -+ <seeElement stepKey="seeConfigurableInOrder" selector="{{AdminProductDropdownOrderSection.configurableProduct}}"/> -+ </actionGroup> -+</actionGroups> -diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Data/ConfigurableProductAttributeNameDesignData.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Data/ConfigurableProductAttributeNameDesignData.xml -index 73a668fd2fe..0018f5996c9 100644 ---- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Data/ConfigurableProductAttributeNameDesignData.xml -+++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Data/ConfigurableProductAttributeNameDesignData.xml -@@ -7,7 +7,7 @@ - --> - - <entities xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -- xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/DataGenerator/etc/dataProfileSchema.xsd"> -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataProfileSchema.xsd"> - <entity name="NewProductsData" type="user"> - <data key="productName" unique="prefix">Shoes</data> - <data key="price">60</data> -@@ -31,5 +31,4 @@ - <data key="configurableProduct">configurable</data> - <data key="errorMessage">element.disabled is not a function</data> - </entity> -- - </entities> -diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Data/ConfigurableProductData.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Data/ConfigurableProductData.xml -index f92b388b466..e7d9d61491a 100644 ---- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Data/ConfigurableProductData.xml -+++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Data/ConfigurableProductData.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="BaseConfigurableProduct" type="product"> - <data key="sku" unique="suffix">configurable</data> - <data key="type_id">configurable</data> -@@ -29,11 +29,24 @@ - <data key="visibility">4</data> - <data key="name" unique="suffix">API Configurable Product</data> - <data key="urlKey" unique="suffix">api-configurable-product</data> -+ <data key="price">123.00</data> -+ <data key="weight">2</data> - <data key="status">1</data> - <data key="quantity">100</data> - <requiredEntity type="product_extension_attribute">EavStockItem</requiredEntity> - <requiredEntity type="custom_attribute_array">CustomAttributeCategoryIds</requiredEntity> - </entity> -+ <entity name="ApiConfigurableProductWithOutCategory" type="product"> -+ <data key="sku" unique="suffix">api-configurable-product-with-out-category</data> -+ <data key="type_id">configurable</data> -+ <data key="attribute_set_id">4</data> -+ <data key="visibility">4</data> -+ <data key="name" unique="suffix">API Configurable Product</data> -+ <data key="urlKey" unique="suffix">api-configurable-product</data> -+ <data key="status">1</data> -+ <data key="quantity">100</data> -+ <requiredEntity type="product_extension_attribute">EavStockItem</requiredEntity> -+ </entity> - <entity name="ApiConfigurableProductWithDescription" type="product"> - <data key="sku" unique="suffix">api-configurable-product</data> - <data key="type_id">configurable</data> -diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Data/ConfigurableProductOptionData.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Data/ConfigurableProductOptionData.xml -index 21dcf998a63..a1a499f33ed 100644 ---- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Data/ConfigurableProductOptionData.xml -+++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Data/ConfigurableProductOptionData.xml -@@ -7,11 +7,23 @@ - --> - - <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="ConfigurableProductOneOption" type="ConfigurableProductOption"> -+ <var key="attribute_id" entityKey="attribute_id" entityType="ProductAttribute" /> -+ <data key="label">option</data> -+ <requiredEntity type="ValueIndex">ValueIndex1</requiredEntity> -+ </entity> - <entity name="ConfigurableProductTwoOptions" type="ConfigurableProductOption"> - <var key="attribute_id" entityKey="attribute_id" entityType="ProductAttribute" /> - <data key="label">option</data> - <requiredEntity type="ValueIndex">ValueIndex1</requiredEntity> - <requiredEntity type="ValueIndex">ValueIndex2</requiredEntity> - </entity> -+ <entity name="ConfigurableProductThreeOptions" type="ConfigurableProductOption"> -+ <var key="attribute_id" entityKey="attribute_id" entityType="ProductAttribute" /> -+ <data key="label">option</data> -+ <requiredEntity type="ValueIndex">ValueIndex1</requiredEntity> -+ <requiredEntity type="ValueIndex">ValueIndex2</requiredEntity> -+ <requiredEntity type="ValueIndex">ValueIndex3</requiredEntity> -+ </entity> - </entities> -diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Data/ConstData.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Data/ConstData.xml -index 974be1e14a3..7e21729ba15 100644 ---- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Data/ConstData.xml -+++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Data/ConstData.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"> - <!-- @TODO: Get rid off this workaround and its usages after MQE-498 is implemented --> - <entity name="CONST" type="CONST"> - <data key="three">3</data> -diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Data/ProductConfigurableAttributeData.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Data/ProductConfigurableAttributeData.xml -index f99f960f6a9..13760f74297 100644 ---- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Data/ProductConfigurableAttributeData.xml -+++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Data/ProductConfigurableAttributeData.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="colorProductAttribute" type="product_attribute"> - <data key="default_label" unique="suffix">Color</data> - <data key="input_type">Dropdown</data> -@@ -29,6 +29,11 @@ - <data key="name" unique="suffix">Orange</data> - <data key="price">99.99</data> - </entity> -+ <entity name="productAttributeColor" type="product_attribute"> -+ <data key="default_label" unique="suffix">color</data> -+ <data key="input_type">Dropdown</data> -+ <data key="attribute_quantity">1</data> -+ </entity> - <entity name="colorDefaultProductAttribute" type="product_attribute"> - <data key="default_label">Color</data> - <data key="input_type">Dropdown</data> -@@ -42,4 +47,22 @@ - <data key="name">Black</data> - <data key="price">5.00</data> - </entity> -+ <entity name="colorConfigurableProductAttribute1" type="product_attribute"> -+ <data key="name" unique="suffix">Green</data> -+ <data key="sku" unique="suffix">sku-green</data> -+ <data key="type_id">simple</data> -+ <data key="price">1</data> -+ <data key="visibility">1</data> -+ <data key="quantity">1</data> -+ <data key="weight">1</data> -+ </entity> -+ <entity name="colorConfigurableProductAttribute2" type="product_attribute"> -+ <data key="name" unique="suffix">Red</data> -+ <data key="sku" unique="suffix">sku-red</data> -+ <data key="type_id">simple</data> -+ <data key="price">2</data> -+ <data key="visibility">1</data> -+ <data key="quantity">10</data> -+ <data key="weight">1</data> -+ </entity> - </entities> -diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Data/ValueIndexData.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Data/ValueIndexData.xml -index 54d489a446f..537ba2bbce0 100644 ---- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Data/ValueIndexData.xml -+++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Data/ValueIndexData.xml -@@ -7,11 +7,14 @@ - --> - - <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="ValueIndex1" type="ValueIndex"> - <var key="value_index" entityKey="value" entityType="ProductAttributeOption"/> - </entity> - <entity name="ValueIndex2" type="ValueIndex"> - <var key="value_index" entityKey="value" entityType="ProductAttributeOption"/> - </entity> -+ <entity name="ValueIndex3" type="ValueIndex"> -+ <var key="value_index" entityKey="value" entityType="ProductAttributeOption"/> -+ </entity> - </entities> -diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Metadata/configurable_product_add_child-meta.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Metadata/configurable_product_add_child-meta.xml -index 6a77e97d8f2..ec4da787541 100644 ---- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Metadata/configurable_product_add_child-meta.xml -+++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Metadata/configurable_product_add_child-meta.xml -@@ -7,7 +7,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"> -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataOperation.xsd"> - <operation name="ConfigurableProductAddChild" dataType="ConfigurableProductAddChild" type="create" auth="adminOauth" url="/V1/configurable-products/{sku}/child" method="POST"> - <contentType>application/json</contentType> - <field key="childSku">string</field> -diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Metadata/configurable_product_options-meta.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Metadata/configurable_product_options-meta.xml -index 37e6be683c2..4d894f78009 100644 ---- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Metadata/configurable_product_options-meta.xml -+++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Metadata/configurable_product_options-meta.xml -@@ -7,7 +7,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"> -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataOperation.xsd"> - <operation name="CreateConfigurableProductOption" dataType="ConfigurableProductOption" type="create" auth="adminOauth" url="/V1/configurable-products/{sku}/options" method="POST"> - <contentType>application/json</contentType> - <object dataType="ConfigurableProductOption" key="option"> -diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Metadata/extension_attribute_configurable_product_options-meta.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Metadata/extension_attribute_configurable_product_options-meta.xml -index 2f1db19a1fd..4b12abd3053 100644 ---- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Metadata/extension_attribute_configurable_product_options-meta.xml -+++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Metadata/extension_attribute_configurable_product_options-meta.xml -@@ -7,7 +7,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"> -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataOperation.xsd"> - <operation name="CreateExtensionAttributeConfigProductOption" dataType="ExtensionAttributeConfigProductOption" type="create"> - <contentType>application/json</contentType> - <array key="configurable_product_options"> -diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Metadata/valueIndex-meta.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Metadata/valueIndex-meta.xml -index 8d955fcc944..e83faddf0e3 100644 ---- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Metadata/valueIndex-meta.xml -+++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Metadata/valueIndex-meta.xml -@@ -7,7 +7,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"> -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataOperation.xsd"> - <operation name="ValueIndex" dataType="ValueIndex" type="create"> - <field key="value_index">integer</field> - </operation> -diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Page/AdminProductCreatePage.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Page/AdminProductCreatePage.xml -index 01a494afd10..7705b34f0af 100644 ---- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Page/AdminProductCreatePage.xml -+++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Page/AdminProductCreatePage.xml -@@ -7,7 +7,7 @@ - --> - - <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="AdminProductCreatePage" url="catalog/product/new/set/{{set}}/type/{{type}}/" area="admin" module="Magento_Catalog" parameterized="true"> - <section name="AdminProductFormConfigurationsSection"/> - <section name="AdminCreateProductConfigurationsPanel"/> -diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Section/AdminChooseAffectedAttributeSetSection.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Section/AdminChooseAffectedAttributeSetSection.xml -index dac7027b739..78e4c7bced8 100644 ---- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Section/AdminChooseAffectedAttributeSetSection.xml -+++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Section/AdminChooseAffectedAttributeSetSection.xml -@@ -7,8 +7,11 @@ - --> - - <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="AdminChooseAffectedAttributeSetPopup"> - <element name="confirm" type="button" selector="button[data-index='confirm_button']" timeout="30"/> -+ <element name="addNewAttrSet" type="radio" selector="//input[@data-index='affectedAttributeSetNew']" timeout="30"/> -+ <element name="createNewAttrSetName" type="input" selector="//input[@name='configurableNewAttributeSetName']" timeout="30"/> -+ <element name="closePopUp" type="button" selector="//*[contains(@class,'product_form_product_form_configurable_attribute_set')]//button[@data-role='closeBtn']" timeout="30"/> - </section> --</sections> -\ No newline at end of file -+</sections> -diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Section/AdminCreateProductConfigurationsPanelSection.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Section/AdminCreateProductConfigurationsPanelSection.xml -index 77759043a05..f3c628d002e 100644 ---- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Section/AdminCreateProductConfigurationsPanelSection.xml -+++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Section/AdminCreateProductConfigurationsPanelSection.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="AdminCreateProductConfigurationsPanel"> - <element name="next" type="button" selector=".steps-wizard-navigation .action-next-step" timeout="30"/> - <element name="createNewAttribute" type="button" selector=".select-attributes-actions button[title='Create New Attribute']" timeout="30"/> -@@ -16,14 +16,22 @@ - <element name="applyFilters" type="button" selector="button[data-action='grid-filter-apply']" timeout="30"/> - <element name="firstCheckbox" type="input" selector="tr[data-repeat-index='0'] .admin__control-checkbox"/> - <element name="id" type="text" selector="//tr[contains(@data-repeat-index, '0')]/td[2]/div"/> -+ <element name="attributeCheckbox" type="checkbox" selector="//div[contains(text(), '{{arg}}')]/ancestor::tr//input[@data-action='select-row']" parameterized="true"/> -+ <element name="defaultLabel" type="text" selector="//div[contains(text(), '{{arg}}')]/ancestor::tr//td[3]/div[@class='data-grid-cell-content']" parameterized="true"/> - - <element name="selectAll" type="button" selector=".action-select-all"/> - <element name="selectAllByAttribute" type="button" selector="//div[@data-attribute-title='{{attr}}']//button[contains(@class, 'action-select-all')]" parameterized="true"/> - <element name="createNewValue" type="input" selector=".action-create-new" timeout="30"/> -+ <element name="attributeNameInTitle" type="input" selector="//div[contains(@class,'attribute-entity-title-block')]/div[contains(@class,'attribute-entity-title')]"/> - <element name="attributeName" type="input" selector="li[data-attribute-option-title=''] .admin__field-create-new .admin__control-text"/> -+ <element name="attributeNameWithError" type="text" selector="//li[@data-attribute-option-title='']/div[contains(@class,'admin__field admin__field-create-new _error')]"/> - <element name="saveAttribute" type="button" selector="li[data-attribute-option-title=''] .action-save" timeout="30"/> - <element name="attributeCheckboxByIndex" type="input" selector="li.attribute-option:nth-of-type({{var1}}) input" parameterized="true"/> - -+ <element name="applySingleSetOfImages" type="radio" selector=".admin__field-label[for='apply-single-set-radio']" timeout="30"/> -+ <element name="imageFileUpload" type="input" selector=".steps-wizard-section input[type='file'][name='image']"/> -+ <element name="imageUploadButton" type="button" selector=".steps-wizard-section div.gallery"/> -+ - <element name="applyUniquePricesByAttributeToEachSku" type="radio" selector=".admin__field-label[for='apply-unique-prices-radio']"/> - <element name="applySinglePriceToAllSkus" type="radio" selector=".admin__field-label[for='apply-single-price-radio']"/> - <element name="singlePrice" type="input" selector="#apply-single-price-input"/> -@@ -33,7 +41,21 @@ - <element name="attribute3" type="input" selector="#apply-single-price-input-2"/> - - <element name="applySingleQuantityToEachSkus" type="radio" selector=".admin__field-label[for='apply-single-inventory-radio']" timeout="30"/> -+ <element name="applyUniqueQuantityToEachSkus" type="radio" selector=".admin__field-label[for='apply-unique-inventory-radio']" timeout="30"/> -+ <element name="selectQuantity" type="select" selector="#apply-single-price-input-qty" timeout="30"/> -+ <element name="applyUniqueQuantity" type="input" selector="//*[text()='{{option}}']/ancestor::div[contains(@class, 'admin__field _required')]//input[contains(@id, 'apply-qty-input')]" parameterized="true"/> -+ <element name="applyUniqueImagesToEachSkus" type="radio" selector=".admin__field-label[for='apply-unique-images-radio']" timeout="30"/> -+ <element name="applyUniquePricesToEachSkus" type="radio" selector=".admin__field-label[for='apply-unique-prices-radio']" timeout="30"/> -+ <element name="selectImagesButton" type="select" selector="#apply-images-attributes" timeout="30"/> -+ <element name="uploadImagesButton" type="file" selector="//*[text()='{{option}}']/../../div[@data-role='gallery']//input[@type='file']" timeout="30" parameterized="true"/> -+ <element name="uploadProgressBar" type="text" selector=".uploader .file-row"/> -+ <element name="imageFile" type="text" selector="//*[@data-role='gallery']//img[contains(@src, '{{url}}')]" parameterized="true"/> -+ <element name="selectPriceButton" type="select" selector="#select-each-price" timeout="30"/> -+ <element name="price" type="input" selector="//*[text()='{{option}}']/../..//input[contains(@id, 'apply-single-price-input')]" parameterized="true"/> - <element name="quantity" type="input" selector="#apply-single-inventory-input"/> - <element name="gridLoadingMask" type="text" selector="[data-role='spinner'][data-component*='product_attributes_listing']"/> -+ <element name="attributeCheckboxByName" type="input" selector="//*[contains(@data-attribute-option-title,'{{arg}}')]//input[@type='checkbox']" parameterized="true"/> -+ <element name="attributeColorCheckbox" type="select" selector="//div[contains(text(),'color') and @class='data-grid-cell-content']/../preceding-sibling::td/label/input"/> -+ <element name="attributeRowByAttributeCode" type="block" selector="//td[count(../../..//th[./*[.='Attribute Code']]/preceding-sibling::th) + 1][./*[.='{{attribute_code}}']]/../td//input[@data-action='select-row']" parameterized="true"/> - </section> - </sections> -diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Section/AdminNewAttributePanelSection.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Section/AdminNewAttributePanelSection.xml -index 31787ca75f1..573f1265931 100644 ---- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Section/AdminNewAttributePanelSection.xml -+++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Section/AdminNewAttributePanelSection.xml -@@ -7,8 +7,17 @@ - --> - - <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="AdminNewAttributePanel"> -+ <element name="useInSearch" type="select" selector="#is_searchable"/> -+ <element name="visibleInAdvancedSearch" type="select" selector="#is_visible_in_advanced_search"/> -+ <element name="comparableOnStorefront" type="select" selector="#is_comparable"/> -+ <element name="useInLayeredNavigation" type="select" selector="#is_filterable"/> -+ <element name="visibleOnCatalogPagesOnStorefront" type="select" selector="#is_visible_on_front"/> -+ <element name="useInProductListing" type="select" selector="#used_in_product_listing"/> -+ <element name="usedForStoringInProductListing" type="select" selector="#used_for_sort_by"/> -+ <element name="storefrontPropertiesTab" type="button" selector="#front_fieldset-wrapper"/> -+ <element name="storefrontPropertiesTitle" type="text" selector="//span[text()='Storefront Properties']"/> - <element name="container" type="text" selector="#create_new_attribute"/> - <element name="saveAttribute" type="button" selector="#save"/> - <element name="newAttributeIFrame" type="iframe" selector="create_new_attribute_container"/> -@@ -20,5 +29,6 @@ - <element name="optionAdminValue" type="input" selector="[data-role='options-container'] input[name='option[value][option_{{row}}][0]']" parameterized="true"/> - <element name="optionDefaultStoreValue" type="input" selector="[data-role='options-container'] input[name='option[value][option_{{row}}][1]']" parameterized="true"/> - <element name="deleteOption" type="button" selector="#delete_button_option_{{row}}" parameterized="true"/> -+ <element name="deleteOptionByName" type="button" selector="//*[contains(@value, '{{arg}}')]/../following-sibling::td[contains(@id, 'delete_button_container')]/button" parameterized="true"/> - </section> - </sections> -diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Section/AdminProductDropdownOrderSection.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Section/AdminProductDropdownOrderSection.xml -new file mode 100644 -index 00000000000..6056e7f3cbd ---- /dev/null -+++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Section/AdminProductDropdownOrderSection.xml -@@ -0,0 +1,14 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> -+ <section name="AdminProductDropdownOrderSection"> -+ <element name="configurableProduct" type="text" selector="//li[not(preceding-sibling::li[span[@title='Virtual Product']]) and not(preceding-sibling::li[span[@title='Grouped Product']]) and not(preceding-sibling::li[span[@title='Bundle Product']]) and not(preceding-sibling::li[span[@title='Downloadable Product']]) and not(following-sibling::li[span[@title='Simple Product']])]/span[@title='Configurable Product']"/> -+ </section> -+</sections> -diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Section/AdminProductFormConfigurationsSection.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Section/AdminProductFormConfigurationsSection.xml -index 8a2cd192a20..c1707f6f1f0 100644 ---- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Section/AdminProductFormConfigurationsSection.xml -+++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Section/AdminProductFormConfigurationsSection.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="AdminProductFormConfigurationsSection"> - <element name="sectionHeader" type="text" selector=".admin__collapsible-block-wrapper[data-index='configurable']"/> -+ <element name="createdConfigurationsBlock" type="text" selector="div.admin__field.admin__field-wide"/> - <element name="createConfigurations" type="button" selector="button[data-index='create_configurable_products_button']" timeout="30"/> - <element name="currentVariationsRows" type="button" selector=".data-row"/> - <element name="currentVariationsNameCells" type="textarea" selector=".admin__control-fields[data-index='name_container']"/> -@@ -18,10 +19,26 @@ - <element name="currentVariationsQuantityCells" type="textarea" selector=".admin__control-fields[data-index='quantity_container']"/> - <element name="currentVariationsAttributesCells" type="textarea" selector=".admin__control-fields[data-index='attributes']"/> - <element name="currentVariationsStatusCells" type="textarea" selector="._no-header[data-index='status']"/> -+ <element name="firstSKUInConfigurableProductsGrid" type="input" selector="//input[@name='configurable-matrix[0][sku]']"/> - <element name="actionsBtn" type="button" selector="(//button[@class='action-select']/span[contains(text(), 'Select')])[{{var1}}]" parameterized="true"/> -+ <element name="actionsBtnByProductName" type="textarea" selector="//*[.='Attributes']/ancestor::tr/td[@data-index='attributes']//span[contains(text(), '{{var}}')]/ancestor::tr//button[@class='action-select']" parameterized="true"/> -+ <element name="addProduct" type="button" selector="//*[.='Attributes']/ancestor::tr/td[@data-index='attributes']//span[contains(text(), '{{var}}')]/ancestor::tr//a[text()='Choose a different Product']" parameterized="true"/> - <element name="removeProductBtn" type="button" selector="//a[text()='Remove Product']"/> - <element name="disableProductBtn" type="button" selector="//a[text()='Disable Product']"/> - <element name="enableProductBtn" type="button" selector="//a[text()='Enable Product']"/> -+ <element name="confProductSku" type="input" selector="//*[@name='configurable-matrix[{{arg}}][sku]']" parameterized="true"/> -+ <element name="confProductNameCell" type="input" selector="//*[.='Attributes']/ancestor::tr//span[contains(text(), '{{var}}')]/ancestor::tr/td[@data-index='name_container']//input" parameterized="true"/> -+ <element name="confProductSkuCell" type="input" selector="//*[.='Attributes']/ancestor::tr//span[contains(text(), '{{var}}')]/ancestor::tr/td[@data-index='sku_container']//input" parameterized="true"/> -+ <element name="confProductPriceCell" type="input" selector="//*[.='Attributes']/ancestor::tr//span[contains(text(), '{{var}}')]/ancestor::tr/td[@data-index='price_container']//input" parameterized="true"/> -+ <element name="confProductQuantityCell" type="input" selector="//*[.='Attributes']/ancestor::tr//span[contains(text(), '{{var}}')]/ancestor::tr/td[@data-index='quantity_container']//input" parameterized="true"/> -+ <element name="confProductWeightCell" type="input" selector="//*[.='Attributes']/ancestor::tr//span[contains(text(), '{{var}}')]/ancestor::tr/td[@data-index='price_weight']//input" parameterized="true"/> -+ <element name="confProductOptionStatusCell" type="text" selector="//*[.='Attributes']/ancestor::tr//span[contains(text(), '{{productName}}')]/ancestor::tr/td[@data-index='status']" parameterized="true"/> -+ <element name="confProductSkuMessage" type="text" selector="//*[@name='configurable-matrix[{{arg}}][sku]']/following-sibling::label" parameterized="true"/> -+ <element name="variationsSkuInputByRow" selector="[data-index='configurable-matrix'] table > tbody > tr:nth-of-type({{row}}) input[name*='sku']" type="input" parameterized="true"/> -+ <element name="variationsSkuInputErrorByRow" selector="[data-index='configurable-matrix'] table > tbody > tr:nth-of-type({{row}}) .admin__field-error" type="text" parameterized="true"/> -+ <element name="variationLabel" type="text" selector="//div[@data-index='configurable-matrix']/label"/> -+ <element name="stepsWizardTitle" type="text" selector="div.content:not([style='display: none;']) .steps-wizard-title"/> -+ <element name="attributeEntityByName" type="text" selector="//div[@class='attribute-entity']//div[normalize-space(.)='{{attributeLabel}}']" parameterized="true"/> - </section> - <section name="AdminConfigurableProductFormSection"> - <element name="productWeight" type="input" selector=".admin__control-text[name='product[weight]']"/> -diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Section/AdminProductGridActionSection.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Section/AdminProductGridActionSection.xml -index 1d4ec9270ef..e3403ce71ac 100644 ---- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Section/AdminProductGridActionSection.xml -+++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Section/AdminProductGridActionSection.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="AdminProductGridActionSection"> - <element name="addConfigurableProduct" type="button" selector=".item[data-ui-id='products-list-add-new-product-button-item-configurable']" timeout="30"/> - </section> -diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Section/ConfigurableProductAttributeNameDesignSection.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Section/ConfigurableProductAttributeNameDesignSection.xml -index b3077d9d5d5..ea5638f6816 100644 ---- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Section/ConfigurableProductAttributeNameDesignSection.xml -+++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Section/ConfigurableProductAttributeNameDesignSection.xml -@@ -7,7 +7,7 @@ - --> - - <sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -- xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Page/etc/SectionObject.xsd"> -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> - <section name="CatalogProductsSection"> - <element name="catalogItem" type="button" selector="//*[@id='menu-magento-catalog-catalog']/a/span"/> - <element name="productItem" type="button" selector="//*[@data-ui-id='menu-magento-catalog-catalog-products']/a"/> -@@ -53,7 +53,6 @@ - <element name="saveAttributeButton" type="button" selector="//*[@id='save']"/> - <element name="advancedAttributeProperties" type="button" selector="//*[@id='advanced_fieldset-wrapper']//*[contains(text(),'Advanced Attribute Properties')]"/> - <element name="attributeCodeField" type="input" selector="//*[@id='attribute_code']"/> -- - </section> - - <section name="CreateProductConfigurations"> -@@ -64,5 +63,4 @@ - <element name="checkboxBlack" type="input" selector="//fieldset[@class='admin__fieldset admin__fieldset-options']//*[contains(text(),'black')]/preceding-sibling::input"/> - <element name="errorMessage" type="input" selector="//div[@data-ui-id='messages-message-error']"/> - </section> -- - </sections> -diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Section/StorefrontProductInfoMainSection.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Section/StorefrontProductInfoMainSection.xml -index 4f96ea41eac..0853d22eda7 100644 ---- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Section/StorefrontProductInfoMainSection.xml -+++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Section/StorefrontProductInfoMainSection.xml -@@ -7,15 +7,19 @@ - --> - - <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="StorefrontProductInfoMainSection"> - <element name="optionByAttributeId" type="input" selector="#attribute{{var1}}" parameterized="true"/> - <element name="productAttributeTitle1" type="text" selector="#product-options-wrapper div[tabindex='0'] label"/> -+ <element name="productPrice" type="text" selector="div.price-box.price-final_price"/> - <element name="productAttributeOptions1" type="select" selector="#product-options-wrapper div[tabindex='0'] option"/> - <element name="productAttributeOptionsSelectButton" type="select" selector="#product-options-wrapper .super-attribute-select"/> - <element name="productAttributeOptionsError" type="text" selector="//div[@class='mage-error']"/> -+ <element name="selectCustomOptionByName" type="radio" selector="//*[@class='options-list nested']//span[contains(text(), '{{value}}')]/../../input" parameterized="true"/> - <!-- Parameter is the order number of the attribute on the page (1 is the newest) --> - <element name="nthAttributeOnPage" type="block" selector="tr:nth-of-type({{numElement}}) .data" parameterized="true"/> - <element name="stockIndication" type="block" selector=".stock" /> -+ <element name="attributeSelectByAttributeID" type="select" selector="//div[@class='fieldset']//div[//span[text()='{{attribute_code}}']]//select" parameterized="true"/> -+ <element name="attributeOptionByAttributeID" type="select" selector="//div[@class='fieldset']//div[//span[text()='{{attribute_code}}']]//option[text()='{{optionName}}']" parameterized="true"/> - </section> - </sections> -diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminAddDefaultImageConfigurableTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminAddDefaultImageConfigurableTest.xml -index 7dbacfa2ce6..92928c93846 100644 ---- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminAddDefaultImageConfigurableTest.xml -+++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminAddDefaultImageConfigurableTest.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="AdminAddDefaultImageConfigurableTest"> - <annotations> - <features value="ConfigurableProduct"/> -diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminAddingNewOptionsWithImagesAndPricesToConfigurableProductTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminAddingNewOptionsWithImagesAndPricesToConfigurableProductTest.xml -new file mode 100644 -index 00000000000..52443a17dfe ---- /dev/null -+++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminAddingNewOptionsWithImagesAndPricesToConfigurableProductTest.xml -@@ -0,0 +1,124 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> -+ <test name="AdminAddingNewOptionsWithImagesAndPricesToConfigurableProductTest"> -+ <annotations> -+ <features value="ConfigurableProduct"/> -+ <stories value="Update product"/> -+ <title value="Adding new options with images and prices to Configurable Product"/> -+ <description value="Test case verifies possibility to add new options for configurable attribute for existing configurable product."/> -+ <severity value="CRITICAL"/> -+ <testCaseId value="MC-13339"/> -+ <group value="configurableProduct"/> -+ </annotations> -+ -+ <before> -+ <actionGroup ref="AdminCreateApiConfigurableProductActionGroup" stepKey="createConfigurableProduct"/> -+ <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> -+ </before> -+ -+ <after> -+ <deleteData createDataKey="createConfigProductCreateConfigurableProduct" stepKey="deleteConfigProduct"/> -+ <deleteData createDataKey="createConfigProductAttributeCreateConfigurableProduct" stepKey="deleteConfigProductAttribute"/> -+ <deleteData createDataKey="createConfigChildProduct1CreateConfigurableProduct" stepKey="deleteConfigChildProduct1"/> -+ <deleteData createDataKey="createConfigChildProduct2CreateConfigurableProduct" stepKey="deleteConfigChildProduct2"/> -+ <actionGroup ref="logout" stepKey="logout"/> -+ </after> -+ -+ <!--Open edit product page--> -+ <amOnPage url="{{AdminProductEditPage.url($$createConfigProductCreateConfigurableProduct.id$$)}}" stepKey="goToProductEditPage"/> -+ -+ <!--Open edit configuration wizard--> -+ <click selector="{{AdminProductFormConfigurationsSection.createConfigurations}}" stepKey="clickEditConfigurations"/> -+ <see userInput="Select Attributes" selector="{{AdminProductFormConfigurationsSection.stepsWizardTitle}}" stepKey="seeStepTitle"/> -+ -+ <!--Click Next button--> -+ <actionGroup ref="AdminConfigurableWizardMoveToNextStepActionGroup" stepKey="navigateToAttributeValuesStep"> -+ <argument name="title" value="Attribute Values"/> -+ </actionGroup> -+ <seeElement selector="{{AdminProductFormConfigurationsSection.attributeEntityByName($$createConfigProductAttributeCreateConfigurableProduct.default_frontend_label$$)}}" stepKey="seeAttribute"/> -+ -+ <!--Create one color option via "Create New Value" link--> -+ <click selector="{{AdminCreateProductConfigurationsPanel.createNewValue}}" stepKey="clickOnCreateNewValue"/> -+ <fillField userInput="{{colorDefaultProductAttribute1.name}}" selector="{{AdminCreateProductConfigurationsPanel.attributeName}}" stepKey="fillFieldForNewAttribute"/> -+ <click selector="{{AdminCreateProductConfigurationsPanel.saveAttribute}}" stepKey="clickOnSaveNewAttribute"/> -+ -+ <!--Click Next button--> -+ <actionGroup ref="AdminConfigurableWizardMoveToNextStepActionGroup" stepKey="navigateToBulkStep"> -+ <argument name="title" value="Bulk Images, Price and Quantity"/> -+ </actionGroup> -+ -+ <!--Select Apply unique images by attribute to each SKU and color attribute in dropdown in Images--> -+ <click selector="{{AdminCreateProductConfigurationsPanel.applyUniqueImagesToEachSkus}}" stepKey="clickOnApplyUniqueImagesToEachSku"/> -+ <selectOption userInput="$$createConfigProductAttributeCreateConfigurableProduct.default_frontend_label$$" -+ selector="{{AdminCreateProductConfigurationsPanel.selectImagesButton}}" stepKey="selectAttributeOption"/> -+ -+ <!-- Add images to configurable product attribute options --> -+ <actionGroup ref="addUniqueImageToConfigurableProductOption" stepKey="addImageToConfigurableProductOptionOne"> -+ <argument name="image" value="ImageUpload"/> -+ <argument name="frontend_label" value="$$createConfigProductAttributeCreateConfigurableProduct.default_frontend_label$$"/> -+ <argument name="label" value="$$getConfigAttributeOption1CreateConfigurableProduct.label$$"/> -+ </actionGroup> -+ <actionGroup ref="addUniqueImageToConfigurableProductOption" stepKey="addImageToConfigurableProductOptionTwo"> -+ <argument name="image" value="ImageUpload_1"/> -+ <argument name="frontend_label" value="$$createConfigProductAttributeCreateConfigurableProduct.default_frontend_label$$"/> -+ <argument name="label" value="$$getConfigAttributeOption2CreateConfigurableProduct.label$$"/> -+ </actionGroup> -+ <actionGroup ref="addUniqueImageToConfigurableProductOption" stepKey="addImageToConfigurableProductOptionThree"> -+ <argument name="image" value="ImageUpload3"/> -+ <argument name="frontend_label" value="$$createConfigProductAttributeCreateConfigurableProduct.default_frontend_label$$"/> -+ <argument name="label" value="{{colorDefaultProductAttribute1.name}}"/> -+ </actionGroup> -+ -+ <!--Add prices to configurable product attribute options--> -+ <click selector="{{AdminCreateProductConfigurationsPanel.applyUniquePricesToEachSkus}}" stepKey="clickOnApplyUniquePricesByAttributeToEachSku"/> -+ <selectOption userInput="$$createConfigProductAttributeCreateConfigurableProduct.default_frontend_label$$" -+ selector="{{AdminCreateProductConfigurationsPanel.selectAttribute}}" stepKey="selectAttributes"/> -+ <fillField userInput="10" selector="{{AdminCreateProductConfigurationsPanel.price($$getConfigAttributeOption1CreateConfigurableProduct.label$$)}}" stepKey="fillAttributePrice"/> -+ <fillField userInput="20" selector="{{AdminCreateProductConfigurationsPanel.price($$getConfigAttributeOption2CreateConfigurableProduct.label$$)}}" stepKey="fillAttributePrice1"/> -+ <fillField userInput="30" selector="{{AdminCreateProductConfigurationsPanel.price(colorDefaultProductAttribute1.name)}}" stepKey="fillAttributePrice2"/> -+ -+ <!-- Add quantity to product attribute options --> -+ <click selector="{{AdminCreateProductConfigurationsPanel.applySingleQuantityToEachSkus}}" stepKey="clickOnApplySingleQuantityToEachSku"/> -+ <fillField selector="{{AdminCreateProductConfigurationsPanel.quantity}}" userInput="100" stepKey="enterAttributeQuantity"/> -+ -+ <!--Click Next button--> -+ <actionGroup ref="AdminConfigurableWizardMoveToNextStepActionGroup" stepKey="navigateToSummaryStep"> -+ <argument name="title" value="Summary"/> -+ </actionGroup> -+ -+ <!--Click Generate Configure button--> -+ <click selector="{{ConfigurableProductSection.generateConfigure}}" stepKey="clickGenerateConfigure"/> -+ <waitForPageLoad stepKey="waitForProductPageLoad"/> -+ -+ <actionGroup ref="saveProductForm" stepKey="saveProduct"/> -+ -+ <!--Go to frontend and check image and price--> -+ <amOnPage url="{{StorefrontProductPage.url($$createConfigProductCreateConfigurableProduct.custom_attributes[url_key]$$)}}" stepKey="goToProductPage"/> -+ -+ <actionGroup ref="AssertOptionImageAndPriceInStorefrontProductActionGroup" stepKey="assertFirstOptionImageAndPriceInStorefrontProductPage"> -+ <argument name="label" value="$$getConfigAttributeOption1CreateConfigurableProduct.label$$"/> -+ <argument name="image" value="{{ImageUpload.filename}}"/> -+ <argument name="price" value="10"/> -+ </actionGroup> -+ -+ <actionGroup ref="AssertOptionImageAndPriceInStorefrontProductActionGroup" stepKey="assertSecondOptionImageAndPriceInStorefrontProductPage"> -+ <argument name="label" value="$$getConfigAttributeOption2CreateConfigurableProduct.label$$"/> -+ <argument name="image" value="{{ImageUpload_1.filename}}"/> -+ <argument name="price" value="20"/> -+ </actionGroup> -+ -+ <actionGroup ref="AssertOptionImageAndPriceInStorefrontProductActionGroup" stepKey="assertThirdOptionImageAndPriceInStorefrontProductPage"> -+ <argument name="label" value="{{colorDefaultProductAttribute1.name}}"/> -+ <argument name="image" value="{{ImageUpload3.filename}}"/> -+ <argument name="price" value="30"/> -+ </actionGroup> -+ </test> -+</tests> -diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminAssertNoticeThatExistingSkuAutomaticallyChangedWhenSavingProductWithSameSkuTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminAssertNoticeThatExistingSkuAutomaticallyChangedWhenSavingProductWithSameSkuTest.xml -new file mode 100644 -index 00000000000..68bf703ecda ---- /dev/null -+++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminAssertNoticeThatExistingSkuAutomaticallyChangedWhenSavingProductWithSameSkuTest.xml -@@ -0,0 +1,75 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> -+ <test name="AdminAssertNoticeThatExistingSkuAutomaticallyChangedWhenSavingProductWithSameSkuTest"> -+ <annotations> -+ <features value="ConfigurableProduct"/> -+ <stories value="Create configurable product"/> -+ <title value="Assert notice that existing sku automatically changed when saving product with same sku"/> -+ <description value="Admin should not be able to create configurable product and two new options with the same sku"/> -+ <testCaseId value="MC-13693"/> -+ <severity value="CRITICAL"/> -+ <group value="mtf_migrated"/> -+ </annotations> -+ <before> -+ <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> -+ </before> -+ <after> -+ <!-- Delete configurable product --> -+ <actionGroup ref="deleteProductUsingProductGrid" stepKey="deleteProduct"> -+ <argument name="product" value="ApiConfigurableProduct"/> -+ </actionGroup> -+ -+ <!-- Delete product attribute --> -+ <actionGroup ref="deleteProductAttributeByLabel" stepKey="deleteProductAttribute"> -+ <argument name="ProductAttribute" value="colorProductAttribute"/> -+ </actionGroup> -+ -+ <!-- Log out --> -+ <actionGroup ref="logout" stepKey="logout"/> -+ </after> -+ -+ <!-- Create configurable product --> -+ <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="amOnProductGridPage"/> -+ <waitForPageLoad stepKey="waitForProductGridPageLoad"/> -+ <actionGroup ref="goToCreateProductPage" stepKey="createConfigurableProduct"> -+ <argument name="product" value="ApiConfigurableProduct"/> -+ </actionGroup> -+ -+ <!-- Fill configurable product values --> -+ <actionGroup ref="fillMainProductForm" stepKey="fillConfigurableProductValues"> -+ <argument name="product" value="ApiConfigurableProduct"/> -+ </actionGroup> -+ -+ <!--Create product configurations--> -+ <click selector="{{AdminProductFormConfigurationsSection.createConfigurations}}" stepKey="clickCreateConfigurations" after="fillConfigurableProductValues"/> -+ <waitForElementVisible selector="{{AdminCreateProductConfigurationsPanel.createNewAttribute}}" time="30" stepKey="waitForConfigurationModalOpen" after="clickCreateConfigurations"/> -+ -+ <!--Create new attribute with two option --> -+ <actionGroup ref="addNewProductConfigurationAttribute" stepKey="createProductConfigurationAttribute"> -+ <argument name="attribute" value="colorProductAttribute"/> -+ <argument name="firstOption" value="colorConfigurableProductAttribute1"/> -+ <argument name="secondOption" value="colorConfigurableProductAttribute2"/> -+ </actionGroup> -+ -+ <!-- Change products sku configurations in grid --> -+ <fillField userInput="{{ApiConfigurableProduct.sku}}" selector="{{AdminProductFormConfigurationsSection.confProductSkuCell(colorConfigurableProductAttribute1.name)}}" stepKey="fillFieldSkuForFirstAttributeOption"/> -+ <fillField userInput="{{ApiConfigurableProduct.sku}}" selector="{{AdminProductFormConfigurationsSection.confProductSkuCell(colorConfigurableProductAttribute2.name)}}" stepKey="fillFieldSkuForSecondAttributeOption"/> -+ -+ <!-- Save product --> -+ <waitForElementVisible selector="{{AdminProductFormActionSection.saveButton}}" stepKey="waitForSaveBtnVisible"/> -+ <click selector="{{AdminProductFormActionSection.saveButton}}" stepKey="saveProductAgain"/> -+ <waitForElementVisible selector="{{AdminChooseAffectedAttributeSetPopup.confirm}}" stepKey="waitPopUpVisible"/> -+ <click selector="{{AdminChooseAffectedAttributeSetPopup.confirm}}" stepKey="clickOnConfirmPopup"/> -+ -+ <!-- Assert product auto incremented sku notice message; see success message --> -+ <see selector="{{AdminMessagesSection.noticeMessage}}" stepKey="seeNoticeMessage" userInput="SKU for product {{ApiConfigurableProduct.name}} has been changed to {{ApiConfigurableProduct.sku}}-2."/> -+ <see selector="{{AdminMessagesSection.successMessage}}" stepKey="seeSuccessMessage" userInput="You saved the product."/> -+ </test> -+</tests> -\ No newline at end of file -diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminCheckConfigurableProductAttributeValueUniquenessTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminCheckConfigurableProductAttributeValueUniquenessTest.xml -new file mode 100644 -index 00000000000..df934446fc8 ---- /dev/null -+++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminCheckConfigurableProductAttributeValueUniquenessTest.xml -@@ -0,0 +1,72 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> -+ <test name="AdminCheckConfigurableProductAttributeValueUniquenessTest"> -+ <annotations> -+ <features value="ConfigurableProduct"/> -+ <title value="Attribute value validation (check for uniqueness) in configurable products"/> -+ <description value="Attribute value validation (check for uniqueness) in configurable products"/> -+ <severity value="MAJOR"/> -+ <testCaseId value="MC-17450"/> -+ <useCaseId value="MAGETWO-99443"/> -+ <group value="ConfigurableProduct"/> -+ </annotations> -+ <before> -+ <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> -+ <createData entity="dropdownProductAttribute" stepKey="createProductAttribute"/> -+ </before> -+ <after> -+ <!--Delete created data--> -+ <comment userInput="Delete created data" stepKey="deleteCreatedData"/> -+ <actionGroup ref="deleteProductUsingProductGrid" stepKey="deleteConfigurableProductAndOptions"> -+ <argument name="product" value="$$createConfigProduct$$"/> -+ </actionGroup> -+ <waitForPageLoad stepKey="waitForProductIndexPage"/> -+ <actionGroup ref="resetProductGridToDefaultView" stepKey="resetProductGridColumnsInitial"/> -+ <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> -+ <deleteData createDataKey="createProductAttribute" stepKey="deleteAttribute"/> -+ <actionGroup ref="logout" stepKey="logOut"/> -+ </after> -+ <!--Create configurable product--> -+ <comment userInput="Create configurable product" stepKey="createConfProd"/> -+ <createData entity="ApiCategory" stepKey="createCategory"/> -+ <createData entity="ApiConfigurableProduct" stepKey="createConfigProduct"> -+ <requiredEntity createDataKey="createCategory"/> -+ </createData> -+ <!--Go to created product page--> -+ <comment userInput="Go to created product page" stepKey="goToProdPage"/> -+ <amOnPage url="{{ProductCatalogPage.url}}" stepKey="goToProductGrid"/> -+ <waitForPageLoad stepKey="waitForProductPage1"/> -+ <actionGroup ref="filterProductGridByName2" stepKey="filterByName"> -+ <argument name="name" value="$$createConfigProduct.name$$"/> -+ </actionGroup> -+ <click selector="{{AdminProductGridSection.firstRow}}" stepKey="clickOnProductName"/> -+ <waitForPageLoad stepKey="waitForProductEditPageToLoad"/> -+ <!--Create configurations for the product--> -+ <comment userInput="Create configurations for the product" stepKey="createConfigurations"/> -+ <conditionalClick selector="{{AdminProductFormConfigurationsSection.sectionHeader}}" dependentSelector="{{AdminProductFormConfigurationsSection.createConfigurations}}" visible="false" stepKey="expandConfigurationsTab1"/> -+ <click selector="{{AdminProductFormConfigurationsSection.createConfigurations}}" stepKey="clickOnCreateConfigurations1"/> -+ <waitForPageLoad stepKey="waitForSelectAttributesPage1"/> -+ <actionGroup ref="selectCreatedAttributeAndCreateTwoOptions" stepKey="selectCreatedAttributeAndCreateOptions"> -+ <argument name="attribute" value="dropdownProductAttribute"/> -+ <argument name="firstOption" value="productAttributeOption1"/> -+ <argument name="secondOption" value="productAttributeOption1"/> -+ </actionGroup> -+ <!--Check that system does not allow to save 2 options with same name--> -+ <comment userInput="Check that system does not allow to save 2 options with same name" stepKey="checkOptionNameUniqueness"/> -+ <seeElement selector="{{AdminCreateProductConfigurationsPanel.attributeNameWithError}}" stepKey="seeThatOptionWithSameNameIsNotSaved"/> -+ <!--Click next and assert error message--> -+ <comment userInput="Click next and assert error message" stepKey="clickNextAndAssertErrMssg"/> -+ <click selector="{{AdminCreateProductConfigurationsPanel.next}}" stepKey="clickNext"/> -+ <waitForPageLoad time="10" stepKey="waitForPageLoad"/> -+ <grabTextFrom selector="{{AdminCreateProductConfigurationsPanel.attributeNameInTitle}}" stepKey="grabErrMsg"/> -+ <see userInput='The value of attribute "$grabErrMsg" must be unique' stepKey="verifyAttributesValueUniqueness"/> -+ </test> -+</tests> -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 00000000000..dd641fd370b ---- /dev/null -+++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminCheckValidatorConfigurableProductTest.xml -@@ -0,0 +1,126 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> -+ <test name="AdminCheckValidatorConfigurableProductTest"> -+ <annotations> -+ <stories value="Configurable Product"/> -+ <title value="Check that validator works correctly when creating Configurations for Configurable Products"/> -+ <description value="Verify validator works correctly for Configurable Products"/> -+ <severity value="MAJOR"/> -+ <testCaseId value="MAGETWO-95995"/> -+ <useCaseId value="MAGETWO-95834"/> -+ <group value="ConfigurableProduct"/> -+ </annotations> -+ -+ <before> -+ <!--Login as admin--> -+ <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> -+ <!--Create Category--> -+ <createData entity="ApiCategory" stepKey="createCategory"/> -+ <!--Create Configurable product--> -+ <createData entity="ApiConfigurableProduct" stepKey="createConfigProduct"> -+ <requiredEntity createDataKey="createCategory"/> -+ </createData> -+ </before> -+ -+ <after> -+ <!--Delete created data--> -+ <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> -+ <deleteData createDataKey="createConfigProduct" stepKey="deleteConfigProduct"/> -+ <actionGroup ref="deleteProductBySku" stepKey="deleteProduct"> -+ <argument name="sku" value="{{ApiConfigurableProduct.name}}-thisIsShortName"/> -+ </actionGroup> -+ <conditionalClick selector="{{AdminProductGridFilterSection.clearFilters}}" -+ dependentSelector="{{AdminProductGridFilterSection.clearFilters}}" visible="true" stepKey="clickClearFilters"/> -+ <!-- Remove attribute --> -+ <actionGroup ref="deleteProductAttribute" stepKey="deleteAttribute"> -+ <argument name="ProductAttribute" value="productDropDownAttribute"/> -+ </actionGroup> -+ <actionGroup ref="logout" stepKey="logout"/> -+ </after> -+ -+ <!-- Find the product that we just created using the product grid --> -+ <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="visitAdminProductPage"/> -+ <waitForPageLoad stepKey="waitForAdminProductPageLoad"/> -+ <conditionalClick selector="{{AdminProductGridFilterSection.clearFilters}}" -+ dependentSelector="{{AdminProductGridFilterSection.clearFilters}}" visible="true" stepKey="clickClearFiltersInitial"/> -+ <actionGroup ref="filterProductGridBySku" stepKey="findCreatedProduct"> -+ <argument name="product" value="ApiConfigurableProduct"/> -+ </actionGroup> -+ <waitForPageLoad stepKey="waitForProductFilterLoad"/> -+ <click selector="{{AdminProductGridSection.firstRow}}" stepKey="clickOnProductPage"/> -+ <waitForPageLoad stepKey="waitForProductPageLoad"/> -+ -+ <!-- Create configurations based off the Text Swatch we created earlier --> -+ <click selector="{{AdminProductFormConfigurationsSection.createConfigurations}}" stepKey="clickCreateConfigurations"/> -+ -+ <!--Create new attribute--> -+ <waitForElementVisible stepKey="waitForNewAttributePageOpened" selector="{{AdminCreateProductConfigurationsPanel.createNewAttribute}}"/> -+ <click selector="{{AdminCreateProductConfigurationsPanel.createNewAttribute}}" stepKey="clickCreateNewAttribute" after="waitForNewAttributePageOpened"/> -+ <switchToIFrame selector="{{AdminNewAttributePanel.newAttributeIFrame}}" stepKey="enterAttributePanelIFrame" after="clickCreateNewAttribute"/> -+ <waitForElementVisible selector="{{AdminNewAttributePanel.defaultLabel}}" time="30" stepKey="waitForIframeLoad" after="enterAttributePanelIFrame"/> -+ <fillField selector="{{AdminNewAttributePanel.defaultLabel}}" userInput="{{productDropDownAttribute.attribute_code}}" stepKey="fillDefaultLabel" after="waitForIframeLoad"/> -+ <selectOption selector="{{AdminNewAttributePanel.inputType}}" userInput="{{colorProductAttribute.input_type}}" stepKey="selectAttributeInputType" after="fillDefaultLabel"/> -+ <!--Add option to attribute--> -+ <click selector="{{AdminNewAttributePanel.addOption}}" stepKey="clickAddOption1" after="selectAttributeInputType"/> -+ <waitForElementVisible selector="{{AdminNewAttributePanel.isDefault('1')}}" time="30" stepKey="waitForOptionRow1" after="clickAddOption1"/> -+ <fillField selector="{{AdminNewAttributePanel.optionAdminValue('0')}}" userInput="ThisIsLongNameNameLengthMoreThanSixtyFourThisIsLongNameNameLength" stepKey="fillAdminLabel1" after="waitForOptionRow1"/> -+ <fillField selector="{{AdminNewAttributePanel.optionDefaultStoreValue('0')}}" userInput="{{colorProductAttribute1.name}}" stepKey="fillDefaultLabel1" after="fillAdminLabel1"/> -+ -+ <!--Save attribute--> -+ <click selector="{{AdminNewAttributePanel.saveAttribute}}" stepKey="clickOnNewAttributePanel"/> -+ <waitForPageLoad stepKey="waitForSaveAttribute"/> -+ <switchToIFrame stepKey="switchOutOfIFrame"/> -+ -+ <!--Find attribute in grid and select--> -+ <conditionalClick selector="{{AdminDataGridHeaderSection.clearFilters}}" dependentSelector="{{AdminDataGridHeaderSection.clearFilters}}" visible="true" stepKey="clearExistingFilters"/> -+ <click selector="{{AdminDataGridHeaderSection.filters}}" stepKey="clickOnFilters"/> -+ <fillField selector="{{AdminDataGridHeaderSection.attributeCodeFilterInput}}" userInput="{{productDropDownAttribute.attribute_code}}" stepKey="fillFilterAttributeCodeField"/> -+ <click selector="{{AdminDataGridHeaderSection.applyFilters}}" stepKey="clickApplyFiltersButton"/> -+ <click selector="{{AdminDataGridTableSection.rowCheckbox('1')}}" stepKey="clickOnFirstCheckbox"/> -+ <click selector="{{AdminCreateProductConfigurationsPanel.next}}" stepKey="clickNextStep1"/> -+ <waitForElementVisible selector="{{AdminCreateProductConfigurationsPanel.selectAllByAttribute(productDropDownAttribute.attribute_code)}}" stepKey="waitForNextPageOpened"/> -+ <click selector="{{AdminCreateProductConfigurationsPanel.selectAllByAttribute(productDropDownAttribute.attribute_code)}}" stepKey="clickSelectAll"/> -+ <click selector="{{AdminCreateProductConfigurationsPanel.next}}" stepKey="clickNextStep2"/> -+ <waitForElementVisible selector="{{AdminCreateProductConfigurationsPanel.applySinglePriceToAllSkus}}" stepKey="waitForNextPageOpened2"/> -+ <click selector="{{AdminCreateProductConfigurationsPanel.applySinglePriceToAllSkus}}" stepKey="clickOnApplySinglePriceToAllSkus"/> -+ <fillField selector="{{AdminCreateProductConfigurationsPanel.singlePrice}}" userInput="10" stepKey="enterAttributePrice"/> -+ <click selector="{{AdminCreateProductConfigurationsPanel.applySingleQuantityToEachSkus}}" stepKey="clickOnApplySingleQuantityToEachSku"/> -+ <fillField selector="{{AdminCreateProductConfigurationsPanel.quantity}}" userInput="100" stepKey="enterAttributeQuantity"/> -+ <click selector="{{AdminCreateProductConfigurationsPanel.next}}" stepKey="clickOnNextStep3"/> -+ <waitForElementVisible selector="{{AdminCreateProductConfigurationsPanel.next}}" stepKey="waitForNextPageOpened3"/> -+ <click selector="{{AdminCreateProductConfigurationsPanel.next}}" stepKey="generateProducts"/> -+ <waitForElementVisible selector="{{AdminProductFormActionSection.saveButton}}" stepKey="waitForSaveButtonVisible"/> -+ <click selector="{{AdminProductFormActionSection.saveButton}}" stepKey="saveProduct"/> -+ <waitForElementVisible selector="{{AdminChooseAffectedAttributeSetPopup.confirm}}" stepKey="waitForPopUpVisible"/> -+ <click selector="{{AdminChooseAffectedAttributeSetPopup.confirm}}" stepKey="clickOnConfirmInPopup"/> -+ <dontSeeElement selector="{{AdminMessagesSection.success}}" stepKey="dontSeeSaveProductMessage"/> -+ -+ <!--Close modal window--> -+ <click selector="{{AdminChooseAffectedAttributeSetPopup.closePopUp}}" stepKey="clickOnClosePopup"/> -+ <waitForElementNotVisible selector="{{AdminChooseAffectedAttributeSetPopup.closePopUp}}" stepKey="waitForDialogClosed"/> -+ -+ <!--See that validation message is shown under the fields--> -+ <scrollTo selector="{{AdminProductFormConfigurationsSection.currentVariationsSkuCells}}" stepKey="scrollTConfigurationTab"/> -+ <see userInput="Please enter less or equal than 64 symbols." selector="{{AdminProductFormConfigurationsSection.confProductSkuMessage('0')}}" stepKey="SeeValidationMessage"/> -+ -+ <!--Edit "SKU" with valid quantity--> -+ <fillField stepKey="fillValidValue" selector="{{AdminProductFormConfigurationsSection.confProductSku('0')}}" userInput="{{ApiConfigurableProduct.name}}-thisIsShortName"/> -+ -+ <!--Click on "Save"--> -+ <waitForElementVisible selector="{{AdminProductFormActionSection.saveButton}}" stepKey="waitForSaveBtnVisible"/> -+ <click selector="{{AdminProductFormActionSection.saveButton}}" stepKey="saveProductAgain"/> -+ -+ <!--Click on "Confirm". Product is saved, success message appears --> -+ <waitForElementVisible selector="{{AdminChooseAffectedAttributeSetPopup.confirm}}" stepKey="waitPopUpVisible"/> -+ <click selector="{{AdminChooseAffectedAttributeSetPopup.confirm}}" stepKey="clickOnConfirmPopup"/> -+ <seeElement selector="{{AdminMessagesSection.success}}" stepKey="seeSaveProductMessage"/> -+ </test> -+</tests> -diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductCreateTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductCreateTest.xml -index 17ace8419c0..2af85e1bac0 100644 ---- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductCreateTest.xml -+++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductCreateTest.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="AdminConfigurableProductCreateTest"> - <annotations> - <features value="ConfigurableProduct"/> -@@ -36,6 +36,7 @@ - </actionGroup> - - <!-- assert color configurations on the admin create product page --> -+ <dontSee selector="{{AdminProductFormConfigurationsSection.variationLabel}}" stepKey="seeLabelNotVisible"/> - <seeNumberOfElements selector="{{AdminProductFormConfigurationsSection.currentVariationsRows}}" userInput="3" stepKey="seeNumberOfRows"/> - <see selector="{{AdminProductFormConfigurationsSection.currentVariationsNameCells}}" userInput="{{colorProductAttribute1.name}}" stepKey="seeAttributeName1InField"/> - <see selector="{{AdminProductFormConfigurationsSection.currentVariationsNameCells}}" userInput="{{colorProductAttribute2.name}}" stepKey="seeAttributeName2InField"/> -@@ -68,4 +69,71 @@ - <see selector="{{StorefrontProductInfoMainSection.productAttributeOptions1}}" userInput="{{colorProductAttribute2.name}}" stepKey="seeInDropDown2"/> - <see selector="{{StorefrontProductInfoMainSection.productAttributeOptions1}}" userInput="{{colorProductAttribute3.name}}" stepKey="seeInDropDown3"/> - </test> -+ -+ <test name="AdminCreateConfigurableProductAfterGettingIncorrectSKUMessageTest"> -+ <annotations> -+ <features value="ConfigurableProduct"/> -+ <stories value="Create, Read, Update, Delete"/> -+ <title value="admin should be able to create a configurable product after incorrect sku"/> -+ <description value="admin should be able to create a configurable product after incorrect sku"/> -+ <severity value="MAJOR"/> -+ <testCaseId value="MAGETWO-96365"/> -+ <useCaseId value="MAGETWO-94556"/> -+ <group value="ConfigurableProduct"/> -+ </annotations> -+ -+ <before> -+ <createData entity="ApiCategory" stepKey="createCategory"/> -+ <createData entity="ApiConfigurableProduct" stepKey="createConfigProduct"> -+ <requiredEntity createDataKey="createCategory"/> -+ </createData> -+ <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> -+ </before> -+ <after> -+ <deleteData createDataKey="createConfigProduct" stepKey="deleteConfigProduct"/> -+ <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> -+ <actionGroup ref="logout" stepKey="logout"/> -+ </after> -+ -+ <amOnPage url="{{AdminProductEditPage.url($$createConfigProduct.id$$)}}" stepKey="goToEditPage"/> -+ <waitForPageLoad stepKey="waitForProductPage"/> -+ <conditionalClick selector="{{AdminProductFormConfigurationsSection.sectionHeader}}" dependentSelector="{{AdminProductFormConfigurationsSection.createConfigurations}}" visible="false" stepKey="openConfigurationSection"/> -+ <click selector="{{AdminProductFormConfigurationsSection.createConfigurations}}" stepKey="openConfigurationPane"/> -+ <click selector="{{AdminCreateProductConfigurationsPanel.filters}}" stepKey="clickFilters"/> -+ <fillField selector="{{AdminCreateProductConfigurationsPanel.attributeCode}}" userInput="color" stepKey="fillFilterAttributeCodeField"/> -+ <click selector="{{AdminCreateProductConfigurationsPanel.applyFilters}}" stepKey="clickApplyFiltersButton"/> -+ <click selector="{{AdminCreateProductConfigurationsPanel.firstCheckbox}}" stepKey="clickOnFirstCheckbox"/> -+ <click selector="{{AdminCreateProductConfigurationsPanel.next}}" stepKey="clickOnNextButton1"/> -+ <click selector="{{AdminCreateProductConfigurationsPanel.createNewValue}}" stepKey="clickOnCreateNewValue1"/> -+ <fillField userInput="{{colorProductAttribute2.name}}" selector="{{AdminCreateProductConfigurationsPanel.attributeName}}" stepKey="fillFieldForNewAttribute1"/> -+ <click selector="{{AdminCreateProductConfigurationsPanel.saveAttribute}}" stepKey="clickOnSaveNewAttribute1"/> -+ <click selector="{{AdminCreateProductConfigurationsPanel.next}}" stepKey="clickOnNextButton2"/> -+ <click selector="{{AdminCreateProductConfigurationsPanel.next}}" stepKey="clickOnNextButton3"/> -+ <click selector="{{ConfigurableProductSection.generateConfigure}}" stepKey="generateConfigure"/> -+ <waitForPageLoad stepKey="waitForGenerateConfigure"/> -+ <grabValueFrom selector="{{AdminProductFormConfigurationsSection.firstSKUInConfigurableProductsGrid}}" stepKey="grabTextFromContent"/> -+ <fillField stepKey="fillMoreThan64Symbols" selector="{{AdminProductFormConfigurationsSection.firstSKUInConfigurableProductsGrid}}" userInput="01234567890123456789012345678901234567890123456789012345678901234"/> -+ <click selector="{{AdminProductFormActionSection.saveButton}}" stepKey="saveProduct1"/> -+ <conditionalClick selector="{{AdminChooseAffectedAttributeSetPopup.closePopUp}}" dependentSelector="{{AdminChooseAffectedAttributeSetPopup.closePopUp}}" visible="true" stepKey="clickOnCloseInPopup"/> -+ <see stepKey="seeErrorMessage" userInput="Please enter less or equal than 64 symbols."/> -+ <fillField stepKey="fillCorrectSKU" selector="{{AdminProductFormConfigurationsSection.firstSKUInConfigurableProductsGrid}}" userInput="$grabTextFromContent"/> -+ <click selector="{{AdminProductFormActionSection.saveButton}}" stepKey="saveProduct2"/> -+ <conditionalClick selector="{{AdminChooseAffectedAttributeSetPopup.confirm}}" dependentSelector="{{AdminChooseAffectedAttributeSetPopup.confirm}}" visible="true" stepKey="clickOnConfirmInPopup"/> -+ <see userInput="You saved the product." stepKey="seeSaveConfirmation"/> -+ <amOnPage url="{{AdminProductAttributeGridPage.url}}" stepKey="goToProductAttributes"/> -+ <waitForPageLoad stepKey="waitForProductAttributes"/> -+ <click selector="{{AdminProductAttributeGridSection.ResetFilter}}" stepKey="resetFiltersOnGrid1"/> -+ <fillField selector="{{AdminProductAttributeGridSection.FilterByAttributeCode}}" userInput="color" stepKey="fillFilter"/> -+ <click selector="{{AdminProductAttributeGridSection.Search}}" stepKey="clickSearch"/> -+ <click selector="{{AdminProductAttributeGridSection.AttributeCode('color')}}" stepKey="clickRowToEdit"/> -+ <click selector="{{DropdownAttributeOptionsSection.deleteButton(1)}}" stepKey="deleteOption"/> -+ <click selector="{{AttributePropertiesSection.Save}}" stepKey="saveAttribute"/> -+ <click selector="{{AdminProductAttributeGridSection.ResetFilter}}" stepKey="resetFiltersOnGrid2"/> -+ <actionGroup stepKey="deleteProduct1" ref="deleteProductBySku"> -+ <argument name="sku" value="$grabTextFromContent"/> -+ </actionGroup> -+ <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="visitAdminProductPage"/> -+ <waitForPageLoad time="60" stepKey="waitForPageLoadInitial"/> -+ <conditionalClick selector="{{AdminProductGridFilterSection.clearFilters}}" dependentSelector="{{AdminProductGridFilterSection.clearFilters}}" visible="true" stepKey="clickClearFiltersInitial"/> -+ </test> - </tests> -diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductDeleteTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductDeleteTest.xml -index 17f17323a9e..1a694b8adf1 100644 ---- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductDeleteTest.xml -+++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductDeleteTest.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="AdminConfigurableProductDeleteTest"> - <annotations> - <features value="ConfigurableProduct"/> -@@ -16,6 +16,7 @@ - <description value="admin should be able to delete a configurable product"/> - <testCaseId value="MC-87"/> - <group value="ConfigurableProduct"/> -+ <severity value="AVERAGE"/> - </annotations> - - <before> -@@ -106,6 +107,7 @@ - <description value="admin should be able to mass delete configurable products"/> - <testCaseId value="MC-99"/> - <group value="ConfigurableProduct"/> -+ <severity value="AVERAGE"/> - </annotations> - - <before> -diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductLongSkuTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductLongSkuTest.xml -new file mode 100644 -index 00000000000..c599a6a23f1 ---- /dev/null -+++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductLongSkuTest.xml -@@ -0,0 +1,108 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> -+ <test name="AdminConfigurableProductLongSkuTest"> -+ <annotations> -+ <features value="ConfigurableProduct"/> -+ <stories value="Create configurable product"/> -+ <title value="Admin is able to create an product with a long sku below that is below the character limit"/> -+ <description value="Try to create a product with sku slightly less than char limit. Get client side SKU length error for child products. Correct SKUs and save product succeeds."/> -+ <severity value="MAJOR"/> -+ <testCaseId value="MC-5685"/> -+ <group value="ConfigurableProduct"/> -+ </annotations> -+ -+ <before> -+ <!--Create product attribute with options--> -+ <createData entity="productAttributeWithTwoOptions" stepKey="createConfigProductAttribute"/> -+ <createData entity="productAttributeOption1" stepKey="createConfigProductAttributeOption1"> -+ <requiredEntity createDataKey="createConfigProductAttribute"/> -+ </createData> -+ <createData entity="productAttributeOption2" stepKey="createConfigProductAttributeOption2"> -+ <requiredEntity createDataKey="createConfigProductAttribute"/> -+ </createData> -+ <createData entity="AddToDefaultSet" stepKey="createConfigAddToAttributeSet"> -+ <requiredEntity createDataKey="createConfigProductAttribute"/> -+ </createData> -+ <getData entity="ProductAttributeOptionGetter" index="1" stepKey="getConfigAttributeOption1"> -+ <requiredEntity createDataKey="createConfigProductAttribute"/> -+ </getData> -+ <getData entity="ProductAttributeOptionGetter" index="2" stepKey="getConfigAttributeOption2"> -+ <requiredEntity createDataKey="createConfigProductAttribute"/> -+ </getData> -+ <!--Create Category--> -+ <createData entity="ApiCategory" stepKey="createCategory"/> -+ <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin1"/> -+ </before> -+ -+ <after> -+ <!--Clean up products--> -+ <actionGroup ref="deleteProductByName" stepKey="cleanUpProducts"> -+ <argument name="sku" value="{{ProductWithLongNameSku.sku}}"/> -+ <argument name="name" value="{{ProductWithLongNameSku.name}}"/> -+ </actionGroup> -+ <!--Clean up attribute--> -+ <deleteData createDataKey="createConfigProductAttribute" stepKey="deleteConfigProductAttribute"/> -+ <!--Clean up category--> -+ <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> -+ <amOnPage url="admin/admin/auth/logout/" stepKey="amOnLogoutPage"/> -+ </after> -+ -+ <!--Create a configurable product with long name and sku--> -+ <amOnPage url="{{AdminProductCreatePage.url(AddToDefaultSet.attributeSetId, 'configurable')}}" stepKey="goToProductCreatePage"/> -+ <waitForPageLoad stepKey="waitForProductCreatePage"/> -+ <fillField selector="{{AdminProductFormSection.productName}}" userInput="{{ProductWithLongNameSku.name}}" stepKey="fillProductName"/> -+ <fillField selector="{{AdminProductFormSection.productSku}}" userInput="{{ProductWithLongNameSku.sku}}" stepKey="fillProductSku"/> -+ <fillField selector="{{AdminProductFormSection.productPrice}}" userInput="{{ProductWithLongNameSku.price}}" stepKey="fillProductPrice"/> -+ <searchAndMultiSelectOption selector="{{AdminProductFormSection.categoriesDropdown}}" parameterArray="[$$createCategory.name$$]" stepKey="selectCategory"/> -+ <!--Setup configurations--> -+ <actionGroup ref="generateConfigurationsByAttributeCode" stepKey="setupConfigurations"> -+ <argument name="attributeCode" value="$$createConfigProductAttribute.attribute_code$$"/> -+ </actionGroup> -+ -+ <!--See SKU length errors in Current Variations grid--> -+ <click selector="{{AdminProductFormActionSection.saveButton}}" stepKey="saveProductFail"/> -+ <seeInCurrentUrl url="{{AdminProductCreatePage.url(AddToDefaultSet.attributeSetId, 'configurable')}}" stepKey="seeRemainOnCreateProductPage"/> -+ <see selector="{{AdminProductFormConfigurationsSection.variationsSkuInputErrorByRow('1')}}" userInput="Please enter less or equal than 64 symbols." stepKey="seeSkuTooLongError1"/> -+ <see selector="{{AdminProductFormConfigurationsSection.variationsSkuInputErrorByRow('2')}}" userInput="Please enter less or equal than 64 symbols." stepKey="seeSkuTooLongError2"/> -+ <!--Fix SKU lengths--> -+ <fillField selector="{{AdminProductFormConfigurationsSection.variationsSkuInputByRow('1')}}" userInput="LongSku-$$getConfigAttributeOption1.label$$" stepKey="fixConfigurationSku1"/> -+ <fillField selector="{{AdminProductFormConfigurationsSection.variationsSkuInputByRow('2')}}" userInput="LongSku-$$getConfigAttributeOption2.label$$" stepKey="fixConfigurationSku2"/> -+ <!--Save product successfully--> -+ <click selector="{{AdminProductFormActionSection.saveButton}}" stepKey="saveProductSuccess"/> -+ <seeElement selector="{{AdminProductMessagesSection.successMessage}}" stepKey="seeSaveProductMessage"/> -+ -+ <!--Assert configurations on the product edit pag--> -+ <seeNumberOfElements selector="{{AdminProductFormConfigurationsSection.currentVariationsRows}}" userInput="2" stepKey="seeNumberOfRows"/> -+ <see selector="{{AdminProductFormConfigurationsSection.currentVariationsNameCells}}" userInput="{{ProductWithLongNameSku.name}}-$$getConfigAttributeOption1.label$$" stepKey="seeChildProductName1"/> -+ <see selector="{{AdminProductFormConfigurationsSection.currentVariationsNameCells}}" userInput="{{ProductWithLongNameSku.name}}-$$getConfigAttributeOption2.label$$" stepKey="seeChildProductName2"/> -+ <see selector="{{AdminProductFormConfigurationsSection.currentVariationsSkuCells}}" userInput="LongSku-$$getConfigAttributeOption1.label$$" stepKey="seeChildProductSku1"/> -+ <see selector="{{AdminProductFormConfigurationsSection.currentVariationsSkuCells}}" userInput="LongSku-$$getConfigAttributeOption2.label$$" stepKey="seeChildProductSku2"/> -+ <see selector="{{AdminProductFormConfigurationsSection.currentVariationsPriceCells}}" userInput="{{ProductWithLongNameSku.price}}" stepKey="seeConfigurationsPrice"/> -+ -+ <!--Assert storefront category list page--> -+ <amOnPage url="/" stepKey="amOnStorefront"/> -+ <waitForPageLoad stepKey="waitForStorefrontLoad"/> -+ <click userInput="$$createCategory.name$$" stepKey="clickOnCategoryName"/> -+ <waitForPageLoad stepKey="waitForCategoryPageLoad"/> -+ <see userInput="{{ProductWithLongNameSku.name}}" stepKey="assertProductPresent"/> -+ <see userInput="{{ProductWithLongNameSku.price}}" stepKey="assertProductPricePresent"/> -+ -+ <!--Assert storefront product details page--> -+ <click selector="{{StorefrontCategoryProductSection.ProductTitleByName(ProductWithLongNameSku.name)}}" stepKey="clickOnProductName"/> -+ <waitForPageLoad stepKey="waitForProductPageLoad"/> -+ <seeInTitle userInput="{{ProductWithLongNameSku.name}}" stepKey="assertProductNameTitle"/> -+ <see userInput="{{ProductWithLongNameSku.name}}" selector="{{StorefrontProductInfoMainSection.productName}}" stepKey="assertProductName"/> -+ <see userInput="{{ProductWithLongNameSku.sku}}" selector="{{StorefrontProductInfoMainSection.productSku}}" stepKey="assertProductSku"/> -+ <see selector="{{StorefrontProductInfoMainSection.productAttributeTitle1}}" userInput="$$createConfigProductAttribute.default_frontend_label$$" stepKey="seeColorAttributeName1"/> -+ <see selector="{{StorefrontProductInfoMainSection.productAttributeOptions1}}" userInput="$$getConfigAttributeOption1.label$$" stepKey="seeInDropDown1"/> -+ <see selector="{{StorefrontProductInfoMainSection.productAttributeOptions1}}" userInput="$$getConfigAttributeOption2.label$$" stepKey="seeInDropDown2"/> -+ </test> -+</tests> -diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductOutOfStockTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductOutOfStockTest.xml -index c612431ec70..5633c3675ca 100644 ---- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductOutOfStockTest.xml -+++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductOutOfStockTest.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="AdminConfigurableProductChildrenOutOfStockTest"> - <annotations> - <features value="ConfigurableProduct"/> -diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductSearchTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductSearchTest.xml -index 77ccf7bc690..059a18200e9 100644 ---- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductSearchTest.xml -+++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductSearchTest.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="AdminConfigurableProductSearchTest"> - <annotations> - <features value="ConfigurableProduct"/> -@@ -16,6 +16,7 @@ - <description value="admin should be able to search for a configurable product"/> - <testCaseId value="MC-100"/> - <group value="ConfigurableProduct"/> -+ <severity value="AVERAGE"/> - </annotations> - - <before> -@@ -94,6 +95,7 @@ - <description value="admin should be able to filter by type configurable product"/> - <testCaseId value="MC-66"/> - <group value="ConfigurableProduct"/> -+ <severity value="AVERAGE"/> - </annotations> - - <before> -diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductSetEditContentTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductSetEditContentTest.xml -new file mode 100644 -index 00000000000..42e12852f56 ---- /dev/null -+++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductSetEditContentTest.xml -@@ -0,0 +1,40 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> -+ <test name="AdminConfigurableProductSetEditContentTest" extends="AdminSimpleProductSetEditContentTest"> -+ <annotations> -+ <features value="ConfigurableProduct"/> -+ <stories value="Create/edit configurable product"/> -+ <title value="Admin should be able to set/edit product Content when editing a configurable product"/> -+ <description value="Admin should be able to set/edit product Content when editing a configurable product"/> -+ <severity value="CRITICAL"/> -+ <testCaseId value="MC-3424"/> -+ <group value="ConfigurableProduct"/> -+ <group value="WYSIWYGDisabled"/> -+ </annotations> -+ <after> -+ <!-- Delete configurable product --> -+ <actionGroup ref="deleteProductUsingProductGrid" stepKey="deleteProduct"> -+ <argument name="product" value="BaseConfigurableProduct"/> -+ </actionGroup> -+ </after> -+ -+ <!-- Create product --> -+ <actionGroup ref="goToCreateProductPage" stepKey="goToCreateProduct"> -+ <argument name="product" value="BaseConfigurableProduct"/> -+ </actionGroup> -+ <actionGroup ref="fillMainProductFormNoWeight" stepKey="fillProductForm"> -+ <argument name="product" value="BaseConfigurableProduct"/> -+ </actionGroup> -+ -+ <!--Checking content storefront--> -+ <amOnPage url="{{BaseConfigurableProduct.name}}.html" stepKey="goToStorefront"/> -+ </test> -+</tests> -diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductUpdateAttributeTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductUpdateAttributeTest.xml -index 2282da467a9..001d4d17ec2 100644 ---- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductUpdateAttributeTest.xml -+++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductUpdateAttributeTest.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="AdminConfigurableProductUpdateAttributeTest"> - <annotations> - <features value="ConfigurableProduct"/> -@@ -228,12 +228,12 @@ - <waitForPageLoad stepKey="waitForAdminProductPageLoad"/> - <conditionalClick selector="{{AdminProductGridFilterSection.clearFilters}}" dependentSelector="{{AdminProductGridFilterSection.clearFilters}}" visible="true" stepKey="clickClearFiltersInitial"/> - <actionGroup ref="filterProductGridBySku" stepKey="findCreatedProduct"> -- <argument name="product" value="ApiConfigurableProduct"/> -+ <argument name="product" value="$$createConfigProduct$$"/> - </actionGroup> - <waitForPageLoad stepKey="waitForProductFilterLoad"/> -- -- <click selector="{{AdminProductGridSection.firstRow}}" stepKey="clickOnProductPage"/> -- <waitForPageLoad stepKey="waitForProductPageLoad"/> -+ <actionGroup ref="OpenEditProductOnBackendActionGroup" stepKey="openProduct"> -+ <argument name="product" value="$$createConfigProduct$$" /> -+ </actionGroup> - - <!-- Open the wizard for editing configurations and fill out a new attribute --> - <click stepKey="clickEditConfig" selector="{{AdminProductFormConfigurationsSection.createConfigurations}}"/> -diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductUpdateTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductUpdateTest.xml -index 154ce019f8c..1791fc002ab 100644 ---- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductUpdateTest.xml -+++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductUpdateTest.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="AdminConfigurableProductBulkUpdateTest"> - <annotations> - <features value="ConfigurableProduct"/> -@@ -16,6 +16,10 @@ - <description value="admin should be able to bulk update attributes of configurable products"/> - <testCaseId value="MC-88"/> - <group value="ConfigurableProduct"/> -+ <severity value="AVERAGE"/> -+ <skip> -+ <issueId value="MC-17140"/> -+ </skip> - </annotations> - - <before> -@@ -56,7 +60,13 @@ - <click selector="{{AdminUpdateAttributesSection.toggleDescription}}" stepKey="clickToggleDescription"/> - <fillField selector="{{AdminUpdateAttributesSection.description}}" userInput="MFTF automation!" stepKey="fillDescription"/> - <click selector="{{AdminUpdateAttributesSection.saveButton}}" stepKey="clickSave"/> -- <see selector="{{AdminProductMessagesSection.successMessage}}" userInput="A total of 3 record(s) were updated." stepKey="seeSaveSuccess"/> -+ <see selector="{{AdminProductMessagesSection.successMessage}}" userInput="Message is added to queue" stepKey="seeSaveSuccess"/> -+ -+ <!-- Run cron twice --> -+ <magentoCLI command="cron:run" stepKey="runCron1"/> -+ <magentoCLI command="cron:run" stepKey="runCron2"/> -+ <reloadPage stepKey="refreshPage"/> -+ <waitForPageLoad stepKey="waitFormToReload1"/> - - <!-- Check storefront for description --> - <amOnPage url="$$createProduct1.sku$$.html" stepKey="gotoProduct1"/> -@@ -78,6 +88,7 @@ - <description value="Admin should be able to remove a product configuration"/> - <testCaseId value="MC-63"/> - <group value="ConfigurableProduct"/> -+ <severity value="AVERAGE"/> - </annotations> - - <before> -@@ -170,6 +181,7 @@ - <description value="Admin should be able to disable a product configuration"/> - <testCaseId value="MC-119"/> - <group value="ConfigurableProduct"/> -+ <severity value="AVERAGE"/> - </annotations> - - <before> -diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableSetEditRelatedProductsTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableSetEditRelatedProductsTest.xml -new file mode 100644 -index 00000000000..11f1e9bb33c ---- /dev/null -+++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableSetEditRelatedProductsTest.xml -@@ -0,0 +1,43 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> -+ <test name="AdminConfigurableSetEditRelatedProductsTest" extends="AdminSimpleSetEditRelatedProductsTest"> -+ <annotations> -+ <features value="ConfigurableProduct"/> -+ <stories value="Create/Edit configurable product"/> -+ <title value="Admin should be able to set/edit Related Products information when editing a configurable product"/> -+ <description value="Admin should be able to set/edit Related Products information when editing a configurable product"/> -+ <severity value="CRITICAL"/> -+ <testCaseId value="MC-3414"/> -+ <group value="ConfigurableProduct"/> -+ <group value="WYSIWYGDisabled"/> -+ </annotations> -+ <before> -+ <createData entity="ApiCategory" stepKey="createCategory"/> -+ </before> -+ <after> -+ <!-- Delete configurable product --> -+ <actionGroup ref="deleteProductUsingProductGrid" stepKey="deleteProduct"> -+ <argument name="product" value="_defaultProduct"/> -+ </actionGroup> -+ <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> -+ </after> -+ -+ <!-- Create product --> -+ <remove keyForRemoval="goToCreateProduct"/> -+ <actionGroup ref="createConfigurableProduct" stepKey="fillProductForm"> -+ <argument name="product" value="_defaultProduct"/> -+ <argument name="category" value="$$createCategory$$"/> -+ </actionGroup> -+ -+ <!--See related product in storefront--> -+ <amOnPage url="{{_defaultProduct.urlKey}}.html" stepKey="goToStorefront"/> -+ </test> -+</tests> -diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminCreateAndSwitchProductType.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminCreateAndSwitchProductType.xml -new file mode 100755 -index 00000000000..2cc71964042 ---- /dev/null -+++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminCreateAndSwitchProductType.xml -@@ -0,0 +1,192 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> -+ <test name="AdminCreateConfigurableProductSwitchToSimpleTest" extends="AdminCreateSimpleProductSwitchToVirtualTest"> -+ <annotations> -+ <features value="Catalog"/> -+ <stories value="Product Type Switching"/> -+ <title value="Admin should be able to switch a new product from configurable to simple"/> -+ <description value="After selecting a configurable product when adding Admin should be switch to simple implicitly"/> -+ <severity value="CRITICAL"/> -+ <testCaseId value="MC-10926"/> -+ <group value="catalog"/> -+ <group value="mtf_migrated"/> -+ </annotations> -+ <actionGroup ref="GoToSpecifiedCreateProductPage" stepKey="openProductFillForm"> -+ <argument name="productType" value="configurable"/> -+ </actionGroup> -+ <actionGroup ref="fillMainProductForm" stepKey="fillProductForm"> -+ <argument name="product" value="_defaultProduct"/> -+ </actionGroup> -+ <see selector="{{AdminProductGridSection.productGridCell('1', 'Type')}}" userInput="Simple Product" stepKey="seeProductTypeInGrid"/> -+ </test> -+ <test name="AdminCreateConfigurableProductSwitchToVirtualTest" extends="AdminCreateSimpleProductSwitchToVirtualTest"> -+ <annotations> -+ <features value="Catalog"/> -+ <stories value="Product Type Switching"/> -+ <title value="Admin should be able to switch a new product from configurable to virtual"/> -+ <description value="After selecting a configurable product when adding Admin should be switch to virtual implicitly"/> -+ <severity value="CRITICAL"/> -+ <testCaseId value="MC-10927"/> -+ <group value="catalog"/> -+ <group value="mtf_migrated"/> -+ </annotations> -+ <actionGroup ref="GoToSpecifiedCreateProductPage" stepKey="openProductFillForm"> -+ <argument name="productType" value="configurable"/> -+ </actionGroup> -+ <see selector="{{AdminProductGridSection.productGridCell('1', 'Type')}}" userInput="Virtual Product" stepKey="seeProductTypeInGrid"/> -+ </test> -+ <test name="AdminCreateVirtualProductSwitchToConfigurableTest" extends="AdminCreateSimpleProductSwitchToVirtualTest"> -+ <annotations> -+ <features value="Catalog"/> -+ <stories value="Product Type Switching"/> -+ <title value="Admin should be able to switch a new product from virtual to configurable"/> -+ <description value="After selecting a virtual product when adding Admin should be switch to configurable implicitly"/> -+ <severity value="CRITICAL"/> -+ <testCaseId value="MC-10930"/> -+ <group value="catalog"/> -+ <group value="mtf_migrated"/> -+ </annotations> -+ <before> -+ <createData entity="productAttributeWithTwoOptions" stepKey="createConfigProductAttribute"/> -+ <createData entity="productAttributeOption1" stepKey="createConfigProductAttributeOption1"> -+ <requiredEntity createDataKey="createConfigProductAttribute"/> -+ </createData> -+ </before> -+ <after> -+ <deleteData stepKey="deleteAttribute" createDataKey="createConfigProductAttribute"/> -+ </after> -+ <actionGroup ref="GoToSpecifiedCreateProductPage" stepKey="openProductFillForm"> -+ <argument name="productType" value="virtual"/> -+ </actionGroup> -+ <actionGroup ref="fillMainProductForm" stepKey="fillProductForm"> -+ <argument name="product" value="_defaultProduct"/> -+ </actionGroup> -+ <comment before="createConfiguration" stepKey="beforeCreateConfiguration" userInput="Adding Configuration to Product"/> -+ <actionGroup ref="generateConfigurationsByAttributeCode" stepKey="createConfiguration" after="fillProductForm"> -+ <argument name="attributeCode" value="$$createConfigProductAttribute.attribute_code$$"/> -+ </actionGroup> -+ <actionGroup ref="saveConfiguredProduct" stepKey="saveProductForm"/> -+ <see selector="{{AdminProductGridSection.productGridCell('2', 'Type')}}" userInput="Configurable Product" stepKey="seeProductTypeInGrid"/> -+ <actionGroup ref="VerifyOptionInProductStorefront" stepKey="verifyConfigurableOption" after="AssertProductInStorefrontProductPage"> -+ <argument name="attributeCode" value="$createConfigProductAttribute.default_frontend_label$"/> -+ <argument name="optionName" value="$createConfigProductAttributeOption1.option[store_labels][1][label]$"/> -+ </actionGroup> -+ </test> -+ <test name="AdminCreateSimpleProductSwitchToConfigurableTest" extends="AdminCreateSimpleProductSwitchToVirtualTest"> -+ <annotations> -+ <features value="Catalog"/> -+ <stories value="Product Type Switching"/> -+ <title value="Admin should be able to switch a new product from simple to configurable"/> -+ <description value="After selecting a simple product when adding Admin should be switch to configurable implicitly"/> -+ <severity value="CRITICAL"/> -+ <useCaseId value="MAGETWO-44165"/> -+ <testCaseId value="MAGETWO-29398"/> -+ <group value="catalog"/> -+ </annotations> -+ <before> -+ <createData entity="productAttributeWithTwoOptions" stepKey="createConfigProductAttribute"/> -+ <createData entity="productAttributeOption1" stepKey="createConfigProductAttributeOption1"> -+ <requiredEntity createDataKey="createConfigProductAttribute"/> -+ </createData> -+ </before> -+ <after> -+ <deleteData stepKey="deleteAttribute" createDataKey="createConfigProductAttribute"/> -+ </after> -+ <actionGroup ref="GoToSpecifiedCreateProductPage" stepKey="openProductFillForm"> -+ <argument name="productType" value="simple"/> -+ </actionGroup> -+ <!-- Create configurable product from simple product page--> -+ <comment userInput="Create configurable product" stepKey="commentCreateProduct"/> -+ <actionGroup ref="fillMainProductForm" stepKey="fillProductForm"> -+ <argument name="product" value="_defaultProduct"/> -+ </actionGroup> -+ <comment before="createConfiguration" stepKey="beforeCreateConfiguration" userInput="Adding Configuration to Product"/> -+ <actionGroup ref="generateConfigurationsByAttributeCode" stepKey="createConfiguration" after="fillProductForm"> -+ <argument name="attributeCode" value="$$createConfigProductAttribute.attribute_code$$"/> -+ </actionGroup> -+ <actionGroup ref="saveConfiguredProduct" stepKey="saveProductForm"/> -+ <see selector="{{AdminProductGridSection.productGridCell('2', 'Type')}}" userInput="Configurable Product" stepKey="seeProductTypeInGrid"/> -+ <!-- Verify product on store front --> -+ <comment userInput="Verify product on store front" stepKey="commentVerifyProductGrid"/> -+ <actionGroup ref="VerifyOptionInProductStorefront" stepKey="verifyConfigurableOption" after="AssertProductInStorefrontProductPage"> -+ <argument name="attributeCode" value="$createConfigProductAttribute.default_frontend_label$"/> -+ <argument name="optionName" value="$createConfigProductAttributeOption1.option[store_labels][1][label]$"/> -+ </actionGroup> -+ </test> -+ <test name="AdminCreateDownloadableProductSwitchToConfigurableTest"> -+ <annotations> -+ <features value="Catalog"/> -+ <stories value="Product Type Switching"/> -+ <title value="Admin should be able to switch a new product from downloadable to configurable"/> -+ <description value="After selecting a downloadable product when adding Admin should be switch to configurable implicitly"/> -+ <severity value="CRITICAL"/> -+ <useCaseId value="MAGETWO-44165"/> -+ <testCaseId value="MAGETWO-29398"/> -+ <group value="catalog"/> -+ <group value="mtf_migrated"/> -+ </annotations> -+ <before> -+ <createData entity="_defaultCategory" stepKey="createPreReqCategory"/> -+ <createData entity="productAttributeWithTwoOptions" stepKey="createConfigProductAttribute"/> -+ <createData entity="productAttributeOption1" stepKey="createConfigProductAttributeOption1"> -+ <requiredEntity createDataKey="createConfigProductAttribute"/> -+ </createData> -+ </before> -+ <after> -+ <actionGroup ref="GoToProductCatalogPage" stepKey="goToProductCatalogPage"/> -+ <actionGroup ref="deleteProductUsingProductGrid" stepKey="deleteConfigurableProduct"> -+ <argument name="product" value="_defaultProduct"/> -+ </actionGroup> -+ <actionGroup ref="resetProductGridToDefaultView" stepKey="resetSearch"/> -+ <deleteData createDataKey="createPreReqCategory" stepKey="deletePreReqCategory"/> -+ <deleteData stepKey="deleteAttribute" createDataKey="createConfigProductAttribute"/> -+ <actionGroup ref="logout" stepKey="logout"/> -+ </after> -+ <!-- Create configurable product from downloadable product page--> -+ <comment userInput="Create configurable product" stepKey="commentCreateProduct"/> -+ <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin1"/> -+ <!-- Open Dropdown and select downloadable product option --> -+ <comment stepKey="beforeOpenProductFillForm" userInput="Selecting Product from the Add Product Dropdown"/> -+ <actionGroup ref="GoToSpecifiedCreateProductPage" stepKey="openProductFillForm"> -+ <argument name="productType" value="downloadable"/> -+ </actionGroup> -+ <scrollTo selector="{{AdminProductDownloadableSection.sectionHeader}}" stepKey="scrollToDownloadableInfo" /> -+ <uncheckOption selector="{{AdminProductDownloadableSection.isDownloadableProduct}}" stepKey="checkIsDownloadable"/> -+ <!-- Fill form for Downloadable Product Type --> -+ <comment stepKey="beforeFillProductForm" userInput="Filling Product Form"/> -+ <actionGroup ref="fillMainProductForm" stepKey="fillProductForm"> -+ <argument name="product" value="_defaultProduct"/> -+ </actionGroup> -+ <actionGroup ref="SetProductUrlKey" stepKey="setProductUrl"> -+ <argument name="product" value="_defaultProduct"/> -+ </actionGroup> -+ <comment before="createConfiguration" stepKey="beforeCreateConfiguration" userInput="Adding Configuration to Product"/> -+ <actionGroup ref="generateConfigurationsByAttributeCode" stepKey="createConfiguration"> -+ <argument name="attributeCode" value="$$createConfigProductAttribute.attribute_code$$"/> -+ </actionGroup> -+ <actionGroup ref="saveConfiguredProduct" stepKey="saveProductForm"/> -+ <!-- Check that product was added with implicit type change --> -+ <comment stepKey="beforeVerify" userInput="Verify Product Type Assigned Correctly"/> -+ <actionGroup ref="GoToProductCatalogPage" stepKey="goToProductCatalogPage"/> -+ <actionGroup ref="resetProductGridToDefaultView" stepKey="resetSearch"/> -+ <actionGroup ref="filterProductGridByName" stepKey="searchForProduct"> -+ <argument name="product" value="_defaultProduct"/> -+ </actionGroup> -+ <see selector="{{AdminProductGridSection.productGridCell('2', 'Type')}}" userInput="Configurable Product" stepKey="seeProductTypeInGrid"/> -+ <actionGroup ref="AssertProductInStorefrontProductPage" stepKey="assertProductInStorefrontProductPage"> -+ <argument name="product" value="_defaultProduct"/> -+ </actionGroup> -+ <actionGroup ref="VerifyOptionInProductStorefront" stepKey="verifyConfigurableOption"> -+ <argument name="attributeCode" value="$createConfigProductAttribute.default_frontend_label$"/> -+ <argument name="optionName" value="$createConfigProductAttributeOption1.option[store_labels][1][label]$"/> -+ </actionGroup> -+ </test> -+</tests> -diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminCreateConfigurableProductBasedOnParentSkuTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminCreateConfigurableProductBasedOnParentSkuTest.xml -new file mode 100644 -index 00000000000..f4f607e9119 ---- /dev/null -+++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminCreateConfigurableProductBasedOnParentSkuTest.xml -@@ -0,0 +1,82 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> -+ <test name="AdminCreateConfigurableProductBasedOnParentSkuTest"> -+ <annotations> -+ <features value="ConfigurableProduct"/> -+ <stories value="Create configurable product"/> -+ <title value="Configurable product variation's sku should be based on parent SKU"/> -+ <description value="Admin should be able to create configurable product with two new options based on parent SKU, without assigned to category and attribute set"/> -+ <testCaseId value="MC-13689"/> -+ <severity value="CRITICAL"/> -+ <group value="mtf_migrated"/> -+ </annotations> -+ <before> -+ <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> -+ </before> -+ <after> -+ <!-- Delete configurable product with children products --> -+ <actionGroup ref="deleteProductBySku" stepKey="deleteProducts"> -+ <argument name="sku" value="{{ApiConfigurableProduct.sku}}"/> -+ </actionGroup> -+ -+ <!-- Delete product attribute --> -+ <actionGroup ref="deleteProductAttributeByLabel" stepKey="deleteProductAttribute"> -+ <argument name="ProductAttribute" value="colorProductAttribute"/> -+ </actionGroup> -+ -+ <!-- Log out --> -+ <actionGroup ref="logout" stepKey="logout"/> -+ </after> -+ -+ <!-- Create configurable product --> -+ <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="amOnProductGridPage"/> -+ <waitForPageLoad stepKey="waitForProductGridPageLoad"/> -+ <actionGroup ref="goToCreateProductPage" stepKey="createConfigurableProduct"> -+ <argument name="product" value="ApiConfigurableProduct"/> -+ </actionGroup> -+ -+ <!-- Fill configurable product values --> -+ <actionGroup ref="fillMainProductForm" stepKey="fillConfigurableProductValues"> -+ <argument name="product" value="ApiConfigurableProduct"/> -+ </actionGroup> -+ -+ <!--Create product configurations--> -+ <click selector="{{AdminProductFormConfigurationsSection.createConfigurations}}" stepKey="clickCreateConfigurations" after="fillConfigurableProductValues"/> -+ <waitForElementVisible selector="{{AdminCreateProductConfigurationsPanel.createNewAttribute}}" time="30" stepKey="waitForConfigurationModalOpen" after="clickCreateConfigurations"/> -+ -+ <!--Create new attribute with two option --> -+ <actionGroup ref="addNewProductConfigurationAttribute" stepKey="createProductConfigurationAttribute"> -+ <argument name="attribute" value="colorProductAttribute"/> -+ <argument name="firstOption" value="colorConfigurableProductAttribute1"/> -+ <argument name="secondOption" value="colorConfigurableProductAttribute2"/> -+ </actionGroup> -+ -+ <!-- Change product configurations except sku --> -+ <actionGroup ref="changeProductConfigurationsInGridExceptSku" stepKey="changeProductConfigurationsInGridExceptSku"> -+ <argument name="firstOption" value="colorConfigurableProductAttribute1"/> -+ <argument name="secondOption" value="colorConfigurableProductAttribute2"/> -+ </actionGroup> -+ -+ <!-- Save product --> -+ <actionGroup ref="saveConfigurableProductAddToCurrentAttributeSet" stepKey="saveProduct"/> -+ -+ <!-- Assert child products generated sku in grid --> -+ <amOnPage url="{{ProductCatalogPage.url}}" stepKey="openProductCatalogPage"/> -+ <waitForPageLoad stepKey="waitForProductCatalogPageLoad"/> -+ <actionGroup ref="filterProductGridByName2" stepKey="filterFirstProductByNameInGrid"> -+ <argument name="name" value="{{colorConfigurableProductAttribute1.name}}"/> -+ </actionGroup> -+ <see selector="{{AdminProductGridSection.productGridCell('1', 'SKU')}}" userInput="{{ApiConfigurableProduct.sku}}-{{colorConfigurableProductAttribute1.name}}" stepKey="seeFirstProductSkuInGrid"/> -+ <actionGroup ref="filterProductGridByName2" stepKey="filterSecondProductByNameInGrid"> -+ <argument name="name" value="{{colorConfigurableProductAttribute2.name}}"/> -+ </actionGroup> -+ <see selector="{{AdminProductGridSection.productGridCell('1', 'SKU')}}" userInput="{{ApiConfigurableProduct.sku}}-{{colorConfigurableProductAttribute2.name}}" stepKey="seeSecondProductSkuInGrid"/> -+ </test> -+</tests> -\ No newline at end of file -diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminCreateConfigurableProductWithCreatingCategoryAndAttributeTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminCreateConfigurableProductWithCreatingCategoryAndAttributeTest.xml -new file mode 100644 -index 00000000000..a7242b43c2b ---- /dev/null -+++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminCreateConfigurableProductWithCreatingCategoryAndAttributeTest.xml -@@ -0,0 +1,124 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> -+ <test name="AdminCreateConfigurableProductWithCreatingCategoryAndAttributeTest"> -+ <annotations> -+ <features value="ConfigurableProduct"/> -+ <stories value="Create configurable product"/> -+ <title value="Create configurable product with creating new category and new attribute (required fields only)"/> -+ <description value="Admin should be able to create configurable product with creating new category and new attribute (required fields only)"/> -+ <testCaseId value="MC-13687"/> -+ <severity value="CRITICAL"/> -+ <group value="mtf_migrated"/> -+ </annotations> -+ <before> -+ <createData entity="SimpleSubCategory" stepKey="createCategory"/> -+ <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> -+ </before> -+ <after> -+ <!-- Delete configurable product --> -+ <actionGroup ref="deleteProductUsingProductGrid" stepKey="deleteProduct"> -+ <argument name="product" value="ApiConfigurableProduct"/> -+ </actionGroup> -+ -+ <!-- Delete children products --> -+ <actionGroup ref="deleteProductBySku" stepKey="deleteFirstChildProduct"> -+ <argument name="sku" value="{{colorConfigurableProductAttribute1.sku}}"/> -+ </actionGroup> -+ <actionGroup ref="deleteProductBySku" stepKey="deleteSecondChildProduct"> -+ <argument name="sku" value="{{colorConfigurableProductAttribute2.sku}}"/> -+ </actionGroup> -+ -+ <!-- Delete product attribute --> -+ <actionGroup ref="deleteProductAttributeByLabel" stepKey="deleteProductAttribute"> -+ <argument name="ProductAttribute" value="colorProductAttribute"/> -+ </actionGroup> -+ -+ <!-- Delete attribute set --> -+ <actionGroup ref="deleteAttributeSetByLabel" stepKey="deleteAttributeSet"> -+ <argument name="label" value="{{ProductAttributeFrontendLabel.label}}"/> -+ </actionGroup> -+ -+ <!-- Delete category --> -+ <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> -+ -+ <!-- Log out --> -+ <actionGroup ref="logout" stepKey="logout"/> -+ </after> -+ -+ <!-- Create configurable product --> -+ <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="amOnProductGridPage"/> -+ <waitForPageLoad stepKey="waitForProductGridPageLoad"/> -+ <actionGroup ref="goToCreateProductPage" stepKey="createConfigurableProduct"> -+ <argument name="product" value="ApiConfigurableProduct"/> -+ </actionGroup> -+ -+ <!-- Fill configurable product required fields only--> -+ <fillField selector="{{AdminProductFormSection.productName}}" userInput="{{ApiConfigurableProduct.name}}" stepKey="fillProductName"/> -+ <fillField selector="{{AdminProductFormSection.productSku}}" userInput="{{ApiConfigurableProduct.sku}}" stepKey="fillProductSku"/> -+ <fillField selector="{{AdminProductFormSection.productPrice}}" userInput="{{ApiConfigurableProduct.price}}" stepKey="fillProductPrice"/> -+ -+ <!-- Add configurable product in category --> -+ <searchAndMultiSelectOption selector="{{AdminProductFormSection.categoriesDropdown}}" parameterArray="[$$createCategory.name$$]" stepKey="fillCategory"/> -+ -+ <!--Create product configurations--> -+ <click selector="{{AdminProductFormConfigurationsSection.createConfigurations}}" stepKey="clickCreateConfigurations"/> -+ <waitForElementVisible selector="{{AdminCreateProductConfigurationsPanel.createNewAttribute}}" time="30" stepKey="waitForConfigurationModalOpen" after="clickCreateConfigurations"/> -+ -+ <!--Create new attribute with two option --> -+ <actionGroup ref="addNewProductConfigurationAttribute" stepKey="createProductConfigurationAttribute"> -+ <argument name="attribute" value="colorProductAttribute"/> -+ <argument name="firstOption" value="colorConfigurableProductAttribute1"/> -+ <argument name="secondOption" value="colorConfigurableProductAttribute2"/> -+ </actionGroup> -+ -+ <!-- Change product configurations in grid --> -+ <actionGroup ref="changeProductConfigurationsInGrid" stepKey="changeProductConfigurationsInGrid"> -+ <argument name="firstOption" value="colorConfigurableProductAttribute1"/> -+ <argument name="secondOption" value="colorConfigurableProductAttribute2"/> -+ </actionGroup> -+ -+ <!-- Save configurable product; add product to new attribute set --> -+ <actionGroup ref="saveConfigurableProductWithNewAttributeSet" stepKey="saveConfigurableProduct"/> -+ -+ <!-- Find configurable product in grid --> -+ <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="visitAdminProductPage"/> -+ <waitForPageLoad stepKey="waitForAdminProductPageLoad"/> -+ <actionGroup ref="filterProductGridBySku" stepKey="findCreatedConfigurableProduct"> -+ <argument name="product" value="ApiConfigurableProduct"/> -+ </actionGroup> -+ -+ <!-- Assert configurable product on admin product page --> -+ <click selector="{{AdminProductGridSection.firstRow}}" stepKey="clickOnProductPage"/> -+ <waitForPageLoad stepKey="waitForProductPageLoad"/> -+ <actionGroup ref="assertConfigurableProductOnAdminProductPage" stepKey="assertConfigurableProductOnAdminProductPage"> -+ <argument name="product" value="ApiConfigurableProduct"/> -+ </actionGroup> -+ -+ <!-- Flash cache --> -+ <magentoCLI command="cache:flush" stepKey="flushCache"/> -+ -+ <!--Assert configurable product in category --> -+ <amOnPage url="$$createCategory.name$$.html" stepKey="amOnCategoryPage"/> -+ <waitForPageLoad stepKey="waitForCategoryPageLoad"/> -+ <actionGroup ref="StorefrontCheckCategoryConfigurableProduct" stepKey="assertConfigurableProductInCategory"> -+ <argument name="product" value="ApiConfigurableProduct"/> -+ <argument name="optionProduct" value="colorConfigurableProductAttribute1"/> -+ </actionGroup> -+ -+ <!--Assert configurable product on product page --> -+ <amOnPage url="{{ApiConfigurableProduct.urlKey}}.html" stepKey="amOnProductPage"/> -+ <waitForPageLoad stepKey="waitForPageLoad"/> -+ <actionGroup ref="storefrontCheckConfigurableProductOptions" stepKey="checkConfigurableProductOptions"> -+ <argument name="product" value="ApiConfigurableProduct"/> -+ <argument name="firstOption" value="colorConfigurableProductAttribute1"/> -+ <argument name="secondOption" value="colorConfigurableProductAttribute2"/> -+ </actionGroup> -+ </test> -+</tests> -diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminCreateConfigurableProductWithDisabledChildrenProductsTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminCreateConfigurableProductWithDisabledChildrenProductsTest.xml -new file mode 100644 -index 00000000000..49f3f8b5ea9 ---- /dev/null -+++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminCreateConfigurableProductWithDisabledChildrenProductsTest.xml -@@ -0,0 +1,123 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> -+ <test name="AdminCreateConfigurableProductWithDisabledChildrenProductsTest"> -+ <annotations> -+ <features value="ConfigurableProduct"/> -+ <stories value="Create configurable product"/> -+ <title value="Create configurable product with disabled children products"/> -+ <description value="Admin should be able to create configurable product with disabled children products, assigned to category"/> -+ <testCaseId value="MC-13711"/> -+ <severity value="CRITICAL"/> -+ <group value="mtf_migrated"/> -+ </annotations> -+ <before> -+ <!-- Create attribute with one options --> -+ <createData entity="productAttributeWithTwoOptionsNotVisible" stepKey="createConfigProductAttribute"/> -+ <createData entity="productAttributeOption1" stepKey="createConfigProductAttributeOption"> -+ <requiredEntity createDataKey="createConfigProductAttribute"/> -+ </createData> -+ <createData entity="AddToDefaultSet" stepKey="createConfigAddToAttributeSet"> -+ <requiredEntity createDataKey="createConfigProductAttribute"/> -+ </createData> -+ <getData entity="ProductAttributeOptionGetter" index="1" stepKey="getConfigAttributeOption"> -+ <requiredEntity createDataKey="createConfigProductAttribute"/> -+ </getData> -+ -+ <!-- Create the child that will be a part of the configurable product --> -+ <createData entity="SimpleProductOffline" stepKey="createSimpleProduct"> -+ <requiredEntity createDataKey="createConfigProductAttribute"/> -+ <requiredEntity createDataKey="getConfigAttributeOption"/> -+ </createData> -+ <createData entity="SimpleSubCategory" stepKey="createCategory"/> -+ <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> -+ </before> -+ <after> -+ <!-- Don't display out of stock product --> -+ <actionGroup ref="noDisplayOutOfStockProduct" stepKey="revertDisplayOutOfStockProduct"/> -+ -+ <!-- Delete configurable product --> -+ <actionGroup ref="deleteProductUsingProductGrid" stepKey="deleteProduct"> -+ <argument name="product" value="ApiConfigurableProduct"/> -+ </actionGroup> -+ -+ <!-- Delete created data --> -+ <deleteData createDataKey="createSimpleProduct" stepKey="deleteSimpleProduct"/> -+ <deleteData createDataKey="createConfigProductAttribute" stepKey="deleteAttribute"/> -+ <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> -+ -+ <!-- Log out --> -+ <actionGroup ref="logout" stepKey="logout"/> -+ </after> -+ -+ <!-- Create configurable product --> -+ <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="amOnProductGridPage"/> -+ <waitForPageLoad stepKey="waitForProductGridPageLoad"/> -+ <actionGroup ref="goToCreateProductPage" stepKey="createConfigurableProduct"> -+ <argument name="product" value="ApiConfigurableProduct"/> -+ </actionGroup> -+ -+ <!-- Fill configurable product values --> -+ <actionGroup ref="fillMainProductForm" stepKey="fillConfigurableProductValues"> -+ <argument name="product" value="ApiConfigurableProduct"/> -+ </actionGroup> -+ -+ <!-- Create product configurations and add attribute and select all options --> -+ <actionGroup ref="generateConfigurationsByAttributeCode" stepKey="generateConfigurationsByAttributeCode" after="fillConfigurableProductValues"> -+ <argument name="attributeCode" value="$$createConfigProductAttribute.attribute_code$$"/> -+ </actionGroup> -+ -+ <!-- Add configurable product to category --> -+ <searchAndMultiSelectOption selector="{{AdminProductFormSection.categoriesDropdown}}" parameterArray="[$$createCategory.name$$]" stepKey="fillCategory" after="fillConfigurableProductValues"/> -+ -+ <!-- Add child product to configurations grid --> -+ <actionGroup ref="addProductToConfigurationsGrid" stepKey="addSimpleProduct"> -+ <argument name="sku" value="$$createSimpleProduct.sku$$"/> -+ <argument name="name" value="$$createConfigProductAttributeOption.option[store_labels][1][label]$$"/> -+ </actionGroup> -+ -+ <!-- Save configurable product --> -+ <actionGroup ref="saveProductForm" stepKey="saveConfigurableProduct"/> -+ -+ <!-- Find configurable product in grid --> -+ <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="visitAdminProductPage"/> -+ <waitForPageLoad stepKey="waitForAdminProductPageLoad"/> -+ <actionGroup ref="filterProductGridBySku" stepKey="findCreatedConfigurableProduct"> -+ <argument name="product" value="ApiConfigurableProduct"/> -+ </actionGroup> -+ -+ <!-- Assert configurable product on admin product page --> -+ <click selector="{{AdminProductGridSection.firstRow}}" stepKey="clickOnProductPage"/> -+ <waitForPageLoad stepKey="waitForProductPageLoad"/> -+ <actionGroup ref="assertConfigurableProductOnAdminProductPage" stepKey="assertConfigurableProductOnAdminProductPage"> -+ <argument name="product" value="ApiConfigurableProduct"/> -+ </actionGroup> -+ -+ <!--Assert configurable attributes block is present on product page --> -+ <scrollTo selector="{{AdminProductFormConfigurationsSection.sectionHeader}}" stepKey="scrollToSearchEngineTab" /> -+ <seeElement selector="{{AdminProductFormConfigurationsSection.createdConfigurationsBlock}}" stepKey="seeCreatedConfigurations"/> -+ <see userInput="$$createSimpleProduct.name$$" selector="{{AdminProductFormConfigurationsSection.currentVariationsNameCells}}" stepKey="seeProductNameInConfigurations"/> -+ -+ <!-- Display out of stock product --> -+ <actionGroup ref="displayOutOfStockProduct" stepKey="displayOutOfStockProduct"/> -+ -+ <!-- Flash cache --> -+ <magentoCLI command="cache:flush" stepKey="flushCache"/> -+ -+ <!-- Assert configurable product is not present in category --> -+ <amOnPage url="$$createCategory.name$$.html" stepKey="amOnCategoryPage"/> -+ <waitForPageLoad stepKey="waitForCategoryPageLoad"/> -+ <see selector="{{StorefrontCategoryMainSection.emptyProductMessage}}" userInput="We can't find products matching the selection." stepKey="seeEmptyProductMessage"/> -+ -+ <!-- Assert configurable product is out of stock--> -+ <amOnPage url="{{ApiConfigurableProduct.urlKey}}.html" stepKey="amOnProductPage"/> -+ <waitForPageLoad stepKey="waitForPageLoad"/> -+ <see stepKey="checkForOutOfStock" selector="{{StorefrontProductInfoMainSection.stockIndication}}" userInput="OUT OF STOCK"/> -+ </test> -+</tests> -diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminCreateConfigurableProductWithImagesTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminCreateConfigurableProductWithImagesTest.xml -new file mode 100644 -index 00000000000..925e7a890ce ---- /dev/null -+++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminCreateConfigurableProductWithImagesTest.xml -@@ -0,0 +1,159 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> -+ <test name="AdminCreateConfigurableProductWithImagesTest"> -+ <annotations> -+ <features value="ConfigurableProduct"/> -+ <stories value="Create configurable product"/> -+ <title value="Create configurable product with images"/> -+ <description value="Admin should be able to create configurable product with images"/> -+ <testCaseId value="MC-13713"/> -+ <severity value="CRITICAL"/> -+ <group value="mtf_migrated"/> -+ </annotations> -+ <before> -+ <!-- Create first attribute with 2 options --> -+ <createData entity="productAttributeWithTwoOptionsNotVisible" stepKey="createFirstConfigProductAttribute"/> -+ <createData entity="productAttributeOption1" stepKey="createConfigProductAttributeOptionOne"> -+ <requiredEntity createDataKey="createFirstConfigProductAttribute"/> -+ </createData> -+ <createData entity="productAttributeOption2" stepKey="createConfigProductAttributeOptionTwo"> -+ <requiredEntity createDataKey="createFirstConfigProductAttribute"/> -+ </createData> -+ -+ <!-- Create second attribute with 2 options --> -+ <createData entity="productAttributeWithTwoOptions" stepKey="createSecondConfigProductAttribute"/> -+ <createData entity="productAttributeOption3" stepKey="createConfigProductAttributeOptionThree"> -+ <requiredEntity createDataKey="createSecondConfigProductAttribute"/> -+ </createData> -+ <createData entity="productAttributeOption4" stepKey="createConfigProductAttributeOptionFour"> -+ <requiredEntity createDataKey="createSecondConfigProductAttribute"/> -+ </createData> -+ -+ <createData entity="AddToDefaultSet" stepKey="createConfigAddToAttributeSet"> -+ <requiredEntity createDataKey="createFirstConfigProductAttribute"/> -+ <requiredEntity createDataKey="createSecondConfigProductAttribute"/> -+ </createData> -+ -+ <createData entity="SimpleSubCategory" stepKey="createCategory"/> -+ <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> -+ </before> -+ <after> -+ <!-- Delete configurable product --> -+ <actionGroup ref="deleteProductUsingProductGrid" stepKey="deleteProduct"> -+ <argument name="product" value="ApiConfigurableProduct"/> -+ </actionGroup> -+ -+ <!-- Delete created data --> -+ <deleteData createDataKey="createFirstConfigProductAttribute" stepKey="deleteFirstConfigProductAttribute"/> -+ <deleteData createDataKey="createSecondConfigProductAttribute" stepKey="deleteSecondConfigProductAttribute"/> -+ <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> -+ -+ <!-- Log out --> -+ <actionGroup ref="logout" stepKey="logout"/> -+ </after> -+ -+ <!--Create configurable product --> -+ <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="amOnProductGridPage"/> -+ <waitForPageLoad stepKey="waitForProductGridPageLoad"/> -+ <actionGroup ref="goToCreateProductPage" stepKey="createConfigurableProduct"> -+ <argument name="product" value="ApiConfigurableProduct"/> -+ </actionGroup> -+ -+ <!-- Fill configurable product values --> -+ <actionGroup ref="fillMainProductForm" stepKey="fillConfigurableProductValues"> -+ <argument name="product" value="ApiConfigurableProduct"/> -+ </actionGroup> -+ -+ <!-- Add configurable product to category --> -+ <searchAndMultiSelectOption selector="{{AdminProductFormSection.categoriesDropdown}}" parameterArray="[$$createCategory.name$$]" stepKey="fillCategory" after="fillConfigurableProductValues"/> -+ -+ <!-- Add image to product --> -+ <actionGroup ref="addProductImage" stepKey="addImageForProduct"> -+ <argument name="image" value="MagentoLogo"/> -+ </actionGroup> -+ -+ <!-- Create product configurations --> -+ <click selector="{{AdminProductFormConfigurationsSection.createConfigurations}}" stepKey="clickCreateConfigurations" after="addImageForProduct"/> -+ <waitForElementVisible selector="{{AdminCreateProductConfigurationsPanel.createNewAttribute}}" time="30" stepKey="waitForConfigurationModalOpen" after="clickCreateConfigurations"/> -+ -+ <!-- Show 100 attributes per page --> -+ <actionGroup ref="adminDataGridSelectPerPage" stepKey="selectNumberOfAttributesPerPage"> -+ <argument name="perPage" value="100"/> -+ </actionGroup> -+ -+ <!--Add attributes and select all options --> -+ <click selector="{{AdminCreateProductConfigurationsPanel.attributeRowByAttributeCode($$createFirstConfigProductAttribute.attribute_code$$)}}" stepKey="clickOnFirstAttributeCheckbox"/> -+ <click selector="{{AdminCreateProductConfigurationsPanel.attributeRowByAttributeCode($$createSecondConfigProductAttribute.attribute_code$$)}}" stepKey="clickOnSecondAttributeCheckbox"/> -+ <click selector="{{AdminCreateProductConfigurationsPanel.next}}" stepKey="clickOnNextButton1"/> -+ <click selector="{{AdminCreateProductConfigurationsPanel.selectAllByAttribute($$createFirstConfigProductAttribute.default_frontend_label$$)}}" stepKey="clickOnSelectAllInFirstAttribute"/> -+ <click selector="{{AdminCreateProductConfigurationsPanel.selectAllByAttribute($$createSecondConfigProductAttribute.default_frontend_label$$)}}" stepKey="clickOnSelectAllInSecondAttribute"/> -+ <click selector="{{AdminCreateProductConfigurationsPanel.next}}" stepKey="clickOnNextButton2"/> -+ -+ <!-- Add images to first product attribute options --> -+ <actionGroup ref="addUniqueImageToConfigurableProductOption" stepKey="addImageToConfigurableProductOptionOne"> -+ <argument name="image" value="MagentoLogo"/> -+ <argument name="frontend_label" value="$$createFirstConfigProductAttribute.default_frontend_label$$"/> -+ <argument name="label" value="$$createConfigProductAttributeOptionOne.option[store_labels][1][label]$$"/> -+ </actionGroup> -+ <actionGroup ref="addUniqueImageToConfigurableProductOption" stepKey="addImageToConfigurableProductOptionTwo"> -+ <argument name="image" value="TestImageNew"/> -+ <argument name="frontend_label" value="$$createFirstConfigProductAttribute.default_frontend_label$$"/> -+ <argument name="label" value="$$createConfigProductAttributeOptionTwo.option[store_labels][1][label]$$"/> -+ </actionGroup> -+ -+ <!-- Add price to second product attribute options --> -+ <actionGroup ref="addUniquePriceToConfigurableProductOption" stepKey="addPriceToConfigurableProductOptionThree"> -+ <argument name="frontend_label" value="$$createSecondConfigProductAttribute.default_frontend_label$$"/> -+ <argument name="label" value="$$createConfigProductAttributeOptionThree.option[store_labels][1][label]$$"/> -+ <argument name="price" value="{{virtualProductWithRequiredFields.price}}"/> -+ </actionGroup> -+ <actionGroup ref="addUniquePriceToConfigurableProductOption" stepKey="addPriceToConfigurableProductOptionFour"> -+ <argument name="frontend_label" value="$$createSecondConfigProductAttribute.default_frontend_label$$"/> -+ <argument name="label" value="$$createConfigProductAttributeOptionFour.option[store_labels][1][label]$$"/> -+ <argument name="price" value="{{virtualProductWithRequiredFields.price}}"/> -+ </actionGroup> -+ -+ <!-- Add quantity to product attribute options --> -+ <click selector="{{AdminCreateProductConfigurationsPanel.applySingleQuantityToEachSkus}}" stepKey="clickOnApplySingleQuantityToEachSku"/> -+ <fillField selector="{{AdminCreateProductConfigurationsPanel.quantity}}" userInput="100" stepKey="enterAttributeQuantity"/> -+ <click selector="{{AdminCreateProductConfigurationsPanel.next}}" stepKey="clickOnNextButton3"/> -+ <click selector="{{AdminCreateProductConfigurationsPanel.next}}" stepKey="clickOnNextButton4"/> -+ -+ <!-- Save product --> -+ <actionGroup ref="saveConfigurableProductAddToCurrentAttributeSet" stepKey="saveProduct"/> -+ -+ <!-- Assert configurable product in category --> -+ <amOnPage url="$$createCategory.name$$.html" stepKey="amOnCategoryPage"/> -+ <waitForPageLoad stepKey="waitForCategoryPageLoad"/> -+ <actionGroup ref="StorefrontCheckCategoryConfigurableProduct" stepKey="assertConfigurableProductInCategory"> -+ <argument name="product" value="ApiConfigurableProduct"/> -+ <argument name="optionProduct" value="virtualProductWithRequiredFields"/> -+ </actionGroup> -+ -+ <!-- Assert product image in storefront product page --> -+ <amOnPage url="{{ApiConfigurableProduct.urlKey}}.html" stepKey="amOnProductPage"/> -+ <actionGroup ref="assertProductImageStorefrontProductPage" stepKey="assertProductImageStorefrontProductPage"> -+ <argument name="product" value="ApiConfigurableProduct"/> -+ <argument name="image" value="MagentoLogo"/> -+ </actionGroup> -+ -+ <!-- Assert product options images in storefront product page --> -+ <actionGroup ref="assertOptionImageInStorefrontProductPage" stepKey="assertFirstOptionImageInStorefrontProductPage"> -+ <argument name="product" value="ApiConfigurableProduct"/> -+ <argument name="label" value="$$createConfigProductAttributeOptionOne.option[store_labels][1][label]$$"/> -+ <argument name="image" value="MagentoLogo"/> -+ </actionGroup> -+ <actionGroup ref="assertOptionImageInStorefrontProductPage" stepKey="assertSecondOptionImageInStorefrontProductPage"> -+ <argument name="product" value="ApiConfigurableProduct"/> -+ <argument name="label" value="$$createConfigProductAttributeOptionTwo.option[store_labels][1][label]$$"/> -+ <argument name="image" value="TestImageNew"/> -+ </actionGroup> -+ </test> -+</tests> -diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminCreateConfigurableProductWithThreeProductDisplayOutOfStockProductsTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminCreateConfigurableProductWithThreeProductDisplayOutOfStockProductsTest.xml -new file mode 100644 -index 00000000000..0b83fdc1788 ---- /dev/null -+++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminCreateConfigurableProductWithThreeProductDisplayOutOfStockProductsTest.xml -@@ -0,0 +1,140 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> -+ <test name="AdminCreateConfigurableProductWithThreeProductDisplayOutOfStockProductsTest"> -+ <annotations> -+ <features value="ConfigurableProduct"/> -+ <stories value="Create configurable product"/> -+ <title value="Create Configurable Product with three product, display out of stock products"/> -+ <description value="Admin should be able to create Configurable Product with one out of stock and several in stock options, display out of stock products"/> -+ <testCaseId value="MC-13714"/> -+ <severity value="CRITICAL"/> -+ <group value="mtf_migrated"/> -+ </annotations> -+ <before> -+ <!-- Create attribute with 3 options to be used in children products --> -+ <createData entity="productAttributeWithTwoOptionsNotVisible" stepKey="createConfigProductAttribute"/> -+ <createData entity="productAttributeOption1" stepKey="createConfigProductAttributeOptionOne"> -+ <requiredEntity createDataKey="createConfigProductAttribute"/> -+ </createData> -+ <createData entity="productAttributeOption2" stepKey="createConfigProductAttributeOptionTwo"> -+ <requiredEntity createDataKey="createConfigProductAttribute"/> -+ </createData> -+ <createData entity="productAttributeOption3" stepKey="createConfigProductAttributeOptionThree"> -+ <requiredEntity createDataKey="createConfigProductAttribute"/> -+ </createData> -+ <createData entity="AddToDefaultSet" stepKey="createConfigAddToAttributeSet"> -+ <requiredEntity createDataKey="createConfigProductAttribute"/> -+ </createData> -+ <getData entity="ProductAttributeOptionGetter" index="1" stepKey="getConfigAttributeOptionOne"> -+ <requiredEntity createDataKey="createConfigProductAttribute"/> -+ </getData> -+ <getData entity="ProductAttributeOptionGetter" index="2" stepKey="getConfigAttributeOptionTwo"> -+ <requiredEntity createDataKey="createConfigProductAttribute"/> -+ </getData> -+ <getData entity="ProductAttributeOptionGetter" index="3" stepKey="getConfigAttributeOptionThree"> -+ <requiredEntity createDataKey="createConfigProductAttribute"/> -+ </getData> -+ -+ <!-- Create the 3 children that will be a part of the configurable product --> -+ <createData entity="ApiSimpleOne" stepKey="createFirstSimpleProduct"> -+ <requiredEntity createDataKey="createConfigProductAttribute"/> -+ <requiredEntity createDataKey="getConfigAttributeOptionOne"/> -+ </createData> -+ <createData entity="ApiSimpleTwo" stepKey="createSecondSimpleProduct"> -+ <requiredEntity createDataKey="createConfigProductAttribute"/> -+ <requiredEntity createDataKey="getConfigAttributeOptionTwo"/> -+ </createData> -+ <createData entity="ApiSimpleOutOfStock" stepKey="createSimpleOutOfStockProduct"> -+ <requiredEntity createDataKey="createConfigProductAttribute"/> -+ <requiredEntity createDataKey="getConfigAttributeOptionThree"/> -+ </createData> -+ -+ <createData entity="SimpleSubCategory" stepKey="createCategory"/> -+ <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> -+ </before> -+ <after> -+ <!-- Don't display out of stock product --> -+ <actionGroup ref="noDisplayOutOfStockProduct" stepKey="revertDisplayOutOfStockProduct"/> -+ -+ <!-- Delete configurable product --> -+ <actionGroup ref="deleteProductUsingProductGrid" stepKey="deleteProduct"> -+ <argument name="product" value="ApiConfigurableProduct"/> -+ </actionGroup> -+ -+ <!-- Delete created data --> -+ <deleteData createDataKey="createFirstSimpleProduct" stepKey="deleteFirstSimpleProduct"/> -+ <deleteData createDataKey="createSecondSimpleProduct" stepKey="deleteSecondSimpleProduct"/> -+ <deleteData createDataKey="createSimpleOutOfStockProduct" stepKey="deleteSimpleOutOfStockProduct"/> -+ <deleteData createDataKey="createConfigProductAttribute" stepKey="deleteConfigProductAttribute"/> -+ <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> -+ -+ <!-- Log out --> -+ <actionGroup ref="logout" stepKey="logout"/> -+ </after> -+ -+ <!--Create configurable product --> -+ <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="amOnProductGridPage"/> -+ <waitForPageLoad stepKey="waitForProductGridPageLoad"/> -+ <actionGroup ref="goToCreateProductPage" stepKey="createConfigurableProduct"> -+ <argument name="product" value="ApiConfigurableProduct"/> -+ </actionGroup> -+ -+ <!--Fill configurable product values --> -+ <actionGroup ref="fillMainProductForm" stepKey="fillConfigurableProductValues"> -+ <argument name="product" value="ApiConfigurableProduct"/> -+ </actionGroup> -+ -+ <!-- Create product configurations and add attribute and select all options --> -+ <actionGroup ref="generateConfigurationsByAttributeCode" stepKey="generateConfigurationsByAttributeCode" after="fillConfigurableProductValues"> -+ <argument name="attributeCode" value="$$createConfigProductAttribute.attribute_code$$"/> -+ </actionGroup> -+ -+ <!-- Add configurable product to category --> -+ <searchAndMultiSelectOption selector="{{AdminProductFormSection.categoriesDropdown}}" parameterArray="[$$createCategory.name$$]" stepKey="fillCategory" after="fillConfigurableProductValues"/> -+ -+ <!-- Add child products to configurations grid --> -+ <actionGroup ref="addProductToConfigurationsGrid" stepKey="addFirstSimpleProduct"> -+ <argument name="sku" value="$$createFirstSimpleProduct.sku$$"/> -+ <argument name="name" value="$$createConfigProductAttributeOptionOne.option[store_labels][1][label]$$"/> -+ </actionGroup> -+ -+ <actionGroup ref="addProductToConfigurationsGrid" stepKey="addSecondSimpleProduct"> -+ <argument name="sku" value="$$createSecondSimpleProduct.sku$$"/> -+ <argument name="name" value="$$createConfigProductAttributeOptionTwo.option[store_labels][1][label]$$"/> -+ </actionGroup> -+ -+ <actionGroup ref="addProductToConfigurationsGrid" stepKey="addOutOfStockProduct"> -+ <argument name="sku" value="$$createSimpleOutOfStockProduct.sku$$"/> -+ <argument name="name" value="$$createConfigProductAttributeOptionThree.option[store_labels][1][label]$$"/> -+ </actionGroup> -+ -+ <!-- Save configurable product --> -+ <actionGroup ref="saveProductForm" stepKey="saveConfigurableProduct"/> -+ -+ <!-- Display out of stock product --> -+ <actionGroup ref="displayOutOfStockProduct" stepKey="displayOutOfStockProduct"/> -+ -+ <!-- Flash cache --> -+ <magentoCLI command="cache:flush" stepKey="flushCache"/> -+ -+ <!--Assert configurable product in category --> -+ <amOnPage url="$$createCategory.name$$.html" stepKey="amOnCategoryPage"/> -+ <waitForPageLoad stepKey="waitForCategoryPageLoad"/> -+ <actionGroup ref="StorefrontCheckCategoryConfigurableProduct" stepKey="assertConfigurableProductInCategory"> -+ <argument name="product" value="ApiConfigurableProduct"/> -+ <argument name="optionProduct" value="$$createFirstSimpleProduct$$"/> -+ </actionGroup> -+ -+ <!-- Assert out of stock option is absent on product page --> -+ <amOnPage url="{{ApiConfigurableProduct.urlKey}}.html" stepKey="amOnProductPage"/> -+ <waitForPageLoad stepKey="waitForProductPageLoad"/> -+ <dontSee userInput="$$createConfigProductAttributeOptionThree.option[store_labels][1][label]$$" selector="{{StorefrontProductInfoMainSection.optionByAttributeId($$createConfigProductAttribute.attribute_id$$)}}" stepKey="assertOptionNotAvailable" /> -+ </test> -+</tests> -\ No newline at end of file -diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminCreateConfigurableProductWithThreeProductDontDisplayOutOfStockProductsTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminCreateConfigurableProductWithThreeProductDontDisplayOutOfStockProductsTest.xml -new file mode 100644 -index 00000000000..e24ac07c30d ---- /dev/null -+++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminCreateConfigurableProductWithThreeProductDontDisplayOutOfStockProductsTest.xml -@@ -0,0 +1,134 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> -+ <test name="AdminCreateConfigurableProductWithThreeProductDontDisplayOutOfStockProductsTest"> -+ <annotations> -+ <features value="ConfigurableProduct"/> -+ <stories value="Create configurable product"/> -+ <title value="Create Configurable Product with three product, don't display out of stock products"/> -+ <description value="Admin should be able to create Configurable Product with one out of stock and several in stock options, don't display out of stock products"/> -+ <testCaseId value="MC-13715"/> -+ <severity value="CRITICAL"/> -+ <group value="mtf_migrated"/> -+ </annotations> -+ <before> -+ <!-- Create attribute with 3 options to be used in children products --> -+ <createData entity="productAttributeWithTwoOptionsNotVisible" stepKey="createConfigProductAttribute"/> -+ <createData entity="productAttributeOption1" stepKey="createConfigProductAttributeOptionOne"> -+ <requiredEntity createDataKey="createConfigProductAttribute"/> -+ </createData> -+ <createData entity="productAttributeOption2" stepKey="createConfigProductAttributeOptionTwo"> -+ <requiredEntity createDataKey="createConfigProductAttribute"/> -+ </createData> -+ <createData entity="productAttributeOption3" stepKey="createConfigProductAttributeOptionThree"> -+ <requiredEntity createDataKey="createConfigProductAttribute"/> -+ </createData> -+ <createData entity="AddToDefaultSet" stepKey="createConfigAddToAttributeSet"> -+ <requiredEntity createDataKey="createConfigProductAttribute"/> -+ </createData> -+ <getData entity="ProductAttributeOptionGetter" index="1" stepKey="getConfigAttributeOptionOne"> -+ <requiredEntity createDataKey="createConfigProductAttribute"/> -+ </getData> -+ <getData entity="ProductAttributeOptionGetter" index="2" stepKey="getConfigAttributeOptionTwo"> -+ <requiredEntity createDataKey="createConfigProductAttribute"/> -+ </getData> -+ <getData entity="ProductAttributeOptionGetter" index="3" stepKey="getConfigAttributeOptionThree"> -+ <requiredEntity createDataKey="createConfigProductAttribute"/> -+ </getData> -+ -+ <!-- Create the 3 children that will be a part of the configurable product --> -+ <createData entity="ApiSimpleOne" stepKey="createFirstSimpleProduct"> -+ <requiredEntity createDataKey="createConfigProductAttribute"/> -+ <requiredEntity createDataKey="getConfigAttributeOptionOne"/> -+ </createData> -+ <createData entity="ApiSimpleTwo" stepKey="createSecondSimpleProduct"> -+ <requiredEntity createDataKey="createConfigProductAttribute"/> -+ <requiredEntity createDataKey="getConfigAttributeOptionTwo"/> -+ </createData> -+ <createData entity="ApiSimpleOutOfStock" stepKey="createSimpleOutOfStockProduct"> -+ <requiredEntity createDataKey="createConfigProductAttribute"/> -+ <requiredEntity createDataKey="getConfigAttributeOptionThree"/> -+ </createData> -+ -+ <createData entity="SimpleSubCategory" stepKey="createCategory"/> -+ <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> -+ </before> -+ <after> -+ <!-- Delete configurable product --> -+ <actionGroup ref="deleteProductUsingProductGrid" stepKey="deleteProduct"> -+ <argument name="product" value="ApiConfigurableProduct"/> -+ </actionGroup> -+ -+ <!-- Delete created data --> -+ <deleteData createDataKey="createFirstSimpleProduct" stepKey="deleteFirstSimpleProduct"/> -+ <deleteData createDataKey="createSecondSimpleProduct" stepKey="deleteSecondSimpleProduct"/> -+ <deleteData createDataKey="createSimpleOutOfStockProduct" stepKey="deleteSimpleOutOfStockProduct"/> -+ <deleteData createDataKey="createConfigProductAttribute" stepKey="deleteConfigProductAttribute"/> -+ <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> -+ -+ <!-- Log out --> -+ <actionGroup ref="logout" stepKey="logout"/> -+ </after> -+ -+ <!--Create configurable product --> -+ <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="amOnProductGridPage"/> -+ <waitForPageLoad stepKey="waitForProductGridPageLoad"/> -+ <actionGroup ref="goToCreateProductPage" stepKey="createConfigurableProduct"> -+ <argument name="product" value="ApiConfigurableProduct"/> -+ </actionGroup> -+ -+ <!--Fill configurable product values --> -+ <actionGroup ref="fillMainProductForm" stepKey="fillConfigurableProductValues"> -+ <argument name="product" value="ApiConfigurableProduct"/> -+ </actionGroup> -+ -+ <!-- Create product configurations and add attribute and select all options --> -+ <actionGroup ref="generateConfigurationsByAttributeCode" stepKey="generateConfigurationsByAttributeCode" after="fillConfigurableProductValues"> -+ <argument name="attributeCode" value="$$createConfigProductAttribute.attribute_code$$"/> -+ </actionGroup> -+ -+ <!-- Add configurable product to category --> -+ <searchAndMultiSelectOption selector="{{AdminProductFormSection.categoriesDropdown}}" parameterArray="[$$createCategory.name$$]" stepKey="fillCategory" after="fillConfigurableProductValues"/> -+ -+ <!-- Add child products to configurations grid --> -+ <actionGroup ref="addProductToConfigurationsGrid" stepKey="addFirstSimpleProduct"> -+ <argument name="sku" value="$$createFirstSimpleProduct.sku$$"/> -+ <argument name="name" value="$$createConfigProductAttributeOptionOne.option[store_labels][1][label]$$"/> -+ </actionGroup> -+ -+ <actionGroup ref="addProductToConfigurationsGrid" stepKey="addSecondSimpleProduct"> -+ <argument name="sku" value="$$createSecondSimpleProduct.sku$$"/> -+ <argument name="name" value="$$createConfigProductAttributeOptionTwo.option[store_labels][1][label]$$"/> -+ </actionGroup> -+ -+ <actionGroup ref="addProductToConfigurationsGrid" stepKey="addOutOfStockProduct"> -+ <argument name="sku" value="$$createSimpleOutOfStockProduct.sku$$"/> -+ <argument name="name" value="$createConfigProductAttributeOptionThree.option[store_labels][1][label]$$"/> -+ </actionGroup> -+ -+ <!-- Save configurable product --> -+ <actionGroup ref="saveProductForm" stepKey="saveConfigurableProduct"/> -+ -+ <!-- Flash cache --> -+ <magentoCLI command="cache:flush" stepKey="flushCache"/> -+ -+ <!--Assert configurable product in category --> -+ <amOnPage url="$$createCategory.name$$.html" stepKey="amOnCategoryPage"/> -+ <waitForPageLoad stepKey="waitForCategoryPageLoad"/> -+ <actionGroup ref="StorefrontCheckCategoryConfigurableProduct" stepKey="assertConfigurableProductInCategory"> -+ <argument name="product" value="ApiConfigurableProduct"/> -+ <argument name="optionProduct" value="$$createFirstSimpleProduct$$"/> -+ </actionGroup> -+ -+ <!-- Assert out of stock option is absent on product page --> -+ <amOnPage url="{{ApiConfigurableProduct.urlKey}}.html" stepKey="amOnProductPage"/> -+ <waitForPageLoad stepKey="waitForPageLoad"/> -+ <dontSee userInput="$$createConfigProductAttributeOptionThree.option[store_labels][1][label]$$" selector="{{StorefrontProductInfoMainSection.optionByAttributeId($$createConfigProductAttribute.attribute_id$$)}}" stepKey="assertOptionNotAvailable"/> -+ </test> -+</tests> -\ No newline at end of file -diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminCreateConfigurableProductWithTierPriceForOneItemTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminCreateConfigurableProductWithTierPriceForOneItemTest.xml -new file mode 100644 -index 00000000000..51f4bf02799 ---- /dev/null -+++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminCreateConfigurableProductWithTierPriceForOneItemTest.xml -@@ -0,0 +1,111 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> -+ <test name="AdminCreateConfigurableProductWithTierPriceForOneItemTest"> -+ <annotations> -+ <features value="ConfigurableProduct"/> -+ <stories value="Create configurable product"/> -+ <title value="Create configurable product with tier price for one item"/> -+ <description value="Admin should be able to create configurable product with tier price for one item"/> -+ <testCaseId value="MC-13695"/> -+ <severity value="CRITICAL"/> -+ <group value="mtf_migrated"/> -+ </annotations> -+ <before> -+ <!-- Create attribute with 2 options to be used in children products --> -+ <createData entity="productAttributeWithTwoOptionsNotVisible" stepKey="createConfigProductAttribute"/> -+ <createData entity="productAttributeOption1" stepKey="createConfigProductAttributeOptionOne"> -+ <requiredEntity createDataKey="createConfigProductAttribute"/> -+ </createData> -+ <createData entity="productAttributeOption2" stepKey="createConfigProductAttributeOptionTwo"> -+ <requiredEntity createDataKey="createConfigProductAttribute"/> -+ </createData> -+ <createData entity="AddToDefaultSet" stepKey="createConfigAddToAttributeSet"> -+ <requiredEntity createDataKey="createConfigProductAttribute"/> -+ </createData> -+ <getData entity="ProductAttributeOptionGetter" index="1" stepKey="getConfigAttributeOptionOne"> -+ <requiredEntity createDataKey="createConfigProductAttribute"/> -+ </getData> -+ <getData entity="ProductAttributeOptionGetter" index="2" stepKey="getConfigAttributeOptionTwo"> -+ <requiredEntity createDataKey="createConfigProductAttribute"/> -+ </getData> -+ -+ <!-- Create the 2 children that will be a part of the configurable product --> -+ <createData entity="ApiSimpleOne" stepKey="createFirstSimpleProduct"> -+ <requiredEntity createDataKey="createConfigProductAttribute"/> -+ <requiredEntity createDataKey="getConfigAttributeOptionOne"/> -+ </createData> -+ <createData entity="ApiSimpleTwo" stepKey="createSecondSimpleProduct"> -+ <requiredEntity createDataKey="createConfigProductAttribute"/> -+ <requiredEntity createDataKey="getConfigAttributeOptionTwo"/> -+ </createData> -+ -+ <!--Add tier price in one product --> -+ <createData entity="tierProductPrice" stepKey="addTierPrice"> -+ <requiredEntity createDataKey="createFirstSimpleProduct" /> -+ </createData> -+ <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> -+ </before> -+ <after> -+ <!-- Delete configurable product --> -+ <actionGroup ref="deleteProductUsingProductGrid" stepKey="deleteProduct"> -+ <argument name="product" value="ApiConfigurableProduct"/> -+ </actionGroup> -+ -+ <!-- Delete created data --> -+ <deleteData createDataKey="createFirstSimpleProduct" stepKey="deleteFirstSimpleProduct"/> -+ <deleteData createDataKey="createSecondSimpleProduct" stepKey="deleteSecondSimpleProduct"/> -+ <deleteData createDataKey="createConfigProductAttribute" stepKey="deleteConfigProductAttribute"/> -+ -+ <!-- Log out --> -+ <actionGroup ref="logout" stepKey="logout"/> -+ </after> -+ -+ <!--Create configurable product --> -+ <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="amOnProductGridPage"/> -+ <waitForPageLoad stepKey="waitProductGridPageLoad"/> -+ <actionGroup ref="goToCreateProductPage" stepKey="createConfigurableProduct"> -+ <argument name="product" value="ApiConfigurableProduct"/> -+ </actionGroup> -+ -+ <!--Fill configurable product values --> -+ <actionGroup ref="fillMainProductForm" stepKey="fillConfigurableProductValues"> -+ <argument name="product" value="ApiConfigurableProduct"/> -+ </actionGroup> -+ -+ <!-- Create product configurations and add attribute and select all options --> -+ <actionGroup ref="generateConfigurationsByAttributeCode" stepKey="generateConfigurationsByAttributeCode" after="fillConfigurableProductValues"> -+ <argument name="attributeCode" value="$$createConfigProductAttribute.attribute_code$$"/> -+ </actionGroup> -+ -+ <!-- Add associated products to configurations grid --> -+ <actionGroup ref="addProductToConfigurationsGrid" stepKey="addFirstSimpleProduct"> -+ <argument name="sku" value="$$createFirstSimpleProduct.sku$$"/> -+ <argument name="name" value="$$createConfigProductAttributeOptionOne.option[store_labels][1][label]$$"/> -+ </actionGroup> -+ -+ <actionGroup ref="addProductToConfigurationsGrid" stepKey="addSecondSimpleProduct"> -+ <argument name="sku" value="$$createSecondSimpleProduct.sku$$"/> -+ <argument name="name" value="$$createConfigProductAttributeOptionTwo.option[store_labels][1][label]$$"/> -+ </actionGroup> -+ -+ <!-- Save configurable product --> -+ <actionGroup ref="saveProductForm" stepKey="saveConfigurableProduct"/> -+ -+ <!-- Assert product tier price on product page --> -+ <amOnPage url="{{ApiConfigurableProduct.urlKey}}.html" stepKey="amOnProductPage"/> -+ <waitForPageLoad stepKey="waitForProductPageLoad"/> -+ <selectOption userInput="$$createConfigProductAttributeOptionOne.option[store_labels][1][label]$$" selector="{{StorefrontProductInfoMainSection.productAttributeOptionsSelectButton}}" stepKey="selectOption1"/> -+ <grabTextFrom selector="{{StorefrontProductInfoMainSection.tierPriceText}}" stepKey="tierPriceText"/> -+ <assertEquals stepKey="assertTierPriceTextOnProductPage"> -+ <expectedResult type="string">Buy {{tierProductPrice.quantity}} for ${{tierProductPrice.price}} each and save 27%</expectedResult> -+ <actualResult type="variable">tierPriceText</actualResult> -+ </assertEquals> -+ </test> -+</tests> -diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminCreateConfigurableProductWithTwoOptionsAssignedToCategoryTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminCreateConfigurableProductWithTwoOptionsAssignedToCategoryTest.xml -new file mode 100644 -index 00000000000..1db9b3e5b79 ---- /dev/null -+++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminCreateConfigurableProductWithTwoOptionsAssignedToCategoryTest.xml -@@ -0,0 +1,154 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> -+ <test name="AdminCreateConfigurableProductWithTwoOptionsAssignedToCategoryTest"> -+ <annotations> -+ <features value="ConfigurableProduct"/> -+ <stories value="Create configurable product"/> -+ <title value="Create configurable product with two new options assigned to category with not visible child products"/> -+ <description value="Admin should be able to create configurable product with two new options, assigned to category, child products are not visible individually"/> -+ <testCaseId value="MC-13685"/> -+ <severity value="CRITICAL"/> -+ <group value="mtf_migrated"/> -+ </annotations> -+ <before> -+ <!-- Create category --> -+ <createData entity="SimpleSubCategory" stepKey="createCategory"/> -+ -+ <!-- Login as admin --> -+ <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> -+ </before> -+ <after> -+ <!-- Delete configurable product --> -+ <actionGroup ref="deleteProductUsingProductGrid" stepKey="deleteProduct"> -+ <argument name="product" value="ApiConfigurableProduct"/> -+ </actionGroup> -+ -+ <!-- Delete children products --> -+ <actionGroup ref="deleteProductBySku" stepKey="deleteFirstChildProduct"> -+ <argument name="sku" value="{{colorConfigurableProductAttribute1.sku}}"/> -+ </actionGroup> -+ <actionGroup ref="deleteProductBySku" stepKey="deleteSecondChildProduct"> -+ <argument name="sku" value="{{colorConfigurableProductAttribute2.sku}}"/> -+ </actionGroup> -+ -+ <!-- Delete product attribute --> -+ <actionGroup ref="deleteProductAttributeByLabel" stepKey="deleteProductAttribute"> -+ <argument name="ProductAttribute" value="colorProductAttribute"/> -+ </actionGroup> -+ -+ <!-- Delete attribute set --> -+ <actionGroup ref="deleteAttributeSetByLabel" stepKey="deleteAttributeSet"> -+ <argument name="label" value="{{ProductAttributeFrontendLabel.label}}"/> -+ </actionGroup> -+ -+ <!-- Delete category --> -+ <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> -+ -+ <!-- Log out --> -+ <actionGroup ref="logout" stepKey="logout"/> -+ </after> -+ -+ <!-- Create configurable product --> -+ <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="amOnProductGridPage"/> -+ <waitForPageLoad stepKey="waitForProductGridPageLoad"/> -+ <actionGroup ref="goToCreateProductPage" stepKey="createConfigurableProduct"> -+ <argument name="product" value="ApiConfigurableProduct"/> -+ </actionGroup> -+ -+ <!-- Fill configurable product values --> -+ <actionGroup ref="fillMainProductForm" stepKey="fillConfigurableProductValues"> -+ <argument name="product" value="ApiConfigurableProduct"/> -+ </actionGroup> -+ -+ <!-- Create product configurations --> -+ <click selector="{{AdminProductFormConfigurationsSection.createConfigurations}}" stepKey="clickCreateConfigurations" after="fillConfigurableProductValues"/> -+ <waitForElementVisible selector="{{AdminCreateProductConfigurationsPanel.createNewAttribute}}" time="30" stepKey="waitForConfigurationModalOpen" after="clickCreateConfigurations"/> -+ -+ <!--Create new attribute with two options --> -+ <actionGroup ref="addNewProductConfigurationAttribute" stepKey="createProductConfigurationAttribute"> -+ <argument name="attribute" value="colorProductAttribute"/> -+ <argument name="firstOption" value="colorConfigurableProductAttribute1"/> -+ <argument name="secondOption" value="colorConfigurableProductAttribute2"/> -+ </actionGroup> -+ -+ <!-- Change product configurations in grid --> -+ <actionGroup ref="changeProductConfigurationsInGrid" stepKey="changeProductConfigurationsInGrid"> -+ <argument name="firstOption" value="colorConfigurableProductAttribute1"/> -+ <argument name="secondOption" value="colorConfigurableProductAttribute2"/> -+ </actionGroup> -+ -+ <!-- Add configurable product to category --> -+ <searchAndMultiSelectOption selector="{{AdminProductFormSection.categoriesDropdown}}" parameterArray="[$$createCategory.name$$]" stepKey="fillCategory" after="fillConfigurableProductValues"/> -+ -+ <!-- Save configurable product; add product to new attribute set --> -+ <actionGroup ref="saveConfigurableProductWithNewAttributeSet" stepKey="saveConfigurableProduct"/> -+ -+ <!-- Assert child products in grid --> -+ <actionGroup ref="viewProductInAdminGrid" stepKey="viewFirstChildProductInAdminGrid"> -+ <argument name="product" value="colorConfigurableProductAttribute1"/> -+ </actionGroup> -+ <actionGroup ref="viewProductInAdminGrid" stepKey="viewSecondChildProductInAdminGrid"> -+ <argument name="product" value="colorConfigurableProductAttribute2"/> -+ </actionGroup> -+ -+ <!-- Assert configurable product in grid --> -+ <actionGroup ref="filterProductGridBySkuAndName" stepKey="findCreatedConfigurableProduct"> -+ <argument name="product" value="ApiConfigurableProduct"/> -+ </actionGroup> -+ <see selector="{{AdminProductGridSection.productGridCell('1', 'Type')}}" userInput="{{ApiConfigurableProduct.type_id}}" stepKey="seeProductTypeInGrid"/> -+ <click selector="{{AdminProductGridFilterSection.clearFilters}}" stepKey="clickClearFiltersAfter"/> -+ -+ <!-- Flash cache --> -+ <magentoCLI command="cache:flush" stepKey="flushCache"/> -+ -+ <!--Assert configurable product in category --> -+ <amOnPage url="$$createCategory.name$$.html" stepKey="amOnCategoryPage"/> -+ <waitForPageLoad stepKey="waitForCategoryPageLoad"/> -+ <actionGroup ref="StorefrontCheckCategoryConfigurableProduct" stepKey="assertConfigurableProductInCategory"> -+ <argument name="product" value="ApiConfigurableProduct"/> -+ <argument name="optionProduct" value="colorConfigurableProductAttribute1"/> -+ </actionGroup> -+ -+ <!--Assert configurable product on product page --> -+ <amOnPage url="{{ApiConfigurableProduct.urlKey}}.html" stepKey="amOnProductPage" after="assertConfigurableProductInCategory"/> -+ <waitForPageLoad stepKey="waitForProductPageLoad"/> -+ <actionGroup ref="storefrontCheckConfigurableProductOptions" stepKey="checkConfigurableProductOptions"> -+ <argument name="product" value="ApiConfigurableProduct"/> -+ <argument name="firstOption" value="colorConfigurableProductAttribute1"/> -+ <argument name="secondOption" value="colorConfigurableProductAttribute2"/> -+ </actionGroup> -+ -+ <!-- Add configurable product to the cart with selected first option --> -+ <selectOption userInput="{{colorConfigurableProductAttribute1.name}}" selector="{{StorefrontProductInfoMainSection.productAttributeOptionsSelectButton}}" stepKey="selectOptionForAddingToCart"/> -+ <click selector="{{StorefrontProductInfoMainSection.AddToCart}}" stepKey="clickAddToCart"/> -+ <waitForElementVisible selector="{{StorefrontCategoryMainSection.SuccessMsg}}" stepKey="waitForSuccessMessage"/> -+ -+ <!-- Assert configurable product in cart --> -+ <amOnPage url="/checkout/cart/" stepKey="amOnShoppingCartPage"/> -+ <waitForPageLoad stepKey="waitForShoppingCartPageLoad"/> -+ <actionGroup ref="StorefrontCheckCartConfigurableProductActionGroup" stepKey="storefrontCheckCartConfigurableProductActionGroup"> -+ <argument name="product" value="ApiConfigurableProduct"/> -+ <argument name="optionProduct" value="colorConfigurableProductAttribute1"/> -+ <argument name="productQuantity" value="CONST.one"/> -+ </actionGroup> -+ -+ <!-- Assert child products are not displayed separately: two next step --> -+ <amOnPage url="{{StorefrontHomePage.url}}" stepKey="goToStoreFront"/> -+ <waitForPageLoad stepKey="waitForStoreFrontPageLoad"/> -+ -+ <!-- Quick search the storefront for the first attribute option --> -+ <submitForm selector="{{StorefrontQuickSearchSection.searchMiniForm}}" parameterArray="['q' => {{colorConfigurableProductAttribute1.sku}}]" stepKey="searchStorefrontFirstChildProduct"/> -+ <dontSee selector="{{StorefrontCategoryProductSection.ProductTitleByName(colorConfigurableProductAttribute1.name)}}" stepKey="dontSeeConfigurableProductFirstChild"/> -+ -+ <!-- Quick search the storefront for the second attribute option --> -+ <submitForm selector="{{StorefrontQuickSearchSection.searchMiniForm}}" parameterArray="['q' => {{colorConfigurableProductAttribute2.sku}}]" stepKey="searchStorefrontSecondChildProduct"/> -+ <dontSee selector="{{StorefrontCategoryProductSection.ProductTitleByName(colorConfigurableProductAttribute2.name)}}" stepKey="dontSeeConfigurableProductSecondChild"/> -+ </test> -+</tests> -diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminCreateConfigurableProductWithTwoOptionsWithoutAssignedToCategoryTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminCreateConfigurableProductWithTwoOptionsWithoutAssignedToCategoryTest.xml -new file mode 100644 -index 00000000000..934a410d58a ---- /dev/null -+++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminCreateConfigurableProductWithTwoOptionsWithoutAssignedToCategoryTest.xml -@@ -0,0 +1,136 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> -+ <test name="AdminCreateConfigurableProductWithTwoOptionsWithoutAssignedToCategoryTest"> -+ <annotations> -+ <features value="ConfigurableProduct"/> -+ <stories value="Create configurable product"/> -+ <title value="Create configurable product with two new options without assigned to category with not visible child products"/> -+ <description value="Admin should be able to create configurable product with two options without assigned to category, child products are not visible individually"/> -+ <testCaseId value="MC-13686"/> -+ <severity value="CRITICAL"/> -+ <group value="mtf_migrated"/> -+ </annotations> -+ <before> -+ <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> -+ </before> -+ <after> -+ <!-- Delete configurable product --> -+ <actionGroup ref="deleteProductUsingProductGrid" stepKey="deleteProduct"> -+ <argument name="product" value="ApiConfigurableProduct"/> -+ </actionGroup> -+ -+ <!-- Delete children products --> -+ <actionGroup ref="deleteProductBySku" stepKey="deleteFirstChildProduct"> -+ <argument name="sku" value="{{colorConfigurableProductAttribute1.sku}}"/> -+ </actionGroup> -+ <actionGroup ref="deleteProductBySku" stepKey="deleteSecondChildProduct"> -+ <argument name="sku" value="{{colorConfigurableProductAttribute2.sku}}"/> -+ </actionGroup> -+ -+ <!-- Delete product attribute --> -+ <actionGroup ref="deleteProductAttributeByLabel" stepKey="deleteProductAttribute"> -+ <argument name="ProductAttribute" value="colorProductAttribute"/> -+ </actionGroup> -+ -+ <!-- Delete attribute set --> -+ <actionGroup ref="deleteAttributeSetByLabel" stepKey="deleteAttributeSet"> -+ <argument name="label" value="{{ProductAttributeFrontendLabel.label}}"/> -+ </actionGroup> -+ -+ <!-- Log out --> -+ <actionGroup ref="logout" stepKey="logout"/> -+ </after> -+ -+ <!-- Create configurable product --> -+ <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="amOnProductGridPage"/> -+ <waitForPageLoad stepKey="waitForProductGridPageLoad"/> -+ <actionGroup ref="goToCreateProductPage" stepKey="createConfigurableProduct"> -+ <argument name="product" value="ApiConfigurableProduct"/> -+ </actionGroup> -+ -+ <!-- Fill configurable product values --> -+ <actionGroup ref="fillMainProductForm" stepKey="fillConfigurableProductValues"> -+ <argument name="product" value="ApiConfigurableProduct"/> -+ </actionGroup> -+ -+ <!--Create product configurations--> -+ <click selector="{{AdminProductFormConfigurationsSection.createConfigurations}}" stepKey="clickCreateConfigurations" after="fillConfigurableProductValues"/> -+ <waitForElementVisible selector="{{AdminCreateProductConfigurationsPanel.createNewAttribute}}" time="30" stepKey="waitForConfigurationModalOpen" after="clickCreateConfigurations"/> -+ -+ <!--Create new attribute with two option --> -+ <actionGroup ref="addNewProductConfigurationAttribute" stepKey="createProductConfigurationAttribute"> -+ <argument name="attribute" value="colorProductAttribute"/> -+ <argument name="firstOption" value="colorConfigurableProductAttribute1"/> -+ <argument name="secondOption" value="colorConfigurableProductAttribute2"/> -+ </actionGroup> -+ -+ <!-- Change product configurations in grid --> -+ <actionGroup ref="changeProductConfigurationsInGrid" stepKey="changeProductConfigurationsInGrid"> -+ <argument name="firstOption" value="colorConfigurableProductAttribute1"/> -+ <argument name="secondOption" value="colorConfigurableProductAttribute2"/> -+ </actionGroup> -+ -+ <!-- Save configurable product; add product to new attribute set --> -+ <actionGroup ref="saveConfigurableProductWithNewAttributeSet" stepKey="saveConfigurableProduct"/> -+ -+ <!-- Assert Child Products in grid --> -+ <actionGroup ref="viewProductInAdminGrid" stepKey="viewFirstChildProductInAdminGrid"> -+ <argument name="product" value="colorConfigurableProductAttribute1"/> -+ </actionGroup> -+ <actionGroup ref="viewProductInAdminGrid" stepKey="viewSecondChildProductInAdminGrid"> -+ <argument name="product" value="colorConfigurableProductAttribute2"/> -+ </actionGroup> -+ -+ <!-- Assert Configurable Product in grid --> -+ <actionGroup ref="filterProductGridBySkuAndName" stepKey="findCreatedConfigurableProduct"> -+ <argument name="product" value="ApiConfigurableProduct"/> -+ </actionGroup> -+ <see selector="{{AdminProductGridSection.productGridCell('1', 'Type')}}" userInput="{{ApiConfigurableProduct.type_id}}" stepKey="seeProductTypeInGrid"/> -+ <click selector="{{AdminProductGridFilterSection.clearFilters}}" stepKey="clickClearFiltersAfter"/> -+ -+ <!-- Flash cache --> -+ <magentoCLI command="cache:flush" stepKey="flushCache"/> -+ -+ <!-- Assert configurable product on product page --> -+ <amOnPage url="{{ApiConfigurableProduct.urlKey}}.html" stepKey="amOnProductPage"/> -+ <waitForPageLoad stepKey="waitForProductPageLoad"/> -+ <actionGroup ref="storefrontCheckConfigurableProductOptions" stepKey="checkConfigurableProductOptions"> -+ <argument name="product" value="ApiConfigurableProduct"/> -+ <argument name="firstOption" value="colorConfigurableProductAttribute1"/> -+ <argument name="secondOption" value="colorConfigurableProductAttribute2"/> -+ </actionGroup> -+ -+ <!-- Add configurable product to the cart with selected first option --> -+ <selectOption userInput="{{colorConfigurableProductAttribute1.name}}" selector="{{StorefrontProductInfoMainSection.productAttributeOptionsSelectButton}}" stepKey="selectOptionForAddingToCart"/> -+ <click selector="{{StorefrontProductInfoMainSection.AddToCart}}" stepKey="clickAddToCart"/> -+ <waitForElementVisible selector="{{StorefrontCategoryMainSection.SuccessMsg}}" stepKey="waitForSuccessMessage"/> -+ -+ <!-- Assert configurable product in cart --> -+ <amOnPage url="/checkout/cart/" stepKey="amOnShoppingCartPage"/> -+ <waitForPageLoad stepKey="waitForShoppingCartPageLoad"/> -+ <actionGroup ref="StorefrontCheckCartConfigurableProductActionGroup" stepKey="storefrontCheckCartConfigurableProductActionGroup"> -+ <argument name="product" value="ApiConfigurableProduct"/> -+ <argument name="optionProduct" value="colorConfigurableProductAttribute1"/> -+ <argument name="productQuantity" value="CONST.one"/> -+ </actionGroup> -+ -+ <!-- Assert child products are not displayed separately: two next step --> -+ <amOnPage url="{{StorefrontHomePage.url}}" stepKey="goToStoreFront"/> -+ <waitForPageLoad stepKey="waitForStoreFrontPageLoad"/> -+ -+ <!-- Quick search the storefront for the first attribute option --> -+ <submitForm selector="{{StorefrontQuickSearchSection.searchMiniForm}}" parameterArray="['q' => {{colorConfigurableProductAttribute1.sku}}]" stepKey="searchStorefrontFirstChildProduct"/> -+ <dontSee selector="{{StorefrontCategoryProductSection.ProductTitleByName(colorConfigurableProductAttribute1.name)}}" stepKey="dontSeeConfigurableProductFirstChild"/> -+ -+ <!-- Quick search the storefront for the second attribute option --> -+ <submitForm selector="{{StorefrontQuickSearchSection.searchMiniForm}}" parameterArray="['q' => {{colorConfigurableProductAttribute2.sku}}]" stepKey="searchStorefrontSecondChildProduct"/> -+ <dontSee selector="{{StorefrontCategoryProductSection.ProductTitleByName(colorConfigurableProductAttribute2.name)}}" stepKey="dontSeeConfigurableProductSecondChild"/> -+ </test> -+</tests> -diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminDeleteConfigurableProductTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminDeleteConfigurableProductTest.xml -new file mode 100644 -index 00000000000..fb2920be528 ---- /dev/null -+++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminDeleteConfigurableProductTest.xml -@@ -0,0 +1,52 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> -+ <test name="AdminDeleteConfigurableProductTest"> -+ <annotations> -+ <features value="ConfigurableProduct"/> -+ <stories value="Delete products"/> -+ <title value="Delete configurable product test"/> -+ <description value="Admin should be able to delete a configurable product"/> -+ <severity value="CRITICAL"/> -+ <testCaseId value="MC-11020"/> -+ <group value="mtf_migrated"/> -+ </annotations> -+ <before> -+ <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> -+ <createData entity="_defaultCategory" stepKey="createCategory"/> -+ <createData entity="BaseConfigurableProduct" stepKey="createConfigurableProduct"> -+ <requiredEntity createDataKey="createCategory"/> -+ </createData> -+ </before> -+ <after> -+ <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> -+ <actionGroup ref="logout" stepKey="logout"/> -+ </after> -+ <actionGroup ref="deleteProductUsingProductGrid" stepKey="deleteConfigurableProductFilteredBySkuAndName"> -+ <argument name="product" value="$$createConfigurableProduct$$"/> -+ </actionGroup> -+ <see selector="{{AdminCategoryMessagesSection.SuccessMessage}}" userInput="A total of 1 record(s) have been deleted." stepKey="deleteMessage"/> -+ <!-- Verify product on Product Page --> -+ <amOnPage url="{{StorefrontProductPage.url($$createConfigurableProduct.name$$)}}" stepKey="amOnConfigurableProductPage"/> -+ <see selector="{{StorefrontProductInfoMainSection.productName}}" userInput="Whoops, our bad..." stepKey="seeWhoops"/> -+ <!-- Search for the product by sku --> -+ <fillField selector="{{StorefrontQuickSearchSection.searchPhrase}}" userInput="$$createConfigurableProduct.sku$$" stepKey="fillSearchBarByProductSku"/> -+ <waitForPageLoad stepKey="waitForSearchButton"/> -+ <click selector="{{StorefrontQuickSearchSection.searchButton}}" stepKey="clickSearchButton"/> -+ <waitForPageLoad stepKey="waitForSearchResults"/> -+ <!-- Should not see any search results --> -+ <dontSee userInput="$$createConfigurableProduct.sku$$" selector="{{StorefrontCatalogSearchMainSection.searchResults}}" stepKey="dontSeeProduct"/> -+ <see selector="{{StorefrontCatalogSearchMainSection.message}}" userInput="Your search returned no results." stepKey="seeCantFindProductOneMessage"/> -+ <!-- Go to the category page that we created in the before block --> -+ <amOnPage url="{{StorefrontCategoryPage.url($$createCategory.name$$)}}" stepKey="onCategoryPage"/> -+ <!-- Should not see the product --> -+ <dontSee userInput="$$createConfigurableProduct.name$$" selector="{{StorefrontCategoryMainSection.productsList}}" stepKey="dontSeeProductInCategory"/> -+ <see selector="{{StorefrontCategoryMainSection.emptyProductMessage}}" userInput="We can't find products matching the selection." stepKey="seeEmptyProductMessage"/> -+ </test> -+</tests> -diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminRelatedProductsTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminRelatedProductsTest.xml -index 4461c06ed6b..aa34693ed82 100644 ---- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminRelatedProductsTest.xml -+++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminRelatedProductsTest.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="AdminRelatedProductsTest"> - <annotations> - <features value="ConfigurableProduct"/> -diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminRemoveDefaultImageConfigurableTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminRemoveDefaultImageConfigurableTest.xml -index 65e1300a4e6..e7492f4eeae 100644 ---- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminRemoveDefaultImageConfigurableTest.xml -+++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminRemoveDefaultImageConfigurableTest.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="AdminRemoveDefaultImageConfigurableTest"> - <annotations> - <features value="ConfigurableProduct"/> -diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdvanceCatalogSearchConfigurableTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdvanceCatalogSearchConfigurableTest.xml -new file mode 100644 -index 00000000000..c303e4d19db ---- /dev/null -+++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdvanceCatalogSearchConfigurableTest.xml -@@ -0,0 +1,307 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> -+ <test name="AdvanceCatalogSearchConfigurableByNameTest" extends="AdvanceCatalogSearchSimpleProductByNameTest"> -+ <annotations> -+ <features value="ConfigurableProduct"/> -+ <stories value="Advanced Catalog Product Search for all product types"/> -+ <title value="Guest customer should be able to advance search configurable product with product name"/> -+ <description value="Guest customer should be able to advance search configurable product with product name"/> -+ <severity value="MAJOR"/> -+ <testCaseId value="MC-138"/> -+ <group value="ConfigurableProduct"/> -+ </annotations> -+ <before> -+ <createData entity="SimpleSubCategory" stepKey="categoryHandle" before="simple1Handle"/> -+ -+ <createData entity="SimpleProduct" stepKey="simple1Handle" before="simple2Handle"> -+ <requiredEntity createDataKey="categoryHandle"/> -+ </createData> -+ -+ <createData entity="SimpleProduct" stepKey="simple2Handle" before="product"> -+ <requiredEntity createDataKey="categoryHandle"/> -+ </createData> -+ -+ <!-- TODO: Move configurable product creation to an actionGroup when MQE-697 is fixed --> -+ <createData entity="ApiConfigurableProductWithDescription" stepKey="product"/> -+ -+ <createData entity="productDropDownAttribute" stepKey="productAttributeHandle"/> -+ -+ <createData entity="productAttributeOption1" stepKey="productAttributeOption1Handle"> -+ <requiredEntity createDataKey="productAttributeHandle"/> -+ </createData> -+ <createData entity="productAttributeOption2" stepKey="productAttributeOption2Handle"> -+ <requiredEntity createDataKey="productAttributeHandle"/> -+ </createData> -+ -+ <createData entity="AddToDefaultSet" stepKey="addToAttributeSetHandle"> -+ <requiredEntity createDataKey="productAttributeHandle"/> -+ </createData> -+ -+ <getData entity="ProductAttributeOptionGetter" index="1" stepKey="getAttributeOption1Handle"> -+ <requiredEntity createDataKey="productAttributeHandle"/> -+ </getData> -+ <getData entity="ProductAttributeOptionGetter" index="2" stepKey="getAttributeOption2Handle"> -+ <requiredEntity createDataKey="productAttributeHandle"/> -+ </getData> -+ -+ <createData entity="SimpleOne" stepKey="childProductHandle1"> -+ <requiredEntity createDataKey="productAttributeHandle"/> -+ <requiredEntity createDataKey="getAttributeOption1Handle"/> -+ </createData> -+ <createData entity="SimpleOne" stepKey="childProductHandle2"> -+ <requiredEntity createDataKey="productAttributeHandle"/> -+ <requiredEntity createDataKey="getAttributeOption2Handle"/> -+ </createData> -+ -+ <createData entity="ConfigurableProductTwoOptions" stepKey="configProductOptionHandle"> -+ <requiredEntity createDataKey="product"/> -+ <requiredEntity createDataKey="productAttributeHandle"/> -+ <requiredEntity createDataKey="getAttributeOption1Handle"/> -+ <requiredEntity createDataKey="getAttributeOption2Handle"/> -+ </createData> -+ -+ <createData entity="ConfigurableProductAddChild" stepKey="configProductHandle1"> -+ <requiredEntity createDataKey="product"/> -+ <requiredEntity createDataKey="childProductHandle1"/> -+ </createData> -+ <createData entity="ConfigurableProductAddChild" stepKey="configProductHandle2"> -+ <requiredEntity createDataKey="product"/> -+ <requiredEntity createDataKey="childProductHandle2"/> -+ </createData> -+ </before> -+ <after> -+ <deleteData createDataKey="simple1Handle" stepKey="deleteSimple1" before="deleteSimple2"/> -+ <deleteData createDataKey="simple2Handle" stepKey="deleteSimple2" before="delete"/> -+ </after> -+ </test> -+ <test name="AdvanceCatalogSearchConfigurableBySkuTest" extends="AdvanceCatalogSearchSimpleProductBySkuTest"> -+ <annotations> -+ <features value="ConfigurableProduct"/> -+ <stories value="Advanced Catalog Product Search for all product types"/> -+ <title value="Guest customer should be able to advance search configurable product with product sku"/> -+ <description value="Guest customer should be able to advance search configurable product with product sku"/> -+ <severity value="MAJOR"/> -+ <testCaseId value="MC-144"/> -+ <group value="ConfigurableProduct"/> -+ </annotations> -+ <before> -+ <createData entity="SimpleSubCategory" stepKey="categoryHandle" before="simple1Handle"/> -+ -+ <createData entity="SimpleProduct" stepKey="simple1Handle" before="simple2Handle"> -+ <requiredEntity createDataKey="categoryHandle"/> -+ </createData> -+ -+ <createData entity="SimpleProduct" stepKey="simple2Handle" before="product"> -+ <requiredEntity createDataKey="categoryHandle"/> -+ </createData> -+ -+ <!-- TODO: Move configurable product creation to an actionGroup when MQE-697 is fixed --> -+ <createData entity="ApiConfigurableProductWithDescription" stepKey="product"/> -+ -+ <createData entity="productDropDownAttribute" stepKey="productAttributeHandle"/> -+ -+ <createData entity="productAttributeOption1" stepKey="productAttributeOption1Handle"> -+ <requiredEntity createDataKey="productAttributeHandle"/> -+ </createData> -+ <createData entity="productAttributeOption2" stepKey="productAttributeOption2Handle"> -+ <requiredEntity createDataKey="productAttributeHandle"/> -+ </createData> -+ -+ <createData entity="AddToDefaultSet" stepKey="addToAttributeSetHandle"> -+ <requiredEntity createDataKey="productAttributeHandle"/> -+ </createData> -+ -+ <getData entity="ProductAttributeOptionGetter" index="1" stepKey="getAttributeOption1Handle"> -+ <requiredEntity createDataKey="productAttributeHandle"/> -+ </getData> -+ <getData entity="ProductAttributeOptionGetter" index="2" stepKey="getAttributeOption2Handle"> -+ <requiredEntity createDataKey="productAttributeHandle"/> -+ </getData> -+ -+ <createData entity="SimpleOne" stepKey="childProductHandle1"> -+ <requiredEntity createDataKey="productAttributeHandle"/> -+ <requiredEntity createDataKey="getAttributeOption1Handle"/> -+ </createData> -+ <createData entity="SimpleOne" stepKey="childProductHandle2"> -+ <requiredEntity createDataKey="productAttributeHandle"/> -+ <requiredEntity createDataKey="getAttributeOption2Handle"/> -+ </createData> -+ -+ <createData entity="ConfigurableProductTwoOptions" stepKey="configProductOptionHandle"> -+ <requiredEntity createDataKey="product"/> -+ <requiredEntity createDataKey="productAttributeHandle"/> -+ <requiredEntity createDataKey="getAttributeOption1Handle"/> -+ <requiredEntity createDataKey="getAttributeOption2Handle"/> -+ </createData> -+ -+ <createData entity="ConfigurableProductAddChild" stepKey="configProductHandle1"> -+ <requiredEntity createDataKey="product"/> -+ <requiredEntity createDataKey="childProductHandle1"/> -+ </createData> -+ <createData entity="ConfigurableProductAddChild" stepKey="configProductHandle2"> -+ <requiredEntity createDataKey="product"/> -+ <requiredEntity createDataKey="childProductHandle2"/> -+ </createData> -+ </before> -+ <after> -+ <deleteData createDataKey="simple1Handle" stepKey="deleteSimple1" before="deleteSimple2"/> -+ <deleteData createDataKey="simple2Handle" stepKey="deleteSimple2" before="delete"/> -+ </after> -+ </test> -+ <test name="AdvanceCatalogSearchConfigurableByDescriptionTest" extends="AdvanceCatalogSearchSimpleProductByDescriptionTest"> -+ <annotations> -+ <features value="ConfigurableProduct"/> -+ <stories value="Advanced Catalog Product Search for all product types"/> -+ <title value="Guest customer should be able to advance search configurable product with product description"/> -+ <description value="Guest customer should be able to advance search configurable product with product description"/> -+ <severity value="MAJOR"/> -+ <testCaseId value="MC-237"/> -+ <group value="ConfigurableProduct"/> -+ </annotations> -+ <before> -+ <createData entity="SimpleSubCategory" stepKey="categoryHandle" before="simple1Handle"/> -+ -+ <createData entity="SimpleProduct" stepKey="simple1Handle" before="simple2Handle"> -+ <requiredEntity createDataKey="categoryHandle"/> -+ </createData> -+ -+ <createData entity="SimpleProduct" stepKey="simple2Handle" before="product"> -+ <requiredEntity createDataKey="categoryHandle"/> -+ </createData> -+ -+ <!-- TODO: Move configurable product creation to an actionGroup when MQE-697 is fixed --> -+ <createData entity="ApiConfigurableProductWithDescription" stepKey="product"/> -+ -+ <createData entity="productDropDownAttribute" stepKey="productAttributeHandle"/> -+ -+ <createData entity="productAttributeOption1" stepKey="productAttributeOption1Handle"> -+ <requiredEntity createDataKey="productAttributeHandle"/> -+ </createData> -+ <createData entity="productAttributeOption2" stepKey="productAttributeOption2Handle"> -+ <requiredEntity createDataKey="productAttributeHandle"/> -+ </createData> -+ -+ <createData entity="AddToDefaultSet" stepKey="addToAttributeSetHandle"> -+ <requiredEntity createDataKey="productAttributeHandle"/> -+ </createData> -+ -+ <getData entity="ProductAttributeOptionGetter" index="1" stepKey="getAttributeOption1Handle"> -+ <requiredEntity createDataKey="productAttributeHandle"/> -+ </getData> -+ <getData entity="ProductAttributeOptionGetter" index="2" stepKey="getAttributeOption2Handle"> -+ <requiredEntity createDataKey="productAttributeHandle"/> -+ </getData> -+ -+ <createData entity="SimpleOne" stepKey="childProductHandle1"> -+ <requiredEntity createDataKey="productAttributeHandle"/> -+ <requiredEntity createDataKey="getAttributeOption1Handle"/> -+ </createData> -+ <createData entity="SimpleOne" stepKey="childProductHandle2"> -+ <requiredEntity createDataKey="productAttributeHandle"/> -+ <requiredEntity createDataKey="getAttributeOption2Handle"/> -+ </createData> -+ -+ <createData entity="ConfigurableProductTwoOptions" stepKey="configProductOptionHandle"> -+ <requiredEntity createDataKey="product"/> -+ <requiredEntity createDataKey="productAttributeHandle"/> -+ <requiredEntity createDataKey="getAttributeOption1Handle"/> -+ <requiredEntity createDataKey="getAttributeOption2Handle"/> -+ </createData> -+ -+ <createData entity="ConfigurableProductAddChild" stepKey="configProductHandle1"> -+ <requiredEntity createDataKey="product"/> -+ <requiredEntity createDataKey="childProductHandle1"/> -+ </createData> -+ <createData entity="ConfigurableProductAddChild" stepKey="configProductHandle2"> -+ <requiredEntity createDataKey="product"/> -+ <requiredEntity createDataKey="childProductHandle2"/> -+ </createData> -+ </before> -+ <after> -+ <deleteData createDataKey="simple1Handle" stepKey="deleteSimple1" before="deleteSimple2"/> -+ <deleteData createDataKey="simple2Handle" stepKey="deleteSimple2" before="delete"/> -+ </after> -+ </test> -+ <test name="AdvanceCatalogSearchConfigurableByShortDescriptionTest" extends="AdvanceCatalogSearchSimpleProductByShortDescriptionTest"> -+ <annotations> -+ <features value="ConfigurableProduct"/> -+ <stories value="Advanced Catalog Product Search for all product types"/> -+ <title value="Guest customer should be able to advance search configurable product with product short description"/> -+ <description value="Guest customer should be able to advance search configurable product with product short description"/> -+ <severity value="MAJOR"/> -+ <testCaseId value="MC-240"/> -+ <group value="ConfigurableProduct"/> -+ </annotations> -+ <before> -+ <createData entity="SimpleSubCategory" stepKey="categoryHandle" before="simple1Handle"/> -+ -+ <createData entity="SimpleProduct" stepKey="simple1Handle" before="simple2Handle"> -+ <requiredEntity createDataKey="categoryHandle"/> -+ </createData> -+ -+ <createData entity="SimpleProduct" stepKey="simple2Handle" before="product"> -+ <requiredEntity createDataKey="categoryHandle"/> -+ </createData> -+ -+ <!-- TODO: Move configurable product creation to an actionGroup when MQE-697 is fixed --> -+ <createData entity="ApiConfigurableProductWithDescription" stepKey="product"/> -+ -+ <createData entity="productDropDownAttribute" stepKey="productAttributeHandle"/> -+ -+ <createData entity="productAttributeOption1" stepKey="productAttributeOption1Handle"> -+ <requiredEntity createDataKey="productAttributeHandle"/> -+ </createData> -+ <createData entity="productAttributeOption2" stepKey="productAttributeOption2Handle"> -+ <requiredEntity createDataKey="productAttributeHandle"/> -+ </createData> -+ -+ <createData entity="AddToDefaultSet" stepKey="addToAttributeSetHandle"> -+ <requiredEntity createDataKey="productAttributeHandle"/> -+ </createData> -+ -+ <getData entity="ProductAttributeOptionGetter" index="1" stepKey="getAttributeOption1Handle"> -+ <requiredEntity createDataKey="productAttributeHandle"/> -+ </getData> -+ <getData entity="ProductAttributeOptionGetter" index="2" stepKey="getAttributeOption2Handle"> -+ <requiredEntity createDataKey="productAttributeHandle"/> -+ </getData> -+ -+ <createData entity="SimpleOne" stepKey="childProductHandle1"> -+ <requiredEntity createDataKey="productAttributeHandle"/> -+ <requiredEntity createDataKey="getAttributeOption1Handle"/> -+ </createData> -+ <createData entity="SimpleOne" stepKey="childProductHandle2"> -+ <requiredEntity createDataKey="productAttributeHandle"/> -+ <requiredEntity createDataKey="getAttributeOption2Handle"/> -+ </createData> -+ -+ <createData entity="ConfigurableProductTwoOptions" stepKey="configProductOptionHandle"> -+ <requiredEntity createDataKey="product"/> -+ <requiredEntity createDataKey="productAttributeHandle"/> -+ <requiredEntity createDataKey="getAttributeOption1Handle"/> -+ <requiredEntity createDataKey="getAttributeOption2Handle"/> -+ </createData> -+ -+ <createData entity="ConfigurableProductAddChild" stepKey="configProductHandle1"> -+ <requiredEntity createDataKey="product"/> -+ <requiredEntity createDataKey="childProductHandle1"/> -+ </createData> -+ <createData entity="ConfigurableProductAddChild" stepKey="configProductHandle2"> -+ <requiredEntity createDataKey="product"/> -+ <requiredEntity createDataKey="childProductHandle2"/> -+ </createData> -+ </before> -+ <after> -+ <deleteData createDataKey="simple1Handle" stepKey="deleteSimple1" before="deleteSimple2"/> -+ <deleteData createDataKey="simple2Handle" stepKey="deleteSimple2" before="delete"/> -+ </after> -+ </test> -+</tests> -diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/ConfigurableProductAttributeNameDesignTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/ConfigurableProductAttributeNameDesignTest.xml -index bb7792b9d37..7fbff5eac25 100644 ---- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/ConfigurableProductAttributeNameDesignTest.xml -+++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/ConfigurableProductAttributeNameDesignTest.xml -@@ -7,10 +7,11 @@ - --> - - <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="ConfigurableProductAttributeNameDesignTest"> - <annotations> - <title value="Generation of configurable products with an attribute named 'design'"/> -+ <description value="Generation of configurable products with an attribute named 'design'"/> - <features value="Product Customizable Option"/> - <severity value="AVERAGE"/> - <testCaseId value="MAGETWO-93307"/> -diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/ConfigurableProductPriceAdditionalStoreViewTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/ConfigurableProductPriceAdditionalStoreViewTest.xml -index 676c23f1cfb..a71f51526c8 100644 ---- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/ConfigurableProductPriceAdditionalStoreViewTest.xml -+++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/ConfigurableProductPriceAdditionalStoreViewTest.xml -@@ -7,17 +7,16 @@ - --> - - <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="ConfigurableProductPriceAdditionalStoreViewTest"> - <annotations> - <features value="ConfigurableProductPriceStoreFront"/> -+ <stories value="View products"/> - <title value="Configurable product prices should not disappear on storefront for additional store"/> - <description value="Configurable product price should not disappear for additional stores on frontEnd if disabled for default store"/> - <severity value="CRITICAL"/> - <testCaseId value="MAGETWO-92247"/> -- <skip> -- <issueId value="MAGETWO-92247"/> -- </skip> -+ <group value="ConfigurableProduct"/> - </annotations> - <before> - -@@ -132,12 +131,13 @@ - <click selector="{{AdminProductFormActionSection.changeStoreButton}}" stepKey="clickStoreviewSwitcher"/> - <click selector="{{AdminProductFormActionSection.selectStoreView('Second Store View')}}" stepKey="chooseStoreView"/> - <click selector="{{AdminConfirmationModalSection.ok}}" stepKey="acceptStoreSwitchingMessage"/> -- <!--<waitForPageLoad stepKey="waitForStoreViewSwitched"/>--> - <waitForPageLoad time="30" stepKey="waitForPageLoad9"/> - <see userInput="Second Store View" selector="{{AdminMainActionsSection.storeSwitcher}}" stepKey="seeNewStoreViewName"/> - - <!-- enable the config product for the second store --> -- <waitForElementVisible selector="{{AdminProductFormSection.enableProductLabel}}" stepKey="waitForProductEnableSlider"/> -+ <waitForElementVisible selector="{{AdminProductFormSection.productStatusUseDefault}}" stepKey="waitForDefaultValueCheckBox"/> -+ <click selector="{{AdminProductFormSection.productStatusUseDefault}}" stepKey="unCheckUseDefaultValueCheckBox"/> -+ <waitForElementVisible selector="{{AdminProductFormSection.enableProductLabel}}" stepKey="waitForProductEnableSlider"/> - <click selector="{{AdminProductFormSection.enableProductLabel}}" stepKey="enableProductForSecondStoreView"/> - <seeCheckboxIsChecked selector="{{AdminProductFormSection.productStatus}}" stepKey="seeThatProductIsEnabled"/> - <actionGroup ref="AdminFormSaveAndClose" stepKey="enabledConfigProductSecondStore"/> -@@ -150,7 +150,6 @@ - <click selector="{{AdminConfirmationModalSection.ok}}" stepKey="acceptStoreSwitchingMessage1"/> - <waitForPageLoad time="30" stepKey="waitForPageLoad8"/> - <see userInput="Second Store View" selector="{{AdminMainActionsSection.storeSwitcher}}" stepKey="seeNewStoreViewName1"/> -- - <click selector="{{AdminProductFormConfigurationsSection.actionsBtn('1')}}" stepKey="clickToExpandActionsForFirstVariation2"/> - <click selector="{{AdminProductFormConfigurationsSection.enableProductBtn}}" stepKey="clickEnableChildProduct1"/> - <click selector="{{AdminProductFormConfigurationsSection.actionsBtn('2')}}" stepKey="clickToExpandActionsForSecondVariation2"/> -@@ -180,7 +179,6 @@ - <waitForPageLoad time="30" stepKey="waitForStoreViewSwitchedP1"/> - <see userInput="Second Store View" selector="{{AdminMainActionsSection.storeSwitcher}}" stepKey="seeNewStoreViewNameP1"/> - <waitForElementVisible selector="{{AdminProductFormSection.enableProductLabel}}" stepKey="waitForProductEnableSliderP1"/> -- <click selector="{{AdminProductFormSection.enableProductLabel}}" stepKey="enableProductForSecondStoreViewP1"/> - <seeCheckboxIsChecked selector="{{AdminProductFormSection.productStatus}}" stepKey="seeThatProduct1IsEnabled"/> - <actionGroup ref="AdminFormSaveAndClose" stepKey="save2UpdatedChild1"/> - -@@ -200,7 +198,6 @@ - <waitForPageLoad time="30" stepKey="waitForStoreViewSwitchedP2"/> - <see userInput="Second Store View" selector="{{AdminMainActionsSection.storeSwitcher}}" stepKey="seeNewStoreViewNameP2"/> - <waitForElementVisible selector="{{AdminProductFormSection.enableProductLabel}}" stepKey="waitForProductEnableSliderP2"/> -- <click selector="{{AdminProductFormSection.enableProductLabel}}" stepKey="enableProductForSecondStoreViewP2"/> - <seeCheckboxIsChecked selector="{{AdminProductFormSection.productStatus}}" stepKey="seeThatProduct2IsEnabled"/> - <actionGroup ref="AdminFormSaveAndClose" stepKey="save2UpdatedChild2"/> - -@@ -210,6 +207,6 @@ - <click userInput="$$createCategory.name$$" stepKey="clickOnCategoryName"/> - <waitForPageLoad stepKey="waitForPageLoad4"/> - <see userInput="$$createConfigProduct.name$$" stepKey="assertProductPresent"/> -- <dontSee userInput="$$createConfigChildProduct1.price$$" stepKey="assertProductPricePresent"/> -+ <see userInput="$$createConfigChildProduct1.price$$" stepKey="assertProductPricePresent"/> - </test> - </tests> -diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/EndToEndB2CAdminTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/EndToEndB2CAdminTest.xml -index a00ce52f442..6bbb97c66cd 100644 ---- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/EndToEndB2CAdminTest.xml -+++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/EndToEndB2CAdminTest.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="EndToEndB2CAdminTest"> - <!--Create configurable product--> - <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="visitAdminProductPageConfigurable" after="seeSimpleProductInGrid"/> -diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/EndToEndB2CGuestUserTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/EndToEndB2CGuestUserTest.xml -index 1f7b2bff0a7..47ee09e4b20 100644 ---- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/EndToEndB2CGuestUserTest.xml -+++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/EndToEndB2CGuestUserTest.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="EndToEndB2CGuestUserTest"> - <before> - <createData entity="ApiConfigurableProduct" stepKey="createConfigProduct"> -diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/EndToEndB2CLoggedInUserTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/EndToEndB2CLoggedInUserTest.xml -index 07e2ecf86ca..6f9ad93a56d 100644 ---- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/EndToEndB2CLoggedInUserTest.xml -+++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/EndToEndB2CLoggedInUserTest.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="EndToEndB2CLoggedInUserTest"> - <before> - <createData entity="ApiConfigurableProduct" stepKey="createConfigProduct"> -diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/NewProductsListWidgetConfigurableProductTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/NewProductsListWidgetConfigurableProductTest.xml -new file mode 100644 -index 00000000000..eadc7dadaf7 ---- /dev/null -+++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/NewProductsListWidgetConfigurableProductTest.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="NewProductsListWidgetConfigurableProductTest" extends="NewProductsListWidgetTest"> -+ <annotations> -+ <features value="ConfigurableProduct"/> -+ <stories value="New products list widget"/> -+ <title value="Admin should be able to set Configurable Product as new so that it shows up in the Catalog New Products List Widget"/> -+ <description value="Admin should be able to set Configurable Product as new so that it shows up in the Catalog New Products List Widget"/> -+ <severity value="MAJOR"/> -+ <testCaseId value="MC-120"/> -+ <group value="ConfigurableProduct"/> -+ <group value="WYSIWYGDisabled"/> -+ </annotations> -+ -+ <before> -+ <createData entity="ApiCategory" stepKey="createCategory"/> -+ <createData entity="ApiConfigurableProduct" stepKey="createConfigProduct"> -+ <requiredEntity createDataKey="createCategory"/> -+ </createData> -+ <createData entity="productAttributeWithTwoOptions" stepKey="createConfigProductAttribute"/> -+ <createData entity="productAttributeOption1" stepKey="createConfigProductAttributeOption1"> -+ <requiredEntity createDataKey="createConfigProductAttribute"/> -+ </createData> -+ <createData entity="productAttributeOption2" stepKey="createConfigProductAttributeOption2"> -+ <requiredEntity createDataKey="createConfigProductAttribute"/> -+ </createData> -+ <createData entity="AddToDefaultSet" stepKey="createConfigAddToAttributeSet"> -+ <requiredEntity createDataKey="createConfigProductAttribute"/> -+ </createData> -+ <getData entity="ProductAttributeOptionGetter" index="1" stepKey="getConfigAttributeOption1"> -+ <requiredEntity createDataKey="createConfigProductAttribute"/> -+ </getData> -+ <getData entity="ProductAttributeOptionGetter" index="2" stepKey="getConfigAttributeOption2"> -+ <requiredEntity createDataKey="createConfigProductAttribute"/> -+ </getData> -+ <createData entity="ApiSimpleOne" stepKey="createConfigChildProduct1"> -+ <requiredEntity createDataKey="createConfigProductAttribute"/> -+ <requiredEntity createDataKey="getConfigAttributeOption1"/> -+ </createData> -+ <createData entity="ApiSimpleTwo" stepKey="createConfigChildProduct2"> -+ <requiredEntity createDataKey="createConfigProductAttribute"/> -+ <requiredEntity createDataKey="getConfigAttributeOption2"/> -+ </createData> -+ <createData entity="ConfigurableProductTwoOptions" stepKey="createConfigProductOption"> -+ <requiredEntity createDataKey="createConfigProduct"/> -+ <requiredEntity createDataKey="createConfigProductAttribute"/> -+ <requiredEntity createDataKey="getConfigAttributeOption1"/> -+ <requiredEntity createDataKey="getConfigAttributeOption2"/> -+ </createData> -+ <createData entity="ConfigurableProductAddChild" stepKey="createConfigProductAddChild1"> -+ <requiredEntity createDataKey="createConfigProduct"/> -+ <requiredEntity createDataKey="createConfigChildProduct1"/> -+ </createData> -+ <createData entity="ConfigurableProductAddChild" stepKey="createConfigProductAddChild2"> -+ <requiredEntity createDataKey="createConfigProduct"/> -+ <requiredEntity createDataKey="createConfigChildProduct2"/> -+ </createData> -+ </before> -+ -+ <after> -+ <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> -+ <deleteData createDataKey="createConfigChildProduct1" stepKey="deleteConfigChildProduct1"/> -+ <deleteData createDataKey="createConfigChildProduct2" stepKey="deleteConfigChildProduct2"/> -+ <deleteData createDataKey="createConfigProduct" stepKey="deleteConfigProduct"/> -+ <deleteData createDataKey="createConfigProductAttribute" stepKey="deleteConfigProductAttribute"/> -+ </after> -+ -+ <!-- A Cms page containing the New Products Widget gets created here via extends --> -+ -+ <!-- Modify the Configurable Product that we created in the before block --> -+ <amOnPage url="{{AdminProductEditPage.url($$createConfigProduct.id$$)}}" stepKey="amOnEditPage"/> -+ <waitForPageLoad stepKey="waitForEditPage"/> -+ <fillField selector="{{AdminProductFormSection.setProductAsNewFrom}}" userInput="01/1/2000" stepKey="fillProductNewFrom"/> -+ <fillField selector="{{AdminProductFormSection.setProductAsNewTo}}" userInput="01/1/2099" stepKey="fillProductNewTo"/> -+ <click selector="{{AdminProductFormActionSection.saveButton}}" stepKey="clickSaveProduct"/> -+ -+ <!-- If PageCache is enabled, Cache clearing happens here, via merge --> -+ -+ <!-- Check for product on the CMS page with the New Products widget --> -+ <amOnPage url="{{_newDefaultCmsPage.identifier}}" stepKey="amOnCmsPage"/> -+ <waitForPageLoad stepKey="waitForCmsPage"/> -+ <see selector="{{StorefrontCategoryMainSection.ProductItemInfo}}" userInput="$$createConfigProduct.name$$" stepKey="seeProductName"/> -+ </test> -+</tests> -\ No newline at end of file -diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/NoOptionAvailableToConfigureDisabledProductTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/NoOptionAvailableToConfigureDisabledProductTest.xml -new file mode 100644 -index 00000000000..cb0ea1c5de6 ---- /dev/null -+++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/NoOptionAvailableToConfigureDisabledProductTest.xml -@@ -0,0 +1,127 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> -+ <test name="NoOptionAvailableToConfigureDisabledProductTest"> -+ <annotations> -+ <title value="Disabled variation of configurable product can't be added to shopping cart via admin"/> -+ <description value="Disabled variation of configurable product can't be added to shopping cart via admin"/> -+ <severity value="AVERAGE"/> -+ <testCaseId value="MC-17373"/> -+ <useCaseId value="MAGETWO-72172"/> -+ <group value="ConfigurableProduct"/> -+ </annotations> -+ <before> -+ <actionGroup ref="LoginAsAdmin" stepKey="login"/> -+ <!--Create category--> -+ <comment userInput="Create category" stepKey="commentCreateCategory"/> -+ <createData entity="SimpleSubCategory" stepKey="createCategory"/> -+ <!-- Create the configurable product based on the data in the data folder --> -+ <comment userInput="Create the configurable product based on the data in the data folder" stepKey="createConfigurableProduct"/> -+ <createData entity="ApiConfigurableProduct" stepKey="createConfigProduct"> -+ <requiredEntity createDataKey="createCategory"/> -+ </createData> -+ <!-- Create the configurable product with two options based on the default attribute set --> -+ <comment userInput="Create the configurable product with two options based on the default attribute set" stepKey="configurableProductWithTwoOptions"/> -+ <createData entity="productAttributeWithTwoOptions" stepKey="createConfigProductAttribute"/> -+ <createData entity="productAttributeOption1" stepKey="createConfigProductAttributeOption1"> -+ <requiredEntity createDataKey="createConfigProductAttribute"/> -+ </createData> -+ <createData entity="productAttributeOption2" stepKey="createConfigProductAttributeOption2"> -+ <requiredEntity createDataKey="createConfigProductAttribute"/> -+ </createData> -+ <createData entity="productAttributeOption3" stepKey="createConfigProductAttributeOption3"> -+ <requiredEntity createDataKey="createConfigProductAttribute"/> -+ </createData> -+ <createData entity="AddToDefaultSet" stepKey="createConfigAddToAttributeSet"> -+ <requiredEntity createDataKey="createConfigProductAttribute"/> -+ </createData> -+ <getData entity="ProductAttributeOptionGetter" index="1" stepKey="getConfigAttributeOption1"> -+ <requiredEntity createDataKey="createConfigProductAttribute"/> -+ </getData> -+ <getData entity="ProductAttributeOptionGetter" index="2" stepKey="getConfigAttributeOption2"> -+ <requiredEntity createDataKey="createConfigProductAttribute"/> -+ </getData> -+ <getData entity="ProductAttributeOptionGetter" index="3" stepKey="getConfigAttributeOption3"> -+ <requiredEntity createDataKey="createConfigProductAttribute"/> -+ </getData> -+ <!-- Create the 2 children that will be a part of the configurable product --> -+ <comment userInput="Create the 2 children that will be a part of the configurable product" stepKey="createTwoChildrenProducts"/> -+ <createData entity="ApiSimpleProductWithPrice50" stepKey="createConfigChildProduct1"> -+ <requiredEntity createDataKey="createConfigProductAttribute"/> -+ <requiredEntity createDataKey="getConfigAttributeOption1"/> -+ </createData> -+ <createData entity="ApiSimpleProductWithPrice60" stepKey="createConfigChildProduct2"> -+ <requiredEntity createDataKey="createConfigProductAttribute"/> -+ <requiredEntity createDataKey="getConfigAttributeOption2"/> -+ </createData> -+ <createData entity="ApiSimpleProductWithPrice70" stepKey="createConfigChildProduct3"> -+ <requiredEntity createDataKey="createConfigProductAttribute"/> -+ <requiredEntity createDataKey="getConfigAttributeOption3"/> -+ </createData> -+ <!-- Assign two products to the configurable product --> -+ <comment userInput="Assign two products to the configurable product" stepKey="assignToConfigurableProduct"/> -+ <createData entity="ConfigurableProductTwoOptions" stepKey="createConfigProductOption"> -+ <requiredEntity createDataKey="createConfigProduct"/> -+ <requiredEntity createDataKey="createConfigProductAttribute"/> -+ <requiredEntity createDataKey="getConfigAttributeOption1"/> -+ <requiredEntity createDataKey="getConfigAttributeOption2"/> -+ <requiredEntity createDataKey="getConfigAttributeOption3"/> -+ </createData> -+ <createData entity="ConfigurableProductAddChild" stepKey="createConfigProductAddChild1"> -+ <requiredEntity createDataKey="createConfigProduct"/> -+ <requiredEntity createDataKey="createConfigChildProduct1"/> -+ </createData> -+ <createData entity="ConfigurableProductAddChild" stepKey="createConfigProductAddChild2"> -+ <requiredEntity createDataKey="createConfigProduct"/> -+ <requiredEntity createDataKey="createConfigChildProduct2"/> -+ </createData> -+ <createData entity="ConfigurableProductAddChild" stepKey="createConfigProductAddChild3"> -+ <requiredEntity createDataKey="createConfigProduct"/> -+ <requiredEntity createDataKey="createConfigChildProduct3"/> -+ </createData> -+ <!--Create Customer--> -+ <comment userInput="Create customer" stepKey="commentCreateCustomer"/> -+ <createData entity="Simple_US_Customer_CA" stepKey="createCustomer"/> -+ </before> -+ <after> -+ <!--Delete created data--> -+ <comment userInput="Delete created data" stepKey="deleteData"/> -+ <deleteData createDataKey="createCategory" stepKey="deleteCategory2"/> -+ <deleteData createDataKey="createConfigProduct" stepKey="deleteConfigProduct"/> -+ <deleteData createDataKey="createConfigChildProduct1" stepKey="deleteConfigChildProduct1"/> -+ <deleteData createDataKey="createConfigChildProduct2" stepKey="deleteConfigChildProduct2"/> -+ <deleteData createDataKey="createConfigChildProduct3" stepKey="deleteConfigChildProduct3"/> -+ <deleteData createDataKey="createConfigProductAttribute" stepKey="deleteConfigProductAttribute"/> -+ <deleteData createDataKey="createCustomer" stepKey="deleteCreatedCustomer"/> -+ <actionGroup ref="logout" stepKey="logout"/> -+ </after> -+ <!--Disable child product --> -+ <comment userInput="Disable child product" stepKey="disableChildProduct"/> -+ <amOnPage url="{{AdminProductEditPage.url($$createConfigChildProduct1.id$$)}}" stepKey="goToEditPage"/> -+ <waitForPageLoad stepKey="waitForChildProductPageLoad"/> -+ <click selector="{{AdminProductFormSection.enableProductLabel}}" stepKey="disableProduct"/> -+ <actionGroup ref="saveProductForm" stepKey="saveProductForm"/> -+ <!--Go to created customer page--> -+ <comment userInput="Go to created customer page" stepKey="goToCreatedCustomerPage"/> -+ <actionGroup ref="navigateToNewOrderPageExistingCustomer" stepKey="createNewOrder"> -+ <argument name="customer" value="$$createCustomer$$"/> -+ </actionGroup> -+ <click selector="{{AdminOrderFormItemsSection.addProducts}}" stepKey="clickToAddProduct"/> -+ <waitForPageLoad stepKey="waitForProductsOpened"/> -+ <!--Find created configurable product and click on "Configure" link--> -+ <comment userInput="Find created configurable product and click on Configure link" stepKey="goToConfigurableLink"/> -+ <click selector="{{AdminOrderFormConfigureProductSection.configure($$createConfigProduct.id$$)}}" stepKey="clickOnConfigure"/> -+ <!--Click on attribute drop-down and check no option 1 is available--> -+ <comment userInput="Click on attribute drop-down and check no option 1 is available" stepKey="commentNoOptionIsAvailable"/> -+ <waitForElement selector="{{AdminOrderFormConfigureProductSection.selectOption}}" stepKey="waitForShippingSectionLoaded"/> -+ <click selector="{{AdminOrderFormConfigureProductSection.selectOption}}" stepKey="clickToSelectOption"/> -+ <dontSee userInput="$$createConfigProductAttributeOption1.option[store_labels][1][label]$$" stepKey="dontSeeOption1"/> -+ </test> -+</tests> -diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/ProductsQtyReturnAfterOrderCancelTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/ProductsQtyReturnAfterOrderCancelTest.xml -new file mode 100644 -index 00000000000..c3459aec344 ---- /dev/null -+++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/ProductsQtyReturnAfterOrderCancelTest.xml -@@ -0,0 +1,101 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> -+ <test name="ProductsQtyReturnAfterOrderCancel"> -+ -+ <annotations> -+ <features value="ConfigurableProduct"/> -+ <stories value="Cancel order"/> -+ <title value="Product qunatity return after order cancel"/> -+ <description value="Check Product qunatity return after order cancel"/> -+ <severity value="CRITICAL"/> -+ <testCaseId value="MAGETWO-97228"/> -+ <useCaseId value="MAGETWO-82221"/> -+ <group value="ConfigurableProduct"/> -+ </annotations> -+ -+ <before> -+ <createData entity="ApiCategory" stepKey="createCategory"/> -+ <createData entity="ApiConfigurableProduct" stepKey="createConfigProduct"> -+ <requiredEntity createDataKey="createCategory"/> -+ </createData> -+ <actionGroup ref="LoginAsAdmin" stepKey="login"/> -+ </before> -+ -+ <after> -+ <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> -+ <deleteData createDataKey="createConfigProduct" stepKey="deleteConfigProduct"/> -+ <actionGroup ref="logout" stepKey="amOnLogoutPage"/> -+ </after> -+ -+ <amOnPage url="{{AdminCatalogProductPage.url}}" stepKey="GoToCatalogProductPage1"/> -+ -+ <conditionalClick selector="{{AdminProductGridFilterSection.clearFilters}}" dependentSelector="{{AdminProductGridFilterSection.clearFilters}}" visible="true" stepKey="clickClearFiltersInitial"/> -+ <actionGroup ref="OpenEditProductOnBackendActionGroup" stepKey="openEditProduct"> -+ <argument name="product" value="$$createConfigProduct$$"/> -+ </actionGroup> -+ <fillField selector="{{AdminProductFormSection.productQuantity}}" userInput="100" stepKey="changeProductQuantity"/> -+ <click selector="{{AdminProductFormActionSection.saveButton}}" stepKey="saveChanges"/> -+ <waitForPageLoad stepKey="waitProductGridToBeLoaded"/> -+ -+ <amOnPage url="$$createConfigProduct.sku$$.html" stepKey="navigateToProductPage"/> -+ <waitForPageLoad stepKey="waitForProductPage"/> -+ <fillField selector="{{StorefrontProductInfoMainSection.qty}}" userInput="4" stepKey="fillQuantity"/> -+ -+ <actionGroup ref="addToCartFromStorefrontProductPage" stepKey="addToCartFromStorefrontProductPage"> -+ <argument name="productName" value="$$createConfigProduct.name$$"/> -+ </actionGroup> -+ -+ <actionGroup ref="GoToCheckoutFromMinicartActionGroup" stepKey="goToCheckoutFromMinicart"/> -+ <actionGroup ref="GuestCheckoutFillingShippingSectionActionGroup" stepKey="guestCheckoutFillingShippingSection"> -+ <argument name="customerVar" value="CustomerEntityOne"/> -+ <argument name="customerAddressVar" value="CustomerAddressSimple"/> -+ </actionGroup> -+ -+ <actionGroup ref="CheckoutPlaceOrderActionGroup" stepKey="placeOrder"> -+ <argument name="orderNumberMessage" value="CONST.successGuestCheckoutOrderNumberMessage"/> -+ <argument name="emailYouMessage" value="CONST.successCheckoutEmailYouMessage" /> -+ </actionGroup> -+ -+ <grabTextFrom selector="{{CheckoutSuccessMainSection.orderNumber}}" stepKey="grabOrderNumber"/> -+ -+ <actionGroup ref="filterOrderGridById" stepKey="filterOrderGridById"> -+ <argument name="orderId" value="$grabOrderNumber"/> -+ </actionGroup> -+ -+ <click selector="{{AdminOrdersGridSection.firstRow}}" stepKey="clickOrderRow"/> -+ <waitForPageLoad stepKey="waitForOrderPageToLoad"/> -+ <click selector="{{AdminOrderDetailsMainActionsSection.invoice}}" stepKey="clickInvoiceButton"/> -+ <waitForPageLoad stepKey="waitForNewInvoicePageLoad"/> -+ <fillField selector="{{AdminInvoiceItemsSection.qtyToInvoiceColumn}}" userInput="1" stepKey="ChangeQtyToInvoice"/> -+ <click selector="{{AdminInvoiceItemsSection.updateQty}}" stepKey="updateQunatity"/> -+ <waitForPageLoad stepKey="waitPageToBeLoaded"/> -+ <click selector="{{AdminInvoiceMainActionsSection.submitInvoice}}" stepKey="clickSubmitInvoice"/> -+ <click selector="{{AdminOrderDetailsMainActionsSection.ship}}" stepKey="clickShipAction"/> -+ <waitForPageLoad stepKey="waitOrderDetailToLoad"/> -+ <fillField selector="{{AdminShipmentItemsSection.itemQtyToShip('1')}}" userInput="1" stepKey="changeItemQtyToShip"/> -+ <click selector="{{AdminShipmentMainActionsSection.submitShipment}}" stepKey="clickSubmitShipment"/> -+ <waitForPageLoad stepKey="waitShipmentSectionToLoad"/> -+ <actionGroup ref="cancelPendingOrder" stepKey="cancelPendingOption"> -+ <argument name="orderStatus" value="Complete"/> -+ </actionGroup> -+ -+ <see selector="{{AdminOrderItemsOrderedSection.itemQty('1')}}" userInput="Canceled 3" stepKey="seeCanceledQuantity"/> -+ -+ <amOnPage url="{{AdminCatalogProductPage.url}}" stepKey="GoToCatalogProductPage"/> -+ -+ <actionGroup ref="filterProductGridBySku2" stepKey="filterProductGridBySku"> -+ <argument name="sku" value="$$createConfigProduct.sku$$"/> -+ </actionGroup> -+ <see selector="{{AdminProductGridSection.productGridCell('1', 'Quantity')}}" userInput="99" stepKey="seeProductSkuInGrid"/> -+ <actionGroup ref="AdminClearFiltersActionGroup" stepKey="clearProductFilters"/> -+ <actionGroup ref="AdminOrdersGridClearFiltersActionGroup" stepKey="clearOrderFilters"/> -+ </test> -+</tests> -diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontConfigurableProductCategoryViewChildOnlyTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontConfigurableProductCategoryViewChildOnlyTest.xml -new file mode 100644 -index 00000000000..ac468fc92e4 ---- /dev/null -+++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontConfigurableProductCategoryViewChildOnlyTest.xml -@@ -0,0 +1,129 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> -+ <test name="StorefrontConfigurableProductCategoryViewChildOnlyTest"> -+ <annotations> -+ <features value="ConfigurableProduct"/> -+ <stories value="View configurable product child in storefront"/> -+ <title value="It should be possible to only view the child product of a configurable product"/> -+ <description value="Create configurable product, add to category such that only child variation is visible in category"/> -+ <severity value="CRITICAL"/> -+ <testCaseId value="MC-5832"/> -+ <group value="ConfigurableProduct"/> -+ <skip> -+ <issueId value="MC-17140"/> -+ </skip> -+ </annotations> -+ <before> -+ <!-- Create the category --> -+ <createData entity="ApiCategory" stepKey="createCategory"/> -+ <createData entity="ApiCategory" stepKey="secondCategory"/> -+ -+ <!-- Create an attribute with two options to be used in the first child product --> -+ <createData entity="productAttributeWithTwoOptions" stepKey="createConfigProductAttribute"/> -+ <createData entity="productAttributeOption1" stepKey="createConfigProductAttributeOption1"> -+ <requiredEntity createDataKey="createConfigProductAttribute"/> -+ </createData> -+ <createData entity="productAttributeOption2" stepKey="createConfigProductAttributeOption2"> -+ <requiredEntity createDataKey="createConfigProductAttribute"/> -+ </createData> -+ -+ <!-- Add the attribute we just created to default attribute set --> -+ <createData entity="AddToDefaultSet" stepKey="createConfigAddToAttributeSet"> -+ <requiredEntity createDataKey="createConfigProductAttribute"/> -+ </createData> -+ -+ <!-- Get the first option of the attribute we created --> -+ <getData entity="ProductAttributeOptionGetter" index="1" stepKey="getConfigAttributeOption1"> -+ <requiredEntity createDataKey="createConfigProductAttribute"/> -+ </getData> -+ -+ <!-- Get the second option of the attribute we created --> -+ <getData entity="ProductAttributeOptionGetter" index="2" stepKey="getConfigAttributeOption2"> -+ <requiredEntity createDataKey="createConfigProductAttribute"/> -+ </getData> -+ -+ <!-- Create the configurable product and add it to the category --> -+ <createData entity="ApiConfigurableProduct" stepKey="createConfigProduct"> -+ <requiredEntity createDataKey="createCategory"/> -+ </createData> -+ -+ <!-- Create a simple product and give it the attribute with the first option --> -+ <createData entity="ApiSimpleOne" stepKey="createConfigChildProduct1"> -+ <requiredEntity createDataKey="createConfigProductAttribute"/> -+ <requiredEntity createDataKey="getConfigAttributeOption1"/> -+ </createData> -+ -+ <!-- Create a simple product and give it the attribute with the second option --> -+ <createData entity="ApiSimpleTwo" stepKey="createConfigChildProduct2"> -+ <requiredEntity createDataKey="createConfigProductAttribute"/> -+ <requiredEntity createDataKey="getConfigAttributeOption2"/> -+ </createData> -+ -+ <createData entity="ConfigurableProductTwoOptions" stepKey="createConfigProductOption"> -+ <requiredEntity createDataKey="createConfigProduct"/> -+ <requiredEntity createDataKey="createConfigProductAttribute"/> -+ <requiredEntity createDataKey="getConfigAttributeOption1"/> -+ <requiredEntity createDataKey="getConfigAttributeOption2"/> -+ </createData> -+ -+ <!-- Add the first simple product to the configurable product --> -+ <createData entity="ConfigurableProductAddChild" stepKey="createConfigProductAddChild1"> -+ <requiredEntity createDataKey="createConfigProduct"/> -+ <requiredEntity createDataKey="createConfigChildProduct1"/> -+ </createData> -+ -+ <!-- Add the second simple product to the configurable product --> -+ <createData entity="ConfigurableProductAddChild" stepKey="createConfigProductAddChild2"> -+ <requiredEntity createDataKey="createConfigProduct"/> -+ <requiredEntity createDataKey="createConfigChildProduct2"/> -+ </createData> -+ </before> -+ -+ <after> -+ <amOnPage url="admin/admin/auth/logout/" stepKey="logout"/> -+ <deleteData createDataKey="createConfigProduct" stepKey="deleteConfigProduct"/> -+ <deleteData createDataKey="createConfigChildProduct1" stepKey="deleteConfigChildProduct1"/> -+ <deleteData createDataKey="createConfigChildProduct2" stepKey="deleteConfigChildProduct2"/> -+ <deleteData createDataKey="createConfigProductAttribute" stepKey="deleteConfigProductAttribute"/> -+ <deleteData createDataKey="createCategory" stepKey="deleteApiCategory"/> -+ <deleteData createDataKey="secondCategory" stepKey="deleteSecondCategory"/> -+ </after> -+ -+ <actionGroup ref="LoginAsAdmin" stepKey="login"/> -+ -+ <!-- Go to the product page for the first product --> -+ <amOnPage stepKey="goToProductGrid" url="{{ProductCatalogPage.url}}"/> -+ <waitForPageLoad stepKey="waitForProductGridLoad"/> -+ <actionGroup stepKey="searchForSimpleProduct" ref="filterProductGridBySku2"> -+ <argument name="sku" value="$$createConfigChildProduct1.sku$$"/> -+ </actionGroup> -+ <actionGroup stepKey="openProductEditPage" ref="openProducForEditByClickingRowXColumnYInProductGrid"/> -+ <!-- Edit the visibility the first simple product --> -+ <selectOption selector="{{AdminProductFormSection.visibility}}" userInput="Catalog, Search" stepKey="selectVisibilityCatalogSearch"/> -+ <!--Add to category--> -+ <searchAndMultiSelectOption selector="{{AdminProductFormSection.categoriesDropdown}}" parameterArray="[$$secondCategory.name$$]" stepKey="addProductToCategory"/> -+ <click selector="{{AdminProductFormActionSection.saveButton}}" stepKey="saveProduct"/> -+ <seeElement selector="{{AdminProductMessagesSection.successMessage}}" stepKey="assertSaveMessageSuccess"/> -+ -+ <!-- Go to storefront to view child product --> -+ <amOnPage stepKey="goToStoreFront" url="{{StorefrontHomePage.url}}"/> -+ <waitForPageLoad stepKey="waitForStorefront"/> -+ <click selector="{{StorefrontHeaderSection.NavigationCategoryByName($$secondCategory.name$$)}}" stepKey="goToCategoryStorefront"/> -+ <waitForPageLoad stepKey="waitForStorefrontCategory"/> -+ <seeElement selector="{{StorefrontCategoryProductSection.ProductTitleByName($$createConfigChildProduct1.name$$)}}" stepKey="seeChildProductInCategory"/> -+ <dontSeeElement selector="{{StorefrontCategoryProductSection.ProductTitleByName($$createConfigChildProduct2.name$$)}}" stepKey="dontSeeOtherChildProduct"/> -+ <dontSeeElement selector="{{StorefrontCategoryProductSection.ProductTitleByName($$createConfigProduct.name$$)}}" stepKey="dontSeeParentProduct"/> -+ <click selector="{{StorefrontCategoryProductSection.ProductTitleByName($$createConfigChildProduct1.name$$)}}" stepKey="clickProductName"/> -+ <waitForPageLoad stepKey="waitForProductPageLoad"/> -+ <seeInCurrentUrl url="$$createConfigChildProduct1.custom_attributes[url_key]$$" stepKey="seeProductPageIsAccessible"/> -+ <seeElement selector="{{StorefrontProductInfoMainSection.productName($$createConfigChildProduct1.name$$)}}" stepKey="seeProductNameOnProductPage"/> -+ </test> -+</tests> -diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontConfigurableProductChildSearchTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontConfigurableProductChildSearchTest.xml -index c0e7c886d0c..1075f79aef1 100644 ---- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontConfigurableProductChildSearchTest.xml -+++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontConfigurableProductChildSearchTest.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="StorefrontConfigurableProductChildSearchTest"> - <annotations> - <features value="ConfigurableProduct"/> -@@ -144,18 +144,15 @@ - <!-- Quick search the storefront for the first attribute option --> - <amOnPage stepKey="goToStoreFront" url="{{StorefrontHomePage.url}}"/> - <waitForPageLoad stepKey="waitForStorefront"/> -- <fillField stepKey="searchStorefront1" selector="{{StorefrontQuickSearchSection.searchPhrase}}" userInput="$$createConfigProductAttributeSelectOption1.option[store_labels][0][label]$$"/> -- <click stepKey="clickSearch1" selector="{{StorefrontQuickSearchSection.searchButton}}"/> -+ <submitForm selector="#search_mini_form" parameterArray="['q' => $$createConfigProductAttributeSelectOption1.option[store_labels][0][label]$$]" stepKey="searchStorefront1" /> - <seeElement stepKey="seeProduct1" selector="{{StorefrontCategoryProductSection.ProductTitleByName('$$createConfigProduct.name$$')}}"/> - - <!-- Quick search the storefront for the second attribute option --> -- <fillField stepKey="searchStorefront2" selector="{{StorefrontQuickSearchSection.searchPhrase}}" userInput="$$createConfigProductAttributeOption1Multiselect.option[store_labels][0][label]$$"/> -- <click stepKey="clickSearch2" selector="{{StorefrontQuickSearchSection.searchButton}}"/> -+ <submitForm selector="#search_mini_form" parameterArray="['q' => $$createConfigProductAttributeOption1Multiselect.option[store_labels][0][label]$$]" stepKey="searchStorefront2" /> - <seeElement stepKey="seeProduct2" selector="{{StorefrontCategoryProductSection.ProductTitleByName('$$createConfigProduct.name$$')}}"/> - - <!-- Quick search the storefront for the first product description --> -- <fillField stepKey="searchStorefront3" selector="{{StorefrontQuickSearchSection.searchPhrase}}" userInput="'$$createConfigChildProduct1.custom_attributes[short_description]$$'"/> -- <click stepKey="clickSearch3" selector="{{StorefrontQuickSearchSection.searchButton}}"/> -+ <submitForm selector="#search_mini_form" parameterArray="['q' => $$createConfigChildProduct1.custom_attributes[short_description]$$]" stepKey="searchStorefront3" /> - <seeElement stepKey="seeProduct3" selector="{{StorefrontCategoryProductSection.ProductTitleByName('$$createConfigProduct.name$$')}}"/> - </test> - </tests> -\ No newline at end of file -diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontConfigurableProductDetailsTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontConfigurableProductDetailsTest.xml -index 3b0f5752ebf..836bc2cdca9 100644 ---- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontConfigurableProductDetailsTest.xml -+++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontConfigurableProductDetailsTest.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="StorefrontConfigurableProductBasicInfoTest"> - <annotations> - <features value="ConfigurableProduct"/> -diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontConfigurableProductViewTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontConfigurableProductViewTest.xml -index b8e894ccf36..cc8291a83eb 100644 ---- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontConfigurableProductViewTest.xml -+++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontConfigurableProductViewTest.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="StorefrontConfigurableProductGridViewTest"> - <annotations> - <features value="ConfigurableProduct"/> -diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontConfigurableProductWithFileCustomOptionTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontConfigurableProductWithFileCustomOptionTest.xml -index 602e04b138e..d890d598581 100644 ---- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontConfigurableProductWithFileCustomOptionTest.xml -+++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontConfigurableProductWithFileCustomOptionTest.xml -@@ -7,7 +7,7 @@ - --> - - <tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -- xsi:noNamespaceSchemaLocation="../../../../../../vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Test/etc/testSchema.xsd"> -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> - <test name="StorefrontConfigurableProductWithFileCustomOptionTest"> - <annotations> - <features value="ConfigurableProduct"/> -@@ -17,7 +17,6 @@ - <severity value="CRITICAL"/> - <testCaseId value="MAGETWO-93059"/> - <group value="ConfigurableProduct"/> -- <group value="skip"/><!-- MAGETWO-94170 --> - </annotations> - - <before> -@@ -41,7 +40,7 @@ - <click selector="{{AdminProductFormActionSection.saveButton}}" stepKey="saveProduct"/> - - <!--Go to storefront--> -- <amOnPage url="" stepKey="goToHomePage"/> -+ <amOnPage url="{{StorefrontHomePage.url}}" stepKey="goToHomePage"/> - <waitForPageLoad stepKey="waitForHomePageLoad"/> - <click selector="{{StorefrontNavigationSection.topCategory($$createCategory.name$$)}}" stepKey="goToCategoryStorefront"/> - <waitForPageLoad stepKey="waitForCategoryPageLoad"/> -diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontSortingByPriceForConfigurableWithCatalogRuleAppliedTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontSortingByPriceForConfigurableWithCatalogRuleAppliedTest.xml -new file mode 100644 -index 00000000000..57c45ee1e89 ---- /dev/null -+++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontSortingByPriceForConfigurableWithCatalogRuleAppliedTest.xml -@@ -0,0 +1,167 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> -+ <test name="StorefrontSortingByPriceForConfigurableProductWithCatalogRuleAppliedTest"> -+ <annotations> -+ <features value="ConfigurableProduct"/> -+ <stories value="View soting by price in storefront"/> -+ <title value="Sorting by price for Configurable with Catalog Rule applied"/> -+ <description value="Sort by price should be correct if the apply Catalog Rule to child product of configurable product"/> -+ <severity value="CRITICAL"/> -+ <testCaseId value="MAGETWO-69988"/> -+ <skip> -+ <issueId value="MC-5777"/> -+ </skip> -+ <group value="сonfigurable_product"/> -+ </annotations> -+ <before> -+ <createData entity="ApiCategory" stepKey="createCategory"/> -+ <createData entity="SimpleProduct" stepKey="createSimpleProduct"> -+ <requiredEntity createDataKey="createCategory"/> -+ <field key="price">5.00</field> -+ </createData> -+ <createData entity="SimpleProduct" stepKey="createSimpleProduct2"> -+ <requiredEntity createDataKey="createCategory"/> -+ <field key="price">10.00</field> -+ </createData> -+ <createData entity="ApiConfigurableProduct" stepKey="createConfigProduct"> -+ <requiredEntity createDataKey="createCategory"/> -+ </createData> -+ <createData entity="productAttributeWithTwoOptions" stepKey="createConfigProductAttribute"/> -+ <createData entity="productAttributeOption1" stepKey="createConfigProductAttributeOption1"> -+ <requiredEntity createDataKey="createConfigProductAttribute"/> -+ </createData> -+ <createData entity="productAttributeOption2" stepKey="createConfigProductAttributeOption2"> -+ <requiredEntity createDataKey="createConfigProductAttribute"/> -+ </createData> -+ <createData entity="productAttributeOption3" stepKey="createConfigProductAttributeOption3"> -+ <requiredEntity createDataKey="createConfigProductAttribute"/> -+ </createData> -+ <createData entity="AddToDefaultSet" stepKey="createConfigAddToAttributeSet"> -+ <requiredEntity createDataKey="createConfigProductAttribute"/> -+ </createData> -+ <getData entity="ProductAttributeOptionGetter" index="1" stepKey="getConfigAttributeOption1"> -+ <requiredEntity createDataKey="createConfigProductAttribute"/> -+ </getData> -+ <getData entity="ProductAttributeOptionGetter" index="2" stepKey="getConfigAttributeOption2"> -+ <requiredEntity createDataKey="createConfigProductAttribute"/> -+ </getData> -+ <getData entity="ProductAttributeOptionGetter" index="3" stepKey="getConfigAttributeOption3"> -+ <requiredEntity createDataKey="createConfigProductAttribute"/> -+ </getData> -+ <createData entity="ApiSimpleOne" stepKey="createConfigChildProduct1"> -+ <requiredEntity createDataKey="createConfigProductAttribute"/> -+ <requiredEntity createDataKey="getConfigAttributeOption1"/> -+ <field key="price">15.00</field> -+ </createData> -+ <createData entity="ApiSimpleTwo" stepKey="createConfigChildProduct2"> -+ <requiredEntity createDataKey="createConfigProductAttribute"/> -+ <requiredEntity createDataKey="getConfigAttributeOption2"/> -+ <field key="price">20.00</field> -+ </createData> -+ <createData entity="ApiSimpleTwo" stepKey="createConfigChildProduct3"> -+ <requiredEntity createDataKey="createConfigProductAttribute"/> -+ <requiredEntity createDataKey="getConfigAttributeOption3"/> -+ <field key="price">25.00</field> -+ </createData> -+ <createData entity="ConfigurableProductThreeOptions" stepKey="createConfigProductOption"> -+ <requiredEntity createDataKey="createConfigProduct"/> -+ <requiredEntity createDataKey="createConfigProductAttribute"/> -+ <requiredEntity createDataKey="getConfigAttributeOption1"/> -+ <requiredEntity createDataKey="getConfigAttributeOption2"/> -+ <requiredEntity createDataKey="getConfigAttributeOption3"/> -+ </createData> -+ <createData entity="ConfigurableProductAddChild" stepKey="createConfigProductAddChild1"> -+ <requiredEntity createDataKey="createConfigProduct"/> -+ <requiredEntity createDataKey="createConfigChildProduct1"/> -+ </createData> -+ <createData entity="ConfigurableProductAddChild" stepKey="createConfigProductAddChild2"> -+ <requiredEntity createDataKey="createConfigProduct"/> -+ <requiredEntity createDataKey="createConfigChildProduct2"/> -+ </createData> -+ <createData entity="ConfigurableProductAddChild" stepKey="createConfigProductAddChild3"> -+ <requiredEntity createDataKey="createConfigProduct"/> -+ <requiredEntity createDataKey="createConfigChildProduct3"/> -+ </createData> -+ <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> -+ <!--SKU Product Attribute is enabled for Promo Rule Conditions--> -+ <actionGroup ref="navigateToEditProductAttribute" stepKey="navigateToSkuProductAttribute"> -+ <argument name="ProductAttribute" value="sku"/> -+ </actionGroup> -+ <actionGroup ref="changeUseForPromoRuleConditionsProductAttribute" stepKey="changeUseForPromoRuleConditionsProductAttributeToYes"> -+ <argument name="option" value="Yes"/> -+ </actionGroup> -+ <magentoCLI command="indexer:reindex" stepKey="reindex1"/> -+ <magentoCLI command="cache:flush" stepKey="flushCache1"/> -+ </before> -+ -+ <after> -+ <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> -+ <deleteData createDataKey="createSimpleProduct" stepKey="deleteSimpleProduct"/> -+ <deleteData createDataKey="createSimpleProduct2" stepKey="deleteSimpleProduct2"/> -+ <deleteData createDataKey="createConfigChildProduct1" stepKey="deleteConfigChildProduct1"/> -+ <deleteData createDataKey="createConfigChildProduct2" stepKey="deleteConfigChildProduct2"/> -+ <deleteData createDataKey="createConfigChildProduct3" stepKey="deleteConfigChildProduct3"/> -+ <deleteData createDataKey="createConfigProduct" stepKey="deleteConfigProduct"/> -+ <deleteData createDataKey="createConfigProductAttribute" stepKey="deleteConfigProductAttribute"/> -+ -+ <!--SKU Product Attribute is disable for Promo Rule Conditions--> -+ <actionGroup ref="navigateToEditProductAttribute" stepKey="navigateToSkuProductAttribute"> -+ <argument name="ProductAttribute" value="sku"/> -+ </actionGroup> -+ <actionGroup ref="changeUseForPromoRuleConditionsProductAttribute" stepKey="changeUseForPromoRuleConditionsProductAttributeToNo"> -+ <argument name="option" value="No"/> -+ </actionGroup> -+ <actionGroup ref="logout" stepKey="logoutFromAdmin"/> -+ </after> -+ -+ <!--Open category with products and Sort by price desc--> -+ <actionGroup ref="GoToStorefrontCategoryPageByParameters" stepKey="goToStorefrontCategoryPage"> -+ <argument name="category" value="$$createCategory.custom_attributes[url_key]$$"/> -+ <argument name="mode" value="grid"/> -+ <argument name="numOfProductsPerPage" value="25"/> -+ <argument name="sortBy" value="price"/> -+ <argument name="sort" value="desc"/> -+ </actionGroup> -+ <see selector="{{StorefrontCategoryMainSection.lineProductName('1')}}" userInput="$$createConfigProduct.name$$" stepKey="seeConfigurableProduct"/> -+ <see selector="{{StorefrontCategoryMainSection.lineProductName('2')}}" userInput="$$createSimpleProduct2.name$$" stepKey="seeSimpleProductTwo"/> -+ <see selector="{{StorefrontCategoryMainSection.lineProductName('3')}}" userInput="$$createSimpleProduct.name$$" stepKey="seeSimpleProduct"/> -+ -+ <!--Create and apply catalog price rule--> -+ <actionGroup ref="newCatalogPriceRuleByUIWithConditionIsSKU" stepKey="createCatalogPriceRule"> -+ <argument name="catalogRule" value="CatalogRuleByPercentWith96Amount" /> -+ <argument name="productSku" value="$$createConfigChildProduct3.sku$$" /> -+ </actionGroup> -+ <actionGroup ref="selectNotLoggedInCustomerGroup" stepKey="selectNotLoggedInCustomerGroup"/> -+ <click selector="{{AdminNewCatalogPriceRule.saveAndApply}}" stepKey="clickSaveAndApplyRules"/> -+ -+ <magentoCLI command="indexer:reindex" stepKey="reindex"/> -+ <magentoCLI command="cache:flush" stepKey="flushCache"/> -+ -+ <!--Reopen category with products and Sort by price desc--> -+ <actionGroup ref="GoToStorefrontCategoryPageByParameters" stepKey="goToStorefrontCategoryPage2"> -+ <argument name="category" value="$$createCategory.custom_attributes[url_key]$$"/> -+ <argument name="mode" value="grid"/> -+ <argument name="numOfProductsPerPage" value="9"/> -+ <argument name="sortBy" value="price"/> -+ <argument name="sort" value="desc"/> -+ </actionGroup> -+ <see selector="{{StorefrontCategoryMainSection.lineProductName('1')}}" userInput="$$createConfigProduct.name$$" stepKey="seeConfigurableProduct2"/> -+ <see selector="{{StorefrontCategoryMainSection.lineProductName('2')}}" userInput="$$createSimpleProduct2.name$$" stepKey="seeSimpleProductTwo2"/> -+ <see selector="{{StorefrontCategoryMainSection.lineProductName('3')}}" userInput="$$createSimpleProduct.name$$" stepKey="seeSimpleProduct2"/> -+ -+ <!-- Delete the rule --> -+ <amOnPage url="{{CatalogRulePage.url}}" stepKey="goToPriceRulePage"/> -+ <actionGroup ref="deleteEntitySecondaryGrid" stepKey="deletePriceRule"> -+ <argument name="name" value="{{CatalogRuleByPercentWith96Amount.name}}"/> -+ <argument name="searchInput" value="{{AdminSecondaryGridSection.catalogRuleIdentifierSearch}}"/> -+ </actionGroup> -+ </test> -+</tests> -diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontVerifyConfigurableProductLayeredNavigationTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontVerifyConfigurableProductLayeredNavigationTest.xml -new file mode 100644 -index 00000000000..bb69122dc0b ---- /dev/null -+++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontVerifyConfigurableProductLayeredNavigationTest.xml -@@ -0,0 +1,149 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> -+ <test name="StorefrontVerifyConfigurableProductLayeredNavigationTest"> -+ <annotations> -+ <stories value="Create configurable product"/> -+ <title value="Out of stock configurable attribute option doesn't show in Layered Navigation"/> -+ <description value=" Login as admin and verify out of stock configurable attribute option doesn't show in Layered Navigation"/> -+ <testCaseId value="MC-13734"/> -+ <severity value="CRITICAL"/> -+ <group value="ConfigurableProduct"/> -+ <group value="mtf_migrated"/> -+ </annotations> -+ -+ <before> -+ <!-- Login as Admin --> -+ <actionGroup ref="LoginAsAdmin" stepKey="loginToAdminPanel"/> -+ -+ <!-- Create Default Category --> -+ <createData entity="_defaultCategory" stepKey="createCategory"/> -+ -+ <!-- Create an attribute with three options --> -+ <createData entity="productAttributeWithTwoOptions" stepKey="createConfigProductAttribute"/> -+ <createData entity="productAttributeOption1" stepKey="createConfigProductAttributeOption1"> -+ <requiredEntity createDataKey="createConfigProductAttribute"/> -+ </createData> -+ <createData entity="productAttributeOption2" stepKey="createConfigProductAttributeOption2"> -+ <requiredEntity createDataKey="createConfigProductAttribute"/> -+ </createData> -+ <createData entity="productAttributeOption3" stepKey="createConfigProductAttributeOption3"> -+ <requiredEntity createDataKey="createConfigProductAttribute"/> -+ </createData> -+ -+ <!-- Add the attribute just created to default attribute set --> -+ <createData entity="AddToDefaultSet" stepKey="createConfigAddToAttributeSet"> -+ <requiredEntity createDataKey="createConfigProductAttribute"/> -+ </createData> -+ -+ <!-- Get the first option of the attribute created --> -+ <getData entity="ProductAttributeOptionGetter" index="1" stepKey="getConfigAttributeOption1"> -+ <requiredEntity createDataKey="createConfigProductAttribute"/> -+ </getData> -+ -+ <!-- Get the second option of the attribute created --> -+ <getData entity="ProductAttributeOptionGetter" index="2" stepKey="getConfigAttributeOption2"> -+ <requiredEntity createDataKey="createConfigProductAttribute"/> -+ </getData> -+ -+ <!-- Get the third option of the attribute created --> -+ <getData entity="ProductAttributeOptionGetter" index="3" stepKey="getConfigAttributeOption3"> -+ <requiredEntity createDataKey="createConfigProductAttribute"/> -+ </getData> -+ -+ <!-- Create Configurable product --> -+ <createData entity="BaseConfigurableProduct" stepKey="createConfigProduct"> -+ <requiredEntity createDataKey="createCategory"/> -+ </createData> -+ -+ <!-- Create a simple product and give it the attribute with the first option --> -+ <createData entity="ApiSimpleOne" stepKey="createConfigChildProduct1"> -+ <requiredEntity createDataKey="createConfigProductAttribute"/> -+ <requiredEntity createDataKey="getConfigAttributeOption1"/> -+ </createData> -+ -+ <!--Create a simple product and give it the attribute with the second option --> -+ <createData entity="ApiSimpleTwo" stepKey="createConfigChildProduct2"> -+ <requiredEntity createDataKey="createConfigProductAttribute"/> -+ <requiredEntity createDataKey="getConfigAttributeOption2"/> -+ </createData> -+ -+ <!--Create a simple product and give it the attribute with the Third option --> -+ <createData entity="ApiSimpleTwo" stepKey="createConfigChildProduct3"> -+ <requiredEntity createDataKey="createConfigProductAttribute"/> -+ <requiredEntity createDataKey="getConfigAttributeOption3"/> -+ </createData> -+ -+ <!-- Create the configurable product --> -+ <createData entity="ConfigurableProductThreeOptions" stepKey="createConfigProductOption"> -+ <requiredEntity createDataKey="createConfigProduct"/> -+ <requiredEntity createDataKey="createConfigProductAttribute"/> -+ <requiredEntity createDataKey="getConfigAttributeOption1"/> -+ <requiredEntity createDataKey="getConfigAttributeOption2"/> -+ <requiredEntity createDataKey="getConfigAttributeOption3"/> -+ </createData> -+ -+ <!-- Add the first simple product to the configurable product --> -+ <createData entity="ConfigurableProductAddChild" stepKey="createConfigProductAddChild1"> -+ <requiredEntity createDataKey="createConfigProduct"/> -+ <requiredEntity createDataKey="createConfigChildProduct1"/> -+ </createData> -+ -+ <!-- Add the second simple product to the configurable product --> -+ <createData entity="ConfigurableProductAddChild" stepKey="createConfigProductAddChild2"> -+ <requiredEntity createDataKey="createConfigProduct"/> -+ <requiredEntity createDataKey="createConfigChildProduct2"/> -+ </createData> -+ -+ <!-- Add the third simple product to the configurable product --> -+ <createData entity="ConfigurableProductAddChild" stepKey="createConfigProductAddChild3"> -+ <requiredEntity createDataKey="createConfigProduct"/> -+ <requiredEntity createDataKey="createConfigChildProduct3"/> -+ </createData> -+ </before> -+ <after> -+ <!-- Delete Created Data --> -+ <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> -+ <deleteData createDataKey="createConfigProduct" stepKey="deleteConfigProduct"/> -+ <deleteData createDataKey="createConfigChildProduct1" stepKey="deleteConfigChildProduct1"/> -+ <deleteData createDataKey="createConfigChildProduct2" stepKey="deleteConfigChildProduct2"/> -+ <deleteData createDataKey="createConfigChildProduct3" stepKey="deleteConfigChildProduct3"/> -+ <deleteData createDataKey="createConfigProductAttribute" stepKey="deleteAttribute"/> -+ <actionGroup ref="logout" stepKey="logout"/> -+ </after> -+ -+ <!-- Open Product Index Page and Filter First Child product --> -+ <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="navigateToProductIndex"/> -+ <waitForPageLoad stepKey="waitForProductIndexPageToLoad"/> -+ <actionGroup ref="filterProductGridBySku" stepKey="filterProduct"> -+ <argument name="product" value="ApiSimpleOne"/> -+ </actionGroup> -+ -+ <!-- Change the First Child Product stock status as 'Out Of Stock'--> -+ <click selector="{{AdminProductGridFilterSection.nthRow('1')}}" stepKey="selectFirstRow"/> -+ <waitForPageLoad stepKey="waitForProductPageToLoad"/> -+ <scrollTo selector="{{AdminProductFormSection.productQuantity}}" stepKey="scrollToProductQuantity"/> -+ <selectOption selector="{{AdminProductFormSection.productStockStatus}}" userInput="Out of Stock" stepKey="disableProduct"/> -+ <click selector="{{AdminProductFormActionSection.saveButton}}" stepKey="clickOnSaveButton"/> -+ <waitForPageLoad stepKey="waitForPageLoad1"/> -+ <see selector="{{AdminCategoryMessagesSection.SuccessMessage}}" userInput="You saved the product." stepKey="messageYouSavedTheProductIsShown"/> -+ -+ <!--Open Category in Store Front and select product attribute option from sidebar --> -+ <actionGroup ref="SelectStorefrontSideBarAttributeOption" stepKey="selectStorefrontProductAttributeOption"> -+ <argument name="categoryName" value="$$createCategory.name$$"/> -+ <argument name="attributeDefaultLabel" value="$$createConfigProductAttribute.default_value$$"/> -+ </actionGroup> -+ -+ <!--Assert Out Of Stock product is not visible in Storefront Page --> -+ <dontSee selector="{{StorefrontCategorySidebarSection.filterOption}}" userInput="$$getConfigAttributeOption1.label$$" stepKey="dontSeeOption1"/> -+ <see selector="{{StorefrontCategorySidebarSection.filterOption}}" userInput="$$getConfigAttributeOption2.label$$" stepKey="seeOption2"/> -+ <see selector="{{StorefrontCategorySidebarSection.filterOption}}" userInput="$$getConfigAttributeOption3.label$$" stepKey="seeOption3"/> -+ </test> -+</tests> -diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontVisibilityOfDuplicateProductTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontVisibilityOfDuplicateProductTest.xml -new file mode 100644 -index 00000000000..36c8301b23d ---- /dev/null -+++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontVisibilityOfDuplicateProductTest.xml -@@ -0,0 +1,207 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> -+ <test name="StorefrontVisibilityOfDuplicateProductTest"> -+ <annotations> -+ <features value="ConfigurableProduct"/> -+ <title value="Visibility of duplicate product on the Storefront"/> -+ <description value="Check visibility of duplicate product on the Storefront"/> -+ <severity value="AVERAGE"/> -+ <testCaseId value="MC-17226"/> -+ <useCaseId value="MAGETWO-64923"/> -+ <group value="ConfigurableProduct"/> -+ </annotations> -+ <before> -+ <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> -+ <!--Create configurable product--> -+ <comment userInput="Create configurable product" stepKey="commentCreateConfigProduct"/> -+ <createData entity="SimpleSubCategory" stepKey="createCategory"/> -+ <createData entity="SimpleProduct" stepKey="createConfigProduct"> -+ <requiredEntity createDataKey="createCategory"/> -+ </createData> -+ </before> -+ <after> -+ <!--Delete created data--> -+ <comment userInput="Delete created data" stepKey="commentDeleteCreatedData"/> -+ <actionGroup ref="deleteAllDuplicateProductUsingProductGrid" stepKey="deleteDuplicatedProduct"> -+ <argument name="product" value="$$createConfigProduct$$"/> -+ </actionGroup> -+ <actionGroup ref="resetProductGridToDefaultView" stepKey="resetGridToDefaultKeywordSearch"/> -+ <!--Delete product attributes--> -+ <comment userInput="Delete product attributes" stepKey="deleteCommentAttributes"/> -+ <actionGroup ref="deleteProductAttributeByLabel" stepKey="deleteProductAttribute"> -+ <argument name="ProductAttribute" value="colorProductAttribute"/> -+ </actionGroup> -+ <click selector="{{AdminProductAttributeGridSection.ResetFilter}}" stepKey="resetFiltersOnGridFirst"/> -+ <actionGroup ref="deleteProductAttributeByLabel" stepKey="deleteProductSecondAttribute"> -+ <argument name="ProductAttribute" value="productAttributeColor"/> -+ </actionGroup> -+ <click selector="{{AdminProductAttributeGridSection.ResetFilter}}" stepKey="resetFiltersOnGridSecond"/> -+ <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> -+ <actionGroup ref="logout" stepKey="logout"/> -+ </after> -+ <!--Create attribute and options for product--> -+ <comment userInput="Create attribute and options for product" stepKey="commentCreateAttributesAndOptions"/> -+ <amOnPage url="{{AdminProductEditPage.url($$createConfigProduct.id$$)}}" stepKey="navigateToConfigProductPage"/> -+ <waitForPageLoad stepKey="waitForProductPageLoad"/> -+ <actionGroup ref="addProductImage" stepKey="addImageForProduct1"> -+ <argument name="image" value="MagentoLogo"/> -+ </actionGroup> -+ <actionGroup ref="saveProductForm" stepKey="saveProductForm"/> -+ <actionGroup ref="AdminCreateAttributeFromProductPageWithScope" stepKey="createAttributeForProduct"> -+ <argument name="attributeName" value="{{colorProductAttribute.default_label}}"/> -+ <argument name="attributeType" value="{{colorProductAttribute.input_type}}"/> -+ <argument name="scope" value="Global"/> -+ </actionGroup> -+ <reloadPage stepKey="reloadPage"/> -+ <waitForPageLoad stepKey="waitForProductPageReload"/> -+ <click selector="{{AdminProductFormConfigurationsSection.createConfigurations}}" stepKey="clickOnCreateConfigurations"/> -+ <waitForPageLoad stepKey="waitForFilters"/> -+ <actionGroup ref="createOptionsForAttribute" stepKey="createOptions"> -+ <argument name="attributeName" value="{{colorProductAttribute.default_label}}"/> -+ <argument name="firstOptionName" value="{{colorConfigurableProductAttribute1.name}}"/> -+ <argument name="secondOptionName" value="{{colorConfigurableProductAttribute2.name}}"/> -+ </actionGroup> -+ <click selector="{{AdminCreateProductConfigurationsPanel.next}}" stepKey="clickOnFirstNextButton"/> -+ <waitForPageLoad stepKey="waitForBulkImagesPriceQuantityPageLoad"/> -+ <!--Add images to configurable product attribute options--> -+ <comment userInput="Add images to configurable product attribute options" stepKey="commentAddImages"/> -+ <actionGroup ref="addUniqueImageToConfigurableProductOption" stepKey="addImageToConfigurableProductOptionOne"> -+ <argument name="image" value="ImageUpload"/> -+ <argument name="frontend_label" value="{{colorProductAttribute.default_label}}"/> -+ <argument name="label" value="{{colorConfigurableProductAttribute1.name}}"/> -+ </actionGroup> -+ <actionGroup ref="addUniqueImageToConfigurableProductOption" stepKey="addImageToConfigurableProductOptionTwo"> -+ <argument name="image" value="ImageUpload_1"/> -+ <argument name="frontend_label" value="{{colorProductAttribute.default_label}}"/> -+ <argument name="label" value="{{colorConfigurableProductAttribute2.name}}"/> -+ </actionGroup> -+ <!--Add price to product attribute options--> -+ <comment userInput="Add price to product attribute options" stepKey="commentAddPrice"/> -+ <actionGroup ref="addUniquePriceToConfigurableProductOption" stepKey="addPriceToConfigurableProductOptionFirst"> -+ <argument name="frontend_label" value="{{colorProductAttribute.default_label}}"/> -+ <argument name="label" value="{{colorConfigurableProductAttribute1.name}}"/> -+ <argument name="price" value="{{virtualProductWithRequiredFields.price}}"/> -+ </actionGroup> -+ <actionGroup ref="addUniquePriceToConfigurableProductOption" stepKey="addPriceToConfigurableProductOptionSecond"> -+ <argument name="frontend_label" value="{{colorProductAttribute.default_label}}"/> -+ <argument name="label" value="{{colorConfigurableProductAttribute2.name}}"/> -+ <argument name="price" value="{{virtualProductWithRequiredFields.price}}"/> -+ </actionGroup> -+ <!--Add quantity to product attribute options--> -+ <comment userInput="Add quantity to product attribute options" stepKey="commentAddQuantity"/> -+ <actionGroup ref="addUniqueQuantityToConfigurableProductOption" stepKey="addUniqueQtyForFirstOption"> -+ <argument name="frontend_label" value="{{colorProductAttribute.default_label}}"/> -+ <argument name="label" value="{{colorConfigurableProductAttribute1.name}}"/> -+ <argument name="quantity" value="{{virtualProductBigQty.quantity}}"/> -+ </actionGroup> -+ <actionGroup ref="addUniqueQuantityToConfigurableProductOption" stepKey="addUniqueQtyForSecondOption"> -+ <argument name="frontend_label" value="{{colorProductAttribute.default_label}}"/> -+ <argument name="label" value="{{colorConfigurableProductAttribute2.name}}"/> -+ <argument name="quantity" value="{{virtualProductBigQty.quantity}}"/> -+ </actionGroup> -+ <click selector="{{AdminCreateProductConfigurationsPanel.next}}" stepKey="clickOnSecondNextButton"/> -+ <waitForPageLoad stepKey="waitForSummaryPageLoad"/> -+ <click selector="{{AdminCreateProductConfigurationsPanel.next}}" stepKey="clickOnNextButtonToGenerateConfigs"/> -+ <waitForPageLoad stepKey="waitForConfigurableProductPageLoad"/> -+ <actionGroup ref="saveProductForm" stepKey="saveProduct"/> -+ <!--Duplicate the product--> -+ <comment userInput="Duplicate the product" stepKey="commentDuplicateProduct"/> -+ <actionGroup ref="AdminFormSaveAndDuplicate" stepKey="saveAndDuplicateProductForm"/> -+ <click selector="{{AdminProductFormSection.enableProductLabel}}" stepKey="clickEnableProduct"/> -+ <fillField selector="{{AdminProductFormSection.productName}}" userInput="$$createConfigProduct.name$$-Updated" stepKey="fillProductName"/> -+ <selectOption selector="{{AdminProductFormSection.productStockStatus}}" userInput="1" stepKey="selectInStock"/> -+ <!--Change product image--> -+ <comment userInput="Change product image" stepKey="commentChangeProductImage"/> -+ <actionGroup ref="removeProductImage" stepKey="removeProductImage"/> -+ <actionGroup ref="addProductImage" stepKey="addImageForProduct"> -+ <argument name="image" value="TestImageAdobe"/> -+ </actionGroup> -+ <!--Disable configurations--> -+ <comment userInput="Disable configurations" stepKey="commentDisableConfigurations"/> -+ <actionGroup ref="AdminConfigurableProductDisableConfigurationsActionGroup" stepKey="disableFirstConfig"> -+ <argument name="productName" value="{{colorConfigurableProductAttribute1.name}}"/> -+ </actionGroup> -+ <actionGroup ref="AdminConfigurableProductDisableConfigurationsActionGroup" stepKey="disableSecondConfig"> -+ <argument name="productName" value="{{colorConfigurableProductAttribute2.name}}"/> -+ </actionGroup> -+ <actionGroup ref="saveProductForm" stepKey="saveDuplicatedProductForm"/> -+ <!--Create new configurations with another attribute--> -+ <comment userInput="Create new configurations with another attribute" stepKey="commentCreateNewConfigurations"/> -+ <actionGroup ref="AdminCreateAttributeFromProductPageWithScope" stepKey="createAttributeForDuplicatedProduct"> -+ <argument name="attributeName" value="{{productAttributeColor.default_label}}"/> -+ <argument name="attributeType" value="{{productAttributeColor.input_type}}"/> -+ <argument name="scope" value="Global"/> -+ </actionGroup> -+ <reloadPage stepKey="reloadDuplicatedProductPage"/> -+ <waitForPageLoad stepKey="waitForDuplicatedProductReload"/> -+ <click selector="{{AdminProductFormConfigurationsSection.createConfigurations}}" stepKey="createConfigurationsDuplicatedProduct"/> -+ <actionGroup ref="createOptionsForAttribute" stepKey="createOptionsForDuplicatedProduct"> -+ <argument name="attributeName" value="{{productAttributeColor.default_label}}"/> -+ <argument name="firstOptionName" value="{{colorConfigurableProductAttribute1.name}}"/> -+ <argument name="secondOptionName" value="{{colorConfigurableProductAttribute2.name}}"/> -+ </actionGroup> -+ <click selector="{{AdminCreateProductConfigurationsPanel.next}}" stepKey="clickOnFirstNextButtonForDuplicatedProduct"/> -+ <waitForPageLoad stepKey="waitForBulkImagesPriceQuantityPageLoadForDuplicatedProduct"/> -+ <actionGroup ref="addUniqueImageToConfigurableProductOption" stepKey="addImgConfigProductOption1DuplicatedProduct"> -+ <argument name="image" value="MagentoLogo"/> -+ <argument name="frontend_label" value="{{productAttributeColor.default_label}}"/> -+ <argument name="label" value="{{colorConfigurableProductAttribute1.name}}"/> -+ </actionGroup> -+ <actionGroup ref="addUniqueImageToConfigurableProductOption" stepKey="addImgConfigProductOption2DuplicatedProduct"> -+ <argument name="image" value="MagentoLogo"/> -+ <argument name="frontend_label" value="{{productAttributeColor.default_label}}"/> -+ <argument name="label" value="{{colorConfigurableProductAttribute2.name}}"/> -+ </actionGroup> -+ <actionGroup ref="addUniquePriceToConfigurableProductOption" stepKey="addPriceConfigProductOption1DuplicatedProduct"> -+ <argument name="frontend_label" value="{{productAttributeColor.default_label}}"/> -+ <argument name="label" value="{{colorConfigurableProductAttribute1.name}}"/> -+ <argument name="price" value="{{virtualProductWithRequiredFields.price}}"/> -+ </actionGroup> -+ <actionGroup ref="addUniquePriceToConfigurableProductOption" stepKey="addPriceConfigProductOption2DuplicatedProduct"> -+ <argument name="frontend_label" value="{{productAttributeColor.default_label}}"/> -+ <argument name="label" value="{{colorConfigurableProductAttribute2.name}}"/> -+ <argument name="price" value="{{virtualProductWithRequiredFields.price}}"/> -+ </actionGroup> -+ <actionGroup ref="addUniqueQuantityToConfigurableProductOption" stepKey="addUniqueQtyOption1DuplicatedProduct"> -+ <argument name="frontend_label" value="{{productAttributeColor.default_label}}"/> -+ <argument name="label" value="{{colorConfigurableProductAttribute1.name}}"/> -+ <argument name="quantity" value="{{virtualProductBigQty.quantity}}"/> -+ </actionGroup> -+ <actionGroup ref="addUniqueQuantityToConfigurableProductOption" stepKey="addUniqueQtyOption2DuplicatedProduct"> -+ <argument name="frontend_label" value="{{productAttributeColor.default_label}}"/> -+ <argument name="label" value="{{colorConfigurableProductAttribute2.name}}"/> -+ <argument name="quantity" value="{{virtualProductBigQty.quantity}}"/> -+ </actionGroup> -+ <click selector="{{AdminCreateProductConfigurationsPanel.next}}" stepKey="clickOn2NextButtonForDuplicatedProduct"/> -+ <waitForPageLoad stepKey="waitForSummaryPageLoadForDuplicatedProduct"/> -+ <click selector="{{AdminCreateProductConfigurationsPanel.next}}" stepKey="generateConfigsForDuplicatedProduct"/> -+ <waitForPageLoad stepKey="waitForDuplicatedProductPageLoad"/> -+ <actionGroup ref="saveProductForm" stepKey="saveDuplicatedProduct"/> -+ <magentoCLI command="indexer:reindex" stepKey="reindex"/> -+ <magentoCLI command="cache:flush" stepKey="flushCache"/> -+ <!--Assert configurable product in category--> -+ <comment userInput="Assert configurable product in category" stepKey="commentAssertProductInCategoryPage"/> -+ <amOnPage url="{{StorefrontCategoryPage.url($$createCategory.name$$)}}" stepKey="onStorefrontCategoryPage"/> -+ <waitForPageLoad stepKey="waitForCategoryPageLoad"/> -+ <seeElement selector="{{StorefrontCategoryProductSection.ProductTitleByName($$createConfigProduct.name$$-Updated)}}" stepKey="assertProductNameInCategoryPage"/> -+ <!--Assert product options in Storefront product page--> -+ <comment userInput="Assert product options in Storefront product page" stepKey="commentAssertProductOptions"/> -+ <amOnPage url="{{StorefrontProductPage.url($$createConfigProduct.sku$$-1)}}" stepKey="amOnSimpleProductPage"/> -+ <waitForPageLoad stepKey="waitForProductPageLoadOnStorefront"/> -+ <see selector="{{StorefrontProductInfoMainSection.productName}}" userInput="$$createConfigProduct.name$$-Updated" stepKey="seeConfigurableProductName"/> -+ <see userInput="{{productAttributeColor.default_label}}" selector="{{StorefrontProductInfoMainSection.productAttributeTitle1}}" stepKey="seeColorAttributeName"/> -+ <selectOption selector="{{StorefrontProductInfoMainSection.productAttributeOptionsSelectButton}}" userInput="{{colorConfigurableProductAttribute1.name}}" stepKey="selectFirstOption"/> -+ <see userInput="{{virtualProductWithRequiredFields.price}}" selector="{{StorefrontProductInfoMainSection.productPrice}}" stepKey="assertFirstOptionProductPrice"/> -+ <seeElement selector="{{StorefrontProductMediaSection.imageFile(MagentoLogo.filename)}}" stepKey="seeFirstImage"/> -+ <selectOption selector="{{StorefrontProductInfoMainSection.productAttributeOptionsSelectButton}}" userInput="{{colorConfigurableProductAttribute1.name}}" stepKey="selectSecondOption"/> -+ <see userInput="{{virtualProductWithRequiredFields.price}}" selector="{{StorefrontProductInfoMainSection.productPrice}}" stepKey="seeSecondOptionProductPrice"/> -+ <seeElement selector="{{StorefrontProductMediaSection.imageFile(MagentoLogo.filename)}}" stepKey="seeSecondImage"/> -+ </test> -+</tests> -diff --git a/app/code/Magento/ConfigurableProduct/Test/Unit/Block/Adminhtml/Product/Edit/Button/SaveTest.php b/app/code/Magento/ConfigurableProduct/Test/Unit/Block/Adminhtml/Product/Edit/Button/SaveTest.php -index 8df6df53cc0..2d73b61245a 100644 ---- a/app/code/Magento/ConfigurableProduct/Test/Unit/Block/Adminhtml/Product/Edit/Button/SaveTest.php -+++ b/app/code/Magento/ConfigurableProduct/Test/Unit/Block/Adminhtml/Product/Edit/Button/SaveTest.php -@@ -38,7 +38,7 @@ class SaveTest extends \PHPUnit\Framework\TestCase - ->disableOriginalConstructor() - ->getMock(); - $this->productMock = $this->getMockBuilder(ProductInterface::class) -- ->setMethods(['isReadonly', 'isDuplicable']) -+ ->setMethods(['isReadonly', 'isDuplicable', 'isComposite']) - ->getMockForAbstractClass(); - - $this->registryMock->expects(static::any()) -diff --git a/app/code/Magento/ConfigurableProduct/Test/Unit/Block/Product/View/Type/ConfigurableTest.php b/app/code/Magento/ConfigurableProduct/Test/Unit/Block/Product/View/Type/ConfigurableTest.php -index 25d8412c910..c5c2368720b 100644 ---- a/app/code/Magento/ConfigurableProduct/Test/Unit/Block/Product/View/Type/ConfigurableTest.php -+++ b/app/code/Magento/ConfigurableProduct/Test/Unit/Block/Product/View/Type/ConfigurableTest.php -@@ -379,6 +379,9 @@ class ConfigurableTest extends \PHPUnit\Framework\TestCase - 'percentage' => $percentage, - ], - ], -+ 'msrpPrice' => [ -+ 'amount' => null , -+ ] - ], - ], - 'priceFormat' => [], -diff --git a/app/code/Magento/ConfigurableProduct/Test/Unit/Model/LinkManagementTest.php b/app/code/Magento/ConfigurableProduct/Test/Unit/Model/LinkManagementTest.php -index ad2fcd1e593..c385934352a 100644 ---- a/app/code/Magento/ConfigurableProduct/Test/Unit/Model/LinkManagementTest.php -+++ b/app/code/Magento/ConfigurableProduct/Test/Unit/Model/LinkManagementTest.php -@@ -149,16 +149,19 @@ class LinkManagementTest extends \PHPUnit\Framework\TestCase - ->disableOriginalConstructor() - ->setMethods(['getId', 'getData']) - ->getMock(); -- - $extensionAttributesMock = $this->getMockBuilder(\Magento\Catalog\Api\Data\ProductExtension::class) - ->disableOriginalConstructor() -- ->setMethods([ -- 'getConfigurableProductOptions', 'setConfigurableProductOptions', 'setConfigurableProductLinks' -- ]) -+ ->setMethods( -+ [ -+ 'getConfigurableProductOptions', -+ 'setConfigurableProductOptions', -+ 'setConfigurableProductLinks' -+ ] -+ ) - ->getMock(); - $optionMock = $this->getMockBuilder(\Magento\ConfigurableProduct\Api\Data\Option::class) - ->disableOriginalConstructor() -- ->setMethods(['getProductAttribute', 'getAttributeId']) -+ ->setMethods(['getProductAttribute', 'getPosition', 'getAttributeId']) - ->getMock(); - $productAttributeMock = $this->getMockBuilder(\Magento\Eav\Model\Entity\Attribute\AbstractAttribute::class) - ->disableOriginalConstructor() -@@ -189,7 +192,6 @@ class LinkManagementTest extends \PHPUnit\Framework\TestCase - ->disableOriginalConstructor() - ->setMethods(['getValue', 'getLabel']) - ->getMock(); -- - $attributeCollectionMock = $this->getMockBuilder( - \Magento\ConfigurableProduct\Model\ResourceModel\Product\Type\Configurable\Attribute\Collection::class - ) -@@ -216,20 +218,18 @@ class LinkManagementTest extends \PHPUnit\Framework\TestCase - $productAttributeMock->expects($this->any())->method('getAttributeCode')->willReturn('color'); - $simple->expects($this->any())->method('getData')->willReturn('color'); - $optionMock->expects($this->any())->method('getAttributeId')->willReturn('1'); -+ $optionMock->expects($this->any())->method('getPosition')->willReturn('0'); - - $optionsFactoryMock->expects($this->any())->method('create')->willReturn([$optionMock]); - $attributeFactoryMock->expects($this->any())->method('create')->willReturn($attributeMock); - $attributeMock->expects($this->any())->method('getCollection')->willReturn($attributeCollectionMock); - $attributeCollectionMock->expects($this->any())->method('addFieldToFilter')->willReturnSelf(); - $attributeCollectionMock->expects($this->any())->method('getItems')->willReturn([$attributeMock]); -- -+ $attributeMock->expects($this->any())->method('getId')->willReturn(1); - $attributeMock->expects($this->any())->method('getOptions')->willReturn([$attributeOptionMock]); -- - $extensionAttributesMock->expects($this->any())->method('setConfigurableProductOptions'); - $extensionAttributesMock->expects($this->any())->method('setConfigurableProductLinks'); -- - $this->productRepository->expects($this->once())->method('save'); -- - $this->assertTrue(true, $this->object->addChild($productSku, $childSku)); - } - -diff --git a/app/code/Magento/ConfigurableProduct/Test/Unit/Model/Plugin/Frontend/ProductIdentitiesExtenderTest.php b/app/code/Magento/ConfigurableProduct/Test/Unit/Model/Plugin/Frontend/ProductIdentitiesExtenderTest.php -new file mode 100644 -index 00000000000..b4fb5ccfaa5 ---- /dev/null -+++ b/app/code/Magento/ConfigurableProduct/Test/Unit/Model/Plugin/Frontend/ProductIdentitiesExtenderTest.php -@@ -0,0 +1,77 @@ -+<?php -+/** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+declare(strict_types=1); -+ -+namespace Magento\ConfigurableProduct\Test\Unit\Model\Plugin\Frontend; -+ -+use Magento\ConfigurableProduct\Model\Plugin\Frontend\ProductIdentitiesExtender; -+use Magento\ConfigurableProduct\Model\Product\Type\Configurable; -+use Magento\Catalog\Model\Product; -+ -+/** -+ * Class ProductIdentitiesExtenderTest -+ */ -+class ProductIdentitiesExtenderTest extends \PHPUnit\Framework\TestCase -+{ -+ /** -+ * @var \PHPUnit_Framework_MockObject_MockObject|Configurable -+ */ -+ private $configurableTypeMock; -+ -+ /** -+ * @var ProductIdentitiesExtender -+ */ -+ private $plugin; -+ -+ /** @var MockObject|\Magento\Catalog\Model\Product */ -+ private $product; -+ -+ protected function setUp() -+ { -+ $this->product = $this->getMockBuilder(Product::class) -+ ->disableOriginalConstructor() -+ ->setMethods(['getId']) -+ ->getMock(); -+ -+ $this->configurableTypeMock = $this->getMockBuilder(Configurable::class) -+ ->disableOriginalConstructor() -+ ->getMock(); -+ -+ $this->plugin = new ProductIdentitiesExtender($this->configurableTypeMock); -+ } -+ -+ public function testAfterGetIdentities() -+ { -+ $identities = [ -+ 'SomeCacheId', -+ 'AnotherCacheId', -+ ]; -+ $productId = 12345; -+ $childIdentities = [ -+ 0 => [1, 2, 5, 100500] -+ ]; -+ $expectedIdentities = [ -+ 'SomeCacheId', -+ 'AnotherCacheId', -+ Product::CACHE_TAG . '_' . 1, -+ Product::CACHE_TAG . '_' . 2, -+ Product::CACHE_TAG . '_' . 5, -+ Product::CACHE_TAG . '_' . 100500, -+ ]; -+ -+ $this->product->expects($this->once()) -+ ->method('getId') -+ ->willReturn($productId); -+ -+ $this->configurableTypeMock->expects($this->once()) -+ ->method('getChildrenIds') -+ ->with($productId) -+ ->willReturn($childIdentities); -+ -+ $productIdentities = $this->plugin->afterGetIdentities($this->product, $identities); -+ $this->assertEquals($expectedIdentities, $productIdentities); -+ } -+} -diff --git a/app/code/Magento/ConfigurableProduct/Test/Unit/Model/Product/Configuration/Item/ItemProductResolverTest.php b/app/code/Magento/ConfigurableProduct/Test/Unit/Model/Product/Configuration/Item/ItemProductResolverTest.php -new file mode 100644 -index 00000000000..8dac2dee10d ---- /dev/null -+++ b/app/code/Magento/ConfigurableProduct/Test/Unit/Model/Product/Configuration/Item/ItemProductResolverTest.php -@@ -0,0 +1,143 @@ -+<?php -+/** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+declare(strict_types=1); -+ -+namespace Magento\ConfigurableProduct\Test\Unit\Model\Product\Configuration\Item; -+ -+use Magento\Catalog\Model\Config\Source\Product\Thumbnail; -+use Magento\Catalog\Model\Product; -+use Magento\Catalog\Model\Product\Configuration\Item\ItemInterface; -+use Magento\Catalog\Model\Product\Configuration\Item\Option\OptionInterface; -+use Magento\ConfigurableProduct\Model\Product\Configuration\Item\ItemProductResolver; -+use Magento\Framework\App\Config\ScopeConfigInterface; -+use Magento\Quote\Model\Quote\Item\Option; -+use PHPUnit\Framework\MockObject\MockObject; -+use PHPUnit\Framework\TestCase; -+ -+class ItemProductResolverTest extends TestCase -+{ -+ /** @var ItemProductResolver */ -+ private $model; -+ /** @var ItemInterface | MockObject */ -+ private $item; -+ /** @var Product | MockObject */ -+ private $parentProduct; -+ /** @var ScopeConfigInterface | MockObject */ -+ private $scopeConfig; -+ /** @var OptionInterface | MockObject */ -+ private $option; -+ /** @var Product | MockObject */ -+ private $childProduct; -+ -+ /** -+ * Set up method -+ */ -+ protected function setUp() -+ { -+ parent::setUp(); -+ -+ $this->scopeConfig = $this->getMockBuilder(ScopeConfigInterface::class) -+ ->disableOriginalConstructor() -+ ->getMock(); -+ -+ $this->parentProduct = $this->getMockBuilder(Product::class) -+ ->disableOriginalConstructor() -+ ->getMock(); -+ $this->parentProduct -+ ->method('getSku') -+ ->willReturn('parent_product'); -+ -+ $this->childProduct = $this->getMockBuilder(Product::class) -+ ->disableOriginalConstructor() -+ ->getMock(); -+ $this->childProduct -+ ->method('getSku') -+ ->willReturn('child_product'); -+ -+ $this->option = $this->getMockBuilder(Option::class) -+ ->disableOriginalConstructor() -+ ->getMock(); -+ -+ $this->option -+ ->method('getProduct') -+ ->willReturn($this->childProduct); -+ -+ $this->item = $this->getMockBuilder(ItemInterface::class) -+ ->disableOriginalConstructor() -+ ->getMock(); -+ -+ $this->item -+ ->expects($this->once()) -+ ->method('getProduct') -+ ->willReturn($this->parentProduct); -+ -+ $this->model = new ItemProductResolver($this->scopeConfig); -+ } -+ -+ /** -+ * Test for deleted child product from configurable product -+ */ -+ public function testGetFinalProductChildIsNull(): void -+ { -+ $this->scopeConfig->expects($this->never())->method('getValue'); -+ $this->childProduct->expects($this->never())->method('getData'); -+ -+ $this->item->expects($this->once()) -+ ->method('getOptionByCode') -+ ->willReturn(null); -+ -+ $finalProduct = $this->model->getFinalProduct($this->item); -+ $this->assertEquals( -+ $this->parentProduct->getSku(), -+ $finalProduct->getSku() -+ ); -+ } -+ -+ /** -+ * Tests child product from configurable product -+ * -+ * @dataProvider provideScopeConfig -+ * @param string $expectedSku -+ * @param string $scopeValue -+ * @param string | null $thumbnail -+ */ -+ public function testGetFinalProductChild($expectedSku, $scopeValue, $thumbnail): void -+ { -+ $this->item->expects($this->once()) -+ ->method('getOptionByCode') -+ ->willReturn($this->option); -+ -+ $this->childProduct -+ ->expects($this->once()) -+ ->method('getData') -+ ->willReturn($thumbnail); -+ -+ $this->scopeConfig->expects($this->once()) -+ ->method('getValue') -+ ->willReturn($scopeValue); -+ -+ $finalProduct = $this->model->getFinalProduct($this->item); -+ $this->assertEquals($expectedSku, $finalProduct->getSku()); -+ } -+ -+ /** -+ * Dataprovider for scope test -+ * @return array -+ */ -+ public function provideScopeConfig(): array -+ { -+ return [ -+ ['child_product', Thumbnail::OPTION_USE_OWN_IMAGE, 'thumbnail'], -+ ['parent_product', Thumbnail::OPTION_USE_PARENT_IMAGE, 'thumbnail'], -+ -+ ['parent_product', Thumbnail::OPTION_USE_OWN_IMAGE, null], -+ ['parent_product', Thumbnail::OPTION_USE_OWN_IMAGE, 'no_selection'], -+ -+ ['parent_product', Thumbnail::OPTION_USE_PARENT_IMAGE, null], -+ ['parent_product', Thumbnail::OPTION_USE_PARENT_IMAGE, 'no_selection'], -+ ]; -+ } -+} -diff --git a/app/code/Magento/ConfigurableProduct/Test/Unit/Model/Product/Type/Configurable/PriceTest.php b/app/code/Magento/ConfigurableProduct/Test/Unit/Model/Product/Type/Configurable/PriceTest.php -index 64b9b377644..ab52d4eb860 100644 ---- a/app/code/Magento/ConfigurableProduct/Test/Unit/Model/Product/Type/Configurable/PriceTest.php -+++ b/app/code/Magento/ConfigurableProduct/Test/Unit/Model/Product/Type/Configurable/PriceTest.php -@@ -6,22 +6,47 @@ - - namespace Magento\ConfigurableProduct\Test\Unit\Model\Product\Type\Configurable; - -+use Magento\Catalog\Model\Product; -+use Magento\Catalog\Model\Product\Configuration\Item\Option; -+use Magento\ConfigurableProduct\Model\Product\Type\Configurable\Price as ConfigurablePrice; -+use Magento\Framework\Event\ManagerInterface; -+use Magento\Framework\Pricing\Amount\AmountInterface; -+use Magento\Framework\Pricing\Price\PriceInterface; -+use Magento\Framework\Pricing\PriceInfo\Base as PriceInfoBase; - use Magento\Framework\TestFramework\Unit\Helper\ObjectManager as ObjectManagerHelper; -+use PHPUnit\Framework\MockObject\MockObject; - - class PriceTest extends \PHPUnit\Framework\TestCase - { -- /** @var \Magento\ConfigurableProduct\Model\Product\Type\Configurable\Price */ -+ /** -+ * @var ObjectManagerHelper -+ */ -+ protected $objectManagerHelper; -+ -+ /** -+ * @var ConfigurablePrice -+ */ - protected $model; - -- /** @var ObjectManagerHelper */ -- protected $objectManagerHelper; -+ /** -+ * @var ManagerInterface|MockObject -+ */ -+ private $eventManagerMock; - -+ /** -+ * @inheritdoc -+ */ - protected function setUp() - { - $this->objectManagerHelper = new ObjectManagerHelper($this); - -+ $this->eventManagerMock = $this->createPartialMock( -+ ManagerInterface::class, -+ ['dispatch'] -+ ); - $this->model = $this->objectManagerHelper->getObject( -- \Magento\ConfigurableProduct\Model\Product\Type\Configurable\Price::class -+ ConfigurablePrice::class, -+ ['eventManager' => $this->eventManagerMock] - ); - } - -@@ -29,29 +54,29 @@ class PriceTest extends \PHPUnit\Framework\TestCase - { - $finalPrice = 10; - $qty = 1; -- $configurableProduct = $this->getMockBuilder(\Magento\Catalog\Model\Product::class) -- ->disableOriginalConstructor() -- ->setMethods(['getCustomOption', 'getPriceInfo', 'setFinalPrice', '__wakeUp']) -- ->getMock(); -- $customOption = $this->getMockBuilder(\Magento\Catalog\Model\Product\Configuration\Item\Option::class) -+ -+ /** @var Product|MockObject $configurableProduct */ -+ $configurableProduct = $this->getMockBuilder(Product::class) - ->disableOriginalConstructor() -- ->setMethods(['getProduct']) -+ ->setMethods(['getCustomOption', 'getPriceInfo', 'setFinalPrice']) - ->getMock(); -- $priceInfo = $this->getMockBuilder(\Magento\Framework\Pricing\PriceInfo\Base::class) -+ /** @var PriceInfoBase|MockObject $priceInfo */ -+ $priceInfo = $this->getMockBuilder(PriceInfoBase::class) - ->disableOriginalConstructor() - ->setMethods(['getPrice']) - ->getMock(); -- $price = $this->getMockBuilder(\Magento\Framework\Pricing\Price\PriceInterface::class) -+ /** @var PriceInterface|MockObject $price */ -+ $price = $this->getMockBuilder(PriceInterface::class) - ->disableOriginalConstructor() - ->getMock(); -- $amount = $this->getMockBuilder(\Magento\Framework\Pricing\Amount\AmountInterface::class) -+ /** @var AmountInterface|MockObject $amount */ -+ $amount = $this->getMockBuilder(AmountInterface::class) - ->disableOriginalConstructor() - ->getMock(); - - $configurableProduct->expects($this->any()) - ->method('getCustomOption') - ->willReturnMap([['simple_product', false], ['option_ids', false]]); -- $customOption->expects($this->never())->method('getProduct'); - $configurableProduct->expects($this->once())->method('getPriceInfo')->willReturn($priceInfo); - $priceInfo->expects($this->once())->method('getPrice')->with('final_price')->willReturn($price); - $price->expects($this->once())->method('getAmount')->willReturn($amount); -@@ -60,4 +85,60 @@ class PriceTest extends \PHPUnit\Framework\TestCase - - $this->assertEquals($finalPrice, $this->model->getFinalPrice($qty, $configurableProduct)); - } -+ -+ public function testGetFinalPriceWithSimpleProduct() -+ { -+ $finalPrice = 10; -+ $qty = 1; -+ $customerGroupId = 1; -+ -+ /** @var Product|MockObject $configurableProduct */ -+ $configurableProduct = $this->createPartialMock( -+ Product::class, -+ ['getCustomOption', 'setFinalPrice', 'getCustomerGroupId'] -+ ); -+ /** @var Option|MockObject $customOption */ -+ $customOption = $this->createPartialMock( -+ Option::class, -+ ['getProduct'] -+ ); -+ /** @var Product|MockObject $simpleProduct */ -+ $simpleProduct = $this->createPartialMock( -+ Product::class, -+ ['setCustomerGroupId', 'setFinalPrice', 'getPrice', 'getTierPrice', 'getData', 'getCustomOption'] -+ ); -+ -+ $configurableProduct->method('getCustomOption') -+ ->willReturnMap([ -+ ['simple_product', $customOption], -+ ['option_ids', false] -+ ]); -+ $configurableProduct->method('getCustomerGroupId')->willReturn($customerGroupId); -+ $configurableProduct->expects($this->atLeastOnce()) -+ ->method('setFinalPrice') -+ ->with($finalPrice) -+ ->willReturnSelf(); -+ $customOption->method('getProduct')->willReturn($simpleProduct); -+ $simpleProduct->expects($this->atLeastOnce()) -+ ->method('setCustomerGroupId') -+ ->with($customerGroupId) -+ ->willReturnSelf(); -+ $simpleProduct->method('getPrice')->willReturn($finalPrice); -+ $simpleProduct->method('getTierPrice')->with($qty)->willReturn($finalPrice); -+ $simpleProduct->expects($this->atLeastOnce()) -+ ->method('setFinalPrice') -+ ->with($finalPrice) -+ ->willReturnSelf(); -+ $simpleProduct->method('getData')->with('final_price')->willReturn($finalPrice); -+ $simpleProduct->method('getCustomOption')->with('option_ids')->willReturn(false); -+ $this->eventManagerMock->expects($this->once()) -+ ->method('dispatch') -+ ->with('catalog_product_get_final_price', ['product' => $simpleProduct, 'qty' => $qty]); -+ -+ $this->assertEquals( -+ $finalPrice, -+ $this->model->getFinalPrice($qty, $configurableProduct), -+ 'The final price calculation is wrong' -+ ); -+ } - } -diff --git a/app/code/Magento/ConfigurableProduct/Test/Unit/Model/Product/Type/ConfigurableTest.php b/app/code/Magento/ConfigurableProduct/Test/Unit/Model/Product/Type/ConfigurableTest.php -index 5e9399ddd3d..c351d12fa81 100644 ---- a/app/code/Magento/ConfigurableProduct/Test/Unit/Model/Product/Type/ConfigurableTest.php -+++ b/app/code/Magento/ConfigurableProduct/Test/Unit/Model/Product/Type/ConfigurableTest.php -@@ -11,10 +11,13 @@ use Magento\Catalog\Model\Config; - use Magento\Catalog\Model\Product\Configuration\Item\Option\OptionInterface; - use Magento\ConfigurableProduct\Model\Product\Type\Collection\SalableProcessor; - use Magento\ConfigurableProduct\Model\Product\Type\Configurable; -+use Magento\ConfigurableProduct\Model\Product\Type\Configurable\Attribute; - use Magento\ConfigurableProduct\Model\Product\Type\Configurable\AttributeFactory; - use Magento\ConfigurableProduct\Model\ResourceModel\Product\Type\Configurable\Attribute\Collection; - use Magento\ConfigurableProduct\Model\ResourceModel\Product\Type\Configurable\Attribute\CollectionFactory; - use Magento\ConfigurableProduct\Model\ResourceModel\Product\Type\Configurable\Product\Collection as ProductCollection; -+use Magento\ConfigurableProduct\Model\ResourceModel\Product\Type\Configurable\Product\CollectionFactory -+ as ProductCollectionFactory; - use Magento\ConfigurableProduct\Model\ResourceModel\Product\Type\ConfigurableFactory; - use Magento\Customer\Model\Session; - use Magento\Eav\Model\Entity\Attribute\Frontend\AbstractFrontend; -@@ -153,9 +156,7 @@ class ConfigurableTest extends \PHPUnit\Framework\TestCase - ->disableOriginalConstructor() - ->setMethods(['create']) - ->getMock(); -- $this->productCollectionFactory = $this->getMockBuilder( -- \Magento\ConfigurableProduct\Model\ResourceModel\Product\Type\Configurable\Product\CollectionFactory::class -- ) -+ $this->productCollectionFactory = $this->getMockBuilder(ProductCollectionFactory::class) - ->disableOriginalConstructor() - ->setMethods(['create']) - ->getMock(); -@@ -197,11 +198,6 @@ class ConfigurableTest extends \PHPUnit\Framework\TestCase - ->disableOriginalConstructor() - ->getMock(); - -- $this->productFactory = $this->getMockBuilder(\Magento\Catalog\Api\Data\ProductInterfaceFactory::class) -- ->setMethods(['create']) -- ->disableOriginalConstructor() -- ->getMock(); -- - $this->salableProcessor = $this->createMock(SalableProcessor::class); - - $this->model = $this->objectHelper->getObject( -@@ -286,9 +282,7 @@ class ConfigurableTest extends \PHPUnit\Framework\TestCase - $product->expects($this->atLeastOnce()) - ->method('getData') - ->willReturnMap($dataMap); -- $attribute = $this->getMockBuilder( -- \Magento\ConfigurableProduct\Model\Product\Type\Configurable\Attribute::class -- ) -+ $attribute = $this->getMockBuilder(Attribute::class) - ->disableOriginalConstructor() - ->setMethods(['addData', 'setStoreId', 'setProductId', 'save', '__wakeup', '__sleep']) - ->getMock(); -@@ -385,7 +379,7 @@ class ConfigurableTest extends \PHPUnit\Framework\TestCase - ['_cache_instance_used_product_attributes', null, []] - ] - ); -- -+ $this->catalogConfig->expects($this->any())->method('getProductAttributes')->willReturn([]); - $productCollection->expects($this->atLeastOnce())->method('addAttributeToSelect')->willReturnSelf(); - $productCollection->expects($this->once())->method('setProductFilter')->willReturnSelf(); - $productCollection->expects($this->atLeastOnce())->method('setFlag')->willReturnSelf(); -@@ -470,9 +464,7 @@ class ConfigurableTest extends \PHPUnit\Framework\TestCase - $eavAttribute->expects($this->once())->method('getSource')->willReturn($attributeSource); - $eavAttribute->expects($this->atLeastOnce())->method('getStoreLabel')->willReturn('Store Label'); - -- $attribute = $this->getMockBuilder( -- \Magento\ConfigurableProduct\Model\Product\Type\Configurable\Attribute::class -- ) -+ $attribute = $this->getMockBuilder(Attribute::class) - ->disableOriginalConstructor() - ->setMethods(['getProductAttribute', '__wakeup', '__sleep']) - ->getMock(); -@@ -515,17 +507,34 @@ class ConfigurableTest extends \PHPUnit\Framework\TestCase - ]; - } - -+ public function testGetConfigurableAttributesNewProduct() -+ { -+ $configurableAttributes = '_cache_instance_configurable_attributes'; -+ -+ /** @var \Magento\Catalog\Model\Product|\PHPUnit_Framework_MockObject_MockObject $product */ -+ $product = $this->getMockBuilder(\Magento\Catalog\Model\Product::class) -+ ->setMethods(['hasData', 'getId']) -+ ->disableOriginalConstructor() -+ ->getMock(); -+ -+ $product->expects($this->once())->method('hasData')->with($configurableAttributes)->willReturn(false); -+ $product->expects($this->once())->method('getId')->willReturn(null); -+ -+ $this->assertEquals([], $this->model->getConfigurableAttributes($product)); -+ } -+ - public function testGetConfigurableAttributes() - { - $configurableAttributes = '_cache_instance_configurable_attributes'; - - /** @var \Magento\Catalog\Model\Product|\PHPUnit_Framework_MockObject_MockObject $product */ - $product = $this->getMockBuilder(\Magento\Catalog\Model\Product::class) -- ->setMethods(['getData', 'hasData', 'setData']) -+ ->setMethods(['getData', 'hasData', 'setData', 'getId']) - ->disableOriginalConstructor() - ->getMock(); - - $product->expects($this->once())->method('hasData')->with($configurableAttributes)->willReturn(false); -+ $product->expects($this->once())->method('getId')->willReturn(1); - - $attributeCollection = $this->getMockBuilder(Collection::class) - ->setMethods(['setProductFilter', 'orderByPosition', 'load']) -@@ -581,9 +590,7 @@ class ConfigurableTest extends \PHPUnit\Framework\TestCase - ->setMethods(['__wakeup', 'getAttributeCode', 'getOptions', 'hasData', 'getData']) - ->disableOriginalConstructor() - ->getMock(); -- $attributeMock = $this->getMockBuilder( -- \Magento\ConfigurableProduct\Model\Product\Type\Configurable\Attribute::class -- ) -+ $attributeMock = $this->getMockBuilder(Attribute::class) - ->disableOriginalConstructor() - ->getMock(); - -@@ -689,7 +696,7 @@ class ConfigurableTest extends \PHPUnit\Framework\TestCase - ->disableOriginalConstructor() - ->getMock(); - $usedAttributeMock = $this->getMockBuilder( -- \Magento\ConfigurableProduct\Model\Product\Type\Configurable\Attribute::class -+ Attribute::class - ) - ->setMethods(['getProductAttribute']) - ->disableOriginalConstructor() -diff --git a/app/code/Magento/ConfigurableProduct/Test/Unit/Model/ResourceModel/Attribute/OptionSelectBuilderTest.php b/app/code/Magento/ConfigurableProduct/Test/Unit/Model/ResourceModel/Attribute/OptionSelectBuilderTest.php -index 9802c97372b..537288618b6 100644 ---- a/app/code/Magento/ConfigurableProduct/Test/Unit/Model/ResourceModel/Attribute/OptionSelectBuilderTest.php -+++ b/app/code/Magento/ConfigurableProduct/Test/Unit/Model/ResourceModel/Attribute/OptionSelectBuilderTest.php -@@ -112,8 +112,8 @@ class OptionSelectBuilderTest extends \PHPUnit\Framework\TestCase - { - $this->select->expects($this->exactly(1))->method('from')->willReturnSelf(); - $this->select->expects($this->exactly(1))->method('columns')->willReturnSelf(); -- $this->select->expects($this->exactly(6))->method('joinInner')->willReturnSelf(); -- $this->select->expects($this->exactly(3))->method('joinLeft')->willReturnSelf(); -+ $this->select->expects($this->exactly(5))->method('joinInner')->willReturnSelf(); -+ $this->select->expects($this->exactly(4))->method('joinLeft')->willReturnSelf(); - $this->select->expects($this->exactly(1))->method('order')->willReturnSelf(); - $this->select->expects($this->exactly(2))->method('where')->willReturnSelf(); - -@@ -156,8 +156,8 @@ class OptionSelectBuilderTest extends \PHPUnit\Framework\TestCase - { - $this->select->expects($this->exactly(1))->method('from')->willReturnSelf(); - $this->select->expects($this->exactly(0))->method('columns')->willReturnSelf(); -- $this->select->expects($this->exactly(6))->method('joinInner')->willReturnSelf(); -- $this->select->expects($this->exactly(1))->method('joinLeft')->willReturnSelf(); -+ $this->select->expects($this->exactly(5))->method('joinInner')->willReturnSelf(); -+ $this->select->expects($this->exactly(2))->method('joinLeft')->willReturnSelf(); - $this->select->expects($this->exactly(1))->method('order')->willReturnSelf(); - $this->select->expects($this->exactly(2))->method('where')->willReturnSelf(); - -diff --git a/app/code/Magento/ConfigurableProduct/Test/Unit/Plugin/Catalog/Model/Product/Pricing/Renderer/SalableResolverTest.php b/app/code/Magento/ConfigurableProduct/Test/Unit/Plugin/Catalog/Model/Product/Pricing/Renderer/SalableResolverTest.php -new file mode 100644 -index 00000000000..7ec2ce370ac ---- /dev/null -+++ b/app/code/Magento/ConfigurableProduct/Test/Unit/Plugin/Catalog/Model/Product/Pricing/Renderer/SalableResolverTest.php -@@ -0,0 +1,82 @@ -+<?php -+/** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+namespace Magento\ConfigurableProduct\Test\Unit\Plugin\Catalog\Model\Product\Pricing\Renderer; -+ -+use Magento\Catalog\Model\Product\Pricing\Renderer\SalableResolver; -+use Magento\ConfigurableProduct\Model\Product\Type\Configurable as TypeConfigurable; -+use Magento\ConfigurableProduct\Plugin\Catalog\Model\Product\Pricing\Renderer\SalableResolver as SalableResolverPlugin; -+use Magento\Framework\Pricing\SaleableInterface; -+use PHPUnit\Framework\MockObject\MockObject; -+use PHPUnit\Framework\TestCase; -+ -+class SalableResolverTest extends TestCase -+{ -+ /** -+ * @var TypeConfigurable|MockObject -+ */ -+ private $typeConfigurable; -+ -+ /** -+ * @var SalableResolverPlugin -+ */ -+ private $salableResolver; -+ -+ protected function setUp() -+ { -+ $this->typeConfigurable = $this->createMock(TypeConfigurable::class); -+ $this->salableResolver = new SalableResolverPlugin($this->typeConfigurable); -+ } -+ -+ /** -+ * @param SaleableInterface|MockObject $salableItem -+ * @param bool $isSalable -+ * @param bool $typeIsSalable -+ * @param bool $expectedResult -+ * @return void -+ * @dataProvider afterIsSalableDataProvider -+ */ -+ public function testAfterIsSalable($salableItem, bool $isSalable, bool $typeIsSalable, bool $expectedResult): void -+ { -+ $salableResolver = $this->createMock(SalableResolver::class); -+ -+ $this->typeConfigurable->method('isSalable') -+ ->willReturn($typeIsSalable); -+ -+ $result = $this->salableResolver->afterIsSalable($salableResolver, $isSalable, $salableItem); -+ $this->assertEquals($expectedResult, $result); -+ } -+ -+ /** -+ * @return array -+ */ -+ public function afterIsSalableDataProvider(): array -+ { -+ $simpleSalableItem = $this->createMock(SaleableInterface::class); -+ $simpleSalableItem->expects($this->once()) -+ ->method('getTypeId') -+ ->willReturn('simple'); -+ -+ $configurableSalableItem = $this->createMock(SaleableInterface::class); -+ $configurableSalableItem->expects($this->once()) -+ ->method('getTypeId') -+ ->willReturn('configurable'); -+ -+ return [ -+ [ -+ $simpleSalableItem, -+ true, -+ false, -+ true, -+ ], -+ [ -+ $configurableSalableItem, -+ true, -+ false, -+ false, -+ ], -+ ]; -+ } -+} -diff --git a/app/code/Magento/ConfigurableProduct/Test/Unit/Plugin/SalesRule/Model/Rule/Condition/ProductTest.php b/app/code/Magento/ConfigurableProduct/Test/Unit/Plugin/SalesRule/Model/Rule/Condition/ProductTest.php -new file mode 100644 -index 00000000000..80979148c49 ---- /dev/null -+++ b/app/code/Magento/ConfigurableProduct/Test/Unit/Plugin/SalesRule/Model/Rule/Condition/ProductTest.php -@@ -0,0 +1,226 @@ -+<?php declare(strict_types=1); -+/** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+ -+namespace Magento\ConfigurableProduct\Test\Unit\Plugin\SalesRule\Model\Rule\Condition; -+ -+use Magento\Backend\Helper\Data; -+use Magento\Catalog\Api\ProductRepositoryInterface; -+use Magento\Catalog\Model\Product\Type; -+use Magento\Catalog\Model\ProductFactory; -+use Magento\Catalog\Model\ResourceModel\Product; -+use Magento\ConfigurableProduct\Model\Product\Type\Configurable; -+use Magento\ConfigurableProduct\Plugin\SalesRule\Model\Rule\Condition\Product as ValidatorPlugin; -+use Magento\Directory\Model\CurrencyFactory; -+use Magento\Eav\Model\Config; -+use Magento\Eav\Model\Entity\AbstractEntity; -+use Magento\Eav\Model\ResourceModel\Entity\Attribute\Set\Collection; -+use Magento\Framework\App\ScopeResolverInterface; -+use Magento\Framework\Locale\Format; -+use Magento\Framework\Locale\FormatInterface; -+use Magento\Framework\Locale\ResolverInterface; -+use Magento\Quote\Model\Quote\Item\AbstractItem; -+use Magento\Rule\Model\Condition\Context; -+use Magento\SalesRule\Model\Rule\Condition\Product as SalesRuleProduct; -+ -+/** -+ * @SuppressWarnings(PHPMD.CouplingBetweenObjects) -+ * @SuppressWarnings(PHPMD.LongVariable) -+ */ -+class ProductTest extends \PHPUnit\Framework\TestCase -+{ -+ /** -+ * @var \Magento\Framework\TestFramework\Unit\Helper\ObjectManager -+ */ -+ private $objectManager; -+ -+ /** -+ * @var SalesRuleProduct -+ */ -+ private $validator; -+ -+ /** -+ * @var \Magento\ConfigurableProduct\Plugin\SalesRule\Model\Rule\Condition\Product -+ */ -+ private $validatorPlugin; -+ -+ public function setUp() -+ { -+ $this->objectManager = new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this); -+ $this->validator = $this->createValidator(); -+ $this->validatorPlugin = $this->objectManager->getObject(ValidatorPlugin::class); -+ } -+ -+ /** -+ * @return \Magento\SalesRule\Model\Rule\Condition\Product -+ */ -+ private function createValidator(): SalesRuleProduct -+ { -+ /** @var Context|\PHPUnit_Framework_MockObject_MockObject $contextMock */ -+ $contextMock = $this->getMockBuilder(Context::class) -+ ->disableOriginalConstructor() -+ ->getMock(); -+ /** @var Data|\PHPUnit_Framework_MockObject_MockObject $backendHelperMock */ -+ $backendHelperMock = $this->getMockBuilder(Data::class) -+ ->disableOriginalConstructor() -+ ->getMock(); -+ /** @var Config|\PHPUnit_Framework_MockObject_MockObject $configMock */ -+ $configMock = $this->getMockBuilder(Config::class) -+ ->disableOriginalConstructor() -+ ->getMock(); -+ /** @var ProductFactory|\PHPUnit_Framework_MockObject_MockObject $productFactoryMock */ -+ $productFactoryMock = $this->getMockBuilder(ProductFactory::class) -+ ->disableOriginalConstructor() -+ ->getMock(); -+ /** @var ProductRepositoryInterface|\PHPUnit_Framework_MockObject_MockObject $productRepositoryMock */ -+ $productRepositoryMock = $this->getMockBuilder(ProductRepositoryInterface::class) -+ ->getMockForAbstractClass(); -+ $attributeLoaderInterfaceMock = $this->getMockBuilder(AbstractEntity::class) -+ ->disableOriginalConstructor() -+ ->setMethods(['getAttributesByCode']) -+ ->getMock(); -+ $attributeLoaderInterfaceMock -+ ->expects($this->any()) -+ ->method('getAttributesByCode') -+ ->willReturn([]); -+ /** @var Product|\PHPUnit_Framework_MockObject_MockObject $productMock */ -+ $productMock = $this->getMockBuilder(Product::class) -+ ->disableOriginalConstructor() -+ ->setMethods(['loadAllAttributes', 'getConnection', 'getTable']) -+ ->getMock(); -+ $productMock->expects($this->any()) -+ ->method('loadAllAttributes') -+ ->willReturn($attributeLoaderInterfaceMock); -+ /** @var Collection|\PHPUnit_Framework_MockObject_MockObject $collectionMock */ -+ $collectionMock = $this->getMockBuilder(Collection::class) -+ ->disableOriginalConstructor() -+ ->getMock(); -+ /** @var FormatInterface|\PHPUnit_Framework_MockObject_MockObject $formatMock */ -+ $formatMock = new Format( -+ $this->getMockBuilder(ScopeResolverInterface::class)->disableOriginalConstructor()->getMock(), -+ $this->getMockBuilder(ResolverInterface::class)->disableOriginalConstructor()->getMock(), -+ $this->getMockBuilder(CurrencyFactory::class)->disableOriginalConstructor()->getMock() -+ ); -+ -+ return new SalesRuleProduct( -+ $contextMock, -+ $backendHelperMock, -+ $configMock, -+ $productFactoryMock, -+ $productRepositoryMock, -+ $productMock, -+ $collectionMock, -+ $formatMock -+ ); -+ } -+ -+ public function testChildIsUsedForValidation() -+ { -+ $configurableProductMock = $this->createProductMock(); -+ $configurableProductMock -+ ->expects($this->any()) -+ ->method('getTypeId') -+ ->willReturn(Configurable::TYPE_CODE); -+ $configurableProductMock -+ ->expects($this->any()) -+ ->method('hasData') -+ ->with($this->equalTo('special_price')) -+ ->willReturn(false); -+ -+ /* @var AbstractItem|\PHPUnit_Framework_MockObject_MockObject $item */ -+ $item = $this->getMockBuilder(AbstractItem::class) -+ ->disableOriginalConstructor() -+ ->setMethods(['setProduct', 'getProduct', 'getChildren']) -+ ->getMockForAbstractClass(); -+ $item->expects($this->any()) -+ ->method('getProduct') -+ ->willReturn($configurableProductMock); -+ -+ $simpleProductMock = $this->createProductMock(); -+ $simpleProductMock -+ ->expects($this->any()) -+ ->method('getTypeId') -+ ->willReturn(Type::TYPE_SIMPLE); -+ $simpleProductMock -+ ->expects($this->any()) -+ ->method('hasData') -+ ->with($this->equalTo('special_price')) -+ ->willReturn(true); -+ -+ $childItem = $this->getMockBuilder(AbstractItem::class) -+ ->disableOriginalConstructor() -+ ->setMethods(['getProduct']) -+ ->getMockForAbstractClass(); -+ $childItem->expects($this->any()) -+ ->method('getProduct') -+ ->willReturn($simpleProductMock); -+ -+ $item->expects($this->any()) -+ ->method('getChildren') -+ ->willReturn([$childItem]); -+ $item->expects($this->once()) -+ ->method('setProduct') -+ ->with($simpleProductMock); -+ -+ $this->validator->setAttribute('special_price'); -+ -+ $this->validatorPlugin->beforeValidate($this->validator, $item); -+ } -+ -+ /** -+ * @return Product|\PHPUnit_Framework_MockObject_MockObject -+ */ -+ private function createProductMock(): \PHPUnit_Framework_MockObject_MockObject -+ { -+ $productMock = $this->getMockBuilder(\Magento\Catalog\Model\Product::class) -+ ->disableOriginalConstructor() -+ ->setMethods([ -+ 'getAttribute', -+ 'getId', -+ 'setQuoteItemQty', -+ 'setQuoteItemPrice', -+ 'getTypeId', -+ 'hasData', -+ ]) -+ ->getMock(); -+ $productMock -+ ->expects($this->any()) -+ ->method('setQuoteItemQty') -+ ->willReturnSelf(); -+ $productMock -+ ->expects($this->any()) -+ ->method('setQuoteItemPrice') -+ ->willReturnSelf(); -+ -+ return $productMock; -+ } -+ -+ public function testChildIsNotUsedForValidation() -+ { -+ $simpleProductMock = $this->createProductMock(); -+ $simpleProductMock -+ ->expects($this->any()) -+ ->method('getTypeId') -+ ->willReturn(Type::TYPE_SIMPLE); -+ $simpleProductMock -+ ->expects($this->any()) -+ ->method('hasData') -+ ->with($this->equalTo('special_price')) -+ ->willReturn(true); -+ -+ /* @var AbstractItem|\PHPUnit_Framework_MockObject_MockObject $item */ -+ $item = $this->getMockBuilder(AbstractItem::class) -+ ->disableOriginalConstructor() -+ ->setMethods(['setProduct', 'getProduct']) -+ ->getMockForAbstractClass(); -+ $item->expects($this->any()) -+ ->method('getProduct') -+ ->willReturn($simpleProductMock); -+ -+ $this->validator->setAttribute('special_price'); -+ -+ $this->validatorPlugin->beforeValidate($this->validator, $item); -+ } -+} -diff --git a/app/code/Magento/ConfigurableProduct/Test/Unit/Plugin/Tax/Model/Sales/Total/Quote/CommonTaxCollectorTest.php b/app/code/Magento/ConfigurableProduct/Test/Unit/Plugin/Tax/Model/Sales/Total/Quote/CommonTaxCollectorTest.php -new file mode 100644 -index 00000000000..1a5c6c0003b ---- /dev/null -+++ b/app/code/Magento/ConfigurableProduct/Test/Unit/Plugin/Tax/Model/Sales/Total/Quote/CommonTaxCollectorTest.php -@@ -0,0 +1,94 @@ -+<?php -+/** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+declare(strict_types=1); -+ -+namespace Magento\ConfigurableProduct\Test\Unit\Plugin\Tax\Model\Sales\Total\Quote; -+ -+use Magento\Catalog\Model\Product; -+use Magento\ConfigurableProduct\Model\Product\Type\Configurable; -+use Magento\ConfigurableProduct\Plugin\Tax\Model\Sales\Total\Quote\CommonTaxCollector as CommonTaxCollectorPlugin; -+use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; -+use Magento\Quote\Model\Quote\Item\AbstractItem; -+use Magento\Tax\Api\Data\QuoteDetailsItemInterface; -+use Magento\Tax\Api\Data\QuoteDetailsItemInterfaceFactory; -+use Magento\Tax\Api\Data\TaxClassKeyInterface; -+use Magento\Tax\Model\Sales\Total\Quote\CommonTaxCollector; -+use PHPUnit\Framework\MockObject\MockObject; -+ -+/** -+ * Test for CommonTaxCollector plugin -+ */ -+class CommonTaxCollectorTest extends \PHPUnit\Framework\TestCase -+{ -+ /** -+ * @var ObjectManager -+ */ -+ private $objectManager; -+ -+ /** -+ * @var CommonTaxCollectorPlugin -+ */ -+ private $commonTaxCollectorPlugin; -+ -+ /** -+ * @inheritdoc -+ */ -+ public function setUp() -+ { -+ $this->objectManager = new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this); -+ $this->commonTaxCollectorPlugin = $this->objectManager->getObject(CommonTaxCollectorPlugin::class); -+ } -+ -+ /** -+ * Test to apply Tax Class Id from child item for configurable product -+ */ -+ public function testAfterMapItem() -+ { -+ $childTaxClassId = 10; -+ -+ /** @var Product|MockObject $childProductMock */ -+ $childProductMock = $this->createPartialMock( -+ Product::class, -+ ['getTaxClassId'] -+ ); -+ $childProductMock->method('getTaxClassId')->willReturn($childTaxClassId); -+ /* @var AbstractItem|MockObject $quoteItemMock */ -+ $childQuoteItemMock = $this->createMock( -+ AbstractItem::class -+ ); -+ $childQuoteItemMock->method('getProduct')->willReturn($childProductMock); -+ -+ /** @var Product|MockObject $productMock */ -+ $productMock = $this->createPartialMock( -+ Product::class, -+ ['getTypeId'] -+ ); -+ $productMock->method('getTypeId')->willReturn(Configurable::TYPE_CODE); -+ /* @var AbstractItem|MockObject $quoteItemMock */ -+ $quoteItemMock = $this->createPartialMock( -+ AbstractItem::class, -+ ['getProduct', 'getHasChildren', 'getChildren', 'getQuote', 'getAddress', 'getOptionByCode'] -+ ); -+ $quoteItemMock->method('getProduct')->willReturn($productMock); -+ $quoteItemMock->method('getHasChildren')->willReturn(true); -+ $quoteItemMock->method('getChildren')->willReturn([$childQuoteItemMock]); -+ -+ /* @var TaxClassKeyInterface|MockObject $taxClassObjectMock */ -+ $taxClassObjectMock = $this->createMock(TaxClassKeyInterface::class); -+ $taxClassObjectMock->expects($this->once())->method('setValue')->with($childTaxClassId); -+ -+ /* @var QuoteDetailsItemInterface|MockObject $quoteDetailsItemMock */ -+ $quoteDetailsItemMock = $this->createMock(QuoteDetailsItemInterface::class); -+ $quoteDetailsItemMock->method('getTaxClassKey')->willReturn($taxClassObjectMock); -+ -+ $this->commonTaxCollectorPlugin->afterMapItem( -+ $this->createMock(CommonTaxCollector::class), -+ $quoteDetailsItemMock, -+ $this->createMock(QuoteDetailsItemInterfaceFactory::class), -+ $quoteItemMock -+ ); -+ } -+} -diff --git a/app/code/Magento/ConfigurableProduct/Ui/DataProvider/Product/Form/Modifier/ConfigurablePanel.php b/app/code/Magento/ConfigurableProduct/Ui/DataProvider/Product/Form/Modifier/ConfigurablePanel.php -index fbab25ff1be..e0cc83922e0 100644 ---- a/app/code/Magento/ConfigurableProduct/Ui/DataProvider/Product/Form/Modifier/ConfigurablePanel.php -+++ b/app/code/Magento/ConfigurableProduct/Ui/DataProvider/Product/Form/Modifier/ConfigurablePanel.php -@@ -5,14 +5,14 @@ - */ - namespace Magento\ConfigurableProduct\Ui\DataProvider\Product\Form\Modifier; - -+use Magento\Catalog\Model\Locator\LocatorInterface; - use Magento\Catalog\Model\Product\Attribute\Backend\Sku; - use Magento\Catalog\Ui\DataProvider\Product\Form\Modifier\AbstractModifier; -+use Magento\Framework\UrlInterface; - use Magento\Ui\Component\Container; --use Magento\Ui\Component\Form; - use Magento\Ui\Component\DynamicRows; -+use Magento\Ui\Component\Form; - use Magento\Ui\Component\Modal; --use Magento\Framework\UrlInterface; --use Magento\Catalog\Model\Locator\LocatorInterface; - - /** - * Data provider for Configurable panel -@@ -90,7 +90,7 @@ class ConfigurablePanel extends AbstractModifier - } - - /** -- * {@inheritdoc} -+ * @inheritdoc - */ - public function modifyData(array $data) - { -@@ -98,7 +98,7 @@ class ConfigurablePanel extends AbstractModifier - } - - /** -- * {@inheritdoc} -+ * @inheritdoc - * @SuppressWarnings(PHPMD.ExcessiveMethodLength) - */ - public function modifyMeta(array $meta) -@@ -197,7 +197,7 @@ class ConfigurablePanel extends AbstractModifier - 'autoRender' => false, - 'componentType' => 'insertListing', - 'component' => 'Magento_ConfigurableProduct/js' -- .'/components/associated-product-insert-listing', -+ . '/components/associated-product-insert-listing', - 'dataScope' => $this->associatedListingPrefix - . static::ASSOCIATED_PRODUCT_LISTING, - 'externalProvider' => $this->associatedListingPrefix -@@ -328,14 +328,12 @@ class ConfigurablePanel extends AbstractModifier - 'component' => 'Magento_Ui/js/form/components/button', - 'actions' => [ - [ -- 'targetName' => -- $this->dataScopeName . '.configurableModal', -+ 'targetName' => $this->dataScopeName . '.configurableModal', - 'actionName' => 'trigger', - 'params' => ['active', true], - ], - [ -- 'targetName' => -- $this->dataScopeName . '.configurableModal', -+ 'targetName' => $this->dataScopeName . '.configurableModal', - 'actionName' => 'openModal', - ], - ], -@@ -471,8 +469,7 @@ class ConfigurablePanel extends AbstractModifier - 'sku', - __('SKU'), - [ -- 'validation' => -- [ -+ 'validation' => [ - 'required-entry' => true, - 'max_text_length' => Sku::SKU_MAX_LENGTH, - ], -@@ -577,6 +574,7 @@ class ConfigurablePanel extends AbstractModifier - 'dataType' => Form\Element\DataType\Text::NAME, - 'dataScope' => $name, - 'visibleIfCanEdit' => false, -+ 'labelVisible' => false, - 'imports' => [ - 'visible' => '!${$.provider}:${$.parentScope}.canEdit' - ], -@@ -595,6 +593,7 @@ class ConfigurablePanel extends AbstractModifier - 'component' => 'Magento_Ui/js/form/components/group', - 'label' => $label, - 'dataScope' => '', -+ 'showLabel' => false - ]; - $container['children'] = [ - $name . '_edit' => $fieldEdit, -diff --git a/app/code/Magento/ConfigurableProduct/composer.json b/app/code/Magento/ConfigurableProduct/composer.json -index b03a8a65702..76096fd6bdf 100644 ---- a/app/code/Magento/ConfigurableProduct/composer.json -+++ b/app/code/Magento/ConfigurableProduct/composer.json -@@ -22,9 +22,11 @@ - "magento/module-msrp": "*", - "magento/module-webapi": "*", - "magento/module-sales": "*", -+ "magento/module-sales-rule": "*", - "magento/module-product-video": "*", - "magento/module-configurable-sample-data": "*", -- "magento/module-product-links-sample-data": "*" -+ "magento/module-product-links-sample-data": "*", -+ "magento/module-tax": "*" - }, - "type": "magento2-module", - "license": [ -diff --git a/app/code/Magento/ConfigurableProduct/etc/db_schema.xml b/app/code/Magento/ConfigurableProduct/etc/db_schema.xml -index 7c6661a5f39..d6917e8c184 100644 ---- a/app/code/Magento/ConfigurableProduct/etc/db_schema.xml -+++ b/app/code/Magento/ConfigurableProduct/etc/db_schema.xml -@@ -17,13 +17,13 @@ - default="0" comment="Attribute ID"/> - <column xsi:type="smallint" name="position" padding="5" unsigned="true" nullable="false" identity="false" - default="0" comment="Position"/> -- <constraint xsi:type="primary" name="PRIMARY"> -+ <constraint xsi:type="primary" referenceId="PRIMARY"> - <column name="product_super_attribute_id"/> - </constraint> -- <constraint xsi:type="foreign" name="CAT_PRD_SPR_ATTR_PRD_ID_CAT_PRD_ENTT_ENTT_ID" -+ <constraint xsi:type="foreign" referenceId="CAT_PRD_SPR_ATTR_PRD_ID_CAT_PRD_ENTT_ENTT_ID" - table="catalog_product_super_attribute" column="product_id" referenceTable="catalog_product_entity" - referenceColumn="entity_id" onDelete="CASCADE"/> -- <constraint xsi:type="unique" name="CATALOG_PRODUCT_SUPER_ATTRIBUTE_PRODUCT_ID_ATTRIBUTE_ID"> -+ <constraint xsi:type="unique" referenceId="CATALOG_PRODUCT_SUPER_ATTRIBUTE_PRODUCT_ID_ATTRIBUTE_ID"> - <column name="product_id"/> - <column name="attribute_id"/> - </constraint> -@@ -39,21 +39,21 @@ - <column xsi:type="smallint" name="use_default" padding="5" unsigned="true" nullable="true" identity="false" - default="0" comment="Use Default Value"/> - <column xsi:type="varchar" name="value" nullable="true" length="255" comment="Value"/> -- <constraint xsi:type="primary" name="PRIMARY"> -+ <constraint xsi:type="primary" referenceId="PRIMARY"> - <column name="value_id"/> - </constraint> -- <constraint xsi:type="foreign" name="FK_309442281DF7784210ED82B2CC51E5D5" -+ <constraint xsi:type="foreign" referenceId="FK_309442281DF7784210ED82B2CC51E5D5" - table="catalog_product_super_attribute_label" column="product_super_attribute_id" - referenceTable="catalog_product_super_attribute" referenceColumn="product_super_attribute_id" - onDelete="CASCADE"/> -- <constraint xsi:type="foreign" name="CATALOG_PRODUCT_SUPER_ATTRIBUTE_LABEL_STORE_ID_STORE_STORE_ID" -+ <constraint xsi:type="foreign" referenceId="CATALOG_PRODUCT_SUPER_ATTRIBUTE_LABEL_STORE_ID_STORE_STORE_ID" - table="catalog_product_super_attribute_label" column="store_id" referenceTable="store" - referenceColumn="store_id" onDelete="CASCADE"/> -- <constraint xsi:type="unique" name="CAT_PRD_SPR_ATTR_LBL_PRD_SPR_ATTR_ID_STORE_ID"> -+ <constraint xsi:type="unique" referenceId="CAT_PRD_SPR_ATTR_LBL_PRD_SPR_ATTR_ID_STORE_ID"> - <column name="product_super_attribute_id"/> - <column name="store_id"/> - </constraint> -- <index name="CATALOG_PRODUCT_SUPER_ATTRIBUTE_LABEL_STORE_ID" indexType="btree"> -+ <index referenceId="CATALOG_PRODUCT_SUPER_ATTRIBUTE_LABEL_STORE_ID" indexType="btree"> - <column name="store_id"/> - </index> - </table> -@@ -65,20 +65,20 @@ - default="0" comment="Product ID"/> - <column xsi:type="int" name="parent_id" padding="10" unsigned="true" nullable="false" identity="false" - default="0" comment="Parent ID"/> -- <constraint xsi:type="primary" name="PRIMARY"> -+ <constraint xsi:type="primary" referenceId="PRIMARY"> - <column name="link_id"/> - </constraint> -- <constraint xsi:type="foreign" name="CAT_PRD_SPR_LNK_PRD_ID_CAT_PRD_ENTT_ENTT_ID" -+ <constraint xsi:type="foreign" referenceId="CAT_PRD_SPR_LNK_PRD_ID_CAT_PRD_ENTT_ENTT_ID" - table="catalog_product_super_link" column="product_id" referenceTable="catalog_product_entity" - referenceColumn="entity_id" onDelete="CASCADE"/> -- <constraint xsi:type="foreign" name="CAT_PRD_SPR_LNK_PARENT_ID_CAT_PRD_ENTT_ENTT_ID" -+ <constraint xsi:type="foreign" referenceId="CAT_PRD_SPR_LNK_PARENT_ID_CAT_PRD_ENTT_ENTT_ID" - table="catalog_product_super_link" column="parent_id" referenceTable="catalog_product_entity" - referenceColumn="entity_id" onDelete="CASCADE"/> -- <constraint xsi:type="unique" name="CATALOG_PRODUCT_SUPER_LINK_PRODUCT_ID_PARENT_ID"> -+ <constraint xsi:type="unique" referenceId="CATALOG_PRODUCT_SUPER_LINK_PRODUCT_ID_PARENT_ID"> - <column name="product_id"/> - <column name="parent_id"/> - </constraint> -- <index name="CATALOG_PRODUCT_SUPER_LINK_PARENT_ID" indexType="btree"> -+ <index referenceId="CATALOG_PRODUCT_SUPER_LINK_PARENT_ID" indexType="btree"> - <column name="parent_id"/> - </index> - </table> -diff --git a/app/code/Magento/ConfigurableProduct/etc/di.xml b/app/code/Magento/ConfigurableProduct/etc/di.xml -index a16feacc4f9..8cec84abc4f 100644 ---- a/app/code/Magento/ConfigurableProduct/etc/di.xml -+++ b/app/code/Magento/ConfigurableProduct/etc/di.xml -@@ -125,6 +125,13 @@ - </argument> - </arguments> - </type> -+ <type name="Magento\Sales\Model\Order\ProductOption"> -+ <arguments> -+ <argument name="processorPool" xsi:type="array"> -+ <item name="configurable" xsi:type="object">Magento\ConfigurableProduct\Model\ProductOptionProcessor</item> -+ </argument> -+ </arguments> -+ </type> - <virtualType name="ConfigurableFinalPriceResolver" type="Magento\ConfigurableProduct\Pricing\Price\ConfigurablePriceResolver"> - <arguments> - <argument name="priceResolver" xsi:type="object">Magento\ConfigurableProduct\Pricing\Price\FinalPriceResolver</argument> -@@ -197,6 +204,13 @@ - <argument name="productIndexer" xsi:type="object">Magento\Catalog\Model\Indexer\Product\Full</argument> - </arguments> - </type> -+ <virtualType name="LinkedProductSelectBuilderByIndexMinPrice" type="Magento\ConfigurableProduct\Model\ResourceModel\Product\LinkedProductSelectBuilderComposite"> -+ <arguments> -+ <argument name="linkedProductSelectBuilder" xsi:type="array"> -+ <item name="indexPrice" xsi:type="object">Magento\Catalog\Model\ResourceModel\Product\Indexer\LinkedProductSelectBuilderByIndexPrice</item> -+ </argument> -+ </arguments> -+ </virtualType> - <type name="Magento\ConfigurableProduct\Pricing\Price\LowestPriceOptionsProvider"> - <arguments> - <argument name="linkedProductSelectBuilder" xsi:type="object">Magento\ConfigurableProduct\Model\ResourceModel\Product\LinkedProductSelectBuilder</argument> -@@ -205,6 +219,7 @@ - <type name="Magento\ConfigurableProduct\Model\ResourceModel\Product\LinkedProductSelectBuilder"> - <arguments> - <argument name="baseSelectProcessor" xsi:type="object">Magento\ConfigurableProduct\Model\ResourceModel\Product\StockStatusBaseSelectProcessor</argument> -+ <argument name="linkedProductSelectBuilder" xsi:type="object">LinkedProductSelectBuilderByIndexMinPrice</argument> - </arguments> - </type> - <type name="Magento\Catalog\Model\Product\Pricing\Renderer\SalableResolver"> -@@ -220,4 +235,31 @@ - </argument> - </arguments> - </type> -+ <type name="Magento\SalesRule\Model\Quote\ChildrenValidationLocator"> -+ <arguments> -+ <argument name="productTypeChildrenValidationMap" xsi:type="array"> -+ <item name="configurable" xsi:type="boolean">false</item> -+ </argument> -+ </arguments> -+ </type> -+ <type name="Magento\SalesRule\Model\Rule\Condition\Product"> -+ <plugin name="apply_rule_on_configurable_children" type="Magento\ConfigurableProduct\Plugin\SalesRule\Model\Rule\Condition\Product" /> -+ </type> -+ <type name="Magento\Tax\Model\Sales\Total\Quote\CommonTaxCollector"> -+ <plugin name="apply_tax_class_id" type="Magento\ConfigurableProduct\Plugin\Tax\Model\Sales\Total\Quote\CommonTaxCollector" /> -+ </type> -+ <type name="Magento\Eav\Model\Entity\Attribute\Group"> -+ <arguments> -+ <argument name="reservedSystemNames" xsi:type="array"> -+ <item name="configurable" xsi:type="string">configurable</item> -+ </argument> -+ </arguments> -+ </type> -+ <type name="Magento\CatalogInventory\Observer\SaveInventoryDataObserver"> -+ <arguments> -+ <argument name="parentItemProcessorPool" xsi:type="array"> -+ <item name="configurable" xsi:type="object"> Magento\ConfigurableProduct\Model\Inventory\ParentItemProcessor</item> -+ </argument> -+ </arguments> -+ </type> - </config> -diff --git a/app/code/Magento/ConfigurableProduct/etc/frontend/di.xml b/app/code/Magento/ConfigurableProduct/etc/frontend/di.xml -index bb830c36b92..df96829b354 100644 ---- a/app/code/Magento/ConfigurableProduct/etc/frontend/di.xml -+++ b/app/code/Magento/ConfigurableProduct/etc/frontend/di.xml -@@ -10,4 +10,7 @@ - <type name="Magento\ConfigurableProduct\Model\ResourceModel\Attribute\OptionSelectBuilderInterface"> - <plugin name="Magento_ConfigurableProduct_Plugin_Model_ResourceModel_Attribute_InStockOptionSelectBuilder" type="Magento\ConfigurableProduct\Plugin\Model\ResourceModel\Attribute\InStockOptionSelectBuilder"/> - </type> -+ <type name="Magento\Catalog\Model\Product"> -+ <plugin name="product_identities_extender" type="Magento\ConfigurableProduct\Model\Plugin\Frontend\ProductIdentitiesExtender" /> -+ </type> - </config> -diff --git a/app/code/Magento/ConfigurableProduct/view/adminhtml/templates/catalog/product/attribute/new/created.phtml b/app/code/Magento/ConfigurableProduct/view/adminhtml/templates/catalog/product/attribute/new/created.phtml -index 110defd5248..9307da21e66 100644 ---- a/app/code/Magento/ConfigurableProduct/view/adminhtml/templates/catalog/product/attribute/new/created.phtml -+++ b/app/code/Magento/ConfigurableProduct/view/adminhtml/templates/catalog/product/attribute/new/created.phtml -@@ -8,7 +8,7 @@ - <script> - (function ($) { - -- var data = <?= /* @escapeNotVerified */ $block->getAttributesBlockJson() ?>; -+ var data = <?= /* @noEscape */ $block->getAttributesBlockJson() ?>; - var set = data.set || {id: $('#attribute_set_id').val()}; - if (data.tab == 'variations') { - $('[data-role=product-variations-matrix]').trigger('add', data.attribute); -diff --git a/app/code/Magento/ConfigurableProduct/view/adminhtml/templates/catalog/product/attribute/set/js.phtml b/app/code/Magento/ConfigurableProduct/view/adminhtml/templates/catalog/product/attribute/set/js.phtml -index 9c4612c972d..5f49d5eb474 100644 ---- a/app/code/Magento/ConfigurableProduct/view/adminhtml/templates/catalog/product/attribute/set/js.phtml -+++ b/app/code/Magento/ConfigurableProduct/view/adminhtml/templates/catalog/product/attribute/set/js.phtml -@@ -5,8 +5,6 @@ - * See COPYING.txt for license details. - */ - --// @codingStandardsIgnoreFile -- - ?> - <script> - require([ -diff --git a/app/code/Magento/ConfigurableProduct/view/adminhtml/templates/catalog/product/composite/fieldset/configurable.phtml b/app/code/Magento/ConfigurableProduct/view/adminhtml/templates/catalog/product/composite/fieldset/configurable.phtml -index a8712cdc183..1166adca972 100644 ---- a/app/code/Magento/ConfigurableProduct/view/adminhtml/templates/catalog/product/composite/fieldset/configurable.phtml -+++ b/app/code/Magento/ConfigurableProduct/view/adminhtml/templates/catalog/product/composite/fieldset/configurable.phtml -@@ -3,39 +3,36 @@ - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -- --// @codingStandardsIgnoreFile -- -- ?> -+// phpcs:disable Magento2.Templates.ThisInTemplate.FoundThis -+?> - - <?php /* @var $block \Magento\ConfigurableProduct\Block\Adminhtml\Product\Composite\Fieldset\Configurable */ ?> - <?php $_product = $block->getProduct(); ?> - <?php $_attributes = $block->decorateArray($block->getAllowAttributes()); ?> --<?php $_skipSaleableCheck = $this->helper('Magento\Catalog\Helper\Product')->getSkipSaleableCheck(); ?> --<?php if (($_product->isSaleable() || $_skipSaleableCheck) && count($_attributes)):?> -+<?php $_skipSaleableCheck = $this->helper(Magento\Catalog\Helper\Product::class)->getSkipSaleableCheck(); ?> -+<?php if (($_product->isSaleable() || $_skipSaleableCheck) && count($_attributes)) :?> - <fieldset id="catalog_product_composite_configure_fields_configurable" class="fieldset admin__fieldset"> - <legend class="legend admin__legend"> -- <span><?= /* @escapeNotVerified */ __('Associated Products') ?></span> -+ <span><?= $block->escapeHtml(__('Associated Products')) ?></span> - </legend> -- <div class="product-options"> -- <div class="field admin__field _required required"> -- <?php foreach ($_attributes as $_attribute): ?> -- <label class="label admin__field-label"><?php -- /* @escapeNotVerified */ echo $_attribute->getProductAttribute() -- ->getStoreLabel($_product->getStoreId()); -- ?></label> -+ <div class="product-options fieldset admin__fieldset"> -+ <?php foreach ($_attributes as $_attribute) : ?> -+ <div class="field admin__field _required required"> -+ <label class="label admin__field-label"><?= -+ $block->escapeHtml($_attribute->getProductAttribute()->getStoreLabel($_product->getStoreId())); -+ ?></label> - <div class="control admin__field-control <?php -- if ($_attribute->getDecoratedIsLast()): -- ?> last<?php -+ if ($_attribute->getDecoratedIsLast()) : -+ ?> last<?php - endif; ?>"> -- <select name="super_attribute[<?= /* @escapeNotVerified */ $_attribute->getAttributeId() ?>]" -- id="attribute<?= /* @escapeNotVerified */ $_attribute->getAttributeId() ?>" -+ <select name="super_attribute[<?= $block->escapeHtmlAttr($_attribute->getAttributeId()) ?>]" -+ id="attribute<?= $block->escapeHtmlAttr($_attribute->getAttributeId()) ?>" - class="admin__control-select required-entry super-attribute-select"> -- <option><?= /* @escapeNotVerified */ __('Choose an Option...') ?></option> -+ <option><?= $block->escapeHtml(__('Choose an Option...')) ?></option> - </select> - </div> -- <?php endforeach; ?> -- </div> -+ </div> -+ <?php endforeach; ?> - </div> - </fieldset> - <script> -@@ -44,7 +41,7 @@ require([ - "Magento_Catalog/catalog/product/composite/configure" - ], function(){ - -- var config = <?= /* @escapeNotVerified */ $block->getJsonConfig() ?>; -+ var config = <?= /* @noEscape */ $block->getJsonConfig() ?>; - if (window.productConfigure) { - config.containerId = window.productConfigure.blockFormFields.id; - if (window.productConfigure.restorePhase) { -diff --git a/app/code/Magento/ConfigurableProduct/view/adminhtml/templates/catalog/product/edit/attribute/steps/attributes_values.phtml b/app/code/Magento/ConfigurableProduct/view/adminhtml/templates/catalog/product/edit/attribute/steps/attributes_values.phtml -index cc254740491..e996df82607 100644 ---- a/app/code/Magento/ConfigurableProduct/view/adminhtml/templates/catalog/product/edit/attribute/steps/attributes_values.phtml -+++ b/app/code/Magento/ConfigurableProduct/view/adminhtml/templates/catalog/product/edit/attribute/steps/attributes_values.phtml -@@ -4,18 +4,16 @@ - * See COPYING.txt for license details. - */ - --// @codingStandardsIgnoreFile -- - /* @var $block \Magento\ConfigurableProduct\Block\Adminhtml\Product\Steps\AttributeValues */ - ?> - <div data-bind="scope: '<?= /* @noEscape */ $block->getComponentName() ?>'"> - <h2 class="steps-wizard-title"><?= $block->escapeHtml( -- __('Step 2: Attribute Values') -- ); ?></h2> -+ __('Step 2: Attribute Values') -+ ); ?></h2> - <div class="steps-wizard-info"> - <span><?= $block->escapeHtml( -- __('Select values from each attribute to include in this product. Each unique combination of values creates a unique product SKU.') -- );?></span> -+ __('Select values from each attribute to include in this product. Each unique combination of values creates a unique product SKU.') -+ );?></span> - </div> - <div data-bind="foreach: attributes, sortableList: attributes"> - -@@ -41,24 +39,24 @@ - data-bind="click: $parent.selectAllAttributes" - title="<?= $block->escapeHtml(__('Select All')) ?>"> - <span><?= $block->escapeHtml( -- __('Select All') -- ); ?></span> -+ __('Select All') -+ ); ?></span> - </button> - <button type="button" - class="action-deselect-all action-tertiary" - data-bind="click: $parent.deSelectAllAttributes" - title="<?= $block->escapeHtml(__('Deselect All')) ?>"> - <span><?= $block->escapeHtml( -- __('Deselect All') -- ); ?></span> -+ __('Deselect All') -+ ); ?></span> - </button> - <button type="button" - class="action-remove-all action-tertiary" - data-bind="click: $parent.removeAttribute.bind($parent)" - title="<?= $block->escapeHtml(__('Remove Attribute')) ?>"> - <span><?= $block->escapeHtml( -- __('Remove Attribute') -- ); ?></span> -+ __('Remove Attribute') -+ ); ?></span> - </button> - </div> - </div> -@@ -74,7 +72,7 @@ - <label data-bind="text: label, visible: label, attr:{for:id}" - class="admin__field-label"></label> - </div> -- <div class="admin__field admin__field-create-new" data-bind="visible: !label"> -+ <div class="admin__field admin__field-create-new" data-bind="attr:{'data-role':id}, visible: !label"> - <div class="admin__field-control"> - <input class="admin__control-text" - name="label" -@@ -87,8 +85,8 @@ - data-action="save" - data-bind="click: $parents[1].saveOption.bind($parent)"> - <span><?= $block->escapeHtml( -- __('Save Option') -- ); ?></span> -+ __('Save Option') -+ ); ?></span> - </button> - <button type="button" - class="action-remove" -@@ -96,8 +94,8 @@ - data-action="remove" - data-bind="click: $parents[1].removeOption.bind($parent)"> - <span><?= $block->escapeHtml( -- __('Remove Option') -- ); ?></span> -+ __('Remove Option') -+ ); ?></span> - </button> - </div> - </li> -@@ -108,8 +106,8 @@ - data-action="addOption" - data-bind="click: $parent.createOption, visible: canCreateOption"> - <span><?= $block->escapeHtml( -- __('Create New Value') -- ); ?></span> -+ __('Create New Value') -+ ); ?></span> - </button> - </div> - </div> -diff --git a/app/code/Magento/ConfigurableProduct/view/adminhtml/templates/catalog/product/edit/attribute/steps/bulk.phtml b/app/code/Magento/ConfigurableProduct/view/adminhtml/templates/catalog/product/edit/attribute/steps/bulk.phtml -index 9d144b0f569..a792a35da80 100644 ---- a/app/code/Magento/ConfigurableProduct/view/adminhtml/templates/catalog/product/edit/attribute/steps/bulk.phtml -+++ b/app/code/Magento/ConfigurableProduct/view/adminhtml/templates/catalog/product/edit/attribute/steps/bulk.phtml -@@ -3,24 +3,20 @@ - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -- --// @codingStandardsIgnoreFile -- -+// phpcs:disable Magento2.Templates.ThisInTemplate.FoundThis - /* @var $block \Magento\ConfigurableProduct\Block\Adminhtml\Product\Steps\Bulk */ - ?> - - <div data-bind="scope: '<?= /* @noEscape */ $block->getComponentName() ?>'" data-role="bulk-step"> - <h2 class="steps-wizard-title"><?= $block->escapeHtml(__('Step 3: Bulk Images, Price and Quantity')) ?></h2> - <div class="steps-wizard-info"> -- <?= /* @escapeNotVerified */ __('Based on your selections %1 new products will be created. Use this step to customize images and price for your new products.', '<span class="new-products-count" data-bind="text:countVariations"></span>') ?> -+ <?= /* @noEscape */ __('Based on your selections %1 new products will be created. Use this step to customize images and price for your new products.', '<span class="new-products-count" data-bind="text:countVariations"></span>') ?> - </div> - - <div data-bind="with: sections().images" class="steps-wizard-section"> - <div data-role="section"> - <div class="steps-wizard-section-title"> -- <span><?= $block->escapeHtml( -- __('Images') -- ); ?></span> -+ <span><?= $block->escapeHtml(__('Images')); ?></span> - </div> - - <ul class="steps-wizard-section-list"> -@@ -32,9 +28,7 @@ - value="single" - data-bind="checked:type"> - <label for="apply-single-set-radio" class="admin__field-label"> -- <span><?= $block->escapeHtml( -- __('Apply single set of images to all SKUs') -- ); ?></span> -+ <span><?= $block->escapeHtml(__('Apply single set of images to all SKUs')); ?></span> - </label> - </div> - </li> -@@ -71,10 +65,7 @@ - <div data-role="gallery" - class="gallery" - data-images="[]" -- data-types="<?= $block->escapeHtml( -- $this->helper('Magento\Framework\Json\Helper\Data')->jsonEncode($block->getImageTypes()) -- ) ?>" -- > -+ data-types="<?= $block->escapeHtml($this->helper(Magento\Framework\Json\Helper\Data::class)->jsonEncode($block->getImageTypes())) ?>"> - <div class="image image-placeholder"> - <div data-role="uploader" class="uploader"> - <div class="image-browse"> -@@ -92,15 +83,12 @@ - </div> - </div> - -- <?php foreach ($block->getImageTypes() as $typeData): -- ?> -+ <?php foreach ($block->getImageTypes() as $typeData) : ?> - <input name="<?= $block->escapeHtml($typeData['name']) ?>" - class="image-<?= $block->escapeHtml($typeData['code']) ?>" - type="hidden" - value="<?= $block->escapeHtml($typeData['value']) ?>"/> -- <?php -- endforeach; -- ?> -+ <?php endforeach; ?> - - <script data-template="uploader" type="text/x-magento-template"> - <div id="<%- data.id %>" class="file-row"> -@@ -155,19 +143,12 @@ - </div> - </div> - <ul class="item-roles" data-role="roles-labels"> -- <?php -- foreach ($block->getMediaAttributes() as $attribute): -- ?> -- <li data-role-code="<?= $block->escapeHtml( -- $attribute->getAttributeCode() -- ) ?>" class="item-role item-role-<?= $block->escapeHtml( -- $attribute->getAttributeCode() -- ) ?>"> -+ <?php foreach ($block->getMediaAttributes() as $attribute) :?> -+ <li data-role-code="<?= $block->escapeHtml($attribute->getAttributeCode()) ?>" -+ class="item-role item-role-<?= $block->escapeHtml($attribute->getAttributeCode()) ?>"> - <?= /* @noEscape */ $attribute->getFrontendLabel() ?> - </li> -- <?php -- endforeach; -- ?> -+ <?php endforeach; ?> - </ul> - </div> - </script> -@@ -229,9 +210,7 @@ - - <div class="admin__field field-image-role"> - <label class="admin__field-label"> -- <span><?= $block->escapeHtml( -- __('Role') -- ); ?></span> -+ <span><?= $block->escapeHtml(__('Role')); ?></span> - </label> - <div class="admin__field-control"> - <ul class="multiselect-alt"> -@@ -243,42 +222,28 @@ - <input class="image-type" - data-role="type-selector" - type="checkbox" -- value="<?= $block->escapeHtml( -- $attribute->getAttributeCode() -- ) ?>" -+ value="<?= $block->escapeHtml($attribute->getAttributeCode()) ?>" - /> -- <?= $block->escapeHtml( -- $attribute->getFrontendLabel() -- ); ?> -+ <?= $block->escapeHtml($attribute->getFrontendLabel()); ?> - </label> - </li> -- <?php -- endforeach; -- ?> -+ <?php endforeach; ?> - </ul> - </div> - </div> - - <div class="admin__field admin__field-inline field-image-size" data-role="size"> - <label class="admin__field-label"> -- <span><?= $block->escapeHtml( -- __('Image Size') -- ); ?></span> -+ <span><?= $block->escapeHtml(__('Image Size')); ?></span> - </label> -- <div class="admin__field-value" data-message="<?= $block->escapeHtml( -- __('{size}') -- );?>"></div> -+ <div class="admin__field-value" data-message="<?= $block->escapeHtml(__('{size}'));?>"></div> - </div> - - <div class="admin__field admin__field-inline field-image-resolution" data-role="resolution"> - <label class="admin__field-label"> -- <span><?= $block->escapeHtml( -- __('Image Resolution') -- ); ?></span> -+ <span><?= $block->escapeHtml(__('Image Resolution')); ?></span> - </label> -- <div class="admin__field-value" data-message="<?= $block->escapeHtml( -- __('{width}^{height} px') -- );?>"></div> -+ <div class="admin__field-value" data-message="<?= $block->escapeHtml(__('{width}^{height} px'));?>"></div> - </div> - - <div class="admin__field field-image-hide"> -@@ -293,9 +258,7 @@ - <% if (data.disabled == 1) { %>checked="checked"<% } %> /> - - <label for="hide-from-product-page" class="admin__field-label"> -- <?= $block->escapeHtml( -- __('Hide from Product Page') -- ); ?> -+ <?= $block->escapeHtml(__('Hide from Product Page')); ?> - </label> - </div> - </div> -@@ -310,9 +273,7 @@ - <fieldset class="admin__fieldset bulk-attribute-values"> - <div class="admin__field _required"> - <label class="admin__field-label" for="apply-images-attributes"> -- <span><?= $block->escapeHtml( -- __('Select attribute') -- ); ?></span> -+ <span><?= $block->escapeHtml(__('Select attribute')); ?></span> - </label> - <div class="admin__field-control"> - <select -@@ -322,9 +283,7 @@ - options: $parent.attributes, - optionsText: 'label', - value: attribute, -- optionsCaption: '<?= $block->escapeHtml( -- __("Select") -- ); ?>' -+ optionsCaption: '<?= $block->escapeHtml(__("Select")); ?>' - "> - </select> - </div> -@@ -341,24 +300,17 @@ - <div data-role="gallery" - class="gallery" - data-images="[]" -- data-types="<?= $block->escapeHtml( -- $this->helper('Magento\Framework\Json\Helper\Data')->jsonEncode($block->getImageTypes()) -- ) ?>" -- > -+ data-types="<?= $block->escapeHtml($this->helper(Magento\Framework\Json\Helper\Data::class)->jsonEncode($block->getImageTypes())) ?>"> - <div class="image image-placeholder"> - <div data-role="uploader" class="uploader"> - <div class="image-browse"> -- <span><?= $block->escapeHtml( -- __('Browse Files...') -- ); ?></span> -+ <span><?= $block->escapeHtml(__('Browse Files...')); ?></span> - <input type="file" name="image" multiple="multiple" - data-url="<?= /* @noEscape */ $block->getUrl('catalog/product_gallery/upload') ?>" /> - </div> - </div> - <div class="product-image-wrapper"> -- <p class="image-placeholder-text"><?= $block->escapeHtml( -- __('Browse to find or drag image here') -- ); ?></p> -+ <p class="image-placeholder-text"><?= $block->escapeHtml(__('Browse to find or drag image here')); ?></p> - </div> - <div class="spinner"> - <span></span><span></span><span></span><span></span> -@@ -366,15 +318,12 @@ - </div> - </div> - -- <?php foreach ($block->getImageTypes() as $typeData): -- ?> -+ <?php foreach ($block->getImageTypes() as $typeData) :?> - <input name="<?= $block->escapeHtml($typeData['name']) ?>" - class="image-<?= $block->escapeHtml($typeData['code']) ?>" - type="hidden" - value="<?= $block->escapeHtml($typeData['value']) ?>"/> -- <?php -- endforeach; -- ?> -+ <?php endforeach; ?> - - <script data-template="uploader" type="text/x-magento-template"> - <div id="<%- data.id %>" class="file-row"> -@@ -418,15 +367,11 @@ - class="action-remove" - data-role="delete-button" - title="<?= $block->escapeHtml(__('Remove image')) ?>"> -- <span><?= $block->escapeHtml( -- __('Remove image') -- ); ?></span> -+ <span><?= $block->escapeHtml(__('Remove image')); ?></span> - </button> - <div class="draggable-handle"></div> - </div> -- <div class="image-fade"><span><?= $block->escapeHtml( -- __('Hidden') -- ); ?></span></div> -+ <div class="image-fade"><span><?= $block->escapeHtml(__('Hidden')); ?></span></div> - </div> - <div class="item-description"> - <div class="item-title" data-role="img-title"><%- data.label %></div> -@@ -435,19 +380,12 @@ - </div> - </div> - <ul class="item-roles" data-role="roles-labels"> -- <?php -- foreach ($block->getMediaAttributes() as $attribute): -- ?> -- <li data-role-code="<?= $block->escapeHtml( -- $attribute->getAttributeCode() -- ) ?>" class="item-role item-role-<?= $block->escapeHtml( -- $attribute->getAttributeCode() -- ) ?>"> -+ <?php foreach ($block->getMediaAttributes() as $attribute) :?> -+ <li data-role-code="<?= $block->escapeHtml($attribute->getAttributeCode()) ?>" -+ class="item-role item-role-<?= $block->escapeHtml($attribute->getAttributeCode()) ?>"> - <?= $block->escapeHtml($attribute->getFrontendLabel()) ?> - </li> -- <?php -- endforeach; -- ?> -+ <?php endforeach; ?> - </ul> - </div> - </script> -@@ -492,9 +430,7 @@ - <fieldset class="admin__fieldset fieldset-image-panel"> - <div class="admin__field field-image-description"> - <label class="admin__field-label" for="image-description"> -- <span><?= $block->escapeHtml( -- __('Alt Text') -- );?></span> -+ <span><?= $block->escapeHtml(__('Alt Text'));?></span> - </label> - - <div class="admin__field-control"> -@@ -508,56 +444,38 @@ - - <div class="admin__field field-image-role"> - <label class="admin__field-label"> -- <span><?= $block->escapeHtml( -- __('Role') -- );?></span> -+ <span><?= $block->escapeHtml(__('Role'));?></span> - </label> - <div class="admin__field-control"> - <ul class="multiselect-alt"> -- <?php -- foreach ($block->getMediaAttributes() as $attribute) : -- ?> -+ <?php foreach ($block->getMediaAttributes() as $attribute) :?> - <li class="item"> - <label> - <input class="image-type" - data-role="type-selector" - type="checkbox" -- value="<?= $block->escapeHtml( -- $attribute->getAttributeCode() -- ) ?>" -+ value="<?= $block->escapeHtml($attribute->getAttributeCode()) ?>" - /> -- <?= $block->escapeHtml( -- $attribute->getFrontendLabel() -- ) ?> -+ <?= $block->escapeHtml($attribute->getFrontendLabel()) ?> - </label> - </li> -- <?php -- endforeach; -- ?> -+ <?php endforeach; ?> - </ul> - </div> - </div> - - <div class="admin__field admin__field-inline field-image-size" data-role="size"> - <label class="admin__field-label"> -- <span><?= $block->escapeHtml( -- __('Image Size') -- ); ?></span> -+ <span><?= $block->escapeHtml(__('Image Size')); ?></span> - </label> -- <div class="admin__field-value" data-message="<?= $block->escapeHtml( -- __('{size}') -- ); ?>"></div> -+ <div class="admin__field-value" data-message="<?= $block->escapeHtml(__('{size}')); ?>"></div> - </div> - - <div class="admin__field admin__field-inline field-image-resolution" data-role="resolution"> - <label class="admin__field-label"> -- <span><?= $block->escapeHtml( -- __('Image Resolution') -- ); ?></span> -+ <span><?= $block->escapeHtml(__('Image Resolution')); ?></span> - </label> -- <div class="admin__field-value" data-message="<?= $block->escapeHtml( -- __('{width}^{height} px') -- ); ?>"></div> -+ <div class="admin__field-value" data-message="<?= $block->escapeHtml(__('{width}^{height} px')); ?>"></div> - </div> - - <div class="admin__field field-image-hide"> -@@ -572,9 +490,7 @@ - <% if (data.disabled == 1) { %>checked="checked"<% } %> /> - - <label for="hide-from-product-page" class="admin__field-label"> -- <?= $block->escapeHtml( -- __('Hide from Product Page') -- ); ?> -+ <?= $block->escapeHtml(__('Hide from Product Page')); ?> - </label> - </div> - </div> -@@ -593,9 +509,7 @@ - <div data-bind="with: sections().price" class="steps-wizard-section"> - <div data-role="section"> - <div class="steps-wizard-section-title"> -- <span><?= $block->escapeHtml( -- __('Price') -- ); ?></span> -+ <span><?= $block->escapeHtml(__('Price')); ?></span> - </div> - <ul class="steps-wizard-section-list"> - <li> -@@ -607,9 +521,7 @@ - data-bind="checked:type" /> - <label for="apply-single-price-radio" - class="admin__field-label"> -- <span><?= $block->escapeHtml( -- __('Apply single price to all SKUs') -- ); ?></span> -+ <span><?= $block->escapeHtml(__('Apply single price to all SKUs')); ?></span> - </label> - </div> - </li> -@@ -622,9 +534,7 @@ - data-bind="checked:type" /> - <label for="apply-unique-prices-radio" - class="admin__field-label"> -- <span><?= $block->escapeHtml( -- __('Apply unique prices by attribute to each SKU') -- ); ?></span> -+ <span><?= $block->escapeHtml(__('Apply unique prices by attribute to each SKU')); ?></span> - </label> - </div> - </li> -@@ -637,9 +547,7 @@ - checked - data-bind="checked:type" /> - <label for="skip-pricing-radio" class="admin__field-label"> -- <span><?= $block->escapeHtml( -- __('Skip price at this time') -- ); ?></span> -+ <span><?= $block->escapeHtml(__('Skip price at this time')); ?></span> - </label> - </div> - </li> -@@ -648,9 +556,7 @@ - <fieldset class="admin__fieldset bulk-attribute-values" data-bind="visible: type() == 'single'"> - <div class="admin__field _required"> - <label for="apply-single-price-input" class="admin__field-label"> -- <span><?= $block->escapeHtml( -- __('Price') -- ); ?></span> -+ <span><?= $block->escapeHtml(__('Price')); ?></span> - </label> - <div class="admin__field-control"> - <div class="currency-addon"> -@@ -667,9 +573,7 @@ - <fieldset class="admin__fieldset bulk-attribute-values"> - <div class="admin__field _required"> - <label for="select-each-price" class="admin__field-label"> -- <span><?= $block->escapeHtml( -- __('Select attribute') -- ); ?></span> -+ <span><?= $block->escapeHtml(__('Select attribute')); ?></span> - </label> - <div class="admin__field-control"> - <select id="select-each-price" class="admin__control-select" data-bind=" -diff --git a/app/code/Magento/ConfigurableProduct/view/adminhtml/templates/catalog/product/edit/attribute/steps/select_attributes.phtml b/app/code/Magento/ConfigurableProduct/view/adminhtml/templates/catalog/product/edit/attribute/steps/select_attributes.phtml -index cfb742e80f7..c3dc6142322 100644 ---- a/app/code/Magento/ConfigurableProduct/view/adminhtml/templates/catalog/product/edit/attribute/steps/select_attributes.phtml -+++ b/app/code/Magento/ConfigurableProduct/view/adminhtml/templates/catalog/product/edit/attribute/steps/select_attributes.phtml -@@ -4,8 +4,6 @@ - * See COPYING.txt for license details. - */ - --// @codingStandardsIgnoreFile -- - /* @var $block \Magento\ConfigurableProduct\Block\Adminhtml\Product\Steps\SelectAttributes */ - ?> - <div class="select-attributes-block <?= /* @noEscape */ $block->getData('config/dataScope') ?>" data-role="select-attributes-step"> -@@ -13,8 +11,8 @@ - <?= /* @noEscape */ $block->getAddNewAttributeButton() ?> - </div> - <h2 class="steps-wizard-title"><?= $block->escapeHtml( -- __('Step 1: Select Attributes') -- ); ?></h2> -+ __('Step 1: Select Attributes') -+ ); ?></h2> - <div class="selected-attributes" data-bind="scope: '<?= /* @noEscape */ $block->getComponentName() ?>'"> - <?= $block->escapeHtml( - __('Selected Attributes:') -diff --git a/app/code/Magento/ConfigurableProduct/view/adminhtml/templates/catalog/product/edit/attribute/steps/summary.phtml b/app/code/Magento/ConfigurableProduct/view/adminhtml/templates/catalog/product/edit/attribute/steps/summary.phtml -index 2ded3aa1079..379e129b68c 100644 ---- a/app/code/Magento/ConfigurableProduct/view/adminhtml/templates/catalog/product/edit/attribute/steps/summary.phtml -+++ b/app/code/Magento/ConfigurableProduct/view/adminhtml/templates/catalog/product/edit/attribute/steps/summary.phtml -@@ -4,14 +4,12 @@ - * See COPYING.txt for license details. - */ - --// @codingStandardsIgnoreFile -- - /* @var $block \Magento\ConfigurableProduct\Block\Adminhtml\Product\Steps\Summary */ - ?> - <div data-bind="scope: '<?= /* @noEscape */ $block->getComponentName() ?>'"> - <h2 class="steps-wizard-title"><?= $block->escapeHtml( -- __('Step 4: Summary') -- ); ?></h2> -+ __('Step 4: Summary') -+ ); ?></h2> - - <div class="admin__data-grid-wrap admin__data-grid-wrap-static"> - <!-- ko if: gridNew().length --> -diff --git a/app/code/Magento/ConfigurableProduct/view/adminhtml/templates/catalog/product/edit/super/config.phtml b/app/code/Magento/ConfigurableProduct/view/adminhtml/templates/catalog/product/edit/super/config.phtml -index 07f4e39e43d..c11a1adc198 100644 ---- a/app/code/Magento/ConfigurableProduct/view/adminhtml/templates/catalog/product/edit/super/config.phtml -+++ b/app/code/Magento/ConfigurableProduct/view/adminhtml/templates/catalog/product/edit/super/config.phtml -@@ -4,34 +4,32 @@ - * See COPYING.txt for license details. - */ - --// @codingStandardsIgnoreFile -- -- /** @var $block \Magento\ConfigurableProduct\Block\Adminhtml\Product\Edit\Tab\Variations\Config */ -+/** @var $block \Magento\ConfigurableProduct\Block\Adminhtml\Product\Edit\Tab\Variations\Config */ - ?> --<div class="entry-edit form-inline" id="<?= /* @escapeNotVerified */ $block->getId() ?>" data-panel="product-variations"> -+<div class="entry-edit form-inline" id="<?= $block->escapeHtmlAttr($block->getId()) ?>" data-panel="product-variations"> - <div data-bind="scope: 'variation-steps-wizard'" class="product-create-configuration"> - <div class="product-create-configuration-info"> - <div class="note" data-role="product-create-configuration-info"> -- <?= /* @escapeNotVerified */ __('Configurable products allow customers to choose options (Ex: shirt color). -- You need to create a simple product for each configuration (Ex: a product for each color).');?> -+ <?= $block->escapeHtml(__('Configurable products allow customers to choose options (Ex: shirt color). -+ You need to create a simple product for each configuration (Ex: a product for each color).'));?> - </div> - </div> - <div class="product-create-configuration-actions" data-action="product-create-configuration-buttons"> - <div class="product-create-configuration-action"> - <button type="button" data-action="open-steps-wizard" title="Create Product Configurations" - class="action-secondary" data-bind="click: open"> -- <span data-role="button-label" data-edit-label="<?= /* @escapeNotVerified */ __('Edit Configurations') ?>"> -- <?= /* @escapeNotVerified */ $block->isHasVariations() -+ <span data-role="button-label" data-edit-label="<?= $block->escapeHtmlAttr(__('Edit Configurations')) ?>"> -+ <?= $block->escapeHtml($block->isHasVariations() - ? __('Edit Configurations') -- : __('Create Configurations') -- ?> -+ : __('Create Configurations')) -+?> - </span> - </button> - </div> - <div class="product-create-configuration-action" data-bind="scope: 'configurableProductGrid'"> - <button class="action-tertiary action-menu-item" type="button" data-action="choose" - data-bind="click: showManuallyGrid, visible: button"> -- <?= /* @noEscape */ __('Add Products Manually') ?> -+ <?= $block->escapeHtml(__('Add Products Manually')) ?> - </button> - </div> - </div> -diff --git a/app/code/Magento/ConfigurableProduct/view/adminhtml/templates/catalog/product/edit/super/matrix.phtml b/app/code/Magento/ConfigurableProduct/view/adminhtml/templates/catalog/product/edit/super/matrix.phtml -index 230e0fd14cc..22ff1992c94 100644 ---- a/app/code/Magento/ConfigurableProduct/view/adminhtml/templates/catalog/product/edit/super/matrix.phtml -+++ b/app/code/Magento/ConfigurableProduct/view/adminhtml/templates/catalog/product/edit/super/matrix.phtml -@@ -3,9 +3,7 @@ - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -- --// @codingStandardsIgnoreFile -- -+// phpcs:disable Magento2.Templates.ThisInTemplate.FoundThis - /** @var $block \Magento\ConfigurableProduct\Block\Adminhtml\Product\Edit\Tab\Variations\Config\Matrix */ - ?> - <?php -@@ -17,7 +15,7 @@ $currencySymbol = $block->getCurrencySymbol(); - <div id="product-variations-matrix" data-role="product-variations-matrix"> - <div data-bind="scope: 'configurableVariations'"> - <h3 class="hidden" data-bind="css: {hidden: !showVariations() }" class="title"> -- <?= /* @escapeNotVerified */ __('Current Variations') ?> -+ <?= $block->escapeHtml(__('Current Variations')) ?> - </h3> - - <script data-template-for="variation-image" type="text/x-magento-template"> -@@ -47,22 +45,22 @@ $currencySymbol = $block->getCurrencySymbol(); - <thead> - <tr> - <th class="data-grid-th data-grid-thumbnail-cell col-image" data-column="image"> -- <?= /* @escapeNotVerified */ __('Image') ?> -+ <?= $block->escapeHtml(__('Image')) ?> - </th> - <th class="data-grid-th col-name" data-column="name"> -- <?= /* @escapeNotVerified */ __('Name') ?> -+ <?= $block->escapeHtml(__('Name')) ?> - </th> - <th class="data-grid-th col-sku" data-column="sku"> -- <?= /* @escapeNotVerified */ __('SKU') ?> -+ <?= $block->escapeHtml(__('SKU')) ?> - </th> - <th class="data-grid-th col-price" data-column="price"> -- <?= /* @escapeNotVerified */ __('Price') ?> -+ <?= $block->escapeHtml(__('Price')) ?> - </th> - <th class="data-grid-th col-qty" data-column="qty"> -- <?= /* @escapeNotVerified */ __('Quantity') ?> -+ <?= $block->escapeHtml(__('Quantity')) ?> - </th> - <th class="data-grid-th col-weight" data-column="weight"> -- <?= /* @escapeNotVerified */ __('Weight') ?> -+ <?= $block->escapeHtml(__('Weight')) ?> - </th> - <!-- ko foreach: getAttributesOptions() --> - <th data-bind="attr: {class:'data-grid-th col-' + $data.attribute_code}, -@@ -70,7 +68,7 @@ $currencySymbol = $block->getCurrencySymbol(); - </th> - <!-- /ko --> - <th class="data-grid-th"> -- <?= /* @escapeNotVerified */ __('Actions') ?> -+ <?= $block->escapeHtml(__('Actions')) ?> - </th> - </tr> - </thead> -@@ -88,7 +86,7 @@ $currencySymbol = $block->getCurrencySymbol(); - <input type="hidden" data-bind=" - attr: {id: $parent.getRowId(variation, 'image'), - name: $parent.getVariationRowName(variation, 'image')}"/> -- <span><?= /* @escapeNotVerified */ __('Upload Image') ?></span> -+ <span><?= $block->escapeHtml(__('Upload Image')) ?></span> - <input name="image" type="file" - data-url="<?= $block->escapeHtml($block->getImageUploadUrl()) ?>" - title="<?= $block->escapeHtml(__('Upload image')) ?>"/> -@@ -102,11 +100,11 @@ $currencySymbol = $block->getCurrencySymbol(); - <!-- /ko --> - <button type="button" class="action toggle no-display" data-toggle="dropdown" - data-mage-init='{"dropdown":{}}'> -- <span><?= /* @escapeNotVerified */ __('Select') ?></span> -+ <span><?= $block->escapeHtml(__('Select')) ?></span> - </button> - <ul class="dropdown"> - <li> -- <a class="item" data-action="no-image"><?= /* @escapeNotVerified */ __('No Image') ?></a> -+ <a class="item" data-action="no-image"><?= $block->escapeHtml(__('No Image')) ?></a> - </li> - </ul> - </div> -@@ -208,7 +206,7 @@ $currencySymbol = $block->getCurrencySymbol(); - " - data-action="choose" - href="#"> -- <?= /* @escapeNotVerified */ __('Choose a different Product') ?> -+ <?= $block->escapeHtml(__('Choose a different Product')) ?> - </a> - </li> - <li> -@@ -219,7 +217,7 @@ $currencySymbol = $block->getCurrencySymbol(); - </li> - <li> - <a class="action-menu-item" data-bind="click: $parent.removeProduct.bind($parent, $index())"> -- <?= /* @escapeNotVerified */ __('Remove Product') ?> -+ <?= $block->escapeHtml(__('Remove Product')) ?> - </a> - </li> - </ul> -@@ -233,15 +231,14 @@ $currencySymbol = $block->getCurrencySymbol(); - <!-- /ko --> - </div> - <div data-role="step-wizard-dialog" -- data-mage-init='{"Magento_Ui/js/modal/modal":{"type":"slide","title":"<?= /* @escapeNotVerified */ __('Create Product Configurations') ?>", -+ data-mage-init='{"Magento_Ui/js/modal/modal":{"type":"slide","title":"<?= $block->escapeJs(__('Create Product Configurations')) ?>", - "buttons":[]}}' - class="no-display"> -- <?php -- /* @escapeNotVerified */ echo $block->getVariationWizard([ -+ <?= /* @noEscape */ $block->getVariationWizard([ - 'attributes' => $attributes, - 'configurations' => $productMatrix - ]); -- ?> -+?> - </div> - </div> - -@@ -252,8 +249,8 @@ $currencySymbol = $block->getCurrencySymbol(); - "components": { - "configurableVariations": { - "component": "Magento_ConfigurableProduct/js/variations/variations", -- "variations": <?= /* @noEscape */ $this->helper('Magento\Framework\Json\Helper\Data')->jsonEncode($productMatrix) ?>, -- "productAttributes": <?= /* @noEscape */ $this->helper('Magento\Framework\Json\Helper\Data')->jsonEncode($attributes) ?>, -+ "variations": <?= /* @noEscape */ $this->helper(Magento\Framework\Json\Helper\Data::class)->jsonEncode($productMatrix) ?>, -+ "productAttributes": <?= /* @noEscape */ $this->helper(Magento\Framework\Json\Helper\Data::class)->jsonEncode($attributes) ?>, - "productUrl": "<?= /* @noEscape */ $block->getUrl('catalog/product/edit', ['id' => '%id%']) ?>", - "currencySymbol": "<?= /* @noEscape */ $currencySymbol ?>", - "configurableProductGrid": "configurableProductGrid" -diff --git a/app/code/Magento/ConfigurableProduct/view/adminhtml/templates/catalog/product/edit/super/wizard-ajax.phtml b/app/code/Magento/ConfigurableProduct/view/adminhtml/templates/catalog/product/edit/super/wizard-ajax.phtml -index 2e386332186..7b85efdbb73 100644 ---- a/app/code/Magento/ConfigurableProduct/view/adminhtml/templates/catalog/product/edit/super/wizard-ajax.phtml -+++ b/app/code/Magento/ConfigurableProduct/view/adminhtml/templates/catalog/product/edit/super/wizard-ajax.phtml -@@ -4,8 +4,6 @@ - * See COPYING.txt for license details. - */ - --// @codingStandardsIgnoreFile -- - /** @var $block \Magento\ConfigurableProduct\Block\Adminhtml\Product\Edit\Tab\Variations\Config\Matrix */ - - $productMatrix = $block->getProductMatrix(); -diff --git a/app/code/Magento/ConfigurableProduct/view/adminhtml/templates/catalog/product/edit/super/wizard.phtml b/app/code/Magento/ConfigurableProduct/view/adminhtml/templates/catalog/product/edit/super/wizard.phtml -index 3a23257cbbf..f009962bb97 100644 ---- a/app/code/Magento/ConfigurableProduct/view/adminhtml/templates/catalog/product/edit/super/wizard.phtml -+++ b/app/code/Magento/ConfigurableProduct/view/adminhtml/templates/catalog/product/edit/super/wizard.phtml -@@ -3,9 +3,7 @@ - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -- --// @codingStandardsIgnoreFile -- -+// phpcs:disable Magento2.Templates.ThisInTemplate.FoundThis - /** @var $block \Magento\ConfigurableProduct\Block\Adminhtml\Product\Edit\Tab\Variations\Config\Matrix */ - ?> - <?php -@@ -48,9 +46,9 @@ $currencySymbol = $block->getCurrencySymbol(); - "attributeSetHandler": "<?= /* @noEscape */ $block->getForm() ?>.configurable_attribute_set_handler_modal", - "wizardModalButtonName": "<?= /* @noEscape */ $block->getForm() ?>.configurable.configurable_products_button_set.create_configurable_products_button", - "wizardModalButtonTitle": "<?= $block->escapeHtml(__('Edit Configurations')) ?>", -- "productAttributes": <?= /* @noEscape */ $this->helper('Magento\Framework\Json\Helper\Data')->jsonEncode($attributes) ?>, -+ "productAttributes": <?= /* @noEscape */ $this->helper(Magento\Framework\Json\Helper\Data::class)->jsonEncode($attributes) ?>, - "productUrl": "<?= /* @noEscape */ $block->getUrl('catalog/product/edit', ['id' => '%id%']) ?>", -- "variations": <?= /* @noEscape */ $this->helper('Magento\Framework\Json\Helper\Data')->jsonEncode($productMatrix) ?>, -+ "variations": <?= /* @noEscape */ $this->helper(Magento\Framework\Json\Helper\Data::class)->jsonEncode($productMatrix) ?>, - "currencySymbol": "<?= /* @noEscape */ $currencySymbol ?>", - "attributeSetCreationUrl": "<?= /* @noEscape */ $block->getUrl('*/product_set/save') ?>" - } -diff --git a/app/code/Magento/ConfigurableProduct/view/adminhtml/templates/product/configurable/affected-attribute-set-selector/form.phtml b/app/code/Magento/ConfigurableProduct/view/adminhtml/templates/product/configurable/affected-attribute-set-selector/form.phtml -index 6c993b243da..6b30b3eba33 100644 ---- a/app/code/Magento/ConfigurableProduct/view/adminhtml/templates/product/configurable/affected-attribute-set-selector/form.phtml -+++ b/app/code/Magento/ConfigurableProduct/view/adminhtml/templates/product/configurable/affected-attribute-set-selector/form.phtml -@@ -4,8 +4,6 @@ - * See COPYING.txt for license details. - */ - --// @codingStandardsIgnoreFile -- - /* @var $block \Magento\Framework\View\Element\Template */ - ?> - <div data-role="affected-attribute-set-selector" class="no-display affected-attribute-set"> -diff --git a/app/code/Magento/ConfigurableProduct/view/adminhtml/templates/product/configurable/affected-attribute-set-selector/js.phtml b/app/code/Magento/ConfigurableProduct/view/adminhtml/templates/product/configurable/affected-attribute-set-selector/js.phtml -index 0e0eb3464ea..cdb12b54e5e 100644 ---- a/app/code/Magento/ConfigurableProduct/view/adminhtml/templates/product/configurable/affected-attribute-set-selector/js.phtml -+++ b/app/code/Magento/ConfigurableProduct/view/adminhtml/templates/product/configurable/affected-attribute-set-selector/js.phtml -@@ -4,8 +4,6 @@ - * See COPYING.txt for license details. - */ - --// @codingStandardsIgnoreFile -- - /* @var $block \Magento\ConfigurableProduct\Block\Product\Configurable\AttributeSelector */ - ?> - <script> -@@ -48,12 +46,12 @@ - - $form - .modal({ -- title: '<?= /* @escapeNotVerified */ __('Choose Affected Attribute Set') ?>', -+ title: '<?= $block->escapeJs(__('Choose Affected Attribute Set')) ?>', - closed: function () { - resetValidation(); - }, - buttons: [{ -- text: '<?= /* @escapeNotVerified */ __('Confirm') ?>', -+ text: '<?= $block->escapeJs(__('Confirm')) ?>', - attr: { - 'data-action': 'confirm' - }, -@@ -77,12 +75,12 @@ - - $.ajax({ - type: 'POST', -- url: '<?= /* @escapeNotVerified */ $block->getAttributeSetCreationUrl() ?>', -+ url: '<?= $block->escapeUrl($block->getAttributeSetCreationUrl()) ?>', - data: { - gotoEdit: 1, - attribute_set_name: $form.find('input[name=new-attribute-set-name]').val(), - skeleton_set: $('#attribute_set_id').val(), -- form_key: '<?= /* @escapeNotVerified */ $block->getFormKey() ?>', -+ form_key: '<?= $block->escapeJs($block->getFormKey()) ?>', - return_session_messages_only: 1 - }, - dataType: 'json', -@@ -101,8 +99,8 @@ - return false; - } - },{ -- text: '<?= /* @escapeNotVerified */ __('Cancel') ?>', -- id: '<?= /* @escapeNotVerified */ $block->getJsId('close-button') ?>', -+ text: '<?= $block->escapeJs(__('Cancel')) ?>', -+ id: '<?= $block->escapeJs($block->getJsId('close-button')) ?>', - 'class': 'action-close', - click: function() { - $form.modal('closeModal'); -diff --git a/app/code/Magento/ConfigurableProduct/view/adminhtml/templates/product/configurable/attribute-selector/js.phtml b/app/code/Magento/ConfigurableProduct/view/adminhtml/templates/product/configurable/attribute-selector/js.phtml -index 4246d8f53a7..e6cf1e9c687 100644 ---- a/app/code/Magento/ConfigurableProduct/view/adminhtml/templates/product/configurable/attribute-selector/js.phtml -+++ b/app/code/Magento/ConfigurableProduct/view/adminhtml/templates/product/configurable/attribute-selector/js.phtml -@@ -3,16 +3,12 @@ - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -- --// @codingStandardsIgnoreFile -- -+// phpcs:disable Magento2.Templates.ThisInTemplate.FoundThis - /** @var $block \Magento\ConfigurableProduct\Block\Product\Configurable\AttributeSelector */ - ?> - <script> - require(["jquery","mage/mage","mage/backend/suggest"], function($){ -- var options = <?php -- /* @escapeNotVerified */ echo $this->helper('Magento\Framework\Json\Helper\Data')->jsonEncode($block->getSuggestWidgetOptions()) -- ?>; -+ var options = <?= /* @noEscape */ $this->helper(Magento\Framework\Json\Helper\Data::class)->jsonEncode($block->getSuggestWidgetOptions()) ?>; - $('#configurable-attribute-selector') - .mage('suggest', options) - .on('suggestselect', function (event, ui) { -diff --git a/app/code/Magento/ConfigurableProduct/view/adminhtml/web/js/components/dynamic-rows-configurable.js b/app/code/Magento/ConfigurableProduct/view/adminhtml/web/js/components/dynamic-rows-configurable.js -index 94a24779450..7b04bebd4d7 100644 ---- a/app/code/Magento/ConfigurableProduct/view/adminhtml/web/js/components/dynamic-rows-configurable.js -+++ b/app/code/Magento/ConfigurableProduct/view/adminhtml/web/js/components/dynamic-rows-configurable.js -@@ -6,8 +6,9 @@ - define([ - 'underscore', - 'uiRegistry', -- 'Magento_Ui/js/dynamic-rows/dynamic-rows' --], function (_, registry, dynamicRows) { -+ 'Magento_Ui/js/dynamic-rows/dynamic-rows', -+ 'jquery' -+], function (_, registry, dynamicRows, $) { - 'use strict'; - - return dynamicRows.extend({ -@@ -217,6 +218,8 @@ define([ - - _.each(tmpData, function (row, index) { - path = this.dataScope + '.' + this.index + '.' + (this.startIndex + index); -+ row.attributes = $('<i></i>').text(row.attributes).html(); -+ row.sku = $('<i></i>').text(row.sku).html(); - this.source.set(path, row); - }, this); - -@@ -401,8 +404,8 @@ define([ - product = { - 'id': row.productId, - 'product_link': row.productUrl, -- 'name': row.name, -- 'sku': row.sku, -+ 'name': $('<i></i>').text(row.name).html(), -+ 'sku': $('<i></i>').text(row.sku).html(), - 'status': row.status, - 'price': row.price, - 'price_currency': row.priceCurrency, -diff --git a/app/code/Magento/ConfigurableProduct/view/adminhtml/web/js/components/price-configurable.js b/app/code/Magento/ConfigurableProduct/view/adminhtml/web/js/components/price-configurable.js -index 28e775b984b..b2ef35546ee 100644 ---- a/app/code/Magento/ConfigurableProduct/view/adminhtml/web/js/components/price-configurable.js -+++ b/app/code/Magento/ConfigurableProduct/view/adminhtml/web/js/components/price-configurable.js -@@ -11,9 +11,6 @@ define([ - - return Abstract.extend({ - defaults: { -- listens: { -- isConfigurable: 'handlePriceValue' -- }, - imports: { - isConfigurable: '!ns = ${ $.ns }, index = configurable-matrix:isEmpty' - }, -@@ -22,12 +19,15 @@ define([ - } - }, - -- /** -- * Invokes initialize method of parent class, -- * contains initialization logic -- */ -+ /** @inheritdoc */ - initialize: function () { - this._super(); -+ // resolve initial disable state -+ this.handlePriceValue(this.isConfigurable); -+ // add listener to track "configurable" type -+ this.setListeners({ -+ isConfigurable: 'handlePriceValue' -+ }); - - return this; - }, -@@ -50,8 +50,9 @@ define([ - * @param {String} isConfigurable - */ - handlePriceValue: function (isConfigurable) { -+ this.disabled(!!this.isUseDefault() || isConfigurable); -+ - if (isConfigurable) { -- this.disable(); - this.clear(); - } - } -diff --git a/app/code/Magento/ConfigurableProduct/view/adminhtml/web/js/variations/paging/sizes.js b/app/code/Magento/ConfigurableProduct/view/adminhtml/web/js/variations/paging/sizes.js -index a8b8f95f653..155b176bd43 100644 ---- a/app/code/Magento/ConfigurableProduct/view/adminhtml/web/js/variations/paging/sizes.js -+++ b/app/code/Magento/ConfigurableProduct/view/adminhtml/web/js/variations/paging/sizes.js -@@ -9,21 +9,21 @@ define([ - - return Sizes.extend({ - defaults: { -- excludedOptions: ['100', '200'] -- }, -- -- /** -- * @override -- */ -- initialize: function () { -- this._super(); -- -- this.excludedOptions.forEach(function (excludedOption) { -- delete this.options[excludedOption]; -- }, this); -- this.updateArray(); -- -- return this; -+ options: { -+ '20': { -+ value: 20, -+ label: 20 -+ }, -+ '30': { -+ value: 30, -+ label: 30 -+ }, -+ '50': { -+ value: 50, -+ label: 50 -+ } -+ }, -+ value: 20 - } - }); - }); -diff --git a/app/code/Magento/ConfigurableProduct/view/adminhtml/web/js/variations/steps/attributes_values.js b/app/code/Magento/ConfigurableProduct/view/adminhtml/web/js/variations/steps/attributes_values.js -index cc565cab6b2..6c790c634ee 100644 ---- a/app/code/Magento/ConfigurableProduct/view/adminhtml/web/js/variations/steps/attributes_values.js -+++ b/app/code/Magento/ConfigurableProduct/view/adminhtml/web/js/variations/steps/attributes_values.js -@@ -90,13 +90,46 @@ define([ - * @param {Object} option - */ - saveOption: function (option) { -- if (!_.isEmpty(option.label)) { -+ if (this.isValidOption(option)) { - this.options.remove(option); - this.options.push(option); - this.chosenOptions.push(option.id); - } - }, - -+ /** -+ * @param {Object} newOption -+ * @return boolean -+ */ -+ isValidOption: function (newOption) { -+ var duplicatedOptions = [], -+ errorOption, -+ allOptions = []; -+ -+ if (_.isEmpty(newOption.label)) { -+ return false; -+ } -+ -+ _.each(this.options(), function (option) { -+ if (!_.isUndefined(allOptions[option.label]) && newOption.label === option.label) { -+ duplicatedOptions.push(option); -+ } -+ -+ allOptions[option.label] = option.label; -+ }); -+ -+ if (duplicatedOptions.length) { -+ _.each(duplicatedOptions, function (duplicatedOption) { -+ errorOption = $('[data-role="' + duplicatedOption.id + '"]'); -+ errorOption.addClass('_error'); -+ }); -+ -+ return false; -+ } -+ -+ return true; -+ }, -+ - /** - * @param {Object} option - */ -@@ -128,6 +161,7 @@ define([ - })); - attribute.opened = ko.observable(this.initialOpened(index)); - attribute.collapsible = ko.observable(true); -+ attribute.isValidOption = this.isValidOption; - - return attribute; - }, -@@ -148,22 +182,22 @@ define([ - saveAttribute: function () { - var errorMessage = $.mage.__('Select options for all attributes or remove unused attributes.'); - -- this.attributes.each(function (attribute) { -+ if (!this.attributes().length) { -+ throw new Error(errorMessage); -+ } -+ -+ _.each(this.attributes(), function (attribute) { - attribute.chosen = []; - - if (!attribute.chosenOptions.getLength()) { - throw new Error(errorMessage); - } -- attribute.chosenOptions.each(function (id) { -+ _.each(attribute.chosenOptions(), function (id) { - attribute.chosen.push(attribute.options.findWhere({ - id: id - })); - }); - }); -- -- if (!this.attributes().length) { -- throw new Error(errorMessage); -- } - }, - - /** -@@ -184,33 +218,47 @@ define([ - * @return {Boolean} - */ - saveOptions: function () { -- var options = []; -+ var newOptions = []; - -- this.attributes.each(function (attribute) { -- attribute.chosenOptions.each(function (id) { -+ _.each(this.attributes(), function (attribute) { -+ _.each(attribute.options(), function (element) { - var option = attribute.options.findWhere({ -- id: id, -- 'is_new': true -+ id: element.id - }); - -- if (option) { -- options.push(option); -+ if (option['is_new'] === true) { -+ if (!attribute.isValidOption(option)) { -+ throw new Error( -+ $.mage.__('The value of attribute ""%1"" must be unique') -+ .replace('"%1"', attribute.label) -+ ); -+ } -+ -+ newOptions.push(option); - } - }); - }); - -- if (!options.length) { -+ if (!newOptions.length) { - return false; - } -+ - $.ajax({ - type: 'POST', - url: this.createOptionsUrl, - data: { -- options: options -+ options: newOptions - }, - showLoader: true - }).done(function (savedOptions) { -- this.attributes.each(function (attribute) { -+ if (savedOptions.error) { -+ this.notificationMessage.error = savedOptions.error; -+ this.notificationMessage.text = savedOptions.message; -+ -+ return; -+ } -+ -+ _.each(this.attributes(), function (attribute) { - _.each(savedOptions, function (newOptionId, oldOptionId) { - var option = attribute.options.findWhere({ - id: oldOptionId -@@ -233,7 +281,7 @@ define([ - */ - requestAttributes: function (attributeIds) { - $.ajax({ -- type: 'POST', -+ type: 'GET', - url: this.optionsUrl, - data: { - attributes: attributeIds -diff --git a/app/code/Magento/ConfigurableProduct/view/adminhtml/web/js/variations/variations.js b/app/code/Magento/ConfigurableProduct/view/adminhtml/web/js/variations/variations.js -index e7c8aa6f717..6e82fd42692 100644 ---- a/app/code/Magento/ConfigurableProduct/view/adminhtml/web/js/variations/variations.js -+++ b/app/code/Magento/ConfigurableProduct/view/adminhtml/web/js/variations/variations.js -@@ -383,26 +383,48 @@ define([ - * Chose action for the form save button - */ - saveFormHandler: function () { -- this.serializeData(); -+ this.formElement().validate(); -+ -+ if (this.formElement().source.get('params.invalid') === false) { -+ this.serializeData(); -+ } - - if (this.checkForNewAttributes()) { - this.formSaveParams = arguments; - this.attributeSetHandlerModal().openModal(); - } else { -+ if (this.validateForm(this.formElement())) { -+ this.clearOutdatedData(); -+ } - this.formElement().save(arguments[0], arguments[1]); -+ -+ if (this.formElement().source.get('params.invalid')) { -+ this.unserializeData(); -+ } - } - }, - -+ /** -+ * @param {Object} formElement -+ * -+ * Validates each form element and returns true, if all elements are valid. -+ */ -+ validateForm: function (formElement) { -+ formElement.validate(); -+ -+ return !formElement.additionalInvalid && !formElement.source.get('params.invalid'); -+ }, -+ - /** - * Serialize data for specific form fields - * -- * Get data from outdated fields, serialize it and produce new form fields. -+ * Serializes some complex data fields - * -- * Outdated fields: -+ * Original fields: - * - configurable-matrix; - * - associated_product_ids. - * -- * New fields: -+ * Serialized fields in request: - * - configurable-matrix-serialized; - * - associated_product_ids_serialized. - */ -@@ -410,18 +432,50 @@ define([ - if (this.source.data['configurable-matrix']) { - this.source.data['configurable-matrix-serialized'] = - JSON.stringify(this.source.data['configurable-matrix']); -- -- delete this.source.data['configurable-matrix']; - } - - if (this.source.data['associated_product_ids']) { - this.source.data['associated_product_ids_serialized'] = - JSON.stringify(this.source.data['associated_product_ids']); -+ } -+ }, -+ -+ /** -+ * Clear outdated data for specific form fields -+ * -+ * Outdated fields: -+ * - configurable-matrix; -+ * - associated_product_ids. -+ */ -+ clearOutdatedData: function () { -+ if (this.source.data['configurable-matrix']) { -+ delete this.source.data['configurable-matrix']; -+ } - -+ if (this.source.data['associated_product_ids']) { - delete this.source.data['associated_product_ids']; - } - }, - -+ /** -+ * Unserialize data for specific form fields -+ * -+ * Unserializes some fields that were serialized this.serializeData -+ */ -+ unserializeData: function () { -+ if (this.source.data['configurable-matrix-serialized']) { -+ this.source.data['configurable-matrix'] = -+ JSON.parse(this.source.data['configurable-matrix-serialized']); -+ delete this.source.data['configurable-matrix-serialized']; -+ } -+ -+ if (this.source.data['associated_product_ids_serialized']) { -+ this.source.data['associated_product_ids'] = -+ JSON.parse(this.source.data['associated_product_ids_serialized']); -+ delete this.source.data['associated_product_ids_serialized']; -+ } -+ }, -+ - /** - * Check for newly added attributes - * @returns {Boolean} -@@ -445,20 +499,20 @@ define([ - * @returns {Boolean} - */ - addNewAttributeSetHandler: function () { -- var choosenAttributeSetOption; -+ var chosenAttributeSetOption; - - this.formElement().validate(); - - if (this.formElement().source.get('params.invalid') === false) { -- choosenAttributeSetOption = this.attributeSetSelection; -+ chosenAttributeSetOption = this.attributeSetSelection; - -- if (choosenAttributeSetOption === 'new') { -+ if (chosenAttributeSetOption === 'new') { - this.createNewAttributeSet(); - - return false; - } - -- if (choosenAttributeSetOption === 'existing') { -+ if (chosenAttributeSetOption === 'existing') { - this.set( - 'skeletonAttributeSet', - this.attributeSetId -@@ -469,6 +523,10 @@ define([ - - return true; - } -+ -+ this.unserializeData(); -+ -+ return false; - }, - - /** -diff --git a/app/code/Magento/ConfigurableProduct/view/base/templates/product/price/final_price.phtml b/app/code/Magento/ConfigurableProduct/view/base/templates/product/price/final_price.phtml -index f020e4c2c94..a6dc6c81998 100644 ---- a/app/code/Magento/ConfigurableProduct/view/base/templates/product/price/final_price.phtml -+++ b/app/code/Magento/ConfigurableProduct/view/base/templates/product/price/final_price.phtml -@@ -4,37 +4,28 @@ - * See COPYING.txt for license details. - */ - --// @codingStandardsIgnoreFile -- --?> -- --<?php - /** @var \Magento\ConfigurableProduct\Pricing\Render\FinalPriceBox$block */ -- - /** @var \Magento\Framework\Pricing\Price\PriceInterface $priceModel */ - $priceModel = $block->getPriceType('regular_price'); -- - /** @var \Magento\Framework\Pricing\Price\PriceInterface $finalPriceModel */ - $finalPriceModel = $block->getPriceType('final_price'); - $idSuffix = $block->getIdSuffix() ? $block->getIdSuffix() : ''; - $schema = ($block->getZone() == 'item_view') ? true : false; - ?> - <span class="normal-price"> -- <?php -- $arguments = [ -+ <?= /* @noEscape */ $block->renderAmount($finalPriceModel->getAmount(), [ - 'display_label' => __('As low as'), - 'price_id' => $block->getPriceId('product-price-' . $idSuffix), - 'price_type' => 'finalPrice', - 'include_container' => true, - 'schema' => $schema, -- ]; -- /* @noEscape */ echo $block->renderAmount($finalPriceModel->getAmount(), $arguments); -- ?> -+ ]); -+?> - </span> - --<?php if (!$block->isProductList() && $block->hasSpecialPrice()): ?> -+<?php if (!$block->isProductList() && $block->hasSpecialPrice()) : ?> - <span class="old-price sly-old-price no-display"> -- <?php /* @escapeNotVerified */ echo $block->renderAmount($priceModel->getAmount(), [ -+ <?= /* @noEscape */ $block->renderAmount($priceModel->getAmount(), [ - 'display_label' => __('Regular Price'), - 'price_id' => $block->getPriceId('old-price-' . $idSuffix), - 'price_type' => 'oldPrice', -@@ -44,14 +35,14 @@ $schema = ($block->getZone() == 'item_view') ? true : false; - </span> - <?php endif; ?> - --<?php if ($block->showMinimalPrice()): ?> -- <?php if ($block->getUseLinkForAsLowAs()):?> -- <a href="<?= /* @escapeNotVerified */ $block->getSaleableItem()->getProductUrl() ?>" class="minimal-price-link"> -- <?= /* @escapeNotVerified */ $block->renderAmountMinimal() ?> -+<?php if ($block->showMinimalPrice()) : ?> -+ <?php if ($block->getUseLinkForAsLowAs()) :?> -+ <a href="<?= $block->escapeUrl($block->getSaleableItem()->getProductUrl()) ?>" class="minimal-price-link"> -+ <?= /* @noEscape */ $block->renderAmountMinimal() ?> - </a> -- <?php else:?> -+ <?php else :?> - <span class="minimal-price-link"> -- <?= /* @escapeNotVerified */ $block->renderAmountMinimal() ?> -+ <?= /* @noEscape */ $block->renderAmountMinimal() ?> - </span> - <?php endif?> - <?php endif; ?> -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 18f96cfaaf3..c68419b955e 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 -@@ -3,7 +3,6 @@ - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -- - ?> - <script type="text/x-magento-template" id="tier-prices-template"> - <ul class="prices-tier items"> -@@ -15,10 +14,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/templates/js/components.phtml b/app/code/Magento/ConfigurableProduct/view/frontend/templates/js/components.phtml -index bad5acc209b..5902a9f25cc 100644 ---- a/app/code/Magento/ConfigurableProduct/view/frontend/templates/js/components.phtml -+++ b/app/code/Magento/ConfigurableProduct/view/frontend/templates/js/components.phtml -@@ -3,8 +3,5 @@ - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -- --// @codingStandardsIgnoreFile -- - ?> - <?= $block->getChildHtml() ?> -diff --git a/app/code/Magento/ConfigurableProduct/view/frontend/templates/product/view/type/options/configurable.phtml b/app/code/Magento/ConfigurableProduct/view/frontend/templates/product/view/type/options/configurable.phtml -index f5ed0679675..f7db41225c9 100644 ---- a/app/code/Magento/ConfigurableProduct/view/frontend/templates/product/view/type/options/configurable.phtml -+++ b/app/code/Magento/ConfigurableProduct/view/frontend/templates/product/view/type/options/configurable.phtml -@@ -3,9 +3,6 @@ - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -- --// @codingStandardsIgnoreFile -- - ?> - - <?php -@@ -13,19 +10,19 @@ - $_product = $block->getProduct(); - $_attributes = $block->decorateArray($block->getAllowAttributes()); - ?> --<?php if ($_product->isSaleable() && count($_attributes)):?> -- <?php foreach ($_attributes as $_attribute): ?> -+<?php if ($_product->isSaleable() && count($_attributes)) :?> -+ <?php foreach ($_attributes as $_attribute) : ?> - <div class="field configurable required"> -- <label class="label" for="attribute<?= /* @escapeNotVerified */ $_attribute->getAttributeId() ?>"> -+ <label class="label" for="attribute<?= $block->escapeHtmlAttr($_attribute->getAttributeId()) ?>"> - <span><?= $block->escapeHtml($_attribute->getProductAttribute()->getStoreLabel()) ?></span> - </label> - <div class="control"> -- <select name="super_attribute[<?= /* @escapeNotVerified */ $_attribute->getAttributeId() ?>]" -- data-selector="super_attribute[<?= /* @escapeNotVerified */ $_attribute->getAttributeId() ?>]" -+ <select name="super_attribute[<?= $block->escapeHtmlAttr($_attribute->getAttributeId()) ?>]" -+ data-selector="super_attribute[<?= $block->escapeHtmlAttr($_attribute->getAttributeId()) ?>]" - data-validate="{required:true}" -- id="attribute<?= /* @escapeNotVerified */ $_attribute->getAttributeId() ?>" -+ id="attribute<?= $block->escapeHtmlAttr($_attribute->getAttributeId()) ?>" - class="super-attribute-select"> -- <option value=""><?= /* @escapeNotVerified */ __('Choose an Option...') ?></option> -+ <option value=""><?= $block->escapeHtml(__('Choose an Option...')) ?></option> - </select> - </div> - </div> -@@ -34,9 +31,11 @@ $_attributes = $block->decorateArray($block->getAllowAttributes()); - { - "#product_addtocart_form": { - "configurable": { -- "spConfig": <?= /* @escapeNotVerified */ $block->getJsonConfig() ?>, -- "gallerySwitchStrategy": "<?php /* @escapeNotVerified */ echo $block->getVar('gallery_switch_strategy', -- 'Magento_ConfigurableProduct') ?: 'replace'; ?>" -+ "spConfig": <?= /* @noEscape */ $block->getJsonConfig() ?>, -+ "gallerySwitchStrategy": "<?= $block->escapeJs($block->getVar( -+ 'gallery_switch_strategy', -+ 'Magento_ConfigurableProduct' -+ ) ?: 'replace'); ?>" - } - }, - "*" : { -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 8cabe71c175..ef40dcb9a73 100644 ---- a/app/code/Magento/ConfigurableProduct/view/frontend/web/js/configurable.js -+++ b/app/code/Magento/ConfigurableProduct/view/frontend/web/js/configurable.js -@@ -291,6 +291,8 @@ define([ - images = this.options.spConfig.images[this.simpleProduct]; - - if (images) { -+ images = this._sortImages(images); -+ - if (this.options.gallerySwitchStrategy === 'prepend') { - images = images.concat(initialImages); - } -@@ -309,7 +311,17 @@ define([ - $(this.options.mediaGallerySelector).AddFotoramaVideoEvents(); - } - -- galleryObject.first(); -+ }, -+ -+ /** -+ * Sorting images array -+ * -+ * @private -+ */ -+ _sortImages: function (images) { -+ return _.sortBy(images, function (image) { -+ return image.position; -+ }); - }, - - /** -@@ -360,7 +372,12 @@ define([ - index = 1, - allowedProducts, - i, -- j; -+ j, -+ finalPrice = parseFloat(this.options.spConfig.prices.finalPrice.amount), -+ optionFinalPrice, -+ optionPriceDiff, -+ optionPrices = this.options.spConfig.optionPrices, -+ allowedProductMinPrice; - - this._clearSelect(element); - element.options[0] = new Option('', ''); -@@ -374,6 +391,7 @@ define([ - if (options) { - for (i = 0; i < options.length; i++) { - allowedProducts = []; -+ optionPriceDiff = 0; - - /* eslint-disable max-depth */ - if (prevConfig) { -@@ -387,6 +405,20 @@ define([ - } - } else { - allowedProducts = options[i].products.slice(0); -+ -+ if (typeof allowedProducts[0] !== 'undefined' && -+ typeof optionPrices[allowedProducts[0]] !== 'undefined') { -+ allowedProductMinPrice = this._getAllowedProductWithMinPrice(allowedProducts); -+ optionFinalPrice = parseFloat(optionPrices[allowedProductMinPrice].finalPrice.amount); -+ optionPriceDiff = optionFinalPrice - finalPrice; -+ -+ if (optionPriceDiff !== 0) { -+ options[i].label = options[i].label + ' ' + priceUtils.formatPrice( -+ optionPriceDiff, -+ this.options.priceFormat, -+ true); -+ } -+ } - } - - if (allowedProducts.length > 0) { -@@ -394,7 +426,7 @@ define([ - element.options[index] = new Option(this._getOptionLabel(options[i]), options[i].id); - - if (typeof options[i].price !== 'undefined') { -- element.options[index].setAttribute('price', options[i].prices); -+ element.options[index].setAttribute('price', options[i].price); - } - - element.options[index].config = options[i]; -@@ -458,24 +490,56 @@ define([ - _getPrices: function () { - var prices = {}, - elements = _.toArray(this.options.settings), -- hasProductPrice = false; -+ allowedProduct; - - _.each(elements, function (element) { - var selected = element.options[element.selectedIndex], - config = selected && selected.config, - priceValue = {}; - -- if (config && config.allowedProducts.length === 1 && !hasProductPrice) { -+ if (config && config.allowedProducts.length === 1) { - priceValue = this._calculatePrice(config); -- hasProductPrice = true; -+ } else if (element.value) { -+ allowedProduct = this._getAllowedProductWithMinPrice(config.allowedProducts); -+ priceValue = this._calculatePrice({ -+ 'allowedProducts': [ -+ allowedProduct -+ ] -+ }); - } - -- prices[element.attributeId] = priceValue; -+ if (!_.isEmpty(priceValue)) { -+ prices.prices = priceValue; -+ } - }, this); - - return prices; - }, - -+ /** -+ * Get product with minimum price from selected options. -+ * -+ * @param {Array} allowedProducts -+ * @returns {String} -+ * @private -+ */ -+ _getAllowedProductWithMinPrice: function (allowedProducts) { -+ var optionPrices = this.options.spConfig.optionPrices, -+ product = {}, -+ optionMinPrice, optionFinalPrice; -+ -+ _.each(allowedProducts, function (allowedProduct) { -+ optionFinalPrice = parseFloat(optionPrices[allowedProduct].finalPrice.amount); -+ -+ if (_.isEmpty(product) || optionFinalPrice < optionMinPrice) { -+ optionMinPrice = optionFinalPrice; -+ product = allowedProduct; -+ } -+ }, this); -+ -+ return product; -+ }, -+ - /** - * Returns prices for configured products - * -@@ -545,6 +609,13 @@ define([ - } else { - $(this.options.slyOldPriceSelector).hide(); - } -+ -+ $(document).trigger('updateMsrpPriceBlock', -+ [ -+ optionId, -+ this.options.spConfig.optionPrices -+ ] -+ ); - }, - - /** -diff --git a/app/code/Magento/ConfigurableProductGraphQl/Model/ConfigurableProductTypeResolver.php b/app/code/Magento/ConfigurableProductGraphQl/Model/ConfigurableProductTypeResolver.php -index aae39800cdd..eda2ce11daa 100644 ---- a/app/code/Magento/ConfigurableProductGraphQl/Model/ConfigurableProductTypeResolver.php -+++ b/app/code/Magento/ConfigurableProductGraphQl/Model/ConfigurableProductTypeResolver.php -@@ -8,19 +8,25 @@ declare(strict_types=1); - namespace Magento\ConfigurableProductGraphQl\Model; - - use Magento\Framework\GraphQl\Query\Resolver\TypeResolverInterface; -+use Magento\ConfigurableProduct\Model\Product\Type\Configurable as Type; - - /** -- * {@inheritdoc} -+ * @inheritdoc - */ - class ConfigurableProductTypeResolver implements TypeResolverInterface - { - /** -- * {@inheritdoc} -+ * Configurable product type resolver code - */ -- public function resolveType(array $data) : string -+ const TYPE_RESOLVER = 'ConfigurableProduct'; -+ -+ /** -+ * @inheritdoc -+ */ -+ public function resolveType(array $data): string - { -- if (isset($data['type_id']) && $data['type_id'] == 'configurable') { -- return 'ConfigurableProduct'; -+ if (isset($data['type_id']) && $data['type_id'] == Type::TYPE_CODE) { -+ return self::TYPE_RESOLVER; - } - return ''; - } -diff --git a/app/code/Magento/ConfigurableProductGraphQl/Model/Options/Collection.php b/app/code/Magento/ConfigurableProductGraphQl/Model/Options/Collection.php -index 90ed5cf5489..36ee00d5533 100644 ---- a/app/code/Magento/ConfigurableProductGraphQl/Model/Options/Collection.php -+++ b/app/code/Magento/ConfigurableProductGraphQl/Model/Options/Collection.php -@@ -124,6 +124,8 @@ class Collection - $this->attributeMap[$productId][$attribute->getId()]['attribute_code'] - = $attribute->getProductAttribute()->getAttributeCode(); - $this->attributeMap[$productId][$attribute->getId()]['values'] = $attributeData['options']; -+ $this->attributeMap[$productId][$attribute->getId()]['label'] -+ = $attribute->getProductAttribute()->getStoreLabel(); - } - - return $this->attributeMap; -diff --git a/app/code/Magento/ConfigurableProductGraphQl/Model/Resolver/ConfigurableVariant.php b/app/code/Magento/ConfigurableProductGraphQl/Model/Resolver/ConfigurableVariant.php -index e63c75d5003..3e07fecb2eb 100644 ---- a/app/code/Magento/ConfigurableProductGraphQl/Model/Resolver/ConfigurableVariant.php -+++ b/app/code/Magento/ConfigurableProductGraphQl/Model/Resolver/ConfigurableVariant.php -@@ -16,12 +16,11 @@ use Magento\ConfigurableProductGraphQl\Model\Options\Collection as OptionCollect - use Magento\ConfigurableProductGraphQl\Model\Variant\Collection; - use Magento\Framework\EntityManager\MetadataPool; - use Magento\Framework\GraphQl\Config\Element\Field; --use Magento\Framework\GraphQl\Query\Resolver\Value; - use Magento\Framework\GraphQl\Query\Resolver\ValueFactory; - use Magento\Framework\GraphQl\Query\ResolverInterface; - - /** -- * {@inheritdoc} -+ * @inheritdoc - */ - class ConfigurableVariant implements ResolverInterface - { -@@ -72,11 +71,9 @@ class ConfigurableVariant implements ResolverInterface - } - - /** -- * Fetch and format configurable variants. -- * -- * {@inheritDoc} -+ * @inheritdoc - */ -- public function resolve(Field $field, $context, ResolveInfo $info, array $value = null, array $args = null) : Value -+ public function resolve(Field $field, $context, ResolveInfo $info, array $value = null, array $args = null) - { - $linkField = $this->metadataPool->getMetadata(ProductInterface::class)->getLinkField(); - if ($value['type_id'] !== Type::TYPE_CODE || !isset($value[$linkField])) { -@@ -86,7 +83,7 @@ class ConfigurableVariant implements ResolverInterface - return $this->valueFactory->create($result); - } - -- $this->variantCollection->addParentId((int)$value[$linkField]); -+ $this->variantCollection->addParentProduct($value['model']); - $fields = $this->getProductFields($info); - $matchedFields = $this->attributeCollection->getRequestAttributes($fields); - $this->variantCollection->addEavAttributes($matchedFields); -diff --git a/app/code/Magento/ConfigurableProductGraphQl/Model/Resolver/Options.php b/app/code/Magento/ConfigurableProductGraphQl/Model/Resolver/Options.php -index 53912f7029e..aa7ed6f5525 100644 ---- a/app/code/Magento/ConfigurableProductGraphQl/Model/Resolver/Options.php -+++ b/app/code/Magento/ConfigurableProductGraphQl/Model/Resolver/Options.php -@@ -13,12 +13,11 @@ use Magento\ConfigurableProduct\Model\Product\Type\Configurable as Type; - use Magento\ConfigurableProductGraphQl\Model\Options\Collection as OptionCollection; - use Magento\Framework\EntityManager\MetadataPool; - use Magento\Framework\GraphQl\Config\Element\Field; --use Magento\Framework\GraphQl\Query\Resolver\Value; - use Magento\Framework\GraphQl\Query\Resolver\ValueFactory; - use Magento\Framework\GraphQl\Query\ResolverInterface; - - /** -- * {@inheritdoc} -+ * @inheritdoc - */ - class Options implements ResolverInterface - { -@@ -55,9 +54,9 @@ class Options implements ResolverInterface - /** - * Fetch and format configurable variants. - * -- * {@inheritDoc} -+ * {@inheritdoc} - */ -- public function resolve(Field $field, $context, ResolveInfo $info, array $value = null, array $args = null) : Value -+ public function resolve(Field $field, $context, ResolveInfo $info, array $value = null, array $args = null) - { - $linkField = $this->metadataPool->getMetadata(ProductInterface::class)->getLinkField(); - if ($value['type_id'] !== Type::TYPE_CODE || !isset($value[$linkField])) { -diff --git a/app/code/Magento/ConfigurableProductGraphQl/Model/Resolver/Variant/Attributes.php b/app/code/Magento/ConfigurableProductGraphQl/Model/Resolver/Variant/Attributes.php -index 9c275de3f09..dd2b84e1da5 100644 ---- a/app/code/Magento/ConfigurableProductGraphQl/Model/Resolver/Variant/Attributes.php -+++ b/app/code/Magento/ConfigurableProductGraphQl/Model/Resolver/Variant/Attributes.php -@@ -7,10 +7,11 @@ declare(strict_types=1); - - namespace Magento\ConfigurableProductGraphQl\Model\Resolver\Variant; - -+use Magento\Framework\GraphQl\Query\Resolver\ContextInterface; -+use Magento\Framework\GraphQl\Query\Resolver\Value; -+use Magento\Catalog\Model\Product; - use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; - use Magento\Framework\GraphQl\Config\Element\Field; --use Magento\Framework\GraphQl\Query\Resolver\Value; --use Magento\Framework\GraphQl\Query\Resolver\ValueFactory; - use Magento\Framework\GraphQl\Query\ResolverInterface; - - /** -@@ -19,22 +20,17 @@ use Magento\Framework\GraphQl\Query\ResolverInterface; - class Attributes implements ResolverInterface - { - /** -- * @var ValueFactory -- */ -- private $valueFactory; -- -- /** -- * @param ValueFactory $valueFactory -- */ -- public function __construct(ValueFactory $valueFactory) -- { -- $this->valueFactory = $valueFactory; -- } -- -- /** -+ * @inheritdoc -+ * - * Format product's option data to conform to GraphQL schema - * -- * {@inheritdoc} -+ * @param Field $field -+ * @param ContextInterface $context -+ * @param ResolveInfo $info -+ * @param array|null $value -+ * @param array|null $args -+ * @throws \Exception -+ * @return mixed|Value - */ - public function resolve( - Field $field, -@@ -42,38 +38,32 @@ class Attributes implements ResolverInterface - ResolveInfo $info, - array $value = null, - array $args = null -- ): Value { -+ ) { - if (!isset($value['options']) || !isset($value['product'])) { -- $result = function () { -- return null; -- }; -- return $this->valueFactory->create($result); -+ return null; - } - -- $result = function () use ($value) { -- $data = []; -- foreach ($value['options'] as $option) { -- $code = $option['attribute_code']; -- if (!isset($value['product'][$code])) { -- continue; -- } -+ $data = []; -+ foreach ($value['options'] as $option) { -+ $code = $option['attribute_code']; -+ /** @var Product|null $model */ -+ $model = $value['product']['model'] ?? null; -+ if (!$model || !$model->getData($code)) { -+ continue; -+ } - -- foreach ($option['values'] as $optionValue) { -- if ($optionValue['value_index'] != $value['product'][$code]) { -- continue; -- } -- $data[] = [ -- 'label' => $optionValue['label'], -- 'code' => $code, -- 'use_default_value' => $optionValue['use_default_value'], -- 'value_index' => $optionValue['value_index'] -- ]; -+ foreach ($option['values'] as $optionValue) { -+ if ($optionValue['value_index'] != $model->getData($code)) { -+ continue; - } -+ $data[] = [ -+ 'label' => $optionValue['label'], -+ 'code' => $code, -+ 'use_default_value' => $optionValue['use_default_value'], -+ 'value_index' => $optionValue['value_index'] -+ ]; - } -- -- return $data; -- }; -- -- return $this->valueFactory->create($result); -+ } -+ return $data; - } - } -diff --git a/app/code/Magento/ConfigurableProductGraphQl/Model/Variant/Collection.php b/app/code/Magento/ConfigurableProductGraphQl/Model/Variant/Collection.php -index 0d86e165743..d517c9aa29b 100644 ---- a/app/code/Magento/ConfigurableProductGraphQl/Model/Variant/Collection.php -+++ b/app/code/Magento/ConfigurableProductGraphQl/Model/Variant/Collection.php -@@ -9,12 +9,11 @@ namespace Magento\ConfigurableProductGraphQl\Model\Variant; - - use Magento\Catalog\Api\Data\ProductInterface; - use Magento\Catalog\Model\Product; --use Magento\ConfigurableProduct\Model\ResourceModel\Product\Type\Configurable\Product\CollectionFactory; - use Magento\ConfigurableProduct\Model\ResourceModel\Product\Type\Configurable\Product\Collection as ChildCollection; --use Magento\Catalog\Model\ProductFactory; -+use Magento\ConfigurableProduct\Model\ResourceModel\Product\Type\Configurable\Product\CollectionFactory; - use Magento\Framework\EntityManager\MetadataPool; - use Magento\Framework\Api\SearchCriteriaBuilder; --use Magento\CatalogGraphQl\Model\Resolver\Products\DataProvider\Product as DataProvider; -+use Magento\CatalogGraphQl\Model\Resolver\Products\DataProvider\Product\CollectionProcessorInterface; - - /** - * Collection for fetching configurable child product data. -@@ -26,30 +25,20 @@ class Collection - */ - private $childCollectionFactory; - -- /** -- * @var ProductFactory -- */ -- private $productFactory; -- - /** - * @var SearchCriteriaBuilder - */ - private $searchCriteriaBuilder; - -- /** -- * @var DataProvider -- */ -- private $productDataProvider; -- - /** - * @var MetadataPool - */ - private $metadataPool; - - /** -- * @var int[] -+ * @var Product[] - */ -- private $parentIds = []; -+ private $parentProducts = []; - - /** - * @var array -@@ -61,41 +50,48 @@ class Collection - */ - private $attributeCodes = []; - -+ /** -+ * @var CollectionProcessorInterface -+ */ -+ private $collectionProcessor; -+ - /** - * @param CollectionFactory $childCollectionFactory -- * @param ProductFactory $productFactory - * @param SearchCriteriaBuilder $searchCriteriaBuilder -- * @param DataProvider $productDataProvider - * @param MetadataPool $metadataPool -+ * @param CollectionProcessorInterface $collectionProcessor - */ - public function __construct( - CollectionFactory $childCollectionFactory, -- ProductFactory $productFactory, - SearchCriteriaBuilder $searchCriteriaBuilder, -- DataProvider $productDataProvider, -- MetadataPool $metadataPool -+ MetadataPool $metadataPool, -+ CollectionProcessorInterface $collectionProcessor - ) { - $this->childCollectionFactory = $childCollectionFactory; -- $this->productFactory = $productFactory; - $this->searchCriteriaBuilder = $searchCriteriaBuilder; -- $this->productDataProvider = $productDataProvider; - $this->metadataPool = $metadataPool; -+ $this->collectionProcessor = $collectionProcessor; - } - - /** -- * Add parent Id to collection filter -+ * Add parent to collection filter - * -- * @param int $id -+ * @param Product $product - * @return void - */ -- public function addParentId(int $id) : void -+ public function addParentProduct(Product $product) : void - { -- if (!in_array($id, $this->parentIds) && !empty($this->childrenMap)) { -+ $linkField = $this->metadataPool->getMetadata(ProductInterface::class)->getLinkField(); -+ $productId = $product->getData($linkField); -+ -+ if (isset($this->parentProducts[$productId])) { -+ return; -+ } -+ -+ if (!empty($this->childrenMap)) { - $this->childrenMap = []; -- $this->parentIds[] = $id; -- } elseif (!in_array($id, $this->parentIds)) { -- $this->parentIds[] = $id; - } -+ $this->parentProducts[$productId] = $product; - } - - /** -@@ -130,21 +126,24 @@ class Collection - * Fetch all children products from parent id's. - * - * @return array -+ * @throws \Exception - */ - private function fetch() : array - { -- if (empty($this->parentIds) || !empty($this->childrenMap)) { -+ if (empty($this->parentProducts) || !empty($this->childrenMap)) { - return $this->childrenMap; - } - -- $linkField = $this->metadataPool->getMetadata(ProductInterface::class)->getLinkField(); -- foreach ($this->parentIds as $id) { -+ foreach ($this->parentProducts as $product) { -+ $attributeData = $this->getAttributesCodes($product); - /** @var ChildCollection $childCollection */ - $childCollection = $this->childCollectionFactory->create(); -- /** @var Product $product */ -- $product = $this->productFactory->create(); -- $product->setData($linkField, $id); - $childCollection->setProductFilter($product); -+ $this->collectionProcessor->process( -+ $childCollection, -+ $this->searchCriteriaBuilder->create(), -+ $attributeData -+ ); - - /** @var Product $childProduct */ - foreach ($childCollection->getItems() as $childProduct) { -@@ -160,4 +159,24 @@ class Collection - - return $this->childrenMap; - } -+ -+ /** -+ * Get attributes code -+ * -+ * @param \Magento\Catalog\Model\Product $currentProduct -+ * @return array -+ */ -+ private function getAttributesCodes(Product $currentProduct): array -+ { -+ $attributeCodes = []; -+ $allowAttributes = $currentProduct->getTypeInstance()->getConfigurableAttributes($currentProduct); -+ foreach ($allowAttributes as $attribute) { -+ $productAttribute = $attribute->getProductAttribute(); -+ if (!\in_array($productAttribute->getAttributeCode(), $attributeCodes)) { -+ $attributeCodes[] = $productAttribute->getAttributeCode(); -+ } -+ } -+ -+ return $attributeCodes; -+ } - } -diff --git a/app/code/Magento/ConfigurableProductGraphQl/etc/schema.graphqls b/app/code/Magento/ConfigurableProductGraphQl/etc/schema.graphqls -index 267a94a1d43..45a4323e7f4 100644 ---- a/app/code/Magento/ConfigurableProductGraphQl/etc/schema.graphqls -+++ b/app/code/Magento/ConfigurableProductGraphQl/etc/schema.graphqls -@@ -1,5 +1,8 @@ - # Copyright © Magento, Inc. All rights reserved. - # See COPYING.txt for license details. -+type Mutation { -+ addConfigurableProductsToCart(input: AddConfigurableProductsToCartInput): AddConfigurableProductsToCartOutput @resolver(class: "Magento\\QuoteGraphQl\\Model\\Resolver\\AddSimpleProductsToCart") -+} - - type ConfigurableProduct implements ProductInterface, PhysicalProductInterface, CustomizableProductInterface @doc(description: "ConfigurableProduct defines basic features of a configurable product and its simple product variants") { - variants: [ConfigurableVariant] @doc(description: "An array of variants of products") @resolver(class: "Magento\\ConfigurableProductGraphQl\\Model\\Resolver\\ConfigurableVariant") -@@ -35,3 +38,30 @@ type ConfigurableProductOptionsValues @doc(description: "ConfigurableProductOpti - store_label: String @doc(description: "The label of the product on the current store") - use_default_value: Boolean @doc(description: "Indicates whether to use the default_label") - } -+ -+input AddConfigurableProductsToCartInput { -+ cart_id: String! -+ cart_items: [ConfigurableProductCartItemInput!]! -+} -+ -+type AddConfigurableProductsToCartOutput { -+ cart: Cart! -+} -+ -+input ConfigurableProductCartItemInput { -+ data: CartItemInput! -+ variant_sku: String! -+ customizable_options:[CustomizableOptionInput!] -+} -+ -+type ConfigurableCartItem implements CartItemInterface { -+ customizable_options: [SelectedCustomizableOption]! -+ configurable_options: [SelectedConfigurableOption!]! -+} -+ -+type SelectedConfigurableOption { -+ id: Int! -+ option_label: String! -+ value_id: Int! -+ value_label: String! -+} -diff --git a/app/code/Magento/Contact/Controller/Index/Index.php b/app/code/Magento/Contact/Controller/Index/Index.php -index 4b734c4f9b6..562b0770872 100644 ---- a/app/code/Magento/Contact/Controller/Index/Index.php -+++ b/app/code/Magento/Contact/Controller/Index/Index.php -@@ -6,9 +6,10 @@ - */ - namespace Magento\Contact\Controller\Index; - -+use Magento\Framework\App\Action\HttpGetActionInterface as HttpGetActionInterface; - use Magento\Framework\Controller\ResultFactory; - --class Index extends \Magento\Contact\Controller\Index -+class Index extends \Magento\Contact\Controller\Index implements HttpGetActionInterface - { - /** - * Show Contact Us page -diff --git a/app/code/Magento/Contact/Controller/Index/Post.php b/app/code/Magento/Contact/Controller/Index/Post.php -index bdc2fee088d..cbe49a76726 100644 ---- a/app/code/Magento/Contact/Controller/Index/Post.php -+++ b/app/code/Magento/Contact/Controller/Index/Post.php -@@ -7,6 +7,7 @@ - - namespace Magento\Contact\Controller\Index; - -+use Magento\Framework\App\Action\HttpPostActionInterface as HttpPostActionInterface; - use Magento\Contact\Model\ConfigInterface; - use Magento\Contact\Model\MailInterface; - use Magento\Framework\App\Action\Context; -@@ -17,7 +18,7 @@ use Psr\Log\LoggerInterface; - use Magento\Framework\App\ObjectManager; - use Magento\Framework\DataObject; - --class Post extends \Magento\Contact\Controller\Index -+class Post extends \Magento\Contact\Controller\Index implements HttpPostActionInterface - { - /** - * @var DataPersistorInterface -diff --git a/app/code/Magento/Contact/Test/Mftf/ActionGroup/AssertMessageContactUsFormActionGroup.xml b/app/code/Magento/Contact/Test/Mftf/ActionGroup/AssertMessageContactUsFormActionGroup.xml -new file mode 100644 -index 00000000000..eec21948251 ---- /dev/null -+++ b/app/code/Magento/Contact/Test/Mftf/ActionGroup/AssertMessageContactUsFormActionGroup.xml -@@ -0,0 +1,18 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> -+ <actionGroup name="AssertMessageContactUsFormActionGroup"> -+ <arguments> -+ <argument name="message" type="string" defaultValue="Thanks for contacting us with your comments and questions. We'll respond to you very soon." /> -+ <argument name="messageType" type="string" defaultValue="success" /> -+ </arguments> -+ <see userInput="{{message}}" selector="{{StorefrontContactUsMessagesSection.messageByType(messageType)}}" stepKey="verifyMessage" /> -+ </actionGroup> -+</actionGroups> -diff --git a/app/code/Magento/Contact/Test/Mftf/ActionGroup/StorefrontFillContactUsFormActionGroup.xml b/app/code/Magento/Contact/Test/Mftf/ActionGroup/StorefrontFillContactUsFormActionGroup.xml -new file mode 100644 -index 00000000000..df4964ea042 ---- /dev/null -+++ b/app/code/Magento/Contact/Test/Mftf/ActionGroup/StorefrontFillContactUsFormActionGroup.xml -@@ -0,0 +1,20 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> -+ <actionGroup name="StorefrontFillContactUsFormActionGroup"> -+ <arguments> -+ <argument name="customer" type="entity" /> -+ <argument name="contactUsData" type="entity" /> -+ </arguments> -+ <fillField selector="{{StorefrontContactUsFormSection.nameField}}" userInput="{{customer.firstname}}" stepKey="fillName"/> -+ <fillField selector="{{StorefrontContactUsFormSection.emailField}}" userInput="{{customer.email}}" stepKey="fillEmail"/> -+ <fillField selector="{{StorefrontContactUsFormSection.commentField}}" userInput="{{contactUsData.comment}}" stepKey="fillComment"/> -+ </actionGroup> -+</actionGroups> -diff --git a/app/code/Magento/Contact/Test/Mftf/ActionGroup/StorefrontOpenContactUsPageActionGroup.xml b/app/code/Magento/Contact/Test/Mftf/ActionGroup/StorefrontOpenContactUsPageActionGroup.xml -new file mode 100644 -index 00000000000..d333d5d9989 ---- /dev/null -+++ b/app/code/Magento/Contact/Test/Mftf/ActionGroup/StorefrontOpenContactUsPageActionGroup.xml -@@ -0,0 +1,15 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> -+ <actionGroup name="StorefrontOpenContactUsPageActionGroup"> -+ <amOnPage url="{{StorefrontContactUsPage.url}}" stepKey="amOnContactUpPage"/> -+ <waitForPageLoad stepKey="waitForContactUpPageLoad"/> -+ </actionGroup> -+</actionGroups> -diff --git a/app/code/Magento/Contact/Test/Mftf/ActionGroup/StorefrontSubmitContactUsFormActionGroup.xml b/app/code/Magento/Contact/Test/Mftf/ActionGroup/StorefrontSubmitContactUsFormActionGroup.xml -new file mode 100644 -index 00000000000..f3fe34f20c3 ---- /dev/null -+++ b/app/code/Magento/Contact/Test/Mftf/ActionGroup/StorefrontSubmitContactUsFormActionGroup.xml -@@ -0,0 +1,15 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> -+ <actionGroup name="StorefrontSubmitContactUsFormActionGroup"> -+ <click selector="{{StorefrontContactUsFormSection.submitFormButton}}" stepKey="clickSubmitFormButton"/> -+ <waitForPageLoad stepKey="waitForCommentSubmitted" /> -+ </actionGroup> -+</actionGroups> -diff --git a/app/code/Magento/Contact/Test/Mftf/Data/ContactUsData.xml b/app/code/Magento/Contact/Test/Mftf/Data/ContactUsData.xml -new file mode 100644 -index 00000000000..eadf760776c ---- /dev/null -+++ b/app/code/Magento/Contact/Test/Mftf/Data/ContactUsData.xml -@@ -0,0 +1,14 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<entities xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataProfileSchema.xsd"> -+ <entity name="DefaultContactUsData"> -+ <data key="comment" unique="suffix">Lorem ipsum dolor sit amet, ne enim aliquando eam, oblique deserunt no usu. Unique: </data> -+ </entity> -+</entities> -diff --git a/app/code/Magento/Contact/Test/Mftf/Page/StorefrontContactUsPage.xml b/app/code/Magento/Contact/Test/Mftf/Page/StorefrontContactUsPage.xml -new file mode 100644 -index 00000000000..5e793b23385 ---- /dev/null -+++ b/app/code/Magento/Contact/Test/Mftf/Page/StorefrontContactUsPage.xml -@@ -0,0 +1,14 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<pages xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/PageObject.xsd"> -+ <page name="StorefrontContactUsPage" url="/contact/" area="storefront" module="Magento_Contact"> -+ <section name="StorefrontContactUsFormSection"/> -+ </page> -+</pages> -diff --git a/app/code/Magento/Contact/Test/Mftf/Section/StorefrontContactUsFormSection.xml b/app/code/Magento/Contact/Test/Mftf/Section/StorefrontContactUsFormSection.xml -new file mode 100644 -index 00000000000..fdaddf33f51 ---- /dev/null -+++ b/app/code/Magento/Contact/Test/Mftf/Section/StorefrontContactUsFormSection.xml -@@ -0,0 +1,18 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> -+ <section name="StorefrontContactUsFormSection"> -+ <element name="nameField" type="input" selector="#contact-form input[name='name']" /> -+ <element name="emailField" type="input" selector="#contact-form input[name='email']" /> -+ <element name="phoneField" type="input" selector="#contact-form input[name='telephone']" /> -+ <element name="commentField" type="textarea" selector="#contact-form textarea[name='comment']" /> -+ <element name="submitFormButton" type="button" selector="#contact-form button[type='submit']" timeout="30" /> -+ </section> -+</sections> -diff --git a/app/code/Magento/Contact/Test/Mftf/Section/StorefrontContactUsMessagesSection.xml b/app/code/Magento/Contact/Test/Mftf/Section/StorefrontContactUsMessagesSection.xml -new file mode 100644 -index 00000000000..0970f1f8f6b ---- /dev/null -+++ b/app/code/Magento/Contact/Test/Mftf/Section/StorefrontContactUsMessagesSection.xml -@@ -0,0 +1,14 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> -+ <section name="StorefrontContactUsMessagesSection"> -+ <element name="messageByType" type="block" selector="#maincontent .message-{{messageType}}" parameterized="true" /> -+ </section> -+</sections> -diff --git a/app/code/Magento/Contact/view/frontend/email/submitted_form.html b/app/code/Magento/Contact/view/frontend/email/submitted_form.html -index 1bce6159c58..17146257aef 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/templates/form.phtml b/app/code/Magento/Contact/view/frontend/templates/form.phtml -index d64a991bcaf..673bdc73840 100644 ---- a/app/code/Magento/Contact/view/frontend/templates/form.phtml -+++ b/app/code/Magento/Contact/view/frontend/templates/form.phtml -@@ -4,7 +4,6 @@ - * See COPYING.txt for license details. - */ - --// @codingStandardsIgnoreFile - /** @var \Magento\Contact\Block\ContactForm $block */ - ?> - <form class="form contact" -@@ -19,25 +18,25 @@ - <div class="field name required"> - <label class="label" for="name"><span><?= $block->escapeHtml(__('Name')) ?></span></label> - <div class="control"> -- <input name="name" id="name" title="<?= $block->escapeHtmlAttr(__('Name')) ?>" value="<?= $block->escapeHtmlAttr($this->helper('Magento\Contact\Helper\Data')->getPostValue('name') ?: $this->helper('Magento\Contact\Helper\Data')->getUserName()) ?>" class="input-text" type="text" data-validate="{required:true}"/> -+ <input name="name" id="name" title="<?= $block->escapeHtmlAttr(__('Name')) ?>" value="<?= $block->escapeHtmlAttr($this->helper(\Magento\Contact\Helper\Data::class)->getPostValue('name') ?: $this->helper(\Magento\Contact\Helper\Data::class)->getUserName()) ?>" class="input-text" type="text" data-validate="{required:true}"/> - </div> - </div> - <div class="field email required"> - <label class="label" for="email"><span><?= $block->escapeHtml(__('Email')) ?></span></label> - <div class="control"> -- <input name="email" id="email" title="<?= $block->escapeHtmlAttr(__('Email')) ?>" value="<?= $block->escapeHtmlAttr($this->helper('Magento\Contact\Helper\Data')->getPostValue('email') ?: $this->helper('Magento\Contact\Helper\Data')->getUserEmail()) ?>" class="input-text" type="email" data-validate="{required:true, 'validate-email':true}"/> -+ <input name="email" id="email" title="<?= $block->escapeHtmlAttr(__('Email')) ?>" value="<?= $block->escapeHtmlAttr($this->helper(\Magento\Contact\Helper\Data::class)->getPostValue('email') ?: $this->helper(\Magento\Contact\Helper\Data::class)->getUserEmail()) ?>" class="input-text" type="email" data-validate="{required:true, 'validate-email':true}"/> - </div> - </div> - <div class="field telephone"> - <label class="label" for="telephone"><span><?= $block->escapeHtml(__('Phone Number')) ?></span></label> - <div class="control"> -- <input name="telephone" id="telephone" title="<?= $block->escapeHtmlAttr(__('Phone Number')) ?>" value="<?= $block->escapeHtmlAttr($this->helper('Magento\Contact\Helper\Data')->getPostValue('telephone')) ?>" class="input-text" type="text" /> -+ <input name="telephone" id="telephone" title="<?= $block->escapeHtmlAttr(__('Phone Number')) ?>" value="<?= $block->escapeHtmlAttr($this->helper(\Magento\Contact\Helper\Data::class)->getPostValue('telephone')) ?>" class="input-text" type="text" /> - </div> - </div> - <div class="field comment required"> - <label class="label" for="comment"><span><?= $block->escapeHtml(__('What’s on your mind?')) ?></span></label> - <div class="control"> -- <textarea name="comment" id="comment" title="<?= $block->escapeHtmlAttr(__('What’s on your mind?')) ?>" class="input-text" cols="5" rows="3" data-validate="{required:true}"><?= $block->escapeHtml($this->helper('Magento\Contact\Helper\Data')->getPostValue('comment')) ?></textarea> -+ <textarea name="comment" id="comment" title="<?= $block->escapeHtmlAttr(__('What’s on your mind?')) ?>" class="input-text" cols="5" rows="3" data-validate="{required:true}"><?= $block->escapeHtml($this->helper(\Magento\Contact\Helper\Data::class)->getPostValue('comment')) ?></textarea> - </div> - </div> - <?= $block->getChildHtml('form.additional.info') ?> -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 00000000000..d79806eecbe ---- /dev/null -+++ b/app/code/Magento/Contact/view/frontend/web/css/source/_module.less -@@ -0,0 +1,52 @@ -+/** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+*/ -+ -+& when (@media-common = true) { -+ .contact-index-index { -+ .column:not(.sidebar-main) { -+ .form.contact { -+ float: none; -+ width: 50%; -+ } -+ } -+ -+ .column:not(.sidebar-additional) { -+ .form.contact { -+ float: none; -+ width: 50%; -+ } -+ } -+ } -+} -+ -+// -+// Desktop -+// _____________________________________________ -+ -+.media-width(@extremum, @break) when (@extremum = 'min') and (@break = @screen__m) { -+ .contact-index-index .column:not(.sidebar-additional) .form.contact { -+ min-width: 600px; -+ } -+} -+ -+// Mobile -+.media-width(@extremum, @break) when (@extremum = 'max') and (@break = @screen__m) { -+ .contact-index-index { -+ .column:not(.sidebar-main) { -+ .form.contact { -+ float: none; -+ width: 100%; -+ } -+ } -+ -+ .column:not(.sidebar-additional) { -+ .form.contact { -+ float: none; -+ width: 100%; -+ } -+ } -+ } -+} -+ -diff --git a/app/code/Magento/Cookie/Helper/Cookie.php b/app/code/Magento/Cookie/Helper/Cookie.php -index 05ab02d7a2a..8bab596ab4c 100644 ---- a/app/code/Magento/Cookie/Helper/Cookie.php -+++ b/app/code/Magento/Cookie/Helper/Cookie.php -@@ -42,7 +42,8 @@ class Cookie extends \Magento\Framework\App\Helper\AbstractHelper - * @param \Magento\Store\Model\StoreManagerInterface $storeManager - * @param array $data - * -- * @throws \InvalidArgumentException -+ * @throws \Magento\Framework\Exception\LocalizedException -+ * @throws \Magento\Framework\Exception\NoSuchEntityException - */ - public function __construct( - \Magento\Framework\App\Helper\Context $context, -diff --git a/app/code/Magento/Cookie/etc/adminhtml/system.xml b/app/code/Magento/Cookie/etc/adminhtml/system.xml -index 26c963ddba7..97904109690 100644 ---- a/app/code/Magento/Cookie/etc/adminhtml/system.xml -+++ b/app/code/Magento/Cookie/etc/adminhtml/system.xml -@@ -22,7 +22,7 @@ - <label>Cookie Domain</label> - <backend_model>Magento\Cookie\Model\Config\Backend\Domain</backend_model> - </field> -- <field id="cookie_httponly" translate="label" type="select" sortOrder="40" showInDefault="1" showInWebsite="1" showInStore="1"> -+ <field id="cookie_httponly" translate="label comment" type="select" sortOrder="40" showInDefault="1" showInWebsite="1" showInStore="1"> - <label>Use HTTP Only</label> - <comment> - <![CDATA[<strong style="color:red">Warning</strong>: Do not set to "No". User security could be compromised.]]> -diff --git a/app/code/Magento/Cookie/view/frontend/templates/html/notices.phtml b/app/code/Magento/Cookie/view/frontend/templates/html/notices.phtml -index c5cfda8cd7d..8712f31e71b 100644 ---- a/app/code/Magento/Cookie/view/frontend/templates/html/notices.phtml -+++ b/app/code/Magento/Cookie/view/frontend/templates/html/notices.phtml -@@ -4,11 +4,9 @@ - * See COPYING.txt for license details. - */ - --// @codingStandardsIgnoreFile -- - /** @var \Magento\Cookie\Block\Html\Notices $block */ - ?> --<?php if ($this->helper(\Magento\Cookie\Helper\Cookie::class)->isCookieRestrictionModeEnabled()): ?> -+<?php if ($this->helper(\Magento\Cookie\Helper\Cookie::class)->isCookieRestrictionModeEnabled()) : ?> - <div role="alertdialog" - tabindex="-1" - class="message global cookie" -diff --git a/app/code/Magento/Cron/Model/Config/Backend/Product/Alert.php b/app/code/Magento/Cron/Model/Config/Backend/Product/Alert.php -index 2fc0f0ab4c1..eeef291fb6a 100644 ---- a/app/code/Magento/Cron/Model/Config/Backend/Product/Alert.php -+++ b/app/code/Magento/Cron/Model/Config/Backend/Product/Alert.php -@@ -11,6 +11,9 @@ - */ - namespace Magento\Cron\Model\Config\Backend\Product; - -+/** -+ * Cron job Alert configuration -+ */ - class Alert extends \Magento\Framework\App\Config\Value - { - /** -@@ -61,7 +64,7 @@ class Alert extends \Magento\Framework\App\Config\Value - } - - /** -- * {@inheritdoc} -+ * @inheritdoc - * - * @return $this - * @throws \Exception -@@ -72,8 +75,8 @@ class Alert extends \Magento\Framework\App\Config\Value - $frequency = $this->getData('groups/productalert_cron/fields/frequency/value'); - - $cronExprArray = [ -- intval($time[1]), //Minute -- intval($time[0]), //Hour -+ (int)$time[1], //Minute -+ (int)$time[0], //Hour - $frequency == \Magento\Cron\Model\Config\Source\Frequency::CRON_MONTHLY ? '1' : '*', //Day of the Month - '*', //Month of the Year - $frequency == \Magento\Cron\Model\Config\Source\Frequency::CRON_WEEKLY ? '1' : '*', //Day of the Week -diff --git a/app/code/Magento/Cron/Model/Config/Backend/Sitemap.php b/app/code/Magento/Cron/Model/Config/Backend/Sitemap.php -index 68112991664..44ed4c001d2 100644 ---- a/app/code/Magento/Cron/Model/Config/Backend/Sitemap.php -+++ b/app/code/Magento/Cron/Model/Config/Backend/Sitemap.php -@@ -11,6 +11,9 @@ - */ - namespace Magento\Cron\Model\Config\Backend; - -+/** -+ * Sitemap configuration -+ */ - class Sitemap extends \Magento\Framework\App\Config\Value - { - /** -@@ -61,6 +64,8 @@ class Sitemap extends \Magento\Framework\App\Config\Value - } - - /** -+ * After save handler -+ * - * @return $this - * @throws \Exception - */ -@@ -70,8 +75,8 @@ class Sitemap extends \Magento\Framework\App\Config\Value - $frequency = $this->getData('groups/generate/fields/frequency/value'); - - $cronExprArray = [ -- intval($time[1]), //Minute -- intval($time[0]), //Hour -+ (int)$time[1], //Minute -+ (int)$time[0], //Hour - $frequency == \Magento\Cron\Model\Config\Source\Frequency::CRON_MONTHLY ? '1' : '*', //Day of the Month - '*', //Month of the Year - $frequency == \Magento\Cron\Model\Config\Source\Frequency::CRON_WEEKLY ? '1' : '*', //# Day of the Week -diff --git a/app/code/Magento/Cron/Model/Schedule.php b/app/code/Magento/Cron/Model/Schedule.php -index b127ecae6f9..582c7c811b7 100644 ---- a/app/code/Magento/Cron/Model/Schedule.php -+++ b/app/code/Magento/Cron/Model/Schedule.php -@@ -9,6 +9,7 @@ namespace Magento\Cron\Model; - use Magento\Framework\Exception\CronException; - use Magento\Framework\App\ObjectManager; - use Magento\Framework\Stdlib\DateTime\TimezoneInterface; -+use Magento\Framework\Intl\DateTimeFactory; - - /** - * Crontab schedule model -@@ -50,13 +51,19 @@ class Schedule extends \Magento\Framework\Model\AbstractModel - */ - private $timezoneConverter; - -+ /** -+ * @var DateTimeFactory -+ */ -+ private $dateTimeFactory; -+ - /** - * @param \Magento\Framework\Model\Context $context - * @param \Magento\Framework\Registry $registry - * @param \Magento\Framework\Model\ResourceModel\AbstractResource $resource - * @param \Magento\Framework\Data\Collection\AbstractDb $resourceCollection - * @param array $data -- * @param TimezoneInterface $timezoneConverter -+ * @param TimezoneInterface|null $timezoneConverter -+ * @param DateTimeFactory|null $dateTimeFactory - */ - public function __construct( - \Magento\Framework\Model\Context $context, -@@ -64,14 +71,16 @@ class Schedule extends \Magento\Framework\Model\AbstractModel - \Magento\Framework\Model\ResourceModel\AbstractResource $resource = null, - \Magento\Framework\Data\Collection\AbstractDb $resourceCollection = null, - array $data = [], -- TimezoneInterface $timezoneConverter = null -+ TimezoneInterface $timezoneConverter = null, -+ DateTimeFactory $dateTimeFactory = null - ) { - parent::__construct($context, $registry, $resource, $resourceCollection, $data); - $this->timezoneConverter = $timezoneConverter ?: ObjectManager::getInstance()->get(TimezoneInterface::class); -+ $this->dateTimeFactory = $dateTimeFactory ?: ObjectManager::getInstance()->get(DateTimeFactory::class); - } - - /** -- * @return void -+ * @inheritdoc - */ - public function _construct() - { -@@ -79,6 +88,8 @@ class Schedule extends \Magento\Framework\Model\AbstractModel - } - - /** -+ * Set cron expression. -+ * - * @param string $expr - * @return $this - * @throws \Magento\Framework\Exception\CronException -@@ -87,7 +98,7 @@ class Schedule extends \Magento\Framework\Model\AbstractModel - { - $e = preg_split('#\s+#', $expr, null, PREG_SPLIT_NO_EMPTY); - if (sizeof($e) < 5 || sizeof($e) > 6) { -- throw new CronException(__('The "%1" cron expression is invalid. Verify and try again.', $expr)); -+ throw new CronException(__('Invalid cron expression: %1', $expr)); - } - - $this->setCronExprArr($e); -@@ -95,7 +106,7 @@ class Schedule extends \Magento\Framework\Model\AbstractModel - } - - /** -- * Checks the observer's cron expression against time -+ * Checks the observer's cron expression against time. - * - * Supports $this->setCronExpr('* 0-5,10-59/5 2-10,15-25 january-june/2 mon-fri') - * -@@ -109,22 +120,27 @@ class Schedule extends \Magento\Framework\Model\AbstractModel - if (!$e || !$time) { - return false; - } -+ $configTimeZone = $this->timezoneConverter->getConfigTimezone(); -+ $storeDateTime = $this->dateTimeFactory->create(null, new \DateTimeZone($configTimeZone)); - if (!is_numeric($time)) { - //convert time from UTC to admin store timezone - //we assume that all schedules in configuration (crontab.xml and DB tables) are in admin store timezone -- $time = $this->timezoneConverter->date($time)->format('Y-m-d H:i'); -- $time = strtotime($time); -+ $dateTimeUtc = $this->dateTimeFactory->create($time); -+ $time = $dateTimeUtc->getTimestamp(); - } -- $match = $this->matchCronExpression($e[0], strftime('%M', $time)) -- && $this->matchCronExpression($e[1], strftime('%H', $time)) -- && $this->matchCronExpression($e[2], strftime('%d', $time)) -- && $this->matchCronExpression($e[3], strftime('%m', $time)) -- && $this->matchCronExpression($e[4], strftime('%w', $time)); -+ $time = $storeDateTime->setTimestamp($time); -+ $match = $this->matchCronExpression($e[0], $time->format('i')) -+ && $this->matchCronExpression($e[1], $time->format('H')) -+ && $this->matchCronExpression($e[2], $time->format('d')) -+ && $this->matchCronExpression($e[3], $time->format('m')) -+ && $this->matchCronExpression($e[4], $time->format('w')); - - return $match; - } - - /** -+ * Match cron expression. -+ * - * @param string $expr - * @param int $num - * @return bool -@@ -184,13 +200,15 @@ class Schedule extends \Magento\Framework\Model\AbstractModel - } - - if ($from === false || $to === false) { -- throw new CronException(__('The "%1" cron expression is invalid. Verify and try again.', $expr)); -+ throw new CronException(__('Invalid cron expression: %1', $expr)); - } - - return $num >= $from && $num <= $to && $num % $mod === 0; - } - - /** -+ * Get number of a month. -+ * - * @param int|string $value - * @return bool|int|string - */ -diff --git a/app/code/Magento/Cron/Observer/ProcessCronQueueObserver.php b/app/code/Magento/Cron/Observer/ProcessCronQueueObserver.php -index ed5e46d7a60..5c8aa1dc78a 100644 ---- a/app/code/Magento/Cron/Observer/ProcessCronQueueObserver.php -+++ b/app/code/Magento/Cron/Observer/ProcessCronQueueObserver.php -@@ -3,18 +3,21 @@ - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -- - /** - * Handling cron jobs - */ - namespace Magento\Cron\Observer; - -+use Magento\Cron\Model\Schedule; - use Magento\Framework\App\State; - use Magento\Framework\Console\Cli; - use Magento\Framework\Event\ObserverInterface; --use Magento\Cron\Model\Schedule; -+use Magento\Framework\Profiler\Driver\Standard\Stat; -+use Magento\Framework\Profiler\Driver\Standard\StatFactory; - - /** -+ * The observer for processing cron jobs. -+ * - * @SuppressWarnings(PHPMD.CouplingBetweenObjects) - */ - class ProcessCronQueueObserver implements ObserverInterface -@@ -56,6 +59,16 @@ class ProcessCronQueueObserver implements ObserverInterface - */ - const SECONDS_IN_MINUTE = 60; - -+ /** -+ * How long to wait for cron group to become unlocked -+ */ -+ const LOCK_TIMEOUT = 5; -+ -+ /** -+ * Static lock prefix for cron group locking -+ */ -+ const LOCK_PREFIX = 'CRON_GROUP_'; -+ - /** - * @var \Magento\Cron\Model\ResourceModel\Schedule\Collection - */ -@@ -116,15 +129,20 @@ class ProcessCronQueueObserver implements ObserverInterface - */ - private $state; - -+ /** -+ * @var \Magento\Framework\Lock\LockManagerInterface -+ */ -+ private $lockManager; -+ - /** - * @var array - */ - private $invalid = []; - - /** -- * @var array -+ * @var Stat - */ -- private $jobs; -+ private $statProfiler; - - /** - * @param \Magento\Framework\ObjectManagerInterface $objectManager -@@ -137,7 +155,9 @@ class ProcessCronQueueObserver implements ObserverInterface - * @param \Magento\Framework\Stdlib\DateTime\DateTime $dateTime - * @param \Magento\Framework\Process\PhpExecutableFinderFactory $phpExecutableFinderFactory - * @param \Psr\Log\LoggerInterface $logger -- * @param \Magento\Framework\App\State $state -+ * @param State $state -+ * @param StatFactory $statFactory -+ * @param \Magento\Framework\Lock\LockManagerInterface $lockManager - * @SuppressWarnings(PHPMD.ExcessiveParameterList) - */ - public function __construct( -@@ -151,7 +171,9 @@ class ProcessCronQueueObserver implements ObserverInterface - \Magento\Framework\Stdlib\DateTime\DateTime $dateTime, - \Magento\Framework\Process\PhpExecutableFinderFactory $phpExecutableFinderFactory, - \Psr\Log\LoggerInterface $logger, -- \Magento\Framework\App\State $state -+ \Magento\Framework\App\State $state, -+ StatFactory $statFactory, -+ \Magento\Framework\Lock\LockManagerInterface $lockManager - ) { - $this->_objectManager = $objectManager; - $this->_scheduleFactory = $scheduleFactory; -@@ -164,6 +186,8 @@ class ProcessCronQueueObserver implements ObserverInterface - $this->phpExecutableFinder = $phpExecutableFinderFactory->create(); - $this->logger = $logger; - $this->state = $state; -+ $this->statProfiler = $statFactory->create(); -+ $this->lockManager = $lockManager; - } - - /** -@@ -179,27 +203,25 @@ class ProcessCronQueueObserver implements ObserverInterface - */ - public function execute(\Magento\Framework\Event\Observer $observer) - { -- $pendingJobs = $this->_getPendingSchedules(); - $currentTime = $this->dateTime->gmtTimestamp(); - $jobGroupsRoot = $this->_config->getJobs(); -+ // sort jobs groups to start from used in separated process -+ uksort( -+ $jobGroupsRoot, -+ function ($a, $b) { -+ return $this->getCronGroupConfigurationValue($b, 'use_separate_process') -+ - $this->getCronGroupConfigurationValue($a, 'use_separate_process'); -+ } -+ ); - - $phpPath = $this->phpExecutableFinder->find() ?: 'php'; - - foreach ($jobGroupsRoot as $groupId => $jobsRoot) { -- $this->_cleanup($groupId); -- $this->_generate($groupId); -- if ($this->_request->getParam('group') !== null -- && $this->_request->getParam('group') !== '\'' . ($groupId) . '\'' -- && $this->_request->getParam('group') !== $groupId -- ) { -+ if (!$this->isGroupInFilter($groupId)) { - continue; - } -- if (($this->_request->getParam(self::STANDALONE_PROCESS_STARTED) !== '1') && ( -- $this->_scopeConfig->getValue( -- 'system/cron/' . $groupId . '/use_separate_process', -- \Magento\Store\Model\ScopeInterface::SCOPE_STORE -- ) == 1 -- ) -+ if ($this->_request->getParam(self::STANDALONE_PROCESS_STARTED) !== '1' -+ && $this->getCronGroupConfigurationValue($groupId, 'use_separate_process') == 1 - ) { - $this->_shell->execute( - $phpPath . ' %s cron:run --group=' . $groupId . ' --' . Cli::INPUT_KEY_BOOTSTRAP . '=' -@@ -211,42 +233,42 @@ class ProcessCronQueueObserver implements ObserverInterface - continue; - } - -- /** @var \Magento\Cron\Model\Schedule $schedule */ -- foreach ($pendingJobs as $schedule) { -- $jobConfig = isset($jobsRoot[$schedule->getJobCode()]) ? $jobsRoot[$schedule->getJobCode()] : null; -- if (!$jobConfig) { -- continue; -- } -- -- $scheduledTime = strtotime($schedule->getScheduledAt()); -- if ($scheduledTime > $currentTime) { -- continue; -+ $this->lockGroup( -+ $groupId, -+ function ($groupId) use ($currentTime, $jobsRoot) { -+ $this->cleanupJobs($groupId, $currentTime); -+ $this->generateSchedules($groupId); -+ $this->processPendingJobs($groupId, $jobsRoot, $currentTime); - } -+ ); -+ } -+ } - -- try { -- if ($schedule->tryLockJob()) { -- $this->_runJob($scheduledTime, $currentTime, $jobConfig, $schedule, $groupId); -- } -- } catch (\Exception $e) { -- $schedule->setMessages($e->getMessage()); -- if ($schedule->getStatus() === Schedule::STATUS_ERROR) { -- $this->logger->critical($e); -- } -- if ($schedule->getStatus() === Schedule::STATUS_MISSED -- && $this->state->getMode() === State::MODE_DEVELOPER -- ) { -- $this->logger->info( -- sprintf( -- "%s Schedule Id: %s Job Code: %s", -- $schedule->getMessages(), -- $schedule->getScheduleId(), -- $schedule->getJobCode() -- ) -- ); -- } -- } -- $schedule->save(); -- } -+ /** -+ * Lock group -+ * -+ * It should be taken by standalone (child) process, not by the parent process. -+ * -+ * @param int $groupId -+ * @param callable $callback -+ * -+ * @return void -+ */ -+ private function lockGroup($groupId, callable $callback) -+ { -+ if (!$this->lockManager->lock(self::LOCK_PREFIX . $groupId, self::LOCK_TIMEOUT)) { -+ $this->logger->warning( -+ sprintf( -+ "Could not acquire lock for cron group: %s, skipping run", -+ $groupId -+ ) -+ ); -+ return; -+ } -+ try { -+ $callback($groupId); -+ } finally { -+ $this->lockManager->unlock(self::LOCK_PREFIX . $groupId); - } - } - -@@ -263,24 +285,25 @@ class ProcessCronQueueObserver implements ObserverInterface - */ - protected function _runJob($scheduledTime, $currentTime, $jobConfig, $schedule, $groupId) - { -- $scheduleLifetime = (int)$this->_scopeConfig->getValue( -- 'system/cron/' . $groupId . '/' . self::XML_PATH_SCHEDULE_LIFETIME, -- \Magento\Store\Model\ScopeInterface::SCOPE_STORE -- ); -+ $jobCode = $schedule->getJobCode(); -+ $scheduleLifetime = $this->getCronGroupConfigurationValue($groupId, self::XML_PATH_SCHEDULE_LIFETIME); - $scheduleLifetime = $scheduleLifetime * self::SECONDS_IN_MINUTE; - if ($scheduledTime < $currentTime - $scheduleLifetime) { - $schedule->setStatus(Schedule::STATUS_MISSED); -- throw new \Exception('Too late for the schedule'); -+ // phpcs:ignore Magento2.Exceptions.DirectThrow -+ throw new \Exception(sprintf('Cron Job %s is missed at %s', $jobCode, $schedule->getScheduledAt())); - } - - if (!isset($jobConfig['instance'], $jobConfig['method'])) { - $schedule->setStatus(Schedule::STATUS_ERROR); -- throw new \Exception('No callbacks found'); -+ // phpcs:ignore Magento2.Exceptions.DirectThrow -+ throw new \Exception(sprintf('No callbacks found for cron job %s', $jobCode)); - } - $model = $this->_objectManager->create($jobConfig['instance']); - $callback = [$model, $jobConfig['method']]; - if (!is_callable($callback)) { - $schedule->setStatus(Schedule::STATUS_ERROR); -+ // phpcs:ignore Magento2.Exceptions.DirectThrow - throw new \Exception( - sprintf('Invalid callback: %s::%s can\'t be called', $jobConfig['instance'], $jobConfig['method']) - ); -@@ -288,10 +311,21 @@ class ProcessCronQueueObserver implements ObserverInterface - - $schedule->setExecutedAt(strftime('%Y-%m-%d %H:%M:%S', $this->dateTime->gmtTimestamp()))->save(); - -+ $this->startProfiling(); - try { -+ $this->logger->info(sprintf('Cron Job %s is run', $jobCode)); -+ //phpcs:ignore Magento2.Functions.DiscouragedFunction - call_user_func_array($callback, [$schedule]); - } catch (\Throwable $e) { - $schedule->setStatus(Schedule::STATUS_ERROR); -+ $this->logger->error( -+ sprintf( -+ 'Cron Job %s has an error: %s. Statistics: %s', -+ $jobCode, -+ $e->getMessage(), -+ $this->getProfilingStat() -+ ) -+ ); - if (!$e instanceof \Exception) { - $e = new \RuntimeException( - 'Error when running a cron job', -@@ -300,28 +334,96 @@ class ProcessCronQueueObserver implements ObserverInterface - ); - } - throw $e; -+ } finally { -+ $this->stopProfiling(); - } - -- $schedule->setStatus(Schedule::STATUS_SUCCESS)->setFinishedAt(strftime( -- '%Y-%m-%d %H:%M:%S', -- $this->dateTime->gmtTimestamp() -- )); -+ $schedule->setStatus( -+ Schedule::STATUS_SUCCESS -+ )->setFinishedAt( -+ strftime( -+ '%Y-%m-%d %H:%M:%S', -+ $this->dateTime->gmtTimestamp() -+ ) -+ ); -+ -+ $this->logger->info( -+ sprintf( -+ 'Cron Job %s is successfully finished. Statistics: %s', -+ $jobCode, -+ $this->getProfilingStat() -+ ) -+ ); -+ } -+ -+ /** -+ * Starts profiling -+ * -+ * @return void -+ */ -+ private function startProfiling() -+ { -+ $this->statProfiler->clear(); -+ $this->statProfiler->start('job', microtime(true), memory_get_usage(true), memory_get_usage()); -+ } -+ -+ /** -+ * Stops profiling -+ * -+ * @return void -+ */ -+ private function stopProfiling() -+ { -+ $this->statProfiler->stop('job', microtime(true), memory_get_usage(true), memory_get_usage()); - } - - /** -- * Return job collection from data base with status 'pending' -+ * Retrieves statistics in the JSON format - * -+ * @return string -+ */ -+ private function getProfilingStat() -+ { -+ $stat = $this->statProfiler->get('job'); -+ unset($stat[Stat::START]); -+ return json_encode($stat); -+ } -+ -+ /** -+ * Return job collection from data base with status 'pending'. -+ * -+ * @param string $groupId - * @return \Magento\Cron\Model\ResourceModel\Schedule\Collection - */ -- protected function _getPendingSchedules() -+ private function getPendingSchedules($groupId) - { -- if (!$this->_pendingSchedules) { -- $this->_pendingSchedules = $this->_scheduleFactory->create()->getCollection()->addFieldToFilter( -- 'status', -- Schedule::STATUS_PENDING -- )->load(); -- } -- return $this->_pendingSchedules; -+ $jobs = $this->_config->getJobs(); -+ $pendingJobs = $this->_scheduleFactory->create()->getCollection(); -+ $pendingJobs->addFieldToFilter('status', Schedule::STATUS_PENDING); -+ $pendingJobs->addFieldToFilter('job_code', ['in' => array_keys($jobs[$groupId])]); -+ return $pendingJobs; -+ } -+ -+ /** -+ * Return job collection from database with status 'pending', 'running' or 'success' -+ * -+ * @param string $groupId -+ * @return \Magento\Framework\Model\ResourceModel\Db\Collection\AbstractCollection -+ */ -+ private function getNonExitedSchedules($groupId) -+ { -+ $jobs = $this->_config->getJobs(); -+ $pendingJobs = $this->_scheduleFactory->create()->getCollection(); -+ $pendingJobs->addFieldToFilter( -+ 'status', -+ [ -+ 'in' => [ -+ Schedule::STATUS_PENDING, Schedule::STATUS_RUNNING, Schedule::STATUS_SUCCESS -+ ] -+ ] -+ ); -+ $pendingJobs->addFieldToFilter('job_code', ['in' => array_keys($jobs[$groupId])]); -+ return $pendingJobs; - } - - /** -@@ -330,22 +432,32 @@ class ProcessCronQueueObserver implements ObserverInterface - * @param string $groupId - * @return $this - */ -- protected function _generate($groupId) -+ private function generateSchedules($groupId) - { - /** - * check if schedule generation is needed - */ - $lastRun = (int)$this->_cache->load(self::CACHE_KEY_LAST_SCHEDULE_GENERATE_AT . $groupId); -- $rawSchedulePeriod = (int)$this->_scopeConfig->getValue( -- 'system/cron/' . $groupId . '/' . self::XML_PATH_SCHEDULE_GENERATE_EVERY, -- \Magento\Store\Model\ScopeInterface::SCOPE_STORE -+ $rawSchedulePeriod = (int)$this->getCronGroupConfigurationValue( -+ $groupId, -+ self::XML_PATH_SCHEDULE_GENERATE_EVERY - ); - $schedulePeriod = $rawSchedulePeriod * self::SECONDS_IN_MINUTE; - if ($lastRun > $this->dateTime->gmtTimestamp() - $schedulePeriod) { - return $this; - } - -- $schedules = $this->_getPendingSchedules(); -+ /** -+ * save time schedules generation was ran with no expiration -+ */ -+ $this->_cache->save( -+ $this->dateTime->gmtTimestamp(), -+ self::CACHE_KEY_LAST_SCHEDULE_GENERATE_AT . $groupId, -+ ['crontab'], -+ null -+ ); -+ -+ $schedules = $this->getNonExitedSchedules($groupId); - $exists = []; - /** @var Schedule $schedule */ - foreach ($schedules as $schedule) { -@@ -355,21 +467,11 @@ class ProcessCronQueueObserver implements ObserverInterface - /** - * generate global crontab jobs - */ -- $jobs = $this->getJobs(); -+ $jobs = $this->_config->getJobs(); - $this->invalid = []; - $this->_generateJobs($jobs[$groupId], $exists, $groupId); - $this->cleanupScheduleMismatches(); - -- /** -- * save time schedules generation was ran with no expiration -- */ -- $this->_cache->save( -- $this->dateTime->gmtTimestamp(), -- self::CACHE_KEY_LAST_SCHEDULE_GENERATE_AT . $groupId, -- ['crontab'], -- null -- ); -- - return $this; - } - -@@ -379,7 +481,7 @@ class ProcessCronQueueObserver implements ObserverInterface - * @param array $jobs - * @param array $exists - * @param string $groupId -- * @return $this -+ * @return void - */ - protected function _generateJobs($jobs, $exists, $groupId) - { -@@ -392,80 +494,65 @@ class ProcessCronQueueObserver implements ObserverInterface - $timeInterval = $this->getScheduleTimeInterval($groupId); - $this->saveSchedule($jobCode, $cronExpression, $timeInterval, $exists); - } -- return $this; - } - - /** - * Clean expired jobs - * - * @param string $groupId -- * @return $this -+ * @param int $currentTime -+ * @return void - */ -- protected function _cleanup($groupId) -+ private function cleanupJobs($groupId, $currentTime) - { -- $this->cleanupDisabledJobs($groupId); -- - // check if history cleanup is needed - $lastCleanup = (int)$this->_cache->load(self::CACHE_KEY_LAST_HISTORY_CLEANUP_AT . $groupId); -- $historyCleanUp = (int)$this->_scopeConfig->getValue( -- 'system/cron/' . $groupId . '/' . self::XML_PATH_HISTORY_CLEANUP_EVERY, -- \Magento\Store\Model\ScopeInterface::SCOPE_STORE -- ); -+ $historyCleanUp = (int)$this->getCronGroupConfigurationValue($groupId, self::XML_PATH_HISTORY_CLEANUP_EVERY); - if ($lastCleanup > $this->dateTime->gmtTimestamp() - $historyCleanUp * self::SECONDS_IN_MINUTE) { - return $this; - } -- -- // check how long the record should stay unprocessed before marked as MISSED -- $scheduleLifetime = (int)$this->_scopeConfig->getValue( -- 'system/cron/' . $groupId . '/' . self::XML_PATH_SCHEDULE_LIFETIME, -- \Magento\Store\Model\ScopeInterface::SCOPE_STORE -+ // save time history cleanup was ran with no expiration -+ $this->_cache->save( -+ $this->dateTime->gmtTimestamp(), -+ self::CACHE_KEY_LAST_HISTORY_CLEANUP_AT . $groupId, -+ ['crontab'], -+ null - ); -- $scheduleLifetime = $scheduleLifetime * self::SECONDS_IN_MINUTE; - -- /** -- * @var \Magento\Cron\Model\ResourceModel\Schedule\Collection $history -- */ -- $history = $this->_scheduleFactory->create()->getCollection()->addFieldToFilter( -- 'status', -- ['in' => [Schedule::STATUS_SUCCESS, Schedule::STATUS_MISSED, Schedule::STATUS_ERROR]] -- )->load(); -+ $this->cleanupDisabledJobs($groupId); - -- $historySuccess = (int)$this->_scopeConfig->getValue( -- 'system/cron/' . $groupId . '/' . self::XML_PATH_HISTORY_SUCCESS, -- \Magento\Store\Model\ScopeInterface::SCOPE_STORE -- ); -- $historyFailure = (int)$this->_scopeConfig->getValue( -- 'system/cron/' . $groupId . '/' . self::XML_PATH_HISTORY_FAILURE, -- \Magento\Store\Model\ScopeInterface::SCOPE_STORE -- ); -+ $historySuccess = (int)$this->getCronGroupConfigurationValue($groupId, self::XML_PATH_HISTORY_SUCCESS); -+ $historyFailure = (int)$this->getCronGroupConfigurationValue($groupId, self::XML_PATH_HISTORY_FAILURE); - $historyLifetimes = [ - Schedule::STATUS_SUCCESS => $historySuccess * self::SECONDS_IN_MINUTE, - Schedule::STATUS_MISSED => $historyFailure * self::SECONDS_IN_MINUTE, - Schedule::STATUS_ERROR => $historyFailure * self::SECONDS_IN_MINUTE, -+ Schedule::STATUS_PENDING => max($historyFailure, $historySuccess) * self::SECONDS_IN_MINUTE, - ]; - -- $now = $this->dateTime->gmtTimestamp(); -- /** @var Schedule $record */ -- foreach ($history as $record) { -- $checkTime = $record->getExecutedAt() ? strtotime($record->getExecutedAt()) : -- strtotime($record->getScheduledAt()) + $scheduleLifetime; -- if ($checkTime < $now - $historyLifetimes[$record->getStatus()]) { -- $record->delete(); -- } -+ $jobs = $this->_config->getJobs()[$groupId]; -+ $scheduleResource = $this->_scheduleFactory->create()->getResource(); -+ $connection = $scheduleResource->getConnection(); -+ $count = 0; -+ foreach ($historyLifetimes as $status => $time) { -+ $count += $connection->delete( -+ $scheduleResource->getMainTable(), -+ [ -+ 'status = ?' => $status, -+ 'job_code in (?)' => array_keys($jobs), -+ 'created_at < ?' => $connection->formatDate($currentTime - $time) -+ ] -+ ); - } - -- // save time history cleanup was ran with no expiration -- $this->_cache->save( -- $this->dateTime->gmtTimestamp(), -- self::CACHE_KEY_LAST_HISTORY_CLEANUP_AT . $groupId, -- ['crontab'], -- null -- ); -- -- return $this; -+ if ($count) { -+ $this->logger->info(sprintf('%d cron jobs were cleaned', $count)); -+ } - } - - /** -+ * Get config of schedule. -+ * - * @param array $jobConfig - * @return mixed - */ -@@ -480,6 +567,8 @@ class ProcessCronQueueObserver implements ObserverInterface - } - - /** -+ * Save a schedule of cron job. -+ * - * @param string $jobCode - * @param string $cronExpression - * @param int $timeInterval -@@ -493,7 +582,7 @@ class ProcessCronQueueObserver implements ObserverInterface - for ($time = $currentTime; $time < $timeAhead; $time += self::SECONDS_IN_MINUTE) { - $scheduledAt = strftime('%Y-%m-%d %H:%M:00', $time); - $alreadyScheduled = !empty($exists[$jobCode . '/' . $scheduledAt]); -- $schedule = $this->generateSchedule($jobCode, $cronExpression, $time); -+ $schedule = $this->createSchedule($jobCode, $cronExpression, $time); - $valid = $schedule->trySchedule(); - if (!$valid) { - if ($alreadyScheduled) { -@@ -512,12 +601,14 @@ class ProcessCronQueueObserver implements ObserverInterface - } - - /** -+ * Create a schedule of cron job. -+ * - * @param string $jobCode - * @param string $cronExpression - * @param int $time - * @return Schedule - */ -- protected function generateSchedule($jobCode, $cronExpression, $time) -+ protected function createSchedule($jobCode, $cronExpression, $time) - { - $schedule = $this->_scheduleFactory->create() - ->setCronExpr($cronExpression) -@@ -530,43 +621,55 @@ class ProcessCronQueueObserver implements ObserverInterface - } - - /** -+ * Get time interval for scheduling. -+ * - * @param string $groupId - * @return int - */ - protected function getScheduleTimeInterval($groupId) - { -- $scheduleAheadFor = (int)$this->_scopeConfig->getValue( -- 'system/cron/' . $groupId . '/' . self::XML_PATH_SCHEDULE_AHEAD_FOR, -- \Magento\Store\Model\ScopeInterface::SCOPE_STORE -- ); -+ $scheduleAheadFor = (int)$this->getCronGroupConfigurationValue($groupId, self::XML_PATH_SCHEDULE_AHEAD_FOR); - $scheduleAheadFor = $scheduleAheadFor * self::SECONDS_IN_MINUTE; - - return $scheduleAheadFor; - } - - /** -- * Clean up scheduled jobs that are disabled in the configuration -- * This can happen when you turn off a cron job in the config and flush the cache -+ * Clean up scheduled jobs that are disabled in the configuration. -+ * -+ * This can happen when you turn off a cron job in the config and flush the cache. - * - * @param string $groupId - * @return void - */ - private function cleanupDisabledJobs($groupId) - { -- $jobs = $this->getJobs(); -+ $jobs = $this->_config->getJobs(); -+ $jobsToCleanup = []; - foreach ($jobs[$groupId] as $jobCode => $jobConfig) { - if (!$this->getCronExpression($jobConfig)) { - /** @var \Magento\Cron\Model\ResourceModel\Schedule $scheduleResource */ -- $scheduleResource = $this->_scheduleFactory->create()->getResource(); -- $scheduleResource->getConnection()->delete($scheduleResource->getMainTable(), [ -- 'status=?' => Schedule::STATUS_PENDING, -- 'job_code=?' => $jobCode, -- ]); -+ $jobsToCleanup[] = $jobCode; - } - } -+ -+ if (count($jobsToCleanup) > 0) { -+ $scheduleResource = $this->_scheduleFactory->create()->getResource(); -+ $count = $scheduleResource->getConnection()->delete( -+ $scheduleResource->getMainTable(), -+ [ -+ 'status = ?' => Schedule::STATUS_PENDING, -+ 'job_code in (?)' => $jobsToCleanup, -+ ] -+ ); -+ -+ $this->logger->info(sprintf('%d cron jobs were cleaned', $count)); -+ } - } - - /** -+ * Get cron expression of cron job. -+ * - * @param array $jobConfig - * @return null|string - */ -@@ -586,33 +689,114 @@ class ProcessCronQueueObserver implements ObserverInterface - } - - /** -- * Clean up scheduled jobs that do not match their cron expression anymore -- * This can happen when you change the cron expression and flush the cache -+ * Clean up scheduled jobs that do not match their cron expression anymore. -+ * -+ * This can happen when you change the cron expression and flush the cache. - * - * @return $this - */ - private function cleanupScheduleMismatches() - { -+ /** @var \Magento\Cron\Model\ResourceModel\Schedule $scheduleResource */ -+ $scheduleResource = $this->_scheduleFactory->create()->getResource(); - foreach ($this->invalid as $jobCode => $scheduledAtList) { -- /** @var \Magento\Cron\Model\ResourceModel\Schedule $scheduleResource */ -- $scheduleResource = $this->_scheduleFactory->create()->getResource(); -- $scheduleResource->getConnection()->delete($scheduleResource->getMainTable(), [ -- 'status=?' => Schedule::STATUS_PENDING, -- 'job_code=?' => $jobCode, -- 'scheduled_at in (?)' => $scheduledAtList, -- ]); -+ $scheduleResource->getConnection()->delete( -+ $scheduleResource->getMainTable(), -+ [ -+ 'status = ?' => Schedule::STATUS_PENDING, -+ 'job_code = ?' => $jobCode, -+ 'scheduled_at in (?)' => $scheduledAtList, -+ ] -+ ); - } - return $this; - } - - /** -- * @return array -+ * Get CronGroup Configuration Value. -+ * -+ * @param string $groupId -+ * @param string $path -+ * @return int -+ */ -+ private function getCronGroupConfigurationValue($groupId, $path) -+ { -+ return $this->_scopeConfig->getValue( -+ 'system/cron/' . $groupId . '/' . $path, -+ \Magento\Store\Model\ScopeInterface::SCOPE_STORE -+ ); -+ } -+ -+ /** -+ * Is Group In Filter. -+ * -+ * @param string $groupId -+ * @return bool -+ */ -+ private function isGroupInFilter($groupId): bool -+ { -+ return !($this->_request->getParam('group') !== null -+ && trim($this->_request->getParam('group'), "'") !== $groupId); -+ } -+ -+ /** -+ * Process pending jobs. -+ * -+ * @param string $groupId -+ * @param array $jobsRoot -+ * @param int $currentTime -+ */ -+ private function processPendingJobs($groupId, $jobsRoot, $currentTime) -+ { -+ $procesedJobs = []; -+ $pendingJobs = $this->getPendingSchedules($groupId); -+ /** @var \Magento\Cron\Model\Schedule $schedule */ -+ foreach ($pendingJobs as $schedule) { -+ if (isset($procesedJobs[$schedule->getJobCode()])) { -+ // process only on job per run -+ continue; -+ } -+ $jobConfig = isset($jobsRoot[$schedule->getJobCode()]) ? $jobsRoot[$schedule->getJobCode()] : null; -+ if (!$jobConfig) { -+ continue; -+ } -+ -+ $scheduledTime = strtotime($schedule->getScheduledAt()); -+ if ($scheduledTime > $currentTime) { -+ continue; -+ } -+ -+ try { -+ if ($schedule->tryLockJob()) { -+ $this->_runJob($scheduledTime, $currentTime, $jobConfig, $schedule, $groupId); -+ } -+ } catch (\Exception $e) { -+ $this->processError($schedule, $e); -+ } -+ if ($schedule->getStatus() === Schedule::STATUS_SUCCESS) { -+ $procesedJobs[$schedule->getJobCode()] = true; -+ } -+ $schedule->save(); -+ } -+ } -+ -+ /** -+ * Process error messages. -+ * -+ * @param Schedule $schedule -+ * @param \Exception $exception -+ * @return void - */ -- private function getJobs() -+ private function processError(\Magento\Cron\Model\Schedule $schedule, \Exception $exception) - { -- if ($this->jobs === null) { -- $this->jobs = $this->_config->getJobs(); -+ $schedule->setMessages($exception->getMessage()); -+ if ($schedule->getStatus() === Schedule::STATUS_ERROR) { -+ $this->logger->critical($exception); -+ } -+ if ($schedule->getStatus() === Schedule::STATUS_MISSED -+ && $this->state->getMode() === State::MODE_DEVELOPER -+ ) { -+ $this->logger->info($schedule->getMessages()); - } -- return $this->jobs; - } - } -diff --git a/app/code/Magento/Cron/Test/Unit/Model/ScheduleTest.php b/app/code/Magento/Cron/Test/Unit/Model/ScheduleTest.php -index dd1fa0e79dc..da5539859a4 100644 ---- a/app/code/Magento/Cron/Test/Unit/Model/ScheduleTest.php -+++ b/app/code/Magento/Cron/Test/Unit/Model/ScheduleTest.php -@@ -6,6 +6,9 @@ - namespace Magento\Cron\Test\Unit\Model; - - use Magento\Cron\Model\Schedule; -+use Magento\Framework\Intl\DateTimeFactory; -+use Magento\Framework\Stdlib\DateTime\TimezoneInterface; -+use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; - - /** - * Class \Magento\Cron\Test\Unit\Model\ObserverTest -@@ -18,11 +21,27 @@ class ScheduleTest extends \PHPUnit\Framework\TestCase - */ - protected $helper; - -+ /** -+ * @var \Magento\Cron\Model\ResourceModel\Schedule -+ */ - protected $resourceJobMock; - -+ /** -+ * @var TimezoneInterface|\PHPUnit_Framework_MockObject_MockObject -+ */ -+ private $timezoneConverter; -+ -+ /** -+ * @var DateTimeFactory|\PHPUnit_Framework_MockObject_MockObject -+ */ -+ private $dateTimeFactory; -+ -+ /** -+ * @inheritdoc -+ */ - protected function setUp() - { -- $this->helper = new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this); -+ $this->helper = new ObjectManager($this); - - $this->resourceJobMock = $this->getMockBuilder(\Magento\Cron\Model\ResourceModel\Schedule::class) - ->disableOriginalConstructor() -@@ -32,18 +51,30 @@ class ScheduleTest extends \PHPUnit\Framework\TestCase - $this->resourceJobMock->expects($this->any()) - ->method('getIdFieldName') - ->will($this->returnValue('id')); -+ -+ $this->timezoneConverter = $this->getMockBuilder(TimezoneInterface::class) -+ ->setMethods(['date']) -+ ->getMockForAbstractClass(); -+ -+ $this->dateTimeFactory = $this->getMockBuilder(DateTimeFactory::class) -+ ->setMethods(['create']) -+ ->getMock(); - } - - /** -+ * Test for SetCronExpr -+ * - * @param string $cronExpression - * @param array $expected -+ * -+ * @return void - * @dataProvider setCronExprDataProvider - */ -- public function testSetCronExpr($cronExpression, $expected) -+ public function testSetCronExpr($cronExpression, $expected): void - { - // 1. Create mocks -- /** @var \Magento\Cron\Model\Schedule $model */ -- $model = $this->helper->getObject(\Magento\Cron\Model\Schedule::class); -+ /** @var Schedule $model */ -+ $model = $this->helper->getObject(Schedule::class); - - // 2. Run tested method - $model->setCronExpr($cronExpression); -@@ -61,7 +92,7 @@ class ScheduleTest extends \PHPUnit\Framework\TestCase - * - * @return array - */ -- public function setCronExprDataProvider() -+ public function setCronExprDataProvider(): array - { - return [ - ['1 2 3 4 5', [1, 2, 3, 4, 5]], -@@ -121,27 +152,33 @@ class ScheduleTest extends \PHPUnit\Framework\TestCase - } - - /** -+ * Test for SetCronExprException -+ * - * @param string $cronExpression -+ * -+ * @return void - * @expectedException \Magento\Framework\Exception\CronException - * @dataProvider setCronExprExceptionDataProvider - */ -- public function testSetCronExprException($cronExpression) -+ public function testSetCronExprException($cronExpression): void - { - // 1. Create mocks -- /** @var \Magento\Cron\Model\Schedule $model */ -- $model = $this->helper->getObject(\Magento\Cron\Model\Schedule::class); -+ /** @var Schedule $model */ -+ $model = $this->helper->getObject(Schedule::class); - - // 2. Run tested method - $model->setCronExpr($cronExpression); - } - - /** -+ * Data provider -+ * - * Here is a list of allowed characters and values for Cron expression - * http://docs.oracle.com/cd/E12058_01/doc/doc.1014/e12030/cron_expressions.htm - * - * @return array - */ -- public function setCronExprExceptionDataProvider() -+ public function setCronExprExceptionDataProvider(): array - { - return [ - [''], -@@ -153,17 +190,31 @@ class ScheduleTest extends \PHPUnit\Framework\TestCase - } - - /** -+ * Test for trySchedule -+ * - * @param int $scheduledAt - * @param array $cronExprArr - * @param $expected -+ * -+ * @return void - * @dataProvider tryScheduleDataProvider - */ -- public function testTrySchedule($scheduledAt, $cronExprArr, $expected) -+ public function testTrySchedule($scheduledAt, $cronExprArr, $expected): void - { - // 1. Create mocks -+ $this->timezoneConverter->method('getConfigTimezone') -+ ->willReturn('UTC'); -+ -+ $this->dateTimeFactory->method('create') -+ ->willReturn(new \DateTime()); -+ - /** @var \Magento\Cron\Model\Schedule $model */ - $model = $this->helper->getObject( -- \Magento\Cron\Model\Schedule::class -+ \Magento\Cron\Model\Schedule::class, -+ [ -+ 'timezoneConverter' => $this->timezoneConverter, -+ 'dateTimeFactory' => $this->dateTimeFactory, -+ ] - ); - - // 2. Set fixtures -@@ -177,22 +228,29 @@ class ScheduleTest extends \PHPUnit\Framework\TestCase - $this->assertEquals($expected, $result); - } - -- public function testTryScheduleWithConversionToAdminStoreTime() -+ /** -+ * Test for tryScheduleWithConversionToAdminStoreTime -+ * -+ * @return void -+ */ -+ public function testTryScheduleWithConversionToAdminStoreTime(): void - { - $scheduledAt = '2011-12-13 14:15:16'; - $cronExprArr = ['*', '*', '*', '*', '*']; - -- // 1. Create mocks -- $timezoneConverter = $this->createMock(\Magento\Framework\Stdlib\DateTime\TimezoneInterface::class); -- $timezoneConverter->expects($this->once()) -- ->method('date') -- ->with($scheduledAt) -- ->willReturn(new \DateTime($scheduledAt)); -+ $this->timezoneConverter->method('getConfigTimezone') -+ ->willReturn('UTC'); -+ -+ $this->dateTimeFactory->method('create') -+ ->willReturn(new \DateTime()); - - /** @var \Magento\Cron\Model\Schedule $model */ - $model = $this->helper->getObject( - \Magento\Cron\Model\Schedule::class, -- ['timezoneConverter' => $timezoneConverter] -+ [ -+ 'timezoneConverter' => $this->timezoneConverter, -+ 'dateTimeFactory' => $this->dateTimeFactory, -+ ] - ); - - // 2. Set fixtures -@@ -207,11 +265,15 @@ class ScheduleTest extends \PHPUnit\Framework\TestCase - } - - /** -+ * Data provider -+ * - * @return array - */ -- public function tryScheduleDataProvider() -+ public function tryScheduleDataProvider(): array - { - $date = '2011-12-13 14:15:16'; -+ $timestamp = (new \DateTime($date))->getTimestamp(); -+ $day = 'Monday'; - return [ - [$date, [], false], - [$date, null, false], -@@ -219,22 +281,26 @@ class ScheduleTest extends \PHPUnit\Framework\TestCase - [$date, [], false], - [$date, null, false], - [$date, false, false], -- [strtotime($date), ['*', '*', '*', '*', '*'], true], -- [strtotime($date), ['15', '*', '*', '*', '*'], true], -- [strtotime($date), ['*', '14', '*', '*', '*'], true], -- [strtotime($date), ['*', '*', '13', '*', '*'], true], -- [strtotime($date), ['*', '*', '*', '12', '*'], true], -- [strtotime('Monday'), ['*', '*', '*', '*', '1'], true], -+ [$timestamp, ['*', '*', '*', '*', '*'], true], -+ [$timestamp, ['15', '*', '*', '*', '*'], true], -+ [$timestamp, ['*', '14', '*', '*', '*'], true], -+ [$timestamp, ['*', '*', '13', '*', '*'], true], -+ [$timestamp, ['*', '*', '*', '12', '*'], true], -+ [(new \DateTime($day))->getTimestamp(), ['*', '*', '*', '*', '1'], true], - ]; - } - - /** -+ * Test for matchCronExpression -+ * - * @param string $cronExpressionPart - * @param int $dateTimePart - * @param bool $expectedResult -+ * -+ * @return void - * @dataProvider matchCronExpressionDataProvider - */ -- public function testMatchCronExpression($cronExpressionPart, $dateTimePart, $expectedResult) -+ public function testMatchCronExpression($cronExpressionPart, $dateTimePart, $expectedResult): void - { - // 1. Create mocks - /** @var \Magento\Cron\Model\Schedule $model */ -@@ -248,9 +314,11 @@ class ScheduleTest extends \PHPUnit\Framework\TestCase - } - - /** -+ * Data provider -+ * - * @return array - */ -- public function matchCronExpressionDataProvider() -+ public function matchCronExpressionDataProvider(): array - { - return [ - ['*', 0, true], -@@ -287,11 +355,15 @@ class ScheduleTest extends \PHPUnit\Framework\TestCase - } - - /** -+ * Test for matchCronExpressionException -+ * - * @param string $cronExpressionPart -+ * -+ * @return void - * @expectedException \Magento\Framework\Exception\CronException - * @dataProvider matchCronExpressionExceptionDataProvider - */ -- public function testMatchCronExpressionException($cronExpressionPart) -+ public function testMatchCronExpressionException($cronExpressionPart): void - { - $dateTimePart = 10; - -@@ -304,24 +376,30 @@ class ScheduleTest extends \PHPUnit\Framework\TestCase - } - - /** -+ * Data provider -+ * - * @return array - */ -- public function matchCronExpressionExceptionDataProvider() -+ public function matchCronExpressionExceptionDataProvider(): array - { - return [ - ['1/2/3'], //Invalid cron expression, expecting 'match/modulus': 1/2/3 - ['1/'], //Invalid cron expression, expecting numeric modulus: 1/ -- ['-'], //The "-" cron expression is invalid. Verify and try again. -+ ['-'], //Invalid cron expression - ['1-2-3'], //Invalid cron expression, expecting 'from-to' structure: 1-2-3 - ]; - } - - /** -+ * Test for GetNumeric -+ * - * @param mixed $param - * @param int $expectedResult -+ * -+ * @return void - * @dataProvider getNumericDataProvider - */ -- public function testGetNumeric($param, $expectedResult) -+ public function testGetNumeric($param, $expectedResult): void - { - // 1. Create mocks - /** @var \Magento\Cron\Model\Schedule $model */ -@@ -335,9 +413,11 @@ class ScheduleTest extends \PHPUnit\Framework\TestCase - } - - /** -+ * Data provider -+ * - * @return array - */ -- public function getNumericDataProvider() -+ public function getNumericDataProvider(): array - { - return [ - [null, false], -@@ -362,7 +442,12 @@ class ScheduleTest extends \PHPUnit\Framework\TestCase - ]; - } - -- public function testTryLockJobSuccess() -+ /** -+ * Test for tryLockJobSuccess -+ * -+ * @return void -+ */ -+ public function testTryLockJobSuccess(): void - { - $scheduleId = 1; - -@@ -386,7 +471,12 @@ class ScheduleTest extends \PHPUnit\Framework\TestCase - $this->assertEquals(Schedule::STATUS_RUNNING, $model->getStatus()); - } - -- public function testTryLockJobFailure() -+ /** -+ * Test for tryLockJobFailure -+ * -+ * @return void -+ */ -+ public function testTryLockJobFailure(): void - { - $scheduleId = 1; - -diff --git a/app/code/Magento/Cron/Test/Unit/Model/System/Config/Initial/ConverterTest.php b/app/code/Magento/Cron/Test/Unit/Model/System/Config/Initial/ConverterTest.php -new file mode 100644 -index 00000000000..703926b4c01 ---- /dev/null -+++ b/app/code/Magento/Cron/Test/Unit/Model/System/Config/Initial/ConverterTest.php -@@ -0,0 +1,89 @@ -+<?php -+/** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+declare(strict_types=1); -+ -+namespace Magento\Cron\Test\Unit\Model\System\Config\Initial; -+ -+use Magento\Cron\Model\Groups\Config\Data as GroupsConfigModel; -+use Magento\Cron\Model\System\Config\Initial\Converter as ConverterPlugin; -+use Magento\Framework\App\Config\Initial\Converter; -+ -+/** -+ * Class ConverterTest -+ * -+ * Unit test for \Magento\Cron\Model\System\Config\Initial\Converter -+ */ -+class ConverterTest extends \PHPUnit\Framework\TestCase -+{ -+ /** -+ * @var GroupsConfigModel|\PHPUnit_Framework_MockObject_MockObject -+ */ -+ private $groupsConfigMock; -+ -+ /** -+ * @var Converter|\PHPUnit_Framework_MockObject_MockObject -+ */ -+ private $converterMock; -+ -+ /** -+ * @var ConverterPlugin -+ */ -+ private $converterPlugin; -+ -+ /** -+ * @inheritdoc -+ */ -+ protected function setUp() -+ { -+ $this->groupsConfigMock = $this->getMockBuilder( -+ GroupsConfigModel::class -+ )->disableOriginalConstructor()->getMock(); -+ $this->converterMock = $this->getMockBuilder(Converter::class)->getMock(); -+ $this->converterPlugin = new ConverterPlugin($this->groupsConfigMock); -+ } -+ -+ /** -+ * Tests afterConvert method with no $result['data']['default']['system'] set -+ */ -+ public function testAfterConvertWithNoData() -+ { -+ $expectedResult = ['test']; -+ $this->groupsConfigMock->expects($this->never()) -+ ->method('get'); -+ -+ $result = $this->converterPlugin->afterConvert($this->converterMock, $expectedResult); -+ -+ self::assertSame($expectedResult, $result); -+ } -+ -+ /** -+ * Tests afterConvert method with $result['data']['default']['system'] set -+ */ -+ public function testAfterConvertWithData() -+ { -+ $groups = [ -+ 'group1' => ['val1' => ['value' => '1']], -+ 'group2' => ['val2' => ['value' => '2']] -+ ]; -+ $expectedResult['data']['default']['system']['cron'] = [ -+ 'group1' => [ -+ 'val1' => '1' -+ ], -+ 'group2' => [ -+ 'val2' => '2' -+ ] -+ ]; -+ $result['data']['default']['system']['cron'] = '1'; -+ -+ $this->groupsConfigMock->expects($this->once()) -+ ->method('get') -+ ->willReturn($groups); -+ -+ $result = $this->converterPlugin->afterConvert($this->converterMock, $result); -+ -+ self::assertEquals($expectedResult, $result); -+ } -+} -diff --git a/app/code/Magento/Cron/Test/Unit/Observer/ProcessCronQueueObserverTest.php b/app/code/Magento/Cron/Test/Unit/Observer/ProcessCronQueueObserverTest.php -index d8cb79af521..462dde98f99 100644 ---- a/app/code/Magento/Cron/Test/Unit/Observer/ProcessCronQueueObserverTest.php -+++ b/app/code/Magento/Cron/Test/Unit/Observer/ProcessCronQueueObserverTest.php -@@ -8,6 +8,7 @@ namespace Magento\Cron\Test\Unit\Observer; - use Magento\Cron\Model\Schedule; - use Magento\Cron\Observer\ProcessCronQueueObserver as ProcessCronQueueObserver; - use Magento\Framework\App\State; -+use Magento\Framework\Profiler\Driver\Standard\StatFactory; - - /** - * Class \Magento\Cron\Test\Unit\Model\ObserverTest -@@ -84,6 +85,11 @@ class ProcessCronQueueObserverTest extends \PHPUnit\Framework\TestCase - */ - protected $appStateMock; - -+ /** -+ * @var \Magento\Framework\Lock\LockManagerInterface|\PHPUnit_Framework_MockObject_MockObject -+ */ -+ private $lockManagerMock; -+ - /** - * @var \Magento\Cron\Model\ResourceModel\Schedule|\PHPUnit_Framework_MockObject_MockObject - */ -@@ -116,6 +122,7 @@ class ProcessCronQueueObserverTest extends \PHPUnit\Framework\TestCase - )->disableOriginalConstructor()->getMock(); - $this->_collection->expects($this->any())->method('addFieldToFilter')->will($this->returnSelf()); - $this->_collection->expects($this->any())->method('load')->will($this->returnSelf()); -+ - $this->_scheduleFactory = $this->getMockBuilder( - \Magento\Cron\Model\ScheduleFactory::class - )->setMethods( -@@ -135,6 +142,12 @@ class ProcessCronQueueObserverTest extends \PHPUnit\Framework\TestCase - ->disableOriginalConstructor() - ->getMock(); - -+ $this->lockManagerMock = $this->getMockBuilder(\Magento\Framework\Lock\LockManagerInterface::class) -+ ->disableOriginalConstructor() -+ ->getMock(); -+ $this->lockManagerMock->method('lock')->willReturn(true); -+ $this->lockManagerMock->method('unlock')->willReturn(true); -+ - $this->observer = $this->createMock(\Magento\Framework\Event\Observer::class); - - $this->dateTimeMock = $this->getMockBuilder(\Magento\Framework\Stdlib\DateTime\DateTime::class) -@@ -159,6 +172,16 @@ class ProcessCronQueueObserverTest extends \PHPUnit\Framework\TestCase - $this->scheduleResource->method('getConnection')->willReturn($connection); - $connection->method('delete')->willReturn(1); - -+ $this->statFactory = $this->getMockBuilder(StatFactory::class) -+ ->setMethods(['create']) -+ ->disableOriginalConstructor() -+ ->getMock(); -+ -+ $this->stat = $this->getMockBuilder(\Magento\Framework\Profiler\Driver\Standard\Stat::class) -+ ->disableOriginalConstructor() -+ ->getMock(); -+ $this->statFactory->expects($this->any())->method('create')->willReturn($this->stat); -+ - $this->_observer = new ProcessCronQueueObserver( - $this->_objectManager, - $this->_scheduleFactory, -@@ -170,41 +193,23 @@ class ProcessCronQueueObserverTest extends \PHPUnit\Framework\TestCase - $this->dateTimeMock, - $phpExecutableFinderFactory, - $this->loggerMock, -- $this->appStateMock -+ $this->appStateMock, -+ $this->statFactory, -+ $this->lockManagerMock - ); - } - -- /** -- * Test case without saved cron jobs in data base -- */ -- public function testDispatchNoPendingJobs() -- { -- $lastRun = $this->time + 10000000; -- $this->_cache->expects($this->any())->method('load')->will($this->returnValue($lastRun)); -- $this->_scopeConfig->expects($this->any())->method('getValue')->will($this->returnValue(0)); -- -- $this->_config->expects($this->once())->method('getJobs')->will($this->returnValue([])); -- -- $scheduleMock = $this->getMockBuilder( -- \Magento\Cron\Model\Schedule::class -- )->disableOriginalConstructor()->getMock(); -- $scheduleMock->expects($this->any())->method('getCollection')->will($this->returnValue($this->_collection)); -- $this->_scheduleFactory->expects($this->once())->method('create')->will($this->returnValue($scheduleMock)); -- -- $this->_observer->execute($this->observer); -- } -- - /** - * Test case for not existed cron jobs in files but in data base is presented - */ - public function testDispatchNoJobConfig() - { - $lastRun = $this->time + 10000000; -- $this->_cache->expects($this->any())->method('load')->will($this->returnValue($lastRun)); -- $this->_scopeConfig->expects($this->any())->method('getValue')->will($this->returnValue(0)); -+ $this->_cache->expects($this->atLeastOnce())->method('load')->will($this->returnValue($lastRun)); -+ $this->_scopeConfig->expects($this->atLeastOnce())->method('getValue')->will($this->returnValue(0)); - - $this->_config->expects( -- $this->any() -+ $this->atLeastOnce() - )->method( - 'getJobs' - )->will( -@@ -212,16 +217,21 @@ class ProcessCronQueueObserverTest extends \PHPUnit\Framework\TestCase - ); - - $schedule = $this->createPartialMock(\Magento\Cron\Model\Schedule::class, ['getJobCode', '__wakeup']); -- $schedule->expects($this->once())->method('getJobCode')->will($this->returnValue('not_existed_job_code')); -+ $schedule->expects($this->atLeastOnce()) -+ ->method('getJobCode') -+ ->will($this->returnValue('not_existed_job_code')); - - $this->_collection->addItem($schedule); - - $scheduleMock = $this->getMockBuilder( - \Magento\Cron\Model\Schedule::class - )->disableOriginalConstructor()->getMock(); -- $scheduleMock->expects($this->any())->method('getCollection')->will($this->returnValue($this->_collection)); -- $scheduleMock->expects($this->any())->method('getResource')->will($this->returnValue($this->scheduleResource)); -- $this->_scheduleFactory->expects($this->any())->method('create')->will($this->returnValue($scheduleMock)); -+ $scheduleMock->expects($this->atLeastOnce()) -+ ->method('getCollection') -+ ->will($this->returnValue($this->_collection)); -+ $this->_scheduleFactory->expects($this->atLeastOnce()) -+ ->method('create') -+ ->will($this->returnValue($scheduleMock)); - - $this->_observer->execute($this->observer); - } -@@ -240,11 +250,13 @@ class ProcessCronQueueObserverTest extends \PHPUnit\Framework\TestCase - $schedule = $this->getMockBuilder( - \Magento\Cron\Model\Schedule::class - )->setMethods( -- ['getJobCode', 'tryLockJob', 'getScheduledAt', '__wakeup', 'save'] -+ ['getJobCode', 'tryLockJob', 'getScheduledAt', '__wakeup', 'save', 'setFinishedAt'] - )->disableOriginalConstructor()->getMock(); - $schedule->expects($this->any())->method('getJobCode')->will($this->returnValue('test_job1')); -- $schedule->expects($this->once())->method('getScheduledAt')->will($this->returnValue($dateScheduledAt)); -+ $schedule->expects($this->atLeastOnce())->method('getScheduledAt')->will($this->returnValue($dateScheduledAt)); - $schedule->expects($this->once())->method('tryLockJob')->will($this->returnValue(false)); -+ $schedule->expects($this->never())->method('setFinishedAt'); -+ - $abstractModel = $this->createMock(\Magento\Framework\Model\AbstractModel::class); - $schedule->expects($this->any())->method('save')->will($this->returnValue($abstractModel)); - $this->_collection->addItem($schedule); -@@ -262,7 +274,9 @@ class ProcessCronQueueObserverTest extends \PHPUnit\Framework\TestCase - )->disableOriginalConstructor()->getMock(); - $scheduleMock->expects($this->any())->method('getCollection')->will($this->returnValue($this->_collection)); - $scheduleMock->expects($this->any())->method('getResource')->will($this->returnValue($this->scheduleResource)); -- $this->_scheduleFactory->expects($this->exactly(2))->method('create')->will($this->returnValue($scheduleMock)); -+ $this->_scheduleFactory->expects($this->atLeastOnce()) -+ ->method('create') -+ ->will($this->returnValue($scheduleMock)); - - $this->_observer->execute($this->observer); - } -@@ -272,10 +286,8 @@ class ProcessCronQueueObserverTest extends \PHPUnit\Framework\TestCase - */ - public function testDispatchExceptionTooLate() - { -- $exceptionMessage = 'Too late for the schedule'; -- $scheduleId = 42; -+ $exceptionMessage = 'Cron Job test_job1 is missed at 2017-07-30 15:00:00'; - $jobCode = 'test_job1'; -- $exception = $exceptionMessage . ' Schedule Id: ' . $scheduleId . ' Job Code: ' . $jobCode; - - $lastRun = $this->time + 10000000; - $this->_cache->expects($this->any())->method('load')->willReturn($lastRun); -@@ -299,25 +311,25 @@ class ProcessCronQueueObserverTest extends \PHPUnit\Framework\TestCase - 'getScheduleId', - ] - )->disableOriginalConstructor()->getMock(); -- $schedule->expects($this->any())->method('getJobCode')->willReturn($jobCode); -- $schedule->expects($this->once())->method('getScheduledAt')->willReturn($dateScheduledAt); -+ $schedule->expects($this->atLeastOnce())->method('getJobCode')->willReturn($jobCode); -+ $schedule->expects($this->atLeastOnce())->method('getScheduledAt')->willReturn($dateScheduledAt); - $schedule->expects($this->once())->method('tryLockJob')->willReturn(true); - $schedule->expects( -- $this->once() -+ $this->any() - )->method( - 'setStatus' - )->with( - $this->equalTo(\Magento\Cron\Model\Schedule::STATUS_MISSED) - )->willReturnSelf(); - $schedule->expects($this->once())->method('setMessages')->with($this->equalTo($exceptionMessage)); -- $schedule->expects($this->any())->method('getStatus')->willReturn(Schedule::STATUS_MISSED); -- $schedule->expects($this->once())->method('getMessages')->willReturn($exceptionMessage); -- $schedule->expects($this->once())->method('getScheduleId')->willReturn($scheduleId); -+ $schedule->expects($this->atLeastOnce())->method('getStatus')->willReturn(Schedule::STATUS_MISSED); -+ $schedule->expects($this->atLeastOnce())->method('getMessages')->willReturn($exceptionMessage); - $schedule->expects($this->once())->method('save'); - - $this->appStateMock->expects($this->once())->method('getMode')->willReturn(State::MODE_DEVELOPER); - -- $this->loggerMock->expects($this->once())->method('info')->with($exception); -+ $this->loggerMock->expects($this->once())->method('info') -+ ->with('Cron Job test_job1 is missed at 2017-07-30 15:00:00'); - - $this->_collection->addItem($schedule); - -@@ -333,7 +345,7 @@ class ProcessCronQueueObserverTest extends \PHPUnit\Framework\TestCase - ->disableOriginalConstructor()->getMock(); - $scheduleMock->expects($this->any())->method('getCollection')->willReturn($this->_collection); - $scheduleMock->expects($this->any())->method('getResource')->will($this->returnValue($this->scheduleResource)); -- $this->_scheduleFactory->expects($this->exactly(2))->method('create')->willReturn($scheduleMock); -+ $this->_scheduleFactory->expects($this->atLeastOnce())->method('create')->willReturn($scheduleMock); - - $this->_observer->execute($this->observer); - } -@@ -343,7 +355,8 @@ class ProcessCronQueueObserverTest extends \PHPUnit\Framework\TestCase - */ - public function testDispatchExceptionNoCallback() - { -- $exceptionMessage = 'No callbacks found'; -+ $jobName = 'test_job1'; -+ $exceptionMessage = 'No callbacks found for cron job ' . $jobName; - $exception = new \Exception(__($exceptionMessage)); - - $dateScheduledAt = date('Y-m-d H:i:s', $this->time - 86400); -@@ -372,7 +385,7 @@ class ProcessCronQueueObserverTest extends \PHPUnit\Framework\TestCase - - $this->loggerMock->expects($this->once())->method('critical')->with($exception); - -- $jobConfig = ['test_group' => ['test_job1' => ['instance' => 'Some_Class']]]; -+ $jobConfig = ['test_group' => [$jobName => ['instance' => 'Some_Class']]]; - - $this->_config->expects($this->exactly(2))->method('getJobs')->will($this->returnValue($jobConfig)); - -@@ -388,7 +401,7 @@ class ProcessCronQueueObserverTest extends \PHPUnit\Framework\TestCase - )->disableOriginalConstructor()->getMock(); - $scheduleMock->expects($this->any())->method('getCollection')->will($this->returnValue($this->_collection)); - $scheduleMock->expects($this->any())->method('getResource')->will($this->returnValue($this->scheduleResource)); -- $this->_scheduleFactory->expects($this->exactly(2))->method('create')->will($this->returnValue($scheduleMock)); -+ $this->_scheduleFactory->expects($this->once())->method('create')->will($this->returnValue($scheduleMock)); - - $this->_observer->execute($this->observer); - } -@@ -453,7 +466,7 @@ class ProcessCronQueueObserverTest extends \PHPUnit\Framework\TestCase - )->disableOriginalConstructor()->getMock(); - $scheduleMock->expects($this->any())->method('getCollection')->will($this->returnValue($this->_collection)); - $scheduleMock->expects($this->any())->method('getResource')->will($this->returnValue($this->scheduleResource)); -- $this->_scheduleFactory->expects($this->exactly(2))->method('create')->will($this->returnValue($scheduleMock)); -+ $this->_scheduleFactory->expects($this->once())->method('create')->will($this->returnValue($scheduleMock)); - $this->_objectManager - ->expects($this->once()) - ->method('create') -@@ -469,7 +482,6 @@ class ProcessCronQueueObserverTest extends \PHPUnit\Framework\TestCase - public function dispatchExceptionInCallbackDataProvider() - { - $throwable = new \TypeError(); -- - return [ - 'non-callable callback' => [ - 'Not_Existed_Class', -@@ -496,7 +508,7 @@ class ProcessCronQueueObserverTest extends \PHPUnit\Framework\TestCase - 'Error when running a cron job', - 0, - $throwable -- ), -+ ) - ], - ]; - } -@@ -530,23 +542,22 @@ class ProcessCronQueueObserverTest extends \PHPUnit\Framework\TestCase - $scheduleMethods - )->disableOriginalConstructor()->getMock(); - $schedule->expects($this->any())->method('getJobCode')->will($this->returnValue('test_job1')); -- $schedule->expects($this->once())->method('getScheduledAt')->will($this->returnValue($dateScheduledAt)); -- $schedule->expects($this->once())->method('tryLockJob')->will($this->returnValue(true)); -+ $schedule->expects($this->atLeastOnce())->method('getScheduledAt')->will($this->returnValue($dateScheduledAt)); -+ $schedule->expects($this->atLeastOnce())->method('tryLockJob')->will($this->returnValue(true)); -+ $schedule->expects($this->any())->method('setFinishedAt')->willReturnSelf(); - - // cron start to execute some job - $schedule->expects($this->any())->method('setExecutedAt')->will($this->returnSelf()); -- $schedule->expects($this->at(5))->method('save'); -+ $schedule->expects($this->atLeastOnce())->method('save'); - - // cron end execute some job - $schedule->expects( -- $this->at(6) -+ $this->atLeastOnce() - )->method( - 'setStatus' - )->with( - $this->equalTo(\Magento\Cron\Model\Schedule::STATUS_SUCCESS) -- )->will( -- $this->returnSelf() -- ); -+ )->willReturnSelf(); - - $schedule->expects($this->at(8))->method('save'); - -@@ -565,7 +576,7 @@ class ProcessCronQueueObserverTest extends \PHPUnit\Framework\TestCase - )->disableOriginalConstructor()->getMock(); - $scheduleMock->expects($this->any())->method('getCollection')->will($this->returnValue($this->_collection)); - $scheduleMock->expects($this->any())->method('getResource')->will($this->returnValue($this->scheduleResource)); -- $this->_scheduleFactory->expects($this->exactly(2))->method('create')->will($this->returnValue($scheduleMock)); -+ $this->_scheduleFactory->expects($this->once(2))->method('create')->will($this->returnValue($scheduleMock)); - - $testCronJob = $this->getMockBuilder('CronJob')->setMethods(['execute'])->getMock(); - $testCronJob->expects($this->atLeastOnce())->method('execute')->with($schedule); -@@ -600,6 +611,8 @@ class ProcessCronQueueObserverTest extends \PHPUnit\Framework\TestCase - )->will( - $this->returnValue(['test_group' => []]) - ); -+ $this->_config->expects($this->at(2))->method('getJobs')->will($this->returnValue($jobConfig)); -+ $this->_config->expects($this->at(3))->method('getJobs')->will($this->returnValue($jobConfig)); - $this->_request->expects($this->any())->method('getParam')->will($this->returnValue('test_group')); - $this->_cache->expects( - $this->at(0) -@@ -669,6 +682,8 @@ class ProcessCronQueueObserverTest extends \PHPUnit\Framework\TestCase - ]; - $this->_config->expects($this->at(0))->method('getJobs')->willReturn($jobConfig); - $this->_config->expects($this->at(1))->method('getJobs')->willReturn($jobs); -+ $this->_config->expects($this->at(2))->method('getJobs')->willReturn($jobs); -+ $this->_config->expects($this->at(3))->method('getJobs')->willReturn($jobs); - $this->_request->expects($this->any())->method('getParam')->willReturn('default'); - $this->_cache->expects( - $this->at(0) -@@ -745,7 +760,7 @@ class ProcessCronQueueObserverTest extends \PHPUnit\Framework\TestCase - $this->_request->expects($this->any())->method('getParam')->will($this->returnValue('test_group')); - $this->_collection->addItem($schedule); - -- $this->_config->expects($this->exactly(2))->method('getJobs')->will($this->returnValue($jobConfig)); -+ $this->_config->expects($this->atLeastOnce())->method('getJobs')->will($this->returnValue($jobConfig)); - - $this->_cache->expects($this->at(0))->method('load')->will($this->returnValue($this->time + 10000000)); - $this->_cache->expects($this->at(1))->method('load')->will($this->returnValue($this->time - 10000000)); -@@ -772,7 +787,7 @@ class ProcessCronQueueObserverTest extends \PHPUnit\Framework\TestCase - )->setMethods(['getCollection', 'getResource'])->disableOriginalConstructor()->getMock(); - $scheduleMock->expects($this->any())->method('getCollection')->will($this->returnValue($collection)); - $scheduleMock->expects($this->any())->method('getResource')->will($this->returnValue($this->scheduleResource)); -- $this->_scheduleFactory->expects($this->at(1))->method('create')->will($this->returnValue($scheduleMock)); -+ $this->_scheduleFactory->expects($this->any())->method('create')->will($this->returnValue($scheduleMock)); - - $this->_observer->execute($this->observer); - } -@@ -796,55 +811,17 @@ class ProcessCronQueueObserverTest extends \PHPUnit\Framework\TestCase - $this->_cache->expects($this->at(2))->method('load')->will($this->returnValue($this->time + 10000000)); - $this->_scheduleFactory->expects($this->at(2))->method('create')->will($this->returnValue($scheduleMock)); - -- // This item was scheduled 2 days and 2 hours ago -- $dateScheduledAt = date('Y-m-d H:i:s', $this->time - 180000); -- /** @var \Magento\Cron\Model\Schedule|\PHPUnit_Framework_MockObject_MockObject $schedule1 */ -- $schedule1 = $this->getMockBuilder( -- \Magento\Cron\Model\Schedule::class -- )->disableOriginalConstructor()->setMethods( -- ['getExecutedAt', 'getScheduledAt', 'getStatus', 'delete', '__wakeup'] -- )->getMock(); -- $schedule1->expects($this->any())->method('getExecutedAt')->will($this->returnValue(null)); -- $schedule1->expects($this->any())->method('getScheduledAt')->will($this->returnValue($dateScheduledAt)); -- $schedule1->expects($this->any())->method('getStatus')->will($this->returnValue(Schedule::STATUS_MISSED)); -- //we expect this job be deleted from the list -- $schedule1->expects($this->once())->method('delete')->will($this->returnValue(true)); -- $this->_collection->addItem($schedule1); -- -- // This item was scheduled 1 day ago -- $dateScheduledAt = date('Y-m-d H:i:s', $this->time - 86400); -- $schedule2 = $this->getMockBuilder( -- \Magento\Cron\Model\Schedule::class -- )->disableOriginalConstructor()->setMethods( -- ['getExecutedAt', 'getScheduledAt', 'getStatus', 'delete', '__wakeup'] -- )->getMock(); -- $schedule2->expects($this->any())->method('getExecutedAt')->will($this->returnValue(null)); -- $schedule2->expects($this->any())->method('getScheduledAt')->will($this->returnValue($dateScheduledAt)); -- $schedule2->expects($this->any())->method('getStatus')->will($this->returnValue(Schedule::STATUS_MISSED)); -- //we don't expect this job be deleted from the list -- $schedule2->expects($this->never())->method('delete'); -- $this->_collection->addItem($schedule2); -- -- $this->_config->expects($this->exactly(2))->method('getJobs')->will($this->returnValue($jobConfig)); -- -- $this->_scopeConfig->expects($this->at(0))->method('getValue') -- ->with($this->equalTo('system/cron/test_group/history_cleanup_every')) -- ->will($this->returnValue(10)); -- $this->_scopeConfig->expects($this->at(1))->method('getValue') -- ->with($this->equalTo('system/cron/test_group/schedule_lifetime')) -- ->will($this->returnValue(2*24*60)); -- $this->_scopeConfig->expects($this->at(2))->method('getValue') -- ->with($this->equalTo('system/cron/test_group/history_success_lifetime')) -- ->will($this->returnValue(0)); -- $this->_scopeConfig->expects($this->at(3))->method('getValue') -- ->with($this->equalTo('system/cron/test_group/history_failure_lifetime')) -- ->will($this->returnValue(0)); -- $this->_scopeConfig->expects($this->at(4))->method('getValue') -- ->with($this->equalTo('system/cron/test_group/schedule_generate_every')) -- ->will($this->returnValue(0)); -- $this->_scopeConfig->expects($this->at(5))->method('getValue') -- ->with($this->equalTo('system/cron/test_group/use_separate_process')) -- ->will($this->returnValue(0)); -+ $this->_config->expects($this->atLeastOnce())->method('getJobs')->will($this->returnValue($jobConfig)); -+ -+ $this->_scopeConfig->expects($this->any())->method('getValue') -+ ->willReturnMap([ -+ ['system/cron/test_group/use_separate_process', 0], -+ ['system/cron/test_group/history_cleanup_every', 10], -+ ['system/cron/test_group/schedule_lifetime', 2*24*60], -+ ['system/cron/test_group/history_success_lifetime', 0], -+ ['system/cron/test_group/history_failure_lifetime', 0], -+ ['system/cron/test_group/schedule_generate_every', 0], -+ ]); - - $this->_collection->expects($this->any())->method('addFieldToFilter')->will($this->returnSelf()); - $this->_collection->expects($this->any())->method('load')->will($this->returnSelf()); -diff --git a/app/code/Magento/Cron/etc/db_schema.xml b/app/code/Magento/Cron/etc/db_schema.xml -index deff05d3eec..b3061eefa63 100644 ---- a/app/code/Magento/Cron/etc/db_schema.xml -+++ b/app/code/Magento/Cron/etc/db_schema.xml -@@ -18,13 +18,13 @@ - <column xsi:type="timestamp" name="scheduled_at" on_update="false" nullable="true" comment="Scheduled At"/> - <column xsi:type="timestamp" name="executed_at" on_update="false" nullable="true" comment="Executed At"/> - <column xsi:type="timestamp" name="finished_at" on_update="false" nullable="true" comment="Finished At"/> -- <constraint xsi:type="primary" name="PRIMARY"> -+ <constraint xsi:type="primary" referenceId="PRIMARY"> - <column name="schedule_id"/> - </constraint> -- <index name="CRON_SCHEDULE_JOB_CODE" indexType="btree"> -+ <index referenceId="CRON_SCHEDULE_JOB_CODE" indexType="btree"> - <column name="job_code"/> - </index> -- <index name="CRON_SCHEDULE_SCHEDULED_AT_STATUS" indexType="btree"> -+ <index referenceId="CRON_SCHEDULE_SCHEDULED_AT_STATUS" indexType="btree"> - <column name="scheduled_at"/> - <column name="status"/> - </index> -diff --git a/app/code/Magento/Cron/etc/di.xml b/app/code/Magento/Cron/etc/di.xml -index a37f3760b70..3e3bdc20535 100644 ---- a/app/code/Magento/Cron/etc/di.xml -+++ b/app/code/Magento/Cron/etc/di.xml -@@ -16,6 +16,18 @@ - <type name="Magento\Framework\App\Config\Initial\Converter"> - <plugin name="cron_system_config_initial_converter_plugin" type="Magento\Cron\Model\System\Config\Initial\Converter" /> - </type> -+ <virtualType name="Magento\Cron\Model\VirtualLoggerHandler" type="Magento\Framework\Logger\Handler\Base"> -+ <arguments> -+ <argument name="fileName" xsi:type="string">/var/log/cron.log</argument> -+ </arguments> -+ </virtualType> -+ <virtualType name="Magento\Cron\Model\VirtualLogger" type="Magento\Framework\Logger\Monolog"> -+ <arguments> -+ <argument name="handlers" xsi:type="array"> -+ <item name="system" xsi:type="object">Magento\Cron\Model\VirtualLoggerHandler</item> -+ </argument> -+ </arguments> -+ </virtualType> - <!-- @api --> - <virtualType name="shellBackground" type="Magento\Framework\Shell"> - <arguments> -@@ -25,6 +37,7 @@ - <type name="Magento\Cron\Observer\ProcessCronQueueObserver"> - <arguments> - <argument name="shell" xsi:type="object">shellBackground</argument> -+ <argument name="logger" xsi:type="object">Magento\Cron\Model\VirtualLogger</argument> - </arguments> - </type> - <type name="Magento\Framework\Console\CommandListInterface"> -diff --git a/app/code/Magento/CurrencySymbol/Controller/Adminhtml/System/Currency/FetchRates.php b/app/code/Magento/CurrencySymbol/Controller/Adminhtml/System/Currency/FetchRates.php -index 38e20355b66..34d24a8b0a7 100644 ---- a/app/code/Magento/CurrencySymbol/Controller/Adminhtml/System/Currency/FetchRates.php -+++ b/app/code/Magento/CurrencySymbol/Controller/Adminhtml/System/Currency/FetchRates.php -@@ -7,10 +7,13 @@ - - namespace Magento\CurrencySymbol\Controller\Adminhtml\System\Currency; - -+use Magento\Framework\App\Action\HttpGetActionInterface; -+use Magento\Framework\App\Action\HttpPostActionInterface as HttpPostActionInterface; - use Magento\Framework\Exception\LocalizedException; - use Magento\Framework\Controller\ResultFactory; -+use Magento\CurrencySymbol\Controller\Adminhtml\System\Currency as CurrencyAction; - --class FetchRates extends \Magento\CurrencySymbol\Controller\Adminhtml\System\Currency -+class FetchRates extends CurrencyAction implements HttpGetActionInterface, HttpPostActionInterface - { - /** - * Fetch rates action -diff --git a/app/code/Magento/CurrencySymbol/Controller/Adminhtml/System/Currency/Index.php b/app/code/Magento/CurrencySymbol/Controller/Adminhtml/System/Currency/Index.php -index 9b07777a14d..a9b1b78cbc6 100644 ---- a/app/code/Magento/CurrencySymbol/Controller/Adminhtml/System/Currency/Index.php -+++ b/app/code/Magento/CurrencySymbol/Controller/Adminhtml/System/Currency/Index.php -@@ -6,7 +6,9 @@ - */ - namespace Magento\CurrencySymbol\Controller\Adminhtml\System\Currency; - --class Index extends \Magento\CurrencySymbol\Controller\Adminhtml\System\Currency -+use Magento\Framework\App\Action\HttpGetActionInterface as HttpGetActionInterface; -+ -+class Index extends \Magento\CurrencySymbol\Controller\Adminhtml\System\Currency implements HttpGetActionInterface - { - /** - * Currency management main page -diff --git a/app/code/Magento/CurrencySymbol/Controller/Adminhtml/System/Currency/SaveRates.php b/app/code/Magento/CurrencySymbol/Controller/Adminhtml/System/Currency/SaveRates.php -index ae13c4d399e..8dd6b5e6fac 100644 ---- a/app/code/Magento/CurrencySymbol/Controller/Adminhtml/System/Currency/SaveRates.php -+++ b/app/code/Magento/CurrencySymbol/Controller/Adminhtml/System/Currency/SaveRates.php -@@ -7,7 +7,9 @@ - - namespace Magento\CurrencySymbol\Controller\Adminhtml\System\Currency; - --class SaveRates extends \Magento\CurrencySymbol\Controller\Adminhtml\System\Currency -+use Magento\Framework\App\Action\HttpPostActionInterface as HttpPostActionInterface; -+ -+class SaveRates extends \Magento\CurrencySymbol\Controller\Adminhtml\System\Currency implements HttpPostActionInterface - { - /** - * Save rates action -diff --git a/app/code/Magento/CurrencySymbol/Controller/Adminhtml/System/Currencysymbol/Index.php b/app/code/Magento/CurrencySymbol/Controller/Adminhtml/System/Currencysymbol/Index.php -index 808372bd3a6..1762a907a75 100644 ---- a/app/code/Magento/CurrencySymbol/Controller/Adminhtml/System/Currencysymbol/Index.php -+++ b/app/code/Magento/CurrencySymbol/Controller/Adminhtml/System/Currencysymbol/Index.php -@@ -6,7 +6,9 @@ - */ - namespace Magento\CurrencySymbol\Controller\Adminhtml\System\Currencysymbol; - --class Index extends \Magento\CurrencySymbol\Controller\Adminhtml\System\Currencysymbol -+use Magento\Framework\App\Action\HttpGetActionInterface as HttpGetActionInterface; -+ -+class Index extends \Magento\CurrencySymbol\Controller\Adminhtml\System\Currencysymbol implements HttpGetActionInterface - { - /** - * Show Currency Symbols Management dialog -diff --git a/app/code/Magento/CurrencySymbol/Controller/Adminhtml/System/Currencysymbol/Save.php b/app/code/Magento/CurrencySymbol/Controller/Adminhtml/System/Currencysymbol/Save.php -index eee7961b02f..703117f34fc 100644 ---- a/app/code/Magento/CurrencySymbol/Controller/Adminhtml/System/Currencysymbol/Save.php -+++ b/app/code/Magento/CurrencySymbol/Controller/Adminhtml/System/Currencysymbol/Save.php -@@ -6,7 +6,9 @@ - */ - namespace Magento\CurrencySymbol\Controller\Adminhtml\System\Currencysymbol; - --class Save extends \Magento\CurrencySymbol\Controller\Adminhtml\System\Currencysymbol -+use Magento\Framework\App\Action\HttpPostActionInterface as HttpPostActionInterface; -+ -+class Save extends \Magento\CurrencySymbol\Controller\Adminhtml\System\Currencysymbol implements HttpPostActionInterface - { - /** - * Save custom Currency symbol -diff --git a/app/code/Magento/CurrencySymbol/Model/System/Currencysymbol.php b/app/code/Magento/CurrencySymbol/Model/System/Currencysymbol.php -index fcde688a1e1..6c7019986cc 100644 ---- a/app/code/Magento/CurrencySymbol/Model/System/Currencysymbol.php -+++ b/app/code/Magento/CurrencySymbol/Model/System/Currencysymbol.php -@@ -187,14 +187,16 @@ class Currencysymbol - /** - * Save currency symbol to config - * -- * @param $symbols array -+ * @param array $symbols - * @return $this - */ - public function setCurrencySymbolsData($symbols = []) - { -- foreach ($this->getCurrencySymbolsData() as $code => $values) { -- if (isset($symbols[$code]) && ($symbols[$code] == $values['parentSymbol'] || empty($symbols[$code]))) { -- unset($symbols[$code]); -+ if (!$this->_storeManager->isSingleStoreMode()) { -+ foreach ($this->getCurrencySymbolsData() as $code => $values) { -+ if (isset($symbols[$code]) && ($symbols[$code] == $values['parentSymbol'] || empty($symbols[$code]))) { -+ unset($symbols[$code]); -+ } - } - } - $value = []; -diff --git a/app/code/Magento/CurrencySymbol/Test/Mftf/Data/AdminMenuData.xml b/app/code/Magento/CurrencySymbol/Test/Mftf/Data/AdminMenuData.xml -new file mode 100644 -index 00000000000..9166c8745c9 ---- /dev/null -+++ b/app/code/Magento/CurrencySymbol/Test/Mftf/Data/AdminMenuData.xml -@@ -0,0 +1,21 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<entities xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataProfileSchema.xsd"> -+ <entity name="AdminMenuStoresCurrencyCurrencyRates"> -+ <data key="pageTitle">Currency Rates</data> -+ <data key="title">Currency Rates</data> -+ <data key="dataUiId">magento-currencysymbol-system-currency-rates</data> -+ </entity> -+ <entity name="AdminMenuStoresCurrencyCurrencySymbols"> -+ <data key="pageTitle">Currency Symbols</data> -+ <data key="title">Currency Symbols</data> -+ <data key="dataUiId">magento-currencysymbol-system-currency-symbols</data> -+ </entity> -+</entities> -diff --git a/app/code/Magento/CurrencySymbol/Test/Mftf/Page/ConfigCurrencySetupPage.xml b/app/code/Magento/CurrencySymbol/Test/Mftf/Page/ConfigCurrencySetupPage.xml -new file mode 100644 -index 00000000000..f523cb58d3b ---- /dev/null -+++ b/app/code/Magento/CurrencySymbol/Test/Mftf/Page/ConfigCurrencySetupPage.xml -@@ -0,0 +1,13 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<pages xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/PageObject.xsd"> -+ <page name="ConfigCurrencySetupPage" url="admin/system_config/edit/section/currency" area="admin" module="Magento_Config"> -+ </page> -+</pages> -diff --git a/app/code/Magento/CurrencySymbol/Test/Mftf/Section/CurrencySetupSection.xml b/app/code/Magento/CurrencySymbol/Test/Mftf/Section/CurrencySetupSection.xml -new file mode 100644 -index 00000000000..20fcd1e8936 ---- /dev/null -+++ b/app/code/Magento/CurrencySymbol/Test/Mftf/Section/CurrencySetupSection.xml -@@ -0,0 +1,15 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> -+ <section name="CurrencySetupSection"> -+ <element name="allowCurrencies" type="select" selector="#currency_options_allow"/> -+ <element name="currencyOptions" type="select" selector="#currency_options-head"/> -+ </section> -+</sections> -\ No newline at end of file -diff --git a/app/code/Magento/CurrencySymbol/Test/Mftf/Test/AdminStoresCurrencyRatesNavigateMenuTest.xml b/app/code/Magento/CurrencySymbol/Test/Mftf/Test/AdminStoresCurrencyRatesNavigateMenuTest.xml -new file mode 100644 -index 00000000000..4a33d40d2a3 ---- /dev/null -+++ b/app/code/Magento/CurrencySymbol/Test/Mftf/Test/AdminStoresCurrencyRatesNavigateMenuTest.xml -@@ -0,0 +1,36 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> -+ <test name="AdminStoresCurrencyRatesNavigateMenuTest"> -+ <annotations> -+ <features value="CurrencySymbol"/> -+ <stories value="Menu Navigation"/> -+ <title value="Admin stores currency rates navigate menu test"/> -+ <description value="Admin should be able to navigate to Stores > Currency Rates"/> -+ <severity value="CRITICAL"/> -+ <testCaseId value="MC-14150"/> -+ <group value="menu"/> -+ <group value="mtf_migrated"/> -+ </annotations> -+ <before> -+ <actionGroup ref="LoginAsAdmin" stepKey="LoginAsAdmin"/> -+ </before> -+ <after> -+ <actionGroup ref="logout" stepKey="logout"/> -+ </after> -+ <actionGroup ref="AdminNavigateMenuActionGroup" stepKey="navigateToStoresCurrencyRatesPage"> -+ <argument name="menuUiId" value="{{AdminMenuStores.dataUiId}}"/> -+ <argument name="submenuUiId" value="{{AdminMenuStoresCurrencyCurrencyRates.dataUiId}}"/> -+ </actionGroup> -+ <actionGroup ref="AdminAssertPageTitleActionGroup" stepKey="seePageTitle"> -+ <argument name="title" value="{{AdminMenuStoresCurrencyCurrencyRates.pageTitle}}"/> -+ </actionGroup> -+ </test> -+</tests> -diff --git a/app/code/Magento/CurrencySymbol/Test/Mftf/Test/AdminStoresCurrencySymbolsNavigateMenuTest.xml b/app/code/Magento/CurrencySymbol/Test/Mftf/Test/AdminStoresCurrencySymbolsNavigateMenuTest.xml -new file mode 100644 -index 00000000000..978917772f2 ---- /dev/null -+++ b/app/code/Magento/CurrencySymbol/Test/Mftf/Test/AdminStoresCurrencySymbolsNavigateMenuTest.xml -@@ -0,0 +1,36 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> -+ <test name="AdminStoresCurrencySymbolsNavigateMenuTest"> -+ <annotations> -+ <features value="CurrencySymbol"/> -+ <stories value="Menu Navigation"/> -+ <title value="Admin stores currency symbols navigate menu test"/> -+ <description value="Admin should be able to navigate to Stores > Currency Symbols"/> -+ <severity value="CRITICAL"/> -+ <testCaseId value="MC-14151"/> -+ <group value="menu"/> -+ <group value="mtf_migrated"/> -+ </annotations> -+ <before> -+ <actionGroup ref="LoginAsAdmin" stepKey="LoginAsAdmin"/> -+ </before> -+ <after> -+ <actionGroup ref="logout" stepKey="logout"/> -+ </after> -+ <actionGroup ref="AdminNavigateMenuActionGroup" stepKey="navigateToStoresCurrencySymbolsPage"> -+ <argument name="menuUiId" value="{{AdminMenuStores.dataUiId}}"/> -+ <argument name="submenuUiId" value="{{AdminMenuStoresCurrencyCurrencySymbols.dataUiId}}"/> -+ </actionGroup> -+ <actionGroup ref="AdminAssertPageTitleActionGroup" stepKey="seePageTitle"> -+ <argument name="title" value="{{AdminMenuStoresCurrencyCurrencySymbols.pageTitle}}"/> -+ </actionGroup> -+ </test> -+</tests> -diff --git a/app/code/Magento/CurrencySymbol/view/adminhtml/templates/grid.phtml b/app/code/Magento/CurrencySymbol/view/adminhtml/templates/grid.phtml -index 0ba3c7ed2d7..9248337e5c4 100644 ---- a/app/code/Magento/CurrencySymbol/view/adminhtml/templates/grid.phtml -+++ b/app/code/Magento/CurrencySymbol/view/adminhtml/templates/grid.phtml -@@ -3,9 +3,6 @@ - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -- --// @codingStandardsIgnoreFile -- - ?> - <?php - /** -@@ -13,29 +10,33 @@ - */ - ?> - --<form id="currency-symbols-form" action="<?= /* @escapeNotVerified */ $block->getFormActionUrl() ?>" method="post"> -- <input name="form_key" type="hidden" value="<?= /* @escapeNotVerified */ $block->getFormKey() ?>" /> -+<form id="currency-symbols-form" action="<?= $block->escapeUrl($block->getFormActionUrl()) ?>" method="post"> -+ <input name="form_key" type="hidden" value="<?= $block->escapeHtmlAttr($block->getFormKey()) ?>" /> - <fieldset class="admin__fieldset"> -- <?php foreach ($block->getCurrencySymbolsData() as $code => $data): ?> -+ <?php foreach ($block->getCurrencySymbolsData() as $code => $data) : ?> - <div class="admin__field _required"> -- <label class="admin__field-label" for="custom_currency_symbol<?= /* @escapeNotVerified */ $code ?>"> -- <span><?= /* @escapeNotVerified */ $code ?> (<?= /* @escapeNotVerified */ $data['displayName'] ?>)</span> -+ <label class="admin__field-label" for="custom_currency_symbol<?= $block->escapeHtmlAttr($code) ?>"> -+ <span><?= $block->escapeHtml($code) ?> (<?= $block->escapeHtml($data['displayName']) ?>)</span> - </label> - <div class="admin__field-control"> -- <input id="custom_currency_symbol<?= /* @escapeNotVerified */ $code ?>" -- class="required-entry admin__control-text" -+ <input id="custom_currency_symbol<?= $block->escapeHtmlAttr($code) ?>" -+ class="required-entry admin__control-text <?= $data['inherited'] ? 'disabled' : '' ?>" - type="text" - value="<?= $block->escapeHtmlAttr($data['displaySymbol']) ?>" -- <?= $data['inherited'] ? ' disabled="disabled"' : '' ?> -- name="custom_currency_symbol[<?= /* @escapeNotVerified */ $code ?>]"> -+ name="custom_currency_symbol[<?= $block->escapeHtmlAttr($code) ?>]"> - <div class="admin__field admin__field-option"> -- <input id="custom_currency_symbol_inherit<?= /* @escapeNotVerified */ $code ?>" -+ <input id="custom_currency_symbol_inherit<?= $block->escapeHtmlAttr($code) ?>" - class="admin__control-checkbox" type="checkbox" -- onclick="toggleUseDefault(<?= /* @escapeNotVerified */ '\'' . $code . '\',\'' . $block->escapeJs($data['parentSymbol']) . '\'' ?>)" -+ onclick="toggleUseDefault(<?= '\'' . $block->escapeJs($code) . '\',\'' . $block->escapeJs($data['parentSymbol']) . '\'' ?>)" - <?= $data['inherited'] ? ' checked="checked"' : '' ?> - value="1" -- name="inherit_custom_currency_symbol[<?= /* @escapeNotVerified */ $code ?>]"> -- <label class="admin__field-label" for="custom_currency_symbol_inherit<?= /* @escapeNotVerified */ $code ?>"><span><?= /* @escapeNotVerified */ $block->getInheritText() ?></span></label> -+ name="inherit_custom_currency_symbol[<?= $block->escapeHtmlAttr($code) ?>]"> -+ <label class="admin__field-label" -+ for="custom_currency_symbol_inherit<?= $block->escapeHtmlAttr($code) ?>"> -+ <span> -+ <?= $block->escapeHtml($block->getInheritText()) ?> -+ </span> -+ </label> - </div> - </div> - </div> -@@ -49,16 +50,18 @@ require(['jquery', "mage/mage", 'prototype'], function(jQuery){ - - function toggleUseDefault(code, value) - { -- checkbox = $('custom_currency_symbol_inherit'+code); -- input = $('custom_currency_symbol'+code); -- if (checkbox.checked) { -- input.value = value; -- input.disabled = true; -+ checkbox = jQuery('#custom_currency_symbol_inherit'+code); -+ input = jQuery('#custom_currency_symbol'+code); -+ -+ if (checkbox.is(':checked')) { -+ input.addClass('disabled'); -+ input.val(value); -+ input.prop('readonly', true); - } else { -- input.disabled = false; -+ input.removeClass('disabled'); -+ input.prop('readonly', false); - } - } -- - window.toggleUseDefault = toggleUseDefault; - }); - </script> -diff --git a/app/code/Magento/CurrencySymbol/view/adminhtml/templates/system/currency/rate/matrix.phtml b/app/code/Magento/CurrencySymbol/view/adminhtml/templates/system/currency/rate/matrix.phtml -index 8e0abcb3197..18b3c7eef74 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 -@@ -4,63 +4,59 @@ - * See COPYING.txt for license details. - */ - --// @codingStandardsIgnoreFile -- --?> --<?php - /** - * @var $block \Magento\CurrencySymbol\Block\Adminhtml\System\Currency\Rate\Matrix - */ --?> --<?php -+ - $_oldRates = $block->getOldRates(); - $_newRates = $block->getNewRates(); - $_rates = ($_newRates) ? $_newRates : $_oldRates; - ?> --<?php if (empty($_rates)): ?> -- <div class="message message-warning warning"><p><?= /* @escapeNotVerified */ __('You must first configure currency options before being able to see currency rates.') ?></p></div> --<?php else: ?> -- <form name="rateForm" id="rate-form" method="post" action="<?= /* @escapeNotVerified */ $block->getRatesFormAction() ?>"> -+<?php if (empty($_rates)) : ?> -+ <div class="message message-warning warning"><p><?= $block->escapeHtml(__('You must first configure currency options before being able to see currency rates.')) ?></p></div> -+<?php else : ?> -+ <form name="rateForm" id="rate-form" method="post" action="<?= $block->escapeUrl($block->getRatesFormAction()) ?>"> - <?= $block->getBlockHtml('formkey') ?> - <div class="admin__control-table-wrapper"> - <table class="admin__control-table"> - <thead> - <tr> - <th> </th> -- <?php $_i = 0; foreach ($block->getAllowedCurrencies() as $_currencyCode): ?> -- <th><span><?= /* @escapeNotVerified */ $_currencyCode ?></span></th> -+ <?php $_i = 0; foreach ($block->getAllowedCurrencies() as $_currencyCode) : ?> -+ <th><span><?= $block->escapeHtml($_currencyCode) ?></span></th> - <?php endforeach; ?> - </tr> - </thead> -- <?php $_j = 0; foreach ($block->getDefaultCurrencies() as $_currencyCode): ?> -+ <?php $_j = 0; foreach ($block->getDefaultCurrencies() as $_currencyCode) : ?> - <tr> -- <?php if (isset($_rates[$_currencyCode]) && is_array($_rates[$_currencyCode])): ?> -- <?php foreach ($_rates[$_currencyCode] as $_rate => $_value): ?> -- <?php if (++$_j == 1): ?> -- <td><span class="admin__control-support-text"><?= /* @escapeNotVerified */ $_currencyCode ?></span></td> -+ <?php if (isset($_rates[$_currencyCode]) && is_array($_rates[$_currencyCode])) : ?> -+ <?php foreach ($_rates[$_currencyCode] as $_rate => $_value) : ?> -+ <?php if (++$_j == 1) : ?> -+ <td><span class="admin__control-support-text"><?= $block->escapeHtml($_currencyCode) ?></span></td> - <td> - <input type="text" -- name="rate[<?= /* @escapeNotVerified */ $_currencyCode ?>][<?= /* @escapeNotVerified */ $_rate ?>]" -- value="<?= ($_currencyCode == $_rate) ? '1.0000' : ($_value>0 ? $_value : (isset($_oldRates[$_currencyCode][$_rate]) ? $_oldRates[$_currencyCode][$_rate] : '')) ?>" -+ name="rate[<?= $block->escapeHtmlAttr($_currencyCode) ?>][<?= $block->escapeHtmlAttr($_rate) ?>]" -+ value="<?= ($_currencyCode == $_rate) ? '1.0000' : ($_value>0 ? $block->escapeHtmlAttr($_value) : (isset($_oldRates[$_currencyCode][$_rate]) ? $block->escapeHtmlAttr($_oldRates[$_currencyCode][$_rate]) : '')) ?>" - class="admin__control-text" - <?= ($_currencyCode == $_rate) ? ' disabled' : '' ?> /> -- <?php if (isset($_newRates) && $_currencyCode != $_rate && isset($_oldRates[$_currencyCode][$_rate])): ?> -- <div class="admin__field-note"><?= /* @escapeNotVerified */ __('Old rate:') ?> <b><?= /* @escapeNotVerified */ $_oldRates[$_currencyCode][$_rate] ?></b></div> -+ <?php if (isset($_newRates) && $_currencyCode != $_rate && isset($_oldRates[$_currencyCode][$_rate])) : ?> -+ <div class="admin__field-note"><?= $block->escapeHtml(__('Old rate:')) ?> <strong><?= $block->escapeHtml($_oldRates[$_currencyCode][$_rate]) ?></strong></div> - <?php endif; ?> - </td> -- <?php else: ?> -+ <?php else : ?> - <td> - <input type="text" -- name="rate[<?= /* @escapeNotVerified */ $_currencyCode ?>][<?= /* @escapeNotVerified */ $_rate ?>]" -- value="<?= ($_currencyCode == $_rate) ? '1.0000' : ($_value>0 ? $_value : (isset($_oldRates[$_currencyCode][$_rate]) ? $_oldRates[$_currencyCode][$_rate] : '')) ?>" -+ name="rate[<?= $block->escapeHtmlAttr($_currencyCode) ?>][<?= $block->escapeHtmlAttr($_rate) ?>]" -+ value="<?= ($_currencyCode == $_rate) ? '1.0000' : ($_value>0 ? $block->escapeHtmlAttr($_value) : (isset($_oldRates[$_currencyCode][$_rate]) ? $block->escapeHtmlAttr($_oldRates[$_currencyCode][$_rate]) : '')) ?>" - class="admin__control-text" - <?= ($_currencyCode == $_rate) ? ' disabled' : '' ?> /> -- <?php if (isset($_newRates) && $_currencyCode != $_rate && isset($_oldRates[$_currencyCode][$_rate])): ?> -- <div class="admin__field-note"><?= /* @escapeNotVerified */ __('Old rate:') ?> <b><?= /* @escapeNotVerified */ $_oldRates[$_currencyCode][$_rate] ?></b></div> -+ <?php if (isset($_newRates) && $_currencyCode != $_rate && isset($_oldRates[$_currencyCode][$_rate])) : ?> -+ <div class="admin__field-note"><?= $block->escapeHtml(__('Old rate:')) ?> <strong><?= $block->escapeHtml($_oldRates[$_currencyCode][$_rate]) ?></strong></div> - <?php endif; ?> - </td> - <?php endif; ?> -- <?php endforeach; $_j = 0; ?> -+ <?php endforeach; ?> -+ <?php $_j = 0; ?> - <?php endif; ?> - </tr> - <?php endforeach; ?> -diff --git a/app/code/Magento/CurrencySymbol/view/adminhtml/templates/system/currency/rate/services.phtml b/app/code/Magento/CurrencySymbol/view/adminhtml/templates/system/currency/rate/services.phtml -index 985831c5de7..471a4a05daa 100644 ---- a/app/code/Magento/CurrencySymbol/view/adminhtml/templates/system/currency/rate/services.phtml -+++ b/app/code/Magento/CurrencySymbol/view/adminhtml/templates/system/currency/rate/services.phtml -@@ -3,9 +3,6 @@ - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -- --// @codingStandardsIgnoreFile -- - ?> - <?php - /** -@@ -13,7 +10,7 @@ - */ - ?> - <div class="admin__field"> -- <label class="admin__field-label"><span><?= /* @escapeNotVerified */ __('Import Service') ?></span></label> -+ <label class="admin__field-label"><span><?= $block->escapeHtml(__('Import Service')) ?></span></label> - <div class="admin__field-control"> - <?= $block->getChildHtml('import_services') ?> - </div> -diff --git a/app/code/Magento/CurrencySymbol/view/adminhtml/templates/system/currency/rates.phtml b/app/code/Magento/CurrencySymbol/view/adminhtml/templates/system/currency/rates.phtml -index 42b2affa2c1..f5390bee3e4 100644 ---- a/app/code/Magento/CurrencySymbol/view/adminhtml/templates/system/currency/rates.phtml -+++ b/app/code/Magento/CurrencySymbol/view/adminhtml/templates/system/currency/rates.phtml -@@ -3,9 +3,6 @@ - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -- --// @codingStandardsIgnoreFile -- - ?> - <?php - /** -@@ -13,7 +10,7 @@ - */ - ?> - --<form action="<?= /* @escapeNotVerified */ $block->getImportFormAction() ?>" method="post" class="import-service"> -+<form action="<?= $block->escapeUrl($block->getImportFormAction()) ?>" method="post" class="import-service"> - <?= $block->getBlockHtml('formkey') ?> - <fieldset class="admin__fieldset admin__fieldset-import-service"> - <?= $block->getServicesHtml() ?> -diff --git a/app/code/Magento/Customer/Api/AccountManagementInterface.php b/app/code/Magento/Customer/Api/AccountManagementInterface.php -index d2f9fb7ebc4..10fc2349968 100644 ---- a/app/code/Magento/Customer/Api/AccountManagementInterface.php -+++ b/app/code/Magento/Customer/Api/AccountManagementInterface.php -@@ -7,6 +7,8 @@ - - namespace Magento\Customer\Api; - -+use Magento\Framework\Exception\InputException; -+ - /** - * Interface for managing customers accounts. - * @api -@@ -144,19 +146,24 @@ interface AccountManagementInterface - /** - * Reset customer password. - * -- * @param string $email -+ * @param string $email If empty value given then the customer -+ * will be matched by the RP token. - * @param string $resetToken - * @param string $newPassword -+ * - * @return bool true on success - * @throws \Magento\Framework\Exception\LocalizedException -+ * @throws InputException - */ - public function resetPassword($email, $resetToken, $newPassword); - - /** - * Check if password reset token is valid. - * -- * @param int $customerId -+ * @param int $customerId If null is given then a customer -+ * will be matched by the RP token. - * @param string $resetPasswordLinkToken -+ * - * @return bool True if the token is valid - * @throws \Magento\Framework\Exception\State\InputMismatchException If token is mismatched - * @throws \Magento\Framework\Exception\State\ExpiredException If token is expired -diff --git a/app/code/Magento/Customer/Api/CustomerRepositoryInterface.php b/app/code/Magento/Customer/Api/CustomerRepositoryInterface.php -index 2133ae5a323..ca9bf4dc7af 100644 ---- a/app/code/Magento/Customer/Api/CustomerRepositoryInterface.php -+++ b/app/code/Magento/Customer/Api/CustomerRepositoryInterface.php -@@ -51,7 +51,7 @@ interface CustomerRepositoryInterface - * Retrieve customers which match a specified criteria. - * - * This call returns an array of objects, but detailed information about each object’s attributes might not be -- * included. See http://devdocs.magento.com/codelinks/attributes.html#CustomerRepositoryInterface to determine -+ * included. See https://devdocs.magento.com/codelinks/attributes.html#CustomerRepositoryInterface to determine - * which call to use to get detailed information about all attributes for an object. - * - * @param \Magento\Framework\Api\SearchCriteriaInterface $searchCriteria -diff --git a/app/code/Magento/Customer/Api/GroupRepositoryInterface.php b/app/code/Magento/Customer/Api/GroupRepositoryInterface.php -index 2f5e637a769..f6ba387e913 100644 ---- a/app/code/Magento/Customer/Api/GroupRepositoryInterface.php -+++ b/app/code/Magento/Customer/Api/GroupRepositoryInterface.php -@@ -42,7 +42,7 @@ interface GroupRepositoryInterface - * be filtered by tax class. - * - * This call returns an array of objects, but detailed information about each object’s attributes might not be -- * included. See http://devdocs.magento.com/codelinks/attributes.html#GroupRepositoryInterface to determine -+ * included. See https://devdocs.magento.com/codelinks/attributes.html#GroupRepositoryInterface to determine - * which call to use to get detailed information about all attributes for an object. - * - * @param \Magento\Framework\Api\SearchCriteriaInterface $searchCriteria -diff --git a/app/code/Magento/Customer/Block/Account/Dashboard.php b/app/code/Magento/Customer/Block/Account/Dashboard.php -index fa9c8f98a8c..8e0f79d4577 100644 ---- a/app/code/Magento/Customer/Block/Account/Dashboard.php -+++ b/app/code/Magento/Customer/Block/Account/Dashboard.php -@@ -114,10 +114,13 @@ class Dashboard extends \Magento\Framework\View\Element\Template - * Retrieve the Url for customer orders. - * - * @return string -+ * @deprecated Action does not exist - */ - public function getOrdersUrl() - { -- return $this->_urlBuilder->getUrl('customer/order/index', ['_secure' => true]); -+ //phpcs:ignore Magento2.Functions.DiscouragedFunction -+ trigger_error('Method is deprecated', E_USER_DEPRECATED); -+ return ''; - } - - /** -@@ -137,7 +140,7 @@ class Dashboard extends \Magento\Framework\View\Element\Template - */ - public function getWishlistUrl() - { -- return $this->_urlBuilder->getUrl('customer/wishlist/index', ['_secure' => true]); -+ return $this->_urlBuilder->getUrl('wishlist/index', ['_secure' => true]); - } - - /** -diff --git a/app/code/Magento/Customer/Block/Account/Dashboard/Info.php b/app/code/Magento/Customer/Block/Account/Dashboard/Info.php -index ded7238edc7..87132c3afb8 100644 ---- a/app/code/Magento/Customer/Block/Account/Dashboard/Info.php -+++ b/app/code/Magento/Customer/Block/Account/Dashboard/Info.php -@@ -102,7 +102,7 @@ class Info extends \Magento\Framework\View\Element\Template - $this->_subscription = $this->_createSubscriber(); - $customer = $this->getCustomer(); - if ($customer) { -- $this->_subscription->loadByEmail($customer->getEmail()); -+ $this->_subscription->loadByCustomerId($customer->getId()); - } - } - return $this->_subscription; -diff --git a/app/code/Magento/Customer/Block/Address/Book.php b/app/code/Magento/Customer/Block/Address/Book.php -index 8b38946a063..04669446ffe 100644 ---- a/app/code/Magento/Customer/Block/Address/Book.php -+++ b/app/code/Magento/Customer/Block/Address/Book.php -@@ -6,8 +6,8 @@ - namespace Magento\Customer\Block\Address; - - use Magento\Customer\Api\AddressRepositoryInterface; --use Magento\Customer\Api\CustomerRepositoryInterface; - use Magento\Customer\Model\Address\Mapper; -+use Magento\Customer\Block\Address\Grid as AddressesGrid; - - /** - * Customer address book block -@@ -24,7 +24,7 @@ class Book extends \Magento\Framework\View\Element\Template - protected $currentCustomer; - - /** -- * @var CustomerRepositoryInterface -+ * @var \Magento\Customer\Api\CustomerRepositoryInterface - */ - protected $customerRepository; - -@@ -43,33 +43,44 @@ class Book extends \Magento\Framework\View\Element\Template - */ - protected $addressMapper; - -+ /** -+ * @var AddressesGrid -+ */ -+ private $addressesGrid; -+ - /** - * @param \Magento\Framework\View\Element\Template\Context $context -- * @param CustomerRepositoryInterface $customerRepository -+ * @param CustomerRepositoryInterface|null $customerRepository - * @param AddressRepositoryInterface $addressRepository - * @param \Magento\Customer\Helper\Session\CurrentCustomer $currentCustomer - * @param \Magento\Customer\Model\Address\Config $addressConfig - * @param Mapper $addressMapper - * @param array $data -+ * @param AddressesGrid|null $addressesGrid -+ * @SuppressWarnings(PHPMD.UnusedFormalParameter) - */ - public function __construct( - \Magento\Framework\View\Element\Template\Context $context, -- CustomerRepositoryInterface $customerRepository, -+ \Magento\Customer\Api\CustomerRepositoryInterface $customerRepository = null, - AddressRepositoryInterface $addressRepository, - \Magento\Customer\Helper\Session\CurrentCustomer $currentCustomer, - \Magento\Customer\Model\Address\Config $addressConfig, - Mapper $addressMapper, -- array $data = [] -+ array $data = [], -+ Grid $addressesGrid = null - ) { -- $this->customerRepository = $customerRepository; - $this->currentCustomer = $currentCustomer; - $this->addressRepository = $addressRepository; - $this->_addressConfig = $addressConfig; - $this->addressMapper = $addressMapper; -+ $this->addressesGrid = $addressesGrid ?: \Magento\Framework\App\ObjectManager::getInstance() -+ ->get(AddressesGrid::class); - parent::__construct($context, $data); - } - - /** -+ * Prepare the Address Book section layout -+ * - * @return $this - */ - protected function _prepareLayout() -@@ -79,14 +90,20 @@ class Book extends \Magento\Framework\View\Element\Template - } - - /** -+ * Generate and return "New Address" URL -+ * - * @return string -+ * @deprecated not used in this block -+ * @see \Magento\Customer\Block\Address\Grid::getAddAddressUrl - */ - public function getAddAddressUrl() - { -- return $this->getUrl('customer/address/new', ['_secure' => true]); -+ return $this->addressesGrid->getAddAddressUrl(); - } - - /** -+ * Generate and return "Back" URL -+ * - * @return string - */ - public function getBackUrl() -@@ -98,24 +115,37 @@ class Book extends \Magento\Framework\View\Element\Template - } - - /** -+ * Generate and return "Delete" URL -+ * - * @return string -+ * @deprecated not used in this block -+ * @see \Magento\Customer\Block\Address\Grid::getDeleteUrl - */ - public function getDeleteUrl() - { -- return $this->getUrl('customer/address/delete'); -+ return $this->addressesGrid->getDeleteUrl(); - } - - /** -+ * Generate and return "Edit Address" URL. -+ * -+ * Address ID passed in parameters -+ * - * @param int $addressId - * @return string -+ * @deprecated not used in this block -+ * @see \Magento\Customer\Block\Address\Grid::getAddressEditUrl - */ - public function getAddressEditUrl($addressId) - { -- return $this->getUrl('customer/address/edit', ['_secure' => true, 'id' => $addressId]); -+ return $this->addressesGrid->getAddressEditUrl($addressId); - } - - /** -+ * Determines is the address primary (billing or shipping) -+ * - * @return bool -+ * @throws \Magento\Framework\Exception\LocalizedException - */ - public function hasPrimaryAddress() - { -@@ -123,22 +153,22 @@ class Book extends \Magento\Framework\View\Element\Template - } - - /** -+ * Get current additional customer addresses -+ * -+ * Will return array of address interfaces if customer have additional addresses and false in other case. -+ * - * @return \Magento\Customer\Api\Data\AddressInterface[]|bool -+ * @throws \Magento\Framework\Exception\LocalizedException -+ * @deprecated not used in this block -+ * @see \Magento\Customer\Block\Address\Grid::getAdditionalAddresses - */ - public function getAdditionalAddresses() - { - try { -- $addresses = $this->customerRepository->getById($this->currentCustomer->getCustomerId())->getAddresses(); -+ $addresses = $this->addressesGrid->getAdditionalAddresses(); - } catch (\Magento\Framework\Exception\NoSuchEntityException $e) { -- return false; -- } -- $primaryAddressIds = [$this->getDefaultBilling(), $this->getDefaultShipping()]; -- foreach ($addresses as $address) { -- if (!in_array($address->getId(), $primaryAddressIds)) { -- $additional[] = $address; -- } - } -- return empty($additional) ? false : $additional; -+ return empty($addresses) ? false : $addresses; - } - - /** -@@ -158,23 +188,23 @@ class Book extends \Magento\Framework\View\Element\Template - } - - /** -+ * Get current customer -+ * - * @return \Magento\Customer\Api\Data\CustomerInterface|null - */ - public function getCustomer() - { -- $customer = $this->getData('customer'); -- if ($customer === null) { -- try { -- $customer = $this->customerRepository->getById($this->currentCustomer->getCustomerId()); -- } catch (\Magento\Framework\Exception\NoSuchEntityException $e) { -- return null; -- } -- $this->setData('customer', $customer); -+ $customer = null; -+ try { -+ $customer = $this->currentCustomer->getCustomer(); -+ } catch (\Magento\Framework\Exception\NoSuchEntityException $e) { - } - return $customer; - } - - /** -+ * Get customer's default billing address -+ * - * @return int|null - */ - public function getDefaultBilling() -@@ -188,8 +218,11 @@ class Book extends \Magento\Framework\View\Element\Template - } - - /** -+ * Get customer address by ID -+ * - * @param int $addressId - * @return \Magento\Customer\Api\Data\AddressInterface|null -+ * @throws \Magento\Framework\Exception\LocalizedException - */ - public function getAddressById($addressId) - { -@@ -201,6 +234,8 @@ class Book extends \Magento\Framework\View\Element\Template - } - - /** -+ * Get customer's default shipping address -+ * - * @return int|null - */ - public function getDefaultShipping() -diff --git a/app/code/Magento/Customer/Block/Address/Edit.php b/app/code/Magento/Customer/Block/Address/Edit.php -index 6a42e9670cc..afefb1138de 100644 ---- a/app/code/Magento/Customer/Block/Address/Edit.php -+++ b/app/code/Magento/Customer/Block/Address/Edit.php -@@ -5,6 +5,7 @@ - */ - namespace Magento\Customer\Block\Address; - -+use Magento\Framework\Exception\LocalizedException; - use Magento\Framework\Exception\NoSuchEntityException; - - /** -@@ -46,6 +47,11 @@ class Edit extends \Magento\Directory\Block\Data - */ - protected $dataObjectHelper; - -+ /** -+ * @var \Magento\Customer\Api\AddressMetadataInterface -+ */ -+ private $addressMetadata; -+ - /** - * Constructor - * -@@ -61,6 +67,7 @@ class Edit extends \Magento\Directory\Block\Data - * @param \Magento\Customer\Helper\Session\CurrentCustomer $currentCustomer - * @param \Magento\Framework\Api\DataObjectHelper $dataObjectHelper - * @param array $data -+ * @param \Magento\Customer\Api\AddressMetadataInterface|null $addressMetadata - * - * @SuppressWarnings(PHPMD.ExcessiveParameterList) - */ -@@ -76,13 +83,15 @@ class Edit extends \Magento\Directory\Block\Data - \Magento\Customer\Api\Data\AddressInterfaceFactory $addressDataFactory, - \Magento\Customer\Helper\Session\CurrentCustomer $currentCustomer, - \Magento\Framework\Api\DataObjectHelper $dataObjectHelper, -- array $data = [] -+ array $data = [], -+ \Magento\Customer\Api\AddressMetadataInterface $addressMetadata = null - ) { - $this->_customerSession = $customerSession; - $this->_addressRepository = $addressRepository; - $this->addressDataFactory = $addressDataFactory; - $this->currentCustomer = $currentCustomer; - $this->dataObjectHelper = $dataObjectHelper; -+ $this->addressMetadata = $addressMetadata; - parent::__construct( - $context, - $directoryHelper, -@@ -103,6 +112,32 @@ class Edit extends \Magento\Directory\Block\Data - { - parent::_prepareLayout(); - -+ $this->initAddressObject(); -+ -+ $this->pageConfig->getTitle()->set($this->getTitle()); -+ -+ if ($postedData = $this->_customerSession->getAddressFormData(true)) { -+ $postedData['region'] = [ -+ 'region_id' => isset($postedData['region_id']) ? $postedData['region_id'] : null, -+ 'region' => $postedData['region'], -+ ]; -+ $this->dataObjectHelper->populateWithArray( -+ $this->_address, -+ $postedData, -+ \Magento\Customer\Api\Data\AddressInterface::class -+ ); -+ } -+ $this->precheckRequiredAttributes(); -+ return $this; -+ } -+ -+ /** -+ * Initialize address object. -+ * -+ * @return void -+ */ -+ private function initAddressObject() -+ { - // Init address object - if ($addressId = $this->getRequest()->getParam('id')) { - try { -@@ -124,22 +159,26 @@ class Edit extends \Magento\Directory\Block\Data - $this->_address->setLastname($customer->getLastname()); - $this->_address->setSuffix($customer->getSuffix()); - } -+ } - -- $this->pageConfig->getTitle()->set($this->getTitle()); -- -- if ($postedData = $this->_customerSession->getAddressFormData(true)) { -- $postedData['region'] = [ -- 'region_id' => isset($postedData['region_id']) ? $postedData['region_id'] : null, -- 'region' => $postedData['region'], -- ]; -- $this->dataObjectHelper->populateWithArray( -- $this->_address, -- $postedData, -- \Magento\Customer\Api\Data\AddressInterface::class -- ); -+ /** -+ * Precheck attributes that may be required in attribute configuration. -+ * -+ * @return void -+ */ -+ private function precheckRequiredAttributes() -+ { -+ $precheckAttributes = $this->getData('check_attributes_on_render'); -+ $requiredAttributesPrechecked = []; -+ if (!empty($precheckAttributes) && is_array($precheckAttributes)) { -+ foreach ($precheckAttributes as $attributeCode) { -+ $attributeMetadata = $this->addressMetadata->getAttributeMetadata($attributeCode); -+ if ($attributeMetadata && $attributeMetadata->isRequired()) { -+ $requiredAttributesPrechecked[$attributeCode] = $attributeCode; -+ } -+ } - } -- -- return $this; -+ $this->setData('required_attributes_prechecked', $requiredAttributesPrechecked); - } - - /** -diff --git a/app/code/Magento/Customer/Block/Address/Grid.php b/app/code/Magento/Customer/Block/Address/Grid.php -new file mode 100644 -index 00000000000..963efc648d9 ---- /dev/null -+++ b/app/code/Magento/Customer/Block/Address/Grid.php -@@ -0,0 +1,250 @@ -+<?php -+/** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+declare(strict_types=1); -+ -+namespace Magento\Customer\Block\Address; -+ -+use Magento\Customer\Model\ResourceModel\Address\CollectionFactory as AddressCollectionFactory; -+use Magento\Directory\Model\CountryFactory; -+use Magento\Framework\Exception\NoSuchEntityException; -+ -+/** -+ * Customer address grid -+ * -+ * @api -+ */ -+class Grid extends \Magento\Framework\View\Element\Template -+{ -+ /** -+ * @var \Magento\Customer\Helper\Session\CurrentCustomer -+ */ -+ private $currentCustomer; -+ -+ /** -+ * @var \Magento\Customer\Model\ResourceModel\Address\CollectionFactory -+ */ -+ private $addressCollectionFactory; -+ -+ /** -+ * @var \Magento\Customer\Model\ResourceModel\Address\Collection -+ */ -+ private $addressCollection; -+ -+ /** -+ * @var CountryFactory -+ */ -+ private $countryFactory; -+ -+ /** -+ * @param \Magento\Framework\View\Element\Template\Context $context -+ * @param \Magento\Customer\Helper\Session\CurrentCustomer $currentCustomer -+ * @param AddressCollectionFactory $addressCollectionFactory -+ * @param CountryFactory $countryFactory -+ * @param array $data -+ */ -+ public function __construct( -+ \Magento\Framework\View\Element\Template\Context $context, -+ \Magento\Customer\Helper\Session\CurrentCustomer $currentCustomer, -+ AddressCollectionFactory $addressCollectionFactory, -+ CountryFactory $countryFactory, -+ array $data = [] -+ ) { -+ $this->currentCustomer = $currentCustomer; -+ $this->addressCollectionFactory = $addressCollectionFactory; -+ $this->countryFactory = $countryFactory; -+ -+ parent::__construct($context, $data); -+ } -+ -+ /** -+ * Prepare the Address Book section layout -+ * -+ * @return void -+ * @throws \Magento\Framework\Exception\LocalizedException -+ */ -+ protected function _prepareLayout(): void -+ { -+ parent::_prepareLayout(); -+ $this->preparePager(); -+ } -+ -+ /** -+ * Generate and return "New Address" URL -+ * -+ * @return string -+ */ -+ public function getAddAddressUrl(): string -+ { -+ return $this->getUrl('customer/address/new', ['_secure' => true]); -+ } -+ -+ /** -+ * Generate and return "Delete" URL -+ * -+ * @return string -+ */ -+ public function getDeleteUrl(): string -+ { -+ return $this->getUrl('customer/address/delete'); -+ } -+ -+ /** -+ * Generate and return "Edit Address" URL. -+ * -+ * Address ID passed in parameters -+ * -+ * @param int $addressId -+ * @return string -+ */ -+ public function getAddressEditUrl($addressId): string -+ { -+ return $this->getUrl('customer/address/edit', ['_secure' => true, 'id' => $addressId]); -+ } -+ -+ /** -+ * Get current additional customer addresses -+ * -+ * Return array of address interfaces if customer has additional addresses and false in other cases -+ * -+ * @return \Magento\Customer\Api\Data\AddressInterface[] -+ * @throws \Magento\Framework\Exception\LocalizedException -+ * @throws NoSuchEntityException -+ */ -+ public function getAdditionalAddresses(): array -+ { -+ $additional = []; -+ $addresses = $this->getAddressCollection(); -+ $primaryAddressIds = [$this->getDefaultBilling(), $this->getDefaultShipping()]; -+ foreach ($addresses as $address) { -+ if (!in_array((int)$address->getId(), $primaryAddressIds, true)) { -+ $additional[] = $address->getDataModel(); -+ } -+ } -+ return $additional; -+ } -+ -+ /** -+ * Get current customer -+ * -+ * Return stored customer or get it from session -+ * -+ * @return \Magento\Customer\Api\Data\CustomerInterface -+ */ -+ public function getCustomer(): \Magento\Customer\Api\Data\CustomerInterface -+ { -+ $customer = $this->getData('customer'); -+ if ($customer === null) { -+ $customer = $this->currentCustomer->getCustomer(); -+ $this->setData('customer', $customer); -+ } -+ return $customer; -+ } -+ -+ /** -+ * Get one string street address from the Address DTO passed in parameters -+ * -+ * @param \Magento\Customer\Api\Data\AddressInterface $address -+ * @return string -+ */ -+ public function getStreetAddress(\Magento\Customer\Api\Data\AddressInterface $address): string -+ { -+ $street = $address->getStreet(); -+ if (is_array($street)) { -+ $street = implode(', ', $street); -+ } -+ return $street; -+ } -+ -+ /** -+ * Get country name by $countryCode -+ * -+ * Using \Magento\Directory\Model\Country to get country name by $countryCode -+ * -+ * @param string $countryCode -+ * @return string -+ */ -+ public function getCountryByCode(string $countryCode): string -+ { -+ /** @var \Magento\Directory\Model\Country $country */ -+ $country = $this->countryFactory->create(); -+ return $country->loadByCode($countryCode)->getName(); -+ } -+ -+ /** -+ * Get default billing address -+ * -+ * Return address string if address found and null if not -+ * -+ * @return int -+ * @throws \Magento\Framework\Exception\LocalizedException -+ */ -+ private function getDefaultBilling(): int -+ { -+ $customer = $this->getCustomer(); -+ -+ return (int)$customer->getDefaultBilling(); -+ } -+ -+ /** -+ * Get default shipping address -+ * -+ * Return address string if address found and null if not -+ * -+ * @return int -+ * @throws \Magento\Framework\Exception\LocalizedException -+ */ -+ private function getDefaultShipping(): int -+ { -+ $customer = $this->getCustomer(); -+ -+ return (int)$customer->getDefaultShipping(); -+ } -+ -+ /** -+ * Get pager layout -+ * -+ * @return void -+ * @throws \Magento\Framework\Exception\LocalizedException -+ */ -+ private function preparePager(): void -+ { -+ $addressCollection = $this->getAddressCollection(); -+ if (null !== $addressCollection) { -+ $pager = $this->getLayout()->createBlock( -+ \Magento\Theme\Block\Html\Pager::class, -+ 'customer.addresses.pager' -+ )->setCollection($addressCollection); -+ $this->setChild('pager', $pager); -+ } -+ } -+ -+ /** -+ * Get customer addresses collection. -+ * -+ * Filters collection by customer id -+ * -+ * @return \Magento\Customer\Model\ResourceModel\Address\Collection -+ * @throws NoSuchEntityException -+ */ -+ private function getAddressCollection(): \Magento\Customer\Model\ResourceModel\Address\Collection -+ { -+ if (null === $this->addressCollection) { -+ if (null === $this->getCustomer()) { -+ throw new NoSuchEntityException(__('Customer not logged in')); -+ } -+ /** @var \Magento\Customer\Model\ResourceModel\Address\Collection $collection */ -+ $collection = $this->addressCollectionFactory->create(); -+ $collection->setOrder('entity_id', 'desc'); -+ $collection->addFieldToFilter( -+ 'entity_id', -+ ['nin' => [$this->getDefaultBilling(), $this->getDefaultShipping()]] -+ ); -+ $collection->setCustomerFilter([$this->getCustomer()->getId()]); -+ $this->addressCollection = $collection; -+ } -+ return $this->addressCollection; -+ } -+} -diff --git a/app/code/Magento/Customer/Block/Adminhtml/Edit/Address/CancelButton.php b/app/code/Magento/Customer/Block/Adminhtml/Edit/Address/CancelButton.php -new file mode 100644 -index 00000000000..d94b9569183 ---- /dev/null -+++ b/app/code/Magento/Customer/Block/Adminhtml/Edit/Address/CancelButton.php -@@ -0,0 +1,42 @@ -+<?php -+declare(strict_types=1); -+/** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+namespace Magento\Customer\Block\Adminhtml\Edit\Address; -+ -+use Magento\Framework\View\Element\UiComponent\Control\ButtonProviderInterface; -+use Magento\Customer\Block\Adminhtml\Edit\GenericButton; -+ -+/** -+ * Class CancelButton -+ */ -+class CancelButton extends GenericButton implements ButtonProviderInterface -+{ -+ /** -+ * @inheritdoc -+ * -+ * @return array -+ */ -+ public function getButtonData() -+ { -+ return [ -+ 'label' => __('Cancel'), -+ 'on_click' => '', -+ 'data_attribute' => [ -+ 'mage-init' => [ -+ 'Magento_Ui/js/form/button-adapter' => [ -+ 'actions' => [ -+ [ -+ 'targetName' => 'customer_form.areas.address.address.customer_address_update_modal', -+ 'actionName' => 'closeModal' -+ ], -+ ], -+ ], -+ ], -+ ], -+ 'sort_order' => 20 -+ ]; -+ } -+} -diff --git a/app/code/Magento/Customer/Block/Adminhtml/Edit/Address/DeleteButton.php b/app/code/Magento/Customer/Block/Adminhtml/Edit/Address/DeleteButton.php -new file mode 100644 -index 00000000000..da589a25df2 ---- /dev/null -+++ b/app/code/Magento/Customer/Block/Adminhtml/Edit/Address/DeleteButton.php -@@ -0,0 +1,65 @@ -+<?php -+declare(strict_types=1); -+/** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+namespace Magento\Customer\Block\Adminhtml\Edit\Address; -+ -+use Magento\Framework\View\Element\UiComponent\Control\ButtonProviderInterface; -+use Magento\Customer\Ui\Component\Listing\Address\Column\Actions; -+ -+/** -+ * Delete button on edit customer address form -+ */ -+class DeleteButton extends GenericButton implements ButtonProviderInterface -+{ -+ /** -+ * Get delete button data. -+ * -+ * @return array -+ * @throws \Magento\Framework\Exception\LocalizedException -+ */ -+ public function getButtonData() -+ { -+ $data = []; -+ if ($this->getAddressId()) { -+ $data = [ -+ 'label' => __('Delete'), -+ 'on_click' => '', -+ 'data_attribute' => [ -+ 'mage-init' => [ -+ 'Magento_Ui/js/form/button-adapter' => [ -+ 'actions' => [ -+ [ -+ 'targetName' => 'customer_address_form.customer_address_form', -+ 'actionName' => 'deleteAddress', -+ 'params' => [ -+ $this->getDeleteUrl(), -+ ], -+ -+ ] -+ ], -+ ], -+ ], -+ ], -+ 'sort_order' => 20 -+ ]; -+ } -+ return $data; -+ } -+ -+ /** -+ * Get delete button url. -+ * -+ * @return string -+ * @throws \Magento\Framework\Exception\LocalizedException -+ */ -+ private function getDeleteUrl(): string -+ { -+ return $this->getUrl( -+ Actions::CUSTOMER_ADDRESS_PATH_DELETE, -+ ['parent_id' => $this->getCustomerId(), 'id' => $this->getAddressId()] -+ ); -+ } -+} -diff --git a/app/code/Magento/Customer/Block/Adminhtml/Edit/Address/GenericButton.php b/app/code/Magento/Customer/Block/Adminhtml/Edit/Address/GenericButton.php -new file mode 100644 -index 00000000000..ae09ee68968 ---- /dev/null -+++ b/app/code/Magento/Customer/Block/Adminhtml/Edit/Address/GenericButton.php -@@ -0,0 +1,110 @@ -+<?php -+declare(strict_types=1); -+/** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+namespace Magento\Customer\Block\Adminhtml\Edit\Address; -+ -+use Magento\Customer\Model\AddressFactory; -+use Magento\Framework\App\RequestInterface; -+use Magento\Framework\UrlInterface; -+use Magento\Customer\Model\ResourceModel\Address; -+use Magento\Customer\Model\ResourceModel\AddressRepository; -+ -+/** -+ * Class for common code for buttons on the create/edit address form -+ */ -+class GenericButton -+{ -+ /** -+ * @var AddressFactory -+ */ -+ private $addressFactory; -+ -+ /** -+ * @var UrlInterface -+ */ -+ private $urlBuilder; -+ -+ /** -+ * @var Address -+ */ -+ private $addressResourceModel; -+ -+ /** -+ * @var RequestInterface -+ */ -+ private $request; -+ -+ /** -+ * @var AddressRepository -+ */ -+ private $addressRepository; -+ -+ /** -+ * @param AddressFactory $addressFactory -+ * @param UrlInterface $urlBuilder -+ * @param Address $addressResourceModel -+ * @param RequestInterface $request -+ * @param AddressRepository $addressRepository -+ */ -+ public function __construct( -+ AddressFactory $addressFactory, -+ UrlInterface $urlBuilder, -+ Address $addressResourceModel, -+ RequestInterface $request, -+ AddressRepository $addressRepository -+ ) { -+ $this->addressFactory = $addressFactory; -+ $this->urlBuilder = $urlBuilder; -+ $this->addressResourceModel = $addressResourceModel; -+ $this->request = $request; -+ $this->addressRepository = $addressRepository; -+ } -+ -+ /** -+ * Return address Id. -+ * -+ * @return int|null -+ */ -+ public function getAddressId() -+ { -+ $address = $this->addressFactory->create(); -+ -+ $entityId = $this->request->getParam('entity_id'); -+ $this->addressResourceModel->load( -+ $address, -+ $entityId -+ ); -+ -+ return $address->getEntityId() ?: null; -+ } -+ -+ /** -+ * Get customer id. -+ * -+ * @return int|null -+ * @throws \Magento\Framework\Exception\LocalizedException -+ */ -+ public function getCustomerId() -+ { -+ $addressId = $this->request->getParam('entity_id'); -+ -+ $address = $this->addressRepository->getById($addressId); -+ -+ return $address->getCustomerId() ?: null; -+ } -+ -+ /** -+ * Generate url by route and parameters -+ * -+ * @param string $route -+ * @param array $params -+ * @return string -+ */ -+ public function getUrl($route = '', array $params = []): string -+ { -+ return $this->urlBuilder->getUrl($route, $params); -+ } -+} -diff --git a/app/code/Magento/Customer/Block/Adminhtml/Edit/Address/SaveButton.php b/app/code/Magento/Customer/Block/Adminhtml/Edit/Address/SaveButton.php -new file mode 100644 -index 00000000000..9d403185ca8 ---- /dev/null -+++ b/app/code/Magento/Customer/Block/Adminhtml/Edit/Address/SaveButton.php -@@ -0,0 +1,34 @@ -+<?php -+declare(strict_types=1); -+/** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+namespace Magento\Customer\Block\Adminhtml\Edit\Address; -+ -+use Magento\Framework\View\Element\UiComponent\Control\ButtonProviderInterface; -+use Magento\Customer\Block\Adminhtml\Edit\GenericButton; -+ -+/** -+ * Class SaveButton -+ */ -+class SaveButton extends GenericButton implements ButtonProviderInterface -+{ -+ /** -+ * @inheritdoc -+ * -+ * @return array -+ */ -+ public function getButtonData() -+ { -+ return [ -+ 'label' => __('Save'), -+ 'class' => 'save primary', -+ 'data_attribute' => [ -+ 'mage-init' => ['button' => ['event' => 'save']], -+ 'form-role' => 'save', -+ ], -+ 'sort_order' => 10 -+ ]; -+ } -+} -diff --git a/app/code/Magento/Customer/Block/Adminhtml/Edit/DeleteButton.php b/app/code/Magento/Customer/Block/Adminhtml/Edit/DeleteButton.php -index e1bb6feb236..38b2f410d2f 100644 ---- a/app/code/Magento/Customer/Block/Adminhtml/Edit/DeleteButton.php -+++ b/app/code/Magento/Customer/Block/Adminhtml/Edit/DeleteButton.php -@@ -10,6 +10,7 @@ use Magento\Framework\View\Element\UiComponent\Control\ButtonProviderInterface; - - /** - * Class DeleteButton -+ * - * @package Magento\Customer\Block\Adminhtml\Edit - */ - class DeleteButton extends GenericButton implements ButtonProviderInterface -@@ -36,6 +37,8 @@ class DeleteButton extends GenericButton implements ButtonProviderInterface - } - - /** -+ * Get button data. -+ * - * @return array - */ - public function getButtonData() -@@ -53,12 +56,15 @@ class DeleteButton extends GenericButton implements ButtonProviderInterface - ], - 'on_click' => '', - 'sort_order' => 20, -+ 'aclResource' => 'Magento_Customer::delete', - ]; - } - return $data; - } - - /** -+ * Get delete url. -+ * - * @return string - */ - public function getDeleteUrl() -diff --git a/app/code/Magento/Customer/Block/Adminhtml/Edit/InvalidateTokenButton.php b/app/code/Magento/Customer/Block/Adminhtml/Edit/InvalidateTokenButton.php -index 180cb3d66ea..ca24ac9356d 100644 ---- a/app/code/Magento/Customer/Block/Adminhtml/Edit/InvalidateTokenButton.php -+++ b/app/code/Magento/Customer/Block/Adminhtml/Edit/InvalidateTokenButton.php -@@ -9,11 +9,14 @@ use Magento\Framework\View\Element\UiComponent\Control\ButtonProviderInterface; - - /** - * Class InvalidateTokenButton -+ * - * @package Magento\Customer\Block\Adminhtml\Edit - */ - class InvalidateTokenButton extends GenericButton implements ButtonProviderInterface - { - /** -+ * Get button data. -+ * - * @return array - */ - public function getButtonData() -@@ -27,12 +30,15 @@ class InvalidateTokenButton extends GenericButton implements ButtonProviderInter - 'class' => 'invalidate-token', - 'on_click' => 'deleteConfirm("' . $deleteConfirmMsg . '", "' . $this->getInvalidateTokenUrl() . '")', - 'sort_order' => 65, -+ 'aclResource' => 'Magento_Customer::invalidate_tokens', - ]; - } - return $data; - } - - /** -+ * Get invalidate token url. -+ * - * @return string - */ - public function getInvalidateTokenUrl() -diff --git a/app/code/Magento/Customer/Block/Adminhtml/Edit/Renderer/Region.php b/app/code/Magento/Customer/Block/Adminhtml/Edit/Renderer/Region.php -index 9a025211c9b..0aeed1562c5 100644 ---- a/app/code/Magento/Customer/Block/Adminhtml/Edit/Renderer/Region.php -+++ b/app/code/Magento/Customer/Block/Adminhtml/Edit/Renderer/Region.php -@@ -48,7 +48,7 @@ class Region extends \Magento\Backend\Block\AbstractBlock implements - - $regionId = $element->getForm()->getElement('region_id')->getValue(); - -- $html = '<div class="field field-state required admin__field _required">'; -+ $html = '<div class="field field-state admin__field">'; - $element->setClass('input-text admin__control-text'); - $element->setRequired(true); - $html .= $element->getLabelHtml() . '<div class="control admin__field-control">'; -diff --git a/app/code/Magento/Customer/Block/Adminhtml/Edit/ResetPasswordButton.php b/app/code/Magento/Customer/Block/Adminhtml/Edit/ResetPasswordButton.php -index f2d1dd9f0a8..f8a6b3505ae 100644 ---- a/app/code/Magento/Customer/Block/Adminhtml/Edit/ResetPasswordButton.php -+++ b/app/code/Magento/Customer/Block/Adminhtml/Edit/ResetPasswordButton.php -@@ -13,6 +13,8 @@ use Magento\Framework\View\Element\UiComponent\Control\ButtonProviderInterface; - class ResetPasswordButton extends GenericButton implements ButtonProviderInterface - { - /** -+ * Retrieve button-specified settings -+ * - * @return array - */ - public function getButtonData() -@@ -25,12 +27,15 @@ class ResetPasswordButton extends GenericButton implements ButtonProviderInterfa - 'class' => 'reset reset-password', - 'on_click' => sprintf("location.href = '%s';", $this->getResetPasswordUrl()), - 'sort_order' => 60, -+ 'aclResource' => 'Magento_Customer::reset_password', - ]; - } - return $data; - } - - /** -+ * Get reset password url -+ * - * @return string - */ - public function getResetPasswordUrl() -diff --git a/app/code/Magento/Customer/Block/Adminhtml/Edit/Tab/Newsletter/Grid/Renderer/Action.php b/app/code/Magento/Customer/Block/Adminhtml/Edit/Tab/Newsletter/Grid/Renderer/Action.php -index 43d9af36a56..b9ef3966169 100644 ---- a/app/code/Magento/Customer/Block/Adminhtml/Edit/Tab/Newsletter/Grid/Renderer/Action.php -+++ b/app/code/Magento/Customer/Block/Adminhtml/Edit/Tab/Newsletter/Grid/Renderer/Action.php -@@ -5,11 +5,19 @@ - */ - namespace Magento\Customer\Block\Adminhtml\Edit\Tab\Newsletter\Grid\Renderer; - -+use Magento\Framework\Escaper; -+use Magento\Framework\App\ObjectManager; -+ - /** - * Adminhtml newsletter queue grid block action item renderer - */ - class Action extends \Magento\Backend\Block\Widget\Grid\Column\Renderer\AbstractRenderer - { -+ /** -+ * @var Escaper -+ */ -+ private $escaper; -+ - /** - * Core registry - * -@@ -21,17 +29,24 @@ class Action extends \Magento\Backend\Block\Widget\Grid\Column\Renderer\Abstract - * @param \Magento\Backend\Block\Context $context - * @param \Magento\Framework\Registry $registry - * @param array $data -+ * @param Escaper|null $escaper - */ - public function __construct( - \Magento\Backend\Block\Context $context, - \Magento\Framework\Registry $registry, -- array $data = [] -+ array $data = [], -+ Escaper $escaper = null - ) { - $this->_coreRegistry = $registry; -+ $this->escaper = $escaper ?? ObjectManager::getInstance()->get( -+ Escaper::class -+ ); - parent::__construct($context, $data); - } - - /** -+ * Render actions -+ * - * @param \Magento\Framework\DataObject $row - * @return string - */ -@@ -57,15 +72,20 @@ class Action extends \Magento\Backend\Block\Widget\Grid\Column\Renderer\Abstract - } - - /** -+ * Retrieve escaped value -+ * - * @param string $value - * @return string - */ - protected function _getEscapedValue($value) - { -- return addcslashes(htmlspecialchars($value), '\\\''); -+ // phpcs:ignore Magento2.Functions.DiscouragedFunction -+ return addcslashes($this->escaper->escapeHtml($value), '\\\''); - } - - /** -+ * Actions to html -+ * - * @param array $actions - * @return string - */ -diff --git a/app/code/Magento/Customer/Block/Adminhtml/Edit/Tab/Orders.php b/app/code/Magento/Customer/Block/Adminhtml/Edit/Tab/Orders.php -index bb190260e47..3d4ccb789dc 100644 ---- a/app/code/Magento/Customer/Block/Adminhtml/Edit/Tab/Orders.php -+++ b/app/code/Magento/Customer/Block/Adminhtml/Edit/Tab/Orders.php -@@ -57,13 +57,14 @@ class Orders extends \Magento\Backend\Block\Widget\Grid\Extended - } - - /** -- * {@inheritdoc} -+ * @inheritdoc - */ - protected function _construct() - { - parent::_construct(); - $this->setId('customer_orders_grid'); -- $this->setDefaultSort('created_at', 'desc'); -+ $this->setDefaultSort('created_at'); -+ $this->setDefaultDir('desc'); - $this->setUseAjax(true); - } - -@@ -102,11 +103,11 @@ class Orders extends \Magento\Backend\Block\Widget\Grid\Extended - } - - /** -- * {@inheritdoc} -+ * @inheritdoc - */ - protected function _prepareColumns() - { -- $this->addColumn('increment_id', ['header' => __('Order'), 'width' => '100', 'index' => 'increment_id']); -+ $this->addColumn('increment_id', ['header' => __('Order #'), 'width' => '100', 'index' => 'increment_id']); - - $this->addColumn( - 'created_at', -@@ -123,7 +124,8 @@ class Orders extends \Magento\Backend\Block\Widget\Grid\Extended - 'header' => __('Order Total'), - 'index' => 'grand_total', - 'type' => 'currency', -- 'currency' => 'order_currency_code' -+ 'currency' => 'order_currency_code', -+ 'rate' => 1 - ] - ); - -@@ -138,7 +140,7 @@ class Orders extends \Magento\Backend\Block\Widget\Grid\Extended - $this->addColumn( - 'action', - [ -- 'header' => ' ', -+ 'header' => 'Action', - 'filter' => false, - 'sortable' => false, - 'width' => '100px', -@@ -162,7 +164,7 @@ class Orders extends \Magento\Backend\Block\Widget\Grid\Extended - } - - /** -- * {@inheritdoc} -+ * @inheritdoc - */ - public function getGridUrl() - { -diff --git a/app/code/Magento/Customer/Block/Adminhtml/Edit/Tab/View/Cart.php b/app/code/Magento/Customer/Block/Adminhtml/Edit/Tab/View/Cart.php -index 3f2c7cda760..e63c00ba18d 100644 ---- a/app/code/Magento/Customer/Block/Adminhtml/Edit/Tab/View/Cart.php -+++ b/app/code/Magento/Customer/Block/Adminhtml/Edit/Tab/View/Cart.php -@@ -71,13 +71,14 @@ class Cart extends \Magento\Backend\Block\Widget\Grid\Extended - } - - /** -- * {@inheritdoc} -+ * @inheritdoc - */ - protected function _construct() - { - parent::_construct(); - $this->setId('customer_view_cart_grid'); -- $this->setDefaultSort('added_at', 'desc'); -+ $this->setDefaultSort('added_at'); -+ $this->setDefaultDir('desc'); - $this->setSortable(false); - $this->setPagerVisibility(false); - $this->setFilterVisibility(false); -@@ -94,7 +95,7 @@ class Cart extends \Magento\Backend\Block\Widget\Grid\Extended - $quote = $this->getQuote(); - - if ($quote) { -- $collection = $quote->getItemsCollection(false); -+ $collection = $quote->getItemsCollection(true); - } else { - $collection = $this->_dataCollectionFactory->create(); - } -@@ -106,7 +107,7 @@ class Cart extends \Magento\Backend\Block\Widget\Grid\Extended - } - - /** -- * {@inheritdoc} -+ * @inheritdoc - */ - protected function _prepareColumns() - { -@@ -144,7 +145,7 @@ class Cart extends \Magento\Backend\Block\Widget\Grid\Extended - } - - /** -- * {@inheritdoc} -+ * @inheritdoc - */ - public function getRowUrl($row) - { -@@ -152,7 +153,7 @@ class Cart extends \Magento\Backend\Block\Widget\Grid\Extended - } - - /** -- * {@inheritdoc} -+ * @inheritdoc - */ - public function getHeadersVisibility() - { -diff --git a/app/code/Magento/Customer/Block/Adminhtml/Edit/Tab/View/PersonalInfo.php b/app/code/Magento/Customer/Block/Adminhtml/Edit/Tab/View/PersonalInfo.php -index 81b7b8b3f96..a6e0eb0bcbc 100644 ---- a/app/code/Magento/Customer/Block/Adminhtml/Edit/Tab/View/PersonalInfo.php -+++ b/app/code/Magento/Customer/Block/Adminhtml/Edit/Tab/View/PersonalInfo.php -@@ -152,13 +152,12 @@ class PersonalInfo extends \Magento\Backend\Block\Template - /** - * Set customer registry - * -- * @param \Magento\Framework\Registry $coreRegistry -+ * @param \Magento\Framework\Registry $customerRegistry - * @return void - * @deprecated 100.1.0 - */ - public function setCustomerRegistry(\Magento\Customer\Model\CustomerRegistry $customerRegistry) - { -- - $this->customerRegistry = $customerRegistry; - } - -@@ -461,7 +460,7 @@ class PersonalInfo extends \Magento\Backend\Block\Template - 'customer/online_customers/online_minutes_interval', - \Magento\Store\Model\ScopeInterface::SCOPE_STORE - ); -- return intval($configValue) > 0 ? intval($configValue) : self::DEFAULT_ONLINE_MINUTES_INTERVAL; -+ return (int)$configValue > 0 ? (int)$configValue : self::DEFAULT_ONLINE_MINUTES_INTERVAL; - } - - /** -diff --git a/app/code/Magento/Customer/Block/Adminhtml/Edit/Tab/View/Sales.php b/app/code/Magento/Customer/Block/Adminhtml/Edit/Tab/View/Sales.php -index 76c33f143e6..5eeacca4c73 100644 ---- a/app/code/Magento/Customer/Block/Adminhtml/Edit/Tab/View/Sales.php -+++ b/app/code/Magento/Customer/Block/Adminhtml/Edit/Tab/View/Sales.php -@@ -147,10 +147,12 @@ class Sales extends \Magento\Backend\Block\Template - */ - public function getWebsiteCount($websiteId) - { -- return isset($this->_websiteCounts[$websiteId]) ? $this->_websiteCounts[$websiteId] : 0; -+ return $this->_websiteCounts[$websiteId] ?? 0; - } - - /** -+ * Returns Grouped Collection Rows -+ * - * @return array - */ - public function getRows() -@@ -159,6 +161,8 @@ class Sales extends \Magento\Backend\Block\Template - } - - /** -+ * Return totals data -+ * - * @return \Magento\Framework\DataObject - */ - public function getTotals() -@@ -171,7 +175,9 @@ class Sales extends \Magento\Backend\Block\Template - * - * @param float $price - * @param null|int $websiteId -+ * - * @return string -+ * @throws \Magento\Framework\Exception\LocalizedException - */ - public function formatCurrency($price, $websiteId = null) - { -diff --git a/app/code/Magento/Customer/Block/Adminhtml/Edit/Tab/Wishlist/Grid/Renderer/Description.php b/app/code/Magento/Customer/Block/Adminhtml/Edit/Tab/Wishlist/Grid/Renderer/Description.php -index d0b47886dca..aef91184fc7 100644 ---- a/app/code/Magento/Customer/Block/Adminhtml/Edit/Tab/Wishlist/Grid/Renderer/Description.php -+++ b/app/code/Magento/Customer/Block/Adminhtml/Edit/Tab/Wishlist/Grid/Renderer/Description.php -@@ -18,6 +18,6 @@ class Description extends \Magento\Backend\Block\Widget\Grid\Column\Renderer\Abs - */ - public function render(\Magento\Framework\DataObject $row) - { -- return nl2br(htmlspecialchars($row->getData($this->getColumn()->getIndex()))); -+ return nl2br($this->escapeHtml($row->getData($this->getColumn()->getIndex()))); - } - } -diff --git a/app/code/Magento/Customer/Block/DataProviders/AddressAttributeData.php b/app/code/Magento/Customer/Block/DataProviders/AddressAttributeData.php -new file mode 100644 -index 00000000000..2be340c8ccc ---- /dev/null -+++ b/app/code/Magento/Customer/Block/DataProviders/AddressAttributeData.php -@@ -0,0 +1,62 @@ -+<?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\Escaper; -+use Magento\Framework\View\Element\Block\ArgumentInterface; -+use Magento\Customer\Api\AddressMetadataInterface; -+use Magento\Framework\Exception\LocalizedException; -+use Magento\Framework\Exception\NoSuchEntityException; -+ -+/** -+ * Provides address attribute data into template. -+ */ -+class AddressAttributeData implements ArgumentInterface -+{ -+ /** -+ * @var AddressMetadataInterface -+ */ -+ private $addressMetadata; -+ -+ /** -+ * @var Escaper -+ */ -+ private $escaper; -+ -+ /** -+ * @param AddressMetadataInterface $addressMetadata -+ * @param Escaper $escaper -+ */ -+ public function __construct( -+ AddressMetadataInterface $addressMetadata, -+ Escaper $escaper -+ ) { -+ -+ $this->addressMetadata = $addressMetadata; -+ $this->escaper = $escaper; -+ } -+ -+ /** -+ * Returns frontend label for attribute. -+ * -+ * @param string $attributeCode -+ * @return string -+ * @throws LocalizedException -+ */ -+ public function getFrontendLabel(string $attributeCode): string -+ { -+ try { -+ $attribute = $this->addressMetadata->getAttributeMetadata($attributeCode); -+ $frontendLabel = $attribute->getFrontendLabel(); -+ } catch (NoSuchEntityException $e) { -+ $frontendLabel = ''; -+ } -+ -+ return $this->escaper->escapeHtml(__($frontendLabel)); -+ } -+} -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 00000000000..280948439e1 ---- /dev/null -+++ b/app/code/Magento/Customer/Block/DataProviders/PostCodesPatternsAttributeData.php -@@ -0,0 +1,50 @@ -+<?php -+/** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+declare(strict_types=1); -+ -+namespace Magento\Customer\Block\DataProviders; -+ -+use Magento\Framework\Serialize\SerializerInterface; -+use Magento\Framework\View\Element\Block\ArgumentInterface; -+use Magento\Directory\Model\Country\Postcode\Config as PostCodeConfig; -+ -+/** -+ * Provides postcodes patterns into template. -+ */ -+class PostCodesPatternsAttributeData implements ArgumentInterface -+{ -+ /** -+ * @var PostCodeConfig -+ */ -+ private $postCodeConfig; -+ -+ /** -+ * @var SerializerInterface -+ */ -+ private $serializer; -+ -+ /** -+ * Constructor -+ * -+ * @param PostCodeConfig $postCodeConfig -+ * @param SerializerInterface $serializer -+ */ -+ public function __construct(PostCodeConfig $postCodeConfig, SerializerInterface $serializer) -+ { -+ $this->postCodeConfig = $postCodeConfig; -+ $this->serializer = $serializer; -+ } -+ -+ /** -+ * Get serialized post codes -+ * -+ * @return string -+ */ -+ public function getSerializedPostCodes(): string -+ { -+ return $this->serializer->serialize($this->postCodeConfig->getPostCodes()); -+ } -+} -diff --git a/app/code/Magento/Customer/Block/Form/Login.php b/app/code/Magento/Customer/Block/Form/Login.php -index 7b265ae1f0f..d3d3306a49b 100644 ---- a/app/code/Magento/Customer/Block/Form/Login.php -+++ b/app/code/Magento/Customer/Block/Form/Login.php -@@ -47,15 +47,6 @@ class Login extends \Magento\Framework\View\Element\Template - $this->_customerSession = $customerSession; - } - -- /** -- * @return $this -- */ -- protected function _prepareLayout() -- { -- $this->pageConfig->getTitle()->set(__('Customer Login')); -- return parent::_prepareLayout(); -- } -- - /** - * Retrieve form posting url - * -diff --git a/app/code/Magento/Customer/Block/Form/Register.php b/app/code/Magento/Customer/Block/Form/Register.php -index f31012a52a9..a190ccde50b 100644 ---- a/app/code/Magento/Customer/Block/Form/Register.php -+++ b/app/code/Magento/Customer/Block/Form/Register.php -@@ -6,6 +6,7 @@ - namespace Magento\Customer\Block\Form; - - use Magento\Customer\Model\AccountManagement; -+use Magento\Newsletter\Observer\PredispatchNewsletterObserver; - - /** - * Customer register form block -@@ -22,7 +23,7 @@ class Register extends \Magento\Directory\Block\Data - protected $_customerSession; - - /** -- * @var \Magento\Framework\Module\Manager -+ * @var \Magento\Framework\Module\ModuleManagerInterface - */ - protected $_moduleManager; - -@@ -40,7 +41,7 @@ class Register extends \Magento\Directory\Block\Data - * @param \Magento\Framework\App\Cache\Type\Config $configCacheType - * @param \Magento\Directory\Model\ResourceModel\Region\CollectionFactory $regionCollectionFactory - * @param \Magento\Directory\Model\ResourceModel\Country\CollectionFactory $countryCollectionFactory -- * @param \Magento\Framework\Module\Manager $moduleManager -+ * @param \Magento\Framework\Module\ModuleManagerInterface $moduleManager - * @param \Magento\Customer\Model\Session $customerSession - * @param \Magento\Customer\Model\Url $customerUrl - * @param array $data -@@ -54,7 +55,7 @@ class Register extends \Magento\Directory\Block\Data - \Magento\Framework\App\Cache\Type\Config $configCacheType, - \Magento\Directory\Model\ResourceModel\Region\CollectionFactory $regionCollectionFactory, - \Magento\Directory\Model\ResourceModel\Country\CollectionFactory $countryCollectionFactory, -- \Magento\Framework\Module\Manager $moduleManager, -+ \Magento\Framework\Module\ModuleManagerInterface $moduleManager, - \Magento\Customer\Model\Session $customerSession, - \Magento\Customer\Model\Url $customerUrl, - array $data = [] -@@ -85,15 +86,6 @@ class Register extends \Magento\Directory\Block\Data - return $this->_scopeConfig->getValue($path, \Magento\Store\Model\ScopeInterface::SCOPE_STORE); - } - -- /** -- * @return $this -- */ -- protected function _prepareLayout() -- { -- $this->pageConfig->getTitle()->set(__('Create New Customer Account')); -- return parent::_prepareLayout(); -- } -- - /** - * Retrieve form posting url - * -@@ -177,11 +169,13 @@ class Register extends \Magento\Directory\Block\Data - */ - public function isNewsletterEnabled() - { -- return $this->_moduleManager->isOutputEnabled('Magento_Newsletter'); -+ return $this->_moduleManager->isOutputEnabled('Magento_Newsletter') -+ && $this->getConfig(PredispatchNewsletterObserver::XML_PATH_NEWSLETTER_ACTIVE); - } - - /** - * Restore entity data from session -+ * - * Entity and form code must be defined for the form - * - * @param \Magento\Customer\Model\Metadata\Form $form -diff --git a/app/code/Magento/Customer/Block/Widget/Dob.php b/app/code/Magento/Customer/Block/Widget/Dob.php -index 936563d5198..55101fb82af 100644 ---- a/app/code/Magento/Customer/Block/Widget/Dob.php -+++ b/app/code/Magento/Customer/Block/Widget/Dob.php -@@ -61,7 +61,7 @@ class Dob extends AbstractWidget - } - - /** -- * @return void -+ * @inheritdoc - */ - public function _construct() - { -@@ -70,6 +70,8 @@ class Dob extends AbstractWidget - } - - /** -+ * Check if dob attribute enabled in system -+ * - * @return bool - */ - public function isEnabled() -@@ -79,6 +81,8 @@ class Dob extends AbstractWidget - } - - /** -+ * Check if dob attribute marked as required -+ * - * @return bool - */ - public function isRequired() -@@ -88,6 +92,8 @@ class Dob extends AbstractWidget - } - - /** -+ * Set date -+ * - * @param string $date - * @return $this - */ -@@ -135,6 +141,8 @@ class Dob extends AbstractWidget - } - - /** -+ * Get day -+ * - * @return string|bool - */ - public function getDay() -@@ -143,6 +151,8 @@ class Dob extends AbstractWidget - } - - /** -+ * Get month -+ * - * @return string|bool - */ - public function getMonth() -@@ -151,6 +161,8 @@ class Dob extends AbstractWidget - } - - /** -+ * Get year -+ * - * @return string|bool - */ - public function getYear() -@@ -168,6 +180,19 @@ class Dob extends AbstractWidget - return __('Date of Birth'); - } - -+ /** -+ * Retrieve store attribute label -+ * -+ * @param string $attributeCode -+ * -+ * @return string -+ */ -+ public function getStoreLabel($attributeCode) -+ { -+ $attribute = $this->_getAttribute($attributeCode); -+ return $attribute ? __($attribute->getStoreLabel()) : ''; -+ } -+ - /** - * Create correct date field - * -diff --git a/app/code/Magento/Customer/Block/Widget/Gender.php b/app/code/Magento/Customer/Block/Widget/Gender.php -index d03c64a54fb..9df3f1072ce 100644 ---- a/app/code/Magento/Customer/Block/Widget/Gender.php -+++ b/app/code/Magento/Customer/Block/Widget/Gender.php -@@ -64,6 +64,7 @@ class Gender extends AbstractWidget - - /** - * Check if gender attribute enabled in system -+ * - * @return bool - */ - public function isEnabled() -@@ -73,6 +74,7 @@ class Gender extends AbstractWidget - - /** - * Check if gender attribute marked as required -+ * - * @return bool - */ - public function isRequired() -@@ -80,6 +82,19 @@ class Gender extends AbstractWidget - return $this->_getAttribute('gender') ? (bool)$this->_getAttribute('gender')->isRequired() : false; - } - -+ /** -+ * Retrieve store attribute label -+ * -+ * @param string $attributeCode -+ * -+ * @return string -+ */ -+ public function getStoreLabel($attributeCode) -+ { -+ $attribute = $this->_getAttribute($attributeCode); -+ return $attribute ? __($attribute->getStoreLabel()) : ''; -+ } -+ - /** - * Get current customer from session - * -@@ -92,6 +107,7 @@ class Gender extends AbstractWidget - - /** - * Returns options from gender attribute -+ * - * @return OptionInterface[] - */ - public function getGenderOptions() -diff --git a/app/code/Magento/Customer/Block/Widget/Name.php b/app/code/Magento/Customer/Block/Widget/Name.php -index d50045f4a40..6f1b051af74 100644 ---- a/app/code/Magento/Customer/Block/Widget/Name.php -+++ b/app/code/Magento/Customer/Block/Widget/Name.php -@@ -55,7 +55,7 @@ class Name extends AbstractWidget - } - - /** -- * @return void -+ * @inheritdoc - */ - public function _construct() - { -@@ -245,10 +245,13 @@ class Name extends AbstractWidget - */ - public function getAttributeValidationClass($attributeCode) - { -- return $this->_addressHelper->getAttributeValidationClass($attributeCode); -+ $attributeMetadata = $this->_getAttribute($attributeCode); -+ return $attributeMetadata ? $attributeMetadata->getFrontendClass() : ''; - } - - /** -+ * Check if attribute is required -+ * - * @param string $attributeCode - * @return bool - */ -@@ -259,6 +262,8 @@ class Name extends AbstractWidget - } - - /** -+ * Check if attribute is visible -+ * - * @param string $attributeCode - * @return bool - */ -diff --git a/app/code/Magento/Customer/Block/Widget/Taxvat.php b/app/code/Magento/Customer/Block/Widget/Taxvat.php -index e5c9c01dc3a..e35f04f592a 100644 ---- a/app/code/Magento/Customer/Block/Widget/Taxvat.php -+++ b/app/code/Magento/Customer/Block/Widget/Taxvat.php -@@ -63,4 +63,17 @@ class Taxvat extends AbstractWidget - { - return $this->_getAttribute('taxvat') ? (bool)$this->_getAttribute('taxvat')->isRequired() : false; - } -+ -+ /** -+ * Retrieve store attribute label -+ * -+ * @param string $attributeCode -+ * -+ * @return string -+ */ -+ public function getStoreLabel($attributeCode) -+ { -+ $attribute = $this->_getAttribute($attributeCode); -+ return $attribute ? __($attribute->getStoreLabel()) : ''; -+ } - } -diff --git a/app/code/Magento/Customer/Controller/Account/Create.php b/app/code/Magento/Customer/Controller/Account/Create.php -index 8f1bc107547..450fe461534 100644 ---- a/app/code/Magento/Customer/Controller/Account/Create.php -+++ b/app/code/Magento/Customer/Controller/Account/Create.php -@@ -6,12 +6,13 @@ - */ - namespace Magento\Customer\Controller\Account; - -+use Magento\Framework\App\Action\HttpGetActionInterface as HttpGetActionInterface; - use Magento\Customer\Model\Registration; - use Magento\Customer\Model\Session; - use Magento\Framework\View\Result\PageFactory; - use Magento\Framework\App\Action\Context; - --class Create extends \Magento\Customer\Controller\AbstractAccount -+class Create extends \Magento\Customer\Controller\AbstractAccount implements HttpGetActionInterface - { - /** - * @var \Magento\Customer\Model\Registration -diff --git a/app/code/Magento/Customer/Controller/Account/CreatePassword.php b/app/code/Magento/Customer/Controller/Account/CreatePassword.php -index fb2e3dd4290..d12ec57d3c3 100644 ---- a/app/code/Magento/Customer/Controller/Account/CreatePassword.php -+++ b/app/code/Magento/Customer/Controller/Account/CreatePassword.php -@@ -1,17 +1,26 @@ - <?php - /** -- * - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -+declare(strict_types=1); -+ - namespace Magento\Customer\Controller\Account; - - use Magento\Customer\Api\AccountManagementInterface; -+use Magento\Customer\Model\ForgotPasswordToken\ConfirmCustomerByToken; - use Magento\Customer\Model\Session; -+use Magento\Framework\App\Action\HttpGetActionInterface; - use Magento\Framework\View\Result\PageFactory; - use Magento\Framework\App\Action\Context; -+use Magento\Framework\App\ObjectManager; - --class CreatePassword extends \Magento\Customer\Controller\AbstractAccount -+/** -+ * Class CreatePassword -+ * -+ * @package Magento\Customer\Controller\Account -+ */ -+class CreatePassword extends \Magento\Customer\Controller\AbstractAccount implements HttpGetActionInterface - { - /** - * @var \Magento\Customer\Api\AccountManagementInterface -@@ -29,20 +38,30 @@ class CreatePassword extends \Magento\Customer\Controller\AbstractAccount - protected $resultPageFactory; - - /** -- * @param Context $context -- * @param Session $customerSession -- * @param PageFactory $resultPageFactory -- * @param AccountManagementInterface $accountManagement -+ * @var \Magento\Customer\Model\ForgotPasswordToken\ConfirmCustomerByToken -+ */ -+ private $confirmByToken; -+ -+ /** -+ * @param \Magento\Framework\App\Action\Context $context -+ * @param \Magento\Customer\Model\Session $customerSession -+ * @param \Magento\Framework\View\Result\PageFactory $resultPageFactory -+ * @param \Magento\Customer\Api\AccountManagementInterface $accountManagement -+ * @param \Magento\Customer\Model\ForgotPasswordToken\ConfirmCustomerByToken $confirmByToken - */ - public function __construct( - Context $context, - Session $customerSession, - PageFactory $resultPageFactory, -- AccountManagementInterface $accountManagement -+ AccountManagementInterface $accountManagement, -+ ConfirmCustomerByToken $confirmByToken = null - ) { - $this->session = $customerSession; - $this->resultPageFactory = $resultPageFactory; - $this->accountManagement = $accountManagement; -+ $this->confirmByToken = $confirmByToken -+ ?? ObjectManager::getInstance()->get(ConfirmCustomerByToken::class); -+ - parent::__construct($context); - } - -@@ -54,34 +73,37 @@ class CreatePassword extends \Magento\Customer\Controller\AbstractAccount - public function execute() - { - $resetPasswordToken = (string)$this->getRequest()->getParam('token'); -- $customerId = (int)$this->getRequest()->getParam('id'); -- $isDirectLink = $resetPasswordToken != '' && $customerId != 0; -+ $isDirectLink = $resetPasswordToken != ''; - if (!$isDirectLink) { - $resetPasswordToken = (string)$this->session->getRpToken(); -- $customerId = (int)$this->session->getRpCustomerId(); - } - - try { -- $this->accountManagement->validateResetPasswordLinkToken($customerId, $resetPasswordToken); -+ $this->accountManagement->validateResetPasswordLinkToken(null, $resetPasswordToken); -+ -+ $this->confirmByToken->execute($resetPasswordToken); - - if ($isDirectLink) { - $this->session->setRpToken($resetPasswordToken); -- $this->session->setRpCustomerId($customerId); - $resultRedirect = $this->resultRedirectFactory->create(); - $resultRedirect->setPath('*/*/createpassword'); -+ - return $resultRedirect; - } else { - /** @var \Magento\Framework\View\Result\Page $resultPage */ - $resultPage = $this->resultPageFactory->create(); -- $resultPage->getLayout()->getBlock('resetPassword')->setCustomerId($customerId) -- ->setResetPasswordLinkToken($resetPasswordToken); -+ $resultPage->getLayout() -+ ->getBlock('resetPassword') -+ ->setResetPasswordLinkToken($resetPasswordToken); -+ - return $resultPage; - } - } catch (\Exception $exception) { -- $this->messageManager->addError(__('Your password reset link has expired.')); -+ $this->messageManager->addErrorMessage(__('Your password reset link has expired.')); - /** @var \Magento\Framework\Controller\Result\Redirect $resultRedirect */ - $resultRedirect = $this->resultRedirectFactory->create(); - $resultRedirect->setPath('*/*/forgotpassword'); -+ - return $resultRedirect; - } - } -diff --git a/app/code/Magento/Customer/Controller/Account/CreatePost.php b/app/code/Magento/Customer/Controller/Account/CreatePost.php -index bb94063226f..4c9c25b5f33 100644 ---- a/app/code/Magento/Customer/Controller/Account/CreatePost.php -+++ b/app/code/Magento/Customer/Controller/Account/CreatePost.php -@@ -5,6 +5,8 @@ - */ - namespace Magento\Customer\Controller\Account; - -+use Magento\Customer\Api\CustomerRepositoryInterface as CustomerRepository; -+use Magento\Framework\App\Action\HttpPostActionInterface as HttpPostActionInterface; - use Magento\Customer\Model\Account\Redirect as AccountRedirect; - use Magento\Customer\Api\Data\AddressInterface; - use Magento\Framework\Api\DataObjectHelper; -@@ -37,10 +39,12 @@ use Magento\Framework\Data\Form\FormKey\Validator; - use Magento\Customer\Controller\AbstractAccount; - - /** -+ * Post create customer action -+ * - * @SuppressWarnings(PHPMD.TooManyFields) - * @SuppressWarnings(PHPMD.CouplingBetweenObjects) - */ --class CreatePost extends AbstractAccount implements CsrfAwareActionInterface -+class CreatePost extends AbstractAccount implements CsrfAwareActionInterface, HttpPostActionInterface - { - /** - * @var \Magento\Customer\Api\AccountManagementInterface -@@ -132,6 +136,11 @@ class CreatePost extends AbstractAccount implements CsrfAwareActionInterface - */ - private $formKeyValidator; - -+ /** -+ * @var CustomerRepository -+ */ -+ private $customerRepository; -+ - /** - * @param Context $context - * @param Session $customerSession -@@ -151,6 +160,7 @@ class CreatePost extends AbstractAccount implements CsrfAwareActionInterface - * @param CustomerExtractor $customerExtractor - * @param DataObjectHelper $dataObjectHelper - * @param AccountRedirect $accountRedirect -+ * @param CustomerRepository $customerRepository - * @param Validator $formKeyValidator - * - * @SuppressWarnings(PHPMD.ExcessiveParameterList) -@@ -174,6 +184,7 @@ class CreatePost extends AbstractAccount implements CsrfAwareActionInterface - CustomerExtractor $customerExtractor, - DataObjectHelper $dataObjectHelper, - AccountRedirect $accountRedirect, -+ CustomerRepository $customerRepository, - Validator $formKeyValidator = null - ) { - $this->session = $customerSession; -@@ -194,6 +205,7 @@ class CreatePost extends AbstractAccount implements CsrfAwareActionInterface - $this->dataObjectHelper = $dataObjectHelper; - $this->accountRedirect = $accountRedirect; - $this->formKeyValidator = $formKeyValidator ?: ObjectManager::getInstance()->get(Validator::class); -+ $this->customerRepository = $customerRepository; - parent::__construct($context); - } - -@@ -327,34 +339,29 @@ class CreatePost extends AbstractAccount implements CsrfAwareActionInterface - return $this->resultRedirectFactory->create() - ->setUrl($this->_redirect->error($url)); - } -- - $this->session->regenerateId(); -- - try { - $address = $this->extractAddress(); - $addresses = $address === null ? [] : [$address]; -- - $customer = $this->customerExtractor->extract('customer_account_create', $this->_request); - $customer->setAddresses($addresses); -- - $password = $this->getRequest()->getParam('password'); - $confirmation = $this->getRequest()->getParam('password_confirmation'); - $redirectUrl = $this->session->getBeforeAuthUrl(); -- - $this->checkPasswordConfirmation($password, $confirmation); -- - $customer = $this->accountManagement - ->createAccount($customer, $password, $redirectUrl); - - if ($this->getRequest()->getParam('is_subscribed', false)) { -- $this->subscriberFactory->create()->subscribeCustomerById($customer->getId()); -+ $extensionAttributes = $customer->getExtensionAttributes(); -+ $extensionAttributes->setIsSubscribed(true); -+ $customer->setExtensionAttributes($extensionAttributes); -+ $this->customerRepository->save($customer); - } -- - $this->_eventManager->dispatch( - 'customer_register_success', - ['account_controller' => $this, 'customer' => $customer] - ); -- - $confirmationStatus = $this->accountManagement->getConfirmationStatus($customer->getId()); - if ($confirmationStatus === AccountManagementInterface::ACCOUNT_CONFIRMATION_REQUIRED) { - $email = $this->customerUrl->getEmailConfirmationUrl($customer->getEmail()); -diff --git a/app/code/Magento/Customer/Controller/Account/Edit.php b/app/code/Magento/Customer/Controller/Account/Edit.php -index 3c1e6019939..7c2b7215a05 100644 ---- a/app/code/Magento/Customer/Controller/Account/Edit.php -+++ b/app/code/Magento/Customer/Controller/Account/Edit.php -@@ -6,13 +6,14 @@ - */ - namespace Magento\Customer\Controller\Account; - -+use Magento\Framework\App\Action\HttpGetActionInterface as HttpGetActionInterface; - use Magento\Customer\Api\CustomerRepositoryInterface; - use Magento\Framework\Api\DataObjectHelper; - use Magento\Customer\Model\Session; - use Magento\Framework\View\Result\PageFactory; - use Magento\Framework\App\Action\Context; - --class Edit extends \Magento\Customer\Controller\AbstractAccount -+class Edit extends \Magento\Customer\Controller\AbstractAccount implements HttpGetActionInterface - { - /** - * @var \Magento\Customer\Api\CustomerRepositoryInterface -diff --git a/app/code/Magento/Customer/Controller/Account/EditPost.php b/app/code/Magento/Customer/Controller/Account/EditPost.php -index aa5e088f9c8..4eb41cedea2 100644 ---- a/app/code/Magento/Customer/Controller/Account/EditPost.php -+++ b/app/code/Magento/Customer/Controller/Account/EditPost.php -@@ -7,6 +7,9 @@ - - namespace Magento\Customer\Controller\Account; - -+use Magento\Customer\Api\Data\CustomerInterface; -+use Magento\Customer\Model\AddressRegistry; -+use Magento\Framework\App\Action\HttpPostActionInterface as HttpPostActionInterface; - use Magento\Customer\Model\AuthenticationInterface; - use Magento\Customer\Model\Customer\Mapper; - use Magento\Customer\Model\EmailNotificationInterface; -@@ -21,8 +24,10 @@ use Magento\Customer\Api\CustomerRepositoryInterface; - use Magento\Customer\Model\CustomerExtractor; - use Magento\Customer\Model\Session; - use Magento\Framework\App\Action\Context; -+use Magento\Framework\Escaper; - use Magento\Framework\Exception\InputException; - use Magento\Framework\Exception\InvalidEmailOrPasswordException; -+use Magento\Framework\Exception\NoSuchEntityException; - use Magento\Framework\Exception\State\UserLockedException; - use Magento\Customer\Controller\AbstractAccount; - use Magento\Framework\Phrase; -@@ -31,7 +36,7 @@ use Magento\Framework\Phrase; - * Class EditPost - * @SuppressWarnings(PHPMD.CouplingBetweenObjects) - */ --class EditPost extends AbstractAccount implements CsrfAwareActionInterface -+class EditPost extends AbstractAccount implements CsrfAwareActionInterface, HttpPostActionInterface - { - /** - * Form code for data extractor -@@ -78,6 +83,16 @@ class EditPost extends AbstractAccount implements CsrfAwareActionInterface - */ - private $customerMapper; - -+ /** -+ * @var Escaper -+ */ -+ private $escaper; -+ -+ /** -+ * @var AddressRegistry -+ */ -+ private $addressRegistry; -+ - /** - * @param Context $context - * @param Session $customerSession -@@ -85,6 +100,8 @@ class EditPost extends AbstractAccount implements CsrfAwareActionInterface - * @param CustomerRepositoryInterface $customerRepository - * @param Validator $formKeyValidator - * @param CustomerExtractor $customerExtractor -+ * @param Escaper|null $escaper -+ * @param AddressRegistry|null $addressRegistry - */ - public function __construct( - Context $context, -@@ -92,7 +109,9 @@ class EditPost extends AbstractAccount implements CsrfAwareActionInterface - AccountManagementInterface $customerAccountManagement, - CustomerRepositoryInterface $customerRepository, - Validator $formKeyValidator, -- CustomerExtractor $customerExtractor -+ CustomerExtractor $customerExtractor, -+ ?Escaper $escaper = null, -+ AddressRegistry $addressRegistry = null - ) { - parent::__construct($context); - $this->session = $customerSession; -@@ -100,6 +119,8 @@ class EditPost extends AbstractAccount implements CsrfAwareActionInterface - $this->customerRepository = $customerRepository; - $this->formKeyValidator = $formKeyValidator; - $this->customerExtractor = $customerExtractor; -+ $this->escaper = $escaper ?: ObjectManager::getInstance()->get(Escaper::class); -+ $this->addressRegistry = $addressRegistry ?: ObjectManager::getInstance()->get(AddressRegistry::class); - } - - /** -@@ -185,6 +206,9 @@ class EditPost extends AbstractAccount implements CsrfAwareActionInterface - // whether a customer enabled change password option - $isPasswordChanged = $this->changeCustomerPassword($currentCustomerDataObject->getEmail()); - -+ // No need to validate customer address while editing customer profile -+ $this->disableAddressValidation($customerCandidateDataObject); -+ - $this->customerRepository->save($customerCandidateDataObject); - $this->getEmailNotification()->credentialsChanged( - $customerCandidateDataObject, -@@ -195,7 +219,7 @@ class EditPost extends AbstractAccount implements CsrfAwareActionInterface - $this->messageManager->addSuccess(__('You saved the account information.')); - return $resultRedirect->setPath('customer/account'); - } catch (InvalidEmailOrPasswordException $e) { -- $this->messageManager->addError($e->getMessage()); -+ $this->messageManager->addErrorMessage($this->escaper->escapeHtml($e->getMessage())); - } catch (UserLockedException $e) { - $message = __( - 'The account sign-in was incorrect or your account is disabled temporarily. ' -@@ -206,9 +230,9 @@ class EditPost extends AbstractAccount implements CsrfAwareActionInterface - $this->messageManager->addError($message); - return $resultRedirect->setPath('customer/account/login'); - } catch (InputException $e) { -- $this->messageManager->addError($e->getMessage()); -+ $this->messageManager->addErrorMessage($this->escaper->escapeHtml($e->getMessage())); - foreach ($e->getErrors() as $error) { -- $this->messageManager->addError($error->getMessage()); -+ $this->messageManager->addErrorMessage($this->escaper->escapeHtml($error->getMessage())); - } - } catch (\Magento\Framework\Exception\LocalizedException $e) { - $this->messageManager->addError($e->getMessage()); -@@ -342,4 +366,18 @@ class EditPost extends AbstractAccount implements CsrfAwareActionInterface - } - return $this->customerMapper; - } -+ -+ /** -+ * Disable Customer Address Validation -+ * -+ * @param CustomerInterface $customer -+ * @throws NoSuchEntityException -+ */ -+ private function disableAddressValidation($customer) -+ { -+ foreach ($customer->getAddresses() as $address) { -+ $addressModel = $this->addressRegistry->retrieve($address->getId()); -+ $addressModel->setShouldIgnoreValidation(true); -+ } -+ } - } -diff --git a/app/code/Magento/Customer/Controller/Account/ForgotPassword.php b/app/code/Magento/Customer/Controller/Account/ForgotPassword.php -index f115b64efeb..cfa60558077 100644 ---- a/app/code/Magento/Customer/Controller/Account/ForgotPassword.php -+++ b/app/code/Magento/Customer/Controller/Account/ForgotPassword.php -@@ -1,16 +1,19 @@ - <?php - /** -- * - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ - namespace Magento\Customer\Controller\Account; - -+use Magento\Framework\App\Action\HttpGetActionInterface as HttpGetActionInterface; - use Magento\Customer\Model\Session; - use Magento\Framework\App\Action\Context; - use Magento\Framework\View\Result\PageFactory; - --class ForgotPassword extends \Magento\Customer\Controller\AbstractAccount -+/** -+ * Forgot Password controller -+ */ -+class ForgotPassword extends \Magento\Customer\Controller\AbstractAccount implements HttpGetActionInterface - { - /** - * @var PageFactory -@@ -40,10 +43,17 @@ class ForgotPassword extends \Magento\Customer\Controller\AbstractAccount - /** - * Forgot customer password page - * -- * @return \Magento\Framework\View\Result\Page -+ * @return \Magento\Framework\Controller\Result\Redirect|\Magento\Framework\View\Result\Page - */ - public function execute() - { -+ if ($this->session->isLoggedIn()) { -+ /** @var \Magento\Framework\Controller\Result\Redirect $resultRedirect */ -+ $resultRedirect = $this->resultRedirectFactory->create(); -+ $resultRedirect->setPath('*/*/'); -+ return $resultRedirect; -+ } -+ - /** @var \Magento\Framework\View\Result\Page $resultPage */ - $resultPage = $this->resultPageFactory->create(); - $resultPage->getLayout()->getBlock('forgotPassword')->setEmailValue($this->session->getForgottenEmail()); -diff --git a/app/code/Magento/Customer/Controller/Account/ForgotPasswordPost.php b/app/code/Magento/Customer/Controller/Account/ForgotPasswordPost.php -index f3024738730..3c7fca99184 100644 ---- a/app/code/Magento/Customer/Controller/Account/ForgotPasswordPost.php -+++ b/app/code/Magento/Customer/Controller/Account/ForgotPasswordPost.php -@@ -6,6 +6,7 @@ - */ - namespace Magento\Customer\Controller\Account; - -+use Magento\Framework\App\Action\HttpPostActionInterface as HttpPostActionInterface; - use Magento\Customer\Api\AccountManagementInterface; - use Magento\Customer\Model\AccountManagement; - use Magento\Customer\Model\Session; -@@ -18,7 +19,7 @@ use Magento\Framework\Exception\SecurityViolationException; - * ForgotPasswordPost controller - * @SuppressWarnings(PHPMD.CouplingBetweenObjects) - */ --class ForgotPasswordPost extends \Magento\Customer\Controller\AbstractAccount -+class ForgotPasswordPost extends \Magento\Customer\Controller\AbstractAccount implements HttpPostActionInterface - { - /** - * @var \Magento\Customer\Api\AccountManagementInterface -diff --git a/app/code/Magento/Customer/Controller/Account/Index.php b/app/code/Magento/Customer/Controller/Account/Index.php -index 2ecf79d35b1..301fd584cfa 100644 ---- a/app/code/Magento/Customer/Controller/Account/Index.php -+++ b/app/code/Magento/Customer/Controller/Account/Index.php -@@ -6,10 +6,11 @@ - */ - namespace Magento\Customer\Controller\Account; - -+use Magento\Framework\App\Action\HttpGetActionInterface as HttpGetActionInterface; - use Magento\Framework\App\Action\Context; - use Magento\Framework\View\Result\PageFactory; - --class Index extends \Magento\Customer\Controller\AbstractAccount -+class Index extends \Magento\Customer\Controller\AbstractAccount implements HttpGetActionInterface - { - /** - * @var PageFactory -diff --git a/app/code/Magento/Customer/Controller/Account/Login.php b/app/code/Magento/Customer/Controller/Account/Login.php -index d685191bf43..64cc24095f9 100644 ---- a/app/code/Magento/Customer/Controller/Account/Login.php -+++ b/app/code/Magento/Customer/Controller/Account/Login.php -@@ -6,12 +6,17 @@ - */ - namespace Magento\Customer\Controller\Account; - -+use Magento\Framework\App\Action\HttpGetActionInterface; - use Magento\Customer\Model\Session; - use Magento\Framework\App\Action\Context; -+use Magento\Framework\App\Action\HttpPostActionInterface; - use Magento\Framework\View\Result\PageFactory; - use Magento\Customer\Controller\AbstractAccount; - --class Login extends AbstractAccount -+/** -+ * Login form page. Accepts POST for backward compatibility reasons. -+ */ -+class Login extends AbstractAccount implements HttpGetActionInterface, HttpPostActionInterface - { - /** - * @var Session -diff --git a/app/code/Magento/Customer/Controller/Account/LoginPost.php b/app/code/Magento/Customer/Controller/Account/LoginPost.php -index 5c7eee78e5f..04051fbbf36 100644 ---- a/app/code/Magento/Customer/Controller/Account/LoginPost.php -+++ b/app/code/Magento/Customer/Controller/Account/LoginPost.php -@@ -6,6 +6,7 @@ - - namespace Magento\Customer\Controller\Account; - -+use Magento\Framework\App\Action\HttpPostActionInterface as HttpPostActionInterface; - use Magento\Customer\Model\Account\Redirect as AccountRedirect; - use Magento\Framework\App\Action\Context; - use Magento\Customer\Model\Session; -@@ -27,7 +28,7 @@ use Magento\Framework\Phrase; - /** - * @SuppressWarnings(PHPMD.CouplingBetweenObjects) - */ --class LoginPost extends AbstractAccount implements CsrfAwareActionInterface -+class LoginPost extends AbstractAccount implements CsrfAwareActionInterface, HttpPostActionInterface - { - /** - * @var \Magento\Customer\Api\AccountManagementInterface -diff --git a/app/code/Magento/Customer/Controller/Account/Logout.php b/app/code/Magento/Customer/Controller/Account/Logout.php -index 19dabf9effa..9344f482bd6 100644 ---- a/app/code/Magento/Customer/Controller/Account/Logout.php -+++ b/app/code/Magento/Customer/Controller/Account/Logout.php -@@ -6,6 +6,8 @@ - */ - namespace Magento\Customer\Controller\Account; - -+use Magento\Framework\App\Action\HttpPostActionInterface; -+use Magento\Framework\App\Action\HttpGetActionInterface; - use Magento\Customer\Model\Session; - use Magento\Framework\App\Action\Context; - use Magento\Framework\App\ObjectManager; -@@ -13,7 +15,10 @@ use Magento\Framework\Stdlib\Cookie\CookieMetadataFactory; - use Magento\Framework\Stdlib\Cookie\PhpCookieManager; - use Magento\Customer\Controller\AbstractAccount; - --class Logout extends AbstractAccount -+/** -+ * Sign out a customer. -+ */ -+class Logout extends AbstractAccount implements HttpGetActionInterface, HttpPostActionInterface - { - /** - * @var Session -diff --git a/app/code/Magento/Customer/Controller/Account/LogoutSuccess.php b/app/code/Magento/Customer/Controller/Account/LogoutSuccess.php -index c58416434c0..e00494d221d 100644 ---- a/app/code/Magento/Customer/Controller/Account/LogoutSuccess.php -+++ b/app/code/Magento/Customer/Controller/Account/LogoutSuccess.php -@@ -6,10 +6,11 @@ - */ - namespace Magento\Customer\Controller\Account; - -+use Magento\Framework\App\Action\HttpGetActionInterface as HttpGetActionInterface; - use Magento\Framework\App\Action\Context; - use Magento\Framework\View\Result\PageFactory; - --class LogoutSuccess extends \Magento\Customer\Controller\AbstractAccount -+class LogoutSuccess extends \Magento\Customer\Controller\AbstractAccount implements HttpGetActionInterface - { - /** - * @var PageFactory -diff --git a/app/code/Magento/Customer/Controller/Account/ResetPasswordPost.php b/app/code/Magento/Customer/Controller/Account/ResetPasswordPost.php -index 3de44e35d24..27a00f86dd9 100644 ---- a/app/code/Magento/Customer/Controller/Account/ResetPasswordPost.php -+++ b/app/code/Magento/Customer/Controller/Account/ResetPasswordPost.php -@@ -1,6 +1,5 @@ - <?php - /** -- * - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -@@ -10,11 +9,16 @@ use Magento\Customer\Api\AccountManagementInterface; - use Magento\Customer\Api\CustomerRepositoryInterface; - use Magento\Customer\Model\Session; - use Magento\Framework\App\Action\Context; -+use Magento\Framework\App\Action\HttpPostActionInterface; - use Magento\Framework\Exception\InputException; - use Magento\Customer\Model\Customer\CredentialsValidator; --use Magento\Framework\App\ObjectManager; - --class ResetPasswordPost extends \Magento\Customer\Controller\AbstractAccount -+/** -+ * Class ResetPasswordPost -+ * -+ * @package Magento\Customer\Controller\Account -+ */ -+class ResetPasswordPost extends \Magento\Customer\Controller\AbstractAccount implements HttpPostActionInterface - { - /** - * @var \Magento\Customer\Api\AccountManagementInterface -@@ -31,17 +35,14 @@ class ResetPasswordPost extends \Magento\Customer\Controller\AbstractAccount - */ - protected $session; - -- /** -- * @var CredentialsValidator -- */ -- private $credentialsValidator; -- - /** - * @param Context $context - * @param Session $customerSession - * @param AccountManagementInterface $accountManagement - * @param CustomerRepositoryInterface $customerRepository - * @param CredentialsValidator|null $credentialsValidator -+ * -+ * @SuppressWarnings(PHPMD.UnusedFormalParameter) - */ - public function __construct( - Context $context, -@@ -53,8 +54,6 @@ class ResetPasswordPost extends \Magento\Customer\Controller\AbstractAccount - $this->session = $customerSession; - $this->accountManagement = $accountManagement; - $this->customerRepository = $customerRepository; -- $this->credentialsValidator = $credentialsValidator ?: ObjectManager::getInstance() -- ->get(CredentialsValidator::class); - parent::__construct($context); - } - -@@ -70,29 +69,32 @@ class ResetPasswordPost extends \Magento\Customer\Controller\AbstractAccount - /** @var \Magento\Framework\Controller\Result\Redirect $resultRedirect */ - $resultRedirect = $this->resultRedirectFactory->create(); - $resetPasswordToken = (string)$this->getRequest()->getQuery('token'); -- $customerId = (int)$this->getRequest()->getQuery('id'); - $password = (string)$this->getRequest()->getPost('password'); - $passwordConfirmation = (string)$this->getRequest()->getPost('password_confirmation'); - - if ($password !== $passwordConfirmation) { - $this->messageManager->addError(__("New Password and Confirm New Password values didn't match.")); -- $resultRedirect->setPath('*/*/createPassword', ['id' => $customerId, 'token' => $resetPasswordToken]); -+ $resultRedirect->setPath('*/*/createPassword', ['token' => $resetPasswordToken]); -+ - return $resultRedirect; - } - if (iconv_strlen($password) <= 0) { - $this->messageManager->addError(__('Please enter a new password.')); -- $resultRedirect->setPath('*/*/createPassword', ['id' => $customerId, 'token' => $resetPasswordToken]); -+ $resultRedirect->setPath('*/*/createPassword', ['token' => $resetPasswordToken]); -+ - return $resultRedirect; - } - - try { -- $customerEmail = $this->customerRepository->getById($customerId)->getEmail(); -- $this->credentialsValidator->checkPasswordDifferentFromEmail($customerEmail, $password); -- $this->accountManagement->resetPassword($customerEmail, $resetPasswordToken, $password); -+ $this->accountManagement->resetPassword( -+ null, -+ $resetPasswordToken, -+ $password -+ ); - $this->session->unsRpToken(); -- $this->session->unsRpCustomerId(); - $this->messageManager->addSuccess(__('You updated your password.')); - $resultRedirect->setPath('*/*/login'); -+ - return $resultRedirect; - } catch (InputException $e) { - $this->messageManager->addError($e->getMessage()); -@@ -102,7 +104,8 @@ class ResetPasswordPost extends \Magento\Customer\Controller\AbstractAccount - } catch (\Exception $exception) { - $this->messageManager->addError(__('Something went wrong while saving the new password.')); - } -- $resultRedirect->setPath('*/*/createPassword', ['id' => $customerId, 'token' => $resetPasswordToken]); -+ $resultRedirect->setPath('*/*/createPassword', ['token' => $resetPasswordToken]); -+ - return $resultRedirect; - } - } -diff --git a/app/code/Magento/Customer/Controller/Address/Delete.php b/app/code/Magento/Customer/Controller/Address/Delete.php -index ef92bd2ef53..a30e15db4b3 100644 ---- a/app/code/Magento/Customer/Controller/Address/Delete.php -+++ b/app/code/Magento/Customer/Controller/Address/Delete.php -@@ -6,9 +6,16 @@ - */ - namespace Magento\Customer\Controller\Address; - --class Delete extends \Magento\Customer\Controller\Address -+use Magento\Framework\App\Action\HttpGetActionInterface; -+use Magento\Framework\App\Action\HttpPostActionInterface; -+ -+/** -+ * Delete customer address controller action. -+ */ -+class Delete extends \Magento\Customer\Controller\Address implements HttpPostActionInterface, HttpGetActionInterface - { - /** -+ * @inheritdoc - * @return \Magento\Framework\Controller\Result\Redirect - */ - public function execute() -diff --git a/app/code/Magento/Customer/Controller/Address/Edit.php b/app/code/Magento/Customer/Controller/Address/Edit.php -index a30eb10b115..0a5affefae3 100644 ---- a/app/code/Magento/Customer/Controller/Address/Edit.php -+++ b/app/code/Magento/Customer/Controller/Address/Edit.php -@@ -6,7 +6,9 @@ - */ - namespace Magento\Customer\Controller\Address; - --class Edit extends \Magento\Customer\Controller\Address -+use Magento\Framework\App\Action\HttpGetActionInterface as HttpGetActionInterface; -+ -+class Edit extends \Magento\Customer\Controller\Address implements HttpGetActionInterface - { - /** - * Customer address edit action -diff --git a/app/code/Magento/Customer/Controller/Address/File/Upload.php b/app/code/Magento/Customer/Controller/Address/File/Upload.php -new file mode 100644 -index 00000000000..adb4c7abd17 ---- /dev/null -+++ b/app/code/Magento/Customer/Controller/Address/File/Upload.php -@@ -0,0 +1,146 @@ -+<?php -+/** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+declare(strict_types=1); -+ -+namespace Magento\Customer\Controller\Address\File; -+ -+use Magento\Framework\App\Action\Action; -+use Magento\Framework\App\Action\Context; -+use Magento\Framework\App\Action\HttpPostActionInterface; -+use Magento\Framework\Api\CustomAttributesDataInterface; -+use Magento\Customer\Api\AddressMetadataInterface; -+use Magento\Customer\Model\FileUploader; -+use Magento\Customer\Model\FileUploaderFactory; -+use Magento\Framework\Controller\ResultFactory; -+use Magento\Framework\Exception\LocalizedException; -+use Psr\Log\LoggerInterface; -+use Magento\Customer\Model\FileProcessorFactory; -+ -+/** -+ * Class for upload files for customer custom address attributes -+ */ -+class Upload extends Action implements HttpPostActionInterface -+{ -+ /** -+ * @var FileUploaderFactory -+ */ -+ private $fileUploaderFactory; -+ -+ /** -+ * @var AddressMetadataInterface -+ */ -+ private $addressMetadataService; -+ -+ /** -+ * @var LoggerInterface -+ */ -+ private $logger; -+ -+ /** -+ * @var FileProcessorFactory -+ */ -+ private $fileProcessorFactory; -+ -+ /** -+ * @param Context $context -+ * @param FileUploaderFactory $fileUploaderFactory -+ * @param AddressMetadataInterface $addressMetadataService -+ * @param LoggerInterface $logger -+ * @param FileProcessorFactory $fileProcessorFactory -+ */ -+ public function __construct( -+ Context $context, -+ FileUploaderFactory $fileUploaderFactory, -+ AddressMetadataInterface $addressMetadataService, -+ LoggerInterface $logger, -+ FileProcessorFactory $fileProcessorFactory -+ ) { -+ $this->fileUploaderFactory = $fileUploaderFactory; -+ $this->addressMetadataService = $addressMetadataService; -+ $this->logger = $logger; -+ $this->fileProcessorFactory = $fileProcessorFactory; -+ parent::__construct($context); -+ } -+ -+ /** -+ * @inheritDoc -+ */ -+ public function execute() -+ { -+ try { -+ $requestedFiles = $this->getRequest()->getFiles('custom_attributes'); -+ if (empty($requestedFiles)) { -+ $result = $this->processError(__('No files for upload.')); -+ } else { -+ $attributeCode = key($requestedFiles); -+ $attributeMetadata = $this->addressMetadataService->getAttributeMetadata($attributeCode); -+ -+ /** @var FileUploader $fileUploader */ -+ $fileUploader = $this->fileUploaderFactory->create([ -+ 'attributeMetadata' => $attributeMetadata, -+ 'entityTypeCode' => AddressMetadataInterface::ENTITY_TYPE_ADDRESS, -+ 'scope' => CustomAttributesDataInterface::CUSTOM_ATTRIBUTES, -+ ]); -+ -+ $errors = $fileUploader->validate(); -+ if (true !== $errors) { -+ $errorMessage = implode('</br>', $errors); -+ $result = $this->processError(($errorMessage)); -+ } else { -+ $result = $fileUploader->upload(); -+ $this->moveTmpFileToSuitableFolder($result); -+ } -+ } -+ } catch (LocalizedException $e) { -+ $result = $this->processError($e->getMessage(), $e->getCode()); -+ } catch (\Exception $e) { -+ $this->logger->critical($e); -+ $result = $this->processError($e->getMessage(), $e->getCode()); -+ } -+ -+ /** @var \Magento\Framework\Controller\Result\Json $resultJson */ -+ $resultJson = $this->resultFactory->create(ResultFactory::TYPE_JSON); -+ $resultJson->setData($result); -+ return $resultJson; -+ } -+ -+ /** -+ * Move file from temporary folder to the 'customer_address' media folder -+ * -+ * @param array $fileInfo -+ * @throws LocalizedException -+ */ -+ private function moveTmpFileToSuitableFolder(&$fileInfo) -+ { -+ $fileName = $fileInfo['file']; -+ $fileProcessor = $this->fileProcessorFactory -+ ->create(['entityTypeCode' => AddressMetadataInterface::ENTITY_TYPE_ADDRESS]); -+ -+ $newFilePath = $fileProcessor->moveTemporaryFile($fileName); -+ $fileInfo['file'] = $newFilePath; -+ $fileInfo['url'] = $fileProcessor->getViewUrl( -+ $newFilePath, -+ 'file' -+ ); -+ } -+ -+ /** -+ * Prepare result array for errors -+ * -+ * @param string $message -+ * @param int $code -+ * @return array -+ */ -+ private function processError($message, $code = 0) -+ { -+ $result = [ -+ 'error' => $message, -+ 'errorcode' => $code, -+ ]; -+ -+ return $result; -+ } -+} -diff --git a/app/code/Magento/Customer/Controller/Address/Form.php b/app/code/Magento/Customer/Controller/Address/Form.php -index fc62a6c1a57..9b3f4e36be0 100644 ---- a/app/code/Magento/Customer/Controller/Address/Form.php -+++ b/app/code/Magento/Customer/Controller/Address/Form.php -@@ -6,7 +6,9 @@ - */ - namespace Magento\Customer\Controller\Address; - --class Form extends \Magento\Customer\Controller\Address -+use Magento\Framework\App\Action\HttpGetActionInterface as HttpGetActionInterface; -+ -+class Form extends \Magento\Customer\Controller\Address implements HttpGetActionInterface - { - /** - * Address book form -diff --git a/app/code/Magento/Customer/Controller/Address/FormPost.php b/app/code/Magento/Customer/Controller/Address/FormPost.php -index 60e1f6eb172..25618e31291 100644 ---- a/app/code/Magento/Customer/Controller/Address/FormPost.php -+++ b/app/code/Magento/Customer/Controller/Address/FormPost.php -@@ -6,6 +6,7 @@ - - namespace Magento\Customer\Controller\Address; - -+use Magento\Framework\App\Action\HttpPostActionInterface as HttpPostActionInterface; - use Magento\Customer\Api\AddressRepositoryInterface; - use Magento\Customer\Api\Data\AddressInterfaceFactory; - use Magento\Customer\Api\Data\RegionInterface; -@@ -25,9 +26,11 @@ use Magento\Framework\Reflection\DataObjectProcessor; - use Magento\Framework\View\Result\PageFactory; - - /** -+ * Customer Address Form Post Controller -+ * - * @SuppressWarnings(PHPMD.CouplingBetweenObjects) - */ --class FormPost extends \Magento\Customer\Controller\Address -+class FormPost extends \Magento\Customer\Controller\Address implements HttpPostActionInterface - { - /** - * @var RegionFactory -@@ -119,8 +122,18 @@ class FormPost extends \Magento\Customer\Controller\Address - \Magento\Customer\Api\Data\AddressInterface::class - ); - $addressDataObject->setCustomerId($this->_getSession()->getCustomerId()) -- ->setIsDefaultBilling($this->getRequest()->getParam('default_billing', false)) -- ->setIsDefaultShipping($this->getRequest()->getParam('default_shipping', false)); -+ ->setIsDefaultBilling( -+ $this->getRequest()->getParam( -+ 'default_billing', -+ isset($existingAddressData['default_billing']) ? $existingAddressData['default_billing'] : false -+ ) -+ ) -+ ->setIsDefaultShipping( -+ $this->getRequest()->getParam( -+ 'default_shipping', -+ isset($existingAddressData['default_shipping']) ? $existingAddressData['default_shipping'] : false -+ ) -+ ); - - return $addressDataObject; - } -diff --git a/app/code/Magento/Customer/Controller/Address/Index.php b/app/code/Magento/Customer/Controller/Address/Index.php -index ad04c7bd5c7..92c6078349d 100644 ---- a/app/code/Magento/Customer/Controller/Address/Index.php -+++ b/app/code/Magento/Customer/Controller/Address/Index.php -@@ -6,12 +6,13 @@ - */ - namespace Magento\Customer\Controller\Address; - -+use Magento\Framework\App\Action\HttpGetActionInterface as HttpGetActionInterface; - use Magento\Customer\Api\CustomerRepositoryInterface; - - /** - * @SuppressWarnings(PHPMD.CouplingBetweenObjects) - */ --class Index extends \Magento\Customer\Controller\Address -+class Index extends \Magento\Customer\Controller\Address implements HttpGetActionInterface - { - /** - * @var CustomerRepositoryInterface -@@ -28,9 +29,9 @@ class Index extends \Magento\Customer\Controller\Address - * @param \Magento\Customer\Api\Data\RegionInterfaceFactory $regionDataFactory - * @param \Magento\Framework\Reflection\DataObjectProcessor $dataProcessor - * @param \Magento\Framework\Api\DataObjectHelper $dataObjectHelper -- * @param CustomerRepositoryInterface $customerRepository - * @param \Magento\Framework\Controller\Result\ForwardFactory $resultForwardFactory - * @param \Magento\Framework\View\Result\PageFactory $resultPageFactory -+ * @param CustomerRepositoryInterface $customerRepository - * @SuppressWarnings(PHPMD.ExcessiveParameterList) - */ - public function __construct( -diff --git a/app/code/Magento/Customer/Controller/Address/NewAction.php b/app/code/Magento/Customer/Controller/Address/NewAction.php -index e97c7460578..043c2b91db2 100644 ---- a/app/code/Magento/Customer/Controller/Address/NewAction.php -+++ b/app/code/Magento/Customer/Controller/Address/NewAction.php -@@ -6,7 +6,9 @@ - */ - namespace Magento\Customer\Controller\Address; - --class NewAction extends \Magento\Customer\Controller\Address -+use Magento\Framework\App\Action\HttpGetActionInterface as HttpGetActionInterface; -+ -+class NewAction extends \Magento\Customer\Controller\Address implements HttpGetActionInterface - { - /** - * @return \Magento\Framework\Controller\Result\Forward -diff --git a/app/code/Magento/Customer/Controller/Adminhtml/Address/DefaultBillingAddress.php b/app/code/Magento/Customer/Controller/Adminhtml/Address/DefaultBillingAddress.php -new file mode 100644 -index 00000000000..bf8ca4aeb5c ---- /dev/null -+++ b/app/code/Magento/Customer/Controller/Adminhtml/Address/DefaultBillingAddress.php -@@ -0,0 +1,109 @@ -+<?php -+declare(strict_types=1); -+/** -+ * -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+namespace Magento\Customer\Controller\Adminhtml\Address; -+ -+use Magento\Backend\App\Action; -+use Magento\Customer\Api\Data\AddressInterface; -+use Magento\Customer\Api\AddressRepositoryInterface; -+use Magento\Framework\App\Action\HttpPostActionInterface; -+use Magento\Framework\Controller\Result\Json; -+use Magento\Framework\Controller\Result\JsonFactory; -+use Psr\Log\LoggerInterface; -+ -+/** -+ * Class to process set default billing address action -+ */ -+class DefaultBillingAddress extends Action implements HttpPostActionInterface -+{ -+ /** -+ * Authorization level of a basic admin session -+ * -+ * @see _isAllowed() -+ */ -+ public const ADMIN_RESOURCE = 'Magento_Customer::manage'; -+ -+ /** -+ * @var AddressRepositoryInterface -+ */ -+ private $addressRepository; -+ -+ /** -+ * @var LoggerInterface -+ */ -+ private $logger; -+ -+ /** -+ * @var JsonFactory -+ */ -+ private $resultJsonFactory; -+ -+ /** -+ * @param Action\Context $context -+ * @param AddressRepositoryInterface $addressRepository -+ * @param LoggerInterface $logger -+ * @param JsonFactory $resultJsonFactory -+ */ -+ public function __construct( -+ Action\Context $context, -+ AddressRepositoryInterface $addressRepository, -+ LoggerInterface $logger, -+ JsonFactory $resultJsonFactory -+ ) { -+ parent::__construct($context); -+ $this->addressRepository = $addressRepository; -+ $this->logger = $logger; -+ $this->resultJsonFactory = $resultJsonFactory; -+ } -+ -+ /** -+ * Execute action to set customer default billing address -+ * -+ * @return Json -+ */ -+ public function execute(): Json -+ { -+ $customerId = $this->getRequest()->getParam('parent_id', false); -+ $addressId = $this->getRequest()->getParam('id', false); -+ $error = true; -+ $message = __('There is no address id in setting default billing address.'); -+ -+ if ($addressId) { -+ try { -+ $address = $this->addressRepository->getById($addressId)->setCustomerId($customerId); -+ $this->setAddressAsDefault($address); -+ $this->addressRepository->save($address); -+ $message = __('Default billing address has been changed.'); -+ $error = false; -+ } catch (\Exception $e) { -+ $message = __('We can\'t change default billing address right now.'); -+ $this->logger->critical($e); -+ } -+ } -+ -+ $resultJson = $this->resultJsonFactory->create(); -+ $resultJson->setData( -+ [ -+ 'message' => $message, -+ 'error' => $error, -+ ] -+ ); -+ -+ return $resultJson; -+ } -+ -+ /** -+ * Set address as default billing address -+ * -+ * @param AddressInterface $address -+ * @return void -+ */ -+ private function setAddressAsDefault(AddressInterface $address): void -+ { -+ $address->setIsDefaultBilling(true); -+ } -+} -diff --git a/app/code/Magento/Customer/Controller/Adminhtml/Address/DefaultShippingAddress.php b/app/code/Magento/Customer/Controller/Adminhtml/Address/DefaultShippingAddress.php -new file mode 100644 -index 00000000000..81928ae2d28 ---- /dev/null -+++ b/app/code/Magento/Customer/Controller/Adminhtml/Address/DefaultShippingAddress.php -@@ -0,0 +1,109 @@ -+<?php -+declare(strict_types=1); -+/** -+ * -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+namespace Magento\Customer\Controller\Adminhtml\Address; -+ -+use Magento\Backend\App\Action; -+use Magento\Customer\Api\Data\AddressInterface; -+use Magento\Customer\Api\AddressRepositoryInterface; -+use Magento\Framework\App\Action\HttpPostActionInterface; -+use Magento\Framework\Controller\Result\Json; -+use Magento\Framework\Controller\Result\JsonFactory; -+use Psr\Log\LoggerInterface; -+ -+/** -+ * Class to process set default shipping address action -+ */ -+class DefaultShippingAddress extends Action implements HttpPostActionInterface -+{ -+ /** -+ * Authorization level of a basic admin session -+ * -+ * @see _isAllowed() -+ */ -+ public const ADMIN_RESOURCE = 'Magento_Customer::manage'; -+ -+ /** -+ * @var AddressRepositoryInterface -+ */ -+ private $addressRepository; -+ -+ /** -+ * @var LoggerInterface -+ */ -+ private $logger; -+ -+ /** -+ * @var JsonFactory -+ */ -+ private $resultJsonFactory; -+ -+ /** -+ * @param Action\Context $context -+ * @param AddressRepositoryInterface $addressRepository -+ * @param LoggerInterface $logger -+ * @param JsonFactory $resultJsonFactory -+ */ -+ public function __construct( -+ Action\Context $context, -+ AddressRepositoryInterface $addressRepository, -+ LoggerInterface $logger, -+ JsonFactory $resultJsonFactory -+ ) { -+ parent::__construct($context); -+ $this->addressRepository = $addressRepository; -+ $this->logger = $logger; -+ $this->resultJsonFactory = $resultJsonFactory; -+ } -+ -+ /** -+ * Execute action to set customer default shipping address -+ * -+ * @return Json -+ */ -+ public function execute(): Json -+ { -+ $customerId = $this->getRequest()->getParam('parent_id', false); -+ $addressId = $this->getRequest()->getParam('id', false); -+ $error = true; -+ $message = __('There is no address id in setting default shipping address.'); -+ -+ if ($addressId) { -+ try { -+ $address = $this->addressRepository->getById($addressId)->setCustomerId($customerId); -+ $this->setAddressAsDefault($address); -+ $this->addressRepository->save($address); -+ $message = __('Default shipping address has been changed.'); -+ $error = false; -+ } catch (\Exception $e) { -+ $message = __('We can\'t change default shipping address right now.'); -+ $this->logger->critical($e); -+ } -+ } -+ -+ $resultJson = $this->resultJsonFactory->create(); -+ $resultJson->setData( -+ [ -+ 'message' => $message, -+ 'error' => $error, -+ ] -+ ); -+ -+ return $resultJson; -+ } -+ -+ /** -+ * Set address as default shipping address -+ * -+ * @param AddressInterface $address -+ * @return void -+ */ -+ private function setAddressAsDefault(AddressInterface $address): void -+ { -+ $address->setIsDefaultShipping(true); -+ } -+} -diff --git a/app/code/Magento/Customer/Controller/Adminhtml/Address/Delete.php b/app/code/Magento/Customer/Controller/Adminhtml/Address/Delete.php -new file mode 100644 -index 00000000000..711cd2473cd ---- /dev/null -+++ b/app/code/Magento/Customer/Controller/Adminhtml/Address/Delete.php -@@ -0,0 +1,95 @@ -+<?php -+declare(strict_types=1); -+/** -+ * -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+namespace Magento\Customer\Controller\Adminhtml\Address; -+ -+use Magento\Backend\App\Action; -+use Magento\Framework\App\Action\HttpPostActionInterface; -+use Magento\Framework\Controller\Result\Json; -+use Magento\Framework\Controller\Result\JsonFactory; -+use Magento\Customer\Api\AddressRepositoryInterface; -+use Psr\Log\LoggerInterface; -+ -+/** -+ * Button for deletion of customer address in admin -+ */ -+class Delete extends Action implements HttpPostActionInterface -+{ -+ /** -+ * Authorization level of a basic admin session -+ * -+ * @see _isAllowed() -+ */ -+ public const ADMIN_RESOURCE = 'Magento_Customer::manage'; -+ -+ /** -+ * @var AddressRepositoryInterface -+ */ -+ private $addressRepository; -+ -+ /** -+ * @var JsonFactory -+ */ -+ private $resultJsonFactory; -+ -+ /** -+ * @var LoggerInterface -+ */ -+ private $logger; -+ -+ /** -+ * @param Action\Context $context -+ * @param AddressRepositoryInterface $addressRepository -+ * @param JsonFactory $resultJsonFactory -+ * @param LoggerInterface $logger -+ */ -+ public function __construct( -+ Action\Context $context, -+ AddressRepositoryInterface $addressRepository, -+ JsonFactory $resultJsonFactory, -+ LoggerInterface $logger -+ ) { -+ parent::__construct($context); -+ $this->addressRepository = $addressRepository; -+ $this->resultJsonFactory = $resultJsonFactory; -+ $this->logger = $logger; -+ } -+ -+ /** -+ * Delete customer address action -+ * -+ * @return Json -+ * @throws \Magento\Framework\Exception\LocalizedException -+ */ -+ public function execute(): Json -+ { -+ $customerId = $this->getRequest()->getParam('parent_id', false); -+ $addressId = $this->getRequest()->getParam('id', false); -+ $error = false; -+ $message = ''; -+ if ($addressId && $this->addressRepository->getById($addressId)->getCustomerId() === $customerId) { -+ try { -+ $this->addressRepository->deleteById($addressId); -+ $message = __('You deleted the address.'); -+ } catch (\Exception $e) { -+ $error = true; -+ $message = __('We can\'t delete the address right now.'); -+ $this->logger->critical($e); -+ } -+ } -+ -+ $resultJson = $this->resultJsonFactory->create(); -+ $resultJson->setData( -+ [ -+ 'message' => $message, -+ 'error' => $error, -+ ] -+ ); -+ -+ return $resultJson; -+ } -+} -diff --git a/app/code/Magento/Customer/Controller/Adminhtml/Address/MassDelete.php b/app/code/Magento/Customer/Controller/Adminhtml/Address/MassDelete.php -new file mode 100644 -index 00000000000..cd319d662a0 ---- /dev/null -+++ b/app/code/Magento/Customer/Controller/Adminhtml/Address/MassDelete.php -@@ -0,0 +1,131 @@ -+<?php -+declare(strict_types=1); -+/** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+namespace Magento\Customer\Controller\Adminhtml\Address; -+ -+use Magento\Backend\App\Action; -+use Magento\Framework\App\Action\HttpPostActionInterface; -+use Magento\Framework\Controller\Result\Json; -+use Magento\Backend\App\Action\Context; -+use Magento\Framework\Controller\Result\JsonFactory; -+use Magento\Framework\Exception\LocalizedException; -+use Magento\Framework\Exception\NoSuchEntityException; -+use Magento\Ui\Component\MassAction\Filter; -+use Magento\Customer\Model\ResourceModel\Address\CollectionFactory; -+use Magento\Customer\Api\AddressRepositoryInterface; -+use Psr\Log\LoggerInterface; -+ -+/** -+ * Class to delete selected customer addresses through massaction -+ */ -+class MassDelete extends Action implements HttpPostActionInterface -+{ -+ /** -+ * Authorization level of a basic admin session -+ * -+ * @see MassDelete::_isAllowed() -+ */ -+ const ADMIN_RESOURCE = 'Magento_Customer::manage'; -+ -+ /** -+ * @var Filter -+ */ -+ private $filter; -+ -+ /** -+ * @var CollectionFactory -+ */ -+ private $collectionFactory; -+ -+ /** -+ * @var AddressRepositoryInterface -+ */ -+ private $addressRepository; -+ -+ /** -+ * @var LoggerInterface -+ */ -+ private $logger; -+ -+ /** -+ * @var JsonFactory -+ */ -+ private $resultJsonFactory; -+ -+ /** -+ * @param Context $context -+ * @param Filter $filter -+ * @param CollectionFactory $collectionFactory -+ * @param AddressRepositoryInterface $addressRepository -+ * @param LoggerInterface $logger -+ * @param JsonFactory $resultJsonFactory -+ */ -+ public function __construct( -+ Context $context, -+ Filter $filter, -+ CollectionFactory $collectionFactory, -+ AddressRepositoryInterface $addressRepository, -+ LoggerInterface $logger, -+ JsonFactory $resultJsonFactory -+ ) { -+ $this->filter = $filter; -+ $this->collectionFactory = $collectionFactory; -+ $this->addressRepository = $addressRepository; -+ $this->logger = $logger; -+ $this->resultJsonFactory = $resultJsonFactory; -+ parent::__construct($context); -+ } -+ -+ /** -+ * Delete specified customer addresses using grid massaction -+ * -+ * @return Json -+ * @throws LocalizedException -+ */ -+ public function execute(): Json -+ { -+ $customerData = $this->_session->getData('customer_data'); -+ /** @var \Magento\Customer\Model\ResourceModel\Address\Collection $collection */ -+ $collection = $this->filter->getCollection($this->collectionFactory->create()); -+ $error = false; -+ -+ try { -+ if ($customerData && $customerData['customer_id']) { -+ $collection->addFieldToFilter('parent_id', $customerData['customer_id']); -+ } else { -+ throw new \Exception(); -+ } -+ $collectionSize = $collection->getSize(); -+ /** @var \Magento\Customer\Model\Address $address */ -+ foreach ($collection as $address) { -+ $this->addressRepository->deleteById($address->getId()); -+ } -+ $message = __('A total of %1 record(s) have been deleted.', $collectionSize); -+ } catch (NoSuchEntityException $e) { -+ $message = __('There is no such address entity to delete.'); -+ $error = true; -+ $this->logger->critical($e); -+ } catch (LocalizedException $e) { -+ $message = __($e->getMessage()); -+ $error = true; -+ $this->logger->critical($e); -+ } catch (\Exception $e) { -+ $message = __('We can\'t mass delete the addresses right now.'); -+ $error = true; -+ $this->logger->critical($e); -+ } -+ -+ $resultJson = $this->resultJsonFactory->create(); -+ $resultJson->setData( -+ [ -+ 'message' => $message, -+ 'error' => $error, -+ ] -+ ); -+ -+ return $resultJson; -+ } -+} -diff --git a/app/code/Magento/Customer/Controller/Adminhtml/Address/Save.php b/app/code/Magento/Customer/Controller/Adminhtml/Address/Save.php -new file mode 100644 -index 00000000000..e4daea6f59f ---- /dev/null -+++ b/app/code/Magento/Customer/Controller/Adminhtml/Address/Save.php -@@ -0,0 +1,174 @@ -+<?php -+declare(strict_types=1); -+/** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+namespace Magento\Customer\Controller\Adminhtml\Address; -+ -+use Magento\Backend\App\Action; -+use Magento\Framework\App\Action\HttpPostActionInterface; -+use Magento\Framework\Controller\Result\Json; -+use Magento\Framework\Controller\Result\JsonFactory; -+use Magento\Framework\Exception\LocalizedException; -+use Magento\Framework\Exception\NoSuchEntityException; -+use Psr\Log\LoggerInterface; -+ -+/** -+ * Class for saving of customer address -+ * @SuppressWarnings(PHPMD.CouplingBetweenObjects) -+ */ -+class Save extends Action implements HttpPostActionInterface -+{ -+ /** -+ * Authorization level of a basic admin session -+ * -+ * @see _isAllowed() -+ */ -+ public const ADMIN_RESOURCE = 'Magento_Customer::manage'; -+ -+ /** -+ * @var \Magento\Customer\Api\AddressRepositoryInterface -+ */ -+ private $addressRepository; -+ -+ /** -+ * @var \Magento\Customer\Model\Metadata\FormFactory -+ */ -+ private $formFactory; -+ -+ /** -+ * @var \Magento\Customer\Api\CustomerRepositoryInterface -+ */ -+ private $customerRepository; -+ -+ /** -+ * @var \Magento\Framework\Api\DataObjectHelper -+ */ -+ private $dataObjectHelper; -+ -+ /** -+ * @var \Magento\Customer\Api\Data\AddressInterfaceFactory -+ */ -+ private $addressDataFactory; -+ -+ /** -+ * @var LoggerInterface -+ */ -+ private $logger; -+ -+ /** -+ * @var JsonFactory -+ */ -+ private $resultJsonFactory; -+ -+ /** -+ * @param Action\Context $context -+ * @param \Magento\Customer\Api\AddressRepositoryInterface $addressRepository -+ * @param \Magento\Customer\Model\Metadata\FormFactory $formFactory -+ * @param \Magento\Customer\Api\CustomerRepositoryInterface $customerRepository -+ * @param \Magento\Framework\Api\DataObjectHelper $dataObjectHelper -+ * @param \Magento\Customer\Api\Data\AddressInterfaceFactory $addressDataFactory -+ * @param LoggerInterface $logger -+ * @param JsonFactory $resultJsonFactory -+ */ -+ public function __construct( -+ Action\Context $context, -+ \Magento\Customer\Api\AddressRepositoryInterface $addressRepository, -+ \Magento\Customer\Model\Metadata\FormFactory $formFactory, -+ \Magento\Customer\Api\CustomerRepositoryInterface $customerRepository, -+ \Magento\Framework\Api\DataObjectHelper $dataObjectHelper, -+ \Magento\Customer\Api\Data\AddressInterfaceFactory $addressDataFactory, -+ LoggerInterface $logger, -+ JsonFactory $resultJsonFactory -+ ) { -+ parent::__construct($context); -+ $this->addressRepository = $addressRepository; -+ $this->formFactory = $formFactory; -+ $this->customerRepository = $customerRepository; -+ $this->dataObjectHelper = $dataObjectHelper; -+ $this->addressDataFactory = $addressDataFactory; -+ $this->logger = $logger; -+ $this->resultJsonFactory = $resultJsonFactory; -+ } -+ -+ /** -+ * Save customer address action -+ * -+ * @return Json -+ */ -+ public function execute(): Json -+ { -+ $customerId = $this->getRequest()->getParam('parent_id', false); -+ $addressId = $this->getRequest()->getParam('entity_id', false); -+ -+ $error = false; -+ try { -+ /** @var \Magento\Customer\Api\Data\CustomerInterface $customer */ -+ $customer = $this->customerRepository->getById($customerId); -+ -+ $addressForm = $this->formFactory->create( -+ 'customer_address', -+ 'adminhtml_customer_address', -+ [], -+ false, -+ false -+ ); -+ $addressData = $addressForm->extractData($this->getRequest()); -+ $addressData = $addressForm->compactData($addressData); -+ -+ $addressData['region'] = [ -+ 'region' => $addressData['region'] ?? null, -+ 'region_id' => $addressData['region_id'] ?? null, -+ ]; -+ $addressToSave = $this->addressDataFactory->create(); -+ $this->dataObjectHelper->populateWithArray( -+ $addressToSave, -+ $addressData, -+ \Magento\Customer\Api\Data\AddressInterface::class -+ ); -+ $addressToSave->setCustomerId($customer->getId()); -+ $addressToSave->setIsDefaultBilling( -+ (bool)$this->getRequest()->getParam('default_billing', false) -+ ); -+ $addressToSave->setIsDefaultShipping( -+ (bool)$this->getRequest()->getParam('default_shipping', false) -+ ); -+ if ($addressId) { -+ $addressToSave->setId($addressId); -+ $message = __('Customer address has been updated.'); -+ } else { -+ $addressToSave->setId(null); -+ $message = __('New customer address has been added.'); -+ } -+ $savedAddress = $this->addressRepository->save($addressToSave); -+ $addressId = $savedAddress->getId(); -+ } catch (NoSuchEntityException $e) { -+ $this->logger->critical($e); -+ $error = true; -+ $message = __('There is no customer with such id.'); -+ } catch (LocalizedException $e) { -+ $error = true; -+ $message = __($e->getMessage()); -+ $this->logger->critical($e); -+ } catch (\Exception $e) { -+ $error = true; -+ $message = __('We can\'t change customer address right now.'); -+ $this->logger->critical($e); -+ } -+ -+ $addressId = empty($addressId) ? null : $addressId; -+ $resultJson = $this->resultJsonFactory->create(); -+ $resultJson->setData( -+ [ -+ 'message' => $message, -+ 'error' => $error, -+ 'data' => [ -+ 'entity_id' => $addressId -+ ] -+ ] -+ ); -+ -+ return $resultJson; -+ } -+} -diff --git a/app/code/Magento/Customer/Controller/Adminhtml/Address/Validate.php b/app/code/Magento/Customer/Controller/Adminhtml/Address/Validate.php -new file mode 100644 -index 00000000000..16a40fd9016 ---- /dev/null -+++ b/app/code/Magento/Customer/Controller/Adminhtml/Address/Validate.php -@@ -0,0 +1,99 @@ -+<?php -+declare(strict_types=1); -+/** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+namespace Magento\Customer\Controller\Adminhtml\Address; -+ -+use Magento\Backend\App\Action; -+use Magento\Framework\App\Action\HttpGetActionInterface; -+use Magento\Framework\App\Action\HttpPostActionInterface as HttpPostActionInterface; -+use Magento\Framework\Controller\Result\Json; -+use Magento\Framework\DataObject; -+ -+/** -+ * Class for validation of customer address form on admin. -+ */ -+class Validate extends Action implements HttpPostActionInterface, HttpGetActionInterface -+{ -+ /** -+ * Authorization level of a basic admin session -+ * -+ * @see _isAllowed() -+ */ -+ public const ADMIN_RESOURCE = 'Magento_Customer::manage'; -+ -+ /** -+ * @var \Magento\Framework\Controller\Result\JsonFactory -+ */ -+ private $resultJsonFactory; -+ -+ /** -+ * @var \Magento\Customer\Model\Metadata\FormFactory -+ */ -+ private $formFactory; -+ -+ /** -+ * @param Action\Context $context -+ * @param \Magento\Framework\Controller\Result\JsonFactory $resultJsonFactory -+ * @param \Magento\Customer\Model\Metadata\FormFactory $formFactory -+ */ -+ public function __construct( -+ Action\Context $context, -+ \Magento\Framework\Controller\Result\JsonFactory $resultJsonFactory, -+ \Magento\Customer\Model\Metadata\FormFactory $formFactory -+ ) { -+ parent::__construct($context); -+ $this->resultJsonFactory = $resultJsonFactory; -+ $this->formFactory = $formFactory; -+ } -+ -+ /** -+ * AJAX customer address validation action -+ * -+ * @return Json -+ */ -+ public function execute(): Json -+ { -+ /** @var \Magento\Framework\DataObject $response */ -+ $response = new \Magento\Framework\DataObject(); -+ $response->setError(false); -+ -+ /** @var \Magento\Framework\DataObject $validatedResponse */ -+ $validatedResponse = $this->validateCustomerAddress($response); -+ $resultJson = $this->resultJsonFactory->create(); -+ if ($validatedResponse->getError()) { -+ $validatedResponse->setError(true); -+ $validatedResponse->setMessages($response->getMessages()); -+ } -+ -+ $resultJson->setData($validatedResponse); -+ -+ return $resultJson; -+ } -+ -+ /** -+ * Customer address validation. -+ * -+ * @param DataObject $response -+ * @return \Magento\Framework\DataObject -+ */ -+ private function validateCustomerAddress(DataObject $response): DataObject -+ { -+ $addressForm = $this->formFactory->create('customer_address', 'adminhtml_customer_address'); -+ $formData = $addressForm->extractData($this->getRequest()); -+ -+ $errors = $addressForm->validateData($formData); -+ if ($errors !== true) { -+ $messages = $response->hasMessages() ? $response->getMessages() : []; -+ foreach ($errors as $error) { -+ $messages[] = $error; -+ } -+ $response->setMessages($messages); -+ $response->setError(true); -+ } -+ -+ return $response; -+ } -+} -diff --git a/app/code/Magento/Customer/Controller/Adminhtml/Customer/InvalidateToken.php b/app/code/Magento/Customer/Controller/Adminhtml/Customer/InvalidateToken.php -index 7337d005a73..b69410ecbfc 100644 ---- a/app/code/Magento/Customer/Controller/Adminhtml/Customer/InvalidateToken.php -+++ b/app/code/Magento/Customer/Controller/Adminhtml/Customer/InvalidateToken.php -@@ -7,6 +7,7 @@ - - namespace Magento\Customer\Controller\Adminhtml\Customer; - -+use Magento\Framework\App\Action\HttpGetActionInterface; - use Magento\Integration\Api\CustomerTokenServiceInterface; - use Magento\Customer\Api\AccountManagementInterface; - use Magento\Customer\Api\AddressRepositoryInterface; -@@ -25,8 +26,15 @@ use Magento\Framework\Api\DataObjectHelper; - * @SuppressWarnings(PHPMD.TooManyFields) - * @SuppressWarnings(PHPMD.NumberOfChildren) - */ --class InvalidateToken extends \Magento\Customer\Controller\Adminhtml\Index -+class InvalidateToken extends \Magento\Customer\Controller\Adminhtml\Index implements HttpGetActionInterface - { -+ /** -+ * Authorization level of a basic admin session -+ * -+ * @see _isAllowed() -+ */ -+ const ADMIN_RESOURCE = 'Magento_Customer::invalidate_tokens'; -+ - /** - * @var CustomerTokenServiceInterface - */ -diff --git a/app/code/Magento/Customer/Controller/Adminhtml/File/Address/Upload.php b/app/code/Magento/Customer/Controller/Adminhtml/File/Address/Upload.php -index 506eac32302..e9034c80503 100644 ---- a/app/code/Magento/Customer/Controller/Adminhtml/File/Address/Upload.php -+++ b/app/code/Magento/Customer/Controller/Adminhtml/File/Address/Upload.php -@@ -10,11 +10,16 @@ use Magento\Backend\App\Action\Context; - use Magento\Customer\Api\AddressMetadataInterface; - use Magento\Customer\Model\FileUploader; - use Magento\Customer\Model\FileUploaderFactory; -+use Magento\Framework\App\Action\HttpGetActionInterface; -+use Magento\Framework\App\Action\HttpPostActionInterface; - use Magento\Framework\Controller\ResultFactory; - use Magento\Framework\Exception\LocalizedException; - use Psr\Log\LoggerInterface; - --class Upload extends Action -+/** -+ * Uploads files for customer address -+ */ -+class Upload extends Action implements HttpGetActionInterface, HttpPostActionInterface - { - /** - * Authorization level of a basic admin session -@@ -38,21 +43,29 @@ class Upload extends Action - */ - private $logger; - -+ /** -+ * @var string -+ */ -+ private $scope; -+ - /** - * @param Context $context - * @param FileUploaderFactory $fileUploaderFactory - * @param AddressMetadataInterface $addressMetadataService - * @param LoggerInterface $logger -+ * @param string $scope - */ - public function __construct( - Context $context, - FileUploaderFactory $fileUploaderFactory, - AddressMetadataInterface $addressMetadataService, -- LoggerInterface $logger -+ LoggerInterface $logger, -+ string $scope = 'address' - ) { - $this->fileUploaderFactory = $fileUploaderFactory; - $this->addressMetadataService = $addressMetadataService; - $this->logger = $logger; -+ $this->scope = $scope; - parent::__construct($context); - } - -@@ -69,14 +82,14 @@ class Upload extends Action - // Must be executed before any operations with $_FILES! - $this->convertFilesArray(); - -- $attributeCode = key($_FILES['address']['name']); -+ $attributeCode = key($_FILES[$this->scope]['name']); - $attributeMetadata = $this->addressMetadataService->getAttributeMetadata($attributeCode); - - /** @var FileUploader $fileUploader */ - $fileUploader = $this->fileUploaderFactory->create([ - 'attributeMetadata' => $attributeMetadata, - 'entityTypeCode' => AddressMetadataInterface::ENTITY_TYPE_ADDRESS, -- 'scope' => 'address', -+ 'scope' => $this->scope, - ]); - - $errors = $fileUploader->validate(); -@@ -114,14 +127,11 @@ class Upload extends Action - */ - private function convertFilesArray() - { -- foreach ($_FILES['address'] as $itemKey => $item) { -- foreach ($item as $value) { -- if (is_array($value)) { -- $_FILES['address'][$itemKey] = [ -- key($value) => current($value), -- ]; -- } -+ foreach ($_FILES as $itemKey => $item) { -+ foreach ($item as $fieldName => $value) { -+ $_FILES[$this->scope][$fieldName] = [$itemKey => $value]; - } -+ unset($_FILES[$itemKey]); - } - } - } -diff --git a/app/code/Magento/Customer/Controller/Adminhtml/Group/Delete.php b/app/code/Magento/Customer/Controller/Adminhtml/Group/Delete.php -index 571ef57702b..ab32ea08a44 100644 ---- a/app/code/Magento/Customer/Controller/Adminhtml/Group/Delete.php -+++ b/app/code/Magento/Customer/Controller/Adminhtml/Group/Delete.php -@@ -6,9 +6,10 @@ - */ - namespace Magento\Customer\Controller\Adminhtml\Group; - -+use Magento\Framework\App\Action\HttpPostActionInterface; - use Magento\Framework\Exception\NoSuchEntityException; - --class Delete extends \Magento\Customer\Controller\Adminhtml\Group -+class Delete extends \Magento\Customer\Controller\Adminhtml\Group implements HttpPostActionInterface - { - /** - * Delete customer group. -diff --git a/app/code/Magento/Customer/Controller/Adminhtml/Group/Edit.php b/app/code/Magento/Customer/Controller/Adminhtml/Group/Edit.php -index 9da132078c9..221d01a112e 100644 ---- a/app/code/Magento/Customer/Controller/Adminhtml/Group/Edit.php -+++ b/app/code/Magento/Customer/Controller/Adminhtml/Group/Edit.php -@@ -6,7 +6,9 @@ - */ - namespace Magento\Customer\Controller\Adminhtml\Group; - --class Edit extends \Magento\Customer\Controller\Adminhtml\Group -+use Magento\Framework\App\Action\HttpGetActionInterface as HttpGetActionInterface; -+ -+class Edit extends \Magento\Customer\Controller\Adminhtml\Group implements HttpGetActionInterface - { - /** - * Edit customer group action. Forward to new action. -diff --git a/app/code/Magento/Customer/Controller/Adminhtml/Group/Index.php b/app/code/Magento/Customer/Controller/Adminhtml/Group/Index.php -index 1246b6a6d0b..6da79d2c40f 100644 ---- a/app/code/Magento/Customer/Controller/Adminhtml/Group/Index.php -+++ b/app/code/Magento/Customer/Controller/Adminhtml/Group/Index.php -@@ -6,7 +6,9 @@ - */ - namespace Magento\Customer\Controller\Adminhtml\Group; - --class Index extends \Magento\Customer\Controller\Adminhtml\Group -+use Magento\Framework\App\Action\HttpGetActionInterface as HttpGetActionInterface; -+ -+class Index extends \Magento\Customer\Controller\Adminhtml\Group implements HttpGetActionInterface - { - /** - * Customer groups list. -diff --git a/app/code/Magento/Customer/Controller/Adminhtml/Group/NewAction.php b/app/code/Magento/Customer/Controller/Adminhtml/Group/NewAction.php -index 2c230cd0b3f..a5c832bf0f1 100644 ---- a/app/code/Magento/Customer/Controller/Adminhtml/Group/NewAction.php -+++ b/app/code/Magento/Customer/Controller/Adminhtml/Group/NewAction.php -@@ -6,9 +6,10 @@ - */ - namespace Magento\Customer\Controller\Adminhtml\Group; - -+use Magento\Framework\App\Action\HttpGetActionInterface as HttpGetActionInterface; - use Magento\Customer\Controller\RegistryConstants; - --class NewAction extends \Magento\Customer\Controller\Adminhtml\Group -+class NewAction extends \Magento\Customer\Controller\Adminhtml\Group implements HttpGetActionInterface - { - /** - * Initialize current group and set it in the registry. -diff --git a/app/code/Magento/Customer/Controller/Adminhtml/Group/Save.php b/app/code/Magento/Customer/Controller/Adminhtml/Group/Save.php -index 936d9cdbc17..7549315f9ff 100644 ---- a/app/code/Magento/Customer/Controller/Adminhtml/Group/Save.php -+++ b/app/code/Magento/Customer/Controller/Adminhtml/Group/Save.php -@@ -6,11 +6,12 @@ - */ - namespace Magento\Customer\Controller\Adminhtml\Group; - -+use Magento\Framework\App\Action\HttpPostActionInterface as HttpPostActionInterface; - use Magento\Customer\Api\Data\GroupInterfaceFactory; - use Magento\Customer\Api\Data\GroupInterface; - use Magento\Customer\Api\GroupRepositoryInterface; - --class Save extends \Magento\Customer\Controller\Adminhtml\Group -+class Save extends \Magento\Customer\Controller\Adminhtml\Group implements HttpPostActionInterface - { - /** - * @var \Magento\Framework\Reflection\DataObjectProcessor -diff --git a/app/code/Magento/Customer/Controller/Adminhtml/Index/Delete.php b/app/code/Magento/Customer/Controller/Adminhtml/Index/Delete.php -index 7a981b82b7e..ab39ca09816 100644 ---- a/app/code/Magento/Customer/Controller/Adminhtml/Index/Delete.php -+++ b/app/code/Magento/Customer/Controller/Adminhtml/Index/Delete.php -@@ -5,10 +5,21 @@ - */ - namespace Magento\Customer\Controller\Adminhtml\Index; - -+use Magento\Framework\App\Action\HttpPostActionInterface as HttpPostActionInterface; - use Magento\Framework\Controller\ResultFactory; - --class Delete extends \Magento\Customer\Controller\Adminhtml\Index -+/** -+ * Delete customer action. -+ */ -+class Delete extends \Magento\Customer\Controller\Adminhtml\Index implements HttpPostActionInterface - { -+ /** -+ * Authorization level of a basic admin session -+ * -+ * @see _isAllowed() -+ */ -+ const ADMIN_RESOURCE = 'Magento_Customer::delete'; -+ - /** - * Delete customer action - * -diff --git a/app/code/Magento/Customer/Controller/Adminhtml/Index/Edit.php b/app/code/Magento/Customer/Controller/Adminhtml/Index/Edit.php -index 90417c1ad54..25b4ddd4e17 100644 ---- a/app/code/Magento/Customer/Controller/Adminhtml/Index/Edit.php -+++ b/app/code/Magento/Customer/Controller/Adminhtml/Index/Edit.php -@@ -5,10 +5,11 @@ - */ - namespace Magento\Customer\Controller\Adminhtml\Index; - -+use Magento\Framework\App\Action\HttpGetActionInterface as HttpGetActionInterface; - use Magento\Customer\Api\Data\CustomerInterface; - use Magento\Framework\Exception\NoSuchEntityException; - --class Edit extends \Magento\Customer\Controller\Adminhtml\Index -+class Edit extends \Magento\Customer\Controller\Adminhtml\Index implements HttpGetActionInterface - { - /** - * Customer edit action -diff --git a/app/code/Magento/Customer/Controller/Adminhtml/Index/Index.php b/app/code/Magento/Customer/Controller/Adminhtml/Index/Index.php -index 861ac93b262..b1986c8b6f0 100644 ---- a/app/code/Magento/Customer/Controller/Adminhtml/Index/Index.php -+++ b/app/code/Magento/Customer/Controller/Adminhtml/Index/Index.php -@@ -5,7 +5,9 @@ - */ - namespace Magento\Customer\Controller\Adminhtml\Index; - --class Index extends \Magento\Customer\Controller\Adminhtml\Index -+use Magento\Framework\App\Action\HttpGetActionInterface as HttpGetActionInterface; -+ -+class Index extends \Magento\Customer\Controller\Adminhtml\Index implements HttpGetActionInterface - { - /** - * Customers list action -diff --git a/app/code/Magento/Customer/Controller/Adminhtml/Index/InlineEdit.php b/app/code/Magento/Customer/Controller/Adminhtml/Index/InlineEdit.php -index 2d0ee3ae13d..7220de03568 100644 ---- a/app/code/Magento/Customer/Controller/Adminhtml/Index/InlineEdit.php -+++ b/app/code/Magento/Customer/Controller/Adminhtml/Index/InlineEdit.php -@@ -6,16 +6,22 @@ - namespace Magento\Customer\Controller\Adminhtml\Index; - - use Magento\Backend\App\Action; -+use Magento\Customer\Api\CustomerRepositoryInterface; -+use Magento\Customer\Api\Data\CustomerInterface; -+use Magento\Customer\Model\AddressRegistry; - use Magento\Customer\Model\EmailNotificationInterface; --use Magento\Customer\Test\Block\Form\Login; - use Magento\Customer\Ui\Component\Listing\AttributeRepository; --use Magento\Customer\Api\Data\CustomerInterface; --use Magento\Customer\Api\CustomerRepositoryInterface; -+use Magento\Framework\App\Action\HttpPostActionInterface; -+use Magento\Framework\Exception\NoSuchEntityException; -+use Magento\Framework\Message\MessageInterface; -+use Magento\Framework\App\ObjectManager; - - /** -+ * Customer inline edit action -+ * - * @SuppressWarnings(PHPMD.CouplingBetweenObjects) - */ --class InlineEdit extends \Magento\Backend\App\Action -+class InlineEdit extends \Magento\Backend\App\Action implements HttpPostActionInterface - { - /** - * Authorization level of a basic admin session -@@ -59,6 +65,11 @@ class InlineEdit extends \Magento\Backend\App\Action - */ - private $emailNotification; - -+ /** -+ * @var AddressRegistry -+ */ -+ private $addressRegistry; -+ - /** - * @param Action\Context $context - * @param CustomerRepositoryInterface $customerRepository -@@ -66,6 +77,7 @@ class InlineEdit extends \Magento\Backend\App\Action - * @param \Magento\Customer\Model\Customer\Mapper $customerMapper - * @param \Magento\Framework\Api\DataObjectHelper $dataObjectHelper - * @param \Psr\Log\LoggerInterface $logger -+ * @param AddressRegistry|null $addressRegistry - */ - public function __construct( - Action\Context $context, -@@ -73,13 +85,15 @@ class InlineEdit extends \Magento\Backend\App\Action - \Magento\Framework\Controller\Result\JsonFactory $resultJsonFactory, - \Magento\Customer\Model\Customer\Mapper $customerMapper, - \Magento\Framework\Api\DataObjectHelper $dataObjectHelper, -- \Psr\Log\LoggerInterface $logger -+ \Psr\Log\LoggerInterface $logger, -+ AddressRegistry $addressRegistry = null - ) { - $this->customerRepository = $customerRepository; - $this->resultJsonFactory = $resultJsonFactory; - $this->customerMapper = $customerMapper; - $this->dataObjectHelper = $dataObjectHelper; - $this->logger = $logger; -+ $this->addressRegistry = $addressRegistry ?: ObjectManager::getInstance()->get(AddressRegistry::class); - parent::__construct($context); - } - -@@ -101,7 +115,11 @@ class InlineEdit extends \Magento\Backend\App\Action - } - - /** -+ * Inline edit action execute -+ * - * @return \Magento\Framework\Controller\Result\Json -+ * @throws \Magento\Framework\Exception\LocalizedException -+ * @throws \Magento\Framework\Exception\NoSuchEntityException - */ - public function execute() - { -@@ -139,7 +157,7 @@ class InlineEdit extends \Magento\Backend\App\Action - * Receive entity(customer|customer_address) data from request - * - * @param array $data -- * @param null $isCustomerData -+ * @param mixed $isCustomerData - * @return array - */ - protected function getData(array $data, $isCustomerData = null) -@@ -212,6 +230,8 @@ class InlineEdit extends \Magento\Backend\App\Action - protected function saveCustomer(CustomerInterface $customer) - { - try { -+ // No need to validate customer address during inline edit action -+ $this->disableAddressValidation($customer); - $this->customerRepository->save($customer); - } catch (\Magento\Framework\Exception\InputException $e) { - $this->getMessageManager()->addError($this->getErrorWithCustomerId($e->getMessage())); -@@ -249,7 +269,7 @@ class InlineEdit extends \Magento\Backend\App\Action - protected function getErrorMessages() - { - $messages = []; -- foreach ($this->getMessageManager()->getMessages()->getItems() as $error) { -+ foreach ($this->getMessageManager()->getMessages()->getErrors() as $error) { - $messages[] = $error->getText(); - } - return $messages; -@@ -262,7 +282,7 @@ class InlineEdit extends \Magento\Backend\App\Action - */ - protected function isErrorExists() - { -- return (bool)$this->getMessageManager()->getMessages(true)->getCount(); -+ return (bool)$this->getMessageManager()->getMessages(true)->getCountByType(MessageInterface::TYPE_ERROR); - } - - /** -@@ -297,4 +317,18 @@ class InlineEdit extends \Magento\Backend\App\Action - { - return '[Customer ID: ' . $this->getCustomer()->getId() . '] ' . __($errorText); - } -+ -+ /** -+ * Disable Customer Address Validation -+ * -+ * @param CustomerInterface $customer -+ * @throws NoSuchEntityException -+ */ -+ private function disableAddressValidation($customer) -+ { -+ foreach ($customer->getAddresses() as $address) { -+ $addressModel = $this->addressRegistry->retrieve($address->getId()); -+ $addressModel->setShouldIgnoreValidation(true); -+ } -+ } - } -diff --git a/app/code/Magento/Customer/Controller/Adminhtml/Index/MassAssignGroup.php b/app/code/Magento/Customer/Controller/Adminhtml/Index/MassAssignGroup.php -index 762b872b97b..5a9c52bf9b1 100644 ---- a/app/code/Magento/Customer/Controller/Adminhtml/Index/MassAssignGroup.php -+++ b/app/code/Magento/Customer/Controller/Adminhtml/Index/MassAssignGroup.php -@@ -5,6 +5,8 @@ - */ - namespace Magento\Customer\Controller\Adminhtml\Index; - -+use Magento\Customer\Model\Customer; -+use Magento\Framework\App\Action\HttpPostActionInterface as HttpPostActionInterface; - use Magento\Backend\App\Action\Context; - use Magento\Customer\Model\ResourceModel\Customer\CollectionFactory; - use Magento\Eav\Model\Entity\Collection\AbstractCollection; -@@ -15,7 +17,7 @@ use Magento\Framework\Controller\ResultFactory; - /** - * Class MassAssignGroup - */ --class MassAssignGroup extends AbstractMassAction -+class MassAssignGroup extends AbstractMassAction implements HttpPostActionInterface - { - /** - * @var CustomerRepositoryInterface -@@ -51,6 +53,8 @@ class MassAssignGroup extends AbstractMassAction - // Verify customer exists - $customer = $this->customerRepository->getById($customerId); - $customer->setGroupId($this->getRequest()->getParam('group')); -+ // No need to validate customer and customer address during assigning customer to the group -+ $this->setIgnoreValidationFlag($customer); - $this->customerRepository->save($customer); - $customersUpdated++; - } -@@ -64,4 +68,15 @@ class MassAssignGroup extends AbstractMassAction - - return $resultRedirect; - } -+ -+ /** -+ * Set ignore_validation_flag to skip unnecessary address and customer validation -+ * -+ * @param Customer $customer -+ * @return void -+ */ -+ private function setIgnoreValidationFlag($customer) -+ { -+ $customer->setData('ignore_validation_flag', true); -+ } - } -diff --git a/app/code/Magento/Customer/Controller/Adminhtml/Index/MassDelete.php b/app/code/Magento/Customer/Controller/Adminhtml/Index/MassDelete.php -index 453585c881a..edaeea6a15e 100644 ---- a/app/code/Magento/Customer/Controller/Adminhtml/Index/MassDelete.php -+++ b/app/code/Magento/Customer/Controller/Adminhtml/Index/MassDelete.php -@@ -5,6 +5,7 @@ - */ - namespace Magento\Customer\Controller\Adminhtml\Index; - -+use Magento\Framework\App\Action\HttpPostActionInterface as HttpPostActionInterface; - use Magento\Backend\App\Action\Context; - use Magento\Customer\Model\ResourceModel\Customer\CollectionFactory; - use Magento\Eav\Model\Entity\Collection\AbstractCollection; -@@ -15,8 +16,15 @@ use Magento\Framework\Controller\ResultFactory; - /** - * Class MassDelete - */ --class MassDelete extends AbstractMassAction -+class MassDelete extends AbstractMassAction implements HttpPostActionInterface - { -+ /** -+ * Authorization level of a basic admin session -+ * -+ * @see _isAllowed() -+ */ -+ const ADMIN_RESOURCE = 'Magento_Customer::delete'; -+ - /** - * @var CustomerRepositoryInterface - */ -@@ -39,8 +47,7 @@ class MassDelete extends AbstractMassAction - } - - /** -- * @param AbstractCollection $collection -- * @return \Magento\Backend\Model\View\Result\Redirect -+ * @inheritdoc - */ - protected function massAction(AbstractCollection $collection) - { -diff --git a/app/code/Magento/Customer/Controller/Adminhtml/Index/NewAction.php b/app/code/Magento/Customer/Controller/Adminhtml/Index/NewAction.php -index e6cf2aa234e..19a16f01acf 100644 ---- a/app/code/Magento/Customer/Controller/Adminhtml/Index/NewAction.php -+++ b/app/code/Magento/Customer/Controller/Adminhtml/Index/NewAction.php -@@ -5,7 +5,9 @@ - */ - namespace Magento\Customer\Controller\Adminhtml\Index; - --class NewAction extends \Magento\Customer\Controller\Adminhtml\Index -+use Magento\Framework\App\Action\HttpGetActionInterface as HttpGetActionInterface; -+ -+class NewAction extends \Magento\Customer\Controller\Adminhtml\Index implements HttpGetActionInterface - { - /** - * Create new customer action -diff --git a/app/code/Magento/Customer/Controller/Adminhtml/Index/ResetPassword.php b/app/code/Magento/Customer/Controller/Adminhtml/Index/ResetPassword.php -index 37c8ed5a252..1e4fa91cbf8 100644 ---- a/app/code/Magento/Customer/Controller/Adminhtml/Index/ResetPassword.php -+++ b/app/code/Magento/Customer/Controller/Adminhtml/Index/ResetPassword.php -@@ -5,11 +5,24 @@ - */ - namespace Magento\Customer\Controller\Adminhtml\Index; - -+use Magento\Framework\App\Action\HttpGetActionInterface; - use Magento\Framework\Exception\NoSuchEntityException; - use Magento\Framework\Exception\SecurityViolationException; - --class ResetPassword extends \Magento\Customer\Controller\Adminhtml\Index -+/** -+ * Reset password controller -+ * -+ * @package Magento\Customer\Controller\Adminhtml\Index -+ */ -+class ResetPassword extends \Magento\Customer\Controller\Adminhtml\Index implements HttpGetActionInterface - { -+ /** -+ * Authorization level of a basic admin session -+ * -+ * @see _isAllowed() -+ */ -+ const ADMIN_RESOURCE = 'Magento_Customer::reset_password'; -+ - /** - * Reset password handler - * -diff --git a/app/code/Magento/Customer/Controller/Adminhtml/Index/Save.php b/app/code/Magento/Customer/Controller/Adminhtml/Index/Save.php -index 12732f81f78..38ed688a835 100644 ---- a/app/code/Magento/Customer/Controller/Adminhtml/Index/Save.php -+++ b/app/code/Magento/Customer/Controller/Adminhtml/Index/Save.php -@@ -5,6 +5,16 @@ - */ - namespace Magento\Customer\Controller\Adminhtml\Index; - -+use Magento\Customer\Api\AccountManagementInterface; -+use Magento\Customer\Api\AddressRepositoryInterface; -+use Magento\Customer\Api\CustomerRepositoryInterface; -+use Magento\Customer\Model\Address\Mapper; -+use Magento\Customer\Model\AddressRegistry; -+use Magento\Framework\Api\DataObjectHelper; -+use Magento\Customer\Api\Data\AddressInterfaceFactory; -+use Magento\Customer\Api\Data\CustomerInterfaceFactory; -+use Magento\Framework\DataObjectFactory as ObjectFactory; -+use Magento\Framework\App\Action\HttpPostActionInterface as HttpPostActionInterface; - use Magento\Customer\Api\AddressMetadataInterface; - use Magento\Customer\Api\CustomerMetadataInterface; - use Magento\Customer\Api\Data\CustomerInterface; -@@ -12,17 +22,115 @@ use Magento\Customer\Controller\RegistryConstants; - use Magento\Customer\Model\EmailNotificationInterface; - use Magento\Customer\Model\Metadata\Form; - use Magento\Framework\Exception\LocalizedException; -+use Magento\Framework\Exception\NoSuchEntityException; -+use Magento\Framework\App\ObjectManager; - - /** -+ * Save customer action. -+ * - * @SuppressWarnings(PHPMD.CouplingBetweenObjects) - */ --class Save extends \Magento\Customer\Controller\Adminhtml\Index -+class Save extends \Magento\Customer\Controller\Adminhtml\Index implements HttpPostActionInterface - { - /** - * @var EmailNotificationInterface - */ - private $emailNotification; - -+ /** -+ * @var AddressRegistry -+ */ -+ private $addressRegistry; -+ -+ /** -+ * Constructor -+ * -+ * @param \Magento\Backend\App\Action\Context $context -+ * @param \Magento\Framework\Registry $coreRegistry -+ * @param \Magento\Framework\App\Response\Http\FileFactory $fileFactory -+ * @param \Magento\Customer\Model\CustomerFactory $customerFactory -+ * @param \Magento\Customer\Model\AddressFactory $addressFactory -+ * @param \Magento\Customer\Model\Metadata\FormFactory $formFactory -+ * @param \Magento\Newsletter\Model\SubscriberFactory $subscriberFactory -+ * @param \Magento\Customer\Helper\View $viewHelper -+ * @param \Magento\Framework\Math\Random $random -+ * @param CustomerRepositoryInterface $customerRepository -+ * @param \Magento\Framework\Api\ExtensibleDataObjectConverter $extensibleDataObjectConverter -+ * @param Mapper $addressMapper -+ * @param AccountManagementInterface $customerAccountManagement -+ * @param AddressRepositoryInterface $addressRepository -+ * @param CustomerInterfaceFactory $customerDataFactory -+ * @param AddressInterfaceFactory $addressDataFactory -+ * @param \Magento\Customer\Model\Customer\Mapper $customerMapper -+ * @param \Magento\Framework\Reflection\DataObjectProcessor $dataObjectProcessor -+ * @param DataObjectHelper $dataObjectHelper -+ * @param ObjectFactory $objectFactory -+ * @param \Magento\Framework\View\LayoutFactory $layoutFactory -+ * @param \Magento\Framework\View\Result\LayoutFactory $resultLayoutFactory -+ * @param \Magento\Framework\View\Result\PageFactory $resultPageFactory -+ * @param \Magento\Backend\Model\View\Result\ForwardFactory $resultForwardFactory -+ * @param \Magento\Framework\Controller\Result\JsonFactory $resultJsonFactory -+ * @param AddressRegistry|null $addressRegistry -+ * @SuppressWarnings(PHPMD.ExcessiveParameterList) -+ */ -+ public function __construct( -+ \Magento\Backend\App\Action\Context $context, -+ \Magento\Framework\Registry $coreRegistry, -+ \Magento\Framework\App\Response\Http\FileFactory $fileFactory, -+ \Magento\Customer\Model\CustomerFactory $customerFactory, -+ \Magento\Customer\Model\AddressFactory $addressFactory, -+ \Magento\Customer\Model\Metadata\FormFactory $formFactory, -+ \Magento\Newsletter\Model\SubscriberFactory $subscriberFactory, -+ \Magento\Customer\Helper\View $viewHelper, -+ \Magento\Framework\Math\Random $random, -+ CustomerRepositoryInterface $customerRepository, -+ \Magento\Framework\Api\ExtensibleDataObjectConverter $extensibleDataObjectConverter, -+ Mapper $addressMapper, -+ AccountManagementInterface $customerAccountManagement, -+ AddressRepositoryInterface $addressRepository, -+ CustomerInterfaceFactory $customerDataFactory, -+ AddressInterfaceFactory $addressDataFactory, -+ \Magento\Customer\Model\Customer\Mapper $customerMapper, -+ \Magento\Framework\Reflection\DataObjectProcessor $dataObjectProcessor, -+ DataObjectHelper $dataObjectHelper, -+ ObjectFactory $objectFactory, -+ \Magento\Framework\View\LayoutFactory $layoutFactory, -+ \Magento\Framework\View\Result\LayoutFactory $resultLayoutFactory, -+ \Magento\Framework\View\Result\PageFactory $resultPageFactory, -+ \Magento\Backend\Model\View\Result\ForwardFactory $resultForwardFactory, -+ \Magento\Framework\Controller\Result\JsonFactory $resultJsonFactory, -+ AddressRegistry $addressRegistry = null -+ ) { -+ parent::__construct( -+ $context, -+ $coreRegistry, -+ $fileFactory, -+ $customerFactory, -+ $addressFactory, -+ $formFactory, -+ $subscriberFactory, -+ $viewHelper, -+ $random, -+ $customerRepository, -+ $extensibleDataObjectConverter, -+ $addressMapper, -+ $customerAccountManagement, -+ $addressRepository, -+ $customerDataFactory, -+ $addressDataFactory, -+ $customerMapper, -+ $dataObjectProcessor, -+ $dataObjectHelper, -+ $objectFactory, -+ $layoutFactory, -+ $resultLayoutFactory, -+ $resultPageFactory, -+ $resultForwardFactory, -+ $resultJsonFactory -+ ); -+ $this->addressRegistry = $addressRegistry ?: ObjectManager::getInstance()->get(AddressRegistry::class); -+ } -+ - /** - * Reformat customer account data to be compatible with customer service interface - * -@@ -107,6 +215,7 @@ class Save extends \Magento\Customer\Controller\Adminhtml\Index - /** - * Saves default_billing and default_shipping flags for customer address - * -+ * @deprecated must be removed because addresses are save separately for now - * @param array $addressIdList - * @param array $extractedCustomerData - * @return array -@@ -149,6 +258,7 @@ class Save extends \Magento\Customer\Controller\Adminhtml\Index - /** - * Reformat customer addresses data to be compatible with customer service interface - * -+ * @deprecated addresses are saved separately for now - * @param array $extractedCustomerData - * @return array - */ -@@ -179,18 +289,17 @@ class Save extends \Magento\Customer\Controller\Adminhtml\Index - public function execute() - { - $returnToEdit = false; -- $originalRequestData = $this->getRequest()->getPostValue(); -- - $customerId = $this->getCurrentCustomerId(); - -- if ($originalRequestData) { -+ if ($this->getRequest()->getPostValue()) { - try { - // optional fields might be set in request for future processing by observers in other modules - $customerData = $this->_extractCustomerData(); -- $addressesData = $this->_extractCustomerAddressData($customerData); - - if ($customerId) { - $currentCustomer = $this->_customerRepository->getById($customerId); -+ // No need to validate customer address while editing customer profile -+ $this->disableAddressValidation($currentCustomer); - $customerData = array_merge( - $this->customerMapper->toFlatArray($currentCustomer), - $customerData -@@ -205,28 +314,12 @@ class Save extends \Magento\Customer\Controller\Adminhtml\Index - $customerData, - \Magento\Customer\Api\Data\CustomerInterface::class - ); -- $addresses = []; -- foreach ($addressesData as $addressData) { -- $region = isset($addressData['region']) ? $addressData['region'] : null; -- $regionId = isset($addressData['region_id']) ? $addressData['region_id'] : null; -- $addressData['region'] = [ -- 'region' => $region, -- 'region_id' => $regionId, -- ]; -- $addressDataObject = $this->addressDataFactory->create(); -- $this->dataObjectHelper->populateWithArray( -- $addressDataObject, -- $addressData, -- \Magento\Customer\Api\Data\AddressInterface::class -- ); -- $addresses[] = $addressDataObject; -- } - - $this->_eventManager->dispatch( - 'adminhtml_customer_prepare_save', - ['customer' => $customer, 'request' => $this->getRequest()] - ); -- $customer->setAddresses($addresses); -+ - if (isset($customerData['sendemail_store_id'])) { - $customer->setStoreId($customerData['sendemail_store_id']); - } -@@ -269,7 +362,7 @@ class Save extends \Magento\Customer\Controller\Adminhtml\Index - $messages = $exception->getMessage(); - } - $this->_addSessionErrorMessages($messages); -- $this->_getSession()->setCustomerFormData($originalRequestData); -+ $this->_getSession()->setCustomerFormData($this->retrieveFormattedFormData()); - $returnToEdit = true; - } catch (\Magento\Framework\Exception\AbstractAggregateException $exception) { - $errors = $exception->getErrors(); -@@ -278,18 +371,19 @@ class Save extends \Magento\Customer\Controller\Adminhtml\Index - $messages[] = $error->getMessage(); - } - $this->_addSessionErrorMessages($messages); -- $this->_getSession()->setCustomerFormData($originalRequestData); -+ $this->_getSession()->setCustomerFormData($this->retrieveFormattedFormData()); - $returnToEdit = true; - } catch (LocalizedException $exception) { - $this->_addSessionErrorMessages($exception->getMessage()); -- $this->_getSession()->setCustomerFormData($originalRequestData); -+ $this->_getSession()->setCustomerFormData($this->retrieveFormattedFormData()); - $returnToEdit = true; - } catch (\Exception $exception) { - $this->messageManager->addException($exception, __('Something went wrong while saving the customer.')); -- $this->_getSession()->setCustomerFormData($originalRequestData); -+ $this->_getSession()->setCustomerFormData($this->retrieveFormattedFormData()); - $returnToEdit = true; - } - } -+ - $resultRedirect = $this->resultRedirectFactory->create(); - if ($returnToEdit) { - if ($customerId) { -@@ -380,4 +474,43 @@ class Save extends \Magento\Customer\Controller\Adminhtml\Index - - return $customerId; - } -+ -+ /** -+ * Disable Customer Address Validation -+ * -+ * @param CustomerInterface $customer -+ * @throws NoSuchEntityException -+ */ -+ private function disableAddressValidation($customer) -+ { -+ foreach ($customer->getAddresses() as $address) { -+ $addressModel = $this->addressRegistry->retrieve($address->getId()); -+ $addressModel->setShouldIgnoreValidation(true); -+ } -+ } -+ -+ /** -+ * Retrieve formatted form data -+ * -+ * @return array -+ */ -+ private function retrieveFormattedFormData(): array -+ { -+ $originalRequestData = $this->getRequest()->getPostValue(); -+ -+ /* Customer data filtration */ -+ if (isset($originalRequestData['customer'])) { -+ $customerData = $this->_extractData( -+ 'adminhtml_customer', -+ CustomerMetadataInterface::ENTITY_TYPE_CUSTOMER, -+ [], -+ 'customer' -+ ); -+ -+ $customerData = array_intersect_key($customerData, $originalRequestData['customer']); -+ $originalRequestData['customer'] = array_merge($originalRequestData['customer'], $customerData); -+ } -+ -+ return $originalRequestData; -+ } - } -diff --git a/app/code/Magento/Customer/Controller/Adminhtml/Index/Validate.php b/app/code/Magento/Customer/Controller/Adminhtml/Index/Validate.php -index 8e098a3b7ee..d91bc7424bf 100644 ---- a/app/code/Magento/Customer/Controller/Adminhtml/Index/Validate.php -+++ b/app/code/Magento/Customer/Controller/Adminhtml/Index/Validate.php -@@ -5,10 +5,16 @@ - */ - namespace Magento\Customer\Controller\Adminhtml\Index; - -+use Magento\Framework\App\Action\HttpGetActionInterface; -+use Magento\Framework\App\Action\HttpPostActionInterface as HttpPostActionInterface; - use Magento\Customer\Api\Data\CustomerInterface; - use Magento\Framework\Message\Error; -+use Magento\Customer\Controller\Adminhtml\Index as CustomerAction; - --class Validate extends \Magento\Customer\Controller\Adminhtml\Index -+/** -+ * Class for validation of customer -+ */ -+class Validate extends CustomerAction implements HttpPostActionInterface, HttpGetActionInterface - { - /** - * Customer validation -@@ -69,40 +75,6 @@ class Validate extends \Magento\Customer\Controller\Adminhtml\Index - return $customer; - } - -- /** -- * Customer address validation. -- * -- * @param \Magento\Framework\DataObject $response -- * @return void -- */ -- protected function _validateCustomerAddress($response) -- { -- $addresses = $this->getRequest()->getPost('address'); -- if (!is_array($addresses)) { -- return; -- } -- foreach (array_keys($addresses) as $index) { -- if ($index == '_template_') { -- continue; -- } -- -- $addressForm = $this->_formFactory->create('customer_address', 'adminhtml_customer_address'); -- -- $requestScope = sprintf('address/%s', $index); -- $formData = $addressForm->extractData($this->getRequest(), $requestScope); -- -- $errors = $addressForm->validateData($formData); -- if ($errors !== true) { -- $messages = $response->hasMessages() ? $response->getMessages() : []; -- foreach ($errors as $error) { -- $messages[] = $error; -- } -- $response->setMessages($messages); -- $response->setError(1); -- } -- } -- } -- - /** - * AJAX customer validation action - * -@@ -113,10 +85,7 @@ class Validate extends \Magento\Customer\Controller\Adminhtml\Index - $response = new \Magento\Framework\DataObject(); - $response->setError(0); - -- $customer = $this->_validateCustomer($response); -- if ($customer) { -- $this->_validateCustomerAddress($response); -- } -+ $this->_validateCustomer($response); - $resultJson = $this->resultJsonFactory->create(); - if ($response->getError()) { - $response->setError(true); -diff --git a/app/code/Magento/Customer/Controller/Adminhtml/Index/Viewfile.php b/app/code/Magento/Customer/Controller/Adminhtml/Index/Viewfile.php -index 20d330354bc..02a04508622 100644 ---- a/app/code/Magento/Customer/Controller/Adminhtml/Index/Viewfile.php -+++ b/app/code/Magento/Customer/Controller/Adminhtml/Index/Viewfile.php -@@ -3,6 +3,8 @@ - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -+declare(strict_types=1); -+ - namespace Magento\Customer\Controller\Adminhtml\Index; - - use Magento\Customer\Api\AccountManagementInterface; -@@ -17,7 +19,10 @@ use Magento\Framework\App\Filesystem\DirectoryList; - use Magento\Framework\DataObjectFactory; - - /** -+ * Class Viewfile serves to show file or image by file/image name provided in request parameters. -+ * - * @SuppressWarnings(PHPMD.CouplingBetweenObjects) -+ * @SuppressWarnings(PHPMD.AllPurposeAction) - */ - class Viewfile extends \Magento\Customer\Controller\Adminhtml\Index - { -@@ -127,8 +132,6 @@ class Viewfile extends \Magento\Customer\Controller\Adminhtml\Index - * - * @return \Magento\Framework\Controller\ResultInterface|void - * @throws NotFoundException -- * -- * @SuppressWarnings(PHPMD.ExitExpression) - */ - public function execute() - { -@@ -146,6 +149,7 @@ class Viewfile extends \Magento\Customer\Controller\Adminhtml\Index - } - - if ($plain) { -+ // phpcs:ignore Magento2.Functions.DiscouragedFunction - $extension = pathinfo($path, PATHINFO_EXTENSION); - switch (strtolower($extension)) { - case 'gif': -@@ -175,6 +179,7 @@ class Viewfile extends \Magento\Customer\Controller\Adminhtml\Index - $resultRaw->setContents($directory->readFile($fileName)); - return $resultRaw; - } else { -+ // phpcs:ignore Magento2.Functions.DiscouragedFunction - $name = pathinfo($path, PATHINFO_BASENAME); - $this->_fileFactory->create( - $name, -diff --git a/app/code/Magento/Customer/Controller/Adminhtml/Online/Index.php b/app/code/Magento/Customer/Controller/Adminhtml/Online/Index.php -index 3cf9a82b8b8..0262513d029 100644 ---- a/app/code/Magento/Customer/Controller/Adminhtml/Online/Index.php -+++ b/app/code/Magento/Customer/Controller/Adminhtml/Online/Index.php -@@ -6,10 +6,11 @@ - */ - namespace Magento\Customer\Controller\Adminhtml\Online; - -+use Magento\Framework\App\Action\HttpGetActionInterface as HttpGetActionInterface; - use Magento\Backend\App\Action\Context; - use Magento\Framework\View\Result\PageFactory; - --class Index extends \Magento\Backend\App\Action -+class Index extends \Magento\Backend\App\Action implements HttpGetActionInterface - { - /** - * Authorization level of a basic admin session -diff --git a/app/code/Magento/Customer/Controller/Ajax/Login.php b/app/code/Magento/Customer/Controller/Ajax/Login.php -index 73869bc3f29..5049c83e60f 100644 ---- a/app/code/Magento/Customer/Controller/Ajax/Login.php -+++ b/app/code/Magento/Customer/Controller/Ajax/Login.php -@@ -6,6 +6,7 @@ - - namespace Magento\Customer\Controller\Ajax; - -+use Magento\Framework\App\Action\HttpPostActionInterface as HttpPostActionInterface; - use Magento\Customer\Api\AccountManagementInterface; - use Magento\Framework\Exception\EmailNotConfirmedException; - use Magento\Framework\Exception\InvalidEmailOrPasswordException; -@@ -23,12 +24,12 @@ use Magento\Framework\Stdlib\CookieManagerInterface; - * @method \Magento\Framework\App\Response\Http getResponse() - * @SuppressWarnings(PHPMD.CouplingBetweenObjects) - */ --class Login extends \Magento\Framework\App\Action\Action -+class Login extends \Magento\Framework\App\Action\Action implements HttpPostActionInterface - { - /** -- * @var \Magento\Framework\Session\Generic -+ * @var \Magento\Customer\Model\Session - */ -- protected $session; -+ protected $customerSession; - - /** - * @var AccountManagementInterface -@@ -106,7 +107,6 @@ class Login extends \Magento\Framework\App\Action\Action - - /** - * Get account redirect. -- * For release backward compatibility. - * - * @deprecated 100.0.10 - * @return AccountRedirect -@@ -132,6 +132,8 @@ class Login extends \Magento\Framework\App\Action\Action - } - - /** -+ * Initializes config dependency. -+ * - * @deprecated 100.0.10 - * @return ScopeConfigInterface - */ -@@ -144,6 +146,8 @@ class Login extends \Magento\Framework\App\Action\Action - } - - /** -+ * Sets config dependency. -+ * - * @deprecated 100.0.10 - * @param ScopeConfigInterface $value - * @return void -@@ -198,25 +202,15 @@ class Login extends \Magento\Framework\App\Action\Action - $response['redirectUrl'] = $this->_redirect->success($redirectRoute); - $this->getAccountRedirect()->clearRedirectCookie(); - } -- } catch (EmailNotConfirmedException $e) { -- $response = [ -- 'errors' => true, -- 'message' => $e->getMessage() -- ]; -- } catch (InvalidEmailOrPasswordException $e) { -- $response = [ -- 'errors' => true, -- 'message' => $e->getMessage() -- ]; - } catch (LocalizedException $e) { - $response = [ - 'errors' => true, -- 'message' => $e->getMessage() -+ 'message' => $e->getMessage(), - ]; - } catch (\Exception $e) { - $response = [ - 'errors' => true, -- 'message' => __('Invalid login or password.') -+ 'message' => __('Invalid login or password.'), - ]; - } - /** @var \Magento\Framework\Controller\Result\Json $resultJson */ -diff --git a/app/code/Magento/Customer/Controller/Ajax/Logout.php b/app/code/Magento/Customer/Controller/Ajax/Logout.php -index 0edd41e3632..c9eadacd12e 100644 ---- a/app/code/Magento/Customer/Controller/Ajax/Logout.php -+++ b/app/code/Magento/Customer/Controller/Ajax/Logout.php -@@ -7,18 +7,20 @@ - - namespace Magento\Customer\Controller\Ajax; - -+use Magento\Framework\App\Action\HttpGetActionInterface; -+ - /** - * Logout controller - * - * @method \Magento\Framework\App\RequestInterface getRequest() - * @method \Magento\Framework\App\Response\Http getResponse() - */ --class Logout extends \Magento\Framework\App\Action\Action -+class Logout extends \Magento\Framework\App\Action\Action implements HttpGetActionInterface - { - /** -- * @var \Magento\Framework\Session\Generic -+ * @var \Magento\Customer\Model\Session - */ -- protected $session; -+ protected $customerSession; - - /** - * @var \Magento\Framework\Controller\Result\JsonFactory -diff --git a/app/code/Magento/Customer/Controller/Section/Load.php b/app/code/Magento/Customer/Controller/Section/Load.php -index 7a2345c9175..37cd071b136 100644 ---- a/app/code/Magento/Customer/Controller/Section/Load.php -+++ b/app/code/Magento/Customer/Controller/Section/Load.php -@@ -5,6 +5,7 @@ - */ - namespace Magento\Customer\Controller\Section; - -+use Magento\Framework\App\Action\HttpGetActionInterface as HttpGetActionInterface; - use Magento\Customer\CustomerData\Section\Identifier; - use Magento\Customer\CustomerData\SectionPoolInterface; - use Magento\Framework\App\Action\Context; -@@ -13,7 +14,7 @@ use Magento\Framework\Controller\Result\JsonFactory; - /** - * Customer section controller - */ --class Load extends \Magento\Framework\App\Action\Action -+class Load extends \Magento\Framework\App\Action\Action implements HttpGetActionInterface - { - /** - * @var JsonFactory -@@ -58,7 +59,7 @@ class Load extends \Magento\Framework\App\Action\Action - } - - /** -- * @return \Magento\Framework\Controller\Result\Json -+ * @inheritdoc - */ - public function execute() - { -@@ -70,11 +71,11 @@ class Load extends \Magento\Framework\App\Action\Action - $sectionNames = $this->getRequest()->getParam('sections'); - $sectionNames = $sectionNames ? array_unique(\explode(',', $sectionNames)) : null; - -- $updateSectionId = $this->getRequest()->getParam('update_section_id'); -- if ('false' === $updateSectionId) { -- $updateSectionId = false; -+ $forceNewSectionTimestamp = $this->getRequest()->getParam('force_new_section_timestamp'); -+ if ('false' === $forceNewSectionTimestamp) { -+ $forceNewSectionTimestamp = false; - } -- $response = $this->sectionPool->getSectionsData($sectionNames, (bool)$updateSectionId); -+ $response = $this->sectionPool->getSectionsData($sectionNames, (bool)$forceNewSectionTimestamp); - } catch (\Exception $e) { - $resultJson->setStatusHeader( - \Zend\Http\Response::STATUS_CODE_400, -diff --git a/app/code/Magento/Customer/CustomerData/Plugin/SessionChecker.php b/app/code/Magento/Customer/CustomerData/Plugin/SessionChecker.php -index aa73e275ee0..f82a4d15ae8 100644 ---- a/app/code/Magento/Customer/CustomerData/Plugin/SessionChecker.php -+++ b/app/code/Magento/Customer/CustomerData/Plugin/SessionChecker.php -@@ -5,10 +5,13 @@ - */ - namespace Magento\Customer\CustomerData\Plugin; - --use Magento\Framework\Session\SessionManager; -+use Magento\Framework\Session\SessionManagerInterface; - use Magento\Framework\Stdlib\Cookie\CookieMetadataFactory; - use Magento\Framework\Stdlib\Cookie\PhpCookieManager; - -+/** -+ * Class SessionChecker -+ */ - class SessionChecker - { - /** -@@ -36,10 +39,12 @@ class SessionChecker - /** - * Delete frontend session cookie if customer session is expired - * -- * @param SessionManager $sessionManager -+ * @param SessionManagerInterface $sessionManager - * @return void -+ * @throws \Magento\Framework\Exception\InputException -+ * @throws \Magento\Framework\Stdlib\Cookie\FailureToSendException - */ -- public function beforeStart(SessionManager $sessionManager) -+ public function beforeStart(SessionManagerInterface $sessionManager) - { - if (!$this->cookieManager->getCookie($sessionManager->getName()) - && $this->cookieManager->getCookie('mage-cache-sessid') -diff --git a/app/code/Magento/Customer/CustomerData/Section/Identifier.php b/app/code/Magento/Customer/CustomerData/Section/Identifier.php -index 2a770925d1c..a8bc2c8abc1 100644 ---- a/app/code/Magento/Customer/CustomerData/Section/Identifier.php -+++ b/app/code/Magento/Customer/CustomerData/Section/Identifier.php -@@ -43,12 +43,12 @@ class Identifier - /** - * Init mark(identifier) for sections - * -- * @param bool $forceUpdate -+ * @param bool $forceNewTimestamp - * @return int - */ -- public function initMark($forceUpdate) -+ public function initMark($forceNewTimestamp) - { -- if ($forceUpdate) { -+ if ($forceNewTimestamp) { - $this->markId = time(); - return $this->markId; - } -@@ -67,19 +67,19 @@ class Identifier - * Mark sections with data id - * - * @param array $sectionsData -- * @param null $sectionNames -- * @param bool $updateIds -+ * @param array|null $sectionNames -+ * @param bool $forceNewTimestamp - * @return array - */ -- public function markSections(array $sectionsData, $sectionNames = null, $updateIds = false) -+ public function markSections(array $sectionsData, $sectionNames = null, $forceNewTimestamp = false) - { - if (!$sectionNames) { - $sectionNames = array_keys($sectionsData); - } -- $markId = $this->initMark($updateIds); -+ $markId = $this->initMark($forceNewTimestamp); - - foreach ($sectionNames as $name) { -- if ($updateIds || !array_key_exists(self::SECTION_KEY, $sectionsData[$name])) { -+ if ($forceNewTimestamp || !array_key_exists(self::SECTION_KEY, $sectionsData[$name])) { - $sectionsData[$name][self::SECTION_KEY] = $markId; - } - } -diff --git a/app/code/Magento/Customer/CustomerData/SectionPool.php b/app/code/Magento/Customer/CustomerData/SectionPool.php -index 0e0d7b992e3..efea1762d9d 100644 ---- a/app/code/Magento/Customer/CustomerData/SectionPool.php -+++ b/app/code/Magento/Customer/CustomerData/SectionPool.php -@@ -53,12 +53,12 @@ class SectionPool implements SectionPoolInterface - } - - /** -- * {@inheritdoc} -+ * @inheritdoc - */ -- public function getSectionsData(array $sectionNames = null, $updateIds = false) -+ public function getSectionsData(array $sectionNames = null, $forceNewTimestamp = false) - { - $sectionsData = $sectionNames ? $this->getSectionDataByNames($sectionNames) : $this->getAllSectionData(); -- $sectionsData = $this->identifier->markSections($sectionsData, $sectionNames, $updateIds); -+ $sectionsData = $this->identifier->markSections($sectionsData, $sectionNames, $forceNewTimestamp); - return $sectionsData; - } - -diff --git a/app/code/Magento/Customer/CustomerData/SectionPoolInterface.php b/app/code/Magento/Customer/CustomerData/SectionPoolInterface.php -index c308804fd0f..ad73b9722b1 100644 ---- a/app/code/Magento/Customer/CustomerData/SectionPoolInterface.php -+++ b/app/code/Magento/Customer/CustomerData/SectionPoolInterface.php -@@ -14,8 +14,8 @@ interface SectionPoolInterface - * Get section data by section names. If $sectionNames is null then return all sections data - * - * @param array $sectionNames -- * @param bool $updateIds -+ * @param bool $forceNewTimestamp - * @return array - */ -- public function getSectionsData(array $sectionNames = null, $updateIds = false); -+ public function getSectionsData(array $sectionNames = null, $forceNewTimestamp = false); - } -diff --git a/app/code/Magento/Customer/Helper/Address.php b/app/code/Magento/Customer/Helper/Address.php -index c74c62dc6d9..765c13b2877 100644 ---- a/app/code/Magento/Customer/Helper/Address.php -+++ b/app/code/Magento/Customer/Helper/Address.php -@@ -10,6 +10,8 @@ use Magento\Customer\Api\CustomerMetadataInterface; - use Magento\Customer\Api\Data\AttributeMetadataInterface; - use Magento\Directory\Model\Country\Format; - use Magento\Framework\Exception\NoSuchEntityException; -+use Magento\Framework\View\Element\BlockInterface; -+use Magento\Store\Model\ScopeInterface; - - /** - * Customer address helper -@@ -94,6 +96,8 @@ class Address extends \Magento\Framework\App\Helper\AbstractHelper - protected $_addressConfig; - - /** -+ * Address constructor. -+ * - * @param \Magento\Framework\App\Helper\Context $context - * @param \Magento\Framework\View\Element\BlockFactory $blockFactory - * @param \Magento\Store\Model\StoreManagerInterface $storeManager -@@ -127,6 +131,8 @@ class Address extends \Magento\Framework\App\Helper\AbstractHelper - } - - /** -+ * Retrieve edit url. -+ * - * @return void - */ - public function getEditUrl() -@@ -134,6 +140,8 @@ class Address extends \Magento\Framework\App\Helper\AbstractHelper - } - - /** -+ * Retrieve delete url. -+ * - * @return void - */ - public function getDeleteUrl() -@@ -141,6 +149,8 @@ class Address extends \Magento\Framework\App\Helper\AbstractHelper - } - - /** -+ * Retrieve create url. -+ * - * @return void - */ - public function getCreateUrl() -@@ -148,6 +158,8 @@ class Address extends \Magento\Framework\App\Helper\AbstractHelper - } - - /** -+ * Retrieve block renderer. -+ * - * @param string $renderer - * @return \Magento\Framework\View\Element\BlockInterface - */ -@@ -165,7 +177,9 @@ class Address extends \Magento\Framework\App\Helper\AbstractHelper - * - * @param string $key - * @param \Magento\Store\Model\Store|int|string $store -+ * - * @return string|null -+ * @throws NoSuchEntityException - */ - public function getConfig($key, $store = null) - { -@@ -174,7 +188,7 @@ class Address extends \Magento\Framework\App\Helper\AbstractHelper - if (!isset($this->_config[$websiteId])) { - $this->_config[$websiteId] = $this->scopeConfig->getValue( - 'customer/address', -- \Magento\Store\Model\ScopeInterface::SCOPE_STORE, -+ ScopeInterface::SCOPE_STORE, - $store - ); - } -@@ -185,7 +199,10 @@ class Address extends \Magento\Framework\App\Helper\AbstractHelper - * Return Number of Lines in a Street Address for store - * - * @param \Magento\Store\Model\Store|int|string $store -+ * - * @return int -+ * @throws NoSuchEntityException -+ * @throws \Magento\Framework\Exception\LocalizedException - */ - public function getStreetLines($store = null) - { -@@ -204,6 +221,8 @@ class Address extends \Magento\Framework\App\Helper\AbstractHelper - } - - /** -+ * Retrieve address format. -+ * - * @param string $code - * @return Format|string - */ -@@ -232,7 +251,9 @@ class Address extends \Magento\Framework\App\Helper\AbstractHelper - * Determine if specified address config value can be shown - * - * @param string $key -+ * - * @return bool -+ * @throws NoSuchEntityException - */ - public function canShowConfig($key) - { -@@ -243,9 +264,11 @@ class Address extends \Magento\Framework\App\Helper\AbstractHelper - * Get string with frontend validation classes for attribute - * - * @param string $attributeCode -+ * - * @return string - * - * @SuppressWarnings(PHPMD.NPathComplexity) -+ * @throws \Magento\Framework\Exception\LocalizedException - */ - public function getAttributeValidationClass($attributeCode) - { -@@ -313,9 +336,9 @@ class Address extends \Magento\Framework\App\Helper\AbstractHelper - */ - public function isVatValidationEnabled($store = null) - { -- return (bool)$this->scopeConfig->getValue( -+ return $this->scopeConfig->isSetFlag( - self::XML_PATH_VAT_VALIDATION_ENABLED, -- \Magento\Store\Model\ScopeInterface::SCOPE_STORE, -+ ScopeInterface::SCOPE_STORE, - $store - ); - } -@@ -327,9 +350,9 @@ class Address extends \Magento\Framework\App\Helper\AbstractHelper - */ - public function isDisableAutoGroupAssignDefaultValue() - { -- return (bool)$this->scopeConfig->getValue( -+ return $this->scopeConfig->isSetFlag( - self::XML_PATH_VIV_DISABLE_AUTO_ASSIGN_DEFAULT, -- \Magento\Store\Model\ScopeInterface::SCOPE_STORE -+ ScopeInterface::SCOPE_STORE - ); - } - -@@ -341,9 +364,9 @@ class Address extends \Magento\Framework\App\Helper\AbstractHelper - */ - public function hasValidateOnEachTransaction($store = null) - { -- return (bool)$this->scopeConfig->getValue( -+ return $this->scopeConfig->isSetFlag( - self::XML_PATH_VIV_ON_EACH_TRANSACTION, -- \Magento\Store\Model\ScopeInterface::SCOPE_STORE, -+ ScopeInterface::SCOPE_STORE, - $store - ); - } -@@ -358,7 +381,7 @@ class Address extends \Magento\Framework\App\Helper\AbstractHelper - { - return (string)$this->scopeConfig->getValue( - self::XML_PATH_VIV_TAX_CALCULATION_ADDRESS_TYPE, -- \Magento\Store\Model\ScopeInterface::SCOPE_STORE, -+ ScopeInterface::SCOPE_STORE, - $store - ); - } -@@ -370,9 +393,9 @@ class Address extends \Magento\Framework\App\Helper\AbstractHelper - */ - public function isVatAttributeVisible() - { -- return (bool)$this->scopeConfig->getValue( -+ return $this->scopeConfig->isSetFlag( - self::XML_PATH_VAT_FRONTEND_VISIBILITY, -- \Magento\Store\Model\ScopeInterface::SCOPE_STORE -+ ScopeInterface::SCOPE_STORE - ); - } - -@@ -380,7 +403,10 @@ class Address extends \Magento\Framework\App\Helper\AbstractHelper - * Retrieve attribute visibility - * - * @param string $code -+ * - * @return bool -+ * @throws NoSuchEntityException -+ * @throws \Magento\Framework\Exception\LocalizedException - * @since 100.2.0 - */ - public function isAttributeVisible($code) -diff --git a/app/code/Magento/Customer/Helper/Session/CurrentCustomer.php b/app/code/Magento/Customer/Helper/Session/CurrentCustomer.php -index e6082de1da4..5cd09aca9f8 100644 ---- a/app/code/Magento/Customer/Helper/Session/CurrentCustomer.php -+++ b/app/code/Magento/Customer/Helper/Session/CurrentCustomer.php -@@ -10,11 +10,12 @@ use Magento\Customer\Api\Data\CustomerInterfaceFactory; - use Magento\Customer\Model\Session as CustomerSession; - use Magento\Framework\App\RequestInterface; - use Magento\Framework\App\ViewInterface; --use Magento\Framework\Module\Manager as ModuleManager; -+use \Magento\Framework\Module\ModuleManagerInterface as ModuleManager; - use Magento\Framework\View\LayoutInterface; - - /** - * Class CurrentCustomer -+ * @SuppressWarnings(PHPMD.CookieAndSessionMisuse) - */ - class CurrentCustomer - { -@@ -44,7 +45,7 @@ class CurrentCustomer - protected $request; - - /** -- * @var \Magento\Framework\Module\Manager -+ * @var \Magento\Framework\Module\ModuleManagerInterface - */ - protected $moduleManager; - -diff --git a/app/code/Magento/Customer/Model/AccountConfirmation.php b/app/code/Magento/Customer/Model/AccountConfirmation.php -index 7d01ff0efc4..f29330af258 100644 ---- a/app/code/Magento/Customer/Model/AccountConfirmation.php -+++ b/app/code/Magento/Customer/Model/AccountConfirmation.php -@@ -10,8 +10,7 @@ use Magento\Framework\App\Config\ScopeConfigInterface; - use Magento\Framework\Registry; - - /** -- * Class AccountConfirmation. -- * Checks if email confirmation required for customer. -+ * Class AccountConfirmation. Checks if email confirmation required for customer. - */ - class AccountConfirmation - { -@@ -31,6 +30,8 @@ class AccountConfirmation - private $registry; - - /** -+ * AccountConfirmation constructor. -+ * - * @param ScopeConfigInterface $scopeConfig - * @param Registry $registry - */ -@@ -56,7 +57,7 @@ class AccountConfirmation - return false; - } - -- return (bool)$this->scopeConfig->getValue( -+ return $this->scopeConfig->isSetFlag( - self::XML_PATH_IS_CONFIRM, - ScopeInterface::SCOPE_WEBSITES, - $websiteId -diff --git a/app/code/Magento/Customer/Model/AccountManagement.php b/app/code/Magento/Customer/Model/AccountManagement.php -index 3aba7d1da89..15d98af86b7 100644 ---- a/app/code/Magento/Customer/Model/AccountManagement.php -+++ b/app/code/Magento/Customer/Model/AccountManagement.php -@@ -17,9 +17,13 @@ use Magento\Customer\Helper\View as CustomerViewHelper; - use Magento\Customer\Model\Config\Share as ConfigShare; - use Magento\Customer\Model\Customer as CustomerModel; - use Magento\Customer\Model\Customer\CredentialsValidator; -+use Magento\Customer\Model\ForgotPasswordToken\GetCustomerByToken; - use Magento\Customer\Model\Metadata\Validator; -+use Magento\Customer\Model\ResourceModel\Visitor\CollectionFactory; -+use Magento\Directory\Model\AllowedCountries; - use Magento\Eav\Model\Validator\Attribute\Backend; - use Magento\Framework\Api\ExtensibleDataObjectConverter; -+use Magento\Framework\Api\SearchCriteriaBuilder; - use Magento\Framework\App\Area; - use Magento\Framework\App\Config\ScopeConfigInterface; - use Magento\Framework\App\ObjectManager; -@@ -43,14 +47,13 @@ use Magento\Framework\Mail\Template\TransportBuilder; - use Magento\Framework\Math\Random; - use Magento\Framework\Reflection\DataObjectProcessor; - use Magento\Framework\Registry; -+use Magento\Framework\Session\SaveHandlerInterface; -+use Magento\Framework\Session\SessionManagerInterface; - use Magento\Framework\Stdlib\DateTime; - use Magento\Framework\Stdlib\StringUtils as StringHelper; - use Magento\Store\Model\ScopeInterface; - use Magento\Store\Model\StoreManagerInterface; - use Psr\Log\LoggerInterface as PsrLogger; --use Magento\Framework\Session\SessionManagerInterface; --use Magento\Framework\Session\SaveHandlerInterface; --use Magento\Customer\Model\ResourceModel\Visitor\CollectionFactory; - - /** - * Handle various customer account actions -@@ -58,6 +61,7 @@ use Magento\Customer\Model\ResourceModel\Visitor\CollectionFactory; - * @SuppressWarnings(PHPMD.CouplingBetweenObjects) - * @SuppressWarnings(PHPMD.TooManyFields) - * @SuppressWarnings(PHPMD.ExcessiveClassComplexity) -+ * @SuppressWarnings(PHPMD.CookieAndSessionMisuse) - */ - class AccountManagement implements AccountManagementInterface - { -@@ -326,6 +330,26 @@ class AccountManagement implements AccountManagementInterface - */ - private $accountConfirmation; - -+ /** -+ * @var SearchCriteriaBuilder -+ */ -+ private $searchCriteriaBuilder; -+ -+ /** -+ * @var AddressRegistry -+ */ -+ private $addressRegistry; -+ -+ /** -+ * @var AllowedCountries -+ */ -+ private $allowedCountriesReader; -+ -+ /** -+ * @var GetCustomerByToken -+ */ -+ private $getByToken; -+ - /** - * @param CustomerFactory $customerFactory - * @param ManagerInterface $eventManager -@@ -356,7 +380,14 @@ class AccountManagement implements AccountManagementInterface - * @param SessionManagerInterface|null $sessionManager - * @param SaveHandlerInterface|null $saveHandler - * @param CollectionFactory|null $visitorCollectionFactory -+ * @param SearchCriteriaBuilder|null $searchCriteriaBuilder -+ * @param AddressRegistry|null $addressRegistry -+ * @param GetCustomerByToken|null $getByToken -+ * @param AllowedCountries|null $allowedCountriesReader -+ * @SuppressWarnings(PHPMD.CyclomaticComplexity) - * @SuppressWarnings(PHPMD.ExcessiveParameterList) -+ * @SuppressWarnings(PHPMD.NPathComplexity) -+ * @SuppressWarnings(PHPMD.LongVariable) - */ - public function __construct( - CustomerFactory $customerFactory, -@@ -387,7 +418,11 @@ class AccountManagement implements AccountManagementInterface - AccountConfirmation $accountConfirmation = null, - SessionManagerInterface $sessionManager = null, - SaveHandlerInterface $saveHandler = null, -- CollectionFactory $visitorCollectionFactory = null -+ CollectionFactory $visitorCollectionFactory = null, -+ SearchCriteriaBuilder $searchCriteriaBuilder = null, -+ AddressRegistry $addressRegistry = null, -+ GetCustomerByToken $getByToken = null, -+ AllowedCountries $allowedCountriesReader = null - ) { - $this->customerFactory = $customerFactory; - $this->eventManager = $eventManager; -@@ -412,17 +447,26 @@ class AccountManagement implements AccountManagementInterface - $this->customerModel = $customerModel; - $this->objectFactory = $objectFactory; - $this->extensibleDataObjectConverter = $extensibleDataObjectConverter; -+ $objectManager = ObjectManager::getInstance(); - $this->credentialsValidator = -- $credentialsValidator ?: ObjectManager::getInstance()->get(CredentialsValidator::class); -- $this->dateTimeFactory = $dateTimeFactory ?: ObjectManager::getInstance()->get(DateTimeFactory::class); -- $this->accountConfirmation = $accountConfirmation ?: ObjectManager::getInstance() -+ $credentialsValidator ?: $objectManager->get(CredentialsValidator::class); -+ $this->dateTimeFactory = $dateTimeFactory ?: $objectManager->get(DateTimeFactory::class); -+ $this->accountConfirmation = $accountConfirmation ?: $objectManager - ->get(AccountConfirmation::class); - $this->sessionManager = $sessionManager -- ?: ObjectManager::getInstance()->get(SessionManagerInterface::class); -+ ?: $objectManager->get(SessionManagerInterface::class); - $this->saveHandler = $saveHandler -- ?: ObjectManager::getInstance()->get(SaveHandlerInterface::class); -+ ?: $objectManager->get(SaveHandlerInterface::class); - $this->visitorCollectionFactory = $visitorCollectionFactory -- ?: ObjectManager::getInstance()->get(CollectionFactory::class); -+ ?: $objectManager->get(CollectionFactory::class); -+ $this->searchCriteriaBuilder = $searchCriteriaBuilder -+ ?: $objectManager->get(SearchCriteriaBuilder::class); -+ $this->addressRegistry = $addressRegistry -+ ?: $objectManager->get(AddressRegistry::class); -+ $this->getByToken = $getByToken -+ ?: $objectManager->get(GetCustomerByToken::class); -+ $this->allowedCountriesReader = $allowedCountriesReader -+ ?: $objectManager->get(AllowedCountries::class); - } - - /** -@@ -488,8 +532,11 @@ class AccountManagement implements AccountManagementInterface - * @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) - { -@@ -503,6 +550,8 @@ class AccountManagement implements AccountManagementInterface - } - - $customer->setConfirmation(null); -+ // No need to validate customer and customer address while activating customer -+ $this->setIgnoreValidationFlag($customer); - $this->customerRepository->save($customer); - $this->getEmailNotification()->newAccount( - $customer, -@@ -530,6 +579,7 @@ class AccountManagement implements AccountManagementInterface - } - try { - $this->getAuthentication()->authenticate($customerId, $password); -+ // phpcs:disable Magento2.Exceptions.ThrowCatch - } catch (InvalidEmailOrPasswordException $e) { - throw new InvalidEmailOrPasswordException(__('Invalid login or password.')); - } -@@ -568,6 +618,9 @@ class AccountManagement implements AccountManagementInterface - // 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); - -@@ -599,15 +652,17 @@ class AccountManagement implements AccountManagementInterface - */ - private function handleUnknownTemplate($template) - { -- throw new InputException(__( -- 'Invalid value of "%value" provided for the %fieldName field. Possible values: %template1 or %template2.', -- [ -- 'value' => $template, -- 'fieldName' => 'template', -- 'template1' => AccountManagement::EMAIL_REMINDER, -- 'template2' => AccountManagement::EMAIL_RESET -- ] -- )); -+ throw new InputException( -+ __( -+ 'Invalid value of "%value" provided for the %fieldName field. Possible values: %template1 or %template2.', -+ [ -+ 'value' => $template, -+ 'fieldName' => 'template', -+ 'template1' => AccountManagement::EMAIL_REMINDER, -+ 'template2' => AccountManagement::EMAIL_RESET -+ ] -+ ) -+ ); - } - - /** -@@ -615,18 +670,36 @@ class AccountManagement implements AccountManagementInterface - */ - public function resetPassword($email, $resetToken, $newPassword) - { -- $customer = $this->customerRepository->get($email); -+ if (!$email) { -+ $customer = $this->getByToken->execute($resetToken); -+ $email = $customer->getEmail(); -+ } else { -+ $customer = $this->customerRepository->get($email); -+ } -+ -+ // No need to validate customer and customer address while saving customer reset password token -+ $this->disableAddressValidation($customer); -+ $this->setIgnoreValidationFlag($customer); -+ - //Validate Token and new password strength - $this->validateResetPasswordToken($customer->getId(), $resetToken); -+ $this->credentialsValidator->checkPasswordDifferentFromEmail( -+ $email, -+ $newPassword -+ ); - $this->checkPasswordStrength($newPassword); - //Update secure data - $customerSecure = $this->customerRegistry->retrieveSecureData($customer->getId()); - $customerSecure->setRpToken(null); - $customerSecure->setRpTokenCreatedAt(null); - $customerSecure->setPasswordHash($this->createPasswordHash($newPassword)); -- $this->getAuthentication()->unlock($customer->getId()); -- $this->sessionManager->destroy(); - $this->destroyCustomerSessions($customer->getId()); -+ if ($this->sessionManager->isSessionExists()) { -+ //delete old session and move data to the new session -+ //use this instead of $this->sessionManager->regenerateId because last one doesn't delete old session -+ // phpcs:ignore Magento2.Functions.DiscouragedFunction -+ session_regenerate_id(true); -+ } - $this->customerRepository->save($customer); - - return true; -@@ -737,6 +810,8 @@ class AccountManagement implements AccountManagementInterface - - /** - * @inheritdoc -+ * -+ * @throws LocalizedException - */ - public function createAccount(CustomerInterface $customer, $password = null, $redirectUrl = '') - { -@@ -759,6 +834,8 @@ class AccountManagement implements AccountManagementInterface - - /** - * @inheritdoc -+ * -+ * @throws InputMismatchException - * @SuppressWarnings(PHPMD.CyclomaticComplexity) - * @SuppressWarnings(PHPMD.NPathComplexity) - */ -@@ -781,6 +858,7 @@ class AccountManagement implements AccountManagementInterface - if ($customer->getWebsiteId()) { - $storeId = $this->storeManager->getWebsite($customer->getWebsiteId())->getDefaultStore()->getId(); - } else { -+ $this->storeManager->setCurrentStore(null); - $storeId = $this->storeManager->getStore()->getId(); - } - $customer->setStoreId($storeId); -@@ -812,11 +890,15 @@ class AccountManagement implements AccountManagementInterface - throw new InputMismatchException( - __('A customer with the same email address already exists in an associated website.') - ); -+ // phpcs:disable Magento2.Exceptions.ThrowCatch - } catch (LocalizedException $e) { - throw $e; - } - try { - foreach ($customerAddresses as $address) { -+ if (!$this->isAddressAllowedForWebsite($address, $customer->getStoreId())) { -+ continue; -+ } - if ($address->getId()) { - $newAddress = clone $address; - $newAddress->setId(null); -@@ -828,6 +910,7 @@ class AccountManagement implements AccountManagementInterface - } - } - $this->customerRegistry->remove($customer->getId()); -+ // phpcs:disable Magento2.Exceptions.ThrowCatch - } catch (InputException $e) { - $this->customerRepository->delete($customer); - throw $e; -@@ -864,6 +947,8 @@ class AccountManagement implements AccountManagementInterface - * @param CustomerInterface $customer - * @param string $redirectUrl - * @return void -+ * @throws LocalizedException -+ * @throws NoSuchEntityException - */ - protected function sendEmailConfirmation(CustomerInterface $customer, $redirectUrl) - { -@@ -886,6 +971,8 @@ class AccountManagement implements AccountManagementInterface - - /** - * @inheritdoc -+ * -+ * @throws InvalidEmailOrPasswordException - */ - public function changePassword($email, $currentPassword, $newPassword) - { -@@ -899,6 +986,8 @@ class AccountManagement implements AccountManagementInterface - - /** - * @inheritdoc -+ * -+ * @throws InvalidEmailOrPasswordException - */ - public function changePasswordById($customerId, $currentPassword, $newPassword) - { -@@ -918,13 +1007,17 @@ class AccountManagement implements AccountManagementInterface - * @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) - { - try { - $this->getAuthentication()->authenticate($customer->getId(), $currentPassword); -+ // phpcs:disable Magento2.Exceptions.ThrowCatch - } catch (InvalidEmailOrPasswordException $e) { - throw new InvalidEmailOrPasswordException( - __("The password doesn't match this account. Verify the password and try again.") -@@ -938,6 +1031,7 @@ class AccountManagement implements AccountManagementInterface - $this->checkPasswordStrength($newPassword); - $customerSecure->setPasswordHash($this->createPasswordHash($newPassword)); - $this->destroyCustomerSessions($customer->getId()); -+ $this->disableAddressValidation($customer); - $this->customerRepository->save($customer); - - return true; -@@ -955,6 +1049,8 @@ class AccountManagement implements AccountManagementInterface - } - - /** -+ * Get EAV validator -+ * - * @return Backend - */ - private function getEavValidator() -@@ -981,6 +1077,7 @@ class AccountManagement implements AccountManagementInterface - $result = $this->getEavValidator()->isValid($customerModel); - if ($result === false && is_array($this->getEavValidator()->getMessages())) { - return $validationResults->setIsValid(false)->setMessages( -+ // phpcs:ignore Magento2.Functions.DiscouragedFunction - call_user_func_array( - 'array_merge', - $this->getEavValidator()->getMessages() -@@ -1028,15 +1125,18 @@ class AccountManagement implements AccountManagementInterface - * - * @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 If token is expired -+ * @throws InputException If token or customer id is invalid -+ * @throws InputMismatchException If token is mismatched -+ * @throws LocalizedException -+ * @throws NoSuchEntityException If customer doesn't exist -+ * @SuppressWarnings(PHPMD.LongVariable) - */ - private function validateResetPasswordToken($customerId, $resetPasswordLinkToken) - { -- if (empty($customerId) || $customerId < 0) { -+ if ($customerId !== null && $customerId <= 0) { - throw new InputException( - __( - 'Invalid value of "%value" provided for the %fieldName field.', -@@ -1044,21 +1144,25 @@ class AccountManagement implements AccountManagementInterface - ) - ); - } -+ -+ if ($customerId === null) { -+ //Looking for the customer. -+ $customerId = $this->getByToken -+ ->execute($resetPasswordLinkToken) -+ ->getId(); -+ } - if (!is_string($resetPasswordLinkToken) || empty($resetPasswordLinkToken)) { - $params = ['fieldName' => 'resetPasswordLinkToken']; - throw new InputException(__('"%fieldName" is required. Enter and try again.', $params)); - } -- - $customerSecureData = $this->customerRegistry->retrieveSecureData($customerId); - $rpToken = $customerSecureData->getRpToken(); - $rpTokenCreatedAt = $customerSecureData->getRpTokenCreatedAt(); -- - if (!Security::compareStrings($rpToken, $resetPasswordLinkToken)) { - throw new InputMismatchException(__('The password token is mismatched. Reset and try again.')); - } elseif ($this->isResetPasswordLinkTokenExpired($rpToken, $rpTokenCreatedAt)) { - throw new ExpiredException(__('The password token is expired. Reset and try again.')); - } -- - return true; - } - -@@ -1127,6 +1231,8 @@ class AccountManagement implements AccountManagementInterface - * - * @param CustomerInterface $customer - * @return $this -+ * @throws LocalizedException -+ * @throws NoSuchEntityException - * @deprecated 100.1.0 - */ - protected function sendPasswordResetNotificationEmail($customer) -@@ -1141,6 +1247,7 @@ class AccountManagement implements AccountManagementInterface - * @param int|string|null $defaultStoreId - * @return int - * @deprecated 100.1.0 -+ * @throws LocalizedException - */ - protected function getWebsiteStoreId($customer, $defaultStoreId = null) - { -@@ -1153,6 +1260,8 @@ class AccountManagement implements AccountManagementInterface - } - - /** -+ * Get template types -+ * - * @return array - * @deprecated 100.1.0 - */ -@@ -1186,6 +1295,7 @@ class AccountManagement implements AccountManagementInterface - * @param int|null $storeId - * @param string $email - * @return $this -+ * @throws MailException - * @deprecated 100.1.0 - */ - protected function sendEmailTemplate( -@@ -1206,13 +1316,20 @@ class AccountManagement implements AccountManagementInterface - } - - $transport = $this->transportBuilder->setTemplateIdentifier($templateId) -- ->setTemplateOptions(['area' => Area::AREA_FRONTEND, 'store' => $storeId]) -+ ->setTemplateOptions( -+ [ -+ 'area' => Area::AREA_FRONTEND, -+ 'store' => $storeId -+ ] -+ ) - ->setTemplateVars($templateParams) -- ->setFrom($this->scopeConfig->getValue( -- $sender, -- ScopeInterface::SCOPE_STORE, -- $storeId -- )) -+ ->setFrom( -+ $this->scopeConfig->getValue( -+ $sender, -+ ScopeInterface::SCOPE_STORE, -+ $storeId -+ ) -+ ) - ->addTo($email, $this->customerViewHelper->getCustomerName($customer)) - ->getTransport(); - -@@ -1301,6 +1418,9 @@ class AccountManagement implements AccountManagementInterface - * @param string $passwordLinkToken - * @return bool - * @throws InputException -+ * @throws InputMismatchException -+ * @throws LocalizedException -+ * @throws NoSuchEntityException - */ - public function changeResetPasswordLinkToken($customer, $passwordLinkToken) - { -@@ -1318,6 +1438,7 @@ class AccountManagement implements AccountManagementInterface - $customerSecure->setRpTokenCreatedAt( - $this->dateTimeFactory->create()->format(DateTime::DATETIME_PHP_FORMAT) - ); -+ $this->setIgnoreValidationFlag($customer); - $this->customerRepository->save($customer); - } - return true; -@@ -1328,6 +1449,8 @@ class AccountManagement implements AccountManagementInterface - * - * @param CustomerInterface $customer - * @return $this -+ * @throws LocalizedException -+ * @throws NoSuchEntityException - * @deprecated 100.1.0 - */ - public function sendPasswordReminderEmail($customer) -@@ -1355,6 +1478,8 @@ class AccountManagement implements AccountManagementInterface - * - * @param CustomerInterface $customer - * @return $this -+ * @throws LocalizedException -+ * @throws NoSuchEntityException - * @deprecated 100.1.0 - */ - public function sendPasswordResetConfirmationEmail($customer) -@@ -1399,6 +1524,7 @@ class AccountManagement implements AccountManagementInterface - * - * @param CustomerInterface $customer - * @return Data\CustomerSecure -+ * @throws NoSuchEntityException - * @deprecated 100.1.0 - */ - protected function getFullCustomerObject($customer) -@@ -1426,6 +1552,20 @@ class AccountManagement implements AccountManagementInterface - return $this->encryptor->getHash($password); - } - -+ /** -+ * Disable Customer Address Validation -+ * -+ * @param CustomerInterface $customer -+ * @throws NoSuchEntityException -+ */ -+ private function disableAddressValidation($customer) -+ { -+ foreach ($customer->getAddresses() as $address) { -+ $addressModel = $this->addressRegistry->retrieve($address->getId()); -+ $addressModel->setShouldIgnoreValidation(true); -+ } -+ } -+ - /** - * Get email notification - * -@@ -1445,6 +1585,7 @@ class AccountManagement implements AccountManagementInterface - - /** - * Destroy all active customer sessions by customer id (current session will not be destroyed). -+ * - * Customer sessions which should be deleted are collecting from the "customer_visitor" table considering - * configured session lifetime. - * -@@ -1468,9 +1609,32 @@ class AccountManagement implements AccountManagementInterface - /** @var \Magento\Customer\Model\Visitor $visitor */ - foreach ($visitorCollection->getItems() as $visitor) { - $sessionId = $visitor->getSessionId(); -- $this->sessionManager->start(); - $this->saveHandler->destroy($sessionId); -- $this->sessionManager->writeClose(); - } - } -+ -+ /** -+ * Set ignore_validation_flag for reset password flow to skip unnecessary address and customer validation -+ * -+ * @param Customer $customer -+ * @return void -+ */ -+ private function setIgnoreValidationFlag($customer) -+ { -+ $customer->setData('ignore_validation_flag', true); -+ } -+ -+ /** -+ * Check is address allowed for store -+ * -+ * @param AddressInterface $address -+ * @param int|null $storeId -+ * @return bool -+ */ -+ private function isAddressAllowedForWebsite(AddressInterface $address, $storeId): bool -+ { -+ $allowedCountries = $this->allowedCountriesReader->getAllowedCountries(ScopeInterface::SCOPE_STORE, $storeId); -+ -+ return in_array($address->getCountryId(), $allowedCountries); -+ } - } -diff --git a/app/code/Magento/Customer/Model/Address.php b/app/code/Magento/Customer/Model/Address.php -index 1dcd8516af6..ea9b103f422 100644 ---- a/app/code/Magento/Customer/Model/Address.php -+++ b/app/code/Magento/Customer/Model/Address.php -@@ -122,6 +122,8 @@ class Address extends \Magento\Customer\Model\Address\AbstractAddress - } - - /** -+ * Initialize address model -+ * - * @return void - */ - protected function _construct() -@@ -154,9 +156,6 @@ class Address extends \Magento\Customer\Model\Address\AbstractAddress - // Need to explicitly set this due to discrepancy in the keys between model and data object - $this->setIsDefaultBilling($address->isDefaultBilling()); - $this->setIsDefaultShipping($address->isDefaultShipping()); -- if (!$this->getAttributeSetId()) { -- $this->setAttributeSetId(AddressMetadataInterface::ATTRIBUTE_SET_ID_ADDRESS); -- } - $customAttributes = $address->getCustomAttributes(); - if ($customAttributes !== null) { - foreach ($customAttributes as $attribute) { -@@ -168,17 +167,21 @@ class Address extends \Magento\Customer\Model\Address\AbstractAddress - } - - /** -- * {@inheritdoc} -+ * Create address data object based on current address model. -+ * -+ * @param int|null $defaultBillingAddressId -+ * @param int|null $defaultShippingAddressId -+ * @return AddressInterface -+ * Use Api/Data/AddressInterface as a result of service operations. Don't rely on the model to provide -+ * the instance of Api/Data/AddressInterface -+ * @SuppressWarnings(PHPMD.CyclomaticComplexity) - */ - public function getDataModel($defaultBillingAddressId = null, $defaultShippingAddressId = null) - { - if ($this->getCustomerId() || $this->getParentId()) { -- if ($this->getCustomer()->getDefaultBillingAddress()) { -- $defaultBillingAddressId = $this->getCustomer()->getDefaultBillingAddress()->getId(); -- } -- if ($this->getCustomer()->getDefaultShippingAddress()) { -- $defaultShippingAddressId = $this->getCustomer()->getDefaultShippingAddress()->getId(); -- } -+ $customer = $this->getCustomer(); -+ $defaultBillingAddressId = $customer->getDefaultBilling() ?: $defaultBillingAddressId; -+ $defaultShippingAddressId = $customer->getDefaultShipping() ?: $defaultShippingAddressId; - } - return parent::getDataModel($defaultBillingAddressId, $defaultShippingAddressId); - } -@@ -261,6 +264,8 @@ class Address extends \Magento\Customer\Model\Address\AbstractAddress - } - - /** -+ * Clone address -+ * - * @return void - */ - public function __clone() -@@ -301,6 +306,8 @@ class Address extends \Magento\Customer\Model\Address\AbstractAddress - } - - /** -+ * Create customer model -+ * - * @return Customer - */ - protected function _createCustomer() -@@ -356,7 +363,11 @@ class Address extends \Magento\Customer\Model\Address\AbstractAddress - } - - /** -- * {@inheritdoc} -+ * Get a list of custom attribute codes. -+ * -+ * By default, entity can be extended only using extension attributes functionality. -+ * -+ * @return string[] - * @since 100.0.6 - */ - protected function getCustomAttributesCodes() -@@ -366,6 +377,7 @@ class Address extends \Magento\Customer\Model\Address\AbstractAddress - - /** - * Get new AttributeList dependency for application code. -+ * - * @return \Magento\Customer\Model\Address\CustomAttributeListInterface - * @deprecated 100.0.6 - */ -diff --git a/app/code/Magento/Customer/Model/Address/AbstractAddress.php b/app/code/Magento/Customer/Model/Address/AbstractAddress.php -index 6408276630c..158461b4d9c 100644 ---- a/app/code/Magento/Customer/Model/Address/AbstractAddress.php -+++ b/app/code/Magento/Customer/Model/Address/AbstractAddress.php -@@ -23,7 +23,7 @@ use Magento\Framework\Model\AbstractExtensibleModel; - * @method string getFirstname() - * @method string getMiddlename() - * @method string getLastname() -- * @method int getCountryId() -+ * @method string getCountryId() - * @method string getCity() - * @method string getTelephone() - * @method string getCompany() -@@ -222,7 +222,7 @@ class AbstractAddress extends AbstractExtensibleModel implements AddressModelInt - } - - /** -- * Get steet line by number -+ * Get street line by number - * - * @param int $number - * @return string -@@ -230,7 +230,7 @@ class AbstractAddress extends AbstractExtensibleModel implements AddressModelInt - public function getStreetLine($number) - { - $lines = $this->getStreet(); -- return isset($lines[$number - 1]) ? $lines[$number - 1] : ''; -+ return $lines[$number - 1] ?? ''; - } - - /** -@@ -271,7 +271,8 @@ class AbstractAddress extends AbstractExtensibleModel implements AddressModelInt - * Enforce format of the street field or other multiline custom attributes - * - * @param array|string $key -- * @param null $value -+ * @param array|string|null $value -+ * - * @return \Magento\Framework\DataObject - */ - public function setData($key, $value = null) -@@ -286,6 +287,7 @@ class AbstractAddress extends AbstractExtensibleModel implements AddressModelInt - - /** - * Check that address can have multiline attribute by this code (as street or some custom attribute) -+ * - * @param string $code - * @return bool - */ -@@ -373,6 +375,8 @@ class AbstractAddress extends AbstractExtensibleModel implements AddressModelInt - } - } elseif (is_string($region)) { - $this->setData('region', $region); -+ } elseif (!$regionId && is_array($region)) { -+ $this->setData('region', $regionId); - } - - return $this->getData('region'); -@@ -403,6 +407,8 @@ class AbstractAddress extends AbstractExtensibleModel implements AddressModelInt - } - - /** -+ * Return Region ID -+ * - * @return int - */ - public function getRegionId() -@@ -425,7 +431,9 @@ class AbstractAddress extends AbstractExtensibleModel implements AddressModelInt - } - - /** -- * @return int -+ * Get country -+ * -+ * @return string - */ - public function getCountry() - { -@@ -502,6 +510,8 @@ class AbstractAddress extends AbstractExtensibleModel implements AddressModelInt - } - - /** -+ * Processing object before save data -+ * - * @return $this - */ - public function beforeSave() -@@ -516,10 +526,12 @@ class AbstractAddress extends AbstractExtensibleModel implements AddressModelInt - * - * @param int|null $defaultBillingAddressId - * @param int|null $defaultShippingAddressId -+ * - * @return AddressInterface - * Use Api/Data/AddressInterface as a result of service operations. Don't rely on the model to provide - * the instance of Api/Data/AddressInterface - * @SuppressWarnings(PHPMD.CyclomaticComplexity) -+ * @throws \Magento\Framework\Exception\LocalizedException - */ - public function getDataModel($defaultBillingAddressId = null, $defaultShippingAddressId = null) - { -@@ -591,6 +603,8 @@ class AbstractAddress extends AbstractExtensibleModel implements AddressModelInt - } - - /** -+ * Create region instance -+ * - * @return \Magento\Directory\Model\Region - */ - protected function _createRegionInstance() -@@ -599,6 +613,8 @@ class AbstractAddress extends AbstractExtensibleModel implements AddressModelInt - } - - /** -+ * Create country instance -+ * - * @return \Magento\Directory\Model\Country - */ - protected function _createCountryInstance() -@@ -608,6 +624,7 @@ class AbstractAddress extends AbstractExtensibleModel implements AddressModelInt - - /** - * Unset Region from address -+ * - * @return $this - * @since 100.2.0 - */ -@@ -617,8 +634,11 @@ class AbstractAddress extends AbstractExtensibleModel implements AddressModelInt - } - - /** -+ * Is company required -+ * - * @return bool - * @since 100.2.0 -+ * @throws \Magento\Framework\Exception\LocalizedException - */ - protected function isCompanyRequired() - { -@@ -626,8 +646,11 @@ class AbstractAddress extends AbstractExtensibleModel implements AddressModelInt - } - - /** -+ * Is telephone required -+ * - * @return bool - * @since 100.2.0 -+ * @throws \Magento\Framework\Exception\LocalizedException - */ - protected function isTelephoneRequired() - { -@@ -635,8 +658,11 @@ class AbstractAddress extends AbstractExtensibleModel implements AddressModelInt - } - - /** -+ * Is fax required -+ * - * @return bool - * @since 100.2.0 -+ * @throws \Magento\Framework\Exception\LocalizedException - */ - protected function isFaxRequired() - { -diff --git a/app/code/Magento/Customer/Model/Address/AddressModelInterface.php b/app/code/Magento/Customer/Model/Address/AddressModelInterface.php -index 0af36e87755..06de3a99a83 100644 ---- a/app/code/Magento/Customer/Model/Address/AddressModelInterface.php -+++ b/app/code/Magento/Customer/Model/Address/AddressModelInterface.php -@@ -15,7 +15,7 @@ namespace Magento\Customer\Model\Address; - interface AddressModelInterface - { - /** -- * Get steet line by number -+ * Get street line by number - * - * @param int $number - * @return string -diff --git a/app/code/Magento/Customer/Model/Address/CustomAttributesProcessor.php b/app/code/Magento/Customer/Model/Address/CustomAttributesProcessor.php -new file mode 100644 -index 00000000000..d6e63e11ee4 ---- /dev/null -+++ b/app/code/Magento/Customer/Model/Address/CustomAttributesProcessor.php -@@ -0,0 +1,112 @@ -+<?php -+/** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+declare(strict_types=1); -+ -+namespace Magento\Customer\Model\Address; -+ -+use Magento\Customer\Api\AddressMetadataInterface; -+use Magento\Eav\Api\AttributeOptionManagementInterface; -+ -+/** -+ * Provides customer address data. -+ */ -+class CustomAttributesProcessor -+{ -+ /** -+ * @var AddressMetadataInterface -+ */ -+ private $addressMetadata; -+ -+ /** -+ * @var AttributeOptionManagementInterface -+ */ -+ private $attributeOptionManager; -+ -+ /** -+ * @param AddressMetadataInterface $addressMetadata -+ * @param AttributeOptionManagementInterface $attributeOptionManager -+ */ -+ public function __construct( -+ AddressMetadataInterface $addressMetadata, -+ AttributeOptionManagementInterface $attributeOptionManager -+ ) { -+ $this->addressMetadata = $addressMetadata; -+ $this->attributeOptionManager = $attributeOptionManager; -+ } -+ -+ /** -+ * Set Labels to custom Attributes -+ * -+ * @param \Magento\Framework\Api\AttributeValue[] $customAttributes -+ * @return array $customAttributes -+ * @throws \Magento\Framework\Exception\InputException -+ * @throws \Magento\Framework\Exception\StateException -+ */ -+ private function setLabelsForAttributes(array $customAttributes): array -+ { -+ if (!empty($customAttributes)) { -+ foreach ($customAttributes as $customAttributeCode => $customAttribute) { -+ $attributeOptionLabels = $this->getAttributeLabels($customAttribute, $customAttributeCode); -+ if (!empty($attributeOptionLabels)) { -+ $customAttributes[$customAttributeCode]['label'] = implode(', ', $attributeOptionLabels); -+ } -+ } -+ } -+ -+ return $customAttributes; -+ } -+ /** -+ * Get Labels by CustomAttribute and CustomAttributeCode -+ * -+ * @param array $customAttribute -+ * @param string $customAttributeCode -+ * @return array $attributeOptionLabels -+ * @throws \Magento\Framework\Exception\InputException -+ * @throws \Magento\Framework\Exception\StateException -+ */ -+ private function getAttributeLabels(array $customAttribute, string $customAttributeCode) : array -+ { -+ $attributeOptionLabels = []; -+ -+ if (!empty($customAttribute['value'])) { -+ $customAttributeValues = explode(',', $customAttribute['value']); -+ $attributeOptions = $this->attributeOptionManager->getItems( -+ \Magento\Customer\Model\Indexer\Address\AttributeProvider::ENTITY, -+ $customAttributeCode -+ ); -+ -+ if (!empty($attributeOptions)) { -+ foreach ($attributeOptions as $attributeOption) { -+ $attributeOptionValue = $attributeOption->getValue(); -+ if (\in_array($attributeOptionValue, $customAttributeValues, false)) { -+ $attributeOptionLabels[] = $attributeOption->getLabel() ?? $attributeOptionValue; -+ } -+ } -+ } -+ } -+ -+ return $attributeOptionLabels; -+ } -+ -+ /** -+ * Filter not visible on storefront custom attributes. -+ * -+ * @param array $attributes -+ * @return array -+ * @throws \Magento\Framework\Exception\LocalizedException -+ */ -+ public function filterNotVisibleAttributes(array $attributes): array -+ { -+ $attributesMetadata = $this->addressMetadata->getAllAttributesMetadata(); -+ foreach ($attributesMetadata as $attributeMetadata) { -+ if (!$attributeMetadata->isVisible()) { -+ unset($attributes[$attributeMetadata->getAttributeCode()]); -+ } -+ } -+ -+ return $this->setLabelsForAttributes($attributes); -+ } -+} -diff --git a/app/code/Magento/Customer/Model/Address/CustomerAddressDataFormatter.php b/app/code/Magento/Customer/Model/Address/CustomerAddressDataFormatter.php -new file mode 100644 -index 00000000000..9202d749204 ---- /dev/null -+++ b/app/code/Magento/Customer/Model/Address/CustomerAddressDataFormatter.php -@@ -0,0 +1,110 @@ -+<?php -+/** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+declare(strict_types=1); -+ -+namespace Magento\Customer\Model\Address; -+ -+use Magento\Customer\Api\Data\AddressInterface; -+use Magento\Customer\Model\Address\Mapper as AddressMapper; -+use Magento\Customer\Model\Address\Config as AddressConfig; -+ -+/** -+ * Provides method to format customer address data. -+ */ -+class CustomerAddressDataFormatter -+{ -+ /** -+ * @var AddressMapper -+ */ -+ private $addressMapper; -+ -+ /** -+ * @var AddressConfig -+ */ -+ private $addressConfig; -+ -+ /** -+ * @var CustomAttributesProcessor -+ */ -+ private $customAttributesProcessor; -+ -+ /** -+ * @param Mapper $addressMapper -+ * @param Config $addressConfig -+ * @param CustomAttributesProcessor $customAttributesProcessor -+ */ -+ public function __construct( -+ AddressMapper $addressMapper, -+ AddressConfig $addressConfig, -+ CustomAttributesProcessor $customAttributesProcessor -+ ) { -+ $this->addressMapper = $addressMapper; -+ $this->addressConfig = $addressConfig; -+ $this->customAttributesProcessor = $customAttributesProcessor; -+ } -+ -+ /** -+ * Prepare customer address data. -+ * -+ * @param AddressInterface $customerAddress -+ * @return array -+ * @throws \Magento\Framework\Exception\LocalizedException -+ */ -+ public function prepareAddress(AddressInterface $customerAddress) -+ { -+ $resultAddress = [ -+ 'id' => $customerAddress->getId(), -+ 'customer_id' => $customerAddress->getCustomerId(), -+ 'company' => $customerAddress->getCompany(), -+ 'prefix' => $customerAddress->getPrefix(), -+ 'firstname' => $customerAddress->getFirstname(), -+ 'lastname' => $customerAddress->getLastname(), -+ 'middlename' => $customerAddress->getMiddlename(), -+ 'suffix' => $customerAddress->getSuffix(), -+ 'street' => $customerAddress->getStreet(), -+ 'city' => $customerAddress->getCity(), -+ 'region' => [ -+ 'region' => $customerAddress->getRegion()->getRegion(), -+ 'region_code' => $customerAddress->getRegion()->getRegionCode(), -+ 'region_id' => $customerAddress->getRegion()->getRegionId(), -+ ], -+ 'region_id' => $customerAddress->getRegionId(), -+ 'postcode' => $customerAddress->getPostcode(), -+ 'country_id' => $customerAddress->getCountryId(), -+ 'telephone' => $customerAddress->getTelephone(), -+ 'fax' => $customerAddress->getFax(), -+ 'default_billing' => $customerAddress->isDefaultBilling(), -+ 'default_shipping' => $customerAddress->isDefaultShipping(), -+ 'inline' => $this->getCustomerAddressInline($customerAddress), -+ 'custom_attributes' => [], -+ 'extension_attributes' => $customerAddress->getExtensionAttributes(), -+ ]; -+ -+ if ($customerAddress->getCustomAttributes()) { -+ $customerAddress = $customerAddress->__toArray(); -+ $resultAddress['custom_attributes'] = $this->customAttributesProcessor->filterNotVisibleAttributes( -+ $customerAddress['custom_attributes'] -+ ); -+ } -+ -+ return $resultAddress; -+ } -+ -+ /** -+ * Set additional customer address data -+ * -+ * @param AddressInterface $address -+ * @return string -+ */ -+ private function getCustomerAddressInline(AddressInterface $address): string -+ { -+ $builtOutputAddressData = $this->addressMapper->toFlatArray($address); -+ return $this->addressConfig -+ ->getFormatByCode(AddressConfig::DEFAULT_ADDRESS_FORMAT) -+ ->getRenderer() -+ ->renderArray($builtOutputAddressData); -+ } -+} -diff --git a/app/code/Magento/Customer/Model/Address/CustomerAddressDataProvider.php b/app/code/Magento/Customer/Model/Address/CustomerAddressDataProvider.php -new file mode 100644 -index 00000000000..04fdd2a7f72 ---- /dev/null -+++ b/app/code/Magento/Customer/Model/Address/CustomerAddressDataProvider.php -@@ -0,0 +1,64 @@ -+<?php -+/** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+declare(strict_types=1); -+ -+namespace Magento\Customer\Model\Address; -+ -+/** -+ * Provides customer address data. -+ */ -+class CustomerAddressDataProvider -+{ -+ /** -+ * Customer addresses. -+ * -+ * @var array -+ */ -+ private $customerAddresses = []; -+ -+ /** -+ * @var CustomerAddressDataFormatter -+ */ -+ private $customerAddressDataFormatter; -+ -+ /** -+ * @param CustomerAddressDataFormatter $customerAddressDataFormatter -+ */ -+ public function __construct( -+ CustomerAddressDataFormatter $customerAddressDataFormatter -+ ) { -+ $this->customerAddressDataFormatter = $customerAddressDataFormatter; -+ } -+ -+ /** -+ * Get addresses for customer. -+ * -+ * @param \Magento\Customer\Api\Data\CustomerInterface $customer -+ * @return array -+ * @throws \Magento\Framework\Exception\LocalizedException -+ */ -+ public function getAddressDataByCustomer( -+ \Magento\Customer\Api\Data\CustomerInterface $customer -+ ): array { -+ if (!empty($this->customerAddresses)) { -+ return $this->customerAddresses; -+ } -+ -+ $customerOriginAddresses = $customer->getAddresses(); -+ if (!$customerOriginAddresses) { -+ return []; -+ } -+ -+ $customerAddresses = []; -+ foreach ($customerOriginAddresses as $address) { -+ $customerAddresses[$address->getId()] = $this->customerAddressDataFormatter->prepareAddress($address); -+ } -+ -+ $this->customerAddresses = $customerAddresses; -+ -+ return $this->customerAddresses; -+ } -+} -diff --git a/app/code/Magento/Customer/Model/Address/DataProvider.php b/app/code/Magento/Customer/Model/Address/DataProvider.php -new file mode 100644 -index 00000000000..e7f9fce0217 ---- /dev/null -+++ b/app/code/Magento/Customer/Model/Address/DataProvider.php -@@ -0,0 +1,233 @@ -+<?php -+declare(strict_types=1); -+/** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+namespace Magento\Customer\Model\Address; -+ -+use Magento\Customer\Api\CustomerRepositoryInterface; -+use Magento\Customer\Model\ResourceModel\Address\CollectionFactory; -+use Magento\Eav\Model\Config; -+use Magento\Eav\Model\Entity\Type; -+use Magento\Eav\Model\Entity\Attribute\AbstractAttribute; -+use Magento\Framework\View\Element\UiComponent\ContextInterface; -+use Magento\Customer\Model\Address; -+use Magento\Customer\Model\FileUploaderDataResolver; -+use Magento\Customer\Model\AttributeMetadataResolver; -+use Magento\Ui\Component\Form\Element\Multiline; -+ -+/** -+ * Dataprovider of customer addresses for customer address grid. -+ * -+ * @property \Magento\Customer\Model\ResourceModel\Address\Collection $collection -+ */ -+class DataProvider extends \Magento\Ui\DataProvider\AbstractDataProvider -+{ -+ /** -+ * @var CustomerRepositoryInterface -+ */ -+ private $customerRepository; -+ -+ /** -+ * @var array -+ */ -+ private $loadedData; -+ -+ /** -+ * Allow to manage attributes, even they are hidden on storefront -+ * -+ * @var bool -+ */ -+ private $allowToShowHiddenAttributes; -+ -+ /* -+ * @var ContextInterface -+ */ -+ private $context; -+ -+ /** -+ * @var array -+ */ -+ private $bannedInputTypes = ['media_image']; -+ -+ /** -+ * @var array -+ */ -+ private static $attributesToEliminate = [ -+ 'region', -+ 'vat_is_valid', -+ 'vat_request_date', -+ 'vat_request_id', -+ 'vat_request_success' -+ ]; -+ -+ /** -+ * @var FileUploaderDataResolver -+ */ -+ private $fileUploaderDataResolver; -+ -+ /** -+ * @var AttributeMetadataResolver -+ */ -+ private $attributeMetadataResolver; -+ -+ /** -+ * DataProvider constructor. -+ * @param string $name -+ * @param string $primaryFieldName -+ * @param string $requestFieldName -+ * @param CollectionFactory $addressCollectionFactory -+ * @param CustomerRepositoryInterface $customerRepository -+ * @param Config $eavConfig -+ * @param ContextInterface $context -+ * @param FileUploaderDataResolver $fileUploaderDataResolver -+ * @param AttributeMetadataResolver $attributeMetadataResolver -+ * @param array $meta -+ * @param array $data -+ * @param bool $allowToShowHiddenAttributes -+ * @throws \Magento\Framework\Exception\LocalizedException -+ * @SuppressWarnings(PHPMD.ExcessiveParameterList) -+ */ -+ public function __construct( -+ $name, -+ $primaryFieldName, -+ $requestFieldName, -+ CollectionFactory $addressCollectionFactory, -+ CustomerRepositoryInterface $customerRepository, -+ Config $eavConfig, -+ ContextInterface $context, -+ FileUploaderDataResolver $fileUploaderDataResolver, -+ AttributeMetadataResolver $attributeMetadataResolver, -+ array $meta = [], -+ array $data = [], -+ $allowToShowHiddenAttributes = true -+ ) { -+ parent::__construct($name, $primaryFieldName, $requestFieldName, $meta, $data); -+ $this->collection = $addressCollectionFactory->create(); -+ $this->collection->addAttributeToSelect('*'); -+ $this->customerRepository = $customerRepository; -+ $this->allowToShowHiddenAttributes = $allowToShowHiddenAttributes; -+ $this->context = $context; -+ $this->fileUploaderDataResolver = $fileUploaderDataResolver; -+ $this->attributeMetadataResolver = $attributeMetadataResolver; -+ $this->meta['general']['children'] = $this->getAttributesMeta( -+ $eavConfig->getEntityType('customer_address') -+ ); -+ } -+ -+ /** -+ * Get Addresses data and process customer default billing & shipping addresses -+ * -+ * @return array -+ * @throws \Magento\Framework\Exception\LocalizedException -+ * @throws \Magento\Framework\Exception\NoSuchEntityException -+ */ -+ public function getData(): array -+ { -+ if (null !== $this->loadedData) { -+ return $this->loadedData; -+ } -+ $items = $this->collection->getItems(); -+ /** @var Address $item */ -+ foreach ($items as $item) { -+ $addressId = $item->getEntityId(); -+ $item->load($addressId); -+ $this->loadedData[$addressId] = $item->getData(); -+ $customerId = $this->loadedData[$addressId]['parent_id']; -+ /** @var \Magento\Customer\Model\Customer $customer */ -+ $customer = $this->customerRepository->getById($customerId); -+ $defaultBilling = $customer->getDefaultBilling(); -+ $defaultShipping = $customer->getDefaultShipping(); -+ $this->prepareAddressData($addressId, $this->loadedData, $defaultBilling, $defaultShipping); -+ $this->fileUploaderDataResolver->overrideFileUploaderData($item, $this->loadedData[$addressId]); -+ } -+ -+ if (null === $this->loadedData) { -+ $this->loadedData[''] = $this->getDefaultData(); -+ } -+ -+ return $this->loadedData; -+ } -+ -+ /** -+ * Prepare address data -+ * -+ * @param int $addressId -+ * @param array $addresses -+ * @param string|null $defaultBilling -+ * @param string|null $defaultShipping -+ * @return void -+ */ -+ private function prepareAddressData($addressId, array &$addresses, $defaultBilling, $defaultShipping): void -+ { -+ if (null !== $defaultBilling && $addressId === $defaultBilling) { -+ $addresses[$addressId]['default_billing'] = '1'; -+ } -+ if (null !== $defaultShipping && $addressId === $defaultShipping) { -+ $addresses[$addressId]['default_shipping'] = '1'; -+ } -+ foreach ($this->meta['general']['children'] as $attributeName => $attributeMeta) { -+ if ($attributeMeta['arguments']['data']['config']['dataType'] === Multiline::NAME -+ && isset($this->loadedData[$addressId][$attributeName]) -+ && !\is_array($this->loadedData[$addressId][$attributeName]) -+ ) { -+ $this->loadedData[$addressId][$attributeName] = explode( -+ "\n", -+ $this->loadedData[$addressId][$attributeName] -+ ); -+ } -+ } -+ } -+ -+ /** -+ * Get default customer data for adding new address -+ * -+ * @throws \Magento\Framework\Exception\LocalizedException -+ * @throws \Magento\Framework\Exception\NoSuchEntityException -+ * @return array -+ */ -+ private function getDefaultData(): array -+ { -+ $parentId = $this->context->getRequestParam('parent_id'); -+ $customer = $this->customerRepository->getById($parentId); -+ $data = [ -+ 'parent_id' => $parentId, -+ 'firstname' => $customer->getFirstname(), -+ 'lastname' => $customer->getLastname() -+ ]; -+ -+ return $data; -+ } -+ -+ /** -+ * Get attributes meta -+ * -+ * @param Type $entityType -+ * @return array -+ * @throws \Magento\Framework\Exception\LocalizedException -+ */ -+ private function getAttributesMeta(Type $entityType): array -+ { -+ $meta = []; -+ $attributes = $entityType->getAttributeCollection(); -+ /* @var AbstractAttribute $attribute */ -+ foreach ($attributes as $attribute) { -+ if (\in_array($attribute->getFrontendInput(), $this->bannedInputTypes, true)) { -+ continue; -+ } -+ if (\in_array($attribute->getAttributeCode(), self::$attributesToEliminate, true)) { -+ continue; -+ } -+ -+ $meta[$attribute->getAttributeCode()] = $this->attributeMetadataResolver->getAttributesMeta( -+ $attribute, -+ $entityType, -+ $this->allowToShowHiddenAttributes -+ ); -+ } -+ $this->attributeMetadataResolver->processWebsiteMeta($meta); -+ -+ return $meta; -+ } -+} -diff --git a/app/code/Magento/Customer/Model/Address/Validator/Country.php b/app/code/Magento/Customer/Model/Address/Validator/Country.php -index d7fb51dbbd4..e0eb1cbadd8 100644 ---- a/app/code/Magento/Customer/Model/Address/Validator/Country.php -+++ b/app/code/Magento/Customer/Model/Address/Validator/Country.php -@@ -7,6 +7,9 @@ namespace Magento\Customer\Model\Address\Validator; - - use Magento\Customer\Model\Address\AbstractAddress; - use Magento\Customer\Model\Address\ValidatorInterface; -+use Magento\Directory\Helper\Data; -+use Magento\Directory\Model\AllowedCountries; -+use Magento\Framework\Escaper; - use Magento\Framework\App\ObjectManager; - use Magento\Store\Model\ScopeInterface; - -@@ -16,35 +19,35 @@ use Magento\Store\Model\ScopeInterface; - class Country implements ValidatorInterface - { - /** -- * @var \Magento\Directory\Helper\Data -+ * @var Escaper - */ -- private $directoryData; -+ private $escaper; - - /** -- * @var \Magento\Directory\Model\AllowedCountries -+ * @var Data - */ -- private $allowedCountriesReader; -+ private $directoryData; - - /** -- * @var \Magento\Customer\Model\Config\Share -+ * @var AllowedCountries - */ -- private $shareConfig; -+ private $allowedCountriesReader; - - /** -- * @param \Magento\Directory\Helper\Data $directoryData -- * @param \Magento\Directory\Model\AllowedCountries|null $allowedCountriesReader -- * @param \Magento\Customer\Model\Config\Share|null $shareConfig -+ * @param Data $directoryData -+ * @param AllowedCountries $allowedCountriesReader -+ * @param Escaper|null $escaper - */ - public function __construct( -- \Magento\Directory\Helper\Data $directoryData, -- \Magento\Directory\Model\AllowedCountries $allowedCountriesReader = null, -- \Magento\Customer\Model\Config\Share $shareConfig = null -+ Data $directoryData, -+ AllowedCountries $allowedCountriesReader, -+ Escaper $escaper = null - ) { - $this->directoryData = $directoryData; -- $this->allowedCountriesReader = $allowedCountriesReader -- ?: ObjectManager::getInstance()->get(\Magento\Directory\Model\AllowedCountries::class); -- $this->shareConfig = $shareConfig -- ?: ObjectManager::getInstance()->get(\Magento\Customer\Model\Config\Share::class); -+ $this->allowedCountriesReader = $allowedCountriesReader; -+ $this->escaper = $escaper ?? ObjectManager::getInstance()->get( -+ Escaper::class -+ ); - } - - /** -@@ -76,7 +79,7 @@ class Country implements ValidatorInterface - //Checking if such country exists. - $errors[] = __( - 'Invalid value of "%value" provided for the %fieldName field.', -- ['fieldName' => 'countryId', 'value' => htmlspecialchars($countryId)] -+ ['fieldName' => 'countryId', 'value' => $this->escaper->escapeHtml($countryId)] - ); - } - -@@ -113,7 +116,7 @@ class Country implements ValidatorInterface - //If a region is selected then checking if it exists. - $errors[] = __( - 'Invalid value of "%value" provided for the %fieldName field.', -- ['fieldName' => 'regionId', 'value' => htmlspecialchars($regionId)] -+ ['fieldName' => 'regionId', 'value' => $this->escaper->escapeHtml($regionId)] - ); - } - -@@ -128,12 +131,7 @@ class Country implements ValidatorInterface - */ - private function getWebsiteAllowedCountries(AbstractAddress $address): array - { -- $websiteId = null; -- -- if (!$this->shareConfig->isGlobalScope()) { -- $websiteId = $address->getCustomer() ? $address->getCustomer()->getWebsiteId() : null; -- } -- -- return $this->allowedCountriesReader->getAllowedCountries(ScopeInterface::SCOPE_WEBSITE, $websiteId); -+ $storeId = $address->getData('store_id'); -+ return $this->allowedCountriesReader->getAllowedCountries(ScopeInterface::SCOPE_STORE, $storeId); - } - } -diff --git a/app/code/Magento/Customer/Model/Attribute.php b/app/code/Magento/Customer/Model/Attribute.php -index 98a97872f15..ae714f99308 100644 ---- a/app/code/Magento/Customer/Model/Attribute.php -+++ b/app/code/Magento/Customer/Model/Attribute.php -@@ -202,9 +202,14 @@ class Attribute extends \Magento\Eav\Model\Attribute - - /** - * @inheritdoc -+ * -+ * @SuppressWarnings(PHPMD.SerializationAware) -+ * @deprecated Do not use PHP serialization. - */ - public function __sleep() - { -+ trigger_error('Using PHP serialization is deprecated', E_USER_DEPRECATED); -+ - $this->unsetData('entity_type'); - return array_diff( - parent::__sleep(), -@@ -214,9 +219,14 @@ class Attribute extends \Magento\Eav\Model\Attribute - - /** - * @inheritdoc -+ * -+ * @SuppressWarnings(PHPMD.SerializationAware) -+ * @deprecated Do not use PHP serialization. - */ - public function __wakeup() - { -+ trigger_error('Using PHP serialization is deprecated', E_USER_DEPRECATED); -+ - parent::__wakeup(); - $objectManager = \Magento\Framework\App\ObjectManager::getInstance(); - $this->indexerRegistry = $objectManager->get(\Magento\Framework\Indexer\IndexerRegistry::class); -diff --git a/app/code/Magento/Customer/Model/Attribute/Data/Postcode.php b/app/code/Magento/Customer/Model/Attribute/Data/Postcode.php -index 380b8a4d344..b1602e8ca19 100644 ---- a/app/code/Magento/Customer/Model/Attribute/Data/Postcode.php -+++ b/app/code/Magento/Customer/Model/Attribute/Data/Postcode.php -@@ -3,6 +3,7 @@ - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -+ - namespace Magento\Customer\Model\Attribute\Data; - - use Magento\Directory\Helper\Data as DirectoryHelper; -@@ -13,7 +14,8 @@ use Psr\Log\LoggerInterface as PsrLogger; - use Magento\Framework\Stdlib\DateTime\TimezoneInterface as MagentoTimezone; - - /** -- * Customer Address Postal/Zip Code Attribute Data Model -+ * Customer Address Postal/Zip Code Attribute Data Model. -+ * - * This Data Model Has to Be Set Up in additional EAV attribute table - */ - class Postcode extends \Magento\Eav\Model\Attribute\Data\AbstractData -@@ -40,7 +42,8 @@ class Postcode extends \Magento\Eav\Model\Attribute\Data\AbstractData - } - - /** -- * Validate postal/zip code -+ * Validate postal/zip code. -+ * - * Return true and skip validation if country zip code is optional - * - * @param array|string $value -@@ -104,7 +107,7 @@ class Postcode extends \Magento\Eav\Model\Attribute\Data\AbstractData - } - - /** -- * Return formated attribute value from entity model -+ * Return formatted attribute value from entity model - * - * @param string $format - * @return string|array -diff --git a/app/code/Magento/Customer/Model/AttributeMetadataResolver.php b/app/code/Magento/Customer/Model/AttributeMetadataResolver.php -new file mode 100644 -index 00000000000..979730eb1c9 ---- /dev/null -+++ b/app/code/Magento/Customer/Model/AttributeMetadataResolver.php -@@ -0,0 +1,206 @@ -+<?php -+declare(strict_types=1); -+/** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+namespace Magento\Customer\Model; -+ -+use Magento\Customer\Model\ResourceModel\Address\Attribute\Source\CountryWithWebsites; -+use Magento\Eav\Model\Entity\Attribute\AbstractAttribute; -+use Magento\Customer\Api\Data\AddressInterface; -+use Magento\Ui\DataProvider\EavValidationRules; -+use Magento\Ui\Component\Form\Field; -+use Magento\Eav\Model\Entity\Type; -+use Magento\Eav\Api\Data\AttributeInterface; -+use Magento\Framework\View\Element\UiComponent\ContextInterface; -+use Magento\Customer\Api\Data\CustomerInterface; -+use Magento\Customer\Model\Config\Share as ShareConfig; -+ -+/** -+ * Class to build meta data of the customer or customer address attribute -+ * @SuppressWarnings(PHPMD.CouplingBetweenObjects) -+ */ -+class AttributeMetadataResolver -+{ -+ /** -+ * EAV attribute properties to fetch from meta storage -+ * @var array -+ */ -+ private static $metaProperties = [ -+ 'dataType' => 'frontend_input', -+ 'visible' => 'is_visible', -+ 'required' => 'is_required', -+ 'label' => 'frontend_label', -+ 'sortOrder' => 'sort_order', -+ 'notice' => 'note', -+ 'default' => 'default_value', -+ 'size' => 'multiline_count', -+ ]; -+ -+ /** -+ * Form element mapping -+ * -+ * @var array -+ */ -+ private static $formElement = [ -+ 'text' => 'input', -+ 'hidden' => 'input', -+ 'boolean' => 'checkbox', -+ ]; -+ -+ /** -+ * @var CountryWithWebsites -+ */ -+ private $countryWithWebsiteSource; -+ -+ /** -+ * @var EavValidationRules -+ */ -+ private $eavValidationRules; -+ -+ /** -+ * @var FileUploaderDataResolver -+ */ -+ private $fileUploaderDataResolver; -+ -+ /** -+ * @var ContextInterface -+ */ -+ private $context; -+ -+ /** -+ * @var ShareConfig -+ */ -+ private $shareConfig; -+ -+ /** -+ * @param CountryWithWebsites $countryWithWebsiteSource -+ * @param EavValidationRules $eavValidationRules -+ * @param \Magento\Customer\Model\FileUploaderDataResolver $fileUploaderDataResolver -+ * @param ContextInterface $context -+ * @param ShareConfig $shareConfig -+ */ -+ public function __construct( -+ CountryWithWebsites $countryWithWebsiteSource, -+ EavValidationRules $eavValidationRules, -+ fileUploaderDataResolver $fileUploaderDataResolver, -+ ContextInterface $context, -+ ShareConfig $shareConfig -+ ) { -+ $this->countryWithWebsiteSource = $countryWithWebsiteSource; -+ $this->eavValidationRules = $eavValidationRules; -+ $this->fileUploaderDataResolver = $fileUploaderDataResolver; -+ $this->context = $context; -+ $this->shareConfig = $shareConfig; -+ } -+ -+ /** -+ * Get meta data of the customer or customer address attribute -+ * -+ * @param AbstractAttribute $attribute -+ * @param Type $entityType -+ * @param bool $allowToShowHiddenAttributes -+ * @return array -+ * @throws \Magento\Framework\Exception\LocalizedException -+ */ -+ public function getAttributesMeta( -+ AbstractAttribute $attribute, -+ Type $entityType, -+ bool $allowToShowHiddenAttributes -+ ): array { -+ $meta = $this->modifyBooleanAttributeMeta($attribute); -+ // use getDataUsingMethod, since some getters are defined and apply additional processing of returning value -+ foreach (self::$metaProperties as $metaName => $origName) { -+ $value = $attribute->getDataUsingMethod($origName); -+ $meta['arguments']['data']['config'][$metaName] = ($metaName === 'label') ? __($value) : $value; -+ if ('frontend_input' === $origName) { -+ $meta['arguments']['data']['config']['formElement'] = self::$formElement[$value] ?? $value; -+ } -+ } -+ -+ if ($attribute->usesSource()) { -+ if ($attribute->getAttributeCode() === AddressInterface::COUNTRY_ID) { -+ $meta['arguments']['data']['config']['options'] = $this->countryWithWebsiteSource -+ ->getAllOptions(); -+ } else { -+ $meta['arguments']['data']['config']['options'] = $attribute->getSource()->getAllOptions(); -+ } -+ } -+ -+ $rules = $this->eavValidationRules->build($attribute, $meta['arguments']['data']['config']); -+ if (!empty($rules)) { -+ $meta['arguments']['data']['config']['validation'] = $rules; -+ } -+ -+ $meta['arguments']['data']['config']['componentType'] = Field::NAME; -+ $meta['arguments']['data']['config']['visible'] = $this->canShowAttribute( -+ $attribute, -+ $allowToShowHiddenAttributes -+ ); -+ -+ $this->fileUploaderDataResolver->overrideFileUploaderMetadata( -+ $entityType, -+ $attribute, -+ $meta['arguments']['data']['config'] -+ ); -+ -+ return $meta; -+ } -+ -+ /** -+ * Detect can we show attribute on specific form or not -+ * -+ * @param AbstractAttribute $customerAttribute -+ * @param bool $allowToShowHiddenAttributes -+ * @return bool -+ */ -+ private function canShowAttribute( -+ AbstractAttribute $customerAttribute, -+ bool $allowToShowHiddenAttributes -+ ) { -+ return $allowToShowHiddenAttributes && (bool) $customerAttribute->getIsUserDefined() -+ ? true -+ : (bool) $customerAttribute->getIsVisible(); -+ } -+ -+ /** -+ * Modify boolean attribute meta data -+ * -+ * @param AttributeInterface $attribute -+ * @return array -+ */ -+ private function modifyBooleanAttributeMeta(AttributeInterface $attribute): array -+ { -+ $meta = []; -+ if ($attribute->getFrontendInput() === 'boolean') { -+ $meta['arguments']['data']['config']['prefer'] = 'toggle'; -+ $meta['arguments']['data']['config']['valueMap'] = [ -+ 'true' => '1', -+ 'false' => '0', -+ ]; -+ } -+ -+ return $meta; -+ } -+ -+ /** -+ * Add global scope parameter and filter options to website meta -+ * -+ * @param array $meta -+ * @return void -+ */ -+ public function processWebsiteMeta(&$meta): void -+ { -+ if (isset($meta[CustomerInterface::WEBSITE_ID]) && $this->shareConfig->isGlobalScope()) { -+ $meta[CustomerInterface::WEBSITE_ID]['arguments']['data']['config']['isGlobalScope'] = 1; -+ } -+ -+ if (isset($meta[AddressInterface::COUNTRY_ID]) && !$this->shareConfig->isGlobalScope()) { -+ $meta[AddressInterface::COUNTRY_ID]['arguments']['data']['config']['filterBy'] = [ -+ 'target' => 'customer_form.customer_form_data_source:data.customer.website_id', -+ 'field' => 'website_ids' -+ ]; -+ } -+ } -+} -diff --git a/app/code/Magento/Customer/Model/Authentication.php b/app/code/Magento/Customer/Model/Authentication.php -index 0967f1a0189..9a9a4630620 100644 ---- a/app/code/Magento/Customer/Model/Authentication.php -+++ b/app/code/Magento/Customer/Model/Authentication.php -@@ -83,7 +83,7 @@ class Authentication implements AuthenticationInterface - } - - /** -- * {@inheritdoc} -+ * @inheritdoc - */ - public function processAuthenticationFailure($customerId) - { -@@ -120,7 +120,7 @@ class Authentication implements AuthenticationInterface - } - - /** -- * {@inheritdoc} -+ * @inheritdoc - */ - public function unlock($customerId) - { -@@ -152,7 +152,7 @@ class Authentication implements AuthenticationInterface - } - - /** -- * {@inheritdoc} -+ * @inheritdoc - */ - public function isLocked($customerId) - { -@@ -161,12 +161,12 @@ class Authentication implements AuthenticationInterface - } - - /** -- * {@inheritdoc} -+ * @inheritdoc - */ - public function authenticate($customerId, $password) - { - $customerSecure = $this->customerRegistry->retrieveSecureData($customerId); -- $hash = $customerSecure->getPasswordHash(); -+ $hash = $customerSecure->getPasswordHash() ?? ''; - if (!$this->encryptor->validateHash($password, $hash)) { - $this->processAuthenticationFailure($customerId); - if ($this->isLocked($customerId)) { -diff --git a/app/code/Magento/Customer/Model/Config/Backend/Address/Street.php b/app/code/Magento/Customer/Model/Config/Backend/Address/Street.php -index fc0fa3ebc07..40a10a1db09 100644 ---- a/app/code/Magento/Customer/Model/Config/Backend/Address/Street.php -+++ b/app/code/Magento/Customer/Model/Config/Backend/Address/Street.php -@@ -87,15 +87,20 @@ class Street extends \Magento\Framework\App\Config\Value - { - $result = parent::afterDelete(); - -- if ($this->getScope() == \Magento\Store\Model\ScopeInterface::SCOPE_WEBSITES) { -- $attribute = $this->_eavConfig->getAttribute('customer_address', 'street'); -- $website = $this->_storeManager->getWebsite($this->getScopeCode()); -- $attribute->setWebsite($website); -- $attribute->load($attribute->getId()); -- $attribute->setData('scope_multiline_count', null); -- $attribute->save(); -- } -+ $attribute = $this->_eavConfig->getAttribute('customer_address', 'street'); -+ switch ($this->getScope()) { -+ case \Magento\Store\Model\ScopeInterface::SCOPE_WEBSITES: -+ $website = $this->_storeManager->getWebsite($this->getScopeCode()); -+ $attribute->setWebsite($website); -+ $attribute->load($attribute->getId()); -+ $attribute->setData('scope_multiline_count', null); -+ break; - -+ case ScopeConfigInterface::SCOPE_TYPE_DEFAULT: -+ $attribute->setData('multiline_count', 2); -+ break; -+ } -+ $attribute->save(); - return $result; - } - } -diff --git a/app/code/Magento/Customer/Model/Customer.php b/app/code/Magento/Customer/Model/Customer.php -index 6d1c1549216..1287dbe5df7 100644 ---- a/app/code/Magento/Customer/Model/Customer.php -+++ b/app/code/Magento/Customer/Model/Customer.php -@@ -219,6 +219,13 @@ class Customer extends \Magento\Framework\Model\AbstractModel - */ - private $accountConfirmation; - -+ /** -+ * Caching property to store customer address data models by the address ID. -+ * -+ * @var array -+ */ -+ private $storedAddress; -+ - /** - * @param \Magento\Framework\Model\Context $context - * @param \Magento\Framework\Registry $registry -@@ -314,7 +321,10 @@ class Customer extends \Magento\Framework\Model\AbstractModel - $addressesData = []; - /** @var \Magento\Customer\Model\Address $address */ - foreach ($this->getAddresses() as $address) { -- $addressesData[] = $address->getDataModel(); -+ if (!isset($this->storedAddress[$address->getId()])) { -+ $this->storedAddress[$address->getId()] = $address->getDataModel(); -+ } -+ $addressesData[] = $this->storedAddress[$address->getId()]; - } - $customerDataObject = $this->customerDataFactory->create(); - $this->dataObjectHelper->populateWithArray( -@@ -359,13 +369,6 @@ class Customer extends \Magento\Framework\Model\AbstractModel - $this->setId($customerId); - } - -- // Need to use attribute set or future updates can cause data loss -- if (!$this->getAttributeSetId()) { -- $this->setAttributeSetId( -- CustomerMetadataInterface::ATTRIBUTE_SET_ID_CUSTOMER -- ); -- } -- - return $this; - } - -@@ -961,6 +964,16 @@ class Customer extends \Magento\Framework\Model\AbstractModel - return $ids; - } - -+ /** -+ * Retrieve attribute set id for customer. -+ * -+ * @return int -+ */ -+ public function getAttributeSetId() -+ { -+ return parent::getAttributeSetId() ?: CustomerMetadataInterface::ATTRIBUTE_SET_ID_CUSTOMER; -+ } -+ - /** - * Set store to customer - * -@@ -1041,17 +1054,6 @@ class Customer extends \Magento\Framework\Model\AbstractModel - return $this; - } - -- /** -- * Prepare customer for delete -- * -- * @return $this -- */ -- public function beforeDelete() -- { -- //TODO : Revisit and figure handling permissions in MAGETWO-11084 Implementation: Service Context Provider -- return parent::beforeDelete(); -- } -- - /** - * Processing object after save data - * -@@ -1292,6 +1294,8 @@ class Customer extends \Magento\Framework\Model\AbstractModel - } - - /** -+ * Create Address from Factory -+ * - * @return Address - */ - protected function _createAddressInstance() -@@ -1300,6 +1304,8 @@ class Customer extends \Magento\Framework\Model\AbstractModel - } - - /** -+ * Create Address Collection from Factory -+ * - * @return \Magento\Customer\Model\ResourceModel\Address\Collection - */ - protected function _createAddressCollection() -@@ -1308,6 +1314,8 @@ class Customer extends \Magento\Framework\Model\AbstractModel - } - - /** -+ * Get Template Types -+ * - * @return array - */ - protected function getTemplateTypes() -diff --git a/app/code/Magento/Customer/Model/Customer/DataProvider.php b/app/code/Magento/Customer/Model/Customer/DataProvider.php -index ce976d3f62c..0b7c618e6a1 100644 ---- a/app/code/Magento/Customer/Model/Customer/DataProvider.php -+++ b/app/code/Magento/Customer/Model/Customer/DataProvider.php -@@ -5,14 +5,11 @@ - */ - namespace Magento\Customer\Model\Customer; - --use Magento\Customer\Api\AddressMetadataInterface; --use Magento\Customer\Api\CustomerMetadataInterface; - use Magento\Customer\Api\Data\AddressInterface; - use Magento\Customer\Api\Data\CustomerInterface; - use Magento\Customer\Model\Address; - use Magento\Customer\Model\Attribute; - use Magento\Customer\Model\Customer; --use Magento\Customer\Model\FileProcessor; - use Magento\Customer\Model\FileProcessorFactory; - use Magento\Customer\Model\ResourceModel\Address\Attribute\Source\CountryWithWebsites; - use Magento\Customer\Model\ResourceModel\Customer\Collection; -@@ -25,12 +22,18 @@ use Magento\Framework\App\ObjectManager; - use Magento\Framework\Session\SessionManagerInterface; - use Magento\Framework\View\Element\UiComponent\ContextInterface; - use Magento\Framework\View\Element\UiComponent\DataProvider\FilterPool; -+use Magento\Ui\Component\Form\Element\Multiline; - use Magento\Ui\Component\Form\Field; - use Magento\Ui\DataProvider\EavValidationRules; -+use Magento\Customer\Model\FileUploaderDataResolver; - - /** -+ * Supplies the data for the customer UI component -+ * - * @SuppressWarnings(PHPMD.CouplingBetweenObjects) -+ * @SuppressWarnings(PHPMD.TooManyFields) - * -+ * @deprecated \Magento\Customer\Model\Customer\DataProviderWithDefaultAddresses is used instead - * @api - * @since 100.0.2 - */ -@@ -108,21 +111,6 @@ class DataProvider extends \Magento\Ui\DataProvider\AbstractDataProvider - */ - protected $session; - -- /** -- * @var FileProcessorFactory -- */ -- private $fileProcessorFactory; -- -- /** -- * File types allowed for file_uploader UI component -- * -- * @var array -- */ -- private $fileUploaderTypes = [ -- 'image', -- 'file', -- ]; -- - /** - * Customer fields that must be removed - * -@@ -146,6 +134,11 @@ class DataProvider extends \Magento\Ui\DataProvider\AbstractDataProvider - */ - private $allowToShowHiddenAttributes; - -+ /** -+ * @var FileUploaderDataResolver -+ */ -+ private $fileUploaderDataResolver; -+ - /** - * @param string $name - * @param string $primaryFieldName -@@ -155,11 +148,13 @@ class DataProvider extends \Magento\Ui\DataProvider\AbstractDataProvider - * @param Config $eavConfig - * @param FilterPool $filterPool - * @param FileProcessorFactory $fileProcessorFactory -- * @param ContextInterface $context - * @param array $meta - * @param array $data -+ * @param ContextInterface $context - * @param bool $allowToShowHiddenAttributes -+ * @param FileUploaderDataResolver|null $fileUploaderDataResolver - * @SuppressWarnings(PHPMD.ExcessiveParameterList) -+ * @SuppressWarnings(PHPMD.UnusedFormalParameter) - */ - public function __construct( - $name, -@@ -173,7 +168,8 @@ class DataProvider extends \Magento\Ui\DataProvider\AbstractDataProvider - array $meta = [], - array $data = [], - ContextInterface $context = null, -- $allowToShowHiddenAttributes = true -+ $allowToShowHiddenAttributes = true, -+ $fileUploaderDataResolver = null - ) { - parent::__construct($name, $primaryFieldName, $requestFieldName, $meta, $data); - $this->eavValidationRules = $eavValidationRules; -@@ -181,9 +177,10 @@ class DataProvider extends \Magento\Ui\DataProvider\AbstractDataProvider - $this->collection->addAttributeToSelect('*'); - $this->eavConfig = $eavConfig; - $this->filterPool = $filterPool; -- $this->fileProcessorFactory = $fileProcessorFactory ?: $this->getFileProcessorFactory(); - $this->context = $context ?: ObjectManager::getInstance()->get(ContextInterface::class); - $this->allowToShowHiddenAttributes = $allowToShowHiddenAttributes; -+ $this->fileUploaderDataResolver = $fileUploaderDataResolver -+ ?: ObjectManager::getInstance()->get(FileUploaderDataResolver::class); - $this->meta['customer']['children'] = $this->getAttributesMeta( - $this->eavConfig->getEntityType('customer') - ); -@@ -224,7 +221,7 @@ class DataProvider extends \Magento\Ui\DataProvider\AbstractDataProvider - foreach ($items as $customer) { - $result['customer'] = $customer->getData(); - -- $this->overrideFileUploaderData($customer, $result['customer']); -+ $this->fileUploaderDataResolver->overrideFileUploaderData($customer, $result['customer']); - - $result['customer'] = array_diff_key( - $result['customer'], -@@ -239,7 +236,7 @@ class DataProvider extends \Magento\Ui\DataProvider\AbstractDataProvider - $result['address'][$addressId] = $address->getData(); - $this->prepareAddressData($addressId, $result['address'], $result['customer']); - -- $this->overrideFileUploaderData($address, $result['address'][$addressId]); -+ $this->fileUploaderDataResolver->overrideFileUploaderData($address, $result['address'][$addressId]); - } - $this->loadedData[$customer->getId()] = $result; - } -@@ -254,75 +251,6 @@ class DataProvider extends \Magento\Ui\DataProvider\AbstractDataProvider - return $this->loadedData; - } - -- /** -- * Override file uploader UI component data -- * -- * Overrides data for attributes with frontend_input equal to 'image' or 'file'. -- * -- * @param Customer|Address $entity -- * @param array $entityData -- * @return void -- */ -- private function overrideFileUploaderData($entity, array &$entityData) -- { -- $attributes = $entity->getAttributes(); -- foreach ($attributes as $attribute) { -- /** @var Attribute $attribute */ -- if (in_array($attribute->getFrontendInput(), $this->fileUploaderTypes)) { -- $entityData[$attribute->getAttributeCode()] = $this->getFileUploaderData( -- $entity->getEntityType(), -- $attribute, -- $entityData -- ); -- } -- } -- } -- -- /** -- * Retrieve array of values required by file uploader UI component -- * -- * @param Type $entityType -- * @param Attribute $attribute -- * @param array $customerData -- * @return array -- * @SuppressWarnings(PHPMD.NPathComplexity) -- */ -- private function getFileUploaderData( -- Type $entityType, -- Attribute $attribute, -- array $customerData -- ) { -- $attributeCode = $attribute->getAttributeCode(); -- -- $file = isset($customerData[$attributeCode]) -- ? $customerData[$attributeCode] -- : ''; -- -- /** @var FileProcessor $fileProcessor */ -- $fileProcessor = $this->getFileProcessorFactory()->create([ -- 'entityTypeCode' => $entityType->getEntityTypeCode(), -- ]); -- -- if (!empty($file) -- && $fileProcessor->isExist($file) -- ) { -- $stat = $fileProcessor->getStat($file); -- $viewUrl = $fileProcessor->getViewUrl($file, $attribute->getFrontendInput()); -- -- return [ -- [ -- 'file' => $file, -- 'size' => isset($stat) ? $stat['size'] : 0, -- 'url' => isset($viewUrl) ? $viewUrl : '', -- 'name' => basename($file), -- 'type' => $fileProcessor->getMimeType($file), -- ], -- ]; -- } -- -- return []; -- } -- - /** - * Get attributes meta - * -@@ -368,52 +296,28 @@ class DataProvider extends \Magento\Ui\DataProvider\AbstractDataProvider - $meta[$code]['arguments']['data']['config']['componentType'] = Field::NAME; - $meta[$code]['arguments']['data']['config']['visible'] = $this->canShowAttribute($attribute); - -- $this->overrideFileUploaderMetadata($entityType, $attribute, $meta[$code]['arguments']['data']['config']); -+ $this->fileUploaderDataResolver->overrideFileUploaderMetadata( -+ $entityType, -+ $attribute, -+ $meta[$code]['arguments']['data']['config'] -+ ); - } - - $this->processWebsiteMeta($meta); - return $meta; - } - -- /** -- * Check whether the specific attribute can be shown in form: customer registration, customer edit, etc... -- * -- * @param Attribute $customerAttribute -- * @return bool -- */ -- private function canShowAttributeInForm(AbstractAttribute $customerAttribute) -- { -- $isRegistration = $this->context->getRequestParam($this->getRequestFieldName()) === null; -- -- if ($customerAttribute->getEntityType()->getEntityTypeCode() === 'customer') { -- return is_array($customerAttribute->getUsedInForms()) && -- ( -- (in_array('customer_account_create', $customerAttribute->getUsedInForms()) && $isRegistration) || -- (in_array('customer_account_edit', $customerAttribute->getUsedInForms()) && !$isRegistration) -- ); -- } else { -- return is_array($customerAttribute->getUsedInForms()) && -- in_array('customer_address_edit', $customerAttribute->getUsedInForms()); -- } -- } -- - /** - * Detect can we show attribute on specific form or not - * - * @param Attribute $customerAttribute - * @return bool - */ -- private function canShowAttribute(AbstractAttribute $customerAttribute) -+ private function canShowAttribute(AbstractAttribute $customerAttribute): bool - { -- $userDefined = (bool) $customerAttribute->getIsUserDefined(); -- if (!$userDefined) { -- return $customerAttribute->getIsVisible(); -- } -- -- $canShowOnForm = $this->canShowAttributeInForm($customerAttribute); -- -- return ($this->allowToShowHiddenAttributes && $canShowOnForm) || -- (!$this->allowToShowHiddenAttributes && $canShowOnForm && $customerAttribute->getIsVisible()); -+ return $this->allowToShowHiddenAttributes && (bool) $customerAttribute->getIsUserDefined() -+ ? true -+ : (bool) $customerAttribute->getIsVisible(); - } - - /** -@@ -466,97 +370,6 @@ class DataProvider extends \Magento\Ui\DataProvider\AbstractDataProvider - } - } - -- /** -- * Override file uploader UI component metadata -- * -- * Overrides metadata for attributes with frontend_input equal to 'image' or 'file'. -- * -- * @param Type $entityType -- * @param AbstractAttribute $attribute -- * @param array $config -- * @return void -- */ -- private function overrideFileUploaderMetadata( -- Type $entityType, -- AbstractAttribute $attribute, -- array &$config -- ) { -- if (in_array($attribute->getFrontendInput(), $this->fileUploaderTypes)) { -- $maxFileSize = self::MAX_FILE_SIZE; -- -- if (isset($config['validation']['max_file_size'])) { -- $maxFileSize = (int)$config['validation']['max_file_size']; -- } -- -- $allowedExtensions = []; -- -- if (isset($config['validation']['file_extensions'])) { -- $allowedExtensions = explode(',', $config['validation']['file_extensions']); -- array_walk($allowedExtensions, function (&$value) { -- $value = strtolower(trim($value)); -- }); -- } -- -- $allowedExtensions = implode(' ', $allowedExtensions); -- -- $entityTypeCode = $entityType->getEntityTypeCode(); -- $url = $this->getFileUploadUrl($entityTypeCode); -- -- $config = [ -- 'formElement' => 'fileUploader', -- 'componentType' => 'fileUploader', -- 'maxFileSize' => $maxFileSize, -- 'allowedExtensions' => $allowedExtensions, -- 'uploaderConfig' => [ -- 'url' => $url, -- ], -- 'label' => $this->getMetadataValue($config, 'label'), -- 'sortOrder' => $this->getMetadataValue($config, 'sortOrder'), -- 'required' => $this->getMetadataValue($config, 'required'), -- 'visible' => $this->getMetadataValue($config, 'visible'), -- 'validation' => $this->getMetadataValue($config, 'validation'), -- ]; -- } -- } -- -- /** -- * Retrieve metadata value -- * -- * @param array $config -- * @param string $name -- * @param mixed $default -- * @return mixed -- */ -- private function getMetadataValue($config, $name, $default = null) -- { -- $value = isset($config[$name]) ? $config[$name] : $default; -- return $value; -- } -- -- /** -- * Retrieve URL to file upload -- * -- * @param string $entityTypeCode -- * @return string -- */ -- private function getFileUploadUrl($entityTypeCode) -- { -- switch ($entityTypeCode) { -- case CustomerMetadataInterface::ENTITY_TYPE_CUSTOMER: -- $url = 'customer/file/customer_upload'; -- break; -- -- case AddressMetadataInterface::ENTITY_TYPE_ADDRESS: -- $url = 'customer/file/address_upload'; -- break; -- -- default: -- $url = ''; -- break; -- } -- return $url; -- } -- - /** - * Process attributes by frontend input type - * -@@ -596,23 +409,14 @@ class DataProvider extends \Magento\Ui\DataProvider\AbstractDataProvider - ) { - $addresses[$addressId]['default_shipping'] = $customer['default_shipping']; - } -- if (isset($addresses[$addressId]['street']) && !is_array($addresses[$addressId]['street'])) { -- $addresses[$addressId]['street'] = explode("\n", $addresses[$addressId]['street']); -- } -- } - -- /** -- * Get FileProcessorFactory instance -- * -- * @return FileProcessorFactory -- * @deprecated 100.1.3 -- */ -- private function getFileProcessorFactory() -- { -- if ($this->fileProcessorFactory === null) { -- $this->fileProcessorFactory = ObjectManager::getInstance() -- ->get(\Magento\Customer\Model\FileProcessorFactory::class); -+ foreach ($this->meta['address']['children'] as $attributeName => $attributeMeta) { -+ if ($attributeMeta['arguments']['data']['config']['dataType'] === Multiline::NAME -+ && isset($addresses[$addressId][$attributeName]) -+ && !is_array($addresses[$addressId][$attributeName]) -+ ) { -+ $addresses[$addressId][$attributeName] = explode("\n", $addresses[$addressId][$attributeName]); -+ } - } -- return $this->fileProcessorFactory; - } - } -diff --git a/app/code/Magento/Customer/Model/Customer/DataProviderWithDefaultAddresses.php b/app/code/Magento/Customer/Model/Customer/DataProviderWithDefaultAddresses.php -new file mode 100644 -index 00000000000..6d18b881a69 ---- /dev/null -+++ b/app/code/Magento/Customer/Model/Customer/DataProviderWithDefaultAddresses.php -@@ -0,0 +1,198 @@ -+<?php -+declare(strict_types=1); -+/** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+namespace Magento\Customer\Model\Customer; -+ -+use Magento\Customer\Model\Address; -+use Magento\Customer\Model\Customer; -+use Magento\Customer\Model\ResourceModel\Customer\CollectionFactory as CustomerCollectionFactory; -+use Magento\Eav\Model\Config; -+use Magento\Eav\Model\Entity\Attribute\AbstractAttribute; -+use Magento\Eav\Model\Entity\Type; -+use Magento\Framework\Session\SessionManagerInterface; -+use Magento\Customer\Model\FileUploaderDataResolver; -+use Magento\Customer\Model\AttributeMetadataResolver; -+ -+/** -+ * Refactored version of Magento\Customer\Model\Customer\DataProvider with eliminated usage of addresses collection. -+ */ -+class DataProviderWithDefaultAddresses extends \Magento\Ui\DataProvider\AbstractDataProvider -+{ -+ /** -+ * @var array -+ */ -+ private $loadedData = []; -+ -+ /** -+ * @var SessionManagerInterface -+ */ -+ private $session; -+ -+ /** -+ * Customer fields that must be removed -+ * -+ * @var array -+ */ -+ private static $forbiddenCustomerFields = [ -+ 'password_hash', -+ 'rp_token', -+ ]; -+ -+ /** -+ * Allow to manage attributes, even they are hidden on storefront -+ * -+ * @var bool -+ */ -+ private $allowToShowHiddenAttributes; -+ -+ /** -+ * @var \Magento\Directory\Model\CountryFactory -+ */ -+ private $countryFactory; -+ -+ /** -+ * @var FileUploaderDataResolver -+ */ -+ private $fileUploaderDataResolver; -+ -+ /** -+ * @var AttributeMetadataResolver -+ */ -+ private $attributeMetadataResolver; -+ -+ /** -+ * @param string $name -+ * @param string $primaryFieldName -+ * @param string $requestFieldName -+ * @param CustomerCollectionFactory $customerCollectionFactory -+ * @param Config $eavConfig -+ * @param \Magento\Directory\Model\CountryFactory $countryFactory -+ * @param SessionManagerInterface $session -+ * @param FileUploaderDataResolver $fileUploaderDataResolver -+ * @param AttributeMetadataResolver $attributeMetadataResolver -+ * @param bool $allowToShowHiddenAttributes -+ * @param array $meta -+ * @param array $data -+ * @throws \Magento\Framework\Exception\LocalizedException -+ * @SuppressWarnings(PHPMD.ExcessiveParameterList) -+ */ -+ public function __construct( -+ string $name, -+ string $primaryFieldName, -+ string $requestFieldName, -+ CustomerCollectionFactory $customerCollectionFactory, -+ Config $eavConfig, -+ \Magento\Directory\Model\CountryFactory $countryFactory, -+ SessionManagerInterface $session, -+ FileUploaderDataResolver $fileUploaderDataResolver, -+ AttributeMetadataResolver $attributeMetadataResolver, -+ $allowToShowHiddenAttributes = true, -+ array $meta = [], -+ array $data = [] -+ ) { -+ parent::__construct($name, $primaryFieldName, $requestFieldName, $meta, $data); -+ $this->collection = $customerCollectionFactory->create(); -+ $this->collection->addAttributeToSelect('*'); -+ $this->allowToShowHiddenAttributes = $allowToShowHiddenAttributes; -+ $this->session = $session; -+ $this->countryFactory = $countryFactory; -+ $this->fileUploaderDataResolver = $fileUploaderDataResolver; -+ $this->attributeMetadataResolver = $attributeMetadataResolver; -+ $this->meta['customer']['children'] = $this->getAttributesMeta( -+ $eavConfig->getEntityType('customer') -+ ); -+ } -+ -+ /** -+ * Get data -+ * -+ * @return array -+ */ -+ public function getData(): array -+ { -+ if (!empty($this->loadedData)) { -+ return $this->loadedData; -+ } -+ $items = $this->collection->getItems(); -+ /** @var Customer $customer */ -+ foreach ($items as $customer) { -+ $result['customer'] = $customer->getData(); -+ -+ $this->fileUploaderDataResolver->overrideFileUploaderData($customer, $result['customer']); -+ -+ $result['customer'] = array_diff_key( -+ $result['customer'], -+ array_flip(self::$forbiddenCustomerFields) -+ ); -+ unset($result['address']); -+ -+ $result['default_billing_address'] = $this->prepareDefaultAddress( -+ $customer->getDefaultBillingAddress() -+ ); -+ $result['default_shipping_address'] = $this->prepareDefaultAddress( -+ $customer->getDefaultShippingAddress() -+ ); -+ $result['customer_id'] = $customer->getId(); -+ -+ $this->loadedData[$customer->getId()] = $result; -+ } -+ -+ $data = $this->session->getCustomerFormData(); -+ if (!empty($data)) { -+ $customerId = $data['customer']['entity_id'] ?? null; -+ $this->loadedData[$customerId] = $data; -+ $this->session->unsCustomerFormData(); -+ } -+ -+ return $this->loadedData; -+ } -+ -+ /** -+ * Prepare default address data. -+ * -+ * @param Address|false $address -+ * @return array -+ */ -+ private function prepareDefaultAddress($address): array -+ { -+ $addressData = []; -+ -+ if (!empty($address)) { -+ $addressData = $address->getData(); -+ if (isset($addressData['street']) && !\is_array($address['street'])) { -+ $addressData['street'] = explode("\n", $addressData['street']); -+ } -+ $addressData['country'] = $this->countryFactory->create() -+ ->loadByCode($addressData['country_id'])->getName(); -+ } -+ -+ return $addressData; -+ } -+ -+ /** -+ * Get attributes meta -+ * -+ * @param Type $entityType -+ * @return array -+ * @throws \Magento\Framework\Exception\LocalizedException -+ */ -+ private function getAttributesMeta(Type $entityType): array -+ { -+ $meta = []; -+ $attributes = $entityType->getAttributeCollection(); -+ /* @var AbstractAttribute $attribute */ -+ foreach ($attributes as $attribute) { -+ $meta[$attribute->getAttributeCode()] = $this->attributeMetadataResolver->getAttributesMeta( -+ $attribute, -+ $entityType, -+ $this->allowToShowHiddenAttributes -+ ); -+ } -+ $this->attributeMetadataResolver->processWebsiteMeta($meta); -+ -+ return $meta; -+ } -+} -diff --git a/app/code/Magento/Customer/Model/Customer/Source/Group.php b/app/code/Magento/Customer/Model/Customer/Source/Group.php -index e4c1d2e75be..efcc7d0fe93 100644 ---- a/app/code/Magento/Customer/Model/Customer/Source/Group.php -+++ b/app/code/Magento/Customer/Model/Customer/Source/Group.php -@@ -6,11 +6,14 @@ - namespace Magento\Customer\Model\Customer\Source; - - use Magento\Customer\Api\Data\GroupSearchResultsInterface; --use Magento\Framework\Module\Manager as ModuleManager; -+use \Magento\Framework\Module\ModuleManagerInterface as ModuleManager; - use Magento\Customer\Api\Data\GroupInterface; - use Magento\Customer\Api\GroupRepositoryInterface; - use Magento\Framework\Api\SearchCriteriaBuilder; - -+/** -+ * Group. -+ */ - class Group implements GroupSourceInterface - { - /** -diff --git a/app/code/Magento/Customer/Model/CustomerAuthUpdate.php b/app/code/Magento/Customer/Model/CustomerAuthUpdate.php -index 06de649524e..bc9bffb6ffd 100644 ---- a/app/code/Magento/Customer/Model/CustomerAuthUpdate.php -+++ b/app/code/Magento/Customer/Model/CustomerAuthUpdate.php -@@ -6,31 +6,43 @@ - - namespace Magento\Customer\Model; - -+use Magento\Customer\Model\ResourceModel\Customer as CustomerResourceModel; -+use Magento\Framework\App\ObjectManager; -+use Magento\Framework\Exception\NoSuchEntityException; -+ - /** - * Customer Authentication update model. - */ - class CustomerAuthUpdate - { - /** -- * @var \Magento\Customer\Model\CustomerRegistry -+ * @var CustomerRegistry - */ - protected $customerRegistry; - - /** -- * @var \Magento\Customer\Model\ResourceModel\Customer -+ * @var CustomerResourceModel - */ - protected $customerResourceModel; - - /** -- * @param \Magento\Customer\Model\CustomerRegistry $customerRegistry -- * @param \Magento\Customer\Model\ResourceModel\Customer $customerResourceModel -+ * @var Customer -+ */ -+ private $customerModel; -+ -+ /** -+ * @param CustomerRegistry $customerRegistry -+ * @param CustomerResourceModel $customerResourceModel -+ * @param Customer|null $customerModel - */ - public function __construct( -- \Magento\Customer\Model\CustomerRegistry $customerRegistry, -- \Magento\Customer\Model\ResourceModel\Customer $customerResourceModel -+ CustomerRegistry $customerRegistry, -+ CustomerResourceModel $customerResourceModel, -+ Customer $customerModel = null - ) { - $this->customerRegistry = $customerRegistry; - $this->customerResourceModel = $customerResourceModel; -+ $this->customerModel = $customerModel ?: ObjectManager::getInstance()->get(Customer::class); - } - - /** -@@ -38,21 +50,30 @@ class CustomerAuthUpdate - * - * @param int $customerId - * @return $this -+ * @throws NoSuchEntityException - */ - public function saveAuth($customerId) - { - $customerSecure = $this->customerRegistry->retrieveSecureData($customerId); - -+ $this->customerResourceModel->load($this->customerModel, $customerId); -+ $currentLockExpiresVal = $this->customerModel->getData('lock_expires'); -+ $newLockExpiresVal = $customerSecure->getData('lock_expires'); -+ - $this->customerResourceModel->getConnection()->update( - $this->customerResourceModel->getTable('customer_entity'), - [ - 'failures_num' => $customerSecure->getData('failures_num'), - 'first_failure' => $customerSecure->getData('first_failure'), -- 'lock_expires' => $customerSecure->getData('lock_expires'), -+ 'lock_expires' => $newLockExpiresVal, - ], - $this->customerResourceModel->getConnection()->quoteInto('entity_id = ?', $customerId) - ); - -+ if ($currentLockExpiresVal !== $newLockExpiresVal) { -+ $this->customerModel->reindex(); -+ } -+ - return $this; - } - } -diff --git a/app/code/Magento/Customer/Model/CustomerIdProvider.php b/app/code/Magento/Customer/Model/CustomerIdProvider.php -new file mode 100644 -index 00000000000..e670d5eb02d ---- /dev/null -+++ b/app/code/Magento/Customer/Model/CustomerIdProvider.php -@@ -0,0 +1,40 @@ -+<?php -+/** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+declare(strict_types=1); -+ -+namespace Magento\Customer\Model; -+ -+use Magento\Framework\App\RequestInterface; -+ -+/** -+ * Provides customer id from request. -+ */ -+class CustomerIdProvider -+{ -+ /** -+ * @var RequestInterface -+ */ -+ private $request; -+ -+ /** -+ * @param RequestInterface $request -+ */ -+ public function __construct( -+ RequestInterface $request -+ ) { -+ $this->request = $request; -+ } -+ -+ /** -+ * Get customer id from request. -+ * -+ * @return int -+ */ -+ public function getCustomerId(): int -+ { -+ return (int)$this->request->getParam('id'); -+ } -+} -diff --git a/app/code/Magento/Customer/Model/EmailNotification.php b/app/code/Magento/Customer/Model/EmailNotification.php -index 4b65dcca097..144c24f8e83 100644 ---- a/app/code/Magento/Customer/Model/EmailNotification.php -+++ b/app/code/Magento/Customer/Model/EmailNotification.php -@@ -17,6 +17,8 @@ use Magento\Framework\Reflection\DataObjectProcessor; - use Magento\Framework\Exception\LocalizedException; - - /** -+ * Customer email notification -+ * - * @SuppressWarnings(PHPMD.CouplingBetweenObjects) - */ - class EmailNotification implements EmailNotificationInterface -diff --git a/app/code/Magento/Customer/Model/FileProcessor.php b/app/code/Magento/Customer/Model/FileProcessor.php -index 6a8472758c1..c16faea2842 100644 ---- a/app/code/Magento/Customer/Model/FileProcessor.php -+++ b/app/code/Magento/Customer/Model/FileProcessor.php -@@ -3,8 +3,13 @@ - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -+declare(strict_types=1); -+ - namespace Magento\Customer\Model; - -+/** -+ * Processor class for work with uploaded files -+ */ - class FileProcessor - { - /** -@@ -232,7 +237,7 @@ class FileProcessor - ); - } - -- $fileName = $dispersionPath . '/' . $fileName; -+ $fileName = $dispersionPath . '/' . $destinationFileName; - return $fileName; - } - -diff --git a/app/code/Magento/Customer/Model/FileUploaderDataResolver.php b/app/code/Magento/Customer/Model/FileUploaderDataResolver.php -new file mode 100644 -index 00000000000..535bfe97bc4 ---- /dev/null -+++ b/app/code/Magento/Customer/Model/FileUploaderDataResolver.php -@@ -0,0 +1,204 @@ -+<?php -+declare(strict_types=1); -+/** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+namespace Magento\Customer\Model; -+ -+use Magento\Eav\Model\Entity\Type; -+use Magento\Eav\Model\Entity\Attribute\AbstractAttribute; -+use Magento\Customer\Api\AddressMetadataInterface; -+use Magento\Customer\Api\CustomerMetadataInterface; -+ -+/** -+ * Class to retrieve file uploader data for customer and customer address file & image attributes -+ */ -+class FileUploaderDataResolver -+{ -+ /** -+ * Maximum file size allowed for file_uploader UI component -+ * This constant was copied from deprecated data provider \Magento\Customer\Model\Customer\DataProvider -+ */ -+ private const MAX_FILE_SIZE = 2097152; -+ -+ /** -+ * @var FileProcessorFactory -+ */ -+ private $fileProcessorFactory; -+ -+ /** -+ * File types allowed for file_uploader UI component -+ * -+ * @var array -+ */ -+ private $fileUploaderTypes = [ -+ 'image', -+ 'file', -+ ]; -+ -+ /** -+ * @param FileProcessorFactory $fileProcessorFactory -+ */ -+ public function __construct( -+ FileProcessorFactory $fileProcessorFactory -+ ) { -+ $this->fileProcessorFactory = $fileProcessorFactory; -+ } -+ -+ /** -+ * Override file uploader UI component data -+ * -+ * Overrides data for attributes with frontend_input equal to 'image' or 'file'. -+ * -+ * @param Customer|Address $entity -+ * @param array $entityData -+ * @return void -+ */ -+ public function overrideFileUploaderData($entity, array &$entityData): void -+ { -+ $attributes = $entity->getAttributes(); -+ foreach ($attributes as $attribute) { -+ /** @var Attribute $attribute */ -+ if (\in_array($attribute->getFrontendInput(), $this->fileUploaderTypes, true)) { -+ $entityData[$attribute->getAttributeCode()] = $this->getFileUploaderData( -+ $entity->getEntityType(), -+ $attribute, -+ $entityData -+ ); -+ } -+ } -+ } -+ -+ /** -+ * Retrieve array of values required by file uploader UI component -+ * -+ * @param Type $entityType -+ * @param Attribute $attribute -+ * @param array $customerData -+ * @return array -+ */ -+ private function getFileUploaderData( -+ Type $entityType, -+ Attribute $attribute, -+ array $customerData -+ ): array { -+ $attributeCode = $attribute->getAttributeCode(); -+ -+ $file = $customerData[$attributeCode] ?? null; -+ -+ /** @var FileProcessor $fileProcessor */ -+ $fileProcessor = $this->fileProcessorFactory->create([ -+ 'entityTypeCode' => $entityType->getEntityTypeCode(), -+ ]); -+ -+ if (!empty($file) -+ && $fileProcessor->isExist($file) -+ ) { -+ $stat = $fileProcessor->getStat($file); -+ $viewUrl = $fileProcessor->getViewUrl($file, $attribute->getFrontendInput()); -+ -+ return [ -+ [ -+ 'file' => $file, -+ 'size' => null !== $stat ? $stat['size'] : 0, -+ 'url' => $viewUrl ?? '', -+ 'name' => basename($file), -+ 'type' => $fileProcessor->getMimeType($file), -+ ], -+ ]; -+ } -+ -+ return []; -+ } -+ -+ /** -+ * Override file uploader UI component metadata -+ * -+ * Overrides metadata for attributes with frontend_input equal to 'image' or 'file'. -+ * -+ * @param Type $entityType -+ * @param AbstractAttribute $attribute -+ * @param array $config -+ * @return void -+ */ -+ public function overrideFileUploaderMetadata( -+ Type $entityType, -+ AbstractAttribute $attribute, -+ array &$config -+ ): void { -+ if (\in_array($attribute->getFrontendInput(), $this->fileUploaderTypes, true)) { -+ $maxFileSize = self::MAX_FILE_SIZE; -+ -+ if (isset($config['validation']['max_file_size'])) { -+ $maxFileSize = (int)$config['validation']['max_file_size']; -+ } -+ -+ $allowedExtensions = []; -+ -+ if (isset($config['validation']['file_extensions'])) { -+ $allowedExtensions = explode(',', $config['validation']['file_extensions']); -+ array_walk($allowedExtensions, function (&$value) { -+ $value = strtolower(trim($value)); -+ }); -+ } -+ -+ $allowedExtensions = implode(' ', $allowedExtensions); -+ -+ $entityTypeCode = $entityType->getEntityTypeCode(); -+ $url = $this->getFileUploadUrl($entityTypeCode); -+ -+ $config = [ -+ 'formElement' => 'fileUploader', -+ 'componentType' => 'fileUploader', -+ 'maxFileSize' => $maxFileSize, -+ 'allowedExtensions' => $allowedExtensions, -+ 'uploaderConfig' => [ -+ 'url' => $url, -+ ], -+ 'label' => $this->getMetadataValue($config, 'label'), -+ 'sortOrder' => $this->getMetadataValue($config, 'sortOrder'), -+ 'required' => $this->getMetadataValue($config, 'required'), -+ 'visible' => $this->getMetadataValue($config, 'visible'), -+ 'validation' => $this->getMetadataValue($config, 'validation'), -+ ]; -+ } -+ } -+ -+ /** -+ * Retrieve metadata value -+ * -+ * @param array $config -+ * @param string $name -+ * @param mixed $default -+ * @return mixed -+ */ -+ private function getMetadataValue($config, $name, $default = null) -+ { -+ return $config[$name] ?? $default; -+ } -+ -+ /** -+ * Retrieve URL to file upload -+ * -+ * @param string $entityTypeCode -+ * @return string -+ */ -+ private function getFileUploadUrl($entityTypeCode): string -+ { -+ switch ($entityTypeCode) { -+ case CustomerMetadataInterface::ENTITY_TYPE_CUSTOMER: -+ $url = 'customer/file/customer_upload'; -+ break; -+ -+ case AddressMetadataInterface::ENTITY_TYPE_ADDRESS: -+ $url = 'customer/file/address_upload'; -+ break; -+ -+ default: -+ $url = ''; -+ break; -+ } -+ return $url; -+ } -+} -diff --git a/app/code/Magento/Customer/Model/ForgotPasswordToken/ConfirmCustomerByToken.php b/app/code/Magento/Customer/Model/ForgotPasswordToken/ConfirmCustomerByToken.php -new file mode 100644 -index 00000000000..6aadc814a4b ---- /dev/null -+++ b/app/code/Magento/Customer/Model/ForgotPasswordToken/ConfirmCustomerByToken.php -@@ -0,0 +1,58 @@ -+<?php -+/** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+declare(strict_types=1); -+ -+namespace Magento\Customer\Model\ForgotPasswordToken; -+ -+use Magento\Customer\Api\CustomerRepositoryInterface; -+ -+/** -+ * Confirm customer by reset password token -+ */ -+class ConfirmCustomerByToken -+{ -+ /** -+ * @var GetCustomerByToken -+ */ -+ private $getByToken; -+ -+ /** -+ * @var CustomerRepositoryInterface -+ */ -+ private $customerRepository; -+ -+ /** -+ * ConfirmByToken constructor. -+ * -+ * @param GetCustomerByToken $getByToken -+ * @param CustomerRepositoryInterface $customerRepository -+ */ -+ public function __construct( -+ GetCustomerByToken $getByToken, -+ CustomerRepositoryInterface $customerRepository -+ ) { -+ $this->getByToken = $getByToken; -+ $this->customerRepository = $customerRepository; -+ } -+ -+ /** -+ * Confirm customer account my rp_token -+ * -+ * @param string $resetPasswordToken -+ * -+ * @return void -+ * @throws \Magento\Framework\Exception\LocalizedException -+ */ -+ public function execute(string $resetPasswordToken): void -+ { -+ $customer = $this->getByToken->execute($resetPasswordToken); -+ if ($customer->getConfirmation()) { -+ $this->customerRepository->save( -+ $customer->setConfirmation(null) -+ ); -+ } -+ } -+} -diff --git a/app/code/Magento/Customer/Model/ForgotPasswordToken/GetCustomerByToken.php b/app/code/Magento/Customer/Model/ForgotPasswordToken/GetCustomerByToken.php -new file mode 100644 -index 00000000000..09af4e296bd ---- /dev/null -+++ b/app/code/Magento/Customer/Model/ForgotPasswordToken/GetCustomerByToken.php -@@ -0,0 +1,89 @@ -+<?php -+/** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+declare(strict_types=1); -+ -+namespace Magento\Customer\Model\ForgotPasswordToken; -+ -+use Magento\Customer\Api\CustomerRepositoryInterface; -+use Magento\Customer\Api\Data\CustomerInterface; -+use Magento\Framework\Api\SearchCriteriaBuilder; -+use Magento\Framework\Exception\NoSuchEntityException; -+use Magento\Framework\Exception\State\ExpiredException; -+use Magento\Framework\Phrase; -+ -+/** -+ * Get Customer By reset password token -+ * @SuppressWarnings(PHPMD.LongVariable) -+ */ -+class GetCustomerByToken -+{ -+ /** -+ * @var \Magento\Customer\Api\CustomerRepositoryInterface -+ */ -+ private $customerRepository; -+ -+ /** -+ * @var \Magento\Framework\Api\SearchCriteriaBuilder -+ */ -+ private $searchCriteriaBuilder; -+ -+ /** -+ * ForgotPassword constructor. -+ * -+ * @param \Magento\Customer\Api\CustomerRepositoryInterface $customerRepository -+ * @param \Magento\Framework\Api\SearchCriteriaBuilder $searchCriteriaBuilder -+ */ -+ public function __construct( -+ CustomerRepositoryInterface $customerRepository, -+ SearchCriteriaBuilder $searchCriteriaBuilder -+ ) { -+ $this->customerRepository = $customerRepository; -+ $this->searchCriteriaBuilder = $searchCriteriaBuilder; -+ } -+ -+ /** -+ * Get customer by rp_token -+ * -+ * @param string $resetPasswordToken -+ * -+ * @return \Magento\Customer\Api\Data\CustomerInterface -+ * @throws ExpiredException -+ * @throws NoSuchEntityException -+ * @throws \Magento\Framework\Exception\LocalizedException -+ */ -+ public function execute(string $resetPasswordToken):CustomerInterface -+ { -+ $this->searchCriteriaBuilder->addFilter( -+ 'rp_token', -+ $resetPasswordToken -+ ); -+ $this->searchCriteriaBuilder->setPageSize(1); -+ $found = $this->customerRepository->getList( -+ $this->searchCriteriaBuilder->create() -+ ); -+ -+ if ($found->getTotalCount() > 1) { -+ //Failed to generated unique RP token -+ throw new ExpiredException( -+ new Phrase('Reset password token expired.') -+ ); -+ } -+ if ($found->getTotalCount() === 0) { -+ //Customer with such token not found. -+ new NoSuchEntityException( -+ new Phrase( -+ 'No such entity with rp_token = %value', -+ [ -+ 'value' => $resetPasswordToken -+ ] -+ ) -+ ); -+ } -+ -+ //Unique customer found. -+ return $found->getItems()[0]; -+ } -+} -diff --git a/app/code/Magento/Customer/Model/GroupManagement.php b/app/code/Magento/Customer/Model/GroupManagement.php -index 47d7d7ad1ac..48cb5d55061 100644 ---- a/app/code/Magento/Customer/Model/GroupManagement.php -+++ b/app/code/Magento/Customer/Model/GroupManagement.php -@@ -8,16 +8,19 @@ - namespace Magento\Customer\Model; - - use Magento\Customer\Api\Data\GroupInterface; -+use Magento\Customer\Api\Data\GroupInterfaceFactory; -+use Magento\Customer\Api\GroupRepositoryInterface; -+use Magento\Framework\Api\FilterBuilder; - use Magento\Framework\Api\SearchCriteriaBuilder; -+use Magento\Framework\Api\SortOrderBuilder; - use Magento\Framework\App\Config\ScopeConfigInterface; --use Magento\Framework\Api\FilterBuilder; -+use Magento\Framework\App\ObjectManager; - use Magento\Framework\Exception\NoSuchEntityException; - use Magento\Store\Model\StoreManagerInterface; --use Magento\Customer\Api\GroupRepositoryInterface; --use Magento\Customer\Api\Data\GroupInterfaceFactory; --use Magento\Customer\Model\GroupFactory; - - /** -+ * The class contains methods for getting information about a customer group -+ * - * @SuppressWarnings(PHPMD.CouplingBetweenObjects) - */ - class GroupManagement implements \Magento\Customer\Api\GroupManagementInterface -@@ -65,6 +68,11 @@ class GroupManagement implements \Magento\Customer\Api\GroupManagementInterface - */ - protected $filterBuilder; - -+ /** -+ * @var SortOrderBuilder -+ */ -+ private $sortOrderBuilder; -+ - /** - * @param StoreManagerInterface $storeManager - * @param ScopeConfigInterface $scopeConfig -@@ -73,6 +81,7 @@ class GroupManagement implements \Magento\Customer\Api\GroupManagementInterface - * @param GroupInterfaceFactory $groupDataFactory - * @param SearchCriteriaBuilder $searchCriteriaBuilder - * @param FilterBuilder $filterBuilder -+ * @param SortOrderBuilder $sortOrderBuilder - */ - public function __construct( - StoreManagerInterface $storeManager, -@@ -81,7 +90,8 @@ class GroupManagement implements \Magento\Customer\Api\GroupManagementInterface - GroupRepositoryInterface $groupRepository, - GroupInterfaceFactory $groupDataFactory, - SearchCriteriaBuilder $searchCriteriaBuilder, -- FilterBuilder $filterBuilder -+ FilterBuilder $filterBuilder, -+ SortOrderBuilder $sortOrderBuilder = null - ) { - $this->storeManager = $storeManager; - $this->scopeConfig = $scopeConfig; -@@ -90,10 +100,12 @@ class GroupManagement implements \Magento\Customer\Api\GroupManagementInterface - $this->groupDataFactory = $groupDataFactory; - $this->searchCriteriaBuilder = $searchCriteriaBuilder; - $this->filterBuilder = $filterBuilder; -+ $this->sortOrderBuilder = $sortOrderBuilder ?: ObjectManager::getInstance() -+ ->get(SortOrderBuilder::class); - } - - /** -- * {@inheritdoc} -+ * @inheritdoc - */ - public function isReadonly($groupId) - { -@@ -107,7 +119,7 @@ class GroupManagement implements \Magento\Customer\Api\GroupManagementInterface - } - - /** -- * {@inheritdoc} -+ * @inheritdoc - */ - public function getDefaultGroup($storeId = null) - { -@@ -133,7 +145,7 @@ class GroupManagement implements \Magento\Customer\Api\GroupManagementInterface - } - - /** -- * {@inheritdoc} -+ * @inheritdoc - */ - public function getNotLoggedInGroup() - { -@@ -141,7 +153,7 @@ class GroupManagement implements \Magento\Customer\Api\GroupManagementInterface - } - - /** -- * {@inheritdoc} -+ * @inheritdoc - */ - public function getLoggedInGroups() - { -@@ -155,15 +167,20 @@ class GroupManagement implements \Magento\Customer\Api\GroupManagementInterface - ->setConditionType('neq') - ->setValue(self::CUST_GROUP_ALL) - ->create(); -+ $groupNameSortOrder = $this->sortOrderBuilder -+ ->setField('customer_group_code') -+ ->setAscendingDirection() -+ ->create(); - $searchCriteria = $this->searchCriteriaBuilder - ->addFilters($notLoggedInFilter) - ->addFilters($groupAll) -+ ->addSortOrder($groupNameSortOrder) - ->create(); - return $this->groupRepository->getList($searchCriteria)->getItems(); - } - - /** -- * {@inheritdoc} -+ * @inheritdoc - */ - public function getAllCustomersGroup() - { -diff --git a/app/code/Magento/Customer/Model/Indexer/CustomerGroupDimensionProvider.php b/app/code/Magento/Customer/Model/Indexer/CustomerGroupDimensionProvider.php -index d4e0c5cce34..336e7ab770b 100644 ---- a/app/code/Magento/Customer/Model/Indexer/CustomerGroupDimensionProvider.php -+++ b/app/code/Magento/Customer/Model/Indexer/CustomerGroupDimensionProvider.php -@@ -11,6 +11,9 @@ use Magento\Customer\Model\ResourceModel\Group\CollectionFactory as CustomerGrou - use Magento\Framework\Indexer\DimensionFactory; - use Magento\Framework\Indexer\DimensionProviderInterface; - -+/** -+ * Class CustomerGroupDimensionProvider -+ */ - class CustomerGroupDimensionProvider implements DimensionProviderInterface - { - /** -@@ -34,12 +37,19 @@ class CustomerGroupDimensionProvider implements DimensionProviderInterface - */ - private $dimensionFactory; - -+ /** -+ * @param CustomerGroupCollectionFactory $collectionFactory -+ * @param DimensionFactory $dimensionFactory -+ */ - public function __construct(CustomerGroupCollectionFactory $collectionFactory, DimensionFactory $dimensionFactory) - { - $this->dimensionFactory = $dimensionFactory; - $this->collectionFactory = $collectionFactory; - } - -+ /** -+ * @inheritdoc -+ */ - public function getIterator(): \Traversable - { - foreach ($this->getCustomerGroups() as $customerGroup) { -@@ -48,6 +58,8 @@ class CustomerGroupDimensionProvider implements DimensionProviderInterface - } - - /** -+ * Get Customer Groups -+ * - * @return array - */ - private function getCustomerGroups(): array -diff --git a/app/code/Magento/Customer/Model/Indexer/Source.php b/app/code/Magento/Customer/Model/Indexer/Source.php -index e4bf03e08a9..a8878e2084e 100644 ---- a/app/code/Magento/Customer/Model/Indexer/Source.php -+++ b/app/code/Magento/Customer/Model/Indexer/Source.php -@@ -5,6 +5,7 @@ - */ - namespace Magento\Customer\Model\Indexer; - -+use Magento\Customer\Model\ResourceModel\Customer\Indexer\CollectionFactory; - use Magento\Customer\Model\ResourceModel\Customer\Indexer\Collection; - use Magento\Framework\App\ResourceConnection\SourceProviderInterface; - use Traversable; -@@ -25,11 +26,11 @@ class Source implements \IteratorAggregate, \Countable, SourceProviderInterface - private $batchSize; - - /** -- * @param \Magento\Customer\Model\ResourceModel\Customer\Indexer\CollectionFactory $collection -+ * @param CollectionFactory $collectionFactory - * @param int $batchSize - */ - public function __construct( -- \Magento\Customer\Model\ResourceModel\Customer\Indexer\CollectionFactory $collectionFactory, -+ CollectionFactory $collectionFactory, - $batchSize = 10000 - ) { - $this->customerCollection = $collectionFactory->create(); -@@ -37,7 +38,7 @@ class Source implements \IteratorAggregate, \Countable, SourceProviderInterface - } - - /** -- * {@inheritdoc} -+ * @inheritdoc - */ - public function getMainTable() - { -@@ -45,7 +46,7 @@ class Source implements \IteratorAggregate, \Countable, SourceProviderInterface - } - - /** -- * {@inheritdoc} -+ * @inheritdoc - */ - public function getIdFieldName() - { -@@ -53,7 +54,7 @@ class Source implements \IteratorAggregate, \Countable, SourceProviderInterface - } - - /** -- * {@inheritdoc} -+ * @inheritdoc - */ - public function addFieldToSelect($fieldName, $alias = null) - { -@@ -62,7 +63,7 @@ class Source implements \IteratorAggregate, \Countable, SourceProviderInterface - } - - /** -- * {@inheritdoc} -+ * @inheritdoc - */ - public function getSelect() - { -@@ -70,7 +71,7 @@ class Source implements \IteratorAggregate, \Countable, SourceProviderInterface - } - - /** -- * {@inheritdoc} -+ * @inheritdoc - */ - public function addFieldToFilter($attribute, $condition = null) - { -@@ -79,7 +80,7 @@ class Source implements \IteratorAggregate, \Countable, SourceProviderInterface - } - - /** -- * @return int -+ * @inheritdoc - */ - public function count() - { -@@ -105,4 +106,28 @@ class Source implements \IteratorAggregate, \Countable, SourceProviderInterface - $pageNumber++; - } while ($pageNumber <= $lastPage); - } -+ -+ /** -+ * Joins Attribute -+ * -+ * @param string $alias alias for the joined attribute -+ * @param string|\Magento\Eav\Model\Entity\Attribute\AbstractAttribute $attribute -+ * @param string $bind attribute of the main entity to link with joined $filter -+ * @param string|null $filter primary key for the joined entity (entity_id default) -+ * @param string $joinType inner|left -+ * @param int|null $storeId -+ * @return void -+ * @throws \Magento\Framework\Exception\LocalizedException -+ * @see Collection::joinAttribute() -+ */ -+ public function joinAttribute( -+ string $alias, -+ $attribute, -+ string $bind, -+ ?string $filter = null, -+ string $joinType = 'inner', -+ ?int $storeId = null -+ ): void { -+ $this->customerCollection->joinAttribute($alias, $attribute, $bind, $filter, $joinType, $storeId); -+ } - } -diff --git a/app/code/Magento/Customer/Model/Metadata/AttributeMetadataCache.php b/app/code/Magento/Customer/Model/Metadata/AttributeMetadataCache.php -index 5a46fdb9def..8e64fba4a9b 100644 ---- a/app/code/Magento/Customer/Model/Metadata/AttributeMetadataCache.php -+++ b/app/code/Magento/Customer/Model/Metadata/AttributeMetadataCache.php -@@ -12,6 +12,7 @@ use Magento\Eav\Model\Entity\Attribute; - use Magento\Framework\App\Cache\StateInterface; - use Magento\Framework\App\CacheInterface; - use Magento\Framework\Serialize\SerializerInterface; -+use Magento\Store\Model\StoreManagerInterface; - - /** - * Cache for attribute metadata -@@ -53,6 +54,11 @@ class AttributeMetadataCache - */ - private $serializer; - -+ /** -+ * @var StoreManagerInterface -+ */ -+ private $storeManager; -+ - /** - * Constructor - * -@@ -60,17 +66,21 @@ class AttributeMetadataCache - * @param StateInterface $state - * @param SerializerInterface $serializer - * @param AttributeMetadataHydrator $attributeMetadataHydrator -+ * @param StoreManagerInterface $storeManager - */ - public function __construct( - CacheInterface $cache, - StateInterface $state, - SerializerInterface $serializer, -- AttributeMetadataHydrator $attributeMetadataHydrator -+ AttributeMetadataHydrator $attributeMetadataHydrator, -+ StoreManagerInterface $storeManager = null - ) { - $this->cache = $cache; - $this->state = $state; - $this->serializer = $serializer; - $this->attributeMetadataHydrator = $attributeMetadataHydrator; -+ $this->storeManager = $storeManager ?: \Magento\Framework\App\ObjectManager::getInstance() -+ ->get(StoreManagerInterface::class); - } - - /** -@@ -82,11 +92,12 @@ class AttributeMetadataCache - */ - public function load($entityType, $suffix = '') - { -- if (isset($this->attributes[$entityType . $suffix])) { -- return $this->attributes[$entityType . $suffix]; -+ $storeId = $this->storeManager->getStore()->getId(); -+ if (isset($this->attributes[$entityType . $suffix . $storeId])) { -+ return $this->attributes[$entityType . $suffix . $storeId]; - } - if ($this->isEnabled()) { -- $cacheKey = self::ATTRIBUTE_METADATA_CACHE_PREFIX . $entityType . $suffix; -+ $cacheKey = self::ATTRIBUTE_METADATA_CACHE_PREFIX . $entityType . $suffix . $storeId; - $serializedData = $this->cache->load($cacheKey); - if ($serializedData) { - $attributesData = $this->serializer->unserialize($serializedData); -@@ -94,7 +105,7 @@ class AttributeMetadataCache - foreach ($attributesData as $key => $attributeData) { - $attributes[$key] = $this->attributeMetadataHydrator->hydrate($attributeData); - } -- $this->attributes[$entityType . $suffix] = $attributes; -+ $this->attributes[$entityType . $suffix . $storeId] = $attributes; - return $attributes; - } - } -@@ -111,9 +122,10 @@ class AttributeMetadataCache - */ - public function save($entityType, array $attributes, $suffix = '') - { -- $this->attributes[$entityType . $suffix] = $attributes; -+ $storeId = $this->storeManager->getStore()->getId(); -+ $this->attributes[$entityType . $suffix . $storeId] = $attributes; - if ($this->isEnabled()) { -- $cacheKey = self::ATTRIBUTE_METADATA_CACHE_PREFIX . $entityType . $suffix; -+ $cacheKey = self::ATTRIBUTE_METADATA_CACHE_PREFIX . $entityType . $suffix . $storeId; - $attributesData = []; - foreach ($attributes as $key => $attribute) { - $attributesData[$key] = $this->attributeMetadataHydrator->extract($attribute); -diff --git a/app/code/Magento/Customer/Model/Metadata/CustomerMetadata.php b/app/code/Magento/Customer/Model/Metadata/CustomerMetadata.php -index 7ed806e657e..38f3fbcbdbd 100644 ---- a/app/code/Magento/Customer/Model/Metadata/CustomerMetadata.php -+++ b/app/code/Magento/Customer/Model/Metadata/CustomerMetadata.php -@@ -33,20 +33,30 @@ class CustomerMetadata implements CustomerMetadataInterface - */ - private $attributeMetadataDataProvider; - -+ /** -+ * List of system attributes which should be available to the clients. -+ * -+ * @var string[] -+ */ -+ private $systemAttributes; -+ - /** - * @param AttributeMetadataConverter $attributeMetadataConverter - * @param AttributeMetadataDataProvider $attributeMetadataDataProvider -+ * @param string[] $systemAttributes - */ - public function __construct( - AttributeMetadataConverter $attributeMetadataConverter, -- AttributeMetadataDataProvider $attributeMetadataDataProvider -+ AttributeMetadataDataProvider $attributeMetadataDataProvider, -+ array $systemAttributes = [] - ) { - $this->attributeMetadataConverter = $attributeMetadataConverter; - $this->attributeMetadataDataProvider = $attributeMetadataDataProvider; -+ $this->systemAttributes = $systemAttributes; - } - - /** -- * {@inheritdoc} -+ * @inheritdoc - */ - public function getAttributes($formCode) - { -@@ -67,7 +77,7 @@ class CustomerMetadata implements CustomerMetadataInterface - } - - /** -- * {@inheritdoc} -+ * @inheritdoc - */ - public function getAttributeMetadata($attributeCode) - { -@@ -92,7 +102,7 @@ class CustomerMetadata implements CustomerMetadataInterface - } - - /** -- * {@inheritdoc} -+ * @inheritdoc - */ - public function getAllAttributesMetadata() - { -@@ -116,7 +126,7 @@ class CustomerMetadata implements CustomerMetadataInterface - } - - /** -- * {@inheritdoc} -+ * @inheritdoc - */ - public function getCustomAttributesMetadata($dataObjectClassName = self::DATA_INTERFACE_NAME) - { -@@ -134,9 +144,10 @@ class CustomerMetadata implements CustomerMetadataInterface - $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 168f00be16e..8e443e93354 100644 ---- a/app/code/Magento/Customer/Model/Metadata/Form/AbstractData.php -+++ b/app/code/Magento/Customer/Model/Metadata/Form/AbstractData.php -@@ -12,6 +12,8 @@ use Magento\Framework\Api\ArrayObjectSearch; - use Magento\Framework\Validator\EmailAddress; - - /** -+ * Form Element Abstract Data Model -+ * - * @SuppressWarnings(PHPMD.CouplingBetweenObjects) - */ - abstract class AbstractData -@@ -137,6 +139,7 @@ abstract class AbstractData - - /** - * Set scope visibility -+ * - * Search value only in scope or search value in scope and global - * - * @param boolean $flag -@@ -281,9 +284,14 @@ abstract class AbstractData - ); - - if ($inputValidation !== null) { -+ $allowWhiteSpace = false; -+ - switch ($inputValidation) { -+ case 'alphanum-with-spaces': -+ $allowWhiteSpace = true; -+ // Continue to alphanumeric validation - case 'alphanumeric': -- $validator = new \Zend_Validate_Alnum(true); -+ $validator = new \Zend_Validate_Alnum($allowWhiteSpace); - $validator->setMessage(__('"%1" invalid type entered.', $label), \Zend_Validate_Alnum::INVALID); - $validator->setMessage( - __('"%1" contains non-alphabetic or non-numeric characters.', $label), -diff --git a/app/code/Magento/Customer/Model/Metadata/Form/Date.php b/app/code/Magento/Customer/Model/Metadata/Form/Date.php -index b27f6627439..6f14b2e6f1d 100644 ---- a/app/code/Magento/Customer/Model/Metadata/Form/Date.php -+++ b/app/code/Magento/Customer/Model/Metadata/Form/Date.php -@@ -12,7 +12,7 @@ use Magento\Framework\Api\ArrayObjectSearch; - class Date extends AbstractData - { - /** -- * {@inheritdoc} -+ * @inheritdoc - */ - public function extractValue(\Magento\Framework\App\RequestInterface $request) - { -@@ -21,7 +21,7 @@ class Date extends AbstractData - } - - /** -- * {@inheritdoc} -+ * @inheritdoc - * @SuppressWarnings(PHPMD.CyclomaticComplexity) - * @SuppressWarnings(PHPMD.NPathComplexity) - */ -@@ -95,21 +95,15 @@ class Date extends AbstractData - } - - /** -- * {@inheritdoc} -+ * @inheritdoc - */ - public function compactValue($value) - { -- if ($value !== false) { -- if (empty($value)) { -- $value = null; -- } -- return $value; -- } -- return false; -+ return $value; - } - - /** -- * {@inheritdoc} -+ * @inheritdoc - */ - public function restoreValue($value) - { -@@ -117,7 +111,7 @@ class Date extends AbstractData - } - - /** -- * {@inheritdoc} -+ * @inheritdoc - */ - public function outputValue($format = \Magento\Customer\Model\Metadata\ElementFactory::OUTPUT_FORMAT_TEXT) - { -diff --git a/app/code/Magento/Customer/Model/Metadata/Form/File.php b/app/code/Magento/Customer/Model/Metadata/Form/File.php -index e6e9c2b50c0..227e85ed98f 100644 ---- a/app/code/Magento/Customer/Model/Metadata/Form/File.php -+++ b/app/code/Magento/Customer/Model/Metadata/Form/File.php -@@ -15,6 +15,8 @@ use Magento\Framework\File\UploaderFactory; - use Magento\Framework\Filesystem; - - /** -+ * Processes files that are save for customer. -+ * - * @SuppressWarnings(PHPMD.CouplingBetweenObjects) - */ - class File extends AbstractData -@@ -66,7 +68,7 @@ class File extends AbstractData - * @param \Psr\Log\LoggerInterface $logger - * @param \Magento\Customer\Api\Data\AttributeMetadataInterface $attribute - * @param \Magento\Framework\Locale\ResolverInterface $localeResolver -- * @param null $value -+ * @param string|array $value - * @param string $entityTypeCode - * @param bool $isAjax - * @param \Magento\Framework\Url\EncoderInterface $urlEncoder -@@ -101,7 +103,7 @@ class File extends AbstractData - } - - /** -- * {@inheritdoc} -+ * @inheritdoc - * @SuppressWarnings(PHPMD.CyclomaticComplexity) - */ - public function extractValue(\Magento\Framework\App\RequestInterface $request) -@@ -109,7 +111,7 @@ class File extends AbstractData - $extend = $this->_getRequestValue($request); - - $attrCode = $this->getAttribute()->getAttributeCode(); -- if ($this->_requestScope) { -+ if ($this->_requestScope || !isset($_FILES[$attrCode])) { - $value = []; - if (strpos($this->_requestScope, '/') !== false) { - $scopes = explode('/', $this->_requestScope); -@@ -160,8 +162,7 @@ class File extends AbstractData - } - - /** -- * Validate file by attribute validate rules -- * Return array of errors -+ * Validate file by attribute validate rules. Returns array of errors. - * - * @param array $value - * @return string[] -@@ -232,7 +233,7 @@ class File extends AbstractData - } - - /** -- * {@inheritdoc} -+ * @inheritdoc - * @SuppressWarnings(PHPMD.CyclomaticComplexity) - * @SuppressWarnings(PHPMD.NPathComplexity) - */ -@@ -273,7 +274,7 @@ class File extends AbstractData - } - - /** -- * {@inheritdoc} -+ * @inheritdoc - * - * @return ImageContentInterface|array|string|null - */ -@@ -358,7 +359,7 @@ class File extends AbstractData - } - - /** -- * {@inheritdoc} -+ * @inheritdoc - */ - public function restoreValue($value) - { -@@ -366,7 +367,7 @@ class File extends AbstractData - } - - /** -- * {@inheritdoc} -+ * @inheritdoc - */ - public function outputValue($format = \Magento\Customer\Model\Metadata\ElementFactory::OUTPUT_FORMAT_TEXT) - { -diff --git a/app/code/Magento/Customer/Model/Metadata/Form/Image.php b/app/code/Magento/Customer/Model/Metadata/Form/Image.php -index 2104f941a6b..33bdf827f80 100644 ---- a/app/code/Magento/Customer/Model/Metadata/Form/Image.php -+++ b/app/code/Magento/Customer/Model/Metadata/Form/Image.php -@@ -16,6 +16,8 @@ use Magento\Framework\File\UploaderFactory; - use Magento\Framework\Filesystem; - - /** -+ * Metadata for form image field -+ * - * @SuppressWarnings(PHPMD.CouplingBetweenObjects) - */ - class Image extends File -@@ -32,7 +34,7 @@ class Image extends File - * @param \Psr\Log\LoggerInterface $logger - * @param \Magento\Customer\Api\Data\AttributeMetadataInterface $attribute - * @param \Magento\Framework\Locale\ResolverInterface $localeResolver -- * @param null $value -+ * @param null|string $value - * @param string $entityTypeCode - * @param bool $isAjax - * @param \Magento\Framework\Url\EncoderInterface $urlEncoder -@@ -78,6 +80,7 @@ class Image extends File - - /** - * Validate file by attribute validate rules -+ * - * Return array of errors - * - * @param array $value -@@ -133,7 +136,7 @@ class Image extends File - - $maxImageHeight = ArrayObjectSearch::getArrayElementByName( - $rules, -- 'max_image_heght' -+ 'max_image_height' - ); - if ($maxImageHeight !== null) { - if ($maxImageHeight < $imageProp[1]) { -diff --git a/app/code/Magento/Customer/Model/Metadata/Form/Text.php b/app/code/Magento/Customer/Model/Metadata/Form/Text.php -index c8b9a1e46a1..c639b607e27 100644 ---- a/app/code/Magento/Customer/Model/Metadata/Form/Text.php -+++ b/app/code/Magento/Customer/Model/Metadata/Form/Text.php -@@ -11,6 +11,9 @@ namespace Magento\Customer\Model\Metadata\Form; - use Magento\Customer\Api\Data\AttributeMetadataInterface; - use Magento\Framework\Api\ArrayObjectSearch; - -+/** -+ * Form Text metadata -+ */ - class Text extends AbstractData - { - /** -@@ -52,8 +55,6 @@ class Text extends AbstractData - - /** - * @inheritdoc -- * @SuppressWarnings(PHPMD.CyclomaticComplexity) -- * @SuppressWarnings(PHPMD.NPathComplexity) - */ - public function validateValue($value) - { -@@ -66,12 +67,12 @@ class Text extends AbstractData - $value = $this->_value; - } - -- if ($attribute->isRequired() && empty($value) && $value !== '0') { -- $errors[] = __('"%1" is a required value.', $label); -+ if (!$attribute->isRequired() && empty($value)) { -+ return true; - } - -- if (!$errors && !$attribute->isRequired() && empty($value)) { -- return true; -+ if (empty($value) && $value !== '0') { -+ $errors[] = __('"%1" is a required value.', $label); - } - - $errors = $this->validateLength($value, $attribute, $errors); -@@ -80,6 +81,7 @@ class Text extends AbstractData - if ($result !== true) { - $errors = array_merge($errors, $result); - } -+ - if (count($errors) == 0) { - return true; - } -diff --git a/app/code/Magento/Customer/Model/Options.php b/app/code/Magento/Customer/Model/Options.php -index 7747e309d82..71e70f8e142 100644 ---- a/app/code/Magento/Customer/Model/Options.php -+++ b/app/code/Magento/Customer/Model/Options.php -@@ -8,7 +8,11 @@ namespace Magento\Customer\Model; - use Magento\Config\Model\Config\Source\Nooptreq as NooptreqSource; - use Magento\Customer\Helper\Address as AddressHelper; - use Magento\Framework\Escaper; -+use Magento\Store\Api\Data\StoreInterface; - -+/** -+ * Customer Options. -+ */ - class Options - { - /** -@@ -38,7 +42,7 @@ class Options - /** - * Retrieve name prefix dropdown options - * -- * @param null $store -+ * @param null|string|bool|int|StoreInterface $store - * @return array|bool - */ - public function getNamePrefixOptions($store = null) -@@ -52,7 +56,7 @@ class Options - /** - * Retrieve name suffix dropdown options - * -- * @param null $store -+ * @param null|string|bool|int|StoreInterface $store - * @return array|bool - */ - public function getNameSuffixOptions($store = null) -@@ -64,7 +68,9 @@ class Options - } - - /** -- * @param $options -+ * Unserialize and clear name prefix or suffix options. -+ * -+ * @param string $options - * @param bool $isOptional - * @return array|bool - * -@@ -78,6 +84,7 @@ class Options - - /** - * Unserialize and clear name prefix or suffix options -+ * - * If field is optional, add an empty first option. - * - * @param string $options -@@ -91,7 +98,7 @@ class Options - return false; - } - $result = []; -- $options = explode(';', $options); -+ $options = array_filter(explode(';', $options)); - foreach ($options as $value) { - $value = $this->escaper->escapeHtml(trim($value)); - $result[$value] = $value; -diff --git a/app/code/Magento/Customer/Model/Renderer/Region.php b/app/code/Magento/Customer/Model/Renderer/Region.php -index 5c7fcd38d6c..a26cfb96fe0 100644 ---- a/app/code/Magento/Customer/Model/Renderer/Region.php -+++ b/app/code/Magento/Customer/Model/Renderer/Region.php -@@ -54,6 +54,8 @@ class Region implements \Magento\Framework\Data\Form\Element\Renderer\RendererIn - } - - /** -+ * Render element -+ * - * @param AbstractElement $element - * @return string - * @SuppressWarnings(PHPMD.CyclomaticComplexity) -@@ -80,7 +82,7 @@ class Region implements \Magento\Framework\Data\Form\Element\Renderer\RendererIn - $regionCollection = self::$_regionCollections[$countryId]; - } - -- $regionId = intval($element->getForm()->getElement('region_id')->getValue()); -+ $regionId = (int)$element->getForm()->getElement('region_id')->getValue(); - - $htmlAttributes = $element->getHtmlAttributes(); - foreach ($htmlAttributes as $key => $attribute) { -diff --git a/app/code/Magento/Customer/Model/ResourceModel/Address.php b/app/code/Magento/Customer/Model/ResourceModel/Address.php -index a52c3723108..200eaabe651 100644 ---- a/app/code/Magento/Customer/Model/ResourceModel/Address.php -+++ b/app/code/Magento/Customer/Model/ResourceModel/Address.php -@@ -14,6 +14,7 @@ use Magento\Framework\App\ObjectManager; - - /** - * Class Address -+ * - * @package Magento\Customer\Model\ResourceModel - * @SuppressWarnings(PHPMD.CouplingBetweenObjects) - */ -@@ -31,8 +32,8 @@ class Address extends \Magento\Eav\Model\Entity\VersionControl\AbstractEntity - - /** - * @param \Magento\Eav\Model\Entity\Context $context -- * @param \Magento\Framework\Model\ResourceModel\Db\VersionControl\Snapshot $entitySnapshot, -- * @param \Magento\Framework\Model\ResourceModel\Db\VersionControl\RelationComposite $entityRelationComposite, -+ * @param \Magento\Framework\Model\ResourceModel\Db\VersionControl\Snapshot $entitySnapshot -+ * @param \Magento\Framework\Model\ResourceModel\Db\VersionControl\RelationComposite $entityRelationComposite - * @param \Magento\Framework\Validator\Factory $validatorFactory - * @param \Magento\Customer\Api\CustomerRepositoryInterface $customerRepository - * @param array $data -@@ -98,6 +99,9 @@ class Address extends \Magento\Eav\Model\Entity\VersionControl\AbstractEntity - */ - protected function _validate($address) - { -+ if ($address->getDataByKey('should_ignore_validation')) { -+ return; -+ }; - $validator = $this->_validatorFactory->createValidator('customer_address', 'save'); - - if (!$validator->isValid($address)) { -@@ -110,7 +114,7 @@ class Address extends \Magento\Eav\Model\Entity\VersionControl\AbstractEntity - } - - /** -- * {@inheritdoc} -+ * @inheritdoc - */ - public function delete($object) - { -@@ -120,6 +124,8 @@ class Address extends \Magento\Eav\Model\Entity\VersionControl\AbstractEntity - } - - /** -+ * Get instance of DeleteRelation class -+ * - * @deprecated 100.2.0 - * @return DeleteRelation - */ -@@ -129,6 +135,8 @@ class Address extends \Magento\Eav\Model\Entity\VersionControl\AbstractEntity - } - - /** -+ * Get instance of CustomerRegistry class -+ * - * @deprecated 100.2.0 - * @return CustomerRegistry - */ -@@ -138,6 +146,8 @@ class Address extends \Magento\Eav\Model\Entity\VersionControl\AbstractEntity - } - - /** -+ * After delete entity process -+ * - * @param \Magento\Customer\Model\Address $address - * @return $this - */ -diff --git a/app/code/Magento/Customer/Model/ResourceModel/Address/Grid/Collection.php b/app/code/Magento/Customer/Model/ResourceModel/Address/Grid/Collection.php -new file mode 100644 -index 00000000000..4e034705908 ---- /dev/null -+++ b/app/code/Magento/Customer/Model/ResourceModel/Address/Grid/Collection.php -@@ -0,0 +1,143 @@ -+<?php -+declare(strict_types=1); -+/** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+namespace Magento\Customer\Model\ResourceModel\Address\Grid; -+ -+use Magento\Framework\Api\Search\SearchResultInterface; -+use Magento\Framework\Api\Search\AggregationInterface; -+use Magento\Framework\Model\ResourceModel\Db\Collection\AbstractCollection; -+ -+/** -+ * Class getting collection of addresses assigned to customer -+ */ -+class Collection extends AbstractCollection implements SearchResultInterface -+{ -+ /** -+ * @var AggregationInterface -+ */ -+ private $aggregations; -+ -+ /** -+ * @param \Magento\Framework\Data\Collection\EntityFactoryInterface $entityFactory -+ * @param \Psr\Log\LoggerInterface $logger -+ * @param \Magento\Framework\Data\Collection\Db\FetchStrategyInterface $fetchStrategy -+ * @param \Magento\Framework\Event\ManagerInterface $eventManager -+ * @param string $mainTable -+ * @param string $eventPrefix -+ * @param string $eventObject -+ * @param string $resourceModel -+ * @param string $model -+ * @param \Magento\Framework\DB\Adapter\AdapterInterface|string|null $connection -+ * @param \Magento\Framework\Model\ResourceModel\Db\AbstractDb $resource -+ * -+ * @SuppressWarnings(PHPMD.ExcessiveParameterList) -+ */ -+ public function __construct( -+ \Magento\Framework\Data\Collection\EntityFactoryInterface $entityFactory, -+ \Psr\Log\LoggerInterface $logger, -+ \Magento\Framework\Data\Collection\Db\FetchStrategyInterface $fetchStrategy, -+ \Magento\Framework\Event\ManagerInterface $eventManager, -+ $mainTable, -+ $eventPrefix, -+ $eventObject, -+ $resourceModel, -+ $model = \Magento\Framework\View\Element\UiComponent\DataProvider\Document::class, -+ $connection = null, -+ \Magento\Framework\Model\ResourceModel\Db\AbstractDb $resource = null -+ ) { -+ $this->_eventPrefix = $eventPrefix; -+ $this->_eventObject = $eventObject; -+ $this->_init($model, $resourceModel); -+ $this->setMainTable($mainTable); -+ $this->_idFieldName = 'entity_id'; -+ parent::__construct( -+ $entityFactory, -+ $logger, -+ $fetchStrategy, -+ $eventManager, -+ $connection, -+ $resource -+ ); -+ } -+ -+ /** -+ * @inheritdoc -+ * -+ * @return AggregationInterface -+ */ -+ public function getAggregations() -+ { -+ return $this->aggregations; -+ } -+ -+ /** -+ * @inheritdoc -+ * -+ * @param AggregationInterface $aggregations -+ * @return $this -+ */ -+ public function setAggregations($aggregations) -+ { -+ $this->aggregations = $aggregations; -+ return $this; -+ } -+ -+ /** -+ * Get search criteria. -+ * -+ * @return \Magento\Framework\Api\SearchCriteriaInterface|null -+ */ -+ public function getSearchCriteria() -+ { -+ return null; -+ } -+ -+ /** -+ * Set search criteria. -+ * -+ * @param \Magento\Framework\Api\SearchCriteriaInterface $searchCriteria -+ * @return $this -+ * @SuppressWarnings(PHPMD.UnusedFormalParameter) -+ */ -+ public function setSearchCriteria(\Magento\Framework\Api\SearchCriteriaInterface $searchCriteria = null) -+ { -+ return $this; -+ } -+ -+ /** -+ * Get total count. -+ * -+ * @return int -+ */ -+ public function getTotalCount() -+ { -+ return $this->getSize(); -+ } -+ -+ /** -+ * Set total count. -+ * -+ * @param int $totalCount -+ * @return $this -+ * @SuppressWarnings(PHPMD.UnusedFormalParameter) -+ */ -+ public function setTotalCount($totalCount) -+ { -+ return $this; -+ } -+ -+ /** -+ * Set items list. -+ * -+ * @param \Magento\Framework\Api\ExtensibleDataInterface[] $items -+ * @return $this -+ * @SuppressWarnings(PHPMD.UnusedFormalParameter) -+ */ -+ public function setItems(array $items = null) -+ { -+ return $this; -+ } -+} -diff --git a/app/code/Magento/Customer/Model/ResourceModel/Address/Relation.php b/app/code/Magento/Customer/Model/ResourceModel/Address/Relation.php -index d473a4dc018..ae342a1b10d 100644 ---- a/app/code/Magento/Customer/Model/ResourceModel/Address/Relation.php -+++ b/app/code/Magento/Customer/Model/ResourceModel/Address/Relation.php -@@ -7,6 +7,8 @@ - */ - namespace Magento\Customer\Model\ResourceModel\Address; - -+use Magento\Customer\Model\Address; -+use Magento\Customer\Model\Customer; - use Magento\Framework\Model\ResourceModel\Db\VersionControl\RelationInterface; - - /** -@@ -36,19 +38,14 @@ class Relation implements RelationInterface - public function processRelation(\Magento\Framework\Model\AbstractModel $object) - { - /** -- * @var $object \Magento\Customer\Model\Address -+ * @var $object Address - */ -- if (!$object->getIsCustomerSaveTransaction() && $this->isAddressDefault($object)) { -+ if (!$object->getIsCustomerSaveTransaction() && $object->getId()) { - $customer = $this->customerFactory->create()->load($object->getCustomerId()); -- $changedAddresses = []; -- -- if ($object->getIsDefaultBilling()) { -- $changedAddresses['default_billing'] = $object->getId(); -- } - -- if ($object->getIsDefaultShipping()) { -- $changedAddresses['default_shipping'] = $object->getId(); -- } -+ $changedAddresses = []; -+ $changedAddresses = $this->getDefaultBillingChangedAddress($object, $customer, $changedAddresses); -+ $changedAddresses = $this->getDefaultShippingChangedAddress($object, $customer, $changedAddresses); - - if ($changedAddresses) { - $customer->getResource()->getConnection()->update( -@@ -60,9 +57,62 @@ class Relation implements RelationInterface - } - } - -+ /** -+ * Get default billing changed address -+ * -+ * @param Address $object -+ * @param Customer $customer -+ * @param array $changedAddresses -+ * @return array -+ */ -+ private function getDefaultBillingChangedAddress( -+ Address $object, -+ Customer $customer, -+ array $changedAddresses -+ ): array { -+ if ($object->getIsDefaultBilling()) { -+ $changedAddresses['default_billing'] = $object->getId(); -+ } elseif ($customer->getDefaultBillingAddress() -+ && $object->getIsDefaultBilling() === false -+ && (int)$customer->getDefaultBillingAddress()->getId() === (int)$object->getId() -+ ) { -+ $changedAddresses['default_billing'] = null; -+ } -+ -+ return $changedAddresses; -+ } -+ -+ /** -+ * Get default shipping changed address -+ * -+ * @param Address $object -+ * @param Customer $customer -+ * @param array $changedAddresses -+ * @return array -+ */ -+ private function getDefaultShippingChangedAddress( -+ Address $object, -+ Customer $customer, -+ array $changedAddresses -+ ): array { -+ if ($object->getIsDefaultShipping()) { -+ $changedAddresses['default_shipping'] = $object->getId(); -+ } elseif ($customer->getDefaultShippingAddress() -+ && $object->getIsDefaultShipping() === false -+ && (int)$customer->getDefaultShippingAddress()->getId() === (int)$object->getId() -+ ) { -+ $changedAddresses['default_shipping'] = null; -+ } -+ -+ return $changedAddresses; -+ } -+ - /** - * Checks if address has chosen as default and has had an id - * -+ * @deprecated Is not used anymore due to changes in logic of save of address. -+ * If address was default and becomes not default than default address id for customer must be -+ * set to null - * @param \Magento\Framework\Model\AbstractModel $object - * @return bool - */ -diff --git a/app/code/Magento/Customer/Model/ResourceModel/AddressRepository.php b/app/code/Magento/Customer/Model/ResourceModel/AddressRepository.php -index 7f69ab3c02b..3fe61785de8 100644 ---- a/app/code/Magento/Customer/Model/ResourceModel/AddressRepository.php -+++ b/app/code/Magento/Customer/Model/ResourceModel/AddressRepository.php -@@ -7,6 +7,7 @@ - */ - namespace Magento\Customer\Model\ResourceModel; - -+use Magento\Customer\Api\Data\AddressInterface; - use Magento\Customer\Model\Address as CustomerAddressModel; - use Magento\Customer\Model\Customer as CustomerModel; - use Magento\Customer\Model\ResourceModel\Address\Collection; -@@ -16,6 +17,8 @@ use Magento\Framework\Api\SearchCriteriaInterface; - use Magento\Framework\Exception\InputException; - - /** -+ * Address repository. -+ * - * @SuppressWarnings(PHPMD.CouplingBetweenObjects) - */ - class AddressRepository implements \Magento\Customer\Api\AddressRepositoryInterface -@@ -123,6 +126,7 @@ class AddressRepository implements \Magento\Customer\Api\AddressRepositoryInterf - } else { - $addressModel->updateData($address); - } -+ $addressModel->setStoreId($customerModel->getStoreId()); - - $errors = $addressModel->validate(); - if ($errors !== true) { -@@ -143,6 +147,8 @@ class AddressRepository implements \Magento\Customer\Api\AddressRepositoryInterf - } - - /** -+ * Update address collection. -+ * - * @param Customer $customer - * @param Address $address - * @throws \Magento\Framework\Exception\LocalizedException -diff --git a/app/code/Magento/Customer/Model/ResourceModel/Customer.php b/app/code/Magento/Customer/Model/ResourceModel/Customer.php -index f5102015596..94196df6fe0 100644 ---- a/app/code/Magento/Customer/Model/ResourceModel/Customer.php -+++ b/app/code/Magento/Customer/Model/ResourceModel/Customer.php -@@ -95,9 +95,12 @@ class Customer extends \Magento\Eav\Model\Entity\VersionControl\AbstractEntity - /** - * Check customer scope, email and confirmation key before saving - * -- * @param \Magento\Framework\DataObject $customer -+ * @param \Magento\Framework\DataObject|\Magento\Customer\Api\Data\CustomerInterface $customer -+ * - * @return $this -- * @throws \Magento\Framework\Exception\LocalizedException -+ * @throws AlreadyExistsException -+ * @throws ValidatorException -+ * @throws \Magento\Framework\Exception\NoSuchEntityException - * @SuppressWarnings(PHPMD.CyclomaticComplexity) - * @SuppressWarnings(PHPMD.NPathComplexity) - */ -@@ -141,9 +144,7 @@ class Customer extends \Magento\Eav\Model\Entity\VersionControl\AbstractEntity - } - - // set confirmation key logic -- if ($customer->getForceConfirmed() || $customer->getPasswordHash() == '') { -- $customer->setConfirmation(null); -- } elseif (!$customer->getId() && $customer->isConfirmationRequired()) { -+ if (!$customer->getId() && $customer->isConfirmationRequired()) { - $customer->setConfirmation($customer->getRandomConfirmationKey()); - } - // remove customer confirmation key from database, if empty -@@ -151,7 +152,9 @@ class Customer extends \Magento\Eav\Model\Entity\VersionControl\AbstractEntity - $customer->setConfirmation(null); - } - -- $this->_validate($customer); -+ if (!$customer->getData('ignore_validation_flag')) { -+ $this->_validate($customer); -+ } - - return $this; - } -@@ -161,7 +164,7 @@ class Customer extends \Magento\Eav\Model\Entity\VersionControl\AbstractEntity - * - * @param \Magento\Customer\Model\Customer $customer - * @return void -- * @throws \Magento\Framework\Validator\Exception -+ * @throws ValidatorException - */ - protected function _validate($customer) - { -diff --git a/app/code/Magento/Customer/Model/ResourceModel/Customer/Relation.php b/app/code/Magento/Customer/Model/ResourceModel/Customer/Relation.php -index e55c5d443c9..96f47154e87 100644 ---- a/app/code/Magento/Customer/Model/ResourceModel/Customer/Relation.php -+++ b/app/code/Magento/Customer/Model/ResourceModel/Customer/Relation.php -@@ -23,41 +23,43 @@ class Relation implements \Magento\Framework\Model\ResourceModel\Db\VersionContr - $defaultBillingId = $customer->getData('default_billing'); - $defaultShippingId = $customer->getData('default_shipping'); - -- /** @var \Magento\Customer\Model\Address $address */ -- foreach ($customer->getAddresses() as $address) { -- if ($address->getData('_deleted')) { -- if ($address->getId() == $defaultBillingId) { -- $customer->setData('default_billing', null); -- } -+ if (!$customer->getData('ignore_validation_flag')) { -+ /** @var \Magento\Customer\Model\Address $address */ -+ foreach ($customer->getAddresses() as $address) { -+ if ($address->getData('_deleted')) { -+ if ($address->getId() == $defaultBillingId) { -+ $customer->setData('default_billing', null); -+ } - -- if ($address->getId() == $defaultShippingId) { -- $customer->setData('default_shipping', null); -- } -+ if ($address->getId() == $defaultShippingId) { -+ $customer->setData('default_shipping', null); -+ } - -- $removedAddressId = $address->getId(); -- $address->delete(); -+ $removedAddressId = $address->getId(); -+ $address->delete(); - -- // Remove deleted address from customer address collection -- $customer->getAddressesCollection()->removeItemByKey($removedAddressId); -- } else { -- $address->setParentId( -- $customer->getId() -- )->setStoreId( -- $customer->getStoreId() -- )->setIsCustomerSaveTransaction( -- true -- )->save(); -+ // Remove deleted address from customer address collection -+ $customer->getAddressesCollection()->removeItemByKey($removedAddressId); -+ } else { -+ $address->setParentId( -+ $customer->getId() -+ )->setStoreId( -+ $customer->getStoreId() -+ )->setIsCustomerSaveTransaction( -+ true -+ )->save(); - -- if (($address->getIsPrimaryBilling() || -- $address->getIsDefaultBilling()) && $address->getId() != $defaultBillingId -- ) { -- $customer->setData('default_billing', $address->getId()); -- } -+ if (($address->getIsPrimaryBilling() || -+ $address->getIsDefaultBilling()) && $address->getId() != $defaultBillingId -+ ) { -+ $customer->setData('default_billing', $address->getId()); -+ } - -- if (($address->getIsPrimaryShipping() || -- $address->getIsDefaultShipping()) && $address->getId() != $defaultShippingId -- ) { -- $customer->setData('default_shipping', $address->getId()); -+ if (($address->getIsPrimaryShipping() || -+ $address->getIsDefaultShipping()) && $address->getId() != $defaultShippingId -+ ) { -+ $customer->setData('default_shipping', $address->getId()); -+ } - } - } - } -diff --git a/app/code/Magento/Customer/Model/ResourceModel/CustomerRepository.php b/app/code/Magento/Customer/Model/ResourceModel/CustomerRepository.php -index 29e35c721a3..529b0e80697 100644 ---- a/app/code/Magento/Customer/Model/ResourceModel/CustomerRepository.php -+++ b/app/code/Magento/Customer/Model/ResourceModel/CustomerRepository.php -@@ -8,69 +8,80 @@ namespace Magento\Customer\Model\ResourceModel; - - use Magento\Customer\Api\CustomerMetadataInterface; - use Magento\Customer\Api\Data\CustomerInterface; --use Magento\Customer\Model\Delegation\Data\NewOperation; -+use Magento\Customer\Api\Data\CustomerSearchResultsInterfaceFactory; -+use Magento\Framework\Api\ExtensibleDataObjectConverter; -+use Magento\Framework\Api\ExtensionAttribute\JoinProcessorInterface; -+use Magento\Customer\Model\CustomerFactory; -+use Magento\Customer\Model\CustomerRegistry; -+use Magento\Customer\Model\Data\CustomerSecureFactory; - use Magento\Customer\Model\Customer\NotificationStorage; -+use Magento\Customer\Model\Delegation\Data\NewOperation; -+use Magento\Customer\Api\CustomerRepositoryInterface; - use Magento\Framework\Api\DataObjectHelper; - use Magento\Framework\Api\ImageProcessorInterface; - use Magento\Framework\Api\SearchCriteria\CollectionProcessorInterface; - use Magento\Framework\Api\SearchCriteriaInterface; -+use Magento\Framework\Api\Search\FilterGroup; -+use Magento\Framework\Event\ManagerInterface; - use Magento\Customer\Model\Delegation\Storage as DelegatedStorage; - use Magento\Framework\App\ObjectManager; -+use Magento\Store\Model\StoreManagerInterface; - - /** - * Customer repository. -+ * - * @SuppressWarnings(PHPMD.CouplingBetweenObjects) - * @SuppressWarnings(PHPMD.TooManyFields) - */ --class CustomerRepository implements \Magento\Customer\Api\CustomerRepositoryInterface -+class CustomerRepository implements CustomerRepositoryInterface - { - /** -- * @var \Magento\Customer\Model\CustomerFactory -+ * @var CustomerFactory - */ - protected $customerFactory; - - /** -- * @var \Magento\Customer\Model\Data\CustomerSecureFactory -+ * @var CustomerSecureFactory - */ - protected $customerSecureFactory; - - /** -- * @var \Magento\Customer\Model\CustomerRegistry -+ * @var CustomerRegistry - */ - protected $customerRegistry; - - /** -- * @var \Magento\Customer\Model\ResourceModel\AddressRepository -+ * @var AddressRepository - */ - protected $addressRepository; - - /** -- * @var \Magento\Customer\Model\ResourceModel\Customer -+ * @var Customer - */ - protected $customerResourceModel; - - /** -- * @var \Magento\Customer\Api\CustomerMetadataInterface -+ * @var CustomerMetadataInterface - */ - protected $customerMetadata; - - /** -- * @var \Magento\Customer\Api\Data\CustomerSearchResultsInterfaceFactory -+ * @var CustomerSearchResultsInterfaceFactory - */ - protected $searchResultsFactory; - - /** -- * @var \Magento\Framework\Event\ManagerInterface -+ * @var ManagerInterface - */ - protected $eventManager; - - /** -- * @var \Magento\Store\Model\StoreManagerInterface -+ * @var StoreManagerInterface - */ - protected $storeManager; - - /** -- * @var \Magento\Framework\Api\ExtensibleDataObjectConverter -+ * @var ExtensibleDataObjectConverter - */ - protected $extensibleDataObjectConverter; - -@@ -85,7 +96,7 @@ class CustomerRepository implements \Magento\Customer\Api\CustomerRepositoryInte - protected $imageProcessor; - - /** -- * @var \Magento\Framework\Api\ExtensionAttribute\JoinProcessorInterface -+ * @var JoinProcessorInterface - */ - protected $extensionAttributesJoinProcessor; - -@@ -105,38 +116,38 @@ class CustomerRepository implements \Magento\Customer\Api\CustomerRepositoryInte - private $delegatedStorage; - - /** -- * @param \Magento\Customer\Model\CustomerFactory $customerFactory -- * @param \Magento\Customer\Model\Data\CustomerSecureFactory $customerSecureFactory -- * @param \Magento\Customer\Model\CustomerRegistry $customerRegistry -- * @param \Magento\Customer\Model\ResourceModel\AddressRepository $addressRepository -- * @param \Magento\Customer\Model\ResourceModel\Customer $customerResourceModel -- * @param \Magento\Customer\Api\CustomerMetadataInterface $customerMetadata -- * @param \Magento\Customer\Api\Data\CustomerSearchResultsInterfaceFactory $searchResultsFactory -- * @param \Magento\Framework\Event\ManagerInterface $eventManager -- * @param \Magento\Store\Model\StoreManagerInterface $storeManager -- * @param \Magento\Framework\Api\ExtensibleDataObjectConverter $extensibleDataObjectConverter -+ * @param CustomerFactory $customerFactory -+ * @param CustomerSecureFactory $customerSecureFactory -+ * @param CustomerRegistry $customerRegistry -+ * @param AddressRepository $addressRepository -+ * @param Customer $customerResourceModel -+ * @param CustomerMetadataInterface $customerMetadata -+ * @param CustomerSearchResultsInterfaceFactory $searchResultsFactory -+ * @param ManagerInterface $eventManager -+ * @param StoreManagerInterface $storeManager -+ * @param ExtensibleDataObjectConverter $extensibleDataObjectConverter - * @param DataObjectHelper $dataObjectHelper - * @param ImageProcessorInterface $imageProcessor -- * @param \Magento\Framework\Api\ExtensionAttribute\JoinProcessorInterface $extensionAttributesJoinProcessor -+ * @param JoinProcessorInterface $extensionAttributesJoinProcessor - * @param CollectionProcessorInterface $collectionProcessor - * @param NotificationStorage $notificationStorage - * @param DelegatedStorage|null $delegatedStorage - * @SuppressWarnings(PHPMD.ExcessiveParameterList) - */ - public function __construct( -- \Magento\Customer\Model\CustomerFactory $customerFactory, -- \Magento\Customer\Model\Data\CustomerSecureFactory $customerSecureFactory, -- \Magento\Customer\Model\CustomerRegistry $customerRegistry, -- \Magento\Customer\Model\ResourceModel\AddressRepository $addressRepository, -- \Magento\Customer\Model\ResourceModel\Customer $customerResourceModel, -- \Magento\Customer\Api\CustomerMetadataInterface $customerMetadata, -- \Magento\Customer\Api\Data\CustomerSearchResultsInterfaceFactory $searchResultsFactory, -- \Magento\Framework\Event\ManagerInterface $eventManager, -- \Magento\Store\Model\StoreManagerInterface $storeManager, -- \Magento\Framework\Api\ExtensibleDataObjectConverter $extensibleDataObjectConverter, -+ CustomerFactory $customerFactory, -+ CustomerSecureFactory $customerSecureFactory, -+ CustomerRegistry $customerRegistry, -+ AddressRepository $addressRepository, -+ Customer $customerResourceModel, -+ CustomerMetadataInterface $customerMetadata, -+ CustomerSearchResultsInterfaceFactory $searchResultsFactory, -+ ManagerInterface $eventManager, -+ StoreManagerInterface $storeManager, -+ ExtensibleDataObjectConverter $extensibleDataObjectConverter, - DataObjectHelper $dataObjectHelper, - ImageProcessorInterface $imageProcessor, -- \Magento\Framework\Api\ExtensionAttribute\JoinProcessorInterface $extensionAttributesJoinProcessor, -+ JoinProcessorInterface $extensionAttributesJoinProcessor, - CollectionProcessorInterface $collectionProcessor, - NotificationStorage $notificationStorage, - DelegatedStorage $delegatedStorage = null -@@ -156,12 +167,18 @@ class CustomerRepository implements \Magento\Customer\Api\CustomerRepositoryInte - $this->extensionAttributesJoinProcessor = $extensionAttributesJoinProcessor; - $this->collectionProcessor = $collectionProcessor; - $this->notificationStorage = $notificationStorage; -- $this->delegatedStorage = $delegatedStorage -- ?? ObjectManager::getInstance()->get(DelegatedStorage::class); -+ $this->delegatedStorage = $delegatedStorage ?? ObjectManager::getInstance()->get(DelegatedStorage::class); - } - - /** -- * {@inheritdoc} -+ * Create or update a customer. -+ * -+ * @param \Magento\Customer\Api\Data\CustomerInterface $customer -+ * @param string $passwordHash -+ * @return \Magento\Customer\Api\Data\CustomerInterface -+ * @throws \Magento\Framework\Exception\InputException If bad input is provided -+ * @throws \Magento\Framework\Exception\State\InputMismatchException If the provided email is already used -+ * @throws \Magento\Framework\Exception\LocalizedException - * @SuppressWarnings(PHPMD.CyclomaticComplexity) - * @SuppressWarnings(PHPMD.NPathComplexity) - */ -@@ -192,7 +209,9 @@ class CustomerRepository implements \Magento\Customer\Api\CustomerRepositoryInte - $customerModel->setId($customer->getId()); - $storeId = $customerModel->getStoreId(); - if ($storeId === null) { -- $customerModel->setStoreId($this->storeManager->getStore()->getId()); -+ $customerModel->setStoreId( -+ $prevCustomerData ? $prevCustomerData->getStoreId() : $this->storeManager->getStore()->getId() -+ ); - } - // Need to use attribute set or future updates can cause data loss - if (!$customerModel->getAttributeSetId()) { -@@ -204,18 +223,19 @@ class CustomerRepository implements \Magento\Customer\Api\CustomerRepositoryInte - $customerModel->setRpToken(null); - $customerModel->setRpTokenCreatedAt(null); - } -- if (!array_key_exists('default_billing', $customerArr) -+ if (!array_key_exists('addresses', $customerArr) - && null !== $prevCustomerDataArr - && array_key_exists('default_billing', $prevCustomerDataArr) - ) { - $customerModel->setDefaultBilling($prevCustomerDataArr['default_billing']); - } -- if (!array_key_exists('default_shipping', $customerArr) -+ if (!array_key_exists('addresses', $customerArr) - && null !== $prevCustomerDataArr - && array_key_exists('default_shipping', $prevCustomerDataArr) - ) { - $customerModel->setDefaultShipping($prevCustomerDataArr['default_shipping']); - } -+ $this->setValidationFlag($customerArr, $customerModel); - $customerModel->save(); - $this->customerRegistry->push($customerModel); - $customerId = $customerModel->getId(); -@@ -225,7 +245,7 @@ class CustomerRepository implements \Magento\Customer\Api\CustomerRepositoryInte - ) { - $customer->setAddresses($delegatedNewOperation->getCustomer()->getAddresses()); - } -- if ($customer->getAddresses() !== null) { -+ if ($customer->getAddresses() !== null && !$customerModel->getData('ignore_validation_flag')) { - if ($customer->getId()) { - $existingAddresses = $this->getById($customer->getId())->getAddresses(); - $getIdFunc = function ($address) { -@@ -259,7 +279,6 @@ class CustomerRepository implements \Magento\Customer\Api\CustomerRepositoryInte - 'delegate_data' => $delegatedNewOperation ? $delegatedNewOperation->getAdditionalData() : [], - ] - ); -- - return $savedCustomer; - } - -@@ -293,7 +312,13 @@ class CustomerRepository implements \Magento\Customer\Api\CustomerRepositoryInte - } - - /** -- * {@inheritdoc} -+ * Retrieve customer. -+ * -+ * @param string $email -+ * @param int|null $websiteId -+ * @return \Magento\Customer\Api\Data\CustomerInterface -+ * @throws \Magento\Framework\Exception\NoSuchEntityException If customer with the specified email does not exist. -+ * @throws \Magento\Framework\Exception\LocalizedException - */ - public function get($email, $websiteId = null) - { -@@ -302,7 +327,12 @@ class CustomerRepository implements \Magento\Customer\Api\CustomerRepositoryInte - } - - /** -- * {@inheritdoc} -+ * Get customer by Customer ID. -+ * -+ * @param int $customerId -+ * @return \Magento\Customer\Api\Data\CustomerInterface -+ * @throws \Magento\Framework\Exception\NoSuchEntityException If customer with the specified ID does not exist. -+ * @throws \Magento\Framework\Exception\LocalizedException - */ - public function getById($customerId) - { -@@ -311,7 +341,15 @@ class CustomerRepository implements \Magento\Customer\Api\CustomerRepositoryInte - } - - /** -- * {@inheritdoc} -+ * Retrieve customers which match a specified criteria. -+ * -+ * This call returns an array of objects, but detailed information about each object’s attributes might not be -+ * included. See https://devdocs.magento.com/codelinks/attributes.html#CustomerRepositoryInterface to determine -+ * which call to use to get detailed information about all attributes for an object. -+ * -+ * @param \Magento\Framework\Api\SearchCriteriaInterface $searchCriteria -+ * @return \Magento\Customer\Api\Data\CustomerSearchResultsInterface -+ * @throws \Magento\Framework\Exception\LocalizedException - */ - public function getList(SearchCriteriaInterface $searchCriteria) - { -@@ -335,7 +373,7 @@ class CustomerRepository implements \Magento\Customer\Api\CustomerRepositoryInte - ->joinAttribute('billing_telephone', 'customer_address/telephone', 'default_billing', null, 'left') - ->joinAttribute('billing_region', 'customer_address/region', 'default_billing', null, 'left') - ->joinAttribute('billing_country_id', 'customer_address/country_id', 'default_billing', null, 'left') -- ->joinAttribute('company', 'customer_address/company', 'default_billing', null, 'left'); -+ ->joinAttribute('billing_company', 'customer_address/company', 'default_billing', null, 'left'); - - $this->collectionProcessor->process($searchCriteria, $collection); - -@@ -351,7 +389,11 @@ class CustomerRepository implements \Magento\Customer\Api\CustomerRepositoryInte - } - - /** -- * {@inheritdoc} -+ * Delete customer. -+ * -+ * @param \Magento\Customer\Api\Data\CustomerInterface $customer -+ * @return bool true on success -+ * @throws \Magento\Framework\Exception\LocalizedException - */ - public function delete(CustomerInterface $customer) - { -@@ -359,7 +401,12 @@ class CustomerRepository implements \Magento\Customer\Api\CustomerRepositoryInte - } - - /** -- * {@inheritdoc} -+ * Delete customer by Customer ID. -+ * -+ * @param int $customerId -+ * @return bool true on success -+ * @throws \Magento\Framework\Exception\NoSuchEntityException -+ * @throws \Magento\Framework\Exception\LocalizedException - */ - public function deleteById($customerId) - { -@@ -375,15 +422,12 @@ class CustomerRepository implements \Magento\Customer\Api\CustomerRepositoryInte - * Helper function that adds a FilterGroup to the collection. - * - * @deprecated 100.2.0 -- * @param \Magento\Framework\Api\Search\FilterGroup $filterGroup -- * @param \Magento\Customer\Model\ResourceModel\Customer\Collection $collection -+ * @param FilterGroup $filterGroup -+ * @param Collection $collection - * @return void -- * @throws \Magento\Framework\Exception\InputException - */ -- protected function addFilterGroupToCollection( -- \Magento\Framework\Api\Search\FilterGroup $filterGroup, -- \Magento\Customer\Model\ResourceModel\Customer\Collection $collection -- ) { -+ protected function addFilterGroupToCollection(FilterGroup $filterGroup, Collection $collection) -+ { - $fields = []; - foreach ($filterGroup->getFilters() as $filter) { - $condition = $filter->getConditionType() ? $filter->getConditionType() : 'eq'; -@@ -393,4 +437,18 @@ class CustomerRepository implements \Magento\Customer\Api\CustomerRepositoryInte - $collection->addFieldToFilter($fields); - } - } -+ -+ /** -+ * Set ignore_validation_flag to skip model validation -+ * -+ * @param array $customerArray -+ * @param Customer $customerModel -+ * @return void -+ */ -+ private function setValidationFlag($customerArray, $customerModel) -+ { -+ if (isset($customerArray['ignore_validation_flag'])) { -+ $customerModel->setData('ignore_validation_flag', true); -+ } -+ } - } -diff --git a/app/code/Magento/Customer/Model/ResourceModel/Group.php b/app/code/Magento/Customer/Model/ResourceModel/Group.php -index 80203e742e0..987723c5c9f 100644 ---- a/app/code/Magento/Customer/Model/ResourceModel/Group.php -+++ b/app/code/Magento/Customer/Model/ResourceModel/Group.php -@@ -29,8 +29,8 @@ class Group extends \Magento\Framework\Model\ResourceModel\Db\VersionControl\Abs - - /** - * @param \Magento\Framework\Model\ResourceModel\Db\Context $context -- * @param Snapshot $entitySnapshot, -- * @param RelationComposite $entityRelationComposite, -+ * @param Snapshot $entitySnapshot -+ * @param RelationComposite $entityRelationComposite - * @param \Magento\Customer\Api\GroupManagementInterface $groupManagement - * @param Customer\CollectionFactory $customersFactory - * @param string $connectionName -@@ -110,6 +110,8 @@ class Group extends \Magento\Framework\Model\ResourceModel\Db\VersionControl\Abs - } - - /** -+ * Create customers collection. -+ * - * @return \Magento\Customer\Model\ResourceModel\Customer\Collection - */ - protected function _createCustomersCollection() -@@ -131,7 +133,7 @@ class Group extends \Magento\Framework\Model\ResourceModel\Db\VersionControl\Abs - } - - /** -- * {@inheritdoc} -+ * @inheritdoc - */ - protected function _afterSave(\Magento\Framework\Model\AbstractModel $object) - { -diff --git a/app/code/Magento/Customer/Model/Vat.php b/app/code/Magento/Customer/Model/Vat.php -index 9822e2ad1b8..123a9eef4b7 100644 ---- a/app/code/Magento/Customer/Model/Vat.php -+++ b/app/code/Magento/Customer/Model/Vat.php -@@ -179,15 +179,23 @@ class Vat - return $gatewayResponse; - } - -+ $countryCodeForVatNumber = $this->getCountryCodeForVatNumber($countryCode); -+ $requesterCountryCodeForVatNumber = $this->getCountryCodeForVatNumber($requesterCountryCode); -+ - try { - $soapClient = $this->createVatNumberValidationSoapClient(); - - $requestParams = []; -- $requestParams['countryCode'] = $countryCode; -- $requestParams['vatNumber'] = str_replace([' ', '-'], ['', ''], $vatNumber); -- $requestParams['requesterCountryCode'] = $requesterCountryCode; -- $requestParams['requesterVatNumber'] = str_replace([' ', '-'], ['', ''], $requesterVatNumber); -- -+ $requestParams['countryCode'] = $countryCodeForVatNumber; -+ $vatNumberSanitized = $this->isCountryInEU($countryCode) -+ ? str_replace([' ', '-', $countryCodeForVatNumber], ['', '', ''], $vatNumber) -+ : str_replace([' ', '-'], ['', ''], $vatNumber); -+ $requestParams['vatNumber'] = $vatNumberSanitized; -+ $requestParams['requesterCountryCode'] = $requesterCountryCodeForVatNumber; -+ $reqVatNumSanitized = $this->isCountryInEU($requesterCountryCode) -+ ? str_replace([' ', '-', $requesterCountryCodeForVatNumber], ['', '', ''], $requesterVatNumber) -+ : str_replace([' ', '-'], ['', ''], $requesterVatNumber); -+ $requestParams['requesterVatNumber'] = $reqVatNumSanitized; - // Send request to service - $result = $soapClient->checkVatApprox($requestParams); - -@@ -296,4 +304,22 @@ class Vat - ); - return in_array($countryCode, $euCountries); - } -+ -+ /** -+ * Returns the country code to use in the VAT number which is not always the same as the normal country code -+ * -+ * @param string $countryCode -+ * @return string -+ */ -+ private function getCountryCodeForVatNumber(string $countryCode): string -+ { -+ // Greece uses a different code for VAT numbers then its country code -+ // See: http://ec.europa.eu/taxation_customs/vies/faq.html#item_11 -+ // And https://en.wikipedia.org/wiki/VAT_identification_number: -+ // "The full identifier starts with an ISO 3166-1 alpha-2 (2 letters) country code -+ // (except for Greece, which uses the ISO 639-1 language code EL for the Greek language, -+ // instead of its ISO 3166-1 alpha-2 country code GR)" -+ -+ return $countryCode === 'GR' ? 'EL' : $countryCode; -+ } - } -diff --git a/app/code/Magento/Customer/Model/Visitor.php b/app/code/Magento/Customer/Model/Visitor.php -index 763a7afbfa8..4f129f05aa8 100644 ---- a/app/code/Magento/Customer/Model/Visitor.php -+++ b/app/code/Magento/Customer/Model/Visitor.php -@@ -6,10 +6,15 @@ - - namespace Magento\Customer\Model; - -+use Magento\Framework\App\ObjectManager; -+use Magento\Framework\App\RequestSafetyInterface; -+ - /** - * Class Visitor -+ * - * @package Magento\Customer\Model - * @SuppressWarnings(PHPMD.CouplingBetweenObjects) -+ * @SuppressWarnings(PHPMD.CookieAndSessionMisuse) - */ - class Visitor extends \Magento\Framework\Model\AbstractModel - { -@@ -65,6 +70,11 @@ class Visitor extends \Magento\Framework\Model\AbstractModel - */ - protected $indexerRegistry; - -+ /** -+ * @var RequestSafetyInterface -+ */ -+ private $requestSafety; -+ - /** - * @param \Magento\Framework\Model\Context $context - * @param \Magento\Framework\Registry $registry -@@ -78,6 +88,7 @@ class Visitor extends \Magento\Framework\Model\AbstractModel - * @param array $ignoredUserAgents - * @param array $ignores - * @param array $data -+ * @param RequestSafetyInterface|null $requestSafety - * - * @SuppressWarnings(PHPMD.ExcessiveParameterList) - */ -@@ -93,7 +104,8 @@ class Visitor extends \Magento\Framework\Model\AbstractModel - \Magento\Framework\Data\Collection\AbstractDb $resourceCollection = null, - array $ignoredUserAgents = [], - array $ignores = [], -- array $data = [] -+ array $data = [], -+ RequestSafetyInterface $requestSafety = null - ) { - $this->session = $session; - $this->httpHeader = $httpHeader; -@@ -103,6 +115,7 @@ class Visitor extends \Magento\Framework\Model\AbstractModel - $this->scopeConfig = $scopeConfig; - $this->dateTime = $dateTime; - $this->indexerRegistry = $indexerRegistry; -+ $this->requestSafety = $requestSafety ?? ObjectManager::getInstance()->get(RequestSafetyInterface::class); - } - - /** -@@ -175,7 +188,8 @@ class Visitor extends \Magento\Framework\Model\AbstractModel - */ - public function saveByRequest($observer) - { -- if ($this->skipRequestLogging || $this->isModuleIgnored($observer)) { -+ // prevent saving Visitor for safe methods, e.g. GET request -+ if ($this->skipRequestLogging || $this->requestSafety->isSafeMethod() || $this->isModuleIgnored($observer)) { - return $this; - } - -@@ -262,6 +276,7 @@ class Visitor extends \Magento\Framework\Model\AbstractModel - - /** - * Destroy binding of checkout quote -+ * - * @param \Magento\Framework\Event\Observer $observer - * @return \Magento\Customer\Model\Visitor - */ -@@ -305,11 +320,9 @@ class Visitor extends \Magento\Framework\Model\AbstractModel - */ - public function getOnlineInterval() - { -- $configValue = intval( -- $this->scopeConfig->getValue( -- static::XML_PATH_ONLINE_INTERVAL, -- \Magento\Store\Model\ScopeInterface::SCOPE_STORE -- ) -+ $configValue = (int)$this->scopeConfig->getValue( -+ static::XML_PATH_ONLINE_INTERVAL, -+ \Magento\Store\Model\ScopeInterface::SCOPE_STORE - ); - return $configValue ?: static::DEFAULT_ONLINE_MINUTES_INTERVAL; - } -diff --git a/app/code/Magento/Customer/Observer/UpgradeCustomerPasswordObserver.php b/app/code/Magento/Customer/Observer/UpgradeCustomerPasswordObserver.php -index eb7e81009c9..26c4c50009b 100644 ---- a/app/code/Magento/Customer/Observer/UpgradeCustomerPasswordObserver.php -+++ b/app/code/Magento/Customer/Observer/UpgradeCustomerPasswordObserver.php -@@ -6,11 +6,15 @@ - - namespace Magento\Customer\Observer; - -+use Magento\Customer\Model\Customer; - use Magento\Framework\Encryption\EncryptorInterface; - use Magento\Framework\Event\ObserverInterface; - use Magento\Customer\Api\CustomerRepositoryInterface; - use Magento\Customer\Model\CustomerRegistry; - -+/** -+ * Class observer UpgradeCustomerPasswordObserver to upgrade customer password hash when customer has logged in -+ */ - class UpgradeCustomerPasswordObserver implements ObserverInterface - { - /** -@@ -61,7 +65,20 @@ class UpgradeCustomerPasswordObserver implements ObserverInterface - - if (!$this->encryptor->validateHashVersion($customerSecure->getPasswordHash(), true)) { - $customerSecure->setPasswordHash($this->encryptor->getHash($password, true)); -+ // No need to validate customer and customer address while upgrading customer password -+ $this->setIgnoreValidationFlag($customer); - $this->customerRepository->save($customer); - } - } -+ -+ /** -+ * Set ignore_validation_flag to skip unnecessary address and customer validation -+ * -+ * @param Customer $customer -+ * @return void -+ */ -+ private function setIgnoreValidationFlag($customer) -+ { -+ $customer->setData('ignore_validation_flag', true); -+ } - } -diff --git a/app/code/Magento/Customer/Setup/Patch/Data/MigrateStoresAllowedCountriesToWebsite.php b/app/code/Magento/Customer/Setup/Patch/Data/MigrateStoresAllowedCountriesToWebsite.php -index 7488f3fd4a9..e4978070f53 100644 ---- a/app/code/Magento/Customer/Setup/Patch/Data/MigrateStoresAllowedCountriesToWebsite.php -+++ b/app/code/Magento/Customer/Setup/Patch/Data/MigrateStoresAllowedCountriesToWebsite.php -@@ -8,12 +8,14 @@ namespace Magento\Customer\Setup\Patch\Data; - - use Magento\Directory\Model\AllowedCountries; - use Magento\Framework\Setup\ModuleDataSetupInterface; --use Magento\Directory\Model\AllowedCountriesFactory; - use Magento\Store\Model\ScopeInterface; - use Magento\Store\Model\StoreManagerInterface; - use Magento\Framework\Setup\Patch\DataPatchInterface; - use Magento\Framework\Setup\Patch\PatchVersionInterface; - -+/** -+ * Migrate store allowed countries to website. -+ */ - class MigrateStoresAllowedCountriesToWebsite implements DataPatchInterface, PatchVersionInterface - { - /** -@@ -27,7 +29,7 @@ class MigrateStoresAllowedCountriesToWebsite implements DataPatchInterface, Patc - private $storeManager; - - /** -- * @var AllowedCountriesFactory -+ * @var AllowedCountries - */ - private $allowedCountries; - -@@ -48,10 +50,11 @@ class MigrateStoresAllowedCountriesToWebsite implements DataPatchInterface, Patc - } - - /** -- * {@inheritdoc} -+ * @inheritdoc - */ - public function apply() - { -+ - $this->moduleDataSetup->getConnection()->beginTransaction(); - - try { -@@ -149,7 +152,7 @@ class MigrateStoresAllowedCountriesToWebsite implements DataPatchInterface, Patc - } - - /** -- * {@inheritdoc} -+ * @inheritdoc - */ - public static function getDependencies() - { -@@ -159,7 +162,7 @@ class MigrateStoresAllowedCountriesToWebsite implements DataPatchInterface, Patc - } - - /** -- * {@inheritdoc} -+ * @inheritdoc - */ - public static function getVersion() - { -@@ -167,7 +170,7 @@ class MigrateStoresAllowedCountriesToWebsite implements DataPatchInterface, Patc - } - - /** -- * {@inheritdoc} -+ * @inheritdoc - */ - public function getAliases() - { -diff --git a/app/code/Magento/Customer/Test/Mftf/ActionGroup/AdminAssertAddressInCustomersAddressGridActionGroup.xml b/app/code/Magento/Customer/Test/Mftf/ActionGroup/AdminAssertAddressInCustomersAddressGridActionGroup.xml -new file mode 100644 -index 00000000000..53dba774d6c ---- /dev/null -+++ b/app/code/Magento/Customer/Test/Mftf/ActionGroup/AdminAssertAddressInCustomersAddressGridActionGroup.xml -@@ -0,0 +1,18 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> -+ <!--Assert customer info in customers grid row --> -+ <actionGroup name="AdminAssertAddressInCustomersAddressGrid"> -+ <arguments> -+ <argument name="text" type="string"/> -+ </arguments> -+ <see selector="{{AdminCustomerAddressesGridSection.rowsInGrid}}" userInput="{{text}}" stepKey="seeTextInGrid"/> -+ </actionGroup> -+</actionGroups> -diff --git a/app/code/Magento/Customer/Test/Mftf/ActionGroup/AdminAssertCustomerAccountInformationActionGroup.xml b/app/code/Magento/Customer/Test/Mftf/ActionGroup/AdminAssertCustomerAccountInformationActionGroup.xml -new file mode 100644 -index 00000000000..a908d042fcc ---- /dev/null -+++ b/app/code/Magento/Customer/Test/Mftf/ActionGroup/AdminAssertCustomerAccountInformationActionGroup.xml -@@ -0,0 +1,22 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> -+ <!-- Assert Customer Account Information --> -+ <actionGroup name="AdminAssertCustomerAccountInformation" > -+ <arguments> -+ <argument name="firstName" type="string" defaultValue=""/> -+ <argument name="lastName" type="string" defaultValue=""/> -+ <argument name="email" type="string" defaultValue=""/> -+ </arguments> -+ <click selector="{{AdminCustomerAccountInformationSection.accountInformationTab}}" stepKey="proceedToAccountInformation"/> -+ <seeInField userInput="{{firstName}}" selector="{{AdminCustomerAccountInformationSection.firstName}}" stepKey="firstName"/> -+ <seeInField userInput="{{lastName}}" selector="{{AdminCustomerAccountInformationSection.lastName}}" stepKey="lastName"/> -+ <seeInField userInput="{{email}}" selector="{{AdminCustomerAccountInformationSection.email}}" stepKey="email"/> -+ </actionGroup> -+</actionGroups> -diff --git a/app/code/Magento/Customer/Test/Mftf/ActionGroup/AdminAssertCustomerDefaultBillingAddressActionGroup.xml b/app/code/Magento/Customer/Test/Mftf/ActionGroup/AdminAssertCustomerDefaultBillingAddressActionGroup.xml -new file mode 100644 -index 00000000000..32b62470610 ---- /dev/null -+++ b/app/code/Magento/Customer/Test/Mftf/ActionGroup/AdminAssertCustomerDefaultBillingAddressActionGroup.xml -@@ -0,0 +1,30 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> -+ <!-- Assert Customer Default Billing Address --> -+ <actionGroup name="AdminAssertCustomerDefaultBillingAddress" > -+ <arguments> -+ <argument name="firstName" type="string" defaultValue=""/> -+ <argument name="lastName" type="string" defaultValue=""/> -+ <argument name="street1" type="string" defaultValue=""/> -+ <argument name="state" type="string" defaultValue=""/> -+ <argument name="postcode" type="string" defaultValue=""/> -+ <argument name="country" type="string" defaultValue=""/> -+ <argument name="telephone" type="string" defaultValue=""/> -+ </arguments> -+ <click selector="{{AdminEditCustomerAddressesSection.addresses}}" stepKey="proceedToAddresses"/> -+ <see userInput="{{firstName}}" selector="{{AdminCustomerAddressesDefaultBillingSection.addressDetails}}" stepKey="firstName"/> -+ <see userInput="{{lastName}}" selector="{{AdminCustomerAddressesDefaultBillingSection.addressDetails}}" stepKey="lastName"/> -+ <see userInput="{{street1}}" selector="{{AdminCustomerAddressesDefaultBillingSection.addressDetails}}" stepKey="street1"/> -+ <see userInput="{{state}}" selector="{{AdminCustomerAddressesDefaultBillingSection.addressDetails}}" stepKey="state"/> -+ <see userInput="{{postcode}}" selector="{{AdminCustomerAddressesDefaultBillingSection.addressDetails}}" stepKey="postcode"/> -+ <see userInput="{{country}}" selector="{{AdminCustomerAddressesDefaultBillingSection.addressDetails}}" stepKey="country"/> -+ <see userInput="{{telephone}}" selector="{{AdminCustomerAddressesDefaultBillingSection.addressDetails}}" stepKey="telephone"/> -+ </actionGroup> -+</actionGroups> -diff --git a/app/code/Magento/Customer/Test/Mftf/ActionGroup/AdminAssertCustomerDefaultShippingAddressActionGroup.xml b/app/code/Magento/Customer/Test/Mftf/ActionGroup/AdminAssertCustomerDefaultShippingAddressActionGroup.xml -new file mode 100644 -index 00000000000..9d7c209121f ---- /dev/null -+++ b/app/code/Magento/Customer/Test/Mftf/ActionGroup/AdminAssertCustomerDefaultShippingAddressActionGroup.xml -@@ -0,0 +1,30 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> -+ <!-- Assert Customer Default Shipping Address --> -+ <actionGroup name="AdminAssertCustomerDefaultShippingAddress" > -+ <arguments> -+ <argument name="firstName" type="string" defaultValue=""/> -+ <argument name="lastName" type="string" defaultValue=""/> -+ <argument name="street1" type="string" defaultValue=""/> -+ <argument name="state" type="string" defaultValue="" /> -+ <argument name="postcode" type="string" defaultValue=""/> -+ <argument name="country" type="string" defaultValue=""/> -+ <argument name="telephone" type="string" defaultValue=""/> -+ </arguments> -+ <click selector="{{AdminEditCustomerAddressesSection.addresses}}" stepKey="proceedToAddresses"/> -+ <see userInput="{{firstName}}" selector="{{AdminCustomerAddressesDefaultShippingSection.addressDetails}}" stepKey="firstName"/> -+ <see userInput="{{lastName}}" selector="{{AdminCustomerAddressesDefaultShippingSection.addressDetails}}" stepKey="lastName"/> -+ <see userInput="{{street1}}" selector="{{AdminCustomerAddressesDefaultShippingSection.addressDetails}}" stepKey="street1"/> -+ <see userInput="{{state}}" selector="{{AdminCustomerAddressesDefaultShippingSection.addressDetails}}" stepKey="state"/> -+ <see userInput="{{postcode}}" selector="{{AdminCustomerAddressesDefaultShippingSection.addressDetails}}" stepKey="postcode"/> -+ <see userInput="{{country}}" selector="{{AdminCustomerAddressesDefaultShippingSection.addressDetails}}" stepKey="country"/> -+ <see userInput="{{telephone}}" selector="{{AdminCustomerAddressesDefaultShippingSection.addressDetails}}" stepKey="telephone"/> -+ </actionGroup> -+</actionGroups> -diff --git a/app/code/Magento/Customer/Test/Mftf/ActionGroup/AdminAssertCustomerGroupOnCustomerFormActionGroup.xml b/app/code/Magento/Customer/Test/Mftf/ActionGroup/AdminAssertCustomerGroupOnCustomerFormActionGroup.xml -new file mode 100644 -index 00000000000..3112f2b3efe ---- /dev/null -+++ b/app/code/Magento/Customer/Test/Mftf/ActionGroup/AdminAssertCustomerGroupOnCustomerFormActionGroup.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="AdminAssertCustomerGroupOnCustomerForm"> -+ <arguments> -+ <argument name="customerGroupName" type="string"/> -+ </arguments> -+ <amOnPage url="{{AdminNewCustomerPage.url}}" stepKey="amOnCustomerCreatePage"/> -+ <waitForElementVisible selector="{{AdminCustomerAccountInformationSection.group}}" stepKey="waitForElementVisible"/> -+ <see selector="{{AdminCustomerAccountInformationSection.group}}" userInput="{{customerGroupName}}" stepKey="assertCustomerGroupPresent"/> -+ </actionGroup> -+</actionGroups> -diff --git a/app/code/Magento/Customer/Test/Mftf/ActionGroup/AdminAssertCustomerGroupOnProductFormActionGroup.xml b/app/code/Magento/Customer/Test/Mftf/ActionGroup/AdminAssertCustomerGroupOnProductFormActionGroup.xml -new file mode 100644 -index 00000000000..0f6d6a5fcfd ---- /dev/null -+++ b/app/code/Magento/Customer/Test/Mftf/ActionGroup/AdminAssertCustomerGroupOnProductFormActionGroup.xml -@@ -0,0 +1,21 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> -+ <actionGroup name="AdminAssertCustomerGroupOnProductForm"> -+ <arguments> -+ <argument name="customerGroupName" type="string"/> -+ </arguments> -+ <amOnPage url="{{AdminProductCreatePage.url(AddToDefaultSet.attributeSetId, 'simple')}}" stepKey="amOnProductCreatePage"/> -+ <waitForElementVisible selector="{{AdminProductFormSection.advancedPricingLink}}" stepKey="waitForAdvancedPricingLinkVisible"/> -+ <click selector="{{AdminProductFormSection.advancedPricingLink}}" stepKey="clickOnAdvancedPricingButton"/> -+ <waitForElementVisible selector="{{AdminProductFormAdvancedPricingSection.customerGroupPriceAddButton}}" stepKey="waitForAddButtonVisible"/> -+ <click selector="{{AdminProductFormAdvancedPricingSection.customerGroupPriceAddButton}}" stepKey="clickAddButton"/> -+ <see selector="{{AdminProductFormAdvancedPricingSection.productTierPriceCustGroupSelect('0')}}" userInput="{{customerGroupName}}" stepKey="assertCustomerGroupPresent"/> -+ </actionGroup> -+</actionGroups> -diff --git a/app/code/Magento/Customer/Test/Mftf/ActionGroup/AdminAssertCustomerGroupPresentInGridActionGroup.xml b/app/code/Magento/Customer/Test/Mftf/ActionGroup/AdminAssertCustomerGroupPresentInGridActionGroup.xml -new file mode 100644 -index 00000000000..248c93a16de ---- /dev/null -+++ b/app/code/Magento/Customer/Test/Mftf/ActionGroup/AdminAssertCustomerGroupPresentInGridActionGroup.xml -@@ -0,0 +1,14 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> -+ <actionGroup name="AdminAssertCustomerGroupPresentInGrid" extends="AdminFilterCustomerGroupByNameActionGroup"> -+ <!--- Assume we are on admin customer group page. --> -+ <see selector="{{AdminDataGridTableSection.column('Group')}}" userInput="{{customerGroupName}}" after="clickApplyFiltersButton" stepKey="seeCustomerGroupNameInGrid"/> -+ </actionGroup> -+</actionGroups> -diff --git a/app/code/Magento/Customer/Test/Mftf/ActionGroup/AdminAssertCustomerInCustomersGridActionGroup.xml b/app/code/Magento/Customer/Test/Mftf/ActionGroup/AdminAssertCustomerInCustomersGridActionGroup.xml -new file mode 100644 -index 00000000000..d7529b3bdd5 ---- /dev/null -+++ b/app/code/Magento/Customer/Test/Mftf/ActionGroup/AdminAssertCustomerInCustomersGridActionGroup.xml -@@ -0,0 +1,19 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> -+ <!--Assert customer info in customers grid row --> -+ <actionGroup name="AdminAssertCustomerInCustomersGrid"> -+ <arguments> -+ <argument name="text" type="string"/> -+ <argument name="row" type="string"/> -+ </arguments> -+ <see selector="{{AdminCustomerGridSection.gridRow(row)}}" userInput="{{text}}" stepKey="seeCustomerInGrid"/> -+ </actionGroup> -+</actionGroups> -diff --git a/app/code/Magento/Customer/Test/Mftf/ActionGroup/AdminAssertCustomerNoDefaultBillingAddressActionGroup.xml b/app/code/Magento/Customer/Test/Mftf/ActionGroup/AdminAssertCustomerNoDefaultBillingAddressActionGroup.xml -new file mode 100644 -index 00000000000..5557025c4b1 ---- /dev/null -+++ b/app/code/Magento/Customer/Test/Mftf/ActionGroup/AdminAssertCustomerNoDefaultBillingAddressActionGroup.xml -@@ -0,0 +1,15 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> -+ <!-- Assert Customer Have No Default Billing Address --> -+ <actionGroup name="AdminAssertCustomerNoDefaultBillingAddress" > -+ <click selector="{{AdminEditCustomerAddressesSection.addresses}}" stepKey="proceedToAddresses"/> -+ <see userInput="The customer does not have default billing address" selector="{{AdminCustomerAddressesDefaultBillingSection.address}}" stepKey="see"/> -+ </actionGroup> -+</actionGroups> -diff --git a/app/code/Magento/Customer/Test/Mftf/ActionGroup/AdminAssertCustomerNoDefaultShippingAddressActionGroup.xml b/app/code/Magento/Customer/Test/Mftf/ActionGroup/AdminAssertCustomerNoDefaultShippingAddressActionGroup.xml -new file mode 100644 -index 00000000000..e33ebbb96ee ---- /dev/null -+++ b/app/code/Magento/Customer/Test/Mftf/ActionGroup/AdminAssertCustomerNoDefaultShippingAddressActionGroup.xml -@@ -0,0 +1,15 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> -+ <!-- Assert Customer Have No Default Shipping Address --> -+ <actionGroup name="AdminAssertCustomerNoDefaultShippingAddress" > -+ <click selector="{{AdminEditCustomerAddressesSection.addresses}}" stepKey="proceedToAddresses"/> -+ <see userInput="The customer does not have default shipping address" selector="{{AdminCustomerAddressesDefaultShippingSection.address}}" stepKey="see"/> -+ </actionGroup> -+</actionGroups> -diff --git a/app/code/Magento/Customer/Test/Mftf/ActionGroup/AdminAssertErrorMessageCustomerGroupAlreadyExistsActionGroup.xml b/app/code/Magento/Customer/Test/Mftf/ActionGroup/AdminAssertErrorMessageCustomerGroupAlreadyExistsActionGroup.xml -new file mode 100644 -index 00000000000..5eb52630d90 ---- /dev/null -+++ b/app/code/Magento/Customer/Test/Mftf/ActionGroup/AdminAssertErrorMessageCustomerGroupAlreadyExistsActionGroup.xml -@@ -0,0 +1,15 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> -+ <actionGroup name="AdminAssertErrorMessageCustomerGroupAlreadyExists" extends="AdminCreateCustomerGroupActionGroup"> -+ <remove keyForRemoval="seeCustomerGroupSaveMessage"/> -+ <waitForElementVisible selector="{{AdminMessagesSection.errorMessage}}" stepKey="waitForElementVisible"/> -+ <see selector="{{AdminMessagesSection.errorMessage}}" userInput="Customer Group already exists." stepKey="seeErrorMessage"/> -+ </actionGroup> -+</actionGroups> -diff --git a/app/code/Magento/Customer/Test/Mftf/ActionGroup/AdminAssertNumberOfRecordsInCustomersAddressGridActionGroup.xml b/app/code/Magento/Customer/Test/Mftf/ActionGroup/AdminAssertNumberOfRecordsInCustomersAddressGridActionGroup.xml -new file mode 100644 -index 00000000000..390f723d91f ---- /dev/null -+++ b/app/code/Magento/Customer/Test/Mftf/ActionGroup/AdminAssertNumberOfRecordsInCustomersAddressGridActionGroup.xml -@@ -0,0 +1,18 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> -+ <!--Assert number of records in customer address grid --> -+ <actionGroup name="AdminAssertNumberOfRecordsInCustomersAddressGrid"> -+ <arguments> -+ <argument name="number" type="string"/> -+ </arguments> -+ <see userInput="{{number}} records found" selector="{{AdminCustomerAddressesGridActionsSection.headerRow}}" stepKey="seeRecords"/> -+ </actionGroup> -+</actionGroups> -diff --git a/app/code/Magento/Customer/Test/Mftf/ActionGroup/AdminConfigCustomerActionGroup.xml b/app/code/Magento/Customer/Test/Mftf/ActionGroup/AdminConfigCustomerActionGroup.xml -new file mode 100644 -index 00000000000..8a3ab706869 ---- /dev/null -+++ b/app/code/Magento/Customer/Test/Mftf/ActionGroup/AdminConfigCustomerActionGroup.xml -@@ -0,0 +1,20 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> -+ <actionGroup name="SetCustomerDataLifetimeActionGroup"> -+ <arguments> -+ <argument name="minutes" defaultValue="60" type="string"/> -+ </arguments> -+ <amOnPage url="{{AdminCustomerConfigPage.url('#customer_online_customers-link')}}" stepKey="openCustomerConfigPage"/> -+ <fillField userInput="{{minutes}}" selector="{{AdminCustomerConfigSection.customerDataLifetime}}" stepKey="fillCustomerDataLifetime"/> -+ <click selector="{{AdminMainActionsSection.save}}" stepKey="clickSave"/> -+ <seeElement selector="{{AdminMessagesSection.success}}" stepKey="seeSuccessMessage"/> -+ </actionGroup> -+</actionGroups> -diff --git a/app/code/Magento/Customer/Test/Mftf/ActionGroup/AdminCreateCustomerGroupActionGroup.xml b/app/code/Magento/Customer/Test/Mftf/ActionGroup/AdminCreateCustomerGroupActionGroup.xml -new file mode 100644 -index 00000000000..ba984a4d825 ---- /dev/null -+++ b/app/code/Magento/Customer/Test/Mftf/ActionGroup/AdminCreateCustomerGroupActionGroup.xml -@@ -0,0 +1,25 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> -+ <actionGroup name="AdminCreateCustomerGroupActionGroup"> -+ <arguments> -+ <argument name="groupName" type="string"/> -+ <argument name="taxClass" type="string"/> -+ </arguments> -+ <amOnPage url="{{AdminNewCustomerGroupPage.url}}" stepKey="goToNewCustomerGroupPage"/> -+ <waitForPageLoad stepKey="waitForNewCustomerGroupPageLoad"/> -+ -+ <!--Set tax class for customer group--> -+ <fillField stepKey="fillGroupName" selector="{{AdminNewCustomerGroupSection.groupName}}" userInput="{{groupName}}"/> -+ <selectOption selector="{{AdminNewCustomerGroupSection.taxClass}}" userInput="{{taxClass}}" stepKey="selectTaxClassOption"/> -+ <click selector="{{AdminNewCustomerGroupSection.saveCustomerGroup}}" stepKey="clickToSaveCustomerGroup"/> -+ <waitForPageLoad stepKey="waitForCustomerGroupSaved"/> -+ <see stepKey="seeCustomerGroupSaveMessage" userInput="You saved the customer group."/> -+ </actionGroup> -+</actionGroups> -diff --git a/app/code/Magento/Customer/Test/Mftf/ActionGroup/AdminCreateCustomerWithWebsiteAndStoreViewActionGroup.xml b/app/code/Magento/Customer/Test/Mftf/ActionGroup/AdminCreateCustomerWithWebsiteAndStoreViewActionGroup.xml -new file mode 100644 -index 00000000000..02366415d9e ---- /dev/null -+++ b/app/code/Magento/Customer/Test/Mftf/ActionGroup/AdminCreateCustomerWithWebsiteAndStoreViewActionGroup.xml -@@ -0,0 +1,65 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> -+ <actionGroup name="AdminCreateCustomerWithWebsiteAndStoreViewActionGroup"> -+ <arguments> -+ <argument name="customerData"/> -+ <argument name="address"/> -+ <argument name="website" type="string"/> -+ <argument name="storeView" type="string"/> -+ </arguments> -+ <amOnPage url="{{AdminCustomerPage.url}}" stepKey="goToCustomersPage"/> -+ <click stepKey="addNewCustomer" selector="{{AdminCustomerGridMainActionsSection.addNewCustomer}}"/> -+ <selectOption stepKey="selectWebSite" selector="{{AdminCustomerAccountInformationSection.associateToWebsite}}" userInput="{{website}}"/> -+ <fillField stepKey="FillFirstName" selector="{{AdminCustomerAccountInformationSection.firstName}}" userInput="{{customerData.firstname}}"/> -+ <fillField stepKey="FillLastName" selector="{{AdminCustomerAccountInformationSection.lastName}}" userInput="{{customerData.lastname}}"/> -+ <fillField stepKey="FillEmail" selector="{{AdminCustomerAccountInformationSection.email}}" userInput="{{customerData.email}}"/> -+ <selectOption stepKey="selectStoreView" selector="{{AdminCustomerAccountInformationSection.storeView}}" userInput="{{storeView}}"/> -+ <scrollToTopOfPage stepKey="scrollToTopOfThePage"/> -+ <click stepKey="saveCustomer" selector="{{AdminCustomerAccountInformationSection.saveCustomerAndContinueEdit}}"/> -+ <waitForPageLoad stepKey="waitForCustomersPage"/> -+ <see stepKey="seeSuccessMessage" userInput="You saved the customer."/> -+ <click stepKey="goToAddresses" selector="{{AdminCustomerAccountInformationSection.addressesButton}}"/> -+ <waitForPageLoad stepKey="waitForAddresses"/> -+ <click stepKey="clickOnAddNewAddress" selector="{{AdminCustomerAddressesSection.addNewAddress}}"/> -+ <waitForPageLoad stepKey="waitForAddressFields"/> -+ <click stepKey="thickBillingAddress" selector="{{AdminCustomerAddressesSection.defaultBillingAddress}}"/> -+ <click stepKey="thickShippingAddress" selector="{{AdminCustomerAddressesSection.defaultShippingAddress}}"/> -+ <fillField stepKey="fillFirstNameForAddress" selector="{{AdminCustomerAddressesSection.firstNameForAddress}}" userInput="{{address.firstname}}"/> -+ <fillField stepKey="fillLastNameForAddress" selector="{{AdminCustomerAddressesSection.lastNameForAddress}}" userInput="{{address.lastname}}"/> -+ <fillField stepKey="fillStreetAddress" selector="{{AdminCustomerAddressesSection.streetAddress}}" userInput="{{address.street[0]}}"/> -+ <fillField stepKey="fillCity" selector="{{AdminCustomerAddressesSection.city}}" userInput="{{address.city}}"/> -+ <selectOption stepKey="selectCountry" selector="{{AdminCustomerAddressesSection.country}}" userInput="{{address.country}}"/> -+ <selectOption stepKey="selectState" selector="{{AdminCustomerAddressesSection.state}}" userInput="{{address.state}}"/> -+ <fillField stepKey="fillZip" selector="{{AdminCustomerAddressesSection.zip}}" userInput="{{address.postcode}}"/> -+ <fillField stepKey="fillPhoneNumber" selector="{{AdminCustomerAddressesSection.phoneNumber}}" userInput="{{address.telephone}}"/> -+ <click stepKey="saveAddress" selector="{{AdminCustomerAddressesSection.saveAddress}}"/> -+ <waitForPageLoad stepKey="waitForAddressSave"/> -+ </actionGroup> -+ -+ <actionGroup name="AdminCreateCustomerWithWebSiteAndGroup"> -+ <arguments> -+ <argument name="customerData" defaultValue="Simple_US_Customer"/> -+ <argument name="website" type="string" defaultValue="{{_defaultWebsite.name}}"/> -+ <argument name="storeView" type="string" defaultValue="{{_defaultStore.name}}"/> -+ </arguments> -+ <amOnPage url="{{AdminCustomerPage.url}}" stepKey="goToCustomersPage"/> -+ <click stepKey="addNewCustomer" selector="{{AdminCustomerGridMainActionsSection.addNewCustomer}}"/> -+ <selectOption stepKey="selectWebSite" selector="{{AdminCustomerAccountInformationSection.associateToWebsite}}" userInput="{{website}}"/> -+ <selectOption selector="{{AdminCustomerAccountInformationSection.group}}" userInput="{{customerData.group}}" stepKey="selectCustomerGroup"/> -+ <fillField stepKey="FillFirstName" selector="{{AdminCustomerAccountInformationSection.firstName}}" userInput="{{customerData.firstname}}"/> -+ <fillField stepKey="FillLastName" selector="{{AdminCustomerAccountInformationSection.lastName}}" userInput="{{customerData.lastname}}"/> -+ <fillField stepKey="FillEmail" selector="{{AdminCustomerAccountInformationSection.email}}" userInput="{{customerData.email}}"/> -+ <selectOption stepKey="selectStoreView" selector="{{AdminCustomerAccountInformationSection.storeView}}" userInput="{{storeView}}"/> -+ <waitForElement selector="{{AdminCustomerAccountInformationSection.storeView}}" stepKey="waitForCustomerStoreViewExpand"/> -+ <click stepKey="save" selector="{{AdminCustomerAccountInformationSection.saveCustomer}}"/> -+ <waitForPageLoad stepKey="waitForCustomersPage"/> -+ <see stepKey="seeSuccessMessage" userInput="You saved the customer."/> -+ </actionGroup> -+</actionGroups> -diff --git a/app/code/Magento/Customer/Test/Mftf/ActionGroup/AdminCustomerGridActionGroup.xml b/app/code/Magento/Customer/Test/Mftf/ActionGroup/AdminCustomerGridActionGroup.xml -new file mode 100644 -index 00000000000..86039056999 ---- /dev/null -+++ b/app/code/Magento/Customer/Test/Mftf/ActionGroup/AdminCustomerGridActionGroup.xml -@@ -0,0 +1,23 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> -+ <actionGroup name="AdminFilterCustomerByEmail"> -+ <arguments> -+ <argument name="email" type="string"/> -+ </arguments> -+ <amOnPage url="{{AdminCustomerPage.url}}" stepKey="openCustomerIndexPage"/> -+ <waitForPageLoad stepKey="waitToCustomerIndexPageToLoad"/> -+ <click selector="{{AdminDataGridHeaderSection.filters}}" stepKey="openFiltersSectionOnCustomerIndexPage"/> -+ <conditionalClick selector="{{AdminDataGridHeaderSection.clearFilters}}" dependentSelector="{{AdminDataGridHeaderSection.clearFilters}}" visible="true" stepKey="cleanFiltersIfTheySet"/> -+ <fillField userInput="{{email}}" selector="{{AdminCustomerFiltersSection.emailInput}}" stepKey="filterEmail"/> -+ <click selector="{{AdminCustomerFiltersSection.apply}}" stepKey="applyFilter"/> -+ <waitForPageLoad stepKey="waitForPageToLoad"/> -+ </actionGroup> -+</actionGroups> -diff --git a/app/code/Magento/Customer/Test/Mftf/ActionGroup/AdminCustomerSaveAndContinueActionGroup.xml b/app/code/Magento/Customer/Test/Mftf/ActionGroup/AdminCustomerSaveAndContinueActionGroup.xml -new file mode 100644 -index 00000000000..03b950a6dbe ---- /dev/null -+++ b/app/code/Magento/Customer/Test/Mftf/ActionGroup/AdminCustomerSaveAndContinueActionGroup.xml -@@ -0,0 +1,14 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> -+ <!-- Save Customer and Assert Success Message --> -+ <actionGroup name="AdminCustomerSaveAndContinue" > -+ <click selector="{{AdminCustomerMainActionsSection.saveAndContinue}}" stepKey="saveAndContinue"/> -+ </actionGroup> -+</actionGroups> -diff --git a/app/code/Magento/Customer/Test/Mftf/ActionGroup/AdminCustomerShopingCartActionGroup.xml b/app/code/Magento/Customer/Test/Mftf/ActionGroup/AdminCustomerShopingCartActionGroup.xml -new file mode 100644 -index 00000000000..f5d5682e374 ---- /dev/null -+++ b/app/code/Magento/Customer/Test/Mftf/ActionGroup/AdminCustomerShopingCartActionGroup.xml -@@ -0,0 +1,27 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> -+ <actionGroup name="AdminAddProductToShoppingCartActionGroup"> -+ <arguments> -+ <argument name="productName" type="string"/> -+ </arguments> -+ <waitForElementVisible selector="{{AdminCustomerShoppingCartProductItemSection.productItem}}" stepKey="waitForElementVisible"/> -+ <click selector="{{AdminCustomerShoppingCartProductItemSection.productItem}}" stepKey="expandProductItem"/> -+ <waitForElementVisible selector="{{AdminCustomerShoppingCartProductItemSection.productNameFilter}}" stepKey="waitForProductFilterFieldVisible"/> -+ <fillField selector="{{AdminCustomerShoppingCartProductItemSection.productNameFilter}}" stepKey="setProductName" userInput="{{productName}}"/> -+ <click selector="{{AdminCustomerShoppingCartProductItemSection.searchButton}}" stepKey="clickSearchButton"/> -+ <waitForAjaxLoad stepKey="waitForAjax"/> -+ <waitForElementVisible selector="{{AdminCustomerShoppingCartProductItemSection.firstProductCheckbox}}" stepKey="waitForElementCheckboxVisible"/> -+ <click selector="{{AdminCustomerShoppingCartProductItemSection.firstProductCheckbox}}" stepKey="selectFirstCheckbox"/> -+ <click selector="{{AdminCustomerShoppingCartProductItemSection.addSelectionsToMyCartButton}}" stepKey="clickAddSelectionsToMyCartButton" after="selectFirstCheckbox"/> -+ <waitForAjaxLoad stepKey="waitForAjax2"/> -+ <seeElement stepKey="seeAddedProduct" selector="{{AdminCustomerShoppingCartProductItemSection.addedProductName('productName')}}"/> -+ </actionGroup> -+</actionGroups> -diff --git a/app/code/Magento/Customer/Test/Mftf/ActionGroup/AdminDeleteAddressInCustomersAddressGridActionGroup.xml b/app/code/Magento/Customer/Test/Mftf/ActionGroup/AdminDeleteAddressInCustomersAddressGridActionGroup.xml -new file mode 100644 -index 00000000000..b61b353714e ---- /dev/null -+++ b/app/code/Magento/Customer/Test/Mftf/ActionGroup/AdminDeleteAddressInCustomersAddressGridActionGroup.xml -@@ -0,0 +1,22 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> -+ <!--Delete customer address from grid row, row starts at 0 --> -+ <actionGroup name="AdminDeleteAddressInCustomersAddressGrid"> -+ <arguments> -+ <argument name="row" type="string"/> -+ </arguments> -+ <click selector="{{AdminCustomerAddressesGridSection.checkboxByRow(row)}}" stepKey="clickRowCustomerAddressCheckbox"/> -+ <click selector="{{AdminCustomerAddressesGridSection.selectLinkByRow(row)}}" stepKey="openActionsDropdown"/> -+ <click selector="{{AdminCustomerAddressesGridSection.deleteLinkByRow(row)}}" stepKey="chooseDeleteOption"/> -+ <waitForPageLoad stepKey="waitForCustomerAddressesGridPageLoad"/> -+ <click selector="{{AdminCustomerAddressesGridActionsSection.ok}}" stepKey="clickOkOnPopup"/> -+ </actionGroup> -+</actionGroups> -diff --git a/app/code/Magento/Customer/Test/Mftf/ActionGroup/AdminDeleteCustomerActionGroup.xml b/app/code/Magento/Customer/Test/Mftf/ActionGroup/AdminDeleteCustomerActionGroup.xml -new file mode 100644 -index 00000000000..d08f10b2241 ---- /dev/null -+++ b/app/code/Magento/Customer/Test/Mftf/ActionGroup/AdminDeleteCustomerActionGroup.xml -@@ -0,0 +1,24 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> -+ <actionGroup name="AdminDeleteCustomerActionGroup"> -+ <arguments> -+ <argument name="customerEmail"/> -+ </arguments> -+ <amOnPage url="{{AdminCustomerPage.url}}" stepKey="navigateToCustomersPage"/> -+ <conditionalClick selector="{{AdminCustomerFiltersSection.clearAll}}" dependentSelector="{{AdminCustomerFiltersSection.clearAll}}" visible="true" stepKey="clickClearFilters"/> -+ <click stepKey="chooseCustomer" selector="{{AdminCustomerGridMainActionsSection.customerCheckbox(customerEmail)}}"/> -+ <click stepKey="openActions" selector="{{AdminCustomerGridMainActionsSection.actions}}"/> -+ <waitForPageLoad stepKey="waitActions"/> -+ <click stepKey="delete" selector="{{AdminCustomerGridMainActionsSection.delete}}"/> -+ <waitForPageLoad stepKey="waitForConfirmationAlert"/> -+ <click stepKey="accept" selector="{{AdminCustomerGridMainActionsSection.ok}}"/> -+ <see stepKey="seeSuccessMessage" userInput="were deleted."/> -+ </actionGroup> -+</actionGroups> -diff --git a/app/code/Magento/Customer/Test/Mftf/ActionGroup/AdminDeleteCustomerGroupActionGroup.xml b/app/code/Magento/Customer/Test/Mftf/ActionGroup/AdminDeleteCustomerGroupActionGroup.xml -new file mode 100644 -index 00000000000..788e5f8967f ---- /dev/null -+++ b/app/code/Magento/Customer/Test/Mftf/ActionGroup/AdminDeleteCustomerGroupActionGroup.xml -@@ -0,0 +1,26 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> -+ <actionGroup name="AdminDeleteCustomerGroupActionGroup"> -+ <arguments> -+ <argument name="customerGroupName" type="string"/> -+ </arguments> -+ <amOnPage url="{{AdminCustomerGroupsIndexPage.url}}" stepKey="goToAdminCustomerGroupIndexPage"/> -+ <waitForPageLoad time="30" stepKey="waitForCustomerGroupIndexPageLoad"/> -+ <click selector="{{AdminDataGridHeaderSection.filters}}" stepKey="openFiltersSectionOnCustomerGroupIndexPage"/> -+ <conditionalClick selector="{{AdminDataGridHeaderSection.clearFilters}}" dependentSelector="{{AdminDataGridHeaderSection.clearFilters}}" visible="true" stepKey="cleanFiltersIfTheySet"/> -+ <fillField userInput="{{customerGroupName}}" selector="{{AdminDataGridHeaderSection.filterFieldInput('customer_group_code')}}" stepKey="fillNameFieldOnFiltersSection"/> -+ <click selector="{{AdminDataGridHeaderSection.applyFilters}}" stepKey="clickApplyFiltersButton"/> -+ <click selector="{{AdminCustomerGroupGridActionsSection.selectButton(customerGroupName)}}" stepKey="clickSelectButton"/> -+ <click selector="{{AdminCustomerGroupGridActionsSection.deleteAction(customerGroupName)}}" stepKey="clickOnDeleteItem"/> -+ <click selector="{{AdminConfirmationModalSection.ok}}" stepKey="confirmDeleteCustomerGroup"/> -+ <seeElement selector="{{AdminMessagesSection.success}}" stepKey="seeSuccessMessage"/> -+ </actionGroup> -+</actionGroups> -diff --git a/app/code/Magento/Customer/Test/Mftf/ActionGroup/AdminEditCustomerAddressNoZipNoStateActionGroup.xml b/app/code/Magento/Customer/Test/Mftf/ActionGroup/AdminEditCustomerAddressNoZipNoStateActionGroup.xml -new file mode 100644 -index 00000000000..954b83bead1 ---- /dev/null -+++ b/app/code/Magento/Customer/Test/Mftf/ActionGroup/AdminEditCustomerAddressNoZipNoStateActionGroup.xml -@@ -0,0 +1,16 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> -+ <actionGroup name="AdminEditCustomerAddressNoZipNoState" extends="AdminEditCustomerAddressesFrom"> -+ <remove keyForRemoval="selectState"/> -+ <remove keyForRemoval="fillZipCode"/> -+ <click selector="{{AdminEditCustomerAddressesSection.defaultBillingAddressButton}}" stepKey="setDefaultBilling" before="setDefaultShipping"/> -+ <click selector="{{AdminEditCustomerAddressesSection.defaultShippingAddressButton}}" stepKey="setDefaultShipping" before="fillPrefixName"/> -+ </actionGroup> -+</actionGroups> -diff --git a/app/code/Magento/Customer/Test/Mftf/ActionGroup/AdminEditCustomerAddressSetDefaultShippingAndBillingActionGroup.xml b/app/code/Magento/Customer/Test/Mftf/ActionGroup/AdminEditCustomerAddressSetDefaultShippingAndBillingActionGroup.xml -new file mode 100644 -index 00000000000..0c1af1cb5b6 ---- /dev/null -+++ b/app/code/Magento/Customer/Test/Mftf/ActionGroup/AdminEditCustomerAddressSetDefaultShippingAndBillingActionGroup.xml -@@ -0,0 +1,14 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> -+ <actionGroup name="AdminEditCustomerAddressSetDefaultShippingAndBilling" extends="AdminEditCustomerAddressesFrom"> -+ <click selector="{{AdminEditCustomerAddressesSection.defaultBillingAddressButton}}" stepKey="setDefaultBilling" before="setDefaultShipping"/> -+ <click selector="{{AdminEditCustomerAddressesSection.defaultShippingAddressButton}}" stepKey="setDefaultShipping" before="fillPrefixName"/> -+ </actionGroup> -+</actionGroups> -diff --git a/app/code/Magento/Customer/Test/Mftf/ActionGroup/AdminEditCustomerAddressesFromActionGroup.xml b/app/code/Magento/Customer/Test/Mftf/ActionGroup/AdminEditCustomerAddressesFromActionGroup.xml -new file mode 100644 -index 00000000000..9c38f23739b ---- /dev/null -+++ b/app/code/Magento/Customer/Test/Mftf/ActionGroup/AdminEditCustomerAddressesFromActionGroup.xml -@@ -0,0 +1,51 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> -+ <!-- Same as "EditCustomerAddressesFromAdminActionGroup" but taking country and state from input "customerAddress" --> -+ <actionGroup name="AdminEditCustomerAddressesFrom" > -+ <arguments> -+ <argument name="customerAddress" type="entity"/> -+ </arguments> -+ <click selector="{{AdminEditCustomerAddressesSection.addresses}}" stepKey="proceedToAddresses"/> -+ <click selector="{{AdminEditCustomerAddressesSection.addNewAddress}}" stepKey="addNewAddresses"/> -+ <waitForPageLoad time="60" stepKey="wait5678" /> -+ <fillField stepKey="fillPrefixName" userInput="{{customerAddress.prefix}}" selector="{{AdminEditCustomerAddressesSection.prefixName}}"/> -+ <fillField stepKey="fillMiddleName" userInput="{{customerAddress.middlename}}" selector="{{AdminEditCustomerAddressesSection.middleName}}"/> -+ <fillField stepKey="fillSuffixName" userInput="{{customerAddress.suffix}}" selector="{{AdminEditCustomerAddressesSection.suffixName}}"/> -+ <fillField stepKey="fillCompany" userInput="{{customerAddress.company}}" selector="{{AdminEditCustomerAddressesSection.company}}"/> -+ <fillField stepKey="fillStreetAddress" userInput="{{customerAddress.street[0]}}" selector="{{AdminEditCustomerAddressesSection.streetAddress}}"/> -+ <fillField stepKey="fillCity" userInput="{{customerAddress.city}}" selector="{{AdminEditCustomerAddressesSection.city}}"/> -+ <selectOption stepKey="selectCountry" selector="{{AdminEditCustomerAddressesSection.country}}" userInput="{{customerAddress.country_id}}"/> -+ <selectOption stepKey="selectState" selector="{{AdminEditCustomerAddressesSection.state}}" userInput="{{customerAddress.state}}"/> -+ <fillField stepKey="fillZipCode" userInput="{{customerAddress.postcode}}" selector="{{AdminEditCustomerAddressesSection.zipCode}}"/> -+ <fillField stepKey="fillPhone" userInput="{{customerAddress.telephone}}" selector="{{AdminEditCustomerAddressesSection.phone}}"/> -+ <fillField stepKey="fillVAT" userInput="{{customerAddress.vat_id}}" selector="{{AdminEditCustomerAddressesSection.vat}}"/> -+ <click selector="{{AdminEditCustomerAddressesSection.save}}" stepKey="saveAddress"/> -+ <waitForPageLoad stepKey="waitForAddressSaved"/> -+ </actionGroup> -+ <actionGroup name="AdminEditCustomerAddressSetDefaultShippingAndBilling" extends="AdminEditCustomerAddressesFrom"> -+ <click selector="{{AdminEditCustomerAddressesSection.defaultBillingAddressButton}}" stepKey="setDefaultBilling" before="setDefaultShipping"/> -+ <click selector="{{AdminEditCustomerAddressesSection.defaultShippingAddressButton}}" stepKey="setDefaultShipping" before="fillPrefixName"/> -+ </actionGroup> -+ <actionGroup name="AdminEditCustomerAddressNoZipNoState" extends="AdminEditCustomerAddressesFrom"> -+ <remove keyForRemoval="selectState"/> -+ <remove keyForRemoval="fillZipCode"/> -+ <click selector="{{AdminEditCustomerAddressesSection.defaultBillingAddressButton}}" stepKey="setDefaultBilling" before="setDefaultShipping"/> -+ <click selector="{{AdminEditCustomerAddressesSection.defaultShippingAddressButton}}" stepKey="setDefaultShipping" before="fillPrefixName"/> -+ </actionGroup> -+ <actionGroup name="SelectDropdownCustomerAddressAttributeValueActionGroup"> -+ <arguments> -+ <argument name="customerAddressAttribute"/> -+ <argument name="optionValue" type="string"/> -+ </arguments> -+ <selectOption selector="{{AdminEditCustomerAddressesSection.dropDownAttribute(customerAddressAttribute.code)}}" userInput="{{optionValue}}" stepKey="selectOptionValue"/> -+ <click selector="{{AdminEditCustomerAddressesSection.save}}" stepKey="saveAddress"/> -+ <waitForPageLoad stepKey="waitForAddressSaved"/> -+ </actionGroup> -+</actionGroups> -diff --git a/app/code/Magento/Customer/Test/Mftf/ActionGroup/AdminEditCustomerInformationFromActionGroup.xml b/app/code/Magento/Customer/Test/Mftf/ActionGroup/AdminEditCustomerInformationFromActionGroup.xml -new file mode 100644 -index 00000000000..ddeefeb3c37 ---- /dev/null -+++ b/app/code/Magento/Customer/Test/Mftf/ActionGroup/AdminEditCustomerInformationFromActionGroup.xml -@@ -0,0 +1,28 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> -+ <!-- Edit Customer Account Information Required Fields in Admin --> -+ <actionGroup name="AdminEditCustomerAccountInformationActionGroup" > -+ <arguments> -+ <argument name="firstName" type="string"/> -+ <argument name="lastName" type="string"/> -+ <argument name="email" type="string"/> -+ </arguments> -+ <click selector="{{AdminCustomerAccountInformationSection.accountInformationTab}}" stepKey="goToAccountInformation"/> -+ <clearField stepKey="clearFirstName" selector="{{AdminCustomerAccountInformationSection.firstName}}"/> -+ <fillField stepKey="fillFirstName" userInput="{{firstName}}" selector="{{AdminCustomerAccountInformationSection.firstName}}"/> -+ <clearField stepKey="clearLastName" selector="{{AdminCustomerAccountInformationSection.lastName}}"/> -+ <fillField stepKey="fillLastName" userInput="{{lastName}}" selector="{{AdminCustomerAccountInformationSection.lastName}}"/> -+ <clearField stepKey="clearEmail" selector="{{AdminCustomerAccountInformationSection.email}}"/> -+ <fillField stepKey="fillEmail" userInput="{{email}}" selector="{{AdminCustomerAccountInformationSection.email}}"/> -+ <click selector="{{AdminCustomerMainActionsSection.saveAndContinue}}" stepKey="saveAndContinue"/> -+ <waitForPageLoad stepKey="wait"/> -+ <scrollToTopOfPage stepKey="scrollToTop"/> -+ </actionGroup> -+</actionGroups> -diff --git a/app/code/Magento/Customer/Test/Mftf/ActionGroup/AdminFilterCustomerAddressGridByPhoneNumberActionGroup.xml b/app/code/Magento/Customer/Test/Mftf/ActionGroup/AdminFilterCustomerAddressGridByPhoneNumberActionGroup.xml -new file mode 100644 -index 00000000000..2d0d44a4cc5 ---- /dev/null -+++ b/app/code/Magento/Customer/Test/Mftf/ActionGroup/AdminFilterCustomerAddressGridByPhoneNumberActionGroup.xml -@@ -0,0 +1,21 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> -+ <!--Filter customer address grid by phone number --> -+ <actionGroup name="AdminFilterCustomerAddressGridByPhoneNumber"> -+ <arguments> -+ <argument name="phone" type="string"/> -+ </arguments> -+ <conditionalClick selector="{{AdminCustomerAddressFiltersSection.clearAll}}" dependentSelector="{{AdminCustomerAddressFiltersSection.clearAll}}" visible="true" stepKey="clickClearFilters"/> -+ <click selector="{{AdminCustomerAddressFiltersSection.filtersButton}}" stepKey="openFilters"/> -+ <fillField selector="{{AdminCustomerAddressFiltersSection.telephoneInput}}" userInput="{{phone}}" stepKey="fill"/> -+ <click selector="{{AdminCustomerAddressFiltersSection.applyFilter}}" stepKey="clickApplyFilters"/> -+ </actionGroup> -+</actionGroups> -diff --git a/app/code/Magento/Customer/Test/Mftf/ActionGroup/AdminFilterCustomerByNameActionGroup.xml b/app/code/Magento/Customer/Test/Mftf/ActionGroup/AdminFilterCustomerByNameActionGroup.xml -new file mode 100644 -index 00000000000..c49a0dbe20a ---- /dev/null -+++ b/app/code/Magento/Customer/Test/Mftf/ActionGroup/AdminFilterCustomerByNameActionGroup.xml -@@ -0,0 +1,20 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> -+ <actionGroup name="AdminFilterCustomerByName"> -+ <arguments> -+ <argument name="customerName" type="string"/> -+ </arguments> -+ <click selector="{{AdminDataGridHeaderSection.filters}}" stepKey="openFiltersSectionOnCustomerGroupIndexPage"/> -+ <conditionalClick selector="{{AdminDataGridHeaderSection.clearFilters}}" dependentSelector="{{AdminDataGridHeaderSection.clearFilters}}" visible="true" stepKey="cleanFiltersIfTheySet"/> -+ <fillField userInput="{{customerName}}" selector="{{AdminDataGridHeaderSection.filterFieldInput('name')}}" stepKey="fillNameFieldOnFiltersSection"/> -+ <click selector="{{AdminDataGridHeaderSection.applyFilters}}" stepKey="clickApplyFiltersButton"/> -+ </actionGroup> -+</actionGroups> -diff --git a/app/code/Magento/Customer/Test/Mftf/ActionGroup/AdminFilterCustomerGridByEmailActionGroup.xml b/app/code/Magento/Customer/Test/Mftf/ActionGroup/AdminFilterCustomerGridByEmailActionGroup.xml -new file mode 100644 -index 00000000000..9cab8a790ff ---- /dev/null -+++ b/app/code/Magento/Customer/Test/Mftf/ActionGroup/AdminFilterCustomerGridByEmailActionGroup.xml -@@ -0,0 +1,21 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> -+ <!--Filter customer grid by the email --> -+ <actionGroup name="AdminFilterCustomerGridByEmail"> -+ <arguments> -+ <argument name="email" type="string"/> -+ </arguments> -+ <conditionalClick selector="{{AdminCustomerFiltersSection.clearAll}}" dependentSelector="{{AdminCustomerFiltersSection.clearAll}}" visible="true" stepKey="clickClearFilters"/> -+ <click selector="{{AdminCustomerFiltersSection.filtersButton}}" stepKey="openFilters"/> -+ <fillField selector="{{AdminCustomerFiltersSection.emailInput}}" userInput="{{email}}" stepKey="fillEmail"/> -+ <click selector="{{AdminCustomerFiltersSection.apply}}" stepKey="clickApplyFilters"/> -+ </actionGroup> -+</actionGroups> -diff --git a/app/code/Magento/Customer/Test/Mftf/ActionGroup/AdminFilterCustomerGroupByNameActionGroup.xml b/app/code/Magento/Customer/Test/Mftf/ActionGroup/AdminFilterCustomerGroupByNameActionGroup.xml -new file mode 100644 -index 00000000000..1681a8e850c ---- /dev/null -+++ b/app/code/Magento/Customer/Test/Mftf/ActionGroup/AdminFilterCustomerGroupByNameActionGroup.xml -@@ -0,0 +1,20 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> -+ <actionGroup name="AdminFilterCustomerGroupByNameActionGroup"> -+ <arguments> -+ <argument name="customerGroupName" type="string"/> -+ </arguments> -+ <click selector="{{AdminDataGridHeaderSection.filters}}" stepKey="openFiltersSectionOnCustomerGroupIndexPage"/> -+ <conditionalClick selector="{{AdminDataGridHeaderSection.clearFilters}}" dependentSelector="{{AdminDataGridHeaderSection.clearFilters}}" visible="true" stepKey="cleanFiltersIfTheySet"/> -+ <fillField userInput="{{customerGroupName}}" selector="{{AdminDataGridHeaderSection.filterFieldInput('customer_group_code')}}" stepKey="fillNameFieldOnFiltersSection"/> -+ <click selector="{{AdminDataGridHeaderSection.applyFilters}}" stepKey="clickApplyFiltersButton"/> -+ </actionGroup> -+</actionGroups> -diff --git a/app/code/Magento/Customer/Test/Mftf/ActionGroup/AdminOpenCustomerEditPageActionGroup.xml b/app/code/Magento/Customer/Test/Mftf/ActionGroup/AdminOpenCustomerEditPageActionGroup.xml -new file mode 100644 -index 00000000000..8e6b56b19d6 ---- /dev/null -+++ b/app/code/Magento/Customer/Test/Mftf/ActionGroup/AdminOpenCustomerEditPageActionGroup.xml -@@ -0,0 +1,17 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> -+ <actionGroup name="AdminOpenCustomerEditPageActionGroup"> -+ <arguments> -+ <argument name="customerId" type="string" /> -+ </arguments> -+ <amOnPage url="{{AdminEditCustomerPage.url(customerId)}}" stepKey="openCustomerEditPage" /> -+ <waitForPageLoad stepKey="waitForPageLoad" /> -+ </actionGroup> -+</actionGroups> -diff --git a/app/code/Magento/Customer/Test/Mftf/ActionGroup/AdminResetFilterInCustomerAddressGridActionGroup.xml b/app/code/Magento/Customer/Test/Mftf/ActionGroup/AdminResetFilterInCustomerAddressGridActionGroup.xml -new file mode 100644 -index 00000000000..135f0107841 ---- /dev/null -+++ b/app/code/Magento/Customer/Test/Mftf/ActionGroup/AdminResetFilterInCustomerAddressGridActionGroup.xml -@@ -0,0 +1,19 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> -+ <!--Reset customers grid filter and to default view --> -+ <actionGroup name="AdminResetFilterInCustomerAddressGrid"> -+ <conditionalClick selector="{{AdminCustomerAddressFiltersSection.clearAll}}" dependentSelector="{{AdminCustomerAddressFiltersSection.clearAll}}" visible="true" stepKey="clickClearFilters"/> -+ <click selector="{{AdminCustomerAddressFiltersSection.viewDropdown}}" stepKey="openViewBookmarksTab"/> -+ <click selector="{{AdminCustomerAddressFiltersSection.viewBookmark('Default View')}}" stepKey="resetToDefaultGridView"/> -+ <waitForPageLoad stepKey="waitForGridLoad"/> -+ <see selector="{{AdminCustomerFiltersSection.viewDropdown}}" userInput="Default View" stepKey="seeDefaultViewSelected"/> -+ </actionGroup> -+</actionGroups> -diff --git a/app/code/Magento/Customer/Test/Mftf/ActionGroup/AdminResetFilterInCustomerGridActionGroup.xml b/app/code/Magento/Customer/Test/Mftf/ActionGroup/AdminResetFilterInCustomerGridActionGroup.xml -new file mode 100644 -index 00000000000..5c6ff347d56 ---- /dev/null -+++ b/app/code/Magento/Customer/Test/Mftf/ActionGroup/AdminResetFilterInCustomerGridActionGroup.xml -@@ -0,0 +1,19 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> -+ <!--Reset customers grid filter and to default view --> -+ <actionGroup name="AdminResetFilterInCustomerGrid"> -+ <conditionalClick selector="{{AdminCustomerFiltersSection.clearAll}}" dependentSelector="{{AdminCustomerFiltersSection.clearAll}}" visible="true" stepKey="clickClearFilters"/> -+ <click selector="{{AdminCustomerFiltersSection.viewDropdown}}" stepKey="openViewBookmarksTab"/> -+ <click selector="{{AdminCustomerFiltersSection.viewBookmark('Default View')}}" stepKey="resetToDefaultGridView"/> -+ <waitForPageLoad stepKey="waitForGridLoad"/> -+ <see selector="{{AdminCustomerFiltersSection.viewDropdown}}" userInput="Default View" stepKey="seeDefaultViewSelected"/> -+ </actionGroup> -+</actionGroups> -diff --git a/app/code/Magento/Customer/Test/Mftf/ActionGroup/AdminSaveCustomerAddressActionGroup.xml b/app/code/Magento/Customer/Test/Mftf/ActionGroup/AdminSaveCustomerAddressActionGroup.xml -new file mode 100644 -index 00000000000..e47aa8809f0 ---- /dev/null -+++ b/app/code/Magento/Customer/Test/Mftf/ActionGroup/AdminSaveCustomerAddressActionGroup.xml -@@ -0,0 +1,14 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> -+ <actionGroup name="AdminSaveCustomerAddressActionGroup"> -+ <click selector="{{StorefrontCustomerAddressFormSection.saveAddress}}" stepKey="saveCustomerAddress"/> -+ <see selector="{{AdminMessagesSection.successMessage}}" userInput="You saved the address." stepKey="seeSuccessMessage"/> -+ </actionGroup> -+</actionGroups> -diff --git a/app/code/Magento/Customer/Test/Mftf/ActionGroup/AdminSaveCustomerAndAssertSuccessMessageActionGroup.xml b/app/code/Magento/Customer/Test/Mftf/ActionGroup/AdminSaveCustomerAndAssertSuccessMessageActionGroup.xml -new file mode 100644 -index 00000000000..d3907e96b0d ---- /dev/null -+++ b/app/code/Magento/Customer/Test/Mftf/ActionGroup/AdminSaveCustomerAndAssertSuccessMessageActionGroup.xml -@@ -0,0 +1,15 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> -+ <!-- Save Customer and Assert Success Message --> -+ <actionGroup name="AdminSaveCustomerAndAssertSuccessMessage" > -+ <click selector="{{AdminCustomerMainActionsSection.saveButton}}" stepKey="saveCustomer"/> -+ <see userInput="You saved the customer" selector="{{AdminCustomerMessagesSection.successMessage}}" stepKey="seeMessage"/> -+ </actionGroup> -+</actionGroups> -diff --git a/app/code/Magento/Customer/Test/Mftf/ActionGroup/AdminSelectAllCustomersActionGroup.xml b/app/code/Magento/Customer/Test/Mftf/ActionGroup/AdminSelectAllCustomersActionGroup.xml -new file mode 100644 -index 00000000000..1a8b4da67e7 ---- /dev/null -+++ b/app/code/Magento/Customer/Test/Mftf/ActionGroup/AdminSelectAllCustomersActionGroup.xml -@@ -0,0 +1,14 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> -+ <actionGroup name="AdminSelectAllCustomers"> -+ <checkOption selector="{{AdminCustomerGridMainActionsSection.multicheck}}" stepKey="checkAllCustomers"/> -+ </actionGroup> -+</actionGroups> -diff --git a/app/code/Magento/Customer/Test/Mftf/ActionGroup/AdminSelectCustomerByEmailActionGroup.xml b/app/code/Magento/Customer/Test/Mftf/ActionGroup/AdminSelectCustomerByEmailActionGroup.xml -new file mode 100644 -index 00000000000..bb84d578fd9 ---- /dev/null -+++ b/app/code/Magento/Customer/Test/Mftf/ActionGroup/AdminSelectCustomerByEmailActionGroup.xml -@@ -0,0 +1,17 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> -+ <actionGroup name="AdminSelectCustomerByEmail"> -+ <arguments> -+ <argument name="customerEmail" type="string"/> -+ </arguments> -+ <checkOption selector="{{AdminCustomerGridSection.customerCheckboxByEmail(customerEmail)}}" stepKey="checkCustomerBox"/> -+ </actionGroup> -+</actionGroups> -diff --git a/app/code/Magento/Customer/Test/Mftf/ActionGroup/AdminUpdateCustomerGroupActionGroup.xml b/app/code/Magento/Customer/Test/Mftf/ActionGroup/AdminUpdateCustomerGroupActionGroup.xml -new file mode 100644 -index 00000000000..b1b82fb9fb7 ---- /dev/null -+++ b/app/code/Magento/Customer/Test/Mftf/ActionGroup/AdminUpdateCustomerGroupActionGroup.xml -@@ -0,0 +1,38 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> -+ <actionGroup name="AdminUpdateCustomerGroupByEmailActionGroup"> -+ <arguments> -+ <argument name="emailAddress"/> -+ <argument name="customerGroup" type="string"/> -+ </arguments> -+ -+ <amOnPage url="{{AdminCustomerPage.url}}" stepKey="goToCustomerPage01"/> -+ -+ <!-- Start of Action Group: searchAdminDataGridByKeyword --> -+ <conditionalClick selector="{{AdminDataGridHeaderSection.clearFilters}}" dependentSelector="{{AdminDataGridHeaderSection.clearFilters}}" visible="true" stepKey="clickClearFilters0"/> -+ <fillField selector="{{AdminDataGridHeaderSection.search}}" userInput="{{emailAddress}}" stepKey="fillKeywordSearchField01"/> -+ <click selector="{{AdminDataGridHeaderSection.submitSearch}}" stepKey="clickKeywordSearch01"/> -+ <waitForPageLoad stepKey="waitForPageLoad02"/> -+ <!-- End of Action Group: searchAdminDataGridByKeyword --> -+ -+ <click selector="{{AdminGridRow.editByValue(emailAddress)}}" stepKey="clickOnCustomer01"/> -+ <waitForPageLoad stepKey="waitForPageLoad03"/> -+ -+ <conditionalClick selector="{{AdminCustomerAccountInformationSection.accountInformationTab}}" dependentSelector="{{AdminCustomerAccountInformationSection.accountInformationTab}}" visible="true" stepKey="clickOnAccountInformation01"/> -+ <waitForPageLoad stepKey="waitForPageLoad04"/> -+ -+ <click selector="{{AdminCustomerAccountInformationSection.group}}" stepKey="clickOnCustomerGroup01"/> -+ <selectOption selector="{{AdminCustomerAccountInformationSection.group}}" userInput="{{customerGroup}}" stepKey="selectCustomerGroup01"/> -+ -+ <click selector="{{AdminMainActionsSection.save}}" stepKey="clickOnSave01"/> -+ <waitForPageLoad stepKey="waitForPageLoad05"/> -+ </actionGroup> -+</actionGroups> -diff --git a/app/code/Magento/Customer/Test/Mftf/ActionGroup/AssertAuthorizationPopUpPasswordAutoCompleteOffActionGroup.xml b/app/code/Magento/Customer/Test/Mftf/ActionGroup/AssertAuthorizationPopUpPasswordAutoCompleteOffActionGroup.xml -new file mode 100644 -index 00000000000..186d0244e8c ---- /dev/null -+++ b/app/code/Magento/Customer/Test/Mftf/ActionGroup/AssertAuthorizationPopUpPasswordAutoCompleteOffActionGroup.xml -@@ -0,0 +1,14 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> -+ <actionGroup name="AssertAuthorizationPopUpPasswordAutoCompleteOffActionGroup"> -+ <waitForElementVisible selector="{{StorefrontCustomerSignInPopupFormSection.password}}" stepKey="waitPasswordFieldVisible"/> -+ <assertElementContainsAttribute selector="{{StorefrontCustomerSignInPopupFormSection.password}}" attribute="autocomplete" expectedValue="off" stepKey="assertAuthorizationPopupPasswordAutocompleteOff"/> -+ </actionGroup> -+</actionGroups> -diff --git a/app/code/Magento/Customer/Test/Mftf/ActionGroup/AssertCustomerAccountPageTitleActionGroup.xml b/app/code/Magento/Customer/Test/Mftf/ActionGroup/AssertCustomerAccountPageTitleActionGroup.xml -new file mode 100644 -index 00000000000..132b5ca8188 ---- /dev/null -+++ b/app/code/Magento/Customer/Test/Mftf/ActionGroup/AssertCustomerAccountPageTitleActionGroup.xml -@@ -0,0 +1,16 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> -+ <actionGroup name="AssertCustomerAccountPageTitleActionGroup"> -+ <arguments> -+ <argument name="pageTitle" type="string" /> -+ </arguments> -+ <see selector="{{StorefrontCustomerAccountMainSection.pageTitle}}" userInput="{{pageTitle}}" stepKey="assertPageTitle" /> -+ </actionGroup> -+</actionGroups> -diff --git a/app/code/Magento/Customer/Test/Mftf/ActionGroup/AssertCustomerGroupNotInGridActionGroup.xml b/app/code/Magento/Customer/Test/Mftf/ActionGroup/AssertCustomerGroupNotInGridActionGroup.xml -new file mode 100644 -index 00000000000..26c4f23fc9a ---- /dev/null -+++ b/app/code/Magento/Customer/Test/Mftf/ActionGroup/AssertCustomerGroupNotInGridActionGroup.xml -@@ -0,0 +1,20 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> -+ <actionGroup name="AssertCustomerGroupNotInGridActionGroup"> -+ <arguments> -+ <argument name="customerGroup" type="entity" /> -+ </arguments> -+ <conditionalClick selector="{{AdminDataGridHeaderSection.clearFilters}}" dependentSelector="{{AdminDataGridHeaderSection.clearFilters}}" visible="true" stepKey="cleanFilters"/> -+ <click selector="{{AdminDataGridHeaderSection.filters}}" stepKey="openFiltersSectionOnCustomerGroupIndexPage"/> -+ <fillField userInput="{{customerGroup.code}}" selector="{{AdminDataGridHeaderSection.filterFieldInput('customer_group_code')}}" stepKey="fillNameFieldOnFiltersSection"/> -+ <click selector="{{AdminDataGridHeaderSection.applyFilters}}" stepKey="clickApplyFiltersButton"/> -+ <see selector="{{AdminDataGridTableSection.dataGridEmpty}}" userInput="We couldn't find any records." stepKey="assertDataGridEmptyMessage"/> -+ </actionGroup> -+</actionGroups> -diff --git a/app/code/Magento/Customer/Test/Mftf/ActionGroup/AssertCustomerGroupNotOnProductFormActionGroup.xml b/app/code/Magento/Customer/Test/Mftf/ActionGroup/AssertCustomerGroupNotOnProductFormActionGroup.xml -new file mode 100644 -index 00000000000..94e01db5c1f ---- /dev/null -+++ b/app/code/Magento/Customer/Test/Mftf/ActionGroup/AssertCustomerGroupNotOnProductFormActionGroup.xml -@@ -0,0 +1,23 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> -+ <actionGroup name="AssertCustomerGroupNotOnProductFormActionGroup"> -+ <arguments> -+ <argument name="customerGroup" type="entity" /> -+ </arguments> -+ <click selector="{{AdminProductFormSection.advancedPricingLink}}" stepKey="clickOnAdvancedPricingButton"/> -+ <waitForElementVisible selector="{{AdminProductFormAdvancedPricingSection.customerGroupPriceAddButton}}" stepKey="waitForCustomerGroupPriceAddButton"/> -+ <click selector="{{AdminProductFormAdvancedPricingSection.customerGroupPriceAddButton}}" stepKey="addCustomerGroupAllGroupsQty1PriceDiscountAnd10percent"/> -+ <grabMultiple selector="{{AdminProductFormAdvancedPricingSection.productTierPriceCustGroupSelectOptions('0')}}" stepKey="customerGroups" /> -+ <assertNotContains stepKey="assertCustomerGroupNotInOptions"> -+ <actualResult type="variable">customerGroups</actualResult> -+ <expectedResult type="string">{{customerGroup.code}}</expectedResult> -+ </assertNotContains> -+ </actionGroup> -+</actionGroups> -diff --git a/app/code/Magento/Customer/Test/Mftf/ActionGroup/AssertCustomerGroupOnCustomerFormActionGroup.xml b/app/code/Magento/Customer/Test/Mftf/ActionGroup/AssertCustomerGroupOnCustomerFormActionGroup.xml -new file mode 100644 -index 00000000000..2c8e0081e5e ---- /dev/null -+++ b/app/code/Magento/Customer/Test/Mftf/ActionGroup/AssertCustomerGroupOnCustomerFormActionGroup.xml -@@ -0,0 +1,18 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> -+ <actionGroup name="AssertCustomerGroupOnCustomerFormActionGroup"> -+ <arguments> -+ <argument name="customerGroup" type="entity" /> -+ </arguments> -+ <click selector="{{AdminCustomerAccountInformationSection.accountInformationTab}}" stepKey="clickOnAccountInfoTab" /> -+ <waitForPageLoad stepKey="waitForPageLoad" /> -+ <seeOptionIsSelected userInput="{{customerGroup.code}}" selector="{{AdminCustomerAccountInformationSection.group}}" stepKey="verifyNeededCustomerGroupSelected" /> -+ </actionGroup> -+</actionGroups> -diff --git a/app/code/Magento/Customer/Test/Mftf/ActionGroup/AssertCustomerLoggedInActionGroup.xml b/app/code/Magento/Customer/Test/Mftf/ActionGroup/AssertCustomerLoggedInActionGroup.xml -new file mode 100644 -index 00000000000..d2d4d86d7f9 ---- /dev/null -+++ b/app/code/Magento/Customer/Test/Mftf/ActionGroup/AssertCustomerLoggedInActionGroup.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="AssertCustomerWelcomeMessageActionGroup"> -+ <arguments> -+ <argument name="customerFullName" type="string" /> -+ </arguments> -+ <waitForPageLoad stepKey="waitForPageLoad"/> -+ <see userInput="Welcome, {{customerFullName}}!" selector="{{StorefrontPanelHeaderSection.welcomeMessage}}" stepKey="verifyMessage" /> -+ </actionGroup> -+</actionGroups> -diff --git a/app/code/Magento/Customer/Test/Mftf/ActionGroup/AssertCustomerResetPasswordActionGroup.xml b/app/code/Magento/Customer/Test/Mftf/ActionGroup/AssertCustomerResetPasswordActionGroup.xml -new file mode 100644 -index 00000000000..644254443d1 ---- /dev/null -+++ b/app/code/Magento/Customer/Test/Mftf/ActionGroup/AssertCustomerResetPasswordActionGroup.xml -@@ -0,0 +1,22 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> -+ <actionGroup name="AssertCustomerResetPasswordActionGroup"> -+ <arguments> -+ <argument name="url" type="string"/> -+ <argument name="message" type="string" defaultValue="" /> -+ <argument name="messageType" type="string" defaultValue="success" /> -+ </arguments> -+ -+ <waitForElementVisible selector="{{StorefrontCustomerLoginMessagesSection.messageByType(messageType)}}" stepKey="waitForMessage" /> -+ <see stepKey="seeMessage" userInput="{{message}}" selector="{{StorefrontCustomerLoginMessagesSection.messageByType(messageType)}}"/> -+ <seeInCurrentUrl stepKey="seeCorrectCurrentUrl" url="{{url}}"/> -+ </actionGroup> -+</actionGroups> -diff --git a/app/code/Magento/Customer/Test/Mftf/ActionGroup/AssertMessageCustomerChangeAccountInfoActionGroup.xml b/app/code/Magento/Customer/Test/Mftf/ActionGroup/AssertMessageCustomerChangeAccountInfoActionGroup.xml -new file mode 100644 -index 00000000000..ab48184ed98 ---- /dev/null -+++ b/app/code/Magento/Customer/Test/Mftf/ActionGroup/AssertMessageCustomerChangeAccountInfoActionGroup.xml -@@ -0,0 +1,18 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> -+ <actionGroup name="AssertMessageCustomerChangeAccountInfoActionGroup"> -+ <arguments> -+ <argument name="message" type="string" defaultValue="You saved the account information." /> -+ <argument name="messageType" type="string" defaultValue="success" /> -+ </arguments> -+ <see userInput="{{message}}" selector="{{StorefrontCustomerAccountMainSection.messageByType(messageType)}}" stepKey="verifyMessage" /> -+ </actionGroup> -+</actionGroups> -diff --git a/app/code/Magento/Customer/Test/Mftf/ActionGroup/AssertMessageCustomerCreateAccountActionGroup.xml b/app/code/Magento/Customer/Test/Mftf/ActionGroup/AssertMessageCustomerCreateAccountActionGroup.xml -new file mode 100644 -index 00000000000..65c9b025a9c ---- /dev/null -+++ b/app/code/Magento/Customer/Test/Mftf/ActionGroup/AssertMessageCustomerCreateAccountActionGroup.xml -@@ -0,0 +1,18 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> -+ <actionGroup name="AssertMessageCustomerCreateAccountActionGroup"> -+ <arguments> -+ <argument name="message" type="string" defaultValue="Thank you for registering with Main Website Store." /> -+ <argument name="messageType" type="string" defaultValue="success" /> -+ </arguments> -+ <see userInput="{{message}}" selector="{{StorefrontCustomerAccountMainSection.messageByType(messageType)}}" stepKey="verifyMessage" /> -+ </actionGroup> -+</actionGroups> -diff --git a/app/code/Magento/Customer/Test/Mftf/ActionGroup/AssertMessageCustomerLoginActionGroup.xml b/app/code/Magento/Customer/Test/Mftf/ActionGroup/AssertMessageCustomerLoginActionGroup.xml -new file mode 100644 -index 00000000000..6b886619858 ---- /dev/null -+++ b/app/code/Magento/Customer/Test/Mftf/ActionGroup/AssertMessageCustomerLoginActionGroup.xml -@@ -0,0 +1,18 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> -+ <actionGroup name="AssertMessageCustomerLoginActionGroup"> -+ <arguments> -+ <argument name="message" type="string" defaultValue="The account sign-in was incorrect or your account is disabled temporarily. Please wait and try again later." /> -+ <argument name="messageType" type="string" defaultValue="error" /> -+ </arguments> -+ <see userInput="{{message}}" selector="{{StorefrontCustomerLoginMessagesSection.messageByType(messageType)}}" stepKey="verifyMessage" /> -+ </actionGroup> -+</actionGroups> -diff --git a/app/code/Magento/Customer/Test/Mftf/ActionGroup/AssertStorefrontCustomerSavedCardActionGroup.xml b/app/code/Magento/Customer/Test/Mftf/ActionGroup/AssertStorefrontCustomerSavedCardActionGroup.xml -new file mode 100644 -index 00000000000..94cd759e5ce ---- /dev/null -+++ b/app/code/Magento/Customer/Test/Mftf/ActionGroup/AssertStorefrontCustomerSavedCardActionGroup.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="AssertStorefrontCustomerSavedCardActionGroup"> -+ <arguments> -+ <argument name="card" type="entity" defaultValue="StoredPaymentMethods"/> -+ </arguments> -+ <see selector="{{StorefrontCustomerStoredPaymentMethodsSection.cardNumber}}" userInput="{{card.cardNumberEnding}}" stepKey="verifyCardNumber"/> -+ <see selector="{{StorefrontCustomerStoredPaymentMethodsSection.expirationDate}}" userInput="{{card.cardExpire}}" stepKey="verifyCardExpire"/> -+ </actionGroup> -+</actionGroups> -diff --git a/app/code/Magento/Customer/Test/Mftf/ActionGroup/AssertStorefrontPasswordAutocompleteOffActionGroup.xml b/app/code/Magento/Customer/Test/Mftf/ActionGroup/AssertStorefrontPasswordAutocompleteOffActionGroup.xml -new file mode 100644 -index 00000000000..23a067cd94e ---- /dev/null -+++ b/app/code/Magento/Customer/Test/Mftf/ActionGroup/AssertStorefrontPasswordAutocompleteOffActionGroup.xml -@@ -0,0 +1,14 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> -+ <actionGroup name="AssertStorefrontPasswordAutoCompleteOffActionGroup"> -+ <amOnPage stepKey="amOnSignInPage" url="{{StorefrontCustomerSignInPage.url}}"/> -+ <assertElementContainsAttribute selector="{{StorefrontCustomerSignInFormSection.passwordField}}" attribute="autocomplete" expectedValue="off" stepKey="assertSignInPasswordAutocompleteOff"/> -+ </actionGroup> -+</actionGroups> -diff --git a/app/code/Magento/Customer/Test/Mftf/ActionGroup/CreateCustomerActionGroup.xml b/app/code/Magento/Customer/Test/Mftf/ActionGroup/CreateCustomerActionGroup.xml -new file mode 100644 -index 00000000000..047f656f5ea ---- /dev/null -+++ b/app/code/Magento/Customer/Test/Mftf/ActionGroup/CreateCustomerActionGroup.xml -@@ -0,0 +1,44 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> -+ <actionGroup name="CreateCustomerActionGroup"> -+ <click stepKey="openCustomers" selector="{{AdminMenuSection.customers}}"/> -+ <waitForAjaxLoad stepKey="waitForCatalogSubmenu" time="5"/> -+ <click stepKey="clickOnAllCustomers" selector="{{CustomersSubmenuSection.allCustomers}}"/> -+ <waitForPageLoad stepKey="waitForProductsPage" time="10"/> -+ <click stepKey="addNewCustomer" selector="{{CustomersPageSection.addNewCustomerButton}}"/> -+ <waitForPageLoad stepKey="waitForNewProductPage" time="10"/> -+ <click stepKey="AssociateToWebsite" selector="{{NewCustomerPageSection.associateToWebsite}}"/> -+ <click stepKey="Group" selector="{{NewCustomerPageSection.group}}"/> -+ <fillField stepKey="FillFirstName" selector="{{NewCustomerPageSection.firstName}}" userInput="{{NewCustomerData.FirstName}}"/> -+ <fillField stepKey="FillLastName" selector="{{NewCustomerPageSection.lastName}}" userInput="{{NewCustomerData.LastName}}"/> -+ <fillField stepKey="FillEmail" selector="{{NewCustomerPageSection.email}}" userInput="{{NewCustomerData.Email}}"/> -+ <scrollToTopOfPage stepKey="scrollToAddresses"/> -+ <click stepKey="goToAddresses" selector="{{NewCustomerPageSection.addresses}}"/> -+ <waitForAjaxLoad stepKey="waitForAddresses" time="5"/> -+ <click stepKey="AddNewAddress" selector="{{NewCustomerPageSection.addNewAddress}}"/> -+ <waitForPageLoad stepKey="waitForAddressFields" time="5"/> -+ <click stepKey="thickBillingAddress" selector="{{NewCustomerPageSection.defaultBillingAddress}}"/> -+ <click stepKey="thickShippingAddress" selector="{{NewCustomerPageSection.defaultShippingAddress}}"/> -+ <fillField stepKey="fillFirstNameForAddress" selector="{{NewCustomerPageSection.firstNameForAddress}}" userInput="{{NewCustomerData.AddressFirstName}}"/> -+ <fillField stepKey="fillLastNameForAddress" selector="{{NewCustomerPageSection.lastNameForAddress}}" userInput="{{NewCustomerData.AddressLastName}}"/> -+ <fillField stepKey="fillStreetAddress" selector="{{NewCustomerPageSection.streetAddress}}" userInput="{{NewCustomerData.StreetAddress}}"/> -+ <fillField stepKey="fillCity" selector="{{NewCustomerPageSection.city}}" userInput="{{NewCustomerData.City}}"/> -+ <click stepKey="openCountry" selector="{{NewCustomerPageSection.country}}"/> -+ <waitForAjaxLoad stepKey="waitForCountryList" time="5"/> -+ <click stepKey="chooseCountry" selector="{{NewCustomerPageSection.countryArmenia}}"/> -+ <fillField stepKey="fillZip" selector="{{NewCustomerPageSection.zip}}" userInput="{{NewCustomerData.Zip}}"/> -+ <fillField stepKey="fillPhoneNumber" selector="{{NewCustomerPageSection.phoneNumber}}" userInput="{{NewCustomerData.PhoneNumber}}"/> -+ <waitForPageLoad stepKey="wait" time="10"/> -+ <click stepKey="save" selector="{{NewCustomerPageSection.saveCustomer}}"/> -+ <waitForPageLoad stepKey="waitForCustomersPage" time="10"/> -+ <waitForElementVisible selector="{{NewCustomerPageSection.createdSuccessMessage}}" stepKey="waitForSuccessfullyCreatedMessage" time="20"/> -+ </actionGroup> -+</actionGroups> -diff --git a/app/code/Magento/Customer/Test/Mftf/ActionGroup/DeleteCustomerActionGroup.xml b/app/code/Magento/Customer/Test/Mftf/ActionGroup/DeleteCustomerActionGroup.xml -new file mode 100644 -index 00000000000..06659dae156 ---- /dev/null -+++ b/app/code/Magento/Customer/Test/Mftf/ActionGroup/DeleteCustomerActionGroup.xml -@@ -0,0 +1,46 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> -+ <actionGroup name="DeleteCustomerActionGroup"> -+ <arguments> -+ <argument name="lastName" defaultValue=""/> -+ </arguments> -+ <!--Clear filter if exist--> -+ <amOnPage url="{{AdminCustomerPage.url}}" stepKey="navigateToCustomers"/> -+ <conditionalClick selector="{{AdminDataGridHeaderSection.clearFilters}}" dependentSelector="{{AdminDataGridHeaderSection.clearFilters}}" visible="true" stepKey="clearExistingCustomerFilters"/> -+ -+ <click stepKey="chooseCustomer" selector="{{CustomersPageSection.customerCheckbox(lastName)}}"/> -+ <waitForAjaxLoad stepKey="waitForThick" time="2"/> -+ <click stepKey="OpenActions" selector="{{CustomersPageSection.actions}}"/> -+ <waitForAjaxLoad stepKey="waitForDelete" time="5"/> -+ <click stepKey="ChooseDelete" selector="{{CustomersPageSection.delete}}"/> -+ <waitForPageLoad stepKey="waitForDeleteItemPopup" time="10"/> -+ <click stepKey="clickOnOk" selector="{{CustomersPageSection.ok}}"/> -+ <waitForElementVisible stepKey="waitForSuccessfullyDeletedMessage" selector="{{CustomersPageSection.deletedSuccessMessage}}" time="10"/> -+ </actionGroup> -+ <actionGroup name="DeleteCustomerByEmailActionGroup"> -+ <arguments> -+ <argument name="email" type="string"/> -+ </arguments> -+ <amOnPage url="{{AdminCustomerPage.url}}" stepKey="navigateToCustomers"/> -+ <waitForPageLoad stepKey="waitForAdminCustomerPageLoad"/> -+ <click selector="{{AdminCustomerFiltersSection.filtersButton}}" stepKey="clickFilterButton"/> -+ <conditionalClick selector="{{AdminDataGridHeaderSection.clearFilters}}" dependentSelector="{{AdminDataGridHeaderSection.clearFilters}}" visible="true" stepKey="cleanFiltersIfTheySet"/> -+ <waitForPageLoad stepKey="waitForClearFilters"/> -+ <fillField selector="{{AdminCustomerFiltersSection.emailInput}}" userInput="{{email}}" stepKey="filterEmail"/> -+ <click selector="{{AdminCustomerFiltersSection.apply}}" stepKey="applyFilter"/> -+ <waitForPageLoad stepKey="waitForPageToLoad"/> -+ <click selector="{{AdminCustomerGridSection.selectFirstRow}}" stepKey="clickOnEditButton1"/> -+ <click selector="{{CustomersPageSection.actions}}" stepKey="clickActionsDropdown"/> -+ <click selector="{{CustomersPageSection.delete}}" stepKey="clickDelete"/> -+ <waitForElementVisible selector="{{CustomersPageSection.ok}}" stepKey="waitForOkToVisible"/> -+ <click selector="{{CustomersPageSection.ok}}" stepKey="clickOkConfirmationButton"/> -+ <waitForElementVisible stepKey="waitForSuccessfullyDeletedMessage" selector="{{CustomersPageSection.deletedSuccessMessage}}" time="30"/> -+ </actionGroup> -+</actionGroups> -diff --git a/app/code/Magento/Customer/Test/Mftf/ActionGroup/EditCustomerAddressesFromAdminActionGroup.xml b/app/code/Magento/Customer/Test/Mftf/ActionGroup/EditCustomerAddressesFromAdminActionGroup.xml -new file mode 100644 -index 00000000000..617c895bc12 ---- /dev/null -+++ b/app/code/Magento/Customer/Test/Mftf/ActionGroup/EditCustomerAddressesFromAdminActionGroup.xml -@@ -0,0 +1,31 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> -+ <actionGroup name="EditCustomerAddressesFromAdminActionGroup" > -+ <arguments> -+ <argument name="customerAddress"/> -+ </arguments> -+ <click selector="{{AdminEditCustomerAddressesSection.addresses}}" stepKey="proceedToAddresses"/> -+ <click selector="{{AdminEditCustomerAddressesSection.addNewAddress}}" stepKey="addNewAddresses"/> -+ <waitForPageLoad time="60" stepKey="wait5678" /> -+ <fillField stepKey="fillPrefixName" userInput="{{customerAddress.prefix}}" selector="{{AdminEditCustomerAddressesSection.prefixName}}"/> -+ <fillField stepKey="fillMiddleName" userInput="{{customerAddress.middlename}}" selector="{{AdminEditCustomerAddressesSection.middleName}}"/> -+ <fillField stepKey="fillSuffixName" userInput="{{customerAddress.suffix}}" selector="{{AdminEditCustomerAddressesSection.suffixName}}"/> -+ <fillField stepKey="fillCompany" userInput="{{customerAddress.company}}" selector="{{AdminEditCustomerAddressesSection.company}}"/> -+ <fillField stepKey="fillStreetAddress" userInput="{{customerAddress.street}}" selector="{{AdminEditCustomerAddressesSection.streetAddress}}"/> -+ <fillField stepKey="fillCity" userInput="{{customerAddress.city}}" selector="{{AdminEditCustomerAddressesSection.city}}"/> -+ <selectOption stepKey="selectCountry" selector="{{AdminEditCustomerAddressesSection.country}}" userInput="{{US_Address_CA.country_id}}"/> -+ <selectOption stepKey="selectState" selector="{{AdminEditCustomerAddressesSection.state}}" userInput="{{US_Address_CA.state}}"/> -+ <fillField stepKey="fillZipCode" userInput="{{customerAddress.postcode}}" selector="{{AdminEditCustomerAddressesSection.zipCode}}"/> -+ <fillField stepKey="fillPhone" userInput="{{customerAddress.telephone}}" selector="{{AdminEditCustomerAddressesSection.phone}}"/> -+ <fillField stepKey="fillVAT" userInput="{{customerAddress.vat_id}}" selector="{{AdminEditCustomerAddressesSection.vat}}"/> -+ <click selector="{{AdminEditCustomerAddressesSection.save}}" stepKey="saveAddress"/> -+ <waitForPageLoad stepKey="waitForAddressSaved"/> -+ </actionGroup> -+</actionGroups> -diff --git a/app/code/Magento/Customer/Test/Mftf/ActionGroup/FillCustomerSignInPopupFormActionGroup.xml b/app/code/Magento/Customer/Test/Mftf/ActionGroup/FillCustomerSignInPopupFormActionGroup.xml -new file mode 100644 -index 00000000000..56e9cfa2c0a ---- /dev/null -+++ b/app/code/Magento/Customer/Test/Mftf/ActionGroup/FillCustomerSignInPopupFormActionGroup.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="FillCustomerSignInPopupFormActionGroup" > -+ <arguments> -+ <argument name="customer" type="entity"/> -+ </arguments> -+ <waitForElementVisible selector="{{StorefrontCustomerSignInPopupFormSection.email}}" stepKey="waitEmailFieldVisible"/> -+ <fillField selector="{{StorefrontCustomerSignInPopupFormSection.email}}" userInput="{{customer.email}}" stepKey="fillCustomerEmail"/> -+ <fillField selector="{{StorefrontCustomerSignInPopupFormSection.password}}" userInput="{{customer.password}}" stepKey="fillCustomerPassword"/> -+ <click selector="{{StorefrontCustomerSignInPopupFormSection.signIn}}" stepKey="clickSignIn"/> -+ </actionGroup> -+</actionGroups> -diff --git a/app/code/Magento/Customer/Test/Mftf/ActionGroup/FillNewCustomerAddressRequiredFieldsActionGroup.xml b/app/code/Magento/Customer/Test/Mftf/ActionGroup/FillNewCustomerAddressRequiredFieldsActionGroup.xml -new file mode 100644 -index 00000000000..c533d1a6e55 ---- /dev/null -+++ b/app/code/Magento/Customer/Test/Mftf/ActionGroup/FillNewCustomerAddressRequiredFieldsActionGroup.xml -@@ -0,0 +1,24 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> -+ <actionGroup name="FillNewCustomerAddressRequiredFieldsActionGroup"> -+ <arguments> -+ <argument name="address" type="entity"/> -+ </arguments> -+ <fillField selector="{{StorefrontCustomerAddressFormSection.firstName}}" userInput="{{address.firstname}}" stepKey="fillFirstName"/> -+ <fillField selector="{{StorefrontCustomerAddressFormSection.lastName}}" userInput="{{address.lastname}}" stepKey="fillLastName"/> -+ <fillField selector="{{StorefrontCustomerAddressFormSection.phoneNumber}}" userInput="{{address.telephone}}" stepKey="fillPhoneNumber"/> -+ <fillField selector="{{StorefrontCustomerAddressFormSection.streetAddress}}" userInput="{{address.street[0]}}" stepKey="fillStreetAddress"/> -+ <fillField selector="{{StorefrontCustomerAddressFormSection.city}}" userInput="{{address.city}}" stepKey="fillCity"/> -+ <selectOption selector="{{StorefrontCustomerAddressFormSection.state}}" userInput="{{address.state}}" stepKey="selectState"/> -+ <fillField selector="{{StorefrontCustomerAddressFormSection.zip}}" userInput="{{address.postcode}}" stepKey="fillZip"/> -+ <selectOption selector="{{StorefrontCustomerAddressFormSection.country}}" userInput="{{address.country}}" stepKey="selectCountry"/> -+ </actionGroup> -+</actionGroups> -diff --git a/app/code/Magento/Customer/Test/Mftf/ActionGroup/LoginToStorefrontActionGroup.xml b/app/code/Magento/Customer/Test/Mftf/ActionGroup/LoginToStorefrontActionGroup.xml -index 1a4e9071d30..703b9f542f8 100644 ---- a/app/code/Magento/Customer/Test/Mftf/ActionGroup/LoginToStorefrontActionGroup.xml -+++ b/app/code/Magento/Customer/Test/Mftf/ActionGroup/LoginToStorefrontActionGroup.xml -@@ -6,14 +6,17 @@ - */ - --> - <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="LoginToStorefrontActionGroup"> - <arguments> - <argument name="Customer"/> - </arguments> -- <amOnPage stepKey="amOnSignInPage" url="{{StorefrontCustomerSignInPage.url}}"/> -- <fillField stepKey="fillEmail" userInput="{{Customer.email}}" selector="{{StorefrontCustomerSignInFormSection.emailField}}"/> -- <fillField stepKey="fillPassword" userInput="{{Customer.password}}" selector="{{StorefrontCustomerSignInFormSection.passwordField}}"/> -- <click stepKey="clickSignInAccountButton" selector="{{StorefrontCustomerSignInFormSection.signInAccountButton}}"/> -+ <amOnPage url="{{StorefrontCustomerSignInPage.url}}" stepKey="amOnSignInPage"/> -+ <waitForPageLoad time="30" stepKey="waitPageFullyLoaded"/> -+ <waitForElementVisible selector="{{StorefrontCustomerSignInFormSection.emailField}}" stepKey="waitFormAppears"/> -+ <fillField userInput="{{Customer.email}}" selector="{{StorefrontCustomerSignInFormSection.emailField}}" stepKey="fillEmail"/> -+ <fillField userInput="{{Customer.password}}" selector="{{StorefrontCustomerSignInFormSection.passwordField}}" stepKey="fillPassword"/> -+ <click selector="{{StorefrontCustomerSignInFormSection.signInAccountButton}}" stepKey="clickSignInAccountButton"/> -+ <waitForPageLoad stepKey="waitForCustomerLoggedIn" /> - </actionGroup> - </actionGroups> -diff --git a/app/code/Magento/Customer/Test/Mftf/ActionGroup/LoginToStorefrontWithEmailAndPasswordActionGroup.xml b/app/code/Magento/Customer/Test/Mftf/ActionGroup/LoginToStorefrontWithEmailAndPasswordActionGroup.xml -new file mode 100644 -index 00000000000..07145000105 ---- /dev/null -+++ b/app/code/Magento/Customer/Test/Mftf/ActionGroup/LoginToStorefrontWithEmailAndPasswordActionGroup.xml -@@ -0,0 +1,21 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> -+ <actionGroup name="LoginToStorefrontWithEmailAndPassword"> -+ <arguments> -+ <argument name="email" type="string"/> -+ <argument name="password" type="string"/> -+ </arguments> -+ <amOnPage stepKey="amOnSignInPage" url="{{StorefrontCustomerSignInPage.url}}"/> -+ <waitForPageLoad stepKey="wait"/> -+ <fillField stepKey="fillEmail" userInput="{{email}}" selector="{{StorefrontCustomerSignInFormSection.emailField}}"/> -+ <fillField stepKey="fillPassword" userInput="{{password}}" selector="{{StorefrontCustomerSignInFormSection.passwordField}}"/> -+ <click stepKey="clickSignInAccountButton" selector="{{StorefrontCustomerSignInFormSection.signInAccountButton}}"/> -+ </actionGroup> -+</actionGroups> -diff --git a/app/code/Magento/Customer/Test/Mftf/ActionGroup/NavigateCustomerActionGroup.xml b/app/code/Magento/Customer/Test/Mftf/ActionGroup/NavigateCustomerActionGroup.xml -new file mode 100644 -index 00000000000..be639d245f0 ---- /dev/null -+++ b/app/code/Magento/Customer/Test/Mftf/ActionGroup/NavigateCustomerActionGroup.xml -@@ -0,0 +1,15 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> -+ <actionGroup name="NavigateToAllCustomerPage"> -+ <amOnPage url="{{AdminCustomerPage.url}}" stepKey="openCustomersGridPage"/> -+ <waitForPageLoad stepKey="waitForPageLoad"/> -+ </actionGroup> -+</actionGroups> -diff --git a/app/code/Magento/Customer/Test/Mftf/ActionGroup/NavigateCustomerGroupActionGroup.xml b/app/code/Magento/Customer/Test/Mftf/ActionGroup/NavigateCustomerGroupActionGroup.xml -new file mode 100644 -index 00000000000..076797f3491 ---- /dev/null -+++ b/app/code/Magento/Customer/Test/Mftf/ActionGroup/NavigateCustomerGroupActionGroup.xml -@@ -0,0 +1,15 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> -+ <actionGroup name="NavigateToCustomerGroupPage"> -+ <amOnPage url="{{AdminCustomerGroupPage.url}}" stepKey="openCustomersGridPage"/> -+ <waitForPageLoad stepKey="waitForPageLoad"/> -+ </actionGroup> -+</actionGroups> -diff --git a/app/code/Magento/Customer/Test/Mftf/ActionGroup/NavigateThroughCustomerTabsActionGroup.xml b/app/code/Magento/Customer/Test/Mftf/ActionGroup/NavigateThroughCustomerTabsActionGroup.xml -new file mode 100644 -index 00000000000..5591bee5296 ---- /dev/null -+++ b/app/code/Magento/Customer/Test/Mftf/ActionGroup/NavigateThroughCustomerTabsActionGroup.xml -@@ -0,0 +1,17 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> -+ <actionGroup name="NavigateThroughCustomerTabsActionGroup"> -+ <arguments> -+ <argument name="navigationItemName" type="string" /> -+ </arguments> -+ <click selector="{{StorefrontCustomerSidebarSection.sidebarTab(navigationItemName)}}" stepKey="clickOnDesiredNavItem" /> -+ <waitForPageLoad stepKey="waitForPageLoad"/> -+ </actionGroup> -+</actionGroups> -diff --git a/app/code/Magento/Customer/Test/Mftf/ActionGroup/OpenEditCustomerFromAdminActionGroup.xml b/app/code/Magento/Customer/Test/Mftf/ActionGroup/OpenEditCustomerFromAdminActionGroup.xml -old mode 100644 -new mode 100755 -index 3d6e0fb54b0..208f4f51e38 ---- a/app/code/Magento/Customer/Test/Mftf/ActionGroup/OpenEditCustomerFromAdminActionGroup.xml -+++ b/app/code/Magento/Customer/Test/Mftf/ActionGroup/OpenEditCustomerFromAdminActionGroup.xml -@@ -6,18 +6,57 @@ - */ - --> - <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="OpenEditCustomerFromAdminActionGroup"> - <arguments> - <argument name="customer"/> - </arguments> - <amOnPage url="{{AdminCustomerPage.url}}" stepKey="navigateToCustomers"/> -- <waitForPageLoad stepKey="waitForPageLoad1" /> -+ <waitForPageLoad stepKey="waitForPageLoad1"/> -+ <conditionalClick selector="{{AdminDataGridHeaderSection.clearFilters}}" dependentSelector="{{AdminDataGridHeaderSection.clearFilters}}" visible="true" stepKey="clearExistingOrderFilters"/> - <click selector="{{AdminCustomerFiltersSection.filtersButton}}" stepKey="openFilter"/> - <fillField userInput="{{customer.email}}" selector="{{AdminCustomerFiltersSection.emailInput}}" stepKey="filterEmail"/> - <click selector="{{AdminCustomerFiltersSection.apply}}" stepKey="applyFilter"/> -- <waitForLoadingMaskToDisappear stepKey="waitForLoadingMaskToDisappear"/> -+ <waitForPageLoad stepKey="waitForPageLoad2"/> - <click selector="{{AdminCustomerGridSection.firstRowEditLink}}" stepKey="clickEdit"/> -- <waitForPageLoad stepKey="waitForPageLoad2" /> -+ <waitForPageLoad stepKey="waitForPageLoad3"/> -+ </actionGroup> -+ <actionGroup name="OpenEditCustomerAddressFromAdminActionGroup"> -+ <arguments> -+ <argument name="address"/> -+ </arguments> -+ <click selector="{{AdminCustomerAccountInformationSection.addressesButton}}" stepKey="openAddressesTab"/> -+ <waitForElementVisible selector="{{AdminCustomerAddressFiltersSection.filtersButton}}" stepKey="waitForComponentLoad"/> -+ <click selector="{{AdminCustomerAddressFiltersSection.filtersButton}}" stepKey="openAddressesFilter"/> -+ <fillField userInput="{{address.firstname}}" selector="{{AdminCustomerAddressFiltersSection.firstnameInput}}" stepKey="fillFirstname"/> -+ <fillField userInput="{{address.lastname}}" selector="{{AdminCustomerAddressFiltersSection.lastnameInput}}" stepKey="fillLastname"/> -+ <fillField userInput="{{address.telephone}}" selector="{{AdminCustomerAddressFiltersSection.telephoneInput}}" stepKey="fillCountry"/> -+ <fillField userInput="{{address.postcode}}" selector="{{AdminCustomerAddressFiltersSection.postcodeInput}}" stepKey="fillPostcode"/> -+ <click selector="{{AdminCustomerAddressFiltersSection.applyFilter}}" stepKey="applyAddressesFilter"/> -+ <waitForLoadingMaskToDisappear stepKey="waitForLoadingMaskToDisappear"/> -+ <click selector="{{AdminCustomerAddressGridSection.firstRowSelectActionLink}}" stepKey="clickAction"/> -+ <click selector="{{AdminCustomerAddressGridSection.firstRowEditActionLink}}" stepKey="clickEdit"/> -+ <waitForPageLoad stepKey="waitForModalWindow" /> -+ </actionGroup> -+ <actionGroup name="DeleteCustomerFromAdminActionGroup"> -+ <arguments> -+ <argument name="customer" defaultValue="CustomerEntityOne"/> -+ </arguments> -+ <amOnPage url="{{AdminCustomerPage.url}}" stepKey="navigateToCustomers"/> -+ <conditionalClick selector="{{AdminDataGridHeaderSection.clearFilters}}" dependentSelector="{{AdminDataGridHeaderSection.clearFilters}}" visible="true" stepKey="clickOnButtonToRemoveFiltersIfPresent"/> -+ <fillField selector="{{AdminDataGridHeaderSection.search}}" userInput="{{customer.email}}" stepKey="fillSearch"/> -+ <click selector="{{AdminDataGridHeaderSection.submitSearch}}" stepKey="clickSubmit"/> -+ <waitForAjaxLoad stepKey="waitForLoadAjax"/> -+ <click selector="{{AdminCustomerGridMainActionsSection.multicheck}}" stepKey="selectAll"/> -+ <click selector="{{AdminCustomerGridMainActionsSection.actions}}" stepKey="clickActions"/> -+ <click selector="{{AdminCustomerGridMainActionsSection.delete}}" stepKey="clickDelete"/> -+ <waitForAjaxLoad stepKey="waitForLoadConfirmation"/> -+ <click selector="{{AdminConfirmationModalSection.ok}}" stepKey="confirmDelete"/> -+ <see selector="{{AdminMessagesSection.successMessage}}" userInput="A total of 1 record(s) were deleted" stepKey="seeSuccess"/> -+ </actionGroup> -+ <actionGroup name="AdminClearCustomersFiltersActionGroup"> -+ <amOnPage url="{{AdminCustomerPage.url}}" stepKey="amOnCustomersPage"/> -+ <waitForPageLoad stepKey="WaitForPageToLoad"/> -+ <conditionalClick selector="{{AdminDataGridHeaderSection.clearFilters}}" dependentSelector="{{AdminDataGridHeaderSection.clearFilters}}" visible="true" stepKey="clickOnButtonToRemoveFiltersIfPresent"/> - </actionGroup> - </actionGroups> -diff --git a/app/code/Magento/Customer/Test/Mftf/ActionGroup/OpenMyAccountPageActionGroup.xml b/app/code/Magento/Customer/Test/Mftf/ActionGroup/OpenMyAccountPageActionGroup.xml -new file mode 100644 -index 00000000000..6ca0f612dee ---- /dev/null -+++ b/app/code/Magento/Customer/Test/Mftf/ActionGroup/OpenMyAccountPageActionGroup.xml -@@ -0,0 +1,15 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> -+ <actionGroup name="OpenMyAccountPageActionGroup"> -+ <click selector="{{LoggedInCustomerHeaderLinksSection.customerDropdownMenu}}" stepKey="openCustomerDropdownMenu"/> -+ <click selector="{{LoggedInCustomerHeaderLinksSection.myAccount}}" stepKey="clickOnMyAccount"/> -+ <waitForPageLoad stepKey="waitForPageLoad"/> -+ </actionGroup> -+</actionGroups> -diff --git a/app/code/Magento/Customer/Test/Mftf/ActionGroup/OpenStorefrontStoredPaymentMethodsPageActionGroup.xml b/app/code/Magento/Customer/Test/Mftf/ActionGroup/OpenStorefrontStoredPaymentMethodsPageActionGroup.xml -new file mode 100644 -index 00000000000..9975431e9fe ---- /dev/null -+++ b/app/code/Magento/Customer/Test/Mftf/ActionGroup/OpenStorefrontStoredPaymentMethodsPageActionGroup.xml -@@ -0,0 +1,14 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> -+ <actionGroup name="OpenStorefrontCustomerStoredPaymentMethodsPageActionGroup"> -+ <amOnPage url="{{StorefrontCustomerStoredPaymentMethodsPage.url}}" stepKey="goToCustomerStoredPaymentMethodsPage"/> -+ <waitForPageLoad stepKey="waitForCustomerStoredPaymentMethodsPageLoad"/> -+ </actionGroup> -+</actionGroups> -diff --git a/app/code/Magento/Customer/Test/Mftf/ActionGroup/SetGroupCustomerActionGroup.xml b/app/code/Magento/Customer/Test/Mftf/ActionGroup/SetGroupCustomerActionGroup.xml -new file mode 100644 -index 00000000000..ca5e16c4ddb ---- /dev/null -+++ b/app/code/Magento/Customer/Test/Mftf/ActionGroup/SetGroupCustomerActionGroup.xml -@@ -0,0 +1,22 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> -+ <actionGroup name="SetCustomerGroupForSelectedCustomersViaGrid"> -+ <arguments> -+ <argument name="groupName" type="string"/> -+ </arguments> -+ <click selector="{{CustomersPageSection.actions}}" stepKey="clickActions"/> -+ <click selector="{{CustomersPageSection.actionItem('Assign a Customer Group')}}" stepKey="clickAssignAction"/> -+ <executeJS function="document.getElementsByClassName('action-menu _active')[0].scrollBy(0, 10000)" stepKey="scrollToGroup"/> -+ <click selector="{{CustomersPageSection.assignGroup(groupName)}}" stepKey="selectGroup"/> -+ <waitForPageLoad stepKey="waitAfterSelectingGroup"/> -+ <click selector="{{AdminConfirmationModalSection.ok}}" stepKey="acceptModal"/> -+ </actionGroup> -+</actionGroups> -diff --git a/app/code/Magento/Customer/Test/Mftf/ActionGroup/SignUpNewUserFromStorefrontActionGroup.xml b/app/code/Magento/Customer/Test/Mftf/ActionGroup/SignUpNewUserFromStorefrontActionGroup.xml -index de8418774f7..da018871339 100644 ---- a/app/code/Magento/Customer/Test/Mftf/ActionGroup/SignUpNewUserFromStorefrontActionGroup.xml -+++ b/app/code/Magento/Customer/Test/Mftf/ActionGroup/SignUpNewUserFromStorefrontActionGroup.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"> -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> - <actionGroup name="SignUpNewUserFromStorefrontActionGroup"> - <arguments> -- <argument name="Customer"/> -+ <argument name="Customer" defaultValue="CustomerEntityOne"/> - </arguments> -- <amOnPage stepKey="amOnStorefrontPage" url="/"/> -- <waitForPageLoad stepKey="waitForStorefrontPage"/> -+ <amOnPage url="{{StorefrontHomePage.url}}" stepKey="amOnStorefrontPage"/> - <click stepKey="clickOnCreateAccountLink" selector="{{StorefrontPanelHeaderSection.createAnAccountLink}}"/> - <fillField stepKey="fillFirstName" userInput="{{Customer.firstname}}" selector="{{StorefrontCustomerCreateFormSection.firstnameField}}"/> - <fillField stepKey="fillLastName" userInput="{{Customer.lastname}}" selector="{{StorefrontCustomerCreateFormSection.lastnameField}}"/> -@@ -25,12 +24,52 @@ - <see stepKey="seeLastName" userInput="{{Customer.lastname}}" selector="{{StorefrontCustomerDashboardAccountInformationSection.ContactInformation}}" /> - <see stepKey="seeEmail" userInput="{{Customer.email}}" selector="{{StorefrontCustomerDashboardAccountInformationSection.ContactInformation}}" /> - </actionGroup> -- -+ -+ <actionGroup name="StorefrontCreateCustomerSignedUpNewsletterActionGroup"> -+ <arguments> -+ <argument name="customer" defaultValue="CustomerEntityOne"/> -+ </arguments> -+ <amOnPage url="{{StorefrontHomePage.url}}" stepKey="amOnStorefrontPage"/> -+ <waitForPageLoad stepKey="waitForNavigateToCustomersPageLoad"/> -+ <click stepKey="clickOnCreateAccountLink" selector="{{StorefrontPanelHeaderSection.createAnAccountLink}}"/> -+ <fillField stepKey="fillFirstName" userInput="{{customer.firstname}}" selector="{{StorefrontCustomerCreateFormSection.firstnameField}}"/> -+ <fillField stepKey="fillLastName" userInput="{{customer.lastname}}" selector="{{StorefrontCustomerCreateFormSection.lastnameField}}"/> -+ <checkOption selector="{{StorefrontCustomerCreateFormSection.signUpForNewsletter}}" stepKey="checkSignUpForNewsletter"/> -+ <fillField stepKey="fillEmail" userInput="{{customer.email}}" selector="{{StorefrontCustomerCreateFormSection.emailField}}"/> -+ <fillField stepKey="fillPassword" userInput="{{customer.password}}" selector="{{StorefrontCustomerCreateFormSection.passwordField}}"/> -+ <fillField stepKey="fillConfirmPassword" userInput="{{customer.password}}" selector="{{StorefrontCustomerCreateFormSection.confirmPasswordField}}"/> -+ <click stepKey="clickCreateAccountButton" selector="{{StorefrontCustomerCreateFormSection.createAccountButton}}"/> -+ <waitForPageLoad stepKey="waitForCreateAccountButtonToLoad" /> -+ </actionGroup> -+ -+ <actionGroup name="StorefrontFillRegistrationFormActionGroup" extends="StorefrontCreateCustomerSignedUpNewsletterActionGroup"> -+ <remove keyForRemoval="checkSignUpForNewsletter"/> -+ <remove keyForRemoval="clickCreateAccountButton"/> -+ <remove keyForRemoval="waitForCreateAccountButtonToLoad"/> -+ </actionGroup> -+ -+ <actionGroup name="SaveRegistrationFormActionGroup"> -+ <click stepKey="clickCreateAccountButton" selector="{{StorefrontCustomerCreateFormSection.createAccountButton}}"/> -+ <waitForPageLoad stepKey="waitForCreateAccountButtonToLoad" /> -+ </actionGroup> -+ -+ <actionGroup name="AssertSignedUpNewsletterActionGroup"> -+ <arguments> -+ <argument name="customer" defaultValue="CustomerEntityOne"/> -+ <argument name="storeName" defaultValue="Main Website" type="string"/> -+ </arguments> -+ <see stepKey="successMessage" userInput="Thank you for registering with {{storeName}} Store." selector="{{AdminCustomerMessagesSection.successMessage}}"/> -+ <see stepKey="seeFirstName" userInput="{{customer.firstname}}" selector="{{StorefrontCustomerDashboardAccountInformationSection.ContactInformation}}" /> -+ <see stepKey="seeLastName" userInput="{{customer.lastname}}" selector="{{StorefrontCustomerDashboardAccountInformationSection.ContactInformation}}" /> -+ <see stepKey="seeEmail" userInput="{{customer.email}}" selector="{{StorefrontCustomerDashboardAccountInformationSection.ContactInformation}}" /> -+ <seeInCurrentUrl url="{{StorefrontCustomerDashboardPage.url}}" stepKey="seeAssertInCurrentUrl"/> -+ </actionGroup> -+ - <actionGroup name="EnterCustomerAddressInfo"> - <arguments> - <argument name="Address"/> - </arguments> -- -+ - <amOnPage url="customer/address/new/" stepKey="goToAddressPage"/> - <waitForPageLoad stepKey="waitForAddressPage"/> - <fillField stepKey="fillFirstName" selector="{{StorefrontCustomerAddressSection.firstName}}" userInput="{{Address.firstname}}"/> -@@ -40,10 +79,91 @@ - <fillField stepKey="fillStreetAddress1" selector="{{StorefrontCustomerAddressSection.streetAddress1}}" userInput="{{Address.street[0]}}"/> - <fillField stepKey="fillStreetAddress2" selector="{{StorefrontCustomerAddressSection.streetAddress2}}" userInput="{{Address.street[1]}}"/> - <fillField stepKey="fillCityName" selector="{{StorefrontCustomerAddressSection.city}}" userInput="{{Address.city}}"/> -+ <selectOption stepKey="selectCounty" selector="{{StorefrontCustomerAddressSection.country}}" userInput="{{Address.country_id}}"/> - <selectOption stepKey="selectState" selector="{{StorefrontCustomerAddressSection.stateProvince}}" userInput="{{Address.state}}"/> - <fillField stepKey="fillZip" selector="{{StorefrontCustomerAddressSection.zip}}" userInput="{{Address.postcode}}"/> -- <selectOption stepKey="selectCounty" selector="{{StorefrontCustomerAddressSection.country}}" userInput="{{Address.country_id}}"/> -- - <click stepKey="saveAddress" selector="{{StorefrontCustomerAddressSection.saveAddress}}"/> - </actionGroup> -+ <!-- Fills State Field instead of selecting it--> -+ <actionGroup name="EnterCustomerAddressInfoFillState" extends="EnterCustomerAddressInfo"> -+ <fillField stepKey="selectState" selector="{{StorefrontCustomerAddressSection.stateProvinceFill}}" userInput="{{Address.state}}"/> -+ </actionGroup> -+ -+ <actionGroup name="VerifyCustomerBillingAddress"> -+ <arguments> -+ <argument name="address"/> -+ </arguments> -+ <amOnPage url="customer/address/index/" stepKey="goToAddressPage"/> -+ <waitForPageLoad stepKey="waitForAddressPageLoad"/> -+ <!--Verify customer default billing address--> -+ <see selector="{{StorefrontCustomerAddressesSection.defaultBillingAddress}}" userInput="{{address.firstname}} {{address.lastname}}" stepKey="seeAssertCustomerDefaultBillingAddressFirstnameAndLastname"/> -+ <see selector="{{StorefrontCustomerAddressesSection.defaultBillingAddress}}" userInput="{{address.company}}" stepKey="seeAssertCustomerDefaultBillingAddressCompany"/> -+ <see selector="{{StorefrontCustomerAddressesSection.defaultBillingAddress}}" userInput="{{address.street[0]}}" stepKey="seeAssertCustomerDefaultBillingAddressStreet"/> -+ <see selector="{{StorefrontCustomerAddressesSection.defaultBillingAddress}}" userInput="{{address.street[1]}}" stepKey="seeAssertCustomerDefaultBillingAddressStreet1"/> -+ <see selector="{{StorefrontCustomerAddressesSection.defaultBillingAddress}}" userInput="{{address.city}}, {{address.postcode}}" stepKey="seeAssertCustomerDefaultBillingAddressCityAndPostcode"/> -+ <see selector="{{StorefrontCustomerAddressesSection.defaultBillingAddress}}" userInput="{{address.country}}" stepKey="seeAssertCustomerDefaultBillingAddressCountry"/> -+ <see selector="{{StorefrontCustomerAddressesSection.defaultBillingAddress}}" userInput="{{address.telephone}}" stepKey="seeAssertCustomerDefaultBillingAddressTelephone"/> -+ </actionGroup> -+ <actionGroup name="VerifyCustomerShippingAddress"> -+ <arguments> -+ <argument name="address"/> -+ </arguments> -+ <amOnPage url="customer/address/index/" stepKey="goToAddressPage"/> -+ <waitForPageLoad stepKey="waitForAddressPageLoad"/> -+ <!--Verify customer default shipping address--> -+ <see selector="{{StorefrontCustomerAddressesSection.defaultShippingAddress}}" userInput="{{address.firstname}} {{address.lastname}}" stepKey="seeAssertCustomerDefaultShippingAddressFirstnameAndLastname"/> -+ <see selector="{{StorefrontCustomerAddressesSection.defaultShippingAddress}}" userInput="{{address.company}}" stepKey="seeAssertCustomerDefaultShippingAddressCompany"/> -+ <see selector="{{StorefrontCustomerAddressesSection.defaultShippingAddress}}" userInput="{{address.street[0]}}" stepKey="seeAssertCustomerDefaultShippingAddressStreet"/> -+ <see selector="{{StorefrontCustomerAddressesSection.defaultShippingAddress}}" userInput="{{address.street[1]}}" stepKey="seeAssertCustomerDefaultShippingAddressStreet1"/> -+ <see selector="{{StorefrontCustomerAddressesSection.defaultShippingAddress}}" userInput="{{address.city}}, {{address.postcode}}" stepKey="seeAssertCustomerDefaultShippingAddressCityAndPostcode"/> -+ <see selector="{{StorefrontCustomerAddressesSection.defaultShippingAddress}}" userInput="{{address.country}}" stepKey="seeAssertCustomerDefaultShippingAddressCountry"/> -+ <see selector="{{StorefrontCustomerAddressesSection.defaultShippingAddress}}" userInput="{{address.telephone}}" stepKey="seeAssertCustomerDefaultShippingAddressTelephone"/> -+ </actionGroup> -+ <actionGroup name="VerifyCustomerBillingAddressWithState"> -+ <arguments> -+ <argument name="address"/> -+ </arguments> -+ <amOnPage url="customer/address/index/" stepKey="goToAddressPage"/> -+ <waitForPageLoad stepKey="waitForAddressPageLoad"/> -+ <!--Verify customer default billing address--> -+ <see selector="{{StorefrontCustomerAddressesSection.defaultBillingAddress}}" userInput="{{address.firstname}} {{address.lastname}}" stepKey="seeAssertCustomerDefaultBillingAddressFirstnameAndLastname"/> -+ <see selector="{{StorefrontCustomerAddressesSection.defaultBillingAddress}}" userInput="{{address.company}}" stepKey="seeAssertCustomerDefaultBillingAddressCompany"/> -+ <see selector="{{StorefrontCustomerAddressesSection.defaultBillingAddress}}" userInput="{{address.street[0]}}" stepKey="seeAssertCustomerDefaultBillingAddressStreet"/> -+ <see selector="{{StorefrontCustomerAddressesSection.defaultBillingAddress}}" userInput="{{address.street[1]}}" stepKey="seeAssertCustomerDefaultBillingAddressStreet1"/> -+ <see selector="{{StorefrontCustomerAddressesSection.defaultBillingAddress}}" userInput="{{address.city}}, {{address.state}}, {{address.postcode}}" stepKey="seeAssertCustomerDefaultBillingAddressCityAndPostcode"/> -+ <see selector="{{StorefrontCustomerAddressesSection.defaultBillingAddress}}" userInput="{{address.country}}" stepKey="seeAssertCustomerDefaultBillingAddressCountry"/> -+ <see selector="{{StorefrontCustomerAddressesSection.defaultBillingAddress}}" userInput="{{address.telephone}}" stepKey="seeAssertCustomerDefaultBillingAddressTelephone"/> -+ </actionGroup> -+ <actionGroup name="VerifyCustomerShippingAddressWithState"> -+ <arguments> -+ <argument name="address"/> -+ </arguments> -+ <amOnPage url="customer/address/index/" stepKey="goToAddressPage"/> -+ <waitForPageLoad stepKey="waitForAddressPageLoad"/> -+ <!--Verify customer default shipping address--> -+ <see selector="{{StorefrontCustomerAddressesSection.defaultShippingAddress}}" userInput="{{address.firstname}} {{address.lastname}}" stepKey="seeAssertCustomerDefaultShippingAddressFirstnameAndLastname"/> -+ <see selector="{{StorefrontCustomerAddressesSection.defaultShippingAddress}}" userInput="{{address.company}}" stepKey="seeAssertCustomerDefaultShippingAddressCompany"/> -+ <see selector="{{StorefrontCustomerAddressesSection.defaultShippingAddress}}" userInput="{{address.street[0]}}" stepKey="seeAssertCustomerDefaultShippingAddressStreet"/> -+ <see selector="{{StorefrontCustomerAddressesSection.defaultShippingAddress}}" userInput="{{address.street[1]}}" stepKey="seeAssertCustomerDefaultShippingAddressStreet1"/> -+ <see selector="{{StorefrontCustomerAddressesSection.defaultShippingAddress}}" userInput="{{address.city}}, {{address.state}}, {{address.postcode}}" stepKey="seeAssertCustomerDefaultShippingAddressCityAndPostcode"/> -+ <see selector="{{StorefrontCustomerAddressesSection.defaultShippingAddress}}" userInput="{{address.country}}" stepKey="seeAssertCustomerDefaultShippingAddressCountry"/> -+ <see selector="{{StorefrontCustomerAddressesSection.defaultShippingAddress}}" userInput="{{address.telephone}}" stepKey="seeAssertCustomerDefaultShippingAddressTelephone"/> -+ </actionGroup> -+ <actionGroup name="VerifyCustomerNameOnFrontend"> -+ <arguments> -+ <argument name="customer"/> -+ </arguments> -+ <!--Verify customer name on frontend--> -+ <amOnPage url="customer/account/edit/" stepKey="goToAddressPage"/> -+ <waitForPageLoad stepKey="waitForAddressPageLoad"/> -+ <click selector="{{StorefrontCustomerSidebarSection.sidebarCurrentTab('Account Information')}}" stepKey="clickAccountInformationFromSidebarCurrentTab"/> -+ <waitForPageLoad stepKey="waitForAccountInformationTabToOpen"/> -+ <seeInField selector="{{StorefrontCustomerAccountInformationSection.firstName}}" userInput="{{customer.firstname}}" stepKey="seeAssertCustomerFirstName"/> -+ <seeInField selector="{{StorefrontCustomerAccountInformationSection.lastName}}" userInput="{{customer.lastname}}" stepKey="seeAssertCustomerLastName"/> -+ </actionGroup> -+ -+ <actionGroup name="SignUpNewCustomerStorefrontActionGroup" extends="SignUpNewUserFromStorefrontActionGroup"> -+ <waitForPageLoad stepKey="waitForRegistered" after="clickCreateAccountButton"/> -+ <remove keyForRemoval="seeThankYouMessage"/> -+ </actionGroup> - </actionGroups> -diff --git a/app/code/Magento/Customer/Test/Mftf/ActionGroup/StorefrontAddCustomerAddressActionGroup.xml b/app/code/Magento/Customer/Test/Mftf/ActionGroup/StorefrontAddCustomerAddressActionGroup.xml -new file mode 100644 -index 00000000000..a45fcf31f7b ---- /dev/null -+++ b/app/code/Magento/Customer/Test/Mftf/ActionGroup/StorefrontAddCustomerAddressActionGroup.xml -@@ -0,0 +1,48 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> -+ <actionGroup name="StorefrontAddNewCustomerAddressActionGroup"> -+ <amOnPage url="customer/address/new/" stepKey="OpenCustomerAddNewAddress"/> -+ <arguments> -+ <argument name="Address"/> -+ </arguments> -+ <fillField stepKey="fillFirstName" userInput="{{Address.firstname}}" selector="{{StorefrontCustomerAddressFormSection.firstName}}"/> -+ <fillField stepKey="fillLastName" userInput="{{Address.lastname}}" selector="{{StorefrontCustomerAddressFormSection.lastName}}"/> -+ <fillField stepKey="fillCompanyName" userInput="{{Address.company}}" selector="{{StorefrontCustomerAddressFormSection.company}}"/> -+ <fillField stepKey="fillPhoneNumber" userInput="{{Address.telephone}}" selector="{{StorefrontCustomerAddressFormSection.phoneNumber}}"/> -+ <fillField stepKey="fillStreetAddress" userInput="{{Address.street[0]}}" selector="{{StorefrontCustomerAddressFormSection.streetAddress}}"/> -+ <fillField stepKey="fillCity" userInput="{{Address.city}}" selector="{{StorefrontCustomerAddressFormSection.city}}"/> -+ <selectOption stepKey="selectState" userInput="{{Address.state}}" selector="{{StorefrontCustomerAddressFormSection.state}}"/> -+ <fillField stepKey="fillZip" userInput="{{Address.postcode}}" selector="{{StorefrontCustomerAddressFormSection.zip}}"/> -+ <selectOption stepKey="selectCountry" userInput="{{Address.country}}" selector="{{StorefrontCustomerAddressFormSection.country}}"/> -+ <click stepKey="saveCustomerAddress" selector="{{StorefrontCustomerAddressFormSection.saveAddress}}"/> -+ <see userInput="You saved the address." stepKey="verifyAddressAdded"/> -+ </actionGroup> -+ <actionGroup name="StorefrontAddCustomerDefaultAddressActionGroup"> -+ <amOnPage url="customer/address/new/" stepKey="OpenCustomerAddNewAddress"/> -+ <arguments> -+ <argument name="Address"/> -+ </arguments> -+ <fillField stepKey="fillFirstName" userInput="{{Address.firstname}}" selector="{{StorefrontCustomerAddressFormSection.firstName}}"/> -+ <fillField stepKey="fillLastName" userInput="{{Address.lastname}}" selector="{{StorefrontCustomerAddressFormSection.lastName}}"/> -+ <fillField stepKey="fillCompanyName" userInput="{{Address.company}}" selector="{{StorefrontCustomerAddressFormSection.company}}"/> -+ <fillField stepKey="fillPhoneNumber" userInput="{{Address.telephone}}" selector="{{StorefrontCustomerAddressFormSection.phoneNumber}}"/> -+ <fillField stepKey="fillStreetAddress" userInput="{{Address.street[0]}}" selector="{{StorefrontCustomerAddressFormSection.streetAddress}}"/> -+ <fillField stepKey="fillCity" userInput="{{Address.city}}" selector="{{StorefrontCustomerAddressFormSection.city}}"/> -+ <selectOption stepKey="selectState" userInput="{{Address.state}}" selector="{{StorefrontCustomerAddressFormSection.state}}"/> -+ <fillField stepKey="fillZip" userInput="{{Address.postcode}}" selector="{{StorefrontCustomerAddressFormSection.zip}}"/> -+ <selectOption stepKey="selectCountry" userInput="{{Address.country}}" selector="{{StorefrontCustomerAddressFormSection.country}}"/> -+ <click stepKey="checkUseAsDefaultBillingAddressCheckBox" selector="{{StorefrontCustomerAddressFormSection.useAsDefaultBillingAddressCheckBox}}"/> -+ <click stepKey="checkUseAsDefaultShippingAddressCheckBox" selector="{{StorefrontCustomerAddressFormSection.useAsDefaultShippingAddressCheckBox}}"/> -+ <click stepKey="saveCustomerAddress" selector="{{StorefrontCustomerAddressFormSection.saveAddress}}"/> -+ <waitForPageLoad stepKey="waitForPageLoad"/> -+ <see userInput="You saved the address." stepKey="verifyAddressAdded"/> -+ </actionGroup> -+</actionGroups> -diff --git a/app/code/Magento/Customer/Test/Mftf/ActionGroup/StorefrontAssertRegistrationPageFieldsActionGroup.xml b/app/code/Magento/Customer/Test/Mftf/ActionGroup/StorefrontAssertRegistrationPageFieldsActionGroup.xml -new file mode 100644 -index 00000000000..d76277d2e5e ---- /dev/null -+++ b/app/code/Magento/Customer/Test/Mftf/ActionGroup/StorefrontAssertRegistrationPageFieldsActionGroup.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="StorefrontAssertRegistrationPageFields"> -+ <seeInCurrentUrl url="{{StorefrontCustomerCreatePage.url}}" stepKey="seeCreateNewCustomerAccountPage"/> -+ <seeElement selector="{{StorefrontCustomerCreateFormSection.firstnameField}}" stepKey="seeFirstNameField"/> -+ <seeElement selector="{{StorefrontCustomerCreateFormSection.lastnameField}}" stepKey="seeFLastNameField"/> -+ <seeElement selector="{{StorefrontCustomerCreateFormSection.emailField}}" stepKey="seeEmailField"/> -+ <seeElement selector="{{StorefrontCustomerCreateFormSection.passwordField}}" stepKey="seePasswordField"/> -+ <seeElement selector="{{StorefrontCustomerCreateFormSection.confirmPasswordField}}" stepKey="seeConfirmPasswordField"/> -+ </actionGroup> -+</actionGroups> -diff --git a/app/code/Magento/Customer/Test/Mftf/ActionGroup/StorefrontAssertSuccessLoginToStorefrontActionGroup.xml b/app/code/Magento/Customer/Test/Mftf/ActionGroup/StorefrontAssertSuccessLoginToStorefrontActionGroup.xml -new file mode 100644 -index 00000000000..475702ad692 ---- /dev/null -+++ b/app/code/Magento/Customer/Test/Mftf/ActionGroup/StorefrontAssertSuccessLoginToStorefrontActionGroup.xml -@@ -0,0 +1,16 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> -+ <actionGroup name="StorefrontAssertSuccessLoginToStorefront" extends="LoginToStorefrontActionGroup"> -+ <arguments> -+ <argument name="Customer" type="entity"/> -+ </arguments> -+ <see stepKey="assertWelcome" userInput="{{Customer.firstname}}" selector="{{StorefrontPanelHeaderSection.customerWelcome}}" after="clickSignInAccountButton"/> -+ </actionGroup> -+</actionGroups> -diff --git a/app/code/Magento/Customer/Test/Mftf/ActionGroup/StorefrontClickCreateAnAccountCustomerAccountCreationFormActionGroup.xml b/app/code/Magento/Customer/Test/Mftf/ActionGroup/StorefrontClickCreateAnAccountCustomerAccountCreationFormActionGroup.xml -new file mode 100644 -index 00000000000..dfb9d1b2c25 ---- /dev/null -+++ b/app/code/Magento/Customer/Test/Mftf/ActionGroup/StorefrontClickCreateAnAccountCustomerAccountCreationFormActionGroup.xml -@@ -0,0 +1,15 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> -+ <actionGroup name="StorefrontClickCreateAnAccountCustomerAccountCreationFormActionGroup"> -+ <click stepKey="clickCreateAccountButton" selector="{{StorefrontCustomerCreateFormSection.createAccountButton}}" /> -+ <waitForPageLoad stepKey="waitForCustomerSaved" /> -+ </actionGroup> -+</actionGroups> -diff --git a/app/code/Magento/Customer/Test/Mftf/ActionGroup/StorefrontClickSignInButtonActionGroup.xml b/app/code/Magento/Customer/Test/Mftf/ActionGroup/StorefrontClickSignInButtonActionGroup.xml -new file mode 100644 -index 00000000000..b12858fc103 ---- /dev/null -+++ b/app/code/Magento/Customer/Test/Mftf/ActionGroup/StorefrontClickSignInButtonActionGroup.xml -@@ -0,0 +1,15 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> -+ <actionGroup name="StorefrontClickSignInButtonActionGroup"> -+ <click stepKey="signIn" selector="{{StorefrontPanelHeaderSection.customerLoginLink}}" /> -+ <waitForPageLoad stepKey="waitForStorefrontSignInPageLoad"/> -+ </actionGroup> -+</actionGroups> -diff --git a/app/code/Magento/Customer/Test/Mftf/ActionGroup/StorefrontClickSignOnCustomerLoginFormActionGroup.xml b/app/code/Magento/Customer/Test/Mftf/ActionGroup/StorefrontClickSignOnCustomerLoginFormActionGroup.xml -new file mode 100644 -index 00000000000..9cd52b841fc ---- /dev/null -+++ b/app/code/Magento/Customer/Test/Mftf/ActionGroup/StorefrontClickSignOnCustomerLoginFormActionGroup.xml -@@ -0,0 +1,15 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> -+ <actionGroup name="StorefrontClickSignOnCustomerLoginFormActionGroup"> -+ <click stepKey="clickSignInButton" selector="{{StorefrontCustomerSignInFormSection.signInAccountButton}}" /> -+ <waitForPageLoad stepKey="waitForCustomerSignIn" /> -+ </actionGroup> -+</actionGroups> -diff --git a/app/code/Magento/Customer/Test/Mftf/ActionGroup/StorefrontCustomerActionGroup.xml b/app/code/Magento/Customer/Test/Mftf/ActionGroup/StorefrontCustomerActionGroup.xml -new file mode 100644 -index 00000000000..c3b92b1af7f ---- /dev/null -+++ b/app/code/Magento/Customer/Test/Mftf/ActionGroup/StorefrontCustomerActionGroup.xml -@@ -0,0 +1,18 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> -+ <actionGroup name="CustomerLogoutStorefrontByMenuItemsActionGroup"> -+ <conditionalClick selector="{{StorefrontPanelHeaderSection.customerWelcomeMenu}}" -+ dependentSelector="{{StorefrontPanelHeaderSection.customerLogoutLink}}" -+ visible="false" -+ stepKey="clickHeaderCustomerMenuButton" /> -+ <click selector="{{StorefrontPanelHeaderSection.customerLogoutLink}}" stepKey="clickSignOutButton" /> -+ </actionGroup> -+</actionGroups> -diff --git a/app/code/Magento/Customer/Test/Mftf/ActionGroup/StorefrontCustomerAddressBookContainsActionGroup.xml b/app/code/Magento/Customer/Test/Mftf/ActionGroup/StorefrontCustomerAddressBookContainsActionGroup.xml -new file mode 100644 -index 00000000000..8385dc17ecf ---- /dev/null -+++ b/app/code/Magento/Customer/Test/Mftf/ActionGroup/StorefrontCustomerAddressBookContainsActionGroup.xml -@@ -0,0 +1,18 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> -+ <!-- Go to Address Book --> -+ <actionGroup name="StorefrontCustomerAddressBookContains"> -+ <arguments> -+ <argument name="text" type="string"/> -+ </arguments> -+ <see userInput="{{text}}" selector="{{StorefrontCustomerAddressesSection.addressesList}}" stepKey="containsText"/> -+ </actionGroup> -+</actionGroups> -diff --git a/app/code/Magento/Customer/Test/Mftf/ActionGroup/StorefrontCustomerAddressBookNotContainsActionGroup.xml b/app/code/Magento/Customer/Test/Mftf/ActionGroup/StorefrontCustomerAddressBookNotContainsActionGroup.xml -new file mode 100644 -index 00000000000..afef2d9a04e ---- /dev/null -+++ b/app/code/Magento/Customer/Test/Mftf/ActionGroup/StorefrontCustomerAddressBookNotContainsActionGroup.xml -@@ -0,0 +1,18 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> -+ <!-- Go to Address Book --> -+ <actionGroup name="StorefrontCustomerAddressBookNotContains"> -+ <arguments> -+ <argument name="text" type="string"/> -+ </arguments> -+ <dontSee userInput="{{text}}" selector="{{StorefrontCustomerAddressesSection.addressesList}}" stepKey="doesNotContainsText"/> -+ </actionGroup> -+</actionGroups> -diff --git a/app/code/Magento/Customer/Test/Mftf/ActionGroup/StorefrontCustomerAddressBookNumberOfAddressesActionGroup.xml b/app/code/Magento/Customer/Test/Mftf/ActionGroup/StorefrontCustomerAddressBookNumberOfAddressesActionGroup.xml -new file mode 100644 -index 00000000000..febc482d62e ---- /dev/null -+++ b/app/code/Magento/Customer/Test/Mftf/ActionGroup/StorefrontCustomerAddressBookNumberOfAddressesActionGroup.xml -@@ -0,0 +1,18 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> -+ <!-- Go to Address Book --> -+ <actionGroup name="StorefrontCustomerAddressBookNumberOfAddresses"> -+ <arguments> -+ <argument name="number" type="string"/> -+ </arguments> -+ <see userInput="{{number}} Item" selector="{{StorefrontCustomerAddressesSection.numberOfAddresses}}" stepKey="checkNumberOfAddresses"/> -+ </actionGroup> -+</actionGroups> -diff --git a/app/code/Magento/Customer/Test/Mftf/ActionGroup/StorefrontCustomerChangeEmailActionGroup.xml b/app/code/Magento/Customer/Test/Mftf/ActionGroup/StorefrontCustomerChangeEmailActionGroup.xml -new file mode 100644 -index 00000000000..844d13aa1fe ---- /dev/null -+++ b/app/code/Magento/Customer/Test/Mftf/ActionGroup/StorefrontCustomerChangeEmailActionGroup.xml -@@ -0,0 +1,24 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> -+ <actionGroup name="StorefrontCustomerChangeEmailActionGroup"> -+ <arguments> -+ <argument name="email" type="string" /> -+ <argument name="password" type="string" /> -+ </arguments> -+ <conditionalClick selector="{{StorefrontCustomerSidebarSection.sidebarTab('Account Information')}}" dependentSelector="{{StorefrontCustomerSidebarSection.sidebarTab('Account Information')}}" visible="true" stepKey="openAccountInfoTab" /> -+ <waitForPageLoad stepKey="waitForAccountInfoTabOpened" /> -+ <checkOption selector="{{StorefrontCustomerAccountInformationSection.changeEmail}}" stepKey="clickChangeEmailCheckbox" /> -+ <fillField selector="{{StorefrontCustomerAccountInformationSection.email}}" userInput="{{email}}" stepKey="fillEmail" /> -+ <fillField selector="{{StorefrontCustomerAccountInformationSection.currentPassword}}" userInput="{{password}}" stepKey="fillCurrentPassword" /> -+ <click selector="{{StorefrontCustomerAccountInformationSection.saveButton}}" stepKey="saveChange" /> -+ <waitForPageLoad stepKey="waitForPageLoaded" /> -+ </actionGroup> -+</actionGroups> -diff --git a/app/code/Magento/Customer/Test/Mftf/ActionGroup/StorefrontCustomerGoToSidebarMenuActionGroup.xml b/app/code/Magento/Customer/Test/Mftf/ActionGroup/StorefrontCustomerGoToSidebarMenuActionGroup.xml -new file mode 100644 -index 00000000000..84d2f353b51 ---- /dev/null -+++ b/app/code/Magento/Customer/Test/Mftf/ActionGroup/StorefrontCustomerGoToSidebarMenuActionGroup.xml -@@ -0,0 +1,19 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> -+ <!-- Go to Address Book --> -+ <actionGroup name="StorefrontCustomerGoToSidebarMenu"> -+ <arguments> -+ <argument name="menu" type="string"/> -+ </arguments> -+ <click selector="{{StorefrontCustomerSidebarSection.sidebarTab(menu)}}" stepKey="goToAddressBook"/> -+ <waitForPageLoad stepKey="waitForPageLoad" /> -+ </actionGroup> -+</actionGroups> -diff --git a/app/code/Magento/Customer/Test/Mftf/ActionGroup/StorefrontCustomerLogoutActionGroup.xml b/app/code/Magento/Customer/Test/Mftf/ActionGroup/StorefrontCustomerLogoutActionGroup.xml -new file mode 100644 -index 00000000000..295346875e3 ---- /dev/null -+++ b/app/code/Magento/Customer/Test/Mftf/ActionGroup/StorefrontCustomerLogoutActionGroup.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="StorefrontCustomerLogoutActionGroup"> -+ <amOnPage url="{{StorefrontCustomerLogoutPage.url}}" stepKey="storefrontSignOut"/> -+ <waitForPageLoad stepKey="waitForSignOut"/> -+ </actionGroup> -+ -+ <actionGroup name="StorefrontSignOutActionGroup"> -+ <click selector="{{StoreFrontSignOutSection.customerAccount}}" stepKey="clickCustomerButton"/> -+ <click selector="{{StoreFrontSignOutSection.signOut}}" stepKey="clickToSignOut"/> -+ <waitForPageLoad stepKey="waitForPageLoad"/> -+ <see userInput="You are signed out" stepKey="signOut"/> -+ </actionGroup> -+</actionGroups> -diff --git a/app/code/Magento/Customer/Test/Mftf/ActionGroup/StorefrontCustomerResetPasswordActionGroup.xml b/app/code/Magento/Customer/Test/Mftf/ActionGroup/StorefrontCustomerResetPasswordActionGroup.xml -new file mode 100644 -index 00000000000..a28593f1b77 ---- /dev/null -+++ b/app/code/Magento/Customer/Test/Mftf/ActionGroup/StorefrontCustomerResetPasswordActionGroup.xml -@@ -0,0 +1,24 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> -+ <actionGroup name="StorefrontCustomerResetPasswordActionGroup"> -+ <arguments> -+ <argument name="email" type="string" /> -+ </arguments> -+ -+ <amOnPage stepKey="amOnSignInPage" url="{{StorefrontCustomerSignInPage.url}}"/> -+ <click stepKey="clickForgotPasswordLink" selector="{{StorefrontCustomerSignInFormSection.forgotPasswordLink}}"/> -+ <see stepKey="seePageTitle" userInput="Forgot Your Password" selector="{{StorefrontForgotPasswordSection.pageTitle}}"/> -+ <!-- Enter email and submit the forgot password form --> -+ <fillField stepKey="fillEmailField" userInput="{{email}}" selector="{{StorefrontForgotPasswordSection.email}}"/> -+ <click stepKey="clickResetPassword" selector="{{StorefrontForgotPasswordSection.resetMyPasswordButton}}"/> -+ <waitForPageLoad stepKey="waitForPageLoaded" /> -+ </actionGroup> -+</actionGroups> -diff --git a/app/code/Magento/Customer/Test/Mftf/ActionGroup/StorefrontFillCustomerAccountCreationFormActionGroup.xml b/app/code/Magento/Customer/Test/Mftf/ActionGroup/StorefrontFillCustomerAccountCreationFormActionGroup.xml -new file mode 100644 -index 00000000000..62d77d4548c ---- /dev/null -+++ b/app/code/Magento/Customer/Test/Mftf/ActionGroup/StorefrontFillCustomerAccountCreationFormActionGroup.xml -@@ -0,0 +1,22 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> -+ <actionGroup name="StorefrontFillCustomerAccountCreationFormActionGroup"> -+ <arguments> -+ <argument name="customer" type="entity" /> -+ </arguments> -+ -+ <fillField stepKey="fillFirstName" userInput="{{customer.firstname}}" selector="{{StorefrontCustomerCreateFormSection.firstnameField}}" /> -+ <fillField stepKey="fillLastName" userInput="{{customer.lastname}}" selector="{{StorefrontCustomerCreateFormSection.lastnameField}}" /> -+ <fillField stepKey="fillEmail" userInput="{{customer.email}}" selector="{{StorefrontCustomerCreateFormSection.emailField}}"/> -+ <fillField stepKey="fillPassword" userInput="{{customer.password}}" selector="{{StorefrontCustomerCreateFormSection.passwordField}}"/> -+ <fillField stepKey="fillConfirmPassword" userInput="{{customer.password}}" selector="{{StorefrontCustomerCreateFormSection.confirmPasswordField}}"/> -+ </actionGroup> -+</actionGroups> -diff --git a/app/code/Magento/Customer/Test/Mftf/ActionGroup/StorefrontFillCustomerLoginFormActionGroup.xml b/app/code/Magento/Customer/Test/Mftf/ActionGroup/StorefrontFillCustomerLoginFormActionGroup.xml -new file mode 100644 -index 00000000000..22883ada7c2 ---- /dev/null -+++ b/app/code/Magento/Customer/Test/Mftf/ActionGroup/StorefrontFillCustomerLoginFormActionGroup.xml -@@ -0,0 +1,19 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> -+ <actionGroup name="StorefrontFillCustomerLoginFormActionGroup"> -+ <arguments> -+ <argument name="customer" type="entity" /> -+ </arguments> -+ -+ <fillField userInput="{{customer.email}}" selector="{{StorefrontCustomerSignInFormSection.emailField}}" stepKey="fillEmail"/> -+ <fillField userInput="{{customer.password}}" selector="{{StorefrontCustomerSignInFormSection.passwordField}}" stepKey="fillPassword"/> -+ </actionGroup> -+</actionGroups> -diff --git a/app/code/Magento/Customer/Test/Mftf/ActionGroup/StorefrontFillCustomerLoginFormWithWrongPasswordActionGroup.xml b/app/code/Magento/Customer/Test/Mftf/ActionGroup/StorefrontFillCustomerLoginFormWithWrongPasswordActionGroup.xml -new file mode 100644 -index 00000000000..16d7fd197b5 ---- /dev/null -+++ b/app/code/Magento/Customer/Test/Mftf/ActionGroup/StorefrontFillCustomerLoginFormWithWrongPasswordActionGroup.xml -@@ -0,0 +1,15 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> -+ <actionGroup name="StorefrontFillCustomerLoginFormWithWrongPasswordActionGroup" extends="StorefrontFillCustomerLoginFormActionGroup"> -+ <remove keyForRemoval="fillPassword"/> -+ <fillField userInput="{{customer.password}}_INCORRECT" selector="{{StorefrontCustomerSignInFormSection.passwordField}}" stepKey="fillPassword"/> -+ </actionGroup> -+</actionGroups> -diff --git a/app/code/Magento/Customer/Test/Mftf/ActionGroup/StorefrontOpenCustomerAccountCreatePageActionGroup.xml b/app/code/Magento/Customer/Test/Mftf/ActionGroup/StorefrontOpenCustomerAccountCreatePageActionGroup.xml -new file mode 100644 -index 00000000000..0ae470d0497 ---- /dev/null -+++ b/app/code/Magento/Customer/Test/Mftf/ActionGroup/StorefrontOpenCustomerAccountCreatePageActionGroup.xml -@@ -0,0 +1,15 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> -+ <actionGroup name="StorefrontOpenCustomerAccountCreatePageActionGroup"> -+ <amOnPage url="{{StorefrontCustomerCreatePage.url}}" stepKey="goToCustomerAccountCreatePage"/> -+ <waitForPageLoad stepKey="waitForPageLoaded"/> -+ </actionGroup> -+</actionGroups> -diff --git a/app/code/Magento/Customer/Test/Mftf/ActionGroup/StorefrontOpenCustomerAccountInfoEditPageActionGroup.xml b/app/code/Magento/Customer/Test/Mftf/ActionGroup/StorefrontOpenCustomerAccountInfoEditPageActionGroup.xml -new file mode 100644 -index 00000000000..c1ea2da8a95 ---- /dev/null -+++ b/app/code/Magento/Customer/Test/Mftf/ActionGroup/StorefrontOpenCustomerAccountInfoEditPageActionGroup.xml -@@ -0,0 +1,15 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> -+ <actionGroup name="StorefrontOpenCustomerAccountInfoEditPageActionGroup"> -+ <amOnPage url="{{StorefrontCustomerEditPage.url}}" stepKey="goToCustomerEditPage"/> -+ <waitForPageLoad stepKey="waitForEditPage"/> -+ </actionGroup> -+</actionGroups> -diff --git a/app/code/Magento/Customer/Test/Mftf/ActionGroup/StorefrontOpenCustomerLoginPageActionGroup.xml b/app/code/Magento/Customer/Test/Mftf/ActionGroup/StorefrontOpenCustomerLoginPageActionGroup.xml -new file mode 100644 -index 00000000000..0a5c7226552 ---- /dev/null -+++ b/app/code/Magento/Customer/Test/Mftf/ActionGroup/StorefrontOpenCustomerLoginPageActionGroup.xml -@@ -0,0 +1,15 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> -+ <actionGroup name="StorefrontOpenCustomerLoginPageActionGroup"> -+ <amOnPage url="{{StorefrontCustomerSignInPage.url}}" stepKey="amOnSignInPage"/> -+ <waitForPageLoad stepKey="waitForPageLoaded"/> -+ </actionGroup> -+</actionGroups> -diff --git a/app/code/Magento/Customer/Test/Mftf/ActionGroup/StorefrontOpenMyAccountPageActionGroup.xml b/app/code/Magento/Customer/Test/Mftf/ActionGroup/StorefrontOpenMyAccountPageActionGroup.xml -new file mode 100644 -index 00000000000..57c350700d5 ---- /dev/null -+++ b/app/code/Magento/Customer/Test/Mftf/ActionGroup/StorefrontOpenMyAccountPageActionGroup.xml -@@ -0,0 +1,15 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> -+ <actionGroup name="StorefrontOpenMyAccountPageActionGroup"> -+ <amOnPage url="{{StorefrontCustomerDashboardPage.url}}" stepKey="goToMyAccountPage"/> -+ <waitForPageLoad stepKey="waitForPageLoad"/> -+ </actionGroup> -+</actionGroups> -diff --git a/app/code/Magento/Customer/Test/Mftf/ActionGroup/StorefrontRegisterCustomerFromOrderSuccessPageActionGroup.xml b/app/code/Magento/Customer/Test/Mftf/ActionGroup/StorefrontRegisterCustomerFromOrderSuccessPageActionGroup.xml -new file mode 100644 -index 00000000000..5e24592bf01 ---- /dev/null -+++ b/app/code/Magento/Customer/Test/Mftf/ActionGroup/StorefrontRegisterCustomerFromOrderSuccessPageActionGroup.xml -@@ -0,0 +1,21 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> -+ <actionGroup name="StorefrontRegisterCustomerFromOrderSuccessPage"> -+ <arguments> -+ <argument name="customer" /> -+ </arguments> -+ <click selector="{{CheckoutSuccessRegisterSection.createAccountButton}}" stepKey="clickCreateAccountButton"/> -+ <fillField selector="{{StorefrontCustomerCreateFormSection.passwordField}}" userInput="{{customer.password}}" stepKey="typePassword"/> -+ <fillField selector="{{StorefrontCustomerCreateFormSection.confirmPasswordField}}" userInput="{{customer.password}}" stepKey="typeConfirmationPassword"/> -+ <click selector="{{StorefrontCustomerCreateFormSection.createAccountButton}}" stepKey="clickOnCreateAccount"/> -+ <see selector="{{StorefrontMessagesSection.success}}" userInput="Thank you for registering" stepKey="verifyAccountCreated"/> -+ </actionGroup> -+</actionGroups> -diff --git a/app/code/Magento/Customer/Test/Mftf/ActionGroup/SwitchAccountActionGroup.xml b/app/code/Magento/Customer/Test/Mftf/ActionGroup/SwitchAccountActionGroup.xml -new file mode 100644 -index 00000000000..85e23940e14 ---- /dev/null -+++ b/app/code/Magento/Customer/Test/Mftf/ActionGroup/SwitchAccountActionGroup.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"> -+ <!--Sign out--> -+ <actionGroup name="SignOut"> -+ <click selector="{{SignOutSection.admin}}" stepKey="clickToAdminProfile"/> -+ <click selector="{{SignOutSection.logout}}" stepKey="clickToLogOut"/> -+ <waitForPageLoad stepKey="waitForPageLoad" /> -+ <see userInput="You have logged out." stepKey="seeSuccessMessage" /> -+ <waitForElementVisible selector="//*[@data-ui-id='messages-message-success']" stepKey="waitForSuccessMessageLoggedOut" time="5"/> -+ </actionGroup> -+</actionGroups> -diff --git a/app/code/Magento/Customer/Test/Mftf/ActionGroup/VerifyGroupCustomerActionGroup.xml b/app/code/Magento/Customer/Test/Mftf/ActionGroup/VerifyGroupCustomerActionGroup.xml -new file mode 100644 -index 00000000000..712d3a59a21 ---- /dev/null -+++ b/app/code/Magento/Customer/Test/Mftf/ActionGroup/VerifyGroupCustomerActionGroup.xml -@@ -0,0 +1,20 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> -+ <actionGroup name="VerifyCustomerGroupForCustomer"> -+ <arguments> -+ <argument name="customerEmail" type="string"/> -+ <argument name="groupName" type="string"/> -+ </arguments> -+ <click selector="{{AdminCustomerGridSection.customerEditLinkByEmail(customerEmail)}}" stepKey="openCustomerPage"/> -+ <waitForPageLoad stepKey="waitForCustomerPage"/> -+ <see userInput="{{groupName}}" selector="{{AdminEditCustomerInformationSection.group}}" stepKey="checkCustomerGroup"/> -+ </actionGroup> -+</actionGroups> -diff --git a/app/code/Magento/Customer/Test/Mftf/Data/AddressData.xml b/app/code/Magento/Customer/Test/Mftf/Data/AddressData.xml -old mode 100644 -new mode 100755 -index d84dcef5b92..88f86e456e5 ---- a/app/code/Magento/Customer/Test/Mftf/Data/AddressData.xml -+++ b/app/code/Magento/Customer/Test/Mftf/Data/AddressData.xml -@@ -7,13 +7,13 @@ - --> - - <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="CustomerAddressSimple" type="address"> - <data key="id">0</data> - <data key="customer_id">12</data> - <requiredEntity type="region">CustomerRegionOne</requiredEntity> - <data key="region_id">0</data> -- <data key="country_id">USA</data> -+ <data key="country_id">US</data> - <array key="street"> - <item>7700 W Parmer Ln</item> - <item>Bld D</item> -@@ -32,6 +32,7 @@ - <data key="vat_id">vatData</data> - <data key="default_shipping">true</data> - <data key="default_billing">true</data> -+ <data key="region_qty">66</data> - </entity> - <entity name="US_Address_TX" type="address"> - <data key="firstname">John</data> -@@ -43,12 +44,29 @@ - <data key="city">Austin</data> - <data key="state">Texas</data> - <data key="country_id">US</data> -+ <data key="country">United States</data> - <data key="postcode">78729</data> - <data key="telephone">512-345-6789</data> - <data key="default_billing">Yes</data> - <data key="default_shipping">Yes</data> - <requiredEntity type="region">RegionTX</requiredEntity> - </entity> -+ <entity name="US_Address_TX_Default_Billing" type="address"> -+ <data key="firstname">John</data> -+ <data key="lastname">Doe</data> -+ <data key="company">Magento</data> -+ <array key="street"> -+ <item>7700 West Parmer Lane</item> -+ </array> -+ <data key="city">Austin</data> -+ <data key="state">Texas</data> -+ <data key="country_id">US</data> -+ <data key="country">United States</data> -+ <data key="postcode">78729</data> -+ <data key="telephone">512-345-6789</data> -+ <data key="default_billing">Yes</data> -+ <requiredEntity type="region">RegionTX</requiredEntity> -+ </entity> - <entity name="US_Address_NY" type="address"> - <data key="firstname">John</data> - <data key="lastname">Doe</data> -@@ -65,6 +83,40 @@ - <data key="default_billing">Yes</data> - <data key="default_shipping">Yes</data> - <requiredEntity type="region">RegionNY</requiredEntity> -+ <data key="country">United States</data> -+ </entity> -+ <entity name="US_Address_NY_Default_Shipping" type="address"> -+ <data key="firstname">John</data> -+ <data key="lastname">Doe</data> -+ <data key="company">368</data> -+ <array key="street"> -+ <item>368 Broadway St.</item> -+ <item>113</item> -+ </array> -+ <data key="city">New York</data> -+ <data key="state">New York</data> -+ <data key="country_id">US</data> -+ <data key="postcode">10001</data> -+ <data key="telephone">512-345-6789</data> -+ <data key="default_shipping">Yes</data> -+ <requiredEntity type="region">RegionNY</requiredEntity> -+ <data key="country">United States</data> -+ </entity> -+ <entity name="US_Address_NY_Not_Default_Address" type="address"> -+ <data key="firstname">John</data> -+ <data key="lastname">Doe</data> -+ <data key="company">368</data> -+ <array key="street"> -+ <item>368 Broadway St.</item> -+ <item>Apt. 113</item> -+ </array> -+ <data key="city">New York</data> -+ <data key="state">New York</data> -+ <data key="country_id">US</data> -+ <data key="postcode">10001</data> -+ <data key="telephone">512-345-6789</data> -+ <requiredEntity type="region">RegionNY</requiredEntity> -+ <data key="country">United States</data> - </entity> - <entity name="US_Address_CA" type="address"> - <data key="firstname">John</data> -@@ -77,6 +129,7 @@ - <data key="city">Los Angeles</data> - <data key="state">California</data> - <data key="country_id">US</data> -+ <data key="country">United States</data> - <data key="postcode">90001</data> - <data key="telephone">512-345-6789</data> - <data key="default_billing">Yes</data> -@@ -106,4 +159,143 @@ - <data key="country_id">GB</data> - <data key="telephone">444-44-444-44</data> - </entity> -+ <entity name="US_Address_Utah" type="address"> -+ <data key="firstname">John</data> -+ <data key="lastname">Doe</data> -+ <data key="company">Magento</data> -+ <array key="street"> -+ <item>1234 Some Utah address</item> -+ </array> -+ <data key="city">Provo</data> -+ <data key="state">Utah</data> -+ <data key="country_id">US</data> -+ <data key="country">United States</data> -+ <data key="postcode">84001</data> -+ <data key="telephone">512-345-6789</data> -+ <data key="default_billing">Yes</data> -+ <data key="default_shipping">Yes</data> -+ <requiredEntity type="region">RegionUT</requiredEntity> -+ </entity> -+ <entity name="UK_Simple_Address" extends="UK_Not_Default_Address"> -+ <array key="street"> -+ <item>172, Westminster Bridge Rd</item> -+ <item>7700 xyz street</item> -+ </array> -+ <data key="state">California</data> -+ </entity> -+ <entity name="US_Default_Billing_Address_TX" type="address" extends="US_Address_TX"> -+ <data key="default_billing">false</data> -+ <data key="default_shipping">true</data> -+ </entity> -+ <entity name="US_Default_Shipping_Address_CA" type="address" extends="US_Address_CA"> -+ <data key="default_billing">true</data> -+ <data key="default_shipping">false</data> -+ </entity> -+ <entity name="addressNoZipNoState" type="address"> -+ <data key="country_id">United Kingdom</data> -+ <array key="street"> -+ <item>3962 Horner Street</item> -+ </array> -+ <data key="company">Magento</data> -+ <data key="telephone">334-200-4061</data> -+ <data key="city">London</data> -+ <data key="firstname">Fn</data> -+ <data key="lastname">Ln</data> -+ <data key="middlename">Mn</data> -+ <data key="prefix">Mr</data> -+ <data key="suffix">Sr</data> -+ <data key="vat_id">U1234567891</data> -+ <data key="default_shipping">true</data> -+ <data key="default_billing">true</data> -+ </entity> -+ <entity name="updateCustomerUKAddress" type="address"> -+ <data key="firstname">John</data> -+ <data key="lastname">Doe</data> -+ <data key="company">Magento</data> -+ <data key="telephone">0123456789-02134567</data> -+ <array key="street"> -+ <item>172, Westminster Bridge Rd</item> -+ <item>7700 xyz street</item> -+ </array> -+ <data key="country_id">GB</data> -+ <data key="country">United Kingdom</data> -+ <data key="city">London</data> -+ <!-- State not required for UK address on frontend--> -+ <data key="state"> </data> -+ <data key="postcode">12345</data> -+ </entity> -+ <entity name="updateCustomerFranceAddress" type="address"> -+ <data key="firstname">Jaen</data> -+ <data key="lastname">Reno</data> -+ <data key="company">Magento</data> -+ <data key="telephone">555-888-111-999</data> -+ <array key="street"> -+ <item>18-20 Rue Maréchal Lecler</item> -+ <item>18-20 Rue Maréchal Lecler</item> -+ </array> -+ <data key="country_id">FR</data> -+ <data key="country">France</data> -+ <data key="city">Quintin</data> -+ <data key="state">Côtes-d'Armor</data> -+ <data key="postcode">12345</data> -+ </entity> -+ <entity name="updateCustomerNoXSSInjection" type="address"> -+ <data key="firstname">Jany</data> -+ <data key="lastname">Doe</data> -+ <data key="company">Magento</data> -+ <data key="telephone">555-888-111-999</data> -+ <array key="street"> -+ <item>7700 West Parmer Lane</item> -+ <item>7700 West Parmer Lane</item> -+ </array> -+ <data key="country_id">US</data> -+ <data key="country">United States</data> -+ <data key="city">Denver</data> -+ <data key="state">Colorado</data> -+ <data key="postcode">12345</data> -+ </entity> -+ <entity name="PolandAddress" type="address"> -+ <data key="firstname">Mag</data> -+ <data key="lastname">Ento</data> -+ <data key="company">Magento</data> -+ <array key="street"> -+ <item>Piwowarska 6</item> -+ </array> -+ <data key="city">Bielsko-Biała</data> -+ <data key="state"> Bielsko</data> -+ <data key="country_id">PL</data> -+ <data key="country">Poland</data> -+ <data key="postcode">43-310</data> -+ <data key="telephone">799885616</data> -+ <data key="default_billing">Yes</data> -+ <data key="default_shipping">Yes</data> -+ <requiredEntity type="region">RegionUT</requiredEntity> -+ </entity> -+ <entity name="DE_Address_Berlin_Not_Default_Address" type="address"> -+ <data key="firstname">John</data> -+ <data key="lastname">Doe</data> -+ <data key="company">Magento</data> -+ <array key="street"> -+ <item>Augsburger Strabe 41</item> -+ </array> -+ <data key="city">Berlin</data> -+ <data key="country_id">DE</data> -+ <data key="postcode">10789</data> -+ <data key="telephone">333-33-333-33</data> -+ <data key="country">Germany</data> -+ </entity> -+ <entity name="US_Address_California"> -+ <data key="firstname">John</data> -+ <data key="lastname">Doe</data> -+ <data key="company">Magento</data> -+ <array key="street"> -+ <item>6161 West Centinela Avenue</item> -+ </array> -+ <data key="city">Culver City</data> -+ <data key="country_id">United States</data> -+ <data key="country">United States</data> -+ <data key="state">California</data> -+ <data key="postcode">90230</data> -+ <data key="telephone">555-55-555-55</data> -+ </entity> - </entities> -diff --git a/app/code/Magento/Customer/Test/Mftf/Data/AdminMenuData.xml b/app/code/Magento/Customer/Test/Mftf/Data/AdminMenuData.xml -new file mode 100644 -index 00000000000..4e433c76b3d ---- /dev/null -+++ b/app/code/Magento/Customer/Test/Mftf/Data/AdminMenuData.xml -@@ -0,0 +1,31 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<entities xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataProfileSchema.xsd"> -+ <entity name="AdminMenuCustomers"> -+ <data key="pageTitle">Customers</data> -+ <data key="title">Customers</data> -+ <data key="dataUiId">magento-customer-customer</data> -+ </entity> -+ <entity name="AdminMenuCustomersAllCustomers"> -+ <data key="pageTitle">Customers</data> -+ <data key="title">All Customers</data> -+ <data key="dataUiId">magento-customer-customer-manage</data> -+ </entity> -+ <entity name="AdminMenuCustomersCustomerGroups"> -+ <data key="pageTitle">Customer Groups</data> -+ <data key="title">Customer Groups</data> -+ <data key="dataUiId">magento-customer-customer-group</data> -+ </entity> -+ <entity name="AdminMenuCustomersNowOnline"> -+ <data key="pageTitle">Customers Now Online</data> -+ <data key="title">Now Online</data> -+ <data key="dataUiId">magento-customer-customer-online</data> -+ </entity> -+</entities> -diff --git a/app/code/Magento/Customer/Test/Mftf/Data/CustomerConfigData.xml b/app/code/Magento/Customer/Test/Mftf/Data/CustomerConfigData.xml -new file mode 100644 -index 00000000000..870bd929eea ---- /dev/null -+++ b/app/code/Magento/Customer/Test/Mftf/Data/CustomerConfigData.xml -@@ -0,0 +1,44 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<entities xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataProfileSchema.xsd"> -+ <entity name="CustomerAccountSharingDefault" type="customer_account_sharing_config"> -+ <requiredEntity type="account_share_scope_value">CustomerAccountSharingPerWebsite</requiredEntity> -+ </entity> -+ <entity name="CustomerAccountSharingPerWebsite" type="account_share_scope_value"> -+ <data key="value">1</data> -+ </entity> -+ -+ <entity name="CustomerAccountSharingGlobal" type="customer_account_sharing_config"> -+ <requiredEntity type="account_share_scope_value">GlobalCustomerAccountSharing</requiredEntity> -+ </entity> -+ <entity name="GlobalCustomerAccountSharing" type="account_share_scope_value"> -+ <data key="value">0</data> -+ </entity> -+ -+ <entity name="CustomerAccountSharingSystemValue" type="customer_account_sharing_config_inherit"> -+ <requiredEntity type="account_share_scope_inherit">CustomerAccountSharingInherit</requiredEntity> -+ </entity> -+ <entity name="CustomerAccountSharingInherit" type="account_share_scope_inherit"> -+ <data key="inherit">true</data> -+ </entity> -+ <entity name="StorefrontCustomerLockoutFailuresDefaultConfigData"> -+ <!-- Magento default value --> -+ <data key="path">customer/password/lockout_failures</data> -+ <data key="scope_id">0</data> -+ <data key="label">10</data> -+ <data key="value">10</data> -+ </entity> -+ <entity name="StorefrontCustomerLockoutFailures5ConfigData"> -+ <data key="path">customer/password/lockout_failures</data> -+ <data key="scope_id">0</data> -+ <data key="label">5</data> -+ <data key="value">5</data> -+ </entity> -+</entities> -diff --git a/app/code/Magento/Customer/Test/Mftf/Data/CustomerConfigurationData.xml b/app/code/Magento/Customer/Test/Mftf/Data/CustomerConfigurationData.xml -new file mode 100644 -index 00000000000..60d8b13887e ---- /dev/null -+++ b/app/code/Magento/Customer/Test/Mftf/Data/CustomerConfigurationData.xml -@@ -0,0 +1,40 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<entities xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataProfileSchema.xsd"> -+ <entity name="SetCustomerCreateNewAccountOptionsConfig" type="customer_create_new_account_config"> -+ <requiredEntity type="auto_group_assign">EnableAutomaticAssignmentCustomerGroup</requiredEntity> -+ <requiredEntity type="viv_on_each_transaction">EnableValidateEachTransaction</requiredEntity> -+ <requiredEntity type="vat_frontend_visibility">EnableShowVATNumberStorefront</requiredEntity> -+ </entity> -+ <entity name="EnableAutomaticAssignmentCustomerGroup" type="auto_group_assign"> -+ <data key="value">1</data> -+ </entity> -+ <entity name="EnableValidateEachTransaction" type="viv_on_each_transaction"> -+ <data key="value">1</data> -+ </entity> -+ <entity name="EnableShowVATNumberStorefront" type="vat_frontend_visibility"> -+ <data key="value">1</data> -+ </entity> -+ -+ <entity name="SetCustomerCreateNewAccountOptionsDefaultConfig" type="customer_create_new_account_config"> -+ <requiredEntity type="auto_group_assign">DefaultAutomaticAssignmentCustomerGroup</requiredEntity> -+ <requiredEntity type="viv_on_each_transaction">DefaultValidateEachTransaction</requiredEntity> -+ <requiredEntity type="vat_frontend_visibility">DefaultShowVATNumberStorefront</requiredEntity> -+ </entity> -+ <entity name="DefaultAutomaticAssignmentCustomerGroup" type="auto_group_assign"> -+ <data key="value">0</data> -+ </entity> -+ <entity name="DefaultValidateEachTransaction" type="viv_on_each_transaction"> -+ <data key="value">0</data> -+ </entity> -+ <entity name="DefaultShowVATNumberStorefront" type="vat_frontend_visibility"> -+ <data key="value">0</data> -+ </entity> -+</entities> -diff --git a/app/code/Magento/Customer/Test/Mftf/Data/CustomerData.xml b/app/code/Magento/Customer/Test/Mftf/Data/CustomerData.xml -index 1f6a01ea815..55bc9b56c79 100644 ---- a/app/code/Magento/Customer/Test/Mftf/Data/CustomerData.xml -+++ b/app/code/Magento/Customer/Test/Mftf/Data/CustomerData.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="CustomerEntityOne" type="customer"> - <data key="group_id">0</data> - <data key="default_billing">defaultBillingValue</data> -@@ -21,6 +21,7 @@ - <data key="firstname">John</data> - <data key="lastname">Doe</data> - <data key="middlename">S</data> -+ <data key="fullname">John Doe</data> - <data key="password">pwdTest123!</data> - <data key="prefix">Mr</data> - <data key="suffix">Sr</data> -@@ -33,7 +34,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> -@@ -43,8 +44,58 @@ - <data key="password">pwdTest123!</data> - <data key="store_id">0</data> - <data key="website_id">0</data> -+ <data key="group">General</data> - <requiredEntity type="address">US_Address_TX</requiredEntity> - </entity> -+ <entity name="SimpleUsCustomerWithNewCustomerGroup" type="customer"> -+ <data key="default_billing">true</data> -+ <data key="default_shipping">true</data> -+ <data key="email" unique="prefix">John.Doe@example.com</data> -+ <data key="firstname">John</data> -+ <data key="lastname">Doe</data> -+ <data key="fullname">John Doe</data> -+ <data key="password">pwdTest123!</data> -+ <data key="store_id">0</data> -+ <data key="website_id">0</data> -+ <requiredEntity type="address">US_Address_TX</requiredEntity> -+ <var entityType="customerGroup" entityKey="id" key="group_id" /> -+ </entity> -+ <entity name="UsCustomerAssignedToNewCustomerGroup" type="customer"> -+ <var key="group_id" entityKey="id" entityType="customerGroup" /> -+ <data key="default_billing">true</data> -+ <data key="default_shipping">true</data> -+ <data key="email" unique="prefix">John.Doe@example.com</data> -+ <data key="firstname">John</data> -+ <data key="lastname">Doe</data> -+ <data key="fullname">John Doe</data> -+ <data key="password">pwdTest123!</data> -+ <data key="store_id">0</data> -+ <data key="website_id">0</data> -+ <requiredEntity type="address">US_Address_TX</requiredEntity> -+ </entity> -+ <entity name="Simple_US_Customer_Incorrect_Name" type="customer"> -+ <data key="group_id">1</data> -+ <data key="default_billing">true</data> -+ <data key="default_shipping">true</data> -+ <data key="email" unique="prefix">John.Doe@example.com</data> -+ <data key="firstname">LoremIpsumLoremIpsumLoremIpsumLoremIpsumLoremIpsumLoremIpsumLoremIpsumLoremIpsumLoremIpsumLoremIpsumLoremIpsumLoremIpsumLoremIpsumLoremIpsumLoremIpsumLoremIpsumLoremIpsumLoremIpsumLoremIpsumLoremIpsumLoremIpsumLoremIpsumLoremIpsumLoremIpsumLoremIpsumLoremIpsum</data> -+ <data key="lastname">Doe</data> -+ <data key="fullname">John Doe</data> -+ <data key="password">pwdTest123!</data> -+ <data key="store_id">0</data> -+ <data key="website_id">0</data> -+ <requiredEntity type="address">US_Address_TX</requiredEntity> -+ </entity> -+ <entity name="Simple_Customer_Without_Address" type="customer"> -+ <data key="group_id">1</data> -+ <data key="email" unique="prefix">John.Doe@example.com</data> -+ <data key="firstname">John</data> -+ <data key="lastname">Doe</data> -+ <data key="fullname">John Doe</data> -+ <data key="password">pwdTest123!</data> -+ <data key="store_id">0</data> -+ <data key="website_id">0</data> -+ </entity> - <entity name="Simple_US_Customer_Multiple_Addresses" type="customer"> - <data key="group_id">0</data> - <data key="default_billing">true</data> -@@ -59,6 +110,34 @@ - <requiredEntity type="address">US_Address_NY</requiredEntity> - <requiredEntity type="address">UK_Not_Default_Address</requiredEntity> - </entity> -+ <entity name="Simple_US_Customer_Multiple_Addresses_No_Default_Address" type="customer"> -+ <data key="group_id">0</data> -+ <data key="default_billing">true</data> -+ <data key="default_shipping">true</data> -+ <data key="email" unique="prefix">John.Doe@example.com</data> -+ <data key="firstname">John</data> -+ <data key="lastname">Doe</data> -+ <data key="fullname">John Doe</data> -+ <data key="password">pwdTest123!</data> -+ <data key="store_id">0</data> -+ <data key="website_id">0</data> -+ <requiredEntity type="address">US_Address_NY_Not_Default_Address</requiredEntity> -+ <requiredEntity type="address">UK_Not_Default_Address</requiredEntity> -+ </entity> -+ <entity name="Simple_US_Customer_With_Different_Billing_Shipping_Addresses" type="customer"> -+ <data key="group_id">0</data> -+ <data key="default_billing">true</data> -+ <data key="default_shipping">true</data> -+ <data key="email" unique="prefix">John.Doe@example.com</data> -+ <data key="firstname">John</data> -+ <data key="lastname">Doe</data> -+ <data key="fullname">John Doe</data> -+ <data key="password">pwdTest123!</data> -+ <data key="store_id">0</data> -+ <data key="website_id">0</data> -+ <requiredEntity type="address">US_Address_TX_Default_Billing</requiredEntity> -+ <requiredEntity type="address">US_Address_NY_Default_Shipping</requiredEntity> -+ </entity> - <entity name="Simple_US_Customer_NY" type="customer"> - <data key="group_id">0</data> - <data key="default_billing">true</data> -@@ -89,4 +168,131 @@ - <var key="id" entityKey="id" entityType="customer"/> - <data key="firstname">Jane</data> - </entity> -+ <entity name="Simple_US_CA_Customer" type="customer"> -+ <data key="group_id">1</data> -+ <data key="default_billing">true</data> -+ <data key="default_shipping">true</data> -+ <data key="email" unique="prefix">John.Doe@example.com</data> -+ <data key="firstname">John</data> -+ <data key="lastname">Doe</data> -+ <data key="fullname">John Doe</data> -+ <data key="password">pwdTest123!</data> -+ <data key="store_id">0</data> -+ <data key="website_id">0</data> -+ <requiredEntity type="address">US_Address_CA</requiredEntity> -+ </entity> -+ <entity name="Simple_US_Utah_Customer" type="customer"> -+ <data key="group_id">1</data> -+ <data key="default_billing">true</data> -+ <data key="default_shipping">true</data> -+ <data key="email" unique="prefix">John.Doe@example.com</data> -+ <data key="firstname">John</data> -+ <data key="lastname">Doe</data> -+ <data key="fullname">John Doe</data> -+ <data key="password">pwdTest123!</data> -+ <data key="store_id">0</data> -+ <data key="website_id">0</data> -+ <requiredEntity type="address">US_Address_Utah</requiredEntity> -+ </entity> -+ <entity name="Simple_GB_Customer" type="customer"> -+ <data key="group_id">0</data> -+ <data key="default_billing">true</data> -+ <data key="default_shipping">true</data> -+ <data key="email" unique="prefix">Jane.Doe@example.com</data> -+ <data key="firstname">Jane</data> -+ <data key="lastname">Doe</data> -+ <data key="fullname">Jane Doe</data> -+ <data key="password">pwdTest123!</data> -+ <data key="store_id">0</data> -+ <data key="website_id">0</data> -+ <requiredEntity type="address">UK_Not_Default_Address</requiredEntity> -+ </entity> -+ <entity name="Customer_With_Different_Default_Billing_Shipping_Addresses" type="customer"> -+ <data key="group_id">1</data> -+ <data key="email" unique="prefix">John.Doe@example.com</data> -+ <data key="firstname">John</data> -+ <data key="lastname">Doe</data> -+ <data key="fullname">John Doe</data> -+ <data key="password">pwdTest123!</data> -+ <data key="store_id">0</data> -+ <data key="website_id">0</data> -+ <requiredEntity type="address">US_Default_Billing_Address_TX</requiredEntity> -+ <requiredEntity type="address">US_Default_Shipping_Address_CA</requiredEntity> -+ </entity> -+ <entity name="Colorado_US_Customer" type="customer"> -+ <data key="group_id">1</data> -+ <data key="default_billing">true</data> -+ <data key="default_shipping">true</data> -+ <data key="email" unique="prefix">Patric.Patric@example.com</data> -+ <data key="firstname">Patrick</title></head><svg/onload=alert('XSS')></data> -+ <data key="lastname"><script>alert('Last name')</script></data> -+ <data key="password">123123^q</data> -+ <data key="store_id">0</data> -+ <data key="website_id">0</data> -+ </entity> -+ <entity name="Customer_US_UK_DE" type="customer"> -+ <data key="group_id">0</data> -+ <data key="default_billing">true</data> -+ <data key="default_shipping">true</data> -+ <data key="email" unique="prefix">John.Doe@example.com</data> -+ <data key="firstname">John</data> -+ <data key="lastname">Doe</data> -+ <data key="fullname">John Doe</data> -+ <data key="password">pwdTest123!</data> -+ <data key="store_id">0</data> -+ <data key="website_id">0</data> -+ <requiredEntity type="address">US_Address_NY</requiredEntity> -+ <requiredEntity type="address">DE_Address_Berlin_Not_Default_Address</requiredEntity> -+ <requiredEntity type="address">UK_Not_Default_Address</requiredEntity> -+ </entity> -+ <entity name="Retailer_Customer" type="customer"> -+ <data key="group_id">3</data> -+ <data key="default_billing">true</data> -+ <data key="default_shipping">true</data> -+ <data key="email" unique="prefix">John.Doe@example.com</data> -+ <data key="firstname">John</data> -+ <data key="lastname">Doe</data> -+ <data key="fullname">John Doe</data> -+ <data key="password">pwdTest123!</data> -+ <data key="store_id">0</data> -+ <data key="website_id">0</data> -+ <requiredEntity type="address">US_Address_CA</requiredEntity> -+ </entity> -+ <entity name="Simple_US_Customer_Two_Addresses" type="customer"> -+ <data key="group_id">0</data> -+ <data key="default_billing">true</data> -+ <data key="default_shipping">true</data> -+ <data key="email" unique="prefix">John.Doe@example.com</data> -+ <data key="firstname">John</data> -+ <data key="lastname">Doe</data> -+ <data key="fullname">John Doe</data> -+ <data key="password">pwdTest123!</data> -+ <data key="store_id">0</data> -+ <data key="website_id">0</data> -+ <requiredEntity type="address">US_Address_TX</requiredEntity> -+ <requiredEntity type="address">US_Address_NY_Not_Default_Address</requiredEntity> -+ </entity> -+ <entity name="Simple_US_Customer_Incorrect_Email" type="customer"> -+ <data key="group_id">0</data> -+ <data key="default_billing">true</data> -+ <data key="default_shipping">true</data> -+ <data key="email">><script>alert(1);</script>@example.com</data> -+ <data key="firstname">John</data> -+ <data key="lastname">Doe</data> -+ <data key="fullname">John Doe</data> -+ <data key="password">pwdTest123!</data> -+ <data key="store_id">0</data> -+ <data key="website_id">0</data> -+ <requiredEntity type="address">US_Address_CA</requiredEntity> -+ </entity> -+ <entity name="John_Smith_Customer" type="customer"> -+ <data key="group_id">1</data> -+ <data key="email" unique="prefix">john.smith@example.com</data> -+ <data key="firstname">John</data> -+ <data key="lastname">Smith</data> -+ <data key="fullname">John Smith</data> -+ <data key="password">pwdTest123!</data> -+ <data key="store_id">0</data> -+ <data key="website_id">0</data> -+ </entity> - </entities> -diff --git a/app/code/Magento/Customer/Test/Mftf/Data/CustomerGroupData.xml b/app/code/Magento/Customer/Test/Mftf/Data/CustomerGroupData.xml -index cc8e16f017f..28305d37cf7 100644 ---- a/app/code/Magento/Customer/Test/Mftf/Data/CustomerGroupData.xml -+++ b/app/code/Magento/Customer/Test/Mftf/Data/CustomerGroupData.xml -@@ -7,15 +7,31 @@ - --> - - <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="NotLoggedInCustomerGroup" type="customerGroup"> -+ <data key="id">0</data> -+ <data key="code">NOT LOGGED IN</data> -+ <data key="tax_class_id">3</data> -+ <data key="tax_class_name">Retail Customer</data> -+ </entity> - <entity name="GeneralCustomerGroup" type="customerGroup"> - <data key="code">General</data> - <data key="tax_class_id">3</data> - <data key="tax_class_name">Retail Customer</data> - </entity> -+ <entity name="CustomerGroupChange" type="customerGroup"> -+ <data key="code" unique="suffix">Group_</data> -+ <data key="tax_class_id">3</data> -+ <data key="tax_class_name">Retail Customer</data> -+ </entity> - <entity name="DefaultCustomerGroup" type="customerGroup"> - <array key="group_names"> - <item>General</item> - </array> - </entity> -+ <entity name="CustomCustomerGroup" type="customerGroup"> -+ <data key="code" unique="suffix">Group </data> -+ <data key="tax_class_id">3</data> -+ <data key="tax_class_name">Retail Customer</data> -+ </entity> - </entities> -diff --git a/app/code/Magento/Customer/Test/Mftf/Data/ExtensionAttributeSimple.xml b/app/code/Magento/Customer/Test/Mftf/Data/ExtensionAttributeSimple.xml -index fee4463709d..90540251877 100644 ---- a/app/code/Magento/Customer/Test/Mftf/Data/ExtensionAttributeSimple.xml -+++ b/app/code/Magento/Customer/Test/Mftf/Data/ExtensionAttributeSimple.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="ExtensionAttributeSimple" type="extension_attribute"> - <data key="is_subscribed">true</data> - </entity> -diff --git a/app/code/Magento/Customer/Test/Mftf/Data/NewCustomerData.xml b/app/code/Magento/Customer/Test/Mftf/Data/NewCustomerData.xml -new file mode 100644 -index 00000000000..347a04251f9 ---- /dev/null -+++ b/app/code/Magento/Customer/Test/Mftf/Data/NewCustomerData.xml -@@ -0,0 +1,49 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<entities xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataProfileSchema.xsd"> -+ <entity name="NewCustomerData" type="braintree_config_state"> -+ <data key="FirstName">Abgar</data> -+ <data key="LastName">Abgaryan</data> -+ <data key="Email">m@m.com</data> -+ <data key="AddressFirstName">Abgar</data> -+ <data key="AddressLastName">Abgaryan</data> -+ <data key="StreetAddress">Street</data> -+ <data key="City">Yerevan</data> -+ <data key="Zip">9999</data> -+ <data key="PhoneNumber">9999</data> -+ <data key="Country">Armenia</data> -+ </entity> -+ <entity name="Simple_UK_Customer_For_Shipment" type="customer"> -+ <data key="firstName">John</data> -+ <data key="lastName">Doe</data> -+ <data key="email">johndoe@example.com</data> -+ <data key="company">Test Company</data> -+ <data key="streetFirstLine">39 St Maurices Road</data> -+ <data key="streetSecondLine">ap. 654</data> -+ <data key="city">PULDAGON</data> -+ <data key="telephone">077 5866 0667</data> -+ <data key="country">United Kingdom</data> -+ <data key="region"> </data> -+ <data key="postcode">KW1 7NQ</data> -+ </entity> -+ <entity name="Simple_US_CA_Customer_For_Shipment" type="customer"> -+ <data key="firstName">John</data> -+ <data key="lastName">Doe</data> -+ <data key="email">johndoeusca@example.com</data> -+ <data key="company">Magento</data> -+ <data key="streetFirstLine">123 Alphabet Drive</data> -+ <data key="streetSecondLine">ap. 350</data> -+ <data key="city">Los Angeles</data> -+ <data key="telephone">555-55-555-55</data> -+ <data key="country">United States</data> -+ <data key="region">California</data> -+ <data key="postcode">90240</data> -+ </entity> -+</entities> -diff --git a/app/code/Magento/Customer/Test/Mftf/Data/RegionData.xml b/app/code/Magento/Customer/Test/Mftf/Data/RegionData.xml -index 99741a35710..280bae7de41 100644 ---- a/app/code/Magento/Customer/Test/Mftf/Data/RegionData.xml -+++ b/app/code/Magento/Customer/Test/Mftf/Data/RegionData.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="CustomerRegionOne" type="region"> - <data key="region_code">100</data> - <data key="region_id">12</data> -@@ -15,7 +15,7 @@ - <entity name="RegionTX" type="region"> - <data key="region">Texas</data> - <data key="region_code">TX</data> -- <data key="region_id">1</data> -+ <data key="region_id">57</data> - </entity> - <entity name="RegionCA" type="region"> - <data key="region">California</data> -@@ -27,4 +27,9 @@ - <data key="region_code">NY</data> - <data key="region_id">43</data> - </entity> -+ <entity name="RegionUT" type="region"> -+ <data key="region">Utah</data> -+ <data key="region_code">UT</data> -+ <data key="region_id">58</data> -+ </entity> - </entities> -diff --git a/app/code/Magento/Customer/Test/Mftf/Metadata/address-meta.xml b/app/code/Magento/Customer/Test/Mftf/Metadata/address-meta.xml -index deb911f244f..10f63a5a2a8 100644 ---- a/app/code/Magento/Customer/Test/Mftf/Metadata/address-meta.xml -+++ b/app/code/Magento/Customer/Test/Mftf/Metadata/address-meta.xml -@@ -7,7 +7,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"> -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataOperation.xsd"> - <operation name="CreateAddress" dataType="address" type="create"> - <field key="region">region</field> - <field key="country_id">string</field> -diff --git a/app/code/Magento/Customer/Test/Mftf/Metadata/customer-meta.xml b/app/code/Magento/Customer/Test/Mftf/Metadata/customer-meta.xml -index ab2ee2aeddb..0d8aeb6614b 100644 ---- a/app/code/Magento/Customer/Test/Mftf/Metadata/customer-meta.xml -+++ b/app/code/Magento/Customer/Test/Mftf/Metadata/customer-meta.xml -@@ -7,7 +7,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"> -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataOperation.xsd"> - <operation name="CreateCustomer" dataType="customer" type="create" auth="adminOauth" url="/V1/customers" method="POST"> - <contentType>application/json</contentType> - <object dataType="customer" key="customer"> -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 00000000000..c3132b5b6a4 ---- /dev/null -+++ b/app/code/Magento/Customer/Test/Mftf/Metadata/customer_config_account_sharing-meta.xml -@@ -0,0 +1,33 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+<operations xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataOperation.xsd"> -+ <operation name="CustomerAccountShareConfig" dataType="customer_account_sharing_config" type="create" auth="adminFormKey" url="/admin/system_config/save/section/customer/" -+ successRegex="/messages-message-success/" returnRegex="" method="POST"> -+ <object key="groups" dataType="customer_account_sharing_config"> -+ <object key="account_share" dataType="customer_account_sharing_config"> -+ <object key="fields" dataType="customer_account_sharing_config"> -+ <object key="scope" dataType="account_share_scope_value"> -+ <field key="value">string</field> -+ </object> -+ </object> -+ </object> -+ </object> -+ </operation> -+ <operation name="CustomerAccountShareConfigInherit" dataType="customer_account_sharing_config_inherit" type="create" auth="adminFormKey" url="/admin/system_config/save/section/customer/" -+ method="POST"> -+ <object key="groups" dataType="customer_account_sharing_config_inherit"> -+ <object key="account_share" dataType="customer_account_sharing_config_inherit"> -+ <object key="fields" dataType="customer_account_sharing_config_inherit"> -+ <object key="scope" dataType="account_share_scope_inherit"> -+ <field key="inherit">boolean</field> -+ </object> -+ </object> -+ </object> -+ </object> -+ </operation> -+</operations> -diff --git a/app/code/Magento/Customer/Test/Mftf/Metadata/customer_create_new_account-meta.xml b/app/code/Magento/Customer/Test/Mftf/Metadata/customer_create_new_account-meta.xml -new file mode 100644 -index 00000000000..89ed477cb32 ---- /dev/null -+++ b/app/code/Magento/Customer/Test/Mftf/Metadata/customer_create_new_account-meta.xml -@@ -0,0 +1,28 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<operations xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataOperation.xsd"> -+ <operation name="CustomerCreateNewAccountOptionsConfigState" dataType="customer_create_new_account_config" type="create" auth="adminFormKey" url="/admin/system_config/save/section/customer/" method="POST"> -+ <object key="groups" dataType="customer_create_new_account_config"> -+ <object key="create_account" dataType="customer_create_new_account_config"> -+ <object key="fields" dataType="customer_create_new_account_config"> -+ <object key="auto_group_assign" dataType="auto_group_assign"> -+ <field key="value">string</field> -+ </object> -+ <object key="viv_on_each_transaction" dataType="viv_on_each_transaction"> -+ <field key="value">string</field> -+ </object> -+ <object key="vat_frontend_visibility" dataType="vat_frontend_visibility"> -+ <field key="value">string</field> -+ </object> -+ </object> -+ </object> -+ </object> -+ </operation> -+</operations> -diff --git a/app/code/Magento/Customer/Test/Mftf/Metadata/customer_extension_attribute-meta.xml b/app/code/Magento/Customer/Test/Mftf/Metadata/customer_extension_attribute-meta.xml -index 8561e937221..06c7b74aef0 100644 ---- a/app/code/Magento/Customer/Test/Mftf/Metadata/customer_extension_attribute-meta.xml -+++ b/app/code/Magento/Customer/Test/Mftf/Metadata/customer_extension_attribute-meta.xml -@@ -7,7 +7,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"> -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataOperation.xsd"> - <operation name="CreateCustomerExtensionAttribute" dataType="customer_extension_attribute" type="create"> - <field key="is_subscribed">boolean</field> - <field key="extension_attribute">customer_nested_extension_attribute</field> -diff --git a/app/code/Magento/Customer/Test/Mftf/Metadata/customer_group-meta.xml b/app/code/Magento/Customer/Test/Mftf/Metadata/customer_group-meta.xml -new file mode 100644 -index 00000000000..3139ea278a0 ---- /dev/null -+++ b/app/code/Magento/Customer/Test/Mftf/Metadata/customer_group-meta.xml -@@ -0,0 +1,22 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<operations xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataOperation.xsd"> -+ <operation name="CreateCustomerGroup" dataType="customerGroup" type="create" auth="adminOauth" url="/V1/customerGroups" method="POST"> -+ <contentType>application/json</contentType> -+ <object dataType="customerGroup" key="group"> -+ <field key="code">string</field> -+ <field key="tax_class_id">integer</field> -+ <field key="tax_class_name">string</field> -+ </object> -+ </operation> -+ <operation name="DeleteCustomerGroup" dataType="customerGroup" type="delete" auth="adminOauth" url="/V1/customerGroups/{id}" method="DELETE"> -+ <contentType>application/json</contentType> -+ </operation> -+</operations> -\ No newline at end of file -diff --git a/app/code/Magento/Customer/Test/Mftf/Metadata/customer_nested_extension_attribute-meta.xml b/app/code/Magento/Customer/Test/Mftf/Metadata/customer_nested_extension_attribute-meta.xml -index eb9829cca49..a2741b7817b 100644 ---- a/app/code/Magento/Customer/Test/Mftf/Metadata/customer_nested_extension_attribute-meta.xml -+++ b/app/code/Magento/Customer/Test/Mftf/Metadata/customer_nested_extension_attribute-meta.xml -@@ -7,7 +7,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"> -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataOperation.xsd"> - <operation name="CreateNestedExtensionAttribute" dataType="customer_nested_extension_attribute" type="create"> - <field key="id">integer</field> - <field key="customer_id">integer</field> -diff --git a/app/code/Magento/Customer/Test/Mftf/Metadata/region-meta.xml b/app/code/Magento/Customer/Test/Mftf/Metadata/region-meta.xml -index 3dd019462c8..5c21c5318e5 100644 ---- a/app/code/Magento/Customer/Test/Mftf/Metadata/region-meta.xml -+++ b/app/code/Magento/Customer/Test/Mftf/Metadata/region-meta.xml -@@ -7,7 +7,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"> -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataOperation.xsd"> - <operation name="CreateRegion" dataType="region" type="create"> - <field key="region_code">string</field> - <field key="region">string</field> -diff --git a/app/code/Magento/Customer/Test/Mftf/Page/AdminCustomerConfigPage.xml b/app/code/Magento/Customer/Test/Mftf/Page/AdminCustomerConfigPage.xml -new file mode 100644 -index 00000000000..282f9bb6fde ---- /dev/null -+++ b/app/code/Magento/Customer/Test/Mftf/Page/AdminCustomerConfigPage.xml -@@ -0,0 +1,12 @@ -+<?xml version="1.0" encoding="utf-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+<pages xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/PageObject.xsd"> -+ <page name="AdminCustomerConfigPage" url="admin/system_config/edit/section/customer/{{tabLink}}" area="admin" parameterized="true" module="Magento_Customer"> -+ <section name="AdminCustomerConfigSection"/> -+ </page> -+</pages> -diff --git a/app/code/Magento/Customer/Test/Mftf/Page/AdminCustomerGroupPage.xml b/app/code/Magento/Customer/Test/Mftf/Page/AdminCustomerGroupPage.xml -new file mode 100644 -index 00000000000..9adda6a74ba ---- /dev/null -+++ b/app/code/Magento/Customer/Test/Mftf/Page/AdminCustomerGroupPage.xml -@@ -0,0 +1,14 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<pages xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/PageObject.xsd"> -+ <page name="AdminCustomerGroupPage" url="/customer/group/" area="admin" module="Magento_Customer"> -+ <section name="AdminCustomerGroupMainSection"/> -+ </page> -+</pages> -diff --git a/app/code/Magento/Customer/Test/Mftf/Page/AdminCustomerGroupsIndexPage.xml b/app/code/Magento/Customer/Test/Mftf/Page/AdminCustomerGroupsIndexPage.xml -new file mode 100644 -index 00000000000..5981fb7c907 ---- /dev/null -+++ b/app/code/Magento/Customer/Test/Mftf/Page/AdminCustomerGroupsIndexPage.xml -@@ -0,0 +1,14 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<pages xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/PageObject.xsd"> -+ <page name="AdminCustomerGroupsIndexPage" url="/customer/group/" area="admin" module="Magento_Customer"> -+ <section name="AdminCustomerGroupGridActionsSection"/> -+ </page> -+</pages> -diff --git a/app/code/Magento/Customer/Test/Mftf/Page/AdminCustomerPage.xml b/app/code/Magento/Customer/Test/Mftf/Page/AdminCustomerPage.xml -index 06ab646aa4c..114c737e361 100644 ---- a/app/code/Magento/Customer/Test/Mftf/Page/AdminCustomerPage.xml -+++ b/app/code/Magento/Customer/Test/Mftf/Page/AdminCustomerPage.xml -@@ -7,7 +7,7 @@ - --> - - <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="AdminCustomerPage" url="/customer/index/" area="admin" module="Magento_Customer"> - <section name="AdminCustomerGridMainActionsSection"/> - <section name="AdminCustomerMessagesSection"/> -diff --git a/app/code/Magento/Customer/Test/Mftf/Page/AdminEditCustomerPage.xml b/app/code/Magento/Customer/Test/Mftf/Page/AdminEditCustomerPage.xml -index 9a28bad4e0d..9bd382da8eb 100644 ---- a/app/code/Magento/Customer/Test/Mftf/Page/AdminEditCustomerPage.xml -+++ b/app/code/Magento/Customer/Test/Mftf/Page/AdminEditCustomerPage.xml -@@ -6,9 +6,13 @@ - */ - --> - <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="AdminEditCustomerPage" url="/customer/index/edit/id/{{var1}}" area="admin" module="Magento_Customer" parameterized="true"> - <section name="AdminCustomerAccountInformationSection"/> -+ <section name="AdminCustomerAddressesGridSection"/> -+ <section name="AdminCustomerAddressesGridActionsSection"/> -+ <section name="AdminCustomerAddressesSection"/> - <section name="AdminCustomerMainActionsSection"/> -+ <section name="AdminEditCustomerAddressesSection" /> - </page> - </pages> -diff --git a/app/code/Magento/Customer/Test/Mftf/Page/AdminNewCustomerGroupPage.xml b/app/code/Magento/Customer/Test/Mftf/Page/AdminNewCustomerGroupPage.xml -new file mode 100644 -index 00000000000..2c52b3ec05c ---- /dev/null -+++ b/app/code/Magento/Customer/Test/Mftf/Page/AdminNewCustomerGroupPage.xml -@@ -0,0 +1,14 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<pages xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/PageObject.xsd"> -+ <page name="AdminNewCustomerGroupPage" url="/customer/group/new/" area="admin" module="Magento_Customer"> -+ <section name="AdminNewCustomerGroupSection"/> -+ </page> -+</pages> -diff --git a/app/code/Magento/Customer/Test/Mftf/Page/AdminNewCustomerPage.xml b/app/code/Magento/Customer/Test/Mftf/Page/AdminNewCustomerPage.xml -index 646f03181d8..57a30d6f989 100644 ---- a/app/code/Magento/Customer/Test/Mftf/Page/AdminNewCustomerPage.xml -+++ b/app/code/Magento/Customer/Test/Mftf/Page/AdminNewCustomerPage.xml -@@ -7,7 +7,7 @@ - --> - - <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="AdminNewCustomerPage" url="/customer/index/new" area="admin" module="Magento_Customer"> - <section name="AdminCustomerAccountInformationSection"/> - <section name="AdminCustomerMainActionsSection"/> -diff --git a/app/code/Magento/Customer/Test/Mftf/Page/StorefrontCustomerAccountChangePasswordPage.xml b/app/code/Magento/Customer/Test/Mftf/Page/StorefrontCustomerAccountChangePasswordPage.xml -new file mode 100644 -index 00000000000..43198297b17 ---- /dev/null -+++ b/app/code/Magento/Customer/Test/Mftf/Page/StorefrontCustomerAccountChangePasswordPage.xml -@@ -0,0 +1,14 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<pages xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/PageObject.xsd"> -+ <page name="StorefrontCustomerAccountChangePasswordPage" url="/customer/account/edit/changepass/1/" area="storefront" module="Magento_Customer"> -+ <section name="StorefrontCustomerAccountInformationSection"/> -+ </page> -+</pages> -diff --git a/app/code/Magento/Customer/Test/Mftf/Page/StorefrontCustomerAddressesPage.xml b/app/code/Magento/Customer/Test/Mftf/Page/StorefrontCustomerAddressesPage.xml -new file mode 100644 -index 00000000000..b9bede51330 ---- /dev/null -+++ b/app/code/Magento/Customer/Test/Mftf/Page/StorefrontCustomerAddressesPage.xml -@@ -0,0 +1,14 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<pages xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/PageObject.xsd"> -+ <page name="StorefrontCustomerAddressesPage" url="/customer/address/" area="storefront" module="Magento_Customer"> -+ <section name="StorefrontCustomerAddressesSection"/> -+ </page> -+</pages> -diff --git a/app/code/Magento/Customer/Test/Mftf/Page/StorefrontCustomerCreatePage.xml b/app/code/Magento/Customer/Test/Mftf/Page/StorefrontCustomerCreatePage.xml -index ba61cbb0bca..0d273da3530 100644 ---- a/app/code/Magento/Customer/Test/Mftf/Page/StorefrontCustomerCreatePage.xml -+++ b/app/code/Magento/Customer/Test/Mftf/Page/StorefrontCustomerCreatePage.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="StorefrontCustomerCreatePage" url="/customer/account/create/" area="storefront" module="Magento_Customer"> -- <section name="StorefrontCustomerCreateFormSection" /> -+ <section name="StorefrontCustomerCreateFormSection"/> -+ <section name="StoreFrontCustomerAdvancedAttributesSection"/> - </page> - </pages> -diff --git a/app/code/Magento/Customer/Test/Mftf/Page/StorefrontCustomerDashboardPage.xml b/app/code/Magento/Customer/Test/Mftf/Page/StorefrontCustomerDashboardPage.xml -index 941e247e18b..83a4d0a4ba8 100644 ---- a/app/code/Magento/Customer/Test/Mftf/Page/StorefrontCustomerDashboardPage.xml -+++ b/app/code/Magento/Customer/Test/Mftf/Page/StorefrontCustomerDashboardPage.xml -@@ -7,8 +7,12 @@ - --> - - <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="StorefrontCustomerDashboardPage" url="/customer/account/" area="storefront" module="Magento_Customer"> - <section name="StorefrontCustomerDashboardAccountInformationSection" /> -+ <section name="StorefrontCustomerResentOrdersSection"/> -+ <section name="StorefrontCustomerSidebarSection"/> -+ <section name="StorefrontMinicartSection"/> -+ <section name="StorefrontCustomerFooterSection"/> - </page> - </pages> -diff --git a/app/code/Magento/Customer/Test/Mftf/Page/StorefrontCustomerEditPage.xml b/app/code/Magento/Customer/Test/Mftf/Page/StorefrontCustomerEditPage.xml -new file mode 100644 -index 00000000000..d4cf90dde08 ---- /dev/null -+++ b/app/code/Magento/Customer/Test/Mftf/Page/StorefrontCustomerEditPage.xml -@@ -0,0 +1,14 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<pages xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/PageObject.xsd"> -+ <page name="StorefrontCustomerEditPage" url="/customer/account/edit/" area="storefront" module="Magento_Customer"> -+ <section name="StorefrontCustomerAccountInformationSection"/> -+ </page> -+</pages> -\ No newline at end of file -diff --git a/app/code/Magento/Customer/Test/Mftf/Page/StorefrontCustomerLogoutPage.xml b/app/code/Magento/Customer/Test/Mftf/Page/StorefrontCustomerLogoutPage.xml -new file mode 100644 -index 00000000000..b3cea8f2c29 ---- /dev/null -+++ b/app/code/Magento/Customer/Test/Mftf/Page/StorefrontCustomerLogoutPage.xml -@@ -0,0 +1,11 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+<pages xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/PageObject.xsd"> -+ <page name="StorefrontCustomerLogoutPage" url="customer/account/logout/" area="storefront" module="Magento_Customer"/> -+</pages> -diff --git a/app/code/Magento/Customer/Test/Mftf/Page/StorefrontCustomerLogoutSuccessPage.xml b/app/code/Magento/Customer/Test/Mftf/Page/StorefrontCustomerLogoutSuccessPage.xml -new file mode 100644 -index 00000000000..9c1fc7aa8a8 ---- /dev/null -+++ b/app/code/Magento/Customer/Test/Mftf/Page/StorefrontCustomerLogoutSuccessPage.xml -@@ -0,0 +1,11 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+<pages xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/PageObject.xsd"> -+ <page name="StorefrontCustomerLogoutSuccessPage" url="customer/account/logoutSuccess/" area="storefront" module="Magento_Customer"/> -+</pages> -diff --git a/app/code/Magento/Customer/Test/Mftf/Page/StorefrontCustomerOrderPage.xml b/app/code/Magento/Customer/Test/Mftf/Page/StorefrontCustomerOrderPage.xml -index bd25c67c8c9..05c4c71a61e 100644 ---- a/app/code/Magento/Customer/Test/Mftf/Page/StorefrontCustomerOrderPage.xml -+++ b/app/code/Magento/Customer/Test/Mftf/Page/StorefrontCustomerOrderPage.xml -@@ -6,7 +6,7 @@ - */ - --> - <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="StorefrontCustomerOrderPage" url="sales/order/view/order_id/" area="storefront" module="Magento_Customer"> - <section name="StorefrontCustomerOrderViewSection"/> - </page> -diff --git a/app/code/Magento/Customer/Test/Mftf/Page/StorefrontCustomerOrderViewPage.xml b/app/code/Magento/Customer/Test/Mftf/Page/StorefrontCustomerOrderViewPage.xml -index 7e6cebe6f3c..2305bd3a9b8 100644 ---- a/app/code/Magento/Customer/Test/Mftf/Page/StorefrontCustomerOrderViewPage.xml -+++ b/app/code/Magento/Customer/Test/Mftf/Page/StorefrontCustomerOrderViewPage.xml -@@ -7,7 +7,7 @@ - --> - - <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" /> - </page> -diff --git a/app/code/Magento/Customer/Test/Mftf/Page/StorefrontCustomerSignInPage.xml b/app/code/Magento/Customer/Test/Mftf/Page/StorefrontCustomerSignInPage.xml -index f6673227bea..b4814a3e4be 100644 ---- a/app/code/Magento/Customer/Test/Mftf/Page/StorefrontCustomerSignInPage.xml -+++ b/app/code/Magento/Customer/Test/Mftf/Page/StorefrontCustomerSignInPage.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="StorefrontCustomerSignInPage" url="/customer/account/login/" area="storefront" module="Magento_Customer"> - <section name="StorefrontCustomerSignInFormSection" /> -+ <section name="StorefrontCustomerLoginMessagesSection"/> - </page> - </pages> -diff --git a/app/code/Magento/Customer/Test/Mftf/Page/StorefrontCustomerStoredPaymentMethodsPage.xml b/app/code/Magento/Customer/Test/Mftf/Page/StorefrontCustomerStoredPaymentMethodsPage.xml -new file mode 100644 -index 00000000000..bec802689a6 ---- /dev/null -+++ b/app/code/Magento/Customer/Test/Mftf/Page/StorefrontCustomerStoredPaymentMethodsPage.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="StorefrontCustomerStoredPaymentMethodsPage" url="/vault/cards/listaction/" area="storefront" module="Magento_Customer"> -+ <section name="StorefrontCustomerStoredPaymentMethodsSection"/> -+ </page> -+</pages> -diff --git a/app/code/Magento/Customer/Test/Mftf/Page/StorefrontForgotPasswordPage.xml b/app/code/Magento/Customer/Test/Mftf/Page/StorefrontForgotPasswordPage.xml -new file mode 100644 -index 00000000000..2633a0c760c ---- /dev/null -+++ b/app/code/Magento/Customer/Test/Mftf/Page/StorefrontForgotPasswordPage.xml -@@ -0,0 +1,14 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<pages xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/PageObject.xsd"> -+ <page name="StorefrontForgotPasswordPage" url="/customer/account/forgotpassword/" area="storefront" module="Magento_Customer"> -+ <section name="StorefrontForgotPasswordSection" /> -+ </page> -+</pages> -diff --git a/app/code/Magento/Customer/Test/Mftf/Page/StorefrontHomePage.xml b/app/code/Magento/Customer/Test/Mftf/Page/StorefrontHomePage.xml -index 6b65bd97e8c..a466ceab2f7 100644 ---- a/app/code/Magento/Customer/Test/Mftf/Page/StorefrontHomePage.xml -+++ b/app/code/Magento/Customer/Test/Mftf/Page/StorefrontHomePage.xml -@@ -7,7 +7,7 @@ - --> - - <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="StorefrontHomePage" url="/" area="storefront" module="Magento_Customer"> - <section name="StorefrontPanelHeader" /> - </page> -diff --git a/app/code/Magento/Customer/Test/Mftf/Section/AdminCreateUserSection.xml b/app/code/Magento/Customer/Test/Mftf/Section/AdminCreateUserSection.xml -new file mode 100644 -index 00000000000..376b0b9f66d ---- /dev/null -+++ b/app/code/Magento/Customer/Test/Mftf/Section/AdminCreateUserSection.xml -@@ -0,0 +1,25 @@ -+<?xml version="1.0" encoding="utf-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> -+ <section name="AdminCreateUserSection"> -+ <element name="system" type="input" selector="#menu-magento-backend-system"/> -+ <element name="allUsers" type="input" selector="//span[contains(text(), 'All Users')]"/> -+ <element name="create" type="input" selector="#add"/> -+ <element name="usernameTextField" type="input" selector="#user_username"/> -+ <element name="firstNameTextField" type="input" selector="#user_firstname"/> -+ <element name="lastNameTextField" type="input" selector="#user_lastname"/> -+ <element name="emailTextField" type="input" selector="#user_email"/> -+ <element name="passwordTextField" type="input" selector="#user_password"/> -+ <element name="pwConfirmationTextField" type="input" selector="#user_confirmation"/> -+ <element name="currentPasswordField" type="input" selector="#user_current_password"/> -+ <element name="userRoleTab" type="button" selector="#page_tabs_roles_section"/> -+ <element name="saveButton" type="button" selector="#save"/> -+ </section> -+</sections> -diff --git a/app/code/Magento/Customer/Test/Mftf/Section/AdminCustomerAccountInformationSection.xml b/app/code/Magento/Customer/Test/Mftf/Section/AdminCustomerAccountInformationSection.xml -index 647cc6e3ee1..1de280ff4d1 100644 ---- a/app/code/Magento/Customer/Test/Mftf/Section/AdminCustomerAccountInformationSection.xml -+++ b/app/code/Magento/Customer/Test/Mftf/Section/AdminCustomerAccountInformationSection.xml -@@ -7,12 +7,32 @@ - --> - - <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="AdminCustomerAccountInformationSection"> -+ <element name="accountInformationTab" type="button" selector="#tab_customer" timeout="30"/> -+ <element name="statusInactive" type="button" selector=".admin__actions-switch-label"/> - <element name="accountInformationTitle" type="text" selector=".admin__page-nav-title"/> -+ <element name="accountInformationButton" type="text" selector="//a/span[text()='Account Information']"/> -+ <element name="addressesButton" type="select" selector="//a//span[contains(text(), 'Addresses')]"/> - <element name="firstName" type="input" selector="input[name='customer[firstname]']"/> - <element name="lastName" type="input" selector="input[name='customer[lastname]']"/> - <element name="email" type="input" selector="input[name='customer[email]']"/> - <element name="group" type="select" selector="[name='customer[group_id]']"/> -+ <element name="groupIdValue" type="text" selector="//*[@name='customer[group_id]']/option"/> -+ <element name="groupValue" type="button" selector="//span[text()='{{groupValue}}']" parameterized="true"/> -+ <element name="associateToWebsite" type="select" selector="//select[@name='customer[website_id]']"/> -+ <element name="saveCustomer" type="button" selector="//button[@title='Save Customer']"/> -+ <element name="saveCustomerAndContinueEdit" type="button" selector="//button[@title='Save and Continue Edit']"/> -+ <element name="storeView" type="select" selector="//select[@name='customer[sendemail_store_id]']"/> -+ <element name="namePrefix" type="input" selector="//input[contains(@name, 'customer[prefix]')]"/> -+ <element name="nameSuffix" type="input" selector="//input[contains(@name, 'customer[suffix]')]"/> -+ <element name="dateOfBirth" type="input" selector="//input[contains(@name, 'customer[dob]')]"/> -+ <element name="gender" type="select" selector="//select[contains(@name, 'customer[gender]')]"/> -+ <element name="firstNameRequiredMessage" type="text" selector="//input[@name='customer[firstname]']/../label[contains(.,'This is a required field.')]"/> -+ <element name="lastNameRequiredMessage" type="text" selector="//input[@name='customer[lastname]']/../label[contains(.,'This is a required field.')]"/> -+ <element name="emailRequiredMessage" type="text" selector="//input[@name='customer[email]']/../label[contains(.,'This is a required field.')]"/> -+ <element name="customAttribute" type="select" selector="//select[contains(@name, 'customer[{{attribute_code}}]')]" parameterized="true"/> -+ <element name="disabledGroup" type="text" selector="//div[@class='admin__action-group-wrap admin__action-multiselect-wrap action-select-wrap _disabled']"/> -+ <element name="attributeImage" type="block" selector="//div[contains(concat(' ',normalize-space(@class),' '),' file-uploader-preview ')]//img"/> - </section> - </sections> -diff --git a/app/code/Magento/Customer/Test/Mftf/Section/AdminCustomerActivitiesComparisonListSection.xml b/app/code/Magento/Customer/Test/Mftf/Section/AdminCustomerActivitiesComparisonListSection.xml -new file mode 100644 -index 00000000000..46eed69f224 ---- /dev/null -+++ b/app/code/Magento/Customer/Test/Mftf/Section/AdminCustomerActivitiesComparisonListSection.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="AdminCustomerActivitiesComparisonListSection"> -+ <element name="addProductToOrder" type="text" selector="//div[@id='order-sidebar_compared']//tr[td[.='{{productName}}']]//input[contains(@name,'add')]" parameterized="true" timeout="30"/> -+ <element name="addToOrderConfigure" type="button" selector="//div[@id='order-sidebar_compared']//tr[td[contains(.,'{{productName}}')]]//a[contains(@class, 'icon-configure')]" parameterized="true" timeout="30"/> -+ </section> -+</sections> -diff --git a/app/code/Magento/Customer/Test/Mftf/Section/AdminCustomerActivitiesConfigureSection.xml b/app/code/Magento/Customer/Test/Mftf/Section/AdminCustomerActivitiesConfigureSection.xml -new file mode 100644 -index 00000000000..cbe22fd26e4 ---- /dev/null -+++ b/app/code/Magento/Customer/Test/Mftf/Section/AdminCustomerActivitiesConfigureSection.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="AdminCustomerActivitiesConfigureSection"> -+ <element name="addAttribute" type="select" selector="//select[contains(concat(' ',normalize-space(@class),' '),' super-attribute-select ')]" timeout="30"/> -+ <element name="dropdownProductSelection" type="select" selector="//option[contains(text(), '{{productName}}')]" parameterized="true" timeout="30"/> -+ <element name="okButton" type="button" selector="//button[contains(concat(' ',normalize-space(@class),' '),' action-primary ')]" timeout="30"/> -+ </section> -+</sections> -diff --git a/app/code/Magento/Customer/Test/Mftf/Section/AdminCustomerActivitiesLastOrderedSection.xml b/app/code/Magento/Customer/Test/Mftf/Section/AdminCustomerActivitiesLastOrderedSection.xml -new file mode 100644 -index 00000000000..22fa1b5bd21 ---- /dev/null -+++ b/app/code/Magento/Customer/Test/Mftf/Section/AdminCustomerActivitiesLastOrderedSection.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="AdminCustomerActivitiesLastOrderedSection"> -+ <element name="addProductToOrder" type="text" selector="//div[@id='sidebar_data_reorder']//tr[td[.='{{productName}}']]//input[contains(@name,'add')]" parameterized="true" timeout="30"/> -+ </section> -+</sections> -diff --git a/app/code/Magento/Customer/Test/Mftf/Section/AdminCustomerActivitiesRecentlyViewedSection.xml b/app/code/Magento/Customer/Test/Mftf/Section/AdminCustomerActivitiesRecentlyViewedSection.xml -new file mode 100644 -index 00000000000..b3a01511354 ---- /dev/null -+++ b/app/code/Magento/Customer/Test/Mftf/Section/AdminCustomerActivitiesRecentlyViewedSection.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="AdminCustomerActivitiesRecentlyViewedSection"> -+ <element name="addToOrderConfigure" type="button" selector="//div[@id='sidebar_data_pviewed']//tr[td[contains(.,'{{productName}}')]]//a[contains(@class, 'icon-configure')]" parameterized="true" timeout="30"/> -+ </section> -+</sections> -diff --git a/app/code/Magento/Customer/Test/Mftf/Section/AdminCustomerActivitiesShoppingCartSection.xml b/app/code/Magento/Customer/Test/Mftf/Section/AdminCustomerActivitiesShoppingCartSection.xml -new file mode 100644 -index 00000000000..fb8d05f4274 ---- /dev/null -+++ b/app/code/Magento/Customer/Test/Mftf/Section/AdminCustomerActivitiesShoppingCartSection.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="AdminCustomerActivitiesShoppingCartSection"> -+ <element name="productName" type="text" selector="//div[@id='sidebar_data_cart']//td[@class='col-item']"/> -+ <element name="productPrice" type="text" selector="//div[@id='sidebar_data_cart']//td[@class='col-price']"/> -+ <element name="addToOrder" type="checkbox" selector="//input[contains(@id, 'sidebar-add_cart_item')]"/> -+ </section> -+</sections> -diff --git a/app/code/Magento/Customer/Test/Mftf/Section/AdminCustomerAddressFiltersSection.xml b/app/code/Magento/Customer/Test/Mftf/Section/AdminCustomerAddressFiltersSection.xml -new file mode 100644 -index 00000000000..f3df6cc5e8c ---- /dev/null -+++ b/app/code/Magento/Customer/Test/Mftf/Section/AdminCustomerAddressFiltersSection.xml -@@ -0,0 +1,26 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> -+ <section name="AdminCustomerAddressFiltersSection"> -+ <element name="filtersButton" type="button" selector="button[data-action='grid-filter-expand']" timeout="30"/> -+ <element name="firstnameInput" type="input" selector="input[name=firstname]"/> -+ <element name="lastnameInput" type="input" selector="input[name=lastname]"/> -+ <element name="streetInput" type="input" selector="input[name=street]"/> -+ <element name="cityInput" type="input" selector="input[name=city]"/> -+ <element name="stateSelector" type="select" selector="input[name=input]"/> -+ <element name="postcodeInput" type="input" selector="input[name=postcode]"/> -+ <element name="countryInput" type="input" selector="select[name=country_id]"/> -+ <element name="telephoneInput" type="input" selector="input[name=telephone]"/> -+ <element name="applyFilter" type="button" selector="button[data-action=grid-filter-apply]" timeout="30"/> -+ <element name="clearAll" type="button" selector=".admin__data-grid-header .action-tertiary.action-clear" timeout="30"/> -+ <element name="viewDropdown" type="button" selector=".admin__data-grid-action-bookmarks button.admin__action-dropdown"/> -+ <element name="viewBookmark" type="button" selector="//div[contains(@class, 'admin__data-grid-action-bookmarks')]/ul/li/div/a[text() = '{{label}}']" parameterized="true" timeout="30"/> -+ </section> -+</sections> -diff --git a/app/code/Magento/Customer/Test/Mftf/Section/AdminCustomerAddressGridMainActionsSection.xml b/app/code/Magento/Customer/Test/Mftf/Section/AdminCustomerAddressGridMainActionsSection.xml -new file mode 100644 -index 00000000000..f226d49e3bf ---- /dev/null -+++ b/app/code/Magento/Customer/Test/Mftf/Section/AdminCustomerAddressGridMainActionsSection.xml -@@ -0,0 +1,15 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> -+ <section name="AdminCustomerGridMainActionsSection"> -+ <element name="addNewAddress" type="button" selector=".add-new-address-button" timeout="30"/> -+ <element name="actions" type="text" selector=".admin__data-grid-header-row .action-select"/> -+ </section> -+</sections> -diff --git a/app/code/Magento/Customer/Test/Mftf/Section/AdminCustomerAddressGridSection.xml b/app/code/Magento/Customer/Test/Mftf/Section/AdminCustomerAddressGridSection.xml -new file mode 100644 -index 00000000000..e639fca834b ---- /dev/null -+++ b/app/code/Magento/Customer/Test/Mftf/Section/AdminCustomerAddressGridSection.xml -@@ -0,0 +1,16 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> -+ <section name="AdminCustomerAddressGridSection"> -+ <element name="customerGrid" type="text" selector="table[data-role='grid']"/> -+ <element name="firstRowSelectActionLink" type="text" selector="tr[data-repeat-index='0'] .action-select" timeout="30"/> -+ <element name="firstRowEditActionLink" type="text" selector="tr[data-repeat-index='0'] [data-action='item-edit']" timeout="30"/> -+ </section> -+</sections> -diff --git a/app/code/Magento/Customer/Test/Mftf/Section/AdminCustomerAddressesDefaultBillingSection.xml b/app/code/Magento/Customer/Test/Mftf/Section/AdminCustomerAddressesDefaultBillingSection.xml -new file mode 100644 -index 00000000000..a85c12fda10 ---- /dev/null -+++ b/app/code/Magento/Customer/Test/Mftf/Section/AdminCustomerAddressesDefaultBillingSection.xml -@@ -0,0 +1,16 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> -+ <section name="AdminCustomerAddressesDefaultBillingSection"> -+ <element name="addressDetails" type="text" selector="//div[@class='customer-default-billing-address-content']//div[@class='address_details']"/> -+ <element name="address" type="text" selector="//div[@class='customer-default-billing-address-content']//address//span"/> -+ <element name="editButton" type="text" selector="//button[@data-index='edit_billing_address']"/> -+ </section> -+</sections> -diff --git a/app/code/Magento/Customer/Test/Mftf/Section/AdminCustomerAddressesDefaultShippingSection.xml b/app/code/Magento/Customer/Test/Mftf/Section/AdminCustomerAddressesDefaultShippingSection.xml -new file mode 100644 -index 00000000000..610bb16874b ---- /dev/null -+++ b/app/code/Magento/Customer/Test/Mftf/Section/AdminCustomerAddressesDefaultShippingSection.xml -@@ -0,0 +1,16 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> -+ <section name="AdminCustomerAddressesDefaultShippingSection"> -+ <element name="addressDetails" type="text" selector="//div[@class='customer-default-shipping-address-content']//div[@class='address_details']"/> -+ <element name="address" type="text" selector="//div[@class='customer-default-shipping-address-content']//address//span"/> -+ <element name="editButton" type="text" selector="//button[@data-index='edit_shipping_address']"/> -+ </section> -+</sections> -diff --git a/app/code/Magento/Customer/Test/Mftf/Section/AdminCustomerAddressesGridActionsSection.xml b/app/code/Magento/Customer/Test/Mftf/Section/AdminCustomerAddressesGridActionsSection.xml -new file mode 100644 -index 00000000000..e743c4af66d ---- /dev/null -+++ b/app/code/Magento/Customer/Test/Mftf/Section/AdminCustomerAddressesGridActionsSection.xml -@@ -0,0 +1,21 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> -+ <section name="AdminCustomerAddressesGridActionsSection"> -+ <element name="spinner" type="button" selector=".spinner"/> -+ <element name="gridLoadingMask" type="button" selector=".admin__data-grid-loading-mask"/> -+ <element name="search" type="input" selector="#fulltext"/> -+ <element name="delete" type="button" selector="//*[contains(@class, 'admin__data-grid-header')]//span[contains(@class,'action-menu-item') and text()='Delete']" timeout="30"/> -+ <element name="actions" type="text" selector="//div[@class='admin__data-grid-header']//button[@class='action-select']"/> -+ <element name="filters" type="button" selector="button[data-action='grid-filter-expand']" timeout="30"/> -+ <element name="ok" type="button" selector="//button[@data-role='action']//span[text()='OK']" timeout="30"/> -+ <element name="headerRow" type="text" selector=".admin__data-grid-header-row.row.row-gutter"/> -+ </section> -+</sections> -diff --git a/app/code/Magento/Customer/Test/Mftf/Section/AdminCustomerAddressesGridSection.xml b/app/code/Magento/Customer/Test/Mftf/Section/AdminCustomerAddressesGridSection.xml -new file mode 100644 -index 00000000000..5393d6c1ab9 ---- /dev/null -+++ b/app/code/Magento/Customer/Test/Mftf/Section/AdminCustomerAddressesGridSection.xml -@@ -0,0 +1,26 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> -+ <section name="AdminCustomerAddressesGridSection"> -+ <element name="customerAddressGrid" type="text" selector="table[data-role='grid']"/> -+ <element name="firstRowSelectLink" type="text" selector="//tr[contains(@data-repeat-index, '0')]//button[@class='action-select']"/> -+ <element name="firstRowEditLink" type="text" selector="//tr[contains(@data-repeat-index, '0')]//a[contains(@data-action,'item-edit')]" timeout="30"/> -+ <element name="firstRowSetAsDefaultBillingLink" type="text" selector="//tr[contains(@data-repeat-index, '0')]//a[contains(@data-action,'item-setDefaultBilling')]" timeout="30"/> -+ <element name="firstRowSetAsDefaultShippingLink" type="text" selector="//tr[contains(@data-repeat-index, '0')]//a[contains(@data-action,'item-setDefaultShipping')]" timeout="30"/> -+ <element name="firstRowDeleteLink" type="text" selector="//tr[contains(@data-repeat-index, '0')]//a[contains(@data-action,'item-delete')]" timeout="30"/> -+ <element name="firstRowCheckbox" type="checkbox" selector="//tr[contains(@data-repeat-index, '0')]//input[contains(@data-action, 'select-row')]"/> -+ <element name="secondRowCheckbox" type="checkbox" selector="//tr[contains(@data-repeat-index, '1')]//input[contains(@data-action, 'select-row')]"/> -+ <element name="checkboxByName" type="checkbox" selector="//div[contains(text(),'{{customer}}')]/ancestor::tr[contains(@class, 'data-row')]//input[@class='admin__control-checkbox']" parameterized="true" /> -+ <element name="rowsInGrid" type="text" selector="//tr[contains(@class,'data-row')]"/> -+ <element name="checkboxByRow" type="checkbox" selector="//tr[contains(@data-repeat-index, '{{row}}')]//input[contains(@data-action, 'select-row')]" parameterized="true"/> -+ <element name="selectLinkByRow" type="text" selector="//tr[contains(@data-repeat-index, '{{row}}')]//button[@class='action-select']" parameterized="true"/> -+ <element name="deleteLinkByRow" type="text" selector="//tr[contains(@data-repeat-index, '{{row}}')]//a[contains(@data-action,'item-delete')]" parameterized="true" timeout="30"/> -+ </section> -+</sections> -diff --git a/app/code/Magento/Customer/Test/Mftf/Section/AdminCustomerAddressesSection.xml b/app/code/Magento/Customer/Test/Mftf/Section/AdminCustomerAddressesSection.xml -new file mode 100644 -index 00000000000..26df107708c ---- /dev/null -+++ b/app/code/Magento/Customer/Test/Mftf/Section/AdminCustomerAddressesSection.xml -@@ -0,0 +1,39 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> -+ <section name="AdminCustomerAddressesSection"> -+ <element name="addNewAddress" type="button" selector="//span[text()='Add New Address']"/> -+ <element name="defaultBillingAddress" type="button" selector="div[data-index=default_billing] .admin__actions-switch-label"/> -+ <element name="defaultBillingAddressCheckBox" type="input" selector="//div[@class='admin__field-control']//input[@name='default_billing']"/> -+ <element name="defaultShippingAddress" type="button" selector="div[data-index=default_shipping] .admin__actions-switch-label"/> -+ <element name="defaultShippingAddressCheckBox" type="input" selector="//div[@class='admin__field-control']//input[@name='default_shipping']"/> -+ <element name="firstNameForAddress" type="input" selector="//div[@class='admin__field-control']//input[contains(@name, 'firstname')]"/> -+ <element name="lastNameForAddress" type="input" selector="//div[@class='admin__field-control']//input[contains(@name, 'lastname')]"/> -+ <element name="streetAddress" type="input" selector="//div[@class='admin__field-control']//input[contains(@name, 'street')]"/> -+ <element name="city" type="input" selector="//div[@class='admin__field-control']//input[contains(@name, 'city')]"/> -+ <element name="company" type="input" selector="//div[@class='admin__field-control']//input[contains(@name, 'company')]"/> -+ <element name="region" type="select" selector="//div[@class='admin__field-control']//select[@name='region_id']"/> -+ <element name="regionId" type="select" selector="//div[@class='admin__field-control']//select[@name='region_id']//option[@data-title='{{regionName}}']" parameterized="true"/> -+ <element name="country" type="select" selector="//div[@class='admin__field-control']//select[contains(@name, 'country_id')]"/> -+ <element name="countryId" type="input" selector="//div[@class='admin__field-control']//select[contains(@name, 'country_id')]//option[@value='{{countryName}}']" parameterized="true"/> -+ <element name="state" type="select" selector="//div[@class='admin__field-control']//select[contains(@name, 'region_id')]"/> -+ <element name="zip" type="input" selector="//div[@class='admin__field-control']//input[contains(@name, 'postcode')]"/> -+ <element name="phoneNumber" type="input" selector="//div[@class='admin__field-control']//input[contains(@name, 'telephone')]"/> -+ <element name="saveAddress" type="button" selector="//button[@title='Save']"/> -+ <element name="customerAddressRow" type="input" selector="//tbody//tr//td//div[contains(., '{{var1}}')]" parameterized="true"/> -+ <element name="deleteButton" type="button" selector="//button[@id='delete']"/> -+ <element name="ok" type="button" selector="//button[@data-role='action']//span[text()='OK']"/> -+ <element name="streetRequiredMessage" type="text" selector="//input[@name='street[0]']/../label[contains(.,'This is a required field.')]"/> -+ <element name="cityRequiredMessage" type="text" selector="//input[@name='city']/../label[contains(.,'This is a required field.')]"/> -+ <element name="countryRequiredMessage" type="text" selector="//select[@name='country_id']/../label[contains(.,'This is a required field.')]"/> -+ <element name="postcodeRequiredMessage" type="text" selector="//input[@name='postcode']/../label[contains(.,'This is a required field.')]"/> -+ <element name="phoneNumberRequiredMessage" type="text" selector="//input[@name='telephone']/../label[contains(.,'This is a required field.')]"/> -+ </section> -+</sections> -diff --git a/app/code/Magento/Customer/Test/Mftf/Section/AdminCustomerConfigSection.xml b/app/code/Magento/Customer/Test/Mftf/Section/AdminCustomerConfigSection.xml -new file mode 100644 -index 00000000000..a934d71397b ---- /dev/null -+++ b/app/code/Magento/Customer/Test/Mftf/Section/AdminCustomerConfigSection.xml -@@ -0,0 +1,15 @@ -+<?xml version="1.0" encoding="utf-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> -+ <section name="AdminCustomerConfigSection"> -+ <element name="customerDataLifetime" type="input" selector="#customer_online_customers_section_data_lifetime"/> -+ <element name="accountSharingOptionsTab" type="button" selector="#customer_account_share-head"/> -+ <element name="shareCustomerAccountInherit" type="checkbox" selector="#customer_account_share_scope_inherit"/> -+ <element name="shareCustomerAccount" type="select" selector="#customer_account_share_scope"/> -+ </section> -+</sections> -diff --git a/app/code/Magento/Customer/Test/Mftf/Section/AdminCustomerCreateNewOrderSection.xml b/app/code/Magento/Customer/Test/Mftf/Section/AdminCustomerCreateNewOrderSection.xml -new file mode 100644 -index 00000000000..a0168799099 ---- /dev/null -+++ b/app/code/Magento/Customer/Test/Mftf/Section/AdminCustomerCreateNewOrderSection.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="AdminCustomerCreateNewOrderSection"> -+ <element name="updateChangesBtn" type="button" selector=".order-sidebar .actions .action-default.scalable" timeout="30"/> -+ <element name="productName" type="text" selector="#order-items_grid span[id*=order_item]"/> -+ <element name="productPrice" type="text" selector=".even td[class=col-price] span[class=price]"/> -+ <element name="productQty" type="input" selector="td[class=col-qty] input"/> -+ <element name="gridCell" type="text" selector="//div[contains(@id, 'order-items_grid')]//tbody[{{row}}]//td[count(//table[contains(@class, 'order-tables')]//th[contains(., '{{column}}')]/preceding-sibling::th) +1 ]" parameterized="true" timeout="30"/> -+ </section> -+</sections> -diff --git a/app/code/Magento/Customer/Test/Mftf/Section/AdminCustomerFiltersSection.xml b/app/code/Magento/Customer/Test/Mftf/Section/AdminCustomerFiltersSection.xml -index 7d106a35f0e..17a4a283c26 100644 ---- a/app/code/Magento/Customer/Test/Mftf/Section/AdminCustomerFiltersSection.xml -+++ b/app/code/Magento/Customer/Test/Mftf/Section/AdminCustomerFiltersSection.xml -@@ -7,11 +7,17 @@ - --> - - <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="AdminCustomerFiltersSection"> -+ <element name="customerStatus" type="button" selector="select[name='status']"/> - <element name="filtersButton" type="button" selector="#container > div > div.admin__data-grid-header > div:nth-child(1) > div.data-grid-filters-actions-wrap > div > button" timeout="30"/> - <element name="nameInput" type="input" selector="input[name=name]"/> - <element name="emailInput" type="input" selector="input[name=email]"/> - <element name="apply" type="button" selector="button[data-action=grid-filter-apply]" timeout="30"/> -+ <element name="clearAllFilters" type="text" selector=".admin__current-filters-actions-wrap.action-clear"/> -+ <element name="clearAll" type="button" selector=".admin__data-grid-header .action-tertiary.action-clear" timeout="30"/> -+ <element name="viewDropdown" type="button" selector=".admin__data-grid-action-bookmarks button.admin__action-dropdown"/> -+ <element name="viewBookmark" type="button" selector="//div[contains(@class, 'admin__data-grid-action-bookmarks')]/ul/li/div/a[text() = '{{label}}']" parameterized="true" timeout="30"/> -+ <element name="countryOptions" type="button" selector=".admin__data-grid-filters select[name=billing_country_id] option"/> - </section> - </sections> -diff --git a/app/code/Magento/Customer/Test/Mftf/Section/AdminCustomerGridMainActionsSection.xml b/app/code/Magento/Customer/Test/Mftf/Section/AdminCustomerGridMainActionsSection.xml -old mode 100644 -new mode 100755 -index 760b2c36633..d644b581088 ---- a/app/code/Magento/Customer/Test/Mftf/Section/AdminCustomerGridMainActionsSection.xml -+++ b/app/code/Magento/Customer/Test/Mftf/Section/AdminCustomerGridMainActionsSection.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="AdminCustomerGridMainActionsSection"> - <element name="addNewCustomer" type="button" selector="#add" timeout="30"/> -+ <element name="multicheck" type="checkbox" selector="#container>div>div.admin__data-grid-wrap>table>thead>tr>th.data-grid-multicheck-cell>div>label"/> -+ <element name="delete" type="button" selector="//*[contains(@class, 'admin__data-grid-header')]//span[contains(@class,'action-menu-item') and text()='Delete']"/> -+ <element name="actions" type="text" selector=".action-select"/> -+ <element name="customerCheckbox" type="button" selector="//*[contains(text(),'{{arg}}')]/parent::td/preceding-sibling::td/label[@class='data-grid-checkbox-cell-inner']//input" parameterized="true"/> -+ <element name="ok" type="button" selector="//button[@data-role='action']//span[text()='OK']"/> - </section> - </sections> -diff --git a/app/code/Magento/Customer/Test/Mftf/Section/AdminCustomerGridSection.xml b/app/code/Magento/Customer/Test/Mftf/Section/AdminCustomerGridSection.xml -index 515d5eed112..91363c614c1 100644 ---- a/app/code/Magento/Customer/Test/Mftf/Section/AdminCustomerGridSection.xml -+++ b/app/code/Magento/Customer/Test/Mftf/Section/AdminCustomerGridSection.xml -@@ -7,9 +7,14 @@ - --> - - <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="AdminCustomerGridSection"> - <element name="customerGrid" type="text" selector="table[data-role='grid']"/> - <element name="firstRowEditLink" type="text" selector="tr[data-repeat-index='0'] .action-menu-item" timeout="30"/> -+ <element name="gridRow" type="text" selector="//*[@data-role='sticky-el-root']/parent::div/parent::div/following-sibling::div//tbody//*[@class='data-row'][{{row}}]" parameterized="true"/> -+ <element name="selectFirstRow" type="checkbox" selector="//td[@class='data-grid-checkbox-cell']"/> -+ <element name="customerCheckboxByEmail" type="checkbox" selector="//tr[@class='data-row' and //div[text()='{{customerEmail}}']]//input[@type='checkbox']" parameterized="true" timeout="30"/> -+ <element name="customerEditLinkByEmail" type="text" selector="//tr[@class='data-row' and //div[text()='{{customerEmail}}']]//a[@class='action-menu-item']" parameterized="true" timeout="30"/> -+ <element name="customerGroupByEmail" type="text" selector="//tr[@class='data-row' and //div[text()='{{customerEmail}}']]//div[text()='{{customerGroup}}']" parameterized="true"/> - </section> - </sections> -diff --git a/app/code/Magento/Customer/Test/Mftf/Section/AdminCustomerGroupGridActionsSection.xml b/app/code/Magento/Customer/Test/Mftf/Section/AdminCustomerGroupGridActionsSection.xml -new file mode 100644 -index 00000000000..391292ca7fa ---- /dev/null -+++ b/app/code/Magento/Customer/Test/Mftf/Section/AdminCustomerGroupGridActionsSection.xml -@@ -0,0 +1,16 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> -+ <section name="AdminCustomerGroupGridActionsSection"> -+ <element name="selectButton" type="button" selector="//div[text()='{{groupName}}']/parent::td//following-sibling::td[@class='data-grid-actions-cell']//button[text()='Select']" timeout="30" parameterized="true"/> -+ <element name="deleteAction" type="button" selector="//div[text()='{{groupName}}']/parent::td//following-sibling::td[@class='data-grid-actions-cell']//a[text()='Delete']" timeout="30" parameterized="true"/> -+ <element name="actionsMenuButton" type="text" selector="//div[text()='{{groupName}}']/parent::td//following-sibling::td[@class='data-grid-actions-cell']//a[text()='{{selectItem}}']" timeout="30" parameterized="true"/> -+ </section> -+</sections> -diff --git a/app/code/Magento/Customer/Test/Mftf/Section/AdminCustomerGroupMainSection.xml b/app/code/Magento/Customer/Test/Mftf/Section/AdminCustomerGroupMainSection.xml -new file mode 100644 -index 00000000000..4cb7f5e3f62 ---- /dev/null -+++ b/app/code/Magento/Customer/Test/Mftf/Section/AdminCustomerGroupMainSection.xml -@@ -0,0 +1,20 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> -+ <section name="AdminCustomerGroupMainSection"> -+ <element name="filterBtn" type="button" selector="//button[text()='Filters']"/> -+ <element name="groupField" type="input" selector="//*[@name='customer_group_code']"/> -+ <element name="applyFiltersBtn" type="button" selector="//*[text()='Apply Filters']"/> -+ <element name="selectFirstRow" type="button" selector="//button[@class='action-select']"/> -+ <element name="deleteBtn" type="button" selector="//*[text()='Delete']"/> -+ <element name="clearAllBtn" type="button" selector="//button[text()='Clear all']"/> -+ <element name="editButtonByCustomerGroupCode" type="button" selector="//tr[.//td[count(//th[./*[.='Group']]/preceding-sibling::th) + 1][./*[.='{{code}}']]]//a[contains(@href, '/edit/')]" parameterized="true" /> -+ </section> -+</sections> -diff --git a/app/code/Magento/Customer/Test/Mftf/Section/AdminCustomerMainActionsSection.xml b/app/code/Magento/Customer/Test/Mftf/Section/AdminCustomerMainActionsSection.xml -index 1aadcb2fa46..18bc26bfd49 100644 ---- a/app/code/Magento/Customer/Test/Mftf/Section/AdminCustomerMainActionsSection.xml -+++ b/app/code/Magento/Customer/Test/Mftf/Section/AdminCustomerMainActionsSection.xml -@@ -7,8 +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="AdminCustomerMainActionsSection"> - <element name="saveButton" type="button" selector="#save" timeout="30"/> -+ <element name="saveAndContinue" type="button" selector="#save_and_continue" timeout="30"/> -+ <element name="resetPassword" type="button" selector="#resetPassword" timeout="30"/> -+ <element name="manageShoppingCart" type="button" selector="#manage_quote" timeout="30"/> -+ <element name="createOrderBtn" type="button" selector="#order" timeout="30"/> - </section> - </sections> -diff --git a/app/code/Magento/Customer/Test/Mftf/Section/AdminCustomerMessagesSection.xml b/app/code/Magento/Customer/Test/Mftf/Section/AdminCustomerMessagesSection.xml -index 08c29473a7e..b11142fd1ce 100644 ---- a/app/code/Magento/Customer/Test/Mftf/Section/AdminCustomerMessagesSection.xml -+++ b/app/code/Magento/Customer/Test/Mftf/Section/AdminCustomerMessagesSection.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="AdminCustomerMessagesSection"> - <element name="successMessage" type="text" selector=".message-success"/> - </section> -diff --git a/app/code/Magento/Customer/Test/Mftf/Section/AdminCustomerShoppingCartSection.xml b/app/code/Magento/Customer/Test/Mftf/Section/AdminCustomerShoppingCartSection.xml -new file mode 100644 -index 00000000000..c4a4d650c1e ---- /dev/null -+++ b/app/code/Magento/Customer/Test/Mftf/Section/AdminCustomerShoppingCartSection.xml -@@ -0,0 +1,23 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> -+ <section name="AdminCustomerShoppingCartSection"> -+ <element name="createOrderButton" type="button" selector="button[title='Create Order']"/> -+ </section> -+ -+ <section name="AdminCustomerShoppingCartProductItemSection"> -+ <element name="productItem" type="button" selector="#dt-products"/> -+ <element name="productNameFilter" type="input" selector="#source_products_filter_name"/> -+ <element name="searchButton" type="button" selector="//*[@id='anchor-content']//button[@title='Search']"/> -+ <element name="firstProductCheckbox" type="checkbox" selector="//*[@id='source_products_table']/tbody/tr[1]//*[@name='source_products']"/> -+ <element name="addSelectionsToMyCartButton" type="button" selector="//*[@id='products_search']/div[1]//*[text()='Add selections to my cart']"/> -+ <element name="addedProductName" type="text" selector="//*[@id='order-items_grid']//*[text()='{{var}}']" parameterized="true"/> -+ </section> -+</sections> -diff --git a/app/code/Magento/Customer/Test/Mftf/Section/AdminDeleteUserSection.xml b/app/code/Magento/Customer/Test/Mftf/Section/AdminDeleteUserSection.xml -new file mode 100644 -index 00000000000..0ba197999be ---- /dev/null -+++ b/app/code/Magento/Customer/Test/Mftf/Section/AdminDeleteUserSection.xml -@@ -0,0 +1,17 @@ -+<?xml version="1.0" encoding="utf-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> -+ <section name="AdminDeleteUserSection"> -+ <element name="theUser" selector="//td[contains(text(), 'John')]" type="button"/> -+ <element name="password" selector="#user_current_password" type="input"/> -+ <element name="delete" selector="//button/span[contains(text(), 'Delete User')]" type="button"/> -+ <element name="confirm" selector="//*[@class='action-primary action-accept']" type="button"/> -+ </section> -+</sections> -diff --git a/app/code/Magento/Customer/Test/Mftf/Section/AdminEditCustomerAddressesSection.xml b/app/code/Magento/Customer/Test/Mftf/Section/AdminEditCustomerAddressesSection.xml -new file mode 100644 -index 00000000000..4d66b40ee66 ---- /dev/null -+++ b/app/code/Magento/Customer/Test/Mftf/Section/AdminEditCustomerAddressesSection.xml -@@ -0,0 +1,35 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> -+ <section name="AdminEditCustomerAddressesSection"> -+ <element name="addresses" type="button" selector="//span[text()='Addresses']" timeout="30"/> -+ <element name="addNewAddress" type="button" selector="//span[text()='Add New Address']"/> -+ <element name="defaultBillingAddress" type="text" selector="input[name='default_billing']"/> -+ <element name="defaultShippingAddress" type="text" selector="input[name='default_shipping']"/> -+ <element name="defaultBillingAddressButton" type="text" selector="//input[@name='default_billing']/following-sibling::label"/> -+ <element name="defaultShippingAddressButton" type="text" selector="//input[@name='default_shipping']/following-sibling::label"/> -+ <element name="prefixName" type="text" selector="input[name='prefix']"/> -+ <element name="firstName" type="text" selector="input[name='firstname']" /> -+ <element name="middleName" type="text" selector="input[name='middlename']" /> -+ <element name="lastName" type="text" selector="input[name='lastname']" /> -+ <element name="suffixName" type="text" selector="input[name='suffix']" /> -+ <element name="company" type="text" selector="input[name='company']" /> -+ <element name="streetAddress" type="text" selector="input[name='street[0]']" /> -+ <element name="city" type="text" selector="//*[@class='modal-component']//input[@name='city']" /> -+ <element name="country" type="select" selector="//*[@class='modal-component']//select[@name='country_id']" /> -+ <element name="state" type="select" selector="//*[@class='modal-component']//select[@name='region_id']" /> -+ <element name="dropDownAttribute" type="select" selector="//select[@name='{{var1}}']" parameterized="true"/> -+ <element name="zipCode" type="text" selector="//*[@class='modal-component']//input[@name='postcode']" /> -+ <element name="phone" type="text" selector="//*[@class='modal-component']//input[@name='telephone']" /> -+ <element name="vat" type="text" selector="input[name='vat_id']" /> -+ <element name="save" type="button" selector="//button[@title='Save']" /> -+ </section> -+ -+</sections> -diff --git a/app/code/Magento/Customer/Test/Mftf/Section/AdminEditCustomerInformationSection.xml b/app/code/Magento/Customer/Test/Mftf/Section/AdminEditCustomerInformationSection.xml -index 76feb2624b5..1c5bbc76e4d 100644 ---- a/app/code/Magento/Customer/Test/Mftf/Section/AdminEditCustomerInformationSection.xml -+++ b/app/code/Magento/Customer/Test/Mftf/Section/AdminEditCustomerInformationSection.xml -@@ -7,8 +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="AdminEditCustomerInformationSection"> - <element name="orders" type="button" selector="#tab_orders_content" timeout="30"/> -+ <element name="addresses" type="button" selector="//a[@id='tab_address']" timeout="30"/> -+ <element name="newsLetter" type="button" selector="//a[@class='admin__page-nav-link' and @id='tab_newsletter_content']" timeout="30"/> -+ <element name="group" type="text" selector="//th[text()='Customer Group:']/../td"/> -+ <element name="customerTitle" type="text" selector="h1.page-title"/> - </section> - </sections> -diff --git a/app/code/Magento/Customer/Test/Mftf/Section/AdminEditCustomerNewsletterSection.xml b/app/code/Magento/Customer/Test/Mftf/Section/AdminEditCustomerNewsletterSection.xml -new file mode 100644 -index 00000000000..51b4b54c5c8 ---- /dev/null -+++ b/app/code/Magento/Customer/Test/Mftf/Section/AdminEditCustomerNewsletterSection.xml -@@ -0,0 +1,14 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> -+ <section name="AdminEditCustomerNewsletterSection"> -+ <element name="subscribedToNewsletter" type="checkbox" selector="//div[@class='admin__field-control control']/input[@name='subscription']"/> -+ </section> -+</sections> -\ No newline at end of file -diff --git a/app/code/Magento/Customer/Test/Mftf/Section/AdminEditCustomerOrdersSection.xml b/app/code/Magento/Customer/Test/Mftf/Section/AdminEditCustomerOrdersSection.xml -index bce4a7e848c..89fed43184b 100644 ---- a/app/code/Magento/Customer/Test/Mftf/Section/AdminEditCustomerOrdersSection.xml -+++ b/app/code/Magento/Customer/Test/Mftf/Section/AdminEditCustomerOrdersSection.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="AdminEditCustomerOrdersSection"> - <element name="orderGrid" type="text" selector="#customer_orders_grid_table"/> - </section> -diff --git a/app/code/Magento/Customer/Test/Mftf/Section/AdminNewCustomerGroupSection.xml b/app/code/Magento/Customer/Test/Mftf/Section/AdminNewCustomerGroupSection.xml -new file mode 100644 -index 00000000000..ec4a64b0146 ---- /dev/null -+++ b/app/code/Magento/Customer/Test/Mftf/Section/AdminNewCustomerGroupSection.xml -@@ -0,0 +1,18 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> -+ <section name="AdminNewCustomerGroupSection"> -+ <element name="groupName" type="input" selector="#customer_group_code"/> -+ <element name="taxClass" type="select" selector="#tax_class_id"/> -+ <element name="saveCustomerGroup" type="button" selector="#save"/> -+ <element name="resetBtn" type="button" selector="#reset"/> -+ <element name="backBtn" type="button" selector="#back"/> -+ </section> -+</sections> -diff --git a/app/code/Magento/Customer/Test/Mftf/Section/AdminUserGridSection.xml b/app/code/Magento/Customer/Test/Mftf/Section/AdminUserGridSection.xml -new file mode 100644 -index 00000000000..7c4a76871d5 ---- /dev/null -+++ b/app/code/Magento/Customer/Test/Mftf/Section/AdminUserGridSection.xml -@@ -0,0 +1,19 @@ -+<?xml version="1.0" encoding="utf-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> -+ <section name="AdminUserGridSection"> -+ <element name="usernameFilterTextField" type="input" selector="#permissionsUserGrid_filter_username"/> -+ <element name="searchButton" type="button" selector=".admin__data-grid-header button[title=Search]"/> -+ <element name="resetButton" type="button" selector="button[title='Reset Filter']"/> -+ <element name="usernameInFirstRow" type="text" selector=".col-username"/> -+ <element name="searchResultFirstRow" type="text" selector=".data-grid>tbody>tr"/> -+ <element name="successMessage" type="text" selector=".message-success"/> -+ </section> -+</sections> -diff --git a/app/code/Magento/Customer/Test/Mftf/Section/CustomersPageSection.xml b/app/code/Magento/Customer/Test/Mftf/Section/CustomersPageSection.xml -new file mode 100644 -index 00000000000..93a988caf3d ---- /dev/null -+++ b/app/code/Magento/Customer/Test/Mftf/Section/CustomersPageSection.xml -@@ -0,0 +1,21 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> -+ <section name="CustomersPageSection"> -+ <element name="addNewCustomerButton" type="button" selector="//*[@id='add']"/> -+ <element name="customerCheckbox" type="button" selector="//*[contains(text(),'{{args}}')]/parent::td/preceding-sibling::td/label[@class='data-grid-checkbox-cell-inner']" parameterized="true"/> -+ <element name="actions" type="button" selector="//div[@class='col-xs-2']/div[@class='action-select-wrap']/button[@class='action-select']" timeout="30"/> -+ <element name="delete" type="button" selector="//*[contains(@class,'admin__data-grid-header-row row row-gutter')]//*[text()='Delete']" timeout="30"/> -+ <element name="ok" type="button" selector="//button[@data-role='action']//span[text()='OK']" timeout="30"/> -+ <element name="actionItem" type="button" selector="//div[@class='admin__data-grid-outer-wrap']/div[@class='admin__data-grid-header']//span[text()='{{actionItem}}']" parameterized="true" timeout="30"/> -+ <element name="assignGroup" type="button" selector="//div[@class='admin__data-grid-outer-wrap']/div[@class='admin__data-grid-header']//ul[@class='action-submenu _active']//span[text()='{{groupName}}']" parameterized="true"/> -+ <element name="deletedSuccessMessage" type="button" selector="//*[@class='message message-success success']"/> -+ </section> -+</sections> -diff --git a/app/code/Magento/Customer/Test/Mftf/Section/CustomersSubmenuSection.xml b/app/code/Magento/Customer/Test/Mftf/Section/CustomersSubmenuSection.xml -new file mode 100644 -index 00000000000..6eeef1ba9da ---- /dev/null -+++ b/app/code/Magento/Customer/Test/Mftf/Section/CustomersSubmenuSection.xml -@@ -0,0 +1,14 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> -+ <section name="CustomersSubmenuSection"> -+ <element name="allCustomers" type="button" selector="//li[@id='menu-magento-customer-customer']//li[@data-ui-id='menu-magento-customer-customer-manage']"/> -+ </section> -+</sections> -diff --git a/app/code/Magento/Customer/Test/Mftf/Section/LoggedInCustomerHeaderLinksSection.xml b/app/code/Magento/Customer/Test/Mftf/Section/LoggedInCustomerHeaderLinksSection.xml -new file mode 100644 -index 00000000000..907551e932f ---- /dev/null -+++ b/app/code/Magento/Customer/Test/Mftf/Section/LoggedInCustomerHeaderLinksSection.xml -@@ -0,0 +1,17 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> -+ <section name="LoggedInCustomerHeaderLinksSection"> -+ <element name="customerDropdownMenu" type="button" selector=".customer-name"/> -+ <element name="myAccount" type="button" selector="//*[contains(@class, 'page-header')]//*[contains(@class, 'customer-menu')]//a[contains(., 'My Account')]"/> -+ <element name="myWishList" type="button" selector=".page-header .customer-menu .wishlist"/> -+ <element name="signOut" type="button" selector=".page-header .customer-menu .authorization-link"/> -+ </section> -+</sections> -diff --git a/app/code/Magento/Customer/Test/Mftf/Section/NewCustomerPageSection.xml b/app/code/Magento/Customer/Test/Mftf/Section/NewCustomerPageSection.xml -new file mode 100644 -index 00000000000..abb8aa6c1d8 ---- /dev/null -+++ b/app/code/Magento/Customer/Test/Mftf/Section/NewCustomerPageSection.xml -@@ -0,0 +1,32 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> -+ <section name="NewCustomerPageSection"> -+ <element name="associateToWebsite" type="select" selector="//*[@class='admin__field-control _with-tooltip']//*[@class='admin__control-select']"/> -+ <element name="group" type="select" selector="//div[@class='admin__field-control admin__control-fields required']//div[@class='admin__field-control']//select[@class='admin__control-select']"/> -+ <element name="firstName" type="input" selector="//input[@name='customer[firstname]']"/> -+ <element name="lastName" type="input" selector="//input[@name='customer[lastname]']"/> -+ <element name="email" type="input" selector="//input[@name='customer[email]']"/> -+ <element name="addresses" type="button" selector="//a[@id='tab_address']"/> -+ <element name="addNewAddress" type="button" selector="//span[text()='Add New Addresses']"/> -+ <element name="defaultBillingAddress" type="button" selector="//label[text()='Default Billing Address']"/> -+ <element name="defaultShippingAddress" type="button" selector="//label[text()='Default Shipping Address']"/> -+ <element name="firstNameForAddress" type="button" selector="//input[contains(@name, 'address')][contains(@name, 'firstname')]"/> -+ <element name="lastNameForAddress" type="button" selector="//input[contains(@name, 'address')][contains(@name, 'lastname')]"/> -+ <element name="streetAddress" type="button" selector="//input[contains(@name, 'street')]"/> -+ <element name="city" type="input" selector="//input[contains(@name, 'city')]"/> -+ <element name="country" type="select" selector="//select[contains(@name, 'country_id')]"/> -+ <element name="countryArmenia" type="select" selector="//select[contains(@name, 'country_id')]//option[@data-title='Armenia']"/> -+ <element name="zip" type="input" selector="//input[contains(@name, 'postcode')]"/> -+ <element name="phoneNumber" type="input" selector="//input[contains(@name, 'telephone')]"/> -+ <element name="saveCustomer" type="button" selector="//button[@title='Save Customer']"/> -+ <element name="createdSuccessMessage" type="button" selector="//div[@data-ui-id='messages-message-success']"/> -+ </section> -+</sections> -diff --git a/app/code/Magento/Customer/Test/Mftf/Section/StoreFrontSignOutSection.xml b/app/code/Magento/Customer/Test/Mftf/Section/StoreFrontSignOutSection.xml -new file mode 100644 -index 00000000000..29c1c9c01be ---- /dev/null -+++ b/app/code/Magento/Customer/Test/Mftf/Section/StoreFrontSignOutSection.xml -@@ -0,0 +1,15 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> -+ <section name="StoreFrontSignOutSection"> -+ <element name="customerAccount" type="button" selector=".customer-name"/> -+ <element name="signOut" type="button" selector="div.customer-menu li.authorization-link"/> -+ </section> -+</sections> -\ No newline at end of file -diff --git a/app/code/Magento/Customer/Test/Mftf/Section/StorefrontCustomerAccountInformationSection.xml b/app/code/Magento/Customer/Test/Mftf/Section/StorefrontCustomerAccountInformationSection.xml -new file mode 100644 -index 00000000000..9828c42211e ---- /dev/null -+++ b/app/code/Magento/Customer/Test/Mftf/Section/StorefrontCustomerAccountInformationSection.xml -@@ -0,0 +1,27 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> -+ <section name="StorefrontCustomerAccountInformationSection"> -+ <element name="firstName" type="input" selector="#firstname"/> -+ <element name="lastName" type="input" selector="#lastname"/> -+ <element name="changeEmail" type="checkbox" selector=".form-edit-account input[name='change_email']"/> -+ <element name="changePassword" type="checkbox" selector=".form-edit-account input[name='change_password']"/> -+ <element name="testAddedAttributeFiled" type="input" selector="//input[contains(@id,'{{var}}')]" parameterized="true"/> -+ <element name="saveButton" type="button" selector="#form-validate .action.save.primary" timeout="30"/> -+ <element name="currentPassword" type="input" selector="#current-password"/> -+ <element name="newPassword" type="input" selector="#password"/> -+ <element name="confirmNewPassword" type="input" selector="#password-confirmation"/> -+ <element name="confirmNewPasswordError" type="text" selector="#password-confirmation-error"/> -+ <element name="email" type="input" selector=".form-edit-account input[name='email']" /> -+ <element name="emailErrorMessage" type="text" selector="#email-error"/> -+ <element name="currentPasswordErrorMessage" type="text" selector="#current-password-error"/> -+ <element name="emailField" type="input" selector="#email"/> -+ </section> -+</sections> -diff --git a/app/code/Magento/Customer/Test/Mftf/Section/StorefrontCustomerAccountMainSection.xml b/app/code/Magento/Customer/Test/Mftf/Section/StorefrontCustomerAccountMainSection.xml -new file mode 100644 -index 00000000000..a501a7ac4a6 ---- /dev/null -+++ b/app/code/Magento/Customer/Test/Mftf/Section/StorefrontCustomerAccountMainSection.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="StorefrontCustomerAccountMainSection"> -+ <element name="pageTitle" type="text" selector="#maincontent .column.main [data-ui-id='page-title-wrapper']" /> -+ <element name="messageByType" type="block" selector="#maincontent .message-{{messageType}}" parameterized="true" /> -+ <element name="alertMessage" type="text" selector=".page.messages [role=alert]"/> -+ </section> -+</sections> -diff --git a/app/code/Magento/Customer/Test/Mftf/Section/StorefrontCustomerAddressFormSection.xml b/app/code/Magento/Customer/Test/Mftf/Section/StorefrontCustomerAddressFormSection.xml -new file mode 100644 -index 00000000000..112ced1bc37 ---- /dev/null -+++ b/app/code/Magento/Customer/Test/Mftf/Section/StorefrontCustomerAddressFormSection.xml -@@ -0,0 +1,25 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> -+ <section name="StorefrontCustomerAddressFormSection"> -+ <element name="firstName" type="input" selector="//form[@class='form-address-edit']//input[contains(@name, 'firstname')]"/> -+ <element name="lastName" type="input" selector="//form[@class='form-address-edit']//input[contains(@name, 'lastname')]"/> -+ <element name="company" type="input" selector="//form[@class='form-address-edit']//input[contains(@name, 'company')]"/> -+ <element name="phoneNumber" type="input" selector="//form[@class='form-address-edit']//input[contains(@name, 'telephone')]"/> -+ <element name="streetAddress" type="input" selector="//form[@class='form-address-edit']//input[contains(@name, 'street')]"/> -+ <element name="city" type="input" selector="//form[@class='form-address-edit']//input[contains(@name, 'city')]"/> -+ <element name="state" type="select" selector="//form[@class='form-address-edit']//select[contains(@name, 'region_id')]"/> -+ <element name="zip" type="input" selector="//form[@class='form-address-edit']//input[contains(@name, 'postcode')]"/> -+ <element name="country" type="select" selector="//form[@class='form-address-edit']//select[contains(@name, 'country_id')]"/> -+ <element name="useAsDefaultBillingAddressCheckBox" type="input" selector="//form[@class='form-address-edit']//input[@name='default_billing']"/> -+ <element name="useAsDefaultShippingAddressCheckBox" type="input" selector="//form[@class='form-address-edit']//input[@name='default_shipping']"/> -+ <element name="saveAddress" type="button" selector="//button[@title='Save Address']" timeout="30"/> -+ </section> -+</sections> -diff --git a/app/code/Magento/Customer/Test/Mftf/Section/StorefrontCustomerAddressesSection.xml b/app/code/Magento/Customer/Test/Mftf/Section/StorefrontCustomerAddressesSection.xml -new file mode 100644 -index 00000000000..6e01742938e ---- /dev/null -+++ b/app/code/Magento/Customer/Test/Mftf/Section/StorefrontCustomerAddressesSection.xml -@@ -0,0 +1,24 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> -+ <section name="StorefrontCustomerAddressesSection"> -+ <element name="defaultBillingAddress" type="text" selector=".box-address-billing" /> -+ <element name="billingAddressBlock" type="text" selector=".box-address-billing address" /> -+ <element name="editDefaultBillingAddress" type="text" selector="//div[@class='box-actions']//span[text()='Change Billing Address']" timeout="30"/> -+ <element name="defaultShippingAddress" type="text" selector=".box-address-shipping" /> -+ <element name="shippingAddressBlock" type="text" selector=".box-address-shipping address" /> -+ <element name="editDefaultShippingAddress" type="text" selector="//div[@class='box-actions']//span[text()='Change Shipping Address']" timeout="30"/> -+ <element name="addressesList" type="text" selector=".additional-addresses" /> -+ <element name="deleteAdditionalAddress" type="button" selector="//tbody//tr[{{var}}]//a[@class='action delete']" parameterized="true"/> -+ <element name="editAdditionalAddress" type="button" selector="//tbody//tr[{{var}}]//a[@class='action edit']" parameterized="true" timeout="30"/> -+ <element name="addNewAddress" type="button" selector="//span[text()='Add New Address']"/> -+ <element name="numberOfAddresses" type="text" selector=".toolbar-number"/> -+ </section> -+</sections> -diff --git a/app/code/Magento/Customer/Test/Mftf/Section/StorefrontCustomerCompareProductSection.xml b/app/code/Magento/Customer/Test/Mftf/Section/StorefrontCustomerCompareProductSection.xml -new file mode 100644 -index 00000000000..c2d0d58b3c2 ---- /dev/null -+++ b/app/code/Magento/Customer/Test/Mftf/Section/StorefrontCustomerCompareProductSection.xml -@@ -0,0 +1,24 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> -+ <section name="StorefrontCustomerCompareProductSection"> -+ <element name="title" type="text" selector="#block-compare-heading"/> -+ <element name="emptyMessage" type="text" selector=".block-compare .empty"/> -+ -+ <element name="productListMainArea" type="block" selector="#compare-items"/> -+ <element name="productCount" type="text" selector=".block-compare .counter"/> -+ -+ <element name="productByName" type="button" selector="//*[contains(@class, 'product-items')]//a[contains(@class, 'product-item-link')][contains(text(), '{{productName}}')]" parameterized="true"/> -+ <element name="removeProductByName" type="button" selector="//li[contains(@class, 'product-item')]//*[contains(text(), '{{productName}}')]/../../a" parameterized="true"/> -+ -+ <element name="compare" type="button" selector=".actions-toolbar .compare" timeout="30"/> -+ <element name="clearAll" type="button" selector="#compare-clear-all" timeout="30"/> -+ </section> -+</sections> -diff --git a/app/code/Magento/Customer/Test/Mftf/Section/StorefrontCustomerCreateFormSection.xml b/app/code/Magento/Customer/Test/Mftf/Section/StorefrontCustomerCreateFormSection.xml -index adf898a65f2..8881a2a012c 100644 ---- a/app/code/Magento/Customer/Test/Mftf/Section/StorefrontCustomerCreateFormSection.xml -+++ b/app/code/Magento/Customer/Test/Mftf/Section/StorefrontCustomerCreateFormSection.xml -@@ -7,13 +7,28 @@ - --> - - <sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -- xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Page/etc/SectionObject.xsd"> -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> - <section name="StorefrontCustomerCreateFormSection"> - <element name="firstnameField" type="input" selector="#firstname"/> - <element name="lastnameField" type="input" selector="#lastname"/> -+ <element name="lastnameLabel" type="text" selector="//label[@for='lastname']"/> -+ <element name="signUpForNewsletter" type="checkbox" selector="//div/input[@name='is_subscribed']"/> - <element name="emailField" type="input" selector="#email_address"/> - <element name="passwordField" type="input" selector="#password"/> - <element name="confirmPasswordField" type="input" selector="#password-confirmation"/> - <element name="createAccountButton" type="button" selector="button.action.submit.primary" timeout="30"/> - </section> -+ <section name="StoreFrontCustomerAdvancedAttributesSection"> -+ <element name="textFieldAttribute" type="input" selector="//input[@id='{{var}}']" parameterized="true" /> -+ <element name="textAreaAttribute" type="input" selector="//textarea[@id='{{var}}']" parameterized="true" /> -+ <element name="multiLineFirstAttribute" type="input" selector="//input[@id='{{var}}_0']" parameterized="true" /> -+ <element name="multiLineSecondAttribute" type="input" selector="//input[@id='{{var}}_1']" parameterized="true" /> -+ <element name="datedAttribute" type="input" selector="//input[@id='{{var}}']" parameterized="true" /> -+ <element name="dropDownAttribute" type="select" selector="//select[@id='{{var}}']" parameterized="true" /> -+ <element name="dropDownOptionAttribute" type="text" selector="//*[@id='{{var}}']/option[2]" parameterized="true" /> -+ <element name="multiSelectFirstOptionAttribute" type="text" selector="//select[@id='{{var}}']/option[3]" parameterized="true" /> -+ <element name="yesNoAttribute" type="select" selector="//select[@id='{{var}}']" parameterized="true" /> -+ <element name="yesNoOptionAttribute" type="select" selector="//select[@id='{{var}}']/option[2]" parameterized="true" /> -+ <element name="selectedOption" type="text" selector="//select[@id='{{var}}']/option[@selected='selected']" parameterized="true"/> -+ </section> - </sections> -diff --git a/app/code/Magento/Customer/Test/Mftf/Section/StorefrontCustomerDashboardAccountInformationSection.xml b/app/code/Magento/Customer/Test/Mftf/Section/StorefrontCustomerDashboardAccountInformationSection.xml -index 21205c6d5d9..85d0fd166a7 100644 ---- a/app/code/Magento/Customer/Test/Mftf/Section/StorefrontCustomerDashboardAccountInformationSection.xml -+++ b/app/code/Magento/Customer/Test/Mftf/Section/StorefrontCustomerDashboardAccountInformationSection.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="StorefrontCustomerDashboardAccountInformationSection"> - <element name="ContactInformation" type="textarea" selector=".box.box-information .box-content"/> -+ <element name="edit" type="block" selector=".action.edit" timeout="15"/> -+ <element name="changePassword" type="block" selector=".action.change-password" timeout="15"/> -+ <element name="editLink" type="text" selector=".box-information .edit"/> - </section> - <section name="StorefrontCustomerAddressSection"> - <element name="firstName" type="input" selector="#firstname"/> -@@ -20,8 +23,12 @@ - <element name="streetAddress2" type="input" selector="#street_2"/> - <element name="city" type="input" selector="#city"/> - <element name="stateProvince" type="select" selector="#region_id"/> -+ <element name="stateProvinceFill" type="input" selector="#region"/> - <element name="zip" type="input" selector="#zip"/> - <element name="country" type="select" selector="#country"/> - <element name="saveAddress" type="button" selector="[data-action='save-address']" timeout="30"/> - </section> -+ <section name="StorefrontCustomerRecentOrdersSection"> -+ <element name="orderTotal" type="text" selector=".total .price"/> -+ </section> - </sections> -diff --git a/app/code/Magento/Customer/Test/Mftf/Section/StorefrontCustomerFooterSection.xml b/app/code/Magento/Customer/Test/Mftf/Section/StorefrontCustomerFooterSection.xml -new file mode 100644 -index 00000000000..f68a69df058 ---- /dev/null -+++ b/app/code/Magento/Customer/Test/Mftf/Section/StorefrontCustomerFooterSection.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="StorefrontCustomerFooterSection"> -+ <element name="footerBlock" type="block" selector="//footer"/> -+ <element name="formSubscribe" type="input" selector="input#newsletter"/> -+ <element name="buttonSubscribe" type="button" selector="//form[@id='newsletter-validate-detail']//button[contains(@class, 'subscribe')]" timeout="15"/> -+ </section> -+</sections> -diff --git a/app/code/Magento/Customer/Test/Mftf/Section/StorefrontCustomerLoginMessagesSection.xml b/app/code/Magento/Customer/Test/Mftf/Section/StorefrontCustomerLoginMessagesSection.xml -new file mode 100644 -index 00000000000..a9859cf5875 ---- /dev/null -+++ b/app/code/Magento/Customer/Test/Mftf/Section/StorefrontCustomerLoginMessagesSection.xml -@@ -0,0 +1,16 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> -+ <section name="StorefrontCustomerLoginMessagesSection"> -+ <element name="successMessage" type="text" selector=".message-success"/> -+ <element name="errorMessage" type="text" selector=".message-error"/> -+ <element name="messageByType" type="block" selector="#maincontent .message-{{messageType}}" parameterized="true"/> -+ </section> -+</sections> -diff --git a/app/code/Magento/Customer/Test/Mftf/Section/StorefrontCustomerMessagesSection.xml b/app/code/Magento/Customer/Test/Mftf/Section/StorefrontCustomerMessagesSection.xml -new file mode 100644 -index 00000000000..07d044921c8 ---- /dev/null -+++ b/app/code/Magento/Customer/Test/Mftf/Section/StorefrontCustomerMessagesSection.xml -@@ -0,0 +1,15 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> -+ <section name="StorefrontCustomerMessagesSection"> -+ <element name="successMessage" type="text" selector=".message-success"/> -+ <element name="errorMessage" type="text" selector=".message-error"/> -+ </section> -+</sections> -diff --git a/app/code/Magento/Customer/Test/Mftf/Section/StorefrontCustomerOrderSection.xml b/app/code/Magento/Customer/Test/Mftf/Section/StorefrontCustomerOrderSection.xml -index c39dfef5f74..ec5141d84b1 100644 ---- a/app/code/Magento/Customer/Test/Mftf/Section/StorefrontCustomerOrderSection.xml -+++ b/app/code/Magento/Customer/Test/Mftf/Section/StorefrontCustomerOrderSection.xml -@@ -7,9 +7,15 @@ - --> - - <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="StorefrontCustomerOrderSection"> -+ <element name="isMyOrdersSection" type="text" selector="//*[@class='page-title']//*[contains(text(), 'My Orders')]"/> - <element name="productCustomOptions" type="text" selector="//strong[contains(@class, 'product-item-name') and normalize-space(.)='{{var1}}']/following-sibling::*[contains(@class, 'item-options')]/dt[normalize-space(.)='{{var2}}']/following-sibling::dd[normalize-space(.)='{{var3}}']" parameterized="true"/> - <element name="productCustomOptionsFile" type="text" selector="//strong[contains(@class, 'product-item-name') and normalize-space(.)='{{var1}}']/following-sibling::*[contains(@class, 'item-options')]/dt[normalize-space(.)='{{var2}}']/following-sibling::dd[contains(.,'{{var3}}')]" parameterized="true"/> -+ <element name="productCustomOptionsLink" type="text" selector="//strong[contains(@class, 'product-item-name') and normalize-space(.)='{{var1}}']/following-sibling::*[contains(@class, 'item-options')]/dt[normalize-space(.)='{{var2}}']/following-sibling::dd//a[text() = '{{var3}}']" parameterized="true"/> -+ <element name="status" type="text" selector="//td[contains(concat(' ',normalize-space(@class),' '),' col status ')]"/> -+ <element name="viewOrder" type="button" selector="//td[contains(concat(' ',normalize-space(@class),' '),' col actions ')]/a[contains(concat(' ',normalize-space(@class),' '),' action view ')]"/> -+ <element name="tabRefund" type="button" selector="//a[text()='Refunds']"/> -+ <element name="grandTotalRefund" type="text" selector="td[data-th='Grand Total'] > strong > span.price"/> - </section> - </sections> -diff --git a/app/code/Magento/Customer/Test/Mftf/Section/StorefrontCustomerOrderViewSection.xml b/app/code/Magento/Customer/Test/Mftf/Section/StorefrontCustomerOrderViewSection.xml -index 9ea271dad7b..09b79fe8311 100644 ---- a/app/code/Magento/Customer/Test/Mftf/Section/StorefrontCustomerOrderViewSection.xml -+++ b/app/code/Magento/Customer/Test/Mftf/Section/StorefrontCustomerOrderViewSection.xml -@@ -6,12 +6,17 @@ - */ - --> - <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="StorefrontCustomerOrderViewSection"> - <element name="reorder" type="text" selector="a.action.order" timeout="30"/> - <element name="orderTitle" type="text" selector=".page-title span"/> - <element name="myOrdersTable" type="text" selector="#my-orders-table"/> - <element name="subtotal" type="text" selector=".subtotal .amount"/> - <element name="paymentMethod" type="text" selector=".payment-method dt"/> -+ <element name="printOrderLink" type="text" selector="a.action.print" timeout="30"/> -+ <element name="shippingAddress" type="text" selector=".box.box-order-shipping-address"/> -+ <element name="billingAddress" type="text" selector=".box.box-order-billing-address"/> -+ <element name="orderStatusInGrid" type="text" selector="//td[contains(.,'{{orderId}}')]/../td[contains(.,'{{status}}')]" parameterized="true"/> -+ <element name="pager" type="block" selector=".pager"/> - </section> - </sections> -diff --git a/app/code/Magento/Customer/Test/Mftf/Section/StorefrontCustomerResentOrdersSection.xml b/app/code/Magento/Customer/Test/Mftf/Section/StorefrontCustomerResentOrdersSection.xml -new file mode 100644 -index 00000000000..6dc81a3a933 ---- /dev/null -+++ b/app/code/Magento/Customer/Test/Mftf/Section/StorefrontCustomerResentOrdersSection.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="StorefrontCustomerResentOrdersSection"> -+ <element name="blockResentOrders" type="text" selector="//div[@class='block-title order']"/> -+ <element name="viewOrder" type="button" selector="//td[text()='{{orderId}}']/following-sibling::td[@data-th='Actions']/a[@class='action view']" parameterized="true"/> -+ </section> -+</sections> -diff --git a/app/code/Magento/Customer/Test/Mftf/Section/StorefrontCustomerSidebarSection.xml b/app/code/Magento/Customer/Test/Mftf/Section/StorefrontCustomerSidebarSection.xml -new file mode 100644 -index 00000000000..c6b9aa0372e ---- /dev/null -+++ b/app/code/Magento/Customer/Test/Mftf/Section/StorefrontCustomerSidebarSection.xml -@@ -0,0 +1,15 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> -+ <section name="StorefrontCustomerSidebarSection"> -+ <element name="sidebarTab" type="text" selector="//div[@id='block-collapsible-nav']//a[text()='{{tabName}}']" parameterized="true" timeout="60"/> -+ <element name="sidebarCurrentTab" type="text" selector="//div[@id='block-collapsible-nav']//*[contains(text(), '{{var}}')]" parameterized="true"/> -+ </section> -+</sections> -diff --git a/app/code/Magento/Customer/Test/Mftf/Section/StorefrontCustomerSignInFormSection.xml b/app/code/Magento/Customer/Test/Mftf/Section/StorefrontCustomerSignInFormSection.xml -index 9cc4a43d31b..b2c583e008a 100644 ---- a/app/code/Magento/Customer/Test/Mftf/Section/StorefrontCustomerSignInFormSection.xml -+++ b/app/code/Magento/Customer/Test/Mftf/Section/StorefrontCustomerSignInFormSection.xml -@@ -7,10 +7,24 @@ - --> - - <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="StorefrontCustomerSignInFormSection"> - <element name="emailField" type="input" selector="#email"/> - <element name="passwordField" type="input" selector="#pass"/> - <element name="signInAccountButton" type="button" selector="#send2" timeout="30"/> -+ <element name="forgotPasswordLink" type="button" selector=".action.remind" timeout="10"/> -+ <element name="customerLoginBlock" type="text" selector=".login-container .block.block-customer-login"/> -+ </section> -+ <section name="StorefrontCustomerSignInPopupFormSection"> -+ <element name="errorMessage" type="input" selector="[data-ui-id='checkout-cart-validationmessages-message-error']"/> -+ <element name="email" type="input" selector="#customer-email"/> -+ <element name="password" type="input" selector="#pass"/> -+ <element name="signIn" type="button" selector="#send2" timeout="30"/> -+ </section> -+ <section name="StorefrontCustomerSignInLinkSection"> -+ <element name="signInLink" type="button" selector=".action-auth-toggle" timeout="30"/> -+ <element name="email" type="input" selector="#login-email"/> -+ <element name="password" type="input" selector="#login-password"/> -+ <element name="signInBtn" type="button" selector="//button[contains(@class, 'action-login') and not(contains(@id,'send2'))]" timeout="30"/> - </section> - </sections> -diff --git a/app/code/Magento/Customer/Test/Mftf/Section/StorefrontCustomerStoredPaymentMethodsSection.xml b/app/code/Magento/Customer/Test/Mftf/Section/StorefrontCustomerStoredPaymentMethodsSection.xml -new file mode 100644 -index 00000000000..d6b586e42f2 ---- /dev/null -+++ b/app/code/Magento/Customer/Test/Mftf/Section/StorefrontCustomerStoredPaymentMethodsSection.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="StorefrontCustomerStoredPaymentMethodsSection"> -+ <element name="cardNumber" type="text" selector="td.card-number"/> -+ <element name="expirationDate" type="text" selector="td.card-expire"/> -+ </section> -+</sections> -diff --git a/app/code/Magento/Customer/Test/Mftf/Section/StorefrontForgotPasswordSection.xml b/app/code/Magento/Customer/Test/Mftf/Section/StorefrontForgotPasswordSection.xml -new file mode 100644 -index 00000000000..bdae69c425d ---- /dev/null -+++ b/app/code/Magento/Customer/Test/Mftf/Section/StorefrontForgotPasswordSection.xml -@@ -0,0 +1,16 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> -+ <section name="StorefrontForgotPasswordSection"> -+ <element name="pageTitle" type="text" selector=".page-title"/> -+ <element name="email" type="input" selector="#email_address"/> -+ <element name="resetMyPasswordButton" type="button" selector=".action.submit.primary" timeout="30"/> -+ </section> -+</sections> -diff --git a/app/code/Magento/Customer/Test/Mftf/Section/StorefrontPanelHeaderSection.xml b/app/code/Magento/Customer/Test/Mftf/Section/StorefrontPanelHeaderSection.xml -index 65e7aa7a121..3610532c535 100644 ---- a/app/code/Magento/Customer/Test/Mftf/Section/StorefrontPanelHeaderSection.xml -+++ b/app/code/Magento/Customer/Test/Mftf/Section/StorefrontPanelHeaderSection.xml -@@ -7,9 +7,17 @@ - --> - - <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="WelcomeMessage" Deprecated due to incorrect naming convention please use name="welcomeMessage" --> - <element name="WelcomeMessage" type="text" selector=".greet.welcome span"/> -- <element name="createAnAccountLink" type="select" selector=".panel.header li:nth-child(3)" timeout="30"/> -+ -+ <element name="welcomeMessage" type="text" selector="header>.panel .greet.welcome" /> -+ <element name="createAnAccountLink" type="select" selector="//div[@class='panel wrapper']//li/a[contains(.,'Create an Account')]" timeout="30"/> -+ <element name="notYouLink" type="button" selector=".greet.welcome span a"/> -+ <element name="customerWelcome" type="text" selector=".panel.header .greet.welcome"/> -+ <element name="customerWelcomeMenu" type="text" selector=".panel.header .customer-welcome .customer-name"/> -+ <element name="customerLoginLink" type="button" selector=".panel.header .header.links .authorization-link a" timeout="30"/> -+ <element name="customerLogoutLink" type="text" selector=".panel.header .customer-welcome .customer-menu .authorization-link a" timeout="30"/> - </section> - </sections> -diff --git a/app/code/Magento/Customer/Test/Mftf/Section/SwitchAccountSection.xml b/app/code/Magento/Customer/Test/Mftf/Section/SwitchAccountSection.xml -new file mode 100644 -index 00000000000..4442e317694 ---- /dev/null -+++ b/app/code/Magento/Customer/Test/Mftf/Section/SwitchAccountSection.xml -@@ -0,0 +1,21 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> -+ <section name="LoginFormSection"> -+ <element name="username" type="input" selector="#username"/> -+ <element name="password" type="input" selector="#login"/> -+ <element name="signIn" type="button" selector=".actions .action-primary" timeout="30"/> -+ </section> -+ -+ <section name="SignOutSection"> -+ <element name="admin" type="button" selector=".admin__action-dropdown-text"/> -+ <element name="logout" type="button" selector="//*[contains(text(), 'Sign Out')]"/> -+ </section> -+</sections> -diff --git a/app/code/Magento/Customer/Test/Mftf/Test/AddingProductWithExpiredSessionTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/AddingProductWithExpiredSessionTest.xml -new file mode 100644 -index 00000000000..01f35439f23 ---- /dev/null -+++ b/app/code/Magento/Customer/Test/Mftf/Test/AddingProductWithExpiredSessionTest.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="AddingProductWithExpiredSessionTest"> -+ <annotations> -+ <title value="Adding a product to cart from category page with an expired session"/> -+ <description value="Adding a product to cart from category page with an expired session"/> -+ <features value="Module/ Catalog"/> -+ <severity value="MAJOR"/> -+ <testCaseId value="MAGETWO-93289"/> -+ <stories value="MAGETWO-66666: Adding a product to cart from category page with an expired session does not allow product to be added"/> -+ <group value="customer"/> -+ </annotations> -+ -+ <before> -+ <createData entity="_defaultCategory" stepKey="createCategory"/> -+ <createData entity="_defaultProduct" stepKey="createSimpleProduct"> -+ <requiredEntity createDataKey="createCategory"/> -+ </createData> -+ </before> -+ -+ <!--Navigate to a category page --> -+ <amOnPage url="$$createSimpleProduct.name$$.html" stepKey="goToProductPage"/> -+ <waitForPageLoad stepKey="waitForPageLoad"/> -+ -+ <!-- Remove PHPSESSID and form_key to replicate an expired session--> -+ <resetCookie userInput="PHPSESSID" stepKey="resetCookieForCart"/> -+ <resetCookie userInput="form_key" stepKey="resetCookieForCart2"/> -+ -+ <!-- "Add to Cart" any product--> -+ <click selector="{{StorefrontProductPageSection.addToCartBtn}}" stepKey="addToCart"/> -+ <see stepKey="assertErrorMessage" userInput="Your session has expired"/> -+ <after> -+ <!--Delete created product--> -+ <deleteData createDataKey="createSimpleProduct" stepKey="deleteProduct"/> -+ <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> -+ </after> -+ -+ </test> -+</tests> -diff --git a/app/code/Magento/Customer/Test/Mftf/Test/AdminAddNewDefaultBillingShippingCustomerAddressTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/AdminAddNewDefaultBillingShippingCustomerAddressTest.xml -new file mode 100644 -index 00000000000..fba7ebd2d4d ---- /dev/null -+++ b/app/code/Magento/Customer/Test/Mftf/Test/AdminAddNewDefaultBillingShippingCustomerAddressTest.xml -@@ -0,0 +1,69 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> -+ <test name="AdminAddNewDefaultBillingShippingCustomerAddressTest"> -+ <annotations> -+ <stories value="Add new default billing/shipping customer address"/> -+ <title value="Add new default billing/shipping customer address"/> -+ <description value="Add new default billing/shipping customer address on customer addresses tab"/> -+ <severity value="CRITICAL"/> -+ <testCaseId value="MAGETWO-94814"/> -+ <group value="customer"/> -+ </annotations> -+ <before> -+ <createData entity="Simple_US_Customer_Multiple_Addresses_No_Default_Address" stepKey="customer"/> -+ <actionGroup ref="LoginAsAdmin" stepKey="login"/> -+ </before> -+ <after> -+ <deleteData createDataKey="customer" stepKey="deleteCustomer"/> -+ <actionGroup ref="logout" stepKey="logout"/> -+ </after> -+ <!-- - -+ Step1. Login to admin and go to Customers > All Customers. -+ Step2. On *Customers* page choose customer from preconditions and open it to edit -+ Step3. Open *Addresses* tab on edit customer page and press *Add New Address* button -+ <!- --> -+ <amOnPage url="{{AdminCustomerPage.url}}" stepKey="openCustomersGridPage"/> -+ <actionGroup ref="OpenEditCustomerFromAdminActionGroup" stepKey="openEditCustomerPage"> -+ <argument name="customer" value="Simple_US_Customer_Multiple_Addresses_No_Default_Address"/> -+ </actionGroup> -+ <click selector="{{AdminEditCustomerInformationSection.addresses}}" stepKey="openAddressesTab"/> -+ <waitForPageLoad stepKey="waitForAddresses"/> -+ <conditionalClick selector="{{AdminCustomerAddressFiltersSection.clearAll}}" dependentSelector="{{AdminCustomerAddressFiltersSection.clearAll}}" visible="true" stepKey="clickOnButtonToRemoveFiltersIfPresent"/> -+ <seeElement selector="{{AdminCustomerAddressesDefaultBillingSection.address}}" stepKey="seeDefaultBillingAddressSectionBeforeChangingDefaultAddress"/> -+ <see userInput="The customer does not have default billing address" selector="{{AdminCustomerAddressesDefaultBillingSection.address}}" stepKey="assertThereIsNoDefaultBillingAddressSet"/> -+ <seeElement selector="{{AdminCustomerAddressesDefaultShippingSection.address}}" stepKey="seeDefaultShippingAddressSectionBeforeChangingDefaultAddress"/> -+ <see userInput="The customer does not have default shipping address" selector="{{AdminCustomerAddressesDefaultShippingSection.address}}" stepKey="assertThereIsNoDefaultShippingAddressSet"/> -+ <click selector="{{AdminCustomerAddressesSection.addNewAddress}}" stepKey="clickAddNewAddressButton"/> -+ <waitForPageLoad stepKey="waitForAddUpdateCustomerAddressForm"/> -+ <!--Step4. Fill all the fields with test data and press *Save* button--> -+ <click selector="{{AdminCustomerAddressesSection.defaultBillingAddress}}" stepKey="enableDefaultBillingAddress"/> -+ <click selector="{{AdminCustomerAddressesSection.defaultShippingAddress}}" stepKey="enableDefaultShippingAddress"/> -+ <fillField userInput="{{US_Address_TX.firstname}}" selector="{{AdminCustomerAddressesSection.firstNameForAddress}}" stepKey="fillFirstName"/> -+ <fillField userInput="{{US_Address_TX.lastname}}" selector="{{AdminCustomerAddressesSection.lastNameForAddress}}" stepKey="fillLastName"/> -+ <fillField userInput="{{US_Address_TX.company}}" selector="{{AdminCustomerAddressesSection.company}}" stepKey="fillCompany"/> -+ <fillField userInput="{{US_Address_TX.street[0]}}" selector="{{AdminCustomerAddressesSection.streetAddress}}" stepKey="fillStreet"/> -+ <fillField userInput="{{US_Address_TX.city}}" selector="{{AdminCustomerAddressesSection.city}}" stepKey="fillCity"/> -+ <click selector="{{AdminCustomerAddressesSection.country}}" stepKey="clickCountryToOpenListOfCountries"/> -+ <click selector="{{AdminCustomerAddressesSection.countryId(US_Address_TX.country_id)}}" stepKey="fillCountry"/> -+ <fillField userInput="{{US_Address_TX.postcode}}" selector="{{AdminCustomerAddressesSection.zip}}" stepKey="fillPostcode"/> -+ <fillField userInput="{{US_Address_TX.telephone}}" selector="{{AdminCustomerAddressesSection.phoneNumber}}" stepKey="fillTelephone"/> -+ <click selector="{{AdminCustomerAddressesSection.region}}" stepKey="clickRegionToOpenListOfRegions"/> -+ <click selector="{{AdminCustomerAddressesSection.regionId(US_Address_TX.state)}}" stepKey="fillRegion"/> -+ <click selector="{{AdminCustomerAddressesSection.saveAddress}}" stepKey="clickSaveCustomerAddressOnAddUpdateAddressForm"/> -+ <waitForPageLoad stepKey="waitForNewAddressIsCreated"/> -+ <see userInput="{{US_Address_TX.street[0]}}" selector="{{AdminCustomerAddressesDefaultBillingSection.addressDetails}}" stepKey="assertDefaultBillingAddressIsChanged"/> -+ <see userInput="{{US_Address_TX.street[0]}}" selector="{{AdminCustomerAddressesDefaultShippingSection.addressDetails}}" stepKey="assertDefaultShippingAddressIsChanged"/> -+ <click selector="{{AdminCustomerAddressesDefaultBillingSection.editButton}}" stepKey="clickEditDefaultBillingAddress"/> -+ <waitForPageLoad stepKey="waitForCustomerAddressAddUpdateFormLoad"/> -+ <assertElementContainsAttribute selector="{{AdminCustomerAddressesSection.defaultBillingAddressCheckBox}}" attribute="value" expectedValue="1" stepKey="assertDefaultBillingIsEnabledCustomerAddressAddUpdateForm"/> -+ <assertElementContainsAttribute selector="{{AdminCustomerAddressesSection.defaultShippingAddressCheckBox}}" attribute="value" expectedValue="1" stepKey="assertDefaultShippingIsEnabledOnCustomerAddressAddUpdateForm"/> -+ </test> -+</tests> -diff --git a/app/code/Magento/Customer/Test/Mftf/Test/AdminCreateCustomerGroupAlreadyExistsTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/AdminCreateCustomerGroupAlreadyExistsTest.xml -new file mode 100644 -index 00000000000..6b1c1f29f97 ---- /dev/null -+++ b/app/code/Magento/Customer/Test/Mftf/Test/AdminCreateCustomerGroupAlreadyExistsTest.xml -@@ -0,0 +1,39 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> -+ <test name="AdminCreateCustomerGroupAlreadyExistsTest"> -+ <annotations> -+ <features value="Create customer group already exists"/> -+ <stories value="Create customer group"/> -+ <title value="Create customer group already exists"/> -+ <description value="Create customer group already exists"/> -+ <severity value="CRITICAL"/> -+ <testCaseId value="MC-5302"/> -+ <group value="customer"/> -+ <group value="mtf_migrated"/> -+ </annotations> -+ <before> -+ <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> -+ </before> -+ <after> -+ <actionGroup ref="logout" stepKey="logout"/> -+ </after> -+ <!-- Steps: 1. Log in to backend as admin user. -+ 2. Navigate to Stores > Other Settings > Customer Groups. -+ 3. Start to create new Customer Group. -+ 4. Fill in all data according to data set: Group Name "General", Tax Class "Retail customer" -+ 5. Click "Save Customer Group" button. --> -+ <!-- Assert "Customer Group already exists." error message displayed --> -+ <actionGroup ref="AdminAssertErrorMessageCustomerGroupAlreadyExists" stepKey="seeErrorMessageCustomerGroupAlreadyExists"> -+ <argument name="groupName" value="{{GeneralCustomerGroup.code}}"/> -+ <argument name="taxClass" value="{{GeneralCustomerGroup.tax_class_name}}"/> -+ </actionGroup> -+ </test> -+</tests> -diff --git a/app/code/Magento/Customer/Test/Mftf/Test/AdminCreateCustomerRetailerWithoutAddressTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/AdminCreateCustomerRetailerWithoutAddressTest.xml -new file mode 100644 -index 00000000000..36592ab38e9 ---- /dev/null -+++ b/app/code/Magento/Customer/Test/Mftf/Test/AdminCreateCustomerRetailerWithoutAddressTest.xml -@@ -0,0 +1,63 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> -+ <test name="AdminCreateCustomerRetailerWithoutAddressTest"> -+ <annotations> -+ <stories value="Create customer"/> -+ <title value="Create customer, retailer without address"/> -+ <description value="Login as admin and create customer retailer without address"/> -+ <severity value="CRITICAL"/> -+ <testCaseId value="MC-5310"/> -+ <group value="mtf_migrated"/> -+ </annotations> -+ -+ <before> -+ <actionGroup ref="LoginAsAdmin" stepKey="loginToAdminPanel"/> -+ </before> -+ <after> -+ <actionGroup ref="DeleteCustomerByEmailActionGroup" stepKey="deleteCustomer"> -+ <argument name="email" value="{{CustomerEntityOne.email}}"/> -+ </actionGroup> -+ <actionGroup ref="logout" stepKey="logout"/> -+ </after> -+ -+ <!--Filter the customer From grid--> -+ <amOnPage url="{{AdminNewCustomerPage.url}}" stepKey="navigateToNewCustomerPage"/> -+ <waitForPageLoad time="30" stepKey="waitToCustomerPageLoad"/> -+ <selectOption selector="{{AdminCustomerAccountInformationSection.group}}" userInput="Retailer" stepKey="fillCustomerGroup"/> -+ <fillField userInput="{{CustomerEntityOne.firstname}}" selector="{{AdminCustomerAccountInformationSection.firstName}}" stepKey="fillFirstName"/> -+ <fillField userInput="{{CustomerEntityOne.lastname}}" selector="{{AdminCustomerAccountInformationSection.lastName}}" stepKey="fillLastName"/> -+ <fillField userInput="{{CustomerEntityOne.email}}" selector="{{AdminCustomerAccountInformationSection.email}}" stepKey="fillEmail"/> -+ <click selector="{{AdminCustomerMainActionsSection.saveButton}}" stepKey="saveCustomer"/> -+ <waitForPageLoad time="30" stepKey="waitForPageLoad1"/> -+ <seeElement selector="{{AdminCustomerMessagesSection.successMessage}}" stepKey="assertSuccessMessage"/> -+ <reloadPage stepKey="reloadPage"/> -+ -+ <!--Verify Customer in grid --> -+ <actionGroup ref="AdminFilterCustomerByEmail" stepKey="filterTheCustomerByEmail1"> -+ <argument name="email" value="{{CustomerEntityOne.email}}"/> -+ </actionGroup> -+ <waitForPageLoad stepKey="waitForCustomerPageToLoad"/> -+ <see selector="{{AdminCustomerGridSection.customerGrid}}" userInput="Retailer" stepKey="assertGroup"/> -+ <see userInput="{{CustomerEntityOne.firstname}}" selector="{{AdminCustomerGridSection.customerGrid}}" stepKey="assertFirstName"/> -+ <see userInput="{{CustomerEntityOne.lastname}}" selector="{{AdminCustomerGridSection.customerGrid}}" stepKey="assertLastName"/> -+ <see userInput="{{CustomerEntityOne.email}}" selector="{{AdminCustomerGridSection.customerGrid}}" stepKey="assertEmail"/> -+ -+ <!--Assert Customer Form --> -+ <click selector="{{AdminCustomerGridSection.firstRowEditLink}}" stepKey="clickOnEditButton1"/> -+ <waitForPageLoad stepKey="waitForCustomerEditPageToLoad1"/> -+ <click selector="{{AdminCustomerAccountInformationSection.accountInformationButton}}" stepKey="clickOnAccountInformation"/> -+ <waitForPageLoad stepKey="waitForCustomerInformationPageToLoad"/> -+ <see selector="{{AdminCustomerAccountInformationSection.groupIdValue}}" userInput="Retailer" stepKey="seeCustomerGroup1"/> -+ <seeInField selector="{{AdminCustomerAccountInformationSection.firstName}}" userInput="{{CustomerEntityOne.firstname}}" stepKey="seeCustomerFirstName"/> -+ <seeInField selector="{{AdminCustomerAccountInformationSection.lastName}}" userInput="{{CustomerEntityOne.lastname}}" stepKey="seeCustomerLastName"/> -+ <seeInField selector="{{AdminCustomerAccountInformationSection.email}}" userInput="{{CustomerEntityOne.email}}" stepKey="seeCustomerEmail"/> -+ </test> -+</tests> -diff --git a/app/code/Magento/Customer/Test/Mftf/Test/AdminCreateCustomerTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/AdminCreateCustomerTest.xml -index bde15b31ff1..78bae7ad60d 100644 ---- a/app/code/Magento/Customer/Test/Mftf/Test/AdminCreateCustomerTest.xml -+++ b/app/code/Magento/Customer/Test/Mftf/Test/AdminCreateCustomerTest.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="AdminCreateCustomerTest"> - <annotations> - <features value="Customer"/> -@@ -19,6 +19,9 @@ - <group value="customer"/> - <group value="create"/> - </annotations> -+ <before> -+ <magentoCLI command="indexer:reindex customer_grid" stepKey="reindexCustomerGrid"/> -+ </before> - <after> - <amOnPage url="admin/admin/auth/logout/" stepKey="amOnLogoutPage"/> - </after> -@@ -27,17 +30,17 @@ - <amOnPage url="{{AdminCustomerPage.url}}" stepKey="navigateToCustomers"/> - <waitForPageLoad stepKey="waitForLoad1"/> - <click selector="{{AdminCustomerGridMainActionsSection.addNewCustomer}}" stepKey="clickCreateCustomer"/> -- <waitForElement selector="{{AdminCustomerAccountInformationSection.firstName}}" stepKey="wait1"/> - <fillField userInput="{{CustomerEntityOne.firstname}}" selector="{{AdminCustomerAccountInformationSection.firstName}}" stepKey="fillFirstName"/> - <fillField userInput="{{CustomerEntityOne.lastname}}" selector="{{AdminCustomerAccountInformationSection.lastName}}" stepKey="fillLastName"/> - <fillField userInput="{{CustomerEntityOne.email}}" selector="{{AdminCustomerAccountInformationSection.email}}" stepKey="fillEmail"/> - <click selector="{{AdminCustomerMainActionsSection.saveButton}}" stepKey="saveCustomer"/> -- <waitForElementNotVisible selector="div [data-role='spinner']" time="10" stepKey="waitForSpinner1"/> - <seeElement selector="{{AdminCustomerMessagesSection.successMessage}}" stepKey="assertSuccessMessage"/> -+ <magentoCLI stepKey="reindex" command="indexer:reindex"/> -+ <reloadPage stepKey="reloadPage"/> -+ <waitForPageLoad stepKey="waitForLoad2"/> - <click selector="{{AdminCustomerFiltersSection.filtersButton}}" stepKey="openFilter"/> - <fillField userInput="{{CustomerEntityOne.email}}" selector="{{AdminCustomerFiltersSection.emailInput}}" stepKey="filterEmail"/> - <click selector="{{AdminCustomerFiltersSection.apply}}" stepKey="applyFilter"/> -- <waitForElementNotVisible selector="div [data-role='spinner']" time="10" stepKey="waitForSpinner2"/> - <see userInput="{{CustomerEntityOne.firstname}}" selector="{{AdminCustomerGridSection.customerGrid}}" stepKey="assertFirstName"/> - <see userInput="{{CustomerEntityOne.lastname}}" selector="{{AdminCustomerGridSection.customerGrid}}" stepKey="assertLastName"/> - <see userInput="{{CustomerEntityOne.email}}" selector="{{AdminCustomerGridSection.customerGrid}}" stepKey="assertEmail"/> -diff --git a/app/code/Magento/Customer/Test/Mftf/Test/AdminCreateCustomerWithCountryPolandTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/AdminCreateCustomerWithCountryPolandTest.xml -new file mode 100644 -index 00000000000..cbc8b89d3f2 ---- /dev/null -+++ b/app/code/Magento/Customer/Test/Mftf/Test/AdminCreateCustomerWithCountryPolandTest.xml -@@ -0,0 +1,92 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> -+ <test name="AdminCreateCustomerWithCountryPolandTest"> -+ <annotations> -+ <stories value="Create customer"/> -+ <title value="Create customer, from Poland"/> -+ <description value="Login as admin and create customer with Poland address"/> -+ <severity value="CRITICAL"/> -+ <testCaseId value="MC-5311"/> -+ <group value="mtf_migrated"/> -+ </annotations> -+ -+ <before> -+ <actionGroup ref="LoginAsAdmin" stepKey="loginToAdminPanel"/> -+ <createData entity="Simple_Customer_Without_Address" stepKey="createCustomer"/> -+ </before> -+ <after> -+ <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> -+ <actionGroup ref="logout" stepKey="logout"/> -+ </after> -+ -+ <!--Filter the created customer From grid--> -+ <actionGroup ref="AdminFilterCustomerByEmail" stepKey="filterTheCustomerByEmail"> -+ <argument name="email" value="$$createCustomer.email$$"/> -+ </actionGroup> -+ <click selector="{{AdminCustomerGridSection.firstRowEditLink}}" stepKey="clickOnEditButton"/> -+ <waitForPageLoad stepKey="waitForCustomerEditPageToLoad"/> -+ -+ <!--Add the Address --> -+ <click selector="{{AdminEditCustomerAddressesSection.addresses}}" stepKey="selectAddress"/> -+ <waitForPageLoad stepKey="waitForAddressPageToLoad"/> -+ <click selector="{{AdminEditCustomerAddressesSection.addNewAddress}}" stepKey="ClickOnAddNewAddressButton"/> -+ <waitForPageLoad stepKey="waitForNewAddressPageToLoad"/> -+ <checkOption selector="{{AdminCustomerAddressesSection.defaultBillingAddress}}" stepKey="EnableDefaultBillingAddress"/> -+ <fillField selector="{{AdminEditCustomerAddressesSection.streetAddress}}" userInput="{{PolandAddress.street}}" stepKey="fillStreetAddress"/> -+ <fillField selector="{{AdminEditCustomerAddressesSection.city}}" userInput="{{PolandAddress.city}}" stepKey="fillCity"/> -+ <scrollTo selector="{{AdminEditCustomerAddressesSection.phone}}" x="0" y="-80" stepKey="scrollToPhone"/> -+ <selectOption selector="{{AdminEditCustomerAddressesSection.country}}" userInput="{{PolandAddress.country}}" stepKey="fillCountry"/> -+ <fillField selector="{{AdminEditCustomerAddressesSection.zipCode}}" userInput="{{PolandAddress.postcode}}" stepKey="fillPostCode"/> -+ <fillField selector="{{AdminEditCustomerAddressesSection.phone}}" userInput="{{PolandAddress.telephone}}" stepKey="fillPhoneNumber"/> -+ <scrollToTopOfPage stepKey="scrollToTopOfPage"/> -+ <click selector="{{AdminEditCustomerAddressesSection.save}}" stepKey="clickOnSaveButton"/> -+ <waitForPageLoad stepKey="waitForPageToBeSaved"/> -+ <click selector="{{AdminCustomerMainActionsSection.saveButton}}" stepKey="saveCustomer"/> -+ <seeElement selector="{{AdminCustomerMessagesSection.successMessage}}" stepKey="assertSuccessMessage"/> -+ -+ <!-- Assert Customer in grid --> -+ <actionGroup ref="AdminFilterCustomerByEmail" stepKey="filterTheCustomerByEmail1"> -+ <argument name="email" value="$$createCustomer.email$$"/> -+ </actionGroup> -+ <waitForPageLoad stepKey="waitForPageToLoad"/> -+ <see userInput="$$createCustomer.firstname$$" selector="{{AdminCustomerGridSection.customerGrid}}" stepKey="assertFirstName"/> -+ <see userInput="$$createCustomer.lastname$$" selector="{{AdminCustomerGridSection.customerGrid}}" stepKey="assertLastName"/> -+ <see userInput="$$createCustomer.email$$" selector="{{AdminCustomerGridSection.customerGrid}}" stepKey="assertEmail"/> -+ <see userInput="{{PolandAddress.country}}" selector="{{AdminCustomerGridSection.customerGrid}}" stepKey="assertCountry"/> -+ <see userInput="{{PolandAddress.postcode}}" selector="{{AdminCustomerGridSection.customerGrid}}" stepKey="assertPostCode"/> -+ <see userInput="{{PolandAddress.telephone}}" selector="{{AdminCustomerGridSection.customerGrid}}" stepKey="assertPhoneNumber"/> -+ -+ <!--Assert Customer Form --> -+ <click selector="{{AdminCustomerGridSection.firstRowEditLink}}" stepKey="clickOnEditButton1"/> -+ <waitForPageLoad stepKey="waitForCustomerEditPageToLoad1"/> -+ <click selector="{{AdminCustomerAccountInformationSection.accountInformationButton}}" stepKey="clickOnAccountInformation"/> -+ <waitForPageLoad stepKey="waitForCustomerInformationPageToLoad"/> -+ <seeInField selector="{{AdminCustomerAccountInformationSection.firstName}}" userInput="$$createCustomer.firstname$$" stepKey="seeCustomerFirstName"/> -+ <seeInField selector="{{AdminCustomerAccountInformationSection.lastName}}" userInput="$$createCustomer.lastname$$" stepKey="seeCustomerLastName"/> -+ <seeInField selector="{{AdminCustomerAccountInformationSection.email}}" userInput="$$createCustomer.email$$" stepKey="seeCustomerEmail"/> -+ <click selector="{{AdminCustomerAccountInformationSection.addressesButton}}" stepKey="clickOnAddressButton"/> -+ <waitForPageLoad stepKey="waitForAddressGridToLoad"/> -+ <see selector="{{AdminCustomerAddressesDefaultBillingSection.addressDetails}}" userInput="$$createCustomer.firstname$$" stepKey="seeAFirstNameInDefaultAddressSection"/> -+ <see selector="{{AdminCustomerAddressesDefaultBillingSection.addressDetails}}" userInput="$$createCustomer.lastname$$" stepKey="seeLastNameInDefaultAddressSection"/> -+ <see selector="{{AdminCustomerAddressesDefaultBillingSection.addressDetails}}" userInput="{{PolandAddress.street}}" stepKey="seeStreetInDefaultAddressSection"/> -+ <see selector="{{AdminCustomerAddressesDefaultBillingSection.addressDetails}}" userInput="{{PolandAddress.city}}" stepKey="seeLCityInDefaultAddressSection"/> -+ <see selector="{{AdminCustomerAddressesDefaultBillingSection.addressDetails}}" userInput="{{PolandAddress.country}}" stepKey="seeCountrynDefaultAddressSection"/> -+ <see selector="{{AdminCustomerAddressesDefaultBillingSection.addressDetails}}" userInput="{{PolandAddress.postcode}}" stepKey="seePostCodeInDefaultAddressSection"/> -+ <see selector="{{AdminCustomerAddressesDefaultBillingSection.addressDetails}}" userInput="{{PolandAddress.telephone}}" stepKey="seePhoneNumberInDefaultAddressSection"/> -+ -+ <!--Assert Customer Address Grid --> -+ <see selector="{{AdminCustomerAddressesGridSection.customerAddressGrid}}" userInput="{{PolandAddress.street}}" stepKey="seeStreetAddress"/> -+ <see selector="{{AdminCustomerAddressesGridSection.customerAddressGrid}}" userInput="{{PolandAddress.city}}" stepKey="seeCity"/> -+ <see selector="{{AdminCustomerAddressesGridSection.customerAddressGrid}}" userInput="{{PolandAddress.country}}" stepKey="seeCountry"/> -+ <see selector="{{AdminCustomerAddressesGridSection.customerAddressGrid}}" userInput="{{PolandAddress.postcode}}" stepKey="seePostCode"/> -+ <see selector="{{AdminCustomerAddressesGridSection.customerAddressGrid}}" userInput="{{PolandAddress.telephone}}" stepKey="seePhoneNumber"/> -+ </test> -+</tests> -diff --git a/app/code/Magento/Customer/Test/Mftf/Test/AdminCreateCustomerWithCountryUSATest.xml b/app/code/Magento/Customer/Test/Mftf/Test/AdminCreateCustomerWithCountryUSATest.xml -new file mode 100644 -index 00000000000..43f2aa7f8de ---- /dev/null -+++ b/app/code/Magento/Customer/Test/Mftf/Test/AdminCreateCustomerWithCountryUSATest.xml -@@ -0,0 +1,95 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> -+ <test name="AdminCreateCustomerWithCountryUSATest"> -+ <annotations> -+ <stories value="Create customer"/> -+ <title value="Create customer, from USA"/> -+ <description value="Login as admin and create customer with USA address"/> -+ <severity value="CRITICAL"/> -+ <testCaseId value="MC-5309"/> -+ <group value="mtf_migrated"/> -+ </annotations> -+ -+ <before> -+ <actionGroup ref="LoginAsAdmin" stepKey="loginToAdminPanel"/> -+ <createData entity="Simple_Customer_Without_Address" stepKey="createCustomer"/> -+ </before> -+ <after> -+ <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> -+ <actionGroup ref="logout" stepKey="logout"/> -+ </after> -+ -+ <!--Filter the customer From grid--> -+ <actionGroup ref="AdminFilterCustomerByEmail" stepKey="filterTheCustomerByEmail"> -+ <argument name="email" value="$$createCustomer.email$$"/> -+ </actionGroup> -+ <click selector="{{AdminCustomerGridSection.firstRowEditLink}}" stepKey="clickOnEditButton"/> -+ <waitForPageLoad stepKey="waitForCustomerEditPageToLoad"/> -+ -+ <!-- Add the Address --> -+ <click selector="{{AdminEditCustomerAddressesSection.addresses}}" stepKey="selectAddress"/> -+ <waitForPageLoad stepKey="waitForAddressPageToLoad"/> -+ <click selector="{{AdminEditCustomerAddressesSection.addNewAddress}}" stepKey="ClickOnAddNewAddressButton"/> -+ <waitForPageLoad stepKey="waitForNewAddressPageToLoad"/> -+ <checkOption selector="{{AdminCustomerAddressesSection.defaultBillingAddress}}" stepKey="EnableDefaultBillingAddress"/> -+ <fillField selector="{{AdminEditCustomerAddressesSection.streetAddress}}" userInput="{{US_Address_CA.street}}" stepKey="fillStreetAddress"/> -+ <fillField selector="{{AdminEditCustomerAddressesSection.city}}" userInput="{{US_Address_CA.city}}" stepKey="fillCity"/> -+ <scrollTo selector="{{AdminEditCustomerAddressesSection.phone}}" x="0" y="-80" stepKey="scrollToPhone"/> -+ <selectOption selector="{{AdminEditCustomerAddressesSection.country}}" userInput="{{US_Address_CA.country}}" stepKey="fillCountry"/> -+ <selectOption selector="{{AdminEditCustomerAddressesSection.state}}" userInput="{{US_Address_CA.state}}" stepKey="fillState"/> -+ <fillField selector="{{AdminEditCustomerAddressesSection.zipCode}}" userInput="{{US_Address_CA.postcode}}" stepKey="fillPostCode"/> -+ <fillField selector="{{AdminEditCustomerAddressesSection.phone}}" userInput="{{US_Address_CA.telephone}}" stepKey="fillPhoneNumber"/> -+ <scrollToTopOfPage stepKey="scrollToTopOfPage"/> -+ <click selector="{{AdminEditCustomerAddressesSection.save}}" stepKey="clickOnSaveButton"/> -+ <waitForPageLoad stepKey="waitForPageToBeSaved"/> -+ <click selector="{{AdminCustomerMainActionsSection.saveButton}}" stepKey="saveCustomer"/> -+ <seeElement selector="{{AdminCustomerMessagesSection.successMessage}}" stepKey="assertSuccessMessage"/> -+ -+ <!-- Assert Customer in grid --> -+ <actionGroup ref="AdminFilterCustomerByEmail" stepKey="filterTheCustomerByEmail1"> -+ <argument name="email" value="$$createCustomer.email$$"/> -+ </actionGroup> -+ <waitForPageLoad stepKey="waitForPageToLoad"/> -+ <see userInput="$$createCustomer.firstname$$" selector="{{AdminCustomerGridSection.customerGrid}}" stepKey="assertFirstName"/> -+ <see userInput="$$createCustomer.lastname$$" selector="{{AdminCustomerGridSection.customerGrid}}" stepKey="assertLastName"/> -+ <see userInput="$$createCustomer.email$$" selector="{{AdminCustomerGridSection.customerGrid}}" stepKey="assertEmail"/> -+ <see userInput="{{US_Address_CA.state}}" selector="{{AdminCustomerGridSection.customerGrid}}" stepKey="assertState"/> -+ <see userInput="{{US_Address_CA.country}}" selector="{{AdminCustomerGridSection.customerGrid}}" stepKey="assertCountry"/> -+ <see userInput="{{US_Address_CA.postcode}}" selector="{{AdminCustomerGridSection.customerGrid}}" stepKey="assertPostCode"/> -+ <see userInput="{{US_Address_CA.telephone}}" selector="{{AdminCustomerGridSection.customerGrid}}" stepKey="assertPhoneNumber"/> -+ -+ <!--Assert Customer Form --> -+ <click selector="{{AdminCustomerGridSection.firstRowEditLink}}" stepKey="clickOnEditButton1"/> -+ <waitForPageLoad stepKey="waitForCustomerEditPageToLoad1"/> -+ <click selector="{{AdminCustomerAccountInformationSection.accountInformationButton}}" stepKey="clickOnAccountInformation"/> -+ <waitForPageLoad stepKey="waitForCustomerInformationPageToLoad"/> -+ <seeInField selector="{{AdminCustomerAccountInformationSection.firstName}}" userInput="$$createCustomer.firstname$$" stepKey="seeCustomerFirstName"/> -+ <seeInField selector="{{AdminCustomerAccountInformationSection.lastName}}" userInput="$$createCustomer.lastname$$" stepKey="seeCustomerLastName"/> -+ <seeInField selector="{{AdminCustomerAccountInformationSection.email}}" userInput="$$createCustomer.email$$" stepKey="seeCustomerEmail"/> -+ <click selector="{{AdminCustomerAccountInformationSection.addressesButton}}" stepKey="clickOnAddressButton"/> -+ <waitForPageLoad stepKey="waitForAddressGridToLoad"/> -+ <see selector="{{AdminCustomerAddressesDefaultBillingSection.addressDetails}}" userInput="$$createCustomer.firstname$$" stepKey="seeAFirstNameInDefaultAddressSection"/> -+ <see selector="{{AdminCustomerAddressesDefaultBillingSection.addressDetails}}" userInput="$$createCustomer.lastname$$" stepKey="seeLastNameInDefaultAddressSection"/> -+ <see selector="{{AdminCustomerAddressesDefaultBillingSection.addressDetails}}" userInput="{{US_Address_CA.street}}" stepKey="seeStreetInDefaultAddressSection"/> -+ <see selector="{{AdminCustomerAddressesDefaultBillingSection.addressDetails}}" userInput="{{US_Address_CA.city}}" stepKey="seeLCityInDefaultAddressSection"/> -+ <see selector="{{AdminCustomerAddressesDefaultBillingSection.addressDetails}}" userInput="{{US_Address_CA.country}}" stepKey="seeCountrynDefaultAddressSection"/> -+ <see selector="{{AdminCustomerAddressesDefaultBillingSection.addressDetails}}" userInput="{{US_Address_CA.postcode}}" stepKey="seePostCodeInDefaultAddressSection"/> -+ <see selector="{{AdminCustomerAddressesDefaultBillingSection.addressDetails}}" userInput="{{US_Address_CA.telephone}}" stepKey="seePhoneNumberInDefaultAddressSection"/> -+ -+ <!--Assert Customer Address Grid --> -+ <see selector="{{AdminCustomerAddressesGridSection.customerAddressGrid}}" userInput="{{US_Address_CA.street}}" stepKey="seeStreetAddress"/> -+ <see selector="{{AdminCustomerAddressesGridSection.customerAddressGrid}}" userInput="{{US_Address_CA.city}}" stepKey="seeCity"/> -+ <see selector="{{AdminCustomerAddressesGridSection.customerAddressGrid}}" userInput="{{US_Address_CA.country}}" stepKey="seeCountry"/> -+ <see selector="{{AdminCustomerAddressesGridSection.customerAddressGrid}}" userInput="{{US_Address_CA.state}}" stepKey="seeState"/> -+ <see selector="{{AdminCustomerAddressesGridSection.customerAddressGrid}}" userInput="{{US_Address_CA.postcode}}" stepKey="seePostCode"/> -+ <see selector="{{AdminCustomerAddressesGridSection.customerAddressGrid}}" userInput="{{US_Address_CA.telephone}}" stepKey="seePhoneNumber"/> -+ </test> -+</tests> -diff --git a/app/code/Magento/Customer/Test/Mftf/Test/AdminCreateCustomerWithCustomGroupTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/AdminCreateCustomerWithCustomGroupTest.xml -new file mode 100644 -index 00000000000..872da149ed0 ---- /dev/null -+++ b/app/code/Magento/Customer/Test/Mftf/Test/AdminCreateCustomerWithCustomGroupTest.xml -@@ -0,0 +1,65 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> -+ <test name="AdminCreateCustomerWithCustomGroupTest"> -+ <annotations> -+ <stories value="Create customer"/> -+ <title value="Create customer, with custom group"/> -+ <description value="Login as admin and create customer with custom group"/> -+ <severity value="CRITICAL"/> -+ <testCaseId value="MC-5313"/> -+ <group value="mtf_migrated"/> -+ </annotations> -+ -+ <before> -+ <createData entity="CustomCustomerGroup" stepKey="customerGroup" /> -+ <actionGroup ref="LoginAsAdmin" stepKey="login"/> -+ </before> -+ <after> -+ <actionGroup ref="DeleteCustomerByEmailActionGroup" stepKey="deleteCustomer"> -+ <argument name="email" value="{{CustomerEntityOne.email}}" /> -+ </actionGroup> -+ <deleteData createDataKey="customerGroup" stepKey="deleteCustomerGroup"/> -+ <actionGroup ref="logout" stepKey="logout"/> -+ </after> -+ -+ <!--Open New Customer Page --> -+ <amOnPage url="{{AdminNewCustomerPage.url}}" stepKey="navigateToNewCustomerPage"/> -+ <waitForPageLoad stepKey="waitToCustomerPageLoad"/> -+ <selectOption selector="{{AdminCustomerAccountInformationSection.group}}" userInput="$$customerGroup.code$$" stepKey="fillCustomerGroup"/> -+ <fillField userInput="{{CustomerEntityOne.firstname}}" selector="{{AdminCustomerAccountInformationSection.firstName}}" stepKey="fillFirstName"/> -+ <fillField userInput="{{CustomerEntityOne.lastname}}" selector="{{AdminCustomerAccountInformationSection.lastName}}" stepKey="fillLastName"/> -+ <fillField userInput="{{CustomerEntityOne.email}}" selector="{{AdminCustomerAccountInformationSection.email}}" stepKey="fillEmail"/> -+ <click selector="{{AdminCustomerMainActionsSection.saveButton}}" stepKey="saveCustomer"/> -+ <seeElement selector="{{AdminCustomerMessagesSection.successMessage}}" stepKey="assertSuccessMessage"/> -+ <magentoCLI stepKey="flushMagentoCache" command="cache:flush" /> -+ <reloadPage stepKey="reloadPage"/> -+ -+ <!--Verify Customer in grid --> -+ <actionGroup ref="AdminFilterCustomerByEmail" stepKey="filterTheCustomerByEmail1"> -+ <argument name="email" value="{{CustomerEntityOne.email}}"/> -+ </actionGroup> -+ <waitForPageLoad stepKey="waitForCustomerPageToLoad"/> -+ <see selector="{{AdminCustomerGridSection.customerGrid}}" userInput="$$customerGroup.code$$" stepKey="assertGroup"/> -+ <see userInput="{{CustomerEntityOne.firstname}}" selector="{{AdminCustomerGridSection.customerGrid}}" stepKey="assertFirstName"/> -+ <see userInput="{{CustomerEntityOne.lastname}}" selector="{{AdminCustomerGridSection.customerGrid}}" stepKey="assertLastName"/> -+ <see userInput="{{CustomerEntityOne.email}}" selector="{{AdminCustomerGridSection.customerGrid}}" stepKey="assertEmail"/> -+ -+ <!--Assert Customer Form --> -+ <click selector="{{AdminCustomerGridSection.firstRowEditLink}}" stepKey="clickOnEditButton1"/> -+ <waitForPageLoad stepKey="waitForCustomerEditPageToLoad1"/> -+ <click selector="{{AdminCustomerAccountInformationSection.accountInformationButton}}" stepKey="clickOnAccountInformation"/> -+ <waitForPageLoad stepKey="waitForCustomerInformationPageToLoad"/> -+ <see selector="{{AdminCustomerAccountInformationSection.groupIdValue}}" userInput="$$customerGroup.code$$" stepKey="seeCustomerGroup1"/> -+ <seeInField selector="{{AdminCustomerAccountInformationSection.firstName}}" userInput="{{CustomerEntityOne.firstname}}" stepKey="seeCustomerFirstName"/> -+ <seeInField selector="{{AdminCustomerAccountInformationSection.lastName}}" userInput="{{CustomerEntityOne.lastname}}" stepKey="seeCustomerLastName"/> -+ <seeInField selector="{{AdminCustomerAccountInformationSection.email}}" userInput="{{CustomerEntityOne.email}}" stepKey="seeCustomerEmail"/> -+ </test> -+</tests> -diff --git a/app/code/Magento/Customer/Test/Mftf/Test/AdminCreateCustomerWithPrefixTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/AdminCreateCustomerWithPrefixTest.xml -new file mode 100644 -index 00000000000..1b901a7b3e1 ---- /dev/null -+++ b/app/code/Magento/Customer/Test/Mftf/Test/AdminCreateCustomerWithPrefixTest.xml -@@ -0,0 +1,70 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> -+ <test name="AdminCreateCustomerWithPrefixTest"> -+ <annotations> -+ <stories value="Create customer"/> -+ <title value="Create customer, with prefix"/> -+ <description value="Login as admin and create a customer with name prefix and suffix"/> -+ <severity value="CRITICAL"/> -+ <testCaseId value="MC-5308"/> -+ <group value="mtf_migrated"/> -+ </annotations> -+ -+ <before> -+ <actionGroup ref="LoginAsAdmin" stepKey="loginToAdminPanel"/> -+ </before> -+ <after> -+ <actionGroup ref="DeleteCustomerByEmailActionGroup" stepKey="deleteCustomer"> -+ <argument name="email" value="{{CustomerEntityOne.email}}"/> -+ </actionGroup> -+ <actionGroup ref="logout" stepKey="logout"/> -+ </after> -+ -+ <!--Open New Customer Page and create a customer with Prefix and Suffix--> -+ <amOnPage url="{{AdminNewCustomerPage.url}}" stepKey="navigateToNewCustomerPage"/> -+ <waitForPageLoad stepKey="waitToCustomerPageLoad"/> -+ <selectOption selector="{{AdminCustomerAccountInformationSection.group}}" userInput="Wholesale" stepKey="fillCustomerGroup"/> -+ <fillField selector="{{AdminCustomerAccountInformationSection.namePrefix}}" userInput="{{CustomerEntityOne.prefix}}" stepKey="fillNamePrefix"/> -+ <fillField userInput="{{CustomerEntityOne.firstname}}" selector="{{AdminCustomerAccountInformationSection.firstName}}" stepKey="fillFirstName"/> -+ <fillField userInput="{{CustomerEntityOne.lastname}}" selector="{{AdminCustomerAccountInformationSection.lastName}}" stepKey="fillLastName"/> -+ <fillField selector="{{AdminCustomerAccountInformationSection.nameSuffix}}" userInput="{{CustomerEntityOne.suffix}}" stepKey="fillNameSuffix"/> -+ <fillField userInput="{{CustomerEntityOne.email}}" selector="{{AdminCustomerAccountInformationSection.email}}" stepKey="fillEmail"/> -+ <fillField userInput="{{CustomerEntityOne.dob}}" selector="{{AdminCustomerAccountInformationSection.dateOfBirth}}" stepKey="fillDateOfBirth"/> -+ <selectOption userInput="Male" selector="{{AdminCustomerAccountInformationSection.gender}}" stepKey="fillGender"/> -+ <click selector="{{AdminCustomerMainActionsSection.saveButton}}" stepKey="saveCustomer"/> -+ <seeElement selector="{{AdminCustomerMessagesSection.successMessage}}" stepKey="assertSuccessMessage"/> -+ <reloadPage stepKey="reloadPage"/> -+ -+ <!--Filter the customer From grid--> -+ <actionGroup ref="AdminFilterCustomerByEmail" stepKey="filterTheCustomerByEmail"> -+ <argument name="email" value="{{CustomerEntityOne.email}}"/> -+ </actionGroup> -+ <waitForPageLoad stepKey="waitForPageToLoad"/> -+ -+ <!-- Assert Customer in grid --> -+ <see userInput="{{CustomerEntityOne.prefix}} {{CustomerEntityOne.firstname}} {{CustomerEntityOne.lastname}} {{CustomerEntityOne.suffix}}" selector="{{AdminCustomerGridSection.customerGrid}}" stepKey="assertFirstName"/> -+ <see userInput="Wholesale" selector="{{AdminCustomerGridSection.customerGrid}}" stepKey="assertCustomerGroup"/> -+ <see userInput="{{CustomerEntityOne.email}}" selector="{{AdminCustomerGridSection.customerGrid}}" stepKey="assertEmail"/> -+ <see userInput="Jan 1, 1970" selector="{{AdminCustomerGridSection.customerGrid}}" stepKey="assertDateOfBirth"/> -+ <see userInput="Male" selector="{{AdminCustomerGridSection.customerGrid}}" stepKey="assertGender"/> -+ -+ <!--Assert Customer Form --> -+ <click selector="{{AdminCustomerGridSection.firstRowEditLink}}" stepKey="clickOnEditButton1"/> -+ <waitForPageLoad stepKey="waitForCustomerEditPageToLoad1"/> -+ <click selector="{{AdminCustomerAccountInformationSection.accountInformationButton}}" stepKey="clickOnAccountInformation"/> -+ <waitForPageLoad stepKey="waitForCustomerInformationPageToLoad"/> -+ <seeInField selector="{{AdminCustomerAccountInformationSection.namePrefix}}" userInput="{{CustomerEntityOne.prefix}}" stepKey="seeCustomerNamePrefix"/> -+ <seeInField selector="{{AdminCustomerAccountInformationSection.firstName}}" userInput="{{CustomerEntityOne.firstname}}" stepKey="seeCustomerFirstName"/> -+ <seeInField selector="{{AdminCustomerAccountInformationSection.lastName}}" userInput="{{CustomerEntityOne.lastname}}" stepKey="seeCustomerLastName"/> -+ <seeInField selector="{{AdminCustomerAccountInformationSection.nameSuffix}}" userInput="{{CustomerEntityOne.suffix}}" stepKey="seeCustomerNameSuffix"/> -+ <seeInField selector="{{AdminCustomerAccountInformationSection.email}}" userInput="{{CustomerEntityOne.email}}" stepKey="seeCustomerEmail"/> -+ </test> -+</tests> -diff --git a/app/code/Magento/Customer/Test/Mftf/Test/AdminCreateCustomerWithoutAddressTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/AdminCreateCustomerWithoutAddressTest.xml -new file mode 100644 -index 00000000000..fe4bb3ee59e ---- /dev/null -+++ b/app/code/Magento/Customer/Test/Mftf/Test/AdminCreateCustomerWithoutAddressTest.xml -@@ -0,0 +1,61 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> -+ <test name="AdminCreateCustomerWithoutAddressTest"> -+ <annotations> -+ <stories value="Create customer"/> -+ <title value="Create customer, without address"/> -+ <description value="Login as admin and create a customer without address"/> -+ <severity value="CRITICAL"/> -+ <testCaseId value="MC-5307"/> -+ <group value="mtf_migrated"/> -+ </annotations> -+ -+ <before> -+ <actionGroup ref="LoginAsAdmin" stepKey="loginToAdminPanel"/> -+ </before> -+ <after> -+ <actionGroup ref="DeleteCustomerByEmailActionGroup" stepKey="deleteCustomer"> -+ <argument name="email" value="{{CustomerEntityOne.email}}"/> -+ </actionGroup> -+ <actionGroup ref="logout" stepKey="logout"/> -+ </after> -+ -+ <!--Open New Customer Page --> -+ <amOnPage url="{{AdminNewCustomerPage.url}}" stepKey="navigateToNewCustomerPage"/> -+ <waitForPageLoad stepKey="waitToCustomerPageLoad"/> -+ <fillField userInput="{{CustomerEntityOne.firstname}}" selector="{{AdminCustomerAccountInformationSection.firstName}}" stepKey="fillFirstName"/> -+ <fillField userInput="{{CustomerEntityOne.lastname}}" selector="{{AdminCustomerAccountInformationSection.lastName}}" stepKey="fillLastName"/> -+ <fillField userInput="{{CustomerEntityOne.email}}" selector="{{AdminCustomerAccountInformationSection.email}}" stepKey="fillEmail"/> -+ <click selector="{{AdminCustomerMainActionsSection.saveButton}}" stepKey="saveCustomer"/> -+ <seeElement selector="{{AdminCustomerMessagesSection.successMessage}}" stepKey="assertSuccessMessage"/> -+ <reloadPage stepKey="reloadPage"/> -+ -+ <!--Filter the customer From grid--> -+ <actionGroup ref="AdminFilterCustomerByEmail" stepKey="filterTheCustomerByEmail"> -+ <argument name="email" value="{{CustomerEntityOne.email}}"/> -+ </actionGroup> -+ <waitForPageLoad stepKey="waitForPageToLoad"/> -+ -+ <!-- Assert Customer in grid --> -+ <see userInput="{{CustomerEntityOne.firstname}}" selector="{{AdminCustomerGridSection.customerGrid}}" stepKey="assertFirstName"/> -+ <see userInput="{{CustomerEntityOne.lastname}}" selector="{{AdminCustomerGridSection.customerGrid}}" stepKey="assertLastName"/> -+ <see userInput="{{CustomerEntityOne.email}}" selector="{{AdminCustomerGridSection.customerGrid}}" stepKey="assertEmail"/> -+ -+ <!--Assert Customer Form --> -+ <click selector="{{AdminCustomerGridSection.firstRowEditLink}}" stepKey="clickOnEditButton1"/> -+ <waitForPageLoad stepKey="waitForCustomerEditPageToLoad1"/> -+ <click selector="{{AdminCustomerAccountInformationSection.accountInformationButton}}" stepKey="clickOnAccountInformation"/> -+ <waitForPageLoad stepKey="waitForCustomerInformationPageToLoad"/> -+ <seeInField selector="{{AdminCustomerAccountInformationSection.firstName}}" userInput="{{CustomerEntityOne.firstname}}" stepKey="seeCustomerFirstName"/> -+ <seeInField selector="{{AdminCustomerAccountInformationSection.lastName}}" userInput="{{CustomerEntityOne.lastname}}" stepKey="seeCustomerLastName"/> -+ <seeInField selector="{{AdminCustomerAccountInformationSection.email}}" userInput="{{CustomerEntityOne.email}}" stepKey="seeCustomerEmail"/> -+ </test> -+</tests> -diff --git a/app/code/Magento/Customer/Test/Mftf/Test/AdminCreateNewCustomerOnStorefrontSignupNewsletterTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/AdminCreateNewCustomerOnStorefrontSignupNewsletterTest.xml -new file mode 100644 -index 00000000000..22ad60ff5de ---- /dev/null -+++ b/app/code/Magento/Customer/Test/Mftf/Test/AdminCreateNewCustomerOnStorefrontSignupNewsletterTest.xml -@@ -0,0 +1,54 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> -+ <test name="AdminCreateNewCustomerOnStorefrontSignupNewsletterTest"> -+ <annotations> -+ <stories value="Create New Customer"/> -+ <title value="Create New Customer on Storefront, Sign-up Newsletter"/> -+ <description value="Test log in to Create New Customer and Create New Customer on Storefront, Sign-up Newsletter"/> -+ <testCaseId value="MC-10914"/> -+ <severity value="CRITICAL"/> -+ <group value="customer"/> -+ <group value="mtf_migrated"/> -+ </annotations> -+ -+ <before> -+ <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> -+ </before> -+ <after> -+ <actionGroup ref="DeleteCustomerByEmailActionGroup" stepKey="deleteNewUser"> -+ <argument name="email" value="{{CustomerEntityOne.email}}"/> -+ </actionGroup> -+ <actionGroup ref="logout" stepKey="logout"/> -+ </after> -+ -+ <!--Create new customer on storefront and signup news letter--> -+ <actionGroup ref="StorefrontCreateCustomerSignedUpNewsletterActionGroup" stepKey="createCustomer"> -+ <argument name="customer" value="CustomerEntityOne" /> -+ </actionGroup> -+ -+ <!--Assert verify created new customer in grid--> -+ <amOnPage url="{{AdminCustomerPage.url}}" stepKey="navigateToCustomers"/> -+ <waitForPageLoad stepKey="waitForNavigateToCustomersPageLoad"/> -+ <click selector="{{AdminCustomerFiltersSection.filtersButton}}" stepKey="clickFilterButton"/> -+ <fillField userInput="{{CustomerEntityOne.email}}" selector="{{AdminCustomerFiltersSection.emailInput}}" stepKey="filterEmail"/> -+ <click selector="{{AdminCustomerFiltersSection.apply}}" stepKey="clickApplyFilter"/> -+ <see selector="{{AdminCustomerGridSection.customerGrid}}" userInput="{{CustomerEntityOne.firstname}}" stepKey="seeAssertCustomerFirstNameInGrid"/> -+ <see selector="{{AdminCustomerGridSection.customerGrid}}" userInput="{{CustomerEntityOne.lastname}}" stepKey="seeAssertCustomerLastNameInGrid"/> -+ <see selector="{{AdminCustomerGridSection.customerGrid}}" userInput="{{CustomerEntityOne.email}}" stepKey="seeAssertCustomerEmailInGrid"/> -+ -+ <!--Assert verify created new customer is subscribed to newsletter--> -+ <click selector="{{AdminCustomerGridSection.firstRowEditLink}}" stepKey="clickFirstRowEditLink"/> -+ <waitForPageLoad stepKey="waitForEditLinkLoad"/> -+ <click selector="{{AdminEditCustomerInformationSection.newsLetter}}" stepKey="clickNewsLetter"/> -+ <waitForPageLoad stepKey="waitForNewsletterTabToOpen"/> -+ <seeCheckboxIsChecked selector="{{AdminEditCustomerNewsletterSection.subscribedToNewsletter}}" stepKey="seeAssertSubscribedToNewsletterCheckboxIsChecked"/> -+ </test> -+</tests> -\ No newline at end of file -diff --git a/app/code/Magento/Customer/Test/Mftf/Test/AdminCreateNewCustomerOnStorefrontTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/AdminCreateNewCustomerOnStorefrontTest.xml -new file mode 100644 -index 00000000000..fc65a271a81 ---- /dev/null -+++ b/app/code/Magento/Customer/Test/Mftf/Test/AdminCreateNewCustomerOnStorefrontTest.xml -@@ -0,0 +1,40 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> -+ <test name="AdminCreateNewCustomerOnStorefrontTest"> -+ <annotations> -+ <stories value="Create New Customer"/> -+ <title value="Create New Customer on Storefront"/> -+ <description value="Test log in to Create New Customer and Create New Customer on Storefront"/> -+ <testCaseId value="MC-10915"/> -+ <severity value="CRITICAL"/> -+ <group value="customer"/> -+ <group value="mtf_migrated"/> -+ </annotations> -+ -+ <before> -+ <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> -+ </before> -+ <after> -+ <actionGroup ref="DeleteCustomerByEmailActionGroup" stepKey="deleteNewUser"> -+ <argument name="email" value="{{CustomerEntityOne.email}}"/> -+ </actionGroup> -+ <actionGroup ref="logout" stepKey="logout"/> -+ </after> -+ -+ <!--Create new customer on storefront and perform the asserts--> -+ <actionGroup ref="SignUpNewUserFromStorefrontActionGroup" stepKey="signUpNewUser"> -+ <argument name="customer" value="CustomerEntityOne"/> -+ </actionGroup> -+ <actionGroup ref="AssertSignedUpNewsletterActionGroup" stepKey="assertSignedUpNewsLetter"> -+ <argument name="customer" value="CustomerEntityOne"/> -+ </actionGroup> -+ </test> -+</tests> -\ No newline at end of file -diff --git a/app/code/Magento/Customer/Test/Mftf/Test/AdminCreateNewCustomerTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/AdminCreateNewCustomerTest.xml -new file mode 100644 -index 00000000000..de4ab9ffaa1 ---- /dev/null -+++ b/app/code/Magento/Customer/Test/Mftf/Test/AdminCreateNewCustomerTest.xml -@@ -0,0 +1,54 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> -+ <test name="AdminCreateNewCustomerTest"> -+ <annotations> -+ <stories value="Create customer"/> -+ <title value="Create customer, new via backend"/> -+ <description value="Login as admin and create a new customer"/> -+ <severity value="CRITICAL"/> -+ <testCaseId value="MC-5312"/> -+ <group value="mtf_migrated"/> -+ </annotations> -+ -+ <before> -+ <actionGroup ref="LoginAsAdmin" stepKey="loginToAdminPanel"/> -+ </before> -+ <after> -+ <actionGroup ref="DeleteCustomerByEmailActionGroup" stepKey="deleteCustomer"> -+ <argument name="email" value="{{CustomerEntityOne.email}}"/> -+ </actionGroup> -+ <actionGroup ref="logout" stepKey="logout"/> -+ </after> -+ -+ <!--Open New Customer Page --> -+ <amOnPage url="{{AdminNewCustomerPage.url}}" stepKey="navigateToNewCustomerPage"/> -+ <waitForPageLoad stepKey="waitToCustomerPageLoad"/> -+ <fillField userInput="{{CustomerEntityOne.firstname}}" selector="{{AdminCustomerAccountInformationSection.firstName}}" stepKey="fillFirstName"/> -+ <fillField userInput="{{CustomerEntityOne.lastname}}" selector="{{AdminCustomerAccountInformationSection.lastName}}" stepKey="fillLastName"/> -+ <fillField userInput="{{CustomerEntityOne.email}}" selector="{{AdminCustomerAccountInformationSection.email}}" stepKey="fillEmail"/> -+ <click selector="{{AdminCustomerMainActionsSection.saveButton}}" stepKey="saveCustomer"/> -+ <seeElement selector="{{AdminCustomerMessagesSection.successMessage}}" stepKey="assertSuccessMessage"/> -+ <reloadPage stepKey="reloadPage"/> -+ -+ <!--Filter the customer From grid--> -+ <actionGroup ref="AdminFilterCustomerByEmail" stepKey="filterTheCustomerByEmail"> -+ <argument name="email" value="{{CustomerEntityOne.email}}"/> -+ </actionGroup> -+ <waitForPageLoad stepKey="waitForPageToLoad"/> -+ <click selector="{{AdminCustomerGridSection.firstRowEditLink}}" stepKey="clickOnEditButton1"/> -+ <waitForPageLoad stepKey="waitForCustomerEditPageToLoad"/> -+ -+ <!-- Assert Customer Title --> -+ <click selector="{{AdminCustomerAccountInformationSection.accountInformationButton}}" stepKey="clickOnAccountInformation"/> -+ <waitForPageLoad stepKey="waitForCustomerInformationPageToLoad"/> -+ <see stepKey="seeCustomerTitle" selector="{{AdminEditCustomerInformationSection.customerTitle}}" userInput="{{CustomerEntityOne.firstname}} {{CustomerEntityOne.lastname}} "/> -+ </test> -+</tests> -diff --git a/app/code/Magento/Customer/Test/Mftf/Test/AdminCreateRetailCustomerGroupTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/AdminCreateRetailCustomerGroupTest.xml -new file mode 100644 -index 00000000000..4f1d88ffe99 ---- /dev/null -+++ b/app/code/Magento/Customer/Test/Mftf/Test/AdminCreateRetailCustomerGroupTest.xml -@@ -0,0 +1,71 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> -+ <test name="AdminCreateRetailCustomerGroupTest"> -+ <annotations> -+ <features value="Create retail customer group"/> -+ <stories value="Create customer group"/> -+ <title value="Create retail customer group"/> -+ <description value="Create retail customer group"/> -+ <severity value="CRITICAL"/> -+ <testCaseId value="MC-5301"/> -+ <group value="customer"/> -+ <group value="mtf_migrated"/> -+ </annotations> -+ <before> -+ <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> -+ </before> -+ <after> -+ <actionGroup ref="AdminDeleteCustomerGroupActionGroup" stepKey="deleteCustomerGroup"> -+ <argument name="customerGroupName" value="{{CustomCustomerGroup.code}}"/> -+ </actionGroup> -+ <actionGroup ref="clearFiltersAdminDataGrid" stepKey="clearFilters"/> -+ <actionGroup ref="logout" stepKey="logout"/> -+ </after> -+ <!-- Steps: 1. Log in to backend as admin user. -+ 2. Navigate to Stores > Other Settings > Customer Groups. -+ 3. Start to create new Customer Group. -+ 4. Fill in all data according to data set. Tax Class - "Retail customer" -+ 5. Click "Save Customer Group" button. --> -+ <!-- Assert "You saved the customer group." success message displayed --> -+ <!-- Assert created Customer Group displayed In Grid --> -+ <actionGroup ref="AdminCreateCustomerGroupActionGroup" stepKey="createCustomerGroup"> -+ <argument name="groupName" value="{{CustomCustomerGroup.code}}"/> -+ <argument name="taxClass" value="{{CustomCustomerGroup.tax_class_name}}"/> -+ </actionGroup> -+ <actionGroup ref="AdminAssertCustomerGroupPresentInGrid" stepKey="assertCustomerGroupDisplayedInGrid"> -+ <argument name="customerGroupName" value="{{CustomCustomerGroup.code}}"/> -+ </actionGroup> -+ -+ <!-- 6. Go to Catalog -> Products -> click "Add Product" button -> click "Advanced Pricing" link -> Customer Group Price -> click "Add" button --> -+ <!-- Assert: Customer Group Displayed On Product Form --> -+ <actionGroup ref="AdminAssertCustomerGroupOnProductForm" stepKey="assertCustomerGroupDisplayedOnProductForm"> -+ <argument name="customerGroupName" value="{{CustomCustomerGroup.code}}"/> -+ </actionGroup> -+ -+ <!-- 7. Go to Customers -> All Customers -> click "Add New Customer" button --> -+ <!-- Assert created Customer Group displayed On Customer Form --> -+ <actionGroup ref="AdminAssertCustomerGroupOnCustomerForm" stepKey="assertCustomerGroupDisplayedOnCustomerForm"> -+ <argument name="customerGroupName" value="{{CustomCustomerGroup.code}}"/> -+ </actionGroup> -+ -+ <!-- 8. Go to Marketing - Catalog Price Rule - click "Add New Rule" button --> -+ <!-- Assert created Customer Group displayed On Catalog Price Rule Form --> -+ <actionGroup ref="AdminAssertCustomerGroupOnCatalogPriceRuleForm" stepKey="assertCustomerGroupDisplayedOnCatalogPriceRuleForm"> -+ <argument name="customerGroupName" value="{{CustomCustomerGroup.code}}"/> -+ </actionGroup> -+ -+ <!-- 9. Go to Marketing - Cart Price Rule - click "Add New Rule" button --> -+ <!-- Assert created Customer Group displayed On Cart Price Rule Form --> -+ <actionGroup ref="AdminAssertCustomerGroupOnCartPriceRuleForm" stepKey="assertCustomerGroupDisplayedOnCartPriceRuleForm"> -+ <argument name="customerGroupName" value="{{CustomCustomerGroup.code}}"/> -+ </actionGroup> -+ </test> -+</tests> -diff --git a/app/code/Magento/Customer/Test/Mftf/Test/AdminCreateTaxClassCustomerGroupTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/AdminCreateTaxClassCustomerGroupTest.xml -new file mode 100644 -index 00000000000..7d54ede7c16 ---- /dev/null -+++ b/app/code/Magento/Customer/Test/Mftf/Test/AdminCreateTaxClassCustomerGroupTest.xml -@@ -0,0 +1,58 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> -+ <test name="AdminCreateTaxClassCustomerGroupTest"> -+ <annotations> -+ <features value="Create tax class customer group"/> -+ <stories value="Create customer group"/> -+ <title value="Create tax class customer group"/> -+ <description value="Create tax class customer group"/> -+ <severity value="CRITICAL"/> -+ <testCaseId value="MC-5303"/> -+ <group value="customer"/> -+ <group value="mtf_migrated"/> -+ </annotations> -+ <before> -+ <!-- Create Tax Class "Customer tax class"--> -+ <createData entity="customerTaxClass" stepKey="createCustomerTaxClass"/> -+ <getData entity="customerTaxClass" stepKey="customerTaxClassData"> -+ <requiredEntity createDataKey="createCustomerTaxClass"/> -+ </getData> -+ <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> -+ </before> -+ <after> -+ <actionGroup ref="AdminDeleteCustomerGroupActionGroup" stepKey="deleteCustomerGroup"> -+ <argument name="customerGroupName" value="{{CustomCustomerGroup.code}}"/> -+ </actionGroup> -+ <actionGroup ref="clearFiltersAdminDataGrid" stepKey="clearFilters"/> -+ <deleteData createDataKey="createCustomerTaxClass" stepKey="deleteCustomerTaxClass"/> -+ <actionGroup ref="logout" stepKey="logout"/> -+ </after> -+ <!-- Steps: 1. Log in to backend as admin user. -+ 2. Navigate to Stores > Other Settings > Customer Groups. -+ 3. Start to create new Customer Group. -+ 4. Fill in all data according to data set: Tax Class "Customer tax class" -+ 5. Click "Save Customer Group" button. --> -+ <!-- Assert "You saved the customer group." success message displayed --> -+ <!-- Assert created Customer Group displayed In Grid --> -+ <actionGroup ref="AdminCreateCustomerGroupActionGroup" stepKey="createNewCustomerGroup"> -+ <argument name="groupName" value="{{CustomCustomerGroup.code}}"/> -+ <argument name="taxClass" value="$$customerTaxClassData.class_name$$"/> -+ </actionGroup> -+ <actionGroup ref="AdminAssertCustomerGroupPresentInGrid" stepKey="assertCustomerGroupDisplayedInGrid"> -+ <argument name="customerGroupName" value="{{CustomCustomerGroup.code}}"/> -+ </actionGroup> -+ <!-- 6. Go to Customers -> All Customers -> click "Add New Customer" button --> -+ <!-- Assert created Customer Group displayed On Customer Form --> -+ <actionGroup ref="AdminAssertCustomerGroupOnCustomerForm" stepKey="assertCustomerGroupDisplayedOnCustomerForm"> -+ <argument name="customerGroupName" value="{{CustomCustomerGroup.code}}"/> -+ </actionGroup> -+ </test> -+</tests> -diff --git a/app/code/Magento/Customer/Test/Mftf/Test/AdminCustomersAllCustomersNavigateMenuTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/AdminCustomersAllCustomersNavigateMenuTest.xml -new file mode 100644 -index 00000000000..76e4407675e ---- /dev/null -+++ b/app/code/Magento/Customer/Test/Mftf/Test/AdminCustomersAllCustomersNavigateMenuTest.xml -@@ -0,0 +1,36 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> -+ <test name="AdminCustomersAllCustomersNavigateMenuTest"> -+ <annotations> -+ <features value="Customer"/> -+ <stories value="Menu Navigation"/> -+ <title value="Admin customers all customers navigate menu test"/> -+ <description value="Admin should be able to navigate to Customers > All Customers"/> -+ <severity value="CRITICAL"/> -+ <testCaseId value="MC-14113"/> -+ <group value="menu"/> -+ <group value="mtf_migrated"/> -+ </annotations> -+ <before> -+ <actionGroup ref="LoginAsAdmin" stepKey="LoginAsAdmin"/> -+ </before> -+ <after> -+ <actionGroup ref="logout" stepKey="logout"/> -+ </after> -+ <actionGroup ref="AdminNavigateMenuActionGroup" stepKey="navigateToAllCustomerPage"> -+ <argument name="menuUiId" value="{{AdminMenuCustomers.dataUiId}}"/> -+ <argument name="submenuUiId" value="{{AdminMenuCustomersAllCustomers.dataUiId}}"/> -+ </actionGroup> -+ <actionGroup ref="AdminAssertPageTitleActionGroup" stepKey="seePageTitle"> -+ <argument name="title" value="{{AdminMenuCustomersAllCustomers.pageTitle}}"/> -+ </actionGroup> -+ </test> -+</tests> -diff --git a/app/code/Magento/Customer/Test/Mftf/Test/AdminCustomersCustomerGroupsNavigateMenuTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/AdminCustomersCustomerGroupsNavigateMenuTest.xml -new file mode 100644 -index 00000000000..13a4b1c7143 ---- /dev/null -+++ b/app/code/Magento/Customer/Test/Mftf/Test/AdminCustomersCustomerGroupsNavigateMenuTest.xml -@@ -0,0 +1,36 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> -+ <test name="AdminCustomersCustomerGroupsNavigateMenuTest"> -+ <annotations> -+ <features value="Customer"/> -+ <stories value="Menu Navigation"/> -+ <title value="Admin customers customer groups navigate menu test"/> -+ <description value="Admin should be able to navigate to Customers > Customer Groups"/> -+ <severity value="CRITICAL"/> -+ <testCaseId value="MC-14115"/> -+ <group value="menu"/> -+ <group value="mtf_migrated"/> -+ </annotations> -+ <before> -+ <actionGroup ref="LoginAsAdmin" stepKey="LoginAsAdmin"/> -+ </before> -+ <after> -+ <actionGroup ref="logout" stepKey="logout"/> -+ </after> -+ <actionGroup ref="AdminNavigateMenuActionGroup" stepKey="navigateToCustomerGroupsPage"> -+ <argument name="menuUiId" value="{{AdminMenuCustomers.dataUiId}}"/> -+ <argument name="submenuUiId" value="{{AdminMenuCustomersCustomerGroups.dataUiId}}"/> -+ </actionGroup> -+ <actionGroup ref="AdminAssertPageTitleActionGroup" stepKey="seePageTitle"> -+ <argument name="title" value="{{AdminMenuCustomersCustomerGroups.pageTitle}}"/> -+ </actionGroup> -+ </test> -+</tests> -diff --git a/app/code/Magento/Customer/Test/Mftf/Test/AdminCustomersNowOnlineNavigateMenuTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/AdminCustomersNowOnlineNavigateMenuTest.xml -new file mode 100644 -index 00000000000..e9eb7803e01 ---- /dev/null -+++ b/app/code/Magento/Customer/Test/Mftf/Test/AdminCustomersNowOnlineNavigateMenuTest.xml -@@ -0,0 +1,36 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> -+ <test name="AdminCustomersNowOnlineNavigateMenuTest"> -+ <annotations> -+ <features value="Customer"/> -+ <stories value="Menu Navigation"/> -+ <title value="Admin customers now online navigate menu test"/> -+ <description value="Admin should be able to navigate to Customers > Now Online"/> -+ <severity value="CRITICAL"/> -+ <testCaseId value="MC-14114"/> -+ <group value="menu"/> -+ <group value="mtf_migrated"/> -+ </annotations> -+ <before> -+ <actionGroup ref="LoginAsAdmin" stepKey="LoginAsAdmin"/> -+ </before> -+ <after> -+ <actionGroup ref="logout" stepKey="logout"/> -+ </after> -+ <actionGroup ref="AdminNavigateMenuActionGroup" stepKey="navigateToNowOnlinePage"> -+ <argument name="menuUiId" value="{{AdminMenuCustomers.dataUiId}}"/> -+ <argument name="submenuUiId" value="{{AdminMenuCustomersNowOnline.dataUiId}}"/> -+ </actionGroup> -+ <actionGroup ref="AdminAssertPageTitleActionGroup" stepKey="seePageTitle"> -+ <argument name="title" value="{{AdminMenuCustomersNowOnline.pageTitle}}"/> -+ </actionGroup> -+ </test> -+</tests> -diff --git a/app/code/Magento/Customer/Test/Mftf/Test/AdminDeleteCustomerAddressesFromTheGridTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/AdminDeleteCustomerAddressesFromTheGridTest.xml -new file mode 100644 -index 00000000000..4f501c27352 ---- /dev/null -+++ b/app/code/Magento/Customer/Test/Mftf/Test/AdminDeleteCustomerAddressesFromTheGridTest.xml -@@ -0,0 +1,58 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> -+ <test name="AdminDeleteCustomerAddressesFromTheGridTest"> -+ <annotations> -+ <title value="Admin delete customer addresses from the grid"/> -+ <description value="Admin delete customer addresses from the grid"/> -+ <features value="Module/ Customer"/> -+ <severity value="MAJOR"/> -+ <testCaseId value="MAGETWO-94850"/> -+ <stories value="MAGETWO-94346: Implement handling of large number of addresses on admin edit customer page"/> -+ <group value="customer"/> -+ </annotations> -+ -+ <before> -+ <createData entity="Simple_US_Customer_Multiple_Addresses" stepKey="createCustomer"/> -+ <magentoCLI command="indexer:reindex" stepKey="reindex"/> -+ <actionGroup ref="LoginAsAdmin" stepKey="login"/> -+ </before> -+ <after> -+ <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> -+ <actionGroup ref="logout" stepKey="logout"/> -+ </after> -+ <!-- - -+ Step1. Login to admin and go to Customers > All Customerts. -+ Step2. On *Customers* page choose customer from preconditions and open it to edit -+ Step3. On edit customer page open *Addresses* tab and find a grid with the additional addresses -+ <!- --> -+ <amOnPage url="{{AdminCustomerPage.url}}" stepKey="openCustomersGridPage"/> -+ <actionGroup ref="OpenEditCustomerFromAdminActionGroup" stepKey="openEditCustomerPage"> -+ <argument name="customer" value="Simple_US_Customer_Multiple_Addresses"/> -+ </actionGroup> -+ <click selector="{{AdminEditCustomerInformationSection.addresses}}" stepKey="openAddressesTab"/> -+ <!--Step4. Click *Select* link in *Actions* column for target additional address--> -+ <conditionalClick selector="{{AdminCustomerAddressFiltersSection.clearAll}}" dependentSelector="{{AdminCustomerAddressFiltersSection.clearAll}}" visible="true" stepKey="clickOnButtonToRemoveFiltersIfPresent"/> -+ <click selector="{{AdminCustomerAddressesGridSection.firstRowSelectLink}}" stepKey="clickOnSelectLinkInFirstRow"/> -+ <!--Step5. Click *Delete*--> -+ <click selector="{{AdminCustomerAddressesGridSection.firstRowDeleteLink}}" stepKey="chooseDeleteOptionInFirstRow"/> -+ <waitForPageLoad stepKey="waitForCustomerAddressesGridPageLoad1"/> -+ <!--Step6. Press *Ok* button on the pop-up--> -+ <click selector="{{AdminCustomerAddressesGridActionsSection.ok}}" stepKey="clickOkOnPopup"/> -+ <waitForPageLoad stepKey="waitForCustomerAddressesGridPageLoad2"/> -+ <seeNumberOfElements userInput="1" selector="{{AdminCustomerAddressesGridSection.rowsInGrid}}" stepKey="seeOnlyOneCustomerAddressesInGrid"/> -+ <!--Step7. Delete last customer address--> -+ <click selector="{{AdminCustomerAddressesGridSection.firstRowSelectLink}}" stepKey="clickOnSelectLinkInFirstRow2"/> -+ <click selector="{{AdminCustomerAddressesGridSection.firstRowDeleteLink}}" stepKey="chooseDeleteOptionInFirstRow2"/> -+ <waitForPageLoad stepKey="waitForCustomerAddressesGridPageLoad3"/> -+ <click selector="{{AdminCustomerAddressesGridActionsSection.ok}}" stepKey="clickOkOnPopup2"/> -+ <waitForPageLoad stepKey="waitForCustomerAddressesGridPageLoad4"/> -+ <see userInput="We couldn't find any records." selector="{{AdminCustomerAddressesGridSection.customerAddressGrid}}" stepKey="checkThatCustomerAddressesGridHasNoRecords"/> -+</test> -+</tests> -diff --git a/app/code/Magento/Customer/Test/Mftf/Test/AdminDeleteCustomerAddressesFromTheGridViaMassActionsTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/AdminDeleteCustomerAddressesFromTheGridViaMassActionsTest.xml -new file mode 100644 -index 00000000000..a703c5a7c5d ---- /dev/null -+++ b/app/code/Magento/Customer/Test/Mftf/Test/AdminDeleteCustomerAddressesFromTheGridViaMassActionsTest.xml -@@ -0,0 +1,54 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> -+ <test name="AdminDeleteCustomerAddressesFromTheGridViaMassActionsTest"> -+ <annotations> -+ <title value="Admin delete customer addresses from the grid via mass actions"/> -+ <description value="Admin delete customer addresses from the grid via mass actions"/> -+ <features value="Module/ Customer"/> -+ <severity value="MAJOR"/> -+ <testCaseId value="MAGETWO-94951"/> -+ <stories value="MAGETWO-94346: Implement handling of large number of addresses on admin edit customer page"/> -+ <group value="customer"/> -+ </annotations> -+ -+ <before> -+ <createData entity="Simple_US_Customer_Multiple_Addresses" stepKey="createCustomer"/> -+ <magentoCLI command="indexer:reindex" stepKey="reindex"/> -+ <actionGroup ref="LoginAsAdmin" stepKey="login"/> -+ </before> -+ <after> -+ <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> -+ <actionGroup ref="logout" stepKey="logout"/> -+ </after> -+ <!-- - -+ Step1. Login to admin and go to Customers > All Customerts. -+ Step2. On *Customers* page choose customer from preconditions and open it to edit -+ Step3. On edit customer page open *Addresses* tab and find a grid with the additional addresses -+ <!- --> -+ <amOnPage url="{{AdminCustomerPage.url}}" stepKey="openCustomersGridPage"/> -+ <actionGroup ref="OpenEditCustomerFromAdminActionGroup" stepKey="openEditCustomerPage"> -+ <argument name="customer" value="Simple_US_Customer_Multiple_Addresses"/> -+ </actionGroup> -+ <click selector="{{AdminEditCustomerInformationSection.addresses}}" stepKey="openAddressesTab"/> -+ <conditionalClick selector="{{AdminCustomerAddressFiltersSection.clearAll}}" dependentSelector="{{AdminCustomerAddressFiltersSection.clearAll}}" visible="true" stepKey="clickOnButtonToRemoveFiltersIfPresent"/> -+ <!-- - -+ Step4. Check checkboxes for several addresses open *Actions* dropdown at the top of addresses grid and select action *Delete* -+ Step5. Press *Ok* button on the pop-up -+ <!- --> -+ <click selector="{{AdminCustomerAddressesGridSection.firstRowCheckbox}}" stepKey="tickFirstRowCustomerAddressCheckbox"/> -+ <click selector="{{AdminCustomerAddressesGridSection.secondRowCheckbox}}" stepKey="tickSecondRowCustomerAddressCheckbox"/> -+ <click selector="{{AdminCustomerAddressesGridActionsSection.actions}}" stepKey="openActionsDropdown"/> -+ <click selector="{{AdminCustomerAddressesGridActionsSection.delete}}" stepKey="chooseDeleteOption"/> -+ <waitForPageLoad stepKey="waitForCustomerAddressesGridPageLoad1"/> -+ <click selector="{{AdminCustomerAddressesGridActionsSection.ok}}" stepKey="clickOkOnPopup"/> -+ <waitForPageLoad stepKey="waitForCustomerAddressesGridPageLoad2"/> -+ <see userInput="We couldn't find any records." selector="{{AdminCustomerAddressesGridSection.customerAddressGrid}}" stepKey="checkThatCustomerAddressesGridHasNoRecords"/> -+</test> -+</tests> -diff --git a/app/code/Magento/Customer/Test/Mftf/Test/AdminDeleteCustomerTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/AdminDeleteCustomerTest.xml -new file mode 100644 -index 00000000000..7fef916fc45 ---- /dev/null -+++ b/app/code/Magento/Customer/Test/Mftf/Test/AdminDeleteCustomerTest.xml -@@ -0,0 +1,47 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> -+ <test name="AdminDeleteCustomerTest"> -+ <annotations> -+ <stories value="Delete customer"/> -+ <title value="DeleteCustomerBackendEntityTestVariation1"/> -+ <description value="Login as admin and delete the customer"/> -+ <severity value="CRITICAL"/> -+ <testCaseId value="MC-14587"/> -+ <group value="mtf_migrated"/> -+ </annotations> -+ -+ <before> -+ <!-- Create Customer --> -+ <createData entity="CustomerEntityOne" stepKey="createCustomer"/> -+ <!-- Login as admin --> -+ <actionGroup ref="LoginAsAdmin" stepKey="login"/> -+ </before> -+ <after> -+ <actionGroup ref="logout" stepKey="logout"/> -+ </after> -+ -+ <!-- Delete created customer --> -+ <actionGroup ref="DeleteCustomerByEmailActionGroup" stepKey="deleteCustomer"> -+ <argument name="email" value="$$createCustomer.email$$"/> -+ </actionGroup> -+ <seeElement selector="{{CustomersPageSection.deletedSuccessMessage}}" stepKey="seeSuccessMessage"/> -+ <waitForPageLoad stepKey="waitForCustomerGridPageToLoad"/> -+ -+ <!--Assert Customer is not in Grid --> -+ <click selector="{{AdminCustomerFiltersSection.filtersButton}}" stepKey="clickFilterButton"/> -+ <conditionalClick selector="{{AdminDataGridHeaderSection.clearFilters}}" dependentSelector="{{AdminDataGridHeaderSection.clearFilters}}" visible="true" stepKey="cleanFiltersIfTheySet"/> -+ <waitForPageLoad stepKey="waitForClearFilters1"/> -+ <fillField selector="{{AdminCustomerFiltersSection.emailInput}}" userInput="$$createCustomer.email$$" stepKey="filterEmail"/> -+ <click selector="{{AdminCustomerFiltersSection.apply}}" stepKey="applyFilter"/> -+ <waitForPageLoad stepKey="waitForPageToLoad"/> -+ <see selector="{{AdminCustomerGridSection.customerGrid}}" userInput="We couldn't find any records." stepKey="seeEmptyRecordMessage"/> -+ </test> -+</tests> -diff --git a/app/code/Magento/Customer/Test/Mftf/Test/AdminDeleteDefaultBillingCustomerAddressTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/AdminDeleteDefaultBillingCustomerAddressTest.xml -new file mode 100644 -index 00000000000..bb455677d5e ---- /dev/null -+++ b/app/code/Magento/Customer/Test/Mftf/Test/AdminDeleteDefaultBillingCustomerAddressTest.xml -@@ -0,0 +1,52 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> -+ <test name="AdminDeleteDefaultBillingCustomerAddressTest"> -+ <annotations> -+ <title value="Admin delete default billing customer address"/> -+ <description value="Admin delete default billing customer address"/> -+ <features value="Module/ Customer"/> -+ <severity value="MAJOR"/> -+ <testCaseId value="MAGETWO-94816"/> -+ <stories value="MAGETWO-94346: Implement handling of large number of addresses on admin edit customer page"/> -+ <group value="customer"/> -+ </annotations> -+ -+ <before> -+ <createData entity="Simple_US_Customer_Multiple_Addresses" stepKey="createCustomer"/> -+ <magentoCLI command="indexer:reindex" stepKey="reindex"/> -+ <actionGroup ref="LoginAsAdmin" stepKey="login"/> -+ </before> -+ <after> -+ <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> -+ <actionGroup ref="logout" stepKey="logout"/> -+ </after> -+ <!-- - -+ Step1. Login to admin and go to Customers > All Customers. -+ Step2. On *Customers* page choose customer from preconditions and open it to edit -+ <!- --> -+ <amOnPage url="{{AdminCustomerPage.url}}" stepKey="openCustomersGridPage"/> -+ <actionGroup ref="OpenEditCustomerFromAdminActionGroup" stepKey="openEditCustomerPage"> -+ <argument name="customer" value="Simple_US_Customer_Multiple_Addresses"/> -+ </actionGroup> -+ <!--Step3. Open *Addresses* tab on edit customer page and click *edit* link near *Default Billing Address* block--> -+ <click selector="{{AdminEditCustomerInformationSection.addresses}}" stepKey="openAddressesTab"/> -+ <conditionalClick selector="{{AdminCustomerAddressFiltersSection.clearAll}}" dependentSelector="{{AdminCustomerAddressFiltersSection.clearAll}}" visible="true" stepKey="clickOnButtonToRemoveFiltersIfPresent"/> -+ <seeNumberOfElements userInput="2" selector="{{AdminCustomerAddressesGridSection.rowsInGrid}}" stepKey="seeTwoCustomerAddressesInGrid"/> -+ <click selector="{{AdminCustomerAddressesDefaultBillingSection.editButton}}" stepKey="clickEditNearDefaultBillingAddress"/> -+ <waitForPageLoad stepKey="waitForDefaultBillingAddressPopupLoad"/> -+ <!--Step4. Press *Delete* button--> -+ <click selector="{{AdminCustomerAddressesSection.deleteButton}}" stepKey="clickDeleteButton"/> -+ <waitForPageLoad stepKey="waitForConfirmationPopupLoad"/> -+ <click selector="{{AdminCustomerAddressesSection.ok}}" stepKey="clickOkOnPopup"/> -+ <waitForPageLoad stepKey="waitForDefaultBillingAddressPopupLoad2"/> -+ <seeNumberOfElements userInput="1" selector="{{AdminCustomerAddressesGridSection.rowsInGrid}}" stepKey="seeOnlyOneCustomerAddressesInGrid"/> -+ <dontSee userInput="{{US_Address_NY.street[0]}}" stepKey="assertDefaultBillingIsSet"/> -+ </test> -+</tests> -diff --git a/app/code/Magento/Customer/Test/Mftf/Test/AdminEditDefaultBillingShippingCustomerAddressTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/AdminEditDefaultBillingShippingCustomerAddressTest.xml -new file mode 100644 -index 00000000000..df317c8bf70 ---- /dev/null -+++ b/app/code/Magento/Customer/Test/Mftf/Test/AdminEditDefaultBillingShippingCustomerAddressTest.xml -@@ -0,0 +1,69 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> -+ <test name="AdminEditDefaultBillingShippingCustomerAddressTest"> -+ <annotations> -+ <stories value="Edit default billing/shipping customer address"/> -+ <title value="Edit default billing/shipping customer address"/> -+ <description value="Edit default billing/shipping customer address on customer addresses tab"/> -+ <severity value="CRITICAL"/> -+ <testCaseId value="MAGETWO-94815"/> -+ <group value="customer"/> -+ </annotations> -+ <before> -+ <createData entity="Simple_US_Customer_Multiple_Addresses" stepKey="customer"/> -+ <actionGroup ref="LoginAsAdmin" stepKey="login"/> -+ </before> -+ <after> -+ <deleteData createDataKey="customer" stepKey="deleteCustomer"/> -+ <actionGroup ref="logout" stepKey="logout"/> -+ </after> -+ <!-- - -+ Step1. Login to admin and go to Customers > All Customers. -+ Step2. On *Customers* page choose customer from preconditions and open it to edit -+ Step3. Open *Addresses* tab on edit customer page and press *Add New Address* button -+ <!- --> -+ <amOnPage url="{{AdminCustomerPage.url}}" stepKey="openCustomersGridPage"/> -+ <actionGroup ref="OpenEditCustomerFromAdminActionGroup" stepKey="openEditCustomerPage"> -+ <argument name="customer" value="Simple_US_Customer_Multiple_Addresses"/> -+ </actionGroup> -+ <click selector="{{AdminEditCustomerInformationSection.addresses}}" stepKey="openAddressesTab"/> -+ <waitForPageLoad stepKey="waitForAddresses"/> -+ <conditionalClick selector="{{AdminCustomerAddressFiltersSection.clearAll}}" dependentSelector="{{AdminCustomerAddressFiltersSection.clearAll}}" visible="true" stepKey="clickOnButtonToRemoveFiltersIfPresent"/> -+ <seeElement selector="{{AdminCustomerAddressesDefaultBillingSection.addressDetails}}" stepKey="seeDefaultBillingAddressSectionBeforeChangingDefaultAddress"/> -+ <see userInput="{{US_Address_NY.street[0]}}" selector="{{AdminCustomerAddressesDefaultBillingSection.addressDetails}}" stepKey="assertDefaultBillingAddressIsSetBeforeChangingDefaultAddress"/> -+ <seeElement selector="{{AdminCustomerAddressesDefaultShippingSection.addressDetails}}" stepKey="seeDefaultShippingAddressSectionBeforeChangingDefaultAddress"/> -+ <see userInput="{{US_Address_NY.street[0]}}" selector="{{AdminCustomerAddressesDefaultShippingSection.addressDetails}}" stepKey="assertDefaultShippingAddressIsSetBeforeChangingDefaultAddress"/> -+ <click selector="{{AdminCustomerAddressesSection.addNewAddress}}" stepKey="clickAddNewAddressButton"/> -+ <waitForPageLoad stepKey="waitForAddUpdateCustomerAddressForm"/> -+ <!--Step4. Fill all the fields with test data and press *Save* button--> -+ <click selector="{{AdminCustomerAddressesSection.defaultBillingAddress}}" stepKey="enableDefaultBillingAddress"/> -+ <click selector="{{AdminCustomerAddressesSection.defaultShippingAddress}}" stepKey="enableDefaultShippingAddress"/> -+ <fillField userInput="{{US_Address_TX.firstname}}" selector="{{AdminCustomerAddressesSection.firstNameForAddress}}" stepKey="fillFirstName"/> -+ <fillField userInput="{{US_Address_TX.lastname}}" selector="{{AdminCustomerAddressesSection.lastNameForAddress}}" stepKey="fillLastName"/> -+ <fillField userInput="{{US_Address_TX.company}}" selector="{{AdminCustomerAddressesSection.company}}" stepKey="fillCompany"/> -+ <fillField userInput="{{US_Address_TX.street[0]}}" selector="{{AdminCustomerAddressesSection.streetAddress}}" stepKey="fillStreet"/> -+ <fillField userInput="{{US_Address_TX.city}}" selector="{{AdminCustomerAddressesSection.city}}" stepKey="fillCity"/> -+ <click selector="{{AdminCustomerAddressesSection.country}}" stepKey="clickCountryToOpenListOfCountries"/> -+ <click selector="{{AdminCustomerAddressesSection.countryId(US_Address_TX.country_id)}}" stepKey="fillCountry"/> -+ <fillField userInput="{{US_Address_TX.postcode}}" selector="{{AdminCustomerAddressesSection.zip}}" stepKey="fillPostcode"/> -+ <fillField userInput="{{US_Address_TX.telephone}}" selector="{{AdminCustomerAddressesSection.phoneNumber}}" stepKey="fillTelephone"/> -+ <click selector="{{AdminCustomerAddressesSection.region}}" stepKey="clickRegionToOpenListOfRegions"/> -+ <click selector="{{AdminCustomerAddressesSection.regionId(US_Address_TX.state)}}" stepKey="fillRegion"/> -+ <click selector="{{AdminCustomerAddressesSection.saveAddress}}" stepKey="clickSaveCustomerAddressOnAddUpdateAddressForm"/> -+ <waitForPageLoad stepKey="waitForNewAddressIsCreated"/> -+ <see userInput="{{US_Address_TX.street[0]}}" selector="{{AdminCustomerAddressesDefaultBillingSection.addressDetails}}" stepKey="assertDefaultBillingAddressIsChanged"/> -+ <see userInput="{{US_Address_TX.street[0]}}" selector="{{AdminCustomerAddressesDefaultShippingSection.addressDetails}}" stepKey="assertDefaultShippingAddressIsChanged"/> -+ <click selector="{{AdminCustomerAddressesDefaultBillingSection.editButton}}" stepKey="clickEditDefaultBillingAddress"/> -+ <waitForPageLoad stepKey="waitForCustomerAddressAddUpdateFormLoad"/> -+ <assertElementContainsAttribute selector="{{AdminCustomerAddressesSection.defaultBillingAddressCheckBox}}" attribute="value" expectedValue="1" stepKey="assertDefaultBillingIsEnabledCustomerAddressAddUpdateForm"/> -+ <assertElementContainsAttribute selector="{{AdminCustomerAddressesSection.defaultShippingAddressCheckBox}}" attribute="value" expectedValue="1" stepKey="assertDefaultShippingIsEnabledOnCustomerAddressAddUpdateForm"/> -+ </test> -+</tests> -diff --git a/app/code/Magento/Customer/Test/Mftf/Test/AdminExactMatchSearchInCustomerGridTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/AdminExactMatchSearchInCustomerGridTest.xml -new file mode 100644 -index 00000000000..6a7aeab78bc ---- /dev/null -+++ b/app/code/Magento/Customer/Test/Mftf/Test/AdminExactMatchSearchInCustomerGridTest.xml -@@ -0,0 +1,47 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> -+ <test name="AdminExactMatchSearchInCustomerGridTest"> -+ <annotations> -+ <features value="Customer"/> -+ <stories value="Customer Search"/> -+ <title value="Admin customer grid exact match searching"/> -+ <description value="Admin customer grid exact match searching with quotes in keyword"/> -+ <severity value="MAJOR"/> -+ <testCaseId value="MC-16335"/> -+ <useCaseId value="MAGETWO-99605"/> -+ <group value="customer"/> -+ </annotations> -+ <before> -+ <createData entity="Simple_US_Customer" stepKey="createFirstCustomer"/> -+ <createData entity="Simple_US_Customer" stepKey="createSecondCustomer"> -+ <field key="firstname">"Jane Doe"</field> -+ </createData> -+ <actionGroup ref="LoginAsAdmin" stepKey="login"/> -+ </before> -+ <after> -+ <deleteData createDataKey="createFirstCustomer" stepKey="deleteFirstCustomer"/> -+ <deleteData createDataKey="createSecondCustomer" stepKey="deleteSecondCustomer"/> -+ <amOnPage url="{{AdminCustomerPage.url}}" stepKey="openCustomersGridPage"/> -+ <actionGroup ref="AdminResetFilterInCustomerAddressGrid" stepKey="clearCustomerGridFilter"/> -+ <actionGroup ref="logout" stepKey="logout"/> -+ </after> -+ <!--Step 1: Go to Customers > All Customers--> -+ <amOnPage url="{{AdminCustomerPage.url}}" stepKey="openCustomersGridPage"/> -+ <!--Step 2: On Customers grid page search customer by keyword with quotes--> -+ <actionGroup ref="searchAdminDataGridByKeyword" stepKey="searchCustomer"> -+ <argument name="keyword" value="$$createSecondCustomer.firstname$$"/> -+ </actionGroup> -+ <!--Step 3: Check if customer is placed in a first row and clear grid filter--> -+ <actionGroup ref="AdminAssertCustomerInCustomersGrid" stepKey="checkCustomerInGrid"> -+ <argument name="text" value="$$createSecondCustomer.fullname$$"/> -+ <argument name="row" value="1"/> -+ </actionGroup> -+ </test> -+</tests> -diff --git a/app/code/Magento/Customer/Test/Mftf/Test/AdminResetCustomerPasswordTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/AdminResetCustomerPasswordTest.xml -new file mode 100644 -index 00000000000..fb67838e941 ---- /dev/null -+++ b/app/code/Magento/Customer/Test/Mftf/Test/AdminResetCustomerPasswordTest.xml -@@ -0,0 +1,39 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> -+ <test name="AdminResetCustomerPasswordTest"> -+ <annotations> -+ <stories value="Reset password"/> -+ <title value="Admin should be able to reset customer password"/> -+ <description value="Admin should be able to reset customer password"/> -+ <severity value="CRITICAL"/> -+ <testCaseId value="MAGETWO-30875"/> -+ <group value="customer"/> -+ </annotations> -+ <before> -+ <createData entity="Simple_US_Customer" stepKey="customer"/> -+ <actionGroup ref="LoginAsAdmin" stepKey="login"/> -+ </before> -+ <after> -+ <deleteData createDataKey="customer" stepKey="deleteCustomer"/> -+ <actionGroup ref="logout" stepKey="logout"/> -+ </after> -+ <magentoCLI command="indexer:reindex" stepKey="reindex"/> -+ <magentoCLI command="cache:flush" stepKey="flushCache"/> -+ <!--Edit customer info--> -+ <actionGroup ref="OpenEditCustomerFromAdminActionGroup" stepKey="OpenEditCustomerFrom"> -+ <argument name="customer" value="$$customer$$"/> -+ </actionGroup> -+ <click selector="{{AdminCustomerMainActionsSection.resetPassword}}" stepKey="resetPassword"/> -+ <see userInput="The customer will receive an email with a link to reset password." stepKey="messageThatLinkToPasswordResetIsSent"/> -+ </test> -+</tests> -+ -+ -diff --git a/app/code/Magento/Customer/Test/Mftf/Test/AdminSearchCustomerAddressByKeywordTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/AdminSearchCustomerAddressByKeywordTest.xml -new file mode 100644 -index 00000000000..9f1c5e8cd92 ---- /dev/null -+++ b/app/code/Magento/Customer/Test/Mftf/Test/AdminSearchCustomerAddressByKeywordTest.xml -@@ -0,0 +1,47 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> -+ <test name="AdminSearchCustomerAddressByKeywordTest"> -+ <annotations> -+ <title value="Admin search customer address by keyword"/> -+ <description value="Admin search customer address by keyword"/> -+ <features value="Module/ Customer"/> -+ <severity value="MAJOR"/> -+ <testCaseId value="MAGETWO-94954"/> -+ <stories value="MAGETWO-94346: Implement handling of large number of addresses on admin edit customer page"/> -+ <group value="customer"/> -+ </annotations> -+ -+ <before> -+ <createData entity="Simple_US_Customer_Multiple_Addresses" stepKey="createCustomer"/> -+ <magentoCLI command="indexer:reindex" stepKey="reindex"/> -+ <actionGroup ref="LoginAsAdmin" stepKey="login"/> -+ </before> -+ <after> -+ <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> -+ <actionGroup ref="logout" stepKey="logout"/> -+ </after> -+ <!-- - -+ Step1. Login to admin and go to Customers > All Customerts. -+ Step2. On *Customers* page choose customer from preconditions and open it to edit -+ Step3. On edit customer page open *Addresses* tab and find a grid with the additional addresses -+ <!- --> -+ <amOnPage url="{{AdminCustomerPage.url}}" stepKey="openCustomersGridPage"/> -+ <actionGroup ref="OpenEditCustomerFromAdminActionGroup" stepKey="openEditCustomerPage"> -+ <argument name="customer" value="Simple_US_Customer_Multiple_Addresses"/> -+ </actionGroup> -+ <click selector="{{AdminEditCustomerInformationSection.addresses}}" stepKey="openAddressesTab"/> -+ <conditionalClick selector="{{AdminCustomerAddressFiltersSection.clearAll}}" dependentSelector="{{AdminCustomerAddressFiltersSection.clearAll}}" visible="true" stepKey="clickOnButtonToRemoveFiltersIfPresent"/> -+ <!--Step4. Fill *Search by keyword* filed with the query and press enter or clock on the magnifier icon--> -+ <fillField userInput="{{US_Address_NY.street[0]}}" selector="{{AdminCustomerAddressesGridActionsSection.search}}" stepKey="FillCustomerAddressStreetInSearchByKeyword"/> -+ <pressKey parameterArray="[\Facebook\WebDriver\WebDriverKeys::ENTER]" selector="{{AdminCustomerAddressesGridActionsSection.search}}" stepKey="pressEnterKey"/> -+ <waitForPageLoad stepKey="waitForCustomerAddressesGridPageLoad"/> -+ <seeNumberOfElements userInput="1" selector="{{AdminCustomerAddressesGridSection.rowsInGrid}}" stepKey="seeOnlyOneCustomerAddressesInGrid"/> -+ </test> -+</tests> -\ No newline at end of file -diff --git a/app/code/Magento/Customer/Test/Mftf/Test/AdminSetCustomerDefaultBillingAddressTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/AdminSetCustomerDefaultBillingAddressTest.xml -new file mode 100644 -index 00000000000..db8d4e1ee1e ---- /dev/null -+++ b/app/code/Magento/Customer/Test/Mftf/Test/AdminSetCustomerDefaultBillingAddressTest.xml -@@ -0,0 +1,57 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> -+ <test name="AdminSetCustomerDefaultBillingAddressTest"> -+ <annotations> -+ <stories value="Set customer default billing address"/> -+ <title value="Admin should be able to set customer default billing address"/> -+ <description value="Admin should be able to set customer default billing address from customer addresses grid row actions"/> -+ <severity value="CRITICAL"/> -+ <testCaseId value="MAGETWO-94952"/> -+ <group value="customer"/> -+ </annotations> -+ <before> -+ <createData entity="Simple_US_Customer_Multiple_Addresses_No_Default_Address" stepKey="customer"/> -+ <actionGroup ref="LoginAsAdmin" stepKey="login"/> -+ </before> -+ <after> -+ <deleteData createDataKey="customer" stepKey="deleteCustomer"/> -+ <actionGroup ref="logout" stepKey="logout"/> -+ </after> -+ <!-- - -+ Step1. Login to admin and go to Customers > All Customers. -+ Step2. On *Customers* page choose customer from preconditions and open it to edit -+ Step3. On edit customer page open *Addresses* tab and find a grid with the additional addresses -+ <!- --> -+ <amOnPage url="{{AdminCustomerPage.url}}" stepKey="openCustomersGridPage"/> -+ <actionGroup ref="OpenEditCustomerFromAdminActionGroup" stepKey="openEditCustomerPage"> -+ <argument name="customer" value="Simple_US_Customer_Multiple_Addresses_No_Default_Address"/> -+ </actionGroup> -+ <click selector="{{AdminEditCustomerInformationSection.addresses}}" stepKey="openAddressesTab"/> -+ <waitForPageLoad stepKey="waitForAddresses"/> -+ <conditionalClick selector="{{AdminCustomerAddressFiltersSection.clearAll}}" dependentSelector="{{AdminCustomerAddressFiltersSection.clearAll}}" visible="true" stepKey="clickOnButtonToRemoveFiltersIfPresent"/> -+ <fillField userInput="{{US_Address_NY_Not_Default_Address.street[0]}}" selector="{{AdminCustomerAddressesGridActionsSection.search}}" stepKey="fillCustomerAddressStreetInSearchByKeyword"/> -+ <pressKey parameterArray="[\Facebook\WebDriver\WebDriverKeys::ENTER]" selector="{{AdminCustomerAddressesGridActionsSection.search}}" stepKey="pressEnterKey"/> -+ <waitForPageLoad stepKey="waitForCustomerAddressesGridPageLoad"/> -+ <see userInput="The customer does not have default billing address" selector="{{AdminCustomerAddressesDefaultBillingSection.address}}" stepKey="assertThatThereIsNoDefaultBillingAddress"/> -+ <seeNumberOfElements userInput="1" selector="{{AdminCustomerAddressesGridSection.rowsInGrid}}" stepKey="seeOnlyOneCustomerAddressesInGrid"/> -+ <!--Step4. Click *Select* link in *Actions* column for target additional address--> -+ <click selector="{{AdminCustomerAddressesGridSection.firstRowSelectLink}}" stepKey="clickSelectElementFromRow" /> -+ <!--Step4. Click *Set as default billing*--> -+ <click selector="{{AdminCustomerAddressesGridSection.firstRowSetAsDefaultBillingLink}}" stepKey="clickOnSetAddressAsDefaultBilling"/> -+ <!--Step5. Press *Ok* button on the pop-up--> -+ <click selector="{{AdminConfirmationModalSection.ok}}" stepKey="confirmSetAddressAsDefaultBilling"/> -+ <seeElement selector="{{AdminCustomerAddressesDefaultBillingSection.addressDetails}}" stepKey="seeDefaultBillingAddressSection"/> -+ <see userInput="{{US_Address_NY_Not_Default_Address.street[0]}}" selector="{{AdminCustomerAddressesDefaultBillingSection.addressDetails}}" stepKey="assertDefaultBillingAddressIsSet"/> -+ <click selector="{{AdminCustomerAddressesDefaultBillingSection.editButton}}" stepKey="clickEditDefaultBillingAddress"/> -+ <waitForPageLoad stepKey="waitForCustomerAddressAddUpdateFormLoad"/> -+ <assertElementContainsAttribute selector="{{AdminCustomerAddressesSection.defaultBillingAddressCheckBox}}" attribute="value" expectedValue="1" stepKey="assertDefaultBillingCheckboxIsCheckedOnCustomerAddressAddUpdateForm"/> -+ </test> -+</tests> -diff --git a/app/code/Magento/Customer/Test/Mftf/Test/AdminSetCustomerDefaultShippingAddressTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/AdminSetCustomerDefaultShippingAddressTest.xml -new file mode 100644 -index 00000000000..6e832181769 ---- /dev/null -+++ b/app/code/Magento/Customer/Test/Mftf/Test/AdminSetCustomerDefaultShippingAddressTest.xml -@@ -0,0 +1,57 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> -+ <test name="AdminSetCustomerDefaultShippingAddressTest"> -+ <annotations> -+ <stories value="Set customer default shipping address"/> -+ <title value="Admin should be able to set customer default shipping address"/> -+ <description value="Admin should be able to set customer default shipping address from customer addresses grid row actions"/> -+ <severity value="CRITICAL"/> -+ <testCaseId value="MAGETWO-94953"/> -+ <group value="customer"/> -+ </annotations> -+ <before> -+ <createData entity="Simple_US_Customer_Multiple_Addresses_No_Default_Address" stepKey="customer"/> -+ <actionGroup ref="LoginAsAdmin" stepKey="login"/> -+ </before> -+ <after> -+ <deleteData createDataKey="customer" stepKey="deleteCustomer"/> -+ <actionGroup ref="logout" stepKey="logout"/> -+ </after> -+ <!-- - -+ Step1. Login to admin and go to Customers > All Customers. -+ Step2. On *Customers* page choose customer from preconditions and open it to edit -+ Step3. On edit customer page open *Addresses* tab and find a grid with the additional addresses -+ <!- --> -+ <amOnPage url="{{AdminCustomerPage.url}}" stepKey="openCustomersGridPage"/> -+ <actionGroup ref="OpenEditCustomerFromAdminActionGroup" stepKey="openEditCustomerPage"> -+ <argument name="customer" value="Simple_US_Customer_Multiple_Addresses_No_Default_Address"/> -+ </actionGroup> -+ <click selector="{{AdminEditCustomerInformationSection.addresses}}" stepKey="openAddressesTab"/> -+ <waitForPageLoad stepKey="waitForAddresses"/> -+ <conditionalClick selector="{{AdminCustomerAddressFiltersSection.clearAll}}" dependentSelector="{{AdminCustomerAddressFiltersSection.clearAll}}" visible="true" stepKey="clickOnButtonToRemoveFiltersIfPresent"/> -+ <fillField userInput="{{US_Address_NY_Not_Default_Address.street[0]}}" selector="{{AdminCustomerAddressesGridActionsSection.search}}" stepKey="fillCustomerAddressStreetInSearchByKeyword"/> -+ <pressKey parameterArray="[\Facebook\WebDriver\WebDriverKeys::ENTER]" selector="{{AdminCustomerAddressesGridActionsSection.search}}" stepKey="pressEnterKey"/> -+ <waitForPageLoad stepKey="waitForCustomerAddressesGridPageLoad"/> -+ <see userInput="The customer does not have default shipping address" selector="{{AdminCustomerAddressesDefaultShippingSection.address}}" stepKey="assertThatThereIsNoDefaultShippingAddress"/> -+ <seeNumberOfElements userInput="1" selector="{{AdminCustomerAddressesGridSection.rowsInGrid}}" stepKey="seeOnlyOneCustomerAddressesInGrid"/> -+ <!--Step4. Click *Select* link in *Actions* column for target additional address--> -+ <click selector="{{AdminCustomerAddressesGridSection.firstRowSelectLink}}" stepKey="clickSelectElementFromRow" /> -+ <!--Step4. Click *Set as default shipping*--> -+ <click selector="{{AdminCustomerAddressesGridSection.firstRowSetAsDefaultShippingLink}}" stepKey="clickOnSetAddressAsDefaultShipping"/> -+ <!--Step5. Press *Ok* button on the pop-up--> -+ <click selector="{{AdminConfirmationModalSection.ok}}" stepKey="confirmSetAddressAsDefaultShipping"/> -+ <seeElement selector="{{AdminCustomerAddressesDefaultShippingSection.addressDetails}}" stepKey="seeDefaultShippingAddressSection"/> -+ <see userInput="{{US_Address_NY_Not_Default_Address.street[0]}}" selector="{{AdminCustomerAddressesDefaultShippingSection.addressDetails}}" stepKey="assertDefaultShippingAddressIsSet"/> -+ <click selector="{{AdminCustomerAddressesDefaultShippingSection.editButton}}" stepKey="clickEditDefaultShippingAddress"/> -+ <waitForPageLoad stepKey="waitForCustomerAddressAddUpdateFormLoad"/> -+ <assertElementContainsAttribute selector="{{AdminCustomerAddressesSection.defaultShippingAddressCheckBox}}" attribute="value" expectedValue="1" stepKey="assertDefaultShippingCheckboxIsCheckedOnCustomerAddressAddUpdateForm"/> -+ </test> -+</tests> -diff --git a/app/code/Magento/Customer/Test/Mftf/Test/AdminUpdateCustomerTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/AdminUpdateCustomerTest.xml -new file mode 100644 -index 00000000000..f58f23dee42 ---- /dev/null -+++ b/app/code/Magento/Customer/Test/Mftf/Test/AdminUpdateCustomerTest.xml -@@ -0,0 +1,308 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> -+ <test name="AdminUpdateCustomerInfoFromDefaultToNonDefaultTest"> -+ <annotations> -+ <features value="Customer"/> -+ <stories value="Update Customer Information in Admin"/> -+ <title value="Update Customer Info from Default to Non-Default in Admin"/> -+ <description value="Update Customer Info from Default to Non-Default in Admin"/> -+ <severity value="CRITICAL"/> -+ <testCaseId value="MC-13619"/> -+ <group value="Customer"/> -+ <group value="mtf_migrated"/> -+ </annotations> -+ <before> -+ <createData stepKey="customer" entity="Simple_Customer_Without_Address"/> -+ <magentoCLI command="indexer:reindex" stepKey="reindex"/> -+ <actionGroup ref="LoginAsAdmin" stepKey="login"/> -+ </before> -+ <after> -+ <deleteData stepKey="deleteCustomer" createDataKey="customer"/> -+ <!-- Reset customer grid filter --> -+ <amOnPage stepKey="goToCustomersGridPage" url="{{AdminCustomerPage.url}}"/> -+ <waitForPageLoad stepKey="waitForCustomersGrid"/> -+ <actionGroup stepKey="resetFilter" ref="AdminResetFilterInCustomerGrid"/> -+ <actionGroup ref="logout" stepKey="logout"/> -+ </after> -+ -+ <amOnPage url="{{AdminCustomerPage.url}}edit/id/$$customer.id$$/" stepKey="openCustomerEditPage"/> -+ <waitForPageLoad stepKey="waitForCustomerEditPage"/> -+ <!-- Update Customer Account Information --> -+ <actionGroup stepKey="editCustomerInformation" ref="AdminEditCustomerAccountInformationActionGroup"> -+ <argument name="firstName" value="$$customer.firstname$$updated"/> -+ <argument name="lastName" value="$$customer.lastname$$updated"/> -+ <argument name="email" value="updated$$customer.email$$"/> -+ </actionGroup> -+ <!--Update Customer Addresses --> -+ <actionGroup stepKey="editCustomerAddress" ref="AdminEditCustomerAddressSetDefaultShippingAndBilling"> -+ <argument name="customerAddress" value="CustomerAddressSimple"/> -+ </actionGroup> -+ <actionGroup stepKey="saveAndCheckSuccessMessage" ref="AdminSaveCustomerAndAssertSuccessMessage"/> -+ <!-- Assert Customer in Customer grid --> -+ <amOnPage stepKey="goToCustomersGridPage" url="{{AdminCustomerPage.url}}"/> -+ <waitForPageLoad stepKey="waitForCustomersGrid"/> -+ <actionGroup stepKey="resetFilter" ref="AdminResetFilterInCustomerGrid"/> -+ <actionGroup stepKey="filterByEamil" ref="AdminFilterCustomerGridByEmail"> -+ <argument name="email" value="updated$$customer.email$$"/> -+ </actionGroup> -+ <actionGroup stepKey="checkCustomerInGrid" ref="AdminAssertCustomerInCustomersGrid"> -+ <argument name="text" value="updated$$customer.email$$"/> -+ <argument name="row" value="1"/> -+ </actionGroup> -+ <!-- Assert Customer in Customer Form --> -+ <amOnPage url="{{AdminCustomerPage.url}}edit/id/$$customer.id$$/" stepKey="openCustomerEditPageAfterSave"/> -+ <waitForPageLoad stepKey="waitForCustomerEditPageAfterSave"/> -+ <!-- Assert Customer Account Information --> -+ <actionGroup stepKey="checkCustomerAccountInformation" ref="AdminAssertCustomerAccountInformation"> -+ <argument name="firstName" value="$$customer.firstname$$updated"/> -+ <argument name="lastName" value="$$customer.lastname$$updated"/> -+ <argument name="email" value="updated$$customer.email$$"/> -+ </actionGroup> -+ <!-- Assert Customer Default Billing Address --> -+ <actionGroup stepKey="checkDefaultBilling" ref="AdminAssertCustomerDefaultBillingAddress"> -+ <argument name="firstName" value="$$customer.firstname$$updated"/> -+ <argument name="lastName" value="$$customer.lastname$$updated"/> -+ <argument name="street1" value="{{CustomerAddressSimple.street[0]}}"/> -+ <argument name="state" value="{{CustomerAddressSimple.state}}"/> -+ <argument name="postcode" value="{{CustomerAddressSimple.postcode}}"/> -+ <argument name="country" value="{{CustomerAddressSimple.country_id}}"/> -+ <argument name="telephone" value="{{CustomerAddressSimple.telephone}}"/> -+ </actionGroup> -+ <!-- Assert Customer Default Shipping Address --> -+ <actionGroup stepKey="checkDefaultShipping" ref="AdminAssertCustomerDefaultShippingAddress"> -+ <argument name="firstName" value="$$customer.firstname$$updated"/> -+ <argument name="lastName" value="$$customer.lastname$$updated"/> -+ <argument name="street1" value="{{CustomerAddressSimple.street[0]}}"/> -+ <argument name="state" value="{{CustomerAddressSimple.state}}"/> -+ <argument name="postcode" value="{{CustomerAddressSimple.postcode}}"/> -+ <argument name="country" value="{{CustomerAddressSimple.country_id}}"/> -+ <argument name="telephone" value="{{CustomerAddressSimple.telephone}}"/> -+ </actionGroup> -+ </test> -+ -+ <test name="AdminUpdateCustomerAddressNoZipNoStateTest" extends="AdminUpdateCustomerInfoFromDefaultToNonDefaultTest"> -+ <annotations> -+ <features value="Customer"/> -+ <stories value="Update Customer Information in Admin"/> -+ <title value="Update Customer Address, without zip/state required, default billing/shipping checked in Admin"/> -+ <description value="Update Customer Address, without zip/state required, default billing/shipping checked in Admin"/> -+ <severity value="CRITICAL"/> -+ <testCaseId value="MC-13621"/> -+ <group value="Customer"/> -+ <group value="mtf_migrated"/> -+ </annotations> -+ <after> -+ <remove keyForRemoval="goToCustomersGridPage"/> -+ <remove keyForRemoval="waitForCustomersGrid"/> -+ <remove keyForRemoval="resetFilter"/> -+ </after> -+ -+ <!-- Remove steps that are not used for this test --> -+ <remove keyForRemoval="editCustomerInformation"/> -+ <remove keyForRemoval="goToCustomersGridPage"/> -+ <remove keyForRemoval="waitForCustomersGrid"/> -+ <remove keyForRemoval="resetFilter"/> -+ <remove keyForRemoval="filterByEamil"/> -+ <remove keyForRemoval="checkCustomerInGrid"/> -+ <remove keyForRemoval="checkCustomerAccountInformation"/> -+ -+ <!--Update Customer Addresses With No Zip and No State --> -+ <actionGroup stepKey="editCustomerAddress" ref="AdminEditCustomerAddressNoZipNoState"> -+ <argument name="customerAddress" value="addressNoZipNoState"/> -+ </actionGroup> -+ -+ <!-- Assert Customer Default Billing Address --> -+ <actionGroup stepKey="checkDefaultBilling" ref="AdminAssertCustomerDefaultBillingAddress"> -+ <argument name="firstName" value="$$customer.firstname$$"/> -+ <argument name="lastName" value="$$customer.lastname$$"/> -+ <argument name="street1" value="{{addressNoZipNoState.street[0]}}"/> -+ <argument name="country" value="{{addressNoZipNoState.country_id}}"/> -+ <argument name="telephone" value="{{addressNoZipNoState.telephone}}"/> -+ </actionGroup> -+ <!-- Assert Customer Default Shipping Address --> -+ <actionGroup stepKey="checkDefaultShipping" ref="AdminAssertCustomerDefaultShippingAddress"> -+ <argument name="firstName" value="$$customer.firstname$$"/> -+ <argument name="lastName" value="$$customer.lastname$$"/> -+ <argument name="street1" value="{{addressNoZipNoState.street[0]}}"/> -+ <argument name="country" value="{{addressNoZipNoState.country_id}}"/> -+ <argument name="telephone" value="{{addressNoZipNoState.telephone}}"/> -+ </actionGroup> -+ <!-- Assert Customer Login Storefront --> -+ <actionGroup stepKey="login" ref="StorefrontAssertSuccessLoginToStorefront" after="checkDefaultShipping" > -+ <argument name="Customer" value="$$customer$$"/> -+ </actionGroup> -+ </test> -+ -+ <test name="AdminUpdateCustomerAddressNoBillingNoShippingTest" extends="AdminUpdateCustomerInfoFromDefaultToNonDefaultTest"> -+ <annotations> -+ <features value="Customer"/> -+ <stories value="Update Customer Information in Admin"/> -+ <title value="Update Customer Address, default billing/shipping unchecked in Admin"/> -+ <description value="Update Customer Address, default billing/shipping unchecked in Admin"/> -+ <severity value="CRITICAL"/> -+ <testCaseId value="MC-13622"/> -+ <group value="Customer"/> -+ <group value="mtf_migrated"/> -+ </annotations> -+ <after> -+ <remove keyForRemoval="goToCustomersGridPage"/> -+ <remove keyForRemoval="waitForCustomersGrid"/> -+ <remove keyForRemoval="resetFilter"/> -+ </after> -+ -+ <!-- Remove steps that are not used for this test --> -+ <remove keyForRemoval="editCustomerInformation"/> -+ <remove keyForRemoval="goToCustomersGridPage"/> -+ <remove keyForRemoval="waitForCustomersGrid"/> -+ <remove keyForRemoval="resetFilter"/> -+ <remove keyForRemoval="filterByEamil"/> -+ <remove keyForRemoval="checkCustomerInGrid"/> -+ <remove keyForRemoval="checkCustomerAccountInformation"/> -+ <remove keyForRemoval="checkDefaultBilling"/> -+ <remove keyForRemoval="checkDefaultShipping"/> -+ -+ <!--Update Customer Addresses With Default Billing and Shipping Unchecked --> -+ <actionGroup stepKey="editCustomerAddress" ref="AdminEditCustomerAddressesFrom"> -+ <argument name="customerAddress" value="CustomerAddressSimple"/> -+ </actionGroup> -+ -+ <!-- Check Customer Address in Customer Form --> -+ <actionGroup stepKey="checkNoDefaultBilling" ref="AdminAssertCustomerNoDefaultBillingAddress" after="waitForCustomerEditPageAfterSave"/> -+ <actionGroup stepKey="checkNoDefaultShipping" ref="AdminAssertCustomerNoDefaultShippingAddress" after="checkNoDefaultBilling"/> -+ <actionGroup stepKey="resetFilter" ref="AdminResetFilterInCustomerAddressGrid" after="checkNoDefaultShipping"/> -+ <actionGroup stepKey="searchAddress" ref="AdminFilterCustomerAddressGridByPhoneNumber" after="resetFilter"> -+ <argument name="phone" value="{{CustomerAddressSimple.telephone}}"/> -+ </actionGroup> -+ <actionGroup stepKey="checkAddressStreetInGrid" ref="AdminAssertAddressInCustomersAddressGrid" after="searchAddress"> -+ <argument name="text" value="{{CustomerAddressSimple.street[0]}}"/> -+ </actionGroup> -+ <actionGroup stepKey="checkAddressPhoneInGrid" ref="AdminAssertAddressInCustomersAddressGrid" after="checkAddressStreetInGrid"> -+ <argument name="text" value="{{CustomerAddressSimple.telephone}}"/> -+ </actionGroup> -+ <actionGroup stepKey="checkAddressStateInGrid" ref="AdminAssertAddressInCustomersAddressGrid" after="checkAddressPhoneInGrid"> -+ <argument name="text" value="{{CustomerAddressSimple.state}}"/> -+ </actionGroup> -+ <actionGroup stepKey="checkAddressCityInGrid" ref="AdminAssertAddressInCustomersAddressGrid" after="checkAddressStateInGrid"> -+ <argument name="text" value="{{CustomerAddressSimple.city}}"/> -+ </actionGroup> -+ <actionGroup stepKey="checkAddressCountryInGrid" ref="AdminAssertAddressInCustomersAddressGrid" after="checkAddressCityInGrid"> -+ <argument name="text" value="{{CustomerAddressSimple.country_id}}"/> -+ </actionGroup> -+ <actionGroup stepKey="resetFilterWhenDone" ref="AdminResetFilterInCustomerAddressGrid" after="checkAddressCountryInGrid"/> -+ <!-- Assert Customer Login Storefront --> -+ <actionGroup stepKey="login" ref="StorefrontAssertSuccessLoginToStorefront" after="resetFilterWhenDone" > -+ <argument name="Customer" value="$$customer$$"/> -+ </actionGroup> -+ </test> -+ -+ <test name="AdminDeleteCustomerAddressTest"> -+ <annotations> -+ <features value="Customer"/> -+ <stories value="Delete Customer Address in Admin"/> -+ <title value="Delete Customer Address in Admin"/> -+ <description value="Delete Customer Address in Admin"/> -+ <severity value="CRITICAL"/> -+ <testCaseId value="MC-13623"/> -+ <group value="Customer"/> -+ <group value="mtf_migrated"/> -+ </annotations> -+ <before> -+ <createData stepKey="customer" entity="Simple_US_Customer_Multiple_Addresses"/> -+ <magentoCLI command="indexer:reindex" stepKey="reindex"/> -+ <actionGroup ref="LoginAsAdmin" stepKey="login"/> -+ </before> -+ <after> -+ <deleteData stepKey="deleteCustomer" createDataKey="customer"/> -+ <actionGroup ref="logout" stepKey="logout"/> -+ </after> -+ -+ <amOnPage url="{{AdminCustomerPage.url}}edit/id/$$customer.id$$/" stepKey="openCustomerEditPage"/> -+ <waitForPageLoad stepKey="waitForCustomerEditPage"/> -+ <!-- Assert Customer Default Billing Address --> -+ <actionGroup stepKey="checkDefaultBilling" ref="AdminAssertCustomerDefaultBillingAddress"> -+ <argument name="firstName" value="$$customer.firstname$$"/> -+ <argument name="lastName" value="$$customer.lastname$$"/> -+ <argument name="street1" value="{{US_Address_NY.street[0]}}"/> -+ <argument name="state" value="{{US_Address_NY.state}}"/> -+ <argument name="postcode" value="{{US_Address_NY.postcode}}"/> -+ <argument name="country" value="{{US_Address_NY.country}}"/> -+ <argument name="telephone" value="{{US_Address_NY.telephone}}"/> -+ </actionGroup> -+ <!-- Assert Customer Default Shipping Address --> -+ <actionGroup stepKey="checkDefaultShipping" ref="AdminAssertCustomerDefaultShippingAddress"> -+ <argument name="firstName" value="$$customer.firstname$$"/> -+ <argument name="lastName" value="$$customer.lastname$$"/> -+ <argument name="street1" value="{{US_Address_NY.street[0]}}"/> -+ <argument name="state" value="{{US_Address_NY.state}}"/> -+ <argument name="postcode" value="{{US_Address_NY.postcode}}"/> -+ <argument name="country" value="{{US_Address_NY.country}}"/> -+ <argument name="telephone" value="{{US_Address_NY.telephone}}"/> -+ </actionGroup> -+ <actionGroup stepKey="resetFilter" ref="AdminResetFilterInCustomerAddressGrid"/> -+ <!-- Assert 2 records in Customer Address Grid --> -+ <actionGroup stepKey="see2Record" ref="AdminAssertNumberOfRecordsInCustomersAddressGrid"> -+ <argument name="number" value="2"/> -+ </actionGroup> -+ <!-- Assert Address 1 in Grid --> -+ <actionGroup stepKey="checkAddressStreetInGrid" ref="AdminAssertAddressInCustomersAddressGrid"> -+ <argument name="text" value="{{US_Address_NY.street[0]}}"/> -+ </actionGroup> -+ <actionGroup stepKey="checkAddressPhoneInGrid" ref="AdminAssertAddressInCustomersAddressGrid"> -+ <argument name="text" value="{{US_Address_NY.telephone}}"/> -+ </actionGroup> -+ <actionGroup stepKey="checkAddressStateInGrid" ref="AdminAssertAddressInCustomersAddressGrid"> -+ <argument name="text" value="{{US_Address_NY.state}}"/> -+ </actionGroup> -+ <actionGroup stepKey="checkAddressCityInGrid" ref="AdminAssertAddressInCustomersAddressGrid"> -+ <argument name="text" value="{{US_Address_NY.city}}"/> -+ </actionGroup> -+ <actionGroup stepKey="checkAddressCountryInGrid" ref="AdminAssertAddressInCustomersAddressGrid"> -+ <argument name="text" value="{{US_Address_NY.country}}"/> -+ </actionGroup> -+ <!-- Assert Address 2 in Grid --> -+ <actionGroup stepKey="checkAddressStreetInGrid2" ref="AdminAssertAddressInCustomersAddressGrid"> -+ <argument name="text" value="{{UK_Not_Default_Address.street[0]}}"/> -+ </actionGroup> -+ <actionGroup stepKey="checkAddressPhoneInGrid2" ref="AdminAssertAddressInCustomersAddressGrid"> -+ <argument name="text" value="{{UK_Not_Default_Address.telephone}}"/> -+ </actionGroup> -+ <actionGroup stepKey="checkAddressCityInGrid2" ref="AdminAssertAddressInCustomersAddressGrid"> -+ <argument name="text" value="{{UK_Not_Default_Address.city}}"/> -+ </actionGroup> -+ <!-- Delete Customer in Customer Address Grid --> -+ <actionGroup stepKey="deleteAddress" ref="AdminDeleteAddressInCustomersAddressGrid"> -+ <argument name="row" value="0"/> -+ </actionGroup> -+ <!-- Assert 1 record in Customer Address Grid --> -+ <actionGroup stepKey="see1Record" ref="AdminAssertNumberOfRecordsInCustomersAddressGrid"> -+ <argument name="number" value="1"/> -+ </actionGroup> -+ <actionGroup stepKey="saveAndContinue" ref="AdminCustomerSaveAndContinue"/> -+ <actionGroup stepKey="saveAndCheckSuccessMessage" ref="AdminSaveCustomerAndAssertSuccessMessage"/> -+ <!-- Assert Customer Login Storefront --> -+ <actionGroup stepKey="login" ref="StorefrontAssertSuccessLoginToStorefront"> -+ <argument name="Customer" value="$$customer$$"/> -+ </actionGroup> -+ <!-- Assert Customer Address Book --> -+ <actionGroup stepKey="goToAddressBook" ref="StorefrontCustomerGoToSidebarMenu"> -+ <argument name="menu" value="Address Book"/> -+ </actionGroup> -+ <actionGroup stepKey="assertAddressNumber" ref="StorefrontCustomerAddressBookNumberOfAddresses"> -+ <argument name="number" value="1"/> -+ </actionGroup> -+ <actionGroup stepKey="assertNoAddress1" ref="StorefrontCustomerAddressBookNotContains"> -+ <argument name="text" value="{{US_Address_NY.street[0]}}"/> -+ </actionGroup> -+ <actionGroup stepKey="assertAddress2" ref="StorefrontCustomerAddressBookContains"> -+ <argument name="text" value="{{UK_Not_Default_Address.street[0]}}"/> -+ </actionGroup> -+ </test> -+</tests> -\ No newline at end of file -diff --git a/app/code/Magento/Customer/Test/Mftf/Test/AdminVerifyCreateCustomerRequiredFieldsTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/AdminVerifyCreateCustomerRequiredFieldsTest.xml -new file mode 100644 -index 00000000000..7dab6eefde8 ---- /dev/null -+++ b/app/code/Magento/Customer/Test/Mftf/Test/AdminVerifyCreateCustomerRequiredFieldsTest.xml -@@ -0,0 +1,39 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> -+ <test name="AdminVerifyCreateCustomerRequiredFieldsTest"> -+ <annotations> -+ <stories value="Create customer"/> -+ <title value="Create customer, verify required fields on Account Information tab"/> -+ <description value="Login as admin and verify required fields on account information section"/> -+ <severity value="CRITICAL"/> -+ <testCaseId value="MC-5314"/> -+ <group value="mtf_migrated"/> -+ </annotations> -+ -+ <before> -+ <actionGroup ref="LoginAsAdmin" stepKey="loginToAdminPanel"/> -+ </before> -+ <after> -+ <actionGroup ref="logout" stepKey="logout"/> -+ </after> -+ -+ <!--Open New Customer Page --> -+ <amOnPage url="{{AdminNewCustomerPage.url}}" stepKey="navigateToNewCustomerPage"/> -+ <waitForPageLoad stepKey="waitToCustomerPageLoad"/> -+ <click selector="{{AdminCustomerMainActionsSection.saveButton}}" stepKey="saveCustomer"/> -+ <waitForPageLoad stepKey="waitForPageToLoad"/> -+ -+ <!--Assert Required Fields --> -+ <seeElement selector="{{AdminCustomerAccountInformationSection.firstNameRequiredMessage}}" stepKey="seeFirstNameRequiredFieldMessage"/> -+ <seeElement selector="{{AdminCustomerAccountInformationSection.lastNameRequiredMessage}}" stepKey="seeLastNameRequiredFieldMessage"/> -+ <seeElement selector="{{AdminCustomerAccountInformationSection.emailRequiredMessage}}" stepKey="seeEmailRequiredFieldMessage"/> -+ </test> -+</tests> -diff --git a/app/code/Magento/Customer/Test/Mftf/Test/AdminVerifyCustomerAddressRequiredFieldsTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/AdminVerifyCustomerAddressRequiredFieldsTest.xml -new file mode 100644 -index 00000000000..bfb47dc9e19 ---- /dev/null -+++ b/app/code/Magento/Customer/Test/Mftf/Test/AdminVerifyCustomerAddressRequiredFieldsTest.xml -@@ -0,0 +1,49 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> -+ <test name="AdminVerifyCustomerAddressRequiredFieldsTest"> -+ <annotations> -+ <stories value="Create customer"/> -+ <title value="Create customer, verify required fields on Addresses tab"/> -+ <description value="Login as admin and verify required fields on Address tab"/> -+ <severity value="CRITICAL"/> -+ <testCaseId value="MC-5315"/> -+ <group value="mtf_migrated"/> -+ </annotations> -+ -+ <before> -+ <createData entity="Simple_Customer_Without_Address" stepKey="createCustomer"/> -+ <actionGroup ref="LoginAsAdmin" stepKey="loginToAdminPanel"/> -+ </before> -+ <after> -+ <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> -+ <actionGroup ref="logout" stepKey="logout"/> -+ </after> -+ -+ <!--Open Created Customer --> -+ <actionGroup ref="OpenEditCustomerFromAdminActionGroup" stepKey="editCustomerForm"> -+ <argument name="customer" value="Simple_Customer_Without_Address"/> -+ </actionGroup> -+ <click selector="{{AdminCustomerAccountInformationSection.addressesButton}}" stepKey="openAddressesTab"/> -+ <waitForPageLoad stepKey="waitForPageToLoad"/> -+ <click selector="{{AdminEditCustomerAddressesSection.addNewAddress}}" stepKey="ClickOnAddNewAddressButton"/> -+ <waitForPageLoad stepKey="waitForAdressPageToLoad"/> -+ <click selector="{{AdminEditCustomerAddressesSection.save}}" stepKey="clickOnSaveButton"/> -+ <waitForPageLoad stepKey="waitForPageToBeSaved"/> -+ -+ <!--Assert Required Field Messages --> -+ <seeElement selector="{{AdminCustomerAddressesSection.streetRequiredMessage}}" stepKey="seeStreetRequiredMessage"/> -+ <seeElement selector="{{AdminCustomerAddressesSection.cityRequiredMessage}}" stepKey="seeCityRequiredMessage"/> -+ <scrollTo selector="{{AdminEditCustomerAddressesSection.phone}}" x="0" y="-80" stepKey="scrollToPhone"/> -+ <seeElement selector="{{AdminCustomerAddressesSection.countryRequiredMessage}}" stepKey="seeCountryRequiredMessage"/> -+ <seeElement selector="{{AdminCustomerAddressesSection.postcodeRequiredMessage}}" stepKey="seePostcodeRequiredMessage"/> -+ <seeElement selector="{{AdminCustomerAddressesSection.phoneNumberRequiredMessage}}" stepKey="seePhoneNumberRequiredMessage"/> -+ </test> -+</tests> -diff --git a/app/code/Magento/Customer/Test/Mftf/Test/AdminVerifyCustomerAddressStateContainValuesOnceTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/AdminVerifyCustomerAddressStateContainValuesOnceTest.xml -new file mode 100644 -index 00000000000..daab5fd2061 ---- /dev/null -+++ b/app/code/Magento/Customer/Test/Mftf/Test/AdminVerifyCustomerAddressStateContainValuesOnceTest.xml -@@ -0,0 +1,73 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> -+ <test name="AdminVerifyCustomerAddressStateContainValuesOnceTest"> -+ <annotations> -+ <features value="Customer"/> -+ <stories value="Update Customer Address"/> -+ <title value="State/Province dropdown contain values once"/> -+ <description value="When editing a customer in the backend from the Magento Admin Panel the State/Province should only be listed once"/> -+ <severity value="MAJOR"/> -+ <testCaseId value="MAGETWO-99461"/> -+ <useCaseId value="MAGETWO-99302"/> -+ <group value="customer"/> -+ </annotations> -+ <before> -+ <createData entity="Simple_US_Customer_Multiple_Addresses" stepKey="firstCustomer"/> -+ <actionGroup ref="LoginAsAdmin" stepKey="login"/> -+ </before> -+ <after> -+ <deleteData createDataKey="firstCustomer" stepKey="deleteFirstCustomer"/> -+ <actionGroup ref="AdminDeleteCustomerActionGroup" stepKey="deleteSecondCustomer"> -+ <argument name="customerEmail" value="Simple_US_Customer.email"/> -+ </actionGroup> -+ <actionGroup ref="AdminClearCustomersFiltersActionGroup" stepKey="clearFilters"/> -+ <actionGroup ref="logout" stepKey="logout"/> -+ </after> -+ -+ <!-- Go to Customers > All Customers.--> -+ <amOnPage url="{{AdminCustomerPage.url}}" stepKey="openCustomersGridPage"/> -+ -+ <!--Select created customer, Click Edit mode--> -+ <actionGroup ref="OpenEditCustomerFromAdminActionGroup" stepKey="openEditCustomerPageWithAddresses"> -+ <argument name="customer" value="$$firstCustomer$$"/> -+ </actionGroup> -+ -+ <!--Select Addresses tab--> -+ <click selector="{{AdminEditCustomerInformationSection.addresses}}" stepKey="openAddressesTabOfFirstCustomer"/> -+ <waitForPageLoad stepKey="waitForAddressesOfFirstCustomer"/> -+ -+ <!--Click on Edit link for Default Billing Address--> -+ <click selector="{{AdminCustomerAddressesDefaultBillingSection.editButton}}" stepKey="clickEditDefaultBillingAddress"/> -+ <waitForPageLoad stepKey="waitForCustomerAddressAddUpdateFormLoad"/> -+ -+ <!--Check that State/Province drop down contain all values once--> -+ <seeNumberOfElements userInput="1" selector="{{AdminCustomerAddressesSection.regionId(US_Address_NY.state)}}" stepKey="seeOnlyOneRegionInSelectStateForFirstCustomer"/> -+ -+ <!--Go to Customers > All customers, Click Add new Customers, fill all necessary fields, Save--> -+ <actionGroup ref="AdminCreateCustomerWithWebSiteAndGroup" stepKey="createSimpleUSCustomerWithoutAddress"/> -+ -+ <!--Select new created customer, Click Edit mode--> -+ <actionGroup ref="OpenEditCustomerFromAdminActionGroup" stepKey="openEditCustomerPageWithoutAddresses"> -+ <argument name="customer" value="Simple_US_Customer"/> -+ </actionGroup> -+ -+ <!--Select Addresses tab, Click on create new addresses btn--> -+ <click selector="{{AdminEditCustomerInformationSection.addresses}}" stepKey="openAddressesTabOfSecondCustomer"/> -+ <waitForPageLoad stepKey="waitForAddressesOfSecondCustomer"/> -+ <click selector="{{AdminCustomerAddressesSection.addNewAddress}}" stepKey="clickAddNewAddressButton"/> -+ <waitForPageLoad stepKey="waitForAddUpdateCustomerAddressForm"/> -+ -+ <!--Select Country = United States and check that State/Province drop down contain all values once--> -+ <click selector="{{AdminCustomerAddressesSection.country}}" stepKey="clickCountryToOpenListOfCountries"/> -+ <click selector="{{AdminCustomerAddressesSection.countryId(US_Address_NY.country_id)}}" stepKey="fillCountry"/> -+ <seeNumberOfElements userInput="1" selector="{{AdminCustomerAddressesSection.regionId(US_Address_NY.state)}}" stepKey="seeOnlyOneRegionInSelectStateForSecondCustomer"/> -+ </test> -+</tests> -diff --git a/app/code/Magento/Customer/Test/Mftf/Test/AllowedCountriesRestrictionApplyOnBackendTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/AllowedCountriesRestrictionApplyOnBackendTest.xml -new file mode 100644 -index 00000000000..6de03e225ae ---- /dev/null -+++ b/app/code/Magento/Customer/Test/Mftf/Test/AllowedCountriesRestrictionApplyOnBackendTest.xml -@@ -0,0 +1,119 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> -+ <test name="AllowedCountriesRestrictionApplyOnBackendTest"> -+ <annotations> -+ <features value="Customer"/> -+ <stories value="Country filter"/> -+ <title value="Country filter on Customers page when allowed countries restriction for a default website is applied"/> -+ <description value="Country filter on Customers page when allowed countries restriction for a default website is applied"/> -+ <severity value="MAJOR"/> -+ <testCaseId value="MC-6441"/> -+ <useCaseId value="MAGETWO-91523"/> -+ <group value="customer"/> -+ </annotations> -+ <before> -+ <createData entity="ApiCategory" stepKey="createCategory"/> -+ <createData entity="SimpleProduct" stepKey="createSimpleProduct"> -+ <requiredEntity createDataKey="createCategory"/> -+ </createData> -+ <actionGroup ref="LoginActionGroup" stepKey="login"/> -+ <!--Create new website,store and store view--> -+ <comment userInput="Create new website,store and store view" stepKey="createWebsite"/> -+ <amOnPage url="{{AdminSystemStorePage.url}}" stepKey="goToAdminSystemStorePage"/> -+ <actionGroup ref="AdminCreateWebsiteActionGroup" stepKey="adminCreateNewWebsite"> -+ <argument name="newWebsiteName" value="{{NewWebSiteData.name}}"/> -+ <argument name="websiteCode" value="{{NewWebSiteData.code}}"/> -+ </actionGroup> -+ <actionGroup ref="AdminCreateNewStoreGroupActionGroup" stepKey="adminCreateNewStore"> -+ <argument name="website" value="{{NewWebSiteData.name}}"/> -+ <argument name="storeGroupName" value="{{NewStoreData.name}}"/> -+ <argument name="storeGroupCode" value="{{NewStoreData.code}}"/> -+ </actionGroup> -+ <actionGroup ref="AdminCreateStoreViewActionGroup" stepKey="adminCreateNewStoreView"> -+ <argument name="StoreGroup" value="NewStoreData"/> -+ </actionGroup> -+ <!--Set account sharing option - Default value is 'Per Website'--> -+ <comment userInput="Set account sharing option - Default value is 'Per Website'" stepKey="setAccountSharingOption"/> -+ <createData entity="CustomerAccountSharingDefault" stepKey="setToAccountSharingToDefault"/> -+ <magentoCLI command="indexer:reindex" stepKey="reindex"/> -+ </before> -+ <after> -+ <!--delete all created data and set main website country options to default--> -+ <comment userInput="Delete all created data and set main website country options to default" stepKey="resetConfigToDefault"/> -+ <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> -+ <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> -+ <deleteData createDataKey="createSimpleProduct" stepKey="deleteSimpleProduct"/> -+ <actionGroup ref="AdminDeleteWebsiteActionGroup" stepKey="deleteTestWebsite"> -+ <argument name="websiteName" value="{{NewWebSiteData.name}}"/> -+ </actionGroup> -+ <actionGroup ref="NavigateToConfigurationGeneralPage" stepKey="navigateToConfigGeneralPage2"/> -+ <actionGroup ref="AdminSwitchWebsiteActionGroup" stepKey="adminSwitchWebsiteActionGroup"> -+ <argument name="website" value="_defaultWebsite"/> -+ </actionGroup> -+ <actionGroup ref="SetWebsiteCountryOptionsToDefaultActionGroup" stepKey="setCountryOptionsToDefault"/> -+ <createData entity="CustomerAccountSharingSystemValue" stepKey="setAccountSharingToSystemValue"/> -+ <actionGroup ref="logout" stepKey="logout"/> -+ <magentoCLI command="indexer:reindex" stepKey="reindex"/> -+ <magentoCLI command="cache:flush" stepKey="flushCache"/> -+ </after> -+ <!--Check that all countries are allowed initially and get amount--> -+ <comment userInput="Check that all countries are allowed initially and get amount" stepKey="checkAllCountriesAreAllowed"/> -+ <actionGroup ref="NavigateToConfigurationGeneralPage" stepKey="navigateToConfigGeneralPage"/> -+ <createData entity="DisableAdminAccountAllowCountry" stepKey="setDefaultValueForAllowCountries"/> -+ <executeJS function="return document.querySelectorAll('{{CountryOptionsSection.allowedCountries}} option').length" stepKey="countriesAmount"/> -+ <!-- Create customer for US --> -+ <comment userInput="Create customer for US" stepKey="createUSCustomer"/> -+ <createData entity="Simple_US_CA_Customer" stepKey="createCustomer"/> -+ <!-- Switch to first website, allow only Canada and set Canada as default country --> -+ <comment userInput="Switch to first website, allow only Canada and set Canada as default country" stepKey="setCanadaAsDefaultCountry"/> -+ <actionGroup ref="AdminSwitchWebsiteActionGroup" stepKey="adminSwitchWebsiteActionGroup"> -+ <argument name="website" value="_defaultWebsite"/> -+ </actionGroup> -+ <conditionalClick selector="{{CountryOptionsSection.countryOptions}}" dependentSelector="{{CountryOptionsSection.countryOptionsOpen}}" visible="false" stepKey="clickOnStoreInformation2"/> -+ <waitForElementVisible selector="{{CountryOptionsSection.allowedCountries}}" stepKey="waitAllowedCountriesToBeVisible"/> -+ <uncheckOption selector="{{CountryOptionsSection.generalCountryDefaultInherit}}" stepKey="uncheckCheckbox1"/> -+ <selectOption selector="{{CountryOptionsSection.generalCountryDefault}}" userInput="Canada" stepKey="chooseCanada1"/> -+ <uncheckOption selector="{{CountryOptionsSection.generalCountryAllowInherit}}" stepKey="uncheckCheckbox2"/> -+ <selectOption selector="{{CountryOptionsSection.allowedCountries}}" userInput="Canada" stepKey="chooseCanada2"/> -+ <click selector="{{ContentManagementSection.Save}}" stepKey="saveConfig2"/> -+ <waitForPageLoad stepKey="waitForSavingSystemConfiguration2"/> -+ <!--Switch to second website and allow all countries except Canada--> -+ <comment userInput="Switch to second website and allow all countries except Canada" stepKey="switchToWebsiteAndAllowOnlyCanada"/> -+ <actionGroup ref="AdminSwitchWebsiteActionGroup" stepKey="adminSwitchWebsiteActionGroup2"> -+ <argument name="website" value="NewWebSiteData"/> -+ </actionGroup> -+ <conditionalClick selector="{{CountryOptionsSection.countryOptions}}" dependentSelector="{{CountryOptionsSection.countryOptionsOpen}}" visible="false" stepKey="clickOnStoreInformation3"/> -+ <waitForElementVisible selector="{{CountryOptionsSection.allowedCountries}}" stepKey="waitAllowedCountriesToBeVisible2"/> -+ <uncheckOption selector="{{CountryOptionsSection.generalCountryAllowInherit}}" stepKey="uncheckCheckbox3"/> -+ <unselectOption selector="{{CountryOptionsSection.allowedCountries}}" userInput="Canada" stepKey="unselectCanada"/> -+ <click selector="{{ContentManagementSection.Save}}" stepKey="saveConfig3"/> -+ <waitForPageLoad stepKey="waitForSavingSystemConfiguration3"/> -+ <amOnPage url="{{AdminEditCustomerPage.url($$createCustomer.id$$)}}" stepKey="goToCustomerEditPage"/> -+ <waitForPageLoad stepKey="waitPageToLoad"/> -+ <!--Open created customer details page and change US address to Canada address--> -+ <comment userInput="Open created customer details page and change US address to Canada address" stepKey="changeCustomerAddressToCanada"/> -+ <actionGroup ref="OpenEditCustomerAddressFromAdminActionGroup" stepKey="editCustomerAddress"> -+ <argument name="address" value="US_Address_CA"/> -+ </actionGroup> -+ <selectOption selector="{{AdminEditCustomerAddressesSection.country}}" userInput="Canada" stepKey="selectCountry"/> -+ <selectOption selector="{{AdminEditCustomerAddressesSection.state}}" userInput="Quebec" stepKey="selectState"/> -+ <click selector="{{AdminEditCustomerAddressesSection.save}}" stepKey="saveAddress"/> -+ <waitForPageLoad stepKey="waitForAddressSaved"/> -+ <click stepKey="saveCustomer" selector="{{AdminCustomerAccountInformationSection.saveCustomerAndContinueEdit}}"/> -+ <waitForPageLoad stepKey="waitForCustomersPage"/> -+ <!--Go to Customers grid and check that filter countries amount is the same as initial allowed countries amount--> -+ <comment userInput="Go to Customers grid and check that filter countries amount is the same as initial allowed countries amount" stepKey="compareCountriesAmount"/> -+ <amOnPage url="{{AdminCustomerPage.url}}" stepKey="goToCustomersGrid"/> -+ <waitForPageLoad stepKey="waitForCustomersGrid"/> -+ <click selector="{{AdminDataGridHeaderSection.filters}}" stepKey="openFiltersSectionOnCustomersGrid"/> -+ <executeJS function="var len = document.querySelectorAll('{{AdminCustomerFiltersSection.countryOptions}}').length; return len-1;" stepKey="countriesAmount2"/> -+ <assertEquals expected="($countriesAmount)" actual="($countriesAmount2)" stepKey="assertCountryAmounts"/> -+ </test> -+</tests> -diff --git a/app/code/Magento/Customer/Test/Mftf/Test/ChangeCustomerGroupTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/ChangeCustomerGroupTest.xml -new file mode 100644 -index 00000000000..e35a1ad61dc ---- /dev/null -+++ b/app/code/Magento/Customer/Test/Mftf/Test/ChangeCustomerGroupTest.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="ChangingSingleCustomerGroupViaGrid"> -+ <annotations> -+ <title value="Change a single customer group via grid"/> -+ <description value="From the selection of All Customers select a single customer to change their group"/> -+ <severity value="MAJOR"/> -+ <testCaseId value="MC-10921"/> -+ <stories value="Change Customer Group"/> -+ <group value="customer"/> -+ <group value="mtf_migrated"/> -+ </annotations> -+ -+ <before> -+ <createData entity="Simple_US_Customer" stepKey="createCustomer"/> -+ -+ <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> -+ </before> -+ <after> -+ <!--Delete created product--> -+ <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> -+ <actionGroup ref="NavigateToCustomerGroupPage" stepKey="navToCustomers"/> -+ <actionGroup ref="AdminDeleteCustomerGroupActionGroup" stepKey="deleteCustomerGroup"> -+ <argument name="customerGroupName" value="{{CustomerGroupChange.code}}"/> -+ </actionGroup> -+ -+ <actionGroup ref="logout" stepKey="logout"/> -+ </after> -+ -+ <actionGroup ref="AdminCreateCustomerGroupActionGroup" stepKey="createCustomerGroup"> -+ <argument name="groupName" value="{{CustomerGroupChange.code}}"/> -+ <argument name="taxClass" value="{{CustomerGroupChange.tax_class_name}}"/> -+ </actionGroup> -+ <actionGroup ref="NavigateToAllCustomerPage" stepKey="navToCustomers"/> -+ <actionGroup ref="AdminFilterCustomerByName" stepKey="filterCustomer"> -+ <argument name="customerName" value="{{Simple_US_Customer.fullname}}"/> -+ </actionGroup> -+ <actionGroup ref="AdminSelectCustomerByEmail" stepKey="selectCustomer"> -+ <argument name="customerEmail" value="$$createCustomer.email$$"/> -+ </actionGroup> -+ <actionGroup ref="SetCustomerGroupForSelectedCustomersViaGrid" stepKey="setCustomerGroup"> -+ <argument name="groupName" value="{{CustomerGroupChange.code}}"/> -+ </actionGroup> -+ <actionGroup ref="AdminFilterCustomerByName" stepKey="filterCustomerAfterGroupChange"> -+ <argument name="customerName" value="{{Simple_US_Customer.fullname}}"/> -+ </actionGroup> -+ <actionGroup ref="VerifyCustomerGroupForCustomer" stepKey="verifyCustomerGroupSet"> -+ <argument name="customerEmail" value="$$createCustomer.email$$"/> -+ <argument name="groupName" value="{{CustomerGroupChange.code}}"/> -+ </actionGroup> -+ </test> -+ -+ <test name="ChangingAllCustomerGroupViaGrid" extends="ChangingSingleCustomerGroupViaGrid"> -+ <annotations> -+ <title value="Change all customers' group via grid"/> -+ <description value="Select All customers to change their group"/> -+ <severity value="MAJOR"/> -+ <testCaseId value="MC-10924"/> -+ <stories value="Change Customer Group"/> -+ <group value="customer"/> -+ <group value="mtf_migrated"/> -+ <skip> -+ <issueId value="MC-17140"/> -+ </skip> -+ </annotations> -+ -+ <remove keyForRemoval="filterCustomer"/> -+ <actionGroup ref="clearFiltersAdminDataGrid" stepKey="clearFilters" before="selectCustomer"/> -+ <actionGroup ref="AdminSelectAllCustomers" stepKey="selectCustomer"/> -+ </test> -+</tests> -diff --git a/app/code/Magento/Customer/Test/Mftf/Test/DeleteCustomerGroupTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/DeleteCustomerGroupTest.xml -new file mode 100644 -index 00000000000..b19966e0102 ---- /dev/null -+++ b/app/code/Magento/Customer/Test/Mftf/Test/DeleteCustomerGroupTest.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="DeleteCustomerGroupTest"> -+ <annotations> -+ <features value="Customer"/> -+ <stories value="Delete Customer Group"/> -+ <title value="Delete Customer Group in Admin Panel"/> -+ <description value="Admin should be able to delete a Customer Group"/> -+ <testCaseId value="MC-14590"/> -+ <severity value="MAJOR"/> -+ <group value="customers"/> -+ <group value="mtf_migrated"/> -+ </annotations> -+ -+ <before> -+ <createData entity="CustomCustomerGroup" stepKey="customerGroup"/> -+ <createData entity="UsCustomerAssignedToNewCustomerGroup" stepKey="customer"> -+ <requiredEntity createDataKey="customerGroup"/> -+ </createData> -+ <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> -+ </before> -+ <after> -+ <deleteData createDataKey="customer" stepKey="deleteCustomer"/> -+ <actionGroup ref="logout" stepKey="logout"/> -+ </after> -+ -+ <!--Customer Group success delete message--> -+ <actionGroup ref="AdminDeleteCustomerGroupActionGroup" stepKey="deleteCustomerGroup"> -+ <argument name="customerGroupName" value="$$customerGroup.code$$"/> -+ </actionGroup> -+ <actionGroup ref="AssertCustomerGroupNotInGridActionGroup" stepKey="assertCustomerGroupNotInGrid"> -+ <argument name="customerGroup" value="$$customerGroup$$"/> -+ </actionGroup> -+ -+ <actionGroup ref="AdminOpenCustomerEditPageActionGroup" stepKey="openCustomerEditPage"> -+ <argument name="customerId" value="$$customer.id$$"/> -+ </actionGroup> -+ -+ <actionGroup ref="AssertCustomerGroupOnCustomerFormActionGroup" stepKey="assertCustomerGroupOnCustomerForm"> -+ <argument name="customerGroup" value="GeneralCustomerGroup"/> -+ </actionGroup> -+ -+ <actionGroup ref="AdminOpenNewProductFormPageActionGroup" stepKey="openNewProductForm"/> -+ -+ <actionGroup ref="AssertCustomerGroupNotOnProductFormActionGroup" stepKey="assertCustomerGroupNotOnProductForm"> -+ <argument name="customerGroup" value="$$customerGroup$$"/> -+ </actionGroup> -+ </test> -+</tests> -diff --git a/app/code/Magento/Customer/Test/Mftf/Test/EndToEndB2CLoggedInUserTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/EndToEndB2CLoggedInUserTest.xml -index a9563c4cc93..2b24233e8b0 100644 ---- a/app/code/Magento/Customer/Test/Mftf/Test/EndToEndB2CLoggedInUserTest.xml -+++ b/app/code/Magento/Customer/Test/Mftf/Test/EndToEndB2CLoggedInUserTest.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="EndToEndB2CLoggedInUserTest"> - <annotations> - <features value="End to End scenarios"/> -@@ -17,6 +17,9 @@ - <description value="New user signup and browses catalog, searches for product, adds product to cart, adds product to wishlist, compares products, uses coupon code and checks out."/> - <severity value="CRITICAL"/> - <testCaseId value="MAGETWO-87653"/> -+ <skip> -+ <issueId value="MC-17140"/> -+ </skip> - </annotations> - <before> - <resetCookie userInput="PHPSESSID" stepKey="resetCookieForCart"/> -diff --git a/app/code/Magento/Customer/Test/Mftf/Test/PasswordAutocompleteOffTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/PasswordAutocompleteOffTest.xml -new file mode 100644 -index 00000000000..f364d24806b ---- /dev/null -+++ b/app/code/Magento/Customer/Test/Mftf/Test/PasswordAutocompleteOffTest.xml -@@ -0,0 +1,61 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> -+ <test name="PasswordAutocompleteOffTest"> -+ <annotations> -+ <features value="Customer"/> -+ <stories value="Customer Password Autocomplete"/> -+ <title value="[Security] Autocomplete attribute with off value is added to password input"/> -+ <description value="[Security] Autocomplete attribute with off value is added to password input"/> -+ <testCaseId value="MC-13678"/> -+ <severity value="CRITICAL"/> -+ <group value="customers"/> -+ <group value="mtf_migrated"/> -+ </annotations> -+ <before> -+ <!-- Configure Magento via CLI: disable_guest_checkout --> -+ <magentoCLI command="config:set checkout/options/guest_checkout 0" stepKey="disableGuestCheckout"/> -+ -+ <!-- Configure Magento via CLI: password_autocomplete_off--> -+ <magentoCLI command="config:set customer/password/autocomplete_on_storefront 0" stepKey="turnPasswordAutocompleteOff"/> -+ -+ <!-- Create a simple product --> -+ <createData entity="SimpleSubCategory" stepKey="category"/> -+ <createData entity="SimpleProduct" stepKey="product"> -+ <requiredEntity createDataKey="category"/> -+ </createData> -+ </before> -+ <after> -+ <!-- Set Magento configuration back to default values --> -+ <magentoCLI command="config:set checkout/options/guest_checkout 1" stepKey="disableGuestCheckoutRollback"/> -+ <magentoCLI command="config:set customer/password/autocomplete_on_storefront 1" stepKey="turnPasswordAutocompleteOffRollback"/> -+ -+ <!-- Delete the simple product created in the before block --> -+ <deleteData createDataKey="product" stepKey="deleteProduct"/> -+ <deleteData createDataKey="category" stepKey="deleteCategory"/> -+ </after> -+ -+ <!-- Go to the created product page and add it to the cart--> -+ <actionGroup ref="AddSimpleProductToCart" stepKey="cartAddSimpleProductToCart"> -+ <argument name="product" value="$$product$$"/> -+ </actionGroup> -+ -+ <!--Click Sign in - on the top right of the page --> -+ <actionGroup ref="StorefrontClickSignInButtonActionGroup" stepKey="storeFrontClickSignInButton"/> -+ -+ <!--Verify if the password field on store front sign-in page has the autocomplete attribute set to off --> -+ <actionGroup ref="AssertStorefrontPasswordAutoCompleteOffActionGroup" stepKey="assertStorefrontPasswordAutoCompleteOff"/> -+ -+ <!--Proceed to checkout--> -+ <actionGroup ref="GoToCheckoutFromMinicartActionGroup" stepKey="goToCheckoutFromMinicart"/> -+ -+ <!--Verify if the password field on the authorization popup has the autocomplete attribute set to off --> -+ <actionGroup ref="AssertAuthorizationPopUpPasswordAutoCompleteOffActionGroup" stepKey="assertAuthorizationPopUpPasswordAutoCompleteOff"/> -+ </test> -+</tests> -diff --git a/app/code/Magento/Customer/Test/Mftf/Test/StorefrontAddCustomerAddressTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/StorefrontAddCustomerAddressTest.xml -new file mode 100644 -index 00000000000..413bbfd06a5 ---- /dev/null -+++ b/app/code/Magento/Customer/Test/Mftf/Test/StorefrontAddCustomerAddressTest.xml -@@ -0,0 +1,121 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> -+ <test name="StorefrontAddNewCustomerAddressTest"> -+ <annotations> -+ <features value="Customer address"/> -+ <stories value="Implement handling of large number of addresses on storefront Address book"/> -+ <title value="Storefront - My account - Address Book - add new address"/> -+ <description value="Storefront user should be able to create a new address via the storefront"/> -+ <severity value="MAJOR"/> -+ <testCaseId value="MAGETWO-97364"/> -+ <group value="customer"/> -+ <group value="create"/> -+ </annotations> -+ <before> -+ <createData entity="Simple_Customer_Without_Address" stepKey="createCustomer"/> -+ </before> -+ <after> -+ <deleteData createDataKey="createCustomer" stepKey="DeleteCustomer"/> -+ <amOnPage url="admin/admin/auth/logout/" stepKey="AmOnLogoutPage"/> -+ </after> -+ -+ <!--Log in to Storefront as Customer 1 --> -+ <actionGroup ref="LoginToStorefrontActionGroup" stepKey="signUp"> -+ <argument name="Customer" value="$$createCustomer$$"/> -+ </actionGroup> -+ <actionGroup ref="StorefrontAddNewCustomerAddressActionGroup" stepKey="AddNewAddress"> -+ <argument name="Address" value="US_Address_TX"/> -+ </actionGroup> -+ <see userInput="{{US_Address_TX.street[0]}}" -+ selector="{{StorefrontCustomerAddressesSection.defaultBillingAddress}}" stepKey="checkNewAddressesStreetOnDefaultBilling"/> -+ <see userInput="{{US_Address_TX.city}}" -+ selector="{{StorefrontCustomerAddressesSection.defaultBillingAddress}}" stepKey="checkNewAddressesCityOnDefaultBilling"/> -+ <see userInput="{{US_Address_TX.postcode}}" -+ selector="{{StorefrontCustomerAddressesSection.defaultBillingAddress}}" stepKey="checkNewAddressesPostcodeOnDefaultBilling"/> -+ <see userInput="{{US_Address_TX.street[0]}}" -+ selector="{{StorefrontCustomerAddressesSection.defaultShippingAddress}}" stepKey="checkNewAddressesStreetOnDefaultShipping"/> -+ <see userInput="{{US_Address_TX.city}}" -+ selector="{{StorefrontCustomerAddressesSection.defaultShippingAddress}}" stepKey="checkNewAddressesCityOnDefaultShipping"/> -+ <see userInput="{{US_Address_TX.postcode}}" -+ selector="{{StorefrontCustomerAddressesSection.defaultShippingAddress}}" stepKey="checkNewAddressesPostcodeOnDefaultShipping"/> -+ </test> -+ <test name="StorefrontAddCustomerDefaultAddressTest"> -+ <annotations> -+ <features value="Customer address"/> -+ <stories value="Implement handling of large number of addresses on storefront Address book"/> -+ <title value="Storefront - My account - Address Book - add new default billing/shipping address"/> -+ <description value="Storefront user should be able to create a new default address via the storefront"/> -+ <severity value="MAJOR"/> -+ <testCaseId value="MAGETWO-97364"/> -+ <group value="customer"/> -+ <group value="create"/> -+ </annotations> -+ <before> -+ <createData entity="Simple_US_Customer" stepKey="createCustomer"/> -+ </before> -+ <after> -+ <deleteData createDataKey="createCustomer" stepKey="DeleteCustomer"/> -+ </after> -+ -+ <!--Log in to Storefront as Customer 1 --> -+ <actionGroup ref="LoginToStorefrontActionGroup" stepKey="signUp"> -+ <argument name="Customer" value="$$createCustomer$$"/> -+ </actionGroup> -+ <actionGroup ref="StorefrontAddCustomerDefaultAddressActionGroup" stepKey="AddNewDefaultAddress"> -+ <argument name="Address" value="US_Address_TX"/> -+ </actionGroup> -+ <see userInput="{{US_Address_TX.street[0]}}" -+ selector="{{StorefrontCustomerAddressesSection.defaultBillingAddress}}" stepKey="checkNewAddressesStreetOnDefaultBilling"/> -+ <see userInput="{{US_Address_TX.city}}" -+ selector="{{StorefrontCustomerAddressesSection.defaultBillingAddress}}" stepKey="checkNewAddressesCityOnDefaultBilling"/> -+ <see userInput="{{US_Address_TX.postcode}}" -+ selector="{{StorefrontCustomerAddressesSection.defaultBillingAddress}}" stepKey="checkNewAddressesPostcodeOnDefaultBilling"/> -+ <see userInput="{{US_Address_TX.street[0]}}" -+ selector="{{StorefrontCustomerAddressesSection.defaultShippingAddress}}" stepKey="checkNewAddressesStreetOnDefaultShipping"/> -+ <see userInput="{{US_Address_TX.city}}" -+ selector="{{StorefrontCustomerAddressesSection.defaultShippingAddress}}" stepKey="checkNewAddressesCityOnDefaultShipping"/> -+ <see userInput="{{US_Address_TX.postcode}}" -+ selector="{{StorefrontCustomerAddressesSection.defaultShippingAddress}}" stepKey="checkNewAddressesPostcodeOnDefaultShipping"/> -+ </test> -+ <test name="StorefrontAddCustomerNonDefaultAddressTest"> -+ <annotations> -+ <features value="Customer address"/> -+ <stories value="Implement handling of large number of addresses on storefront Address book"/> -+ <title value="Storefront - My account - Address Book - add new non default billing/shipping address"/> -+ <description value="Storefront user should be able to create a new non default address via the storefront"/> -+ <severity value="MAJOR"/> -+ <testCaseId value="MAGETWO-97500"/> -+ <group value="customer"/> -+ <group value="create"/> -+ </annotations> -+ <before> -+ <createData entity="Simple_US_Customer_NY" stepKey="createCustomer"/> -+ </before> -+ <after> -+ <deleteData createDataKey="createCustomer" stepKey="DeleteCustomer"/> -+ <amOnPage url="admin/admin/auth/logout/" stepKey="AmOnLogoutPage"/> -+ </after> -+ -+ <!--Log in to Storefront as Customer 1 --> -+ <actionGroup ref="LoginToStorefrontActionGroup" stepKey="signUp"> -+ <argument name="Customer" value="$$createCustomer$$"/> -+ </actionGroup> -+ <actionGroup ref="StorefrontAddNewCustomerAddressActionGroup" stepKey="AddNewNonDefaultAddress"> -+ <argument name="Address" value="US_Address_TX"/> -+ </actionGroup> -+ <see userInput="{{US_Address_TX.street[0]}}" -+ selector="{{StorefrontCustomerAddressesSection.addressesList}}" stepKey="checkNewAddressesStreetOnDefaultShipping"/> -+ <see userInput="{{US_Address_TX.city}}" -+ selector="{{StorefrontCustomerAddressesSection.addressesList}}" stepKey="checkNewAddressesCityOnDefaultShipping"/> -+ <see userInput="{{US_Address_TX.postcode}}" -+ selector="{{StorefrontCustomerAddressesSection.addressesList}}" stepKey="checkNewAddressesPostcodeOnDefaultShipping"/> -+ </test> -+</tests> -\ No newline at end of file -diff --git a/app/code/Magento/Customer/Test/Mftf/Test/StorefrontCheckTaxAddingValidVATIdTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/StorefrontCheckTaxAddingValidVATIdTest.xml -new file mode 100644 -index 00000000000..0cba9159dd5 ---- /dev/null -+++ b/app/code/Magento/Customer/Test/Mftf/Test/StorefrontCheckTaxAddingValidVATIdTest.xml -@@ -0,0 +1,152 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> -+ <test name="StorefrontCheckTaxAddingValidVATIdTest"> -+ <annotations> -+ <features value="Customer"/> -+ <stories value="MAGETWO-91639: Tax is added despite customer group changes"/> -+ <title value="Check tax adding when it's changed to 'Valid VAT ID - Intra-Union'"/> -+ <description value="Tax should be applied"/> -+ <severity value="MAJOR"/> -+ <testCaseId value="MAGETWO-95028"/> -+ <group value="customer"/> -+ <skip> -+ <issueId value="MC-16684"/> -+ </skip> -+ </annotations> -+ <before> -+ <!--Log In--> -+ <actionGroup ref="LoginAsAdmin" stepKey="logIn"/> -+ <!--Create category--> -+ <createData entity="_defaultCategory" stepKey="createCategory"/> -+ <!--Create product--> -+ <createData entity="SimpleProduct" stepKey="createProduct"> -+ <requiredEntity createDataKey="createCategory"/> -+ </createData> -+ </before> -+ -+ <!--Add new tax rates. Go to tax rule page --> -+ <actionGroup ref="addNewTaxRuleActionGroup" stepKey="addFirstTaxRuleActionGroup"/> -+ <fillField stepKey="fillRuleName" selector="{{AdminTaxRulesSection.ruleName}}" userInput="{{TaxRule.name}}"/> -+ -+ <!-- Add NY and CA tax rules --> -+ <actionGroup ref="addNewTaxRateNoZip" stepKey="addSimpleTaxUK"> -+ <argument name="taxCode" value="SimpleTaxUK"/> -+ </actionGroup> -+ <click stepKey="clickSave" selector="{{AdminStoresMainActionsSection.saveButton}}"/> -+ <waitForPageLoad stepKey="waitForNewTaxRuleCreated"/> -+ -+ <!-- Go to tax rule page to create second Tax Rule--> -+ <actionGroup ref="addNewTaxRuleActionGroup" stepKey="addSecondTaxRuleActionGroup"/> -+ <fillField stepKey="fillSecondRuleName" selector="{{AdminTaxRulesSection.ruleName}}" userInput="{{TaxRuleZeroRate.name}}"/> -+ <actionGroup ref="addNewTaxRateNoZip" stepKey="addSimpleTaxUKZeroRate"> -+ <argument name="taxCode" value="SimpleTaxUKZeroRate"/> -+ </actionGroup> -+ <actionGroup ref="addCustomerTaxClass" stepKey="addCustomerTaxClass"> -+ <argument name="customerTaxClassName" value="UK_zero"/> -+ </actionGroup> -+ <click stepKey="disableDefaultProdTaxClass" selector="{{AdminTaxRulesSection.defaultCustomerTaxClass}}"/> -+ <wait stepKey="waitForDisableDefaultProdTaxClass" time="2"/> -+ <click stepKey="clickSaveBtn" selector="{{AdminStoresMainActionsSection.saveButton}}"/> -+ <waitForPageLoad stepKey="waitForSecondTaxRuleCreated"/> -+ -+ <!--Create a Customer Group (CUSTOMERS > Customer Groups)--> -+ <actionGroup ref="AdminCreateCustomerGroupActionGroup" stepKey="createCustomerGroup"> -+ <argument name="groupName" value="test_UK"/> -+ <argument name="taxClass" value="UK_zero"/> -+ </actionGroup> -+ -+ <!--Set Customer Create New Account Options Config--> -+ <createData entity="SetCustomerCreateNewAccountOptionsConfig" stepKey="setCustomerCreateNewAccountOptionsConfig"/> -+ <actionGroup ref="SetGroupForValidVATIdIntraUnionActionGroup" stepKey="setGroupForValidVATIdIntraUnionActionGroup" after="setCustomerCreateNewAccountOptionsConfig"> -+ <argument name="value" value="test_UK"/> -+ </actionGroup> -+ -+ <!--Register customer on storefront--> -+ <actionGroup ref="SignUpNewCustomerStorefrontActionGroup" stepKey="createAnAccount"> -+ <argument name="Customer" value="CustomerEntityOne"/> -+ </actionGroup> -+ -+ <!--Go to My account > Address book--> -+ <actionGroup ref="EnterCustomerAddressInfoFillState" stepKey="enterAddressInfo"> -+ <argument name="Address" value="UK_Simple_Address"/> -+ </actionGroup> -+ -+ <!-- Go to product visible --> -+ <amOnPage url="$$createProduct.name$$.html" stepKey="navigateToProductPageOnDefaultStore"/> -+ <see userInput="$$createProduct.name$$" selector="{{StorefrontProductInfoMainSection.productName}}" stepKey="assertFirstProductNameTitle"/> -+ -+ <!--Add a product to the cart--> -+ <click selector="{{StorefrontProductActionSection.addToCart}}" stepKey="addProductToCart"/> -+ <waitForPageLoad stepKey="waitForAddProductToCart"/> -+ <waitForElementVisible selector="{{StorefrontMessagesSection.success}}" stepKey="waitForSuccessMessage"/> -+ <!--Proceed to checkout--> -+ <actionGroup ref="GoToCheckoutFromMinicartActionGroup" stepKey="GoToCheckoutFromMinicartActionGroup"/> -+ <!-- Click next button to open payment section --> -+ <click selector="{{CheckoutShippingGuestInfoSection.next}}" stepKey="clickNext"/> -+ <waitForPageLoad stepKey="waitForShipmentPageLoad"/> -+ -+ <!-- Check order summary in checkout --> -+ <waitForElement selector="{{CheckoutPaymentSection.paymentSectionTitle}}" time="30" stepKey="waitForPaymentSectionLoaded"/> -+ <!--Verify that Tax 50% is applied --> -+ <see userInput="$123.00" selector="{{CheckoutPaymentSection.orderSummarySubtotal}}" stepKey="assertSubtotal"/> -+ <see userInput="$5.00" selector="{{CheckoutPaymentSection.orderSummaryShippingTotal}}" stepKey="assertShipping"/> -+ <see userInput="Flat Rate - Fixed" selector="{{CheckoutPaymentSection.orderSummaryShippingMethod}}" stepKey="assertShippingMethod"/> -+ <see userInput="$61.50" selector="{{CheckoutPaymentSection.tax}}" stepKey="assertTax"/> -+ <see userInput="$189.50" selector="{{CheckoutPaymentSection.orderSummaryTotal}}" stepKey="assertTotal"/> -+ -+ <after> -+ <!-- Go to the tax rule page and delete the row we created--> -+ <amOnPage url="{{AdminTaxRuleGridPage.url}}" stepKey="goToTaxRulesPage"/> -+ <waitForPageLoad stepKey="waitForRulesPage"/> -+ -+ <actionGroup ref="deleteEntitySecondaryGrid" stepKey="deleteRule"> -+ <argument name="name" value="{{TaxRule.name}}"/> -+ <argument name="searchInput" value="{{AdminSecondaryGridSection.taxIdentifierSearch}}"/> -+ </actionGroup> -+ -+ <actionGroup ref="deleteEntitySecondaryGrid" stepKey="deleteSecondRule"> -+ <argument name="name" value="{{TaxRuleZeroRate.name}}"/> -+ <argument name="searchInput" value="{{AdminSecondaryGridSection.taxIdentifierSearch}}"/> -+ </actionGroup> -+ -+ <!-- Go to the tax rate page --> -+ <amOnPage url="{{AdminTaxRateGridPage.url}}" stepKey="goToTaxRatesPage"/> -+ <waitForPageLoad stepKey="waitForRatesPage"/> -+ -+ <!-- Delete the two tax rates that were created --> -+ <actionGroup ref="deleteEntitySecondaryGrid" stepKey="deleteNYRate"> -+ <argument name="name" value="{{SimpleTaxUK.state}}-{{SimpleTaxUK.rate}}"/> -+ <argument name="searchInput" value="{{AdminSecondaryGridSection.taxIdentifierSearch}}"/> -+ </actionGroup> -+ -+ <actionGroup ref="deleteEntitySecondaryGrid" stepKey="deleteCARate"> -+ <argument name="name" value="{{SimpleTaxUKZeroRate.state}}-{{SimpleTaxUKZeroRate.rate}}"/> -+ <argument name="searchInput" value="{{AdminSecondaryGridSection.taxIdentifierSearch}}"/> -+ </actionGroup> -+ -+ <!--Delete created customer group--> -+ <actionGroup ref="AdminDeleteCustomerGroupActionGroup" stepKey="deleteCustomerGroup"> -+ <argument name="customerGroupName" value="test_UK"/> -+ </actionGroup> -+ -+ <createData entity="SetCustomerCreateNewAccountOptionsDefaultConfig" stepKey="setCustomerCreateNewAccountOptionsDefaultConfig"/> -+ <deleteData createDataKey="createProduct" stepKey="deleteSimpleProduct"/> -+ <deleteData createDataKey="createCategory" stepKey="deleteCategoryFirst"/> -+ -+ <actionGroup ref="deleteProductTaxClass" stepKey="deleteFirstProductTaxClass"> -+ <argument name="taxClassName" value="UK_zero"/> -+ </actionGroup> -+ -+ <!--Log Out--> -+ <actionGroup ref="logout" stepKey="logout"/> -+ </after> -+ </test> -+</tests> -diff --git a/app/code/Magento/Customer/Test/Mftf/Test/StorefrontClearAllCompareProductsTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/StorefrontClearAllCompareProductsTest.xml -new file mode 100644 -index 00000000000..2b88657c6ca ---- /dev/null -+++ b/app/code/Magento/Customer/Test/Mftf/Test/StorefrontClearAllCompareProductsTest.xml -@@ -0,0 +1,156 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> -+ <test name="StorefrontClearAllCompareProductsTest"> -+ <annotations> -+ <stories value="Compare Products"/> -+ <title value="Clear all products from the 'Compare Products' list"/> -+ <description value="You should be able to remove all Products in the 'Compare Products' list."/> -+ <testCaseId value="MC-14208"/> -+ <severity value="CRITICAL"/> -+ <group value="catalog"/> -+ <group value="mtf_migrated"/> -+ </annotations> -+ -+ <before> -+ <!-- Create Simple Customer --> -+ <createData entity="Simple_US_Customer_CA" stepKey="createSimpleCustomer1"/> -+ -+ <!-- Create Simple Category --> -+ <createData entity="SimpleSubCategory" stepKey="createSimpleCategory1"/> -+ -+ <!-- Create Simple Products --> -+ <createData entity="SimpleProduct" stepKey="createSimpleProduct1"> -+ <requiredEntity createDataKey="createSimpleCategory1"/> -+ </createData> -+ <createData entity="SimpleProduct" stepKey="createSimpleProduct2"> -+ <requiredEntity createDataKey="createSimpleCategory1"/> -+ </createData> -+ -+ <!-- Create Configurable Product --> -+ <createData entity="ApiConfigurableProduct" stepKey="createConfigProduct1"> -+ <requiredEntity createDataKey="createSimpleCategory1"/> -+ </createData> -+ -+ <!-- Create Virtual Product --> -+ <createData entity="VirtualProduct" stepKey="createVirtualProduct1"> -+ <requiredEntity createDataKey="createSimpleCategory1"/> -+ </createData> -+ -+ <!-- Create Bundled Product --> -+ <createData entity="ApiBundleProduct" stepKey="createBundleProduct1"> -+ <requiredEntity createDataKey="createSimpleCategory1"/> -+ </createData> -+ <createData entity="DropDownBundleOption" stepKey="createBundleOption1"> -+ <requiredEntity createDataKey="createBundleProduct1"/> -+ </createData> -+ <createData entity="ApiBundleLink" stepKey="createBundleLink1"> -+ <requiredEntity createDataKey="createBundleProduct1"/> -+ <requiredEntity createDataKey="createBundleOption1"/> -+ <requiredEntity createDataKey="createSimpleProduct1"/> -+ <field key="qty">10</field> -+ </createData> -+ -+ <!-- Create Grouped Product --> -+ <createData entity="ApiGroupedProduct2" stepKey="createGroupedProduct1"> -+ <requiredEntity createDataKey="createSimpleCategory1"/> -+ </createData> -+ <createData entity="OneSimpleProductLink" stepKey="addFirstProduct1"> -+ <requiredEntity createDataKey="createGroupedProduct1"/> -+ <requiredEntity createDataKey="createSimpleProduct1"/> -+ </createData> -+ <updateData entity="OneMoreSimpleProductLink" createDataKey="addFirstProduct1" stepKey="addSecondProduct1"> -+ <requiredEntity createDataKey="createGroupedProduct1"/> -+ <requiredEntity createDataKey="createSimpleProduct2"/> -+ </updateData> -+ -+ <!-- Create Downloadable Product --> -+ <createData entity="ApiDownloadableProduct" stepKey="createDownloadableProduct1"/> -+ <createData entity="ApiDownloadableLink" stepKey="addDownloadableLink1"> -+ <requiredEntity createDataKey="createDownloadableProduct1"/> -+ </createData> -+ -+ <!-- Login --> -+ <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin1"/> -+ </before> -+ <after> -+ <!-- Logout --> -+ <actionGroup ref="logout" stepKey="logoutOfAdmin1"/> -+ -+ <!-- Delete Created Entities --> -+ <deleteData createDataKey="createSimpleCustomer1" stepKey="deleteSimpleCustomer1"/> -+ <deleteData createDataKey="createSimpleCategory1" stepKey="deleteSimpleCategory1"/> -+ <deleteData createDataKey="createSimpleProduct1" stepKey="deleteSimpleProduct1"/> -+ <deleteData createDataKey="createSimpleProduct2" stepKey="deleteSimpleProduct2"/> -+ <deleteData createDataKey="createConfigProduct1" stepKey="deleteConfigProduct1"/> -+ <deleteData createDataKey="createVirtualProduct1" stepKey="deleteVirtualProduct1"/> -+ <deleteData createDataKey="createBundleProduct1" stepKey="deleteBundleProduct1"/> -+ <deleteData createDataKey="createGroupedProduct1" stepKey="deleteGroupedProduct1"/> -+ <deleteData createDataKey="createDownloadableProduct1" stepKey="deleteDownloadableProduct1"/> -+ </after> -+ -+ <actionGroup ref="LoginToStorefrontActionGroup" stepKey="loginAsCustomer1"> -+ <argument name="Customer" value="$$createSimpleCustomer1$$" /> -+ </actionGroup> -+ -+ <actionGroup ref="StorefrontOpenProductPageActionGroup" stepKey="openProductPage1"> -+ <argument name="productUrl" value="$$createSimpleProduct1.custom_attributes[url_key]$$"/> -+ </actionGroup> -+ <scrollTo selector="{{StorefrontProductInfoMainSection.productAddToCompare}}" stepKey="scrollToCompareProductButton1"/> -+ <actionGroup ref="StorefrontAddProductToCompareActionGroup" stepKey="addProductToCompare1"> -+ <argument name="productVar" value="$$createSimpleProduct1$$"/> -+ </actionGroup> -+ -+ <actionGroup ref="StorefrontOpenProductPageActionGroup" stepKey="openProductPage2"> -+ <argument name="productUrl" value="$$createConfigProduct1.custom_attributes[url_key]$$"/> -+ </actionGroup> -+ <scrollTo selector="{{StorefrontProductInfoMainSection.productAddToCompare}}" stepKey="scrollToCompareProductButton2"/> -+ <actionGroup ref="StorefrontAddProductToCompareActionGroup" stepKey="addProductToCompare2"> -+ <argument name="productVar" value="$$createConfigProduct1$$"/> -+ </actionGroup> -+ -+ <actionGroup ref="StorefrontOpenProductPageActionGroup" stepKey="openProductPage3"> -+ <argument name="productUrl" value="$$createVirtualProduct1.custom_attributes[url_key]$$"/> -+ </actionGroup> -+ <scrollTo selector="{{StorefrontProductInfoMainSection.productAddToCompare}}" stepKey="scrollToCompareProductButton3"/> -+ <actionGroup ref="StorefrontAddProductToCompareActionGroup" stepKey="addProductToCompare3"> -+ <argument name="productVar" value="$$createVirtualProduct1$$"/> -+ </actionGroup> -+ -+ <actionGroup ref="StorefrontOpenProductPageActionGroup" stepKey="openProductPage4"> -+ <argument name="productUrl" value="$$createBundleProduct1.custom_attributes[url_key]$$"/> -+ </actionGroup> -+ <scrollTo selector="{{StorefrontProductInfoMainSection.productAddToCompare}}" stepKey="scrollToCompareProductButton4"/> -+ <actionGroup ref="StorefrontAddProductToCompareActionGroup" stepKey="addProductToCompare4"> -+ <argument name="productVar" value="$$createBundleProduct1$$"/> -+ </actionGroup> -+ -+ <actionGroup ref="StorefrontOpenProductPageActionGroup" stepKey="openProductPage5"> -+ <argument name="productUrl" value="$$createGroupedProduct1.custom_attributes[url_key]$$"/> -+ </actionGroup> -+ <scrollTo selector="{{StorefrontProductInfoMainSection.productAddToCompare}}" stepKey="scrollToCompareProductButton5"/> -+ <actionGroup ref="StorefrontAddProductToCompareActionGroup" stepKey="addProductToCompare5"> -+ <argument name="productVar" value="$$createGroupedProduct1$$"/> -+ </actionGroup> -+ -+ <actionGroup ref="StorefrontOpenProductPageActionGroup" stepKey="openProductPage6"> -+ <argument name="productUrl" value="$$createDownloadableProduct1.custom_attributes[url_key]$$"/> -+ </actionGroup> -+ <scrollTo selector="{{StorefrontProductInfoMainSection.productAddToCompare}}" stepKey="scrollToCompareProductButton6"/> -+ <actionGroup ref="StorefrontAddProductToCompareActionGroup" stepKey="addProductToCompare6"> -+ <argument name="productVar" value="$$createDownloadableProduct1$$"/> -+ </actionGroup> -+ -+ <amOnPage url="{{StorefrontCustomerDashboardPage.url}}" stepKey="amOnMyAccountDashboard1"/> -+ <waitForPageLoad stepKey="waitForPageLoad1"/> -+ -+ <actionGroup ref="StorefrontClearCompareActionGroup" stepKey="clearComparedProducts1"/> -+ </test> -+</tests> -diff --git a/app/code/Magento/Customer/Test/Mftf/Test/StorefrontCreateCustomerTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/StorefrontCreateCustomerTest.xml -index 3670cdba387..97c932f0cb2 100644 ---- a/app/code/Magento/Customer/Test/Mftf/Test/StorefrontCreateCustomerTest.xml -+++ b/app/code/Magento/Customer/Test/Mftf/Test/StorefrontCreateCustomerTest.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="StorefrontCreateCustomerTest"> - <annotations> - <features value="Customer"/> -diff --git a/app/code/Magento/Customer/Test/Mftf/Test/StorefrontCreateExistingCustomerTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/StorefrontCreateExistingCustomerTest.xml -new file mode 100644 -index 00000000000..952ac235d92 ---- /dev/null -+++ b/app/code/Magento/Customer/Test/Mftf/Test/StorefrontCreateExistingCustomerTest.xml -@@ -0,0 +1,39 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> -+ <test name="StorefrontCreateExistingCustomerTest"> -+ <annotations> -+ <features value="Customer"/> -+ <stories value="Customer Registration"/> -+ <title value="Attempt to register customer on storefront with existing email"/> -+ <description value="Attempt to register customer on storefront with existing email"/> -+ <testCaseId value="MC-10907"/> -+ <severity value="MAJOR"/> -+ <group value="customers"/> -+ <group value="mtf_migrated"/> -+ </annotations> -+ <before> -+ <createData entity="Simple_US_Customer" stepKey="customer"/> -+ </before> -+ <after> -+ <deleteData createDataKey="customer" stepKey="deleteCustomer"/> -+ </after> -+ -+ <actionGroup ref="StorefrontOpenCustomerAccountCreatePageActionGroup" stepKey="openCreateAccountPage"/> -+ <actionGroup ref="StorefrontFillCustomerAccountCreationFormActionGroup" stepKey="fillCreateAccountForm"> -+ <argument name="customer" value="$$customer$$"/> -+ </actionGroup> -+ <actionGroup ref="StorefrontClickCreateAnAccountCustomerAccountCreationFormActionGroup" stepKey="submitCreateAccountForm"/> -+ <actionGroup ref="AssertMessageCustomerCreateAccountActionGroup" stepKey="seeErrorMessage"> -+ <argument name="messageType" value="error"/> -+ <argument name="message" value="There is already an account with this email address."/> -+ </actionGroup> -+ </test> -+</tests> -diff --git a/app/code/Magento/Customer/Test/Mftf/Test/StorefrontDeleteCustomerAddressTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/StorefrontDeleteCustomerAddressTest.xml -new file mode 100644 -index 00000000000..7a966168854 ---- /dev/null -+++ b/app/code/Magento/Customer/Test/Mftf/Test/StorefrontDeleteCustomerAddressTest.xml -@@ -0,0 +1,41 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> -+ <test name="StorefrontDeleteCustomerAddressTest"> -+ <annotations> -+ <stories value="Delete customer address from storefront"/> -+ <title value="User should be able to delete Customer address successfully from storefront"/> -+ <description value="User should be able to delete Customer address successfully from storefront"/> -+ <severity value="CRITICAL"/> -+ <testCaseId value="MC-5713"/> -+ <group value="Customer"/> -+ </annotations> -+ <before> -+ <createData entity="Simple_US_Customer" stepKey="createCustomer"/> -+ </before> -+ <after> -+ <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> -+ </after> -+ <amOnPage stepKey="amOnSignInPage" url="{{StorefrontCustomerSignInPage.url}}"/> -+ <fillField stepKey="fillEmail" userInput="$$createCustomer.email$$" selector="{{StorefrontCustomerSignInFormSection.emailField}}"/> -+ <fillField stepKey="fillPassword" userInput="$$createCustomer.password$$" selector="{{StorefrontCustomerSignInFormSection.passwordField}}"/> -+ <click stepKey="clickSignInAccountButton" selector="{{StorefrontCustomerSignInFormSection.signInAccountButton}}"/> -+ <actionGroup ref="EnterCustomerAddressInfo" stepKey="enterAddressInfo"> -+ <argument name="Address" value="US_Address_NY"/> -+ </actionGroup> -+ <see userInput="You saved the address." stepKey="verifyAddressCreated"/> -+ <click selector="{{StorefrontCustomerAddressesSection.deleteAdditionalAddress('1')}}" stepKey="deleteAdditionalAddress"/> -+ <waitForElementVisible selector="{{ModalConfirmationSection.modalContent}}" stepKey="waitFortheConfirmationModal"/> -+ <see selector="{{ModalConfirmationSection.modalContent}}" userInput="Are you sure you want to delete this address?" stepKey="seeAddressDeleteConfirmationMessage"/> -+ <click selector="{{ModalConfirmationSection.OkButton}}" stepKey="confirmDelete"/> -+ <waitForPageLoad stepKey="waitForDeleteToFinish"/> -+ <see userInput="You deleted the address." stepKey="verifyDeleteAddress"/> -+ </test> -+</tests> -diff --git a/app/code/Magento/Customer/Test/Mftf/Test/StorefrontForgotPasswordTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/StorefrontForgotPasswordTest.xml -new file mode 100644 -index 00000000000..8d4be5fda3c ---- /dev/null -+++ b/app/code/Magento/Customer/Test/Mftf/Test/StorefrontForgotPasswordTest.xml -@@ -0,0 +1,41 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> -+ <test name="StorefrontCustomerForgotPasswordTest"> -+ <annotations> -+ <features value="Customer"/> -+ <stories value="Customer Login"/> -+ <title value="Forgot Password on Storefront validates customer email input"/> -+ <description value="Forgot Password on Storefront validates customer email input"/> -+ <severity value="CRITICAL"/> -+ <testCaseId value="MC-13679"/> -+ <group value="Customer"/> -+ <group value="mtf_migrated"/> -+ </annotations> -+ <before> -+ <magentoCLI command="config:set {{StorefrontCustomerCaptchaDisableConfigData.path}} {{StorefrontCustomerCaptchaDisableConfigData.value}}" stepKey="disableCaptcha"/> -+ <createData stepKey="customer" entity="Simple_US_Customer"/> -+ </before> -+ <after> -+ <magentoCLI command="config:set {{StorefrontCustomerCaptchaEnableConfigData.path}} {{StorefrontCustomerCaptchaEnableConfigData.value}}" stepKey="enableCaptcha"/> -+ <deleteData stepKey="deleteCustomer" createDataKey="customer" /> -+ </after> -+ -+ <amOnPage stepKey="amOnSignInPage" url="{{StorefrontCustomerSignInPage.url}}"/> -+ <fillField stepKey="fillEmail" userInput="$$customer.email$$" selector="{{StorefrontCustomerSignInFormSection.emailField}}"/> -+ <fillField stepKey="fillPassword" userInput="something" selector="{{StorefrontCustomerSignInFormSection.passwordField}}"/> -+ <click stepKey="clickForgotPasswordLink" selector="{{StorefrontCustomerSignInFormSection.forgotPasswordLink}}"/> -+ <see stepKey="seePageTitle" userInput="Forgot Your Password" selector="{{StorefrontForgotPasswordSection.pageTitle}}"/> -+ <fillField stepKey="enterEmail" userInput="$$customer.email$$" selector="{{StorefrontForgotPasswordSection.email}}"/> -+ <click stepKey="clickResetPassword" selector="{{StorefrontForgotPasswordSection.resetMyPasswordButton}}"/> -+ <seeInCurrentUrl stepKey="seeInSignInPage" url="account/login"/> -+ <see stepKey="seeSuccessMessage" userInput="If there is an account associated with $$customer.email$$ you will receive an email with a link to reset your password." selector="{{StorefrontCustomerLoginMessagesSection.successMessage}}"/> -+ </test> -+</tests> -diff --git a/app/code/Magento/Customer/Test/Mftf/Test/StorefrontLockCustomerOnLoginPageTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/StorefrontLockCustomerOnLoginPageTest.xml -new file mode 100644 -index 00000000000..c69c4dd071e ---- /dev/null -+++ b/app/code/Magento/Customer/Test/Mftf/Test/StorefrontLockCustomerOnLoginPageTest.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="StorefrontLockCustomerOnLoginPageTest"> -+ <annotations> -+ <features value="Customer"/> -+ <stories value="Lock Customer entering incorrect login credentials"/> -+ <title value="Lock customer on Storefront with after many attempts to log in with incorrect credentials"/> -+ <description value="Lock customer on Storefront with after many attempts to log in with incorrect credentials"/> -+ <testCaseId value="MC-14388"/> -+ <severity value="CRITICAL"/> -+ <group value="customer"/> -+ <group value="security"/> -+ <group value="mtf_migrated"/> -+ </annotations> -+ <before> -+ <magentoCLI command="config:set {{StorefrontCustomerCaptchaDisableConfigData.path}} {{StorefrontCustomerCaptchaDisableConfigData.value}}" stepKey="disableCaptcha"/> -+ <magentoCLI command="config:set {{StorefrontCustomerLockoutFailures5ConfigData.path}} {{StorefrontCustomerLockoutFailures5ConfigData.value}}" stepKey="setInvalidAttemptsCountConfigTo5"/> -+ <createData stepKey="customer" entity="Simple_US_Customer"/> -+ </before> -+ <after> -+ <magentoCLI command="config:set {{StorefrontCustomerCaptchaEnableConfigData.path}} {{StorefrontCustomerCaptchaEnableConfigData.value}}" stepKey="enableCaptcha"/> -+ <magentoCLI command="config:set {{StorefrontCustomerLockoutFailuresDefaultConfigData.path}} {{StorefrontCustomerLockoutFailuresDefaultConfigData.value}}" stepKey="revertInvalidAttemptsCountConfig"/> -+ <deleteData stepKey="deleteCustomer" createDataKey="customer"/> -+ </after> -+ -+ <actionGroup ref="StorefrontOpenCustomerLoginPageActionGroup" stepKey="goToSignInPage"/> -+ -+ <!-- Perform 5 attempts to log in with invalid credentials --> -+ <actionGroup ref="StorefrontFillCustomerLoginFormWithWrongPasswordActionGroup" stepKey="fillLoginFormFirstAttempt"> -+ <argument name="customer" value="$$customer$$"/> -+ </actionGroup> -+ <actionGroup ref="StorefrontClickSignOnCustomerLoginFormActionGroup" stepKey="clickSignInAccountButtonFirstAttempt"/> -+ <actionGroup ref="AssertMessageCustomerLoginActionGroup" stepKey="seeErrorMessageAfterFirstAttempt"> -+ <argument name="messageType" value="error"/> -+ <argument name="message" value="The account sign-in was incorrect or your account is disabled temporarily. Please wait and try again later"/> -+ </actionGroup> -+ -+ <actionGroup ref="StorefrontFillCustomerLoginFormWithWrongPasswordActionGroup" stepKey="fillLoginFormSecondAttempt"> -+ <argument name="customer" value="$$customer$$"/> -+ </actionGroup> -+ <actionGroup ref="StorefrontClickSignOnCustomerLoginFormActionGroup" stepKey="clickSignInAccountButtonSecondAttempt"/> -+ <actionGroup ref="AssertMessageCustomerLoginActionGroup" stepKey="seeErrorMessageAfterSecondAttempt"> -+ <argument name="messageType" value="error"/> -+ <argument name="message" value="The account sign-in was incorrect or your account is disabled temporarily. Please wait and try again later"/> -+ </actionGroup> -+ -+ <actionGroup ref="StorefrontFillCustomerLoginFormWithWrongPasswordActionGroup" stepKey="fillLoginFormThirdAttempt"> -+ <argument name="customer" value="$$customer$$"/> -+ </actionGroup> -+ <actionGroup ref="StorefrontClickSignOnCustomerLoginFormActionGroup" stepKey="clickSignInAccountButtonThirdAttempt"/> -+ <actionGroup ref="AssertMessageCustomerLoginActionGroup" stepKey="seeErrorMessageAfterThirdAttempt"> -+ <argument name="messageType" value="error"/> -+ <argument name="message" value="The account sign-in was incorrect or your account is disabled temporarily. Please wait and try again later"/> -+ </actionGroup> -+ -+ <actionGroup ref="StorefrontFillCustomerLoginFormWithWrongPasswordActionGroup" stepKey="fillLoginFormFourthAttempt"> -+ <argument name="customer" value="$$customer$$"/> -+ </actionGroup> -+ <actionGroup ref="StorefrontClickSignOnCustomerLoginFormActionGroup" stepKey="clickSignInAccountButtonFourthAttempt"/> -+ <actionGroup ref="AssertMessageCustomerLoginActionGroup" stepKey="seeErrorMessageAfterFourthAttempt"> -+ <argument name="messageType" value="error"/> -+ <argument name="message" value="The account sign-in was incorrect or your account is disabled temporarily. Please wait and try again later"/> -+ </actionGroup> -+ -+ <actionGroup ref="StorefrontFillCustomerLoginFormWithWrongPasswordActionGroup" stepKey="fillLoginFormFifthAttempt"> -+ <argument name="customer" value="$$customer$$"/> -+ </actionGroup> -+ <actionGroup ref="StorefrontClickSignOnCustomerLoginFormActionGroup" stepKey="clickSignInAccountButtonFifthAttempt"/> -+ <actionGroup ref="AssertMessageCustomerLoginActionGroup" stepKey="seeErrorMessageAfterFifthAttempt"> -+ <argument name="messageType" value="error"/> -+ <argument name="message" value="The account sign-in was incorrect or your account is disabled temporarily. Please wait and try again later"/> -+ </actionGroup> -+ -+ <!-- Make sure that the customer is locked --> -+ <actionGroup ref="StorefrontFillCustomerLoginFormActionGroup" stepKey="fillLoginFormWithCorrectCredentials"> -+ <argument name="customer" value="$$customer$$"/> -+ </actionGroup> -+ <actionGroup ref="StorefrontClickSignOnCustomerLoginFormActionGroup" stepKey="clickSignInAccountButtonWithCorrectCredentials"/> -+ <actionGroup ref="AssertMessageCustomerLoginActionGroup" stepKey="seeLockoutErrorMessage"> -+ <argument name="messageType" value="error"/> -+ <argument name="message" value="The account sign-in was incorrect or your account is disabled temporarily. Please wait and try again later."/> -+ </actionGroup> -+ </test> -+</tests> -diff --git a/app/code/Magento/Customer/Test/Mftf/Test/StorefrontLoginWithIncorrectCredentialsTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/StorefrontLoginWithIncorrectCredentialsTest.xml -new file mode 100644 -index 00000000000..104b5d56314 ---- /dev/null -+++ b/app/code/Magento/Customer/Test/Mftf/Test/StorefrontLoginWithIncorrectCredentialsTest.xml -@@ -0,0 +1,35 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> -+ <test name="StorefrontLoginWithIncorrectCredentialsTest"> -+ <annotations> -+ <features value="Customer"/> -+ <stories value="Customer Login"/> -+ <title value="Customer Login on Storefront with Incorrect Credentials"/> -+ <description value="Customer Login on Storefront with Incorrect Credentials"/> -+ <severity value="CRITICAL"/> -+ <testCaseId value="MC-10913"/> -+ <group value="Customer"/> -+ <group value="mtf_migrated"/> -+ </annotations> -+ <before> -+ <createData stepKey="customer" entity="Simple_US_Customer"/> -+ </before> -+ <after> -+ <deleteData stepKey="deleteCustomer" createDataKey="customer" /> -+ </after> -+ -+ <amOnPage stepKey="amOnSignInPage" url="{{StorefrontCustomerSignInPage.url}}"/> -+ <fillField stepKey="fillEmail" userInput="$$customer.email$$" selector="{{StorefrontCustomerSignInFormSection.emailField}}"/> -+ <fillField stepKey="fillPassword" userInput="$$customer.password$$INVALID" selector="{{StorefrontCustomerSignInFormSection.passwordField}}"/> -+ <click stepKey="clickSignInAccountButton" selector="{{StorefrontCustomerSignInFormSection.signInAccountButton}}"/> -+ <see stepKey="seeErrorMessage" selector="{{StorefrontCustomerLoginMessagesSection.errorMessage}}" userInput="The account sign-in was incorrect or your account is disabled temporarily. Please wait and try again later."/> -+ </test> -+</tests> -diff --git a/app/code/Magento/Customer/Test/Mftf/Test/StorefrontPersistedCustomerLoginTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/StorefrontPersistedCustomerLoginTest.xml -index ec669be165e..250da687866 100644 ---- a/app/code/Magento/Customer/Test/Mftf/Test/StorefrontPersistedCustomerLoginTest.xml -+++ b/app/code/Magento/Customer/Test/Mftf/Test/StorefrontPersistedCustomerLoginTest.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="StorefrontPersistedCustomerLoginTest"> - <annotations> - <features value="Customer"/> -diff --git a/app/code/Magento/Customer/Test/Mftf/Test/StorefrontResetCustomerPasswordFailedTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/StorefrontResetCustomerPasswordFailedTest.xml -new file mode 100644 -index 00000000000..5d0eec935e1 ---- /dev/null -+++ b/app/code/Magento/Customer/Test/Mftf/Test/StorefrontResetCustomerPasswordFailedTest.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="StorefrontResetCustomerPasswordFailedTest"> -+ <annotations> -+ <features value="Customer"/> -+ <stories value="Reset password"/> -+ <title value="Customer tries to reset password several times"/> -+ <description value="Customer tries to reset password several times"/> -+ <severity value="CRITICAL" /> -+ <testCaseId value="MC-14374" /> -+ <group value="Customer"/> -+ <group value="security"/> -+ <group value="mtf_migrated"/> -+ </annotations> -+ <before> -+ <createData stepKey="customer" entity="Simple_US_Customer"/> -+ </before> -+ <after> -+ <deleteData stepKey="deleteCustomer" createDataKey="customer" /> -+ </after> -+ -+ <actionGroup ref="StorefrontCustomerResetPasswordActionGroup" stepKey="resetPasswordFirstAttempt"> -+ <argument name="email" value="$$customer.email$$" /> -+ </actionGroup> -+ <actionGroup ref="AssertCustomerResetPasswordActionGroup" stepKey="seePageWithSuccessMessage"> -+ <argument name="url" value="{{StorefrontCustomerSignInPage.url}}"/> -+ <argument name="message" value="If there is an account associated with $$customer.email$$ you will receive an email with a link to reset your password."/> -+ </actionGroup> -+ <actionGroup ref="StorefrontCustomerResetPasswordActionGroup" stepKey="resetPasswordSecondAttempt"> -+ <argument name="email" value="$$customer.email$$" /> -+ </actionGroup> -+ <actionGroup ref="AssertCustomerResetPasswordActionGroup" stepKey="seePageWithErrorMessage"> -+ <argument name="url" value="{{StorefrontForgotPasswordPage.url}}"/> -+ <argument name="message" value="We received too many requests for password resets. Please wait and try again later or contact hello@example.com."/> -+ <argument name="messageType" value="error" /> -+ </actionGroup> -+ </test> -+</tests> -diff --git a/app/code/Magento/Customer/Test/Mftf/Test/StorefrontUpdateCustomerAddressFranceTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/StorefrontUpdateCustomerAddressFranceTest.xml -new file mode 100644 -index 00000000000..dae456c96a6 ---- /dev/null -+++ b/app/code/Magento/Customer/Test/Mftf/Test/StorefrontUpdateCustomerAddressFranceTest.xml -@@ -0,0 +1,52 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> -+ <test name="StorefrontUpdateCustomerAddressFranceTest"> -+ <annotations> -+ <stories value="Update Customer Address"/> -+ <title value="Update Customer Address (France) in Storefront"/> -+ <description value="Test log in to Storefront and Update Customer Address (France) in Storefront"/> -+ <testCaseId value="MC-10912"/> -+ <severity value="CRITICAL"/> -+ <group value="customer"/> -+ <group value="mtf_migrated"/> -+ </annotations> -+ -+ <before> -+ <actionGroup ref = "LoginAsAdmin" stepKey="loginAsAdmin"/> -+ <actionGroup ref="SignUpNewUserFromStorefrontActionGroup" stepKey="SignUpNewUser"> -+ <argument name="Customer" value="CustomerEntityOne"/> -+ </actionGroup> -+ </before> -+ <after> -+ <actionGroup ref="DeleteCustomerByEmailActionGroup" stepKey="deleteNewUser"> -+ <argument name="email" value="{{CustomerEntityOne.email}}"/> -+ </actionGroup> -+ <actionGroup ref="logout" stepKey="logout"/> -+ </after> -+ -+ <!--Update customer address France in storefront--> -+ <actionGroup ref="EnterCustomerAddressInfo" stepKey="enterAddress"> -+ <argument name="Address" value="updateCustomerFranceAddress"/> -+ </actionGroup> -+ <!--Verify customer address save success message--> -+ <see selector="{{AdminCustomerMessagesSection.successMessage}}" userInput="You saved the address." stepKey="seeAssertCustomerAddressSuccessSaveMessage"/> -+ -+ <!--Verify customer default billing address--> -+ <actionGroup ref="VerifyCustomerBillingAddressWithState" stepKey="verifyBillingAddress"> -+ <argument name="address" value="updateCustomerFranceAddress"/> -+ </actionGroup> -+ -+ <!--Verify customer default shipping address--> -+ <actionGroup ref="VerifyCustomerShippingAddressWithState" stepKey="verifyShippingAddress"> -+ <argument name="address" value="updateCustomerFranceAddress"/> -+ </actionGroup> -+ </test> -+</tests> -\ No newline at end of file -diff --git a/app/code/Magento/Customer/Test/Mftf/Test/StorefrontUpdateCustomerAddressTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/StorefrontUpdateCustomerAddressTest.xml -new file mode 100644 -index 00000000000..d9d1c9f2e05 ---- /dev/null -+++ b/app/code/Magento/Customer/Test/Mftf/Test/StorefrontUpdateCustomerAddressTest.xml -@@ -0,0 +1,122 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> -+ <test name="StorefrontUpdateCustomerDefaultBillingAddressFromBlockTest"> -+ <annotations> -+ <features value="Customer address"/> -+ <stories value="Implement handling of large number of addresses on storefront Address book"/> -+ <title value="Add default customer address via the Storefront6"/> -+ <description value="Storefront user should be able to create a new default address via the storefront"/> -+ <severity value="MAJOR"/> -+ <testCaseId value="MAGETWO-97501"/> -+ <group value="customer"/> -+ <group value="update"/> -+ </annotations> -+ <before> -+ <createData entity="Simple_US_Customer_With_Different_Billing_Shipping_Addresses" stepKey="createCustomer"/> -+ </before> -+ <after> -+ <deleteData createDataKey="createCustomer" stepKey="DeleteCustomer"/> -+ </after> -+ -+ <!--Log in to Storefront as Customer 1 --> -+ <actionGroup ref="LoginToStorefrontActionGroup" stepKey="signUp"> -+ <argument name="Customer" value="$$createCustomer$$"/> -+ </actionGroup> -+ <amOnPage url="customer/address/" stepKey="OpenCustomerAddNewAddress"/> -+ <click stepKey="ClickEditDefaultBillingAddress" selector="{{StorefrontCustomerAddressesSection.editDefaultBillingAddress}}"/> -+ <fillField stepKey="fillFirstName" userInput="EditedFirstNameBilling" selector="{{StorefrontCustomerAddressFormSection.firstName}}"/> -+ <fillField stepKey="fillLastName" userInput="EditedLastNameBilling" selector="{{StorefrontCustomerAddressFormSection.lastName}}"/> -+ <click stepKey="saveCustomerAddress" selector="{{StorefrontCustomerAddressFormSection.saveAddress}}"/> -+ <see userInput="You saved the address." stepKey="verifyAddressAdded"/> -+ <see userInput="EditedFirstNameBilling" -+ selector="{{StorefrontCustomerAddressesSection.defaultBillingAddress}}" stepKey="checkNewAddressesFirstNameOnDefaultBilling"/> -+ <see userInput="EditedLastNameBilling" -+ selector="{{StorefrontCustomerAddressesSection.defaultBillingAddress}}" stepKey="checkNewAddressesLastNameOnDefaultBilling"/> -+ <see userInput="{{US_Address_NY_Default_Shipping.firstname}}" -+ selector="{{StorefrontCustomerAddressesSection.defaultShippingAddress}}" stepKey="checkNewAddressesFirstNameOnDefaultShipping"/> -+ <see userInput="{{US_Address_NY_Default_Shipping.lastname}}" -+ selector="{{StorefrontCustomerAddressesSection.defaultShippingAddress}}" stepKey="checkNewAddressesLastNameOnDefaultShipping"/> -+ </test> -+ <test name="StorefrontUpdateCustomerDefaultShippingAddressFromBlockTest"> -+ <annotations> -+ <features value="Customer address"/> -+ <stories value="Implement handling of large number of addresses on storefront Address book"/> -+ <title value="Add default customer address via the Storefront611"/> -+ <description value="Storefront user should be able to create a new default address via the storefront"/> -+ <severity value="MAJOR"/> -+ <testCaseId value="MAGETWO-97501"/> -+ <group value="customer"/> -+ <group value="update"/> -+ <skip> -+ <issueId value="MAGETWO-97504"/> -+ </skip> -+ </annotations> -+ <before> -+ <createData entity="Simple_US_Customer_With_Different_Billing_Shipping_Addresses" stepKey="createCustomer"/> -+ </before> -+ <after> -+ <deleteData createDataKey="createCustomer" stepKey="DeleteCustomer"/> -+ </after> -+ -+ <!--Log in to Storefront as Customer 1 --> -+ <actionGroup ref="LoginToStorefrontActionGroup" stepKey="signUp"> -+ <argument name="Customer" value="$$createCustomer$$"/> -+ </actionGroup> -+ <amOnPage url="customer/address/" stepKey="OpenCustomerAddNewAddress"/> -+ <click stepKey="ClickEditDefaultShippingAddress" selector="{{StorefrontCustomerAddressesSection.editDefaultShippingAddress}}"/> -+ <fillField stepKey="fillFirstName" userInput="EditedFirstNameShipping" selector="{{StorefrontCustomerAddressFormSection.firstName}}"/> -+ <fillField stepKey="fillLastName" userInput="EditedLastNameShipping" selector="{{StorefrontCustomerAddressFormSection.lastName}}"/> -+ <click stepKey="saveCustomerAddress" selector="{{StorefrontCustomerAddressFormSection.saveAddress}}"/> -+ <see userInput="You saved the address." stepKey="verifyAddressAdded"/> -+ <see userInput="EditedFirstNameShipping" -+ selector="{{StorefrontCustomerAddressesSection.defaultShippingAddress}}" stepKey="checkNewAddressesFirstNameOnDefaultShipping"/> -+ <see userInput="EditedLastNameShipping" -+ selector="{{StorefrontCustomerAddressesSection.defaultBillingAddress}}" stepKey="checkNewAddressesLastNameOnDefaultShipping"/> -+ <see userInput="{{US_Address_TX_Default_Billing.firstname}}" -+ selector="{{StorefrontCustomerAddressesSection.defaultBillingAddress}}" stepKey="checkNewAddressesFirstNameOnDefaultBilling"/> -+ <see userInput="{{US_Address_TX_Default_Billing.lastname}}" -+ selector="{{StorefrontCustomerAddressesSection.defaultBillingAddress}}" stepKey="checkNewAddressesLastNameOnDefaultBilling"/> -+ </test> -+ <test name="StorefrontUpdateCustomerAddressFromGridTest"> -+ <annotations> -+ <features value="Customer address"/> -+ <stories value="Add default customer address via the Storefront7"/> -+ <title value="Add default customer address via the Storefront7"/> -+ <description value="Storefront user should be able to create a new default address via the storefront2"/> -+ <severity value="CRITICAL"/> -+ <testCaseId value="MAGETWO-97502"/> -+ <group value="customer"/> -+ <group value="update"/> -+ </annotations> -+ <before> -+ <createData entity="Simple_US_Customer_Multiple_Addresses" stepKey="createCustomer"/> -+ </before> -+ <after> -+ <deleteData createDataKey="createCustomer" stepKey="DeleteCustomer"/> -+ </after> -+ -+ <!--Log in to Storefront as Customer 1 --> -+ <actionGroup ref="LoginToStorefrontActionGroup" stepKey="signUp"> -+ <argument name="Customer" value="$$createCustomer$$"/> -+ </actionGroup> -+ -+ <amOnPage url="customer/address/" stepKey="OpenCustomerAddNewAddress"/> -+ <click selector="{{StorefrontCustomerAddressesSection.editAdditionalAddress('1')}}" stepKey="editAdditionalAddress"/> -+ <fillField stepKey="fillFirstName" userInput="EditedFirstName" selector="{{StorefrontCustomerAddressFormSection.firstName}}"/> -+ <fillField stepKey="fillLastName" userInput="EditedLastName" selector="{{StorefrontCustomerAddressFormSection.lastName}}"/> -+ <click stepKey="saveCustomerAddress" selector="{{StorefrontCustomerAddressFormSection.saveAddress}}"/> -+ <see userInput="You saved the address." stepKey="verifyAddressAdded"/> -+ <see userInput="EditedFirstName" -+ selector="{{StorefrontCustomerAddressesSection.addressesList}}" stepKey="checkNewAddressFirstNameOnGrid"/> -+ <see userInput="EditedLastName" -+ selector="{{StorefrontCustomerAddressesSection.addressesList}}" stepKey="checkNewAddressLastNameOnGrid"/> -+ </test> -+</tests> -\ No newline at end of file -diff --git a/app/code/Magento/Customer/Test/Mftf/Test/StorefrontUpdateCustomerAddressUKTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/StorefrontUpdateCustomerAddressUKTest.xml -new file mode 100644 -index 00000000000..7b6e695aa8d ---- /dev/null -+++ b/app/code/Magento/Customer/Test/Mftf/Test/StorefrontUpdateCustomerAddressUKTest.xml -@@ -0,0 +1,57 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> -+ <test name="StorefrontUpdateCustomerAddressUKTest"> -+ <annotations> -+ <stories value="Update Customer Address"/> -+ <title value="Update Customer Address (UK) in Storefront"/> -+ <description value="Test log in to Storefront and Update Customer Address (UK) in Storefront"/> -+ <testCaseId value="MC-10911"/> -+ <severity value="CRITICAL"/> -+ <group value="customer"/> -+ <group value="mtf_migrated"/> -+ </annotations> -+ -+ <before> -+ <actionGroup ref = "LoginAsAdmin" stepKey="loginAsAdmin"/> -+ <actionGroup ref="SignUpNewUserFromStorefrontActionGroup" stepKey="SignUpNewUser"> -+ <argument name="Customer" value="CustomerEntityOne"/> -+ </actionGroup> -+ </before> -+ <after> -+ <actionGroup ref="DeleteCustomerByEmailActionGroup" stepKey="deleteNewUser"> -+ <argument name="email" value="{{CustomerEntityOne.email}}"/> -+ </actionGroup> -+ <actionGroup ref="logout" stepKey="logout"/> -+ </after> -+ -+ <!--Update customer address UK in storefront--> -+ <actionGroup ref="EnterCustomerAddressInfo" stepKey="enterAddress"> -+ <argument name="Address" value="updateCustomerUKAddress"/> -+ </actionGroup> -+ <!--Verify customer address save success message--> -+ <see selector="{{AdminCustomerMessagesSection.successMessage}}" userInput="You saved the address." stepKey="seeAssertCustomerAddressSuccessSaveMessage"/> -+ -+ <!--Verify customer default billing address--> -+ <actionGroup ref="VerifyCustomerBillingAddress" stepKey="verifyBillingAddress"> -+ <argument name="address" value="updateCustomerUKAddress"/> -+ </actionGroup> -+ -+ <!--Verify customer default shipping address--> -+ <actionGroup ref="VerifyCustomerShippingAddress" stepKey="verifyShippingAddress"> -+ <argument name="address" value="updateCustomerUKAddress"/> -+ </actionGroup> -+ -+ <!--Verify customer name on frontend--> -+ <actionGroup ref="VerifyCustomerNameOnFrontend" stepKey="verifyVerifyCustomerName"> -+ <argument name="customer" value="CustomerEntityOne"/> -+ </actionGroup> -+ </test> -+</tests> -\ No newline at end of file -diff --git a/app/code/Magento/Customer/Test/Mftf/Test/StorefrontUpdateCustomerPasswordTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/StorefrontUpdateCustomerPasswordTest.xml -new file mode 100644 -index 00000000000..9bc253c91af ---- /dev/null -+++ b/app/code/Magento/Customer/Test/Mftf/Test/StorefrontUpdateCustomerPasswordTest.xml -@@ -0,0 +1,81 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> -+ <test name="StorefrontUpdateCustomerPasswordValidCurrentPasswordTest"> -+ <annotations> -+ <features value="Customer"/> -+ <stories value="Customer Update Password"/> -+ <title value="Update Customer Password on Storefront, Valid Current Password"/> -+ <description value="Update Customer Password on Storefront, Valid Current Password"/> -+ <severity value="CRITICAL"/> -+ <testCaseId value="MC-10916"/> -+ <group value="Customer"/> -+ <group value="mtf_migrated"/> -+ </annotations> -+ <before> -+ <createData stepKey="customer" entity="Simple_US_Customer"/> -+ </before> -+ <after> -+ <deleteData stepKey="deleteCustomer" createDataKey="customer" /> -+ </after> -+ -+ <!--Log in to Storefront as Customer --> -+ <actionGroup stepKey="login" ref="LoginToStorefrontActionGroup"> -+ <argument name="Customer" value="$$customer$$"/> -+ </actionGroup> -+ <seeInCurrentUrl stepKey="onCustomerAccountPage" url="customer/account"/> -+ <click stepKey="clickChangePassword" selector="{{StorefrontCustomerDashboardAccountInformationSection.changePassword}}"/> -+ <fillField stepKey="fillValidCurrentPassword" userInput="$$customer.password$$" selector="{{StorefrontCustomerAccountInformationSection.currentPassword}}"/> -+ <fillField stepKey="fillNewPassword" userInput="$$customer.password$$#" selector="{{StorefrontCustomerAccountInformationSection.newPassword}}"/> -+ <fillField stepKey="fillNewPasswordConfirmation" userInput="$$customer.password$$#" selector="{{StorefrontCustomerAccountInformationSection.confirmNewPassword}}"/> -+ <click stepKey="saveChange" selector="{{StorefrontCustomerAccountInformationSection.saveButton}}"/> -+ <see stepKey="verifyMessage" userInput="You saved the account information." selector="{{StorefrontCustomerMessagesSection.successMessage}}"/> -+ <actionGroup stepKey="logout" ref="StorefrontCustomerLogoutActionGroup"/> -+ <actionGroup stepKey="loginWithNewPassword" ref="LoginToStorefrontWithEmailAndPassword"> -+ <argument name="email" value="$$customer.email$$"/> -+ <argument name="password" value="$$customer.password$$#"/> -+ </actionGroup> -+ <see stepKey="seeMyEmail" userInput="$$customer.email$$" selector="{{StorefrontCustomerDashboardAccountInformationSection.ContactInformation}}"/> -+ </test> -+ <test name="StorefrontUpdateCustomerPasswordInvalidCurrentPasswordTest" extends="StorefrontUpdateCustomerPasswordValidCurrentPasswordTest"> -+ <annotations> -+ <features value="Customer"/> -+ <stories value="Customer Update Password"/> -+ <title value="Update Customer Password on Storefront, Invalid Current Password"/> -+ <description value="Update Customer Password on Storefront, Invalid Current Password"/> -+ <severity value="CRITICAL"/> -+ <testCaseId value="MC-10917"/> -+ <group value="Customer"/> -+ <group value="mtf_migrated"/> -+ </annotations> -+ -+ <fillField stepKey="fillValidCurrentPassword" userInput="$$customer.password$$^" selector="{{StorefrontCustomerAccountInformationSection.currentPassword}}"/> -+ <see stepKey="verifyMessage" userInput="The password doesn't match this account. Verify the password and try again." selector="{{StorefrontCustomerMessagesSection.errorMessage}}"/> -+ <remove keyForRemoval="loginWithNewPassword"/> -+ <remove keyForRemoval="seeMyEmail"/> -+ </test> -+ <test name="StorefrontUpdateCustomerPasswordInvalidConfirmationPasswordTest" extends="StorefrontUpdateCustomerPasswordValidCurrentPasswordTest"> -+ <annotations> -+ <features value="Customer"/> -+ <stories value="Customer Update Password"/> -+ <title value="Update Customer Password on Storefront, Invalid Confirmation Password"/> -+ <description value="Update Customer Password on Storefront, Invalid Confirmation Password"/> -+ <severity value="CRITICAL"/> -+ <testCaseId value="MC-10918"/> -+ <group value="Customer"/> -+ <group value="mtf_migrated"/> -+ </annotations> -+ -+ <fillField stepKey="fillNewPasswordConfirmation" userInput="$$customer.password$$^" selector="{{StorefrontCustomerAccountInformationSection.confirmNewPassword}}"/> -+ <see stepKey="verifyMessage" userInput="Please enter the same value again." selector="{{StorefrontCustomerAccountInformationSection.confirmNewPasswordError}}"/> -+ <remove keyForRemoval="loginWithNewPassword"/> -+ <remove keyForRemoval="seeMyEmail"/> -+ </test> -+</tests> -\ No newline at end of file -diff --git a/app/code/Magento/Customer/Test/Mftf/Test/StorefrontVerifyNoXssInjectionOnUpdateCustomerInformationAddAddressTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/StorefrontVerifyNoXssInjectionOnUpdateCustomerInformationAddAddressTest.xml -new file mode 100644 -index 00000000000..e11404db9a9 ---- /dev/null -+++ b/app/code/Magento/Customer/Test/Mftf/Test/StorefrontVerifyNoXssInjectionOnUpdateCustomerInformationAddAddressTest.xml -@@ -0,0 +1,57 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> -+ <test name="StorefrontVerifyNoXssInjectionOnUpdateCustomerInformationAddAddressTest"> -+ <annotations> -+ <stories value="Update Customer Address"/> -+ <title value="[Security] Verify No XSS Injection on Update Customer Information Add Address"/> -+ <description value="Test log in to Storefront and Verify No XSS Injection on Update Customer Information Add Address"/> -+ <testCaseId value="MC-10910"/> -+ <severity value="CRITICAL"/> -+ <group value="customer"/> -+ <group value="mtf_migrated"/> -+ </annotations> -+ -+ <before> -+ <actionGroup ref = "LoginAsAdmin" stepKey="loginAsAdmin"/> -+ <actionGroup ref="SignUpNewUserFromStorefrontActionGroup" stepKey="SignUpNewUser"> -+ <argument name="Customer" value="Colorado_US_Customer"/> -+ </actionGroup> -+ </before> -+ <after> -+ <actionGroup ref="DeleteCustomerByEmailActionGroup" stepKey="deleteNewUser"> -+ <argument name="email" value="{{Colorado_US_Customer.email}}"/> -+ </actionGroup> -+ <actionGroup ref="logout" stepKey="logout"/> -+ </after> -+ -+ <!--Update customer address in storefront--> -+ <actionGroup ref="EnterCustomerAddressInfo" stepKey="enterAddress"> -+ <argument name="Address" value="updateCustomerNoXSSInjection"/> -+ </actionGroup> -+ <!--Verify customer address save success message--> -+ <see selector="{{AdminCustomerMessagesSection.successMessage}}" userInput="You saved the address." stepKey="seeAssertCustomerAddressSuccessSaveMessage"/> -+ -+ <!--Verify customer default billing address--> -+ <actionGroup ref="VerifyCustomerBillingAddressWithState" stepKey="verifyBillingAddress"> -+ <argument name="address" value="updateCustomerNoXSSInjection"/> -+ </actionGroup> -+ -+ <!--Verify customer default shipping address--> -+ <actionGroup ref="VerifyCustomerShippingAddressWithState" stepKey="verifyShippingAddress"> -+ <argument name="address" value="updateCustomerNoXSSInjection"/> -+ </actionGroup> -+ -+ <!--Verify customer name on frontend--> -+ <actionGroup ref="VerifyCustomerNameOnFrontend" stepKey="verifyVerifyCustomerName"> -+ <argument name="customer" value="Colorado_US_Customer"/> -+ </actionGroup> -+ </test> -+</tests> -\ No newline at end of file -diff --git a/app/code/Magento/Customer/Test/Mftf/Test/VerifyDisabledCustomerGroupFieldTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/VerifyDisabledCustomerGroupFieldTest.xml -new file mode 100644 -index 00000000000..9979f616818 ---- /dev/null -+++ b/app/code/Magento/Customer/Test/Mftf/Test/VerifyDisabledCustomerGroupFieldTest.xml -@@ -0,0 +1,39 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+ /** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+ -+<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" -+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> -+ <test name="VerifyDisabledCustomerGroupFieldTest"> -+ <annotations> -+ <stories value="Check that field is disabled in system Customer Group"/> -+ <title value="Check that field is disabled in system Customer Group"/> -+ <description value="Checks that customer_group_code field is disabled in NOT LOGGED IN Customer Group"/> -+ <testCaseId value="MC-14206"/> -+ <severity value="CRITICAL"/> -+ <group value="customers"/> -+ <group value="mtf_migrated"/> -+ </annotations> -+ -+ <!-- Steps --> -+ <!-- 1. Login to backend as admin user --> -+ <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> -+ <waitForPageLoad stepKey="waitForAdminPageLoad" /> -+ -+ <!-- 2. Navigate to Customers > Customer Groups --> -+ <amOnPage url="{{AdminCustomerGroupPage.url}}" stepKey="amOnCustomerGroupPage" /> -+ <waitForPageLoad stepKey="waitForCustomerGroupsPageLoad" /> -+ <conditionalClick selector="{{AdminDataGridHeaderSection.clearFilters}}" dependentSelector="{{AdminDataGridHeaderSection.clearFilters}}" visible="true" stepKey="clearFilters"/> -+ -+ <!-- 3. Select system Customer Group specified in data set from grid --> -+ <click selector="{{AdminCustomerGroupMainSection.editButtonByCustomerGroupCode(NotLoggedInCustomerGroup.code)}}" stepKey="clickOnEditCustomerGroup" /> -+ -+ <!-- 4. Perform all assertions --> -+ <seeInField selector="{{AdminNewCustomerGroupSection.groupName}}" userInput="{{NotLoggedInCustomerGroup.code}}" stepKey="seeNotLoggedInTextInGroupName" /> -+ <assertElementContainsAttribute selector="{{AdminNewCustomerGroupSection.groupName}}" attribute="disabled" expectedValue="true" stepKey="checkIfGroupNameIsDisabled" /> -+ </test> -+</tests> -diff --git a/app/code/Magento/Customer/Test/Unit/Block/Address/GridTest.php b/app/code/Magento/Customer/Test/Unit/Block/Address/GridTest.php -new file mode 100644 -index 00000000000..47f96b132b3 ---- /dev/null -+++ b/app/code/Magento/Customer/Test/Unit/Block/Address/GridTest.php -@@ -0,0 +1,198 @@ -+<?php -+/** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+namespace Magento\Customer\Test\Unit\Block\Address; -+ -+use Magento\Customer\Model\ResourceModel\Address\CollectionFactory; -+ -+/** -+ * Unit tests for \Magento\Customer\Block\Address\Grid class -+ * @SuppressWarnings(PHPMD.CouplingBetweenObjects) -+ */ -+class GridTest extends \PHPUnit\Framework\TestCase -+{ -+ /** -+ * @var \Magento\Framework\TestFramework\Unit\Helper\ObjectManager -+ */ -+ private $objectManager; -+ -+ /** -+ * @var \Magento\Customer\Helper\Session\CurrentCustomer|\PHPUnit_Framework_MockObject_MockObject -+ */ -+ private $addressCollectionFactory; -+ -+ /** -+ * @var \Magento\Customer\Model\ResourceModel\Address\CollectionFactory|\PHPUnit_Framework_MockObject_MockObject -+ */ -+ private $currentCustomer; -+ -+ /** -+ * @var \Magento\Directory\Model\CountryFactory|\PHPUnit_Framework_MockObject_MockObject -+ */ -+ private $countryFactory; -+ -+ /** -+ * @var \Magento\Framework\UrlInterface|\PHPUnit_Framework_MockObject_MockObject -+ */ -+ private $urlBuilder; -+ -+ /** -+ * @var \Magento\Customer\Block\Address\Grid -+ */ -+ private $gridBlock; -+ -+ protected function setUp() -+ { -+ $this->objectManager = new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this); -+ -+ $this->currentCustomer = $this->getMockBuilder(\Magento\Customer\Helper\Session\CurrentCustomer::class) -+ ->disableOriginalConstructor() -+ ->setMethods(['getCustomer']) -+ ->getMock(); -+ -+ $this->addressCollectionFactory = $this->getMockBuilder(CollectionFactory::class) -+ ->disableOriginalConstructor() -+ ->setMethods(['create']) -+ ->getMock(); -+ -+ $this->countryFactory = $this->getMockBuilder(\Magento\Directory\Model\CountryFactory::class) -+ ->disableOriginalConstructor() -+ ->setMethods(['create']) -+ ->getMock(); -+ -+ $this->urlBuilder = $this->getMockForAbstractClass(\Magento\Framework\UrlInterface::class); -+ -+ $this->gridBlock = $this->objectManager->getObject( -+ \Magento\Customer\Block\Address\Grid::class, -+ [ -+ 'addressCollectionFactory' => $this->addressCollectionFactory, -+ 'currentCustomer' => $this->currentCustomer, -+ 'countryFactory' => $this->countryFactory, -+ '_urlBuilder' => $this->urlBuilder -+ ] -+ ); -+ } -+ -+ /** -+ * Test for \Magento\Customer\Block\Address\Book::getChildHtml method with 'pager' argument -+ */ -+ public function testGetChildHtml() -+ { -+ $customerId = 1; -+ $outputString = 'OutputString'; -+ /** @var \Magento\Framework\View\Element\BlockInterface|\PHPUnit_Framework_MockObject_MockObject $block */ -+ $block = $this->getMockBuilder(\Magento\Framework\View\Element\BlockInterface::class) -+ ->setMethods(['setCollection']) -+ ->getMockForAbstractClass(); -+ /** @var $layout \Magento\Framework\View\LayoutInterface|\PHPUnit_Framework_MockObject_MockObject */ -+ $layout = $this->getMockForAbstractClass(\Magento\Framework\View\LayoutInterface::class); -+ /** @var \Magento\Customer\Api\Data\CustomerInterface|\PHPUnit_Framework_MockObject_MockObject $customer */ -+ $customer = $this->getMockForAbstractClass(\Magento\Customer\Api\Data\CustomerInterface::class); -+ /** @var \PHPUnit_Framework_MockObject_MockObject */ -+ $addressCollection = $this->getMockBuilder(\Magento\Customer\Model\ResourceModel\Address\Collection::class) -+ ->disableOriginalConstructor() -+ ->setMethods(['setOrder', 'setCustomerFilter', 'load','addFieldToFilter']) -+ ->getMock(); -+ -+ $layout->expects($this->atLeastOnce())->method('getChildName')->with('NameInLayout', 'pager') -+ ->willReturn('ChildName'); -+ $layout->expects($this->atLeastOnce())->method('renderElement')->with('ChildName', true) -+ ->willReturn('OutputString'); -+ $layout->expects($this->atLeastOnce())->method('createBlock') -+ ->with(\Magento\Theme\Block\Html\Pager::class, 'customer.addresses.pager')->willReturn($block); -+ $customer->expects($this->atLeastOnce())->method('getId')->willReturn($customerId); -+ $this->currentCustomer->expects($this->atLeastOnce())->method('getCustomer')->willReturn($customer); -+ $addressCollection->expects($this->atLeastOnce())->method('setOrder')->with('entity_id', 'desc') -+ ->willReturnSelf(); -+ $addressCollection->expects($this->atLeastOnce())->method('setCustomerFilter')->with([$customerId]) -+ ->willReturnSelf(); -+ $addressCollection->expects(static::any())->method('addFieldToFilter')->willReturnSelf(); -+ $this->addressCollectionFactory->expects($this->atLeastOnce())->method('create') -+ ->willReturn($addressCollection); -+ $block->expects($this->atLeastOnce())->method('setCollection')->with($addressCollection)->willReturnSelf(); -+ $this->gridBlock->setNameInLayout('NameInLayout'); -+ $this->gridBlock->setLayout($layout); -+ $this->assertEquals($outputString, $this->gridBlock->getChildHtml('pager')); -+ } -+ -+ /** -+ * Test for \Magento\Customer\Block\Address\Grid::getAddressEditUrl method -+ */ -+ public function testGetAddAddressUrl() -+ { -+ $addressId = 1; -+ $expectedUrl = 'expected_url'; -+ $this->urlBuilder->expects($this->atLeastOnce())->method('getUrl') -+ ->with('customer/address/edit', ['_secure' => true, 'id' => $addressId]) -+ ->willReturn($expectedUrl); -+ $this->assertEquals($expectedUrl, $this->gridBlock->getAddressEditUrl($addressId)); -+ } -+ -+ public function testGetAdditionalAddresses() -+ { -+ $customerId = 1; -+ /** @var \Magento\Customer\Api\Data\CustomerInterface|\PHPUnit_Framework_MockObject_MockObject $customer */ -+ $customer = $this->getMockForAbstractClass(\Magento\Customer\Api\Data\CustomerInterface::class); -+ /** @var \PHPUnit_Framework_MockObject_MockObject */ -+ $addressCollection = $this->getMockBuilder(\Magento\Customer\Model\ResourceModel\Address\Collection::class) -+ ->disableOriginalConstructor() -+ ->setMethods(['setOrder', 'setCustomerFilter', 'load', 'getIterator','addFieldToFilter']) -+ ->getMock(); -+ $addressDataModel = $this->getMockForAbstractClass(\Magento\Customer\Api\Data\AddressInterface::class); -+ $address = $this->getMockBuilder(\Magento\Customer\Model\Address::class) -+ ->disableOriginalConstructor() -+ ->setMethods(['getId', 'getDataModel']) -+ ->getMock(); -+ $collection = [$address, $address, $address]; -+ $address->expects($this->exactly(3))->method('getId') -+ ->willReturnOnConsecutiveCalls(1, 2, 3); -+ $address->expects($this->atLeastOnce())->method('getDataModel')->willReturn($addressDataModel); -+ $customer->expects($this->atLeastOnce())->method('getId')->willReturn($customerId); -+ $customer->expects($this->atLeastOnce())->method('getDefaultBilling')->willReturn('1'); -+ $customer->expects($this->atLeastOnce())->method('getDefaultShipping')->willReturn('2'); -+ -+ $this->currentCustomer->expects($this->atLeastOnce())->method('getCustomer')->willReturn($customer); -+ $addressCollection->expects($this->atLeastOnce())->method('setOrder')->with('entity_id', 'desc') -+ ->willReturnSelf(); -+ $addressCollection->expects($this->atLeastOnce())->method('setCustomerFilter')->with([$customerId]) -+ ->willReturnSelf(); -+ $addressCollection->expects(static::any())->method('addFieldToFilter')->willReturnSelf(); -+ $addressCollection->expects($this->atLeastOnce())->method('getIterator') -+ ->willReturn(new \ArrayIterator($collection)); -+ $this->addressCollectionFactory->expects($this->atLeastOnce())->method('create') -+ ->willReturn($addressCollection); -+ -+ $this->assertEquals($addressDataModel, $this->gridBlock->getAdditionalAddresses()[0]); -+ } -+ -+ /** -+ * Test for \Magento\Customer\ViewModel\CustomerAddress::getStreetAddress method -+ */ -+ public function testGetStreetAddress() -+ { -+ $street = ['Line 1', 'Line 2']; -+ $expectedAddress = 'Line 1, Line 2'; -+ $address = $this->getMockForAbstractClass(\Magento\Customer\Api\Data\AddressInterface::class); -+ $address->expects($this->atLeastOnce())->method('getStreet')->willReturn($street); -+ $this->assertEquals($expectedAddress, $this->gridBlock->getStreetAddress($address)); -+ } -+ -+ /** -+ * Test for \Magento\Customer\ViewModel\CustomerAddress::getCountryByCode method -+ */ -+ public function testGetCountryByCode() -+ { -+ $countryId = 'US'; -+ $countryName = 'United States'; -+ $country = $this->getMockBuilder(\Magento\Directory\Model\Country::class) -+ ->disableOriginalConstructor() -+ ->setMethods(['loadByCode', 'getName']) -+ ->getMock(); -+ $this->countryFactory->expects($this->atLeastOnce())->method('create')->willReturn($country); -+ $country->expects($this->atLeastOnce())->method('loadByCode')->with($countryId)->willReturnSelf(); -+ $country->expects($this->atLeastOnce())->method('getName')->willReturn($countryName); -+ $this->assertEquals($countryName, $this->gridBlock->getCountryByCode($countryId)); -+ } -+} -diff --git a/app/code/Magento/Customer/Test/Unit/Block/Adminhtml/Edit/Address/DeleteButtonTest.php b/app/code/Magento/Customer/Test/Unit/Block/Adminhtml/Edit/Address/DeleteButtonTest.php -new file mode 100644 -index 00000000000..7b0da3bd422 ---- /dev/null -+++ b/app/code/Magento/Customer/Test/Unit/Block/Adminhtml/Edit/Address/DeleteButtonTest.php -@@ -0,0 +1,106 @@ -+<?php -+/** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+namespace Magento\Customer\Test\Unit\Block\Adminhtml\Edit\Address; -+ -+use Magento\Framework\TestFramework\Unit\Helper\ObjectManager as ObjectManagerHelper; -+ -+/** -+ * Class for \Magento\Customer\Block\Adminhtml\Edit\Address\DeleteButton unit tests -+ */ -+class DeleteButtonTest extends \PHPUnit\Framework\TestCase -+{ -+ /** -+ * @var \Magento\Customer\Model\AddressFactory|\PHPUnit_Framework_MockObject_MockObject -+ */ -+ private $addressFactory; -+ -+ /** -+ * @var \Magento\Framework\UrlInterface|\PHPUnit_Framework_MockObject_MockObject -+ */ -+ private $urlBuilder; -+ -+ /** -+ * @var \Magento\Customer\Model\ResourceModel\Address|\PHPUnit_Framework_MockObject_MockObject -+ */ -+ private $addressResourceModel; -+ -+ /** -+ * @var \Magento\Framework\App\RequestInterface|\PHPUnit_Framework_MockObject_MockObject -+ */ -+ private $request; -+ -+ /** -+ * @var \Magento\Customer\Model\ResourceModel\AddressRepository|\PHPUnit_Framework_MockObject_MockObject -+ */ -+ private $addressRepository; -+ -+ /** -+ * @var \Magento\Customer\Block\Adminhtml\Edit\Address\DeleteButton -+ */ -+ private $deleteButton; -+ -+ /** -+ * @inheritdoc -+ */ -+ protected function setUp() -+ { -+ $this->addressFactory = $this->getMockBuilder(\Magento\Customer\Model\AddressFactory::class) -+ ->disableOriginalConstructor() -+ ->getMock(); -+ $this->urlBuilder = $this->getMockForAbstractClass(\Magento\Framework\UrlInterface::class); -+ $this->addressResourceModel = $this->getMockBuilder(\Magento\Customer\Model\ResourceModel\Address::class) -+ ->disableOriginalConstructor() -+ ->getMock(); -+ $this->request = $this->getMockForAbstractClass(\Magento\Framework\App\RequestInterface::class); -+ $this->addressRepository = $this->getMockBuilder( -+ \Magento\Customer\Model\ResourceModel\AddressRepository::class -+ ) -+ ->disableOriginalConstructor() -+ ->getMock(); -+ $objectManagerHelper = new ObjectManagerHelper($this); -+ -+ $this->deleteButton = $objectManagerHelper->getObject( -+ \Magento\Customer\Block\Adminhtml\Edit\Address\DeleteButton::class, -+ [ -+ 'addressFactory' => $this->addressFactory, -+ 'urlBuilder' => $this->urlBuilder, -+ 'addressResourceModel' => $this->addressResourceModel, -+ 'request' => $this->request, -+ 'addressRepository' => $this->addressRepository, -+ ] -+ ); -+ } -+ -+ /** -+ * Unit test for \Magento\Customer\Block\Adminhtml\Edit\Address\DeleteButton::getButtonData() method -+ */ -+ public function testGetButtonData() -+ { -+ $addressId = 1; -+ $customerId = 2; -+ -+ /** @var \Magento\Customer\Model\Address|\PHPUnit_Framework_MockObject_MockObject $address */ -+ $address = $this->getMockBuilder(\Magento\Customer\Model\Address::class) -+ ->disableOriginalConstructor() -+ ->getMock(); -+ $address->expects($this->atLeastOnce())->method('getEntityId')->willReturn($addressId); -+ $address->expects($this->atLeastOnce())->method('getCustomerId')->willReturn($customerId); -+ $this->addressFactory->expects($this->atLeastOnce())->method('create')->willReturn($address); -+ $this->request->expects($this->atLeastOnce())->method('getParam')->with('entity_id') -+ ->willReturn($addressId); -+ $this->addressResourceModel->expects($this->atLeastOnce())->method('load')->with($address, $addressId); -+ $this->addressRepository->expects($this->atLeastOnce())->method('getById')->with($addressId) -+ ->willReturn($address); -+ $this->urlBuilder->expects($this->atLeastOnce())->method('getUrl') -+ ->with( -+ \Magento\Customer\Ui\Component\Listing\Address\Column\Actions::CUSTOMER_ADDRESS_PATH_DELETE, -+ ['parent_id' => $customerId, 'id' => $addressId] -+ )->willReturn('url'); -+ -+ $buttonData = $this->deleteButton->getButtonData(); -+ $this->assertEquals('Delete', (string)$buttonData['label']); -+ } -+} -diff --git a/app/code/Magento/Customer/Test/Unit/Block/Form/RegisterTest.php b/app/code/Magento/Customer/Test/Unit/Block/Form/RegisterTest.php -index f1629d61fe9..b93b9f40d75 100644 ---- a/app/code/Magento/Customer/Test/Unit/Block/Form/RegisterTest.php -+++ b/app/code/Magento/Customer/Test/Unit/Block/Form/RegisterTest.php -@@ -7,6 +7,7 @@ namespace Magento\Customer\Test\Unit\Block\Form; - - use Magento\Customer\Block\Form\Register; - use Magento\Customer\Model\AccountManagement; -+use Magento\Newsletter\Observer\PredispatchNewsletterObserver; - - /** - * Test class for \Magento\Customer\Block\Form\Register. -@@ -39,7 +40,7 @@ class RegisterTest extends \PHPUnit\Framework\TestCase - /** @var \PHPUnit_Framework_MockObject_MockObject | \Magento\Customer\Model\Session */ - private $_customerSession; - -- /** @var \PHPUnit_Framework_MockObject_MockObject | \Magento\Framework\Module\Manager */ -+ /** @var \PHPUnit_Framework_MockObject_MockObject | \Magento\Framework\Module\ModuleManagerInterface */ - private $_moduleManager; - - /** @var \PHPUnit_Framework_MockObject_MockObject | \Magento\Customer\Model\Url */ -@@ -274,12 +275,13 @@ class RegisterTest extends \PHPUnit\Framework\TestCase - } - - /** -- * @param $isNewsletterEnabled -- * @param $expectedValue -+ * @param boolean $isNewsletterEnabled -+ * @param string $isNewsletterActive -+ * @param boolean $expectedValue - * - * @dataProvider isNewsletterEnabledProvider - */ -- public function testIsNewsletterEnabled($isNewsletterEnabled, $expectedValue) -+ public function testIsNewsletterEnabled($isNewsletterEnabled, $isNewsletterActive, $expectedValue) - { - $this->_moduleManager->expects( - $this->once() -@@ -290,6 +292,17 @@ class RegisterTest extends \PHPUnit\Framework\TestCase - )->will( - $this->returnValue($isNewsletterEnabled) - ); -+ -+ $this->_scopeConfig->expects( -+ $this->any() -+ )->method( -+ 'getValue' -+ )->with( -+ PredispatchNewsletterObserver::XML_PATH_NEWSLETTER_ACTIVE -+ )->will( -+ $this->returnValue($isNewsletterActive) -+ ); -+ - $this->assertEquals($expectedValue, $this->_block->isNewsletterEnabled()); - } - -@@ -298,7 +311,7 @@ class RegisterTest extends \PHPUnit\Framework\TestCase - */ - public function isNewsletterEnabledProvider() - { -- return [[true, true], [false, false]]; -+ return [[true, true, true], [true, false, false], [false, true, false], [false, false, false]]; - } - - /** -diff --git a/app/code/Magento/Customer/Test/Unit/Controller/Account/CreatePasswordTest.php b/app/code/Magento/Customer/Test/Unit/Controller/Account/CreatePasswordTest.php -deleted file mode 100644 -index 77f41024ba0..00000000000 ---- a/app/code/Magento/Customer/Test/Unit/Controller/Account/CreatePasswordTest.php -+++ /dev/null -@@ -1,233 +0,0 @@ --<?php --/** -- * Copyright © Magento, Inc. All rights reserved. -- * See COPYING.txt for license details. -- */ --namespace Magento\Customer\Test\Unit\Controller\Account; -- --use Magento\Framework\Controller\Result\Redirect; --use Magento\Framework\TestFramework\Unit\Helper\ObjectManager as ObjectManagerHelper; -- --/** -- * @SuppressWarnings(PHPMD.CouplingBetweenObjects) -- */ --class CreatePasswordTest extends \PHPUnit\Framework\TestCase --{ -- /** @var \Magento\Customer\Controller\Account\CreatePassword */ -- protected $model; -- -- /** @var ObjectManagerHelper */ -- protected $objectManagerHelper; -- -- /** @var \Magento\Customer\Model\Session|\PHPUnit_Framework_MockObject_MockObject */ -- protected $sessionMock; -- -- /** @var \Magento\Framework\View\Result\PageFactory|\PHPUnit_Framework_MockObject_MockObject */ -- protected $pageFactoryMock; -- -- /** @var \Magento\Customer\Api\AccountManagementInterface|\PHPUnit_Framework_MockObject_MockObject */ -- protected $accountManagementMock; -- -- /** @var \Magento\Framework\App\RequestInterface|\PHPUnit_Framework_MockObject_MockObject */ -- protected $requestMock; -- -- /** @var \Magento\Framework\Controller\Result\RedirectFactory|\PHPUnit_Framework_MockObject_MockObject */ -- protected $redirectFactoryMock; -- -- /** @var \Magento\Framework\Message\ManagerInterface|\PHPUnit_Framework_MockObject_MockObject */ -- protected $messageManagerMock; -- -- protected function setUp() -- { -- $this->sessionMock = $this->getMockBuilder(\Magento\Customer\Model\Session::class) -- ->disableOriginalConstructor() -- ->setMethods(['setRpToken', 'setRpCustomerId', 'getRpToken', 'getRpCustomerId']) -- ->getMock(); -- $this->pageFactoryMock = $this->getMockBuilder(\Magento\Framework\View\Result\PageFactory::class) -- ->disableOriginalConstructor() -- ->getMock(); -- $this->accountManagementMock = $this->getMockBuilder(\Magento\Customer\Api\AccountManagementInterface::class) -- ->getMockForAbstractClass(); -- $this->requestMock = $this->getMockBuilder(\Magento\Framework\App\RequestInterface::class) -- ->getMockForAbstractClass(); -- $this->redirectFactoryMock = $this->getMockBuilder(\Magento\Framework\Controller\Result\RedirectFactory::class) -- ->disableOriginalConstructor() -- ->getMock(); -- $this->messageManagerMock = $this->getMockBuilder(\Magento\Framework\Message\ManagerInterface::class) -- ->getMockForAbstractClass(); -- -- $this->objectManagerHelper = new ObjectManagerHelper($this); -- $this->model = $this->objectManagerHelper->getObject( -- \Magento\Customer\Controller\Account\CreatePassword::class, -- [ -- 'customerSession' => $this->sessionMock, -- 'resultPageFactory' => $this->pageFactoryMock, -- 'accountManagement' => $this->accountManagementMock, -- 'request' => $this->requestMock, -- 'resultRedirectFactory' => $this->redirectFactoryMock, -- 'messageManager' => $this->messageManagerMock, -- ] -- ); -- } -- -- public function testExecuteWithLink() -- { -- $token = 'token'; -- $customerId = '11'; -- -- $this->requestMock->expects($this->exactly(2)) -- ->method('getParam') -- ->willReturnMap( -- [ -- ['token', null, $token], -- ['id', null, $customerId], -- ] -- ); -- -- $this->accountManagementMock->expects($this->once()) -- ->method('validateResetPasswordLinkToken') -- ->with($customerId, $token) -- ->willReturn(true); -- -- $this->sessionMock->expects($this->once()) -- ->method('setRpToken') -- ->with($token); -- $this->sessionMock->expects($this->once()) -- ->method('setRpCustomerId') -- ->with($customerId); -- -- /** @var Redirect|\PHPUnit_Framework_MockObject_MockObject $redirectMock */ -- $redirectMock = $this->getMockBuilder(\Magento\Framework\Controller\Result\Redirect::class) -- ->disableOriginalConstructor() -- ->getMock(); -- -- $this->redirectFactoryMock->expects($this->once()) -- ->method('create') -- ->with([]) -- ->willReturn($redirectMock); -- -- $redirectMock->expects($this->once()) -- ->method('setPath') -- ->with('*/*/createpassword', []) -- ->willReturnSelf(); -- -- $this->assertEquals($redirectMock, $this->model->execute()); -- } -- -- public function testExecuteWithSession() -- { -- $token = 'token'; -- $customerId = '11'; -- -- $this->requestMock->expects($this->exactly(2)) -- ->method('getParam') -- ->willReturnMap( -- [ -- ['token', null, null], -- ['id', null, $customerId], -- ] -- ); -- -- $this->sessionMock->expects($this->once()) -- ->method('getRpToken') -- ->willReturn($token); -- $this->sessionMock->expects($this->once()) -- ->method('getRpCustomerId') -- ->willReturn($customerId); -- -- $this->accountManagementMock->expects($this->once()) -- ->method('validateResetPasswordLinkToken') -- ->with($customerId, $token) -- ->willReturn(true); -- -- /** @var \Magento\Framework\View\Result\Page|\PHPUnit_Framework_MockObject_MockObject $pageMock */ -- $pageMock = $this->getMockBuilder(\Magento\Framework\View\Result\Page::class) -- ->disableOriginalConstructor() -- ->getMock(); -- -- $this->pageFactoryMock->expects($this->once()) -- ->method('create') -- ->with(false, []) -- ->willReturn($pageMock); -- -- /** @var \Magento\Framework\View\Layout|\PHPUnit_Framework_MockObject_MockObject $layoutMock */ -- $layoutMock = $this->getMockBuilder(\Magento\Framework\View\Layout::class) -- ->disableOriginalConstructor() -- ->getMock(); -- -- $pageMock->expects($this->once()) -- ->method('getLayout') -- ->willReturn($layoutMock); -- -- /** @var \Magento\Customer\Block\Account\Resetpassword|\PHPUnit_Framework_MockObject_MockObject $layoutMock */ -- $blockMock = $this->getMockBuilder(\Magento\Customer\Block\Account\Resetpassword::class) -- ->disableOriginalConstructor() -- ->setMethods(['setCustomerId', 'setResetPasswordLinkToken']) -- ->getMock(); -- -- $layoutMock->expects($this->once()) -- ->method('getBlock') -- ->with('resetPassword') -- ->willReturn($blockMock); -- -- $blockMock->expects($this->once()) -- ->method('setCustomerId') -- ->with($customerId) -- ->willReturnSelf(); -- $blockMock->expects($this->once()) -- ->method('setResetPasswordLinkToken') -- ->with($token) -- ->willReturnSelf(); -- -- $this->assertEquals($pageMock, $this->model->execute()); -- } -- -- public function testExecuteWithException() -- { -- $token = 'token'; -- $customerId = '11'; -- -- $this->requestMock->expects($this->exactly(2)) -- ->method('getParam') -- ->willReturnMap( -- [ -- ['token', null, $token], -- ['id', null, null], -- ] -- ); -- -- $this->sessionMock->expects($this->once()) -- ->method('getRpToken') -- ->willReturn($token); -- $this->sessionMock->expects($this->once()) -- ->method('getRpCustomerId') -- ->willReturn($customerId); -- -- $this->accountManagementMock->expects($this->once()) -- ->method('validateResetPasswordLinkToken') -- ->with($customerId, $token) -- ->willThrowException(new \Exception('Exception.')); -- -- $this->messageManagerMock->expects($this->once()) -- ->method('addError') -- ->with(__('Your password reset link has expired.')) -- ->willReturnSelf(); -- -- /** @var Redirect|\PHPUnit_Framework_MockObject_MockObject $redirectMock */ -- $redirectMock = $this->getMockBuilder(\Magento\Framework\Controller\Result\Redirect::class) -- ->disableOriginalConstructor() -- ->getMock(); -- -- $this->redirectFactoryMock->expects($this->once()) -- ->method('create') -- ->with([]) -- ->willReturn($redirectMock); -- -- $redirectMock->expects($this->once()) -- ->method('setPath') -- ->with('*/*/forgotpassword', []) -- ->willReturnSelf(); -- -- $this->assertEquals($redirectMock, $this->model->execute()); -- } --} -diff --git a/app/code/Magento/Customer/Test/Unit/Controller/Account/ResetPasswordPostTest.php b/app/code/Magento/Customer/Test/Unit/Controller/Account/ResetPasswordPostTest.php -deleted file mode 100644 -index b79ad008e5e..00000000000 ---- a/app/code/Magento/Customer/Test/Unit/Controller/Account/ResetPasswordPostTest.php -+++ /dev/null -@@ -1,379 +0,0 @@ --<?php --/** -- * Copyright © Magento, Inc. All rights reserved. -- * See COPYING.txt for license details. -- */ --namespace Magento\Customer\Test\Unit\Controller\Account; -- --use Magento\Framework\Controller\Result\Redirect; --use Magento\Framework\TestFramework\Unit\Helper\ObjectManager as ObjectManagerHelper; -- --/** -- * @SuppressWarnings(PHPMD.CouplingBetweenObjects) -- */ --class ResetPasswordPostTest extends \PHPUnit\Framework\TestCase --{ -- /** @var \Magento\Customer\Controller\Account\ResetPasswordPost */ -- protected $model; -- -- /** @var ObjectManagerHelper */ -- protected $objectManagerHelper; -- -- /** @var \Magento\Customer\Model\Session|\PHPUnit_Framework_MockObject_MockObject */ -- protected $sessionMock; -- -- /** @var \Magento\Framework\View\Result\PageFactory|\PHPUnit_Framework_MockObject_MockObject */ -- protected $pageFactoryMock; -- -- /** @var \Magento\Customer\Api\AccountManagementInterface|\PHPUnit_Framework_MockObject_MockObject */ -- protected $accountManagementMock; -- -- /** @var \Magento\Customer\Api\CustomerRepositoryInterface|\PHPUnit_Framework_MockObject_MockObject */ -- protected $customerRepositoryMock; -- -- /** @var \Magento\Framework\App\RequestInterface|\PHPUnit_Framework_MockObject_MockObject */ -- protected $requestMock; -- -- /** @var \Magento\Framework\Controller\Result\RedirectFactory|\PHPUnit_Framework_MockObject_MockObject */ -- protected $redirectFactoryMock; -- -- /** @var \Magento\Framework\Message\ManagerInterface|\PHPUnit_Framework_MockObject_MockObject */ -- protected $messageManagerMock; -- -- protected function setUp() -- { -- $this->sessionMock = $this->getMockBuilder(\Magento\Customer\Model\Session::class) -- ->disableOriginalConstructor() -- ->setMethods(['unsRpToken', 'unsRpCustomerId']) -- ->getMock(); -- $this->pageFactoryMock = $this->getMockBuilder(\Magento\Framework\View\Result\PageFactory::class) -- ->disableOriginalConstructor() -- ->getMock(); -- $this->accountManagementMock = $this->getMockBuilder(\Magento\Customer\Api\AccountManagementInterface::class) -- ->getMockForAbstractClass(); -- $this->customerRepositoryMock = $this->getMockBuilder(\Magento\Customer\Api\CustomerRepositoryInterface::class) -- ->getMockForAbstractClass(); -- $this->requestMock = $this->getMockBuilder(\Magento\Framework\App\RequestInterface::class) -- ->setMethods(['getQuery', 'getPost']) -- ->getMockForAbstractClass(); -- $this->redirectFactoryMock = $this->getMockBuilder(\Magento\Framework\Controller\Result\RedirectFactory::class) -- ->disableOriginalConstructor() -- ->getMock(); -- $this->messageManagerMock = $this->getMockBuilder(\Magento\Framework\Message\ManagerInterface::class) -- ->getMockForAbstractClass(); -- -- $this->objectManagerHelper = new ObjectManagerHelper($this); -- $this->model = $this->objectManagerHelper->getObject( -- \Magento\Customer\Controller\Account\ResetPasswordPost::class, -- [ -- 'customerSession' => $this->sessionMock, -- 'resultPageFactory' => $this->pageFactoryMock, -- 'accountManagement' => $this->accountManagementMock, -- 'customerRepository' => $this->customerRepositoryMock, -- 'request' => $this->requestMock, -- 'resultRedirectFactory' => $this->redirectFactoryMock, -- 'messageManager' => $this->messageManagerMock, -- ] -- ); -- } -- -- public function testExecute() -- { -- $token = 'token'; -- $customerId = '11'; -- $password = 'password'; -- $passwordConfirmation = 'password'; -- $email = 'email@email.com'; -- -- $this->requestMock->expects($this->exactly(2)) -- ->method('getQuery') -- ->willReturnMap( -- [ -- ['token', $token], -- ['id', $customerId], -- ] -- ); -- $this->requestMock->expects($this->exactly(2)) -- ->method('getPost') -- ->willReturnMap( -- [ -- ['password', $password], -- ['password_confirmation', $passwordConfirmation], -- ] -- ); -- -- /** @var \Magento\Customer\Api\Data\CustomerInterface|\PHPUnit_Framework_MockObject_MockObject $customerMock */ -- $customerMock = $this->getMockBuilder(\Magento\Customer\Api\Data\CustomerInterface::class) -- ->getMockForAbstractClass(); -- -- $this->customerRepositoryMock->expects($this->once()) -- ->method('getById') -- ->with($customerId) -- ->willReturn($customerMock); -- -- $customerMock->expects($this->once()) -- ->method('getEmail') -- ->willReturn($email); -- -- $this->accountManagementMock->expects($this->once()) -- ->method('resetPassword') -- ->with($email, $token, $password) -- ->willReturn(true); -- -- $this->sessionMock->expects($this->once()) -- ->method('unsRpToken'); -- $this->sessionMock->expects($this->once()) -- ->method('unsRpCustomerId'); -- -- $this->messageManagerMock->expects($this->once()) -- ->method('addSuccess') -- ->with(__('You updated your password.')) -- ->willReturnSelf(); -- -- /** @var Redirect|\PHPUnit_Framework_MockObject_MockObject $redirectMock */ -- $redirectMock = $this->getMockBuilder(\Magento\Framework\Controller\Result\Redirect::class) -- ->disableOriginalConstructor() -- ->getMock(); -- -- $this->redirectFactoryMock->expects($this->once()) -- ->method('create') -- ->with([]) -- ->willReturn($redirectMock); -- -- $redirectMock->expects($this->once()) -- ->method('setPath') -- ->with('*/*/login', []) -- ->willReturnSelf(); -- -- $this->assertEquals($redirectMock, $this->model->execute()); -- } -- -- public function testExecuteWithException() -- { -- $token = 'token'; -- $customerId = '11'; -- $password = 'password'; -- $passwordConfirmation = 'password'; -- $email = 'email@email.com'; -- -- $this->requestMock->expects($this->exactly(2)) -- ->method('getQuery') -- ->willReturnMap( -- [ -- ['token', $token], -- ['id', $customerId], -- ] -- ); -- $this->requestMock->expects($this->exactly(2)) -- ->method('getPost') -- ->willReturnMap( -- [ -- ['password', $password], -- ['password_confirmation', $passwordConfirmation], -- ] -- ); -- -- /** @var \Magento\Customer\Api\Data\CustomerInterface|\PHPUnit_Framework_MockObject_MockObject $customerMock */ -- $customerMock = $this->getMockBuilder(\Magento\Customer\Api\Data\CustomerInterface::class) -- ->getMockForAbstractClass(); -- -- $this->customerRepositoryMock->expects($this->once()) -- ->method('getById') -- ->with($customerId) -- ->willReturn($customerMock); -- -- $customerMock->expects($this->once()) -- ->method('getEmail') -- ->willReturn($email); -- -- $this->accountManagementMock->expects($this->once()) -- ->method('resetPassword') -- ->with($email, $token, $password) -- ->willThrowException(new \Exception('Exception.')); -- -- $this->messageManagerMock->expects($this->once()) -- ->method('addError') -- ->with(__('Something went wrong while saving the new password.')) -- ->willReturnSelf(); -- -- /** @var Redirect|\PHPUnit_Framework_MockObject_MockObject $redirectMock */ -- $redirectMock = $this->getMockBuilder(\Magento\Framework\Controller\Result\Redirect::class) -- ->disableOriginalConstructor() -- ->getMock(); -- -- $this->redirectFactoryMock->expects($this->once()) -- ->method('create') -- ->with([]) -- ->willReturn($redirectMock); -- -- $redirectMock->expects($this->once()) -- ->method('setPath') -- ->with('*/*/createPassword', ['id' => $customerId, 'token' => $token]) -- ->willReturnSelf(); -- -- $this->assertEquals($redirectMock, $this->model->execute()); -- } -- -- /** -- * Test for InputException -- */ -- public function testExecuteWithInputException() -- { -- $token = 'token'; -- $customerId = '11'; -- $password = 'password'; -- $passwordConfirmation = 'password'; -- $email = 'email@email.com'; -- -- $this->requestMock->expects($this->exactly(2)) -- ->method('getQuery') -- ->willReturnMap( -- [ -- ['token', $token], -- ['id', $customerId], -- ] -- ); -- $this->requestMock->expects($this->exactly(2)) -- ->method('getPost') -- ->willReturnMap( -- [ -- ['password', $password], -- ['password_confirmation', $passwordConfirmation], -- ] -- ); -- -- /** @var \Magento\Customer\Api\Data\CustomerInterface|\PHPUnit_Framework_MockObject_MockObject $customerMock */ -- $customerMock = $this->getMockBuilder(\Magento\Customer\Api\Data\CustomerInterface::class) -- ->getMockForAbstractClass(); -- -- $this->customerRepositoryMock->expects($this->once()) -- ->method('getById') -- ->with($customerId) -- ->willReturn($customerMock); -- -- $customerMock->expects($this->once()) -- ->method('getEmail') -- ->willReturn($email); -- -- $this->accountManagementMock->expects($this->once()) -- ->method('resetPassword') -- ->with($email, $token, $password) -- ->willThrowException(new \Magento\Framework\Exception\InputException(__('InputException.'))); -- -- $this->messageManagerMock->expects($this->once()) -- ->method('addError') -- ->with(__('InputException.')) -- ->willReturnSelf(); -- -- /** @var Redirect|\PHPUnit_Framework_MockObject_MockObject $redirectMock */ -- $redirectMock = $this->getMockBuilder(\Magento\Framework\Controller\Result\Redirect::class) -- ->disableOriginalConstructor() -- ->getMock(); -- -- $this->redirectFactoryMock->expects($this->once()) -- ->method('create') -- ->with([]) -- ->willReturn($redirectMock); -- -- $redirectMock->expects($this->once()) -- ->method('setPath') -- ->with('*/*/createPassword', ['id' => $customerId, 'token' => $token]) -- ->willReturnSelf(); -- -- $this->assertEquals($redirectMock, $this->model->execute()); -- } -- -- public function testExecuteWithWrongConfirmation() -- { -- $token = 'token'; -- $customerId = '11'; -- $password = 'password'; -- $passwordConfirmation = 'wrong_password'; -- -- $this->requestMock->expects($this->exactly(2)) -- ->method('getQuery') -- ->willReturnMap( -- [ -- ['token', $token], -- ['id', $customerId], -- ] -- ); -- $this->requestMock->expects($this->exactly(2)) -- ->method('getPost') -- ->willReturnMap( -- [ -- ['password', $password], -- ['password_confirmation', $passwordConfirmation], -- ] -- ); -- -- $this->messageManagerMock->expects($this->once()) -- ->method('addError') -- ->with(__('New Password and Confirm New Password values didn\'t match.')) -- ->willReturnSelf(); -- -- /** @var Redirect|\PHPUnit_Framework_MockObject_MockObject $redirectMock */ -- $redirectMock = $this->getMockBuilder(\Magento\Framework\Controller\Result\Redirect::class) -- ->disableOriginalConstructor() -- ->getMock(); -- -- $this->redirectFactoryMock->expects($this->once()) -- ->method('create') -- ->with([]) -- ->willReturn($redirectMock); -- -- $redirectMock->expects($this->once()) -- ->method('setPath') -- ->with('*/*/createPassword', ['id' => $customerId, 'token' => $token]) -- ->willReturnSelf(); -- -- $this->assertEquals($redirectMock, $this->model->execute()); -- } -- -- public function testExecuteWithEmptyPassword() -- { -- $token = 'token'; -- $customerId = '11'; -- $password = ''; -- $passwordConfirmation = ''; -- -- $this->requestMock->expects($this->exactly(2)) -- ->method('getQuery') -- ->willReturnMap( -- [ -- ['token', $token], -- ['id', $customerId], -- ] -- ); -- $this->requestMock->expects($this->exactly(2)) -- ->method('getPost') -- ->willReturnMap( -- [ -- ['password', $password], -- ['password_confirmation', $passwordConfirmation], -- ] -- ); -- -- $this->messageManagerMock->expects($this->once()) -- ->method('addError') -- ->with(__('Please enter a new password.')) -- ->willReturnSelf(); -- -- /** @var Redirect|\PHPUnit_Framework_MockObject_MockObject $redirectMock */ -- $redirectMock = $this->getMockBuilder(\Magento\Framework\Controller\Result\Redirect::class) -- ->disableOriginalConstructor() -- ->getMock(); -- -- $this->redirectFactoryMock->expects($this->once()) -- ->method('create') -- ->with([]) -- ->willReturn($redirectMock); -- -- $redirectMock->expects($this->once()) -- ->method('setPath') -- ->with('*/*/createPassword', ['id' => $customerId, 'token' => $token]) -- ->willReturnSelf(); -- -- $this->assertEquals($redirectMock, $this->model->execute()); -- } --} -diff --git a/app/code/Magento/Customer/Test/Unit/Controller/Address/FormPostTest.php b/app/code/Magento/Customer/Test/Unit/Controller/Address/FormPostTest.php -index c2a795fc950..7ae55f44421 100644 ---- a/app/code/Magento/Customer/Test/Unit/Controller/Address/FormPostTest.php -+++ b/app/code/Magento/Customer/Test/Unit/Controller/Address/FormPostTest.php -@@ -455,14 +455,20 @@ class FormPostTest extends \PHPUnit\Framework\TestCase - $regionCode, - $newRegionId, - $newRegion, -- $newRegionCode -+ $newRegionCode, -+ $existingDefaultBilling = false, -+ $existingDefaultShipping = false, -+ $setDefaultBilling = false, -+ $setDefaultShipping = false - ): void { - $existingAddressData = [ - 'country_id' => $countryId, - 'region_id' => $regionId, - 'region' => $region, - 'region_code' => $regionCode, -- 'customer_id' => $customerId -+ 'customer_id' => $customerId, -+ 'default_billing' => $existingDefaultBilling, -+ 'default_shipping' => $existingDefaultShipping, - ]; - $newAddressData = [ - 'country_id' => $countryId, -@@ -486,8 +492,8 @@ class FormPostTest extends \PHPUnit\Framework\TestCase - ->method('getParam') - ->willReturnMap([ - ['id', null, $addressId], -- ['default_billing', false, $addressId], -- ['default_shipping', false, $addressId], -+ ['default_billing', $existingDefaultBilling, $setDefaultBilling], -+ ['default_shipping', $existingDefaultShipping, $setDefaultShipping], - ]); - - $this->addressRepository->expects($this->once()) -@@ -565,11 +571,11 @@ class FormPostTest extends \PHPUnit\Framework\TestCase - ->willReturnSelf(); - $this->addressData->expects($this->once()) - ->method('setIsDefaultBilling') -- ->with() -+ ->with($setDefaultBilling) - ->willReturnSelf(); - $this->addressData->expects($this->once()) - ->method('setIsDefaultShipping') -- ->with() -+ ->with($setDefaultShipping) - ->willReturnSelf(); - - $this->messageManager->expects($this->once()) -@@ -628,11 +634,11 @@ class FormPostTest extends \PHPUnit\Framework\TestCase - - [1, 1, 1, 2, null, null, 12, null, null], - [1, 1, 1, 2, 'Alaska', null, 12, null, 'CA'], -- [1, 1, 1, 2, 'Alaska', 'AK', 12, 'California', null], -+ [1, 1, 1, 2, 'Alaska', 'AK', 12, 'California', null, true, true, true, false], - -- [1, 1, 1, 2, null, null, 12, null, null], -- [1, 1, 1, 2, 'Alaska', null, 12, null, 'CA'], -- [1, 1, 1, 2, 'Alaska', 'AK', 12, 'California', null], -+ [1, 1, 1, 2, null, null, 12, null, null, false, false, true, false], -+ [1, 1, 1, 2, 'Alaska', null, 12, null, 'CA', true, false, true, false], -+ [1, 1, 1, 2, 'Alaska', 'AK', 12, 'California', null, true, true, true, true], - ]; - } - -diff --git a/app/code/Magento/Customer/Test/Unit/Controller/Address/SaveTest.php b/app/code/Magento/Customer/Test/Unit/Controller/Address/SaveTest.php -new file mode 100644 -index 00000000000..1bf881ff0a9 ---- /dev/null -+++ b/app/code/Magento/Customer/Test/Unit/Controller/Address/SaveTest.php -@@ -0,0 +1,213 @@ -+<?php -+declare(strict_types=1); -+/** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+ -+namespace Magento\Customer\Test\Unit\Controller\Address; -+ -+use Magento\Customer\Api\Data\AddressInterface; -+use Magento\Framework\Controller\Result\Json; -+use Magento\Framework\Controller\Result\JsonFactory; -+use Magento\Framework\TestFramework\Unit\Helper\ObjectManager as ObjectManagerHelper; -+ -+/** -+ * @SuppressWarnings(PHPMD.CouplingBetweenObjects) -+ */ -+class SaveTest extends \PHPUnit\Framework\TestCase -+{ -+ /** -+ * @var \Magento\Customer\Controller\Adminhtml\Address\Save -+ */ -+ private $model; -+ -+ /** -+ * @var \Magento\Customer\Api\AddressRepositoryInterface|\PHPUnit_Framework_MockObject_MockObject -+ */ -+ private $addressRepositoryMock; -+ -+ /** -+ * @var \Magento\Customer\Model\Metadata\FormFactory|\PHPUnit_Framework_MockObject_MockObject -+ */ -+ private $formFactoryMock; -+ -+ /** -+ * @var \Magento\Customer\Api\CustomerRepositoryInterface|\PHPUnit_Framework_MockObject_MockObject -+ */ -+ private $customerRepositoryMock; -+ -+ /** -+ * @var \Magento\Framework\Api\DataObjectHelper|\PHPUnit_Framework_MockObject_MockObject -+ */ -+ private $dataObjectHelperMock; -+ -+ /** -+ * @var \Magento\Customer\Api\Data\AddressInterfaceFactory|\PHPUnit_Framework_MockObject_MockObject -+ */ -+ private $addressDataFactoryMock; -+ -+ /** -+ * @var \Psr\Log\LoggerInterface|\PHPUnit_Framework_MockObject_MockObject -+ */ -+ private $loggerMock; -+ -+ /** -+ * @var \Magento\Framework\App\RequestInterface|\PHPUnit_Framework_MockObject_MockObject -+ */ -+ private $requestMock; -+ -+ /** -+ * @var AddressInterface|\PHPUnit_Framework_MockObject_MockObject -+ */ -+ private $address; -+ -+ /** -+ * @var JsonFactory|\PHPUnit_Framework_MockObject_MockObject -+ */ -+ private $resultJsonFactory; -+ -+ /** -+ * @var Json|\PHPUnit_Framework_MockObject_MockObject -+ */ -+ private $json; -+ -+ /** -+ * @inheritdoc -+ */ -+ protected function setUp() -+ { -+ $this->addressRepositoryMock = $this->createMock(\Magento\Customer\Api\AddressRepositoryInterface::class); -+ $this->formFactoryMock = $this->createMock(\Magento\Customer\Model\Metadata\FormFactory::class); -+ $this->customerRepositoryMock = $this->createMock(\Magento\Customer\Api\CustomerRepositoryInterface::class); -+ $this->dataObjectHelperMock = $this->createMock(\Magento\Framework\Api\DataObjectHelper ::class); -+ $this->addressDataFactoryMock = $this->createMock(\Magento\Customer\Api\Data\AddressInterfaceFactory::class); -+ $this->loggerMock = $this->createMock(\Psr\Log\LoggerInterface::class); -+ $this->requestMock = $this->createMock(\Magento\Framework\App\RequestInterface::class); -+ $this->address = $this->getMockBuilder(AddressInterface::class) -+ ->disableOriginalConstructor() -+ ->getMockForAbstractClass(); -+ $this->resultJsonFactory = $this->getMockBuilder(JsonFactory::class) -+ ->disableOriginalConstructor() -+ ->setMethods(['create']) -+ ->getMock(); -+ $this->json = $this->getMockBuilder(Json::class) -+ ->disableOriginalConstructor() -+ ->getMock(); -+ -+ $objectManager = new ObjectManagerHelper($this); -+ -+ $this->model = $objectManager->getObject( -+ \Magento\Customer\Controller\Adminhtml\Address\Save::class, -+ [ -+ 'addressRepository' => $this->addressRepositoryMock, -+ 'formFactory' => $this->formFactoryMock, -+ 'customerRepository' => $this->customerRepositoryMock, -+ 'dataObjectHelper' => $this->dataObjectHelperMock, -+ 'addressDataFactory' => $this->addressDataFactoryMock, -+ 'logger' => $this->loggerMock, -+ 'request' => $this->requestMock, -+ 'resultJsonFactory' => $this->resultJsonFactory -+ ] -+ ); -+ } -+ -+ public function testExecute(): void -+ { -+ $addressId = 11; -+ $customerId = 22; -+ -+ $addressExtractedData = [ -+ 'entity_id' => $addressId, -+ 'code' => 'value', -+ 'coolness' => false, -+ 'region' => 'region', -+ 'region_id' => 'region_id', -+ ]; -+ -+ $addressCompactedData = [ -+ 'entity_id' => $addressId, -+ 'default_billing' => 'true', -+ 'default_shipping' => 'true', -+ 'code' => 'value', -+ 'coolness' => false, -+ 'region' => 'region', -+ 'region_id' => 'region_id', -+ ]; -+ -+ $mergedAddressData = [ -+ 'entity_id' => $addressId, -+ 'default_billing' => true, -+ 'default_shipping' => true, -+ 'code' => 'value', -+ 'region' => [ -+ 'region' => 'region', -+ 'region_id' => 'region_id', -+ ], -+ 'region_id' => 'region_id', -+ 'id' => $addressId, -+ ]; -+ -+ $this->requestMock->method('getParam') -+ ->withConsecutive(['parent_id'], ['entity_id']) -+ ->willReturnOnConsecutiveCalls(22, 1); -+ -+ $customerMock = $this->getMockBuilder( -+ \Magento\Customer\Api\Data\CustomerInterface::class -+ )->disableOriginalConstructor()->getMock(); -+ -+ $this->customerRepositoryMock->expects($this->atLeastOnce()) -+ ->method('getById') -+ ->with($customerId) -+ ->willReturn($customerMock); -+ -+ $customerAddressFormMock = $this->createMock(\Magento\Customer\Model\Metadata\Form::class); -+ $customerAddressFormMock->expects($this->atLeastOnce()) -+ ->method('extractData') -+ ->with($this->requestMock) -+ ->willReturn($addressExtractedData); -+ $customerAddressFormMock->expects($this->once()) -+ ->method('compactData') -+ ->with($addressExtractedData) -+ ->willReturn($addressCompactedData); -+ -+ $this->formFactoryMock->expects($this->exactly(1)) -+ ->method('create') -+ ->willReturn($customerAddressFormMock); -+ -+ $addressMock = $this->getMockBuilder(AddressInterface::class) -+ ->disableOriginalConstructor() -+ ->getMock(); -+ -+ $this->addressDataFactoryMock->expects($this->once())->method('create')->willReturn($addressMock); -+ -+ $this->dataObjectHelperMock->expects($this->atLeastOnce()) -+ ->method('populateWithArray') -+ ->willReturn( -+ [ -+ $addressMock, -+ $mergedAddressData, AddressInterface::class, -+ $this->dataObjectHelperMock, -+ ] -+ ); -+ $this->addressRepositoryMock->expects($this->once())->method('save')->willReturn($this->address); -+ $this->address->expects($this->once())->method('getId')->willReturn($addressId); -+ -+ $this->resultJsonFactory->expects($this->once()) -+ ->method('create') -+ ->willReturn($this->json); -+ $this->json->expects($this->once()) -+ ->method('setData') -+ ->with( -+ [ -+ 'message' => __('Customer address has been updated.'), -+ 'error' => false, -+ 'data' => [ -+ 'entity_id' => $addressId -+ ] -+ ] -+ )->willReturnSelf(); -+ -+ $this->assertEquals($this->json, $this->model->execute()); -+ } -+} -diff --git a/app/code/Magento/Customer/Test/Unit/Controller/Address/ValidateTest.php b/app/code/Magento/Customer/Test/Unit/Controller/Address/ValidateTest.php -new file mode 100644 -index 00000000000..a724bdd2495 ---- /dev/null -+++ b/app/code/Magento/Customer/Test/Unit/Controller/Address/ValidateTest.php -@@ -0,0 +1,118 @@ -+<?php -+declare(strict_types=1); -+/** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+ -+namespace Magento\Customer\Test\Unit\Controller\Address; -+ -+use Magento\Framework\TestFramework\Unit\Helper\ObjectManager as ObjectManagerHelper; -+ -+/** -+ * @SuppressWarnings(PHPMD.CouplingBetweenObjects) -+ */ -+class ValidateTest extends \PHPUnit\Framework\TestCase -+{ -+ /** -+ * @var \Magento\Customer\Controller\Adminhtml\Address\Validate -+ */ -+ private $model; -+ -+ /** -+ * @var \Magento\Customer\Model\Metadata\FormFactory|\PHPUnit_Framework_MockObject_MockObject -+ */ -+ private $formFactoryMock; -+ -+ /** -+ * @var \Magento\Framework\App\RequestInterface|\PHPUnit_Framework_MockObject_MockObject -+ */ -+ private $requestMock; -+ -+ /** -+ * @var \Magento\Backend\Model\View\Result\RedirectFactory|\PHPUnit_Framework_MockObject_MockObject -+ */ -+ private $resultRedirectFactoryMock; -+ -+ /** -+ * @var \Magento\Framework\Controller\Result\JsonFactory|\PHPUnit_Framework_MockObject_MockObject -+ */ -+ private $resultJsonFactoryMock; -+ -+ /** -+ * @inheritdoc -+ */ -+ protected function setUp() -+ { -+ $this->formFactoryMock = $this->createMock(\Magento\Customer\Model\Metadata\FormFactory::class); -+ $this->requestMock = $this->createMock(\Magento\Framework\App\RequestInterface::class); -+ $this->resultJsonFactoryMock = $this->createMock(\Magento\Framework\Controller\Result\JsonFactory::class); -+ $this->resultRedirectFactoryMock = $this->createMock(\Magento\Backend\Model\View\Result\RedirectFactory::class); -+ -+ $objectManager = new ObjectManagerHelper($this); -+ -+ $this->model = $objectManager->getObject( -+ \Magento\Customer\Controller\Adminhtml\Address\Validate::class, -+ [ -+ 'formFactory' => $this->formFactoryMock, -+ 'request' => $this->requestMock, -+ 'resultRedirectFactory' => $this->resultRedirectFactoryMock, -+ 'resultJsonFactory' => $this->resultJsonFactoryMock, -+ ] -+ ); -+ } -+ -+ /** -+ * Test method \Magento\Customer\Controller\Adminhtml\Address\Save::execute -+ * -+ * @throws \Magento\Framework\Exception\NoSuchEntityException -+ */ -+ public function testExecute() -+ { -+ $addressId = 11; -+ $errors = ['Error Message 1', 'Error Message 2']; -+ -+ $addressExtractedData = [ -+ 'entity_id' => $addressId, -+ 'default_billing' => true, -+ 'default_shipping' => true, -+ 'code' => 'value', -+ 'region' => [ -+ 'region' => 'region', -+ 'region_id' => 'region_id', -+ ], -+ 'region_id' => 'region_id', -+ 'id' => $addressId, -+ ]; -+ -+ $customerAddressFormMock = $this->createMock(\Magento\Customer\Model\Metadata\Form::class); -+ -+ $customerAddressFormMock->expects($this->atLeastOnce()) -+ ->method('extractData') -+ ->with($this->requestMock) -+ ->willReturn($addressExtractedData); -+ $customerAddressFormMock->expects($this->once()) -+ ->method('validateData') -+ ->with($addressExtractedData) -+ ->willReturn($errors); -+ -+ $this->formFactoryMock->expects($this->exactly(1)) -+ ->method('create') -+ ->willReturn($customerAddressFormMock); -+ -+ $resultJson = $this->createMock(\Magento\Framework\Controller\Result\Json::class); -+ $this->resultJsonFactoryMock->method('create') -+ ->willReturn($resultJson); -+ -+ $validateResponseMock = $this->createPartialMock( -+ \Magento\Framework\DataObject::class, -+ ['getError', 'setMessages'] -+ ); -+ $validateResponseMock->method('setMessages')->willReturnSelf(); -+ $validateResponseMock->method('getError')->willReturn(1); -+ -+ $resultJson->method('setData')->willReturnSelf(); -+ -+ $this->assertEquals($resultJson, $this->model->execute()); -+ } -+} -diff --git a/app/code/Magento/Customer/Test/Unit/Controller/Adminhtml/File/Address/UploadTest.php b/app/code/Magento/Customer/Test/Unit/Controller/Adminhtml/File/Address/UploadTest.php -index 20177ab0b0d..8f8ed0e37a4 100644 ---- a/app/code/Magento/Customer/Test/Unit/Controller/Adminhtml/File/Address/UploadTest.php -+++ b/app/code/Magento/Customer/Test/Unit/Controller/Adminhtml/File/Address/UploadTest.php -@@ -70,7 +70,8 @@ class UploadTest extends \PHPUnit\Framework\TestCase - $this->context, - $this->fileUploaderFactory, - $this->addressMetadataService, -- $this->logger -+ $this->logger, -+ 'address' - ); - } - -@@ -104,27 +105,27 @@ class UploadTest extends \PHPUnit\Framework\TestCase - - public function testExecute() - { -- $attributeCode = 'attribute_code'; -+ $attributeCode = 'file_address_attribute'; -+ $resultFileSize = 20000; -+ $resultFileName = 'text.txt'; -+ $resultType = 'text/plain'; - - $_FILES = [ -- 'address' => [ -- 'name' => [ -- 'new_0' => [ -- $attributeCode => 'filename', -- ], -- ], -+ $attributeCode => [ -+ 'name' => $resultFileName, -+ 'type' => $resultType, -+ 'size' => $resultFileSize - ], - ]; - -- $resultFileName = '/filename.ext1'; - $resultFilePath = 'filepath'; - $resultFileUrl = 'viewFileUrl'; - - $result = [ - 'name' => $resultFileName, -- 'file' => $resultFileName, -- 'path' => $resultFilePath, -- 'tmp_name' => $resultFilePath . $resultFileName, -+ 'type' => $resultType, -+ 'size' => $resultFileSize, -+ 'tmp_name' => $resultFilePath . '/' . $resultFileName, - 'url' => $resultFileUrl, - ]; - -@@ -173,15 +174,16 @@ class UploadTest extends \PHPUnit\Framework\TestCase - - public function testExecuteWithErrors() - { -- $attributeCode = 'attribute_code'; -+ $attributeCode = 'file_address_attribute'; -+ $resultFileSize = 20000; -+ $resultFileName = 'text.txt'; -+ $resultType = 'text/plain'; - - $_FILES = [ -- 'address' => [ -- 'name' => [ -- 'new_0' => [ -- $attributeCode => 'filename', -- ], -- ], -+ $attributeCode => [ -+ 'name' => $resultFileName, -+ 'type' => $resultType, -+ 'size' => $resultFileSize - ], - ]; - -diff --git a/app/code/Magento/Customer/Test/Unit/Controller/Adminhtml/Index/InlineEditTest.php b/app/code/Magento/Customer/Test/Unit/Controller/Adminhtml/Index/InlineEditTest.php -index 913c4107085..45e64f6557d 100644 ---- a/app/code/Magento/Customer/Test/Unit/Controller/Adminhtml/Index/InlineEditTest.php -+++ b/app/code/Magento/Customer/Test/Unit/Controller/Adminhtml/Index/InlineEditTest.php -@@ -5,9 +5,14 @@ - */ - namespace Magento\Customer\Test\Unit\Controller\Adminhtml\Index; - -+use Magento\Customer\Model\AddressRegistry; - use Magento\Customer\Model\EmailNotificationInterface; -+use Magento\Framework\DataObject; -+use Magento\Framework\Message\MessageInterface; - - /** -+ * Unit tests for Inline customer edit -+ * - * @SuppressWarnings(PHPMD.TooManyFields) - * @SuppressWarnings(PHPMD.CouplingBetweenObjects) - */ -@@ -67,14 +72,27 @@ class InlineEditTest extends \PHPUnit\Framework\TestCase - /** @var EmailNotificationInterface|\PHPUnit_Framework_MockObject_MockObject */ - private $emailNotification; - -+ /** @var AddressRegistry|\PHPUnit_Framework_MockObject_MockObject */ -+ private $addressRegistry; -+ - /** @var array */ - private $items; - -+ /** -+ * Sets up mocks -+ * -+ * @SuppressWarnings(PHPMD.ExcessiveMethodLength) -+ */ - protected function setUp() - { - $objectManager = new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this); - -- $this->request = $this->getMockForAbstractClass(\Magento\Framework\App\RequestInterface::class, [], '', false); -+ $this->request = $this->getMockForAbstractClass( -+ \Magento\Framework\App\RequestInterface::class, -+ [], -+ '', -+ false -+ ); - $this->messageManager = $this->getMockForAbstractClass( - \Magento\Framework\Message\ManagerInterface::class, - [], -@@ -124,8 +142,12 @@ class InlineEditTest extends \PHPUnit\Framework\TestCase - '', - false - ); -- $this->logger = $this->getMockForAbstractClass(\Psr\Log\LoggerInterface::class, [], '', false); -- -+ $this->logger = $this->getMockForAbstractClass( -+ \Psr\Log\LoggerInterface::class, -+ [], -+ '', -+ false -+ ); - $this->emailNotification = $this->getMockBuilder(EmailNotificationInterface::class) - ->disableOriginalConstructor() - ->getMock(); -@@ -137,6 +159,7 @@ class InlineEditTest extends \PHPUnit\Framework\TestCase - 'messageManager' => $this->messageManager, - ] - ); -+ $this->addressRegistry = $this->createMock(\Magento\Customer\Model\AddressRegistry::class); - $this->controller = $objectManager->getObject( - \Magento\Customer\Controller\Adminhtml\Index\InlineEdit::class, - [ -@@ -149,6 +172,7 @@ class InlineEditTest extends \PHPUnit\Framework\TestCase - 'addressDataFactory' => $this->addressDataFactory, - 'addressRepository' => $this->addressRepository, - 'logger' => $this->logger, -+ 'addressRegistry' => $this->addressRegistry - ] - ); - $reflection = new \ReflectionClass(get_class($this->controller)); -@@ -165,6 +189,8 @@ class InlineEditTest extends \PHPUnit\Framework\TestCase - } - - /** -+ * Prepare mocks for tests -+ * - * @param int $populateSequence - */ - protected function prepareMocksForTesting($populateSequence = 0) -@@ -203,6 +229,9 @@ class InlineEditTest extends \PHPUnit\Framework\TestCase - ->willReturn(12); - } - -+ /** -+ * Prepare mocks for update customers default billing address use case -+ */ - protected function prepareMocksForUpdateDefaultBilling() - { - $this->prepareMocksForProcessAddressData(); -@@ -211,12 +240,15 @@ class InlineEditTest extends \PHPUnit\Framework\TestCase - 'firstname' => 'Firstname', - 'lastname' => 'Lastname', - ]; -- $this->customerData->expects($this->once()) -+ $this->customerData->expects($this->exactly(2)) - ->method('getAddresses') - ->willReturn([$this->address]); - $this->address->expects($this->once()) - ->method('isDefaultBilling') - ->willReturn(true); -+ $this->addressRegistry->expects($this->once()) -+ ->method('retrieve') -+ ->willReturn(new DataObject()); - $this->dataObjectHelper->expects($this->at(0)) - ->method('populateWithArray') - ->with( -@@ -226,6 +258,9 @@ class InlineEditTest extends \PHPUnit\Framework\TestCase - ); - } - -+ /** -+ * Prepare mocks for processing customers address data use case -+ */ - protected function prepareMocksForProcessAddressData() - { - $this->customerData->expects($this->once()) -@@ -236,16 +271,20 @@ class InlineEditTest extends \PHPUnit\Framework\TestCase - ->willReturn('Lastname'); - } - -+ /** -+ * Prepare mocks for error messages processing test -+ */ - protected function prepareMocksForErrorMessagesProcessing() - { - $this->messageManager->expects($this->atLeastOnce()) - ->method('getMessages') - ->willReturn($this->messageCollection); - $this->messageCollection->expects($this->once()) -- ->method('getItems') -+ ->method('getErrors') - ->willReturn([$this->message]); - $this->messageCollection->expects($this->once()) -- ->method('getCount') -+ ->method('getCountByType') -+ ->with(MessageInterface::TYPE_ERROR) - ->willReturn(1); - $this->message->expects($this->once()) - ->method('getText') -@@ -259,6 +298,9 @@ class InlineEditTest extends \PHPUnit\Framework\TestCase - ->willReturnSelf(); - } - -+ /** -+ * Unit test for updating customers billing address use case -+ */ - public function testExecuteWithUpdateBilling() - { - $this->prepareMocksForTesting(1); -@@ -279,6 +321,9 @@ class InlineEditTest extends \PHPUnit\Framework\TestCase - $this->assertSame($this->resultJson, $this->controller->execute()); - } - -+ /** -+ * Unit test for creating customer with empty data use case -+ */ - public function testExecuteWithoutItems() - { - $this->resultJsonFactory->expects($this->once()) -@@ -303,6 +348,9 @@ class InlineEditTest extends \PHPUnit\Framework\TestCase - $this->assertSame($this->resultJson, $this->controller->execute()); - } - -+ /** -+ * Unit test for verifying Localized Exception during inline edit -+ */ - public function testExecuteLocalizedException() - { - $exception = new \Magento\Framework\Exception\LocalizedException(__('Exception message')); -@@ -310,6 +358,9 @@ class InlineEditTest extends \PHPUnit\Framework\TestCase - $this->customerData->expects($this->once()) - ->method('getDefaultBilling') - ->willReturn(false); -+ $this->customerData->expects($this->once()) -+ ->method('getAddresses') -+ ->willReturn([]); - $this->customerRepository->expects($this->once()) - ->method('save') - ->with($this->customerData) -@@ -325,6 +376,9 @@ class InlineEditTest extends \PHPUnit\Framework\TestCase - $this->assertSame($this->resultJson, $this->controller->execute()); - } - -+ /** -+ * Unit test for verifying Execute Exception during inline edit -+ */ - public function testExecuteException() - { - $exception = new \Exception('Exception message'); -@@ -332,6 +386,9 @@ class InlineEditTest extends \PHPUnit\Framework\TestCase - $this->customerData->expects($this->once()) - ->method('getDefaultBilling') - ->willReturn(false); -+ $this->customerData->expects($this->once()) -+ ->method('getAddresses') -+ ->willReturn([]); - $this->customerRepository->expects($this->once()) - ->method('save') - ->with($this->customerData) -diff --git a/app/code/Magento/Customer/Test/Unit/Controller/Adminhtml/Index/MassAssignGroupTest.php b/app/code/Magento/Customer/Test/Unit/Controller/Adminhtml/Index/MassAssignGroupTest.php -index 884aab711d1..10144bdc318 100644 ---- a/app/code/Magento/Customer/Test/Unit/Controller/Adminhtml/Index/MassAssignGroupTest.php -+++ b/app/code/Magento/Customer/Test/Unit/Controller/Adminhtml/Index/MassAssignGroupTest.php -@@ -11,6 +11,7 @@ use Magento\Framework\TestFramework\Unit\Helper\ObjectManager as ObjectManagerHe - - /** - * Class MassAssignGroupTest -+ * - * @SuppressWarnings(PHPMD.CouplingBetweenObjects) - */ - class MassAssignGroupTest extends \PHPUnit\Framework\TestCase -@@ -70,12 +71,17 @@ class MassAssignGroupTest extends \PHPUnit\Framework\TestCase - */ - protected $customerRepositoryMock; - -+ /** -+ * @inheritdoc -+ */ - protected function setUp() - { - $objectManagerHelper = new ObjectManagerHelper($this); - - $this->contextMock = $this->createMock(\Magento\Backend\App\Action\Context::class); -- $resultRedirectFactory = $this->createMock(\Magento\Backend\Model\View\Result\RedirectFactory::class); -+ $resultRedirectFactory = $this->createMock( -+ \Magento\Backend\Model\View\Result\RedirectFactory::class -+ ); - $this->responseMock = $this->createMock(\Magento\Framework\App\ResponseInterface::class); - $this->requestMock = $this->getMockBuilder(\Magento\Framework\App\Request\Http::class) - ->disableOriginalConstructor()->getMock(); -@@ -129,7 +135,8 @@ class MassAssignGroupTest extends \PHPUnit\Framework\TestCase - $this->customerCollectionFactoryMock->expects($this->once()) - ->method('create') - ->willReturn($this->customerCollectionMock); -- $this->customerRepositoryMock = $this->getMockBuilder(\Magento\Customer\Api\CustomerRepositoryInterface::class) -+ $this->customerRepositoryMock = $this -+ ->getMockBuilder(\Magento\Customer\Api\CustomerRepositoryInterface::class) - ->getMockForAbstractClass(); - $this->massAction = $objectManagerHelper->getObject( - \Magento\Customer\Controller\Adminhtml\Index\MassAssignGroup::class, -@@ -142,12 +149,18 @@ class MassAssignGroupTest extends \PHPUnit\Framework\TestCase - ); - } - -+ /** -+ * Unit test to verify mass customer group assignment use case -+ * -+ * @throws \Magento\Framework\Exception\LocalizedException -+ */ - public function testExecute() - { - $customersIds = [10, 11, 12]; -- $customerMock = $this->getMockBuilder( -- \Magento\Customer\Api\Data\CustomerInterface::class -- )->getMockForAbstractClass(); -+ $customerMock = $this->getMockBuilder(\Magento\Customer\Api\Data\CustomerInterface::class) -+ ->setMethods(['setData']) -+ ->disableOriginalConstructor() -+ ->getMockForAbstractClass(); - $this->customerCollectionMock->expects($this->any()) - ->method('getAllIds') - ->willReturn($customersIds); -@@ -168,6 +181,11 @@ class MassAssignGroupTest extends \PHPUnit\Framework\TestCase - $this->massAction->execute(); - } - -+ /** -+ * Unit test to verify expected error during mass customer group assignment use case -+ * -+ * @throws \Magento\Framework\Exception\LocalizedException -+ */ - public function testExecuteWithException() - { - $customersIds = [10, 11, 12]; -diff --git a/app/code/Magento/Customer/Test/Unit/Controller/Adminhtml/Index/SaveTest.php b/app/code/Magento/Customer/Test/Unit/Controller/Adminhtml/Index/SaveTest.php -index 5372bb11a89..57f384d32d9 100644 ---- a/app/code/Magento/Customer/Test/Unit/Controller/Adminhtml/Index/SaveTest.php -+++ b/app/code/Magento/Customer/Test/Unit/Controller/Adminhtml/Index/SaveTest.php -@@ -5,7 +5,6 @@ - */ - namespace Magento\Customer\Test\Unit\Controller\Adminhtml\Index; - --use Magento\Customer\Api\AddressMetadataInterface; - use Magento\Customer\Api\CustomerMetadataInterface; - use Magento\Customer\Api\Data\AttributeMetadataInterface; - use Magento\Customer\Api\Data\CustomerInterface; -@@ -15,6 +14,8 @@ use Magento\Customer\Model\Metadata\Form; - use Magento\Framework\Controller\Result\Redirect; - - /** -+ * Testing Save Customer use case from admin page -+ * - * @SuppressWarnings(PHPMD.TooManyFields) - * @SuppressWarnings(PHPMD.CouplingBetweenObjects) - * @covers \Magento\Customer\Controller\Adminhtml\Index\Save -@@ -281,7 +282,6 @@ class SaveTest extends \PHPUnit\Framework\TestCase - public function testExecuteWithExistentCustomer() - { - $customerId = 22; -- $addressId = 11; - $subscription = 'true'; - $postValue = [ - 'customer' => [ -@@ -290,18 +290,6 @@ class SaveTest extends \PHPUnit\Framework\TestCase - 'coolness' => false, - 'disable_auto_group_change' => 'false', - ], -- 'address' => [ -- '_template_' => '_template_', -- $addressId => [ -- 'entity_id' => $addressId, -- 'default_billing' => 'true', -- 'default_shipping' => 'true', -- 'code' => 'value', -- 'coolness' => false, -- 'region' => 'region', -- 'region_id' => 'region_id', -- ], -- ], - 'subscription' => $subscription, - ]; - $extractedData = [ -@@ -318,22 +306,6 @@ class SaveTest extends \PHPUnit\Framework\TestCase - CustomerInterface::DEFAULT_BILLING => 2, - CustomerInterface::DEFAULT_SHIPPING => 2 - ]; -- $addressExtractedData = [ -- 'entity_id' => $addressId, -- 'code' => 'value', -- 'coolness' => false, -- 'region' => 'region', -- 'region_id' => 'region_id', -- ]; -- $addressCompactedData = [ -- 'entity_id' => $addressId, -- 'default_billing' => 'true', -- 'default_shipping' => 'true', -- 'code' => 'value', -- 'coolness' => false, -- 'region' => 'region', -- 'region_id' => 'region_id', -- ]; - $savedData = [ - 'entity_id' => $customerId, - 'darkness' => true, -@@ -341,61 +313,40 @@ class SaveTest extends \PHPUnit\Framework\TestCase - CustomerInterface::DEFAULT_BILLING => false, - CustomerInterface::DEFAULT_SHIPPING => false, - ]; -- $savedAddressData = [ -- 'entity_id' => $addressId, -- 'default_billing' => true, -- 'default_shipping' => true, -- ]; - $mergedData = [ - 'entity_id' => $customerId, - 'darkness' => true, - 'name' => 'Name', - 'code' => 'value', - 'disable_auto_group_change' => 0, -- CustomerInterface::DEFAULT_BILLING => $addressId, -- CustomerInterface::DEFAULT_SHIPPING => $addressId, - 'confirmation' => false, - 'sendemail_store_id' => '1', - 'id' => $customerId, - ]; -- $mergedAddressData = [ -- 'entity_id' => $addressId, -- 'default_billing' => true, -- 'default_shipping' => true, -- 'code' => 'value', -- 'region' => [ -- 'region' => 'region', -- 'region_id' => 'region_id', -- ], -- 'region_id' => 'region_id', -- 'id' => $addressId, -- ]; - - /** @var AttributeMetadataInterface|\PHPUnit_Framework_MockObject_MockObject $customerFormMock */ - $attributeMock = $this->getMockBuilder( - \Magento\Customer\Api\Data\AttributeMetadataInterface::class - )->disableOriginalConstructor()->getMock(); -- $attributeMock->expects($this->exactly(2)) -+ $attributeMock->expects($this->atLeastOnce()) - ->method('getAttributeCode') - ->willReturn('coolness'); -- $attributeMock->expects($this->exactly(2)) -+ $attributeMock->expects($this->atLeastOnce()) - ->method('getFrontendInput') - ->willReturn('int'); - $attributes = [$attributeMock]; - -- $this->requestMock->expects($this->any()) -+ $this->requestMock->expects($this->atLeastOnce()) - ->method('getPostValue') - ->willReturnMap([ - [null, null, $postValue], - [CustomerMetadataInterface::ENTITY_TYPE_CUSTOMER, null, $postValue['customer']], -- ['address/' . $addressId, null, $postValue['address'][$addressId]], - ]); -- $this->requestMock->expects($this->exactly(3)) -+ $this->requestMock->expects($this->atLeastOnce()) - ->method('getPost') - ->willReturnMap( - [ - ['customer', null, $postValue['customer']], -- ['address', null, $postValue['address']], - ['subscription', null, $subscription], - ] - ); -@@ -404,16 +355,15 @@ class SaveTest extends \PHPUnit\Framework\TestCase - $objectMock = $this->getMockBuilder(\Magento\Framework\DataObject::class) - ->disableOriginalConstructor() - ->getMock(); -- $objectMock->expects($this->exactly(2)) -+ $objectMock->expects($this->atLeastOnce()) - ->method('getData') - ->willReturnMap( - [ - ['customer', null, $postValue['customer']], -- ['address/' . $addressId, null, $postValue['address'][$addressId]], - ] - ); - -- $this->objectFactoryMock->expects($this->exactly(2)) -+ $this->objectFactoryMock->expects($this->exactly(1)) - ->method('create') - ->with(['data' => $postValue]) - ->willReturn($objectMock); -@@ -432,23 +382,7 @@ class SaveTest extends \PHPUnit\Framework\TestCase - $customerFormMock->expects($this->once()) - ->method('getAttributes') - ->willReturn($attributes); -- -- $customerAddressFormMock = $this->getMockBuilder( -- \Magento\Customer\Model\Metadata\Form::class -- )->disableOriginalConstructor()->getMock(); -- $customerAddressFormMock->expects($this->once()) -- ->method('extractData') -- ->with($this->requestMock, 'address/' . $addressId) -- ->willReturn($addressExtractedData); -- $customerAddressFormMock->expects($this->once()) -- ->method('compactData') -- ->with($addressExtractedData) -- ->willReturn($addressCompactedData); -- $customerAddressFormMock->expects($this->once()) -- ->method('getAttributes') -- ->willReturn($attributes); -- -- $this->formFactoryMock->expects($this->exactly(2)) -+ $this->formFactoryMock->expects($this->exactly(1)) - ->method('create') - ->willReturnMap( - [ -@@ -461,15 +395,6 @@ class SaveTest extends \PHPUnit\Framework\TestCase - [], - $customerFormMock - ], -- [ -- AddressMetadataInterface::ENTITY_TYPE_ADDRESS, -- 'adminhtml_customer_address', -- $savedAddressData, -- false, -- Form::DONT_IGNORE_INVISIBLE, -- [], -- $customerAddressFormMock -- ], - ] - ); - -@@ -492,25 +417,7 @@ class SaveTest extends \PHPUnit\Framework\TestCase - ->with($customerMock) - ->willReturn($savedData); - -- $addressMock = $this->getMockBuilder(\Magento\Customer\Api\Data\AddressInterface::class) -- ->disableOriginalConstructor() -- ->getMock(); -- -- $this->customerAddressRepositoryMock->expects($this->once()) -- ->method('getById') -- ->with($addressId) -- ->willReturn($addressMock); -- -- $this->customerAddressMapperMock->expects($this->once()) -- ->method('toFlatArray') -- ->with($addressMock) -- ->willReturn($savedAddressData); -- -- $this->addressDataFactoryMock->expects($this->once()) -- ->method('create') -- ->willReturn($addressMock); -- -- $this->dataHelperMock->expects($this->exactly(2)) -+ $this->dataHelperMock->expects($this->atLeastOnce()) - ->method('populateWithArray') - ->willReturnMap( - [ -@@ -519,19 +426,9 @@ class SaveTest extends \PHPUnit\Framework\TestCase - $mergedData, \Magento\Customer\Api\Data\CustomerInterface::class, - $this->dataHelperMock - ], -- [ -- $addressMock, -- $mergedAddressData, \Magento\Customer\Api\Data\AddressInterface::class, -- $this->dataHelperMock -- ], - ] - ); - -- $customerMock->expects($this->once()) -- ->method('setAddresses') -- ->with([$addressMock]) -- ->willReturnSelf(); -- - $this->customerRepositoryMock->expects($this->once()) - ->method('save') - ->with($customerMock) -@@ -540,6 +437,10 @@ class SaveTest extends \PHPUnit\Framework\TestCase - $customerEmail = 'customer@email.com'; - $customerMock->expects($this->once())->method('getEmail')->willReturn($customerEmail); - -+ $customerMock->expects($this->once()) -+ ->method('getAddresses') -+ ->willReturn([]); -+ - $this->emailNotificationMock->expects($this->once()) - ->method('credentialsChanged') - ->with($customerMock, $customerEmail) -@@ -608,63 +509,33 @@ class SaveTest extends \PHPUnit\Framework\TestCase - public function testExecuteWithNewCustomer() - { - $customerId = 22; -- $addressId = 11; -+ - $subscription = '0'; - $postValue = [ - 'customer' => [ - 'coolness' => false, - 'disable_auto_group_change' => 'false', - ], -- 'address' => [ -- '_template_' => '_template_', -- $addressId => [ -- 'entity_id' => $addressId, -- 'code' => 'value', -- 'coolness' => false, -- 'region' => 'region', -- 'region_id' => 'region_id', -- ], -- ], - 'subscription' => $subscription, - ]; - $extractedData = [ - 'coolness' => false, - 'disable_auto_group_change' => 'false', - ]; -- $addressExtractedData = [ -- 'entity_id' => $addressId, -- 'code' => 'value', -- 'coolness' => false, -- 'region' => 'region', -- 'region_id' => 'region_id', -- ]; - $mergedData = [ - 'disable_auto_group_change' => 0, - CustomerInterface::DEFAULT_BILLING => null, - CustomerInterface::DEFAULT_SHIPPING => null, - 'confirmation' => false, - ]; -- $mergedAddressData = [ -- 'entity_id' => $addressId, -- 'default_billing' => false, -- 'default_shipping' => false, -- 'code' => 'value', -- 'region' => [ -- 'region' => 'region', -- 'region_id' => 'region_id', -- ], -- 'region_id' => 'region_id', -- 'id' => $addressId, -- ]; -- - /** @var AttributeMetadataInterface|\PHPUnit_Framework_MockObject_MockObject $customerFormMock */ - $attributeMock = $this->getMockBuilder( - \Magento\Customer\Api\Data\AttributeMetadataInterface::class - )->disableOriginalConstructor()->getMock(); -- $attributeMock->expects($this->exactly(2)) -+ $attributeMock->expects($this->atLeastOnce()) - ->method('getAttributeCode') - ->willReturn('coolness'); -- $attributeMock->expects($this->exactly(2)) -+ $attributeMock->expects($this->atLeastOnce()) - ->method('getFrontendInput') - ->willReturn('int'); - $attributes = [$attributeMock]; -@@ -674,14 +545,12 @@ class SaveTest extends \PHPUnit\Framework\TestCase - ->willReturnMap([ - [null, null, $postValue], - [CustomerMetadataInterface::ENTITY_TYPE_CUSTOMER, null, $postValue['customer']], -- ['address/' . $addressId, null, $postValue['address'][$addressId]], - ]); -- $this->requestMock->expects($this->exactly(3)) -+ $this->requestMock->expects($this->atLeastOnce()) - ->method('getPost') - ->willReturnMap( - [ - ['customer', null, $postValue['customer']], -- ['address', null, $postValue['address']], - ['subscription', null, $subscription], - ] - ); -@@ -690,16 +559,15 @@ class SaveTest extends \PHPUnit\Framework\TestCase - $objectMock = $this->getMockBuilder(\Magento\Framework\DataObject::class) - ->disableOriginalConstructor() - ->getMock(); -- $objectMock->expects($this->exactly(2)) -+ $objectMock->expects($this->atLeastOnce()) - ->method('getData') - ->willReturnMap( - [ - ['customer', null, $postValue['customer']], -- ['address/' . $addressId, null, $postValue['address'][$addressId]], - ] - ); - -- $this->objectFactoryMock->expects($this->exactly(2)) -+ $this->objectFactoryMock->expects($this->atLeastOnce()) - ->method('create') - ->with(['data' => $postValue]) - ->willReturn($objectMock); -@@ -719,22 +587,7 @@ class SaveTest extends \PHPUnit\Framework\TestCase - ->method('getAttributes') - ->willReturn($attributes); - -- $customerAddressFormMock = $this->getMockBuilder( -- \Magento\Customer\Model\Metadata\Form::class -- )->disableOriginalConstructor()->getMock(); -- $customerAddressFormMock->expects($this->once()) -- ->method('extractData') -- ->with($this->requestMock, 'address/' . $addressId) -- ->willReturn($addressExtractedData); -- $customerAddressFormMock->expects($this->once()) -- ->method('compactData') -- ->with($addressExtractedData) -- ->willReturn($addressExtractedData); -- $customerAddressFormMock->expects($this->once()) -- ->method('getAttributes') -- ->willReturn($attributes); -- -- $this->formFactoryMock->expects($this->exactly(2)) -+ $this->formFactoryMock->expects($this->exactly(1)) - ->method('create') - ->willReturnMap( - [ -@@ -747,15 +600,6 @@ class SaveTest extends \PHPUnit\Framework\TestCase - [], - $customerFormMock - ], -- [ -- AddressMetadataInterface::ENTITY_TYPE_ADDRESS, -- 'adminhtml_customer_address', -- [], -- false, -- Form::DONT_IGNORE_INVISIBLE, -- [], -- $customerAddressFormMock -- ], - ] - ); - -@@ -768,25 +612,7 @@ class SaveTest extends \PHPUnit\Framework\TestCase - ->method('create') - ->willReturn($customerMock); - -- $addressMock = $this->getMockBuilder(\Magento\Customer\Api\Data\AddressInterface::class) -- ->disableOriginalConstructor() -- ->getMock(); -- -- $this->addressDataFactoryMock->expects($this->once()) -- ->method('create') -- ->willReturn($addressMock); -- -- $this->customerAddressRepositoryMock->expects($this->once()) -- ->method('getById') -- ->with($addressId) -- ->willReturn($addressMock); -- -- $this->customerAddressMapperMock->expects($this->once()) -- ->method('toFlatArray') -- ->with($addressMock) -- ->willReturn([]); -- -- $this->dataHelperMock->expects($this->exactly(2)) -+ $this->dataHelperMock->expects($this->atLeastOnce()) - ->method('populateWithArray') - ->willReturnMap( - [ -@@ -795,11 +621,6 @@ class SaveTest extends \PHPUnit\Framework\TestCase - $mergedData, \Magento\Customer\Api\Data\CustomerInterface::class, - $this->dataHelperMock - ], -- [ -- $addressMock, -- $mergedAddressData, \Magento\Customer\Api\Data\AddressInterface::class, -- $this->dataHelperMock -- ], - ] - ); - -@@ -878,22 +699,24 @@ class SaveTest extends \PHPUnit\Framework\TestCase - 'customer' => [ - 'coolness' => false, - 'disable_auto_group_change' => 'false', -+ 'dob' => '3/12/1996', - ], - 'subscription' => $subscription, - ]; - $extractedData = [ - 'coolness' => false, - 'disable_auto_group_change' => 'false', -+ 'dob' => '1996-03-12', - ]; - - /** @var AttributeMetadataInterface|\PHPUnit_Framework_MockObject_MockObject $customerFormMock */ - $attributeMock = $this->getMockBuilder( - \Magento\Customer\Api\Data\AttributeMetadataInterface::class - )->disableOriginalConstructor()->getMock(); -- $attributeMock->expects($this->once()) -+ $attributeMock->expects($this->exactly(2)) - ->method('getAttributeCode') - ->willReturn('coolness'); -- $attributeMock->expects($this->once()) -+ $attributeMock->expects($this->exactly(2)) - ->method('getFrontendInput') - ->willReturn('int'); - $attributes = [$attributeMock]; -@@ -904,12 +727,11 @@ class SaveTest extends \PHPUnit\Framework\TestCase - [null, null, $postValue], - [CustomerMetadataInterface::ENTITY_TYPE_CUSTOMER, null, $postValue['customer']], - ]); -- $this->requestMock->expects($this->exactly(2)) -+ $this->requestMock->expects($this->atLeastOnce()) - ->method('getPost') - ->willReturnMap( - [ - ['customer', null, $postValue['customer']], -- ['address', null, null], - ] - ); - -@@ -917,12 +739,12 @@ class SaveTest extends \PHPUnit\Framework\TestCase - $objectMock = $this->getMockBuilder(\Magento\Framework\DataObject::class) - ->disableOriginalConstructor() - ->getMock(); -- $objectMock->expects($this->once()) -+ $objectMock->expects($this->exactly(2)) - ->method('getData') - ->with('customer') - ->willReturn($postValue['customer']); - -- $this->objectFactoryMock->expects($this->once()) -+ $this->objectFactoryMock->expects($this->exactly(2)) - ->method('create') - ->with(['data' => $postValue]) - ->willReturn($objectMock); -@@ -930,19 +752,19 @@ class SaveTest extends \PHPUnit\Framework\TestCase - $customerFormMock = $this->getMockBuilder( - \Magento\Customer\Model\Metadata\Form::class - )->disableOriginalConstructor()->getMock(); -- $customerFormMock->expects($this->once()) -+ $customerFormMock->expects($this->exactly(2)) - ->method('extractData') - ->with($this->requestMock, 'customer') - ->willReturn($extractedData); -- $customerFormMock->expects($this->once()) -+ $customerFormMock->expects($this->exactly(2)) - ->method('compactData') - ->with($extractedData) - ->willReturn($extractedData); -- $customerFormMock->expects($this->once()) -+ $customerFormMock->expects($this->exactly(2)) - ->method('getAttributes') - ->willReturn($attributes); - -- $this->formFactoryMock->expects($this->once()) -+ $this->formFactoryMock->expects($this->exactly(2)) - ->method('create') - ->with( - CustomerMetadataInterface::ENTITY_TYPE_CUSTOMER, -@@ -990,7 +812,10 @@ class SaveTest extends \PHPUnit\Framework\TestCase - - $this->sessionMock->expects($this->once()) - ->method('setCustomerFormData') -- ->with($postValue); -+ ->with([ -+ 'customer' => $extractedData, -+ 'subscription' => $subscription, -+ ]); - - /** @var Redirect|\PHPUnit_Framework_MockObject_MockObject $redirectMock */ - $redirectMock = $this->getMockBuilder(\Magento\Framework\Controller\Result\Redirect::class) -@@ -1021,22 +846,24 @@ class SaveTest extends \PHPUnit\Framework\TestCase - 'customer' => [ - 'coolness' => false, - 'disable_auto_group_change' => 'false', -+ 'dob' => '3/12/1996', - ], - 'subscription' => $subscription, - ]; - $extractedData = [ - 'coolness' => false, - 'disable_auto_group_change' => 'false', -+ 'dob' => '1996-03-12', - ]; - - /** @var AttributeMetadataInterface|\PHPUnit_Framework_MockObject_MockObject $customerFormMock */ - $attributeMock = $this->getMockBuilder( - \Magento\Customer\Api\Data\AttributeMetadataInterface::class - )->disableOriginalConstructor()->getMock(); -- $attributeMock->expects($this->once()) -+ $attributeMock->expects($this->exactly(2)) - ->method('getAttributeCode') - ->willReturn('coolness'); -- $attributeMock->expects($this->once()) -+ $attributeMock->expects($this->exactly(2)) - ->method('getFrontendInput') - ->willReturn('int'); - $attributes = [$attributeMock]; -@@ -1047,12 +874,11 @@ class SaveTest extends \PHPUnit\Framework\TestCase - [null, null, $postValue], - [CustomerMetadataInterface::ENTITY_TYPE_CUSTOMER, null, $postValue['customer']], - ]); -- $this->requestMock->expects($this->exactly(2)) -+ $this->requestMock->expects($this->atLeastOnce()) - ->method('getPost') - ->willReturnMap( - [ - ['customer', null, $postValue['customer']], -- ['address', null, null], - ] - ); - -@@ -1060,12 +886,12 @@ class SaveTest extends \PHPUnit\Framework\TestCase - $objectMock = $this->getMockBuilder(\Magento\Framework\DataObject::class) - ->disableOriginalConstructor() - ->getMock(); -- $objectMock->expects($this->once()) -+ $objectMock->expects($this->exactly(2)) - ->method('getData') - ->with('customer') - ->willReturn($postValue['customer']); - -- $this->objectFactoryMock->expects($this->once()) -+ $this->objectFactoryMock->expects($this->exactly(2)) - ->method('create') - ->with(['data' => $postValue]) - ->willReturn($objectMock); -@@ -1074,19 +900,19 @@ class SaveTest extends \PHPUnit\Framework\TestCase - $customerFormMock = $this->getMockBuilder( - \Magento\Customer\Model\Metadata\Form::class - )->disableOriginalConstructor()->getMock(); -- $customerFormMock->expects($this->once()) -+ $customerFormMock->expects($this->exactly(2)) - ->method('extractData') - ->with($this->requestMock, 'customer') - ->willReturn($extractedData); -- $customerFormMock->expects($this->once()) -+ $customerFormMock->expects($this->exactly(2)) - ->method('compactData') - ->with($extractedData) - ->willReturn($extractedData); -- $customerFormMock->expects($this->once()) -+ $customerFormMock->expects($this->exactly(2)) - ->method('getAttributes') - ->willReturn($attributes); - -- $this->formFactoryMock->expects($this->once()) -+ $this->formFactoryMock->expects($this->exactly(2)) - ->method('create') - ->with( - CustomerMetadataInterface::ENTITY_TYPE_CUSTOMER, -@@ -1133,7 +959,10 @@ class SaveTest extends \PHPUnit\Framework\TestCase - - $this->sessionMock->expects($this->once()) - ->method('setCustomerFormData') -- ->with($postValue); -+ ->with([ -+ 'customer' => $extractedData, -+ 'subscription' => $subscription, -+ ]); - - /** @var Redirect|\PHPUnit_Framework_MockObject_MockObject $redirectMock */ - $redirectMock = $this->getMockBuilder(\Magento\Framework\Controller\Result\Redirect::class) -@@ -1164,22 +993,24 @@ class SaveTest extends \PHPUnit\Framework\TestCase - 'customer' => [ - 'coolness' => false, - 'disable_auto_group_change' => 'false', -+ 'dob' => '3/12/1996', - ], - 'subscription' => $subscription, - ]; - $extractedData = [ - 'coolness' => false, - 'disable_auto_group_change' => 'false', -+ 'dob' => '1996-03-12', - ]; - - /** @var AttributeMetadataInterface|\PHPUnit_Framework_MockObject_MockObject $customerFormMock */ - $attributeMock = $this->getMockBuilder( - \Magento\Customer\Api\Data\AttributeMetadataInterface::class - )->disableOriginalConstructor()->getMock(); -- $attributeMock->expects($this->once()) -+ $attributeMock->expects($this->exactly(2)) - ->method('getAttributeCode') - ->willReturn('coolness'); -- $attributeMock->expects($this->once()) -+ $attributeMock->expects($this->exactly(2)) - ->method('getFrontendInput') - ->willReturn('int'); - $attributes = [$attributeMock]; -@@ -1190,12 +1021,11 @@ class SaveTest extends \PHPUnit\Framework\TestCase - [null, null, $postValue], - [CustomerMetadataInterface::ENTITY_TYPE_CUSTOMER, null, $postValue['customer']], - ]); -- $this->requestMock->expects($this->exactly(2)) -+ $this->requestMock->expects($this->atLeastOnce()) - ->method('getPost') - ->willReturnMap( - [ - ['customer', null, $postValue['customer']], -- ['address', null, null], - ] - ); - -@@ -1203,12 +1033,12 @@ class SaveTest extends \PHPUnit\Framework\TestCase - $objectMock = $this->getMockBuilder(\Magento\Framework\DataObject::class) - ->disableOriginalConstructor() - ->getMock(); -- $objectMock->expects($this->once()) -+ $objectMock->expects($this->exactly(2)) - ->method('getData') - ->with('customer') - ->willReturn($postValue['customer']); - -- $this->objectFactoryMock->expects($this->once()) -+ $this->objectFactoryMock->expects($this->exactly(2)) - ->method('create') - ->with(['data' => $postValue]) - ->willReturn($objectMock); -@@ -1216,19 +1046,19 @@ class SaveTest extends \PHPUnit\Framework\TestCase - $customerFormMock = $this->getMockBuilder( - \Magento\Customer\Model\Metadata\Form::class - )->disableOriginalConstructor()->getMock(); -- $customerFormMock->expects($this->once()) -+ $customerFormMock->expects($this->exactly(2)) - ->method('extractData') - ->with($this->requestMock, 'customer') - ->willReturn($extractedData); -- $customerFormMock->expects($this->once()) -+ $customerFormMock->expects($this->exactly(2)) - ->method('compactData') - ->with($extractedData) - ->willReturn($extractedData); -- $customerFormMock->expects($this->once()) -+ $customerFormMock->expects($this->exactly(2)) - ->method('getAttributes') - ->willReturn($attributes); - -- $this->formFactoryMock->expects($this->once()) -+ $this->formFactoryMock->expects($this->exactly(2)) - ->method('create') - ->with( - CustomerMetadataInterface::ENTITY_TYPE_CUSTOMER, -@@ -1277,7 +1107,10 @@ class SaveTest extends \PHPUnit\Framework\TestCase - - $this->sessionMock->expects($this->once()) - ->method('setCustomerFormData') -- ->with($postValue); -+ ->with([ -+ 'customer' => $extractedData, -+ 'subscription' => $subscription, -+ ]); - - /** @var Redirect|\PHPUnit_Framework_MockObject_MockObject $redirectMock */ - $redirectMock = $this->getMockBuilder(\Magento\Framework\Controller\Result\Redirect::class) -diff --git a/app/code/Magento/Customer/Test/Unit/Controller/Adminhtml/Index/ValidateTest.php b/app/code/Magento/Customer/Test/Unit/Controller/Adminhtml/Index/ValidateTest.php -index 7209ac9fd24..5adb9026016 100644 ---- a/app/code/Magento/Customer/Test/Unit/Controller/Adminhtml/Index/ValidateTest.php -+++ b/app/code/Magento/Customer/Test/Unit/Controller/Adminhtml/Index/ValidateTest.php -@@ -141,12 +141,6 @@ class ValidateTest extends \PHPUnit\Framework\TestCase - - public function testExecute() - { -- $this->request->expects($this->once()) -- ->method('getPost') -- ->willReturn([ -- '_template_' => null, -- 'address_index' => null -- ]); - $customerEntityId = 2; - $this->request->expects($this->once()) - ->method('getParam') -@@ -162,11 +156,6 @@ class ValidateTest extends \PHPUnit\Framework\TestCase - $this->form->expects($this->once())->method('setInvisibleIgnored'); - $this->form->expects($this->atLeastOnce())->method('extractData')->willReturn([]); - -- $error = $this->createMock(\Magento\Framework\Message\Error::class); -- $this->form->expects($this->once()) -- ->method('validateData') -- ->willReturn([$error]); -- - $validationResult = $this->getMockForAbstractClass( - \Magento\Customer\Api\Data\ValidationResultsInterface::class, - [], -@@ -188,9 +177,6 @@ class ValidateTest extends \PHPUnit\Framework\TestCase - - public function testExecuteWithoutAddresses() - { -- $this->request->expects($this->once()) -- ->method('getPost') -- ->willReturn(null); - $this->form->expects($this->once()) - ->method('setInvisibleIgnored'); - $this->form->expects($this->atLeastOnce()) -@@ -223,9 +209,6 @@ class ValidateTest extends \PHPUnit\Framework\TestCase - - public function testExecuteWithException() - { -- $this->request->expects($this->once()) -- ->method('getPost') -- ->willReturn(null); - $this->form->expects($this->once()) - ->method('setInvisibleIgnored'); - $this->form->expects($this->atLeastOnce()) -@@ -265,12 +248,6 @@ class ValidateTest extends \PHPUnit\Framework\TestCase - - public function testExecuteWithNewCustomerAndNoEntityId() - { -- $this->request->expects($this->once()) -- ->method('getPost') -- ->willReturn([ -- '_template_' => null, -- 'address_index' => null -- ]); - $this->request->expects($this->once()) - ->method('getParam') - ->with('customer') -@@ -282,11 +259,6 @@ class ValidateTest extends \PHPUnit\Framework\TestCase - $this->form->expects($this->once())->method('setInvisibleIgnored'); - $this->form->expects($this->atLeastOnce())->method('extractData')->willReturn([]); - -- $error = $this->createMock(\Magento\Framework\Message\Error::class); -- $this->form->expects($this->once()) -- ->method('validateData') -- ->willReturn([$error]); -- - $validationResult = $this->getMockForAbstractClass( - \Magento\Customer\Api\Data\ValidationResultsInterface::class, - [], -diff --git a/app/code/Magento/Customer/Test/Unit/Controller/Ajax/LoginTest.php b/app/code/Magento/Customer/Test/Unit/Controller/Ajax/LoginTest.php -index aaaa799a5e2..14ed09f7332 100644 ---- a/app/code/Magento/Customer/Test/Unit/Controller/Ajax/LoginTest.php -+++ b/app/code/Magento/Customer/Test/Unit/Controller/Ajax/LoginTest.php -@@ -3,13 +3,32 @@ - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -+declare(strict_types=1); - --/** -- * Test customer ajax login controller -- */ - namespace Magento\Customer\Test\Unit\Controller\Ajax; - -+use Magento\Customer\Api\Data\CustomerInterface; -+use Magento\Customer\Controller\Ajax\Login; -+use Magento\Customer\Model\Account\Redirect; -+use Magento\Customer\Model\AccountManagement; -+use Magento\Customer\Model\Session; -+use Magento\Framework\App\Action\Context; -+use Magento\Framework\App\Config\ScopeConfigInterface; -+use Magento\Framework\App\Request\Http; -+use Magento\Framework\App\Response\RedirectInterface; -+use Magento\Framework\App\ResponseInterface; -+use Magento\Framework\Controller\Result\Json; -+use Magento\Framework\Controller\Result\JsonFactory; -+use Magento\Framework\Controller\Result\Raw; -+use Magento\Framework\Controller\Result\RawFactory; - use Magento\Framework\Exception\InvalidEmailOrPasswordException; -+use Magento\Framework\Json\Helper\Data; -+use Magento\Framework\ObjectManager\ObjectManager as FakeObjectManager; -+use Magento\Framework\Stdlib\Cookie\CookieMetadata; -+use Magento\Framework\Stdlib\Cookie\CookieMetadataFactory; -+use Magento\Framework\Stdlib\CookieManagerInterface; -+use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; -+use PHPUnit_Framework_MockObject_MockObject as MockObject; - - /** - * @SuppressWarnings(PHPMD.CouplingBetweenObjects) -@@ -17,223 +36,190 @@ use Magento\Framework\Exception\InvalidEmailOrPasswordException; - class LoginTest extends \PHPUnit\Framework\TestCase - { - /** -- * @var \Magento\Customer\Controller\Ajax\Login -+ * @var Login - */ -- protected $object; -+ private $controller; - - /** -- * @var \Magento\Framework\App\Request\Http|\PHPUnit_Framework_MockObject_MockObject -+ * @var Http|MockObject - */ -- protected $request; -+ private $request; - - /** -- * @var \Magento\Framework\App\ResponseInterface|\PHPUnit_Framework_MockObject_MockObject -+ * @var ResponseInterface|MockObject - */ -- protected $response; -+ private $response; - - /** -- * @var \Magento\Customer\Model\Session|\PHPUnit_Framework_MockObject_MockObject -+ * @var Session|MockObject - */ -- protected $customerSession; -+ private $customerSession; - - /** -- * @var \Magento\Framework\ObjectManagerInterface|\PHPUnit_Framework_MockObject_MockObject -+ * @var FakeObjectManager|MockObject - */ -- protected $objectManager; -+ private $objectManager; - - /** -- * @var \Magento\Customer\Api\AccountManagementInterface|\PHPUnit_Framework_MockObject_MockObject -+ * @var AccountManagement|MockObject - */ -- protected $customerAccountManagementMock; -+ private $accountManagement; - - /** -- * @var \Magento\Framework\Json\Helper\Data|\PHPUnit_Framework_MockObject_MockObject -+ * @var Data|MockObject - */ -- protected $jsonHelperMock; -+ private $jsonHelper; - - /** -- * @var \Magento\Framework\Controller\Result\Json|\PHPUnit_Framework_MockObject_MockObject -+ * @var Json|MockObject - */ -- protected $resultJson; -+ private $resultJson; - - /** -- * @var \Magento\Framework\Controller\Result\JsonFactory| \PHPUnit_Framework_MockObject_MockObject -+ * @var JsonFactory|MockObject - */ -- protected $resultJsonFactory; -+ private $resultJsonFactory; - - /** -- * @var \Magento\Framework\Controller\Result\Raw| \PHPUnit_Framework_MockObject_MockObject -+ * @var Raw|MockObject - */ -- protected $resultRaw; -+ private $resultRaw; - - /** -- * @var \PHPUnit_Framework_MockObject_MockObject -+ * @var RedirectInterface|MockObject - */ -- protected $redirectMock; -+ private $redirect; - - /** -- * @var \Magento\Framework\Stdlib\CookieManagerInterface| \PHPUnit_Framework_MockObject_MockObject -+ * @var CookieManagerInterface|MockObject - */ - private $cookieManager; - - /** -- * @var \Magento\Framework\Stdlib\Cookie\CookieMetadataFactory| \PHPUnit_Framework_MockObject_MockObject -+ * @var CookieMetadataFactory|MockObject - */ - private $cookieMetadataFactory; - - /** -- * @var \Magento\Framework\Stdlib\Cookie\CookieMetadata| \PHPUnit_Framework_MockObject_MockObject -+ * @inheritdoc - */ -- private $cookieMetadata; -- -- protected function setUp() -+ protected function setUp(): void - { -- $this->request = $this->getMockBuilder(\Magento\Framework\App\Request\Http::class) -- ->disableOriginalConstructor()->getMock(); -+ $this->request = $this->getMockBuilder(Http::class) -+ ->disableOriginalConstructor() -+ ->getMock(); - $this->response = $this->createPartialMock( -- \Magento\Framework\App\ResponseInterface::class, -+ ResponseInterface::class, - ['setRedirect', 'sendResponse', 'representJson', 'setHttpResponseCode'] - ); - $this->customerSession = $this->createPartialMock( -- \Magento\Customer\Model\Session::class, -+ Session::class, - [ - 'isLoggedIn', - 'getLastCustomerId', - 'getBeforeAuthUrl', - 'setBeforeAuthUrl', - 'setCustomerDataAsLoggedIn', -- 'regenerateId' -+ 'regenerateId', -+ 'getData' - ] - ); -- $this->objectManager = $this->createPartialMock(\Magento\Framework\ObjectManager\ObjectManager::class, ['get']); -- $this->customerAccountManagementMock = -- $this->createPartialMock(\Magento\Customer\Model\AccountManagement::class, ['authenticate']); -+ $this->objectManager = $this->createPartialMock(FakeObjectManager::class, ['get']); -+ $this->accountManagement = $this->createPartialMock(AccountManagement::class, ['authenticate']); - -- $this->jsonHelperMock = $this->createPartialMock(\Magento\Framework\Json\Helper\Data::class, ['jsonDecode']); -+ $this->jsonHelper = $this->createPartialMock(Data::class, ['jsonDecode']); - -- $this->resultJson = $this->getMockBuilder(\Magento\Framework\Controller\Result\Json::class) -+ $this->resultJson = $this->getMockBuilder(Json::class) - ->disableOriginalConstructor() - ->getMock(); -- $this->resultJsonFactory = $this->getMockBuilder(\Magento\Framework\Controller\Result\JsonFactory::class) -+ $this->resultJsonFactory = $this->getMockBuilder(JsonFactory::class) - ->disableOriginalConstructor() - ->setMethods(['create']) - ->getMock(); - -- $this->cookieManager = $this->getMockBuilder(\Magento\Framework\Stdlib\CookieManagerInterface::class) -+ $this->cookieManager = $this->getMockBuilder(CookieManagerInterface::class) - ->setMethods(['getCookie', 'deleteCookie']) - ->getMockForAbstractClass(); -- $this->cookieMetadataFactory = $this->getMockBuilder( -- \Magento\Framework\Stdlib\Cookie\CookieMetadataFactory::class -- )->disableOriginalConstructor()->getMock(); -- $this->cookieMetadata = $this->getMockBuilder(\Magento\Framework\Stdlib\Cookie\CookieMetadata::class) -+ $this->cookieMetadataFactory = $this->getMockBuilder(CookieMetadataFactory::class) - ->disableOriginalConstructor() - ->getMock(); - -- $this->resultRaw = $this->getMockBuilder(\Magento\Framework\Controller\Result\Raw::class) -+ $this->resultRaw = $this->getMockBuilder(Raw::class) - ->disableOriginalConstructor() - ->getMock(); -- $resultRawFactory = $this->getMockBuilder(\Magento\Framework\Controller\Result\RawFactory::class) -+ $resultRawFactory = $this->getMockBuilder(RawFactory::class) - ->disableOriginalConstructor() - ->setMethods(['create']) - ->getMock(); -- $resultRawFactory->expects($this->atLeastOnce()) -- ->method('create') -+ $resultRawFactory->method('create') - ->willReturn($this->resultRaw); - -- $contextMock = $this->createMock(\Magento\Framework\App\Action\Context::class); -- $this->redirectMock = $this->createMock(\Magento\Framework\App\Response\RedirectInterface::class); -- $contextMock->expects($this->atLeastOnce())->method('getRedirect')->willReturn($this->redirectMock); -- $contextMock->expects($this->atLeastOnce())->method('getRequest')->willReturn($this->request); -- -- $objectManager = new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this); -- $this->object = $objectManager->getObject( -- \Magento\Customer\Controller\Ajax\Login::class, -+ /** @var Context|MockObject $context */ -+ $context = $this->createMock(Context::class); -+ $this->redirect = $this->createMock(RedirectInterface::class); -+ $context->method('getRedirect') -+ ->willReturn($this->redirect); -+ $context->method('getRequest') -+ ->willReturn($this->request); -+ -+ $objectManager = new ObjectManager($this); -+ $this->controller = $objectManager->getObject( -+ Login::class, - [ -- 'context' => $contextMock, -+ 'context' => $context, - 'customerSession' => $this->customerSession, -- 'helper' => $this->jsonHelperMock, -+ 'helper' => $this->jsonHelper, - 'response' => $this->response, - 'resultRawFactory' => $resultRawFactory, - 'resultJsonFactory' => $this->resultJsonFactory, - 'objectManager' => $this->objectManager, -- 'customerAccountManagement' => $this->customerAccountManagementMock, -+ 'customerAccountManagement' => $this->accountManagement, - 'cookieManager' => $this->cookieManager, - 'cookieMetadataFactory' => $this->cookieMetadataFactory - ] - ); - } - -- public function testLogin() -+ /** -+ * Checks successful login. -+ */ -+ public function testLogin(): void - { - $jsonRequest = '{"username":"customer@example.com", "password":"password"}'; - $loginSuccessResponse = '{"errors": false, "message":"Login successful."}'; -+ $this->withRequest($jsonRequest); - -- $this->request -- ->expects($this->any()) -- ->method('getContent') -- ->willReturn($jsonRequest); -- -- $this->request -- ->expects($this->any()) -- ->method('getMethod') -- ->willReturn('POST'); -- -- $this->request -- ->expects($this->any()) -- ->method('isXmlHttpRequest') -- ->willReturn(true); -- -- $this->resultJsonFactory->expects($this->atLeastOnce()) -- ->method('create') -+ $this->resultJsonFactory->method('create') - ->willReturn($this->resultJson); - -- $this->jsonHelperMock -- ->expects($this->any()) -- ->method('jsonDecode') -+ $this->jsonHelper->method('jsonDecode') - ->with($jsonRequest) - ->willReturn(['username' => 'customer@example.com', 'password' => 'password']); - -- $customerMock = $this->getMockForAbstractClass(\Magento\Customer\Api\Data\CustomerInterface::class); -- $this->customerAccountManagementMock -- ->expects($this->any()) -- ->method('authenticate') -+ /** @var CustomerInterface|MockObject $customer */ -+ $customer = $this->getMockForAbstractClass(CustomerInterface::class); -+ $this->accountManagement->method('authenticate') - ->with('customer@example.com', 'password') -- ->willReturn($customerMock); -+ ->willReturn($customer); - -- $this->customerSession->expects($this->once()) -- ->method('setCustomerDataAsLoggedIn') -- ->with($customerMock); -+ $this->customerSession->method('setCustomerDataAsLoggedIn') -+ ->with($customer); -+ $this->customerSession->method('regenerateId'); - -- $this->customerSession->expects($this->once())->method('regenerateId'); -+ /** @var Redirect|MockObject $redirect */ -+ $redirect = $this->createMock(Redirect::class); -+ $this->controller->setAccountRedirect($redirect); -+ $redirect->method('getRedirectCookie') -+ ->willReturn('some_url1'); - -- $redirectMock = $this->createMock(\Magento\Customer\Model\Account\Redirect::class); -- $this->object->setAccountRedirect($redirectMock); -- $redirectMock->expects($this->once())->method('getRedirectCookie')->willReturn('some_url1'); -+ $this->withCookieManager(); - -- $this->cookieManager->expects($this->once()) -- ->method('getCookie') -- ->with('mage-cache-sessid') -- ->willReturn(true); -- $this->cookieMetadataFactory->expects($this->once()) -- ->method('createCookieMetadata') -- ->willReturn($this->cookieMetadata); -- $this->cookieMetadata->expects($this->once()) -- ->method('setPath') -- ->with('/') -- ->willReturnSelf(); -- $this->cookieManager->expects($this->once()) -- ->method('deleteCookie') -- ->with('mage-cache-sessid', $this->cookieMetadata) -- ->willReturnSelf(); -+ $this->withScopeConfig(); - -- $scopeConfigMock = $this->createMock(\Magento\Framework\App\Config\ScopeConfigInterface::class); -- $this->object->setScopeConfig($scopeConfigMock); -- $scopeConfigMock->expects($this->once())->method('getValue') -- ->with('customer/startup/redirect_dashboard') -- ->willReturn(0); -- -- $this->redirectMock->expects($this->once())->method('success')->willReturn('some_url2'); -- $this->resultRaw->expects($this->never())->method('setHttpResponseCode'); -+ $this->redirect->method('success') -+ ->willReturn('some_url2'); -+ $this->resultRaw->expects(self::never()) -+ ->method('setHttpResponseCode'); - - $result = [ - 'errors' => false, -@@ -241,67 +227,99 @@ class LoginTest extends \PHPUnit\Framework\TestCase - 'redirectUrl' => 'some_url2', - ]; - -- $this->resultJson -- ->expects($this->once()) -- ->method('setData') -+ $this->resultJson->method('setData') - ->with($result) - ->willReturn($loginSuccessResponse); -- $this->assertEquals($loginSuccessResponse, $this->object->execute()); -+ self::assertEquals($loginSuccessResponse, $this->controller->execute()); - } - -- public function testLoginFailure() -+ /** -+ * Checks unsuccessful login. -+ */ -+ public function testLoginFailure(): void - { - $jsonRequest = '{"username":"invalid@example.com", "password":"invalid"}'; - $loginFailureResponse = '{"message":"Invalid login or password."}'; -+ $this->withRequest($jsonRequest); - -- $this->request -- ->expects($this->any()) -- ->method('getContent') -- ->willReturn($jsonRequest); -- -- $this->request -- ->expects($this->any()) -- ->method('getMethod') -- ->willReturn('POST'); -- -- $this->request -- ->expects($this->any()) -- ->method('isXmlHttpRequest') -- ->willReturn(true); -- -- $this->resultJsonFactory->expects($this->once()) -- ->method('create') -+ $this->resultJsonFactory->method('create') - ->willReturn($this->resultJson); - -- $this->jsonHelperMock -- ->expects($this->any()) -- ->method('jsonDecode') -+ $this->jsonHelper->method('jsonDecode') - ->with($jsonRequest) - ->willReturn(['username' => 'invalid@example.com', 'password' => 'invalid']); - -- $customerMock = $this->getMockForAbstractClass(\Magento\Customer\Api\Data\CustomerInterface::class); -- $this->customerAccountManagementMock -- ->expects($this->any()) -- ->method('authenticate') -+ /** @var CustomerInterface|MockObject $customer */ -+ $customer = $this->getMockForAbstractClass(CustomerInterface::class); -+ $this->accountManagement->method('authenticate') - ->with('invalid@example.com', 'invalid') - ->willThrowException(new InvalidEmailOrPasswordException(__('Invalid login or password.'))); - -- $this->customerSession->expects($this->never()) -+ $this->customerSession->expects(self::never()) - ->method('setCustomerDataAsLoggedIn') -- ->with($customerMock); -- -- $this->customerSession->expects($this->never())->method('regenerateId'); -+ ->with($customer); -+ $this->customerSession->expects(self::never()) -+ ->method('regenerateId'); - - $result = [ - 'errors' => true, - 'message' => __('Invalid login or password.') - ]; -- $this->resultJson -- ->expects($this->once()) -- ->method('setData') -+ $this->resultJson->method('setData') - ->with($result) - ->willReturn($loginFailureResponse); - -- $this->assertEquals($loginFailureResponse, $this->object->execute()); -+ self::assertEquals($loginFailureResponse, $this->controller->execute()); -+ } -+ -+ /** -+ * Emulates request behavior. -+ * -+ * @param string $jsonRequest -+ */ -+ private function withRequest(string $jsonRequest): void -+ { -+ $this->request->method('getContent') -+ ->willReturn($jsonRequest); -+ -+ $this->request->method('getMethod') -+ ->willReturn('POST'); -+ -+ $this->request->method('isXmlHttpRequest') -+ ->willReturn(true); -+ } -+ -+ /** -+ * Emulates cookie manager behavior. -+ */ -+ private function withCookieManager(): void -+ { -+ $this->cookieManager->method('getCookie') -+ ->with('mage-cache-sessid') -+ ->willReturn(true); -+ $cookieMetadata = $this->getMockBuilder(CookieMetadata::class) -+ ->disableOriginalConstructor() -+ ->getMock(); -+ $this->cookieMetadataFactory->method('createCookieMetadata') -+ ->willReturn($cookieMetadata); -+ $cookieMetadata->method('setPath') -+ ->with('/') -+ ->willReturnSelf(); -+ $this->cookieManager->method('deleteCookie') -+ ->with('mage-cache-sessid', $cookieMetadata) -+ ->willReturnSelf(); -+ } -+ -+ /** -+ * Emulates config behavior. -+ */ -+ private function withScopeConfig(): void -+ { -+ /** @var ScopeConfigInterface|MockObject $scopeConfig */ -+ $scopeConfig = $this->createMock(ScopeConfigInterface::class); -+ $this->controller->setScopeConfig($scopeConfig); -+ $scopeConfig->method('getValue') -+ ->with('customer/startup/redirect_dashboard') -+ ->willReturn(0); - } - } -diff --git a/app/code/Magento/Customer/Test/Unit/Controller/Section/LoadTest.php b/app/code/Magento/Customer/Test/Unit/Controller/Section/LoadTest.php -index f4bf184f9eb..5a7cf42be2c 100644 ---- a/app/code/Magento/Customer/Test/Unit/Controller/Section/LoadTest.php -+++ b/app/code/Magento/Customer/Test/Unit/Controller/Section/LoadTest.php -@@ -83,13 +83,13 @@ class LoadTest extends \PHPUnit\Framework\TestCase - } - - /** -- * @param $sectionNames -- * @param $updateSectionID -- * @param $sectionNamesAsArray -- * @param $updateIds -+ * @param string $sectionNames -+ * @param bool $forceNewSectionTimestamp -+ * @param string[] $sectionNamesAsArray -+ * @param bool $forceNewTimestamp - * @dataProvider executeDataProvider - */ -- public function testExecute($sectionNames, $updateSectionID, $sectionNamesAsArray, $updateIds) -+ public function testExecute($sectionNames, $forceNewSectionTimestamp, $sectionNamesAsArray, $forceNewTimestamp) - { - $this->resultJsonFactoryMock->expects($this->once()) - ->method('create') -@@ -103,12 +103,12 @@ class LoadTest extends \PHPUnit\Framework\TestCase - - $this->httpRequestMock->expects($this->exactly(2)) - ->method('getParam') -- ->withConsecutive(['sections'], ['update_section_id']) -- ->willReturnOnConsecutiveCalls($sectionNames, $updateSectionID); -+ ->withConsecutive(['sections'], ['force_new_section_timestamp']) -+ ->willReturnOnConsecutiveCalls($sectionNames, $forceNewSectionTimestamp); - - $this->sectionPoolMock->expects($this->once()) - ->method('getSectionsData') -- ->with($sectionNamesAsArray, $updateIds) -+ ->with($sectionNamesAsArray, $forceNewTimestamp) - ->willReturn([ - 'message' => 'some message', - 'someKey' => 'someValue' -@@ -133,15 +133,15 @@ class LoadTest extends \PHPUnit\Framework\TestCase - return [ - [ - 'sectionNames' => 'sectionName1,sectionName2,sectionName3', -- 'updateSectionID' => 'updateSectionID', -+ 'forceNewSectionTimestamp' => 'forceNewSectionTimestamp', - 'sectionNamesAsArray' => ['sectionName1', 'sectionName2', 'sectionName3'], -- 'updateIds' => true -+ 'forceNewTimestamp' => true - ], - [ - 'sectionNames' => null, -- 'updateSectionID' => null, -+ 'forceNewSectionTimestamp' => null, - 'sectionNamesAsArray' => null, -- 'updateIds' => false -+ 'forceNewTimestamp' => false - ], - ]; - } -diff --git a/app/code/Magento/Customer/Test/Unit/CustomerData/SectionPoolTest.php b/app/code/Magento/Customer/Test/Unit/CustomerData/SectionPoolTest.php -index 98fee70e335..2b67df1aee2 100644 ---- a/app/code/Magento/Customer/Test/Unit/CustomerData/SectionPoolTest.php -+++ b/app/code/Magento/Customer/Test/Unit/CustomerData/SectionPoolTest.php -@@ -63,7 +63,7 @@ class SectionPoolTest extends \PHPUnit\Framework\TestCase - - $this->identifierMock->expects($this->once()) - ->method('markSections') -- //check also default value for $updateIds = false -+ //check also default value for $forceTimestamp = false - ->with($allSectionsData, $sectionNames, false) - ->willReturn($identifierResult); - $modelResult = $this->model->getSectionsData($sectionNames); -diff --git a/app/code/Magento/Customer/Test/Unit/Helper/AddressTest.php b/app/code/Magento/Customer/Test/Unit/Helper/AddressTest.php -index 74af4ec57c7..0818d94afe5 100644 ---- a/app/code/Magento/Customer/Test/Unit/Helper/AddressTest.php -+++ b/app/code/Magento/Customer/Test/Unit/Helper/AddressTest.php -@@ -212,7 +212,7 @@ class AddressTest extends \PHPUnit\Framework\TestCase - public function testIsVatValidationEnabled($store, $result) - { - $this->scopeConfig->expects($this->once()) -- ->method('getValue') -+ ->method('isSetFlag') - ->with( - \Magento\Customer\Helper\Address::XML_PATH_VAT_VALIDATION_ENABLED, - \Magento\Store\Model\ScopeInterface::SCOPE_STORE, -@@ -242,7 +242,7 @@ class AddressTest extends \PHPUnit\Framework\TestCase - public function testHasValidateOnEachTransaction($store, $result) - { - $this->scopeConfig->expects($this->once()) -- ->method('getValue') -+ ->method('isSetFlag') - ->with( - \Magento\Customer\Helper\Address::XML_PATH_VIV_ON_EACH_TRANSACTION, - \Magento\Store\Model\ScopeInterface::SCOPE_STORE, -@@ -297,7 +297,7 @@ class AddressTest extends \PHPUnit\Framework\TestCase - public function testIsDisableAutoGroupAssignDefaultValue() - { - $this->scopeConfig->expects($this->once()) -- ->method('getValue') -+ ->method('isSetFlag') - ->with( - \Magento\Customer\Helper\Address::XML_PATH_VIV_DISABLE_AUTO_ASSIGN_DEFAULT, - \Magento\Store\Model\ScopeInterface::SCOPE_STORE -@@ -309,7 +309,7 @@ class AddressTest extends \PHPUnit\Framework\TestCase - public function testIsVatAttributeVisible() - { - $this->scopeConfig->expects($this->once()) -- ->method('getValue') -+ ->method('isSetFlag') - ->with( - \Magento\Customer\Helper\Address::XML_PATH_VAT_FRONTEND_VISIBILITY, - \Magento\Store\Model\ScopeInterface::SCOPE_STORE -@@ -414,4 +414,17 @@ class AddressTest extends \PHPUnit\Framework\TestCase - ['invalid_code', false] - ]; - } -+ -+ /** -+ * Data provider for test testIsAttributeRequire -+ * -+ * @return array -+ */ -+ public function isAttributeRequiredDataProvider() -+ { -+ return [ -+ ['fax', true], -+ ['invalid_code', false] -+ ]; -+ } - } -diff --git a/app/code/Magento/Customer/Test/Unit/Helper/Session/CurrentCustomerTest.php b/app/code/Magento/Customer/Test/Unit/Helper/Session/CurrentCustomerTest.php -index 364c3700cab..03158d05db8 100644 ---- a/app/code/Magento/Customer/Test/Unit/Helper/Session/CurrentCustomerTest.php -+++ b/app/code/Magento/Customer/Test/Unit/Helper/Session/CurrentCustomerTest.php -@@ -6,6 +6,9 @@ - - namespace Magento\Customer\Test\Unit\Helper\Session; - -+/** -+ * Current customer test. -+ */ - class CurrentCustomerTest extends \PHPUnit\Framework\TestCase - { - /** -@@ -44,7 +47,7 @@ class CurrentCustomerTest extends \PHPUnit\Framework\TestCase - protected $requestMock; - - /** -- * @var \Magento\Framework\Module\Manager|\PHPUnit_Framework_MockObject_MockObject -+ * @var \Magento\Framework\Module\ModuleManagerInterface|\PHPUnit_Framework_MockObject_MockObject - */ - protected $moduleManagerMock; - -@@ -77,7 +80,7 @@ class CurrentCustomerTest extends \PHPUnit\Framework\TestCase - $this->customerDataMock = $this->createMock(\Magento\Customer\Api\Data\CustomerInterface::class); - $this->customerRepositoryMock = $this->createMock(\Magento\Customer\Api\CustomerRepositoryInterface::class); - $this->requestMock = $this->createMock(\Magento\Framework\App\Request\Http::class); -- $this->moduleManagerMock = $this->createMock(\Magento\Framework\Module\Manager::class); -+ $this->moduleManagerMock = $this->createMock(\Magento\Framework\Module\ModuleManagerInterface::class); - $this->viewMock = $this->createMock(\Magento\Framework\App\View::class); - - $this->currentCustomer = new \Magento\Customer\Helper\Session\CurrentCustomer( -diff --git a/app/code/Magento/Customer/Test/Unit/Model/AccountConfirmationTest.php b/app/code/Magento/Customer/Test/Unit/Model/AccountConfirmationTest.php -index ae246665b28..1ce80d9d1e9 100644 ---- a/app/code/Magento/Customer/Test/Unit/Model/AccountConfirmationTest.php -+++ b/app/code/Magento/Customer/Test/Unit/Model/AccountConfirmationTest.php -@@ -59,7 +59,7 @@ class AccountConfirmationTest extends \PHPUnit\Framework\TestCase - $websiteId = 1; - - $this->scopeConfig->expects($this->any()) -- ->method('getValue') -+ ->method('isSetFlag') - ->with( - $this->accountConfirmation::XML_PATH_IS_CONFIRM, - ScopeInterface::SCOPE_WEBSITES, -diff --git a/app/code/Magento/Customer/Test/Unit/Model/AccountManagementTest.php b/app/code/Magento/Customer/Test/Unit/Model/AccountManagementTest.php -index 86e7683545f..5eda0c52c1d 100644 ---- a/app/code/Magento/Customer/Test/Unit/Model/AccountManagementTest.php -+++ b/app/code/Magento/Customer/Test/Unit/Model/AccountManagementTest.php -@@ -6,10 +6,14 @@ - - namespace Magento\Customer\Test\Unit\Model; - --use Magento\Customer\Model\AccountManagement; -+use Magento\Customer\Api\Data\CustomerInterface; - use Magento\Customer\Model\AccountConfirmation; -+use Magento\Customer\Model\AccountManagement; - use Magento\Customer\Model\AuthenticationInterface; -+use Magento\Customer\Model\Data\Customer; - use Magento\Customer\Model\EmailNotificationInterface; -+use Magento\Directory\Model\AllowedCountries; -+use Magento\Framework\Api\SearchCriteriaBuilder; - use Magento\Framework\App\Area; - use Magento\Framework\Exception\NoSuchEntityException; - use Magento\Framework\Intl\DateTimeFactory; -@@ -142,6 +146,21 @@ class AccountManagementTest extends \PHPUnit\Framework\TestCase - */ - private $saveHandler; - -+ /** -+ * @var \PHPUnit_Framework_MockObject_MockObject|\Magento\Customer\Model\AddressRegistry -+ */ -+ private $addressRegistryMock; -+ -+ /** -+ * @var \PHPUnit_Framework_MockObject_MockObject|SearchCriteriaBuilder -+ */ -+ private $searchCriteriaBuilderMock; -+ -+ /** -+ * @var AllowedCountries|\PHPUnit_Framework_MockObject_MockObject -+ */ -+ private $allowedCountriesReader; -+ - /** - * @SuppressWarnings(PHPMD.ExcessiveMethodLength) - */ -@@ -176,9 +195,11 @@ class AccountManagementTest extends \PHPUnit\Framework\TestCase - $this->dateTime = $this->createMock(\Magento\Framework\Stdlib\DateTime::class); - $this->customer = $this->createMock(\Magento\Customer\Model\Customer::class); - $this->objectFactory = $this->createMock(\Magento\Framework\DataObjectFactory::class); -+ $this->addressRegistryMock = $this->createMock(\Magento\Customer\Model\AddressRegistry::class); - $this->extensibleDataObjectConverter = $this->createMock( - \Magento\Framework\Api\ExtensibleDataObjectConverter::class - ); -+ $this->allowedCountriesReader = $this->createMock(AllowedCountries::class); - $this->authenticationMock = $this->getMockBuilder(AuthenticationInterface::class) - ->disableOriginalConstructor() - ->getMock(); -@@ -193,6 +214,7 @@ class AccountManagementTest extends \PHPUnit\Framework\TestCase - - $this->dateTimeFactory = $this->createMock(DateTimeFactory::class); - $this->accountConfirmation = $this->createMock(AccountConfirmation::class); -+ $this->searchCriteriaBuilderMock = $this->createMock(SearchCriteriaBuilder::class); - - $this->visitorCollectionFactory = $this->getMockBuilder( - \Magento\Customer\Model\ResourceModel\Visitor\CollectionFactory::class -@@ -239,6 +261,9 @@ class AccountManagementTest extends \PHPUnit\Framework\TestCase - 'sessionManager' => $this->sessionManager, - 'saveHandler' => $this->saveHandler, - 'visitorCollectionFactory' => $this->visitorCollectionFactory, -+ 'searchCriteriaBuilder' => $this->searchCriteriaBuilderMock, -+ 'addressRegistry' => $this->addressRegistryMock, -+ 'allowedCountriesReader' => $this->allowedCountriesReader, - ] - ); - $this->objectManagerHelper->setBackwardCompatibleProperty( -@@ -268,7 +293,7 @@ class AccountManagementTest extends \PHPUnit\Framework\TestCase - $website->expects($this->once()) - ->method('getStoreIds') - ->willReturn([1, 2, 3]); -- $customer = $this->getMockBuilder(\Magento\Customer\Api\Data\CustomerInterface::class)->getMock(); -+ $customer = $this->getMockBuilder(Customer::class)->disableOriginalConstructor()->getMock(); - $customer->expects($this->once()) - ->method('getId') - ->willReturn($customerId); -@@ -324,7 +349,7 @@ class AccountManagementTest extends \PHPUnit\Framework\TestCase - $website->expects($this->once()) - ->method('getDefaultStore') - ->willReturn($store); -- $customer = $this->getMockBuilder(\Magento\Customer\Api\Data\CustomerInterface::class)->getMock(); -+ $customer = $this->getMockBuilder(Customer::class)->disableOriginalConstructor()->getMock(); - $customer->expects($this->atLeastOnce()) - ->method('getId') - ->willReturn($customerId); -@@ -400,7 +425,7 @@ class AccountManagementTest extends \PHPUnit\Framework\TestCase - $website->expects($this->once()) - ->method('getDefaultStore') - ->willReturn($store); -- $customer = $this->getMockBuilder(\Magento\Customer\Api\Data\CustomerInterface::class)->getMock(); -+ $customer = $this->getMockBuilder(Customer::class)->disableOriginalConstructor()->getMock(); - $customer->expects($this->atLeastOnce()) - ->method('getId') - ->willReturn($customerId); -@@ -479,7 +504,7 @@ class AccountManagementTest extends \PHPUnit\Framework\TestCase - $website->expects($this->once()) - ->method('getDefaultStore') - ->willReturn($store); -- $customer = $this->getMockBuilder(\Magento\Customer\Api\Data\CustomerInterface::class)->getMock(); -+ $customer = $this->getMockBuilder(Customer::class)->disableOriginalConstructor()->getMock(); - $customer->expects($this->atLeastOnce()) - ->method('getId') - ->willReturn($customerId); -@@ -534,7 +559,14 @@ class AccountManagementTest extends \PHPUnit\Framework\TestCase - ->expects($this->once()) - ->method('delete') - ->with($customer); -- -+ $this->allowedCountriesReader -+ ->expects($this->atLeastOnce()) -+ ->method('getAllowedCountries') -+ ->willReturn(['US' => 'US']); -+ $address -+ ->expects($this->atLeastOnce()) -+ ->method('getCountryId') -+ ->willReturn('US'); - $this->accountManagement->createAccountWithPasswordHash($customer, $hash); - } - -@@ -548,8 +580,9 @@ class AccountManagementTest extends \PHPUnit\Framework\TestCase - $websiteId = 1; - $hash = '4nj54lkj5jfi03j49f8bgujfgsd'; - -- $customerMock = $this->getMockBuilder(\Magento\Customer\Api\Data\CustomerInterface::class) -- ->getMockForAbstractClass(); -+ $customerMock = $this->getMockBuilder(Customer::class) -+ ->disableOriginalConstructor() -+ ->getMock(); - - $customerMock->expects($this->atLeastOnce()) - ->method('getId') -@@ -640,7 +673,7 @@ class AccountManagementTest extends \PHPUnit\Framework\TestCase - $website->expects($this->once()) - ->method('getDefaultStore') - ->willReturn($store); -- $customer = $this->getMockBuilder(\Magento\Customer\Api\Data\CustomerInterface::class)->getMock(); -+ $customer = $this->getMockBuilder(Customer::class)->disableOriginalConstructor()->getMock(); - $customer->expects($this->atLeastOnce()) - ->method('getId') - ->willReturn($customerId); -@@ -707,6 +740,14 @@ class AccountManagementTest extends \PHPUnit\Framework\TestCase - $this->emailNotificationMock->expects($this->once()) - ->method('newAccount') - ->willReturnSelf(); -+ $this->allowedCountriesReader -+ ->expects($this->atLeastOnce()) -+ ->method('getAllowedCountries') -+ ->willReturn(['US' => 'US']); -+ $address -+ ->expects($this->atLeastOnce()) -+ ->method('getCountryId') -+ ->willReturn('US'); - - $this->accountManagement->createAccount($customer); - } -@@ -785,7 +826,7 @@ class AccountManagementTest extends \PHPUnit\Framework\TestCase - $minCharacterSetsNum . '. Classes of characters: Lower Case, Upper Case, Digits, Special Characters.'); - } - -- $customer = $this->getMockBuilder(\Magento\Customer\Api\Data\CustomerInterface::class)->getMock(); -+ $customer = $this->getMockBuilder(Customer::class)->disableOriginalConstructor()->getMock(); - $this->accountManagement->createAccount($customer, $password); - } - -@@ -806,7 +847,7 @@ class AccountManagementTest extends \PHPUnit\Framework\TestCase - $this->expectException(\Magento\Framework\Exception\InputException::class); - $this->expectExceptionMessage('Please enter a password with at most 256 characters.'); - -- $customer = $this->getMockBuilder(\Magento\Customer\Api\Data\CustomerInterface::class)->getMock(); -+ $customer = $this->getMockBuilder(Customer::class)->disableOriginalConstructor()->getMock(); - $this->accountManagement->createAccount($customer, $password); - } - -@@ -885,7 +926,7 @@ class AccountManagementTest extends \PHPUnit\Framework\TestCase - $website->expects($this->once()) - ->method('getDefaultStore') - ->willReturn($store); -- $customer = $this->getMockBuilder(\Magento\Customer\Api\Data\CustomerInterface::class)->getMock(); -+ $customer = $this->getMockBuilder(Customer::class)->disableOriginalConstructor()->getMock(); - $customer->expects($this->atLeastOnce()) - ->method('getId') - ->willReturn($customerId); -@@ -952,6 +993,14 @@ class AccountManagementTest extends \PHPUnit\Framework\TestCase - $this->emailNotificationMock->expects($this->once()) - ->method('newAccount') - ->willReturnSelf(); -+ $this->allowedCountriesReader -+ ->expects($this->atLeastOnce()) -+ ->method('getAllowedCountries') -+ ->willReturn(['US' => 'US']); -+ $address -+ ->expects($this->atLeastOnce()) -+ ->method('getCountryId') -+ ->willReturn('US'); - - $this->accountManagement->createAccount($customer, $password); - } -@@ -969,7 +1018,8 @@ class AccountManagementTest extends \PHPUnit\Framework\TestCase - $templateIdentifier = 'Template Identifier'; - $sender = 'Sender'; - -- $customer = $this->getMockBuilder(\Magento\Customer\Api\Data\CustomerInterface::class) -+ $customer = $this->getMockBuilder(Customer::class) -+ ->disableOriginalConstructor() - ->getMock(); - $customer->expects($this->any()) - ->method('getStoreId') -@@ -1001,7 +1051,7 @@ class AccountManagementTest extends \PHPUnit\Framework\TestCase - - $this->dataObjectProcessor->expects($this->once()) - ->method('buildOutputDataArray') -- ->with($customer, \Magento\Customer\Api\Data\CustomerInterface::class) -+ ->with($customer, CustomerInterface::class) - ->willReturn($customerData); - - $this->customerViewHelper->expects($this->any()) -@@ -1071,9 +1121,8 @@ class AccountManagementTest extends \PHPUnit\Framework\TestCase - protected function prepareInitiatePasswordReset($email, $templateIdentifier, $sender, $storeId, $customerId, $hash) - { - $websiteId = 1; -- -+ $addressId = 5; - $datetime = $this->prepareDateTimeFactory(); -- - $customerData = ['key' => 'value']; - $customerName = 'Customer Name'; - -@@ -1083,12 +1132,23 @@ class AccountManagementTest extends \PHPUnit\Framework\TestCase - $this->store->expects($this->any()) - ->method('getId') - ->willReturn($storeId); -- - $this->storeManager->expects($this->any()) - ->method('getStore') - ->willReturn($this->store); - -- $customer = $this->getMockBuilder(\Magento\Customer\Api\Data\CustomerInterface::class) -+ /** @var \Magento\Customer\Model\Address|\PHPUnit_Framework_MockObject_MockObject $addressModel */ -+ $addressModel = $this->getMockBuilder(\Magento\Customer\Model\Address::class)->disableOriginalConstructor() -+ ->setMethods(['setShouldIgnoreValidation'])->getMock(); -+ -+ /** @var \Magento\Customer\Api\Data\AddressInterface|\PHPUnit_Framework_MockObject_MockObject $customer */ -+ $address = $this->createMock(\Magento\Customer\Api\Data\AddressInterface::class); -+ $address->expects($this->once()) -+ ->method('getId') -+ ->willReturn($addressId); -+ -+ /** @var Customer|\PHPUnit_Framework_MockObject_MockObject $customer */ -+ $customer = $this->getMockBuilder(Customer::class) -+ ->disableOriginalConstructor() - ->getMock(); - $customer->expects($this->any()) - ->method('getEmail') -@@ -1099,7 +1159,19 @@ class AccountManagementTest extends \PHPUnit\Framework\TestCase - $customer->expects($this->any()) - ->method('getStoreId') - ->willReturn($storeId); -- -+ $customer->expects($this->any()) -+ ->method('getAddresses') -+ ->willReturn([$address]); -+ $this->customerRepository->expects($this->once()) -+ ->method('get') -+ ->willReturn($customer); -+ $this->addressRegistryMock->expects($this->once()) -+ ->method('retrieve') -+ ->with($addressId) -+ ->willReturn($addressModel); -+ $addressModel->expects($this->once()) -+ ->method('setShouldIgnoreValidation') -+ ->with(true); - $this->customerRepository->expects($this->once()) - ->method('get') - ->with($email, $websiteId) -@@ -1108,16 +1180,13 @@ class AccountManagementTest extends \PHPUnit\Framework\TestCase - ->method('save') - ->with($customer) - ->willReturnSelf(); -- - $this->random->expects($this->once()) - ->method('getUniqueHash') - ->willReturn($hash); -- - $this->customerViewHelper->expects($this->any()) - ->method('getCustomerName') - ->with($customer) - ->willReturn($customerName); -- - $this->customerSecure->expects($this->any()) - ->method('setRpToken') - ->with($hash) -@@ -1134,15 +1203,13 @@ class AccountManagementTest extends \PHPUnit\Framework\TestCase - ->method('setData') - ->with('name', $customerName) - ->willReturnSelf(); -- - $this->customerRegistry->expects($this->any()) - ->method('retrieveSecureData') - ->with($customerId) - ->willReturn($this->customerSecure); -- - $this->dataObjectProcessor->expects($this->any()) - ->method('buildOutputDataArray') -- ->with($customer, \Magento\Customer\Api\Data\CustomerInterface::class) -+ ->with($customer, Customer::class) - ->willReturn($customerData); - - $this->prepareEmailSend($email, $templateIdentifier, $sender, $storeId, $customerName); -@@ -1202,8 +1269,7 @@ class AccountManagementTest extends \PHPUnit\Framework\TestCase - - $storeId = 1; - -- mt_srand(mt_rand() + (100000000 * (float)microtime()) % PHP_INT_MAX); -- $hash = md5(uniqid(microtime() . mt_rand(0, mt_getrandmax()), true)); -+ $hash = hash('sha256', microtime() . random_int(PHP_INT_MIN, PHP_INT_MAX)); - - $this->emailNotificationMock->expects($this->once()) - ->method('passwordReminder') -@@ -1227,8 +1293,7 @@ class AccountManagementTest extends \PHPUnit\Framework\TestCase - $templateIdentifier = 'Template Identifier'; - $sender = 'Sender'; - -- mt_srand(mt_rand() + (100000000 * (float)microtime()) % PHP_INT_MAX); -- $hash = md5(uniqid(microtime() . mt_rand(0, mt_getrandmax()), true)); -+ $hash = hash('sha256', microtime() . random_int(PHP_INT_MIN, PHP_INT_MAX)); - - $this->emailNotificationMock->expects($this->once()) - ->method('passwordResetConfirmation') -@@ -1252,8 +1317,7 @@ class AccountManagementTest extends \PHPUnit\Framework\TestCase - $templateIdentifier = 'Template Identifier'; - $sender = 'Sender'; - -- mt_srand(mt_rand() + (100000000 * (float)microtime()) % PHP_INT_MAX); -- $hash = md5(uniqid(microtime() . mt_rand(0, mt_getrandmax()), true)); -+ $hash = hash('sha256', microtime() . random_int(PHP_INT_MIN, PHP_INT_MAX)); - - $this->prepareInitiatePasswordReset($email, $templateIdentifier, $sender, $storeId, $customerId, $hash); - -@@ -1266,11 +1330,11 @@ class AccountManagementTest extends \PHPUnit\Framework\TestCase - - /** - * @expectedException \Magento\Framework\Exception\InputException -- * @expectedExceptionMessage Invalid value of "" provided for the customerId field -+ * @expectedExceptionMessage Invalid value of "0" provided for the customerId field - */ - public function testValidateResetPasswordTokenBadCustomerId() - { -- $this->accountManagement->validateResetPasswordLinkToken(null, ''); -+ $this->accountManagement->validateResetPasswordLinkToken(0, ''); - } - - /** -@@ -1361,7 +1425,6 @@ class AccountManagementTest extends \PHPUnit\Framework\TestCase - $this->prepareDateTimeFactory(); - $this->sessionManager = $this->getMockBuilder(\Magento\Framework\Session\SessionManagerInterface::class) - ->disableOriginalConstructor() -- ->setMethods(['destroy', 'start', 'writeClose']) - ->getMockForAbstractClass(); - $this->visitorCollectionFactory = $this->getMockBuilder( - \Magento\Customer\Model\ResourceModel\Visitor\CollectionFactory::class -@@ -1414,6 +1477,7 @@ class AccountManagementTest extends \PHPUnit\Framework\TestCase - 'encryptor' => $this->encryptor, - 'dataProcessor' => $this->dataObjectProcessor, - 'storeManager' => $this->storeManager, -+ 'addressRegistry' => $this->addressRegistryMock, - 'transportBuilder' => $this->transportBuilder, - ] - ); -@@ -1436,11 +1500,15 @@ class AccountManagementTest extends \PHPUnit\Framework\TestCase - $passwordHash = '1a2b3f4c'; - - $this->reInitModel(); -- $customer = $this->getMockBuilder(\Magento\Customer\Api\Data\CustomerInterface::class) -+ $customer = $this->getMockBuilder(CustomerInterface::class) -+ ->disableOriginalConstructor() - ->getMock(); - $customer->expects($this->any()) - ->method('getId') - ->willReturn($customerId); -+ $customer->expects($this->once()) -+ ->method('getAddresses') -+ ->willReturn([]); - - $this->customerRepository - ->expects($this->once()) -@@ -1494,8 +1562,6 @@ class AccountManagementTest extends \PHPUnit\Framework\TestCase - ->method('save') - ->with($customer); - -- $this->sessionManager->expects($this->atLeastOnce())->method('start'); -- $this->sessionManager->expects($this->atLeastOnce())->method('writeClose'); - $this->sessionManager->expects($this->atLeastOnce())->method('getSessionId'); - - $visitor = $this->getMockBuilder(\Magento\Customer\Model\Visitor::class) -@@ -1528,12 +1594,34 @@ class AccountManagementTest extends \PHPUnit\Framework\TestCase - { - $customerEmail = 'customer@example.com'; - $customerId = '1'; -+ $addressId = 5; - $resetToken = 'newStringToken'; - $newPassword = 'new_password'; - - $this->reInitModel(); -- $customer = $this->getMockBuilder(\Magento\Customer\Api\Data\CustomerInterface::class)->getMock(); -+ /** @var \Magento\Customer\Model\Address|\PHPUnit_Framework_MockObject_MockObject $addressModel */ -+ $addressModel = $this->getMockBuilder(\Magento\Customer\Model\Address::class)->disableOriginalConstructor() -+ ->setMethods(['setShouldIgnoreValidation'])->getMock(); -+ -+ /** @var \Magento\Customer\Api\Data\AddressInterface|\PHPUnit_Framework_MockObject_MockObject $customer */ -+ $address = $this->createMock(\Magento\Customer\Api\Data\AddressInterface::class); -+ $address->expects($this->any()) -+ ->method('getId') -+ ->willReturn($addressId); -+ -+ /** @var Customer|\PHPUnit_Framework_MockObject_MockObject $customer */ -+ $customer = $this->getMockBuilder(Customer::class)->disableOriginalConstructor()->getMock(); - $customer->expects($this->any())->method('getId')->willReturn($customerId); -+ $customer->expects($this->any()) -+ ->method('getAddresses') -+ ->willReturn([$address]); -+ $this->addressRegistryMock->expects($this->once()) -+ ->method('retrieve') -+ ->with($addressId) -+ ->willReturn($addressModel); -+ $addressModel->expects($this->once()) -+ ->method('setShouldIgnoreValidation') -+ ->with(true); - $this->customerRepository->expects($this->atLeastOnce())->method('get')->with($customerEmail) - ->willReturn($customer); - $this->customer->expects($this->atLeastOnce())->method('getResetPasswordLinkExpirationPeriod') -@@ -1550,9 +1638,7 @@ class AccountManagementTest extends \PHPUnit\Framework\TestCase - $this->customerSecure->expects($this->once())->method('setRpTokenCreatedAt')->with(null); - $this->customerSecure->expects($this->any())->method('setPasswordHash')->willReturn(null); - -- $this->sessionManager->expects($this->atLeastOnce())->method('destroy'); -- $this->sessionManager->expects($this->atLeastOnce())->method('start'); -- $this->sessionManager->expects($this->atLeastOnce())->method('writeClose'); -+ $this->sessionManager->method('isSessionExists')->willReturn(false); - $this->sessionManager->expects($this->atLeastOnce())->method('getSessionId'); - $visitor = $this->getMockBuilder(\Magento\Customer\Model\Visitor::class) - ->disableOriginalConstructor() -@@ -1609,7 +1695,8 @@ class AccountManagementTest extends \PHPUnit\Framework\TestCase - $password = '1234567'; - $passwordHash = '1a2b3f4c'; - -- $customerData = $this->getMockBuilder(\Magento\Customer\Api\Data\CustomerInterface::class) -+ $customerData = $this->getMockBuilder(Customer::class) -+ ->disableOriginalConstructor() - ->getMock(); - - $customerModel = $this->getMockBuilder(\Magento\Customer\Model\Customer::class) -@@ -1674,7 +1761,7 @@ class AccountManagementTest extends \PHPUnit\Framework\TestCase - $customerId = 1; - $customerEmail = 'test1@example.com'; - -- $customerMock = $this->getMockBuilder(\Magento\Customer\Api\Data\CustomerInterface::class) -+ $customerMock = $this->getMockBuilder(Customer::class) - ->disableOriginalConstructor() - ->getMock(); - $customerMock->expects($this->once()) -@@ -1744,8 +1831,9 @@ class AccountManagementTest extends \PHPUnit\Framework\TestCase - ->method('getStore') - ->willReturn($storeMock); - -- $customerMock = $this->getMockBuilder(\Magento\Customer\Api\Data\CustomerInterface::class) -- ->getMockForAbstractClass(); -+ $customerMock = $this->getMockBuilder(Customer::class) -+ ->disableOriginalConstructor() -+ ->getMock(); - $customerMock->expects($this->exactly(2)) - ->method('getId') - ->willReturn(null); -@@ -1828,7 +1916,7 @@ class AccountManagementTest extends \PHPUnit\Framework\TestCase - ->method("setId") - ->with(null); - //Handle Customer calls -- $customer = $this->getMockBuilder(\Magento\Customer\Api\Data\CustomerInterface::class)->getMock(); -+ $customer = $this->getMockBuilder(Customer::class)->disableOriginalConstructor()->getMock(); - $customer - ->expects($this->atLeastOnce()) - ->method('getWebsiteId') -@@ -1894,6 +1982,14 @@ class AccountManagementTest extends \PHPUnit\Framework\TestCase - ->method('getWebsite') - ->with($websiteId) - ->willReturn($website); -+ $this->allowedCountriesReader -+ ->expects($this->atLeastOnce()) -+ ->method('getAllowedCountries') -+ ->willReturn(['US' => 'US']); -+ $existingAddress -+ ->expects($this->atLeastOnce()) -+ ->method('getCountryId') -+ ->willReturn('US'); - - $this->assertSame($customer, $this->accountManagement->createAccountWithPasswordHash($customer, $hash)); - } -@@ -1954,7 +2050,7 @@ class AccountManagementTest extends \PHPUnit\Framework\TestCase - $website->expects($this->once()) - ->method('getDefaultStore') - ->willReturn($store); -- $customer = $this->createMock(\Magento\Customer\Api\Data\CustomerInterface::class); -+ $customer = $this->createMock(Customer::class); - $customer->expects($this->atLeastOnce()) - ->method('getId') - ->willReturn($customerId); -@@ -2021,7 +2117,9 @@ class AccountManagementTest extends \PHPUnit\Framework\TestCase - ->method('newAccount') - ->willThrowException($exception); - $this->logger->expects($this->once())->method('error')->with($exception); -- -+ $this->allowedCountriesReader->expects($this->atLeastOnce()) -+ ->method('getAllowedCountries')->willReturn(['US' => 'US']); -+ $address->expects($this->atLeastOnce())->method('getCountryId')->willReturn('US'); - $this->accountManagement->createAccount($customer); - } - -@@ -2033,8 +2131,9 @@ class AccountManagementTest extends \PHPUnit\Framework\TestCase - $storeId = 1; - $websiteId = 1; - $hash = '4nj54lkj5jfi03j49f8bgujfgsd'; -- $customerMock = $this->getMockBuilder(\Magento\Customer\Api\Data\CustomerInterface::class) -- ->getMockForAbstractClass(); -+ $customerMock = $this->getMockBuilder(Customer::class) -+ ->disableOriginalConstructor() -+ ->getMock(); - $customerMock->expects($this->atLeastOnce()) - ->method('getId') - ->willReturn(null); -diff --git a/app/code/Magento/Customer/Test/Unit/Model/Address/DataProviderTest.php b/app/code/Magento/Customer/Test/Unit/Model/Address/DataProviderTest.php -new file mode 100644 -index 00000000000..4dafd305d61 ---- /dev/null -+++ b/app/code/Magento/Customer/Test/Unit/Model/Address/DataProviderTest.php -@@ -0,0 +1,351 @@ -+<?php -+declare(strict_types=1); -+/** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+ -+namespace Magento\Customer\Test\Unit\Model\Address; -+ -+use Magento\Customer\Api\CustomerRepositoryInterface; -+use Magento\Customer\Model\Address\DataProvider; -+use Magento\Customer\Model\AttributeMetadataResolver; -+use Magento\Customer\Model\FileUploaderDataResolver; -+use Magento\Customer\Model\ResourceModel\Address\CollectionFactory; -+use Magento\Customer\Model\ResourceModel\Address\Collection as AddressCollection; -+use Magento\Eav\Model\Config; -+use Magento\Eav\Model\Entity\Type; -+use Magento\Customer\Api\Data\CustomerInterface; -+use Magento\Framework\View\Element\UiComponent\ContextInterface; -+use Magento\Customer\Model\Address as AddressModel; -+use Magento\Ui\Component\Form\Element\Multiline; -+use Magento\Ui\Component\Form\Field; -+use Magento\Eav\Model\Entity\Attribute\AbstractAttribute; -+ -+/** -+ * @SuppressWarnings(PHPMD.CouplingBetweenObjects) -+ */ -+class DataProviderTest extends \PHPUnit\Framework\TestCase -+{ -+ private const ATTRIBUTE_CODE = 'street'; -+ -+ /** -+ * @var CollectionFactory|\PHPUnit_Framework_MockObject_MockObject -+ */ -+ private $addressCollectionFactory; -+ -+ /** -+ * @var AddressCollection|\PHPUnit_Framework_MockObject_MockObject -+ */ -+ private $collection; -+ -+ /** -+ * @var CustomerRepositoryInterface|\PHPUnit_Framework_MockObject_MockObject -+ */ -+ private $customerRepository; -+ -+ /** -+ * @var CustomerInterface|\PHPUnit_Framework_MockObject_MockObject -+ */ -+ private $customer; -+ -+ /** -+ * @var Config|\PHPUnit_Framework_MockObject_MockObject -+ */ -+ private $eavConfig; -+ -+ /* -+ * @var ContextInterface|\PHPUnit_Framework_MockObject_MockObject -+ */ -+ private $context; -+ -+ /** -+ * @var AddressModel|\PHPUnit_Framework_MockObject_MockObject -+ */ -+ private $address; -+ -+ /** -+ * @var FileUploaderDataResolver|\PHPUnit_Framework_MockObject_MockObject -+ */ -+ private $fileUploaderDataResolver; -+ -+ /** -+ * @var AttributeMetadataResolver|\PHPUnit_Framework_MockObject_MockObject -+ */ -+ private $attributeMetadataResolver; -+ -+ /** -+ * @var DataProvider -+ */ -+ private $model; -+ -+ protected function setUp() -+ { -+ $objectManagerHelper = new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this); -+ $this->fileUploaderDataResolver = $this->getMockBuilder(FileUploaderDataResolver::class) -+ ->disableOriginalConstructor() -+ ->getMock(); -+ $this->attributeMetadataResolver = $this->getMockBuilder(AttributeMetadataResolver::class) -+ ->disableOriginalConstructor() -+ ->getMock(); -+ $this->addressCollectionFactory = $this->getMockBuilder(CollectionFactory::class) -+ ->disableOriginalConstructor() -+ ->setMethods(['create']) -+ ->getMock(); -+ $this->collection = $this->getMockBuilder(AddressCollection::class) -+ ->disableOriginalConstructor() -+ ->getMock(); -+ $this->customerRepository = $this->getMockForAbstractClass(CustomerRepositoryInterface::class); -+ $this->context = $this->getMockForAbstractClass(ContextInterface::class); -+ $this->addressCollectionFactory->expects($this->once()) -+ ->method('create') -+ ->willReturn($this->collection); -+ $this->eavConfig = $this->getMockBuilder(Config::class) -+ ->disableOriginalConstructor() -+ ->getMock(); -+ $this->eavConfig->expects($this->once()) -+ ->method('getEntityType') -+ ->with('customer_address') -+ ->willReturn($this->getTypeAddressMock([])); -+ $this->customer = $this->getMockForAbstractClass(CustomerInterface::class); -+ $this->address = $this->getMockBuilder(AddressModel::class) -+ ->disableOriginalConstructor() -+ ->getMock(); -+ $this->attributeMetadataResolver->expects($this->at(0)) -+ ->method('getAttributesMeta') -+ ->willReturn( -+ [ -+ 'arguments' => [ -+ 'data' => [ -+ 'config' => [ -+ 'dataType' => Multiline::NAME, -+ 'formElement' => 'frontend_input', -+ 'options' => 'test-options', -+ 'visible' => null, -+ 'required' => 'is_required', -+ 'label' => __('Street'), -+ 'sortOrder' => 'sort_order', -+ 'default' => 'default_value', -+ 'size' => 'multiline_count', -+ 'componentType' => Field::NAME, -+ ], -+ ], -+ ], -+ ] -+ ); -+ $this->attributeMetadataResolver->expects($this->at(1)) -+ ->method('getAttributesMeta') -+ ->willReturn( -+ [ -+ 'arguments' => [ -+ 'data' => [ -+ 'config' => [ -+ 'dataType' => 'frontend_input', -+ 'formElement' => 'frontend_input', -+ 'visible' => null, -+ 'required' => 'is_required', -+ 'label' => __('frontend_label'), -+ 'sortOrder' => 'sort_order', -+ 'default' => 'default_value', -+ 'size' => 'multiline_count', -+ 'componentType' => Field::NAME, -+ 'prefer' => 'toggle', -+ 'valueMap' => [ -+ 'true' => 1, -+ 'false' => 0, -+ ], -+ ], -+ ], -+ ], -+ ] -+ ); -+ $this->model = $objectManagerHelper->getObject( -+ DataProvider::class, -+ [ -+ 'name' => 'test-name', -+ 'primaryFieldName' => 'primary-field-name', -+ 'requestFieldName' => 'request-field-name', -+ 'addressCollectionFactory' => $this->addressCollectionFactory, -+ 'customerRepository' => $this->customerRepository, -+ 'eavConfig' => $this->eavConfig, -+ 'context' => $this->context, -+ 'fileUploaderDataResolver' => $this->fileUploaderDataResolver, -+ 'attributeMetadataResolver' => $this->attributeMetadataResolver, -+ [], -+ [], -+ true -+ ] -+ ); -+ } -+ -+ public function testGetDefaultData(): void -+ { -+ $expectedData = [ -+ '' => [ -+ 'parent_id' => 1, -+ 'firstname' => 'John', -+ 'lastname' => 'Doe' -+ ] -+ ]; -+ -+ $this->collection->expects($this->once()) -+ ->method('getItems') -+ ->willReturn([]); -+ -+ $this->context->expects($this->once()) -+ ->method('getRequestParam') -+ ->willReturn(1); -+ $this->customerRepository->expects($this->once()) -+ ->method('getById') -+ ->willReturn($this->customer); -+ $this->customer->expects($this->once()) -+ ->method('getFirstname') -+ ->willReturn('John'); -+ $this->customer->expects($this->once()) -+ ->method('getLastname') -+ ->willReturn('Doe'); -+ -+ $this->assertEquals($expectedData, $this->model->getData()); -+ } -+ -+ public function testGetData(): void -+ { -+ $expectedData = [ -+ '1' => [ -+ 'parent_id' => '1', -+ 'default_billing' => '1', -+ 'default_shipping' => '1', -+ 'firstname' => 'John', -+ 'lastname' => 'Doe', -+ 'street' => [ -+ '42000 Ave W 55 Cedar City', -+ 'Apt. 33' -+ ] -+ ] -+ ]; -+ -+ $this->collection->expects($this->once()) -+ ->method('getItems') -+ ->willReturn([ -+ $this->address -+ ]); -+ -+ $this->customerRepository->expects($this->once()) -+ ->method('getById') -+ ->willReturn($this->customer); -+ $this->customer->expects($this->once()) -+ ->method('getDefaultBilling') -+ ->willReturn('1'); -+ $this->customer->expects($this->once()) -+ ->method('getDefaultShipping') -+ ->willReturn('1'); -+ -+ $this->address->expects($this->once()) -+ ->method('getEntityId') -+ ->willReturn('1'); -+ $this->address->expects($this->once()) -+ ->method('load') -+ ->with('1') -+ ->willReturnSelf(); -+ $this->address->expects($this->once()) -+ ->method('getData') -+ ->willReturn([ -+ 'parent_id' => '1', -+ 'firstname' => 'John', -+ 'lastname' => 'Doe', -+ 'street' => "42000 Ave W 55 Cedar City\nApt. 33" -+ ]); -+ $this->fileUploaderDataResolver->expects($this->once()) -+ ->method('overrideFileUploaderData') -+ ->willReturnSelf(); -+ -+ $this->assertEquals($expectedData, $this->model->getData()); -+ } -+ -+ /** -+ * Get customer address type mock -+ * -+ * @param array $customerAttributes -+ * @return Type|\PHPUnit_Framework_MockObject_MockObject -+ */ -+ protected function getTypeAddressMock($customerAttributes = []) -+ { -+ $typeAddressMock = $this->getMockBuilder(Type::class) -+ ->disableOriginalConstructor() -+ ->getMock(); -+ $attributesCollection = !empty($customerAttributes) ? $customerAttributes : $this->getAttributeMock(); -+ foreach ($attributesCollection as $attribute) { -+ $attribute->expects($this->any()) -+ ->method('getEntityType') -+ ->willReturn($typeAddressMock); -+ } -+ -+ $typeAddressMock->expects($this->once()) -+ ->method('getAttributeCollection') -+ ->willReturn($attributesCollection); -+ -+ return $typeAddressMock; -+ } -+ -+ /** -+ * Get attribute mock -+ * -+ * @param array $options -+ * @return AbstractAttribute[]|\PHPUnit_Framework_MockObject_MockObject[] -+ */ -+ protected function getAttributeMock($options = []): array -+ { -+ $attributeMock = $this->getMockBuilder(AbstractAttribute::class) -+ ->setMethods( -+ [ -+ 'getAttributeCode', -+ 'getDataUsingMethod', -+ 'getFrontendInput', -+ 'getIsVisible', -+ 'getSource', -+ 'getIsUserDefined', -+ 'getUsedInForms', -+ 'getEntityType', -+ ] -+ ) -+ ->disableOriginalConstructor() -+ ->getMockForAbstractClass(); -+ -+ $attributeCode = self::ATTRIBUTE_CODE; -+ if (isset($options[self::ATTRIBUTE_CODE]['specific_code_prefix'])) { -+ $attributeCode .= $options[self::ATTRIBUTE_CODE]['specific_code_prefix']; -+ } -+ -+ $attributeMock->expects($this->exactly(2)) -+ ->method('getAttributeCode') -+ ->willReturn($attributeCode); -+ -+ $attributeBooleanMock = $this->getMockBuilder(AbstractAttribute::class) -+ ->setMethods( -+ [ -+ 'getAttributeCode', -+ 'getDataUsingMethod', -+ 'getFrontendInput', -+ 'getIsVisible', -+ 'getIsUserDefined', -+ 'getUsedInForms', -+ 'getSource', -+ 'getEntityType', -+ ] -+ ) -+ ->disableOriginalConstructor() -+ ->getMockForAbstractClass(); -+ -+ $booleanAttributeCode = 'test-code-boolean'; -+ if (isset($options['test-code-boolean']['specific_code_prefix'])) { -+ $booleanAttributeCode .= $options['test-code-boolean']['specific_code_prefix']; -+ } -+ -+ $attributeBooleanMock->expects($this->exactly(2)) -+ ->method('getAttributeCode') -+ ->willReturn($booleanAttributeCode); -+ -+ $mocks = [$attributeMock, $attributeBooleanMock]; -+ return $mocks; -+ } -+} -diff --git a/app/code/Magento/Customer/Test/Unit/Model/Address/Validator/CountryTest.php b/app/code/Magento/Customer/Test/Unit/Model/Address/Validator/CountryTest.php -index d8148543a55..1e5d44e22ad 100644 ---- a/app/code/Magento/Customer/Test/Unit/Model/Address/Validator/CountryTest.php -+++ b/app/code/Magento/Customer/Test/Unit/Model/Address/Validator/CountryTest.php -@@ -27,11 +27,6 @@ class CountryTest extends \PHPUnit\Framework\TestCase - */ - private $allowedCountriesReaderMock; - -- /** -- * @var \Magento\Customer\Model\Config\Share|\PHPUnit_Framework_MockObject_MockObject -- */ -- private $shareConfigMock; -- - protected function setUp() - { - $this->directoryDataMock = $this->createMock(\Magento\Directory\Helper\Data::class); -@@ -40,16 +35,17 @@ class CountryTest extends \PHPUnit\Framework\TestCase - \Magento\Directory\Model\AllowedCountries::class, - ['getAllowedCountries'] - ); -- $this->shareConfigMock = $this->createPartialMock( -- \Magento\Customer\Model\Config\Share::class, -- ['isGlobalScope'] -+ -+ $escaper = $this->objectManager->getObject( -+ \Magento\Framework\Escaper::class - ); -+ - $this->model = $this->objectManager->getObject( - \Magento\Customer\Model\Address\Validator\Country::class, - [ - 'directoryData' => $this->directoryDataMock, - 'allowedCountriesReader' => $this->allowedCountriesReaderMock, -- 'shareConfig' => $this->shareConfigMock, -+ 'escaper' => $escaper - ] - ); - } -@@ -81,10 +77,9 @@ class CountryTest extends \PHPUnit\Framework\TestCase - ->method('isRegionRequired') - ->willReturn($data['regionRequired']); - -- $this->shareConfigMock->method('isGlobalScope')->willReturn(false); - $this->allowedCountriesReaderMock - ->method('getAllowedCountries') -- ->with(ScopeInterface::SCOPE_WEBSITE, null) -+ ->with(ScopeInterface::SCOPE_STORE, null) - ->willReturn($countryIds); - - $addressMock->method('getCountryId')->willReturn($data['country_id']); -diff --git a/app/code/Magento/Customer/Test/Unit/Model/Customer/DataProviderTest.php b/app/code/Magento/Customer/Test/Unit/Model/Customer/DataProviderTest.php -index 50c21379054..3cbcbdf80b2 100644 ---- a/app/code/Magento/Customer/Test/Unit/Model/Customer/DataProviderTest.php -+++ b/app/code/Magento/Customer/Test/Unit/Model/Customer/DataProviderTest.php -@@ -49,14 +49,14 @@ class DataProviderTest extends \PHPUnit\Framework\TestCase - protected $sessionMock; - - /** -- * @var \Magento\Customer\Model\FileProcessorFactory|\PHPUnit_Framework_MockObject_MockObject -+ * @var \Magento\Customer\Model\FileProcessor|\PHPUnit_Framework_MockObject_MockObject - */ -- protected $fileProcessorFactory; -+ protected $fileProcessor; - - /** -- * @var \Magento\Customer\Model\FileProcessor|\PHPUnit_Framework_MockObject_MockObject -+ * @var \Magento\Customer\Model\FileUploaderDataResolver|\PHPUnit_Framework_MockObject_MockObject - */ -- protected $fileProcessor; -+ private $fileUploaderDataResolver; - - /** - * Set up -@@ -84,10 +84,9 @@ class DataProviderTest extends \PHPUnit\Framework\TestCase - $this->fileProcessor = $this->getMockBuilder(\Magento\Customer\Model\FileProcessor::class) - ->disableOriginalConstructor() - ->getMock(); -- -- $this->fileProcessorFactory = $this->getMockBuilder(\Magento\Customer\Model\FileProcessorFactory::class) -+ $this->fileUploaderDataResolver = $this->getMockBuilder(\Magento\Customer\Model\FileUploaderDataResolver::class) - ->disableOriginalConstructor() -- ->setMethods(['create']) -+ ->setMethods(['overrideFileUploaderMetadata', 'overrideFileUploaderData']) - ->getMock(); - } - -@@ -111,16 +110,11 @@ class DataProviderTest extends \PHPUnit\Framework\TestCase - 'requestFieldName' => 'request-field-name', - 'eavValidationRules' => $this->eavValidationRulesMock, - 'customerCollectionFactory' => $this->getCustomerCollectionFactoryMock(), -- 'eavConfig' => $this->getEavConfigMock() -+ 'eavConfig' => $this->getEavConfigMock(), -+ 'fileUploaderDataResolver' => $this->fileUploaderDataResolver - ] - ); - -- $helper->setBackwardCompatibleProperty( -- $dataProvider, -- 'fileProcessorFactory', -- $this->fileProcessorFactory -- ); -- - $meta = $dataProvider->getMeta(); - $this->assertNotEmpty($meta); - $this->assertEquals($expected, $meta); -@@ -591,10 +585,6 @@ class DataProviderTest extends \PHPUnit\Framework\TestCase - $customer->expects($this->once()) - ->method('getAddresses') - ->willReturn([$address]); -- $customer->expects($this->once()) -- ->method('getAttributes') -- ->willReturn([]); -- - $address->expects($this->atLeastOnce()) - ->method('getId') - ->willReturn(2); -@@ -605,9 +595,6 @@ class DataProviderTest extends \PHPUnit\Framework\TestCase - $address->expects($this->once()) - ->method('getData') - ->willReturn($addressData); -- $address->expects($this->once()) -- ->method('getAttributes') -- ->willReturn([]); - - $helper = new ObjectManager($this); - $dataProvider = $helper->getObject( -@@ -618,7 +605,8 @@ class DataProviderTest extends \PHPUnit\Framework\TestCase - 'requestFieldName' => 'request-field-name', - 'eavValidationRules' => $this->eavValidationRulesMock, - 'customerCollectionFactory' => $this->customerCollectionFactoryMock, -- 'eavConfig' => $this->getEavConfigMock() -+ 'eavConfig' => $this->getEavConfigMock(), -+ 'fileUploaderDataResolver' => $this->fileUploaderDataResolver - ] - ); - -@@ -631,12 +619,6 @@ class DataProviderTest extends \PHPUnit\Framework\TestCase - ->method('getCustomerFormData') - ->willReturn(null); - -- $helper->setBackwardCompatibleProperty( -- $dataProvider, -- 'fileProcessorFactory', -- $this->fileProcessorFactory -- ); -- - $this->assertEquals( - [ - '' => [ -@@ -649,10 +631,8 @@ class DataProviderTest extends \PHPUnit\Framework\TestCase - 2 => [ - 'firstname' => 'firstname', - 'lastname' => 'lastname', -- 'street' => [ -- 'street', -- 'street', -- ], -+ // Won't be an array because it isn't defined as a multiline field in this test -+ 'street' => "street\nstreet", - 'default_billing' => 2, - 'default_shipping' => 2, - ] -@@ -725,10 +705,6 @@ class DataProviderTest extends \PHPUnit\Framework\TestCase - $customer->expects($this->once()) - ->method('getAddresses') - ->willReturn([$address]); -- $customer->expects($this->once()) -- ->method('getAttributes') -- ->willReturn([]); -- - $address->expects($this->atLeastOnce()) - ->method('getId') - ->willReturn(2); -@@ -743,10 +719,6 @@ class DataProviderTest extends \PHPUnit\Framework\TestCase - 'lastname' => 'lastname', - 'street' => "street\nstreet", - ]); -- $address->expects($this->once()) -- ->method('getAttributes') -- ->willReturn([]); -- - $helper = new ObjectManager($this); - $dataProvider = $helper->getObject( - \Magento\Customer\Model\Customer\DataProvider::class, -@@ -756,7 +728,8 @@ class DataProviderTest extends \PHPUnit\Framework\TestCase - 'requestFieldName' => 'request-field-name', - 'eavValidationRules' => $this->eavValidationRulesMock, - 'customerCollectionFactory' => $this->customerCollectionFactoryMock, -- 'eavConfig' => $this->getEavConfigMock() -+ 'eavConfig' => $this->getEavConfigMock(), -+ 'fileUploaderDataResolver' => $this->fileUploaderDataResolver - ] - ); - -@@ -771,12 +744,6 @@ class DataProviderTest extends \PHPUnit\Framework\TestCase - $this->sessionMock->expects($this->once()) - ->method('unsCustomerFormData'); - -- $helper->setBackwardCompatibleProperty( -- $dataProvider, -- 'fileProcessorFactory', -- $this->fileProcessorFactory -- ); -- - $this->assertEquals([$customerId => $customerFormData], $dataProvider->getData()); - } - -@@ -790,42 +757,6 @@ class DataProviderTest extends \PHPUnit\Framework\TestCase - $customerEmail = 'user1@example.com'; - - $filename = '/filename.ext1'; -- $viewUrl = 'viewUrl'; -- $mime = 'image/png'; -- -- $expectedData = [ -- $customerId => [ -- 'customer' => [ -- 'email' => $customerEmail, -- 'img1' => [ -- [ -- 'file' => $filename, -- 'size' => 1, -- 'url' => $viewUrl, -- 'name' => 'filename.ext1', -- 'type' => $mime, -- ], -- ], -- ], -- ], -- ]; -- -- $attributeMock = $this->getMockBuilder(\Magento\Customer\Model\Attribute::class) -- ->disableOriginalConstructor() -- ->getMock(); -- $attributeMock->expects($this->exactly(2)) -- ->method('getFrontendInput') -- ->willReturn('image'); -- $attributeMock->expects($this->exactly(2)) -- ->method('getAttributeCode') -- ->willReturn('img1'); -- -- $entityTypeMock = $this->getMockBuilder(\Magento\Eav\Model\Entity\Type::class) -- ->disableOriginalConstructor() -- ->getMock(); -- $entityTypeMock->expects($this->once()) -- ->method('getEntityTypeCode') -- ->willReturn(CustomerMetadataInterface::ENTITY_TYPE_CUSTOMER); - - $customerMock = $this->getMockBuilder(\Magento\Customer\Model\Customer::class) - ->disableOriginalConstructor() -@@ -842,13 +773,6 @@ class DataProviderTest extends \PHPUnit\Framework\TestCase - $customerMock->expects($this->once()) - ->method('getId') - ->willReturn($customerId); -- $customerMock->expects($this->once()) -- ->method('getAttributes') -- ->willReturn([$attributeMock]); -- $customerMock->expects($this->once()) -- ->method('getEntityType') -- ->willReturn($entityTypeMock); -- - $collectionMock = $this->getMockBuilder(\Magento\Customer\Model\ResourceModel\Customer\Collection::class) - ->disableOriginalConstructor() - ->getMock(); -@@ -864,30 +788,6 @@ class DataProviderTest extends \PHPUnit\Framework\TestCase - ->method('getCustomerFormData') - ->willReturn([]); - -- $this->fileProcessorFactory->expects($this->any()) -- ->method('create') -- ->with([ -- 'entityTypeCode' => CustomerMetadataInterface::ENTITY_TYPE_CUSTOMER, -- ]) -- ->willReturn($this->fileProcessor); -- -- $this->fileProcessor->expects($this->once()) -- ->method('isExist') -- ->with($filename) -- ->willReturn(true); -- $this->fileProcessor->expects($this->once()) -- ->method('getStat') -- ->with($filename) -- ->willReturn(['size' => 1]); -- $this->fileProcessor->expects($this->once()) -- ->method('getViewUrl') -- ->with('/filename.ext1', 'image') -- ->willReturn($viewUrl); -- $this->fileProcessor->expects($this->once()) -- ->method('getMimeType') -- ->with($filename) -- ->willReturn($mime); -- - $objectManager = new ObjectManager($this); - $dataProvider = $objectManager->getObject( - \Magento\Customer\Model\Customer\DataProvider::class, -@@ -897,7 +797,8 @@ class DataProviderTest extends \PHPUnit\Framework\TestCase - 'requestFieldName' => 'request-field-name', - 'eavValidationRules' => $this->eavValidationRulesMock, - 'customerCollectionFactory' => $this->customerCollectionFactoryMock, -- 'eavConfig' => $this->getEavConfigMock() -+ 'eavConfig' => $this->getEavConfigMock(), -+ 'fileUploaderDataResolver' => $this->fileUploaderDataResolver - ] - ); - -@@ -907,108 +808,15 @@ class DataProviderTest extends \PHPUnit\Framework\TestCase - $this->sessionMock - ); - -- $objectManager->setBackwardCompatibleProperty( -- $dataProvider, -- 'fileProcessorFactory', -- $this->fileProcessorFactory -- ); -- -- $this->assertEquals($expectedData, $dataProvider->getData()); -- } -- -- public function testGetDataWithCustomAttributeImageNoData() -- { -- $customerId = 1; -- $customerEmail = 'user1@example.com'; -- -- $expectedData = [ -- $customerId => [ -- 'customer' => [ -+ $this->fileUploaderDataResolver->expects($this->atLeastOnce())->method('overrideFileUploaderData') -+ ->with( -+ $customerMock, -+ [ - 'email' => $customerEmail, -- 'img1' => [], -- ], -- ], -- ]; -- -- $attributeMock = $this->getMockBuilder(\Magento\Customer\Model\Attribute::class) -- ->disableOriginalConstructor() -- ->getMock(); -- $attributeMock->expects($this->once()) -- ->method('getFrontendInput') -- ->willReturn('image'); -- $attributeMock->expects($this->exactly(2)) -- ->method('getAttributeCode') -- ->willReturn('img1'); -- -- $entityTypeMock = $this->getMockBuilder(\Magento\Eav\Model\Entity\Type::class) -- ->disableOriginalConstructor() -- ->getMock(); -- $entityTypeMock->expects($this->once()) -- ->method('getEntityTypeCode') -- ->willReturn(CustomerMetadataInterface::ENTITY_TYPE_CUSTOMER); -- -- $customerMock = $this->getMockBuilder(\Magento\Customer\Model\Customer::class) -- ->disableOriginalConstructor() -- ->getMock(); -- $customerMock->expects($this->once()) -- ->method('getData') -- ->willReturn([ -- 'email' => $customerEmail, -- ]); -- $customerMock->expects($this->once()) -- ->method('getAddresses') -- ->willReturn([]); -- $customerMock->expects($this->once()) -- ->method('getId') -- ->willReturn($customerId); -- $customerMock->expects($this->once()) -- ->method('getAttributes') -- ->willReturn([$attributeMock]); -- $customerMock->expects($this->once()) -- ->method('getEntityType') -- ->willReturn($entityTypeMock); -- -- $collectionMock = $this->getMockBuilder(\Magento\Customer\Model\ResourceModel\Customer\Collection::class) -- ->disableOriginalConstructor() -- ->getMock(); -- $collectionMock->expects($this->once()) -- ->method('getItems') -- ->willReturn([$customerMock]); -- -- $this->customerCollectionFactoryMock->expects($this->once()) -- ->method('create') -- ->willReturn($collectionMock); -- -- $this->sessionMock->expects($this->once()) -- ->method('getCustomerFormData') -- ->willReturn([]); -- -- $objectManager = new ObjectManager($this); -- $dataProvider = $objectManager->getObject( -- \Magento\Customer\Model\Customer\DataProvider::class, -- [ -- 'name' => 'test-name', -- 'primaryFieldName' => 'primary-field-name', -- 'requestFieldName' => 'request-field-name', -- 'eavValidationRules' => $this->eavValidationRulesMock, -- 'customerCollectionFactory' => $this->customerCollectionFactoryMock, -- 'eavConfig' => $this->getEavConfigMock() -- ] -- ); -- -- $objectManager->setBackwardCompatibleProperty( -- $dataProvider, -- 'session', -- $this->sessionMock -- ); -- -- $objectManager->setBackwardCompatibleProperty( -- $dataProvider, -- 'fileProcessorFactory', -- $this->fileProcessorFactory -- ); -- -- $this->assertEquals($expectedData, $dataProvider->getData()); -+ 'img1' => $filename, -+ ] -+ ); -+ $dataProvider->getData(); - } - - /** -@@ -1099,13 +907,6 @@ class DataProviderTest extends \PHPUnit\Framework\TestCase - 'file_extensions' => 'ext1, eXt2 ', // Added spaces and upper-cases - ]); - -- $this->fileProcessorFactory->expects($this->any()) -- ->method('create') -- ->with([ -- 'entityTypeCode' => CustomerMetadataInterface::ENTITY_TYPE_CUSTOMER, -- ]) -- ->willReturn($this->fileProcessor); -- - $objectManager = new ObjectManager($this); - $dataProvider = $objectManager->getObject( - \Magento\Customer\Model\Customer\DataProvider::class, -@@ -1115,8 +916,7 @@ class DataProviderTest extends \PHPUnit\Framework\TestCase - 'requestFieldName' => 'request-field-name', - 'eavValidationRules' => $this->eavValidationRulesMock, - 'customerCollectionFactory' => $this->customerCollectionFactoryMock, -- 'eavConfig' => $this->eavConfigMock, -- 'fileProcessorFactory' => $this->fileProcessorFactory, -+ 'eavConfig' => $this->eavConfigMock - ] - ); - -@@ -1211,16 +1011,11 @@ class DataProviderTest extends \PHPUnit\Framework\TestCase - 'requestFieldName' => 'request-field-name', - 'eavValidationRules' => $this->eavValidationRulesMock, - 'customerCollectionFactory' => $this->getCustomerCollectionFactoryMock(), -- 'eavConfig' => $this->getEavConfigMock(array_merge($firstAttributesBundle, $secondAttributesBundle)) -+ 'eavConfig' => $this->getEavConfigMock(array_merge($firstAttributesBundle, $secondAttributesBundle)), -+ 'fileUploaderDataResolver' => $this->fileUploaderDataResolver - ] - ); - -- $helper->setBackwardCompatibleProperty( -- $dataProvider, -- 'fileProcessorFactory', -- $this->fileProcessorFactory -- ); -- - $meta = $dataProvider->getMeta(); - $this->assertNotEmpty($meta); - $this->assertEquals($this->getExpectationForVisibleAttributes(), $meta); -@@ -1284,27 +1079,23 @@ class DataProviderTest extends \PHPUnit\Framework\TestCase - 'eavValidationRules' => $this->eavValidationRulesMock, - 'customerCollectionFactory' => $this->getCustomerCollectionFactoryMock(), - 'context' => $context, -- 'eavConfig' => $this->getEavConfigMock(array_merge($firstAttributesBundle, $secondAttributesBundle)) -+ 'eavConfig' => $this->getEavConfigMock(array_merge($firstAttributesBundle, $secondAttributesBundle)), -+ 'fileUploaderDataResolver' => $this->fileUploaderDataResolver -+ - ] - ); -- $helper->setBackwardCompatibleProperty( -- $dataProvider, -- 'fileProcessorFactory', -- $this->fileProcessorFactory -- ); - - $meta = $dataProvider->getMeta(); - $this->assertNotEmpty($meta); -- $this->assertEquals($this->getExpectationForVisibleAttributes(false), $meta); -+ $this->assertEquals($this->getExpectationForVisibleAttributes(), $meta); - } - - /** - * Retrieve all customer variations of attributes with all variations of visibility - * -- * @param bool $isRegistration - * @return array - */ -- private function getCustomerAttributeExpectations($isRegistration) -+ private function getCustomerAttributeExpectations() - { - return [ - self::ATTRIBUTE_CODE . "_1" => [ -@@ -1314,7 +1105,7 @@ class DataProviderTest extends \PHPUnit\Framework\TestCase - 'dataType' => 'frontend_input', - 'formElement' => 'frontend_input', - 'options' => 'test-options', -- 'visible' => !$isRegistration, -+ 'visible' => true, - 'required' => 'is_required', - 'label' => __('frontend_label'), - 'sortOrder' => 'sort_order', -@@ -1351,7 +1142,7 @@ class DataProviderTest extends \PHPUnit\Framework\TestCase - 'config' => [ - 'dataType' => 'frontend_input', - 'formElement' => 'frontend_input', -- 'visible' => $isRegistration, -+ 'visible' => true, - 'required' => 'is_required', - 'label' => __('frontend_label'), - 'sortOrder' => 'sort_order', -@@ -1374,7 +1165,7 @@ class DataProviderTest extends \PHPUnit\Framework\TestCase - 'config' => [ - 'dataType' => 'frontend_input', - 'formElement' => 'frontend_input', -- 'visible' => $isRegistration, -+ 'visible' => true, - 'required' => 'is_required', - 'label' => __('frontend_label'), - 'sortOrder' => 'sort_order', -@@ -1397,14 +1188,13 @@ class DataProviderTest extends \PHPUnit\Framework\TestCase - /** - * Retrieve all variations of attributes with all variations of visibility - * -- * @param bool $isRegistration - * @return array - */ -- private function getExpectationForVisibleAttributes($isRegistration = true) -+ private function getExpectationForVisibleAttributes() - { - return [ - 'customer' => [ -- 'children' => $this->getCustomerAttributeExpectations($isRegistration), -+ 'children' => $this->getCustomerAttributeExpectations(), - ], - 'address' => [ - 'children' => [ -diff --git a/app/code/Magento/Customer/Test/Unit/Model/Customer/DataProviderWithDefaultAddressesTest.php b/app/code/Magento/Customer/Test/Unit/Model/Customer/DataProviderWithDefaultAddressesTest.php -new file mode 100644 -index 00000000000..d5eaecb3ef3 ---- /dev/null -+++ b/app/code/Magento/Customer/Test/Unit/Model/Customer/DataProviderWithDefaultAddressesTest.php -@@ -0,0 +1,421 @@ -+<?php -+declare(strict_types=1); -+/** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+namespace Magento\Customer\Test\Unit\Model\Customer; -+ -+use Magento\Customer\Model\AttributeMetadataResolver; -+use Magento\Customer\Model\Customer\DataProviderWithDefaultAddresses; -+use Magento\Customer\Model\FileUploaderDataResolver; -+use Magento\Customer\Model\ResourceModel\Customer\CollectionFactory as CustomerCollectionFactory; -+use Magento\Customer\Model\ResourceModel\Customer\Collection as CustomerCollection; -+use Magento\Eav\Model\Config; -+use Magento\Eav\Model\Entity\Attribute\AbstractAttribute; -+use Magento\Eav\Model\Entity\Type; -+use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; -+use Magento\Ui\Component\Form\Field; -+ -+/** -+ * Test for class \Magento\Customer\Model\Customer\DataProviderWithDefaultAddresses -+ * -+ * @SuppressWarnings(PHPMD.CouplingBetweenObjects) -+ */ -+class DataProviderWithDefaultAddressesTest extends \PHPUnit\Framework\TestCase -+{ -+ private const ATTRIBUTE_CODE = 'test-code'; -+ -+ /** -+ * @var Config|\PHPUnit_Framework_MockObject_MockObject -+ */ -+ private $eavConfigMock; -+ -+ /** -+ * @var CustomerCollectionFactory|\PHPUnit_Framework_MockObject_MockObject -+ */ -+ private $customerCollectionFactoryMock; -+ -+ /** -+ * @var \Magento\Framework\Session\SessionManagerInterface|\PHPUnit_Framework_MockObject_MockObject -+ */ -+ private $sessionMock; -+ -+ /** -+ * @var \Magento\Directory\Model\CountryFactory|\PHPUnit_Framework_MockObject_MockObject -+ */ -+ private $countryFactoryMock; -+ -+ /** -+ * @var \Magento\Customer\Model\Customer|\PHPUnit_Framework_MockObject_MockObject -+ */ -+ private $customerMock; -+ -+ /** -+ * @var CustomerCollection|\PHPUnit_Framework_MockObject_MockObject -+ */ -+ private $customerCollectionMock; -+ -+ /** -+ * @var FileUploaderDataResolver|\PHPUnit_Framework_MockObject_MockObject -+ */ -+ private $fileUploaderDataResolver; -+ -+ /** -+ * @var AttributeMetadataResolver|\PHPUnit_Framework_MockObject_MockObject -+ */ -+ private $attributeMetadataResolver; -+ -+ /** -+ * @return void -+ */ -+ protected function setUp(): void -+ { -+ $this->eavConfigMock = $this->getMockBuilder(Config::class)->disableOriginalConstructor()->getMock(); -+ $this->customerCollectionFactoryMock = $this->createPartialMock(CustomerCollectionFactory::class, ['create']); -+ $this->sessionMock = $this->getMockBuilder(\Magento\Framework\Session\SessionManagerInterface::class) -+ ->setMethods(['getCustomerFormData', 'unsCustomerFormData']) -+ ->getMockForAbstractClass(); -+ $this->countryFactoryMock = $this->getMockBuilder(\Magento\Directory\Model\CountryFactory::class) -+ ->disableOriginalConstructor() -+ ->setMethods(['create', 'loadByCode', 'getName']) -+ ->getMock(); -+ $this->customerMock = $this->getMockBuilder(\Magento\Customer\Model\Customer::class) -+ ->disableOriginalConstructor() -+ ->getMock(); -+ $this->customerCollectionMock = $this->getMockBuilder(CustomerCollection::class) -+ ->disableOriginalConstructor() -+ ->getMock(); -+ $this->customerCollectionMock->expects($this->once())->method('addAttributeToSelect')->with('*'); -+ $this->customerCollectionFactoryMock->expects($this->once()) -+ ->method('create') -+ ->willReturn($this->customerCollectionMock); -+ $this->eavConfigMock->expects($this->atLeastOnce()) -+ ->method('getEntityType') -+ ->with('customer') -+ ->willReturn($this->getTypeCustomerMock([])); -+ $this->fileUploaderDataResolver = $this->getMockBuilder(FileUploaderDataResolver::class) -+ ->disableOriginalConstructor() -+ ->getMock(); -+ $this->attributeMetadataResolver = $this->getMockBuilder(AttributeMetadataResolver::class) -+ ->disableOriginalConstructor() -+ ->setMethods(['getAttributesMeta']) -+ ->getMock(); -+ $this->attributeMetadataResolver->expects($this->at(0)) -+ ->method('getAttributesMeta') -+ ->willReturn( -+ [ -+ 'arguments' => [ -+ 'data' => [ -+ 'config' => [ -+ 'dataType' => 'frontend_input', -+ 'formElement' => 'frontend_input', -+ 'options' => 'test-options', -+ 'visible' => null, -+ 'required' => 'is_required', -+ 'label' => __('frontend_label'), -+ 'sortOrder' => 'sort_order', -+ 'notice' => 'note', -+ 'default' => 'default_value', -+ 'size' => 'multiline_count', -+ 'componentType' => Field::NAME, -+ ], -+ ], -+ ], -+ ] -+ ); -+ $this->attributeMetadataResolver->expects($this->at(1)) -+ ->method('getAttributesMeta') -+ ->willReturn( -+ [ -+ 'arguments' => [ -+ 'data' => [ -+ 'config' => [ -+ 'dataType' => 'frontend_input', -+ 'formElement' => 'frontend_input', -+ 'visible' => null, -+ 'required' => 'is_required', -+ 'label' => __('frontend_label'), -+ 'sortOrder' => 'sort_order', -+ 'notice' => 'note', -+ 'default' => 'default_value', -+ 'size' => 'multiline_count', -+ 'componentType' => Field::NAME, -+ 'prefer' => 'toggle', -+ 'valueMap' => [ -+ 'true' => 1, -+ 'false' => 0, -+ ], -+ ], -+ ], -+ ], -+ ] -+ ); -+ $helper = new ObjectManager($this); -+ $this->dataProvider = $helper->getObject( -+ DataProviderWithDefaultAddresses::class, -+ [ -+ 'name' => 'test-name', -+ 'primaryFieldName' => 'primary-field-name', -+ 'requestFieldName' => 'request-field-name', -+ 'customerCollectionFactory' => $this->customerCollectionFactoryMock, -+ 'eavConfig' => $this->eavConfigMock, -+ 'countryFactory' => $this->countryFactoryMock, -+ 'session' => $this->sessionMock, -+ 'fileUploaderDataResolver' => $this->fileUploaderDataResolver, -+ 'attributeMetadataResolver' => $this->attributeMetadataResolver, -+ true -+ ] -+ ); -+ } -+ -+ /** -+ * Run test getAttributesMeta method -+ * -+ * @param array $expected -+ * @return void -+ * -+ * @dataProvider getAttributesMetaDataProvider -+ */ -+ public function testGetAttributesMetaWithOptions(array $expected): void -+ { -+ $meta = $this->dataProvider->getMeta(); -+ $this->assertNotEmpty($meta); -+ $this->assertEquals($expected, $meta); -+ } -+ -+ /** -+ * Data provider for testGetAttributesMeta -+ * -+ * @return array -+ */ -+ public function getAttributesMetaDataProvider(): array -+ { -+ return [ -+ [ -+ 'expected' => [ -+ 'customer' => [ -+ 'children' => [ -+ self::ATTRIBUTE_CODE => [ -+ 'arguments' => [ -+ 'data' => [ -+ 'config' => [ -+ 'dataType' => 'frontend_input', -+ 'formElement' => 'frontend_input', -+ 'options' => 'test-options', -+ 'visible' => null, -+ 'required' => 'is_required', -+ 'label' => __('frontend_label'), -+ 'sortOrder' => 'sort_order', -+ 'notice' => 'note', -+ 'default' => 'default_value', -+ 'size' => 'multiline_count', -+ 'componentType' => Field::NAME, -+ ], -+ ], -+ ], -+ ], -+ 'test-code-boolean' => [ -+ 'arguments' => [ -+ 'data' => [ -+ 'config' => [ -+ 'dataType' => 'frontend_input', -+ 'formElement' => 'frontend_input', -+ 'visible' => null, -+ 'required' => 'is_required', -+ 'label' => __('frontend_label'), -+ 'sortOrder' => 'sort_order', -+ 'notice' => 'note', -+ 'default' => 'default_value', -+ 'size' => 'multiline_count', -+ 'componentType' => Field::NAME, -+ 'prefer' => 'toggle', -+ 'valueMap' => [ -+ 'true' => 1, -+ 'false' => 0, -+ ], -+ ], -+ ], -+ ], -+ ], -+ ], -+ ], -+ ] -+ ] -+ ]; -+ } -+ -+ /** -+ * @param array $customerAttributes -+ * @return Type|\PHPUnit_Framework_MockObject_MockObject -+ */ -+ protected function getTypeCustomerMock($customerAttributes = []) -+ { -+ $typeCustomerMock = $this->getMockBuilder(\Magento\Eav\Model\Entity\Type::class) -+ ->disableOriginalConstructor() -+ ->getMock(); -+ $attributesCollection = !empty($customerAttributes) ? $customerAttributes : $this->getAttributeMock(); -+ foreach ($attributesCollection as $attribute) { -+ $attribute->expects($this->any()) -+ ->method('getEntityType') -+ ->willReturn($typeCustomerMock); -+ } -+ -+ $typeCustomerMock->expects($this->atLeastOnce()) -+ ->method('getAttributeCollection') -+ ->willReturn($attributesCollection); -+ -+ return $typeCustomerMock; -+ } -+ -+ /** -+ * @param array $options -+ * @return AbstractAttribute[]|\PHPUnit_Framework_MockObject_MockObject[] -+ */ -+ protected function getAttributeMock($options = []): array -+ { -+ $attributeMock = $this->getMockBuilder(AbstractAttribute::class) -+ ->setMethods( -+ [ -+ 'getAttributeCode', -+ 'getDataUsingMethod', -+ 'getFrontendInput', -+ 'getIsVisible', -+ 'getSource', -+ 'getIsUserDefined', -+ 'getUsedInForms', -+ 'getEntityType', -+ ] -+ ) -+ ->disableOriginalConstructor() -+ ->getMockForAbstractClass(); -+ -+ $attributeCode = self::ATTRIBUTE_CODE; -+ if (isset($options[self::ATTRIBUTE_CODE]['specific_code_prefix'])) { -+ $attributeCode .= $options[self::ATTRIBUTE_CODE]['specific_code_prefix']; -+ } -+ -+ $attributeMock->expects($this->once()) -+ ->method('getAttributeCode') -+ ->willReturn($attributeCode); -+ -+ $attributeBooleanMock = $this->getMockBuilder(AbstractAttribute::class) -+ ->setMethods( -+ [ -+ 'getAttributeCode', -+ 'getDataUsingMethod', -+ 'getFrontendInput', -+ 'getIsVisible', -+ 'getIsUserDefined', -+ 'getUsedInForms', -+ 'getSource', -+ 'getEntityType', -+ ] -+ ) -+ ->disableOriginalConstructor() -+ ->getMockForAbstractClass(); -+ -+ $booleanAttributeCode = 'test-code-boolean'; -+ if (isset($options['test-code-boolean']['specific_code_prefix'])) { -+ $booleanAttributeCode .= $options['test-code-boolean']['specific_code_prefix']; -+ } -+ -+ $attributeBooleanMock->expects($this->once()) -+ ->method('getAttributeCode') -+ ->willReturn($booleanAttributeCode); -+ -+ $mocks = [$attributeMock, $attributeBooleanMock]; -+ return $mocks; -+ } -+ -+ /** -+ * @return void -+ */ -+ public function testGetData(): void -+ { -+ $customerData = [ -+ 'email' => 'test@test.ua', -+ 'default_billing' => 2, -+ 'default_shipping' => 2, -+ 'password_hash' => 'password_hash', -+ 'rp_token' => 'rp_token', -+ ]; -+ -+ $address = $this->getMockBuilder(\Magento\Customer\Model\Address::class) -+ ->disableOriginalConstructor() -+ ->getMock(); -+ $this->customerCollectionMock->expects($this->once())->method('getItems')->willReturn([$this->customerMock]); -+ $this->customerMock->expects($this->once())->method('getData')->willReturn($customerData); -+ $this->customerMock->expects($this->atLeastOnce())->method('getId')->willReturn(1); -+ -+ $this->customerMock->expects($this->once())->method('getDefaultBillingAddress')->willReturn($address); -+ $this->countryFactoryMock->expects($this->once())->method('create')->willReturnSelf(); -+ $this->countryFactoryMock->expects($this->once())->method('loadByCode')->willReturnSelf(); -+ $this->countryFactoryMock->expects($this->once())->method('getName')->willReturn('Ukraine'); -+ -+ $this->sessionMock->expects($this->once()) -+ ->method('getCustomerFormData') -+ ->willReturn(null); -+ -+ $this->assertEquals( -+ [ -+ 1 => [ -+ 'customer' => [ -+ 'email' => 'test@test.ua', -+ 'default_billing' => 2, -+ 'default_shipping' => 2, -+ ], -+ 'default_billing_address' => [ -+ 'country' => 'Ukraine', -+ ], -+ 'default_shipping_address' => [], -+ 'customer_id' => 1 -+ ] -+ ], -+ $this->dataProvider->getData() -+ ); -+ } -+ -+ /** -+ * @return void -+ */ -+ public function testGetDataWithCustomerFormData(): void -+ { -+ $customerId = 11; -+ $customerFormData = [ -+ 'customer' => [ -+ 'email' => 'test1@test1.ua', -+ 'default_billing' => 3, -+ 'default_shipping' => 3, -+ 'entity_id' => $customerId, -+ ], -+ 'address' => [ -+ 3 => [ -+ 'firstname' => 'firstname1', -+ 'lastname' => 'lastname1', -+ 'street' => [ -+ 'street1', -+ 'street2', -+ ], -+ 'default_billing' => 3, -+ 'default_shipping' => 3, -+ ], -+ ], -+ ]; -+ -+ $this->customerCollectionMock->expects($this->once())->method('getItems')->willReturn([$this->customerMock]); -+ $this->customerMock->expects($this->once()) -+ ->method('getData') -+ ->willReturn([ -+ 'email' => 'test@test.ua', -+ 'default_billing' => 2, -+ 'default_shipping' => 2, -+ ]); -+ $this->customerMock->expects($this->atLeastOnce())->method('getId')->willReturn($customerId); -+ -+ $this->sessionMock->expects($this->once())->method('getCustomerFormData')->willReturn($customerFormData); -+ $this->sessionMock->expects($this->once())->method('unsCustomerFormData'); -+ -+ $this->assertEquals([$customerId => $customerFormData], $this->dataProvider->getData()); -+ } -+} -diff --git a/app/code/Magento/Customer/Test/Unit/Model/Customer/Source/GroupTest.php b/app/code/Magento/Customer/Test/Unit/Model/Customer/Source/GroupTest.php -index e07f2f0add9..9128d7c6752 100644 ---- a/app/code/Magento/Customer/Test/Unit/Model/Customer/Source/GroupTest.php -+++ b/app/code/Magento/Customer/Test/Unit/Model/Customer/Source/GroupTest.php -@@ -6,12 +6,15 @@ - namespace Magento\Customer\Test\Unit\Model\Customer\Source; - - use Magento\Customer\Model\Customer\Source\Group; --use Magento\Framework\Module\Manager; -+use Magento\Framework\Module\ModuleManagerInterface; - use Magento\Customer\Api\GroupRepositoryInterface; - use Magento\Framework\Api\SearchCriteriaBuilder; - use Magento\Framework\Api\SearchCriteria; - use Magento\Customer\Api\Data\GroupSearchResultsInterface; - -+/** -+ * Group test. -+ */ - class GroupTest extends \PHPUnit\Framework\TestCase - { - /** -@@ -20,7 +23,7 @@ class GroupTest extends \PHPUnit\Framework\TestCase - private $model; - - /** -- * @var Manager|\PHPUnit_Framework_MockObject_MockObject -+ * @var ModuleManagerInterface|\PHPUnit_Framework_MockObject_MockObject - */ - private $moduleManagerMock; - -@@ -46,7 +49,7 @@ class GroupTest extends \PHPUnit\Framework\TestCase - - protected function setUp() - { -- $this->moduleManagerMock = $this->getMockBuilder(Manager::class) -+ $this->moduleManagerMock = $this->getMockBuilder(ModuleManagerInterface::class) - ->disableOriginalConstructor() - ->getMock(); - $this->groupRepositoryMock = $this->getMockBuilder(GroupRepositoryInterface::class) -diff --git a/app/code/Magento/Customer/Test/Unit/Model/CustomerAuthUpdateTest.php b/app/code/Magento/Customer/Test/Unit/Model/CustomerAuthUpdateTest.php -index a1a243066bb..81a612c519f 100644 ---- a/app/code/Magento/Customer/Test/Unit/Model/CustomerAuthUpdateTest.php -+++ b/app/code/Magento/Customer/Test/Unit/Model/CustomerAuthUpdateTest.php -@@ -5,7 +5,14 @@ - */ - namespace Magento\Customer\Test\Unit\Model; - -+use Magento\Customer\Model\Customer as CustomerModel; - use Magento\Customer\Model\CustomerAuthUpdate; -+use Magento\Customer\Model\CustomerRegistry; -+use Magento\Customer\Model\Data\CustomerSecure; -+use Magento\Customer\Model\ResourceModel\Customer as CustomerResourceModel; -+use Magento\Framework\DB\Adapter\AdapterInterface; -+use Magento\Framework\Exception\NoSuchEntityException; -+use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; - - /** - * Class CustomerAuthUpdateTest -@@ -18,17 +25,22 @@ class CustomerAuthUpdateTest extends \PHPUnit\Framework\TestCase - protected $model; - - /** -- * @var \Magento\Customer\Model\CustomerRegistry|\PHPUnit_Framework_MockObject_MockObject -+ * @var CustomerRegistry|\PHPUnit_Framework_MockObject_MockObject - */ - protected $customerRegistry; - - /** -- * @var \Magento\Customer\Model\ResourceModel\Customer|\PHPUnit_Framework_MockObject_MockObject -+ * @var CustomerResourceModel|\PHPUnit_Framework_MockObject_MockObject - */ - protected $customerResourceModel; - - /** -- * @var \Magento\Framework\TestFramework\Unit\Helper\ObjectManager -+ * @var CustomerModel|\PHPUnit_Framework_MockObject_MockObject -+ */ -+ protected $customerModel; -+ -+ /** -+ * @var ObjectManager - */ - protected $objectManager; - -@@ -37,32 +49,36 @@ class CustomerAuthUpdateTest extends \PHPUnit\Framework\TestCase - */ - protected function setUp() - { -- $this->objectManager = new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this); -+ $this->objectManager = new ObjectManager($this); - - $this->customerRegistry = -- $this->createMock(\Magento\Customer\Model\CustomerRegistry::class); -+ $this->createMock(CustomerRegistry::class); - $this->customerResourceModel = -- $this->createMock(\Magento\Customer\Model\ResourceModel\Customer::class); -+ $this->createMock(CustomerResourceModel::class); -+ $this->customerModel = -+ $this->createMock(CustomerModel::class); - - $this->model = $this->objectManager->getObject( -- \Magento\Customer\Model\CustomerAuthUpdate::class, -+ CustomerAuthUpdate::class, - [ - 'customerRegistry' => $this->customerRegistry, - 'customerResourceModel' => $this->customerResourceModel, -+ 'customerModel' => $this->customerModel - ] - ); - } - - /** - * test SaveAuth -+ * @throws NoSuchEntityException - */ - public function testSaveAuth() - { - $customerId = 1; - -- $customerSecureMock = $this->createMock(\Magento\Customer\Model\Data\CustomerSecure::class); -+ $customerSecureMock = $this->createMock(CustomerSecure::class); - -- $dbAdapter = $this->createMock(\Magento\Framework\DB\Adapter\AdapterInterface::class); -+ $dbAdapter = $this->createMock(AdapterInterface::class); - - $this->customerRegistry->expects($this->once()) - ->method('retrieveSecureData') -@@ -98,6 +114,9 @@ class CustomerAuthUpdateTest extends \PHPUnit\Framework\TestCase - $customerId - ); - -+ $this->customerModel->expects($this->once()) -+ ->method('reindex'); -+ - $this->model->saveAuth($customerId); - } - } -diff --git a/app/code/Magento/Customer/Test/Unit/Model/CustomerTest.php b/app/code/Magento/Customer/Test/Unit/Model/CustomerTest.php -index 9848a09540c..65831069aa1 100644 ---- a/app/code/Magento/Customer/Test/Unit/Model/CustomerTest.php -+++ b/app/code/Magento/Customer/Test/Unit/Model/CustomerTest.php -@@ -13,9 +13,12 @@ namespace Magento\Customer\Test\Unit\Model; - - use Magento\Customer\Model\Customer; - use Magento\Customer\Model\AccountConfirmation; -+use Magento\Customer\Model\ResourceModel\Address\CollectionFactory as AddressCollectionFactory; -+use Magento\Customer\Api\Data\CustomerInterfaceFactory; - - /** - * @SuppressWarnings(PHPMD.CouplingBetweenObjects) -+ * @SuppressWarnings(PHPMD.TooManyFields) - */ - class CustomerTest extends \PHPUnit\Framework\TestCase - { -@@ -68,6 +71,21 @@ class CustomerTest extends \PHPUnit\Framework\TestCase - */ - private $accountConfirmation; - -+ /** -+ * @var AddressCollectionFactory|\PHPUnit_Framework_MockObject_MockObject -+ */ -+ private $addressesFactory; -+ -+ /** -+ * @var CustomerInterfaceFactory|\PHPUnit_Framework_MockObject_MockObject -+ */ -+ private $customerDataFactory; -+ -+ /** -+ * @var \Magento\Framework\Api\DataObjectHelper|\PHPUnit_Framework_MockObject_MockObject -+ */ -+ private $dataObjectHelper; -+ - protected function setUp() - { - $this->_website = $this->createMock(\Magento\Store\Model\Website::class); -@@ -100,6 +118,19 @@ class CustomerTest extends \PHPUnit\Framework\TestCase - $this->_encryptor = $this->createMock(\Magento\Framework\Encryption\EncryptorInterface::class); - $helper = new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this); - $this->accountConfirmation = $this->createMock(AccountConfirmation::class); -+ $this->addressesFactory = $this->getMockBuilder(AddressCollectionFactory::class) -+ ->disableOriginalConstructor() -+ ->setMethods(['create']) -+ ->getMock(); -+ $this->customerDataFactory = $this->getMockBuilder(CustomerInterfaceFactory::class) -+ ->disableOriginalConstructor() -+ ->setMethods(['create']) -+ ->getMock(); -+ $this->dataObjectHelper = $this->getMockBuilder(\Magento\Framework\Api\DataObjectHelper::class) -+ ->disableOriginalConstructor() -+ ->setMethods(['populateWithArray']) -+ ->getMock(); -+ - $this->_model = $helper->getObject( - \Magento\Customer\Model\Customer::class, - [ -@@ -112,7 +143,10 @@ class CustomerTest extends \PHPUnit\Framework\TestCase - 'registry' => $this->registryMock, - 'resource' => $this->resourceMock, - 'dataObjectProcessor' => $this->dataObjectProcessor, -- 'accountConfirmation' => $this->accountConfirmation -+ 'accountConfirmation' => $this->accountConfirmation, -+ '_addressesFactory' => $this->addressesFactory, -+ 'customerDataFactory' => $this->customerDataFactory, -+ 'dataObjectHelper' => $this->dataObjectHelper - ] - ); - } -@@ -186,13 +220,13 @@ class CustomerTest extends \PHPUnit\Framework\TestCase - ->will($this->returnValue($transportMock)); - - $this->_model->setData([ -- 'website_id' => 1, -- 'store_id' => 1, -- 'email' => 'email@example.com', -- 'firstname' => 'FirstName', -- 'lastname' => 'LastName', -- 'middlename' => 'MiddleName', -- 'prefix' => 'Name Prefix', -+ 'website_id' => 1, -+ 'store_id' => 1, -+ 'email' => 'email@example.com', -+ 'firstname' => 'FirstName', -+ 'lastname' => 'LastName', -+ 'middlename' => 'MiddleName', -+ 'prefix' => 'Name Prefix', - ]); - $this->_model->sendNewAccountEmail('registered'); - } -@@ -307,9 +341,46 @@ class CustomerTest extends \PHPUnit\Framework\TestCase - } - - $expectedResult[$attribute->getAttributeCode()] = $attribute->getValue(); -- $expectedResult['attribute_set_id'] = -- \Magento\Customer\Api\CustomerMetadataInterface::ATTRIBUTE_SET_ID_CUSTOMER; - - $this->assertEquals($this->_model->getData(), $expectedResult); - } -+ -+ /** -+ * Test for the \Magento\Customer\Model\Customer::getDataModel() method -+ */ -+ public function testGetDataModel() -+ { -+ $customerId = 1; -+ $this->_model->setEntityId($customerId); -+ $this->_model->setId($customerId); -+ $addressDataModel = $this->getMockForAbstractClass(\Magento\Customer\Api\Data\AddressInterface::class); -+ $address = $this->getMockBuilder(\Magento\Customer\Model\Address::class) -+ ->disableOriginalConstructor() -+ ->setMethods(['setCustomer', 'getDataModel']) -+ ->getMock(); -+ $address->expects($this->atLeastOnce())->method('getDataModel')->willReturn($addressDataModel); -+ $addresses = new \ArrayIterator([$address, $address]); -+ $addressCollection = $this->getMockBuilder(\Magento\Customer\Model\ResourceModel\Address\Collection::class) -+ ->disableOriginalConstructor() -+ ->setMethods(['setCustomerFilter', 'addAttributeToSelect', 'getIterator', 'getItems']) -+ ->getMock(); -+ $addressCollection->expects($this->atLeastOnce())->method('setCustomerFilter')->willReturnSelf(); -+ $addressCollection->expects($this->atLeastOnce())->method('addAttributeToSelect')->willReturnSelf(); -+ $addressCollection->expects($this->atLeastOnce())->method('getIterator') -+ ->willReturn($addresses); -+ $addressCollection->expects($this->atLeastOnce())->method('getItems') -+ ->willReturn($addresses); -+ $this->addressesFactory->expects($this->atLeastOnce())->method('create')->willReturn($addressCollection); -+ $customerDataObject = $this->getMockForAbstractClass(\Magento\Customer\Api\Data\CustomerInterface::class); -+ $this->customerDataFactory->expects($this->atLeastOnce())->method('create')->willReturn($customerDataObject); -+ $this->dataObjectHelper->expects($this->atLeastOnce())->method('populateWithArray') -+ ->with($customerDataObject, $this->_model->getData(), \Magento\Customer\Api\Data\CustomerInterface::class) -+ ->willReturnSelf(); -+ $customerDataObject->expects($this->atLeastOnce())->method('setAddresses') -+ ->with([$addressDataModel, $addressDataModel]) -+ ->willReturnSelf(); -+ $customerDataObject->expects($this->atLeastOnce())->method('setId')->with($customerId)->willReturnSelf(); -+ $this->_model->getDataModel(); -+ $this->assertEquals($customerDataObject, $this->_model->getDataModel()); -+ } - } -diff --git a/app/code/Magento/Customer/Test/Unit/Model/Metadata/AttributeMetadataCacheTest.php b/app/code/Magento/Customer/Test/Unit/Model/Metadata/AttributeMetadataCacheTest.php -index 658472d13ab..83915731ea5 100644 ---- a/app/code/Magento/Customer/Test/Unit/Model/Metadata/AttributeMetadataCacheTest.php -+++ b/app/code/Magento/Customer/Test/Unit/Model/Metadata/AttributeMetadataCacheTest.php -@@ -15,7 +15,14 @@ use Magento\Framework\App\Cache\StateInterface; - use Magento\Framework\App\CacheInterface; - use Magento\Framework\Serialize\SerializerInterface; - use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; -+use Magento\Store\Api\Data\StoreInterface; -+use Magento\Store\Model\StoreManagerInterface; - -+/** -+ * AttributeMetadataCache Test -+ * -+ * @SuppressWarnings(PHPMD.CouplingBetweenObjects) -+ */ - class AttributeMetadataCacheTest extends \PHPUnit\Framework\TestCase - { - /** -@@ -43,6 +50,16 @@ class AttributeMetadataCacheTest extends \PHPUnit\Framework\TestCase - */ - private $attributeMetadataCache; - -+ /** -+ * @var StoreInterface|\PHPUnit_Framework_MockObject_MockObject -+ */ -+ private $storeMock; -+ -+ /** -+ * @var StoreManagerInterface|\PHPUnit_Framework_MockObject_MockObject -+ */ -+ private $storeManagerMock; -+ - protected function setUp() - { - $objectManager = new ObjectManager($this); -@@ -50,13 +67,18 @@ class AttributeMetadataCacheTest extends \PHPUnit\Framework\TestCase - $this->stateMock = $this->createMock(StateInterface::class); - $this->serializerMock = $this->createMock(SerializerInterface::class); - $this->attributeMetadataHydratorMock = $this->createMock(AttributeMetadataHydrator::class); -+ $this->storeMock = $this->createMock(StoreInterface::class); -+ $this->storeManagerMock = $this->createMock(StoreManagerInterface::class); -+ $this->storeManagerMock->method('getStore')->willReturn($this->storeMock); -+ $this->storeMock->method('getId')->willReturn(1); - $this->attributeMetadataCache = $objectManager->getObject( - AttributeMetadataCache::class, - [ - 'cache' => $this->cacheMock, - 'state' => $this->stateMock, - 'serializer' => $this->serializerMock, -- 'attributeMetadataHydrator' => $this->attributeMetadataHydratorMock -+ 'attributeMetadataHydrator' => $this->attributeMetadataHydratorMock, -+ 'storeManager' => $this->storeManagerMock - ] - ); - } -@@ -80,7 +102,8 @@ class AttributeMetadataCacheTest extends \PHPUnit\Framework\TestCase - { - $entityType = 'EntityType'; - $suffix = 'none'; -- $cacheKey = AttributeMetadataCache::ATTRIBUTE_METADATA_CACHE_PREFIX . $entityType . $suffix; -+ $storeId = 1; -+ $cacheKey = AttributeMetadataCache::ATTRIBUTE_METADATA_CACHE_PREFIX . $entityType . $suffix . $storeId; - $this->stateMock->expects($this->once()) - ->method('isEnabled') - ->with(Type::TYPE_IDENTIFIER) -@@ -96,7 +119,8 @@ class AttributeMetadataCacheTest extends \PHPUnit\Framework\TestCase - { - $entityType = 'EntityType'; - $suffix = 'none'; -- $cacheKey = AttributeMetadataCache::ATTRIBUTE_METADATA_CACHE_PREFIX . $entityType . $suffix; -+ $storeId = 1; -+ $cacheKey = AttributeMetadataCache::ATTRIBUTE_METADATA_CACHE_PREFIX . $entityType . $suffix . $storeId; - $serializedString = 'serialized string'; - $attributeMetadataOneData = [ - 'attribute_code' => 'attribute_code', -@@ -156,7 +180,8 @@ class AttributeMetadataCacheTest extends \PHPUnit\Framework\TestCase - { - $entityType = 'EntityType'; - $suffix = 'none'; -- $cacheKey = AttributeMetadataCache::ATTRIBUTE_METADATA_CACHE_PREFIX . $entityType . $suffix; -+ $storeId = 1; -+ $cacheKey = AttributeMetadataCache::ATTRIBUTE_METADATA_CACHE_PREFIX . $entityType . $suffix . $storeId; - $serializedString = 'serialized string'; - $attributeMetadataOneData = [ - 'attribute_code' => 'attribute_code', -diff --git a/app/code/Magento/Customer/Test/Unit/Model/Metadata/Form/AbstractDataTest.php b/app/code/Magento/Customer/Test/Unit/Model/Metadata/Form/AbstractDataTest.php -index e4dc22ba40e..5b4b50ca821 100644 ---- a/app/code/Magento/Customer/Test/Unit/Model/Metadata/Form/AbstractDataTest.php -+++ b/app/code/Magento/Customer/Test/Unit/Model/Metadata/Form/AbstractDataTest.php -@@ -205,37 +205,32 @@ class AbstractDataTest extends \PHPUnit\Framework\TestCase - } - - /** -+ * Tests input validation rules. -+ * - * @param null|string $value - * @param null|string $label - * @param null|string $inputValidation - * @param bool|array $expectedOutput - * @dataProvider validateInputRuleDataProvider - */ -- public function testValidateInputRule($value, $label, $inputValidation, $expectedOutput) -+ public function testValidateInputRule($value, $label, $inputValidation, $expectedOutput): void - { - $validationRule = $this->getMockBuilder(\Magento\Customer\Api\Data\ValidationRuleInterface::class) - ->disableOriginalConstructor() - ->setMethods(['getName', 'getValue']) - ->getMockForAbstractClass(); -- $validationRule->expects($this->any()) -- ->method('getName') -- ->will($this->returnValue('input_validation')); -- $validationRule->expects($this->any()) -- ->method('getValue') -- ->will($this->returnValue($inputValidation)); -- -- $this->_attributeMock->expects($this->any())->method('getStoreLabel')->will($this->returnValue($label)); -- $this->_attributeMock->expects( -- $this->any() -- )->method( -- 'getValidationRules' -- )->will( -- $this->returnValue( -- [ -- $validationRule, -- ] -- ) -- ); -+ -+ $validationRule->method('getName') -+ ->willReturn('input_validation'); -+ -+ $validationRule->method('getValue') -+ ->willReturn($inputValidation); -+ -+ $this->_attributeMock->method('getStoreLabel') -+ ->willReturn($label); -+ -+ $this->_attributeMock->method('getValidationRules') -+ ->willReturn([$validationRule]); - - $this->assertEquals($expectedOutput, $this->_model->validateInputRule($value)); - } -@@ -256,6 +251,16 @@ class AbstractDataTest extends \PHPUnit\Framework\TestCase - \Zend_Validate_Alnum::NOT_ALNUM => '"mylabel" contains non-alphabetic or non-numeric characters.' - ] - ], -+ [ -+ 'abc qaz', -+ 'mylabel', -+ 'alphanumeric', -+ [ -+ \Zend_Validate_Alnum::NOT_ALNUM => '"mylabel" contains non-alphabetic or non-numeric characters.' -+ ] -+ ], -+ ['abcqaz', 'mylabel', 'alphanumeric', true], -+ ['abc qaz', 'mylabel', 'alphanum-with-spaces', true], - [ - '!@#$', - 'mylabel', -diff --git a/app/code/Magento/Customer/Test/Unit/Model/Metadata/Form/DateTest.php b/app/code/Magento/Customer/Test/Unit/Model/Metadata/Form/DateTest.php -index 6329970e0ca..553efea38a8 100644 ---- a/app/code/Magento/Customer/Test/Unit/Model/Metadata/Form/DateTest.php -+++ b/app/code/Magento/Customer/Test/Unit/Model/Metadata/Form/DateTest.php -@@ -12,6 +12,9 @@ class DateTest extends AbstractFormTestCase - /** @var \Magento\Customer\Model\Metadata\Form\Date */ - protected $date; - -+ /** -+ * @inheritdoc -+ */ - protected function setUp() - { - parent::setUp(); -@@ -46,6 +49,9 @@ class DateTest extends AbstractFormTestCase - ); - } - -+ /** -+ * Test extractValue -+ */ - public function testExtractValue() - { - $requestMock = $this->getMockBuilder(\Magento\Framework\App\RequestInterface::class) -@@ -174,7 +180,7 @@ class DateTest extends AbstractFormTestCase - return [ - [1, 1], - [false, false], -- ['', null], -+ [null, null], - ['test', 'test'], - [['element1', 'element2'], ['element1', 'element2']] - ]; -@@ -191,6 +197,9 @@ class DateTest extends AbstractFormTestCase - $this->assertSame($expected, $this->date->restoreValue($value)); - } - -+ /** -+ * Test outputValue -+ */ - public function testOutputValue() - { - $this->assertEquals(null, $this->date->outputValue()); -diff --git a/app/code/Magento/Customer/Test/Unit/Model/Metadata/Form/ImageTest.php b/app/code/Magento/Customer/Test/Unit/Model/Metadata/Form/ImageTest.php -index 0278e2b2d79..31d2a31ceae 100644 ---- a/app/code/Magento/Customer/Test/Unit/Model/Metadata/Form/ImageTest.php -+++ b/app/code/Magento/Customer/Test/Unit/Model/Metadata/Form/ImageTest.php -@@ -259,7 +259,7 @@ class ImageTest extends AbstractFormTestCase - )->getMockForAbstractClass(); - $validationRuleMock->expects($this->any()) - ->method('getName') -- ->willReturn('max_image_heght'); -+ ->willReturn('max_image_height'); - $validationRuleMock->expects($this->any()) - ->method('getValue') - ->willReturn($maxImageHeight); -diff --git a/app/code/Magento/Customer/Test/Unit/Model/Renderer/RegionTest.php b/app/code/Magento/Customer/Test/Unit/Model/Renderer/RegionTest.php -index c655ff7056e..e67adc47b88 100644 ---- a/app/code/Magento/Customer/Test/Unit/Model/Renderer/RegionTest.php -+++ b/app/code/Magento/Customer/Test/Unit/Model/Renderer/RegionTest.php -@@ -5,6 +5,8 @@ - */ - namespace Magento\Customer\Test\Unit\Model\Renderer; - -+use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; -+ - class RegionTest extends \PHPUnit\Framework\TestCase - { - /** -@@ -58,6 +60,14 @@ class RegionTest extends \PHPUnit\Framework\TestCase - ] - ) - ); -+ -+ $objectManager = new ObjectManager($this); -+ $escaper = $objectManager->getObject(\Magento\Framework\Escaper::class); -+ $reflection = new \ReflectionClass($elementMock); -+ $reflection_property = $reflection->getProperty('_escaper'); -+ $reflection_property->setAccessible(true); -+ $reflection_property->setValue($elementMock, $escaper); -+ - $formMock->expects( - $this->any() - )->method( -diff --git a/app/code/Magento/Customer/Test/Unit/Model/ResourceModel/Address/RelationTest.php b/app/code/Magento/Customer/Test/Unit/Model/ResourceModel/Address/RelationTest.php -index e81637cfb23..319179c5e27 100644 ---- a/app/code/Magento/Customer/Test/Unit/Model/ResourceModel/Address/RelationTest.php -+++ b/app/code/Magento/Customer/Test/Unit/Model/ResourceModel/Address/RelationTest.php -@@ -6,6 +6,7 @@ - namespace Magento\Customer\Test\Unit\Model\ResourceModel\Address; - - use Magento\Framework\TestFramework\Unit\Helper\ObjectManager as ObjectManagerHelper; -+use Magento\Customer\Model\Address; - - /** - * Class AddressTest -@@ -40,7 +41,7 @@ class RelationTest extends \PHPUnit\Framework\TestCase - */ - public function testProcessRelation($addressId, $isDefaultBilling, $isDefaultShipping) - { -- $addressModel = $this->createPartialMock(\Magento\Framework\Model\AbstractModel::class, [ -+ $addressModel = $this->createPartialMock(Address::class, [ - '__wakeup', - 'getId', - 'getEntityTypeId', -@@ -55,7 +56,17 @@ class RelationTest extends \PHPUnit\Framework\TestCase - ]); - $customerModel = $this->createPartialMock( - \Magento\Customer\Model\Customer::class, -- ['__wakeup', 'setDefaultBilling', 'setDefaultShipping', 'save', 'load', 'getResource', 'getId'] -+ [ -+ '__wakeup', -+ 'setDefaultBilling', -+ 'setDefaultShipping', -+ 'save', -+ 'load', -+ 'getResource', -+ 'getId', -+ 'getDefaultShippingAddress', -+ 'getDefaultBillingAddress' -+ ] - ); - $customerResource = $this->getMockForAbstractClass( - \Magento\Framework\Model\ResourceModel\Db\AbstractDb::class, -@@ -88,6 +99,7 @@ class RelationTest extends \PHPUnit\Framework\TestCase - $this->customerFactoryMock->expects($this->any()) - ->method('create') - ->willReturn($customerModel); -+ - if ($addressId && ($isDefaultBilling || $isDefaultShipping)) { - $customerId = 1; - $customerResource->expects($this->exactly(2))->method('getConnection')->willReturn($connectionMock); -diff --git a/app/code/Magento/Customer/Test/Unit/Model/ResourceModel/CustomerRepositoryTest.php b/app/code/Magento/Customer/Test/Unit/Model/ResourceModel/CustomerRepositoryTest.php -index bd1dc774b53..8032399e148 100644 ---- a/app/code/Magento/Customer/Test/Unit/Model/ResourceModel/CustomerRepositoryTest.php -+++ b/app/code/Magento/Customer/Test/Unit/Model/ResourceModel/CustomerRepositoryTest.php -@@ -107,7 +107,7 @@ class CustomerRepositoryTest extends \PHPUnit\Framework\TestCase - $this->createMock(\Magento\Customer\Model\ResourceModel\Customer::class); - $this->customerRegistry = $this->createMock(\Magento\Customer\Model\CustomerRegistry::class); - $this->dataObjectHelper = $this->createMock(\Magento\Framework\Api\DataObjectHelper::class); -- $this->customerFactory = -+ $this->customerFactory = - $this->createPartialMock(\Magento\Customer\Model\CustomerFactory::class, ['create']); - $this->customerSecureFactory = $this->createPartialMock( - \Magento\Customer\Model\Data\CustomerSecureFactory::class, -@@ -193,38 +193,10 @@ class CustomerRepositoryTest extends \PHPUnit\Framework\TestCase - public function testSave() - { - $customerId = 1; -- $storeId = 2; - -- $region = $this->getMockForAbstractClass(\Magento\Customer\Api\Data\RegionInterface::class, [], '', false); -- $address = $this->getMockForAbstractClass( -- \Magento\Customer\Api\Data\AddressInterface::class, -- [], -- '', -- false, -- false, -- true, -+ $customerModel = $this->createPartialMock( -+ \Magento\Customer\Model\Customer::class, - [ -- 'setCustomerId', -- 'setRegion', -- 'getRegion', -- 'getId' -- ] -- ); -- $address2 = $this->getMockForAbstractClass( -- \Magento\Customer\Api\Data\AddressInterface::class, -- [], -- '', -- false, -- false, -- true, -- [ -- 'setCustomerId', -- 'setRegion', -- 'getRegion', -- 'getId' -- ] -- ); -- $customerModel = $this->createPartialMock(\Magento\Customer\Model\Customer::class, [ - 'getId', - 'setId', - 'setStoreId', -@@ -239,14 +211,11 @@ class CustomerRepositoryTest extends \PHPUnit\Framework\TestCase - 'setFirstFailure', - 'setLockExpires', - 'save', -- ]); -+ ] -+ ); - - $origCustomer = $this->customer; - -- $this->customer->expects($this->atLeastOnce()) -- ->method('__toArray') -- ->willReturn(['default_billing', 'default_shipping']); -- - $customerAttributesMetaData = $this->getMockForAbstractClass( - \Magento\Framework\Api\CustomAttributesDataInterface::class, - [], -@@ -262,17 +231,23 @@ class CustomerRepositoryTest extends \PHPUnit\Framework\TestCase - 'setAddresses' - ] - ); -- $customerSecureData = $this->createPartialMock(\Magento\Customer\Model\Data\CustomerSecure::class, [ -- 'getRpToken', -- 'getRpTokenCreatedAt', -- 'getPasswordHash', -- 'getFailuresNum', -- 'getFirstFailure', -- 'getLockExpires', -- ]); -+ $customerSecureData = $this->createPartialMock( -+ \Magento\Customer\Model\Data\CustomerSecure::class, -+ [ -+ 'getRpToken', -+ 'getRpTokenCreatedAt', -+ 'getPasswordHash', -+ 'getFailuresNum', -+ 'getFirstFailure', -+ 'getLockExpires', -+ ] -+ ); - $this->customer->expects($this->atLeastOnce()) - ->method('getId') - ->willReturn($customerId); -+ $this->customer->expects($this->atLeastOnce()) -+ ->method('__toArray') -+ ->willReturn([]); - $this->customerRegistry->expects($this->atLeastOnce()) - ->method('retrieve') - ->with($customerId) -@@ -287,28 +262,6 @@ class CustomerRepositoryTest extends \PHPUnit\Framework\TestCase - $this->customerRegistry->expects($this->atLeastOnce()) - ->method("remove") - ->with($customerId); -- $address->expects($this->once()) -- ->method('setCustomerId') -- ->with($customerId) -- ->willReturnSelf(); -- $address->expects($this->once()) -- ->method('getRegion') -- ->willReturn($region); -- $address->expects($this->atLeastOnce()) -- ->method('getId') -- ->willReturn(7); -- $address->expects($this->once()) -- ->method('setRegion') -- ->with($region); -- $customerAttributesMetaData->expects($this->atLeastOnce()) -- ->method('getAddresses') -- ->willReturn([$address]); -- $customerAttributesMetaData->expects($this->at(1)) -- ->method('setAddresses') -- ->with([]); -- $customerAttributesMetaData->expects($this->at(2)) -- ->method('setAddresses') -- ->with([$address]); - $this->extensibleDataObjectConverter->expects($this->once()) - ->method('toNestedArray') - ->with($customerAttributesMetaData, [], \Magento\Customer\Api\Data\CustomerInterface::class) -@@ -320,26 +273,9 @@ class CustomerRepositoryTest extends \PHPUnit\Framework\TestCase - $customerModel->expects($this->once()) - ->method('getStoreId') - ->willReturn(null); -- $store = $this->createMock(\Magento\Store\Model\Store::class); -- $store->expects($this->once()) -- ->method('getId') -- ->willReturn($storeId); -- $this->storeManager -- ->expects($this->once()) -- ->method('getStore') -- ->willReturn($store); -- $customerModel->expects($this->once()) -- ->method('setStoreId') -- ->with($storeId); - $customerModel->expects($this->once()) - ->method('setId') - ->with($customerId); -- $customerModel->expects($this->once()) -- ->method('getAttributeSetId') -- ->willReturn(null); -- $customerModel->expects($this->once()) -- ->method('setAttributeSetId') -- ->with(\Magento\Customer\Api\CustomerMetadataInterface::ATTRIBUTE_SET_ID_CUSTOMER); - $customerAttributesMetaData->expects($this->atLeastOnce()) - ->method('getId') - ->willReturn($customerId); -@@ -368,16 +304,20 @@ class CustomerRepositoryTest extends \PHPUnit\Framework\TestCase - - $customerModel->expects($this->once()) - ->method('setRpToken') -- ->willReturnMap([ -+ ->willReturnMap( -+ [ - ['rpToken', $customerModel], - [null, $customerModel], -- ]); -+ ] -+ ); - $customerModel->expects($this->once()) - ->method('setRpTokenCreatedAt') -- ->willReturnMap([ -+ ->willReturnMap( -+ [ - ['rpTokenCreatedAt', $customerModel], - [null, $customerModel], -- ]); -+ ] -+ ); - - $customerModel->expects($this->once()) - ->method('setPasswordHash') -@@ -399,12 +339,6 @@ class CustomerRepositoryTest extends \PHPUnit\Framework\TestCase - $this->customerRegistry->expects($this->once()) - ->method('push') - ->with($customerModel); -- $this->customer->expects($this->once()) -- ->method('getAddresses') -- ->willReturn([$address, $address2]); -- $this->addressRepository->expects($this->once()) -- ->method('save') -- ->with($address); - $customerAttributesMetaData->expects($this->once()) - ->method('getEmail') - ->willReturn('example@example.com'); -@@ -435,71 +369,37 @@ class CustomerRepositoryTest extends \PHPUnit\Framework\TestCase - public function testSaveWithPasswordHash() - { - $customerId = 1; -- $storeId = 2; - $passwordHash = 'ukfa4sdfa56s5df02asdf4rt'; - -- $customerSecureData = $this->createPartialMock(\Magento\Customer\Model\Data\CustomerSecure::class, [ -- 'getRpToken', -- 'getRpTokenCreatedAt', -- 'getPasswordHash', -- 'getFailuresNum', -- 'getFirstFailure', -- 'getLockExpires', -- ]); -- $region = $this->getMockForAbstractClass( -- \Magento\Customer\Api\Data\RegionInterface::class, -- [], -- '', -- false -- ); -- $address = $this->getMockForAbstractClass( -- \Magento\Customer\Api\Data\AddressInterface::class, -- [], -- '', -- false, -- false, -- true, -+ $customerSecureData = $this->createPartialMock( -+ \Magento\Customer\Model\Data\CustomerSecure::class, - [ -- 'setCustomerId', -- 'setRegion', -- 'getRegion', -- 'getId' -+ 'getRpToken', -+ 'getRpTokenCreatedAt', -+ 'getPasswordHash', -+ 'getFailuresNum', -+ 'getFirstFailure', -+ 'getLockExpires', - ] - ); -- $address2 = $this->getMockForAbstractClass( -- \Magento\Customer\Api\Data\AddressInterface::class, -- [], -- '', -- false, -- false, -- true, -+ $origCustomer = $this->customer; -+ -+ $customerModel = $this->createPartialMock( -+ \Magento\Customer\Model\Customer::class, - [ -- 'setCustomerId', -- 'setRegion', -- 'getRegion', -- 'getId' -+ 'getId', -+ 'setId', -+ 'setStoreId', -+ 'getStoreId', -+ 'getAttributeSetId', -+ 'setAttributeSetId', -+ 'setRpToken', -+ 'setRpTokenCreatedAt', -+ 'getDataModel', -+ 'setPasswordHash', -+ 'save', - ] - ); -- -- $origCustomer = $this->customer; -- -- $this->customer->expects($this->atLeastOnce()) -- ->method('__toArray') -- ->willReturn(['default_billing', 'default_shipping']); -- -- $customerModel = $this->createPartialMock(\Magento\Customer\Model\Customer::class, [ -- 'getId', -- 'setId', -- 'setStoreId', -- 'getStoreId', -- 'getAttributeSetId', -- 'setAttributeSetId', -- 'setRpToken', -- 'setRpTokenCreatedAt', -- 'getDataModel', -- 'setPasswordHash', -- 'save', -- ]); - $customerAttributesMetaData = $this->getMockForAbstractClass( - \Magento\Framework\Api\CustomAttributesDataInterface::class, - [], -@@ -550,10 +450,12 @@ class CustomerRepositoryTest extends \PHPUnit\Framework\TestCase - $customerSecureData->expects($this->once()) - ->method('getLockExpires') - ->willReturn('lockExpires'); -- - $this->customer->expects($this->atLeastOnce()) - ->method('getId') - ->willReturn($customerId); -+ $this->customer->expects($this->atLeastOnce()) -+ ->method('__toArray') -+ ->willReturn([]); - $this->customerRegistry->expects($this->atLeastOnce()) - ->method('retrieve') - ->with($customerId) -@@ -565,28 +467,6 @@ class CustomerRepositoryTest extends \PHPUnit\Framework\TestCase - ->method('save') - ->with($this->customer, CustomerMetadataInterface::ENTITY_TYPE_CUSTOMER, $this->customer) - ->willReturn($customerAttributesMetaData); -- $address->expects($this->once()) -- ->method('setCustomerId') -- ->with($customerId) -- ->willReturnSelf(); -- $address->expects($this->once()) -- ->method('getRegion') -- ->willReturn($region); -- $address->expects($this->atLeastOnce()) -- ->method('getId') -- ->willReturn(7); -- $address->expects($this->once()) -- ->method('setRegion') -- ->with($region); -- $customerAttributesMetaData->expects($this->any()) -- ->method('getAddresses') -- ->willReturn([$address]); -- $customerAttributesMetaData->expects($this->at(1)) -- ->method('setAddresses') -- ->with([]); -- $customerAttributesMetaData->expects($this->at(2)) -- ->method('setAddresses') -- ->with([$address]); - $customerAttributesMetaData - ->expects($this->atLeastOnce()) - ->method('getId') -@@ -599,29 +479,9 @@ class CustomerRepositoryTest extends \PHPUnit\Framework\TestCase - ->method('create') - ->with(['data' => ['customerData']]) - ->willReturn($customerModel); -- $customerModel->expects($this->once()) -- ->method('getStoreId') -- ->willReturn(null); -- $store = $this->createMock(\Magento\Store\Model\Store::class); -- $store->expects($this->once()) -- ->method('getId') -- ->willReturn($storeId); -- $this->storeManager -- ->expects($this->once()) -- ->method('getStore') -- ->willReturn($store); -- $customerModel->expects($this->once()) -- ->method('setStoreId') -- ->with($storeId); - $customerModel->expects($this->once()) - ->method('setId') - ->with($customerId); -- $customerModel->expects($this->once()) -- ->method('getAttributeSetId') -- ->willReturn(null); -- $customerModel->expects($this->once()) -- ->method('setAttributeSetId') -- ->with(\Magento\Customer\Api\CustomerMetadataInterface::ATTRIBUTE_SET_ID_CUSTOMER); - $customerModel->expects($this->atLeastOnce()) - ->method('getId') - ->willReturn($customerId); -@@ -630,12 +490,6 @@ class CustomerRepositoryTest extends \PHPUnit\Framework\TestCase - $this->customerRegistry->expects($this->once()) - ->method('push') - ->with($customerModel); -- $this->customer->expects($this->any()) -- ->method('getAddresses') -- ->willReturn([$address, $address2]); -- $this->addressRepository->expects($this->once()) -- ->method('save') -- ->with($address); - $customerAttributesMetaData->expects($this->once()) - ->method('getEmail') - ->willReturn('example@example.com'); -@@ -752,7 +606,7 @@ class CustomerRepositoryTest extends \PHPUnit\Framework\TestCase - ->willReturnSelf(); - $collection->expects($this->at(7)) - ->method('joinAttribute') -- ->with('company', 'customer_address/company', 'default_billing', null, 'left') -+ ->with('billing_company', 'customer_address/company', 'default_billing', null, 'left') - ->willReturnSelf(); - $this->collectionProcessorMock->expects($this->once()) - ->method('process') -diff --git a/app/code/Magento/Customer/Test/Unit/Observer/UpgradeCustomerPasswordObserverTest.php b/app/code/Magento/Customer/Test/Unit/Observer/UpgradeCustomerPasswordObserverTest.php -index 8971f155f78..188bbde71c1 100644 ---- a/app/code/Magento/Customer/Test/Unit/Observer/UpgradeCustomerPasswordObserverTest.php -+++ b/app/code/Magento/Customer/Test/Unit/Observer/UpgradeCustomerPasswordObserverTest.php -@@ -7,6 +7,9 @@ namespace Magento\Customer\Test\Unit\Observer; - - use Magento\Customer\Observer\UpgradeCustomerPasswordObserver; - -+/** -+ * Class UpgradeCustomerPasswordObserverTest for testing upgrade password observer -+ */ - class UpgradeCustomerPasswordObserverTest extends \PHPUnit\Framework\TestCase - { - /** -@@ -29,9 +32,13 @@ class UpgradeCustomerPasswordObserverTest extends \PHPUnit\Framework\TestCase - */ - protected $customerRegistry; - -+ /** -+ * @inheritdoc -+ */ - protected function setUp() - { -- $this->customerRepository = $this->getMockBuilder(\Magento\Customer\Api\CustomerRepositoryInterface::class) -+ $this->customerRepository = $this -+ ->getMockBuilder(\Magento\Customer\Api\CustomerRepositoryInterface::class) - ->getMockForAbstractClass(); - $this->customerRegistry = $this->getMockBuilder(\Magento\Customer\Model\CustomerRegistry::class) - ->disableOriginalConstructor() -@@ -47,6 +54,9 @@ class UpgradeCustomerPasswordObserverTest extends \PHPUnit\Framework\TestCase - ); - } - -+ /** -+ * Unit test for verifying customers password upgrade observer -+ */ - public function testUpgradeCustomerPassword() - { - $customerId = '1'; -@@ -57,6 +67,8 @@ class UpgradeCustomerPasswordObserverTest extends \PHPUnit\Framework\TestCase - ->setMethods(['getId']) - ->getMock(); - $customer = $this->getMockBuilder(\Magento\Customer\Api\Data\CustomerInterface::class) -+ ->setMethods(['setData']) -+ ->disableOriginalConstructor() - ->getMockForAbstractClass(); - $customerSecure = $this->getMockBuilder(\Magento\Customer\Model\Data\CustomerSecure::class) - ->disableOriginalConstructor() -diff --git a/app/code/Magento/Customer/Test/Unit/Ui/Component/DataProvider/DocumentTest.php b/app/code/Magento/Customer/Test/Unit/Ui/Component/DataProvider/DocumentTest.php -index 1d7905cca79..a9c6de72acb 100644 ---- a/app/code/Magento/Customer/Test/Unit/Ui/Component/DataProvider/DocumentTest.php -+++ b/app/code/Magento/Customer/Test/Unit/Ui/Component/DataProvider/DocumentTest.php -@@ -175,7 +175,7 @@ class DocumentTest extends \PHPUnit\Framework\TestCase - $this->document->setData('original_website_id', $websiteId); - - $this->scopeConfig->expects(static::once()) -- ->method('getValue') -+ ->method('isSetFlag') - ->with() - ->willReturn(true); - -diff --git a/app/code/Magento/Customer/Test/Unit/Ui/Component/Form/AddressFieldsetTest.php b/app/code/Magento/Customer/Test/Unit/Ui/Component/Form/AddressFieldsetTest.php -new file mode 100644 -index 00000000000..65a0443aed8 ---- /dev/null -+++ b/app/code/Magento/Customer/Test/Unit/Ui/Component/Form/AddressFieldsetTest.php -@@ -0,0 +1,69 @@ -+<?php -+declare(strict_types=1); -+/** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+namespace Magento\Customer\Test\Unit\Ui\Component\Form; -+ -+use Magento\Customer\Ui\Component\Form\AddressFieldset; -+use Magento\Framework\View\Element\UiComponent\ContextInterface; -+ -+/** -+ * Test for class \Magento\Customer\Ui\Component\Form\AddressFieldset -+ */ -+class AddressFieldsetTest extends \PHPUnit\Framework\TestCase -+{ -+ /** -+ * @var AddressFieldset -+ */ -+ protected $fieldset; -+ -+ /** -+ * @var ContextInterface|\PHPUnit_Framework_MockObject_MockObject -+ */ -+ private $context; -+ -+ /** -+ * Set up -+ * -+ * @return void -+ */ -+ protected function setUp() -+ { -+ $this->context = $this->getMockForAbstractClass( -+ \Magento\Framework\View\Element\UiComponent\ContextInterface::class -+ ); -+ $this->fieldset = new AddressFieldset( -+ $this->context, -+ [], -+ [] -+ ); -+ } -+ -+ /** -+ * Run test for canShow() method -+ * -+ * @return void -+ * -+ */ -+ public function testCanShow() -+ { -+ $this->context->expects($this->atLeastOnce())->method('getRequestParam')->with('id') -+ ->willReturn(1); -+ $this->assertTrue($this->fieldset->isComponentVisible()); -+ } -+ -+ /** -+ * Run test for canShow() method without customer id in context -+ * -+ * @return void -+ * -+ */ -+ public function testCanShowWithoutId() -+ { -+ $this->context->expects($this->atLeastOnce())->method('getRequestParam')->with('id') -+ ->willReturn(null); -+ $this->assertEquals(false, $this->fieldset->isComponentVisible()); -+ } -+} -diff --git a/app/code/Magento/Customer/Test/Unit/Ui/Component/Listing/Column/GroupActionsTest.php b/app/code/Magento/Customer/Test/Unit/Ui/Component/Listing/Column/GroupActionsTest.php -deleted file mode 100644 -index fdd841ea88c..00000000000 ---- a/app/code/Magento/Customer/Test/Unit/Ui/Component/Listing/Column/GroupActionsTest.php -+++ /dev/null -@@ -1,141 +0,0 @@ --<?php --/** -- * Copyright © Magento, Inc. All rights reserved. -- * See COPYING.txt for license details. -- */ -- --declare(strict_types=1); -- --namespace Magento\Customer\Test\Unit\Ui\Component\Listing\Column; -- --use Magento\Customer\Ui\Component\Listing\Column\GroupActions; --use Magento\Framework\Escaper; --use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; --use Magento\Framework\UrlInterface; --use Magento\Framework\View\Element\UiComponent\ContextInterface; --use Magento\Framework\View\Element\UiComponent\Processor; --use PHPUnit_Framework_MockObject_MockObject as MockObject; -- --/** -- * GroupActionsTest contains unit tests for \Magento\Customer\Ui\Component\Listing\Column\GroupActions class -- */ --class GroupActionsTest extends \PHPUnit\Framework\TestCase --{ -- /** -- * @var GroupActions -- */ -- protected $groupActions; -- -- /** -- * @var Escaper|MockObject -- */ -- protected $escaper; -- -- /** -- * @var UrlInterface|MockObject -- */ -- protected $urlBuilder; -- -- /** -- * SetUp method -- * -- * @return void -- */ -- protected function setUp() -- { -- $objectManager = new ObjectManager($this); -- -- $context = $this->createMock(ContextInterface::class); -- -- $processor = $this->getMockBuilder(Processor::class) -- ->disableOriginalConstructor() -- ->getMock(); -- $context->expects(static::never()) -- ->method('getProcessor') -- ->willReturn($processor); -- -- $this->urlBuilder = $this->createMock(UrlInterface::class); -- -- $this->escaper = $this->getMockBuilder(Escaper::class) -- ->disableOriginalConstructor() -- ->setMethods(['escapeHtml']) -- ->getMock(); -- -- $this->groupActions = $objectManager->getObject(GroupActions::class, [ -- 'context' => $context, -- 'urlBuilder' => $this->urlBuilder, -- 'escaper' => $this->escaper, -- ]); -- } -- -- /** -- * @covers \Magento\Customer\Ui\Component\Listing\Column\GroupActions::prepareDataSource -- */ -- public function testPrepareDataSource() -- { -- $groupId = 1; -- $groupCode = 'group code'; -- $items = [ -- 'data' => [ -- 'items' => [ -- [ -- 'customer_group_id' => $groupId, -- 'customer_group_code' => $groupCode -- ] -- ] -- ] -- ]; -- $name = 'item_name'; -- $expectedItems = [ -- [ -- 'customer_group_id' => $groupId, -- 'customer_group_code' => $groupCode, -- $name => [ -- 'edit' => [ -- 'href' => 'test/url/edit', -- 'label' => __('Edit'), -- ], -- 'delete' => [ -- 'href' => 'test/url/delete', -- 'label' => __('Delete'), -- 'confirm' => [ -- 'title' => __('Delete %1', $groupCode), -- 'message' => __('Are you sure you want to delete a %1 record?', $groupCode) -- ], -- ] -- ], -- ] -- ]; -- -- $this->escaper->expects(static::once()) -- ->method('escapeHtml') -- ->with($groupCode) -- ->willReturn($groupCode); -- -- $this->urlBuilder->expects(static::exactly(2)) -- ->method('getUrl') -- ->willReturnMap( -- [ -- [ -- GroupActions::URL_PATH_EDIT, -- [ -- 'id' => $groupId -- ], -- 'test/url/edit', -- ], -- [ -- GroupActions::URL_PATH_DELETE, -- [ -- 'id' => $groupId -- ], -- 'test/url/delete', -- ], -- ] -- ); -- -- $this->groupActions->setData('name', $name); -- -- $actual = $this->groupActions->prepareDataSource($items); -- static::assertEquals($expectedItems, $actual['data']['items']); -- } --} -diff --git a/app/code/Magento/Customer/Test/Unit/Ui/Component/Listing/Column/ValidationRulesTest.php b/app/code/Magento/Customer/Test/Unit/Ui/Component/Listing/Column/ValidationRulesTest.php -index 130b3acd11e..07b0a760432 100644 ---- a/app/code/Magento/Customer/Test/Unit/Ui/Component/Listing/Column/ValidationRulesTest.php -+++ b/app/code/Magento/Customer/Test/Unit/Ui/Component/Listing/Column/ValidationRulesTest.php -@@ -18,12 +18,6 @@ class ValidationRulesTest extends \PHPUnit\Framework\TestCase - - protected function setUp() - { -- $this->validationRules = $this->getMockBuilder( -- \Magento\Customer\Ui\Component\Listing\Column\ValidationRules::class -- ) -- ->disableOriginalConstructor() -- ->getMock(); -- - $this->validationRule = $this->getMockBuilder(\Magento\Customer\Api\Data\ValidationRuleInterface::class) - ->disableOriginalConstructor() - ->getMock(); -@@ -31,20 +25,26 @@ class ValidationRulesTest extends \PHPUnit\Framework\TestCase - $this->validationRules = new ValidationRules(); - } - -- public function testGetValidationRules() -+ /** -+ * Tests input validation rules -+ * -+ * @param String $validationRule - provided input validation rules -+ * @param String $validationClass - expected input validation class -+ * @dataProvider validationRulesDataProvider -+ */ -+ public function testGetValidationRules(String $validationRule, String $validationClass): void - { - $expectsRules = [ - 'required-entry' => true, -- 'validate-number' => true, -+ $validationClass => true, - ]; -- $this->validationRule->expects($this->atLeastOnce()) -- ->method('getName') -+ $this->validationRule->method('getName') - ->willReturn('input_validation'); -- $this->validationRule->expects($this->atLeastOnce()) -- ->method('getValue') -- ->willReturn('numeric'); - -- $this->assertEquals( -+ $this->validationRule->method('getValue') -+ ->willReturn($validationRule); -+ -+ self::assertEquals( - $expectsRules, - $this->validationRules->getValidationRules( - true, -@@ -56,6 +56,23 @@ class ValidationRulesTest extends \PHPUnit\Framework\TestCase - ); - } - -+ /** -+ * Provides possible validation rules. -+ * -+ * @return array -+ */ -+ public function validationRulesDataProvider(): array -+ { -+ return [ -+ ['alpha', 'validate-alpha'], -+ ['numeric', 'validate-number'], -+ ['alphanumeric', 'validate-alphanum'], -+ ['alphanum-with-spaces', 'validate-alphanum-with-spaces'], -+ ['url', 'validate-url'], -+ ['email', 'validate-email'] -+ ]; -+ } -+ - public function testGetValidationRulesWithOnlyRequiredRule() - { - $expectsRules = [ -diff --git a/app/code/Magento/Customer/Ui/Component/DataProvider/Document.php b/app/code/Magento/Customer/Ui/Component/DataProvider/Document.php -index a9a5c5b1774..468a9e7946f 100644 ---- a/app/code/Magento/Customer/Ui/Component/DataProvider/Document.php -+++ b/app/code/Magento/Customer/Ui/Component/DataProvider/Document.php -@@ -72,6 +72,7 @@ class Document extends \Magento\Framework\View\Element\UiComponent\DataProvider\ - - /** - * Document constructor. -+ * - * @param AttributeValueFactory $attributeValueFactory - * @param GroupRepositoryInterface $groupRepository - * @param CustomerMetadataInterface $customerMetadata -@@ -118,9 +119,10 @@ class Document extends \Magento\Framework\View\Element\UiComponent\DataProvider\ - } - - /** -- * Update customer gender value -- * Method set gender label instead of id value -+ * Update customer gender value. Method set gender label instead of id value -+ * - * @return void -+ * @throws \Magento\Framework\Exception\LocalizedException - */ - private function setGenderValue() - { -@@ -141,9 +143,10 @@ class Document extends \Magento\Framework\View\Element\UiComponent\DataProvider\ - } - - /** -- * Update customer group value -- * Method set group code instead id value -+ * Update customer group value. Method set group code instead id value -+ * - * @return void -+ * @throws \Magento\Framework\Exception\LocalizedException - */ - private function setCustomerGroupValue() - { -@@ -157,8 +160,8 @@ class Document extends \Magento\Framework\View\Element\UiComponent\DataProvider\ - } - - /** -- * Update website value -- * Method set website name instead id value -+ * Update website value. Method set website name instead id value -+ * - * @return void - */ - private function setWebsiteValue() -@@ -170,22 +173,22 @@ class Document extends \Magento\Framework\View\Element\UiComponent\DataProvider\ - } - - /** -- * Update confirmation value -- * Method set confirmation text value to match what is shown in grid -+ * Update confirmation value. Method set confirmation text value to match what is shown in grid -+ * - * @return void - */ - private function setConfirmationValue() - { - $value = $this->getData(self::$confirmationAttributeCode); - $websiteId = $this->getData(self::$websiteIdAttributeCode) ?: $this->getData(self::$websiteAttributeCode); -- $isConfirmationRequired = (bool)$this->scopeConfig->getValue( -+ $isConfirmRequired = $this->scopeConfig->isSetFlag( - AccountManagement::XML_PATH_IS_CONFIRM, - ScopeInterface::SCOPE_WEBSITES, - $websiteId - ); - - $valueText = __('Confirmation Not Required'); -- if ($isConfirmationRequired) { -+ if ($isConfirmRequired) { - $valueText = $value === null ? __('Confirmed') : __('Confirmation Required'); - } - -@@ -193,8 +196,8 @@ class Document extends \Magento\Framework\View\Element\UiComponent\DataProvider\ - } - - /** -- * Update lock expires value -- * Method set account lock text value to match what is shown in grid -+ * Update lock expires value. Method set account lock text value to match what is shown in grid -+ * - * @return void - */ - private function setAccountLockValue() -diff --git a/app/code/Magento/Customer/Ui/Component/Form/AddressFieldset.php b/app/code/Magento/Customer/Ui/Component/Form/AddressFieldset.php -new file mode 100644 -index 00000000000..3e3a6e74166 ---- /dev/null -+++ b/app/code/Magento/Customer/Ui/Component/Form/AddressFieldset.php -@@ -0,0 +1,46 @@ -+<?php -+/** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+declare(strict_types=1); -+ -+namespace Magento\Customer\Ui\Component\Form; -+ -+use Magento\Framework\View\Element\UiComponent\ContextInterface; -+use Magento\Framework\View\Element\ComponentVisibilityInterface; -+ -+/** -+ * Customer addresses fieldset class -+ */ -+class AddressFieldset extends \Magento\Ui\Component\Form\Fieldset implements ComponentVisibilityInterface -+{ -+ /** -+ * @param ContextInterface $context -+ * @param array $components -+ * @param array $data -+ */ -+ public function __construct( -+ ContextInterface $context, -+ array $components = [], -+ array $data = [] -+ ) { -+ $this->context = $context; -+ -+ parent::__construct($context, $components, $data); -+ } -+ -+ /** -+ * Can show customer addresses tab in tabs or not -+ * -+ * Will return false for not registered customer in a case when admin user created new customer account. -+ * Needed to hide addresses tab from create new customer page -+ * -+ * @return boolean -+ */ -+ public function isComponentVisible(): bool -+ { -+ $customerId = $this->context->getRequestParam('id'); -+ return (bool)$customerId; -+ } -+} -diff --git a/app/code/Magento/Customer/Ui/Component/Listing/Address/Column/Actions.php b/app/code/Magento/Customer/Ui/Component/Listing/Address/Column/Actions.php -new file mode 100644 -index 00000000000..6d1d7472a79 ---- /dev/null -+++ b/app/code/Magento/Customer/Ui/Component/Listing/Address/Column/Actions.php -@@ -0,0 +1,128 @@ -+<?php -+declare(strict_types=1); -+/** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+namespace Magento\Customer\Ui\Component\Listing\Address\Column; -+ -+use Magento\Framework\View\Element\UiComponent\ContextInterface; -+use Magento\Framework\View\Element\UiComponentFactory; -+use Magento\Ui\Component\Listing\Columns\Column; -+use Magento\Framework\UrlInterface; -+ -+/** -+ * Prepare actions column for customer addresses grid -+ */ -+class Actions extends Column -+{ -+ const CUSTOMER_ADDRESS_PATH_DELETE = 'customer/address/delete'; -+ const CUSTOMER_ADDRESS_PATH_DEFAULT_SHIPPING = 'customer/address/defaultShippingAddress'; -+ const CUSTOMER_ADDRESS_PATH_DEFAULT_BILLING = 'customer/address/defaultBillingAddress'; -+ -+ /** -+ * @var UrlInterface -+ */ -+ private $urlBuilder; -+ -+ /** -+ * @param ContextInterface $context -+ * @param UiComponentFactory $uiComponentFactory -+ * @param UrlInterface $urlBuilder -+ * @param array $components -+ * @param array $data -+ */ -+ public function __construct( -+ ContextInterface $context, -+ UiComponentFactory $uiComponentFactory, -+ UrlInterface $urlBuilder, -+ array $components = [], -+ array $data = [] -+ ) { -+ $this->urlBuilder = $urlBuilder; -+ parent::__construct($context, $uiComponentFactory, $components, $data); -+ } -+ -+ /** -+ * Prepare Data Source -+ * -+ * @param array $dataSource -+ * @return array -+ */ -+ public function prepareDataSource(array $dataSource): array -+ { -+ if (isset($dataSource['data']['items'])) { -+ foreach ($dataSource['data']['items'] as &$item) { -+ $name = $this->getData('name'); -+ if (isset($item['entity_id'])) { -+ $item[$name]['edit'] = [ -+ 'callback' => [ -+ [ -+ 'provider' => 'customer_form.areas.address.address' -+ . '.customer_address_update_modal.update_customer_address_form_loader', -+ 'target' => 'destroyInserted', -+ ], -+ [ -+ 'provider' => 'customer_form.areas.address.address' -+ . '.customer_address_update_modal', -+ 'target' => 'openModal', -+ ], -+ [ -+ 'provider' => 'customer_form.areas.address.address' -+ . '.customer_address_update_modal.update_customer_address_form_loader', -+ 'target' => 'render', -+ 'params' => [ -+ 'entity_id' => $item['entity_id'], -+ ], -+ ] -+ ], -+ 'href' => '#', -+ 'label' => __('Edit'), -+ 'hidden' => false, -+ ]; -+ -+ $item[$name]['setDefaultBilling'] = [ -+ 'href' => $this->urlBuilder->getUrl( -+ self::CUSTOMER_ADDRESS_PATH_DEFAULT_BILLING, -+ ['parent_id' => $item['parent_id'], 'id' => $item['entity_id']] -+ ), -+ 'label' => __('Set as default billing'), -+ 'isAjax' => true, -+ 'confirm' => [ -+ 'title' => __('Set address as default billing'), -+ 'message' => __('Are you sure you want to set the address as default billing address?') -+ ] -+ ]; -+ -+ $item[$name]['setDefaultShipping'] = [ -+ 'href' => $this->urlBuilder->getUrl( -+ self::CUSTOMER_ADDRESS_PATH_DEFAULT_SHIPPING, -+ ['parent_id' => $item['parent_id'], 'id' => $item['entity_id']] -+ ), -+ 'label' => __('Set as default shipping'), -+ 'isAjax' => true, -+ 'confirm' => [ -+ 'title' => __('Set address as default shipping'), -+ 'message' => __('Are you sure you want to set the address as default shipping address?') -+ ] -+ ]; -+ -+ $item[$name]['delete'] = [ -+ 'href' => $this->urlBuilder->getUrl( -+ self::CUSTOMER_ADDRESS_PATH_DELETE, -+ ['parent_id' => $item['parent_id'], 'id' => $item['entity_id']] -+ ), -+ 'label' => __('Delete'), -+ 'isAjax' => true, -+ 'confirm' => [ -+ 'title' => __('Delete address'), -+ 'message' => __('Are you sure you want to delete the address?') -+ ] -+ ]; -+ } -+ } -+ } -+ -+ return $dataSource; -+ } -+} -diff --git a/app/code/Magento/Customer/Ui/Component/Listing/Address/Column/Countries.php b/app/code/Magento/Customer/Ui/Component/Listing/Address/Column/Countries.php -new file mode 100644 -index 00000000000..d05d5d1c592 ---- /dev/null -+++ b/app/code/Magento/Customer/Ui/Component/Listing/Address/Column/Countries.php -@@ -0,0 +1,41 @@ -+<?php -+declare(strict_types=1); -+/** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+namespace Magento\Customer\Ui\Component\Listing\Address\Column; -+ -+use Magento\Framework\Data\OptionSourceInterface; -+ -+/** -+ * Class for process countries in customer addresses grid -+ */ -+class Countries implements OptionSourceInterface -+{ -+ /** -+ * @var \Magento\Directory\Model\ResourceModel\Country\CollectionFactory -+ */ -+ private $countryCollectionFactory; -+ -+ /** -+ * @param \Magento\Directory\Model\ResourceModel\Country\CollectionFactory $collectionFactory -+ */ -+ public function __construct( -+ \Magento\Directory\Model\ResourceModel\Country\CollectionFactory $collectionFactory -+ ) { -+ $this->countryCollectionFactory = $collectionFactory; -+ } -+ -+ /** -+ * Get list of countries with country id as value and code as label -+ * -+ * @return array -+ */ -+ public function toOptionArray(): array -+ { -+ /** @var \Magento\Directory\Model\ResourceModel\Country\Collection $countryCollection */ -+ $countryCollection = $this->countryCollectionFactory->create(); -+ return $countryCollection->toOptionArray(); -+ } -+} -diff --git a/app/code/Magento/Customer/Ui/Component/Listing/Address/DataProvider.php b/app/code/Magento/Customer/Ui/Component/Listing/Address/DataProvider.php -new file mode 100644 -index 00000000000..c70e25ee99e ---- /dev/null -+++ b/app/code/Magento/Customer/Ui/Component/Listing/Address/DataProvider.php -@@ -0,0 +1,113 @@ -+<?php -+/** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+namespace Magento\Customer\Ui\Component\Listing\Address; -+ -+use Magento\Customer\Model\ResourceModel\Address\Grid\CollectionFactory; -+use Magento\Directory\Model\CountryFactory; -+use Magento\Framework\Api\Filter; -+ -+/** -+ * Custom DataProvider for customer addresses listing -+ */ -+class DataProvider extends \Magento\Ui\DataProvider\AbstractDataProvider -+{ -+ /** -+ * @var \Magento\Framework\App\RequestInterface $request, -+ */ -+ private $request; -+ -+ /** -+ * @var CountryFactory -+ */ -+ private $countryDirectory; -+ -+ /** -+ * @param string $name -+ * @param string $primaryFieldName -+ * @param string $requestFieldName -+ * @param CollectionFactory $collectionFactory -+ * @param \Magento\Framework\App\RequestInterface $request -+ * @param CountryFactory $countryFactory -+ * @param array $meta -+ * @param array $data -+ */ -+ public function __construct( -+ $name, -+ $primaryFieldName, -+ $requestFieldName, -+ CollectionFactory $collectionFactory, -+ \Magento\Framework\App\RequestInterface $request, -+ CountryFactory $countryFactory, -+ array $meta = [], -+ array $data = [] -+ ) { -+ parent::__construct($name, $primaryFieldName, $requestFieldName, $meta, $data); -+ $this->collection = $collectionFactory->create(); -+ $this->countryDirectory = $countryFactory->create(); -+ $this->request = $request; -+ } -+ -+ /** -+ * Add country key for default billing/shipping blocks on customer addresses tab -+ * -+ * @return array -+ */ -+ public function getData(): array -+ { -+ $collection = $this->getCollection(); -+ $data['items'] = []; -+ if ($this->request->getParam('parent_id')) { -+ $collection->addFieldToFilter('parent_id', $this->request->getParam('parent_id')); -+ $data = $collection->toArray(); -+ } -+ foreach ($data['items'] as $key => $item) { -+ if (isset($item['country_id']) && !isset($item['country'])) { -+ $data['items'][$key]['country'] = $this->countryDirectory->loadByCode($item['country_id'])->getName(); -+ } -+ } -+ -+ return $data; -+ } -+ -+ /** -+ * Add full text search filter to collection -+ * -+ * @param Filter $filter -+ * @return void -+ */ -+ public function addFilter(Filter $filter): void -+ { -+ if ($filter->getField() !== 'fulltext') { -+ $this->collection->addFieldToFilter( -+ $filter->getField(), -+ [$filter->getConditionType() => $filter->getValue()] -+ ); -+ } else { -+ $value = trim($filter->getValue()); -+ $this->collection->addFieldToFilter( -+ [ -+ ['attribute' => 'firstname'], -+ ['attribute' => 'lastname'], -+ ['attribute' => 'street'], -+ ['attribute' => 'city'], -+ ['attribute' => 'region'], -+ ['attribute' => 'postcode'], -+ ['attribute' => 'telephone'] -+ ], -+ [ -+ ['like' => "%{$value}%"], -+ ['like' => "%{$value}%"], -+ ['like' => "%{$value}%"], -+ ['like' => "%{$value}%"], -+ ['like' => "%{$value}%"], -+ ['like' => "%{$value}%"], -+ ['like' => "%{$value}%"], -+ ['like' => "%{$value}%"], -+ ] -+ ); -+ } -+ } -+} -diff --git a/app/code/Magento/Customer/Ui/Component/Listing/Column/GroupActions.php b/app/code/Magento/Customer/Ui/Component/Listing/Column/GroupActions.php -index a8a3429ebad..00c5f99fab4 100644 ---- a/app/code/Magento/Customer/Ui/Component/Listing/Column/GroupActions.php -+++ b/app/code/Magento/Customer/Ui/Component/Listing/Column/GroupActions.php -@@ -98,7 +98,8 @@ class GroupActions extends Column - 'confirm' => [ - 'title' => __('Delete %1', $title), - 'message' => __('Are you sure you want to delete a %1 record?', $title) -- ] -+ ], -+ 'post' => true - ]; - } - } -diff --git a/app/code/Magento/Customer/Ui/Component/Listing/Column/ValidationRules.php b/app/code/Magento/Customer/Ui/Component/Listing/Column/ValidationRules.php -index b8f83421a6d..6befec8e942 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 @@ namespace Magento\Customer\Ui\Component\Listing\Column; - - use Magento\Customer\Api\Data\ValidationRuleInterface; - -+/** -+ * Provides validation classes according to corresponding rules. -+ */ - class ValidationRules - { - /** -@@ -16,6 +19,7 @@ class ValidationRules - 'alpha' => 'validate-alpha', - 'numeric' => 'validate-number', - 'alphanumeric' => 'validate-alphanum', -+ 'alphanum-with-spaces' => 'validate-alphanum-with-spaces', - 'url' => 'validate-url', - 'email' => 'validate-email', - ]; -diff --git a/app/code/Magento/Customer/etc/acl.xml b/app/code/Magento/Customer/etc/acl.xml -index e8e6219bef4..1583c190d5c 100644 ---- a/app/code/Magento/Customer/etc/acl.xml -+++ b/app/code/Magento/Customer/etc/acl.xml -@@ -10,7 +10,13 @@ - <resources> - <resource id="Magento_Backend::admin"> - <resource id="Magento_Customer::customer" title="Customers" translate="title" sortOrder="40"> -- <resource id="Magento_Customer::manage" title="All Customers" translate="title" sortOrder="10" /> -+ <resource id="Magento_Customer::manage" title="All Customers" translate="title" sortOrder="10"> -+ <resource id="Magento_Customer::actions" title="Actions" translate="title" sortOrder="10"> -+ <resource id="Magento_Customer::delete" title="Delete" translate="title" sortOrder="10" /> -+ <resource id="Magento_Customer::reset_password" title="Reset password" translate="title" sortOrder="20" /> -+ <resource id="Magento_Customer::invalidate_tokens" title="Invalidate tokens" translate="title" sortOrder="30" /> -+ </resource> -+ </resource> - <resource id="Magento_Customer::online" title="Now Online" translate="title" sortOrder="20" /> - <resource id="Magento_Customer::group" title="Customer Groups" translate="title" sortOrder="30" /> - </resource> -@@ -20,7 +26,7 @@ - <resource id="Magento_Customer::config_customer" title="Customers Section" translate="title" sortOrder="50" /> - </resource> - </resource> -- </resource> -+ </resource> - </resource> - </resources> - </acl> -diff --git a/app/code/Magento/Customer/etc/adminhtml/system.xml b/app/code/Magento/Customer/etc/adminhtml/system.xml -index 31e968de14d..86e5852d67a 100644 ---- a/app/code/Magento/Customer/etc/adminhtml/system.xml -+++ b/app/code/Magento/Customer/etc/adminhtml/system.xml -@@ -26,7 +26,7 @@ - </group> - <group id="create_account" translate="label" sortOrder="20" showInDefault="1" showInWebsite="1" showInStore="1"> - <label>Create New Account Options</label> -- <field id="auto_group_assign" translate="label comment" type="select" sortOrder="10" showInDefault="1" showInWebsite="1" showInStore="1"> -+ <field id="auto_group_assign" translate="label" type="select" sortOrder="10" showInDefault="1" showInWebsite="1" showInStore="1"> - <label>Enable Automatic Assignment to Customer Group</label> - <source_model>Magento\Config\Model\Config\Source\Yesno</source_model> - </field> -@@ -170,7 +170,7 @@ - <comment>Use 0 to disable account locking.</comment> - <frontend_class>required-entry validate-digits</frontend_class> - </field> -- <field id="lockout_threshold" translate="label" sortOrder="80" showInDefault="1" showInWebsite="0" showInStore="0" canRestore="1"> -+ <field id="lockout_threshold" translate="label comment" sortOrder="80" showInDefault="1" showInWebsite="0" showInStore="0" canRestore="1"> - <label>Lockout Time (minutes)</label> - <comment>Account will be unlocked after provided time.</comment> - <frontend_class>required-entry validate-digits</frontend_class> -@@ -272,16 +272,16 @@ - </group> - <group id="address_templates" translate="label" type="text" sortOrder="100" showInDefault="1" showInWebsite="1" showInStore="1"> - <label>Address Templates</label> -- <field id="text" type="textarea" sortOrder="1" showInDefault="1" showInWebsite="1" showInStore="1" canRestore="1"> -+ <field id="text" translate="label" type="textarea" sortOrder="1" showInDefault="1" showInWebsite="1" showInStore="1" canRestore="1"> - <label>Text</label> - </field> -- <field id="oneline" type="textarea" sortOrder="2" showInDefault="1" showInWebsite="1" showInStore="1" canRestore="1"> -+ <field id="oneline" translate="label" type="textarea" sortOrder="2" showInDefault="1" showInWebsite="1" showInStore="1" canRestore="1"> - <label>Text One Line</label> - </field> -- <field id="html" type="textarea" sortOrder="3" showInDefault="1" showInWebsite="1" showInStore="1" canRestore="1"> -+ <field id="html" translate="label" type="textarea" sortOrder="3" showInDefault="1" showInWebsite="1" showInStore="1" canRestore="1"> - <label>HTML</label> - </field> -- <field id="pdf" type="textarea" sortOrder="4" showInDefault="1" showInWebsite="1" showInStore="1" canRestore="1"> -+ <field id="pdf" translate="label" type="textarea" sortOrder="4" showInDefault="1" showInWebsite="1" showInStore="1" canRestore="1"> - <label>PDF</label> - </field> - </group> -diff --git a/app/code/Magento/Customer/etc/db_schema.xml b/app/code/Magento/Customer/etc/db_schema.xml -index 368ca417432..c699db06d30 100644 ---- a/app/code/Magento/Customer/etc/db_schema.xml -+++ b/app/code/Magento/Customer/etc/db_schema.xml -@@ -9,15 +9,15 @@ - xsi:noNamespaceSchemaLocation="urn:magento:framework:Setup/Declaration/Schema/etc/schema.xsd"> - <table name="customer_entity" resource="default" engine="innodb" comment="Customer Entity"> - <column xsi:type="int" name="entity_id" padding="10" unsigned="true" nullable="false" identity="true" -- comment="Entity Id"/> -+ comment="Entity ID"/> - <column xsi:type="smallint" name="website_id" padding="5" unsigned="true" nullable="true" identity="false" -- comment="Website Id"/> -+ comment="Website ID"/> - <column xsi:type="varchar" name="email" nullable="true" length="255" comment="Email"/> - <column xsi:type="smallint" name="group_id" padding="5" unsigned="true" nullable="false" identity="false" -- default="0" comment="Group Id"/> -+ default="0" comment="Group ID"/> - <column xsi:type="varchar" name="increment_id" nullable="true" length="50" comment="Increment Id"/> - <column xsi:type="smallint" name="store_id" padding="5" unsigned="true" nullable="true" identity="false" -- default="0" comment="Store Id"/> -+ default="0" comment="Store ID"/> - <column xsi:type="timestamp" name="created_at" on_update="false" nullable="false" default="CURRENT_TIMESTAMP" - comment="Created At"/> - <column xsi:type="timestamp" name="updated_at" on_update="true" nullable="false" default="CURRENT_TIMESTAMP" -@@ -50,37 +50,37 @@ - <column xsi:type="timestamp" name="first_failure" on_update="false" nullable="true" comment="First Failure"/> - <column xsi:type="timestamp" name="lock_expires" on_update="false" nullable="true" - comment="Lock Expiration Date"/> -- <constraint xsi:type="primary" name="PRIMARY"> -+ <constraint xsi:type="primary" referenceId="PRIMARY"> - <column name="entity_id"/> - </constraint> -- <constraint xsi:type="foreign" name="CUSTOMER_ENTITY_STORE_ID_STORE_STORE_ID" table="customer_entity" -+ <constraint xsi:type="foreign" referenceId="CUSTOMER_ENTITY_STORE_ID_STORE_STORE_ID" table="customer_entity" - column="store_id" referenceTable="store" referenceColumn="store_id" onDelete="SET NULL"/> -- <constraint xsi:type="foreign" name="CUSTOMER_ENTITY_WEBSITE_ID_STORE_WEBSITE_WEBSITE_ID" -+ <constraint xsi:type="foreign" referenceId="CUSTOMER_ENTITY_WEBSITE_ID_STORE_WEBSITE_WEBSITE_ID" - table="customer_entity" column="website_id" referenceTable="store_website" - referenceColumn="website_id" onDelete="SET NULL"/> -- <constraint xsi:type="unique" name="CUSTOMER_ENTITY_EMAIL_WEBSITE_ID"> -+ <constraint xsi:type="unique" referenceId="CUSTOMER_ENTITY_EMAIL_WEBSITE_ID"> - <column name="email"/> - <column name="website_id"/> - </constraint> -- <index name="CUSTOMER_ENTITY_STORE_ID" indexType="btree"> -+ <index referenceId="CUSTOMER_ENTITY_STORE_ID" indexType="btree"> - <column name="store_id"/> - </index> -- <index name="CUSTOMER_ENTITY_WEBSITE_ID" indexType="btree"> -+ <index referenceId="CUSTOMER_ENTITY_WEBSITE_ID" indexType="btree"> - <column name="website_id"/> - </index> -- <index name="CUSTOMER_ENTITY_FIRSTNAME" indexType="btree"> -+ <index referenceId="CUSTOMER_ENTITY_FIRSTNAME" indexType="btree"> - <column name="firstname"/> - </index> -- <index name="CUSTOMER_ENTITY_LASTNAME" indexType="btree"> -+ <index referenceId="CUSTOMER_ENTITY_LASTNAME" indexType="btree"> - <column name="lastname"/> - </index> - </table> - <table name="customer_address_entity" resource="default" engine="innodb" comment="Customer Address Entity"> - <column xsi:type="int" name="entity_id" padding="10" unsigned="true" nullable="false" identity="true" -- comment="Entity Id"/> -+ comment="Entity ID"/> - <column xsi:type="varchar" name="increment_id" nullable="true" length="50" comment="Increment Id"/> - <column xsi:type="int" name="parent_id" padding="10" unsigned="true" nullable="true" identity="false" -- comment="Parent Id"/> -+ comment="Parent ID"/> - <column xsi:type="timestamp" name="created_at" on_update="false" nullable="false" default="CURRENT_TIMESTAMP" - comment="Created At"/> - <column xsi:type="timestamp" name="updated_at" on_update="true" nullable="false" default="CURRENT_TIMESTAMP" -@@ -111,13 +111,13 @@ - comment="VAT number validation request ID"/> - <column xsi:type="int" name="vat_request_success" padding="10" unsigned="true" nullable="true" identity="false" - comment="VAT number validation request success"/> -- <constraint xsi:type="primary" name="PRIMARY"> -+ <constraint xsi:type="primary" referenceId="PRIMARY"> - <column name="entity_id"/> - </constraint> -- <constraint xsi:type="foreign" name="CUSTOMER_ADDRESS_ENTITY_PARENT_ID_CUSTOMER_ENTITY_ENTITY_ID" -+ <constraint xsi:type="foreign" referenceId="CUSTOMER_ADDRESS_ENTITY_PARENT_ID_CUSTOMER_ENTITY_ENTITY_ID" - table="customer_address_entity" column="parent_id" referenceTable="customer_entity" - referenceColumn="entity_id" onDelete="CASCADE"/> -- <index name="CUSTOMER_ADDRESS_ENTITY_PARENT_ID" indexType="btree"> -+ <index referenceId="CUSTOMER_ADDRESS_ENTITY_PARENT_ID" indexType="btree"> - <column name="parent_id"/> - </index> - </table> -@@ -128,25 +128,25 @@ - <column xsi:type="smallint" name="attribute_id" padding="5" unsigned="true" nullable="false" identity="false" - default="0" comment="Attribute Id"/> - <column xsi:type="int" name="entity_id" padding="10" unsigned="true" nullable="false" identity="false" -- default="0" comment="Entity Id"/> -+ default="0" comment="Entity ID"/> - <column xsi:type="datetime" name="value" on_update="false" nullable="true" comment="Value"/> -- <constraint xsi:type="primary" name="PRIMARY"> -+ <constraint xsi:type="primary" referenceId="PRIMARY"> - <column name="value_id"/> - </constraint> -- <constraint xsi:type="foreign" name="CSTR_ADDR_ENTT_DTIME_ATTR_ID_EAV_ATTR_ATTR_ID" -+ <constraint xsi:type="foreign" referenceId="CSTR_ADDR_ENTT_DTIME_ATTR_ID_EAV_ATTR_ATTR_ID" - table="customer_address_entity_datetime" column="attribute_id" referenceTable="eav_attribute" - referenceColumn="attribute_id" onDelete="CASCADE"/> -- <constraint xsi:type="foreign" name="CSTR_ADDR_ENTT_DTIME_ENTT_ID_CSTR_ADDR_ENTT_ENTT_ID" -+ <constraint xsi:type="foreign" referenceId="CSTR_ADDR_ENTT_DTIME_ENTT_ID_CSTR_ADDR_ENTT_ENTT_ID" - table="customer_address_entity_datetime" column="entity_id" referenceTable="customer_address_entity" - referenceColumn="entity_id" onDelete="CASCADE"/> -- <constraint xsi:type="unique" name="CUSTOMER_ADDRESS_ENTITY_DATETIME_ENTITY_ID_ATTRIBUTE_ID"> -+ <constraint xsi:type="unique" referenceId="CUSTOMER_ADDRESS_ENTITY_DATETIME_ENTITY_ID_ATTRIBUTE_ID"> - <column name="entity_id"/> - <column name="attribute_id"/> - </constraint> -- <index name="CUSTOMER_ADDRESS_ENTITY_DATETIME_ATTRIBUTE_ID" indexType="btree"> -+ <index referenceId="CUSTOMER_ADDRESS_ENTITY_DATETIME_ATTRIBUTE_ID" indexType="btree"> - <column name="attribute_id"/> - </index> -- <index name="CUSTOMER_ADDRESS_ENTITY_DATETIME_ENTITY_ID_ATTRIBUTE_ID_VALUE" indexType="btree"> -+ <index referenceId="CUSTOMER_ADDRESS_ENTITY_DATETIME_ENTITY_ID_ATTRIBUTE_ID_VALUE" indexType="btree"> - <column name="entity_id"/> - <column name="attribute_id"/> - <column name="value"/> -@@ -159,26 +159,26 @@ - <column xsi:type="smallint" name="attribute_id" padding="5" unsigned="true" nullable="false" identity="false" - default="0" comment="Attribute Id"/> - <column xsi:type="int" name="entity_id" padding="10" unsigned="true" nullable="false" identity="false" -- default="0" comment="Entity Id"/> -+ default="0" comment="Entity ID"/> - <column xsi:type="decimal" name="value" scale="4" precision="12" unsigned="false" nullable="false" default="0" - comment="Value"/> -- <constraint xsi:type="primary" name="PRIMARY"> -+ <constraint xsi:type="primary" referenceId="PRIMARY"> - <column name="value_id"/> - </constraint> -- <constraint xsi:type="foreign" name="CSTR_ADDR_ENTT_DEC_ATTR_ID_EAV_ATTR_ATTR_ID" -+ <constraint xsi:type="foreign" referenceId="CSTR_ADDR_ENTT_DEC_ATTR_ID_EAV_ATTR_ATTR_ID" - table="customer_address_entity_decimal" column="attribute_id" referenceTable="eav_attribute" - referenceColumn="attribute_id" onDelete="CASCADE"/> -- <constraint xsi:type="foreign" name="CSTR_ADDR_ENTT_DEC_ENTT_ID_CSTR_ADDR_ENTT_ENTT_ID" -+ <constraint xsi:type="foreign" referenceId="CSTR_ADDR_ENTT_DEC_ENTT_ID_CSTR_ADDR_ENTT_ENTT_ID" - table="customer_address_entity_decimal" column="entity_id" referenceTable="customer_address_entity" - referenceColumn="entity_id" onDelete="CASCADE"/> -- <constraint xsi:type="unique" name="CUSTOMER_ADDRESS_ENTITY_DECIMAL_ENTITY_ID_ATTRIBUTE_ID"> -+ <constraint xsi:type="unique" referenceId="CUSTOMER_ADDRESS_ENTITY_DECIMAL_ENTITY_ID_ATTRIBUTE_ID"> - <column name="entity_id"/> - <column name="attribute_id"/> - </constraint> -- <index name="CUSTOMER_ADDRESS_ENTITY_DECIMAL_ATTRIBUTE_ID" indexType="btree"> -+ <index referenceId="CUSTOMER_ADDRESS_ENTITY_DECIMAL_ATTRIBUTE_ID" indexType="btree"> - <column name="attribute_id"/> - </index> -- <index name="CUSTOMER_ADDRESS_ENTITY_DECIMAL_ENTITY_ID_ATTRIBUTE_ID_VALUE" indexType="btree"> -+ <index referenceId="CUSTOMER_ADDRESS_ENTITY_DECIMAL_ENTITY_ID_ATTRIBUTE_ID_VALUE" indexType="btree"> - <column name="entity_id"/> - <column name="attribute_id"/> - <column name="value"/> -@@ -186,30 +186,30 @@ - </table> - <table name="customer_address_entity_int" resource="default" engine="innodb" comment="Customer Address Entity Int"> - <column xsi:type="int" name="value_id" padding="11" unsigned="false" nullable="false" identity="true" -- comment="Value Id"/> -+ comment="Value ID"/> - <column xsi:type="smallint" name="attribute_id" padding="5" unsigned="true" nullable="false" identity="false" -- default="0" comment="Attribute Id"/> -+ default="0" comment="Attribute ID"/> - <column xsi:type="int" name="entity_id" padding="10" unsigned="true" nullable="false" identity="false" -- default="0" comment="Entity Id"/> -+ default="0" comment="Entity ID"/> - <column xsi:type="int" name="value" padding="11" unsigned="false" nullable="false" identity="false" default="0" - comment="Value"/> -- <constraint xsi:type="primary" name="PRIMARY"> -+ <constraint xsi:type="primary" referenceId="PRIMARY"> - <column name="value_id"/> - </constraint> -- <constraint xsi:type="foreign" name="CSTR_ADDR_ENTT_INT_ATTR_ID_EAV_ATTR_ATTR_ID" -+ <constraint xsi:type="foreign" referenceId="CSTR_ADDR_ENTT_INT_ATTR_ID_EAV_ATTR_ATTR_ID" - table="customer_address_entity_int" column="attribute_id" referenceTable="eav_attribute" - referenceColumn="attribute_id" onDelete="CASCADE"/> -- <constraint xsi:type="foreign" name="CSTR_ADDR_ENTT_INT_ENTT_ID_CSTR_ADDR_ENTT_ENTT_ID" -+ <constraint xsi:type="foreign" referenceId="CSTR_ADDR_ENTT_INT_ENTT_ID_CSTR_ADDR_ENTT_ENTT_ID" - table="customer_address_entity_int" column="entity_id" referenceTable="customer_address_entity" - referenceColumn="entity_id" onDelete="CASCADE"/> -- <constraint xsi:type="unique" name="CUSTOMER_ADDRESS_ENTITY_INT_ENTITY_ID_ATTRIBUTE_ID"> -+ <constraint xsi:type="unique" referenceId="CUSTOMER_ADDRESS_ENTITY_INT_ENTITY_ID_ATTRIBUTE_ID"> - <column name="entity_id"/> - <column name="attribute_id"/> - </constraint> -- <index name="CUSTOMER_ADDRESS_ENTITY_INT_ATTRIBUTE_ID" indexType="btree"> -+ <index referenceId="CUSTOMER_ADDRESS_ENTITY_INT_ATTRIBUTE_ID" indexType="btree"> - <column name="attribute_id"/> - </index> -- <index name="CUSTOMER_ADDRESS_ENTITY_INT_ENTITY_ID_ATTRIBUTE_ID_VALUE" indexType="btree"> -+ <index referenceId="CUSTOMER_ADDRESS_ENTITY_INT_ENTITY_ID_ATTRIBUTE_ID_VALUE" indexType="btree"> - <column name="entity_id"/> - <column name="attribute_id"/> - <column name="value"/> -@@ -218,55 +218,55 @@ - <table name="customer_address_entity_text" resource="default" engine="innodb" - comment="Customer Address Entity Text"> - <column xsi:type="int" name="value_id" padding="11" unsigned="false" nullable="false" identity="true" -- comment="Value Id"/> -+ comment="Value ID"/> - <column xsi:type="smallint" name="attribute_id" padding="5" unsigned="true" nullable="false" identity="false" -- default="0" comment="Attribute Id"/> -+ default="0" comment="Attribute ID"/> - <column xsi:type="int" name="entity_id" padding="10" unsigned="true" nullable="false" identity="false" -- default="0" comment="Entity Id"/> -+ default="0" comment="Entity ID"/> - <column xsi:type="text" name="value" nullable="false" comment="Value"/> -- <constraint xsi:type="primary" name="PRIMARY"> -+ <constraint xsi:type="primary" referenceId="PRIMARY"> - <column name="value_id"/> - </constraint> -- <constraint xsi:type="foreign" name="CSTR_ADDR_ENTT_TEXT_ATTR_ID_EAV_ATTR_ATTR_ID" -+ <constraint xsi:type="foreign" referenceId="CSTR_ADDR_ENTT_TEXT_ATTR_ID_EAV_ATTR_ATTR_ID" - table="customer_address_entity_text" column="attribute_id" referenceTable="eav_attribute" - referenceColumn="attribute_id" onDelete="CASCADE"/> -- <constraint xsi:type="foreign" name="CSTR_ADDR_ENTT_TEXT_ENTT_ID_CSTR_ADDR_ENTT_ENTT_ID" -+ <constraint xsi:type="foreign" referenceId="CSTR_ADDR_ENTT_TEXT_ENTT_ID_CSTR_ADDR_ENTT_ENTT_ID" - table="customer_address_entity_text" column="entity_id" referenceTable="customer_address_entity" - referenceColumn="entity_id" onDelete="CASCADE"/> -- <constraint xsi:type="unique" name="CUSTOMER_ADDRESS_ENTITY_TEXT_ENTITY_ID_ATTRIBUTE_ID"> -+ <constraint xsi:type="unique" referenceId="CUSTOMER_ADDRESS_ENTITY_TEXT_ENTITY_ID_ATTRIBUTE_ID"> - <column name="entity_id"/> - <column name="attribute_id"/> - </constraint> -- <index name="CUSTOMER_ADDRESS_ENTITY_TEXT_ATTRIBUTE_ID" indexType="btree"> -+ <index referenceId="CUSTOMER_ADDRESS_ENTITY_TEXT_ATTRIBUTE_ID" indexType="btree"> - <column name="attribute_id"/> - </index> - </table> - <table name="customer_address_entity_varchar" resource="default" engine="innodb" - comment="Customer Address Entity Varchar"> - <column xsi:type="int" name="value_id" padding="11" unsigned="false" nullable="false" identity="true" -- comment="Value Id"/> -+ comment="Value ID"/> - <column xsi:type="smallint" name="attribute_id" padding="5" unsigned="true" nullable="false" identity="false" -- default="0" comment="Attribute Id"/> -+ default="0" comment="Attribute ID"/> - <column xsi:type="int" name="entity_id" padding="10" unsigned="true" nullable="false" identity="false" -- default="0" comment="Entity Id"/> -+ default="0" comment="Entity ID"/> - <column xsi:type="varchar" name="value" nullable="true" length="255" comment="Value"/> -- <constraint xsi:type="primary" name="PRIMARY"> -+ <constraint xsi:type="primary" referenceId="PRIMARY"> - <column name="value_id"/> - </constraint> -- <constraint xsi:type="foreign" name="CSTR_ADDR_ENTT_VCHR_ATTR_ID_EAV_ATTR_ATTR_ID" -+ <constraint xsi:type="foreign" referenceId="CSTR_ADDR_ENTT_VCHR_ATTR_ID_EAV_ATTR_ATTR_ID" - table="customer_address_entity_varchar" column="attribute_id" referenceTable="eav_attribute" - referenceColumn="attribute_id" onDelete="CASCADE"/> -- <constraint xsi:type="foreign" name="CSTR_ADDR_ENTT_VCHR_ENTT_ID_CSTR_ADDR_ENTT_ENTT_ID" -+ <constraint xsi:type="foreign" referenceId="CSTR_ADDR_ENTT_VCHR_ENTT_ID_CSTR_ADDR_ENTT_ENTT_ID" - table="customer_address_entity_varchar" column="entity_id" referenceTable="customer_address_entity" - referenceColumn="entity_id" onDelete="CASCADE"/> -- <constraint xsi:type="unique" name="CUSTOMER_ADDRESS_ENTITY_VARCHAR_ENTITY_ID_ATTRIBUTE_ID"> -+ <constraint xsi:type="unique" referenceId="CUSTOMER_ADDRESS_ENTITY_VARCHAR_ENTITY_ID_ATTRIBUTE_ID"> - <column name="entity_id"/> - <column name="attribute_id"/> - </constraint> -- <index name="CUSTOMER_ADDRESS_ENTITY_VARCHAR_ATTRIBUTE_ID" indexType="btree"> -+ <index referenceId="CUSTOMER_ADDRESS_ENTITY_VARCHAR_ATTRIBUTE_ID" indexType="btree"> - <column name="attribute_id"/> - </index> -- <index name="CUSTOMER_ADDRESS_ENTITY_VARCHAR_ENTITY_ID_ATTRIBUTE_ID_VALUE" indexType="btree"> -+ <index referenceId="CUSTOMER_ADDRESS_ENTITY_VARCHAR_ENTITY_ID_ATTRIBUTE_ID_VALUE" indexType="btree"> - <column name="entity_id"/> - <column name="attribute_id"/> - <column name="value"/> -@@ -274,29 +274,29 @@ - </table> - <table name="customer_entity_datetime" resource="default" engine="innodb" comment="Customer Entity Datetime"> - <column xsi:type="int" name="value_id" padding="11" unsigned="false" nullable="false" identity="true" -- comment="Value Id"/> -+ comment="Value ID"/> - <column xsi:type="smallint" name="attribute_id" padding="5" unsigned="true" nullable="false" identity="false" -- default="0" comment="Attribute Id"/> -+ default="0" comment="Attribute ID"/> - <column xsi:type="int" name="entity_id" padding="10" unsigned="true" nullable="false" identity="false" -- default="0" comment="Entity Id"/> -+ default="0" comment="Entity ID"/> - <column xsi:type="datetime" name="value" on_update="false" nullable="true" comment="Value"/> -- <constraint xsi:type="primary" name="PRIMARY"> -+ <constraint xsi:type="primary" referenceId="PRIMARY"> - <column name="value_id"/> - </constraint> -- <constraint xsi:type="foreign" name="CUSTOMER_ENTITY_DATETIME_ATTRIBUTE_ID_EAV_ATTRIBUTE_ATTRIBUTE_ID" -+ <constraint xsi:type="foreign" referenceId="CUSTOMER_ENTITY_DATETIME_ATTRIBUTE_ID_EAV_ATTRIBUTE_ATTRIBUTE_ID" - table="customer_entity_datetime" column="attribute_id" referenceTable="eav_attribute" - referenceColumn="attribute_id" onDelete="CASCADE"/> -- <constraint xsi:type="foreign" name="CUSTOMER_ENTITY_DATETIME_ENTITY_ID_CUSTOMER_ENTITY_ENTITY_ID" -+ <constraint xsi:type="foreign" referenceId="CUSTOMER_ENTITY_DATETIME_ENTITY_ID_CUSTOMER_ENTITY_ENTITY_ID" - table="customer_entity_datetime" column="entity_id" referenceTable="customer_entity" - referenceColumn="entity_id" onDelete="CASCADE"/> -- <constraint xsi:type="unique" name="CUSTOMER_ENTITY_DATETIME_ENTITY_ID_ATTRIBUTE_ID"> -+ <constraint xsi:type="unique" referenceId="CUSTOMER_ENTITY_DATETIME_ENTITY_ID_ATTRIBUTE_ID"> - <column name="entity_id"/> - <column name="attribute_id"/> - </constraint> -- <index name="CUSTOMER_ENTITY_DATETIME_ATTRIBUTE_ID" indexType="btree"> -+ <index referenceId="CUSTOMER_ENTITY_DATETIME_ATTRIBUTE_ID" indexType="btree"> - <column name="attribute_id"/> - </index> -- <index name="CUSTOMER_ENTITY_DATETIME_ENTITY_ID_ATTRIBUTE_ID_VALUE" indexType="btree"> -+ <index referenceId="CUSTOMER_ENTITY_DATETIME_ENTITY_ID_ATTRIBUTE_ID_VALUE" indexType="btree"> - <column name="entity_id"/> - <column name="attribute_id"/> - <column name="value"/> -@@ -304,30 +304,30 @@ - </table> - <table name="customer_entity_decimal" resource="default" engine="innodb" comment="Customer Entity Decimal"> - <column xsi:type="int" name="value_id" padding="11" unsigned="false" nullable="false" identity="true" -- comment="Value Id"/> -+ comment="Value ID"/> - <column xsi:type="smallint" name="attribute_id" padding="5" unsigned="true" nullable="false" identity="false" -- default="0" comment="Attribute Id"/> -+ default="0" comment="Attribute ID"/> - <column xsi:type="int" name="entity_id" padding="10" unsigned="true" nullable="false" identity="false" -- default="0" comment="Entity Id"/> -+ default="0" comment="Entity ID"/> - <column xsi:type="decimal" name="value" scale="4" precision="12" unsigned="false" nullable="false" default="0" - comment="Value"/> -- <constraint xsi:type="primary" name="PRIMARY"> -+ <constraint xsi:type="primary" referenceId="PRIMARY"> - <column name="value_id"/> - </constraint> -- <constraint xsi:type="foreign" name="CUSTOMER_ENTITY_DECIMAL_ATTRIBUTE_ID_EAV_ATTRIBUTE_ATTRIBUTE_ID" -+ <constraint xsi:type="foreign" referenceId="CUSTOMER_ENTITY_DECIMAL_ATTRIBUTE_ID_EAV_ATTRIBUTE_ATTRIBUTE_ID" - table="customer_entity_decimal" column="attribute_id" referenceTable="eav_attribute" - referenceColumn="attribute_id" onDelete="CASCADE"/> -- <constraint xsi:type="foreign" name="CUSTOMER_ENTITY_DECIMAL_ENTITY_ID_CUSTOMER_ENTITY_ENTITY_ID" -+ <constraint xsi:type="foreign" referenceId="CUSTOMER_ENTITY_DECIMAL_ENTITY_ID_CUSTOMER_ENTITY_ENTITY_ID" - table="customer_entity_decimal" column="entity_id" referenceTable="customer_entity" - referenceColumn="entity_id" onDelete="CASCADE"/> -- <constraint xsi:type="unique" name="CUSTOMER_ENTITY_DECIMAL_ENTITY_ID_ATTRIBUTE_ID"> -+ <constraint xsi:type="unique" referenceId="CUSTOMER_ENTITY_DECIMAL_ENTITY_ID_ATTRIBUTE_ID"> - <column name="entity_id"/> - <column name="attribute_id"/> - </constraint> -- <index name="CUSTOMER_ENTITY_DECIMAL_ATTRIBUTE_ID" indexType="btree"> -+ <index referenceId="CUSTOMER_ENTITY_DECIMAL_ATTRIBUTE_ID" indexType="btree"> - <column name="attribute_id"/> - </index> -- <index name="CUSTOMER_ENTITY_DECIMAL_ENTITY_ID_ATTRIBUTE_ID_VALUE" indexType="btree"> -+ <index referenceId="CUSTOMER_ENTITY_DECIMAL_ENTITY_ID_ATTRIBUTE_ID_VALUE" indexType="btree"> - <column name="entity_id"/> - <column name="attribute_id"/> - <column name="value"/> -@@ -335,30 +335,30 @@ - </table> - <table name="customer_entity_int" resource="default" engine="innodb" comment="Customer Entity Int"> - <column xsi:type="int" name="value_id" padding="11" unsigned="false" nullable="false" identity="true" -- comment="Value Id"/> -+ comment="Value ID"/> - <column xsi:type="smallint" name="attribute_id" padding="5" unsigned="true" nullable="false" identity="false" -- default="0" comment="Attribute Id"/> -+ default="0" comment="Attribute ID"/> - <column xsi:type="int" name="entity_id" padding="10" unsigned="true" nullable="false" identity="false" -- default="0" comment="Entity Id"/> -+ default="0" comment="Entity ID"/> - <column xsi:type="int" name="value" padding="11" unsigned="false" nullable="false" identity="false" default="0" - comment="Value"/> -- <constraint xsi:type="primary" name="PRIMARY"> -+ <constraint xsi:type="primary" referenceId="PRIMARY"> - <column name="value_id"/> - </constraint> -- <constraint xsi:type="foreign" name="CUSTOMER_ENTITY_INT_ATTRIBUTE_ID_EAV_ATTRIBUTE_ATTRIBUTE_ID" -+ <constraint xsi:type="foreign" referenceId="CUSTOMER_ENTITY_INT_ATTRIBUTE_ID_EAV_ATTRIBUTE_ATTRIBUTE_ID" - table="customer_entity_int" column="attribute_id" referenceTable="eav_attribute" - referenceColumn="attribute_id" onDelete="CASCADE"/> -- <constraint xsi:type="foreign" name="CUSTOMER_ENTITY_INT_ENTITY_ID_CUSTOMER_ENTITY_ENTITY_ID" -+ <constraint xsi:type="foreign" referenceId="CUSTOMER_ENTITY_INT_ENTITY_ID_CUSTOMER_ENTITY_ENTITY_ID" - table="customer_entity_int" column="entity_id" referenceTable="customer_entity" - referenceColumn="entity_id" onDelete="CASCADE"/> -- <constraint xsi:type="unique" name="CUSTOMER_ENTITY_INT_ENTITY_ID_ATTRIBUTE_ID"> -+ <constraint xsi:type="unique" referenceId="CUSTOMER_ENTITY_INT_ENTITY_ID_ATTRIBUTE_ID"> - <column name="entity_id"/> - <column name="attribute_id"/> - </constraint> -- <index name="CUSTOMER_ENTITY_INT_ATTRIBUTE_ID" indexType="btree"> -+ <index referenceId="CUSTOMER_ENTITY_INT_ATTRIBUTE_ID" indexType="btree"> - <column name="attribute_id"/> - </index> -- <index name="CUSTOMER_ENTITY_INT_ENTITY_ID_ATTRIBUTE_ID_VALUE" indexType="btree"> -+ <index referenceId="CUSTOMER_ENTITY_INT_ENTITY_ID_ATTRIBUTE_ID_VALUE" indexType="btree"> - <column name="entity_id"/> - <column name="attribute_id"/> - <column name="value"/> -@@ -366,54 +366,54 @@ - </table> - <table name="customer_entity_text" resource="default" engine="innodb" comment="Customer Entity Text"> - <column xsi:type="int" name="value_id" padding="11" unsigned="false" nullable="false" identity="true" -- comment="Value Id"/> -+ comment="Value ID"/> - <column xsi:type="smallint" name="attribute_id" padding="5" unsigned="true" nullable="false" identity="false" -- default="0" comment="Attribute Id"/> -+ default="0" comment="Attribute ID"/> - <column xsi:type="int" name="entity_id" padding="10" unsigned="true" nullable="false" identity="false" -- default="0" comment="Entity Id"/> -+ default="0" comment="Entity ID"/> - <column xsi:type="text" name="value" nullable="false" comment="Value"/> -- <constraint xsi:type="primary" name="PRIMARY"> -+ <constraint xsi:type="primary" referenceId="PRIMARY"> - <column name="value_id"/> - </constraint> -- <constraint xsi:type="foreign" name="CUSTOMER_ENTITY_TEXT_ATTRIBUTE_ID_EAV_ATTRIBUTE_ATTRIBUTE_ID" -+ <constraint xsi:type="foreign" referenceId="CUSTOMER_ENTITY_TEXT_ATTRIBUTE_ID_EAV_ATTRIBUTE_ATTRIBUTE_ID" - table="customer_entity_text" column="attribute_id" referenceTable="eav_attribute" - referenceColumn="attribute_id" onDelete="CASCADE"/> -- <constraint xsi:type="foreign" name="CUSTOMER_ENTITY_TEXT_ENTITY_ID_CUSTOMER_ENTITY_ENTITY_ID" -+ <constraint xsi:type="foreign" referenceId="CUSTOMER_ENTITY_TEXT_ENTITY_ID_CUSTOMER_ENTITY_ENTITY_ID" - table="customer_entity_text" column="entity_id" referenceTable="customer_entity" - referenceColumn="entity_id" onDelete="CASCADE"/> -- <constraint xsi:type="unique" name="CUSTOMER_ENTITY_TEXT_ENTITY_ID_ATTRIBUTE_ID"> -+ <constraint xsi:type="unique" referenceId="CUSTOMER_ENTITY_TEXT_ENTITY_ID_ATTRIBUTE_ID"> - <column name="entity_id"/> - <column name="attribute_id"/> - </constraint> -- <index name="CUSTOMER_ENTITY_TEXT_ATTRIBUTE_ID" indexType="btree"> -+ <index referenceId="CUSTOMER_ENTITY_TEXT_ATTRIBUTE_ID" indexType="btree"> - <column name="attribute_id"/> - </index> - </table> - <table name="customer_entity_varchar" resource="default" engine="innodb" comment="Customer Entity Varchar"> - <column xsi:type="int" name="value_id" padding="11" unsigned="false" nullable="false" identity="true" -- comment="Value Id"/> -+ comment="Value ID"/> - <column xsi:type="smallint" name="attribute_id" padding="5" unsigned="true" nullable="false" identity="false" -- default="0" comment="Attribute Id"/> -+ default="0" comment="Attribute ID"/> - <column xsi:type="int" name="entity_id" padding="10" unsigned="true" nullable="false" identity="false" -- default="0" comment="Entity Id"/> -+ default="0" comment="Entity ID"/> - <column xsi:type="varchar" name="value" nullable="true" length="255" comment="Value"/> -- <constraint xsi:type="primary" name="PRIMARY"> -+ <constraint xsi:type="primary" referenceId="PRIMARY"> - <column name="value_id"/> - </constraint> -- <constraint xsi:type="foreign" name="CUSTOMER_ENTITY_VARCHAR_ATTRIBUTE_ID_EAV_ATTRIBUTE_ATTRIBUTE_ID" -+ <constraint xsi:type="foreign" referenceId="CUSTOMER_ENTITY_VARCHAR_ATTRIBUTE_ID_EAV_ATTRIBUTE_ATTRIBUTE_ID" - table="customer_entity_varchar" column="attribute_id" referenceTable="eav_attribute" - referenceColumn="attribute_id" onDelete="CASCADE"/> -- <constraint xsi:type="foreign" name="CUSTOMER_ENTITY_VARCHAR_ENTITY_ID_CUSTOMER_ENTITY_ENTITY_ID" -+ <constraint xsi:type="foreign" referenceId="CUSTOMER_ENTITY_VARCHAR_ENTITY_ID_CUSTOMER_ENTITY_ENTITY_ID" - table="customer_entity_varchar" column="entity_id" referenceTable="customer_entity" - referenceColumn="entity_id" onDelete="CASCADE"/> -- <constraint xsi:type="unique" name="CUSTOMER_ENTITY_VARCHAR_ENTITY_ID_ATTRIBUTE_ID"> -+ <constraint xsi:type="unique" referenceId="CUSTOMER_ENTITY_VARCHAR_ENTITY_ID_ATTRIBUTE_ID"> - <column name="entity_id"/> - <column name="attribute_id"/> - </constraint> -- <index name="CUSTOMER_ENTITY_VARCHAR_ATTRIBUTE_ID" indexType="btree"> -+ <index referenceId="CUSTOMER_ENTITY_VARCHAR_ATTRIBUTE_ID" indexType="btree"> - <column name="attribute_id"/> - </index> -- <index name="CUSTOMER_ENTITY_VARCHAR_ENTITY_ID_ATTRIBUTE_ID_VALUE" indexType="btree"> -+ <index referenceId="CUSTOMER_ENTITY_VARCHAR_ENTITY_ID_ATTRIBUTE_ID_VALUE" indexType="btree"> - <column name="entity_id"/> - <column name="attribute_id"/> - <column name="value"/> -@@ -425,7 +425,7 @@ - comment="Customer Group Code"/> - <column xsi:type="int" name="tax_class_id" padding="10" unsigned="true" nullable="false" identity="false" - default="0" comment="Tax Class Id"/> -- <constraint xsi:type="primary" name="PRIMARY"> -+ <constraint xsi:type="primary" referenceId="PRIMARY"> - <column name="customer_group_id"/> - </constraint> - </table> -@@ -451,10 +451,10 @@ - identity="false" default="0" comment="Is Filterable in Grid"/> - <column xsi:type="smallint" name="is_searchable_in_grid" padding="5" unsigned="true" nullable="false" - identity="false" default="0" comment="Is Searchable in Grid"/> -- <constraint xsi:type="primary" name="PRIMARY"> -+ <constraint xsi:type="primary" referenceId="PRIMARY"> - <column name="attribute_id"/> - </constraint> -- <constraint xsi:type="foreign" name="CUSTOMER_EAV_ATTRIBUTE_ATTRIBUTE_ID_EAV_ATTRIBUTE_ATTRIBUTE_ID" -+ <constraint xsi:type="foreign" referenceId="CUSTOMER_EAV_ATTRIBUTE_ATTRIBUTE_ID_EAV_ATTRIBUTE_ATTRIBUTE_ID" - table="customer_eav_attribute" column="attribute_id" referenceTable="eav_attribute" - referenceColumn="attribute_id" onDelete="CASCADE"/> - </table> -@@ -462,14 +462,14 @@ - <column xsi:type="varchar" name="form_code" nullable="false" length="32" comment="Form Code"/> - <column xsi:type="smallint" name="attribute_id" padding="5" unsigned="true" nullable="false" identity="false" - comment="Attribute Id"/> -- <constraint xsi:type="primary" name="PRIMARY"> -+ <constraint xsi:type="primary" referenceId="PRIMARY"> - <column name="form_code"/> - <column name="attribute_id"/> - </constraint> -- <constraint xsi:type="foreign" name="CUSTOMER_FORM_ATTRIBUTE_ATTRIBUTE_ID_EAV_ATTRIBUTE_ATTRIBUTE_ID" -+ <constraint xsi:type="foreign" referenceId="CUSTOMER_FORM_ATTRIBUTE_ATTRIBUTE_ID_EAV_ATTRIBUTE_ATTRIBUTE_ID" - table="customer_form_attribute" column="attribute_id" referenceTable="eav_attribute" - referenceColumn="attribute_id" onDelete="CASCADE"/> -- <index name="CUSTOMER_FORM_ATTRIBUTE_ATTRIBUTE_ID" indexType="btree"> -+ <index referenceId="CUSTOMER_FORM_ATTRIBUTE_ATTRIBUTE_ID" indexType="btree"> - <column name="attribute_id"/> - </index> - </table> -@@ -486,17 +486,17 @@ - <column xsi:type="text" name="default_value" nullable="true" comment="Default Value"/> - <column xsi:type="smallint" name="multiline_count" padding="5" unsigned="true" nullable="true" identity="false" - comment="Multiline Count"/> -- <constraint xsi:type="primary" name="PRIMARY"> -+ <constraint xsi:type="primary" referenceId="PRIMARY"> - <column name="attribute_id"/> - <column name="website_id"/> - </constraint> -- <constraint xsi:type="foreign" name="CSTR_EAV_ATTR_WS_ATTR_ID_EAV_ATTR_ATTR_ID" -+ <constraint xsi:type="foreign" referenceId="CSTR_EAV_ATTR_WS_ATTR_ID_EAV_ATTR_ATTR_ID" - table="customer_eav_attribute_website" column="attribute_id" referenceTable="eav_attribute" - referenceColumn="attribute_id" onDelete="CASCADE"/> -- <constraint xsi:type="foreign" name="CSTR_EAV_ATTR_WS_WS_ID_STORE_WS_WS_ID" -+ <constraint xsi:type="foreign" referenceId="CSTR_EAV_ATTR_WS_WS_ID_STORE_WS_WS_ID" - table="customer_eav_attribute_website" column="website_id" referenceTable="store_website" - referenceColumn="website_id" onDelete="CASCADE"/> -- <index name="CUSTOMER_EAV_ATTRIBUTE_WEBSITE_WEBSITE_ID" indexType="btree"> -+ <index referenceId="CUSTOMER_EAV_ATTRIBUTE_WEBSITE_WEBSITE_ID" indexType="btree"> - <column name="website_id"/> - </index> - </table> -@@ -506,15 +506,15 @@ - <column xsi:type="int" name="customer_id" padding="11" unsigned="false" nullable="true" identity="false" - comment="Customer Id"/> - <column xsi:type="varchar" name="session_id" nullable="true" length="64" comment="Session ID"/> -- <column xsi:type="timestamp" name="last_visit_at" on_update="true" nullable="true" default="CURRENT_TIMESTAMP" -+ <column xsi:type="timestamp" name="last_visit_at" on_update="true" nullable="false" default="CURRENT_TIMESTAMP" - comment="Last Visit Time"/> -- <constraint xsi:type="primary" name="PRIMARY"> -+ <constraint xsi:type="primary" referenceId="PRIMARY"> - <column name="visitor_id"/> - </constraint> -- <index name="CUSTOMER_VISITOR_CUSTOMER_ID" indexType="btree"> -+ <index referenceId="CUSTOMER_VISITOR_CUSTOMER_ID" indexType="btree"> - <column name="customer_id"/> - </index> -- <index name="CUSTOMER_VISITOR_LAST_VISIT_AT" indexType="btree"> -+ <index referenceId="CUSTOMER_VISITOR_LAST_VISIT_AT" indexType="btree"> - <column name="last_visit_at"/> - </index> - </table> -@@ -526,10 +526,10 @@ - <column xsi:type="timestamp" name="last_login_at" on_update="false" nullable="true" comment="Last Login Time"/> - <column xsi:type="timestamp" name="last_logout_at" on_update="false" nullable="true" - comment="Last Logout Time"/> -- <constraint xsi:type="primary" name="PRIMARY"> -+ <constraint xsi:type="primary" referenceId="PRIMARY"> - <column name="log_id"/> - </constraint> -- <constraint xsi:type="unique" name="CUSTOMER_LOG_CUSTOMER_ID"> -+ <constraint xsi:type="unique" referenceId="CUSTOMER_LOG_CUSTOMER_ID"> - <column name="customer_id"/> - </constraint> - </table> -diff --git a/app/code/Magento/Customer/etc/db_schema_whitelist.json b/app/code/Magento/Customer/etc/db_schema_whitelist.json -index 4aada8f0d81..ec7a53945ab 100644 ---- a/app/code/Magento/Customer/etc/db_schema_whitelist.json -+++ b/app/code/Magento/Customer/etc/db_schema_whitelist.json -@@ -73,7 +73,8 @@ - "vat_request_success": true - }, - "index": { -- "CUSTOMER_ADDRESS_ENTITY_PARENT_ID": true -+ "CUSTOMER_ADDRESS_ENTITY_PARENT_ID": true, -+ "FTI_BA70344390184AC3F063AB2EB38BC0ED": true - }, - "constraint": { - "PRIMARY": true, -diff --git a/app/code/Magento/Customer/etc/di.xml b/app/code/Magento/Customer/etc/di.xml -index 6e8c3dc68ed..a181d6dd217 100644 ---- a/app/code/Magento/Customer/etc/di.xml -+++ b/app/code/Magento/Customer/etc/di.xml -@@ -127,6 +127,13 @@ - <argument name="groupManagement" xsi:type="object">Magento\Customer\Api\GroupManagementInterface\Proxy</argument> - </arguments> - </type> -+ <type name="Magento\Customer\Model\Metadata\CustomerMetadata"> -+ <arguments> -+ <argument name="systemAttributes" xsi:type="array"> -+ <item name="disable_auto_group_change" xsi:type="string">disable_auto_group_change</item> -+ </argument> -+ </arguments> -+ </type> - <virtualType name="SectionInvalidationConfigReader" type="Magento\Framework\Config\Reader\Filesystem"> - <arguments> - <argument name="idAttributes" xsi:type="array"> -@@ -223,6 +230,7 @@ - <item name="customer_listing_data_source" xsi:type="string">Magento\Customer\Model\ResourceModel\Grid\Collection</item> - <item name="customer_online_grid_data_source" xsi:type="string">Magento\Customer\Model\ResourceModel\Online\Grid\Collection</item> - <item name="customer_group_listing_data_source" xsi:type="string">Magento\Customer\Model\ResourceModel\Group\Grid\Collection</item> -+ <item name="customer_address_listing_data_source" xsi:type="string">Magento\Customer\Model\ResourceModel\Address\Grid\Collection</item> - </argument> - </arguments> - </type> -@@ -449,6 +457,14 @@ - <argument name="resourceModel" xsi:type="string">Magento\Customer\Model\ResourceModel\Group</argument> - </arguments> - </type> -+ <type name="Magento\Customer\Model\ResourceModel\Address\Grid\Collection"> -+ <arguments> -+ <argument name="mainTable" xsi:type="string">customer_address_entity</argument> -+ <argument name="eventPrefix" xsi:type="string">customer_address_entity_grid_collection</argument> -+ <argument name="eventObject" xsi:type="string">customer_address_entity_grid_collection</argument> -+ <argument name="resourceModel" xsi:type="string">Magento\Customer\Model\ResourceModel\Address</argument> -+ </arguments> -+ </type> - <preference - for="Magento\Customer\Api\AccountDelegationInterface" - type="Magento\Customer\Model\Delegation\AccountDelegation" /> -diff --git a/app/code/Magento/Customer/etc/frontend/di.xml b/app/code/Magento/Customer/etc/frontend/di.xml -index 4a45c4ad48d..c31742519e5 100644 ---- a/app/code/Magento/Customer/etc/frontend/di.xml -+++ b/app/code/Magento/Customer/etc/frontend/di.xml -@@ -57,7 +57,7 @@ - <type name="Magento\Checkout\Block\Cart\Sidebar"> - <plugin name="customer_cart" type="Magento\Customer\Model\Cart\ConfigPlugin" /> - </type> -- <type name="Magento\Framework\Session\SessionManager"> -+ <type name="Magento\Framework\Session\SessionManagerInterface"> - <plugin name="session_checker" type="Magento\Customer\CustomerData\Plugin\SessionChecker" /> - </type> - <type name="Magento\Authorization\Model\CompositeUserContext"> -@@ -77,4 +77,4 @@ - </argument> - </arguments> - </type> --</config> -+</config> -\ No newline at end of file -diff --git a/app/code/Magento/Customer/i18n/en_US.csv b/app/code/Magento/Customer/i18n/en_US.csv -index bf73d6361d4..e1c68f3d81e 100644 ---- a/app/code/Magento/Customer/i18n/en_US.csv -+++ b/app/code/Magento/Customer/i18n/en_US.csv -@@ -47,7 +47,7 @@ Sending,Sending - Paused,Paused - View,View - Unknown,Unknown --Order,Order -+"Order #","Order #" - Purchased,Purchased - "Bill-to Name","Bill-to Name" - "Ship-to Name","Ship-to Name" -@@ -506,10 +506,10 @@ Strong,Strong - "Rebuild Customer grid index","Rebuild Customer grid index" - Group,Group - "Add New Customer","Add New Customer" --"Are you sure to delete selected customers?","Are you sure to delete selected customers?" -+"Are you sure you want to delete the selected customers?","Are you sure you want to delete the selected customers?" - "Delete items","Delete items" - "Subscribe to Newsletter","Subscribe to Newsletter" --"Are you sure to unsubscribe selected customers from newsletter?","Are you sure to unsubscribe selected customers from newsletter?" -+"Are you sure you want to unsubscribe the selected customers from the newsletter?","Are you sure you want to unsubscribe the selected customers from the newsletter?" - "Unsubscribe from Newsletter","Unsubscribe from Newsletter" - "Assign a Customer Group","Assign a Customer Group" - Phone,Phone -diff --git a/app/code/Magento/Customer/view/adminhtml/layout/customer_address_edit.xml b/app/code/Magento/Customer/view/adminhtml/layout/customer_address_edit.xml -new file mode 100644 -index 00000000000..3acae3acec8 ---- /dev/null -+++ b/app/code/Magento/Customer/view/adminhtml/layout/customer_address_edit.xml -@@ -0,0 +1,15 @@ -+<?xml version="1.0"?> -+<!-- -+/** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+<page xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" layout="admin-1column" xsi:noNamespaceSchemaLocation="urn:magento:framework:View/Layout/etc/page_configuration.xsd"> -+ <update handle="styles"/> -+ <body> -+ <referenceContainer name="content"> -+ <uiComponent name="customer_address_form"/> -+ </referenceContainer> -+ </body> -+</page> -diff --git a/app/code/Magento/Customer/view/adminhtml/layout/customer_index_edit.xml b/app/code/Magento/Customer/view/adminhtml/layout/customer_index_edit.xml -index 23b57188702..e982d8bb096 100644 ---- a/app/code/Magento/Customer/view/adminhtml/layout/customer_index_edit.xml -+++ b/app/code/Magento/Customer/view/adminhtml/layout/customer_index_edit.xml -@@ -15,5 +15,8 @@ - <referenceContainer name="content"> - <uiComponent name="customer_form"/> - </referenceContainer> -+ <referenceContainer name="after.body.start"> -+ <block class="Magento\Catalog\Block\Adminhtml\Product\Composite\Configure" name="after.body.start.product_composite_configure" template="Magento_Catalog::catalog/product/composite/configure.phtml"/> -+ </referenceContainer> - </body> - </page> -diff --git a/app/code/Magento/Customer/view/adminhtml/templates/edit/js.phtml b/app/code/Magento/Customer/view/adminhtml/templates/edit/js.phtml -index 143b4be507a..14aa3f4763e 100644 ---- a/app/code/Magento/Customer/view/adminhtml/templates/edit/js.phtml -+++ b/app/code/Magento/Customer/view/adminhtml/templates/edit/js.phtml -@@ -3,7 +3,4 @@ - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -- --// @codingStandardsIgnoreFile -- - ?> -diff --git a/app/code/Magento/Customer/view/adminhtml/templates/sales/order/create/address/form/renderer/vat.phtml b/app/code/Magento/Customer/view/adminhtml/templates/sales/order/create/address/form/renderer/vat.phtml -index f55562682d9..b792bc27f5b 100644 ---- a/app/code/Magento/Customer/view/adminhtml/templates/sales/order/create/address/form/renderer/vat.phtml -+++ b/app/code/Magento/Customer/view/adminhtml/templates/sales/order/create/address/form/renderer/vat.phtml -@@ -4,8 +4,6 @@ - * See COPYING.txt for license details. - */ - --// @codingStandardsIgnoreFile -- - /** @var \Magento\Customer\Block\Adminhtml\Sales\Order\Address\Form\Renderer\Vat $block */ - - $_element = $block->getElement(); -@@ -13,16 +11,16 @@ $_note = $_element->getNote(); - $_class = $_element->getFieldsetHtmlClass(); - $_validateButton = $block->getValidateButton(); - ?> --<?php if (!$_element->getNoDisplay()): ?> -+<?php if (!$_element->getNoDisplay()) : ?> - <div class="admin__field field-vat-number"> -- <?php if ($_element->getType() == 'hidden'): ?> -+ <?php if ($_element->getType() == 'hidden') : ?> - <div class="hidden"><?= $_element->getElementHtml() ?></div> -- <?php else: ?> -+ <?php else : ?> - <?= $_element->getLabelHtml() ?> - <div class="admin__field-control <?= /* @noEscape */ $_element->hasValueClass() ? $block->escapeHtmlAttr($_element->getValueClass()) : 'value' ?><?= $_class ? $block->escapeHtmlAttr($_class) . '-value' : '' ?>"> - <?= $_element->getElementHtml() ?> -- <?php if ($_note): ?> -- <div class="admin__field-note<?= $_class ? " {$_class}-note" : '' ?>" id="note_<?= $block->escapeHtmlAttr($_element->getId()) ?>"> -+ <?php if ($_note) : ?> -+ <div class="admin__field-note<?= /* @noEscape */ $_class ? " {$block->escapeHtmlAttr($_class)}-note" : '' ?>" id="note_<?= $block->escapeHtmlAttr($_element->getId()) ?>"> - <span><?= $block->escapeHtml($_note) ?></span> - </div> - <?php endif; ?> -diff --git a/app/code/Magento/Customer/view/adminhtml/templates/system/config/validatevat.phtml b/app/code/Magento/Customer/view/adminhtml/templates/system/config/validatevat.phtml -index 8eb3057d0a3..ab1671ede6e 100644 ---- a/app/code/Magento/Customer/view/adminhtml/templates/system/config/validatevat.phtml -+++ b/app/code/Magento/Customer/view/adminhtml/templates/system/config/validatevat.phtml -@@ -4,8 +4,6 @@ - * See COPYING.txt for license details. - */ - --// @codingStandardsIgnoreFile -- - /** @var \Magento\Customer\Block\Adminhtml\System\Config\Validatevat $block */ - ?> - <script> -diff --git a/app/code/Magento/Customer/view/adminhtml/templates/tab/cart.phtml b/app/code/Magento/Customer/view/adminhtml/templates/tab/cart.phtml -index 76fa53c1254..434e5606cd0 100644 ---- a/app/code/Magento/Customer/view/adminhtml/templates/tab/cart.phtml -+++ b/app/code/Magento/Customer/view/adminhtml/templates/tab/cart.phtml -@@ -4,11 +4,9 @@ - * See COPYING.txt for license details. - */ - --// @codingStandardsIgnoreFile -- - /* @var \Magento\Customer\Block\Adminhtml\Edit\Tab\Cart $block */ - ?> --<?php if ($block->getCartHeader()): ?> -+<?php if ($block->getCartHeader()) : ?> - <div class="content-header skip-header"> - <table> - <tr> -@@ -19,76 +17,74 @@ - <?php endif ?> - <?= $block->getGridParentHtml() ?> - <?php if ($block->canDisplayContainer()) : ?> --<?php -- $listType = $block->getJsObjectName(); --?> --<script> --require([ -- "Magento_Ui/js/modal/alert", -- "Magento_Ui/js/modal/confirm", -- "Magento_Catalog/catalog/product/composite/configure" --], function(alert, confirm){ -+ <?php $listType = $block->getJsObjectName(); ?> -+ <script> -+ require([ -+ "Magento_Ui/js/modal/alert", -+ "Magento_Ui/js/modal/confirm", -+ "Magento_Catalog/catalog/product/composite/configure" -+ ], function(alert, confirm){ - --<?= $block->escapeJs($block->getJsObjectName()) ?>cartControl = { -- reload: function (params) { -- if (!params) { -- params = {}; -- } -- <?= $block->escapeJs($block->getJsObjectName()) ?>.reloadParams = params; -- <?= $block->escapeJs($block->getJsObjectName()) ?>.reload(); -- <?= $block->escapeJs($block->getJsObjectName()) ?>.reloadParams = {}; -- }, -+ <?= $block->escapeJs($block->getJsObjectName()) ?>cartControl = { -+ reload: function (params) { -+ if (!params) { -+ params = {}; -+ } -+ <?= $block->escapeJs($block->getJsObjectName()) ?>.reloadParams = params; -+ <?= $block->escapeJs($block->getJsObjectName()) ?>.reload(); -+ <?= $block->escapeJs($block->getJsObjectName()) ?>.reloadParams = {}; -+ }, - -- configureItem: function (itemId) { -- productConfigure.setOnLoadIFrameCallback('<?= $block->escapeJs($listType) ?>', this.cbOnLoadIframe.bind(this)); -- productConfigure.showItemConfiguration('<?= $block->escapeJs($listType) ?>', itemId); -- return false; -- }, -+ configureItem: function (itemId) { -+ productConfigure.setOnLoadIFrameCallback('<?= $block->escapeJs($listType) ?>', this.cbOnLoadIframe.bind(this)); -+ productConfigure.showItemConfiguration('<?= $block->escapeJs($listType) ?>', itemId); -+ return false; -+ }, - -- cbOnLoadIframe: function (response) { -- if (!response.ok) { -- return; -- } -- this.reload(); -- }, -+ cbOnLoadIframe: function (response) { -+ if (!response.ok) { -+ return; -+ } -+ this.reload(); -+ }, - -- removeItem: function (itemId) { -- var self = this; -+ removeItem: function (itemId) { -+ var self = this; - -- if (!itemId) { -- alert({ -- content: '<?= $block->escapeJs(__('No item specified.')) ?>' -- }); -+ if (!itemId) { -+ alert({ -+ content: '<?= $block->escapeJs(__('No item specified.')) ?>' -+ }); - -- return false; -- } -+ return false; -+ } - -- confirm({ -- content: '<?= $block->escapeJs(__('Are you sure you want to remove this item?')) ?>', -- actions: { -- confirm: function(){ -- self.reload({'delete':itemId}); -+ confirm({ -+ content: '<?= $block->escapeJs(__('Are you sure you want to remove this item?')) ?>', -+ actions: { -+ confirm: function(){ -+ self.reload({'delete':itemId}); -+ } - } -- } -- }); -- } --}; -+ }); -+ } -+ }; - --<?php --$params = [ -- 'customer_id' => $block->getCustomerId(), -- 'website_id' => $block->getWebsiteId(), --]; --?> --productConfigure.addListType( -- '<?= $block->escapeJs($listType) ?>', -- { -- urlFetch: '<?= $block->escapeJs($block->escapeUrl($block->getUrl('customer/cart_product_composite_cart/configure', $params))) ?>', -- urlConfirm: '<?= $block->escapeJs($block->escapeUrl($block->getUrl('customer/cart_product_composite_cart/update', $params))) ?>' -- } --); -+ <?php -+ $params = [ -+ 'customer_id' => $block->getCustomerId(), -+ 'website_id' => $block->getWebsiteId(), -+ ]; -+ ?> -+ productConfigure.addListType( -+ '<?= $block->escapeJs($listType) ?>', -+ { -+ urlFetch: '<?= $block->escapeJs($block->escapeUrl($block->getUrl('customer/cart_product_composite_cart/configure', $params))) ?>', -+ urlConfirm: '<?= $block->escapeJs($block->escapeUrl($block->getUrl('customer/cart_product_composite_cart/update', $params))) ?>' -+ } -+ ); - --}); --</script> -+ }); -+ </script> - <?php endif ?> - <br /> -diff --git a/app/code/Magento/Customer/view/adminhtml/templates/tab/newsletter.phtml b/app/code/Magento/Customer/view/adminhtml/templates/tab/newsletter.phtml -index 30acb16c158..12d4902fb18 100644 ---- a/app/code/Magento/Customer/view/adminhtml/templates/tab/newsletter.phtml -+++ b/app/code/Magento/Customer/view/adminhtml/templates/tab/newsletter.phtml -@@ -3,9 +3,6 @@ - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -- --// @codingStandardsIgnoreFile -- - ?> - <div class="entry-edit"> - <?= $block->getForm()->getHtml() ?> -diff --git a/app/code/Magento/Customer/view/adminhtml/templates/tab/view/personal_info.phtml b/app/code/Magento/Customer/view/adminhtml/templates/tab/view/personal_info.phtml -index f43cb8ab5f0..b3baeace897 100644 ---- a/app/code/Magento/Customer/view/adminhtml/templates/tab/view/personal_info.phtml -+++ b/app/code/Magento/Customer/view/adminhtml/templates/tab/view/personal_info.phtml -@@ -4,8 +4,6 @@ - * See COPYING.txt for license details. - */ - --// @codingStandardsIgnoreFile -- - /** @var \Magento\Customer\Block\Adminhtml\Edit\Tab\View\PersonalInfo $block */ - - $lastLoginDateAdmin = $block->getLastLoginDate(); -@@ -25,7 +23,7 @@ $createDateStore = $block->getStoreCreateDate(); - <th><?= $block->escapeHtml(__('Last Logged In:')) ?></th> - <td><?= $block->escapeHtml($lastLoginDateAdmin) ?> (<?= $block->escapeHtml($block->getCurrentStatus()) ?>)</td> - </tr> -- <?php if ($lastLoginDateAdmin != $lastLoginDateStore): ?> -+ <?php if ($lastLoginDateAdmin != $lastLoginDateStore) : ?> - <tr> - <th><?= $block->escapeHtml(__('Last Logged In (%1):', $block->getStoreLastLoginDateTimezone())) ?></th> - <td><?= $block->escapeHtml($lastLoginDateStore) ?> (<?= $block->escapeHtml($block->getCurrentStatus()) ?>)</td> -@@ -43,7 +41,7 @@ $createDateStore = $block->getStoreCreateDate(); - <th><?= $block->escapeHtml(__('Account Created:')) ?></th> - <td><?= $block->escapeHtml($createDateAdmin) ?></td> - </tr> -- <?php if ($createDateAdmin != $createDateStore): ?> -+ <?php if ($createDateAdmin != $createDateStore) : ?> - <tr> - <th><?= $block->escapeHtml(__('Account Created on (%1):', $block->getStoreCreateDateTimezone())) ?></th> - <td><?= $block->escapeHtml($createDateStore) ?></td> -diff --git a/app/code/Magento/Customer/view/adminhtml/templates/tab/view/sales.phtml b/app/code/Magento/Customer/view/adminhtml/templates/tab/view/sales.phtml -index 12eae5cac9b..7b888c04046 100644 ---- a/app/code/Magento/Customer/view/adminhtml/templates/tab/view/sales.phtml -+++ b/app/code/Magento/Customer/view/adminhtml/templates/tab/view/sales.phtml -@@ -4,8 +4,6 @@ - * See COPYING.txt for license details. - */ - --// @codingStandardsIgnoreFile -- - /** @var \Magento\Customer\Block\Adminhtml\Edit\Tab\View\Sales $block */ - - $singleStoreMode = $block->isSingleStoreMode(); -@@ -19,7 +17,7 @@ $singleStoreMode = $block->isSingleStoreMode(); - <table class="data-table"> - <thead> - <tr> -- <?php if (!$singleStoreMode): ?> -+ <?php if (!$singleStoreMode) : ?> - <th><?= $block->escapeHtml(__('Web Site')) ?></th> - <th><?= $block->escapeHtml(__('Store')) ?></th> - <th><?= $block->escapeHtml(__('Store View')) ?></th> -@@ -28,7 +26,7 @@ $singleStoreMode = $block->isSingleStoreMode(); - <th class="last"><?= $block->escapeHtml(__('Average Sale')) ?></th> - </tr> - </thead> -- <?php if (!$singleStoreMode): ?> -+ <?php if (!$singleStoreMode) : ?> - <tfoot> - <tr> - <td colspan="3"><strong><?= $block->escapeHtml(__('All Store Views')) ?></strong></td> -@@ -37,40 +35,40 @@ $singleStoreMode = $block->isSingleStoreMode(); - </tr> - </tfoot> - <?php endif; ?> -- <?php if ($block->getRows()): ?> -+ <?php if ($block->getRows()) : ?> - <tbody> - <?php $_i = 0; ?> -- <?php foreach ($block->getRows() as $_websiteId => $_groups): ?> -- <?php $_websiteRow = false; ?> -- <?php foreach ($_groups as $_groupId => $_stores): ?> -- <?php $_groupRow = false; ?> -- <?php foreach ($_stores as $_row): ?> -- <?php if (!$singleStoreMode): ?> -- <?php if ($_row->getStoreId() == 0): ?> -- <td colspan="3"><?= $block->escapeHtml($_row->getStoreName()) ?></td> -- <?php else: ?> -- <tr<?= ($_i++ % 2 ? ' class="even"' : '') ?>> -- <?php if (!$_websiteRow): ?> -- <td rowspan="<?= $block->escapeHtmlAttr($block->getWebsiteCount($_websiteId)) ?>"><?= $block->escapeHtml($_row->getWebsiteName()) ?></td> -- <?php $_websiteRow = true; ?> -+ <?php foreach ($block->getRows() as $_websiteId => $_groups) : ?> -+ <?php $_websiteRow = false; ?> -+ <?php foreach ($_groups as $_groupId => $_stores) : ?> -+ <?php $_groupRow = false; ?> -+ <?php foreach ($_stores as $_row) : ?> -+ <?php if (!$singleStoreMode) : ?> -+ <?php if ($_row->getStoreId() == 0) : ?> -+ <td colspan="3"><?= $block->escapeHtml($_row->getStoreName()) ?></td> -+ <?php else : ?> -+ <tr<?= ($_i++ % 2 ? ' class="even"' : '') ?>> -+ <?php if (!$_websiteRow) : ?> -+ <td rowspan="<?= $block->escapeHtmlAttr($block->getWebsiteCount($_websiteId)) ?>"><?= $block->escapeHtml($_row->getWebsiteName()) ?></td> -+ <?php $_websiteRow = true; ?> - <?php endif; ?> -- <?php if (!$_groupRow): ?> -- <td rowspan="<?= count($_stores) ?>"><?= $block->escapeHtml($_row->getGroupName()) ?></td> -- <?php $_groupRow = true; ?> -+ <?php if (!$_groupRow) : ?> -+ <td rowspan="<?= count($_stores) ?>"><?= $block->escapeHtml($_row->getGroupName()) ?></td> -+ <?php $_groupRow = true; ?> - <?php endif; ?> -- <td><?= $block->escapeHtml($_row->getStoreName()) ?></td> -+ <td><?= $block->escapeHtml($_row->getStoreName()) ?></td> - <?php endif; ?> -- <?php else: ?> -- <tr> -+ <?php else : ?> -+ <tr> - <?php endif; ?> -- <td><?= $block->escapeHtml($block->formatCurrency($_row->getLifetime(), $_row->getWebsiteId())) ?></td> -- <td><?= $block->escapeHtml($block->formatCurrency($_row->getAvgsale(), $_row->getWebsiteId())) ?></td> -- </tr> -+ <td><?= $block->escapeHtml($block->formatCurrency($_row->getLifetime(), $_row->getWebsiteId())) ?></td> -+ <td><?= $block->escapeHtml($block->formatCurrency($_row->getAvgsale(), $_row->getWebsiteId())) ?></td> -+ </tr> - <?php endforeach; ?> - <?php endforeach; ?> - <?php endforeach; ?> - </tbody> -- <?php else: ?> -+ <?php else : ?> - <tbody> - <tr class="hidden"><td colspan="<?= /* @noEscape */ $singleStoreMode ? 2 : 5 ?>"></td></tr> - </tbody> -diff --git a/app/code/Magento/Customer/view/adminhtml/ui_component/customer_address_form.xml b/app/code/Magento/Customer/view/adminhtml/ui_component/customer_address_form.xml -new file mode 100644 -index 00000000000..692cb2ecb96 ---- /dev/null -+++ b/app/code/Magento/Customer/view/adminhtml/ui_component/customer_address_form.xml -@@ -0,0 +1,249 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+/** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+<form xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Ui:etc/ui_configuration.xsd" component="Magento_Customer/js/form/components/form"> -+ <argument name="data" xsi:type="array"> -+ <item name="js_config" xsi:type="array"> -+ <item name="provider" xsi:type="string">customer_address_form.customer_address_form_data_source</item> -+ </item> -+ <item name="config" xsi:type="array"> -+ <item name="deleteConfirmationMessage" translate="true" xsi:type="string">Are you sure you want to delete this address?</item> -+ </item> -+ <item name="label" xsi:type="string" translate="true">Update Address</item> -+ <item name="reverseMetadataMerge" xsi:type="boolean">true</item> -+ <item name="template" xsi:type="string">templates/form/collapsible</item> -+ </argument> -+ <settings> -+ <buttons> -+ <button name="cancel" class="Magento\Customer\Block\Adminhtml\Edit\Address\CancelButton"/> -+ <button name="delete" class="Magento\Customer\Block\Adminhtml\Edit\Address\DeleteButton"/> -+ <button name="save" class="Magento\Customer\Block\Adminhtml\Edit\Address\SaveButton"/> -+ </buttons> -+ <namespace>customer_address_form</namespace> -+ <ajaxSave>true</ajaxSave> -+ <ajaxSaveType>simple</ajaxSaveType> -+ <dataScope>data</dataScope> -+ <deps> -+ <dep>customer_address_form.customer_address_form_data_source</dep> -+ </deps> -+ </settings> -+ <dataSource name="customer_address_form_data_source"> -+ <argument name="data" xsi:type="array"> -+ <item name="js_config" xsi:type="array"> -+ <item name="component" xsi:type="string">Magento_Ui/js/form/provider</item> -+ </item> -+ </argument> -+ <settings> -+ <submitUrl path="customer/address/save"/> -+ <validateUrl path="customer/address/validate"/> -+ </settings> -+ <aclResource>Magento_Customer::manage</aclResource> -+ <dataProvider class="Magento\Customer\Model\Address\DataProvider" name="customer_address_form_data_source"> -+ <settings> -+ <requestFieldName>entity_id</requestFieldName> -+ <primaryFieldName>entity_id</primaryFieldName> -+ </settings> -+ </dataProvider> -+ </dataSource> -+ <container name="messages" component="Magento_Ui/js/form/components/html"> -+ <argument name="data" xsi:type="array"> -+ <item name="config" xsi:type="array"> -+ <item name="additionalClasses" xsi:type="string">message message-error</item> -+ <item name="visible" xsi:type="boolean">false</item> -+ <item name="imports" xsi:type="array"> -+ <item name="responseData" xsi:type="string">${ $.parentName }:responseData</item> -+ </item> -+ <item name="listens" xsi:type="array"> -+ <item name="responseData.error" xsi:type="string">visible</item> -+ <item name="responseData.messages" xsi:type="string">content</item> -+ </item> -+ </item> -+ </argument> -+ </container> -+ <fieldset name="general"> -+ <argument name="data" xsi:type="array"> -+ <item name="config" xsi:type="array"> -+ <item name="is_collection" xsi:type="boolean">true</item> -+ </item> -+ </argument> -+ <settings> -+ <label/> -+ <dataScope/> -+ </settings> -+ -+ <field name="entity_id" formElement="hidden"> -+ <settings> -+ <dataType>text</dataType> -+ </settings> -+ </field> -+ <field name="default_billing" sortOrder="5" formElement="checkbox"> -+ <argument name="data" xsi:type="array"> -+ <item name="config" xsi:type="array"> -+ <item name="default" xsi:type="number">0</item> -+ </item> -+ </argument> -+ <settings> -+ <dataType>boolean</dataType> -+ <label translate="true">Default Billing Address</label> -+ <dataScope>default_billing</dataScope> -+ </settings> -+ <formElements> -+ <checkbox> -+ <settings> -+ <valueMap> -+ <map name="false" xsi:type="number">0</map> -+ <map name="true" xsi:type="number">1</map> -+ </valueMap> -+ <prefer>toggle</prefer> -+ </settings> -+ </checkbox> -+ </formElements> -+ </field> -+ <field name="default_shipping" sortOrder="7" formElement="checkbox"> -+ <argument name="data" xsi:type="array"> -+ <item name="config" xsi:type="array"> -+ <item name="default" xsi:type="number">0</item> -+ </item> -+ </argument> -+ <settings> -+ <dataType>boolean</dataType> -+ <label translate="true">Default Shipping Address</label> -+ <dataScope>default_shipping</dataScope> -+ </settings> -+ <formElements> -+ <checkbox> -+ <settings> -+ <valueMap> -+ <map name="false" xsi:type="number">0</map> -+ <map name="true" xsi:type="number">1</map> -+ </valueMap> -+ <prefer>toggle</prefer> -+ </settings> -+ </checkbox> -+ </formElements> -+ </field> -+ <field name="prefix" sortOrder="10" formElement="input"> -+ <settings> -+ <dataType>text</dataType> -+ <visible>true</visible> -+ <label translate="true">Name Prefix</label> -+ </settings> -+ </field> -+ <field name="firstname" sortOrder="20" formElement="input"> -+ <settings> -+ <dataType>text</dataType> -+ <visible>true</visible> -+ <label translate="true">First Name</label> -+ <validation> -+ <rule name="required-entry" xsi:type="boolean">true</rule> -+ </validation> -+ </settings> -+ </field> -+ <field name="lastname" sortOrder="30" formElement="input"> -+ <settings> -+ <dataType>text</dataType> -+ <visible>true</visible> -+ <label translate="true">Last Name</label> -+ <validation> -+ <rule name="required-entry" xsi:type="boolean">true</rule> -+ </validation> -+ </settings> -+ </field> -+ <field name="suffix" sortOrder="40" formElement="input"> -+ <settings> -+ <dataType>text</dataType> -+ <visible>true</visible> -+ <label translate="true">Name Suffix</label> -+ </settings> -+ </field> -+ <field name="middlename" sortOrder="50" formElement="input"> -+ <settings> -+ <dataType>text</dataType> -+ <visible>true</visible> -+ <label translate="true">Middle Name/Initial</label> -+ </settings> -+ </field> -+ <field name="company" sortOrder="60" formElement="input"> -+ <settings> -+ <dataType>text</dataType> -+ <visible>true</visible> -+ <label translate="true">Company</label> -+ </settings> -+ </field> -+ <field name="city" sortOrder="80" formElement="input"> -+ <settings> -+ <dataType>text</dataType> -+ <label translate="true">City</label> -+ <visible>true</visible> -+ <validation> -+ <rule name="required-entry" xsi:type="boolean">true</rule> -+ </validation> -+ </settings> -+ </field> -+ <field name="country_id" component="Magento_Customer/js/form/element/country" sortOrder="90" formElement="select"> -+ <settings> -+ <validation> -+ <rule name="required-entry" xsi:type="boolean">true</rule> -+ </validation> -+ <dataType>text</dataType> -+ </settings> -+ <formElements> -+ <select> -+ <settings> -+ <options class="Magento\Directory\Model\ResourceModel\Country\Collection"/> -+ </settings> -+ </select> -+ </formElements> -+ </field> -+ <field name="region_id" component="Magento_Customer/js/form/element/region" formElement="select"> -+ <settings> -+ <validation> -+ <rule name="required-entry" xsi:type="boolean">true</rule> -+ </validation> -+ <dataType>text</dataType> -+ <label translate="true">State/Province</label> -+ </settings> -+ <formElements> -+ <select> -+ <settings> -+ <filterBy> -+ <field>country_id</field> -+ <target>${ $.provider }:${ $.parentScope }.country_id</target> -+ </filterBy> -+ <customEntry>region</customEntry> -+ </settings> -+ </select> -+ </formElements> -+ </field> -+ <field name="postcode" component="Magento_Ui/js/form/element/post-code" sortOrder="120" formElement="input"> -+ <settings> -+ <dataType>text</dataType> -+ <visible>true</visible> -+ <label translate="true">Zip/Postal Code</label> -+ <validation> -+ <rule name="required-entry" xsi:type="boolean">true</rule> -+ </validation> -+ </settings> -+ </field> -+ <field name="telephone" sortOrder="130" formElement="input"> -+ <settings> -+ <dataType>text</dataType> -+ <visible>true</visible> -+ <label translate="true">Phone Number</label> -+ </settings> -+ </field> -+ <field name="vat_id" sortOrder="140" formElement="input"> -+ <settings> -+ <dataType>text</dataType> -+ <label translate="true">VAT Number</label> -+ <validation> -+ <rule name="validate-alphanum" xsi:type="boolean">true</rule> -+ </validation> -+ </settings> -+ </field> -+ </fieldset> -+</form> -diff --git a/app/code/Magento/Customer/view/adminhtml/ui_component/customer_address_listing.xml b/app/code/Magento/Customer/view/adminhtml/ui_component/customer_address_listing.xml -new file mode 100644 -index 00000000000..fb42a2c5a07 ---- /dev/null -+++ b/app/code/Magento/Customer/view/adminhtml/ui_component/customer_address_listing.xml -@@ -0,0 +1,162 @@ -+<?xml version="1.0" encoding="UTF-8"?> -+<!-- -+/** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+--> -+<listing xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Ui:etc/ui_configuration.xsd"> -+ <argument name="data" xsi:type="array"> -+ <item name="js_config" xsi:type="array"> -+ <item name="provider" xsi:type="string">customer_address_listing.customer_address_listing_data_source</item> -+ </item> -+ </argument> -+ <settings> -+ <spinner>customer_address_columns</spinner> -+ <deps> -+ <dep>customer_address_listing.customer_address_listing_data_source</dep> -+ </deps> -+ </settings> -+ <dataSource name="customer_address_listing_data_source" component="Magento_Ui/js/grid/provider"> -+ <settings> -+ <filterUrlParams> -+ <param name="id">*</param> -+ </filterUrlParams> -+ <storageConfig> -+ <param name="indexField" xsi:type="string">entity_id</param> -+ </storageConfig> -+ <updateUrl path="mui/index/render"/> -+ </settings> -+ <aclResource>Magento_Customer::manage</aclResource> -+ <dataProvider class="Magento\Customer\Ui\Component\Listing\Address\DataProvider" name="customer_address_listing_data_source"> -+ <settings> -+ <requestFieldName>id</requestFieldName> -+ <primaryFieldName>entity_id</primaryFieldName> -+ </settings> -+ </dataProvider> -+ </dataSource> -+ <listingToolbar name="listing_top"> -+ <bookmark name="bookmarks"/> -+ <columnsControls name="columns_controls"/> -+ <filterSearch name="fulltext"/> -+ <filters name="listing_filters" component="Magento_Customer/js/grid/filters/filters"> -+ <settings> -+ <storageConfig> -+ <param name="provider" xsi:type="string">customer_address_listing.customer_address_listing.listing_top.bookmarks</param> -+ <param name="namespace" xsi:type="string">current.filters</param> -+ </storageConfig> -+ <childDefaults> -+ <param name="provider" xsi:type="string">customer_address_listing.customer_address_listing.listing_top.listing_filters</param> -+ <param name="imports" xsi:type="array"> -+ <item name="visible" xsi:type="string">customer_address_listing.customer_address_listing.listing_top.bookmarks:current.columns.${ $.index }.visible</item> -+ </param> -+ </childDefaults> -+ </settings> -+ </filters> -+ <massaction name="listing_massaction" component="Magento_Customer/js/grid/massactions"> -+ <action name="delete"> -+ <argument name="data" xsi:type="array"> -+ <item name="config" xsi:type="array"> -+ <item name="isAjax" xsi:type="boolean">true</item> -+ </item> -+ </argument> -+ <settings> -+ <url path="customer/address/massDelete"/> -+ <type>delete</type> -+ <label translate="true">Delete</label> -+ <confirm> -+ <message translate="true">Are you sure to delete selected address?</message> -+ <title translate="true">Delete items -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ false -+ -+ entity_id -+ true -+ customer_address_listing.customer_address_listing.address_columns.ids -+ -+ -+ -+ customer_address_listing.customer_address_listing.address_columns_editor -+ startEdit -+ -+ ${ $.$data.rowIndex } -+ true -+ -+ -+ -+ -+ -+ -+ entity_id -+ -+ -+ -+ -+ text -+ -+ -+ -+ -+ -+ text -+ -+ -+ -+ -+ -+ text -+ -+ -+ -+ -+ -+ text -+ -+ -+ -+ -+ -+ text -+ -+ -+ -+ -+ -+ text -+ -+ -+ -+ -+ -+ select -+ -+ select -+ -+ -+ -+ -+ -+ text -+ -+ text -+ -+ -+ -+ -+ -+ -+ entity_id -+ -+ -+ -+ -diff --git a/app/code/Magento/Customer/view/adminhtml/ui_component/customer_listing.xml b/app/code/Magento/Customer/view/adminhtml/ui_component/customer_listing.xml -index f8aa078f45e..97ae9a9953e 100644 ---- a/app/code/Magento/Customer/view/adminhtml/ui_component/customer_listing.xml -+++ b/app/code/Magento/Customer/view/adminhtml/ui_component/customer_listing.xml -@@ -49,7 +49,7 @@ - - - -- Are you sure to delete selected customers? -+ Are you sure you want to delete the selected customers? - Delete items - - -@@ -67,7 +67,7 @@ - - - -- Are you sure to unsubscribe selected customers from newsletter? -+ Are you sure you want to unsubscribe the selected customers from the newsletter? - Unsubscribe from Newsletter - - -@@ -157,23 +157,18 @@ - - - text -- -- text -- - - - - - - text -- -- text -- - - - - - -+ - select - select - -@@ -268,9 +263,6 @@ - - - text -- -- text -- - - false - -@@ -278,9 +270,6 @@ - - - text -- -- text -- - - false - -@@ -288,9 +277,6 @@ - - - text -- -- text -- - - false - -@@ -298,9 +284,6 @@ - - - text -- -- text -- - - false - -@@ -308,9 +291,6 @@ - - - text -- -- text -- - - false - -@@ -318,9 +298,6 @@ - - - text -- -- text -- - - false - -diff --git a/app/code/Magento/Customer/view/adminhtml/web/js/address/default-address.js b/app/code/Magento/Customer/view/adminhtml/web/js/address/default-address.js -new file mode 100644 -index 00000000000..07662cd9b10 ---- /dev/null -+++ b/app/code/Magento/Customer/view/adminhtml/web/js/address/default-address.js -@@ -0,0 +1,50 @@ -+/** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+ -+define([ -+ 'Magento_Ui/js/form/components/button', -+ 'underscore' -+], function (Button, _) { -+ 'use strict'; -+ -+ return Button.extend({ -+ defaults: { -+ entityId: null, -+ parentId: null, -+ listens: { -+ entity: 'changeVisibility' -+ } -+ }, -+ -+ /** -+ * Apply action on target component, -+ * but previously create this component from template if it is not existed -+ * -+ * @param {Object} action - action configuration -+ */ -+ applyAction: function (action) { -+ if (action.params && action.params[0]) { -+ action.params[0]['entity_id'] = this.entityId; -+ action.params[0]['parent_id'] = this.parentId; -+ } else { -+ action.params = [{ -+ 'entity_id': this.entityId, -+ 'parent_id': this.parentId -+ }]; -+ } -+ -+ this._super(); -+ }, -+ -+ /** -+ * Change visibility of the default address shipping/billing blocks -+ * -+ * @param {Object} entity - customer address -+ */ -+ changeVisibility: function (entity) { -+ this.visible(!_.isEmpty(entity)); -+ } -+ }); -+}); -diff --git a/app/code/Magento/Customer/view/adminhtml/web/js/form/components/form.js b/app/code/Magento/Customer/view/adminhtml/web/js/form/components/form.js -new file mode 100644 -index 00000000000..812dbe5975c ---- /dev/null -+++ b/app/code/Magento/Customer/view/adminhtml/web/js/form/components/form.js -@@ -0,0 +1,81 @@ -+/** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+define([ -+ 'jquery', -+ 'Magento_Ui/js/modal/alert', -+ 'Magento_Ui/js/modal/confirm', -+ 'Magento_Ui/js/form/form', -+ 'underscore', -+ 'mage/translate' -+], function ($, uiAlert, uiConfirm, Form, _, $t) { -+ 'use strict'; -+ -+ return Form.extend({ -+ defaults: { -+ deleteConfirmationMessage: '', -+ ajaxSettings: { -+ method: 'POST', -+ dataType: 'json' -+ } -+ }, -+ -+ /** -+ * Delete customer address by provided url. -+ * Will call confirmation message to be sure that user is really wants to delete this address -+ * -+ * @param {String} url - ajax url -+ */ -+ deleteAddress: function (url) { -+ var that = this; -+ -+ uiConfirm({ -+ content: this.deleteConfirmationMessage, -+ actions: { -+ /** @inheritdoc */ -+ confirm: function () { -+ that._delete(url); -+ } -+ } -+ }); -+ }, -+ -+ /** -+ * Perform asynchronous DELETE request to server. -+ * @param {String} url - ajax url -+ * @returns {Deferred} -+ */ -+ _delete: function (url) { -+ var settings = _.extend({}, this.ajaxSettings, { -+ url: url, -+ data: { -+ 'form_key': window.FORM_KEY -+ } -+ }), -+ that = this; -+ -+ $('body').trigger('processStart'); -+ -+ return $.ajax(settings) -+ .done(function (response) { -+ if (response.error) { -+ uiAlert({ -+ content: response.message -+ }); -+ } else { -+ that.trigger('deleteAddressAction', that.source.get('data.entity_id')); -+ } -+ }) -+ .fail(function () { -+ uiAlert({ -+ content: $t('Sorry, there has been an error processing your request. Please try again later.') -+ }); -+ }) -+ .always(function () { -+ $('body').trigger('processStop'); -+ }); -+ -+ } -+ }); -+}); -diff --git a/app/code/Magento/Customer/view/adminhtml/web/js/form/components/insert-form.js b/app/code/Magento/Customer/view/adminhtml/web/js/form/components/insert-form.js -new file mode 100644 -index 00000000000..a1ffbde62d4 ---- /dev/null -+++ b/app/code/Magento/Customer/view/adminhtml/web/js/form/components/insert-form.js -@@ -0,0 +1,80 @@ -+/** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+ -+define([ -+ 'Magento_Ui/js/form/components/insert-form' -+], function (Insert) { -+ 'use strict'; -+ -+ return Insert.extend({ -+ defaults: { -+ listens: { -+ responseData: 'onResponse' -+ }, -+ modules: { -+ addressListing: '${ $.addressListingProvider }', -+ addressModal: '${ $.addressModalProvider }' -+ } -+ }, -+ -+ /** -+ * Close modal, reload customer address listing and save customer address -+ * -+ * @param {Object} responseData -+ */ -+ onResponse: function (responseData) { -+ var data; -+ -+ if (!responseData.error) { -+ this.addressModal().closeModal(); -+ this.addressListing().reload({ -+ refresh: true -+ }); -+ data = this.externalSource().get('data'); -+ this.saveAddress(responseData, data); -+ } -+ }, -+ -+ /** -+ * Save customer address to customer form data source -+ * -+ * @param {Object} responseData -+ * @param {Object} data - customer address -+ */ -+ saveAddress: function (responseData, data) { -+ data['entity_id'] = responseData.data['entity_id']; -+ -+ if (parseFloat(data['default_billing'])) { -+ this.source.set('data.default_billing_address', data); -+ } else if ( -+ parseFloat(this.source.get('data.default_billing_address')['entity_id']) === data['entity_id'] -+ ) { -+ this.source.set('data.default_billing_address', []); -+ } -+ -+ if (parseFloat(data['default_shipping'])) { -+ this.source.set('data.default_shipping_address', data); -+ } else if ( -+ parseFloat(this.source.get('data.default_shipping_address')['entity_id']) === data['entity_id'] -+ ) { -+ this.source.set('data.default_shipping_address', []); -+ } -+ }, -+ -+ /** -+ * Event method that closes "Edit customer address" modal and refreshes grid after customer address -+ * was removed through "Delete" button on the "Edit customer address" modal -+ * -+ * @param {String} id - customer address ID to delete -+ */ -+ onAddressDelete: function (id) { -+ this.addressModal().closeModal(); -+ this.addressListing().reload({ -+ refresh: true -+ }); -+ this.addressListing()._delete([parseFloat(id)]); -+ } -+ }); -+}); -diff --git a/app/code/Magento/Customer/view/adminhtml/web/js/form/components/insert-listing.js b/app/code/Magento/Customer/view/adminhtml/web/js/form/components/insert-listing.js -new file mode 100644 -index 00000000000..912d4b32130 ---- /dev/null -+++ b/app/code/Magento/Customer/view/adminhtml/web/js/form/components/insert-listing.js -@@ -0,0 +1,90 @@ -+/** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+ -+define([ -+ 'Magento_Ui/js/form/components/insert-listing', -+ 'underscore' -+], function (Insert, _) { -+ 'use strict'; -+ -+ return Insert.extend({ -+ -+ /** -+ * On action call -+ * -+ * @param {Object} data - customer address and actions -+ */ -+ onAction: function (data) { -+ this[data.action + 'Action'].call(this, data.data); -+ }, -+ -+ /** -+ * On mass action call -+ * -+ * @param {Object} data - customer address -+ */ -+ onMassAction: function (data) { -+ this[data.action + 'Massaction'].call(this, data.data); -+ }, -+ -+ /** -+ * Set default billing address -+ * -+ * @param {Object} data - customer address -+ */ -+ setDefaultBillingAction: function (data) { -+ this.source.set('data.default_billing_address', data); -+ }, -+ -+ /** -+ * Set default shipping address -+ * -+ * @param {Object} data - customer address -+ */ -+ setDefaultShippingAction: function (data) { -+ this.source.set('data.default_shipping_address', data); -+ }, -+ -+ /** -+ * Delete customer address -+ * -+ * @param {Object} data - customer address -+ */ -+ deleteAction: function (data) { -+ this._delete([parseFloat(data[data['id_field_name']])]); -+ }, -+ -+ /** -+ * Mass action delete -+ * -+ * @param {Object} data - customer address -+ */ -+ deleteMassaction: function (data) { -+ var ids = _.map(data, function (val) { -+ return parseFloat(val); -+ }); -+ -+ this._delete(ids); -+ }, -+ -+ /** -+ * Delete customer address by ids -+ * -+ * @param {Array} ids -+ */ -+ _delete: function (ids) { -+ var defaultShippingId = parseFloat(this.source.get('data.default_shipping_address.entity_id')), -+ defaultBillingId = parseFloat(this.source.get('data.default_billing_address.entity_id')); -+ -+ if (ids.indexOf(defaultShippingId) !== -1) { -+ this.source.set('data.default_shipping_address', []); -+ } -+ -+ if (ids.indexOf(defaultBillingId) !== -1) { -+ this.source.set('data.default_billing_address', []); -+ } -+ } -+ }); -+}); -diff --git a/app/code/Magento/Customer/view/adminhtml/web/js/form/element/country.js b/app/code/Magento/Customer/view/adminhtml/web/js/form/element/country.js -new file mode 100644 -index 00000000000..1ed30ae388b ---- /dev/null -+++ b/app/code/Magento/Customer/view/adminhtml/web/js/form/element/country.js -@@ -0,0 +1,29 @@ -+/** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+ -+define([ -+ 'Magento_Ui/js/form/element/country' -+], function (Country) { -+ 'use strict'; -+ -+ return Country.extend({ -+ defaults: { -+ countryScope: 'data.country' -+ }, -+ -+ /** -+ * Set country to customer address form -+ * -+ * @param {String} value - country -+ */ -+ setDifferedFromDefault: function (value) { -+ this._super(); -+ -+ if (value) { -+ this.source.set(this.countryScope, this.indexedOptions[value].label); -+ } -+ } -+ }); -+}); -diff --git a/app/code/Magento/Customer/view/adminhtml/web/js/form/element/region.js b/app/code/Magento/Customer/view/adminhtml/web/js/form/element/region.js -new file mode 100644 -index 00000000000..755a8e6df3d ---- /dev/null -+++ b/app/code/Magento/Customer/view/adminhtml/web/js/form/element/region.js -@@ -0,0 +1,29 @@ -+/** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+ -+define([ -+ 'Magento_Ui/js/form/element/region' -+], function (Region) { -+ 'use strict'; -+ -+ return Region.extend({ -+ defaults: { -+ regionScope: 'data.region' -+ }, -+ -+ /** -+ * Set region to customer address form -+ * -+ * @param {String} value - region -+ */ -+ setDifferedFromDefault: function (value) { -+ this._super(); -+ -+ if (parseFloat(value)) { -+ this.source.set(this.regionScope, this.indexedOptions[value].label); -+ } -+ } -+ }); -+}); -diff --git a/app/code/Magento/Customer/view/adminhtml/web/js/grid/columns/actions.js b/app/code/Magento/Customer/view/adminhtml/web/js/grid/columns/actions.js -new file mode 100644 -index 00000000000..66ef89c9413 ---- /dev/null -+++ b/app/code/Magento/Customer/view/adminhtml/web/js/grid/columns/actions.js -@@ -0,0 +1,103 @@ -+/** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+ -+define([ -+ 'Magento_Ui/js/grid/columns/actions', -+ 'Magento_Ui/js/modal/alert', -+ 'underscore', -+ 'jquery', -+ 'mage/translate' -+], function (Actions, uiAlert, _, $, $t) { -+ 'use strict'; -+ -+ return Actions.extend({ -+ defaults: { -+ ajaxSettings: { -+ method: 'POST', -+ dataType: 'json' -+ }, -+ listens: { -+ action: 'onAction' -+ } -+ }, -+ -+ /** -+ * Reload customer address listing data source after customer address delete action -+ * -+ * @param {Object} data -+ */ -+ onAction: function (data) { -+ if (data.action === 'delete') { -+ this.source().reload({ -+ refresh: true -+ }); -+ } -+ }, -+ -+ /** -+ * Default action callback. Redirects to -+ * the specified in action's data url. -+ * -+ * @param {String} actionIndex - Action's identifier. -+ * @param {(Number|String)} recordId - Id of the record associated -+ * with a specified action. -+ * @param {Object} action - Action's data. -+ */ -+ defaultCallback: function (actionIndex, recordId, action) { -+ if (action.isAjax) { -+ this.request(action.href).done(function (response) { -+ var data; -+ -+ if (!response.error) { -+ data = _.findWhere(this.rows, { -+ _rowIndex: action.rowIndex -+ }); -+ -+ this.trigger('action', { -+ action: actionIndex, -+ data: data -+ }); -+ } -+ }.bind(this)); -+ -+ } else { -+ this._super(); -+ } -+ }, -+ -+ /** -+ * Send customer address listing ajax request -+ * -+ * @param {String} href -+ */ -+ request: function (href) { -+ var settings = _.extend({}, this.ajaxSettings, { -+ url: href, -+ data: { -+ 'form_key': window.FORM_KEY -+ } -+ }); -+ -+ $('body').trigger('processStart'); -+ -+ return $.ajax(settings) -+ .done(function (response) { -+ if (response.error) { -+ uiAlert({ -+ content: response.message -+ }); -+ } -+ }) -+ .fail(function () { -+ uiAlert({ -+ content: $t('Sorry, there has been an error processing your request. Please try again later.') -+ }); -+ }) -+ .always(function () { -+ $('body').trigger('processStop'); -+ }); -+ } -+ }); -+}); -diff --git a/app/code/Magento/Customer/view/adminhtml/web/js/grid/filters/chips.js b/app/code/Magento/Customer/view/adminhtml/web/js/grid/filters/chips.js -new file mode 100644 -index 00000000000..5dbf62a209b ---- /dev/null -+++ b/app/code/Magento/Customer/view/adminhtml/web/js/grid/filters/chips.js -@@ -0,0 +1,23 @@ -+/** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+ -+define([ -+ 'Magento_Ui/js/grid/filters/chips' -+], function (Chips) { -+ 'use strict'; -+ -+ return Chips.extend({ -+ -+ /** -+ * Clear previous filters while initializing element to prevent filters sharing between customers -+ * -+ * @param {Object} elem -+ */ -+ initElement: function (elem) { -+ this.clear(); -+ this._super(elem); -+ } -+ }); -+}); -diff --git a/app/code/Magento/Customer/view/adminhtml/web/js/grid/filters/filters.js b/app/code/Magento/Customer/view/adminhtml/web/js/grid/filters/filters.js -new file mode 100644 -index 00000000000..f8bf7d3d7ec ---- /dev/null -+++ b/app/code/Magento/Customer/view/adminhtml/web/js/grid/filters/filters.js -@@ -0,0 +1,20 @@ -+/** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+ -+define([ -+ 'Magento_Ui/js/grid/filters/filters' -+], function (Filters) { -+ 'use strict'; -+ -+ return Filters.extend({ -+ defaults: { -+ chipsConfig: { -+ name: '${ $.name }_chips', -+ provider: '${ $.chipsConfig.name }', -+ component: 'Magento_Customer/js/grid/filters/chips' -+ } -+ } -+ }); -+}); -diff --git a/app/code/Magento/Customer/view/adminhtml/web/js/grid/massactions.js b/app/code/Magento/Customer/view/adminhtml/web/js/grid/massactions.js -new file mode 100644 -index 00000000000..384f48554a9 ---- /dev/null -+++ b/app/code/Magento/Customer/view/adminhtml/web/js/grid/massactions.js -@@ -0,0 +1,106 @@ -+/** -+ * Copyright © Magento, Inc. All rights reserved. -+ * See COPYING.txt for license details. -+ */ -+ -+define([ -+ 'Magento_Ui/js/grid/massactions', -+ 'Magento_Ui/js/modal/alert', -+ 'underscore', -+ 'jquery', -+ 'mage/translate' -+], function (Massactions, uiAlert, _, $, $t) { -+ 'use strict'; -+ -+ return Massactions.extend({ -+ defaults: { -+ ajaxSettings: { -+ method: 'POST', -+ dataType: 'json' -+ }, -+ listens: { -+ massaction: 'onAction' -+ } -+ }, -+ -+ /** -+ * Reload customer addresses listing -+ * -+ * @param {Object} data -+ */ -+ onAction: function (data) { -+ if (data.action === 'delete') { -+ this.source.reload({ -+ refresh: true -+ }); -+ } -+ }, -+ -+ /** -+ * Default action callback. Send selections data -+ * via POST request. -+ * -+ * @param {Object} action - Action data. -+ * @param {Object} data - Selections data. -+ */ -+ defaultCallback: function (action, data) { -+ var itemsType, selections; -+ -+ if (action.isAjax) { -+ itemsType = data.excludeMode ? 'excluded' : 'selected'; -+ selections = {}; -+ -+ selections[itemsType] = data[itemsType]; -+ -+ if (!selections[itemsType].length) { -+ selections[itemsType] = false; -+ } -+ -+ _.extend(selections, data.params || {}); -+ -+ this.request(action.url, selections).done(function (response) { -+ if (!response.error) { -+ this.trigger('massaction', { -+ action: action.type, -+ data: selections -+ }); -+ } -+ }.bind(this)); -+ } else { -+ this._super(); -+ } -+ }, -+ -+ /** -+ * Send customer address listing mass action ajax request -+ * -+ * @param {String} href -+ * @param {Object} data -+ */ -+ request: function (href, data) { -+ var settings = _.extend({}, this.ajaxSettings, { -+ url: href, -+ data: data -+ }); -+ -+ $('body').trigger('processStart'); -+ -+ return $.ajax(settings) -+ .done(function (response) { -+ if (response.error) { -+ uiAlert({ -+ content: response.message -+ }); -+ } -+ }) -+ .fail(function () { -+ uiAlert({ -+ content: $t('Sorry, there has been an error processing your request. Please try again later.') -+ }); -+ }) -+ .always(function () { -+ $('body').trigger('processStop'); -+ }); -+ } -+ }); -+}); -diff --git a/app/code/Magento/Customer/view/adminhtml/web/template/default-address-wrapper.html b/app/code/Magento/Customer/view/adminhtml/web/template/default-address-wrapper.html -new file mode 100644 -index 00000000000..2af366b0334 ---- /dev/null -+++ b/app/code/Magento/Customer/view/adminhtml/web/template/default-address-wrapper.html -@@ -0,0 +1,7 @@ -+ -+
-diff --git a/app/code/Magento/Customer/view/adminhtml/web/template/default-address.html b/app/code/Magento/Customer/view/adminhtml/web/template/default-address.html -new file mode 100644 -index 00000000000..8d68b93445b ---- /dev/null -+++ b/app/code/Magento/Customer/view/adminhtml/web/template/default-address.html -@@ -0,0 +1,47 @@ -+
-+
-+
-+
-+ -+ -+ -+ -+
-+
-+ -+ -+ -+ -+ -+
-+ -+ -+
-+
-+ -+ -+ -+
-+
-+ -+ -+ -+ -+
-+
-+ -+ -+
T: -+
-+ -+
F: -+
-+ -+
VAT: -+
-+
-+ -+
-+
-+
-diff --git a/app/code/Magento/Customer/view/base/ui_component/customer_form.xml b/app/code/Magento/Customer/view/base/ui_component/customer_form.xml -index 4de6644b948..5fb8b17dbb8 100644 ---- a/app/code/Magento/Customer/view/base/ui_component/customer_form.xml -+++ b/app/code/Magento/Customer/view/base/ui_component/customer_form.xml -@@ -43,7 +43,7 @@ - - - -- -+ - - id - entity_id -@@ -74,6 +74,17 @@ - false - - -+ -+ -+ -+ customer -+ -+ -+ -+ text -+ false -+ -+ - - - -@@ -118,7 +129,7 @@ - - number - -- http://docs.magento.com/m2/ce/user_guide/configuration/scope.html -+ https://docs.magento.com/m2/ce/user_guide/configuration/scope.html - If your Magento installation has multiple websites, you can edit the scope to associate the customer with a specific site. - - -@@ -303,265 +314,196 @@ - - - --
-- -- -- true -- Are you sure you want to delete this item? -- -- -+
- -+ false - -+ fieldset -+ -+ true -+ - -- -- -- -- address -- -- -- -- number -- false -- -- -- -- -- -- address -- -- -- -- text -- true -- -- -- -- -- -- address -- -- -- -- -- true -- -- text -- -- ${ $.provider }:data.customer.firstname -- -- -- -- -- -- -- address -- -- -- -- text -- true -- -- -- -- -- -- address -+ -+ -+ -+ -+ billing-address -+ Default Billing Address -+ customer-default-billing-address-content -+ The customer does not have default billing address -+ -+ true -+ -+ -+ -+ -+ -+ ${ $.provider}:data.default_billing_address -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ shipping-address -+ Default Shipping Address -+ customer-default-shipping-address-content -+ The customer does not have default shipping address -+ -+ true -+ -+ -+ -+ -+ -+ ${ $.provider}:data.default_shipping_address -+ -+ -+ -+ -+ -+ -+ - -- -- 0 -- -- text -+ -+ -+ - -- -- -- -- -- address -- -- -- -- -- true -- -- text -- -- -- -- -- -- address -- -- -- -- -- true -- -- text -+ -+ -+ -+ ns = customer_address_listing, index = customer_address_listing -+ ${ $.parentName } -+ -+ -+ -+ ajax -+ -+ customer_address_edit -+ 1 -+ -+ false -+ ${ $.parentName } -+ ${ $.ns }.customer_address_form_data_source -+ customer_address_form -+ -+ ${ $.externalProvider }:data.parent_id -+ -+ -+ ${ $.provider}:data.customer_id -+ ${ $.ns }.${ $.ns }:deleteAddressAction -+ -+ -+ -+ -+ -+ -+ -+ false -+ true -+ -+ customer_address_listing.customer_address_listing_data_source -+ customer_address_listing.customer_address_listing.customer_address_listing_columns.ids -+ true -+ customer_address_listing -+ customer_address_listing -+ -+ ${ $.externalProvider }:params.parent_id -+ - -- ${ $.provider }:data.customer.website_id -+ ${ $.provider }:data.customer.entity_id -+ ns = ${ $.ns }, index = actions:action -+ ns = ${ $.ns }, index = listing_massaction:massaction - - -- -- -- -- -- address -- -- -- -- text -- false -- -- -- -- -- -- address -- -- -- -- -- true -- -- text -- -- -- -- -- -- -- -- -- address -- -- -- -- -- true -- -- text -- -- -- -- -- -- address -- -- -- -- -- true -- -- text -- -- -- -- -- -- address -- -- -- -- -- 0 -- -- text -- -- -- -- -- -- address -- -- -- -- text -- -- -- -- -- -- default_billing -- value -- -- customer -- default_billing -- entity_id -- -- -- -- -- ui/form/element/checkbox -- boolean -- -- -- -- -- Default Billing Address -- -- -- -- -- -- -- -- default_shipping -- value -- -- customer -- default_shipping -- entity_id -- -- -- -- -- ui/form/element/checkbox -- boolean -- -- -- -- -- Default Shipping Address -- -- -- -- -+ -
- -diff --git a/app/code/Magento/Customer/view/frontend/email/password_reset_confirmation.html b/app/code/Magento/Customer/view/frontend/email/password_reset_confirmation.html -index 6c17762a882..59e7f16adfd 100644 ---- a/app/code/Magento/Customer/view/frontend/email/password_reset_confirmation.html -+++ b/app/code/Magento/Customer/view/frontend/email/password_reset_confirmation.html -@@ -21,7 +21,7 @@ - - - - -
-- {{trans "Set a New Password"}} -+ {{trans "Set a New Password"}} -
-diff --git a/app/code/Magento/Customer/view/frontend/layout/customer_account_create.xml b/app/code/Magento/Customer/view/frontend/layout/customer_account_create.xml -index 8f65d54f458..0c5af453f23 100644 ---- a/app/code/Magento/Customer/view/frontend/layout/customer_account_create.xml -+++ b/app/code/Magento/Customer/view/frontend/layout/customer_account_create.xml -@@ -6,12 +6,18 @@ - */ - --> - -+ -+ Create New Customer Account -+ - - - - - - -+ -+ Magento\Customer\Block\DataProviders\AddressAttributeData -+ - - - -diff --git a/app/code/Magento/Customer/view/frontend/layout/customer_account_forgotpassword.xml b/app/code/Magento/Customer/view/frontend/layout/customer_account_forgotpassword.xml -index 9a701c14a03..24cede5f023 100644 ---- a/app/code/Magento/Customer/view/frontend/layout/customer_account_forgotpassword.xml -+++ b/app/code/Magento/Customer/view/frontend/layout/customer_account_forgotpassword.xml -@@ -7,7 +7,7 @@ - --> - - -- Forgot Your Password -+ Forgot Your Password? - - - -diff --git a/app/code/Magento/Customer/view/frontend/layout/customer_account_login.xml b/app/code/Magento/Customer/view/frontend/layout/customer_account_login.xml -index d49dae6dee5..3518df736c4 100644 ---- a/app/code/Magento/Customer/view/frontend/layout/customer_account_login.xml -+++ b/app/code/Magento/Customer/view/frontend/layout/customer_account_login.xml -@@ -6,6 +6,9 @@ - */ - --> - -+ -+ Customer Login -+ - - - -diff --git a/app/code/Magento/Customer/view/frontend/layout/customer_address_form.xml b/app/code/Magento/Customer/view/frontend/layout/customer_address_form.xml -index 194dfd1bc7d..f5ee2b347a5 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 -@@ -17,7 +17,12 @@ - - - -- -+ -+ -+ Magento\Customer\Block\DataProviders\AddressAttributeData -+ Magento\Customer\Block\DataProviders\PostCodesPatternsAttributeData -+ -+ - - - -diff --git a/app/code/Magento/Customer/view/frontend/layout/customer_address_index.xml b/app/code/Magento/Customer/view/frontend/layout/customer_address_index.xml -index bad120e4627..2c5e5b98e5f 100644 ---- a/app/code/Magento/Customer/view/frontend/layout/customer_address_index.xml -+++ b/app/code/Magento/Customer/view/frontend/layout/customer_address_index.xml -@@ -13,6 +13,7 @@ - - - -+ - - - -diff --git a/app/code/Magento/Customer/view/frontend/requirejs-config.js b/app/code/Magento/Customer/view/frontend/requirejs-config.js -index 20dd53ded11..f1bf5c1d1b6 100644 ---- a/app/code/Magento/Customer/view/frontend/requirejs-config.js -+++ b/app/code/Magento/Customer/view/frontend/requirejs-config.js -@@ -7,11 +7,13 @@ var config = { - map: { - '*': { - checkoutBalance: 'Magento_Customer/js/checkout-balance', -- address: 'Magento_Customer/address', -- changeEmailPassword: 'Magento_Customer/change-email-password', -+ address: 'Magento_Customer/js/address', -+ changeEmailPassword: 'Magento_Customer/js/change-email-password', - passwordStrengthIndicator: 'Magento_Customer/js/password-strength-indicator', - zxcvbn: 'Magento_Customer/js/zxcvbn', -- addressValidation: 'Magento_Customer/js/addressValidation' -+ addressValidation: 'Magento_Customer/js/addressValidation', -+ 'Magento_Customer/address': 'Magento_Customer/js/address', -+ 'Magento_Customer/change-email-password': 'Magento_Customer/js/change-email-password' - } - } - }; -diff --git a/app/code/Magento/Customer/view/frontend/templates/account/authentication-popup.phtml b/app/code/Magento/Customer/view/frontend/templates/account/authentication-popup.phtml -index ca7393f2129..0d4cf3c721d 100644 ---- a/app/code/Magento/Customer/view/frontend/templates/account/authentication-popup.phtml -+++ b/app/code/Magento/Customer/view/frontend/templates/account/authentication-popup.phtml -@@ -4,8 +4,6 @@ - * See COPYING.txt for license details. - */ - --// @codingStandardsIgnoreFile -- - /** @var \Magento\Customer\Block\Account\AuthenticationPopup $block */ - ?> -
-
- escapeHtml(__('Address')) ?>
-- helper('Magento\Customer\Helper\Address')->getAttributeValidationClass('street'); ?> -+ helper(\Magento\Customer\Helper\Address::class)->getAttributeValidationClass('street'); ?> -
- -
- -
- -- helper('Magento\Customer\Helper\Address')->getStreetLines(); $_i < $_n; $_i++): ?> -+ helper(\Magento\Customer\Helper\Address::class)->getStreetLines(); $_i < $_n; $_i++) : ?> -
-
-
- -- helper('Magento\Customer\Helper\Address')->isVatAttributeVisible()) : ?> -+ helper(\Magento\Customer\Helper\Address::class)->isVatAttributeVisible()) : ?> -
- -
- -
-
- -
-- -+ -
- -
-
-
- -
- - getConfig('general/region/display_all') ? ' disabled="disabled"' : '' ?>/> -+ title="getAttributeData()->getFrontendLabel('region') ?>" -+ class="input-text validate-not-number-first escapeHtmlAttr($this->helper(\Magento\Customer\Helper\Address::class)->getAttributeValidationClass('region')) ?>"getConfig('general/region/display_all') ? ' disabled="disabled"' : '' ?>/> -
-
-
- -
- -+ class="input-text validate-zip-international escapeHtmlAttr($this->helper(\Magento\Customer\Helper\Address::class)->getAttributeValidationClass('postcode')) ?>"> -+ -
-
-
-- -+ -
- getCountryHtmlSelect() ?> -
-
- -- isDefaultBilling()): ?> -+ isDefaultBilling()) : ?> -
- escapeHtml(__("It's a default billing address.")) ?> -
-- canSetAsDefaultBilling()): ?> -+ canSetAsDefaultBilling()) : ?> -
- - -
-- -+ - - - -- isDefaultShipping()): ?> -+ isDefaultShipping()) : ?> -
- escapeHtml(__("It's a default shipping address.")) ?> -
-- canSetAsDefaultShipping()): ?> -+ canSetAsDefaultShipping()) : ?> -
- - -
-- -+ - - -
-@@ -184,7 +185,9 @@ - -diff --git a/app/code/Magento/Customer/view/frontend/templates/form/confirmation.phtml b/app/code/Magento/Customer/view/frontend/templates/form/confirmation.phtml -index 8edb21ac8e1..f558d305e70 100644 ---- a/app/code/Magento/Customer/view/frontend/templates/form/confirmation.phtml -+++ b/app/code/Magento/Customer/view/frontend/templates/form/confirmation.phtml -@@ -4,8 +4,6 @@ - * See COPYING.txt for license details. - */ - --// @codingStandardsIgnoreFile -- - /** @var \Magento\Framework\View\Element\Template $block */ - ?> - -diff --git a/app/code/Magento/Customer/view/frontend/templates/form/edit.phtml b/app/code/Magento/Customer/view/frontend/templates/form/edit.phtml -index 75c7d9cffed..2718f03909b 100644 ---- a/app/code/Magento/Customer/view/frontend/templates/form/edit.phtml -+++ b/app/code/Magento/Customer/view/frontend/templates/form/edit.phtml -@@ -4,26 +4,24 @@ - * See COPYING.txt for license details. - */ - --// @codingStandardsIgnoreFile -- - /** @var \Magento\Customer\Block\Form\Edit $block */ - ?> - -
- getBlockHtml('formkey') ?> - escapeHtml(__('Account Information')) ?>
-- getLayout()->createBlock('Magento\Customer\Block\Widget\Name')->setObject($block->getCustomer())->toHtml() ?> -+ getLayout()->createBlock(\Magento\Customer\Block\Widget\Name::class)->setObject($block->getCustomer())->toHtml() ?> - -- getLayout()->createBlock('Magento\Customer\Block\Widget\Dob') ?> -- getLayout()->createBlock('Magento\Customer\Block\Widget\Taxvat') ?> -- getLayout()->createBlock('Magento\Customer\Block\Widget\Gender') ?> -- isEnabled()): ?> -+ getLayout()->createBlock(\Magento\Customer\Block\Widget\Dob::class) ?> -+ getLayout()->createBlock(\Magento\Customer\Block\Widget\Taxvat::class) ?> -+ getLayout()->createBlock(\Magento\Customer\Block\Widget\Gender::class) ?> -+ isEnabled()) : ?> - setDate($block->getCustomer()->getDob())->toHtml() ?> - -- isEnabled()): ?> -+ isEnabled()) : ?> - setTaxvat($block->getCustomer()->getTaxvat())->toHtml() ?> - -- isEnabled()): ?> -+ isEnabled()) : ?> - setGender($block->getCustomer()->getGender())->toHtml() ?> - -
-@@ -31,7 +29,7 @@ - -
-
-- getChangePassword()): ?> checked="checked" class="checkbox" /> -+ getChangePassword()) : ?> checked="checked" class="checkbox" /> - -
-
-@@ -97,7 +95,7 @@ - var ignore = isEnabled() ? '\'input[id$="full"]\'' : 'null' ?>; - - dataForm.mage('validation', { -- isEnabled()): ?> -+ isEnabled()) : ?> - errorPlacement: function(error, element) { - if (element.prop('id').search('full') !== -1) { - var dobElement = $(element).parents('.customer-dob'), -@@ -111,7 +109,7 @@ - } - }, - ignore: ':hidden:not(' + ignore + ')' -- -+ - ignore: ignore ? ':hidden:not(' + ignore + ')' : ':hidden' - - }); -diff --git a/app/code/Magento/Customer/view/frontend/templates/form/forgotpassword.phtml b/app/code/Magento/Customer/view/frontend/templates/form/forgotpassword.phtml -index f8eb0bd44b6..be201afa8f6 100644 ---- a/app/code/Magento/Customer/view/frontend/templates/form/forgotpassword.phtml -+++ b/app/code/Magento/Customer/view/frontend/templates/form/forgotpassword.phtml -@@ -6,8 +6,6 @@ - * @var $block \Magento\Customer\Block\Account\Forgotpassword - */ - --// @codingStandardsIgnoreFile -- - /** @var \Magento\Customer\Block\Account\Forgotpassword $block */ - ?> - -