diff --git a/app/code/Magento/Backend/App/AbstractAction.php b/app/code/Magento/Backend/App/AbstractAction.php index 99ee86b2b64..3f658ee90bf 100644 --- a/app/code/Magento/Backend/App/AbstractAction.php +++ b/app/code/Magento/Backend/App/AbstractAction.php @@ -217,6 +217,7 @@ public function dispatch(\Magento\Framework\App\RequestInterface $request) $this->_view->loadLayout(['default', 'adminhtml_denied'], true, true, false); $this->_view->renderLayout(); $this->_request->setDispatched(true); + return $this->_response; } @@ -226,6 +227,11 @@ public function dispatch(\Magento\Framework\App\RequestInterface $request) $this->_processLocaleSettings(); + // Need to preload isFirstPageAfterLogin (see https://github.com/magento/magento2/issues/15510) + if ($this->_auth->isLoggedIn()) { + $this->_auth->getAuthStorage()->isFirstPageAfterLogin(); + } + return parent::dispatch($request); } diff --git a/app/code/Magento/Backend/Block/GlobalSearch.php b/app/code/Magento/Backend/Block/GlobalSearch.php index f4a46283808..3cea12fea20 100644 --- a/app/code/Magento/Backend/Block/GlobalSearch.php +++ b/app/code/Magento/Backend/Block/GlobalSearch.php @@ -31,6 +31,7 @@ public function getWidgetInitOptions() 'filterProperty' => 'name', 'preventClickPropagation' => false, 'minLength' => 2, + 'submitInputOnEnter' => false, ] ]; } diff --git a/app/code/Magento/Braintree/Gateway/Response/CancelDetailsHandler.php b/app/code/Magento/Braintree/Gateway/Response/CancelDetailsHandler.php new file mode 100644 index 00000000000..3d6ed025791 --- /dev/null +++ b/app/code/Magento/Braintree/Gateway/Response/CancelDetailsHandler.php @@ -0,0 +1,43 @@ +subjectReader = $subjectReader; + } + + /** + * @inheritdoc + */ + public function handle(array $handlingSubject, array $response) + { + $paymentDO = $this->subjectReader->readPayment($handlingSubject); + /** @var Payment $orderPayment */ + $orderPayment = $paymentDO->getPayment(); + $orderPayment->setIsTransactionClosed(true); + $orderPayment->setShouldCloseParentTransaction(true); + } +} diff --git a/app/code/Magento/Braintree/Gateway/SubjectReader.php b/app/code/Magento/Braintree/Gateway/SubjectReader.php index d5dc43a4c5e..7cf00233e7f 100644 --- a/app/code/Magento/Braintree/Gateway/SubjectReader.php +++ b/app/code/Magento/Braintree/Gateway/SubjectReader.php @@ -43,19 +43,20 @@ public function readPayment(array $subject) } /** - * Reads transaction from subject + * Reads transaction from the subject. * * @param array $subject - * @return \Braintree\Transaction + * @return Transaction + * @throws \InvalidArgumentException if the subject doesn't contain transaction details. */ public function readTransaction(array $subject) { if (!isset($subject['object']) || !is_object($subject['object'])) { - throw new \InvalidArgumentException('Response object does not exist'); + throw new \InvalidArgumentException('Response object does not exist.'); } if (!isset($subject['object']->transaction) - && !$subject['object']->transaction instanceof Transaction + || !$subject['object']->transaction instanceof Transaction ) { throw new \InvalidArgumentException('The object is not a class \Braintree\Transaction.'); } diff --git a/app/code/Magento/Braintree/Gateway/Validator/CancelResponseValidator.php b/app/code/Magento/Braintree/Gateway/Validator/CancelResponseValidator.php new file mode 100644 index 00000000000..5e31547e950 --- /dev/null +++ b/app/code/Magento/Braintree/Gateway/Validator/CancelResponseValidator.php @@ -0,0 +1,90 @@ +generalResponseValidator = $generalResponseValidator; + $this->subjectReader = $subjectReader; + } + + /** + * @inheritdoc + */ + public function validate(array $validationSubject): ResultInterface + { + $result = $this->generalResponseValidator->validate($validationSubject); + if (!$result->isValid()) { + $response = $this->subjectReader->readResponseObject($validationSubject); + if ($this->isErrorAcceptable($response->errors)) { + $result = $this->createResult(true, [__('Transaction is cancelled offline.')]); + } + } + + return $result; + } + + /** + * Checks if error collection has an acceptable error code. + * + * @param ErrorCollection $errorCollection + * @return bool + */ + private function isErrorAcceptable(ErrorCollection $errorCollection): bool + { + $errors = $errorCollection->deepAll(); + // there is should be only one acceptable error + if (count($errors) > 1) { + return false; + } + + /** @var Validation $error */ + $error = array_pop($errors); + + return (int)$error->code === self::$acceptableTransactionCode; + } +} diff --git a/app/code/Magento/Braintree/Test/Unit/Gateway/Response/CancelDetailsHandlerTest.php b/app/code/Magento/Braintree/Test/Unit/Gateway/Response/CancelDetailsHandlerTest.php new file mode 100644 index 00000000000..2fa3d2ea658 --- /dev/null +++ b/app/code/Magento/Braintree/Test/Unit/Gateway/Response/CancelDetailsHandlerTest.php @@ -0,0 +1,61 @@ +handler = new CancelDetailsHandler(new SubjectReader()); + } + + /** + * Checks a case when cancel handler closes the current and parent transactions. + * + * @return void + */ + public function testHandle(): void + { + /** @var OrderAdapterInterface|MockObject $order */ + $order = $this->getMockForAbstractClass(OrderAdapterInterface::class); + /** @var Payment|MockObject $payment */ + $payment = $this->getMockBuilder(Payment::class) + ->disableOriginalConstructor() + ->setMethods(['setOrder']) + ->getMock(); + + $paymentDO = new PaymentDataObject($order, $payment); + $response = [ + 'payment' => $paymentDO, + ]; + + $this->handler->handle($response, []); + + self::assertTrue($payment->getIsTransactionClosed(), 'The current transaction should be closed.'); + self::assertTrue($payment->getShouldCloseParentTransaction(), 'The parent transaction should be closed.'); + } +} diff --git a/app/code/Magento/Braintree/Test/Unit/Gateway/SubjectReaderTest.php b/app/code/Magento/Braintree/Test/Unit/Gateway/SubjectReaderTest.php index 4213acc8b4f..fd524a10ba5 100644 --- a/app/code/Magento/Braintree/Test/Unit/Gateway/SubjectReaderTest.php +++ b/app/code/Magento/Braintree/Test/Unit/Gateway/SubjectReaderTest.php @@ -5,6 +5,7 @@ */ namespace Magento\Braintree\Test\Unit\Gateway; +use Braintree\Result\Successful; use Braintree\Transaction; use Magento\Braintree\Gateway\SubjectReader; @@ -18,6 +19,9 @@ class SubjectReaderTest extends \PHPUnit\Framework\TestCase */ private $subjectReader; + /** + * @inheritdoc + */ protected function setUp() { $this->subjectReader = new SubjectReader(); @@ -27,67 +31,137 @@ protected function setUp() * @covers \Magento\Braintree\Gateway\SubjectReader::readCustomerId * @expectedException \InvalidArgumentException * @expectedExceptionMessage The "customerId" field does not exists + * @return void */ - public function testReadCustomerIdWithException() + public function testReadCustomerIdWithException(): void { $this->subjectReader->readCustomerId([]); } /** * @covers \Magento\Braintree\Gateway\SubjectReader::readCustomerId + * @return void */ - public function testReadCustomerId() + public function testReadCustomerId(): void { $customerId = 1; - static::assertEquals($customerId, $this->subjectReader->readCustomerId(['customer_id' => $customerId])); + $this->assertEquals($customerId, $this->subjectReader->readCustomerId(['customer_id' => $customerId])); } /** * @covers \Magento\Braintree\Gateway\SubjectReader::readPublicHash * @expectedException \InvalidArgumentException * @expectedExceptionMessage The "public_hash" field does not exists + * @return void */ - public function testReadPublicHashWithException() + public function testReadPublicHashWithException(): void { $this->subjectReader->readPublicHash([]); } /** * @covers \Magento\Braintree\Gateway\SubjectReader::readPublicHash + * @return void */ - public function testReadPublicHash() + public function testReadPublicHash(): void { $hash = 'fj23djf2o1fd'; - static::assertEquals($hash, $this->subjectReader->readPublicHash(['public_hash' => $hash])); + $this->assertEquals($hash, $this->subjectReader->readPublicHash(['public_hash' => $hash])); } /** * @covers \Magento\Braintree\Gateway\SubjectReader::readPayPal * @expectedException \InvalidArgumentException * @expectedExceptionMessage Transaction has't paypal attribute + * @return void */ - public function testReadPayPalWithException() + public function testReadPayPalWithException(): void { $transaction = Transaction::factory([ - 'id' => 'u38rf8kg6vn' + 'id' => 'u38rf8kg6vn', ]); $this->subjectReader->readPayPal($transaction); } /** * @covers \Magento\Braintree\Gateway\SubjectReader::readPayPal + * @return void */ - public function testReadPayPal() + public function testReadPayPal(): void { $paypal = [ 'paymentId' => '3ek7dk7fn0vi1', - 'payerEmail' => 'payer@example.com' + 'payerEmail' => 'payer@example.com', ]; $transaction = Transaction::factory([ 'id' => '4yr95vb', - 'paypal' => $paypal + 'paypal' => $paypal, ]); - static::assertEquals($paypal, $this->subjectReader->readPayPal($transaction)); + $this->assertEquals($paypal, $this->subjectReader->readPayPal($transaction)); + } + + /** + * Checks a case when subject reader retrieves successful Braintree transaction. + * + * @return void + */ + public function testReadTransaction(): void + { + $transaction = Transaction::factory(['id' => 1]); + $response = [ + 'object' => new Successful($transaction, 'transaction'), + ]; + $actual = $this->subjectReader->readTransaction($response); + + $this->assertSame($transaction, $actual); + } + + /** + * Checks a case when subject reader retrieves invalid data instead transaction details. + * + * @param array $response + * @param string $expectedMessage + * @dataProvider invalidTransactionResponseDataProvider + * @expectedException \InvalidArgumentException + * @return void + */ + public function testReadTransactionWithInvalidResponse(array $response, string $expectedMessage): void + { + $this->expectExceptionMessage($expectedMessage); + $this->subjectReader->readTransaction($response); + } + + /** + * Gets list of variations with invalid subject data. + * + * @return array + */ + public function invalidTransactionResponseDataProvider(): array + { + $transaction = new \stdClass(); + $response = new \stdClass(); + $response->transaction = $transaction; + + return [ + [ + 'response' => [ + 'object' => [], + ], + 'expectedMessage' => 'Response object does not exist.', + ], + [ + 'response' => [ + 'object' => new \stdClass(), + ], + 'expectedMessage' => 'The object is not a class \Braintree\Transaction.', + ], + [ + 'response' => [ + 'object' => $response, + ], + 'expectedMessage' => 'The object is not a class \Braintree\Transaction.', + ], + ]; } } diff --git a/app/code/Magento/Braintree/Test/Unit/Gateway/Validator/CancelResponseValidatorTest.php b/app/code/Magento/Braintree/Test/Unit/Gateway/Validator/CancelResponseValidatorTest.php new file mode 100644 index 00000000000..65386272fe5 --- /dev/null +++ b/app/code/Magento/Braintree/Test/Unit/Gateway/Validator/CancelResponseValidatorTest.php @@ -0,0 +1,179 @@ +generalValidator = $this->getMockBuilder(GeneralResponseValidator::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->resultFactory = $this->getMockBuilder(ResultInterfaceFactory::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->validator = new CancelResponseValidator( + $this->resultFactory, + $this->generalValidator, + new SubjectReader() + ); + } + + /** + * Checks a case when response is successful and additional validation doesn't needed. + * + * @return void + */ + public function testValidateSuccessfulTransaction(): void + { + /** @var ResultInterface|MockObject $result */ + $result = $this->getMockForAbstractClass(ResultInterface::class); + $result->method('isValid')->willReturn(true); + $this->generalValidator->method('validate')->willReturn($result); + $actual = $this->validator->validate([]); + + $this->assertSame($result, $actual); + } + + /** + * Checks a case when response contains error related to expired authorization transaction and + * validator should return positive result. + * + * @return void + */ + public function testValidateExpiredTransaction(): void + { + /** @var ResultInterface|MockObject $result */ + $result = $this->getMockForAbstractClass(ResultInterface::class); + $result->method('isValid')->willReturn(false); + $this->generalValidator->method('validate')->willReturn($result); + + $expected = $this->getMockForAbstractClass(ResultInterface::class); + $expected->method('isValid')->willReturn(true); + $this->resultFactory->method('create') + ->with( + [ + 'isValid' => true, + 'failsDescription' => ['Transaction is cancelled offline.'], + 'errorCodes' => [] + ] + )->willReturn($expected); + + $errors = [ + 'errors' => [ + [ + 'code' => 91504, + 'message' => 'Transaction can only be voided if status is authorized.', + ], + ], + ]; + $buildSubject = [ + 'response' => [ + 'object' => new Error(['errors' => $errors]), + ], + ]; + + $actual = $this->validator->validate($buildSubject); + + $this->assertSame($expected, $actual); + } + + /** + * Checks a case when response contains multiple errors and validator should return negative result. + * + * @param array $responseErrors + * @return void + * @dataProvider getErrorsDataProvider + */ + public function testValidateWithMultipleErrors(array $responseErrors): void + { + /** @var ResultInterface|MockObject $result */ + $result = $this->getMockForAbstractClass(ResultInterface::class); + $result->method('isValid')->willReturn(false); + + $this->generalValidator->method('validate')->willReturn($result); + + $this->resultFactory->expects($this->never())->method('create'); + + $errors = [ + 'errors' => $responseErrors, + ]; + $buildSubject = [ + 'response' => [ + 'object' => new Error(['errors' => $errors]), + ] + ]; + + $actual = $this->validator->validate($buildSubject); + + $this->assertSame($result, $actual); + } + + /** + * Gets list of errors variations. + * + * @return array + */ + public function getErrorsDataProvider(): array + { + return [ + [ + 'errors' => [ + [ + 'code' => 91734, + 'message' => 'Credit card type is not accepted by this merchant account.', + ], + [ + 'code' => 91504, + 'message' => 'Transaction can only be voided if status is authorized.', + ], + ], + ], + [ + 'errors' => [ + [ + 'code' => 91734, + 'message' => 'Credit card type is not accepted by this merchant account.', + ], + ], + ], + ]; + } +} diff --git a/app/code/Magento/Braintree/etc/di.xml b/app/code/Magento/Braintree/etc/di.xml index 2bb4cea6742..290fb5be58f 100644 --- a/app/code/Magento/Braintree/etc/di.xml +++ b/app/code/Magento/Braintree/etc/di.xml @@ -133,8 +133,8 @@ BraintreeVaultCaptureCommand BraintreeVoidCommand BraintreeRefundCommand - BraintreeVoidCommand - BraintreeVoidCommand + Magento\Braintree\Gateway\CancelCommand + Magento\Braintree\Gateway\CancelCommand @@ -150,7 +150,7 @@ BraintreeVaultCaptureCommand BraintreeVoidCommand BraintreeRefundCommand - BraintreeVoidCommand + Magento\Braintree\Gateway\CancelCommand @@ -505,6 +505,14 @@ + + + + Magento\Braintree\Gateway\Response\CancelDetailsHandler + Magento\Braintree\Gateway\Validator\CancelResponseValidator + + + diff --git a/app/code/Magento/Bundle/Controller/Adminhtml/Product/Initialization/Helper/Plugin/Bundle.php b/app/code/Magento/Bundle/Controller/Adminhtml/Product/Initialization/Helper/Plugin/Bundle.php index 7f21d9e69c6..3c9eac68eb9 100644 --- a/app/code/Magento/Bundle/Controller/Adminhtml/Product/Initialization/Helper/Plugin/Bundle.php +++ b/app/code/Magento/Bundle/Controller/Adminhtml/Product/Initialization/Helper/Plugin/Bundle.php @@ -105,8 +105,13 @@ public function afterInitialize( if ($result['bundle_options'] && !$compositeReadonly) { $product->setBundleOptionsData($result['bundle_options']); } + $this->processBundleOptionsData($product); $this->processDynamicOptionsData($product); + } elseif (!$compositeReadonly) { + $extension = $product->getExtensionAttributes(); + $extension->setBundleProductOptions([]); + $product->setExtensionAttributes($extension); } $affectProductSelections = (bool)$this->request->getPost('affect_bundle_product_selections'); diff --git a/app/code/Magento/Bundle/Model/OptionRepository.php b/app/code/Magento/Bundle/Model/OptionRepository.php index a70f4af56e9..59e658b08df 100644 --- a/app/code/Magento/Bundle/Model/OptionRepository.php +++ b/app/code/Magento/Bundle/Model/OptionRepository.php @@ -234,7 +234,7 @@ protected function updateOptionSelection( */ private function getProduct($sku) { - $product = $this->productRepository->get($sku, true); + $product = $this->productRepository->get($sku, true, null, true); if ($product->getTypeId() != \Magento\Catalog\Model\Product\Type::TYPE_BUNDLE) { throw new InputException(__('This is implemented for bundle products only.')); } diff --git a/app/code/Magento/Bundle/Model/Product/SaveHandler.php b/app/code/Magento/Bundle/Model/Product/SaveHandler.php index e5fa688c7fe..fc215aa6b8e 100644 --- a/app/code/Magento/Bundle/Model/Product/SaveHandler.php +++ b/app/code/Magento/Bundle/Model/Product/SaveHandler.php @@ -5,7 +5,6 @@ */ namespace Magento\Bundle\Model\Product; -use Magento\Bundle\Api\Data\OptionInterface; use Magento\Bundle\Model\Option\SaveAction; use Magento\Catalog\Api\Data\ProductInterface; use Magento\Bundle\Api\ProductOptionRepositoryInterface as OptionRepository; @@ -58,36 +57,6 @@ public function __construct( ?: ObjectManager::getInstance()->get(MetadataPool::class); } - /** - * @param ProductInterface $bundle - * @param OptionInterface[] $currentOptions - * - * @return void - */ - private function removeOldOptions( - ProductInterface $bundle, - array $currentOptions - ) { - $oldOptions = $this->optionRepository->getList($bundle->getSku()); - if ($oldOptions) { - $remainingOptions = []; - $metadata - = $this->metadataPool->getMetadata(ProductInterface::class); - $productId = $bundle->getData($metadata->getLinkField()); - - foreach ($currentOptions as $option) { - $remainingOptions[] = $option->getOptionId(); - } - foreach ($oldOptions as $option) { - if (!in_array($option->getOptionId(), $remainingOptions)) { - $option->setParentId($productId); - $this->removeOptionLinks($bundle->getSku(), $option); - $this->optionRepository->delete($option); - } - } - } - } - /** * @param object $entity * @param array $arguments @@ -98,24 +67,31 @@ private function removeOldOptions( */ public function execute($entity, $arguments = []) { - /** @var \Magento\Bundle\Api\Data\OptionInterface[] $options */ - $options = $entity->getExtensionAttributes()->getBundleProductOptions() ?: []; + /** @var \Magento\Bundle\Api\Data\OptionInterface[] $bundleProductOptions */ + $bundleProductOptions = $entity->getExtensionAttributes()->getBundleProductOptions() ?: []; //Only processing bundle products. - if ($entity->getTypeId() !== 'bundle' || empty($options)) { + if ($entity->getTypeId() !== Type::TYPE_CODE || empty($bundleProductOptions)) { return $entity; } - /** @var ProductInterface $entity */ - //Removing old options + + $existingBundleProductOptions = $this->optionRepository->getList($entity->getSku()); + $existingOptionsIds = !empty($existingBundleProductOptions) + ? $this->getOptionIds($existingBundleProductOptions) + : []; + $optionIds = !empty($bundleProductOptions) + ? $this->getOptionIds($bundleProductOptions) + : []; + if (!$entity->getCopyFromView()) { - $this->removeOldOptions($entity, $options); - } - //Saving active options. - foreach ($options as $option) { - $this->optionSave->save($entity, $option); + $this->processRemovedOptions($entity->getSku(), $existingOptionsIds, $optionIds); + $newOptionsIds = array_diff($optionIds, $existingOptionsIds); + $this->saveOptions($entity, $bundleProductOptions, $newOptionsIds); + } else { + //save only labels and not selections + product links + $this->saveOptions($entity, $bundleProductOptions); + $entity->setCopyFromView(false); } - $entity->setCopyFromView(false); - return $entity; } @@ -133,4 +109,62 @@ protected function removeOptionLinks($entitySku, $option) } } } + + /** + * Perform save for all options entities. + * + * @param object $entity + * @param array $options + * @param array $newOptionsIds + * @return void + */ + private function saveOptions($entity, array $options, array $newOptionsIds = []): void + { + foreach ($options as $option) { + if (in_array($option->getOptionId(), $newOptionsIds, true)) { + $option->setOptionId(null); + } + + $this->optionSave->save($entity, $option); + } + } + + /** + * Get options ids from array of the options entities. + * + * @param array $options + * @return array + */ + private function getOptionIds(array $options): array + { + $optionIds = []; + + if (!empty($options)) { + /** @var \Magento\Bundle\Api\Data\OptionInterface $option */ + foreach ($options as $option) { + if ($option->getOptionId()) { + $optionIds[] = $option->getOptionId(); + } + } + } + + return $optionIds; + } + + /** + * Removes old options that no longer exists. + * + * @param string $entitySku + * @param array $existingOptionsIds + * @param array $optionIds + * @return void + */ + private function processRemovedOptions(string $entitySku, array $existingOptionsIds, array $optionIds): void + { + foreach (array_diff($existingOptionsIds, $optionIds) as $optionId) { + $option = $this->optionRepository->get($entitySku, $optionId); + $this->removeOptionLinks($entitySku, $option); + $this->optionRepository->delete($option); + } + } } diff --git a/app/code/Magento/Bundle/Model/ResourceModel/Indexer/Price.php b/app/code/Magento/Bundle/Model/ResourceModel/Indexer/Price.php index 401374db86f..0b6e97cfb92 100644 --- a/app/code/Magento/Bundle/Model/ResourceModel/Indexer/Price.php +++ b/app/code/Magento/Bundle/Model/ResourceModel/Indexer/Price.php @@ -153,52 +153,43 @@ protected function _prepareBundlePriceByType($priceType, $entityIds = null) $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'); - $curentDate = new \Zend_Db_Expr('cwd.website_date'); - - $specialExpr = $connection->getCheckSql( - $connection->getCheckSql( - $specialFrom . ' IS NULL', - '1', - $connection->getCheckSql($specialFrom . ' <= ' . $curentDate, '1', '0') - ) . " > 0 AND " . $connection->getCheckSql( - $specialTo . ' IS NULL', - '1', - $connection->getCheckSql($specialTo . ' >= ' . $curentDate, '1', '0') - ) . " > 0 AND {$specialPrice} > 0 AND {$specialPrice} < 100 ", - $specialPrice, - '0' - ); + $currentDate = new \Zend_Db_Expr('cwd.website_date'); - $tierExpr = new \Zend_Db_Expr("tp.min_price"); + $specialFromDate = $connection->getDatePartSql($specialFrom); + $specialToDate = $connection->getDatePartSql($specialTo); + $specialFromExpr = "{$specialFrom} IS NULL OR {$specialFromDate} <= {$currentDate}"; + $specialToExpr = "{$specialTo} IS NULL OR {$specialToDate} >= {$currentDate}"; + $specialExpr = "{$specialPrice} IS NOT NULL AND {$specialPrice} > 0 AND {$specialPrice} < 100" + . " AND {$specialFromExpr} AND {$specialToExpr}"; + $tierExpr = new \Zend_Db_Expr('tp.min_price'); if ($priceType == \Magento\Bundle\Model\Product\Price::PRICE_TYPE_FIXED) { - $finalPrice = $connection->getCheckSql( - $specialExpr . ' > 0', - 'ROUND(' . $price . ' * (' . $specialExpr . ' / 100), 4)', - $price + $specialPriceExpr = $connection->getCheckSql( + $specialExpr, + 'ROUND(' . $price . ' * (' . $specialPrice . ' / 100), 4)', + 'NULL' ); $tierPrice = $connection->getCheckSql( $tierExpr . ' IS NOT NULL', - 'ROUND(' . $price . ' - ' . '(' . $price . ' * (' . $tierExpr . ' / 100)), 4)', + 'ROUND((1 - ' . $tierExpr . ' / 100) * ' . $price . ', 4)', 'NULL' ); - - $finalPrice = $connection->getCheckSql( - "{$tierPrice} < {$finalPrice}", - $tierPrice, - $finalPrice - ); + $finalPrice = $connection->getLeastSql([ + $price, + $connection->getIfNullSql($specialPriceExpr, $price), + $connection->getIfNullSql($tierPrice, $price), + ]); } else { - $finalPrice = new \Zend_Db_Expr("0"); + $finalPrice = new \Zend_Db_Expr('0'); $tierPrice = $connection->getCheckSql($tierExpr . ' IS NOT NULL', '0', 'NULL'); } $select->columns( [ 'price_type' => new \Zend_Db_Expr($priceType), - 'special_price' => $specialExpr, + 'special_price' => $connection->getCheckSql($specialExpr, $specialPrice, '0'), 'tier_percent' => $tierExpr, - 'orig_price' => $connection->getCheckSql($price . ' IS NULL', '0', $price), + 'orig_price' => $connection->getIfNullSql($price, '0'), 'price' => $finalPrice, 'min_price' => $finalPrice, 'max_price' => $finalPrice, @@ -246,17 +237,20 @@ protected function _calculateBundleOptionPrice() $this->_prepareBundleOptionTable(); $select = $connection->select()->from( - ['i' => $this->_getBundleSelectionTable()], + $this->_getBundleSelectionTable(), ['entity_id', 'customer_group_id', 'website_id', 'option_id'] )->group( - ['entity_id', 'customer_group_id', 'website_id', 'option_id', 'is_required', 'group_type'] - )->columns( + ['entity_id', 'customer_group_id', 'website_id', 'option_id'] + ); + $minPrice = $connection->getCheckSql('is_required = 1', 'price', 'NULL'); + $tierPrice = $connection->getCheckSql('is_required = 1', 'tier_price', 'NULL'); + $select->columns( [ - 'min_price' => $connection->getCheckSql('i.is_required = 1', 'MIN(i.price)', '0'), - 'alt_price' => $connection->getCheckSql('i.is_required = 0', 'MIN(i.price)', '0'), - 'max_price' => $connection->getCheckSql('i.group_type = 1', 'SUM(i.price)', 'MAX(i.price)'), - 'tier_price' => $connection->getCheckSql('i.is_required = 1', 'MIN(i.tier_price)', '0'), - 'alt_tier_price' => $connection->getCheckSql('i.is_required = 0', 'MIN(i.tier_price)', '0'), + 'min_price' => new \Zend_Db_Expr('MIN(' . $minPrice . ')'), + 'alt_price' => new \Zend_Db_Expr('MIN(price)'), + 'max_price' => $connection->getCheckSql('group_type = 0', 'MAX(price)', 'SUM(price)'), + 'tier_price' => new \Zend_Db_Expr('MIN(' . $tierPrice . ')'), + 'alt_tier_price' => new \Zend_Db_Expr('MIN(tier_price)'), ] ); @@ -264,45 +258,8 @@ protected function _calculateBundleOptionPrice() $connection->query($query); $this->_prepareDefaultFinalPriceTable(); - - $minPrice = new \Zend_Db_Expr( - $connection->getCheckSql('SUM(io.min_price) = 0', 'MIN(io.alt_price)', 'SUM(io.min_price)') . ' + i.price' - ); - $maxPrice = new \Zend_Db_Expr("SUM(io.max_price) + i.price"); - $tierPrice = $connection->getCheckSql( - 'MIN(i.tier_percent) IS NOT NULL', - $connection->getCheckSql( - 'SUM(io.tier_price) = 0', - 'SUM(io.alt_tier_price)', - 'SUM(io.tier_price)' - ) . ' + MIN(i.tier_price)', - 'NULL' - ); - - $select = $connection->select()->from( - ['io' => $this->_getBundleOptionTable()], - ['entity_id', 'customer_group_id', 'website_id'] - )->join( - ['i' => $this->_getBundlePriceTable()], - 'i.entity_id = io.entity_id AND i.customer_group_id = io.customer_group_id' . - ' AND i.website_id = io.website_id', - [] - )->group( - ['io.entity_id', 'io.customer_group_id', 'io.website_id', 'i.tax_class_id', 'i.orig_price', 'i.price'] - )->columns( - [ - 'i.tax_class_id', - 'orig_price' => 'i.orig_price', - 'price' => 'i.price', - 'min_price' => $minPrice, - 'max_price' => $maxPrice, - 'tier_price' => $tierPrice, - 'base_tier' => 'MIN(i.base_tier)', - ] - ); - - $query = $select->insertFromSelect($this->_getDefaultFinalPriceTable()); - $connection->query($query); + $this->applyBundlePrice(); + $this->applyBundleOptionPrice(); return $this; } @@ -348,33 +305,33 @@ protected function _calculateBundleSelectionPrice($priceType) 'ROUND(i.base_tier - (i.base_tier * (' . $selectionPriceValue . ' / 100)),4)', $connection->getCheckSql( 'i.tier_percent > 0', - 'ROUND(' . - $selectionPriceValue . - ' - (' . - $selectionPriceValue . - ' * (i.tier_percent / 100)),4)', + 'ROUND((1 - i.tier_percent / 100) * ' . $selectionPriceValue . ',4)', $selectionPriceValue ) ) . ' * bs.selection_qty', 'NULL' ); - $priceExpr = new \Zend_Db_Expr( - $connection->getCheckSql("{$tierExpr} < {$priceExpr}", $tierExpr, $priceExpr) - ); + $priceExpr = $connection->getLeastSql([ + $priceExpr, + $connection->getIfNullSql($tierExpr, $priceExpr), + ]); } else { - $priceExpr = new \Zend_Db_Expr( - $connection->getCheckSql( - 'i.special_price > 0 AND i.special_price < 100', - 'ROUND(idx.min_price * (i.special_price / 100), 4)', - 'idx.min_price' - ) . ' * bs.selection_qty' + $price = 'idx.min_price * bs.selection_qty'; + $specialExpr = $connection->getCheckSql( + 'i.special_price > 0 AND i.special_price < 100', + 'ROUND(' . $price . ' * (i.special_price / 100), 4)', + $price ); $tierExpr = $connection->getCheckSql( - 'i.base_tier IS NOT NULL', - 'ROUND(idx.min_price * (i.base_tier / 100), 4)* bs.selection_qty', + 'i.tier_percent IS NOT NULL', + 'ROUND((1 - i.tier_percent / 100) * ' . $price . ', 4)', 'NULL' ); + $priceExpr = $connection->getLeastSql([ + $specialExpr, + $connection->getIfNullSql($tierExpr, $price), + ]); } $linkField = $this->getMetadataPool()->getMetadata(ProductInterface::class)->getLinkField(); @@ -508,4 +465,76 @@ protected function _prepareTierPriceIndex($entityIds = null) return $this; } + + /** + * Create bundle price. + * + * @return void + */ + private function applyBundlePrice(): void + { + $select = $this->getConnection()->select(); + $select->from( + $this->_getBundlePriceTable(), + [ + 'entity_id', + 'customer_group_id', + 'website_id', + 'tax_class_id', + 'orig_price', + 'price', + 'min_price', + 'max_price', + 'tier_price', + 'base_tier', + ] + ); + + $query = $select->insertFromSelect($this->_getDefaultFinalPriceTable()); + $this->getConnection()->query($query); + } + + /** + * Make insert/update bundle option price. + * + * @return void + */ + private function applyBundleOptionPrice(): void + { + $connection = $this->getConnection(); + + $subSelect = $connection->select()->from( + $this->_getBundleOptionTable(), + [ + 'entity_id', + 'customer_group_id', + 'website_id', + 'min_price' => new \Zend_Db_Expr('SUM(min_price)'), + 'alt_price' => new \Zend_Db_Expr('MIN(alt_price)'), + 'max_price' => new \Zend_Db_Expr('SUM(max_price)'), + 'tier_price' => new \Zend_Db_Expr('SUM(tier_price)'), + 'alt_tier_price' => new \Zend_Db_Expr('MIN(alt_tier_price)'), + ] + )->group( + ['entity_id', 'customer_group_id', 'website_id'] + ); + + $minPrice = 'i.min_price + ' . $connection->getIfNullSql('io.min_price', '0'); + $tierPrice = 'i.tier_price + ' . $connection->getIfNullSql('io.tier_price', '0'); + $select = $connection->select()->join( + ['io' => $subSelect], + 'i.entity_id = io.entity_id AND i.customer_group_id = io.customer_group_id' . + ' AND i.website_id = io.website_id', + [] + )->columns( + [ + 'min_price' => $connection->getCheckSql("{$minPrice} = 0", 'io.alt_price', $minPrice), + 'max_price' => new \Zend_Db_Expr('io.max_price + i.max_price'), + 'tier_price' => $connection->getCheckSql("{$tierPrice} = 0", 'io.alt_tier_price', $tierPrice), + ] + ); + + $query = $select->crossUpdateFromSelect(['i' => $this->_getDefaultFinalPriceTable()]); + $connection->query($query); + } } diff --git a/app/code/Magento/Bundle/Model/ResourceModel/Option.php b/app/code/Magento/Bundle/Model/ResourceModel/Option.php index 46fd8b910f6..7babd0b349f 100644 --- a/app/code/Magento/Bundle/Model/ResourceModel/Option.php +++ b/app/code/Magento/Bundle/Model/ResourceModel/Option.php @@ -81,39 +81,29 @@ protected function _afterSave(\Magento\Framework\Model\AbstractModel $object) { parent::_afterSave($object); - $conditions = [ + $condition = [ 'option_id = ?' => $object->getId(), 'store_id = ? OR store_id = 0' => $object->getStoreId(), 'parent_product_id = ?' => $object->getParentId() ]; $connection = $this->getConnection(); + $connection->delete($this->getTable('catalog_product_bundle_option_value'), $condition); - if ($this->isOptionPresent($conditions)) { - $connection->update( - $this->getTable('catalog_product_bundle_option_value'), - [ - 'title' => $object->getTitle() - ], - $conditions - ); - } else { - $data = new \Magento\Framework\DataObject(); - $data->setOptionId($object->getId()) - ->setStoreId($object->getStoreId()) - ->setParentProductId($object->getParentId()) - ->setTitle($object->getTitle()); + $data = new \Magento\Framework\DataObject(); + $data->setOptionId($object->getId()) + ->setStoreId($object->getStoreId()) + ->setParentProductId($object->getParentId()) + ->setTitle($object->getTitle()); - $connection->insert($this->getTable('catalog_product_bundle_option_value'), $data->getData()); + $connection->insert($this->getTable('catalog_product_bundle_option_value'), $data->getData()); - /** - * also saving default value if this store view scope - */ - if ($object->getStoreId()) { - $data->setStoreId(0); - $data->setTitle($object->getDefaultTitle()); - $connection->insert($this->getTable('catalog_product_bundle_option_value'), $data->getData()); - } + /** + * also saving default fallback value + */ + if (0 !== (int)$object->getStoreId()) { + $data->setStoreId(0)->setTitle($object->getDefaultTitle()); + $connection->insert($this->getTable('catalog_product_bundle_option_value'), $data->getData()); } return $this; @@ -218,26 +208,4 @@ public function save(\Magento\Framework\Model\AbstractModel $object) return $this; } - - /** - * Is Bundle option present in the database - * - * @param array $conditions - * - * @return bool - */ - private function isOptionPresent($conditions) - { - $connection = $this->getConnection(); - - $select = $connection->select()->from($this->getTable('catalog_product_bundle_option_value')); - foreach ($conditions as $condition => $conditionValue) { - $select->where($condition, $conditionValue); - } - $select->limit(1); - - $rowSelect = $connection->fetchRow($select); - - return (is_array($rowSelect) && !empty($rowSelect)); - } } diff --git a/app/code/Magento/Bundle/Pricing/Adjustment/DefaultSelectionPriceListProvider.php b/app/code/Magento/Bundle/Pricing/Adjustment/DefaultSelectionPriceListProvider.php index 56c403ad996..297c4659cb8 100644 --- a/app/code/Magento/Bundle/Pricing/Adjustment/DefaultSelectionPriceListProvider.php +++ b/app/code/Magento/Bundle/Pricing/Adjustment/DefaultSelectionPriceListProvider.php @@ -61,8 +61,8 @@ public function getPriceList(Product $bundleProduct, $searchMin, $useRegularPric if (!$useRegularPrice) { $selectionsCollection->addAttributeToSelect('special_price'); - $selectionsCollection->addAttributeToSelect('special_price_from'); - $selectionsCollection->addAttributeToSelect('special_price_to'); + $selectionsCollection->addAttributeToSelect('special_from_date'); + $selectionsCollection->addAttributeToSelect('special_to_date'); $selectionsCollection->addAttributeToSelect('tax_class_id'); } diff --git a/app/code/Magento/Bundle/Test/Unit/Controller/Adminhtml/Product/Initialization/Helper/Plugin/BundleTest.php b/app/code/Magento/Bundle/Test/Unit/Controller/Adminhtml/Product/Initialization/Helper/Plugin/BundleTest.php index 59a2190a43e..1fa7f186786 100644 --- a/app/code/Magento/Bundle/Test/Unit/Controller/Adminhtml/Product/Initialization/Helper/Plugin/BundleTest.php +++ b/app/code/Magento/Bundle/Test/Unit/Controller/Adminhtml/Product/Initialization/Helper/Plugin/BundleTest.php @@ -163,4 +163,27 @@ public function testAfterInitializeIfBundleSelectionsAndCustomOptionsExist() $this->productMock->expects($this->once())->method('setCanSaveBundleSelections')->with(false); $this->model->afterInitialize($this->subjectMock, $this->productMock); } + + /** + * @return void + */ + public function testAfterInitializeIfBundleOptionsNotExist(): void + { + $valueMap = [ + ['bundle_options', null, null], + ['affect_bundle_product_selections', null, false], + ]; + $this->requestMock->expects($this->any())->method('getPost')->will($this->returnValueMap($valueMap)); + $extentionAttribute = $this->getMockBuilder(\Magento\Catalog\Api\Data\ProductExtensionInterface::class) + ->disableOriginalConstructor() + ->setMethods(['setBundleProductOptions']) + ->getMockForAbstractClass(); + $extentionAttribute->expects($this->once())->method('setBundleProductOptions')->with([]); + $this->productMock->expects($this->any())->method('getCompositeReadonly')->will($this->returnValue(false)); + $this->productMock->expects($this->once())->method('getExtensionAttributes')->willReturn($extentionAttribute); + $this->productMock->expects($this->once())->method('setExtensionAttributes')->with($extentionAttribute); + $this->productMock->expects($this->once())->method('setCanSaveBundleSelections')->with(false); + + $this->model->afterInitialize($this->subjectMock, $this->productMock); + } } diff --git a/app/code/Magento/Bundle/composer.json b/app/code/Magento/Bundle/composer.json index aa2b9e12f77..84c9a97698b 100644 --- a/app/code/Magento/Bundle/composer.json +++ b/app/code/Magento/Bundle/composer.json @@ -25,7 +25,8 @@ }, "suggest": { "magento/module-webapi": "*", - "magento/module-bundle-sample-data": "*" + "magento/module-bundle-sample-data": "*", + "magento/module-sales-rule": "*" }, "type": "magento2-module", "license": [ diff --git a/app/code/Magento/Bundle/etc/di.xml b/app/code/Magento/Bundle/etc/di.xml index b7fba3937de..733b089dccd 100644 --- a/app/code/Magento/Bundle/etc/di.xml +++ b/app/code/Magento/Bundle/etc/di.xml @@ -207,4 +207,11 @@ + + + + false + + + diff --git a/app/code/Magento/Captcha/Test/Unit/Observer/CaptchaStringResolverTest.php b/app/code/Magento/Captcha/Test/Unit/Observer/CaptchaStringResolverTest.php new file mode 100644 index 00000000000..2bd8ac6f169 --- /dev/null +++ b/app/code/Magento/Captcha/Test/Unit/Observer/CaptchaStringResolverTest.php @@ -0,0 +1,69 @@ +objectManagerHelper = new ObjectManager($this); + $this->requestMock = $this->createMock(HttpRequest::class); + $this->captchaStringResolver = $this->objectManagerHelper->getObject(CaptchaStringResolver::class); + } + + public function testResolveWithFormIdSet() + { + $formId = 'contact_us'; + $captchaValue = 'some-value'; + + $this->requestMock->expects($this->once()) + ->method('getPost') + ->with(CaptchaDataHelper::INPUT_NAME_FIELD_VALUE) + ->willReturn([$formId => $captchaValue]); + + self::assertEquals( + $this->captchaStringResolver->resolve($this->requestMock, $formId), + $captchaValue + ); + } + + public function testResolveWithNoFormIdInRequest() + { + $formId = 'contact_us'; + + $this->requestMock->expects($this->once()) + ->method('getPost') + ->with(CaptchaDataHelper::INPUT_NAME_FIELD_VALUE) + ->willReturn([]); + + self::assertEquals( + $this->captchaStringResolver->resolve($this->requestMock, $formId), + '' + ); + } +} diff --git a/app/code/Magento/Captcha/Test/Unit/Observer/CheckRegisterCheckoutObserverTest.php b/app/code/Magento/Captcha/Test/Unit/Observer/CheckRegisterCheckoutObserverTest.php new file mode 100644 index 00000000000..89012ef6538 --- /dev/null +++ b/app/code/Magento/Captcha/Test/Unit/Observer/CheckRegisterCheckoutObserverTest.php @@ -0,0 +1,211 @@ +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/Catalog/Block/Category/Plugin/PriceBoxTags.php b/app/code/Magento/Catalog/Block/Category/Plugin/PriceBoxTags.php index 03932151358..99399110505 100644 --- a/app/code/Magento/Catalog/Block/Category/Plugin/PriceBoxTags.php +++ b/app/code/Magento/Catalog/Block/Category/Plugin/PriceBoxTags.php @@ -71,7 +71,7 @@ public function afterGetCacheKey(PriceBox $subject, $result) '-', [ $result, - $this->priceCurrency->getCurrencySymbol(), + $this->priceCurrency->getCurrency()->getCode(), $this->dateTime->scopeDate($this->scopeResolver->getScope()->getId())->format('Ymd'), $this->scopeResolver->getScope()->getId(), $this->customerSession->getCustomerGroupId(), diff --git a/app/code/Magento/Catalog/Controller/Adminhtml/Category.php b/app/code/Magento/Catalog/Controller/Adminhtml/Category.php index 13c4353e652..1e0cb9f197a 100644 --- a/app/code/Magento/Catalog/Controller/Adminhtml/Category.php +++ b/app/code/Magento/Catalog/Controller/Adminhtml/Category.php @@ -3,8 +3,12 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\Catalog\Controller\Adminhtml; +use Magento\Store\Model\Store; + /** * Catalog category controller */ @@ -44,7 +48,7 @@ public function __construct( protected function _initCategory($getRootInstead = false) { $categoryId = $this->resolveCategoryId(); - $storeId = (int)$this->getRequest()->getParam('store'); + $storeId = $this->resolveStoreId(); $category = $this->_objectManager->create(\Magento\Catalog\Model\Category::class); $category->setStoreId($storeId); @@ -70,7 +74,7 @@ protected function _initCategory($getRootInstead = false) $this->_objectManager->get(\Magento\Framework\Registry::class)->register('category', $category); $this->_objectManager->get(\Magento\Framework\Registry::class)->register('current_category', $category); $this->_objectManager->get(\Magento\Cms\Model\Wysiwyg\Config::class) - ->setStoreId($this->getRequest()->getParam('store')); + ->setStoreId($storeId); return $category; } @@ -79,13 +83,28 @@ protected function _initCategory($getRootInstead = false) * * @return int */ - private function resolveCategoryId() + private function resolveCategoryId() : int { $categoryId = (int)$this->getRequest()->getParam('id', false); return $categoryId ?: (int)$this->getRequest()->getParam('entity_id', false); } + /** + * Resolve store id + * + * Tries to take store id from store HTTP parameter + * @see Store + * + * @return int + */ + private function resolveStoreId() : int + { + $storeId = (int)$this->getRequest()->getParam('store', false); + + return $storeId ?: (int)$this->getRequest()->getParam('store_id', Store::DEFAULT_STORE_ID); + } + /** * Build response for ajax request * diff --git a/app/code/Magento/Catalog/Controller/Adminhtml/Product/Attribute/Save.php b/app/code/Magento/Catalog/Controller/Adminhtml/Product/Attribute/Save.php index e054a9d49b4..817de6828e4 100644 --- a/app/code/Magento/Catalog/Controller/Adminhtml/Product/Attribute/Save.php +++ b/app/code/Magento/Catalog/Controller/Adminhtml/Product/Attribute/Save.php @@ -69,6 +69,7 @@ class Save extends Attribute * @var LayoutFactory */ private $layoutFactory; + /** * @var Presentation */ @@ -124,6 +125,7 @@ public function execute() { $data = $this->getRequest()->getPostValue(); if ($data) { + $this->preprocessOptionsData($data); $setId = $this->getRequest()->getParam('set'); $attributeSet = null; @@ -313,6 +315,28 @@ public function execute() return $this->returnResult('catalog/*/', [], ['error' => true]); } + /** + * 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. + * + * @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 diff --git a/app/code/Magento/Catalog/Model/ResourceModel/Category.php b/app/code/Magento/Catalog/Model/ResourceModel/Category.php index 6c9867359d4..fa68ae3f865 100644 --- a/app/code/Magento/Catalog/Model/ResourceModel/Category.php +++ b/app/code/Magento/Catalog/Model/ResourceModel/Category.php @@ -11,6 +11,8 @@ */ namespace Magento\Catalog\Model\ResourceModel; +use Magento\Catalog\Model\Indexer\Category\Product\Processor; +use Magento\Framework\DataObject; use Magento\Framework\EntityManager\EntityManager; /** @@ -82,6 +84,11 @@ class Category extends AbstractResource */ protected $aggregateCount; + /** + * @var Processor + */ + private $indexerProcessor; + /** * Category constructor. * @param \Magento\Eav\Model\Entity\Context $context @@ -90,6 +97,7 @@ class Category extends AbstractResource * @param \Magento\Framework\Event\ManagerInterface $eventManager * @param Category\TreeFactory $categoryTreeFactory * @param Category\CollectionFactory $categoryCollectionFactory + * @param Processor $indexerProcessor * @param array $data * @param \Magento\Framework\Serialize\Serializer\Json|null $serializer */ @@ -100,6 +108,7 @@ public function __construct( \Magento\Framework\Event\ManagerInterface $eventManager, \Magento\Catalog\Model\ResourceModel\Category\TreeFactory $categoryTreeFactory, \Magento\Catalog\Model\ResourceModel\Category\CollectionFactory $categoryCollectionFactory, + Processor $indexerProcessor, $data = [], \Magento\Framework\Serialize\Serializer\Json $serializer = null ) { @@ -113,6 +122,7 @@ public function __construct( $this->_categoryCollectionFactory = $categoryCollectionFactory; $this->_eventManager = $eventManager; $this->connectionName = 'catalog'; + $this->indexerProcessor = $indexerProcessor; $this->serializer = $serializer ?: \Magento\Framework\App\ObjectManager::getInstance() ->get(\Magento\Framework\Serialize\Serializer\Json::class); } @@ -197,6 +207,18 @@ protected function _beforeDelete(\Magento\Framework\DataObject $object) $this->deleteChildren($object); } + /** + * Mark Category indexer as invalid to be picked up by cron. + * + * @param DataObject $object + * @return $this + */ + protected function _afterDelete(DataObject $object) + { + $this->indexerProcessor->markIndexerAsInvalid(); + return parent::_afterDelete($object); + } + /** * Delete children categories of specific category * 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 285e1781e2f..4ca407a53f8 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 @@ -536,6 +536,7 @@ protected function _applyCustomOption() $finalPriceTable = $this->_getDefaultFinalPriceTable(); $coaTable = $this->_getCustomOptionAggregateTable(); $copTable = $this->_getCustomOptionPriceTable(); + $metadata = $this->getMetadataPool()->getMetadata(\Magento\Catalog\Api\Data\ProductInterface::class); $this->_prepareCustomOptionAggregateTable(); $this->_prepareCustomOptionPriceTable(); @@ -543,6 +544,10 @@ protected function _applyCustomOption() $select = $connection->select()->from( ['i' => $finalPriceTable], ['entity_id', 'customer_group_id', 'website_id'] + )->join( + ['e' => $this->getTable('catalog_product_entity')], + 'e.entity_id = i.entity_id', + [] )->join( ['cw' => $this->getTable('store_website')], 'cw.website_id = i.website_id', @@ -557,7 +562,7 @@ protected function _applyCustomOption() [] )->join( ['o' => $this->getTable('catalog_product_option')], - 'o.product_id = i.entity_id', + 'o.product_id = e.' . $metadata->getLinkField(), ['option_id'] )->join( ['ot' => $this->getTable('catalog_product_option_type_value')], @@ -610,6 +615,10 @@ protected function _applyCustomOption() $select = $connection->select()->from( ['i' => $finalPriceTable], ['entity_id', 'customer_group_id', 'website_id'] + )->join( + ['e' => $this->getTable('catalog_product_entity')], + 'e.entity_id = i.entity_id', + [] )->join( ['cw' => $this->getTable('store_website')], 'cw.website_id = i.website_id', @@ -624,7 +633,7 @@ protected function _applyCustomOption() [] )->join( ['o' => $this->getTable('catalog_product_option')], - 'o.product_id = i.entity_id', + 'o.product_id = e.' . $metadata->getLinkField(), ['option_id'] )->join( ['opd' => $this->getTable('catalog_product_option_price')], @@ -641,13 +650,13 @@ protected function _applyCustomOption() $minPriceRound = new \Zend_Db_Expr("ROUND(i.price * ({$optPriceValue} / 100), 4)"); $priceExpr = $connection->getCheckSql("{$optPriceType} = 'fixed'", $optPriceValue, $minPriceRound); - $minPrice = $connection->getCheckSql("{$priceExpr} > 0 AND o.is_require > 1", $priceExpr, 0); + $minPrice = $connection->getCheckSql("{$priceExpr} > 0 AND o.is_require = 1", $priceExpr, 0); $maxPrice = $priceExpr; $tierPriceRound = new \Zend_Db_Expr("ROUND(i.base_tier * ({$optPriceValue} / 100), 4)"); $tierPriceExpr = $connection->getCheckSql("{$optPriceType} = 'fixed'", $optPriceValue, $tierPriceRound); - $tierPriceValue = $connection->getCheckSql("{$tierPriceExpr} > 0 AND o.is_require > 0", $tierPriceExpr, 0); + $tierPriceValue = $connection->getCheckSql("{$tierPriceExpr} > 0 AND o.is_require = 1", $tierPriceExpr, 0); $tierPrice = $connection->getCheckSql("i.base_tier IS NOT NULL", $tierPriceValue, "NULL"); $select->columns( diff --git a/app/code/Magento/Catalog/Test/Unit/Block/Category/Plugin/PriceBoxTagsTest.php b/app/code/Magento/Catalog/Test/Unit/Block/Category/Plugin/PriceBoxTagsTest.php index 55402eb1f6f..3f388d00eaf 100644 --- a/app/code/Magento/Catalog/Test/Unit/Block/Category/Plugin/PriceBoxTagsTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Block/Category/Plugin/PriceBoxTagsTest.php @@ -16,6 +16,11 @@ class PriceBoxTagsTest extends \PHPUnit\Framework\TestCase */ private $priceCurrencyInterface; + /** + * @var \Magento\Directory\Model\Currency | \PHPUnit_Framework_MockObject_MockObject + */ + private $currency; + /** * @var \Magento\Framework\Stdlib\DateTime\TimezoneInterface | \PHPUnit_Framework_MockObject_MockObject */ @@ -46,6 +51,9 @@ protected function setUp() $this->priceCurrencyInterface = $this->getMockBuilder( \Magento\Framework\Pricing\PriceCurrencyInterface::class )->getMock(); + $this->currency = $this->getMockBuilder(\Magento\Directory\Model\Currency::class) + ->disableOriginalConstructor() + ->getMock(); $this->timezoneInterface = $this->getMockBuilder( \Magento\Framework\Stdlib\DateTime\TimezoneInterface::class )->getMock(); @@ -82,7 +90,7 @@ protected function setUp() public function testAfterGetCacheKey() { $date = date('Ymd'); - $currencySymbol = '$'; + $currencyCode = 'USD'; $result = 'result_string'; $billingAddress = ['billing_address']; $shippingAddress = ['shipping_address']; @@ -95,7 +103,7 @@ public function testAfterGetCacheKey() '-', [ $result, - $currencySymbol, + $currencyCode, $date, $scopeId, $customerGroupId, @@ -104,7 +112,8 @@ public function testAfterGetCacheKey() ); $priceBox = $this->getMockBuilder(\Magento\Framework\Pricing\Render\PriceBox::class) ->disableOriginalConstructor()->getMock(); - $this->priceCurrencyInterface->expects($this->once())->method('getCurrencySymbol')->willReturn($currencySymbol); + $this->priceCurrencyInterface->expects($this->once())->method('getCurrency')->willReturn($this->currency); + $this->currency->expects($this->once())->method('getCode')->willReturn($currencyCode); $scope = $this->getMockBuilder(\Magento\Framework\App\ScopeInterface::class)->getMock(); $this->scopeResolverInterface->expects($this->any())->method('getScope')->willReturn($scope); $scope->expects($this->any())->method('getId')->willReturn($scopeId); diff --git a/app/code/Magento/Catalog/Test/Unit/Model/ResourceModel/CategoryTest.php b/app/code/Magento/Catalog/Test/Unit/Model/ResourceModel/CategoryTest.php index 4812751792f..b7d05fd2b70 100644 --- a/app/code/Magento/Catalog/Test/Unit/Model/ResourceModel/CategoryTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Model/ResourceModel/CategoryTest.php @@ -7,6 +7,7 @@ namespace Magento\Catalog\Test\Unit\Model\ResourceModel; use Magento\Catalog\Model\Factory; +use Magento\Catalog\Model\Indexer\Category\Product\Processor; use Magento\Catalog\Model\ResourceModel\Category; use Magento\Catalog\Model\ResourceModel\Category\CollectionFactory; use Magento\Eav\Model\Config; @@ -91,6 +92,11 @@ class CategoryTest extends \PHPUnit\Framework\TestCase */ private $serializerMock; + /** + * @var Processor|\PHPUnit_Framework_MockObject_MockObject + */ + private $indexerProcessorMock; + /** * {@inheritDoc} */ @@ -121,6 +127,9 @@ protected function setUp() $this->collectionFactoryMock = $this->getMockBuilder(CollectionFactory::class) ->disableOriginalConstructor() ->getMock(); + $this->indexerProcessorMock = $this->getMockBuilder(Processor::class) + ->disableOriginalConstructor() + ->getMock(); $this->serializerMock = $this->getMockBuilder(Json::class)->getMock(); @@ -131,6 +140,7 @@ protected function setUp() $this->managerMock, $this->treeFactoryMock, $this->collectionFactoryMock, + $this->indexerProcessorMock, [], $this->serializerMock ); diff --git a/app/code/Magento/Catalog/Test/Unit/Ui/DataProvider/Product/Form/Modifier/AbstractModifierTest.php b/app/code/Magento/Catalog/Test/Unit/Ui/DataProvider/Product/Form/Modifier/AbstractModifierTest.php index af10eeea42f..473f1aea336 100644 --- a/app/code/Magento/Catalog/Test/Unit/Ui/DataProvider/Product/Form/Modifier/AbstractModifierTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Ui/DataProvider/Product/Form/Modifier/AbstractModifierTest.php @@ -63,7 +63,8 @@ protected function setUp() 'getAttributes', 'getStore', 'getAttributeDefaultValue', - 'getExistsStoreValueFlag' + 'getExistsStoreValueFlag', + 'isLockedAttribute' ])->getMockForAbstractClass(); $this->storeMock = $this->getMockBuilder(StoreInterface::class) ->setMethods(['load', 'getId', 'getConfig']) @@ -81,9 +82,6 @@ protected function setUp() $this->arrayManagerMock->expects($this->any()) ->method('set') ->willReturnArgument(1); - $this->arrayManagerMock->expects($this->any()) - ->method('merge') - ->willReturnArgument(1); $this->arrayManagerMock->expects($this->any()) ->method('remove') ->willReturnArgument(1); diff --git a/app/code/Magento/Catalog/Test/Unit/Ui/DataProvider/Product/Form/Modifier/AttributeSetTest.php b/app/code/Magento/Catalog/Test/Unit/Ui/DataProvider/Product/Form/Modifier/AttributeSetTest.php index c22dde0b456..5fc6231b037 100644 --- a/app/code/Magento/Catalog/Test/Unit/Ui/DataProvider/Product/Form/Modifier/AttributeSetTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Ui/DataProvider/Product/Form/Modifier/AttributeSetTest.php @@ -5,6 +5,7 @@ */ namespace Magento\Catalog\Test\Unit\Ui\DataProvider\Product\Form\Modifier; +use Magento\Catalog\Ui\DataProvider\Product\Form\Modifier\AbstractModifier; use Magento\Catalog\Ui\DataProvider\Product\Form\Modifier\AttributeSet; use Magento\Eav\Model\ResourceModel\Entity\Attribute\Set\CollectionFactory; use Magento\Eav\Model\ResourceModel\Entity\Attribute\Set\Collection; @@ -84,7 +85,30 @@ protected function createModel() public function testModifyMeta() { - $this->assertNotEmpty($this->getModel()->modifyMeta(['test_group' => []])); + $modifyMeta = $this->getModel()->modifyMeta(['test_group' => []]); + $this->assertNotEmpty($modifyMeta); + } + + /** + * @param bool $locked + * @dataProvider modifyMetaLockedDataProvider + */ + public function testModifyMetaLocked($locked) + { + $this->productMock->expects($this->any()) + ->method('isLockedAttribute') + ->willReturn($locked); + $modifyMeta = $this->getModel()->modifyMeta([AbstractModifier::DEFAULT_GENERAL_PANEL => []]); + $children = $modifyMeta[AbstractModifier::DEFAULT_GENERAL_PANEL]['children']; + $this->assertEquals( + $locked, + $children['attribute_set_id']['arguments']['data']['config']['disabled'] + ); + } + + public function modifyMetaLockedDataProvider() + { + return [[true], [false]]; } public function testModifyMetaToBeEmpty() 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 4daff7e7930..5f5913c2020 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 @@ -114,6 +114,44 @@ public function testModifyMeta() $this->assertArrayHasKey($groupCode, $this->getModel()->modifyMeta($meta)); } + /** + * @param bool $locked + * @dataProvider modifyMetaLockedDataProvider + */ + public function testModifyMetaLocked($locked) + { + $groupCode = 'test_group_code'; + $meta = [ + $groupCode => [ + 'children' => [ + 'category_ids' => [ + 'sortOrder' => 10, + ], + ], + ], + ]; + + $this->arrayManagerMock->expects($this->any()) + ->method('findPath') + ->willReturn('path'); + + $this->productMock->expects($this->any()) + ->method('isLockedAttribute') + ->willReturn($locked); + + $this->arrayManagerMock->expects($this->any()) + ->method('merge') + ->willReturnArgument(2); + + $modifyMeta = $this->createModel()->modifyMeta($meta); + $this->assertEquals($locked, $modifyMeta['arguments']['data']['config']['disabled']); + } + + public function modifyMetaLockedDataProvider() + { + return [[true], [false]]; + } + public function testModifyMetaWithCaching() { $this->arrayManagerMock->expects($this->exactly(2)) 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 0426e389d9a..22bb712d42f 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 @@ -462,7 +462,7 @@ public function testModifyData() * @param bool $productRequired * @param string|null $attrValue * @param array $expected - * @return void + * @param bool $locked * @covers \Magento\Catalog\Ui\DataProvider\Product\Form\Modifier\Eav::isProductExists * @covers \Magento\Catalog\Ui\DataProvider\Product\Form\Modifier\Eav::setupAttributeMeta * @dataProvider setupAttributeMetaDataProvider @@ -471,7 +471,8 @@ public function testSetupAttributeMetaDefaultAttribute( $productId, bool $productRequired, $attrValue, - array $expected + array $expected, + $locked = false ) : void { $configPath = 'arguments/data/config'; $groupCode = 'product-details'; @@ -492,6 +493,7 @@ public function testSetupAttributeMetaDefaultAttribute( ]; $this->productMock->method('getId')->willReturn($productId); + $this->productMock->expects($this->any())->method('isLockedAttribute')->willReturn($locked); $this->productAttributeMock->method('getIsRequired')->willReturn($productRequired); $this->productAttributeMock->method('getDefaultValue')->willReturn('required_value'); $this->productAttributeMock->method('getAttributeCode')->willReturn('code'); @@ -520,14 +522,14 @@ public function testSetupAttributeMetaDefaultAttribute( ) ->willReturn($expected); - $this->arrayManagerMock->expects($this->once()) + $this->arrayManagerMock->expects($this->any()) ->method('merge') ->with( $this->anything(), $this->anything(), $this->callback( function ($value) use ($attributeOptionsExpected) { - return $value['options'] === $attributeOptionsExpected; + return isset($value['options']) ? $value['options'] === $attributeOptionsExpected : true; } ) ) @@ -544,6 +546,7 @@ function ($value) use ($attributeOptionsExpected) { /** * @return array + * @SuppressWarnings(PHPMD.ExcessiveMethodLength) */ public function setupAttributeMetaDataProvider() { @@ -567,6 +570,26 @@ public function setupAttributeMetaDataProvider() 'sortOrder' => 0, ], ], + 'default_null_prod_not_new_locked_and_required' => [ + 'productId' => 1, + 'productRequired' => true, + 'attrValue' => 'val', + 'expected' => [ + 'dataType' => null, + 'formElement' => null, + 'visible' => null, + 'required' => true, + 'notice' => null, + 'default' => null, + 'label' => new Phrase(null), + 'code' => 'code', + 'source' => 'product-details', + 'scopeLabel' => '', + 'globalScope' => false, + 'sortOrder' => 0, + ], + 'locked' => true, + ], 'default_null_prod_not_new_and_not_required' => [ 'productId' => 1, 'productRequired' => false, @@ -605,6 +628,26 @@ public function setupAttributeMetaDataProvider() 'sortOrder' => 0, ], ], + 'default_null_prod_new_locked_and_not_required' => [ + 'productId' => null, + 'productRequired' => false, + 'attrValue' => null, + 'expected' => [ + 'dataType' => null, + 'formElement' => null, + 'visible' => null, + 'required' => false, + 'notice' => null, + 'default' => 'required_value', + 'label' => new Phrase(null), + 'code' => 'code', + 'source' => 'product-details', + 'scopeLabel' => '', + 'globalScope' => false, + 'sortOrder' => 0, + ], + 'locked' => true, + ], 'default_null_prod_new_and_required' => [ 'productId' => null, 'productRequired' => false, diff --git a/app/code/Magento/Catalog/Test/Unit/Ui/DataProvider/Product/Form/Modifier/GeneralTest.php b/app/code/Magento/Catalog/Test/Unit/Ui/DataProvider/Product/Form/Modifier/GeneralTest.php index 78502ae297b..a9d717db7b7 100644 --- a/app/code/Magento/Catalog/Test/Unit/Ui/DataProvider/Product/Form/Modifier/GeneralTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Ui/DataProvider/Product/Form/Modifier/GeneralTest.php @@ -60,6 +60,9 @@ protected function createModel() public function testModifyMeta() { + $this->arrayManagerMock->expects($this->any()) + ->method('merge') + ->willReturnArgument(2); $this->assertNotEmpty($this->getModel()->modifyMeta([ 'first_panel_code' => [ 'arguments' => [ diff --git a/app/code/Magento/Catalog/Test/Unit/Ui/DataProvider/Product/Form/Modifier/ScheduleDesignUpdateTest.php b/app/code/Magento/Catalog/Test/Unit/Ui/DataProvider/Product/Form/Modifier/ScheduleDesignUpdateTest.php index d4d4136bf41..783c6247b9d 100644 --- a/app/code/Magento/Catalog/Test/Unit/Ui/DataProvider/Product/Form/Modifier/ScheduleDesignUpdateTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Ui/DataProvider/Product/Form/Modifier/ScheduleDesignUpdateTest.php @@ -24,6 +24,9 @@ protected function createModel() public function testModifyMeta() { + $this->arrayManagerMock->expects($this->any()) + ->method('merge') + ->willReturnArgument(1); $this->assertSame([], $this->getModel()->modifyMeta([])); } 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 997b66861c2..c3096770729 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 @@ -76,7 +76,10 @@ class WebsitesTest extends AbstractModifierTest protected function setUp() { - $this->objectManager = new ObjectManager($this); + parent::setUp(); + $this->productMock->expects($this->any()) + ->method('getId') + ->willReturn(self::PRODUCT_ID); $this->assignedWebsites = [self::SECOND_WEBSITE_ID]; $this->websiteMock = $this->getMockBuilder(\Magento\Store\Model\Website::class) ->setMethods(['getId', 'getName']) @@ -101,15 +104,9 @@ protected function setUp() $this->storeRepositoryMock = $this->getMockBuilder(\Magento\Store\Api\StoreRepositoryInterface::class) ->setMethods(['getList']) ->getMockForAbstractClass(); - $this->locatorMock = $this->getMockBuilder(\Magento\Catalog\Model\Locator\LocatorInterface::class) - ->setMethods(['getProduct', 'getWebsiteIds']) - ->getMockForAbstractClass(); $this->productMock = $this->getMockBuilder(\Magento\Catalog\Api\Data\ProductInterface::class) ->setMethods(['getId']) ->getMockForAbstractClass(); - $this->locatorMock->expects($this->any()) - ->method('getProduct') - ->willReturn($this->productMock); $this->locatorMock->expects($this->any()) ->method('getWebsiteIds') ->willReturn($this->assignedWebsites); @@ -148,9 +145,6 @@ protected function setUp() $this->storeRepositoryMock->expects($this->any()) ->method('getList') ->willReturn([$this->storeViewMock]); - $this->productMock->expects($this->any()) - ->method('getId') - ->willReturn(self::PRODUCT_ID); $this->secondWebsiteMock->expects($this->any()) ->method('getId') ->willReturn($this->assignedWebsites[0]); 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 a8378c364a6..336aeffa105 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 @@ -432,7 +432,8 @@ private function getTierPriceStructure($tierPricePath) 'dndConfig' => [ 'enabled' => false, ], - 'disabled' => false, + 'disabled' => + $this->arrayManager->get($tierPricePath . '/arguments/data/config/disabled', $this->meta), 'required' => false, 'sortOrder' => $this->arrayManager->get($tierPricePath . '/arguments/data/config/sortOrder', $this->meta), @@ -500,7 +501,8 @@ private function getTierPriceStructure($tierPricePath) 'validation' => [ 'required-entry' => true, 'validate-greater-than-zero' => true, - 'validate-digits' => true, + 'validate-digits' => false, + 'validate-number' => true, ], ], ], diff --git a/app/code/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/AttributeSet.php b/app/code/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/AttributeSet.php index a1aacc91f2e..0733d21bf47 100644 --- a/app/code/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/AttributeSet.php +++ b/app/code/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/AttributeSet.php @@ -108,6 +108,7 @@ public function modifyMeta(array $meta) self::ATTRIBUTE_SET_FIELD_ORDER ), 'multiple' => false, + 'disabled' => $this->locator->getProduct()->isLockedAttribute('attribute_set_id'), ]; } 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 7456c1bfef9..ed737df708a 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 @@ -228,6 +228,7 @@ protected function customizeCategoriesField(array $meta) 'componentType' => 'container', 'component' => 'Magento_Ui/js/form/components/group', 'scopeLabel' => __('[GLOBAL]'), + 'disabled' => $this->locator->getProduct()->isLockedAttribute($fieldCode), ], ], ], diff --git a/app/code/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/Eav.php b/app/code/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/Eav.php index 0e6f17d761b..7cd81419c03 100755 --- a/app/code/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/Eav.php +++ b/app/code/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/Eav.php @@ -41,6 +41,7 @@ * * @SuppressWarnings(PHPMD.CouplingBetweenObjects) * @SuppressWarnings(PHPMD.TooManyFields) + * @SuppressWarnings(PHPMD.ExcessiveClassComplexity) * @since 101.0.0 */ class Eav extends AbstractModifier @@ -593,6 +594,7 @@ private function isProductExists() public function setupAttributeMeta(ProductAttributeInterface $attribute, $groupCode, $sortOrder) { $configPath = ltrim(static::META_CONFIG_PATH, ArrayManager::DEFAULT_PATH_DELIMITER); + $attributeCode = $attribute->getAttributeCode(); $meta = $this->arrayManager->set($configPath, [], [ 'dataType' => $attribute->getFrontendInput(), 'formElement' => $this->getFormElementsMapValue($attribute->getFrontendInput()), @@ -601,7 +603,7 @@ public function setupAttributeMeta(ProductAttributeInterface $attribute, $groupC 'notice' => $attribute->getNote() === null ? null : __($attribute->getNote()), 'default' => (!$this->isProductExists()) ? $this->getAttributeDefaultValue($attribute) : null, 'label' => __($attribute->getDefaultFrontendLabel()), - 'code' => $attribute->getAttributeCode(), + 'code' => $attributeCode, 'source' => $groupCode, 'scopeLabel' => $this->getScopeLabel($attribute), 'globalScope' => $this->isScopeGlobal($attribute), @@ -631,7 +633,9 @@ public function setupAttributeMeta(ProductAttributeInterface $attribute, $groupC ]); } - if (in_array($attribute->getAttributeCode(), $this->attributesToDisable)) { + $product = $this->locator->getProduct(); + if (in_array($attributeCode, $this->attributesToDisable) + || $product->isLockedAttribute($attributeCode)) { $meta = $this->arrayManager->merge($configPath, $meta, [ 'disabled' => true, ]); 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 03d4dde3114..98de8ea3476 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 @@ -122,7 +122,7 @@ protected function customizeAdvancedPriceFormat(array $data) $value[ProductAttributeInterface::CODE_TIER_PRICE_FIELD_PRICE] = $this->formatPrice($value[ProductAttributeInterface::CODE_TIER_PRICE_FIELD_PRICE]); $value[ProductAttributeInterface::CODE_TIER_PRICE_FIELD_PRICE_QTY] = - (int)$value[ProductAttributeInterface::CODE_TIER_PRICE_FIELD_PRICE_QTY]; + (float) $value[ProductAttributeInterface::CODE_TIER_PRICE_FIELD_PRICE_QTY]; } } @@ -203,7 +203,7 @@ protected function customizeStatusField(array $meta) protected function customizeWeightField(array $meta) { $weightPath = $this->arrayManager->findPath(ProductAttributeInterface::CODE_WEIGHT, $meta, null, 'children'); - + $disabled = $this->arrayManager->get($weightPath . '/arguments/data/config/disabled', $meta); if ($weightPath) { $meta = $this->arrayManager->merge( $weightPath . static::META_CONFIG_PATH, @@ -215,7 +215,7 @@ protected function customizeWeightField(array $meta) ], 'additionalClasses' => 'admin__field-small', 'addafter' => $this->locator->getStore()->getConfig('general/locale/weight_unit'), - 'imports' => [ + 'imports' => $disabled ? [] : [ 'disabled' => '!${$.provider}:' . self::DATA_SCOPE_PRODUCT . '.product_has_weight:value' ] @@ -255,6 +255,7 @@ protected function customizeWeightField(array $meta) ], ], 'value' => (int)$this->locator->getProduct()->getTypeInstance()->hasWeight(), + 'disabled' => $disabled, ] ); } 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 298da3d5cd6..bab36ce5fc4 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 @@ -135,7 +135,6 @@ public function modifyMeta(array $meta) 'collapsible' => true, 'componentType' => Form\Fieldset::NAME, 'dataScope' => self::DATA_SCOPE_PRODUCT, - 'disabled' => false, 'sortOrder' => $this->getNextGroupSortOrder( $meta, 'search-engine-optimization', @@ -196,6 +195,7 @@ protected function getFieldsForFieldset() 'false' => '0', ], 'value' => $isChecked ? (string)$website['id'] : '0', + 'disabled' => $this->locator->getProduct()->isLockedAttribute('website_ids'), ], ], ], diff --git a/app/code/Magento/Catalog/etc/adminhtml/menu.xml b/app/code/Magento/Catalog/etc/adminhtml/menu.xml index aa910e6d5ad..cfcce3a26cb 100644 --- a/app/code/Magento/Catalog/etc/adminhtml/menu.xml +++ b/app/code/Magento/Catalog/etc/adminhtml/menu.xml @@ -12,7 +12,6 @@ - diff --git a/app/code/Magento/Catalog/i18n/en_US.csv b/app/code/Magento/Catalog/i18n/en_US.csv index 0727d03df13..f2a7cf0b195 100644 --- a/app/code/Magento/Catalog/i18n/en_US.csv +++ b/app/code/Magento/Catalog/i18n/en_US.csv @@ -516,6 +516,9 @@ Groups,Groups "Maximum image width","Maximum image width" "Maximum image height","Maximum image height" "Maximum number of characters:","Maximum number of characters:" +"Maximum %1 characters", "Maximum %1 characters" +"too many", "too many" +"remaining", "remaining" "start typing to search template","start typing to search template" "Product online","Product online" "Product offline","Product offline" 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 787516a9abf..6ea00591576 100644 --- a/app/code/Magento/Catalog/view/adminhtml/web/js/options.js +++ b/app/code/Magento/Catalog/view/adminhtml/web/js/options.js @@ -13,12 +13,16 @@ define([ 'jquery/ui', 'prototype', 'form', - 'validation' + 'validation', + 'mage/translate' ], function (jQuery, mageTemplate, rg) { 'use strict'; return function (config) { - var attributeOption = { + var optionPanel = jQuery('#manage-options-panel'), + optionsValues = [], + editForm = jQuery('#edit_form'), + attributeOption = { table: $('attribute-options-table'), itemCount: 0, totalItems: 0, @@ -150,7 +154,7 @@ define([ attributeOption.remove(event); }); - jQuery('#manage-options-panel').on('render', function () { + optionPanel.on('render', function () { attributeOption.ignoreValidate(); if (attributeOption.rendered) { @@ -176,7 +180,31 @@ define([ }); }); } + editForm.on('submit', function () { + optionPanel.find('input') + .each(function () { + if (this.disabled) { + return; + } + 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('') + .attr({ + type: 'hidden', + name: 'serialized_options' + }) + .val(JSON.stringify(optionsValues)) + .prependTo(editForm); + optionPanel.find('table') + .replaceWith(jQuery('
').text(jQuery.mage.__('Sending attribute values as package.'))); + }); window.attributeOption = attributeOption; window.optionDefaultInputType = attributeOption.getOptionInputType(); diff --git a/app/code/Magento/Catalog/view/frontend/templates/product/breadcrumbs.phtml b/app/code/Magento/Catalog/view/frontend/templates/product/breadcrumbs.phtml index 528b2b5c59f..063f8857329 100644 --- a/app/code/Magento/Catalog/view/frontend/templates/product/breadcrumbs.phtml +++ b/app/code/Magento/Catalog/view/frontend/templates/product/breadcrumbs.phtml @@ -11,7 +11,7 @@ $viewModel = $block->getData('viewModel'); "breadcrumbs": { "categoryUrlSuffix": "escapeHtml($viewModel->getCategoryUrlSuffix()); ?>", "useCategoryPathInUrl": isCategoryUsedInProductUrl(); ?>, - "product": "escapeHtml($block->escapeJsQuote($viewModel->getProductName(), '"')); ?>" + "product": "escapeHtml($block->escapeJs($viewModel->getProductName())); ?>" } }'>
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 11aedc33c2d..852e0095f2f 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 @@ -61,8 +61,23 @@ $class = ($_option->getIsRequire()) ? ' required' : ''; cols="25">escapeHtml($block->getDefaultValue()) ?> getMaxCharacters()): ?> -

- getMaxCharacters() ?>

+

+ getMaxCharacters()) ?> + +

+ getMaxCharacters()): ?> + + diff --git a/app/code/Magento/Catalog/view/frontend/web/js/product/remaining-characters.js b/app/code/Magento/Catalog/view/frontend/web/js/product/remaining-characters.js new file mode 100644 index 00000000000..3e29e1ebd4d --- /dev/null +++ b/app/code/Magento/Catalog/view/frontend/web/js/product/remaining-characters.js @@ -0,0 +1,62 @@ +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +define([ + 'jquery', + 'mage/translate', + 'jquery/ui' +], function ($, $t) { + 'use strict'; + + $.widget('mage.remainingCharacters', { + options: { + remainingText: $t('remaining'), + tooManyText: $t('too many'), + errorClass: 'mage-error', + noDisplayClass: 'no-display' + }, + + /** + * Initializes custom option component + * + * @private + */ + _create: function () { + this.note = $(this.options.noteSelector); + this.counter = $(this.options.counterSelector); + + this.updateCharacterCount(); + this.element.on('change keyup paste', this.updateCharacterCount.bind(this)); + }, + + /** + * Updates counter message + */ + updateCharacterCount: function () { + var length = this.element.val().length, + diff = this.options.maxLength - length; + + this.counter.text(this._formatMessage(diff)); + this.counter.toggleClass(this.options.noDisplayClass, length === 0); + this.note.toggleClass(this.options.errorClass, diff < 0); + }, + + /** + * Format remaining characters message + * + * @param {int} diff + * @returns {String} + * @private + */ + _formatMessage: function (diff) { + var count = Math.abs(diff), + qualifier = diff < 0 ? this.options.tooManyText : this.options.remainingText; + + return '(' + count + ' ' + qualifier + ')'; + } + }); + + return $.mage.remainingCharacters; +}); diff --git a/app/code/Magento/CatalogGraphQl/etc/schema.graphqls b/app/code/Magento/CatalogGraphQl/etc/schema.graphqls index 5d9e174f169..f9df24e8ff7 100644 --- a/app/code/Magento/CatalogGraphQl/etc/schema.graphqls +++ b/app/code/Magento/CatalogGraphQl/etc/schema.graphqls @@ -492,7 +492,6 @@ input ProductSortInput @doc(description: "ProductSortInput specifies the attrib 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") - category_ids: SortEnum @doc(description: "An array of category IDs the product belongs to") 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") diff --git a/app/code/Magento/CatalogInventory/Api/Data/StockStatusInterface.php b/app/code/Magento/CatalogInventory/Api/Data/StockStatusInterface.php index 83defa64df2..c9ae6a96a36 100644 --- a/app/code/Magento/CatalogInventory/Api/Data/StockStatusInterface.php +++ b/app/code/Magento/CatalogInventory/Api/Data/StockStatusInterface.php @@ -14,6 +14,14 @@ */ interface StockStatusInterface extends ExtensibleDataInterface { + /**#@+ + * Stock Status values. + */ + const STATUS_OUT_OF_STOCK = 0; + + const STATUS_IN_STOCK = 1; + /**#@-*/ + /**#@+ * Stock status object data keys */ diff --git a/app/code/Magento/CatalogInventory/Model/Stock/Status.php b/app/code/Magento/CatalogInventory/Model/Stock/Status.php index 9a56c8e8804..899056d8f08 100644 --- a/app/code/Magento/CatalogInventory/Model/Stock/Status.php +++ b/app/code/Magento/CatalogInventory/Model/Stock/Status.php @@ -17,14 +17,6 @@ */ class Status extends AbstractExtensibleModel implements StockStatusInterface { - /**#@+ - * Stock Status values - */ - const STATUS_OUT_OF_STOCK = 0; - - const STATUS_IN_STOCK = 1; - /**#@-*/ - /**#@+ * Field name */ diff --git a/app/code/Magento/CatalogInventory/Model/Stock/StockItemRepository.php b/app/code/Magento/CatalogInventory/Model/Stock/StockItemRepository.php index 6928ab99470..515080d5654 100644 --- a/app/code/Magento/CatalogInventory/Model/Stock/StockItemRepository.php +++ b/app/code/Magento/CatalogInventory/Model/Stock/StockItemRepository.php @@ -11,7 +11,7 @@ use Magento\CatalogInventory\Api\Data\StockItemInterface; use Magento\CatalogInventory\Api\Data\StockItemInterfaceFactory; use Magento\CatalogInventory\Api\StockConfigurationInterface; -use Magento\CatalogInventory\Api\StockItemRepositoryInterface as StockItemRepositoryInterface; +use Magento\CatalogInventory\Api\StockItemRepositoryInterface; use Magento\CatalogInventory\Model\Indexer\Stock\Processor; use Magento\CatalogInventory\Model\ResourceModel\Stock\Item as StockItemResource; use Magento\CatalogInventory\Model\Spi\StockStateProviderInterface; diff --git a/app/code/Magento/CatalogInventory/Ui/DataProvider/Product/Form/Modifier/AdvancedInventory.php b/app/code/Magento/CatalogInventory/Ui/DataProvider/Product/Form/Modifier/AdvancedInventory.php index d15f17530ff..7386f133b56 100644 --- a/app/code/Magento/CatalogInventory/Ui/DataProvider/Product/Form/Modifier/AdvancedInventory.php +++ b/app/code/Magento/CatalogInventory/Ui/DataProvider/Product/Form/Modifier/AdvancedInventory.php @@ -223,6 +223,7 @@ private function prepareMeta() $this->arrayManager->slicePath($pathField, 0, -2) . '/arguments/data/config/sortOrder', $this->meta ) - 1, + 'disabled' => $this->locator->getProduct()->isLockedAttribute($fieldCode), ]; $qty['arguments']['data']['config'] = [ 'component' => 'Magento_CatalogInventory/js/components/qty-validator-changer', diff --git a/app/code/Magento/CatalogInventory/view/adminhtml/web/js/components/qty-validator-changer.js b/app/code/Magento/CatalogInventory/view/adminhtml/web/js/components/qty-validator-changer.js index 75d684137a2..23a33f51af6 100644 --- a/app/code/Magento/CatalogInventory/view/adminhtml/web/js/components/qty-validator-changer.js +++ b/app/code/Magento/CatalogInventory/view/adminhtml/web/js/components/qty-validator-changer.js @@ -20,6 +20,7 @@ define([ var isDigits = value !== 1; this.validation['validate-integer'] = isDigits; + this.validation['validate-digits'] = isDigits; this.validation['less-than-equals-to'] = isDigits ? 99999999 : 99999999.9999; this.validate(); } diff --git a/app/code/Magento/CatalogSearch/Model/Indexer/Fulltext/Model/Plugin/Category.php b/app/code/Magento/CatalogSearch/Model/Indexer/Fulltext/Model/Plugin/Category.php new file mode 100644 index 00000000000..ed841996ea0 --- /dev/null +++ b/app/code/Magento/CatalogSearch/Model/Indexer/Fulltext/Model/Plugin/Category.php @@ -0,0 +1,45 @@ +fulltextIndexerProcessor = $fulltextIndexerProcessor; + } + + /** + * Mark fulltext indexer as invalid post-deletion of category. + * + * @param Resource $subjectCategory + * @param Resource $resultCategory + * @return Resource + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function afterDelete(Resource $subjectCategory, Resource $resultCategory) : Resource + { + $this->fulltextIndexerProcessor->markIndexerAsInvalid(); + + return $resultCategory; + } +} diff --git a/app/code/Magento/CatalogSearch/etc/adminhtml/di.xml b/app/code/Magento/CatalogSearch/etc/adminhtml/di.xml index d6c72d883fe..2d41d17889e 100644 --- a/app/code/Magento/CatalogSearch/etc/adminhtml/di.xml +++ b/app/code/Magento/CatalogSearch/etc/adminhtml/di.xml @@ -31,4 +31,7 @@ Magento\CatalogSearch\Ui\DataProvider\Product\AddFulltextFilterToCollection + + + diff --git a/app/code/Magento/CatalogSearch/etc/webapi_rest/di.xml b/app/code/Magento/CatalogSearch/etc/webapi_rest/di.xml new file mode 100644 index 00000000000..c7293783dc6 --- /dev/null +++ b/app/code/Magento/CatalogSearch/etc/webapi_rest/di.xml @@ -0,0 +1,12 @@ + + + + + + + diff --git a/app/code/Magento/CatalogSearch/etc/webapi_soap/di.xml b/app/code/Magento/CatalogSearch/etc/webapi_soap/di.xml new file mode 100644 index 00000000000..c7293783dc6 --- /dev/null +++ b/app/code/Magento/CatalogSearch/etc/webapi_soap/di.xml @@ -0,0 +1,12 @@ + + + + + + + diff --git a/app/code/Magento/Checkout/etc/webapi.xml b/app/code/Magento/Checkout/etc/webapi.xml index 7b435db200f..26c601a4e9f 100644 --- a/app/code/Magento/Checkout/etc/webapi.xml +++ b/app/code/Magento/Checkout/etc/webapi.xml @@ -104,7 +104,7 @@ - + diff --git a/app/code/Magento/Checkout/view/frontend/web/js/proceed-to-checkout.js b/app/code/Magento/Checkout/view/frontend/web/js/proceed-to-checkout.js index f0679c657ab..0bb0a53ce0a 100644 --- a/app/code/Magento/Checkout/view/frontend/web/js/proceed-to-checkout.js +++ b/app/code/Magento/Checkout/view/frontend/web/js/proceed-to-checkout.js @@ -22,6 +22,7 @@ define([ return false; } + $(element).attr('disabled', true); location.href = config.checkoutUrl; }); diff --git a/app/code/Magento/ConfigurableProduct/Test/Unit/Ui/DataProvider/Product/Form/Modifier/CustomOptionsTest.php b/app/code/Magento/ConfigurableProduct/Test/Unit/Ui/DataProvider/Product/Form/Modifier/CustomOptionsTest.php index e2e9fe9b2b1..659f7346fab 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Unit/Ui/DataProvider/Product/Form/Modifier/CustomOptionsTest.php +++ b/app/code/Magento/ConfigurableProduct/Test/Unit/Ui/DataProvider/Product/Form/Modifier/CustomOptionsTest.php @@ -10,6 +10,14 @@ class CustomOptionsTest extends AbstractModifierTest { + protected function setUp() + { + parent::setUp(); + $this->arrayManagerMock->expects($this->any()) + ->method('merge') + ->willReturnArgument(1); + } + /** * {@inheritdoc} */ diff --git a/app/code/Magento/Customer/view/frontend/templates/form/login.phtml b/app/code/Magento/Customer/view/frontend/templates/form/login.phtml index 16206525aa5..77e250c5de9 100644 --- a/app/code/Magento/Customer/view/frontend/templates/form/login.phtml +++ b/app/code/Magento/Customer/view/frontend/templates/form/login.phtml @@ -42,3 +42,13 @@ + + diff --git a/app/code/Magento/Customer/view/frontend/web/js/trim-username.js b/app/code/Magento/Customer/view/frontend/web/js/trim-username.js new file mode 100644 index 00000000000..1b6aab60868 --- /dev/null +++ b/app/code/Magento/Customer/view/frontend/web/js/trim-username.js @@ -0,0 +1,65 @@ +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +define([ + 'jquery' +], function ($) { + 'use strict'; + + $.widget('mage.trimUsername', { + options: { + cache: {}, + formSelector: 'form', + emailSelector: 'input[type="email"]' + }, + + /** + * Widget initialization + * @private + */ + _create: function () { + // We need to look outside the module for backward compatibility, since someone can already use the module. + // @todo Narrow this selector in 2.3 so it doesn't accidentally finds the email field from the + // newsletter email field or any other "email" field. + this.options.cache.email = $(this.options.formSelector).find(this.options.emailSelector); + this._bind(); + }, + + /** + * Event binding, will monitor change, keyup and paste events. + * @private + */ + _bind: function () { + if (this.options.cache.email.length) { + this._on(this.options.cache.email, { + 'change': this._trimUsername, + 'keyup': this._trimUsername, + 'paste': this._trimUsername + }); + } + }, + + /** + * Trim username + * @private + */ + _trimUsername: function () { + var username = this._getUsername().trim(); + + this.options.cache.email.val(username); + }, + + /** + * Get username value + * @returns {*} + * @private + */ + _getUsername: function () { + return this.options.cache.email.val(); + } + }); + + return $.mage.trimUsername; +}); diff --git a/app/code/Magento/CustomerImportExport/Model/Import/Address.php b/app/code/Magento/CustomerImportExport/Model/Import/Address.php index d5564f207fb..e1345edcf14 100644 --- a/app/code/Magento/CustomerImportExport/Model/Import/Address.php +++ b/app/code/Magento/CustomerImportExport/Model/Import/Address.php @@ -10,6 +10,8 @@ use Magento\Framework\App\ObjectManager; use Magento\ImportExport\Model\Import\ErrorProcessing\ProcessingErrorAggregatorInterface; use Magento\Store\Model\Store; +use Magento\CustomerImportExport\Model\ResourceModel\Import\Address\Storage as AddressStorage; +use Magento\ImportExport\Model\Import\AbstractSource; /** * Customer address import @@ -85,20 +87,6 @@ class Address extends AbstractCustomer */ protected $_permanentAttributes = [self::COLUMN_WEBSITE, self::COLUMN_EMAIL, self::COLUMN_ADDRESS_ID]; - /** - * Existing addresses - * - * Example Array: [customer ID] => array( - * address ID 1, - * address ID 2, - * ... - * address ID N - * ) - * - * @var array - */ - protected $_addresses = []; - /** * Attributes with index (not label) value * @@ -180,13 +168,6 @@ class Address extends AbstractCustomer */ protected $_attributeCollection; - /** - * Collection of existent addresses - * - * @var \Magento\Customer\Model\ResourceModel\Address\Collection - */ - protected $_addressCollection; - /** * Store imported row primary keys * @@ -252,6 +233,11 @@ class Address extends AbstractCustomer */ private $optionsByWebsite = []; + /** + * @var AddressStorage + */ + private $addressStorage; + /** * @param \Magento\Framework\Stdlib\StringUtils $string * @param \Magento\Framework\App\Config\ScopeConfigInterface $scopeConfig @@ -266,12 +252,12 @@ class Address extends AbstractCustomer * @param \Magento\Customer\Model\AddressFactory $addressFactory * @param \Magento\Directory\Model\ResourceModel\Region\CollectionFactory $regionColFactory * @param \Magento\Customer\Model\CustomerFactory $customerFactory - * @param \Magento\Customer\Model\ResourceModel\Address\CollectionFactory $addressColFactory * @param \Magento\Customer\Model\ResourceModel\Address\Attribute\CollectionFactory $attributesFactory * @param \Magento\Framework\Stdlib\DateTime $dateTime * @param \Magento\Customer\Model\Address\Validator\Postcode $postcodeValidator * @param array $data * @param CountryWithWebsitesSource|null $countryWithWebsites + * @param AddressStorage|null $addressStorage * * @SuppressWarnings(PHPMD.NPathComplexity) * @SuppressWarnings(PHPMD.ExcessiveParameterList) @@ -290,12 +276,12 @@ public function __construct( \Magento\Customer\Model\AddressFactory $addressFactory, \Magento\Directory\Model\ResourceModel\Region\CollectionFactory $regionColFactory, \Magento\Customer\Model\CustomerFactory $customerFactory, - \Magento\Customer\Model\ResourceModel\Address\CollectionFactory $addressColFactory, \Magento\Customer\Model\ResourceModel\Address\Attribute\CollectionFactory $attributesFactory, \Magento\Framework\Stdlib\DateTime $dateTime, \Magento\Customer\Model\Address\Validator\Postcode $postcodeValidator, array $data = [], - CountryWithWebsitesSource $countryWithWebsites = null + CountryWithWebsitesSource $countryWithWebsites = null, + AddressStorage $addressStorage = null ) { $this->_customerFactory = $customerFactory; $this->_addressFactory = $addressFactory; @@ -326,9 +312,6 @@ public function __construct( $data ); - $this->_addressCollection = isset( - $data['address_collection'] - ) ? $data['address_collection'] : $addressColFactory->create(); $this->_entityTable = isset( $data['entity_table'] ) ? $data['entity_table'] : $addressFactory->create()->getResource()->getEntityTable(); @@ -346,9 +329,11 @@ public function __construct( self::ERROR_DUPLICATE_PK, __('We found another row with this email, website and address ID combination.') ); + $this->addressStorage = $addressStorage + ?: ObjectManager::getInstance()->get(AddressStorage::class); $this->_initAttributes(); - $this->_initAddresses()->_initCountryRegions(); + $this->_initCountryRegions(); } /** @@ -435,27 +420,6 @@ protected function _getNextEntityId() return $this->_nextEntityId++; } - /** - * Initialize existent addresses data - * - * @return $this - */ - protected function _initAddresses() - { - /** @var $address \Magento\Customer\Model\Address */ - foreach ($this->_addressCollection as $address) { - $customerId = $address->getParentId(); - if (!isset($this->_addresses[$customerId])) { - $this->_addresses[$customerId] = []; - } - $addressId = $address->getId(); - if (!in_array($addressId, $this->_addresses[$customerId])) { - $this->_addresses[$customerId][] = $addressId; - } - } - return $this; - } - /** * Initialize country regions hash for clever recognition * @@ -475,6 +439,56 @@ protected function _initCountryRegions() return $this; } + /** + * Pre-loading customers for existing customers checks in order + * to perform mass validation/import efficiently. + * Also loading existing addresses for requested customers. + * + * @param array|AbstractSource $rows Each row must contain data from columns email + * and website code. + * + * @return void + */ + public function prepareCustomerData($rows): void + { + $customersPresent = []; + foreach ($rows as $rowData) { + $email = $rowData[static::COLUMN_EMAIL] ?? null; + $websiteId = isset($rowData[static::COLUMN_WEBSITE]) + ? $this->getWebsiteId($rowData[static::COLUMN_WEBSITE]) : false; + if ($email && $websiteId !== false) { + $customersPresent[] = [ + 'email' => $email, + 'website_id' => $websiteId, + ]; + } + } + $this->getCustomerStorage()->prepareCustomers($customersPresent); + + $ids = []; + foreach ($customersPresent as $customerData) { + $id = $this->getCustomerStorage()->getCustomerId( + $customerData['email'], + $customerData['website_id'] + ); + if ($id) { + $ids[] = $id; + } + } + + $this->addressStorage->prepareAddresses($ids); + } + + /** + * @inheritDoc + */ + public function validateData() + { + $this->prepareCustomerData($this->getSource()); + + return parent::validateData(); + } + /** * Import data rows * @@ -484,6 +498,16 @@ protected function _initCountryRegions() */ protected function _importData() { + //Preparing data for mass validation/import. + $rows = []; + while ($bunch = $this->_dataSourceModel->getNextBunch()) { + $rows = array_merge($rows, $bunch); + } + $this->prepareCustomerData($rows); + unset($bunch, $rows); + $this->_dataSourceModel->getIterator()->rewind(); + + //Importing while ($bunch = $this->_dataSourceModel->getNextBunch()) { $newRows = []; $updateRows = []; @@ -554,7 +578,7 @@ protected function _mergeEntityAttributes(array $newAttributes, array $attribute * @SuppressWarnings(PHPMD.CyclomaticComplexity) * @SuppressWarnings(PHPMD.NPathComplexity) */ - protected function _prepareDataForUpdate(array $rowData) + protected function _prepareDataForUpdate(array $rowData):array { $multiSeparator = $this->getMultipleValueSeparator(); $email = strtolower($rowData[self::COLUMN_EMAIL]); @@ -568,12 +592,11 @@ protected function _prepareDataForUpdate(array $rowData) $defaults = []; $newAddress = true; // get address id - if (isset( - $this->_addresses[$customerId] - ) && in_array( - $rowData[self::COLUMN_ADDRESS_ID], - $this->_addresses[$customerId] - ) + if ($rowData[self::COLUMN_ADDRESS_ID] + && $this->addressStorage->doesExist( + $rowData[self::COLUMN_ADDRESS_ID], + (string)$customerId + ) ) { $newAddress = false; $addressId = $rowData[self::COLUMN_ADDRESS_ID]; @@ -823,12 +846,11 @@ protected function _validateRowForUpdate(array $rowData, $rowNumber) $rowNumber, $multiSeparator ); - } elseif ($attributeParams['is_required'] && (!isset( - $this->_addresses[$customerId] - ) || !in_array( - $addressId, - $this->_addresses[$customerId] - )) + } elseif ($attributeParams['is_required'] + && !$this->addressStorage->doesExist( + (string)$addressId, + (string)$customerId + ) ) { $this->addRowError(self::ERROR_VALUE_IS_REQUIRED, $rowNumber, $attributeCode); } @@ -883,7 +905,10 @@ protected function _validateRowForDelete(array $rowData, $rowNumber) } else { if (!strlen($addressId)) { $this->addRowError(self::ERROR_ADDRESS_ID_IS_EMPTY, $rowNumber); - } elseif (!in_array($addressId, $this->_addresses[$customerId])) { + } elseif (!$this->addressStorage->doesExist( + (string)$addressId, + (string)$customerId + )) { $this->addRowError(self::ERROR_ADDRESS_NOT_FOUND, $rowNumber); } } @@ -899,7 +924,10 @@ protected function _validateRowForDelete(array $rowData, $rowNumber) */ protected function _checkRowDuplicate($customerId, $addressId) { - if (isset($this->_addresses[$customerId]) && in_array($addressId, $this->_addresses[$customerId])) { + if ($this->addressStorage->doesExist( + (string)$addressId, + (string)$customerId + )) { if (!isset($this->_importedRowPks[$customerId][$addressId])) { $this->_importedRowPks[$customerId][$addressId] = true; return false; diff --git a/app/code/Magento/CustomerImportExport/Model/Import/Customer.php b/app/code/Magento/CustomerImportExport/Model/Import/Customer.php index ad405190262..e5cc543db6a 100644 --- a/app/code/Magento/CustomerImportExport/Model/Import/Customer.php +++ b/app/code/Magento/CustomerImportExport/Model/Import/Customer.php @@ -8,6 +8,7 @@ use Magento\Customer\Api\Data\CustomerInterface; use Magento\ImportExport\Model\Import; use Magento\ImportExport\Model\Import\ErrorProcessing\ProcessingErrorAggregatorInterface; +use Magento\ImportExport\Model\Import\AbstractSource; /** * Customer entity import @@ -345,6 +346,40 @@ protected function _getNextEntityId() return $this->_nextEntityId++; } + /** + * Prepare customers data for existing customers checks to perform mass validation/import efficiently. + * + * @param array|AbstractSource $rows + * + * @return void + */ + public function prepareCustomerData($rows): void + { + $customersPresent = []; + foreach ($rows as $rowData) { + $email = $rowData[static::COLUMN_EMAIL] ?? null; + $websiteId = isset($rowData[static::COLUMN_WEBSITE]) + ? $this->getWebsiteId($rowData[static::COLUMN_WEBSITE]) : false; + if ($email && $websiteId !== false) { + $customersPresent[] = [ + 'email' => $email, + 'website_id' => $websiteId, + ]; + } + } + $this->getCustomerStorage()->prepareCustomers($customersPresent); + } + + /** + * @inheritDoc + */ + public function validateData() + { + $this->prepareCustomerData($this->getSource()); + + return parent::validateData(); + } + /** * Prepare customer data for update * @@ -456,6 +491,7 @@ protected function _prepareDataForUpdate(array $rowData) protected function _importData() { while ($bunch = $this->_dataSourceModel->getNextBunch()) { + $this->prepareCustomerData($bunch); $entitiesToCreate = []; $entitiesToUpdate = []; $entitiesToDelete = []; diff --git a/app/code/Magento/CustomerImportExport/Model/Import/CustomerComposite.php b/app/code/Magento/CustomerImportExport/Model/Import/CustomerComposite.php index f52e5d8c881..956c9695623 100644 --- a/app/code/Magento/CustomerImportExport/Model/Import/CustomerComposite.php +++ b/app/code/Magento/CustomerImportExport/Model/Import/CustomerComposite.php @@ -287,6 +287,28 @@ public function getEntityTypeCode() return 'customer_composite'; } + /** + * @inheritDoc + */ + public function validateData() + { + //Preparing both customer and address imports for mass validation. + $source = $this->getSource(); + $this->_customerEntity->prepareCustomerData($source); + $source->rewind(); + $rows = []; + foreach ($source as $row) { + $rows[] = [ + Address::COLUMN_EMAIL => $row[Customer::COLUMN_EMAIL], + Address::COLUMN_WEBSITE => $row[Customer::COLUMN_WEBSITE], + ]; + } + $source->rewind(); + $this->_addressEntity->prepareCustomerData($rows); + + return parent::validateData(); + } + /** * Validate data row * diff --git a/app/code/Magento/CustomerImportExport/Model/ResourceModel/Import/Address/Storage.php b/app/code/Magento/CustomerImportExport/Model/ResourceModel/Import/Address/Storage.php new file mode 100644 index 00000000000..6d29d7ae792 --- /dev/null +++ b/app/code/Magento/CustomerImportExport/Model/ResourceModel/Import/Address/Storage.php @@ -0,0 +1,150 @@ +addressCollectionFactory = $addressCollectionFactory; + $this->collectionIterator = $byPagesIterator; + $this->config = $config; + } + + /** + * Record existing address. + * + * @param string $customerId + * @param string $addressId + * + * @return void + */ + private function addRecord(string $customerId, string $addressId): void + { + if (!$customerId || !$addressId) { + return; + } + if (!array_key_exists($customerId, $this->addresses)) { + $this->addresses[$customerId] = []; + } + + if (!in_array($addressId, $this->addresses[$customerId], true)) { + $this->addresses[$customerId][] = $addressId; + } + } + + /** + * Load addresses IDs for given customers. + * + * @param string[] $customerIds + * + * @return void + */ + private function loadAddresses(array $customerIds): void + { + /** @var AddressCollection $collection */ + $collection = $this->addressCollectionFactory->create(); + $collection->removeAttributeToSelect(); + $select = $collection->getSelect(); + $tableId = array_keys($select->getPart(Select::FROM))[0]; + $select->where($tableId .'.parent_id in (?)', $customerIds); + + $this->collectionIterator->iterate( + $collection, + $this->config->getValue(AbstractEntity::XML_PATH_PAGE_SIZE), + [ + function (DataObject $record) { + $this->addRecord($record->getParentId(), $record->getId()); + } + ] + ); + } + + /** + * Check if given address exists for given customer. + * + * @param string $addressId + * @param string $forCustomerId + * @return bool + */ + public function doesExist(string $addressId, string $forCustomerId): bool + { + return array_key_exists($forCustomerId, $this->addresses) + && in_array( + $addressId, + $this->addresses[$forCustomerId], + true + ); + } + + /** + * Pre-load addresses for given customers. + * + * @param string[] $forCustomersIds + * @return void + */ + public function prepareAddresses(array $forCustomersIds): void + { + if (!$forCustomersIds) { + return; + } + + $forCustomersIds = array_unique($forCustomersIds); + $customerIdsToUse = []; + foreach ($forCustomersIds as $customerId) { + if (!array_key_exists((string)$customerId, $this->addresses)) { + $customerIdsToUse[] = $customerId; + } + } + + $this->loadAddresses($customerIdsToUse); + } +} diff --git a/app/code/Magento/CustomerImportExport/Model/ResourceModel/Import/Customer/Storage.php b/app/code/Magento/CustomerImportExport/Model/ResourceModel/Import/Customer/Storage.php index 65d7a85faf0..f779505a380 100644 --- a/app/code/Magento/CustomerImportExport/Model/ResourceModel/Import/Customer/Storage.php +++ b/app/code/Magento/CustomerImportExport/Model/ResourceModel/Import/Customer/Storage.php @@ -5,22 +5,16 @@ */ namespace Magento\CustomerImportExport\Model\ResourceModel\Import\Customer; +use Magento\CustomerImportExport\Test\Unit\Model\Import\CustomerCompositeTest; +use Magento\Framework\DataObject; +use Magento\Framework\DB\Select; +use Magento\Customer\Model\ResourceModel\Customer\CollectionFactory as CustomerCollectionFactory; +use Magento\Customer\Model\ResourceModel\Customer\Collection as CustomerCollection; +use Magento\ImportExport\Model\ResourceModel\CollectionByPagesIteratorFactory; +use Magento\ImportExport\Model\ResourceModel\CollectionByPagesIterator; + class Storage { - /** - * Flag to not load collection more than one time - * - * @var bool - */ - protected $_isCollectionLoaded = false; - - /** - * Customer collection - * - * @var \Magento\Customer\Model\ResourceModel\Customer\Collection - */ - protected $_customerCollection; - /** * Existing customers information. In form of: * @@ -43,20 +37,25 @@ class Storage protected $_pageSize; /** - * Collection by pages iterator + * Collection by pages iterator. * - * @var \Magento\ImportExport\Model\ResourceModel\CollectionByPagesIterator + * @var CollectionByPagesIterator */ protected $_byPagesIterator; /** - * @param \Magento\Customer\Model\ResourceModel\Customer\CollectionFactory $collectionFactory - * @param \Magento\ImportExport\Model\ResourceModel\CollectionByPagesIteratorFactory $colIteratorFactory + * @var CustomerCollectionFactory + */ + private $customerCollectionFactory; + + /** + * @param CustomerCollectionFactory $collectionFactory + * @param CollectionByPagesIteratorFactory $colIteratorFactory * @param array $data */ public function __construct( - \Magento\Customer\Model\ResourceModel\Customer\CollectionFactory $collectionFactory, - \Magento\ImportExport\Model\ResourceModel\CollectionByPagesIteratorFactory $colIteratorFactory, + CustomerCollectionFactory $collectionFactory, + CollectionByPagesIteratorFactory $colIteratorFactory, array $data = [] ) { $this->_customerCollection = isset( @@ -66,33 +65,57 @@ public function __construct( $this->_byPagesIterator = isset( $data['collection_by_pages_iterator'] ) ? $data['collection_by_pages_iterator'] : $colIteratorFactory->create(); + $this->customerCollectionFactory = $collectionFactory; } /** - * Load needed data from customer collection + * Create new collection to load customer data with proper filters. * - * @return void + * @param array[] $customerIdentifiers With keys "email" and "website_id". + * + * @return CustomerCollection */ - public function load() + private function prepareCollection(array $customerIdentifiers): CustomerCollection { - if ($this->_isCollectionLoaded == false) { - $connection = $this->_customerCollection->getConnection(); - $select = $connection->select(); - $select->from($this->_customerCollection->getMainTable(), ['entity_id', 'website_id', 'email']); - $results = $connection->fetchAll($select); - foreach ($results as $customer) { - $this->addCustomerByArray($customer); - } + /** @var CustomerCollection $collection */ + $collection = $this->customerCollectionFactory->create(); + $collection->removeAttributeToSelect(); + $select = $collection->getSelect(); + $customerTableId = array_keys($select->getPart(Select::FROM))[0]; + $select->where( + $customerTableId .'.email in (?)', + array_map( + function (array $customer) { + return $customer['email']; + }, + $customerIdentifiers + ) + ); - $this->_isCollectionLoaded = true; - } + return $collection; + } + + /** + * Load customers' data that can be found by given identifiers. + * + * @param array $customerIdentifiers With keys "email" and "website_id". + * + * @return void + */ + private function loadCustomersData(array $customerIdentifiers) + { + $this->_byPagesIterator->iterate( + $this->prepareCollection($customerIdentifiers), + $this->_pageSize, + [[$this, 'addCustomer']] + ); } /** * @param array $customer * @return $this */ - public function addCustomerByArray(array $customer) + public function addCustomerByArray(array $customer): Storage { $email = strtolower(trim($customer['email'])); if (!isset($this->_customerIds[$email])) { @@ -107,10 +130,10 @@ public function addCustomerByArray(array $customer) * Add customer to array * * @deprecated @see addCustomerByArray - * @param \Magento\Framework\DataObject|\Magento\Customer\Model\Customer $customer + * @param DataObject $customer * @return $this */ - public function addCustomer(\Magento\Framework\DataObject $customer) + public function addCustomer(DataObject $customer): Storage { $customerData = $customer->toArray(); if (!isset($customerData['entity_id']) && isset($customer['id'])) { @@ -122,16 +145,20 @@ public function addCustomer(\Magento\Framework\DataObject $customer) } /** - * Get customer id + * Find customer ID for unique pair of email and website ID. * * @param string $email * @param int $websiteId * @return bool|int */ - public function getCustomerId($email, $websiteId) + public function getCustomerId(string $email, int $websiteId) { - // lazy loading - $this->load(); + $email = mb_strtolower($email); + //Trying to load the customer. + if (!array_key_exists($email, $this->_customerIds) || !array_key_exists($websiteId, $this->_customerIds[$email]) + ) { + $this->loadCustomersData([['email' => $email, 'website_id' => $websiteId]]); + } if (isset($this->_customerIds[$email][$websiteId])) { return $this->_customerIds[$email][$websiteId]; @@ -139,4 +166,41 @@ public function getCustomerId($email, $websiteId) return false; } + + /** + * Pre-load customers for future checks. + * + * @param array[] $customersToFind With keys: email, website_id. + * @return void + */ + public function prepareCustomers(array $customersToFind): void + { + $identifiers = []; + foreach ($customersToFind as $customerToFind) { + $email = mb_strtolower($customerToFind['email']); + $websiteId = $customerToFind['website_id']; + if (!array_key_exists($email, $this->_customerIds) + || !array_key_exists($websiteId, $this->_customerIds[$email]) + ) { + //Only looking for customers we don't already have ID for. + //We need unique identifiers. + $uniqueKey = $email .'_' .$websiteId; + $identifiers[$uniqueKey] = [ + 'email' => $email, + 'website_id' => $websiteId, + ]; + //Recording that we've searched for a customer. + if (!array_key_exists($email, $this->_customerIds)) { + $this->_customerIds[$email] = []; + } + $this->_customerIds[$email][$websiteId] = null; + } + } + if (!$identifiers) { + return; + } + + //Loading customers data. + $this->loadCustomersData($identifiers); + } } diff --git a/app/code/Magento/CustomerImportExport/Test/Unit/Model/Import/AddressTest.php b/app/code/Magento/CustomerImportExport/Test/Unit/Model/Import/AddressTest.php index db860946485..126a9e17917 100644 --- a/app/code/Magento/CustomerImportExport/Test/Unit/Model/Import/AddressTest.php +++ b/app/code/Magento/CustomerImportExport/Test/Unit/Model/Import/AddressTest.php @@ -254,55 +254,25 @@ protected function _createAttrCollectionMock() */ protected function _createCustomerStorageMock() { - /** @var Select|\PHPUnit_Framework_MockObject_MockObject $selectMock */ - $selectMock = $this->getMockBuilder(Select::class) - ->disableOriginalConstructor() - ->setMethods(['from']) - ->getMock(); - $selectMock->expects($this->any())->method('from')->will($this->returnSelf()); - - /** @var $connectionMock AdapterInterface|\PHPUnit_Framework_MockObject_MockObject */ - $connectionMock = $this->getMockBuilder(\Magento\Framework\DB\Adapter\Pdo\Mysql::class) - ->disableOriginalConstructor() - ->setMethods(['select', 'fetchAll']) - ->getMock(); - $connectionMock->expects($this->any()) - ->method('select') - ->will($this->returnValue($selectMock)); - - /** @var Collection|\PHPUnit_Framework_MockObject_MockObject $customerCollection */ - $customerCollection = $this->getMockBuilder(Collection::class) - ->disableOriginalConstructor() - ->setMethods(['getConnection']) - ->getMock(); - $customerCollection->expects($this->any()) - ->method('getConnection') - ->will($this->returnValue($connectionMock)); - - /** @var CollectionFactory|\PHPUnit_Framework_MockObject_MockObject $collectionFactory */ - $collectionFactory = $this->getMockBuilder(CollectionFactory::class) - ->disableOriginalConstructor() - ->setMethods(['create']) - ->getMock(); - $collectionFactory->expects($this->any()) - ->method('create') - ->willReturn($customerCollection); - - /** @var CollectionByPagesIteratorFactory|\PHPUnit_Framework_MockObject_MockObject $byPagesIteratorFactory */ - $byPagesIteratorFactory = $this->getMockBuilder(CollectionByPagesIteratorFactory::class) - ->disableOriginalConstructor() - ->setMethods(['create']) - ->getMock(); - - /** @var Storage|\PHPUnit_Framework_MockObject_MockObject $customerStorage */ - $customerStorage = $this->getMockBuilder(Storage::class) - ->setMethods(['load']) - ->setConstructorArgs([$collectionFactory, $byPagesIteratorFactory]) - ->getMock(); + /** @var $customerStorage Storage|\PHPUnit_Framework_MockObject_MockObject */ + $customerStorage = $this->createMock(Storage::class); + $customerStorage->expects($this->any()) + ->method('getCustomerId') + ->willReturnCallback( + function ($email, $websiteId) { + foreach ($this->_customers as $customerData) { + if ($customerData['email'] === $email + && $customerData['website_id'] === $websiteId + ) { + return $customerData['entity_id']; + } + } + + return false; + } + ); + $customerStorage->expects($this->any())->method('prepareCustomers'); - foreach ($this->_customers as $customerData) { - $customerStorage->addCustomerByArray($customerData); - } return $customerStorage; } @@ -384,7 +354,6 @@ protected function _getModelMockForTestImportDataWithCustomBehaviour() 'attributes' => [], 'defaults' => [], ]; - // entity adapter mock $modelMock = $this->createPartialMock( \Magento\CustomerImportExport\Model\Import\Address::class, @@ -396,21 +365,25 @@ protected function _getModelMockForTestImportDataWithCustomBehaviour() '_saveCustomerDefaults', '_deleteAddressEntities', '_mergeEntityAttributes', - 'getErrorAggregator' + 'getErrorAggregator', + 'getCustomerStorage', + 'prepareCustomerData', ] ); - + //Adding behaviours $availableBehaviors = new \ReflectionProperty($modelMock, '_availableBehaviors'); $availableBehaviors->setAccessible(true); $availableBehaviors->setValue($modelMock, $this->_availableBehaviors); - // mock to imitate data source model $dataSourceMock = $this->createPartialMock( \Magento\ImportExport\Model\ResourceModel\Import\Data::class, - ['getNextBunch', '__wakeup'] + ['getNextBunch', '__wakeup', 'getIterator'] ); $dataSourceMock->expects($this->at(0))->method('getNextBunch')->will($this->returnValue($customBehaviorRows)); $dataSourceMock->expects($this->at(1))->method('getNextBunch')->will($this->returnValue(null)); + $dataSourceMock->expects($this->any()) + ->method('getIterator') + ->willReturn($this->getMockForAbstractClass(\Iterator::class)); $dataSourceModel = new \ReflectionProperty( \Magento\CustomerImportExport\Model\Import\Address::class, @@ -418,15 +391,12 @@ protected function _getModelMockForTestImportDataWithCustomBehaviour() ); $dataSourceModel->setAccessible(true); $dataSourceModel->setValue($modelMock, $dataSourceMock); - // mock expects for entity adapter $modelMock->expects($this->any())->method('validateRow')->will($this->returnValue(true)); $modelMock->expects($this->any()) ->method('getErrorAggregator') ->will($this->returnValue($this->errorAggregator)); - $modelMock->expects($this->any())->method('_prepareDataForUpdate')->will($this->returnValue($updateResult)); - $modelMock->expects( $this->any() )->method( @@ -434,11 +404,8 @@ protected function _getModelMockForTestImportDataWithCustomBehaviour() )->will( $this->returnCallback([$this, 'validateSaveAddressEntities']) ); - $modelMock->expects($this->any())->method('_saveAddressAttributes')->will($this->returnValue($modelMock)); - $modelMock->expects($this->any())->method('_saveCustomerDefaults')->will($this->returnValue($modelMock)); - $modelMock->expects( $this->any() )->method( @@ -446,8 +413,10 @@ protected function _getModelMockForTestImportDataWithCustomBehaviour() )->will( $this->returnCallback([$this, 'validateDeleteAddressEntities']) ); - $modelMock->expects($this->any())->method('_mergeEntityAttributes')->will($this->returnValue([])); + $modelMock->expects($this->any()) + ->method('getCustomerStorage') + ->willReturn($this->_createCustomerStorageMock()); return $modelMock; } @@ -476,7 +445,6 @@ protected function _getModelMock() $this->createMock(\Magento\Customer\Model\AddressFactory::class), $this->createMock(\Magento\Directory\Model\ResourceModel\Region\CollectionFactory::class), $this->createMock(\Magento\Customer\Model\CustomerFactory::class), - $this->createMock(\Magento\Customer\Model\ResourceModel\Address\CollectionFactory::class), $this->createMock(\Magento\Customer\Model\ResourceModel\Address\Attribute\CollectionFactory::class), new \Magento\Framework\Stdlib\DateTime(), $this->createMock(\Magento\Customer\Model\Address\Validator\Postcode::class), diff --git a/app/code/Magento/CustomerImportExport/Test/Unit/Model/Import/CustomerCompositeTest.php b/app/code/Magento/CustomerImportExport/Test/Unit/Model/Import/CustomerCompositeTest.php index 7fcbd1d0d06..1b900c21395 100644 --- a/app/code/Magento/CustomerImportExport/Test/Unit/Model/Import/CustomerCompositeTest.php +++ b/app/code/Magento/CustomerImportExport/Test/Unit/Model/Import/CustomerCompositeTest.php @@ -100,24 +100,6 @@ class CustomerCompositeTest extends \PHPUnit\Framework\TestCase Address::COLUMN_ADDRESS_ID => null, ]; - /** - * List of mocked methods for customer and address entity adapters - * - * @var array - */ - protected $_entityMockedMethods = [ - 'validateRow', - 'getErrorMessages', - 'getErrorsCount', - 'getErrorsLimit', - 'getInvalidRowsCount', - 'getNotices', - 'getProcessedEntitiesCount', - 'setParameters', - 'setSource', - 'importData', - ]; - protected function setUp() { $translateInline = $this->createMock(\Magento\Framework\Translate\InlineInterface::class); @@ -199,30 +181,30 @@ protected function _getModelMock() */ protected function _getModelMockForPrepareRowForDb() { - $customerEntity = $this->_getCustomerEntityMock(['validateRow']); - $customerEntity->expects($this->any())->method('validateRow')->will($this->returnValue(true)); - - $customerStorage = $this->createPartialMock(\stdClass::class, ['getCustomerId']); - $customerStorage->expects($this->any())->method('getCustomerId')->will($this->returnValue(1)); - - $addressEntity = $this->_getAddressEntityMock(['validateRow', 'getCustomerStorage']); - $addressEntity->expects($this->any())->method('validateRow')->will($this->returnValue(true)); - $addressEntity->expects( - $this->any() - )->method( - 'getCustomerStorage' - )->will( - $this->returnValue($customerStorage) + $customerStorage = $this->createPartialMock( + 'stdClass', + ['getCustomerId', 'prepareCustomers', 'addCustomer'] ); + $customerStorage->expects($this->any())->method('getCustomerId')->willReturn(1); + $customerEntity = $this->_getCustomerEntityMock(); + $customerEntity->expects($this->any())->method('validateRow')->willReturn(true); + $customerEntity->expects($this->any()) + ->method('getCustomerStorage') + ->willReturn($customerStorage); + $customerEntity->expects($this->any()) + ->method('getValidColumnNames') + ->willReturn(['cols']); + + $addressEntity = $this->_getAddressEntityMock(); + $addressEntity->expects($this->any())->method('validateRow')->willReturn(true); + $addressEntity->expects($this->any()) + ->method('getCustomerStorage') + ->willReturn($customerStorage); $dataSourceMock = $this->createPartialMock(\stdClass::class, ['cleanBunches', 'saveBunch']); - $dataSourceMock->expects( - $this->any() - )->method( - 'saveBunch' - )->will( - $this->returnCallback([$this, 'verifyPrepareRowForDbData']) - ); + $dataSourceMock->expects($this->any()) + ->method('saveBunch') + ->willReturnCallback([$this, 'verifyPrepareRowForDbData']); $data = $this->_getModelDependencies(); $data['customer_entity'] = $customerEntity; @@ -243,14 +225,15 @@ protected function _getModelMockForPrepareRowForDb() protected function _getModelMockForImportData($isDeleteBehavior, $customerImport, $addressImport) { $customerEntity = $this->_getCustomerEntityMock(); - $customerEntity->expects($this->once())->method('importData')->will($this->returnValue($customerImport)); + $customerEntity->expects($this->once())->method('importData')->willReturn($customerImport); $addressEntity = $this->_getAddressEntityMock(); // address import starts only if customer import finished successfully if ($isDeleteBehavior || !$customerImport) { $addressEntity->expects($this->never())->method('importData'); } else { - $addressEntity->expects($this->once())->method('importData')->will($this->returnValue($addressImport)); + $addressEntity->expects($this->once())->method('setCustomerAttributes')->willReturnSelf(); + $addressEntity->expects($this->once())->method('importData')->willReturn($addressImport); } $data = $this->_getModelDependencies(); @@ -261,65 +244,39 @@ protected function _getModelMockForImportData($isDeleteBehavior, $customerImport } /** - * @param array $mockedMethods * @return Customer|\PHPUnit_Framework_MockObject_MockObject */ - protected function _getCustomerEntityMock(array $mockedMethods = null) + protected function _getCustomerEntityMock() { - if ($mockedMethods === null) { - $mockedMethods = $this->_entityMockedMethods; - } - $mockedMethods[] = 'getAttributeCollection'; - $mockedMethods[] = 'getWebsiteId'; + $customerEntity = $this->createMock(Customer::class); - $customerEntity = $this->createPartialMock( - \Magento\CustomerImportExport\Model\Import\Customer::class, - $mockedMethods - ); $attributeList = []; foreach ($this->_customerAttributes as $code) { $attribute = new \Magento\Framework\DataObject(['attribute_code' => $code]); $attributeList[] = $attribute; } - $customerEntity->expects( - $this->once() - )->method( - 'getAttributeCollection' - )->will( - $this->returnValue($attributeList) - ); + $customerEntity->expects($this->once()) + ->method('getAttributeCollection') + ->willReturn($attributeList); return $customerEntity; } /** - * @param array $mockedMethods * @return Address|\PHPUnit_Framework_MockObject_MockObject */ - protected function _getAddressEntityMock(array $mockedMethods = null) + private function _getAddressEntityMock() { - if ($mockedMethods === null) { - $mockedMethods = $this->_entityMockedMethods; - } - $mockedMethods[] = 'getAttributeCollection'; - - $addressEntity = $this->createPartialMock( - \Magento\CustomerImportExport\Model\Import\Address::class, - $mockedMethods - ); + $addressEntity = $this->createMock(Address::class); $attributeList = []; foreach ($this->_addressAttributes as $code) { $attribute = new \Magento\Framework\DataObject(['attribute_code' => $code]); $attributeList[] = $attribute; } - $addressEntity->expects( - $this->once() - )->method( - 'getAttributeCollection' - )->will( - $this->returnValue($attributeList) - ); + $addressEntity->expects($this->once()) + ->method('getAttributeCollection') + ->willReturn($attributeList); return $addressEntity; } @@ -373,33 +330,26 @@ public function testIsAttributeParticular() public function testValidateRow(array $rows, array $calls, $validationReturn, array $expectedErrors, $behavior) { $customerEntity = $this->_getCustomerEntityMock(); - $this->_entityMockedMethods[] = 'getCustomerStorage'; $addressEntity = $this->_getAddressEntityMock(); $customerEntity->expects($this->exactly($calls['customerValidationCalls'])) ->method('validateRow') - ->will($this->returnValue($validationReturn)); - - $customerEntity->expects($this->any()) - ->method('getErrorMessages') - ->will($this->returnValue([])); + ->willReturn($validationReturn); $addressEntity ->expects($this->exactly($calls['addressValidationCalls'])) ->method('validateRow') - ->will($this->returnValue($validationReturn)); + ->willReturn($validationReturn); $customerStorage = $this->createPartialMock(\stdClass::class, ['getCustomerId']); - $customerStorage->expects($this->any())->method('getCustomerId')->will($this->returnValue(true)); - $addressEntity->expects( - $this->any() - )->method( - 'getCustomerStorage' - )->will( - $this->returnValue($customerStorage) - ); + $customerStorage->expects($this->any())->method('getCustomerId')->willReturn(true); + $addressEntity->expects($this->any()) + ->method('getCustomerStorage') + ->willReturn($customerStorage); - $addressEntity->expects($this->any())->method('getErrorMessages')->will($this->returnValue([])); + $customerEntity->expects($this->any()) + ->method('getCustomerStorage') + ->willReturn($customerStorage); $data = $this->_getModelDependencies(); $data['customer_entity'] = $customerEntity; @@ -419,24 +369,22 @@ public function testValidateRow(array $rows, array $calls, $validationReturn, ar public function testPrepareAddressRowData() { $customerEntity = $this->_getCustomerEntityMock(); - $this->_entityMockedMethods[] = 'getCustomerStorage'; $addressEntity = $this->_getAddressEntityMock(); - $customerEntity->expects($this->once())->method('validateRow')->will($this->returnValue(true)); + $customerEntity->expects($this->once())->method('validateRow')->willReturn(true); - $addressEntity->expects( - $this->once() - )->method( - 'validateRow' - )->will( - $this->returnCallback([$this, 'validateAddressRowParams']) - ); + $addressEntity->expects($this->once()) + ->method('validateRow') + ->willReturnCallback([$this, 'validateAddressRowParams']); $customerStorage = $this->createPartialMock(\stdClass::class, ['getCustomerId']); - $customerStorage->expects($this->any())->method('getCustomerId')->will($this->returnValue(true)); + $customerStorage->expects($this->any())->method('getCustomerId')->willReturn(true); $addressEntity->expects($this->any()) ->method('getCustomerStorage') - ->will($this->returnValue($customerStorage)); + ->willReturn($customerStorage); + $customerEntity->expects($this->any()) + ->method('getCustomerStorage') + ->willReturn($customerStorage); $data = $this->_getModelDependencies(); $data['customer_entity'] = $customerEntity; @@ -551,20 +499,12 @@ public function testSetParameters() $customerEntity = $this->_getCustomerEntityMock(); $addressEntity = $this->_getAddressEntityMock(); - $customerEntity->expects( - $this->once() - )->method( - 'setParameters' - )->will( - $this->returnCallback([$this, 'callbackCheckParameters']) - ); - $addressEntity->expects( - $this->once() - )->method( - 'setParameters' - )->will( - $this->returnCallback([$this, 'callbackCheckParameters']) - ); + $customerEntity->expects($this->once()) + ->method('setParameters') + ->willReturnCallback([$this, 'callbackCheckParameters']); + $addressEntity->expects($this->once()) + ->method('setParameters') + ->willReturnCallback([$this, 'callbackCheckParameters']); $data = $this->_getModelDependencies(); $data['customer_entity'] = $customerEntity; $data['address_entity'] = $addressEntity; @@ -730,8 +670,8 @@ protected function _getModelForGetterTest($method, $customerReturnData, $address $customerEntity = $this->_getCustomerEntityMock(); $addressEntity = $this->_getAddressEntityMock(); - $customerEntity->expects($this->once())->method($method)->will($this->returnValue($customerReturnData)); - $addressEntity->expects($this->once())->method($method)->will($this->returnValue($addressReturnData)); + $customerEntity->expects($this->once())->method($method)->willReturn($customerReturnData); + $addressEntity->expects($this->once())->method($method)->willReturn($addressReturnData); $data = $this->_getModelDependencies(); $data['customer_entity'] = $customerEntity; diff --git a/app/code/Magento/CustomerImportExport/Test/Unit/Model/Import/CustomerTest.php b/app/code/Magento/CustomerImportExport/Test/Unit/Model/Import/CustomerTest.php index 5a698388da5..9a7183d5b5f 100644 --- a/app/code/Magento/CustomerImportExport/Test/Unit/Model/Import/CustomerTest.php +++ b/app/code/Magento/CustomerImportExport/Test/Unit/Model/Import/CustomerTest.php @@ -10,6 +10,7 @@ namespace Magento\CustomerImportExport\Test\Unit\Model\Import; use Magento\CustomerImportExport\Model\Import\Customer; +use Magento\CustomerImportExport\Model\ResourceModel\Import\Customer\Storage; class CustomerTest extends \PHPUnit\Framework\TestCase { @@ -90,6 +91,7 @@ protected function _getModelMockForTestImportDataWithCustomBehaviour() '_saveCustomerAttributes', '_deleteCustomerEntities', 'getErrorAggregator', + 'getCustomerStorage', ] ) ->getMock(); @@ -153,6 +155,12 @@ protected function _getModelMockForTestImportDataWithCustomBehaviour() $modelMock->expects($this->any()) ->method('getErrorAggregator') ->will($this->returnValue($errorAggregator)); + /** @var \PHPUnit_Framework_MockObject_MockObject $storageMock */ + $storageMock = $this->createMock(Storage::class); + $storageMock->expects($this->any())->method('prepareCustomers'); + $modelMock->expects($this->any()) + ->method('getCustomerStorage') + ->willReturn($storageMock); return $modelMock; } diff --git a/app/code/Magento/CustomerImportExport/Test/Unit/Model/ResourceModel/Import/Customer/StorageTest.php b/app/code/Magento/CustomerImportExport/Test/Unit/Model/ResourceModel/Import/Customer/StorageTest.php index 9a06ebae375..1f2f9f3fed5 100644 --- a/app/code/Magento/CustomerImportExport/Test/Unit/Model/ResourceModel/Import/Customer/StorageTest.php +++ b/app/code/Magento/CustomerImportExport/Test/Unit/Model/ResourceModel/Import/Customer/StorageTest.php @@ -3,119 +3,77 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + +/** + * Test class for \Magento\CustomerImportExport\Model\ResourceModel\Import\Customer\Storage + */ namespace Magento\CustomerImportExport\Test\Unit\Model\ResourceModel\Import\Customer; use Magento\CustomerImportExport\Model\ResourceModel\Import\Customer\Storage; -use Magento\Framework\DB\Adapter\AdapterInterface; use Magento\Customer\Model\ResourceModel\Customer\Collection; use Magento\Customer\Model\ResourceModel\Customer\CollectionFactory; use Magento\ImportExport\Model\ResourceModel\CollectionByPagesIteratorFactory; +use Magento\Framework\DataObject; +use Magento\Framework\DB\Select; +use Magento\ImportExport\Model\ResourceModel\CollectionByPagesIterator; class StorageTest extends \PHPUnit\Framework\TestCase { /** * @var Storage */ - protected $_model; + private $model; /** - * @var string + * @var CollectionByPagesIterator|\PHPUnit_Framework_MockObject_MockObject */ - protected $_entityTable = 'test'; + private $iteratorMock; /** - * @var array + * @var Collection|\PHPUnit_Framework_MockObject_MockObject */ - protected $_expectedFields = ['entity_id', 'website_id', 'email']; + private $collectionMock; protected function setUp() { - /** @var \Magento\Framework\DB\Select|\PHPUnit_Framework_MockObject_MockObject $selectMock */ - $selectMock = $this->getMockBuilder(\Magento\Framework\DB\Select::class) - ->disableOriginalConstructor() - ->setMethods(['from']) - ->getMock(); - $selectMock->expects($this->any())->method('from')->will($this->returnSelf()); - - /** @var $connectionMock AdapterInterface|\PHPUnit_Framework_MockObject_MockObject */ - $connectionMock = $this->getMockBuilder(\Magento\Framework\DB\Adapter\Pdo\Mysql::class) - ->disableOriginalConstructor() - ->setMethods(['select', 'fetchAll']) - ->getMock(); - $connectionMock->expects($this->any()) - ->method('select') - ->will($this->returnValue($selectMock)); - $connectionMock->expects($this->any()) - ->method('fetchAll') - ->will($this->returnValue([])); - - /** @var Collection|\PHPUnit_Framework_MockObject_MockObject $customerCollection */ - $customerCollection = $this->getMockBuilder(Collection::class) - ->disableOriginalConstructor() - ->setMethods(['getConnection','getMainTable']) - ->getMock(); - $customerCollection->expects($this->any()) - ->method('getConnection') - ->will($this->returnValue($connectionMock)); - - $customerCollection->expects($this->any()) - ->method('getMainTable') - ->willReturn('customer_entity'); - - /** @var CollectionFactory|\PHPUnit_Framework_MockObject_MockObject $collectionFactory */ - $collectionFactory = $this->getMockBuilder(CollectionFactory::class) - ->disableOriginalConstructor() - ->setMethods(['create']) - ->getMock(); - $collectionFactory->expects($this->any()) + $this->iteratorMock = $this->createMock( + CollectionByPagesIterator::class + ); + /** @var \PHPUnit_Framework_MockObject_MockObject|CollectionByPagesIteratorFactory $iteratorFactoryMock */ + $iteratorFactoryMock = $this->createMock( + CollectionByPagesIteratorFactory::class + ); + $iteratorFactoryMock->expects($this->any()) ->method('create') - ->willReturn($customerCollection); - - /** @var CollectionByPagesIteratorFactory|\PHPUnit_Framework_MockObject_MockObject $byPagesIteratorFactory */ - $byPagesIteratorFactory = $this->getMockBuilder(CollectionByPagesIteratorFactory::class) - ->disableOriginalConstructor() - ->setMethods(['create']) - ->getMock(); - - $this->_model = new Storage( - $collectionFactory, - $byPagesIteratorFactory + ->willReturn($this->iteratorMock); + $this->collectionMock = $this->createMock(Collection::class); + /** @var CollectionFactory|\PHPUnit_Framework_MockObject_MockObject $collectionFactoryMock */ + $collectionFactoryMock = $this->createMock( + CollectionFactory::class + ); + $collectionFactoryMock->expects($this->any()) + ->method('create') + ->willReturn($this->collectionMock); + /** @var \PHPUnit_Framework_MockObject_MockObject $selectMock */ + $selectMock = $this->createMock(Select::class); + $selectMock->expects($this->any()) + ->method('getPart') + ->with(Select::FROM) + ->willReturn(['e' => []]); + $this->collectionMock->expects($this->any()) + ->method('getSelect') + ->willReturn($selectMock); + + $this->model = new Storage( + $collectionFactoryMock, + $iteratorFactoryMock, + [] ); - $this->_model->load(); } protected function tearDown() { - unset($this->_model); - } - - /** - * @param string $tableName - * @param array $fields - */ - public function validateFrom($tableName, $fields) - { - $this->assertEquals($this->_entityTable, $tableName); - $this->assertEquals($this->_expectedFields, $fields); - } - - public function testLoad() - { - $this->assertAttributeEquals(true, '_isCollectionLoaded', $this->_model); - } - - public function testAddCustomer() - { - $customer = new \Magento\Framework\DataObject(['id' => 1, 'website_id' => 1, 'email' => 'test@test.com']); - $this->_model->addCustomer($customer); - - $propertyName = '_customerIds'; - $this->assertAttributeCount(1, $propertyName, $this->_model); - $this->assertAttributeContains([$customer->getWebsiteId() => $customer->getId()], $propertyName, $this->_model); - $this->assertEquals( - $customer->getId(), - $this->_model->getCustomerId($customer->getEmail(), $customer->getWebsiteId()) - ); + unset($this->model); } public function testAddCustomerByArray() @@ -123,20 +81,49 @@ public function testAddCustomerByArray() $propertyName = '_customerIds'; $customer = $this->_addCustomerToStorage(); - $this->assertAttributeCount(1, $propertyName, $this->_model); + $this->assertAttributeCount(1, $propertyName, $this->model); $expectedCustomerData = [$customer['website_id'] => $customer['entity_id']]; - $this->assertAttributeContains($expectedCustomerData, $propertyName, $this->_model); + $this->assertAttributeContains($expectedCustomerData, $propertyName, $this->model); } public function testGetCustomerId() { - $customer = $this->_addCustomerToStorage(); - + $existingEmail = 'test@magento.com'; + $existingWebsiteId = 0; + $existingId = 1; + $nonExistingEmail = 'test1@magento.com'; + $nonExistingWebsiteId = 2; + + $this->iteratorMock->expects($this->at(0)) + ->method('iterate') + ->willReturnCallback( + function (...$args) use ( + $existingId, + $existingEmail, + $existingWebsiteId + ) { + /** @var callable $callable */ + foreach ($args[2] as $callable) { + $callable( + new DataObject([ + 'id' => $existingId, + 'email' => $existingEmail, + 'website_id' => $existingWebsiteId, + ]) + ); + } + } + ); $this->assertEquals( - $customer['entity_id'], - $this->_model->getCustomerId($customer['email'], $customer['website_id']) + $existingId, + $this->model->getCustomerId($existingEmail, $existingWebsiteId) + ); + $this->assertFalse( + $this->model->getCustomerId( + $nonExistingEmail, + $nonExistingWebsiteId + ) ); - $this->assertFalse($this->_model->getCustomerId('new@test.com', $customer['website_id'])); } /** @@ -145,7 +132,7 @@ public function testGetCustomerId() protected function _addCustomerToStorage() { $customer = ['entity_id' => 1, 'website_id' => 1, 'email' => 'test@test.com']; - $this->_model->addCustomerByArray($customer); + $this->model->addCustomerByArray($customer); return $customer; } diff --git a/app/code/Magento/Elasticsearch/Model/Config.php b/app/code/Magento/Elasticsearch/Model/Config.php index 93e715f9280..a0f3b6433b4 100644 --- a/app/code/Magento/Elasticsearch/Model/Config.php +++ b/app/code/Magento/Elasticsearch/Model/Config.php @@ -6,6 +6,8 @@ namespace Magento\Elasticsearch\Model; use Magento\Framework\App\Config\ScopeConfigInterface; +use Magento\Framework\Search\EngineResolverInterface; +use Magento\Search\Model\EngineResolver; use Magento\Store\Model\ScopeInterface; use Magento\AdvancedSearch\Model\Client\ClientOptionsInterface; use Magento\AdvancedSearch\Model\Client\ClientResolver; @@ -54,21 +56,28 @@ class Config implements ClientOptionsInterface */ private $clientResolver; + /** + * @var EngineResolverInterface + */ + private $engineResolver; + /** * Constructor * * @param ScopeConfigInterface $scopeConfig - * @param ClientResolver $clientResolver - * @param string $prefix + * @param ClientResolver|null $clientResolver + * @param EngineResolverInterface|null $engineResolver + * @param string|null $prefix */ public function __construct( ScopeConfigInterface $scopeConfig, ClientResolver $clientResolver = null, + EngineResolverInterface $engineResolver = null, $prefix = null ) { $this->scopeConfig = $scopeConfig; - $this->clientResolver = $clientResolver ?: - ObjectManager::getInstance()->get(ClientResolver::class); + $this->clientResolver = $clientResolver ?: ObjectManager::getInstance()->get(ClientResolver::class); + $this->engineResolver = $engineResolver ?: ObjectManager::getInstance()->get(EngineResolverInterface::class); $this->prefix = $prefix ?: $this->clientResolver->getCurrentEngine(); } @@ -126,7 +135,7 @@ public function getSearchConfigData($field, $storeId = null) */ public function isElasticsearchEnabled() { - return $this->getSearchConfigData('engine') == self::ENGINE_NAME; + return $this->engineResolver->getCurrentSearchEngine() === self::ENGINE_NAME; } /** diff --git a/app/code/Magento/Elasticsearch/Model/Indexer/Plugin/DependencyUpdaterPlugin.php b/app/code/Magento/Elasticsearch/Model/Indexer/Plugin/DependencyUpdaterPlugin.php new file mode 100644 index 00000000000..f1b153c98b2 --- /dev/null +++ b/app/code/Magento/Elasticsearch/Model/Indexer/Plugin/DependencyUpdaterPlugin.php @@ -0,0 +1,85 @@ +config = $config; + } + + /** + * Remove index dependency, if it needed, on run reindexing by specifics indexes. + * + * @param Provider $provider + * @param array $dependencies + * @param string $indexerId + * @return array + * @see \Magento\Indexer\Console\Command\IndexerReindexCommand::getIndexers() + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function afterGetIndexerIdsToRunBefore(Provider $provider, array $dependencies, string $indexerId): array + { + if ($this->isFilteringNeeded($indexerId, CatalogSearchFulltextIndexer::INDEXER_ID)) { + $dependencies = array_diff($dependencies, [CatalogInventoryStockIndexer::INDEXER_ID]); + } + + return $dependencies; + } + + /** + * Remove index dependency, if it needed, on reindex triggers. + * + * @param Provider $provider + * @param array $dependencies + * @param string $indexerId + * @return array + * @see \Magento\Indexer\Model\Indexer\DependencyDecorator + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function afterGetIndexerIdsToRunAfter(Provider $provider, array $dependencies, string $indexerId): array + { + if ($this->isFilteringNeeded($indexerId, CatalogInventoryStockIndexer::INDEXER_ID)) { + $dependencies = array_diff($dependencies, [CatalogSearchFulltextIndexer::INDEXER_ID]); + } + + return $dependencies; + } + + /** + * @param string $currentIndexerId + * @param string $targetIndexerId + * @return bool + */ + private function isFilteringNeeded(string $currentIndexerId, string $targetIndexerId): bool + { + return (!$this->config->isElasticsearchEnabled() && $targetIndexerId === $currentIndexerId); + } +} diff --git a/app/code/Magento/Elasticsearch/Model/Indexer/Plugin/StockedProductsFilterPlugin.php b/app/code/Magento/Elasticsearch/Model/Indexer/Plugin/StockedProductsFilterPlugin.php new file mode 100644 index 00000000000..ec18b955a29 --- /dev/null +++ b/app/code/Magento/Elasticsearch/Model/Indexer/Plugin/StockedProductsFilterPlugin.php @@ -0,0 +1,94 @@ +config = $config; + $this->stockConfiguration = $stockConfiguration; + $this->stockStatusRepository = $stockStatusRepository; + $this->stockStatusCriteriaFactory = $stockStatusCriteriaFactory; + } + + /** + * Filter out of stock options for configurable product. + * + * @param DataProvider $dataProvider + * @param array $indexData + * @param array $productData + * @param int $storeId + * @return array + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function beforePrepareProductIndex( + DataProvider $dataProvider, + array $indexData, + array $productData, + int $storeId + ): array { + if ($this->config->isElasticsearchEnabled() && !$this->stockConfiguration->isShowOutOfStock($storeId)) { + $productIds = array_keys($indexData); + $stockStatusCriteria = $this->stockStatusCriteriaFactory->create(); + $stockStatusCriteria->setProductsFilter($productIds); + $stockStatusCollection = $this->stockStatusRepository->getList($stockStatusCriteria); + $stockStatuses = $stockStatusCollection->getItems(); + $stockStatuses = array_filter($stockStatuses, function (StockStatusInterface $stockStatus) { + return StockStatusInterface::STATUS_IN_STOCK == $stockStatus->getStockStatus(); + }); + $indexData = array_intersect_key($indexData, $stockStatuses); + } + + return [ + $indexData, + $productData, + $storeId, + ]; + } +} diff --git a/app/code/Magento/Elasticsearch/SearchAdapter/Query/Builder/Aggregation.php b/app/code/Magento/Elasticsearch/SearchAdapter/Query/Builder/Aggregation.php index cad32eb727e..7ba2ef47c77 100644 --- a/app/code/Magento/Elasticsearch/SearchAdapter/Query/Builder/Aggregation.php +++ b/app/code/Magento/Elasticsearch/SearchAdapter/Query/Builder/Aggregation.php @@ -15,6 +15,14 @@ */ class Aggregation { + /** + * Max number of results returned per single term bucket, i.e. limit of options for layered navigation filter. + * Default ElasticSearch limit is 10 + * + * @var int + */ + private static $maxTermBacketSize = 500; + /** * @var FieldMapperInterface * @since 100.1.0 @@ -67,6 +75,7 @@ protected function buildBucket( $searchQuery['body']['aggregations'][$bucket->getName()]= [ 'terms' => [ 'field' => $field, + 'size' => self::$maxTermBacketSize, ], ]; break; diff --git a/app/code/Magento/Elasticsearch/Test/Unit/Model/Indexer/Plugin/DependencyUpdaterPluginTest.php b/app/code/Magento/Elasticsearch/Test/Unit/Model/Indexer/Plugin/DependencyUpdaterPluginTest.php new file mode 100644 index 00000000000..15510843b0e --- /dev/null +++ b/app/code/Magento/Elasticsearch/Test/Unit/Model/Indexer/Plugin/DependencyUpdaterPluginTest.php @@ -0,0 +1,87 @@ +configMock = $this->getMockBuilder(Config::class) + ->disableOriginalConstructor() + ->getMock(); + $this->configMock->expects($this->exactly(2)) + ->method('isElasticsearchEnabled') + ->willReturnOnConsecutiveCalls(true, false); + $this->providerMock = $this->getMockBuilder(DependencyInfoProvider::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->plugin = new DependencyUpdaterPlugin($this->configMock); + } + + /** + * @return void + */ + public function testAfterGetIndexerIdsToRunBefore(): void + { + $dependencies = [ + CatalogInventoryStockIndexer::INDEXER_ID, + ]; + $indexerId = CatalogSearchFulltextIndexer::INDEXER_ID; + + $indexerIds = $this->plugin->afterGetIndexerIdsToRunBefore($this->providerMock, $dependencies, $indexerId); + $this->assertContains(CatalogInventoryStockIndexer::INDEXER_ID, $indexerIds); + + $indexerIds = $this->plugin->afterGetIndexerIdsToRunBefore($this->providerMock, $dependencies, $indexerId); + $this->assertNotContains(CatalogInventoryStockIndexer::INDEXER_ID, $indexerIds); + } + + /** + * @return void + */ + public function testAfterGetIndexerIdsToRunAfter(): void + { + $dependencies = [ + CatalogSearchFulltextIndexer::INDEXER_ID, + ]; + $indexerId = CatalogInventoryStockIndexer::INDEXER_ID; + + $indexerIds = $this->plugin->afterGetIndexerIdsToRunAfter($this->providerMock, $dependencies, $indexerId); + $this->assertContains(CatalogSearchFulltextIndexer::INDEXER_ID, $indexerIds); + + $indexerIds = $this->plugin->afterGetIndexerIdsToRunAfter($this->providerMock, $dependencies, $indexerId); + $this->assertNotContains(CatalogSearchFulltextIndexer::INDEXER_ID, $indexerIds); + } +} diff --git a/app/code/Magento/Elasticsearch/Test/Unit/Model/Indexer/Plugin/StockedProductsFilterPluginTest.php b/app/code/Magento/Elasticsearch/Test/Unit/Model/Indexer/Plugin/StockedProductsFilterPluginTest.php new file mode 100644 index 00000000000..f66d2532b32 --- /dev/null +++ b/app/code/Magento/Elasticsearch/Test/Unit/Model/Indexer/Plugin/StockedProductsFilterPluginTest.php @@ -0,0 +1,134 @@ +configMock = $this->getMockBuilder(Config::class)->disableOriginalConstructor()->getMock(); + $this->stockConfigurationMock = $this->getMockBuilder(StockConfigurationInterface::class) + ->disableOriginalConstructor() + ->getMock(); + $this->stockStatusRepositoryMock = $this->getMockBuilder(StockStatusRepositoryInterface::class) + ->disableOriginalConstructor() + ->getMock(); + $this->stockStatusCriteriaFactoryMock = $this->getMockBuilder(StockStatusCriteriaInterfaceFactory::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->plugin = new StockedProductsFilterPlugin( + $this->configMock, + $this->stockConfigurationMock, + $this->stockStatusRepositoryMock, + $this->stockStatusCriteriaFactoryMock + ); + } + + /** + * @return void + */ + public function testBeforePrepareProductIndex(): void + { + /** @var DataProvider|\PHPUnit_Framework_MockObject_MockObject $dataProviderMock */ + $dataProviderMock = $this->getMockBuilder(DataProvider::class)->disableOriginalConstructor()->getMock(); + $indexData = [ + 1 => [], + 2 => [], + ]; + $productData = []; + $storeId = 1; + + $this->configMock + ->expects($this->once()) + ->method('isElasticsearchEnabled') + ->willReturn(true); + $this->stockConfigurationMock + ->expects($this->once()) + ->method('isShowOutOfStock') + ->willReturn(false); + + $stockStatusCriteriaMock = $this->getMockBuilder(StockStatusCriteriaInterface::class)->getMock(); + $stockStatusCriteriaMock + ->expects($this->once()) + ->method('setProductsFilter') + ->willReturn(true); + $this->stockStatusCriteriaFactoryMock + ->expects($this->once()) + ->method('create') + ->willReturn($stockStatusCriteriaMock); + + $stockStatusMock = $this->getMockBuilder(StockStatusInterface::class)->getMock(); + $stockStatusMock->expects($this->atLeastOnce()) + ->method('getStockStatus') + ->willReturnOnConsecutiveCalls(Stock::STOCK_IN_STOCK, Stock::STOCK_OUT_OF_STOCK); + $stockStatusCollectionMock = $this->getMockBuilder(StockStatusCollectionInterface::class)->getMock(); + $stockStatusCollectionMock + ->expects($this->once()) + ->method('getItems') + ->willReturn([ + 1 => $stockStatusMock, + 2 => $stockStatusMock, + ]); + $this->stockStatusRepositoryMock + ->expects($this->once()) + ->method('getList') + ->willReturn($stockStatusCollectionMock); + + list ($indexData, $productData, $storeId) = $this->plugin->beforePrepareProductIndex( + $dataProviderMock, + $indexData, + $productData, + $storeId + ); + + $this->assertEquals([1], array_keys($indexData)); + } +} diff --git a/app/code/Magento/Elasticsearch/Test/Unit/SearchAdapter/Query/Builder/AggregationTest.php b/app/code/Magento/Elasticsearch/Test/Unit/SearchAdapter/Query/Builder/AggregationTest.php index 4d2eb35b50a..03724caf74d 100644 --- a/app/code/Magento/Elasticsearch/Test/Unit/SearchAdapter/Query/Builder/AggregationTest.php +++ b/app/code/Magento/Elasticsearch/Test/Unit/SearchAdapter/Query/Builder/AggregationTest.php @@ -6,6 +6,7 @@ namespace Magento\Elasticsearch\Test\Unit\SearchAdapter\Query\Builder; use Magento\Elasticsearch\SearchAdapter\Query\Builder\Aggregation; +use Magento\Framework\Search\Request\BucketInterface; use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; class AggregationTest extends \PHPUnit\Framework\TestCase @@ -26,7 +27,7 @@ class AggregationTest extends \PHPUnit\Framework\TestCase protected $requestInterface; /** - * @var \Magento\Framework\Search\Request\BucketInterface|\PHPUnit_Framework_MockObject_MockObject + * @var BucketInterface|\PHPUnit_Framework_MockObject_MockObject */ protected $requestBucketInterface; @@ -47,7 +48,7 @@ protected function setUp() ->disableOriginalConstructor() ->getMock(); - $this->requestBucketInterface = $this->getMockBuilder(\Magento\Framework\Search\Request\BucketInterface::class) + $this->requestBucketInterface = $this->getMockBuilder(BucketInterface::class) ->disableOriginalConstructor() ->getMock(); @@ -139,28 +140,35 @@ public function testBuildTerm() 'type' => 'product', 'body' => [], ]; + $bucketName = 'price_bucket'; - $this->requestInterface->expects($this->any()) + $this->requestInterface ->method('getAggregation') ->willReturn([$this->requestBucketInterface]); - $this->fieldMapper->expects($this->any()) + $this->fieldMapper ->method('getFieldName') ->willReturn('price'); - $this->requestBucketInterface->expects($this->any()) + $this->requestBucketInterface ->method('getField') ->willReturn('price'); - $this->requestBucketInterface->expects($this->any()) + $this->requestBucketInterface ->method('getType') - ->willReturn('termBucket'); + ->willReturn(BucketInterface::TYPE_TERM); - $this->requestBucketInterface->expects($this->any()) + $this->requestBucketInterface ->method('getName') - ->willReturn('price_bucket'); + ->willReturn($bucketName); $result = $this->model->build($this->requestInterface, $query); + $this->assertNotNull($result); + $this->assertTrue( + isset($result['body']['aggregations'][$bucketName]['terms']['size']), + 'The size have to be specified since by default, ' . + 'the terms aggregation will return only the buckets for the top ten terms ordered by the doc_count' + ); } } diff --git a/app/code/Magento/Elasticsearch/composer.json b/app/code/Magento/Elasticsearch/composer.json index 8e379579641..a6b4f93ade5 100644 --- a/app/code/Magento/Elasticsearch/composer.json +++ b/app/code/Magento/Elasticsearch/composer.json @@ -10,6 +10,7 @@ "magento/module-eav": "*", "magento/module-search": "*", "magento/module-store": "*", + "magento/module-catalog-inventory": "*", "magento/framework": "*", "elasticsearch/elasticsearch": "~2.0|~5.1" }, diff --git a/app/code/Magento/Elasticsearch/etc/di.xml b/app/code/Magento/Elasticsearch/etc/di.xml index b3ba240ae23..2d569eecfee 100644 --- a/app/code/Magento/Elasticsearch/etc/di.xml +++ b/app/code/Magento/Elasticsearch/etc/di.xml @@ -189,6 +189,12 @@ + + + + + + diff --git a/app/code/Magento/Elasticsearch/etc/indexer.xml b/app/code/Magento/Elasticsearch/etc/indexer.xml new file mode 100644 index 00000000000..245829396a6 --- /dev/null +++ b/app/code/Magento/Elasticsearch/etc/indexer.xml @@ -0,0 +1,14 @@ + + + + + + + + + diff --git a/app/code/Magento/GoogleAnalytics/view/frontend/web/js/google-analytics.js b/app/code/Magento/GoogleAnalytics/view/frontend/web/js/google-analytics.js index eb708ab8b63..a8b8303d47c 100644 --- a/app/code/Magento/GoogleAnalytics/view/frontend/web/js/google-analytics.js +++ b/app/code/Magento/GoogleAnalytics/view/frontend/web/js/google-analytics.js @@ -53,7 +53,7 @@ define([ } // Process orders data - if (config.ordersTrackingData.length) { + if (config.ordersTrackingData.hasOwnProperty('currency')) { ga('require', 'ec', 'ec.js'); ga('set', 'currencyCode', config.ordersTrackingData.currency); diff --git a/app/code/Magento/Paypal/Model/Ipn.php b/app/code/Magento/Paypal/Model/Ipn.php index a370bbc77ff..9107762c54b 100644 --- a/app/code/Magento/Paypal/Model/Ipn.php +++ b/app/code/Magento/Paypal/Model/Ipn.php @@ -7,9 +7,9 @@ namespace Magento\Paypal\Model; use Exception; +use Magento\Framework\Exception\LocalizedException; use Magento\Sales\Model\Order\Email\Sender\CreditmemoSender; use Magento\Sales\Model\Order\Email\Sender\OrderSender; -use Magento\Paypal\Model\Info; /** * PayPal Instant Payment Notification processor model @@ -164,11 +164,11 @@ protected function _processOrder() case Info::TXN_TYPE_NEW_CASE: $this->_registerDispute(); break; - // handle new adjustment is created + // handle new adjustment is created case Info::TXN_TYPE_ADJUSTMENT: $this->_registerAdjustment(); break; - //handle new transaction created + //handle new transaction created default: $this->_registerTransaction(); break; @@ -239,16 +239,16 @@ protected function _registerTransaction() case Info::PAYMENTSTATUS_COMPLETED: $this->_registerPaymentCapture(true); break; - // the holded payment was denied on paypal side + // the holded payment was denied on paypal side case Info::PAYMENTSTATUS_DENIED: $this->_registerPaymentDenial(); break; - // customer attempted to pay via bank account, but failed + // customer attempted to pay via bank account, but failed case Info::PAYMENTSTATUS_FAILED: // cancel order $this->_registerPaymentFailure(); break; - // payment was obtained, but money were not captured yet + // payment was obtained, but money were not captured yet case Info::PAYMENTSTATUS_PENDING: $this->_registerPaymentPending(); break; @@ -263,7 +263,7 @@ protected function _registerTransaction() case Info::PAYMENTSTATUS_REFUNDED: $this->_registerPaymentRefund(); break; - // authorization expire/void + // authorization expire/void case Info::PAYMENTSTATUS_EXPIRED: // break is intentionally omitted case Info::PAYMENTSTATUS_VOIDED: @@ -288,24 +288,12 @@ protected function _registerPaymentCapture($skipFraudDetection = false) $parentTransactionId = $this->getRequestData('parent_txn_id'); $this->_importPaymentInformation(); $payment = $this->_order->getPayment(); - $payment->setTransactionId( - $this->getRequestData('txn_id') - ); - $payment->setCurrencyCode( - $this->getRequestData('mc_currency') - ); - $payment->setPreparedMessage( - $this->_createIpnComment('') - ); - $payment->setParentTransactionId( - $parentTransactionId - ); - $payment->setShouldCloseParentTransaction( - 'Completed' === $this->getRequestData('auth_status') - ); - $payment->setIsTransactionClosed( - 0 - ); + $payment->setTransactionId($this->getRequestData('txn_id')); + $payment->setCurrencyCode($this->getRequestData('mc_currency')); + $payment->setPreparedMessage($this->_createIpnComment('')); + $payment->setParentTransactionId($parentTransactionId); + $payment->setShouldCloseParentTransaction('Completed' === $this->getRequestData('auth_status')); + $payment->setIsTransactionClosed(0); $payment->registerCaptureNotification( $this->getRequestData('mc_gross'), $skipFraudDetection && $parentTransactionId @@ -318,9 +306,9 @@ protected function _registerPaymentCapture($skipFraudDetection = false) $this->orderSender->send($this->_order); $this->_order->addStatusHistoryComment( __('You notified customer about invoice #%1.', $invoice->getIncrementId()) - )->setIsCustomerNotified( - true - )->save(); + ) + ->setIsCustomerNotified(true) + ->save(); } } @@ -334,15 +322,13 @@ protected function _registerPaymentDenial() { try { $this->_importPaymentInformation(); - $this->_order->getPayment()->setTransactionId( - $this->getRequestData('txn_id') - )->setNotificationResult( - true - )->setIsTransactionClosed( - true - )->deny(false); + $this->_order->getPayment() + ->setTransactionId($this->getRequestData('txn_id')) + ->setNotificationResult(true) + ->setIsTransactionClosed(true) + ->deny(false); $this->_order->save(); - } catch (\Magento\Framework\Exception\LocalizedException $e) { + } catch (LocalizedException $e) { if ($e->getMessage() != __('We cannot cancel this order.')) { throw $e; } @@ -386,13 +372,11 @@ public function _registerPaymentPending() $this->_importPaymentInformation(); - $this->_order->getPayment()->setPreparedMessage( - $this->_createIpnComment($this->_paypalInfo->explainPendingReason($reason)) - )->setTransactionId( - $this->getRequestData('txn_id') - )->setIsTransactionClosed( - 0 - )->update(false); + $this->_order->getPayment() + ->setPreparedMessage($this->_createIpnComment($this->_paypalInfo->explainPendingReason($reason))) + ->setTransactionId($this->getRequestData('txn_id')) + ->setIsTransactionClosed(0) + ->update(false); $this->_order->save(); } @@ -409,19 +393,12 @@ protected function _registerPaymentAuthorization() $payment->update(true); } else { $this->_importPaymentInformation(); - $payment->setPreparedMessage( - $this->_createIpnComment('') - )->setTransactionId( - $this->getRequestData('txn_id') - )->setParentTransactionId( - $this->getRequestData('parent_txn_id') - )->setCurrencyCode( - $this->getRequestData('mc_currency') - )->setIsTransactionClosed( - 0 - )->registerAuthorizationNotification( - $this->getRequestData('mc_gross') - ); + $payment->setPreparedMessage($this->_createIpnComment('')) + ->setTransactionId($this->getRequestData('txn_id')) + ->setParentTransactionId($this->getRequestData('parent_txn_id')) + ->setCurrencyCode($this->getRequestData('mc_currency')) + ->setIsTransactionClosed(0) + ->registerAuthorizationNotification($this->getRequestData('mc_gross')); } if (!$this->_order->getEmailSent()) { $this->orderSender->send($this->_order); @@ -449,12 +426,13 @@ protected function _registerPaymentReversal() { $reasonCode = $this->getRequestData('reason_code'); $reasonComment = $this->_paypalInfo->explainReasonCode($reasonCode); - $notificationAmount = $this->_order->getBaseCurrency()->formatTxt( - $this->getRequestData('mc_gross') + $this->getRequestData('mc_fee') - ); + $notificationAmount = $this->_order->getBaseCurrency() + ->formatTxt( + $this->getRequestData('mc_gross') + $this->getRequestData('mc_fee') + ); $paymentStatus = $this->_filterPaymentStatus($this->getRequestData('payment_status')); $orderStatus = $paymentStatus == - Info::PAYMENTSTATUS_REVERSED ? Info::ORDER_STATUS_REVERSED : Info::ORDER_STATUS_CANCELED_REVERSAL; + Info::PAYMENTSTATUS_REVERSED ? Info::ORDER_STATUS_REVERSED : Info::ORDER_STATUS_CANCELED_REVERSAL; //Change order status to PayPal Reversed/PayPal Cancelled Reversal if it is possible. $message = __( 'IPN "%1". %2 Transaction amount %3. Transaction ID: "%4"', @@ -464,8 +442,9 @@ protected function _registerPaymentReversal() $this->getRequestData('txn_id') ); $this->_order->setStatus($orderStatus); - $this->_order->save(); - $this->_order->addStatusHistoryComment($message, $orderStatus)->setIsCustomerNotified(false)->save(); + $this->_order->addStatusHistoryComment($message, $orderStatus) + ->setIsCustomerNotified(false) + ->save(); } /** @@ -478,17 +457,12 @@ protected function _registerPaymentRefund() $this->_importPaymentInformation(); $reason = $this->getRequestData('reason_code'); $isRefundFinal = !$this->_paypalInfo->isReversalDisputable($reason); - $payment = $this->_order->getPayment()->setPreparedMessage( - $this->_createIpnComment($this->_paypalInfo->explainReasonCode($reason)) - )->setTransactionId( - $this->getRequestData('txn_id') - )->setParentTransactionId( - $this->getRequestData('parent_txn_id') - )->setIsTransactionClosed( - $isRefundFinal - )->registerRefundNotification( - -1 * $this->getRequestData('mc_gross') - ); + $payment = $this->_order->getPayment() + ->setPreparedMessage($this->_createIpnComment($this->_paypalInfo->explainReasonCode($reason))) + ->setTransactionId($this->getRequestData('txn_id')) + ->setParentTransactionId($this->getRequestData('parent_txn_id')) + ->setIsTransactionClosed($isRefundFinal) + ->registerRefundNotification(-1 * $this->getRequestData('mc_gross')); $this->_order->save(); // TODO: there is no way to close a capture right now @@ -498,9 +472,9 @@ protected function _registerPaymentRefund() $this->creditmemoSender->send($creditMemo); $this->_order->addStatusHistoryComment( __('You notified customer about creditmemo #%1.', $creditMemo->getIncrementId()) - )->setIsCustomerNotified( - true - )->save(); + ) + ->setIsCustomerNotified(true) + ->save(); } } @@ -513,19 +487,14 @@ protected function _registerPaymentVoid() { $this->_importPaymentInformation(); - $parentTxnId = $this->getRequestData( - 'transaction_entity' - ) == 'auth' ? $this->getRequestData( - 'txn_id' - ) : $this->getRequestData( - 'parent_txn_id' - ); + $parentTxnId = $this->getRequestData('transaction_entity') == 'auth' + ? $this->getRequestData('txn_id') + : $this->getRequestData('parent_txn_id'); - $this->_order->getPayment()->setPreparedMessage( - $this->_createIpnComment('') - )->setParentTransactionId( - $parentTxnId - )->registerVoidNotification(); + $this->_order->getPayment() + ->setPreparedMessage($this->_createIpnComment('')) + ->setParentTransactionId($parentTxnId) + ->registerVoidNotification(); $this->_order->save(); } @@ -546,14 +515,14 @@ protected function _importPaymentInformation() // collect basic information $from = []; foreach ([ - Info::PAYER_ID, - 'payer_email' => Info::PAYER_EMAIL, - Info::PAYER_STATUS, - Info::ADDRESS_STATUS, - Info::PROTECTION_EL, - Info::PAYMENT_STATUS, - Info::PENDING_REASON, - ] as $privateKey => $publicKey) { + Info::PAYER_ID, + 'payer_email' => Info::PAYER_EMAIL, + Info::PAYER_STATUS, + Info::ADDRESS_STATUS, + Info::PROTECTION_EL, + Info::PAYMENT_STATUS, + Info::PENDING_REASON, + ] as $privateKey => $publicKey) { if (is_int($privateKey)) { $privateKey = $publicKey; } diff --git a/app/code/Magento/Persistent/Observer/CheckExpirePersistentQuoteObserver.php b/app/code/Magento/Persistent/Observer/CheckExpirePersistentQuoteObserver.php index 2aaf0f30fe7..f3720960ca6 100644 --- a/app/code/Magento/Persistent/Observer/CheckExpirePersistentQuoteObserver.php +++ b/app/code/Magento/Persistent/Observer/CheckExpirePersistentQuoteObserver.php @@ -50,6 +50,20 @@ class CheckExpirePersistentQuoteObserver implements ObserverInterface */ protected $_persistentData = null; + /** + * Request + * + * @var \Magento\Framework\App\RequestInterface + */ + private $request; + + /** + * Checkout Page path + * + * @var string + */ + private $checkoutPagePath = 'checkout'; + /** * @param \Magento\Persistent\Helper\Session $persistentSession * @param \Magento\Persistent\Helper\Data $persistentData @@ -57,6 +71,7 @@ class CheckExpirePersistentQuoteObserver implements ObserverInterface * @param \Magento\Framework\Event\ManagerInterface $eventManager * @param \Magento\Customer\Model\Session $customerSession * @param \Magento\Checkout\Model\Session $checkoutSession + * @param \Magento\Framework\App\RequestInterface $request */ public function __construct( \Magento\Persistent\Helper\Session $persistentSession, @@ -64,7 +79,8 @@ public function __construct( \Magento\Persistent\Model\QuoteManager $quoteManager, \Magento\Framework\Event\ManagerInterface $eventManager, \Magento\Customer\Model\Session $customerSession, - \Magento\Checkout\Model\Session $checkoutSession + \Magento\Checkout\Model\Session $checkoutSession, + \Magento\Framework\App\RequestInterface $request ) { $this->_persistentSession = $persistentSession; $this->quoteManager = $quoteManager; @@ -72,6 +88,7 @@ public function __construct( $this->_checkoutSession = $checkoutSession; $this->_eventManager = $eventManager; $this->_persistentData = $persistentData; + $this->request = $request; } /** @@ -90,12 +107,32 @@ public function execute(\Magento\Framework\Event\Observer $observer) !$this->_persistentSession->isPersistent() && !$this->_customerSession->isLoggedIn() && $this->_checkoutSession->getQuoteId() && - !$observer->getControllerAction() instanceof \Magento\Checkout\Controller\Onepage - // persistent session does not expire on onepage checkout page to not spoil customer group id + !$this->isRequestFromCheckoutPage($this->request) + // persistent session does not expire on onepage checkout page ) { $this->_eventManager->dispatch('persistent_session_expired'); $this->quoteManager->expire(); $this->_customerSession->setCustomerId(null)->setCustomerGroupId(null); } } + + /** + * Check current request is coming from onepage checkout page. + * + * @param \Magento\Framework\App\RequestInterface $request + * @return bool + */ + private function isRequestFromCheckoutPage(\Magento\Framework\App\RequestInterface $request): bool + { + $requestUri = (string)$request->getRequestUri(); + $refererUri = (string)$request->getServer('HTTP_REFERER'); + + /** @var bool $isCheckoutPage */ + $isCheckoutPage = ( + false !== strpos($requestUri, $this->checkoutPagePath) || + false !== strpos($refererUri, $this->checkoutPagePath) + ); + + return $isCheckoutPage; + } } diff --git a/app/code/Magento/Persistent/Test/Unit/Observer/CheckExpirePersistentQuoteObserverTest.php b/app/code/Magento/Persistent/Test/Unit/Observer/CheckExpirePersistentQuoteObserverTest.php index a52e22a960e..8cad0b9f2dd 100644 --- a/app/code/Magento/Persistent/Test/Unit/Observer/CheckExpirePersistentQuoteObserverTest.php +++ b/app/code/Magento/Persistent/Test/Unit/Observer/CheckExpirePersistentQuoteObserverTest.php @@ -49,24 +49,39 @@ class CheckExpirePersistentQuoteObserverTest extends \PHPUnit\Framework\TestCase */ protected $eventManagerMock; + /** + * @var \PHPUnit_Framework_MockObject_MockObject|\Magento\Framework\App\RequestInterface + */ + private $requestMock; + + /** + * @inheritdoc + */ protected function setUp() { $this->sessionMock = $this->createMock(\Magento\Persistent\Helper\Session::class); $this->customerSessionMock = $this->createMock(\Magento\Customer\Model\Session::class); $this->persistentHelperMock = $this->createMock(\Magento\Persistent\Helper\Data::class); - $this->observerMock - = $this->createPartialMock(\Magento\Framework\Event\Observer::class, ['getControllerAction', - '__wakeUp']); + $this->observerMock = $this->createPartialMock( + \Magento\Framework\Event\Observer::class, + ['getControllerAction','__wakeUp'] + ); $this->quoteManagerMock = $this->createMock(\Magento\Persistent\Model\QuoteManager::class); $this->eventManagerMock = $this->createMock(\Magento\Framework\Event\ManagerInterface::class); $this->checkoutSessionMock = $this->createMock(\Magento\Checkout\Model\Session::class); + $this->requestMock = $this->getMockBuilder(\Magento\Framework\App\RequestInterface::class) + ->disableOriginalConstructor() + ->setMethods(['getRequestUri', 'getServer']) + ->getMockForAbstractClass(); + $this->model = new \Magento\Persistent\Observer\CheckExpirePersistentQuoteObserver( $this->sessionMock, $this->persistentHelperMock, $this->quoteManagerMock, $this->eventManagerMock, $this->customerSessionMock, - $this->checkoutSessionMock + $this->checkoutSessionMock, + $this->requestMock ); } @@ -76,7 +91,7 @@ public function testExecuteWhenCanNotApplyPersistentData() ->expects($this->once()) ->method('canProcess') ->with($this->observerMock) - ->will($this->returnValue(false)); + ->willReturn(false); $this->persistentHelperMock->expects($this->never())->method('isEnabled'); $this->model->execute($this->observerMock); } @@ -87,31 +102,97 @@ public function testExecuteWhenPersistentIsNotEnabled() ->expects($this->once()) ->method('canProcess') ->with($this->observerMock) - ->will($this->returnValue(true)); - $this->persistentHelperMock->expects($this->once())->method('isEnabled')->will($this->returnValue(false)); + ->willReturn(true); + $this->persistentHelperMock->expects($this->once())->method('isEnabled')->willReturn(false); $this->eventManagerMock->expects($this->never())->method('dispatch'); $this->model->execute($this->observerMock); } - public function testExecuteWhenPersistentIsEnabled() - { + /** + * Test method \Magento\Persistent\Observer\CheckExpirePersistentQuoteObserver::execute when persistent is enabled. + * + * @param string $refererUri + * @param string $requestUri + * @param \PHPUnit_Framework_MockObject_Matcher_InvokedCount $expireCounter + * @param \PHPUnit_Framework_MockObject_Matcher_InvokedCount $dispatchCounter + * @param \PHPUnit_Framework_MockObject_Matcher_InvokedCount $setCustomerIdCounter + * @return void + * @dataProvider requestDataProvider + */ + public function testExecuteWhenPersistentIsEnabled( + string $refererUri, + string $requestUri, + \PHPUnit_Framework_MockObject_Matcher_InvokedCount $expireCounter, + \PHPUnit_Framework_MockObject_Matcher_InvokedCount $dispatchCounter, + \PHPUnit_Framework_MockObject_Matcher_InvokedCount $setCustomerIdCounter + ): void { $this->persistentHelperMock ->expects($this->once()) ->method('canProcess') ->with($this->observerMock) - ->will($this->returnValue(true)); - $this->persistentHelperMock->expects($this->once())->method('isEnabled')->will($this->returnValue(true)); - $this->sessionMock->expects($this->once())->method('isPersistent')->will($this->returnValue(false)); - $this->customerSessionMock->expects($this->once())->method('isLoggedIn')->will($this->returnValue(false)); - $this->checkoutSessionMock->expects($this->once())->method('getQuoteId')->will($this->returnValue(10)); - $this->observerMock->expects($this->once())->method('getControllerAction'); - $this->eventManagerMock->expects($this->once())->method('dispatch'); - $this->quoteManagerMock->expects($this->once())->method('expire'); + ->willReturn(true); + $this->persistentHelperMock->expects($this->once())->method('isEnabled')->willReturn(true); + $this->sessionMock->expects($this->once())->method('isPersistent')->willReturn(false); $this->customerSessionMock - ->expects($this->once()) + ->expects($this->atLeastOnce()) + ->method('isLoggedIn') + ->willReturn(false); + $this->checkoutSessionMock + ->expects($this->atLeastOnce()) + ->method('getQuoteId') + ->willReturn(10); + $this->eventManagerMock->expects($dispatchCounter)->method('dispatch'); + $this->quoteManagerMock->expects($expireCounter)->method('expire'); + $this->customerSessionMock + ->expects($setCustomerIdCounter) ->method('setCustomerId') ->with(null) - ->will($this->returnSelf()); + ->willReturnSelf(); + $this->requestMock->expects($this->atLeastOnce())->method('getRequestUri')->willReturn($refererUri); + $this->requestMock + ->expects($this->atLeastOnce()) + ->method('getServer') + ->with('HTTP_REFERER') + ->willReturn($requestUri); $this->model->execute($this->observerMock); } + + /** + * Request Data Provider + * + * @return array + */ + public function requestDataProvider() + { + return [ + [ + 'refererUri' => 'checkout', + 'requestUri' => 'index', + 'expireCounter' => $this->never(), + 'dispatchCounter' => $this->never(), + 'setCustomerIdCounter' => $this->never(), + ], + [ + 'refererUri' => 'checkout', + 'requestUri' => 'checkout', + 'expireCounter' => $this->never(), + 'dispatchCounter' => $this->never(), + 'setCustomerIdCounter' => $this->never(), + ], + [ + 'refererUri' => 'index', + 'requestUri' => 'checkout', + 'expireCounter' => $this->never(), + 'dispatchCounter' => $this->never(), + 'setCustomerIdCounter' => $this->never(), + ], + [ + 'refererUri' => 'index', + 'requestUri' => 'index', + 'expireCounter' => $this->once(), + 'dispatchCounter' => $this->once(), + 'setCustomerIdCounter' => $this->once(), + ], + ]; + } } diff --git a/app/code/Magento/Rule/Model/Action/AbstractAction.php b/app/code/Magento/Rule/Model/Action/AbstractAction.php index fb15edf8a48..4d56f6cc56e 100644 --- a/app/code/Magento/Rule/Model/Action/AbstractAction.php +++ b/app/code/Magento/Rule/Model/Action/AbstractAction.php @@ -49,13 +49,16 @@ public function __construct( $this->loadAttributeOptions()->loadOperatorOptions()->loadValueOptions(); - foreach (array_keys($this->getAttributeOption()) as $attr) { - $this->setAttribute($attr); - break; + $attributes = $this->getAttributeOption(); + if ($attributes) { + reset($attributes); + $this->setAttribute(key($attributes)); } - foreach (array_keys($this->getOperatorOption()) as $operator) { - $this->setOperator($operator); - break; + + $operators = $this->getOperatorOption(); + if ($operators) { + reset($operators); + $this->setOperator(key($operators)); } } diff --git a/app/code/Magento/Rule/Model/Condition/Combine.php b/app/code/Magento/Rule/Model/Condition/Combine.php index 24ed1cb4974..48873aec662 100644 --- a/app/code/Magento/Rule/Model/Condition/Combine.php +++ b/app/code/Magento/Rule/Model/Condition/Combine.php @@ -46,10 +46,8 @@ public function __construct(Context $context, array $data = []) $this->loadAggregatorOptions(); $options = $this->getAggregatorOptions(); if ($options) { - foreach (array_keys($options) as $aggregator) { - $this->setAggregator($aggregator); - break; - } + reset($options); + $this->setAggregator(key($options)); } } @@ -90,9 +88,10 @@ public function getAggregatorName() public function getAggregatorElement() { if ($this->getAggregator() === null) { - foreach (array_keys($this->getAggregatorOption()) as $key) { - $this->setAggregator($key); - break; + $options = $this->getAggregatorOption(); + if ($options) { + reset($options); + $this->setAggregator(key($options)); } } return $this->getForm()->addField( diff --git a/app/code/Magento/Rule/view/adminhtml/web/conditions-data-normalizer.js b/app/code/Magento/Rule/view/adminhtml/web/conditions-data-normalizer.js index c9c36c4fa58..8eebc9ef0bd 100644 --- a/app/code/Magento/Rule/view/adminhtml/web/conditions-data-normalizer.js +++ b/app/code/Magento/Rule/view/adminhtml/web/conditions-data-normalizer.js @@ -14,11 +14,11 @@ define([ */ var ConditionsDataNormalizer = function () { this.patterns = { - validate: /^[a-z0-9_-][a-z0-9_-]*(?:\[(?:\d*|[a-z0-9_-]+)\])*$/i, - key: /[a-z0-9_-]+|(?=\[\])/gi, + validate: /^[a-z0-9_.-][a-z0-9_.-]*(?:\[(?:\d*|[a-z0-9_.-]+)\])*$/i, + key: /[a-z0-9_.-]+|(?=\[\])/gi, push: /^$/, fixed: /^\d+$/, - named: /^[a-z0-9_-]+$/i + named: /^[a-z0-9_.-]+$/i }; }; diff --git a/app/code/Magento/Sales/Model/Order/Payment/State/RegisterCaptureNotificationCommand.php b/app/code/Magento/Sales/Model/Order/Payment/State/RegisterCaptureNotificationCommand.php index ee12b459118..d38e58d7341 100644 --- a/app/code/Magento/Sales/Model/Order/Payment/State/RegisterCaptureNotificationCommand.php +++ b/app/code/Magento/Sales/Model/Order/Payment/State/RegisterCaptureNotificationCommand.php @@ -35,7 +35,7 @@ public function __construct(StatusResolver $statusResolver = null) */ public function execute(OrderPaymentInterface $payment, $amount, OrderInterface $order) { - $state = Order::STATE_PROCESSING; + $state = $order->getState() ?: Order::STATE_PROCESSING; $status = null; $message = 'Registered notification about captured amount of %1.'; diff --git a/app/code/Magento/Sales/Test/Unit/Model/Order/Payment/State/RegisterCaptureNotificationCommandTest.php b/app/code/Magento/Sales/Test/Unit/Model/Order/Payment/State/RegisterCaptureNotificationCommandTest.php index 32ea9d88693..1b762fafe0b 100644 --- a/app/code/Magento/Sales/Test/Unit/Model/Order/Payment/State/RegisterCaptureNotificationCommandTest.php +++ b/app/code/Magento/Sales/Test/Unit/Model/Order/Payment/State/RegisterCaptureNotificationCommandTest.php @@ -32,26 +32,29 @@ class RegisterCaptureNotificationCommandTest extends \PHPUnit\Framework\TestCase * * @param bool $isTransactionPending * @param bool $isFraudDetected + * @param string|null $currentState * @param string $expectedState * @param string $expectedStatus * @param string $expectedMessage - * + * @return void * @dataProvider commandResultDataProvider */ public function testExecute( - $isTransactionPending, - $isFraudDetected, - $expectedState, - $expectedStatus, - $expectedMessage - ) { + bool $isTransactionPending, + bool $isFraudDetected, + $currentState, + string $expectedState, + string $expectedStatus, + string $expectedMessage + ): void { + $order = $this->getOrder($currentState); $actualReturn = (new RegisterCaptureNotificationCommand($this->getStatusResolver()))->execute( $this->getPayment($isTransactionPending, $isFraudDetected), $this->amount, - $this->getOrder() + $order ); - $this->assertOrderStateAndStatus($this->getOrder(), $expectedState, $expectedStatus); + $this->assertOrderStateAndStatus($order, $expectedState, $expectedStatus); self::assertEquals(__($expectedMessage, $this->amount), $actualReturn); } @@ -64,30 +67,42 @@ public function commandResultDataProvider() [ false, false, + Order::STATE_COMPLETE, + Order::STATE_COMPLETE, + $this->newOrderStatus, + 'Registered notification about captured amount of %1.', + ], + [ + false, + false, + null, Order::STATE_PROCESSING, $this->newOrderStatus, - 'Registered notification about captured amount of %1.' + 'Registered notification about captured amount of %1.', ], [ true, false, + Order::STATE_PROCESSING, Order::STATE_PAYMENT_REVIEW, $this->newOrderStatus, - 'An amount of %1 will be captured after being approved at the payment gateway.' + 'An amount of %1 will be captured after being approved at the payment gateway.', ], [ false, true, + Order::STATE_PROCESSING, Order::STATE_PAYMENT_REVIEW, Order::STATUS_FRAUD, - 'Order is suspended as its capture amount %1 is suspected to be fraudulent.' + 'Order is suspended as its capture amount %1 is suspected to be fraudulent.', ], [ true, true, + Order::STATE_PROCESSING, Order::STATE_PAYMENT_REVIEW, Order::STATUS_FRAUD, - 'Order is suspended as its capture amount %1 is suspected to be fraudulent.' + 'Order is suspended as its capture amount %1 is suspected to be fraudulent.', ], ]; } @@ -107,15 +122,19 @@ private function getStatusResolver() } /** + * @param string|null $state * @return Order|MockObject */ - private function getOrder() + private function getOrder($state) { + /** @var Order|MockObject $order */ $order = $this->getMockBuilder(Order::class) ->disableOriginalConstructor() + ->setMethods(['getBaseCurrency', 'getOrderStatusByState']) ->getMock(); $order->method('getBaseCurrency') ->willReturn($this->getCurrency()); + $order->setState($state); return $order; } @@ -159,7 +178,7 @@ private function getCurrency() */ private function assertOrderStateAndStatus($order, $expectedState, $expectedStatus) { - $order->method('setState')->with($expectedState); - $order->method('setStatus')->with($expectedStatus); + self::assertEquals($expectedState, $order->getState(), 'The order {state} should match.'); + self::assertEquals($expectedStatus, $order->getStatus(), 'The order {status} should match.'); } } diff --git a/app/code/Magento/SalesRule/Model/Quote/ChildrenValidationLocator.php b/app/code/Magento/SalesRule/Model/Quote/ChildrenValidationLocator.php new file mode 100644 index 00000000000..af1f61c1871 --- /dev/null +++ b/app/code/Magento/SalesRule/Model/Quote/ChildrenValidationLocator.php @@ -0,0 +1,53 @@ + + * [ + * 'ProductType1' => true, + * 'ProductType2' => false + * ] + * + */ + public function __construct( + array $productTypeChildrenValidationMap = [] + ) { + $this->productTypeChildrenValidationMap = $productTypeChildrenValidationMap; + } + + /** + * Checks necessity to validate rule on item's children. + * + * @param QuoteItem $item + * @return bool + */ + public function isChildrenValidationRequired(QuoteItem $item): bool + { + $type = $item->getProduct()->getTypeId(); + if (isset($this->productTypeChildrenValidationMap[$type])) { + return (bool)$this->productTypeChildrenValidationMap[$type]; + } + + return true; + } +} diff --git a/app/code/Magento/SalesRule/Model/RulesApplier.php b/app/code/Magento/SalesRule/Model/RulesApplier.php index 06a4e252bf6..f771a4f1e38 100644 --- a/app/code/Magento/SalesRule/Model/RulesApplier.php +++ b/app/code/Magento/SalesRule/Model/RulesApplier.php @@ -6,6 +6,9 @@ namespace Magento\SalesRule\Model; use Magento\Quote\Model\Quote\Address; +use Magento\SalesRule\Model\Quote\ChildrenValidationLocator; +use Magento\Framework\App\ObjectManager; +use Magento\SalesRule\Model\Rule\Action\Discount\CalculatorFactory; /** * Class RulesApplier @@ -25,19 +28,33 @@ class RulesApplier */ protected $validatorUtility; + /** + * @var ChildrenValidationLocator + */ + private $childrenValidationLocator; + + /** + * @var CalculatorFactory + */ + private $calculatorFactory; + /** * @param \Magento\SalesRule\Model\Rule\Action\Discount\CalculatorFactory $calculatorFactory * @param \Magento\Framework\Event\ManagerInterface $eventManager * @param \Magento\SalesRule\Model\Utility $utility + * @param ChildrenValidationLocator|null $childrenValidationLocator */ public function __construct( \Magento\SalesRule\Model\Rule\Action\Discount\CalculatorFactory $calculatorFactory, \Magento\Framework\Event\ManagerInterface $eventManager, - \Magento\SalesRule\Model\Utility $utility + \Magento\SalesRule\Model\Utility $utility, + ChildrenValidationLocator $childrenValidationLocator = null ) { $this->calculatorFactory = $calculatorFactory; $this->validatorUtility = $utility; $this->_eventManager = $eventManager; + $this->childrenValidationLocator = $childrenValidationLocator + ?: ObjectManager::getInstance()->get(ChildrenValidationLocator::class); } /** @@ -61,6 +78,9 @@ public function applyRules($item, $rules, $skipValidation, $couponCode) } if (!$skipValidation && !$rule->getActions()->validate($item)) { + if (!$this->childrenValidationLocator->isChildrenValidationRequired($item)) { + continue; + } $childItems = $item->getChildren(); $isContinue = true; if (!empty($childItems)) { diff --git a/app/code/Magento/SalesRule/Test/Unit/Model/Quote/ChildrenValidationLocatorTest.php b/app/code/Magento/SalesRule/Test/Unit/Model/Quote/ChildrenValidationLocatorTest.php new file mode 100644 index 00000000000..abb8d791d74 --- /dev/null +++ b/app/code/Magento/SalesRule/Test/Unit/Model/Quote/ChildrenValidationLocatorTest.php @@ -0,0 +1,104 @@ +objectManager = new ObjectManager($this); + + $this->productTypeChildrenValidationMap = [ + 'type1' => true, + 'type2' => false, + ]; + + $this->quoteItemMock = $this->getMockBuilder(QuoteItem::class) + ->disableOriginalConstructor() + ->setMethods(['getProduct']) + ->getMockForAbstractClass(); + + $this->productMock = $this->getMockBuilder(Product::class) + ->disableOriginalConstructor() + ->setMethods(['getTypeId']) + ->getMock(); + + $this->model = $this->objectManager->getObject( + ChildrenValidationLocator::class, + [ + 'productTypeChildrenValidationMap' => $this->productTypeChildrenValidationMap, + ] + ); + } + + /** + * @dataProvider productTypeDataProvider + * @param string $type + * @param bool $expected + * + * @return void + */ + public function testIsChildrenValidationRequired(string $type, bool $expected): void + { + $this->quoteItemMock->expects($this->once()) + ->method('getProduct') + ->willReturn($this->productMock); + + $this->productMock->expects($this->once()) + ->method('getTypeId') + ->willReturn($type); + + $this->assertEquals($this->model->isChildrenValidationRequired($this->quoteItemMock), $expected); + } + + /** + * @return array + */ + public function productTypeDataProvider(): array + { + return [ + ['type1', true], + ['type2', false], + ['type3', true], + ]; + } +} diff --git a/app/code/Magento/SalesRule/Test/Unit/Model/RulesApplierTest.php b/app/code/Magento/SalesRule/Test/Unit/Model/RulesApplierTest.php index 814048c2ac1..37c839d413d 100644 --- a/app/code/Magento/SalesRule/Test/Unit/Model/RulesApplierTest.php +++ b/app/code/Magento/SalesRule/Test/Unit/Model/RulesApplierTest.php @@ -6,6 +6,9 @@ namespace Magento\SalesRule\Test\Unit\Model; +/** + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + */ class RulesApplierTest extends \PHPUnit\Framework\TestCase { /** @@ -28,6 +31,11 @@ class RulesApplierTest extends \PHPUnit\Framework\TestCase */ protected $validatorUtility; + /** + * @var \Magento\SalesRule\Model\Quote\ChildrenValidationLocator|\PHPUnit_Framework_MockObject_MockObject + */ + protected $childrenValidationLocator; + protected function setUp() { $this->calculatorFactory = $this->createMock( @@ -38,11 +46,15 @@ protected function setUp() \Magento\SalesRule\Model\Utility::class, ['canProcessRule', 'minFix', 'deltaRoundingFix', 'getItemQty'] ); - + $this->childrenValidationLocator = $this->createPartialMock( + \Magento\SalesRule\Model\Quote\ChildrenValidationLocator::class, + ['isChildrenValidationRequired'] + ); $this->rulesApplier = new \Magento\SalesRule\Model\RulesApplier( $this->calculatorFactory, $this->eventManager, - $this->validatorUtility + $this->validatorUtility, + $this->childrenValidationLocator ); } @@ -84,6 +96,10 @@ public function testApplyRulesWhenRuleWithStopRulesProcessingIsUsed($isChildren, $item->setDiscountCalculationPrice($positivePrice); $item->setData('calculation_price', $positivePrice); + $this->childrenValidationLocator->expects($this->any()) + ->method('isChildrenValidationRequired') + ->willReturn(true); + $this->validatorUtility->expects($this->atLeastOnce()) ->method('canProcessRule') ->will($this->returnValue(true)); diff --git a/app/code/Magento/Search/Model/SynonymAnalyzer.php b/app/code/Magento/Search/Model/SynonymAnalyzer.php index a63c10da169..eea6a950d7c 100644 --- a/app/code/Magento/Search/Model/SynonymAnalyzer.php +++ b/app/code/Magento/Search/Model/SynonymAnalyzer.php @@ -83,7 +83,7 @@ public function getSynonymsForPhrase($phrase) /** * Helper method to find the matching of $pattern to $synonymGroupsToExamine. * If matches, the particular array index is returned. - * Otherwise false will be returned. + * Otherwise null will be returned. * * @param string $pattern * @param array $synonymGroupsToExamine @@ -99,6 +99,7 @@ private function findInArray(string $pattern, array $synonymGroupsToExamine) } $position++; } + return null; } diff --git a/app/code/Magento/Signifyd/Block/Fingerprint.php b/app/code/Magento/Signifyd/Block/Fingerprint.php index 7afa092b3d0..db76fc6c944 100644 --- a/app/code/Magento/Signifyd/Block/Fingerprint.php +++ b/app/code/Magento/Signifyd/Block/Fingerprint.php @@ -85,6 +85,8 @@ public function getSignifydOrderSessionId() */ public function isModuleActive() { - return $this->config->isActive(); + $storeId = $this->quoteSession->getQuote()->getStoreId(); + + return $this->config->isActive($storeId); } } diff --git a/app/code/Magento/Signifyd/Model/Config.php b/app/code/Magento/Signifyd/Model/Config.php index b68380ee15b..15d3608bd38 100644 --- a/app/code/Magento/Signifyd/Model/Config.php +++ b/app/code/Magento/Signifyd/Model/Config.php @@ -34,13 +34,15 @@ public function __construct(ScopeConfigInterface $scopeConfig) * If this config option set to false no Signifyd integration should be available * (only possibility to configure Signifyd setting in admin) * + * @param int|null $storeId * @return bool */ - public function isActive() + public function isActive($storeId = null): bool { $enabled = $this->scopeConfig->isSetFlag( 'fraud_protection/signifyd/active', - ScopeInterface::SCOPE_STORE + ScopeInterface::SCOPE_STORE, + $storeId ); return $enabled; } @@ -51,13 +53,15 @@ public function isActive() * @see https://www.signifyd.com/docs/api/#/introduction/authentication * @see https://app.signifyd.com/settings * + * @param int|null $storeId * @return string */ - public function getApiKey() + public function getApiKey($storeId = null): string { $apiKey = $this->scopeConfig->getValue( 'fraud_protection/signifyd/api_key', - ScopeInterface::SCOPE_STORE + ScopeInterface::SCOPE_STORE, + $storeId ); return $apiKey; } @@ -66,13 +70,15 @@ public function getApiKey() * Base URL to Signifyd REST API. * Usually equals to https://api.signifyd.com/v2 and should not be changed * + * @param int|null $storeId * @return string */ - public function getApiUrl() + public function getApiUrl($storeId = null): string { $apiUrl = $this->scopeConfig->getValue( 'fraud_protection/signifyd/api_url', - ScopeInterface::SCOPE_STORE + ScopeInterface::SCOPE_STORE, + $storeId ); return $apiUrl; } @@ -80,13 +86,15 @@ public function getApiUrl() /** * If is "true" extra information about interaction with Signifyd API are written to debug.log file * + * @param int|null $storeId * @return bool */ - public function isDebugModeEnabled() + public function isDebugModeEnabled($storeId = null): bool { $debugModeEnabled = $this->scopeConfig->isSetFlag( 'fraud_protection/signifyd/debug', - ScopeInterface::SCOPE_STORE + ScopeInterface::SCOPE_STORE, + $storeId ); return $debugModeEnabled; } diff --git a/app/code/Magento/Signifyd/Model/SignifydGateway/ApiClient.php b/app/code/Magento/Signifyd/Model/SignifydGateway/ApiClient.php index 0950ca1e22c..2d6d57a510a 100644 --- a/app/code/Magento/Signifyd/Model/SignifydGateway/ApiClient.php +++ b/app/code/Magento/Signifyd/Model/SignifydGateway/ApiClient.php @@ -36,12 +36,13 @@ public function __construct( * * @param string $url * @param string $method - * @param array $params + * @param array $params + * @param int|null $storeId * @return array */ - public function makeApiCall($url, $method, array $params = []) + public function makeApiCall($url, $method, array $params = [], $storeId = null): array { - $result = $this->requestBuilder->doRequest($url, $method, $params); + $result = $this->requestBuilder->doRequest($url, $method, $params, $storeId); return $result; } diff --git a/app/code/Magento/Signifyd/Model/SignifydGateway/Client/HttpClientFactory.php b/app/code/Magento/Signifyd/Model/SignifydGateway/Client/HttpClientFactory.php index 41006bd7d1e..2a9b933b98b 100644 --- a/app/code/Magento/Signifyd/Model/SignifydGateway/Client/HttpClientFactory.php +++ b/app/code/Magento/Signifyd/Model/SignifydGateway/Client/HttpClientFactory.php @@ -73,12 +73,13 @@ public function __construct( * @param string $url * @param string $method * @param array $params + * @param int|null $storeId * @return ZendClient */ - public function create($url, $method, array $params = []) + public function create($url, $method, array $params = [], $storeId = null): ZendClient { - $apiKey = $this->getApiKey(); - $apiUrl = $this->buildFullApiUrl($url); + $apiKey = $this->getApiKey($storeId); + $apiUrl = $this->buildFullApiUrl($url, $storeId); $client = $this->createNewClient(); $client->setHeaders( @@ -107,22 +108,24 @@ private function createNewClient() * Signifyd API key for merchant account. * * @see https://www.signifyd.com/docs/api/#/introduction/authentication + * @param int|null $storeId * @return string */ - private function getApiKey() + private function getApiKey($storeId): string { - return $this->config->getApiKey(); + return $this->config->getApiKey($storeId); } /** * Full URL for Singifyd API based on relative URL. * * @param string $url + * @param int|null $storeId * @return string */ - private function buildFullApiUrl($url) + private function buildFullApiUrl($url, $storeId): string { - $baseApiUrl = $this->getBaseApiUrl(); + $baseApiUrl = $this->getBaseApiUrl($storeId); $fullUrl = $baseApiUrl . self::$urlSeparator . ltrim($url, self::$urlSeparator); return $fullUrl; @@ -131,11 +134,12 @@ private function buildFullApiUrl($url) /** * Base Sigifyd API URL without trailing slash. * + * @param int|null $storeId * @return string */ - private function getBaseApiUrl() + private function getBaseApiUrl($storeId): string { - $baseApiUrl = $this->config->getApiUrl(); + $baseApiUrl = $this->config->getApiUrl($storeId); return rtrim($baseApiUrl, self::$urlSeparator); } diff --git a/app/code/Magento/Signifyd/Model/SignifydGateway/Client/RequestBuilder.php b/app/code/Magento/Signifyd/Model/SignifydGateway/Client/RequestBuilder.php index 2ab4395e199..ee079a74d34 100644 --- a/app/code/Magento/Signifyd/Model/SignifydGateway/Client/RequestBuilder.php +++ b/app/code/Magento/Signifyd/Model/SignifydGateway/Client/RequestBuilder.php @@ -5,8 +5,6 @@ */ namespace Magento\Signifyd\Model\SignifydGateway\Client; -use Magento\Framework\HTTP\ZendClient; - /** * Class RequestBuilder * Creates HTTP client, sends request to Signifyd and handles response @@ -50,13 +48,14 @@ public function __construct( * * @param string $url * @param string $method - * @param array $params + * @param array $params + * @param int|null $storeId * @return array */ - public function doRequest($url, $method, array $params = []) + public function doRequest($url, $method, array $params = [], $storeId = null): array { - $client = $this->clientCreator->create($url, $method, $params); - $response = $this->requestSender->send($client); + $client = $this->clientCreator->create($url, $method, $params, $storeId); + $response = $this->requestSender->send($client, $storeId); $result = $this->responseHandler->handle($response); return $result; diff --git a/app/code/Magento/Signifyd/Model/SignifydGateway/Client/RequestSender.php b/app/code/Magento/Signifyd/Model/SignifydGateway/Client/RequestSender.php index 38128a799fd..a63331e055c 100644 --- a/app/code/Magento/Signifyd/Model/SignifydGateway/Client/RequestSender.php +++ b/app/code/Magento/Signifyd/Model/SignifydGateway/Client/RequestSender.php @@ -39,15 +39,16 @@ public function __construct( * debug information is recorded to debug.log. * * @param ZendClient $client + * @param int|null $storeId * @return \Zend_Http_Response * @throws ApiCallException */ - public function send(ZendClient $client) + public function send(ZendClient $client, $storeId = null): \Zend_Http_Response { try { $response = $client->request(); - $this->debuggerFactory->create()->success( + $this->debuggerFactory->create($storeId)->success( $client->getUri(true), $client->getLastRequest(), $response->getStatus() . ' ' . $response->getMessage(), @@ -56,7 +57,7 @@ public function send(ZendClient $client) return $response; } catch (\Exception $e) { - $this->debuggerFactory->create()->failure( + $this->debuggerFactory->create($storeId)->failure( $client->getUri(true), $client->getLastRequest(), $e diff --git a/app/code/Magento/Signifyd/Model/SignifydGateway/Debugger/DebuggerFactory.php b/app/code/Magento/Signifyd/Model/SignifydGateway/Debugger/DebuggerFactory.php index 19408e99ae0..1e61a313899 100644 --- a/app/code/Magento/Signifyd/Model/SignifydGateway/Debugger/DebuggerFactory.php +++ b/app/code/Magento/Signifyd/Model/SignifydGateway/Debugger/DebuggerFactory.php @@ -44,11 +44,12 @@ public function __construct( /** * Create debugger instance * + * @param int|null $storeId * @return DebuggerInterface */ - public function create() + public function create($storeId = null): DebuggerInterface { - if (!$this->config->isDebugModeEnabled()) { + if (!$this->config->isDebugModeEnabled($storeId)) { return $this->objectManager->get(BlackHole::class); } diff --git a/app/code/Magento/Signifyd/Model/SignifydGateway/Gateway.php b/app/code/Magento/Signifyd/Model/SignifydGateway/Gateway.php index ddcaa6cd696..9f7a053c587 100644 --- a/app/code/Magento/Signifyd/Model/SignifydGateway/Gateway.php +++ b/app/code/Magento/Signifyd/Model/SignifydGateway/Gateway.php @@ -5,8 +5,9 @@ */ namespace Magento\Signifyd\Model\SignifydGateway; +use Magento\Sales\Api\OrderRepositoryInterface; +use Magento\Signifyd\Api\CaseRepositoryInterface; use Magento\Signifyd\Model\SignifydGateway\Request\CreateCaseBuilderInterface; -use Magento\Signifyd\Model\SignifydGateway\ApiClient; /** * Signifyd Gateway. @@ -53,18 +54,34 @@ class Gateway */ private $apiClient; + /** + * @var OrderRepositoryInterface + */ + private $orderRepository; + + /** + * @var CaseRepositoryInterface + */ + private $caseRepository; + /** * Gateway constructor. * * @param CreateCaseBuilderInterface $createCaseBuilder * @param ApiClient $apiClient + * @param OrderRepositoryInterface $orderRepository + * @param CaseRepositoryInterface $caseRepository */ public function __construct( CreateCaseBuilderInterface $createCaseBuilder, - ApiClient $apiClient + ApiClient $apiClient, + OrderRepositoryInterface $orderRepository, + CaseRepositoryInterface $caseRepository ) { $this->createCaseBuilder = $createCaseBuilder; $this->apiClient = $apiClient; + $this->orderRepository = $orderRepository; + $this->caseRepository = $caseRepository; } /** @@ -78,11 +95,13 @@ public function __construct( public function createCase($orderId) { $caseParams = $this->createCaseBuilder->build($orderId); + $storeId = $this->getStoreIdFromOrder($orderId); $caseCreationResult = $this->apiClient->makeApiCall( '/cases', 'POST', - $caseParams + $caseParams, + $storeId ); if (!isset($caseCreationResult['investigationId'])) { @@ -102,12 +121,14 @@ public function createCase($orderId) */ public function submitCaseForGuarantee($signifydCaseId) { + $storeId = $this->getStoreIdFromCase($signifydCaseId); $guaranteeCreationResult = $this->apiClient->makeApiCall( '/guarantees', 'POST', [ 'caseId' => $signifydCaseId, - ] + ], + $storeId ); $disposition = $this->processDispositionResult($guaranteeCreationResult); @@ -124,12 +145,14 @@ public function submitCaseForGuarantee($signifydCaseId) */ public function cancelGuarantee($caseId) { + $storeId = $this->getStoreIdFromCase($caseId); $result = $this->apiClient->makeApiCall( '/cases/' . $caseId . '/guarantee', 'PUT', [ 'guaranteeDisposition' => self::GUARANTEE_CANCELED - ] + ], + $storeId ); $disposition = $this->processDispositionResult($result); @@ -172,4 +195,31 @@ private function processDispositionResult(array $result) return $disposition; } + + /** + * Returns store id by case. + * + * @param int $caseId + * @return int|null + */ + private function getStoreIdFromCase(int $caseId) + { + $case = $this->caseRepository->getByCaseId($caseId); + $orderId = $case->getOrderId(); + + return $this->getStoreIdFromOrder($orderId); + } + + /** + * Returns store id from order. + * + * @param int $orderId + * @return int|null + */ + private function getStoreIdFromOrder(int $orderId) + { + $order = $this->orderRepository->get($orderId); + + return $order->getStoreId(); + } } diff --git a/app/code/Magento/Signifyd/Observer/PlaceOrder.php b/app/code/Magento/Signifyd/Observer/PlaceOrder.php index 3798522dbe5..8415bc006b8 100644 --- a/app/code/Magento/Signifyd/Observer/PlaceOrder.php +++ b/app/code/Magento/Signifyd/Observer/PlaceOrder.php @@ -55,10 +55,6 @@ public function __construct( */ public function execute(Observer $observer) { - if (!$this->signifydIntegrationConfig->isActive()) { - return; - } - $orders = $this->extractOrders( $observer->getEvent() ); @@ -68,7 +64,10 @@ public function execute(Observer $observer) } foreach ($orders as $order) { - $this->createCaseForOrder($order); + $storeId = $order->getStoreId(); + if ($this->signifydIntegrationConfig->isActive($storeId)) { + $this->createCaseForOrder($order); + } } } diff --git a/app/code/Magento/Signifyd/Test/Unit/Model/SignifydGateway/Client/HttpClientFactoryTest.php b/app/code/Magento/Signifyd/Test/Unit/Model/SignifydGateway/Client/HttpClientFactoryTest.php index 776e8a75b96..4aefd633557 100644 --- a/app/code/Magento/Signifyd/Test/Unit/Model/SignifydGateway/Client/HttpClientFactoryTest.php +++ b/app/code/Magento/Signifyd/Test/Unit/Model/SignifydGateway/Client/HttpClientFactoryTest.php @@ -101,14 +101,17 @@ public function testCreateHttpClient() public function testCreateWithParams() { $param = ['id' => 1]; + $storeId = 1; $json = '{"id":1}'; $this->config->expects($this->once()) ->method('getApiKey') + ->with($storeId) ->willReturn('testKey'); $this->config->expects($this->once()) ->method('getApiUrl') + ->with($storeId) ->willReturn(self::$dummy); $this->dataEncoder->expects($this->once()) @@ -121,7 +124,7 @@ public function testCreateWithParams() ->with($this->equalTo($json), 'application/json') ->willReturnSelf(); - $client = $this->httpClient->create('url', 'method', $param); + $client = $this->httpClient->create('url', 'method', $param, $storeId); $this->assertInstanceOf(ZendClient::class, $client); } diff --git a/app/code/Magento/Signifyd/Test/Unit/Model/SignifydGateway/GatewayTest.php b/app/code/Magento/Signifyd/Test/Unit/Model/SignifydGateway/GatewayTest.php index c34e64f469f..2a05189e0e3 100644 --- a/app/code/Magento/Signifyd/Test/Unit/Model/SignifydGateway/GatewayTest.php +++ b/app/code/Magento/Signifyd/Test/Unit/Model/SignifydGateway/GatewayTest.php @@ -5,7 +5,10 @@ */ namespace Magento\Signifyd\Test\Unit\Model\SignifydGateway; -use \PHPUnit\Framework\TestCase as TestCase; +use Magento\Sales\Api\Data\OrderInterface; +use Magento\Sales\Api\OrderRepositoryInterface; +use Magento\Signifyd\Api\CaseRepositoryInterface; +use Magento\Signifyd\Api\Data\CaseInterface; use PHPUnit_Framework_MockObject_MockObject as MockObject; use Magento\Signifyd\Model\SignifydGateway\Gateway; use Magento\Signifyd\Model\SignifydGateway\GatewayException; @@ -30,6 +33,16 @@ class GatewayTest extends \PHPUnit\Framework\TestCase */ private $gateway; + /** + * @var OrderRepositoryInterface|MockObject + */ + private $orderRepository; + + /** + * @var CaseRepositoryInterface|MockObject + */ + private $caseRepository; + public function setUp() { $this->createCaseBuilder = $this->getMockBuilder(CreateCaseBuilderInterface::class) @@ -39,16 +52,27 @@ public function setUp() ->disableOriginalConstructor() ->getMock(); + $this->orderRepository = $this->getMockBuilder(OrderRepositoryInterface::class) + ->getMockForAbstractClass(); + + $this->caseRepository= $this->getMockBuilder(CaseRepositoryInterface::class) + ->getMockForAbstractClass(); + $this->gateway = new Gateway( $this->createCaseBuilder, - $this->apiClient + $this->apiClient, + $this->orderRepository, + $this->caseRepository ); } public function testCreateCaseForSpecifiedOrder() { $dummyOrderId = 1; + $dummyStoreId = 2; $dummySignifydInvestigationId = 42; + + $this->withOrderEntity($dummyOrderId, $dummyStoreId); $this->apiClient ->method('makeApiCall') ->willReturn([ @@ -68,7 +92,10 @@ public function testCreateCaseForSpecifiedOrder() public function testCreateCaseCallsValidApiMethod() { $dummyOrderId = 1; + $dummyStoreId = 2; $dummySignifydInvestigationId = 42; + + $this->withOrderEntity($dummyOrderId, $dummyStoreId); $this->createCaseBuilder ->method('build') ->willReturn([]); @@ -79,7 +106,8 @@ public function testCreateCaseCallsValidApiMethod() ->with( $this->equalTo('/cases'), $this->equalTo('POST'), - $this->isType('array') + $this->isType('array'), + $this->equalTo($dummyStoreId) ) ->willReturn([ 'investigationId' => $dummySignifydInvestigationId @@ -92,7 +120,10 @@ public function testCreateCaseCallsValidApiMethod() public function testCreateCaseNormalFlow() { $dummyOrderId = 1; + $dummyStoreId = 2; $dummySignifydInvestigationId = 42; + + $this->withOrderEntity($dummyOrderId, $dummyStoreId); $this->createCaseBuilder ->method('build') ->willReturn([]); @@ -113,7 +144,10 @@ public function testCreateCaseNormalFlow() public function testCreateCaseWithFailedApiCall() { $dummyOrderId = 1; + $dummyStoreId = 2; $apiCallFailureMessage = 'Api call failed'; + + $this->withOrderEntity($dummyOrderId, $dummyStoreId); $this->createCaseBuilder ->method('build') ->willReturn([]); @@ -129,6 +163,9 @@ public function testCreateCaseWithFailedApiCall() public function testCreateCaseWithMissedResponseRequiredData() { $dummyOrderId = 1; + $dummyStoreId = 2; + + $this->withOrderEntity($dummyOrderId, $dummyStoreId); $this->createCaseBuilder ->method('build') ->willReturn([]); @@ -145,7 +182,10 @@ public function testCreateCaseWithMissedResponseRequiredData() public function testCreateCaseWithAdditionalResponseData() { $dummyOrderId = 1; + $dummyStoreId = 2; $dummySignifydInvestigationId = 42; + + $this->withOrderEntity($dummyOrderId, $dummyStoreId); $this->createCaseBuilder ->method('build') ->willReturn([]); @@ -167,8 +207,10 @@ public function testCreateCaseWithAdditionalResponseData() public function testSubmitCaseForGuaranteeCallsValidApiMethod() { $dummySygnifydCaseId = 42; + $dummyStoreId = 1; $dummyDisposition = 'APPROVED'; + $this->withCaseEntity($dummySygnifydCaseId, $dummyStoreId); $this->apiClient ->expects($this->atLeastOnce()) ->method('makeApiCall') @@ -177,7 +219,8 @@ public function testSubmitCaseForGuaranteeCallsValidApiMethod() $this->equalTo('POST'), $this->equalTo([ 'caseId' => $dummySygnifydCaseId - ]) + ]), + $this->equalTo($dummyStoreId) )->willReturn([ 'disposition' => $dummyDisposition ]); @@ -189,8 +232,10 @@ public function testSubmitCaseForGuaranteeCallsValidApiMethod() public function testSubmitCaseForGuaranteeWithFailedApiCall() { $dummySygnifydCaseId = 42; + $dummyStoreId = 1; $apiCallFailureMessage = 'Api call failed'; + $this->withCaseEntity($dummySygnifydCaseId, $dummyStoreId); $this->apiClient ->method('makeApiCall') ->willThrowException(new ApiCallException($apiCallFailureMessage)); @@ -204,10 +249,12 @@ public function testSubmitCaseForGuaranteeWithFailedApiCall() public function testSubmitCaseForGuaranteeReturnsDisposition() { $dummySygnifydCaseId = 42; + $dummyStoreId = 1; $dummyDisposition = 'APPROVED'; $dummyGuaranteeId = 123; $dummyRereviewCount = 0; + $this->withCaseEntity($dummySygnifydCaseId, $dummyStoreId); $this->apiClient ->method('makeApiCall') ->willReturn([ @@ -227,9 +274,11 @@ public function testSubmitCaseForGuaranteeReturnsDisposition() public function testSubmitCaseForGuaranteeWithMissedDisposition() { $dummySygnifydCaseId = 42; + $dummyStoreId = 1; $dummyGuaranteeId = 123; $dummyRereviewCount = 0; + $this->withCaseEntity($dummySygnifydCaseId, $dummyStoreId); $this->apiClient ->method('makeApiCall') ->willReturn([ @@ -244,8 +293,10 @@ public function testSubmitCaseForGuaranteeWithMissedDisposition() public function testSubmitCaseForGuaranteeWithUnexpectedDisposition() { $dummySygnifydCaseId = 42; + $dummyStoreId = 1; $dummyUnexpectedDisposition = 'UNEXPECTED'; + $this->withCaseEntity($dummySygnifydCaseId, $dummyStoreId); $this->apiClient ->method('makeApiCall') ->willReturn([ @@ -263,7 +314,9 @@ public function testSubmitCaseForGuaranteeWithUnexpectedDisposition() public function testSubmitCaseForGuaranteeWithExpectedDisposition($dummyExpectedDisposition) { $dummySygnifydCaseId = 42; + $dummyStoreId = 1; + $this->withCaseEntity($dummySygnifydCaseId, $dummyStoreId); $this->apiClient ->method('makeApiCall') ->willReturn([ @@ -290,11 +343,20 @@ public function testSubmitCaseForGuaranteeWithExpectedDisposition($dummyExpected public function testCancelGuarantee() { $caseId = 123; + $dummyStoreId = 1; + $this->withCaseEntity($caseId, $dummyStoreId); $this->apiClient->expects(self::once()) ->method('makeApiCall') - ->with('/cases/' . $caseId . '/guarantee', 'PUT', ['guaranteeDisposition' => Gateway::GUARANTEE_CANCELED]) - ->willReturn(['disposition' => Gateway::GUARANTEE_CANCELED]); + ->with( + '/cases/' . $caseId . '/guarantee', + 'PUT', + ['guaranteeDisposition' => Gateway::GUARANTEE_CANCELED], + $dummyStoreId + ) + ->willReturn( + ['disposition' => Gateway::GUARANTEE_CANCELED] + ); $result = $this->gateway->cancelGuarantee($caseId); self::assertEquals(Gateway::GUARANTEE_CANCELED, $result); @@ -310,10 +372,17 @@ public function testCancelGuarantee() public function testCancelGuaranteeWithUnexpectedDisposition() { $caseId = 123; + $dummyStoreId = 1; + $this->withCaseEntity($caseId, $dummyStoreId); $this->apiClient->expects(self::once()) ->method('makeApiCall') - ->with('/cases/' . $caseId . '/guarantee', 'PUT', ['guaranteeDisposition' => Gateway::GUARANTEE_CANCELED]) + ->with( + '/cases/' . $caseId . '/guarantee', + 'PUT', + ['guaranteeDisposition' => Gateway::GUARANTEE_CANCELED], + $dummyStoreId + ) ->willReturn(['disposition' => Gateway::GUARANTEE_DECLINED]); $result = $this->gateway->cancelGuarantee($caseId); @@ -331,4 +400,46 @@ public function supportedGuaranteeDispositionsProvider() 'UNREQUESTED' => ['UNREQUESTED'], ]; } + + /** + * Specifies order entity mock execution. + * + * @param int $orderId + * @param int $storeId + * @return void + */ + private function withOrderEntity(int $orderId, int $storeId): void + { + $orderEntity = $this->getMockBuilder(OrderInterface::class) + ->disableOriginalConstructor() + ->getMock(); + $orderEntity->method('getStoreId') + ->willReturn($storeId); + $this->orderRepository->method('get') + ->with($orderId) + ->willReturn($orderEntity); + } + + /** + * Specifies case entity mock execution. + * + * @param int $caseId + * @param int $storeId + * @return void + */ + private function withCaseEntity(int $caseId, int $storeId): void + { + $orderId = 1; + + $caseEntity = $this->getMockBuilder(CaseInterface::class) + ->disableOriginalConstructor() + ->getMock(); + $caseEntity->method('getOrderId') + ->willReturn($orderId); + $this->caseRepository->method('getByCaseId') + ->with($caseId) + ->willReturn($caseEntity); + + $this->withOrderEntity($orderId, $storeId); + } } diff --git a/app/code/Magento/Signifyd/Test/Unit/Observer/PlaceOrderTest.php b/app/code/Magento/Signifyd/Test/Unit/Observer/PlaceOrderTest.php index e2870953ec2..4e7edddf7b9 100644 --- a/app/code/Magento/Signifyd/Test/Unit/Observer/PlaceOrderTest.php +++ b/app/code/Magento/Signifyd/Test/Unit/Observer/PlaceOrderTest.php @@ -97,7 +97,10 @@ protected function setUp() */ public function testExecuteWithDisabledModule() { - $this->withActiveSignifydIntegration(false); + $orderId = 1; + $storeId = 2; + $this->withActiveSignifydIntegration(false, $storeId); + $this->withOrderEntity($orderId, $storeId); $this->creationService->expects(self::never()) ->method('createForOrder'); @@ -113,7 +116,7 @@ public function testExecuteWithDisabledModule() public function testExecuteWithoutOrder() { $this->withActiveSignifydIntegration(true); - $this->withOrderEntity(null); + $this->withOrderEntity(null, null); $this->creationService->expects(self::never()) ->method('createForOrder'); @@ -129,8 +132,9 @@ public function testExecuteWithoutOrder() public function testExecuteWithOfflinePayment() { $orderId = 1; - $this->withActiveSignifydIntegration(true); - $this->withOrderEntity($orderId); + $storeId = 2; + $this->withActiveSignifydIntegration(true, $storeId); + $this->withOrderEntity($orderId, $storeId); $this->withAvailablePaymentMethod(false); $this->creationService->expects(self::never()) @@ -147,10 +151,11 @@ public function testExecuteWithOfflinePayment() public function testExecuteWithFailedCaseCreation() { $orderId = 1; + $storeId = 2; $exceptionMessage = __('Case with the same order id already exists.'); - $this->withActiveSignifydIntegration(true); - $this->withOrderEntity($orderId); + $this->withActiveSignifydIntegration(true, $storeId); + $this->withOrderEntity($orderId, $storeId); $this->withAvailablePaymentMethod(true); $this->creationService->method('createForOrder') @@ -172,9 +177,10 @@ public function testExecuteWithFailedCaseCreation() public function testExecute() { $orderId = 1; + $storeId = 2; - $this->withActiveSignifydIntegration(true); - $this->withOrderEntity($orderId); + $this->withActiveSignifydIntegration(true, $storeId); + $this->withOrderEntity($orderId, $storeId); $this->withAvailablePaymentMethod(true); $this->creationService @@ -190,10 +196,11 @@ public function testExecute() /** * Specifies order entity mock execution. * - * @param int $orderId + * @param int|null $orderId + * @param int|null $storeId * @return void */ - private function withOrderEntity($orderId) + private function withOrderEntity($orderId, $storeId): void { $this->orderEntity = $this->getMockBuilder(OrderInterface::class) ->disableOriginalConstructor() @@ -201,6 +208,8 @@ private function withOrderEntity($orderId) $this->orderEntity->method('getEntityId') ->willReturn($orderId); + $this->orderEntity->method('getStoreId') + ->willReturn($storeId); $this->observer->method('getEvent') ->willReturn($this->event); @@ -214,11 +223,13 @@ private function withOrderEntity($orderId) * Specifies config mock execution. * * @param bool $isActive + * @param int|null $storeId * @return void */ - private function withActiveSignifydIntegration($isActive) + private function withActiveSignifydIntegration(bool $isActive, $storeId = null): void { $this->config->method('isActive') + ->with($storeId) ->willReturn($isActive); } diff --git a/app/code/Magento/Swatches/Controller/Adminhtml/Product/Attribute/Plugin/Save.php b/app/code/Magento/Swatches/Controller/Adminhtml/Product/Attribute/Plugin/Save.php index dfda76372df..383c97a166d 100644 --- a/app/code/Magento/Swatches/Controller/Adminhtml/Product/Attribute/Plugin/Save.php +++ b/app/code/Magento/Swatches/Controller/Adminhtml/Product/Attribute/Plugin/Save.php @@ -11,7 +11,7 @@ use Magento\Swatches\Model\Swatch; /** - * Class Save + * Plugin for product attribute save controller. */ class Save { @@ -24,7 +24,17 @@ class Save public function beforeDispatch(Attribute\Save $subject, RequestInterface $request) { $data = $request->getPostValue(); + if (isset($data['frontend_input'])) { + //Data is serialized to overcome issues caused by max_input_vars value if it's modification is unavailable. + //See subject controller code and comments for more info. + if (isset($data['serialized_swatch_values']) + && in_array($data['frontend_input'], ['swatch_visual', 'swatch_text']) + ) { + $data['serialized_options'] = $data['serialized_swatch_values']; + unset($data['serialized_swatch_values']); + } + switch ($data['frontend_input']) { case 'swatch_visual': $data[Swatch::SWATCH_INPUT_TYPE_KEY] = Swatch::SWATCH_INPUT_TYPE_VISUAL; diff --git a/app/code/Magento/Swatches/view/adminhtml/web/js/product-attributes.js b/app/code/Magento/Swatches/view/adminhtml/web/js/product-attributes.js index 1187e6bc4fd..01411523108 100644 --- a/app/code/Magento/Swatches/view/adminhtml/web/js/product-attributes.js +++ b/app/code/Magento/Swatches/view/adminhtml/web/js/product-attributes.js @@ -414,6 +414,8 @@ define([ }; $(function () { + var editForm = $('#edit_form'); + $('#frontend_input').bind('change', function () { swatchProductAttributes.bindAttributeInputType(); }); @@ -427,6 +429,34 @@ define([ $('.attribute-popup .collapse, [data-role="advanced_fieldset-content"]') .collapsable() .collapse('hide'); + + editForm.on('submit', function () { + var activePanel, + swatchValues = [], + swatchVisualPanel = $('#swatch-visual-options-panel'), + swatchTextPanel = $('#swatch-text-options-panel'); + + activePanel = swatchTextPanel.is(':visible') ? swatchTextPanel : swatchVisualPanel; + + activePanel + .find('table input') + .each(function () { + swatchValues.push(this.name + '=' + $(this).val()); + }); + + $('') + .attr({ + type: 'hidden', + name: 'serialized_swatch_values' + }) + .val(JSON.stringify(swatchValues)) + .prependTo(editForm); + + [swatchVisualPanel, swatchTextPanel].forEach(function (el) { + $(el).find('table') + .replaceWith($('
').text($.mage.__('Sending swatch values as package.'))); + }); + }); }); window.saveAttributeInNewSet = swatchProductAttributes.saveAttributeInNewSet; diff --git a/app/code/Magento/TaxGraphQl/etc/schema.graphqls b/app/code/Magento/TaxGraphQl/etc/schema.graphqls index b39673f5431..2b819834784 100644 --- a/app/code/Magento/TaxGraphQl/etc/schema.graphqls +++ b/app/code/Magento/TaxGraphQl/etc/schema.graphqls @@ -1,18 +1,6 @@ # Copyright © Magento, Inc. All rights reserved. # See COPYING.txt for license details. -input ProductFilterInput { - tax_class_id: FilterTypeInput -} - -interface ProductInterface { - tax_class_id: Int -} - -input ProductSortInput { - tax_class_id: SortEnum -} - enum PriceAdjustmentCodesEnum { TAX } diff --git a/app/code/Magento/Ui/view/base/web/js/modal/modal.js b/app/code/Magento/Ui/view/base/web/js/modal/modal.js index af2b31f7eab..6c9b4b89bec 100644 --- a/app/code/Magento/Ui/view/base/web/js/modal/modal.js +++ b/app/code/Magento/Ui/view/base/web/js/modal/modal.js @@ -336,11 +336,12 @@ define([ * Set z-index and margin for modal and overlay. */ _setActive: function () { - var zIndex = this.modal.zIndex(); + var zIndex = this.modal.zIndex(), + baseIndex = zIndex + this._getVisibleCount(); + this.overlay.zIndex(++baseIndex); this.prevOverlayIndex = this.overlay.zIndex(); - this.modal.zIndex(zIndex + this._getVisibleCount()); - this.overlay.zIndex(zIndex + (this._getVisibleCount() - 1)); + this.modal.zIndex(this.overlay.zIndex() + 1); if (this._getVisibleSlideCount()) { this.modal.css('marginLeft', this.options.modalLeftMargin * this._getVisibleSlideCount()); @@ -354,7 +355,14 @@ define([ this.modal.removeAttr('style'); if (this.overlay) { - this.overlay.zIndex(this.prevOverlayIndex); + // In cases when one modal is closed but there is another modal open (e.g. admin notifications) + // to avoid collisions between overlay and modal zIndexes + // overlay zIndex is set to be less than modal one + if (this._getVisibleCount() === 1) { + this.overlay.zIndex(this.prevOverlayIndex - 1); + } else { + this.overlay.zIndex(this.prevOverlayIndex); + } } }, diff --git a/app/code/Magento/UrlRewrite/Block/Catalog/Product/Edit.php b/app/code/Magento/UrlRewrite/Block/Catalog/Product/Edit.php index fa06a47bb56..ca3756a356a 100644 --- a/app/code/Magento/UrlRewrite/Block/Catalog/Product/Edit.php +++ b/app/code/Magento/UrlRewrite/Block/Catalog/Product/Edit.php @@ -55,7 +55,7 @@ protected function _prepareLayoutFeatures() } if ($this->_getProduct()->getId()) { - $this->_addProductLinkBlock($this->_getProduct()); + $this->_addProductLinkBlock(); } if ($this->_getCategory()->getId()) { diff --git a/app/code/Magento/Webapi/Model/Rest/Swagger/Generator.php b/app/code/Magento/Webapi/Model/Rest/Swagger/Generator.php index 61c56fb691a..855f455120d 100644 --- a/app/code/Magento/Webapi/Model/Rest/Swagger/Generator.php +++ b/app/code/Magento/Webapi/Model/Rest/Swagger/Generator.php @@ -39,7 +39,7 @@ class Generator extends AbstractSchemaGenerator const UNAUTHORIZED_DESCRIPTION = '401 Unauthorized'; /** Array signifier */ - const ARRAY_SIGNIFIER = '[]'; + const ARRAY_SIGNIFIER = '[0]'; /** * Swagger factory instance. diff --git a/app/code/Magento/Wishlist/view/frontend/templates/item/list.phtml b/app/code/Magento/Wishlist/view/frontend/templates/item/list.phtml index c2172afab94..ef02768c46a 100644 --- a/app/code/Magento/Wishlist/view/frontend/templates/item/list.phtml +++ b/app/code/Magento/Wishlist/view/frontend/templates/item/list.phtml @@ -19,7 +19,7 @@ $columns = $block->getColumns();
  • - setItem($item); echo $column->toHtml($item);?> + setItem($item)->toHtml();?>
  • diff --git a/app/design/adminhtml/Magento/backend/web/app/setup/styles/less/lib/forms/_checkbox-radio.less b/app/design/adminhtml/Magento/backend/web/app/setup/styles/less/lib/forms/_checkbox-radio.less index 44b6a1568ef..82a6f7b2f19 100644 --- a/app/design/adminhtml/Magento/backend/web/app/setup/styles/less/lib/forms/_checkbox-radio.less +++ b/app/design/adminhtml/Magento/backend/web/app/setup/styles/less/lib/forms/_checkbox-radio.less @@ -87,7 +87,7 @@ .form-el-checkbox { &:checked { + .form-label { - &::before { + &:before { content: @checkbox-icon__content; font-family: @icons__font-family; } diff --git a/app/design/adminhtml/Magento/backend/web/app/updater/styles/less/components/_data-grid.less b/app/design/adminhtml/Magento/backend/web/app/updater/styles/less/components/_data-grid.less index 2e8f06c0912..313efe6cdbc 100644 --- a/app/design/adminhtml/Magento/backend/web/app/updater/styles/less/components/_data-grid.less +++ b/app/design/adminhtml/Magento/backend/web/app/updater/styles/less/components/_data-grid.less @@ -22,8 +22,8 @@ vertical-align: middle; width: @component-indicator__size; - &::before, - &::after { + &:before, + &:after { background: @color-white; display: block; opacity: 0; @@ -32,7 +32,7 @@ visibility: hidden; } - &::before { + &:before { border: 1px solid @color-gray68; border-radius: 1px; box-shadow: 0 0 2px rgba(0,0,0,.4); @@ -43,7 +43,7 @@ padding: 4px 5px; } - &::after { + &:after { border-color: darken(@color-gray68, 8); border-style: solid; border-width: 1px 0 0 1px; @@ -56,8 +56,8 @@ } &:hover { - &::before, - &::after { + &:before, + &:after { opacity: 1; transition: opacity .2s linear; visibility: visible; diff --git a/app/design/adminhtml/Magento/backend/web/css/source/components/_file-uploader.less b/app/design/adminhtml/Magento/backend/web/css/source/components/_file-uploader.less index 959b37de5a5..79e3975a41b 100644 --- a/app/design/adminhtml/Magento/backend/web/css/source/components/_file-uploader.less +++ b/app/design/adminhtml/Magento/backend/web/css/source/components/_file-uploader.less @@ -66,6 +66,7 @@ opacity: 0; overflow: hidden; position: absolute; + visibility: hidden; width: 0; &:focus { @@ -241,7 +242,7 @@ } } -// Placeholder for multiple uploader +// Placeholder for multiple uploader .file-uploader-placeholder { &.placeholder-document { .lib-icon-font( diff --git a/app/design/frontend/Magento/blank/Magento_Catalog/web/css/source/module/_toolbar.less b/app/design/frontend/Magento/blank/Magento_Catalog/web/css/source/module/_toolbar.less index a0bd5e695a6..aceccb06d47 100644 --- a/app/design/frontend/Magento/blank/Magento_Catalog/web/css/source/module/_toolbar.less +++ b/app/design/frontend/Magento/blank/Magento_Catalog/web/css/source/module/_toolbar.less @@ -188,11 +188,10 @@ .lib-icon-font-symbol(@icon-list); } - .limiter { - float: right; - - .products.wrapper ~ .toolbar & { + .toolbar { + .products.wrapper ~ & .limiter { display: block; + float: right; } } } diff --git a/app/design/frontend/Magento/blank/Magento_Checkout/web/css/source/module/checkout/_authentication.less b/app/design/frontend/Magento/blank/Magento_Checkout/web/css/source/module/checkout/_authentication.less index e1e23a9ffbb..f583c5dae0c 100644 --- a/app/design/frontend/Magento/blank/Magento_Checkout/web/css/source/module/checkout/_authentication.less +++ b/app/design/frontend/Magento/blank/Magento_Checkout/web/css/source/module/checkout/_authentication.less @@ -94,7 +94,7 @@ padding-top: @indent__xl; position: relative; - &::before { + &:before { .lib-css(height, @block-auth__or-label__size); .lib-css(line-height, @block-auth__or-label__size - 2px); .lib-css(margin, -(@block-auth__or-label__size/2 + 1px) 0 0 -(@block-auth__or-label__size / 2)); @@ -212,7 +212,7 @@ margin: 0; padding: @indent__s 0 0 @indent__xl; - &::before { + &:before { left: 0; top: 50%; } diff --git a/app/design/frontend/Magento/blank/web/css/source/_extends.less b/app/design/frontend/Magento/blank/web/css/source/_extends.less index a36934111eb..c177f91e9e7 100644 --- a/app/design/frontend/Magento/blank/web/css/source/_extends.less +++ b/app/design/frontend/Magento/blank/web/css/source/_extends.less @@ -1402,7 +1402,7 @@ &.active { > .title { .lib-icon-font-symbol( - @_icon-font-content: @icon-prev, + @_icon-font-content: @icon-up, @_icon-font-position: after ); } diff --git a/app/design/frontend/Magento/blank/web/css/source/_sections.less b/app/design/frontend/Magento/blank/web/css/source/_sections.less index d5d6e5a3d10..f0a3518c92a 100644 --- a/app/design/frontend/Magento/blank/web/css/source/_sections.less +++ b/app/design/frontend/Magento/blank/web/css/source/_sections.less @@ -20,9 +20,7 @@ .media-width(@extremum, @break) when (@extremum = 'min') and (@break = @screen__m) { .product.data.items { - .lib-data-tabs( - @_tab-content-border-top-status: true - ); + .lib-data-tabs(); } } diff --git a/app/design/frontend/Magento/blank/web/css/source/_variables.less b/app/design/frontend/Magento/blank/web/css/source/_variables.less index 5d946dd8a82..256bc796dba 100644 --- a/app/design/frontend/Magento/blank/web/css/source/_variables.less +++ b/app/design/frontend/Magento/blank/web/css/source/_variables.less @@ -15,6 +15,16 @@ @font-family-name__base: 'Open Sans'; @font-family__base: @font-family-name__base, @font-family__sans-serif; +// +// Sections variables +// _____________________________________________ + +// +// Tabs +// --------------------------------------------- +@tab-content__border-top-status: true; + + // // Sidebar // --------------------------------------------- diff --git a/app/design/frontend/Magento/luma/Magento_Catalog/web/css/source/_module.less b/app/design/frontend/Magento/luma/Magento_Catalog/web/css/source/_module.less index 6f7da202c16..228c6947c93 100644 --- a/app/design/frontend/Magento/luma/Magento_Catalog/web/css/source/_module.less +++ b/app/design/frontend/Magento/luma/Magento_Catalog/web/css/source/_module.less @@ -336,6 +336,11 @@ .lib-css(margin-top, @indent__xs); } } + .field { + .note.mage-error { + color: @error__color; + } + } } .product-options-bottom .price-box, @@ -789,7 +794,7 @@ clear: both; max-width: 100%; overflow-x: auto; - position: relative; // Needed for Safari(iOS) to properly render "overflow-x" rule. + position: relative; // Needed for Safari(iOS) to properly render 'overflow-x' rule. .table-comparison > tbody > tr { > th, diff --git a/app/design/frontend/Magento/luma/Magento_Catalog/web/css/source/module/_listings.less b/app/design/frontend/Magento/luma/Magento_Catalog/web/css/source/module/_listings.less index 01b8c7be93a..d0382d34d39 100644 --- a/app/design/frontend/Magento/luma/Magento_Catalog/web/css/source/module/_listings.less +++ b/app/design/frontend/Magento/luma/Magento_Catalog/web/css/source/module/_listings.less @@ -167,11 +167,11 @@ .column.main { .product { &-items { - margin-left: -@indent__base; + margin-left: 0; } &-item { - padding-left: @indent__base; + padding-left: 0; } } } @@ -277,8 +277,8 @@ } .sidebar & { .product-item-photo { - top: 9px; left: 9px; + top: 9px; } } } @@ -310,8 +310,8 @@ .actions-primary + .actions-secondary { display: table-cell; padding-left: 10px; - width: 50%; vertical-align: middle; + width: 50%; > .action { margin-right: 10px; @@ -443,7 +443,7 @@ .product-item { margin-left: calc(~'(100% - 4 * 24.439%) / 3'); - padding: 0; + padding: 5px; width: 24.439%; &:nth-child(4n + 1) { diff --git a/app/design/frontend/Magento/luma/Magento_Catalog/web/css/source/module/_toolbar.less b/app/design/frontend/Magento/luma/Magento_Catalog/web/css/source/module/_toolbar.less index 580abf264ca..0997b973912 100644 --- a/app/design/frontend/Magento/luma/Magento_Catalog/web/css/source/module/_toolbar.less +++ b/app/design/frontend/Magento/luma/Magento_Catalog/web/css/source/module/_toolbar.less @@ -233,10 +233,10 @@ } } - .limiter { - float: right; - .products.wrapper ~ .toolbar & { + .toolbar { + .products.wrapper ~ & .limiter { display: block; + float: right; } } } diff --git a/app/design/frontend/Magento/luma/Magento_Checkout/web/css/source/module/checkout/_payments.less b/app/design/frontend/Magento/luma/Magento_Checkout/web/css/source/module/checkout/_payments.less index c7fa7a62fd6..dd9db0e7153 100644 --- a/app/design/frontend/Magento/luma/Magento_Checkout/web/css/source/module/checkout/_payments.less +++ b/app/design/frontend/Magento/luma/Magento_Checkout/web/css/source/module/checkout/_payments.less @@ -63,6 +63,10 @@ } } } + + #po_number { + margin-bottom: 20px; + } } .payment-method-title { diff --git a/bin/UpgradeScripts/pre_composer_update_2.3.php b/bin/UpgradeScripts/pre_composer_update_2.3.php deleted file mode 100644 index 1658c8a4e66..00000000000 --- a/bin/UpgradeScripts/pre_composer_update_2.3.php +++ /dev/null @@ -1,251 +0,0 @@ -#!/usr/bin/php -" [--composer=] - [--edition=] [--version=] [--repo=] - [--help] - -Required: - --root="" - Path to the Magento installation root directory - -Optional: - --composer="" - Path to the composer executable - - Default: The composer found in PATH - - --edition= - The Magento edition to upgrade to. Open Source = 'community', Commerce = 'enterprise' - - Default: The edition currently required in composer.json - - --version= - The Magento version to upgrade to - - Default: The value for the "version" field in composer.json - - --repo= - The Magento composer repository to pull new packages from - - Default: The Magento repository configured in composer.json - - --help - Display this message -SYNOPSIS -); - - -$opts = getopt('', [ - 'root:', - 'composer:', - 'edition:', - 'version:', - 'repo:', - 'help' -]); - -if (isset($opts['help'])) { - echo SYNOPSIS . PHP_EOL; - exit(0); -} - -try { - if (empty($opts['root'])) { - throw new BadMethodCallException("Magento root must be given with '--root'" . PHP_EOL . PHP_EOL . SYNOPSIS); - } - - $rootDir = $opts['root']; - if (!is_dir($rootDir)) { - throw new InvalidArgumentException("Magento root directory '$rootDir' does not exist"); - } - - $cmd = (!empty($opts['composer']) ? $opts['composer'] : 'composer') . " --working-dir='$rootDir'"; - $jsonData = json_decode(file_get_contents("$rootDir/composer.json"), true); - - $version = !empty($opts['version']) ? $opts['version'] : $jsonData['version']; - if (empty($version)) { - throw new InvalidArgumentException('Value not found for "version" field in composer.json'); - } - - if (!empty($opts['edition'])) { - $edition = $opts['edition']; - } - else { - $editionRegex = '|^magento/product\-(?[a-z]+)\-edition$|'; - - foreach (array_keys($jsonData["require"]) as $requiredPackage) { - if (preg_match($editionRegex, $requiredPackage, $matches)) { - $edition = $matches['edition']; - break; - } - } - if (empty($edition)) { - throw new InvalidArgumentException('No valid Magento edition found in composer.json requirements'); - } - } - - echo "Backing up $rootDir/composer.json" . PHP_EOL; - copy("$rootDir/composer.json", "$rootDir/composer.json.bak"); - - echo "Updating Magento product requirement to magento/product-$edition-edition=$version" . PHP_EOL; - if ($edition == "enterprise") { - execVerbose("$cmd remove --verbose magento/product-community-edition --no-update"); - } - execVerbose("$cmd require --verbose magento/product-$edition-edition=$version --no-update"); - - echo 'Updating "require-dev" section of composer.json' . PHP_EOL; - execVerbose("$cmd require --dev --verbose " . - "phpunit/phpunit:~6.2.0 " . - "friendsofphp/php-cs-fixer:~2.10.1 " . - "lusitanian/oauth:~0.8.10 " . - "pdepend/pdepend:2.5.2 " . - "sebastian/phpcpd:~3.0.0 " . - "squizlabs/php_codesniffer:3.2.2 --no-update"); - - execVerbose("$cmd remove --dev --verbose sjparkinson/static-review fabpot/php-cs-fixer --no-update"); - - echo 'Adding "Zend\\\\Mvc\\\\Controller\\\\": "setup/src/Zend/Mvc/Controller/" to "autoload":"psr-4"' . PHP_EOL; - $jsonData = json_decode(file_get_contents("$rootDir/composer.json"), true); - $jsonData["autoload"]["psr-4"]["Zend\\Mvc\\Controller\\"] = "setup/src/Zend/Mvc/Controller/"; - - $jsonData["version"] = $version; - file_put_contents("$rootDir/composer.json", json_encode($jsonData, JSON_UNESCAPED_SLASHES|JSON_PRETTY_PRINT)); - - if (file_exists("$rootDir/update")) { - echo "Replacing Magento/Updater" . PHP_EOL; - - $mageUrls = []; - if (isset($opts['repo'])) { - $mageUrls[] = $opts['repo']; - } - else { - $composerUrls = array_map(function ($r) { return $r["url"]; }, - array_filter($jsonData['repositories']), function ($r) { return $r["type"] == "composer"; }); - $mageUrls = array_filter($composerUrls, function($u) { return strpos($u, ".mage") !== false; }); - - if (count($mageUrls) == 0) { - throw new InvalidArgumentException('No Magento composer repository urls found in composer.json'); - } - } - - echo "Backing up $rootDir/update" . PHP_EOL; - rename("$rootDir/update", "$rootDir/update.bak"); - $newPackage = "magento/project-$edition-edition=$version"; - foreach ($mageUrls as $repoUrl) { - try { - deleteFilepath("$rootDir/temp_update"); - execVerbose("$cmd create-project --repository=$repoUrl $newPackage $rootDir/temp_update --no-install"); - rename("$rootDir/temp_update/update", "$rootDir/update"); - echo "Upgraded Magento/Updater from magento/project-$edition-edition $version on $repoUrl" . PHP_EOL; - unset($exception); - break; - } - catch (Exception $e) { - echo "Failed to find Magento package on $repoUrl" . PHP_EOL; - $exception = $e; - } - } - deleteFilepath("$rootDir/temp_update"); - - if (isset($exception)) { - throw $exception; - } - } -} catch (Exception $e) { - if ($e->getPrevious()) { - $message = (string)$e->getPrevious(); - } else { - $message = $e->getMessage(); - } - - try { - error_log($message . PHP_EOL . PHP_EOL . "Error encountered; resetting backups" . PHP_EOL); - if (file_exists("$rootDir/update.bak")) { - deleteFilepath("$rootDir/update_temp"); - deleteFilepath("$rootDir/update"); - rename("$rootDir/update.bak", "$rootDir/update"); - } - - if (file_exists("$rootDir/composer.json.bak")) { - deleteFilepath("$rootDir/composer.json"); - rename("$rootDir/composer.json.bak", "$rootDir/composer.json"); - } - } - catch (Exception $e) { - error_log($e->getMessage() . PHP_EOL); - } - - exit($e->getCode() == 0 ? 1 : $e->getCode()); -} - -/** - * Execute a command with automatic escaping of arguments - * - * @param string $command - * @return array - * @throws Exception - */ -function execVerbose($command) -{ - $args = func_get_args(); - $args = array_map('escapeshellarg', $args); - $args[0] = $command; - $command = call_user_func_array('sprintf', $args); - echo $command . PHP_EOL; - exec($command . " 2>&1", $output, $exitCode); - $outputString = join(PHP_EOL, $output); - if (0 !== $exitCode) { - throw new Exception($outputString, $exitCode); - } - echo $outputString . PHP_EOL; - return $output; -} - -/** - * Deletes a file or a directory and all its contents - * - * @param string $path - * @throws Exception - */ -function deleteFilepath($path) { - if (!file_exists($path)) { - return; - } - if (is_dir($path)) { - $files = array_diff(scandir($path), array('..', '.')); - foreach ($files as $file) { - deleteFilepath("$path/$file"); - } - rmdir($path); - } - else { - unlink($path); - } - if (file_exists($path)) { - throw new Exception("Failed to delete $path"); - } -} diff --git a/dev/tests/acceptance/tests/functional/Magento/FunctionalTest/Analytics/Data/UserData.xml b/dev/tests/acceptance/tests/functional/Magento/FunctionalTest/Analytics/Data/UserData.xml index 65ddfa7bb95..c15e6004ed6 100644 --- a/dev/tests/acceptance/tests/functional/Magento/FunctionalTest/Analytics/Data/UserData.xml +++ b/dev/tests/acceptance/tests/functional/Magento/FunctionalTest/Analytics/Data/UserData.xml @@ -18,5 +18,16 @@ en_US true 123123q + + + restrictedWebUser + restricted + webUser + restrictedWebUser@example.com + 123123q + 123123q + en_US + true + 123123q diff --git a/dev/tests/acceptance/tests/functional/Magento/FunctionalTest/Bundle/Data/BundleLinkData.xml b/dev/tests/acceptance/tests/functional/Magento/FunctionalTest/Bundle/Data/BundleLinkData.xml new file mode 100644 index 00000000000..65add76a12a --- /dev/null +++ b/dev/tests/acceptance/tests/functional/Magento/FunctionalTest/Bundle/Data/BundleLinkData.xml @@ -0,0 +1,21 @@ + + + + + + + + + 1 + 1 + 1.11 + 1 + 1 + + diff --git a/dev/tests/acceptance/tests/functional/Magento/FunctionalTest/Bundle/Data/BundleOptionData.xml b/dev/tests/acceptance/tests/functional/Magento/FunctionalTest/Bundle/Data/BundleOptionData.xml new file mode 100644 index 00000000000..02f70ec15ca --- /dev/null +++ b/dev/tests/acceptance/tests/functional/Magento/FunctionalTest/Bundle/Data/BundleOptionData.xml @@ -0,0 +1,21 @@ + + + + + + bundle-option-dropdown + true + dropdown + 1 + + + + + + diff --git a/dev/tests/acceptance/tests/functional/Magento/FunctionalTest/Bundle/Data/CustomAttributeData.xml b/dev/tests/acceptance/tests/functional/Magento/FunctionalTest/Bundle/Data/CustomAttributeData.xml new file mode 100644 index 00000000000..c7f150e7ad6 --- /dev/null +++ b/dev/tests/acceptance/tests/functional/Magento/FunctionalTest/Bundle/Data/CustomAttributeData.xml @@ -0,0 +1,22 @@ + + + + + price_type + 0 + + + price_type + 1 + + + price_view + 1 + + diff --git a/dev/tests/acceptance/tests/functional/Magento/FunctionalTest/Bundle/Data/ProductData.xml b/dev/tests/acceptance/tests/functional/Magento/FunctionalTest/Bundle/Data/ProductData.xml index a8598ded6ec..69741ccd502 100644 --- a/dev/tests/acceptance/tests/functional/Magento/FunctionalTest/Bundle/Data/ProductData.xml +++ b/dev/tests/acceptance/tests/functional/Magento/FunctionalTest/Bundle/Data/ProductData.xml @@ -19,4 +19,18 @@ 1 bundleproduct + + Api Bundle Product + api-bundle-product + bundle + 4 + 4 + 1 + api-bundle-product + EavStockItem + ApiProductDescription + ApiProductShortDescription + CustomAttributeDynamicPrice + CustomAttributePriceView + diff --git a/dev/tests/acceptance/tests/functional/Magento/FunctionalTest/Bundle/Metadata/bundle_link-meta.xml b/dev/tests/acceptance/tests/functional/Magento/FunctionalTest/Bundle/Metadata/bundle_link-meta.xml new file mode 100644 index 00000000000..be881a7e98d --- /dev/null +++ b/dev/tests/acceptance/tests/functional/Magento/FunctionalTest/Bundle/Metadata/bundle_link-meta.xml @@ -0,0 +1,24 @@ + + + + + + application/json + + string + integer + integer + integer + boolean + number + integer + integer + + + diff --git a/dev/tests/acceptance/tests/functional/Magento/FunctionalTest/Bundle/Metadata/bundle_option-meta.xml b/dev/tests/acceptance/tests/functional/Magento/FunctionalTest/Bundle/Metadata/bundle_option-meta.xml new file mode 100644 index 00000000000..991c01ec4c6 --- /dev/null +++ b/dev/tests/acceptance/tests/functional/Magento/FunctionalTest/Bundle/Metadata/bundle_option-meta.xml @@ -0,0 +1,21 @@ + + + + + + application/json + + string + boolean + string + integer + string + + + diff --git a/dev/tests/acceptance/tests/functional/Magento/FunctionalTest/Bundle/Metadata/bundle_options-meta.xml b/dev/tests/acceptance/tests/functional/Magento/FunctionalTest/Bundle/Metadata/bundle_options-meta.xml new file mode 100644 index 00000000000..a81d5dda6a4 --- /dev/null +++ b/dev/tests/acceptance/tests/functional/Magento/FunctionalTest/Bundle/Metadata/bundle_options-meta.xml @@ -0,0 +1,14 @@ + + + + + + application/json + + diff --git a/dev/tests/acceptance/tests/functional/Magento/FunctionalTest/Catalog/ActionGroup/AddProductToCartActionGroup.xml b/dev/tests/acceptance/tests/functional/Magento/FunctionalTest/Catalog/ActionGroup/AddProductToCartActionGroup.xml index 6caa4fef770..9380c3052a5 100644 --- a/dev/tests/acceptance/tests/functional/Magento/FunctionalTest/Catalog/ActionGroup/AddProductToCartActionGroup.xml +++ b/dev/tests/acceptance/tests/functional/Magento/FunctionalTest/Catalog/ActionGroup/AddProductToCartActionGroup.xml @@ -14,5 +14,6 @@ + diff --git a/dev/tests/acceptance/tests/functional/Magento/FunctionalTest/Catalog/ActionGroup/AdminCategoryActionGroup.xml b/dev/tests/acceptance/tests/functional/Magento/FunctionalTest/Catalog/ActionGroup/AdminCategoryActionGroup.xml index 96e40e348a6..e8e5d0730fd 100644 --- a/dev/tests/acceptance/tests/functional/Magento/FunctionalTest/Catalog/ActionGroup/AdminCategoryActionGroup.xml +++ b/dev/tests/acceptance/tests/functional/Magento/FunctionalTest/Catalog/ActionGroup/AdminCategoryActionGroup.xml @@ -57,7 +57,7 @@ - + @@ -81,7 +81,7 @@ - + @@ -118,6 +118,68 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -128,6 +190,26 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/dev/tests/acceptance/tests/functional/Magento/FunctionalTest/Catalog/ActionGroup/AdminProductActionGroup.xml b/dev/tests/acceptance/tests/functional/Magento/FunctionalTest/Catalog/ActionGroup/AdminProductActionGroup.xml index c4843260f93..3ef8b961a81 100644 --- a/dev/tests/acceptance/tests/functional/Magento/FunctionalTest/Catalog/ActionGroup/AdminProductActionGroup.xml +++ b/dev/tests/acceptance/tests/functional/Magento/FunctionalTest/Catalog/ActionGroup/AdminProductActionGroup.xml @@ -77,14 +77,14 @@ - + - - + + @@ -139,6 +139,40 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/dev/tests/acceptance/tests/functional/Magento/FunctionalTest/Catalog/ActionGroup/CustomOptionsActionGroup.xml b/dev/tests/acceptance/tests/functional/Magento/FunctionalTest/Catalog/ActionGroup/CustomOptionsActionGroup.xml new file mode 100644 index 00000000000..0409c3f1950 --- /dev/null +++ b/dev/tests/acceptance/tests/functional/Magento/FunctionalTest/Catalog/ActionGroup/CustomOptionsActionGroup.xml @@ -0,0 +1,46 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/dev/tests/acceptance/tests/functional/Magento/FunctionalTest/Catalog/ActionGroup/StorefrontProductPageActionGroup.xml b/dev/tests/acceptance/tests/functional/Magento/FunctionalTest/Catalog/ActionGroup/StorefrontProductPageActionGroup.xml index 0644cdf3cc0..4923b1d969b 100644 --- a/dev/tests/acceptance/tests/functional/Magento/FunctionalTest/Catalog/ActionGroup/StorefrontProductPageActionGroup.xml +++ b/dev/tests/acceptance/tests/functional/Magento/FunctionalTest/Catalog/ActionGroup/StorefrontProductPageActionGroup.xml @@ -21,4 +21,21 @@ + + + + + + + + + + + + + + + + + diff --git a/dev/tests/acceptance/tests/functional/Magento/FunctionalTest/Catalog/Data/ProductData.xml b/dev/tests/acceptance/tests/functional/Magento/FunctionalTest/Catalog/Data/ProductData.xml index 34110d04176..3c6e7d3ddc3 100644 --- a/dev/tests/acceptance/tests/functional/Magento/FunctionalTest/Catalog/Data/ProductData.xml +++ b/dev/tests/acceptance/tests/functional/Magento/FunctionalTest/Catalog/Data/ProductData.xml @@ -154,12 +154,13 @@ EavStockItem CustomAttributeProductUrlKey - + Image1 1.00 Upload File Yes magento-logo.png + magento-logo MagentoLogo @@ -281,4 +282,41 @@ EavStockItem ApiProductNewsFromDate + + Double Quote" + doubleQuote + simple + 4 + 10.00 + 4 + 1 + 1000 + 1 + EavStockItem + + + + + + + + + + + + api-simple-product + simple + 4 + 4 + Api Simple Product + 1.00 + + + api-simple-product + simple + 4 + 4 + Api Simple Product + 100.00 + diff --git a/dev/tests/acceptance/tests/functional/Magento/FunctionalTest/Catalog/Data/ProductOptionData.xml b/dev/tests/acceptance/tests/functional/Magento/FunctionalTest/Catalog/Data/ProductOptionData.xml index f2f6e1a6f55..2abc557d44d 100644 --- a/dev/tests/acceptance/tests/functional/Magento/FunctionalTest/Catalog/Data/ProductOptionData.xml +++ b/dev/tests/acceptance/tests/functional/Magento/FunctionalTest/Catalog/Data/ProductOptionData.xml @@ -18,6 +18,16 @@ fixed 0 + + + OptionField2 + field + true + 1 + 20 + fixed + 0 + OptionArea diff --git a/dev/tests/acceptance/tests/functional/Magento/FunctionalTest/Catalog/Metadata/product-meta.xml b/dev/tests/acceptance/tests/functional/Magento/FunctionalTest/Catalog/Metadata/product-meta.xml index edbf1133d4f..b04b4bb98c8 100644 --- a/dev/tests/acceptance/tests/functional/Magento/FunctionalTest/Catalog/Metadata/product-meta.xml +++ b/dev/tests/acceptance/tests/functional/Magento/FunctionalTest/Catalog/Metadata/product-meta.xml @@ -60,7 +60,10 @@ boolean - + + application/json + + application/json @@ -115,7 +118,43 @@ boolean - + + application/json + + + application/json + + + + application/json + + string + string + integer + number + integer + integer + string + string + string + integer + product_extension_attribute + + product_link + + + custom_attribute_array + + + product_option + + + + + + application/json + + application/json diff --git a/dev/tests/acceptance/tests/functional/Magento/FunctionalTest/Catalog/Metadata/product_link-meta.xml b/dev/tests/acceptance/tests/functional/Magento/FunctionalTest/Catalog/Metadata/product_link-meta.xml index a6d418dc675..899dc3a7f4a 100644 --- a/dev/tests/acceptance/tests/functional/Magento/FunctionalTest/Catalog/Metadata/product_link-meta.xml +++ b/dev/tests/acceptance/tests/functional/Magento/FunctionalTest/Catalog/Metadata/product_link-meta.xml @@ -14,9 +14,9 @@ string string integer - - product_link_extension_attribute - + + integer + string @@ -24,8 +24,8 @@ string string integer - - product_link_extension_attribute - + + integer + diff --git a/dev/tests/acceptance/tests/functional/Magento/FunctionalTest/Catalog/Metadata/product_links-meta.xml b/dev/tests/acceptance/tests/functional/Magento/FunctionalTest/Catalog/Metadata/product_links-meta.xml new file mode 100644 index 00000000000..34e8d0fca68 --- /dev/null +++ b/dev/tests/acceptance/tests/functional/Magento/FunctionalTest/Catalog/Metadata/product_links-meta.xml @@ -0,0 +1,21 @@ + + + + + + application/json + + product_link + + + + application/json + product_link + + diff --git a/dev/tests/acceptance/tests/functional/Magento/FunctionalTest/Catalog/Section/AdminCategoryBasicFieldSection.xml b/dev/tests/acceptance/tests/functional/Magento/FunctionalTest/Catalog/Section/AdminCategoryBasicFieldSection.xml index 26f56f6176f..7de2390e534 100644 --- a/dev/tests/acceptance/tests/functional/Magento/FunctionalTest/Catalog/Section/AdminCategoryBasicFieldSection.xml +++ b/dev/tests/acceptance/tests/functional/Magento/FunctionalTest/Catalog/Section/AdminCategoryBasicFieldSection.xml @@ -11,9 +11,12 @@
    + + +
    diff --git a/dev/tests/acceptance/tests/functional/Magento/FunctionalTest/Catalog/Section/AdminCategoryMainActionsSection.xml b/dev/tests/acceptance/tests/functional/Magento/FunctionalTest/Catalog/Section/AdminCategoryMainActionsSection.xml index 3c806fbbea6..e726e5bfb7c 100644 --- a/dev/tests/acceptance/tests/functional/Magento/FunctionalTest/Catalog/Section/AdminCategoryMainActionsSection.xml +++ b/dev/tests/acceptance/tests/functional/Magento/FunctionalTest/Catalog/Section/AdminCategoryMainActionsSection.xml @@ -11,5 +11,8 @@
    + + +
    diff --git a/dev/tests/acceptance/tests/functional/Magento/FunctionalTest/Catalog/Section/AdminCategorySEOSection.xml b/dev/tests/acceptance/tests/functional/Magento/FunctionalTest/Catalog/Section/AdminCategorySEOSection.xml index 679cd2d7bee..0e01660d5fc 100644 --- a/dev/tests/acceptance/tests/functional/Magento/FunctionalTest/Catalog/Section/AdminCategorySEOSection.xml +++ b/dev/tests/acceptance/tests/functional/Magento/FunctionalTest/Catalog/Section/AdminCategorySEOSection.xml @@ -11,6 +11,8 @@
    + + diff --git a/dev/tests/acceptance/tests/functional/Magento/FunctionalTest/Catalog/Section/AdminCategorySidebarTreeSection.xml b/dev/tests/acceptance/tests/functional/Magento/FunctionalTest/Catalog/Section/AdminCategorySidebarTreeSection.xml index e914c80c3e6..5e080bbb7fd 100644 --- a/dev/tests/acceptance/tests/functional/Magento/FunctionalTest/Catalog/Section/AdminCategorySidebarTreeSection.xml +++ b/dev/tests/acceptance/tests/functional/Magento/FunctionalTest/Catalog/Section/AdminCategorySidebarTreeSection.xml @@ -14,5 +14,7 @@ + +
    diff --git a/dev/tests/acceptance/tests/functional/Magento/FunctionalTest/Catalog/Section/AdminProductAttributeSetSection.xml b/dev/tests/acceptance/tests/functional/Magento/FunctionalTest/Catalog/Section/AdminProductAttributeSetSection.xml index 19e59d1cd22..011b2fce3a7 100644 --- a/dev/tests/acceptance/tests/functional/Magento/FunctionalTest/Catalog/Section/AdminProductAttributeSetSection.xml +++ b/dev/tests/acceptance/tests/functional/Magento/FunctionalTest/Catalog/Section/AdminProductAttributeSetSection.xml @@ -17,4 +17,8 @@
    +
    + + +
    diff --git a/dev/tests/acceptance/tests/functional/Magento/FunctionalTest/Catalog/Section/AdminProductCategoryCreationSection.xml b/dev/tests/acceptance/tests/functional/Magento/FunctionalTest/Catalog/Section/AdminProductCategoryCreationSection.xml new file mode 100644 index 00000000000..a4566115099 --- /dev/null +++ b/dev/tests/acceptance/tests/functional/Magento/FunctionalTest/Catalog/Section/AdminProductCategoryCreationSection.xml @@ -0,0 +1,22 @@ + + + + +
    + + + + + + + + + +
    +
    diff --git a/dev/tests/acceptance/tests/functional/Magento/FunctionalTest/Catalog/Section/AdminProductContentSection.xml b/dev/tests/acceptance/tests/functional/Magento/FunctionalTest/Catalog/Section/AdminProductContentSection.xml new file mode 100644 index 00000000000..12a00ae8b37 --- /dev/null +++ b/dev/tests/acceptance/tests/functional/Magento/FunctionalTest/Catalog/Section/AdminProductContentSection.xml @@ -0,0 +1,16 @@ + + + + +
    + + + +
    +
    diff --git a/dev/tests/acceptance/tests/functional/Magento/FunctionalTest/Catalog/Section/AdminProductCustomizableOptionsSection.xml b/dev/tests/acceptance/tests/functional/Magento/FunctionalTest/Catalog/Section/AdminProductCustomizableOptionsSection.xml index 703ba081e31..cb80dade856 100644 --- a/dev/tests/acceptance/tests/functional/Magento/FunctionalTest/Catalog/Section/AdminProductCustomizableOptionsSection.xml +++ b/dev/tests/acceptance/tests/functional/Magento/FunctionalTest/Catalog/Section/AdminProductCustomizableOptionsSection.xml @@ -15,6 +15,12 @@ + + + + + + @@ -23,5 +29,14 @@ + + + + + + + + + \ No newline at end of file diff --git a/dev/tests/acceptance/tests/functional/Magento/FunctionalTest/Catalog/Section/AdminProductImagesSection.xml b/dev/tests/acceptance/tests/functional/Magento/FunctionalTest/Catalog/Section/AdminProductImagesSection.xml index a9749ac6779..b31bcaf04e4 100644 --- a/dev/tests/acceptance/tests/functional/Magento/FunctionalTest/Catalog/Section/AdminProductImagesSection.xml +++ b/dev/tests/acceptance/tests/functional/Magento/FunctionalTest/Catalog/Section/AdminProductImagesSection.xml @@ -14,5 +14,6 @@ + \ No newline at end of file diff --git a/dev/tests/acceptance/tests/functional/Magento/FunctionalTest/Catalog/Section/StorefrontCategoryProductSection.xml b/dev/tests/acceptance/tests/functional/Magento/FunctionalTest/Catalog/Section/StorefrontCategoryProductSection.xml index e736378ca8c..e8df8650e8d 100644 --- a/dev/tests/acceptance/tests/functional/Magento/FunctionalTest/Catalog/Section/StorefrontCategoryProductSection.xml +++ b/dev/tests/acceptance/tests/functional/Magento/FunctionalTest/Catalog/Section/StorefrontCategoryProductSection.xml @@ -19,5 +19,6 @@ + diff --git a/dev/tests/acceptance/tests/functional/Magento/FunctionalTest/Catalog/Section/StorefrontProductInfoMainSection.xml b/dev/tests/acceptance/tests/functional/Magento/FunctionalTest/Catalog/Section/StorefrontProductInfoMainSection.xml index 5abc388a7d6..a981f125c5e 100644 --- a/dev/tests/acceptance/tests/functional/Magento/FunctionalTest/Catalog/Section/StorefrontProductInfoMainSection.xml +++ b/dev/tests/acceptance/tests/functional/Magento/FunctionalTest/Catalog/Section/StorefrontProductInfoMainSection.xml @@ -18,12 +18,20 @@ + + + + + + + + diff --git a/dev/tests/acceptance/tests/functional/Magento/FunctionalTest/Catalog/Section/StorefrontProductPageSection.xml b/dev/tests/acceptance/tests/functional/Magento/FunctionalTest/Catalog/Section/StorefrontProductPageSection.xml index 8e902b111fc..a3703839601 100644 --- a/dev/tests/acceptance/tests/functional/Magento/FunctionalTest/Catalog/Section/StorefrontProductPageSection.xml +++ b/dev/tests/acceptance/tests/functional/Magento/FunctionalTest/Catalog/Section/StorefrontProductPageSection.xml @@ -13,5 +13,7 @@ + + diff --git a/dev/tests/acceptance/tests/functional/Magento/FunctionalTest/Catalog/Test/AdminAddImageForCategoryTest.xml b/dev/tests/acceptance/tests/functional/Magento/FunctionalTest/Catalog/Test/AdminAddImageForCategoryTest.xml index ba90296d123..04e34871c8b 100644 --- a/dev/tests/acceptance/tests/functional/Magento/FunctionalTest/Catalog/Test/AdminAddImageForCategoryTest.xml +++ b/dev/tests/acceptance/tests/functional/Magento/FunctionalTest/Catalog/Test/AdminAddImageForCategoryTest.xml @@ -43,6 +43,6 @@ - + \ No newline at end of file diff --git a/dev/tests/acceptance/tests/functional/Magento/FunctionalTest/Catalog/Test/AdminCreateCategoryFromProductPageTest.xml b/dev/tests/acceptance/tests/functional/Magento/FunctionalTest/Catalog/Test/AdminCreateCategoryFromProductPageTest.xml new file mode 100644 index 00000000000..7c81f4472e9 --- /dev/null +++ b/dev/tests/acceptance/tests/functional/Magento/FunctionalTest/Catalog/Test/AdminCreateCategoryFromProductPageTest.xml @@ -0,0 +1,55 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/dev/tests/acceptance/tests/functional/Magento/FunctionalTest/Catalog/Test/AdminUpdateCategoryStoreUrlKeyTest.xml b/dev/tests/acceptance/tests/functional/Magento/FunctionalTest/Catalog/Test/AdminUpdateCategoryStoreUrlKeyTest.xml new file mode 100644 index 00000000000..e29890befd8 --- /dev/null +++ b/dev/tests/acceptance/tests/functional/Magento/FunctionalTest/Catalog/Test/AdminUpdateCategoryStoreUrlKeyTest.xml @@ -0,0 +1,77 @@ + + + + + + + + + + <description value="SEO-friendly URL should update regardless of scope or redirect change."/> + <severity value="CRITICAL"/> + <testCaseId value="MAGETWO-92338"/> + <group value="category"/> + </annotations> + <after> + <actionGroup ref="DeleteCategory" stepKey="deleteCategory"> + <argument name="categoryEntity" value="_defaultCategory"/> + </actionGroup> + <amOnPage url="admin/admin/auth/logout/" stepKey="amOnLogoutPage"/> + </after> + + <!-- Create category, change store view to default --> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + <amOnPage url="{{AdminCategoryPage.url}}" stepKey="navigateToCategoryPage"/> + <waitForPageLoad stepKey="waitForPageLoad1"/> + <actionGroup ref="CreateCategory" stepKey="createCategory"> + <argument name="categoryEntity" value="_defaultCategory"/> + </actionGroup> + + <!--Switch to "Default Store View" scope--> + <actionGroup ref="switchCategoryStoreView" stepKey="SwitchStoreView"> + <argument name="Store" value="_defaultStore.name"/> + <argument name="CatName" value="_defaultCategory.name"/> + </actionGroup> + <!--See "Use Default Value" checkboxes--> + <seeElement selector="{{AdminCategoryBasicFieldSection.enableUseDefault}}" stepKey="seeUseDefaultEnable"/> + <seeElement selector="{{AdminCategoryBasicFieldSection.includeInMenuUseDefault}}" stepKey="seeUseDefaultMenu"/> + <seeElement selector="{{AdminCategoryBasicFieldSection.categoryNameUseDefault}}" stepKey="seeUseDefaultName"/> + <!-- Update SEO key, uncheck "Create Redirect", confirm in frontend --> + <click selector="{{AdminCategorySEOSection.SectionHeader}}" stepKey="openSeoSection"/> + <uncheckOption selector="{{AdminCategorySEOSection.UrlKeyDefaultValueCheckbox}}" stepKey="uncheckUseDefaultUrlKey"/> + <fillField selector="{{AdminCategorySEOSection.UrlKeyInput}}" userInput="{{_defaultCategory.name_lwr}}-hattest" stepKey="enterURLKey"/> + <uncheckOption selector="{{AdminCategorySEOSection.UrlKeyRedirectCheckbox}}" stepKey="uncheckRedirect1"/> + <click selector="{{AdminCategoryMainActionsSection.SaveButton}}" stepKey="saveCategoryAfterFirstSeoUpdate"/> + <seeElement selector="{{AdminCategoryMessagesSection.SuccessMessage}}" stepKey="assertSuccessMessage"/> + <amOnPage url="" stepKey="goToStorefront"/> + <waitForPageLoad stepKey="waitForFrontendLoad"/> + <click stepKey="clickCategory" selector="{{StorefrontHeaderSection.NavigationCategoryByName(_defaultCategory.name)}}"/> + <see selector="{{StorefrontCategoryMainSection.CategoryTitle}}" userInput="{{_defaultCategory.name}}" stepKey="assertCategoryOnStorefront"/> + <seeInTitle userInput="{{_defaultCategory.name}}" stepKey="seeCategoryNameInTitle"/> + <seeInCurrentUrl stepKey="verifyUrlKey" url="{{_defaultCategory.name_lwr}}-hattest.html"/> + + <!-- Update SEO key to original, uncheck "Create Redirect", confirm in frontend, delete category --> + <!--Switch to "Default Store View" scope--> + <actionGroup ref="switchCategoryStoreView" stepKey="SwitchStoreView2"> + <argument name="Store" value="_defaultStore.name"/> + <argument name="CatName" value="_defaultCategory.name"/> + </actionGroup> + <click selector="{{AdminCategorySEOSection.SectionHeader}}" stepKey="openSeoSection2"/> + <fillField selector="{{AdminCategorySEOSection.UrlKeyInput}}" userInput="{{_defaultCategory.name_lwr}}" stepKey="enterOriginalURLKey"/> + <uncheckOption selector="{{AdminCategorySEOSection.UrlKeyRedirectCheckbox}}" stepKey="uncheckRedirect2"/> + <click selector="{{AdminCategoryMainActionsSection.SaveButton}}" stepKey="saveCategoryAfterOriginalSeoKey"/> + <seeElement selector="{{AdminCategoryMessagesSection.SuccessMessage}}" stepKey="assertSuccessMessageAfterOriginalSeoKey"/> + <amOnPage url="" stepKey="goToStorefrontAfterOriginalSeoKey"/> + <waitForPageLoad stepKey="waitForFrontendLoadAfterOriginalSeoKey"/> + <click stepKey="clickCategoryAfterOriginalSeoKey" selector="{{StorefrontHeaderSection.NavigationCategoryByName(_defaultCategory.name)}}"/> + <see selector="{{StorefrontCategoryMainSection.CategoryTitle}}" userInput="{{_defaultCategory.name}}" stepKey="assertCategoryOnStorefront2"/> + <seeInTitle userInput="{{_defaultCategory.name}}" stepKey="seeCategoryNameInTitle2"/> + <seeInCurrentUrl stepKey="verifyUrlKeyAfterOriginalSeoKey" url="{{_defaultCategory.name_lwr}}.html"/> + </test> +</tests> \ No newline at end of file diff --git a/dev/tests/acceptance/tests/functional/Magento/FunctionalTest/Catalog/Test/AdvanceCatalogSearchSimpleProductTest.xml b/dev/tests/acceptance/tests/functional/Magento/FunctionalTest/Catalog/Test/AdvanceCatalogSearchSimpleProductTest.xml new file mode 100644 index 00000000000..a302fa58ec2 --- /dev/null +++ b/dev/tests/acceptance/tests/functional/Magento/FunctionalTest/Catalog/Test/AdvanceCatalogSearchSimpleProductTest.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="../../../../../../vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Test/etc/testSchema.xsd"> + <test name="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 simple product with product name"/> + <description value="Guest customer should be able to advance search simple product with product name"/> + <severity value="MAJOR"/> + <testCaseId value="MC-132"/> + <group value="Catalog"/> + </annotations> + <before> + <createData entity="ApiProductWithDescription" stepKey="product"/> + </before> + <after> + <deleteData createDataKey="product" stepKey="delete"/> + </after> + </test> + <test name="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 simple product with product sku"/> + <description value="Guest customer should be able to advance search simple product with product sku"/> + <severity value="MAJOR"/> + <testCaseId value="MC-133"/> + <group value="Catalog"/> + </annotations> + <before> + <createData entity="ApiProductWithDescription" stepKey="product"/> + </before> + <after> + <deleteData createDataKey="product" stepKey="delete"/> + </after> + </test> + <test name="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 simple product with product description"/> + <description value="Guest customer should be able to advance search simple product with product description"/> + <severity value="MAJOR"/> + <testCaseId value="MC-134"/> + <group value="Catalog"/> + </annotations> + <before> + <createData entity="ApiProductWithDescription" stepKey="product"/> + </before> + <after> + <deleteData createDataKey="product" stepKey="delete"/> + </after> + </test> + <test name="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 simple product with product short description"/> + <description value="Guest customer should be able to advance search simple product with product short description"/> + <severity value="MAJOR"/> + <testCaseId value="MC-135"/> + <group value="Catalog"/> + </annotations> + <before> + <createData entity="ApiProductWithDescription" stepKey="product"/> + </before> + <after> + <deleteData createDataKey="product" stepKey="delete"/> + </after> + </test> + <test name="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 simple product with product price"/> + <description value="Guest customer should be able to advance search simple product with product price"/> + <severity value="MAJOR"/> + <testCaseId value="MC-136"/> + <group value="Catalog"/> + </annotations> + <before> + <createData entity="ApiProductWithDescription" stepKey="product"/> + <getData entity="GetProduct" stepKey="arg1"> + <requiredEntity createDataKey="product"/> + </getData> + <getData entity="GetProduct" stepKey="arg2"> + <requiredEntity createDataKey="product"/> + </getData> + <getData entity="GetProduct" stepKey="arg3"> + <requiredEntity createDataKey="product"/> + </getData> + </before> + <after> + <deleteData createDataKey="product" stepKey="delete"/> + </after> + </test> +</tests> diff --git a/dev/tests/acceptance/tests/functional/Magento/FunctionalTest/Catalog/Test/ConfigurableOptionTextInputLengthValidationHint.xml b/dev/tests/acceptance/tests/functional/Magento/FunctionalTest/Catalog/Test/ConfigurableOptionTextInputLengthValidationHint.xml new file mode 100644 index 00000000000..45db40dd2e3 --- /dev/null +++ b/dev/tests/acceptance/tests/functional/Magento/FunctionalTest/Catalog/Test/ConfigurableOptionTextInputLengthValidationHint.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="../../../../../../vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Test/etc/testSchema.xsd"> + <test name="ConfigurableOptionTextinputLengthValidationHintTest"> + <annotations> + <features value="Product Customizable Option"/> + <stories value="Customizable text option input-length validation hint changes dynamically"/> + <title value="You should have a dynamic length validation hint when using text option has max char limit"/> + <description value="You should have a dynamic length validation hint when using text option has max char limit"/> + <severity value="MINOR"/> + <testCaseId value="MAGETWO-92229"/> + <group value="product"/> + </annotations> + <before> + <createData entity="_defaultCategory" stepKey="createPreReqCategory"/> + </before> + <after> + <amOnPage url="admin/admin/auth/logout/" stepKey="amOnLogoutPage"/> + <deleteData createDataKey="createPreReqCategory" stepKey="deletePreReqCategory"/> + </after> + + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin1"/> + <actionGroup ref="AdminCreateSimpleProductWithTextOptionCharLimit" stepKey="fillProductFieldsInAdmin"> + <argument name="category" value="$$createPreReqCategory$$"/> + <argument name="simpleProduct" value="_defaultProduct"/> + <argument name="charLimit" value="20"/> + </actionGroup> + <actionGroup ref="AssertProductInStorefrontCategoryPage" stepKey="assertProductInStorefront1"> + <argument name="category" value="$$createPreReqCategory$$"/> + <argument name="product" value="_defaultProduct"/> + </actionGroup> + <actionGroup ref="AssertProductInStorefrontProductPage" stepKey="assertProductInStorefront2"> + <argument name="product" value="_defaultProduct"/> + </actionGroup> + <actionGroup ref="testDynamicValidationHint" stepKey="testDynamicValidationHint1"> + <argument name="charLimit" value="20"/> + </actionGroup> + </test> +</tests> diff --git a/dev/tests/acceptance/tests/functional/Magento/FunctionalTest/Catalog/Test/EndToEndB2CAdminTest.xml b/dev/tests/acceptance/tests/functional/Magento/FunctionalTest/Catalog/Test/EndToEndB2CAdminTest.xml index 7ce754e8f00..0f466ccc3d9 100644 --- a/dev/tests/acceptance/tests/functional/Magento/FunctionalTest/Catalog/Test/EndToEndB2CAdminTest.xml +++ b/dev/tests/acceptance/tests/functional/Magento/FunctionalTest/Catalog/Test/EndToEndB2CAdminTest.xml @@ -39,7 +39,7 @@ <argument name="product" value="SimpleProduct"/> </actionGroup> <actionGroup ref="addProductImage" stepKey="addImageForProductSimple"> - <argument name="image" value="ImageUpload"/> + <argument name="image" value="ProductImage"/> </actionGroup> <actionGroup ref="saveProductForm" stepKey="saveSimpleProduct"/> <click selector="{{AdminProductFormActionSection.backButton}}" stepKey="clickBackToGridSimple"/> diff --git a/dev/tests/acceptance/tests/functional/Magento/FunctionalTest/Catalog/Test/SaveProductWithCustomOptionsSecondWebsiteTest.xml b/dev/tests/acceptance/tests/functional/Magento/FunctionalTest/Catalog/Test/SaveProductWithCustomOptionsSecondWebsiteTest.xml index cc33059dd10..cf7f996736e 100644 --- a/dev/tests/acceptance/tests/functional/Magento/FunctionalTest/Catalog/Test/SaveProductWithCustomOptionsSecondWebsiteTest.xml +++ b/dev/tests/acceptance/tests/functional/Magento/FunctionalTest/Catalog/Test/SaveProductWithCustomOptionsSecondWebsiteTest.xml @@ -32,6 +32,7 @@ <!--Create Store view --> <amOnPage url="{{AdminSystemStorePage.url}}" stepKey="amOnAdminSystemStorePage"/> + <waitForPageLoad stepKey="waitForAdminSystemStorePage"/> <click selector="{{AdminStoresMainActionsSection.createStoreViewButton}}" stepKey="createStoreViewButton"/> <waitForPageLoad stepKey="waitForProductPageLoad"/> <selectOption userInput="Second Store" selector="{{AdminNewStoreSection.storeGrpDropdown}}" stepKey="selectStoreGroup"/> diff --git a/dev/tests/acceptance/tests/functional/Magento/FunctionalTest/Catalog/Test/SimpleProductTwoCustomOptionsTest.xml b/dev/tests/acceptance/tests/functional/Magento/FunctionalTest/Catalog/Test/SimpleProductTwoCustomOptionsTest.xml new file mode 100644 index 00000000000..2710002d625 --- /dev/null +++ b/dev/tests/acceptance/tests/functional/Magento/FunctionalTest/Catalog/Test/SimpleProductTwoCustomOptionsTest.xml @@ -0,0 +1,89 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="../../../../../../vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Test/etc/testSchema.xsd"> + <test name="SimpleProductTwoCustomOptionsTest"> + <annotations> + <features value="Catalog"/> + <stories value="Create simple product with two custom options" /> + <title value="Admin should be able to create simple product with two custom options"/> + <description value="Admin should be able to create simple product with two custom options"/> + <severity value="AVERAGE"/> + <testCaseId value="MC-248"/> + <group value="Catalog"/> + </annotations> + <before> + <!-- log in as admin --> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + + <!--Create product--> + <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="navigateToProductIndex"/> + <waitForPageLoad stepKey="waitForProductIndexPage"/> + <actionGroup ref="goToCreateProductPage" stepKey="goToCreateSimpleProduct"> + <argument name="product" value="SimpleProduct3"/> + </actionGroup> + <actionGroup ref="fillMainProductFormNoWeight" stepKey="fillSimpleProductMain"> + <argument name="product" value="SimpleProduct3"/> + </actionGroup> + </before> + <after> + <!-- Delete the created product --> + <actionGroup ref="deleteProductUsingProductGrid" stepKey="deleteProduct"> + <argument name="product" value="SimpleProduct3"/> + </actionGroup> + <amOnPage url="{{AdminLogoutPage.url}}" stepKey="amOnLogoutPage"/> + </after> + + <!-- opens the custom option panel and clicks add options --> + <click stepKey="openCustomizableOptions" selector="{{AdminProductCustomizableOptionsSection.customezableOptions}}"/> + <waitForPageLoad stepKey="waitForCustomOptionsOpen"/> + + <!-- Create a custom option with 2 values --> + <actionGroup ref="CreateCustomRadioOptions" stepKey="createCustomOption1"> + <argument name="customOptionName" value="ProductOptionRadiobutton.title"/> + <argument name="productOption" value="ProductOptionField"/> + <argument name="productOption2" value="ProductOptionField2"/> + </actionGroup> + + <!-- Create another custom option with 2 values --> + <actionGroup ref="CreateCustomRadioOptions" stepKey="createCustomOption2"> + <argument name="customOptionName" value="ProductOptionRadiobutton.title"/> + <argument name="productOption" value="ProductOptionField"/> + <argument name="productOption2" value="ProductOptionField2"/> + </actionGroup> + + <!-- Save the product --> + <click stepKey="saveProduct" selector="{{AdminProductFormActionSection.saveButton}}"/> + <waitForPageLoad stepKey="waitForProductSaved"/> + <seeElement selector="{{AdminCategoryMessagesSection.SuccessMessage}}" stepKey="assertSuccess"/> + + <!-- navigate to the created product page --> + <amOnPage url="/{{SimpleProduct3.name}}.html" stepKey="goToCreatedProduct"/> + <waitForPageLoad stepKey="waitForProductPageLoad"/> + + <!-- Check to make sure all of the created names are there --> + <see stepKey="assertNameInFirstOption" selector="{{StorefrontProductInfoMainSection.nthCustomOption('1')}}" userInput="{{ProductOptionField.title}}"/> + <see stepKey="assertNameInSecondOption" selector="{{StorefrontProductInfoMainSection.nthCustomOption('2')}}" userInput="{{ProductOptionField.title}}"/> + <see stepKey="assertSecondNameInFirstOption" selector="{{StorefrontProductInfoMainSection.nthCustomOption('1')}}" userInput="{{ProductOptionField2.title}}"/> + <see stepKey="assertSecondNameInSecondOption" selector="{{StorefrontProductInfoMainSection.nthCustomOption('2')}}" userInput="{{ProductOptionField2.title}}"/> + + <!-- Check to see that all of the created prices are there --> + <see stepKey="assertPriceInFirstOption" selector="{{StorefrontProductInfoMainSection.nthCustomOption('1')}}" userInput="{{ProductOptionField.price}}"/> + <see stepKey="assertPriceInSecondOption" selector="{{StorefrontProductInfoMainSection.nthCustomOption('2')}}" userInput="{{ProductOptionField.price}}"/> + <see stepKey="assertSecondPriceInFirstOption" selector="{{StorefrontProductInfoMainSection.nthCustomOption('1')}}" userInput="{{ProductOptionField2.price}}"/> + <see stepKey="assertSecondPriceInSecondOption" selector="{{StorefrontProductInfoMainSection.nthCustomOption('2')}}" userInput="{{ProductOptionField2.price}}"/> + + <!-- select two of the radio buttons --> + <click stepKey="selectFirstCustomOption" selector="{{StorefrontProductInfoMainSection.nthCustomOptionInput('1','2')}}"/> + <click stepKey="selectSecondCustomOption" selector="{{StorefrontProductInfoMainSection.nthCustomOptionInput('2','1')}}"/> + + <!-- Check that the price has actually changed --> + <see stepKey="assertPriceHasChanged" selector="{{StorefrontProductInfoMainSection.productPrice}}" userInput="153.00"/> + </test> +</tests> diff --git a/dev/tests/acceptance/tests/functional/Magento/FunctionalTest/Catalog/Test/StorefrontProductNameWithDoubleQuote.xml b/dev/tests/acceptance/tests/functional/Magento/FunctionalTest/Catalog/Test/StorefrontProductNameWithDoubleQuote.xml new file mode 100644 index 00000000000..a848515aee8 --- /dev/null +++ b/dev/tests/acceptance/tests/functional/Magento/FunctionalTest/Catalog/Test/StorefrontProductNameWithDoubleQuote.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="../../../../../../vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Test/etc/testSchema.xsd"> + <test name="StorefrontProductNameWithDoubleQuote"> + <annotations> + <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"/> + </annotations> + <before> + <createData entity="_defaultCategory" stepKey="createCategory"/> + </before> + <after> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + </after> + + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + <!--Create product via admin--> + <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="goToProductIndex"/> + <waitForPageLoad stepKey="waitForProductIndexPage"/> + <actionGroup ref="goToCreateProductPage" stepKey="goToProductCreatePage"> + <argument name="product" value="SimpleProductNameWithDoubleQuote"/> + </actionGroup> + <actionGroup ref="fillMainProductForm" stepKey="fillProductForm"> + <argument name="product" value="SimpleProductNameWithDoubleQuote"/> + </actionGroup> + <searchAndMultiSelectOption selector="{{AdminProductFormSection.categoriesDropdown}}" parameterArray="[$$createCategory.name$$]" stepKey="selectCategory"/> + <actionGroup ref="addProductImage" stepKey="addImageToProduct"> + <argument name="image" value="ProductImage"/> + </actionGroup> + <actionGroup ref="saveProductForm" stepKey="saveProduct"/> + + <!--Check product in category listing--> + <amOnPage url="{{StorefrontCategoryPage.url($$createCategory.name$$)}}" stepKey="goToCategoryPage"/> + <seeElement selector="{{StorefrontCategoryProductSection.ProductImageByNameAndSrc(SimpleProductNameWithDoubleQuote.name, ProductImage.fileName)}}" stepKey="seeCorrectImageCategoryPage"/> + <see selector="{{StorefrontCategoryProductSection.ProductTitleByName(SimpleProductNameWithDoubleQuote.name)}}" userInput="{{SimpleProductNameWithDoubleQuote.name}}" stepKey="seeCorrectNameCategoryPage"/> + <see selector="{{StorefrontCategoryProductSection.ProductPriceByName(SimpleProductNameWithDoubleQuote.name)}}" userInput="${{SimpleProductNameWithDoubleQuote.price}}" stepKey="seeCorrectPriceCategoryPage"/> + <!--Open product display page--> + <click selector="{{StorefrontCategoryProductSection.ProductTitleByName(SimpleProductNameWithDoubleQuote.name)}}" stepKey="clickProductToGoProductPage"/> + <waitForPageLoad stepKey="waitForProductDisplayPageLoad"/> + <see selector="{{StorefrontProductInfoMainSection.productName}}" userInput="{{SimpleProductNameWithDoubleQuote.name}}" stepKey="seeCorrectName"/> + <see selector="{{StorefrontProductInfoMainSection.productSku}}" userInput="{{SimpleProductNameWithDoubleQuote.sku}}" stepKey="seeCorrectSku"/> + <see selector="{{StorefrontProductInfoMainSection.productPrice}}" userInput="${{SimpleProductNameWithDoubleQuote.price}}" stepKey="seeCorrectPrice"/> + <seeElement selector="{{StorefrontProductInfoMainSection.productImageSrc(ProductImage.fileName)}}" stepKey="seeCorrectImage"/> + <see selector="{{StorefrontProductInfoMainSection.stock}}" userInput="In Stock" stepKey="seeInStock"/> + <see selector="{{StorefrontNavigationSection.breadcrumbs}}" userInput="$$createCategory.name$$" stepKey="seeCorrectBreadCrumbCategory"/> + <see selector="{{StorefrontNavigationSection.breadcrumbs}}" userInput="{{SimpleProductNameWithDoubleQuote.name}}" stepKey="seeCorrectBreadCrumbProduct"/> + + <!--Remove product--> + <actionGroup ref="deleteProductUsingProductGrid" stepKey="deleteProduct"> + <argument name="product" value="SimpleProductNameWithDoubleQuote"/> + </actionGroup> + </test> +</tests> diff --git a/dev/tests/acceptance/tests/functional/Magento/FunctionalTest/CatalogSearch/Test/AdvanceCatalogSearchSimpleProductTest.xml b/dev/tests/acceptance/tests/functional/Magento/FunctionalTest/CatalogSearch/Test/AdvanceCatalogSearchSimpleProductTest.xml new file mode 100644 index 00000000000..11bc308902c --- /dev/null +++ b/dev/tests/acceptance/tests/functional/Magento/FunctionalTest/CatalogSearch/Test/AdvanceCatalogSearchSimpleProductTest.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="../../../../../../vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Test/etc/testSchema.xsd"> + <test name="AdvanceCatalogSearchSimpleProductByNameTest"> + <annotations> + <features value="CatalogSearch"/> + <group value="CatalogSearch"/> + </annotations> + <actionGroup ref="GoToStoreViewAdvancedCatalogSearchActionGroup" stepKey="GoToStoreViewAdvancedCatalogSearchActionGroup"/> + <actionGroup ref="StorefrontAdvancedCatalogSearchByProductNameActionGroup" stepKey="search"> + <argument name="name" value="$$product.name$$"/> + </actionGroup> + <actionGroup ref="StorefrontCheckAdvancedSearchResultActionGroup" stepKey="StorefrontCheckAdvancedSearchResult"/> + <see userInput="1 item" selector="{{StorefrontCatalogSearchAdvancedResultMainSection.itemFound}}" stepKey="see"/> + <see userInput="$$product.name$$" selector="{{StorefrontCatalogSearchAdvancedResultMainSection.productName}}" stepKey="seeProductName"/> + </test> + <test name="AdvanceCatalogSearchSimpleProductBySkuTest"> + <annotations> + <features value="CatalogSearch"/> + <group value="CatalogSearch"/> + </annotations> + <actionGroup ref="GoToStoreViewAdvancedCatalogSearchActionGroup" stepKey="GoToStoreViewAdvancedCatalogSearchActionGroup"/> + <actionGroup ref="StorefrontAdvancedCatalogSearchByProductSkuActionGroup" stepKey="search"> + <argument name="sku" value="$$product.sku$$"/> + </actionGroup> + <actionGroup ref="StorefrontCheckAdvancedSearchResultActionGroup" stepKey="StorefrontCheckAdvancedSearchResult"/> + <see userInput="1 item" selector="{{StorefrontCatalogSearchAdvancedResultMainSection.itemFound}}" stepKey="see"/> + <see userInput="$$product.name$$" selector="{{StorefrontCatalogSearchAdvancedResultMainSection.productName}}" stepKey="seeProductName"/> + </test> + <test name="AdvanceCatalogSearchSimpleProductByDescriptionTest"> + <annotations> + <features value="CatalogSearch"/> + <group value="CatalogSearch"/> + </annotations> + <actionGroup ref="GoToStoreViewAdvancedCatalogSearchActionGroup" stepKey="GoToStoreViewAdvancedCatalogSearchActionGroup"/> + <actionGroup ref="StorefrontAdvancedCatalogSearchByDescriptionActionGroup" stepKey="search"> + <argument name="description" value="$$product.custom_attributes[description]$$"/> + </actionGroup> + <actionGroup ref="StorefrontCheckAdvancedSearchResultActionGroup" stepKey="StorefrontCheckAdvancedSearchResult"/> + <see userInput="1 item" selector="{{StorefrontCatalogSearchAdvancedResultMainSection.itemFound}}" stepKey="see"/> + <see userInput="$$product.name$$" selector="{{StorefrontCatalogSearchAdvancedResultMainSection.productName}}" stepKey="seeProductName"/> + </test> + <test name="AdvanceCatalogSearchSimpleProductByShortDescriptionTest"> + <annotations> + <features value="CatalogSearch"/> + <group value="CatalogSearch"/> + </annotations> + <actionGroup ref="GoToStoreViewAdvancedCatalogSearchActionGroup" stepKey="GoToStoreViewAdvancedCatalogSearchActionGroup"/> + <actionGroup ref="StorefrontAdvancedCatalogSearchByShortDescriptionActionGroup" stepKey="search"> + <argument name="shortDescription" value="$$product.custom_attributes[short_description]$$"/> + </actionGroup> + <actionGroup ref="StorefrontCheckAdvancedSearchResultActionGroup" stepKey="StorefrontCheckAdvancedSearchResult"/> + <see userInput="1 item" selector="{{StorefrontCatalogSearchAdvancedResultMainSection.itemFound}}" stepKey="see"/> + <see userInput="$$product.name$$" selector="{{StorefrontCatalogSearchAdvancedResultMainSection.productName}}" stepKey="seeProductName"/> + </test> + <test name="AdvanceCatalogSearchSimpleProductByPriceTest"> + <annotations> + <features value="CatalogSearch"/> + <group value="CatalogSearch"/> + </annotations> + <actionGroup ref="GoToStoreViewAdvancedCatalogSearchActionGroup" stepKey="GoToStoreViewAdvancedCatalogSearchActionGroup"/> + <actionGroup ref="StorefrontAdvancedCatalogSearchByProductNameAndPriceActionGroup" stepKey="search"> + <argument name="name" value="$$arg1.name$$"/> + <argument name="priceFrom" value="$$arg2.price$$"/> + <argument name="priceTo" value="$$arg3.price$$"/> + </actionGroup> + <actionGroup ref="StorefrontCheckAdvancedSearchResultActionGroup" stepKey="StorefrontCheckAdvancedSearchResult"/> + <see userInput="1 item" selector="{{StorefrontCatalogSearchAdvancedResultMainSection.itemFound}}" stepKey="see"/> + <see userInput="$$arg1.name$$" selector="{{StorefrontCatalogSearchAdvancedResultMainSection.productName}}" stepKey="seeProductName"/> + </test> +</tests> diff --git a/dev/tests/acceptance/tests/functional/Magento/FunctionalTest/CatalogSearch/Test/AdvanceCatalogSearchSimpleProductsTest.xml b/dev/tests/acceptance/tests/functional/Magento/FunctionalTest/CatalogSearch/Test/AdvanceCatalogSearchSimpleProductsTest.xml deleted file mode 100644 index f52a8681084..00000000000 --- a/dev/tests/acceptance/tests/functional/Magento/FunctionalTest/CatalogSearch/Test/AdvanceCatalogSearchSimpleProductsTest.xml +++ /dev/null @@ -1,140 +0,0 @@ -<?xml version="1.0" encoding="UTF-8"?> -<!-- - /** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ ---> - -<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xsi:noNamespaceSchemaLocation="../../../../../../vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Test/etc/testSchema.xsd"> - <test name="AdvanceCatalogSearchSimpleProductByNameTest"> - <annotations> - <features value="CatalogSearch"/> - <stories value="Advanced Catalog Product Search for all product types"/> - <title value="Guest customer should be able to advance search simple product with product name"/> - <description value="Guest customer should be able to advance search simple product with product name"/> - <severity value="MAJOR"/> - <testCaseId value="MC-132"/> - <group value="CatalogSearch"/> - </annotations> - <before> - <createData entity="ApiProductWithDescription" stepKey="createProductOne"/> - </before> - <after> - <deleteData createDataKey="createProductOne" stepKey="deleteProductOne"/> - </after> - - <actionGroup ref="GoToStoreViewAdvancedCatalogSearchActionGroup" stepKey="GoToStoreViewAdvancedCatalogSearchActionGroup"/> - <actionGroup ref="StorefrontAdvancedCatalogSearchByProductNameActionGroup" stepKey="search"> - <argument name="name" value="$$createProductOne.name$$"/> - </actionGroup> - <actionGroup ref="StorefrontCheckAdvancedSearchResultActionGroup" stepKey="StorefrontCheckAdvancedSearchResult"/> - <see userInput="1 item" selector="{{StorefrontCatalogSearchAdvancedResultMainSection.itemFound}}" stepKey="see"/> - </test> - <test name="AdvanceCatalogSearchSimpleProductBySkuTest"> - <annotations> - <features value="CatalogSearch"/> - <stories value="Advanced Catalog Product Search for all product types"/> - <title value="Guest customer should be able to advance search simple product with product sku"/> - <description value="Guest customer should be able to advance search simple product with product sku"/> - <severity value="MAJOR"/> - <testCaseId value="MC-133"/> - <group value="CatalogSearch"/> - </annotations> - <before> - <createData entity="ApiProductWithDescription" stepKey="createProductOne"/> - </before> - <after> - <deleteData createDataKey="createProductOne" stepKey="deleteProductOne"/> - </after> - - <actionGroup ref="GoToStoreViewAdvancedCatalogSearchActionGroup" stepKey="GoToStoreViewAdvancedCatalogSearchActionGroup"/> - <actionGroup ref="StorefrontAdvancedCatalogSearchByProductSkuActionGroup" stepKey="search"> - <argument name="sku" value="$$createProductOne.sku$$"/> - </actionGroup> - <actionGroup ref="StorefrontCheckAdvancedSearchResultActionGroup" stepKey="StorefrontCheckAdvancedSearchResult"/> - <see userInput="1 item" selector="{{StorefrontCatalogSearchAdvancedResultMainSection.itemFound}}" stepKey="see"/> - <see userInput="$$createProductOne.name$$" selector="{{StorefrontCatalogSearchAdvancedResultMainSection.productName}}" stepKey="seeProductName"/> - </test> - - <test name="AdvanceCatalogSearchSimpleProductByDescriptionTest"> - <annotations> - <features value="CatalogSearch"/> - <stories value="Advanced Catalog Product Search for all product types"/> - <title value="Guest customer should be able to advance search simple product with product description"/> - <description value="Guest customer should be able to advance search simple product with product description"/> - <severity value="MAJOR"/> - <testCaseId value="MC-134"/> - <group value="CatalogSearch"/> - </annotations> - <before> - <createData entity="ApiProductWithDescription" stepKey="createProductOne"/> - </before> - <after> - <deleteData createDataKey="createProductOne" stepKey="deleteProductOne"/> - </after> - - <actionGroup ref="GoToStoreViewAdvancedCatalogSearchActionGroup" stepKey="GoToStoreViewAdvancedCatalogSearchActionGroup"/> - <actionGroup ref="StorefrontAdvancedCatalogSearchByDescriptionActionGroup" stepKey="search"> - <argument name="description" value="$$createProductOne.custom_attributes[description]$$"/> - </actionGroup> - <actionGroup ref="StorefrontCheckAdvancedSearchResultActionGroup" stepKey="StorefrontCheckAdvancedSearchResult"/> - <see userInput="1 item" selector="{{StorefrontCatalogSearchAdvancedResultMainSection.itemFound}}" stepKey="see"/> - <see userInput="$$createProductOne.name$$" selector="{{StorefrontCatalogSearchAdvancedResultMainSection.productName}}" stepKey="seeProductName"/> - </test> - - <test name="AdvanceCatalogSearchSimpleProductByShortDescriptionTest"> - <annotations> - <features value="CatalogSearch"/> - <stories value="Advanced Catalog Product Search for all product types"/> - <title value="Guest customer should be able to advance search simple product with product short description"/> - <description value="Guest customer should be able to advance search simple product with product short description"/> - <severity value="MAJOR"/> - <testCaseId value="MC-135"/> - <group value="CatalogSearch"/> - </annotations> - <before> - <createData entity="ApiProductWithDescription" stepKey="createProductOne"/> - </before> - <after> - <deleteData createDataKey="createProductOne" stepKey="deleteProductOne"/> - </after> - - <actionGroup ref="GoToStoreViewAdvancedCatalogSearchActionGroup" stepKey="GoToStoreViewAdvancedCatalogSearchActionGroup"/> - <actionGroup ref="StorefrontAdvancedCatalogSearchByShortDescriptionActionGroup" stepKey="search"> - <argument name="shortDescription" value="$$createProductOne.custom_attributes[short_description]$$"/> - </actionGroup> - <actionGroup ref="StorefrontCheckAdvancedSearchResultActionGroup" stepKey="StorefrontCheckAdvancedSearchResult"/> - <see userInput="1 item" selector="{{StorefrontCatalogSearchAdvancedResultMainSection.itemFound}}" stepKey="see"/> - <see userInput="$$createProductOne.name$$" selector="{{StorefrontCatalogSearchAdvancedResultMainSection.productName}}" stepKey="seeProductName"/> - </test> - - <test name="AdvanceCatalogSearchSimpleProductByPriceTest"> - <annotations> - <features value="CatalogSearch"/> - <stories value="Advanced Catalog Product Search for all product types"/> - <title value="Guest customer should be able to advance search simple product with product price"/> - <description value="Guest customer should be able to advance search simple product with product price"/> - <severity value="MAJOR"/> - <testCaseId value="MC-136"/> - <group value="CatalogSearch"/> - </annotations> - <before> - <createData entity="ApiProductWithDescription" stepKey="createProductOne"/> - </before> - <after> - <deleteData createDataKey="createProductOne" stepKey="deleteProductOne"/> - </after> - - <actionGroup ref="GoToStoreViewAdvancedCatalogSearchActionGroup" stepKey="GoToStoreViewAdvancedCatalogSearchActionGroup"/> - <actionGroup ref="StorefrontAdvancedCatalogSearchByProductNameAndPriceActionGroup" stepKey="search"> - <argument name="name" value="$$createProductOne.name$$"/> - <argument name="priceFrom" value="$$createProductOne.price$$"/> - <argument name="priceTo" value="$$createProductOne.price$$"/> - </actionGroup> - <actionGroup ref="StorefrontCheckAdvancedSearchResultActionGroup" stepKey="StorefrontCheckAdvancedSearchResult"/> - <see userInput="1 item" selector="{{StorefrontCatalogSearchAdvancedResultMainSection.itemFound}}" stepKey="see"/> - <see userInput="$$createProductOne.name$$" selector="{{StorefrontCatalogSearchAdvancedResultMainSection.productName}}" stepKey="seeProductName"/> - </test> -</tests> diff --git a/dev/tests/acceptance/tests/functional/Magento/FunctionalTest/CatalogSearch/Test/AdvanceCatalogSearchVirtualProductsTest.xml b/dev/tests/acceptance/tests/functional/Magento/FunctionalTest/CatalogSearch/Test/AdvanceCatalogSearchVirtualProductsTest.xml deleted file mode 100644 index 80b1b294fec..00000000000 --- a/dev/tests/acceptance/tests/functional/Magento/FunctionalTest/CatalogSearch/Test/AdvanceCatalogSearchVirtualProductsTest.xml +++ /dev/null @@ -1,141 +0,0 @@ -<?xml version="1.0" encoding="UTF-8"?> -<!-- - /** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ ---> - -<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xsi:noNamespaceSchemaLocation="../../../../../../vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Test/etc/testSchema.xsd"> - <test name="AdvanceCatalogSearchVirtualProductByNameTest"> - <annotations> - <features value="CatalogSearch"/> - <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="CatalogSearch"/> - </annotations> - <before> - <createData entity="ApiVirtualProductWithDescription" stepKey="createProductOne"/> - </before> - <after> - <deleteData createDataKey="createProductOne" stepKey="deleteProductOne"/> - </after> - - <actionGroup ref="GoToStoreViewAdvancedCatalogSearchActionGroup" stepKey="GoToStoreViewAdvancedCatalogSearchActionGroup"/> - <actionGroup ref="StorefrontAdvancedCatalogSearchByProductNameActionGroup" stepKey="search"> - <argument name="name" value="$$createProductOne.name$$"/> - </actionGroup> - <actionGroup ref="StorefrontCheckAdvancedSearchResultActionGroup" stepKey="StorefrontCheckAdvancedSearchResult"/> - <see userInput="1 item" selector="{{StorefrontCatalogSearchAdvancedResultMainSection.itemFound}}" stepKey="see"/> - <see userInput="$$createProductOne.name$$" selector="{{StorefrontCatalogSearchAdvancedResultMainSection.productName}}" stepKey="seeProductName"/> - </test> - <test name="AdvanceCatalogSearchVirtualProductBySkuTest"> - <annotations> - <features value="CatalogSearch"/> - <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="CatalogSearch"/> - </annotations> - <before> - <createData entity="ApiVirtualProductWithDescription" stepKey="createProductOne"/> - </before> - <after> - <deleteData createDataKey="createProductOne" stepKey="deleteProductOne"/> - </after> - - <actionGroup ref="GoToStoreViewAdvancedCatalogSearchActionGroup" stepKey="GoToStoreViewAdvancedCatalogSearchActionGroup"/> - <actionGroup ref="StorefrontAdvancedCatalogSearchByProductSkuActionGroup" stepKey="search"> - <argument name="sku" value="$$createProductOne.sku$$"/> - </actionGroup> - <actionGroup ref="StorefrontCheckAdvancedSearchResultActionGroup" stepKey="StorefrontCheckAdvancedSearchResult"/> - <see userInput="1 item" selector="{{StorefrontCatalogSearchAdvancedResultMainSection.itemFound}}" stepKey="see"/> - <see userInput="$$createProductOne.name$$" selector="{{StorefrontCatalogSearchAdvancedResultMainSection.productName}}" stepKey="seeProductName"/> - </test> - - <test name="AdvanceCatalogSearchVirtualProductByDescriptionTest"> - <annotations> - <features value="CatalogSearch"/> - <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="CatalogSearch"/> - </annotations> - <before> - <createData entity="ApiVirtualProductWithDescription" stepKey="createProductOne"/> - </before> - <after> - <deleteData createDataKey="createProductOne" stepKey="deleteProductOne"/> - </after> - - <actionGroup ref="GoToStoreViewAdvancedCatalogSearchActionGroup" stepKey="GoToStoreViewAdvancedCatalogSearchActionGroup"/> - <actionGroup ref="StorefrontAdvancedCatalogSearchByDescriptionActionGroup" stepKey="search"> - <argument name="description" value="$$createProductOne.custom_attributes[description]$$"/> - </actionGroup> - <actionGroup ref="StorefrontCheckAdvancedSearchResultActionGroup" stepKey="StorefrontCheckAdvancedSearchResult"/> - <see userInput="1 item" selector="{{StorefrontCatalogSearchAdvancedResultMainSection.itemFound}}" stepKey="see"/> - <see userInput="$$createProductOne.name$$" selector="{{StorefrontCatalogSearchAdvancedResultMainSection.productName}}" stepKey="seeProductName"/> - </test> - - <test name="AdvanceCatalogSearchVirtualProductByShortDescriptionTest"> - <annotations> - <features value="CatalogSearch"/> - <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="CatalogSearch"/> - </annotations> - <before> - <createData entity="ApiVirtualProductWithDescription" stepKey="createProductOne"/> - </before> - <after> - <deleteData createDataKey="createProductOne" stepKey="deleteProductOne"/> - </after> - - <actionGroup ref="GoToStoreViewAdvancedCatalogSearchActionGroup" stepKey="GoToStoreViewAdvancedCatalogSearchActionGroup"/> - <actionGroup ref="StorefrontAdvancedCatalogSearchByShortDescriptionActionGroup" stepKey="search"> - <argument name="shortDescription" value="$$createProductOne.custom_attributes[short_description]$$"/> - </actionGroup> - <actionGroup ref="StorefrontCheckAdvancedSearchResultActionGroup" stepKey="StorefrontCheckAdvancedSearchResult"/> - <see userInput="1 item" selector="{{StorefrontCatalogSearchAdvancedResultMainSection.itemFound}}" stepKey="see"/> - <see userInput="$$createProductOne.name$$" selector="{{StorefrontCatalogSearchAdvancedResultMainSection.productName}}" stepKey="seeProductName"/> - </test> - - <test name="AdvanceCatalogSearchVirtualProductByPriceTest"> - <annotations> - <features value="CatalogSearch"/> - <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="CatalogSearch"/> - </annotations> - <before> - <createData entity="ApiVirtualProductWithDescription" stepKey="createProductOne"/> - </before> - <after> - <deleteData createDataKey="createProductOne" stepKey="deleteProductOne"/> - </after> - - <actionGroup ref="GoToStoreViewAdvancedCatalogSearchActionGroup" stepKey="GoToStoreViewAdvancedCatalogSearchActionGroup"/> - <actionGroup ref="StorefrontAdvancedCatalogSearchByProductNameAndPriceActionGroup" stepKey="search"> - <argument name="name" value="$$createProductOne.name$$"/> - <argument name="priceFrom" value="$$createProductOne.price$$"/> - <argument name="priceTo" value="$$createProductOne.price$$"/> - </actionGroup> - <actionGroup ref="StorefrontCheckAdvancedSearchResultActionGroup" stepKey="StorefrontCheckAdvancedSearchResult"/> - <see userInput="1 item" selector="{{StorefrontCatalogSearchAdvancedResultMainSection.itemFound}}" stepKey="see"/> - <see userInput="$$createProductOne.name$$" selector="{{StorefrontCatalogSearchAdvancedResultMainSection.productName}}" stepKey="seeProductName"/> - </test> -</tests> diff --git a/dev/tests/acceptance/tests/functional/Magento/FunctionalTest/Checkout/Page/GuestCheckoutReviewAndPaymentsPage.xml b/dev/tests/acceptance/tests/functional/Magento/FunctionalTest/Checkout/Page/GuestCheckoutReviewAndPaymentsPage.xml new file mode 100644 index 00000000000..3fb6e99ed67 --- /dev/null +++ b/dev/tests/acceptance/tests/functional/Magento/FunctionalTest/Checkout/Page/GuestCheckoutReviewAndPaymentsPage.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="../../../../../../vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Page/etc/PageObject.xsd"> + <page name="GuestCheckoutReviewAndPaymentsPage" url="/checkout/#payment" area="storefront" module="Magento_Checkout"> + <section name="CheckoutPaymentSection"/> + </page> +</pages> diff --git a/dev/tests/acceptance/tests/functional/Magento/FunctionalTest/Checkout/Section/CheckoutCartSummarySection.xml b/dev/tests/acceptance/tests/functional/Magento/FunctionalTest/Checkout/Section/CheckoutCartSummarySection.xml index 32a57a14cd9..24807094054 100644 --- a/dev/tests/acceptance/tests/functional/Magento/FunctionalTest/Checkout/Section/CheckoutCartSummarySection.xml +++ b/dev/tests/acceptance/tests/functional/Magento/FunctionalTest/Checkout/Section/CheckoutCartSummarySection.xml @@ -15,5 +15,9 @@ <element name="total" type="text" selector="//*[@id='cart-totals']//tr[@class='grand totals']//td//span[@class='price']"/> <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="postcode" type="input" selector="input[name='postcode']"/> + <element name="stateProvince" type="select" selector="select[name='region_id']"/> + <element name="country" type="select" selector="select[name='country_id']"/> </section> </sections> diff --git a/dev/tests/acceptance/tests/functional/Magento/FunctionalTest/Checkout/Section/CheckoutShippingGuestInfoSection.xml b/dev/tests/acceptance/tests/functional/Magento/FunctionalTest/Checkout/Section/CheckoutShippingGuestInfoSection.xml index f20ae4dac07..f2cca89be6c 100644 --- a/dev/tests/acceptance/tests/functional/Magento/FunctionalTest/Checkout/Section/CheckoutShippingGuestInfoSection.xml +++ b/dev/tests/acceptance/tests/functional/Magento/FunctionalTest/Checkout/Section/CheckoutShippingGuestInfoSection.xml @@ -17,5 +17,7 @@ <element name="region" type="select" selector="select[name=region_id]"/> <element name="postcode" type="input" selector="input[name=postcode]"/> <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"/> </section> </sections> diff --git a/dev/tests/acceptance/tests/functional/Magento/FunctionalTest/Cms/Data/CmsPageData.xml b/dev/tests/acceptance/tests/functional/Magento/FunctionalTest/Cms/Data/CmsPageData.xml index 857ac2a2f1b..25e55ea756a 100644 --- a/dev/tests/acceptance/tests/functional/Magento/FunctionalTest/Cms/Data/CmsPageData.xml +++ b/dev/tests/acceptance/tests/functional/Magento/FunctionalTest/Cms/Data/CmsPageData.xml @@ -37,6 +37,7 @@ <data key="price">1.00</data> <data key="file_type">Upload File</data> <data key="shareable">Yes</data> + <data key="file">magento.jpg</data> <data key="value">magento.jpg</data> <data key="fileName">magento</data> <data key="extension">jpg</data> diff --git a/dev/tests/acceptance/tests/functional/Magento/FunctionalTest/ConfigurableProduct/Data/ConfigurableProductData.xml b/dev/tests/acceptance/tests/functional/Magento/FunctionalTest/ConfigurableProduct/Data/ConfigurableProductData.xml index 0a2429fa8f9..70e758a5140 100644 --- a/dev/tests/acceptance/tests/functional/Magento/FunctionalTest/ConfigurableProduct/Data/ConfigurableProductData.xml +++ b/dev/tests/acceptance/tests/functional/Magento/FunctionalTest/ConfigurableProduct/Data/ConfigurableProductData.xml @@ -34,6 +34,19 @@ <requiredEntity type="product_extension_attribute">EavStockItem</requiredEntity> <requiredEntity type="custom_attribute_array">CustomAttributeCategoryIds</requiredEntity> </entity> + <entity name="ApiConfigurableProductWithDescription" type="product"> + <data key="sku" unique="suffix">api-configurable-product</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> + <requiredEntity type="custom_attribute_array">ApiProductDescription</requiredEntity> + <requiredEntity type="custom_attribute_array">ApiProductShortDescription</requiredEntity> + </entity> <entity name="ConfigurableProductAddChild" type="ConfigurableProductAddChild"> <var key="sku" entityKey="sku" entityType="product" /> <var key="childSku" entityKey="sku" entityType="product2"/> diff --git a/dev/tests/acceptance/tests/functional/Magento/FunctionalTest/ConfigurableProduct/Section/StorefrontProductInfoMainSection.xml b/dev/tests/acceptance/tests/functional/Magento/FunctionalTest/ConfigurableProduct/Section/StorefrontProductInfoMainSection.xml index cebb76e68a6..45bf8665513 100644 --- a/dev/tests/acceptance/tests/functional/Magento/FunctionalTest/ConfigurableProduct/Section/StorefrontProductInfoMainSection.xml +++ b/dev/tests/acceptance/tests/functional/Magento/FunctionalTest/ConfigurableProduct/Section/StorefrontProductInfoMainSection.xml @@ -14,5 +14,8 @@ <element name="productAttributeOptions1" type="select" selector="#product-options-wrapper div[tabindex='0'] option"/> <element name="productAttributeOptionsSelectButton" type="select" selector="#product-options-wrapper .super-attribute-select"/> <element name="productAttributeOptionsError" type="text" selector="//div[@class='mage-error']"/> + <!-- Parameter is the order number of the attribute on the page (1 is the newest) --> + <element name="nthAttributeOnPage" type="block" selector="tr:nth-of-type({{numElement}}) .data" parameterized="true"/> + <element name="stockIndication" type="block" selector=".stock" /> </section> </sections> diff --git a/dev/tests/acceptance/tests/functional/Magento/FunctionalTest/ConfigurableProduct/Test/AdminConfigurableProductOutOfStockTest.xml b/dev/tests/acceptance/tests/functional/Magento/FunctionalTest/ConfigurableProduct/Test/AdminConfigurableProductOutOfStockTest.xml new file mode 100644 index 00000000000..96651e303c5 --- /dev/null +++ b/dev/tests/acceptance/tests/functional/Magento/FunctionalTest/ConfigurableProduct/Test/AdminConfigurableProductOutOfStockTest.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="../../../../../../vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Test/etc/testSchema.xsd"> + <test name="AdminConfigurableProductOutOfStockTest"> + <annotations> + <features value="ConfigurableProduct"/> + <stories value="Product visibility when in stock/out of stock"/> + <title value="Configurable Product goes 'Out of Stock' if all associated Simple Products are 'Out of Stock'"/> + <description value="Configurable Product goes 'Out of Stock' if all associated Simple Products are 'Out of Stock'"/> + <testCaseId value="MC-181"/> + <group value="ConfigurableProduct"/> + </annotations> + <before> + <!-- TODO: This should be converted to an actionGroup once MQE-993 is fixed. --> + <!-- Create the category to put the product in --> + <createData entity="ApiCategory" 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> + + <!-- log in --> + <actionGroup ref="LoginAsAdmin" stepKey="login"/> + </before> + + <after> + <amOnPage url="admin/admin/auth/logout/" stepKey="logout"/> + <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"/> + </after> + + <!-- Check to make sure that the configurable product shows up as in stock --> + <amOnPage url="/{{ApiConfigurableProduct.urlKey}}2.html" stepKey="goToConfigProductPage"/> + <waitForPageLoad stepKey="waitForStoreFrontLoad"/> + <see stepKey="lookForOutOfStock" selector="{{StorefrontProductInfoMainSection.stockIndication}}" userInput="IN STOCK" /> + + <!-- Find the first simple product that we just created using the product grid and go to its page--> + <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="visitAdminProductPage"/> + <waitForPageLoad stepKey="waitForAdminProductGridLoad"/> + <conditionalClick selector="{{AdminProductGridFilterSection.clearFilters}}" dependentSelector="{{AdminProductGridFilterSection.clearFilters}}" visible="true" stepKey="clickClearFiltersInitial"/> + <actionGroup ref="filterProductGridBySku" stepKey="findCreatedProduct"> + <argument name="product" value="ApiSimpleOne"/> + </actionGroup> + <waitForPageLoad stepKey="waitForFiltersToBeApplied"/> + <click selector="{{AdminProductGridSection.firstRow}}" stepKey="clickOnProductPage"/> + <waitForPageLoad stepKey="waitForProductPageLoad"/> + + <!-- Edit the quantity of the simple first product as 0 --> + <fillField selector="{{AdminProductFormSection.productQuantity}}" userInput="0" stepKey="fillProductQuantity"/> + <click selector="{{AdminProductFormActionSection.saveButton}}" stepKey="clickSaveProduct"/> + <waitForPageLoad stepKey="waitForProductPageSaved"/> + + <!-- Check to make sure that the configurable product shows up as in stock --> + <amOnPage url="/{{ApiConfigurableProduct.urlKey}}2.html" stepKey="goToConfigProductPage2"/> + <waitForPageLoad stepKey="waitForStoreFrontLoad2"/> + <see stepKey="lookForOutOfStock2" selector="{{StorefrontProductInfoMainSection.stockIndication}}" userInput="IN STOCK"/> + + <!-- Find the second simple product that we just created using the product grid and go to its page--> + <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="visitAdminProductPage2"/> + <waitForPageLoad stepKey="waitForAdminProductGridLoad2"/> + <conditionalClick selector="{{AdminProductGridFilterSection.clearFilters}}" dependentSelector="{{AdminProductGridFilterSection.clearFilters}}" visible="true" stepKey="clickClearFiltersInitial2"/> + <actionGroup ref="filterProductGridBySku" stepKey="findCreatedProduct2"> + <argument name="product" value="ApiSimpleTwo"/> + </actionGroup> + <waitForPageLoad stepKey="waitForFiltersToBeApplied2"/> + <click selector="{{AdminProductGridSection.firstRow}}" stepKey="clickOnProductPage2"/> + <waitForPageLoad stepKey="waitForProductPageLoad2"/> + + <!-- Edit the quantity of the second simple product as 0 --> + <fillField selector="{{AdminProductFormSection.productQuantity}}" userInput="0" stepKey="fillProductQuantity2"/> + <click selector="{{AdminProductFormActionSection.saveButton}}" stepKey="clickSaveProduct2"/> + <waitForPageLoad stepKey="waitForProductPageSaved2"/> + + <!-- Check to make sure that the configurable product shows up as out of stock --> + <amOnPage url="/{{ApiConfigurableProduct.urlKey}}2.html" stepKey="goToConfigProductPage3"/> + <waitForPageLoad stepKey="waitForStoreFrontLoad3"/> + <see stepKey="lookForOutOfStock3" selector="{{StorefrontProductInfoMainSection.stockIndication}}" userInput="OUT OF STOCK"/> + </test> +</tests> diff --git a/dev/tests/acceptance/tests/functional/Magento/FunctionalTest/ConfigurableProduct/Test/AdminConfigurableProductUpdateAttributeTest.xml b/dev/tests/acceptance/tests/functional/Magento/FunctionalTest/ConfigurableProduct/Test/AdminConfigurableProductUpdateAttributeTest.xml new file mode 100644 index 00000000000..2a2c331aa15 --- /dev/null +++ b/dev/tests/acceptance/tests/functional/Magento/FunctionalTest/ConfigurableProduct/Test/AdminConfigurableProductUpdateAttributeTest.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="../../../../../../vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Test/etc/testSchema.xsd"> + <test name="AdminConfigurableProductUpdateAttributeTest"> + <annotations> + <features value="ConfigurableProduct"/> + <stories value="Edit a configurable product in admin"/> + <title value="Admin should be able to update existing attributes of a configurable product"/> + <description value="Admin should be able to update existing attributes of a configurable product"/> + <severity value="AVERAGE"/> + <testCaseId value="MC-179"/> + <group value="ConfigurableProduct"/> + </annotations> + + <before> + <!-- Create the attribute we will be modifying --> + <createData entity="productAttributeWithTwoOptions" stepKey="createModifiableProductAttribute"/> + + <!-- Create the two attributes the product will have --> + <createData entity="productAttributeOption1" stepKey="createModifiableProductAttributeOption1"> + <requiredEntity createDataKey="createModifiableProductAttribute"/> + </createData> + <createData entity="productAttributeOption2" stepKey="createModifiableProductAttributeOption2"> + <requiredEntity createDataKey="createModifiableProductAttribute"/> + </createData> + + <!-- Add the product to the default set --> + <createData entity="AddToDefaultSet" stepKey="createModifiableAddToAttributeSet"> + <requiredEntity createDataKey="createModifiableProductAttribute"/> + </createData> + + <!-- TODO: This should be converted to an actionGroup once MQE-993 is fixed. --> + <!-- Create the category the product will be a part of --> + <createData entity="ApiCategory" stepKey="createCategory"/> + <createData entity="ApiConfigurableProduct" stepKey="createConfigProduct"> + <requiredEntity createDataKey="createCategory"/> + </createData> + <createData entity="productAttributeWithTwoOptions" stepKey="createConfigProductAttribute"/> + + <!-- Create the two attributes the product will have --> + <createData entity="productAttributeOption1" stepKey="createConfigProductAttributeOption1"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </createData> + <createData entity="productAttributeOption2" stepKey="createConfigProductAttributeOption2"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </createData> + + <!-- Add the product to the default set --> + <createData entity="AddToDefaultSet" stepKey="createConfigAddToAttributeSet"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </createData> + + <!-- Get the two attributes --> + <getData entity="ProductAttributeOptionGetter" index="1" stepKey="getConfigAttributeOption1"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </getData> + <getData entity="ProductAttributeOptionGetter" index="2" stepKey="getConfigAttributeOption2"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </getData> + + <!-- Create the two children 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> + + <!-- Create the two configurable product with both children --> + <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 --> + <actionGroup ref="LoginAsAdmin" stepKey="login"/> + </before> + + <after> + <amOnPage url="admin/admin/auth/logout/" stepKey="logout"/> + + <!-- Delete everything that was created in the before block --> + <deleteData createDataKey="createCategory" stepKey="deleteCatagory" /> + <deleteData createDataKey="createConfigProduct" stepKey="deleteConfigProduct"/> + <deleteData createDataKey="createConfigChildProduct1" stepKey="deleteConfigChildProduct1"/> + <deleteData createDataKey="createConfigChildProduct2" stepKey="deleteConfigChildProduct2"/> + <deleteData createDataKey="createModifiableProductAttribute" stepKey="deleteModifiableProductAttribute"/> + <deleteData createDataKey="createConfigProductAttribute" stepKey="deleteConfigProductAttribute"/> + </after> + + <!-- Get the current option of the attribute before it was changed --> + <amOnPage url="/{{ApiConfigurableProduct.urlKey}}2.html" stepKey="goToConfigProductPage"/> + <waitForPageLoad stepKey="waitForStoreFrontProductPageLoad"/> + + <grabTextFrom stepKey="getBeforeOption" selector="{{StorefrontProductInfoMainSection.nthAttributeOnPage('1')}}"/> + + <!-- Find the product that we just created using the product grid --> + <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="visitAdminProductPage"/> + <waitForPageLoad stepKey="waitForAdminProductPageLoad"/> + <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"/> + + <!-- change the option on the first attribute --> + <selectOption stepKey="clickFirstAttribute" selector="{{ModifyAttributes.nthExistingAttribute($$createModifiableProductAttribute.default_frontend_label$$)}}" userInput="option1"/> + + <!-- Save the product --> + <click stepKey="saveProductAttribute" selector="{{AdminProductFormActionSection.saveButton}}"/> + <see stepKey="assertSuccess" selector="{{AdminProductMessagesSection.successMessage}}" userInput="You saved the product."/> + + <!-- Go back to the configurable product page and check to see if it has changed --> + <amOnPage url="/{{ApiConfigurableProduct.urlKey}}2.html" stepKey="goToConfigProductPage2"/> + <waitForPageLoad stepKey="waitForStoreFrontProductPageLoad2"/> + <grabTextFrom stepKey="getCurrentOption" selector="{{StorefrontProductInfoMainSection.nthAttributeOnPage('1')}}"/> + <assertNotEquals expected="{$getBeforeOption}" expectedType="string" actual="{$getCurrentOption}" actualType="string" stepKey="assertNotEquals"/> + + </test> +</tests> diff --git a/dev/tests/acceptance/tests/functional/Magento/FunctionalTest/Downloadable/Data/LinkData.xml b/dev/tests/acceptance/tests/functional/Magento/FunctionalTest/Downloadable/Data/LinkData.xml index 6276f340f85..1498f4b96b3 100644 --- a/dev/tests/acceptance/tests/functional/Magento/FunctionalTest/Downloadable/Data/LinkData.xml +++ b/dev/tests/acceptance/tests/functional/Magento/FunctionalTest/Downloadable/Data/LinkData.xml @@ -41,4 +41,13 @@ <data key="file_type">URL</data> <data key="file">https://static.magento.com/sites/all/themes/mag_redesign/images/magento-logo.svg</data> </entity> + <entity name="ApiDownloadableLink" type="downloadable_link"> + <data key="title" unique="suffix">Api Downloadable Link</data> + <data key="price">2.00</data> + <data key="link_type">url</data> + <data key="shareable">No</data> + <data key="number_of_downloads">1000</data> + <data key="sort_order">0</data> + <data key="link_url">https://static.magento.com/sites/all/themes/mag_redesign/images/magento-logo.svg</data> + </entity> </entities> \ No newline at end of file diff --git a/dev/tests/acceptance/tests/functional/Magento/FunctionalTest/Downloadable/Data/ProductData.xml b/dev/tests/acceptance/tests/functional/Magento/FunctionalTest/Downloadable/Data/ProductData.xml index 427f2577a8a..f71ebd481a9 100644 --- a/dev/tests/acceptance/tests/functional/Magento/FunctionalTest/Downloadable/Data/ProductData.xml +++ b/dev/tests/acceptance/tests/functional/Magento/FunctionalTest/Downloadable/Data/ProductData.xml @@ -19,4 +19,19 @@ <data key="status">1</data> <data key="urlKey" unique="suffix">downloadableproduct</data> </entity> + <entity name="ApiDownloadableProduct" type="product"> + <data key="sku" unique="suffix">api-downloadable-product</data> + <data key="type_id">downloadable</data> + <data key="attribute_set_id">4</data> + <data key="visibility">4</data> + <data key="name" unique="suffix">Api Downloadable Product</data> + <data key="price">123.00</data> + <data key="urlKey" unique="suffix">api-downloadable-product</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> + <requiredEntity type="custom_attribute_array">ApiProductShortDescription</requiredEntity> + <requiredEntity type="downloadable_link">apiDownloadableLink</requiredEntity> + </entity> </entities> diff --git a/dev/tests/acceptance/tests/functional/Magento/FunctionalTest/Downloadable/Metadata/downloadable_link-meta.xml b/dev/tests/acceptance/tests/functional/Magento/FunctionalTest/Downloadable/Metadata/downloadable_link-meta.xml new file mode 100644 index 00000000000..dc86c4e8d79 --- /dev/null +++ b/dev/tests/acceptance/tests/functional/Magento/FunctionalTest/Downloadable/Metadata/downloadable_link-meta.xml @@ -0,0 +1,31 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<operations xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="../../../../../../vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/DataGenerator/etc/dataOperation.xsd"> + <operation name="CreateDownloadableLink" dataType="downloadable_link" type="create" auth="adminOauth" url="/V1/products/{sku}/downloadable-links" method="POST"> + <contentType>application/json</contentType> + <object dataType="downloadable_link" key="link"> + <field key="title">string</field> + <field key="sort_order">integer</field> + <field key="is_shareable">integer</field> + <field key="price">number</field> + <field key="number_of_downloads">integer</field> + <field key="link_type">string</field> + <field key="link_file">string</field> + <field key="link_file_content">link_file_content</field> + <field key="file_data">string</field> + <field key="link_url">string</field> + <field key="sample_type">string</field> + <field key="sample_file">string</field> + <field key="sample_file_content">sample_file_content</field> + <field key="sample_url">string</field> + </object> + <field key="isGlobalScopeContent">boolean</field> + </operation> +</operations> diff --git a/dev/tests/acceptance/tests/functional/Magento/FunctionalTest/Downloadable/Metadata/link_file_content-meta.xml b/dev/tests/acceptance/tests/functional/Magento/FunctionalTest/Downloadable/Metadata/link_file_content-meta.xml new file mode 100644 index 00000000000..72f643e0680 --- /dev/null +++ b/dev/tests/acceptance/tests/functional/Magento/FunctionalTest/Downloadable/Metadata/link_file_content-meta.xml @@ -0,0 +1,15 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<operations xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="../../../../../../vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/DataGenerator/etc/dataOperation.xsd"> + <operation name="CreateLinkFileContent" dataType="link_file_content" type="create"> + <field key="file_data">string</field> + <field key="name">string</field> + </operation> +</operations> diff --git a/dev/tests/acceptance/tests/functional/Magento/FunctionalTest/Downloadable/Metadata/sample_file_content-meta.xml b/dev/tests/acceptance/tests/functional/Magento/FunctionalTest/Downloadable/Metadata/sample_file_content-meta.xml new file mode 100644 index 00000000000..144ce67bb25 --- /dev/null +++ b/dev/tests/acceptance/tests/functional/Magento/FunctionalTest/Downloadable/Metadata/sample_file_content-meta.xml @@ -0,0 +1,15 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<operations xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="../../../../../../vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/DataGenerator/etc/dataOperation.xsd"> + <operation name="CreateSampleFileContent" dataType="sample_file_content" type="create"> + <field key="file_data">string</field> + <field key="name">string</field> + </operation> +</operations> diff --git a/dev/tests/acceptance/tests/functional/Magento/FunctionalTest/GroupedProduct/Data/GroupedProductData.xml b/dev/tests/acceptance/tests/functional/Magento/FunctionalTest/GroupedProduct/Data/GroupedProductData.xml index 019b0d24dec..9960d698a78 100644 --- a/dev/tests/acceptance/tests/functional/Magento/FunctionalTest/GroupedProduct/Data/GroupedProductData.xml +++ b/dev/tests/acceptance/tests/functional/Magento/FunctionalTest/GroupedProduct/Data/GroupedProductData.xml @@ -17,4 +17,15 @@ <data key="urlKey" unique="suffix">groupedproduct</data> <requiredEntity type="product_extension_attribute">EavStockItem</requiredEntity> </entity> + <entity name="ApiGroupedProduct" type="product3"> + <data key="sku" unique="suffix">api-grouped-product</data> + <data key="type_id">grouped</data> + <data key="attribute_set_id">4</data> + <data key="name" unique="suffix">Api Grouped Product</data> + <data key="status">1</data> + <data key="urlKey" unique="suffix">api-grouped-product</data> + <requiredEntity type="product_extension_attribute">EavStockItem</requiredEntity> + <requiredEntity type="custom_attribute_array">ApiProductDescription</requiredEntity> + <requiredEntity type="custom_attribute_array">ApiProductShortDescription</requiredEntity> + </entity> </entities> diff --git a/dev/tests/acceptance/tests/functional/Magento/FunctionalTest/GroupedProduct/Data/ProductLinkData.xml b/dev/tests/acceptance/tests/functional/Magento/FunctionalTest/GroupedProduct/Data/ProductLinkData.xml new file mode 100644 index 00000000000..9a5df1e379a --- /dev/null +++ b/dev/tests/acceptance/tests/functional/Magento/FunctionalTest/GroupedProduct/Data/ProductLinkData.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="../../../../../../vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/DataGenerator/etc/dataProfileSchema.xsd"> + <entity name="ProductLinkSimple1" type="product_link"> + <var key="sku" entityKey="sku" entityType="product3"/> + <var key="linked_product_sku" entityKey="sku" entityType="product"/> + <data key="link_type">associated</data> + <data key="linked_product_type">simple</data> + <data key="position">1</data> + <requiredEntity type="product_link_extension_attribute">Qty1000</requiredEntity> + </entity> + <entity name="ProductLinkSimple2" type="product_link"> + <var key="sku" entityKey="sku" entityType="product3"/> + <var key="linked_product_sku" entityKey="sku" entityType="product"/> + <data key="link_type">associated</data> + <data key="linked_product_type">simple</data> + <data key="position">2</data> + <requiredEntity type="product_link_extension_attribute">Qty1000</requiredEntity> + </entity> +</entities> diff --git a/dev/tests/acceptance/tests/functional/Magento/FunctionalTest/GroupedProduct/Data/ProductLinkExtensionAttributeData.xml b/dev/tests/acceptance/tests/functional/Magento/FunctionalTest/GroupedProduct/Data/ProductLinkExtensionAttributeData.xml new file mode 100644 index 00000000000..5f5dcb3a0ef --- /dev/null +++ b/dev/tests/acceptance/tests/functional/Magento/FunctionalTest/GroupedProduct/Data/ProductLinkExtensionAttributeData.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="../../../../../../vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/DataGenerator/etc/dataProfileSchema.xsd"> + <entity name="Qty1000" type="product_link_extension_attribute"> + <data key="qty">1000</data> + </entity> +</entities> diff --git a/dev/tests/acceptance/tests/functional/Magento/FunctionalTest/GroupedProduct/Data/ProductLinksData.xml b/dev/tests/acceptance/tests/functional/Magento/FunctionalTest/GroupedProduct/Data/ProductLinksData.xml new file mode 100644 index 00000000000..523517aa700 --- /dev/null +++ b/dev/tests/acceptance/tests/functional/Magento/FunctionalTest/GroupedProduct/Data/ProductLinksData.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="../../../../../../vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/DataGenerator/etc/dataProfileSchema.xsd"> + <entity name="OneSimpleProductLink" type="product_links"> + <requiredEntity type="product_link">ProductLinkSimple1</requiredEntity> + </entity> + <entity name="OneMoreSimpleProductLink" type="product_links"> + <requiredEntity type="product_link">ProductLinkSimple2</requiredEntity> + </entity> + <entity name="TwoSimpleProductLinks" type="product_links"> + <array key="items"> + <item>ProductLinkSimple1</item> + <item>ProductLinkSimple2</item> + </array> + </entity> +</entities> diff --git a/dev/tests/acceptance/tests/functional/Magento/FunctionalTest/Persistent/Data/PersistentData.xml b/dev/tests/acceptance/tests/functional/Magento/FunctionalTest/Persistent/Data/PersistentData.xml new file mode 100644 index 00000000000..4ba2e1ae738 --- /dev/null +++ b/dev/tests/acceptance/tests/functional/Magento/FunctionalTest/Persistent/Data/PersistentData.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="../../../../../../vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/DataGenerator/etc/dataProfileSchema.xsd"> + <entity name="PersistentConfigDefault" type="persistent_config_state"> + <requiredEntity type="persistent_options_enabled">persistentDefaultState</requiredEntity> + </entity> + <entity name="persistentDefaultState" type="persistent_options_enabled"> + <data key="value">0</data> + </entity> + + <entity name="PersistentConfigEnabled" type="persistent_config_state"> + <requiredEntity type="persistent_options_enabled">persistentEnabledState</requiredEntity> + </entity> + <entity name="persistentEnabledState" type="persistent_options_enabled"> + <data key="value">1</data> + </entity> +</entities> diff --git a/dev/tests/acceptance/tests/functional/Magento/FunctionalTest/Persistent/Metadata/persistent_config-meta.xml b/dev/tests/acceptance/tests/functional/Magento/FunctionalTest/Persistent/Metadata/persistent_config-meta.xml new file mode 100644 index 00000000000..69a835aa703 --- /dev/null +++ b/dev/tests/acceptance/tests/functional/Magento/FunctionalTest/Persistent/Metadata/persistent_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="../../../../../../vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/DataGenerator/etc/dataOperation.xsd"> + <operation name="CreatePersistentConfigState" dataType="persistent_config_state" type="create" auth="adminFormKey" url="/admin/system_config/save/section/persistent/" method="POST"> + <object key="groups" dataType="persistent_config_state"> + <object key="options" dataType="persistent_config_state"> + <object key="fields" dataType="persistent_config_state"> + <object key="enabled" dataType="persistent_options_enabled"> + <field key="value">string</field> + </object> + </object> + </object> + </object> + </operation> +</operations> diff --git a/dev/tests/acceptance/tests/functional/Magento/FunctionalTest/Persistent/Test/GuestCheckoutWithEnabledPersistentTest.xml b/dev/tests/acceptance/tests/functional/Magento/FunctionalTest/Persistent/Test/GuestCheckoutWithEnabledPersistentTest.xml new file mode 100644 index 00000000000..f7f76da7d38 --- /dev/null +++ b/dev/tests/acceptance/tests/functional/Magento/FunctionalTest/Persistent/Test/GuestCheckoutWithEnabledPersistentTest.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="../../../../../../vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Test/etc/testSchema.xsd"> + <test name="GuestCheckoutWithEnabledPersistentTest"> + <annotations> + <features value="Persistent"/> + <title value="Guest Checkout with Enabled Persistent"/> + <description value="Checkout data must be restored after page checkout reload."/> + <severity value="CRITICAL"/> + <testCaseId value="MAGETWO-92453"/> + <group value="persistent"/> + </annotations> + <before> + <createData entity="PersistentConfigEnabled" stepKey="enablePersistent"/> + <createData entity="_defaultCategory" stepKey="createCategory"/> + <createData entity="_defaultProduct" stepKey="createProduct"> + <requiredEntity createDataKey="createCategory"/> + </createData> + </before> + <after> + <createData entity="PersistentConfigDefault" stepKey="setDefaultPersistentState"/> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + <deleteData createDataKey="createProduct" stepKey="deleteProduct"/> + </after> + <!-- Add simple product to cart --> + <actionGroup ref="AddSimpleProductToCart" stepKey="addProductToCart1"> + <argument name="product" value="$$createProduct$$"/> + </actionGroup> + <!-- Navigate to checkout --> + <actionGroup ref="GoToCheckoutFromMinicartActionGroup" stepKey="navigateToCheckoutFromMinicart"/> + <!-- Fill Shipping Address form --> + <fillField selector="{{CheckoutShippingGuestInfoSection.email}}" userInput="{{CustomerEntityOne.email}}" stepKey="enterEmail"/> + <fillField selector="{{CheckoutShippingGuestInfoSection.firstName}}" userInput="{{CustomerEntityOne.firstname}}" stepKey="enterFirstName"/> + <fillField selector="{{CheckoutShippingGuestInfoSection.lastName}}" userInput="{{CustomerEntityOne.lastname}}" stepKey="enterLastName"/> + <fillField selector="{{CheckoutShippingGuestInfoSection.street}}" userInput="{{CustomerAddressSimple.street[0]}}" stepKey="enterStreet"/> + <fillField selector="{{CheckoutShippingGuestInfoSection.city}}" userInput="{{CustomerAddressSimple.city}}" stepKey="enterCity"/> + <selectOption selector="{{CheckoutShippingGuestInfoSection.region}}" userInput="{{CustomerAddressSimple.state}}" stepKey="selectRegion"/> + <fillField selector="{{CheckoutShippingGuestInfoSection.postcode}}" userInput="{{CustomerAddressSimple.postcode}}" stepKey="enterPostcode"/> + <fillField selector="{{CheckoutShippingGuestInfoSection.telephone}}" userInput="{{CustomerAddressSimple.telephone}}" stepKey="enterTelephone"/> + <waitForLoadingMaskToDisappear stepKey="waitForLoadingMask"/> + <click selector="{{CheckoutShippingGuestInfoSection.firstShippingMethod}}" stepKey="selectFirstShippingMethod"/> + <!-- Check that have the same values after page reload --> + <amOnPage url="{{CheckoutPage.url}}" stepKey="amOnCheckoutShippingInfoPage"/> + <waitForPageLoad stepKey="waitForShippingPageReload"/> + <seeInField selector="{{CheckoutShippingGuestInfoSection.email}}" userInput="{{CustomerEntityOne.email}}" stepKey="seeEmailOnCheckout" /> + <seeInField selector="{{CheckoutShippingGuestInfoSection.firstName}}" userInput="{{CustomerEntityOne.firstName}}" stepKey="seeFirstnameOnCheckout" /> + <seeInField selector="{{CheckoutShippingGuestInfoSection.lastName}}" userInput="{{CustomerEntityOne.lastName}}" stepKey="seeLastnameOnCheckout" /> + <seeInField selector="{{CheckoutShippingGuestInfoSection.street}}" userInput="{{CustomerAddressSimple.street[0]}}" stepKey="seeStreetOnCheckout" /> + <seeInField selector="{{CheckoutShippingGuestInfoSection.city}}" userInput="{{CustomerAddressSimple.city}}" stepKey="seeCityOnCheckout" /> + <seeInField selector="{{CheckoutShippingGuestInfoSection.region}}" userInput="{{CustomerAddressSimple.state}}" stepKey="seeStateOnCheckout" /> + <seeInField selector="{{CheckoutShippingGuestInfoSection.postcode}}" userInput="{{CustomerAddressSimple.postcode}}" stepKey="seePostcodeOnCheckout" /> + <seeInField selector="{{CheckoutShippingGuestInfoSection.telephone}}" userInput="{{CustomerAddressSimple.telephone}}" stepKey="seePhoneOnCheckout" /> + <!-- Click next button to open payment section --> + <click selector="{{CheckoutShippingGuestInfoSection.next}}" stepKey="clickNext"/> + <actionGroup ref="CheckoutSelectCheckMoneyOrderPaymentActionGroup" stepKey="guestSelectCheckMoneyOrderPayment" /> + <!-- Reload payment section --> + <amOnPage url="{{GuestCheckoutReviewAndPaymentsPage.url}}" stepKey="amOnCheckoutPaymentsPage"/> + <waitForElement selector="{{CheckoutPaymentSection.placeOrder}}" time="30" stepKey="waitForPlaceOrderButton2"/> + <!-- Check that address block contains correct information --> + <see selector="{{CheckoutPaymentSection.billingAddress}}" userInput="{{CustomerAddressSimple.firstName}}" stepKey="seeBilllingFirstName" /> + <see selector="{{CheckoutPaymentSection.billingAddress}}" userInput="{{CustomerAddressSimple.lastName}}" stepKey="seeBilllingLastName" /> + <see selector="{{CheckoutPaymentSection.billingAddress}}" userInput="{{CustomerAddressSimple.street[0]}}" stepKey="seeBilllingStreet" /> + <see selector="{{CheckoutPaymentSection.billingAddress}}" userInput="{{CustomerAddressSimple.city}}" stepKey="seeBilllingCity" /> + <see selector="{{CheckoutPaymentSection.billingAddress}}" userInput="{{CustomerAddressSimple.state}}" stepKey="seeBilllingState" /> + <see selector="{{CheckoutPaymentSection.billingAddress}}" userInput="{{CustomerAddressSimple.postcode}}" stepKey="seeBilllingPostcode" /> + <see selector="{{CheckoutPaymentSection.billingAddress}}" userInput="{{CustomerAddressSimple.telephone}}" stepKey="seeBilllingTelephone" /> + <!-- Check that "Ship To" block contains correct information --> + <see selector="{{CheckoutPaymentSection.shipToInfomation}}" userInput="{{CustomerAddressSimple.firstName}}" stepKey="seeShipToFirstName" /> + <see selector="{{CheckoutPaymentSection.shipToInfomation}}" userInput="{{CustomerAddressSimple.lastName}}" stepKey="seeShipToLastName" /> + <see selector="{{CheckoutPaymentSection.shipToInfomation}}" userInput="{{CustomerAddressSimple.street[0]}}" stepKey="seeShipToStreet" /> + <see selector="{{CheckoutPaymentSection.shipToInfomation}}" userInput="{{CustomerAddressSimple.city}}" stepKey="seeShipToCity" /> + <see selector="{{CheckoutPaymentSection.shipToInfomation}}" userInput="{{CustomerAddressSimple.state}}" stepKey="seeShipToState" /> + <see selector="{{CheckoutPaymentSection.shipToInfomation}}" userInput="{{CustomerAddressSimple.postcode}}" stepKey="seeShipToPostcode" /> + <see selector="{{CheckoutPaymentSection.shipToInfomation}}" userInput="{{CustomerAddressSimple.telephone}}" stepKey="seeShipToTelephone" /> + </test> +</tests> diff --git a/dev/tests/acceptance/tests/functional/Magento/FunctionalTest/SalesRule/ActionGroup/AdminCartPriceRuleActionGroup.xml b/dev/tests/acceptance/tests/functional/Magento/FunctionalTest/SalesRule/ActionGroup/AdminCartPriceRuleActionGroup.xml new file mode 100644 index 00000000000..24d881ee608 --- /dev/null +++ b/dev/tests/acceptance/tests/functional/Magento/FunctionalTest/SalesRule/ActionGroup/AdminCartPriceRuleActionGroup.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="../../../../../../vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Test/etc/actionGroupSchema.xsd"> + <actionGroup name="selectNotLoggedInCustomerGroup"> + <!-- This actionGroup was created to be merged from B2B because B2B has a very different form control here --> + <selectOption selector="{{AdminCartPriceRulesFormSection.customerGroups}}" userInput="NOT LOGGED IN" stepKey="selectCustomerGroup"/> + </actionGroup> +</actionGroups> diff --git a/dev/tests/acceptance/tests/functional/Magento/FunctionalTest/SalesRule/Section/PriceRuleConditionsSection.xml b/dev/tests/acceptance/tests/functional/Magento/FunctionalTest/SalesRule/Section/PriceRuleConditionsSection.xml index 39c6dd6b319..93ed408ce7a 100644 --- a/dev/tests/acceptance/tests/functional/Magento/FunctionalTest/SalesRule/Section/PriceRuleConditionsSection.xml +++ b/dev/tests/acceptance/tests/functional/Magento/FunctionalTest/SalesRule/Section/PriceRuleConditionsSection.xml @@ -16,5 +16,9 @@ <element name="treeRoot" type="text" selector=".x-tree-root-ct.x-tree-lines"/> <element name="lastTreeNode" type="text" selector=".x-tree-root-ct.x-tree-lines > div > li > ul > li:last-child div img.x-tree-elbow-end-plus"/> <element name="subcategory4level" type="text" selector=".x-tree-root-ct.x-tree-lines > div > li > ul > li > ul > li > ul > li > ul > li > div img.x-tree-elbow-end-plus"/> + + <element name="ruleParamLink" type="button" selector="//*[@id='conditions__{{var1}}__children']/li[{{var2}}]/span[{{var3}}]/a" parameterized="true"/> + <element name="operatorByIndex" type="input" selector="#conditions__{{var1}}--{{var2}}__operator" parameterized="true"/> + <element name="valueByIndex" type="input" selector="#conditions__{{var1}}--{{var2}}__value" parameterized="true"/> </section> </sections> diff --git a/dev/tests/acceptance/tests/functional/Magento/FunctionalTest/SalesRule/Test/StorefrontCartPriceRuleCountry.xml b/dev/tests/acceptance/tests/functional/Magento/FunctionalTest/SalesRule/Test/StorefrontCartPriceRuleCountry.xml new file mode 100644 index 00000000000..a6a0d669f34 --- /dev/null +++ b/dev/tests/acceptance/tests/functional/Magento/FunctionalTest/SalesRule/Test/StorefrontCartPriceRuleCountry.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="../../../../../../vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Test/etc/testSchema.xsd"> + <test name="StorefrontCartPriceRuleCountry"> + <annotations> + <features value="SalesRule"/> + <stories value="Create cart price rule"/> + <title value="Customer should only see cart price rule discount if condition shipping country"/> + <description value="Customer should only see cart price rule discount if condition shipping country"/> + <severity value="AVERAGE"/> + <testCaseId value="MC-241"/> + <group value="SalesRule"/> + </annotations> + + <before> + <createData entity="_defaultCategory" stepKey="createPreReqCategory"/> + <createData entity="_defaultProduct" stepKey="createPreReqProduct"> + <requiredEntity createDataKey="createPreReqCategory"/> + </createData> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + </before> + + <after> + <actionGroup ref="DeleteCartPriceRuleByName" stepKey="cleanUpRule"> + <argument name="ruleName" value="{{SimpleSalesRule.name}}"/> + </actionGroup> + <deleteData createDataKey="createPreReqCategory" stepKey="deletePreReqCategory"/> + <deleteData createDataKey="createPreReqProduct" stepKey="deletePreReqProduct"/> + <amOnPage url="admin/admin/auth/logout/" stepKey="amOnLogoutPage"/> + </after> + + <!-- Create the rule... --> + <amOnPage url="{{AdminCartPriceRulesPage.url}}" stepKey="amOnCartPriceList"/> + <waitForPageLoad stepKey="waitForRulesPage"/> + <click selector="{{AdminCartPriceRulesSection.addNewRuleButton}}" stepKey="clickAddNewRule"/> + <fillField selector="{{AdminCartPriceRulesFormSection.ruleName}}" userInput="{{SimpleSalesRule.name}}" stepKey="fillRuleName"/> + <selectOption selector="{{AdminCartPriceRulesFormSection.websites}}" userInput="Main Website" stepKey="selectWebsites"/> + <actionGroup ref="selectNotLoggedInCustomerGroup" stepKey="selectNotLoggedInCustomerGroup"/> + <click selector="{{PriceRuleConditionsSection.conditionsTab}}" stepKey="expandConditions"/> + <!-- Scroll down to fix some flaky behavior... --> + <scrollTo selector="{{PriceRuleConditionsSection.conditionsTab}}" stepKey="scrollToConditionsTab"/> + <waitForElementVisible selector="{{PriceRuleConditionsSection.createNewRule}}" stepKey="waitForNewRule"/> + <click selector="{{PriceRuleConditionsSection.createNewRule}}" stepKey="clickNewRule"/> + <selectOption selector="{{PriceRuleConditionsSection.rulesDropdown}}" userInput="Shipping Country" stepKey="selectProductAttributes"/> + <waitForPageLoad stepKey="wait1"/> + <click selector="{{PriceRuleConditionsSection.ruleParamLink('1', '1', '2')}}" stepKey="startEditValue"/> + <waitForPageLoad stepKey="wait4"/> + <selectOption selector="{{PriceRuleConditionsSection.valueByIndex('1', '1')}}" userInput="Brazil" stepKey="fillValue"/> + <waitForPageLoad stepKey="wait5"/> + <click selector="{{AdminCartPriceRulesFormSection.actionsHeader}}" stepKey="clickToExpandActions"/> + <selectOption selector="{{AdminCartPriceRulesFormSection.apply}}" userInput="Fixed amount discount for whole cart" stepKey="selectActionType"/> + <fillField selector="{{AdminCartPriceRulesFormSection.discountAmount}}" userInput="9.99" stepKey="fillDiscountAmount"/> + <click selector="{{AdminCartPriceRulesFormSection.save}}" stepKey="clickSaveButton"/> + <see selector="{{AdminCartPriceRulesSection.messages}}" userInput="You saved the rule." stepKey="seeSuccessMessage"/> + + <!-- Add the product we created to our cart --> + <amOnPage url="$$createPreReqProduct.name$$.html" stepKey="goToProductPage"/> + <waitForPageLoad stepKey="waitForProductPageLoad"/> + <fillField selector="{{StorefrontProductActionSection.quantity}}" userInput="1" stepKey="fillQuantity"/> + <click selector="{{StorefrontProductActionSection.addToCart}}" stepKey="addProductToCart"/> + <waitForPageLoad stepKey="waitForAddToCart"/> + + <!-- Should not see the discount yet because we have not set country --> + <amOnPage url="{{CheckoutCartPage.url}}" stepKey="goToCartPage"/> + <waitForPageLoad stepKey="waitForCartPage"/> + <see selector="{{CheckoutCartSummarySection.subtotal}}" userInput="$123.00" stepKey="seeSubtotal"/> + <dontSeeElement selector="{{CheckoutCartSummarySection.discountAmount}}" stepKey="dontSeeDiscount"/> + + <!-- See discount if we use valid country --> + <click selector="{{CheckoutCartSummarySection.shippingHeading}}" stepKey="expandShipping"/> + <selectOption selector="{{CheckoutCartSummarySection.country}}" userInput="Brazil" stepKey="fillCountry"/> + <waitForPageLoad stepKey="waitForCountry1"/> + <waitForElementVisible selector="{{CheckoutCartSummarySection.discountAmount}}" stepKey="waitForDiscountElement"/> + <see selector="{{CheckoutCartSummarySection.discountAmount}}" userInput="-$9.99" stepKey="seeDiscountTotal"/> + + <!-- Do not see discount with other country --> + <selectOption selector="{{CheckoutCartSummarySection.country}}" userInput="United States" stepKey="fillCountry2"/> + <waitForPageLoad stepKey="waitForCountry2"/> + <dontSeeElement selector="{{CheckoutCartSummarySection.discountAmount}}" stepKey="dontSeeDiscount2"/> + </test> +</tests> diff --git a/dev/tests/acceptance/tests/functional/Magento/FunctionalTest/SalesRule/Test/StorefrontCartPriceRulePostcode.xml b/dev/tests/acceptance/tests/functional/Magento/FunctionalTest/SalesRule/Test/StorefrontCartPriceRulePostcode.xml new file mode 100644 index 00000000000..97936382e60 --- /dev/null +++ b/dev/tests/acceptance/tests/functional/Magento/FunctionalTest/SalesRule/Test/StorefrontCartPriceRulePostcode.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="../../../../../../vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Test/etc/testSchema.xsd"> + <test name="StorefrontCartPriceRulePostcode"> + <annotations> + <features value="SalesRule"/> + <stories value="Create cart price rule"/> + <title value="Customer should only see cart price rule discount if condition shipping postcode"/> + <description value="Customer should only see cart price rule discount if condition shipping postcode"/> + <severity value="AVERAGE"/> + <testCaseId value="MC-238"/> + <group value="SalesRule"/> + </annotations> + + <before> + <createData entity="_defaultCategory" stepKey="createPreReqCategory"/> + <createData entity="_defaultProduct" stepKey="createPreReqProduct"> + <requiredEntity createDataKey="createPreReqCategory"/> + </createData> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + </before> + + <after> + <actionGroup ref="DeleteCartPriceRuleByName" stepKey="cleanUpRule"> + <argument name="ruleName" value="{{SimpleSalesRule.name}}"/> + </actionGroup> + <deleteData createDataKey="createPreReqCategory" stepKey="deletePreReqCategory"/> + <deleteData createDataKey="createPreReqProduct" stepKey="deletePreReqProduct"/> + <amOnPage url="admin/admin/auth/logout/" stepKey="amOnLogoutPage"/> + </after> + + <!-- Create the rule... --> + <amOnPage url="{{AdminCartPriceRulesPage.url}}" stepKey="amOnCartPriceList"/> + <waitForPageLoad stepKey="waitForRulesPage"/> + <click selector="{{AdminCartPriceRulesSection.addNewRuleButton}}" stepKey="clickAddNewRule"/> + <fillField selector="{{AdminCartPriceRulesFormSection.ruleName}}" userInput="{{SimpleSalesRule.name}}" stepKey="fillRuleName"/> + <selectOption selector="{{AdminCartPriceRulesFormSection.websites}}" userInput="Main Website" stepKey="selectWebsites"/> + <actionGroup ref="selectNotLoggedInCustomerGroup" stepKey="selectNotLoggedInCustomerGroup"/> + <click selector="{{PriceRuleConditionsSection.conditionsTab}}" stepKey="expandConditions"/> + <!-- Scroll down to fix some flaky behavior... --> + <scrollTo selector="{{PriceRuleConditionsSection.conditionsTab}}" stepKey="scrollToConditionsTab"/> + <waitForElementVisible selector="{{PriceRuleConditionsSection.createNewRule}}" stepKey="waitForNewRule"/> + <click selector="{{PriceRuleConditionsSection.createNewRule}}" stepKey="clickNewRule"/> + <selectOption selector="{{PriceRuleConditionsSection.rulesDropdown}}" userInput="Shipping Postcode" stepKey="selectProductAttributes"/> + <waitForPageLoad stepKey="wait1"/> + <click selector="{{PriceRuleConditionsSection.ruleParamLink('1', '1', '1')}}" stepKey="startEditOperator"/> + <waitForPageLoad stepKey="wait2"/> + <selectOption selector="{{PriceRuleConditionsSection.operatorByIndex('1', '1')}}" userInput="is one of" stepKey="fillOperator"/> + <waitForPageLoad stepKey="wait3"/> + <click selector="{{PriceRuleConditionsSection.ruleParamLink('1', '1', '2')}}" stepKey="startEditValue"/> + <waitForPageLoad stepKey="wait4"/> + <fillField selector="{{PriceRuleConditionsSection.valueByIndex('1', '1')}}" userInput="78613" stepKey="fillValue"/> + <waitForPageLoad stepKey="wait5"/> + <click selector="{{AdminCartPriceRulesFormSection.actionsHeader}}" stepKey="clickToExpandActions"/> + <selectOption selector="{{AdminCartPriceRulesFormSection.apply}}" userInput="Fixed amount discount for whole cart" stepKey="selectActionType"/> + <fillField selector="{{AdminCartPriceRulesFormSection.discountAmount}}" userInput="9.99" stepKey="fillDiscountAmount"/> + <click selector="{{AdminCartPriceRulesFormSection.save}}" stepKey="clickSaveButton"/> + <see selector="{{AdminCartPriceRulesSection.messages}}" userInput="You saved the rule." stepKey="seeSuccessMessage"/> + + <!-- Add the product we created to our cart --> + <amOnPage url="$$createPreReqProduct.name$$.html" stepKey="goToProductPage"/> + <waitForPageLoad stepKey="waitForProductPageLoad"/> + <fillField selector="{{StorefrontProductActionSection.quantity}}" userInput="1" stepKey="fillQuantity"/> + <click selector="{{StorefrontProductActionSection.addToCart}}" stepKey="addProductToCart"/> + <waitForPageLoad stepKey="waitForAddToCart"/> + + <!-- Should not see the discount yet because we have not filled in postcode --> + <amOnPage url="{{CheckoutCartPage.url}}" stepKey="goToCartPage"/> + <waitForPageLoad stepKey="waitForCartPage"/> + <see selector="{{CheckoutCartSummarySection.subtotal}}" userInput="$123.00" stepKey="seeSubtotal"/> + <dontSeeElement selector="{{CheckoutCartSummarySection.discountAmount}}" stepKey="dontSeeDiscount"/> + + <!-- See discount if we use valid postcode --> + <click selector="{{CheckoutCartSummarySection.shippingHeading}}" stepKey="expandShipping"/> + <fillField selector="{{CheckoutCartSummarySection.postcode}}" userInput="78613" stepKey="fillPostcode"/> + <waitForPageLoad stepKey="waitForPostcode1"/> + <waitForElementVisible selector="{{CheckoutCartSummarySection.discountAmount}}" stepKey="waitForDiscountElement"/> + <see selector="{{CheckoutCartSummarySection.discountAmount}}" userInput="-$9.99" stepKey="seeDiscountTotal"/> + + <!-- Do not see discount with other postcode --> + <fillField selector="{{CheckoutCartSummarySection.postcode}}" userInput="90210" stepKey="fillPostcode2"/> + <waitForPageLoad stepKey="waitForPostcode2"/> + <dontSeeElement selector="{{CheckoutCartSummarySection.discountAmount}}" stepKey="dontSeeDiscount2"/> + </test> +</tests> diff --git a/dev/tests/acceptance/tests/functional/Magento/FunctionalTest/SalesRule/Test/StorefrontCartPriceRuleQuantity.xml b/dev/tests/acceptance/tests/functional/Magento/FunctionalTest/SalesRule/Test/StorefrontCartPriceRuleQuantity.xml new file mode 100644 index 00000000000..f4342b5d480 --- /dev/null +++ b/dev/tests/acceptance/tests/functional/Magento/FunctionalTest/SalesRule/Test/StorefrontCartPriceRuleQuantity.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="../../../../../../vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Test/etc/testSchema.xsd"> + <test name="StorefrontCartPriceRuleQuantity"> + <annotations> + <features value="SalesRule"/> + <stories value="Create cart price rule"/> + <title value="Customer should only see cart price rule discount if condition total items quantity greater than"/> + <description value="Customer should only see cart price rule discount if condition total items quantity greater than"/> + <severity value="AVERAGE"/> + <testCaseId value="MC-236"/> + <group value="SalesRule"/> + </annotations> + + <before> + <createData entity="_defaultCategory" stepKey="createPreReqCategory"/> + <createData entity="_defaultProduct" stepKey="createPreReqProduct"> + <requiredEntity createDataKey="createPreReqCategory"/> + </createData> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + </before> + + <after> + <actionGroup ref="DeleteCartPriceRuleByName" stepKey="cleanUpRule"> + <argument name="ruleName" value="{{SimpleSalesRule.name}}"/> + </actionGroup> + <deleteData createDataKey="createPreReqCategory" stepKey="deletePreReqCategory"/> + <deleteData createDataKey="createPreReqProduct" stepKey="deletePreReqProduct"/> + <amOnPage url="admin/admin/auth/logout/" stepKey="amOnLogoutPage"/> + </after> + + <!-- Create the rule... --> + <amOnPage url="{{AdminCartPriceRulesPage.url}}" stepKey="amOnCartPriceList"/> + <waitForPageLoad stepKey="waitForRulesPage"/> + <click selector="{{AdminCartPriceRulesSection.addNewRuleButton}}" stepKey="clickAddNewRule"/> + <fillField selector="{{AdminCartPriceRulesFormSection.ruleName}}" userInput="{{SimpleSalesRule.name}}" stepKey="fillRuleName"/> + <selectOption selector="{{AdminCartPriceRulesFormSection.websites}}" userInput="Main Website" stepKey="selectWebsites"/> + <actionGroup ref="selectNotLoggedInCustomerGroup" stepKey="selectNotLoggedInCustomerGroup"/> + <click selector="{{PriceRuleConditionsSection.conditionsTab}}" stepKey="expandConditions"/> + <!-- Scroll down to fix some flaky behavior... --> + <scrollTo selector="{{PriceRuleConditionsSection.conditionsTab}}" stepKey="scrollToConditionsTab"/> + <waitForElementVisible selector="{{PriceRuleConditionsSection.createNewRule}}" stepKey="waitForNewRule"/> + <click selector="{{PriceRuleConditionsSection.createNewRule}}" stepKey="clickNewRule"/> + <selectOption selector="{{PriceRuleConditionsSection.rulesDropdown}}" userInput="Total Items Quantity" stepKey="selectProductAttributes"/> + <waitForPageLoad stepKey="waitForConditions"/> + <click selector="{{PriceRuleConditionsSection.ruleParamLink('1', '1', '1')}}" stepKey="startEditOperator"/> + <selectOption selector="{{PriceRuleConditionsSection.operatorByIndex('1', '1')}}" userInput="greater than" stepKey="fillOperator"/> + <click selector="{{PriceRuleConditionsSection.ruleParamLink('1', '1', '2')}}" stepKey="startEditValue"/> + <fillField selector="{{PriceRuleConditionsSection.valueByIndex('1', '1')}}" userInput="1" stepKey="fillValue"/> + <click selector="{{AdminCartPriceRulesFormSection.actionsHeader}}" stepKey="clickToExpandActions"/> + <selectOption selector="{{AdminCartPriceRulesFormSection.apply}}" userInput="Fixed amount discount for whole cart" stepKey="selectActionType"/> + <fillField selector="{{AdminCartPriceRulesFormSection.discountAmount}}" userInput="1.00" stepKey="fillDiscountAmount"/> + <click selector="{{AdminCartPriceRulesFormSection.save}}" stepKey="clickSaveButton"/> + <see selector="{{AdminCartPriceRulesSection.messages}}" userInput="You saved the rule." stepKey="seeSuccessMessage"/> + + <!-- Add 1 product to the cart --> + <amOnPage url="$$createPreReqProduct.name$$.html" stepKey="goToProductPage"/> + <waitForPageLoad stepKey="waitForProductPageLoad"/> + <fillField selector="{{StorefrontProductActionSection.quantity}}" userInput="1" stepKey="fillQuantity"/> + <click selector="{{StorefrontProductActionSection.addToCart}}" stepKey="addProductToCart"/> + <waitForPageLoad stepKey="waitForAddToCart"/> + + <!-- Should not see the discount yet because we have only 1 item in our cart --> + <amOnPage url="{{CheckoutCartPage.url}}" stepKey="goToCartPage"/> + <waitForPageLoad stepKey="waitForCartPage"/> + <see selector="{{CheckoutCartSummarySection.subtotal}}" userInput="$123.00" stepKey="seeSubtotal"/> + <dontSeeElement selector="{{CheckoutCartSummarySection.discountAmount}}" stepKey="dontSeeDiscount"/> + + <!-- Add the same product to the cart again (2 total) --> + <amOnPage url="$$createPreReqProduct.name$$.html" stepKey="goToProductPage2"/> + <waitForPageLoad stepKey="waitForProductPageLoad2"/> + <fillField selector="{{StorefrontProductActionSection.quantity}}" userInput="1" stepKey="fillQuantity2"/> + <click selector="{{StorefrontProductActionSection.addToCart}}" stepKey="addProductToCart2"/> + <waitForPageLoad stepKey="waitForAddToCart2"/> + + <!-- Now we should see the discount because we have more than 1 item --> + <amOnPage url="{{CheckoutCartPage.url}}" stepKey="goToCartPage2"/> + <waitForPageLoad stepKey="waitForCartPage2"/> + <see selector="{{CheckoutCartSummarySection.subtotal}}" userInput="$246.00" stepKey="seeSubtotal2"/> + <waitForElementVisible selector="{{CheckoutCartSummarySection.discountAmount}}" stepKey="waitForDiscountElement"/> + <see selector="{{CheckoutCartSummarySection.discountAmount}}" userInput="-$1.00" stepKey="seeDiscountTotal"/> + </test> +</tests> diff --git a/dev/tests/acceptance/tests/functional/Magento/FunctionalTest/SalesRule/Test/StorefrontCartPriceRuleState.xml b/dev/tests/acceptance/tests/functional/Magento/FunctionalTest/SalesRule/Test/StorefrontCartPriceRuleState.xml new file mode 100644 index 00000000000..e2de2117d78 --- /dev/null +++ b/dev/tests/acceptance/tests/functional/Magento/FunctionalTest/SalesRule/Test/StorefrontCartPriceRuleState.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="../../../../../../vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Test/etc/testSchema.xsd"> + <test name="StorefrontCartPriceRuleState"> + <annotations> + <features value="SalesRule"/> + <stories value="Create cart price rule"/> + <title value="Customer should only see cart price rule discount if condition shipping state/province"/> + <description value="Customer should only see cart price rule discount if condition shipping state/province"/> + <severity value="AVERAGE"/> + <testCaseId value="MC-239"/> + <group value="SalesRule"/> + </annotations> + + <before> + <createData entity="_defaultCategory" stepKey="createPreReqCategory"/> + <createData entity="_defaultProduct" stepKey="createPreReqProduct"> + <requiredEntity createDataKey="createPreReqCategory"/> + </createData> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + </before> + + <after> + <actionGroup ref="DeleteCartPriceRuleByName" stepKey="cleanUpRule"> + <argument name="ruleName" value="{{SimpleSalesRule.name}}"/> + </actionGroup> + <deleteData createDataKey="createPreReqCategory" stepKey="deletePreReqCategory"/> + <deleteData createDataKey="createPreReqProduct" stepKey="deletePreReqProduct"/> + <amOnPage url="admin/admin/auth/logout/" stepKey="amOnLogoutPage"/> + </after> + + <!-- Create the rule... --> + <amOnPage url="{{AdminCartPriceRulesPage.url}}" stepKey="amOnCartPriceList"/> + <waitForPageLoad stepKey="waitForRulesPage"/> + <click selector="{{AdminCartPriceRulesSection.addNewRuleButton}}" stepKey="clickAddNewRule"/> + <fillField selector="{{AdminCartPriceRulesFormSection.ruleName}}" userInput="{{SimpleSalesRule.name}}" stepKey="fillRuleName"/> + <selectOption selector="{{AdminCartPriceRulesFormSection.websites}}" userInput="Main Website" stepKey="selectWebsites"/> + <actionGroup ref="selectNotLoggedInCustomerGroup" stepKey="selectNotLoggedInCustomerGroup"/> + <click selector="{{PriceRuleConditionsSection.conditionsTab}}" stepKey="expandConditions"/> + <!-- Scroll down to fix some flaky behavior... --> + <scrollTo selector="{{PriceRuleConditionsSection.conditionsTab}}" stepKey="scrollToConditionsTab"/> + <waitForElementVisible selector="{{PriceRuleConditionsSection.createNewRule}}" stepKey="waitForNewRule"/> + <click selector="{{PriceRuleConditionsSection.createNewRule}}" stepKey="clickNewRule"/> + <selectOption selector="{{PriceRuleConditionsSection.rulesDropdown}}" userInput="Shipping State/Province" stepKey="selectProductAttributes"/> + <waitForPageLoad stepKey="wait1"/> + <click selector="{{PriceRuleConditionsSection.ruleParamLink('1', '1', '2')}}" stepKey="startEditValue"/> + <waitForPageLoad stepKey="wait2"/> + <selectOption selector="{{PriceRuleConditionsSection.valueByIndex('1', '1')}}" userInput="Indiana" stepKey="fillValue"/> + <waitForPageLoad stepKey="wait3"/> + <click selector="{{AdminCartPriceRulesFormSection.actionsHeader}}" stepKey="clickToExpandActions"/> + <selectOption selector="{{AdminCartPriceRulesFormSection.apply}}" userInput="Fixed amount discount for whole cart" stepKey="selectActionType"/> + <fillField selector="{{AdminCartPriceRulesFormSection.discountAmount}}" userInput="9.99" stepKey="fillDiscountAmount"/> + <click selector="{{AdminCartPriceRulesFormSection.save}}" stepKey="clickSaveButton"/> + <see selector="{{AdminCartPriceRulesSection.messages}}" userInput="You saved the rule." stepKey="seeSuccessMessage"/> + + <!-- Add the product we created to our cart --> + <amOnPage url="$$createPreReqProduct.name$$.html" stepKey="goToProductPage"/> + <waitForPageLoad stepKey="waitForProductPageLoad"/> + <fillField selector="{{StorefrontProductActionSection.quantity}}" userInput="1" stepKey="fillQuantity"/> + <click selector="{{StorefrontProductActionSection.addToCart}}" stepKey="addProductToCart"/> + <waitForPageLoad stepKey="waitForAddToCart"/> + + <!-- Should not see the discount yet because we have not filled in postcode --> + <amOnPage url="{{CheckoutCartPage.url}}" stepKey="goToCartPage"/> + <waitForPageLoad stepKey="waitForCartPage"/> + <see selector="{{CheckoutCartSummarySection.subtotal}}" userInput="$123.00" stepKey="seeSubtotal"/> + <dontSeeElement selector="{{CheckoutCartSummarySection.discountAmount}}" stepKey="dontSeeDiscount"/> + + <!-- See discount if we use valid postcode --> + <click selector="{{CheckoutCartSummarySection.shippingHeading}}" stepKey="expandShipping"/> + <selectOption selector="{{CheckoutCartSummarySection.stateProvince}}" userInput="Indiana" stepKey="fillState"/> + <waitForPageLoad stepKey="waitForPostcode1"/> + <waitForElementVisible selector="{{CheckoutCartSummarySection.discountAmount}}" stepKey="waitForDiscountElement"/> + <see selector="{{CheckoutCartSummarySection.discountAmount}}" userInput="-$9.99" stepKey="seeDiscountTotal"/> + + <!-- Do not see discount with other postcode --> + <selectOption selector="{{CheckoutCartSummarySection.stateProvince}}" userInput="Texas" stepKey="fillState2"/> + <waitForPageLoad stepKey="waitForPostcode2"/> + <dontSeeElement selector="{{CheckoutCartSummarySection.discountAmount}}" stepKey="dontSeeDiscount2"/> + </test> +</tests> diff --git a/dev/tests/acceptance/tests/functional/Magento/FunctionalTest/SalesRule/Test/StorefrontCartPriceRuleSubtotal.xml b/dev/tests/acceptance/tests/functional/Magento/FunctionalTest/SalesRule/Test/StorefrontCartPriceRuleSubtotal.xml new file mode 100644 index 00000000000..6c49534ee43 --- /dev/null +++ b/dev/tests/acceptance/tests/functional/Magento/FunctionalTest/SalesRule/Test/StorefrontCartPriceRuleSubtotal.xml @@ -0,0 +1,89 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="../../../../../../vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Test/etc/testSchema.xsd"> + <test name="StorefrontCartPriceRuleSubtotal"> + <annotations> + <features value="SalesRule"/> + <stories value="Create cart price rule"/> + <title value="Customer should only see cart price rule discount if condition subtotal equals or greater than"/> + <description value="Customer should only see cart price rule discount if condition subtotal equals or greater than"/> + <severity value="AVERAGE"/> + <testCaseId value="MC-235"/> + <group value="SalesRule"/> + </annotations> + + <before> + <createData entity="_defaultCategory" stepKey="createPreReqCategory"/> + <createData entity="_defaultProduct" stepKey="createPreReqProduct"> + <requiredEntity createDataKey="createPreReqCategory"/> + </createData> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + </before> + + <after> + <actionGroup ref="DeleteCartPriceRuleByName" stepKey="cleanUpRule"> + <argument name="ruleName" value="{{SimpleSalesRule.name}}"/> + </actionGroup> + <deleteData createDataKey="createPreReqCategory" stepKey="deletePreReqCategory"/> + <deleteData createDataKey="createPreReqProduct" stepKey="deletePreReqProduct"/> + <amOnPage url="admin/admin/auth/logout/" stepKey="amOnLogoutPage"/> + </after> + + <!-- Create the rule... --> + <amOnPage url="{{AdminCartPriceRulesPage.url}}" stepKey="amOnCartPriceList"/> + <waitForPageLoad stepKey="waitForRulesPage"/> + <click selector="{{AdminCartPriceRulesSection.addNewRuleButton}}" stepKey="clickAddNewRule"/> + <fillField selector="{{AdminCartPriceRulesFormSection.ruleName}}" userInput="{{SimpleSalesRule.name}}" stepKey="fillRuleName"/> + <selectOption selector="{{AdminCartPriceRulesFormSection.websites}}" userInput="Main Website" stepKey="selectWebsites"/> + <actionGroup ref="selectNotLoggedInCustomerGroup" stepKey="selectNotLoggedInCustomerGroup"/> + <click selector="{{PriceRuleConditionsSection.conditionsTab}}" stepKey="expandConditions"/> + <!-- Scroll down to fix some flaky behavior... --> + <scrollTo selector="{{PriceRuleConditionsSection.conditionsTab}}" stepKey="scrollToConditionsTab"/> + <click selector="{{PriceRuleConditionsSection.createNewRule}}" stepKey="clickNewRule"/> + <selectOption selector="{{PriceRuleConditionsSection.rulesDropdown}}" userInput="Subtotal" stepKey="selectProductAttributes"/> + <waitForPageLoad stepKey="waitForConditions"/> + <click selector="{{PriceRuleConditionsSection.ruleParamLink('1', '1', '1')}}" stepKey="startEditOperator"/> + <selectOption selector="{{PriceRuleConditionsSection.operatorByIndex('1', '1')}}" userInput="equals or greater than" stepKey="fillOperator"/> + <click selector="{{PriceRuleConditionsSection.ruleParamLink('1', '1', '2')}}" stepKey="startEditValue"/> + <fillField selector="{{PriceRuleConditionsSection.valueByIndex('1', '1')}}" userInput="200" stepKey="fillValue"/> + <click selector="{{AdminCartPriceRulesFormSection.actionsHeader}}" stepKey="clickToExpandActions"/> + <selectOption selector="{{AdminCartPriceRulesFormSection.apply}}" userInput="Fixed amount discount for whole cart" stepKey="selectActionType"/> + <fillField selector="{{AdminCartPriceRulesFormSection.discountAmount}}" userInput="0.01" stepKey="fillDiscountAmount"/> + <click selector="{{AdminCartPriceRulesFormSection.save}}" stepKey="clickSaveButton"/> + <see selector="{{AdminCartPriceRulesSection.messages}}" userInput="You saved the rule." stepKey="seeSuccessMessage"/> + + <!-- Add 1 product worth $123.00 to the cart --> + <amOnPage url="$$createPreReqProduct.name$$.html" stepKey="goToProductPage"/> + <waitForPageLoad stepKey="waitForProductPageLoad"/> + <fillField selector="{{StorefrontProductActionSection.quantity}}" userInput="1" stepKey="fillQuantity"/> + <click selector="{{StorefrontProductActionSection.addToCart}}" stepKey="addProductToCart"/> + <waitForPageLoad stepKey="waitForAddToCart"/> + + <!-- Should not see the discount yet because we have not exceeded $200 --> + <amOnPage url="{{CheckoutCartPage.url}}" stepKey="goToCartPage"/> + <waitForPageLoad stepKey="waitForCartPage"/> + <see selector="{{CheckoutCartSummarySection.subtotal}}" userInput="$123.00" stepKey="seeSubtotal"/> + <dontSeeElement selector="{{CheckoutCartSummarySection.discountAmount}}" stepKey="dontSeeDiscount"/> + + <!-- Add the same product to the cart again ($246.00 subtotal) --> + <amOnPage url="$$createPreReqProduct.name$$.html" stepKey="goToProductPage2"/> + <waitForPageLoad stepKey="waitForProductPageLoad2"/> + <fillField selector="{{StorefrontProductActionSection.quantity}}" userInput="1" stepKey="fillQuantity2"/> + <click selector="{{StorefrontProductActionSection.addToCart}}" stepKey="addProductToCart2"/> + <waitForPageLoad stepKey="waitForAddToCart2"/> + + <!-- Now we should see the discount because we exceeded $200 --> + <amOnPage url="{{CheckoutCartPage.url}}" stepKey="goToCartPage2"/> + <waitForPageLoad stepKey="waitForCartPage2"/> + <see selector="{{CheckoutCartSummarySection.subtotal}}" userInput="$246.00" stepKey="seeSubtotal2"/> + <waitForElementVisible selector="{{CheckoutCartSummarySection.discountAmount}}" stepKey="waitForDiscountElement"/> + <see selector="{{CheckoutCartSummarySection.discountAmount}}" userInput="-$0.01" stepKey="seeDiscountTotal"/> + </test> +</tests> diff --git a/dev/tests/acceptance/tests/functional/Magento/FunctionalTest/User/Section/AdminEditRoleInfoSection.xml b/dev/tests/acceptance/tests/functional/Magento/FunctionalTest/User/Section/AdminEditRoleInfoSection.xml index 3e5b4ac6ecc..69d1fdea9cb 100644 --- a/dev/tests/acceptance/tests/functional/Magento/FunctionalTest/User/Section/AdminEditRoleInfoSection.xml +++ b/dev/tests/acceptance/tests/functional/Magento/FunctionalTest/User/Section/AdminEditRoleInfoSection.xml @@ -12,6 +12,10 @@ <element name="roleResourcesTab" type="button" selector="#role_info_tabs_account"/> <element name="backButton" type="button" selector="button[title='Back']"/> <element name="resetButton" type="button" selector="button[title='Reset']"/> + <element name="deleteButton" type="button" selector="button[title='Delete Role']"/> <element name="saveButton" type="button" selector="button[title='Save Role']"/> + <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/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/CategoryTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/CategoryTest.php index 223fa75006d..dca3bf9abd1 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/CategoryTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/CategoryTest.php @@ -232,7 +232,6 @@ public function testCategoryProducts() special_price special_to_date swatch_image - tax_class_id thumbnail thumbnail_label tier_price @@ -358,7 +357,6 @@ private function assertAttributes($actualResponse) 'meta_keyword', 'meta_title', 'short_description', - 'tax_class_id', 'country_of_manufacture', 'gift_message_available', 'new_from_date', diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/ProductViewTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/ProductViewTest.php index aaf22c8b373..b5aa73a5a7a 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/ProductViewTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/ProductViewTest.php @@ -211,7 +211,6 @@ public function testQueryAllFieldsSimpleProduct() special_price special_to_date swatch_image - tax_class_id thumbnail thumbnail_label tier_price @@ -460,7 +459,6 @@ public function testQueryMediaGalleryEntryFieldsSimpleProduct() special_price special_to_date swatch_image - tax_class_id thumbnail thumbnail_label tier_price @@ -922,7 +920,6 @@ private function assertEavAttributes($product, $actualResponse) 'meta_keyword', 'meta_title', 'short_description', - 'tax_class_id', 'country_of_manufacture', 'gift_message_available', 'news_from_date', diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/VirtualProductViewTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/VirtualProductViewTest.php index f38d71c9b40..df3386f2f78 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/VirtualProductViewTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/VirtualProductViewTest.php @@ -36,7 +36,6 @@ public function testQueryAllFieldsVirtualProduct() name sku type_id - tax_class_id updated_at ... on PhysicalProductInterface { weight @@ -96,7 +95,6 @@ public function testCannotQueryWeightOnVirtualProductException() name sku type_id - tax_class_id updated_at ... on PhysicalProductInterface { weight @@ -130,7 +128,6 @@ private function assertBaseFields($product, $actualResponse) ['response_field' => 'attribute_set_id', 'expected_value' => $product->getAttributeSetId()], ['response_field' => 'id', 'expected_value' => $product->getId()], ['response_field' => 'name', 'expected_value' => $product->getName()], - ['response_field' => 'tax_class_id', 'expected_value' => $product->getTaxClassId()], ['response_field' => 'sku', 'expected_value' => $product->getSku()], ['response_field' => 'type_id', 'expected_value' => $product->getTypeId()] ]; diff --git a/dev/tests/api-functional/testsuite/Magento/Sales/Service/V1/ShipmentCreateTest.php b/dev/tests/api-functional/testsuite/Magento/Sales/Service/V1/ShipmentCreateTest.php index 8c8130d2b39..d8b3c5cac52 100644 --- a/dev/tests/api-functional/testsuite/Magento/Sales/Service/V1/ShipmentCreateTest.php +++ b/dev/tests/api-functional/testsuite/Magento/Sales/Service/V1/ShipmentCreateTest.php @@ -43,13 +43,13 @@ public function testInvoke() 'qty' => $orderItem->getQtyOrdered(), 'additional_data' => null, 'description' => null, - 'entity_id' => null, + 'entity_id' => 1, 'name' => null, 'parent_id' => null, 'price' => null, 'product_id' => null, 'row_total' => null, - 'sku' => null, + 'sku' => 'simple', 'weight' => null, ], ]; diff --git a/dev/tests/api-functional/testsuite/Magento/Sales/Service/V1/ShipmentGetTest.php b/dev/tests/api-functional/testsuite/Magento/Sales/Service/V1/ShipmentGetTest.php index aaa7a5a6d44..2b7e76aee07 100644 --- a/dev/tests/api-functional/testsuite/Magento/Sales/Service/V1/ShipmentGetTest.php +++ b/dev/tests/api-functional/testsuite/Magento/Sales/Service/V1/ShipmentGetTest.php @@ -5,6 +5,8 @@ */ namespace Magento\Sales\Service\V1; +use Magento\Framework\Api\ExtensibleDataInterface; +use Magento\Framework\Api\SimpleDataObjectConverter; use Magento\TestFramework\TestCase\WebapiAbstract; /** @@ -57,7 +59,18 @@ public function testShipmentGet() unset($data['tracks']); foreach ($data as $key => $value) { if (!empty($value)) { - $this->assertEquals($shipment->getData($key), $value, $key); + if ($key === ExtensibleDataInterface::EXTENSION_ATTRIBUTES_KEY) { + foreach ($value as $extensionAttributeKey => $extensionAttributeValue) { + $methodName = 'get' . + SimpleDataObjectConverter::snakeCaseToUpperCamelCase($extensionAttributeKey); + $this->assertEquals( + $shipment->getExtensionAttributes()->$methodName(), + $extensionAttributeValue + ); + } + } else { + $this->assertEquals($shipment->getData($key), $value, $key); + } } } $shipmentItem = $this->objectManager->get(\Magento\Sales\Model\Order\Shipment\Item::class); diff --git a/dev/tests/functional/tests/app/Magento/Braintree/Test/TestCase/CreateOrderBackendTest.xml b/dev/tests/functional/tests/app/Magento/Braintree/Test/TestCase/CreateOrderBackendTest.xml index 8ffcab68cdc..acf15c0e282 100644 --- a/dev/tests/functional/tests/app/Magento/Braintree/Test/TestCase/CreateOrderBackendTest.xml +++ b/dev/tests/functional/tests/app/Magento/Braintree/Test/TestCase/CreateOrderBackendTest.xml @@ -27,8 +27,8 @@ <data name="creditCard/data/payment_code" xsi:type="string">braintree</data> <data name="configData" xsi:type="string">braintree</data> <data name="status" xsi:type="string">Processing</data> - <data name="orderButtonsAvailable" xsi:type="string">Back, Cancel, Send Email, Hold, Invoice, Ship, Reorder, Edit</data> - <constraint name="Magento\Sales\Test\Constraint\AssertOrderSuccessCreateMessage" /> + <data name="orderButtonsAvailable" xsi:type="string">Back, Cancel, Send Email, Invoice, Reorder, Edit</data> + <constraint name="Magento\Shipping\Test\Constraint\AssertShipmentSuccessCreateMessage" /> <constraint name="Magento\Sales\Test\Constraint\AssertOrderButtonsAvailable" /> <constraint name="Magento\Sales\Test\Constraint\AssertOrderGrandTotal" /> <constraint name="Magento\Sales\Test\Constraint\AssertOrderStatusIsCorrect" /> @@ -55,9 +55,9 @@ <data name="creditCard/dataset" xsi:type="string">visa_default</data> <data name="creditCard/data/payment_code" xsi:type="string">braintree</data> <data name="configData" xsi:type="string">braintree, braintree_sale</data> - <data name="status" xsi:type="string">Processing</data> - <data name="orderButtonsAvailable" xsi:type="string">Back, Send Email, Hold, Ship, Reorder</data> - <constraint name="Magento\Sales\Test\Constraint\AssertOrderSuccessCreateMessage" /> + <data name="status" xsi:type="string">Complete</data> + <data name="orderButtonsAvailable" xsi:type="string">Back, Send Email, Reorder</data> + <constraint name="Magento\Shipping\Test\Constraint\AssertShipmentSuccessCreateMessage" /> <constraint name="Magento\Sales\Test\Constraint\AssertOrderButtonsAvailable" /> <constraint name="Magento\Sales\Test\Constraint\AssertOrderGrandTotal" /> <constraint name="Magento\Sales\Test\Constraint\AssertOrderStatusIsCorrect" /> @@ -83,7 +83,7 @@ <data name="creditCard/dataset" xsi:type="string">visa_braintree_fraud_rejected</data> <data name="configData" xsi:type="string">braintree</data> <data name="status" xsi:type="string">Processing</data> - <constraint name="Magento\Sales\Test\Constraint\AssertOrderSuccessCreateMessage" /> + <constraint name="Magento\Shipping\Test\Constraint\AssertShipmentSuccessCreateMessage" /> <constraint name="Magento\Sales\Test\Constraint\AssertOrderStatusIsCorrect" /> <constraint name="Magento\Sales\Test\Constraint\AssertOrderInOrdersGridOnFrontend" /> </variation> diff --git a/dev/tests/functional/tests/app/Magento/Catalog/Test/Block/Product/View/CustomOptions.php b/dev/tests/functional/tests/app/Magento/Catalog/Test/Block/Product/View/CustomOptions.php index 5e9e5526eb6..9f05a4ade8a 100644 --- a/dev/tests/functional/tests/app/Magento/Catalog/Test/Block/Product/View/CustomOptions.php +++ b/dev/tests/functional/tests/app/Magento/Catalog/Test/Block/Product/View/CustomOptions.php @@ -58,7 +58,7 @@ class CustomOptions extends Form * * @var string */ - protected $maxCharacters = './/div[@class="control"]/p[@class="note"]/strong'; + protected $maxCharacters = './/div[@class="control"]/p[contains(@class, "note")]'; /** * Selector for label of option value element @@ -72,7 +72,7 @@ class CustomOptions extends Form * * @var string */ - protected $noteByNumber = './/*[@class="note"][%d]/strong'; + protected $noteByNumber = './/*[contains(@class, "note")][%d]/strong'; /** * Selector for select element of option @@ -220,13 +220,19 @@ public function isJsMessageVisible($customOptionTitle) protected function getFieldData(SimpleElement $option) { $price = $this->getOptionPriceNotice($option); - $maxCharacters = $option->find($this->maxCharacters, Locator::SELECTOR_XPATH); + $maxCharactersElement = $option->find($this->maxCharacters, Locator::SELECTOR_XPATH); + + $maxCharacters = null; + if ($maxCharactersElement->isVisible()) { + preg_match('/\s([0-9]+)\s/', $maxCharactersElement->getText(), $match); + $maxCharacters = isset($match[1]) ? $match[1] : $maxCharactersElement->getText(); + } return [ 'options' => [ [ 'price' => floatval($price), - 'max_characters' => $maxCharacters->isVisible() ? $maxCharacters->getText() : null, + 'max_characters' => $maxCharacters, ], ] ]; diff --git a/dev/tests/functional/tests/app/Magento/CatalogRule/Test/Page/Adminhtml/CatalogRuleNew.xml b/dev/tests/functional/tests/app/Magento/CatalogRule/Test/Page/Adminhtml/CatalogRuleNew.xml index 1823dd1e1bb..ba4ff8d42d9 100644 --- a/dev/tests/functional/tests/app/Magento/CatalogRule/Test/Page/Adminhtml/CatalogRuleNew.xml +++ b/dev/tests/functional/tests/app/Magento/CatalogRule/Test/Page/Adminhtml/CatalogRuleNew.xml @@ -9,6 +9,6 @@ <page name="CatalogRuleNew" area="Adminhtml" mca="catalog_rule/promo_catalog/new" module="Magento_CatalogRule"> <block name="formPageActions" class="Magento\CatalogRule\Test\Block\Adminhtml\FormPageActions" locator=".page-main-actions" strategy="css selector"/> <block name="editForm" class="Magento\CatalogRule\Test\Block\Adminhtml\Promo\Catalog\Edit\PromoForm" locator="[id='page:main-container']" strategy="css selector"/> - <block name="modalBlock" class="Magento\Ui\Test\Block\Adminhtml\Modal" locator="._show[data-role=modal][style='z-index: 900;']" strategy="css selector"/> + <block name="modalBlock" class="Magento\Ui\Test\Block\Adminhtml\Modal" locator="._show[data-role=modal][style='z-index: 902;']" strategy="css selector"/> </page> </config> diff --git a/dev/tests/functional/tests/app/Magento/Sales/Test/Block/Adminhtml/Order/Actions.php b/dev/tests/functional/tests/app/Magento/Sales/Test/Block/Adminhtml/Order/Actions.php index 573f28f09b7..7a6903ef47a 100644 --- a/dev/tests/functional/tests/app/Magento/Sales/Test/Block/Adminhtml/Order/Actions.php +++ b/dev/tests/functional/tests/app/Magento/Sales/Test/Block/Adminhtml/Order/Actions.php @@ -137,6 +137,16 @@ class Actions extends Block */ protected $confirmModal = '.confirm._show[data-role=modal]'; + /** + * Is shipment can be created. + * + * @return bool + */ + public function canShip() + { + return $this->_rootElement->find($this->ship)->isVisible(); + } + /** * Ship order. * diff --git a/dev/tests/functional/tests/app/Magento/Sales/Test/TestCase/CreateOrderBackendTest.xml b/dev/tests/functional/tests/app/Magento/Sales/Test/TestCase/CreateOrderBackendTest.xml index 439b8a4492a..c4e03b94d2a 100644 --- a/dev/tests/functional/tests/app/Magento/Sales/Test/TestCase/CreateOrderBackendTest.xml +++ b/dev/tests/functional/tests/app/Magento/Sales/Test/TestCase/CreateOrderBackendTest.xml @@ -19,7 +19,7 @@ </data> <data name="payment/method" xsi:type="string">cashondelivery</data> <data name="configData" xsi:type="string">cashondelivery</data> - <constraint name="Magento\Sales\Test\Constraint\AssertOrderSuccessCreateMessage" /> + <constraint name="Magento\Shipping\Test\Constraint\AssertShipmentSuccessCreateMessage" /> <constraint name="Magento\Sales\Test\Constraint\AssertOrderGrandTotal" /> <constraint name="Magento\Catalog\Test\Constraint\AssertProductsOutOfStock" /> </variation> @@ -35,7 +35,7 @@ </data> <data name="payment/method" xsi:type="string">cashondelivery</data> <data name="configData" xsi:type="string">cashondelivery</data> - <constraint name="Magento\Sales\Test\Constraint\AssertOrderSuccessCreateMessage" /> + <constraint name="Magento\Shipping\Test\Constraint\AssertShipmentSuccessCreateMessage" /> <constraint name="Magento\Sales\Test\Constraint\AssertOrderGrandTotal" /> <constraint name="Magento\Sales\Test\Constraint\AssertReorderButtonIsNotVisibleOnFrontend" /> </variation> diff --git a/dev/tests/functional/tests/app/Magento/Sales/Test/TestStep/CreateShipmentStep.php b/dev/tests/functional/tests/app/Magento/Sales/Test/TestStep/CreateShipmentStep.php index 79abd438a1f..dcee66b4464 100644 --- a/dev/tests/functional/tests/app/Magento/Sales/Test/TestStep/CreateShipmentStep.php +++ b/dev/tests/functional/tests/app/Magento/Sales/Test/TestStep/CreateShipmentStep.php @@ -99,13 +99,21 @@ public function run() { $this->orderIndex->open(); $this->orderIndex->getSalesOrderGrid()->searchAndOpen(['id' => $this->order->getId()]); - $this->salesOrderView->getPageActions()->ship(); - if (!empty($this->data)) { - $this->orderShipmentNew->getFormBlock()->fillData($this->data, $this->order->getEntityId()['products']); + $shipmentIds = []; + /** + * As this step is used in general scenarios and not all test cases has shippable items(ex: virtual product) + * we need to check, if it possible to create shipment for given order. + */ + if ($this->salesOrderView->getPageActions()->canShip()) { + $this->salesOrderView->getPageActions()->ship(); + if (!empty($this->data)) { + $this->orderShipmentNew->getFormBlock()->fillData($this->data, $this->order->getEntityId()['products']); + } + $this->orderShipmentNew->getFormBlock()->submit(); + $shipmentIds = $this->getShipmentIds(); } - $this->orderShipmentNew->getFormBlock()->submit(); - return ['shipmentIds' => $this->getShipmentIds()]; + return ['shipmentIds' => $shipmentIds]; } /** diff --git a/dev/tests/functional/tests/app/Magento/Sales/Test/etc/testcase.xml b/dev/tests/functional/tests/app/Magento/Sales/Test/etc/testcase.xml index c7c4a2276e4..28b7e3a69f6 100644 --- a/dev/tests/functional/tests/app/Magento/Sales/Test/etc/testcase.xml +++ b/dev/tests/functional/tests/app/Magento/Sales/Test/etc/testcase.xml @@ -36,7 +36,8 @@ <step name="fillShippingAddress" module="Magento_Sales" next="selectShippingMethodForOrder" /> <step name="selectShippingMethodForOrder" module="Magento_Sales" next="selectPaymentMethodForOrder" /> <step name="selectPaymentMethodForOrder" module="Magento_Sales" next="submitOrder" /> - <step name="submitOrder" module="Magento_Sales" /> + <step name="submitOrder" module="Magento_Sales" next="createShipment"/> + <step name="createShipment" module="Magento_Sales"/> </scenario> <scenario name="CreateOrderBackendPartOneTest" firstStep="setupConfiguration"> <step name="setupConfiguration" module="Magento_Config" next="createProducts" /> diff --git a/dev/tests/integration/testsuite/Magento/Bundle/Model/Product/BundlePriceAbstract.php b/dev/tests/integration/testsuite/Magento/Bundle/Model/Product/BundlePriceAbstract.php index ca219405c9c..e30916810b1 100644 --- a/dev/tests/integration/testsuite/Magento/Bundle/Model/Product/BundlePriceAbstract.php +++ b/dev/tests/integration/testsuite/Magento/Bundle/Model/Product/BundlePriceAbstract.php @@ -8,6 +8,7 @@ /** * Abstract class for testing bundle prices + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ abstract class BundlePriceAbstract extends \PHPUnit\Framework\TestCase { @@ -34,6 +35,13 @@ protected function setUp() $this->productRepository = $this->objectManager->create(\Magento\Catalog\Api\ProductRepositoryInterface::class); $this->productCollectionFactory = $this->objectManager->create(\Magento\Catalog\Model\ResourceModel\Product\CollectionFactory::class); + + $scopeConfig = $this->objectManager->get(\Magento\Framework\App\Config\MutableScopeConfigInterface::class); + $scopeConfig->setValue( + \Magento\CatalogInventory\Model\Configuration::XML_PATH_SHOW_OUT_OF_STOCK, + true, + \Magento\Store\Model\ScopeInterface::SCOPE_STORE + ); } /** diff --git a/dev/tests/integration/testsuite/Magento/Bundle/Model/Product/DynamicBundlePriceCalculatorTest.php b/dev/tests/integration/testsuite/Magento/Bundle/Model/Product/DynamicBundlePriceCalculatorTest.php index 75fbf827a3a..1775f09eb58 100644 --- a/dev/tests/integration/testsuite/Magento/Bundle/Model/Product/DynamicBundlePriceCalculatorTest.php +++ b/dev/tests/integration/testsuite/Magento/Bundle/Model/Product/DynamicBundlePriceCalculatorTest.php @@ -28,26 +28,24 @@ public function testPriceForDynamicBundle(array $strategyModifiers, array $expec $priceInfo = $bundleProduct->getPriceInfo(); $priceCode = \Magento\Catalog\Pricing\Price\FinalPrice::PRICE_CODE; - $priceInfoFromIndexer = $this->productCollectionFactory->create() - ->addFieldToFilter('sku', 'bundle_product') - ->addPriceData() - ->load() - ->getFirstItem(); - $this->assertEquals( $expectedResults['minimalPrice'], $priceInfo->getPrice($priceCode)->getMinimalPrice()->getValue(), 'Failed to check minimal price on product' ); - $this->assertEquals( $expectedResults['maximalPrice'], $priceInfo->getPrice($priceCode)->getMaximalPrice()->getValue(), 'Failed to check maximal price on product' ); - $this->assertEquals($expectedResults['minimalPrice'], $priceInfoFromIndexer->getMinimalPrice()); + $priceInfoFromIndexer = $this->productCollectionFactory->create() + ->addFieldToFilter('sku', 'bundle_product') + ->addPriceData() + ->load() + ->getFirstItem(); + $this->assertEquals($expectedResults['minimalPrice'], $priceInfoFromIndexer->getMinimalPrice()); $this->assertEquals($expectedResults['maximalPrice'], $priceInfoFromIndexer->getMaxPrice()); } @@ -74,12 +72,20 @@ public function testPriceForDynamicBundleInWebsiteScope(array $strategyModifiers $priceInfo->getPrice($priceCode)->getMinimalPrice()->getValue(), 'Failed to check minimal price on product' ); - $this->assertEquals( $expectedResults['maximalPrice'], $priceInfo->getPrice($priceCode)->getMaximalPrice()->getValue(), 'Failed to check maximal price on product' ); + + $priceInfoFromIndexer = $this->productCollectionFactory->create() + ->addFieldToFilter('sku', 'bundle_product') + ->addPriceData() + ->load() + ->getFirstItem(); + + $this->assertEquals($expectedResults['minimalPrice'], $priceInfoFromIndexer->getMinimalPrice()); + $this->assertEquals($expectedResults['maximalPrice'], $priceInfoFromIndexer->getMaxPrice()); } /** diff --git a/dev/tests/integration/testsuite/Magento/Bundle/Model/Product/DynamicBundleWithSpecialPriceCalculatorTest.php b/dev/tests/integration/testsuite/Magento/Bundle/Model/Product/DynamicBundleWithSpecialPriceCalculatorTest.php index e95c9f6d1e2..8a76047cbe3 100644 --- a/dev/tests/integration/testsuite/Magento/Bundle/Model/Product/DynamicBundleWithSpecialPriceCalculatorTest.php +++ b/dev/tests/integration/testsuite/Magento/Bundle/Model/Product/DynamicBundleWithSpecialPriceCalculatorTest.php @@ -28,17 +28,11 @@ public function testPriceForDynamicBundle(array $strategyModifiers, array $expec $priceInfo = $bundleProduct->getPriceInfo(); $priceCode = \Magento\Catalog\Pricing\Price\FinalPrice::PRICE_CODE; - $priceInfoFromIndexer = $this->productCollectionFactory->create() - ->addFieldToFilter('sku', 'bundle_product') - ->addPriceData() - ->load() - ->getFirstItem(); $this->assertEquals( $expectedResults['minimalPrice'], $priceInfo->getPrice($priceCode)->getMinimalPrice()->getValue(), 'Failed to check minimal price on product' ); - $this->assertEquals( $expectedResults['maximalPrice'], $priceInfo->getPrice($priceCode)->getMaximalPrice()->getValue(), @@ -63,9 +57,14 @@ public function testPriceForDynamicBundle(array $strategyModifiers, array $expec ); } - $this->assertEquals($expectedResults['indexerMinimalPrice'], $priceInfoFromIndexer->getMinimalPrice()); + $priceInfoFromIndexer = $this->productCollectionFactory->create() + ->addFieldToFilter('sku', 'bundle_product') + ->addPriceData() + ->load() + ->getFirstItem(); - $this->assertEquals($expectedResults['indexerMaximumPrice'], $priceInfoFromIndexer->getMaxPrice()); + $this->assertEquals($expectedResults['minimalPrice'], $priceInfoFromIndexer->getMinimalPrice()); + $this->assertEquals($expectedResults['maximalPrice'], $priceInfoFromIndexer->getMaxPrice()); } /** @@ -83,8 +82,6 @@ public function getTestCases() 'minimalPrice' => 5, // 0.5 * 10 'maximalPrice' => 5, - 'indexerMinimalPrice' => 10, - 'indexerMaximumPrice' => 10 ] ], @@ -95,8 +92,6 @@ public function getTestCases() 'minimalPrice' => 10, // 0.5 * 2 * 10 'maximalPrice' => 10, - 'indexerMinimalPrice' => 20, - 'indexerMaximumPrice' => 20 ] ], @@ -111,8 +106,6 @@ public function getTestCases() 'minimalPrice' => 5, // 0.5 * (1 * 10 + 3 * 30) 'maximalPrice' => 50, - 'indexerMinimalPrice' => 10, - 'indexerMaximumPrice' => 100 ] ], @@ -126,8 +119,6 @@ public function getTestCases() 'minimalPrice' => 4.95, // 0.5 * ( 1 * 9.9 + 2.5 * 4) 'maximalPrice' => 9.95, - 'indexerMinimalPrice' => 9.9, - 'indexerMaximumPrice' => 19.9 ] ], @@ -142,8 +133,6 @@ public function getTestCases() 'regularMinimalPrice' => '10', // 3 * 20 + (30 * 1 + 13 * 3) 'regularMaximalPrice' => '129', - 'indexerMinimalPrice' => 7.5, - 'indexerMaximumPrice' => 79 ] ], @@ -154,8 +143,6 @@ public function getTestCases() 'minimalPrice' => 4.95, // 0.5 * max(4 * 2.5, 1 * 9.9) 'maximalPrice' => 5, - 'indexerMinimalPrice' => 9.9, - 'indexerMaximumPrice' => 10 ] ], ]; diff --git a/dev/tests/integration/testsuite/Magento/Bundle/Model/Product/DynamicBundleWithTierPriceCalculatorTest.php b/dev/tests/integration/testsuite/Magento/Bundle/Model/Product/DynamicBundleWithTierPriceCalculatorTest.php index cf15ee5602b..589a385c3df 100644 --- a/dev/tests/integration/testsuite/Magento/Bundle/Model/Product/DynamicBundleWithTierPriceCalculatorTest.php +++ b/dev/tests/integration/testsuite/Magento/Bundle/Model/Product/DynamicBundleWithTierPriceCalculatorTest.php @@ -39,26 +39,25 @@ public function testPriceForDynamicBundle(array $strategyModifiers, array $expec $priceInfo = $bundleProduct->getPriceInfo(); $priceCode = \Magento\Catalog\Pricing\Price\FinalPrice::PRICE_CODE; - $priceInfoFromIndexer = $this->productCollectionFactory->create() - ->addFieldToFilter('sku', 'bundle_product') - ->addPriceData() - ->load() - ->getFirstItem(); $this->assertEquals( $expectedResults['minimalPrice'], $priceInfo->getPrice($priceCode)->getMinimalPrice()->getValue(), 'Failed to check minimal price on product' ); - $this->assertEquals( $expectedResults['maximalPrice'], $priceInfo->getPrice($priceCode)->getMaximalPrice()->getValue(), 'Failed to check maximal price on product' ); - $this->assertEquals($expectedResults['indexerMinimalPrice'], $priceInfoFromIndexer->getMinimalPrice()); + $priceInfoFromIndexer = $this->productCollectionFactory->create() + ->addFieldToFilter('sku', 'bundle_product') + ->addPriceData() + ->load() + ->getFirstItem(); - $this->assertEquals($expectedResults['indexerMaximumPrice'], $priceInfoFromIndexer->getMaxPrice()); + $this->assertEquals($expectedResults['minimalPrice'], $priceInfoFromIndexer->getMinimalPrice()); + $this->assertEquals($expectedResults['maximalPrice'], $priceInfoFromIndexer->getMaxPrice()); } /** @@ -79,8 +78,6 @@ public function getTestCases() 'minimalPrice' => 5, // 0.5 * 10 'maximalPrice' => 5, - 'indexerMinimalPrice' => 0, - 'indexerMaximumPrice' => 10 ] ], @@ -94,8 +91,6 @@ public function getTestCases() 'minimalPrice' => 10, // 0.5 * 2 * 10 'maximalPrice' => 10, - 'indexerMinimalPrice' => 0, - 'indexerMaximumPrice' => 20 ] ], @@ -109,8 +104,6 @@ public function getTestCases() 'minimalPrice' => 5, // 0.5 * (1 * 10 + 3 * 20) 'maximalPrice' => 35, - 'indexerMinimalPrice' => 0, - 'indexerMaximumPrice' => 70 ] ], @@ -124,8 +117,6 @@ public function getTestCases() 'minimalPrice' => 5, // 0.5 * (1 * 10 + 3 * 20) 'maximalPrice' => 35, - 'indexerMinimalPrice' => 0, - 'indexerMaximumPrice' => 70 ] ], @@ -139,8 +130,6 @@ public function getTestCases() 'minimalPrice' => 5, // 0.5 * 3 * 20 'maximalPrice' => 30, - 'indexerMinimalPrice' => 0, - 'indexerMaximumPrice' => 60 ] ], @@ -155,8 +144,6 @@ public function getTestCases() 'minimalPrice' => 10, // 0.5 * (3 * 20 + 1 * 10 + 3 * 20) 'maximalPrice' => 65, - 'indexerMinimalPrice' => 0, - 'indexerMaximumPrice' => 130 ] ], @@ -170,8 +157,6 @@ public function getTestCases() 'minimalPrice' => 5, // 0.5 * (3 * 20 + 1 * 10 + 3 * 20) 'maximalPrice' => 65, - 'indexerMinimalPrice' => 0, - 'indexerMaximumPrice' => 130 ] ], @@ -185,8 +170,6 @@ public function getTestCases() 'minimalPrice' => 5, // 0.5 * (3 * 20 + 1 * 10 + 3 * 20) 'maximalPrice' => 65, - 'indexerMinimalPrice' => 0, - 'indexerMaximumPrice' => 130 ] ], @@ -200,8 +183,6 @@ public function getTestCases() 'minimalPrice' => 1.25, // 0.5 * 3 * 20 'maximalPrice' => 30, - 'indexerMinimalPrice' => 0, - 'indexerMaximumPrice' => 60 ] ], ]; diff --git a/dev/tests/integration/testsuite/Magento/Bundle/Model/Product/FixedBundlePriceCalculatorTest.php b/dev/tests/integration/testsuite/Magento/Bundle/Model/Product/FixedBundlePriceCalculatorTest.php index 325f663c5e1..34dbb90e142 100644 --- a/dev/tests/integration/testsuite/Magento/Bundle/Model/Product/FixedBundlePriceCalculatorTest.php +++ b/dev/tests/integration/testsuite/Magento/Bundle/Model/Product/FixedBundlePriceCalculatorTest.php @@ -30,23 +30,25 @@ public function testPriceForFixedBundle(array $strategyModifiers, array $expecte $priceInfo = $bundleProduct->getPriceInfo(); $priceCode = \Magento\Catalog\Pricing\Price\FinalPrice::PRICE_CODE; - $priceInfoFromIndexer = $this->productCollectionFactory->create() - ->addIdFilter([42]) - ->addPriceData() - ->load() - ->getFirstItem(); $this->assertEquals( $expectedResults['minimalPrice'], $priceInfo->getPrice($priceCode)->getMinimalPrice()->getValue(), 'Failed to check minimal price on product' ); - $this->assertEquals( $expectedResults['maximalPrice'], $priceInfo->getPrice($priceCode)->getMaximalPrice()->getValue(), 'Failed to check maximal price on product' ); - $this->assertEquals($expectedResults['indexerMinimalPrice'], $priceInfoFromIndexer->getMinimalPrice()); + + $priceInfoFromIndexer = $this->productCollectionFactory->create() + ->addIdFilter([42]) + ->addPriceData() + ->load() + ->getFirstItem(); + + $this->assertEquals($expectedResults['minimalPrice'], $priceInfoFromIndexer->getMinimalPrice()); + $this->assertEquals($expectedResults['maximalPrice'], $priceInfoFromIndexer->getMaxPrice()); } /** @@ -67,23 +69,25 @@ public function testPriceForFixedBundleInWebsiteScope(array $strategyModifiers, $priceInfo = $bundleProduct->getPriceInfo(); $priceCode = \Magento\Catalog\Pricing\Price\FinalPrice::PRICE_CODE; - $priceInfoFromIndexer = $this->productCollectionFactory->create() - ->addFieldToFilter('sku', 'bundle_product') - ->addPriceData() - ->load() - ->getFirstItem(); $this->assertEquals( $expectedResults['minimalPrice'], $priceInfo->getPrice($priceCode)->getMinimalPrice()->getValue(), 'Failed to check minimal price on product' ); - $this->assertEquals( $expectedResults['maximalPrice'], $priceInfo->getPrice($priceCode)->getMaximalPrice()->getValue(), 'Failed to check maximal price on product' ); - $this->assertEquals($expectedResults['indexerMinimalPrice'], $priceInfoFromIndexer->getMinimalPrice()); + + $priceInfoFromIndexer = $this->productCollectionFactory->create() + ->addFieldToFilter('sku', 'bundle_product') + ->addPriceData() + ->load() + ->getFirstItem(); + + $this->assertEquals($expectedResults['minimalPrice'], $priceInfoFromIndexer->getMinimalPrice()); + $this->assertEquals($expectedResults['maximalPrice'], $priceInfoFromIndexer->getMaxPrice()); } /** @@ -101,7 +105,6 @@ public function getTestCases() 'minimalPrice' => 120, // 110 + 10 (sum of simple price) 'maximalPrice' => 120, - 'indexerMinimalPrice' => 120 ] ], @@ -112,7 +115,6 @@ public function getTestCases() 'minimalPrice' => 120, // 110 + (3 * 10) + (2 * 10) + 10 'maximalPrice' => 170, - 'indexerMinimalPrice' => 120 ] ], @@ -123,7 +125,6 @@ public function getTestCases() 'minimalPrice' => 120, // 110 + 60 'maximalPrice' => 170, - 'indexerMinimalPrice' => 120 ] ], @@ -134,7 +135,6 @@ public function getTestCases() 'minimalPrice' => 120, // 110 + 30 'maximalPrice' => 140, - 'indexerMinimalPrice' => 120 ] ], @@ -152,7 +152,6 @@ public function getTestCases() // 110 + 1 * 20 + 100 'maximalPrice' => 230, - 'indexerMinimalPrice' => 130 ] ], @@ -170,7 +169,6 @@ public function getTestCases() // 110 + 110 * 0.2 + 110 * 1 'maximalPrice' => 242, - 'indexerMinimalPrice' => 132 ] ], @@ -188,7 +186,6 @@ public function getTestCases() // 110 + 1 * 20 + 110 * 1 'maximalPrice' => 240, - 'indexerMinimalPrice' => 130 ] ], @@ -206,7 +203,6 @@ public function getTestCases() // 110 + 110 * 0.2 + 100 'maximalPrice' => 232, - 'indexerMinimalPrice' => 132 ] ], ]; diff --git a/dev/tests/integration/testsuite/Magento/Bundle/Model/Product/FixedBundleWithSpecialPriceCalculatorTest.php b/dev/tests/integration/testsuite/Magento/Bundle/Model/Product/FixedBundleWithSpecialPriceCalculatorTest.php index 1113b46b1cb..1fcc205ddc3 100644 --- a/dev/tests/integration/testsuite/Magento/Bundle/Model/Product/FixedBundleWithSpecialPriceCalculatorTest.php +++ b/dev/tests/integration/testsuite/Magento/Bundle/Model/Product/FixedBundleWithSpecialPriceCalculatorTest.php @@ -30,23 +30,25 @@ public function testPriceForFixedBundle(array $strategyModifiers, array $expecte $priceInfo = $bundleProduct->getPriceInfo(); $priceCode = \Magento\Catalog\Pricing\Price\FinalPrice::PRICE_CODE; - $priceInfoFromIndexer = $this->productCollectionFactory->create() - ->addFieldToFilter('sku', 'bundle_product') - ->addPriceData() - ->load() - ->getFirstItem(); $this->assertEquals( $expectedResults['minimalPrice'], $priceInfo->getPrice($priceCode)->getMinimalPrice()->getValue(), 'Failed to check minimal price on product' ); - $this->assertEquals( $expectedResults['maximalPrice'], $priceInfo->getPrice($priceCode)->getMaximalPrice()->getValue(), 'Failed to check maximal price on product' ); - $this->assertEquals($expectedResults['indexerMinimalPrice'], $priceInfoFromIndexer->getMinimalPrice()); + + $priceInfoFromIndexer = $this->productCollectionFactory->create() + ->addFieldToFilter('sku', 'bundle_product') + ->addPriceData() + ->load() + ->getFirstItem(); + + $this->assertEquals($expectedResults['minimalPrice'], $priceInfoFromIndexer->getMinimalPrice()); + $this->assertEquals($expectedResults['maximalPrice'], $priceInfoFromIndexer->getMaxPrice()); } /** @@ -68,7 +70,6 @@ public function getTestCases() // 110 * 0.5 'maximalPrice' => 55, - 'indexerMinimalPrice' => null ] ], @@ -86,7 +87,6 @@ public function getTestCases() // 0.5 * (110 + 1 * 20) + 100 'maximalPrice' => 165, - 'indexerMinimalPrice' => 130 ] ], @@ -104,7 +104,6 @@ public function getTestCases() // 0.5 * (110 + 110 * 0.2 + 110 * 1) 'maximalPrice' => 121, - 'indexerMinimalPrice' => 132 ] ], @@ -122,7 +121,6 @@ public function getTestCases() // 0.5 * (110 + 1 * 20 + 110 * 1) 'maximalPrice' => 120, - 'indexerMinimalPrice' => 130 ] ], @@ -140,7 +138,6 @@ public function getTestCases() // 0.5 * (110 + 110 * 0.2) + 100 'maximalPrice' => 166, - 'indexerMinimalPrice' => 132 ] ], @@ -158,7 +155,6 @@ public function getTestCases() // 0.5 * (110 + 2 * 20) + 100 'maximalPrice' => 175, - 'indexerMinimalPrice' => 150 ] ], @@ -176,7 +172,6 @@ public function getTestCases() // 0.5 * (110 + 2 * 110 * 0.2 + 1 * 110) 'maximalPrice' => 132, - 'indexerMinimalPrice' => 154 ] ], @@ -194,7 +189,6 @@ public function getTestCases() // 0.5 * (110 + 2 * 20 + 1 * 110) 'maximalPrice' => 130, - 'indexerMinimalPrice' => 150 ] ], @@ -212,7 +206,6 @@ public function getTestCases() // 0.5 * (110 + 2 * 0.2 * 110) + 100 'maximalPrice' => 177, - 'indexerMinimalPrice' => 154 ] ], @@ -230,7 +223,6 @@ public function getTestCases() // 0.5 * (110 + 3 * 10 + 1 * 40) + 100 'maximalPrice' => 190, - 'indexerMinimalPrice' => 140 ] ], @@ -248,7 +240,6 @@ public function getTestCases() // 0.5 * (110 + 3 * 110 * 0.1 + 1 * 110 * 0.4 + 110 * 1) 'maximalPrice' => 148.5, - 'indexerMinimalPrice' => 143 ] ], @@ -266,7 +257,6 @@ public function getTestCases() // 0.5 * (110 + 3 * 10 + 1 * 40 + 1 * 110) 'maximalPrice' => 145, - 'indexerMinimalPrice' => 140 ] ], @@ -284,7 +274,6 @@ public function getTestCases() // 0.5 * (110 + 3 * 110 * 0.1 + 1 * 110 * 0.4) + 100 'maximalPrice' => 193.5, - 'indexerMinimalPrice' => 143 ] ], @@ -302,7 +291,6 @@ public function getTestCases() // 0.5 * (110 + 1 * 40 + 3 * 15) + 100 'maximalPrice' => 197.5, - 'indexerMinimalPrice' => 150 ] ], @@ -320,7 +308,6 @@ public function getTestCases() // 0.5 * (110 + 1 * 110 * 0.4 + 3 * 110 * 0.15 + 110 * 1) 'maximalPrice' => 156.75, - 'indexerMinimalPrice' => 154 ] ], @@ -338,7 +325,6 @@ public function getTestCases() // 0.5 * (110 + 1 * 40 + 3 * 15 + 1 * 110) 'maximalPrice' => 152.5, - 'indexerMinimalPrice' => 150 ] ], @@ -356,7 +342,6 @@ public function getTestCases() // 0.5 * (110 + 1 * 110 * 0.4 + 3 * 110 * 0.15) + 100 'maximalPrice' => 201.75, - 'indexerMinimalPrice' => 154 ] ], @@ -374,7 +359,6 @@ public function getTestCases() // 0.5 * (110 + 3 * 15) + 100 'maximalPrice' => 177.5, - 'indexerMinimalPrice' => 150 ] ], @@ -392,7 +376,6 @@ public function getTestCases() // 0.5 * (110 + 3 * 110 * 0.15 + 1 * 110) 'maximalPrice' => 134.75, - 'indexerMinimalPrice' => 154 ] ], @@ -410,7 +393,6 @@ public function getTestCases() // 0.5 * (110 + 3 * 15 + 110 * 1) 'maximalPrice' => 132.5, - 'indexerMinimalPrice' => 150 ] ], @@ -428,7 +410,6 @@ public function getTestCases() // 0.5 * (110 + 3 * 110 * 0.15) + 100 'maximalPrice' => 179.75, - 'indexerMinimalPrice' => 154 ] ], @@ -446,7 +427,6 @@ public function getTestCases() // 0.5 * (110 + 3 * 15 + 1 * 20 + 3 * 10) + 100 'maximalPrice' => 202.5, - 'indexerMinimalPrice' => 170 ] ], @@ -464,7 +444,6 @@ public function getTestCases() // 0.5 * (110 + 3 * 110 * 0.15 + 1 * 110 * 0.2 + 3 * 110 * 0.1 + 110 * 1) 'maximalPrice' => 162.25, - 'indexerMinimalPrice' => 176 ] ], @@ -482,7 +461,6 @@ public function getTestCases() // 0.5 * (110 + 3 * 15 + 1 * 20 + 3 * 10 + 1 * 110) 'maximalPrice' => 157.5, - 'indexerMinimalPrice' => 170, ] ], @@ -500,7 +478,6 @@ public function getTestCases() // 0.5 * (110 + 3 * 110 * 0.15 + 1 * 110 * 0.2 + 3 * 110 * 0.1) + 100 'maximalPrice' => 207.25, - 'indexerMinimalPrice' => 176 ] ], ]; diff --git a/dev/tests/integration/testsuite/Magento/Bundle/Model/Product/FixedBundleWithTierPriceCalculatorTest.php b/dev/tests/integration/testsuite/Magento/Bundle/Model/Product/FixedBundleWithTierPriceCalculatorTest.php index 103304a86f2..3285b1e6450 100644 --- a/dev/tests/integration/testsuite/Magento/Bundle/Model/Product/FixedBundleWithTierPriceCalculatorTest.php +++ b/dev/tests/integration/testsuite/Magento/Bundle/Model/Product/FixedBundleWithTierPriceCalculatorTest.php @@ -41,23 +41,26 @@ public function testPriceForFixedBundle(array $strategyModifiers, array $expecte /** @var \Magento\Framework\Pricing\PriceInfo\Base $priceInfo */ $priceInfo = $bundleProduct->getPriceInfo(); $priceCode = \Magento\Catalog\Pricing\Price\FinalPrice::PRICE_CODE; - $priceInfoFromIndexer = $this->productCollectionFactory->create() - ->addFieldToFilter('sku', 'bundle_product') - ->addPriceData() - ->load() - ->getFirstItem(); + $this->assertEquals( $expectedResults['minimalPrice'], $priceInfo->getPrice($priceCode)->getMinimalPrice()->getValue(), 'Failed to check minimal price on product' ); - $this->assertEquals( $expectedResults['maximalPrice'], $priceInfo->getPrice($priceCode)->getMaximalPrice()->getValue(), 'Failed to check maximal price on product' ); - $this->assertEquals($expectedResults['indexerMinimalPrice'], $priceInfoFromIndexer->getMinimalPrice()); + + $priceInfoFromIndexer = $this->productCollectionFactory->create() + ->addFieldToFilter('sku', 'bundle_product') + ->addPriceData() + ->load() + ->getFirstItem(); + + $this->assertEquals($expectedResults['minimalPrice'], $priceInfoFromIndexer->getMinimalPrice()); + $this->assertEquals($expectedResults['maximalPrice'], $priceInfoFromIndexer->getMaxPrice()); } /** @@ -79,7 +82,6 @@ public function getTestCases() // 110 * 0.5 'maximalPrice' => 55, - 'indexerMinimalPrice' => null ] ], @@ -97,7 +99,6 @@ public function getTestCases() // 0.5 * (110 + 1 * 20) + 100 'maximalPrice' => 165, - 'indexerMinimalPrice' => 65 ] ], @@ -115,7 +116,6 @@ public function getTestCases() // 0.5 * (110 + 110 * 0.2 + 110 * 1) 'maximalPrice' => 121, - 'indexerMinimalPrice' => 66 ] ], @@ -133,7 +133,6 @@ public function getTestCases() // 0.5 * (110 + 1 * 20 + 110 * 1) 'maximalPrice' => 120, - 'indexerMinimalPrice' => 65 ] ], @@ -151,7 +150,6 @@ public function getTestCases() // 0.5 * (110 + 110 * 0.2) + 100 'maximalPrice' => 166, - 'indexerMinimalPrice' => 66 ] ], @@ -169,7 +167,6 @@ public function getTestCases() // 0.5 * (110 + 2 * 20) + 100 'maximalPrice' => 175, - 'indexerMinimalPrice' => 75, ] ], @@ -187,7 +184,6 @@ public function getTestCases() // 0.5 * (110 + 2 * 110 * 0.2 + 1 * 110) 'maximalPrice' => 132, - 'indexerMinimalPrice' => 77 ] ], @@ -205,7 +201,6 @@ public function getTestCases() // 0.5 * (110 + 2 * 20 + 1 * 110) 'maximalPrice' => 130, - 'indexerMinimalPrice' => 75 ] ], @@ -224,7 +219,6 @@ public function getTestCases() // 0.5 * (110 + 2 * 0.2 * 110) + 100 'maximalPrice' => 177, - 'indexerMinimalPrice' => 77 ] ], @@ -242,7 +236,6 @@ public function getTestCases() // 0.5 * (110 + 3 * 10 + 1 * 40) + 100 'maximalPrice' => 190, - 'indexerMinimalPrice' => 70 ] ], @@ -260,7 +253,6 @@ public function getTestCases() // 0.5 * (110 + 3 * 110 * 0.1 + 1 * 110 * 0.4 + 110 * 1) 'maximalPrice' => 148.5, - 'indexerMinimalPrice' => 71.5 ] ], @@ -278,7 +270,6 @@ public function getTestCases() // 0.5 * (110 + 3 * 10 + 1 * 40 + 1 * 110) 'maximalPrice' => 145, - 'indexerMinimalPrice' => 70 ] ], @@ -296,7 +287,6 @@ public function getTestCases() // 0.5 * (110 + 3 * 110 * 0.1 + 1 * 110 * 0.4) + 100 'maximalPrice' => 193.5, - 'indexerMinimalPrice' => 71.5 ] ], @@ -314,7 +304,6 @@ public function getTestCases() // 0.5 * (110 + 1 * 40 + 3 * 15) + 100 'maximalPrice' => 197.5, - 'indexerMinimalPrice' => 75 ] ], @@ -332,7 +321,6 @@ public function getTestCases() // 0.5 * (110 + 1 * 110 * 0.4 + 3 * 110 * 0.15 + 110 * 1) 'maximalPrice' => 156.75, - 'indexerMinimalPrice' => 77 ] ], @@ -350,7 +338,6 @@ public function getTestCases() // 0.5 * (110 + 1 * 40 + 3 * 15 + 1 * 110) 'maximalPrice' => 152.5, - 'indexerMinimalPrice' => 75 ] ], @@ -368,7 +355,6 @@ public function getTestCases() // 0.5 * (110 + 1 * 110 * 0.4 + 3 * 110 * 0.15) + 100 'maximalPrice' => 201.75, - 'indexerMinimalPrice' => 77 ] ], @@ -386,7 +372,6 @@ public function getTestCases() // 0.5 * (110 + 3 * 15) + 100 'maximalPrice' => 177.5, - 'indexerMinimalPrice' => 75 ] ], @@ -404,7 +389,6 @@ public function getTestCases() // 0.5 * (110 + 3 * 110 * 0.15 + 1 * 110) 'maximalPrice' => 134.75, - 'indexerMinimalPrice' => 77 ] ], @@ -422,7 +406,6 @@ public function getTestCases() // 0.5 * (110 + 3 * 15 + 110 * 1) 'maximalPrice' => 132.5, - 'indexerMinimalPrice' => 75 ] ], @@ -440,7 +423,6 @@ public function getTestCases() // 0.5 * (110 + 3 * 110 * 0.15) + 100 'maximalPrice' => 179.75, - 'indexerMinimalPrice' => 77 ] ], @@ -458,7 +440,6 @@ public function getTestCases() // 0.5 * (110 + 3 * 15 + 1 * 20 + 3 * 10) + 100 'maximalPrice' => 202.5, - 'indexerMinimalPrice' => 85 ] ], @@ -476,7 +457,6 @@ public function getTestCases() // 0.5 * (110 + 3 * 110 * 0.15 + 1 * 110 * 0.2 + 3 * 110 * 0.1 + 110 * 1) 'maximalPrice' => 162.25, - 'indexerMinimalPrice' => 88 ] ], @@ -494,7 +474,6 @@ public function getTestCases() // 0.5 * (110 + 3 * 15 + 1 * 20 + 3 * 10 + 1 * 110) 'maximalPrice' => 157.5, - 'indexerMinimalPrice' => 85 ] ], @@ -512,7 +491,6 @@ public function getTestCases() // 0.5 * (110 + 3 * 110 * 0.15 + 1 * 110 * 0.2 + 3 * 110 * 0.1) + 100 'maximalPrice' => 207.25, - 'indexerMinimalPrice' => 88 ] ], ]; diff --git a/dev/tests/integration/testsuite/Magento/Bundle/Model/Product/SaveHandlerTest.php b/dev/tests/integration/testsuite/Magento/Bundle/Model/Product/SaveHandlerTest.php new file mode 100644 index 00000000000..381675d01ae --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Bundle/Model/Product/SaveHandlerTest.php @@ -0,0 +1,89 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Bundle\Model\Product; + +use Magento\Catalog\Api\ProductRepositoryInterface; + +/** + * Test class for \Magento\Bundle\Model\Product\SaveHandler + * The tested class used indirectly + * + * @magentoDataFixture Magento/Bundle/_files/product.php + * @magentoDataFixture Magento/Store/_files/second_website_with_two_stores.php + * @magentoDbIsolation disabled + * @magentoAppIsolation enabled + */ +class SaveHandlerTest extends \PHPUnit\Framework\TestCase +{ + /** + * @var \Magento\Framework\ObjectManagerInterface + */ + private $objectManager; + + /** + * @var \Magento\Store\Model\Store + */ + private $store; + + /** + * @var ProductRepositoryInterface + */ + private $productRepository; + + /** + * @inheritdoc + */ + protected function setUp() + { + $this->objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); + $this->store = $this->objectManager->create(\Magento\Store\Model\Store::class); + /** @var ProductRepositoryInterface $productRepository */ + $this->productRepository = $this->objectManager->create(ProductRepositoryInterface::class); + } + + /** + * @return void + */ + public function testOptionTitlesOnDifferentStores(): void + { + /** + * @var \Magento\Bundle\Model\Product\OptionList $optionList + */ + $optionList = $this->objectManager->create(\Magento\Bundle\Model\Product\OptionList::class); + + $secondStoreId = $this->store->load('fixture_second_store')->getId(); + $thirdStoreId = $this->store->load('fixture_third_store')->getId(); + + $product = $this->productRepository->get('bundle-product', true, $secondStoreId, true); + $options = $optionList->getItems($product); + $title = $options[0]->getTitle(); + $newTitle = $title . ' ' . $this->store->load('fixture_second_store')->getCode(); + $options[0]->setTitle($newTitle); + $extension = $product->getExtensionAttributes(); + $extension->setBundleProductOptions($options); + $product->setExtensionAttributes($extension); + $product->save(); + + $product = $this->productRepository->get('bundle-product', true, $thirdStoreId, true); + $options = $optionList->getItems($product); + $newTitle = $title . ' ' . $this->store->load('fixture_third_store')->getCode(); + $options[0]->setTitle($newTitle); + $extension = $product->getExtensionAttributes(); + $extension->setBundleProductOptions($options); + $product->setExtensionAttributes($extension); + $product->save(); + + $product = $this->productRepository->get('bundle-product', false, $secondStoreId, true); + $options = $optionList->getItems($product); + $this->assertEquals(1, count($options)); + $this->assertEquals( + $title . ' ' . $this->store->load('fixture_second_store')->getCode(), + $options[0]->getTitle() + ); + } +} diff --git a/dev/tests/integration/testsuite/Magento/Bundle/_files/quote_with_bundle_and_options.php b/dev/tests/integration/testsuite/Magento/Bundle/_files/quote_with_bundle_and_options.php new file mode 100644 index 00000000000..03949115ea6 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Bundle/_files/quote_with_bundle_and_options.php @@ -0,0 +1,54 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +use Magento\TestFramework\Helper\Bootstrap; + +require __DIR__ . 'product_with_multiple_options.php'; + +$objectManager = Bootstrap::getObjectManager(); + +/** @var $product \Magento\Catalog\Model\Product */ +$product = $objectManager->create(\Magento\Catalog\Model\Product::class); +$product->load(3); + +/** @var $typeInstance \Magento\Bundle\Model\Product\Type */ +$typeInstance = $product->getTypeInstance(); +$typeInstance->setStoreFilter($product->getStoreId(), $product); +$optionCollection = $typeInstance->getOptionsCollection($product); + +$bundleOptions = []; +$bundleOptionsQty = []; +foreach ($optionCollection as $option) { + /** @var $option \Magento\Bundle\Model\Option */ + $selectionsCollection = $typeInstance->getSelectionsCollection([$option->getId()], $product); + if ($option->isMultiSelection()) { + $bundleOptions[$option->getId()] = array_column($selectionsCollection->toArray(), 'selection_id'); + } else { + $bundleOptions[$option->getId()] = $selectionsCollection->getFirstItem()->getSelectionId(); + } + $bundleOptionsQty[$option->getId()] = 1; +} + +$requestInfo = new \Magento\Framework\DataObject( + [ + 'product' => $product->getId(), + 'bundle_option' => $bundleOptions, + 'bundle_option_qty' => $bundleOptionsQty, + 'qty' => 1, + ] +); + +/** @var $cart \Magento\Checkout\Model\Cart */ +$cart = Bootstrap::getObjectManager()->create(\Magento\Checkout\Model\Cart::class); +$cart->addProduct($product, $requestInfo); +$cart->getQuote()->setReservedOrderId('test_cart_with_bundle_and_options'); +$cart->save(); + +/** @var $objectManager \Magento\TestFramework\ObjectManager */ +$objectManager = Bootstrap::getObjectManager(); +$objectManager->removeSharedInstance(\Magento\Checkout\Model\Session::class); diff --git a/dev/tests/integration/testsuite/Magento/Bundle/_files/quote_with_bundle_and_options_rollback.php b/dev/tests/integration/testsuite/Magento/Bundle/_files/quote_with_bundle_and_options_rollback.php new file mode 100644 index 00000000000..d32d6fab333 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Bundle/_files/quote_with_bundle_and_options_rollback.php @@ -0,0 +1,28 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +/** @var \Magento\Framework\Registry $registry */ +$registry = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->get(\Magento\Framework\Registry::class); + +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', true); + +/** @var $objectManager \Magento\TestFramework\ObjectManager */ +$objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); +$quote = $objectManager->create(\Magento\Quote\Model\Quote::class); +$quote->load('test_cart_with_bundle_and_options', 'reserved_order_id'); +$quote->delete(); + +/** @var \Magento\Quote\Model\QuoteIdMask $quoteIdMask */ +$quoteIdMask = $objectManager->create(\Magento\Quote\Model\QuoteIdMask::class); +$quoteIdMask->delete($quote->getId()); + +require __DIR__ . 'product_with_multiple_options_rollback.php'; + +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', false); diff --git a/dev/tests/integration/testsuite/Magento/BundleImportExport/Model/Export/RowCustomizerTest.php b/dev/tests/integration/testsuite/Magento/BundleImportExport/Model/Export/RowCustomizerTest.php index 6f81421c902..53e8281ffbd 100644 --- a/dev/tests/integration/testsuite/Magento/BundleImportExport/Model/Export/RowCustomizerTest.php +++ b/dev/tests/integration/testsuite/Magento/BundleImportExport/Model/Export/RowCustomizerTest.php @@ -20,6 +20,9 @@ class RowCustomizerTest extends \PHPUnit\Framework\TestCase */ private $objectManager; + /** + * @inheritdoc + */ protected function setUp() { $this->objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); @@ -30,8 +33,10 @@ protected function setUp() /** * @magentoDataFixture Magento/Bundle/_files/product.php + * + * @return void */ - public function testPrepareData() + public function testPrepareData(): void { $parsedAdditionalAttributes = 'text_attribute=!@#$%^&*()_+1234567890-=|\\:;"\'<,>.?/' . ',text_attribute2=,'; @@ -56,4 +61,56 @@ public function testPrepareData() $this->assertEquals([], $this->model->addData([], $ids['simple'])); $this->assertEquals($parsedAdditionalAttributes, $result['additional_attributes']); } + + /** + * @magentoDataFixture Magento/Store/_files/second_store.php + * @magentoDataFixture Magento/Bundle/_files/product.php + * @magentoDbIsolation disabled + * + * @return void + */ + public function testPrepareDataWithDifferentStoreValues(): void + { + $storeCode = 'default'; + $expectedNames = [ + 'name' => 'Bundle Product Items', + 'name_' . $storeCode => 'Bundle Product Items_' . $storeCode, + ]; + $parsedAdditionalAttributes = 'text_attribute=!@#$%^&*()_+1234567890-=|\\:;"\'<,>.?/' + . ',text_attribute2=,'; + $allAdditionalAttributes = $parsedAdditionalAttributes . ',weight_type=0,price_type=1'; + $collection = $this->objectManager->get(\Magento\Catalog\Model\ResourceModel\Product\Collection::class); + /** @var \Magento\Store\Model\Store $store */ + $store = $this->objectManager->create(\Magento\Store\Model\Store::class); + $store->load($storeCode, 'code'); + /** @var \Magento\Catalog\Api\ProductRepositoryInterface $productRepository */ + $productRepository = $this->objectManager->get(\Magento\Catalog\Api\ProductRepositoryInterface::class); + $product = $productRepository->get('bundle-product', 1, $store->getId()); + + $extension = $product->getExtensionAttributes(); + $options = $extension->getBundleProductOptions(); + + foreach ($options as $productOption) { + $productOption->setTitle($productOption->getTitle() . '_' . $store->getCode()); + } + $extension->setBundleProductOptions($options); + $product->setExtensionAttributes($extension); + $productRepository->save($product); + $this->model->prepareData($collection, [$product->getId()]); + $result = $this->model->addData(['additional_attributes' => $allAdditionalAttributes], $product->getId()); + $bundleValues = array_map( + function ($input) { + $data = explode('=', $input); + + return [$data[0] => $data[1]]; + }, + explode(',', $result['bundle_values']) + ); + $actualNames = [ + 'name' => array_column($bundleValues, 'name')[0], + 'name' . '_' . $store->getCode() => array_column($bundleValues, 'name' . '_' . $store->getCode())[0], + ]; + + self::assertSame($expectedNames, $actualNames); + } } diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Controller/Adminhtml/Product/AttributeTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Controller/Adminhtml/Product/AttributeTest.php index fdc85941b32..4261873cc8e 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/Controller/Adminhtml/Product/AttributeTest.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/Controller/Adminhtml/Product/AttributeTest.php @@ -5,6 +5,8 @@ */ namespace Magento\Catalog\Controller\Adminhtml\Product; +use Magento\Framework\Exception\LocalizedException; + /** * @magentoAppArea adminhtml * @magentoDbIsolation enabled @@ -224,6 +226,109 @@ public function testSaveActionCleanAttributeLabelCache() $this->assertEquals('new string translation', $this->_translate('string to translate')); } + /** + * Get attribute data preset. + * + * @return array + */ + private function getLargeOptionsSetAttributeData() + { + return [ + 'frontend_label' => [ + 0 => 'testdrop1', + 1 => '', + 2 => '', + ], + 'frontend_input' => 'select', + 'is_required' => '0', + 'update_product_preview_image' => '0', + 'use_product_image_for_swatch' => '0', + 'visual_swatch_validation' => '', + 'visual_swatch_validation_unique' => '', + 'text_swatch_validation' => '', + 'text_swatch_validation_unique' => '', + 'attribute_code' => 'test_many_options', + 'is_global' => '0', + 'default_value_text' => '', + 'default_value_yesno' => '0', + 'default_value_date' => '', + 'default_value_textarea' => '', + 'is_unique' => '0', + 'is_used_in_grid' => '1', + 'is_visible_in_grid' => '1', + 'is_filterable_in_grid' => '1', + 'is_searchable' => '0', + 'is_comparable' => '0', + 'is_filterable' => '0', + 'is_filterable_in_search' => '0', + 'is_used_for_promo_rules' => '0', + 'is_html_allowed_on_front' => '1', + 'is_visible_on_front' => '0', + 'used_in_product_listing' => '0', + 'used_for_sort_by' => '0', + 'swatch_input_type' => 'dropdown', + ]; + } + + /** + * Test attribute saving with large amount of options exceeding maximum allowed by max_input_vars limit. + * @return void + */ + public function testLargeOptionsDataSet() + { + $maxInputVars = ini_get('max_input_vars'); + // Each option is at least 4 variables array (order, admin value, first store view value, delete flag). + // Set options count to exceed max_input_vars by 100 options (400 variables). + $optionsCount = floor($maxInputVars / 4) + 100; + $attributeData = $this->getLargeOptionsSetAttributeData(); + $optionsData = []; + $expectedOptionsLabels = []; + for ($i = 0; $i < $optionsCount; $i++) { + $order = $i + 1; + $expectedOptionLabelOnStoreView = "value_{$i}_store_1"; + $expectedOptionsLabels[$i+1] = $expectedOptionLabelOnStoreView; + $optionsData []= "option[order][option_{$i}]={$order}"; + $optionsData []= "option[value][option_{$i}][0]=value_{$i}_admin"; + $optionsData []= "option[value][option_{$i}][1]={$expectedOptionLabelOnStoreView}"; + $optionsData []= "option[delete][option_{$i}="; + } + $attributeData['serialized_options'] = json_encode($optionsData); + $this->getRequest()->setPostValue($attributeData); + $this->dispatch('backend/catalog/product_attribute/save'); + $entityTypeId = $this->_objectManager->create( + \Magento\Eav\Model\Entity::class + )->setType( + \Magento\Catalog\Model\Product::ENTITY + )->getTypeId(); + + /** @var $attribute \Magento\Catalog\Model\ResourceModel\Eav\Attribute */ + $attribute = $this->_objectManager->create( + \Magento\Catalog\Model\ResourceModel\Eav\Attribute::class + )->setEntityTypeId( + $entityTypeId + ); + try { + $attribute->loadByCode($entityTypeId, 'test_many_options'); + $options = $attribute->getOptions(); + // assert that all options are saved without truncation + $this->assertEquals( + $optionsCount + 1, + count($options), + 'Expected options count does not match (regarding first empty option for non-required attribute)' + ); + + foreach ($expectedOptionsLabels as $optionOrderNum => $label) { + $this->assertEquals( + $label, + $options[$optionOrderNum]->getLabel(), + "Label for option #{$optionOrderNum} does not match expected." + ); + } + } catch (LocalizedException $e) { + $this->fail('Test failed with exception on attribute model load: ' . $e); + } + } + /** * Return translation for a string literal belonging to backend area * diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Model/Indexer/Category/ProductTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Model/Indexer/Category/ProductTest.php index 6efd2638c0d..dab47c818ec 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/Model/Indexer/Category/ProductTest.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/Model/Indexer/Category/ProductTest.php @@ -27,6 +27,11 @@ class ProductTest extends \PHPUnit\Framework\TestCase */ protected $productResource; + /** + * @var \Magento\Catalog\Api\CategoryRepositoryInterface + */ + private $categoryRepository; + protected function setUp() { /** @var \Magento\Framework\Indexer\IndexerInterface indexer */ @@ -39,6 +44,10 @@ protected function setUp() $this->productResource = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->get( \Magento\Catalog\Model\ResourceModel\Product::class ); + + $this->categoryRepository = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( + \Magento\Catalog\Api\CategoryRepositoryInterface::class + ); } /** @@ -200,6 +209,25 @@ public function testCategoryCreate() } } + /** + * @magentoAppArea adminhtml + * @depends testReindexAll + */ + public function testCatalogCategoryProductIndexInvalidateAfterDelete() + { + $indexerShouldBeValid = (bool)$this->indexer->isInvalid(); + + $categories = $this->getCategories(1); + $this->categoryRepository->delete(array_pop($categories)); + + $state = $this->indexer->getState(); + $state->loadByIndexer($this->indexer->getId()); + $status = $state->getStatus(); + + $this->assertFalse($indexerShouldBeValid); + $this->assertEquals(\Magento\Framework\Indexer\StateInterface::STATUS_INVALID, $status); + } + /** * @param int $count * @return Category[] diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Model/ProductPriceTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Model/ProductPriceTest.php index b867b403f34..594133e984a 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/Model/ProductPriceTest.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/Model/ProductPriceTest.php @@ -3,6 +3,11 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +namespace Magento\Catalog\Model; + +use Magento\TestFramework\Helper\Bootstrap; +use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\Catalog\Model\ResourceModel\Product\Collection; /** * Tests product model: @@ -11,8 +16,6 @@ * @see \Magento\Catalog\Model\ProductTest * @see \Magento\Catalog\Model\ProductExternalTest */ -namespace Magento\Catalog\Model; - class ProductPriceTest extends \PHPUnit\Framework\TestCase { /** @@ -22,9 +25,7 @@ class ProductPriceTest extends \PHPUnit\Framework\TestCase protected function setUp() { - $this->_model = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( - \Magento\Catalog\Model\Product::class - ); + $this->_model = Bootstrap::getObjectManager()->create(Product::class); } public function testGetPrice() @@ -72,4 +73,22 @@ public function testSetGetFinalPrice() $this->_model->setFinalPrice(10); $this->assertEquals(10, $this->_model->getFinalPrice()); } + + /** + * @magentoDbIsolation disabled + * @magentoDataFixture Magento/Catalog/_files/product_with_options.php + * @return void + */ + public function testGetMinPrice(): void + { + $productRepository = Bootstrap::getObjectManager()->create(ProductRepositoryInterface::class); + $product = $productRepository->get('simple'); + $collection = Bootstrap::getObjectManager()->create(Collection::class); + $collection->addIdFilter($product->getId()); + $collection->addPriceData(); + $collection->load(); + /** @var \Magento\Catalog\Model\Product $product */ + $product = $collection->getFirstItem(); + $this->assertEquals(333, $product->getData('min_price')); + } } diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/CategoriesTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/CategoriesTest.php index fd0f6e98840..22bb76ca922 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/CategoriesTest.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/CategoriesTest.php @@ -6,6 +6,7 @@ namespace Magento\Catalog\Ui\DataProvider\Product\Form\Modifier; +use Magento\Catalog\Model\Product; use Magento\TestFramework\Helper\CacheCleaner; /** @@ -29,6 +30,8 @@ protected function setUp() $store = $objectManager->create(\Magento\Store\Model\Store::class); $store->load('admin'); $registry->register('current_store', $store); + $product = $objectManager->create(Product::class); + $registry->register('current_product', $product); $this->object = $objectManager->create(Categories::class); } diff --git a/dev/tests/integration/testsuite/Magento/Catalog/_files/product_with_options.php b/dev/tests/integration/testsuite/Magento/Catalog/_files/product_with_options.php index 9b8a629d24c..a6e01370dfe 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/_files/product_with_options.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/_files/product_with_options.php @@ -11,6 +11,8 @@ $product->setTypeId( 'simple' +)->setId( + 1 )->setAttributeSetId( 4 )->setWebsiteIds( diff --git a/dev/tests/integration/testsuite/Magento/CatalogSearch/Model/Indexer/Fulltext/Model/Plugin/CategoryTest.php b/dev/tests/integration/testsuite/Magento/CatalogSearch/Model/Indexer/Fulltext/Model/Plugin/CategoryTest.php new file mode 100644 index 00000000000..ec2a14abafc --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/CatalogSearch/Model/Indexer/Fulltext/Model/Plugin/CategoryTest.php @@ -0,0 +1,69 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\CatalogSearch\Model\Indexer\Fulltext\Model\Plugin; + +use Magento\Catalog\Api\CategoryRepositoryInterface; +use Magento\Catalog\Model\Category; +use Magento\CatalogSearch\Model\Indexer\Fulltext\Processor; +use Magento\TestFramework\Helper\Bootstrap; + +class CategoryTest extends \PHPUnit\Framework\TestCase +{ + /** + * @var Processor + */ + private $indexerProcessor; + + /** + * @var CategoryRepositoryInterface + */ + private $categoryRepository; + + protected function setUp() + { + $this->indexerProcessor = Bootstrap::getObjectManager()->create(Processor::class); + $this->categoryRepository = Bootstrap::getObjectManager()->create(CategoryRepositoryInterface::class); + } + + /** + * @magentoDataFixture Magento/Catalog/_files/indexer_catalog_category.php + * @magentoAppArea adminhtml + */ + public function testIndexerInvalidatedAfterCategoryDelete() + { + $this->indexerProcessor->reindexAll(); + $isIndexerValid = (bool)$this->indexerProcessor->getIndexer()->isValid(); + + $category = $this->getCategories(1); + $this->categoryRepository->delete(array_pop($category)); + + $state = $this->indexerProcessor->getIndexer()->getState(); + $state->loadByIndexer($this->indexerProcessor->getIndexerId()); + $status = $state->getStatus(); + + $this->assertTrue($isIndexerValid); + $this->assertEquals(\Magento\Framework\Indexer\StateInterface::STATUS_INVALID, $status); + } + + /** + * @param int $count + * @return Category[] + */ + private function getCategories($count) + { + /** @var Category $category */ + $category = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( + \Magento\Catalog\Model\Category::class + ); + + $result = $category->getCollection()->addAttributeToSelect('name')->getItems(); + $result = array_slice($result, 2); + + return array_slice($result, 0, $count); + } +} diff --git a/dev/tests/integration/testsuite/Magento/Customer/_files/import_export/customers_for_address_import.php b/dev/tests/integration/testsuite/Magento/Customer/_files/import_export/customers_for_address_import.php index 80b3613378d..9a90061a6de 100644 --- a/dev/tests/integration/testsuite/Magento/Customer/_files/import_export/customers_for_address_import.php +++ b/dev/tests/integration/testsuite/Magento/Customer/_files/import_export/customers_for_address_import.php @@ -4,6 +4,7 @@ * See COPYING.txt for license details. */ //Create customer +/** @var Magento\Customer\Model\Customer $customer */ $customer = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( \Magento\Customer\Model\Customer::class ); @@ -75,5 +76,4 @@ $addressSecond->isObjectNew(true); $customer->addAddress($addressSecond); $customer->setDefaultShipping($addressSecond->getId()); -$customer->isObjectNew(true); $customer->save(); diff --git a/dev/tests/integration/testsuite/Magento/CustomerImportExport/Model/Import/AddressTest.php b/dev/tests/integration/testsuite/Magento/CustomerImportExport/Model/Import/AddressTest.php index 0915506f061..96b7f8ce8ed 100644 --- a/dev/tests/integration/testsuite/Magento/CustomerImportExport/Model/Import/AddressTest.php +++ b/dev/tests/integration/testsuite/Magento/CustomerImportExport/Model/Import/AddressTest.php @@ -132,15 +132,6 @@ public function testConstruct() ); $this->assertAttributeNotEmpty('_attributes', $this->_entityAdapter, 'Attributes must not be empty'); - // check addresses - $this->assertAttributeInternalType( - 'array', - '_addresses', - $this->_entityAdapter, - 'Addresses must be an array.' - ); - $this->assertAttributeNotEmpty('_addresses', $this->_entityAdapter, 'Addresses must not be empty'); - // check country regions and regions $this->assertAttributeInternalType( 'array', @@ -154,62 +145,6 @@ public function testConstruct() $this->assertAttributeNotEmpty('_regions', $this->_entityAdapter, 'Regions must not be empty'); } - /** - * Test _initAddresses - * - * @magentoDataFixture Magento/Customer/_files/import_export/customer_with_addresses.php - */ - public function testInitAddresses() - { - /** @var $objectManager \Magento\TestFramework\ObjectManager */ - $objectManager = Bootstrap::getObjectManager(); - - // get addressed from fixture - $customers = $objectManager->get(\Magento\Framework\Registry::class)->registry($this->_fixtureKey); - $correctAddresses = []; - /** @var $customer \Magento\Customer\Model\Customer */ - foreach ($customers as $customer) { - $correctAddresses[$customer->getId()] = []; - /** @var $address \Magento\Customer\Model\Address */ - foreach ($customer->getAddressesCollection() as $address) { - $correctAddresses[$customer->getId()][] = $address->getId(); - } - } - - // invoke _initAddresses - $initAddresses = new \ReflectionMethod($this->_testClassName, '_initAddresses'); - $initAddresses->setAccessible(true); - $initAddresses->invoke($this->_entityAdapter); - - // check addresses - $this->assertAttributeInternalType( - 'array', - '_addresses', - $this->_entityAdapter, - 'Addresses must be an array.' - ); - $this->assertAttributeNotEmpty('_addresses', $this->_entityAdapter, 'Addresses must not be empty'); - - $addressesReflection = new \ReflectionProperty($this->_testClassName, '_addresses'); - $addressesReflection->setAccessible(true); - $testAddresses = $addressesReflection->getValue($this->_entityAdapter); - - $correctCustomerIds = array_keys($correctAddresses); - $testCustomerIds = array_keys($testAddresses); - sort($correctCustomerIds); - sort($testCustomerIds); - $this->assertEquals($correctCustomerIds, $testCustomerIds, 'Incorrect customer IDs in addresses array.'); - - foreach ($correctCustomerIds as $customerId) { - $this->assertInternalType('array', $correctAddresses[$customerId], 'Addresses must be an array.'); - $correctAddressIds = $correctAddresses[$customerId]; - $testAddressIds = $testAddresses[$customerId]; - sort($correctAddressIds); - sort($testAddressIds); - $this->assertEquals($correctAddressIds, $testAddressIds, 'Incorrect addresses IDs.'); - } - } - /** * Test _saveAddressEntity * diff --git a/dev/tests/integration/testsuite/Magento/CustomerImportExport/_files/two_addresses_rollback.php b/dev/tests/integration/testsuite/Magento/CustomerImportExport/_files/two_addresses_rollback.php index 510ad2e360b..6e371a102d4 100644 --- a/dev/tests/integration/testsuite/Magento/CustomerImportExport/_files/two_addresses_rollback.php +++ b/dev/tests/integration/testsuite/Magento/CustomerImportExport/_files/two_addresses_rollback.php @@ -17,29 +17,6 @@ /** @var Registry $registry */ $registry = Bootstrap::getObjectManager()->get(Registry::class); -//Removing addresses -$registry->unregister('isSecureArea'); -$registry->register('isSecureArea', true); -/** @var Address $customerAddress */ -$customerAddress = Bootstrap::getObjectManager()->create(Address::class); -$customerAddress->load(1); -if ($customerAddress->getId()) { - $customerAddress->delete(); -} -$registry->unregister('isSecureArea'); -$registry->register('isSecureArea', false); -//Second address -$registry->unregister('isSecureArea'); -$registry->register('isSecureArea', true); -/** @var Address $customerAddress */ -$customerAddress = Bootstrap::getObjectManager()->create(Address::class); -$customerAddress->load(2); -if ($customerAddress->getId()) { - $customerAddress->delete(); -} -$registry->unregister('isSecureArea'); -$registry->register('isSecureArea', false); - //Removing customers. $registry->unregister('isSecureArea'); $registry->register('isSecureArea', true); diff --git a/dev/tests/integration/testsuite/Magento/Elasticsearch/Model/Client/ElasticsearchTest.php b/dev/tests/integration/testsuite/Magento/Elasticsearch/Model/Client/ElasticsearchTest.php index 0731871b4bd..61add5f7d0e 100644 --- a/dev/tests/integration/testsuite/Magento/Elasticsearch/Model/Client/ElasticsearchTest.php +++ b/dev/tests/integration/testsuite/Magento/Elasticsearch/Model/Client/ElasticsearchTest.php @@ -100,7 +100,7 @@ private function search($text) */ public function testSearchConfigurableProductBySimpleProductName() { - $this->assertProductWithSkuFound('configurable', $this->search('Configurable OptionOption')); + $this->assertProductWithSkuFound('configurable', $this->search('Configurable Option')); } /** diff --git a/dev/tests/integration/testsuite/Magento/Elasticsearch/Model/Indexer/IndexHandlerTest.php b/dev/tests/integration/testsuite/Magento/Elasticsearch/Model/Indexer/IndexHandlerTest.php index 4cdff533af7..014aaf7679b 100755 --- a/dev/tests/integration/testsuite/Magento/Elasticsearch/Model/Indexer/IndexHandlerTest.php +++ b/dev/tests/integration/testsuite/Magento/Elasticsearch/Model/Indexer/IndexHandlerTest.php @@ -5,13 +5,18 @@ */ namespace Magento\Elasticsearch\Model\Indexer; -use Magento\Catalog\Model\Product; +use Magento\Catalog\Model\Product\Action as ProductAction; +use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\CatalogInventory\Api\StockRegistryInterface; +use Magento\CatalogInventory\Api\StockItemRepositoryInterface; +use Magento\CatalogSearch\Model\Indexer\Fulltext as CatalogSearchFulltextIndexer; use Magento\TestFramework\Helper\Bootstrap; use Magento\Store\Model\StoreManagerInterface; use Magento\Elasticsearch\SearchAdapter\ConnectionManager; use Magento\Elasticsearch\Model\Client\Elasticsearch as ElasticsearchClient; use Magento\Elasticsearch\Model\Config; use Magento\Elasticsearch\SearchAdapter\SearchIndexNameResolver; +use Magento\Indexer\Model\Indexer; /** * Important: Please make sure that each integration test file works with unique elastic search index. In order to @@ -20,107 +25,79 @@ * * @magentoDbIsolation disabled * @magentoDataFixture Magento/Elasticsearch/_files/indexer.php + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class IndexHandlerTest extends \PHPUnit\Framework\TestCase { /** - * @var ConnectionManager + * @var ProductRepositoryInterface */ - protected $connectionManager; + private $productRepository; /** * @var ElasticsearchClient */ - protected $client; + private $client; /** * @var StoreManagerInterface */ - protected $storeManager; + private $storeManager; /** * @var int[] */ - protected $storeIds; + private $storeIds; /** - * @var Config + * @var string */ - protected $clientConfig; + private $entityType; /** - * @var SearchIndexNameResolver - */ - protected $searchIndexNameResolver; - - /** - * @var Product - */ - protected $productApple; - - /** - * @var Product - */ - protected $productBanana; - - /** - * @var Product - */ - protected $productOrange; - - /** - * @var Product + * @var Indexer */ - protected $productPapaya; + private $indexer; /** - * @var Product + * @var SearchIndexNameResolver */ - protected $productCherry; + private $searchIndexNameResolver; /** - * Setup method + * {@inheritdoc} */ protected function setUp() { - $this->connectionManager = Bootstrap::getObjectManager()->create( - \Magento\Elasticsearch\SearchAdapter\ConnectionManager::class - ); - - $this->client = $this->connectionManager->getConnection(); + $connectionManager = Bootstrap::getObjectManager()->create(ConnectionManager::class); + $this->client = $connectionManager->getConnection(); - $this->storeManager = Bootstrap::getObjectManager()->create( - \Magento\Store\Model\StoreManagerInterface::class - ); + $this->storeManager = Bootstrap::getObjectManager()->create(StoreManagerInterface::class); $this->storeIds = array_keys($this->storeManager->getStores()); - $this->clientConfig = Bootstrap::getObjectManager()->create( - \Magento\Elasticsearch\Model\Config::class - ); + $clientConfig = Bootstrap::getObjectManager()->create(Config::class); + $this->entityType = $clientConfig->getEntityType(); - $this->searchIndexNameResolver = Bootstrap::getObjectManager()->create( - \Magento\Elasticsearch\SearchAdapter\SearchIndexNameResolver::class - ); + $this->indexer = Bootstrap::getObjectManager()->create(Indexer::class); + $this->indexer->load(CatalogSearchFulltextIndexer::INDEXER_ID); + $this->indexer->reindexAll(); - $this->productApple = $this->getProductBySku('fulltext-1'); - $this->productBanana = $this->getProductBySku('fulltext-2'); - $this->productOrange = $this->getProductBySku('fulltext-3'); - $this->productPapaya = $this->getProductBySku('fulltext-4'); - $this->productCherry = $this->getProductBySku('fulltext-5'); + $this->searchIndexNameResolver = Bootstrap::getObjectManager()->create(SearchIndexNameResolver::class); + $this->productRepository = Bootstrap::getObjectManager()->create(ProductRepositoryInterface::class); } /** - * Test reindex process * @magentoConfigFixture default/catalog/search/engine elasticsearch * @magentoConfigFixture current_store catalog/search/elasticsearch_index_prefix indexerhandlertest + * @return void */ - public function testReindexAll() + public function testReindexAll(): void { - $this->reindexAll(); + $productApple = $this->productRepository->get('fulltext-1'); foreach ($this->storeIds as $storeId) { $products = $this->searchByName('Apple', $storeId); $this->assertCount(1, $products); - $this->assertEquals($this->productApple->getId(), $products[0]['_id']); + $this->assertEquals($productApple->getId(), $products[0]['_id']); $products = $this->searchByName('Simple Product', $storeId); $this->assertCount(5, $products); @@ -128,19 +105,17 @@ public function testReindexAll() } /** + * @magentoAppIsolation enabled * @magentoConfigFixture default/catalog/search/engine elasticsearch * @magentoConfigFixture current_store catalog/search/elasticsearch_index_prefix indexerhandlertest + * @return void */ - public function testReindexRowAfterEdit() + public function testReindexRowAfterEdit(): void { - // The test executes fine locally. On bamboo there is some issue with parallel test execution or other - // test interaction. It is being marked as skipped until more time is available to investigate and - // fix the issue. - $this->markTestSkipped('MAGETWO-53851 - Ticket to investiage this test failure on Bamboo and fix it.'); - - $this->productApple->setData('name', 'Simple Product Cucumber'); - $this->productApple->save(); - $this->reindexAll(); + $this->storeManager->setCurrentStore('admin'); + $productApple = $this->productRepository->get('fulltext-1'); + $productApple->setName('Simple Product Cucumber'); + $this->productRepository->save($productApple); foreach ($this->storeIds as $storeId) { $products = $this->searchByName('Apple', $storeId); @@ -148,7 +123,7 @@ public function testReindexRowAfterEdit() $products = $this->searchByName('Cucumber', $storeId); $this->assertCount(1, $products); - $this->assertEquals($this->productApple->getId(), $products[0]['_id']); + $this->assertEquals($productApple->getId(), $products[0]['_id']); $products = $this->searchByName('Simple Product', $storeId); $this->assertCount(5, $products); @@ -158,22 +133,21 @@ public function testReindexRowAfterEdit() /** * @magentoConfigFixture default/catalog/search/engine elasticsearch * @magentoConfigFixture current_store catalog/search/elasticsearch_index_prefix indexerhandlertest + * @return void */ - public function testReindexRowAfterMassAction() + public function testReindexRowAfterMassAction(): void { - $this->reindexAll(); + $productApple = $this->productRepository->get('fulltext-1'); + $productBanana = $this->productRepository->get('fulltext-2'); $productIds = [ - $this->productApple->getId(), - $this->productBanana->getId(), + $productApple->getId(), + $productBanana->getId(), ]; $attrData = [ 'name' => 'Simple Product Common', ]; - - /** @var \Magento\Catalog\Model\Product\Action $action */ - $action = Bootstrap::getObjectManager()->get( - \Magento\Catalog\Model\Product\Action::class - ); + /** @var ProductAction $action */ + $action = Bootstrap::getObjectManager()->get(ProductAction::class); foreach ($this->storeIds as $storeId) { $action->updateAttributes($productIds, $attrData, $storeId); @@ -199,30 +173,67 @@ public function testReindexRowAfterMassAction() * @magentoConfigFixture default/catalog/search/engine elasticsearch * @magentoConfigFixture current_store catalog/search/elasticsearch_index_prefix indexerhandlertest * @magentoAppArea adminhtml + * @return void */ - public function testReindexRowAfterDelete() + public function testReindexRowAfterDelete(): void { - $this->reindexAll(); - $this->productBanana->delete(); + $productBanana = $this->productRepository->get('fulltext-2'); + $this->productRepository->delete($productBanana); foreach ($this->storeIds as $storeId) { + $products = $this->searchByName('Banana', $storeId); + $this->assertEmpty($products); + $products = $this->searchByName('Simple Product', $storeId); $this->assertCount(4, $products); } } /** - * Search docs in Elasticsearch by name + * @magentoDbIsolation enabled + * @magentoAppArea adminhtml + * @magentoConfigFixture default/catalog/search/engine elasticsearch + * @magentoConfigFixture current_store catalog/search/elasticsearch_index_prefix indexerhandlertest + * @magentoDataFixture Magento/Elasticsearch/_files/configurable_products.php + * @return void + */ + public function testReindexRowAfterUpdateStockStatus(): void + { + foreach ($this->storeIds as $storeId) { + $products = $this->searchByName('ProductOption1', $storeId); + $this->assertNotEmpty($products); + } + $product = $this->productRepository->get('simple_10'); + /** @var StockRegistryInterface $stockRegistry */ + $stockRegistry = Bootstrap::getObjectManager()->create(StockRegistryInterface::class); + $stockItem = $stockRegistry->getStockItem($product->getId()); + $stockItem->setIsInStock(false); + /** @var StockItemRepositoryInterface $stockRepository */ + $stockRepository = Bootstrap::getObjectManager()->create(StockItemRepositoryInterface::class); + $stockRepository->save($stockItem); + + foreach ($this->storeIds as $storeId) { + $products = $this->searchByName('ProductOption1', $storeId); + $this->assertEmpty($products); + + $products = $this->searchByName('Configurable', $storeId); + $this->assertNotEmpty($products); + } + } + + /** + * Search docs in Elasticsearch by name. * * @param string $text * @param int $storeId * @return array */ - protected function searchByName($text, $storeId) + private function searchByName(string $text, int $storeId): array { + $index = $this->searchIndexNameResolver->getIndexName($storeId, $this->indexer->getId()); $searchQuery = [ - 'index' => $this->searchIndexNameResolver->getIndexName($storeId, 'catalogsearch_fulltext'), - 'type' => $this->clientConfig->getEntityType(), + 'index' => $index, + 'type' => $this->entityType, 'body' => [ 'query' => [ 'bool' => [ @@ -240,35 +251,7 @@ protected function searchByName($text, $storeId) ]; $queryResult = $this->client->query($searchQuery); $products = isset($queryResult['hits']['hits']) ? $queryResult['hits']['hits'] : []; - return $products; - } - - /** - * Return product by SKU - * - * @param string $sku - * @return Product - */ - protected function getProductBySku($sku) - { - /** @var Product $product */ - $product = Bootstrap::getObjectManager()->get( - \Magento\Catalog\Model\Product::class - ); - return $product->loadByAttribute('sku', $sku); - } - /** - * Perform full reindex - * - * @return void - */ - private function reindexAll() - { - $indexer = Bootstrap::getObjectManager()->create( - \Magento\Indexer\Model\Indexer::class - ); - $indexer->load('catalogsearch_fulltext'); - $indexer->reindexAll(); + return $products; } } diff --git a/dev/tests/integration/testsuite/Magento/Elasticsearch/_files/configurable_attribute.php b/dev/tests/integration/testsuite/Magento/Elasticsearch/_files/configurable_attribute.php new file mode 100644 index 00000000000..7ec53d9099d --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Elasticsearch/_files/configurable_attribute.php @@ -0,0 +1,62 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\TestFramework\Helper\Bootstrap; +use Magento\Eav\Api\AttributeRepositoryInterface; + +$eavConfig = Bootstrap::getObjectManager()->get(\Magento\Eav\Model\Config::class); +$attribute = $eavConfig->getAttribute('catalog_product', 'test_configurable'); + +$eavConfig->clear(); + +/** @var $installer \Magento\Catalog\Setup\CategorySetup */ +$installer = Bootstrap::getObjectManager()->create(\Magento\Catalog\Setup\CategorySetup::class); + +if (!$attribute->getId()) { + /** @var $attribute \Magento\Catalog\Model\ResourceModel\Eav\Attribute */ + $attribute = Bootstrap::getObjectManager()->create( + \Magento\Catalog\Model\ResourceModel\Eav\Attribute::class + ); + + /** @var AttributeRepositoryInterface $attributeRepository */ + $attributeRepository = Bootstrap::getObjectManager()->create(AttributeRepositoryInterface::class); + + $attribute->setData( + [ + 'attribute_code' => 'test_configurable', + 'entity_type_id' => $installer->getEntityTypeId('catalog_product'), + 'is_global' => 1, + 'is_user_defined' => 1, + 'frontend_input' => 'select', + 'is_unique' => 0, + 'is_required' => 0, + 'is_searchable' => 0, + 'is_visible_in_advanced_search' => 0, + 'is_comparable' => 0, + 'is_filterable' => 0, + 'is_filterable_in_search' => 0, + 'is_used_for_promo_rules' => 0, + 'is_html_allowed_on_front' => 1, + 'is_visible_on_front' => 0, + 'used_in_product_listing' => 0, + 'used_for_sort_by' => 0, + 'frontend_label' => ['Test Configurable'], + 'backend_type' => 'int', + 'option' => [ + 'value' => ['option_0' => ['Option 1'], 'option_1' => ['Option 2']], + 'order' => ['option_0' => 1, 'option_1' => 2], + ], + ] + ); + + $attributeRepository->save($attribute); + + /* Assign attribute to attribute set */ + $installer->addAttributeToGroup('catalog_product', 'Default', 'General', $attribute->getId()); +} + +$eavConfig->clear(); diff --git a/dev/tests/integration/testsuite/Magento/Elasticsearch/_files/configurable_attribute_rollback.php b/dev/tests/integration/testsuite/Magento/Elasticsearch/_files/configurable_attribute_rollback.php new file mode 100644 index 00000000000..7bdfbc6d7f9 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Elasticsearch/_files/configurable_attribute_rollback.php @@ -0,0 +1,24 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +/** @var \Magento\Framework\Registry $registry */ +$registry = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->get(\Magento\Framework\Registry::class); + +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', true); + +$eavConfig = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->get(\Magento\Eav\Model\Config::class); +$attribute = $eavConfig->getAttribute('catalog_product', 'test_configurable'); +if ($attribute instanceof \Magento\Eav\Model\Entity\Attribute\AbstractAttribute + && $attribute->getId() +) { + $attribute->delete(); +} +$eavConfig->clear(); + +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', false); diff --git a/dev/tests/integration/testsuite/Magento/Elasticsearch/_files/configurable_products.php b/dev/tests/integration/testsuite/Magento/Elasticsearch/_files/configurable_products.php index c2dd3c2f879..f8872b02ba2 100644 --- a/dev/tests/integration/testsuite/Magento/Elasticsearch/_files/configurable_products.php +++ b/dev/tests/integration/testsuite/Magento/Elasticsearch/_files/configurable_products.php @@ -17,7 +17,7 @@ require __DIR__ . '/select_attribute.php'; require __DIR__ . '/multiselect_attribute.php'; -require __DIR__ . '/../../ConfigurableProduct/_files/configurable_attribute.php'; +require __DIR__ . '/configurable_attribute.php'; $objectManager = Bootstrap::getObjectManager(); @@ -45,7 +45,7 @@ ->setId($productId) ->setAttributeSetId($attributeSetId) ->setWebsiteIds([1]) - ->setName('Configurable Option' . $option->getLabel()) + ->setName('Configurable Option Product' . str_replace(' ', '', $option->getLabel())) ->setSku('simple_' . $productId) ->setPrice($productId) ->setTestConfigurable($option->getValue()) diff --git a/dev/tests/integration/testsuite/Magento/Elasticsearch/_files/configurable_products_rollback.php b/dev/tests/integration/testsuite/Magento/Elasticsearch/_files/configurable_products_rollback.php index 0d062c9d3f4..e73d2ab1b59 100644 --- a/dev/tests/integration/testsuite/Magento/Elasticsearch/_files/configurable_products_rollback.php +++ b/dev/tests/integration/testsuite/Magento/Elasticsearch/_files/configurable_products_rollback.php @@ -25,7 +25,7 @@ } } -require __DIR__ . '/../../ConfigurableProduct/_files/configurable_attribute_rollback.php'; +require __DIR__ . '/configurable_attribute_rollback.php'; require __DIR__ . '/select_attribute_rollback.php'; require __DIR__ . '/multiselect_attribute_rollback.php'; diff --git a/dev/tests/integration/testsuite/Magento/Elasticsearch/_files/indexer.php b/dev/tests/integration/testsuite/Magento/Elasticsearch/_files/indexer.php index 8ee3a409150..b5c86a63fa4 100644 --- a/dev/tests/integration/testsuite/Magento/Elasticsearch/_files/indexer.php +++ b/dev/tests/integration/testsuite/Magento/Elasticsearch/_files/indexer.php @@ -52,7 +52,7 @@ ->setStockData(['use_config_manage_stock' => 0]) ->save(); -/** @var $productFirst \Magento\Catalog\Model\Product */ +/** @var $productSecond \Magento\Catalog\Model\Product */ $productSecond = $objectManager->create(\Magento\Catalog\Model\Product::class); $productSecond->setTypeId('simple') ->setAttributeSetId(4) @@ -68,7 +68,7 @@ ->setStockData(['use_config_manage_stock' => 0]) ->save(); -/** @var $productFirst \Magento\Catalog\Model\Product */ +/** @var $productThird \Magento\Catalog\Model\Product */ $productThird = $objectManager->create(\Magento\Catalog\Model\Product::class); $productThird->setTypeId('simple') ->setAttributeSetId(4) @@ -84,7 +84,7 @@ ->setStockData(['use_config_manage_stock' => 0]) ->save(); -/** @var $productFirst \Magento\Catalog\Model\Product */ +/** @var $productFourth \Magento\Catalog\Model\Product */ $productFourth = $objectManager->create(\Magento\Catalog\Model\Product::class); $productFourth->setTypeId('simple') ->setAttributeSetId(4) @@ -100,7 +100,7 @@ ->setStockData(['use_config_manage_stock' => 0]) ->save(); -/** @var $productFirst \Magento\Catalog\Model\Product */ +/** @var $productFifth \Magento\Catalog\Model\Product */ $productFifth = $objectManager->create(\Magento\Catalog\Model\Product::class); $productFifth->setTypeId('simple') ->setAttributeSetId(4) diff --git a/dev/tests/integration/testsuite/Magento/Elasticsearch/_files/requests.xml b/dev/tests/integration/testsuite/Magento/Elasticsearch/_files/requests.xml index c40ac9e8b9b..0aaaf9b8585 100644 --- a/dev/tests/integration/testsuite/Magento/Elasticsearch/_files/requests.xml +++ b/dev/tests/integration/testsuite/Magento/Elasticsearch/_files/requests.xml @@ -394,13 +394,18 @@ <queries> <query xsi:type="boolQuery" name="filter_out_of_stock_child" boost="1"> <queryReference clause="must" ref="test_configurable"/> + <queryReference clause="must" ref="visibility"/> </query> <query xsi:type="filteredQuery" name="test_configurable"> <filterReference clause="must" ref="test_configurable_filter"/> </query> + <query xsi:type="filteredQuery" name="visibility"> + <filterReference clause="must" ref="visibility_filter"/> + </query> </queries> <filters> <filter xsi:type="termFilter" name="test_configurable_filter" field="test_configurable" value="$test_configurable$"/> + <filter xsi:type="termFilter" name="visibility_filter" field="visibility" value="$visibility$"/> </filters> <aggregations/> <from>0</from> diff --git a/dev/tests/integration/testsuite/Magento/Framework/Search/Adapter/Mysql/AdapterTest.php b/dev/tests/integration/testsuite/Magento/Framework/Search/Adapter/Mysql/AdapterTest.php index ec1538e950c..ecbce25cd33 100644 --- a/dev/tests/integration/testsuite/Magento/Framework/Search/Adapter/Mysql/AdapterTest.php +++ b/dev/tests/integration/testsuite/Magento/Framework/Search/Adapter/Mysql/AdapterTest.php @@ -63,6 +63,10 @@ protected function setUp() ); $this->adapter = $this->createAdapter(); + + $indexer = $this->objectManager->create(\Magento\Indexer\Model\Indexer::class); + $indexer->load('catalogsearch_fulltext'); + $indexer->reindexAll(); } /** @@ -206,7 +210,7 @@ public function testMatchQueryFilters() public function testRangeFilterWithAllFields() { $this->requestBuilder->bind('range_filter_from', 11); - $this->requestBuilder->bind('range_filter_to', 16); + $this->requestBuilder->bind('range_filter_to', 17); $this->requestBuilder->setRequestName('range_filter'); $queryResponse = $this->executeQuery(); @@ -263,7 +267,7 @@ public function testTermFilter() */ public function testTermFilterArray() { - $this->requestBuilder->bind('request.price', [16, 18]); + $this->requestBuilder->bind('request.price', [17, 18]); $this->requestBuilder->setRequestName('term_filter'); $queryResponse = $this->executeQuery(); @@ -310,13 +314,13 @@ public function testSearchLimit() public function testBoolFilter() { $expectedIds = [2, 3]; - $this->requestBuilder->bind('must_range_filter1_from', 12); + $this->requestBuilder->bind('must_range_filter1_from', 13); $this->requestBuilder->bind('must_range_filter1_to', 22); - $this->requestBuilder->bind('should_term_filter1', 12); - $this->requestBuilder->bind('should_term_filter2', 14); - $this->requestBuilder->bind('should_term_filter3', 16); + $this->requestBuilder->bind('should_term_filter1', 13); + $this->requestBuilder->bind('should_term_filter2', 15); + $this->requestBuilder->bind('should_term_filter3', 17); $this->requestBuilder->bind('should_term_filter4', 18); - $this->requestBuilder->bind('not_term_filter1', 12); + $this->requestBuilder->bind('not_term_filter1', 13); $this->requestBuilder->bind('not_term_filter2', 18); $this->requestBuilder->setRequestName('bool_filter'); @@ -335,7 +339,7 @@ public function testBoolFilterWithNestedNegativeBoolFilter() $expectedIds = [1]; $this->requestBuilder->bind('not_range_filter_from', 14); $this->requestBuilder->bind('not_range_filter_to', 20); - $this->requestBuilder->bind('nested_not_term_filter', 12); + $this->requestBuilder->bind('nested_not_term_filter', 13); $this->requestBuilder->setRequestName('bool_filter_with_nested_bool_filter'); $queryResponse = $this->executeQuery(); @@ -531,9 +535,15 @@ public function testAdvancedSearchCompositeProductWithOutOfStockOption() ->create(Collection::class) ->setAttributeFilter($attribute->getId()); + $visibility = [ + \Magento\Catalog\Model\Product\Visibility::VISIBILITY_IN_SEARCH, + \Magento\Catalog\Model\Product\Visibility::VISIBILITY_BOTH, + ]; + $firstOption = $selectOptions->getFirstItem(); $firstOptionId = $firstOption->getId(); $this->requestBuilder->bind('test_configurable', $firstOptionId); + $this->requestBuilder->bind('visibility', $visibility); $this->requestBuilder->setRequestName('filter_out_of_stock_child'); $queryResponse = $this->executeQuery(); @@ -542,6 +552,7 @@ public function testAdvancedSearchCompositeProductWithOutOfStockOption() $secondOption = $selectOptions->getLastItem(); $secondOptionId = $secondOption->getId(); $this->requestBuilder->bind('test_configurable', $secondOptionId); + $this->requestBuilder->bind('visibility', $visibility); $this->requestBuilder->setRequestName('filter_out_of_stock_child'); $queryResponse = $this->executeQuery(); diff --git a/dev/tests/integration/testsuite/Magento/Framework/Search/_files/product_configurable.php b/dev/tests/integration/testsuite/Magento/Framework/Search/_files/product_configurable.php index 2f30cba3d8e..8a7c5246f37 100644 --- a/dev/tests/integration/testsuite/Magento/Framework/Search/_files/product_configurable.php +++ b/dev/tests/integration/testsuite/Magento/Framework/Search/_files/product_configurable.php @@ -15,8 +15,6 @@ use Magento\Eav\Api\Data\AttributeOptionInterface; use Magento\TestFramework\Helper\Bootstrap; -Bootstrap::getInstance()->reinitialize(); - require __DIR__ . '/configurable_attribute.php'; /** @var ProductRepositoryInterface $productRepository */ diff --git a/dev/tests/integration/testsuite/Magento/Framework/Search/_files/requests.xml b/dev/tests/integration/testsuite/Magento/Framework/Search/_files/requests.xml index 70f9ac75b07..dcf3cd58250 100644 --- a/dev/tests/integration/testsuite/Magento/Framework/Search/_files/requests.xml +++ b/dev/tests/integration/testsuite/Magento/Framework/Search/_files/requests.xml @@ -394,13 +394,18 @@ <queries> <query xsi:type="boolQuery" name="filter_out_of_stock_child" boost="1"> <queryReference clause="must" ref="test_configurable"/> + <queryReference clause="must" ref="visibility"/> </query> <query xsi:type="filteredQuery" name="test_configurable"> <filterReference clause="must" ref="test_configurable_filter"/> </query> + <query xsi:type="filteredQuery" name="visibility"> + <filterReference clause="must" ref="visibility_filter"/> + </query> </queries> <filters> <filter xsi:type="termFilter" name="test_configurable_filter" field="test_configurable" value="$test_configurable$"/> + <filter xsi:type="termFilter" name="visibility_filter" field="visibility" value="$visibility$"/> </filters> <aggregations/> <from>0</from> diff --git a/dev/tests/integration/testsuite/Magento/Framework/Search/_files/search_weight_products.php b/dev/tests/integration/testsuite/Magento/Framework/Search/_files/search_weight_products.php index b672fbe9f8c..3902e78d1fb 100644 --- a/dev/tests/integration/testsuite/Magento/Framework/Search/_files/search_weight_products.php +++ b/dev/tests/integration/testsuite/Magento/Framework/Search/_files/search_weight_products.php @@ -7,18 +7,8 @@ */ use Magento\Catalog\Api\ProductRepositoryInterface; -use Magento\Catalog\Model\Product; -use Magento\Catalog\Model\Product\Attribute\Source\Status; -use Magento\Catalog\Model\Product\Type; -use Magento\Catalog\Model\Product\Visibility; -use Magento\Catalog\Setup\CategorySetup; -use Magento\ConfigurableProduct\Helper\Product\Options\Factory; -use Magento\ConfigurableProduct\Model\Product\Type\Configurable; -use Magento\Eav\Api\Data\AttributeOptionInterface; use Magento\TestFramework\Helper\Bootstrap; -Bootstrap::getInstance()->reinitialize(); - $objectManager = Bootstrap::getObjectManager(); /** @var ProductRepositoryInterface $productRepository */ diff --git a/dev/tests/integration/testsuite/Magento/SalesRule/Model/Rule/Condition/ProductTest.php b/dev/tests/integration/testsuite/Magento/SalesRule/Model/Rule/Condition/ProductTest.php index 02a4ba4c282..98826adeb21 100644 --- a/dev/tests/integration/testsuite/Magento/SalesRule/Model/Rule/Condition/ProductTest.php +++ b/dev/tests/integration/testsuite/Magento/SalesRule/Model/Rule/Condition/ProductTest.php @@ -58,6 +58,28 @@ public function testValidateCategorySalesRuleIncludesChildren($categoryId, $expe $this->assertEquals($expectedResult, $rule->validate($quote)); } + /** + * @magentoDbIsolation disabled + * @magentoDataFixture Magento/Bundle/_files/order_item_with_bundle_and_options.php + * @magentoDataFixture Magento/SalesRule/_files/rules_sku_exclude.php + * + * @return void + */ + public function testValidateSalesRuleExcludesBundleChildren(): void + { + // Load the quote that contains a child of a bundle product + /** @var \Magento\Quote\Model\Quote $quote */ + $quote = $this->objectManager->create(\Magento\Quote\Model\Quote::class) + ->load('test_cart_with_bundle_and_options', 'reserved_order_id'); + + // Load the SalesRule looking for excluding products with selected sku + /** @var $rule \Magento\SalesRule\Model\Rule */ + $rule = $this->objectManager->get(\Magento\Framework\Registry::class) + ->registry('_fixture/Magento_SalesRule_Sku_Exclude'); + + $this->assertEquals(false, $rule->validate($quote)); + } + /** * @return array */ diff --git a/dev/tests/integration/testsuite/Magento/SalesRule/_files/rules_sku_exclude.php b/dev/tests/integration/testsuite/Magento/SalesRule/_files/rules_sku_exclude.php new file mode 100644 index 00000000000..9d88fe48ae1 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/SalesRule/_files/rules_sku_exclude.php @@ -0,0 +1,79 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +/** @var \Magento\Eav\Api\AttributeRepositoryInterface $repository */ +$repository = \Magento\TestFramework\Helper\Bootstrap::getObjectManager() + ->create(\Magento\Eav\Api\AttributeRepositoryInterface::class); + +/** @var \Magento\Eav\Api\Data\AttributeInterface $skuAttribute */ +$skuAttribute = $repository->get( + 'catalog_product', + 'sku' +); +$data = $skuAttribute->getData(); +$data['is_used_for_promo_rules'] = 1; +$skuAttribute->setData($data); +$skuAttribute->save(); + +/** @var \Magento\SalesRule\Model\Rule $rule */ +$salesRule = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create(\Magento\SalesRule\Model\Rule::class); +$salesRule->setData( + [ + 'name' => '20% Off', + 'is_active' => 1, + 'customer_group_ids' => [\Magento\Customer\Model\GroupManagement::NOT_LOGGED_IN_ID], + 'coupon_type' => \Magento\SalesRule\Model\Rule::COUPON_TYPE_NO_COUPON, + 'simple_action' => 'by_percent', + 'discount_amount' => 20, + 'discount_step' => 0, + 'stop_rules_processing' => 1, + 'website_ids' => [ + \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->get( + \Magento\Store\Model\StoreManagerInterface::class + )->getWebsite()->getId(), + ], + ] +); + +$salesRule->getConditions()->loadArray([ + 'type' => \Magento\SalesRule\Model\Rule\Condition\Combine::class, + 'attribute' => null, + 'operator' => null, + 'value' => '1', + 'is_value_processed' => null, + 'aggregator' => 'all', + 'conditions' => + [ + [ + 'type' => \Magento\SalesRule\Model\Rule\Condition\Product\Found::class, + 'attribute' => null, + 'operator' => null, + 'value' => '1', + 'is_value_processed' => null, + 'aggregator' => 'all', + 'conditions' => + [ + [ + 'type' => \Magento\SalesRule\Model\Rule\Condition\Product::class, + 'attribute' => 'sku', + 'operator' => '!=', + 'value' => 'product-bundle', + 'is_value_processed' => false, + ], + ], + ], + ], +]); + +$salesRule->save(); + +/** @var Magento\Framework\Registry $registry */ +$registry = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->get(\Magento\Framework\Registry::class); + +$registry->unregister('_fixture/Magento_SalesRule_Sku_Exclude'); +$registry->register('_fixture/Magento_SalesRule_Sku_Exclude', $salesRule); diff --git a/dev/tests/integration/testsuite/Magento/SalesRule/_files/rules_sku_exclude_rollback.php b/dev/tests/integration/testsuite/Magento/SalesRule/_files/rules_sku_exclude_rollback.php new file mode 100644 index 00000000000..79ec3259fd8 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/SalesRule/_files/rules_sku_exclude_rollback.php @@ -0,0 +1,29 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +/** @var \Magento\Eav\Api\AttributeRepositoryInterface $repository */ +$repository = \Magento\TestFramework\Helper\Bootstrap::getObjectManager() + ->create(\Magento\Eav\Api\AttributeRepositoryInterface::class); + +/** @var \Magento\Eav\Api\Data\AttributeInterface $skuAttribute */ +$skuAttribute = $repository->get( + 'catalog_product', + 'sku' +); +$data = $skuAttribute->getData(); +$data['is_used_for_promo_rules'] = 0; +$skuAttribute->setData($data); +$skuAttribute->save(); + +/** @var Magento\Framework\Registry $registry */ +$registry = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->get(\Magento\Framework\Registry::class); + +/** @var Magento\SalesRule\Model\Rule $rule */ +$rule = $registry->registry('_fixture/Magento_SalesRule_Sku_Exclude'); + +$rule->delete(); diff --git a/dev/tests/integration/testsuite/Magento/Signifyd/Observer/PlaceOrderTest.php b/dev/tests/integration/testsuite/Magento/Signifyd/Observer/PlaceOrderTest.php index d4204314453..e547187be5e 100644 --- a/dev/tests/integration/testsuite/Magento/Signifyd/Observer/PlaceOrderTest.php +++ b/dev/tests/integration/testsuite/Magento/Signifyd/Observer/PlaceOrderTest.php @@ -12,10 +12,17 @@ use Magento\Sales\Api\Data\OrderInterface; use Magento\Sales\Api\OrderRepositoryInterface; use Magento\Signifyd\Api\CaseCreationServiceInterface; +use Magento\Store\Api\StoreRepositoryInterface; +use Magento\Store\Model\StoreManagerInterface; use Magento\TestFramework\Helper\Bootstrap; use PHPUnit\Framework\MockObject_MockObject as MockObject; use Psr\Log\LoggerInterface; +/** + * Test for Magento\Signifyd\Observer\PlaceOrderTest class. + * + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + */ class PlaceOrderTest extends \PHPUnit\Framework\TestCase { /** @@ -105,7 +112,46 @@ public function testExecute() $event = $this->objectManager->create( Event::class, [ - 'data' => ['order' => $order] + 'data' => ['order' => $order], + ] + ); + + /** @var Observer $observer */ + $observer = $this->objectManager->get(Observer::class); + $observer->setEvent($event); + + $this->placeOrder->execute($observer); + } + + /** + * Signifyd is enabled for default store. + * Checks a test case when order placed with website where signifyd is disabled. + * + * @return void + * @covers \Magento\Signifyd\Observer\PlaceOrder::execute + * @magentoDataFixture Magento/Signifyd/_files/order_with_customer_and_two_simple_products.php + * @magentoDataFixture Magento/Signifyd/_files/website_configuration.php + */ + public function testExecuteWithWebsiteConfiguration(): void + { + /** @var StoreRepositoryInterface $storeRepository */ + $storeRepository = $this->objectManager->get(StoreRepositoryInterface::class); + $store = $storeRepository->get('test_second_store'); + + /** @var StoreManagerInterface $storeManager */ + $storeManager = $this->objectManager->get(StoreManagerInterface::class); + $storeManager->setCurrentStore($store->getId()); + + $order = $this->getOrder('100000001'); + $order->setStoreId($store->getId()); + + $this->creationService->expects(self::never()) + ->method('createForOrder'); + + $event = $this->objectManager->create( + Event::class, + [ + 'data' => ['order' => $order], ] ); diff --git a/dev/tests/integration/testsuite/Magento/Signifyd/_files/order_with_customer_and_two_simple_products.php b/dev/tests/integration/testsuite/Magento/Signifyd/_files/order_with_customer_and_two_simple_products.php index 8991825c938..49a0a2d33e2 100644 --- a/dev/tests/integration/testsuite/Magento/Signifyd/_files/order_with_customer_and_two_simple_products.php +++ b/dev/tests/integration/testsuite/Magento/Signifyd/_files/order_with_customer_and_two_simple_products.php @@ -10,7 +10,7 @@ use Magento\Sales\Api\OrderRepositoryInterface; use Magento\TestFramework\Helper\Bootstrap; -require __DIR__ . '/../../../Magento/Catalog/_files/multiple_products.php'; +require __DIR__ . '/../../../Magento/Catalog/_files/product_simple.php'; require __DIR__ . '/../../../Magento/Customer/_files/customer.php'; require __DIR__ . '/store.php'; @@ -36,33 +36,28 @@ ->setCcExpMonth('01') ->setCcExpYear('21'); -/** @var \Magento\Catalog\Api\ProductRepositoryInterface $productRepository */ -$productRepository = $objectManager->create(\Magento\Catalog\Api\ProductRepositoryInterface::class); - -$product1 = $productRepository->get('simple1'); /** @var Item $orderItem */ $orderItem1 = $objectManager->create(Item::class); -$orderItem1->setProductId($product1->getId()) - ->setSku($product1->getSku()) - ->setName($product1->getName()) +$orderItem1->setProductId($product->getId()) + ->setSku($product->getSku()) + ->setName($product->getName()) ->setQtyOrdered(1) - ->setBasePrice($product1->getPrice()) - ->setPrice($product1->getPrice()) - ->setRowTotal($product1->getPrice()) - ->setProductType($product1->getTypeId()); + ->setBasePrice($product->getPrice()) + ->setPrice($product->getPrice()) + ->setRowTotal($product->getPrice()) + ->setProductType($product->getTypeId()); -$product2 = $productRepository->get('simple2'); /** @var Item $orderItem */ $orderItem2 = $objectManager->create(Item::class); -$orderItem2->setProductId($product2->getId()) - ->setSku($product2->getSku()) - ->setName($product2->getName()) - ->setPrice($product2->getPrice()) +$orderItem2->setProductId($product->getId()) + ->setSku('simple2') + ->setName('Simple product') + ->setPrice(100) ->setQtyOrdered(2) - ->setBasePrice($product2->getPrice()) - ->setPrice($product2->getPrice()) - ->setRowTotal($product2->getPrice()) - ->setProductType($product2->getTypeId()); + ->setBasePrice($product->getPrice()) + ->setPrice($product->getPrice()) + ->setRowTotal($product->getPrice()) + ->setProductType($product->getTypeId()); $orderAmount = 100; $customerEmail = $billingAddress->getEmail(); diff --git a/dev/tests/integration/testsuite/Magento/Signifyd/_files/website_configuration.php b/dev/tests/integration/testsuite/Magento/Signifyd/_files/website_configuration.php new file mode 100644 index 00000000000..e53b0431503 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Signifyd/_files/website_configuration.php @@ -0,0 +1,67 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\Config\Model\Config; +use Magento\Framework\App\Config\ScopeConfigInterface; +use Magento\Store\Model\ResourceModel\Store as StoreResourceModel; +use Magento\Store\Model\ResourceModel\Website as WebsiteResourceModel; +use Magento\Store\Model\ScopeInterface; +use Magento\Store\Model\Store; +use Magento\Store\Model\StoreManagerInterface; +use Magento\Store\Model\Website; +use Magento\TestFramework\Helper\Bootstrap; + +$objectManager = Bootstrap::getObjectManager(); + +/** @var $website Website */ +$website = $objectManager->create(Website::class); +$website->setData(['code' => 'test_website', 'name' => 'Test Website', 'default_group_id' => '1', 'is_default' => '0']); +$websiteResourceModel = $objectManager->create(WebsiteResourceModel::class); +$websiteResourceModel->save($website); + +$websiteId = $website->getId(); +$store = $objectManager->create(Store::class); +$groupId = Bootstrap::getObjectManager()->get(StoreManagerInterface::class) + ->getWebsite() + ->getDefaultGroupId(); +$store->setCode('test_second_store') + ->setWebsiteId($websiteId) + ->setGroupId($groupId) + ->setName('Test Second Store') + ->setSortOrder(10) + ->setIsActive(1); +$storeResourceModel = $objectManager->create(StoreResourceModel::class); +$storeResourceModel->save($store); + +/* Refresh stores memory cache */ +$objectManager->get(StoreManagerInterface::class)->reinitStores(); + +$processConfigData = function (Config $config, array $data) { + foreach ($data as $key => $value) { + $config->setDataByPath($key, $value); + $config->save(); + } +}; + +// save signifyd configuration for the default scope +$configData = [ + 'fraud_protection/signifyd/active' => '1', +]; +/** @var Config $defConfig */ +$defConfig = $objectManager->create(Config::class); +$defConfig->setScope(ScopeConfigInterface::SCOPE_TYPE_DEFAULT); +$processConfigData($defConfig, $configData); + +// save signifyd website config data +$websiteConfigData = [ + 'fraud_protection/signifyd/active' => '0', +]; +/** @var Config $websiteConfig */ +$websiteConfig = $objectManager->create(Config::class); +$websiteConfig->setScope(ScopeInterface::SCOPE_WEBSITES); +$websiteConfig->setWebsite($websiteId); +$processConfigData($websiteConfig, $websiteConfigData); diff --git a/dev/tests/integration/testsuite/Magento/Signifyd/_files/website_configuration_rollback.php b/dev/tests/integration/testsuite/Magento/Signifyd/_files/website_configuration_rollback.php new file mode 100644 index 00000000000..9b731813fea --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Signifyd/_files/website_configuration_rollback.php @@ -0,0 +1,44 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\Framework\App\Config\ScopeConfigInterface; +use Magento\Framework\App\Config\Storage\WriterInterface; +use Magento\Store\Api\WebsiteRepositoryInterface; +use Magento\Store\Model\ScopeInterface; +use Magento\Store\Model\Store; +use Magento\Store\Model\Website; +use Magento\TestFramework\Helper\Bootstrap; + +$objectManager = Bootstrap::getObjectManager(); + +$deleteConfigData = function (WriterInterface $writer, $scope, $scopeId) { + $configData = [ + 'fraud_protection/signifyd/active', + ]; + foreach ($configData as $path) { + $writer->delete($path, $scope, $scopeId); + } +}; + +/** @var WriterInterface $configWriter */ +$configWriter = $objectManager->get(WriterInterface::class); +$deleteConfigData($configWriter, ScopeConfigInterface::SCOPE_TYPE_DEFAULT, null); + +/** @var WebsiteRepositoryInterface $websiteRepository */ +$websiteRepository = $objectManager->get(WebsiteRepositoryInterface::class); +$website = $websiteRepository->get('test_website'); +$deleteConfigData($configWriter, ScopeInterface::SCOPE_WEBSITES, $website->getId()); + +$website = $objectManager->create(Website::class); +/** @var $website Website */ +if ($website->load('test_website', 'code')->getId()) { + $website->delete(); +} +$store = $objectManager->create(Store::class); +if ($store->load('test_second_store', 'code')->getId()) { + $store->delete(); +} diff --git a/dev/tests/integration/testsuite/Magento/Swatches/Controller/Adminhtml/Product/AttributeTest.php b/dev/tests/integration/testsuite/Magento/Swatches/Controller/Adminhtml/Product/AttributeTest.php new file mode 100644 index 00000000000..969d9530ae5 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Swatches/Controller/Adminhtml/Product/AttributeTest.php @@ -0,0 +1,213 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Swatches\Controller\Adminhtml\Product; + +use Magento\Framework\Exception\LocalizedException; + +/** + * Test for product attribute save controller. + * + * @magentoAppArea adminhtml + * @magentoDbIsolation enabled + */ +class AttributeTest extends \Magento\TestFramework\TestCase\AbstractBackendController +{ + /** + * Generate random hex color. + * + * @return string + */ + private function getRandomColor() : string + { + return '#' . str_pad(dechex(mt_rand(0, 0xFFFFFF)), 6, '0', STR_PAD_LEFT); + } + + /** + * Get visual swatches data set. + * + * @param int $optionsCount + * @return array + */ + private function getSwatchVisualDataSet(int $optionsCount) : array + { + $optionsData = []; + $expectedOptionsLabels = []; + for ($i = 0; $i < $optionsCount; $i++) { + $order = $i + 1; + $expectedOptionLabelOnStoreView = "value_{$i}_store_1"; + $expectedOptionsLabels[$i+1] = $expectedOptionLabelOnStoreView; + $optionsData []= "optionvisual[order][option_{$i}]={$order}"; + $optionsData []= "defaultvisual[]=option_{$i}"; + $optionsData []= "swatchvisual[value][option_{$i}]={$this->getRandomColor()}"; + $optionsData []= "optionvisual[value][option_{$i}][0]=value_{$i}_admin"; + $optionsData []= "optionvisual[value][option_{$i}][1]={$expectedOptionLabelOnStoreView}"; + $optionsData []= "optionvisual[delete][option_{$i}]="; + } + $optionsData []= "visual_swatch_validation="; + $optionsData []= "visual_swatch_validation_unique="; + return [ + 'attribute_data' => array_merge_recursive( + [ + 'serialized_swatch_values' => json_encode($optionsData), + ], + $this->getAttributePreset(), + [ + 'frontend_input' => 'swatch_visual' + ] + ), + 'expected_options_count' => $optionsCount + 1, + 'expected_store_labels' => $expectedOptionsLabels + ]; + } + + /** + * Get text swatches data set. + * + * @param int $optionsCount + * @return array + */ + private function getSwatchTextDataSet(int $optionsCount) : array + { + $optionsData = []; + $expectedOptionsLabels = []; + for ($i = 0; $i < $optionsCount; $i++) { + $order = $i + 1; + $expectedOptionLabelOnStoreView = "value_{$i}_store_1"; + $expectedOptionsLabels[$i+1] = $expectedOptionLabelOnStoreView; + $optionsData []= "optiontext[order][option_{$i}]={$order}"; + $optionsData []= "defaulttext[]=option_{$i}"; + $optionsData []= "swatchtext[value][option_{$i}]=x{$i}"; + $optionsData []= "optiontext[value][option_{$i}][0]=value_{$i}_admin"; + $optionsData []= "optiontext[value][option_{$i}][1]={$expectedOptionLabelOnStoreView}"; + $optionsData []= "optiontext[delete][option_{$i}]="; + } + $optionsData []= "text_swatch_validation="; + $optionsData []= "text_swatch_validation_unique="; + return [ + 'attribute_data' => array_merge_recursive( + [ + 'serialized_swatch_values' => json_encode($optionsData), + ], + $this->getAttributePreset(), + [ + 'frontend_input' => 'swatch_text' + ] + ), + 'expected_options_count' => $optionsCount + 1, + 'expected_store_labels' => $expectedOptionsLabels + ]; + } + + /** + * Get data preset for new attribute. + * + * @return array + */ + private function getAttributePreset() : array + { + return [ + 'serialized_options' => '[]', + 'form_key' => 'XxtpPYjm2YPYUlAt', + 'frontend_label' => [ + 0 => 'asdasd', + 1 => '', + 2 => '', + ], + 'is_required' => '0', + 'update_product_preview_image' => '0', + 'use_product_image_for_swatch' => '0', + 'is_global' => '0', + 'default_value_text' => '512', + 'default_value_yesno' => '1', + 'default_value_date' => '1/1/70', + 'default_value_textarea' => '512', + 'is_unique' => '0', + 'is_used_in_grid' => '1', + 'is_visible_in_grid' => '1', + 'is_filterable_in_grid' => '1', + 'is_searchable' => '0', + 'is_comparable' => '0', + 'is_filterable' => '0', + 'is_filterable_in_search' => '0', + 'position' => '0', + 'is_used_for_promo_rules' => '0', + 'is_html_allowed_on_front' => '1', + 'is_visible_on_front' => '0', + 'used_in_product_listing' => '0', + 'used_for_sort_by' => '0', + 'attribute_code' => 'test_many_swatches', + ]; + } + + /** + * Data provider for large swatches amount test. + * + * @return array + */ + public function getLargeSwatchesAmountAttributeData() : array + { + $maxInputVars = ini_get('max_input_vars'); + // Each option is at least 7 variables array for a visual swatch. + // Set options count to exceed max_input_vars by 20 options (140 variables). + $swatchVisualOptionsCount = (int)floor($maxInputVars / 7) + 20; + $swatchTextOptionsCount = (int)floor($maxInputVars / 4) + 80; + return [ + 'visual swatches' => $this->getSwatchVisualDataSet($swatchVisualOptionsCount), + 'text swatches' => $this->getSwatchTextDataSet($swatchTextOptionsCount) + ]; + } + + /** + * Test attribute saving with large amount of options exceeding maximum allowed by max_input_vars limit. + * @dataProvider getLargeSwatchesAmountAttributeData() + * @param array $attributeData + * @param int $expectedOptionsCount + * @param array $expectedLabels + * @return void + */ + public function testLargeOptionsDataSet( + array $attributeData, + int $expectedOptionsCount, + array $expectedLabels + ) : void { + $this->getRequest()->setPostValue($attributeData); + $this->dispatch('backend/catalog/product_attribute/save'); + $entityTypeId = $this->_objectManager->create( + \Magento\Eav\Model\Entity::class + )->setType( + \Magento\Catalog\Model\Product::ENTITY + )->getTypeId(); + + /** @var $attribute \Magento\Catalog\Model\ResourceModel\Eav\Attribute */ + $attribute = $this->_objectManager->create( + \Magento\Catalog\Model\ResourceModel\Eav\Attribute::class + )->setEntityTypeId( + $entityTypeId + ); + try { + $attribute->loadByCode($entityTypeId, 'test_many_swatches'); + $options = $attribute->getOptions(); + // assert that all options are saved without truncation + $this->assertEquals( + $expectedOptionsCount, + count($options), + 'Expected options count does not match (regarding first empty option for non-required attribute)' + ); + + foreach ($expectedLabels as $optionOrderNum => $label) { + $this->assertEquals( + $label, + $options[$optionOrderNum]->getLabel(), + "Label for option #{$optionOrderNum} does not match expected." + ); + } + } catch (LocalizedException $e) { + $this->fail('Test failed with exception on attribute model load: ' . $e); + } + } +} diff --git a/dev/tests/js/jasmine/tests/app/code/Magento/Catalog/frontend/js/product/remaining-characters.test.js b/dev/tests/js/jasmine/tests/app/code/Magento/Catalog/frontend/js/product/remaining-characters.test.js new file mode 100644 index 00000000000..3b64385a889 --- /dev/null +++ b/dev/tests/js/jasmine/tests/app/code/Magento/Catalog/frontend/js/product/remaining-characters.test.js @@ -0,0 +1,84 @@ +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +/* eslint-disable max-nested-callbacks */ +define([ + 'Magento_Catalog/js/product/remaining-characters', + 'jquery' +], function (remainingCharacters, $) { + 'use strict'; + + describe('Magento_Catalog/js/product/remaining-characters', function () { + var widget, + note; + + beforeEach(function () { + widget = $('<input type="text" data-selector="options[1]"/>'); + note = $('<p class="note note_1"><span class="character-counter"></span></p>'); + $('body').append(widget).append(note); + + widget.remainingCharacters({ + maxLength: '10', + noteSelector: '.note_1', + counterSelector: '.note_1 .character-counter' + }); + }); + + afterEach(function () { + widget.remove(); + note.remove(); + }); + + describe('Note text is updated on input change', function () { + it('check empty input', function () { + var testData = { + input: '', + action: 'change', + expectedText: '(10 remaining)' + }; + + widget.val(testData.input); + widget.trigger(testData.action); + expect(note.find('.character-counter').text()).toBe(testData.expectedText); + }); + + it('check input length less than character limit', function () { + var testData = { + input: 'abc', + action: 'change', + expectedText: '(7 remaining)' + }; + + widget.val(testData.input); + widget.trigger(testData.action); + expect(note.find('.character-counter').text()).toBe(testData.expectedText); + }); + + it('check input length equals character limit', function () { + var testData = { + input: 'abcdefghij', + action: 'paste', + expectedText: '(0 remaining)' + }; + + widget.val(testData.input); + widget.trigger(testData.action); + expect(note.find('.character-counter').text()).toBe(testData.expectedText); + }); + + it('check input length greater than character limit', function () { + var testData = { + input: 'abcdefghijkl', + action: 'change', + expectedText: '(2 too many)' + }; + + widget.val(testData.input); + widget.trigger(testData.action); + expect(note.find('.character-counter').text()).toBe(testData.expectedText); + }); + }); + }); +}); diff --git a/dev/tests/js/jasmine/tests/app/code/Magento/Rule/view/adminhtml/web/conditions-data-normalizer.test.js b/dev/tests/js/jasmine/tests/app/code/Magento/Rule/view/adminhtml/web/conditions-data-normalizer.test.js index 21c04d098ae..ec820e53778 100644 --- a/dev/tests/js/jasmine/tests/app/code/Magento/Rule/view/adminhtml/web/conditions-data-normalizer.test.js +++ b/dev/tests/js/jasmine/tests/app/code/Magento/Rule/view/adminhtml/web/conditions-data-normalizer.test.js @@ -65,5 +65,17 @@ define([ expect(normal.foo['1'].value).toEqual(123); expect(normal.foo['1--1']).toEqual(321); }); + + it('Check keys containing a dot are normalized', function () { + var normal = normalizer.normalize({ + 'foo[1][name.foo]': 'bar', + 'foo[1][value.foo]': 123, + 'foo[1--1]': 321 + }); + + expect(normal.foo['1']['name.foo']).toEqual('bar'); + expect(normal.foo['1']['value.foo']).toEqual(123); + expect(normal.foo['1--1']).toEqual(321); + }); }); }); diff --git a/dev/tests/js/jasmine/tests/app/code/Magento/Ui/base/js/form/client.test.js b/dev/tests/js/jasmine/tests/app/code/Magento/Ui/base/js/form/client.test.js index d3b5072a767..24dc5356385 100644 --- a/dev/tests/js/jasmine/tests/app/code/Magento/Ui/base/js/form/client.test.js +++ b/dev/tests/js/jasmine/tests/app/code/Magento/Ui/base/js/form/client.test.js @@ -114,6 +114,7 @@ define([ $.ajax = jasmine.createSpy().and.callFake(function (req) { request = req.success; }); + jQueryMethods.notification = $.fn.notification; $.fn.notification = jasmine.createSpy(); obj.urls.beforeSave = 'requestPath'; obj.save(); diff --git a/dev/tests/js/jasmine/tests/lib/mage/backend/bootstrap.test.js b/dev/tests/js/jasmine/tests/lib/mage/backend/bootstrap.test.js index 396b62df0df..60115c4ac2c 100644 --- a/dev/tests/js/jasmine/tests/lib/mage/backend/bootstrap.test.js +++ b/dev/tests/js/jasmine/tests/lib/mage/backend/bootstrap.test.js @@ -2,45 +2,35 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ -/* global jQuery */ /* eslint-disable max-nested-callbacks */ define([ 'jquery', - 'squire', - 'mage/backend/notification' -], function ($, Squire) { + 'mage/backend/bootstrap' +], function ($) { 'use strict'; - var injector = new Squire(); - describe('mage/backend/bootstrap', function () { - beforeEach(function (done) { - injector.require(['mage/backend/bootstrap'], function () { - done(); - }); + var $pageMainActions; + + beforeEach(function () { + $pageMainActions = $('<div class="page-main-actions"></div>'); }); afterEach(function () { - try { - injector.clean(); - injector.remove(); - } catch (e) {} + $pageMainActions.remove(); }); describe('"sendPostponeRequest" method', function () { - it('should insert "Error" notification if request failed', function (done) { - jQuery('<div class="page-main-actions"></div>').appendTo('body'); - jQuery('body').notification(); + it('should insert "Error" notification if request failed', function () { + $pageMainActions.appendTo('body'); + $('body').notification(); - jQuery.ajax().abort(); + $.ajaxSettings.error(); - setTimeout(function () { - expect(jQuery('.message-error').length).toBe(1); - expect( - jQuery('body:contains("A technical problem with the server created an error")').length - ).toBe(1); - done(); - }, 1); + expect($('.message-error').length).toBe(1); + expect( + $('body:contains("A technical problem with the server created an error")').length + ).toBe(1); }); }); }); diff --git a/dev/tests/static/framework/Magento/TestFramework/Utility/ChangedFiles.php b/dev/tests/static/framework/Magento/TestFramework/Utility/ChangedFiles.php index 6470bc0c220..3d7f5f2604d 100644 --- a/dev/tests/static/framework/Magento/TestFramework/Utility/ChangedFiles.php +++ b/dev/tests/static/framework/Magento/TestFramework/Utility/ChangedFiles.php @@ -32,7 +32,7 @@ public static function getPhpFiles($changedFilesList, $fileTypes = 0) $fileUtilities = Files::init(); if (isset($_ENV['INCREMENTAL_BUILD'])) { $phpFiles = []; - foreach (glob($changedFilesList) as $listFile) { + foreach (glob($changedFilesList, GLOB_NOSORT) as $listFile) { $phpFiles = array_merge($phpFiles, file($listFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES)); } array_walk( diff --git a/dev/tests/static/testsuite/Magento/Test/Integrity/HhvmCompatibilityTest.php b/dev/tests/static/testsuite/Magento/Test/Integrity/HhvmCompatibilityTest.php index e33b771b3c6..c552e0daa97 100644 --- a/dev/tests/static/testsuite/Magento/Test/Integrity/HhvmCompatibilityTest.php +++ b/dev/tests/static/testsuite/Magento/Test/Integrity/HhvmCompatibilityTest.php @@ -47,6 +47,24 @@ class HhvmCompatibilityTest extends \PHPUnit\Framework\TestCase 'serialize_precision', ]; + /** + * Whitelist of variables allowed in files. + * + * @var array + */ + private $whitelistVarsInFiles = [ + 'max_input_vars' => [ + 'integration/testsuite/Magento/Swatches/Controller/Adminhtml/Product/AttributeTest.php', + 'integration/testsuite/Magento/Catalog/Controller/Adminhtml/Product/AttributeTest.php', + ] + ]; + + /** + * Test allowed directives. + * + * @SuppressWarnings(PHPMD.NPathComplexity) + * @SuppressWarnings(PHPMD.CyclomaticComplexity) + */ public function testAllowedIniGetSetDirectives() { $deniedDirectives = []; @@ -55,7 +73,19 @@ public function testAllowedIniGetSetDirectives() if ($fileDirectives) { $fileDeniedDirectives = array_diff($fileDirectives, $this->allowedDirectives); if ($fileDeniedDirectives) { - $deniedDirectives[$file] = array_unique($fileDeniedDirectives); + $deniedDirectivesInFile = array_unique($fileDeniedDirectives); + foreach ($deniedDirectivesInFile as $key => $deniedDirective) { + if (isset($this->whitelistVarsInFiles[$deniedDirective])) { + foreach ($this->whitelistVarsInFiles[$deniedDirective] as $whitelistFile) { + if (strpos($file, $whitelistFile) !== false) { + unset($deniedDirectivesInFile[$key]); + } + } + } + } + if ($deniedDirectivesInFile) { + $deniedDirectives[$file] = $deniedDirectivesInFile; + } } } } diff --git a/dev/tests/static/testsuite/Magento/Test/Integrity/Xml/SchemaTest.php b/dev/tests/static/testsuite/Magento/Test/Integrity/Xml/SchemaTest.php index 8048925a4e5..5877ee5cbcc 100644 --- a/dev/tests/static/testsuite/Magento/Test/Integrity/Xml/SchemaTest.php +++ b/dev/tests/static/testsuite/Magento/Test/Integrity/Xml/SchemaTest.php @@ -106,7 +106,9 @@ private function _filterSpecialCases(&$files) '#etc/countries.xml$#', '#conf/schema.xml$#', '#layout/swagger_index_index.xml$#', - '#Doc/etc/doc/vars.xml$#' + '#Doc/etc/doc/vars.xml$#', + '#phpunit.xml$#', + '#etc/db_schema.xml$#' ]; foreach ($list as $pattern) { foreach ($files as $key => $value) { diff --git a/dev/tests/static/testsuite/Magento/Test/Js/_files/blacklist/magento.txt b/dev/tests/static/testsuite/Magento/Test/Js/_files/blacklist/magento.txt index 83b061c9f3b..181ecd43ce5 100644 --- a/dev/tests/static/testsuite/Magento/Test/Js/_files/blacklist/magento.txt +++ b/dev/tests/static/testsuite/Magento/Test/Js/_files/blacklist/magento.txt @@ -14,3 +14,6 @@ lib/web/mage/adminhtml/varienLoader.js lib/web/magnifier/magnifier.js lib/web/magnifier/magnify.js lib/web/varien/js.js + +// MINIFIED FILES +app/code/**/*.min.js diff --git a/dev/tests/static/testsuite/Magento/Test/Legacy/_files/copyright/blacklist.php b/dev/tests/static/testsuite/Magento/Test/Legacy/_files/copyright/blacklist.php index f7f2df95974..4ff5d001389 100644 --- a/dev/tests/static/testsuite/Magento/Test/Legacy/_files/copyright/blacklist.php +++ b/dev/tests/static/testsuite/Magento/Test/Legacy/_files/copyright/blacklist.php @@ -7,5 +7,6 @@ '/\.(jpe?g|png|gif|ttf|swf|eot|woff|pdf|mp3|pdf|jar|jbf|php\.dist)$/', '/pub\/opt\/magento\/var/', '/COPYING\.txt/', - '/setup\/src\/Zend\/Mvc\/Controller\/LazyControllerAbstractFactory\.php/' + '/setup\/src\/Zend\/Mvc\/Controller\/LazyControllerAbstractFactory\.php/', + '/app\/code\/(?!Magento)[^\/]*/' ]; diff --git a/dev/tools/UpgradeScripts/pre_composer_update_2.3.php b/dev/tools/UpgradeScripts/pre_composer_update_2.3.php new file mode 100644 index 00000000000..6fe629e717b --- /dev/null +++ b/dev/tools/UpgradeScripts/pre_composer_update_2.3.php @@ -0,0 +1,414 @@ +#!/usr/bin/php +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +$_scriptName = basename(__FILE__); + +define( + 'SYNOPSIS', +<<<SYNOPSIS +Updates Magento with 2.3 requirements that can't be done by `composer update` or `bin/magento setup:upgrade`. +Run this script after upgrading to PHP 7.1/7.2 and before running `composer update` or `bin/magento setup:upgrade`. + +Steps included: + - Require new version of the metapackage + - Update "require-dev" section + - Add "Zend\\Mvc\\Controller\\": "setup/src/Zend/Mvc/Controller/" to composer.json "autoload":"psr-4" section + - Update Magento/Updater if it's installed + - Update name, version, and description fields in the root composer.json + +Usage: php -f $_scriptName -- --root='</path/to/magento/root/>' [--composer='</path/to/composer/executable>'] + [--edition='<community|enterprise>'] [--repo='<composer_repo_url>'] [--version='<version_constraint>'] + [--help] + +Required: + --root='</path/to/magento/root/>' + Path to the Magento installation root directory + +Optional: + --composer='</path/to/composer/executable>' + Path to the composer executable + - Default: The composer found in the system PATH + + --edition='<community|enterprise>' + Target Magento edition for the update. Open Source = 'community', Commerce = 'enterprise' + - Default: The edition currently required in composer.json + + --repo='<composer_repo_url>' + The Magento repository url to use to pull the new packages + - Default: The Magento repository configured in composer.json + + --version='<version_constraint>' + A composer version constraint for allowable 2.3 packages. Versions other than 2.3 are not handled by this script + See https://getcomposer.org/doc/articles/versions.md#writing-version-constraints for more information. + - Default: The latest 2.3 version available in the Magento repository + + --help + Display this message +SYNOPSIS +); + +$opts = getopt('', [ + 'root:', + 'composer:', + 'edition:', + 'repo:', + 'version:', + 'help' +]); + +// Log levels available for use with output() function +define('INFO', 0); +define('WARN', 1); +define('ERROR', 2); + +if (isset($opts['help'])) { + output(SYNOPSIS); + exit(0); +} + +try { + if (version_compare(PHP_VERSION, '7.1', '<') || version_compare(PHP_VERSION, '7.3', '>=')) { + preg_match('/^\d+\.\d+\.\d+/',PHP_VERSION, $matches); + $phpVersion = $matches[0]; + throw new Exception("Invalid PHP version '$phpVersion'. Magento 2.3 requires PHP 7.1 or 7.2"); + } + + /**** Populate and Validate Settings ****/ + + if (empty($opts['root']) || !is_dir($opts['root'])) { + throw new BadMethodCallException('Existing Magento root directory must be supplied with --root'); + } + $rootDir = $opts['root']; + + $composerFile = "$rootDir/composer.json"; + if (!file_exists($composerFile)) { + throw new InvalidArgumentException("Supplied Magento root directory '$rootDir' does not contain composer.json"); + } + + $composerData = json_decode(file_get_contents($composerFile), true); + + $metapackageMatcher = '/^magento\/product\-(?<edition>community|enterprise)\-edition$/'; + foreach (array_keys($composerData['require']) as $requiredPackage) { + if (preg_match($metapackageMatcher, $requiredPackage, $matches)) { + $edition = $matches['edition']; + break; + } + } + if (empty($edition)) { + throw new InvalidArgumentException("No Magento metapackage found in $composerFile"); + } + + // Override composer.json edition if one is passed to the script + if (!empty($opts['edition'])) { + $edition = $opts['edition']; + } + $edition = strtolower($edition); + + if ($edition !== 'community' && $edition !== 'enterprise') { + throw new InvalidArgumentException("Only 'community' and 'enterprise' editions allowed; '$edition' given"); + } + + $composerExec = (!empty($opts['composer']) ? $opts['composer'] : 'composer'); + if (basename($composerExec, '.phar') != 'composer') { + throw new InvalidArgumentException("'$composerExec' is not a composer executable"); + } + + // Use 'command -v' to check if composer is executable + exec("command -v $composerExec", $out, $composerFailed); + if ($composerFailed) { + if ($composerExec == 'composer') { + $message = 'Composer executable is not available in the system PATH'; + } + else { + $message = "Invalid composer executable '$composerExec'"; + } + throw new InvalidArgumentException($message); + } + + // The composer command uses the Magento root as the working directory so this script can be run from anywhere + $composerExec = "$composerExec --working-dir='$rootDir'"; + + // Set the version constraint to any 2.3 package if not specified + $constraint = !empty($opts['version']) ? $opts['version'] : '2.3.*'; + + // Composer package names + $project = "magento/project-$edition-edition"; + $metapackage = "magento/product-$edition-edition"; + + // Get the list of potential Magento repositories to search for the update package + $mageUrls = []; + $authFailed = []; + if (!empty($opts['repo'])) { + $mageUrls[] = $opts['repo']; + } + else { + foreach ($composerData['repositories'] as $label => $repo) { + if (strpos(strtolower($label), 'mage') !== false || strpos($repo['url'], '.mage') !== false) { + $mageUrls[] = $repo['url']; + } + } + + if (count($mageUrls) == 0) { + throw new InvalidArgumentException('No Magento repository urls found in composer.json'); + } + } + + $tempDir = findUnusedFilename($rootDir, 'temp_project'); + $projectConstraint = "$project='$constraint'"; + $version = null; + $description = null; + + output("**** Searching for a matching version of $project ****"); + + // Try to retrieve a 2.3 package from each Magento repository until one is found + foreach ($mageUrls as $repoUrl) { + try { + output("\\nChecking $repoUrl"); + deleteFilepath($tempDir); + runComposer("create-project --repository=$repoUrl $projectConstraint $tempDir --no-install"); + + // Make sure the downloaded package is 2.3 + $newComposer = json_decode(file_get_contents("$tempDir/composer.json"), true); + $version = $newComposer['version']; + $description = $newComposer['description']; + + if (strpos($version, '2.3.') !== 0) { + throw new InvalidArgumentException("Bad 2.3 version constraint '$constraint'; version $version found"); + } + + // If no errors occurred, set this as the correct repo, forget errors from previous repos, and move forward + output("\\n**** Found compatible $project version: $version ****"); + $repo = $repoUrl; + unset($exception); + break; + } + catch (Exception $e) { + // If this repository doesn't have a valid package, save the error but continue checking any others + output("Failed to find a valid 2.3 $project package on $repoUrl", WARN); + $exception = $e; + } + } + + // If a valid project package hasn't been found, throw the last error + if (isset($exception)) { + throw $exception; + } + + output("\\n**** Executing Updates ****"); + + $composerBackup = findUnusedFilename($rootDir, 'composer.json.bak'); + output("\\nBacking up $composerFile to $composerBackup"); + copy($composerFile, $composerBackup); + + // Add the repository to composer.json if needed without overwriting any existing ones + $repoUrls = array_map(function ($r) { return $r['url']; }, $composerData['repositories']); + if (!in_array($repo, $repoUrls)) { + $repoLabels = array_map('strtolower',array_keys($composerData['repositories'])); + $newLabel = 'magento'; + if (in_array($newLabel, $repoLabels)) { + $count = count($repoLabels); + for ($i = 1; $i <= $count; $i++) { + if (!in_array("$newLabel-$i", $repoLabels)) { + $newLabel = "$newLabel-$i"; + break; + } + } + } + output("\\nAdding $repo to composer repositories under label '$newLabel'"); + runComposer("config repositories.$newLabel composer $repo"); + } + + output("\\nUpdating Magento metapackage requirement to $metapackage=$version"); + if ($edition == 'enterprise') { + // Community -> Enterprise upgrades need to remove the community edition metapackage + runComposer('remove magento/product-community-edition --no-update'); + output(''); + } + runComposer("require $metapackage=$version --no-update"); + + output('\nUpdating "require-dev" section of composer.json'); + runComposer('require --dev ' . + 'phpunit/phpunit:~6.2.0 ' . + 'friendsofphp/php-cs-fixer:~2.10.1 ' . + 'lusitanian/oauth:~0.8.10 ' . + 'pdepend/pdepend:2.5.2 ' . + 'sebastian/phpcpd:~3.0.0 ' . + 'squizlabs/php_codesniffer:3.2.2 --no-update'); + output(''); + runComposer('remove --dev sjparkinson/static-review fabpot/php-cs-fixer --no-update'); + + output('\nAdding "Zend\\\\Mvc\\\\Controller\\\\": "setup/src/Zend/Mvc/Controller/" to "autoload": "psr-4"'); + $composerData['autoload']['psr-4']['Zend\\Mvc\\Controller\\'] = 'setup/src/Zend/Mvc/Controller/'; + + if (preg_match('/^magento\/project\-(community|enterprise)\-edition$/', $composerData['name'])) { + output('\nUpdating project name, version, and description'); + $composerData['name'] = $project; + $composerData['version'] = $version; + $composerData['description'] = $description; + } + + file_put_contents($composerFile, json_encode($composerData, JSON_UNESCAPED_SLASHES|JSON_PRETTY_PRINT)); + + // Update Magento/Updater if it's installed + $updateDir = "$rootDir/update"; + if (file_exists($updateDir)) { + $updateBackup = findUnusedFilename($rootDir, 'update.bak'); + output("\\nBacking up Magento/Updater directory $updateDir to $updateBackup"); + rename($updateDir, $updateBackup); + output('\nUpdating Magento/Updater'); + rename("$tempDir/update", $updateDir); + } + + // Remove temp project directory that was used for repo/version validation and new source for Magento/Updater + deleteFilepath($tempDir); + + output("\\n**** Script Complete! $composerFile updated to Magento version $version ****"); + if (count($authFailed) > 0) { + output('Repository authentication failures occurred!', WARN); + output(' * Failed authentication could result in incorrect package versions', WARN); + output(' * To resolve, add credentials for the repositories to auth.json', WARN); + output(' * URL(s) failing authentication: ' . join(', ', array_keys($authFailed)), WARN); + } +} catch (Exception $e) { + if ($e->getPrevious()) { + $e = $e->getPrevious(); + } + + try { + output($e->getMessage(), ERROR, get_class($e)); + output('Script failed! See usage information with --help', ERROR); + + if (isset($composerBackup) && file_exists($composerBackup)) { + output("Resetting $composerFile backup"); + deleteFilepath($composerFile); + rename($composerBackup, $composerFile); + } + if (isset($updateBackup) && file_exists($updateBackup)) { + output("Resetting $updateDir backup"); + deleteFilepath($updateDir); + rename($updateBackup, $updateDir); + } + if (isset($tempDir) && file_exists($tempDir)) { + output('Removing temporary project directory'); + deleteFilepath($tempDir); + } + } + catch (Exception $e2) { + output($e2->getMessage(), ERROR, get_class($e2)); + output('Backup restoration or directory cleanup failed', ERROR); + } + + exit($e->getCode() == 0 ? 1 : $e->getCode()); +} + +/** + * Gets a variant of a filename that doesn't already exist so we don't overwrite anything + * + * @param string $dir + * @param string $filename + * @return string + */ +function findUnusedFilename($dir, $filename) { + $unique = "$dir/$filename"; + if (file_exists($unique)) { + $unique = tempnam($dir, "$filename."); + unlink($unique); + } + return $unique; +} + +/** + * Execute a composer command, reload $composerData afterwards, and check for repo authentication warnings + * + * @param string $command + * @return array Command output split by lines + * @throws RuntimeException + */ +function runComposer($command) +{ + global $composerExec, $composerData, $composerFile, $authFailed; + $command = "$composerExec $command --no-interaction"; + output(" Running command:\\n $command"); + exec("$command 2>&1", $lines, $exitCode); + $output = ' ' . join('\n ', $lines); + + // Reload composer object from the updated composer.json + $composerData = json_decode(file_get_contents($composerFile), true); + + if (0 !== $exitCode) { + $output = "Error encountered running command:\\n $command\\n$output"; + throw new RuntimeException($output, $exitCode); + } + output($output); + + if (strpos($output, 'URL required authentication.') !== false) { + preg_match("/'(https?:\/\/)?(?<url>[^\/']+)(\/[^']*)?' URL required authentication/", $output, $matches); + $authUrl = $matches['url']; + $authFailed[$authUrl] = 1; + output("Repository authentication failed; make sure '$authUrl' exists in auth.json", WARN); + } + + return $lines; +} + +/** + * Deletes a file or a directory and all its contents + * + * @param string $path + * @throws Exception + */ +function deleteFilepath($path) { + if (!file_exists($path)) { + return; + } + if (is_dir($path)) { + $files = array_diff(scandir($path), array('..', '.')); + foreach ($files as $file) { + deleteFilepath("$path/$file"); + } + rmdir($path); + } + else { + unlink($path); + } + if (file_exists($path)) { + throw new Exception("Failed to delete $path"); + } +} + +/** + * Logs the given text with \n newline replacement and log level formatting + * + * @param string $string Text to log + * @param int $level One of INFO, WARN, or ERROR + * @param string $label Optional message label; defaults to WARNING for $level = WARN and ERROR for $level = ERROR + */ +function output($string, $level = INFO, $label = '') { + $string = str_replace('\n', PHP_EOL, $string); + + if (!empty($label)) { + $label = "$label: "; + } + else if ($level == WARN) { + $label = 'WARNING: '; + } + else if ($level == ERROR) { + $label = 'ERROR: '; + } + $string = "$label$string"; + + if ($level == WARN) { + error_log($string); + } + elseif ($level == ERROR) { + error_log(PHP_EOL . $string); + } + else { + echo $string . PHP_EOL; + } +} diff --git a/dev/tools/grunt/tools/collect-validation-files.js b/dev/tools/grunt/tools/collect-validation-files.js index a7bf4a2c67a..15fb5e50be0 100644 --- a/dev/tools/grunt/tools/collect-validation-files.js +++ b/dev/tools/grunt/tools/collect-validation-files.js @@ -3,10 +3,6 @@ * See COPYING.txt for license details. */ -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ 'use strict'; var glob = require('glob'), @@ -29,8 +25,8 @@ module.exports = { getFilesForValidate: function () { var blackListFiles = glob.sync(pc.static.blacklist + '*.txt'), whiteListFiles = glob.sync(pc.static.whitelist + '*.txt'), - blackList = this.readFiles(blackListFiles), - whiteList = this.readFiles(whiteListFiles), + blackList = this.readFiles(blackListFiles).filter(this.isListEntryValid), + whiteList = this.readFiles(whiteListFiles).filter(this.isListEntryValid), files = [], entireBlackList = []; @@ -45,6 +41,11 @@ module.exports = { return files; }, + isListEntryValid: function(line) { + line = line.trim(); + return line.length > 0 && line.startsWith('// ') !== true; + }, + getFiles: function (file) { if (file) { return file.split(','); diff --git a/lib/internal/Magento/Framework/Api/SearchCriteria/CollectionProcessor/FilterProcessor.php b/lib/internal/Magento/Framework/Api/SearchCriteria/CollectionProcessor/FilterProcessor.php index b663a3a2f73..c9f10c183b5 100644 --- a/lib/internal/Magento/Framework/Api/SearchCriteria/CollectionProcessor/FilterProcessor.php +++ b/lib/internal/Magento/Framework/Api/SearchCriteria/CollectionProcessor/FilterProcessor.php @@ -72,6 +72,15 @@ private function addFilterGroupToCollection( if (!$isApplied) { $condition = $filter->getConditionType() ? $filter->getConditionType() : 'eq'; $fields[] = $this->getFieldMapping($filter->getField()); + + if ($condition === 'fulltext') { + // NOTE: This is not a fulltext search, but the best way to search something when + // a SearchCriteria with "fulltext" condition is provided over a MySQL table + // (see https://github.com/magento-engcom/msi/issues/1221) + $condition = 'like'; + $filter->setValue('%' . $filter->getValue() . '%'); + } + $conditions[] = [$condition => $filter->getValue()]; } } diff --git a/lib/internal/Magento/Framework/App/Utility/Files.php b/lib/internal/Magento/Framework/App/Utility/Files.php index 7e8d79eead7..b0dfc50e9f9 100644 --- a/lib/internal/Magento/Framework/App/Utility/Files.php +++ b/lib/internal/Magento/Framework/App/Utility/Files.php @@ -392,7 +392,7 @@ public function getMainConfigFiles($asDataSet = true) $configXmlPaths = array_merge($globPaths, $configXmlPaths); $files = []; foreach ($configXmlPaths as $xmlPath) { - $files = array_merge($files, glob($xmlPath)); + $files = array_merge($files, glob($xmlPath, GLOB_NOSORT)); } self::$_cache[$cacheKey] = $files; } diff --git a/lib/internal/Magento/Framework/Archive/Tar.php b/lib/internal/Magento/Framework/Archive/Tar.php index ccf9838d664..e2a070503f6 100644 --- a/lib/internal/Magento/Framework/Archive/Tar.php +++ b/lib/internal/Magento/Framework/Archive/Tar.php @@ -252,7 +252,7 @@ protected function _createTar($skipRoot = false, $finalize = false) $file = $this->_getCurrentFile(); if (is_dir($file)) { - $dirFiles = scandir($file); + $dirFiles = scandir($file, SCANDIR_SORT_NONE); if (false === $dirFiles) { throw new \Magento\Framework\Exception\LocalizedException( diff --git a/lib/internal/Magento/Framework/Backup/Test/Unit/_files/app_dirs_rollback.php b/lib/internal/Magento/Framework/Backup/Test/Unit/_files/app_dirs_rollback.php index 7a9814bcd69..5d4f18037d0 100644 --- a/lib/internal/Magento/Framework/Backup/Test/Unit/_files/app_dirs_rollback.php +++ b/lib/internal/Magento/Framework/Backup/Test/Unit/_files/app_dirs_rollback.php @@ -15,7 +15,7 @@ } } -$files = glob(TESTS_TEMP_DIR . '/Magento/Backup/data/*'); +$files = glob(TESTS_TEMP_DIR . '/Magento/Backup/data/*', GLOB_NOSORT); foreach ($files as $file) { unlink($file); } diff --git a/lib/internal/Magento/Framework/Data/Collection/Filesystem.php b/lib/internal/Magento/Framework/Data/Collection/Filesystem.php index 67d2a020864..330ff4e975e 100644 --- a/lib/internal/Magento/Framework/Data/Collection/Filesystem.php +++ b/lib/internal/Magento/Framework/Data/Collection/Filesystem.php @@ -243,7 +243,7 @@ protected function _collectRecursive($dir) $dir = [$dir]; } foreach ($dir as $folder) { - if ($nodes = glob($folder . '/*')) { + if ($nodes = glob($folder . '/*', GLOB_NOSORT)) { foreach ($nodes as $node) { $collectedResult[] = $node; } diff --git a/lib/internal/Magento/Framework/Data/SearchResultProcessor.php b/lib/internal/Magento/Framework/Data/SearchResultProcessor.php index 9cbee9c0e67..561a3c66ebc 100644 --- a/lib/internal/Magento/Framework/Data/SearchResultProcessor.php +++ b/lib/internal/Magento/Framework/Data/SearchResultProcessor.php @@ -188,8 +188,8 @@ public function toArray($arrRequiredFields = []) } /** - * @param null $valueField - * @param null $labelField + * @param string|null $valueField + * @param string|null $labelField * @param array $additional * @return array */ diff --git a/lib/internal/Magento/Framework/Filesystem/Io/File.php b/lib/internal/Magento/Framework/Filesystem/Io/File.php index 7849fdf3103..c1cfebc7a0a 100644 --- a/lib/internal/Magento/Framework/Filesystem/Io/File.php +++ b/lib/internal/Magento/Framework/Filesystem/Io/File.php @@ -366,7 +366,7 @@ protected static function _recursiveCallback($dir, array $fileCallback, array $d $dirCallback = $fileCallback; } if (is_dir($dir)) { - foreach (scandir($dir) as $item) { + foreach (scandir($dir, SCANDIR_SORT_NONE) as $item) { if (!strcmp($item, '.') || !strcmp($item, '..')) { continue; } diff --git a/lib/internal/Magento/Framework/Validator/Test/Unit/ConfigTest.php b/lib/internal/Magento/Framework/Validator/Test/Unit/ConfigTest.php index 213a71c0087..67170953766 100644 --- a/lib/internal/Magento/Framework/Validator/Test/Unit/ConfigTest.php +++ b/lib/internal/Magento/Framework/Validator/Test/Unit/ConfigTest.php @@ -51,7 +51,7 @@ public function testConstructException() protected function _initConfig(array $files = null) { if (null === $files) { - $files = glob(__DIR__ . '/_files/validation/positive/*/validation.xml'); + $files = glob(__DIR__ . '/_files/validation/positive/*/validation.xml', GLOB_NOSORT); } $configFiles = []; foreach ($files as $path) { diff --git a/lib/web/css/docs/source/docs.less b/lib/web/css/docs/source/docs.less index 4039d0562da..952beb5c0f9 100644 --- a/lib/web/css/docs/source/docs.less +++ b/lib/web/css/docs/source/docs.less @@ -21,7 +21,6 @@ @import '_icons.less'; @import '_loaders.less'; @import '_messages.less'; -//@import 'navigation.less'; @import '_layout.less'; @import '_pages.less'; @import '_popups.less'; diff --git a/lib/web/css/source/lib/_buttons.less b/lib/web/css/source/lib/_buttons.less index 679c4b11fba..a92093b7429 100644 --- a/lib/web/css/source/lib/_buttons.less +++ b/lib/web/css/source/lib/_buttons.less @@ -276,6 +276,9 @@ // --------------------------------------------- .lib-button-primary( + @_button-font-family: @button-primary__font-family, + @_button-font-size: @button-primary__font-size, + @_button-font-weight: @button-primary__font-weight, @_button-line-height: @button-primary__line-height, @_button-width: @button-primary__width, @_button-margin: @button-primary__margin, @@ -299,6 +302,9 @@ @_button-gradient-direction: @button-primary__gradient-direction ) { .lib-button( + @_button-font-family: @_button-font-family, + @_button-font-size: @_button-font-size, + @_button-font-weight: @_button-font-weight, @_button-line-height: @_button-line-height, @_button-width: @_button-width, @_button-margin: @_button-margin, diff --git a/lib/web/css/source/lib/variables/_buttons.less b/lib/web/css/source/lib/variables/_buttons.less index 82f618faa1d..8c31c161438 100644 --- a/lib/web/css/source/lib/variables/_buttons.less +++ b/lib/web/css/source/lib/variables/_buttons.less @@ -47,6 +47,9 @@ @button__active__gradient-color-end: false; // Primary button +@button-primary__font-family: @button__font-family; +@button-primary__font-size: @button__font-size; +@button-primary__font-weight: @button__font-weight; @button-primary__line-height: false; @button-primary__width: false; @button-primary__margin: false; diff --git a/lib/web/mage/adminhtml/form.js b/lib/web/mage/adminhtml/form.js index e136e8c0f73..487c71484e4 100644 --- a/lib/web/mage/adminhtml/form.js +++ b/lib/web/mage/adminhtml/form.js @@ -389,7 +389,7 @@ define([ var idTo, idFrom, values, fromId, radioFrom; if (config) { - this._config = config; + this._config = jQuery.extend(this._config, config); } for (idTo in elementsMap) { //eslint-disable-line guard-for-in diff --git a/lib/web/mage/apply/scripts.js b/lib/web/mage/apply/scripts.js index f35e9a2140e..bf211c38adb 100644 --- a/lib/web/mage/apply/scripts.js +++ b/lib/web/mage/apply/scripts.js @@ -14,7 +14,7 @@ define([ virtuals = []; /** - * Adds components to the virtula list. + * Adds components to the virtual list. * * @param {Object} components */ diff --git a/lib/web/mage/backend/suggest.js b/lib/web/mage/backend/suggest.js index 412a80804ae..d34be104209 100644 --- a/lib/web/mage/backend/suggest.js +++ b/lib/web/mage/backend/suggest.js @@ -65,7 +65,8 @@ inputWrapper: '<div class="mage-suggest"><div class="mage-suggest-inner"></div></div>', dropdownWrapper: '<div class="mage-suggest-dropdown"></div>', preventClickPropagation: true, - currentlySelected: null + currentlySelected: null, + submitInputOnEnter: true }, /** @@ -79,7 +80,6 @@ label: '' }; this.templates = {}; - this._renderedContext = null; this._selectedItem = this._nonSelectedItem; this._control = this.options.controls || {}; @@ -312,11 +312,12 @@ click: this.search }, this.options.events)); + this._bindSubmit(); this._bindDropdown(); }, /** - * @param {Object} event - event object + * @param {Object} event * @private */ _toggleEnter: function (event) { @@ -324,6 +325,10 @@ activeItems, selectedItem; + if (!this.options.submitInputOnEnter) { + event.preventDefault(); + } + suggestList = $(event.currentTarget.parentNode).find('ul').first(); activeItems = suggestList.find('._active'); @@ -333,12 +338,22 @@ if (selectedItem.find('a') && selectedItem.find('a').attr('href') !== undefined) { window.location = selectedItem.find('a').attr('href'); event.preventDefault(); - - return false; } } }, + /** + * Bind handlers for submit on enter + * @private + */ + _bindSubmit: function () { + this.element.parents('form').on('submit', function (event) { + if (!this.submitInputOnEnter) { + event.preventDefault(); + } + }); + }, + /** * @param {Object} e - event object * @private @@ -465,8 +480,8 @@ } if (this._trigger('beforeselect', e || null, { - item: this._focused - }) === false) { + item: this._focused + }) === false) { return; } this._selectItem(e); @@ -701,9 +716,6 @@ if ($.isArray(o.source)) { response(this.filter(o.source, term)); } else if ($.type(o.source) === 'string') { - if (this._xhr) { - this._xhr.abort(); - } ajaxData = {}; ajaxData[this.options.termAjaxArgument] = term; @@ -729,10 +741,6 @@ _abortSearch: function () { this.element.removeClass(this.options.loadingClass); clearTimeout(this._searchTimeout); - - if (this._xhr) { - this._xhr.abort(); - } }, /** @@ -905,8 +913,8 @@ '<li class="mage-suggest-search-field" data-role="parent-choice-element"><' + 'label class="mage-suggest-search-label"></label></li></ul>', choiceTemplate: '<li class="mage-suggest-choice button"><div><%- text %></div>' + - '<span class="mage-suggest-choice-close" tabindex="-1" ' + - 'data-mage-init=\'{"actionLink":{"event":"removeOption"}}\'></span></li>', + '<span class="mage-suggest-choice-close" tabindex="-1" ' + + 'data-mage-init=\'{"actionLink":{"event":"removeOption"}}\'></span></li>', selectedClass: 'mage-suggest-selected' }, diff --git a/setup/performance-toolkit/benchmark.jmx b/setup/performance-toolkit/benchmark.jmx index 2155f774b1e..0b46a4d5c94 100644 --- a/setup/performance-toolkit/benchmark.jmx +++ b/setup/performance-toolkit/benchmark.jmx @@ -39857,7 +39857,7 @@ adminUserList.add(vars.get("admin_user")); <collectionProp name="Arguments.arguments"> <elementProp name="" elementType="HTTPArgument"> <boolProp name="HTTPArgument.always_encode">false</boolProp> - <stringProp name="Argument.value">{"query":"{\n products(\n filter: {\n price: {gt: \"10\"}\n or: {\n sku:{like:\"%Product%\"}\n name:{like:\"%Configurable Product%\"}\n }\n }\n pageSize: 200\n currentPage: 1\n sort: {\n price: ASC\n name:DESC\n }\n ) {\n total_count\n items {\n attribute_set_id\n country_of_manufacture\n created_at\n description\n gift_message_available\n image\n image_label\n meta_description\n meta_keyword\n meta_title\n name\n new_from_date\n new_to_date\n options_container\n ... on CustomizableProductInterface {\n options\n {\n title\n required\n sort_order\n }\n }\n \n price {\n minimalPrice {\n amount {\n value\n currency\n }\n adjustments {\n amount {\n value\n currency\n }\n code\n description\n }\n }\n maximalPrice {\n amount {\n value\n currency\n }\n adjustments {\n amount {\n value\n currency\n }\n code\n description\n }\n }\n regularPrice {\n amount {\n value\n currency\n }\n adjustments {\n amount {\n value\n currency\n }\n code\n description\n }\n }\n }\n short_description\n sku\n small_image\n small_image_label\n special_from_date\n special_price\n special_to_date\n swatch_image\n tax_class_id\n thumbnail\n thumbnail_label\n tier_price\n type_id\n updated_at\n url_key\n url_path\n websites { id name code sort_order default_group_id is_default }\n \t... on PhysicalProductInterface {\n \tweight\n \t}\n }\n page_info {\n page_size\n current_page\n }\n }\n}\n","variables":null,"operationName":null}</stringProp> + <stringProp name="Argument.value">{"query":"{\n products(\n filter: {\n price: {gt: \"10\"}\n or: {\n sku:{like:\"%Product%\"}\n name:{like:\"%Configurable Product%\"}\n }\n }\n pageSize: 200\n currentPage: 1\n sort: {\n price: ASC\n name:DESC\n }\n ) {\n total_count\n items {\n attribute_set_id\n country_of_manufacture\n created_at\n description\n gift_message_available\n image\n image_label\n meta_description\n meta_keyword\n meta_title\n name\n new_from_date\n new_to_date\n options_container\n ... on CustomizableProductInterface {\n options\n {\n title\n required\n sort_order\n }\n }\n \n price {\n minimalPrice {\n amount {\n value\n currency\n }\n adjustments {\n amount {\n value\n currency\n }\n code\n description\n }\n }\n maximalPrice {\n amount {\n value\n currency\n }\n adjustments {\n amount {\n value\n currency\n }\n code\n description\n }\n }\n regularPrice {\n amount {\n value\n currency\n }\n adjustments {\n amount {\n value\n currency\n }\n code\n description\n }\n }\n }\n short_description\n sku\n small_image\n small_image_label\n special_from_date\n special_price\n special_to_date\n swatch_image\n thumbnail\n thumbnail_label\n tier_price\n type_id\n updated_at\n url_key\n url_path\n websites { id name code sort_order default_group_id is_default }\n \t... on PhysicalProductInterface {\n \tweight\n \t}\n }\n page_info {\n page_size\n current_page\n }\n }\n}\n","variables":null,"operationName":null}</stringProp> <stringProp name="Argument.metadata">=</stringProp> </elementProp> </collectionProp> @@ -39914,7 +39914,7 @@ adminUserList.add(vars.get("admin_user")); <collectionProp name="Arguments.arguments"> <elementProp name="" elementType="HTTPArgument"> <boolProp name="HTTPArgument.always_encode">false</boolProp> - <stringProp name="Argument.value">{"query":"{\n products(filter: {sku: { eq: \"${simple_product_sku}\" } })\n {\n total_count\n items {\n attribute_set_id\n categories\n {\n id\n position\n }\n country_of_manufacture\n created_at\n description\n gift_message_available\n id\n image\n image_label\n meta_description\n meta_keyword\n meta_title\n media_gallery_entries\n {\n disabled\n file\n id\n label\n media_type\n position\n types\n content\n {\n base64_encoded_data\n type\n name\n }\n video_content\n {\n media_type\n video_description\n video_metadata\n video_provider\n video_title\n video_url\n }\n }\n name\n new_from_date\n new_to_date\n options_container\n ... on CustomizableProductInterface {\n options\n {\n title\n required\n sort_order\n }\n }\n \n price {\n minimalPrice {\n amount {\n value\n currency\n }\n adjustments {\n amount {\n value\n currency\n }\n code\n description\n }\n }\n maximalPrice {\n amount {\n value\n currency\n }\n adjustments {\n amount {\n value\n currency\n }\n code\n description\n }\n }\n regularPrice {\n amount {\n value\n currency\n }\n adjustments {\n amount {\n value\n currency\n }\n code\n description\n }\n }\n }\n product_links\n {\n link_type\n linked_product_sku\n linked_product_type\n position\n sku\n }\n short_description\n sku\n small_image\n small_image_label\n special_from_date\n special_price\n special_to_date\n swatch_image\n tax_class_id\n thumbnail\n thumbnail_label\n tier_price\n tier_prices\n {\n customer_group_id\n percentage_value\n qty\n value\n website_id\n }\n type_id\n updated_at\n url_key\n url_path\n websites { id name code sort_order default_group_id is_default }\n ... on PhysicalProductInterface {\n \t\t\tweight\n \t\t }\n }\n }\n}\n","variables":null,"operationName":null}</stringProp> + <stringProp name="Argument.value">{"query":"{\n products(filter: {sku: { eq: \"${simple_product_sku}\" } })\n {\n total_count\n items {\n attribute_set_id\n categories\n {\n id\n position\n }\n country_of_manufacture\n created_at\n description\n gift_message_available\n id\n image\n image_label\n meta_description\n meta_keyword\n meta_title\n media_gallery_entries\n {\n disabled\n file\n id\n label\n media_type\n position\n types\n content\n {\n base64_encoded_data\n type\n name\n }\n video_content\n {\n media_type\n video_description\n video_metadata\n video_provider\n video_title\n video_url\n }\n }\n name\n new_from_date\n new_to_date\n options_container\n ... on CustomizableProductInterface {\n options\n {\n title\n required\n sort_order\n }\n }\n \n price {\n minimalPrice {\n amount {\n value\n currency\n }\n adjustments {\n amount {\n value\n currency\n }\n code\n description\n }\n }\n maximalPrice {\n amount {\n value\n currency\n }\n adjustments {\n amount {\n value\n currency\n }\n code\n description\n }\n }\n regularPrice {\n amount {\n value\n currency\n }\n adjustments {\n amount {\n value\n currency\n }\n code\n description\n }\n }\n }\n product_links\n {\n link_type\n linked_product_sku\n linked_product_type\n position\n sku\n }\n short_description\n sku\n small_image\n small_image_label\n special_from_date\n special_price\n special_to_date\n swatch_image\n thumbnail\n thumbnail_label\n tier_price\n tier_prices\n {\n customer_group_id\n percentage_value\n qty\n value\n website_id\n }\n type_id\n updated_at\n url_key\n url_path\n websites { id name code sort_order default_group_id is_default }\n ... on PhysicalProductInterface {\n \t\t\tweight\n \t\t }\n }\n }\n}\n","variables":null,"operationName":null}</stringProp> <stringProp name="Argument.metadata">=</stringProp> </elementProp> </collectionProp> @@ -39990,7 +39990,7 @@ if (totalCount == null) { <collectionProp name="Arguments.arguments"> <elementProp name="" elementType="HTTPArgument"> <boolProp name="HTTPArgument.always_encode">false</boolProp> - <stringProp name="Argument.value">{"query":"{\n products(filter: {sku: {eq:\"${configurable_product_sku}\"} }) {\n total_count\n items {\n ... on PhysicalProductInterface {\n weight\n }\n attribute_set_id\n categories\n {\n id\n position\n }\n country_of_manufacture\n created_at\n description\n gift_message_available\n id\n image\n image_label\n meta_description\n meta_keyword\n meta_title\n media_gallery_entries\n {\n disabled\n file\n id\n label\n media_type\n position\n types\n content\n {\n base64_encoded_data\n type\n name\n }\n video_content\n {\n media_type\n video_description\n video_metadata\n video_provider\n video_title\n video_url\n }\n }\n name\n new_from_date\n new_to_date\n options_container\n ... on CustomizableProductInterface {\n options\n {\n title\n required\n sort_order\n }\n }\n \n price {\n minimalPrice {\n amount {\n value\n currency\n }\n adjustments {\n amount {\n value\n currency\n }\n code\n description\n }\n }\n maximalPrice {\n amount {\n value\n currency\n }\n adjustments {\n amount {\n value\n currency\n }\n code\n description\n }\n }\n regularPrice {\n amount {\n value\n currency\n }\n adjustments {\n amount {\n value\n currency\n }\n code\n description\n }\n }\n }\n product_links\n {\n link_type\n linked_product_sku\n linked_product_type\n position\n sku\n }\n short_description\n sku\n small_image\n small_image_label\n special_from_date\n special_price\n special_to_date\n swatch_image\n tax_class_id\n thumbnail\n thumbnail_label\n tier_price\n tier_prices\n {\n customer_group_id\n percentage_value\n qty\n value\n website_id\n }\n type_id\n updated_at\n url_key\n url_path\n websites { id name code sort_order default_group_id is_default }\n ... on ConfigurableProduct {\n configurable_options {\n id\n attribute_id\n label\n position\n use_default\n attribute_code\n values {\n value_index\n label\n store_label\n default_label\n use_default_value\n }\n product_id\n }\n variants {\n product {\n ... on PhysicalProductInterface {\n weight\n }\n sku\n color\n attribute_set_id\n categories\n {\n id\n position\n }\n country_of_manufacture\n created_at\n description\n gift_message_available\n id\n image\n image_label\n meta_description\n meta_keyword\n meta_title\n media_gallery_entries\n {\n disabled\n file\n id\n label\n media_type\n position\n types\n content\n {\n base64_encoded_data\n type\n name\n }\n video_content\n {\n media_type\n video_description\n video_metadata\n video_provider\n video_title\n video_url\n }\n }\n name\n new_from_date\n new_to_date\n options_container\n ... on CustomizableProductInterface {\n options\n {\n title\n required\n sort_order\n }\n }\n \n price {\n minimalPrice {\n amount {\n value\n currency\n }\n adjustments {\n amount {\n value\n currency\n }\n code\n description\n }\n }\n maximalPrice {\n amount {\n value\n currency\n }\n adjustments {\n amount {\n value\n currency\n }\n code\n description\n }\n }\n regularPrice {\n amount {\n value\n currency\n }\n adjustments {\n amount {\n value\n currency\n }\n code\n description\n }\n }\n }\n product_links\n {\n link_type\n linked_product_sku\n linked_product_type\n position\n sku\n }\n short_description\n sku\n small_image\n small_image_label\n special_from_date\n special_price\n special_to_date\n swatch_image\n tax_class_id\n thumbnail\n thumbnail_label\n tier_price\n tier_prices\n {\n customer_group_id\n percentage_value\n qty\n value\n website_id\n }\n type_id\n updated_at\n url_key\n url_path\n websites { id name code sort_order default_group_id is_default }\n\n\n }\n attributes {\n label\n code\n value_index\n }\n }\n }\n }\n }\n}\n","variables":null,"operationName":null}</stringProp> + <stringProp name="Argument.value">{"query":"{\n products(filter: {sku: {eq:\"${configurable_product_sku}\"} }) {\n total_count\n items {\n ... on PhysicalProductInterface {\n weight\n }\n attribute_set_id\n categories\n {\n id\n position\n }\n country_of_manufacture\n created_at\n description\n gift_message_available\n id\n image\n image_label\n meta_description\n meta_keyword\n meta_title\n media_gallery_entries\n {\n disabled\n file\n id\n label\n media_type\n position\n types\n content\n {\n base64_encoded_data\n type\n name\n }\n video_content\n {\n media_type\n video_description\n video_metadata\n video_provider\n video_title\n video_url\n }\n }\n name\n new_from_date\n new_to_date\n options_container\n ... on CustomizableProductInterface {\n options\n {\n title\n required\n sort_order\n }\n }\n \n price {\n minimalPrice {\n amount {\n value\n currency\n }\n adjustments {\n amount {\n value\n currency\n }\n code\n description\n }\n }\n maximalPrice {\n amount {\n value\n currency\n }\n adjustments {\n amount {\n value\n currency\n }\n code\n description\n }\n }\n regularPrice {\n amount {\n value\n currency\n }\n adjustments {\n amount {\n value\n currency\n }\n code\n description\n }\n }\n }\n product_links\n {\n link_type\n linked_product_sku\n linked_product_type\n position\n sku\n }\n short_description\n sku\n small_image\n small_image_label\n special_from_date\n special_price\n special_to_date\n swatch_image\n thumbnail\n thumbnail_label\n tier_price\n tier_prices\n {\n customer_group_id\n percentage_value\n qty\n value\n website_id\n }\n type_id\n updated_at\n url_key\n url_path\n websites { id name code sort_order default_group_id is_default }\n ... on ConfigurableProduct {\n configurable_options {\n id\n attribute_id\n label\n position\n use_default\n attribute_code\n values {\n value_index\n label\n store_label\n default_label\n use_default_value\n }\n product_id\n }\n variants {\n product {\n ... on PhysicalProductInterface {\n weight\n }\n sku\n color\n attribute_set_id\n categories\n {\n id\n position\n }\n country_of_manufacture\n created_at\n description\n gift_message_available\n id\n image\n image_label\n meta_description\n meta_keyword\n meta_title\n media_gallery_entries\n {\n disabled\n file\n id\n label\n media_type\n position\n types\n content\n {\n base64_encoded_data\n type\n name\n }\n video_content\n {\n media_type\n video_description\n video_metadata\n video_provider\n video_title\n video_url\n }\n }\n name\n new_from_date\n new_to_date\n options_container\n ... on CustomizableProductInterface {\n options\n {\n title\n required\n sort_order\n }\n }\n \n price {\n minimalPrice {\n amount {\n value\n currency\n }\n adjustments {\n amount {\n value\n currency\n }\n code\n description\n }\n }\n maximalPrice {\n amount {\n value\n currency\n }\n adjustments {\n amount {\n value\n currency\n }\n code\n description\n }\n }\n regularPrice {\n amount {\n value\n currency\n }\n adjustments {\n amount {\n value\n currency\n }\n code\n description\n }\n }\n }\n product_links\n {\n link_type\n linked_product_sku\n linked_product_type\n position\n sku\n }\n short_description\n sku\n small_image\n small_image_label\n special_from_date\n special_price\n special_to_date\n swatch_image\n thumbnail\n thumbnail_label\n tier_price\n tier_prices\n {\n customer_group_id\n percentage_value\n qty\n value\n website_id\n }\n type_id\n updated_at\n url_key\n url_path\n websites { id name code sort_order default_group_id is_default }\n\n\n }\n attributes {\n label\n code\n value_index\n }\n }\n }\n }\n }\n}\n","variables":null,"operationName":null}</stringProp> <stringProp name="Argument.metadata">=</stringProp> </elementProp> </collectionProp> @@ -40059,7 +40059,7 @@ if (totalCount == null) { <collectionProp name="Arguments.arguments"> <elementProp name="" elementType="HTTPArgument"> <boolProp name="HTTPArgument.always_encode">false</boolProp> - <stringProp name="Argument.value">{"query":"{\n products(\n search: \"configurable\"\n filter: {price: {gteq: \"1\"} }\n ) {\n total_count\n items {\n attribute_set_id\n categories\n {\n id\n position\n }\n country_of_manufacture\n created_at\n description\n gift_message_available\n id\n image\n image_label\n meta_description\n meta_keyword\n meta_title\n media_gallery_entries\n {\n disabled\n file\n id\n label\n media_type\n position\n types\n content\n {\n base64_encoded_data\n type\n name\n }\n video_content\n {\n media_type\n video_description\n video_metadata\n video_provider\n video_title\n video_url\n }\n }\n name\n new_from_date\n new_to_date\n options_container\n ... on CustomizableProductInterface {\n options\n {\n title\n required\n sort_order\n }\n }\n \n price {\n minimalPrice {\n amount {\n value\n currency\n }\n adjustments {\n amount {\n value\n currency\n }\n code\n description\n }\n }\n maximalPrice {\n amount {\n value\n currency\n }\n adjustments {\n amount {\n value\n currency\n }\n code\n description\n }\n }\n regularPrice {\n amount {\n value\n currency\n }\n adjustments {\n amount {\n value\n currency\n }\n code\n description\n }\n }\n }\n product_links\n {\n link_type\n linked_product_sku\n linked_product_type\n position\n sku\n }\n short_description\n sku\n small_image\n small_image_label\n special_from_date\n special_price\n special_to_date\n swatch_image\n tax_class_id\n thumbnail\n thumbnail_label\n tier_price\n tier_prices\n {\n customer_group_id\n percentage_value\n qty\n value\n website_id\n }\n type_id\n updated_at\n url_key\n url_path\n websites { id name code sort_order default_group_id is_default }\n ... on PhysicalProductInterface {\n \t\t\tweight\n \t\t\t}\n ... on ConfigurableProduct {\n configurable_options {\n id\n attribute_id\n label\n position\n use_default\n attribute_code\n values {\n value_index\n label\n store_label\n default_label\n use_default_value\n }\n product_id\n }\n variants {\n product {\n ... on PhysicalProductInterface {\n weight\n }\n sku\n color\n attribute_set_id\n categories\n {\n id\n position\n }\n country_of_manufacture\n created_at\n description\n gift_message_available\n id\n image\n image_label\n meta_description\n meta_keyword\n meta_title\n media_gallery_entries\n {\n disabled\n file\n id\n label\n media_type\n position\n types\n content\n {\n base64_encoded_data\n type\n name\n }\n video_content\n {\n media_type\n video_description\n video_metadata\n video_provider\n video_title\n video_url\n }\n }\n name\n new_from_date\n new_to_date\n options_container\n ... on CustomizableProductInterface {\n options\n {\n title\n required\n sort_order\n }\n }\n \n price {\n minimalPrice {\n amount {\n value\n currency\n }\n adjustments {\n amount {\n value\n currency\n }\n code\n description\n }\n }\n maximalPrice {\n amount {\n value\n currency\n }\n adjustments {\n amount {\n value\n currency\n }\n code\n description\n }\n }\n regularPrice {\n amount {\n value\n currency\n }\n adjustments {\n amount {\n value\n currency\n }\n code\n description\n }\n }\n }\n product_links\n {\n link_type\n linked_product_sku\n linked_product_type\n position\n sku\n }\n short_description\n sku\n small_image\n small_image_label\n special_from_date\n special_price\n special_to_date\n swatch_image\n tax_class_id\n thumbnail\n thumbnail_label\n tier_price\n tier_prices\n {\n customer_group_id\n percentage_value\n qty\n value\n website_id\n }\n type_id\n updated_at\n url_key\n url_path\n websites { id name code sort_order default_group_id is_default }\n\n\n }\n attributes {\n label\n code\n value_index\n }\n }\n }\n }\n }\n}\n","variables":null,"operationName":null}</stringProp> + <stringProp name="Argument.value">{"query":"{\n products(\n search: \"configurable\"\n filter: {price: {gteq: \"1\"} }\n ) {\n total_count\n items {\n attribute_set_id\n categories\n {\n id\n position\n }\n country_of_manufacture\n created_at\n description\n gift_message_available\n id\n image\n image_label\n meta_description\n meta_keyword\n meta_title\n media_gallery_entries\n {\n disabled\n file\n id\n label\n media_type\n position\n types\n content\n {\n base64_encoded_data\n type\n name\n }\n video_content\n {\n media_type\n video_description\n video_metadata\n video_provider\n video_title\n video_url\n }\n }\n name\n new_from_date\n new_to_date\n options_container\n ... on CustomizableProductInterface {\n options\n {\n title\n required\n sort_order\n }\n }\n \n price {\n minimalPrice {\n amount {\n value\n currency\n }\n adjustments {\n amount {\n value\n currency\n }\n code\n description\n }\n }\n maximalPrice {\n amount {\n value\n currency\n }\n adjustments {\n amount {\n value\n currency\n }\n code\n description\n }\n }\n regularPrice {\n amount {\n value\n currency\n }\n adjustments {\n amount {\n value\n currency\n }\n code\n description\n }\n }\n }\n product_links\n {\n link_type\n linked_product_sku\n linked_product_type\n position\n sku\n }\n short_description\n sku\n small_image\n small_image_label\n special_from_date\n special_price\n special_to_date\n swatch_image\n thumbnail\n thumbnail_label\n tier_price\n tier_prices\n {\n customer_group_id\n percentage_value\n qty\n value\n website_id\n }\n type_id\n updated_at\n url_key\n url_path\n websites { id name code sort_order default_group_id is_default }\n ... on PhysicalProductInterface {\n \t\t\tweight\n \t\t\t}\n ... on ConfigurableProduct {\n configurable_options {\n id\n attribute_id\n label\n position\n use_default\n attribute_code\n values {\n value_index\n label\n store_label\n default_label\n use_default_value\n }\n product_id\n }\n variants {\n product {\n ... on PhysicalProductInterface {\n weight\n }\n sku\n color\n attribute_set_id\n categories\n {\n id\n position\n }\n country_of_manufacture\n created_at\n description\n gift_message_available\n id\n image\n image_label\n meta_description\n meta_keyword\n meta_title\n media_gallery_entries\n {\n disabled\n file\n id\n label\n media_type\n position\n types\n content\n {\n base64_encoded_data\n type\n name\n }\n video_content\n {\n media_type\n video_description\n video_metadata\n video_provider\n video_title\n video_url\n }\n }\n name\n new_from_date\n new_to_date\n options_container\n ... on CustomizableProductInterface {\n options\n {\n title\n required\n sort_order\n }\n }\n \n price {\n minimalPrice {\n amount {\n value\n currency\n }\n adjustments {\n amount {\n value\n currency\n }\n code\n description\n }\n }\n maximalPrice {\n amount {\n value\n currency\n }\n adjustments {\n amount {\n value\n currency\n }\n code\n description\n }\n }\n regularPrice {\n amount {\n value\n currency\n }\n adjustments {\n amount {\n value\n currency\n }\n code\n description\n }\n }\n }\n product_links\n {\n link_type\n linked_product_sku\n linked_product_type\n position\n sku\n }\n short_description\n sku\n small_image\n small_image_label\n special_from_date\n special_price\n special_to_date\n swatch_image\n thumbnail\n thumbnail_label\n tier_price\n tier_prices\n {\n customer_group_id\n percentage_value\n qty\n value\n website_id\n }\n type_id\n updated_at\n url_key\n url_path\n websites { id name code sort_order default_group_id is_default }\n\n\n }\n attributes {\n label\n code\n value_index\n }\n }\n }\n }\n }\n}\n","variables":null,"operationName":null}</stringProp> <stringProp name="Argument.metadata">=</stringProp> </elementProp> </collectionProp> @@ -40119,7 +40119,7 @@ if (totalCount == null) { <collectionProp name="Arguments.arguments"> <elementProp name="" elementType="HTTPArgument"> <boolProp name="HTTPArgument.always_encode">false</boolProp> - <stringProp name="Argument.value">{"query":"{\n products(search: \"configurable\") {\n total_count\n items {\n attribute_set_id\n categories\n {\n id\n position\n }\n country_of_manufacture\n created_at\n description\n gift_message_available\n id\n image\n image_label\n meta_description\n meta_keyword\n meta_title\n media_gallery_entries\n {\n disabled\n file\n id\n label\n media_type\n position\n types\n content\n {\n base64_encoded_data\n type\n name\n }\n video_content\n {\n media_type\n video_description\n video_metadata\n video_provider\n video_title\n video_url\n }\n }\n name\n new_from_date\n new_to_date\n options_container\n ... on CustomizableProductInterface {\n options\n {\n title\n required\n sort_order\n }\n }\n \n price {\n minimalPrice {\n amount {\n value\n currency\n }\n adjustments {\n amount {\n value\n currency\n }\n code\n description\n }\n }\n maximalPrice {\n amount {\n value\n currency\n }\n adjustments {\n amount {\n value\n currency\n }\n code\n description\n }\n }\n regularPrice {\n amount {\n value\n currency\n }\n adjustments {\n amount {\n value\n currency\n }\n code\n description\n }\n }\n }\n product_links\n {\n link_type\n linked_product_sku\n linked_product_type\n position\n sku\n }\n short_description\n sku\n small_image\n small_image_label\n special_from_date\n special_price\n special_to_date\n swatch_image\n tax_class_id\n thumbnail\n thumbnail_label\n tier_price\n tier_prices\n {\n customer_group_id\n percentage_value\n qty\n value\n website_id\n }\n type_id\n updated_at\n url_key\n url_path\n websites { id name code sort_order default_group_id is_default }\n ... on PhysicalProductInterface {\n \t\t\tweight\n \t\t\t}\n ... on ConfigurableProduct {\n configurable_options {\n id\n attribute_id\n label\n position\n use_default\n attribute_code\n values {\n value_index\n label\n store_label\n default_label\n use_default_value\n }\n product_id\n }\n variants {\n product {\n ... on PhysicalProductInterface {\n weight\n }\n sku\n color\n attribute_set_id\n categories\n {\n id\n position\n }\n country_of_manufacture\n created_at\n description\n gift_message_available\n id\n image\n image_label\n meta_description\n meta_keyword\n meta_title\n media_gallery_entries\n {\n disabled\n file\n id\n label\n media_type\n position\n types\n content\n {\n base64_encoded_data\n type\n name\n }\n video_content\n {\n media_type\n video_description\n video_metadata\n video_provider\n video_title\n video_url\n }\n }\n name\n new_from_date\n new_to_date\n options_container\n ... on CustomizableProductInterface {\n options\n {\n title\n required\n sort_order\n }\n }\n \n price {\n minimalPrice {\n amount {\n value\n currency\n }\n adjustments {\n amount {\n value\n currency\n }\n code\n description\n }\n }\n maximalPrice {\n amount {\n value\n currency\n }\n adjustments {\n amount {\n value\n currency\n }\n code\n description\n }\n }\n regularPrice {\n amount {\n value\n currency\n }\n adjustments {\n amount {\n value\n currency\n }\n code\n description\n }\n }\n }\n product_links\n {\n link_type\n linked_product_sku\n linked_product_type\n position\n sku\n }\n short_description\n sku\n small_image\n small_image_label\n special_from_date\n special_price\n special_to_date\n swatch_image\n tax_class_id\n thumbnail\n thumbnail_label\n tier_price\n tier_prices\n {\n customer_group_id\n percentage_value\n qty\n value\n website_id\n }\n type_id\n updated_at\n url_key\n url_path\n websites { id name code sort_order default_group_id is_default }\n\n\n }\n attributes {\n label\n code\n value_index\n }\n }\n }\n }\n }\n}\n","variables":null,"operationName":null}</stringProp> + <stringProp name="Argument.value">{"query":"{\n products(search: \"configurable\") {\n total_count\n items {\n attribute_set_id\n categories\n {\n id\n position\n }\n country_of_manufacture\n created_at\n description\n gift_message_available\n id\n image\n image_label\n meta_description\n meta_keyword\n meta_title\n media_gallery_entries\n {\n disabled\n file\n id\n label\n media_type\n position\n types\n content\n {\n base64_encoded_data\n type\n name\n }\n video_content\n {\n media_type\n video_description\n video_metadata\n video_provider\n video_title\n video_url\n }\n }\n name\n new_from_date\n new_to_date\n options_container\n ... on CustomizableProductInterface {\n options\n {\n title\n required\n sort_order\n }\n }\n \n price {\n minimalPrice {\n amount {\n value\n currency\n }\n adjustments {\n amount {\n value\n currency\n }\n code\n description\n }\n }\n maximalPrice {\n amount {\n value\n currency\n }\n adjustments {\n amount {\n value\n currency\n }\n code\n description\n }\n }\n regularPrice {\n amount {\n value\n currency\n }\n adjustments {\n amount {\n value\n currency\n }\n code\n description\n }\n }\n }\n product_links\n {\n link_type\n linked_product_sku\n linked_product_type\n position\n sku\n }\n short_description\n sku\n small_image\n small_image_label\n special_from_date\n special_price\n special_to_date\n swatch_image\n thumbnail\n thumbnail_label\n tier_price\n tier_prices\n {\n customer_group_id\n percentage_value\n qty\n value\n website_id\n }\n type_id\n updated_at\n url_key\n url_path\n websites { id name code sort_order default_group_id is_default }\n ... on PhysicalProductInterface {\n \t\t\tweight\n \t\t\t}\n ... on ConfigurableProduct {\n configurable_options {\n id\n attribute_id\n label\n position\n use_default\n attribute_code\n values {\n value_index\n label\n store_label\n default_label\n use_default_value\n }\n product_id\n }\n variants {\n product {\n ... on PhysicalProductInterface {\n weight\n }\n sku\n color\n attribute_set_id\n categories\n {\n id\n position\n }\n country_of_manufacture\n created_at\n description\n gift_message_available\n id\n image\n image_label\n meta_description\n meta_keyword\n meta_title\n media_gallery_entries\n {\n disabled\n file\n id\n label\n media_type\n position\n types\n content\n {\n base64_encoded_data\n type\n name\n }\n video_content\n {\n media_type\n video_description\n video_metadata\n video_provider\n video_title\n video_url\n }\n }\n name\n new_from_date\n new_to_date\n options_container\n ... on CustomizableProductInterface {\n options\n {\n title\n required\n sort_order\n }\n }\n \n price {\n minimalPrice {\n amount {\n value\n currency\n }\n adjustments {\n amount {\n value\n currency\n }\n code\n description\n }\n }\n maximalPrice {\n amount {\n value\n currency\n }\n adjustments {\n amount {\n value\n currency\n }\n code\n description\n }\n }\n regularPrice {\n amount {\n value\n currency\n }\n adjustments {\n amount {\n value\n currency\n }\n code\n description\n }\n }\n }\n product_links\n {\n link_type\n linked_product_sku\n linked_product_type\n position\n sku\n }\n short_description\n sku\n small_image\n small_image_label\n special_from_date\n special_price\n special_to_date\n swatch_image\n thumbnail\n thumbnail_label\n tier_price\n tier_prices\n {\n customer_group_id\n percentage_value\n qty\n value\n website_id\n }\n type_id\n updated_at\n url_key\n url_path\n websites { id name code sort_order default_group_id is_default }\n\n\n }\n attributes {\n label\n code\n value_index\n }\n }\n }\n }\n }\n}\n","variables":null,"operationName":null}</stringProp> <stringProp name="Argument.metadata">=</stringProp> </elementProp> </collectionProp> @@ -40179,7 +40179,7 @@ if (totalCount == null) { <collectionProp name="Arguments.arguments"> <elementProp name="" elementType="HTTPArgument"> <boolProp name="HTTPArgument.always_encode">false</boolProp> - <stringProp name="Argument.value">{"query":"{\n products(search: \"color\") {\n filters {\n name\n filter_items_count\n request_var\n filter_items {\n label\n value_string\n items_count\n ... on SwatchLayerFilterItemInterface {\n swatch_data {\n type\n value\n }\n }\n }\n }\n total_count\n items {\n attribute_set_id\n categories\n {\n id\n position\n }\n country_of_manufacture\n created_at\n description\n gift_message_available\n id\n image\n image_label\n meta_description\n meta_keyword\n meta_title\n media_gallery_entries\n {\n disabled\n file\n id\n label\n media_type\n position\n types\n content\n {\n base64_encoded_data\n type\n name\n }\n video_content\n {\n media_type\n video_description\n video_metadata\n video_provider\n video_title\n video_url\n }\n }\n name\n new_from_date\n new_to_date\n options_container\n ... on CustomizableProductInterface {\n options\n {\n title\n required\n sort_order\n }\n }\n \n price {\n minimalPrice {\n amount {\n value\n currency\n }\n adjustments {\n amount {\n value\n currency\n }\n code\n description\n }\n }\n maximalPrice {\n amount {\n value\n currency\n }\n adjustments {\n amount {\n value\n currency\n }\n code\n description\n }\n }\n regularPrice {\n amount {\n value\n currency\n }\n adjustments {\n amount {\n value\n currency\n }\n code\n description\n }\n }\n }\n product_links\n {\n link_type\n linked_product_sku\n linked_product_type\n position\n sku\n }\n short_description\n sku\n small_image\n small_image_label\n special_from_date\n special_price\n special_to_date\n swatch_image\n tax_class_id\n thumbnail\n thumbnail_label\n tier_price\n tier_prices\n {\n customer_group_id\n percentage_value\n qty\n value\n website_id\n }\n type_id\n updated_at\n url_key\n url_path\n websites { id name code sort_order default_group_id is_default }\n ... on PhysicalProductInterface {\n weight\n }\n ... on ConfigurableProduct {\n configurable_options {\n id\n attribute_id\n label\n position\n use_default\n attribute_code\n values {\n value_index\n label\n store_label\n default_label\n use_default_value\n }\n product_id\n }\n variants {\n product {\n ... on PhysicalProductInterface {\n weight\n }\n sku\n color\n attribute_set_id\n categories\n {\n id\n position\n }\n country_of_manufacture\n created_at\n description\n gift_message_available\n id\n image\n image_label\n meta_description\n meta_keyword\n meta_title\n media_gallery_entries\n {\n disabled\n file\n id\n label\n media_type\n position\n types\n content\n {\n base64_encoded_data\n type\n name\n }\n video_content\n {\n media_type\n video_description\n video_metadata\n video_provider\n video_title\n video_url\n }\n }\n name\n new_from_date\n new_to_date\n options_container\n ... on CustomizableProductInterface {\n options\n {\n title\n required\n sort_order\n }\n }\n \n price {\n minimalPrice {\n amount {\n value\n currency\n }\n adjustments {\n amount {\n value\n currency\n }\n code\n description\n }\n }\n maximalPrice {\n amount {\n value\n currency\n }\n adjustments {\n amount {\n value\n currency\n }\n code\n description\n }\n }\n regularPrice {\n amount {\n value\n currency\n }\n adjustments {\n amount {\n value\n currency\n }\n code\n description\n }\n }\n }\n product_links\n {\n link_type\n linked_product_sku\n linked_product_type\n position\n sku\n }\n short_description\n sku\n small_image\n small_image_label\n special_from_date\n special_price\n special_to_date\n swatch_image\n tax_class_id\n thumbnail\n thumbnail_label\n tier_price\n tier_prices\n {\n customer_group_id\n percentage_value\n qty\n value\n website_id\n }\n type_id\n updated_at\n url_key\n url_path\n websites { id name code sort_order default_group_id is_default }\n\n\n }\n attributes {\n label\n code\n value_index\n }\n }\n }\n }\n }\n}","variables":null,"operationName":null}</stringProp> + <stringProp name="Argument.value">{"query":"{\n products(search: \"color\") {\n filters {\n name\n filter_items_count\n request_var\n filter_items {\n label\n value_string\n items_count\n ... on SwatchLayerFilterItemInterface {\n swatch_data {\n type\n value\n }\n }\n }\n }\n total_count\n items {\n attribute_set_id\n categories\n {\n id\n position\n }\n country_of_manufacture\n created_at\n description\n gift_message_available\n id\n image\n image_label\n meta_description\n meta_keyword\n meta_title\n media_gallery_entries\n {\n disabled\n file\n id\n label\n media_type\n position\n types\n content\n {\n base64_encoded_data\n type\n name\n }\n video_content\n {\n media_type\n video_description\n video_metadata\n video_provider\n video_title\n video_url\n }\n }\n name\n new_from_date\n new_to_date\n options_container\n ... on CustomizableProductInterface {\n options\n {\n title\n required\n sort_order\n }\n }\n \n price {\n minimalPrice {\n amount {\n value\n currency\n }\n adjustments {\n amount {\n value\n currency\n }\n code\n description\n }\n }\n maximalPrice {\n amount {\n value\n currency\n }\n adjustments {\n amount {\n value\n currency\n }\n code\n description\n }\n }\n regularPrice {\n amount {\n value\n currency\n }\n adjustments {\n amount {\n value\n currency\n }\n code\n description\n }\n }\n }\n product_links\n {\n link_type\n linked_product_sku\n linked_product_type\n position\n sku\n }\n short_description\n sku\n small_image\n small_image_label\n special_from_date\n special_price\n special_to_date\n swatch_image\n thumbnail\n thumbnail_label\n tier_price\n tier_prices\n {\n customer_group_id\n percentage_value\n qty\n value\n website_id\n }\n type_id\n updated_at\n url_key\n url_path\n websites { id name code sort_order default_group_id is_default }\n ... on PhysicalProductInterface {\n weight\n }\n ... on ConfigurableProduct {\n configurable_options {\n id\n attribute_id\n label\n position\n use_default\n attribute_code\n values {\n value_index\n label\n store_label\n default_label\n use_default_value\n }\n product_id\n }\n variants {\n product {\n ... on PhysicalProductInterface {\n weight\n }\n sku\n color\n attribute_set_id\n categories\n {\n id\n position\n }\n country_of_manufacture\n created_at\n description\n gift_message_available\n id\n image\n image_label\n meta_description\n meta_keyword\n meta_title\n media_gallery_entries\n {\n disabled\n file\n id\n label\n media_type\n position\n types\n content\n {\n base64_encoded_data\n type\n name\n }\n video_content\n {\n media_type\n video_description\n video_metadata\n video_provider\n video_title\n video_url\n }\n }\n name\n new_from_date\n new_to_date\n options_container\n ... on CustomizableProductInterface {\n options\n {\n title\n required\n sort_order\n }\n }\n \n price {\n minimalPrice {\n amount {\n value\n currency\n }\n adjustments {\n amount {\n value\n currency\n }\n code\n description\n }\n }\n maximalPrice {\n amount {\n value\n currency\n }\n adjustments {\n amount {\n value\n currency\n }\n code\n description\n }\n }\n regularPrice {\n amount {\n value\n currency\n }\n adjustments {\n amount {\n value\n currency\n }\n code\n description\n }\n }\n }\n product_links\n {\n link_type\n linked_product_sku\n linked_product_type\n position\n sku\n }\n short_description\n sku\n small_image\n small_image_label\n special_from_date\n special_price\n special_to_date\n swatch_image\n thumbnail\n thumbnail_label\n tier_price\n tier_prices\n {\n customer_group_id\n percentage_value\n qty\n value\n website_id\n }\n type_id\n updated_at\n url_key\n url_path\n websites { id name code sort_order default_group_id is_default }\n\n\n }\n attributes {\n label\n code\n value_index\n }\n }\n }\n }\n }\n}","variables":null,"operationName":null}</stringProp> <stringProp name="Argument.metadata">=</stringProp> </elementProp> </collectionProp> @@ -40239,7 +40239,7 @@ if (totalCount == null) { <collectionProp name="Arguments.arguments"> <elementProp name="" elementType="HTTPArgument"> <boolProp name="HTTPArgument.always_encode">false</boolProp> - <stringProp name="Argument.value">{"query":"{\nproducts(filter: {sku: {eq:\"${bundle_product_sku}\"} }) {\n total_count\n items {\n ... on PhysicalProductInterface {\n weight\n }\n attribute_set_id\n categories\n {\n id\n position\n }\n country_of_manufacture\n created_at\n description\n gift_message_available\n id\n image\n image_label\n meta_description\n meta_keyword\n meta_title\n media_gallery_entries\n {\n disabled\n file\n id\n label\n media_type\n position\n types\n content\n {\n base64_encoded_data\n type\n name\n }\n video_content\n {\n media_type\n video_description\n video_metadata\n video_provider\n video_title\n video_url\n }\n }\n name\n new_from_date\n new_to_date\n options_container\n ... on CustomizableProductInterface {\n options\n {\n title\n required\n sort_order\n }\n }\n \n price {\n minimalPrice {\n amount {\n value\n currency\n }\n adjustments {\n amount {\n value\n currency\n }\n code\n description\n }\n }\n maximalPrice {\n amount {\n value\n currency\n }\n adjustments {\n amount {\n value\n currency\n }\n code\n description\n }\n }\n regularPrice {\n amount {\n value\n currency\n }\n adjustments {\n amount {\n value\n currency\n }\n code\n description\n }\n }\n }\n product_links\n {\n link_type\n linked_product_sku\n linked_product_type\n position\n sku\n }\n short_description\n sku\n small_image\n small_image_label\n special_from_date\n special_price\n special_to_date\n swatch_image\n tax_class_id\n thumbnail\n thumbnail_label\n tier_price\n tier_prices\n {\n customer_group_id\n percentage_value\n qty\n value\n website_id\n }\n type_id\n updated_at\n url_key\n url_path\n websites { id name code sort_order default_group_id is_default }\n ... on BundleProduct {\n weight\n price_view\n dynamic_price\n dynamic_sku\n ship_bundle_items\n dynamic_weight\n items {\n option_id\n title\n required\n type\n position\n sku\n options {\n id\n qty\n position\n is_default\n price\n price_type\n can_change_quantity\n product {\n attribute_set_id\n categories\n {\n id\n position\n }\n country_of_manufacture\n created_at\n description\n gift_message_available\n id\n image\n image_label\n meta_description\n meta_keyword\n meta_title\n media_gallery_entries\n {\n disabled\n file\n id\n label\n media_type\n position\n types\n content\n {\n base64_encoded_data\n type\n name\n }\n video_content\n {\n media_type\n video_description\n video_metadata\n video_provider\n video_title\n video_url\n }\n }\n name\n new_from_date\n new_to_date\n options_container\n ... on CustomizableProductInterface {\n options\n {\n title\n required\n sort_order\n }\n }\n \n price {\n minimalPrice {\n amount {\n value\n currency\n }\n adjustments {\n amount {\n value\n currency\n }\n code\n description\n }\n }\n maximalPrice {\n amount {\n value\n currency\n }\n adjustments {\n amount {\n value\n currency\n }\n code\n description\n }\n }\n regularPrice {\n amount {\n value\n currency\n }\n adjustments {\n amount {\n value\n currency\n }\n code\n description\n }\n }\n }\n product_links\n {\n link_type\n linked_product_sku\n linked_product_type\n position\n sku\n }\n short_description\n sku\n small_image\n small_image_label\n special_from_date\n special_price\n special_to_date\n swatch_image\n tax_class_id\n thumbnail\n thumbnail_label\n tier_price\n tier_prices\n {\n customer_group_id\n percentage_value\n qty\n value\n website_id\n }\n type_id\n updated_at\n url_key\n url_path\n websites { id name code sort_order default_group_id is_default }\n }\n }\n }\n }\n }\n }\n}\n","variables":null,"operationName":null}</stringProp> + <stringProp name="Argument.value">{"query":"{\nproducts(filter: {sku: {eq:\"${bundle_product_sku}\"} }) {\n total_count\n items {\n ... on PhysicalProductInterface {\n weight\n }\n attribute_set_id\n categories\n {\n id\n position\n }\n country_of_manufacture\n created_at\n description\n gift_message_available\n id\n image\n image_label\n meta_description\n meta_keyword\n meta_title\n media_gallery_entries\n {\n disabled\n file\n id\n label\n media_type\n position\n types\n content\n {\n base64_encoded_data\n type\n name\n }\n video_content\n {\n media_type\n video_description\n video_metadata\n video_provider\n video_title\n video_url\n }\n }\n name\n new_from_date\n new_to_date\n options_container\n ... on CustomizableProductInterface {\n options\n {\n title\n required\n sort_order\n }\n }\n \n price {\n minimalPrice {\n amount {\n value\n currency\n }\n adjustments {\n amount {\n value\n currency\n }\n code\n description\n }\n }\n maximalPrice {\n amount {\n value\n currency\n }\n adjustments {\n amount {\n value\n currency\n }\n code\n description\n }\n }\n regularPrice {\n amount {\n value\n currency\n }\n adjustments {\n amount {\n value\n currency\n }\n code\n description\n }\n }\n }\n product_links\n {\n link_type\n linked_product_sku\n linked_product_type\n position\n sku\n }\n short_description\n sku\n small_image\n small_image_label\n special_from_date\n special_price\n special_to_date\n swatch_image\n thumbnail\n thumbnail_label\n tier_price\n tier_prices\n {\n customer_group_id\n percentage_value\n qty\n value\n website_id\n }\n type_id\n updated_at\n url_key\n url_path\n websites { id name code sort_order default_group_id is_default }\n ... on BundleProduct {\n weight\n price_view\n dynamic_price\n dynamic_sku\n ship_bundle_items\n dynamic_weight\n items {\n option_id\n title\n required\n type\n position\n sku\n options {\n id\n qty\n position\n is_default\n price\n price_type\n can_change_quantity\n product {\n attribute_set_id\n categories\n {\n id\n position\n }\n country_of_manufacture\n created_at\n description\n gift_message_available\n id\n image\n image_label\n meta_description\n meta_keyword\n meta_title\n media_gallery_entries\n {\n disabled\n file\n id\n label\n media_type\n position\n types\n content\n {\n base64_encoded_data\n type\n name\n }\n video_content\n {\n media_type\n video_description\n video_metadata\n video_provider\n video_title\n video_url\n }\n }\n name\n new_from_date\n new_to_date\n options_container\n ... on CustomizableProductInterface {\n options\n {\n title\n required\n sort_order\n }\n }\n \n price {\n minimalPrice {\n amount {\n value\n currency\n }\n adjustments {\n amount {\n value\n currency\n }\n code\n description\n }\n }\n maximalPrice {\n amount {\n value\n currency\n }\n adjustments {\n amount {\n value\n currency\n }\n code\n description\n }\n }\n regularPrice {\n amount {\n value\n currency\n }\n adjustments {\n amount {\n value\n currency\n }\n code\n description\n }\n }\n }\n product_links\n {\n link_type\n linked_product_sku\n linked_product_type\n position\n sku\n }\n short_description\n sku\n small_image\n small_image_label\n special_from_date\n special_price\n special_to_date\n swatch_image\n thumbnail\n thumbnail_label\n tier_price\n tier_prices\n {\n customer_group_id\n percentage_value\n qty\n value\n website_id\n }\n type_id\n updated_at\n url_key\n url_path\n websites { id name code sort_order default_group_id is_default }\n }\n }\n }\n }\n }\n }\n}\n","variables":null,"operationName":null}</stringProp> <stringProp name="Argument.metadata">=</stringProp> </elementProp> </collectionProp> @@ -40308,7 +40308,7 @@ if (totalCount == null) { <collectionProp name="Arguments.arguments"> <elementProp name="" elementType="HTTPArgument"> <boolProp name="HTTPArgument.always_encode">false</boolProp> - <stringProp name="Argument.value">{"query":"{\n products(filter: {sku: { eq: \"${downloadable_product_sku}\" } })\n {\n total_count\n items {\n attribute_set_id\n categories\n {\n id\n position\n }\n country_of_manufacture\n created_at\n description\n gift_message_available\n id\n image\n image_label\n meta_description\n meta_keyword\n meta_title\n media_gallery_entries\n {\n disabled\n file\n id\n label\n media_type\n position\n types\n content\n {\n base64_encoded_data\n type\n name\n }\n video_content\n {\n media_type\n video_description\n video_metadata\n video_provider\n video_title\n video_url\n }\n }\n name\n new_from_date\n new_to_date\n options_container\n ... on CustomizableProductInterface {\n options\n {\n title\n required\n sort_order\n }\n }\n \n price {\n minimalPrice {\n amount {\n value\n currency\n }\n adjustments {\n amount {\n value\n currency\n }\n code\n description\n }\n }\n maximalPrice {\n amount {\n value\n currency\n }\n adjustments {\n amount {\n value\n currency\n }\n code\n description\n }\n }\n regularPrice {\n amount {\n value\n currency\n }\n adjustments {\n amount {\n value\n currency\n }\n code\n description\n }\n }\n }\n product_links\n {\n link_type\n linked_product_sku\n linked_product_type\n position\n sku\n }\n short_description\n sku\n small_image\n small_image_label\n special_from_date\n special_price\n special_to_date\n swatch_image\n tax_class_id\n thumbnail\n thumbnail_label\n tier_price\n tier_prices\n {\n customer_group_id\n percentage_value\n qty\n value\n website_id\n }\n type_id\n updated_at\n url_key\n url_path\n websites { id name code sort_order default_group_id is_default }\n ... on PhysicalProductInterface {\n \t\t\tweight\n \t\t }\n ... on DownloadableProduct {\n links_purchased_separately\n links_title\n downloadable_product_samples {\n id\n title\n sort_order\n sample_type\n sample_file\n sample_url\n }\n downloadable_product_links {\n id\n title\n sort_order\n is_shareable\n price\n number_of_downloads\n link_type\n sample_type\n sample_file\n sample_url\n }\n }\n }\n }\n}\n","variables":null,"operationName":null}</stringProp> + <stringProp name="Argument.value">{"query":"{\n products(filter: {sku: { eq: \"${downloadable_product_sku}\" } })\n {\n total_count\n items {\n attribute_set_id\n categories\n {\n id\n position\n }\n country_of_manufacture\n created_at\n description\n gift_message_available\n id\n image\n image_label\n meta_description\n meta_keyword\n meta_title\n media_gallery_entries\n {\n disabled\n file\n id\n label\n media_type\n position\n types\n content\n {\n base64_encoded_data\n type\n name\n }\n video_content\n {\n media_type\n video_description\n video_metadata\n video_provider\n video_title\n video_url\n }\n }\n name\n new_from_date\n new_to_date\n options_container\n ... on CustomizableProductInterface {\n options\n {\n title\n required\n sort_order\n }\n }\n \n price {\n minimalPrice {\n amount {\n value\n currency\n }\n adjustments {\n amount {\n value\n currency\n }\n code\n description\n }\n }\n maximalPrice {\n amount {\n value\n currency\n }\n adjustments {\n amount {\n value\n currency\n }\n code\n description\n }\n }\n regularPrice {\n amount {\n value\n currency\n }\n adjustments {\n amount {\n value\n currency\n }\n code\n description\n }\n }\n }\n product_links\n {\n link_type\n linked_product_sku\n linked_product_type\n position\n sku\n }\n short_description\n sku\n small_image\n small_image_label\n special_from_date\n special_price\n special_to_date\n swatch_image\n thumbnail\n thumbnail_label\n tier_price\n tier_prices\n {\n customer_group_id\n percentage_value\n qty\n value\n website_id\n }\n type_id\n updated_at\n url_key\n url_path\n websites { id name code sort_order default_group_id is_default }\n ... on PhysicalProductInterface {\n \t\t\tweight\n \t\t }\n ... on DownloadableProduct {\n links_purchased_separately\n links_title\n downloadable_product_samples {\n id\n title\n sort_order\n sample_type\n sample_file\n sample_url\n }\n downloadable_product_links {\n id\n title\n sort_order\n is_shareable\n price\n number_of_downloads\n link_type\n sample_type\n sample_file\n sample_url\n }\n }\n }\n }\n}\n","variables":null,"operationName":null}</stringProp> <stringProp name="Argument.metadata">=</stringProp> </elementProp> </collectionProp> @@ -40395,7 +40395,7 @@ if (totalCount == null) { <collectionProp name="Arguments.arguments"> <elementProp name="" elementType="HTTPArgument"> <boolProp name="HTTPArgument.always_encode">false</boolProp> - <stringProp name="Argument.value">{"query":"{\n products(filter: {sku: { eq: \"${virtual_product_sku}\" } })\n {\n total_count\n items {\n attribute_set_id\n categories\n {\n id\n position\n }\n country_of_manufacture\n created_at\n description\n gift_message_available\n id\n image\n image_label\n meta_description\n meta_keyword\n meta_title\n media_gallery_entries\n {\n disabled\n file\n id\n label\n media_type\n position\n types\n content\n {\n base64_encoded_data\n type\n name\n }\n video_content\n {\n media_type\n video_description\n video_metadata\n video_provider\n video_title\n video_url\n }\n }\n name\n new_from_date\n new_to_date\n options_container\n ... on CustomizableProductInterface {\n options\n {\n title\n required\n sort_order\n }\n }\n \n price {\n minimalPrice {\n amount {\n value\n currency\n }\n adjustments {\n amount {\n value\n currency\n }\n code\n description\n }\n }\n maximalPrice {\n amount {\n value\n currency\n }\n adjustments {\n amount {\n value\n currency\n }\n code\n description\n }\n }\n regularPrice {\n amount {\n value\n currency\n }\n adjustments {\n amount {\n value\n currency\n }\n code\n description\n }\n }\n }\n product_links\n {\n link_type\n linked_product_sku\n linked_product_type\n position\n sku\n }\n short_description\n sku\n small_image\n small_image_label\n special_from_date\n special_price\n special_to_date\n swatch_image\n tax_class_id\n thumbnail\n thumbnail_label\n tier_price\n tier_prices\n {\n customer_group_id\n percentage_value\n qty\n value\n website_id\n }\n type_id\n updated_at\n url_key\n url_path\n websites { id name code sort_order default_group_id is_default }\n ... on PhysicalProductInterface {\n \t\t\tweight\n \t\t }\n }\n }\n}\n","variables":null,"operationName":null}</stringProp> + <stringProp name="Argument.value">{"query":"{\n products(filter: {sku: { eq: \"${virtual_product_sku}\" } })\n {\n total_count\n items {\n attribute_set_id\n categories\n {\n id\n position\n }\n country_of_manufacture\n created_at\n description\n gift_message_available\n id\n image\n image_label\n meta_description\n meta_keyword\n meta_title\n media_gallery_entries\n {\n disabled\n file\n id\n label\n media_type\n position\n types\n content\n {\n base64_encoded_data\n type\n name\n }\n video_content\n {\n media_type\n video_description\n video_metadata\n video_provider\n video_title\n video_url\n }\n }\n name\n new_from_date\n new_to_date\n options_container\n ... on CustomizableProductInterface {\n options\n {\n title\n required\n sort_order\n }\n }\n \n price {\n minimalPrice {\n amount {\n value\n currency\n }\n adjustments {\n amount {\n value\n currency\n }\n code\n description\n }\n }\n maximalPrice {\n amount {\n value\n currency\n }\n adjustments {\n amount {\n value\n currency\n }\n code\n description\n }\n }\n regularPrice {\n amount {\n value\n currency\n }\n adjustments {\n amount {\n value\n currency\n }\n code\n description\n }\n }\n }\n product_links\n {\n link_type\n linked_product_sku\n linked_product_type\n position\n sku\n }\n short_description\n sku\n small_image\n small_image_label\n special_from_date\n special_price\n special_to_date\n swatch_image\n thumbnail\n thumbnail_label\n tier_price\n tier_prices\n {\n customer_group_id\n percentage_value\n qty\n value\n website_id\n }\n type_id\n updated_at\n url_key\n url_path\n websites { id name code sort_order default_group_id is_default }\n ... on PhysicalProductInterface {\n \t\t\tweight\n \t\t }\n }\n }\n}\n","variables":null,"operationName":null}</stringProp> <stringProp name="Argument.metadata">=</stringProp> </elementProp> </collectionProp> @@ -40462,7 +40462,7 @@ if (totalCount == null) { <collectionProp name="Arguments.arguments"> <elementProp name="" elementType="HTTPArgument"> <boolProp name="HTTPArgument.always_encode">false</boolProp> - <stringProp name="Argument.value">{"query":"{\nproducts(filter: {sku: {eq:\"${grouped_product_sku}\"} }) {\n total_count\n items {\n attribute_set_id\n categories\n {\n id\n position\n }\n country_of_manufacture\n created_at\n description\n gift_message_available\n id\n image\n image_label\n meta_description\n meta_keyword\n meta_title\n media_gallery_entries\n {\n disabled\n file\n id\n label\n media_type\n position\n types\n content\n {\n base64_encoded_data\n type\n name\n }\n video_content\n {\n media_type\n video_description\n video_metadata\n video_provider\n video_title\n video_url\n }\n }\n name\n new_from_date\n new_to_date\n options_container\n ... on CustomizableProductInterface {\n options\n {\n title\n required\n sort_order\n }\n }\n \n price {\n minimalPrice {\n amount {\n value\n currency\n }\n adjustments {\n amount {\n value\n currency\n }\n code\n description\n }\n }\n maximalPrice {\n amount {\n value\n currency\n }\n adjustments {\n amount {\n value\n currency\n }\n code\n description\n }\n }\n regularPrice {\n amount {\n value\n currency\n }\n adjustments {\n amount {\n value\n currency\n }\n code\n description\n }\n }\n }\n product_links\n {\n link_type\n linked_product_sku\n linked_product_type\n position\n sku\n }\n short_description\n sku\n small_image\n small_image_label\n special_from_date\n special_price\n special_to_date\n swatch_image\n tax_class_id\n thumbnail\n thumbnail_label\n tier_price\n tier_prices\n {\n customer_group_id\n percentage_value\n qty\n value\n website_id\n }\n type_id\n updated_at\n url_key\n url_path\n websites { id name code sort_order default_group_id is_default }\n ... on GroupedProduct {\n weight\n items {\n qty\n position\n product {\n attribute_set_id\n categories\n {\n id\n position\n }\n country_of_manufacture\n created_at\n description\n gift_message_available\n id\n image\n image_label\n meta_description\n meta_keyword\n meta_title\n media_gallery_entries\n {\n disabled\n file\n id\n label\n media_type\n position\n types\n content\n {\n base64_encoded_data\n type\n name\n }\n video_content\n {\n media_type\n video_description\n video_metadata\n video_provider\n video_title\n video_url\n }\n }\n name\n new_from_date\n new_to_date\n options_container\n ... on CustomizableProductInterface {\n options\n {\n title\n required\n sort_order\n }\n }\n \n price {\n minimalPrice {\n amount {\n value\n currency\n }\n adjustments {\n amount {\n value\n currency\n }\n code\n description\n }\n }\n maximalPrice {\n amount {\n value\n currency\n }\n adjustments {\n amount {\n value\n currency\n }\n code\n description\n }\n }\n regularPrice {\n amount {\n value\n currency\n }\n adjustments {\n amount {\n value\n currency\n }\n code\n description\n }\n }\n }\n product_links\n {\n link_type\n linked_product_sku\n linked_product_type\n position\n sku\n }\n short_description\n sku\n small_image\n small_image_label\n special_from_date\n special_price\n special_to_date\n swatch_image\n tax_class_id\n thumbnail\n thumbnail_label\n tier_price\n tier_prices\n {\n customer_group_id\n percentage_value\n qty\n value\n website_id\n }\n type_id\n updated_at\n url_key\n url_path\n websites { id name code sort_order default_group_id is_default }\n }\n }\n }\n }\n }\n}\n","variables":null,"operationName":null}</stringProp> + <stringProp name="Argument.value">{"query":"{\nproducts(filter: {sku: {eq:\"${grouped_product_sku}\"} }) {\n total_count\n items {\n attribute_set_id\n categories\n {\n id\n position\n }\n country_of_manufacture\n created_at\n description\n gift_message_available\n id\n image\n image_label\n meta_description\n meta_keyword\n meta_title\n media_gallery_entries\n {\n disabled\n file\n id\n label\n media_type\n position\n types\n content\n {\n base64_encoded_data\n type\n name\n }\n video_content\n {\n media_type\n video_description\n video_metadata\n video_provider\n video_title\n video_url\n }\n }\n name\n new_from_date\n new_to_date\n options_container\n ... on CustomizableProductInterface {\n options\n {\n title\n required\n sort_order\n }\n }\n \n price {\n minimalPrice {\n amount {\n value\n currency\n }\n adjustments {\n amount {\n value\n currency\n }\n code\n description\n }\n }\n maximalPrice {\n amount {\n value\n currency\n }\n adjustments {\n amount {\n value\n currency\n }\n code\n description\n }\n }\n regularPrice {\n amount {\n value\n currency\n }\n adjustments {\n amount {\n value\n currency\n }\n code\n description\n }\n }\n }\n product_links\n {\n link_type\n linked_product_sku\n linked_product_type\n position\n sku\n }\n short_description\n sku\n small_image\n small_image_label\n special_from_date\n special_price\n special_to_date\n swatch_image\n thumbnail\n thumbnail_label\n tier_price\n tier_prices\n {\n customer_group_id\n percentage_value\n qty\n value\n website_id\n }\n type_id\n updated_at\n url_key\n url_path\n websites { id name code sort_order default_group_id is_default }\n ... on GroupedProduct {\n weight\n items {\n qty\n position\n product {\n attribute_set_id\n categories\n {\n id\n position\n }\n country_of_manufacture\n created_at\n description\n gift_message_available\n id\n image\n image_label\n meta_description\n meta_keyword\n meta_title\n media_gallery_entries\n {\n disabled\n file\n id\n label\n media_type\n position\n types\n content\n {\n base64_encoded_data\n type\n name\n }\n video_content\n {\n media_type\n video_description\n video_metadata\n video_provider\n video_title\n video_url\n }\n }\n name\n new_from_date\n new_to_date\n options_container\n ... on CustomizableProductInterface {\n options\n {\n title\n required\n sort_order\n }\n }\n \n price {\n minimalPrice {\n amount {\n value\n currency\n }\n adjustments {\n amount {\n value\n currency\n }\n code\n description\n }\n }\n maximalPrice {\n amount {\n value\n currency\n }\n adjustments {\n amount {\n value\n currency\n }\n code\n description\n }\n }\n regularPrice {\n amount {\n value\n currency\n }\n adjustments {\n amount {\n value\n currency\n }\n code\n description\n }\n }\n }\n product_links\n {\n link_type\n linked_product_sku\n linked_product_type\n position\n sku\n }\n short_description\n sku\n small_image\n small_image_label\n special_from_date\n special_price\n special_to_date\n swatch_image\n thumbnail\n thumbnail_label\n tier_price\n tier_prices\n {\n customer_group_id\n percentage_value\n qty\n value\n website_id\n }\n type_id\n updated_at\n url_key\n url_path\n websites { id name code sort_order default_group_id is_default }\n }\n }\n }\n }\n }\n}\n","variables":null,"operationName":null}</stringProp> <stringProp name="Argument.metadata">=</stringProp> </elementProp> </collectionProp> diff --git a/setup/src/Magento/Setup/Fixtures/FixtureModel.php b/setup/src/Magento/Setup/Fixtures/FixtureModel.php index 104c9cb3432..99237e48748 100644 --- a/setup/src/Magento/Setup/Fixtures/FixtureModel.php +++ b/setup/src/Magento/Setup/Fixtures/FixtureModel.php @@ -105,7 +105,7 @@ public function reindex(OutputInterface $output) */ public function loadFixtures() { - $files = glob(__DIR__ . DIRECTORY_SEPARATOR . self::FIXTURE_PATTERN); + $files = glob(__DIR__ . DIRECTORY_SEPARATOR . self::FIXTURE_PATTERN, GLOB_NOSORT); foreach ($files as $file) { $file = basename($file, '.php');